update AutoExposure

This commit is contained in:
StarBeats 2025-07-21 18:10:06 +08:00
parent d694bdc43b
commit 2d834cf06a
13 changed files with 629 additions and 262 deletions

View File

@ -4707,6 +4707,10 @@ PrefabInstance:
propertyPath: m_Name propertyPath: m_Name
value: NestedParentArmature_Unpack value: NestedParentArmature_Unpack
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 8508310942573128499, guid: c708a3b79cd542b42bbfedb17e213bc1, type: 3}
propertyPath: m_RenderPostProcessing
value: 1
objectReference: {fileID: 0}
m_RemovedComponents: m_RemovedComponents:
- {fileID: 3543048675319490572, guid: c708a3b79cd542b42bbfedb17e213bc1, type: 3} - {fileID: 3543048675319490572, guid: c708a3b79cd542b42bbfedb17e213bc1, type: 3}
m_RemovedGameObjects: [] m_RemovedGameObjects: []

View File

@ -1,6 +1,3 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.Rendering; using UnityEngine.Rendering;

View File

@ -63,6 +63,41 @@ MonoBehaviour:
- {fileID: 5935297997615179469} - {fileID: 5935297997615179469}
- {fileID: -4720686990112659349} - {fileID: -4720686990112659349}
- {fileID: 9109530758031880643} - {fileID: 9109530758031880643}
- {fileID: 4702212335856883116}
--- !u!114 &4702212335856883116
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: af08f034e95c2d246b0cbaf4305dfbc4, type: 3}
m_Name: AutoExposureVolumeProfile
m_EditorClassIdentifier:
active: 1
filtering:
m_OverrideState: 0
m_Value: {x: 50, y: 95}
minLuminance:
m_OverrideState: 0
m_Value: 0
maxLuminance:
m_OverrideState: 0
m_Value: 0
exposureCompensation:
m_OverrideState: 0
m_Value: 1
eyeAdaptation:
m_OverrideState: 0
m_Value: 0
speedUp:
m_OverrideState: 0
m_Value: 2
speedDown:
m_OverrideState: 0
m_Value: 1
--- !u!114 &5935297997615179469 --- !u!114 &5935297997615179469
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 3 m_ObjectHideFlags: 3

View File

@ -75,7 +75,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 5a00a63fdd6bd2a45ab1f2d869305ffd, type: 3} m_Script: {fileID: 11500000, guid: 5a00a63fdd6bd2a45ab1f2d869305ffd, type: 3}
m_Name: OasisFogVolumeComponent m_Name: OasisFogVolumeComponent
m_EditorClassIdentifier: m_EditorClassIdentifier:
active: 1 active: 0
Density: Density:
m_OverrideState: 1 m_OverrideState: 1
m_Value: 0.002 m_Value: 0.002
@ -103,7 +103,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 66f335fb1ffd8684294ad653bf1c7564, type: 3} m_Script: {fileID: 11500000, guid: 66f335fb1ffd8684294ad653bf1c7564, type: 3}
m_Name: ColorAdjustments m_Name: ColorAdjustments
m_EditorClassIdentifier: m_EditorClassIdentifier:
active: 1 active: 0
postExposure: postExposure:
m_OverrideState: 1 m_OverrideState: 1
m_Value: 2 m_Value: 2

View File

@ -108,6 +108,25 @@ MonoBehaviour:
cutoffThreshold: 0.2 cutoffThreshold: 0.2
binaryValue: 0.9 binaryValue: 0.9
flags: 13 flags: 13
--- !u!114 &-7143664486661302651
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d34dacd53c1ace84a8c6ac5ff4afb555, type: 3}
m_Name: AutoExposure
m_EditorClassIdentifier:
m_Active: 1
settings:
HistogramRenderPassEvent: 550
AutoExposureRenderPassEvent: 550
BlitMat: {fileID: 2100000, guid: b04591ed716b35e41857f554d491ef4b, type: 2}
ComputeHistogramComputeShader: {fileID: 7200000, guid: b140e27dc74a1fb4d9ae30f8566b8919, type: 3}
AutoExposureComputeShader: {fileID: 7200000, guid: 6febb5a945f3510429c58ed4a45c1846, type: 3}
--- !u!114 &-5418649131825517062 --- !u!114 &-5418649131825517062
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -337,7 +356,8 @@ MonoBehaviour:
- {fileID: 7541218312462517771} - {fileID: 7541218312462517771}
- {fileID: 5808157236138506604} - {fileID: 5808157236138506604}
- {fileID: -5418649131825517062} - {fileID: -5418649131825517062}
m_RendererFeatureMap: bc3f630842f2e70dd6a559c442a94bfd4529d15534f2d3de228858dca8d12222716523fbf3439fdb7a327b7bff4bdd446ac59dfa966ffa88ca6373cd5da9013d6cff55ca297e5e908a7b3653203b82383b2141bb05fbe69aec5704e48e2763e90bc6ff9f19caa7686c79a6bb3bb89a50faad0fe75217cdb4 - {fileID: -7143664486661302651}
m_RendererFeatureMap: bc3f630842f2e70dd6a559c442a94bfd4529d15534f2d3de228858dca8d12222716523fbf3439fdb7a327b7bff4bdd446ac59dfa966ffa88ca6373cd5da9013d6cff55ca297e5e908a7b3653203b82383b2141bb05fbe69aec5704e48e2763e90bc6ff9f19caa7686c79a6bb3bb89a50faad0fe75217cdb485d6fa85ff9adc9c
m_UseNativeRenderPass: 0 m_UseNativeRenderPass: 0
postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2} postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2}
shaders: shaders:

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Drawing.Drawing2D;
using UnityEngine; using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering; using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal; using UnityEngine.Rendering.Universal;
@ -10,57 +12,131 @@ namespace X.Rendering.Feature
[Serializable] [Serializable]
public class Settings public class Settings
{ {
public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing; public RenderPassEvent HistogramRenderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
public RenderPassEvent AutoExposureRenderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
public Material BlitMat; public Material BlitMat;
public ComputeShader Compute; public ComputeShader ComputeHistogramComputeShader;
public ComputeShader AutoExposureComputeShader;
} }
[SerializeField] [SerializeField]
Settings settings; Settings settings;
AutoExposurePass autoExposurePass; AutoExposurePass autoExposurePass;
ComputeLuminanceHistogramPass computeLuminanceHistogramPass;
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{ {
#if UNITY_EDITOR
if(renderingData.cameraData.isSceneViewCamera || renderingData.cameraData.isPreviewCamera)
{
return;
}
#endif
renderer.EnqueuePass(computeLuminanceHistogramPass);
renderer.EnqueuePass(autoExposurePass); renderer.EnqueuePass(autoExposurePass);
} }
public override void Create() public override void Create()
{ {
computeLuminanceHistogramPass = new(settings);
autoExposurePass = new(settings); autoExposurePass = new(settings);
} }
class AutoExposurePass : ScriptableRenderPass protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
autoExposurePass?.Dispose();
computeLuminanceHistogramPass?.Dispose();
}
class ComputeLuminanceHistogramPass : ScriptableRenderPass, IDisposable
{
private ProfilingSampler profiler;
private Settings settings;
private GraphicsBuffer histogramBuffer;
public const int k_Bins = 128;
public static GraphicsBufferHandle HistogramBufferHandle;
public ComputeLuminanceHistogramPass(Settings settings)
{
this.settings = settings;
profiler = new(nameof(ComputeLuminanceHistogramPass));
renderPassEvent = settings.HistogramRenderPassEvent;
histogramBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, k_Bins, sizeof(uint));
}
public void Dispose()
{
histogramBuffer?.Release();
histogramBuffer = null;
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var stack = VolumeManager.instance.stack;
var autoExposureVolume = stack.GetComponent<AutoExposureVolumeProfile>();
if (!autoExposureVolume || !autoExposureVolume.IsActive())
{
return;
}
var cmd = renderingData.commandBuffer;
using var scp = new ProfilingScope(cmd, profiler);
var scaleOffsetRes = AutoExposureVolumeProfile.GetHistogramScaleOffsetRes();
var computeShader = settings.ComputeHistogramComputeShader;
int kernel = computeShader.FindKernel("KEyeHistogramClear");
cmd.SetComputeBufferParam(computeShader, kernel, "_HistogramBuffer", histogramBuffer);
computeShader.GetKernelThreadGroupSizes(kernel, out var threadX, out var threadY, out var threadZ);
cmd.DispatchCompute(computeShader, kernel, Mathf.CeilToInt(k_Bins / (float)threadX), 1, 1);
// Get a log histogram
kernel = 1;
HistogramBufferHandle = histogramBuffer.bufferHandle;
cmd.SetComputeBufferParam(computeShader, kernel, "_HistogramBuffer", histogramBuffer);
cmd.SetComputeTextureParam(computeShader, kernel, "_Source", renderingData.cameraData.renderer.cameraColorTargetHandle);
cmd.SetComputeVectorParam(computeShader, "_ScaleOffsetRes", scaleOffsetRes);
computeShader.GetKernelThreadGroupSizes(kernel, out threadX, out threadY, out threadZ);
cmd.DispatchCompute(computeShader, kernel,
Mathf.CeilToInt(scaleOffsetRes.z / 2f / threadX),
Mathf.CeilToInt(scaleOffsetRes.w / 2f / threadY),
1);
}
}
class AutoExposurePass : ScriptableRenderPass, IDisposable
{ {
private Settings settings; private Settings settings;
private int computeCounter; private int computeCounter;
private ProfilingSampler profiler; private ProfilingSampler profiler;
private ComputeBuffer outputBuffer;
private static readonly int Dims = Shader.PropertyToID("_Dims"); const int k_NumAutoExposureTextures = 2;
private static readonly int Source = Shader.PropertyToID("_Source"); private RTHandle[] autoExposureHandles;
private static readonly int TempOut = Shader.PropertyToID("_OutBuffer"); private int autoExposurePingPong;
private static readonly int ScreenExposureProp = Shader.PropertyToID("_Exposure");
private static readonly int ExposureRangeProp = Shader.PropertyToID("_Range");
private static readonly int WhitePointProp = Shader.PropertyToID("_WhitePoint");
private static readonly int MaxBrightness = Shader.PropertyToID("_MaxBrightness");
private static readonly int BlitTextureId = Shader.PropertyToID("_BlitTexture");
private const int ThreadGroupSize = 32;
private Vector2Int lastSize = Vector2Int.zero;
private static float targetExposure = 0.1f;
private static float currentExposure = 0.1f;
private static AutoExposureVolumeProfile autoExposureVolume = null; private static AutoExposureVolumeProfile autoExposureVolume = null;
private static float lastTime = 0.0f; private bool reset = false;
private static bool autoExposureing = false;
public AutoExposurePass(Settings settings) public AutoExposurePass(Settings settings)
{ {
this.settings = settings; this.settings = settings;
renderPassEvent = settings.renderPassEvent; renderPassEvent = settings.AutoExposureRenderPassEvent;
profiler = new(nameof(AutoExposurePass)); profiler = new(nameof(AutoExposurePass));
var desc = new RenderTextureDescriptor(1,1,GraphicsFormat.R32_SFloat,0,1)
{
enableRandomWrite = true,
msaaSamples = 1,
useMipMap = false
};
autoExposureHandles = new RTHandle[k_NumAutoExposureTextures];
for (var i = 0; i < autoExposureHandles.Length; i++)
{
RenderingUtils.ReAllocateIfNeeded(ref autoExposureHandles[i], desc, name: $"AutoExposure_{i}");
}
} }
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
@ -69,97 +145,89 @@ namespace X.Rendering.Feature
autoExposureVolume = stack.GetComponent<AutoExposureVolumeProfile>(); autoExposureVolume = stack.GetComponent<AutoExposureVolumeProfile>();
if (!autoExposureVolume || !autoExposureVolume.IsActive()) if (!autoExposureVolume || !autoExposureVolume.IsActive())
{ {
reset = true;
return; return;
} }
var cmd = renderingData.commandBuffer; var cmd = renderingData.commandBuffer;
using var scp = new ProfilingScope(cmd, profiler); using var scp = new ProfilingScope(cmd, profiler);
if (computeCounter % Mathf.Max(autoExposureVolume.framesPerCompute.value, 1.0f) == 0) var filteringMinMax = autoExposureVolume.filtering.value;
{ float lowPercent = filteringMinMax.x;
computeCounter = 0; float highPercent = filteringMinMax.y;
var textureDesc = renderingData.cameraData.cameraTargetDescriptor; const float kMinDelta = 1e-2f;
highPercent = Mathf.Clamp(highPercent, 1f + kMinDelta, 99f);
lowPercent = Mathf.Clamp(lowPercent, 1f, highPercent - kMinDelta);
Vector2Int size = new(textureDesc.width, textureDesc.height); // Clamp min/max adaptation values as well
if(outputBuffer == null || lastSize != size) float minLum = autoExposureVolume.minLuminance.value;
{ float maxLum = autoExposureVolume.maxLuminance.value;
lastSize = size; minLum = Mathf.Min(minLum, maxLum);
outputBuffer?.Dispose(); maxLum = Mathf.Max(minLum, maxLum);
outputBuffer = new ComputeBuffer(Mathf.CeilToInt((float)size.y / ThreadGroupSize * 2), 4, ComputeBufferType.Structured) var speedDown = Mathf.Max(0, autoExposureVolume.speedDown.value);
{ var speedUp = Mathf.Max(0, autoExposureVolume.speedUp.value);
name = "Auto Exposure Output Buffer" var exposureCompensation = autoExposureVolume.exposureCompensation.value;
}; string adaptation = null;
}
cmd.SetComputeIntParams(settings.Compute, Dims, size.x, size.y);
cmd.SetComputeTextureParam(settings.Compute, 0, Source, renderingData.cameraData.renderer.cameraColorTargetHandle); if (autoExposureVolume.eyeAdaptation.value == AutoExposureVolumeProfile.EyeAdaptation.Fixed)
cmd.SetComputeBufferParam(settings.Compute, 0, TempOut, outputBuffer); adaptation = "KAutoExposureAvgLuminance_fixed";
cmd.DispatchCompute(settings.Compute, 0, size.y, 1, 1); else
AsyncGPUReadback.Request(outputBuffer, OnCompleteReadBack); adaptation = "KAutoExposureAvgLuminance_progressive";
}
++computeCounter; var compute = settings.AutoExposureComputeShader;
if (!autoExposureing) int kernel = compute.FindKernel(adaptation);
int pp = autoExposurePingPong;
var src = autoExposureHandles[++pp % 2];
var dst = autoExposureHandles[++pp % 2];
if (reset)
{ {
//return; cmd.SetRenderTarget(src, loadAction: RenderBufferLoadAction.DontCare, storeAction: RenderBufferStoreAction.Store);
cmd.ClearRenderTarget(false, true, Color.clear);
} }
UpdateExposure();
settings.BlitMat.SetFloat(ScreenExposureProp, currentExposure); cmd.SetComputeBufferParam(compute, kernel, "_HistogramBuffer", ComputeLuminanceHistogramPass.HistogramBufferHandle);
settings.BlitMat.SetVector(ExposureRangeProp, autoExposureVolume.exposureRange.value); cmd.SetComputeVectorParam(compute, "_Params1", new Vector4(lowPercent * 0.01f, highPercent * 0.01f, Mathf.Pow(2, minLum), Mathf.Pow(2, maxLum)));
settings.BlitMat.SetFloat(WhitePointProp, autoExposureVolume.whitePoint.value); cmd.SetComputeVectorParam(compute, "_Params2", new Vector4(speedDown, speedUp, exposureCompensation, Time.smoothDeltaTime));
settings.BlitMat.SetFloat(MaxBrightness, autoExposureVolume.brightnessLimit.value); cmd.SetComputeVectorParam(compute, "_ScaleOffsetRes", AutoExposureVolumeProfile.GetHistogramScaleOffsetRes());
cmd.SetComputeTextureParam(compute, kernel, "_Source", src);
cmd.SetComputeTextureParam(compute, kernel, "_Destination", dst);
cmd.DispatchCompute(compute, kernel, 1, 1, 1);
autoExposurePingPong = ++pp % 2;
var renderer = renderingData.cameraData.renderer; var renderer = renderingData.cameraData.renderer;
settings.BlitMat.SetTexture(BlitTextureId, renderer.cameraColorTargetHandle); settings.BlitMat.SetTexture("_SourceTexture", renderer.cameraColorTargetHandle);
settings.BlitMat.SetTexture("_AutoExposureTexture", dst);
var destination = renderer.GetCameraColorFrontBuffer(cmd); var destination = renderer.GetCameraColorFrontBuffer(cmd);
cmd.SetRenderTarget(destination, loadAction: RenderBufferLoadAction.DontCare, storeAction: RenderBufferStoreAction.Store); cmd.SetRenderTarget(destination, loadAction: RenderBufferLoadAction.DontCare, storeAction: RenderBufferStoreAction.Store);
cmd.DrawProcedural(Matrix4x4.identity, settings.BlitMat, 0, MeshTopology.Triangles, 3); cmd.DrawProcedural(Matrix4x4.identity, settings.BlitMat, 0, MeshTopology.Triangles, 3);
renderer.SwapColorBuffer(cmd); renderer.SwapColorBuffer(cmd);
reset = false;
//cmd.RequestAsyncReadback(dst, (a) =>
//{
// if (!a.hasError)
// {
// var data = a.GetData<float>();
// for (var i = 0; i < data.Length; i++)
// {
// Debug.Log(data[i]);
// }
// }
//});
} }
private static void OnCompleteReadBack(AsyncGPUReadbackRequest request)
public void Dispose()
{ {
if (request.hasError) if (autoExposureHandles != null)
{ {
return; foreach (var autoExposureHandle in autoExposureHandles)
}
// Get Compute result
float[] groupValues = request.GetData<float>().ToArray();
// Add up all the workgroup's results
double pixelLumTotal = 0;
uint pixels = 0;
for (int i = 0; i < groupValues.Length / 2; i++)
{ {
pixelLumTotal += groupValues[i * 2]; autoExposureHandle?.Release();
pixels += (uint)Mathf.CeilToInt(groupValues[i * 2 + 1]);
} }
// Average the results and set as the target exposure autoExposureHandles = null;
targetExposure = (float)(pixelLumTotal / pixels);
autoExposureing = true;
}
public static float ExpDecay(float a, float b, float decay, float deltaTime) => b + (a - b) * Mathf.Exp(-decay * deltaTime);
private static void UpdateExposure()
{
// Delta time, using the built-in one seems to flicker a lot, weird
float deltaTime = Time.time - lastTime;
lastTime = Time.time;
// Exposure difference, skip if zero
float diff = targetExposure - currentExposure;
if (Mathf.Approximately(diff,0))
{
autoExposureing = false;
return;
}
float decay = diff > 0 ? autoExposureVolume.increaseSpeed.value : autoExposureVolume.decreaseSpeed.value;
currentExposure = ExpDecay(currentExposure, targetExposure, decay, deltaTime);
if (float.IsNaN(currentExposure))
{
currentExposure = 0.1f;
} }
} }
} }

View File

@ -1,32 +1,82 @@
using System;
using UnityEngine; using UnityEngine;
using UnityEngine.Rendering; using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal; using UnityEngine.Rendering.Universal;
namespace X.Rendering.Feature namespace X.Rendering.Feature
{ {
public class AutoExposureVolumeProfile : VolumeComponent, IPostProcessComponent public class AutoExposureVolumeProfile : VolumeComponent, IPostProcessComponent
{ {
[Tooltip("How many frames it takes to recalculate the average brightness. Decreasing this value can affect performance.")] public const int rangeMin = -9; // ev
public ClampedIntParameter framesPerCompute = new(5, 1, 20); public const int rangeMax = 9; // ev
[Space] public static Vector4 GetHistogramScaleOffsetRes()
[Tooltip("Limits the maximum brightness for very bright objects to work properly with bloom and other effects")] {
public FloatParameter brightnessLimit = new(15f); float diff = rangeMax - rangeMin;
float scale = 1f / diff;
float offset = -rangeMin * scale;
return new Vector4(scale, offset, Screen.width, Screen.height);
}
[Tooltip("For reinhard tonemapping")] public enum EyeAdaptation
public FloatParameter whitePoint = new(3f); {
/// <summary>
/// Progressive (smooth) eye adaptation.
/// </summary>
Progressive,
[Tooltip("How much an individual pixel's exposure can vary from the original value.")] /// <summary>
public FloatRangeParameter exposureRange = new(new Vector2(-2.5f, 0.6f), -6.0f, 6.0f); /// Fixed (instant) eye adaptation.
/// </summary>
Fixed
}
[Space] [Serializable]
[Tooltip("How quickly the exposure increases")] public sealed class EyeAdaptationParameter : VolumeParameter<EyeAdaptation> { }
public FloatParameter increaseSpeed = new(5f);
[Tooltip("How quickly the exposure decreases")] [Range(1f, 99f)]
public FloatParameter decreaseSpeed = new(5f); public Vector2Parameter filtering = new Vector2Parameter(new Vector2(50f, 95f));
public bool IsActive() => framesPerCompute.overrideState; /// <summary>
/// Minimum average luminance to consider for auto exposure (in EV).
/// </summary>
[Range(rangeMin, rangeMax), InspectorName("Minimum (EV)")]
public FloatParameter minLuminance = new FloatParameter (0f);
/// <summary>
/// Maximum average luminance to consider for auto exposure (in EV).
/// </summary>
[Range(rangeMin, rangeMax), InspectorName("Maximum (EV)")]
public FloatParameter maxLuminance = new FloatParameter (0f);
/// <summary>
/// Middle-grey value. Use this to compensate the global exposure of the scene.
/// </summary>
[Min(0f), InspectorName("Exposure Compensation"), Tooltip("Use this to scale the global exposure of the scene.")]
public FloatParameter exposureCompensation = new FloatParameter (1f);
/// <summary>
/// The type of eye adaptation to use.
/// </summary>
[InspectorName("Type"), Tooltip("Use \"Progressive\" if you want auto exposure to be animated. Use \"Fixed\" otherwise.")]
public EyeAdaptationParameter eyeAdaptation = new EyeAdaptationParameter() { value = EyeAdaptation.Progressive };
/// <summary>
/// The adaptation speed from a dark to a light environment.
/// </summary>
[Min(0f), Tooltip("Adaptation speed from a dark to a light environment.")]
public FloatParameter speedUp = new FloatParameter (2f);
/// <summary>
/// The adaptation speed from a light to a dark environment.
/// </summary>
[Min(0f), Tooltip("Adaptation speed from a light to a dark environment.")]
public FloatParameter speedDown = new FloatParameter (1f);
public bool IsActive() => filtering.overrideState;
public bool IsTileCompatible() public bool IsTileCompatible()
{ {

View File

@ -0,0 +1,184 @@
#pragma warning(disable : 3568)
#pragma kernel KAutoExposureAvgLuminance_fixed MAIN=KAutoExposureAvgLuminance_fixed
#pragma kernel KAutoExposureAvgLuminance_progressive MAIN=KAutoExposureAvgLuminance_progressive PROGRESSIVE
#pragma enable_d3d11_debug_symbols
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#define EPSILON 1.0e-4
#define HISTOGRAM_BINS 128
#define HISTOGRAM_TEXELS HISTOGRAM_BINS / 4
#if SHADER_API_GLES3 || SHADER_API_METAL
#define HISTOGRAM_THREAD_X 8
#define HISTOGRAM_THREAD_Y 8
#define HISTOGRAM_REDUCTION_THREAD_X 8
#define HISTOGRAM_REDUCTION_THREAD_Y 8
#define HISTOGRAM_REDUCTION_ALT_PATH 1
#else
#define HISTOGRAM_THREAD_X 16
#define HISTOGRAM_THREAD_Y 16
#define HISTOGRAM_REDUCTION_THREAD_X HISTOGRAM_THREAD_X
#define HISTOGRAM_REDUCTION_THREAD_Y HISTOGRAM_BINS / HISTOGRAM_THREAD_Y
#define HISTOGRAM_REDUCTION_ALT_PATH 0
#endif
#define HISTOGRAM_REDUCTION_BINS HISTOGRAM_REDUCTION_THREAD_X * HISTOGRAM_REDUCTION_THREAD_Y
half Luminance(half3 linearRgb)
{
return dot(linearRgb, float3(0.2126729, 0.7151522, 0.0721750));
}
half Luminance(half4 linearRgba)
{
return Luminance(linearRgba.rgb);
}
float GetHistogramBinFromLuminance(float value, float2 scaleOffset)
{
return saturate(log2(value) * scaleOffset.x + scaleOffset.y);
}
float GetLuminanceFromHistogramBin(float bin, float2 scaleOffset)
{
return exp2((bin - scaleOffset.y) / scaleOffset.x);
}
float GetBinValue(StructuredBuffer<uint> buffer, uint index, float maxHistogramValue)
{
return float(buffer[index]) * maxHistogramValue;
}
float FindMaxHistogramValue(StructuredBuffer<uint> buffer)
{
uint maxValue = 0u;
for (uint i = 0; i < HISTOGRAM_BINS; i++)
{
uint h = buffer[i];
maxValue = max(maxValue, h);
}
return float(maxValue);
}
void FilterLuminance(StructuredBuffer<uint> buffer, uint i, float maxHistogramValue, float2 scaleOffset, inout float4 filter)
{
float binValue = GetBinValue(buffer, i, maxHistogramValue);
// Filter dark areas
float offset = min(filter.z, binValue);
binValue -= offset;
filter.zw -= offset.xx;
// Filter highlights
binValue = min(filter.w, binValue);
filter.w -= binValue;
// Luminance at the bin
float luminance = GetLuminanceFromHistogramBin(float(i) / float(HISTOGRAM_BINS), scaleOffset);
filter.xy += float2(luminance * binValue, binValue);
}
float GetAverageLuminance(StructuredBuffer<uint> buffer, float4 params, float maxHistogramValue, float2 scaleOffset)
{
// Sum of all bins
uint i;
float totalSum = 0.0;
UNITY_UNROLL
for (i = 0; i < HISTOGRAM_BINS; i++)
totalSum += GetBinValue(buffer, i, maxHistogramValue);
// Skip darker and lighter parts of the histogram to stabilize the auto exposure
// x: filtered sum
// y: accumulator
// zw: fractions
float4 filter = float4(0.0, 0.0, totalSum * params.xy);
UNITY_UNROLL
for (i = 0; i < HISTOGRAM_BINS; i++)
FilterLuminance(buffer, i, maxHistogramValue, scaleOffset, filter);
// Clamp to user brightness range
return clamp(filter.x / max(filter.y, EPSILON), params.z, params.w);
}
StructuredBuffer<uint> _HistogramBuffer;
Texture2D<float> _Source;
RWTexture2D<float> _Destination;
float4 _Params1; // x: lowPercent, y: highPercent, z: minBrightness, w: maxBrightness
float4 _Params2; // x: speed down, y: speed up, z: exposure compensation, w: delta time
float4 _ScaleOffsetRes; // x: scale, y: offset, w: histogram pass width, h: histogram pass height
groupshared uint gs_pyramid[HISTOGRAM_REDUCTION_BINS];
float GetExposureMultiplier(float avgLuminance)
{
avgLuminance = max(EPSILON, avgLuminance);
//float keyValue = 1.03 - (2.0 / (2.0 + log2(avgLuminance + 1.0)));
float keyValue = _Params2.z;
float exposure = keyValue / avgLuminance;
return exposure;
}
float InterpolateExposure(float newExposure, float oldExposure)
{
float delta = newExposure - oldExposure;
float speed = delta > 0.0 ? _Params2.x : _Params2.y;
float exposure = oldExposure + delta * (1.0 - exp2(-_Params2.w * speed));
return exposure;
}
[numthreads(HISTOGRAM_REDUCTION_THREAD_X, HISTOGRAM_REDUCTION_THREAD_Y, 1)]
void MAIN(uint2 groupThreadId : SV_GroupThreadID)
{
#if HISTOGRAM_REDUCTION_ALT_PATH
const uint thread_id = groupThreadId.y * HISTOGRAM_REDUCTION_THREAD_X + groupThreadId.x;
gs_pyramid[thread_id] = max(_HistogramBuffer[thread_id], _HistogramBuffer[thread_id + HISTOGRAM_REDUCTION_BINS]);
#else
const uint thread_id = groupThreadId.y * HISTOGRAM_REDUCTION_THREAD_X + groupThreadId.x;
gs_pyramid[thread_id] = _HistogramBuffer[thread_id];
#endif
GroupMemoryBarrierWithGroupSync();
// Parallel reduction to find the max value
UNITY_UNROLL
for (uint i = HISTOGRAM_REDUCTION_BINS >> 1u; i > 0u; i >>= 1u)
{
if (thread_id < i)
gs_pyramid[thread_id] = max(gs_pyramid[thread_id], gs_pyramid[thread_id + i]);
GroupMemoryBarrierWithGroupSync();
}
GroupMemoryBarrierWithGroupSync();
if (thread_id == 0u)
{
float maxValue = 1.0 / float(gs_pyramid[0]);
#if PROGRESSIVE
float avgLuminance = GetAverageLuminance(_HistogramBuffer, _Params1, maxValue, _ScaleOffsetRes.xy);
float exposure = GetExposureMultiplier(avgLuminance);
float prevExposure = _Source[uint2(0u, 0u)].x;
exposure = InterpolateExposure(exposure, prevExposure);
_Destination[uint2(0u, 0u)].x = exposure.x;
#else
float avgLuminance = GetAverageLuminance(_HistogramBuffer, _Params1, maxValue, _ScaleOffsetRes.xy);
float exposure = GetExposureMultiplier(avgLuminance);
_Destination[uint2(0u, 0u)].x = exposure.x;
#endif
}
}

View File

@ -21,66 +21,14 @@ Shader "XRP/AutoExposure"
#pragma vertex vert #pragma vertex vert
#pragma fragment frag #pragma fragment frag
SamplerState sampler_BlitTexture; // Screen texture TEXTURE2D_X(_SourceTexture);
TEXTURE2D_X(_AutoExposureTexture);
float _MaxBrightness; // Limit the final brightness to this value half GammaCorrect_half(half linearColor)
float _Exposure; // The average exposure to work with
float _WhitePoint; // White point for reinhard tonemapping
float2 _Range;
// Min and Max luminosity change, how much can each pixel be offset from it's original luminosity
// Converts from RGB space to Yxy (float3.x is Luminance)
float3 convert_rgb_to_Yxy(float3 rgb)
{ {
float3 xyz; return pow(linearColor,1/2.2);
xyz.x = dot(rgb, float3(0.4124f, 0.3576f, 0.1805f));
xyz.y = dot(rgb, float3(0.2126f, 0.7152f, 0.0722f));
xyz.z = dot(rgb, float3(0.0193f, 0.1192f, 0.9505f));
float sum = xyz.x + xyz.y + xyz.z;
float x = (sum > 0.0) ? (xyz.x / sum) : 0.0;
float y = (sum > 0.0) ? (xyz.y / sum) : 0.0;
return float3(xyz.y, x, y); // Y, x, y
} }
// Converts from Yxy (float3.x is Luminance) space to RGB
float3 convert_Yxy_to_rgb(float3 Yxy)
{
float Y = Yxy.x;
float x = Yxy.y;
float y = Yxy.z;
float X = y > 0.0 ? x * Y / y : 0.0;
float Z = y > 0.0 ? (1.0 - x - y) * Y / y : 0.0;
float3 XYZ = float3(X, Y, Z);
float3x3 M_XYZ2RGB = float3x3(
3.2406f, -1.5372f, -0.4986f,
-0.9689f, 1.8758f, 0.0415f,
0.0557f, -0.2040f, 1.0570f
);
return mul(M_XYZ2RGB, XYZ);
}
// Tone Mapping function
float reinhard2(float lp, float wp)
{
return lp * (1.0f + lp / (wp * wp)) / (1.0f + lp);
}
// Sample texture and return RGB and A in separate variables
void sample_split(float2 uv, out float3 rgb, out float alpha)
{
float4 color = _BlitTexture.SampleBias(sampler_BlitTexture, uv, _GlobalMipBias.x);;
rgb = color.rgb;
alpha = color.a;
}
Varyings vert(uint vertexID: SV_VertexID) Varyings vert(uint vertexID: SV_VertexID)
{ {
Varyings o; Varyings o;
@ -89,27 +37,15 @@ Shader "XRP/AutoExposure"
return o; return o;
} }
// Fragment shader, adaptation of: https://bruop.github.io/tonemapping/
float4 frag(Varyings input) : SV_Target float4 frag(Varyings input) : SV_Target
{ {
float3 rgb; float2 uv = input.texcoord.xy;
float alpha; half4 color = SAMPLE_TEXTURE2D_X_LOD(_SourceTexture, sampler_LinearClamp, uv, _BlitMipLevel);
sample_split(input.texcoord, rgb, alpha); float luminance = GammaCorrect_half(SAMPLE_TEXTURE2D_X_LOD(_AutoExposureTexture, sampler_LinearRepeat, float2(0,0), _BlitMipLevel).x) ;
// Yxy.x is Y, the luminance return color * luminance;
float3 Yxy = convert_rgb_to_Yxy(rgb);
Yxy.x = min(Yxy.x, _MaxBrightness);
// Tone mapping
float lp = Yxy.x / (9.6f * _Exposure + 0.0001f);
float new_lum = reinhard2(lp, _WhitePoint);
// Clamp the added luminosity and convert back to RGB
Yxy.x += clamp(new_lum - Yxy.x, _Range.x, _Range.y);
rgb = convert_Yxy_to_rgb(Yxy);
return float4(rgb, alpha);
} }
ENDHLSL ENDHLSL
} }
} }

View File

@ -1,68 +0,0 @@
#pragma kernel Main KERNEL_SCAN
// 32 or 64 should be ok
#define THREAD_COUNT 32
// I/O
uint2 _Dims; // Screen Dimensions
Texture2D _Source; // Screen Texture
RWStructuredBuffer<float> _OutBuffer; // Compute Group Output
// Shared row values
groupshared float rows_lum[THREAD_COUNT];
groupshared uint rows_pixels[THREAD_COUNT];
// Weird ass function for doing things with the luminosity, looks great on desmos
float weird_ass_function(float x)
{
x += 0.1f;
float w = 32.0f;
return x * (x + w) / (w * x + 0.154f) - 0.137f;
}
// Convert screen coordinates to the pixel's luminosity
float get_screen_lum(uint x, uint y)
{
return dot(_Source[uint2(x, y)].rgb, float3(0.21267291f, 0.7151522f, 0.0721750f));
}
// Each thread corresponds to a Y coordinate on the screen and is in charge of the whole row
[numthreads(THREAD_COUNT, 1, 1)]
void Main(uint group : SV_GroupID, uint thread : SV_GroupThreadID, uint y : SV_DispatchThreadID)
{
float lum_row = 0;
uint pixels = 0;
// Get the total pixels and luminosity of each row
if (y < _Dims.y)
{
for (uint x = 0; x < _Dims.x; x++)
{
pixels++;
lum_row += get_screen_lum(x, y);
}
}
// Store the results in the shared arrays
rows_lum[thread] = lum_row;
rows_pixels[thread] = pixels;
GroupMemoryBarrierWithGroupSync();
// The thread id:0 of each workgroup is in charge of adding upp all the values of the threads in its group
if (thread == 0)
{
float groupLumAvg = 0;
uint groupPixels = 0;
// Add upp everything
for (int row = 0; row < THREAD_COUNT; row++)
{
groupLumAvg += rows_lum[row];
groupPixels += rows_pixels[row];
}
// Store the group's luminosity and pixel count in the output buffer
_OutBuffer[group * 2] = groupLumAvg;
_OutBuffer[group * 2 + 1] = groupPixels;
}
}

View File

@ -0,0 +1,134 @@
#pragma warning(disable : 3568)
#pragma kernel KEyeHistogram
#pragma kernel KEyeHistogram USE_VIGNETTE_WEIGHTING
#pragma kernel KEyeHistogramClear
#pragma enable_d3d11_debug_symbols
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#define EPSILON 1.0e-4
#define HISTOGRAM_BINS 128
#define HISTOGRAM_TEXELS HISTOGRAM_BINS / 4
#if SHADER_API_GLES3 || SHADER_API_METAL
#define HISTOGRAM_THREAD_X 8
#define HISTOGRAM_THREAD_Y 8
#define HISTOGRAM_REDUCTION_THREAD_X 8
#define HISTOGRAM_REDUCTION_THREAD_Y 8
#define HISTOGRAM_REDUCTION_ALT_PATH 1
#else
#define HISTOGRAM_THREAD_X 16
#define HISTOGRAM_THREAD_Y 16
#define HISTOGRAM_REDUCTION_THREAD_X HISTOGRAM_THREAD_X
#define HISTOGRAM_REDUCTION_THREAD_Y HISTOGRAM_BINS / HISTOGRAM_THREAD_Y
#define HISTOGRAM_REDUCTION_ALT_PATH 0
#endif
#define HISTOGRAM_REDUCTION_BINS HISTOGRAM_REDUCTION_THREAD_X * HISTOGRAM_REDUCTION_THREAD_Y
half Luminance(half3 linearRgb)
{
return dot(linearRgb, float3(0.2126729, 0.7151522, 0.0721750));
}
half Luminance(half4 linearRgba)
{
return Luminance(linearRgba.rgb);
}
float GetHistogramBinFromLuminance(float value, float2 scaleOffset)
{
return saturate(log2(value) * scaleOffset.x + scaleOffset.y);
}
float GetLuminanceFromHistogramBin(float bin, float2 scaleOffset)
{
return exp2((bin - scaleOffset.y) / scaleOffset.x);
}
float GetBinValue(StructuredBuffer<uint> buffer, uint index, float maxHistogramValue)
{
return float(buffer[index]) * maxHistogramValue;
}
RWStructuredBuffer<uint> _HistogramBuffer;
Texture2D<float4> _Source;
SamplerState sampler_LinearClamp;
cbuffer Params
{
float4 _ScaleOffsetRes; // x: scale, y: offset, z: width, w: height
};
groupshared uint gs_histogram[HISTOGRAM_BINS];
[numthreads(HISTOGRAM_THREAD_X, HISTOGRAM_THREAD_Y, 1)]
void KEyeHistogram(uint2 dispatchThreadId : SV_DispatchThreadID, uint2 groupThreadId : SV_GroupThreadID)
{
const uint localThreadId = groupThreadId.y * HISTOGRAM_THREAD_X + groupThreadId.x;
// Clears the shared memory
#if HISTOGRAM_REDUCTION_ALT_PATH
uint localThreadIdOff = localThreadId << 1u;
if (localThreadIdOff < HISTOGRAM_BINS)
{
gs_histogram[localThreadIdOff ] = 0u;
gs_histogram[localThreadIdOff + 1] = 0u;
}
#else
if (localThreadId < HISTOGRAM_BINS)
{
gs_histogram[localThreadId] = 0u;
}
#endif
float2 ipos = float2(dispatchThreadId) * 2.0;
GroupMemoryBarrierWithGroupSync();
// Gather local group histogram
if (ipos.x < _ScaleOffsetRes.z && ipos.y < _ScaleOffsetRes.w)
{
uint weight = 1u;
float2 sspos = ipos / _ScaleOffsetRes.zw;
// Vignette weighting to put more focus on what's in the center of the screen
#if USE_VIGNETTE_WEIGHTING
{
float2 d = abs(sspos - (0.5).xx);
float vfactor = saturate(1.0 - dot(d, d));
vfactor *= vfactor;
weight = (uint)(64.0 * vfactor);
}
#endif
float3 color = _Source.SampleLevel(sampler_LinearClamp, sspos, 0.0).xyz; // Bilinear downsample 2x
float luminance = Luminance(color);
float logLuminance = GetHistogramBinFromLuminance(luminance, _ScaleOffsetRes.xy);
uint idx = (uint) (logLuminance * (HISTOGRAM_BINS - 1u));
InterlockedAdd(gs_histogram[idx], weight);
}
GroupMemoryBarrierWithGroupSync();
// Merge everything
#if HISTOGRAM_REDUCTION_ALT_PATH
if (localThreadIdOff < HISTOGRAM_BINS)
{
InterlockedAdd(_HistogramBuffer[localThreadIdOff ], gs_histogram[localThreadIdOff ]);
InterlockedAdd(_HistogramBuffer[localThreadIdOff + 1], gs_histogram[localThreadIdOff + 1]);
}
#else
if (localThreadId < HISTOGRAM_BINS)
{
InterlockedAdd(_HistogramBuffer[localThreadId], gs_histogram[localThreadId]);
}
#endif
}
[numthreads(HISTOGRAM_THREAD_X, 1, 1)]
void KEyeHistogramClear(uint dispatchThreadId : SV_DispatchThreadID)
{
if (dispatchThreadId < HISTOGRAM_BINS)
_HistogramBuffer[dispatchThreadId] = 0u;
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b140e27dc74a1fb4d9ae30f8566b8919
ComputeShaderImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: