using System; using System.Runtime.InteropServices; using Unity.Profiling; using UnityEngine; using UnityEngine.Rendering; namespace FidelityFX { /// /// Port of ffx_cacao_impl.cpp/.h /// public class CacaoContext { private Cacao.Settings _settings; private Cacao.BufferSizeInfo _bufferSizeInfo; private bool _useDownsampledSsao; private readonly CacaoResources _resources = new CacaoResources(); private CacaoClearLoadCounterPass _clearLoadCounterPass; private CacaoPrepareDepthsPass _prepareDepthsPass; private CacaoPrepareNormalsPass _prepareNormalsPass; private CacaoGenerateBaseSsaoPass _generateBaseSsaoPass; private CacaoGenerateImportanceMapPass _generateImportanceMapPass; private CacaoGenerateSsaoPass _generateSsaoPass; private CacaoEdgeSensitiveBlurPass _edgeSensitiveBlurPass; private CacaoBilateralUpscalePass _bilateralUpscalePass; private CacaoReinterleavePass _reinterleavePass; private ComputeBuffer _constantsBuffer; private readonly Cacao.Constants[] _constants = new Cacao.Constants[1]; private ref Cacao.Constants Constants => ref _constants[0]; public bool Init(CacaoShaders shaders) { // Create constant buffers _constantsBuffer = CreateConstantBuffer("FFX_CACAO_CONSTANT_BUFFER"); // Create load counter resource if (!_resources.CreateLoadCounter()) { return false; } // Initialize compute shaders CreatePasses(shaders); return true; } private void CreatePasses(CacaoShaders shaders) { _clearLoadCounterPass = new CacaoClearLoadCounterPass(shaders.clearLoadCounter, _resources); _prepareDepthsPass = new CacaoPrepareDepthsPass(shaders.prepareDepths, _resources); _prepareNormalsPass = new CacaoPrepareNormalsPass(shaders.prepareNormals, _resources); _generateBaseSsaoPass = new CacaoGenerateBaseSsaoPass(shaders.generateSsao, _resources); _generateImportanceMapPass = new CacaoGenerateImportanceMapPass(shaders.generateImportanceMap, _resources); _generateSsaoPass = new CacaoGenerateSsaoPass(shaders.generateSsao, _resources); _edgeSensitiveBlurPass = new CacaoEdgeSensitiveBlurPass(shaders.edgeSensitiveBlur, _resources); _bilateralUpscalePass = new CacaoBilateralUpscalePass(shaders.bilateralUpscale, _resources); _reinterleavePass = new CacaoReinterleavePass(shaders.reinterleave, _resources); } public void Destroy() { DestroyPass(ref _clearLoadCounterPass); DestroyPass(ref _prepareDepthsPass); DestroyPass(ref _prepareNormalsPass); DestroyPass(ref _generateBaseSsaoPass); DestroyPass(ref _generateImportanceMapPass); DestroyPass(ref _generateSsaoPass); DestroyPass(ref _edgeSensitiveBlurPass); DestroyPass(ref _bilateralUpscalePass); DestroyPass(ref _reinterleavePass); _resources.DestroyLoadCounter(); DestroyConstantBuffer(ref _constantsBuffer); } public bool InitScreenSizeDependentResources(in Cacao.ScreenSizeInfo info) { _useDownsampledSsao = info.UseDownsampledSsao; UpdateBufferSizeInfo(info.Width, info.Height, _useDownsampledSsao, out _bufferSizeInfo); // Create textures return _resources.CreateResources(_bufferSizeInfo); } public void DestroyScreenSizeDependentResources() { _resources.DestroyResources(); } public void UpdateSettings(in Cacao.Settings settings) { _settings = settings; } private static readonly ProfilerMarker PrepareMarker = new ProfilerMarker("Prepare Downsampled Depth, Normals and Mips"); private static readonly ProfilerMarker BasePassMarker = new ProfilerMarker("Generate High Quality Base Pass"); private static readonly ProfilerMarker BaseSsaoMarker = new ProfilerMarker("Base SSAO"); private static readonly ProfilerMarker ImportanceMapMarker = new ProfilerMarker("Importance Map"); private static readonly ProfilerMarker GenerateSsaoMarker = new ProfilerMarker("Generate SSAO"); private static readonly ProfilerMarker DeinterleavedBlurMarker = new ProfilerMarker("Deinterleaved Blur"); private static readonly ProfilerMarker BilateralUpsampleMarker = new ProfilerMarker("Bilateral Upsample"); private static readonly ProfilerMarker ReinterleaveMarker = new ProfilerMarker("Reinterleave"); public void Draw(CommandBuffer commandBuffer, in Cacao.DispatchInfo dispatchInfo, in Matrix4x4 proj, in Matrix4x4 normalsToView) { // Update constant buffers UpdateConstants(ref Constants, _settings, _bufferSizeInfo, proj, normalsToView); commandBuffer.SetBufferData(_constantsBuffer, _constants); // Clear load counter // The same could be accomplished with a ClearRenderTarget on the LoadCounter texture, but Unity does not allow that with async compute _clearLoadCounterPass.Execute(commandBuffer, _constantsBuffer); // Prepare depths, normals and mips commandBuffer.BeginSample(PrepareMarker); _prepareDepthsPass.Execute(commandBuffer, _constantsBuffer, dispatchInfo, _bufferSizeInfo, _settings.qualityLevel, _useDownsampledSsao); _prepareNormalsPass.Execute(commandBuffer, _constantsBuffer, dispatchInfo, _bufferSizeInfo, _settings.generateNormals, _useDownsampledSsao); commandBuffer.EndSample(PrepareMarker); // Base pass for the highest quality setting if (_settings.qualityLevel == Cacao.Quality.Highest) { commandBuffer.BeginSample(BasePassMarker); commandBuffer.BeginSample(BaseSsaoMarker); _generateBaseSsaoPass.Execute(commandBuffer, _constantsBuffer, _bufferSizeInfo); commandBuffer.EndSample(BaseSsaoMarker); commandBuffer.BeginSample(ImportanceMapMarker); _generateImportanceMapPass.Execute(commandBuffer, _constantsBuffer, _bufferSizeInfo); commandBuffer.EndSample(ImportanceMapMarker); commandBuffer.EndSample(BasePassMarker); } // Main SSAO generation commandBuffer.BeginSample(GenerateSsaoMarker); _generateSsaoPass.Execute(commandBuffer, _constantsBuffer, _bufferSizeInfo, _settings.qualityLevel); commandBuffer.EndSample(GenerateSsaoMarker); // De-interleaved blur uint blurPassCount = Math.Clamp(_settings.blurPassCount, 0, 8); if (blurPassCount > 0) { commandBuffer.BeginSample(DeinterleavedBlurMarker); _edgeSensitiveBlurPass.Execute(commandBuffer, _constantsBuffer, _bufferSizeInfo, _settings.qualityLevel, blurPassCount); commandBuffer.EndSample(DeinterleavedBlurMarker); } if (_useDownsampledSsao) { // Bilateral upsample commandBuffer.BeginSample(BilateralUpsampleMarker); _bilateralUpscalePass.Execute(commandBuffer, _constantsBuffer, dispatchInfo, _bufferSizeInfo, _settings.qualityLevel, blurPassCount > 0); commandBuffer.EndSample(BilateralUpsampleMarker); } else { // Reinterleave commandBuffer.BeginSample(ReinterleaveMarker); _reinterleavePass.Execute(commandBuffer, _constantsBuffer, dispatchInfo, _bufferSizeInfo, _settings.qualityLevel, blurPassCount > 0); commandBuffer.EndSample(ReinterleaveMarker); } } /// /// Update buffer size info for resolution width x height. /// /// Screen width. /// Screen height. /// Whether FFX CACAO should use downsampling. /// Output buffer size info. private static void UpdateBufferSizeInfo(uint width, uint height, bool useDownsampledSsao, out Cacao.BufferSizeInfo bsi) { uint halfWidth = (width + 1) / 2; uint halfHeight = (height + 1) / 2; uint quarterWidth = (halfWidth + 1) / 2; uint quarterHeight = (halfHeight + 1) / 2; uint eighthWidth = (quarterWidth + 1) / 2; uint eighthHeight = (quarterHeight + 1) / 2; uint depthBufferWidth = width; uint depthBufferHeight = height; uint depthBufferHalfWidth = halfWidth; uint depthBufferHalfHeight = halfHeight; uint depthBufferQuarterWidth = quarterWidth; uint depthBufferQuarterHeight = quarterHeight; uint depthBufferXOffset = 0; uint depthBufferYOffset = 0; uint depthBufferHalfXOffset = 0; uint depthBufferHalfYOffset = 0; uint depthBufferQuarterXOffset = 0; uint depthBufferQuarterYOffset = 0; bsi.InputOutputBufferWidth = width; bsi.InputOutputBufferHeight = height; bsi.DepthBufferXOffset = depthBufferXOffset; bsi.DepthBufferYOffset = depthBufferYOffset; bsi.DepthBufferWidth = depthBufferWidth; bsi.DepthBufferHeight = depthBufferHeight; if (useDownsampledSsao) { bsi.SsaoBufferWidth = quarterWidth; bsi.SsaoBufferHeight = quarterHeight; bsi.DeinterleavedDepthBufferXOffset = depthBufferQuarterXOffset; bsi.DeinterleavedDepthBufferYOffset = depthBufferQuarterYOffset; bsi.DeinterleavedDepthBufferWidth = depthBufferQuarterWidth; bsi.DeinterleavedDepthBufferHeight = depthBufferQuarterHeight; bsi.ImportanceMapWidth = eighthWidth; bsi.ImportanceMapHeight = eighthHeight; bsi.DownsampledSsaoBufferWidth = halfWidth; bsi.DownsampledSsaoBufferHeight = halfHeight; } else { bsi.SsaoBufferWidth = halfWidth; bsi.SsaoBufferHeight = halfHeight; bsi.DeinterleavedDepthBufferXOffset = depthBufferHalfXOffset; bsi.DeinterleavedDepthBufferYOffset = depthBufferHalfYOffset; bsi.DeinterleavedDepthBufferWidth = depthBufferHalfWidth; bsi.DeinterleavedDepthBufferHeight = depthBufferHalfHeight; bsi.ImportanceMapWidth = quarterWidth; bsi.ImportanceMapHeight = quarterHeight; bsi.DownsampledSsaoBufferWidth = 1; bsi.DownsampledSsaoBufferHeight = 1; } } /// /// Update the contents of the FFX CACAO constant buffer (an FFX_CACAO_Constants struct). Note, this function does not update per pass constants. /// /// Cacao.Constants constant buffer. /// Cacao.Settings settings. /// Cacao.BufferSizeInfo buffer size info. /// Projection matrix for the frame. /// Normals world space to view space matrix for the frame. private static void UpdateConstants(ref Cacao.Constants consts, in Cacao.Settings settings, in Cacao.BufferSizeInfo bufferSizeInfo, in Matrix4x4 proj, in Matrix4x4 normalsToView) { consts.BilateralSigmaSquared = settings.bilateralSigmaSquared; consts.BilateralSimilarityDistanceSigma = settings.bilateralSimilarityDistanceSigma; if (settings.generateNormals) { consts.NormalsWorldToViewspaceMatrix = Matrix4x4.identity; } else { consts.NormalsWorldToViewspaceMatrix = normalsToView; } // used to get average load per pixel; 9.0 is there to compensate for only doing every 9th InterlockedAdd in PSPostprocessImportanceMapB for performance reasons consts.LoadCounterAvgDiv = 9.0f / (bufferSizeInfo.ImportanceMapWidth * bufferSizeInfo.ImportanceMapHeight * 255); float depthLinearizeMul = -proj.m32; // float depthLinearizeMul = ( clipFar * clipNear ) / ( clipFar - clipNear ); float depthLinearizeAdd = proj.m22; // float depthLinearizeAdd = clipFar / ( clipFar - clipNear ); // correct the handedness issue. need to make sure this below is correct, but I think it is. if (depthLinearizeMul * depthLinearizeAdd < 0) depthLinearizeAdd = -depthLinearizeAdd; consts.DepthUnpackConsts.x = depthLinearizeMul; consts.DepthUnpackConsts.y = depthLinearizeAdd; float tanHalfFovY = 1.0f / proj.m11; // = tanf( drawContext.Camera.GetYFOV( ) * 0.5f ); float tanHalfFovX = 1.0f / proj.m00; // = tanHalfFovY * drawContext.Camera.GetAspect( ); consts.CameraTanHalfFOV.x = tanHalfFovX; consts.CameraTanHalfFOV.y = tanHalfFovY; consts.NDCToViewMul.x = consts.CameraTanHalfFOV.x * 2.0f; consts.NDCToViewMul.y = consts.CameraTanHalfFOV.y * -2.0f; consts.NDCToViewAdd.x = consts.CameraTanHalfFOV.x * -1.0f; consts.NDCToViewAdd.y = consts.CameraTanHalfFOV.y * 1.0f; float ratio = (float)bufferSizeInfo.InputOutputBufferWidth / bufferSizeInfo.DepthBufferWidth; float border = (1.0f - ratio) / 2.0f; for (int i = 0; i < 2; ++i) { consts.DepthBufferUVToViewMul[i] = consts.NDCToViewMul[i] / ratio; consts.DepthBufferUVToViewAdd[i] = consts.NDCToViewAdd[i] - consts.NDCToViewMul[i] * border / ratio; } consts.EffectRadius = Mathf.Clamp(settings.radius, 0.0f, 100000.0f); consts.EffectShadowStrength = Mathf.Clamp(settings.shadowMultiplier * 4.3f, 0.0f, 10.0f); consts.EffectShadowPow = Mathf.Clamp(settings.shadowPower, 0.0f, 10.0f); consts.EffectShadowClamp = Mathf.Clamp(settings.shadowClamp, 0.0f, 1.0f); consts.EffectFadeOutMul = -1.0f / (settings.fadeOutTo - settings.fadeOutFrom); consts.EffectFadeOutAdd = settings.fadeOutFrom / (settings.fadeOutTo - settings.fadeOutFrom) + 1.0f; consts.EffectHorizonAngleThreshold = Mathf.Clamp(settings.horizonAngleThreshold, 0.0f, 1.0f); // 1.2 seems to be around the best trade off - 1.0 means on-screen radius will stop/slow growing when the camera is at 1.0 distance, so, depending on FOV, basically filling up most of the screen // This setting is viewspace-dependent and not screen size dependent intentionally, so that when you change FOV the effect stays (relatively) similar. float effectSamplingRadiusNearLimit = settings.radius * 1.2f; // if the depth precision is switched to 32bit float, this can be set to something closer to 1 (0.9999 is fine) consts.DepthPrecisionOffsetMod = 0.9992f; // Special settings for lowest quality level - just nerf the effect a tiny bit if (settings.qualityLevel <= Cacao.Quality.Low) { //consts.EffectShadowStrength *= 0.9f; effectSamplingRadiusNearLimit *= 1.50f; if (settings.qualityLevel == Cacao.Quality.Lowest) consts.EffectRadius *= 0.8f; } effectSamplingRadiusNearLimit /= tanHalfFovY; // to keep the effect same regardless of FOV consts.EffectSamplingRadiusNearLimitRec = 1.0f / effectSamplingRadiusNearLimit; consts.AdaptiveSampleCountLimit = settings.adaptiveQualityLimit; consts.NegRecEffectRadius = -1.0f / consts.EffectRadius; consts.InvSharpness = Mathf.Clamp01(1.0f - settings.sharpness); consts.DetailAOStrength = settings.detailShadowStrength; // set buffer size constants. consts.SSAOBufferDimensions = new Vector2(bufferSizeInfo.SsaoBufferWidth, bufferSizeInfo.SsaoBufferHeight); consts.SSAOBufferInverseDimensions = new Vector2(1.0f / bufferSizeInfo.SsaoBufferWidth, 1.0f / bufferSizeInfo.SsaoBufferHeight); consts.DepthBufferDimensions = new Vector2(bufferSizeInfo.DepthBufferWidth, bufferSizeInfo.DepthBufferHeight); consts.DepthBufferInverseDimensions = new Vector2(1.0f / bufferSizeInfo.DepthBufferWidth, 1.0f / bufferSizeInfo.DepthBufferHeight); consts.DepthBufferOffset = new Vector2Int((int)bufferSizeInfo.DepthBufferXOffset, (int)bufferSizeInfo.DepthBufferYOffset); consts.InputOutputBufferDimensions = new Vector2(bufferSizeInfo.InputOutputBufferWidth, bufferSizeInfo.InputOutputBufferHeight); consts.InputOutputBufferInverseDimensions = new Vector2(1.0f / bufferSizeInfo.InputOutputBufferWidth, 1.0f / bufferSizeInfo.InputOutputBufferHeight); consts.ImportanceMapDimensions = new Vector2(bufferSizeInfo.ImportanceMapWidth, bufferSizeInfo.ImportanceMapHeight); consts.ImportanceMapInverseDimensions = new Vector2(1.0f / bufferSizeInfo.ImportanceMapWidth, 1.0f / bufferSizeInfo.ImportanceMapHeight); consts.DeinterleavedDepthBufferDimensions = new Vector2(bufferSizeInfo.DeinterleavedDepthBufferWidth, bufferSizeInfo.DeinterleavedDepthBufferHeight); consts.DeinterleavedDepthBufferInverseDimensions = new Vector2(1.0f / bufferSizeInfo.DeinterleavedDepthBufferWidth, 1.0f / bufferSizeInfo.DeinterleavedDepthBufferHeight); consts.DeinterleavedDepthBufferOffset = new Vector2(bufferSizeInfo.DeinterleavedDepthBufferXOffset, bufferSizeInfo.DeinterleavedDepthBufferYOffset); consts.DeinterleavedDepthBufferNormalisedOffset = consts.DeinterleavedDepthBufferOffset / consts.DeinterleavedDepthBufferDimensions; if (!settings.generateNormals) { consts.NormalsUnpackMul = 2.0f; // inputs->NormalsUnpackMul; consts.NormalsUnpackAdd = -1.0f; // inputs->NormalsUnpackAdd; } else { consts.NormalsUnpackMul = 2.0f; consts.NormalsUnpackAdd = -1.0f; } consts.BlurNumPasses = settings.qualityLevel == Cacao.Quality.Lowest ? 2 : 4; float additionalAngleOffset = settings.temporalSupersamplingAngleOffset; // if using temporal supersampling approach (like "Progressive Rendering Using Multi-frame Sampling" from GPU Pro 7, etc.) float additionalRadiusScale = settings.temporalSupersamplingRadiusOffset; // if using temporal supersampling approach (like "Progressive Rendering Using Multi-frame Sampling" from GPU Pro 7, etc.) for (int passId = 0; passId < 4; ++passId) { ref Vector4 perPassFullResUVOffset = ref consts.GetPerPassFullResUVOffset(passId); perPassFullResUVOffset.x = (float)(passId % 2) / bufferSizeInfo.SsaoBufferWidth; perPassFullResUVOffset.y = (float)(passId / 2) / bufferSizeInfo.SsaoBufferHeight; perPassFullResUVOffset.z = 0; perPassFullResUVOffset.w = 0; ref Cacao.PatternRotScaleMatrices patternRotScaleMatrices = ref consts.GetPatternRotScaleMatrices(passId); const int subPassCount = 5; for (int subPass = 0; subPass < subPassCount; subPass++) { int a = passId; int b = SpMap[subPass]; float angle0 = (a + (float)b / subPassCount) * Mathf.PI * 0.5f; float ca = Mathf.Cos(angle0); float sa = Mathf.Sin(angle0); float scale = 1.0f + (a - 1.5f + (b - (subPassCount - 1.0f) * 0.5f) / subPassCount) * 0.07f; ref Vector4 patternRotScaleMatrix = ref patternRotScaleMatrices.GetPatternRotScaleMatrix(subPass); patternRotScaleMatrix.x = scale * ca; patternRotScaleMatrix.y = scale * -sa; patternRotScaleMatrix.z = -scale * sa; patternRotScaleMatrix.w = -scale * ca; } } } private static readonly int[] SpMap = { 0, 1, 4, 3, 2 }; private static ComputeBuffer CreateConstantBuffer(string name) where TConstants: struct { return new ComputeBuffer(1, Marshal.SizeOf(), ComputeBufferType.Constant) { name = name }; } private static void DestroyConstantBuffer(ref ComputeBuffer bufferRef) { if (bufferRef == null) return; bufferRef.Release(); bufferRef = null; } private static void DestroyPass(ref TPass pass) where TPass: CacaoPass { if (pass == null) return; pass.Dispose(); pass = null; } } }