using System; using System.Drawing.Drawing2D; using UnityEngine; using UnityEngine.Experimental.Rendering; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; namespace X.Rendering.Feature { public class AutoExposure : ScriptableRendererFeature { [Serializable] public class Settings { public RenderPassEvent HistogramRenderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing; public RenderPassEvent AutoExposureRenderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing; public Material BlitMat; public ComputeShader ComputeHistogramComputeShader; public ComputeShader AutoExposureComputeShader; } [SerializeField] Settings settings; AutoExposurePass autoExposurePass; ComputeLuminanceHistogramPass computeLuminanceHistogramPass; 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); } public override void Create() { computeLuminanceHistogramPass = new(settings); autoExposurePass = new(settings); } 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(); 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 int computeCounter; private ProfilingSampler profiler; const int k_NumAutoExposureTextures = 2; private RTHandle[] autoExposureHandles; private int autoExposurePingPong; private static AutoExposureVolumeProfile autoExposureVolume = null; private bool reset = false; public AutoExposurePass(Settings settings) { this.settings = settings; renderPassEvent = settings.AutoExposureRenderPassEvent; 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) { var stack = VolumeManager.instance.stack; autoExposureVolume = stack.GetComponent(); if (!autoExposureVolume || !autoExposureVolume.IsActive()) { reset = true; return; } var cmd = renderingData.commandBuffer; using var scp = new ProfilingScope(cmd, profiler); var filteringMinMax = autoExposureVolume.filtering.value; float lowPercent = filteringMinMax.x; float highPercent = filteringMinMax.y; const float kMinDelta = 1e-2f; highPercent = Mathf.Clamp(highPercent, 1f + kMinDelta, 99f); lowPercent = Mathf.Clamp(lowPercent, 1f, highPercent - kMinDelta); // Clamp min/max adaptation values as well float minLum = autoExposureVolume.minLuminance.value; float maxLum = autoExposureVolume.maxLuminance.value; minLum = Mathf.Min(minLum, maxLum); maxLum = Mathf.Max(minLum, maxLum); var speedDown = Mathf.Max(0, autoExposureVolume.speedDown.value); var speedUp = Mathf.Max(0, autoExposureVolume.speedUp.value); var exposureCompensation = autoExposureVolume.exposureCompensation.value; string adaptation = null; if (autoExposureVolume.eyeAdaptation.value == AutoExposureVolumeProfile.EyeAdaptation.Fixed) adaptation = "KAutoExposureAvgLuminance_fixed"; else adaptation = "KAutoExposureAvgLuminance_progressive"; var compute = settings.AutoExposureComputeShader; int kernel = compute.FindKernel(adaptation); int pp = autoExposurePingPong; var src = autoExposureHandles[++pp % 2]; var dst = autoExposureHandles[++pp % 2]; if (reset) { cmd.SetRenderTarget(src, loadAction: RenderBufferLoadAction.DontCare, storeAction: RenderBufferStoreAction.Store); cmd.ClearRenderTarget(false, true, Color.clear); } cmd.SetComputeBufferParam(compute, kernel, "_HistogramBuffer", ComputeLuminanceHistogramPass.HistogramBufferHandle); cmd.SetComputeVectorParam(compute, "_Params1", new Vector4(lowPercent * 0.01f, highPercent * 0.01f, Mathf.Pow(2, minLum), Mathf.Pow(2, maxLum))); cmd.SetComputeVectorParam(compute, "_Params2", new Vector4(speedDown, speedUp, exposureCompensation, Time.smoothDeltaTime)); 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; settings.BlitMat.SetTexture("_SourceTexture", renderer.cameraColorTargetHandle); settings.BlitMat.SetTexture("_AutoExposureTexture", dst); var destination = renderer.GetCameraColorFrontBuffer(cmd); cmd.SetRenderTarget(destination, loadAction: RenderBufferLoadAction.DontCare, storeAction: RenderBufferStoreAction.Store); cmd.DrawProcedural(Matrix4x4.identity, settings.BlitMat, 0, MeshTopology.Triangles, 3); renderer.SwapColorBuffer(cmd); reset = false; //cmd.RequestAsyncReadback(dst, (a) => //{ // if (!a.hasError) // { // var data = a.GetData(); // for (var i = 0; i < data.Length; i++) // { // Debug.Log(data[i]); // } // } //}); } public void Dispose() { if (autoExposureHandles != null) { foreach (var autoExposureHandle in autoExposureHandles) { autoExposureHandle?.Release(); } autoExposureHandles = null; } } } } }