diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli index df1e7a85d5..7fa392ed42 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli @@ -1,6 +1,7 @@ #ifndef __EXPONENTIAL_HEIGHT_FOG_HLSLI__ #define __EXPONENTIAL_HEIGHT_FOG_HLSLI__ +#include "Common/Color.hlsli" #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" #include "ExponentialHeightFog/VolumetricFogCommon.hlsli" @@ -209,7 +210,7 @@ namespace ExponentialHeightFog float3 lightDirection = normalize(SharedData::DirLightDirection.xyz); float cosTheta = dot(lightDirection, viewDirection); float phase = HenyeyGreenstein(cosTheta, SharedData::exponentialHeightFogSettings.directionalInscatteringAnisotropy); - float3 directionalLightInscattering = SharedData::DirLightColor.xyz * phase; + float3 directionalLightInscattering = Color::GamutTransform(SharedData::DirLightColor.xyz) * phase; directionalInscattering = directionalLightInscattering * (1.0f - expFogFactor) * SharedData::exponentialHeightFogSettings.directionalInscatteringMultiplier; } diff --git a/features/HDR Display/Shaders/HDRDisplay/HDROutputCS.hlsl b/features/HDR Display/Shaders/HDRDisplay/HDROutputCS.hlsl index 8062a9b47e..2d4eb3da9b 100644 --- a/features/HDR Display/Shaders/HDRDisplay/HDROutputCS.hlsl +++ b/features/HDR Display/Shaders/HDRDisplay/HDROutputCS.hlsl @@ -35,14 +35,17 @@ cbuffer PerFrame : register(b0) bool hdrEnabled = enableHDR > 0.5; bool skipUI = skipUIComposite > 0.5; + bool isMainLoading = isMainOrLoadingMenu > 0.5; + bool postProcessOutput = SharedData::postProcessingSettings.DisableVanillaTonemapping != 0 && !isMainLoading; float3 finalColor; if (hdrEnabled) { - bool sceneIsLinear = isSceneLinear > 0.5; + bool sceneIsLinear = isSceneLinear > 0.5 || postProcessOutput; float3 outputColor = sceneIsLinear ? scene.xyz : Color::GammaToLinearSafe(scene.xyz); - outputColor = DisplayMapping::PumboAutoHDR(outputColor, SharedData::HDRData.z, SharedData::HDRData.y, 2.75, 1.0); + if (!postProcessOutput) // post processing output is already HDR + outputColor = DisplayMapping::PumboAutoHDR(outputColor, SharedData::HDRData.z, SharedData::HDRData.y, 2.75, 1.0); scene.xyz = sceneIsLinear ? outputColor : Color::LinearToGammaSafe(outputColor); float3 compositedColorLinear; @@ -53,11 +56,15 @@ cbuffer PerFrame : register(b0) compositedColorLinear = sceneLinear; } else { float3 uiLinear = Color::SrgbToLinear(max(0.0, ui.rgb)); - if (!(isMainOrLoadingMenu > 0.5)) { // UI and scene can't be separated in main menu or loading screen + if (!isMainLoading) { // UI and scene can't be separated in main menu or loading screen // scale UI brightness (multiplier based on paperWhite) uiLinear *= uiBrightness; } - compositedColorLinear = uiLinear + sceneLinear * (1.0 - ui.a); + if (postProcessOutput) { + compositedColorLinear = Color::BT709ToBT2020(uiLinear) + sceneLinear * (1.0 - ui.a); + } else { + compositedColorLinear = uiLinear + sceneLinear * (1.0 - ui.a); + } } } else { float3 sceneGamma = scene.rgb; @@ -66,7 +73,7 @@ cbuffer PerFrame : register(b0) compositedColorGamma = sceneGamma; } else { float3 uiGamma = ui.rgb; - if (!(isMainOrLoadingMenu > 0.5)) { // UI and scene can't be separated in main menu or loading screen + if (!isMainLoading) { // UI and scene can't be separated in main menu or loading screen // scale UI brightness (multiplier based on paperWhite) float3 uiLinear = Color::SrgbToLinear(max(0, uiGamma)); uiLinear *= uiBrightness; @@ -91,7 +98,8 @@ cbuffer PerFrame : register(b0) // Crop preview lives in the SDR menu buffer: emit sRGB instead of PQ. finalColor = saturate(Color::LinearToSrgb(max(0.0, compositedColorLinear))); } else { - compositedColorLinear = Color::BT709ToBT2020(compositedColorLinear); + if (!postProcessOutput) + compositedColorLinear = Color::BT709ToBT2020(compositedColorLinear); finalColor = Color::pq::Encode(max(0.0, compositedColorLinear), paperWhite); finalColor = saturate(finalColor); @@ -109,4 +117,4 @@ cbuffer PerFrame : register(b0) } HDROutput[dispatchID.xy] = float4(finalColor, 1.0); -} \ No newline at end of file +} diff --git a/features/Linear Lighting/Shaders/Features/LinearLighting.ini b/features/Linear Lighting/Shaders/Features/LinearLighting.ini index 9e325f8475..376684052a 100644 --- a/features/Linear Lighting/Shaders/Features/LinearLighting.ini +++ b/features/Linear Lighting/Shaders/Features/LinearLighting.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-2-0 +Version = 1-2-1 [Nexus] autoupload = false diff --git a/features/Post Processing/SKSE/Plugins/CommunityShaders/PostProcessing/default.json b/features/Post Processing/SKSE/Plugins/CommunityShaders/PostProcessing/default.json new file mode 100644 index 0000000000..1ee443bdad --- /dev/null +++ b/features/Post Processing/SKSE/Plugins/CommunityShaders/PostProcessing/default.json @@ -0,0 +1,276 @@ +{ + "Border": { + "enabled": false, + "settings": { + "BorderColor": [0.0, 0.0, 0.0], + "DepthThreshold": 0.0, + "Scale": [0.0, 0.0, 0.0, 0.0] + } + }, + "COD Bloom": { + "enabled": true, + "settings": { + "BlendFactor": 0.009999999776482582, + "MipBlendFactor": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0], + "Threshold": 0.0, + "UpsampleRadius": 5.0 + } + }, + "Camera": { + "enabled": false, + "settings": { + "CAStrength": 0.03999999910593033, + "FECrop": 0.0, + "FEFoV": 90.0, + "NoiseStrength": 0.07999999821186066, + "NoiseType": 0, + "UseFE": false + } + }, + "Color Grading and Tone Mapping": { + "enabled": true, + "settings": { + "cdlOffset": [0.0, 0.0, 0.0, 0.0], + "contrast": [1.0, 1.0, 1.0, 0.0], + "currentTonemapper": "GT7", + "enableTonemap": true, + "exposureTemperatureTint": [1.0, 65.0, 0.0, 0.0], + "gain": [1.0, 1.0, 1.0, 1.0], + "gameCinematicBlend": [0.0, 0.0, 0.0], + "gameFadeBlend": 1.0, + "gameTintBlend": 0.0, + "gamma": [0.0, 0.0, 0.0, 0.0], + "highlightsGain": [1.0, 1.0, 1.0, 0.0], + "highlightsOffset": [0.0, 0.0, 0.0, 0.0], + "inOutGamma": [1.0, 1.0, 1.0, 1.0], + "invertLog": true, + "lift": [0.0, 0.0, 0.0, 0.0], + "logType": 0, + "midtonesGain": [1.0, 1.0, 1.0, 0.0], + "midtonesOffset": [0.0, 0.0, 0.0, 0.0], + "oklchColorMixer": [ + [0.0, 1.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0] + ], + "oklchSaturation": [1.0, 1.0, 0.0, 0.0], + "pivot": [0.18000000715255737, 0.18000000715255737, 0.18000000715255737, 0.0], + "power": [1.0, 1.0, 1.0, 0.0], + "processColorSpace": 5, + "shadowsGain": [1.0, 1.0, 1.0, 0.0], + "shadowsHighlightsRange": [0.0, 0.30000001192092896, 0.550000011920929, 1.0], + "shadowsOffset": [0.0, 0.0, 0.0, 0.0], + "skipLDR": false, + "skipLUT": false, + "slope": [1.0, 1.0, 1.0, 0.0], + "tonemapParams": [ + [1.0, 9.999999747378752e-5, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0] + ], + "useLog": true, + "useOpenDrts": false + } + }, + "Composite": { + "enabled": true, + "settings": null + }, + "Depth of Field": { + "enabled": true, + "settings": { + "AutoFocus": true, + "BlurQuality": 7.0, + "BokehBusyFactor": 0.5, + "FNumber": 5.599999904632568, + "FarPlaneMaxBlur": 1.0, + "FocalLength": 50.0, + "FocusCoord": [0.5, 0.5], + "HighlightBoost": 0.0, + "HighlightShape": 0, + "HighlightShapeRotationAngle": 0.0, + "ManualFocusPlane": 0.4000000059604645, + "NearFarDistanceCompensation": 1.0, + "NearPlaneMaxBlur": 0.20000000298023224, + "PetzvalStrength": 0.0, + "PostBlurSmoothing": 0.0, + "TransitionSpeed": 0.5, + "consoleSelection": true, + "targetFocus": true, + "targetFocusFocalLength": 50.0 + } + }, + "Histogram Auto Exposure": { + "enabled": true, + "settings": { + "AdaptArea": [0.6000000238418579, 0.6000000238418579], + "AdaptSpeed": 1.5, + "AdaptationRange": [-2.0, 6.0], + "ExposureCompensation": 0.0, + "PurkinjeMaxEV": -1.0, + "PurkinjeStartEV": 1.5, + "PurkinjeStrength": 0.0 + } + }, + "LUT": { + "enabled": false, + "settings": { + "InputMax": [1.0, 1.0, 1.0], + "InputMin": [0.0, 0.0, 0.0], + "LutPath": "" + } + }, + "Lens Flare": { + "enabled": true, + "settings": { + "ApertureBlades": 6, + "ApertureRotation": 0.0, + "FFTResolution": 512, + "FStop": 14.300000190734863, + "GLocalMask": true, + "GhostChromaShift": 0.014999999664723873, + "GhostModeInt": 1, + "GhostStrength": 0.30000001192092896, + "Ghosts": [ + { + "Color": [1.0, 0.800000011920929, 0.4000000059604645, 1.0], + "Enabled": true, + "KernelScale": 1.0, + "Scale": -1.5 + }, + { + "Color": [1.0, 1.0, 0.6000000238418579, 1.0], + "Enabled": true, + "KernelScale": 1.0, + "Scale": 2.5 + }, + { + "Color": [0.800000011920929, 0.800000011920929, 1.0, 1.0], + "Enabled": true, + "KernelScale": 1.0, + "Scale": -5.0 + }, + { + "Color": [0.5, 1.0, 0.4000000059604645, 1.0], + "Enabled": true, + "KernelScale": 1.0, + "Scale": 10.0 + }, + { + "Color": [0.5, 0.800000011920929, 1.0, 1.0], + "Enabled": true, + "KernelScale": 1.0, + "Scale": 0.699999988079071 + }, + { + "Color": [0.8999999761581421, 1.0, 0.800000011920929, 1.0], + "Enabled": true, + "KernelScale": 1.0, + "Scale": -0.4000000059604645 + }, + { + "Color": [1.0, 0.800000011920929, 0.4000000059604645, 1.0], + "Enabled": true, + "KernelScale": 1.0, + "Scale": -0.20000000298023224 + }, + { + "Color": [0.8999999761581421, 0.699999988079071, 0.699999988079071, 1.0], + "Enabled": true, + "KernelScale": 1.0, + "Scale": -0.10000000149011612 + } + ], + "HaloChromaShift": 0.014999999664723873, + "HaloCompression": 0.6499999761581421, + "HaloRadius": 0.5, + "HaloStrength": 0.20000000298023224, + "HaloWidth": 0.5, + "Intensity": 0.25, + "KernelScale": 0.10000000149011612, + "ThresholdEV": 3.0, + "ThresholdRange": 1.0, + "Tint": [1.0, 0.8500000238418579, 0.699999988079071] + } + }, + "Local Exposure": { + "enabled": true, + "settings": { + "BoostLocalContrast": false, + "DisplayMip": 2, + "Exposure": 0.699999988079071, + "ExposurePreferenceSigma": 5.0, + "Highlights": 1.5, + "Mip": 6, + "Shadows": 1.0 + } + }, + "Motion Blur": { + "enabled": false, + "settings": { + "SampleCount": 8, + "ScalePreset": 4, + "VelocityScale": 300.0 + } + }, + "Physical Glare": { + "enabled": false, + "settings": { + "AdaptSpeed": 3.0, + "ApertureBlades": 6, + "ApertureMode": 0, + "ApertureRotation": 0.0, + "BladeRoughnessAmp": 0.30000001192092896, + "BladeRoughnessFreq": 20, + "ChromaticSpread": 1.0, + "DustCount": 295, + "DustSize": 2.4000000953674316, + "EnableEyelashes": true, + "EyelashCount": 40, + "EyelashCurvature": 0.30000001192092896, + "EyelashLength": 0.4000000059604645, + "FFTResolution": 1024, + "FStop": 2.799999952316284, + "FresnelExponent": 0.0, + "GratingCount": 200, + "GratingStrength": 0.5, + "Intensity": 0.10000000149011612, + "KernelScale": 1.0, + "PSFNoiseFloor": 0.0010000000474974513, + "PSFSharpness": 0.49000000953674316, + "PaddingRatio": 0.25, + "ParticleCount": 200, + "ParticleSize": 1.5, + "ScatterStrength": 1.0, + "ScratchCount": 4, + "ScratchLength": 0.7900000214576721, + "ScratchOpacity": 0.3100000023841858, + "ScratchWidth": 2.0, + "SphericalAberration": 0.0, + "StarburstCount": 48, + "StarburstIrregularity": 0.30000001192092896, + "StarburstStrength": 0.800000011920929, + "SutureBranches": 3, + "SutureStrength": 0.5, + "SutureWidth": 2.0, + "TearFilmComplexity": 8, + "TearFilmSpeed": 2.0, + "TearFilmStrength": 0.7799999713897705, + "ThresholdEV": 8.0 + } + }, + "Vignette": { + "enabled": true, + "settings": { + "FocalLength": 1.0, + "Power": 3.0 + } + }, + "ppsettings": { + "DisableVanillaTonemapping": 1 + }, + "preset_name": "default" +} diff --git a/features/Post Processing/Shaders/Features/PostProcessing.ini b/features/Post Processing/Shaders/Features/PostProcessing.ini new file mode 100644 index 0000000000..19f01444dc --- /dev/null +++ b/features/Post Processing/Shaders/Features/PostProcessing.ini @@ -0,0 +1,2 @@ +[Info] +Version = 1-0-0 \ No newline at end of file diff --git a/features/Post Processing/Shaders/PostProcessing/Border/border.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/Border/border.cs.hlsl new file mode 100644 index 0000000000..9f04ad1e28 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/Border/border.cs.hlsl @@ -0,0 +1,30 @@ +#include "Common/SharedData.hlsli" + +Texture2D InputTexture : register(t0); +Texture2D DepthTexture : register(t1); + +RWTexture2D OutputTexture : register(u0); +RWTexture2D MotionTexture : register(u1); + +cbuffer BorderCB : register(b1) +{ + float4 BorderColor; // color in xyz, depth threshold in w + float4 Scale; // xyzw: up, down, left, right +}; + +[numthreads(8, 8, 1)] void main(uint3 DTid : SV_DispatchThreadID) { + float depth = DepthTexture[DTid.xy]; + float3 borderColor = BorderColor.xyz; + float depthThreshold = BorderColor.w; + if (depth > depthThreshold || depthThreshold == 0.0f) { + float2 uv = (DTid.xy + 0.5f) * SharedData::BufferDim.zw; + if (uv.y < Scale.x || uv.y > (1 - Scale.y) || uv.x < Scale.z || uv.x > (1 - Scale.w)) { + OutputTexture[DTid.xy] = float4(borderColor, 1.0); + MotionTexture[DTid.xy] = float4(0.0, 0.0, 0.0, 1.0); + } else { + OutputTexture[DTid.xy] = InputTexture[DTid.xy]; + } + } else { + OutputTexture[DTid.xy] = InputTexture[DTid.xy]; + } +} \ No newline at end of file diff --git a/features/Post Processing/Shaders/PostProcessing/Border/border_clear_mv.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/Border/border_clear_mv.cs.hlsl new file mode 100644 index 0000000000..21bd708763 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/Border/border_clear_mv.cs.hlsl @@ -0,0 +1,30 @@ +#include "Common/SharedData.hlsli" + +Texture2D DepthTexture : register(t0); + +RWTexture2D MotionTexture : register(u0); + +cbuffer BorderCB : register(b1) +{ + float4 BorderColor; // color in xyz, depth threshold in w + float4 Scale; // xyzw: up, down, left, right +}; + +[numthreads(8, 8, 1)] void main(uint3 DTid : SV_DispatchThreadID) { + // This shader runs before upscaling, so account for dynamic resolution. + float2 dynResDim = SharedData::BufferDim.xy * FrameBuffer::DynamicResolutionParams1.xy; + + // Early exit for pixels outside the dynamic resolution area + if (any(DTid.xy >= uint2(dynResDim))) + return; + + float depth = DepthTexture[DTid.xy]; + float depthThreshold = BorderColor.w; + if (depth > depthThreshold || depthThreshold == 0.0f) { + // UV relative to the dynamic resolution viewport [0, 1] + float2 uv = (DTid.xy + 0.5f) / dynResDim; + if (uv.y < Scale.x || uv.y > (1 - Scale.y) || uv.x < Scale.z || uv.x > (1 - Scale.w)) { + MotionTexture[DTid.xy] = float4(0.0, 0.0, 0.0, 1.0); + } + } +} diff --git a/features/Post Processing/Shaders/PostProcessing/CODBloom/bloom.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/CODBloom/bloom.cs.hlsl new file mode 100644 index 0000000000..f1348c1bb8 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/CODBloom/bloom.cs.hlsl @@ -0,0 +1,100 @@ +/// By ProfJack/五脚猫, 2024-2-28 UTC +/// ref: +/// http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare + +#include "PostProcessing/common.hlsli" + +Texture2D TexColor : register(t0); +Texture2D TexBloomIn : register(t1); + +RWTexture2D RWTexBloomOut : register(u0); + +cbuffer BloomCB : register(b1) +{ + // threshold + float Threshold : packoffset(c0.x); + // upsample + float UpsampleRadius : packoffset(c0.y); + float UpsampleMult : packoffset(c0.z); // in composite: bloom mult + float CurrentMipMult : packoffset(c0.w); +}; + +SamplerState SampColor : register(s0); + +bool3 IsNaN(float3 x) +{ + return !(x < 0.f || x > 0.f || x == 0.f); +} + +float3 Sanitise(float3 v) +{ + bool3 err = IsNaN(v) || (v < 0); + v.x = err.x ? 0 : v.x; + v.y = err.y ? 0 : v.y; + v.z = err.z ? 0 : v.z; + return v; +} + +float3 ThresholdColor(float3 col, float threshold) +{ + float luma = Color::RGBToLuminance(col); + if (luma < 1e-3) + return 0; + return col * (max(0, luma - threshold) / luma); +} + +float4 UpsampleCOD(Texture2D tex, float2 uv, float2 radius) +{ + float4 retval = 0; + for (int x = -1; x <= 1; ++x) + for (int y = -1; y <= 1; ++y) + retval += (1 << (!x + !y)) * 0.0625 * tex.SampleLevel(SampColor, uv + float2(x, y) * radius, 0); + return retval; +} + +[numthreads(32, 32, 1)] void CS_Threshold(uint2 tid : SV_DispatchThreadID) { + float3 col_input = TexColor[tid].rgb; + + float3 col = col_input; + col = Sanitise(col); + col = ThresholdColor(col, Threshold.x); + RWTexBloomOut[tid] = float4(col, 1); +}; + +[numthreads(32, 32, 1)] void CS_Downsample(uint2 tid : SV_DispatchThreadID) { + uint2 dims; + RWTexBloomOut.GetDimensions(dims.x, dims.y); + + float2 px_size = rcp(dims); + float2 uv = (tid + .5) * px_size; + +#ifdef FIRST_MIP + float3 col = DownsampleCODFirstMip(TexBloomIn, SampColor, uv, px_size).rgb; +#else + float3 col = DownsampleCOD(TexBloomIn, SampColor, uv, px_size).rgb; +#endif + RWTexBloomOut[tid] = float4(col, 1); +}; + +[numthreads(32, 32, 1)] void CS_Upsample(uint2 tid : SV_DispatchThreadID) { + uint2 dims; + RWTexBloomOut.GetDimensions(dims.x, dims.y); + + float2 px_size = rcp(dims); + float2 uv = (tid + .5) * px_size; + + float3 col = RWTexBloomOut[tid].rgb * CurrentMipMult + UpsampleCOD(TexBloomIn, uv, px_size * UpsampleRadius).rgb * UpsampleMult; + RWTexBloomOut[tid] = float4(col, 1); +}; + +[numthreads(32, 32, 1)] void CS_Composite(uint2 tid : SV_DispatchThreadID) { + uint2 dims; + RWTexBloomOut.GetDimensions(dims.x, dims.y); + + float2 px_size = rcp(dims); + float2 uv = (tid + .5) * px_size; + + float3 col = TexColor[tid].rgb + UpsampleCOD(TexBloomIn, uv, px_size * UpsampleRadius).rgb * UpsampleMult; + + RWTexBloomOut[tid] = float4(col, 1); +}; \ No newline at end of file diff --git a/features/Post Processing/Shaders/PostProcessing/Camera/camera.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/Camera/camera.cs.hlsl new file mode 100644 index 0000000000..0958af8d33 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/Camera/camera.cs.hlsl @@ -0,0 +1,147 @@ +// Camera effects for Community Shaders +// Film grain based on https://www.shadertoy.com/view/3sGSWV (MIT License) + +#include "Common/Math.hlsli" +#include "Common/SharedData.hlsli" +#include "PostProcessing/common.hlsli" + +cbuffer CameraCB : register(b1) +{ + // Fisheye + float FEFoV; + float FECrop; + + // Chromatic aberration + float CAStrength; + + // Noise + float NoiseStrength; + int NoiseType; + float2 ScreenSize; + + bool UseFE; +} + +Texture2D InputTexture : register(t0); + +SamplerState ColorSampler : register(s0); + +RWTexture2D OutputTexture : register(u0); + +#define BUFFER_ASPECT_RATIO ScreenSize.x / ScreenSize.y +#define ASPECT_RATIO float2(BUFFER_ASPECT_RATIO, 1.0) + +// Film grain helper functions +// From Dave Hoskins: https://www.shadertoy.com/view/4djSRW +float GrainHash(float3 p3) +{ + p3 = frac(p3 * 0.1031); + p3 += dot(p3, p3.yzx + 19.19); + return frac((p3.x + p3.y) * p3.z); +} + +// From iq: https://www.shadertoy.com/view/4sfGzS +float GrainNoise(float3 x) +{ + float3 i = floor(x); + float3 f = frac(x); + f = f * f * (3.0 - 2.0 * f); + return lerp(lerp(lerp(GrainHash(i + float3(0, 0, 0)), + GrainHash(i + float3(1, 0, 0)), f.x), + lerp(GrainHash(i + float3(0, 1, 0)), + GrainHash(i + float3(1, 1, 0)), f.x), + f.y), + lerp(lerp(GrainHash(i + float3(0, 0, 1)), + GrainHash(i + float3(1, 0, 1)), f.x), + lerp(GrainHash(i + float3(0, 1, 1)), + GrainHash(i + float3(1, 1, 1)), f.x), + f.y), + f.z); +} + +// Slightly high-passed continuous value-noise +float GrainSource(float3 x, float strength, float pitch) +{ + float center = GrainNoise(x); + float v1 = center - GrainNoise(float3(1, 0, 0) / pitch + x) + 0.5; + float v2 = center - GrainNoise(float3(0, 1, 0) / pitch + x) + 0.5; + float v3 = center - GrainNoise(float3(-1, 0, 0) / pitch + x) + 0.5; + float v4 = center - GrainNoise(float3(0, -1, 0) / pitch + x) + 0.5; + + float total = (v1 + v2 + v3 + v4) * 0.25; + return lerp(1.0, 0.5 + total, strength); +} + +float2 FishEye(float2 texcoord, float FEFoV, float FECrop) +{ + float2 radiant_vector = texcoord - 0.5; + float diagonal_length = length(ASPECT_RATIO); + + float fov_factor = Math::PI * float(FEFoV) / 360.0; + + float fit_fov = sin(atan(tan(fov_factor) * diagonal_length)); + float crop_value = lerp(1.0 + (diagonal_length - 1.0) * cos(fov_factor), diagonal_length, FECrop * pow(abs(sin(fov_factor)), 6.0)); + + // Circularize radiant vector and apply cropping + float2 cn_radiant_vector = 2.0 * radiant_vector * ASPECT_RATIO / crop_value * fit_fov; + + if (length(cn_radiant_vector) < 1.0) { + float z = sqrt(1.0 - cn_radiant_vector.x * cn_radiant_vector.x - cn_radiant_vector.y * cn_radiant_vector.y); + float theta = acos(z) / fov_factor; + + float2 d = normalize(cn_radiant_vector); + texcoord = (theta * d) / (2.0 * ASPECT_RATIO) + 0.5; + } + + return texcoord; +} + +[numthreads(8, 8, 1)] void CS_Camera(uint3 DTid : SV_DispatchThreadID) { + static const float2 TEXEL_SIZE = float2(1.0f / ScreenSize.x, 1.0f / ScreenSize.y); + float2 texcoord = (DTid.xy + 0.5f) * TEXEL_SIZE; + float2 texcoord_clean = texcoord.xy; + + // Fisheye + if (UseFE) { + texcoord.xy = FishEye(texcoord_clean, FEFoV, FECrop); + } + + float3 color = InputTexture.SampleLevel(ColorSampler, texcoord, 0).rgb; + + // Chromatic aberration + [branch] if (CAStrength != 0.0) + { + color = SampleCA(InputTexture, ColorSampler, texcoord, CAStrength, 0).rgb; + } + + // Film grain + [branch] if (NoiseStrength != 0.0) + { + float2 pixelCoord = DTid.xy; + float t = float(SharedData::FrameCount); + + static const float GRAIN_RATE = 1.0; + static const float GRAIN_PITCH = 1.0; + static const float GRAIN_LIFT_RATIO = 0.5; + + float3 grain; + if (NoiseType == 1) { + // Color grain + float rg = GrainSource(float3(pixelCoord, floor(GRAIN_RATE * t)), NoiseStrength, GRAIN_PITCH); + float gg = GrainSource(float3(pixelCoord, floor(GRAIN_RATE * (t + 9.0))), NoiseStrength, GRAIN_PITCH); + float bg = GrainSource(float3(pixelCoord, floor(GRAIN_RATE * (t - 9.0))), NoiseStrength, GRAIN_PITCH); + + static const float COLOR_LEVEL = 1.0; + float3 color_grain = float3(rg, gg, bg); + grain = lerp(dot(color_grain, float3(0.2126, 0.7152, 0.0722)).xxx, color_grain, COLOR_LEVEL); + } else { + // Monochrome film grain + static const float NEUTRAL_GRAIN_FACTOR = 1.4142135; // sqrt(2) + grain = GrainSource(float3(pixelCoord, floor(GRAIN_RATE * t)), NoiseStrength / NEUTRAL_GRAIN_FACTOR, GRAIN_PITCH).xxx; + } + + color = max(lerp(color * grain, color + (grain - 1.0), GRAIN_LIFT_RATIO), 0.0); + } + + OutputTexture[DTid.xy] = float4(color, 1.0f); +} diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/GT7ToneMapping.hlsli b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/GT7ToneMapping.hlsli new file mode 100644 index 0000000000..f836b5bb10 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/GT7ToneMapping.hlsli @@ -0,0 +1,379 @@ +// Kenichiro Yasutomi, Kentaro Suzuki and Hajime Uchimura 2025 +// "Driving Toward Reality: Physically Based Tone Mapping and Perceptual Fidelity in Gran Turismo 7" +// ----- +// MIT License +// +// Copyright (c) 2025 Polyphony Digital Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef GT7_TONE_MAPPING_HLSLI +#define GT7_TONE_MAPPING_HLSLI + +// ----------------------------------------------------------------------------- +// Configuration +// ----------------------------------------------------------------------------- +#define TONE_MAPPING_UCS_ICTCP 0 +#define TONE_MAPPING_UCS_JZAZBZ 1 +#define TONE_MAPPING_UCS TONE_MAPPING_UCS_ICTCP + +#define GRAN_TURISMO_SDR_PAPER_WHITE 250.0f // cd/m^2 +#define REFERENCE_LUMINANCE 100.0f // cd/m^2 <-> 1.0f +#define JZAZBZ_EXPONENT_SCALE_FACTOR 1.7f + +// ----------------------------------------------------------------------------- +// Luminance conversion helpers +// ----------------------------------------------------------------------------- +float frameBufferValueToPhysicalValue(float fbValue) +{ + return fbValue * REFERENCE_LUMINANCE; +} + +float physicalValueToFrameBufferValue(float physical) +{ + return physical / REFERENCE_LUMINANCE; +} + +// ----------------------------------------------------------------------------- +// Utility functions +// ----------------------------------------------------------------------------- +float smoothStep(float x, float edge0, float edge1) +{ + float t = (x - edge0) / (edge1 - edge0); + t = saturate(t); + return t * t * (3.0f - 2.0f * t); +} + +float chromaCurve(float x, float a, float b) +{ + return 1.0f - smoothStep(x, a, b); +} + +// ----------------------------------------------------------------------------- +// GT Tone Mapping Curve Structure +// ----------------------------------------------------------------------------- +struct GTToneMappingCurveV2 +{ + float peakIntensity; + float alpha; + float midPoint; + float linearSection; + float toeStrength; + float kA, kB, kC; +}; + +GTToneMappingCurveV2 initializeCurve(float monitorIntensity, float alpha, + float grayPoint, float linearSection, float toeStrength) +{ + GTToneMappingCurveV2 curve; + curve.peakIntensity = monitorIntensity; + curve.alpha = alpha; + curve.midPoint = grayPoint; + curve.linearSection = linearSection; + curve.toeStrength = toeStrength; + + // Pre-compute constants for the shoulder region + float k = (linearSection - 1.0f) / (alpha - 1.0f); + curve.kA = monitorIntensity * linearSection + monitorIntensity * k; + curve.kB = -monitorIntensity * k * exp(linearSection / k); + curve.kC = -1.0f / (k * monitorIntensity); + + return curve; +} + +float evaluateCurve(GTToneMappingCurveV2 curve, float x) +{ + if (x < 0.0f) + return 0.0f; + + float weightLinear = smoothStep(x, 0.0f, curve.midPoint); + float weightToe = 1.0f - weightLinear; + + // Shoulder mapping for highlights + float shoulder = curve.kA + curve.kB * exp(x * curve.kC); + + if (x < curve.linearSection * curve.peakIntensity) { + float toeMapped = curve.midPoint * pow(x / curve.midPoint, curve.toeStrength); + return weightToe * toeMapped + weightLinear * x; + } else { + return shoulder; + } +} + +// ----------------------------------------------------------------------------- +// ST-2084 (PQ) EOTF Functions +// ----------------------------------------------------------------------------- +float eotfSt2084(float n, float exponentScaleFactor = 1.0f) +{ + n = saturate(n); + + const float m1 = 0.1593017578125f; + const float m2 = 78.84375f * exponentScaleFactor; + const float c1 = 0.8359375f; + const float c2 = 18.8515625f; + const float c3 = 18.6875f; + const float pqC = 10000.0f; + + float np = pow(n, 1.0f / m2); + float l = max(np - c1, 0.0f); + l = l / (c2 - c3 * np); + l = pow(l, 1.0f / m1); + + return physicalValueToFrameBufferValue(l * pqC); +} + +float inverseEotfSt2084(float v, float exponentScaleFactor = 1.0f) +{ + const float m1 = 0.1593017578125f; + const float m2 = 78.84375f * exponentScaleFactor; + const float c1 = 0.8359375f; + const float c2 = 18.8515625f; + const float c3 = 18.6875f; + const float pqC = 10000.0f; + + float physical = frameBufferValueToPhysicalValue(v); + float y = physical / pqC; + + float ym = pow(y, m1); + return exp2(m2 * (log2(c1 + c2 * ym) - log2(1.0f + c3 * ym))); +} + +// ----------------------------------------------------------------------------- +// ICtCp Color Space Conversion +// ----------------------------------------------------------------------------- +float3 rgbToICtCp(float3 rgb) +{ + float l = (rgb.r * 1688.0f + rgb.g * 2146.0f + rgb.b * 262.0f) / 4096.0f; + float m = (rgb.r * 683.0f + rgb.g * 2951.0f + rgb.b * 462.0f) / 4096.0f; + float s = (rgb.r * 99.0f + rgb.g * 309.0f + rgb.b * 3688.0f) / 4096.0f; + + float lPQ = inverseEotfSt2084(l); + float mPQ = inverseEotfSt2084(m); + float sPQ = inverseEotfSt2084(s); + + float3 ictCp; + ictCp.x = (2048.0f * lPQ + 2048.0f * mPQ) / 4096.0f; + ictCp.y = (6610.0f * lPQ - 13613.0f * mPQ + 7003.0f * sPQ) / 4096.0f; + ictCp.z = (17933.0f * lPQ - 17390.0f * mPQ - 543.0f * sPQ) / 4096.0f; + + return ictCp; +} + +float3 iCtCpToRgb(float3 ictCp) +{ + float l = ictCp.x + 0.00860904f * ictCp.y + 0.11103f * ictCp.z; + float m = ictCp.x - 0.00860904f * ictCp.y - 0.11103f * ictCp.z; + float s = ictCp.x + 0.560031f * ictCp.y - 0.320627f * ictCp.z; + + float lLin = eotfSt2084(l); + float mLin = eotfSt2084(m); + float sLin = eotfSt2084(s); + + float3 rgb; + rgb.r = max(3.43661f * lLin - 2.50645f * mLin + 0.0698454f * sLin, 0.0f); + rgb.g = max(-0.79133f * lLin + 1.9836f * mLin - 0.192271f * sLin, 0.0f); + rgb.b = max(-0.0259499f * lLin - 0.0989137f * mLin + 1.12486f * sLin, 0.0f); + + return rgb; +} + +// ----------------------------------------------------------------------------- +// Jzazbz Color Space Conversion +// ----------------------------------------------------------------------------- +float3 rgbToJzazbz(float3 rgb) +{ + float l = rgb.r * 0.530004f + rgb.g * 0.355704f + rgb.b * 0.086090f; + float m = rgb.r * 0.289388f + rgb.g * 0.525395f + rgb.b * 0.157481f; + float s = rgb.r * 0.091098f + rgb.g * 0.147588f + rgb.b * 0.734234f; + + float lPQ = inverseEotfSt2084(l, JZAZBZ_EXPONENT_SCALE_FACTOR); + float mPQ = inverseEotfSt2084(m, JZAZBZ_EXPONENT_SCALE_FACTOR); + float sPQ = inverseEotfSt2084(s, JZAZBZ_EXPONENT_SCALE_FACTOR); + + float iz = 0.5f * lPQ + 0.5f * mPQ; + + float3 jab; + jab.x = (0.44f * iz) / (1.0f - 0.56f * iz) - 1.6295499532821566e-11f; + jab.y = 3.524000f * lPQ - 4.066708f * mPQ + 0.542708f * sPQ; + jab.z = 0.199076f * lPQ + 1.096799f * mPQ - 1.295875f * sPQ; + + return jab; +} + +float3 jzazbzToRgb(float3 jab) +{ + float jz = jab.x + 1.6295499532821566e-11f; + float iz = jz / (0.44f + 0.56f * jz); + float a = jab.y; + float b = jab.z; + + float l = iz + a * 1.386050432715393e-1f + b * 5.804731615611869e-2f; + float m = iz + a * -1.386050432715393e-1f + b * -5.804731615611869e-2f; + float s = iz + a * -9.601924202631895e-2f + b * -8.118918960560390e-1f; + + float lLin = eotfSt2084(l, JZAZBZ_EXPONENT_SCALE_FACTOR); + float mLin = eotfSt2084(m, JZAZBZ_EXPONENT_SCALE_FACTOR); + float sLin = eotfSt2084(s, JZAZBZ_EXPONENT_SCALE_FACTOR); + + float3 rgb; + rgb.r = lLin * 2.990669f + mLin * -2.049742f + sLin * 0.088977f; + rgb.g = lLin * -1.634525f + mLin * 3.145627f + sLin * -0.483037f; + rgb.b = lLin * -0.042505f + mLin * -0.377983f + sLin * 1.448019f; + + return rgb; +} + +// ----------------------------------------------------------------------------- +// Unified Color Space Functions +// ----------------------------------------------------------------------------- +float3 rgbToUcs(float3 rgb) +{ +#if TONE_MAPPING_UCS == TONE_MAPPING_UCS_ICTCP + return rgbToICtCp(rgb); +#elif TONE_MAPPING_UCS == TONE_MAPPING_UCS_JZAZBZ + return rgbToJzazbz(rgb); +#endif +} + +float3 ucsToRgb(float3 ucs) +{ +#if TONE_MAPPING_UCS == TONE_MAPPING_UCS_ICTCP + return iCtCpToRgb(ucs); +#elif TONE_MAPPING_UCS == TONE_MAPPING_UCS_JZAZBZ + return jzazbzToRgb(ucs); +#endif +} + +// ----------------------------------------------------------------------------- +// GT7 Tone Mapping Structure +// ----------------------------------------------------------------------------- +struct GT7ToneMapper +{ + float sdrCorrectionFactor; + float framebufferLuminanceTarget; + float framebufferLuminanceTargetUcs; + GTToneMappingCurveV2 curve; + float blendRatio; + float fadeStart; + float fadeEnd; +}; + +GT7ToneMapper initializeHDR(float physicalTargetLuminance) +{ + GT7ToneMapper toneMapper; + toneMapper.sdrCorrectionFactor = 1.0f; + toneMapper.framebufferLuminanceTarget = physicalValueToFrameBufferValue(physicalTargetLuminance); + + // Initialize curve with GT7 parameters + toneMapper.curve = initializeCurve(toneMapper.framebufferLuminanceTarget, 0.25f, 0.538f, 0.444f, 1.280f); + + // Default blend parameters + toneMapper.blendRatio = 0.6f; + toneMapper.fadeStart = 0.98f; + toneMapper.fadeEnd = 1.16f; + + // Calculate UCS target luminance + float3 rgb = float3(toneMapper.framebufferLuminanceTarget, + toneMapper.framebufferLuminanceTarget, + toneMapper.framebufferLuminanceTarget); + float3 ucs = rgbToUcs(rgb); + toneMapper.framebufferLuminanceTargetUcs = ucs.x; + + return toneMapper; +} + +GT7ToneMapper initializeSDR() +{ + GT7ToneMapper toneMapper; + toneMapper.sdrCorrectionFactor = 1.0f / physicalValueToFrameBufferValue(GRAN_TURISMO_SDR_PAPER_WHITE); + toneMapper.framebufferLuminanceTarget = physicalValueToFrameBufferValue(GRAN_TURISMO_SDR_PAPER_WHITE); + + // Initialize curve with GT7 parameters + toneMapper.curve = initializeCurve(toneMapper.framebufferLuminanceTarget, 0.25f, 0.538f, 0.444f, 1.280f); + + // Default blend parameters + toneMapper.blendRatio = 0.6f; + toneMapper.fadeStart = 0.98f; + toneMapper.fadeEnd = 1.16f; + + // Calculate UCS target luminance + float3 rgb = float3(toneMapper.framebufferLuminanceTarget, + toneMapper.framebufferLuminanceTarget, + toneMapper.framebufferLuminanceTarget); + float3 ucs = rgbToUcs(rgb); + toneMapper.framebufferLuminanceTargetUcs = ucs.x; + + return toneMapper; +} + +// ----------------------------------------------------------------------------- +// Main Tone Mapping Function +// ----------------------------------------------------------------------------- +float3 applyGT7ToneMapping(GT7ToneMapper toneMapper, float3 rgb) +{ + // Convert to UCS to separate luminance and chroma + float3 ucs = rgbToUcs(rgb); + + // Per-channel tone mapping ("skewed" color) + float3 skewedRgb = float3( + evaluateCurve(toneMapper.curve, rgb.r), + evaluateCurve(toneMapper.curve, rgb.g), + evaluateCurve(toneMapper.curve, rgb.b)); + + float3 skewedUcs = rgbToUcs(skewedRgb); + + float chromaScale = chromaCurve(ucs.x / toneMapper.framebufferLuminanceTargetUcs, + toneMapper.fadeStart, toneMapper.fadeEnd); + + float3 scaledUcs = float3( + skewedUcs.x, + ucs.y * chromaScale, + ucs.z * chromaScale); + + // Convert back to RGB + float3 scaledRgb = ucsToRgb(scaledUcs); + + // Final blend between per-channel and UCS-scaled results + float3 blended = lerp(skewedRgb, scaledRgb, toneMapper.blendRatio); + + // Apply SDR correction factor and clamp + float3 result = toneMapper.sdrCorrectionFactor * min(blended, toneMapper.framebufferLuminanceTarget); + + return result; +} + +// ----------------------------------------------------------------------------- +// Convenience Functions +// ----------------------------------------------------------------------------- + +// Apply GT7 tone mapping for HDR output +float3 GT7ToneMappingHDR(float3 linearRgb, float targetLuminanceNits) +{ + GT7ToneMapper toneMapper = initializeHDR(targetLuminanceNits); + return applyGT7ToneMapping(toneMapper, linearRgb); +} + +// Apply GT7 tone mapping for SDR output +float3 GT7ToneMappingSDR(float3 linearRgb) +{ + GT7ToneMapper toneMapper = initializeSDR(); + return applyGT7ToneMapping(toneMapper, linearRgb); +} + +#endif // GT7_TONE_MAPPING_HLSLI \ No newline at end of file diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/OpenDRT.hlsli b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/OpenDRT.hlsli new file mode 100644 index 0000000000..8823cac3b7 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/OpenDRT.hlsli @@ -0,0 +1,595 @@ +// OpenDRT v1.1.0 HLSL compute shader conversion. +// Original DCTL written by Jed Smith: https://github.com/jedypod/open-display-transform +// License: GPLv3 +#include "Common/Math.hlsli" + +/****************************************************************** + Color Conversion Matrices + ******************************************************************/ +// Linear ACEScg (AP1) to XYZ D65 (CAT02). +#define matrix_ap1_to_xyz float3x3(float3(0.652418717671912951f, 0.127179925537538263f, 0.170857283842220459f), float3(0.268064059194271287f, 0.672464478992617742f, 0.0594714618131108388f), float3(-0.0054699285104975676f, 0.00518279997697511721f, 1.08934487929340107f)) +// Linear Rec.709/sRGB to XYZ D65. +#define matrix_rec709_to_xyz float3x3(float3(0.412390799265959229f, 0.357584339383878125f, 0.180480788401834347f), float3(0.212639005871510217f, 0.71516867876775625f, 0.0721923153607337414f), float3(0.0193308187155918181f, 0.119194779794626018f, 0.950532152249661033f)) +// XYZ D65 to linear Rec.709/sRGB. +#define matrix_xyz_to_rec709 float3x3(float3(3.24096994190452348f, -1.53738317757009435f, -0.498610760293003552f), float3(-0.969243636280879506f, 1.87596750150771996f, 0.0415550574071755843f), float3(0.0556300796969936354f, -0.20397695888897649f, 1.05697151424287816f)) +// P3D65 to XYZ D65. +#define matrix_p3d65_to_xyz float3x3(float3(0.486570948648216151f, 0.265667693169093f, 0.198217285234362467f), float3(0.228974564069748754f, 0.691738521836506193f, 0.079286914093744984f), float3(-4.00000000000000029e-17f, 0.0451133818589026167f, 1.04394436890097575f)) +// XYZ D65 to P3D65. +#define matrix_xyz_to_p3d65 float3x3(float3(2.49349691194142542f, -0.93138361791912383f, -0.402710784450716841f), float3(-0.829488969561574696f, 1.76266406031834655f, 0.0236246858419435941f), float3(0.0358458302437844531f, -0.0761723892680418041f, 0.956884524007687309f)) +// P3D65 to Rec2020 +#define matrix_p3_to_rec2020 float3x3(float3(0.753833034361722221f, 0.198597369052616435f, 0.0475695965856618441f), float3(0.0457438489653582137f, 0.9417772198116936f, 0.0124789312229481135f), float3(-0.0012103403545183941f, 0.0176017173010899926f, 0.983608623053428777f)) + +/****************************************************************** + CAT02 Chromatic Adaptation Matrices + ******************************************************************/ +// D65 to D93 : [0.3127, 0.329] to [0.283, 0.297] +#define matrix_cat_d65_to_d93 float3x3(float3(0.95703423023223877f, -0.0247171502560377121f, 0.0624028593301773071f), float3(-0.0179296955466270447f, 0.990019857883453369f, 0.0248119533061981201f), float3(0.00127589143812656403f, 0.00427919067442417058f, 1.29345715045928955f)) +// D65 to D75 : [0.3127, 0.329] to [0.29903, 0.31488] +#define matrix_cat_d65_to_d75 float3x3(float3(0.981001079082489014f, -0.0116619253531098366f, 0.0265614092350006104f), float3(-0.00843488052487373352f, 0.996506094932556152f, 0.0105696544051170349f), float3(0.000552809564396739006f, 0.00179840810596942902f, 1.12374722957611084f)) +// D65 to D60 : [0.3127, 0.329] to [0.32162624, 0.337737] +#define matrix_cat_d65_to_d60 float3x3(float3(1.01182246208190918f, 0.00778879318386316299f, -0.0157783031463623047f), float3(0.00561682833358645439f, 1.00150644779205322f, -0.00628517568111419678f), float3(-0.000335735734552145004f, -0.0010509500280022619f, 0.927366673946380615f)) +// D65 to D55 : [0.3127, 0.329] to [0.33243, 0.34744] +#define matrix_cat_d65_to_d55 float3x3(float3(1.02585089206695557f, 0.0179439820349216461f, -0.0332137793302536011f), float3(0.0129133854061365128f, 1.00214779376983643f, -0.0132421031594276428f), float3(-0.000719940289855003032f, -0.00218106806278228803f, 0.84868013858795166f)) +// D65 to D50 : [0.3127, 0.329] to [0.3457, 0.3585] +#define matrix_cat_d65_to_d50 float3x3(float3(1.04257404804229736f, 0.03089117631316185f, -0.052812620997428894f), float3(0.0221935361623764038f, 1.00185668468475342f, -0.0210737623274326324f), float3(-0.00116488314233720303f, -0.00342052709311246915f, 0.761789083480834961f)) + +/* Math helper functions ----------------------------*/ +// Return identity 3x3 matrix +float3x3 identity() +{ + return float3x3(float3(1.0f, 0.0f, 0.0f), float3(0.0f, 1.0f, 0.0f), float3(0.0f, 0.0f, 1.0f)); +} + +// Safe division of float a by float b +float sdivf(float a, float b) +{ + if (b == 0.0f) + return 0.0f; + else + return a / b; +} + +// Safe division of float3 a by float b +float3 sdivf3f(float3 a, float b) +{ + return float3(sdivf(a.x, b), sdivf(a.y, b), sdivf(a.z, b)); +} + +// Safe element-wise division of float3 a by float3 b +float3 sdivf3f3(float3 a, float3 b) +{ + return float3(sdivf(a.x, b.x), sdivf(a.y, b.y), sdivf(a.z, b.z)); +} + +// Safe power function raising float a to power float b +float spowf(float a, float b) +{ + if (a <= 0.0f) + return a; + else + return pow(a, b); +} + +// Safe power function raising float3 a to power float b +float3 spowf3(float3 a, float b) +{ + return float3(spowf(a.x, b), spowf(a.y, b), spowf(a.z, b)); +} +// Return the hypot or vector length of float2 v +float hypotf2(float2 v) { return sqrt(max(0.0f, v.x * v.x + v.y * v.y)); } + +// Return the hypot or vector length of float3 v +float hypotf3(float3 v) { return sqrt(max(0.0f, v.x * v.x + v.y * v.y + v.z * v.z)); } + +// Return the min of float3 a +float fmaxf3(float3 a) { return max(a.x, max(a.y, a.z)); } + +// Return the max of float3 a +float fminf3(float3 a) { return min(a.x, min(a.y, a.z)); } + +// Clamp float3 a to max value mx +float3 clampmaxf3(float3 a, float mx) { return float3(min(a.x, mx), min(a.y, mx), min(a.z, mx)); } + +// Clamp float3 a to min value mn +float3 clampminf3(float3 a, float mn) { return float3(max(a.x, mn), max(a.y, mn), max(a.z, mn)); } + +// Clamp float3 a to min value mn and max value mx +float clampf(float a, float mn, float mx) { return min(max(a, mn), mx); } +float3 clampf3(float3 a, float mn, float mx) { return float3(clampf(a.x, mn, mx), clampf(a.y, mn, mx), clampf(a.z, mn, mx)); } + +/* Output and overlay transfer helpers ---------------------------------------- */ + +float tonescale_overlay_to_scene_linear(float x) +{ + return x < 0.075f ? (x - 0.075f) / 16.184376489665897f : exp((x - 0.5520126568606655f) / 0.09232902596577353f) - 0.0057048244042473785f; +} + +/* Functions for OpenDRT ---------------------------------------- */ + +float compress_hyperbolic_power(float x, float s, float p) +{ + // Simple hyperbolic compression function https://www.desmos.com/calculator/ofwtcmzc3w + return spowf(x / (x + s), p); +} + +float compress_toe_quadratic(float x, float toe, int inv) +{ + // Quadratic toe compress function https://www.desmos.com/calculator/skk8ahmnws + if (toe == 0.0f) + return x; + if (inv == 0) { + return spowf(x, 2.0f) / (x + toe); + } else { + return (x + sqrt(x * (4.0f * toe + x))) / 2.0f; + } +} + +float compress_toe_cubic(float x, float m, float w, int inv) +{ + // https://www.desmos.com/calculator/ubgteikoke + if (m == 1.0f) + return x; + float x2 = x * x; + if (inv == 0) { + return x * (x2 + m * w) / (x2 + w); + } else { + float p0 = x2 - 3.0f * m * w; + float p1 = 2.0f * x2 + 27.0f * w - 9.0f * m * w; + float p2 = pow(sqrt(x2 * p1 * p1 - 4 * p0 * p0 * p0) / 2.0f + x * p1 / 2.0f, 1.0f / 3.0f); + return p0 / (3.0f * p2) + p2 / 3.0f + x / 3.0f; + } +} + +float complement_power(float x, float p) +{ + return 1.0f - spowf(1.0f - x, 1.0f / p); +} + +float sigmoid_cubic(float x, float s) +{ + // Simple cubic sigmoid: https://www.desmos.com/calculator/hzgib42en6 + if (x < 0.0f || x > 1.0f) + return 1.0f; + return 1.0f + s * (1.0f - 3.0f * x * x + 2.0f * x * x * x); +} + +float contrast_high(float x, float p, float pv, float pv_lx, int inv) +{ + // High exposure adjustment with linear extension + // https://www.desmos.com/calculator/etjgwyrgad + const float x0 = 0.18f * pow(2.0f, pv); + if (x < x0 || p == 1.0f) + return x; + + const float o = x0 - x0 / p; + const float s0 = pow(x0, 1.0f - p) / p; + const float x1 = x0 * pow(2.0f, pv_lx); + const float k1 = p * s0 * pow(x1, p) / x1; + const float y1 = s0 * pow(x1, p) + o; + if (inv == 1) + return x > y1 ? (x - y1) / k1 + x1 : pow((x - o) / s0, 1.0f / p); + else + return x > x1 ? k1 * (x - x1) + y1 : s0 * pow(x, p) + o; +} + +float softplus_constraint(float x, float s, float x0, float y0) +{ + // Softplus with (x0, y0) intersection constraint + // https://www.desmos.com/calculator/doipi4u0ce + if (x > 10.0f * s + y0 || s < 1e-3f) + return x; + float m = 1.0f; + if (abs(y0) > 1e-6f) + m = exp(y0 / s); + m -= exp(x0 / s); + return s * log(max(0.0f, m + exp(x / s))); +} + +float softplus(float x, float s) +{ + // Softplus unconstrained + // https://www.desmos.com/calculator/mr9rmujsmn + if (x > 10.0f * s || s < 1e-4f) + return x; + return s * log(max(0.0f, 1.0f + exp(x / s))); +} + +float gauss_window(float x, float w) +{ + // Simple gaussian window https://www.desmos.com/calculator/vhr9hstlyk + return exp(-x * x / w); +} + +float2 opponent(float3 rgb) +{ + // Simple Cyan-Yellow / Green-Magenta opponent space for calculating smooth achromatic distance and hue angles + return float2(rgb.x - rgb.z, rgb.y - (rgb.x + rgb.z) / 2.0f); +} + +float hue_offset(float h, float o) +{ + // Offset hue maintaining 0-2*Math::PI range with modulo + return fmod(h - o + Math::PI, 2.0 * Math::PI) - Math::PI; +} + +float3 display_gamut_whitepoint(float3 rgb, float tsn, float creative_white_limit, int display_gamut, int creative_white) +{ + rgb = mul(matrix_p3d65_to_xyz, rgb); + + float3 cwp_neutral = rgb; + float cwp_f = pow(tsn, 2.0f * creative_white_limit); + + if (creative_white == 0) + rgb = mul(matrix_cat_d65_to_d93, rgb); + else if (creative_white == 1) + rgb = mul(matrix_cat_d65_to_d75, rgb); + else if (creative_white == 3) + rgb = mul(matrix_cat_d65_to_d60, rgb); + else if (creative_white == 4) + rgb = mul(matrix_cat_d65_to_d55, rgb); + else if (creative_white == 5) + rgb = mul(matrix_cat_d65_to_d50, rgb); + + rgb = rgb * cwp_f + cwp_neutral * (1.0f - cwp_f); + rgb = mul(matrix_xyz_to_rec709, rgb); + + float cwp_norm = 1.0f; + if (display_gamut == 0) { + if (creative_white == 0) + cwp_norm = 0.744192699063f; + else if (creative_white == 1) + cwp_norm = 0.873470832146f; + else if (creative_white == 3) + cwp_norm = 0.955936992163f; + else if (creative_white == 4) + cwp_norm = 0.905671332781f; + else if (creative_white == 5) + cwp_norm = 0.850004385027f; + } else if (display_gamut == 1) { // P3D65 or P3 Limited Rec.2020 + if (creative_white == 0) + cwp_norm = 0.762687057298f; // D93 + else if (creative_white == 1) + cwp_norm = 0.884054083328f; // D75 + // else if (cwp == 2) cwp_norm = 1.0f; // D65 + else if (creative_white == 3) + cwp_norm = 0.964320186739f; // D60 + else if (creative_white == 4) + cwp_norm = 0.923076518860f; // D55 + else if (creative_white == 5) + cwp_norm = 0.876572837784f; // D50 + } + + rgb *= cwp_norm * cwp_f + 1.0f - cwp_f; + return rgb; +} + +float3 OpenDRTTransform(float3 rgb) +{ + // Move variables here + // int input_color_space = odrtConfig.input_color_space; + int output_encoding = enableHDR ? 1 : 0; + int clamp = odrtConfig.clamp; + int tn_su = odrtConfig.tn_su; + + float hdr_paper_white_nits = max(hdrPaperWhiteNits, 1.0f); + float hdr_peak_nits = max(hdrPeakNits, hdr_paper_white_nits); + float hdr_peak_ratio = hdr_peak_nits / hdr_paper_white_nits; + + float tn_Lp = enableHDR ? hdr_peak_nits : odrtConfig.tn_Lp; + float tn_gb = odrtConfig.tn_gb; + float pt_hdr = odrtConfig.pt_hdr; + float tn_Lg = odrtConfig.tn_Lg; + + float tn_con = odrtConfig.tn_con; + float tn_sh = odrtConfig.tn_sh; + float tn_toe = odrtConfig.tn_toe; + float tn_off = odrtConfig.tn_off; + + int tn_hcon_enable = odrtConfig.tn_hcon_enable; + float tn_hcon = odrtConfig.tn_hcon; + float tn_hcon_pv = odrtConfig.tn_hcon_pv; + float tn_hcon_st = odrtConfig.tn_hcon_st; + + int tn_lcon_enable = odrtConfig.tn_lcon_enable; + float tn_lcon = odrtConfig.tn_lcon; + float tn_lcon_w = odrtConfig.tn_lcon_w; + int cwp = odrtConfig.cwp; + + float cwp_lm = odrtConfig.cwp_lm; + float rs_sa = odrtConfig.rs_sa; + float rs_rw = odrtConfig.rs_rw; + float rs_bw = odrtConfig.rs_bw; + + int pt_enable = odrtConfig.pt_enable; + float pt_lml = odrtConfig.pt_lml; + float pt_lml_r = odrtConfig.pt_lml_r; + float pt_lml_g = odrtConfig.pt_lml_g; + + float pt_lml_b = odrtConfig.pt_lml_b; + float pt_lmh = odrtConfig.pt_lmh; + float pt_lmh_r = odrtConfig.pt_lmh_r; + float pt_lmh_b = odrtConfig.pt_lmh_b; + + int ptl_enable = odrtConfig.ptl_enable; + float ptl_c = odrtConfig.ptl_c; + float ptl_m = odrtConfig.ptl_m; + float ptl_y = odrtConfig.ptl_y; + + int ptm_enable = odrtConfig.ptm_enable; + float ptm_low = odrtConfig.ptm_low; + float ptm_low_rng = odrtConfig.ptm_low_rng; + float ptm_low_st = odrtConfig.ptm_low_st; + + float ptm_high = odrtConfig.ptm_high; + float ptm_high_rng = odrtConfig.ptm_high_rng; + float ptm_high_st = odrtConfig.ptm_high_st; + int brl_enable = odrtConfig.brl_enable; + + float brl = odrtConfig.brl; + float brl_r = odrtConfig.brl_r; + float brl_g = odrtConfig.brl_g; + float brl_b = odrtConfig.brl_b; + + float brl_rng = odrtConfig.brl_rng; + float brl_st = odrtConfig.brl_st; + int brlp_enable = odrtConfig.brlp_enable; + float brlp = odrtConfig.brlp; + + float brlp_r = odrtConfig.brlp_r; + float brlp_g = odrtConfig.brlp_g; + float brlp_b = odrtConfig.brlp_b; + int hc_enable = odrtConfig.hc_enable; + + float hc_r = odrtConfig.hc_r; + float hc_r_rng = odrtConfig.hc_r_rng; + int hs_rgb_enable = odrtConfig.hs_rgb_enable; + float hs_r = odrtConfig.hs_r; + + float hs_r_rng = odrtConfig.hs_r_rng; + float hs_g = odrtConfig.hs_g; + float hs_g_rng = odrtConfig.hs_g_rng; + float hs_b = odrtConfig.hs_b; + + float hs_b_rng = odrtConfig.hs_b_rng; + int hs_cmy_enable = odrtConfig.hs_cmy_enable; + float hs_c = odrtConfig.hs_c; + float hs_c_rng = odrtConfig.hs_c_rng; + + float hs_m = odrtConfig.hs_m; + float hs_m_rng = odrtConfig.hs_m_rng; + float hs_y = odrtConfig.hs_y; + float hs_y_rng = odrtConfig.hs_y_rng; + + // float3x3 in_to_xyz = input_color_space == 1 ? matrix_ap1_to_xyz : matrix_rec709_to_xyz; + + /*************************************************** + Tonescale Constraint Calculations + https://www.desmos.com/calculator/1c4fhzy3bw + + These should be pre-calculated but there is no way to do this in DCTL. + Anything that is const should be precalculated and not run per-pixel + --------------------------------------------------*/ + const float ts_x1 = pow(2.0f, 6.0f * tn_sh + 4.0f); + const float ts_y1 = tn_Lp / 100.0f; + const float ts_x0 = 0.18f + tn_off; + const float ts_y0 = tn_Lg / 100.0f * (1.0f + tn_gb * log2(ts_y1)); + const float ts_s0 = compress_toe_quadratic(ts_y0, tn_toe, 1); + const float ts_p = tn_con / (1.0f + (float)tn_su * 0.05f); // unconstrained surround compensation + const float ts_s10 = ts_x0 * (pow(ts_s0, -1.0f / tn_con) - 1.0f); + const float ts_m1 = ts_y1 / pow(ts_x1 / (ts_x1 + ts_s10), tn_con); + const float ts_m2 = compress_toe_quadratic(ts_m1, tn_toe, 1); + const float ts_s = ts_x0 * (pow(ts_s0 / ts_m2, -1.0f / tn_con) - 1.0f); + const float ts_dsc = 100.0f / tn_Lp; + + // Lerp from pt_cmp at 100 nits to pt_cmp_hdr at 1000 nits + const float pt_cmp_Lf = pt_hdr * min(1.0f, (tn_Lp - 100.0f) / 900.0f); + // Approximate scene-linear scale at Lp=100 nits + const float s_Lp100 = ts_x0 * (pow((tn_Lg / 100.0f), -1.0f / tn_con) - 1.0f); + const float ts_s1 = ts_s * pt_cmp_Lf + s_Lp100 * (1.0f - pt_cmp_Lf); + + // Convert from input gamut into P3-D65 + // rgb = mul(in_to_xyz, rgb); + rgb = mul(matrix_xyz_to_p3d65, rgb); + + // Rendering Space: "Desaturate" to control scale of the color volume in the rgb ratios. + // Controlled by rs_sa (saturation) and red and blue weights (rs_rw and rs_bw) + float3 rs_w = float3(rs_rw, 1.0f - rs_rw - rs_bw, rs_bw); + float sat_L = rgb.x * rs_w.x + rgb.y * rs_w.y + rgb.z * rs_w.z; + rgb = sat_L * rs_sa + rgb * (1.0f - rs_sa); + + // Offset + rgb += tn_off; + + // Tonescale Norm + float tsn = hypotf3(rgb) / 1.73205080756887729353f; // 1 / sqrt3 + + // RGB Ratios + rgb = sdivf3f(rgb, tsn); + + float2 opp = opponent(rgb); + float ach_d = hypotf2(opp) / 2.0f; + + // Smooth ach_d, normalized so 1.0 doesn't change https://www.desmos.com/calculator/ozjg09hzef + ach_d = (1.25f) * compress_toe_quadratic(ach_d, 0.25f, 0); + + // Hue angle, rotated so that red = 0.0 + float hue = fmod(atan2(opp.x, opp.y) + Math::PI + 1.10714931f, 2.0f * Math::PI); + + // RGB Hue Angles + // Wider than CMY by default. R towards M, G towards Y, B towards C + float3 ha_rgb = float3( + gauss_window(hue_offset(hue, 0.1f), 0.66f), + gauss_window(hue_offset(hue, 4.3f), 0.66f), + gauss_window(hue_offset(hue, 2.3f), 0.66f)); + + // RGB Hue Angles for hue shift: red shifted more orange + float3 ha_rgb_hs = float3( + gauss_window(hue_offset(hue, -0.4f), 0.66f), + ha_rgb.y, + gauss_window(hue_offset(hue, 2.5f), 0.66f)); + + // CMY Hue Angles + // Exact alignment to Cyan/Magenta/Yellow secondaries would be Math::PI, Math::PI/3 and -Math::PI/3, but + // we customize these a bit for creative purposes: M towards B, Y towards G, C towards G + float3 ha_cmy = float3( + gauss_window(hue_offset(hue, 3.3f), 0.5f), + gauss_window(hue_offset(hue, 1.3f), 0.5f), + gauss_window(hue_offset(hue, -1.15f), 0.5f)); + + // Brilliance + if (brl_enable != 0) { + float brl_tsf = pow(tsn / (tsn + 1.0f), 1.0f - brl_rng); + float brl_exf = (brl + brl_r * ha_rgb.x + brl_g * ha_rgb.y + brl_b * ha_rgb.z) * pow(ach_d, 1.0f / brl_st); + float brl_ex = pow(2.0f, brl_exf * (brl_exf < 0.0f ? brl_tsf : 1.0f - brl_tsf)); + tsn *= brl_ex; + } + + // Contrast Low + if (tn_lcon_enable != 0) { + float lcon_m = pow(2.0f, -tn_lcon); + float lcon_w = tn_lcon_w / 4.0f; + lcon_w *= lcon_w; + + // Normalize for ts_x0 intersection constraint: https://www.desmos.com/calculator/blyvi8t2b2 + const float lcon_cnst_sc = compress_toe_cubic(ts_x0, lcon_m, lcon_w, 1) / ts_x0; + tsn *= lcon_cnst_sc; + tsn = compress_toe_cubic(tsn, lcon_m, lcon_w, 0); + } + + // Contrast High + if (tn_hcon_enable != 0) { + float hcon_p = pow(2.0f, tn_hcon); + tsn = contrast_high(tsn, hcon_p, tn_hcon_pv, tn_hcon_st, 0); + } + + // Hyperbolic Compression + float tsn_pt = compress_hyperbolic_power(tsn, ts_s1, ts_p); + float tsn_const = compress_hyperbolic_power(tsn, s_Lp100, ts_p); + tsn = compress_hyperbolic_power(tsn, ts_s, ts_p); + + /*************************************************** + Hue Contrast R + --------------------------------------------------*/ + if (hc_enable != 0) { + float hc_ts = 1.0f - tsn_const; + // Limit high purity on bottom end and low purity on top end by ach_d. + // This helps reduce artifacts and over-saturation. + float hc_c = hc_ts * (1.0f - ach_d) + ach_d * (1.0f - hc_ts); + hc_c *= ach_d * ha_rgb.x; + hc_ts = pow(hc_ts, 1.0f / hc_r_rng); + // Bias contrast based on tonescale using Lift/Mult: https://www.desmos.com/calculator/gzbgov62hl + float hc_f = hc_r * (hc_c - 2.0f * hc_c * hc_ts) + 1.0f; + rgb = float3(rgb.x, rgb.y * hc_f, rgb.z * hc_f); + } + + /*************************************************** + Hue Shift + --------------------------------------------------*/ + // Hue Shift RGB by purity compress tonescale, shifting more as intensity increases + if (hs_rgb_enable != 0) { + float3 hs_rgb = float3( + ha_rgb_hs.x * ach_d * pow(tsn_pt, 1.0f / hs_r_rng), + ha_rgb_hs.y * ach_d * pow(tsn_pt, 1.0f / hs_g_rng), + ha_rgb_hs.z * ach_d * pow(tsn_pt, 1.0f / hs_b_rng)); + float3 hsf = float3(hs_rgb.x * hs_r, hs_rgb.y * -hs_g, hs_rgb.z * -hs_b); + hsf = float3(hsf.z - hsf.y, hsf.x - hsf.z, hsf.y - hsf.x); + rgb += hsf; + } + + // Hue Shift CMY by tonescale, shifting less as intensity increases + if (hs_cmy_enable != 0) { + float tsn_pt_compl = 1.0f - tsn_pt; + float3 hs_cmy = float3( + ha_cmy.x * ach_d * pow(tsn_pt_compl, 1.0f / hs_c_rng), + ha_cmy.y * ach_d * pow(tsn_pt_compl, 1.0f / hs_m_rng), + ha_cmy.z * ach_d * pow(tsn_pt_compl, 1.0f / hs_y_rng)); + float3 hsf = float3(hs_cmy.x * -hs_c, hs_cmy.y * hs_m, hs_cmy.z * hs_y); + hsf = float3(hsf.z - hsf.y, hsf.x - hsf.z, hsf.y - hsf.x); + rgb += hsf; + } + + /*************************************************** + Purity Compression + https://www.desmos.com/calculator/adtzkjofgn + --------------------------------------------------*/ + float ptf = 1.0f; + if (pt_enable != 0) { + // Purity Limit Low + float pt_lml_p = 1.0f + 4.0f * (1.0f - tsn_pt) * (pt_lml + pt_lml_r * ha_rgb_hs.x + pt_lml_g * ha_rgb_hs.y + pt_lml_b * ha_rgb_hs.z); + ptf = 1.0f - pow(tsn_pt, pt_lml_p); + + // Purity Limit High + float pt_lmh_p = (1.0f - ach_d * (pt_lmh_r * ha_rgb_hs.x + pt_lmh_b * ha_rgb_hs.z)) * (1.0f - pt_lmh * ach_d); + ptf = pow(ptf, pt_lmh_p); + } + + /*************************************************** + Mid-Range Purity + This boosts mid-range purity on the low end + and reduces mid-range purity on the high end + --------------------------------------------------*/ + if (ptm_enable != 0) { + float ptm_low_f; + if (ptm_low_st == 0.0f || ptm_low_rng == 0.0f) + ptm_low_f = 1.0f; + else + ptm_low_f = 1.0f + ptm_low * exp(-2.0f * ach_d * ach_d / ptm_low_st) * pow(1.0f - tsn_const, 1.0f / ptm_low_rng); + float ptm_high_f; + if (ptm_high_st == 0.0f || ptm_high_rng == 0.0f) + ptm_high_f = 1.0f; + else + ptm_high_f = 1.0f + ptm_high * exp(-2.0f * ach_d * ach_d / ptm_high_st) * pow(tsn_pt, 1.0f / (4.0f * ptm_high_rng)); + ptf *= ptm_low_f * ptm_high_f; + } + + // Lerp to peak achromatic by ptf in rgb ratios + rgb = rgb * ptf + 1.0f - ptf; + + // Inverse Rendering Space + sat_L = rgb.x * rs_w.x + rgb.y * rs_w.y + rgb.z * rs_w.z; + rgb = (sat_L * rs_sa - rgb) / (rs_sa - 1.0f); + + // Convert to linear Rec.709/sRGB and set whitepoint. + rgb = display_gamut_whitepoint(rgb, tsn_const, cwp_lm, output_encoding, cwp); + + // Post Brilliance + if (brlp_enable != 0) { + float2 brlp_opp = opponent(rgb); + float brlp_ach_d = hypotf2(brlp_opp) / 4.0f; + // brlp_ach_d = 1.0f - gauss_window(brlp_ach_d, 8.0f); + brlp_ach_d = 1.1f * (brlp_ach_d * brlp_ach_d / (brlp_ach_d + 0.1f)); + float3 brlp_ha_rgb = ach_d * ha_rgb; + float brlp_m = brlp + brlp_r * brlp_ha_rgb.x + brlp_g * brlp_ha_rgb.y + brlp_b * brlp_ha_rgb.z; + float brlp_ex = pow(2.0f, brlp_m * brlp_ach_d * tsn); + rgb *= brlp_ex; + } + + // Purity Compress Low + if (ptl_enable != 0) + rgb = float3(softplus(rgb.x, ptl_c), softplus(rgb.y, ptl_m), softplus(rgb.z, ptl_y)); + + // Final tonescale adjustments + tsn *= ts_m2; // scale for inverse toe + tsn = compress_toe_quadratic(tsn, tn_toe, 0); + tsn *= ts_dsc; // scale for display encoding + + // Return from RGB ratios + rgb *= tsn; + + // Rec.2020 (P3 Limited) + if (output_encoding == 1) { + rgb = clampminf3(rgb, 0.0f); // Limit to P3 gamut + rgb = mul(matrix_p3_to_rec2020, rgb); + } + + // OpenDRT returns display-linear normalized to display peak. + // HDRDisplay expects scene output normalized to paper white: 1.0 = paper white. + if (enableHDR) + rgb *= hdr_peak_ratio; + + // Clamp + if (clamp != 0) + rgb = clampf3(rgb, 0.0f, enableHDR ? hdr_peak_ratio : 1.0f); + + return rgb; +} diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/OpenDRTConfig.hlsli b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/OpenDRTConfig.hlsli new file mode 100644 index 0000000000..ef1ee17477 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/OpenDRTConfig.hlsli @@ -0,0 +1,97 @@ +// OpenDRT v1.1.0 HLSL compute shader conversion. +// Original DCTL written by Jed Smith: https://github.com/jedypod/open-display-transform +// License: GPLv3 +struct OpenDRTConfig +{ + // 0: linear Rec.709/sRGB, 1: linear ACEScg. + int input_color_space; + // 0: linear Rec.709/sRGB, 1: linear Rec.2020 + int output_encoding; + int clamp; + int tn_su; + + float tn_Lp; + float tn_gb; + float pt_hdr; + float tn_Lg; + + float tn_con; + float tn_sh; + float tn_toe; + float tn_off; + + int tn_hcon_enable; + float tn_hcon; + float tn_hcon_pv; + float tn_hcon_st; + + int tn_lcon_enable; + float tn_lcon; + float tn_lcon_w; + int cwp; + + float cwp_lm; + float rs_sa; + float rs_rw; + float rs_bw; + + int pt_enable; + float pt_lml; + float pt_lml_r; + float pt_lml_g; + + float pt_lml_b; + float pt_lmh; + float pt_lmh_r; + float pt_lmh_b; + + int ptl_enable; + float ptl_c; + float ptl_m; + float ptl_y; + + int ptm_enable; + float ptm_low; + float ptm_low_rng; + float ptm_low_st; + + float ptm_high; + float ptm_high_rng; + float ptm_high_st; + int brl_enable; + + float brl; + float brl_r; + float brl_g; + float brl_b; + + float brl_rng; + float brl_st; + int brlp_enable; + float brlp; + + float brlp_r; + float brlp_g; + float brlp_b; + int hc_enable; + + float hc_r; + float hc_r_rng; + int hs_rgb_enable; + float hs_r; + + float hs_r_rng; + float hs_g; + float hs_g_rng; + float hs_b; + + float hs_b_rng; + int hs_cmy_enable; + float hs_c; + float hs_c_rng; + + float hs_m; + float hs_m_rng; + float hs_y; + float hs_y_rng; +}; \ No newline at end of file diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/RenoDX.MIT.LICENSE b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/RenoDX.MIT.LICENSE new file mode 100644 index 0000000000..55209a2779 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/RenoDX.MIT.LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Carlos Lopez Jr. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/RenoDXToneMapping.hlsli b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/RenoDXToneMapping.hlsli new file mode 100644 index 0000000000..84c64c7416 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/RenoDXToneMapping.hlsli @@ -0,0 +1,119 @@ +#ifndef COLORGRADING_RENODX_TONE_MAPPING_HLSLI +#define COLORGRADING_RENODX_TONE_MAPPING_HLSLI + +#include "PostProcessing/ColorGrading/Include/renodx/tonemap/aces.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/tonemap/frostbite.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/tonemap/hermite_spline.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/tonemap/neutwo.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/tonemap/psychov_17.hlsl" + +namespace renodx +{ + namespace color + { + namespace ictcp + { + namespace from + { + float3 BT2020(float3 bt2020Color, float scaling = 100.0f) + { + float3 lms = mul(mul(renodx::color::ictcp::XYZ_TO_DOLBY_LMS_MAT, renodx::color::BT2020_TO_XYZ_MAT), bt2020Color); + float3 plms = renodx::color::pq::Encode(max(0.0f, lms), scaling); + return mul(renodx::color::ictcp::PLMS_TO_ICTCP_MAT, plms); + } + } + } + + namespace bt2020 + { + namespace from + { + float3 ICtCp(float3 ictcpColor, float scaling = 100.0f) + { + float3 plms = mul(renodx::math::Invert3x3(renodx::color::ictcp::PLMS_TO_ICTCP_MAT), ictcpColor); + float3 lms = renodx::color::pq::Decode(plms, scaling); + return mul( + mul( + renodx::math::Invert3x3(renodx::color::BT2020_TO_XYZ_MAT), + renodx::math::Invert3x3(renodx::color::ictcp::XYZ_TO_DOLBY_LMS_MAT)), + lms); + } + } + } + } +} + +namespace ColorGradingRenoDX +{ + static const float ACES_REFERENCE_WHITE_NITS = 48.0f; + static const float ACES_SDR_MIN_NITS = 0.02f; + static const float ACES_SDR_MAX_NITS = 48.0f; + static const float ACES_MID_GRAY = 0.10f; + static const float ACES_DEFAULT_MID_GRAY_VALUE = 0.18f; + static const float ACES_MIN_NITS = 0.0001f; + + float3 NeutwoBT2020(float3 color, float peak, float clipPoint) + { + return renodx::tonemap::neutwo::BT2020(color, peak, clipPoint); + } + + void GetACESDisplayRange(float minNits, float peakNits, float diffuseWhiteNits, float midGrayValue, out float minY, out float maxY, out float midGrayScale) + { + float diffuseWhite = max(diffuseWhiteNits, 1.0f); + float peakWhite = max(peakNits, diffuseWhite); + + midGrayScale = max(midGrayValue, 1e-6f) / ACES_MID_GRAY; + minY = (max(minNits, ACES_MIN_NITS) / diffuseWhite) / midGrayScale * ACES_REFERENCE_WHITE_NITS; + maxY = (peakWhite / diffuseWhite) / midGrayScale * ACES_REFERENCE_WHITE_NITS; + } + + float3 ACESSDRBT709(float3 color, float minNits) + { + float minY = max(minNits, ACES_SDR_MIN_NITS); + + color = renodx::tonemap::aces::RGCAndRRTAndODT(color, minY, ACES_SDR_MAX_NITS); + return color / ACES_SDR_MAX_NITS; + } + + float3 ACESBT2020(float3 color, float minNits, float peakNits, float diffuseWhiteNits, float midGrayValue) + { + float minY, maxY, midGrayScale; + GetACESDisplayRange(minNits, peakNits, diffuseWhiteNits, midGrayValue, minY, maxY, midGrayScale); + + color = mul(renodx::color::BT2020_TO_AP1_MAT, color); + color = renodx::tonemap::aces::GamutCompress(color); + color = mul(renodx::color::AP1_TO_AP0_MAT, color); + color = renodx::tonemap::aces::RRT(color); + color = renodx::tonemap::aces::ODT(color, minY, maxY, renodx::color::AP1_TO_BT2020_MAT); + return color / ACES_REFERENCE_WHITE_NITS * midGrayScale; + } + + float3 FrostbiteBT2020(float3 color, float maxValue, float rolloffStart, float saturationBoostAmount, float hueCorrectAmount) + { + float3 ictcp = renodx::color::ictcp::from::BT2020(color); + + float saturationAmount = pow(smoothstep(1.0f, 0.3f, ictcp.x), 1.3f); + color = renodx::color::bt2020::from::ICtCp(ictcp * float3(1.0f, saturationAmount, saturationAmount)); + + float maxCol = max(color.x, max(color.y, color.z)); + float mappedMax = renodx::tonemap::frostbite::RangeCompress(maxCol, rolloffStart, maxValue); + float3 compressedHuePreserving = color * mappedMax / maxCol; + float3 perChannelCompressed = renodx::tonemap::frostbite::RangeCompress(color, rolloffStart, maxValue); + color = lerp(perChannelCompressed, compressedHuePreserving, hueCorrectAmount); + + float3 ictcpMapped = renodx::color::ictcp::from::BT2020(color); + float postCompressionSaturationBoost = saturationBoostAmount * smoothstep(1.0f, 0.5f, ictcp.x); + ictcpMapped.yz = lerp(ictcpMapped.yz, ictcp.yz * ictcpMapped.x / max(1e-3f, ictcp.x), postCompressionSaturationBoost); + + return renodx::color::bt2020::from::ICtCp(ictcpMapped); + } + + float3 HermiteSplineBT2020(float3 color, float peak, float whiteClip) + { + float y = renodx::color::y::from::BT2020(color); + float newY = renodx::tonemap::HermiteSplineLuminanceRolloff(y, peak, clamp(whiteClip, peak, 500.0f)); + return renodx::color::correct::Luminance(color, y, newY); + } +} + +#endif // COLORGRADING_RENODX_TONE_MAPPING_HLSLI diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color.hlsl new file mode 100644 index 0000000000..e8c766f3a5 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color.hlsl @@ -0,0 +1,22 @@ +#ifndef SRC_SHADERS_COLOR_HLSL_ +#define SRC_SHADERS_COLOR_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/color/acescc.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/acescct.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/arri.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/castlecsf.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/clamp.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/colorvideovdp.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/dtucs.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/gamma.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/gamut.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/ictcp.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/ipt.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/oklab.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/pq.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/srgb.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/stockman.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/ycbcr.hlsl" + +#endif // SRC_SHADERS_COLOR_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/acescc.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/acescc.hlsl new file mode 100644 index 0000000000..e9f5d7f925 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/acescc.hlsl @@ -0,0 +1,66 @@ +#ifndef SRC_SHADERS_COLOR_ACESCC_HLSL_ +#define SRC_SHADERS_COLOR_ACESCC_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + namespace acescc + { + + // https://docs.acescentral.com/specifications/acescc/#acescc + // Avoid using, since not a 0-1 encoding: + // "ACEScc, however, uses values above 1.0 and below 0.0 to encode the entire range of ACES values." + + float Encode(float ap1_channel) + { + if (ap1_channel <= 0) { + // -0.35844748858447484 + return (log2(pow(2.f, -16.f)) + 9.72f) / 17.52f; + } else if (ap1_channel < pow(2.f, -15.f)) { + return (log2(pow(2.f, -16.f) + ap1_channel * 0.5f) + 9.72f) / 17.52f; + } else { + return (log2(pow(2.f, -16.f) + ap1_channel) + 9.72f) / 17.52f; + } + } + + float Decode(float aces_cc_data) + { + if (aces_cc_data <= ((9.72f - 15.f) / 17.52)) { + // <= -0.3013698630136986 + return (pow(2.f, aces_cc_data * 17.52f - 9.72f) - pow(2.f, -16.f)) * 2.f; + } else if (aces_cc_data < ((log2(65504.f) + 9.72f) / 17.52f)) { + return pow(2.f, aces_cc_data * 17.52f - 9.72f); + } else { + return 65504.f; // FLT16_MAX + } + } + + float3 Encode(float3 ap1_color) + { + return float3(Encode(ap1_color.r), Encode(ap1_color.g), Encode(ap1_color.b)); + } + + float3 Decode(float3 aces_cc_data) + { + return float3(Decode(aces_cc_data.r), Decode(aces_cc_data.g), Decode(aces_cc_data.b)); + } + + // Already safe, just for namespace parity + float3 EncodeSafe(float3 color) + { + return Encode(color); + } + + float3 DecodeSafe(float3 aces_cc_data) + { + return Decode(aces_cc_data); + } + + } // namespace acescc + } // namespace color +} // namespace renodx + +#endif // SRC_SHADERS_COLOR_ACESCC_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/acescct.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/acescct.hlsl new file mode 100644 index 0000000000..7a3834f07d --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/acescct.hlsl @@ -0,0 +1,62 @@ +#ifndef SRC_SHADERS_COLOR_ACESCCT_HLSL_ +#define SRC_SHADERS_COLOR_ACESCCT_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + namespace acescct + { + + // https://docs.acescentral.com/specifications/acescct/#acescct + // Avoid using, since not a 0-1 encoding: + // "ACEScct uses values above 1.0 and below 0.0 to encode the entire range of ACES values." + + float Encode(float ap1_channel) + { + if (ap1_channel <= 0.0078125f) { + return 10.5402377416545f * ap1_channel + 0.0729055341958355; + } else { + return (log2(ap1_channel) + 9.72f) / 17.52f; + } + } + + float Decode(float aces_cct_data) + { + if (aces_cct_data <= 0.155251141552511f) { + return (aces_cct_data - 0.0729055341958355) / 10.5402377416545f; + } else if (aces_cct_data < ((log2(65504.f) + 9.72f) / 17.52f)) { + return pow(2.f, aces_cct_data * 17.52f - 9.72f); + } else { + return 65504.f; // FLT16_MAX + } + } + + float3 Encode(float3 ap1_color) + { + return float3(Encode(ap1_color.r), Encode(ap1_color.g), Encode(ap1_color.b)); + } + + float3 Decode(float3 aces_cct_data) + { + return float3(Decode(aces_cct_data.r), Decode(aces_cct_data.g), Decode(aces_cct_data.b)); + } + + // Already safe, just for namespace parity + float3 EncodeSafe(float3 color) + { + return Encode(color); + } + + float3 DecodeSafe(float3 aces_cct_data) + { + return Decode(aces_cct_data); + } + + } // namespace acescct + } // namespace color +} // namespace renodx + +#endif // SRC_SHADERS_COLOR_ACESCCT_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/arri.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/arri.hlsl new file mode 100644 index 0000000000..74cc2e04ec --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/arri.hlsl @@ -0,0 +1,111 @@ +#ifndef RENODX_SHADERS_COLOR_ARRI_HLSL +#define RENODX_SHADERS_COLOR_ARRI_HLSL + +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + namespace arri + { + namespace logc + { + + struct EncodingParams + { + float a; + float b; + float c; + float d; + float e; + float f; + float cut; + }; + +#define ARRI_ENCODE_GENERATOR(T) \ + T Encode(T c, EncodingParams params, bool use_cut = true) \ + { \ + if (!use_cut) { \ + return (params.c * log10((params.a * c) + params.b) + params.d); \ + } \ + return renodx::math::Select( \ + (c > params.cut), \ + (params.c * log10((params.a * c) + params.b) + params.d), \ + (params.e * c + params.f)); \ + } + + ARRI_ENCODE_GENERATOR(float) + ARRI_ENCODE_GENERATOR(float2) + ARRI_ENCODE_GENERATOR(float3) + +#define ARRI_DECODE_GENERATOR(T) \ + T Decode(T c, EncodingParams params, bool use_cut = true) \ + { \ + return renodx::math::Select( \ + (c > (params.e * params.cut * (use_cut ? 1 : 0) + params.f)), \ + ((pow(10.f, (c - params.d) / params.c) - params.b) / params.a), \ + ((c - params.f) / params.e)); \ + } + + ARRI_DECODE_GENERATOR(float) + ARRI_DECODE_GENERATOR(float2) + ARRI_DECODE_GENERATOR(float3) + +#undef ARRI_ENCODE_GENERATOR +#undef ARRI_DECODE_GENERATOR + +#define GENERATE_ARRI_LOGC_FUNCTIONS(T) \ + T Encode(T c, bool use_cut = true) \ + { \ + return logc::Encode(c, PARAMS, use_cut); \ + } \ + T Decode(T c, bool use_cut = true) \ + { \ + return logc::Decode(c, PARAMS, use_cut); \ + } + + namespace c800 + { + static const EncodingParams PARAMS = { + 5.555556f, + 0.052272f, + 0.247190f, + 0.385537f, + 5.367655f, + 0.092809f, + 0.010591f, + }; + + GENERATE_ARRI_LOGC_FUNCTIONS(float) + GENERATE_ARRI_LOGC_FUNCTIONS(float2) + GENERATE_ARRI_LOGC_FUNCTIONS(float3) + + } // namespace c800 + + namespace c1000 + { + + static const EncodingParams PARAMS = { + 5.555556f, + 0.047996f, + 0.244161f, + 0.386036f, + 5.301883f, + 0.092814f, + 0.011361f + }; + + GENERATE_ARRI_LOGC_FUNCTIONS(float) + GENERATE_ARRI_LOGC_FUNCTIONS(float2) + GENERATE_ARRI_LOGC_FUNCTIONS(float3) + + } // namespace c1000 + +#undef GENERATE_ARRI_LOGC_FUNCTIONS + + } // namespace logc + } // namespace arri + } // namespace color +} // namespace renodx +#endif // RENODX_SHADERS_COLOR_ARRI_HLSL diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/castlecsf.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/castlecsf.hlsl new file mode 100644 index 0000000000..dddb2bb6f0 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/castlecsf.hlsl @@ -0,0 +1,617 @@ +#ifndef CASTLE_CSF_HLSL +#define CASTLE_CSF_HLSL + +#include "PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + namespace castlecsf + { + + // ============================================================================ + // castleCSF / CastleCSF — “33 equations = 33 functions” HLSL layout + // + // Intent: + // - Provide one HLSL function per PDF equation: Eq01_... through Eq33_... + // - Use MATLAB files as the authoritative reference for syntax/branches: + // CSF_base.get_lum_dep() + // CSF_stelaCSF_lum_peak (achromatic sustained/transient + ecc drop) + // CSF_castleCSF_chrom (chromatic spatial + ecc drop) + // CSF_castleCSF (parameter defaults for ach/rg/yv + ecc_drop params) + // + // Notes: + // - Eccentricity math is included (Eq25/Eq26) exactly as in MATLAB: + // alpha = min(1, abs(vis_field-180)/90) + // ecc_drop = alpha*ecc_drop + (1-alpha)*ecc_drop_nasal + // ecc_drop_f = alpha*ecc_drop_f + (1-alpha)*ecc_drop_f_nasal + // a = ecc_drop + rho*ecc_drop_f + // S *= 10^(-a*ecc) + // If you pass ecc=0, it collapses to 1 as desired. + // - “Safe divide” here is EXACT (denom == 0), as requested. + // We do NOT treat tiny denoms as zero. + // - Luminance-domain clamps (lum>=1e-6) remain, because log/pow on <=0 is undefined. + // ============================================================================ + + // ---------------------------------------------------------------------------- + // Parameters (from MATLAB get_default_par) + // ---------------------------------------------------------------------------- + + // --- ACH parameters (from CSF_stelaCSF_lum_peak.get_default_par) --- + static const float ach_Smax_p0 = 56.4947f; + static const float ach_Smax_p1 = 7.54726f; + static const float ach_Smax_p2 = 0.144532f; + static const float ach_Smax_p3 = 5.58341e-07f; + static const float ach_Smax_p4 = 9.66862e+09f; + + static const float ach_fmax_p0 = 1.78119f; + static const float ach_fmax_p1 = 91.5718f; + static const float ach_fmax_p2 = 0.256682f; + + static const float ach_bw = 0.000213047f; + static const float ach_a = 0.100207f; + + static const float ach_A0 = 157.103f; + static const float ach_f0 = 0.702338f; + + // Transient Smax is power law: scale * Y^exp (CSF_base.get_lum_dep case 2) + static const float ach_trans_Smax_p0 = 0.193434f; // exponent + static const float ach_trans_Smax_p1 = 2748.09f; // scale + + static const float ach_trans_fmax = 0.000316696f; // treated as rho_m (peak freq) for truncation + static const float ach_trans_bw = 2.6761f; + static const float ach_trans_a = 0.000241177f; + + static const float ach_trans_A0 = 3.81611f; + static const float ach_trans_f0 = 3.01389f; + + // Temporal (ach) + static const float ach_sigma_trans = 0.0844836f; + static const float ach_sigma_sust = 10.5795f; + static const float ach_omega_trans_sl = 2.41482f; + static const float ach_omega_trans_c = 4.7036f; + + // Eccentricity drop (ach) — from CSF_castleCSF.get_default_par() + static const float ach_ecc_drop = 0.0239853f; + static const float ach_ecc_drop_nasal = 0.0400662f; + static const float ach_ecc_drop_f = 0.0189038f; + static const float ach_ecc_drop_f_nasal = 0.00813619f; + + // --- RG chromatic params (from CSF_castleCSF_chrom.get_default_par('rg')) --- + static const float rg_Smax_p0 = 681.434f; + static const float rg_Smax_p1 = 38.0038f; + static const float rg_Smax_p2 = 0.480386f; + + static const float rg_fmax = 0.0178364f; + static const float rg_bw = 2.42104f; + + static const float rg_A0 = 2816.44f; + static const float rg_f0 = 0.0711058f; + + static const float rg_sigma_sust = 16.4325f; + static const float rg_beta_sust = 1.15591f; + + // Eccentricity drop (rg) — from CSF_castleCSF.get_default_par() + static const float rg_ecc_drop = 0.0591402f; + static const float rg_ecc_drop_nasal = 2.89615e-05f; + static const float rg_ecc_drop_f = 0.0f; // 2.04986e-69f + static const float rg_ecc_drop_f_nasal = 0.18108f; + + // --- YV chromatic params --- + static const float yv_Smax_p0 = 166.683f; + static const float yv_Smax_p1 = 62.8974f; + static const float yv_Smax_p2 = 0.41193f; + + static const float yv_fmax = 0.00425753f; + static const float yv_bw = 2.68197f; + + static const float yv_A0 = 2.82789e+07f; + static const float yv_f0 = 0.000635093f; + + static const float yv_sigma_sust = 7.15012f; + static const float yv_beta_sust = 0.969123f; + + // Eccentricity drop (yv) — from CSF_castleCSF.get_default_par() + static const float yv_ecc_drop = 0.00356865f; + static const float yv_ecc_drop_nasal = 0.0f; // MATLAB: ~5e-141 → FP32 zero + static const float yv_ecc_drop_f = 0.00806631f; + static const float yv_ecc_drop_f_nasal = 0.0110662f; + + // ---------------------------------------------------------------------------- + // shared constants + static const float CSF_LN10 = 2.302585092994046f; + + // ---------------------------------------------------------------------------- + // EXACT safe divide (only treats denom == 0 as unsafe) + float SafeDiv0(float num, float den, float fallback_value) + { + return (den == 0.0f) ? fallback_value : (num / den); + } + + // log10/pow10 helpers (avoid name collisions with HLSL log10) + float csf_log10(float x) + { + return log(x) / log(10.0f); + } + float csf_pow10(float x) + { + return exp(x * CSF_LN10); + } + + // ============================================================================ + // Eq.(01) — LMS background luminance proxy (paper plumbing; used by Eq06) + // Here: Y_LMS = L0 + M0 (matches MATLAB usage in CSF_castleCSF.csf) + // ============================================================================ + float Eq01_Y_LMS(float3 lms_bg) + { + return (lms_bg.x + lms_bg.y); + } + +#if 0 +// ============================================================================ +// Eq.(02) — L cone excitation spectral integral (stub; offline) +// Eq.(03) — M cone excitation spectral integral (stub; offline) +// Eq.(04) — S cone excitation spectral integral (stub; offline) +// ============================================================================ +float Eq02_L_fromSpectrum(/*...*/) { return 0.0f; } +float Eq03_M_fromSpectrum(/*...*/) { return 0.0f; } +float Eq04_S_fromSpectrum(/*...*/) { return 0.0f; } + +// ============================================================================ +// Eq.(12) — Alternative normalized sensitivity form (stub; paper algebra) +// ============================================================================ +float Eq12_AltNormalizedForm(/*...*/) { return 0.0f; } +#endif + + // ============================================================================ + // Eq.(05) — Opponent/ACC transform: ΔD = M * ΔLMS + // MATLAB (CSF_castleCSF): M_lms2acc constructed from D65 LMS ratios (mc1, mc2) + // ============================================================================ + float3 Eq05_LMS_to_ACC(float3 delta_lms, float mc1, float mc2) + { + // [ 1, 1, 0 ] + // [ 1, -mc1, 0 ] + // [ -1, -1, mc2 ] + return float3( + delta_lms.x + delta_lms.y, + delta_lms.x - mc1 * delta_lms.y, + -delta_lms.x - delta_lms.y + mc2 * delta_lms.z); + } + + // ============================================================================ + // Eq.(06) — Opponent contrast: ΔC = |ΔD| / Y_LMS + // NOTE: safe divide is EXACT (Y_LMS==0 only). + // ============================================================================ + float3 Eq06_ACC_to_DeltaC(float3 delta_acc, float Y_LMS) + { + float denom = Y_LMS; // may be 0 in degenerate cases + float3 num = abs(delta_acc); + return float3( + SafeDiv0(num.x, denom, 0.0f), + SafeDiv0(num.y, denom, 0.0f), + SafeDiv0(num.z, denom, 0.0f)); + } + + // ============================================================================ + // Eq.(07) — Detection energy: E = || S ⊙ ΔC ||_2 + // ============================================================================ + float Eq07_Energy(float3 S_mech, float3 deltaC) + { + return length(S_mech * deltaC); + } + + // ============================================================================ + // Eq.(08) — Contrast scaling: ΔC = t * ΔĈ + // ============================================================================ + float3 Eq08_ScaleContrast(float3 deltaC_hat, float t) + { + return deltaC_hat * t; + } + + // ============================================================================ + // Eq.(09) — Energy scaling: E = t * Ê + // ============================================================================ + float Eq09_ScaleEnergy(float E_hat, float t) + { + return E_hat * t; + } + + // ============================================================================ + // Eq.(10) — Normalize contrast direction: ΔĈ = ΔC / E + // ============================================================================ + float3 Eq10_NormalizeDeltaC(float3 deltaC, float E) + { + return (E == 0.0f) ? 0.0f.xxx : (deltaC / E); + } + + // ============================================================================ + // Eq.(11) — Normalize LMS increments (algebraic helper; typically unused at runtime) + // ============================================================================ + float3 Eq11_NormalizeDeltaLMS(float3 delta_lms, float E) + { + return (E == 0.0f) ? 0.0f.xxx : (delta_lms / E); + } + + // ============================================================================ + // Eq.(13) — Sustained temporal response: exp(-|ω|^β / σ) + // MATLAB: exp(-(abs(omega).^beta)./sigma) + // ============================================================================ + float Eq13_R_sust(float omega, float sigma, float beta) + { + return exp(-pow(abs(omega), beta) / sigma); + } + + // ============================================================================ + // Eq.(14) — Ach transient temporal response (Gaussian in warped ω) + // MATLAB: exp(-(abs(abs(ω)^β - ω0^β)^2)/σ) + // ============================================================================ + float Eq14_R_trans_ach(float omega, float omega0, float sigma, float beta) + { + float w = abs(omega); + float d = abs(pow(w, beta) - pow(omega0, beta)); + return exp(-(d * d) / sigma); + } + + // ============================================================================ + // Eq.(15) — Transient center frequency: ω0(Y) = log10(Y)*sl + c + // MATLAB: omega0 = log10(lum).*sl + c + // ============================================================================ + float Eq15_omega0(float lum_cd_m2, float sl, float c) + { + lum_cd_m2 = max(lum_cd_m2, 1e-6f); + return csf_log10(lum_cd_m2) * sl + c; + } + + // ============================================================================ + // Eq.(16) — Log-parabola peak: 10^(-(log10(ρ)-log10(ρm))^2 / (2^bw)) + // MATLAB: 10.^(-(log10(f)-log10(f_max)).^2 ./ (2.^bw)) + // ============================================================================ + float Eq16_log_parabola(float rho, float rho_m, float bw) + { + rho = max(rho, 1e-6f); + rho_m = max(rho_m, 1e-6f); + float d = csf_log10(rho) - csf_log10(rho_m); + return csf_pow10(-(d * d) / pow(2.0f, bw)); + } + + // ============================================================================ + // Eq.(17) — Achromatic truncation floor: if ρ<ρm and lp<(1-a) => lp=(1-a) + // MATLAB: truncation in csf_achrom() + // ============================================================================ + float Eq17_trunc_ach(float rho, float rho_m, float bw, float a) + { + float lp = Eq16_log_parabola(rho, rho_m, bw); + float floorVal = 1.0f - a; + if ((rho < rho_m) && (lp < floorVal)) + lp = floorVal; + return lp; + } + + // ============================================================================ + // Eq.(18) — Chromatic low-frequency clamp: l=1 for ρ=0 (repo) :contentReference[oaicite:5]{index=5} + float cvvdp_safe_pow(float x, float p) + { + return pow(x + CVVDP_EPS, p) - pow(CVVDP_EPS, p); + } + + // pow_neg(x,p) = max(x,eps)^p + max(-x,eps)^p - eps^p (repo) :contentReference[oaicite:6]{index=6} + float cvvdp_pow_neg(float x, float p) + { + float a = pow(max(x, CVVDP_EPS), p); + float b = pow(max(-x, CVVDP_EPS), p); + return a + b - pow(CVVDP_EPS, p); + } + + // ---------------------------------------------------------------------------- + // Channel constants (canonical order) + + static const int CVVDP_CH_Y_SUST = 0; + static const int CVVDP_CH_RG = 1; + static const int CVVDP_CH_YV = 2; + static const int CVVDP_CH_Y_TRANS = 3; + + // ---------------------------------------------------------------------------- + // Parameters from cvvdp_parameters.json (main / 0.5.6) :contentReference[oaicite:7]{index=7} + + static const float CVVDP_mask_p = 2.264355182647705f; + static const float CVVDP_mask_c = -0.7954971194267273f; // content masking adjustment (applied as 10^mask_c) + static const float CVVDP_d_max = 2.5642454624176025f; // clamping scale exponent (used as 10^d_max) + static const float CVVDP_image_int = 0.577918291091919f; // integration correction for images + + // Pooling exponents + static const float CVVDP_beta = 2.0f; // spatial summation exponent + static const float CVVDP_beta_t = 2.0f; // time summation exponent + static const float CVVDP_beta_tch = 4.0f; // temporal+chromatic channels exponent + static const float CVVDP_beta_sch = 4.0f; // spatial bands exponent + + // Masking exponents per channel (Ysust, RG, YV, Ytrans) + static const float4 CVVDP_mask_q = float4( + 1.302622675895691f, + 2.8885908126831055f, + 3.6807713508605957f, + 3.588787317276001f); + + // Channel weights: chromatic weight + transient weight + static const float CVVDP_ch_chrom_w = 1.0f; + static const float CVVDP_ch_trans_w = 0.8081134557723999f; + + // Baseband weights per channel (applied to the baseband band only) + static const float4 CVVDP_baseband_weight = float4( + 0.0036334486212581396f, + 1.6627724170684814f, + 4.11874532699585f, + 25.25969886779785f); + + // JOD regression parameters + static const float CVVDP_jod_a = 0.0439569391310215f; + static const float CVVDP_jod_exp = 0.9302042722702026f; + + // Cross-channel masking weights: stored in LOG2 space (16 values). + // Repo uses them as a 4x4 matrix (reshape) and exponentiates with 2^w. :contentReference[oaicite:8]{index=8} + // We treat the list as row-major: rows = source channels, cols = destination channels. + static const float4 CVVDP_xcm_log2_row0 = float4(-0.18950104713439941f, -5.962151050567627f, -4.31834602355957f, -1.9321587085723877f); + static const float4 CVVDP_xcm_log2_row1 = float4(2.5655593872070312f, 0.34406712651252747f, -2.719646453857422f, -0.4970424771308899f); + static const float4 CVVDP_xcm_log2_row2 = float4(3.8118371963500977f, -1.0051705837249756f, -0.5193376541137695f, -0.5653647780418396f); + static const float4 CVVDP_xcm_log2_row3 = float4(-7.054771423339844f, -5.527150630950928f, -3.5106418132781982f, -2.08804988861084f); + + // ---------------------------------------------------------------------------- + // Eq-like pieces (ColorVideoVDP paper / repo-matching behavior) + + // Eq CMM: mutual masking signal between test & ref (mult-mutual uses min(abs(.)) commonly) + float CVVDP_MutualMask(float Cprime_test, float Cprime_ref) + { + return min(abs(Cprime_test), abs(Cprime_ref)); + } + + // Mask pool (cross-channel mixing): + // M_dst = sum_src ( pooled_src * 2^(xcm_log2[src,dst]) ) + float4 CVVDP_XChannelMaskPool(float4 pooled_src) + { + // Build per-destination columns + float4 wcol0 = exp2(float4(CVVDP_xcm_log2_row0.x, CVVDP_xcm_log2_row1.x, CVVDP_xcm_log2_row2.x, CVVDP_xcm_log2_row3.x)); + float4 wcol1 = exp2(float4(CVVDP_xcm_log2_row0.y, CVVDP_xcm_log2_row1.y, CVVDP_xcm_log2_row2.y, CVVDP_xcm_log2_row3.y)); + float4 wcol2 = exp2(float4(CVVDP_xcm_log2_row0.z, CVVDP_xcm_log2_row1.z, CVVDP_xcm_log2_row2.z, CVVDP_xcm_log2_row3.z)); + float4 wcol3 = exp2(float4(CVVDP_xcm_log2_row0.w, CVVDP_xcm_log2_row1.w, CVVDP_xcm_log2_row2.w, CVVDP_xcm_log2_row3.w)); + + return float4(dot(pooled_src, wcol0), dot(pooled_src, wcol1), dot(pooled_src, wcol2), dot(pooled_src, wcol3)); + } + + // Phase uncertainty scale (repo multiplies by 10^mask_c) :contentReference[oaicite:9]{index=9} + float CVVDP_PhaseUncertaintyScale(float M) + { + return M * pow(10.0f, CVVDP_mask_c); + } + + // Transducer: D = 10^d_max * pow_neg(Cp, mask_p) / (0.2 + M) + float CVVDP_Transducer(float Cprime, float M) + { + float Dmax = pow(10.0f, CVVDP_d_max); + return cvvdp_safe_div(Dmax * cvvdp_pow_neg(Cprime, CVVDP_mask_p), 0.2f + M); + } + + // Soft clamp for difference (dclamp_type="soft"). + // Repo has dclamp_type="soft" but the exact soft curve is implementation-specific. + // This function is a safe "soft saturation" that preserves small values and asymptotically approaches Dmax. + // If you later mirror the repo’s exact clamp expression, swap this function out. + float CVVDP_DiffClampSoft(float D) + { + float Dmax = pow(10.0f, CVVDP_d_max); + // smooth saturation: Dhat = Dmax * D / (Dmax + D) + return cvvdp_safe_div(Dmax * D, Dmax + D); + } + + // ---------------------------------------------------------------------------- + // CSF hook (analytic): use your CastleCSF port instead of repo LUT. + // + // The repo applies sensitivity_correction in dB (negative reduces sensitivity). :contentReference[oaicite:10]{index=10} + // Convert dB to linear gain: gain = 10^(dB/20). + static const float CVVDP_sensitivity_correction_db = -0.2797423303127289f; + static const float CVVDP_sensitivity_gain = 0.968303f; // approx 10^(-0.279742/20), computed once + + // Computes per-channel sensitivity S for (rho_cpd, omega_hz, Lbkg_cd_m2). + // Channel mapping: + // 0: Ysust -> achromatic sustained + // 1: RG -> red-green chromatic + // 2: YV -> yellow-violet chromatic + // 3: Ytrans-> achromatic transient + float CVVDP_Sensitivity_Analytic(int channel, float rho_cpd, float omega_hz, float Lbkg_cd_m2, float ecc_deg, float vis_field_deg, float area_deg2) + { + // Your CastleCSF implementation likely exposes something like: + // Eq27_S_Ach / Eq28_S_RG / Eq29_S_YV or a unified entrypoint. + // We'll use your unified "mechanism sensitivity vector" pattern: + // + // IMPORTANT: ColorVideoVDP uses omega choices [0, 5] in config (sust vs trans), but we accept omega_hz as input. :contentReference[oaicite:11]{index=11} + + // Use your CastleCSF energy helper to get S for a unit contrast direction. + // If you have a direct "sensitivity" function, use it instead. + // + // Here’s a practical mapping: + float S = 0.0f; + if (channel == CVVDP_CH_Y_SUST) { + S = renodx::color::castlecsf::Eq27_S_Ach(rho_cpd, omega_hz, ecc_deg, vis_field_deg, area_deg2, Lbkg_cd_m2); + } else if (channel == CVVDP_CH_RG) { + S = renodx::color::castlecsf::Eq28_S_RG(rho_cpd, omega_hz, ecc_deg, vis_field_deg, area_deg2, Lbkg_cd_m2); + } else if (channel == CVVDP_CH_YV) { + S = renodx::color::castlecsf::Eq29_S_YV(rho_cpd, omega_hz, ecc_deg, vis_field_deg, area_deg2, Lbkg_cd_m2); + } else // CVVDP_CH_Y_TRANS + { + // In your CastleCSF port, transient is part of Eq27_S_Ach already (sust+trans mixture). + // But ColorVideoVDP conceptually separates Ysust and Ytrans temporal channels before masking. + // + // If you can directly compute the transient-only component, prefer that. + // Otherwise, approximate by using Eq27 at omega_hz with parameters that emphasize transient; + // as a fallback, reuse Eq27 (better than returning 0). + S = renodx::color::castlecsf::Eq27_S_Ach(rho_cpd, omega_hz, ecc_deg, vis_field_deg, area_deg2, Lbkg_cd_m2); + } + + return CVVDP_sensitivity_gain * S; + } + + // ---------------------------------------------------------------------------- + // Contrast computation hook (contrast="weber_g1") :contentReference[oaicite:12]{index=12} + // + // You supply the Laplacian band value L_band and the (upsampled) sustained Gaussian background for reference. + // This matches the paper’s definition style: C = L / Ybkg. + // If you do pyramid scaling (factor 2 for interior bands), do it before calling this function. + float CVVDP_Contrast_WeberG1(float L_band, float Ybkg_ref) + { + // Avoid division by 0 exactly; don’t epsilon-clamp denom unless you want stricter numerical behavior. + return cvvdp_safe_div(L_band, Ybkg_ref); + } + + // ---------------------------------------------------------------------------- + // Per-band/channel response pipeline (single pixel / sample) + // + // Inputs: + // C_test, C_ref : band-limited contrasts (already computed) + // S : sensitivity for this band/channel + // M_mask : masking signal for this channel at this pixel (already pooled+blurred+mixed) + // + // Output: + // D_hat : (optionally clamped) distortion response for this band/channel + float CVVDP_BandResponse(float C_test, float C_ref, float S, float M_mask) + { + // Encode contrasts + float Cp_test = C_test * S; + float Cp_ref = C_ref * S; + + // Mutual masking base (if you build masking from Cmm) + // In mult-mutual, the masking signal is constructed externally from Cmm^q, pooled, mixed, etc. + // Here M_mask is assumed to already be in final units (after phase uncertainty scaling). + + // Transducer + float D = CVVDP_Transducer(Cp_test - Cp_ref, M_mask); + + // Optional clamp + if (true) // dclamp_type == "soft" + return CVVDP_DiffClampSoft(D); + else + return min(D, pow(10.0f, CVVDP_d_max)); + } + + // ---------------------------------------------------------------------------- + // Mask construction helpers (single pixel) + // Caller responsibilities: + // - compute Cmm per channel (min(abs(Cp_test), abs(Cp_ref))) + // - raise to q[dst] (per-destination exponent!) then spatially blur (GaussianBlur sigma=pu_dilate) :contentReference[oaicite:13]{index=13} + // - feed the blurred per-source vector into xchannel pool + // + // Because blur is an image operation, we provide a scalar “mix” step here. + + float4 CVVDP_MaskPowPerDst(float4 Cmm_src, int dstChannel) + { + float q = (dstChannel == 0) ? CVVDP_mask_q.x : (dstChannel == 1) ? CVVDP_mask_q.y : + (dstChannel == 2) ? CVVDP_mask_q.z : + CVVDP_mask_q.w; + + return float4( + cvvdp_safe_pow(abs(Cmm_src.x), q), + cvvdp_safe_pow(abs(Cmm_src.y), q), + cvvdp_safe_pow(abs(Cmm_src.z), q), + cvvdp_safe_pow(abs(Cmm_src.w), q)); + } + + // Mix xchannel weights after blur: + // blurred_pow_src is (Cmm_src^q[dst]) already blurred spatially. + float CVVDP_MaskFinalFromBlurredPow(float4 blurred_pow_src, int dstChannel) + { + float4 mixed = CVVDP_XChannelMaskPool(blurred_pow_src); + float Mdst = (dstChannel == 0) ? mixed.x : (dstChannel == 1) ? mixed.y : + (dstChannel == 2) ? mixed.z : + mixed.w; + + return CVVDP_PhaseUncertaintyScale(Mdst); + } + + // ---------------------------------------------------------------------------- + // Pooling helpers (p-norm building blocks) + // Note: These are scalar reducers; you’ll do the summations over pixels/bands/frames externally. + + // accumulate sum(|x|^p) + float CVVDP_PNormAcc(float sumPow, float x, float p) + { + return sumPow + pow(abs(x), p); + } + + // finalize: (sumPow)^(1/p) + float CVVDP_PNormFinal(float sumPow, float p) + { + return pow(max(sumPow, 0.0f), 1.0f / p); + } + + // ---------------------------------------------------------------------------- + // Per-channel weights used during pooling + float CVVDP_ChannelWeight(int channel) + { + if (channel == CVVDP_CH_Y_SUST) + return 1.0f; + if (channel == CVVDP_CH_RG) + return CVVDP_ch_chrom_w; + if (channel == CVVDP_CH_YV) + return CVVDP_ch_chrom_w; + return CVVDP_ch_trans_w; + } + + // ---------------------------------------------------------------------------- + // Baseband weight per channel (apply on baseband band only) + float CVVDP_BasebandWeight(int channel) + { + return (channel == 0) ? CVVDP_baseband_weight.x : (channel == 1) ? CVVDP_baseband_weight.y : + (channel == 2) ? CVVDP_baseband_weight.z : + CVVDP_baseband_weight.w; + } + + // ---------------------------------------------------------------------------- + // JOD mapping + // The repo uses jod_a, jod_exp; the exact expression is in cvvdp_metric.py but our web view is truncated. + // A standard ColorVideoVDP mapping used in practice is: + // JOD = 10 - jod_a * Q^jod_exp + // This matches the visible beginning ("Q_JOD = 10.") and the params. :contentReference[oaicite:14]{index=14} + float CVVDP_MetricToJOD(float Q) + { + return 10.0f - CVVDP_jod_a * pow(max(Q, 0.0f), CVVDP_jod_exp); + } + + } // namespace colorvideovdp + } // namespace color +} // namespace renodx +#endif // COLORVIDEOVDP_HLSL diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/convert.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/convert.hlsl new file mode 100644 index 0000000000..229cc76bcf --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/convert.hlsl @@ -0,0 +1,128 @@ +#ifndef RENODX_SHADERS_COLOR_CONVERT_HLSL +#define RENODX_SHADERS_COLOR_CONVERT_HLSL + +#include "PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl" + +namespace renodx +{ + namespace color + { + namespace convert + { + static const float COLOR_SPACE_UNKNOWN = -1; + static const float COLOR_SPACE_NONE = -1; + static const float COLOR_SPACE_BT709 = 0; + static const float COLOR_SPACE_BT2020 = 1.f; + static const float COLOR_SPACE_AP1 = 2.f; + + float3 ColorSpaceFromBT709(float3 color, float color_space) + { + [branch] if (color_space == COLOR_SPACE_BT2020) + { + color = renodx::color::bt2020::from::BT709(color); + } + else + { + [branch] if (color_space == COLOR_SPACE_AP1) + { + color = renodx::color::ap1::from::BT709(color); + } + else + { + color = color; + } + } + return color; + } + + float3 ColorSpaceFromBT2020(float3 color, float color_space) + { + [branch] if (color_space == COLOR_SPACE_BT709) + { + color = renodx::color::bt709::from::BT2020(color); + } + else + { + [branch] if (color_space == COLOR_SPACE_AP1) + { + color = renodx::color::ap1::from::BT2020(color); + } + else + { + color = color; + } + } + return color; + } + + float3 ColorSpaceFromAP1(float3 color, float color_space) + { + [branch] if (color_space == COLOR_SPACE_BT709) + { + color = renodx::color::bt709::from::AP1(color); + } + else + { + [branch] if (color_space == COLOR_SPACE_BT2020) + { + color = renodx::color::bt2020::from::AP1(color); + } + else + { + color = color; + } + } + return color; + } + + float3 ColorSpaces(float3 color, float input_color_space, float output_color_space) + { + [branch] if (input_color_space == COLOR_SPACE_BT709) + { + color = ColorSpaceFromBT709(color, output_color_space); + } + else + { + [branch] if (input_color_space == COLOR_SPACE_BT2020) + { + color = ColorSpaceFromBT2020(color, output_color_space); + } + else + { + [branch] if (input_color_space == COLOR_SPACE_AP1) + { + color = ColorSpaceFromAP1(color, output_color_space); + } + else + { + color = color; + } + } + } + return color; + } + + float Luminance(float3 color, float color_space) + { + [branch] if (color_space == COLOR_SPACE_BT2020) + { + return renodx::color::y::from::BT2020(color); + } + else + { + [branch] if (color_space == COLOR_SPACE_AP1) + { + return renodx::color::y::from::AP1(color); + } + else + { + return renodx::color::y::from::BT709(color); + } + } + } + + } // namespace convert + } // namespace color +} // namespace renodx + +#endif // RENODX_SHADERS_COLOR_CONVERT_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/dtucs.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/dtucs.hlsl new file mode 100644 index 0000000000..b11c349ad5 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/dtucs.hlsl @@ -0,0 +1,197 @@ +#ifndef RENODX_SHADERS_COLOR_DTUCS_HLSL +#define RENODX_SHADERS_COLOR_DTUCS_HLSL + +#include "PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + // Copyright 2022 - Aurélien PIERRE / darktable project + // URL: https://eng.aurelienpierre.com/2022/02/color-saturation-control-for-the-21th-century/ + // The following source code is released under the MIT license + // (https://opensource.org/licenses/MIT) with the following addenda: + // * Any reuse of this code shall include the names of the author and of the project, as well as the source URL, + // * Any implementation of this colour space MUST call it "darktable Uniform Color Space" or + // "darktable UCS" in the end - user interface of the software. + namespace dtucs + { + const static float L_WHITE_VALUE = 1.f; + const static float L_WHITE_HAT = pow(L_WHITE_VALUE, 0.631651345306265f); + const static float L_WHITE = (2.098883786377f * L_WHITE_HAT) / (L_WHITE_HAT + 1.12426773749357f); + + namespace uvY + { + namespace from + { + static const float3x3 xyToUVD = { + -0.783941002840055f, 0.277512987809202f, 0.153836578598858f, + 0.745273540913283f, -0.205375866083878f, -0.165478376301988f, + 0.318707282433486f, 2.16743692732158f, 0.291320554395942f + }; + + static const float2x2 UVStarToUVStarPrime = { + -1.124983854323892f, -0.980483721769325f, + 1.86323315098672f, 1.971853092390862f + }; + + float3 BT709(float3 bt709) + { + float3 xyY = xyY::from::BT709(bt709); + float3 value; + if (xyY[2] == 0.f) { + value = 0; + } else { + float3 UVD = mul(xyToUVD, float3(xyY.xy, 1.f)); + + float2 UV = UVD.xy / UVD.z; + + float2 UVStar = float2(1.39656225667f, 1.4513954287f) * UV / (abs(UV) + float2(1.49217352929f, 1.52488637914f)); + + float2 UVStarPrime = mul(UVStarToUVStarPrime, UVStar); + + value = float3(UVStarPrime, xyY[2]); + } + return value; + } + } // namespace from + } // namespace uvY + + namespace jch + { + namespace from + { + float3 BT709(float3 bt709, float cz = 1.f) + { + float3 uvY = uvY::from::BT709(bt709); + + float L_star_hat = pow(uvY[2], 0.631651345306265f); + float L_star = 2.098883786377f * L_star_hat / (L_star_hat + 1.12426773749357f); + + float M2 = dot(uvY.xy, uvY.xy); + + float C = 15.932993652962535 * pow(L_star, 0.6523997524738018) * pow(M2, 0.6007557017508491) / L_WHITE; + float J = pow(L_star / L_WHITE, cz); + float H = atan2(uvY[1], uvY[0]); + + return float3(J, C, H); + } + } // from + } // jch + + namespace hcb + { + namespace from + { + float3 BT709(float3 bt709, float cz = 1.f) + { + float3 jch = jch::from::BT709(bt709); + float J = jch[0]; + float C = jch[1]; + float H = jch[2]; + + float B = J * (pow(C, 1.33654221029386) + 1.f); + + return float3(H, C, B); + } + } // from + } // hcb + + namespace hsb + { + namespace from + { + float3 BT709(float3 bt709, float cz = 1.f) + { + float3 hcb = hcb::from::BT709(bt709); + float H = hcb[0]; + float C = hcb[1]; + float B = hcb[2]; + + float S = C / B; + + return float3(H, S, B); + } + } // from + } // hsb + + } // namespace dtucs + + namespace bt709 + { + namespace from + { + namespace dtucs + { + static const float2x2 UVStarPrimeToUVStar = { + -5.037522385190711f, -2.504856328185843f, + 4.760029407436461f, 2.874012963239247f + }; + + static const float3x3 UVToxyD = { + 0.167171472114775f, 0.141299802443708f, -0.00801531300850582f, + -0.150959086409163f, -0.155185060382272f, -0.00843312433578007f, + 0.940254742367256f, 1.f, -0.0256325967652889f + }; + + float3 uvY(float3 uvY) + { + float2 UVStar = mul(UVStarPrimeToUVStar, uvY.xy); + + float2 UV = float2(-1.49217352929f, -1.52488637914f) * UVStar / (abs(UVStar) - float2(1.39656225667f, 1.4513954287f)); + + float3 xyD = mul(UVToxyD, float3(UV, 1.f)); + + float3 xyY; + + xyY.xy = renodx::math::DivideSafe(xyD.xy, xyD.z, 0); + + xyY[2] = uvY[2]; + + return bt709::from::xyY(xyY); + } + + float3 JCH(float3 jch, float cz = 1.f) + { + float J = jch[0]; + float C = jch[1]; + float H = jch[2]; + + float L_star = pow(J, (1 / cz)) * color::dtucs::L_WHITE; + + float M = pow(C * color::dtucs::L_WHITE / (15.932993652962535 * pow(L_star, 0.6523997524738018)), 0.8322850678616855); + + float Y = pow(-1.12426773749357f * L_star / (L_star - 2.098883786377), 1.5831518565279648f); + + return bt709::from::dtucs::uvY(float3(M * cos(H), M * sin(H), Y)); + } + + float3 HCB(float3 hcb, float cz = 1.f) + { + float H = hcb[0]; + float C = hcb[1]; + float B = hcb[2]; + + float J = B / (pow(C, 1.33654221029386) + 1.f); + + return bt709::from::dtucs::JCH(float3(J, C, H), cz); + } + + float3 HSB(float3 hsb, float cz = 1.f) + { + float H = hsb[0]; + float S = hsb[1]; + float B = hsb[2]; + + float C = S * B; + + return bt709::from::dtucs::HCB(float3(H, C, B), cz); + } + + } // dtucs + } // namespace from + } // namespace bt709 + } // namespace color +} // namespace renodx +#endif // RENODX_SHADERS_COLOR_DTUCS_HLSL diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/gamma.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/gamma.hlsl new file mode 100644 index 0000000000..cb369f6712 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/gamma.hlsl @@ -0,0 +1,62 @@ +#ifndef RENODX_SHADERS_COLOR_GAMMA_HLSL +#define RENODX_SHADERS_COLOR_GAMMA_HLSL + +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + namespace gamma + { + +#define ENCODE(T) \ + T Encode(T c, float gamma = 2.2f) \ + { \ + return pow(c, 1.f / gamma); \ + } + + ENCODE(float) + ENCODE(float2) + ENCODE(float3) + +#define ENCODE_SAFE(T) \ + T EncodeSafe(T c, float gamma = 2.2f) \ + { \ + return renodx::math::SignPow(c, 1.f / gamma); \ + } + + ENCODE_SAFE(float) + ENCODE_SAFE(float2) + ENCODE_SAFE(float3) + +#define DECODE(T) \ + T Decode(T c, float gamma = 2.2f) \ + { \ + return pow(c, gamma); \ + } + + DECODE(float) + DECODE(float2) + DECODE(float3) + +#define DECODE_SAFE(T) \ + T DecodeSafe(T c, float gamma = 2.2f) \ + { \ + return renodx::math::SignPow(c, gamma); \ + } + + DECODE_SAFE(float) + DECODE_SAFE(float2) + DECODE_SAFE(float3) + +#undef ENCODE +#undef ENCODE_SAFE +#undef DECODE +#undef DECODE_SAFE + + } // namespace gamma + } // namespace color +} // namespace renodx + +#endif // RENODX_SHADERS_COLOR_GAMMA_HLSL diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/gamut.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/gamut.hlsl new file mode 100644 index 0000000000..0544c6b47c --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/gamut.hlsl @@ -0,0 +1,650 @@ +#ifndef SRC_SHADERS_COLOR_GAMUT_HLSL_ +#define SRC_SHADERS_COLOR_GAMUT_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/color/macleod_boynton.hlsl" + +namespace renodx +{ + namespace color + { + namespace gamut + { + + // MacLeod-Boynton-space gamut logic, including the CIE 170-2 human-gamut + // boundary data and helpers built on top of it. + + static const float EPSILON = 1e-20f; + float2 CIE1702WhiteChromaticity() + { + return renodx::color::macleod_boynton::from::D65XY(); + } + + static const float MB_NEAR_WHITE_EPSILON = 1e-14f; + static const float INTERVAL_MAX = renodx::math::FLT_MAX; + static const float CIE1702_RAY_T_MAX = 1e20f; + static const int CIE1702_EDGE_COUNT = 7; + static const float2 CIE1702_HALFSPACE_NORMALS[CIE1702_EDGE_COUNT] = { + float2(-0.043889f, -0.006807f), + float2(-0.007821f, -0.008564f), + float2(-0.000604f, -0.007942f), + float2(0.f, -0.080835f), + float2(0.953597f, 0.307020f), + float2(-0.060969f, 0.019752f), + float2(-0.106895f, 0.004035f), + }; + static const float CIE1702_HALFSPACE_NUMERATORS[CIE1702_EDGE_COUNT] = { + 0.0065035249f, + 0.00104900495f, + 0.000207697044f, + 0.00165556648f, + 0.252472349f, + 0.0241967351f, + 0.0199621232f, + }; + static const int CIE1702_BOUNDARY_POINT_COUNT = 89; + static const float2 CIE1702_BOUNDARY_LS[CIE1702_BOUNDARY_POINT_COUNT] = { + float2(0.690547000f, 0.855670000f), + float2(0.684874000f, 0.835495000f), + float2(0.677571000f, 0.858452000f), + float2(0.670708000f, 0.915226000f), + float2(0.662656000f, 0.953597000f), + float2(0.645977000f, 0.991436000f), + float2(0.627777000f, 0.996399000f), + float2(0.605668000f, 0.959504000f), + float2(0.585916000f, 0.898535000f), + float2(0.565891000f, 0.807527000f), + float2(0.551744000f, 0.731596000f), + float2(0.539800000f, 0.641510000f), + float2(0.531511000f, 0.548520000f), + float2(0.527476000f, 0.441625000f), + float2(0.524357000f, 0.343327000f), + float2(0.525096000f, 0.258774000f), + float2(0.528335000f, 0.184906000f), + float2(0.533931000f, 0.125009000f), + float2(0.540738000f, 0.081120000f), + float2(0.547797000f, 0.052248000f), + float2(0.555470000f, 0.033091000f), + float2(0.563711000f, 0.020925000f), + float2(0.572275000f, 0.013104000f), + float2(0.580161000f, 0.007722000f), + float2(0.588088000f, 0.004342000f), + float2(0.596380000f, 0.002563000f), + float2(0.603889000f, 0.001511000f), + float2(0.611831000f, 0.000907000f), + float2(0.619954000f, 0.000546000f), + float2(0.627978000f, 0.000332000f), + float2(0.636807000f, 0.000198000f), + float2(0.646077000f, 0.000120000f), + float2(0.655844000f, 0.000074000f), + float2(0.666621000f, 0.000045000f), + float2(0.679293000f, 0.000028000f), + float2(0.692851000f, 0.000017000f), + float2(0.708852000f, 0.000011000f), + float2(0.726397000f, 0.000007000f), + float2(0.746141000f, 0.000005000f), + float2(0.767738000f, 0.000003000f), + float2(0.788583000f, 0.000002000f), + float2(0.810139000f, 0.000001000f), + float2(0.831629000f, 0.000001000f), + float2(0.852855000f, 0.000001000f), + float2(0.871948000f, 0.000001000f), + float2(0.888841000f, 0.000000000f), + float2(0.903949000f, 0.000000000f), + float2(0.917401000f, 0.000000000f), + float2(0.927421000f, 0.000000000f), + float2(0.935885000f, 0.000000000f), + float2(0.943662000f, 0.000000000f), + float2(0.950920000f, 0.000000000f), + float2(0.954901000f, 0.000000000f), + float2(0.958448000f, 0.000000000f), + float2(0.961871000f, 0.000000000f), + float2(0.964655000f, 0.000000000f), + float2(0.966375000f, 0.000000000f), + float2(0.967596000f, 0.000000000f), + float2(0.968455000f, 0.000000000f), + float2(0.969042000f, 0.000000000f), + float2(0.969394000f, 0.000000000f), + float2(0.969636000f, 0.000000000f), + float2(0.969674000f, 0.000000000f), + float2(0.969676000f, 0.000000000f), + float2(0.969559000f, 0.000000000f), + float2(0.969269000f, 0.000000000f), + float2(0.968966000f, 0.000000000f), + float2(0.968597000f, 0.000000000f), + float2(0.968192000f, 0.000000000f), + float2(0.967755000f, 0.000000000f), + float2(0.967192000f, 0.000000000f), + float2(0.966525000f, 0.000000000f), + float2(0.965860000f, 0.000000000f), + float2(0.965189000f, 0.000000000f), + float2(0.964468000f, 0.000000000f), + float2(0.963785000f, 0.000000000f), + float2(0.963138000f, 0.000000000f), + float2(0.962455000f, 0.000000000f), + float2(0.961723000f, 0.000000000f), + float2(0.960958000f, 0.000000000f), + float2(0.960173000f, 0.000000000f), + float2(0.959345000f, 0.000000000f), + float2(0.958384000f, 0.000000000f), + float2(0.957388000f, 0.000000000f), + float2(0.956387000f, 0.000000000f), + float2(0.955383000f, 0.000000000f), + float2(0.954435000f, 0.000000000f), + float2(0.953603000f, 0.000000000f), + float2(0.952920000f, 0.000000000f), + }; + + float RayExitTCIE1702(float2 origin, float2 direction) + { + if (dot(direction, direction) <= MB_NEAR_WHITE_EPSILON) { + return CIE1702_RAY_T_MAX; + } + + float2 white_to_origin = CIE1702WhiteChromaticity() - origin; + float t_best = CIE1702_RAY_T_MAX; + bool hit_any = false; + + [unroll] for (int i = 0; i < CIE1702_EDGE_COUNT; ++i) + { + float2 halfspace_normal = CIE1702_HALFSPACE_NORMALS[i]; + float denom = dot(halfspace_normal, direction); + float numerator = + CIE1702_HALFSPACE_NUMERATORS[i] + dot(halfspace_normal, white_to_origin); + float t = denom > 1e-8f ? numerator * rcp(denom) : CIE1702_RAY_T_MAX; + t_best = min(t_best, t); + hit_any = hit_any || (denom > 1e-8f); + } + + return hit_any ? max(t_best, 0.f) : CIE1702_RAY_T_MAX; + } + + float RayExitTCIE1702D(float2 direction) + { + return RayExitTCIE1702(CIE1702WhiteChromaticity(), direction); + } + + float RayExitTCIE1702(float2 ls) + { + float2 direction = ls - CIE1702WhiteChromaticity(); + return RayExitTCIE1702(CIE1702WhiteChromaticity(), direction); + } + + float Cross2(float2 a, float2 b) + { + return a.x * b.y - a.y * b.x; + } + + bool RaySegmentHit2D(float2 origin, float2 direction, float2 a, float2 b, out float t_hit) + { + t_hit = 0.f; + float2 edge = b - a; + float denom = Cross2(direction, edge); + if (abs(denom) <= EPSILON) + return false; + + float2 a_origin = a - origin; + float t = Cross2(a_origin, edge) / denom; + float u = Cross2(a_origin, direction) / denom; + if (t < 0.f || u < 0.f || u > 1.f) + return false; + + t_hit = t; + return true; + } + + float RayExitTCIE1702Precise(float2 origin, float2 direction) + { + if (dot(direction, direction) <= MB_NEAR_WHITE_EPSILON) { + return CIE1702_RAY_T_MAX; + } + + float t_best = CIE1702_RAY_T_MAX; + bool hit_any = false; + + [loop] for (int i = 0; i < CIE1702_BOUNDARY_POINT_COUNT; ++i) + { + int j = i + 1; + if (j == CIE1702_BOUNDARY_POINT_COUNT) { + j = 0; + } + + float t_hit = 0.f; + if (RaySegmentHit2D( + origin, + direction, + CIE1702_BOUNDARY_LS[i], + CIE1702_BOUNDARY_LS[j], + t_hit)) { + t_best = min(t_best, t_hit); + hit_any = true; + } + } + + return hit_any ? max(t_best, 0.f) : CIE1702_RAY_T_MAX; + } + + float RayExitTCIE1702PreciseD(float2 direction) + { + return RayExitTCIE1702Precise(CIE1702WhiteChromaticity(), direction); + } + + float2 MBFromWeightedPrimary(float3 weighted_lms_primary) + { + return renodx::color::macleod_boynton::from::WeightedLMS(weighted_lms_primary).xy; + } + + float3 RGBToWeightedLMSPrimaryColumn(float3x3 rgb_to_lms_weighted_mat, uint primary_index) + { + return float3( + rgb_to_lms_weighted_mat[0][primary_index], + rgb_to_lms_weighted_mat[1][primary_index], + rgb_to_lms_weighted_mat[2][primary_index]); + } + + float3 RGBToAdaptiveWeightedLMSPrimaryColumn( + float3x3 rgb_to_lms_weighted_mat, + uint primary_index, + float3 current_adaptive_state_lms) + { + return RGBToWeightedLMSPrimaryColumn(rgb_to_lms_weighted_mat, primary_index) / + current_adaptive_state_lms; + } + + void MakeRGBTriangleInMBFromWeightedPrimaries( + float3 weighted_r, + float3 weighted_g, + float3 weighted_b, + out float2 r, + out float2 g, + out float2 b) + { + r = MBFromWeightedPrimary(weighted_r); + g = MBFromWeightedPrimary(weighted_g); + b = MBFromWeightedPrimary(weighted_b); + } + + void MakeRGBTriangleInMBWeighted( + float3x3 rgb_to_lms_weighted_mat, + out float2 r, + out float2 g, + out float2 b) + { + MakeRGBTriangleInMBFromWeightedPrimaries( + RGBToWeightedLMSPrimaryColumn(rgb_to_lms_weighted_mat, 0), + RGBToWeightedLMSPrimaryColumn(rgb_to_lms_weighted_mat, 1), + RGBToWeightedLMSPrimaryColumn(rgb_to_lms_weighted_mat, 2), + r, + g, + b); + } + + void MakeRGBTriangleInMBAdaptiveWeighted( + float3x3 rgb_to_lms_weighted_mat, + float3 current_adaptive_state_lms, + out float2 r, + out float2 g, + out float2 b) + { + MakeRGBTriangleInMBFromWeightedPrimaries( + RGBToAdaptiveWeightedLMSPrimaryColumn( + rgb_to_lms_weighted_mat, 0, current_adaptive_state_lms), + RGBToAdaptiveWeightedLMSPrimaryColumn( + rgb_to_lms_weighted_mat, 1, current_adaptive_state_lms), + RGBToAdaptiveWeightedLMSPrimaryColumn( + rgb_to_lms_weighted_mat, 2, current_adaptive_state_lms), + r, + g, + b); + } + + float RayMaxT_RGBTriangleInMB( + float2 origin, + float2 direction, + float2 r, + float2 g, + float2 b, + out bool has_solution) + { + has_solution = false; + if (dot(direction, direction) <= MB_NEAR_WHITE_EPSILON) + return 0.f; + + float t_best = INTERVAL_MAX; + float t_hit; + bool hit_any = false; + + if (RaySegmentHit2D(origin, direction, r, g, t_hit)) { + t_best = min(t_best, t_hit); + hit_any = true; + } + if (RaySegmentHit2D(origin, direction, g, b, t_hit)) { + t_best = min(t_best, t_hit); + hit_any = true; + } + if (RaySegmentHit2D(origin, direction, b, r, t_hit)) { + t_best = min(t_best, t_hit); + hit_any = true; + } + + has_solution = hit_any; + return hit_any ? max(t_best, 0.f) : 0.f; + } + + float NeutwoPeakClip(float x, float peak, float clip) + { + float peak_safe = max(peak, 0.f); + float clip_safe = max(clip, peak_safe); + float x2 = x * x; + float clip2 = clip_safe * clip_safe; + float peak2 = peak_safe * peak_safe; + float denominator_squared = mad(x2, (clip2 - peak2), clip2 * peak2); + return (clip_safe * peak_safe * x) * rsqrt(max(denominator_squared, 1e-20f)); + } + + float NeutwoScaleFromRayT(float t_peak, float t_clip) + { + float t_peak_safe = max(t_peak, 0.f); + float t_clip_safe = max(t_clip, t_peak_safe); + return saturate(NeutwoPeakClip(1.f, t_peak_safe, t_clip_safe)); + } + + float SoftCompressionActivationFromRayT(float t_peak, float knee = 0.08f) + { + float outside = 1.f - saturate(t_peak); + return renodx::math::DivideSafe(outside, outside + max(knee, 1e-6f), 0.f); + } + + float3 ClampWeightedLMSToCIE1702(float3 lms_weighted_input, float purity_max = 1.f) + { + float3 lms_weighted_clamped = max(lms_weighted_input, 0); + float3 mb = renodx::color::macleod_boynton::from::WeightedLMS(lms_weighted_clamped); + float y_mb = mb.z; + float2 ls = mb.xy; + if (!(y_mb > EPSILON)) { + return float3(lms_weighted_clamped.x, lms_weighted_clamped.y, 0.f); + } + + float2 mb_white = CIE1702WhiteChromaticity(); + float2 direction = ls - mb_white; + if (dot(direction, direction) <= MB_NEAR_WHITE_EPSILON) { + return lms_weighted_clamped; + } + + float t_clip = RayExitTCIE1702D(direction); + float t_final = min(1.f, max(purity_max, 0.f) * t_clip); + float2 ls_out = mb_white + direction * t_final; + return renodx::color::macleod_boynton::WeightedLMSFromMacleodBoynton(ls_out, y_mb); + } + + float3 ClampWeightedLMSToCIE1702Precise(float3 lms_weighted_input, float purity_max = 1.f) + { + float3 lms_weighted_clamped = max(lms_weighted_input, 0); + float3 mb = renodx::color::macleod_boynton::from::WeightedLMS(lms_weighted_clamped); + float y_mb = mb.z; + float2 ls = mb.xy; + if (!(y_mb > EPSILON)) { + return float3(lms_weighted_clamped.x, lms_weighted_clamped.y, 0.f); + } + + float2 mb_white = CIE1702WhiteChromaticity(); + float2 direction = ls - mb_white; + if (dot(direction, direction) <= MB_NEAR_WHITE_EPSILON) { + return lms_weighted_clamped; + } + + float t_clip = RayExitTCIE1702PreciseD(direction); + float t_final = min(1.f, max(purity_max, 0.f) * t_clip); + float2 ls_out = mb_white + direction * t_final; + return renodx::color::macleod_boynton::WeightedLMSFromMacleodBoynton(ls_out, y_mb); + } + + float3 GamutCompressWeightedLMSCoreRGBBoundFromWeightedInput( + float3 lms_weighted_input, + float2 bound_r, + float2 bound_g, + float2 bound_b, + float strength) + { + float3 lms_weighted_clamped = ClampWeightedLMSToCIE1702(max(lms_weighted_input, 0)); + float3 mb = renodx::color::macleod_boynton::from::WeightedLMS(lms_weighted_clamped); + float y_mb = mb.z; + float2 ls = mb.xy; + if (!(y_mb > EPSILON)) { + return float3(lms_weighted_clamped.x, lms_weighted_clamped.y, 0.f); + } + + float2 mb_white = CIE1702WhiteChromaticity(); + float2 direction = ls - mb_white; + if (dot(direction, direction) <= MB_NEAR_WHITE_EPSILON) { + return lms_weighted_clamped; + } + + bool has_peak = false; + float t_peak = RayMaxT_RGBTriangleInMB( + mb_white, direction, bound_r, bound_g, bound_b, has_peak); + float t_clip = RayExitTCIE1702D(direction); + if (!has_peak) { + t_peak = t_clip; + } + + float t_hard = saturate(t_peak); + float t_soft = NeutwoScaleFromRayT(min(t_peak, t_clip), t_clip); + float soft_mix = saturate(strength) * SoftCompressionActivationFromRayT(t_peak); + float t_final = lerp(t_hard, t_soft, soft_mix); + + float2 ls_out = mb_white + t_final * direction; + return renodx::color::macleod_boynton::WeightedLMSFromMacleodBoynton(ls_out, y_mb); + } + + float3 GamutCompressWeightedLMSCoreRGBBoundFromWeightedInput( + float3 lms_weighted_input, float3x3 bound_rgb_to_lms_weighted_mat, + float strength) + { + float2 bound_r; + float2 bound_g; + float2 bound_b; + MakeRGBTriangleInMBWeighted(bound_rgb_to_lms_weighted_mat, bound_r, bound_g, bound_b); + return GamutCompressWeightedLMSCoreRGBBoundFromWeightedInput( + lms_weighted_input, + bound_r, + bound_g, + bound_b, + strength); + } + + float3 GamutCompressWeightedLMSCoreRGBBoundFromAdaptiveWeightedInput( + float3 lms_weighted_input, + float3 current_adaptive_state_lms, + float3x3 bound_rgb_to_lms_weighted_mat, + float strength) + { + float2 bound_r; + float2 bound_g; + float2 bound_b; + MakeRGBTriangleInMBAdaptiveWeighted( + bound_rgb_to_lms_weighted_mat, + current_adaptive_state_lms, + bound_r, + bound_g, + bound_b); + return GamutCompressWeightedLMSCoreRGBBoundFromWeightedInput( + lms_weighted_input, + bound_r, + bound_g, + bound_b, + strength); + } + + float3 GamutCompressWeightedLMSCoreRGBBoundFromWeightedInputPrecise( + float3 lms_weighted_input, + float2 bound_r, + float2 bound_g, + float2 bound_b, + float strength) + { + float3 lms_weighted_clamped = ClampWeightedLMSToCIE1702Precise(max(lms_weighted_input, 0)); + float3 mb = renodx::color::macleod_boynton::from::WeightedLMS(lms_weighted_clamped); + float y_mb = mb.z; + float2 ls = mb.xy; + if (!(y_mb > EPSILON)) { + return float3(lms_weighted_clamped.x, lms_weighted_clamped.y, 0.f); + } + + float2 mb_white = CIE1702WhiteChromaticity(); + float2 direction = ls - mb_white; + if (dot(direction, direction) <= MB_NEAR_WHITE_EPSILON) { + return lms_weighted_clamped; + } + + bool has_peak = false; + float t_peak = RayMaxT_RGBTriangleInMB( + mb_white, direction, bound_r, bound_g, bound_b, has_peak); + float t_clip = RayExitTCIE1702PreciseD(direction); + if (!has_peak) { + t_peak = t_clip; + } + + float t_hard = saturate(t_peak); + float t_soft = NeutwoScaleFromRayT(min(t_peak, t_clip), t_clip); + float soft_mix = saturate(strength) * SoftCompressionActivationFromRayT(t_peak); + float t_final = lerp(t_hard, t_soft, soft_mix); + + float2 ls_out = mb_white + t_final * direction; + + return renodx::color::macleod_boynton::WeightedLMSFromMacleodBoynton(ls_out, y_mb); + } + + float3 GamutCompressWeightedLMSCoreRGBBoundFromWeightedInputPrecise( + float3 lms_weighted_input, float3x3 bound_rgb_to_lms_weighted_mat, + float strength) + { + float2 bound_r; + float2 bound_g; + float2 bound_b; + MakeRGBTriangleInMBWeighted(bound_rgb_to_lms_weighted_mat, bound_r, bound_g, bound_b); + return GamutCompressWeightedLMSCoreRGBBoundFromWeightedInputPrecise( + lms_weighted_input, + bound_r, + bound_g, + bound_b, + strength); + } + + float3 GamutCompressWeightedLMSCoreRGBBoundFromAdaptiveWeightedInputPrecise( + float3 lms_weighted_input, + float3 current_adaptive_state_lms, + float3x3 bound_rgb_to_lms_weighted_mat, + float strength) + { + float2 bound_r; + float2 bound_g; + float2 bound_b; + MakeRGBTriangleInMBAdaptiveWeighted( + bound_rgb_to_lms_weighted_mat, + current_adaptive_state_lms, + bound_r, + bound_g, + bound_b); + return GamutCompressWeightedLMSCoreRGBBoundFromWeightedInputPrecise( + lms_weighted_input, + bound_r, + bound_g, + bound_b, + strength); + } + + float3 GamutCompressLMS(float3 lms_input, float strength = 1.f) + { + float3 lms_weighted_input = renodx::color::macleod_boynton::WeighLMS(lms_input); + float3 lms_weighted_clamped = max(lms_weighted_input, 0); + float3 lms_weighted_out = lerp( + lms_weighted_clamped, + ClampWeightedLMSToCIE1702(lms_weighted_clamped), + saturate(strength)); + return renodx::color::macleod_boynton::UnweighLMS(lms_weighted_out); + } + + float3 GamutCompressLMSPrecise(float3 lms_input, float strength = 1.f) + { + float3 lms_weighted_input = renodx::color::macleod_boynton::WeighLMS(lms_input); + float3 lms_weighted_clamped = max(lms_weighted_input, 0); + float3 lms_weighted_out = lerp( + lms_weighted_clamped, + ClampWeightedLMSToCIE1702Precise(lms_weighted_clamped), + saturate(strength)); + return renodx::color::macleod_boynton::UnweighLMS(lms_weighted_out); + } + + float3 GamutCompressLMSBoundBT709(float3 lms_input, float strength = 1.f) + { + float3 lms_weighted_input = renodx::color::macleod_boynton::WeighLMS(lms_input); + float3 lms_weighted_out = GamutCompressWeightedLMSCoreRGBBoundFromWeightedInput( + lms_weighted_input, renodx::color::macleod_boynton::BT709_TO_LMS_WEIGHTED_MAT, strength); + return renodx::color::macleod_boynton::UnweighLMS(lms_weighted_out); + } + + float3 GamutCompressLMSBoundBT709Precise(float3 lms_input, float strength = 1.f) + { + float3 lms_weighted_input = renodx::color::macleod_boynton::WeighLMS(lms_input); + float3 lms_weighted_out = GamutCompressWeightedLMSCoreRGBBoundFromWeightedInputPrecise( + lms_weighted_input, renodx::color::macleod_boynton::BT709_TO_LMS_WEIGHTED_MAT, strength); + return renodx::color::macleod_boynton::UnweighLMS(lms_weighted_out); + } + + float3 GamutCompressLMSBoundBT2020(float3 lms_input, float strength = 1.f) + { + float3 lms_weighted_input = renodx::color::macleod_boynton::WeighLMS(lms_input); + float3 lms_weighted_out = GamutCompressWeightedLMSCoreRGBBoundFromWeightedInput( + lms_weighted_input, renodx::color::macleod_boynton::BT2020_TO_LMS_WEIGHTED_MAT, strength); + return renodx::color::macleod_boynton::UnweighLMS(lms_weighted_out); + } + + float3 GamutCompressLMSBoundBT2020Precise(float3 lms_input, float strength = 1.f) + { + float3 lms_weighted_input = renodx::color::macleod_boynton::WeighLMS(lms_input); + float3 lms_weighted_out = GamutCompressWeightedLMSCoreRGBBoundFromWeightedInputPrecise( + lms_weighted_input, renodx::color::macleod_boynton::BT2020_TO_LMS_WEIGHTED_MAT, strength); + return renodx::color::macleod_boynton::UnweighLMS(lms_weighted_out); + } + + float3 GamutCompressBT709(float3 bt709_input, float strength = 1.f) + { + float3 lms_weighted_out = GamutCompressWeightedLMSCoreRGBBoundFromWeightedInput( + mul(renodx::color::macleod_boynton::BT709_TO_LMS_WEIGHTED_MAT, bt709_input), + renodx::color::macleod_boynton::BT709_TO_LMS_WEIGHTED_MAT, + strength); + return mul(renodx::color::macleod_boynton::LMS_WEIGHTED_TO_BT709_MAT, lms_weighted_out); + } + + float3 GamutCompressBT709Precise(float3 bt709_input, float strength = 1.f) + { + float3 lms_weighted_out = GamutCompressWeightedLMSCoreRGBBoundFromWeightedInputPrecise( + mul(renodx::color::macleod_boynton::BT709_TO_LMS_WEIGHTED_MAT, bt709_input), + renodx::color::macleod_boynton::BT709_TO_LMS_WEIGHTED_MAT, + strength); + return mul(renodx::color::macleod_boynton::LMS_WEIGHTED_TO_BT709_MAT, lms_weighted_out); + } + + float3 GamutCompressBT2020(float3 bt2020_input, float strength = 1.f) + { + float3 lms_weighted_out = GamutCompressWeightedLMSCoreRGBBoundFromWeightedInput( + mul(renodx::color::macleod_boynton::BT2020_TO_LMS_WEIGHTED_MAT, bt2020_input), + renodx::color::macleod_boynton::BT2020_TO_LMS_WEIGHTED_MAT, + strength); + return mul(renodx::color::macleod_boynton::LMS_WEIGHTED_TO_BT2020_MAT, lms_weighted_out); + } + + float3 GamutCompressBT2020Precise(float3 bt2020_input, float strength = 1.f) + { + float3 lms_weighted_out = GamutCompressWeightedLMSCoreRGBBoundFromWeightedInputPrecise( + mul(renodx::color::macleod_boynton::BT2020_TO_LMS_WEIGHTED_MAT, bt2020_input), + renodx::color::macleod_boynton::BT2020_TO_LMS_WEIGHTED_MAT, + strength); + return mul(renodx::color::macleod_boynton::LMS_WEIGHTED_TO_BT2020_MAT, lms_weighted_out); + } + + } // namespace gamut + } // namespace color +} // namespace renodx + +#endif // SRC_SHADERS_COLOR_GAMUT_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/ictcp.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/ictcp.hlsl new file mode 100644 index 0000000000..778f2626e5 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/ictcp.hlsl @@ -0,0 +1,102 @@ +#ifndef SRC_SHADERS_COLOR_ICTCP_HLSL_ +#define SRC_SHADERS_COLOR_ICTCP_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/color/pq.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + + namespace ictcp + { + // https://professional.dolby.com/siteassets/pdfs/ictcp_dolbywhitepaper_v071.pdf + + static const float3x3 XYZ_TO_ICTCP_LMS_MAT = float3x3( + 0.359168797f, 0.697604775f, -0.0357883982f, + -0.192186400f, 1.10039842f, 0.0755404010f, + 0.00695759989f, 0.0749168023f, 0.843357980f); + + static const float3x3 ICTCP_LMS_TO_XYZ_MAT = float3x3( + 2.07036161f, -1.32659053f, 0.206681042f, + 0.364990383f, 0.680468797f, -0.0454616732f, + -0.0495028905f, -0.0495028905f, 1.18806946f); + + static const float3x3 BT709_TO_ICTCP_LMS_MAT = float3x3( + 0.295764088f, 0.623072445f, 0.0811667516f, + 0.156191974f, 0.727251648f, 0.116557933f, + 0.0351022854f, 0.156589955f, 0.808302998f); + + static const float3x3 ICTCP_LMS_TO_BT709_MAT = float3x3( + 6.17353248f, -5.32089900f, 0.147354885f, + -1.32403194f, 2.56026983f, -0.236238613f, + -0.0115983877f, -0.264921456f, 1.27652633f); + + static const float CROSSTALK = 0.04f; + static const float IPT_OPTIMIZATION = 1.0f; + + static const float3x3 CROSSTALK_MAT = float3x3( + 1.0f - (2 * CROSSTALK), CROSSTALK, CROSSTALK, + CROSSTALK, 1.0f - (2 * CROSSTALK), CROSSTALK, + CROSSTALK, CROSSTALK, 1.0f - (2 * CROSSTALK)); + + static const float3x3 XYZ_TO_DOLBY_LMS_MAT = mul(XYZ_D65_TO_HUNT_POINTER_ESTEVEZ_LMS_MAT, CROSSTALK_MAT); + + static const float3x3 PLMS_TO_IPT_OPTIMIZED_MAT = float3x3( + lerp(PLMS_TO_IPT_MAT[0], float3(0.5f, 0.5f, 0.0f), IPT_OPTIMIZATION), + PLMS_TO_IPT_MAT[1], + PLMS_TO_IPT_MAT[2]); + + static const float VECTORSCOPE_DEGREES = 65.f; + static const float ROTATION_POINT = VECTORSCOPE_DEGREES * renodx::math::PI / 180.f; + + static const float3x3 IPT_ROTATION_MAT = float3x3( + 1.f, 0, 0.f, + 0, cos(ROTATION_POINT), -sin(ROTATION_POINT), + 0, sin(ROTATION_POINT), cos(ROTATION_POINT)); + + static const float SCALE_FACTOR = 1.4f; + static const float3x3 IPT_SCALE_MAT = float3x3( + 1.0f, 1.0f, 1.0f, + SCALE_FACTOR, SCALE_FACTOR, SCALE_FACTOR, + 1.0f, 1.0f, 1.0f); + + static const float3x3 PLMS_TO_ICTCP_MAT = mul(IPT_ROTATION_MAT, PLMS_TO_IPT_OPTIMIZED_MAT) * IPT_SCALE_MAT; + + namespace from + { + float3 BT709(float3 bt709_color, float scaling = 100.f) + { + float3 lms = mul(mul(XYZ_TO_DOLBY_LMS_MAT, BT709_TO_XYZ_MAT), bt709_color); + float3 plms = pq::Encode(max(0, lms), scaling); + float3 ictcp_color = mul(PLMS_TO_ICTCP_MAT, plms); + return ictcp_color; + } + } // namespace from + } // namespace ictcp + + namespace bt709 + { + namespace from + { + + float3 ICtCp(float3 ictcp_color, float scaling = 100.f) + { + float3 plms_color = mul(renodx::math::Invert3x3(ictcp::PLMS_TO_ICTCP_MAT), ictcp_color); + float3 lms_color = pq::Decode(plms_color, scaling); + float3 bt709_color = mul( + mul( + renodx::math::Invert3x3(BT709_TO_XYZ_MAT), + renodx::math::Invert3x3(ictcp::XYZ_TO_DOLBY_LMS_MAT)), + lms_color); + return bt709_color; + } + + } // namespace from + } // namespace bt709 + } // namespace color +} // namespace renodx + +#endif // SRC_SHADERS_COLOR_ICTCP_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/ipt.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/ipt.hlsl new file mode 100644 index 0000000000..fce7fd37b0 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/ipt.hlsl @@ -0,0 +1,54 @@ +#ifndef SRC_SHADERS_COLOR_IPT_HLSL_ +#define SRC_SHADERS_COLOR_IPT_HLSL_ +#include "PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + + namespace ipt + { + // https://repository.rit.edu/cgi/viewcontent.cgi?article=3862&context=theses + // https://www.researchgate.net/publication/221677980_Development_and_Testing_of_a_Color_Space_IPT_with_Improved_Hue_Uniformity + + static const float3x3 XYZ_D65_TO_LMS_EBNER_MAT = float3x3( + 0.4002f, 0.7075f, -0.0807f, + -0.2280f, 1.1500f, 0.0612f, + 0.0000f, 0.0000f, 0.9184f); + + static const float RESPONSE_EXPONENT = 1.f / 2.3f; + namespace from + { + float3 BT709(float3 bt709_color) + { + float3 lms = mul(mul(XYZ_D65_TO_LMS_EBNER_MAT, BT709_TO_XYZ_MAT), bt709_color); + float3 plms = renodx::math::SignPow(lms, RESPONSE_EXPONENT); + float3 ipt_color = mul(PLMS_TO_IPT_MAT, plms); + return ipt_color; + } + } // namespace from + } // namespace ipt + + namespace bt709 + { + namespace from + { + float3 IPT(float3 ipt_color) + { + float3 plms_color = mul(renodx::math::Invert3x3(PLMS_TO_IPT_MAT), ipt_color); + float3 lms_color = renodx::math::SignPow(plms_color, 1.f / ipt::RESPONSE_EXPONENT); + float3 bt709_color = mul( + mul( + renodx::math::Invert3x3(BT709_TO_XYZ_MAT), + renodx::math::Invert3x3(ipt::XYZ_D65_TO_LMS_EBNER_MAT)), + lms_color); + return bt709_color; + } + + } // namespace from + } // namespace bt709 + } // namespace color +} // namespace renodx +#endif // SRC_SHADERS_COLOR_IPT_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/macleod_boynton.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/macleod_boynton.hlsl new file mode 100644 index 0000000000..670e94d708 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/macleod_boynton.hlsl @@ -0,0 +1,240 @@ +#ifndef SRC_SHADERS_COLOR_MACLEOD_BOYNTON_HLSL_ +#define SRC_SHADERS_COLOR_MACLEOD_BOYNTON_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl" + +namespace renodx +{ + namespace color + { + + namespace macleod_boynton + { + // Generic MacLeod-Boynton algebra over an explicitly chosen LMS basis. + // + // Formulation: + // l = Lw / (Lw + Mw) + // s = Sw / (Lw + Mw) + // y = Lw + Mw + // + // The caller is responsible for choosing: + // - the underlying LMS basis + // - the LMS weights used to form (Lw, Mw, Sw) + // - any white anchor or RGB<->LMS matrices + // + + // + // Reference data: + // CVRL functions: http://www.cvrl.org/cvrlfunctions.htm + // CVRL MacLeod-Boynton coordinates: http://www.cvrl.org/ccs.htm + + // Stockman-Sharpe LMS instantiated in MacLeod-Boynton coordinates using the + // repo's CIE 170-2 weighting constants. + // These weights are fixed observer-basis coefficients: + // - they weigh LMS to form weighted LMS / MB coordinates, + // - they carry a physiological achromatic axis tied to Yf / L+M-style + // bookkeeping, + // - they are not adaptation, gain-control, or bleaching parameters. + // Source direction: + // - CVRL/CIE physiological functions are linear transformations of the + // Stockman-Sharpe cone fundamentals. + // - Yf is the correct luminance/luminosity axis for this LMS basis; MB carries + // the weighted L+M term that matches that observer construction. + + static const float3x3 LMS_TO_XYZ_MAT = renodx::color::STOCKMAN_CVRL_LMS_TO_XYZ_2DEG_FIT; + static const float3x3 XYZ_TO_LMS_MAT = renodx::color::STOCKMAN_CVRL_XYZ_TO_LMS_2DEG_FIT; + static const float3 LMS_WEIGHTS = renodx::color::CIE1702_MB_CIE_WEIGHTS; + + static const float3x3 LMS_TO_LMS_WEIGHTED_MAT = float3x3( + LMS_WEIGHTS.x, 0.f, 0.f, + 0.f, LMS_WEIGHTS.y, 0.f, + 0.f, 0.f, LMS_WEIGHTS.z); + + static const float3x3 LMS_WEIGHTED_TO_LMS_MAT = float3x3( + 1.f / LMS_WEIGHTS.x, 0.f, 0.f, + 0.f, 1.f / LMS_WEIGHTS.y, 0.f, + 0.f, 0.f, 1.f / LMS_WEIGHTS.z); + + static const float3x3 XYZ_TO_LMS_WEIGHTED_MAT = float3x3( + mul(LMS_TO_LMS_WEIGHTED_MAT, XYZ_TO_LMS_MAT)); + + static const float3x3 BT709_TO_LMS_WEIGHTED_MAT = + mul(XYZ_TO_LMS_WEIGHTED_MAT, renodx::color::BT709_TO_XYZ_MAT); + + static const float3x3 BT2020_TO_LMS_WEIGHTED_MAT = + mul(XYZ_TO_LMS_WEIGHTED_MAT, renodx::color::BT2020_TO_XYZ_MAT); + + static const float3x3 LMS_WEIGHTED_TO_XYZ_MAT = + mul(LMS_TO_XYZ_MAT, LMS_WEIGHTED_TO_LMS_MAT); + + static const float3x3 LMS_WEIGHTED_TO_BT709_MAT = + mul(renodx::color::XYZ_TO_BT709_MAT, LMS_WEIGHTED_TO_XYZ_MAT); + + static const float3x3 LMS_WEIGHTED_TO_BT2020_MAT = + mul(renodx::color::XYZ_TO_BT2020_MAT, LMS_WEIGHTED_TO_XYZ_MAT); + + float3 WeighLMS(float3 lms, float3 lms_weights = CIE1702_MB_CIE_WEIGHTS) + { + return lms * lms_weights; + } + + float3 UnweighLMS(float3 lms_weighted, float3 lms_weights = CIE1702_MB_CIE_WEIGHTS) + { + return lms_weighted / lms_weights; + } + + float3x3 LMSWeightMatrix(float3 lms_weights = CIE1702_MB_CIE_WEIGHTS) + { + return float3x3( + lms_weights.x, 0.f, 0.f, + 0.f, lms_weights.y, 0.f, + 0.f, 0.f, lms_weights.z); + } + + float3x3 LMSUnweightMatrix(float3 lms_weights = CIE1702_MB_CIE_WEIGHTS) + { + return float3x3( + 1.f / lms_weights.x, 0.f, 0.f, + 0.f, 1.f / lms_weights.y, 0.f, + 0.f, 0.f, 1.f / lms_weights.z); + } + + float3x3 RGBToWeightedLMSMatrix(float3x3 xyz_to_lms, float3x3 rgb_to_xyz, float3 lms_weights = CIE1702_MB_CIE_WEIGHTS) + { + return mul(LMSWeightMatrix(lms_weights), mul(xyz_to_lms, rgb_to_xyz)); + } + + float3x3 WeightedLMSToRGBMatrix(float3x3 lms_to_xyz, float3x3 xyz_to_rgb, float3 lms_weights = CIE1702_MB_CIE_WEIGHTS) + { + return mul(xyz_to_rgb, mul(lms_to_xyz, LMSUnweightMatrix(lms_weights))); + } + + // Reconstruct weighted LMS from MB chromaticity plus the carried MB scale term. + float3 WeightedLMSFromMacleodBoynton(float2 mb, float l_plus_m) + { + return float3(mb.x, (1.f - mb.x), mb.y) * l_plus_m; + } + + float3 WeightedLMSFromMacleodBoynton(float3 mb) + { + return WeightedLMSFromMacleodBoynton(mb.xy, mb.z); + } + + float2 WhiteChromaticityFromWeightedLMS(float3 lms_weighted_white) + { + float y_mb = lms_weighted_white.x + lms_weighted_white.y; + y_mb = max(y_mb, 0); + + float inv = renodx::math::DivideSafe(1.f, y_mb, 0.f); + return float2(lms_weighted_white.x * inv, lms_weighted_white.z * inv); + } + + float3 WhiteFromWeightedLMS(float3 lms_weighted_white) + { + return float3(WhiteChromaticityFromWeightedLMS(lms_weighted_white), 1.f); + } + + namespace from + { + // Convert weighted LMS into MB as (l, s, y), where y is the carried L+M term. + float3 WeightedLMS(float3 weighted_lms) + { + float weighted_l = weighted_lms.x; + float weighted_m = weighted_lms.y; + float y_mb = weighted_l + weighted_m; + y_mb = max(y_mb, 0); + + float inv = renodx::math::DivideSafe(1.f, y_mb, 0.f); + return float3(weighted_l * inv, weighted_lms.z * inv, y_mb); + } + + float3 LMS(float3 lms, float3 lms_weights = CIE1702_MB_CIE_WEIGHTS) + { + return from::WeightedLMS(macleod_boynton::WeighLMS(lms, lms_weights)); + } + + float3 BT709(float3 bt709) + { + return from::WeightedLMS(mul(BT709_TO_LMS_WEIGHTED_MAT, bt709)); + } + + float3 BT2020(float3 bt2020) + { + return from::WeightedLMS(mul(BT2020_TO_LMS_WEIGHTED_MAT, bt2020)); + } + + float3 XYZ(float3 xyz) + { + return from::WeightedLMS(mul(XYZ_TO_LMS_WEIGHTED_MAT, xyz)); + } + + float2 XY(float2 xy, float Y = 1.f) + { + return from::XYZ(renodx::color::xyz::from::xyY(float3(xy, Y))).xy; + } + + float2 D65XY() + { + return from::XY(renodx::color::WHITE_POINT_D65); + } + + } // namespace from + + } // namespace macleod_boynton + + namespace lms + { + namespace from + { + + float3 MacLeodBoynton(float2 mb, float l_plus_m) + { + return macleod_boynton::WeightedLMSFromMacleodBoynton(mb, l_plus_m) / macleod_boynton::LMS_WEIGHTS; + } + + float3 MacLeodBoynton(float3 mb_3) + { + return from::MacLeodBoynton(mb_3.xy, mb_3.z); + } + + } // namespace from + } // namespace lms + + namespace bt709 + { + namespace from + { + float3 MacLeodBoynton(float2 mb, float l_plus_m) + { + return mul(macleod_boynton::LMS_WEIGHTED_TO_BT709_MAT, + macleod_boynton::WeightedLMSFromMacleodBoynton(mb, l_plus_m)); + } + + float3 MacLeodBoynton(float3 mb_3) + { + return bt709::from::MacLeodBoynton(mb_3.xy, mb_3.z); + } + } // namespace from + } // namespace bt709 + + namespace bt2020 + { + namespace from + { + float3 MacLeodBoynton(float2 mb, float l_plus_m) + { + return mul(macleod_boynton::LMS_WEIGHTED_TO_BT2020_MAT, + macleod_boynton::WeightedLMSFromMacleodBoynton(mb, l_plus_m)); + } + + float3 MacLeodBoynton(float3 mb_3) + { + return bt2020::from::MacLeodBoynton(mb_3.xy, mb_3.z); + } + } // namespace from + } // namespace bt2020 + + } // namespace color +} // namespace renodx + +#endif // SRC_SHADERS_COLOR_MACLEOD_BOYNTON_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/oklab.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/oklab.hlsl new file mode 100644 index 0000000000..178e517503 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/oklab.hlsl @@ -0,0 +1,106 @@ +#ifndef SRC_SHADERS_COLOR_OKLAB_HLSL_ +#define SRC_SHADERS_COLOR_OKLAB_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" +namespace renodx +{ + namespace color + { + + namespace bt709 + { + namespace from + { + float3 OkLab(float3 oklab) + { + static const float3x3 OKLAB_2_OKLABLMS = { + 1.f, 0.3963377774f, 0.2158037573f, + 1.f, -0.1055613458f, -0.0638541728f, + 1.f, -0.0894841775f, -1.2914855480f + }; + + static const float3x3 OKLABLMS_2_BT709 = { + 4.0767416621f, -3.3077115913f, 0.2309699292f, + -1.2684380046f, 2.6097574011f, -0.3413193965f, + -0.0041960863f, -0.7034186147f, 1.7076147010f + }; + + float3 lms = mul(OKLAB_2_OKLABLMS, oklab); + + lms = lms * lms * lms; + + return mul(OKLABLMS_2_BT709, lms); + } + } // namespace from + } // namespace bt709 + + namespace oklab + { + namespace from + { + float3 BT709(float3 bt709) + { + static const float3x3 BT709_2_OKLABLMS = { + 0.4122214708f, 0.5363325363f, 0.0514459929f, + 0.2119034982f, 0.6806995451f, 0.1073969566f, + 0.0883024619f, 0.2817188376f, 0.6299787005f + }; + static const float3x3 OKLABLMS_2_OKLAB = { + 0.2104542553f, 0.7936177850f, -0.0040720468f, + 1.9779984951f, -2.4285922050f, 0.4505937099f, + 0.0259040371f, 0.7827717662f, -0.8086757660f + }; + + float3 lms = mul(BT709_2_OKLABLMS, bt709); + + lms = renodx::math::Cbrt(lms); + + return mul(OKLABLMS_2_OKLAB, lms); + } + + float3 OkLCh(float3 oklch) + { + float l = oklch[0]; + float c = oklch[1]; + float h = oklch[2]; + return float3(l, c * cos(h), c * sin(h)); + } + } // namespace from + } // namespace oklab + + namespace oklch + { + namespace from + { + float3 OkLab(float3 oklab) + { + float l = oklab[0]; + float a = oklab[1]; + float b = oklab[2]; + return float3(l, distance(oklab.yz, 0), atan2(b, a)); + } + float3 BT709(float3 bt709) + { + float3 ok_lab = renodx::color::oklab::from::BT709(bt709); + return OkLab(ok_lab); + } + } // namespace from + } // namespace oklch + + namespace bt709 + { + namespace from + { + float3 OkLCh(float3 oklch) + { + float3 ok_lab = renodx::color::oklab::from::OkLCh(oklch); + return OkLab(ok_lab); + } + + } // namespace from + } // namespace bt709 + + } // namespace color +} // namespace renodx + +#endif // SRC_SHADERS_COLOR_OKLAB_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/pq.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/pq.hlsl new file mode 100644 index 0000000000..a5c1a6de20 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/pq.hlsl @@ -0,0 +1,59 @@ +#ifndef SRC_SHADERS_COLOR_PQ_HLSL_ +#define SRC_SHADERS_COLOR_PQ_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + namespace pq + { + static const float M1 = 2610.f / 16384.f; // 0.1593017578125f; + static const float M2 = 128.f * (2523.f / 4096.f); // 78.84375f; + static const float C1 = 3424.f / 4096.f; // 0.8359375f; + static const float C2 = 32.f * (2413.f / 4096.f); // 18.8515625f; + static const float C3 = 32.f * (2392.f / 4096.f); // 18.6875f; + + float Encode(float color, float scaling = 10000.f) + { + color *= (scaling / 10000.f); + float y_m1 = pow(color, M1); + return pow((C1 + C2 * y_m1) / (1.f + C3 * y_m1), M2); + } + + float3 Encode(float3 color, float scaling = 10000.f) + { + color *= (scaling / 10000.f); + float3 y_m1 = pow(color, M1); + return pow((C1 + C2 * y_m1) / (1.f + C3 * y_m1), M2); + } + + float Decode(float color, float scaling = 10000.f) + { + float e_m12 = pow(color, 1.f / M2); + float out_color = pow(max(0, e_m12 - C1) / (C2 - C3 * e_m12), 1.f / M1); + return out_color * (10000.f / scaling); + } + + float3 Decode(float3 color, float scaling = 10000.f) + { + float3 e_m12 = pow(color, 1.f / M2); + float3 out_color = pow(max(0, e_m12 - C1) / (C2 - C3 * e_m12), 1.f / M1); + return out_color * (10000.f / scaling); + } + + float3 EncodeSafe(float3 color, float scaling = 10000.f) + { + return Encode(max(0, color), scaling); + } + + float3 DecodeSafe(float3 color, float scaling = 10000.f) + { + return Decode(max(0, color), scaling); + } + + } // namespace pq + } // namespace color +} // namespace renodx +#endif // SRC_SHADERS_COLOR_PQ_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl new file mode 100644 index 0000000000..5dbd92d785 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl @@ -0,0 +1,534 @@ +#ifndef RENODX_SHADERS_COLOR_RGB_HLSL +#define RENODX_SHADERS_COLOR_RGB_HLSL + +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + + static const float3x3 BT709_TO_XYZ_MAT = float3x3( + 0.4123907993f, 0.3575843394f, 0.1804807884f, + 0.2126390059f, 0.7151686788f, 0.0721923154f, + 0.0193308187f, 0.1191947798f, 0.9505321522f); + + static const float3x3 XYZ_TO_BT709_MAT = float3x3( + 3.2409699419f, -1.5373831776f, -0.4986107603f, + -0.9692436363f, 1.8759675015f, 0.0415550574f, + 0.0556300797f, -0.2039769589f, 1.0569715142f); + + static const float3x3 BT2020_TO_XYZ_MAT = float3x3( + 0.6369580483f, 0.1446169036f, 0.1688809752f, + 0.2627002120f, 0.6779980715f, 0.0593017165f, + 0.0000000000f, 0.0280726930f, 1.0609850577f); + + static const float3x3 XYZ_TO_BT2020_MAT = float3x3( + 1.7166511880f, -0.3556707838f, -0.2533662814f, + -0.6666843518f, 1.6164812366f, 0.0157685458f, + 0.0176398574f, -0.0427706133f, 0.9421031212f); + + static const float3x3 AP0_TO_XYZ_MAT = float3x3( + 0.9525523959f, 0.0000000000f, 0.0000936786f, + 0.3439664498f, 0.7281660966f, -0.0721325464f, + 0.0000000000f, 0.0000000000f, 1.0088251844f); + + static const float3x3 XYZ_TO_AP0_MAT = float3x3( + 1.0498110175f, 0.0000000000f, -0.0000974845f, + -0.4959030231f, 1.3733130458f, 0.0982400361f, + 0.0000000000f, 0.0000000000f, 0.9912520182f); + + static const float3x3 AP1_TO_XYZ_MAT = float3x3( + 0.6624541811f, 0.1340042065f, 0.1561876870f, + 0.2722287168f, 0.6740817658f, 0.0536895174f, + -0.0055746495f, 0.0040607335f, 1.0103391003f); + + static const float3x3 XYZ_TO_AP1_MAT = float3x3( + 1.6410233797f, -0.3248032942f, -0.2364246952f, + -0.6636628587f, 1.6153315917f, 0.0167563477f, + 0.0117218943f, -0.0082844420f, 0.9883948585f); + + static const float3x3 XYZ_TO_HUNT_POINTER_ESTEVEZ_LMS_BAD_MAT = float3x3( + +0.38971f, 0.68898f, -0.07868f, + -0.22981f, 1.18340f, +0.04641f, + +0.00000f, 0.00000f, +1.00000f); + + static const float3x3 XYZ_TO_CAT02_LMS_MAT = XYZ_TO_HUNT_POINTER_ESTEVEZ_LMS_BAD_MAT; + + static const float3x3 XYZ_TO_HUNT_POINTER_ESTEVEZ_LMS_MAT = float3x3( + +0.38971f, 0.68898f, -0.07869f, + -0.22981f, 1.18340f, +0.04641f, + +0.00000f, 0.00000f, +1.00000f); + + // According to Dolby + static const float3x3 XYZ_D65_TO_HUNT_POINTER_ESTEVEZ_LMS_MAT = float3x3( + +0.4002f, 0.7076f, -0.0808f, + -0.2263f, 1.1653f, +0.0457f, + +0.0000f, 0.0000f, +0.9182f); + + // According to Bruce Lind + static const float3x3 XYZ_D65_TO_VON_KRIES_LMS_MAT = float3x3( + +0.4002400f, 0.7076000f, -0.0808100f, + -0.2263000f, 1.1653200f, +0.0457000f, + +0.0000000f, 0.0000000f, +0.9182200f); + + static const float3x3 XYZ_D65_TO_BRADFORD_LMS_MAT = float3x3( + 0.8951000f, 0.2664000f, -0.1614000f, + -0.7502000f, 1.7135000f, 0.0367000f, + 0.0389000f, -0.0685000f, 1.0296000f); + + // AKA Fairchild + static const float3x3 XYZ_TO_CAT97_LMS_MAT = float3x3( + +0.8562f, +0.3372f, -0.1934f, + -0.8360f, +1.8327f, +0.0033f, + +0.0357f, -0.0469f, +1.0112f); + + static const float3x3 STOCKMAN_CVRL_XYZ_TO_LMS_2DEG_FIT = float3x3( + 0.2670502842655792, 0.8471990148492798, -0.03470416612462053, + -0.38706882411220156, 1.165429935890458, 0.10302286696614202, + 0.026727793989083093, -0.02729131667566509, 0.5333267257603284); + static const float3x3 STOCKMAN_CVRL_LMS_TO_XYZ_2DEG_FIT = renodx::math::Invert3x3(STOCKMAN_CVRL_XYZ_TO_LMS_2DEG_FIT); + + static const float3x3 XYZ_TO_STOCKMAN_SHARP_LMS_MAT = STOCKMAN_CVRL_XYZ_TO_LMS_2DEG_FIT; + static const float3x3 STOCKMAN_SHARP_LMS_TO_XYZ_MAT = STOCKMAN_CVRL_LMS_TO_XYZ_2DEG_FIT; + + // Stockman & Sharpe 2 degree + static const float3x3 STOCKMAN_SHARP_LMS_TO_XFYFZF_MAT = float3x3( + 1.94735469f, -1.41445123f, 0.36476327f, + 0.68990272f, +0.34832189f, 0.00000000f, + 0.00000000f, +0.00000000f, 1.93485343f); + + static const float3x3 XFYFZF_TO_STOCKMAN_SHARP_LMS_MAT = renodx::math::Invert3x3(STOCKMAN_SHARP_LMS_TO_XFYFZF_MAT); + + // Fit CIE 170-2 Table 10.5 (`data/CIE_smb_cc_2deg.csv`) using Stockman-Sharpe 2-degree LMS + static const float3 CIE1702_MB_CIE_WEIGHTS = float3( + STOCKMAN_SHARP_LMS_TO_XFYFZF_MAT[1][0], // LMS=>XfYfZf weight for L + STOCKMAN_SHARP_LMS_TO_XFYFZF_MAT[1][1], // LMS=>XfYfZf weight for M + 0.0371597069161f); + + static const float3x3 PLMS_TO_IPT_MAT = float3x3( + 0.4f, 0.4f, 0.2f, + 4.4550f, -4.8510f, 0.3960f, + 0.8056f, 0.3572f, -1.1628f); + + static const float3x3 DISPLAYP3_TO_XYZ_MAT = float3x3( + 0.4865709486f, 0.2656676932f, 0.1982172852f, + 0.2289745641f, 0.6917385218f, 0.0792869141f, + 0.0000000000f, 0.0451133819f, 1.0439443689f); + + static const float3x3 XYZ_TO_DISPLAYP3_MAT = float3x3( + 2.4934969119f, -0.9313836179f, -0.4027107845f, + -0.8294889696f, 1.7626640603f, 0.0236246858f, + 0.0358458302f, -0.0761723893f, 0.9568845240); + + static const float3x3 BT470_PAL_TO_BT709_MAT = float3x3( + 1.04404318f, -0.0440432094f, 0.f, + 0.f, 1.f, 0.f, + 0.f, 0.0117933787f, 0.988206624f); + + static const float3x3 BT601_NTSC_U_TO_BT709_MAT = float3x3( + 0.939542055f, 0.0501813553f, 0.0102765792f, + 0.0177722238f, 0.965792834f, 0.0164349135f, + -0.00162159989f, -0.00436974968f, 1.00599133f); + + static const float3x3 NTSC_U_1953_TO_XYZ_MAT = float3x3( + 0.6068638093f, 0.1735072810f, 0.2003348814f, + 0.2989030703f, 0.5866198547f, 0.1144770751f, + -0.0000000000f, 0.0660980118f, 1.1161514821f); + + // chromatic adaptation method: vK20 + // chromatic adaptation transform: CAT02 + static const float3x3 ARIB_TR_B9_D93_TO_BT709_D65_MAT = float3x3( + 0.886132895f, -0.144765302f, -0.000316959019f, + 0.0408876389f, 0.971982538f, 0.00582195585f, + 0.00312487990f, 0.0414751693f, 1.56167137f); + + // chromatic adaptation method: vK20 + // chromatic adaptation transform: CAT02 + static const float3x3 ARIB_TR_B9_9300K_8_MPCD_TO_BT709_D65_MAT = float3x3( + 0.887350380f, -0.145325064f, -0.0000826625182f, + 0.0409696400f, 0.974039375f, 0.00576419429f, + 0.00292735593f, 0.0404177531f, 1.53643214f); + + // chromatic adaptation method: vK20 + // chromatic adaptation transform: CAT02 + static const float3x3 ARIB_TR_B9_9300K_27_MPCD_TO_BT709_D65_MAT = float3x3( + 0.768497526f, -0.210804164f, 0.000297427177f, + 0.0397904068f, 1.04825413f, 0.00555809540f, + 0.00147510506f, 0.0328789241f, 1.36515128f); + + // chromatic adaptation method: vK20 + // chromatic adaptation transform: CAT02 + static const float3x3 BT709_D93_TO_BT709_D65_MAT = float3x3( + 0.956910431f, -0.0613676644f, -0.00503798108f, + 0.00317927985f, 1.00466823f, 0.000124804413f, + 0.00494503090f, 0.0247498396f, 1.17469859f); + + // chromatic adaptation method: von Kries + // chromatic adaptation transform: Bradford + static const float3x3 D65_TO_D60_CAT = float3x3( + 1.01303493f, 0.00610525766f, -0.0149709433f, + 0.00769822997f, 0.998163342f, -0.00503203831f, + -0.00284131732f, 0.00468515651f, 0.924506127f); + + // chromatic adaptation method: von Kries + // chromatic adaptation transform: Bradford + static const float3x3 D60_TO_D65_MAT = float3x3( + 0.987223982f, -0.00611322838f, 0.0159532874f, + -0.00759837171f, 1.00186145f, 0.00533003592f, + 0.00307257706f, -0.00509596150f, 1.08168065f); + + static const float3x3 IDENTITY_MAT = float3x3( + 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f); + + static const float3x3 BT709_TO_AP0_MAT = mul(XYZ_TO_AP0_MAT, mul(D65_TO_D60_CAT, BT709_TO_XYZ_MAT)); + + // With Bradford + static const float3x3 BT709_TO_AP1_MAT = float3x3( + 0.6130974024, 0.3395231462, 0.0473794514, + 0.0701937225, 0.9163538791, 0.0134523985, + 0.0206155929, 0.1095697729, 0.8698146342); + + // With Bradford + static const float3x3 BT2020_TO_AP1_MAT = float3x3( + 0.9748949779f, 0.0195991086f, 0.0055059134f, + 0.0021795628f, 0.9955354689f, 0.0022849683f, + 0.0047972397f, 0.0245320166f, 0.9706707437f); + + static const float3x3 BT709_TO_BT2020_MAT = mul(XYZ_TO_BT2020_MAT, BT709_TO_XYZ_MAT); + static const float3x3 BT709_TO_BT709D60_MAT = mul(XYZ_TO_BT709_MAT, mul(D65_TO_D60_CAT, BT709_TO_XYZ_MAT)); + static const float3x3 BT709_TO_BT2020D60_MAT = mul(XYZ_TO_BT2020_MAT, mul(D65_TO_D60_CAT, BT709_TO_XYZ_MAT)); + static const float3x3 BT709_TO_DISPLAYP3_MAT = mul(XYZ_TO_DISPLAYP3_MAT, BT709_TO_XYZ_MAT); + static const float3x3 BT709_TO_DISPLAYP3D60_MAT = mul(XYZ_TO_DISPLAYP3_MAT, mul(D65_TO_D60_CAT, BT709_TO_XYZ_MAT)); + + static const float3x3 BT2020_TO_AP0_MAT = mul(XYZ_TO_AP0_MAT, mul(D65_TO_D60_CAT, BT2020_TO_XYZ_MAT)); + static const float3x3 BT2020_TO_BT709_MAT = mul(XYZ_TO_BT709_MAT, BT2020_TO_XYZ_MAT); + + static const float3x3 DISPLAYP3_TO_AP0_MAT = mul(XYZ_TO_AP0_MAT, mul(D65_TO_D60_CAT, DISPLAYP3_TO_XYZ_MAT)); + static const float3x3 DISPLAYP3_TO_BT709_MAT = mul(XYZ_TO_BT709_MAT, DISPLAYP3_TO_XYZ_MAT); + + static const float3x3 AP0_TO_AP1_MAT = mul(XYZ_TO_AP1_MAT, AP0_TO_XYZ_MAT); + + static const float3x3 AP1_TO_AP0_MAT = mul(XYZ_TO_AP0_MAT, AP1_TO_XYZ_MAT); + + // With Bradford + static const float3x3 AP1_TO_BT709_MAT = float3x3( + 1.7050509927, -0.6217921207, -0.0832588720, + -0.1302564175, 1.1408047366, -0.0105483191, + -0.0240033568, -0.1289689761, 1.1529723329); + + // With Bradford + static const float3x3 AP1_TO_BT2020_MAT = float3x3( + 1.0258247477f, -0.0200531908f, -0.0057715568f, + -0.0022343695f, 1.0045865019f, -0.0023521324f, + -0.0050133515f, -0.0252900718f, 1.0303034233f); + + static const float3x3 AP1_TO_BT709D60_MAT = mul(XYZ_TO_BT709_MAT, AP1_TO_XYZ_MAT); + static const float3x3 AP1_TO_BT2020D60_MAT = mul(XYZ_TO_BT2020_MAT, AP1_TO_XYZ_MAT); + static const float3x3 AP1_TO_AP1D65_MAT = mul(XYZ_TO_AP1_MAT, mul(D60_TO_D65_MAT, AP1_TO_XYZ_MAT)); + + // https://www.ilkeratalay.com/colorspacesfaq.php + static const float3 BOURGIN_D65_Y = float3(0.222015, 0.706655, 0.071330); + + // 1931 2 degree standard observer + static const float2 WHITE_POINT_D65 = float2(0.31272, 0.32903); + + // Equal-energy illuminant chromaticity (not related to energy/quantal unit choice). + static const float2 WHITE_POINT_E = float2(1.f / 3.f, 1.f / 3.f); + + // https://www.arri.com/resource/blob/31918/66f56e6abb6e5b6553929edf9aa7483e/2017-03-alexa-logc-curve-in-vfx-data.pdf + static const float3x3 ALEXA_WIDE_GAMUT_TO_BT709_MAT = float3x3( + 1.485007, -0.401216, -0.083791, + -0.033732, 1.282887, -0.249155, + 0.010776, -0.122018, 1.111242); + + namespace xyz + { + namespace from + { + float3 xyY(float3 xyY) + { + float3 XYZ; + + XYZ.xz = float2(xyY.x, (1.f - xyY.xy.x - xyY.xy.y)) / xyY.y * xyY[2]; + + XYZ.y = xyY[2]; + + return XYZ; + } + + float3 BT709(float3 bt709) + { + return mul(BT709_TO_XYZ_MAT, bt709); + } + + float3 BT2020(float3 bt2020) + { + return mul(BT2020_TO_XYZ_MAT, bt2020); + } + + float3 LMS(float3 lms) + { + return mul(STOCKMAN_SHARP_LMS_TO_XYZ_MAT, lms); + } + + } // namespace from + } // namespace xyz + + namespace xfyfzf + { + namespace from + { + float3 LMS(float3 lms) + { + return mul(STOCKMAN_SHARP_LMS_TO_XFYFZF_MAT, lms); + } + float3 BT709(float3 bt709) + { + return mul(STOCKMAN_SHARP_LMS_TO_XFYFZF_MAT, mul(XYZ_TO_STOCKMAN_SHARP_LMS_MAT, mul(BT709_TO_XYZ_MAT, bt709))); + } + float3 BT2020(float3 bt2020) + { + return mul(STOCKMAN_SHARP_LMS_TO_XFYFZF_MAT, mul(XYZ_TO_STOCKMAN_SHARP_LMS_MAT, mul(BT2020_TO_XYZ_MAT, bt2020))); + } + } // namespace from + } // namespace xfyfzf + + namespace yf + { + namespace from + { + float LMS(float3 lms) + { + return xfyfzf::from::LMS(lms).y; + } + float BT709(float3 bt709) + { + return xfyfzf::from::BT709(bt709).y; + } + float BT2020(float3 bt2020) + { + return xfyfzf::from::BT2020(bt2020).y; + } + } // namespace from + } // namespace yf + + namespace xyY + { + namespace from + { + float3 XYZ(float3 XYZ) + { + float xyz = XYZ.x + XYZ.y + XYZ.z; + + float3 xyY; + + xyY.xy = XYZ.xy / xyz; + + xyY[2] = XYZ.y; + + return xyY; + } + + float3 BT709(float3 bt709) + { + float3 XYZ = xyz::from::BT709(bt709); + + return xyY::from::XYZ(XYZ); + } + } // namespace from + } // namespace xyY + + namespace bt709 + { + static const float REFERENCE_WHITE = 100.f; + + namespace from + { + float3 XYZ(float3 XYZ) + { + return mul(XYZ_TO_BT709_MAT, XYZ); + } + + float3 xyY(float3 xyY) + { + float3 XYZ = xyz::from::xyY(xyY); + + return bt709::from::XYZ(XYZ); + } + + float3 AP1(float3 ap1) + { + return mul(AP1_TO_BT709_MAT, ap1); + } + + float3 BT2020(float3 bt2020) + { + return mul(BT2020_TO_BT709_MAT, bt2020); + } + + float3 BT601NTSCU(float3 bt601) + { + return mul(BT601_NTSC_U_TO_BT709_MAT, bt601); + } + + float3 ARIBTRB9(float3 aribtrb9) + { + return mul(ARIB_TR_B9_D93_TO_BT709_D65_MAT, aribtrb9); + } + + float3 ARIBTRB98MPCD(float3 aribtrb9) + { + return mul(ARIB_TR_B9_9300K_8_MPCD_TO_BT709_D65_MAT, aribtrb9); + } + + float3 ARIBTRB927MPCD(float3 aribtrb9) + { + return mul(ARIB_TR_B9_9300K_27_MPCD_TO_BT709_D65_MAT, aribtrb9); + } + + float3 BT709D93(float3 bt709d93) + { + return mul(BT709_D93_TO_BT709_D65_MAT, bt709d93); + } + + float3 LMS(float3 lms) + { + return mul(XYZ_TO_BT709_MAT, mul(STOCKMAN_SHARP_LMS_TO_XYZ_MAT, lms)); + } + + } // namespace from + } // namespace bt709 + + namespace bt2020 + { + namespace from + { + float3 BT709(float3 bt709) + { + return mul(BT709_TO_BT2020_MAT, bt709); + } + + float3 AP1(float3 ap1) + { + return mul(AP1_TO_BT2020_MAT, ap1); + } + + float3 XYZ(float3 XYZ) + { + return mul(XYZ_TO_BT2020_MAT, XYZ); + } + + float3 LMS(float3 lms) + { + return mul(XYZ_TO_BT2020_MAT, mul(STOCKMAN_SHARP_LMS_TO_XYZ_MAT, lms)); + } + + } // namespace from + } // namespace bt2020 + + namespace ap1 + { + namespace from + { + float3 BT709(float3 bt709) + { + return mul(BT709_TO_AP1_MAT, bt709); + } + + float3 BT2020(float3 bt2020) + { + return mul(BT2020_TO_AP1_MAT, bt2020); + } + } // namespace from + } // namespace ap1 + + namespace y + { + namespace from + { + + float XYZMatrix(float3 color, float3x3 toXYZMatrix) + { + return dot(color, toXYZMatrix[1].rgb); + } + + float NTSC1953(float3 ntsc) + { + return XYZMatrix(ntsc, NTSC_U_1953_TO_XYZ_MAT); + } + + float BT709(float3 bt709) + { + return XYZMatrix(bt709, BT709_TO_XYZ_MAT); + } + float BT2020(float3 bt2020) + { + return XYZMatrix(bt2020, BT2020_TO_XYZ_MAT); + } + float AP1(float3 ap1) + { + return XYZMatrix(ap1, AP1_TO_XYZ_MAT); + } + } // namespace from + } // namespace y + + namespace luma + { + namespace from + { + float BT601(float3 bt601) + { + return y::from::NTSC1953(bt601); + } + } // namespace from + } // namespace luma + + namespace lms + { + namespace from + { + float3 BT709(float3 bt709) + { + return mul(XYZ_TO_STOCKMAN_SHARP_LMS_MAT, mul(BT709_TO_XYZ_MAT, bt709)); + } + float3 BT2020(float3 bt2020) + { + return mul(XYZ_TO_STOCKMAN_SHARP_LMS_MAT, mul(BT2020_TO_XYZ_MAT, bt2020)); + } + + float3 XYZ(float3 xyz) + { + return mul(renodx::color::XYZ_TO_STOCKMAN_SHARP_LMS_MAT, xyz); + } + + float3 XYWhite(float2 white_xy, float white_level = 1.f) + { + return XYZ(renodx::color::xyz::from::xyY(float3(white_xy, white_level))); + } + + float3 WhiteD65(float white_level = 1.f) + { + return XYWhite(renodx::color::WHITE_POINT_D65, white_level); + } + + // Compatibility alias. + float3 WhiteE(float white_level = 1.f) + { + return XYWhite(renodx::color::WHITE_POINT_E, white_level); + } + } // namespace from + } // namespace lms + + namespace bt2408 + { + static const float REFERENCE_WHITE = 203.f; + static const float GRAPHICS_WHITE = 203.f; + } // namespace bt2408 + + } // namespace color +} // namespace renodx + +#endif // RENODX_SHADERS_COLOR_RGB_HLSL diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/srgb.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/srgb.hlsl new file mode 100644 index 0000000000..794b026873 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/srgb.hlsl @@ -0,0 +1,120 @@ +#ifndef RENODX_SHADERS_COLOR_SRGB_HLSL +#define RENODX_SHADERS_COLOR_SRGB_HLSL + +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + namespace srgb + { + static const float REFERENCE_WHITE = 80.f; + +#define ENCODE(T) \ + T Encode(T c) \ + { \ + return renodx::math::Select( \ + c <= 0.0031308f, \ + c * 12.92f, \ + mad(1.055f, pow(c, 1.f / 2.4f), -0.055f)); \ + } + + ENCODE(float) + ENCODE(float2) + ENCODE(float3) + + float4 Encode(float4 color) + { + return float4(Encode(color.rgb), color.a); + } + +#define ENCODE_SAFE(T) \ + T EncodeSafe(T c) \ + { \ + return renodx::math::CopySign(Encode(abs(c)), c); \ + } + + ENCODE_SAFE(float) + ENCODE_SAFE(float2) + ENCODE_SAFE(float3) + + float4 EncodeSafe(float4 color) + { + return float4(EncodeSafe(color.rgb), color.a); + } + +#define DECODE(T) \ + T Decode(T c) \ + { \ + return renodx::math::Select( \ + c <= 0.04045f, \ + c / 12.92f, \ + pow(mad(c, 1.f / 1.055f, 0.055f / 1.055f), 2.4f)); \ + } + + DECODE(float) + DECODE(float2) + DECODE(float3) + + float4 Decode(float4 color) + { + return float4(Decode(color.rgb), color.a); + } + +#define DECODE_SAFE(T) \ + T DecodeSafe(T c) \ + { \ + return renodx::math::CopySign(Decode(abs(c)), c); \ + } + + DECODE_SAFE(float) + DECODE_SAFE(float2) + DECODE_SAFE(float3) + + float4 DecodeSafe(float4 color) + { + return float4(DecodeSafe(color.rgb), color.a); + } + +#undef ENCODE +#undef ENCODE_SAFE +#undef DECODE +#undef DECODE_SAFE + + } // namespace srgb + + namespace srgba + { + + float4 Encode(float4 color) + { + return renodx::math::Select( + color <= 0.0031308f, + color * 12.92f, + mad(1.055f, pow(color, 1.f / 2.4f), -0.055f)); + } + + float4 Decode(float4 color) + { + return renodx::math::Select( + color <= 0.04045f, + color / 12.92f, + pow(mad(color, 1.f / 1.055f, 0.055f / 1.055f), 2.4f)); + } + + float4 EncodeSafe(float4 color) + { + return renodx::math::CopySign(Encode(abs(color)), color); + } + + float4 DecodeSafe(float4 color) + { + return renodx::math::CopySign(Decode(abs(color)), color); + } + + } // namespace srgba + } // namespace color +} // namespace renodx + +#endif // RENODX_SHADERS_COLOR_SRGB_HLSL diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/stockman.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/stockman.hlsl new file mode 100644 index 0000000000..2fd35f9464 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/stockman.hlsl @@ -0,0 +1,121 @@ +#ifndef SRC_SHADERS_COLOR_STOCKMAN_HLSL_ +#define SRC_SHADERS_COLOR_STOCKMAN_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +// Deprecated (use renodx::color::lms::* directly) + +namespace renodx +{ + namespace color + { + namespace bt709 + { + namespace from + { + + float3 StockmanDKL(float3 dkl) + { + // Modified Stockman & Sharpe for LCD LED + float3x3 XYZ_TO_LMS_WUERGER_2020 = float3x3( + 0.187596268556126, 0.585168649077728, -0.026384263306304, + -0.133397430663221, 0.405505777260049, 0.034502127690364, + 0.000244379021663, -0.000542995890619, 0.019406849066323); + + // Manually recomputed from CIE 1931 XYZ 1nm to Stockman 2deg 1nm 8dp with MB2 Weights + float3x3 XYZ_TO_LMS_2006 = float3x3( + 0.185082982238733f, 0.584081279463687f, -0.0240722415044404f, + -0.134433056469973f, 0.405752392775348f, 0.0358252602217631f, + 0.000789456671966863f, -0.000912281325916184f, 0.0198490812339463f); + + float3x3 XYZ_FROM_LMS = renodx::math::Invert3x3(XYZ_TO_LMS_2006); + + // CIE 1931 2 degree standard observer + float2 WHITE_POINT_D65 = float2(0.31272, 0.32903); + float3 D65_XYZ = renodx::color::xyz::from::xyY(float3(WHITE_POINT_D65, 1.f)); + float3 LMS_WHITE = mul(XYZ_TO_LMS_2006, D65_XYZ); + + float mc1 = LMS_WHITE.x / LMS_WHITE.y; + float mc2 = (LMS_WHITE.x + LMS_WHITE.y) / LMS_WHITE.z; + + // actual ACC color space (DKL-like / ACC) + float3x3 LMS_TO_DKL_D65 = float3x3( + 1, 1, 0, + 1, -mc1, 0, + -1, -1, mc2); + + float3x3 LMS_FROM_DKL_D65 = renodx::math::Invert3x3(LMS_TO_DKL_D65); + + float3x3 RGB_TO_DKL_D65 = mul(LMS_TO_DKL_D65, XYZ_TO_LMS_2006); + float3x3 DKL_D65_TO_RGB = renodx::math::Invert3x3(RGB_TO_DKL_D65); + + float3 lms_color = mul(LMS_FROM_DKL_D65, dkl); + + float3 lms_background = mul(XYZ_TO_LMS_2006, renodx::color::xyz::from::xyY(float3(WHITE_POINT_D65, 1.00f))); + + lms_background = 0; // skip for now + float3 lms_final = lms_color + lms_background; + + float3 xyz = mul(XYZ_FROM_LMS, lms_final); + + float3 bt709 = renodx::color::bt709::from::XYZ(xyz); + return bt709; + } + } // namespace from + } // namespace bt709 + + namespace stockmandkl + { + namespace from + { + float3 BT709(float3 bt709) + { + // Modified Stockman & Sharpe for LCD LED + float3x3 XYZ_TO_LMS_WUERGER_2020 = float3x3( + 0.187596268556126, 0.585168649077728, -0.026384263306304, + -0.133397430663221, 0.405505777260049, 0.034502127690364, + 0.000244379021663, -0.000542995890619, 0.019406849066323); + + // Manually recomputed from CIE 1931 XYZ 1nm to Stockman 2deg 1nm 8dp with MB2 Weights + float3x3 XYZ_TO_LMS_2006 = float3x3( + 0.185082982238733f, 0.584081279463687f, -0.0240722415044404f, + -0.134433056469973f, 0.405752392775348f, 0.0358252602217631f, + 0.000789456671966863f, -0.000912281325916184f, 0.0198490812339463f); + + float3x3 XYZ_FROM_LMS = renodx::math::Invert3x3(XYZ_TO_LMS_2006); + + // CIE 1931 2 degree standard observer + float2 WHITE_POINT_D65 = float2(0.31272, 0.32903); + float3 D65_XYZ = renodx::color::xyz::from::xyY(float3(WHITE_POINT_D65, 1.f)); + float3 LMS_WHITE = mul(XYZ_TO_LMS_2006, D65_XYZ); + + float mc1 = LMS_WHITE.x / LMS_WHITE.y; + float mc2 = (LMS_WHITE.x + LMS_WHITE.y) / LMS_WHITE.z; + + // actual ACC color space (DKL-like / ACC) + float3x3 LMS_TO_DKL_D65 = float3x3( + 1, 1, 0, + 1, -mc1, 0, + -1, -1, mc2); + + float3x3 LMS_FROM_DKL_D65 = renodx::math::Invert3x3(LMS_TO_DKL_D65); + float3 xyz = renodx::color::xyz::from::BT709(bt709); + float3 lms_input = mul(XYZ_TO_LMS_2006, xyz); + float3 dkl_input = mul(LMS_TO_DKL_D65, lms_input); + + float3 lms_background = mul(XYZ_TO_LMS_2006, renodx::color::xyz::from::xyY(float3(WHITE_POINT_D65, 1.00f))); + + lms_background = 0; // skip for now + float3 delta = lms_input - lms_background; + + float3 dkl = mul(LMS_TO_DKL_D65, delta); + + return dkl; + } + } // namespace from + } // namespace stockmandkl + + } // namespace color +} // namespace renodx +#endif // SRC_SHADERS_COLOR_STOCKMAN_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/ycbcr.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/ycbcr.hlsl new file mode 100644 index 0000000000..a99b8a046e --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/color/ycbcr.hlsl @@ -0,0 +1,86 @@ +#ifndef RENODX_SHADERS_COLOR_YCBCR_HLSL +#define RENODX_SHADERS_COLOR_YCBCR_HLSL + +#include "PostProcessing/ColorGrading/Include/renodx/color/rgb.hlsl" + +namespace renodx +{ + namespace color + { + + namespace ycbcr + { + namespace from + { + + // Limited (studio) -> "full" convention used by Decode(): + // Y expanded to full swing + // Cb/Cr mapped so that 0.5 is neutral and span matches full chroma swing + float3 Limited(float3 ycbcr_limited) + { + const float Yoff = 16.f / 255.f; // 0.06274509803921568627450980392157 + const float chroma_offset = 128.f / 255.f; // 0.50196078431372549019607843137255 + const float Yscale = 255.f / (235.f - 16.f); // 1.1643835616438356164383561643836 + const float Cscale = 255.f / (240.f - 16.f); // 1.1383928571428571428571428571429 + + float Y = (ycbcr_limited.x - Yoff) * Yscale; + float Cb = (ycbcr_limited.y - chroma_offset) * Cscale + chroma_offset; + float Cr = (ycbcr_limited.z - chroma_offset) * Cscale + chroma_offset; + + return float3(Y, Cb, Cr); + } + + } // namespace from + + float3 Decode(float3 ycbcr_full, float3 k) + { + const float chroma_offset = 128.f / 255.0f; // 0.50196078431372549019607843137255 + const float Y = ycbcr_full.x; + const float Cb = ycbcr_full.y - chroma_offset; + const float Cr = ycbcr_full.z - chroma_offset; + + const float Rp = Y + (2.0f - 2.0f * k.r) * Cr; + const float Bp = Y + (2.0f - 2.0f * k.b) * Cb; + const float Gp = Y - (k.r / k.g) * (Rp - Y) - (k.b / k.g) * (Bp - Y); + return float3(Rp, Gp, Bp); + } + + } // namespace ycbcr + + namespace bt601 + { + namespace from + { + float3 YCbCr(float3 ycbcr_full) + { + // 0.714466, 0.345614, 1.402194, 1.771046 + return ycbcr::Decode(ycbcr_full, NTSC_U_1953_TO_XYZ_MAT[1].xyz); + } + float3 YCbCrLimited(float3 ycbcr_limited) + { + // 1.596247, 2.016146, 0.813343, 0.393445 + return YCbCr(ycbcr::from::Limited(ycbcr_limited)); + } + } // namespace from + } // namespace bt601 + + namespace bt709 + { + namespace from + { + float3 YCbCr(float3 ycbcr_full) + { + // 0.468208, 0.187314, 1.574722, 1.855615 + return ycbcr::Decode(ycbcr_full, BT709_TO_XYZ_MAT[1].xyz); + } + float3 YCbCrLimited(float3 ycbcr_limited) + { + // 1.792652, 2.112419, 0.533004, 0.213237 + return YCbCr(ycbcr::from::Limited(ycbcr_limited)); + } + } // namespace from + } // namespace bt709 + + } // namespace color +} // namespace renodx +#endif // RENODX_SHADERS_COLOR_YCBCR_HLSL diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/colorcorrect.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/colorcorrect.hlsl new file mode 100644 index 0000000000..a9c9621f25 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/colorcorrect.hlsl @@ -0,0 +1,356 @@ +#ifndef SRC_SHADERS_COLORCORRECT_HLSL_ +#define SRC_SHADERS_COLORCORRECT_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/color.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace color + { + namespace correct + { + +#define GAMMA(T) \ + T Gamma(T c, bool pow_to_srgb = false, float gamma = 2.2f) \ + { \ + if (pow_to_srgb) { \ + return srgb::Decode(color::gamma::Encode(c, gamma)); \ + } else { \ + return color::gamma::Decode(srgb::Encode(c), gamma); \ + } \ + } + + GAMMA(float) + GAMMA(float2) + GAMMA(float3) + + float4 Gamma(float4 color, bool pow_to_srgb = false, float gamma = 2.2f) + { + return float4(Gamma(color.rgb, pow_to_srgb, gamma), color.a); + } + +#define GAMMA_SAFE(T) \ + T GammaSafe(T c, bool pow_to_srgb = false, float gamma = 2.2f) \ + { \ + if (pow_to_srgb) { \ + return renodx::math::CopySign(srgb::Decode(color::gamma::Encode(abs(c), gamma)), c); \ + } else { \ + return renodx::math::CopySign(color::gamma::Decode(srgb::Encode(abs(c)), gamma), c); \ + } \ + } + + GAMMA_SAFE(float) + GAMMA_SAFE(float2) + GAMMA_SAFE(float3) + + float4 GammaSafe(float4 color, bool pow_to_srgb = false, float gamma = 2.2f) + { + return float4(Gamma(color.rgb, pow_to_srgb, gamma), color.a); + } + +#undef GAMMA +#undef GAMMA_SAFE + + float3 ChrominanceOKLab( + float3 incorrect_color, + float3 reference_color, + float strength = 1.f, + float clamp_chrominance_loss = 0.f) + { + if (strength == 0.f) + return incorrect_color; + + float3 incorrect_lab = renodx::color::oklab::from::BT709(incorrect_color); + float3 reference_lab = renodx::color::oklab::from::BT709(reference_color); + + float2 incorrect_ab = incorrect_lab.yz; + float2 reference_ab = reference_lab.yz; + + // Compute chrominance (magnitude of the a–b vector) + float incorrect_chrominance = length(incorrect_ab); + float correct_chrominance = length(reference_ab); + + // Scale original chrominance vector toward target chrominance + float chrominance_ratio = renodx::math::DivideSafe(correct_chrominance, incorrect_chrominance, 1.f); + float scale = lerp(1.f, chrominance_ratio, strength); + + float t = 1.0f - step(1.0f, scale); // t = 1 when scale < 1, 0 when scale >= 1 + scale = lerp(scale, 1.0f, t * clamp_chrominance_loss); + + incorrect_lab.yz = incorrect_ab * scale; + + float3 result = renodx::color::bt709::from::OkLab(incorrect_lab); + return result; + } + + float3 ChrominanceICtCp( + float3 incorrect_color, + float3 reference_color, + float strength = 1.f, + float clamp_chrominance_loss = 0.f) + { + if (strength == 0.f) + return incorrect_color; + + float3 incorrect_lab = renodx::color::ictcp::from::BT709(incorrect_color); + float3 reference_lab = renodx::color::ictcp::from::BT709(reference_color); + + float2 incorrect_ab = incorrect_lab.yz; + float2 reference_ab = reference_lab.yz; + + // Compute chrominance (magnitude of the Ct-Cp vector) + float incorrect_chrominance = length(incorrect_ab); + float correct_chrominance = length(reference_ab); + + // Scale original chrominance vector toward target chrominance + float chrominance_ratio = renodx::math::DivideSafe(correct_chrominance, incorrect_chrominance, 1.f); + float scale = lerp(1.f, chrominance_ratio, strength); + + float t = 1.0f - step(1.0f, scale); // t = 1 when scale < 1, 0 when scale >= 1 + scale = lerp(scale, 1.0f, t * clamp_chrominance_loss); + + incorrect_lab.yz = incorrect_ab * scale; + + float3 result = renodx::color::bt709::from::ICtCp(incorrect_lab); + return result; + } + + float3 ChrominancedtUCS( + float3 incorrect_color, + float3 reference_color, + float strength = 1.f, + float clamp_chrominance_loss = 0.f) // new param + { + if (strength == 0.f) + return incorrect_color; + + float3 incorrect_uvY = renodx::color::dtucs::uvY::from::BT709(incorrect_color); + float3 reference_uvY = renodx::color::dtucs::uvY::from::BT709(reference_color); + + float2 incorrect_uv = incorrect_uvY.xy; + float2 correct_uv = reference_uvY.xy; + + float Y_incorrect = incorrect_uvY.z; + float Y_correct = reference_uvY.z; + + // Compute perceptual lightness (L*) for both colors + float L_star_hat_i = pow(Y_incorrect, 0.631651345306265f); + float L_star_i = 2.098883786377f * L_star_hat_i / (L_star_hat_i + 1.12426773749357f); + float L_star_hat_c = pow(Y_correct, 0.631651345306265f); + float L_star_c = 2.098883786377f * L_star_hat_c / (L_star_hat_c + 1.12426773749357f); + + // Compute chrominance (C) for both colors + float M2_incorrect = dot(incorrect_uv, incorrect_uv); + float M2_correct = dot(correct_uv, correct_uv); + float C_incorrect = 15.932993652962535f * pow(L_star_i, 0.6523997524738018f) * pow(M2_incorrect, 0.6007557017508491f) / color::dtucs::L_WHITE; + float C_correct = 15.932993652962535f * pow(L_star_c, 0.6523997524738018f) * pow(M2_correct, 0.6007557017508491f) / color::dtucs::L_WHITE; + + // Interpolate chrominance while preserving original hue direction + float C_lerp = lerp(C_incorrect, C_correct, strength); + + float chroma_scale = renodx::math::DivideSafe(C_lerp, C_incorrect, 1.f); + float t = 1.0f - step(1.0f, chroma_scale); // t = 1 when scale < 1, 0 when scale >= 1 + chroma_scale = lerp(chroma_scale, 1.0f, t * clamp_chrominance_loss); + float C = C_incorrect * chroma_scale; + + float h = atan2(incorrect_uv.y, incorrect_uv.x); + + // Compute original perceptual lightness (J) + float J = pow(L_star_i / color::dtucs::L_WHITE, 1.f); + + // Build JCH from original J, clamped/interpolated chrominance, and original hue + float3 final_jch = float3(J, C, h); + + float3 result = renodx::color::bt709::from::dtucs::JCH(final_jch); + return result; + } + + float3 Chrominance(float3 incorrect_color, float3 correct_color, float strength = 1.f, float clamp_chrominance_loss = 0.f, int method = 0) + { + if (method == 1) + return ChrominanceICtCp(incorrect_color, correct_color, strength, clamp_chrominance_loss); + if (method == 2) + return ChrominancedtUCS(incorrect_color, correct_color, strength, clamp_chrominance_loss); + return ChrominanceOKLab(incorrect_color, correct_color, strength, clamp_chrominance_loss); + } + + float3 HueOKLab(float3 incorrect_color, float3 correct_color, float strength = 1.f) + { + if (strength == 0.f) + return incorrect_color; + + float3 incorrect_lab = renodx::color::oklab::from::BT709(incorrect_color); + float3 correct_lab = renodx::color::oklab::from::BT709(correct_color); + + float2 incorrect_ab = incorrect_lab.yz; + float2 correct_ab = correct_lab.yz; + + // Preserve original chrominance (magnitude of the a–b vector) + float chrominance_pre_adjust = length(incorrect_ab); + + // Blend chrominance and hue by interpolating (a, b) components + float2 blended_ab = lerp(incorrect_ab, correct_ab, strength); + + // Rescale to original chrominance to avoid saturation shift + float chrominance_post_adjust = length(blended_ab); + blended_ab *= renodx::math::DivideSafe(chrominance_pre_adjust, chrominance_post_adjust, 1.f); + + incorrect_lab.yz = blended_ab; + + float3 result = renodx::color::bt709::from::OkLab(incorrect_lab); + return renodx::color::bt709::clamp::AP1(result); + } + + float3 HueICtCp(float3 incorrect_color, float3 correct_color, float strength = 1.f) + { + if (strength == 0.f) + return incorrect_color; + + float3 incorrect_ictcp = renodx::color::ictcp::from::BT709(incorrect_color); + float3 correct_ictcp = renodx::color::ictcp::from::BT709(correct_color); + + float2 incorrect_ctcp = incorrect_ictcp.yz; + float2 correct_ctcp = correct_ictcp.yz; + + // Preserve original chrominance (magnitude of the Ct-Cp vector) + float chrominance_pre_adjust = length(incorrect_ctcp); + + // Blend chrominance and hue by interpolating (Ct, Cp) components + float2 blended_ctcp = lerp(incorrect_ctcp, correct_ctcp, strength); + + // Rescale to original chrominance to avoid saturation shift + float chrominance_post_adjust = length(blended_ctcp); + blended_ctcp *= renodx::math::DivideSafe(chrominance_pre_adjust, chrominance_post_adjust, 1.f); + + incorrect_ictcp.yz = blended_ctcp; + + float3 result = renodx::color::bt709::from::ICtCp(incorrect_ictcp); + return renodx::color::bt709::clamp::AP1(result); + } + + float3 HuedtUCS(float3 incorrect_color, float3 correct_color, float strength = 1.f) + { + if (strength == 0.f) + return incorrect_color; + + float3 incorrect_uvY = renodx::color::dtucs::uvY::from::BT709(incorrect_color); + float3 correct_uvY = renodx::color::dtucs::uvY::from::BT709(correct_color); + + float2 incorrect_uv = incorrect_uvY.xy; + float2 correct_uv = correct_uvY.xy; + + float Y_incorrect = incorrect_uvY.z; + float Y_correct = correct_uvY.z; + + // Compute perceptual lightness (L*) for both colors + float L_star_hat_i = pow(Y_incorrect, 0.631651345306265f); + float L_star_i = 2.098883786377f * L_star_hat_i / (L_star_hat_i + 1.12426773749357f); + float L_star_hat_c = pow(Y_correct, 0.631651345306265f); + float L_star_c = 2.098883786377f * L_star_hat_c / (L_star_hat_c + 1.12426773749357f); + + // Compute chrominance (C) for both colors from uv vector magnitude and L* + float M2_incorrect = dot(incorrect_uv, incorrect_uv); + float M2_correct = dot(correct_uv, correct_uv); + float C_incorrect = 15.932993652962535f * pow(L_star_i, 0.6523997524738018f) * pow(M2_incorrect, 0.6007557017508491f) / color::dtucs::L_WHITE; + float C_correct = 15.932993652962535f * pow(L_star_c, 0.6523997524738018f) * pow(M2_correct, 0.6007557017508491f) / color::dtucs::L_WHITE; + + // Build chrominance-direction vectors (C * unit vector of hue angle) + float2 incorrect_vec = C_incorrect * normalize(incorrect_uv); + float2 correct_vec = C_correct * normalize(correct_uv); + + // Blend chrominance and hue by interpolating between chrominance-direction vectors + float2 blended_vec = lerp(incorrect_vec, correct_vec, strength); + + // Rescale to original chrominance to avoid saturation shift + float blended_chrominance = length(blended_vec); + blended_vec *= renodx::math::DivideSafe(C_incorrect, blended_chrominance, 1.f); + + // Reconstruct hue from blended vector + float h = atan2(blended_vec.y, blended_vec.x); + + // Compute original perceptual lightness (J) + float J = pow(L_star_i / color::dtucs::L_WHITE, 1.f); + + // Build JCH from original J, original chrominance, and interpolated hue + float3 final_jch = float3(J, C_incorrect, h); + + float3 result = renodx::color::bt709::from::dtucs::JCH(final_jch); + return renodx::color::bt709::clamp::AP1(result); + } + + float3 Hue(float3 incorrect_color, float3 correct_color, float strength = 1.f, int method = 0) + { + if (method == 1) + return HueICtCp(incorrect_color, correct_color, strength); + if (method == 2) + return HuedtUCS(incorrect_color, correct_color, strength); + return HueOKLab(incorrect_color, correct_color, strength); + } + + float3 Luminance(float3 color, float incorrect_y, float correct_y, float strength = 1.f) + { + return color * lerp(1.f, renodx::math::DivideSafe(correct_y, incorrect_y, 1.f), strength); + } + + float3 Luminance(float3 incorrect_color, float3 correct_color, float strength = 1.f) + { + return Luminance( + incorrect_color, + renodx::color::y::from::BT709(incorrect_color), + renodx::color::y::from::BT709(correct_color), + strength); + } + + float3 GamutDecompress(float3 color, float grayscale, float saturation_scale) + { + return lerp(grayscale, color, 1.f / saturation_scale); + } + + float3 GamutDecompress(float3 color, float saturation_scale) + { + float grayscale = renodx::color::y::from::BT709(color); + return GamutDecompress(color, grayscale, saturation_scale); + } + + float3 GamutCompress(float3 color, float grayscale, float saturation_scale) + { + return lerp(grayscale, color, saturation_scale); + } + + float ComputeGamutCompressionScale(float3 color, float grayscale) + { + // Desaturate (move towards grayscale) until no channel is below 0 + float lowest_negative_channel = min(0.f, min(color.r, min(color.g, color.b))); + + float distance = grayscale - lowest_negative_channel; + + float ratio = renodx::math::DivideSafe(-lowest_negative_channel, distance, 0.f); + + // if grayscale is 0, ratio is 0 via DivideSafe, so no change + // if minchannel is 0, ratio is 0, so no change + float saturation_scale = 1.f - ratio; + return saturation_scale; + } + + float ComputeGamutCompressionScale(float3 color) + { + float grayscale = renodx::color::y::from::BT709(color); + return ComputeGamutCompressionScale(color, grayscale); + } + + float3 GamutCompress(float3 color, float grayscale) + { + return lerp(grayscale, color, ComputeGamutCompressionScale(color, grayscale)); + } + + float3 GamutCompress(float3 color) + { + return GamutCompress(color, renodx::color::y::from::BT709(color)); + } + + } // namespace correct + } // namespace color +} // namespace renodx + +#endif // SRC_SHADERS_COLORCORRECT_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math.hlsl new file mode 100644 index 0000000000..067b2aff00 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math.hlsl @@ -0,0 +1,216 @@ +#ifndef SRC_SHADERS_MATH_HLSL_ +#define SRC_SHADERS_MATH_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/math/constants.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math/cross.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math/select.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math/sign.hlsl" + +START_NAMESPACE(renodx) +START_NAMESPACE(math) + +float Average(float3 color) +{ + return (color.x + color.y + color.z) / 3.f; +} + +float DivideSafe(float dividend, float divisor) +{ + return Select(divisor == 0.f, CopySign(FLT_MAX, dividend), dividend / divisor); +} + +float DivideSafe(float dividend, float divisor, float fallback) +{ + return Select(divisor == 0.f, fallback, dividend / divisor); +} + +float2 DivideSafe(float2 dividend, float2 divisor) +{ + return float2(DivideSafe(dividend.x, divisor.x, CopySign(FLT_MAX, dividend.x)), + DivideSafe(dividend.y, divisor.y, CopySign(FLT_MAX, dividend.y))); +} + +float2 DivideSafe(float2 dividend, float2 divisor, float2 fallback) +{ + return float2(DivideSafe(dividend.x, divisor.x, fallback.x), + DivideSafe(dividend.y, divisor.y, fallback.y)); +} + +float3 DivideSafe(float3 dividend, float3 divisor) +{ + return float3(DivideSafe(dividend.x, divisor.x, CopySign(FLT_MAX, dividend.x)), + DivideSafe(dividend.y, divisor.y, CopySign(FLT_MAX, dividend.y)), + DivideSafe(dividend.z, divisor.z, CopySign(FLT_MAX, dividend.z))); +} + +float3 DivideSafe(float3 dividend, float3 divisor, float3 fallback) +{ + return float3(DivideSafe(dividend.x, divisor.x, fallback.x), + DivideSafe(dividend.y, divisor.y, fallback.y), + DivideSafe(dividend.z, divisor.z, fallback.z)); +} + +float4 DivideSafe(float4 dividend, float4 divisor, float4 fallback) +{ + return float4(DivideSafe(dividend.x, divisor.x, fallback.x), + DivideSafe(dividend.y, divisor.y, fallback.y), + DivideSafe(dividend.z, divisor.z, fallback.z), + DivideSafe(dividend.w, divisor.w, fallback.w)); +} + +float Min(float x, float y) +{ + return min(x, y); +} + +float Min(float x, float y, float z) +{ + return Min(x, Min(y, z)); +} + +float Min(float x, float y, float z, float w) +{ + return Min(x, Min(y, z, w)); +} + +float Min(float2 xy) +{ + return Min(xy.x, xy.y); +} + +float Min(float3 xyz) +{ + return Min(xyz.x, xyz.y, xyz.z); +} + +float Min(float4 xyzw) +{ + return Min(xyzw.x, xyzw.y, xyzw.z, xyzw.w); +} + +float Max(float x, float y) +{ + return max(x, y); +} + +float Max(float x, float y, float z) +{ + return Max(x, Max(y, z)); +} + +float Max(float x, float y, float z, float w) +{ + return Max(x, Max(y, z, w)); +} + +float Max(float2 xy) +{ + return Max(xy.x, xy.y); +} + +float Max(float3 xyz) +{ + return Max(xyz.x, xyz.y, xyz.z); +} + +float Max(float4 xyzw) +{ + return Max(xyzw.x, xyzw.y, xyzw.z, xyzw.w); +} + +// Linear Normalization +// normalize() is reserved in HLSL +float Rescale(float x, float x_min, float x_max, float y_min = 0, float y_max = 1, bool clamp = false) +{ + float value = lerp(y_min, y_max, (x - x_min) / (x_max - x_min)); + if (clamp) { + value = saturate(value); + } + return value; +} + +float Rescale(float x, float x_min, float x_max, bool clamp) +{ + return Rescale(x, x_min, x_max, 0.f, 1.f, clamp); +} + +float3 Rescale(float3 x, float3 x_min, float3 x_max, float3 y_min = float3(0, 0, 0), float3 y_max = float3(1, 1, 1), bool clamp = false) +{ + float3 value = lerp(y_min, y_max, (x - x_min) / (x_max - x_min)); + if (clamp) { + value = saturate(value); + } + return value; +} + +float3 Rescale(float3 x, float3 x_min, float3 x_max, bool clamp) +{ + return Rescale(x, x_min, x_max, float3(0, 0, 0), float3(1, 1, 1), clamp); +} + +float3x3 Invert3x3(float3x3 m) +{ + float a = m[0][0], b = m[0][1], c = m[0][2]; + float d = m[1][0], e = m[1][1], f = m[1][2]; + float g = m[2][0], h = m[2][1], i = m[2][2]; + + float A = (e * i - f * h); + float B = -(d * i - f * g); + float C = (d * h - e * g); + float D = -(b * i - c * h); + float E = (a * i - c * g); + float F = -(a * h - b * g); + float G = (b * f - c * e); + float H = -(a * f - c * d); + float I = (a * e - b * d); + + float det = a * A + b * B + c * C; + float invDet = DivideSafe(1.0, det, 0.0); + + return float3x3( + A, D, G, + B, E, H, + C, F, I) * + invDet; +} + +#if __SHADER_TARGET_MAJOR >= 4 || defined(VULKAN) +float ZeroNaN(float x) +{ + return Select(isnan(x), 0.f, x); +} +float2 ZeroNaN(float2 x) +{ + return Select(isnan(x), 0.f, x); +} +float3 ZeroNaN(float3 x) +{ + return Select(isnan(x), 0.f, x); +} +float4 ZeroNaN(float4 x) +{ + return Select(isnan(x), 0.f, x); +} +#else +float ZeroNaN(float x) +{ + return Select(x != x, 0.f, x); +} +float2 ZeroNaN(float2 value) +{ + return float2(ZeroNaN(value.x), ZeroNaN(value.y)); +} +float3 ZeroNaN(float3 value) +{ + return float3(ZeroNaN(value.x), ZeroNaN(value.y), ZeroNaN(value.z)); +} +float4 ZeroNaN(float4 value) +{ + return float4(ZeroNaN(value.x), ZeroNaN(value.y), ZeroNaN(value.z), ZeroNaN(value.w)); +} +#endif + +END_NAMESPACE(math) +END_NAMESPACE(renodx) + +#endif // SRC_SHADERS_MATH_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/constants.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/constants.hlsl new file mode 100644 index 0000000000..cbad1f85e1 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/constants.hlsl @@ -0,0 +1,41 @@ +#ifndef SRC_SHADERS_MATH_CONSTANTS_HLSL_ +#define SRC_SHADERS_MATH_CONSTANTS_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/math/cross.hlsl" + +#ifdef FLT_MIN +# undef FLT_MIN +#endif + +#ifdef FLT_MAX +# undef FLT_MAX +#endif + +START_NAMESPACE(renodx) +START_NAMESPACE(math) + +static const float FLT10_MAX = 64512.f; +static const float FLT11_MAX = 65024.f; + +static const float FLT16_MIN = CROSS_COMPILE(asfloat(0x0400), 0.00006103515625); +static const float FLT16_MAX = 65504.f; +static const float FLT32_MIN = CROSS_COMPILE(asfloat(0x00800000), 1.17549435082228750797e-38); +static const float FLT32_MAX = CROSS_COMPILE(asfloat(0x7F7FFFFF), 3.40282346638528859812e+38); +static const float FLT_MIN = CROSS_COMPILE(asfloat(0x00800000), 1.17549435082228750797e-38); +static const float FLT_MAX = CROSS_COMPILE(asfloat(0x7F7FFFFF), 3.40282346638528859812e+38); + +static const float INFINITY = CROSS_COMPILE(asfloat(0x7F800000), 1.0 / 0.0); +static const float NEG_INFINITY = CROSS_COMPILE(asfloat(0xFF800000), -1.0 / 0.0); +static const float PI = 3.14159265358979323846f; + +static const float FLT10_EPSILON = CROSS_COMPILE(asfloat(0x3C00 + 0x0040), 0.0078125); // 2^-7 +static const float FLT11_EPSILON = CROSS_COMPILE(asfloat(0x3C00 + 0x0020), 0.00390625); // 2^-8 +static const float FLT12_EPSILON = CROSS_COMPILE(asfloat(0x3C00 + 0x0010), 0.001953125); // 2^-9 +static const float FLT16_EPSILON = CROSS_COMPILE(asfloat(0x3C00 + 0x0004), 0.0009765625); // 2^-10 +static const float FLT32_EPSILON = CROSS_COMPILE(asfloat(0x34000000), 1.1920928955078125e-7); // 2^-23 +static const float FLT_EPSILON = CROSS_COMPILE(asfloat(0x34000000), 1.1920928955078125e-7); // 2^-23 + +END_NAMESPACE(math) +END_NAMESPACE(renodx) + +#endif // SRC_SHADERS_MATH_CONSTANTS_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/cross.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/cross.hlsl new file mode 100644 index 0000000000..e400e278ff --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/cross.hlsl @@ -0,0 +1,72 @@ +#ifndef SRC_SHADERS_MATH_CROSS_HLSL_ +#define SRC_SHADERS_MATH_CROSS_HLSL_ + +#if !defined(__SLANG__) && (defined(VULKAN) || defined(GL_ES)) + +# define START_NAMESPACE(x) +# define END_NAMESPACE(x) +# define CROSS_COMPILE(dx, vulkan_glsl) vulkan_glsl + +# define static +# define asfloat uintBitsToFloat +# define mad(a, b, c) fma(a, b, c) +# define saturate(a) clamp(a, 0.0, 1.0) +# define lerp(a, b, t) mix(a, b, t) + +# define float3x3 mat3 +# define float2 vec2 +# define float3 vec3 +# define float4 vec4 +# define mul(a, b) (b * a) + +vec2 pow(vec2 x, float y) +{ + return pow(x, vec2(y)); +} + +vec3 pow(vec3 x, float y) +{ + return pow(x, vec3(y)); +} + +vec4 pow(vec4 x, float y) +{ + return pow(x, vec4(y)); +} + +vec2 fma(vec2 a, float b, float c) +{ + return fma(a, vec2(b), vec2(c)); +} + +vec3 fma(vec3 a, float b, float c) +{ + return fma(a, vec3(b), vec3(c)); +} + +vec4 fma(vec4 a, float b, float c) +{ + return fma(a, vec4(b), vec4(c)); +} + +# define CROSS_MATRIX(matrix, row, column) matrix[column][row] + +# if defined(VULKAN) +# define CROSS_COMPILE_ALL(dx, vulkan, glsl) vulkan +# else +# define CROSS_COMPILE_ALL(dx, vulkan, glsl) glsl +# endif + +#else // HLSL + +# define START_NAMESPACE(x) \ + namespace x \ + { +# define END_NAMESPACE(x) } +# define CROSS_COMPILE(dx, vulkan_glsl) dx +# define CROSS_COMPILE_ALL(dx, vulkan, glsl) dx +# define CROSS_MATRIX(matrix, row, column) matrix[row][column] + +#endif // HLSL + +#endif // SRC_SHADERS_MATH_CROSS_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/select.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/select.hlsl new file mode 100644 index 0000000000..0f71814468 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/select.hlsl @@ -0,0 +1,119 @@ +#ifndef SRC_SHADERS_MATH_SELECT_HLSL_ +#define SRC_SHADERS_MATH_SELECT_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/math/cross.hlsl" + +START_NAMESPACE(renodx) +START_NAMESPACE(math) + +#if __SHADER_TARGET_MAJOR >= 6 || defined(VULKAN) +# define SELECT_FUNCTION_GENERATOR_SCALAR(TYPE) \ + TYPE Select(bool condition, TYPE trueValue, TYPE falseValue) \ + { \ + return select(condition, trueValue, falseValue); \ + } + +# define SELECT_FUNCTION_GENERATOR_VECTOR(TYPE, SIZE) \ + TYPE##SIZE Select(bool condition, TYPE##SIZE trueValue, TYPE##SIZE falseValue) \ + { \ + return select(condition, trueValue, falseValue); \ + } \ + TYPE##SIZE Select(bool condition, TYPE##SIZE trueValue, TYPE falseValue) \ + { \ + return select(condition, trueValue, falseValue); \ + } \ + TYPE##SIZE Select(bool condition, TYPE trueValue, TYPE##SIZE falseValue) \ + { \ + return select(condition, trueValue, falseValue); \ + } \ + TYPE##SIZE Select(bool##SIZE condition, TYPE##SIZE trueValue, TYPE##SIZE falseValue) \ + { \ + return select(condition, trueValue, falseValue); \ + } + +#else +// Backport of select(t,a,b) +# define SELECT_FUNCTION_GENERATOR_SCALAR(TYPE) \ + TYPE Select(bool condition, TYPE trueValue, TYPE falseValue) \ + { \ + [flatten] if (condition) \ + { \ + return trueValue; \ + } \ + else \ + { \ + return falseValue; \ + } \ + } + +# define SELECT_FUNCTION_GENERATOR_VECTOR(TYPE, SIZE) \ + TYPE##SIZE Select(bool condition, TYPE##SIZE trueValue, TYPE##SIZE falseValue) \ + { \ + [flatten] if (condition) \ + { \ + return trueValue; \ + } \ + else \ + { \ + return falseValue; \ + } \ + } \ + TYPE##SIZE Select(bool condition, TYPE##SIZE trueValue, TYPE falseValue) \ + { \ + [flatten] if (condition) \ + { \ + return trueValue; \ + } \ + else \ + { \ + return falseValue; \ + } \ + } \ + TYPE##SIZE Select(bool condition, TYPE trueValue, TYPE##SIZE falseValue) \ + { \ + [flatten] if (condition) \ + { \ + return trueValue; \ + } \ + else \ + { \ + return falseValue; \ + } \ + } \ + TYPE##SIZE Select(bool##SIZE condition, TYPE##SIZE trueValue, TYPE##SIZE falseValue) \ + { \ + TYPE##SIZE result; \ + [unroll] for (int i = 0; i < SIZE; ++i) \ + { \ + [flatten] if (condition[i]) \ + { \ + result[i] = trueValue[i]; \ + } \ + else \ + { \ + result[i] = falseValue[i]; \ + } \ + } \ + return result; \ + } + +#endif + +#define SELECT_FUNCTION_GENERATOR(TYPE) \ + SELECT_FUNCTION_GENERATOR_SCALAR(TYPE) \ + SELECT_FUNCTION_GENERATOR_VECTOR(TYPE, 2) \ + SELECT_FUNCTION_GENERATOR_VECTOR(TYPE, 3) \ + SELECT_FUNCTION_GENERATOR_VECTOR(TYPE, 4) + +SELECT_FUNCTION_GENERATOR(float) +SELECT_FUNCTION_GENERATOR(uint) +SELECT_FUNCTION_GENERATOR(int) +SELECT_FUNCTION_GENERATOR(half) + +#undef SELECT_FUNCTION_GENERATOR +#undef SELECT_FUNCTION_GENERATOR_SCALAR +#undef SELECT_FUNCTION_GENERATOR_VECTOR + +END_NAMESPACE(math) +END_NAMESPACE(renodx) +#endif // SRC_SHADERS_MATH_SELECT_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/sign.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/sign.hlsl new file mode 100644 index 0000000000..61e749bada --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/math/sign.hlsl @@ -0,0 +1,189 @@ +#ifndef SRC_SHADERS_MATH_SIGN_HLSL_ +#define SRC_SHADERS_MATH_SIGN_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/math/constants.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math/cross.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math/select.hlsl" + +START_NAMESPACE(renodx) +START_NAMESPACE(math) + +static const uint FLT32_SIGN = 0x80000000u; // 0b10000000000000000000000000000000 +static const uint FLT32_MAGNITUDE = 0x7FFFFFFFu; // 0b01111111111111111111111111111111 + +#if __SHADER_TARGET_MAJOR <= 3 +# define SELECT_FUNCTION_GENERATOR(TYPE) \ + TYPE CopySign(TYPE mag, TYPE sgn) \ + { \ + TYPE abs_value = abs(mag); \ + return Select(sgn < 0, -abs_value, abs_value); \ + } +SELECT_FUNCTION_GENERATOR(float) +SELECT_FUNCTION_GENERATOR(float2) +SELECT_FUNCTION_GENERATOR(float3) +SELECT_FUNCTION_GENERATOR(float4) +# undef SELECT_FUNCTION_GENERATOR +#else +float CopySign(float mag, float sgn) +{ + uint sign_bits = asuint(sgn) & FLT32_SIGN; + uint mag_bits = asuint(mag) & FLT32_MAGNITUDE; + return asfloat(sign_bits | mag_bits); +} + +# define COPYSIGN_FUNCTION_GENERATOR_VECTOR(SIZE) \ + float##SIZE CopySign(float##SIZE mag, float##SIZE sgn) \ + { \ + uint##SIZE sign_bits = asuint(sgn) & FLT32_SIGN; \ + uint##SIZE mag_bits = asuint(mag) & FLT32_MAGNITUDE; \ + float##SIZE result = asfloat(sign_bits | mag_bits); \ + return result; \ + } + +COPYSIGN_FUNCTION_GENERATOR_VECTOR(2) +COPYSIGN_FUNCTION_GENERATOR_VECTOR(3) +COPYSIGN_FUNCTION_GENERATOR_VECTOR(4) +# undef COPYSIGN_FUNCTION_GENERATOR_VECTOR +#endif + +// Half-precision CopySign +#if __SHADER_TARGET_MAJOR >= 6 && __SHADER_TARGET_MINOR >= 2 + +static const uint16_t FLT16_SIGN = 0x8000; // 0b1000000000000000 +static const uint16_t FLT16_MAGNITUDE = 0x7FFF; // 0b0111111111111111 + +half CopySign(half mag, half sgn) +{ + uint16_t sign_u = asuint16(sgn) & FLT16_SIGN; + uint16_t mag_u = asuint16(mag) & FLT16_MAGNITUDE; + return asfloat16(sign_u | mag_u); +} + +half2 CopySign(half2 mag, half2 sgn) +{ + uint16_t sign_x_u = asuint16(sgn.x) & FLT16_SIGN; + uint16_t sign_y_u = asuint16(sgn.y) & FLT16_SIGN; + + uint16_t mag_x_u = asuint16(mag.x) & FLT16_MAGNITUDE; + uint16_t mag_y_u = asuint16(mag.y) & FLT16_MAGNITUDE; + + return half2( + asfloat16(sign_x_u | mag_x_u), + asfloat16(sign_y_u | mag_y_u)); +} + +half3 CopySign(half3 mag, half3 sgn) +{ + uint16_t sign_x_u = asuint16(sgn.x) & FLT16_SIGN; + uint16_t sign_y_u = asuint16(sgn.y) & FLT16_SIGN; + uint16_t sign_z_u = asuint16(sgn.z) & FLT16_SIGN; + + uint16_t mag_x_u = asuint16(mag.x) & FLT16_MAGNITUDE; + uint16_t mag_y_u = asuint16(mag.y) & FLT16_MAGNITUDE; + uint16_t mag_z_u = asuint16(mag.z) & FLT16_MAGNITUDE; + + return half3( + asfloat16(sign_x_u | mag_x_u), + asfloat16(sign_y_u | mag_y_u), + asfloat16(sign_z_u | mag_z_u)); +} + +half4 CopySign(half4 mag, half4 sgn) +{ + uint16_t sign_x_u = asuint16(sgn.x) & FLT16_SIGN; + uint16_t sign_y_u = asuint16(sgn.y) & FLT16_SIGN; + uint16_t sign_z_u = asuint16(sgn.z) & FLT16_SIGN; + uint16_t sign_w_u = asuint16(sgn.w) & FLT16_SIGN; + + uint16_t mag_x_u = asuint16(mag.x) & FLT16_MAGNITUDE; + uint16_t mag_y_u = asuint16(mag.y) & FLT16_MAGNITUDE; + uint16_t mag_z_u = asuint16(mag.z) & FLT16_MAGNITUDE; + uint16_t mag_w_u = asuint16(mag.w) & FLT16_MAGNITUDE; + + return half4( + asfloat16(sign_x_u | mag_x_u), + asfloat16(sign_y_u | mag_y_u), + asfloat16(sign_z_u | mag_z_u), + asfloat16(sign_w_u | mag_w_u)); +} +#else +# define COPYSIGN_HALF_FUNCTION_GENERATOR(TYPE) \ + TYPE CopySign(TYPE mag, TYPE sgn) \ + { \ + TYPE abs_value = abs(mag); \ + return Select(sgn < 0, -abs_value, abs_value); \ + } +COPYSIGN_HALF_FUNCTION_GENERATOR(half) +COPYSIGN_HALF_FUNCTION_GENERATOR(half2) +COPYSIGN_HALF_FUNCTION_GENERATOR(half3) +COPYSIGN_HALF_FUNCTION_GENERATOR(half4) +# undef COPYSIGN_HALF_FUNCTION_GENERATOR +#endif + +#define SIGNPOW_FUNCTION_GENERATOR(T) \ + T SignPow(T x, T exponent) \ + { \ + return CopySign(pow(abs(x), exponent), x); \ + } +SIGNPOW_FUNCTION_GENERATOR(float) +SIGNPOW_FUNCTION_GENERATOR(float2) +SIGNPOW_FUNCTION_GENERATOR(float3) +SIGNPOW_FUNCTION_GENERATOR(float4) +SIGNPOW_FUNCTION_GENERATOR(half) +SIGNPOW_FUNCTION_GENERATOR(half2) +SIGNPOW_FUNCTION_GENERATOR(half3) +SIGNPOW_FUNCTION_GENERATOR(half4) +#undef SIGNPOW_FUNCTION_GENERATOR + +#define SIGNSQRT_FUNCTION_GENERATOR(T) \ + T SignSqrt(T x) \ + { \ + return CopySign(sqrt(abs(x)), x); \ + } +SIGNSQRT_FUNCTION_GENERATOR(float) +SIGNSQRT_FUNCTION_GENERATOR(float2) +SIGNSQRT_FUNCTION_GENERATOR(float3) +SIGNSQRT_FUNCTION_GENERATOR(float4) +SIGNSQRT_FUNCTION_GENERATOR(half) +SIGNSQRT_FUNCTION_GENERATOR(half2) +SIGNSQRT_FUNCTION_GENERATOR(half3) +SIGNSQRT_FUNCTION_GENERATOR(half4) +#undef SIGNSQRT_FUNCTION_GENERATOR + +#define CBRT_FUNCTION_GENERATOR_FLOAT(T) \ + T Cbrt(T x) \ + { \ + return SignPow(x, 1.f / 3.f); \ + } +#define CBRT_FUNCTION_GENERATOR_HALF(T) \ + T Cbrt(T x) \ + { \ + return SignPow(x, 1.h / 3.h); \ + } +CBRT_FUNCTION_GENERATOR_FLOAT(float) +CBRT_FUNCTION_GENERATOR_FLOAT(float2) +CBRT_FUNCTION_GENERATOR_FLOAT(float3) +CBRT_FUNCTION_GENERATOR_FLOAT(float4) +CBRT_FUNCTION_GENERATOR_HALF(half) +CBRT_FUNCTION_GENERATOR_HALF(half2) +CBRT_FUNCTION_GENERATOR_HALF(half3) +CBRT_FUNCTION_GENERATOR_HALF(half4) +#undef CBRT_FUNCTION_GENERATOR_HALF +#undef CBRT_FUNCTION_GENERATOR_FLOAT + +// IEEE Float (0, +/-1, NaN) Sign function +#define SIGN_FUNCTION_GENERATOR(T) \ + T Sign(T x) \ + { \ + return sign(x); \ + } + +SIGN_FUNCTION_GENERATOR(float) +SIGN_FUNCTION_GENERATOR(float2) +SIGN_FUNCTION_GENERATOR(float3) +SIGN_FUNCTION_GENERATOR(float4) +#undef SIGN_FUNCTION_GENERATOR + +END_NAMESPACE(math) +END_NAMESPACE(renodx) +#endif // SRC_SHADERS_MATH_SIGN_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/aces.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/aces.hlsl new file mode 100644 index 0000000000..f63add407f --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/aces.hlsl @@ -0,0 +1,494 @@ +#ifndef SRC_SHADERS_ACES_HLSL_ +#define SRC_SHADERS_ACES_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/color.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace tonemap + { + namespace aces + { + + static const float3x3 RRT_SAT_MAT = float3x3( + 0.9708890, 0.0269633, 0.00214758, + 0.0108892, 0.9869630, 0.00214758, + 0.0108892, 0.0269633, 0.96214800); + + static const float3x3 ODT_SAT_MAT = float3x3( + 0.949056, 0.0471857, 0.00375827, + 0.019056, 0.9771860, 0.00375827, + 0.019056, 0.0471857, 0.93375800); + + static const float3x3 M = float3x3( + 0.5, -1.0, 0.5, + -1.0, 1.0, 0.0, + 0.5, 0.5, 0.0); + + float Rgb2Yc(float3 rgb) + { + const float yc_radius_weight = 1.75; + // Converts RGB to a luminance proxy, here called YC + // YC is ~ Y + K * Chroma + // Constant YC is a cone-shaped surface in RGB space, with the tip on the + // neutral axis, towards white. + // YC is normalized: RGB 1 1 1 maps to YC = 1 + // + // ycRadiusWeight defaults to 1.75, although can be overridden in function + // call to rgb_2_yc + // ycRadiusWeight = 1 -> YC for pure cyan, magenta, yellow == YC for neutral + // of same value + // ycRadiusWeight = 2 -> YC for pure red, green, blue == YC for neutral of + // same value. + + float r = rgb[0]; + float g = rgb[1]; + float b = rgb[2]; + + float chroma = sqrt(b * (b - g) + g * (g - r) + r * (r - b)); + + return (b + g + r + yc_radius_weight * chroma) / 3.; + } + + float Rgb2Saturation(float3 rgb) + { + float minrgb = min(min(rgb.r, rgb.g), rgb.b); + float maxrgb = max(max(rgb.r, rgb.g), rgb.b); + return (max(maxrgb, 1e-10) - max(minrgb, 1e-10)) / max(maxrgb, 1e-2); + } + + // Sigmoid function in the range 0 to 1 spanning -2 to +2. + float SigmoidShaper(float x) + { + float t = max(1 - abs(0.5 * x), 0); + float y = 1 + sign(x) * (1 - t * t); + return 0.5 * y; + } + + float GlowFwd(float yc_in, float glow_gain_in, float glow_mid) + { + float glow_gain_out; + + if (yc_in <= 2. / 3. * glow_mid) { + glow_gain_out = glow_gain_in; + } else if (yc_in >= 2 * glow_mid) { + glow_gain_out = 0; + } else { + glow_gain_out = glow_gain_in * (glow_mid / yc_in - 0.5); + } + + return glow_gain_out; + } + + // Transformations from RGB to other color representations + float Rgb2Hue(float3 rgb) + { + const float aces_pi = 3.14159265359f; + // Returns a geometric hue angle in degrees (0-360) based on RGB values. + // For neutral colors, hue is undefined and the function will return a quiet NaN value. + float hue; + if (rgb.r == rgb.g && rgb.g == rgb.b) { + hue = 0.0; // RGB triplets where RGB are equal have an undefined hue + } else { + hue = (180.0f / aces_pi) * atan2(sqrt(3.0f) * (rgb.g - rgb.b), 2.0f * rgb.r - rgb.g - rgb.b); + } + + if (hue < 0.0f) { + hue = hue + 360.0f; + } + + return clamp(hue, 0, 360.f); + } + + float CenterHue(float hue, float center_h) + { + float hue_centered = hue - center_h; + if (hue_centered < -180.) { + hue_centered += 360; + } else if (hue_centered > 180.) { + hue_centered -= 360; + } + return hue_centered; + } + + float3 YToLinCV(float3 y, float y_max, float y_min) + { + return (y - y_min) / (y_max - y_min); + } + + // Transformations between CIE XYZ tristimulus values and CIE x,y + // chromaticity coordinates + float3 XYZToXyY(float3 xyz) + { + float3 xy_y; + float divisor = (xyz[0] + xyz[1] + xyz[2]); + if (divisor == 0.f) + divisor = 1e-10f; + xy_y[0] = xyz[0] / divisor; + xy_y[1] = xyz[1] / divisor; + xy_y[2] = xyz[1]; + + return xy_y; + } + + float3 XyYToXYZ(float3 xy_y) + { + float3 xyz; + xyz[0] = xy_y[0] * xy_y[2] / max(xy_y[1], 1e-10); + xyz[1] = xy_y[2]; + xyz[2] = (1.0 - xy_y[0] - xy_y[1]) * xy_y[2] / max(xy_y[1], 1e-10); + + return xyz; + } + + static const float DIM_SURROUND_GAMMA = 0.9811; + + float3 DarkToDim(float3 xyz, float dim_surround_gamma = DIM_SURROUND_GAMMA) + { + float3 xy_y = XYZToXyY(xyz); + xy_y.z = clamp(xy_y.z, 0.0, 65504.0f); + xy_y.z = pow(xy_y.z, DIM_SURROUND_GAMMA); + return XyYToXYZ(xy_y); + } + + static const float MIN_STOP_SDR = -6.5; + static const float MAX_STOP_SDR = 6.5; + + static const float MIN_STOP_RRT = -15.0; + static const float MAX_STOP_RRT = 18.0; + + static const float MIN_LUM_SDR = 0.02; + static const float MAX_LUM_SDR = 48.0; + + static const float MIN_LUM_RRT = 0.0001; + static const float MAX_LUM_RRT = 10000.0; + + static const float2x2 MIN_LUM_TABLE = float2x2( + log10(MIN_LUM_RRT), MIN_STOP_RRT, + log10(MIN_LUM_SDR), MIN_STOP_SDR); + + static const float2x2 MAX_LUM_TABLE = float2x2( + log10(MAX_LUM_SDR), MAX_STOP_SDR, + log10(MAX_LUM_RRT), MAX_STOP_RRT); + + float Interpolate1D(float2x2 table, float p) + { + if (p < table[0].x) { + return table[0].y; + } else if (p >= table[1].x) { + return table[1].y; + } else { + // p = clamp(p, table[0].x, table[1].x); + float s = (p - table[0].x) / (table[1].x - table[0].x); + return table[0].y * (1 - s) + table[1].y * s; + } + } + + float3 LinCv2Y(float3 lin_cv, float y_max, float y_min) + { + return lin_cv * (y_max - y_min) + y_min; + } + + float LookUpAcesMin(float min_lum_log10) + { + return 0.18 * exp2(Interpolate1D(MIN_LUM_TABLE, min_lum_log10)); + } + + float LookUpAcesMax(float max_lum_log10) + { + return 0.18 * exp2(Interpolate1D(MAX_LUM_TABLE, max_lum_log10)); + } + + struct ODTConfig + { + float3 y_min; + float3 y_mid; + float3 y_max; + float coefs_low[6]; + float coefs_high[6]; + }; + + ODTConfig CreateODTConfig(float min_y, float max_y) + { + ODTConfig config; + + const float min_lum = min_y; + const float max_lum = max_y; + // Aces-dev has more expensive version + // AcesParams PARAMS = init_aces_params(minY, maxY); + + static const float2x2 BENDS_LOW_TABLE = float2x2( + MIN_STOP_RRT, 0.18, MIN_STOP_SDR, 0.35); + + static const float2x2 BENDS_HIGH_TABLE = float2x2( + MAX_STOP_SDR, 0.89, MAX_STOP_RRT, 0.90); + + float min_lum_log10 = log10(min_lum); + float max_lum_log10 = log10(max_lum); + const float aces_min = LookUpAcesMin(min_lum_log10); + const float aces_max = LookUpAcesMax(max_lum_log10); + // float3 MIN_PT = float3(lookup_ACESmin(minLum), minLum, 0.0); + static const float3 MID_PT = float3(0.18, 4.8, 1.55); + // float3 MAX_PT = float3(lookup_ACESmax(maxLum), maxLum, 0.0); + // float coefs_low[5]; + // float coefs_high[5]; + + float2 log_min = float2(log10(aces_min), min_lum_log10); + static const float2 LOG_MID = float2(log10(MID_PT.xy)); + float2 log_max = float2(log10(aces_max), max_lum_log10); + + float knot_inc_low = (LOG_MID.x - log_min.x) / 3.; + // float halfKnotInc = (logMid.x - log_min.x) / 6.; + + // Determine two lowest coefficients (straddling minPt) + // coefs_low[0] = (MIN_PT.z * (log_min.x- 0.5 * knot_inc_low)) + ( log_min.y - MIN_PT.z * log_min.x); + // coefs_low[1] = (MIN_PT.z * (log_min.x+ 0.5 * knot_inc_low)) + ( log_min.y - MIN_PT.z * log_min.x); + // NOTE: if slope=0, then the above becomes just + config.coefs_low[0] = log_min.y; + config.coefs_low[1] = config.coefs_low[0]; + // leaving it as a variable for now in case we decide we need non-zero slope extensions + + // Determine two highest coefficients (straddling midPt) + float min_coef = (LOG_MID.y - MID_PT.z * LOG_MID.x); + config.coefs_low[3] = (MID_PT.z * (LOG_MID.x - 0.5 * knot_inc_low)) + (LOG_MID.y - MID_PT.z * LOG_MID.x); + config.coefs_low[4] = (MID_PT.z * (LOG_MID.x + 0.5 * knot_inc_low)) + (LOG_MID.y - MID_PT.z * LOG_MID.x); + config.coefs_low[5] = config.coefs_low[4]; + + // Middle coefficient (which defines the "sharpness of the bend") is linearly interpolated + float pct_low = Interpolate1D(BENDS_LOW_TABLE, log2(aces_min / 0.18)); + config.coefs_low[2] = log_min.y + pct_low * (LOG_MID.y - log_min.y); + + float knot_inc_high = (log_max.x - LOG_MID.x) / 3.0f; + // float halfKnotInc = (log_max.x - logMid.x) / 6.; + + // Determine two lowest coefficients (straddling midPt) + // float minCoef = ( logMid.y - MID_PT.z * logMid.x); + config.coefs_high[0] = (MID_PT.z * (LOG_MID.x - 0.5 * knot_inc_high)) + min_coef; + config.coefs_high[1] = (MID_PT.z * (LOG_MID.x + 0.5 * knot_inc_high)) + min_coef; + + // Determine two highest coefficients (straddling maxPt) + // coefs_high[3] = (MAX_PT.z * (log_max.x-0.5*knotIncHigh)) + ( log_max.y - MAX_PT.z * log_max.x); + // coefs_high[4] = (MAX_PT.z * (log_max.x+0.5*knotIncHigh)) + ( log_max.y - MAX_PT.z * log_max.x); + // NOTE: if slope=0, then the above becomes just + config.coefs_high[3] = log_max.y; + config.coefs_high[4] = config.coefs_high[3]; + config.coefs_high[5] = config.coefs_high[4]; + // leaving it as a variable for now in case we decide we need non-zero slope extensions + + // Middle coefficient (which defines the "sharpness of the bend") is linearly interpolated + + float pct_high = Interpolate1D(BENDS_HIGH_TABLE, log2(aces_max / 0.18)); + config.coefs_high[2] = LOG_MID.y + pct_high * (log_max.y - LOG_MID.y); + + config.y_min = float3(log_min.x, log_min.y, 0); + config.y_mid = float3(LOG_MID.x, LOG_MID.y, MID_PT.z); + config.y_max = float3(log_max.x, log_max.y, 0); + + return config; + } + + float SSTS(float x, ODTConfig config) + { + static const int N_KNOTS_LOW = 4; + static const int N_KNOTS_HIGH = 4; + + // Check for negatives or zero before taking the log. If negative or zero, + // set to HALF_MIN. + float log_x = log10(max(x, renodx::math::FLT_MIN)); + + float log_y; + + if (log_x > config.y_max.x) { + // Above max breakpoint (overshoot) + // If MAX_PT slope is 0, this is just a straight line and always returns + // maxLum + // y = mx+b + // log_y = computeGraphY(C.Max.z, log_x, (C.Max.y) - (C.Max.z * (C.Max.x))); + log_y = config.y_max.y; + } else if (log_x >= config.y_mid.x) { + // Part of Midtones area (Must have slope) + float knot_coord = (N_KNOTS_HIGH - 1) * (log_x - config.y_mid.x) / (config.y_max.x - config.y_mid.x); + int j = (int)knot_coord; + float t = knot_coord - j; + + float3 cf = float3(config.coefs_high[j], config.coefs_high[j + 1], config.coefs_high[j + 2]); + + float3 monomials = float3(t * t, t, 1.0); + log_y = dot(monomials, mul(M, cf)); + } else if (log_x > config.y_min.x) { + float knot_coord = (N_KNOTS_LOW - 1) * (log_x - config.y_min.x) / (config.y_mid.x - config.y_min.x); + int j = (int)knot_coord; + float t = knot_coord - j; + + float3 cf = float3(config.coefs_low[j], config.coefs_low[j + 1], config.coefs_low[j + 2]); + + float3 monomials = float3(t * t, t, 1.0); + log_y = dot(monomials, mul(M, cf)); + } else { //(log_x <= (C.Min.x)) + // Below min breakpoint (undershoot) + // log_y = computeGraphY(C.Min.z, log_x, ((C.Min.y) - C.Min.z * (C.Min.x))); + log_y = config.y_min.y; + } + + return pow(10.0, log_y); + } + + static const float LIM_CYAN = 1.147f; + static const float LIM_MAGENTA = 1.264f; + static const float LIM_YELLOW = 1.312f; + static const float THR_CYAN = 0.815f; + static const float THR_MAGENTA = 0.803f; + static const float THR_YELLOW = 0.880f; + static const float PWR = 1.2f; + + float GamutCompressChannel(float dist, float lim, float thr, float pwr) + { + float compr_dist; + float scl; + float nd; + float p; + + if (dist < thr) { + compr_dist = dist; // No compression below threshold + } else { + // Calculate scale factor for y = 1 intersect + scl = (lim - thr) / pow(pow((1.0 - thr) / (lim - thr), -pwr) - 1.0, 1.0 / pwr); + + // Normalize distance outside threshold by scale factor + nd = (dist - thr) / scl; + p = pow(nd, pwr); + + compr_dist = thr + scl * nd / (pow(1.0 + p, 1.0 / pwr)); // Compress + } + + return compr_dist; + } + + float3 GamutCompress(float3 lin_ap1) + { + // Achromatic axis + float ach = max(lin_ap1.r, max(lin_ap1.g, lin_ap1.b)); + float abs_ach = abs(ach); + // Distance from the achromatic axis for each color component aka inverse RGB ratios + float3 dist = ach != 0.f ? (ach - lin_ap1) / abs_ach : 0; + + // Compress distance with parameterized shaper function + float3 compr_dist = float3( + GamutCompressChannel(dist.r, LIM_CYAN, THR_CYAN, PWR), + GamutCompressChannel(dist.g, LIM_MAGENTA, THR_MAGENTA, PWR), + GamutCompressChannel(dist.b, LIM_YELLOW, THR_YELLOW, PWR)); + + // Recalculate RGB from compressed distance and achromatic + float3 compr_lin_ap1 = ach - compr_dist * abs_ach; + + return compr_lin_ap1; + } + + float3 RRT(float3 aces) + { + static const float3 AP1_RGB2Y = renodx::color::AP1_TO_XYZ_MAT[1].rgb; + + // --- Glow module --- // + // "Glow" module constants + static const float RRT_GLOW_GAIN = 0.05; + static const float RRT_GLOW_MID = 0.08; + float saturation = Rgb2Saturation(aces); + float yc_in = Rgb2Yc(aces); + const float s = SigmoidShaper((saturation - 0.4) / 0.2); + float added_glow = 1.0 + GlowFwd(yc_in, RRT_GLOW_GAIN * s, RRT_GLOW_MID); + aces *= added_glow; + + // --- Red modifier --- // + // Red modifier constants + static const float RRT_RED_SCALE = 0.82; + static const float RRT_RED_PIVOT = 0.03; + static const float RRT_RED_HUE = 0.; + static const float RRT_RED_WIDTH = 135.; + float hue = Rgb2Hue(aces); + const float centered_hue = CenterHue(hue, RRT_RED_HUE); + float hue_weight; + { + // hueWeight = cubic_basis_shaper(centeredHue, RRT_RED_WIDTH); + hue_weight = smoothstep(0.0, 1.0, 1.0 - abs(2.0 * centered_hue / RRT_RED_WIDTH)); + hue_weight *= hue_weight; + } + + aces.r += hue_weight * saturation * (RRT_RED_PIVOT - aces.r) * (1. - RRT_RED_SCALE); + + // --- ACES to RGB rendering space --- // + aces = clamp(aces, 0, 65535.0f); + float3 rgb_pre = mul(renodx::color::AP0_TO_AP1_MAT, aces); + rgb_pre = clamp(rgb_pre, 0, 65504.0f); + + // --- Global desaturation --- // + // rgbPre = mul( RRT_SAT_MAT, rgbPre); + static const float RRT_SAT_FACTOR = 0.96f; + rgb_pre = lerp(dot(rgb_pre, AP1_RGB2Y).xxx, rgb_pre, RRT_SAT_FACTOR); + + return rgb_pre; + } + + float ODTToneMap(float x, ODTConfig config) + { + return clamp(SSTS(x, config), 0.0, 65535.0f); + } + + float3 ODTToneMap(float3 rgb, ODTConfig config) + { + return clamp( + float3( + SSTS(rgb.r, config), + SSTS(rgb.g, config), + SSTS(rgb.b, config)), + 0.0, 65535.0f); + } + + float ODTToneMap(float x, float min_y, float max_y) + { + return ODTToneMap(x, CreateODTConfig(min_y, max_y)); + } + + float3 ODTToneMap(float3 rgb, float min_y, float max_y) + { + return ODTToneMap(rgb, CreateODTConfig(min_y, max_y)); + } + + float3 ODT(float3 rgb_pre, float min_y, float max_y, float3x3 odt_matrix = renodx::color::AP1_TO_BT709_MAT) + { + float3 tonescaled = ODTToneMap(rgb_pre, min_y, max_y); + + float3 output_color = mul(odt_matrix, tonescaled); + + return output_color; + } + + // ACES with + // Reference Rendering Transform + // Output Display Transform + float3 RRTAndODT(float3 color, float min_y, float max_y, float3x3 odt_matrix = renodx::color::AP1_TO_BT709_MAT) + { + color = mul(renodx::color::BT709_TO_AP0_MAT, color); + color = RRT(color); + color = ODT(color, min_y, max_y, odt_matrix); + return color; + } + + // ACES for Scene-Linear BT709 with: + // Reference Gamma Compression + // Reference Rendering Transform + // Output Display Transform + float3 RGCAndRRTAndODT(float3 color, float min_y, float max_y, float3x3 odt_matrix = renodx::color::AP1_TO_BT709_MAT) + { + color = mul(renodx::color::BT709_TO_AP1_MAT, color); // BT709 to AP1 + color = GamutCompress(color); // Compresses to AP1 + color = mul(renodx::color::AP1_TO_AP0_MAT, color); // Convert to AP0 + color = RRT(color); // RRT AP0 => AP1 + color = ODT(color, min_y, max_y, odt_matrix); // ODT AP1 => Matrix + return color; + } + } // namespace aces + } // namespace tonemap +} // namespace renodx + +#endif // SRC_SHADERS_ACES_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/frostbite.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/frostbite.hlsl new file mode 100644 index 0000000000..cbaa5c3c53 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/frostbite.hlsl @@ -0,0 +1,86 @@ +#ifndef SRC_SHADERS_FROSTBITE_HLSL_ +#define SRC_SHADERS_FROSTBITE_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/color.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace tonemap + { + namespace frostbite + { + + // Frostbite Engine tone mapping + // https://www.ea.com/frostbite/news/high-dynamic-range-color-grading-and-display-in-frostbite + + // Aplies exponential ("Photographic") luma compression + float RangeCompress(float x) + { + return 1.0 - exp(-x); + } + + float RangeCompress(float val, float threshold, float max_value = 1.f) + { + if (val < threshold) { + return val; + } else if (max_value <= threshold) { + return threshold; + } else { + float range = max_value - threshold; + return threshold + range * RangeCompress((val - threshold) / range); + } + } + + float3 RangeCompress(float3 val, float threshold, float max_value = 1.f) + { + return float3( + RangeCompress(val.x, threshold, max_value), + RangeCompress(val.y, threshold, max_value), + RangeCompress(val.z, threshold, max_value)); + } + + float3 BT709(float3 col, float max_value = 1.f, float rolloff_start = 0.25f, float saturation_boost_amount = 0.3f, float hue_correct_amount = 0.6f) + { + float3 ictcp = renodx::color::ictcp::from::BT709(col); + + // Hue-preserving range compression requires desaturation in order to achieve a natural look. We adaptively desaturate the input based on its luminance. + + float saturationAmount = pow(smoothstep(1.0, 0.3, ictcp.x), 1.3); + col = renodx::color::bt709::from::ICtCp(ictcp * float3(1, saturationAmount.xx)); + + // Only compress luminance starting at a certain point. Dimmer inputs are passed through without modification. + float linearSegmentEnd = rolloff_start; + + // Hue-preserving mapping + float maxCol = max(col.x, max(col.y, col.z)); + float mappedMax = RangeCompress(maxCol, linearSegmentEnd, max_value); + float3 compressedHuePreserving = col * mappedMax / maxCol; + + // Non-hue preserving mapping + float3 perChannelCompressed = RangeCompress(col, linearSegmentEnd, max_value); + + // Combine hue-preserving and non-hue-preserving colors. Absolute hue preservation looks unnatural, as bright colors *appear* to have been hue shifted. + // Actually doing some amount of hue shifting looks more pleasing + col = lerp(perChannelCompressed, compressedHuePreserving, hue_correct_amount); + + float3 ictcpMapped = renodx::color::ictcp::from::BT709(col); + + // Smoothly ramp off saturation as brightness increases, but keep some even for very bright input + float postCompressionSaturationBoost = saturation_boost_amount * smoothstep(1.0, 0.5, ictcp.x); + + // Re-introduce some hue from the pre-compression color. Something similar could be accomplished by delaying the luma-dependent desaturation before range compression. + // Doing it here however does a better job of preserving perceptual luminance of highly saturated colors. Because in the hue-preserving path we only range-compress the max channel, + // saturated colors lose luminance. By desaturating them more aggressively first, compressing, and then re-adding some saturation, we can preserve their brightness to a greater extent. + ictcpMapped.yz = lerp(ictcpMapped.yz, ictcp.yz * ictcpMapped.x / max(1e-3, ictcp.x), postCompressionSaturationBoost); + + col = renodx::color::bt709::from::ICtCp(ictcpMapped); + + return col; + } + + } // namespace frostbite + } // namespace tonemap +} // namespace renodx + +#endif // SRC_SHADERS_FROSTBITE_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/hermite_spline.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/hermite_spline.hlsl new file mode 100644 index 0000000000..65aa4515fb --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/hermite_spline.hlsl @@ -0,0 +1,173 @@ +#ifndef RENODX_SHADERS_TONEMAP_HERMITE_SPLINE_HLSL_ +#define RENODX_SHADERS_TONEMAP_HERMITE_SPLINE_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/color.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/colorcorrect.hlsl" +#include "PostProcessing/ColorGrading/Include/renodx/math.hlsl" + +namespace renodx +{ + namespace tonemap + { + + float HermiteSplineRolloff(float input, float target_white = 1.f, float max_white = 20.f) + { + float l_w = max_white; + // float l_b = min_black; + // float l_min = target_black; + float l_max = target_white; + float e_1 = renodx::math::Rescale(input, 0, l_w); + // float min_lum = renodx::math::Rescale(l_min, l_b, l_w); + float max_lum = renodx::math::Rescale(l_max, 0, l_w); + float knee_start = 1.5f * max_lum - 0.5f; + // float b = min_lum; + float t_b = renodx::math::Rescale(e_1, knee_start, 1.f); + + // float p_e1 = (((2 * t_b * t_b * t_b) - (3 * t_b * t_b) + 1) * knee_start) + // + (((t_b * t_b * t_b) - (2 * t_b * t_b) + t_b) * (1.f - knee_start)) + // + ((-(2 * t_b * t_b * t_b) + (3 * t_b * t_b)) * max_lum); + float t_b_squared = t_b * t_b; + float t_b_cubed = t_b_squared * t_b; + float two_t_b_cubed = 2.f * t_b_cubed; + float three_t_b_squared = 3.f * t_b_squared; + float p_e1_h00 = (two_t_b_cubed - three_t_b_squared + 1.f); + float p_e1_h10 = (t_b_cubed - 2.f * t_b_squared + t_b); + float p_e1_h01 = (-two_t_b_cubed + three_t_b_squared); + // float p_e1_h11 = (t_b_cubed - t_b_squared); // Not used since derivative is 0 at max_lum + + float p_e1 = p_e1_h00 * knee_start + p_e1_h10 * (1.f - knee_start) + p_e1_h01 * max_lum; + + float e_2 = (e_1 < knee_start) ? e_1 : p_e1; + + // float e_3 = e_2 + b * pow(1-e_2, 4); + // float e_3a1 = (1 - e_2) * (1 - e_2); + // float e_3a2 = e_3a1 * (1 - e_2); + float e_3 = e_2; + + // Custom: clamp before lerp + // e_3 = saturate(e_3); + + // float e_4 = lerp(l_b, l_w, e_3); + float e_4 = l_w * e_3; + + return min(e_4, target_white); + } + + // Hermite Spline Rolloff + // Must be normalized between 0-1 + // https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2408-8-2024-PDF-E.pdf + float HermiteSplineRolloff( + float input, + float target_white, + float max_white, + float target_black, + float min_black = 0.f) + { + float l_w = max_white; + float l_b = min_black; + float l_min = target_black; + float l_max = target_white; + float e_1 = renodx::math::Rescale(input, l_b, l_w); + float min_lum = renodx::math::Rescale(l_min, l_b, l_w); + float max_lum = renodx::math::Rescale(l_max, l_b, l_w); + float knee_start = 1.5f * max_lum - 0.5f; + float b = min_lum; + float t_b = renodx::math::Rescale(e_1, knee_start, 1.f); + + // float p_e1 = (((2 * t_b * t_b * t_b) - (3 * t_b * t_b) + 1) * knee_start) + // + (((t_b * t_b * t_b) - (2 * t_b * t_b) + t_b) * (1.f - knee_start)) + // + ((-(2 * t_b * t_b * t_b) + (3 * t_b * t_b)) * max_lum); + float t_b_squared = t_b * t_b; + float t_b_cubed = t_b_squared * t_b; + float two_t_b_cubed = 2.f * t_b_cubed; + float three_t_b_squared = 3.f * t_b_squared; + float p_e1_h00 = (two_t_b_cubed - three_t_b_squared + 1.f); + float p_e1_h10 = (t_b_cubed - 2.f * t_b_squared + t_b); + float p_e1_h01 = (-two_t_b_cubed + three_t_b_squared); + // float p_e1_h11 = (t_b_cubed - t_b_squared); // Not used since derivative is 0 at max_lum + + float p_e1 = p_e1_h00 * knee_start + p_e1_h10 * (1.f - knee_start) + p_e1_h01 * max_lum; + + float e_2 = (e_1 < knee_start) ? e_1 : p_e1; + + // float e_3 = e_2 + b * pow(1-e_2, 4); + float e_3a1 = (1 - e_2) * (1 - e_2); + float e_3a2 = e_3a1 * (1 - e_2); + float e_3 = e_2 + (b * e_3a2); + + // Custom: clamp before lerp + e_3 = saturate(e_3); + + float e_4 = lerp(l_b, l_w, e_3); + return e_4; + } + + float HermiteSplineLuminanceRolloff(float luminance, float target_white, float max_white, float target_black, float min_black, float nits = 100.f) + { + float luminance_pq = renodx::color::pq::Encode(luminance, nits); + float target_white_pq = renodx::color::pq::Encode(target_white, nits); + float max_white_pq = renodx::color::pq::Encode(max_white, nits); + float target_black_pq = renodx::color::pq::Encode(target_black, nits); + float min_black_pq = renodx::color::pq::Encode(min_black, nits); + + float scaled = HermiteSplineRolloff(luminance_pq, target_white_pq, max_white_pq, target_black_pq, min_black_pq); + + float unpq_scaled = renodx::color::pq::Decode(scaled, nits); + return unpq_scaled; + } + + float3 HermiteSplineLuminanceRolloff(float3 color, float target_white, float max_white, float target_black, float min_black = 0.f, float nits = 100.f) + { + float y = renodx::color::y::from::BT709(color); + float new_y = HermiteSplineLuminanceRolloff(y, target_white, max_white, target_black, min_black, nits); + float3 new_color = renodx::color::correct::Luminance(color, y, new_y); + return new_color; + } + + float3 HermiteSplinePerChannelRolloff(float3 input, float target_white, float max_white, float target_black, float min_black = 0.f, float nits = 100.f) + { + float3 input_pq = renodx::color::pq::Encode(input, nits); + float target_white_pq = renodx::color::pq::Encode(target_white, nits); + float max_white_pq = renodx::color::pq::Encode(max_white, nits); + float target_black_pq = renodx::color::pq::Encode(target_black, nits); + float min_black_pq = renodx::color::pq::Encode(min_black, nits); + + float3 scaled = float3( + HermiteSplineRolloff(input_pq.r, target_white_pq, max_white_pq, target_black_pq, min_black_pq), + HermiteSplineRolloff(input_pq.g, target_white_pq, max_white_pq, target_black_pq, min_black_pq), + HermiteSplineRolloff(input_pq.b, target_white_pq, max_white_pq, target_black_pq, min_black_pq)); + + float3 unpq_scaled = renodx::color::pq::Decode(scaled, nits); + return unpq_scaled; + } + + float HermiteSplineLuminanceRolloff(float luminance, float target_white = 1.f, float max_white = 20.f) + { + if (luminance == 0) + return 0; + return exp2(HermiteSplineRolloff(log2(luminance), log2(target_white), log2(max_white))); + } + + float3 HermiteSplineLuminanceRolloff(float3 color, float target_white = 1.f, float max_white = 20.f) + { + float y = renodx::color::y::from::BT709(color); + float new_y = HermiteSplineLuminanceRolloff(y, target_white, max_white); + float3 new_color = renodx::color::correct::Luminance(color, y, new_y); + return new_color; + } + + float3 HermiteSplinePerChannelRolloff(float3 input, float target_white = 1.f, float max_white = 20.f) + { + float target_white_log2 = log2(target_white); + float max_white_log2 = log2(max_white); + float3 scaled = float3( + input.r == 0 ? 0 : exp2(HermiteSplineRolloff(log2(input.r), target_white_log2, max_white_log2)), + input.g == 0 ? 0 : exp2(HermiteSplineRolloff(log2(input.g), target_white_log2, max_white_log2)), + input.b == 0 ? 0 : exp2(HermiteSplineRolloff(log2(input.b), target_white_log2, max_white_log2))); + return scaled; + } + + } +} + +#endif // RENODX_SHADERS_TONEMAP_HERMITE_SPLINE_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/neutwo.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/neutwo.hlsl new file mode 100644 index 0000000000..cc7cdc7780 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/neutwo.hlsl @@ -0,0 +1,475 @@ +#ifndef RENODX_SHADERS_TONEMAP_NEUTWO_HLSL_ +#define RENODX_SHADERS_TONEMAP_NEUTWO_HLSL_ + +#include "PostProcessing/ColorGrading/Include/renodx/color.hlsl" + +/* + * Copyright (C) 2026 Carlos Lopez + * SPDX-License-Identifier: MIT + */ + +namespace renodx +{ + namespace tonemap + { + + // Neutral tonemap + // Based on power of 2 (squared/sqrt) + // Naka-Rushton/Reinhard style tonemapper x/(x^2+k)^(1/2) + // Newton-Raphson friendly with rsqrt (faster than division) + // f'''(x) = 0 at x = 0.5 (half peak) + // https://www.desmos.com/calculator/gy1edro6nd + // Polar/Cartesian form of peak * cos(atan2(x, peak)) + // Invertible with same complexity as forward + + // f\left(x\right)=\frac{x}{\sqrt{xx+1}} + float Neutwo(float x) + { + // also written as x * rhypot(x, 1.0) + float numerator = x; + float denominator_squared = mad(x, x, 1.0); + return numerator * rsqrt(denominator_squared); + } + + // f_{p}\left(x\right)=\frac{px}{\sqrt{xx+pp}} + float Neutwo(float x, float peak) + { + // also written as x * rhypot(x, peak) + float p = peak; + + float numerator = p * x; + float denominator_squared = mad(x, x, p * p); + return numerator * rsqrt(denominator_squared); + } + + // f_{c}\left(x\right)=\frac{cpx}{\sqrt{xx\cdot\left(cc-pp\right)+\left(cc\cdot pp\right)}} + float Neutwo(float x, float peak, float clip) + { + float p = peak; + float c = clip; + float cc = c * c; + float pp = p * p; + float xx = x * x; + + float numerator = c * p * x; + float denominator_squared = mad(xx, (cc - pp), cc * pp); + + return numerator * rsqrt(denominator_squared); + } + + // f_{g}\left(x\right)=\frac{pgx\left(cc-gg\right)}{\sqrt{\left(cc-gg\right)\cdot gg\cdot\left(xx\cdot\left(cc-pp\right)+cc\cdot\left(pp-gg\right)\right)}} + float Neutwo(float x, float peak, float clip, float gray) + { + float p = peak; + float g = gray; + float c = clip; + + float cc = c * c; + float pp = p * p; + float gg = g * g; + float xx = x * x; + float cc_minus_gg = cc - gg; + + float numerator = p * g * x * cc_minus_gg; + float denominator_squared = cc_minus_gg * gg * (mad(xx, (cc - pp), cc * (pp - gg))); + return numerator * rsqrt(denominator_squared); + } + + // f_{o}\left(x\right)=\frac{pox\left(cc-gg\right)}{\sqrt{\left(cc-gg\right)\cdot\left(xx\cdot\left(ccoo-ppgg\right)+ccgg\cdot\left(pp-oo\right)\right)}} + float Neutwo(float x, float peak, float clip, float gray_in, float gray_out) + { + float p = peak; + float g = gray_in; + float o = gray_out; + + float cc = clip * clip; + float pp = peak * peak; + float gg = g * g; + float oo = o * o; + float xx = x * x; + + float cc_minus_gg = cc - gg; + + float numerator = p * o * x * cc_minus_gg; + + float ccoo = cc * oo; + float ppgg = pp * gg; + float ccgg = cc * gg; + + float denominator_squared = cc_minus_gg * mad(xx, (ccoo - ppgg), ccgg * (pp - oo)); + + return numerator * rsqrt(denominator_squared); + } + + // f_{m}\left(x\right)=\frac{qzx\left(cc-gg\right)}{\sqrt{\left(cc-gg\right)\cdot\left(xx\cdot\left(cczz-qqgg\right)+ccgg\cdot\left(qq-zz\right)\right)}}+m + float Neutwo(float x, float peak, float clip, float gray_in, float gray_out, float minimum) + { + float m = minimum; + float g = gray_in; + float z = gray_out - m; + float q = peak - m; + float c = clip; + + float cc = c * c; + float gg = g * g; + float cc_minus_gg = cc - gg; + + float numerator = q * z * x * cc_minus_gg; + + float xx = x * x; + float zz = z * z; + float qq = q * q; + + float cczz = cc * zz; + float qqgg = qq * gg; + float ccgg = cc * gg; + + float denominator_squared = cc_minus_gg * mad(xx, (cczz - qqgg), ccgg * (qq - zz)); + + return mad(numerator, rsqrt(denominator_squared), m); + } + + namespace neutwo + { + + float ComputeBT709Scale(float3 color) + { + float y = renodx::color::y::from::BT709(color); + float new_y = renodx::tonemap::Neutwo(y); + float scale = y != 0 ? (new_y / y) : 1.f; + return scale; + } + + float ComputeBT709Scale(float3 color, float peak) + { + float y = renodx::color::y::from::BT709(color); + float new_y = renodx::tonemap::Neutwo(y, peak); + float scale = y != 0 ? (new_y / y) : 1.f; + return scale; + } + + float ComputeBT709Scale(float3 color, float peak, float clip) + { + float y = renodx::color::y::from::BT709(color); + float new_y = renodx::tonemap::Neutwo(y, peak, clip); + float scale = y != 0 ? (new_y / y) : 1.f; + return scale; + } + + float ComputeBT2020Scale(float3 color) + { + float y = renodx::color::y::from::BT2020(color); + float new_y = renodx::tonemap::Neutwo(y); + float scale = y != 0 ? (new_y / y) : 1.f; + return scale; + } + + float ComputeBT2020Scale(float3 color, float peak) + { + float y = renodx::color::y::from::BT2020(color); + float new_y = renodx::tonemap::Neutwo(y, peak); + float scale = y != 0 ? (new_y / y) : 1.f; + return scale; + } + + float ComputeBT2020Scale(float3 color, float peak, float clip) + { + float y = renodx::color::y::from::BT2020(color); + float new_y = renodx::tonemap::Neutwo(y, peak, clip); + float scale = y != 0 ? (new_y / y) : 1.f; + return scale; + } + + float ComputeMaxChannelScale(float3 color) + { + float max_channel = renodx::math::Max(abs(color.rgb)); + float new_max = renodx::tonemap::Neutwo(max_channel); + float scale = max_channel != 0 ? (new_max / max_channel) : 1.f; + return scale; + } + + float ComputeMaxChannelScale(float3 color, float peak) + { + float max_channel = renodx::math::Max(abs(color.rgb)); + float new_max = renodx::tonemap::Neutwo(max_channel, peak); + float scale = max_channel != 0 ? (new_max / max_channel) : 1.f; + return scale; + } + + float ComputeMaxChannelScale(float3 color, float peak, float clip) + { + float max_channel = renodx::math::Max(abs(color.rgb)); + float new_max = renodx::tonemap::Neutwo(max_channel, peak, clip); + float scale = max_channel != 0 ? (new_max / max_channel) : 1.f; + return scale; + } + + float3 BT709(float3 color) + { + return color * ComputeBT709Scale(color); + } + + float3 BT709(float3 color, float peak) + { + return color * ComputeBT709Scale(color, peak); + } + + float3 BT709(float3 color, float peak, float clip) + { + return color * ComputeBT709Scale(color, peak, clip); + } + + float3 BT2020(float3 color) + { + return color * ComputeBT2020Scale(color); + } + + float3 BT2020(float3 color, float peak) + { + return color * ComputeBT2020Scale(color, peak); + } + + float3 BT2020(float3 color, float peak, float clip) + { + return color * ComputeBT2020Scale(color, peak, clip); + } + + float3 MaxChannel(float3 color) + { + return color * ComputeMaxChannelScale(color); + } + + float3 MaxChannel(float3 color, float peak) + { + return color * ComputeMaxChannelScale(color, peak); + } + + float3 MaxChannel(float3 color, float peak, float clip) + { + return color * ComputeMaxChannelScale(color, peak, clip); + } + + float3 PerChannel(float3 color) + { + return float3(renodx::tonemap::Neutwo(color.r), + renodx::tonemap::Neutwo(color.g), + renodx::tonemap::Neutwo(color.b)); + } + + float3 PerChannel(float3 color, float3 peak) + { + return float3(renodx::tonemap::Neutwo(color.r, peak.r), + renodx::tonemap::Neutwo(color.g, peak.g), + renodx::tonemap::Neutwo(color.b, peak.b)); + } + + float3 PerChannel(float3 color, float3 peak, float3 clip) + { + return float3(renodx::tonemap::Neutwo(color.r, peak.r, clip.r), + renodx::tonemap::Neutwo(color.g, peak.g, clip.g), + renodx::tonemap::Neutwo(color.b, peak.b, clip.b)); + } + } + + namespace inverse + { + // f_{i}\left(x\right)=\frac{x}{\sqrt{-xx+1}} + float Neutwo(float x) + { + float numerator = x; + float denominator_squared = mad(-x, x, 1.0); + return numerator * rsqrt(denominator_squared); + } + + // f_{pi}\left(x\right)=\frac{px}{\sqrt{-xx+pp}} + float Neutwo(float x, float peak) + { + float p = peak; + + float numerator = p * x; + float denominator_squared = mad(-x, x, p * p); + return numerator * rsqrt(denominator_squared); + } + + // f_{ci}\left(x\right)=\frac{cpx}{\sqrt{-xx\cdot\left(cc-pp\right)+\left(cc\cdot pp\right)}} + float Neutwo(float x, float peak, float clip) + { + float p = peak; + float c = clip; + float cc = c * c; + float pp = p * p; + float xx = x * x; + + float numerator = c * p * x; + float denominator_squared = mad(-xx, (cc - pp), cc * pp); + + return numerator * rsqrt(denominator_squared); + } + + // f_{gi}\left(x\right)=\frac{pgx\left(cc-gg\right)}{\sqrt{\left(cc-gg\right)\cdot gg\cdot\left(-xx\cdot\left(cc-pp\right)+cc\cdot\left(pp-gg\right)\right)}} + float Neutwo(float x, float peak, float clip, float gray) + { + float p = peak; + float g = gray; + float c = clip; + + float cc = c * c; + float pp = p * p; + float gg = g * g; + float xx = x * x; + float cc_minus_gg = cc - gg; + + float numerator = p * g * x * cc_minus_gg; + float denominator_squared = cc_minus_gg * gg * (mad(-xx, (cc - pp), cc * (pp - gg))); + return numerator * rsqrt(denominator_squared); + } + + // f_{oi}\left(x\right)=\frac{pox\left(cc-gg\right)}{\sqrt{\left(cc-gg\right)\cdot\left(-xx\cdot\left(ccoo-ppgg\right)+ccgg\cdot\left(pp-oo\right)\right)}} + float Neutwo(float x, float peak, float clip, float gray_in, float gray_out) + { + float p = peak; + float g = gray_in; + float o = gray_out; + + float cc = clip * clip; + float pp = peak * peak; + float gg = g * g; + float oo = o * o; + float xx = x * x; + + float cc_minus_gg = cc - gg; + + float numerator = p * o * x * cc_minus_gg; + + float ccoo = cc * oo; + float ppgg = pp * gg; + float ccgg = cc * gg; + + float denominator_squared = cc_minus_gg * mad(-xx, (ccoo - ppgg), ccgg * (pp - oo)); + + return numerator * rsqrt(denominator_squared); + } + + // f_{mi}\left(x\right)=\frac{qzx\left(cc-gg\right)}{\sqrt{\left(cc-gg\right)\cdot\left(-xx\cdot\left(cczz-qqgg\right)+ccgg\cdot\left(qq-zz\right)\right)}}-m + float Neutwo(float x, float peak, float clip, float gray_in, float gray_out, float minimum) + { + float m = minimum; + float g = gray_in; + float z = gray_out - m; + float q = peak - m; + float c = clip; + + float cc = c * c; + float gg = g * g; + float cc_minus_gg = cc - gg; + + float numerator = q * z * x * cc_minus_gg; + + float xx = x * x; + float zz = z * z; + float qq = q * q; + + float cczz = cc * zz; + float qqgg = qq * gg; + float ccgg = cc * gg; + + float denominator_squared = cc_minus_gg * mad(-xx, (cczz - qqgg), ccgg * (qq - zz)); + + return mad(numerator, rsqrt(denominator_squared), -m); + } + + namespace neutwo + { + float3 BT709(float3 color) + { + float y = renodx::color::y::from::BT709(color); + float new_y = renodx::tonemap::inverse::Neutwo(y); + float scale = y != 0 ? (new_y / y) : 0.f; + return color * scale; + } + + float3 BT709(float3 color, float peak) + { + float y = renodx::color::y::from::BT709(color); + float new_y = renodx::tonemap::inverse::Neutwo(y, peak); + float scale = y != 0 ? (new_y / y) : 0.f; + return color * scale; + } + + float3 BT709(float3 color, float peak, float clip) + { + float y = renodx::color::y::from::BT709(color); + float new_y = renodx::tonemap::inverse::Neutwo(y, peak, clip); + float scale = y != 0 ? (new_y / y) : 0.f; + return color * scale; + } + + float3 BT2020(float3 color) + { + float y = renodx::color::y::from::BT2020(color); + float new_y = renodx::tonemap::inverse::Neutwo(y); + float scale = y != 0 ? (new_y / y) : 0.f; + return color * scale; + } + float3 BT2020(float3 color, float peak) + { + float y = renodx::color::y::from::BT2020(color); + float new_y = renodx::tonemap::inverse::Neutwo(y, peak); + float scale = y != 0 ? (new_y / y) : 0.f; + return color * scale; + } + float3 BT2020(float3 color, float peak, float clip) + { + float y = renodx::color::y::from::BT2020(color); + float new_y = renodx::tonemap::inverse::Neutwo(y, peak, clip); + float scale = y != 0 ? (new_y / y) : 0.f; + return color * scale; + } + + float3 MaxChannel(float3 color) + { + float max_channel = max(max(abs(color.r), abs(color.g)), abs(color.b)); + float new_max = renodx::tonemap::inverse::Neutwo(max_channel); + float scale = max_channel != 0 ? (new_max / max_channel) : 0.f; + return color * scale; + } + float3 MaxChannel(float3 color, float peak) + { + float max_channel = max(max(abs(color.r), abs(color.g)), abs(color.b)); + float new_max = renodx::tonemap::inverse::Neutwo(max_channel, peak); + float scale = max_channel != 0 ? (new_max / max_channel) : 0.f; + return color * scale; + } + float3 MaxChannel(float3 color, float peak, float clip) + { + float max_channel = max(max(abs(color.r), abs(color.g)), abs(color.b)); + float new_max = renodx::tonemap::inverse::Neutwo(max_channel, peak, clip); + float scale = max_channel != 0 ? (new_max / max_channel) : 0.f; + return color * scale; + } + float3 PerChannel(float3 color) + { + return float3(renodx::tonemap::inverse::Neutwo(color.r), + renodx::tonemap::inverse::Neutwo(color.g), + renodx::tonemap::inverse::Neutwo(color.b)); + } + float3 PerChannel(float3 color, float3 peak) + { + return float3(renodx::tonemap::inverse::Neutwo(color.r, peak.r), + renodx::tonemap::inverse::Neutwo(color.g, peak.g), + renodx::tonemap::inverse::Neutwo(color.b, peak.b)); + } + float3 PerChannel(float3 color, float3 peak, float3 clip) + { + return float3(renodx::tonemap::inverse::Neutwo(color.r, peak.r, clip.r), + renodx::tonemap::inverse::Neutwo(color.g, peak.g, clip.g), + renodx::tonemap::inverse::Neutwo(color.b, peak.b, clip.b)); + } + } // namespace neutwo + } // namespace inverse + + } // namespace tonemap + +} // namespace renodx + +#endif // RENODX_SHADERS_TONEMAP_NEUTWO_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/psychov_17.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/psychov_17.hlsl new file mode 100644 index 0000000000..3862353f23 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/Include/renodx/tonemap/psychov_17.hlsl @@ -0,0 +1,1248 @@ +/* + * Copyright (C) 2026 Carlos Lopez + * SPDX-License-Identifier: MIT + */ + +#ifndef RENODX_SHADERS_TONEMAP_PSYCHOV_17_HLSL_ +#define RENODX_SHADERS_TONEMAP_PSYCHOV_17_HLSL_ + +// How to use: +// - Include this file directly; it has no dependencies. +// - Use SDR(color, cone_response) for SDR output with BT.709 gamut compression. +// - Use HDR(color, peak, cone_response) for HDR output with BT.2020 gamut compression. +// - For full control, call psychov_17(...) directly. + +namespace renodx +{ + namespace tonemap + { + namespace psycho + { + + float psycho17_Select(bool condition, float true_value, float false_value) + { +#ifdef __SHADER_TARGET_MAJOR +# if __SHADER_TARGET_MAJOR >= 6 + return select(condition, true_value, false_value); +# else + [flatten] if (condition) + { + return true_value; + } + else + { + return false_value; + } +# endif +#else + [flatten] if (condition) + { + return true_value; + } + else + { + return false_value; + } +#endif + } + + float psycho17_DivideSafe(float dividend, float divisor, float fallback) + { + return psycho17_Select(divisor == 0.f, fallback, dividend / divisor); + } + + float3 psycho17_DivideSafe(float3 dividend, float3 divisor, float3 fallback) + { + return float3( + psycho17_DivideSafe(dividend.x, divisor.x, fallback.x), + psycho17_DivideSafe(dividend.y, divisor.y, fallback.y), + psycho17_DivideSafe(dividend.z, divisor.z, fallback.z)); + } + + float3x3 psycho17_Invert3x3(float3x3 m) + { + float a = m[0][0], b = m[0][1], c = m[0][2]; + float d = m[1][0], e = m[1][1], f = m[1][2]; + float g = m[2][0], h = m[2][1], i = m[2][2]; + + float A = (e * i - f * h); + float B = -(d * i - f * g); + float C = (d * h - e * g); + float D = -(b * i - c * h); + float E = (a * i - c * g); + float F = -(a * h - b * g); + float G = (b * f - c * e); + float H = -(a * f - c * d); + float I = (a * e - b * d); + + float det = a * A + b * B + c * C; + float invDet = psycho17_DivideSafe(1.0, det, 0.0); + + return float3x3( + A, D, G, + B, E, H, + C, F, I) * + invDet; + } + + static const float psycho17_EPSILON = 1e-20f; + static const float psycho17_MB_NEAR_WHITE_EPSILON = 1e-14f; + static const float psycho17_INTERVAL_MAX = 3.402823466e+38f; + static const float psycho17_CIE1702_RAY_T_MAX = 1e20f; + static const uint psycho17_FLT32_SIGN = 0x80000000u; + static const uint psycho17_FLT32_MAGNITUDE = 0x7FFFFFFFu; + static const float3x3 psycho17_STOCKMAN_SHARP_LMS_TO_XFYFZF_MAT = float3x3( + 1.94735469f, -1.41445123f, 0.36476327f, + 0.68990272f, 0.34832189f, 0.00000000f, + 0.00000000f, 0.00000000f, 1.93485343f); + static const float3 psycho17_LMS_WEIGHTS = float3( + psycho17_STOCKMAN_SHARP_LMS_TO_XFYFZF_MAT[1][0], + psycho17_STOCKMAN_SHARP_LMS_TO_XFYFZF_MAT[1][1], + 0.0371597069161f); + static const float3x3 psycho17_BT709_TO_XYZ_MAT = float3x3( + 0.4123907993f, 0.3575843394f, 0.1804807884f, + 0.2126390059f, 0.7151686788f, 0.0721923154f, + 0.0193308187f, 0.1191947798f, 0.9505321522f); + static const float3x3 psycho17_XYZ_TO_BT709_MAT = float3x3( + 3.2409699419f, -1.5373831776f, -0.4986107603f, + -0.9692436363f, 1.8759675015f, 0.0415550574f, + 0.0556300797f, -0.2039769589f, 1.0569715142f); + static const float3x3 psycho17_BT2020_TO_XYZ_MAT = float3x3( + 0.6369580483f, 0.1446169036f, 0.1688809752f, + 0.2627002120f, 0.6779980715f, 0.0593017165f, + 0.0000000000f, 0.0280726930f, 1.0609850577f); + static const float3x3 psycho17_XYZ_TO_LMS_MAT = float3x3( + 0.2670502842655792, 0.8471990148492798, -0.03470416612462053, + -0.38706882411220156, 1.165429935890458, 0.10302286696614202, + 0.026727793989083093, -0.02729131667566509, 0.5333267257603284); + static const float3x3 psycho17_LMS_TO_XYZ_MAT = psycho17_Invert3x3(psycho17_XYZ_TO_LMS_MAT); + static const float3x3 psycho17_LMS_TO_LMS_WEIGHTED_MAT = float3x3( + psycho17_LMS_WEIGHTS.x, 0.f, 0.f, + 0.f, psycho17_LMS_WEIGHTS.y, 0.f, + 0.f, 0.f, psycho17_LMS_WEIGHTS.z); + static const float3x3 psycho17_XYZ_TO_LMS_WEIGHTED_MAT = float3x3( + mul(psycho17_LMS_TO_LMS_WEIGHTED_MAT, psycho17_XYZ_TO_LMS_MAT)); + static const float3x3 psycho17_BT709_TO_LMS_WEIGHTED_MAT = + mul(psycho17_XYZ_TO_LMS_WEIGHTED_MAT, psycho17_BT709_TO_XYZ_MAT); + static const float3x3 psycho17_BT2020_TO_LMS_WEIGHTED_MAT = + mul(psycho17_XYZ_TO_LMS_WEIGHTED_MAT, psycho17_BT2020_TO_XYZ_MAT); + + float psycho17_CopySign(float mag, float sgn) + { +#ifdef __SHADER_TARGET_MAJOR +# if __SHADER_TARGET_MAJOR <= 3 + float abs_value = abs(mag); + return psycho17_Select(sgn < 0.f, -abs_value, abs_value); +# else + uint sign_bits = asuint(sgn) & psycho17_FLT32_SIGN; + uint mag_bits = asuint(mag) & psycho17_FLT32_MAGNITUDE; + return asfloat(sign_bits | mag_bits); +# endif +#else + uint sign_bits = asuint(sgn) & psycho17_FLT32_SIGN; + uint mag_bits = asuint(mag) & psycho17_FLT32_MAGNITUDE; + return asfloat(sign_bits | mag_bits); +#endif + } + + float3 psycho17_CopySign(float3 mag, float3 sgn) + { +#ifdef __SHADER_TARGET_MAJOR +# if __SHADER_TARGET_MAJOR <= 3 + float3 abs_value = abs(mag); + return float3( + psycho17_Select(sgn.x < 0.f, -abs_value.x, abs_value.x), + psycho17_Select(sgn.y < 0.f, -abs_value.y, abs_value.y), + psycho17_Select(sgn.z < 0.f, -abs_value.z, abs_value.z)); +# else + uint3 sign_bits = asuint(sgn) & psycho17_FLT32_SIGN; + uint3 mag_bits = asuint(mag) & psycho17_FLT32_MAGNITUDE; + float3 result = asfloat(sign_bits | mag_bits); + return result; +# endif +#else + uint3 sign_bits = asuint(sgn) & psycho17_FLT32_SIGN; + uint3 mag_bits = asuint(mag) & psycho17_FLT32_MAGNITUDE; + float3 result = asfloat(sign_bits | mag_bits); + return result; +#endif + } + + float psycho17_SignPow(float x, float exponent) + { + return psycho17_CopySign(pow(abs(x), exponent), x); + } + + float psycho17_ContrastSafe(float x, float contrast, float mid_gray) + { + return psycho17_SignPow(x / mid_gray, contrast) * mid_gray; + } + + float psycho17_Highlights(float x, float highlights, float mid_gray) + { + if (highlights > 1.f) { + return max(x, lerp(x, mid_gray * pow(x / mid_gray, highlights), x)); + } else if (highlights < 1.f) { + return min(x, x / (1.f + mid_gray * pow(x / mid_gray, 2.f - highlights) - x)); + } + return x; + } + + float psycho17_Shadows(float x, float shadows, float mid_gray) + { + if (shadows > 1.f) { + return max(x, x * (1.f + (x * mid_gray / pow(x / mid_gray, shadows)))); + } else if (shadows < 1.f) { + return clamp(x * (1.f - (x * mid_gray / pow(x / mid_gray, 2.f - shadows))), 0.f, x); + } + return x; + } + + float3 psycho17_BT709ToLMS(float3 bt709) + { + return mul(psycho17_XYZ_TO_LMS_MAT, mul(psycho17_BT709_TO_XYZ_MAT, bt709)); + } + + float3 psycho17_XYZFromxyY(float3 xyY) + { + float3 xyz; + xyz.xz = float2(xyY.x, 1.f - xyY.x - xyY.y) / xyY.y * xyY.z; + xyz.y = xyY.z; + return xyz; + } + + float3 psycho17_LMSToBT709(float3 lms) + { + return mul(psycho17_XYZ_TO_BT709_MAT, mul(psycho17_LMS_TO_XYZ_MAT, lms)); + } + + float psycho17_YFFromLMS(float3 lms) + { + return mul(psycho17_STOCKMAN_SHARP_LMS_TO_XFYFZF_MAT, lms).y; + } + + float psycho17_YFFromBT709(float3 bt709) + { + return psycho17_YFFromLMS(psycho17_BT709ToLMS(bt709)); + } + + float3 psycho17_WeighLMS(float3 lms) + { + return lms * psycho17_LMS_WEIGHTS; + } + + float3 psycho17_UnweighLMS(float3 lms_weighted) + { + return lms_weighted / psycho17_LMS_WEIGHTS; + } + + float3 psycho17_WeightedLMSFromMacleodBoynton(float2 mb, float l_plus_m) + { + return float3(mb.x, 1.f - mb.x, mb.y) * l_plus_m; + } + + float3 psycho17_WeightedLMSFromMacleodBoynton(float3 mb) + { + return psycho17_WeightedLMSFromMacleodBoynton(mb.xy, mb.z); + } + + float3 psycho17_MacleodBoyntonFromWeightedLMS(float3 weighted_lms) + { + float y_mb = max(weighted_lms.x + weighted_lms.y, 0.f); + float inv = psycho17_DivideSafe(1.f, y_mb, 0.f); + return float3(weighted_lms.x * inv, weighted_lms.z * inv, y_mb); + } + + float3 psycho17_MacleodBoyntonFromLMS(float3 lms) + { + return psycho17_MacleodBoyntonFromWeightedLMS(psycho17_WeighLMS(lms)); + } + + float3 psycho17_LMSFromMacleodBoynton(float3 mb) + { + return psycho17_WeightedLMSFromMacleodBoynton(mb) / psycho17_LMS_WEIGHTS; + } + + float2 psycho17_D65MacleodBoyntonXY() + { + return psycho17_MacleodBoyntonFromWeightedLMS( + mul(psycho17_LMS_TO_LMS_WEIGHTED_MAT, + mul(psycho17_XYZ_TO_LMS_MAT, psycho17_XYZFromxyY(float3(float2(0.31272, 0.32903), 1.f))))) + .xy; + } + + float psycho17_Cross2(float2 a, float2 b) + { + return a.x * b.y - a.y * b.x; + } + + bool psycho17_RaySegmentHit2D(float2 origin, float2 direction, float2 a, float2 b, out float t_hit) + { + t_hit = 0.f; + float2 edge = b - a; + float denom = psycho17_Cross2(direction, edge); + if (abs(denom) <= psycho17_EPSILON) + return false; + + float2 a_origin = a - origin; + float t = psycho17_Cross2(a_origin, edge) / denom; + float u = psycho17_Cross2(a_origin, direction) / denom; + if (t < 0.f || u < 0.f || u > 1.f) + return false; + + t_hit = t; + return true; + } + + float psycho17_RayMaxT_RGBTriangleInMB( + float2 origin, + float2 direction, + float2 r, + float2 g, + float2 b, + out bool has_solution) + { + has_solution = false; + if (dot(direction, direction) <= psycho17_MB_NEAR_WHITE_EPSILON) + return 0.f; + + float t_best = psycho17_INTERVAL_MAX; + float t_hit; + bool hit_any = false; + + if (psycho17_RaySegmentHit2D(origin, direction, r, g, t_hit)) { + t_best = min(t_best, t_hit); + hit_any = true; + } + if (psycho17_RaySegmentHit2D(origin, direction, g, b, t_hit)) { + t_best = min(t_best, t_hit); + hit_any = true; + } + if (psycho17_RaySegmentHit2D(origin, direction, b, r, t_hit)) { + t_best = min(t_best, t_hit); + hit_any = true; + } + + has_solution = hit_any; + return hit_any ? max(t_best, 0.f) : 0.f; + } + + float psycho17_NeutwoPeakClip(float x, float peak, float clip) + { + float peak_safe = max(peak, 0.f); + float clip_safe = max(clip, peak_safe); + float x2 = x * x; + float clip2 = clip_safe * clip_safe; + float peak2 = peak_safe * peak_safe; + float denominator_squared = mad(x2, clip2 - peak2, clip2 * peak2); + return (clip_safe * peak_safe * x) * rsqrt(max(denominator_squared, 1e-20f)); + } + + float psycho17_NeutwoScaleFromRayT(float t_peak, float t_clip) + { + float t_peak_safe = max(t_peak, 0.f); + float t_clip_safe = max(t_clip, t_peak_safe); + return saturate(psycho17_NeutwoPeakClip(1.f, t_peak_safe, t_clip_safe)); + } + + float psycho17_SoftCompressionActivationFromRayT(float t_peak, float knee = 0.08f) + { + float outside = 1.f - saturate(t_peak); + return psycho17_DivideSafe(outside, outside + max(knee, 1e-6f), 0.f); + } + + float3 psycho17_NakaRushton( + float3 x, + float3 peak, + float3 anchor_in, + float3 anchor_out, + float cone_response_exponent) + { + float3 peak_minus_anchor_out = peak - anchor_out; + float3 n = cone_response_exponent * peak / peak_minus_anchor_out; + float3 contrasted = anchor_out * pow(abs(x) / anchor_in, n); + float3 saturated = peak * contrasted / (contrasted + peak_minus_anchor_out); + return psycho17_CopySign(saturated, x); + } + + float psycho17_RayExitTCIE1702(float2 origin, float2 direction) + { + if (dot(direction, direction) <= psycho17_MB_NEAR_WHITE_EPSILON) { + return psycho17_CIE1702_RAY_T_MAX; + } + + static const int kEdgeCount = 7; + static const float2 kHalfspaceNormals[kEdgeCount] = { + float2(-0.043889f, -0.006807f), + float2(-0.007821f, -0.008564f), + float2(-0.000604f, -0.007942f), + float2(0.f, -0.080835f), + float2(0.953597f, 0.307020f), + float2(-0.060969f, 0.019752f), + float2(-0.106895f, 0.004035f), + }; + static const float kHalfspaceNumerators[kEdgeCount] = { + 0.0065035249f, + 0.00104900495f, + 0.000207697044f, + 0.00165556648f, + 0.252472349f, + 0.0241967351f, + 0.0199621232f, + }; + + float2 white_to_origin = psycho17_D65MacleodBoyntonXY() - origin; + float t_best = psycho17_CIE1702_RAY_T_MAX; + bool hit_any = false; + + [unroll] for (int i = 0; i < kEdgeCount; ++i) + { + float2 halfspace_normal = kHalfspaceNormals[i]; + float denom = dot(halfspace_normal, direction); + float numerator = kHalfspaceNumerators[i] + dot(halfspace_normal, white_to_origin); + float t = denom > 1e-8f ? numerator * rcp(denom) : psycho17_CIE1702_RAY_T_MAX; + t_best = min(t_best, t); + hit_any = hit_any || (denom > 1e-8f); + } + + return hit_any ? max(t_best, 0.f) : psycho17_CIE1702_RAY_T_MAX; + } + + float psycho17_HueRelativePuritySignalFromTClip(float t_clip) + { + return saturate(psycho17_DivideSafe(1.f, t_clip, 0.f)); + } + + float psycho17_AdaptiveHueSensitivityFromTClip(float t_clip) + { + static const float kMeanD65RayDistance = 0.20139844f; + static const float kMaxD65RayDistance = 1.02634534f; + static const float kMinSensitivity = 0.35f; + + float long_ray_weight = saturate(psycho17_DivideSafe( + t_clip - kMeanD65RayDistance, + kMaxD65RayDistance - kMeanD65RayDistance, + 0.f)); + return lerp(1.f, kMinSensitivity, long_ray_weight); + } + + float psycho17_HueRelativePuritySignalFromMB(float2 mb_xy, float2 mb_anchor) + { + float2 direction = mb_xy - mb_anchor; + if (dot(direction, direction) <= psycho17_MB_NEAR_WHITE_EPSILON) { + return 0.f; + } + + return psycho17_HueRelativePuritySignalFromTClip( + psycho17_RayExitTCIE1702(mb_anchor, direction)); + } + + float psycho17_HueRelativePuritySignalFromMB(float3 mb, float2 mb_anchor) + { + if (!(mb.z > psycho17_EPSILON)) { + return 0.f; + } + + return psycho17_HueRelativePuritySignalFromMB(mb.xy, mb_anchor); + } + + float psycho17_HueRelativePuritySignal(float3 lms_input, float2 mb_anchor) + { + float3 lms_weighted = psycho17_WeighLMS(max(lms_input, 0.f)); + float3 mb = psycho17_MacleodBoyntonFromWeightedLMS(lms_weighted); + if (!(mb.z > psycho17_EPSILON)) { + return 0.f; + } + + return psycho17_HueRelativePuritySignalFromMB(mb, mb_anchor); + } + + float psycho17_D65HueSensitivity(float2 mb_xy) + { + float2 mb_white = psycho17_D65MacleodBoyntonXY(); + float2 direction = mb_xy - mb_white; + if (dot(direction, direction) <= psycho17_MB_NEAR_WHITE_EPSILON) { + return 1.f; + } + + return psycho17_AdaptiveHueSensitivityFromTClip( + psycho17_RayExitTCIE1702(mb_white, direction)); + } + + float3 psycho17_ToAdaptiveRelativeLMS(float3 lms_input, float3 current_adaptive_state_lms) + { + return psycho17_DivideSafe(lms_input, current_adaptive_state_lms, 0.f.xxx); + } + + float3 psycho17_FromAdaptiveRelativeLMS(float3 lms_relative, float3 current_adaptive_state_lms) + { + return lms_relative * max(current_adaptive_state_lms, 1e-6f.xxx); + } + + float3 psycho17_ToAdaptiveRelativeWeightedLMS(float3 lms_input, float3 current_adaptive_state_lms) + { + return psycho17_DivideSafe( + psycho17_WeighLMS(lms_input), + current_adaptive_state_lms, + 0.f.xxx); + } + + float3 psycho17_FromAdaptiveRelativeWeightedLMS( + float3 lms_weighted_relative, + float3 current_adaptive_state_lms) + { + return lms_weighted_relative * max(current_adaptive_state_lms, 1e-6f.xxx); + } + + float2 psycho17_MBFromWeightedPrimary(float3 weighted_lms_primary) + { + return psycho17_MacleodBoyntonFromWeightedLMS(weighted_lms_primary).xy; + } + + float3 psycho17_RGBToWeightedLMSPrimaryColumn(float3x3 rgb_to_lms_weighted_mat, uint primary_index) + { + return float3( + rgb_to_lms_weighted_mat[0][primary_index], + rgb_to_lms_weighted_mat[1][primary_index], + rgb_to_lms_weighted_mat[2][primary_index]); + } + + float3 psycho17_RGBToAdaptiveWeightedLMSPrimaryColumn( + float3x3 rgb_to_lms_weighted_mat, + uint primary_index, + float3 current_adaptive_state_lms) + { + return psycho17_RGBToWeightedLMSPrimaryColumn(rgb_to_lms_weighted_mat, primary_index) / current_adaptive_state_lms; + } + + void psycho17_MakeRGBTriangleInMBFromWeightedPrimaries( + float3 weighted_r, + float3 weighted_g, + float3 weighted_b, + out float2 r, + out float2 g, + out float2 b) + { + r = psycho17_MBFromWeightedPrimary(weighted_r); + g = psycho17_MBFromWeightedPrimary(weighted_g); + b = psycho17_MBFromWeightedPrimary(weighted_b); + } + + void psycho17_MakeRGBTriangleInMBAdaptiveWeighted( + float3x3 rgb_to_lms_weighted_mat, + float3 current_adaptive_state_lms, + out float2 r, + out float2 g, + out float2 b) + { + psycho17_MakeRGBTriangleInMBFromWeightedPrimaries( + psycho17_RGBToAdaptiveWeightedLMSPrimaryColumn( + rgb_to_lms_weighted_mat, 0, current_adaptive_state_lms), + psycho17_RGBToAdaptiveWeightedLMSPrimaryColumn( + rgb_to_lms_weighted_mat, 1, current_adaptive_state_lms), + psycho17_RGBToAdaptiveWeightedLMSPrimaryColumn( + rgb_to_lms_weighted_mat, 2, current_adaptive_state_lms), + r, + g, + b); + } + + float3 psycho17_ClampWeightedLMSToCIE1702(float3 lms_weighted_input, float purity_max = 1.f) + { + float3 lms_weighted_clamped = max(lms_weighted_input, 0.f); + float3 mb = psycho17_MacleodBoyntonFromWeightedLMS(lms_weighted_clamped); + float y_mb = mb.z; + float2 ls = mb.xy; + if (!(y_mb > psycho17_EPSILON)) { + return float3(lms_weighted_clamped.x, lms_weighted_clamped.y, 0.f); + } + + float2 mb_white = psycho17_D65MacleodBoyntonXY(); + float2 direction = ls - mb_white; + if (dot(direction, direction) <= psycho17_MB_NEAR_WHITE_EPSILON) { + return lms_weighted_clamped; + } + + float t_clip = psycho17_RayExitTCIE1702(mb_white, direction); + float t_final = min(1.f, max(purity_max, 0.f) * t_clip); + float2 ls_out = mb_white + direction * t_final; + return psycho17_WeightedLMSFromMacleodBoynton(ls_out, y_mb); + } + + float3 psycho17_GamutCompressWeightedLMSCoreRGBBoundFromWeightedInput( + float3 lms_weighted_input, + float2 bound_r, + float2 bound_g, + float2 bound_b, + float strength) + { + float3 lms_weighted_clamped = psycho17_ClampWeightedLMSToCIE1702(max(lms_weighted_input, 0.f)); + float3 mb = psycho17_MacleodBoyntonFromWeightedLMS(lms_weighted_clamped); + float y_mb = mb.z; + float2 ls = mb.xy; + if (!(y_mb > psycho17_EPSILON)) { + return float3(lms_weighted_clamped.x, lms_weighted_clamped.y, 0.f); + } + + float2 mb_white = psycho17_D65MacleodBoyntonXY(); + float2 direction = ls - mb_white; + if (dot(direction, direction) <= psycho17_MB_NEAR_WHITE_EPSILON) { + return lms_weighted_clamped; + } + + bool has_peak = false; + float t_peak = psycho17_RayMaxT_RGBTriangleInMB( + mb_white, direction, bound_r, bound_g, bound_b, has_peak); + float t_clip = psycho17_RayExitTCIE1702(mb_white, direction); + if (!has_peak) { + t_peak = t_clip; + } + + float t_hard = saturate(t_peak); + float t_soft = psycho17_NeutwoScaleFromRayT(min(t_peak, t_clip), t_clip); + float soft_mix = saturate(strength) * psycho17_SoftCompressionActivationFromRayT(t_peak); + float t_final = lerp(t_hard, t_soft, soft_mix); + + float2 ls_out = mb_white + t_final * direction; + return psycho17_WeightedLMSFromMacleodBoynton(ls_out, y_mb); + } + + float3 psycho17_GamutCompressWeightedLMSCoreRGBBoundFromAdaptiveWeightedInput( + float3 lms_weighted_input, + float3 current_adaptive_state_lms, + float3x3 bound_rgb_to_lms_weighted_mat, + float strength) + { + float2 bound_r; + float2 bound_g; + float2 bound_b; + psycho17_MakeRGBTriangleInMBAdaptiveWeighted( + bound_rgb_to_lms_weighted_mat, + current_adaptive_state_lms, + bound_r, + bound_g, + bound_b); + return psycho17_GamutCompressWeightedLMSCoreRGBBoundFromWeightedInput( + lms_weighted_input, + bound_r, + bound_g, + bound_b, + strength); + } + + float3 psycho17_GamutCompressLMSBoundAdaptive( + float3 lms_input, + float3 current_adaptive_state_lms, + float3x3 bound_rgb_to_lms_weighted_mat, + float strength) + { + float3 lms_weighted_relative = + psycho17_ToAdaptiveRelativeWeightedLMS(lms_input, current_adaptive_state_lms); + float3 lms_weighted_relative_out = + psycho17_GamutCompressWeightedLMSCoreRGBBoundFromAdaptiveWeightedInput( + lms_weighted_relative, + current_adaptive_state_lms, + bound_rgb_to_lms_weighted_mat, + strength); + return psycho17_UnweighLMS( + psycho17_FromAdaptiveRelativeWeightedLMS( + lms_weighted_relative_out, + current_adaptive_state_lms)); + } + + float3 psycho17_GamutCompressAdaptiveRelativeWeightedLMSBound( + float3 lms_weighted_relative_input, + float3 current_adaptive_state_lms, + float3x3 bound_rgb_to_lms_weighted_mat, + float strength) + { + return psycho17_GamutCompressWeightedLMSCoreRGBBoundFromAdaptiveWeightedInput( + lms_weighted_relative_input, + current_adaptive_state_lms, + bound_rgb_to_lms_weighted_mat, + strength); + } + + float psycho17_AdaptiveHueSensitivity(float2 mb_xy, float2 mb_anchor) + { + float2 direction = mb_xy - mb_anchor; + if (dot(direction, direction) <= psycho17_MB_NEAR_WHITE_EPSILON) { + return 1.f; + } + + return psycho17_AdaptiveHueSensitivityFromTClip( + psycho17_RayExitTCIE1702(mb_anchor, direction)); + } + + // psychov-14 + // + // Objective: + // psychov-14 first targets the observer-side bend of the scene: + // - what state the eye adapts to, + // - how the scene is converted to contrast around that adapted state, + // - how the response is shaped around that adapted state, + // - which nonlinear curve applies at each stage. + // The human observer is not a linear gain system, so the first job of this + // test is to model how a cone/adaptation/opponent/normalization cascade bends + // scene light before any device mapping is considered. + // + // psychov-14 therefore treats the problem as two coupled but distinct systems: + // - observer flow: approximate the ordered flow of the human cone visual + // pathway with a literature-backed cascade, so each stage in the code + // corresponds to a recognizable stage in human vision rather than an + // arbitrary tone curve, + // - device-hull mapping: tonemap the observer-domain result into the display's + // luminance / gamut hull as plausibly as possible. + // Tonemapping itself remains a device-hull problem, not an eye model, but the + // observer model decides which scene differences remain perceptually important + // enough to preserve when the device hull forces compression. + // + // Intended human-flow model and supporting references: + // 1) Receptor basis: + // Stockman-Sharpe LMS with CIE 170-2 physiological luminance Yf / weighted + // LMS bookkeeping, not CIE 1931 Y. + // Reference split: + // - Brainard, "Colorimetry" (chapter 10): the cone stage / color match + // foundation. Chapter 11 explicitly points back to this chapter when it + // says "The first stage of color vision is now well understood (see + // Chap. 10)." For psychov, this is the source for step 1: + // scene RGB/XYZ -> absolute cone excitations L, M, S. + // - Stockman & Brainard (chapter 11): builds on that receptor basis for + // first-site and second-site adaptation. + // Sources: + // https://color2.psych.upenn.edu/brainard/papers/Brainard_Stockman_Colorimetry.pdf + // https://color2.psych.upenn.edu/brainard/papers/Stockman_Brainard_ColorVision.pdf + // CVRL background notes that cone signals are formed only after + // prereceptoral filtering by the ocular media and macular pigment, both of + // which absorb mainly at short wavelengths and vary substantially across + // observers. The current repo plumbing should therefore be understood as an + // average-observer receptor basis unless those filters are modeled + // explicitly. + // Reference: CVRL background hub; "Macular and lens pigments" + // (http://www.cvrl.org/background.htm, + // http://www.cvrl.org/database/text/intros/intromaclens.htm). + // MacLeod-Boynton is important here: it is not itself the cortical flow, + // but a weighted cone-chromaticity representation in an equal-luminance + // plane with a separately carried achromatic scale term. In repo notation: + // l = Lw / (Lw + Mw) + // s = Sw / (Lw + Mw) + // y = Lw + Mw + // The weights here are fixed observer-transform coefficients used to form + // weighted LMS, Yf-like achromatic response, and MB coordinates from LMS. + // They are not adaptation, gain, or bleaching terms. CVRL describes the + // CIE physiological functions as linear transformations of the Stockman & + // Sharpe cone fundamentals, and Mantiuk et al. describe a practical LMS + // scaling "so that the sum of L and M cone responses corresponds to + // luminance". That is the math role of the weights in this stage. + // Reference: MacLeod & Boynton (1979, doi:10.1364/JOSA.69.001183); modern + // CIE 170-2 implementations replace ad hoc weights with standardized + // physiological cone-fundamental / luminance weights. + // Citation split for the weights used in this file: + // - Explicit CIE 170-2 / physiological-weight usage: + // CIE / CVRL physiological functions; Psychtoolbox LMSToMacBoyn; this + // repo's stockman_macleod_boynton.hlsl wiring. + // - Classic or modified MacLeod-Boynton without an explicit CIE 170-2 + // coefficient claim: + // MacLeod & Boynton (1979); Webster & Leonard (2008). + // - LMS scaled so the achromatic term is L+M, but without an explicit + // CIE 170-2 MacLeod-Boynton coefficient claim: + // Mantiuk et al. (2020). + // So classic MB, modified MB, and plain L+M-scaled LMS should not be cited + // as if they automatically justify the exact CIE 170-2 coefficients used + // here. + // Sources: + // http://www.cvrl.org/ciexyzpr.htm + // https://psychtoolbox.org/docs/LMSToMacBoyn + // https://pmc.ncbi.nlm.nih.gov/articles/PMC2657039/ + // https://www.cl.cam.ac.uk/~rkm38/pdfs/mantiuk2020practical_csf.pdf + // 2) Early cone adaptation: + // Maintain an adapting background state (L0, M0, S0, Yf0), then express the + // stimulus relative to that background before any postreceptoral transform. + // Chapter split: + // - chapter 10 gives the absolute cone excitations, + // - chapter 11 then defines how those cone excitations depend on the + // adapting background and are converted to a contrast representation. + // Source-backed first-site math is cone-specific contrast / gain control, + // not a rule that every adapted background is mapped to one fixed output + // level. Stockman & Brainard write the first-site cone contrast for the + // L-cone as: + // C_L = ΔL / (L_b + L_0) + // with analogous forms for M and S. Equivalently, a background-dependent + // gain g_L = 1 / (L_b + L_0) acts on the increment: + // g_L * (L - L_b) = ΔL / (L_b + L_0) + // Thus the observer is approximately normalizing cone signals by the + // adapted background, not forcing all backgrounds to one arbitrary response + // value. First-site adaptation is also not complete or instantaneous, and + // later second-site adaptation further reshapes postreceptoral signals. + // References: Stockman & Brainard (2010); Stockman et al. (JOV 2006, + // doi:10.1167/6.11.5). + // Webster & Leonard (2008) add an important structural point, but their + // terms need to stay separate. Their "response norm" is the adapting level + // that does not bias white judgments, while their "perceptual norm" is the + // stimulus that appears white. Those psychophysical norms tracked each + // other closely in their experiments, but they are not the same term as the + // background cone excitations L_b / M_b / S_b in Stockman & Brainard or + // the background responses L0 / M0 / S0 in Mantiuk et al. For psychov, the + // directly modeled early state is therefore best named the adapted + // background reference (L0, M0, S0, optional Yf0), while Webster's + // response-norm / perceptual-norm language remains useful as higher-level + // interpretation of why that adapted reference acts as the current neutral + // coding state. Webster & Leonard also argue that steady uniform-field + // chromatic adaptation appeared to arise largely at early stages affected + // by adaptation. + // Source: + // https://pmc.ncbi.nlm.nih.gov/articles/PMC2657039/ + // CVRL further notes that luminosity functions are strongly dependent on + // chromatic adaptation and observing conditions, whereas cone spectral + // sensitivities remain fixed until photopigment bleaching becomes + // significant. That is the reason to keep Yf / luminosity bookkeeping tied + // to the adapted observer state rather than treating a single photometric Y + // curve as condition-invariant. + // Reference: CVRL "Luminosity functions" + // (http://www.cvrl.org/database/text/intros/introvl.htm). + // 2a) Dim cone-noise regime: + // Before true rod-dominated vision, cone-mediated detection can already be + // limited by quantal / transduction noise. In this dim-but-still-cone + // regime, threshold cone contrast follows approximately De Vries-Rose + // behavior: in log-log space, threshold contrast decreases with retinal + // illuminance with slope near -0.5. At higher light levels the system moves + // toward Weber-like behavior, where threshold contrast is roughly constant + // relative to the adapted background. This matters for psychov because + // weak scene differences can disappear into a cone-noise-limited threshold + // floor before rod vision becomes dominant. + // Reference direction: + // - Stockman & Brainard (2010): cone-contrast space is most useful when + // first-site adaptation is in the Weber regime, and less useful when + // adaptation falls short of Weber's law at lower levels. + // - Angueyra & Rieke (2013): primate cone photoreceptors exhibit measurable + // phototransduction noise, giving a photoreceptor-level source for the + // dim-light threshold floor. + // Sources: + // https://color2.psych.upenn.edu/brainard/papers/Stockman_Brainard_ColorVision.pdf + // https://pmc.ncbi.nlm.nih.gov/articles/PMC3815624/ + // 2b) High-light bleaching: + // At sufficiently high retinal illuminance, reduce per-cone pigment + // availability with a Rushton-Henry-style law in trolands: + // p(I) = 1 / (1 + I / I0) + // This is the complementary "available pigment" form of the more commonly + // cited fraction-bleached law: + // p_bleached(I) = I / (I + I0) + // with I0 ~ 10^4.3 Td for cones. + // Then apply that availability to cone excursions around an adapted white + // anchor. In the current repo bleaching helper this is implemented as + // per-cone attenuation of LMS deltas around a white-at-achromatic-level + // anchor, so availability -> 0 drives the response toward equal white at the + // same carried achromatic level. That is the desired "intensely bright tends + // to bleach to white" behavior for this test. + // Source and attribution: + // - Stockman et al. (JOV 2006, doi:10.1167/6.11.5): high-light sensitivity + // regulation is maintained mainly by photopigment bleaching. + // - Stockman et al. (JOV 2018, 18(6):12): appendix states the steady-state + // bleaching law p = I / (I + I0), I0 = 10^4.3 Td, citing Rushton & Henry + // (1968). + // - CVRL "Bleaching": bleaching reduces effective pigment concentration and + // narrows spectral sensitivity without changing lambda_max; in cones this + // matters enough to affect chromatic adaptation and color matching, so a + // pure scalar availability model is only a first-order approximation. + // (http://www.cvrl.org/database/text/intros/introbleaches.htm) + // - The final white-relative application used here is the repo's rendering + // interpretation of that availability law for color signals; see + // color/bleaching.hlsl for the exact helper. + // Placement in the overall flow: + // - bleaching belongs inside the observer model, + // - after the adapting background state is defined, + // - before postreceptoral opponent encoding and pooled cortical gain, + // - and therefore before any device-hull tonemapping / gamut compression. + // 3) Background-normalized opponent drive: + // Convert cone-domain responses into ACC / DKL-style opponent coordinates + // using a background-referenced, weighted-LMS achromatic axis. + // Reference direction: derive postreceptoral opponent coordinates from the + // adapted cone-domain state and keep this stage clearly separated from the + // receptor / adaptation math above. + // Important distinction: MacLeod-Boynton describes chromaticity on the + // equal-luminance plane, while ACC / DKL are opponent combinations of cone + // increments around a background. MB is therefore the right place to carry + // hue / gamut geometry and achromatic Yf bookkeeping; ACC / DKL is the + // better place to describe postreceptoral opponent response and gain. + // 4) Saturating contrast response: + // Use a Michaelis-Menten / Naka-Rushton-like nonlinearity for receptor or + // early cortical contrast response. A supersaturating variant may be needed + // later for some cortical fits. + // Reference: Peirce (JOV 2007, doi:10.1167/7.6.13). + // 5) ON/OFF separation: + // Split increments and decrements with half-wave rectification before the + // pooled gain stage. + // Reference: Schiller (1992). + // 6) Pooled cortical gain: + // Apply divisive normalization, potentially with energy-like pooling across + // achromatic and chromatic channels. + // Reference: Heeger (1992); Carandini & Heeger (2012); Bun & Horwitz + // (2023); Li et al. (2022). + // 7) Device-hull tonemapping / gamut mapping: + // Map the observer-domain result into the display hull in a space that keeps + // enough achromatic and chromatic contrast / JND energy to preserve the + // scene plausibly on the target device. White is one valid attractor when + // bleaching or the achromatic optimum dominates, but it is not the only + // valid out-of-hull destination. + // Inference for RGB display hulls: + // - many device hulls can produce more total achromatic output by combining + // primaries than they can produce at the same brightness with a high-purity + // chromatic excursion, + // - therefore an out-of-hull observer response may need to trade chromatic + // shape / purity toward the achromatic axis if that is what the device + // hull allows, + // - the preferred mapping is not blind clipping to white, but the in-hull + // point that preserves the most plausible observer-domain contrast energy. + // Engineering direction inferred from the sources above: + // - use MB / weighted-LMS to carry achromatic Yf and hue geometry, + // - use ACC / opponent space to judge postreceptoral contrast structure, + // - construct or project to a display hull in that combined space rather + // than clipping in RGB. + // Coupling constraint: + // - hue preservation and device-hull compression are not independent steps, + // - a hue change applied after hull compression can push the result back out + // of the device hull, + // - therefore hue-preserving motion should either be solved inside the hull + // projection itself or followed by another explicit in-hull reprojection. + // Reference direction: MacLeod-Boynton / CIE 170-2 geometry; + // repo weighted-LMS / MB geometry plus the device-hull notes above. + // 7a) Optional hue model inside the device-hull solve: + // If gamut compression or display desaturation bends hue in a way that + // looks wrong, the hull solve can preserve hue by an "equivalent Gaussian + // peak" proxy rather than by preserving a raw opponent angle. The idea is + // that at short and medium wavelengths, perceived hue can behave more like a + // constant spectral peak of an equivalent Gaussian than a constant cone + // ratio when purity changes. + // Practical form: + // - offline, map weighted LMS / MB chromaticities to an equivalent Gaussian + // peak parameter mu_eq using a spectral forward model, + // - online, preserve mu_eq during gamut compression / display mapping while + // carrying Yf separately, + // - do not apply an unconstrained post-hoc hue shift after final hull + // compression unless the result is reprojected back into the device hull. + // This is not a separate chronological eye stage after stage 7. It is an + // optional hue objective used inside the device-hull mapping stage. + // Reference: Mizokami et al. (JOV 2006, doi:10.1167/6.9.12); + // O'Neil et al. (JOSAA 2012, doi:10.1364/JOSAA.29.00A165). + // + // Mermaid source map of planned psychov inputs, derived states, and outputs: + // ```mermaid + // flowchart TB + // classDef raw fill:#223041,stroke:#7aa2d2,color:#e8f0ff + // classDef func fill:#3c2a4d,stroke:#b48ef7,color:#f5ecff + // classDef state fill:#294436,stroke:#77c79a,color:#effff5 + // classDef out fill:#5a3a1f,stroke:#f2bc6b,color:#fff6e8 + // + // subgraph inputs["Raw inputs / assumptions"] + // direction TB + // rgb["Scene-linear R / G / B"]:::raw + // src_color["Source colorimetry\nprimaries + white + RGB->XYZ/LMS"]:::raw + // abs_scale["Absolute scene scale\nscene-linear -> nits"]:::raw + // local_bg["Adaptation drivers\nrolling average + local background"]:::raw + // scene_range["Late image context\nluminance range / percentile span"]:::raw + // retinal["Retinal context\npupil area + trolands"]:::raw + // observer_basis["Observer basis assumptions\nStockman/CVRL LMS + lens/macula"]:::raw + // display["Display/device context\nprimaries + white + peak + black + hull"]:::raw + // end + // + // subgraph pipeline["Observer pipeline"] + // direction TB + // src_to_lms["RGB -> receptor LMS"]:::func + // abs_lms["Absolute cone-energy state\nL, M, S"]:::state + // adapt_fn["Estimate adapted background reference"]:::func + // adapt["Adapted background reference\nL0, M0, S0, optional Yf0"]:::state + // cone_contrast_fn["Form per-cone background-relative signal"]:::func + // cone_contrast["Per-cone contrast state\nDeltaL/(Lb+L0), DeltaM/(Mb+M0), DeltaS/(Sb+S0)"]:::state + // bleach_fn["Apply per-cone bleaching availability"]:::func + // bleach["Bleaching availability state"]:::state + // cone_nl_fn["Apply per-cone Naka-Rushton"]:::func + // cone_nl["Per-cone nonlinear response"]:::state + // noise_fn["Apply dim cone-noise threshold"]:::func + // noise_floor["Visibility-limited cone response"]:::state + // weighted_basis_fn["Optional weighted LMS / Yf / MB transform"]:::func + // weighted_basis["Observer-summary basis state"]:::state + // opponent_fn["Recombine to opponent / achromatic channels"]:::func + // opponent["Opponent / achromatic response"]:::state + // onoff_fn["Split ON / OFF pathways"]:::func + // onoff["ON / OFF responses"]:::state + // late_gain_fn["Apply late polarity / range gain"]:::func + // late_gain["Late polarity / range-gated response"]:::state + // pool_fn["Apply pooling / divisive normalization"]:::func + // pooled["Pooled / normalized response"]:::state + // observer_out["Observer-domain response"]:::state + // end + // + // subgraph device_map["Device-hull mapping"] + // direction TB + // hue_obj_fn["Optional hue objective\nMB / ACC / mu_eq"]:::func + // hue_obj["Hue-preserving objective state"]:::state + // hull_fn["Solve device-hull tone + gamut mapping"]:::func + // hull["Display-hull output"]:::out + // end + // ``` + // + // Current implementation status: + // - This file currently implements the stage-3 to stage-6 scaffold plus a + // first-pass receptor front end: + // absolute Stockman LMS -> explicit adapted background reference input -> + // per-cone bleaching availability -> local receptor Naka-Rushton -> + // cone-contrast coding -> weighted-LMS ACC placeholder. + // - The full planned early-cone model is still incomplete: + // rolling scene / local surround adaptation plumbing and dim-light noise + // thresholding are not wired into test14 yet. + // - The device-hull tonemapping stage is also not wired into test14 yet. + // - The intended receptor basis should be read as an average-observer, mainly + // foveal Stockman / CVRL basis with prereceptoral lens and macular filtering + // already folded into the standard functions, not a personalized observer + // model with explicit lens / macular / eccentricity variation. + // - The adapted background is now an explicit input with a default + // diffuse-gray proxy, but the caller still needs to supply a real rolling + // scene / local surround estimate before this can count as a complete + // adaptation model. + // - The scalar defaults below are unit-normalized test settings and are not + // yet fitted to a specific physiology dataset. + // - The optional Abney / equivalent-Gaussian hue stage is not implemented yet; + // if used later, it should be bounded because the Gaussian account works best + // at short and medium wavelengths and breaks down toward yellow / red. + // + // Secondary seminar notes (useful framing, not primary citation): + // - "Bright lights ... bleach the cones" and make them less sensitive, with + // slow recovery. This supports the placement of bleaching in the observer + // model before device mapping. + // - "Gamut ... is the technical container." This supports treating tonemapping + // as a device-hull problem distinct from the eye model. + // - Diffuse white and peak luminance were discussed separately, which supports + // keeping display white / diffuse white as explicit anchors in the hull solve. + // Source: "Mythbusting: Colour, Camera, Cinema" (Colorist Society, + // Camerimage 2024 panel with Andrew Stockman / Charles Poynton / Dirk Meier), + // YouTube transcript dated January 13, 2025. + // Ordered summary: + // scene RGB + // -> absolute Stockman LMS (chapter 10 receptor basis) + // -> early cone adaptation (chapter 11 first-site background L0/M0/S0/Yf0) + // -> high-light bleaching + // -> opponent drive + // -> saturating response / ON-OFF / pooled gain + // -> observer-domain result + // -> device-hull mapping + + float3 psychov_17( + float3 bt709_linear_input, + float peak_value = 1000.f / 203.f, + float exposure = 1.f, + float highlights = 1.f, + float shadows = 1.f, + float contrast = 1.f, + float purity_scale = 1.f, + float bleaching_intensity = 1.f, + float clip_point = 100.f, + float hue_restore = 1.f, + float adaptation_contrast = 1.f, + int white_curve_mode = 0, + float cone_response_exponent = 1.f, + float3 current_adaptive_state_bt709 = 0.18f, + float3 current_background_state_bt709 = 0.18f, + float gamut_compression = 1.f, + int gamut_compression_mode = 1, + float adaptive_normalization = 1.f) + { + float3 bt709_scene = bt709_linear_input * exposure; + + float3 lms_in = psycho17_BT709ToLMS(bt709_scene); + float3 lms_peak = psycho17_BT709ToLMS(float(peak_value).xxx); + float3 current_adaptive_state_lms = psycho17_BT709ToLMS(current_adaptive_state_bt709); + float3 desired_background_state_lms = psycho17_BT709ToLMS(current_background_state_bt709); + float3 lms_working = lms_in; + if (true) { + // noop + } else if (gamut_compression == 0) { + lms_working = psycho17_GamutCompressLMSBoundAdaptive( + lms_in, + current_adaptive_state_lms, + psycho17_LMS_TO_LMS_WEIGHTED_MAT, + 1.f); + } else if (gamut_compression_mode == 0) { + lms_working = psycho17_GamutCompressLMSBoundAdaptive( + lms_in, + current_adaptive_state_lms, + psycho17_BT709_TO_LMS_WEIGHTED_MAT, + 1.f); + } else { + lms_working = psycho17_GamutCompressLMSBoundAdaptive( + lms_in, + current_adaptive_state_lms, + psycho17_BT2020_TO_LMS_WEIGHTED_MAT, + 1.f); + } + + float yf_input = psycho17_YFFromLMS(lms_working); + float yf_midgray = psycho17_YFFromBT709(0.18f.xxx); + float yf_target = yf_input; + + // Stage 1: apply UI highlight/shadow/contrast controls in luminosity space. + if (highlights != 1.f) { + yf_target = psycho17_Highlights(yf_target, highlights, yf_midgray); + } + if (shadows != 1.f) { + yf_target = psycho17_Shadows(yf_target, shadows, yf_midgray); + } + if (contrast != 1.f) { + yf_target = psycho17_ContrastSafe(yf_target, contrast, yf_midgray); + } + + float yf_scale = psycho17_DivideSafe(yf_target, yf_input, 1.f); + + float3 lms_graded = lms_working * yf_scale; + if (purity_scale != 1.f) { + float3 lms_graded_relative = psycho17_ToAdaptiveRelativeLMS( + lms_graded, + current_adaptive_state_lms); + float3 mb = psycho17_MacleodBoyntonFromLMS(lms_graded_relative); + float2 mb_white = psycho17_MacleodBoyntonFromLMS(1.f.xxx).xy; + float2 mb_scaled = lerp(mb_white, mb.xy, purity_scale); + lms_graded = psycho17_FromAdaptiveRelativeLMS( + psycho17_LMSFromMacleodBoynton(float3(mb_scaled, mb.z)), + current_adaptive_state_lms); + } + + float3 lms_cones = lms_graded; + + if (bleaching_intensity != 0.f) { + float3 stimulus_trolands = max(current_adaptive_state_lms, 0.f) * 203.f * 4.f; + float3 availability = 1.f.xxx / (1.f.xxx + stimulus_trolands / 20000.f); + availability = lerp(1.f.xxx, availability, bleaching_intensity); + + float y = lms_cones.x + lms_cones.y; + float white_y = current_adaptive_state_lms.x + current_adaptive_state_lms.y; + float3 white_at_y = current_adaptive_state_lms * (y / white_y); + float3 delta = (lms_cones - white_at_y) * availability; + lms_cones = white_at_y + delta; + } + + // Naka-Rushton is scale-equivariant if input, peak, and anchors are all + // normalized by the same adaptive LMS state, so keep the absolute-LMS form. + float3 display_scaled = psycho17_NakaRushton( + lms_cones, + lms_peak, + current_adaptive_state_lms, + desired_background_state_lms, + cone_response_exponent); + float3 display_scaled_relative_weighted = psycho17_ToAdaptiveRelativeWeightedLMS( + display_scaled, + current_adaptive_state_lms); + + if (hue_restore > 0.f) { + float3 lms_cones_relative_weighted = psycho17_ToAdaptiveRelativeWeightedLMS( + lms_cones, + current_adaptive_state_lms); + float3 mb_source = + psycho17_MacleodBoyntonFromWeightedLMS(lms_cones_relative_weighted); + float3 mb_display_target = + psycho17_MacleodBoyntonFromWeightedLMS(display_scaled_relative_weighted); + float3 mb_adapted_bg = psycho17_MacleodBoyntonFromLMS(1.f.xxx); + + float2 source_offset = mb_source.xy - mb_adapted_bg.xy; + float2 display_target_offset = mb_display_target.xy - mb_adapted_bg.xy; + float src2 = dot(source_offset, source_offset); + float display_tgt2 = dot(display_target_offset, display_target_offset); + if (src2 > 1e-7 && display_tgt2 > 1e-7) { + float inv_target_radius = rsqrt(display_tgt2); + float target_radius = display_tgt2 * inv_target_radius; + float source_t_clip = psycho17_RayExitTCIE1702(mb_adapted_bg.xy, source_offset); + float display_t_clip = psycho17_RayExitTCIE1702(mb_adapted_bg.xy, display_target_offset); + // Scale hue restoration by purity loss relative to the adapted neutral anchor. + float source_purity_signal = psycho17_HueRelativePuritySignalFromTClip(source_t_clip); + float display_purity_signal = psycho17_HueRelativePuritySignalFromTClip(display_t_clip); + float purity_signal_loss = saturate(display_purity_signal / source_purity_signal); + float hue_sensitivity = psycho17_AdaptiveHueSensitivityFromTClip(display_t_clip); + float restore_weight = hue_sensitivity * hue_restore * purity_signal_loss; + if (restore_weight > 0.f) { + float inv_source_radius = rsqrt(src2); + float2 source_dir = source_offset * inv_source_radius; + float2 display_target_dir = display_target_offset * inv_target_radius; + float2 blended_dir = lerp(display_target_dir, source_dir, restore_weight); + float blended_len2 = dot(blended_dir, blended_dir); + if (blended_len2 > 1e-7) { + blended_dir *= rsqrt(blended_len2); + } else { + blended_dir = display_target_dir; + } + + // Keep display-scaled chroma radius and y_MB; only replace hue direction. + float2 mb_restored_xy = mb_adapted_bg.xy + blended_dir * target_radius; + float3 mb_restored = float3(mb_restored_xy, mb_display_target.z); + display_scaled_relative_weighted = + psycho17_WeightedLMSFromMacleodBoynton(mb_restored); + } + } + } + + if (gamut_compression != 0.f) { + if (gamut_compression_mode == 0) { + display_scaled_relative_weighted = psycho17_GamutCompressAdaptiveRelativeWeightedLMSBound( + display_scaled_relative_weighted, + current_adaptive_state_lms, + psycho17_BT709_TO_LMS_WEIGHTED_MAT, + gamut_compression); + } else if (gamut_compression_mode == 1) { + display_scaled_relative_weighted = psycho17_GamutCompressAdaptiveRelativeWeightedLMSBound( + display_scaled_relative_weighted, + current_adaptive_state_lms, + psycho17_BT2020_TO_LMS_WEIGHTED_MAT, + gamut_compression); + } + } + + // Scale back from first-site adaptation; + return psycho17_LMSToBT709( + psycho17_UnweighLMS( + psycho17_FromAdaptiveRelativeWeightedLMS( + display_scaled_relative_weighted, + current_adaptive_state_lms))); + } + + float3 SDR(float3 bt709_input, float cone_response = 1.0f) + { + return psychov_17( + bt709_input, // BT709 input + 1.f, // SDR peak + 1.f, // Exposure + 1.f, // Highlights + 1.f, // Shadows + 1.f, // Contrast + 1.f, // Purity scale + 1.f, // Bleaching intensity + 100.f, // Clip point + 1.f, // Hue restore + 1.f, // Adaptation contrast + 0, // White curve mode + cone_response, // Cone response exponent + 0.18f, // Current adaptive state BT709 + 0.18f, // Current background state BT709 + 1.f, // Gamut compression + 0); // BT709 gamut compression mode + } + + float3 HDR(float3 bt709_input, float peak = 1000.f / 203.f, float cone_response = 1.0f) + { + return psychov_17( + bt709_input, // BT709 input + peak, // HDR peak relative to SDR white + 1.f, // Exposure + 1.f, // Highlights + 1.f, // Shadows + 1.f, // Contrast + 1.f, // Purity scale + 1.f, // Bleaching intensity + 100.f, // Clip point + 1.f, // Hue restore + 1.f, // Adaptation contrast + 0, // White curve mode + cone_response); // Cone response exponent + } + + } // namespace psycho + } // namespace tonemap +} // namespace renodx + +#endif // RENODX_SHADERS_TONEMAP_PSYCHOV_17_HLSL_ diff --git a/features/Post Processing/Shaders/PostProcessing/ColorGrading/colorgrading.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/ColorGrading/colorgrading.cs.hlsl new file mode 100644 index 0000000000..5ae1281c83 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/ColorGrading/colorgrading.cs.hlsl @@ -0,0 +1,870 @@ +#include "Common/Color.hlsli" +#include "Common/ColorSpaces.hlsli" +#include "Common/Math.hlsli" + +#define LUT_SIZE 64 + +RWTexture2D RWTexOut : register(u0); + +Texture2D TexColor : register(t0); +Texture3D TexLUT : register(t1); + +SamplerState LinearSampler : register(s0); + +#include "PostProcessing/ColorGrading/Include/OpenDRTConfig.hlsli" + +cbuffer ColorCB : register(b1) +{ + float4 asccdl[3]; + float4 liftgammagain[3]; // lift,gamma,gain + float4 inOutGamma; // .z = input gamma, .w = output gamma + float4 oklchSaturation; + float4 oklchColorMixer[7]; + float4 contrast; + float4 pivot; + float4 exposureTemperatureTint; + float4 shadows; + float4 midtones; + float4 highlights; + float4 shadowsHighlightsRange; // shadowBegin, shadowEnd, highlightBegin, highlightEnd + + float4 tonemapParams[2]; + float4 inputToWorking[3]; // sRGB → working color space + float4 workingToTonemap[3]; // working → tonemapper native space + float4 tonemapToOutput[3]; // tonemapper native → output space + + float4 workingToXYZ[3]; // working → CIE XYZ (for white balance) + float4 xyzToWorking[3]; // CIE XYZ → working (for white balance) + + float4 workingWhitePoint; // .xy = native white chromaticity of working space + + float4 shadowsOffset; // SMH color offsets + float4 midtonesOffset; + float4 highlightsOffset; + + // game value + float4 cinematic; // saturation, brightness, contrast + float4 fade; // color + float4 tint; // color + + uint logType; + uint skipLDR; + uint skipLUT; + uint enableTonemap; + uint enableColorSpaceTransform; + uint enableHDR; // HDR display is enabled (auto-set from HDR feature) + float hdrPeakNits; // Maximum display brightness in nits for HDR + float hdrPaperWhiteNits; // Reference white brightness in nits for HDR + + OpenDRTConfig odrtConfig; +}; + +#include "PostProcessing/ColorGrading/Include/OpenDRT.hlsli" +#include "PostProcessing/ColorGrading/Include/GT7ToneMapping.hlsli" +#include "PostProcessing/ColorGrading/Include/RenoDXToneMapping.hlsli" +#include "PostProcessing/common.hlsli" +namespace LogType +{ + static const uint ACEScct = (1 << 0); + static const uint ARRIlogC4 = (1 << 1); + static const uint SonySLog3 = (1 << 2); + static const uint Invert = (1 << 3); +}; + +// https://www.shadertoy.com/view/ss23DD +float3 LiftGammaGain(float3 rgb, float4 lift, float4 gamma, float4 gain) +{ + float4 liftt = 1.0 - pow(max(1.0 - lift, 0.0), log2(gain + 1.0)); + + float4 gammat = gamma.rgba - float4(0.0, 0.0, 0.0, Color::RGBToLuminance(gamma.rgb)); + float4 gammatTemp = 1.0 + 4.0 * abs(gammat); + gammat = lerp(gammatTemp, 1.0 / gammatTemp, step(0.0, gammat)); + + float3 col = rgb; + float luma = Color::RGBToLuminance(col); + + col = pow(max(col, 0.0), gammat.rgb); + col *= pow(abs(gain.rgb), gammat.rgb); + col = max(lerp(2.0 * liftt.rgb, 1.0, col), 0.0); + + luma = pow(abs(luma), gammat.a); + luma *= pow(abs(gain.a), gammat.a); + luma = max(lerp(2.0 * liftt.a, 1.0, luma), 0.0); + + col += luma - Color::RGBToLuminance(col); + + return col; +} + +// Combined Oklch adjustments: global saturation/vibrance/hue + per-hue colour mixer +// Single Oklab round-trip for efficiency +float3 OklchAdjustments(float3 val) +{ + float3 oklab = RgbToOklab(val); + + float l = oklab.x; + float c = length(oklab.yz); + float h = atan2(oklab.z, oklab.y); + + // === Global adjustments === + + // Saturation + c *= oklchSaturation.x; + + // Vibrance: perceptual boost for low-saturation colors, preserves already-saturated ones + float vibranceFactor = 1.0 + (oklchSaturation.y - 1.0) * (1.0 - smoothstep(0.0, 0.5, c)); + c *= max(0, vibranceFactor); + + // Global hue shift + h += oklchSaturation.z * Math::PI; + + // === Per-hue colour mixer (mimicking Lightroom) === + + static const float redHue = 0.08120523664; // 0xff0000 + + float lerpFactor = (h / (2 * Math::PI) - redHue) * 7; + int leftHue = floor(lerpFactor); + lerpFactor = lerpFactor - leftHue; + leftHue += (leftHue < 0) * 7; + int rightHue = (leftHue + 1) % 7; + float effect = saturate(c / 0.37); + + // Per-hue hue shift + h += lerp(oklchColorMixer[leftHue].x, oklchColorMixer[rightHue].x, lerpFactor) * Math::PI / 4; + + // Per-hue vibrance + float hueVibrance = lerp(oklchColorMixer[leftHue].y, oklchColorMixer[rightHue].y, lerpFactor); + float hueVibranceFactor = 1.0 + (hueVibrance - 1.0) * (1.0 - smoothstep(0.0, 0.5, c)); + c *= max(0, hueVibranceFactor); + + // Per-hue brightness + l += lerp(oklchColorMixer[leftHue].z, oklchColorMixer[rightHue].z, lerpFactor) * effect; + + // === Reconstruct === + + oklab.x = l; + sincos(h, oklab.z, oklab.y); + oklab.yz *= c; + + return OklabToRgb(oklab); +} + +float3 ShadowsMidtonesHighlights(float3 color, float3 shadowsGain, float3 midtonesGain, float3 highlightsGain, + float3 shadowsOff, float3 midtonesOff, float3 highlightsOff, + float shadowBegin, float shadowEnd, float highlightBegin, float highlightEnd) +{ + float luma = Color::RGBToLuminance(color); + + float shadowWeight = 1.0 - smoothstep(shadowBegin, shadowEnd, luma); + float highlightWeight = smoothstep(highlightBegin, highlightEnd, luma); + float midtoneWeight = 1.0 - shadowWeight - highlightWeight; + + // Per-zone gain + offset (industry standard: allows both color scaling and color shift) + float3 gain = shadowsGain * shadowWeight + midtonesGain * midtoneWeight + highlightsGain * highlightWeight; + float3 offset = shadowsOff * shadowWeight + midtonesOff * midtoneWeight + highlightsOff * highlightWeight; + + return color * gain + offset; +} + +float2 IlluminantChromaticity(float temp) +{ + temp *= 1.4388 / 1.438; + float x = temp <= 7000 ? 0.244063 + (0.09911e3 + (2.9678e6 - 4.6070e9 / temp) / temp) / temp : 0.237040 + (0.24748e3 + (1.9018e6 - 2.0064e9 / temp) / temp) / temp; + float y = -3 * x * x + 2.87 * x - 0.275; + return float2(x, y); +} + +// Accurate for 1000K < temp < 15000K +// [Krystek 1985, "An algorithm to calculate correlated colour temperature"] +float2 PlanckianLocusChromaticity(float temp) +{ + float u = (0.860117757f + 1.54118254e-4f * temp + 1.28641212e-7f * temp * temp) / (1.0f + 8.42420235e-4f * temp + 7.08145163e-7f * temp * temp); + float v = (0.317398726f + 4.22806245e-5f * temp + 4.20481691e-8f * temp * temp) / (1.0f - 2.89741816e-5f * temp + 1.61456053e-7f * temp * temp); + float x = 3 * u / (2 * u - 8 * v + 4); + float y = 2 * v / (2 * u - 8 * v + 4); + return float2(x, y); +} + +// [McCamy 1992, "Correlated color temperature as an explicit function of chromaticity coordinates"] +float2 PlanckianIsothermal(float temp, float tint) +{ + float u = (0.860117757f + 1.54118254e-4f * temp + 1.28641212e-7f * temp * temp) / (1.0f + 8.42420235e-4f * temp + 7.08145163e-7f * temp * temp); + float v = (0.317398726f + 4.22806245e-5f * temp + 4.20481691e-8f * temp * temp) / (1.0f - 2.89741816e-5f * temp + 1.61456053e-7f * temp * temp); + float ud = (-1.13758118e9f - 1.91615621e6f * temp - 1.53177f * temp * temp) / pow(1.41213984e6f + 1189.62f * temp + temp * temp, 2); + float vd = (1.97471536e9f - 705674.0f * temp - 308.607f * temp * temp) / pow(6.19363586e6f - 179.456f * temp + temp * temp, 2); + float2 uvd = normalize(float2(u, v)); + u += -uvd.y * tint * 0.05; + v += uvd.x * tint * 0.05; + float x = 3 * u / (2 * u - 8 * v + 4); + float y = 2 * v / (2 * u - 8 * v + 4); + return float2(x, y); +} + +float3 WhiteBalance(float3 linearColor) +{ + float temp = exposureTemperatureTint.y * 100; + float tint = exposureTemperatureTint.z; + float2 srcWhiteDaylight = IlluminantChromaticity(temp); + float2 srcWhitePlankian = PlanckianLocusChromaticity(temp); + + float2 srcWhite = temp < 4000 ? srcWhitePlankian : srcWhiteDaylight; + + float2 isothermal = PlanckianIsothermal(temp, tint) - srcWhitePlankian; + srcWhite += isothermal; + + // Adapt to working space native white (D65 for sRGB, D60 for ACEScg, etc.) + float2 dstWhite = workingWhitePoint.xy; + + // Skip if source and destination are approximately equal + if (all(abs(srcWhite - dstWhite) < 0.001)) + return linearColor; + + float3x3 whiteBalance = ChromaticAdaptation(srcWhite, dstWhite); + const float3x3 xyzToWorkingMat = float3x3(xyzToWorking[0].xyz, xyzToWorking[1].xyz, xyzToWorking[2].xyz); + const float3x3 workingToXYZMat = float3x3(workingToXYZ[0].xyz, workingToXYZ[1].xyz, workingToXYZ[2].xyz); + whiteBalance = mul(xyzToWorkingMat, mul(whiteBalance, workingToXYZMat)); + + return mul(whiteBalance, linearColor); +} + +float3 LogToLinear(float3 logColor) +{ + const float linearRange = 14.0f; + const float linearGrey = 0.18f; + const float exposureGrey = 444.0f; + return exp2((logColor - exposureGrey / 1023.0) * linearRange) * linearGrey; +} + +float3 LinearToLog(float3 linearColor) +{ + const float linearRange = 14.0f; + const float linearGrey = 0.18f; + const float exposureGrey = 444.0f; + return saturate(log2(linearColor) / linearRange - log2(linearGrey) / linearRange + exposureGrey / 1023.0f); +} + +float HDRPaperWhiteNits() +{ + return max(hdrPaperWhiteNits, 1.0f); +} + +float HDRPeakNits() +{ + return max(hdrPeakNits, HDRPaperWhiteNits()); +} + +float HDRPeakRatio() +{ + return HDRPeakNits() / HDRPaperWhiteNits(); +} + +float HDRPeakForReferenceWhite(float referenceWhiteNits) +{ + return HDRPeakRatio() * referenceWhiteNits; +} + +///////////////////////////////////////////////////////////////////////////////// + +/* + tizian/tonemapper + url: https://github.com/tizian/tonemapper + license: + The MIT License (MIT) + + Copyright (c) 2022 Tizian Zeltner + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +float3 Reinhard(float3 val) +{ + val *= tonemapParams[0].x; + float luma = Color::RGBToLuminance(val); + float lumaOut = luma / (1 + luma); + val = val / (luma + 1e-10) * lumaOut; + val = saturate(val); + return val; +} + +float3 ReinhardExt(float3 val) +{ + val *= tonemapParams[0].x; + float luma = Color::RGBToLuminance(val); + float lumaOut = luma * (1 + luma / (tonemapParams[0].y * tonemapParams[0].y)) / (1 + luma); + val = val / (luma + 1e-10) * lumaOut; + val = saturate(val); + return val; +} + +float3 HejlBurgessDawsonFilmic(float3 val) +{ + val *= tonemapParams[0].x; + val = max(0, val - 0.004); + val = (val * (6.2 * val + .5)) / (val * (6.2 * val + 1.7) + 0.06); + val = pow(saturate(val), 2.2); + return val; +} + +float3 AldridgeFilmic(float3 val) +{ + val *= tonemapParams[0].x; + float tmp = 2.0 * tonemapParams[0].y; + val = val + (tmp - val) * clamp(tmp - val, 0.0, 1.0) * (0.25 / tonemapParams[0].y) - tonemapParams[0].y; + val = (val * (6.2 * val + 0.5)) / (val * (6.2 * val + 1.7) + 0.06); + val = pow(saturate(val), 2.2); + return val; +} + +float3 LottesFilmic(float3 val) +{ + val *= tonemapParams[0].x; + float a = tonemapParams[0].y, + d = tonemapParams[0].z; + float maxHDR = tonemapParams[0].w; + float midIn = tonemapParams[1].x; + float midOut = tonemapParams[1].y; + + // In HDR mode, re-derive curve constants so f(maxHDR) = peakOutput + // while keeping f(midIn) = midOut (SDR midtones unchanged) + float peakOutput = enableHDR ? HDRPeakRatio() : 1.0; + + float b = (pow(maxHDR, a) * midOut - pow(midIn, a) * peakOutput) / + ((pow(maxHDR, a * d) - pow(midIn, a * d)) * midOut * peakOutput), + c = (pow(maxHDR, a * d) * pow(midIn, a) * peakOutput - pow(maxHDR, a) * pow(midIn, a * d) * midOut) / + ((pow(maxHDR, a * d) - pow(midIn, a * d)) * midOut * peakOutput); + + val = pow(val, a) / (pow(val, a * d) * b + c); + val = enableHDR ? clamp(val, 0.0, peakOutput) : saturate(val); + return val; +} + +float DayCurve(float x, float k) +{ + const float b = tonemapParams[0].y; + const float w = tonemapParams[0].z; + const float c = tonemapParams[0].w; + const float s = tonemapParams[1].x; + const float t = tonemapParams[1].y; + + if (x < c) { + return k * (1.0 - t) * (x - b) / (c - (1.0 - t) * b - t * x); + } else { + return (1.0 - k) * (x - c) / (s * x + (1.0 - s) * w - c) + k; + } +} + +float3 DayFilmic(float3 val) +{ + const float b = tonemapParams[0].y; + const float w = tonemapParams[0].z; + const float c = tonemapParams[0].w; + const float s = tonemapParams[1].x; + const float t = tonemapParams[1].y; + + val *= tonemapParams[0].x; + float k = (1.0 - t) * (c - b) / ((1.0 - s) * (w - c) + (1.0 - t) * (c - b)); + val = float3(DayCurve(val.r, k), DayCurve(val.g, k), DayCurve(val.b, k)); + + val = saturate(val); + return val; +} + +float3 UchimuraFilmic(float3 val) +{ + float P = tonemapParams[0].y; + const float a = tonemapParams[0].z; + const float m = tonemapParams[0].w; + const float l = tonemapParams[1].x; + const float c = tonemapParams[1].y; + const float b = tonemapParams[1].z; + + val *= tonemapParams[0].x; + + // In HDR mode, extend the peak brightness proportionally to the display's capability + if (enableHDR) + P *= HDRPeakRatio(); + + float l0 = ((P - m) * l) / a, + S0 = m + l0, + S1 = m + a * l0, + C2 = (a * P) / (P - S1), + CP = -C2 / P; + + float3 w0 = 1.0 - smoothstep(0.0, m, val), + w2 = step(m + l0, val), + w1 = 1.0 - w0 - w2; + + float3 T = m * pow(val / m, c) + b, // toe + L = m + a * (val - m), // linear + S = P - (P - S1) * exp(CP * (val - S0)); // shoulder + + val = T * w0 + L * w1 + S * w2; + + val = enableHDR ? clamp(val, 0.0, P) : saturate(val); + return val; +} + +/* AgX Reference: + * AgX by longbool https://www.shadertoy.com/view/dtSGD1 + * AgX Minimal by bwrensch https://www.shadertoy.com/view/cd3XWr + * Fork AgX Minima troy_s 342 by troy_s https://www.shadertoy.com/view/mdcSDH + */ + +// Mean error^2: 3.6705141e-06 +float3 AgxDefaultContrastApprox5(float3 x) +{ + float3 x2 = x * x; + float3 x4 = x2 * x2; + + return +15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + + 0.4298 * x2 + 0.1191 * x - 0.00232; +} + +// Mean error^2: 1.85907662e-06 +float3 AgxDefaultContrastApprox6(float3 x) +{ + float3 x2 = x * x; + float3 x4 = x2 * x2; + + return -17.86 * x4 * x2 * x + 78.01 * x4 * x2 - 126.7 * x4 * x + 92.06 * x4 - + 28.72 * x2 * x + 4.361 * x2 - 0.1718 * x + 0.002857; +} + +float3 Agx(float3 val) +{ + const float3x3 agx_mat = transpose( + float3x3(0.842479062253094, 0.0423282422610123, 0.0423756549057051, + 0.0784335999999992, 0.878468636469772, 0.0784336, + 0.0792237451477643, 0.0791661274605434, 0.879142973793104)); + + const float min_ev = -12.47393f; + const float max_ev = 4.026069f; + + // Input transform + val = mul(agx_mat, val); + + // Log2 space encoding + val = clamp(log2(val), min_ev, max_ev); + val = (val - min_ev) / (max_ev - min_ev); + + // Apply sigmoid function approximation + val = AgxDefaultContrastApprox6(val); + + return val; +} + +float3 AgxEotf(float3 val) +{ + const float3x3 agx_mat_inv = transpose( + float3x3(1.19687900512017, -0.0528968517574562, -0.0529716355144438, + -0.0980208811401368, 1.15190312990417, -0.0980434501171241, + -0.0990297440797205, -0.0989611768448433, 1.15107367264116)); + + // Undo input transform + val = mul(agx_mat_inv, val); + + // sRGB IEC 61966-2-1 2.2 Exponent Reference EOTF Display + // NOTE: We're linearizing the output here. Comment/adjust when + // *not* using a sRGB render target + val = pow(saturate(val), 2.2); + + return val; +} + +float3 AgxMinimal(float3 val) +{ + val *= tonemapParams[0].x; + + val = Agx(val); + val = ASC_CDL(val, tonemapParams[0].y, tonemapParams[0].z, tonemapParams[0].w); + val = Saturation(val, tonemapParams[1].x); + val = AgxEotf(val); + + return val; +} + +// src: https://github.com/ltmx/Melon-Tonemapper +// GPL-3.0 license +float3 MelonHueShift(float3 In) +{ + float A = max(In.x, In.y); + return float3(A, max(A, In.z), In.z); +} + +float3 MelonTonemap(float3 color) +{ + color *= tonemapParams[0].r; + + // remaps the colors to [0-1] range + // tested to be as close ti ACES contrast levels as possible + color = pow(color, float3(1.56, 1.56, 1.56)); + color = color / (color + 0.84); + + // governs the transition to white for high color intensities + float factor = max(color.r, max(color.g, color.b)) * 0.15; // multiply by 0.15 to get a similar look to ACES + factor = factor / (factor + 1); // remaps the factor to [0-1] range + factor *= factor; // smooths the transition to white + + // shift the hue for high intensities (for a more pleasing look). + color = lerp(color, MelonHueShift(color), factor); // can be removed for more neutral colors + color = lerp(color, float3(1.0, 1.0, 1.0), factor); // shift to white for high intensities + + // clamp to [0-1] range + return clamp(color, float3(0.0, 0.0, 0.0), float3(1.0, 1.0, 1.0)); +} + +/* + EmbarkStudios/kajiya + url: https://github.com/EmbarkStudios/kajiya + license: + Copyright (c) 2019 Embark Studios + + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the + Software without restriction, including without + limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software + is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice + shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF + ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT + SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR + IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +float KajiyaCurve(float v) +{ + return 1.0 - exp(-v); +} + +float3 KajiyaCurve(float3 v) +{ + return 1.0 - exp(-v); +} + +float3 KajiyaTonemap(float3 col) +{ + col *= tonemapParams[0].r; + + float3 ycbcr = RgbToYCbCr(col); + + float bt = KajiyaCurve(length(ycbcr.yz) * 2.4); + float desat = max((bt - 0.7) * 0.8, 0); + desat = desat * desat; + + float3 desat_col = lerp(col, ycbcr.x, desat); + + float tm_luma = KajiyaCurve(ycbcr.x); + float3 tm0 = col * max(tm_luma / max(Color::RGBToLuminance(col), 1e-5), 0); + float final_mult = 0.97; + float3 tm1 = KajiyaCurve(desat_col); + + return lerp(tm0, tm1, bt * bt) * final_mult; +} + +float3 GT7ToneMapping(float3 color) +{ + color *= tonemapParams[0].x; + // Use HDR Display paper white and peak brightness for seamless HDR output. + if (enableHDR) + color = GT7ToneMappingHDR(color, HDRPeakForReferenceWhite(REFERENCE_LUMINANCE)); + else + color = GT7ToneMappingSDR(color); + return color; +} + +float3 PsychoVTonemap(float3 color) +{ + color *= tonemapParams[0].x; + + // PsychoV expects scene-linear BT.709 input. The surrounding Color Grading + // pipeline converts into this native space before invoking the tonemapper. + if (enableHDR) + return renodx::tonemap::psycho::HDR(color, HDRPeakRatio()); + else + return renodx::tonemap::psycho::SDR(color); +} + +float3 NeutwoTonemap(float3 color) +{ + color *= tonemapParams[0].x; + + float peak = enableHDR ? HDRPeakRatio() : 1.0f; + float clipPoint = max(tonemapParams[0].y, peak); + if (enableHDR) + return ColorGradingRenoDX::NeutwoBT2020(color, peak, clipPoint); + else + return renodx::tonemap::neutwo::BT709(color, peak, clipPoint); +} + +float3 ACESTonemap(float3 color) +{ + color = max(0.0f, color * tonemapParams[0].x); + + float minNits = max(tonemapParams[0].y, 0.0001f); + float peakNits = enableHDR ? HDRPeakNits() : REFERENCE_LUMINANCE; + float diffuseWhiteNits = enableHDR ? HDRPaperWhiteNits() : REFERENCE_LUMINANCE; + + if (enableHDR) + return ColorGradingRenoDX::ACESBT2020(color, minNits, peakNits, diffuseWhiteNits, ColorGradingRenoDX::ACES_DEFAULT_MID_GRAY_VALUE); + else + return ColorGradingRenoDX::ACESSDRBT709(color, minNits); +} + +float3 FrostbiteTonemap(float3 color) +{ + color = max(0.0f, color * tonemapParams[0].x); + if (max(color.r, max(color.g, color.b)) <= 0.0f) + return 0.0f; + + float maxValue = enableHDR ? HDRPeakRatio() : 1.0f; + float rolloffStart = min(tonemapParams[0].y, maxValue); + if (enableHDR) + return ColorGradingRenoDX::FrostbiteBT2020(color, maxValue, rolloffStart, tonemapParams[0].z, tonemapParams[0].w); + else + return renodx::tonemap::frostbite::BT709(color, maxValue, rolloffStart, tonemapParams[0].z, tonemapParams[0].w); +} + +float3 HermiteSplineTonemap(float3 color) +{ + color = max(0.0f, color * tonemapParams[0].x); + + float peak = enableHDR ? HDRPeakRatio() : 1.0f; + float whiteClip = max(tonemapParams[0].y, peak); + if (enableHDR) + return ColorGradingRenoDX::HermiteSplineBT2020(color, peak, whiteClip); + else + return renodx::tonemap::HermiteSplineLuminanceRolloff(color, peak, whiteClip); +} + +//////////////////////////////////////////////////////////////////////// + +// Linear to Log +float3 ACEScct(float3 linearColor, bool inverse) +{ + const float a = 10.5402377416545; + const float b = 0.0729055341958355; + const float cutoff = 0.0078125; + const float cutoff2 = 0.155251141552511; + + float3 cct = linearColor; + + if (!inverse) { + cct = float3( + linearColor.x > cutoff ? (log2(linearColor.x) + 9.72) / 17.52 : linearColor.x * a + b, + linearColor.y > cutoff ? (log2(linearColor.y) + 9.72) / 17.52 : linearColor.y * a + b, + linearColor.z > cutoff ? (log2(linearColor.z) + 9.72) / 17.52 : linearColor.z * a + b); + } else { + cct = float3( + linearColor.x >= cutoff2 ? pow(2, linearColor.x * 17.52 - 9.72) : (linearColor.x - b) / a, + linearColor.y >= cutoff2 ? pow(2, linearColor.y * 17.52 - 9.72) : (linearColor.y - b) / a, + linearColor.z >= cutoff2 ? pow(2, linearColor.z * 17.52 - 9.72) : (linearColor.z - b) / a); + } + + return cct; +} + +float3 ARRIlogC4(float3 linearColor, bool inverse) +{ + const float a = 2231.8263; + const float b = 0.9071359; + const float c = 0.0928641; + const float s = 0.6816768; + const float t = -0.0180570; + + float3 logColor = linearColor; + + if (!inverse) { + logColor = float3( + (linearColor.x > t ? (log2(linearColor.x * a + 64) - 6) / 14 * b + c : (linearColor.x - t) / s), + (linearColor.y > t ? (log2(linearColor.y * a + 64) - 6) / 14 * b + c : (linearColor.y - t) / s), + (linearColor.z > t ? (log2(linearColor.z * a + 64) - 6) / 14 * b + c : (linearColor.z - t) / s)); + } else { + logColor = float3( + (linearColor.x >= 0 ? (pow(2, 14 * (linearColor.x - c) / b + 6) - 64) / a : linearColor.x * s + t), + (linearColor.y >= 0 ? (pow(2, 14 * (linearColor.y - c) / b + 6) - 64) / a : linearColor.y * s + t), + (linearColor.z >= 0 ? (pow(2, 14 * (linearColor.z - c) / b + 6) - 64) / a : linearColor.z * s + t)); + } + + return logColor; +} + +float3 SonySLog3(float3 linearColor, bool inverse) +{ + float3 logColor = linearColor; + + if (!inverse) { + logColor = float3( + (linearColor.x >= 0.0112500 ? (420.0 + log10((linearColor.x + 0.01) / 0.19) * 261.5) / 1023.0 : (linearColor.x * (171.2102946929 - 95.0) / 0.0112500 + 95.0) / 1023.0), + (linearColor.y >= 0.0112500 ? (420.0 + log10((linearColor.y + 0.01) / 0.19) * 261.5) / 1023.0 : (linearColor.y * (171.2102946929 - 95.0) / 0.0112500 + 95.0) / 1023.0), + (linearColor.z >= 0.0112500 ? (420.0 + log10((linearColor.z + 0.01) / 0.19) * 261.5) / 1023.0 : (linearColor.z * (171.2102946929 - 95.0) / 0.0112500 + 95.0) / 1023.0)); + } else { + logColor = float3( + (linearColor.x > 0.1712102946929 / 1023.0 ? pow(10, (linearColor.x * 1023.0 - 420.0) / 261.5) * 0.19 - 0.01 : (linearColor.x * 1023.0 - 95.0) * 0.0112500 / (171.2102946929 - 95.0)), + (linearColor.y > 0.1712102946929 / 1023.0 ? pow(10, (linearColor.y * 1023.0 - 420.0) / 261.5) * 0.19 - 0.01 : (linearColor.y * 1023.0 - 95.0) * 0.0112500 / (171.2102946929 - 95.0)), + (linearColor.z > 0.1712102946929 / 1023.0 ? pow(10, (linearColor.z * 1023.0 - 420.0) / 261.5) * 0.19 - 0.01 : (linearColor.z * 1023.0 - 95.0) * 0.0112500 / (171.2102946929 - 95.0))); + } + + return logColor; +} + +float3 LinearToLogSpace(float3 val, uint logType) +{ + float3 logColor = val; + if (logType & LogType::ACEScct) + logColor = ACEScct(val, false); + else if (logType & LogType::ARRIlogC4) + logColor = ARRIlogC4(val, false); + else if (logType & LogType::SonySLog3) + logColor = SonySLog3(val, false); + return logColor; +} + +float3 LogToLinearSpace(float3 val, uint logType) +{ + float3 linearColor = val; + if (logType & LogType::ACEScct) + linearColor = ACEScct(val, true); + else if (logType & LogType::ARRIlogC4) + linearColor = ARRIlogC4(val, true); + else if (logType & LogType::SonySLog3) + linearColor = SonySLog3(val, true); + return linearColor; +} + +//////////////////////////////////////////////////////////////////////// + +float3 ColorGrading(float3 color) +{ + // Stage 1: Input (sRGB) → Working color space + if (enableColorSpaceTransform) { + const float3x3 inputToWorkingMat = float3x3(inputToWorking[0].xyz, inputToWorking[1].xyz, inputToWorking[2].xyz); + color = mul(inputToWorkingMat, color); + } + + // HDR color grading (in working space, linear) + // Exposure/White Balance + color *= exposureTemperatureTint.x; + color = WhiteBalance(color); + + // Oklch adjustments (perceptual, HDR-safe: saturation, vibrance, hue shift, per-hue mixer) + color = OklchAdjustments(color); + + // Log + if (logType) + color = LinearToLogSpace(color, logType); // Preferably ACEScct + + // ASC CDL + color = ASC_CDL(color, asccdl[0].xyz, asccdl[1].xyz, asccdl[2].xyz); + + // Shadows Midtones Highlights (gain + offset per zone) + color = ShadowsMidtonesHighlights(color, shadows.xyz, midtones.xyz, highlights.xyz, + shadowsOffset.xyz, midtonesOffset.xyz, highlightsOffset.xyz, + shadowsHighlightsRange.x, shadowsHighlightsRange.y, shadowsHighlightsRange.z, shadowsHighlightsRange.w); + + // Contrast + color = logType ? LogContrast(color, contrast.xyz, pivot.xyz) : LinearContrast(color, contrast.xyz, pivot.xyz); + + if (logType & LogType::Invert) { + color = LogToLinearSpace(color, logType); + } + + // Stage 2: Working → Tonemapper native space + if (enableColorSpaceTransform) { + const float3x3 workingToTonemapMat = float3x3(workingToTonemap[0].xyz, workingToTonemap[1].xyz, workingToTonemap[2].xyz); + color = mul(workingToTonemapMat, color); + } + + // Tonemap (in tonemapper's native space) + if (enableTonemap) { + color = TONEMAP_FUNC(color); + } + + // Stage 3: Tonemapper native → Output space + if (enableColorSpaceTransform) { + const float3x3 tonemapToOutputMat = float3x3(tonemapToOutput[0].xyz, tonemapToOutput[1].xyz, tonemapToOutput[2].xyz); + color = mul(tonemapToOutputMat, color); + } + + // LDR post-tonemap adjustments (in output space) + // Skip when HDR is enabled, as LDR color grading is designed for SDR output + if (!skipLDR && !enableHDR) { + // Lift Gamma Gain + color = LiftGammaGain(color, liftgammagain[0].gbar, liftgammagain[1].gbar, liftgammagain[2].gbar); + } + + float3 luminanceVectorOutSpace = enableHDR ? float3(0.2627, 0.6780, 0.0593) : float3(0.2126, 0.7152, 0.0722); + color = CorrectOutOfRangeColor(color, luminanceVectorOutSpace); + + return color; +} + +float3 ApplyLUT(float3 color) +{ + color = LinearToLog(color + LogToLinear(0)); + color = color * ((LUT_SIZE - 1.0) / LUT_SIZE) + (0.5 / LUT_SIZE); + color = TexLUT.SampleLevel(LinearSampler, color, 0).xyz; + return color; +} + +[numthreads(8, 8, 1)] void CSColorGrading(uint2 DTid : SV_DispatchThreadID) { + // Game cinematic + float3 color = pow(abs(TexColor[DTid].xyz), inOutGamma.z) * cinematic.y; + color = Saturation(color, cinematic.x); + color = LinearContrast(color, cinematic.z, 0.18); + + // Apply LUT or direct Color Grading + if (skipLUT) + color = ColorGrading(color); + else + color = ApplyLUT(color); + + color = pow(abs(color), inOutGamma.w); + + // Game tint + float luma = Color::RGBToLuminance(color); + color = lerp(color, luma * tint.xyz, tint.w); + + // Game fade + color = lerp(color, fade.xyz, fade.w); + + RWTexOut[DTid] = float4(color, 1); +} + +RWTexture3D RWLUT : register(u0); + +[numthreads(8, 8, 8)] void CSLUTGen(uint3 DTid : SV_DispatchThreadID) { + float3 uvw = float3(DTid) / float3((LUT_SIZE - 1).xxx); + float4 neutralColor = float4(uvw, 1); + float3 linearColor = LogToLinear(neutralColor.xyz) - LogToLinear(0); + linearColor = ColorGrading(linearColor); + RWLUT[DTid] = float4(linearColor, 1); +} diff --git a/features/Post Processing/Shaders/PostProcessing/Composite/composite.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/Composite/composite.cs.hlsl new file mode 100644 index 0000000000..8d103d9ff8 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/Composite/composite.cs.hlsl @@ -0,0 +1,173 @@ +/// Bloom/Flare/Glare/Exposure Composite pass +/// Combines bloom, lens flare, physical glare, and auto exposure results with the main color texture. +/// Formula: SceneColor * GlobalExposure * LocalExposure + Bloom * GlobalExposure +/// Scene and Bloom paths are kept separate so LocalExposure is applied on scene only. +/// Purkinje effect is applied after compositing on the final perceived image. +/// Uses #ifdef HAS_BLOOM / HAS_LENS_FLARE / HAS_GLARE / HAS_EXPOSURE / HAS_LOCAL_EXPOSURE to control behavior. + +Texture2D TexColor : register(t0); + +#ifdef HAS_BLOOM +Texture2D TexBloom : register(t1); +#endif + +#ifdef HAS_LENS_FLARE +Texture2D TexFlare : register(t2); +#endif + +#ifdef HAS_GLARE +Texture2D TexGlare : register(t3); +#endif + +#ifdef HAS_EXPOSURE +StructuredBuffer TexAdaptation : register(t4); +#endif + +#ifdef HAS_LOCAL_EXPOSURE +Texture2D TexLocalExposure : register(t5); +#endif + +#ifdef HAS_EXPOSURE +cbuffer AutoExposureCB : register(b1) +{ + float2 AdaptArea; + float2 AdaptationRange; + float AdaptLerp; + float ExposureCompensation; + float PurkinjeStartEV; + float PurkinjeMaxEV; + float PurkinjeStrength; + + float pad[3]; +}; + +// ==================== Purkinje Effect ==================== +// https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2630540/pdf/nihms80286.pdf +// Simulates the blue shift of human vision under low light conditions. +// Applied AFTER compositing, on the final perceived (exposed) image. + +static const float4x3 RGB2LMSR_MATRIX = float4x3( + 0.31670331, 0.70299344, 0.08120592, + 0.10129085, 0.72118661, 0.12041039, + 0.01451538, 0.05643031, 0.53416779, + 0.01724063, 0.60147464, 0.40056206) * + 24.303; + +static const float3x3 LMS2RGB_MATRIX = float3x3( + 4.57829597, -4.48749114, 0.31554848, + -0.63342362, 2.03236026, -0.36183302, + -0.05749394, -0.09275939, 1.90172089) / + 24.303; + +float4 RGB2LMSR(float3 c) +{ + return mul(RGB2LMSR_MATRIX, c); +} + +float3 LMS2RGB(float3 c) +{ + return mul(LMS2RGB_MATRIX, c); +} + +static const float3 m = float3(0.63721, 0.39242, 1.6064); +static const float K = 45.0; +static const float S = 10.0; +static const float k3 = 0.6; +static const float k5 = 0.2; +static const float k6 = 0.29; +static const float rw = 0.139; +static const float p = 0.6189; +static const float logExposure = 380.0f; +static const float K_S = K / S; + +float3 PurkinjeShift(float3 c, float nightAdaptation) +{ + if (nightAdaptation < 1e-5) + return c; + + float4 lmsr = RGB2LMSR(c * logExposure); + + float3 lmsr_w_terms = float3(k5, k5, k6) * lmsr.w; + float3 denominator = 1 + (.33 / m) * (lmsr.xyz + lmsr_w_terms); + float3 g = rsqrt(denominator); + + float g_x_over_m_x = g.x / m.x; + float g_y_over_m_y = g.y / m.y; + float g_z_over_m_z = g.z / m.z; + float k5_lmsr_w = k5 * lmsr.w; + + float rc_gr = K_S * ((1.0 + rw * k3) * g_y_over_m_y - (k3 + rw) * g_x_over_m_x) * k5_lmsr_w; + float rc_by = K_S * (k6 * g_z_over_m_z - k3 * (p * k5 * g_x_over_m_x + (1.0 - p) * k5 * g_y_over_m_y)) * lmsr.w; + float rc_lm = K * (p * g_x_over_m_x + (1.0 - p) * g_y_over_m_y) * k5_lmsr_w; + + float half_rc_gr = 0.5 * rc_gr; + float3 lms_gain = float3(-half_rc_gr + 0.5 * rc_lm, half_rc_gr + 0.5 * rc_lm, rc_by + rc_lm) * nightAdaptation; + + return LMS2RGB(lmsr.rgb + lms_gain) / logExposure; +} +#endif // HAS_EXPOSURE + +RWTexture2D RWTexOutput : register(u0); + +[numthreads(8, 8, 1)] void CSComposite(uint2 tid : SV_DispatchThreadID) { + uint2 dims; + RWTexOutput.GetDimensions(dims.x, dims.y); + + if (any(tid >= dims)) + return; + + float3 sceneColor = TexColor[tid].rgb; + + // Accumulate bloom/flare/glare contributions (separate from scene) + float3 bloomContrib = 0; + +#ifdef HAS_BLOOM + bloomContrib += TexBloom[tid].rgb; +#endif + +#ifdef HAS_LENS_FLARE + bloomContrib += TexFlare[tid].rgb; +#endif + +#ifdef HAS_GLARE + bloomContrib += TexGlare[tid].rgb; +#endif + +#ifdef HAS_EXPOSURE + // Compute global exposure value + float avgLuma = TexAdaptation[0]; + float globalExposure = 0.18 * ExposureCompensation / clamp(avgLuma, AdaptationRange.x, AdaptationRange.y); + + // Formula: SceneColor * GlobalExposure * LocalExposure + Bloom * GlobalExposure + // LocalExposure multiplier from the Local Exposure pass (1.0 if not enabled) +# ifdef HAS_LOCAL_EXPOSURE + float localExposure = TexLocalExposure[tid]; +# else + float localExposure = 1.0; +# endif + + float3 result = sceneColor * globalExposure * localExposure + bloomContrib * globalExposure; + + // Purkinje effect: applied on the final perceived (exposed + composited) image + if (PurkinjeStrength > 1e-3) { + float avgEV100 = log2(avgLuma) + 3.0; + float mix_term = (avgEV100 - PurkinjeMaxEV) / (PurkinjeStartEV - PurkinjeMaxEV); + float purkinjeMix = lerp(PurkinjeStrength, 0.0, saturate(mix_term)); + + if (purkinjeMix > 1e-3) + result = PurkinjeShift(result, purkinjeMix); + } +#else + // No global exposure +# ifdef HAS_LOCAL_EXPOSURE + // Apply local exposure without global exposure + float localExposure = TexLocalExposure[tid]; + float3 result = sceneColor * localExposure + bloomContrib; +# else + // No exposure at all: simple additive composite + float3 result = sceneColor + bloomContrib; +# endif +#endif + + RWTexOutput[tid] = float4(result, 1); +} diff --git a/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/cutestar.png b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/cutestar.png new file mode 100644 index 0000000000..d43a82e406 Binary files /dev/null and b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/cutestar.png differ diff --git a/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/fringy_soft_chr_rb.png b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/fringy_soft_chr_rb.png new file mode 100644 index 0000000000..f97a6a2ea8 Binary files /dev/null and b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/fringy_soft_chr_rb.png differ diff --git a/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/hex.png b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/hex.png new file mode 100644 index 0000000000..e80c9e755c Binary files /dev/null and b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/hex.png differ diff --git a/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/hex_fringy_soft.png b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/hex_fringy_soft.png new file mode 100644 index 0000000000..e5cc201af8 Binary files /dev/null and b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/hex_fringy_soft.png differ diff --git a/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/moyheart.png b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/moyheart.png new file mode 100644 index 0000000000..5a2a87040d Binary files /dev/null and b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/moyheart.png differ diff --git a/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/square.png b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/square.png new file mode 100644 index 0000000000..8c91754a6f Binary files /dev/null and b/features/Post Processing/Shaders/PostProcessing/DoF/bokehshapes/square.png differ diff --git a/features/Post Processing/Shaders/PostProcessing/DoF/dof.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/DoF/dof.cs.hlsl new file mode 100644 index 0000000000..3640fb6c2d --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/DoF/dof.cs.hlsl @@ -0,0 +1,675 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Modified by Jiaye +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Cinematic Depth of Field shader, using scatter-as-gather for ReShade 3.x+ +// By Frans Bouma, aka Otis / Infuse Project (Otis_Inf) +// https://fransbouma.com +// +// This shader has been released under the following license: +// +// Copyright (c) 2018-2022 Frans Bouma +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Original shader version history: +// 16-aug-2023: v1.2.10: Added Cone Overlap support so the HDR conversion first desaturates the colors so channels with a high value don't +// exponentially boost to irrealistic values. Contributed by MartyMcFly. +// 26-jun-2023: v1.2.9: Found a way to compensate for edges on close to in-focus geometry shimmering through which were otherwise only removable with the NearFarDistanceCompensation added +// in the previous version +// 13-jun-2023: v1.2.8: Added the NearFarDistanceCompensation slider for compensating hard edges on geometry that's out of focus but close to the in-focus plane +// 24-jan-2023: v1.2.7: Added custom shape support for bokeh highlights. The included shapes were created by Moyevka, Murchalloo, K-putt and others. +// 11-nov-2022: v1.2.6: Added bokeh sharpening. +// 28-mar-2022: v1.2.5: Made the pre-blur pass optional, as it's not really needed anymore for qualities higher than 4 and reasonable blur values. +// 15-mar-2022: v1.2.4: Corrected the LDR to HDR and HDR to LDR conversion functions so they now apply proper gamma correct and boost, so hue shifts are limited now as long +// as the highlight boost is kept <= 1 +// Added Gamma factor for advanced highlight tweaking. +// 11-mar-2022: v1.2.3: Changed the sampling stages to use full HDR so there's no more back/forth calculations to SDR along the way. Highlight boost is now +// better and upper range has been cranked up. +// 26-feb-2022: v1.2.2: Made the highlight boost also be able to go to -1 to dim highlights a bit in bright scenes. +// 22-feb-2022: v1.2.1: Removed highlight amplification and properly implemented reinhard-esk de/re-tonemapping for proper highlight calculations. Thanks Marty McFly for the tips. +// (1.2.1) small adjustment, added a boost for the highlights which could help in dimly lit scenes. Based on simple levels math. +// 01-jan-2021: v1.1.19: Corrected PS_PostSmoothing2AndFocusing's signature as it contained a redundant argument which caused warnings in newer versions of reshade. +// 23-oct-2020: v1.1.18: Near-plane bleed blurred the unblurred far plane which leads to artifacts around edges in some cases. This has been rolled back to the earlier versions of +// using the blurred far plane (if any). Also added mirroring to the samplers so edges of the screen aren't blurring darker into the result but should be much smoother. +// 26-mar-2020: v1.1.17: FreeStyle support added (not yet ansel superres compatible). Fixed issue with far plane highlight causing near plane edge pixels getting highlighted. +// 15-mar-2020: v1.1.16: Dithering added for low-luma areas to avoid banding. (Contributed by Prod80) +// 03-feb-2020: v1.1.15: Experimental near plane edge blur improvements. +// 04-oct-2019: v1.1.14: Fine-tuning of near plane blur using smaller tiles. +// 23-jun-2019: v1.1.13: Cleanup of highlight code, reimplementing of luma boost / highlightblending. Removal of unnecessary controls. +// 13-jun-2019: v1.1.12: Bugfix in maxColor blending in near/far blur: no more dirty edges on large highlighted areas. +// 10-jun-2019: v1.1.11: Added new weight calculation, added near-plane highlight normalization. +// 25-may-2019: v1.1.10: Added white boost/correction in gathering passes to have lower-intensity highlights become less prominent. +// Added further weight adjustment tweaks. Changed highlight defaults to utilize code changed in 1.1.9/1.1.10 +// 24-may-2019: v1.1.9: Better near-plane bleed mask. Better far plane pixel weights so more samples get accepted. +// 02-mar-2019: v1.1.8: Added anamorphic bokeh support, so bokehs now get stretched and rotated based on the distance from the center of the screen, with various tweaks. +// 08-jan-2019: v1.1.7: Added 9-tap tent filter as described in [Jimenez2014) for mitigating undersampling. Implementation is from KinoBokeh (see credits below). +// 02-jan-2019: v1.1.6: When near plane max blur is set to 0, the original fragment is now used in the near plane instead of the half-res pixel. +// 19-dec-2018: v1.1.5: Added far plane highlight normalizing for non-gained highlights. Added tooltip for reshade v4.x +// 14-dec-2018: v1.1.4: Far plane weight calculation tweaked a bit as near-focus plane elements could lead to hard edges which looked ugly. Highlight far plane +// adjustments have been reworked because of this. +// 10-dec-2018: v1.1.3: Removed averaging pass for CoC values as it resulted in noticeable wrong CoC values around edges in some TAA using games. The net result +// was minimal anyway. +// 10-nov-2018: v1.1.2: Near plane bugfix: tile gatherer should collect min CoC, not average of min CoC: now ends of narrow lines are properly handled too. +// 30-oct-2018: v1.1.1: Near plane bugfix for high resolutions: it's now blurring resolution independently. Highlight bleed fix in near focus. +// 21-oct-2018: v1.1.0: Far plane weights adjustment, half-res with upscale combiner for performance, new highlights implementation, fixed +// pre-blur highlight smoothing. +// 10-oct-2018: v1.0.8: Improved, tile-based near-plane bleed, optimizations, far-plane large CoC bleed limitation, Highlight dimming, fixed in-focus +// bleed with post-smooth blur, fixed highlight edges, fixed pre-blur. +// 21-sep-2018: v1.0.7: Better near-plane bleed. Optimized near plane CoC storage so less reads are needed. +// Corrected post-blur bleed. Corrected near plane highlight bleed. Overall micro-optimizations. +// 04-sep-2018: v1.0.6: Small fix for DX9 and autofocus. +// 17-aug-2018: v1.0.5: Much better highlighting, higher range for manual focus +// 12-aug-2018: v1.0.4: Finetuned the workaround for d3d9 to only affect reshade 3.4 or lower. +// Finetuned the near highlight extrapolation a bit. Removed highlight threshold as it ruined the blur +// 10-aug-2018: v1.0.3: Daodan's crosshair code added. +// 09-aug-2018: v1.0.2: Added workaround for d3d9 glitch in reshade 3.4. +// 08-aug-2018: v1.0.1: namespace addition for samplers/textures. +// 08-aug-2018: v1.0.0: beta. Feature complete. +// +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Additional credits: +// Reinhard de/retonemapping for highlighting information thanks to Marty McFly. +// Gaussian blur code based on the Gaussian blur ReShade shader by Ioxa +// Thanks to Daodan for the crosshair code in the focus helper. +// 9 tap tent filter is from KinoBokeh Copyright (C) 2015 Keijiro Takahashi. MIT licensed. See file below for details. +// Ref: https://github.com/keijiro/KinoBokeh/blob/master/Assets/Kino/Bokeh/Shader/Composition.cginc +// Thanks to Prod80 for contributing dithering in combiner to avoid banding in low-luma blurred areas. +//////////////////////////////////////////////////////////////////////////////////////////////////// +// References: +// +// [Lee2008] Sungkil Lee, Gerard Jounghyun Kim, and Seungmoon Choi: Real-Time Depth-of-Field Rendering Using Point Splatting +// on Per-Pixel Layers. +// https://pdfs.semanticscholar.org/80f6/f40fe971eddc810c3c86fca6fdfe5c0fdd76.pdf +// +// [Jimenez2014] Jorge Jimenez, Sledgehammer Games: Next generation post processing in Call of Duty Advanced Warfare, SIGGRAPH2014 +// http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare +// +// [Nilsson2012] Filip Nilsson: Implementing realistic depth of field in OpenGL. +// http://fileadmin.cs.lth.se/cs/education/edan35/lectures/12dof.pdf +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "Common/Color.hlsli" +#include "Common/Game.hlsli" +#include "Common/Math.hlsli" +#include "Common/SharedData.hlsli" + +RWTexture2D RWTexOut : register(u0); +RWTexture2D RWFocus : register(u1); +RWTexture2D RWTexCoC : register(u2); + +SamplerState LinearSampler : register(s0); + +Texture2D TexColor : register(t0); +Texture2D TexPreviousFocus : register(t1); +Texture2D DepthTexture : register(t2); +Texture2D TexCoCInput : register(t3); +Texture2D TexCoCBlurredInput : register(t4); +Texture2D TexFarBlur : register(t5); +Texture2D TexNearBlur : register(t6); +Texture2D TexPostSmoothInput : register(t7); +Texture2D TexBokehShape : register(t8); + +cbuffer DoFCB : register(b1) +{ + float TransitionSpeed; + float2 FocusCoord; + float ManualFocusPlane; + float FocalLength; + float FNumber; + float FarPlaneMaxBlur; + float NearPlaneMaxBlur; + float BlurQuality; + float NearFarDistanceCompensation; + float BokehBusyFactor; + float HighlightBoost; + float PostBlurSmoothing; + uint HighlightShape; + float HighlightShapeRotationAngle; + float PetzvalStrength; + uint AutoFocus; + uint3 pad; +}; + +#define SENSOR_SIZE 0.024f + +static const float blurPixelSizeLength = length(SharedData::BufferDim.zw) * 0.5f; +static const float invBlurPixelSizeLength = 1.0f / blurPixelSizeLength; + +struct FocusInfo +{ + float2 texcoord; + float focusDepth; + float focusDepthInM; + float focusDepthInMM; + float pixelSizeLength; + float nearPlaneInMM; + float farPlaneInMM; +}; + +struct DiscBlurInfo +{ + float2 texcoord; + float numberOfRings; + float farPlaneMaxBlurInPixels; + float nearPlaneMaxBlurInPixels; + float cocFactorPerPixel; + float highlightBoostFactor; +}; + +float GetDepth(float2 uv) +{ + float depth = DepthTexture.SampleLevel(LinearSampler, uv, 0); + depth = SharedData::GetScreenDepth(depth) * GAME_UNIT_TO_M * 0.001f; // in KM + return max(depth, 1e-6); +} + +float PreviousFocus() +{ + return TexPreviousFocus[uint2(0, 0)].x; +} + +void FillFocusInfoData(inout FocusInfo toFill) +{ + // Reshade depth buffer ranges from 0.0->1.0, where 1.0 is 1000 in world units. All camera element sizes are in mm, so we state 1 in world units is + // 1 meter. This means to calculate from the linearized depth buffer value to meter we have to multiply by 1000. + // Manual focus value is already in meter (well, sort of. This differs per game so we silently assume it's meter), so we first divide it by + // 1000 to make it equal to a depth value read from the depth linearized depth buffer. + // Read from sampler on current focus which is a 1x1 texture filled with the actual depth value of the focus point to use. + toFill.focusDepth = PreviousFocus(); + toFill.focusDepthInM = toFill.focusDepth * 1000.0; // km to m + toFill.focusDepthInMM = toFill.focusDepthInM * 1000.0; // m to mm + toFill.pixelSizeLength = length(SharedData::BufferDim.xy); // in pixels + + // HyperFocal calculation, see https://photo.stackexchange.com/a/33898. Useful to calculate the edges of the depth of field area + float hyperFocal = (FocalLength * FocalLength) / (FNumber * SENSOR_SIZE); + float hyperFocalFocusDepthFocus = (hyperFocal * toFill.focusDepthInMM); + toFill.nearPlaneInMM = (hyperFocalFocusDepthFocus / (hyperFocal + (toFill.focusDepthInMM - FocalLength))); // in mm + toFill.farPlaneInMM = hyperFocalFocusDepthFocus / (hyperFocal - (toFill.focusDepthInMM - FocalLength)); // in mm +} + +// Gets the tap from the shape pointed at with the shapeSampler specified, over the angle specified, from the distance of the center in shapeRingDistance +// Returns in rgb the shape sample, and in a the luma. +float4 GetShapeTap(float angle, float shapeRingDistance) +{ + float2 pointOffsetForShape = 0.f; + + // we have to add 270 degrees to the custom angle, because it's scatter via gather, so a pixel that has to show the top of our shape is *above* + // the highlight, and the angle has to be 270 degrees to hit it (as sampling the highlight *below it* is what makes it brighter). + sincos(angle + (Math::TAU * HighlightShapeRotationAngle) + (Math::TAU * 0.75f), pointOffsetForShape.x, pointOffsetForShape.y); + pointOffsetForShape.y *= -1.0f; + float2 shapeTapCoords = float2((shapeRingDistance * pointOffsetForShape) + 0.5f); // shapeRingDistance is [0, 0.5] so no need to multiply with 0.5 again + float4 shapeTap = TexBokehShape.SampleLevel(LinearSampler, shapeTapCoords, 0); + shapeTap.a = Color::RGBToLuminance(shapeTap.rgb); + return shapeTap; +} + +float CalculateBlurDiscSize(FocusInfo focusInfo) +{ + float pixelDepth = GetDepth(focusInfo.texcoord); + float pixelDepthInM = pixelDepth * 1000.0; // in meter + + // CoC (blur disc size) calculation based on [Lee2008] + // CoC = ((EF / Zf - F) * (abs(Z-Zf) / Z) + // where E is aperture size in mm, F is focal length in mm, Zf is depth of focal plane in mm, Z is depth of pixel in mm. + // To calculate aperture in mm, we use D = F/N, where F is focal length and N is f-number + // For the people getting confused: + // Remember element sizes are in mm, our depth sizes are in meter, so we have to divide S1 by 1000 to get from meter -> mm. We don't have to + // divide the elements in the 'abs(x-S1)/x' part, as the 1000.0 will then simply be muted out (as a / (x/1000) == a * (1000/x)) + // formula: (((f*f) / N) / ((S1/1000.0) -f)) * (abs(x - S1) / x) + // where f = FocalLength, N = FNumber, S1 = focusInfo.focusDepthInM, x = pixelDepthInM. In-lined to save on registers. + float cocInMM = (((FocalLength * FocalLength) / FNumber) / ((focusInfo.focusDepthInM / 1000.0) - FocalLength)) * + (abs(pixelDepthInM - focusInfo.focusDepthInM) / (pixelDepthInM + (pixelDepthInM == 0))); + float toReturn = max(abs(cocInMM) * SENSOR_SIZE, 0); // divide by sensor size to get coc in % of screen (or better: in sampler units) + return (pixelDepth < focusInfo.focusDepth) ? -toReturn : toReturn; +} + +float GetBlurDiscRadiusFromSource(Texture2D source, float2 texcoord, bool flattenToZero) +{ + float coc = source.SampleLevel(LinearSampler, texcoord, 0).x; + // we're only interested in negative coc's (near plane). All coc's in focus/far plane are flattened to 0. Return the + // absolute value of the coc as we're working with positive blurred CoCs (as the sign is no longer needed) + return (flattenToZero && coc >= 0) ? 0 : abs(coc); +} + +float PerformSingleValueGaussianBlur(Texture2D source, float2 texcoord, float2 offsetWeight, bool flattenToZero) +{ + const float offset[18] = { 0.0, 1.4953705027, 3.4891992113, 5.4830312105, 7.4768683759, 9.4707125766, 11.4645656736, 13.4584295168, 15.4523059431, 17.4461967743, 19.4661974725, 21.4627427973, 23.4592916956, 25.455844494, 27.4524015179, 29.4489630909, 31.445529535, 33.4421011704 }; + const float weight[18] = { 0.033245, 0.0659162217, 0.0636705814, 0.0598194658, 0.0546642566, 0.0485871646, 0.0420045997, 0.0353207015, 0.0288880982, 0.0229808311, 0.0177815511, 0.013382297, 0.0097960001, 0.0069746748, 0.0048301008, 0.0032534598, 0.0021315311, 0.0013582974 }; + + float coc = GetBlurDiscRadiusFromSource(source, texcoord, flattenToZero); + coc *= weight[0]; + + float2 factorToUse = offsetWeight * NearPlaneMaxBlur * 0.8f; + for (int i = 1; i < 18; ++i) { + float2 coordOffset = factorToUse * offset[i]; + float weightSample = weight[i]; + coc += GetBlurDiscRadiusFromSource(source, texcoord + coordOffset, flattenToZero) * weightSample; + coc += GetBlurDiscRadiusFromSource(source, texcoord - coordOffset, flattenToZero) * weightSample; + } + + return saturate(coc); +} + +float3 ConeOverlap(float3 fragment) +{ + float k = 0.4 * 0.33; + float2 f = float2(1 - 2 * k, k); + float3x3 m = float3x3(f.xyy, f.yxy, f.yyx); + return mul(fragment, m); +} + +float3 AccentuateWhites(float3 fragment) +{ + // apply small tow to the incoming fragment, so the whitepoint gets slightly lower than max. + // We don't need to de-tonemap since we are under HDR. + // fragment = pow(abs(ConeOverlap(fragment)), 1); + return fragment / (HighlightBoost > 0.f ? max((1.001 - (HighlightBoost * fragment)), 0.001) : 1.0f); +} + +// returns 2 vectors, (x,y) are up vector, (z,w) are right vector. +// In: pixelVector which is the current pixel converted into a vector where (0,0) is the center of the screen. +float2 ApplyPetzvalMorph(float2 pointOffset, float2 texcoord) +{ + float2 centeredUV = texcoord; + float2 fromCenter = centeredUV - 0.5f; + float distanceFromCenter = length(fromCenter); + float radius = saturate(distanceFromCenter * 2.0f); + if (PetzvalStrength <= 0.001f || distanceFromCenter <= 0.0001f) + return pointOffset; + + float2 radialAxis = fromCenter / distanceFromCenter; + float2 tangentialAxis = float2(-radialAxis.y, radialAxis.x); + float radialComponent = dot(pointOffset, radialAxis); + float tangentialComponent = dot(pointOffset, tangentialAxis); + float petzvalAmount = PetzvalStrength * radius * radius * 1.35f; + float tangentialScale = 1.0f + petzvalAmount; + float radialScale = rcp(tangentialScale); + + return radialAxis * (radialComponent * radialScale) + tangentialAxis * (tangentialComponent * tangentialScale); +} + +// calculate the sample weight based on the values specified. +float CalculateSampleWeight(float sampleRadiusInCoC, float ringDistanceInCoC) +{ + return saturate(sampleRadiusInCoC - (ringDistanceInCoC * NearFarDistanceCompensation) + 0.5); +} + +float2 MorphPointOffsetWithAnamorphicDeltas(float2 pointOffset, float4 anamorphicFactors, float2x2 anamorphicRotationMatrix) +{ + pointOffset.x = pointOffset.x * anamorphicFactors.x + pointOffset.x * anamorphicFactors.z; + pointOffset.y = pointOffset.y * anamorphicFactors.y + pointOffset.y * anamorphicFactors.w; + return mul(pointOffset, anamorphicRotationMatrix); +} + +// Gathers min CoC from a horizontal range of pixels around the pixel at texcoord, for a range of -TILE_SIZE+1 to +TILE_SIZE+1. +// returns minCoC +float PerformTileGatherHorizontal(uint2 DTid) +{ + float tileSize = 1; + float minCoC = 10; + float coc; + float2 offset = uint2(1, 0); + for (float i = 0; i <= tileSize; ++i) { + coc = TexCoCInput[DTid + offset].r; + minCoC = min(minCoC, coc); + coc = TexCoCInput[DTid - offset].r; + minCoC = min(minCoC, coc); + offset.x += 1; + } + return minCoC; +} + +// Gathers min CoC from a vertical range of pixels around the pixel at texcoord from the high-res focus plane, for a range of -TILE_SIZE+1 to +TILE_SIZE+1. +// returns min CoC +float PerformTileGatherVertical(uint2 DTid) +{ + float tileSize = 1; + float minCoC = 10; + float coc; + float2 offset = uint2(0, 1); + for (float i = 0; i <= tileSize; ++i) { + coc = TexCoCInput[DTid + offset].r; + minCoC = min(minCoC, coc); + coc = TexCoCInput[DTid - offset].r; + minCoC = min(minCoC, coc); + offset.y += 1; + } + return minCoC; +} + +// Gathers the min CoC of the tile at texcoord and the 8 tiles around it. +float PerformNeighborTileGather(uint2 DTid) +{ + float minCoC = 10; + float tileSizeX = 1; + float tileSizeY = 1; + // tile is TILE_SIZE*2+1 wide. So add that and substract that to get to neighbor tile right/left. + // 3x3 around center. + uint2 baseOffset = uint2(tileSizeX * 2 + 1, tileSizeY * 2 + 1); + for (float i = -1; i < 2; i++) { + for (float j = -1; j < 2; j++) { + uint2 coordOffset = uint2(baseOffset.x * i, baseOffset.y * j); + float coc = TexCoCInput[DTid + coordOffset].r; + minCoC = min(minCoC, coc); + } + } + return minCoC; +} + +// Performs a small blur to the out of focus areas using a lower amount of rings. Additionally it calculates the luma of the fragment into alpha +// and makes sure the fragment post-blur has the maximum luminosity from the taken samples to preserve harder edges on highlights. +// In: blurInfo, the pre-calculated disc blur information from the vertex shader. +// source, the source buffer to read RGBA data from +// Out: RGBA fragment that's the result of the disc-blur on the pixel at texcoord in source. A contains luma of RGB. +float4 PerformPreDiscBlur(DiscBlurInfo blurInfo, Texture2D source) +{ + const float radiusFactor = 1.0 / max(blurInfo.numberOfRings, 1); + const float pointsFirstRing = max(blurInfo.numberOfRings - 3, 2); // each ring has a multiple of this value of sample points. + + float4 fragment = source.SampleLevel(LinearSampler, blurInfo.texcoord, 0); + fragment.rgb = AccentuateWhites(fragment.rgb); + return fragment; +} + +float4 PerformFullFragmentGaussianBlur(Texture2D source, float2 texcoord, uint2 DTid, float2 offsetWeight) +{ + float offset[6] = { 0.0, 1.4584295168, 3.40398480678, 5.3518057801, 7.302940716, 9.2581597095 }; + float weight[6] = { 0.13298, 0.23227575, 0.1353261595, 0.0511557427, 0.01253922, 0.0019913644 }; + + float coc = TexCoCInput[DTid].r; + float4 fragment = source[DTid]; + float fragmentLuma = Color::RGBToLuminance(fragment.rgb); + float4 originalFragment = fragment; + float absoluteCoC = abs(coc); + float lengthPixelSize = length(float2(SharedData::BufferDim.z, SharedData::BufferDim.w)); + + if (absoluteCoC < 0.2 || PostBlurSmoothing < 0.01 || fragmentLuma < 0.3) { + // in focus or postblur smoothing isn't enabled or not really a highlight, ignore + return fragment; + } + + fragment *= weight[0]; + float2 factorToUse = offsetWeight * PostBlurSmoothing; + + for (int i = 1; i < 6; ++i) { + float2 coordOffset = factorToUse * offset[i]; + float weightSample = weight[i]; + float sampleCoC = TexCoCInput.SampleLevel(LinearSampler, texcoord + coordOffset, 0).r; + float maskFactor = abs(sampleCoC) < 0.2; + + fragment += (originalFragment * maskFactor * weightSample) + + (source.SampleLevel(LinearSampler, texcoord + coordOffset, 0) * (1 - maskFactor) * weightSample); + + sampleCoC = TexCoCInput.SampleLevel(LinearSampler, texcoord - coordOffset, 0).r; + maskFactor = abs(sampleCoC) < 0.2; + + fragment += (originalFragment * maskFactor * weightSample) + + (source.SampleLevel(LinearSampler, texcoord - coordOffset, 0) * (1 - maskFactor) * weightSample); + } + return fragment; +} + +[numthreads(1, 1, 1)] void CS_UpdateFocus(uint2 DTid : SV_DispatchThreadID) { + float depth = AutoFocus ? GetDepth(FocusCoord) : ManualFocusPlane; + float previousFocus = TexPreviousFocus[uint2(0, 0)]; + RWFocus[DTid] = lerp(previousFocus, depth, TransitionSpeed); +} + + [numthreads(8, 8, 1)] void CS_CalculateCoC(uint2 DTid : SV_DispatchThreadID) +{ + if (DTid.x >= (uint)SharedData::BufferDim.x || DTid.y >= (uint)SharedData::BufferDim.y) + return; + + float2 uv = (DTid.xy + 0.5f) * SharedData::BufferDim.zw; + float4 color = TexColor[DTid]; + + FocusInfo focusInfo; + focusInfo.texcoord = uv; + FillFocusInfoData(focusInfo); + + float coc = CalculateBlurDiscSize(focusInfo); + RWTexCoC[DTid] = coc; +} + +[numthreads(8, 8, 1)] void CS_CoCTile1(uint2 DTid : SV_DispatchThreadID) { + RWTexCoC[DTid] = PerformTileGatherHorizontal(DTid); +} + + [numthreads(8, 8, 1)] void CS_CoCTile2(uint2 DTid : SV_DispatchThreadID) +{ + RWTexCoC[DTid] = PerformTileGatherVertical(DTid); +} + +[numthreads(8, 8, 1)] void CS_CoCTileNeighbor(uint2 DTid : SV_DispatchThreadID) { + RWTexCoC[DTid] = PerformNeighborTileGather(DTid); +} + + [numthreads(8, 8, 1)] void CS_CoCGaussian1(uint2 DTid : SV_DispatchThreadID) +{ + float2 uv = 2.0f * (DTid.xy + 0.5f) * SharedData::BufferDim.zw; + RWTexCoC[DTid] = PerformSingleValueGaussianBlur(TexCoCInput, uv, float2(2.0f * SharedData::BufferDim.z, 0.0f), true); +} + +[numthreads(8, 8, 1)] void CS_CoCGaussian2(uint2 DTid : SV_DispatchThreadID) { + float2 uv = 2.0f * (DTid.xy + 0.5f) * SharedData::BufferDim.zw; + RWTexCoC[DTid] = PerformSingleValueGaussianBlur(TexCoCInput, uv, float2(0.0f, 2.0f * SharedData::BufferDim.w), false); +} + + [numthreads(8, 8, 1)] void CS_Blur(uint2 DTid : SV_DispatchThreadID) +{ + DiscBlurInfo blurInfo; + blurInfo.texcoord = 2.0f * (DTid.xy + 0.5f) * SharedData::BufferDim.zw; + blurInfo.numberOfRings = round(BlurQuality); + blurInfo.farPlaneMaxBlurInPixels = (FarPlaneMaxBlur * 0.01f) * invBlurPixelSizeLength; + blurInfo.nearPlaneMaxBlurInPixels = (NearPlaneMaxBlur * 0.01f) * invBlurPixelSizeLength; + blurInfo.cocFactorPerPixel = blurPixelSizeLength * blurInfo.farPlaneMaxBlurInPixels; // not needed for near plane. + // Pre Blur + float4 color = PerformPreDiscBlur(blurInfo, TexColor); + RWTexOut[DTid] = color; +} + +[numthreads(8, 8, 1)] void CS_FarBlur(uint2 DTid : SV_DispatchThreadID) { + DiscBlurInfo blurInfo; + blurInfo.texcoord = 2.0f * (DTid.xy + 0.5f) * SharedData::BufferDim.zw; + blurInfo.numberOfRings = round(BlurQuality); + blurInfo.farPlaneMaxBlurInPixels = (FarPlaneMaxBlur * 0.01f) * invBlurPixelSizeLength; + blurInfo.nearPlaneMaxBlurInPixels = (NearPlaneMaxBlur * 0.01f) * invBlurPixelSizeLength; + blurInfo.cocFactorPerPixel = blurPixelSizeLength * blurInfo.farPlaneMaxBlurInPixels; // not needed for near plane. + + const float pointsFirstRing = 7; // each ring has a multiple of this value of sample points. + float4 color = TexColor[DTid]; + float colorRadius = TexCoCInput[2 * DTid].r; + // we'll not process near plane fragments as they're processed in a separate pass. + if (colorRadius < blurPixelSizeLength || blurInfo.farPlaneMaxBlurInPixels <= 0) { + // near plane fragment, will be done in near plane pass + RWTexOut[DTid] = color; + return; + } + float bokehBusyFactorToUse = saturate(1.0 - BokehBusyFactor); // use the busy factor as an edge bias on the blur, not the highlights + float4 average = float4(color.rgb * colorRadius * bokehBusyFactorToUse, bokehBusyFactorToUse); + float2 pointOffset = float2(0, 0); + float2 ringRadiusDeltaCoords = (SharedData::BufferDim.zw * blurInfo.farPlaneMaxBlurInPixels * colorRadius) / blurInfo.numberOfRings; + float2 currentRingRadiusCoords = ringRadiusDeltaCoords; + float cocPerRing = (colorRadius * FarPlaneMaxBlur) / blurInfo.numberOfRings; + float ringDistance = 0; + float pointsOnRing = pointsFirstRing; + bool useShape = HighlightShape > 0; + float4 shapeTap = float4(1.0f, 1.0f, 1.0f, 1.0f); + for (float ringIndex = 0; ringIndex < blurInfo.numberOfRings; ringIndex++) { + float anglePerPoint = Math::TAU / pointsOnRing; + float angle = anglePerPoint; + float ringWeight = lerp(ringIndex / blurInfo.numberOfRings, 1, bokehBusyFactorToUse); + ringDistance += cocPerRing; + float shapeRingDistance = ((ringIndex + 1) / blurInfo.numberOfRings) * 0.5f; + for (float pointNumber = 0; pointNumber < pointsOnRing; pointNumber++) { + sincos(angle, pointOffset.y, pointOffset.x); + // shapeLuma is in Alpha + if (useShape) + shapeTap = GetShapeTap(angle, shapeRingDistance); + else + pointOffset = ApplyPetzvalMorph(pointOffset, blurInfo.texcoord); + float2 tapCoords = float2(blurInfo.texcoord + (pointOffset * currentRingRadiusCoords)); + float sampleRadius = TexCoCInput.SampleLevel(LinearSampler, tapCoords, 0).r; + float4 tap = 0; + float weight = (sampleRadius >= 0) * ringWeight * CalculateSampleWeight(sampleRadius * FarPlaneMaxBlur, ringDistance) * (shapeTap.a > 0.01 ? 1.0f : 0.0f); + // adjust the weight for samples which are in front of the fragment, as they have to get their weight boosted so we don't see edges bleeding through. + // as otherwise they'll get a weight that's too low relatively to the pixels sampled from the plane the fragment is in.The 3.0 value is empirically determined. + weight *= (1.0 + min(FarPlaneMaxBlur, 3.0f) * saturate(colorRadius - sampleRadius)); + if (weight > 0) + tap = TexColor.SampleLevel(LinearSampler, tapCoords, 0); + average.rgb += tap.rgb * weight; + average.w += weight; + angle += anglePerPoint; + } + pointsOnRing += pointsFirstRing; + currentRingRadiusCoords += ringRadiusDeltaCoords; + } + color.rgb = average.rgb / (average.w + (average.w == 0)); + RWTexOut[DTid] = color; +} + + [numthreads(8, 8, 1)] void CS_NearBlur(uint2 DTid : SV_DispatchThreadID) +{ + DiscBlurInfo blurInfo; + blurInfo.texcoord = 2.0f * (DTid.xy + 0.5f) * SharedData::BufferDim.zw; + blurInfo.numberOfRings = round(BlurQuality); + blurInfo.farPlaneMaxBlurInPixels = (FarPlaneMaxBlur * 0.01f) * invBlurPixelSizeLength; + blurInfo.nearPlaneMaxBlurInPixels = (NearPlaneMaxBlur * 0.01f) * invBlurPixelSizeLength; + blurInfo.cocFactorPerPixel = blurPixelSizeLength * blurInfo.farPlaneMaxBlurInPixels; // not needed for near plane. + + float4 color = TexColor[DTid]; + // r contains blurred CoC, g contains original CoC. Original is negative. + float2 colorRadii = float2(TexCoCBlurredInput[DTid], TexCoCInput[2 * DTid]); + float colorRadiusToUse = colorRadii.r; + + if (colorRadii.r <= blurPixelSizeLength) { + // the blurred CoC value is still 0, we'll never end up with a pixel that has a different value than color, so abort now by + // returning the color we already read. + color.a = 0; + RWTexOut[DTid] = color; + return; + } + + // use one extra ring as undersampling is really prominent in near-camera objects. + float numberOfRings = max(blurInfo.numberOfRings, 1) + 1; + float pointsFirstRing = 7; + // luma is stored in alpha + float bokehBusyFactorToUse = saturate(1.0 - BokehBusyFactor); // use the busy factor as an edge bias on the blur, not the highlights + float4 average = float4(color.rgb * colorRadiusToUse * bokehBusyFactorToUse, bokehBusyFactorToUse); + float2 pointOffset = float2(0, 0); + float nearPlaneBlurInPixels = blurInfo.nearPlaneMaxBlurInPixels * colorRadiusToUse; + float2 ringRadiusDeltaCoords = float2(SharedData::BufferDim.z, SharedData::BufferDim.w) * (nearPlaneBlurInPixels / (numberOfRings - 1)); + float pointsOnRing = pointsFirstRing; + float2 currentRingRadiusCoords = ringRadiusDeltaCoords; + bool useShape = HighlightShape > 0; + float4 shapeTap = float4(1.0f, 1.0f, 1.0f, 1.0f); + for (float ringIndex = 0; ringIndex < numberOfRings; ringIndex++) { + float anglePerPoint = Math::TAU / pointsOnRing; + float angle = anglePerPoint; + // no further weight needed, bleed all you want. + float weight = lerp(ringIndex / numberOfRings, 1, smoothstep(0, 1, bokehBusyFactorToUse)); + float shapeRingDistance = ((ringIndex + 1) / numberOfRings) * 0.5f; + for (float pointNumber = 0; pointNumber < pointsOnRing; pointNumber++) { + sincos(angle, pointOffset.y, pointOffset.x); + // shapeLuma is in Alpha + if (useShape) + shapeTap = GetShapeTap(angle, shapeRingDistance); + else + pointOffset = ApplyPetzvalMorph(pointOffset, blurInfo.texcoord); + float2 tapCoords = float2(blurInfo.texcoord + (pointOffset * currentRingRadiusCoords)); + float4 tap = TexColor.SampleLevel(LinearSampler, tapCoords, 0); + // r contains blurred CoC, g contains original CoC. Original can be negative + float2 sampleRadii = float2(TexCoCBlurredInput.SampleLevel(LinearSampler, tapCoords, 0), TexCoCInput.SampleLevel(LinearSampler, tapCoords, 0)); + float blurredSampleRadius = sampleRadii.r; + float sampleWeight = weight * (shapeTap.a > 0.01 ? 1.0f : 0.0f); + average.rgb += tap.rgb * sampleWeight; + average.w += sampleWeight; + angle += anglePerPoint; + } + pointsOnRing += pointsFirstRing; + currentRingRadiusCoords += ringRadiusDeltaCoords; + } + average.rgb /= (average.w + (average.w == 0)); + float alpha = saturate((min(2.5, NearPlaneMaxBlur) + 0.4) * (colorRadiusToUse > 0.1 ? (colorRadii.g <= 0 ? 2 : 1) * colorRadiusToUse : max(colorRadiusToUse, -colorRadii.g))); + color.rgb = average.rgb; + color.a = alpha; + RWTexOut[DTid] = color; +} + +[numthreads(8, 8, 1)] void CS_TentFilter(uint2 DTid : SV_DispatchThreadID) { + float4 average; + uint4 offset = uint4(1, 1, -1, 0); + average = TexColor[DTid - offset.xy]; + average += TexColor[DTid - offset.wy] * 2; + average += TexColor[DTid - offset.zy]; + average += TexColor[DTid + offset.zw] * 2; + average += TexColor[DTid] * 4; + average += TexColor[DTid + offset.xw] * 2; + average += TexColor[DTid + offset.zy]; + average += TexColor[DTid + offset.wy] * 2; + average += TexColor[DTid + offset.xy]; + average /= 16; + RWTexOut[DTid] = average; +} + + [numthreads(8, 8, 1)] void CS_Combiner(uint2 DTid : SV_DispatchThreadID) +{ + float2 uv = (DTid.xy + 0.5f) * SharedData::BufferDim.zw; + // first blend far plane with original buffer, then near plane on top of that. + float4 originalFragment = TexColor[DTid]; + originalFragment.rgb = AccentuateWhites(originalFragment.rgb); + float4 farFragment = TexFarBlur.SampleLevel(LinearSampler, uv, 0); + float4 nearFragment = TexNearBlur.SampleLevel(LinearSampler, uv, 0); + float pixelCoC = TexCoCInput[DTid].r; + // multiply with far plane max blur so if we need to have 0 blur we get full res + float realCoC = pixelCoC * saturate(FarPlaneMaxBlur); + // all CoC's > 0.1 are full far fragment, below that, we're going to blend. This avoids shimmering far plane without the need of a + // 'magic' number to boost up the alpha. + float blendFactor = (realCoC > 0.1) ? 1 : smoothstep(0, 1, (realCoC / 0.1)); + float4 color; + color = lerp(originalFragment, farFragment, blendFactor); + color.rgb = lerp(color.rgb, nearFragment.rgb, nearFragment.a * (NearPlaneMaxBlur != 0)); + color.a = 1.0; + RWTexOut[DTid] = color; +} + +[numthreads(8, 8, 1)] void CS_PostSmoothing1(uint2 DTid : SV_DispatchThreadID) { + float2 uv = (DTid.xy + 0.5f) * SharedData::BufferDim.zw; + + RWTexOut[DTid] = PerformFullFragmentGaussianBlur(TexColor, uv, DTid, float2((SharedData::BufferDim.z), 0.0)); +} + + [numthreads(8, 8, 1)] void CS_PostSmoothing2AndFocusing(uint2 DTid : SV_DispatchThreadID) +{ + float2 uv = (DTid.xy + 0.5f) * SharedData::BufferDim.zw; + + float4 color = PerformFullFragmentGaussianBlur(TexPostSmoothInput, uv, DTid, float2(0.0, (SharedData::BufferDim.w))); + float4 originalColor = TexColor[DTid]; + + float coc = abs(TexCoCInput[DTid].r); + color.rgb = lerp(originalColor.rgb, color.rgb, saturate(coc < length(SharedData::BufferDim.zw) ? 0 : 4 * coc)); + + RWTexOut[DTid] = float4(color.rgb, 1.0f); +} \ No newline at end of file diff --git a/features/Post Processing/Shaders/PostProcessing/HistogramAutoExposure/common.hlsli b/features/Post Processing/Shaders/PostProcessing/HistogramAutoExposure/common.hlsli new file mode 100644 index 0000000000..0b9b2c6e2f --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/HistogramAutoExposure/common.hlsli @@ -0,0 +1,12 @@ +cbuffer AutoExposureCB : register(b1) +{ + float2 AdaptArea; + float2 AdaptationRange; + float AdaptLerp; + float ExposureCompensation; + float PurkinjeStartEV; + float PurkinjeMaxEV; + float PurkinjeStrength; + + float pad[3]; +}; \ No newline at end of file diff --git a/features/Post Processing/Shaders/PostProcessing/HistogramAutoExposure/histogram.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/HistogramAutoExposure/histogram.cs.hlsl new file mode 100644 index 0000000000..e26dca00b9 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/HistogramAutoExposure/histogram.cs.hlsl @@ -0,0 +1,145 @@ +/// By ProfJack/五脚猫, 2024-2-17 UTC +/// ref: +/// https://bruop.github.io/exposure/ +/// https://knarkowicz.wordpress.com/2016/01/09/automatic-exposure/ + +#include "PostProcessing/HistogramAutoExposure/common.hlsli" + +#include "Common/Color.hlsli" + +RWStructuredBuffer RWBufferHistogram : register(u0); +RWStructuredBuffer RWBufferAdaptation : register(u1); + +Texture2D TexColor : register(t0); + +const static float MinLogLum = -13; +const static float LogLumRange = 31; +const static float RcpLogLumRange = rcp(LogLumRange); +const static uint HistogramBins = 256; +const static uint FirstLuminanceBin = 1; +const static uint LastLuminanceBin = HistogramBins - 1; +const static uint SampleStride = 8; +const static uint HistogramWeightScale = 16; +const static uint SampleWeight = SampleStride * SampleStride * HistogramWeightScale; +const static float LowPercent = 0.10; +const static float HighPercent = 0.90; + +// Increased thread count per group for better occupancy +groupshared uint histogramShared[256]; + +// Optimized hash function using fewer operations +float2 hash2D(float2 p) +{ + float3 p3 = frac(float3(p.xyx) * float3(0.1031, 0.1030, 0.0973)); + p3 += dot(p3, p3.yzx + 33.33); + return frac((p3.xx + p3.yz) * p3.zy); +} + +// Precompute box bounds to avoid per-pixel calculations +float4 ComputeBoxBounds(float2 dims) +{ + float4 box = float4(.5 - AdaptArea * .5, .5 + AdaptArea * .5); + return float4( + dims.x * box.r, + dims.y * box.g, + dims.x * box.b, + dims.y * box.a); +} + +[numthreads(32, 32, 1)] void CS_Histogram(uint2 tid : SV_DispatchThreadID, uint gidx : SV_GroupIndex) { + uint2 dims; + TexColor.GetDimensions(dims.x, dims.y); + + // Initialize shared memory - only need to do this once per group + if (gidx < 256) { + histogramShared[gidx] = 0; + } + GroupMemoryBarrierWithGroupSync(); + + uint2 baseCoord = tid * SampleStride; + bool validSample = !any(baseCoord >= dims); + + // Jitter inside each sampled cell to reduce structured aliasing while avoiding edge duplication. + uint2 jitter = uint2(hash2D(tid) * SampleStride); + uint2 pxCoord = min(baseCoord + jitter, dims - 1); + + // Precompute box bounds + float4 boxBounds = ComputeBoxBounds(dims); + + // Optimized box check using precomputed bounds + bool inBox = validSample && + (pxCoord.x > boxBounds.x) && (pxCoord.x < boxBounds.z) && + (pxCoord.y > boxBounds.y) && (pxCoord.y < boxBounds.w); + + if (inBox) { + float3 color = TexColor[pxCoord].rgb; + float luma = Color::RGBToLuminance(color); + + // Optimized bin calculation - avoid unnecessary saturate + if (luma > 1e-10) { + float histogramPos = saturate((log2(luma) - MinLogLum) * RcpLogLumRange); + float fBin = FirstLuminanceBin + histogramPos * (LastLuminanceBin - FirstLuminanceBin); + uint bin0 = min((uint)fBin, LastLuminanceBin); + uint bin1 = min(bin0 + 1, LastLuminanceBin); + float weight1 = frac(fBin); + float weight0 = 1.0 - weight1; + + InterlockedAdd(histogramShared[bin0], (uint)(weight0 * SampleWeight + 0.5)); + InterlockedAdd(histogramShared[bin1], (uint)(weight1 * SampleWeight + 0.5)); + } + } + + GroupMemoryBarrierWithGroupSync(); + + // Save to texture - only need to do this once per group + if (gidx < 256) { + InterlockedAdd(RWBufferHistogram[gidx], histogramShared[gidx]); + } +}; + +[numthreads(256, 1, 1)] void CS_Average(uint gidx : SV_GroupIndex) { + if (gidx == 0) { + float totalWeight = 0.0; + [unroll] for (uint i = FirstLuminanceBin; i < HistogramBins; ++i) + { + totalWeight += (float)RWBufferHistogram[i]; + } + + float avgLum = max(1e-5, RWBufferAdaptation[0]); + if (totalWeight > 0.0) { + float lowCut = totalWeight * LowPercent; + float highCut = totalWeight * HighPercent; + float weightedLogLum = 0.0; + float keptWeight = 0.0; + + [unroll] for (uint bin = FirstLuminanceBin; bin < HistogramBins; ++bin) + { + float binWeight = (float)RWBufferHistogram[bin]; + + float lowDiscard = min(binWeight, lowCut); + binWeight -= lowDiscard; + lowCut -= lowDiscard; + highCut = max(highCut - lowDiscard, 0.0); + + binWeight = min(binWeight, max(highCut, 0.0)); + highCut -= binWeight; + + float histogramPos = ((float)bin - FirstLuminanceBin) / (LastLuminanceBin - FirstLuminanceBin); + float logLum = histogramPos * LogLumRange + MinLogLum; + weightedLogLum += logLum * binWeight; + keptWeight += binWeight; + } + + if (keptWeight > 0.0) + avgLum = exp2(weightedLogLum / keptWeight); + } + + [unroll] for (uint clearBin = 0; clearBin < HistogramBins; ++clearBin) + { + RWBufferHistogram[clearBin] = 0; + } + + float adaptedLum = lerp(max(1e-5, RWBufferAdaptation[0]), avgLum, AdaptLerp); + RWBufferAdaptation[0] = adaptedLum; + } +} diff --git a/features/Post Processing/Shaders/PostProcessing/LUT/lut.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/LUT/lut.cs.hlsl new file mode 100644 index 0000000000..32e0cca0cf --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/LUT/lut.cs.hlsl @@ -0,0 +1,96 @@ + +#include "Common/Color.hlsli" + +RWTexture2D RWTexOut : register(u0); + +Texture2D TexColor : register(t0); +Texture2D TexLut : register(t1); +Texture3D TexLut3D : register(t2); + +cbuffer LUTCB : register(b1) +{ + float3 InputMin : packoffset(c0.x); + float pad : packoffset(c0.w); + float3 InputMax : packoffset(c1.x); + int LutType : packoffset(c1.w); +}; + +// x -> y -> z +float3 biLerp(in float3 values[8], in float3 lerpFactors) +{ + float3 x1 = lerp(values[0], values[1], lerpFactors.x); + float3 x2 = lerp(values[2], values[3], lerpFactors.x); + float3 x3 = lerp(values[4], values[5], lerpFactors.x); + float3 x4 = lerp(values[6], values[7], lerpFactors.x); + float3 y1 = lerp(x1, x2, lerpFactors.y); + float3 y2 = lerp(x3, x4, lerpFactors.y); + float3 z = lerp(y1, y2, lerpFactors.z); + return z; +} + +[numthreads(8, 8, 1)] void main(uint2 tid : SV_DispatchThreadID) { + uint3 dims; + [branch] if (LutType == 3) + TexLut3D.GetDimensions(dims.x, dims.y, dims.z); + else TexLut.GetDimensions(dims.x, dims.y); + + float3 color = TexColor[tid].rgb; + [branch] if (LutType == 0) + { + float luma = Color::RGBToLuminance(color); + float pxCoord = (luma - InputMin.x) / (InputMax.x - InputMin.x) * (dims.x - 1); + int px0 = clamp(int(pxCoord), 0, dims.x - 1); + int px1 = min(px0 + 1, dims.x - 1); + float targetLuma = lerp(TexLut[int2(px0, 1)].x, TexLut[int2(px1, 1)].x, saturate(pxCoord - px0)); + + color *= targetLuma / (luma + 1e-8); + } + else if (LutType == 1) + { + float3 pxCoord = (color - InputMin) / (InputMax - InputMin) * (dims.x - 1); + int3 px0 = clamp(int3(pxCoord), 0, dims.x - 1); + int3 px1 = min(px0 + 1, dims.x - 1); + float3 lerpFactors = saturate(pxCoord - px0); + + color.r = lerp(TexLut[int2(px0.x, 1)].x, TexLut[int2(px1.x, 1)].x, lerpFactors.x); + color.g = lerp(TexLut[int2(px0.y, 1)].x, TexLut[int2(px1.y, 1)].x, lerpFactors.y); + color.b = lerp(TexLut[int2(px0.z, 1)].x, TexLut[int2(px1.z, 1)].x, lerpFactors.z); + } + else + { + dims = LutType == 2 ? uint3(dims.y, dims.y, dims.x / dims.y) : dims; + + float3 pxCoord = (color - InputMin) / (InputMax - InputMin) * (dims - 1); + int3 px0 = clamp(int3(pxCoord), 0, dims - 1); + int3 px1 = min(px0 + 1, dims - 1); + float3 lerpFactors = saturate(pxCoord - px0); + + float3 lutSamples[8]; + [branch] if (LutType == 2) + { + lutSamples[0] = TexLut[int2(px0.x + dims.y * px0.z, px0.y)]; + lutSamples[1] = TexLut[int2(px1.x + dims.y * px0.z, px0.y)]; + lutSamples[2] = TexLut[int2(px0.x + dims.y * px0.z, px1.y)]; + lutSamples[3] = TexLut[int2(px1.x + dims.y * px0.z, px1.y)]; + lutSamples[4] = TexLut[int2(px0.x + dims.y * px1.z, px0.y)]; + lutSamples[5] = TexLut[int2(px1.x + dims.y * px1.z, px0.y)]; + lutSamples[6] = TexLut[int2(px0.x + dims.y * px1.z, px1.y)]; + lutSamples[7] = TexLut[int2(px1.x + dims.y * px1.z, px1.y)]; + } + else + { + lutSamples[0] = TexLut3D[px0]; + lutSamples[1] = TexLut3D[int3(px1.x, px0.y, px0.z)]; + lutSamples[2] = TexLut3D[int3(px0.x, px1.y, px0.z)]; + lutSamples[3] = TexLut3D[int3(px1.x, px1.y, px0.z)]; + lutSamples[4] = TexLut3D[int3(px0.x, px0.y, px1.z)]; + lutSamples[5] = TexLut3D[int3(px1.x, px0.y, px1.z)]; + lutSamples[6] = TexLut3D[int3(px0.x, px1.y, px1.z)]; + lutSamples[7] = TexLut3D[px1]; + } + + color = biLerp(lutSamples, lerpFactors); + } + + RWTexOut[tid] = float4(color, 1); +} \ No newline at end of file diff --git a/features/Post Processing/Shaders/PostProcessing/LensFlare/lensflare.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/LensFlare/lensflare.cs.hlsl new file mode 100644 index 0000000000..eb529cdad5 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/LensFlare/lensflare.cs.hlsl @@ -0,0 +1,303 @@ +#include "PostProcessing/common.hlsli" + +static const float PI = 3.14159265359; +static const float EPSILON = 1e-6; +static const int NUM_GHOSTS = 8; + +// Resources +Texture2D InputTexture : register(t0); +SamplerState ColorSampler : register(s0); +SamplerState BorderSampler : register(s1); +RWTexture2D OutputTexture : register(u0); + +cbuffer LensFlareConstants : register(b1) +{ + // Per-pass dimensions (updated before each dispatch) + float OutputWidth; + float OutputHeight; + float InputWidth; + float InputHeight; + + // Threshold + float ThresholdLevel; + float ThresholdRange; + float GhostStrength; + float GhostChromaShift; + + // Halo + float HaloStrength; + float HaloRadius; + float HaloWidth; + float HaloCompression; + + float HaloChromaShift; + float Intensity; + uint FFTResolution; + int GLocalMask; + + float3 Tint; + float KernelScale; + + float AspectRatio; + int ApertureBlades; + float ApertureRotation; + float PadScale; + + uint ActiveGhostMask; + float ApertureSize; + float2 _pad0; + + // Ghost data + float4 GhostColors[NUM_GHOSTS]; + float4 GhostScalesPacked[2]; // 8 scales packed into 2 float4s + float4 GhostKernelScalesPacked[2]; +} + +// ============================================================ +// Utilities +// ============================================================ + +float GetGhostScale(int i) +{ + return GhostScalesPacked[i / 4][i % 4]; +} + +// Fisheye UV distortion (based on Shadertoy by Crucifer) +float2 FisheyeUV(float2 uv, float compression, float zoom) +{ + float2 negPosUV = 2.0f * uv - 1.0f; + float scale = compression * atan(rcp(compression)); + float radiusDist = length(negPosUV) * scale; + float radiusDir = compression * tan(radiusDist / compression) * zoom; + float phi = atan2(negPosUV.y, negPosUV.x); + float2 newUV = float2(radiusDir * cos(phi) + 1.0f, radiusDir * sin(phi) + 1.0f) * 0.5f; + return newUV; +} + +// Screen-space disc mask (vignette) +float DiscMask(float2 screenPos) +{ + return saturate(1.0f - dot(screenPos, screenPos)); +} + +// ============================================================ +// Pass 1: CSThreshold — 13-tap CoD-style downsample + threshold +// Input: full-res scene (t0), Output: half-res thresholded buffer (u0) +// ============================================================ +[numthreads(8, 8, 1)] void CSThreshold(uint3 DTid : SV_DispatchThreadID) { + if (DTid.x >= (uint)OutputWidth || DTid.y >= (uint)OutputHeight) + return; + + // UV maps output pixel to normalized [0,1] — bilinear sampling downscales from full-res input + float2 uv = (DTid.xy + 0.5f) / float2(OutputWidth, OutputHeight); + // Pixel size in input (full-res) space for sampling offsets + float2 pixelSize = 1.0f / float2(InputWidth, InputHeight); + + float3 color = 0; + + // 4 center samples (weight 0.5) + color += InputTexture.SampleLevel(ColorSampler, uv + pixelSize * float2(-1.0f, 1.0f), 0).rgb; + color += InputTexture.SampleLevel(ColorSampler, uv + pixelSize * float2(1.0f, 1.0f), 0).rgb; + color += InputTexture.SampleLevel(ColorSampler, uv + pixelSize * float2(-1.0f, -1.0f), 0).rgb; + color += InputTexture.SampleLevel(ColorSampler, uv + pixelSize * float2(1.0f, -1.0f), 0).rgb; + float3 result = (color / 4.0f) * 0.5f; + + // 9 outer samples (weight 0.5) + color = 0; + [unroll] for (int x = -1; x <= 1; x++) + { + [unroll] for (int y = -1; y <= 1; y++) + { + color += InputTexture.SampleLevel(ColorSampler, uv + pixelSize * float2(x, y) * 2.0f, 0).rgb; + } + } + result += (color / 9.0f) * 0.5f; + + // Smooth threshold (level + range) + float luminance = dot(result, float3(0.333f, 0.333f, 0.333f)); + float thresholdScale = saturate((luminance - ThresholdLevel) / max(ThresholdRange, 0.001f)); + result *= thresholdScale; + + OutputTexture[DTid.xy] = float4(result, 1.0f); +} + + // ============================================================ + // Pass 2: CSGhostHalo — ghosts + fisheye halo from threshold buffer + // Input: half-res threshold (t0), Output: half-res ghost+halo (u0) + // ============================================================ + [numthreads(8, 8, 1)] void CSGhostHalo(uint3 DTid : SV_DispatchThreadID) +{ + if (DTid.x >= (uint)OutputWidth || DTid.y >= (uint)OutputHeight) + return; + + float2 uv = (DTid.xy + 0.5f) / float2(OutputWidth, OutputHeight); + float3 color = 0; + float2 radiantVector = uv - 0.5f; + + // --- Ghosts --- + [branch] if (GhostStrength > EPSILON) + { + // Chromatic aberration on input for ghosts + for (int i = 0; i < NUM_GHOSTS; i++) { + float4 ghostColor = GhostColors[i]; + float ghostScale = GetGhostScale(i); + + if (abs(ghostColor.a * ghostScale) < 0.00001f) + continue; + + float2 ghostVector = radiantVector * ghostScale; + + // Local mask + float distanceMask = 1.0f - length(ghostVector); + float weight; + if (GLocalMask) { + float mask1 = smoothstep(0.5f, 0.9f, distanceMask); + float mask2 = smoothstep(0.75f, 1.0f, distanceMask) * 0.95f + 0.05f; + weight = mask1 * mask2; + } else { + weight = distanceMask; + } + + float4 s = SampleCA(InputTexture, BorderSampler, ghostVector + 0.5f, 8.0f * GhostChromaShift, 0); + color += s.rgb * ghostColor.rgb * ghostColor.a * weight; + } + + // Screen border mask + float2 screenPos = uv * 2.0f - 1.0f; + float screenBorderMask = DiscMask(screenPos * 0.9f); + color *= screenBorderMask * GhostStrength; + } + + // --- Halo with fisheye distortion --- + if (HaloStrength > EPSILON) { + float2 fishUV = FisheyeUV(uv, HaloCompression, 1.0f); + float2 haloVector = normalize(0.5f - uv) * HaloWidth; + + // Halo mask + float haloMask = distance(uv, 0.5f); + haloMask = saturate(haloMask * 2.0f); + haloMask = smoothstep(HaloRadius, 1.0f, haloMask); + + // Screen border mask + float2 screenPos = uv * 2.0f - 1.0f; + float screenBorderMask = DiscMask(screenPos) * DiscMask(screenPos * 0.8f); + screenBorderMask = screenBorderMask * 0.95f + 0.05f; + + // Chromatic aberration sampling on fisheye-distorted UVs + float2 uvR = (fishUV - 0.5f) * (1.0f + HaloChromaShift) + 0.5f + haloVector; + float2 uvG = fishUV + haloVector; + float2 uvB = (fishUV - 0.5f) * (1.0f - HaloChromaShift) + 0.5f + haloVector; + + float3 haloColor; + haloColor.r = InputTexture.SampleLevel(BorderSampler, uvR, 0).r; + haloColor.g = InputTexture.SampleLevel(BorderSampler, uvG, 0).g; + haloColor.b = InputTexture.SampleLevel(BorderSampler, uvB, 0).b; + + color += haloColor * screenBorderMask * haloMask * HaloStrength; + } + + OutputTexture[DTid.xy] = float4(color, 1.0f); +} + +// ============================================================ +// Pass 3a: Kawase blur downsample — half res → quarter res +// Proper UV-based sampling between actual different-resolution textures +// ============================================================ +[numthreads(8, 8, 1)] void CSFlareDown(uint3 DTid : SV_DispatchThreadID) { + if (DTid.x >= (uint)OutputWidth || DTid.y >= (uint)OutputHeight) + return; + + float2 uv = (DTid.xy + 0.5f) / float2(OutputWidth, OutputHeight); + float2 halfPixel = 0.5f / float2(InputWidth, InputHeight); + + // 5-tap Kawase downsample: center (weight 4) + 4 diagonals + float4 color = InputTexture.SampleLevel(ColorSampler, uv, 0) * 4.0f; + color += InputTexture.SampleLevel(ColorSampler, uv + float2(-halfPixel.x, halfPixel.y), 0); + color += InputTexture.SampleLevel(ColorSampler, uv + float2(halfPixel.x, halfPixel.y), 0); + color += InputTexture.SampleLevel(ColorSampler, uv + float2(halfPixel.x, -halfPixel.y), 0); + color += InputTexture.SampleLevel(ColorSampler, uv + float2(-halfPixel.x, -halfPixel.y), 0); + + OutputTexture[DTid.xy] = color * 0.125f; +} + + // ============================================================ + // Pass 3b: Kawase blur upsample — quarter res → half res + // ============================================================ + [numthreads(8, 8, 1)] void CSFlareUp(uint3 DTid : SV_DispatchThreadID) +{ + if (DTid.x >= (uint)OutputWidth || DTid.y >= (uint)OutputHeight) + return; + + float2 uv = (DTid.xy + 0.5f) / float2(OutputWidth, OutputHeight); + float2 halfPixel = 0.5f / float2(InputWidth, InputHeight); + + // 12-tap Kawase upsample: 4 diagonals (weight 1) + 4 axis (weight 2) + float4 color = 0; + color += InputTexture.SampleLevel(ColorSampler, uv + float2(-halfPixel.x, halfPixel.y), 0); + color += InputTexture.SampleLevel(ColorSampler, uv + float2(halfPixel.x, halfPixel.y), 0); + color += InputTexture.SampleLevel(ColorSampler, uv + float2(halfPixel.x, -halfPixel.y), 0); + color += InputTexture.SampleLevel(ColorSampler, uv + float2(-halfPixel.x, -halfPixel.y), 0); + + color += InputTexture.SampleLevel(ColorSampler, uv + float2(-halfPixel.x, 0), 0) * 2.0f; + color += InputTexture.SampleLevel(ColorSampler, uv + float2(halfPixel.x, 0), 0) * 2.0f; + color += InputTexture.SampleLevel(ColorSampler, uv + float2(0, halfPixel.y), 0) * 2.0f; + color += InputTexture.SampleLevel(ColorSampler, uv + float2(0, -halfPixel.y), 0) * 2.0f; + + OutputTexture[DTid.xy] = color / 12.0f; +} + +// ============================================================ +// ============================================================ +// Pass 4: CSMix — combine ghost+halo, apply tint & gradient +// Input: ghost+halo (t0), Output: full-res final flare (u0) +// Uses InputWidth/InputHeight for sampling half-res inputs +// Bicubic Catmull-Rom upsampling for sharper ghost reproduction +// ============================================================ + +float3 SampleBicubicCatmullRom(Texture2D tex, SamplerState samp, float2 uv, float2 texSize) +{ + float2 samplePos = uv * texSize; + float2 tc = floor(samplePos - 0.5) + 0.5; + float2 f = samplePos - tc; + float2 f2 = f * f; + float2 f3 = f2 * f; + + // Catmull-Rom weights + float2 w0 = f2 - 0.5 * (f3 + f); + float2 w1 = 1.5 * f3 - 2.5 * f2 + 1.0; + float2 w2 = -1.5 * f3 + 2.0 * f2 + 0.5 * f; + float2 w3 = 0.5 * (f3 - f2); + + // Collapse to 4 bilinear taps + float2 w12 = w1 + w2; + float2 tc0 = (tc - 1.0) / texSize; + float2 tc12 = (tc + w2 / w12) / texSize; + float2 tc3 = (tc + 2.0) / texSize; + + float3 result = + tex.SampleLevel(samp, float2(tc12.x, tc0.y), 0).rgb * (w12.x * w0.y) + + tex.SampleLevel(samp, float2(tc0.x, tc12.y), 0).rgb * (w0.x * w12.y) + + tex.SampleLevel(samp, float2(tc12.x, tc12.y), 0).rgb * (w12.x * w12.y) + + tex.SampleLevel(samp, float2(tc3.x, tc12.y), 0).rgb * (w3.x * w12.y) + + tex.SampleLevel(samp, float2(tc12.x, tc3.y), 0).rgb * (w12.x * w3.y); + + // The 4 corner taps have very small weights, skip for performance + return max(result, 0.0); +} + +[numthreads(8, 8, 1)] void CSMix(uint3 DTid : SV_DispatchThreadID) { + if (DTid.x >= (uint)OutputWidth || DTid.y >= (uint)OutputHeight) + return; + + float2 uv = (DTid.xy + 0.5f) / float2(OutputWidth, OutputHeight); + + float3 flares = SampleBicubicCatmullRom(InputTexture, ColorSampler, uv, float2(InputWidth, InputHeight)); + + // Procedural radial gradient based on distance from center + float gradientT = saturate(distance(uv, 0.5f) * 2.0f); + float3 gradient = lerp(1.0f, Tint, gradientT); + + float3 result = flares * gradient * Intensity; + + OutputTexture[DTid.xy] = float4(result, 1.0f); +} diff --git a/features/Post Processing/Shaders/PostProcessing/LensFlare/lensflare_fft.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/LensFlare/lensflare_fft.cs.hlsl new file mode 100644 index 0000000000..b7586bc8f1 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/LensFlare/lensflare_fft.cs.hlsl @@ -0,0 +1,437 @@ +// Lens Flare — FFT Ghost Convolution Pipeline +// Community Shaders / Post Processing +// +// Self-contained FFT pipeline for convolving lens flare ghosts with bokeh shapes. +// Uses LensFlareConstants CB layout (shared with lensflare.cs.hlsl). +// +// Shaders: +// CS_FFT — Stockham radix-2 FFT (row/col, forward/inverse via defines) +// CS_Multiply — Frequency-domain complex multiply (scene × bokeh kernel) +// CSBokehPrepare — Sample bokeh texture → zero-padded N×N RG32F with FFT-shift +// CSFFTThreshold — Convert half-res threshold to N×N RG32F for FFT input +// CSFFTGhostCompose — IFFT result → multi-scale ghost sampling + halo + tint + +#include "PostProcessing/common.hlsli" + +static const float PI = 3.14159265358979323846; +static const float EPSILON = 1e-6; +static const int NUM_GHOSTS = 8; + +// ============================================================ +// Resources +// ============================================================ + +Texture2D InputTexture : register(t0); // Context-dependent (bokeh texture, threshold, FFT result) +Texture2D FlareTexture : register(t1); // Secondary input (threshold texture for compose) +SamplerState BokehSampler : register(s0); +SamplerState BorderSampler : register(s1); + +// Complex textures (RG32F: R=real, G=imaginary) +Texture2D TexComplexA : register(t0); // FFT input A (for multiply: scene FFT) +Texture2D TexComplexB : register(t1); // FFT input B (for multiply: bokeh FFT) + +RWTexture2D RWTexComplex : register(u0); // Complex output (RG32F) +RWTexture2D RWTexColor : register(u0); // Color output (RGBA16F, for compose) + +// ============================================================ +// Constant Buffer — matches LensFlareConstants in LensFlare.h +// ============================================================ + +cbuffer LensFlareConstants : register(b1) +{ + float OutputWidth; + float OutputHeight; + float InputWidth; + float InputHeight; + + float ThresholdLevel; + float ThresholdRange; + float GhostStrength; + float GhostChromaShift; + + float HaloStrength; + float HaloRadius; + float HaloWidth; + float HaloCompression; + + float HaloChromaShift; + float Intensity; + uint FFTResolution; + int GLocalMask; + + float3 Tint; + float KernelScale; + + float AspectRatio; + int ApertureBlades; + float ApertureRotation; + float PadScale; + + uint ActiveGhostMask; + float ApertureSize; + float2 _pad0; + + float4 GhostColors[NUM_GHOSTS]; + float4 GhostScalesPacked[2]; + float4 GhostKernelScalesPacked[2]; +} + +float GetGhostScale(int i) +{ + return GhostScalesPacked[i / 4][i % 4]; +} + +float GetGhostKernelScale(int i) +{ + return GhostKernelScalesPacked[i / 4][i % 4]; +} + +// Convert screen UV to FFT UV (aspect-corrected + zero-padding) +float2 ScreenToFFT(float2 screenUV) +{ + float2 fftUV = screenUV; + if (AspectRatio > 1.0) + fftUV.y = (screenUV.y - 0.5) / AspectRatio + 0.5; + else if (AspectRatio < 1.0) + fftUV.x = (screenUV.x - 0.5) * AspectRatio + 0.5; + // Apply zero-padding: scene occupies center PadScale fraction + fftUV = (fftUV - 0.5) * PadScale + 0.5; + return fftUV; +} + +// ============================================================ +// CS_FFT — Stockham radix-2 FFT (row/column pass) +// Compiled with defines: ROW_PASS/COL_PASS + FORWARD/INVERSE +// Dispatch: (N, 1, 1) where N = FFTResolution +// ============================================================ + +#define MAX_FFT_SIZE 1024 + +groupshared float2 gs_buffer0[MAX_FFT_SIZE]; +groupshared float2 gs_buffer1[MAX_FFT_SIZE]; + +float2 ComplexMul(float2 a, float2 b) +{ + return float2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); +} + +float2 Twiddle(uint k, uint N) +{ +#ifdef INVERSE + float angle = 2.0 * PI * float(k) / float(N); +#else + float angle = -2.0 * PI * float(k) / float(N); +#endif + float s, c; + sincos(angle, s, c); + return float2(c, s); +} + +[numthreads(1024, 1, 1)] void CS_FFT(uint3 groupId : SV_GroupID, uint threadIdx : SV_GroupThreadID) { + uint lineIdx = groupId.x; + uint N = FFTResolution; + bool active = (threadIdx < N); + + // Load + if (active) { + uint2 readPos; +#ifdef ROW_PASS + readPos = uint2(threadIdx, lineIdx); +#else + readPos = uint2(lineIdx, threadIdx); +#endif + gs_buffer0[threadIdx] = TexComplexA[readPos]; + } + GroupMemoryBarrierWithGroupSync(); + + // Bit-reversal permutation + if (active) { + uint bits = firstbithigh(N) - firstbithigh(1); + uint rev = 0; + uint tmp = threadIdx; + for (uint b = 0; b < bits; b++) { + rev = (rev << 1) | (tmp & 1); + tmp >>= 1; + } + gs_buffer1[rev] = gs_buffer0[threadIdx]; + } + GroupMemoryBarrierWithGroupSync(); + + if (active) + gs_buffer0[threadIdx] = gs_buffer1[threadIdx]; + GroupMemoryBarrierWithGroupSync(); + + // Cooley-Tukey butterfly + for (uint stage = 1; stage < N; stage <<= 1) { + if (active) { + uint halfStage = stage; + uint blockIdx = threadIdx / (halfStage * 2); + uint blockOffset = threadIdx % (halfStage * 2); + + if (blockOffset < halfStage) { + uint topIdx = blockIdx * halfStage * 2 + blockOffset; + uint botIdx = topIdx + halfStage; + float2 tw = Twiddle(blockOffset * (N / (halfStage * 2)), N); + float2 top = gs_buffer0[topIdx]; + float2 bot = ComplexMul(tw, gs_buffer0[botIdx]); + gs_buffer0[topIdx] = top + bot; + gs_buffer0[botIdx] = top - bot; + } + } + GroupMemoryBarrierWithGroupSync(); + } + + // Write output + if (active) { + float2 result = gs_buffer0[threadIdx]; +#ifdef INVERSE + result /= float(N); +#endif + uint2 writePos; +#ifdef ROW_PASS + writePos = uint2(threadIdx, lineIdx); +#else + writePos = uint2(lineIdx, threadIdx); +#endif + RWTexComplex[writePos] = result; + } +} + + // ============================================================ + // CS_Multiply — Frequency-domain complex multiplication + // scene_FFT × bokeh_FFT, normalized by DC component + // Dispatch: ((N+7)/8, (N+7)/8, 1) + // ============================================================ + + [numthreads(8, 8, 1)] void CS_Multiply(uint2 tid : SV_DispatchThreadID) +{ + if (tid.x >= FFTResolution || tid.y >= FFTResolution) + return; + + float2 scene = TexComplexA[tid]; + float2 psf = TexComplexB[tid]; + + // Normalize by DC component to preserve brightness + float psfDC = max(TexComplexB[uint2(0, 0)].x, 1e-6); + + float2 result; + result.x = scene.x * psf.x - scene.y * psf.y; + result.y = scene.x * psf.y + scene.y * psf.x; + result /= psfDC; + + RWTexComplex[tid] = result; +} + +// ============================================================ +// CSBokehPrepare — Generate procedural aperture shape into N×N RG32F +// Creates an N-polygon aperture (like Physical Glare) with soft edges, +// zero-pads, and FFT-shifts (center-to-corner). +// u0 = RG32F output (real = aperture transmittance, imag = 0) +// Dispatch: ((N+7)/8, (N+7)/8, 1) +// ============================================================ + +[numthreads(8, 8, 1)] void CSBokehPrepare(uint2 tid : SV_DispatchThreadID) { + uint N = FFTResolution; + if (tid.x >= N || tid.y >= N) + return; + + // Kernel radius based on KernelScale (fraction of N) + float kernelRadius = float(N) * KernelScale * 0.5; + float centerX = float(N) * 0.5; + float centerY = float(N) * 0.5; + + float dx = (float)tid.x - centerX; + float dy = (float)tid.y - centerY; + + // Aperture size scales the polygon relative to the kernel radius + float apertureRadius = kernelRadius * ApertureSize * 2.0; // ApertureSize = 1/FStop + float edgeW = 1.5; // smoothstep half-width in pixels for AA + + float2 result = float2(0, 0); + + // Apply rotation + float2 pos = float2(dx, dy); + if (abs(ApertureRotation) > EPSILON) { + float sr, cr; + sincos(-ApertureRotation, sr, cr); + pos = float2(pos.x * cr - pos.y * sr, pos.x * sr + pos.y * cr); + } + + float dist = length(pos); + + if (ApertureBlades <= 2) { + // Circle fallback + result.x = 1.0 - smoothstep(apertureRadius - edgeW, apertureRadius + edgeW, dist); + } else { + // N-polygon aperture + float sectorAngle = 2.0 * PI / float(ApertureBlades); + float rawAngle = atan2(pos.y, pos.x); + float localAngle = frac(rawAngle / sectorAngle) * sectorAngle - sectorAngle * 0.5; + float apothem = apertureRadius * cos(sectorAngle * 0.5); + float projDist = dist * cos(localAngle); + result.x = 1.0 - smoothstep(apothem - edgeW, apothem + edgeW, projDist); + } + + // FFT-shift: swap quadrants so DC is at corner (0,0) + uint sx = (tid.x + N / 2) % N; + uint sy = (tid.y + N / 2) % N; + + RWTexComplex[uint2(sx, sy)] = result; +} + + // ============================================================ + // CSFFTThreshold — Convert half-res threshold → N×N RG32F + // Centers the threshold image in the FFT buffer with zero padding + // t0 = threshold texture (RGBA float), s0 = sampler + // u0 = RG32F output (real = luminance, imag = 0) + // Dispatch: ((N+7)/8, (N+7)/8, 1) + // ============================================================ + + [numthreads(8, 8, 1)] void CSFFTThreshold(uint2 tid : SV_DispatchThreadID) +{ + uint N = FFTResolution; + if (tid.x >= N || tid.y >= N) + return; + + // Map FFT pixel to threshold texture UV with aspect correction and zero-padding + float2 uv = (float2(tid.xy) + 0.5) / float(N); + + // Undo zero-padding: scene occupies center PadScale fraction + float2 sceneUV = (uv - 0.5) / max(PadScale, 0.01) + 0.5; + + // Preserve scene proportions in square FFT buffer + if (AspectRatio > 1.0) + sceneUV.y = (sceneUV.y - 0.5) * AspectRatio + 0.5; + else if (AspectRatio < 1.0) + sceneUV.x = (sceneUV.x - 0.5) / AspectRatio + 0.5; + + // Outside scene bounds → zero padding + if (any(sceneUV < 0.0) || any(sceneUV > 1.0)) { + RWTexComplex[tid] = float2(0, 0); + return; + } + + float3 color = InputTexture.SampleLevel(BokehSampler, sceneUV, 0).rgb; + float luminance = dot(color, float3(0.333, 0.333, 0.333)); + + RWTexComplex[tid] = float2(luminance, 0); +} + +// ============================================================ +// CSFFTGhostCompose — Compose FFT convolution result into ghost+halo +// Takes IFFT result (RG32F luminance) and applies multi-scale ghost +// sampling with chromatic aberration and tinting, plus halo. +// t0 = FFT convolution result (RG32F: .x = spatial luminance) +// t1 = threshold texture (for halo computation) +// u0 = output ghost+halo texture (RGBA16F, half-res) +// Dispatch: ((halfW+7)/8, (halfH+7)/8, 1) +// ============================================================ + +// Fisheye UV distortion (same as main shader) +float2 FisheyeUV(float2 uv, float compression, float zoom) +{ + float2 negPosUV = 2.0f * uv - 1.0f; + float scale = compression * atan(rcp(compression)); + float radiusDist = length(negPosUV) * scale; + float radiusDir = compression * tan(radiusDist / compression) * zoom; + float phi = atan2(negPosUV.y, negPosUV.x); + float2 newUV = float2(radiusDir * cos(phi) + 1.0f, radiusDir * sin(phi) + 1.0f) * 0.5f; + return newUV; +} + +float DiscMask(float2 screenPos) +{ + return saturate(1.0f - dot(screenPos, screenPos)); +} + +[numthreads(8, 8, 1)] void CSFFTGhostCompose(uint2 tid : SV_DispatchThreadID) { + uint outW = (uint)OutputWidth; + uint outH = (uint)OutputHeight; + if (tid.x >= outW || tid.y >= outH) + return; + + float2 uv = (float2(tid.xy) + 0.5) / float2(OutputWidth, OutputHeight); + float3 color = float3(0, 0, 0); + + // --- Ghost generation using FFT-convolved bokeh result --- + [branch] if (GhostStrength > EPSILON) + { + float2 radiantVector = uv - 0.5; + + for (int i = 0; i < NUM_GHOSTS; i++) { + // Skip ghosts not in current pass mask + if (!(ActiveGhostMask & (1u << (uint)i))) + continue; + + float4 ghostColor = GhostColors[i]; + float ghostScale = GetGhostScale(i); + + if (abs(ghostColor.a * ghostScale) < 0.00001) + continue; + + float2 ghostVector = radiantVector * ghostScale; + + // Local mask + float distanceMask = 1.0 - length(ghostVector); + float weight; + if (GLocalMask) { + float mask1 = smoothstep(0.5, 0.9, distanceMask); + float mask2 = smoothstep(0.75, 1.0, distanceMask) * 0.95 + 0.05; + weight = mask1 * mask2; + } else { + weight = distanceMask; + } + + // Ghost UV in screen space + float2 ghostUV = ghostVector + 0.5; + + // Chromatic aberration in screen space + float chromaOffset = GhostChromaShift * 8.0; + float2 dir = normalize(ghostVector + 0.0001); + float2 pixelOffset = dir * chromaOffset / float2(OutputWidth, OutputHeight); + float2 uvR = ghostUV + pixelOffset; + float2 uvG = ghostUV; + float2 uvB = ghostUV - pixelOffset; + + // Convert to FFT space (aspect + pad corrected) and sample IFFT result + float sR = InputTexture.SampleLevel(BokehSampler, ScreenToFFT(uvR), 0).x; + float sG = InputTexture.SampleLevel(BokehSampler, ScreenToFFT(uvG), 0).x; + float sB = InputTexture.SampleLevel(BokehSampler, ScreenToFFT(uvB), 0).x; + + color += float3(sR, sG, sB) * ghostColor.rgb * ghostColor.a * weight; + } + + // Screen border mask + float2 screenPos = uv * 2.0 - 1.0; + float screenBorderMask = DiscMask(screenPos * 0.9); + color *= screenBorderMask * GhostStrength; + } + + // --- Halo (samples from IFFT result for bokeh-shaped halo) --- + if (HaloStrength > EPSILON) { + float2 fishUV = FisheyeUV(uv, HaloCompression, 1.0); + float2 haloVector = normalize(0.5 - uv) * HaloWidth; + float haloMask = distance(uv, 0.5); + haloMask = saturate(haloMask * 2.0); + haloMask = smoothstep(HaloRadius, 1.0, haloMask); + + float2 screenPos = uv * 2.0 - 1.0; + float screenBorderMask = DiscMask(screenPos) * DiscMask(screenPos * 0.8); + screenBorderMask = screenBorderMask * 0.95 + 0.05; + + float2 uvR = (fishUV - 0.5) * (1.0 + HaloChromaShift) + 0.5 + haloVector; + float2 uvG = fishUV + haloVector; + float2 uvB = (fishUV - 0.5) * (1.0 - HaloChromaShift) + 0.5 + haloVector; + + // Sample halo from IFFT result (aspect + pad corrected) for bokeh-shaped halo + float3 haloColor; + haloColor.r = InputTexture.SampleLevel(BokehSampler, ScreenToFFT(uvR), 0).x; + haloColor.g = InputTexture.SampleLevel(BokehSampler, ScreenToFFT(uvG), 0).x; + haloColor.b = InputTexture.SampleLevel(BokehSampler, ScreenToFFT(uvB), 0).x; + + color += haloColor * screenBorderMask * haloMask * HaloStrength; + } + + // Additive write (supports multi-pass for Ultra mode) + RWTexColor[tid] = RWTexColor[tid] + float4(color, 0.0); +} diff --git a/features/Post Processing/Shaders/PostProcessing/LocalExposure/localexposure.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/LocalExposure/localexposure.cs.hlsl new file mode 100644 index 0000000000..00b8bd9838 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/LocalExposure/localexposure.cs.hlsl @@ -0,0 +1,201 @@ +/// Local Exposure Compute Shader +/// Exposure-fusion local adaptation adapted to output a raw-HDR multiplier +/// consumed later by Composite. +/// +/// Raw scene color is normalized with global exposure when available so the +/// exposure-fusion weights operate in a stable perceptual luminance range. +/// +/// Reference: +/// https://bartwronski.com/2022/02/28/exposure-fusion-local-tonemapping-for-real-time-rendering/ + +#include "Common/Color.hlsli" + +cbuffer LocalExposureCB : register(b1) +{ + float ManualExposure; + float HighlightExposure; + float ShadowExposure; + float ExposurePreferenceSigmaSq; + + uint InputWidth; + uint InputHeight; + uint MipLevel; + uint DisplayMip; + + uint CurrentMip; + uint HasCoarserMip; + uint BoostLocalContrast; + uint UseGlobalExposure; + + float ExposureCompensation; + float AdaptationMin; + float AdaptationMax; + float DarkThreshold; +}; + +Texture2D TexInput0 : register(t0); +Texture2D TexInput1 : register(t1); +Texture2D TexInput2 : register(t2); +Texture2D TexInput3 : register(t3); +StructuredBuffer TexAdaptation : register(t4); +SamplerState LinearSampler : register(s0); + +RWTexture2D RWTexOutput0 : register(u0); +RWTexture2D RWTexOutput1 : register(u1); +RWTexture2D RWTexOutputFloat : register(u2); + +float GetPreExposure() +{ + if (UseGlobalExposure != 0) { + float adaptedLum = clamp(max(TexAdaptation[0], 1e-5), AdaptationMin, AdaptationMax); + return 0.18 * ExposureCompensation / adaptedLum; + } + + return ManualExposure; +} + +float LinearLuminance(float3 preExposedColor) +{ + return max(Color::RGBToLuminance(max(preExposedColor, 0.0)), 1e-5); +} + +float ExposureFusionTonemap(float linearLum) +{ + linearLum = max(linearLum, 0.0); + return sqrt(linearLum / (1.0 + linearLum)); +} + +float ExposureFusionInverseTonemap(float tonemappedLum) +{ + float value = saturate(tonemappedLum); + value *= value; + return value / max(1.0 - value, 1e-4); +} + +float ExposureFusionLuminance(float3 preExposedColor, float exposureScale) +{ + return ExposureFusionTonemap(LinearLuminance(preExposedColor) * exposureScale); +} + +float3 NormalizeWeights(float3 weights) +{ + return weights / (weights.x + weights.y + weights.z + 0.00001); +} + +[numthreads(8, 8, 1)] void CSSetup(uint2 tid : SV_DispatchThreadID) { + if (tid.x >= InputWidth || tid.y >= InputHeight) + return; + + float3 preExposedColor = TexInput0[tid].rgb * GetPreExposure(); + + float highlightLum = ExposureFusionLuminance(preExposedColor, HighlightExposure); + float midLum = ExposureFusionLuminance(preExposedColor, 1.0); + float shadowLum = ExposureFusionLuminance(preExposedColor, ShadowExposure); + float3 lums = float3(highlightLum, midLum, shadowLum); + + float3 diff = lums - 0.5; + float3 weights = exp(-0.5 * diff * diff * ExposurePreferenceSigmaSq); + + RWTexOutput0[tid] = float4(lums, 1.0); + RWTexOutput1[tid] = float4(NormalizeWeights(weights), 1.0); +} + + [numthreads(8, 8, 1)] void CSDownsample(uint2 tid : SV_DispatchThreadID) +{ + uint2 outDims; + RWTexOutput0.GetDimensions(outDims.x, outDims.y); + + if (any(tid >= outDims)) + return; + + float2 uv = (float2(tid) + 0.5) / float2(outDims); + RWTexOutput0[tid] = TexInput0.SampleLevel(LinearSampler, uv, 0); + RWTexOutput1[tid] = TexInput1.SampleLevel(LinearSampler, uv, 0); +} + +[numthreads(8, 8, 1)] void CSBlend(uint2 tid : SV_DispatchThreadID) { + uint2 outDims; + RWTexOutputFloat.GetDimensions(outDims.x, outDims.y); + + if (any(tid >= outDims)) + return; + + float3 exposures = TexInput0[tid].rgb; + float3 weights = TexInput1[tid].rgb; + float prevResult = 0.0; + + if (HasCoarserMip != 0) { + float2 uv = (float2(tid) + 0.5) / float2(outDims); + float3 coarserExposures = TexInput2.SampleLevel(LinearSampler, uv, 0).rgb; + exposures -= coarserExposures; + prevResult = TexInput3.SampleLevel(LinearSampler, uv, 0).r; + + if (BoostLocalContrast != 0) + weights *= abs(exposures) + 0.00001; + } + + weights = NormalizeWeights(weights); + RWTexOutputFloat[tid] = prevResult + dot(exposures, weights); +} + + [numthreads(8, 8, 1)] void CSComputeExposure(uint2 tid : SV_DispatchThreadID) +{ + if (tid.x >= InputWidth || tid.y >= InputHeight) + return; + + float2 uv = (float2(tid) + 0.5) / float2(InputWidth, InputHeight); + + uint2 displayDims; + TexInput2.GetDimensions(displayDims.x, displayDims.y); + float2 displayPixelSize = 1.0 / float2(displayDims); + + float momentX = 0.0; + float momentY = 0.0; + float momentX2 = 0.0; + float momentXY = 0.0; + float ws = 0.0; + + [unroll] for (int dy = -1; dy <= 1; dy++) + { + [unroll] for (int dx = -1; dx <= 1; dx++) + { + float2 sampleUV = uv + float2(dx, dy) * displayPixelSize; + sampleUV = clamp(sampleUV, displayPixelSize * 0.5, 1.0 - displayPixelSize * 0.5); + + float x = TexInput1.SampleLevel(LinearSampler, sampleUV, 0).g; + float y = TexInput2.SampleLevel(LinearSampler, sampleUV, 0).r; + float w = exp(-0.5 * float(dx * dx + dy * dy) / (0.7 * 0.7)); + + momentX += x * w; + momentY += y * w; + momentX2 += x * x * w; + momentXY += x * y * w; + ws += w; + } + } + + momentX /= ws; + momentY /= ws; + momentX2 /= ws; + momentXY /= ws; + + float A = (momentXY - momentX * momentY) / (max(momentX2 - momentX * momentX, 0.0) + 0.00001); + float B = momentY - A * momentX; + + float3 preExposedColor = TexInput0[tid].rgb * GetPreExposure(); + float linearLuminance = LinearLuminance(preExposedColor); + float guideLuminance = ExposureFusionTonemap(linearLuminance); + float fusedLuminance = max(A * guideLuminance + B, 0.0); + float localExposure = ExposureFusionInverseTonemap(fusedLuminance) / linearLuminance; + + float shadowProtection = 1.0 - smoothstep(0.045, 0.18, linearLuminance); + localExposure = lerp(localExposure, max(localExposure, 1.0), shadowProtection); + + localExposure = guideLuminance > DarkThreshold ? localExposure : + lerp(1.0, localExposure, (guideLuminance / DarkThreshold) * (guideLuminance / DarkThreshold)); + + if (UseGlobalExposure == 0) + localExposure *= ManualExposure; + + RWTexOutputFloat[tid] = localExposure; +} diff --git a/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_blurpass.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_blurpass.cs.hlsl new file mode 100644 index 0000000000..2eb199ca6b --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_blurpass.cs.hlsl @@ -0,0 +1,209 @@ +/** + * Motion Blur - Blur Pass (Pass 3 of 3) + * + * Final pass that applies motion blur using neighborhood velocities + * Uses depth-aware sampling to prevent foreground/background bleeding + */ + +#include "Common/FrameBuffer.hlsli" +#include "Common/MotionBlur.hlsli" +#include "PostProcessing/common.hlsli" + +// Textures and buffers +Texture2D TexColor : register(t0); // Color buffer +Texture2D TexVelocity : register(t1); // Motion vectors +Texture2D TexNeighborMax : register(t2); // Neighborhood velocities (20×20 grid) +Texture2D TexDepth : register(t3); // Depth buffer +RWTexture2D RWTexOut : register(u0); // Output + +// Samplers +SamplerState LinearSampler : register(s0); +SamplerState PointSampler : register(s1); + +// Constants +cbuffer MotionBlurCB : register(b0) +{ + float4 g_VelocityParams; // x: velocity multiplier, yz: motion-vector source/display scale + int g_SampleCount; // Number of samples +} + +// Fixed parameters +static const uint GRID_SIZE = 20; // Fixed grid size +static const float g_MaxBlurRadius = 40.0f; +#define PI 3.14159265359f +#define MB_SOFTZ_INCHES 1.0f + +// Extract velocity from encoded color +float2 ExtractVelocity(float4 colorSample) +{ + // Direction from R,G channels + float2 dir; + dir.x = (colorSample.r * 2.0f) - 1.0f; + dir.y = (colorSample.g * 2.0f) - 1.0f; + + float dirLength = length(dir); + if (dirLength > 0.001f) + dir /= dirLength; + + // Magnitude from B channel + float magnitude = colorSample.b * 20.0f; + + return dir * magnitude; +} + +// Depth comparison - returns (foreground, background) weights +float2 DepthCmp(float centerDepth, float sampleDepth, float depthScale) +{ + return saturate(0.5f + float2(depthScale, -depthScale) * (sampleDepth - centerDepth)); +} + +// Spread comparison +float2 SpreadCmp(float offsetLen, float2 spreadLen, float pixelToSampleUnitsScale) +{ + return saturate(pixelToSampleUnitsScale * spreadLen - max(offsetLen - 1.0f, 0.0f)); +} + +// Calculate sample weight +float SampleWeight( + float centerDepth, float sampleDepth, float offsetLen, float centerSpreadLen, + float sampleSpreadLen, float pixelToSampleUnitsScale, float depthScale) +{ + float2 depthCmp = DepthCmp(centerDepth, sampleDepth, depthScale); + float2 spreadCmp = SpreadCmp(offsetLen, float2(centerSpreadLen, sampleSpreadLen), pixelToSampleUnitsScale); + return dot(depthCmp, spreadCmp); +} + +// Generate dithered offset +float GetDitheredOffset(uint2 position, float sampleIndex) +{ + float scale = 0.25f; + uint2 positionMod = position & 1; // 2x2 checkerboard + return (-scale + 2.0f * scale * positionMod.x) * (-1.0f + 2.0f * positionMod.y); +} + +float2 GetVelocityTexCoord(float2 targetTexCoord) +{ + uint2 targetDimensions; + TexColor.GetDimensions(targetDimensions.x, targetDimensions.y); + + uint2 velocityDimensions; + TexVelocity.GetDimensions(velocityDimensions.x, velocityDimensions.y); + + float2 targetToVelocityScale = g_VelocityParams.yz; + float2 sourceDimensions = max(float2(1.0f, 1.0f), float2(targetDimensions) * targetToVelocityScale); + sourceDimensions = min(sourceDimensions, float2(velocityDimensions)); + + float2 uvScale = sourceDimensions / float2(velocityDimensions); + float2 minUV = 0.5f / float2(velocityDimensions); + float2 maxUV = (sourceDimensions - 0.5f) / float2(velocityDimensions); + + return clamp(targetTexCoord * uvScale, minUV, maxUV); +} + +// Main function +[numthreads(8, 8, 1)] void main(uint3 DTid : SV_DispatchThreadID) { + // Get dimensions and check bounds + uint2 dimensions; + TexColor.GetDimensions(dimensions.x, dimensions.y); + + if (DTid.x >= dimensions.x || DTid.y >= dimensions.y) + return; + + uint2 pixelPos = DTid.xy; + + // Sample center pixel data + float2 texCoord = (pixelPos + 0.5f) / float2(dimensions); + float4 centerColor = TexColor.SampleLevel(LinearSampler, texCoord, 0); + float centerDepth = TexDepth.SampleLevel(PointSampler, texCoord, 0); + float2 centerVelocity = TexVelocity.SampleLevel(PointSampler, GetVelocityTexCoord(texCoord), 0).xy; + + centerVelocity *= g_VelocityParams.x; + + // Calculate tile coordinates + uint2 gridCoord; + gridCoord.x = (pixelPos.x * GRID_SIZE) / dimensions.x; + gridCoord.y = (pixelPos.y * GRID_SIZE) / dimensions.y; + + uint2 neighborMaxDimensions; + TexNeighborMax.GetDimensions(neighborMaxDimensions.x, neighborMaxDimensions.y); + + gridCoord = min(gridCoord, neighborMaxDimensions - 1); + + // Get max velocity from grid + float4 neighborMaxSample = TexNeighborMax[gridCoord]; + float2 neighborMaxVelocity = ExtractVelocity(neighborMaxSample); + + // Determine blur direction and length + float2 blurDir = length(neighborMaxVelocity) > 0.001f ? normalize(neighborMaxVelocity) : float2(0.0f, 0.0f); + // Scale blur length down for higher velocity scales to prevent over-blurring + float blurLength = min(length(neighborMaxVelocity) / sqrt(g_VelocityParams.x / 300.0f), g_MaxBlurRadius); + + // Skip if no motion + if (blurLength < 0.5f) { + RWTexOut[pixelPos] = centerColor; + return; + } + + float centerVelocityLen = length(centerVelocity); + + // Initialize for sampling + float4 sum = float4(0.0f, 0.0f, 0.0f, 0.0f); + float pixelToSampleUnitsScale = float(g_SampleCount) / blurLength; + + // Sample in pairs (mirrored) + for (int i = 0; i < g_SampleCount / 2; i++) { + // Calculate offset + float offset = (float(i) + 0.5f) / float(g_SampleCount / 2) * blurLength; + offset += GetDitheredOffset(pixelPos, i); + + // Sample pairs in opposite directions + float2 pixelOffsetFwd = blurDir * offset; + float2 pixelOffsetBck = -pixelOffsetFwd; + + float2 sampleTexCoordsFwd = (pixelPos + pixelOffsetFwd + 0.5f) / float2(dimensions); + float2 sampleTexCoordsBck = (pixelPos + pixelOffsetBck + 0.5f) / float2(dimensions); + + // Sample depth and velocity + float sampleDepthFwd = TexDepth.SampleLevel(PointSampler, sampleTexCoordsFwd, 0); + float sampleDepthBck = TexDepth.SampleLevel(PointSampler, sampleTexCoordsBck, 0); + + float4 rawVelocityDepthFwd = TexVelocity.SampleLevel(PointSampler, GetVelocityTexCoord(sampleTexCoordsFwd), 0); + float4 rawVelocityDepthBck = TexVelocity.SampleLevel(PointSampler, GetVelocityTexCoord(sampleTexCoordsBck), 0); + + float2 sampleVelocityFwd = rawVelocityDepthFwd.xy * g_VelocityParams.x; + float2 sampleVelocityBck = rawVelocityDepthBck.xy * g_VelocityParams.x; + + float sampleVelocityLenFwd = length(sampleVelocityFwd); + float sampleVelocityLenBck = length(sampleVelocityBck); + + float offsetLen = offset; + + // Calculate sample weights + float weightFwd = SampleWeight( + centerDepth, sampleDepthFwd, offsetLen, centerVelocityLen, sampleVelocityLenFwd, + pixelToSampleUnitsScale, MB_SOFTZ_INCHES); + + float weightBck = SampleWeight( + centerDepth, sampleDepthBck, offsetLen, centerVelocityLen, sampleVelocityLenBck, + pixelToSampleUnitsScale, MB_SOFTZ_INCHES); + + // Sample colors and accumulate + float4 sampleColorFwd = TexColor.SampleLevel(LinearSampler, sampleTexCoordsFwd, 0); + float4 sampleColorBck = TexColor.SampleLevel(LinearSampler, sampleTexCoordsBck, 0); + + sum += weightFwd * float4(sampleColorFwd.rgb, 1.0f); + sum += weightBck * float4(sampleColorBck.rgb, 1.0f); + } + + // Normalize + sum.rgb *= 1.0f / float(g_SampleCount); + sum.w *= 1.0f / float(g_SampleCount); + + // Final blend with background + float4 outputColor = float4( + sum.rgb + (1.0f - sum.w) * centerColor.rgb, + 1.0f); + + // Write output + RWTexOut[pixelPos] = outputColor; +} diff --git a/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_horizontalpass.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_horizontalpass.cs.hlsl new file mode 100644 index 0000000000..cf0f991bf6 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_horizontalpass.cs.hlsl @@ -0,0 +1,93 @@ +/** + * Motion Blur - Horizontal Reduction Pass (Pass 1a of 3) + * + * Performs horizontal reduction from [width x height] to [grid x height] + * Each thread processes a horizontal strip of input pixels + * First stage of the separable approach for maximum velocity calculation + */ + +#include "PostProcessing/common.hlsli" + +// Textures +Texture2D TexVelocity : register(t0); // Full resolution motion vectors +RWTexture2D RWTexHorizontal : register(u0); // [20 x height] output + +// Constants +cbuffer MotionBlurCB : register(b0) +{ + float4 g_VelocityParams; // x: velocity multiplier, yz: motion-vector source/display scale + float4 g_TargetResolution; // xy: output color resolution +} + +// Fixed parameters +static const uint GRID_SIZE = 20; // Fixed grid size + +uint2 GetVelocitySamplePosition(uint2 targetPixel, uint2 velocityDimensions) +{ + float2 velocityScale = g_VelocityParams.yz; + float2 sourceDimensionsF = max(float2(1.0f, 1.0f), g_TargetResolution.xy * velocityScale); + uint2 sourceDimensions = min(velocityDimensions, uint2(sourceDimensionsF + 0.5f)); + uint2 sourcePixel = uint2((float2(targetPixel) + 0.5f) * velocityScale); + return min(sourcePixel, sourceDimensions - 1); +} + +// Process horizontal strips +[numthreads(8, 8, 1)] void main(uint3 DTid : SV_DispatchThreadID) { + // Get dimensions and check bounds + uint2 velocityDimensions; + TexVelocity.GetDimensions(velocityDimensions.x, velocityDimensions.y); + + uint2 targetDimensions = uint2(max(g_TargetResolution.xy, float2(1.0f, 1.0f)) + 0.5f); + + if (DTid.y >= targetDimensions.y) + return; + + uint2 horizontalDimensions; + RWTexHorizontal.GetDimensions(horizontalDimensions.x, horizontalDimensions.y); + + if (DTid.x >= horizontalDimensions.x || DTid.y >= horizontalDimensions.y) + return; + + // Calculate horizontal range to process + uint xTileSize = (targetDimensions.x + GRID_SIZE - 1) / GRID_SIZE; + uint xStart = DTid.x * xTileSize; + uint xEnd = min(xStart + xTileSize, targetDimensions.x); + + // Track maximum velocity + float maxVelocityMagnitude = 0.0f; + float2 maxVelocity = float2(0.0f, 0.0f); + + // Process horizontal strip + for (uint x = xStart; x < xEnd; x++) { + // Get velocity from motion vector buffer + uint2 velocityPixel = GetVelocitySamplePosition(uint2(x, DTid.y), velocityDimensions); + float2 velocity = TexVelocity[velocityPixel].xy; + + // Apply velocity scale + velocity *= g_VelocityParams.x; + + // Keep largest velocity + float velocityMagnitude = length(velocity); + if (velocityMagnitude > maxVelocityMagnitude) { + maxVelocityMagnitude = velocityMagnitude; + maxVelocity = velocity; + } + } + + // Encode velocity for output + // R: normalized x direction mapped to 0..1 + // G: normalized y direction mapped to 0..1 + // B: velocity magnitude scaled to 0..1 + float2 normalizedDir = maxVelocityMagnitude > 0.001f ? normalize(maxVelocity) : float2(0.0f, 0.0f); + float encodedMagnitude = saturate(maxVelocityMagnitude * 0.05f); + + // Create output color + float4 outputColor = float4( + normalizedDir.x * 0.5f + 0.5f, + normalizedDir.y * 0.5f + 0.5f, + encodedMagnitude, + 1.0f); + + // Write to horizontal pass texture + RWTexHorizontal[uint2(DTid.x, DTid.y)] = outputColor; +} diff --git a/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_neighborpass.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_neighborpass.cs.hlsl new file mode 100644 index 0000000000..d9ec4da094 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_neighborpass.cs.hlsl @@ -0,0 +1,110 @@ +/** + * Motion Blur - Neighbor Max Pass (Pass 2 of 3) + * + * Examines 3x3 grid neighborhoods to find maximum velocities + * Handles diagonal neighbors by checking if velocity points toward current cell + * Outputs final neighborhood-aware velocity data for blur pass + */ + +#include "PostProcessing/common.hlsli" + +// Textures +Texture2D TexVertical : register(t0); // [20×20] input from vertical pass +RWTexture2D RWTexNeighborMax : register(u0); // [20×20] output + +// Constants +cbuffer MotionBlurCB : register(b0) +{ + float g_VelocityScale; // Velocity multiplier +} + +// Fixed parameters +static const uint GRID_SIZE = 20; // Fixed grid size + +// Extract velocity from encoded color +float2 ExtractVelocity(float4 colorSample) +{ + // Extract direction from R,G channels + float2 dir; + dir.x = (colorSample.r * 2.0f) - 1.0f; + dir.y = (colorSample.g * 2.0f) - 1.0f; + + // Normalize if needed + float dirLength = length(dir); + if (dirLength > 0.001f) + dir /= dirLength; + + // Get magnitude from B channel + float magnitude = colorSample.b * 20.0f; + + return dir * magnitude; +} + +// Process one cell in the grid +[numthreads(8, 8, 1)] void main(uint3 DTid : SV_DispatchThreadID) { + // Check bounds + if (DTid.x >= GRID_SIZE || DTid.y >= GRID_SIZE) + return; + + uint2 cellIndex = DTid.xy; + + // Track maximum velocity + float2 maxVelocity = float2(0.0f, 0.0f); + float maxVelocityMagnitude = 0.0f; + + // Check 3x3 neighborhood + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + // Get neighbor coordinates + int2 neighborCellIndex = int2(cellIndex) + int2(x, y); + + // Skip out-of-bounds neighbors + if (neighborCellIndex.x < 0 || neighborCellIndex.y < 0 || + neighborCellIndex.x >= GRID_SIZE || + neighborCellIndex.y >= GRID_SIZE) + continue; + + // Get neighbor velocity + float4 neighborColor = TexVertical[neighborCellIndex]; + float2 neighborVelocity = ExtractVelocity(neighborColor); + + // For diagonal neighbors, only include if velocity points toward current cell + bool isDiagonal = (x != 0 && y != 0); + if (isDiagonal) { + // Direction from neighbor to current cell + float2 dirToCurrentCell = normalize(float2(-x, -y)); + + // Neighbor velocity direction + float2 neighborDir = normalize(neighborVelocity); + + // Skip if velocity doesn't point toward current cell + if (dot(neighborDir, dirToCurrentCell) <= 0.0f) + continue; + } + + // Keep largest velocity + float neighborVelocityMagnitude = length(neighborVelocity); + if (neighborVelocityMagnitude > maxVelocityMagnitude) { + maxVelocityMagnitude = neighborVelocityMagnitude; + maxVelocity = neighborVelocity; + } + } + } + + // Encode velocity for output + // R: normalized x direction mapped to 0..1 + // G: normalized y direction mapped to 0..1 + // B: velocity magnitude scaled to 0..1 + float2 normalizedDir = maxVelocityMagnitude > 0.001f ? normalize(maxVelocity) : float2(0.0f, 0.0f); + float encodedMagnitude = saturate(maxVelocityMagnitude * 0.05f); + + // Create output color + float4 outputColor = float4( + normalizedDir.x * 0.5f + 0.5f, + normalizedDir.y * 0.5f + 0.5f, + encodedMagnitude, + 1.0f); + + // Write to grid coordinates + RWTexNeighborMax[DTid.xy] = outputColor; +} \ No newline at end of file diff --git a/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_verticalpass.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_verticalpass.cs.hlsl new file mode 100644 index 0000000000..233cf9cf66 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/MotionBlur/motionblur_verticalpass.cs.hlsl @@ -0,0 +1,93 @@ +/** + * Motion Blur - Vertical Reduction Pass (Pass 1b of 3) + * + * Performs vertical reduction from [grid × height] to [grid × grid] + * Each column of threads processes a vertical strip of the input texture + * Preserves the maximum velocity vectors for each grid cell + */ + +#include "PostProcessing/common.hlsli" + +// Textures +Texture2D TexHorizontal : register(t0); // [20 x height] input +RWTexture2D RWTexVertical : register(u0); // [20 x 20] output + +// Constants +cbuffer MotionBlurCB : register(b0) +{ + float g_VelocityScale; // Velocity multiplier +} + +// Fixed parameters +static const uint GRID_SIZE = 20; // Fixed grid size + +// Extract velocity from encoded color +float2 ExtractVelocity(float4 colorSample) +{ + // Extract direction from R,G channels + float2 dir; + dir.x = (colorSample.r * 2.0f) - 1.0f; + dir.y = (colorSample.g * 2.0f) - 1.0f; + + // Normalize if needed + float dirLength = length(dir); + if (dirLength > 0.001f) + dir /= dirLength; + + // Get magnitude from B channel + float magnitude = colorSample.b * 20.0f; + + return dir * magnitude; +} + +// Vertical reduction pass +[numthreads(8, 8, 1)] void main(uint3 DTid : SV_DispatchThreadID, uint3 GTid : SV_GroupThreadID, uint3 Gid : SV_GroupID) { + // Get dimensions and check bounds + uint2 horizontalDimensions; + TexHorizontal.GetDimensions(horizontalDimensions.x, horizontalDimensions.y); + + if (DTid.x >= GRID_SIZE || DTid.y >= GRID_SIZE) + return; + + // Calculate vertical range to process + uint yTileSize = (horizontalDimensions.y + GRID_SIZE - 1) / GRID_SIZE; + uint yStart = DTid.y * yTileSize; + uint yEnd = min(yStart + yTileSize, horizontalDimensions.y); + + // Track maximum velocity + float maxVelocityMagnitude = 0.0f; + float2 maxVelocity = float2(0.0f, 0.0f); + + // Process vertical strip + for (uint y = yStart; y < yEnd; y++) { + // Sample horizontal pass texture + float4 sampleColor = TexHorizontal[uint2(DTid.x, y)]; + + // Extract velocity + float2 velocity = ExtractVelocity(sampleColor); + + // Keep largest velocity + float velocityMagnitude = length(velocity); + if (velocityMagnitude > maxVelocityMagnitude) { + maxVelocityMagnitude = velocityMagnitude; + maxVelocity = velocity; + } + } + + // Encode velocity for output + // R: normalized x direction mapped to 0..1 + // G: normalized y direction mapped to 0..1 + // B: velocity magnitude scaled to 0..1 + float2 normalizedDir = maxVelocityMagnitude > 0.001f ? normalize(maxVelocity) : float2(0.0f, 0.0f); + float encodedMagnitude = saturate(maxVelocityMagnitude * 0.05f); + + // Create output color + float4 outputColor = float4( + normalizedDir.x * 0.5f + 0.5f, + normalizedDir.y * 0.5f + 0.5f, + encodedMagnitude, + 1.0f); + + // Write to grid coordinates + RWTexVertical[DTid.xy] = outputColor; +} \ No newline at end of file diff --git a/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/aperture.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/aperture.cs.hlsl new file mode 100644 index 0000000000..6d65f7024d --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/aperture.cs.hlsl @@ -0,0 +1,493 @@ +// Physical Glare — Aperture transmittance and wavefront generation +// Community Shaders / Post Processing — Author: Jiaye, 2026 +// +// Generates the complex-valued aperture function A(x,y) = t(x,y) · exp(iφ(x,y)) +// where t is the amplitude transmittance and φ encodes wavefront aberrations. +// The Fraunhofer diffraction PSF is obtained as |FFT(A)|². +// +// Supported aperture geometries: +// - Lens mode: regular N-polygon with optional blade roughness, dust +// particles, and surface scratches. +// - Eye mode: circular pupil with eyelash occlusion, scatter particles, +// crystalline lens fiber gratings, Y-pattern suture lines, fiber cell +// starburst (radial phase grating), and tear film phase perturbation. +// +// Wavefront aberrations (phase modulation): +// - Fresnel defocus: φ_defocus = k · (r/R)² [1, section 2.2] +// - Spherical aberration: φ_SA = W₀₄₀ · (r/R)⁴ (Seidel coefficient) +// - Tear film: temporal angular harmonics [1, section 3.1] +// - Suture/starburst: radial phase gratings from fiber cell structure +// +// 4×4 sub-pixel supersampling for anti-aliased aperture edges [2]. +// +// References: +// [1] Delavennat (2021), Physically-based Real-time Glare, LiU. +// [2] Ritschel et al. (2009), Temporal Glare, CGF 28(2). + +RWTexture2D RWTexAperture : register(u0); + +cbuffer GlareCB : register(b1) +{ + float Threshold; + float Intensity; + float ScatterStrength; + uint ApertureMode; + + int ApertureBlades; + float ApertureRotation; + float AdaptSpeed; + float DeltaTime; + + uint FFTResolution; + float PaddingRatio; + float ScreenWidth; + float ScreenHeight; + + uint ChannelIndex; + float FresnelExponent; + float ChromaticSpread; + float ApertureSize; + + float PSFSharpness; + float PSFNoiseFloor; + uint EnableEyelashes; + float EyelashCurvature; + + uint EyelashCount; + float EyelashLength; + uint ParticleCount; + float ParticleSize; + + uint GratingCount; + float GratingStrength; + float TearFilmStrength; + float TearFilmSpeed; + + uint TearFilmComplexity; + float TearFilmTime; + uint SutureBranches; + float SutureStrength; + + float SutureWidth; + uint StarburstCount; + float StarburstStrength; + float StarburstIrregularity; + + uint DustCount; + float DustSize; + uint BladeRoughnessFreq; + float BladeRoughnessAmp; + + uint ScratchCount; + float ScratchOpacity; + float ScratchLength; + float ScratchWidth; + + float SphericalAberration; + uint UseAP1; + float KernelScale; + float _pad11; +}; +static const float PI = 3.14159265358979323846; + +// --------------------------------------------------------------------------- +// Eyelash occlusion mask [1, section 3.1, step 4, fig. 3.6] +// +// Models individual straight eyelash hairs extending radially inward from +// the upper and lower arcs of the circular pupil aperture. Straight +// spatial-domain lines produce straight frequency-domain streaks after FFT; +// curvature is applied later via UV bending in the chromatic blur pass +// [1, fig. 3.7]. +// --------------------------------------------------------------------------- +float EyelashMask(float2 pos, float radius) +{ + if (EyelashCount == 0) + return 1.0; + + float mask = 1.0; + uint totalLashes = EyelashCount; + // Upper lashes: ~60% of count, lower lashes: ~40% + uint upperCount = max(totalLashes * 3 / 5, 1u); + uint lowerCount = max(totalLashes - upperCount, 1u); + + float lashLen = EyelashLength * radius; + float lineHalfWidth = 1.2; // pixels + + // Upper eyelashes: distributed along the top arc + for (uint i = 0; i < upperCount; i++) { + // Base angle along upper arc (roughly -70° to +70° from top) + float t = (float(i) + 0.5) / float(upperCount); + float baseAngle = PI * 0.5 + (t - 0.5) * PI * 0.78; // ~140° arc at top + + // Base position on circular aperture edge + float2 base = float2(cos(baseAngle), sin(baseAngle)) * radius; + + // Lash direction: radially inward toward center + float2 dir = -normalize(base); + + // Point-to-straight-line-segment distance + float2 toP = pos - base; + float proj = dot(toP, dir); + if (proj < 0.0 || proj > lashLen) + continue; + + float2 closestPoint = base + dir * proj; + float dist = length(pos - closestPoint); + + // AA: smoothstep fade over ~1.5px + float paramT = proj / lashLen; + float lashAlpha = 1.0 - smoothstep(lineHalfWidth - 0.75, lineHalfWidth + 0.75, dist); + // Taper toward tip + lashAlpha *= 1.0 - paramT * paramT; + + mask *= (1.0 - lashAlpha); + } + + // Lower eyelashes: distributed along the bottom arc (shorter, less dense) + for (uint j = 0; j < lowerCount; j++) { + float t = (float(j) + 0.5) / float(lowerCount); + float baseAngle = -PI * 0.5 + (t - 0.5) * PI * 0.56; // ~100° arc at bottom + + float2 base = float2(cos(baseAngle), sin(baseAngle)) * radius; + float2 dir = -normalize(base); + + // Lower lashes are shorter + float lowerLen = lashLen * 0.6; + + float2 toP = pos - base; + float proj = dot(toP, dir); + if (proj < 0.0 || proj > lowerLen) + continue; + + float2 closestPoint = base + dir * proj; + float dist = length(pos - closestPoint); + + float paramT = proj / lowerLen; + float lashAlpha = 1.0 - smoothstep(lineHalfWidth - 0.75, lineHalfWidth + 0.75, dist); + lashAlpha *= 1.0 - paramT * paramT; + + mask *= (1.0 - lashAlpha); + } + + return mask; +} + +// --------------------------------------------------------------------------- +// Pseudo-random hash for deterministic particle placement (Wang hash) +// --------------------------------------------------------------------------- +float HashToFloat(uint seed) +{ + seed = (seed ^ 61u) ^ (seed >> 16u); + seed *= 9u; + seed = seed ^ (seed >> 4u); + seed *= 0x27d4eb2du; + seed = seed ^ (seed >> 15u); + return float(seed) / 4294967295.0; +} + +// Generate position of particle at given index within a disk of given radius. +// sqrt(random) gives uniform area distribution on a disk. +float2 ParticlePosition(uint index, float diskRadius) +{ + float angle = HashToFloat(index * 2u) * 2.0 * PI; + float r = sqrt(HashToFloat(index * 2u + 1u)) * diskRadius; + return float2(cos(angle), sin(angle)) * r; +} + +[numthreads(8, 8, 1)] void CS_Aperture(uint2 tid : SV_DispatchThreadID) { + if (tid.x >= FFTResolution || tid.y >= FFTResolution) + return; + + float2 center = float2(FFTResolution, FFTResolution) * 0.5; + float aspect = ScreenWidth / max(ScreenHeight, 1.0); + float radius = float(FFTResolution) * ApertureSize; + + // ----- Supersampled aperture shape + Fresnel phase ----- + // 4×4 sub-pixel supersampling [2, p.17]. + // Uniform smoothstep edge with ±3 px transition for rotational + // symmetry (directional edge width would introduce FFT artefacts). +#define APERTURE_SS 4 + float rcpSS = 1.0 / float(APERTURE_SS); + float edgeW = 3.0; // smoothstep half-width in pixels + + float2 complexSum = float2(0.0, 0.0); + + for (int sy = 0; sy < APERTURE_SS; sy++) { + for (int sx = 0; sx < APERTURE_SS; sx++) { + float2 subPos = float2(tid) + (float2(float(sx), float(sy)) + 0.5) * rcpSS - center; + subPos.y *= aspect; + + float subR = length(subPos); + float subValue = 0.0; + + if (ApertureMode == 1 || ApertureBlades <= 2) { + subValue = 1.0 - smoothstep(radius - edgeW, radius + edgeW, subR); + } else { + float sectorAngle = 2.0 * PI / float(ApertureBlades); + float rawAngle = atan2(subPos.y, subPos.x) - ApertureRotation; + float localAngle = frac(rawAngle / sectorAngle) * sectorAngle - sectorAngle * 0.5; + float apothem = radius * cos(sectorAngle * 0.5); + + // Blade edge roughness: micro-serrations on aperture blade edges + if (ApertureMode == 0 && BladeRoughnessAmp > 0.0 && BladeRoughnessFreq > 0) { + float bladeIdx = floor((rawAngle + PI) / sectorAngle); + float edgeT = localAngle / (sectorAngle * 0.5); // [-1,1] along blade edge + uint bladeHash = uint(abs(bladeIdx) + 0.5) + 8000u; + float roughFreq = float(min(BladeRoughnessFreq, 400u)); + float noise = 0.0; + [unroll] for (int k = 0; k < 4; k++) + { + float freq = roughFreq * (1.0 + float(k) * 0.7); + float ph = HashToFloat(bladeHash * 7u + uint(k) * 31u) * 2.0 * PI; + noise += sin(edgeT * freq + ph) / float(k + 1); + } + apothem += noise * BladeRoughnessAmp * 1.5; + } + + float projDist = subR * cos(localAngle); + subValue = 1.0 - smoothstep(apothem - edgeW, apothem + edgeW, projDist); + } + float subR2 = dot(subPos, subPos); + float R2 = radius * radius; + float rNorm2 = subR2 / max(R2, 1.0); + // Fresnel (r^2 defocus) + Seidel spherical aberration (r^4 lens curvature) + float phase = FresnelExponent * rNorm2 + SphericalAberration * rNorm2 * rNorm2; + + complexSum += float2(subValue * cos(phase), subValue * sin(phase)); + } + } + + complexSum *= (rcpSS * rcpSS); + + // ----- Tear film phase perturbation ----- + // The pre-corneal tear film (~3–5 µm) introduces time-dependent + // optical path differences concentrated near the pupil edge, + // producing temporally varying sharp diffraction spikes [1, section 3.1]. + // Eye mode only. + if (ApertureMode == 1 && TearFilmStrength > 0.0) { + float2 tfPos = float2(tid) + 0.5 - center; + tfPos.y *= aspect; + float r = length(tfPos); + float theta = atan2(tfPos.y, tfPos.x); + + // Phase perturbation strongest in outer 40% of aperture + float edgeFactor = smoothstep(radius * 0.55, radius * 0.95, r); + + // Sum angular harmonics with time-varying phases + float phaseOffset = 0.0; + uint complexity = min(TearFilmComplexity, 16u); + for (uint h = 0; h < complexity; h++) { + // Deterministic random amplitude and speed per harmonic + float amp = HashToFloat(h * 3u + 7000u) * 0.6 + 0.4; + float angularFreq = float(h + 2); + float timeSpeed = (HashToFloat(h * 3u + 7001u) * 1.6 + 0.2) * TearFilmSpeed; + float timePhase = HashToFloat(h * 3u + 7002u) * 2.0 * PI; + phaseOffset += amp * sin(angularFreq * theta + timeSpeed * TearFilmTime + timePhase); + } + phaseOffset *= TearFilmStrength * edgeFactor * 2.5; + + // Apply as complex phase rotation + float c = cos(phaseOffset); + float s = sin(phaseOffset); + complexSum = float2(complexSum.x * c - complexSum.y * s, + complexSum.x * s + complexSum.y * c); + } + + // ----- Eyelash & particle masks (centre texel only) ----- + // High-frequency amplitude features evaluated at texel centre; + // their own diffraction patterns are intentional. + float2 pos = float2(tid) + 0.5 - center; + pos.y *= aspect; + + // Eyelash occlusion — eye mode only + if (ApertureMode == 1 && EnableEyelashes != 0) { + complexSum *= EyelashMask(pos, radius); + } + + // Scatter particles — eye mode only + if (ApertureMode == 1 && ParticleCount > 0) { + uint pCount = min(ParticleCount, 1000u); + float opacity = saturate(ScatterStrength); + float particleMask = 1.0; + for (uint p = 0; p < pCount; p++) { + float2 ppos = ParticlePosition(p, radius * 0.92); + float d = length(pos - ppos); + float alpha = 1.0 - smoothstep(ParticleSize - 0.5, ParticleSize + 0.5, d); + particleMask *= (1.0 - alpha * opacity); + } + complexSum *= particleMask; + } + + // Lens dust — lens mode only + if (ApertureMode == 0 && DustCount > 0) { + uint pCount = min(DustCount, 1000u); + float particleMask = 1.0; + for (uint p = 0; p < pCount; p++) { + float2 ppos = ParticlePosition(p, radius * 0.92); + float d = length(pos - ppos); + float alpha = 1.0 - smoothstep(DustSize - 0.5, DustSize + 0.5, d); + particleMask *= (1.0 - alpha); + } + complexSum *= particleMask; + } + + // Lens surface scratches — lens mode only. + // Linear scratches produce perpendicular streak patterns after FFT. + if (ApertureMode == 0 && ScratchCount > 0 && ScratchOpacity > 0.0) { + uint scratchCnt = min(ScratchCount, 20u); + float scratchLen = ScratchLength * radius * 2.0; + float scratchHW = ScratchWidth * 0.5; + float scratchOpc = saturate(ScratchOpacity); + + for (uint si = 0; si < scratchCnt; si++) { + // Deterministic random position and angle per scratch + float2 scratchCenter = float2( + (HashToFloat(si * 3u + 9000u) - 0.5) * radius * 1.6, + (HashToFloat(si * 3u + 9001u) - 0.5) * radius * 1.6); + float scratchAngle = HashToFloat(si * 3u + 9002u) * PI; + float2 scratchDir = float2(cos(scratchAngle), sin(scratchAngle)); + + // Point-to-line-segment distance + float2 toP = pos - scratchCenter; + float proj = dot(toP, scratchDir); + if (abs(proj) > scratchLen * 0.5) + continue; + + float2 cp = scratchCenter + scratchDir * proj; + float dist = length(pos - cp); + float alpha = 1.0 - smoothstep(scratchHW - 0.5, scratchHW + 0.5, dist); + + // Taper toward ends for natural look + float endFade = 1.0 - smoothstep(scratchLen * 0.35, scratchLen * 0.5, abs(proj)); + + complexSum *= (1.0 - alpha * endFade * scratchOpc); + } + } + + // Crystalline lens fiber gratings [1, section 2.4, fig. 2.12] + // ~200 radial structures with higher refractive index near the pupil + // edge, producing a diffuse lenticular halo via edge diffraction. + // Modelled as semi-opaque radial lines with radial falloff. + // Eye mode only. + if (ApertureMode == 1 && GratingCount > 0) { + float r = length(pos); + float angle = atan2(pos.y, pos.x); + + // Angular spacing between gratings + float gratingCountF = float(min(GratingCount, 400u)); + float sectorAngle = 2.0 * PI / gratingCountF; + + // Fractional position within the nearest grating sector [-0.5, 0.5) + float localAngle = frac(angle / sectorAngle + 0.5) - 0.5; + + // Arc distance from the nearest grating line centre (in pixels) + float arcDist = abs(localAngle) * sectorAngle * r; + + // Thin line AA: each grating is ~1 pixel wide + float lineAlpha = 1.0 - smoothstep(0.0, 1.5, arcDist); + + // Radial falloff: gratings affect mostly the outer ring of the lens + // (paper: "higher refractive index near the edges") + float edgeFade = smoothstep(radius * 0.4, radius, r); + + // Combined grating mask: reduce transmission at grating locations + float gratingMask = 1.0 - lineAlpha * edgeFade * saturate(GratingStrength); + complexSum *= gratingMask; + } + + // Crystalline lens suture lines. + // Elongated fiber cells meet at Y-shaped suture lines: N anterior + // branches and N posterior branches rotated by half a sector. The + // refractive index discontinuity introduces phase shifts that produce + // prominent star spikes after FFT. Fewer and thicker than gratings, + // sutures define the base starburst geometry of human glare. + // Eye mode only. + if (ApertureMode == 1 && SutureBranches > 0) { + float r = length(pos); + float angle = atan2(pos.y, pos.x); + uint branches = min(SutureBranches, 8u); + + // Anterior sutures: N branches from center outward + float sectorAngle = 2.0 * PI / float(branches); + float suturePhase = 0.0; + + // Anterior branches (angle offset 0) + { + float localAngle = frac(angle / sectorAngle + 0.5) - 0.5; + float arcDist = abs(localAngle) * sectorAngle * r; + float lineAlpha = 1.0 - smoothstep(SutureWidth * 0.5 - 0.75, SutureWidth * 0.5 + 0.75, arcDist); + // Sutures run from center to ~90% radius + float radialMask = smoothstep(0.0, radius * 0.08, r) * (1.0 - smoothstep(radius * 0.85, radius * 0.95, r)); + suturePhase += lineAlpha * radialMask; + } + + // Posterior branches: same count, rotated by half a sector (60° for N=3) + { + float rotAngle = angle - sectorAngle * 0.5; + float localAngle = frac(rotAngle / sectorAngle + 0.5) - 0.5; + float arcDist = abs(localAngle) * sectorAngle * r; + float lineAlpha = 1.0 - smoothstep(SutureWidth * 0.5 - 0.75, SutureWidth * 0.5 + 0.75, arcDist); + float radialMask = smoothstep(0.0, radius * 0.08, r) * (1.0 - smoothstep(radius * 0.85, radius * 0.95, r)); + suturePhase += lineAlpha * radialMask; + } + + // Apply as phase shift (refractive index difference → optical path difference) + float phase = suturePhase * SutureStrength * PI * 0.5; + float sc = cos(phase); + float ss = sin(phase); + complexSum = float2(complexSum.x * sc - complexSum.y * ss, + complexSum.x * ss + complexSum.y * sc); + } + + // Fiber cell starburst (radial phase grating). + // Thousands of radially arranged fiber cells create high-angular- + // frequency refractive index discontinuities. The FFT of these thin + // radial phase lines produces numerous sharp radial spikes. + // Unlike sutures (few thick spikes) or gratings (amplitude → diffuse + // halo), this is high-count phase modulation → sharp thin spikes. + // Eye mode only. + if (ApertureMode == 1 && StarburstCount > 0) { + float r = length(pos); + float angle = atan2(pos.y, pos.x); + uint spikeCount = min(StarburstCount, 128u); + float spikeCountF = float(spikeCount); + float sectorAngle = 2.0 * PI / spikeCountF; + + // O(1) nearest-sector lookup: check self + 2 neighbors for jitter safety + float sectorIdx = angle / sectorAngle; + float burstPhase = 0.0; + + for (int di = -1; di <= 1; di++) { + int idx = int(floor(sectorIdx + 0.5)) + di; + uint wrappedIdx = uint((idx % int(spikeCount) + int(spikeCount)) % int(spikeCount)); + + // Per-fiber angular jitter: shifts each line off the regular grid + float jitter = (HashToFloat(wrappedIdx * 2u + 5000u) - 0.5) * StarburstIrregularity; + float fiberAngle = (float(idx) + jitter) * sectorAngle; + + // Arc distance to this fiber line (in pixels) + float angleDiff = angle - fiberAngle; + float arcDist = abs(angleDiff) * r; + + // Very thin line (~0.8px): sharp phase discontinuity → sharp PSF spikes + float lineAlpha = 1.0 - smoothstep(0.0, 0.8, arcDist); + + // Per-fiber random strength variation for natural look + float fiberStr = 1.0 - StarburstIrregularity * 0.5 * (1.0 - HashToFloat(wrappedIdx * 2u + 5001u)); + + burstPhase += lineAlpha * fiberStr; + } + + // Radial mask: present from near-center to outer edge + float radialMask = smoothstep(radius * 0.05, radius * 0.3, r) * (1.0 - smoothstep(radius * 0.88, radius * 0.98, r)); + + // Apply as phase modulation (refractive index difference) + float phase = burstPhase * radialMask * StarburstStrength * PI; + float cb = cos(phase); + float sb = sin(phase); + complexSum = float2(complexSum.x * cb - complexSum.y * sb, + complexSum.x * sb + complexSum.y * cb); + } + + RWTexAperture[tid] = complexSum; +} diff --git a/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/composite.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/composite.cs.hlsl new file mode 100644 index 0000000000..abe52004c8 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/composite.cs.hlsl @@ -0,0 +1,167 @@ +// Physical Glare — Scene composite with energy conservation +// Community Shaders / Post Processing — Author: Jiaye, 2026 +// +// Reconstructs per-channel glare from IFFT results, upsamples from FFT +// resolution to screen resolution via Catmull-Rom bicubic interpolation [4], +// and composites onto the original scene. +// +// Energy conservation: the bright threshold contribution is subtracted from +// the scene and replaced by the convolved (spread) version. At Intensity=1.0 +// total luminous energy is redistributed, not added. +// +// Only the center region [P, 1−P) of the IFFT output is read, corresponding +// to the non-padded scene area (P = PaddingRatio) [1, section 2.5]. +// +// References: +// [1] Delavennat (2021), Physically-based Real-time Glare, LiU. +// [4] Jimenez (2012), Filmic SMAA, SIGGRAPH — bicubic via HW bilinear. + +Texture2D TexScene : register(t0); // Original scene (full resolution) +Texture2D TexIFFT_R : register(t1); // IFFT result, R channel (FFT resolution) +Texture2D TexIFFT_G : register(t2); // IFFT result, G channel (FFT resolution) +Texture2D TexIFFT_B : register(t3); // IFFT result, B channel (FFT resolution) + +RWTexture2D RWTexOutput : register(u0); // Final composited output (full resolution) + +SamplerState LinearSampler : register(s0); + +cbuffer GlareCB : register(b1) +{ + float Threshold; + float Intensity; + float ScatterStrength; + uint ApertureMode; + + int ApertureBlades; + float ApertureRotation; + float AdaptSpeed; + float DeltaTime; + + uint FFTResolution; + float PaddingRatio; + float ScreenWidth; + float ScreenHeight; + + uint ChannelIndex; + float FresnelExponent; + float ChromaticSpread; + float ApertureSize; + + float PSFSharpness; + float PSFNoiseFloor; + uint EnableEyelashes; + float EyelashCurvature; + + // (remaining fields omitted — only needed by aperture/PSF shaders) + // Row 5-10: eye/lens mode params (not needed here) + uint EyelashCount; + float EyelashLength; + uint ParticleCount; + float ParticleSize; + uint GratingCount; + float GratingStrength; + float TearFilmStrength; + float TearFilmSpeed; + uint TearFilmComplexity; + float TearFilmTime; + uint SutureBranches; + float SutureStrength; + float SutureWidth; + uint StarburstCount; + float StarburstStrength; + float StarburstIrregularity; + uint DustCount; + float DustSize; + uint BladeRoughnessFreq; + float BladeRoughnessAmp; + uint ScratchCount; + float ScratchOpacity; + float ScratchLength; + float ScratchWidth; + float SphericalAberration; + uint UseAP1; + float KernelScale; + float _pad11; +}; + +// --------------------------------------------------------------------------- +// Catmull-Rom bicubic upsample (4 bilinear taps → 16-texel footprint). +// Dramatically reduces blocky artifacts when upsampling from FFT resolution +// (e.g. 768→2160p = 2.8×). Cost: 4 SampleLevel per channel vs 1 — negligible +// for a full-screen composite that's bandwidth-bound anyway. +// Ref: Jimenez, "Filmic SMAA" (SIGGRAPH 2012), bicubic with HW bilinear. +// --------------------------------------------------------------------------- +float CatmullRomSample(Texture2D tex, SamplerState samp, float2 uv, float2 texSize) +{ + float2 samplePos = uv * texSize; + float2 texPos1 = floor(samplePos - 0.5) + 0.5; + float2 f = samplePos - texPos1; + + // Catmull-Rom weights for the 4 taps (2 negative lobes + 2 positive) + float2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f)); + float2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f); + float2 w2 = f * (0.5 + f * (2.0 - 1.5 * f)); + float2 w3 = f * f * (-0.5 + 0.5 * f); + + // Combine pairs for 4 bilinear taps instead of 16 point samples + float2 w12 = w1 + w2; + float2 offset12 = w2 / max(w12, 1e-6); + + float2 texPos0 = (texPos1 - 1.0) / texSize; + float2 texPos3 = (texPos1 + 2.0) / texSize; + float2 texPos12 = (texPos1 + offset12) / texSize; + + float result = 0.0; + result += tex.SampleLevel(samp, float2(texPos0.x, texPos0.y), 0).x * w0.x * w0.y; + result += tex.SampleLevel(samp, float2(texPos12.x, texPos0.y), 0).x * w12.x * w0.y; + result += tex.SampleLevel(samp, float2(texPos3.x, texPos0.y), 0).x * w3.x * w0.y; + + result += tex.SampleLevel(samp, float2(texPos0.x, texPos12.y), 0).x * w0.x * w12.y; + result += tex.SampleLevel(samp, float2(texPos12.x, texPos12.y), 0).x * w12.x * w12.y; + result += tex.SampleLevel(samp, float2(texPos3.x, texPos12.y), 0).x * w3.x * w12.y; + + result += tex.SampleLevel(samp, float2(texPos0.x, texPos3.y), 0).x * w0.x * w3.y; + result += tex.SampleLevel(samp, float2(texPos12.x, texPos3.y), 0).x * w12.x * w3.y; + result += tex.SampleLevel(samp, float2(texPos3.x, texPos3.y), 0).x * w3.x * w3.y; + + return result; +} + +[numthreads(8, 8, 1)] void CS_Composite(uint2 tid : SV_DispatchThreadID) { + if (tid.x >= (uint)ScreenWidth || tid.y >= (uint)ScreenHeight) + return; + + float3 scene = TexScene[tid].rgb; + + // Map screen UV [0,1] → IFFT UV [P, 1-P], reading only the centre + // region that contained the original scene within the zero-padded + // texture [1, section 2.5]. + float sceneScale = 1.0 - 2.0 * PaddingRatio; + float2 uv = (float2(tid) + 0.5) / float2(ScreenWidth, ScreenHeight); + float2 ifftUV = uv * sceneScale + PaddingRatio; + + // Bicubic (Catmull-Rom) upsample — eliminates blocky artifacts at high upscale ratios + float2 texSize = float2(FFTResolution, FFTResolution); + float glareR = max(0, CatmullRomSample(TexIFFT_R, LinearSampler, ifftUV, texSize)); + float glareG = max(0, CatmullRomSample(TexIFFT_G, LinearSampler, ifftUV, texSize)); + float glareB = max(0, CatmullRomSample(TexIFFT_B, LinearSampler, ifftUV, texSize)); + float3 glare = float3(glareR, glareG, glareB); + + // Sanitize extreme values + glare = min(glare, 65000.0); + if (any(isnan(glare)) || any(isinf(glare))) + glare = 0; + + // Energy-conserving glare contribution: + // Subtract the thresholded bright component and output the convolved + // (spread) result. At Intensity = 1 total energy is redistributed, + // not gained. Values != 1 allow artistic attenuation or exaggeration. + // This output is combined with the scene in the Composite pass. + float3 bright = max(0, scene - Threshold); + float3 output = (glare - bright) * Intensity; + + // Clamp to prevent negative values at Intensity > 1.0 + output = max(output, 0); + + RWTexOutput[tid] = float4(output, 1.0); +} diff --git a/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/fft.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/fft.cs.hlsl new file mode 100644 index 0000000000..c0da5706ad --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/fft.cs.hlsl @@ -0,0 +1,164 @@ +// Physical Glare — Stockham radix-2 FFT (row/column pass) +// Community Shaders / Post Processing — Author: Jiaye, 2026 +// +// One-dimensional DFT via the Stockham auto-sort algorithm using +// groupshared memory. Compiled with defines: +// ROW_PASS / COL_PASS — selects transform axis. +// FORWARD / INVERSE — selects twiddle factor sign. +// Each thread group processes one row or column; dispatch (N, 1, 1). +// +// References: +// [1] Delavennat (2021), Physically-based Real-time Glare, LiU. + +// Input complex texture (RG32F: R=real, G=imaginary) +Texture2D TexInput : register(t0); + +// Output complex texture +RWTexture2D RWTexOutput : register(u0); + +cbuffer GlareCB : register(b1) +{ + float Threshold; + float Intensity; + float ScatterStrength; + uint ApertureMode; + + int ApertureBlades; + float ApertureRotation; + float AdaptSpeed; + float DeltaTime; + + uint FFTResolution; + float PaddingRatio; + float ScreenWidth; + float ScreenHeight; + + uint ChannelIndex; + float FresnelExponent; + float ChromaticSpread; + float ApertureSize; + + float PSFSharpness; + float PSFNoiseFloor; + uint EnableEyelashes; + float EyelashCurvature; +}; + +static const float PI = 3.14159265358979323846; + +// Max FFT size supported (must match FFT_MAX in C++ code) +#define MAX_FFT_SIZE 1024 + +// Shared memory for the FFT butterfly operations +// Two buffers for ping-pong (2 × 1024 × 8B = 16 KB, within 32 KB CS 5.0 limit) +groupshared float2 gs_buffer0[MAX_FFT_SIZE]; +groupshared float2 gs_buffer1[MAX_FFT_SIZE]; + +// Complex multiplication +float2 ComplexMul(float2 a, float2 b) +{ + return float2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); +} + +// Compute twiddle factor W_N^k = exp(-2*pi*i*k/N) for forward, exp(+2*pi*i*k/N) for inverse +float2 Twiddle(uint k, uint N) +{ +#ifdef INVERSE + float angle = 2.0 * PI * float(k) / float(N); +#else + float angle = -2.0 * PI * float(k) / float(N); +#endif + float s, c; + sincos(angle, s, c); + return float2(c, s); +} + +// Each group processes one row or column. +// 1024 threads = CS 5.0 max; handles up to 1024-point FFT. +// Smaller FFTs guard with `active = (threadIdx < N)`. +[numthreads(1024, 1, 1)] void CS_FFT(uint3 groupId : SV_GroupID, uint threadIdx : SV_GroupThreadID) { + uint lineIdx = groupId.x; // which row or column + uint N = FFTResolution; + + bool active = (threadIdx < N); + + // Load input data into shared memory + if (active) { + uint2 readPos; +#ifdef ROW_PASS + readPos = uint2(threadIdx, lineIdx); +#else + readPos = uint2(lineIdx, threadIdx); +#endif + gs_buffer0[threadIdx] = TexInput[readPos]; + } + + GroupMemoryBarrierWithGroupSync(); + + // Bit-reversal permutation + if (active) { + uint bits = firstbithigh(N) - firstbithigh(1); // log2(N) + uint rev = 0; + uint tmp = threadIdx; + for (uint b = 0; b < bits; b++) { + rev = (rev << 1) | (tmp & 1); + tmp >>= 1; + } + gs_buffer1[rev] = gs_buffer0[threadIdx]; + } + + GroupMemoryBarrierWithGroupSync(); + + // Copy back to buffer0 for butterfly stages + if (active) + gs_buffer0[threadIdx] = gs_buffer1[threadIdx]; + + GroupMemoryBarrierWithGroupSync(); + + // Iterative Cooley-Tukey butterfly + for (uint stage = 1; stage < N; stage <<= 1) { + if (active) { + uint halfStage = stage; + uint fullStage = stage << 1; + + uint butterflyGroup = threadIdx / fullStage; + uint butterflyIdx = threadIdx % fullStage; + + if (butterflyIdx < halfStage) { + uint topIdx = butterflyGroup * fullStage + butterflyIdx; + uint botIdx = topIdx + halfStage; + + float2 tw = Twiddle(butterflyIdx, fullStage); + float2 top = gs_buffer0[topIdx]; + float2 bot = ComplexMul(tw, gs_buffer0[botIdx]); + + gs_buffer1[topIdx] = top + bot; + gs_buffer1[botIdx] = top - bot; + } + } + + GroupMemoryBarrierWithGroupSync(); + + if (active) + gs_buffer0[threadIdx] = gs_buffer1[threadIdx]; + + GroupMemoryBarrierWithGroupSync(); + } + + // Write output + if (active) { + float2 result = gs_buffer0[threadIdx]; +#ifdef INVERSE + result /= float(N); +#endif + + uint2 writePos; +#ifdef ROW_PASS + writePos = uint2(threadIdx, lineIdx); +#else + writePos = uint2(lineIdx, threadIdx); +#endif + + RWTexOutput[writePos] = result; + } +} diff --git a/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/multiply.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/multiply.cs.hlsl new file mode 100644 index 0000000000..f19986f308 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/multiply.cs.hlsl @@ -0,0 +1,64 @@ +// Physical Glare — Frequency-domain complex multiplication +// Community Shaders / Post Processing — Author: Jiaye, 2026 +// +// Element-wise complex multiplication of the scene FFT with the PSF FFT +// for one colour channel: (a+bi)(c+di) = (ac−bd) + (ad+bc)i. +// Implements the convolution theorem: IFFT(F·G) = f∗g [1, section 2.1]. +// +// References: +// [1] Delavennat (2021), Physically-based Real-time Glare, LiU. + +Texture2D TexSceneFFT : register(t0); // Scene FFT (one channel) +Texture2D TexPSF_FFT : register(t1); // PSF FFT (one channel) + +RWTexture2D RWTexResult : register(u0); // Output (one channel) + +cbuffer GlareCB : register(b1) +{ + float Threshold; + float Intensity; + float ScatterStrength; + uint ApertureMode; + + int ApertureBlades; + float ApertureRotation; + float AdaptSpeed; + float DeltaTime; + + uint FFTResolution; + float PaddingRatio; + float ScreenWidth; + float ScreenHeight; + + uint ChannelIndex; + float FresnelExponent; + float ChromaticSpread; + float ApertureSize; + + float PSFSharpness; + float PSFNoiseFloor; + uint EnableEyelashes; + float EyelashCurvature; +}; + +[numthreads(8, 8, 1)] void CS_Multiply(uint2 tid : SV_DispatchThreadID) { + if (tid.x >= FFTResolution || tid.y >= FFTResolution) + return; + + float2 scene = TexSceneFFT[tid]; + float2 psf = TexPSF_FFT[tid]; + + // DC component F[0,0] of PSF FFT equals its spatial-domain sum. + // Dividing by it normalises the PSF to unit energy so the convolution + // preserves the thresholded scene's brightness level. + float psfDC = max(TexPSF_FFT[uint2(0, 0)].x, 1e-6); + + // Complex multiplication: (a+bi)(c+di) = (ac-bd) + (ad+bc)i + float2 result; + result.x = scene.x * psf.x - scene.y * psf.y; + result.y = scene.x * psf.y + scene.y * psf.x; + + result /= psfDC; + + RWTexResult[tid] = result; +} diff --git a/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/psf.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/psf.cs.hlsl new file mode 100644 index 0000000000..f178014c51 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/psf.cs.hlsl @@ -0,0 +1,210 @@ +// Physical Glare — Chromatic PSF generation +// Community Shaders / Post Processing — Author: Jiaye, 2026 +// +// Computes per-channel point spread function via multi-wavelength diffraction +// intensity sampling. For each of 32 spectral samples across 380–770 nm, +// the monochromatic diffraction pattern |F(u,v)|² is sampled at a +// wavelength-scaled UV offset and weighted by CIE 1931 colour matching +// functions [3]. Spectral weights are converted to the working colour +// space (Rec. 709 or ACEScg/AP1) depending on pipeline configuration. +// +// In eye (pupil) mode, eyelash streak curvature is applied via sinusoidal +// UV bending [1, section 3.1, fig. 3.7]. +// +// Pipeline position: aperture → FFT → [this shader] → FFT → convolution +// +// References: +// [1] Delavennat (2021), Physically-based Real-time Glare, LiU. +// [2] Ritschel et al. (2009), Temporal Glare, CGF 28(2). +// [3] Wyman, Sloan, Shirley (2013), Simple Analytic Approximations +// to the CIE XYZ Color Matching Functions, JCGT 2(2). + +Texture2D TexDiffraction : register(t0); // Complex FFT of aperture (RG32F) +SamplerState WrapSampler : register(s0); // Wrap-mode bilinear sampler + +RWTexture2D RWTexPSF : register(u0); // Output: per-channel PSF (real, 0) + +cbuffer GlareCB : register(b1) +{ + float Threshold; + float Intensity; + float ScatterStrength; + uint ApertureMode; + + int ApertureBlades; + float ApertureRotation; + float AdaptSpeed; + float DeltaTime; + + uint FFTResolution; + float PaddingRatio; + float ScreenWidth; + float ScreenHeight; + + uint ChannelIndex; // 0=R, 1=G, 2=B + float FresnelExponent; + float ChromaticSpread; + float ApertureSize; + + float PSFSharpness; + float PSFNoiseFloor; + uint EnableEyelashes; + float EyelashCurvature; + + // Rows 5-10 (used by aperture shader, not this shader) + float4 _cbpad5; + float4 _cbpad6; + float4 _cbpad7; + float4 _cbpad8; + float4 _cbpad9; + float4 _cbpad10; + + // Row 11: Additional optics + float SphericalAberration; + uint UseAP1; + float KernelScale; + float _pad11; +}; + +static const float PI = 3.14159265358979323846; + +// Number of spectral samples for chromatic blur (paper section 3.1: 32 wavelengths) +#define NUM_WAVELENGTHS 32 + +// --------------------------------------------------------------------------- +// CIE 1931 XYZ colour matching — Gaussian multi-lobe fit +// (Wyman, Sloan, Shirley 2013) +// --------------------------------------------------------------------------- +float3 WavelengthToXYZ(float lambda) +{ + // Use intermediate variables to prevent fxc from constant-folding + // the entire expression in double precision (X4122 warnings). + float dx1 = (lambda - 599.8f) / 37.9f; + float dx2 = (lambda - 442.0f) / 16.0f; + float dx3 = (lambda - 501.1f) / 20.4f; + float x = + 1.056f * exp(-0.5f * dx1 * dx1) + + 0.362f * exp(-0.5f * dx2 * dx2) - + 0.065f * exp(-0.5f * dx3 * dx3); + + float dy1 = (lambda - 568.8f) / 46.9f; + float dy2 = (lambda - 530.9f) / 16.3f; + float y = + 0.821f * exp(-0.5f * dy1 * dy1) + + 0.286f * exp(-0.5f * dy2 * dy2); + + float dz1 = (lambda - 437.0f) / 11.8f; + float dz2 = (lambda - 459.0f) / 26.0f; + float z = + 1.217f * exp(-0.5f * dz1 * dz1) + + 0.681f * exp(-0.5f * dz2 * dz2); + + return float3(x, y, z); +} + +// XYZ → linear sRGB (D65 white point, Rec. 709 primaries) +float3 XYZToLinearSRGB(float3 xyz) +{ + return float3( + 3.2406f * xyz.x - 1.5372f * xyz.y - 0.4986f * xyz.z, + -0.9689f * xyz.x + 1.8758f * xyz.y + 0.0415f * xyz.z, + 0.0557f * xyz.x - 0.2040f * xyz.y + 1.0570f * xyz.z); +} + +// XYZ → ACEScg / AP1 (ACES D60 white point, AP1 primaries) +// Matrix from colour-science.org via ColourSpace.h +float3 XYZToAP1(float3 xyz) +{ + return float3( + 1.64102338f * xyz.x - 0.32480329f * xyz.y - 0.2364247f * xyz.z, + -0.66366286f * xyz.x + 1.61533159f * xyz.y + 0.01675635f * xyz.z, + 0.01172189f * xyz.x - 0.00828444f * xyz.y + 0.98839486f * xyz.z); +} + +// --------------------------------------------------------------------------- +[numthreads(8, 8, 1)] void CS_ChromaticBlur(uint2 tid : SV_DispatchThreadID) { + if (tid.x >= FFTResolution || tid.y >= FFTResolution) + return; + + float N = float(FFTResolution); + float rcpN = 1.0 / N; + + // ------------------------------------------------------------------ + // Multi-wavelength chromatic blur [1, section 3.1]: + // For each spectral sample, the monochromatic diffraction pattern + // |F(u,v)|² is sampled at UV scaled by λ/λ_ref, weighted by + // CIE 1931 spectral → RGB conversion for the current channel. + // Reference wavelength: 575 nm (scale = 1). + // Longer λ → larger diffraction pattern; shorter λ → smaller. + // ------------------------------------------------------------------ + float result = 0.0; + + // Centred frequency coordinates (DC at origin). + // Bins [0, N/2) = positive frequencies; [N/2, N) = negative. + float2 freq = float2(tid); + if (freq.x >= N * 0.5) + freq.x -= N; + if (freq.y >= N * 0.5) + freq.y -= N; + + // KernelScale: shrink/grow the PSF spatially by scaling frequency coordinates. + // Dividing by KernelScale < 1 maps each position to further from DC, + // making the PSF drop off faster → smaller glare on screen. + freq /= max(KernelScale, 0.01); + + for (int w = 0; w < NUM_WAVELENGTHS; w++) { + float lambda = 380.0 + float(w) * (770.0 - 380.0) / float(NUM_WAVELENGTHS - 1); + + // UV scale: physical scaling λ/575nm [1, section 2.3]. + // ChromaticSpread controls deviation from unity: + // 1.0 = physically correct, >1 = exaggerated, 0 = monochrome. + float uvScale = 1.0 + (lambda / 575.0 - 1.0) * ChromaticSpread; + + // Scale frequency around DC, convert to UV. + // Wrap-mode sampler handles out-of-range coordinates. + float2 sampleUV = (freq / uvScale + 0.5) * rcpN; + + // Eyelash streak curvature via sinusoidal UV bending + // [1, section 3.1, fig. 3.7]: sin(π·x) · |x| produces a + // symmetric arch that curves both sides of the streak. + // Eye mode only. + if (ApertureMode == 1 && EnableEyelashes != 0) { + float x_norm = freq.x / (N * 0.5); // [-1, 1] + float bend = sin(PI * x_norm) * x_norm * EyelashCurvature; // symmetric arch + sampleUV.y -= bend * 0.5; + } + + float2 cval = TexDiffraction.SampleLevel(WrapSampler, sampleUV, 0); + + // Diffraction intensity = |F(u,v)|² + float intensity = cval.x * cval.x + cval.y * cval.y; + + // Spectral weight for this RGB channel + float3 xyz = WavelengthToXYZ(lambda); + float3 rgb = UseAP1 ? max(XYZToAP1(xyz), 0.0) : max(XYZToLinearSRGB(xyz), 0.0); + + float channelWeight; + if (ChannelIndex == 0) + channelWeight = rgb.r; + else if (ChannelIndex == 1) + channelWeight = rgb.g; + else + channelWeight = rgb.b; + + // Normalise by NUM_WAVELENGTHS to preserve dynamic range [1, section 3.1]. + result += intensity * channelWeight / float(NUM_WAVELENGTHS); + } + + // ------------------------------------------------------------------ + // PSF dynamic range compression [1, Table 3.9]: + // pow(PSFSharpness) compresses the extreme FFT dynamic range so + // diffraction spikes remain visible relative to the DC peak. + // threshold(PSFNoiseFloor) suppresses low-level numerical noise. + // The LDR-tuned empirical factors from [1] (×2000, ×10) are omitted; + // brightness is governed by the Intensity parameter instead. + // ------------------------------------------------------------------ + result = pow(max(result, 0.0), PSFSharpness); + result = max(result - PSFNoiseFloor, 0.0); + + RWTexPSF[tid] = float2(max(result, 0.0), 0.0); +} diff --git a/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/threshold.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/threshold.cs.hlsl new file mode 100644 index 0000000000..561fddecd8 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/PhysicalGlare/threshold.cs.hlsl @@ -0,0 +1,85 @@ +// Physical Glare — Threshold extraction and zero-padded downsampling +// Community Shaders / Post Processing — Author: Jiaye, 2026 +// +// Per-channel luminance thresholding: pixels below the threshold are +// discarded; the remainder is placed into the center [P, 1−P) region of +// the N×N FFT texture. The surrounding border remains zero, providing +// spatial padding to suppress FFT circular convolution wrap-around +// artefacts [1, section 2.5]. P is controlled by PaddingRatio. +// +// References: +// [1] Delavennat (2021), Physically-based Real-time Glare, LiU. + +RWTexture2D RWTexFFT_R : register(u0); +RWTexture2D RWTexFFT_G : register(u1); +RWTexture2D RWTexFFT_B : register(u2); + +Texture2D TexColor : register(t0); + +cbuffer GlareCB : register(b1) +{ + float Threshold; + float Intensity; + float ScatterStrength; + uint ApertureMode; + + int ApertureBlades; + float ApertureRotation; + float AdaptSpeed; + float DeltaTime; + + uint FFTResolution; + float PaddingRatio; + float ScreenWidth; + float ScreenHeight; + + uint ChannelIndex; + float FresnelExponent; + float ChromaticSpread; + float ApertureSize; + + float PSFSharpness; + float PSFNoiseFloor; + uint EnableEyelashes; + float EyelashCurvature; +}; + +[numthreads(8, 8, 1)] void CS_Threshold(uint2 tid : SV_DispatchThreadID) { + if (tid.x >= FFTResolution || tid.y >= FFTResolution) + return; + + // Zero-padding [1, section 2.5]: + // Scene is placed in the centre region of the N×N FFT texture. + // PaddingRatio controls border width per side that absorbs convolution + // overflow: 0.25 = 50% effective (default in [1]), 0.1 = 80%, 0 = none. + uint padding = uint(float(FFTResolution) * PaddingRatio); + uint sceneSize = FFTResolution - 2 * padding; + + if (tid.x >= padding && tid.x < padding + sceneSize && + tid.y >= padding && tid.y < padding + sceneSize) { + // Map center region to screen UV [0,1] + float2 localPos = float2(tid.x - padding, tid.y - padding); + float2 uv = (localPos + 0.5) / float(sceneSize); + + // Sample scene at corresponding screen position + uint2 screenPos = uint2(uv * float2(ScreenWidth, ScreenHeight)); + screenPos = min(screenPos, uint2(uint(ScreenWidth) - 1, uint(ScreenHeight) - 1)); + + float3 color = TexColor[screenPos].rgb; + + // Per-channel thresholding [1, section 3.2], adapted for HDR: + // Values exceeding Threshold pass through at original HDR magnitude. + // DC-normalised convolution preserves energy, so the empirical + // scaling factor from [1] is unnecessary. + float3 extracted = max(0, color.rgb - Threshold); + + RWTexFFT_R[tid] = float2(extracted.r, 0); + RWTexFFT_G[tid] = float2(extracted.g, 0); + RWTexFFT_B[tid] = float2(extracted.b, 0); + } else { + // Zero-padded border — absorbs convolution overflow + RWTexFFT_R[tid] = float2(0, 0); + RWTexFFT_G[tid] = float2(0, 0); + RWTexFFT_B[tid] = float2(0, 0); + } +} diff --git a/features/Post Processing/Shaders/PostProcessing/Vignette/vignette.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/Vignette/vignette.cs.hlsl new file mode 100644 index 0000000000..5114491db2 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/Vignette/vignette.cs.hlsl @@ -0,0 +1,23 @@ +RWTexture2D RWTexOut : register(u0); + +Texture2D TexColor : register(t0); + +cbuffer VignetteCB : register(b1) +{ + float4 Params0; // focal, anamorphism (included in aspect ratio), power, aspect ratio + float4 RcpDynRes; +}; + +[numthreads(8, 8, 1)] void main(uint2 tid : SV_DispatchThreadID) { + float3 color = TexColor[tid].rgb; + + float2 uv = (tid + .5) * RcpDynRes.xy; + + float cos_view = length((uv - .5) * float2(1, Params0.w)); + cos_view = Params0.x * rsqrt(cos_view * cos_view + Params0.x * Params0.x); + float vignette = pow(cos_view, Params0.z); + + color *= vignette; + + RWTexOut[tid] = float4(color, 1); +} \ No newline at end of file diff --git a/features/Post Processing/Shaders/PostProcessing/common.hlsli b/features/Post Processing/Shaders/PostProcessing/common.hlsli new file mode 100644 index 0000000000..fc9e6bd7b5 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/common.hlsli @@ -0,0 +1,223 @@ +#include "Common/Color.hlsli" + +float4 KarisAverage(float4 a, float4 b, float4 c, float4 d) +{ + float wa = rcp(1 + Color::RGBToLuminance(a.rgb)); + float wb = rcp(1 + Color::RGBToLuminance(b.rgb)); + float wc = rcp(1 + Color::RGBToLuminance(c.rgb)); + float wd = rcp(1 + Color::RGBToLuminance(d.rgb)); + float wsum = wa + wb + wc + wd; + return (a * wa + b * wb + c * wc + d * wd) / wsum; +} + +// Maybe rewrite as fetch +float4 DownsampleCOD(Texture2D tex, SamplerState samp, float2 uv, float2 out_px_size) +{ + int x, y; + + float4 retval = 0; + + [unroll] for (x = 0; x < 2; ++x) + [unroll] for (y = 0; y < 2; ++y) + retval += 0.125 * tex.SampleLevel(samp, uv + (int2(x, y) - .5) * out_px_size, 0); + + // const static float weights[9] = { 0.03125, 0.625, 0.03125, 0.625, 0.125, 0.625, 0.03125, 0.625, 0.03125 }; + // corresponds to (1 << (!x + !y)) * 0.03125 when $x,y \in [-1, 1] \cap \mathbb N$ + [unroll] for (x = -1; x <= 1; ++x) + [unroll] for (y = -1; y <= 1; ++y) + retval += (1u << (!x + !y)) * 0.03125 * tex.SampleLevel(samp, uv + int2(x, y) * out_px_size, 0); + + return retval; +} + +float4 DownsampleCODFirstMip(Texture2D tex, SamplerState samp, float2 uv, float2 out_px_size) +{ + int x, y; + + float4 retval = 0; + float4 fetches2x2[4]; + float4 fetches3x3[9]; + + [unroll] for (x = 0; x < 2; ++x) + [unroll] for (y = 0; y < 2; ++y) + fetches2x2[x * 2 + y] = tex.SampleLevel(samp, uv + (int2(x, y) * 2 - 1) * out_px_size, 0); + [unroll] for (x = 0; x < 3; ++x) + [unroll] for (y = 0; y < 3; ++y) + fetches3x3[x * 3 + y] = tex.SampleLevel(samp, uv + (int2(x, y) - 1) * 2 * out_px_size, 0); + + retval += 0.5 * KarisAverage(fetches2x2[0], fetches2x2[1], fetches2x2[2], fetches2x2[3]); + + [unroll] for (x = 0; x < 2; ++x) + [unroll] for (y = 0; y < 2; ++y) + retval += 0.125 * KarisAverage(fetches3x3[x * 3 + y], fetches3x3[(x + 1) * 3 + y], fetches3x3[x * 3 + y + 1], fetches3x3[(x + 1) * 3 + y + 1]); + + return retval; +} + +/* + OpenColorIO + url: https://github.com/AcademySoftwareFoundation/OpenColorIO/ + license: + Copyright Contributors to the OpenColorIO Project. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +float3 LogContrast(float3 col, float3 contrast, float3 pivot) +{ + return lerp(pivot, col, contrast); +} + +float3 LinearContrast(float3 col, float3 contrast, float3 pivot) +{ + col = col / pivot; + float3 sgn = sign(col); + col = pow(abs(col), contrast) * pivot; + col *= sgn; + return col; +} + +float3 Gamma(float3 col, float3 gamma, float3 black_pivot, float3 white_pivot) +{ + col = col - black_pivot; + float3 sgn = sign(col); + float3 range = white_pivot - black_pivot; + col = col / range; + col = pow(max(0, col), gamma); + col = col * sgn * range + black_pivot; + return col; +} + +float3 Saturation(float3 col, float sat) +{ + float luma = Color::RGBToLuminance(col); + return lerp(luma, col, sat); +} + +// https://www.shadertoy.com/view/MdjBRy +float3 HueShift(float3 col, float shift) +{ + float3 P = 0.55735 * dot(0.55735, col); + float3 U = col - P; + float3 V = cross(0.55735, U); + col = U * cos(shift * 6.2832) + V * sin(shift * 6.2832) + P; + return col; +} + +float3 ASC_CDL(float3 col, float3 slope, float3 power, float3 offset) +{ + return Gamma(col * slope + offset, power, 0, 1); +} + +//////////////////////////////////////////////////////////////////////// + +// +// RGB / Full-range YCbCr conversions (ITU-R BT.601) +// +float3 RgbToYCbCr(float3 c) +{ + float Y = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b; + float Cb = -0.169 * c.r - 0.331 * c.g + 0.500 * c.b; + float Cr = 0.500 * c.r - 0.419 * c.g - 0.081 * c.b; + return float3(Y, Cb, Cr); +} + +float3 YCbCrToRgb(float3 c) +{ + float R = c.x + 0.000 * c.y + 1.403 * c.z; + float G = c.x - 0.344 * c.y - 0.714 * c.z; + float B = c.x - 1.773 * c.y + 0.000 * c.z; + return float3(R, G, B); +} + +float3 RgbToOklab(float3 c) +{ + float l = 0.4121656120f * c.r + 0.5362752080f * c.g + 0.0514575653f * c.b; + float m = 0.2118591070f * c.r + 0.6807189584f * c.g + 0.1074065790f * c.b; + float s = 0.0883097947f * c.r + 0.2818474174f * c.g + 0.6302613616f * c.b; + + // Sign-preserving cube root: handles negative LMS from wide-gamut/HDR inputs + float l_ = sign(l) * pow(abs(l), 1. / 3.); + float m_ = sign(m) * pow(abs(m), 1. / 3.); + float s_ = sign(s) * pow(abs(s), 1. / 3.); + + float3 labResult; + labResult.x = 0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_; + labResult.y = 1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_; + labResult.z = 0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_; + + return labResult; +} + +float3 OklabToRgb(float3 c) +{ + float l_ = c.x + 0.3963377774f * c.y + 0.2158037573f * c.z; + float m_ = c.x - 0.1055613458f * c.y - 0.0638541728f * c.z; + float s_ = c.x - 0.0894841775f * c.y - 1.2914855480f * c.z; + + float l = l_ * l_ * l_; + float m = m_ * m_ * m_; + float s = s_ * s_ * s_; + + float3 rgbResult; + rgbResult.r = +4.0767245293f * l - 3.3072168827f * m + 0.2307590544f * s; + rgbResult.g = -1.2681437731f * l + 2.6093323231f * m - 0.3411344290f * s; + rgbResult.b = -0.0041119885f * l - 0.7034763098f * m + 1.7068625689f * s; + return rgbResult; +} + +// Luma-Framework - Filippo Tarpini +// This does basic gamut mapping. +// The color is expected to be in the specified color space and in linear. +// This works best when called after grading or tonemapping (by channel) in a wider color space than the current one. +float3 CorrectOutOfRangeColor(float3 Color, float3 LuminanceVec) +{ + float minChannel = min(Color.r, min(Color.g, Color.b)); + if (minChannel < 0.0) // Optional "optimization" branch + { + float colorLuminance = dot(Color, LuminanceVec); + // Desaturate (move towards luminance/grayscale) until we are not out of gamut anymore (until no channel is below 0) + if (colorLuminance >= 0.0) { + float desaturateAlpha = (minChannel - colorLuminance) != 0.0 ? (minChannel / (minChannel - colorLuminance)) : 0.0; // Both division elements are meant to be negative so the ratio resolves to a positive value + Color = lerp(Color, colorLuminance, desaturateAlpha); + } + } + return Color; +} + +float4 SampleCA(Texture2D tex, SamplerState samp, float2 texcoord, float strength, uint mipLevel) +{ + float3 influence = float3(0.04, 0.0, 0.03); + float2 CAr = (texcoord - 0.5) * (1.0 - strength * influence.r) + 0.5; + float2 CAb = (texcoord - 0.5) * (1.0 + strength * influence.b) + 0.5; + + float4 color; + color.r = tex.SampleLevel(samp, CAr, mipLevel).r; + color.ga = tex.SampleLevel(samp, texcoord, mipLevel).ga; + color.b = tex.SampleLevel(samp, CAb, mipLevel).b; + + return color; +} diff --git a/features/Post Processing/Shaders/PostProcessing/copy.cs.hlsl b/features/Post Processing/Shaders/PostProcessing/copy.cs.hlsl new file mode 100644 index 0000000000..5dfe2b6de8 --- /dev/null +++ b/features/Post Processing/Shaders/PostProcessing/copy.cs.hlsl @@ -0,0 +1,6 @@ +Texture2D texSrc : register(t0); +RWTexture2D texOut : register(u0); + +[numthreads(8, 8, 1)] void main(uint2 tid : SV_DispatchThreadID) { + texOut[tid] = texSrc[tid]; +} \ No newline at end of file diff --git a/features/Post Processing/textures/rgbnoise.dds b/features/Post Processing/textures/rgbnoise.dds new file mode 100644 index 0000000000..7afe8903da Binary files /dev/null and b/features/Post Processing/textures/rgbnoise.dds differ diff --git a/features/Post Processing/textures/starburst.dds b/features/Post Processing/textures/starburst.dds new file mode 100644 index 0000000000..69a02f962f Binary files /dev/null and b/features/Post Processing/textures/starburst.dds differ diff --git a/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini b/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini index 6192a431ca..c17871c6b1 100644 --- a/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini +++ b/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini @@ -1,5 +1,5 @@ [Info] -Version = 3-2-0 +Version = 3-2-1 [Nexus] nexusmodid = 114114 diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli index 39281513cb..ed1ee401a9 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli @@ -61,6 +61,8 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, float sssAmount, bool hum float3 originalColor = centerColor.xyz; float4 diffuseMeanFreePath = humanProfile ? MeanFreePathHuman : MeanFreePathBase; + // MFP channels are defined relative to color space primaries; convert to working gamut + diffuseMeanFreePath.xyz = Color::GamutTransform(diffuseMeanFreePath.xyz); diffuseMeanFreePath.xyz = float3(max(diffuseMeanFreePath.x, 1e-5f), max(diffuseMeanFreePath.y, 1e-5f), max(diffuseMeanFreePath.z, 1e-5f)); diffuseMeanFreePath *= sssAmount; diff --git a/features/Terrain Variation/Shaders/Features/TerrainVariation.ini b/features/Terrain Variation/Shaders/Features/TerrainVariation.ini index 1b41d34dd1..e6e74b9fb6 100644 --- a/features/Terrain Variation/Shaders/Features/TerrainVariation.ini +++ b/features/Terrain Variation/Shaders/Features/TerrainVariation.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-0-1 +Version = 1-0-2 [Nexus] nexusmodid = 148123 diff --git a/features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli b/features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli index 1a34cd0df6..3b21e28dce 100644 --- a/features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli +++ b/features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli @@ -5,6 +5,7 @@ #ifndef TERRAIN_VARIATION_HLSLI #define TERRAIN_VARIATION_HLSLI +#include "Common/Color.hlsli" #include "Common/Math.hlsli" #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" @@ -18,7 +19,7 @@ static const float2x2 SKEW_MATRIX = float2x2(1.0, 0.0, -0.57735027, 1.15470054); static const float WORLD_SCALE = 332.54; // Blending constants static const float3 DEFAULT_WEIGHTS = float3(0.33, 0.33, 0.34); -static const float3 LUMINANCE_WEIGHTS = float3(0.2126, 0.7152, 0.0722); +// Luminance weights now use Color::RGBToLuminance() for correct gamut handling // Hash constants static const float2 HASH_MULTIPLIER = float2(1271.5151, 3337.8237); // Performance optimization constants @@ -157,9 +158,9 @@ inline float4 StochasticEffect(Texture2D tex, SamplerState samp, float2 uv, Stoc // Height calculation - use luminance for RGB data, alpha when available float3 luminanceHeights = float3( - dot(sample1.rgb, LUMINANCE_WEIGHTS), - dot(sample2.rgb, LUMINANCE_WEIGHTS), - dot(sample3.rgb, LUMINANCE_WEIGHTS)); + Color::RGBToLuminance(sample1.rgb), + Color::RGBToLuminance(sample2.rgb), + Color::RGBToLuminance(sample3.rgb)); float3 alphaValues = float3(sample1.a, sample2.a, sample3.a); float3 alphaMask = step(0.001, alphaValues); diff --git a/include/IconsFontAwesome5.h b/include/IconsFontAwesome5.h new file mode 100644 index 0000000000..0c5acab774 --- /dev/null +++ b/include/IconsFontAwesome5.h @@ -0,0 +1,1016 @@ +// Generated by https://github.com/juliettef/IconFontCppHeaders script GenerateIconFontCppHeaders.py +// for C and C++ +// from codepoints https://github.com/FortAwesome/Font-Awesome/raw/5.x/metadata/icons.yml +// for use with font https://github.com/FortAwesome/Font-Awesome/blob/5.x/webfonts/fa-regular-400.ttf, https://github.com/FortAwesome/Font-Awesome/blob/5.x/webfonts/fa-solid-900.ttf + +#pragma once + +#define FONT_ICON_FILE_NAME_FAR "fa-regular-400.ttf" +#define FONT_ICON_FILE_NAME_FAS "fa-solid-900.ttf" + +#define ICON_MIN_FA 0xe005 +#define ICON_MAX_16_FA 0xf8ff +#define ICON_MAX_FA 0xf8ff + +#define ICON_FA_AD "\xef\x99\x81" // U+f641 +#define ICON_FA_ADDRESS_BOOK "\xef\x8a\xb9" // U+f2b9 +#define ICON_FA_ADDRESS_CARD "\xef\x8a\xbb" // U+f2bb +#define ICON_FA_ADJUST "\xef\x81\x82" // U+f042 +#define ICON_FA_AIR_FRESHENER "\xef\x97\x90" // U+f5d0 +#define ICON_FA_ALIGN_CENTER "\xef\x80\xb7" // U+f037 +#define ICON_FA_ALIGN_JUSTIFY "\xef\x80\xb9" // U+f039 +#define ICON_FA_ALIGN_LEFT "\xef\x80\xb6" // U+f036 +#define ICON_FA_ALIGN_RIGHT "\xef\x80\xb8" // U+f038 +#define ICON_FA_ALLERGIES "\xef\x91\xa1" // U+f461 +#define ICON_FA_AMBULANCE "\xef\x83\xb9" // U+f0f9 +#define ICON_FA_AMERICAN_SIGN_LANGUAGE_INTERPRETING "\xef\x8a\xa3" // U+f2a3 +#define ICON_FA_ANCHOR "\xef\x84\xbd" // U+f13d +#define ICON_FA_ANGLE_DOUBLE_DOWN "\xef\x84\x83" // U+f103 +#define ICON_FA_ANGLE_DOUBLE_LEFT "\xef\x84\x80" // U+f100 +#define ICON_FA_ANGLE_DOUBLE_RIGHT "\xef\x84\x81" // U+f101 +#define ICON_FA_ANGLE_DOUBLE_UP "\xef\x84\x82" // U+f102 +#define ICON_FA_ANGLE_DOWN "\xef\x84\x87" // U+f107 +#define ICON_FA_ANGLE_LEFT "\xef\x84\x84" // U+f104 +#define ICON_FA_ANGLE_RIGHT "\xef\x84\x85" // U+f105 +#define ICON_FA_ANGLE_UP "\xef\x84\x86" // U+f106 +#define ICON_FA_ANGRY "\xef\x95\x96" // U+f556 +#define ICON_FA_ANKH "\xef\x99\x84" // U+f644 +#define ICON_FA_APPLE_ALT "\xef\x97\x91" // U+f5d1 +#define ICON_FA_ARCHIVE "\xef\x86\x87" // U+f187 +#define ICON_FA_ARCHWAY "\xef\x95\x97" // U+f557 +#define ICON_FA_ARROW_ALT_CIRCLE_DOWN "\xef\x8d\x98" // U+f358 +#define ICON_FA_ARROW_ALT_CIRCLE_LEFT "\xef\x8d\x99" // U+f359 +#define ICON_FA_ARROW_ALT_CIRCLE_RIGHT "\xef\x8d\x9a" // U+f35a +#define ICON_FA_ARROW_ALT_CIRCLE_UP "\xef\x8d\x9b" // U+f35b +#define ICON_FA_ARROW_CIRCLE_DOWN "\xef\x82\xab" // U+f0ab +#define ICON_FA_ARROW_CIRCLE_LEFT "\xef\x82\xa8" // U+f0a8 +#define ICON_FA_ARROW_CIRCLE_RIGHT "\xef\x82\xa9" // U+f0a9 +#define ICON_FA_ARROW_CIRCLE_UP "\xef\x82\xaa" // U+f0aa +#define ICON_FA_ARROW_DOWN "\xef\x81\xa3" // U+f063 +#define ICON_FA_ARROW_LEFT "\xef\x81\xa0" // U+f060 +#define ICON_FA_ARROW_RIGHT "\xef\x81\xa1" // U+f061 +#define ICON_FA_ARROW_UP "\xef\x81\xa2" // U+f062 +#define ICON_FA_ARROWS_ALT "\xef\x82\xb2" // U+f0b2 +#define ICON_FA_ARROWS_ALT_H "\xef\x8c\xb7" // U+f337 +#define ICON_FA_ARROWS_ALT_V "\xef\x8c\xb8" // U+f338 +#define ICON_FA_ASSISTIVE_LISTENING_SYSTEMS "\xef\x8a\xa2" // U+f2a2 +#define ICON_FA_ASTERISK "\xef\x81\xa9" // U+f069 +#define ICON_FA_AT "\xef\x87\xba" // U+f1fa +#define ICON_FA_ATLAS "\xef\x95\x98" // U+f558 +#define ICON_FA_ATOM "\xef\x97\x92" // U+f5d2 +#define ICON_FA_AUDIO_DESCRIPTION "\xef\x8a\x9e" // U+f29e +#define ICON_FA_AWARD "\xef\x95\x99" // U+f559 +#define ICON_FA_BABY "\xef\x9d\xbc" // U+f77c +#define ICON_FA_BABY_CARRIAGE "\xef\x9d\xbd" // U+f77d +#define ICON_FA_BACKSPACE "\xef\x95\x9a" // U+f55a +#define ICON_FA_BACKWARD "\xef\x81\x8a" // U+f04a +#define ICON_FA_BACON "\xef\x9f\xa5" // U+f7e5 +#define ICON_FA_BACTERIA "\xee\x81\x99" // U+e059 +#define ICON_FA_BACTERIUM "\xee\x81\x9a" // U+e05a +#define ICON_FA_BAHAI "\xef\x99\xa6" // U+f666 +#define ICON_FA_BALANCE_SCALE "\xef\x89\x8e" // U+f24e +#define ICON_FA_BALANCE_SCALE_LEFT "\xef\x94\x95" // U+f515 +#define ICON_FA_BALANCE_SCALE_RIGHT "\xef\x94\x96" // U+f516 +#define ICON_FA_BAN "\xef\x81\x9e" // U+f05e +#define ICON_FA_BAND_AID "\xef\x91\xa2" // U+f462 +#define ICON_FA_BARCODE "\xef\x80\xaa" // U+f02a +#define ICON_FA_BARS "\xef\x83\x89" // U+f0c9 +#define ICON_FA_BASEBALL_BALL "\xef\x90\xb3" // U+f433 +#define ICON_FA_BASKETBALL_BALL "\xef\x90\xb4" // U+f434 +#define ICON_FA_BATH "\xef\x8b\x8d" // U+f2cd +#define ICON_FA_BATTERY_EMPTY "\xef\x89\x84" // U+f244 +#define ICON_FA_BATTERY_FULL "\xef\x89\x80" // U+f240 +#define ICON_FA_BATTERY_HALF "\xef\x89\x82" // U+f242 +#define ICON_FA_BATTERY_QUARTER "\xef\x89\x83" // U+f243 +#define ICON_FA_BATTERY_THREE_QUARTERS "\xef\x89\x81" // U+f241 +#define ICON_FA_BED "\xef\x88\xb6" // U+f236 +#define ICON_FA_BEER "\xef\x83\xbc" // U+f0fc +#define ICON_FA_BELL "\xef\x83\xb3" // U+f0f3 +#define ICON_FA_BELL_SLASH "\xef\x87\xb6" // U+f1f6 +#define ICON_FA_BEZIER_CURVE "\xef\x95\x9b" // U+f55b +#define ICON_FA_BIBLE "\xef\x99\x87" // U+f647 +#define ICON_FA_BICYCLE "\xef\x88\x86" // U+f206 +#define ICON_FA_BIKING "\xef\xa1\x8a" // U+f84a +#define ICON_FA_BINOCULARS "\xef\x87\xa5" // U+f1e5 +#define ICON_FA_BIOHAZARD "\xef\x9e\x80" // U+f780 +#define ICON_FA_BIRTHDAY_CAKE "\xef\x87\xbd" // U+f1fd +#define ICON_FA_BLENDER "\xef\x94\x97" // U+f517 +#define ICON_FA_BLENDER_PHONE "\xef\x9a\xb6" // U+f6b6 +#define ICON_FA_BLIND "\xef\x8a\x9d" // U+f29d +#define ICON_FA_BLOG "\xef\x9e\x81" // U+f781 +#define ICON_FA_BOLD "\xef\x80\xb2" // U+f032 +#define ICON_FA_BOLT "\xef\x83\xa7" // U+f0e7 +#define ICON_FA_BOMB "\xef\x87\xa2" // U+f1e2 +#define ICON_FA_BONE "\xef\x97\x97" // U+f5d7 +#define ICON_FA_BONG "\xef\x95\x9c" // U+f55c +#define ICON_FA_BOOK "\xef\x80\xad" // U+f02d +#define ICON_FA_BOOK_DEAD "\xef\x9a\xb7" // U+f6b7 +#define ICON_FA_BOOK_MEDICAL "\xef\x9f\xa6" // U+f7e6 +#define ICON_FA_BOOK_OPEN "\xef\x94\x98" // U+f518 +#define ICON_FA_BOOK_READER "\xef\x97\x9a" // U+f5da +#define ICON_FA_BOOKMARK "\xef\x80\xae" // U+f02e +#define ICON_FA_BORDER_ALL "\xef\xa1\x8c" // U+f84c +#define ICON_FA_BORDER_NONE "\xef\xa1\x90" // U+f850 +#define ICON_FA_BORDER_STYLE "\xef\xa1\x93" // U+f853 +#define ICON_FA_BOWLING_BALL "\xef\x90\xb6" // U+f436 +#define ICON_FA_BOX "\xef\x91\xa6" // U+f466 +#define ICON_FA_BOX_OPEN "\xef\x92\x9e" // U+f49e +#define ICON_FA_BOX_TISSUE "\xee\x81\x9b" // U+e05b +#define ICON_FA_BOXES "\xef\x91\xa8" // U+f468 +#define ICON_FA_BRAILLE "\xef\x8a\xa1" // U+f2a1 +#define ICON_FA_BRAIN "\xef\x97\x9c" // U+f5dc +#define ICON_FA_BREAD_SLICE "\xef\x9f\xac" // U+f7ec +#define ICON_FA_BRIEFCASE "\xef\x82\xb1" // U+f0b1 +#define ICON_FA_BRIEFCASE_MEDICAL "\xef\x91\xa9" // U+f469 +#define ICON_FA_BROADCAST_TOWER "\xef\x94\x99" // U+f519 +#define ICON_FA_BROOM "\xef\x94\x9a" // U+f51a +#define ICON_FA_BRUSH "\xef\x95\x9d" // U+f55d +#define ICON_FA_BUG "\xef\x86\x88" // U+f188 +#define ICON_FA_BUILDING "\xef\x86\xad" // U+f1ad +#define ICON_FA_BULLHORN "\xef\x82\xa1" // U+f0a1 +#define ICON_FA_BULLSEYE "\xef\x85\x80" // U+f140 +#define ICON_FA_BURN "\xef\x91\xaa" // U+f46a +#define ICON_FA_BUS "\xef\x88\x87" // U+f207 +#define ICON_FA_BUS_ALT "\xef\x95\x9e" // U+f55e +#define ICON_FA_BUSINESS_TIME "\xef\x99\x8a" // U+f64a +#define ICON_FA_CALCULATOR "\xef\x87\xac" // U+f1ec +#define ICON_FA_CALENDAR "\xef\x84\xb3" // U+f133 +#define ICON_FA_CALENDAR_ALT "\xef\x81\xb3" // U+f073 +#define ICON_FA_CALENDAR_CHECK "\xef\x89\xb4" // U+f274 +#define ICON_FA_CALENDAR_DAY "\xef\x9e\x83" // U+f783 +#define ICON_FA_CALENDAR_MINUS "\xef\x89\xb2" // U+f272 +#define ICON_FA_CALENDAR_PLUS "\xef\x89\xb1" // U+f271 +#define ICON_FA_CALENDAR_TIMES "\xef\x89\xb3" // U+f273 +#define ICON_FA_CALENDAR_WEEK "\xef\x9e\x84" // U+f784 +#define ICON_FA_CAMERA "\xef\x80\xb0" // U+f030 +#define ICON_FA_CAMERA_RETRO "\xef\x82\x83" // U+f083 +#define ICON_FA_CAMPGROUND "\xef\x9a\xbb" // U+f6bb +#define ICON_FA_CANDY_CANE "\xef\x9e\x86" // U+f786 +#define ICON_FA_CANNABIS "\xef\x95\x9f" // U+f55f +#define ICON_FA_CAPSULES "\xef\x91\xab" // U+f46b +#define ICON_FA_CAR "\xef\x86\xb9" // U+f1b9 +#define ICON_FA_CAR_ALT "\xef\x97\x9e" // U+f5de +#define ICON_FA_CAR_BATTERY "\xef\x97\x9f" // U+f5df +#define ICON_FA_CAR_CRASH "\xef\x97\xa1" // U+f5e1 +#define ICON_FA_CAR_SIDE "\xef\x97\xa4" // U+f5e4 +#define ICON_FA_CARAVAN "\xef\xa3\xbf" // U+f8ff +#define ICON_FA_CARET_DOWN "\xef\x83\x97" // U+f0d7 +#define ICON_FA_CARET_LEFT "\xef\x83\x99" // U+f0d9 +#define ICON_FA_CARET_RIGHT "\xef\x83\x9a" // U+f0da +#define ICON_FA_CARET_SQUARE_DOWN "\xef\x85\x90" // U+f150 +#define ICON_FA_CARET_SQUARE_LEFT "\xef\x86\x91" // U+f191 +#define ICON_FA_CARET_SQUARE_RIGHT "\xef\x85\x92" // U+f152 +#define ICON_FA_CARET_SQUARE_UP "\xef\x85\x91" // U+f151 +#define ICON_FA_CARET_UP "\xef\x83\x98" // U+f0d8 +#define ICON_FA_CARROT "\xef\x9e\x87" // U+f787 +#define ICON_FA_CART_ARROW_DOWN "\xef\x88\x98" // U+f218 +#define ICON_FA_CART_PLUS "\xef\x88\x97" // U+f217 +#define ICON_FA_CASH_REGISTER "\xef\x9e\x88" // U+f788 +#define ICON_FA_CAT "\xef\x9a\xbe" // U+f6be +#define ICON_FA_CERTIFICATE "\xef\x82\xa3" // U+f0a3 +#define ICON_FA_CHAIR "\xef\x9b\x80" // U+f6c0 +#define ICON_FA_CHALKBOARD "\xef\x94\x9b" // U+f51b +#define ICON_FA_CHALKBOARD_TEACHER "\xef\x94\x9c" // U+f51c +#define ICON_FA_CHARGING_STATION "\xef\x97\xa7" // U+f5e7 +#define ICON_FA_CHART_AREA "\xef\x87\xbe" // U+f1fe +#define ICON_FA_CHART_BAR "\xef\x82\x80" // U+f080 +#define ICON_FA_CHART_LINE "\xef\x88\x81" // U+f201 +#define ICON_FA_CHART_PIE "\xef\x88\x80" // U+f200 +#define ICON_FA_CHECK "\xef\x80\x8c" // U+f00c +#define ICON_FA_CHECK_CIRCLE "\xef\x81\x98" // U+f058 +#define ICON_FA_CHECK_DOUBLE "\xef\x95\xa0" // U+f560 +#define ICON_FA_CHECK_SQUARE "\xef\x85\x8a" // U+f14a +#define ICON_FA_CHEESE "\xef\x9f\xaf" // U+f7ef +#define ICON_FA_CHESS "\xef\x90\xb9" // U+f439 +#define ICON_FA_CHESS_BISHOP "\xef\x90\xba" // U+f43a +#define ICON_FA_CHESS_BOARD "\xef\x90\xbc" // U+f43c +#define ICON_FA_CHESS_KING "\xef\x90\xbf" // U+f43f +#define ICON_FA_CHESS_KNIGHT "\xef\x91\x81" // U+f441 +#define ICON_FA_CHESS_PAWN "\xef\x91\x83" // U+f443 +#define ICON_FA_CHESS_QUEEN "\xef\x91\x85" // U+f445 +#define ICON_FA_CHESS_ROOK "\xef\x91\x87" // U+f447 +#define ICON_FA_CHEVRON_CIRCLE_DOWN "\xef\x84\xba" // U+f13a +#define ICON_FA_CHEVRON_CIRCLE_LEFT "\xef\x84\xb7" // U+f137 +#define ICON_FA_CHEVRON_CIRCLE_RIGHT "\xef\x84\xb8" // U+f138 +#define ICON_FA_CHEVRON_CIRCLE_UP "\xef\x84\xb9" // U+f139 +#define ICON_FA_CHEVRON_DOWN "\xef\x81\xb8" // U+f078 +#define ICON_FA_CHEVRON_LEFT "\xef\x81\x93" // U+f053 +#define ICON_FA_CHEVRON_RIGHT "\xef\x81\x94" // U+f054 +#define ICON_FA_CHEVRON_UP "\xef\x81\xb7" // U+f077 +#define ICON_FA_CHILD "\xef\x86\xae" // U+f1ae +#define ICON_FA_CHURCH "\xef\x94\x9d" // U+f51d +#define ICON_FA_CIRCLE "\xef\x84\x91" // U+f111 +#define ICON_FA_CIRCLE_NOTCH "\xef\x87\x8e" // U+f1ce +#define ICON_FA_CITY "\xef\x99\x8f" // U+f64f +#define ICON_FA_CLINIC_MEDICAL "\xef\x9f\xb2" // U+f7f2 +#define ICON_FA_CLIPBOARD "\xef\x8c\xa8" // U+f328 +#define ICON_FA_CLIPBOARD_CHECK "\xef\x91\xac" // U+f46c +#define ICON_FA_CLIPBOARD_LIST "\xef\x91\xad" // U+f46d +#define ICON_FA_CLOCK "\xef\x80\x97" // U+f017 +#define ICON_FA_CLONE "\xef\x89\x8d" // U+f24d +#define ICON_FA_CLOSED_CAPTIONING "\xef\x88\x8a" // U+f20a +#define ICON_FA_CLOUD "\xef\x83\x82" // U+f0c2 +#define ICON_FA_CLOUD_DOWNLOAD_ALT "\xef\x8e\x81" // U+f381 +#define ICON_FA_CLOUD_MEATBALL "\xef\x9c\xbb" // U+f73b +#define ICON_FA_CLOUD_MOON "\xef\x9b\x83" // U+f6c3 +#define ICON_FA_CLOUD_MOON_RAIN "\xef\x9c\xbc" // U+f73c +#define ICON_FA_CLOUD_RAIN "\xef\x9c\xbd" // U+f73d +#define ICON_FA_CLOUD_SHOWERS_HEAVY "\xef\x9d\x80" // U+f740 +#define ICON_FA_CLOUD_SUN "\xef\x9b\x84" // U+f6c4 +#define ICON_FA_CLOUD_SUN_RAIN "\xef\x9d\x83" // U+f743 +#define ICON_FA_CLOUD_UPLOAD_ALT "\xef\x8e\x82" // U+f382 +#define ICON_FA_COCKTAIL "\xef\x95\xa1" // U+f561 +#define ICON_FA_CODE "\xef\x84\xa1" // U+f121 +#define ICON_FA_CODE_BRANCH "\xef\x84\xa6" // U+f126 +#define ICON_FA_COFFEE "\xef\x83\xb4" // U+f0f4 +#define ICON_FA_COG "\xef\x80\x93" // U+f013 +#define ICON_FA_COGS "\xef\x82\x85" // U+f085 +#define ICON_FA_COINS "\xef\x94\x9e" // U+f51e +#define ICON_FA_COLUMNS "\xef\x83\x9b" // U+f0db +#define ICON_FA_COMMENT "\xef\x81\xb5" // U+f075 +#define ICON_FA_COMMENT_ALT "\xef\x89\xba" // U+f27a +#define ICON_FA_COMMENT_DOLLAR "\xef\x99\x91" // U+f651 +#define ICON_FA_COMMENT_DOTS "\xef\x92\xad" // U+f4ad +#define ICON_FA_COMMENT_MEDICAL "\xef\x9f\xb5" // U+f7f5 +#define ICON_FA_COMMENT_SLASH "\xef\x92\xb3" // U+f4b3 +#define ICON_FA_COMMENTS "\xef\x82\x86" // U+f086 +#define ICON_FA_COMMENTS_DOLLAR "\xef\x99\x93" // U+f653 +#define ICON_FA_COMPACT_DISC "\xef\x94\x9f" // U+f51f +#define ICON_FA_COMPASS "\xef\x85\x8e" // U+f14e +#define ICON_FA_COMPRESS "\xef\x81\xa6" // U+f066 +#define ICON_FA_COMPRESS_ALT "\xef\x90\xa2" // U+f422 +#define ICON_FA_COMPRESS_ARROWS_ALT "\xef\x9e\x8c" // U+f78c +#define ICON_FA_CONCIERGE_BELL "\xef\x95\xa2" // U+f562 +#define ICON_FA_COOKIE "\xef\x95\xa3" // U+f563 +#define ICON_FA_COOKIE_BITE "\xef\x95\xa4" // U+f564 +#define ICON_FA_COPY "\xef\x83\x85" // U+f0c5 +#define ICON_FA_COPYRIGHT "\xef\x87\xb9" // U+f1f9 +#define ICON_FA_COUCH "\xef\x92\xb8" // U+f4b8 +#define ICON_FA_CREDIT_CARD "\xef\x82\x9d" // U+f09d +#define ICON_FA_CROP "\xef\x84\xa5" // U+f125 +#define ICON_FA_CROP_ALT "\xef\x95\xa5" // U+f565 +#define ICON_FA_CROSS "\xef\x99\x94" // U+f654 +#define ICON_FA_CROSSHAIRS "\xef\x81\x9b" // U+f05b +#define ICON_FA_CROW "\xef\x94\xa0" // U+f520 +#define ICON_FA_CROWN "\xef\x94\xa1" // U+f521 +#define ICON_FA_CRUTCH "\xef\x9f\xb7" // U+f7f7 +#define ICON_FA_CUBE "\xef\x86\xb2" // U+f1b2 +#define ICON_FA_CUBES "\xef\x86\xb3" // U+f1b3 +#define ICON_FA_CUT "\xef\x83\x84" // U+f0c4 +#define ICON_FA_DATABASE "\xef\x87\x80" // U+f1c0 +#define ICON_FA_DEAF "\xef\x8a\xa4" // U+f2a4 +#define ICON_FA_DEMOCRAT "\xef\x9d\x87" // U+f747 +#define ICON_FA_DESKTOP "\xef\x84\x88" // U+f108 +#define ICON_FA_DHARMACHAKRA "\xef\x99\x95" // U+f655 +#define ICON_FA_DIAGNOSES "\xef\x91\xb0" // U+f470 +#define ICON_FA_DICE "\xef\x94\xa2" // U+f522 +#define ICON_FA_DICE_D20 "\xef\x9b\x8f" // U+f6cf +#define ICON_FA_DICE_D6 "\xef\x9b\x91" // U+f6d1 +#define ICON_FA_DICE_FIVE "\xef\x94\xa3" // U+f523 +#define ICON_FA_DICE_FOUR "\xef\x94\xa4" // U+f524 +#define ICON_FA_DICE_ONE "\xef\x94\xa5" // U+f525 +#define ICON_FA_DICE_SIX "\xef\x94\xa6" // U+f526 +#define ICON_FA_DICE_THREE "\xef\x94\xa7" // U+f527 +#define ICON_FA_DICE_TWO "\xef\x94\xa8" // U+f528 +#define ICON_FA_DIGITAL_TACHOGRAPH "\xef\x95\xa6" // U+f566 +#define ICON_FA_DIRECTIONS "\xef\x97\xab" // U+f5eb +#define ICON_FA_DISEASE "\xef\x9f\xba" // U+f7fa +#define ICON_FA_DIVIDE "\xef\x94\xa9" // U+f529 +#define ICON_FA_DIZZY "\xef\x95\xa7" // U+f567 +#define ICON_FA_DNA "\xef\x91\xb1" // U+f471 +#define ICON_FA_DOG "\xef\x9b\x93" // U+f6d3 +#define ICON_FA_DOLLAR_SIGN "\xef\x85\x95" // U+f155 +#define ICON_FA_DOLLY "\xef\x91\xb2" // U+f472 +#define ICON_FA_DOLLY_FLATBED "\xef\x91\xb4" // U+f474 +#define ICON_FA_DONATE "\xef\x92\xb9" // U+f4b9 +#define ICON_FA_DOOR_CLOSED "\xef\x94\xaa" // U+f52a +#define ICON_FA_DOOR_OPEN "\xef\x94\xab" // U+f52b +#define ICON_FA_DOT_CIRCLE "\xef\x86\x92" // U+f192 +#define ICON_FA_DOVE "\xef\x92\xba" // U+f4ba +#define ICON_FA_DOWNLOAD "\xef\x80\x99" // U+f019 +#define ICON_FA_DRAFTING_COMPASS "\xef\x95\xa8" // U+f568 +#define ICON_FA_DRAGON "\xef\x9b\x95" // U+f6d5 +#define ICON_FA_DRAW_POLYGON "\xef\x97\xae" // U+f5ee +#define ICON_FA_DRUM "\xef\x95\xa9" // U+f569 +#define ICON_FA_DRUM_STEELPAN "\xef\x95\xaa" // U+f56a +#define ICON_FA_DRUMSTICK_BITE "\xef\x9b\x97" // U+f6d7 +#define ICON_FA_DUMBBELL "\xef\x91\x8b" // U+f44b +#define ICON_FA_DUMPSTER "\xef\x9e\x93" // U+f793 +#define ICON_FA_DUMPSTER_FIRE "\xef\x9e\x94" // U+f794 +#define ICON_FA_DUNGEON "\xef\x9b\x99" // U+f6d9 +#define ICON_FA_EDIT "\xef\x81\x84" // U+f044 +#define ICON_FA_EGG "\xef\x9f\xbb" // U+f7fb +#define ICON_FA_EJECT "\xef\x81\x92" // U+f052 +#define ICON_FA_ELLIPSIS_H "\xef\x85\x81" // U+f141 +#define ICON_FA_ELLIPSIS_V "\xef\x85\x82" // U+f142 +#define ICON_FA_ENVELOPE "\xef\x83\xa0" // U+f0e0 +#define ICON_FA_ENVELOPE_OPEN "\xef\x8a\xb6" // U+f2b6 +#define ICON_FA_ENVELOPE_OPEN_TEXT "\xef\x99\x98" // U+f658 +#define ICON_FA_ENVELOPE_SQUARE "\xef\x86\x99" // U+f199 +#define ICON_FA_EQUALS "\xef\x94\xac" // U+f52c +#define ICON_FA_ERASER "\xef\x84\xad" // U+f12d +#define ICON_FA_ETHERNET "\xef\x9e\x96" // U+f796 +#define ICON_FA_EURO_SIGN "\xef\x85\x93" // U+f153 +#define ICON_FA_EXCHANGE_ALT "\xef\x8d\xa2" // U+f362 +#define ICON_FA_EXCLAMATION "\xef\x84\xaa" // U+f12a +#define ICON_FA_EXCLAMATION_CIRCLE "\xef\x81\xaa" // U+f06a +#define ICON_FA_EXCLAMATION_TRIANGLE "\xef\x81\xb1" // U+f071 +#define ICON_FA_EXPAND "\xef\x81\xa5" // U+f065 +#define ICON_FA_EXPAND_ALT "\xef\x90\xa4" // U+f424 +#define ICON_FA_EXPAND_ARROWS_ALT "\xef\x8c\x9e" // U+f31e +#define ICON_FA_EXTERNAL_LINK_ALT "\xef\x8d\x9d" // U+f35d +#define ICON_FA_EXTERNAL_LINK_SQUARE_ALT "\xef\x8d\xa0" // U+f360 +#define ICON_FA_EYE "\xef\x81\xae" // U+f06e +#define ICON_FA_EYE_DROPPER "\xef\x87\xbb" // U+f1fb +#define ICON_FA_EYE_SLASH "\xef\x81\xb0" // U+f070 +#define ICON_FA_FAN "\xef\xa1\xa3" // U+f863 +#define ICON_FA_FAST_BACKWARD "\xef\x81\x89" // U+f049 +#define ICON_FA_FAST_FORWARD "\xef\x81\x90" // U+f050 +#define ICON_FA_FAUCET "\xee\x80\x85" // U+e005 +#define ICON_FA_FAX "\xef\x86\xac" // U+f1ac +#define ICON_FA_FEATHER "\xef\x94\xad" // U+f52d +#define ICON_FA_FEATHER_ALT "\xef\x95\xab" // U+f56b +#define ICON_FA_FEMALE "\xef\x86\x82" // U+f182 +#define ICON_FA_FIGHTER_JET "\xef\x83\xbb" // U+f0fb +#define ICON_FA_FILE "\xef\x85\x9b" // U+f15b +#define ICON_FA_FILE_ALT "\xef\x85\x9c" // U+f15c +#define ICON_FA_FILE_ARCHIVE "\xef\x87\x86" // U+f1c6 +#define ICON_FA_FILE_AUDIO "\xef\x87\x87" // U+f1c7 +#define ICON_FA_FILE_CODE "\xef\x87\x89" // U+f1c9 +#define ICON_FA_FILE_CONTRACT "\xef\x95\xac" // U+f56c +#define ICON_FA_FILE_CSV "\xef\x9b\x9d" // U+f6dd +#define ICON_FA_FILE_DOWNLOAD "\xef\x95\xad" // U+f56d +#define ICON_FA_FILE_EXCEL "\xef\x87\x83" // U+f1c3 +#define ICON_FA_FILE_EXPORT "\xef\x95\xae" // U+f56e +#define ICON_FA_FILE_IMAGE "\xef\x87\x85" // U+f1c5 +#define ICON_FA_FILE_IMPORT "\xef\x95\xaf" // U+f56f +#define ICON_FA_FILE_INVOICE "\xef\x95\xb0" // U+f570 +#define ICON_FA_FILE_INVOICE_DOLLAR "\xef\x95\xb1" // U+f571 +#define ICON_FA_FILE_MEDICAL "\xef\x91\xb7" // U+f477 +#define ICON_FA_FILE_MEDICAL_ALT "\xef\x91\xb8" // U+f478 +#define ICON_FA_FILE_PDF "\xef\x87\x81" // U+f1c1 +#define ICON_FA_FILE_POWERPOINT "\xef\x87\x84" // U+f1c4 +#define ICON_FA_FILE_PRESCRIPTION "\xef\x95\xb2" // U+f572 +#define ICON_FA_FILE_SIGNATURE "\xef\x95\xb3" // U+f573 +#define ICON_FA_FILE_UPLOAD "\xef\x95\xb4" // U+f574 +#define ICON_FA_FILE_VIDEO "\xef\x87\x88" // U+f1c8 +#define ICON_FA_FILE_WORD "\xef\x87\x82" // U+f1c2 +#define ICON_FA_FILL "\xef\x95\xb5" // U+f575 +#define ICON_FA_FILL_DRIP "\xef\x95\xb6" // U+f576 +#define ICON_FA_FILM "\xef\x80\x88" // U+f008 +#define ICON_FA_FILTER "\xef\x82\xb0" // U+f0b0 +#define ICON_FA_FINGERPRINT "\xef\x95\xb7" // U+f577 +#define ICON_FA_FIRE "\xef\x81\xad" // U+f06d +#define ICON_FA_FIRE_ALT "\xef\x9f\xa4" // U+f7e4 +#define ICON_FA_FIRE_EXTINGUISHER "\xef\x84\xb4" // U+f134 +#define ICON_FA_FIRST_AID "\xef\x91\xb9" // U+f479 +#define ICON_FA_FISH "\xef\x95\xb8" // U+f578 +#define ICON_FA_FIST_RAISED "\xef\x9b\x9e" // U+f6de +#define ICON_FA_FLAG "\xef\x80\xa4" // U+f024 +#define ICON_FA_FLAG_CHECKERED "\xef\x84\x9e" // U+f11e +#define ICON_FA_FLAG_USA "\xef\x9d\x8d" // U+f74d +#define ICON_FA_FLASK "\xef\x83\x83" // U+f0c3 +#define ICON_FA_FLUSHED "\xef\x95\xb9" // U+f579 +#define ICON_FA_FOLDER "\xef\x81\xbb" // U+f07b +#define ICON_FA_FOLDER_MINUS "\xef\x99\x9d" // U+f65d +#define ICON_FA_FOLDER_OPEN "\xef\x81\xbc" // U+f07c +#define ICON_FA_FOLDER_PLUS "\xef\x99\x9e" // U+f65e +#define ICON_FA_FONT "\xef\x80\xb1" // U+f031 +#define ICON_FA_FONT_AWESOME_LOGO_FULL "\xef\x93\xa6" // U+f4e6 +#define ICON_FA_FOOTBALL_BALL "\xef\x91\x8e" // U+f44e +#define ICON_FA_FORWARD "\xef\x81\x8e" // U+f04e +#define ICON_FA_FROG "\xef\x94\xae" // U+f52e +#define ICON_FA_FROWN "\xef\x84\x99" // U+f119 +#define ICON_FA_FROWN_OPEN "\xef\x95\xba" // U+f57a +#define ICON_FA_FUNNEL_DOLLAR "\xef\x99\xa2" // U+f662 +#define ICON_FA_FUTBOL "\xef\x87\xa3" // U+f1e3 +#define ICON_FA_GAMEPAD "\xef\x84\x9b" // U+f11b +#define ICON_FA_GAS_PUMP "\xef\x94\xaf" // U+f52f +#define ICON_FA_GAVEL "\xef\x83\xa3" // U+f0e3 +#define ICON_FA_GEM "\xef\x8e\xa5" // U+f3a5 +#define ICON_FA_GENDERLESS "\xef\x88\xad" // U+f22d +#define ICON_FA_GHOST "\xef\x9b\xa2" // U+f6e2 +#define ICON_FA_GIFT "\xef\x81\xab" // U+f06b +#define ICON_FA_GIFTS "\xef\x9e\x9c" // U+f79c +#define ICON_FA_GLASS_CHEERS "\xef\x9e\x9f" // U+f79f +#define ICON_FA_GLASS_MARTINI "\xef\x80\x80" // U+f000 +#define ICON_FA_GLASS_MARTINI_ALT "\xef\x95\xbb" // U+f57b +#define ICON_FA_GLASS_WHISKEY "\xef\x9e\xa0" // U+f7a0 +#define ICON_FA_GLASSES "\xef\x94\xb0" // U+f530 +#define ICON_FA_GLOBE "\xef\x82\xac" // U+f0ac +#define ICON_FA_GLOBE_AFRICA "\xef\x95\xbc" // U+f57c +#define ICON_FA_GLOBE_AMERICAS "\xef\x95\xbd" // U+f57d +#define ICON_FA_GLOBE_ASIA "\xef\x95\xbe" // U+f57e +#define ICON_FA_GLOBE_EUROPE "\xef\x9e\xa2" // U+f7a2 +#define ICON_FA_GOLF_BALL "\xef\x91\x90" // U+f450 +#define ICON_FA_GOPURAM "\xef\x99\xa4" // U+f664 +#define ICON_FA_GRADUATION_CAP "\xef\x86\x9d" // U+f19d +#define ICON_FA_GREATER_THAN "\xef\x94\xb1" // U+f531 +#define ICON_FA_GREATER_THAN_EQUAL "\xef\x94\xb2" // U+f532 +#define ICON_FA_GRIMACE "\xef\x95\xbf" // U+f57f +#define ICON_FA_GRIN "\xef\x96\x80" // U+f580 +#define ICON_FA_GRIN_ALT "\xef\x96\x81" // U+f581 +#define ICON_FA_GRIN_BEAM "\xef\x96\x82" // U+f582 +#define ICON_FA_GRIN_BEAM_SWEAT "\xef\x96\x83" // U+f583 +#define ICON_FA_GRIN_HEARTS "\xef\x96\x84" // U+f584 +#define ICON_FA_GRIN_SQUINT "\xef\x96\x85" // U+f585 +#define ICON_FA_GRIN_SQUINT_TEARS "\xef\x96\x86" // U+f586 +#define ICON_FA_GRIN_STARS "\xef\x96\x87" // U+f587 +#define ICON_FA_GRIN_TEARS "\xef\x96\x88" // U+f588 +#define ICON_FA_GRIN_TONGUE "\xef\x96\x89" // U+f589 +#define ICON_FA_GRIN_TONGUE_SQUINT "\xef\x96\x8a" // U+f58a +#define ICON_FA_GRIN_TONGUE_WINK "\xef\x96\x8b" // U+f58b +#define ICON_FA_GRIN_WINK "\xef\x96\x8c" // U+f58c +#define ICON_FA_GRIP_HORIZONTAL "\xef\x96\x8d" // U+f58d +#define ICON_FA_GRIP_LINES "\xef\x9e\xa4" // U+f7a4 +#define ICON_FA_GRIP_LINES_VERTICAL "\xef\x9e\xa5" // U+f7a5 +#define ICON_FA_GRIP_VERTICAL "\xef\x96\x8e" // U+f58e +#define ICON_FA_GUITAR "\xef\x9e\xa6" // U+f7a6 +#define ICON_FA_H_SQUARE "\xef\x83\xbd" // U+f0fd +#define ICON_FA_HAMBURGER "\xef\xa0\x85" // U+f805 +#define ICON_FA_HAMMER "\xef\x9b\xa3" // U+f6e3 +#define ICON_FA_HAMSA "\xef\x99\xa5" // U+f665 +#define ICON_FA_HAND_HOLDING "\xef\x92\xbd" // U+f4bd +#define ICON_FA_HAND_HOLDING_HEART "\xef\x92\xbe" // U+f4be +#define ICON_FA_HAND_HOLDING_MEDICAL "\xee\x81\x9c" // U+e05c +#define ICON_FA_HAND_HOLDING_USD "\xef\x93\x80" // U+f4c0 +#define ICON_FA_HAND_HOLDING_WATER "\xef\x93\x81" // U+f4c1 +#define ICON_FA_HAND_LIZARD "\xef\x89\x98" // U+f258 +#define ICON_FA_HAND_MIDDLE_FINGER "\xef\xa0\x86" // U+f806 +#define ICON_FA_HAND_PAPER "\xef\x89\x96" // U+f256 +#define ICON_FA_HAND_PEACE "\xef\x89\x9b" // U+f25b +#define ICON_FA_HAND_POINT_DOWN "\xef\x82\xa7" // U+f0a7 +#define ICON_FA_HAND_POINT_LEFT "\xef\x82\xa5" // U+f0a5 +#define ICON_FA_HAND_POINT_RIGHT "\xef\x82\xa4" // U+f0a4 +#define ICON_FA_HAND_POINT_UP "\xef\x82\xa6" // U+f0a6 +#define ICON_FA_HAND_POINTER "\xef\x89\x9a" // U+f25a +#define ICON_FA_HAND_ROCK "\xef\x89\x95" // U+f255 +#define ICON_FA_HAND_SCISSORS "\xef\x89\x97" // U+f257 +#define ICON_FA_HAND_SPARKLES "\xee\x81\x9d" // U+e05d +#define ICON_FA_HAND_SPOCK "\xef\x89\x99" // U+f259 +#define ICON_FA_HANDS "\xef\x93\x82" // U+f4c2 +#define ICON_FA_HANDS_HELPING "\xef\x93\x84" // U+f4c4 +#define ICON_FA_HANDS_WASH "\xee\x81\x9e" // U+e05e +#define ICON_FA_HANDSHAKE "\xef\x8a\xb5" // U+f2b5 +#define ICON_FA_HANDSHAKE_ALT_SLASH "\xee\x81\x9f" // U+e05f +#define ICON_FA_HANDSHAKE_SLASH "\xee\x81\xa0" // U+e060 +#define ICON_FA_HANUKIAH "\xef\x9b\xa6" // U+f6e6 +#define ICON_FA_HARD_HAT "\xef\xa0\x87" // U+f807 +#define ICON_FA_HASHTAG "\xef\x8a\x92" // U+f292 +#define ICON_FA_HAT_COWBOY "\xef\xa3\x80" // U+f8c0 +#define ICON_FA_HAT_COWBOY_SIDE "\xef\xa3\x81" // U+f8c1 +#define ICON_FA_HAT_WIZARD "\xef\x9b\xa8" // U+f6e8 +#define ICON_FA_HDD "\xef\x82\xa0" // U+f0a0 +#define ICON_FA_HEAD_SIDE_COUGH "\xee\x81\xa1" // U+e061 +#define ICON_FA_HEAD_SIDE_COUGH_SLASH "\xee\x81\xa2" // U+e062 +#define ICON_FA_HEAD_SIDE_MASK "\xee\x81\xa3" // U+e063 +#define ICON_FA_HEAD_SIDE_VIRUS "\xee\x81\xa4" // U+e064 +#define ICON_FA_HEADING "\xef\x87\x9c" // U+f1dc +#define ICON_FA_HEADPHONES "\xef\x80\xa5" // U+f025 +#define ICON_FA_HEADPHONES_ALT "\xef\x96\x8f" // U+f58f +#define ICON_FA_HEADSET "\xef\x96\x90" // U+f590 +#define ICON_FA_HEART "\xef\x80\x84" // U+f004 +#define ICON_FA_HEART_BROKEN "\xef\x9e\xa9" // U+f7a9 +#define ICON_FA_HEARTBEAT "\xef\x88\x9e" // U+f21e +#define ICON_FA_HELICOPTER "\xef\x94\xb3" // U+f533 +#define ICON_FA_HIGHLIGHTER "\xef\x96\x91" // U+f591 +#define ICON_FA_HIKING "\xef\x9b\xac" // U+f6ec +#define ICON_FA_HIPPO "\xef\x9b\xad" // U+f6ed +#define ICON_FA_HISTORY "\xef\x87\x9a" // U+f1da +#define ICON_FA_HOCKEY_PUCK "\xef\x91\x93" // U+f453 +#define ICON_FA_HOLLY_BERRY "\xef\x9e\xaa" // U+f7aa +#define ICON_FA_HOME "\xef\x80\x95" // U+f015 +#define ICON_FA_HORSE "\xef\x9b\xb0" // U+f6f0 +#define ICON_FA_HORSE_HEAD "\xef\x9e\xab" // U+f7ab +#define ICON_FA_HOSPITAL "\xef\x83\xb8" // U+f0f8 +#define ICON_FA_HOSPITAL_ALT "\xef\x91\xbd" // U+f47d +#define ICON_FA_HOSPITAL_SYMBOL "\xef\x91\xbe" // U+f47e +#define ICON_FA_HOSPITAL_USER "\xef\xa0\x8d" // U+f80d +#define ICON_FA_HOT_TUB "\xef\x96\x93" // U+f593 +#define ICON_FA_HOTDOG "\xef\xa0\x8f" // U+f80f +#define ICON_FA_HOTEL "\xef\x96\x94" // U+f594 +#define ICON_FA_HOURGLASS "\xef\x89\x94" // U+f254 +#define ICON_FA_HOURGLASS_END "\xef\x89\x93" // U+f253 +#define ICON_FA_HOURGLASS_HALF "\xef\x89\x92" // U+f252 +#define ICON_FA_HOURGLASS_START "\xef\x89\x91" // U+f251 +#define ICON_FA_HOUSE_DAMAGE "\xef\x9b\xb1" // U+f6f1 +#define ICON_FA_HOUSE_USER "\xee\x81\xa5" // U+e065 +#define ICON_FA_HRYVNIA "\xef\x9b\xb2" // U+f6f2 +#define ICON_FA_I_CURSOR "\xef\x89\x86" // U+f246 +#define ICON_FA_ICE_CREAM "\xef\xa0\x90" // U+f810 +#define ICON_FA_ICICLES "\xef\x9e\xad" // U+f7ad +#define ICON_FA_ICONS "\xef\xa1\xad" // U+f86d +#define ICON_FA_ID_BADGE "\xef\x8b\x81" // U+f2c1 +#define ICON_FA_ID_CARD "\xef\x8b\x82" // U+f2c2 +#define ICON_FA_ID_CARD_ALT "\xef\x91\xbf" // U+f47f +#define ICON_FA_IGLOO "\xef\x9e\xae" // U+f7ae +#define ICON_FA_IMAGE "\xef\x80\xbe" // U+f03e +#define ICON_FA_IMAGES "\xef\x8c\x82" // U+f302 +#define ICON_FA_INBOX "\xef\x80\x9c" // U+f01c +#define ICON_FA_INDENT "\xef\x80\xbc" // U+f03c +#define ICON_FA_INDUSTRY "\xef\x89\xb5" // U+f275 +#define ICON_FA_INFINITY "\xef\x94\xb4" // U+f534 +#define ICON_FA_INFO "\xef\x84\xa9" // U+f129 +#define ICON_FA_INFO_CIRCLE "\xef\x81\x9a" // U+f05a +#define ICON_FA_ITALIC "\xef\x80\xb3" // U+f033 +#define ICON_FA_JEDI "\xef\x99\xa9" // U+f669 +#define ICON_FA_JOINT "\xef\x96\x95" // U+f595 +#define ICON_FA_JOURNAL_WHILLS "\xef\x99\xaa" // U+f66a +#define ICON_FA_KAABA "\xef\x99\xab" // U+f66b +#define ICON_FA_KEY "\xef\x82\x84" // U+f084 +#define ICON_FA_KEYBOARD "\xef\x84\x9c" // U+f11c +#define ICON_FA_KHANDA "\xef\x99\xad" // U+f66d +#define ICON_FA_KISS "\xef\x96\x96" // U+f596 +#define ICON_FA_KISS_BEAM "\xef\x96\x97" // U+f597 +#define ICON_FA_KISS_WINK_HEART "\xef\x96\x98" // U+f598 +#define ICON_FA_KIWI_BIRD "\xef\x94\xb5" // U+f535 +#define ICON_FA_LANDMARK "\xef\x99\xaf" // U+f66f +#define ICON_FA_LANGUAGE "\xef\x86\xab" // U+f1ab +#define ICON_FA_LAPTOP "\xef\x84\x89" // U+f109 +#define ICON_FA_LAPTOP_CODE "\xef\x97\xbc" // U+f5fc +#define ICON_FA_LAPTOP_HOUSE "\xee\x81\xa6" // U+e066 +#define ICON_FA_LAPTOP_MEDICAL "\xef\xa0\x92" // U+f812 +#define ICON_FA_LAUGH "\xef\x96\x99" // U+f599 +#define ICON_FA_LAUGH_BEAM "\xef\x96\x9a" // U+f59a +#define ICON_FA_LAUGH_SQUINT "\xef\x96\x9b" // U+f59b +#define ICON_FA_LAUGH_WINK "\xef\x96\x9c" // U+f59c +#define ICON_FA_LAYER_GROUP "\xef\x97\xbd" // U+f5fd +#define ICON_FA_LEAF "\xef\x81\xac" // U+f06c +#define ICON_FA_LEMON "\xef\x82\x94" // U+f094 +#define ICON_FA_LESS_THAN "\xef\x94\xb6" // U+f536 +#define ICON_FA_LESS_THAN_EQUAL "\xef\x94\xb7" // U+f537 +#define ICON_FA_LEVEL_DOWN_ALT "\xef\x8e\xbe" // U+f3be +#define ICON_FA_LEVEL_UP_ALT "\xef\x8e\xbf" // U+f3bf +#define ICON_FA_LIFE_RING "\xef\x87\x8d" // U+f1cd +#define ICON_FA_LIGHTBULB "\xef\x83\xab" // U+f0eb +#define ICON_FA_LINK "\xef\x83\x81" // U+f0c1 +#define ICON_FA_LIRA_SIGN "\xef\x86\x95" // U+f195 +#define ICON_FA_LIST "\xef\x80\xba" // U+f03a +#define ICON_FA_LIST_ALT "\xef\x80\xa2" // U+f022 +#define ICON_FA_LIST_OL "\xef\x83\x8b" // U+f0cb +#define ICON_FA_LIST_UL "\xef\x83\x8a" // U+f0ca +#define ICON_FA_LOCATION_ARROW "\xef\x84\xa4" // U+f124 +#define ICON_FA_LOCK "\xef\x80\xa3" // U+f023 +#define ICON_FA_LOCK_OPEN "\xef\x8f\x81" // U+f3c1 +#define ICON_FA_LONG_ARROW_ALT_DOWN "\xef\x8c\x89" // U+f309 +#define ICON_FA_LONG_ARROW_ALT_LEFT "\xef\x8c\x8a" // U+f30a +#define ICON_FA_LONG_ARROW_ALT_RIGHT "\xef\x8c\x8b" // U+f30b +#define ICON_FA_LONG_ARROW_ALT_UP "\xef\x8c\x8c" // U+f30c +#define ICON_FA_LOW_VISION "\xef\x8a\xa8" // U+f2a8 +#define ICON_FA_LUGGAGE_CART "\xef\x96\x9d" // U+f59d +#define ICON_FA_LUNGS "\xef\x98\x84" // U+f604 +#define ICON_FA_LUNGS_VIRUS "\xee\x81\xa7" // U+e067 +#define ICON_FA_MAGIC "\xef\x83\x90" // U+f0d0 +#define ICON_FA_MAGNET "\xef\x81\xb6" // U+f076 +#define ICON_FA_MAIL_BULK "\xef\x99\xb4" // U+f674 +#define ICON_FA_MALE "\xef\x86\x83" // U+f183 +#define ICON_FA_MAP "\xef\x89\xb9" // U+f279 +#define ICON_FA_MAP_MARKED "\xef\x96\x9f" // U+f59f +#define ICON_FA_MAP_MARKED_ALT "\xef\x96\xa0" // U+f5a0 +#define ICON_FA_MAP_MARKER "\xef\x81\x81" // U+f041 +#define ICON_FA_MAP_MARKER_ALT "\xef\x8f\x85" // U+f3c5 +#define ICON_FA_MAP_PIN "\xef\x89\xb6" // U+f276 +#define ICON_FA_MAP_SIGNS "\xef\x89\xb7" // U+f277 +#define ICON_FA_MARKER "\xef\x96\xa1" // U+f5a1 +#define ICON_FA_MARS "\xef\x88\xa2" // U+f222 +#define ICON_FA_MARS_DOUBLE "\xef\x88\xa7" // U+f227 +#define ICON_FA_MARS_STROKE "\xef\x88\xa9" // U+f229 +#define ICON_FA_MARS_STROKE_H "\xef\x88\xab" // U+f22b +#define ICON_FA_MARS_STROKE_V "\xef\x88\xaa" // U+f22a +#define ICON_FA_MASK "\xef\x9b\xba" // U+f6fa +#define ICON_FA_MEDAL "\xef\x96\xa2" // U+f5a2 +#define ICON_FA_MEDKIT "\xef\x83\xba" // U+f0fa +#define ICON_FA_MEH "\xef\x84\x9a" // U+f11a +#define ICON_FA_MEH_BLANK "\xef\x96\xa4" // U+f5a4 +#define ICON_FA_MEH_ROLLING_EYES "\xef\x96\xa5" // U+f5a5 +#define ICON_FA_MEMORY "\xef\x94\xb8" // U+f538 +#define ICON_FA_MENORAH "\xef\x99\xb6" // U+f676 +#define ICON_FA_MERCURY "\xef\x88\xa3" // U+f223 +#define ICON_FA_METEOR "\xef\x9d\x93" // U+f753 +#define ICON_FA_MICROCHIP "\xef\x8b\x9b" // U+f2db +#define ICON_FA_MICROPHONE "\xef\x84\xb0" // U+f130 +#define ICON_FA_MICROPHONE_ALT "\xef\x8f\x89" // U+f3c9 +#define ICON_FA_MICROPHONE_ALT_SLASH "\xef\x94\xb9" // U+f539 +#define ICON_FA_MICROPHONE_SLASH "\xef\x84\xb1" // U+f131 +#define ICON_FA_MICROSCOPE "\xef\x98\x90" // U+f610 +#define ICON_FA_MINUS "\xef\x81\xa8" // U+f068 +#define ICON_FA_MINUS_CIRCLE "\xef\x81\x96" // U+f056 +#define ICON_FA_MINUS_SQUARE "\xef\x85\x86" // U+f146 +#define ICON_FA_MITTEN "\xef\x9e\xb5" // U+f7b5 +#define ICON_FA_MOBILE "\xef\x84\x8b" // U+f10b +#define ICON_FA_MOBILE_ALT "\xef\x8f\x8d" // U+f3cd +#define ICON_FA_MONEY_BILL "\xef\x83\x96" // U+f0d6 +#define ICON_FA_MONEY_BILL_ALT "\xef\x8f\x91" // U+f3d1 +#define ICON_FA_MONEY_BILL_WAVE "\xef\x94\xba" // U+f53a +#define ICON_FA_MONEY_BILL_WAVE_ALT "\xef\x94\xbb" // U+f53b +#define ICON_FA_MONEY_CHECK "\xef\x94\xbc" // U+f53c +#define ICON_FA_MONEY_CHECK_ALT "\xef\x94\xbd" // U+f53d +#define ICON_FA_MONUMENT "\xef\x96\xa6" // U+f5a6 +#define ICON_FA_MOON "\xef\x86\x86" // U+f186 +#define ICON_FA_MORTAR_PESTLE "\xef\x96\xa7" // U+f5a7 +#define ICON_FA_MOSQUE "\xef\x99\xb8" // U+f678 +#define ICON_FA_MOTORCYCLE "\xef\x88\x9c" // U+f21c +#define ICON_FA_MOUNTAIN "\xef\x9b\xbc" // U+f6fc +#define ICON_FA_MOUSE "\xef\xa3\x8c" // U+f8cc +#define ICON_FA_MOUSE_POINTER "\xef\x89\x85" // U+f245 +#define ICON_FA_MUG_HOT "\xef\x9e\xb6" // U+f7b6 +#define ICON_FA_MUSIC "\xef\x80\x81" // U+f001 +#define ICON_FA_NETWORK_WIRED "\xef\x9b\xbf" // U+f6ff +#define ICON_FA_NEUTER "\xef\x88\xac" // U+f22c +#define ICON_FA_NEWSPAPER "\xef\x87\xaa" // U+f1ea +#define ICON_FA_NOT_EQUAL "\xef\x94\xbe" // U+f53e +#define ICON_FA_NOTES_MEDICAL "\xef\x92\x81" // U+f481 +#define ICON_FA_OBJECT_GROUP "\xef\x89\x87" // U+f247 +#define ICON_FA_OBJECT_UNGROUP "\xef\x89\x88" // U+f248 +#define ICON_FA_OIL_CAN "\xef\x98\x93" // U+f613 +#define ICON_FA_OM "\xef\x99\xb9" // U+f679 +#define ICON_FA_OTTER "\xef\x9c\x80" // U+f700 +#define ICON_FA_OUTDENT "\xef\x80\xbb" // U+f03b +#define ICON_FA_PAGER "\xef\xa0\x95" // U+f815 +#define ICON_FA_PAINT_BRUSH "\xef\x87\xbc" // U+f1fc +#define ICON_FA_PAINT_ROLLER "\xef\x96\xaa" // U+f5aa +#define ICON_FA_PALETTE "\xef\x94\xbf" // U+f53f +#define ICON_FA_PALLET "\xef\x92\x82" // U+f482 +#define ICON_FA_PAPER_PLANE "\xef\x87\x98" // U+f1d8 +#define ICON_FA_PAPERCLIP "\xef\x83\x86" // U+f0c6 +#define ICON_FA_PARACHUTE_BOX "\xef\x93\x8d" // U+f4cd +#define ICON_FA_PARAGRAPH "\xef\x87\x9d" // U+f1dd +#define ICON_FA_PARKING "\xef\x95\x80" // U+f540 +#define ICON_FA_PASSPORT "\xef\x96\xab" // U+f5ab +#define ICON_FA_PASTAFARIANISM "\xef\x99\xbb" // U+f67b +#define ICON_FA_PASTE "\xef\x83\xaa" // U+f0ea +#define ICON_FA_PAUSE "\xef\x81\x8c" // U+f04c +#define ICON_FA_PAUSE_CIRCLE "\xef\x8a\x8b" // U+f28b +#define ICON_FA_PAW "\xef\x86\xb0" // U+f1b0 +#define ICON_FA_PEACE "\xef\x99\xbc" // U+f67c +#define ICON_FA_PEN "\xef\x8c\x84" // U+f304 +#define ICON_FA_PEN_ALT "\xef\x8c\x85" // U+f305 +#define ICON_FA_PEN_FANCY "\xef\x96\xac" // U+f5ac +#define ICON_FA_PEN_NIB "\xef\x96\xad" // U+f5ad +#define ICON_FA_PEN_SQUARE "\xef\x85\x8b" // U+f14b +#define ICON_FA_PENCIL_ALT "\xef\x8c\x83" // U+f303 +#define ICON_FA_PENCIL_RULER "\xef\x96\xae" // U+f5ae +#define ICON_FA_PEOPLE_ARROWS "\xee\x81\xa8" // U+e068 +#define ICON_FA_PEOPLE_CARRY "\xef\x93\x8e" // U+f4ce +#define ICON_FA_PEPPER_HOT "\xef\xa0\x96" // U+f816 +#define ICON_FA_PERCENT "\xef\x8a\x95" // U+f295 +#define ICON_FA_PERCENTAGE "\xef\x95\x81" // U+f541 +#define ICON_FA_PERSON_BOOTH "\xef\x9d\x96" // U+f756 +#define ICON_FA_PHONE "\xef\x82\x95" // U+f095 +#define ICON_FA_PHONE_ALT "\xef\xa1\xb9" // U+f879 +#define ICON_FA_PHONE_SLASH "\xef\x8f\x9d" // U+f3dd +#define ICON_FA_PHONE_SQUARE "\xef\x82\x98" // U+f098 +#define ICON_FA_PHONE_SQUARE_ALT "\xef\xa1\xbb" // U+f87b +#define ICON_FA_PHONE_VOLUME "\xef\x8a\xa0" // U+f2a0 +#define ICON_FA_PHOTO_VIDEO "\xef\xa1\xbc" // U+f87c +#define ICON_FA_PIGGY_BANK "\xef\x93\x93" // U+f4d3 +#define ICON_FA_PILLS "\xef\x92\x84" // U+f484 +#define ICON_FA_PIZZA_SLICE "\xef\xa0\x98" // U+f818 +#define ICON_FA_PLACE_OF_WORSHIP "\xef\x99\xbf" // U+f67f +#define ICON_FA_PLANE "\xef\x81\xb2" // U+f072 +#define ICON_FA_PLANE_ARRIVAL "\xef\x96\xaf" // U+f5af +#define ICON_FA_PLANE_DEPARTURE "\xef\x96\xb0" // U+f5b0 +#define ICON_FA_PLANE_SLASH "\xee\x81\xa9" // U+e069 +#define ICON_FA_PLAY "\xef\x81\x8b" // U+f04b +#define ICON_FA_PLAY_CIRCLE "\xef\x85\x84" // U+f144 +#define ICON_FA_PLUG "\xef\x87\xa6" // U+f1e6 +#define ICON_FA_PLUS "\xef\x81\xa7" // U+f067 +#define ICON_FA_PLUS_CIRCLE "\xef\x81\x95" // U+f055 +#define ICON_FA_PLUS_SQUARE "\xef\x83\xbe" // U+f0fe +#define ICON_FA_PODCAST "\xef\x8b\x8e" // U+f2ce +#define ICON_FA_POLL "\xef\x9a\x81" // U+f681 +#define ICON_FA_POLL_H "\xef\x9a\x82" // U+f682 +#define ICON_FA_POO "\xef\x8b\xbe" // U+f2fe +#define ICON_FA_POO_STORM "\xef\x9d\x9a" // U+f75a +#define ICON_FA_POOP "\xef\x98\x99" // U+f619 +#define ICON_FA_PORTRAIT "\xef\x8f\xa0" // U+f3e0 +#define ICON_FA_POUND_SIGN "\xef\x85\x94" // U+f154 +#define ICON_FA_POWER_OFF "\xef\x80\x91" // U+f011 +#define ICON_FA_PRAY "\xef\x9a\x83" // U+f683 +#define ICON_FA_PRAYING_HANDS "\xef\x9a\x84" // U+f684 +#define ICON_FA_PRESCRIPTION "\xef\x96\xb1" // U+f5b1 +#define ICON_FA_PRESCRIPTION_BOTTLE "\xef\x92\x85" // U+f485 +#define ICON_FA_PRESCRIPTION_BOTTLE_ALT "\xef\x92\x86" // U+f486 +#define ICON_FA_PRINT "\xef\x80\xaf" // U+f02f +#define ICON_FA_PROCEDURES "\xef\x92\x87" // U+f487 +#define ICON_FA_PROJECT_DIAGRAM "\xef\x95\x82" // U+f542 +#define ICON_FA_PUMP_MEDICAL "\xee\x81\xaa" // U+e06a +#define ICON_FA_PUMP_SOAP "\xee\x81\xab" // U+e06b +#define ICON_FA_PUZZLE_PIECE "\xef\x84\xae" // U+f12e +#define ICON_FA_QRCODE "\xef\x80\xa9" // U+f029 +#define ICON_FA_QUESTION "\xef\x84\xa8" // U+f128 +#define ICON_FA_QUESTION_CIRCLE "\xef\x81\x99" // U+f059 +#define ICON_FA_QUIDDITCH "\xef\x91\x98" // U+f458 +#define ICON_FA_QUOTE_LEFT "\xef\x84\x8d" // U+f10d +#define ICON_FA_QUOTE_RIGHT "\xef\x84\x8e" // U+f10e +#define ICON_FA_QURAN "\xef\x9a\x87" // U+f687 +#define ICON_FA_RADIATION "\xef\x9e\xb9" // U+f7b9 +#define ICON_FA_RADIATION_ALT "\xef\x9e\xba" // U+f7ba +#define ICON_FA_RAINBOW "\xef\x9d\x9b" // U+f75b +#define ICON_FA_RANDOM "\xef\x81\xb4" // U+f074 +#define ICON_FA_RECEIPT "\xef\x95\x83" // U+f543 +#define ICON_FA_RECORD_VINYL "\xef\xa3\x99" // U+f8d9 +#define ICON_FA_RECYCLE "\xef\x86\xb8" // U+f1b8 +#define ICON_FA_REDO "\xef\x80\x9e" // U+f01e +#define ICON_FA_REDO_ALT "\xef\x8b\xb9" // U+f2f9 +#define ICON_FA_REGISTERED "\xef\x89\x9d" // U+f25d +#define ICON_FA_REMOVE_FORMAT "\xef\xa1\xbd" // U+f87d +#define ICON_FA_REPLY "\xef\x8f\xa5" // U+f3e5 +#define ICON_FA_REPLY_ALL "\xef\x84\xa2" // U+f122 +#define ICON_FA_REPUBLICAN "\xef\x9d\x9e" // U+f75e +#define ICON_FA_RESTROOM "\xef\x9e\xbd" // U+f7bd +#define ICON_FA_RETWEET "\xef\x81\xb9" // U+f079 +#define ICON_FA_RIBBON "\xef\x93\x96" // U+f4d6 +#define ICON_FA_RING "\xef\x9c\x8b" // U+f70b +#define ICON_FA_ROAD "\xef\x80\x98" // U+f018 +#define ICON_FA_ROBOT "\xef\x95\x84" // U+f544 +#define ICON_FA_ROCKET "\xef\x84\xb5" // U+f135 +#define ICON_FA_ROUTE "\xef\x93\x97" // U+f4d7 +#define ICON_FA_RSS "\xef\x82\x9e" // U+f09e +#define ICON_FA_RSS_SQUARE "\xef\x85\x83" // U+f143 +#define ICON_FA_RUBLE_SIGN "\xef\x85\x98" // U+f158 +#define ICON_FA_RULER "\xef\x95\x85" // U+f545 +#define ICON_FA_RULER_COMBINED "\xef\x95\x86" // U+f546 +#define ICON_FA_RULER_HORIZONTAL "\xef\x95\x87" // U+f547 +#define ICON_FA_RULER_VERTICAL "\xef\x95\x88" // U+f548 +#define ICON_FA_RUNNING "\xef\x9c\x8c" // U+f70c +#define ICON_FA_RUPEE_SIGN "\xef\x85\x96" // U+f156 +#define ICON_FA_SAD_CRY "\xef\x96\xb3" // U+f5b3 +#define ICON_FA_SAD_TEAR "\xef\x96\xb4" // U+f5b4 +#define ICON_FA_SATELLITE "\xef\x9e\xbf" // U+f7bf +#define ICON_FA_SATELLITE_DISH "\xef\x9f\x80" // U+f7c0 +#define ICON_FA_SAVE "\xef\x83\x87" // U+f0c7 +#define ICON_FA_SCHOOL "\xef\x95\x89" // U+f549 +#define ICON_FA_SCREWDRIVER "\xef\x95\x8a" // U+f54a +#define ICON_FA_SCROLL "\xef\x9c\x8e" // U+f70e +#define ICON_FA_SD_CARD "\xef\x9f\x82" // U+f7c2 +#define ICON_FA_SEARCH "\xef\x80\x82" // U+f002 +#define ICON_FA_SEARCH_DOLLAR "\xef\x9a\x88" // U+f688 +#define ICON_FA_SEARCH_LOCATION "\xef\x9a\x89" // U+f689 +#define ICON_FA_SEARCH_MINUS "\xef\x80\x90" // U+f010 +#define ICON_FA_SEARCH_PLUS "\xef\x80\x8e" // U+f00e +#define ICON_FA_SEEDLING "\xef\x93\x98" // U+f4d8 +#define ICON_FA_SERVER "\xef\x88\xb3" // U+f233 +#define ICON_FA_SHAPES "\xef\x98\x9f" // U+f61f +#define ICON_FA_SHARE "\xef\x81\xa4" // U+f064 +#define ICON_FA_SHARE_ALT "\xef\x87\xa0" // U+f1e0 +#define ICON_FA_SHARE_ALT_SQUARE "\xef\x87\xa1" // U+f1e1 +#define ICON_FA_SHARE_SQUARE "\xef\x85\x8d" // U+f14d +#define ICON_FA_SHEKEL_SIGN "\xef\x88\x8b" // U+f20b +#define ICON_FA_SHIELD_ALT "\xef\x8f\xad" // U+f3ed +#define ICON_FA_SHIELD_VIRUS "\xee\x81\xac" // U+e06c +#define ICON_FA_SHIP "\xef\x88\x9a" // U+f21a +#define ICON_FA_SHIPPING_FAST "\xef\x92\x8b" // U+f48b +#define ICON_FA_SHOE_PRINTS "\xef\x95\x8b" // U+f54b +#define ICON_FA_SHOPPING_BAG "\xef\x8a\x90" // U+f290 +#define ICON_FA_SHOPPING_BASKET "\xef\x8a\x91" // U+f291 +#define ICON_FA_SHOPPING_CART "\xef\x81\xba" // U+f07a +#define ICON_FA_SHOWER "\xef\x8b\x8c" // U+f2cc +#define ICON_FA_SHUTTLE_VAN "\xef\x96\xb6" // U+f5b6 +#define ICON_FA_SIGN "\xef\x93\x99" // U+f4d9 +#define ICON_FA_SIGN_IN_ALT "\xef\x8b\xb6" // U+f2f6 +#define ICON_FA_SIGN_LANGUAGE "\xef\x8a\xa7" // U+f2a7 +#define ICON_FA_SIGN_OUT_ALT "\xef\x8b\xb5" // U+f2f5 +#define ICON_FA_SIGNAL "\xef\x80\x92" // U+f012 +#define ICON_FA_SIGNATURE "\xef\x96\xb7" // U+f5b7 +#define ICON_FA_SIM_CARD "\xef\x9f\x84" // U+f7c4 +#define ICON_FA_SINK "\xee\x81\xad" // U+e06d +#define ICON_FA_SITEMAP "\xef\x83\xa8" // U+f0e8 +#define ICON_FA_SKATING "\xef\x9f\x85" // U+f7c5 +#define ICON_FA_SKIING "\xef\x9f\x89" // U+f7c9 +#define ICON_FA_SKIING_NORDIC "\xef\x9f\x8a" // U+f7ca +#define ICON_FA_SKULL "\xef\x95\x8c" // U+f54c +#define ICON_FA_SKULL_CROSSBONES "\xef\x9c\x94" // U+f714 +#define ICON_FA_SLASH "\xef\x9c\x95" // U+f715 +#define ICON_FA_SLEIGH "\xef\x9f\x8c" // U+f7cc +#define ICON_FA_SLIDERS_H "\xef\x87\x9e" // U+f1de +#define ICON_FA_SMILE "\xef\x84\x98" // U+f118 +#define ICON_FA_SMILE_BEAM "\xef\x96\xb8" // U+f5b8 +#define ICON_FA_SMILE_WINK "\xef\x93\x9a" // U+f4da +#define ICON_FA_SMOG "\xef\x9d\x9f" // U+f75f +#define ICON_FA_SMOKING "\xef\x92\x8d" // U+f48d +#define ICON_FA_SMOKING_BAN "\xef\x95\x8d" // U+f54d +#define ICON_FA_SMS "\xef\x9f\x8d" // U+f7cd +#define ICON_FA_SNOWBOARDING "\xef\x9f\x8e" // U+f7ce +#define ICON_FA_SNOWFLAKE "\xef\x8b\x9c" // U+f2dc +#define ICON_FA_SNOWMAN "\xef\x9f\x90" // U+f7d0 +#define ICON_FA_SNOWPLOW "\xef\x9f\x92" // U+f7d2 +#define ICON_FA_SOAP "\xee\x81\xae" // U+e06e +#define ICON_FA_SOCKS "\xef\x9a\x96" // U+f696 +#define ICON_FA_SOLAR_PANEL "\xef\x96\xba" // U+f5ba +#define ICON_FA_SORT "\xef\x83\x9c" // U+f0dc +#define ICON_FA_SORT_ALPHA_DOWN "\xef\x85\x9d" // U+f15d +#define ICON_FA_SORT_ALPHA_DOWN_ALT "\xef\xa2\x81" // U+f881 +#define ICON_FA_SORT_ALPHA_UP "\xef\x85\x9e" // U+f15e +#define ICON_FA_SORT_ALPHA_UP_ALT "\xef\xa2\x82" // U+f882 +#define ICON_FA_SORT_AMOUNT_DOWN "\xef\x85\xa0" // U+f160 +#define ICON_FA_SORT_AMOUNT_DOWN_ALT "\xef\xa2\x84" // U+f884 +#define ICON_FA_SORT_AMOUNT_UP "\xef\x85\xa1" // U+f161 +#define ICON_FA_SORT_AMOUNT_UP_ALT "\xef\xa2\x85" // U+f885 +#define ICON_FA_SORT_DOWN "\xef\x83\x9d" // U+f0dd +#define ICON_FA_SORT_NUMERIC_DOWN "\xef\x85\xa2" // U+f162 +#define ICON_FA_SORT_NUMERIC_DOWN_ALT "\xef\xa2\x86" // U+f886 +#define ICON_FA_SORT_NUMERIC_UP "\xef\x85\xa3" // U+f163 +#define ICON_FA_SORT_NUMERIC_UP_ALT "\xef\xa2\x87" // U+f887 +#define ICON_FA_SORT_UP "\xef\x83\x9e" // U+f0de +#define ICON_FA_SPA "\xef\x96\xbb" // U+f5bb +#define ICON_FA_SPACE_SHUTTLE "\xef\x86\x97" // U+f197 +#define ICON_FA_SPELL_CHECK "\xef\xa2\x91" // U+f891 +#define ICON_FA_SPIDER "\xef\x9c\x97" // U+f717 +#define ICON_FA_SPINNER "\xef\x84\x90" // U+f110 +#define ICON_FA_SPLOTCH "\xef\x96\xbc" // U+f5bc +#define ICON_FA_SPRAY_CAN "\xef\x96\xbd" // U+f5bd +#define ICON_FA_SQUARE "\xef\x83\x88" // U+f0c8 +#define ICON_FA_SQUARE_FULL "\xef\x91\x9c" // U+f45c +#define ICON_FA_SQUARE_ROOT_ALT "\xef\x9a\x98" // U+f698 +#define ICON_FA_STAMP "\xef\x96\xbf" // U+f5bf +#define ICON_FA_STAR "\xef\x80\x85" // U+f005 +#define ICON_FA_STAR_AND_CRESCENT "\xef\x9a\x99" // U+f699 +#define ICON_FA_STAR_HALF "\xef\x82\x89" // U+f089 +#define ICON_FA_STAR_HALF_ALT "\xef\x97\x80" // U+f5c0 +#define ICON_FA_STAR_OF_DAVID "\xef\x9a\x9a" // U+f69a +#define ICON_FA_STAR_OF_LIFE "\xef\x98\xa1" // U+f621 +#define ICON_FA_STEP_BACKWARD "\xef\x81\x88" // U+f048 +#define ICON_FA_STEP_FORWARD "\xef\x81\x91" // U+f051 +#define ICON_FA_STETHOSCOPE "\xef\x83\xb1" // U+f0f1 +#define ICON_FA_STICKY_NOTE "\xef\x89\x89" // U+f249 +#define ICON_FA_STOP "\xef\x81\x8d" // U+f04d +#define ICON_FA_STOP_CIRCLE "\xef\x8a\x8d" // U+f28d +#define ICON_FA_STOPWATCH "\xef\x8b\xb2" // U+f2f2 +#define ICON_FA_STOPWATCH_20 "\xee\x81\xaf" // U+e06f +#define ICON_FA_STORE "\xef\x95\x8e" // U+f54e +#define ICON_FA_STORE_ALT "\xef\x95\x8f" // U+f54f +#define ICON_FA_STORE_ALT_SLASH "\xee\x81\xb0" // U+e070 +#define ICON_FA_STORE_SLASH "\xee\x81\xb1" // U+e071 +#define ICON_FA_STREAM "\xef\x95\x90" // U+f550 +#define ICON_FA_STREET_VIEW "\xef\x88\x9d" // U+f21d +#define ICON_FA_STRIKETHROUGH "\xef\x83\x8c" // U+f0cc +#define ICON_FA_STROOPWAFEL "\xef\x95\x91" // U+f551 +#define ICON_FA_SUBSCRIPT "\xef\x84\xac" // U+f12c +#define ICON_FA_SUBWAY "\xef\x88\xb9" // U+f239 +#define ICON_FA_SUITCASE "\xef\x83\xb2" // U+f0f2 +#define ICON_FA_SUITCASE_ROLLING "\xef\x97\x81" // U+f5c1 +#define ICON_FA_SUN "\xef\x86\x85" // U+f185 +#define ICON_FA_SUPERSCRIPT "\xef\x84\xab" // U+f12b +#define ICON_FA_SURPRISE "\xef\x97\x82" // U+f5c2 +#define ICON_FA_SWATCHBOOK "\xef\x97\x83" // U+f5c3 +#define ICON_FA_SWIMMER "\xef\x97\x84" // U+f5c4 +#define ICON_FA_SWIMMING_POOL "\xef\x97\x85" // U+f5c5 +#define ICON_FA_SYNAGOGUE "\xef\x9a\x9b" // U+f69b +#define ICON_FA_SYNC "\xef\x80\xa1" // U+f021 +#define ICON_FA_SYNC_ALT "\xef\x8b\xb1" // U+f2f1 +#define ICON_FA_SYRINGE "\xef\x92\x8e" // U+f48e +#define ICON_FA_TABLE "\xef\x83\x8e" // U+f0ce +#define ICON_FA_TABLE_TENNIS "\xef\x91\x9d" // U+f45d +#define ICON_FA_TABLET "\xef\x84\x8a" // U+f10a +#define ICON_FA_TABLET_ALT "\xef\x8f\xba" // U+f3fa +#define ICON_FA_TABLETS "\xef\x92\x90" // U+f490 +#define ICON_FA_TACHOMETER_ALT "\xef\x8f\xbd" // U+f3fd +#define ICON_FA_TAG "\xef\x80\xab" // U+f02b +#define ICON_FA_TAGS "\xef\x80\xac" // U+f02c +#define ICON_FA_TAPE "\xef\x93\x9b" // U+f4db +#define ICON_FA_TASKS "\xef\x82\xae" // U+f0ae +#define ICON_FA_TAXI "\xef\x86\xba" // U+f1ba +#define ICON_FA_TEETH "\xef\x98\xae" // U+f62e +#define ICON_FA_TEETH_OPEN "\xef\x98\xaf" // U+f62f +#define ICON_FA_TEMPERATURE_HIGH "\xef\x9d\xa9" // U+f769 +#define ICON_FA_TEMPERATURE_LOW "\xef\x9d\xab" // U+f76b +#define ICON_FA_TENGE "\xef\x9f\x97" // U+f7d7 +#define ICON_FA_TERMINAL "\xef\x84\xa0" // U+f120 +#define ICON_FA_TEXT_HEIGHT "\xef\x80\xb4" // U+f034 +#define ICON_FA_TEXT_WIDTH "\xef\x80\xb5" // U+f035 +#define ICON_FA_TH "\xef\x80\x8a" // U+f00a +#define ICON_FA_TH_LARGE "\xef\x80\x89" // U+f009 +#define ICON_FA_TH_LIST "\xef\x80\x8b" // U+f00b +#define ICON_FA_THEATER_MASKS "\xef\x98\xb0" // U+f630 +#define ICON_FA_THERMOMETER "\xef\x92\x91" // U+f491 +#define ICON_FA_THERMOMETER_EMPTY "\xef\x8b\x8b" // U+f2cb +#define ICON_FA_THERMOMETER_FULL "\xef\x8b\x87" // U+f2c7 +#define ICON_FA_THERMOMETER_HALF "\xef\x8b\x89" // U+f2c9 +#define ICON_FA_THERMOMETER_QUARTER "\xef\x8b\x8a" // U+f2ca +#define ICON_FA_THERMOMETER_THREE_QUARTERS "\xef\x8b\x88" // U+f2c8 +#define ICON_FA_THUMBS_DOWN "\xef\x85\xa5" // U+f165 +#define ICON_FA_THUMBS_UP "\xef\x85\xa4" // U+f164 +#define ICON_FA_THUMBTACK "\xef\x82\x8d" // U+f08d +#define ICON_FA_TICKET_ALT "\xef\x8f\xbf" // U+f3ff +#define ICON_FA_TIMES "\xef\x80\x8d" // U+f00d +#define ICON_FA_TIMES_CIRCLE "\xef\x81\x97" // U+f057 +#define ICON_FA_TINT "\xef\x81\x83" // U+f043 +#define ICON_FA_TINT_SLASH "\xef\x97\x87" // U+f5c7 +#define ICON_FA_TIRED "\xef\x97\x88" // U+f5c8 +#define ICON_FA_TOGGLE_OFF "\xef\x88\x84" // U+f204 +#define ICON_FA_TOGGLE_ON "\xef\x88\x85" // U+f205 +#define ICON_FA_TOILET "\xef\x9f\x98" // U+f7d8 +#define ICON_FA_TOILET_PAPER "\xef\x9c\x9e" // U+f71e +#define ICON_FA_TOILET_PAPER_SLASH "\xee\x81\xb2" // U+e072 +#define ICON_FA_TOOLBOX "\xef\x95\x92" // U+f552 +#define ICON_FA_TOOLS "\xef\x9f\x99" // U+f7d9 +#define ICON_FA_TOOTH "\xef\x97\x89" // U+f5c9 +#define ICON_FA_TORAH "\xef\x9a\xa0" // U+f6a0 +#define ICON_FA_TORII_GATE "\xef\x9a\xa1" // U+f6a1 +#define ICON_FA_TRACTOR "\xef\x9c\xa2" // U+f722 +#define ICON_FA_TRADEMARK "\xef\x89\x9c" // U+f25c +#define ICON_FA_TRAFFIC_LIGHT "\xef\x98\xb7" // U+f637 +#define ICON_FA_TRAILER "\xee\x81\x81" // U+e041 +#define ICON_FA_TRAIN "\xef\x88\xb8" // U+f238 +#define ICON_FA_TRAM "\xef\x9f\x9a" // U+f7da +#define ICON_FA_TRANSGENDER "\xef\x88\xa4" // U+f224 +#define ICON_FA_TRANSGENDER_ALT "\xef\x88\xa5" // U+f225 +#define ICON_FA_TRASH "\xef\x87\xb8" // U+f1f8 +#define ICON_FA_TRASH_ALT "\xef\x8b\xad" // U+f2ed +#define ICON_FA_TRASH_RESTORE "\xef\xa0\xa9" // U+f829 +#define ICON_FA_TRASH_RESTORE_ALT "\xef\xa0\xaa" // U+f82a +#define ICON_FA_TREE "\xef\x86\xbb" // U+f1bb +#define ICON_FA_TROPHY "\xef\x82\x91" // U+f091 +#define ICON_FA_TRUCK "\xef\x83\x91" // U+f0d1 +#define ICON_FA_TRUCK_LOADING "\xef\x93\x9e" // U+f4de +#define ICON_FA_TRUCK_MONSTER "\xef\x98\xbb" // U+f63b +#define ICON_FA_TRUCK_MOVING "\xef\x93\x9f" // U+f4df +#define ICON_FA_TRUCK_PICKUP "\xef\x98\xbc" // U+f63c +#define ICON_FA_TSHIRT "\xef\x95\x93" // U+f553 +#define ICON_FA_TTY "\xef\x87\xa4" // U+f1e4 +#define ICON_FA_TV "\xef\x89\xac" // U+f26c +#define ICON_FA_UMBRELLA "\xef\x83\xa9" // U+f0e9 +#define ICON_FA_UMBRELLA_BEACH "\xef\x97\x8a" // U+f5ca +#define ICON_FA_UNDERLINE "\xef\x83\x8d" // U+f0cd +#define ICON_FA_UNDO "\xef\x83\xa2" // U+f0e2 +#define ICON_FA_UNDO_ALT "\xef\x8b\xaa" // U+f2ea +#define ICON_FA_UNIVERSAL_ACCESS "\xef\x8a\x9a" // U+f29a +#define ICON_FA_UNIVERSITY "\xef\x86\x9c" // U+f19c +#define ICON_FA_UNLINK "\xef\x84\xa7" // U+f127 +#define ICON_FA_UNLOCK "\xef\x82\x9c" // U+f09c +#define ICON_FA_UNLOCK_ALT "\xef\x84\xbe" // U+f13e +#define ICON_FA_UPLOAD "\xef\x82\x93" // U+f093 +#define ICON_FA_USER "\xef\x80\x87" // U+f007 +#define ICON_FA_USER_ALT "\xef\x90\x86" // U+f406 +#define ICON_FA_USER_ALT_SLASH "\xef\x93\xba" // U+f4fa +#define ICON_FA_USER_ASTRONAUT "\xef\x93\xbb" // U+f4fb +#define ICON_FA_USER_CHECK "\xef\x93\xbc" // U+f4fc +#define ICON_FA_USER_CIRCLE "\xef\x8a\xbd" // U+f2bd +#define ICON_FA_USER_CLOCK "\xef\x93\xbd" // U+f4fd +#define ICON_FA_USER_COG "\xef\x93\xbe" // U+f4fe +#define ICON_FA_USER_EDIT "\xef\x93\xbf" // U+f4ff +#define ICON_FA_USER_FRIENDS "\xef\x94\x80" // U+f500 +#define ICON_FA_USER_GRADUATE "\xef\x94\x81" // U+f501 +#define ICON_FA_USER_INJURED "\xef\x9c\xa8" // U+f728 +#define ICON_FA_USER_LOCK "\xef\x94\x82" // U+f502 +#define ICON_FA_USER_MD "\xef\x83\xb0" // U+f0f0 +#define ICON_FA_USER_MINUS "\xef\x94\x83" // U+f503 +#define ICON_FA_USER_NINJA "\xef\x94\x84" // U+f504 +#define ICON_FA_USER_NURSE "\xef\xa0\xaf" // U+f82f +#define ICON_FA_USER_PLUS "\xef\x88\xb4" // U+f234 +#define ICON_FA_USER_SECRET "\xef\x88\x9b" // U+f21b +#define ICON_FA_USER_SHIELD "\xef\x94\x85" // U+f505 +#define ICON_FA_USER_SLASH "\xef\x94\x86" // U+f506 +#define ICON_FA_USER_TAG "\xef\x94\x87" // U+f507 +#define ICON_FA_USER_TIE "\xef\x94\x88" // U+f508 +#define ICON_FA_USER_TIMES "\xef\x88\xb5" // U+f235 +#define ICON_FA_USERS "\xef\x83\x80" // U+f0c0 +#define ICON_FA_USERS_COG "\xef\x94\x89" // U+f509 +#define ICON_FA_USERS_SLASH "\xee\x81\xb3" // U+e073 +#define ICON_FA_UTENSIL_SPOON "\xef\x8b\xa5" // U+f2e5 +#define ICON_FA_UTENSILS "\xef\x8b\xa7" // U+f2e7 +#define ICON_FA_VECTOR_SQUARE "\xef\x97\x8b" // U+f5cb +#define ICON_FA_VENUS "\xef\x88\xa1" // U+f221 +#define ICON_FA_VENUS_DOUBLE "\xef\x88\xa6" // U+f226 +#define ICON_FA_VENUS_MARS "\xef\x88\xa8" // U+f228 +#define ICON_FA_VEST "\xee\x82\x85" // U+e085 +#define ICON_FA_VEST_PATCHES "\xee\x82\x86" // U+e086 +#define ICON_FA_VIAL "\xef\x92\x92" // U+f492 +#define ICON_FA_VIALS "\xef\x92\x93" // U+f493 +#define ICON_FA_VIDEO "\xef\x80\xbd" // U+f03d +#define ICON_FA_VIDEO_SLASH "\xef\x93\xa2" // U+f4e2 +#define ICON_FA_VIHARA "\xef\x9a\xa7" // U+f6a7 +#define ICON_FA_VIRUS "\xee\x81\xb4" // U+e074 +#define ICON_FA_VIRUS_SLASH "\xee\x81\xb5" // U+e075 +#define ICON_FA_VIRUSES "\xee\x81\xb6" // U+e076 +#define ICON_FA_VOICEMAIL "\xef\xa2\x97" // U+f897 +#define ICON_FA_VOLLEYBALL_BALL "\xef\x91\x9f" // U+f45f +#define ICON_FA_VOLUME_DOWN "\xef\x80\xa7" // U+f027 +#define ICON_FA_VOLUME_MUTE "\xef\x9a\xa9" // U+f6a9 +#define ICON_FA_VOLUME_OFF "\xef\x80\xa6" // U+f026 +#define ICON_FA_VOLUME_UP "\xef\x80\xa8" // U+f028 +#define ICON_FA_VOTE_YEA "\xef\x9d\xb2" // U+f772 +#define ICON_FA_VR_CARDBOARD "\xef\x9c\xa9" // U+f729 +#define ICON_FA_WALKING "\xef\x95\x94" // U+f554 +#define ICON_FA_WALLET "\xef\x95\x95" // U+f555 +#define ICON_FA_WAREHOUSE "\xef\x92\x94" // U+f494 +#define ICON_FA_WATER "\xef\x9d\xb3" // U+f773 +#define ICON_FA_WAVE_SQUARE "\xef\xa0\xbe" // U+f83e +#define ICON_FA_WEIGHT "\xef\x92\x96" // U+f496 +#define ICON_FA_WEIGHT_HANGING "\xef\x97\x8d" // U+f5cd +#define ICON_FA_WHEELCHAIR "\xef\x86\x93" // U+f193 +#define ICON_FA_WIFI "\xef\x87\xab" // U+f1eb +#define ICON_FA_WIND "\xef\x9c\xae" // U+f72e +#define ICON_FA_WINDOW_CLOSE "\xef\x90\x90" // U+f410 +#define ICON_FA_WINDOW_MAXIMIZE "\xef\x8b\x90" // U+f2d0 +#define ICON_FA_WINDOW_MINIMIZE "\xef\x8b\x91" // U+f2d1 +#define ICON_FA_WINDOW_RESTORE "\xef\x8b\x92" // U+f2d2 +#define ICON_FA_WINE_BOTTLE "\xef\x9c\xaf" // U+f72f +#define ICON_FA_WINE_GLASS "\xef\x93\xa3" // U+f4e3 +#define ICON_FA_WINE_GLASS_ALT "\xef\x97\x8e" // U+f5ce +#define ICON_FA_WON_SIGN "\xef\x85\x99" // U+f159 +#define ICON_FA_WRENCH "\xef\x82\xad" // U+f0ad +#define ICON_FA_X_RAY "\xef\x92\x97" // U+f497 +#define ICON_FA_YEN_SIGN "\xef\x85\x97" // U+f157 +#define ICON_FA_YIN_YANG "\xef\x9a\xad" // U+f6ad diff --git a/include/TDM/TrueDirectionalMovementAPI.h b/include/TDM/TrueDirectionalMovementAPI.h new file mode 100644 index 0000000000..fe4385c0fd --- /dev/null +++ b/include/TDM/TrueDirectionalMovementAPI.h @@ -0,0 +1,166 @@ +#pragma once +#include +#include + +/* +* For modders: Copy this file into your own project if you wish to use this API +*/ + namespace TDM_API +{ + constexpr const auto TDMPluginName = "TrueDirectionalMovement"; + + using PluginHandle = SKSE::PluginHandle; + using ActorHandle = RE::ActorHandle; + + // Available True Directional Movement interface versions + enum class InterfaceVersion : uint8_t + { + V1, + V2 + }; + + // Error types that may be returned by the True Directional Movement API + enum class APIResult : uint8_t + { + // Your API call was successful + OK, + + // You tried to release a resource that was not allocated to you + // Do not attempt to manipulate the requested resource if you receive this response + NotOwner, + + // True Directional Movement currently must keep control of this resource for proper functionality + // Do not attempt to manipulate the requested resource if you receive this response + MustKeep, + + // You have already been given control of this resource + AlreadyGiven, + + // Another mod has been given control of this resource at the present time + // Do not attempt to manipulate the requested resource if you receive this response + AlreadyTaken, + + // You sent a command on a thread that could cause a data race were it to be processed + // Do not attempt to manipulate the requested resource if you receive this response + BadThread, + }; + + // True Directional Movement's modder interface + class IVTDM1 + { + public: + /// + /// Get the thread ID True Directional Movement is running in. + /// You may compare this with the result of GetCurrentThreadId() to help determine + /// if you are using the correct thread. + /// + /// TID + [[nodiscard]] virtual unsigned long GetTDMThreadId() const noexcept = 0; + + /// + /// Get the current state (enabled / disabled) of directional movement. + /// + /// The current state (true / false) of directional movement + [[nodiscard]] virtual bool GetDirectionalMovementState() noexcept = 0; + + /// + /// Get the current state (enabled / disabled) of target lock. + /// + /// The current state (true / false) of target lock + [[nodiscard]] virtual bool GetTargetLockState() noexcept = 0; + + /// + /// Get the actor handle of the currently locked target. + /// + /// The actor handle of the currently locked target + [[nodiscard]] virtual ActorHandle GetCurrentTarget() noexcept = 0; + + /// + /// Request the plugin to forcibly disable directional movement. + /// If granted, directional movement will be disabled for the duration of your control. + /// + /// Your assigned plugin handle + /// OK, MustKeep, AlreadyGiven, AlreadyTaken + [[nodiscard]] virtual APIResult RequestDisableDirectionalMovement(PluginHandle a_myPluginHandle) noexcept = 0; + + /// + /// Request the plugin to forcibly disable headtracking. + /// If granted, this mod's headtracking will be disabled for the duration of your control. + /// + /// Your assigned plugin handle + /// OK, MustKeep, AlreadyGiven, AlreadyTaken + [[nodiscard]] virtual APIResult RequestDisableHeadtracking(PluginHandle a_myPluginHandle) noexcept = 0; + + /// + /// Returns the current owner of the forced disable of directional movement. + /// + /// Handle or kPluginHandle_Invalid if no one currently owns the resource + virtual PluginHandle GetDisableDirectionalMovementOwner() const noexcept = 0; + + /// + /// Returns the current owner of the forced disable of headtracking. + /// + /// Handle or kPluginHandle_Invalid if no one currently owns the resource + virtual PluginHandle GetDisableHeadtrackingOwner() const noexcept = 0; + + /// + /// Release your forced disable of directional movement. + /// + /// Your assigned plugin handle + /// OK, NotOwner + virtual APIResult ReleaseDisableDirectionalMovement(PluginHandle a_myPluginHandle) noexcept = 0; + + /// + /// Release your forced disable of headtracking. + /// + /// Your assigned plugin handle + /// OK, NotOwner + virtual APIResult ReleaseDisableHeadtracking(PluginHandle a_myPluginHandle) noexcept = 0; + }; + + class IVTDM2 : public IVTDM1 + { + public: + /// + /// Request the control over the player character's yaw. + /// If granted, you may use the SetPlayerYaw function and TDM will not adjust the yaw for the duration of your control. + /// + /// Your assigned plugin handle + /// The speed multiplier to use when smoothly rotating to the desired yaw. 0 is instant. + /// OK, MustKeep, AlreadyGiven, AlreadyTaken + [[nodiscard]] virtual APIResult RequestYawControl(PluginHandle a_myPluginHandle, float a_yawRotationSpeedMultiplier) noexcept = 0; + + /// + /// Tries to set the player character's desired yaw. Will only do so if granted control. + /// + /// Your assigned plugin handle + /// The desired yaw + /// OK, NotOwner + virtual APIResult SetPlayerYaw(PluginHandle a_myPluginHandle, float a_desiredYaw) noexcept = 0; + + /// + /// Release your control over the player character's yaw. + /// + /// Your assigned plugin handle + /// OK, NotOwner + virtual APIResult ReleaseYawControl(PluginHandle a_myPluginHandle) noexcept = 0; + }; + + typedef void* (*_RequestPluginAPI)(const InterfaceVersion interfaceVersion); + + /// + /// Request the True Directional Movement API interface. + /// Recommended: Send your request during or after SKSEMessagingInterface::kMessage_PostLoad to make sure the dll has already been loaded + /// + /// The interface version to request + /// The pointer to the API singleton, or nullptr if request failed + [[nodiscard]] inline void* RequestPluginAPI(const InterfaceVersion a_interfaceVersion = InterfaceVersion::V2) + { + auto pluginHandle = GetModuleHandleA("TrueDirectionalMovement.dll"); + _RequestPluginAPI requestAPIFunction = (_RequestPluginAPI)GetProcAddress(pluginHandle, "RequestPluginAPI"); + if (requestAPIFunction) { + return requestAPIFunction(a_interfaceVersion); + } + return nullptr; + } +} diff --git a/package/Interface/CommunityShaders/Fonts/fa-solid-900.ttf b/package/Interface/CommunityShaders/Fonts/fa-solid-900.ttf new file mode 100644 index 0000000000..bb2a869565 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/fa-solid-900.ttf differ diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index 0d5ab51869..f78db78c86 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -990,6 +990,8 @@ "feature.linear_lighting.emissive_color_gamma": "Emissive Color Gamma", "feature.linear_lighting.emissive_color_multiplier": "Emissive Color Multiplier", "feature.linear_lighting.enable": "Enable Linear Lighting", + "feature.linear_lighting.enable_acescg": "Enable ACEScg Wide Gamut", + "feature.linear_lighting.enable_acescg_tooltip": "Render in ACEScg color space for wider gamut and more accurate lighting.\nRequires Linear Lighting and Post Processing enabled.\nAll sRGB-gamut textures and colors will be converted to ACEScg during shading.", "feature.linear_lighting.fog_gamma": "Fog Gamma", "feature.linear_lighting.fog_transparency_gamma": "Fog Transparency Gamma", "feature.linear_lighting.gamma_settings": "Gamma Settings", @@ -1065,6 +1067,570 @@ "feature.performance_overlay.key_feature_6": "Color-coded performance metrics with customizable thresholds", "feature.performance_overlay.key_feature_7": "Movable overlay window with persistent positioning", "feature.performance_overlay.name": "Performance Overlay", + "feature.post_processing.back_to_pipeline": "Back to Pipeline", + "feature.post_processing.base_amount": "Base Amount: %.3f", + "feature.post_processing.base_data": "Base Data:", + "feature.post_processing.border.border_color": "Border Color", + "feature.post_processing.border.depth_threshold": "Depth Threshold", + "feature.post_processing.border.description": "Add colored border, optionally with depth threshold.", + "feature.post_processing.border.name": "Border", + "feature.post_processing.border.scale_top_down_left_right": "Scale (Top, Down, Left, Right)", + "feature.post_processing.border.the_color_of_the_border": "The color of the border.", + "feature.post_processing.border.the_depth_threshold_for_the_border_effect": "The depth threshold for the border effect.", + "feature.post_processing.border.the_scale_of_the_border_on_each_side": "The scale of the border on each side of the screen.", + "feature.post_processing.bypass": "Bypass", + "feature.post_processing.camera.amount_of_noise_to_apply": "Amount of noise to apply.", + "feature.post_processing.camera.ca_amount": "CA amount", + "feature.post_processing.camera.chromatic_aberration_strength": "Chromatic aberration strength.", + "feature.post_processing.camera.crop": "Crop", + "feature.post_processing.camera.description": "Camera effects including fisheye, chromatic aberration, and film grain.", + "feature.post_processing.camera.enable_fisheye_effect": "Enable fisheye effect.", + "feature.post_processing.camera.fisheye": "Fisheye", + "feature.post_processing.camera.fov": "FOV", + "feature.post_processing.camera.fov_in_degrees_set_to_in_game_fov": "FOV in degrees.\n\nSet to in-game FOV.", + "feature.post_processing.camera.how_much_to_crop_into_the_image": "How much to crop into the image.\n\n0 = circular, 1 = full-frame.", + "feature.post_processing.camera.name": "Camera", + "feature.post_processing.camera.noise_amount": "Noise amount", + "feature.post_processing.camera.noise_type": "Noise type", + "feature.post_processing.camera.type_of_noise_to_apply": "Type of noise to apply.", + "feature.post_processing.cinematic_values": "Cinematic Values:", + "feature.post_processing.codbloom.a_greater_radius_makes_the_bloom_slightly_blurrier": "A greater radius makes the bloom slightly blurrier.", + "feature.post_processing.codbloom.debug": "Debug", + "feature.post_processing.codbloom.debug_mip_level": "Debug Mip Level", + "feature.post_processing.codbloom.description": "Bloom effect used in Call of Duty: Advanced Warfare. Expect HDR linear RGB inputs.", + "feature.post_processing.codbloom.intensity": "Intensity", + "feature.post_processing.codbloom.mip_level": "Mip Level", + "feature.post_processing.codbloom.mix": "Mix", + "feature.post_processing.codbloom.name": "COD Bloom", + "feature.post_processing.codbloom.texbloom": "texBloom", + "feature.post_processing.codbloom.the_greater_the_level_the_blurrier_the_part": "The greater the level, the blurrier the part it controls", + "feature.post_processing.codbloom.threshold": "Threshold", + "feature.post_processing.codbloom.upsampling_radius": "Upsampling Radius", + "feature.post_processing.color_grading.asc_cdl": "ASC CDL", + "feature.post_processing.color_grading.black_point": "Black Point", + "feature.post_processing.color_grading.black_tightness_offset": "Black Tightness Offset", + "feature.post_processing.color_grading.black_tightness_shape": "Black Tightness Shape", + "feature.post_processing.color_grading.brightness": "Brightness", + "feature.post_processing.color_grading.cinematic_blend": "Cinematic Blend", + "feature.post_processing.color_grading.cinematic_blend_tooltip": "Saturation, Brightness and Contrast.", + "feature.post_processing.color_grading.clip_point": "Clip Point", + "feature.post_processing.color_grading.color_grading": "Color Grading", + "feature.post_processing.color_grading.color_space_transform": "Color Space Transform", + "feature.post_processing.color_grading.contrast": "Contrast", + "feature.post_processing.color_grading.convert_linear_to_log_before_hdr_color_grading": "Convert Linear to Log Before HDR Color Grading", + "feature.post_processing.color_grading.convert_log_to_linear_after_hdr_color_grading": "Convert Log to Linear After HDR Color Grading", + "feature.post_processing.color_grading.cross_over_point": "Cross-over Point", + "feature.post_processing.color_grading.cross_over_point_tooltip": "Point where the toe and shoulder are pieced together into a single curve.", + "feature.post_processing.color_grading.curve_preview": "Curve Preview", + "feature.post_processing.color_grading.curve_preview_axis": "Pre: 0 - %.1f (HDR linear) | Post: 0 - %.1f%s", + "feature.post_processing.color_grading.curve_preview_hdr_suffix": " (HDR)", + "feature.post_processing.color_grading.curve_preview_post_b": "Post B: %.3f", + "feature.post_processing.color_grading.curve_preview_post_g": "Post G: %.3f", + "feature.post_processing.color_grading.curve_preview_post_r": "Post R: %.3f", + "feature.post_processing.color_grading.curve_preview_pre": "Pre: %.3f", + "feature.post_processing.color_grading.curve_preview_requires_lut": "Enable LUT generation to see curve preview (uncheck 'Skip LUT')", + "feature.post_processing.color_grading.cutoff": "Cutoff", + "feature.post_processing.color_grading.description": "Color grading operations and multiple tone mapping options.", + "feature.post_processing.color_grading.enable_tonemapping": "Enable Tonemapping", + "feature.post_processing.color_grading.exposure": "Exposure", + "feature.post_processing.color_grading.exposure_temperature_tint": "Exposure/Temperature/Tint", + "feature.post_processing.color_grading.fade_blend": "Fade Blend", + "feature.post_processing.color_grading.gain": "Gain", + "feature.post_processing.color_grading.game_color_grading": "Game Color Grading", + "feature.post_processing.color_grading.gamma": "Gamma", + "feature.post_processing.color_grading.hdr_output_active": "HDR Output Active", + "feature.post_processing.color_grading.highlights_gain": "Highlights Gain", + "feature.post_processing.color_grading.highlights_offset": "Highlights Offset", + "feature.post_processing.color_grading.highlights_start_end": "Highlights Start/End", + "feature.post_processing.color_grading.hue_correction": "Hue Correction", + "feature.post_processing.color_grading.hue_shift": "Hue Shift", + "feature.post_processing.color_grading.input_color_space": "Input Color Space: %s (%s)", + "feature.post_processing.color_grading.input_color_space_auto_detected": "auto-detected from Linear Lighting ACEScg", + "feature.post_processing.color_grading.input_color_space_fixed": "fixed", + "feature.post_processing.color_grading.input_gamma": "Input Gamma", + "feature.post_processing.color_grading.input_mid_level": "Input Mid-Level", + "feature.post_processing.color_grading.lift": "Lift", + "feature.post_processing.color_grading.lift_gamma_gain": "Lift Gamma Gain", + "feature.post_processing.color_grading.linear_section_length": "Linear Section Length", + "feature.post_processing.color_grading.linear_section_start": "Linear Section Start", + "feature.post_processing.color_grading.log_type": "Log Type", + "feature.post_processing.color_grading.max_brightness": "Max Brightness", + "feature.post_processing.color_grading.maximum_hdr_value": "Maximum HDR Value", + "feature.post_processing.color_grading.midtones_gain": "Midtones Gain", + "feature.post_processing.color_grading.midtones_offset": "Midtones Offset", + "feature.post_processing.color_grading.min_luminance": "Min Luminance", + "feature.post_processing.color_grading.name": "Color Grading and Tone Mapping", + "feature.post_processing.color_grading.offset": "Offset", + "feature.post_processing.color_grading.oklch_color_mixer": "OKLCH Color Mixer", + "feature.post_processing.color_grading.oklch_color_mixer_tooltip": "Adjust brightness, vibrance and hue shift of specific hues in the perceptually uniform OKLCH space.", + "feature.post_processing.color_grading.oklch_saturation": "OKLCH Saturation", + "feature.post_processing.color_grading.output_color_space": "Output Color Space: %s (auto from HDR Display)", + "feature.post_processing.color_grading.output_color_space_tooltip": "Output switches automatically: SDR -> sRGB, HDR -> BT2020.", + "feature.post_processing.color_grading.output_gamma": "Output Gamma", + "feature.post_processing.color_grading.output_mid_level": "Output Mid-Level", + "feature.post_processing.color_grading.output_saved_to": "Output will be saved to: %s", + "feature.post_processing.color_grading.paper_white_nits_from_hdr_settings": "Paper White: %.0f nits (from HDR settings)", + "feature.post_processing.color_grading.peak_brightness_nits_from_hdr_settings": "Peak Brightness: %.0f nits (from HDR settings)", + "feature.post_processing.color_grading.pivot": "Pivot", + "feature.post_processing.color_grading.post_tonemapping_settings": "Post-Tonemapping Settings", + "feature.post_processing.color_grading.power": "Power", + "feature.post_processing.color_grading.pre_tonemapping_settings": "Pre-Tonemapping Settings", + "feature.post_processing.color_grading.reset": "Reset", + "feature.post_processing.color_grading.rolloff_start": "Rolloff Start", + "feature.post_processing.color_grading.saturation": "Saturation", + "feature.post_processing.color_grading.saturation_boost": "Saturation Boost", + "feature.post_processing.color_grading.save_lut_and_output_image": "Save LUT and Output Image", + "feature.post_processing.color_grading.sdr_output_hdr_display_not_enabled": "SDR Output (HDR Display not enabled)", + "feature.post_processing.color_grading.shadows_gain": "Shadows Gain", + "feature.post_processing.color_grading.shadows_midtones_highlights": "Shadows/Midtones/Highlights", + "feature.post_processing.color_grading.shadows_offset": "Shadows Offset", + "feature.post_processing.color_grading.shadows_start_end": "Shadows Start/End", + "feature.post_processing.color_grading.shoulder": "Shoulder", + "feature.post_processing.color_grading.shoulder_strength": "Shoulder Strength", + "feature.post_processing.color_grading.shoulder_strength_tooltip": "Amount of blending between a straight-line curve and a purely asymptotic curve for the shoulder.", + "feature.post_processing.color_grading.skip_ldr_color_grading": "Skip LDR Color Grading", + "feature.post_processing.color_grading.skip_ldr_color_grading_tooltip": "Skip color grading after tonemapping. This includes Lift Gamma Gain. Will be automatically skipped with HDR on.", + "feature.post_processing.color_grading.skip_lut_direct_color_grading": "Skip LUT (Direct Color Grading)", + "feature.post_processing.color_grading.skip_lut_direct_color_grading_tooltip": "Skip baking color grading into a LUT and apply it directly per-pixel. More accurate but slower.", + "feature.post_processing.color_grading.slope": "Slope", + "feature.post_processing.color_grading.temperature": "Temperature", + "feature.post_processing.color_grading.tint": "Tint", + "feature.post_processing.color_grading.tint_blend": "Tint Blend", + "feature.post_processing.color_grading.toe_strength": "Toe Strength", + "feature.post_processing.color_grading.toe_strength_tooltip": "Amount of blending between a straight-line curve and a purely asymptotic curve for the toe.", + "feature.post_processing.color_grading.tonemapper": "Tonemapper", + "feature.post_processing.color_grading.tonemapper.aces.description": "ACES RRT+ODT tonemapper implementation from RenoDX.", + "feature.post_processing.color_grading.tonemapper.agx_minimal.description": "Minimal version of Troy Sobotka's AgX using a 6th order polynomial approximation. Originally created by bwrensch, and improved by Troy Sobotka. Internally uses AgX input transform.", + "feature.post_processing.color_grading.tonemapper.aldridge_filmic.description": "Variation of the Hejl and Burgess-Dawson filmic curve done by Graham Aldridge. See his blog post about \"Approximating Film with Tonemapping\".", + "feature.post_processing.color_grading.tonemapper.day_filmic.description": "Filmic curve by Mike Day, described in his document \"An efficient and user-friendly tone mapping operator\". Also known as the \"Insomniac curve\".", + "feature.post_processing.color_grading.tonemapper.frostbite.description": "Frostbite HDR display mapping implementation from RenoDX, based on EA's Frostbite color grading and display presentation work.", + "feature.post_processing.color_grading.tonemapper.gt7.description": "Tonemapper designed for Gran Turismo 7.", + "feature.post_processing.color_grading.tonemapper.hejl_burgess_dawson_filmic.description": "Variation of the Hejl and Burgess-Dawson filmic curve done by Graham Aldridge. See his blog post about \"Approximating Film with Tonemapping\".", + "feature.post_processing.color_grading.tonemapper.hermite_spline.description": "Hermite spline tonemapper by Musa, from RenoDX.", + "feature.post_processing.color_grading.tonemapper.kajiya.description": "Tonemapper designed by Tomasz Stachowiak/Embark for their real time ray tracing engine Kajiya.", + "feature.post_processing.color_grading.tonemapper.lottes_filmic.description": "Filmic curve by Timothy Lottes, described in his GDC talk \"Advanced Techniques and Optimization of HDR Color Pipelines\". Also known as the \"AMD curve\".", + "feature.post_processing.color_grading.tonemapper.melon.description": "Tonemapper designed by TripleMelon to fix the ACES issue of intense colour being shifted.", + "feature.post_processing.color_grading.tonemapper.neutwo.description": "Neutwo tonemapper by Carlos Lopez, from RenoDX.", + "feature.post_processing.color_grading.tonemapper.psychov.description": "PsychoV 17 tonemapper by Carlos Lopez, from RenoDX.", + "feature.post_processing.color_grading.tonemapper.reinhard.description": "Mapping proposed in \"Photographic Tone Reproduction for Digital Images\" by Reinhard et al. 2002.", + "feature.post_processing.color_grading.tonemapper.reinhard_extended.description": "Extended mapping proposed in \"Photographic Tone Reproduction for Digital Images\" by Reinhard et al. 2002. An additional user parameter specifies the smallest luminance that is mapped to 1, which allows high luminances to burn out.", + "feature.post_processing.color_grading.tonemapper.uchimura_filmic.description": "Filmic curve by Hajime Uchimura, described in his CEDEC talk \"HDR Theory and Practice\". Characterised by its middle linear section. Also known as the \"Gran Turismo curve\".", + "feature.post_processing.color_grading.tonemapping": "Tonemapping", + "feature.post_processing.color_grading.use_open_drt": "Use OpenDRT", + "feature.post_processing.color_grading.vibrance": "Vibrance", + "feature.post_processing.color_grading.white_clip": "White Clip", + "feature.post_processing.color_grading.white_point": "White Point", + "feature.post_processing.color_grading.working_color_space": "Working Color Space", + "feature.post_processing.composite.description": "Composites Bloom, Lens Flare, Physical Glare, and Auto Exposure onto the main image. Applies exposure (SceneColor * Exposure + Bloom * Exposure) before Color Grading. Automatically enabled when any contributing feature is active.", + "feature.post_processing.composite.name": "Composite", + "feature.post_processing.debug": "Debug", + "feature.post_processing.depth_of_field_values": "Depth of Field Values:", + "feature.post_processing.depth_of_field_values_detail": "DOF Strength: %.3f\nDOF Distance: %.3f\nDOF Range: %.3f\nDOF Flags: %d\nDOF Sky Blur Radius: %d", + "feature.post_processing.description": "Post Processing provides advanced image effects and enhancements to improve the visual quality of the game.", + "feature.post_processing.disable_vanilla_tonemapping": "Disable Vanilla Tonemapping", + "feature.post_processing.do_f.auto_focus": "Auto Focus", + "feature.post_processing.do_f.blur_quality": "Blur Quality", + "feature.post_processing.do_f.bokeh_busy_factor": "Bokeh Busy Factor", + "feature.post_processing.do_f.console_selection": "Console Selection", + "feature.post_processing.do_f.debug": "Debug", + "feature.post_processing.do_f.debug_distance": "Debug Distance: %f", + "feature.post_processing.do_f.debug_focus_plane": "Debug Focus Plane: %f", + "feature.post_processing.do_f.description": "Depth of Field, based on CinematicDOF by Frans Bouma.", + "feature.post_processing.do_f.f_number": "F-Number", + "feature.post_processing.do_f.far_plane_max_blur": "Far Plane Max Blur", + "feature.post_processing.do_f.focal_length": "Focal Length", + "feature.post_processing.do_f.focus_point": "Focus Point", + "feature.post_processing.do_f.highlight_boost": "Highlight Boost", + "feature.post_processing.do_f.highlight_custom_shape": "Highlight Custom Shape", + "feature.post_processing.do_f.highlight_shape_rotation": "Highlight Shape Rotation", + "feature.post_processing.do_f.manual_focus": "Manual Focus", + "feature.post_processing.do_f.name": "Depth of Field", + "feature.post_processing.do_f.near_far_plane_distance_compenation": "Near-Far Plane Distance Compenation", + "feature.post_processing.do_f.near_plane_max_blur": "Near Plane Max Blur", + "feature.post_processing.do_f.petzval_strength": "Petzval Strength", + "feature.post_processing.do_f.post_blur_smoothing": "Post Blur Smoothing", + "feature.post_processing.do_f.selected_reference": "Selected Reference: %08X", + "feature.post_processing.do_f.target_focus": "Target Focus", + "feature.post_processing.do_f.target_focus_focal_length": "Target Focus Focal Length", + "feature.post_processing.do_f.transition_speed": "Transition Speed", + "feature.post_processing.do_f.view_resize": "View Resize", + "feature.post_processing.edit_settings_for_this_feature": "Edit settings for this feature.", + "feature.post_processing.enable_the_feature_to_see_its_settings": "Enable the feature to see its settings.", + "feature.post_processing.enabled": "Enabled", + "feature.post_processing.game_imagespace_values": "Game ImageSpace Values", + "feature.post_processing.hdr_values": "HDR Values:", + "feature.post_processing.hdr_values_detail": "Eye Adapt Speed: %.3f\nBloom Blur Radius: %.3f\nBloom Threshold: %.3f\nBloom Scale: %.3f\nReceive Bloom Threshold: %.3f\nWhite: %.3f\nSunlight Scale: %.3f\nSky Scale: %.3f\nEye Adapt Strength: %.3f", + "feature.post_processing.histogram_auto_exposure.adaptation_range": "Adaptation Range", + "feature.post_processing.histogram_auto_exposure.adaptation_speed": "Adaptation Speed", + "feature.post_processing.histogram_auto_exposure.adapted_luminance_ev": "Adapted Luminance: %.6g (%.2f EV100)", + "feature.post_processing.histogram_auto_exposure.applying_additional_exposure_adjustment_to_the_image": "Applying additional exposure adjustment to the image.", + "feature.post_processing.histogram_auto_exposure.bin": "Bin: %d", + "feature.post_processing.histogram_auto_exposure.bin_0_below_luminance_threshold": "Bin 0: below luminance threshold", + "feature.post_processing.histogram_auto_exposure.compensated_target_ev": "Compensated Target: %.6g (%.2f EV100)", + "feature.post_processing.histogram_auto_exposure.cyan_compensation_target": "Cyan: Compensation Target", + "feature.post_processing.histogram_auto_exposure.description": "Auto exposure and eye adaptation method that uses a histogram to calculate average screen brightness. Expects HDR linear RGB inputs.", + "feature.post_processing.histogram_auto_exposure.ev": "EV100: %.2f", + "feature.post_processing.histogram_auto_exposure.exposure_compensation": "Exposure Compensation", + "feature.post_processing.histogram_auto_exposure.fade_in_ev": "Fade In EV100", + "feature.post_processing.histogram_auto_exposure.final_global_exposure_ev": "Final Global Exposure: %.6g (%+.2f EV)", + "feature.post_processing.histogram_auto_exposure.focus_area": "Focus Area", + "feature.post_processing.histogram_auto_exposure.from_this_point_onward_the_purkinje_effect_remains": "From this point onward, the Purkinje effect remains the greatest.", + "feature.post_processing.histogram_auto_exposure.green_adapted_ev": "Green: Adapted EV100", + "feature.post_processing.histogram_auto_exposure.histogram": "Histogram", + "feature.post_processing.histogram_auto_exposure.luminance": "Luminance: %.6g", + "feature.post_processing.histogram_auto_exposure.luminance_histogram_ev": "Luminance Histogram (%.0f - %.0f EV100)", + "feature.post_processing.histogram_auto_exposure.max_effect_ev": "Max Effect EV100", + "feature.post_processing.histogram_auto_exposure.max_strength": "Max Strength", + "feature.post_processing.histogram_auto_exposure.min_max_the_average_scene_luminance_will_be": "[Min, Max] The average scene luminance will be clamped between them when doing auto exposure.Turning up the minimum, for example, makes it adapt less to darkness and therefore prevents over-brightening of dark scenes.", + "feature.post_processing.histogram_auto_exposure.name": "Histogram Auto Exposure", + "feature.post_processing.histogram_auto_exposure.purkinje_effect": "Purkinje Effect", + "feature.post_processing.histogram_auto_exposure.samples": "Samples: %u", + "feature.post_processing.histogram_auto_exposure.specifies_the_proportion_of_the_area_width_height": "Specifies the proportion of the area [width, height] that auto exposure will adapt to.", + "feature.post_processing.histogram_auto_exposure.the_purkinje_effect_simulates_the_blue_shift_of": "The Purkinje effect simulates the blue shift of human vision under low light.\nIf you don't like the effect, you can set the strength to zero.", + "feature.post_processing.histogram_auto_exposure.the_purkinje_effect_will_start_to_take_place": "The Purkinje effect will start to take place when the average scene luminance falls lower than this.", + "feature.post_processing.histogram_auto_exposure.yellow_adaptation_range": "Yellow: Adaptation Range", + "feature.post_processing.invalid_feature_selected_returning_to_list": "Invalid feature selected. Returning to list.", + "feature.post_processing.key_feature_1": "Customizable post-processing effects", + "feature.post_processing.key_feature_2": "Supports various presets for different visual styles", + "feature.post_processing.key_feature_3": "Improves overall image quality and immersion", + "feature.post_processing.key_feature_4": "Includes features like bloom, depth of field, and color grading", + "feature.post_processing.lens_flare.aperture_blades": "Aperture Blades", + "feature.post_processing.lens_flare.aperture_rotation": "Aperture Rotation", + "feature.post_processing.lens_flare.blur_iterations": "Blur Iterations", + "feature.post_processing.lens_flare.color": "Color", + "feature.post_processing.lens_flare.color_tint": "Color Tint", + "feature.post_processing.lens_flare.custom_ghost_colors_scales": "Custom Ghost Colors & Scales", + "feature.post_processing.lens_flare.debug": "Debug", + "feature.post_processing.lens_flare.description": "Screen-space lens flare with ghosts and halo. Supports FFT bokeh convolution for physically-shaped ghosts.", + "feature.post_processing.lens_flare.disable_blur": "Disable Blur", + "feature.post_processing.lens_flare.disable_ghosts": "Disable Ghosts", + "feature.post_processing.lens_flare.disable_threshold": "Disable Threshold", + "feature.post_processing.lens_flare.enabled": "Enabled", + "feature.post_processing.lens_flare.f_stop": "F-Stop", + "feature.post_processing.lens_flare.fft_resolution": "FFT Resolution", + "feature.post_processing.lens_flare.ghost_chroma_shift": "Ghost Chroma Shift", + "feature.post_processing.lens_flare.ghost_mode": "Ghost Mode", + "feature.post_processing.lens_flare.ghost_settings": "Ghost Settings", + "feature.post_processing.lens_flare.ghost_strength": "Ghost Strength", + "feature.post_processing.lens_flare.halo_chroma_shift": "Halo Chroma Shift", + "feature.post_processing.lens_flare.halo_compression": "Halo Compression", + "feature.post_processing.lens_flare.halo_radius": "Halo Radius", + "feature.post_processing.lens_flare.halo_settings": "Halo Settings", + "feature.post_processing.lens_flare.halo_strength": "Halo Strength", + "feature.post_processing.lens_flare.halo_width": "Halo Width", + "feature.post_processing.lens_flare.intensity": "Intensity", + "feature.post_processing.lens_flare.kernel_scale": "Kernel Scale", + "feature.post_processing.lens_flare.kernel_scale_2": "Kernel Scale", + "feature.post_processing.lens_flare.name": "Lens Flare", + "feature.post_processing.lens_flare.non_intrusive_ghosts": "Non-intrusive Ghosts", + "feature.post_processing.lens_flare.reset_ghosts_to_default": "Reset Ghosts to Default", + "feature.post_processing.lens_flare.scale": "Scale", + "feature.post_processing.lens_flare.threshold": "Threshold", + "feature.post_processing.lens_flare.threshold_ev": "Threshold (EV100)", + "feature.post_processing.lens_flare.threshold_range": "Threshold Range", + "feature.post_processing.lens_flare.tint": "Tint", + "feature.post_processing.lens_flare.view_resize": "View Resize", + "feature.post_processing.load": "Load", + "feature.post_processing.local_exposure.boost_local_contrast": "Boost Local Contrast", + "feature.post_processing.local_exposure.coarse_scale_mip": "Coarse Scale (Mip)", + "feature.post_processing.local_exposure.debug": "Debug", + "feature.post_processing.local_exposure.description": "Local Exposure brightens shadows and compresses highlights based on local neighborhood luminance. Runs before Auto Exposure and is applied in the Composite pass.", + "feature.post_processing.local_exposure.detail_scale_display_mip": "Detail Scale (Display Mip)", + "feature.post_processing.local_exposure.exposure": "Exposure", + "feature.post_processing.local_exposure.exposure_preference": "Exposure Preference", + "feature.post_processing.local_exposure.finest_pyramid_level_reconstructed_before_full_resolution_upsampling": "Finest pyramid level reconstructed before full-resolution upsampling. Lower values keep smaller local details.", + "feature.post_processing.local_exposure.gives_high_contrast_local_details_more_influence_during": "Gives high-contrast local details more influence during pyramid reconstruction.", + "feature.post_processing.local_exposure.highlight_recovery": "Highlight Recovery", + "feature.post_processing.local_exposure.how_selectively_each_area_chooses_its_best_exposure": "How selectively each area chooses its best exposure. Higher values create stronger local adaptation; lower values blend more softly.", + "feature.post_processing.local_exposure.how_strongly_bright_areas_are_compressed_higher_values": "How strongly bright areas are compressed. Higher values preserve more highlight detail.", + "feature.post_processing.local_exposure.how_strongly_darker_areas_are_lifted_higher_values": "How strongly darker areas are lifted. Higher values recover more shadow detail.", + "feature.post_processing.local_exposure.largest_image_scale_used_by_the_effect_higher": "Largest image scale used by the effect. Higher values affect broader lighting regions.", + "feature.post_processing.local_exposure.manual_brightness_normalization_used_when_histogram_auto_exposure": "Manual brightness normalization used when Histogram Auto Exposure is disabled. Higher values make the scene behave brighter.", + "feature.post_processing.local_exposure.name": "Local Exposure", + "feature.post_processing.local_exposure.shadow_recovery": "Shadow Recovery", + "feature.post_processing.local_exposure.view_resize": "View Resize", + "feature.post_processing.lut.1d_lut_n_x_1_sized_images": "1D LUT: N x 1 sized images.", + "feature.post_processing.lut.3d_lut_3d_dds_only": "3D LUT: 3D dds only.", + "feature.post_processing.lut.3d_lut_in_2d_format_n_r_x": "3D LUT in 2D format: N (R) x N (G) sized images, stacked horizontally along blue axis.", + "feature.post_processing.lut.clear": "Clear", + "feature.post_processing.lut.description": "Look-up table application.", + "feature.post_processing.lut.input_max": "Input Max", + "feature.post_processing.lut.input_min": "Input Min", + "feature.post_processing.lut.load": "Load", + "feature.post_processing.lut.loaded_texture": "Loaded Texture: %s", + "feature.post_processing.lut.loaded_texture_none": "Loaded Texture: None", + "feature.post_processing.lut.lut_texture_path": "LUT Texture Path", + "feature.post_processing.lut.map_luma": "Map Luma", + "feature.post_processing.lut.map_per_channel": "Map Per Channel", + "feature.post_processing.lut.name": "LUT", + "feature.post_processing.lut.relative_path_starts_from_game_executable_directory_supports": "Relative path starts from game executable directory. Supports dds/bmp/png format.", + "feature.post_processing.mod_amount": "Mod Amount: %.3f", + "feature.post_processing.mod_data": "Mod Data:", + "feature.post_processing.mod_dof_values_detail": "DOF Strength: %.3f\nDOF Distance: %.3f\nDOF Range: %.3f\nDOF Mode: %d", + "feature.post_processing.mod_fade_values_detail": "Fade Amount: %.3f\nFade Color: (%.3f, %.3f, %.3f)\nBlur Radius: %.3f\nDouble Vision Strength: %.3f\n", + "feature.post_processing.motion_blur.a": "(?)", + "feature.post_processing.motion_blur.description": "Creates cinematic motion blur based on camera and object movement.", + "feature.post_processing.motion_blur.name": "Motion Blur", + "feature.post_processing.motion_blur.sample_count_is_doubled_internally_for_smoother_results": "Sample count is doubled internally for smoother results.\nMore samples = better quality but slower performance", + "feature.post_processing.motion_blur.samples": "Samples", + "feature.post_processing.motion_blur_strength": "Motion Blur Strength: %.3f", + "feature.post_processing.name": "Post Processing", + "feature.post_processing.open_drt.adjusts_contrast_or_slope_a_constrained_power_function": "Adjusts contrast or slope.\nA constrained power function applied in display linear.", + "feature.post_processing.open_drt.amount_adjust_highlights_positive_values_increase_highlight_exposure": "Amount adjust highlights.\nPositive values increase highlight exposure, negative values decrease. 0 has no effect.", + "feature.post_processing.open_drt.amount_of_contrast_to_add_0_0_has": "Amount of contrast to add. 0.0 has no effect.\n1.0 will expose down by 1 stop at the origin (0,0)", + "feature.post_processing.open_drt.amount_of_stops_above_middle_grey_0_18": "Amount of stops above middle grey (0.18) to start the adjustment.", + "feature.post_processing.open_drt.amount_of_stops_to_boost_grey_luminance_per": "Amount of stops to boost Grey Luminance, per stop of exposure increase of Peak Luminance.\nFor example, if HDR Grey Boost is 0.1, middle grey will be boosted by 0.1 stops per stop of Peak Luminance increase.", + "feature.post_processing.open_drt.amount_to_decrease_purity_of_upper_midtones_and": "Amount to decrease purity of upper midtones and highlights in mid-range purity areas.\nA value of 0.0 will have no effect. A value of 1.0 is the maximum possible value while preserving smoothness.", + "feature.post_processing.open_drt.amount_to_distort_the_blue_hue_angle_towards": "Amount to distort the blue hue angle towards cyan as intensity increases.", + "feature.post_processing.open_drt.amount_to_distort_the_cyan_hue_angle_towards": "Amount to distort the cyan hue angle towards blue as intensity decreases.", + "feature.post_processing.open_drt.amount_to_distort_the_green_hue_angle_towards": "Amount to distort the green hue angle towards yellow as intensity increases.", + "feature.post_processing.open_drt.amount_to_distort_the_magenta_hue_angle_towards": "Amount to distort the magenta hue angle towards blue as intensity decreases.", + "feature.post_processing.open_drt.amount_to_distort_the_red_hue_angle_towards": "Amount to distort the red hue angle towards yellow as intensity increases.", + "feature.post_processing.open_drt.amount_to_distort_the_yellow_hue_angle_towards": "Amount to distort the yellow hue angle towards red as intensity decreases.", + "feature.post_processing.open_drt.amount_to_increase_hue_contrast_at_the_red": "Amount to increase Hue Contrast at the red hue angle.", + "feature.post_processing.open_drt.amount_to_increase_purity_of_midtones_and_shadows": "Amount to increase purity of midtones and shadows in mid-range purity areas.\nA value of 0.0 will have no effect. A value of 1.0 is the maximum possible value while preserving smoothness.", + "feature.post_processing.open_drt.apply_preset": "Apply Preset", + "feature.post_processing.open_drt.as_brilliance_range_is_increased_the_brilliance_adjustments": "As Brilliance Range is increased, the brilliance adjustments affect more the low intensity values of the image data.", + "feature.post_processing.open_drt.as_brilliance_strength_is_increased_the_brilliance_adjustments": "As Brilliance Strength is increased, the brilliance adjustments affect more the low purity values of the image data.", + "feature.post_processing.open_drt.brilliance": "Brilliance##Header", + "feature.post_processing.open_drt.brilliance_2": "Brilliance", + "feature.post_processing.open_drt.brilliance_b": "Brilliance B", + "feature.post_processing.open_drt.brilliance_g": "Brilliance G", + "feature.post_processing.open_drt.brilliance_post": "Brilliance Post", + "feature.post_processing.open_drt.brilliance_r": "Brilliance R", + "feature.post_processing.open_drt.brilliance_range": "Brilliance Range", + "feature.post_processing.open_drt.brilliance_scales_the_intensity_of_more_pure_stimuli": "Brilliance scales the intensity of more pure stimuli.\nThe brilliance module is applied before the tonescale is taken for purity compression.\nThis means that if you darken reds with this adjustment, the purity compression will also be reduced.\nThis behavior is natural and smooth.", + "feature.post_processing.open_drt.brilliance_strength": "Brilliance Strength", + "feature.post_processing.open_drt.built_in_presets": "Built-in Presets", + "feature.post_processing.open_drt.clamp": "Clamp", + "feature.post_processing.open_drt.clamp_the_final_image_into_the_final_range": "Clamp the final image into the final range supported by the display device.", + "feature.post_processing.open_drt.compresses_purity_as_intensity_increases_bare_minimum_functionality": "Compresses purity as intensity increases.\nBare minimum functionality for a picture formation.", + "feature.post_processing.open_drt.contrast": "Contrast", + "feature.post_processing.open_drt.contrast_high": "Contrast High", + "feature.post_processing.open_drt.contrast_high_allows_control_of_the_upper_section": "Contrast High allows control of the upper section of the tonescale function.\nOff by default, but can be useful if a stronger highlight contrast, or a softer highlight rolloff behavior is desired.", + "feature.post_processing.open_drt.contrast_high_pivot": "Contrast High Pivot", + "feature.post_processing.open_drt.contrast_high_strength": "Contrast High Strength", + "feature.post_processing.open_drt.contrast_low": "Contrast Low", + "feature.post_processing.open_drt.contrast_low_adds_contrast_to_the_midtones_and": "Contrast Low adds contrast to the midtones and shadows.\nMiddle grey (0.18) is un-changed through the adjustment.", + "feature.post_processing.open_drt.contrast_low_width": "Contrast Low Width", + "feature.post_processing.open_drt.creative_white": "Creative White", + "feature.post_processing.open_drt.creative_white_limit": "Creative White Limit", + "feature.post_processing.open_drt.display": "Display", + "feature.post_processing.open_drt.display_grey_luminance": "Display Grey Luminance", + "feature.post_processing.open_drt.display_luminance_for_middle_grey_0_18_in": "Display luminance for middle grey (0.18) in nits.\nSets the target value for middle grey within the available luminance range of the display device.", + "feature.post_processing.open_drt.display_peak_luminance": "Display Peak Luminance", + "feature.post_processing.open_drt.enable_brilliance": "Enable Brilliance", + "feature.post_processing.open_drt.enable_contrast_high": "Enable Contrast High", + "feature.post_processing.open_drt.enable_contrast_low": "Enable Contrast Low", + "feature.post_processing.open_drt.enable_hue_contrast": "Enable Hue Contrast", + "feature.post_processing.open_drt.enable_hueshift_cmy": "Enable Hueshift CMY", + "feature.post_processing.open_drt.enable_hueshift_rgb": "Enable Hueshift RGB", + "feature.post_processing.open_drt.enable_mid_purity": "Enable Mid Purity", + "feature.post_processing.open_drt.enable_post_brilliance": "Enable Post Brilliance", + "feature.post_processing.open_drt.enable_purity_compress_high": "Enable Purity Compress High", + "feature.post_processing.open_drt.enable_purity_softclip": "Enable Purity Softclip", + "feature.post_processing.open_drt.global_intensity_scale_of_high_purity_stimuli": "Global intensity scale of high-purity stimuli.", + "feature.post_processing.open_drt.global_post_purity_compression_brilliance_adjustment": "Global post purity compression brilliance adjustment ", + "feature.post_processing.open_drt.hdr_display_nits_paper_white_nits_peak_linear": "HDR Display: %.0f nits paper white, %.0f nits peak (%.2f linear)", + "feature.post_processing.open_drt.hdr_grey_boost": "HDR Grey Boost", + "feature.post_processing.open_drt.hdr_purity": "HDR Purity", + "feature.post_processing.open_drt.how_much_to_affect_purity_compression_and_hue": "How much to affect purity compression and hue shift as Peak Luminance increases.\nA value of 0.0 will keep the purity compression and hue shift behavior the same for SDR and HDR.\nA value of 1.0 will preserve more purity as peak luminance increases\n(at the risk of gradient disruptions in high purity high intensity light sources), and will reduce hue shift amount in highlights.", + "feature.post_processing.open_drt.how_quickly_above_the_contrast_high_pivot_the": "How quickly above the Contrast High Pivot the effect begins.", + "feature.post_processing.open_drt.hue": "Hue", + "feature.post_processing.open_drt.hue_contrast_compresses_hue_angle_towards_the_primary": "Hue Contrast compresses hue angle towards the primary at the bottom end and expands the hue angle towards the secondary as intensity increases.\nIt also increases purity as it compresses, and decreases purity as it expands.\nThis leads to a nice creatively controllable simulation of this effect from per-channel tonescales.\nFor OpenDRT we only keep the red hue angle control since it is the most useful.", + "feature.post_processing.open_drt.hue_contrast_r": "Hue Contrast R", + "feature.post_processing.open_drt.hue_contrast_r_range": "Hue Contrast R Range", + "feature.post_processing.open_drt.hue_contrast_range_control_determines_where_over_the": "Hue contrast range control: determines where over the intensity range the hue contrast affects.\nHigher values place the crossover point higher in the intensity range.", + "feature.post_processing.open_drt.hue_shift_cmy_adds_hue_distortion_to_the": "Hue Shift CMY adds hue distortion to the cyan magenta and yellow secondary hue angles as intensity decreases.\nThis module allows some very minimal adjustments of secondary hue angles as a creative tool.", + "feature.post_processing.open_drt.hue_shift_rgb_adds_hue_distortion_to_the": "Hue Shift RGB adds hue distortion to the red green and blue primary hue angles as intensity increases.\nBy default OpenDRT will compress purity in a straight line in RGB/Chromaticity space.\nThis can lead to perceived hue shifts due to the Abney Effect, for example a pure blue will perceptually shift towards purple as it desaturates.\nTo compensate for this, and to use as a creative tool, this module allows creative control over the path that red green and blue hue angles take as their purity is compressed.", + "feature.post_processing.open_drt.hueshift_b": "Hueshift B", + "feature.post_processing.open_drt.hueshift_b_range": "Hueshift B Range", + "feature.post_processing.open_drt.hueshift_c": "Hueshift C", + "feature.post_processing.open_drt.hueshift_c_range": "Hueshift C Range", + "feature.post_processing.open_drt.hueshift_g": "Hueshift G", + "feature.post_processing.open_drt.hueshift_g_range": "Hueshift G Range", + "feature.post_processing.open_drt.hueshift_m": "Hueshift M", + "feature.post_processing.open_drt.hueshift_m_range": "Hueshift M Range", + "feature.post_processing.open_drt.hueshift_r": "Hueshift R", + "feature.post_processing.open_drt.hueshift_r_range": "Hueshift R Range", + "feature.post_processing.open_drt.hueshift_y": "Hueshift Y", + "feature.post_processing.open_drt.hueshift_y_range": "Hueshift Y Range", + "feature.post_processing.open_drt.limit_the_intensity_range_affected_by_the_creative": "Limit the intensity range affected by the Creative Whitepoint.\nAt 0.0, the entire intensity range is affected. As the limit is decreased, more of midtones and shadows are kept neutral.\nIt can be creatively desireable to keep midtones more neutral while shifting highlights warmer for example.", + "feature.post_processing.open_drt.limit_the_strength_of_blue_purity_compression_as": "Limit the strength of blue purity compression as intensity increases.\nCan be helpful to keep some color in high intensity pure blue light sources.\nUse with caution as this can cause gradient disruptions.", + "feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity": "Limit the strength of purity compression as intensity decreases, for all hue angles.\nA higher value will compress purity less in midtones and shadows.", + "feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_2": "Limit the strength of purity compression as intensity decreases, for reds.\nA higher value will compress purity less in midtones and shadows.", + "feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_3": "Limit the strength of purity compression as intensity decreases, for all greens.\nA higher value will compress purity less in midtones and shadows.", + "feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_4": "Limit the strength of purity compression as intensity decreases, for blues.\nA higher value will compress purity less in midtones and shadows.", + "feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_5": "Limit the strength of purity compression as intensity increases.\nCan be helpful to keep some color in high intensity high purity light sources.\nUse with caution as this can cause gradient disruptions.", + "feature.post_processing.open_drt.limit_the_strength_of_red_purity_compression_as": "Limit the strength of red purity compression as intensity increases.\nCan be helpful to keep some color in high intensity fire and pure red light sources.\nUse with caution as this can cause gradient disruptions.", + "feature.post_processing.open_drt.look_preset": "Look Preset", + "feature.post_processing.open_drt.mid_purity_high": "Mid Purity High", + "feature.post_processing.open_drt.mid_purity_high_range": "Mid Purity High Range", + "feature.post_processing.open_drt.mid_purity_high_strength": "Mid Purity High Strength", + "feature.post_processing.open_drt.mid_purity_low": "Mid Purity Low", + "feature.post_processing.open_drt.mid_purity_low_range": "Mid Purity Low Range", + "feature.post_processing.open_drt.mid_purity_low_strength": "Mid Purity Low Strength", + "feature.post_processing.open_drt.offset": "Offset", + "feature.post_processing.open_drt.opendrt_includes_a_simple_surround_compensation_model_dci": "OpenDRT includes a simple surround compensation model.\nDCI cinema presets use dark surround. Rec.1886 uses dim surround. And sRGB Display uses bright surround.\nThis functionality should provide a better perceptual match between viewing environments.", + "feature.post_processing.open_drt.peak_display_luminance_in_nits_in_sdr_the": "Peak display luminance in nits.\nIn SDR, the max value stays pinned at 1.0. In HDR, this is overridden by HDR Display Peak Brightness.", + "feature.post_processing.open_drt.post_brilliance_b": "Post Brilliance B", + "feature.post_processing.open_drt.post_brilliance_g": "Post Brilliance G", + "feature.post_processing.open_drt.post_brilliance_r": "Post Brilliance R", + "feature.post_processing.open_drt.post_brilliance_scales_the_intensity_of_more_pure": "Post Brilliance scales the intensity of more pure stimuli after purity compression hue shifts have been applied.\nWith the OpenDRT algorithm it is possible to get high intensity high purity values going out of the top of the display-referred gamut volume,\nwhich can cause discontinuities in gradients, especially on the RGB primaries.\nThis module can help compensate for this.", + "feature.post_processing.open_drt.pre_tonescale_scene_linear_offset_if_0_0": "Pre-tonescale scene-linear offset.\nIf 0.0, scene-linear 0.0 maps to display-linear 0.0 through the tonescale system.\nMany camera imaging pipelines apply a negative offset to set the average of shadow grain at 0.0.\nA positive Offset can be desireable to compensate for this and increase detail in shadows, in addition to being aesthetically desireable.\nOffset should NOT be a negative number (Looking at you ACES 1.x)", + "feature.post_processing.open_drt.purity": "Purity", + "feature.post_processing.open_drt.purity_limit_high": "Purity Limit High", + "feature.post_processing.open_drt.purity_limit_high_b": "Purity Limit High B", + "feature.post_processing.open_drt.purity_limit_high_r": "Purity Limit High R", + "feature.post_processing.open_drt.purity_limit_low": "Purity Limit Low", + "feature.post_processing.open_drt.purity_limit_low_b": "Purity Limit Low B", + "feature.post_processing.open_drt.purity_limit_low_g": "Purity Limit Low G", + "feature.post_processing.open_drt.purity_limit_low_r": "Purity Limit Low R", + "feature.post_processing.open_drt.purity_softclip_c": "Purity Softclip C", + "feature.post_processing.open_drt.purity_softclip_increases_tonality_and_smoothness_in_extremely": "Purity Softclip increases tonality and smoothness in extremely pure input values\nthat can not be adequately compressed into the display-referred gamut volume.\nThe algorithm is tuned for common camera observer colorimetry sources.", + "feature.post_processing.open_drt.purity_softclip_m": "Purity Softclip M", + "feature.post_processing.open_drt.purity_softclip_strength_for_cyan": "Purity Softclip strength for Cyan.", + "feature.post_processing.open_drt.purity_softclip_strength_for_magenta": "Purity Softclip strength for Magenta.", + "feature.post_processing.open_drt.purity_softclip_strength_for_yellow": "Purity Softclip strength for Yellow.", + "feature.post_processing.open_drt.purity_softclip_y": "Purity Softclip Y", + "feature.post_processing.open_drt.quadratic_toe_compression_strongly_compresses_deep_shadows_helpful": "Quadratic toe compression.\nStrongly compresses deep shadows. Helpful to have some amount to smooth the transition into display minimum.\nHigher values with a strong positive Offset also valid. Similar to common camera DRT tonescale strategies.", + "feature.post_processing.open_drt.range_of_the_blue_hueshift_higher_values_affect": "Range of the blue hueshift: higher values affect more of the lower intensity range.", + "feature.post_processing.open_drt.range_of_the_cyan_hueshift_higher_values_affect": "Range of the cyan hueshift: higher values affect more of the upper intensity range.", + "feature.post_processing.open_drt.range_of_the_green_hueshift_higher_values_affect": "Range of the green hueshift: higher values affect more of the lower intensity range.", + "feature.post_processing.open_drt.range_of_the_magenta_hueshift_higher_values_affect": "Range of the magenta hueshift: higher values affect more of the upper intensity range.", + "feature.post_processing.open_drt.range_of_the_red_hueshift_higher_values_affect": "Range of the red hueshift: higher values affect more of the lower intensity range.", + "feature.post_processing.open_drt.range_of_the_yellow_hueshift_higher_values_affect": "Range of the yellow hueshift: higher values affect more of the upper intensity range.", + "feature.post_processing.open_drt.render_space": "Render Space", + "feature.post_processing.open_drt.render_space_is_the_encoding_in_which_the": "Render space is the encoding in which the RGB Ratios are taken.\nStrength controls how much to desaturate from P3 gamut. Creatively, the more you desaturate, the more brilliance is increased in the resulting image.\nTo be used with caution as this affects every other aspect of the image rendering.", + "feature.post_processing.open_drt.render_space_strength": "Render Space Strength", + "feature.post_processing.open_drt.render_space_weight_b": "Render Space Weight B", + "feature.post_processing.open_drt.render_space_weight_r": "Render Space Weight R", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_blue_post_purity": "Scale intensity of high-purity blue, post purity compression.\nThis can help reduce rings or halos around high purity light sources.", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_blues": "Scale intensity of high-purity blues.", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_green_post_purity": "Scale intensity of high-purity green, post purity compression.\nThis can help reduce rings or halos around high purity light sources.", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_greens": "Scale intensity of high-purity greens.", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_red_post_purity": "Scale intensity of high-purity red, post purity compression.\nThis can help reduce rings or halos around high purity light sources.", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_reds": "Scale intensity of high-purity reds.", + "feature.post_processing.open_drt.set_the_creative_whitepoint_of_the_display_peak": "Set the creative whitepoint of the display peak luminance.\nWith D65 all channels are equal. With D50, the peak luminance value will match a D50 whitepoint.\nThis can be creatively desireable. This adjustment is applied post-tonescale.", + "feature.post_processing.open_drt.shoulder_clip": "Shoulder Clip", + "feature.post_processing.open_drt.surround": "Surround", + "feature.post_processing.open_drt.the_blue_weight_of_the_render_space_strength": "The Blue weight of the Render Space Strength.\nModify with caution as this affects every other part of the image rendering.", + "feature.post_processing.open_drt.the_mid_purity_module_adjusts_mid_range_purity": "The Mid Purity module adjusts mid-range purity of midtones and highlights.\nWithout this module enabled, it is likely that midtones will not appear colorful enough,\nand highlights will appear too colorful resulting in chaulky pasty looking images especially in yellows and cyans. ", + "feature.post_processing.open_drt.the_red_weight_of_the_render_space_strength": "The Red weight of the Render Space Strength.\nModify with caution as this affects every other part of the image rendering.", + "feature.post_processing.open_drt.the_strength_of_the_mid_purity_high_adjustment": "The strength of the Mid Purity High adjustment.\nHigher values affect more of the luminance range.", + "feature.post_processing.open_drt.the_strength_of_the_mid_purity_high_adjustment_2": "The strength of the Mid Purity High adjustment.\nHigher values affect more of the purity range.", + "feature.post_processing.open_drt.the_strength_of_the_mid_purity_low_adjustment": "The strength of the Mid Purity Low adjustment.\nHigher values affect more of the luminance range.", + "feature.post_processing.open_drt.the_strength_of_the_mid_purity_low_adjustment_2": "The strength of the Mid Purity Low adjustment.\nHigher values affect more of the purity range.", + "feature.post_processing.open_drt.the_width_of_the_adjustment_width_below_0": "The width of the adjustment.\nWidth below 0.5 will mostly affect values between 0 and middle grey.\nValues above 0.5 will increasingly start to increase highlight contrast, which could be desired or not depending on what you are trying to do.", + "feature.post_processing.open_drt.toe": "Toe", + "feature.post_processing.open_drt.tonescale": "Tonescale", + "feature.post_processing.open_drt.tonescale_preset": "Tonescale Preset", + "feature.post_processing.open_drt.unitless_control_for_the_scene_linear_value_at": "Unitless control for the scene-linear value at which the tonescale system crosses the peak display linear value (1.0) and clips.\nThis is not an exact constraint in order to keep the system simple, but corresponds to roughly 16 at Shoulder Clip = 0 and 1024 at Shoulder Clip = 1", + "feature.post_processing.physical_glare.adapt_speed": "Adapt Speed", + "feature.post_processing.physical_glare.aperture_blades": "Aperture Blades", + "feature.post_processing.physical_glare.aperture_f_number_e_g_f2_8_smaller": "Aperture f-number (e.g. F2.8). Smaller = larger aperture = wider diffraction spikes.\nPhysically: aperture radius = 1 / f-number.", + "feature.post_processing.physical_glare.aperture_mode": "Aperture Mode", + "feature.post_processing.physical_glare.aperture_rotation": "Aperture Rotation", + "feature.post_processing.physical_glare.blade_roughness": "Blade Roughness", + "feature.post_processing.physical_glare.chromatic_spread": "Chromatic Spread", + "feature.post_processing.physical_glare.debug": "Debug", + "feature.post_processing.physical_glare.description": "Physically based glare from aperture diffraction and ocular scattering. FFT convolution with a wavelength-dependent PSF, dual eye/lens modes, wavefront aberrations, and wide-gamut chromatic dispersion.", + "feature.post_processing.physical_glare.dust_count": "Dust Count", + "feature.post_processing.physical_glare.dust_particles_on_lens_element_surfaces_produces_scattered": "Dust particles on lens element surfaces.\nProduces scattered haze via Babinet's principle.", + "feature.post_processing.physical_glare.dust_size": "Dust Size", + "feature.post_processing.physical_glare.dynamic_range_compression_exponent_paper_table_3_9": "Dynamic range compression exponent (paper Table 3.9: 0.45).\nLower = wider/softer glare, higher = concentrated near light source.\nIncrease if glare looks too blurry/spreads too far.", + "feature.post_processing.physical_glare.enable_eyelashes": "Enable Eyelashes", + "feature.post_processing.physical_glare.eyelash_count": "Eyelash Count", + "feature.post_processing.physical_glare.eyelash_curvature": "Eyelash Curvature", + "feature.post_processing.physical_glare.eyelash_length": "Eyelash Length", + "feature.post_processing.physical_glare.eyelashes": "Eyelashes", + "feature.post_processing.physical_glare.f_stop": "F-Stop", + "feature.post_processing.physical_glare.fft_resolution": "FFT Resolution", + "feature.post_processing.physical_glare.fresnel_exponent": "Fresnel Exponent", + "feature.post_processing.physical_glare.fresnel_phase_at_aperture_edge_radians_paper_eq": "Fresnel phase at aperture edge (radians). Paper eq 2.12: e^(i*pi/(lambda*z) * r^2).\nHigher = more Fresnel rings. 0 = pure Fraunhofer (no rings).", + "feature.post_processing.physical_glare.grating_count": "Grating Count", + "feature.post_processing.physical_glare.grating_strength": "Grating Strength", + "feature.post_processing.physical_glare.how_fast_the_glare_adapts_to_brightness_changes": "How fast the glare adapts to brightness changes.", + "feature.post_processing.physical_glare.how_fast_the_tear_film_fluctuates_blink_refresh": "How fast the tear film fluctuates (blink refresh rate ~0.3Hz, breakup ~2-5Hz).", + "feature.post_processing.physical_glare.how_opaque_each_scratch_is_higher_more_visible": "How opaque each scratch is. Higher = more visible streaks.", + "feature.post_processing.physical_glare.intensity": "Intensity", + "feature.post_processing.physical_glare.kernel_scale": "Kernel Scale", + "feature.post_processing.physical_glare.length_of_eyelashes_relative_to_aperture_radius": "Length of eyelashes relative to aperture radius.", + "feature.post_processing.physical_glare.length_of_scratches_relative_to_aperture_size": "Length of scratches relative to aperture size.", + "feature.post_processing.physical_glare.lens_camera_lens_polygon_starburst_pupil_circular_human": "Lens: camera lens polygon starburst. Pupil: circular human eye aperture.", + "feature.post_processing.physical_glare.lens_fiber_radial_phase_grating_creates_many_thin": "Lens fiber radial phase grating.\nCreates many thin, sharp radial star spikes.\nHigher count = more spikes (typical human eye: 20-80). 0 = disabled.", + "feature.post_processing.physical_glare.lens_suture_lines_y_shaped_junctions_where_lens": "Lens suture lines: Y-shaped junctions where lens fiber cells meet.\n3 = young eye (anterior Y + posterior inverted Y = 6 spikes).\nMore branches = older/more complex lens. 0 = disabled.", + "feature.post_processing.physical_glare.linear_scratches_on_lens_element_surfaces_each_scratch": "Linear scratches on lens element surfaces.\nEach scratch produces a perpendicular streak in the glare.", + "feature.post_processing.physical_glare.micro_serrations_on_aperture_blade_edges_manufacturing_imperfections": "Micro-serrations on aperture blade edges (manufacturing imperfections).\nMakes star spikes slightly fuzzy/irregular. 0 = perfect edges.", + "feature.post_processing.physical_glare.multiplier_on_wavelength_dependent_uv_scaling_paper_section": "Multiplier on wavelength-dependent UV scaling (paper section 2.3: lambda/575nm).\n1.0 = physically correct. Higher = more rainbow spread. 0 = monochrome.", + "feature.post_processing.physical_glare.name": "Physical Glare", + "feature.post_processing.physical_glare.number_of_angular_harmonics_more_more_spikes_finer": "Number of angular harmonics. More = more spikes, finer detail.", + "feature.post_processing.physical_glare.number_of_aperture_blades_controls_starburst_pattern": "Number of aperture blades. Controls starburst pattern.", + "feature.post_processing.physical_glare.number_of_bumps_per_blade_edge_higher_finer": "Number of bumps per blade edge. Higher = finer serrations.", + "feature.post_processing.physical_glare.number_of_radial_lens_gratings_paper_section_2": "Number of radial lens gratings (paper section 2.4: Ritschel uses 200).\nProduces lenticular halo via edge diffraction.", + "feature.post_processing.physical_glare.number_of_scatter_particles_in_lens_vitreous_ritschel": "Number of scatter particles in lens/vitreous (Ritschel: 750).\nProduces ciliary corona needle pattern via Babinet's principle.", + "feature.post_processing.physical_glare.opacity_of_lens_gratings_higher_stronger_lenticular_halo": "Opacity of lens gratings. Higher = stronger lenticular halo.", + "feature.post_processing.physical_glare.opacity_of_scatter_particles_in_pupil_mode_paper": "Opacity of scatter particles in pupil mode (paper section 2.4).\n0 = transparent (no scatter), 1 = fully opaque.", + "feature.post_processing.physical_glare.opacity_of_suture_lines_higher_stronger_star_spikes": "Opacity of suture lines. Higher = stronger star spikes.", + "feature.post_processing.physical_glare.overall_glare_intensity": "Overall glare intensity.", + "feature.post_processing.physical_glare.padding_ratio": "Padding Ratio", + "feature.post_processing.physical_glare.particle_count": "Particle Count", + "feature.post_processing.physical_glare.particle_size": "Particle Size", + "feature.post_processing.physical_glare.per_channel_brightness_threshold_for_glare_extraction_in": "Per-channel brightness threshold for glare extraction in EV100 (0 EV100 = 0.125 linear luminance).", + "feature.post_processing.physical_glare.phase_shift_strength_per_fiber_higher_brighter_spikes": "Phase shift strength per fiber. Higher = brighter spikes.", + "feature.post_processing.physical_glare.pixel_width_of_each_scratch": "Pixel width of each scratch.", + "feature.post_processing.physical_glare.pixel_width_of_each_suture_line_thinner_sharper": "Pixel width of each suture line. Thinner = sharper spikes.", + "feature.post_processing.physical_glare.psf_noise_floor": "PSF Noise Floor", + "feature.post_processing.physical_glare.psf_shaping": "PSF Shaping", + "feature.post_processing.physical_glare.psf_sharpness": "PSF Sharpness", + "feature.post_processing.physical_glare.radius_of_each_dust_particle_in_pixels": "Radius of each dust particle in pixels.", + "feature.post_processing.physical_glare.radius_of_each_particle_in_pixels": "Radius of each particle in pixels.", + "feature.post_processing.physical_glare.random_variation_in_fiber_spacing_and_strength_0": "Random variation in fiber spacing and strength.\n0 = perfectly regular (even spikes).\n1 = maximally irregular (natural look).", + "feature.post_processing.physical_glare.resolution_of_the_fft_convolution_higher_sharper_starburst": "Resolution of the FFT convolution. Higher = sharper starburst but more expensive.", + "feature.post_processing.physical_glare.rotation_angle_of_the_aperture": "Rotation angle of the aperture.", + "feature.post_processing.physical_glare.roughness_frequency": "Roughness Frequency", + "feature.post_processing.physical_glare.scale_of_the_glare_kernel_size_on_screen": "Scale of the glare kernel size on screen.\n1.0 = default. Smaller = more concentrated glare.\nDoes not affect aperture physics.", + "feature.post_processing.physical_glare.scatter_strength": "Scatter Strength", + "feature.post_processing.physical_glare.scratch_count": "Scratch Count", + "feature.post_processing.physical_glare.scratch_length": "Scratch Length", + "feature.post_processing.physical_glare.scratch_opacity": "Scratch Opacity", + "feature.post_processing.physical_glare.scratch_width": "Scratch Width", + "feature.post_processing.physical_glare.seidel_spherical_aberration_r_4_wavefront_error_models": "Seidel spherical aberration (r^4 wavefront error).\nModels lens curvature: outer rays focus at a different point\nthan central rays, producing concentric ring structure in the\nPSF and softer glare edges. Physical range: 0-50.", + "feature.post_processing.physical_glare.simulate_eyelash_occlusion_for_streak_effects_paper_section": "Simulate eyelash occlusion for streak effects (paper section 3.1).", + "feature.post_processing.physical_glare.simulates_tear_film_irregularities_on_the_cornea_surface": "Simulates tear film irregularities on the cornea surface.\nProduces flickering, sharp, irregular star spikes.\n0 = disabled (static PSF).", + "feature.post_processing.physical_glare.spherical_aberration": "Spherical Aberration", + "feature.post_processing.physical_glare.starburst_irregularity": "Starburst Irregularity", + "feature.post_processing.physical_glare.starburst_spikes": "Starburst Spikes", + "feature.post_processing.physical_glare.starburst_strength": "Starburst Strength", + "feature.post_processing.physical_glare.streak_curvature_via_uv_bending_paper_fig_3": "Streak curvature via UV bending (paper fig 3.7: sin(x) vertical offset).", + "feature.post_processing.physical_glare.suture_branches": "Suture Branches", + "feature.post_processing.physical_glare.suture_strength": "Suture Strength", + "feature.post_processing.physical_glare.suture_width": "Suture Width", + "feature.post_processing.physical_glare.tear_film_complexity": "Tear Film Complexity", + "feature.post_processing.physical_glare.tear_film_speed": "Tear Film Speed", + "feature.post_processing.physical_glare.tear_film_strength": "Tear Film Strength", + "feature.post_processing.physical_glare.threshold": "Threshold", + "feature.post_processing.physical_glare.threshold_to_remove_low_level_fft_noise_from": "Threshold to remove low-level FFT noise from the PSF.\nPaper default: 0.001. Higher = cleaner glare wings.", + "feature.post_processing.physical_glare.total_number_of_eyelash_hairs_upper_lower": "Total number of eyelash hairs (upper + lower).", + "feature.post_processing.physical_glare.zero_padding_per_side_to_prevent_fft_wrap": "Zero-padding per side to prevent FFT wrap-around.\n0.25 = paper default (50%% effective resolution).\n0.1 = 80%% effective (recommended for high-res).\n0.0 = 100%% (maximum sharpness, may wrap at edges).\nLower = sharper glare on high-res screens.", + "feature.post_processing.radial_blur_values_detail": "Radial Blur Strength: %.3f\nRadial Blur Rampup: %.3f\nRadial Blur Start: %.3f\nRadial Blur Rampdown: %.3f\nRadial Blur Down Start: %.3f\nRadial Blur Center: (%.3f, %.3f)", + "feature.post_processing.recompile_shaders": "Recompile Shaders", + "feature.post_processing.recompile_shaders_for_this_sub_feature_only": "Recompile shaders for this sub-feature only.", + "feature.post_processing.saturation_brightness_contrast_values": "Saturation: %.3f\nBrightness: %.3f\nContrast: %.3f", + "feature.post_processing.save": "Save", + "feature.post_processing.select_a_preset": "Select a preset", + "feature.post_processing.selected_feature_is_not_valid": "Selected feature is not valid.", + "feature.post_processing.tint_values": "Tint Values:", + "feature.post_processing.tint_values_detail": "Tint Amount: %.3f\nTint Color: (%.3f, %.3f, %.3f)", + "feature.post_processing.tonemap_owned_by_effects11": "Tonemapping is currently handled by Effects 11. Post Processing effects that run before tonemapping still apply. To use Post Processing tonemapping instead, either disable Effects 11 or enable its \"UseOriginalPostProcessing\" setting.", + "feature.post_processing.vignette.anamorphic_squeeze": "Anamorphic Squeeze", + "feature.post_processing.vignette.description": "Simulates natural vignetting caused by angled rays hitting the film or sensor array.", + "feature.post_processing.vignette.focal_length": "Focal Length", + "feature.post_processing.vignette.how_flat_the_vignette_looks_simulating_anamorphic_lens": "How flat the vignette looks, simulating anamorphic lens.", + "feature.post_processing.vignette.name": "Vignette", + "feature.post_processing.vignette.power": "Power", + "feature.post_processing.vignette.the_focal_length_of_the_lens_relative_to": "The focal length of the lens, relative to image width.", + "feature.post_processing.vignette.the_natural_vignetting_of_a_camera_follows_the": "The natural vignetting of a camera follows the fourth law, where the vignette is proportional to the fourth power of the incident angle. The actual power in a camera is usually lower due to designed compensation.", "feature.render_doc.description": "In-application RenderDoc capture support and convenience UI.", "feature.render_doc.key_feature_1": "Attach comments to captures that appear in RenderDoc UI", "feature.render_doc.key_feature_2": "Open captures folder", diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json index 5e9bda1fde..edef7566b7 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -867,6 +867,8 @@ "feature.linear_lighting.emissive_color_gamma": "自发光颜色伽马", "feature.linear_lighting.emissive_color_multiplier": "自发光颜色倍率", "feature.linear_lighting.enable": "启用线性光照", + "feature.linear_lighting.enable_acescg": "启用 ACEScg 宽色域", + "feature.linear_lighting.enable_acescg_tooltip": "在 ACEScg 色彩空间中渲染,以获得更宽色域和更准确的光照。\n需要启用线性光照和后处理。\n着色时所有 sRGB 色域纹理和颜色都会转换为 ACEScg。", "feature.linear_lighting.fog_gamma": "雾伽马", "feature.linear_lighting.fog_transparency_gamma": "雾透明度伽马", "feature.linear_lighting.gamma_settings": "伽马设置", @@ -942,6 +944,569 @@ "feature.performance_overlay.key_feature_6": "颜色编码的性能指标,可自定义阈值", "feature.performance_overlay.key_feature_7": "可移动的叠加窗口,位置持久保存", "feature.performance_overlay.name": "性能叠加层", + "feature.post_processing.back_to_pipeline": "返回管线", + "feature.post_processing.base_amount": "基础量:%.3f", + "feature.post_processing.base_data": "基础数据:", + "feature.post_processing.border.border_color": "边框颜色", + "feature.post_processing.border.depth_threshold": "深度阈值", + "feature.post_processing.border.description": "添加彩色边框,可选使用深度阈值。", + "feature.post_processing.border.name": "边框", + "feature.post_processing.border.scale_top_down_left_right": "比例(上、下、左、右)", + "feature.post_processing.border.the_color_of_the_border": "边框颜色。", + "feature.post_processing.border.the_depth_threshold_for_the_border_effect": "边框效果的深度阈值。", + "feature.post_processing.border.the_scale_of_the_border_on_each_side": "屏幕每一侧的边框比例。", + "feature.post_processing.bypass": "旁路", + "feature.post_processing.camera.amount_of_noise_to_apply": "要应用的噪声量。", + "feature.post_processing.camera.ca_amount": "色差量", + "feature.post_processing.camera.chromatic_aberration_strength": "色差强度。", + "feature.post_processing.camera.crop": "裁剪", + "feature.post_processing.camera.description": "相机效果,包括鱼眼、色差和胶片颗粒。", + "feature.post_processing.camera.enable_fisheye_effect": "启用鱼眼效果。", + "feature.post_processing.camera.fisheye": "鱼眼", + "feature.post_processing.camera.fov": "视场角", + "feature.post_processing.camera.fov_in_degrees_set_to_in_game_fov": "以度为单位的视场角。\n\n设为游戏内 FOV。", + "feature.post_processing.camera.how_much_to_crop_into_the_image": "控制图像裁剪量。\n\n0 = 圆形,1 = 全画幅。", + "feature.post_processing.camera.name": "相机", + "feature.post_processing.camera.noise_amount": "噪声量", + "feature.post_processing.camera.noise_type": "噪声类型", + "feature.post_processing.camera.type_of_noise_to_apply": "要应用的噪声类型。", + "feature.post_processing.cinematic_values": "电影效果数值:", + "feature.post_processing.codbloom.a_greater_radius_makes_the_bloom_slightly_blurrier": "半径越大,泛光会略微更模糊。", + "feature.post_processing.codbloom.debug": "调试", + "feature.post_processing.codbloom.debug_mip_level": "调试 Mip 级别", + "feature.post_processing.codbloom.description": "《使命召唤:高级战争》中使用的泛光效果。需要 HDR 线性 RGB 输入。", + "feature.post_processing.codbloom.intensity": "强度", + "feature.post_processing.codbloom.mip_level": "Mip 级别", + "feature.post_processing.codbloom.mix": "混合", + "feature.post_processing.codbloom.name": "COD 泛光", + "feature.post_processing.codbloom.texbloom": "texBloom", + "feature.post_processing.codbloom.the_greater_the_level_the_blurrier_the_part": "级别越高,控制的部分越模糊。", + "feature.post_processing.codbloom.threshold": "阈值", + "feature.post_processing.codbloom.upsampling_radius": "上采样半径", + "feature.post_processing.color_grading.asc_cdl": "ASC CDL", + "feature.post_processing.color_grading.black_point": "黑点", + "feature.post_processing.color_grading.black_tightness_offset": "黑色紧缩偏移", + "feature.post_processing.color_grading.black_tightness_shape": "黑色紧缩形状", + "feature.post_processing.color_grading.brightness": "亮度", + "feature.post_processing.color_grading.cinematic_blend": "电影混合", + "feature.post_processing.color_grading.cinematic_blend_tooltip": "饱和度、亮度和对比度。", + "feature.post_processing.color_grading.clip_point": "裁剪点", + "feature.post_processing.color_grading.color_grading": "颜色分级", + "feature.post_processing.color_grading.color_space_transform": "色彩空间转换", + "feature.post_processing.color_grading.contrast": "对比度", + "feature.post_processing.color_grading.convert_linear_to_log_before_hdr_color_grading": "在 HDR 颜色分级前将线性空间转换为对数空间", + "feature.post_processing.color_grading.convert_log_to_linear_after_hdr_color_grading": "在 HDR 颜色分级后将对数空间转换为线性空间", + "feature.post_processing.color_grading.cross_over_point": "交叉点", + "feature.post_processing.color_grading.cross_over_point_tooltip": "趾部与肩部连接为单条曲线的位置。", + "feature.post_processing.color_grading.curve_preview": "曲线预览", + "feature.post_processing.color_grading.curve_preview_axis": "前:0 - %.1f(HDR 线性) | 后:0 - %.1f%s", + "feature.post_processing.color_grading.curve_preview_hdr_suffix": "(HDR)", + "feature.post_processing.color_grading.curve_preview_post_b": "后 B:%.3f", + "feature.post_processing.color_grading.curve_preview_post_g": "后 G:%.3f", + "feature.post_processing.color_grading.curve_preview_post_r": "后 R:%.3f", + "feature.post_processing.color_grading.curve_preview_pre": "前:%.3f", + "feature.post_processing.color_grading.curve_preview_requires_lut": "请启用 LUT 生成以查看曲线预览(取消勾选“跳过 LUT”)。", + "feature.post_processing.color_grading.cutoff": "截断", + "feature.post_processing.color_grading.description": "颜色分级操作和多种色调映射选项。", + "feature.post_processing.color_grading.enable_tonemapping": "启用色调映射", + "feature.post_processing.color_grading.exposure": "曝光", + "feature.post_processing.color_grading.exposure_temperature_tint": "曝光/色温/色调", + "feature.post_processing.color_grading.fade_blend": "淡化混合", + "feature.post_processing.color_grading.gain": "增益", + "feature.post_processing.color_grading.game_color_grading": "游戏颜色分级", + "feature.post_processing.color_grading.gamma": "伽马", + "feature.post_processing.color_grading.hdr_output_active": "HDR 输出已启用", + "feature.post_processing.color_grading.highlights_gain": "高光增益", + "feature.post_processing.color_grading.highlights_offset": "高光偏移", + "feature.post_processing.color_grading.highlights_start_end": "高光起始/结束", + "feature.post_processing.color_grading.hue_correction": "色相校正", + "feature.post_processing.color_grading.hue_shift": "色相偏移", + "feature.post_processing.color_grading.input_color_space": "输入色彩空间:%s(%s)", + "feature.post_processing.color_grading.input_color_space_auto_detected": "从线性光照 ACEScg 自动检测", + "feature.post_processing.color_grading.input_color_space_fixed": "固定", + "feature.post_processing.color_grading.input_gamma": "输入伽马", + "feature.post_processing.color_grading.input_mid_level": "输入中间层级", + "feature.post_processing.color_grading.lift": "提升", + "feature.post_processing.color_grading.lift_gamma_gain": "提升、伽马、增益", + "feature.post_processing.color_grading.linear_section_length": "线性区段长度", + "feature.post_processing.color_grading.linear_section_start": "线性区段起点", + "feature.post_processing.color_grading.log_type": "对数类型", + "feature.post_processing.color_grading.max_brightness": "最大亮度", + "feature.post_processing.color_grading.maximum_hdr_value": "最大 HDR 值", + "feature.post_processing.color_grading.midtones_gain": "中间调增益", + "feature.post_processing.color_grading.midtones_offset": "中间调偏移", + "feature.post_processing.color_grading.min_luminance": "最小亮度", + "feature.post_processing.color_grading.name": "颜色分级和色调映射", + "feature.post_processing.color_grading.offset": "偏移", + "feature.post_processing.color_grading.oklch_color_mixer": "OKLCH 颜色混合", + "feature.post_processing.color_grading.oklch_color_mixer_tooltip": "在感知均匀的 OKLCH 空间中,调整特定色相的亮度、鲜艳度和色相偏移。", + "feature.post_processing.color_grading.oklch_saturation": "OKLCH 饱和度", + "feature.post_processing.color_grading.output_color_space": "输出色彩空间:%s(根据 HDR 显示自动设置)", + "feature.post_processing.color_grading.output_color_space_tooltip": "输出会自动切换:SDR -> sRGB,HDR -> BT2020。", + "feature.post_processing.color_grading.output_gamma": "输出伽马", + "feature.post_processing.color_grading.output_mid_level": "输出中间层级", + "feature.post_processing.color_grading.output_saved_to": "输出将保存到:%s", + "feature.post_processing.color_grading.paper_white_nits_from_hdr_settings": "纸白亮度:%.0f nits(来自 HDR 设置)", + "feature.post_processing.color_grading.peak_brightness_nits_from_hdr_settings": "峰值亮度:%.0f nits(来自 HDR 设置)", + "feature.post_processing.color_grading.pivot": "支点", + "feature.post_processing.color_grading.post_tonemapping_settings": "色调映射后设置", + "feature.post_processing.color_grading.power": "幂", + "feature.post_processing.color_grading.pre_tonemapping_settings": "色调映射前设置", + "feature.post_processing.color_grading.reset": "重置", + "feature.post_processing.color_grading.rolloff_start": "滚降起点", + "feature.post_processing.color_grading.saturation": "饱和度", + "feature.post_processing.color_grading.saturation_boost": "饱和度增强", + "feature.post_processing.color_grading.save_lut_and_output_image": "保存 LUT 和输出图像", + "feature.post_processing.color_grading.sdr_output_hdr_display_not_enabled": "SDR 输出(未启用 HDR Display)", + "feature.post_processing.color_grading.shadows_gain": "阴影增益", + "feature.post_processing.color_grading.shadows_midtones_highlights": "阴影/中间调/高光", + "feature.post_processing.color_grading.shadows_offset": "阴影偏移", + "feature.post_processing.color_grading.shadows_start_end": "阴影起始/结束", + "feature.post_processing.color_grading.shoulder": "肩部", + "feature.post_processing.color_grading.shoulder_strength": "肩部强度", + "feature.post_processing.color_grading.shoulder_strength_tooltip": "肩部在直线曲线与纯渐近曲线之间的混合量。", + "feature.post_processing.color_grading.skip_ldr_color_grading": "跳过 LDR 颜色分级", + "feature.post_processing.color_grading.skip_ldr_color_grading_tooltip": "在色调映射后跳过颜色分级,包含提升、伽马和增益。启用 HDR 时会自动跳过。", + "feature.post_processing.color_grading.skip_lut_direct_color_grading": "跳过 LUT(直接颜色分级)", + "feature.post_processing.color_grading.skip_lut_direct_color_grading_tooltip": "不将颜色分级烘焙到 LUT 中,而是逐像素直接应用。更精确,但更慢。", + "feature.post_processing.color_grading.slope": "斜率", + "feature.post_processing.color_grading.temperature": "色温", + "feature.post_processing.color_grading.tint": "色调", + "feature.post_processing.color_grading.tint_blend": "色调混合", + "feature.post_processing.color_grading.toe_strength": "趾部强度", + "feature.post_processing.color_grading.toe_strength_tooltip": "趾部在直线曲线与纯渐近曲线之间的混合量。", + "feature.post_processing.color_grading.tonemapper": "色调映射器", + "feature.post_processing.color_grading.tonemapper.aces.description": "来自 RenoDX 的 ACES RRT+ODT 色调映射器实现。", + "feature.post_processing.color_grading.tonemapper.agx_minimal.description": "Troy Sobotka 的 AgX 简化版,使用六阶多项式近似。最初由 bwrensch 创建,后经 Troy Sobotka 改进。内部使用 AgX 输入转换。", + "feature.post_processing.color_grading.tonemapper.aldridge_filmic.description": "由 Graham Aldridge 制作的 Hejl 和 Burgess-Dawson 电影曲线变体。请参阅其关于“Approximating Film with Tonemapping”的博客文章。", + "feature.post_processing.color_grading.tonemapper.day_filmic.description": "Mike Day 的电影曲线,见其文档“An efficient and user-friendly tone mapping operator”。也称为“Insomniac 曲线”。", + "feature.post_processing.color_grading.tonemapper.frostbite.description": "基于 EA Frostbite 颜色分级和显示呈现工作的 RenoDX Frostbite HDR 显示映射实现。", + "feature.post_processing.color_grading.tonemapper.gt7.description": "为《Gran Turismo 7》设计的色调映射器。", + "feature.post_processing.color_grading.tonemapper.hejl_burgess_dawson_filmic.description": "由 Graham Aldridge 制作的 Hejl 和 Burgess-Dawson 电影曲线变体。请参阅其关于“Approximating Film with Tonemapping”的博客文章。", + "feature.post_processing.color_grading.tonemapper.hermite_spline.description": "Musa 制作的 Hermite 样条色调映射器,来自 RenoDX。", + "feature.post_processing.color_grading.tonemapper.kajiya.description": "Tomasz Stachowiak/Embark 为其实时光线追踪引擎 Kajiya 设计的色调映射器。", + "feature.post_processing.color_grading.tonemapper.lottes_filmic.description": "Timothy Lottes 的电影曲线,见其 GDC 演讲“Advanced Techniques and Optimization of HDR Color Pipelines”。也称为“AMD 曲线”。", + "feature.post_processing.color_grading.tonemapper.melon.description": "TripleMelon 设计的色调映射器,用于修复 ACES 中高强度颜色偏移的问题。", + "feature.post_processing.color_grading.tonemapper.neutwo.description": "Carlos Lopez 制作的 Neutwo 色调映射器,来自 RenoDX。", + "feature.post_processing.color_grading.tonemapper.psychov.description": "Carlos Lopez 制作的 PsychoV 17 色调映射器,来自 RenoDX。", + "feature.post_processing.color_grading.tonemapper.reinhard.description": "Reinhard 等人在 2002 年的《Photographic Tone Reproduction for Digital Images》中提出的映射。", + "feature.post_processing.color_grading.tonemapper.reinhard_extended.description": "Reinhard 等人在 2002 年的《Photographic Tone Reproduction for Digital Images》中提出的扩展映射。额外的用户参数指定映射为 1 的最小亮度,从而允许高亮度区域过曝。", + "feature.post_processing.color_grading.tonemapper.uchimura_filmic.description": "Hajime Uchimura 的电影曲线,见其 CEDEC 演讲“HDR Theory and Practice”。其特点是中间的线性区段,也称为“Gran Turismo 曲线”。", + "feature.post_processing.color_grading.tonemapping": "色调映射", + "feature.post_processing.color_grading.use_open_drt": "使用 OpenDRT", + "feature.post_processing.color_grading.vibrance": "鲜艳度", + "feature.post_processing.color_grading.white_clip": "白色裁剪", + "feature.post_processing.color_grading.white_point": "白点", + "feature.post_processing.color_grading.working_color_space": "工作色彩空间", + "feature.post_processing.composite.description": "将泛光、镜头光斑、物理眩光和自动曝光合成到主图像上。在颜色分级前应用曝光(SceneColor * Exposure + Bloom * Exposure)。当任一贡献功能启用时会自动启用。", + "feature.post_processing.composite.name": "合成", + "feature.post_processing.debug": "调试", + "feature.post_processing.depth_of_field_values": "景深数值:", + "feature.post_processing.depth_of_field_values_detail": "景深强度:%.3f\n景深距离:%.3f\n景深范围:%.3f\n景深标志:%d\n天空景深模糊半径:%d", + "feature.post_processing.description": "提供高级后处理图像效果,用于提升游戏画面质量。", + "feature.post_processing.disable_vanilla_tonemapping": "禁用原版色调映射", + "feature.post_processing.do_f.auto_focus": "自动对焦", + "feature.post_processing.do_f.blur_quality": "模糊质量", + "feature.post_processing.do_f.bokeh_busy_factor": "焦外繁杂度", + "feature.post_processing.do_f.console_selection": "控制台选中目标", + "feature.post_processing.do_f.debug": "调试", + "feature.post_processing.do_f.debug_distance": "调试距离:%f", + "feature.post_processing.do_f.debug_focus_plane": "调试焦平面:%f", + "feature.post_processing.do_f.description": "基于 Frans Bouma 的 CinematicDOF 的景深效果。", + "feature.post_processing.do_f.f_number": "F 值", + "feature.post_processing.do_f.far_plane_max_blur": "远平面最大模糊", + "feature.post_processing.do_f.focal_length": "焦距", + "feature.post_processing.do_f.focus_point": "对焦点", + "feature.post_processing.do_f.highlight_boost": "高光增强", + "feature.post_processing.do_f.highlight_custom_shape": "自定义高光形状", + "feature.post_processing.do_f.highlight_shape_rotation": "高光形状旋转", + "feature.post_processing.do_f.manual_focus": "手动对焦", + "feature.post_processing.do_f.name": "景深", + "feature.post_processing.do_f.near_far_plane_distance_compenation": "近/远平面距离补偿", + "feature.post_processing.do_f.near_plane_max_blur": "近平面最大模糊", + "feature.post_processing.do_f.petzval_strength": "匹兹伐强度", + "feature.post_processing.do_f.post_blur_smoothing": "后置模糊平滑", + "feature.post_processing.do_f.selected_reference": "选中引用:%08X", + "feature.post_processing.do_f.target_focus": "目标对焦", + "feature.post_processing.do_f.target_focus_focal_length": "目标对焦焦距", + "feature.post_processing.do_f.transition_speed": "过渡速度", + "feature.post_processing.do_f.view_resize": "视图缩放", + "feature.post_processing.edit_settings_for_this_feature": "编辑此子功能的设置。", + "feature.post_processing.enable_the_feature_to_see_its_settings": "启用该功能后才能查看设置。", + "feature.post_processing.enabled": "启用", + "feature.post_processing.game_imagespace_values": "游戏 ImageSpace 数值", + "feature.post_processing.hdr_values": "HDR 数值:", + "feature.post_processing.hdr_values_detail": "眼适应速度:%.3f\n泛光模糊半径:%.3f\n泛光阈值:%.3f\n泛光缩放:%.3f\n接收泛光阈值:%.3f\n白场:%.3f\n阳光缩放:%.3f\n天空缩放:%.3f\n眼适应强度:%.3f", + "feature.post_processing.histogram_auto_exposure.adaptation_range": "适应范围", + "feature.post_processing.histogram_auto_exposure.adaptation_speed": "适应速度", + "feature.post_processing.histogram_auto_exposure.adapted_luminance_ev": "适应后亮度:%.6g(%.2f EV100)", + "feature.post_processing.histogram_auto_exposure.applying_additional_exposure_adjustment_to_the_image": "对图像应用额外曝光调整。", + "feature.post_processing.histogram_auto_exposure.bin": "区间:%d", + "feature.post_processing.histogram_auto_exposure.bin_0_below_luminance_threshold": "区间 0:低于亮度阈值", + "feature.post_processing.histogram_auto_exposure.compensated_target_ev": "补偿后目标:%.6g(%.2f EV100)", + "feature.post_processing.histogram_auto_exposure.cyan_compensation_target": "青色:补偿目标", + "feature.post_processing.histogram_auto_exposure.description": "使用直方图计算平均屏幕亮度的自动曝光和眼适应方法。需要 HDR 线性 RGB 输入。", + "feature.post_processing.histogram_auto_exposure.ev": "EV100:%.2f", + "feature.post_processing.histogram_auto_exposure.exposure_compensation": "曝光补偿", + "feature.post_processing.histogram_auto_exposure.fade_in_ev": "淡入 EV100", + "feature.post_processing.histogram_auto_exposure.final_global_exposure_ev": "最终全局曝光:%.6g(%+.2f EV)", + "feature.post_processing.histogram_auto_exposure.focus_area": "对焦区域", + "feature.post_processing.histogram_auto_exposure.from_this_point_onward_the_purkinje_effect_remains": "从此点开始,浦肯野效应保持最大强度。", + "feature.post_processing.histogram_auto_exposure.green_adapted_ev": "绿色:适应后 EV100", + "feature.post_processing.histogram_auto_exposure.histogram": "直方图", + "feature.post_processing.histogram_auto_exposure.luminance": "亮度:%.6g", + "feature.post_processing.histogram_auto_exposure.luminance_histogram_ev": "亮度直方图(%.0f - %.0f EV100)", + "feature.post_processing.histogram_auto_exposure.max_effect_ev": "最大效果 EV100", + "feature.post_processing.histogram_auto_exposure.max_strength": "最大强度", + "feature.post_processing.histogram_auto_exposure.min_max_the_average_scene_luminance_will_be": "[最小, 最大] 自动曝光时,平均场景亮度会被限制在该范围内。例如提高最小值会降低对黑暗的适应,从而避免暗场景被过度提亮。", + "feature.post_processing.histogram_auto_exposure.name": "直方图自动曝光", + "feature.post_processing.histogram_auto_exposure.purkinje_effect": "浦肯野效应", + "feature.post_processing.histogram_auto_exposure.samples": "采样数:%u", + "feature.post_processing.histogram_auto_exposure.specifies_the_proportion_of_the_area_width_height": "指定自动曝光适应区域的比例 [宽度, 高度]。", + "feature.post_processing.histogram_auto_exposure.the_purkinje_effect_simulates_the_blue_shift_of": "浦肯野效应模拟人眼在低光照下的蓝移。\n如果不喜欢这个效果,可以将强度设为零。", + "feature.post_processing.histogram_auto_exposure.the_purkinje_effect_will_start_to_take_place": "当平均场景亮度低于此值时,浦肯野效应开始生效。", + "feature.post_processing.histogram_auto_exposure.yellow_adaptation_range": "黄色:适应范围", + "feature.post_processing.invalid_feature_selected_returning_to_list": "选中的功能无效,正在返回列表。", + "feature.post_processing.key_feature_1": "可自定义的后处理效果", + "feature.post_processing.key_feature_2": "支持适配不同视觉风格的预设", + "feature.post_processing.key_feature_3": "提升整体画质和沉浸感", + "feature.post_processing.key_feature_4": "包含泛光、景深、颜色分级等效果", + "feature.post_processing.lens_flare.aperture_blades": "光圈叶片数", + "feature.post_processing.lens_flare.aperture_rotation": "光圈旋转", + "feature.post_processing.lens_flare.blur_iterations": "模糊迭代次数", + "feature.post_processing.lens_flare.color": "颜色", + "feature.post_processing.lens_flare.color_tint": "颜色染色", + "feature.post_processing.lens_flare.custom_ghost_colors_scales": "自定义鬼影颜色和缩放", + "feature.post_processing.lens_flare.debug": "调试", + "feature.post_processing.lens_flare.description": "屏幕空间镜头光斑,包含鬼影和光晕。支持用于物理形状鬼影的 FFT 焦外卷积。", + "feature.post_processing.lens_flare.disable_blur": "禁用模糊", + "feature.post_processing.lens_flare.disable_ghosts": "禁用鬼影", + "feature.post_processing.lens_flare.disable_threshold": "禁用阈值", + "feature.post_processing.lens_flare.enabled": "启用", + "feature.post_processing.lens_flare.f_stop": "F 档", + "feature.post_processing.lens_flare.fft_resolution": "FFT 分辨率", + "feature.post_processing.lens_flare.ghost_chroma_shift": "鬼影色散偏移", + "feature.post_processing.lens_flare.ghost_mode": "鬼影模式", + "feature.post_processing.lens_flare.ghost_settings": "鬼影设置", + "feature.post_processing.lens_flare.ghost_strength": "鬼影强度", + "feature.post_processing.lens_flare.halo_chroma_shift": "光晕色散偏移", + "feature.post_processing.lens_flare.halo_compression": "光晕压缩", + "feature.post_processing.lens_flare.halo_radius": "光晕半径", + "feature.post_processing.lens_flare.halo_settings": "光晕设置", + "feature.post_processing.lens_flare.halo_strength": "光晕强度", + "feature.post_processing.lens_flare.halo_width": "光晕宽度", + "feature.post_processing.lens_flare.intensity": "强度", + "feature.post_processing.lens_flare.kernel_scale": "核缩放", + "feature.post_processing.lens_flare.kernel_scale_2": "核缩放", + "feature.post_processing.lens_flare.name": "镜头光斑", + "feature.post_processing.lens_flare.non_intrusive_ghosts": "非侵入式鬼影", + "feature.post_processing.lens_flare.reset_ghosts_to_default": "将鬼影重置为默认值", + "feature.post_processing.lens_flare.scale": "比例", + "feature.post_processing.lens_flare.threshold": "阈值", + "feature.post_processing.lens_flare.threshold_ev": "阈值(EV100)", + "feature.post_processing.lens_flare.threshold_range": "阈值范围", + "feature.post_processing.lens_flare.tint": "色调", + "feature.post_processing.lens_flare.view_resize": "视图缩放", + "feature.post_processing.load": "加载", + "feature.post_processing.local_exposure.boost_local_contrast": "增强局部对比度", + "feature.post_processing.local_exposure.coarse_scale_mip": "粗尺度(Mip)", + "feature.post_processing.local_exposure.debug": "调试", + "feature.post_processing.local_exposure.description": "局部曝光会根据局部邻域亮度提亮阴影并压缩高光。它在自动曝光前运行,并在合成通道中应用。", + "feature.post_processing.local_exposure.detail_scale_display_mip": "细节 比例 (显示 Mip)", + "feature.post_processing.local_exposure.exposure": "曝光", + "feature.post_processing.local_exposure.exposure_preference": "曝光偏好", + "feature.post_processing.local_exposure.finest_pyramid_level_reconstructed_before_full_resolution_upsampling": "全分辨率上采样前重建的最细金字塔层级。较低的值会保留更小的局部细节。", + "feature.post_processing.local_exposure.gives_high_contrast_local_details_more_influence_during": "在金字塔重建期间,让高对比度局部细节具有更高权重。", + "feature.post_processing.local_exposure.highlight_recovery": "高光恢复", + "feature.post_processing.local_exposure.how_selectively_each_area_chooses_its_best_exposure": "控制每个区域选择最佳曝光的选择性。较高的值会产生更强的局部适应,较低的值混合更柔和。", + "feature.post_processing.local_exposure.how_strongly_bright_areas_are_compressed_higher_values": "控制明亮区域被压缩的强度。较高的值会保留更多高光细节。", + "feature.post_processing.local_exposure.how_strongly_darker_areas_are_lifted_higher_values": "控制暗部区域被提亮的强度。较高的值会恢复更多阴影细节。", + "feature.post_processing.local_exposure.largest_image_scale_used_by_the_effect_higher": "该效果使用的最大图像尺度。较高的值会影响更宽的光照区域。", + "feature.post_processing.local_exposure.manual_brightness_normalization_used_when_histogram_auto_exposure": "禁用直方图自动曝光时使用的手动亮度归一化。较高的值会让场景表现得更亮。", + "feature.post_processing.local_exposure.name": "局部曝光", + "feature.post_processing.local_exposure.shadow_recovery": "阴影恢复", + "feature.post_processing.local_exposure.view_resize": "视图缩放", + "feature.post_processing.lut.1d_lut_n_x_1_sized_images": "1D LUT:N x 1 尺寸图像。", + "feature.post_processing.lut.3d_lut_3d_dds_only": "3D LUT:仅支持 3D dds。", + "feature.post_processing.lut.3d_lut_in_2d_format_n_r_x": "2D 格式的 3D LUT:N(R)x N(G)尺寸图像,沿蓝色轴横向堆叠。", + "feature.post_processing.lut.clear": "清除", + "feature.post_processing.lut.description": "查找表应用。", + "feature.post_processing.lut.input_max": "输入最大值", + "feature.post_processing.lut.input_min": "输入最小值", + "feature.post_processing.lut.load": "加载", + "feature.post_processing.lut.loaded_texture": "已加载纹理:%s", + "feature.post_processing.lut.loaded_texture_none": "已加载纹理:无", + "feature.post_processing.lut.lut_texture_path": "LUT 纹理路径", + "feature.post_processing.lut.map_luma": "映射亮度", + "feature.post_processing.lut.map_per_channel": "逐通道映射", + "feature.post_processing.lut.name": "LUT", + "feature.post_processing.lut.relative_path_starts_from_game_executable_directory_supports": "相对路径从游戏可执行文件目录开始。支持 dds/bmp/png 格式。", + "feature.post_processing.mod_amount": "Mod 量:%.3f", + "feature.post_processing.mod_data": "Mod 数据:", + "feature.post_processing.mod_dof_values_detail": "景深强度:%.3f\n景深距离:%.3f\n景深范围:%.3f\n景深模式:%d", + "feature.post_processing.mod_fade_values_detail": "淡出量:%.3f\n淡出颜色:(%.3f, %.3f, %.3f)\n模糊半径:%.3f\n重影视觉强度:%.3f\n", + "feature.post_processing.motion_blur.a": "(?)", + "feature.post_processing.motion_blur.description": "根据相机和物体运动创建电影感运动模糊。", + "feature.post_processing.motion_blur.name": "运动模糊", + "feature.post_processing.motion_blur.sample_count_is_doubled_internally_for_smoother_results": "内部会将采样数加倍,以获得更平滑的结果。\n更多采样 = 更高质量,但性能更慢。", + "feature.post_processing.motion_blur.samples": "采样数", + "feature.post_processing.motion_blur_strength": "运动模糊强度:%.3f", + "feature.post_processing.name": "后处理", + "feature.post_processing.open_drt.adjusts_contrast_or_slope_a_constrained_power_function": "调整对比度或斜率。\n在显示线性空间中应用受约束的幂函数。", + "feature.post_processing.open_drt.amount_adjust_highlights_positive_values_increase_highlight_exposure": "高光调整量。\n正值提高高光曝光,负值降低;0 无效果。", + "feature.post_processing.open_drt.amount_of_contrast_to_add_0_0_has": "要添加的对比度量。0.0 无效果。\n1.0 会在原点(0,0)处降低 1 档曝光。", + "feature.post_processing.open_drt.amount_of_stops_above_middle_grey_0_18": "从中灰(0.18)以上多少档开始调整。", + "feature.post_processing.open_drt.amount_of_stops_to_boost_grey_luminance_per": "峰值亮度每提高 1 档时,中灰亮度提升的档数。\n例如 HDR 中灰增强为 0.1 时,峰值亮度每提高 1 档,中灰会提升 0.1 档。", + "feature.post_processing.open_drt.amount_to_decrease_purity_of_upper_midtones_and": "在中等纯度区域中,降低上中间调和高光纯度的量。\n0.0 无效果;1.0 是保持平滑时可用的最大值。", + "feature.post_processing.open_drt.amount_to_distort_the_blue_hue_angle_towards": "随着强度增加,将蓝色色相角推向青色的量。", + "feature.post_processing.open_drt.amount_to_distort_the_cyan_hue_angle_towards": "随着强度降低,将青色色相角推向蓝色的量。", + "feature.post_processing.open_drt.amount_to_distort_the_green_hue_angle_towards": "随着强度增加,将绿色色相角推向黄色的量。", + "feature.post_processing.open_drt.amount_to_distort_the_magenta_hue_angle_towards": "随着强度降低,将洋红色相角推向蓝色的量。", + "feature.post_processing.open_drt.amount_to_distort_the_red_hue_angle_towards": "随着强度增加,将红色色相角推向黄色的量。", + "feature.post_processing.open_drt.amount_to_distort_the_yellow_hue_angle_towards": "随着强度降低,将黄色色相角推向红色的量。", + "feature.post_processing.open_drt.amount_to_increase_hue_contrast_at_the_red": "在红色色相角处增加色相对比的量。", + "feature.post_processing.open_drt.amount_to_increase_purity_of_midtones_and_shadows": "在中等纯度区域中,提高中间调和阴影纯度的量。\n0.0 无效果;1.0 是保持平滑时可用的最大值。", + "feature.post_processing.open_drt.apply_preset": "应用预设", + "feature.post_processing.open_drt.as_brilliance_range_is_increased_the_brilliance_adjustments": "亮彩范围越大,亮彩调整会影响图像数据中更多低强度值。", + "feature.post_processing.open_drt.as_brilliance_strength_is_increased_the_brilliance_adjustments": "亮彩强度越大,亮彩调整会影响图像数据中更多低纯度值。", + "feature.post_processing.open_drt.brilliance": "亮彩##Header", + "feature.post_processing.open_drt.brilliance_2": "亮彩", + "feature.post_processing.open_drt.brilliance_b": "亮彩 B", + "feature.post_processing.open_drt.brilliance_g": "亮彩 G", + "feature.post_processing.open_drt.brilliance_post": "后置亮彩", + "feature.post_processing.open_drt.brilliance_r": "亮彩 R", + "feature.post_processing.open_drt.brilliance_range": "亮彩范围", + "feature.post_processing.open_drt.brilliance_scales_the_intensity_of_more_pure_stimuli": "亮彩会缩放更高纯度刺激的强度。\n亮彩模块在进行纯度压缩的色调尺度之前应用。\n这意味着如果用此调整压暗红色,纯度压缩也会相应减少。\n这种行为自然且平滑。", + "feature.post_processing.open_drt.brilliance_strength": "亮彩强度", + "feature.post_processing.open_drt.built_in_presets": "内置预设", + "feature.post_processing.open_drt.clamp": "钳制", + "feature.post_processing.open_drt.clamp_the_final_image_into_the_final_range": "将最终图像钳制到显示设备支持的最终范围内。", + "feature.post_processing.open_drt.compresses_purity_as_intensity_increases_bare_minimum_functionality": "随着强度增加压缩纯度。\n这是形成画面的最低必要功能。", + "feature.post_processing.open_drt.contrast": "对比度", + "feature.post_processing.open_drt.contrast_high": "高光对比度", + "feature.post_processing.open_drt.contrast_high_allows_control_of_the_upper_section": "高光对比度用于控制色调尺度函数的上半段。\n默认关闭;当需要更强的高光对比度或更柔和的高光滚降时可能有用。", + "feature.post_processing.open_drt.contrast_high_pivot": "高光对比度枢轴", + "feature.post_processing.open_drt.contrast_high_strength": "高光对比度强度", + "feature.post_processing.open_drt.contrast_low": "暗部对比度", + "feature.post_processing.open_drt.contrast_low_adds_contrast_to_the_midtones_and": "暗部对比度会为中间调和阴影增加对比。\n中灰(0.18)在调整中保持不变。", + "feature.post_processing.open_drt.contrast_low_width": "暗部对比度宽度", + "feature.post_processing.open_drt.creative_white": "创意白点", + "feature.post_processing.open_drt.creative_white_limit": "创意白点限制", + "feature.post_processing.open_drt.display": "显示", + "feature.post_processing.open_drt.display_grey_luminance": "显示中灰亮度", + "feature.post_processing.open_drt.display_luminance_for_middle_grey_0_18_in": "中灰(0.18)的显示亮度,单位 nits。\n在显示设备可用亮度范围内设置中灰目标值。", + "feature.post_processing.open_drt.display_peak_luminance": "显示峰值亮度", + "feature.post_processing.open_drt.enable_brilliance": "启用亮彩", + "feature.post_processing.open_drt.enable_contrast_high": "启用高光对比度", + "feature.post_processing.open_drt.enable_contrast_low": "启用暗部对比度", + "feature.post_processing.open_drt.enable_hue_contrast": "启用色相对比", + "feature.post_processing.open_drt.enable_hueshift_cmy": "启用 CMY 色相偏移", + "feature.post_processing.open_drt.enable_hueshift_rgb": "启用 RGB 色相偏移", + "feature.post_processing.open_drt.enable_mid_purity": "启用中间纯度", + "feature.post_processing.open_drt.enable_post_brilliance": "启用后置亮彩", + "feature.post_processing.open_drt.enable_purity_compress_high": "启用高亮纯度压缩", + "feature.post_processing.open_drt.enable_purity_softclip": "启用纯度软裁切", + "feature.post_processing.open_drt.global_intensity_scale_of_high_purity_stimuli": "高纯度刺激的全局强度缩放。", + "feature.post_processing.open_drt.global_post_purity_compression_brilliance_adjustment": "纯度压缩后的全局亮彩调整。", + "feature.post_processing.open_drt.hdr_display_nits_paper_white_nits_peak_linear": "HDR Display:%.0f nits 纸白,%.0f nits 峰值(%.2f 线性)", + "feature.post_processing.open_drt.hdr_grey_boost": "HDR 中灰增强", + "feature.post_processing.open_drt.hdr_purity": "HDR 纯度", + "feature.post_processing.open_drt.how_much_to_affect_purity_compression_and_hue": "峰值亮度提高时,对纯度压缩和色相偏移行为产生多大影响。\n0.0 会让 SDR 和 HDR 保持相同的纯度压缩和色相偏移行为。\n1.0 会在峰值亮度提高时保留更多纯度(高纯度高强度光源可能出现渐变断裂),并减少高光中的色相偏移量。", + "feature.post_processing.open_drt.how_quickly_above_the_contrast_high_pivot_the": "超过高光对比度枢轴后,效果开始生效的速度。", + "feature.post_processing.open_drt.hue": "色相", + "feature.post_processing.open_drt.hue_contrast_compresses_hue_angle_towards_the_primary": "色相对比会在低端将色相角压向原色,并随着强度增加将色相角扩向次级色。\n压缩时也会提高纯度,扩展时则降低纯度。\n这提供了一个可创作控制的方式,用来模拟逐通道色调尺度产生的效果。\n在 OpenDRT 中只保留红色色相角控制,因为它最有用。", + "feature.post_processing.open_drt.hue_contrast_r": "红色色相对比", + "feature.post_processing.open_drt.hue_contrast_r_range": "红色色相对比范围", + "feature.post_processing.open_drt.hue_contrast_range_control_determines_where_over_the": "色相对比范围控制:决定色相对比在强度范围中的作用位置。\n较高的值会把交叉点放到更高的强度范围。", + "feature.post_processing.open_drt.hue_shift_cmy_adds_hue_distortion_to_the": "CMY 色相偏移会在强度降低时,为青色、洋红和黄色这些次级色相角添加色相扭曲。\n此模块提供少量次级色相角创作调整。", + "feature.post_processing.open_drt.hue_shift_rgb_adds_hue_distortion_to_the": "RGB 色相偏移会在强度增加时,为红、绿、蓝这些原色色相角添加色相扭曲。\n默认情况下,OpenDRT 会在 RGB/色度空间中以直线压缩纯度。\n这可能因 Abney 效应产生感知色相偏移,例如纯蓝色在去饱和时会感知上偏向紫色。\n为补偿这一点并作为创作工具,此模块允许控制红、绿、蓝色相角在纯度压缩时经过的路径。", + "feature.post_processing.open_drt.hueshift_b": "蓝色色相偏移", + "feature.post_processing.open_drt.hueshift_b_range": "蓝色色相偏移范围", + "feature.post_processing.open_drt.hueshift_c": "青色色相偏移", + "feature.post_processing.open_drt.hueshift_c_range": "青色色相偏移范围", + "feature.post_processing.open_drt.hueshift_g": "绿色色相偏移", + "feature.post_processing.open_drt.hueshift_g_range": "绿色色相偏移范围", + "feature.post_processing.open_drt.hueshift_m": "洋红色相偏移", + "feature.post_processing.open_drt.hueshift_m_range": "洋红色相偏移范围", + "feature.post_processing.open_drt.hueshift_r": "红色色相偏移", + "feature.post_processing.open_drt.hueshift_r_range": "红色色相偏移范围", + "feature.post_processing.open_drt.hueshift_y": "黄色色相偏移", + "feature.post_processing.open_drt.hueshift_y_range": "黄色色相偏移范围", + "feature.post_processing.open_drt.limit_the_intensity_range_affected_by_the_creative": "限制创意白点影响的强度范围。\n0.0 时会影响整个强度范围。随着限制降低,更多中间调和阴影会保持中性。\n例如想让中间调更中性、同时让高光更暖时,这可能具有创作用途。", + "feature.post_processing.open_drt.limit_the_strength_of_blue_purity_compression_as": "限制蓝色纯度随强度增加而压缩的强度。\n有助于在高强度纯蓝光源中保留一些颜色。\n谨慎使用,因为这可能导致渐变断裂。", + "feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity": "限制所有色相角在强度降低时的纯度压缩强度。\n较高的值会减少中间调和阴影中的纯度压缩。", + "feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_2": "限制红色在强度降低时的纯度压缩强度。\n较高的值会减少中间调和阴影中的纯度压缩。", + "feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_3": "限制绿色在强度降低时的纯度压缩强度。\n较高的值会减少中间调和阴影中的纯度压缩。", + "feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_4": "限制蓝色在强度降低时的纯度压缩强度。\n较高的值会减少中间调和阴影中的纯度压缩。", + "feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_5": "限制纯度随强度增加而压缩的强度。\n有助于在高强度高纯度光源中保留一些颜色。\n谨慎使用,因为这可能导致渐变断裂。", + "feature.post_processing.open_drt.limit_the_strength_of_red_purity_compression_as": "限制红色纯度随强度增加而压缩的强度。\n有助于在高强度火焰和纯红光源中保留一些颜色。\n谨慎使用,因为这可能导致渐变断裂。", + "feature.post_processing.open_drt.look_preset": "外观预设", + "feature.post_processing.open_drt.mid_purity_high": "中间纯度高段", + "feature.post_processing.open_drt.mid_purity_high_range": "中间纯度高段范围", + "feature.post_processing.open_drt.mid_purity_high_strength": "中间纯度高段强度", + "feature.post_processing.open_drt.mid_purity_low": "中间纯度低段", + "feature.post_processing.open_drt.mid_purity_low_range": "中间纯度低段范围", + "feature.post_processing.open_drt.mid_purity_low_strength": "中间纯度低段强度", + "feature.post_processing.open_drt.offset": "偏移", + "feature.post_processing.open_drt.opendrt_includes_a_simple_surround_compensation_model_dci": "OpenDRT 包含一个简单的观看环境补偿模型。\nDCI 影院预设使用暗环境,Rec.1886 使用昏暗环境,sRGB Display 使用明亮环境。\n此功能应能让不同观看环境之间获得更好的感知匹配。", + "feature.post_processing.open_drt.peak_display_luminance_in_nits_in_sdr_the": "显示峰值亮度,单位 nits。\n在 SDR 中最大值固定为 1.0;在 HDR 中会被 HDR Display 峰值亮度覆盖。", + "feature.post_processing.open_drt.post_brilliance_b": "后置亮彩 B", + "feature.post_processing.open_drt.post_brilliance_g": "后置亮彩 G", + "feature.post_processing.open_drt.post_brilliance_r": "后置亮彩 R", + "feature.post_processing.open_drt.post_brilliance_scales_the_intensity_of_more_pure": "后置亮彩会在纯度压缩和色相偏移应用后,缩放更高纯度刺激的强度。\n使用 OpenDRT 算法时,高强度高纯度值可能超出显示参考色域体积顶部,\n从而导致渐变不连续,尤其是在 RGB 原色上。\n此模块可帮助补偿这种情况。", + "feature.post_processing.open_drt.pre_tonescale_scene_linear_offset_if_0_0": "色调尺度前的场景线性偏移。\n如果为 0.0,场景线性 0.0 会通过色调尺度系统映射到显示线性 0.0。\n许多相机成像管线会应用负偏移,让阴影颗粒平均值落在 0.0。\n正偏移可用于补偿这一点并增加阴影细节,也可能具有审美用途。\n偏移不应为负数(看着你呢,ACES 1.x)。", + "feature.post_processing.open_drt.purity": "纯度", + "feature.post_processing.open_drt.purity_limit_high": "高亮纯度限制", + "feature.post_processing.open_drt.purity_limit_high_b": "蓝色高亮纯度限制", + "feature.post_processing.open_drt.purity_limit_high_r": "红色高亮纯度限制", + "feature.post_processing.open_drt.purity_limit_low": "低亮纯度限制", + "feature.post_processing.open_drt.purity_limit_low_b": "蓝色低亮纯度限制", + "feature.post_processing.open_drt.purity_limit_low_g": "绿色低亮纯度限制", + "feature.post_processing.open_drt.purity_limit_low_r": "红色低亮纯度限制", + "feature.post_processing.open_drt.purity_softclip_c": "青色纯度软裁切", + "feature.post_processing.open_drt.purity_softclip_increases_tonality_and_smoothness_in_extremely": "纯度软裁切会在极高纯度输入值中增加层次和平滑度,\n这些值无法充分压缩进显示参考色域体积。\n该算法针对常见相机观察者色度源进行了调校。", + "feature.post_processing.open_drt.purity_softclip_m": "洋红纯度软裁切", + "feature.post_processing.open_drt.purity_softclip_strength_for_cyan": "青色的纯度软裁切强度。", + "feature.post_processing.open_drt.purity_softclip_strength_for_magenta": "洋红的纯度软裁切强度。", + "feature.post_processing.open_drt.purity_softclip_strength_for_yellow": "黄色的纯度软裁切强度。", + "feature.post_processing.open_drt.purity_softclip_y": "黄色纯度软裁切", + "feature.post_processing.open_drt.quadratic_toe_compression_strongly_compresses_deep_shadows_helpful": "二次趾部压缩。\n会强烈压缩深阴影。适当使用可平滑过渡到显示最小值。\n较高的值配合较强正偏移也有效,类似常见相机 DRT 色调尺度策略。", + "feature.post_processing.open_drt.range_of_the_blue_hueshift_higher_values_affect": "蓝色色相偏移范围:较高的值会影响更多低强度范围。", + "feature.post_processing.open_drt.range_of_the_cyan_hueshift_higher_values_affect": "青色色相偏移范围:较高的值会影响更多高强度范围。", + "feature.post_processing.open_drt.range_of_the_green_hueshift_higher_values_affect": "绿色色相偏移范围:较高的值会影响更多低强度范围。", + "feature.post_processing.open_drt.range_of_the_magenta_hueshift_higher_values_affect": "洋红色相偏移范围:较高的值会影响更多高强度范围。", + "feature.post_processing.open_drt.range_of_the_red_hueshift_higher_values_affect": "红色色相偏移范围:较高的值会影响更多低强度范围。", + "feature.post_processing.open_drt.range_of_the_yellow_hueshift_higher_values_affect": "黄色色相偏移范围:较高的值会影响更多高强度范围。", + "feature.post_processing.open_drt.render_space": "渲染空间", + "feature.post_processing.open_drt.render_space_is_the_encoding_in_which_the": "渲染空间是计算 RGB 比率时使用的编码。\n强度控制从 P3 色域去饱和的程度。创作上,去饱和越多,结果图像中的亮彩越高。\n谨慎使用,因为它会影响图像渲染的其他所有方面。", + "feature.post_processing.open_drt.render_space_strength": "渲染空间强度", + "feature.post_processing.open_drt.render_space_weight_b": "渲染空间蓝色权重", + "feature.post_processing.open_drt.render_space_weight_r": "渲染空间红色权重", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_blue_post_purity": "在纯度压缩后缩放高纯度蓝色的强度。\n这有助于减少高纯度光源周围的环或光晕。", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_blues": "缩放高纯度蓝色的强度。", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_green_post_purity": "在纯度压缩后缩放高纯度绿色的强度。\n这有助于减少高纯度光源周围的环或光晕。", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_greens": "缩放高纯度绿色的强度。", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_red_post_purity": "在纯度压缩后缩放高纯度红色的强度。\n这有助于减少高纯度光源周围的环或光晕。", + "feature.post_processing.open_drt.scale_intensity_of_high_purity_reds": "缩放高纯度红色的强度。", + "feature.post_processing.open_drt.set_the_creative_whitepoint_of_the_display_peak": "设置显示峰值亮度的创意白点。\n使用 D65 时所有通道相等;使用 D50 时峰值亮度会匹配 D50 白点。\n这可用于创作目的。此调整在色调尺度之后应用。", + "feature.post_processing.open_drt.shoulder_clip": "肩部裁切", + "feature.post_processing.open_drt.surround": "观看环境", + "feature.post_processing.open_drt.the_blue_weight_of_the_render_space_strength": "渲染空间强度的蓝色权重。\n谨慎修改,因为这会影响图像渲染的其他所有部分。", + "feature.post_processing.open_drt.the_mid_purity_module_adjusts_mid_range_purity": "中间纯度模块会调整中间调和高光的中等范围纯度。\n不启用此模块时,中间调可能显得不够鲜艳,\n高光则可能过于鲜艳,导致图像发灰发糊,尤其是黄色和青色。", + "feature.post_processing.open_drt.the_red_weight_of_the_render_space_strength": "渲染空间强度的红色权重。\n谨慎修改,因为这会影响图像渲染的其他所有部分。", + "feature.post_processing.open_drt.the_strength_of_the_mid_purity_high_adjustment": "中间纯度高段调整的强度。\n较高的值会影响更多亮度范围。", + "feature.post_processing.open_drt.the_strength_of_the_mid_purity_high_adjustment_2": "中间纯度高段调整的强度。\n较高的值会影响更多纯度范围。", + "feature.post_processing.open_drt.the_strength_of_the_mid_purity_low_adjustment": "中间纯度低段调整的强度。\n较高的值会影响更多亮度范围。", + "feature.post_processing.open_drt.the_strength_of_the_mid_purity_low_adjustment_2": "中间纯度低段调整的强度。\n较高的值会影响更多纯度范围。", + "feature.post_processing.open_drt.the_width_of_the_adjustment_width_below_0": "调整宽度。\n宽度低于 0.5 时主要影响 0 到中灰之间的值。\n高于 0.5 时会逐渐开始增加高光对比,是否需要取决于你的目标。", + "feature.post_processing.open_drt.toe": "趾部", + "feature.post_processing.open_drt.tonescale": "色调尺度", + "feature.post_processing.open_drt.tonescale_preset": "色调尺度预设", + "feature.post_processing.open_drt.unitless_control_for_the_scene_linear_value_at": "无单位控制值,决定色调尺度系统在何处与显示峰值线性值(1.0)相交并裁切。\n为保持系统简单,这不是精确约束;大致对应肩部裁切为 0 时的 16,以及肩部裁切为 1 时的 1024。", + "feature.post_processing.physical_glare.adapt_speed": "适应速度", + "feature.post_processing.physical_glare.aperture_blades": "光圈叶片数", + "feature.post_processing.physical_glare.aperture_f_number_e_g_f2_8_smaller": "光圈 F 值(例如 F2.8)。越小表示光圈越大,衍射星芒越宽。\n物理上:光圈半径 = 1 / F 值。", + "feature.post_processing.physical_glare.aperture_mode": "光圈模式", + "feature.post_processing.physical_glare.aperture_rotation": "光圈旋转", + "feature.post_processing.physical_glare.blade_roughness": "叶片粗糙度", + "feature.post_processing.physical_glare.chromatic_spread": "色散扩展", + "feature.post_processing.physical_glare.debug": "调试", + "feature.post_processing.physical_glare.description": "基于物理的眩光,来自光圈衍射和眼部散射。使用带波长相关 PSF 的 FFT 卷积,支持眼睛/镜头双模式、波前像差和广色域色散。", + "feature.post_processing.physical_glare.dust_count": "灰尘数量", + "feature.post_processing.physical_glare.dust_particles_on_lens_element_surfaces_produces_scattered": "镜片表面的灰尘粒子。\n通过 Babinet 原理产生散射雾光。", + "feature.post_processing.physical_glare.dust_size": "灰尘大小", + "feature.post_processing.physical_glare.dynamic_range_compression_exponent_paper_table_3_9": "动态范围压缩指数(论文表 3.9:0.45)。\n较低 = 更宽、更柔和的眩光;较高 = 更集中在光源附近。\n如果眩光太糊或扩散太远,可提高此值。", + "feature.post_processing.physical_glare.enable_eyelashes": "启用睫毛", + "feature.post_processing.physical_glare.eyelash_count": "睫毛数量", + "feature.post_processing.physical_glare.eyelash_curvature": "睫毛曲率", + "feature.post_processing.physical_glare.eyelash_length": "睫毛长度", + "feature.post_processing.physical_glare.eyelashes": "睫毛", + "feature.post_processing.physical_glare.f_stop": "F 档", + "feature.post_processing.physical_glare.fft_resolution": "FFT 分辨率", + "feature.post_processing.physical_glare.fresnel_exponent": "菲涅尔指数", + "feature.post_processing.physical_glare.fresnel_phase_at_aperture_edge_radians_paper_eq": "光圈边缘处的菲涅尔相位(弧度)。论文公式 2.12:e^(i*pi/(lambda*z) * r^2)。\n较高 = 更多菲涅尔环;0 = 纯夫琅禾费(无环)。", + "feature.post_processing.physical_glare.grating_count": "光栅数量", + "feature.post_processing.physical_glare.grating_strength": "光栅强度", + "feature.post_processing.physical_glare.how_fast_the_glare_adapts_to_brightness_changes": "眩光适应亮度变化的速度。", + "feature.post_processing.physical_glare.how_fast_the_tear_film_fluctuates_blink_refresh": "泪膜波动速度(眨眼刷新率约 0.3Hz,破裂约 2-5Hz)。", + "feature.post_processing.physical_glare.how_opaque_each_scratch_is_higher_more_visible": "每条划痕的不透明度。较高的值会产生更明显的条纹。", + "feature.post_processing.physical_glare.intensity": "强度", + "feature.post_processing.physical_glare.kernel_scale": "核缩放", + "feature.post_processing.physical_glare.length_of_eyelashes_relative_to_aperture_radius": "睫毛长度,相对于光圈半径。", + "feature.post_processing.physical_glare.length_of_scratches_relative_to_aperture_size": "划痕长度,相对于光圈大小。", + "feature.post_processing.physical_glare.lens_camera_lens_polygon_starburst_pupil_circular_human": "Lens:相机镜头的多边形星芒。Pupil:人眼的圆形瞳孔光圈。", + "feature.post_processing.physical_glare.lens_fiber_radial_phase_grating_creates_many_thin": "晶状体纤维的径向相位光栅。\n产生大量细而锐利的径向星芒。\n数量越高,星芒越多(典型人眼:20-80);0 = 禁用。", + "feature.post_processing.physical_glare.lens_suture_lines_y_shaped_junctions_where_lens": "晶状体缝合线:晶状体纤维细胞交汇的 Y 形连接。\n3 = 年轻眼睛(前 Y + 后倒 Y = 6 条星芒)。\n分支越多表示更年长或更复杂的晶状体;0 = 禁用。", + "feature.post_processing.physical_glare.linear_scratches_on_lens_element_surfaces_each_scratch": "镜片表面的线性划痕。\n每条划痕都会在眩光中产生一条垂直方向的光条。", + "feature.post_processing.physical_glare.micro_serrations_on_aperture_blade_edges_manufacturing_imperfections": "光圈叶片边缘的微小锯齿(制造瑕疵)。\n会让星芒略微发散或不规则;0 = 完美边缘。", + "feature.post_processing.physical_glare.multiplier_on_wavelength_dependent_uv_scaling_paper_section": "基于波长的 UV 缩放倍率(论文 2.3 节:lambda/575nm)。\n1.0 = 物理正确;较高 = 彩虹扩散更明显;0 = 单色。", + "feature.post_processing.physical_glare.name": "物理眩光", + "feature.post_processing.physical_glare.number_of_angular_harmonics_more_more_spikes_finer": "角向谐波数量。更多 = 更多星芒和更细的细节。", + "feature.post_processing.physical_glare.number_of_aperture_blades_controls_starburst_pattern": "光圈叶片数量。控制星芒图案。", + "feature.post_processing.physical_glare.number_of_bumps_per_blade_edge_higher_finer": "每个叶片边缘的凸起数量。较高 = 更细的锯齿。", + "feature.post_processing.physical_glare.number_of_radial_lens_gratings_paper_section_2": "径向晶状体光栅数量(论文 2.4 节:Ritschel 使用 200)。\n通过边缘衍射产生晶状体光晕。", + "feature.post_processing.physical_glare.number_of_scatter_particles_in_lens_vitreous_ritschel": "晶状体/玻璃体中的散射粒子数量(Ritschel:750)。\n通过 Babinet 原理产生纤毛状日冕针状图案。", + "feature.post_processing.physical_glare.opacity_of_lens_gratings_higher_stronger_lenticular_halo": "晶状体光栅的不透明度。较高 = 更强的晶状体光晕。", + "feature.post_processing.physical_glare.opacity_of_scatter_particles_in_pupil_mode_paper": "瞳孔模式中散射粒子的不透明度(论文 2.4 节)。\n0 = 透明(无散射),1 = 完全不透明。", + "feature.post_processing.physical_glare.opacity_of_suture_lines_higher_stronger_star_spikes": "缝合线不透明度。较高 = 更强的星芒。", + "feature.post_processing.physical_glare.overall_glare_intensity": "整体眩光强度。", + "feature.post_processing.physical_glare.padding_ratio": "填充比例", + "feature.post_processing.physical_glare.particle_count": "粒子数量", + "feature.post_processing.physical_glare.particle_size": "粒子大小", + "feature.post_processing.physical_glare.per_channel_brightness_threshold_for_glare_extraction_in": "眩光提取的逐通道亮度阈值,单位 EV100(0 EV100 = 0.125 线性亮度)。", + "feature.post_processing.physical_glare.phase_shift_strength_per_fiber_higher_brighter_spikes": "每根纤维的相位偏移强度。较高 = 更亮的星芒。", + "feature.post_processing.physical_glare.pixel_width_of_each_scratch": "每条划痕的像素宽度。", + "feature.post_processing.physical_glare.pixel_width_of_each_suture_line_thinner_sharper": "每条缝合线的像素宽度。越细 = 星芒越锐利。", + "feature.post_processing.physical_glare.psf_noise_floor": "PSF 噪声底", + "feature.post_processing.physical_glare.psf_shaping": "PSF 形状调整", + "feature.post_processing.physical_glare.psf_sharpness": "PSF 锐度", + "feature.post_processing.physical_glare.radius_of_each_dust_particle_in_pixels": "每个灰尘粒子的半径,单位像素。", + "feature.post_processing.physical_glare.radius_of_each_particle_in_pixels": "每个粒子的半径,单位像素。", + "feature.post_processing.physical_glare.random_variation_in_fiber_spacing_and_strength_0": "纤维间距和强度的随机变化。\n0 = 完全规则(星芒均匀)。\n1 = 最大不规则(更自然)。", + "feature.post_processing.physical_glare.resolution_of_the_fft_convolution_higher_sharper_starburst": "FFT 卷积分辨率。较高 = 星芒更锐利,但开销更高。", + "feature.post_processing.physical_glare.rotation_angle_of_the_aperture": "光圈的旋转角度。", + "feature.post_processing.physical_glare.roughness_frequency": "粗糙度频率", + "feature.post_processing.physical_glare.scale_of_the_glare_kernel_size_on_screen": "屏幕上眩光核大小的缩放。\n1.0 = 默认;较小 = 眩光更集中。\n不影响光圈物理。", + "feature.post_processing.physical_glare.scatter_strength": "散射强度", + "feature.post_processing.physical_glare.scratch_count": "划痕数量", + "feature.post_processing.physical_glare.scratch_length": "划痕长度", + "feature.post_processing.physical_glare.scratch_opacity": "划痕不透明度", + "feature.post_processing.physical_glare.scratch_width": "划痕宽度", + "feature.post_processing.physical_glare.seidel_spherical_aberration_r_4_wavefront_error_models": "Seidel 球面像差(r^4 波前误差)。\n模拟镜头曲率:外侧光线与中心光线聚焦在不同位置,\n在 PSF 中形成同心环结构,并让眩光边缘更柔和。物理范围:0-50。", + "feature.post_processing.physical_glare.simulate_eyelash_occlusion_for_streak_effects_paper_section": "模拟睫毛遮挡产生的条纹效果(论文 3.1 节)。", + "feature.post_processing.physical_glare.simulates_tear_film_irregularities_on_the_cornea_surface": "模拟角膜表面的泪膜不规则。\n产生闪烁、锐利且不规则的星芒。\n0 = 禁用(静态 PSF)。", + "feature.post_processing.physical_glare.spherical_aberration": "球面像差", + "feature.post_processing.physical_glare.starburst_irregularity": "星芒不规则度", + "feature.post_processing.physical_glare.starburst_spikes": "星芒尖刺", + "feature.post_processing.physical_glare.starburst_strength": "星芒强度", + "feature.post_processing.physical_glare.streak_curvature_via_uv_bending_paper_fig_3": "通过 UV 弯曲产生条纹曲率(论文图 3.7:sin(x) 垂直偏移)。", + "feature.post_processing.physical_glare.suture_branches": "缝合线分支", + "feature.post_processing.physical_glare.suture_strength": "缝合线强度", + "feature.post_processing.physical_glare.suture_width": "缝合线宽度", + "feature.post_processing.physical_glare.tear_film_complexity": "泪膜复杂度", + "feature.post_processing.physical_glare.tear_film_speed": "泪膜速度", + "feature.post_processing.physical_glare.tear_film_strength": "泪膜强度", + "feature.post_processing.physical_glare.threshold": "阈值", + "feature.post_processing.physical_glare.threshold_to_remove_low_level_fft_noise_from": "从 PSF 中移除低级 FFT 噪声的阈值。\n论文默认值:0.001。较高 = 更干净的眩光边翼。", + "feature.post_processing.physical_glare.total_number_of_eyelash_hairs_upper_lower": "睫毛总数(上 + 下)。", + "feature.post_processing.physical_glare.zero_padding_per_side_to_prevent_fft_wrap": "每侧零填充,用于避免 FFT 环绕。\n0.25 = 论文默认值(50%% 有效分辨率)。\n0.1 = 80%% 有效分辨率(高分辨率推荐)。\n0.0 = 100%%(最大锐度,边缘可能环绕)。\n较低 = 高分辨率屏幕上的眩光更锐利。", + "feature.post_processing.radial_blur_values_detail": "径向模糊强度:%.3f\n径向模糊渐入:%.3f\n径向模糊起点:%.3f\n径向模糊渐出:%.3f\n径向模糊下降起点:%.3f\n径向模糊中心:(%.3f, %.3f)", + "feature.post_processing.recompile_shaders": "重新编译着色器", + "feature.post_processing.recompile_shaders_for_this_sub_feature_only": "仅重新编译此子功能的着色器。", + "feature.post_processing.saturation_brightness_contrast_values": "饱和度:%.3f\n亮度:%.3f\n对比度:%.3f", + "feature.post_processing.save": "保存", + "feature.post_processing.select_a_preset": "选择预设", + "feature.post_processing.selected_feature_is_not_valid": "选中的功能无效。", + "feature.post_processing.tint_values": "染色数值:", + "feature.post_processing.tint_values_detail": "染色量:%.3f\n染色颜色:(%.3f, %.3f, %.3f)", + "feature.post_processing.vignette.anamorphic_squeeze": "变形镜头压缩", + "feature.post_processing.vignette.description": "模拟光线以倾斜角度照射胶片或传感器阵列造成的自然暗角。", + "feature.post_processing.vignette.focal_length": "焦距", + "feature.post_processing.vignette.how_flat_the_vignette_looks_simulating_anamorphic_lens": "控制暗角看起来有多扁平,用于模拟变形镜头。", + "feature.post_processing.vignette.name": "暗角", + "feature.post_processing.vignette.power": "幂", + "feature.post_processing.vignette.the_focal_length_of_the_lens_relative_to": "镜头焦距,相对于图像宽度。", + "feature.post_processing.vignette.the_natural_vignetting_of_a_camera_follows_the": "相机的自然暗角遵循四次方规律,即暗角与入射角的四次方成正比。真实相机中的幂通常更低,因为镜头设计会进行补偿。", "feature.render_doc.description": "提供应用内的RenderDoc捕获支持与便捷UI。", "feature.render_doc.key_feature_1": "为捕获添加注释,可在RenderDoc UI中查看", "feature.render_doc.key_feature_2": "打开捕获文件夹", diff --git a/package/Shaders/Common/Color.hlsli b/package/Shaders/Common/Color.hlsli index 24a38ef4e7..00c5b95909 100644 --- a/package/Shaders/Common/Color.hlsli +++ b/package/Shaders/Common/Color.hlsli @@ -1,10 +1,14 @@ #ifndef __COLOR_DEPENDENCY_HLSL__ #define __COLOR_DEPENDENCY_HLSL__ +#include "Common/ColorSpaces.hlsli" #include "Common/Math.hlsli" #include "Common/SharedData.hlsli" -#define ENABLE_LL SharedData::linearLightingSettings.enableLinearLighting +#ifndef VSHADER +# define ENABLE_LL SharedData::linearLightingSettings.enableLinearLighting +# define ENABLE_ACEScg SharedData::linearLightingSettings.enableACEScg +#endif #if defined(PSHADER) && defined(LIGHTING) cbuffer LLPerGeometry : register(b8) @@ -46,6 +50,25 @@ namespace Color dot(v4, kBlueVec4) + dot(v2, kBlueVec2)); } +#if defined(PSHADER) || defined(CSHADER) || defined(COMPUTESHADER) + float RGBToLuminance(float3 color) + { + // AP1 (ACEScg) luminance coefficients from AP1_2_XYZ_MAT Y row + return ENABLE_ACEScg ? dot(color, float3(0.2722287168, 0.6740817658, 0.0536895174)) : dot(color, float3(0.2125, 0.7154, 0.0721)); + } + + float RGBToLuminanceAlternative(float3 color) + { + // For ACEScg, fall back to the accurate AP1 luminance + return ENABLE_ACEScg ? dot(color, float3(0.2722287168, 0.6740817658, 0.0536895174)) : dot(color, float3(0.3, 0.59, 0.11)); + } + + float RGBToLuminance2(float3 color) + { + // For ACEScg, fall back to the accurate AP1 luminance + return ENABLE_ACEScg ? dot(color, float3(0.2722287168, 0.6740817658, 0.0536895174)) : dot(color, float3(0.299, 0.587, 0.114)); + } +#else float RGBToLuminance(float3 color) { return dot(color, float3(0.2125, 0.7154, 0.0721)); @@ -60,6 +83,7 @@ namespace Color { return dot(color, float3(0.299, 0.587, 0.114)); } +#endif float3 RGBToYCoCg(float3 color) { @@ -185,6 +209,12 @@ namespace Color const static float PBRLightingCompensation = ENABLE_LL ? 1.0 : Math::PI; // Linear Lighting Functions + // Gamut transform: converts linear sRGB-gamut color to ACEScg when enabled + float3 GamutTransform(float3 linearColor) + { + return ENABLE_ACEScg ? sRGBToAP1(linearColor) : linearColor; + } + float3 LLGammaToLinear(float3 color) { return ENABLE_LL ? SkyrimGammaToLinear(color) : color; @@ -202,15 +232,18 @@ namespace Color color = pow(abs(color), SharedData::enbSettings.ColorPow); # endif # if defined(TRUE_PBR) - return ENABLE_LL ? color : LinearToSrgb(color); + // TRUE_PBR: input is already linear sRGB; gamut-convert only + return ENABLE_LL ? GamutTransform(color) : LinearToSrgb(color); # else - return ENABLE_LL ? pow(abs(color), SharedData::linearLightingSettings.colorGamma) * SharedData::linearLightingSettings.vanillaDiffuseColorMult : color; + // Vanilla: linearize then gamut-convert + return ENABLE_LL ? GamutTransform(pow(abs(color), SharedData::linearLightingSettings.colorGamma)) * SharedData::linearLightingSettings.vanillaDiffuseColorMult : color; # endif } float3 Light(float3 color, bool isLinear = false) { - color = (ENABLE_LL && !isLinear) ? pow(abs(color), SharedData::linearLightingSettings.lightGamma) : color; + color = (ENABLE_LL && !isLinear) ? GamutTransform(pow(abs(color), SharedData::linearLightingSettings.lightGamma)) : (ENABLE_LL && isLinear) ? GamutTransform(color) : + color; # if defined(TRUE_PBR) return color * PBRLightingCompensation; // Compensate for traditional Lambertian diffuse # else @@ -232,27 +265,27 @@ namespace Color # if defined(LIGHTING) float3 EmitColor(float3 color) { - return ENABLE_LL ? (pow(abs(color / max(emissiveMult, 1e-5)), SharedData::linearLightingSettings.emitColorGamma) * emissiveMult * SharedData::linearLightingSettings.emitColorMult) : color; + return ENABLE_LL ? GamutTransform(pow(abs(color / max(emissiveMult, 1e-5)), SharedData::linearLightingSettings.emitColorGamma)) * emissiveMult * SharedData::linearLightingSettings.emitColorMult : color; } # endif float3 Glowmap(float3 color) { # if defined(TRUE_PBR) - return ENABLE_LL ? color * SharedData::linearLightingSettings.glowmapMult : LinearToSrgb(color); + return ENABLE_LL ? GamutTransform(color) * SharedData::linearLightingSettings.glowmapMult : LinearToSrgb(color); # else - return ENABLE_LL ? pow(abs(color), SharedData::linearLightingSettings.glowmapGamma) * SharedData::linearLightingSettings.glowmapMult : color; + return ENABLE_LL ? GamutTransform(pow(abs(color), SharedData::linearLightingSettings.glowmapGamma)) * SharedData::linearLightingSettings.glowmapMult : color; # endif } float3 Ambient(float3 color) { - return ENABLE_LL ? pow(abs(color), SharedData::linearLightingSettings.ambientGamma) * SharedData::linearLightingSettings.ambientMult : color; + return ENABLE_LL ? GamutTransform(pow(abs(color), SharedData::linearLightingSettings.ambientGamma)) * SharedData::linearLightingSettings.ambientMult : color; } float3 Fog(float3 color) { - return ENABLE_LL ? pow(abs(color), SharedData::linearLightingSettings.fogGamma) : color; + return ENABLE_LL ? GamutTransform(pow(abs(color), SharedData::linearLightingSettings.fogGamma)) : color; } float FogAlpha(float alpha) @@ -262,7 +295,7 @@ namespace Color float3 Effect(float3 color) { - return ENABLE_LL ? pow(abs(color), SharedData::linearLightingSettings.effectGamma) : color; + return ENABLE_LL ? GamutTransform(pow(abs(color), SharedData::linearLightingSettings.effectGamma)) : color; } float3 EffectMult(float3 color) @@ -295,20 +328,25 @@ namespace Color float3 Sky(float3 color) { - return ENABLE_LL ? pow(abs(color), SharedData::linearLightingSettings.skyGamma) : color; + return ENABLE_LL ? GamutTransform(pow(abs(color), SharedData::linearLightingSettings.skyGamma)) : color; } float3 Water(float3 color) { - return ENABLE_LL ? pow(abs(color), SharedData::linearLightingSettings.waterGamma) : color; + return ENABLE_LL ? GamutTransform(pow(abs(color), SharedData::linearLightingSettings.waterGamma)) : color; } float3 VolumetricLighting(float3 color) { - return ENABLE_LL ? pow(abs(color), SharedData::linearLightingSettings.vlGamma) : color; + return ENABLE_LL ? GamutTransform(pow(abs(color), SharedData::linearLightingSettings.vlGamma)) : color; } float3 ColorToLinear(float3 color) + { + return ENABLE_LL ? GamutTransform(pow(abs(color), SharedData::linearLightingSettings.colorGamma)) : color; + } + + float ColorToLinear(float color) { return ENABLE_LL ? pow(abs(color), SharedData::linearLightingSettings.colorGamma) : color; } diff --git a/package/Shaders/Common/ColorSpaces.hlsli b/package/Shaders/Common/ColorSpaces.hlsli new file mode 100644 index 0000000000..ab5c068e00 --- /dev/null +++ b/package/Shaders/Common/ColorSpaces.hlsli @@ -0,0 +1,276 @@ +#ifndef COLORSPACES_HLSLI +#define COLORSPACES_HLSLI + +static const float3x3 AP0_2_XYZ_MAT = { + 0.9525523959, + 0.0000000000, + 0.0000936786, + 0.3439664498, + 0.7281660966, + -0.0721325464, + 0.0000000000, + 0.0000000000, + 1.0088251844, +}; + +static const float3x3 XYZ_2_AP0_MAT = { + 1.0498110175, + 0.0000000000, + -0.0000974845, + -0.4959030231, + 1.3733130458, + 0.0982400361, + 0.0000000000, + 0.0000000000, + 0.9912520182, +}; + +static const float3x3 AP1_2_XYZ_MAT = { + 0.6624541811, + 0.1340042065, + 0.1561876870, + 0.2722287168, + 0.6740817658, + 0.0536895174, + -0.0055746495, + 0.0040607335, + 1.0103391003, +}; + +static const float3x3 XYZ_2_AP1_MAT = { + 1.6410233797, + -0.3248032942, + -0.2364246952, + -0.6636628587, + 1.6153315917, + 0.0167563477, + 0.0117218943, + -0.0082844420, + 0.9883948585, +}; + +static const float3x3 AP0_2_AP1_MAT = //mul( AP0_2_XYZ_MAT, XYZ_2_AP1_MAT ); + { + 1.4514393161, + -0.2365107469, + -0.2149285693, + -0.0765537734, + 1.1762296998, + -0.0996759264, + 0.0083161484, + -0.0060324498, + 0.9977163014, + }; + +static const float3x3 AP1_2_AP0_MAT = //mul( AP1_2_XYZ_MAT, XYZ_2_AP0_MAT ); + { + 0.6954522414, + 0.1406786965, + 0.1638690622, + 0.0447945634, + 0.8596711185, + 0.0955343182, + -0.0055258826, + 0.0040252103, + 1.0015006723, + }; + +static const float3 AP1_RGB2Y = { + 0.2722287168, //AP1_2_XYZ_MAT[0][1], + 0.6740817658, //AP1_2_XYZ_MAT[1][1], + 0.0536895174, //AP1_2_XYZ_MAT[2][1] +}; + +// REC 709 primaries +static const float3x3 XYZ_2_sRGB_MAT = { + 3.2409699419, + -1.5373831776, + -0.4986107603, + -0.9692436363, + 1.8759675015, + 0.0415550574, + 0.0556300797, + -0.2039769589, + 1.0569715142, +}; + +static const float3x3 sRGB_2_XYZ_MAT = { + 0.4123907993, + 0.3575843394, + 0.1804807884, + 0.2126390059, + 0.7151686788, + 0.0721923154, + 0.0193308187, + 0.1191947798, + 0.9505321522, +}; + +// REC 2020 primaries +static const float3x3 XYZ_2_Rec2020_MAT = { + 1.7166511880, + -0.3556707838, + -0.2533662814, + -0.6666843518, + 1.6164812366, + 0.0157685458, + 0.0176398574, + -0.0427706133, + 0.9421031212, +}; + +static const float3x3 Rec2020_2_XYZ_MAT = { + 0.6369580483, + 0.1446169036, + 0.1688809752, + 0.2627002120, + 0.6779980715, + 0.0593017165, + 0.0000000000, + 0.0280726930, + 1.0609850577, +}; + +// P3, D65 primaries +static const float3x3 XYZ_2_P3D65_MAT = { + 2.4934969119, + -0.9313836179, + -0.4027107845, + -0.8294889696, + 1.7626640603, + 0.0236246858, + 0.0358458302, + -0.0761723893, + 0.9568845240, +}; + +static const float3x3 P3D65_2_XYZ_MAT = { + 0.4865709486, + 0.2656676932, + 0.1982172852, + 0.2289745641, + 0.6917385218, + 0.0792869141, + 0.0000000000, + 0.0451133819, + 1.0439443689, +}; + +// Bradford chromatic adaptation transforms between ACES white point (D60) and sRGB white point (D65) +static const float3x3 D65_2_D60_CAT = { + 1.0130349146, + 0.0061052578, + -0.0149709436, + 0.0076982301, + 0.9981633521, + -0.0050320385, + -0.0028413174, + 0.0046851567, + 0.9245061375, +}; + +static const float3x3 D60_2_D65_CAT = { + 0.9872240087, + -0.0061132286, + 0.0159532883, + -0.0075983718, + 1.0018614847, + 0.0053300358, + 0.0030725771, + -0.0050959615, + 1.0816806031, +}; + +static const float3x3 CAM16_2_XYZ_MAT = { + 2.0512756811, -1.1400313439, 0.0887556628, + 0.4269389763, 0.7005835277, -0.1275225040, + -0.0174712779, -0.0384725929, 1.0589468739 +}; + +static const float3x3 XYZ_2_CAM16_MAT = { + 0.3640744835, 0.5947008156, 0.04110127349, + -0.2222450987, 1.0738554823, 0.14794533610, + -0.0020676190, 0.0488260453, 0.95038755696 +}; + +// Transformations between CIE XYZ tristimulus values and CIE x,y +// chromaticity coordinates +float3 XYZToxyY(float3 XYZ) +{ + float3 xyY; + float divisor = (XYZ[0] + XYZ[1] + XYZ[2]); + if (divisor == 0.) + divisor = 1e-10; + xyY[0] = XYZ[0] / divisor; + xyY[1] = XYZ[1] / divisor; + xyY[2] = XYZ[1]; + + return xyY; +} + +float3 xyYToXYZ(float3 xyY) +{ + float3 XYZ; + XYZ[0] = xyY[0] * xyY[2] / max(xyY[1], 1e-10); + XYZ[1] = xyY[2]; + XYZ[2] = (1.0 - xyY[0] - xyY[1]) * xyY[2] / max(xyY[1], 1e-10); + + return XYZ; +} + +float3x3 ChromaticAdaptation(float2 src_xy, float2 dst_xy) +{ + // Von Kries chromatic adaptation + + // Bradford + const float3x3 ConeResponse = { + 0.8951, + 0.2664, + -0.1614, + -0.7502, + 1.7135, + 0.0367, + 0.0389, + -0.0685, + 1.0296, + }; + const float3x3 InvConeResponse = { + 0.9869929, + -0.1470543, + 0.1599627, + 0.4323053, + 0.5183603, + 0.0492912, + -0.0085287, + 0.0400428, + 0.9684867, + }; + + float3 src_XYZ = xyYToXYZ(float3(src_xy, 1)); + float3 dst_XYZ = xyYToXYZ(float3(dst_xy, 1)); + + float3 src_coneResp = mul(ConeResponse, src_XYZ); + float3 dst_coneResp = mul(ConeResponse, dst_XYZ); + + float3x3 VonKriesMat = { + { dst_coneResp[0] / src_coneResp[0], 0.0, 0.0 }, + { 0.0, dst_coneResp[1] / src_coneResp[1], 0.0 }, + { 0.0, 0.0, dst_coneResp[2] / src_coneResp[2] } + }; + + return mul(InvConeResponse, mul(VonKriesMat, ConeResponse)); +} + +float3 sRGBToAP1(float3 sRGB) +{ + float3 XYZ = mul(sRGB_2_XYZ_MAT, sRGB); + return mul(XYZ_2_AP1_MAT, XYZ); +} + +float3 AP1TosRGB(float3 AP1) +{ + float3 XYZ = mul(AP1_2_XYZ_MAT, AP1); + return mul(XYZ_2_sRGB_MAT, XYZ); +} + +#endif \ No newline at end of file diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index 726f4a786c..b7dfcb22df 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -216,6 +216,7 @@ namespace SharedData struct LinearLightingSettings { uint enableLinearLighting; + uint enableACEScg; uint isDirLightLinear; float dirLightMult; float lightGamma; @@ -242,7 +243,6 @@ namespace SharedData float projectedEffectMult; float deferredEffectMult; float otherEffectMult; - uint pad0; }; struct ENBSettings @@ -347,6 +347,12 @@ namespace SharedData float4 wetParams; }; + struct PostProcessingSettings + { + uint DisableVanillaTonemapping; + uint3 pad0; + }; + cbuffer FeatureData : register(b6) { GrassLightingSettings grassLightingSettings; @@ -368,6 +374,7 @@ namespace SharedData ExponentialHeightFogSettings exponentialHeightFogSettings; TruePBRSettings truePBRSettings; SkinData skinData; + PostProcessingSettings postProcessingSettings; }; Texture2D DepthTexture : register(t17); diff --git a/package/Shaders/ISHDR.hlsl b/package/Shaders/ISHDR.hlsl index d76ff9357c..78d601f85d 100644 --- a/package/Shaders/ISHDR.hlsl +++ b/package/Shaders/ISHDR.hlsl @@ -134,6 +134,25 @@ PS_OUTPUT main(PS_INPUT input) float3 inputColor = BlendTex.Sample(BlendSampler, uv).xyz; + float4 hdrShared = SharedData::HDRData; + bool isHDR = hdrShared.x > 0.5; + float menuSceneEncoding = hdrShared.w; + static const float MENU_SCENE_ISHDR_BYPASS_THRESHOLD = 0.9; // encoding 1.0 == main/loading + if (menuSceneEncoding > MENU_SCENE_ISHDR_BYPASS_THRESHOLD) + isHDR = false; + +# if defined(POSTPROCESS) + if (SharedData::postProcessingSettings.DisableVanillaTonemapping) { + if (SharedData::linearLightingSettings.enableLinearLighting && !isHDR) { + inputColor = Color::LinearToSrgb(inputColor); + } + + psout.Color = float4(inputColor, 1.0); + + return psout; + } +# endif + float3 bloomColor = 0; if (Flags.x > 0.5) { bloomColor = ImageTex.Sample(ImageSampler, uv).xyz; @@ -143,13 +162,6 @@ PS_OUTPUT main(PS_INPUT input) float2 avgValue = AvgTex.Sample(AvgSampler, input.TexCoord.xy).xy; - float4 hdrShared = SharedData::HDRData; - bool isHDR = hdrShared.x > 0.5; - float menuSceneEncoding = hdrShared.w; - static const float MENU_SCENE_ISHDR_BYPASS_THRESHOLD = 0.9; // encoding 1.0 == main/loading - if (menuSceneEncoding > MENU_SCENE_ISHDR_BYPASS_THRESHOLD) - isHDR = false; - float3 outputColor = 0.0; if (avgValue.x != 0 && avgValue.y != 0) diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 781e132fc0..514ab8eb87 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -2180,7 +2180,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if (SharedData::lodBlendingSettings.DisableTerrainVertexColors) pbrVertexColorSrc = 1; # endif - float3 pbrVertexColor = Color::SrgbToLinear(pbrVertexColorSrc); + float3 pbrVertexColor = Color::GamutTransform(Color::SrgbToLinear(pbrVertexColorSrc)); float pbrVertexAO = max(max(pbrVertexColor.x, pbrVertexColor.y), pbrVertexColor.z); pbrVertexColor = pbrVertexAO == 0.0f ? 1.0f : pbrVertexColor * lerp(1 / max(pbrVertexAO, 0.001), 1, SharedData::truePBRSettings.VertexAOStrength); @@ -2391,7 +2391,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(CREATOR) if (SharedData::cubemapCreatorSettings.Enabled) { - material.F0 = SharedData::cubemapCreatorSettings.CubemapColor.rgb; + material.F0 = Color::GamutTransform(Color::SrgbToLinear(SharedData::cubemapCreatorSettings.CubemapColor.rgb)); material.Roughness = SharedData::cubemapCreatorSettings.CubemapColor.a; } # endif @@ -2423,15 +2423,18 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(DEFERRED) sh2 skylightingSH = Skylighting::Sample(positionMSSkylight, worldNormal # if defined(SKYLIGHTING_SHADOW_VIS) - , skylightingShadowVisibility + , + skylightingShadowVisibility # endif ); # else sh2 skylightingSH = inWorld ? Skylighting::Sample(positionMSSkylight, worldNormal # if defined(SKYLIGHTING_SHADOW_VIS) - , skylightingShadowVisibility + , + skylightingShadowVisibility # endif - ) : Skylighting::UNIT_SH; + ) : + Skylighting::UNIT_SH; # endif # endif @@ -2585,7 +2588,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if !defined(SKYLIGHTING_SHADOW_VIS) dirSoftShadow = # endif - ShadowSampling::GetLightingShadow(input.WorldPosition.xyz, dirVSMDetailedShadow); + ShadowSampling::GetLightingShadow(input.WorldPosition.xyz, dirVSMDetailedShadow); # endif float dirDetailedShadow = 1.0; @@ -3012,7 +3015,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(TRUE_PBR) float3 vertexColor = 1; # else - float3 vertexColor = input.Color.xyz; + float3 vertexColor = Color::ColorToLinear(input.Color.xyz); # endif # if defined(FACEGEN) || defined(FACEGEN_RGB_TINT) || defined(EYE) float vertexAO = 1; diff --git a/package/Shaders/Particle.hlsl b/package/Shaders/Particle.hlsl index d4de5bb0df..2009447135 100644 --- a/package/Shaders/Particle.hlsl +++ b/package/Shaders/Particle.hlsl @@ -91,7 +91,7 @@ VS_OUTPUT main(VS_INPUT input) float4 viewPosition = mul(WorldViewProj, msPosition); # if defined(RAIN) float3 rainVelocity = Velocity.xyz; -# if defined(EFFECTS11) +# if defined(EFFECTS11) if (SharedData::enbSettings.EnableRain) { float velLen = length(rainVelocity); if (velLen > 0) { @@ -99,7 +99,7 @@ VS_OUTPUT main(VS_INPUT input) rainVelocity = lerp(normVel, rainVelocity, SharedData::enbSettings.RainMotionStretch); } } -# endif +# endif float4 adjustedMsPosition = msPosition - float4(rainVelocity, 0); float positionBlendParam = 0.5 * (1 + input.TexCoord1.y); float4 adjustedViewPosition = mul(WorldViewProj, adjustedMsPosition); @@ -191,9 +191,9 @@ VS_OUTPUT main(VS_INPUT input) vsout.Color.xyz = color.xyz; # endif -# if defined(ENVCUBE) && defined(RAIN) && defined(EFFECTS11) +# if defined(ENVCUBE) && defined(RAIN) && defined(EFFECTS11) vsout.RaindropData.xy = input.TexCoord1.xy * 0.5 + 0.5; -# endif +# endif return vsout; } @@ -271,48 +271,48 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif # if defined(ENVCUBE) && defined(RAIN) && defined(DYNAMIC_CUBEMAPS) && defined(EFFECTS11) -if (SharedData::enbSettings.EnableRain) { - float4 raindropNormal = TexRaindropNormals.Sample(SampSourceTexture, input.RaindropData.xy); - float alpha = saturate(raindropNormal.w * (1.0 - SharedData::enbSettings.RainMotionTransparency)); - clip(alpha - (4.0 / 255.0)); - raindropNormal.y = 1.0 - raindropNormal.y; - - // Reconstruct camera-relative worldspace position (camera at origin). - float2 uv = input.Position.xy * SharedData::BufferDim.zw; - float4 posCS = float4(2.0 * float2(uv.x, 1.0 - uv.y) - 1.0, input.Position.z, 1.0); - float4 posWS = mul(FrameBuffer::CameraViewProjInverse, posCS); - posWS.xyz /= posWS.w; - - // Build worldspace TBN from screen-space derivatives. The billboard is camera-aligned, - // so dPdx/dPdy lie in the billboard plane along screen X/Y. - float3 T = normalize(ddx(posWS.xyz)); - float3 N = normalize(cross(T, -ddy(posWS.xyz))); - float3 B = cross(N, T); - float3x3 TBN = float3x3(T, B, N); - - float3 normalTS = normalize(raindropNormal.xyz * 2.0 - 1.0); - float3 normalWS = normalize(mul(normalTS, TBN)); - - if (frontFace) - normalWS = -normalWS; - - float3 V = normalize(-posWS.xyz); - float NdotV = saturate(dot(normalWS, V)); - float fresnel = 0.02 + 0.98 * pow(1.0 - NdotV, 5.0); - - float3 reflectDir = reflect(-V, normalWS); - float3 refractDir = refract(-V, normalWS, 1.0 / 1.33); - if (dot(refractDir, refractDir) < 1e-4) - refractDir = -V; - - float3 reflectColor = Color::IrradianceToLinear(DynamicCubemaps::EnvReflectionsTexture.SampleLevel(SampSourceTexture, reflectDir, 0).xyz); - float3 refractColor = Color::IrradianceToLinear(DynamicCubemaps::EnvReflectionsTexture.SampleLevel(SampSourceTexture, refractDir, 0).xyz); - - psout.Color.xyz = Color::IrradianceToGamma(lerp(refractColor, reflectColor, fresnel)); - psout.Color.w = alpha; - psout.Normal = float4(0, 1, 0, alpha); - return psout; -} + if (SharedData::enbSettings.EnableRain) { + float4 raindropNormal = TexRaindropNormals.Sample(SampSourceTexture, input.RaindropData.xy); + float alpha = saturate(raindropNormal.w * (1.0 - SharedData::enbSettings.RainMotionTransparency)); + clip(alpha - (4.0 / 255.0)); + raindropNormal.y = 1.0 - raindropNormal.y; + + // Reconstruct camera-relative worldspace position (camera at origin). + float2 uv = input.Position.xy * SharedData::BufferDim.zw; + float4 posCS = float4(2.0 * float2(uv.x, 1.0 - uv.y) - 1.0, input.Position.z, 1.0); + float4 posWS = mul(FrameBuffer::CameraViewProjInverse, posCS); + posWS.xyz /= posWS.w; + + // Build worldspace TBN from screen-space derivatives. The billboard is camera-aligned, + // so dPdx/dPdy lie in the billboard plane along screen X/Y. + float3 T = normalize(ddx(posWS.xyz)); + float3 N = normalize(cross(T, -ddy(posWS.xyz))); + float3 B = cross(N, T); + float3x3 TBN = float3x3(T, B, N); + + float3 normalTS = normalize(raindropNormal.xyz * 2.0 - 1.0); + float3 normalWS = normalize(mul(normalTS, TBN)); + + if (frontFace) + normalWS = -normalWS; + + float3 V = normalize(-posWS.xyz); + float NdotV = saturate(dot(normalWS, V)); + float fresnel = 0.02 + 0.98 * pow(1.0 - NdotV, 5.0); + + float3 reflectDir = reflect(-V, normalWS); + float3 refractDir = refract(-V, normalWS, 1.0 / 1.33); + if (dot(refractDir, refractDir) < 1e-4) + refractDir = -V; + + float3 reflectColor = Color::IrradianceToLinear(DynamicCubemaps::EnvReflectionsTexture.SampleLevel(SampSourceTexture, reflectDir, 0).xyz); + float3 refractColor = Color::IrradianceToLinear(DynamicCubemaps::EnvReflectionsTexture.SampleLevel(SampSourceTexture, refractDir, 0).xyz); + + psout.Color.xyz = Color::IrradianceToGamma(lerp(refractColor, reflectColor, fresnel)); + psout.Color.w = alpha; + psout.Normal = float4(0, 1, 0, alpha); + return psout; + } # endif float4 sourceColor = TexSourceTexture.Sample(SampSourceTexture, input.TexCoord0); @@ -338,8 +338,8 @@ if (SharedData::enbSettings.EnableRain) { positionWS.xyz = positionWS.xyz / positionWS.w; float unusedDetailedShadow; - float3 dirLightColor = SharedData::DirLightColor.xyz * ShadowSampling::GetLightingShadow(positionWS.xyz, unusedDetailedShadow); - float3 ambientColor = max(0, SharedData::GetAmbient(float3(0, 0, 1))); + float3 dirLightColor = Color::GamutTransform(SharedData::DirLightColor.xyz) * ShadowSampling::GetLightingShadow(positionWS.xyz, unusedDetailedShadow); + float3 ambientColor = Color::GamutTransform(max(0, SharedData::GetAmbient(float3(0, 0, 1)))); # if defined(IBL) if (SharedData::iblSettings.EnableIBL) { ambientColor = ImageBasedLighting::GetDiffuseIBL(ambientColor, float3(0, 0, -1)); @@ -376,7 +376,7 @@ if (SharedData::enbSettings.EnableRain) { float intensityMultiplier = 1 - intensityFactor * intensityFactor; # endif - float3 lightColor = light.color.xyz * intensityMultiplier; + float3 lightColor = Color::GamutTransform(light.color.xyz) * intensityMultiplier; propertyColor += lightColor; } } diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index e5ed4e5313..d603829d54 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -281,7 +281,7 @@ struct PS_OUTPUT float4 Specular: SV_Target4; float4 Masks: SV_Target6; float4 Masks2: SV_Target7; -# endif // RENDER_DEPTH +# endif // RENDER_DEPTH }; #else struct PS_OUTPUT @@ -303,8 +303,6 @@ struct PS_OUTPUT SamplerState SampBaseSampler : register(s0); SamplerState SampShadowMaskSampler : register(s1); - - Texture2D TexBaseSampler : register(t0); Texture2D TexShadowMaskSampler : register(t1); @@ -315,8 +313,6 @@ cbuffer PerFrame : register(b0) float4 cb0_2[7] : packoffset(c3); } - - cbuffer AlphaTestRefCB : register(b11) { float AlphaTestRefRS : packoffset(c0); @@ -429,8 +425,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3x3 tbn = 0; - if (complex) - { + if (complex) { float3 normalColor = GrassLighting::TransformNormal(specColor.xyz); // world-space -> tangent-space -> world-space. // This is because we don't have pre-computed tangents. @@ -485,22 +480,23 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); vertexColor /= max(vertexAO, EPSILON_DIVISION); -# if defined(SKYLIGHTING) +# if defined(SKYLIGHTING) float3 positionMSSkylight = input.WorldPosition.xyz; sh2 skylightingSH = Skylighting::Sample(positionMSSkylight, normal -# if defined(SKYLIGHTING_SHADOW_VIS) - , skylightingShadowVisibility -# endif +# if defined(SKYLIGHTING_SHADOW_VIS) + , + skylightingShadowVisibility +# endif ); float skylightingDiffuse = Skylighting::GetSkylightingDiffuse(skylightingSH, positionMSSkylight, normal, vertexAO); -# endif // SKYLIGHTING +# endif // SKYLIGHTING float3 albedo = baseColor.xyz * vertexColor; float dirSoftShadow = dirDetailedShadow; -# if defined(SKYLIGHTING_SHADOW_VIS) +# if defined(SKYLIGHTING_SHADOW_VIS) dirSoftShadow = skylightingShadowVisibility; -# endif +# endif float3 subsurfaceColor = dirLightColor * dirSoftShadow * (GetSoftLightMultiplier(dirLightAngle, softLightRolloff)) * Color::VanillaNormalization(); @@ -570,10 +566,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 directionalAmbientColor = Color::Ambient(max(0, SharedData::GetAmbient(normal))); -# if defined(IBL) +# if defined(IBL) if (SharedData::iblSettings.EnableIBL) directionalAmbientColor = ImageBasedLighting::GetDiffuseIBL(directionalAmbientColor, -normal); -# endif +# endif +} +# endif diffuseColor += directionalAmbientColor; diffuseColor += subsurfaceColor * albedo; @@ -581,14 +579,14 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) directionalAmbientColor *= albedo; -# if defined(SKYLIGHTING) +# if defined(SKYLIGHTING) Skylighting::ApplySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); -# endif +# endif specularColor += lightsSpecularColor; specularColor *= specColor.w * SharedData::grassLightingSettings.SpecularStrength; -# if defined(LIGHT_LIMIT_FIX) && defined(LLFDEBUG) +# if defined(LIGHT_LIMIT_FIX) && defined(LLFDEBUG) if (SharedData::lightLimitFixSettings.EnableLightsVisualisation) { if (SharedData::lightLimitFixSettings.LightsVisualisationMode == 0) { diffuseColor.xyz = Color::TurboColormap(0); @@ -600,9 +598,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) } else { psout.Diffuse = float4(diffuseColor, 1); } -# else - psout.Diffuse.xyz = diffuseColor; -# endif +# else +psout.Diffuse.xyz = diffuseColor; +# endif float3 normalVS = normalize(FrameBuffer::WorldToView(normal, false)); psout.Albedo = float4(albedo, 1); @@ -611,32 +609,32 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Specular = float4(specularColor, 1); psout.Masks = float4(0, 0, Color::RGBToYCoCg(directionalAmbientColor).x, 0); psout.Masks2 = float4(1.0 - vertexAO, 0, 0, 0); -# endif +# endif return psout; } -# else +#else PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; -# if defined(SKYLIGHTING_SHADOW_VIS) +# if defined(SKYLIGHTING_SHADOW_VIS) float skylightingShadowVisibility = 1.0; -# endif +# endif float4 baseColor = TexBaseSampler.SampleBias(SampBaseSampler, input.TexCoord.xy, SharedData::MipBias); -# if defined(RENDER_DEPTH) +# if defined(RENDER_DEPTH) float diffuseAlpha = input.Color.w * baseColor.w; if ((diffuseAlpha - AlphaTestRefRS) < 0) { discard; } -# endif // RENDER_DEPTH || DO_ALPHA_TEST +# endif // RENDER_DEPTH || DO_ALPHA_TEST -# if defined(RENDER_DEPTH) +# if defined(RENDER_DEPTH) // Depth psout.PS.xyz = input.Depth.xxx / input.Depth.yyy; psout.PS.w = diffuseAlpha; -# else +# else if (SharedData::lodBlendingSettings.DisableTerrainVertexColors) input.Color.xyz = 1; @@ -658,14 +656,14 @@ PS_OUTPUT main(PS_INPUT input) if (!SharedData::InInterior) dirDetailedShadow = shadowColor.x; -# if defined(SCREEN_SPACE_SHADOWS) +# if defined(SCREEN_SPACE_SHADOWS) if (!SharedData::InInterior) dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.HPosition.xyz, screenUV, screenNoise); -# endif // SCREEN_SPACE_SHADOWS +# endif // SCREEN_SPACE_SHADOWS float3 diffuseColor = dirLightColor * dirDetailedShadow; -# if defined(LIGHT_LIMIT_FIX) +# if defined(LIGHT_LIMIT_FIX) uint clusterIndex = 0; uint lightCount = 0; @@ -682,17 +680,17 @@ PS_OUTPUT main(PS_INPUT input) float3 lightDirection = light.positionWS.xyz - input.WorldPosition.xyz; float lightDist = length(lightDirection); -# if defined(ISL) +# if defined(ISL) float intensityMultiplier = InverseSquareLighting::GetAttenuation(lightDist, light); if (intensityMultiplier < 1e-5) continue; -# else +# else float intensityFactor = saturate(lightDist / light.radius); if (intensityFactor == 1) continue; float intensityMultiplier = 1 - intensityFactor * intensityFactor; -# endif +# endif const bool isPointLightLinear = light.lightFlags & LightLimitFix::LightFlags::Linear; float3 lightColor = Color::PointLight(light.color.xyz, isPointLightLinear) * intensityMultiplier * light.fade; @@ -711,7 +709,7 @@ PS_OUTPUT main(PS_INPUT input) } } } -# endif // LIGHT_LIMIT_FIX +# endif // LIGHT_LIMIT_FIX float3 ddx = ddx_coarse(input.WorldPosition); float3 ddy = ddy_coarse(input.WorldPosition); @@ -721,22 +719,23 @@ PS_OUTPUT main(PS_INPUT input) float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); vertexColor /= max(vertexAO, EPSILON_DIVISION); -# if defined(SKYLIGHTING) +# if defined(SKYLIGHTING) float3 positionMSSkylight = input.WorldPosition.xyz; sh2 skylightingSH = Skylighting::Sample(positionMSSkylight, normal -# if defined(SKYLIGHTING_SHADOW_VIS) - , skylightingShadowVisibility -# endif +# if defined(SKYLIGHTING_SHADOW_VIS) + , + skylightingShadowVisibility +# endif ); float skylightingDiffuse = Skylighting::GetSkylightingDiffuse(skylightingSH, positionMSSkylight, normal, vertexAO); -# endif // SKYLIGHTING +# endif // SKYLIGHTING float3 directionalAmbientColor = Color::Ambient(max(0, SharedData::GetAmbient(normal))); -# if defined(IBL) +# if defined(IBL) if (SharedData::iblSettings.EnableIBL) directionalAmbientColor = ImageBasedLighting::GetDiffuseIBL(directionalAmbientColor, -normal); -# endif +# endif float3 albedo = baseColor.xyz * vertexColor; @@ -745,9 +744,9 @@ PS_OUTPUT main(PS_INPUT input) diffuseColor *= albedo; directionalAmbientColor *= albedo; -# if defined(SKYLIGHTING) +# if defined(SKYLIGHTING) Skylighting::ApplySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); -# endif +# endif psout.Diffuse.xyz = diffuseColor; @@ -760,10 +759,10 @@ PS_OUTPUT main(PS_INPUT input) psout.Albedo = float4(albedo, 1); psout.Masks = float4(0, 0, Color::RGBToYCoCg(directionalAmbientColor).x, 0); psout.Masks2 = float4(1.0 - vertexAO, 0, 0, 0); -# endif +# endif return psout; } -# endif +#endif #endif // PSHADER diff --git a/src/Buffer.h b/src/Buffer.h index 0170de3fdf..bcde8ff439 100644 --- a/src/Buffer.h +++ b/src/Buffer.h @@ -389,6 +389,12 @@ class Texture3D } } + explicit Texture3D(ID3D11Texture3D* a_resource) + { + a_resource->GetDesc(&desc); + resource.attach(a_resource); + } + /** @brief Creates a shader resource view from the given descriptor. */ void CreateSRV(D3D11_SHADER_RESOURCE_VIEW_DESC const& a_desc) { diff --git a/src/Feature.cpp b/src/Feature.cpp index dcc383b121..f6c734b581 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -21,6 +21,7 @@ #include "Features/LightLimitFix.h" #include "Features/LinearLighting.h" #include "Features/PerformanceOverlay.h" +#include "Features/PostProcessing.h" #include "Features/RemoteControl.h" #include "Features/RenderDoc.h" #include "Features/ScreenSpaceGI.h" @@ -257,7 +258,8 @@ const std::vector& Feature::GetFeatureList() &globals::features::horizonFix, &globals::features::exponentialHeightFog, &globals::features::hdrDisplay, - &globals::features::skin + &globals::features::skin, + &globals::features::postProcessing }; return features; diff --git a/src/FeatureBuffer.cpp b/src/FeatureBuffer.cpp index 73aac7c553..cc4a46747a 100644 --- a/src/FeatureBuffer.cpp +++ b/src/FeatureBuffer.cpp @@ -14,6 +14,7 @@ #include "Features/LODBlending.h" #include "Features/LightLimitFix.h" #include "Features/LinearLighting.h" +#include "Features/PostProcessing.h" #include "Features/Skin.h" #include "Features/Skylighting.h" #include "Features/TerrainBlending.h" @@ -62,5 +63,6 @@ std::pair GetFeatureBufferData(bool a_inWorld) globals::features::terrainBlending.settings, globals::features::exponentialHeightFog.GetCommonBufferData(), globals::features::truePBR.settings, - globals::features::skin.GetCommonBufferData()); + globals::features::skin.GetCommonBufferData(), + globals::features::postProcessing.GetCommonBufferData()); } \ No newline at end of file diff --git a/src/FeatureCategories.h b/src/FeatureCategories.h index 9e7fb61e06..8b052f8551 100644 --- a/src/FeatureCategories.h +++ b/src/FeatureCategories.h @@ -11,6 +11,7 @@ namespace FeatureCategories inline constexpr std::string_view kLighting = "Lighting"; inline constexpr std::string_view kMaterials = "Materials"; inline constexpr std::string_view kOther = "Other"; + inline constexpr std::string_view kPostProcessing = "Post-Processing"; inline constexpr std::string_view kSky = "Sky"; inline constexpr std::string_view kUtility = "Utility"; inline constexpr std::string_view kWater = "Water"; diff --git a/src/Features/Effects11.cpp b/src/Features/Effects11.cpp index a8e6248795..3f097b5295 100644 --- a/src/Features/Effects11.cpp +++ b/src/Features/Effects11.cpp @@ -516,19 +516,29 @@ void Effects11::OnSkyUpdateColors(RE::Sky* a_sky) OverrideWeather(a_sky); } -bool Effects11::HandleTonemapRender(RE::RENDER_TARGET a_input, RE::RENDER_TARGET a_output) +bool Effects11::WantsTonemapOwnership() { CheckCommonData(); - auto& settingManager = SettingManager::GetSingleton(); + // The initialized check must be part of ownership, not just of rendering: if it were only + // checked at render time, the arbiter would still report Effects11 as the owner while the + // vanilla pass ran, having already stripped Post Processing's tonemap flag and skipped its + // pipeline for that frame. + if (!EffectManager::GetSingleton().IsInitialized()) + return false; + + return enableEffect && !SettingManager::GetSingleton().GetValue("UseOriginalPostProcessing", "EFFECT"); +} + +bool Effects11::RenderTonemap(RE::RENDER_TARGET a_input, RE::RENDER_TARGET a_output) +{ auto& effectManager = EffectManager::GetSingleton(); + if (!effectManager.IsInitialized()) + return false; - if (enableEffect && !settingManager.GetValue("UseOriginalPostProcessing", "EFFECT")) { - auto& renderTargets = globals::game::renderer->GetRuntimeData().renderTargets; - effectManager.ExecuteEffects(renderTargets[a_input], renderTargets[a_output]); - return true; - } - return false; + auto& renderTargets = globals::game::renderer->GetRuntimeData().renderTargets; + effectManager.ExecuteEffects(renderTargets[a_input], renderTargets[a_output]); + return true; } void Effects11::ModifySky(RE::BSRenderPass* Pass) @@ -548,7 +558,6 @@ void Effects11::ModifySky(RE::BSRenderPass* Pass) } } - void Effects11::ModifyParticle(RE::BSRenderPass* Pass) { if (!enableEffect || !raindropSRV) @@ -569,7 +578,6 @@ void Effects11::ModifyParticle(RE::BSRenderPass* Pass) context->VSSetConstantBuffers(5, 2, cbs); } - void Effects11::ParticleShaderHacks() { if (!enableEffect || !raindropSRV) diff --git a/src/Features/Effects11.h b/src/Features/Effects11.h index e6acdceb9f..3abf9bf6ff 100644 --- a/src/Features/Effects11.h +++ b/src/Features/Effects11.h @@ -110,5 +110,21 @@ struct Effects11 : Feature void ModifySky(RE::BSRenderPass* Pass); __declspec(noinline) void ModifyParticle(RE::BSRenderPass* Pass); void ParticleShaderHacks(); - bool HandleTonemapRender(RE::RENDER_TARGET a_input, RE::RENDER_TARGET a_output); + + /** + * @brief Whether Effects11 wants to replace the vanilla tonemap this frame. + * + * Queried by State::GetTonemapOwner() to arbitrate against Post Processing. Does not + * render anything; refreshes per-frame common data as a side effect. + */ + bool WantsTonemapOwnership(); + + /** + * @brief Runs the ENB effect chain in place of the vanilla tonemap pass. + * @param a_input Render target holding the scene color to tonemap. + * @param a_output Render target receiving the tonemapped result. + * @return True if the chain ran; false if the effect manager is not yet initialized, + * in which case the caller must fall back to the vanilla pass. + */ + bool RenderTonemap(RE::RENDER_TARGET a_input, RE::RENDER_TARGET a_output); }; diff --git a/src/Features/Effects11/MenuManager.cpp b/src/Features/Effects11/MenuManager.cpp index 8cbe2ac3ca..7b15342b28 100644 --- a/src/Features/Effects11/MenuManager.cpp +++ b/src/Features/Effects11/MenuManager.cpp @@ -1,11 +1,14 @@ #include "MenuManager.h" #include "EffectManager.h" -#include "SettingManager.h" -#include "TextureManager.h" #include "Features/Effects11.h" #include "Features/Effects11/ShaderPatches.h" +#include "Features/PostProcessing.h" #include "Globals.h" +#include "Menu.h" +#include "SettingManager.h" +#include "State.h" +#include "TextureManager.h" static const char* const timeOfDayNames[] = { "Dawn", "Sunrise", "Day", "Sunset", "Dusk", "Night", "InteriorDay", "InteriorNight" }; @@ -92,6 +95,16 @@ void MenuManager::RenderSettingsPanel() ImGui::Separator(); + if (globals::state->GetTonemapOwner() == State::TonemapOwner::kEffects11 && + globals::features::postProcessing.loaded && + globals::features::postProcessing.WantsTonemapOwnership()) { + ImGui::TextColored( + Menu::GetSingleton()->GetTheme().StatusPalette.Warning, + "Effects 11 is overriding Post Processing's tonemapping.\n" + "Enable \"UseOriginalPostProcessing\" below to hand it back."); + ImGui::Separator(); + } + if (ImGui::BeginChild("SettingsScroll", ImVec2(0, 0), false)) { RenderAllSettings(); } @@ -465,4 +478,3 @@ void MenuManager::RenderAllSettings() ImGui::EndTabBar(); } } - diff --git a/src/Features/LinearLighting.cpp b/src/Features/LinearLighting.cpp index c7ec62b391..6c71abe27a 100644 --- a/src/Features/LinearLighting.cpp +++ b/src/Features/LinearLighting.cpp @@ -2,6 +2,7 @@ #include "../I18n/I18n.h" #include "State.h" +#include "Util.h" #include "Effects11.h" #include "Effects11/SettingManager.h" @@ -13,6 +14,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( LinearLighting::Settings, enableLinearLighting, + enableACEScg, lightGamma, colorGamma, emitColorGamma, @@ -49,6 +51,12 @@ void LinearLighting::DrawSettings() } ImGui::Checkbox(T(TKEY("enable"), "Enable Linear Lighting"), (bool*)&settings.enableLinearLighting); + ImGui::Checkbox(T(TKEY("enable_acescg"), "Enable ACEScg Wide Gamut"), (bool*)&settings.enableACEScg); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("%s", T(TKEY("enable_acescg_tooltip"), + "Render in ACEScg color space for wider gamut and more accurate lighting.\n" + "Requires Linear Lighting and Post Processing enabled.\n" + "All sRGB-gamut textures and colors will be converted to ACEScg during shading.")); if (ImGui::BeginTabBar("##LinearLightingTabs", ImGuiTabBarFlags_None)) { if (ImGui::BeginTabItem(T(TKEY("tab_general"), "General"))) { @@ -167,6 +175,7 @@ LinearLighting::PerFrameData LinearLighting::GetCommonBufferData() bool isMainLoadingMenu = globals::state->isMainMenuOpen || globals::state->isLoadingMenuOpen; auto data = PerFrameData{}; data.enableLinearLighting = settings.enableLinearLighting && !isMainLoadingMenu; + data.enableACEScg = settings.enableACEScg && settings.enableLinearLighting && !isMainLoadingMenu; data.isDirLightLinear = isDirLightLinear; data.dirLightMult = dirLightMult; data.lightGamma = settings.lightGamma; diff --git a/src/Features/LinearLighting.h b/src/Features/LinearLighting.h index b54a2f8a6f..547e2a3e10 100644 --- a/src/Features/LinearLighting.h +++ b/src/Features/LinearLighting.h @@ -26,6 +26,7 @@ struct LinearLighting : Feature struct Settings { uint enableLinearLighting = false; + uint enableACEScg = false; float lightGamma = 1.8f; float colorGamma = 1.8f; float emitColorGamma = 1.8f; @@ -59,6 +60,7 @@ struct LinearLighting : Feature struct alignas(16) PerFrameData { uint enableLinearLighting; + uint enableACEScg; uint isDirLightLinear; float dirLightMult; float lightGamma; @@ -85,7 +87,6 @@ struct LinearLighting : Feature float projectedEffectMult; float deferredEffectMult; float otherEffectMult; - uint pad0; }; STATIC_ASSERT_ALIGNAS_16(PerFrameData); diff --git a/src/Features/PostProcessing.cpp b/src/Features/PostProcessing.cpp new file mode 100644 index 0000000000..31a53d0a0b --- /dev/null +++ b/src/Features/PostProcessing.cpp @@ -0,0 +1,690 @@ +#include "PostProcessing.h" + +#include "IconsFontAwesome5.h" +#include "imgui_stdlib.h" + +#include "Profiler.h" +#include "State.h" +#include "Util.h" + +#include "Features/Upscaling.h" + +#include + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + PostProcessing::Settings, + DisableVanillaTonemapping) + +void PostProcessing::DrawSettings() +{ + static int pipelinePageNum = 0; + static int pipelineFeatIdx = 0; + static int presetIdx = -1; + + ImGui::BeginGroup(); + std::string currentPreset = (presetIdx >= 0 && presetIdx < presets.size()) ? presets[presetIdx] : T("feature.post_processing.select_a_preset", "Select a preset"); + + if (ImGui::BeginCombo("##PresetCombo", currentPreset.c_str())) { + presets = LoadPresets(); + + for (int i = 0; i < presets.size(); ++i) { + bool isSelected = presetIdx == i; + if (ImGui::Selectable(presets[i].c_str(), isSelected)) + presetIdx = i; + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + ImGui::SameLine(); + if (ImGui::Button(T("feature.post_processing.load", "Load"))) { + if (presetIdx >= 0 && presetIdx < presets.size()) { + LoadPresetFrom(presets[presetIdx]); + } + } + + ImGui::EndGroup(); + ImGui::BeginGroup(); + static std::string newPresetName = ""; + ImGui::InputText("##NewPresetName", &newPresetName); + + ImGui::SameLine(); + if (ImGui::Button(T("feature.post_processing.save", "Save"))) { + if (!newPresetName.empty()) + SavePresetTo(newPresetName); + } + + ImGui::EndGroup(); + + ImGui::Separator(); + ImGui::Checkbox(T("feature.post_processing.bypass", "Bypass"), &bypass); + ImGui::SameLine(); + + // Effects11 replaces the whole tonemap pass, so this toggle would have no effect while + // it owns the frame. Disable it rather than let it silently do nothing. + const bool tonemapTakenByEffects11 = globals::state->GetTonemapOwner() == State::TonemapOwner::kEffects11; + + ImGui::BeginDisabled(tonemapTakenByEffects11); + ImGui::Checkbox(T("feature.post_processing.disable_vanilla_tonemapping", "Disable Vanilla Tonemapping"), (bool*)&settings.DisableVanillaTonemapping); + ImGui::EndDisabled(); + + if (tonemapTakenByEffects11) { + ImGui::TextWrapped("%s", T("feature.post_processing.tonemap_owned_by_effects11", + "Tonemapping is currently handled by Effects 11. Post Processing effects that run " + "before tonemapping still apply. To use Post Processing tonemapping instead, either " + "disable Effects 11 or enable its \"UseOriginalPostProcessing\" setting.")); + } + + ImGui::Separator(); + + if (pipelinePageNum == 0) { + for (int i = 0; i < pipeline.size(); ++i) { + auto& feat = pipeline[i]; + if (feat && feat->IsVisible()) { + auto displayName = feat->GetDisplayName(); + auto description = feat->GetDesc(); + ImGui::PushID(feat->GetType().c_str()); + ImGui::Checkbox("##Enabled", &feat->enabled); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_BARS)) { + pipelineFeatIdx = i; + pipelinePageNum = 1; + } + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("%s", T("feature.post_processing.edit_settings_for_this_feature", "Edit settings for this feature.")); + ImGui::SameLine(); + ImGui::Text("%s", displayName.c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("%s", description.c_str()); + ImGui::PopID(); + } + } + } else if (pipelinePageNum == 1) { + auto backLabel = std::format("{} {}", ICON_FA_ARROW_LEFT, T("feature.post_processing.back_to_pipeline", "Back to Pipeline")); + if (ImGui::Button(backLabel.c_str())) { + pipelinePageNum = 0; + } + ImGui::Separator(); + if (pipelineFeatIdx >= 0 && pipelineFeatIdx < pipeline.size()) { + auto& feat = pipeline[pipelineFeatIdx]; + if (feat) { + auto displayName = feat->GetDisplayName(); + auto description = feat->GetDesc(); + ImGui::PushID(feat->GetType().c_str()); + + ImGui::SeparatorText(displayName.c_str()); + ImGui::TextWrapped("%s", description.c_str()); + + ImGui::Spacing(); + auto recompileLabel = std::format("{} {}", ICON_FA_SYNC, T("feature.post_processing.recompile_shaders", "Recompile Shaders")); + if (ImGui::Button(recompileLabel.c_str())) { + feat->ClearShaderCache(); + } + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("%s", T("feature.post_processing.recompile_shaders_for_this_sub_feature_only", "Recompile shaders for this sub-feature only.")); + ImGui::Separator(); + ImGui::Spacing(); + ImGui::Checkbox(T("feature.post_processing.enabled", "Enabled"), &feat->enabled); + if (feat->enabled) { + ImGui::Indent(); + feat->DrawSettings(); + ImGui::Unindent(); + } else { + ImGui::TextDisabled("%s", T("feature.post_processing.enable_the_feature_to_see_its_settings", "Enable the feature to see its settings.")); + } + + ImGui::PopID(); + } else { + ImGui::TextDisabled("%s", T("feature.post_processing.selected_feature_is_not_valid", "Selected feature is not valid.")); + pipelinePageNum = 0; + } + } else { + ImGui::TextDisabled("%s", T("feature.post_processing.invalid_feature_selected_returning_to_list", "Invalid feature selected. Returning to list.")); + pipelinePageNum = 0; + } + } + + ImGui::Separator(); + + if (ImGui::TreeNode(T("feature.post_processing.debug", "Debug"))) { + if (ImGui::TreeNode(T("feature.post_processing.game_imagespace_values", "Game ImageSpace Values"))) { + ImGui::Text(T("feature.post_processing.base_amount", "Base Amount: %.3f"), imageSpaceManager->gameISData.baseAmount); + ImGui::Text("%s", T("feature.post_processing.base_data", "Base Data:")); + ImGui::Text("%s", T("feature.post_processing.cinematic_values", "Cinematic Values:")); + ImGui::Text(T("feature.post_processing.saturation_brightness_contrast_values", "Saturation: %.3f\nBrightness: %.3f\nContrast: %.3f"), + imageSpaceManager->gameISData.baseData.cinematic.saturation, + imageSpaceManager->gameISData.baseData.cinematic.brightness, + imageSpaceManager->gameISData.baseData.cinematic.contrast); + + ImGui::Text("%s", T("feature.post_processing.hdr_values", "HDR Values:")); + ImGui::Text(T("feature.post_processing.hdr_values_detail", "Eye Adapt Speed: %.3f\nBloom Blur Radius: %.3f\nBloom Threshold: %.3f\nBloom Scale: %.3f\nReceive Bloom Threshold: %.3f\nWhite: %.3f\nSunlight Scale: %.3f\nSky Scale: %.3f\nEye Adapt Strength: %.3f"), + imageSpaceManager->gameISData.baseData.hdr.eyeAdaptSpeed, + imageSpaceManager->gameISData.baseData.hdr.bloomBlurRadius, + imageSpaceManager->gameISData.baseData.hdr.bloomThreshold, + imageSpaceManager->gameISData.baseData.hdr.bloomScale, + imageSpaceManager->gameISData.baseData.hdr.receiveBloomThreshold, + imageSpaceManager->gameISData.baseData.hdr.white, + imageSpaceManager->gameISData.baseData.hdr.sunlightScale, + imageSpaceManager->gameISData.baseData.hdr.skyScale, + imageSpaceManager->gameISData.baseData.hdr.eyeAdaptStrength); + + ImGui::Text("%s", T("feature.post_processing.tint_values", "Tint Values:")); + ImGui::Text(T("feature.post_processing.tint_values_detail", "Tint Amount: %.3f\nTint Color: (%.3f, %.3f, %.3f)"), + imageSpaceManager->gameISData.baseData.tint.amount, + imageSpaceManager->gameISData.baseData.tint.color.red, + imageSpaceManager->gameISData.baseData.tint.color.green, + imageSpaceManager->gameISData.baseData.tint.color.blue); + + ImGui::Text("%s", T("feature.post_processing.depth_of_field_values", "Depth of Field Values:")); + ImGui::Text(T("feature.post_processing.depth_of_field_values_detail", "DOF Strength: %.3f\nDOF Distance: %.3f\nDOF Range: %.3f\nDOF Flags: %d\nDOF Sky Blur Radius: %d"), + imageSpaceManager->gameISData.baseData.depthOfField.strength, + imageSpaceManager->gameISData.baseData.depthOfField.distance, + imageSpaceManager->gameISData.baseData.depthOfField.range, + imageSpaceManager->gameISData.baseData.depthOfField.flags, + static_cast(imageSpaceManager->gameISData.baseData.depthOfField.skyBlurRadius.get())); + + ImGui::Text(T("feature.post_processing.mod_amount", "Mod Amount: %.3f"), imageSpaceManager->gameISData.modAmount); + ImGui::Text("%s", T("feature.post_processing.mod_data", "Mod Data:")); + ImGui::Text(T("feature.post_processing.mod_fade_values_detail", "Fade Amount: %.3f\nFade Color: (%.3f, %.3f, %.3f)\nBlur Radius: %.3f\nDouble Vision Strength: %.3f\n"), + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kFadeAmount], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kFadeR], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kFadeG], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kFadeB], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kBlurRadius], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kDoubleVisionStrength]); + ImGui::Text(T("feature.post_processing.radial_blur_values_detail", "Radial Blur Strength: %.3f\nRadial Blur Rampup: %.3f\nRadial Blur Start: %.3f\nRadial Blur Rampdown: %.3f\nRadial Blur Down Start: %.3f\nRadial Blur Center: (%.3f, %.3f)"), + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kRadialBlurStrength], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kRadialBlurRampup], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kRadialBlurStart], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kRadialBlurRampdown], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kRadialBlurDownStart], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kRadialBlurCenterX], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kRadialBlurCenterY]); + ImGui::Text(T("feature.post_processing.mod_dof_values_detail", "DOF Strength: %.3f\nDOF Distance: %.3f\nDOF Range: %.3f\nDOF Mode: %d"), + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kDOFStrength], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kDOFDistance], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kDOFRange], + imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kDOFMode]); + ImGui::Text(T("feature.post_processing.motion_blur_strength", "Motion Blur Strength: %.3f"), imageSpaceManager->gameISData.modData.data[RE::ImageSpaceModData::kMotionBlurStrength]); + ImGui::TreePop(); + } + ImGui::TreePop(); + } +} + +void PostProcessing::LoadSettings(json& o_json) +{ + pendingSettings = o_json; +} + +void PostProcessing::ProcessSettings(json& o_json) +{ + logger::info("Loading post processing settings..."); + + for (auto& feat : pipeline) { + if (feat && o_json.contains(feat->GetType())) { + if (!feat->IsAutoEnabled()) + feat->enabled = o_json.value(feat->GetType(), json::object()).value("enabled", true); + json featSettings = o_json.value(feat->GetType(), json::object()).value("settings", json::object()); + feat->LoadSettings(featSettings); + if (loaded) + feat->SetupResources(); + } + } + + if (o_json.contains("ppsettings")) + settings = o_json["ppsettings"]; +} + +void PostProcessing::SaveSettings(json& o_json) +{ + if (!pendingSettings.empty()) { + o_json = pendingSettings; + return; + } + + for (auto& pipe : pipeline) { + if (pipe) { + json featureSetting{}; + pipe->SaveSettings(featureSetting); + o_json[pipe->GetType()] = { + { "enabled", pipe->enabled }, + { "settings", featureSetting } + }; + } + } + + o_json["ppsettings"] = settings; +} + +std::vector PostProcessing::LoadPresets() +{ + std::vector o_presets = {}; + + try { + std::filesystem::create_directories(ppPresetPath); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Error creating preset directory during Load ({}) : {}\n", ppPresetPath, e.what()); + return o_presets; + } + + for (const auto& entry : std::filesystem::directory_iterator(ppPresetPath)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + o_presets.push_back(entry.path().stem().string()); + } + } + + return o_presets; +} + +void PostProcessing::LoadPresetFrom(std::string a_name) +{ + json a_presets = {}; + + // if the name has .json, remove it + if (a_name.ends_with(".json")) + a_name = a_name.substr(0, a_name.size() - 5); + + try { + logger::info("Loading preset: {}", a_name); + std::ifstream i{ std::format("{}\\{}.json", ppPresetPath, a_name) }; + i >> a_presets; + } catch (const std::exception& e) { + logger::warn("Failed to load preset: {}. Error: {}", a_name, e.what()); + return; + } + + ProcessSettings(a_presets); +} + +void PostProcessing::SavePresetTo(std::string a_name) +{ + // Check if the name is valid + if (a_name.empty()) { + logger::warn("Invalid preset name."); + return; + } + + json a_presets = {}; + SaveSettings(a_presets); + a_presets["preset_name"] = a_name; + + try { + std::filesystem::create_directories(ppPresetPath); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Error creating preset directory during Save ({}) : {}\n", ppPresetPath, e.what()); + return; + } + + std::string presetPath = std::format("{}\\{}.json", ppPresetPath, a_name); + std::ofstream o{ presetPath }; + if (!o.is_open() || !o.good()) { + logger::warn("Failed to open preset file for writing: {}", presetPath); + return; + } + + try { + o << std::setw(4) << a_presets; + logger::info("Saving preset to {}", presetPath); + } catch (const std::exception& e) { + logger::warn("Failed to write preset to file: {}. Error: {}", presetPath, e.what()); + } +} + +void PostProcessing::RestoreDefaultSettings() +{ + // If pipeline isn't initialized yet (called during early loading before SetupResources), + // load default.json into pendingSettings for deferred application in SetupResources. + // This ensures first-startup defaults match what "Restore Defaults" produces later. + bool pipelineReady = pipeline[static_cast(FeaturePipelineIndex::AutoExposure)] != nullptr; + if (!pipelineReady) { + try { + std::ifstream i{ std::format("{}\\{}.json", ppPresetPath, "default") }; + json defaultPreset; + i >> defaultPreset; + pendingSettings = defaultPreset; + logger::info("Pipeline not ready, loaded default preset into pending settings"); + } catch (const std::exception& e) { + logger::info("No default preset available during early load, C++ defaults will be used. Error: {}", e.what()); + pendingSettings = {}; + } + return; + } + + try { + LoadPresetFrom("default"); + } catch (const std::exception& e) { + logger::warn("Failed to load default preset. Error: {}", e.what()); + settings = {}; + pipeline[static_cast(FeaturePipelineIndex::AutoExposure)].get()->enabled = true; + pipeline[static_cast(FeaturePipelineIndex::ColorGrading)].get()->enabled = true; + pipeline[static_cast(FeaturePipelineIndex::LUT)].get()->enabled = false; + + pipeline[static_cast(FeaturePipelineIndex::MotionBlur)].get()->enabled = false; + pipeline[static_cast(FeaturePipelineIndex::DoF)].get()->enabled = false; + pipeline[static_cast(FeaturePipelineIndex::CODBloom)].get()->enabled = true; + pipeline[static_cast(FeaturePipelineIndex::LensFlare)].get()->enabled = false; + pipeline[static_cast(FeaturePipelineIndex::Vignette)].get()->enabled = true; + pipeline[static_cast(FeaturePipelineIndex::Camera)].get()->enabled = false; + + for (auto& pipe : pipeline) { + if (pipe) { + pipe->RestoreDefaultSettings(); + } + } + } +} + +void PostProcessing::ClearShaderCache() +{ + for (auto& pipe : pipeline) { + if (pipe) + pipe->ClearShaderCache(); + } +} + +void PostProcessing::SetupResources() +{ + { + auto renderer = globals::game::renderer; + auto gameTexMain = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + D3D11_TEXTURE2D_DESC texDesc; + D3D11_TEXTURE2D_DESC texMainDesc; + D3D11_TEXTURE2D_DESC texMainCopyDesc; + gameTexMain.texture->GetDesc(&texMainDesc); + gameTexMainCopy.texture->GetDesc(&texMainCopyDesc); + texDesc = texMainDesc; + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + texDesc.MipLevels = srvDesc.Texture2D.MipLevels = 1; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + + texCopyMain = eastl::make_unique(texDesc); + texCopyMain->CreateUAV(uavDesc); + + if (texMainCopyDesc.Format != texMainDesc.Format) { + texDesc = texMainCopyDesc; + srvDesc.Format = texDesc.Format; + uavDesc.Format = texDesc.Format; + texDesc.MipLevels = srvDesc.Texture2D.MipLevels = 1; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + + texCopyMainCopy = eastl::make_unique(texDesc); + texCopyMainCopy->CreateUAV(uavDesc); + } else { + texCopyMainCopy = nullptr; + } + + texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + srvDesc.Format = texDesc.Format; + uavDesc.Format = texDesc.Format; + + texAfterTAA = eastl::make_unique(texDesc); + texAfterTAA->CreateSRV(srvDesc); + texAfterTAA->CreateUAV(uavDesc); + } + + if (auto rawPtr = reinterpret_cast(Util::CompileShader(L"Data\\Shaders\\PostProcessing\\copy.cs.hlsl", {}, "cs_5_0"))) + copyCS.attach(rawPtr); + + pipeline[static_cast(FeaturePipelineIndex::LocalExposure)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::LocalExposure)].get()->enabled = false; + pipeline[static_cast(FeaturePipelineIndex::AutoExposure)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::AutoExposure)].get()->enabled = true; + pipeline[static_cast(FeaturePipelineIndex::ColorGrading)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::ColorGrading)].get()->enabled = true; + pipeline[static_cast(FeaturePipelineIndex::LUT)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::LUT)].get()->enabled = false; + + pipeline[static_cast(FeaturePipelineIndex::MotionBlur)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::MotionBlur)].get()->enabled = false; + pipeline[static_cast(FeaturePipelineIndex::DoF)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::DoF)].get()->enabled = false; + pipeline[static_cast(FeaturePipelineIndex::PhysicalGlare)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::PhysicalGlare)].get()->enabled = false; + pipeline[static_cast(FeaturePipelineIndex::CODBloom)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::CODBloom)].get()->enabled = true; + pipeline[static_cast(FeaturePipelineIndex::LensFlare)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::LensFlare)].get()->enabled = false; + pipeline[static_cast(FeaturePipelineIndex::Composite)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::Composite)].get()->enabled = true; + pipeline[static_cast(FeaturePipelineIndex::Vignette)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::Vignette)].get()->enabled = true; + pipeline[static_cast(FeaturePipelineIndex::Camera)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::Camera)].get()->enabled = false; + pipeline[static_cast(FeaturePipelineIndex::Border)] = std::make_unique(); + pipeline[static_cast(FeaturePipelineIndex::Border)].get()->enabled = false; + + for (auto& pipe : pipeline) { + if (pipe) { + pipe->owner = this; + pipe->SetupResources(); + } + } + + bokehResources.Setup(); + + ProcessSettings(pendingSettings); + pendingSettings = {}; +} + +void PostProcessing::Reset() +{ + // Cleared per frame rather than only at the end of PreProcess: when Effects11 owns the + // tonemap (or the pipeline is bypassed) PreProcess never runs, and a stale flag would + // make the next frame we do run read from the wrong buffer. + isrefraction = false; + + for (auto& pipe : pipeline) { + if (pipe) + pipe->Reset(); + } +} + +void PostProcessing::CopyToRenderTarget( + RE::BSGraphics::RenderTargetData& targetRT, + Texture2D* convertTex, + ID3D11Texture2D* srcTex, + ID3D11ShaderResourceView* srcSRV) +{ + auto context = globals::d3d::context; + + D3D11_TEXTURE2D_DESC srcDesc; + srcTex->GetDesc(&srcDesc); + + D3D11_TEXTURE2D_DESC targetDesc; + targetRT.texture->GetDesc(&targetDesc); + + if (srcDesc.Format == targetDesc.Format) { + context->CopySubresourceRegion(targetRT.texture, 0, 0, 0, 0, srcTex, 0, nullptr); + return; + } + + if (!copyCS || !convertTex || !convertTex->uav || !convertTex->resource) + return; + + ID3D11ShaderResourceView* srv = srcSRV; + ID3D11UnorderedAccessView* uav = convertTex->uav.get(); + + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShaderResources(0, 1, &srv); + context->CSSetShader(copyCS.get(), nullptr, 0); + context->Dispatch((convertTex->desc.Width + 7) >> 3, (convertTex->desc.Height + 7) >> 3, 1); + + srv = nullptr; + uav = nullptr; + + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShaderResources(0, 1, &srv); + context->CSSetShader(nullptr, nullptr, 0); + + context->CopySubresourceRegion(targetRT.texture, 0, 0, 0, 0, convertTex->resource.get(), 0, nullptr); +} + +void PostProcessing::DrawFeature(PostProcessFeature& feature, PostProcessFeature::TextureInfo& lastTexColor) +{ + if (feature.WritesToMainTexture()) { + feature.Draw(lastTexColor); + } else { + PostProcessFeature::TextureInfo inTex = lastTexColor; + feature.Draw(inTex); + } +} + +void PostProcessing::DrawBeforeUpscaling() +{ + if (bypass) + return; + + auto& upscaling = globals::features::upscaling; + if (!upscaling.loaded) + return; + + auto renderer = globals::game::renderer; + auto state = globals::state; + + bool inMainLoadingMenu = globals::game::ui && (globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) || globals::game::ui->IsMenuOpen(RE::LoadingMenu::MENU_NAME)); + auto gameTexMain = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + PostProcessFeature::TextureInfo lastTexColor = { gameTexMain.texture, gameTexMain.SRV }; + + state->BeginPerfEvent("[Post Processing] Pre-Upscale"); + + // update auto-enabled features + for (auto& pipe : pipeline) { + if (pipe && pipe->IsAutoEnabled()) + pipe->UpdateAutoEnabled(); + } + + // go through each fx + for (auto& pipe : pipeline) { + if (pipe && pipe->enabled && !pipe->DrawAfterColorGrading() && !(inMainLoadingMenu && pipe->DisableInMainLoadingMenu()) && pipe->DrawBeforeUpscaling()) { + DrawFeature(*pipe, lastTexColor); + } + } + + CopyToRenderTarget(gameTexMain, texCopyMain.get(), lastTexColor.tex, lastTexColor.srv); + + state->EndPerfEvent(); +} + +void PostProcessing::PreProcess(RE::RENDER_TARGET a_input) +{ + if (bypass) + return; + + auto renderer = globals::game::renderer; + + auto& upscaling = globals::features::upscaling; + + bool inMainLoadingMenu = globals::game::ui && (globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) || globals::game::ui->IsMenuOpen(RE::LoadingMenu::MENU_NAME)); + + auto& gameTexMainRT = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + auto& gameTexMainCopyRT = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + // The tonemap hook hands us the pass input directly, so no need to probe the bound RTV. + // Refraction still routes through kMAIN_COPY without that being reflected in a_input. + bool useMainCopy = isrefraction || a_input == RE::RENDER_TARGETS::kMAIN_COPY; + + auto gameTexMain = useMainCopy ? gameTexMainCopyRT : gameTexMainRT; + PostProcessFeature::TextureInfo lastTexColor = { gameTexMain.texture, gameTexMain.SRV }; + auto gameTexMainAlt = useMainCopy ? gameTexMainRT : gameTexMainCopyRT; + + // update auto-enabled features + for (auto& pipe : pipeline) { + if (pipe && pipe->IsAutoEnabled()) + pipe->UpdateAutoEnabled(); + } + + // go through each fx + for (auto& pipe : pipeline) { + if (pipe && pipe->enabled && !pipe->DrawAfterColorGrading() && !(inMainLoadingMenu && pipe->DisableInMainLoadingMenu()) && (!pipe->DrawBeforeUpscaling() || !upscaling.loaded)) { + DrawFeature(*pipe, lastTexColor); + } + } + + for (auto& pipe : pipeline) { + if (pipe && pipe->enabled && pipe->DrawAfterColorGrading() && !(inMainLoadingMenu && pipe->DisableInMainLoadingMenu()) && (!pipe->DrawBeforeUpscaling() || !upscaling.loaded)) { + DrawFeature(*pipe, lastTexColor); + } + } + + Texture2D* mainConvertTex = texCopyMain.get(); + Texture2D* mainCopyConvertTex = texCopyMainCopy ? texCopyMainCopy.get() : texCopyMain.get(); + + CopyToRenderTarget(gameTexMain, useMainCopy ? mainCopyConvertTex : mainConvertTex, lastTexColor.tex, lastTexColor.srv); + CopyToRenderTarget(gameTexMainAlt, useMainCopy ? mainConvertTex : mainCopyConvertTex, lastTexColor.tex, lastTexColor.srv); + + isrefraction = false; +} + +void PostProcessing::ClearBorderMotionVectorsForFrameGen() +{ + if (bypass) + return; + + auto borderIdx = static_cast(FeaturePipelineIndex::Border); + auto& pipe = pipeline[borderIdx]; + if (pipe && pipe->enabled) { + auto* border = static_cast(pipe.get()); + border->ClearMotionVectorsForFrameGen(); + } +} + +bool PostProcessing::WantsTonemapOwnership() const +{ + return !bypass && settings.DisableVanillaTonemapping != 0; +} + +PostProcessing::Settings PostProcessing::GetCommonBufferData() +{ + Settings data = settings; + + // Effects11 outputs gamma-space SDR from its own tonemapper. Leaving this flag set would + // make ISHDR take its passthrough branch and HDROutputCS treat the scene as linear and + // already display-mapped, skipping AutoHDR and the BT.2020 conversion. + if (globals::state->GetTonemapOwner() != State::TonemapOwner::kPostProcessing) + data.DisableVanillaTonemapping = 0; + + return data; +} + +void PostProcessing::Prepass() +{ + if (!pendingSettings.empty()) { + logger::info("Processing pending post processing settings..."); + ProcessSettings(pendingSettings); + pendingSettings = {}; + } + + // Update gameISData + const auto ImageSpace = RE::ImageSpaceManager::GetSingleton(); + const auto& iSRuntimeData = ImageSpace->GetRuntimeData(); + imageSpaceManager->gameISData = iSRuntimeData.data; + if (const auto& overrideBaseData = iSRuntimeData.overrideBaseData) { + imageSpaceManager->gameISData.baseData = *overrideBaseData; + } else { + imageSpaceManager->gameISData.baseData = *iSRuntimeData.currentBaseData; + } +} + +void PostProcessing::PostPostLoad() +{ + logger::info("Hooking preprocess passes"); + stl::write_vfunc<0x2, BSImagespaceShaderRefraction_SetupTechnique>(RE::VTABLE_BSImagespaceShaderRefraction[0]); +} diff --git a/src/Features/PostProcessing.h b/src/Features/PostProcessing.h new file mode 100644 index 0000000000..d74b19305d --- /dev/null +++ b/src/Features/PostProcessing.h @@ -0,0 +1,168 @@ +#pragma once + +#include "PostProcessing/PostProcessFeature.h" + +#include "PostProcessing/BokehResources.h" +#include "PostProcessing/Border.h" +#include "PostProcessing/CODBloom.h" +#include "PostProcessing/Camera.h" +#include "PostProcessing/ColorGrading.h" +#include "PostProcessing/Composite.h" +#include "PostProcessing/DoF.h" +#include "PostProcessing/HistogramAutoExposure.h" +#include "PostProcessing/LUT.h" +#include "PostProcessing/LensFlare.h" +#include "PostProcessing/LocalExposure.h" +#include "PostProcessing/MotionBlur.h" +#include "PostProcessing/PhysicalGlare.h" +#include "PostProcessing/Vignette.h" + +struct PostProcessing : Feature +{ + static PostProcessing* GetSingleton() + { + static PostProcessing singleton; + return &singleton; + } + + struct alignas(16) Settings + { + uint DisableVanillaTonemapping = 1; + uint pad[3]; + } settings; + + const std::string ppPresetPath = "Data\\SKSE\\Plugins\\CommunityShaders\\PostProcessing"; + + virtual inline std::string GetName() override { return "Post Processing"; } + virtual inline std::string GetDisplayName() override { return T("feature.post_processing.name", "Post Processing"); } + virtual inline std::string GetShortName() override { return "PostProcessing"; } + virtual inline std::string_view GetShaderDefineName() override { return "POSTPROCESS"; } + virtual inline bool HasShaderDefine(RE::BSShader::Type t) override + { + return t == RE::BSShader::Type::ImageSpace; + }; + virtual std::string_view GetCategory() const override { return FeatureCategories::kPostProcessing; } + virtual std::pair> GetFeatureSummary() override + { + return { + T("feature.post_processing.description", "Post Processing provides advanced image effects and enhancements to improve the visual quality of the game."), + { T("feature.post_processing.key_feature_1", "Customizable post-processing effects"), + T("feature.post_processing.key_feature_2", "Supports various presets for different visual styles"), + T("feature.post_processing.key_feature_3", "Improves overall image quality and immersion"), + T("feature.post_processing.key_feature_4", "Includes features like bloom, depth of field, and color grading") } + }; + } + + virtual void DrawSettings() override; + virtual void LoadSettings(json& o_json) override; + virtual void SaveSettings(json& o_json) override; + virtual void RestoreDefaultSettings() override; + + /** + * @brief Whether Post Processing wants to replace the vanilla tonemap this frame. + * + * Queried by State::GetTonemapOwner() to arbitrate against Effects11. Note this is + * narrower than "is the pipeline active": with DisableVanillaTonemapping off the + * pipeline still runs its effects and then hands off to the vanilla tonemap. + */ + bool WantsTonemapOwnership() const; + + /** + * @brief Builds the shared-buffer payload, masking flags the arbiter has revoked. + * + * DisableVanillaTonemapping is forced to 0 unless Post Processing actually owns the + * tonemap, so ISHDR and HDROutputCS do not assume a linear, already-tonemapped scene + * when another feature produced the image. + */ + Settings GetCommonBufferData(); + + json pendingSettings = {}; + + void ProcessSettings(json& o_json); + + std::vector presets = {}; + std::vector LoadPresets(); + void SavePresetTo(std::string a_name); + void LoadPresetFrom(std::string a_name); + + enum class FeaturePipelineIndex : size_t + { + DoF, + Vignette, + LocalExposure, + AutoExposure, + MotionBlur, + PhysicalGlare, + CODBloom, + LensFlare, + Composite, + ColorGrading, + LUT, + Camera, + Border, + COUNT + }; + + std::array, static_cast(FeaturePipelineIndex::COUNT)> pipeline; + + BokehResources bokehResources; + + template + T* GetPipelineFeature(FeaturePipelineIndex idx) + { + return static_cast(pipeline[static_cast(idx)].get()); + } + + virtual void ClearShaderCache() override; + + virtual void SetupResources() override; + virtual void Reset() override; + + virtual void PostPostLoad() override; + virtual void Prepass() override; + + void PreProcess(RE::RENDER_TARGET a_input); + void DrawBeforeUpscaling(); + void ClearBorderMotionVectorsForFrameGen(); + void DrawFeature(PostProcessFeature& feature, PostProcessFeature::TextureInfo& lastTexColor); + + /// Copy lastTexColor to a render target, performing format conversion via copyCS if needed. + void CopyToRenderTarget( + RE::BSGraphics::RenderTargetData& targetRT, + Texture2D* convertTex, + ID3D11Texture2D* srcTex, + ID3D11ShaderResourceView* srcSRV); + + ///////////////////////////////////////////////////////////////////////////////// + + bool bypass = false; + bool isrefraction = false; + + struct ImageSpaceManager + { + RE::ImageSpaceData gameISData; + }; + + std::unique_ptr imageSpaceManager = std::make_unique(); + + eastl::unique_ptr texCopyMain = nullptr; + eastl::unique_ptr texCopyMainCopy = nullptr; + eastl::unique_ptr texAfterTAA = nullptr; + winrt::com_ptr copyCS = nullptr; + + ///////////////////////////////////////////////////////////////////////////////// + + // Tonemap-time entry is driven by PostProcessingExtensions::Main_HDRTonemapBlendCinematic_Render + // (Hooks.cpp), which arbitrates between this feature and Effects11. Only the refraction + // hook remains here, and it just flags which buffer the scene currently lives in. + + struct BSImagespaceShaderRefraction_SetupTechnique + { + static void thunk(RE::BSShader* a_shader, RE::BSShaderMaterial* a_material) + { + globals::features::postProcessing.isrefraction = true; + func(a_shader, a_material); + } + static inline REL::Relocation func; + }; +}; diff --git a/src/Features/PostProcessing/BokehResources.cpp b/src/Features/PostProcessing/BokehResources.cpp new file mode 100644 index 0000000000..8850000e3b --- /dev/null +++ b/src/Features/PostProcessing/BokehResources.cpp @@ -0,0 +1,127 @@ +#include "BokehResources.h" +#include "Util.h" + +#include + +void BokehResources::Setup() +{ + auto device = globals::d3d::device; + + logger::debug("BokehResources: Loading built-in bokeh shapes..."); + for (int i = 0; i < NUM_BUILTIN_SHAPES; i++) { + auto shapePath = bokehShapesPath / builtinShapeFiles[i]; + LoadTextureFromFile(shapePath, i); + } + numLoadedShapes = NUM_BUILTIN_SHAPES; + + // Load any previously saved custom shapes + for (int i = 0; i < MAX_CUSTOM_SHAPES; i++) { + if (!customShapePaths[i].empty()) { + LoadTextureFromFile(std::filesystem::path(customShapePaths[i]), NUM_BUILTIN_SHAPES + i); + if (texBokehShapes[NUM_BUILTIN_SHAPES + i]) + numLoadedShapes = std::max(numLoadedShapes, NUM_BUILTIN_SHAPES + i + 1); + } + } + + logger::debug("BokehResources: Creating sampler..."); + { + D3D11_SAMPLER_DESC samplerDesc = { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_MIRROR, + .AddressV = D3D11_TEXTURE_ADDRESS_MIRROR, + .AddressW = D3D11_TEXTURE_ADDRESS_MIRROR, + .MaxAnisotropy = 1, + .MinLOD = 0, + .MaxLOD = D3D11_FLOAT32_MAX + }; + DX::ThrowIfFailed(device->CreateSamplerState(&samplerDesc, bokehSampler.put())); + } +} + +bool BokehResources::LoadTextureFromFile(const std::filesystem::path& path, int index) +{ + if (index < 0 || index >= MAX_SHAPES) + return false; + + auto device = globals::d3d::device; + + DirectX::ScratchImage image; + try { + DX::ThrowIfFailed(DirectX::LoadFromWICFile(path.c_str(), DirectX::WIC_FLAGS_NONE, nullptr, image)); + } catch (std::runtime_error& e) { + logger::warn("BokehResources: Error loading bokeh shape {}: {}", path.string(), e.what()); + return false; + } + + ID3D11Resource* pRsrc = nullptr; + try { + DX::ThrowIfFailed(CreateTexture(device, image.GetImages(), image.GetImageCount(), image.GetMetadata(), &pRsrc)); + } catch (std::runtime_error& e) { + logger::warn("BokehResources: Error creating texture for bokeh shape {}: {}", path.string(), e.what()); + return false; + } + + texBokehShapes[index] = eastl::make_unique(reinterpret_cast(pRsrc)); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texBokehShapes[index]->desc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { + .MostDetailedMip = 0, + .MipLevels = 1 } + }; + texBokehShapes[index]->CreateSRV(srvDesc); + return true; +} + +bool BokehResources::LoadCustomShape(const std::string& filePath, int slotIndex) +{ + if (slotIndex < 0 || slotIndex >= MAX_CUSTOM_SHAPES) + return false; + + // Validate file extension + auto ext = std::filesystem::path(filePath).extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + if (ext != ".png" && ext != ".dds" && ext != ".jpg" && ext != ".jpeg" && ext != ".bmp" && ext != ".tga") { + logger::warn("BokehResources: Unsupported file format: {}", ext); + return false; + } + + // Validate that path doesn't traverse outside expected directories + auto absPath = std::filesystem::absolute(filePath); + if (!std::filesystem::exists(absPath)) { + logger::warn("BokehResources: File does not exist: {}", absPath.string()); + return false; + } + + int index = NUM_BUILTIN_SHAPES + slotIndex; + if (LoadTextureFromFile(absPath, index)) { + customShapePaths[slotIndex] = absPath.string(); + // Derive display name from filename without extension + customShapeNames[slotIndex] = absPath.stem().string(); + numLoadedShapes = std::max(numLoadedShapes, index + 1); + return true; + } + return false; +} + +ID3D11ShaderResourceView* BokehResources::GetShapeSRV(int shapeIndex) const +{ + if (shapeIndex < 0 || shapeIndex >= MAX_SHAPES) + return nullptr; + if (!texBokehShapes[shapeIndex]) + return nullptr; + return texBokehShapes[shapeIndex]->srv.get(); +} + +const char* BokehResources::GetShapeName(int index) const +{ + if (index < 0 || index >= MAX_SHAPES) + return "Unknown"; + if (index < NUM_BUILTIN_SHAPES) + return builtinShapeNames[index].c_str(); + int customIdx = index - NUM_BUILTIN_SHAPES; + if (customIdx < MAX_CUSTOM_SHAPES && !customShapeNames[customIdx].empty()) + return customShapeNames[customIdx].c_str(); + return "Empty Slot"; +} diff --git a/src/Features/PostProcessing/BokehResources.h b/src/Features/PostProcessing/BokehResources.h new file mode 100644 index 0000000000..1746119bb4 --- /dev/null +++ b/src/Features/PostProcessing/BokehResources.h @@ -0,0 +1,46 @@ +#pragma once + +#include "Buffer.h" + +struct BokehResources +{ + static constexpr int NUM_BUILTIN_SHAPES = 6; + static constexpr int MAX_CUSTOM_SHAPES = 4; + static constexpr int MAX_SHAPES = NUM_BUILTIN_SHAPES + MAX_CUSTOM_SHAPES; + + std::array, MAX_SHAPES> texBokehShapes = {}; + int numLoadedShapes = NUM_BUILTIN_SHAPES; + + const std::filesystem::path bokehShapesPath = "Data\\Shaders\\PostProcessing\\DoF\\bokehshapes"; + std::array builtinShapeFiles = { + "moyheart.png", + "hex.png", + "fringy_soft_chr_rb.png", + "hex_fringy_soft.png", + "cutestar.png", + "square.png" + }; + std::array builtinShapeNames = { + "Heart", + "Hexagon", + "Fringy Soft", + "Hex Fringy Soft", + "Star", + "Square" + }; + + std::array customShapePaths = {}; + std::array customShapeNames = {}; + + winrt::com_ptr bokehSampler = nullptr; + + void Setup(); + bool LoadCustomShape(const std::string& filePath, int slotIndex); + ID3D11ShaderResourceView* GetShapeSRV(int shapeIndex) const; + int GetTotalShapeCount() const { return numLoadedShapes; } + + const char* GetShapeName(int index) const; + +private: + bool LoadTextureFromFile(const std::filesystem::path& path, int index); +}; diff --git a/src/Features/PostProcessing/Border.cpp b/src/Features/PostProcessing/Border.cpp new file mode 100644 index 0000000000..9f697de7ce --- /dev/null +++ b/src/Features/PostProcessing/Border.cpp @@ -0,0 +1,220 @@ +#include "Border.h" + +#include "Deferred.h" +#include "I18n/I18n.h" +#include "State.h" +#include "Util.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Border::Settings, + BorderColor, + DepthThreshold, + Scale) + +void Border::DrawSettings() +{ + ImGui::ColorEdit3(T("feature.post_processing.border.border_color", "Border Color"), reinterpret_cast(&settings.BorderColor)); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.border.the_color_of_the_border", "The color of the border.")); + + ImGui::SliderFloat(T("feature.post_processing.border.depth_threshold", "Depth Threshold"), &settings.DepthThreshold, 0.f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.border.the_depth_threshold_for_the_border_effect", "The depth threshold for the border effect.")); + + ImGui::SliderFloat4(T("feature.post_processing.border.scale_top_down_left_right", "Scale (Top, Down, Left, Right)"), reinterpret_cast(&settings.Scale), 0.f, 0.5f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.border.the_scale_of_the_border_on_each_side", "The scale of the border on each side of the screen.")); +} + +void Border::RestoreDefaultSettings() +{ + settings = {}; +} + +void Border::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void Border::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void Border::SetupResources() +{ + auto renderer = globals::game::renderer; + + logger::debug("Creating buffers..."); + { + borderCB = eastl::make_unique(ConstantBufferDesc()); + } + + logger::debug("Creating 2D textures..."); + { + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + D3D11_TEXTURE2D_DESC texDesc; + gameTexMainCopy.texture->GetDesc(&texDesc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + texDesc.MipLevels = srvDesc.Texture2D.MipLevels = 1; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + + texOutput = eastl::make_unique(texDesc); + texOutput->CreateSRV(srvDesc); + texOutput->CreateUAV(uavDesc); + } + + CompileComputeShaders(); +} + +void Border::ClearShaderCache() +{ + const auto shaderPtrs = std::array{ + &borderCS, + &borderClearMVCS + }; + + for (auto shader : shaderPtrs) + if ((*shader)) { + (*shader)->Release(); + shader->detach(); + } + + CompileComputeShaders(); +} + +void Border::CompileComputeShaders() +{ + struct ShaderCompileInfo + { + winrt::com_ptr* programPtr; + std::string_view filename; + std::vector> defines = {}; + std::string entry = "main"; + }; + + std::vector + shaderInfos = { + { &borderCS, "border.cs.hlsl" }, + { &borderClearMVCS, "border_clear_mv.cs.hlsl" }, + }; + + for (auto& info : shaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\Border") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } +} + +void Border::ClearMotionVectorsForFrameGen() +{ + if (!borderClearMVCS || !borderCB) + return; + + // Only run when there's an actual border to clear + if (settings.Scale.x <= 0.f && settings.Scale.y <= 0.f && settings.Scale.z <= 0.f && settings.Scale.w <= 0.f) + return; + + auto renderer = globals::game::renderer; + auto context = globals::d3d::context; + + // Compute dynamic resolution dimensions (actual rendered area before upscaling) + float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; + auto dynResDim = Util::ConvertToDynamic(screenSize); + + BorderCB data = { + .BorderColor = float4(settings.BorderColor.x, settings.BorderColor.y, settings.BorderColor.z, settings.DepthThreshold), + .Scale = settings.Scale + }; + borderCB->Update(data); + + auto* depthSRV = Util::GetCurrentSceneDepthSRV(false); + if (!depthSRV) { + return; + } + auto motion = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + + // Bind SharedData (b5) and FrameBuffer (b12) for CS stage — shader needs + // BufferDim and DynamicResolutionParams1 to compute dynamic resolution area. + auto* sharedDataBuf = globals::state->sharedDataCB->CB(); + context->CSSetConstantBuffers(5, 1, &sharedDataBuf); + ID3D11Buffer* perFrameBuf = *globals::game::perFrame.get(); + context->CSSetConstantBuffers(12, 1, &perFrameBuf); + + ID3D11ShaderResourceView* srvs[1] = { depthSRV }; + context->CSSetShaderResources(0, 1, srvs); + ID3D11UnorderedAccessView* uavs[1] = { motion.UAV }; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + ID3D11Buffer* cb = borderCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetShader(borderClearMVCS.get(), nullptr, 0); + + context->Dispatch(((uint)dynResDim.x + 7) >> 3, ((uint)dynResDim.y + 7) >> 3, 1); + + srvs[0] = nullptr; + uavs[0] = nullptr; + cb = nullptr; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + context->CSSetShaderResources(0, 1, srvs); + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetShader(nullptr, nullptr, 0); +} + +void Border::Draw(TextureInfo& inout_tex) +{ + globals::profiler->BeginPass("PostProcessing::Border"); + auto renderer = globals::game::renderer; + auto context = globals::d3d::context; + + float2 res = { (float)texOutput->desc.Width, (float)texOutput->desc.Height }; + res = Util::ConvertToDynamic(res); + + BorderCB data = { + .BorderColor = float4(settings.BorderColor.x, settings.BorderColor.y, settings.BorderColor.z, settings.DepthThreshold), + .Scale = settings.Scale + }; + borderCB->Update(data); + + auto* depthSRV = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].depthSRV; + if (!depthSRV) { + globals::profiler->EndPass(); + return; + } + auto motion = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + ID3D11ShaderResourceView* srvs[2] = { inout_tex.srv, depthSRV }; + context->CSSetShaderResources(0, 2, srvs); + ID3D11UnorderedAccessView* uavs[2] = { texOutput->uav.get(), motion.UAV }; + context->CSSetUnorderedAccessViews(0, 2, uavs, nullptr); + ID3D11Buffer* cb = borderCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetShader(borderCS.get(), nullptr, 0); + + context->Dispatch(((uint)res.x + 7) >> 3, ((uint)res.y + 7) >> 3, 1); + + srvs[0] = nullptr; + srvs[1] = nullptr; + uavs[0] = nullptr; + uavs[1] = nullptr; + cb = nullptr; + context->CSSetUnorderedAccessViews(0, 2, uavs, nullptr); + context->CSSetShaderResources(0, 2, srvs); + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetShader(nullptr, nullptr, 0); + + inout_tex = { texOutput->resource.get(), texOutput->srv.get() }; + globals::profiler->EndPass(); +} diff --git a/src/Features/PostProcessing/Border.h b/src/Features/PostProcessing/Border.h new file mode 100644 index 0000000000..bd3f633ca8 --- /dev/null +++ b/src/Features/PostProcessing/Border.h @@ -0,0 +1,48 @@ +#pragma once + +#include "Buffer.h" +#include "PostProcessFeature.h" + +struct Border : public PostProcessFeature +{ + virtual inline std::string GetType() const override { return "Border"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.border.name", "Border"); } + virtual inline std::string GetDesc() const override { return T("feature.post_processing.border.description", "Add colored border, optionally with depth threshold."); } + virtual inline bool DrawAfterColorGrading() const override { return true; } + virtual inline bool DisableInMainLoadingMenu() const override { return true; } + + struct Settings + { + float3 BorderColor; + float DepthThreshold; + float4 Scale; + } settings; + + struct alignas(16) BorderCB + { + float4 BorderColor; + float4 Scale; + }; + eastl::unique_ptr borderCB = nullptr; + + eastl::unique_ptr texOutput = nullptr; + + winrt::com_ptr borderCS = nullptr; + winrt::com_ptr borderClearMVCS = nullptr; + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json&) override; + virtual void SaveSettings(json&) override; + + virtual void DrawSettings() override; + + virtual void Draw(TextureInfo&) override; + + /// Clear motion vectors in border areas before frame generation copies them. + /// Must be called before CopySharedD3D12Resources to prevent FrameGen artifacts. + void ClearMotionVectorsForFrameGen(); +}; diff --git a/src/Features/PostProcessing/CODBloom.cpp b/src/Features/PostProcessing/CODBloom.cpp new file mode 100644 index 0000000000..5b09024b4e --- /dev/null +++ b/src/Features/PostProcessing/CODBloom.cpp @@ -0,0 +1,299 @@ +#include "CODBloom.h" + +#include "I18n/I18n.h" +#include "State.h" +#include "Util.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + CODBloom::Settings, + Threshold, + UpsampleRadius, + BlendFactor, + MipBlendFactor) + +void CODBloom::DrawSettings() +{ + ImGui::SliderFloat(T("feature.post_processing.codbloom.threshold", "Threshold"), &settings.Threshold, -7.f, 23.f, "%+.2f EV100"); + ImGui::SliderFloat(T("feature.post_processing.codbloom.upsampling_radius", "Upsampling Radius"), &settings.UpsampleRadius, 1.f, 5.f, "%.1f px"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.codbloom.a_greater_radius_makes_the_bloom_slightly_blurrier", "A greater radius makes the bloom slightly blurrier.")); + + ImGui::SliderFloat(T("feature.post_processing.codbloom.mix", "Mix"), &settings.BlendFactor, 0.f, 1.f, "%.2f"); + + ImGui::Separator(); + + static int mipLevel = 1; + ImGui::SliderInt(T("feature.post_processing.codbloom.mip_level", "Mip Level"), &mipLevel, 1, (int)settings.MipBlendFactor.size() + 1, "%d", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.codbloom.the_greater_the_level_the_blurrier_the_part", "The greater the level, the blurrier the part it controls")); + ImGui::Indent(); + { + ImGui::SliderFloat(T("feature.post_processing.codbloom.intensity", "Intensity"), &settings.MipBlendFactor[mipLevel - 1], 0.f, 1.f, "%.2f"); + } + ImGui::Unindent(); + + if (ImGui::CollapsingHeader(T("feature.post_processing.codbloom.debug", "Debug"))) { + static int mip = 0; + ImGui::SliderInt(T("feature.post_processing.codbloom.debug_mip_level", "Debug Mip Level"), &mip, 0, (int)s_BloomMips - 1, "%d", ImGuiSliderFlags_NoInput | ImGuiSliderFlags_AlwaysClamp); + + ImGui::BulletText(T("feature.post_processing.codbloom.texbloom", "texBloom")); + ImGui::Image(texBloomMipSRVs[mip].get(), { texBloom->desc.Width * .2f, texBloom->desc.Height * .2f }); + } +} + +void CODBloom::RestoreDefaultSettings() +{ + settings = {}; +} + +void CODBloom::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void CODBloom::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void CODBloom::SetupResources() +{ + auto renderer = globals::game::renderer; + auto device = globals::d3d::device; + + logger::debug("Creating buffers..."); + { + bloomCB = std::make_unique(ConstantBufferDesc()); + } + + logger::debug("Creating 2D textures..."); + { + // texBloom for bloom mip chain + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + D3D11_TEXTURE2D_DESC texDesc; + gameTexMainCopy.texture->GetDesc(&texDesc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + texDesc.MipLevels = srvDesc.Texture2D.MipLevels = s_BloomMips; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + + texBloom = std::make_unique(texDesc); + texBloom->CreateSRV(srvDesc); + + // SRV for each mip + for (uint i = 0; i < s_BloomMips; i++) { + D3D11_SHADER_RESOURCE_VIEW_DESC mipSrvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = i, .MipLevels = 1 } + }; + DX::ThrowIfFailed(device->CreateShaderResourceView(texBloom->resource.get(), &mipSrvDesc, texBloomMipSRVs[i].put())); + } + + // UAV for each mip + for (uint i = 0; i < s_BloomMips; i++) { + D3D11_UNORDERED_ACCESS_VIEW_DESC mipUavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = i } + }; + DX::ThrowIfFailed(device->CreateUnorderedAccessView(texBloom->resource.get(), &mipUavDesc, texBloomMipUAVs[i].put())); + } + } + + logger::debug("Creating samplers..."); + { + D3D11_SAMPLER_DESC samplerDesc = { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_BORDER, + .AddressV = D3D11_TEXTURE_ADDRESS_BORDER, + .AddressW = D3D11_TEXTURE_ADDRESS_BORDER, + .MaxAnisotropy = 1, + .MinLOD = 0, + .MaxLOD = D3D11_FLOAT32_MAX + }; + + DX::ThrowIfFailed(device->CreateSamplerState(&samplerDesc, colorSampler.put())); + } + + CompileComputeShaders(); +} + +void CODBloom::ClearShaderCache() +{ + auto const shaderPtrs = std::array{ + &thresholdCS, &downsampleCS, &downsampleFirstMipCS, &upsampleCS, &compositeCS + }; + + for (auto shader : shaderPtrs) + if ((*shader)) { + (*shader)->Release(); + shader->detach(); + } + + CompileComputeShaders(); +} + +void CODBloom::CompileComputeShaders() +{ + struct ShaderCompileInfo + { + winrt::com_ptr* programPtr; + std::string_view filename; + std::vector> defines; + std::string entry = "main"; + }; + + std::vector + shaderInfos = { + { &thresholdCS, "bloom.cs.hlsl", {}, "CS_Threshold" }, + { &downsampleCS, "bloom.cs.hlsl", {}, "CS_Downsample" }, + { &downsampleFirstMipCS, "bloom.cs.hlsl", { { "FIRST_MIP", "" } }, "CS_Downsample" }, + { &upsampleCS, "bloom.cs.hlsl", {}, "CS_Upsample" }, + { &compositeCS, "bloom.cs.hlsl", {}, "CS_Composite" } + }; + + for (auto& info : shaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\CODBloom") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } +} + +void CODBloom::Draw(TextureInfo& inout_tex) +{ + auto state = globals::state; + auto context = globals::d3d::context; + + state->BeginPerfEvent("COD Bloom"); + + // update cb + BloomCB cbData = { + .Threshold = exp2(settings.Threshold - 3.0f), + .UpsampleRadius = settings.UpsampleRadius, + .UpsampleMult = 1.f, + .CurrentMipMult = 1.f + }; + bloomCB->Update(cbData); + + ////////////////////////////////////////////////////////////////////////////// + + std::array srvs = { nullptr }; + std::array uavs = { nullptr }; + std::array samplers = { colorSampler.get() }; + auto cb = bloomCB->CB(); + + auto resetViews = [&]() { + srvs.fill(nullptr); + uavs.fill(nullptr); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + }; + + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); + + // Threshold + { + globals::profiler->BeginPass("PostProcessing::CODBloom::Threshold"); + srvs.at(0) = inout_tex.srv; + uavs.at(0) = texBloomMipUAVs[0].get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(thresholdCS.get(), nullptr, 0); + context->Dispatch(((texBloom->desc.Width - 1) >> 5) + 1, ((texBloom->desc.Height - 1) >> 5) + 1, 1); + globals::profiler->EndPass(); + } + + // Downsample + globals::profiler->BeginPass("PostProcessing::CODBloom::Downsample"); + context->CSSetShader(downsampleFirstMipCS.get(), nullptr, 0); + for (int i = 0; i < s_BloomMips - 1; i++) { + resetViews(); + + srvs.at(1) = texBloomMipSRVs[i].get(); + uavs.at(0) = texBloomMipUAVs[i + 1].get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + if (i == 1) + context->CSSetShader(downsampleCS.get(), nullptr, 0); + + uint mipWidth = texBloom->desc.Width >> (i + 1); + uint mipHeight = texBloom->desc.Height >> (i + 1); + context->Dispatch(((mipWidth - 1) >> 5) + 1, ((mipHeight - 1) >> 5) + 1, 1); + } + globals::profiler->EndPass(); + + // upsample + globals::profiler->BeginPass("PostProcessing::CODBloom::Upsample"); + context->CSSetShader(upsampleCS.get(), nullptr, 0); + for (int i = s_BloomMips - 2; i >= 1; i--) { + resetViews(); + + cbData.UpsampleMult = 1.f; + if (i == s_BloomMips - 2) + cbData.UpsampleMult = settings.MipBlendFactor[i]; + cbData.CurrentMipMult = settings.MipBlendFactor[i - 1]; + bloomCB->Update(cbData); + + srvs.at(1) = texBloomMipSRVs[i + 1].get(); + uavs.at(0) = texBloomMipUAVs[i].get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + uint mipWidth = texBloom->desc.Width >> i; + uint mipHeight = texBloom->desc.Height >> i; + context->Dispatch(((mipWidth - 1) >> 5) + 1, ((mipHeight - 1) >> 5) + 1, 1); + } + + // upsample final mip to mip 0 with blend factor applied (CurrentMipMult=0 to discard threshold data in mip 0) + { + resetViews(); + + cbData.UpsampleMult = settings.BlendFactor; + cbData.CurrentMipMult = 0.f; + bloomCB->Update(cbData); + + srvs.at(1) = texBloomMipSRVs[1].get(); + uavs.at(0) = texBloomMipUAVs[0].get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->Dispatch(((texBloom->desc.Width - 1) >> 5) + 1, ((texBloom->desc.Height - 1) >> 5) + 1, 1); + } + globals::profiler->EndPass(); + + // cleanup + resetViews(); + + samplers.fill(nullptr); + cb = nullptr; + + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); + context->CSSetShader(nullptr, nullptr, 0); + + // return + inout_tex = { texBloom->resource.get(), texBloomMipSRVs[0].get() }; + + state->EndPerfEvent(); +} diff --git a/src/Features/PostProcessing/CODBloom.h b/src/Features/PostProcessing/CODBloom.h new file mode 100644 index 0000000000..e6f1d31af3 --- /dev/null +++ b/src/Features/PostProcessing/CODBloom.h @@ -0,0 +1,61 @@ +#pragma once + +#include "PostProcessFeature.h" + +#include "Buffer.h" + +struct CODBloom : public PostProcessFeature +{ + virtual inline std::string GetType() const override { return "COD Bloom"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.codbloom.name", "COD Bloom"); } + virtual inline std::string GetDesc() const override { return T("feature.post_processing.codbloom.description", "Bloom effect used in Call of Duty: Advanced Warfare. Expect HDR linear RGB inputs."); } + virtual bool WritesToMainTexture() const override { return false; } + + TextureInfo GetBloomOutput() const { return { texBloom->resource.get(), texBloomMipSRVs[0].get() }; } + + constexpr static size_t s_BloomMips = 9; + + struct Settings + { + // bloom & lens + float Threshold = 3.f; // EV100 (0 EV100 = 0.125 linear luminance) + float UpsampleRadius = 2.f; + float BlendFactor = .01f; + std::array MipBlendFactor = { 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f }; + } settings; + + struct alignas(16) BloomCB + { + // threshold + float Threshold; + // upsample + float UpsampleRadius; + float UpsampleMult; // in composite: bloom mult + float CurrentMipMult; + }; + std::unique_ptr bloomCB = nullptr; + + winrt::com_ptr colorSampler = nullptr; + + std::unique_ptr texBloom = nullptr; + std::array, s_BloomMips> texBloomMipSRVs = { nullptr }; + std::array, s_BloomMips> texBloomMipUAVs = { nullptr }; + + winrt::com_ptr thresholdCS = nullptr; + winrt::com_ptr downsampleCS = nullptr; + winrt::com_ptr downsampleFirstMipCS = nullptr; + winrt::com_ptr upsampleCS = nullptr; + winrt::com_ptr compositeCS = nullptr; + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json&) override; + virtual void SaveSettings(json&) override; + + virtual void DrawSettings() override; + + virtual void Draw(TextureInfo&) override; +}; diff --git a/src/Features/PostProcessing/Camera.cpp b/src/Features/PostProcessing/Camera.cpp new file mode 100644 index 0000000000..7f37a2b8c4 --- /dev/null +++ b/src/Features/PostProcessing/Camera.cpp @@ -0,0 +1,209 @@ +#include "Camera.h" + +#include "I18n/I18n.h" +#include "State.h" +#include "Util.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Camera::Settings, + UseFE, + FEFoV, + FECrop, + CAStrength, + NoiseStrength, + NoiseType) + +void Camera::DrawSettings() +{ + ImGui::Checkbox(T("feature.post_processing.camera.fisheye", "Fisheye"), &settings.UseFE); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", T("feature.post_processing.camera.enable_fisheye_effect", "Enable fisheye effect.")); + } + + if (settings.UseFE) { + ImGui::SliderFloat(T("feature.post_processing.camera.fov", "FOV"), &settings.FEFoV, 20.0f, 180.0f, "%1.0f °"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", T("feature.post_processing.camera.fov_in_degrees_set_to_in_game_fov", "FOV in degrees.\n\nSet to in-game FOV.")); + } + + ImGui::SliderFloat(T("feature.post_processing.camera.crop", "Crop"), &settings.FECrop, 0.0f, 1.0f, "%.3f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", T("feature.post_processing.camera.how_much_to_crop_into_the_image", "How much to crop into the image.\n\n0 = circular, 1 = full-frame.")); + } + } + + ImGui::SliderFloat(T("feature.post_processing.camera.ca_amount", "CA amount"), &settings.CAStrength, 0.0f, 1.0f, "%.3f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", T("feature.post_processing.camera.chromatic_aberration_strength", "Chromatic aberration strength.")); + } + + ImGui::SliderFloat(T("feature.post_processing.camera.noise_amount", "Noise amount"), &settings.NoiseStrength, 0.0f, 1.0f, "%.3f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", T("feature.post_processing.camera.amount_of_noise_to_apply", "Amount of noise to apply.")); + } + + ImGui::Combo(T("feature.post_processing.camera.noise_type", "Noise type"), &settings.NoiseType, "Film grain\0Color grain\0\0"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", T("feature.post_processing.camera.type_of_noise_to_apply", "Type of noise to apply.")); + } +} + +void Camera::RestoreDefaultSettings() +{ + settings = {}; +} + +void Camera::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void Camera::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void Camera::SetupResources() +{ + auto renderer = globals::game::renderer; + auto device = globals::d3d::device; + + logger::debug("Creating buffers..."); + { + cameraCB = eastl::make_unique(ConstantBufferDesc()); + } + + logger::debug("Creating 2D textures..."); + { + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + D3D11_TEXTURE2D_DESC texDesc; + gameTexMainCopy.texture->GetDesc(&texDesc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + texDesc.MipLevels = srvDesc.Texture2D.MipLevels = 1; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + + texOutput = eastl::make_unique(texDesc); + texOutput->CreateSRV(srvDesc); + texOutput->CreateUAV(uavDesc); + } + + logger::debug("Creating samplers..."); + { + D3D11_SAMPLER_DESC samplerDesc = { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_BORDER, + .AddressV = D3D11_TEXTURE_ADDRESS_BORDER, + .AddressW = D3D11_TEXTURE_ADDRESS_BORDER, + .MaxAnisotropy = 1, + .MinLOD = 0, + .MaxLOD = D3D11_FLOAT32_MAX + }; + + DX::ThrowIfFailed(device->CreateSamplerState(&samplerDesc, colorSampler.put())); + } + + logger::debug("Creating compute shaders..."); + { + CompileComputeShaders(); + } +} + +void Camera::ClearShaderCache() +{ + const auto shaderPtrs = std::array{ + &cameraCS + }; + + for (auto shader : shaderPtrs) + if ((*shader)) { + (*shader)->Release(); + shader->detach(); + } + + CompileComputeShaders(); +} + +void Camera::CompileComputeShaders() +{ + struct ShaderCompileInfo + { + winrt::com_ptr* programPtr; + std::string_view filename; + std::vector> defines = {}; + std::string entry = "main"; + }; + + std::vector + shaderInfos = { + { &cameraCS, "camera.cs.hlsl", {}, "CS_Camera" } + }; + + for (auto& info : shaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\Camera") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } + + if (!cameraCS) { + logger::error("Failed to compile camera compute shader!"); + return; + } +} + +void Camera::Draw(TextureInfo& inout_tex) +{ + globals::profiler->BeginPass("PostProcessing::Camera"); + auto context = globals::d3d::context; + float2 res = { (float)texOutput->desc.Width, (float)texOutput->desc.Height }; + res = Util::ConvertToDynamic(res); + + CameraCB data = { + .FEFoV = settings.FEFoV, + .FECrop = settings.FECrop, + .CAStrength = settings.CAStrength, + .NoiseStrength = settings.NoiseStrength, + .NoiseType = settings.NoiseType, + .res = res, + .UseFE = settings.UseFE + }; + + cameraCB->Update(data); + + ID3D11ShaderResourceView* srv = inout_tex.srv; + ID3D11UnorderedAccessView* uav = texOutput->uav.get(); + ID3D11Buffer* cb = cameraCB->CB(); + + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetShaderResources(0, 1, &srv); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + + context->CSSetShader(cameraCS.get(), nullptr, 0); + context->Dispatch(((uint)res.x + 7) >> 3, ((uint)res.y + 7) >> 3, 1); + + srv = nullptr; + uav = nullptr; + cb = nullptr; + + inout_tex = { texOutput->resource.get(), texOutput->srv.get() }; + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShaderResources(0, 1, &srv); + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetShader(nullptr, nullptr, 0); + + inout_tex = { texOutput->resource.get(), texOutput->srv.get() }; + globals::profiler->EndPass(); +} diff --git a/src/Features/PostProcessing/Camera.h b/src/Features/PostProcessing/Camera.h new file mode 100644 index 0000000000..265dba8bc3 --- /dev/null +++ b/src/Features/PostProcessing/Camera.h @@ -0,0 +1,60 @@ +#pragma once + +#include "Buffer.h" +#include "PostProcessFeature.h" + +struct Camera : public PostProcessFeature +{ + virtual inline std::string GetType() const override { return "Camera"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.camera.name", "Camera"); } + virtual inline std::string GetDesc() const override { return T("feature.post_processing.camera.description", "Camera effects including fisheye, chromatic aberration, and film grain."); } + virtual inline bool DrawAfterColorGrading() const override { return true; } + + struct Settings + { + // Fisheye + bool UseFE = false; + uint8_t pad[3]; + float FEFoV = 90.0f; + float FECrop = 0.0f; + + // Chromatic aberration + float CAStrength = 0.04f; + + // Noise + float NoiseStrength = 0.08f; + int NoiseType = 0; + } settings; + + struct alignas(16) CameraCB + { + float FEFoV; + float FECrop; + float CAStrength; + float NoiseStrength; + int NoiseType; + float2 res; + bool UseFE; + uint8_t pad[3]; + }; + + eastl::unique_ptr cameraCB = nullptr; + + eastl::unique_ptr texOutput = nullptr; + + winrt::com_ptr cameraCS = nullptr; + + winrt::com_ptr colorSampler = nullptr; + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json&) override; + virtual void SaveSettings(json&) override; + + virtual void DrawSettings() override; + + virtual void Draw(TextureInfo&) override; +}; diff --git a/src/Features/PostProcessing/ColorGrading.cpp b/src/Features/PostProcessing/ColorGrading.cpp new file mode 100644 index 0000000000..1303dd70b3 --- /dev/null +++ b/src/Features/PostProcessing/ColorGrading.cpp @@ -0,0 +1,1104 @@ +#include "ColorGrading.h" + +#include "State.h" +#include "Util.h" + +#include "ColorSpace.h" +#include "Features/HDRDisplay.h" +#include "Features/LinearLighting.h" +#include "Features/PostProcessing.h" +#include "Menu.h" +#include "OpenDRTIo.h" + +#include +#include +#include + +#include "IconsFontAwesome5.h" + +#define I18N_KEY_PREFIX "feature.post_processing.color_grading." + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + ColorGrading::Settings, + skipLDR, + skipLUT, + slope, + power, + cdlOffset, + lift, + gamma, + gain, + inOutGamma, + oklchSaturation, + oklchColorMixer, + contrast, + pivot, + exposureTemperatureTint, + shadowsGain, + midtonesGain, + highlightsGain, + shadowsHighlightsRange, + shadowsOffset, + midtonesOffset, + highlightsOffset, + useOpenDrt, + currentTonemapper, + tonemapParams, + gameCinematicBlend, + gameFadeBlend, + gameTintBlend, + useLog, + logType, + invertLog, + enableTonemap, + processColorSpace) + +template +bool exposureSlider(float* val) +{ + float tempVal[num]; + for (int i = 0; i < num; i++) + tempVal[i] = log2(val[i]); + + bool retval; + if constexpr (num == 1) + retval = ImGui::SliderFloat(T(TKEY("exposure"), "Exposure"), tempVal, -4.f, 4.f, "%+.2f EV"); + else if constexpr (num == 2) + retval = Util::ShiftSlider<2>(T(TKEY("exposure"), "Exposure"), tempVal, -4.f, 4.f, "%+.2f EV"); + else if constexpr (num == 3) + retval = Util::ShiftSlider<3>(T(TKEY("exposure"), "Exposure"), tempVal, -4.f, 4.f, "%+.2f EV"); + else if constexpr (num == 4) + retval = Util::ShiftSlider<4>(T(TKEY("exposure"), "Exposure"), tempVal, -4.f, 4.f, "%+.2f EV"); + + for (int i = 0; i < num; i++) + val[i] = exp2(tempVal[i]); + + return retval; +} + +void drawHDRStatus() +{ + auto& hdr = globals::features::hdrDisplay; + if (hdr.loaded && hdr.settings.enableHDR) { + auto hdrOutputActive = std::format("{} {}", ICON_FA_CHECK, T("feature.post_processing.color_grading.hdr_output_active", "HDR Output Active")); + ImGui::TextColored(Util::Colors::GetSuccess(), "%s", hdrOutputActive.c_str()); + ImGui::Text(T("feature.post_processing.color_grading.paper_white_nits_from_hdr_settings", "Paper White: %.0f nits (from HDR settings)"), static_cast(hdr.settings.hdrPaperWhite)); + ImGui::Text(T("feature.post_processing.color_grading.peak_brightness_nits_from_hdr_settings", "Peak Brightness: %.0f nits (from HDR settings)"), static_cast(hdr.settings.hdrPeakNits)); + } else { + ImGui::TextColored(Util::Colors::GetDisabled(), "%s", T("feature.post_processing.color_grading.sdr_output_hdr_display_not_enabled", "SDR Output (HDR Display not enabled)")); + } +} + +// Profjack Design +struct TonemapperInfo +{ + std::string_view name; + std::string_view func_name; + std::string_view desc; + int nativeInputSpace; // color space the tonemapper expects as input + int nativeOutputSpace; // color space the tonemapper produces as output + bool supportsHDR; // whether this tonemapper supports HDR output + int nativeInputSpaceHDR; // input color space index when HDR is active + int nativeOutputSpaceHDR; // output color space index when HDR is active + + using CTP = std::array; + std::function draw_settings_func; + CTP default_settings; + + CTP cached_settings; + + static auto& GetTonemappers() + { + using f4 = float4; + static std::vector tonemappers = { + { "Reinhard"sv, "Reinhard"sv, + T(TKEY("tonemapper.reinhard.description"), "Mapping proposed in \"Photographic Tone Reproduction for Digital Images\" by Reinhard et al. 2002."), 0, 0, false, 0, 0, + [](CTP& params) { exposureSlider(¶ms[0].x); }, + { f4{ 1.f, 0.f, 0.f, 0.f } } }, + + { "Reinhard Extended"sv, "ReinhardExt"sv, + T(TKEY("tonemapper.reinhard_extended.description"), + "Extended mapping proposed in \"Photographic Tone Reproduction for Digital Images\" by Reinhard et al. 2002. " + "An additional user parameter specifies the smallest luminance that is mapped to 1, which allows high luminances to burn out."), + 0, 0, false, 0, 0, + [](CTP& params) { + exposureSlider(¶ms[0].x); + ImGui::SliderFloat(T(TKEY("white_point"), "White Point"), ¶ms[0].y, 0.f, 10.f, "%.2f"); }, + { f4{ 1.f, 2.f, 0.f, 0.f } } }, + + { "Hejl Burgess-Dawson Filmic"sv, "HejlBurgessDawsonFilmic"sv, + T(TKEY("tonemapper.hejl_burgess_dawson_filmic.description"), + "Variation of the Hejl and Burgess-Dawson filmic curve done by Graham Aldridge. " + "See his blog post about \"Approximating Film with Tonemapping\"."), + 0, 0, false, 0, 0, + [](CTP& params) { exposureSlider(¶ms[0].x); }, + { f4{ 1.f, 0.f, 0.f, 0.f } } }, + + { "Aldridge Filmic"sv, "AldridgeFilmic"sv, + T(TKEY("tonemapper.aldridge_filmic.description"), + "Variation of the Hejl and Burgess-Dawson filmic curve done by Graham Aldridge. " + "See his blog post about \"Approximating Film with Tonemapping\"."), + 0, 0, false, 0, 0, + [](CTP& params) { + exposureSlider(¶ms[0].x); + ImGui::SliderFloat(T(TKEY("cutoff"), "Cutoff"), ¶ms[0].y, 0.f, .5f, "%.2f"); }, + { f4{ 1.f, .19f, 0.f, 0.f } } }, + + { "Lottes Filmic/AMD Curve"sv, "LottesFilmic"sv, + T(TKEY("tonemapper.lottes_filmic.description"), + "Filmic curve by Timothy Lottes, described in his GDC talk \"Advanced Techniques and Optimization of HDR Color Pipelines\". " + "Also known as the \"AMD curve\"."), + 0, 0, true, 0, 0, + [](CTP& params) { + exposureSlider(¶ms[0].x); + ImGui::SliderFloat(T(TKEY("contrast"), "Contrast"), ¶ms[0].y, 1.f, 2.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("shoulder"), "Shoulder"), ¶ms[0].z, 0.01f, 2.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("maximum_hdr_value"), "Maximum HDR Value"), ¶ms[0].w, 1.f, 10.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("input_mid_level"), "Input Mid-Level"), ¶ms[1].x, 0.f, 1.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("output_mid_level"), "Output Mid-Level"), ¶ms[1].y, 0.f, 1.f, "%.2f"); + drawHDRStatus(); }, + { f4{ 1.f, 1.6f, 0.977f, 8.f }, f4{ 0.18f, 0.267f, 0.f, 0.f } } }, + + { "Day Filmic/Insomniac Curve"sv, "DayFilmic"sv, + T(TKEY("tonemapper.day_filmic.description"), + "Filmic curve by Mike Day, described in his document \"An efficient and user-friendly tone mapping operator\". " + "Also known as the \"Insomniac curve\"."), + 0, 0, false, 0, 0, + [](CTP& params) { + exposureSlider(¶ms[0].x); + ImGui::SliderFloat(T(TKEY("black_point"), "Black Point"), ¶ms[0].y, 0.f, 5.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("white_point"), "White Point"), ¶ms[0].z, 0.f, 5.f, "%.2f"); + + ImGui::SliderFloat(T(TKEY("cross_over_point"), "Cross-over Point"), ¶ms[0].w, 0.f, 5.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T(TKEY("cross_over_point_tooltip"), "Point where the toe and shoulder are pieced together into a single curve.")); + ImGui::SliderFloat(T(TKEY("shoulder_strength"), "Shoulder Strength"), ¶ms[1].x, 0.f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T(TKEY("shoulder_strength_tooltip"), "Amount of blending between a straight-line curve and a purely asymptotic curve for the shoulder.")); + ImGui::SliderFloat(T(TKEY("toe_strength"), "Toe Strength"), ¶ms[1].y, 0.f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T(TKEY("toe_strength_tooltip"), "Amount of blending between a straight-line curve and a purely asymptotic curve for the toe.")); }, + { f4{ 1.f, 0.f, 2.f, 0.3f }, f4{ 0.8f, 0.7f, 0.f, 0.f } } }, + + { "Uchimura/Grand Turismo Curve"sv, "UchimuraFilmic"sv, + T(TKEY("tonemapper.uchimura_filmic.description"), + "Filmic curve by Hajime Uchimura, described in his CEDEC talk \"HDR Theory and Practice\". Characterised by its middle linear section. " + "Also known as the \"Gran Turismo curve\"."), + 0, 0, true, 0, 0, + [](CTP& params) { + exposureSlider(¶ms[0].x); + ImGui::SliderFloat(T(TKEY("max_brightness"), "Max Brightness"), ¶ms[0].y, 0.01f, 2.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("contrast"), "Contrast"), ¶ms[0].z, 0.f, 5.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("linear_section_start"), "Linear Section Start"), ¶ms[0].w, 0.f, 1.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("linear_section_length"), "Linear Section Length"), ¶ms[1].x, .01f, .99f, "%.2f"); + ImGui::SliderFloat(T(TKEY("black_tightness_shape"), "Black Tightness Shape"), ¶ms[1].y, 1.f, 3.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("black_tightness_offset"), "Black Tightness Offset"), ¶ms[1].z, 0.f, 1.f, "%.2f"); + drawHDRStatus(); }, + { f4{ 1.f, 1.f, 1.f, .22f }, f4{ 0.4f, 1.33f, 0.f, 0.f } } }, + + { "AgX Minimal"sv, "AgxMinimal"sv, + T(TKEY("tonemapper.agx_minimal.description"), + "Minimal version of Troy Sobotka's AgX using a 6th order polynomial approximation. " + "Originally created by bwrensch, and improved by Troy Sobotka. Internally uses AgX input transform."), + 0, 0, false, 0, 0, + [](CTP& params) { + exposureSlider(¶ms[0].x); + ImGui::SliderFloat(T(TKEY("slope"), "Slope"), ¶ms[0].y, 0.f, 2.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("power"), "Power"), ¶ms[0].z, 0.f, 2.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("offset"), "Offset"), ¶ms[0].w, -1.f, 1.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("saturation"), "Saturation"), ¶ms[1].x, 0.f, 2.f, "%.2f"); }, + { f4{ 1.f, 1.f, 1.f, 0.f }, f4{ 1.f, 0.f, 0.f, 0.f } } }, + + { "Melon"sv, "MelonTonemap"sv, + T(TKEY("tonemapper.melon.description"), "Tonemapper designed by TripleMelon to fix the ACES issue of intense colour being shifted."), 0, 0, false, 0, 0, + [](CTP& params) { exposureSlider(¶ms[0].x); }, + { f4{ 1.f, 0.f, 0.f, 0.f } } }, + + { "Kajiya"sv, "KajiyaTonemap"sv, + T(TKEY("tonemapper.kajiya.description"), "Tonemapper designed by Tomasz Stachowiak/Embark for their real time ray tracing engine Kajiya."), 0, 0, false, 0, 0, + [](CTP& params) { exposureSlider(¶ms[0].x); }, + { f4{ 1.f, 0.f, 0.f, 0.f } } }, + + { "GT7"sv, "GT7ToneMapping"sv, + T(TKEY("tonemapper.gt7.description"), "Tonemapper designed for Gran Turismo 7."), 2, 2, true, 2, 2, + [](CTP& params) { + exposureSlider(¶ms[0].x); + drawHDRStatus(); + }, + { f4{ 1.f, 0.f, 1000.f, 0.f } } }, + + { "PsychoV"sv, "PsychoVTonemap"sv, + T(TKEY("tonemapper.psychov.description"), "PsychoV 17 tonemapper by Carlos Lopez, from RenoDX."), + 0, 0, true, 0, 0, + [](CTP& params) { + exposureSlider(¶ms[0].x); + drawHDRStatus(); + }, + { f4{ 1.f, 0.f, 0.f, 0.f } } }, + + { "Neutwo"sv, "NeutwoTonemap"sv, + T(TKEY("tonemapper.neutwo.description"), "Neutwo tonemapper by Carlos Lopez, from RenoDX."), + 0, 0, true, 2, 2, + [](CTP& params) { + exposureSlider(¶ms[0].x); + ImGui::SliderFloat(T(TKEY("clip_point"), "Clip Point"), ¶ms[0].y, 1.f, 100.f, "%.2f"); + drawHDRStatus(); + }, + { f4{ 1.f, 100.f, 0.f, 0.f } } }, + + { "ACES"sv, "ACESTonemap"sv, + T(TKEY("tonemapper.aces.description"), "ACES RRT+ODT tonemapper implementation from RenoDX."), + 0, 0, true, 2, 2, + [](CTP& params) { + exposureSlider(¶ms[0].x); + ImGui::SliderFloat(T(TKEY("min_luminance"), "Min Luminance"), ¶ms[0].y, 0.0001f, 1.f, "%.4f"); + drawHDRStatus(); + }, + { f4{ 1.f, 0.0001f, 0.f, 0.f } } }, + + { "Frostbite"sv, "FrostbiteTonemap"sv, + T(TKEY("tonemapper.frostbite.description"), "Frostbite HDR display mapping implementation from RenoDX, based on EA's Frostbite color grading and display presentation work."), + 0, 0, true, 2, 2, + [](CTP& params) { + exposureSlider(¶ms[0].x); + ImGui::SliderFloat(T(TKEY("rolloff_start"), "Rolloff Start"), ¶ms[0].y, 0.f, 1.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("saturation_boost"), "Saturation Boost"), ¶ms[0].z, 0.f, 1.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("hue_correction"), "Hue Correction"), ¶ms[0].w, 0.f, 1.f, "%.2f"); + drawHDRStatus(); + }, + { f4{ 1.f, 0.25f, 0.3f, 0.6f } } }, + + { "Hermite Spline"sv, "HermiteSplineTonemap"sv, + T(TKEY("tonemapper.hermite_spline.description"), "Hermite spline tonemapper by Musa, from RenoDX."), + 0, 0, true, 2, 2, + [](CTP& params) { + exposureSlider(¶ms[0].x); + ImGui::SliderFloat(T(TKEY("white_clip"), "White Clip"), ¶ms[0].y, 1.f, 500.f, "%.2f"); + drawHDRStatus(); + }, + { f4{ 1.f, 100.f, 0.f, 0.f } } } + }; + + static std::once_flag flag; + std::call_once(flag, + [&]() { + for (auto& t : tonemappers) + t.cached_settings = t.default_settings; + }); + + return tonemappers; + } + + static void GetDefaultParams(int& tonemapperType, CTP& params) + { + auto& tonemappers = GetTonemappers(); + if (auto it = std::ranges::find_if(tonemappers, [&](TonemapperInfo& x) { return "GT7"sv == x.name; }); + it != tonemappers.end()) { + tonemapperType = (int)(it - tonemappers.begin()); + params = it->default_settings; + } else + logger::error("Somehow, the default settings are invalid. Please contact the author."); + } +}; + +void ColorGrading::DrawSettings() +{ + ImGui::Checkbox(T(TKEY("skip_ldr_color_grading"), "Skip LDR Color Grading"), &settings.skipLDR); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T(TKEY("skip_ldr_color_grading_tooltip"), "Skip color grading after tonemapping. This includes Lift Gamma Gain. Will be automatically skipped with HDR on.")); + + ImGui::Checkbox(T(TKEY("skip_lut_direct_color_grading"), "Skip LUT (Direct Color Grading)"), &settings.skipLUT); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T(TKEY("skip_lut_direct_color_grading_tooltip"), "Skip baking color grading into a LUT and apply it directly per-pixel. More accurate but slower.")); + + ImGui::Checkbox(T(TKEY("convert_linear_to_log_before_hdr_color_grading"), "Convert Linear to Log Before HDR Color Grading"), &settings.useLog); + if (settings.useLog) { + ImGui::Checkbox(T(TKEY("convert_log_to_linear_after_hdr_color_grading"), "Convert Log to Linear After HDR Color Grading"), &settings.invertLog); + ImGui::Combo(T(TKEY("log_type"), "Log Type"), (int*)&settings.logType, "ACEScct\0ARRILogC4\0SonySLog3\0"); + } + + ImGui::SeparatorText(T(TKEY("color_grading"), "Color Grading")); + { + ImGui::SliderFloat(T(TKEY("input_gamma"), "Input Gamma"), &settings.inOutGamma.z, 0.f, 3.f, "%.3f"); + ImGui::SliderFloat(T(TKEY("output_gamma"), "Output Gamma"), &settings.inOutGamma.w, 0.f, 3.f, "%.3f"); + + ImGui::Text(T(TKEY("pre_tonemapping_settings"), "Pre-Tonemapping Settings")); + if (ImGui::TreeNode(T(TKEY("exposure_temperature_tint"), "Exposure/Temperature/Tint"))) { + exposureSlider(&settings.exposureTemperatureTint.x); + ImGui::SliderFloat(T(TKEY("temperature"), "Temperature"), &settings.exposureTemperatureTint.y, 10.f, 150.f, "%1.f00K"); + ImGui::SliderFloat(T(TKEY("tint"), "Tint"), &settings.exposureTemperatureTint.z, -1.f, 1.f, "%.3f"); + ImGui::TreePop(); + } + + if (ImGui::TreeNode(T(TKEY("asc_cdl"), "ASC CDL"))) { + Util::ShiftSlider(T(TKEY("slope"), "Slope"), &settings.slope.x, 0.f, 2.f, "%.2f"); + Util::ShiftSlider(T(TKEY("power"), "Power"), &settings.power.x, 0.f, 2.f, "%.2f"); + Util::ShiftSlider(T(TKEY("offset"), "Offset"), &settings.cdlOffset.x, -1.f, 1.f, "%.2f"); + ImGui::TreePop(); + } + + if (ImGui::TreeNode(T(TKEY("oklch_saturation"), "OKLCH Saturation"))) { + ImGui::SliderFloat(T(TKEY("saturation"), "Saturation"), &settings.oklchSaturation.x, 0.f, 2.f, "%.3f"); + ImGui::SliderFloat(T(TKEY("vibrance"), "Vibrance"), &settings.oklchSaturation.y, 0.f, 3.f, "%.3f"); + ImGui::SliderFloat(T(TKEY("hue_shift"), "Hue Shift"), &settings.oklchSaturation.z, -1.f, 1.f, "%.3f"); + ImGui::TreePop(); + } + + if (ImGui::TreeNode(T(TKEY("oklch_color_mixer"), "OKLCH Color Mixer"))) { + ImGui::Text(T(TKEY("oklch_color_mixer_tooltip"), "Adjust brightness, vibrance and hue shift of specific hues in the perceptually uniform OKLCH space.")); + constexpr std::array hues = { { + { 255, 0, 0 }, + { 182, 124, 1 }, + { 87, 159, 0 }, + { 0, 161, 145 }, + { 0, 149, 217 }, + { 133, 100, 255 }, + { 255, 35, 189 }, + } }; + static int hueId = 0; + if (ImGui::BeginTable("##HueTable", 7)) { + for (int i = 0; i < 7; i++) { + ImGui::TableNextColumn(); + + ImGui::PushID(i); + ImGui::PushStyleColor(ImGuiCol_Text, hues[i].Value); + ImGui::RadioButton(ICON_FA_SQUARE, &hueId, i); + ImGui::PopStyleColor(); + ImGui::PopID(); + } + ImGui::EndTable(); + } + ImGui::SliderFloat(T(TKEY("hue_shift"), "Hue Shift"), &settings.oklchColorMixer[hueId].x, -1.f, 1.f, "%.3f"); + ImGui::SliderFloat(T(TKEY("vibrance"), "Vibrance"), &settings.oklchColorMixer[hueId].y, 0.f, 3.f, "%.3f"); + ImGui::SliderFloat(T(TKEY("brightness"), "Brightness"), &settings.oklchColorMixer[hueId].z, -1.f, 1.f, "%.3f"); + ImGui::TreePop(); + } + + if (ImGui::TreeNode(T(TKEY("shadows_midtones_highlights"), "Shadows/Midtones/Highlights"))) { + Util::ShiftSlider(T(TKEY("shadows_gain"), "Shadows Gain"), &settings.shadowsGain.x, 0.f, 2.f, "%.3f"); + Util::ShiftSlider(T(TKEY("shadows_offset"), "Shadows Offset"), &settings.shadowsOffset.x, -0.5f, 0.5f, "%.3f"); + Util::ShiftSlider(T(TKEY("midtones_gain"), "Midtones Gain"), &settings.midtonesGain.x, 0.f, 2.f, "%.3f"); + Util::ShiftSlider(T(TKEY("midtones_offset"), "Midtones Offset"), &settings.midtonesOffset.x, -0.5f, 0.5f, "%.3f"); + Util::ShiftSlider(T(TKEY("highlights_gain"), "Highlights Gain"), &settings.highlightsGain.x, 0.f, 2.f, "%.3f"); + Util::ShiftSlider(T(TKEY("highlights_offset"), "Highlights Offset"), &settings.highlightsOffset.x, -0.5f, 0.5f, "%.3f"); + ImGui::InputFloat2(T(TKEY("shadows_start_end"), "Shadows Start/End"), &settings.shadowsHighlightsRange.x, "%.3f"); + ImGui::InputFloat2(T(TKEY("highlights_start_end"), "Highlights Start/End"), &settings.shadowsHighlightsRange.z, "%.3f"); + ImGui::TreePop(); + } + + if (ImGui::TreeNode(T(TKEY("contrast"), "Contrast"))) { + Util::ShiftSlider(T(TKEY("contrast"), "Contrast"), &settings.contrast.x, 0.f, 2.f, "%.3f"); + Util::ShiftSlider(T(TKEY("pivot"), "Pivot"), &settings.pivot.x, 0.f, 1.f, "%.3f"); + ImGui::TreePop(); + } + + ImGui::Text(T(TKEY("post_tonemapping_settings"), "Post-Tonemapping Settings")); + if (ImGui::TreeNode(T(TKEY("lift_gamma_gain"), "Lift Gamma Gain"))) { + ImGui::DragFloat4(T(TKEY("lift"), "Lift"), &settings.lift.x, 1e-3f, -1.f, 1.f, "%.3f"); + ImGui::DragFloat4(T(TKEY("gamma"), "Gamma"), &settings.gamma.x, 1e-3f, -1.5f, 1.5f, "%.3f"); + ImGui::DragFloat4(T(TKEY("gain"), "Gain"), &settings.gain.x, 1e-3f, 0.f, 2.f, "%.3f"); + ImGui::TreePop(); + } + } + + ImGui::SeparatorText(T(TKEY("tonemapping"), "Tonemapping")); + ImGui::Checkbox(T(TKEY("enable_tonemapping"), "Enable Tonemapping"), &settings.enableTonemap); + if (settings.enableTonemap) { + auto& hdrRef = globals::features::hdrDisplay; + const bool hdrActive = hdrRef.loaded && hdrRef.settings.enableHDR; + + if (ImGui::Checkbox(T(TKEY("use_open_drt"), "Use OpenDRT"), &settings.useOpenDrt)) + recompileFlag = true; + + if (settings.useOpenDrt) { + ImGui::PushID("OpenDRT"); + OpenDRTDrawSettings( + settings.odrtConfig, + hdrActive, + static_cast(hdrRef.settings.hdrPaperWhite), + static_cast(hdrRef.settings.hdrPeakNits)); + ImGui::PopID(); + } else { + auto& tonemappers = TonemapperInfo::GetTonemappers(); + + if (ImGui::BeginCombo(T(TKEY("tonemapper"), "Tonemapper"), tonemappers[tonemapperType].name.data(), ImGuiComboFlags_HeightLargest)) { + for (int i = 0; i < (int)tonemappers.size(); ++i) { + // Hide non-HDR tonemappers when HDR is active + if (hdrActive && !tonemappers[i].supportsHDR) + continue; + + if (ImGui::Selectable(tonemappers[i].name.data(), i == tonemapperType)) { + tonemappers[tonemapperType].cached_settings = settings.tonemapParams; + settings.tonemapParams = tonemappers[i].cached_settings; + tonemapperType = i; + recompileFlag = true; + } + + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(tonemappers[i].desc.data()); + } + ImGui::EndCombo(); + } + ImGui::Spacing(); + ImGui::TextWrapped(tonemappers[tonemapperType].desc.data()); + ImGui::Spacing(); + if (ImGui::Button(T(TKEY("reset"), "Reset"), { -1, 0 })) + settings.tonemapParams = tonemappers[tonemapperType].default_settings; + ImGui::Spacing(); + + ImGui::PushID(tonemapperType); + tonemappers[tonemapperType].draw_settings_func(settings.tonemapParams); + ImGui::PopID(); + } + + // Tonemapping curve visualization (GPU-evaluated, RGB overlay) + if (ImGui::TreeNode(T(TKEY("curve_preview"), "Curve Preview"))) { + curveReadbackRequested = true; + curveReadbackRequestFrame = ImGui::GetFrameCount(); + + if (settings.skipLUT) { + ImGui::TextDisabled(T(TKEY("curve_preview_requires_lut"), "Enable LUT generation to see curve preview (uncheck 'Skip LUT')")); + } else { + // Determine Y-axis max from data + float yMax = 1.f; + if (hdrActive) { + for (int i = 0; i < CurveSamples; i++) { + yMax = std::max({ yMax, curveR[i], curveG[i], curveB[i] }); + } + yMax = std::ceil(yMax * 2.f) / 2.f; // round up to nearest 0.5 + yMax = std::max(yMax, 1.f); + } + + // Plot area + float plotW = ImGui::GetContentRegionAvail().x; + float plotH = 180.f; + ImVec2 canvasPos = ImGui::GetCursorScreenPos(); + ImVec2 canvasSize = { plotW, plotH }; + ImGui::InvisibleButton("##curve_canvas", canvasSize); + bool hovered = ImGui::IsItemHovered(); + + auto* dl = ImGui::GetWindowDrawList(); + + // Background + dl->AddRectFilled(canvasPos, { canvasPos.x + canvasSize.x, canvasPos.y + canvasSize.y }, IM_COL32(20, 20, 20, 255)); + dl->AddRect(canvasPos, { canvasPos.x + canvasSize.x, canvasPos.y + canvasSize.y }, IM_COL32(80, 80, 80, 255)); + + // Grid lines + auto gridColor = IM_COL32(50, 50, 50, 255); + for (int g = 1; g <= 3; g++) { + float gy = canvasPos.y + canvasSize.y * (1.f - (float)g / 4.f); + dl->AddLine({ canvasPos.x, gy }, { canvasPos.x + canvasSize.x, gy }, gridColor); + } + // Vertical grid at input = 1.0 + { + float gx = canvasPos.x + canvasSize.x * (1.f / CurveMaxInput); + dl->AddLine({ gx, canvasPos.y }, { gx, canvasPos.y + canvasSize.y }, gridColor); + } + + // Identity line (input = output, clamped to plot range) + { + float identityEndX = std::min(1.f, yMax) / CurveMaxInput; // where identity line hits yMax + float x0 = canvasPos.x; + float y0 = canvasPos.y + canvasSize.y; + float x1 = canvasPos.x + canvasSize.x * identityEndX; + float y1 = canvasPos.y + canvasSize.y * (1.f - std::min(1.f, yMax) / yMax); + dl->AddLine({ x0, y0 }, { x1, y1 }, IM_COL32(80, 80, 80, 128)); + } + + // Draw RGB curves + auto drawCurve = [&](const std::array& data, ImU32 color) { + for (int i = 0; i < CurveSamples - 1; i++) { + float x0 = canvasPos.x + canvasSize.x * ((float)i / (CurveSamples - 1)); + float x1 = canvasPos.x + canvasSize.x * ((float)(i + 1) / (CurveSamples - 1)); + float y0 = canvasPos.y + canvasSize.y * (1.f - std::clamp(data[i] / yMax, 0.f, 1.f)); + float y1 = canvasPos.y + canvasSize.y * (1.f - std::clamp(data[i + 1] / yMax, 0.f, 1.f)); + dl->AddLine({ x0, y0 }, { x1, y1 }, color, 1.5f); + } + }; + + drawCurve(curveR, IM_COL32(220, 60, 60, 255)); + drawCurve(curveG, IM_COL32(60, 200, 60, 255)); + drawCurve(curveB, IM_COL32(80, 80, 240, 255)); + + // Hover tooltip with Pre/Post values + if (hovered) { + ImVec2 mousePos = ImGui::GetMousePos(); + float t = std::clamp((mousePos.x - canvasPos.x) / canvasSize.x, 0.f, 1.f); + int idx = std::clamp((int)(t * (CurveSamples - 1)), 0, CurveSamples - 1); + float preValue = t * CurveMaxInput; + + // Vertical cursor line + dl->AddLine({ mousePos.x, canvasPos.y }, { mousePos.x, canvasPos.y + canvasSize.y }, IM_COL32(200, 200, 200, 100)); + + ImGui::BeginTooltip(); + ImGui::Text(T(TKEY("curve_preview_pre"), "Pre: %.3f"), preValue); + ImGui::TextColored(ImVec4(0.9f, 0.25f, 0.25f, 1), T(TKEY("curve_preview_post_r"), "Post R: %.3f"), curveR[idx]); + ImGui::TextColored(ImVec4(0.25f, 0.8f, 0.25f, 1), T(TKEY("curve_preview_post_g"), "Post G: %.3f"), curveG[idx]); + ImGui::TextColored(ImVec4(0.35f, 0.35f, 0.95f, 1), T(TKEY("curve_preview_post_b"), "Post B: %.3f"), curveB[idx]); + ImGui::EndTooltip(); + } + + // Axis labels + ImGui::TextDisabled(T(TKEY("curve_preview_axis"), "Pre: 0 - %.1f (HDR linear) | Post: 0 - %.1f%s"), CurveMaxInput, yMax, hdrActive ? T(TKEY("curve_preview_hdr_suffix"), " (HDR)") : ""); + } + ImGui::TreePop(); + } else { + curveReadbackRequested = false; + } + } + + ImGui::SeparatorText(T(TKEY("game_color_grading"), "Game Color Grading")); + ImGui::SliderFloat3(T(TKEY("cinematic_blend"), "Cinematic Blend"), &settings.gameCinematicBlend.x, 0.f, 1.f, "%.3f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T(TKEY("cinematic_blend_tooltip"), "Saturation, Brightness and Contrast.")); + ImGui::SliderFloat(T(TKEY("fade_blend"), "Fade Blend"), &settings.gameFadeBlend, 0.f, 1.f, "%.3f"); + ImGui::SliderFloat(T(TKEY("tint_blend"), "Tint Blend"), &settings.gameTintBlend, 0.f, 1.f, "%.3f"); + ImGui::SeparatorText(T(TKEY("color_space_transform"), "Color Space Transform")); + { + auto& spaces = getAvailableColorSpaces(); + auto& hdr = globals::features::hdrDisplay; + const bool hdrEnabled = hdr.loaded && hdr.settings.enableHDR; + + constexpr int kHDRColorSpace = 2; // BT2020 + constexpr int kSDRColorSpace = 0; // sRGB / BT709 gamut + const int outputColorSpace = hdrEnabled ? kHDRColorSpace : kSDRColorSpace; + + auto& llSettings = globals::features::linearLighting.settings; + const bool wideGamutActive = llSettings.enableACEScg && llSettings.enableLinearLighting; + const char* inputSpaceName = wideGamutActive ? spaces[5] : spaces[0]; + ImGui::TextDisabled(T(TKEY("input_color_space"), "Input Color Space: %s (%s)"), inputSpaceName, wideGamutActive ? T(TKEY("input_color_space_auto_detected"), "auto-detected from Linear Lighting ACEScg") : T(TKEY("input_color_space_fixed"), "fixed")); + ImGui::Combo(T(TKEY("working_color_space"), "Working Color Space"), &settings.processColorSpace, spaces.data(), (int)spaces.size()); + ImGui::TextDisabled(T(TKEY("output_color_space"), "Output Color Space: %s (auto from HDR Display)"), spaces[outputColorSpace]); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T(TKEY("output_color_space_tooltip"), "Output switches automatically: SDR -> sRGB, HDR -> BT2020.")); + + UpdateColorSpaceTransforms(hdrEnabled); + } + + if (ImGui::Button(T(TKEY("save_lut_and_output_image"), "Save LUT and Output Image"))) { + saveImagesFlag = true; + } + ImGui::SameLine(); + ImGui::Text(T(TKEY("output_saved_to"), "Output will be saved to: %s"), outputPath.c_str()); +} + +void ColorGrading::RestoreDefaultSettings() +{ + settings = {}; + TonemapperInfo::GetDefaultParams(tonemapperType, settings.tonemapParams); + recompileFlag = true; +} + +void ColorGrading::LoadSettings(json& o_json) +{ + try { + settings = o_json; + auto& spaces = getAvailableColorSpaces(); + settings.processColorSpace = std::clamp(settings.processColorSpace, 0, static_cast(spaces.size()) - 1); + + auto& tonemappers = TonemapperInfo::GetTonemappers(); + if (auto it = std::ranges::find_if(tonemappers, [&](TonemapperInfo& x) { return settings.currentTonemapper == x.name; }); + it != tonemappers.end()) { + tonemapperType = (int)(it - tonemappers.begin()); + } else { + TonemapperInfo::GetDefaultParams(tonemapperType, settings.tonemapParams); + } + } catch (const json::exception& e) { + logger::error("Failed to load Color Grading settings: {}", e.what()); + RestoreDefaultSettings(); + } + + try { + auto part1 = o_json["ODRT1"].get(); + auto part2 = o_json["ODRT2"].get(); + std::memcpy(&settings.odrtConfig, &part1, sizeof(OpenDRTSettingsPart1)); + std::memcpy( + reinterpret_cast(&settings.odrtConfig) + sizeof(OpenDRTSettingsPart1), + &part2, + sizeof(OpenDRTSettingsPart2)); + } catch (const json::exception&) { + settings.odrtConfig = {}; + } + + recompileFlag = true; +} + +void ColorGrading::SaveSettings(json& o_json) +{ + auto& tonemappers = TonemapperInfo::GetTonemappers(); + settings.currentTonemapper = tonemappers[tonemapperType].name.data(); + o_json = settings; + + OpenDRTSettingsPart1 part1; + OpenDRTSettingsPart2 part2; + std::memcpy(&part1, &settings.odrtConfig, sizeof(part1)); + std::memcpy( + &part2, + reinterpret_cast(&settings.odrtConfig) + sizeof(part1), + sizeof(part2)); + o_json["ODRT1"] = part1; + o_json["ODRT2"] = part2; +} + +void ColorGrading::UpdateColorSpaceTransforms(bool hdrEnabled) +{ + auto& spaces = getAvailableColorSpaces(); + settings.processColorSpace = std::clamp(settings.processColorSpace, 0, static_cast(spaces.size()) - 1); + + auto& tonemappers = TonemapperInfo::GetTonemappers(); + + // Auto-detect input color space: ACEScg when wide gamut mode is active, otherwise sRGB + auto& llSettings = globals::features::linearLighting.settings; + const bool wideGamutActive = llSettings.enableACEScg && llSettings.enableLinearLighting; + const int kInputColorSpace = wideGamutActive ? 5 : 0; // 5 = ACEScg, 0 = sRGB + constexpr int kHDRColorSpace = 2; // BT2020 + constexpr int kSDRColorSpace = 0; // sRGB / BT709 gamut + const int outputColorSpace = hdrEnabled ? kHDRColorSpace : kSDRColorSpace; + const int tonemapInputSpace = + settings.useOpenDrt ? 4 : + ((hdrEnabled && tonemappers[tonemapperType].supportsHDR) ? + tonemappers[tonemapperType].nativeInputSpaceHDR : + tonemappers[tonemapperType].nativeInputSpace); + const int tonemapOutputSpace = + settings.useOpenDrt ? 2 : + ((hdrEnabled && tonemappers[tonemapperType].supportsHDR) ? + tonemappers[tonemapperType].nativeOutputSpaceHDR : + tonemappers[tonemapperType].nativeOutputSpace); + + auto storeMatrix = [](const DirectX::SimpleMath::Matrix& mat, std::array& out) { + out = { + float3{ mat(0, 0), mat(0, 1), mat(0, 2) }, + float3{ mat(1, 0), mat(1, 1), mat(1, 2) }, + float3{ mat(2, 0), mat(2, 1), mat(2, 2) } + }; + }; + + storeMatrix(getRGBMatrix(spaces[kInputColorSpace], spaces[settings.processColorSpace]), inputToWorkingMatrix); + storeMatrix(getRGBMatrix(spaces[settings.processColorSpace], spaces[tonemapInputSpace]), workingToTonemapMatrix); + storeMatrix(getRGBMatrix(spaces[tonemapOutputSpace], spaces[outputColorSpace]), tonemapToOutputMatrix); +} + +void ColorGrading::SetupResources() +{ + auto renderer = globals::game::renderer; + auto device = globals::d3d::device; + auto context = globals::d3d::context; + + logger::debug("Creating buffers..."); + { + colorCB = std::make_unique(ConstantBufferDesc()); + } + + logger::debug("Creating 2D textures..."); + { + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + D3D11_TEXTURE2D_DESC texDesc; + gameTexMainCopy.texture->GetDesc(&texDesc); + + texDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + texDesc.MipLevels = srvDesc.Texture2D.MipLevels = 1; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + + texColor = std::make_unique(texDesc); + texColor->CreateSRV(srvDesc); + texColor->CreateUAV(uavDesc); + + D3D11_TEXTURE3D_DESC lutTexDesc = { + .Width = LUTDim, + .Height = LUTDim, + .Depth = LUTDim, + .MipLevels = 1, + .Format = DXGI_FORMAT_R16G16B16A16_FLOAT, + .Usage = D3D11_USAGE_DEFAULT, + .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS, + .CPUAccessFlags = 0, + .MiscFlags = 0 + }; + + D3D11_SHADER_RESOURCE_VIEW_DESC lutSrvDesc = { + .Format = lutTexDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE3D, + .Texture3D = { .MostDetailedMip = 0, .MipLevels = lutTexDesc.MipLevels } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC lutUavDesc = { + .Format = lutTexDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE3D, + .Texture3D = { .MipSlice = 0, .FirstWSlice = 0, .WSize = lutTexDesc.Depth } + }; + + texLUT = std::make_unique(lutTexDesc); + texLUT->CreateSRV(lutSrvDesc); + texLUT->CreateUAV(lutUavDesc); + } + + logger::debug("Creating samplers..."); + { + D3D11_SAMPLER_DESC samplerDesc = { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_CLAMP, + .AddressV = D3D11_TEXTURE_ADDRESS_CLAMP, + .AddressW = D3D11_TEXTURE_ADDRESS_CLAMP, + .MaxAnisotropy = 1, + .MinLOD = 0, + .MaxLOD = D3D11_FLOAT32_MAX + }; + DX::ThrowIfFailed(device->CreateSamplerState(&samplerDesc, linearSampler.put())); + } + + // Curve preview textures: 256x1 ramp for GPU-based curve evaluation + { + D3D11_TEXTURE2D_DESC curveTexDesc = { + .Width = CurveSamples, + .Height = 1, + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_R16G16B16A16_FLOAT, + .SampleDesc = { .Count = 1 }, + .Usage = D3D11_USAGE_DEFAULT, + .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS, + }; + + D3D11_SHADER_RESOURCE_VIEW_DESC curveSrvDesc = { + .Format = curveTexDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC curveUavDesc = { + .Format = curveTexDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + texCurveInput = eastl::make_unique(curveTexDesc); + texCurveInput->CreateSRV(curveSrvDesc); + + texCurveOutput = eastl::make_unique(curveTexDesc); + texCurveOutput->CreateSRV(curveSrvDesc); + texCurveOutput->CreateUAV(curveUavDesc); + + // Fill input with linear ramp [0, CurveMaxInput] in R=G=B + std::array rampData; + for (int i = 0; i < CurveSamples; i++) { + float v = (float)i / (float)(CurveSamples - 1) * CurveMaxInput; + rampData[i] = { + DirectX::PackedVector::XMConvertFloatToHalf(v), + DirectX::PackedVector::XMConvertFloatToHalf(v), + DirectX::PackedVector::XMConvertFloatToHalf(v), + DirectX::PackedVector::XMConvertFloatToHalf(1.f) + }; + } + context->UpdateSubresource(texCurveInput->resource.get(), 0, nullptr, rampData.data(), CurveSamples * sizeof(DirectX::PackedVector::XMHALF4), 0); + + // Staging texture for readback + D3D11_TEXTURE2D_DESC stagingDesc = curveTexDesc; + stagingDesc.Usage = D3D11_USAGE_STAGING; + stagingDesc.BindFlags = 0; + stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + device->CreateTexture2D(&stagingDesc, nullptr, curveStaging.put()); + } + + CompileComputeShaders(); +} + +void ColorGrading::ClearShaderCache() +{ + const auto shaderPtrs = std::array{ + &colorgradingCS, + &lutgenCS + }; + + for (auto shader : shaderPtrs) + if ((*shader)) { + (*shader)->Release(); + shader->detach(); + } + + CompileComputeShaders(); +} + +void ColorGrading::CompileComputeShaders() +{ + const auto& tonemappers = TonemapperInfo::GetTonemappers(); + + struct ShaderCompileInfo + { + winrt::com_ptr* programPtr; + std::string_view filename; + std::vector> defines; + std::string entry = "main"; + }; + + auto tonemapFuncName = settings.useOpenDrt ? "OpenDRTTransform" : tonemappers[tonemapperType].func_name.data(); + + std::vector + shaderInfos = { + { &colorgradingCS, "colorgrading.cs.hlsl", { { "TONEMAP_FUNC", tonemapFuncName } }, "CSColorGrading" }, + { &lutgenCS, "colorgrading.cs.hlsl", { { "TONEMAP_FUNC", tonemapFuncName } }, "CSLUTGen" } + }; + + for (auto& info : shaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\ColorGrading") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } + + recompileFlag = false; + curveNeedsUpdate = true; // shader changed, curve must update +} + +void ColorGrading::Draw(TextureInfo& inout_tex) +{ + auto context = globals::d3d::context; + auto state = globals::state; + + // Auto-switch to an HDR-capable tonemapper if current one doesn't support HDR. + // This runs every frame so the switch happens immediately when HDR is toggled, + // regardless of which settings page the user is viewing. + { + auto& hdrRef = globals::features::hdrDisplay; + const bool hdrActive = hdrRef.loaded && hdrRef.settings.enableHDR; + auto& tonemappers = TonemapperInfo::GetTonemappers(); + + if (hdrActive && !tonemappers[tonemapperType].supportsHDR) { + for (int i = 0; i < (int)tonemappers.size(); ++i) { + if (tonemappers[i].supportsHDR) { + tonemappers[tonemapperType].cached_settings = settings.tonemapParams; + settings.tonemapParams = tonemappers[i].cached_settings; + tonemapperType = i; + recompileFlag = true; + break; + } + } + } + } + + if (recompileFlag) + ClearShaderCache(); + + globals::profiler->BeginPass("PostProcessing::ColorGrading"); + state->BeginPerfEvent("Color Grading and Tonemapping"); + + auto& pp = globals::features::postProcessing; + + RE::ImageSpaceData imageSpaceData = pp.imageSpaceManager->gameISData; + auto& hdr = globals::features::hdrDisplay; + const bool hdrEnabled = hdr.loaded && hdr.settings.enableHDR; + UpdateColorSpaceTransforms(hdrEnabled); + + // Always compute XYZ matrices for white balance + { + auto& spaces = getAvailableColorSpaces(); + int wsIdx = std::clamp(settings.processColorSpace, 0, static_cast(spaces.size()) - 1); + auto storeMatrix = [](const DirectX::SimpleMath::Matrix& mat, std::array& out) { + out = { + float3{ mat(0, 0), mat(0, 1), mat(0, 2) }, + float3{ mat(1, 0), mat(1, 1), mat(1, 2) }, + float3{ mat(2, 0), mat(2, 1), mat(2, 2) } + }; + }; + storeMatrix(getRGBMatrix(spaces[wsIdx], "XYZ"), workingToXYZMatrix); + storeMatrix(getRGBMatrix("XYZ", spaces[wsIdx]), xyzToWorkingMatrix); + } + + ColorCB colorCBData = { + .asccdl = { settings.slope, settings.power, settings.cdlOffset }, + .liftgammagain = { settings.lift, settings.gamma, settings.gain }, + .inOutGamma = settings.inOutGamma, + .oklchSaturation = settings.oklchSaturation, + .oklchColorMixer = { settings.oklchColorMixer[0], settings.oklchColorMixer[1], settings.oklchColorMixer[2], settings.oklchColorMixer[3], settings.oklchColorMixer[4], settings.oklchColorMixer[5], settings.oklchColorMixer[6] }, + .contrast = settings.contrast, + .pivot = settings.pivot, + .exposureTemperatureTint = settings.exposureTemperatureTint, + .shadows = settings.shadowsGain, + .midtones = settings.midtonesGain, + .highlights = settings.highlightsGain, + .shadowsHighlightsRange = settings.shadowsHighlightsRange, + .tonemapParams = { settings.tonemapParams[0], settings.tonemapParams[1] }, + .inputToWorking = { float4{ inputToWorkingMatrix[0].x, inputToWorkingMatrix[0].y, inputToWorkingMatrix[0].z, 0.f }, float4{ inputToWorkingMatrix[1].x, inputToWorkingMatrix[1].y, inputToWorkingMatrix[1].z, 0.f }, float4{ inputToWorkingMatrix[2].x, inputToWorkingMatrix[2].y, inputToWorkingMatrix[2].z, 0.f } }, + .workingToTonemap = { float4{ workingToTonemapMatrix[0].x, workingToTonemapMatrix[0].y, workingToTonemapMatrix[0].z, 0.f }, float4{ workingToTonemapMatrix[1].x, workingToTonemapMatrix[1].y, workingToTonemapMatrix[1].z, 0.f }, float4{ workingToTonemapMatrix[2].x, workingToTonemapMatrix[2].y, workingToTonemapMatrix[2].z, 0.f } }, + .tonemapToOutput = { float4{ tonemapToOutputMatrix[0].x, tonemapToOutputMatrix[0].y, tonemapToOutputMatrix[0].z, 0.f }, float4{ tonemapToOutputMatrix[1].x, tonemapToOutputMatrix[1].y, tonemapToOutputMatrix[1].z, 0.f }, float4{ tonemapToOutputMatrix[2].x, tonemapToOutputMatrix[2].y, tonemapToOutputMatrix[2].z, 0.f } }, + .workingToXYZ = { float4{ workingToXYZMatrix[0].x, workingToXYZMatrix[0].y, workingToXYZMatrix[0].z, 0.f }, float4{ workingToXYZMatrix[1].x, workingToXYZMatrix[1].y, workingToXYZMatrix[1].z, 0.f }, float4{ workingToXYZMatrix[2].x, workingToXYZMatrix[2].y, workingToXYZMatrix[2].z, 0.f } }, + .xyzToWorking = { float4{ xyzToWorkingMatrix[0].x, xyzToWorkingMatrix[0].y, xyzToWorkingMatrix[0].z, 0.f }, float4{ xyzToWorkingMatrix[1].x, xyzToWorkingMatrix[1].y, xyzToWorkingMatrix[1].z, 0.f }, float4{ xyzToWorkingMatrix[2].x, xyzToWorkingMatrix[2].y, xyzToWorkingMatrix[2].z, 0.f } }, + .workingWhitePoint = [&]() { + auto& spaces = getAvailableColorSpaces(); + int wsIdx = std::clamp(settings.processColorSpace, 0, static_cast(spaces.size()) - 1); + auto wp = getWhitePoint(spaces[wsIdx]); + return float4{ wp.x, wp.y, 0.f, 0.f }; }(), + .shadowsOffset = settings.shadowsOffset, + .midtonesOffset = settings.midtonesOffset, + .highlightsOffset = settings.highlightsOffset, + .cinematic = float4{ std::lerp(1.f, imageSpaceData.baseData.cinematic.saturation, settings.gameCinematicBlend.x), std::lerp(1.f, imageSpaceData.baseData.cinematic.brightness, settings.gameCinematicBlend.y), std::lerp(1.f, imageSpaceData.baseData.cinematic.contrast, settings.gameCinematicBlend.z), imageSpaceData.baseAmount }, + .fade = float4{ imageSpaceData.modData.data[RE::ImageSpaceModData::kFadeR], imageSpaceData.modData.data[RE::ImageSpaceModData::kFadeG], imageSpaceData.modData.data[RE::ImageSpaceModData::kFadeB], imageSpaceData.modData.data[RE::ImageSpaceModData::kFadeAmount] * settings.gameFadeBlend }, + .tint = float4{ imageSpaceData.baseData.tint.color.red, imageSpaceData.baseData.tint.color.green, imageSpaceData.baseData.tint.color.blue, imageSpaceData.baseData.tint.amount * settings.gameTintBlend }, + .logType = settings.useLog ? ((1u << settings.logType) | (settings.invertLog ? (1u << 3u) : 0u)) : 0u, + .skipLDR = settings.skipLDR, + .skipLUT = settings.skipLUT, + .enableTonemap = settings.enableTonemap, + .enableColorSpaceTransform = true, + // Auto-populate HDR settings from HDR feature + .enableHDR = [&]() -> uint { + return hdrEnabled ? 1u : 0u; + }(), + .hdrPeakNits = [&]() -> float { + return hdrEnabled ? static_cast(hdr.settings.hdrPeakNits) : 1000.f; + }(), + .hdrPaperWhiteNits = [&]() -> float { + return hdrEnabled ? static_cast(hdr.settings.hdrPaperWhite) : 203.f; + }(), + .odrtConfig = settings.odrtConfig, + }; + colorCB->Update(colorCBData); + + // Check if curve needs update (CB changed = settings changed) + if (memcmp(&colorCBData, prevCurveCB.data(), sizeof(ColorCB)) != 0) { + curveNeedsUpdate = true; + memcpy(prevCurveCB.data(), &colorCBData, sizeof(ColorCB)); + } + + ID3D11Buffer* cb = colorCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + std::array samplers = { linearSampler.get() }; + context->CSSetSamplers(0, 1, samplers.data()); + ID3D11UnorderedAccessView* uav = nullptr; + + if (!settings.skipLUT) { + // LUT Gen + uav = texLUT->uav.get(); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShader(lutgenCS.get(), nullptr, 0); + context->Dispatch(LUTDim >> 3, LUTDim >> 3, LUTDim >> 3); + + uav = nullptr; + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShader(nullptr, nullptr, 0); + } + + // Apply Color Grading (via LUT or direct) + std::array srvs = { inout_tex.srv, texLUT->srv.get() }; + uav = texColor->uav.get(); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShaderResources(0, (UINT)(settings.skipLUT ? 1 : 2), srvs.data()); + context->CSSetShader(colorgradingCS.get(), nullptr, 0); + + context->Dispatch((texColor->desc.Width + 7) >> 3, (texColor->desc.Height + 7) >> 3, 1); + + // clean up + srvs.fill(nullptr); + uav = nullptr; + cb = nullptr; + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShaderResources(0, 2, srvs.data()); + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetShader(nullptr, nullptr, 0); + + if (saveImagesFlag) { + saveImagesFlag = false; + OutputTextures(); + } + + inout_tex = { texColor->resource.get(), texColor->srv.get() }; + globals::profiler->EndPass(); + + const bool curveReadbackActive = + Menu::GetSingleton()->IsEnabled && + curveReadbackRequested && + ImGui::GetCurrentContext() && + curveReadbackRequestFrame >= ImGui::GetFrameCount() - 1; + if (!curveReadbackActive) + curveReadbackRequested = false; + + // Debug: evaluate color grading pipeline on a neutral ramp for curve preview + if (curveReadbackActive && curveNeedsUpdate && texCurveInput && texCurveOutput && colorgradingCS) { + curveNeedsUpdate = false; + // Re-bind CB and samplers for the curve dispatch + ID3D11Buffer* curveCB = colorCB->CB(); + std::array curveSamplers = { linearSampler.get() }; + context->CSSetConstantBuffers(1, 1, &curveCB); + context->CSSetSamplers(0, 1, curveSamplers.data()); + + // Dispatch colorgradingCS on the 256x1 ramp input (same CB, same LUT) + std::array curveSRVs = { texCurveInput->srv.get(), texLUT ? texLUT->srv.get() : nullptr }; + ID3D11UnorderedAccessView* curveUAV = texCurveOutput->uav.get(); + + context->CSSetShaderResources(0, (UINT)(settings.skipLUT ? 1 : 2), curveSRVs.data()); + context->CSSetUnorderedAccessViews(0, 1, &curveUAV, nullptr); + context->CSSetShader(colorgradingCS.get(), nullptr, 0); + + context->Dispatch((CurveSamples + 7) >> 3, 1, 1); + + // Clean up + curveUAV = nullptr; + curveSRVs.fill(nullptr); + curveCB = nullptr; + context->CSSetUnorderedAccessViews(0, 1, &curveUAV, nullptr); + context->CSSetShaderResources(0, 2, curveSRVs.data()); + context->CSSetConstantBuffers(1, 1, &curveCB); + context->CSSetShader(nullptr, nullptr, 0); + + // Readback + if (curveStaging) { + context->CopyResource(curveStaging.get(), texCurveOutput->resource.get()); + D3D11_MAPPED_SUBRESOURCE mapped{}; + if (SUCCEEDED(context->Map(curveStaging.get(), 0, D3D11_MAP_READ, 0, &mapped))) { + auto* pixels = reinterpret_cast(mapped.pData); + for (int i = 0; i < CurveSamples; i++) { + curveR[i] = DirectX::PackedVector::XMConvertHalfToFloat(pixels[i * 4 + 0]); + curveG[i] = DirectX::PackedVector::XMConvertHalfToFloat(pixels[i * 4 + 1]); + curveB[i] = DirectX::PackedVector::XMConvertHalfToFloat(pixels[i * 4 + 2]); + } + context->Unmap(curveStaging.get(), 0); + } + } + } + + state->EndPerfEvent(); +} + +void ColorGrading::OutputTextures() +{ + auto device = globals::d3d::device; + auto context = globals::d3d::context; + + DirectX::ScratchImage lutImage; + DirectX::ScratchImage colorImage; + + if (texLUT->resource) { + DirectX::CaptureTexture(device, context, texLUT->resource.get(), lutImage); + } + + if (texColor->resource) { + DirectX::CaptureTexture(device, context, texColor->resource.get(), colorImage); + } + + if (std::filesystem::create_directories(outputPath)) { + logger::info("Missing pp directory created: {}", outputPath); + } + + std::filesystem::path savePath = outputPath; + + std::filesystem::path lutPath = savePath / "PP_ColorGrading_BakedLUT.dds"; + std::filesystem::path colorPath = savePath / "PP_ColorGrading_ColorOutput.dds"; + + DX::ThrowIfFailed(SaveToDDSFile(lutImage.GetImages(), lutImage.GetImageCount(), lutImage.GetMetadata(), DirectX::DDS_FLAGS::DDS_FLAGS_NONE, lutPath.c_str())); + DX::ThrowIfFailed(SaveToDDSFile(colorImage.GetImages(), colorImage.GetImageCount(), colorImage.GetMetadata(), DirectX::DDS_FLAGS::DDS_FLAGS_NONE, colorPath.c_str())); +} + +#undef I18N_KEY_PREFIX diff --git a/src/Features/PostProcessing/ColorGrading.h b/src/Features/PostProcessing/ColorGrading.h new file mode 100644 index 0000000000..4b7b3db141 --- /dev/null +++ b/src/Features/PostProcessing/ColorGrading.h @@ -0,0 +1,188 @@ +#pragma once +#include "OpenDRT.h" +#include "PostProcessFeature.h" + +#include "Buffer.h" + +struct ColorGrading : public PostProcessFeature +{ + virtual inline std::string GetType() const override { return "Color Grading and Tone Mapping"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.color_grading.name", "Color Grading and Tone Mapping"); } + virtual inline std::string GetDesc() const override { return T("feature.post_processing.color_grading.description", "Color grading operations and multiple tone mapping options."); } + virtual inline bool DisableInMainLoadingMenu() const override { return true; } + + template + constexpr auto make_array(T value) -> std::array + { + std::array a{}; + for (auto& x : a) + x = value; + return a; + } + + const std::string outputPath = "SKSE\\Plugins\\CommunityShaders\\PostProcessing\\ColorGrading"; + + struct Settings + { + bool skipLDR = false; + bool skipLUT = false; + + // ASC CDL + float4 slope = { 1.f, 1.f, 1.f, 0.f }; + float4 power = { 1.f, 1.f, 1.f, 0.f }; + float4 cdlOffset = { 0.f, 0.f, 0.f, 0.f }; + + // Lift Gamma Gain + float4 lift = { 0.f, 0.f, 0.f, 0.f }; + float4 gamma = { 0.f, 0.f, 0.f, 0.f }; + float4 gain = { 1.f, 1.f, 1.f, 1.f }; + + // Input/Output Gamma + float4 inOutGamma = { 1.f, 1.f, 1.f, 1.f }; + + // OKLCH + float4 oklchSaturation = { 1.f, 1.f, 0.f, 0.f }; + std::array oklchColorMixer = { + float4{ 0.f, 1.f, 0.f, 0.f }, float4{ 0.f, 1.f, 0.f, 0.f }, float4{ 0.f, 1.f, 0.f, 0.f }, + float4{ 0.f, 1.f, 0.f, 0.f }, float4{ 0.f, 1.f, 0.f, 0.f }, float4{ 0.f, 1.f, 0.f, 0.f }, + float4{ 0.f, 1.f, 0.f, 0.f } + }; + + // Contrast + float4 contrast = { 1.f, 1.f, 1.f, 0.f }; + float4 pivot = { 0.18f, 0.18f, 0.18f, 0.f }; + + // Exposure/Temperature/Tint + float4 exposureTemperatureTint = { 1.f, 65.f, 0.f, 0.f }; + + // Shadows/Midtones/Highlights + float4 shadowsGain = { 1.f, 1.f, 1.f, 0.f }; + float4 midtonesGain = { 1.f, 1.f, 1.f, 0.f }; + float4 highlightsGain = { 1.f, 1.f, 1.f, 0.f }; + float4 shadowsHighlightsRange = { 0.f, 0.3f, 0.55f, 1.f }; + + // SMH color offsets + float4 shadowsOffset = { 0.f, 0.f, 0.f, 0.f }; + float4 midtonesOffset = { 0.f, 0.f, 0.f, 0.f }; + float4 highlightsOffset = { 0.f, 0.f, 0.f, 0.f }; + + bool useOpenDrt = false; + std::string currentTonemapper = "GT7"; + std::array tonemapParams = { float4{ 1.f, 2.f, 0.f, 0.f }, float4{ 0.f, 0.f, 0.f, 0.f } }; + float3 gameCinematicBlend = { 1.0f, 1.0f, 1.0f }; + float gameFadeBlend = 1.0f; + float gameTintBlend = 1.0f; + bool useLog = false; + uint logType = 0; + bool invertLog = false; + bool enableTonemap = true; + int processColorSpace = 0; + + OpenDRTSettings odrtConfig; + } settings; + + // Computed matrices (not serialized) + std::array inputToWorkingMatrix = { float3{ 1.0f, 0.0f, 0.0f }, float3{ 0.0f, 1.0f, 0.0f }, float3{ 0.0f, 0.0f, 1.0f } }; + std::array workingToTonemapMatrix = { float3{ 1.0f, 0.0f, 0.0f }, float3{ 0.0f, 1.0f, 0.0f }, float3{ 0.0f, 0.0f, 1.0f } }; + std::array tonemapToOutputMatrix = { float3{ 1.0f, 0.0f, 0.0f }, float3{ 0.0f, 1.0f, 0.0f }, float3{ 0.0f, 0.0f, 1.0f } }; + std::array workingToXYZMatrix = { float3{ 1.0f, 0.0f, 0.0f }, float3{ 0.0f, 1.0f, 0.0f }, float3{ 0.0f, 0.0f, 1.0f } }; + std::array xyzToWorkingMatrix = { float3{ 1.0f, 0.0f, 0.0f }, float3{ 0.0f, 1.0f, 0.0f }, float3{ 0.0f, 0.0f, 1.0f } }; + + int tonemapperType = 10; + + enum class LogType : uint32_t + { + ACEScct = 1 << 0, + ARRIlogC4 = 1 << 1, + SonySLog3 = 1 << 2, + Invert = 1 << 3 + }; + + struct alignas(16) ColorCB + { + float4 asccdl[3]; + float4 liftgammagain[3]; // lift,gamma,gain + float4 inOutGamma; // .z = input gamma, .w = output gamma + float4 oklchSaturation; + float4 oklchColorMixer[7]; + float4 contrast; + float4 pivot; + float4 exposureTemperatureTint; + float4 shadows; + float4 midtones; + float4 highlights; + float4 shadowsHighlightsRange; // shadowBegin, shadowEnd, highlightBegin, highlightEnd + + float4 tonemapParams[2]; + float4 inputToWorking[3]; // sRGB → working color space + float4 workingToTonemap[3]; // working → tonemapper native space + float4 tonemapToOutput[3]; // tonemapper native → output space + + float4 workingToXYZ[3]; // working → CIE XYZ (for white balance) + float4 xyzToWorking[3]; // CIE XYZ → working (for white balance) + + float4 workingWhitePoint; // .xy = native white chromaticity of working space + + float4 shadowsOffset; // SMH color offsets + float4 midtonesOffset; + float4 highlightsOffset; + + // game value + float4 cinematic; // saturation, brightness, contrast + float4 fade; // color + float4 tint; // color + + uint logType; + uint skipLDR; + uint skipLUT; + uint enableTonemap; + uint enableColorSpaceTransform; + uint enableHDR; // HDR display is enabled (auto-set from HDR feature) + float hdrPeakNits; // Maximum display brightness in nits for HDR + float hdrPaperWhiteNits; // Reference white brightness in nits for HDR + + OpenDRTSettings odrtConfig; + }; + std::unique_ptr colorCB = nullptr; + + std::unique_ptr texColor = nullptr; + std::unique_ptr texLUT = nullptr; + + static constexpr int LUTDim = 64; + + bool recompileFlag = true; + bool saveImagesFlag = false; + winrt::com_ptr colorgradingCS = nullptr; + winrt::com_ptr lutgenCS = nullptr; + + winrt::com_ptr linearSampler = nullptr; + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json&) override; + virtual void SaveSettings(json&) override; + + virtual void DrawSettings() override; + + virtual void Draw(TextureInfo&) override; + void UpdateColorSpaceTransforms(bool hdrEnabled = false); + + void OutputTextures(); + + // Debug: tonemapping curve via GPU evaluation + static constexpr int CurveSamples = 256; + static constexpr float CurveMaxInput = 4.f; + eastl::unique_ptr texCurveInput = nullptr; // 256x1 RGBA16F ramp (0-4 linear) + eastl::unique_ptr texCurveOutput = nullptr; // 256x1 RGBA16F result + winrt::com_ptr curveStaging = nullptr; + std::array curveR = {}; + std::array curveG = {}; + std::array curveB = {}; + bool curveReadbackRequested = false; + int curveReadbackRequestFrame = -1; + bool curveNeedsUpdate = true; + std::array prevCurveCB = {}; +}; diff --git a/src/Features/PostProcessing/ColorSpace.h b/src/Features/PostProcessing/ColorSpace.h new file mode 100644 index 0000000000..4c01cdfad8 --- /dev/null +++ b/src/Features/PostProcessing/ColorSpace.h @@ -0,0 +1,85 @@ +#pragma once + +// via https://www.colour-science.org/ + +inline const auto& getAvailableColorSpaces() +{ + static auto spaces = std::array{ + "sRGB", + "BT709", + "BT2020", + "DCI-P3", + "XYZ", + "ACEScg" + }; + return spaces; +} + +// Native white point chromaticity (CIE xy) for each color space +inline DirectX::XMFLOAT2 getWhitePoint(std::string_view space) +{ + if (space == "ACEScg") + return { 0.32168f, 0.33767f }; // ACES White (approx D60) + if (space == "DCI-P3") + return { 0.31400f, 0.35100f }; // DCI White + // sRGB, BT.709, BT.2020, XYZ: D65 + return { 0.31270f, 0.32900f }; +} + +inline DirectX::SimpleMath::Matrix getRGBMatrix(std::string_view in_space, std::string_view out_space) +{ + static ankerl::unordered_dense::map maps = { + { "sRGB-XYZ", + { 0.4123908f, 0.35758434f, 0.18048079f, + 0.21263901f, 0.71516868f, 0.07219232f, + 0.01933082f, 0.11919478f, 0.95053215f } }, + { "XYZ-sRGB", + { 3.24096994f, -1.53738318f, -0.49861076f, + -0.96924364f, 1.8759675f, 0.04155506f, + 0.05563008f, -0.20397696f, 1.05697151f } }, + + { "BT2020-XYZ", + { 6.36958048e-01f, 1.44616904e-01f, 1.68880975e-01f, + 2.62700212e-01f, 6.77998072e-01f, 5.93017165e-02f, + 4.99410657e-17f, 2.80726930e-02f, 1.06098506e+00f } }, + { "XYZ-BT2020", + { 1.71665119f, -0.35567078f, -0.25336628f, + -0.66668435f, 1.61648124f, 0.01576855f, + 0.01763986f, -0.04277061f, 0.94210312f } }, + + { "DCI-P3-XYZ", + { 4.45169816e-01f, 2.77134409e-01f, 1.72282670e-01f, + 2.09491678e-01f, 7.21595254e-01f, 6.89130679e-02f, + -3.63410132e-17f, 4.70605601e-02f, 9.07355394e-01f } }, + { "XYZ-DCI-P3", + { 2.72539403f, -1.01800301f, -0.4401632f, + -0.79516803f, 1.68973205f, 0.02264719f, + 0.04124189f, -0.08763902f, 1.10092938f } }, + + { "ACEScg-XYZ", + { 0.66245418f, 0.13400421f, 0.15618769f, + 0.27222872f, 0.67408177f, 0.05368952f, + -0.00557465f, 0.00406073f, 1.0103391f } }, + { "XYZ-ACEScg", + { 1.64102338f, -0.32480329f, -0.2364247f, + -0.66366286f, 1.61533159f, 0.01675635f, + 0.01172189f, -0.00828444f, 0.98839486f } }, + }; + static std::once_flag flag; + std::call_once(flag, [&]() { + maps["BT709-XYZ"] = maps["sRGB-XYZ"]; + maps["XYZ-BT709"] = maps["XYZ-sRGB"]; + }); + + if (in_space == out_space) + return DirectX::SimpleMath::Matrix::Identity; + + if (in_space == "XYZ" || out_space == "XYZ") + return DirectX::SimpleMath::Matrix{ maps[std::format("{}-{}", in_space, out_space)] }; + else { + DirectX::SimpleMath::Matrix a = maps[std::format("{}-XYZ", in_space)]; + DirectX::SimpleMath::Matrix b = maps[std::format("XYZ-{}", out_space)]; + auto c = DirectX::XMMatrixMultiply(b, a); + return c; + } +} diff --git a/src/Features/PostProcessing/Composite.cpp b/src/Features/PostProcessing/Composite.cpp new file mode 100644 index 0000000000..8eaa4ccda7 --- /dev/null +++ b/src/Features/PostProcessing/Composite.cpp @@ -0,0 +1,194 @@ +#include "Composite.h" + +#include "CODBloom.h" +#include "Features/PostProcessing.h" +#include "HistogramAutoExposure.h" +#include "LensFlare.h" +#include "LocalExposure.h" +#include "PhysicalGlare.h" + +#include "State.h" +#include "Util.h" + +void Composite::UpdateAutoEnabled() +{ + if (!owner) + return; + + auto* bloom = owner->GetPipelineFeature(PostProcessing::FeaturePipelineIndex::CODBloom); + auto* flare = owner->GetPipelineFeature(PostProcessing::FeaturePipelineIndex::LensFlare); + auto* glare = owner->GetPipelineFeature(PostProcessing::FeaturePipelineIndex::PhysicalGlare); + auto* exposure = owner->GetPipelineFeature(PostProcessing::FeaturePipelineIndex::AutoExposure); + auto* localExposure = owner->GetPipelineFeature(PostProcessing::FeaturePipelineIndex::LocalExposure); + + enabled = (bloom && bloom->enabled) || (flare && flare->enabled) || (glare && glare->enabled) || (exposure && exposure->enabled) || (localExposure && localExposure->enabled); +} + +void Composite::SetupResources() +{ + auto renderer = globals::game::renderer; + + { + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + D3D11_TEXTURE2D_DESC texDesc; + gameTexMainCopy.texture->GetDesc(&texDesc); + texDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + texDesc.MipLevels = srvDesc.Texture2D.MipLevels = 1; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + + texOutput = eastl::make_unique(texDesc); + texOutput->CreateSRV(srvDesc); + texOutput->CreateUAV(uavDesc); + } + + CompileComputeShaders(); +} + +void Composite::ClearShaderCache() +{ + for (auto& shader : compositeShaders) { + if (shader) { + shader->Release(); + shader.detach(); + } + } + + CompileComputeShaders(); +} + +void Composite::CompileComputeShaders() +{ + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\Composite\\composite.cs.hlsl"); + + // Compile all non-empty flag combinations (1..31) + for (uint flags = 1; flags < CompositeFlags::FLAG_COUNT; flags++) { + std::vector> defines; + if (flags & BLOOM) + defines.push_back({ "HAS_BLOOM", "" }); + if (flags & FLARE) + defines.push_back({ "HAS_LENS_FLARE", "" }); + if (flags & GLARE) + defines.push_back({ "HAS_GLARE", "" }); + if (flags & EXPOSURE) + defines.push_back({ "HAS_EXPOSURE", "" }); + if (flags & LOCAL_EXPOSURE) + defines.push_back({ "HAS_LOCAL_EXPOSURE", "" }); + + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), defines, "cs_5_0", "CSComposite"))) + compositeShaders[flags].attach(rawPtr); + } +} + +void Composite::Draw(TextureInfo& inout_tex) +{ + if (!owner) + return; + + auto* bloom = owner->GetPipelineFeature(PostProcessing::FeaturePipelineIndex::CODBloom); + auto* flare = owner->GetPipelineFeature(PostProcessing::FeaturePipelineIndex::LensFlare); + auto* glare = owner->GetPipelineFeature(PostProcessing::FeaturePipelineIndex::PhysicalGlare); + auto* exposure = owner->GetPipelineFeature(PostProcessing::FeaturePipelineIndex::AutoExposure); + auto* localExposure = owner->GetPipelineFeature(PostProcessing::FeaturePipelineIndex::LocalExposure); + + bool hasBloom = bloom && bloom->enabled; + bool hasFlare = flare && flare->enabled; + bool hasGlare = glare && glare->enabled; + bool hasExposure = exposure && exposure->enabled; + bool hasLocalExposure = localExposure && localExposure->enabled; + + uint flags = (hasBloom ? BLOOM : 0) | (hasFlare ? FLARE : 0) | (hasGlare ? GLARE : 0) | (hasExposure ? EXPOSURE : 0) | (hasLocalExposure ? LOCAL_EXPOSURE : 0); + if (flags == NONE) + return; + + globals::profiler->BeginPass("PostProcessing::Composite"); + auto state = globals::state; + auto context = globals::d3d::context; + + state->BeginPerfEvent("Composite"); + + ID3D11ComputeShader* shader = compositeShaders[flags].get(); + if (!shader) { + state->EndPerfEvent(); + globals::profiler->EndPass(); + return; + } + + // Bind resources: + // t0 = main color (inout_tex) + // t1 = bloom texture (if available) + // t2 = flare texture (if available) + // t3 = glare texture (if available) + // t4 = adaptation buffer (if exposure enabled) + // t5 = local exposure texture (if local exposure enabled) + // u0 = output + // b1 = auto exposure constant buffer (if exposure enabled) + std::array srvs = { nullptr }; + std::array uavs = { nullptr }; + + srvs[0] = inout_tex.srv; + + if (hasBloom) { + auto bloomOutput = bloom->GetBloomOutput(); + srvs[1] = bloomOutput.srv; + } + if (hasFlare) { + auto flareOutput = flare->GetFlareOutput(); + srvs[2] = flareOutput.srv; + } + if (hasGlare) { + auto glareOutput = glare->GetGlareOutput(); + srvs[3] = glareOutput.srv; + } + if (hasExposure) { + srvs[4] = exposure->GetAdaptationSRV(); + + // Bind the auto exposure constant buffer at b1 + ID3D11Buffer* cb = exposure->GetConstantBuffer(); + context->CSSetConstantBuffers(1, 1, &cb); + } + if (hasLocalExposure) { + srvs[5] = localExposure->GetExposureSRV(); + } + + uavs[0] = texOutput->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(shader, nullptr, 0); + + uint width = texOutput->desc.Width; + uint height = texOutput->desc.Height; + context->Dispatch((width + 7) >> 3, (height + 7) >> 3, 1); + + // cleanup + srvs.fill(nullptr); + uavs.fill(nullptr); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(nullptr, nullptr, 0); + + if (hasExposure) { + ID3D11Buffer* nullCB = nullptr; + context->CSSetConstantBuffers(1, 1, &nullCB); + } + + inout_tex = { texOutput->resource.get(), texOutput->srv.get() }; + state->EndPerfEvent(); + globals::profiler->EndPass(); +} diff --git a/src/Features/PostProcessing/Composite.h b/src/Features/PostProcessing/Composite.h new file mode 100644 index 0000000000..0e0b7cba6f --- /dev/null +++ b/src/Features/PostProcessing/Composite.h @@ -0,0 +1,51 @@ +#pragma once + +#include "PostProcessFeature.h" + +#include "Buffer.h" + +struct Composite : public PostProcessFeature +{ + virtual inline std::string GetType() const override { return "Composite"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.composite.name", "Composite"); } + virtual inline std::string GetDesc() const override + { + return T("feature.post_processing.composite.description", + "Composites Bloom, Lens Flare, Physical Glare, and Auto Exposure onto the main image. " + "Applies exposure (SceneColor * Exposure + Bloom * Exposure) before Color Grading. " + "Automatically enabled when any contributing feature is active."); + } + virtual bool IsVisible() const override { return false; } + virtual bool IsAutoEnabled() const override { return true; } + virtual void UpdateAutoEnabled() override; + virtual inline bool DisableInMainLoadingMenu() const override { return true; } + virtual bool WritesToMainTexture() const override { return true; } + + eastl::unique_ptr texOutput = nullptr; + + // Bit flags for shader permutation selection + enum CompositeFlags : uint + { + NONE = 0, + BLOOM = 1 << 0, + FLARE = 1 << 1, + GLARE = 1 << 2, + EXPOSURE = 1 << 3, + LOCAL_EXPOSURE = 1 << 4, + FLAG_COUNT = 32 // 2^5 combinations + }; + + // Shader permutations indexed by composite flags (index 0 unused) + std::array, CompositeFlags::FLAG_COUNT> compositeShaders = {}; + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + + virtual void RestoreDefaultSettings() override {} + virtual void LoadSettings(json&) override {} + virtual void SaveSettings(json&) override {} + virtual void DrawSettings() override {} + + virtual void Draw(TextureInfo&) override; +}; diff --git a/src/Features/PostProcessing/DoF.cpp b/src/Features/PostProcessing/DoF.cpp new file mode 100644 index 0000000000..d52c551dde --- /dev/null +++ b/src/Features/PostProcessing/DoF.cpp @@ -0,0 +1,695 @@ +#include "DoF.h" + +#include "Features/PostProcessing.h" +#include "Menu.h" +#include "State.h" +#include "Util.h" + +#include "I18n/I18n.h" +#include +#include + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + DoF::Settings, + AutoFocus, + TransitionSpeed, + FocusCoord, + ManualFocusPlane, + FocalLength, + FNumber, + FarPlaneMaxBlur, + NearPlaneMaxBlur, + BlurQuality, + NearFarDistanceCompensation, + HighlightBoost, + BokehBusyFactor, + PostBlurSmoothing, + PetzvalStrength, + HighlightShape, + HighlightShapeRotationAngle, + targetFocus, + targetFocusFocalLength, + consoleSelection) + +void DoF::DrawSettings() +{ + ImGui::Checkbox(T("feature.post_processing.do_f.auto_focus", "Auto Focus"), &settings.AutoFocus); + + if (settings.AutoFocus) { + ImGui::SliderFloat2(T("feature.post_processing.do_f.focus_point", "Focus Point"), &settings.FocusCoord.x, 0.0f, 1.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + } + ImGui::SliderFloat(T("feature.post_processing.do_f.transition_speed", "Transition Speed"), &settings.TransitionSpeed, 0.1f, 1.0f, "%.2f"); + ImGui::SliderFloat(T("feature.post_processing.do_f.manual_focus", "Manual Focus"), &settings.ManualFocusPlane, 0.1f, 150.0f, "%.2f m"); + ImGui::SliderFloat(T("feature.post_processing.do_f.focal_length", "Focal Length"), &settings.FocalLength, 1.0f, 300.0f, "%.1f mm"); + ImGui::SliderFloat(T("feature.post_processing.do_f.f_number", "F-Number"), &settings.FNumber, 1.0f, 22.0f, "f/%.1f"); + ImGui::SliderFloat(T("feature.post_processing.do_f.far_plane_max_blur", "Far Plane Max Blur"), &settings.FarPlaneMaxBlur, 0.0f, 8.0f, "%.2f"); + ImGui::SliderFloat(T("feature.post_processing.do_f.near_plane_max_blur", "Near Plane Max Blur"), &settings.NearPlaneMaxBlur, 0.0f, 4.0f, "%.2f"); + ImGui::SliderFloat(T("feature.post_processing.do_f.blur_quality", "Blur Quality"), &settings.BlurQuality, 2.0f, 30.0f, "%.1f"); + ImGui::SliderFloat(T("feature.post_processing.do_f.near_far_plane_distance_compenation", "Near-Far Plane Distance Compenation"), &settings.NearFarDistanceCompensation, 1.0f, 5.0f, "%.2f"); + ImGui::SliderFloat(T("feature.post_processing.do_f.bokeh_busy_factor", "Bokeh Busy Factor"), &settings.BokehBusyFactor, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T("feature.post_processing.do_f.petzval_strength", "Petzval Strength"), &settings.PetzvalStrength, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat(T("feature.post_processing.do_f.highlight_boost", "Highlight Boost"), &settings.HighlightBoost, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T("feature.post_processing.do_f.post_blur_smoothing", "Post Blur Smoothing"), &settings.PostBlurSmoothing, 0.0f, 2.0f, "%.2f"); + ImGui::Combo(T("feature.post_processing.do_f.highlight_custom_shape", "Highlight Custom Shape"), &settings.HighlightShape, "Circle (No custom shape)\0Heart\0Hexagon\0Circle with fringe\0Hexagon with fringe\0Star\0Square\0"); + ImGui::SliderFloat(T("feature.post_processing.do_f.highlight_shape_rotation", "Highlight Shape Rotation"), &settings.HighlightShapeRotationAngle, 0.0f, 1.0f, "%.2f"); + ImGui::Checkbox(T("feature.post_processing.do_f.target_focus", "Target Focus"), &settings.targetFocus); + ImGui::SliderFloat(T("feature.post_processing.do_f.target_focus_focal_length", "Target Focus Focal Length"), &settings.targetFocusFocalLength, 1.0f, 300.0f, "%.1f mm"); + ImGui::Checkbox(T("feature.post_processing.do_f.console_selection", "Console Selection"), &settings.consoleSelection); + if (settings.consoleSelection && currentRef != 0) { + ImGui::Text(T("feature.post_processing.do_f.selected_reference", "Selected Reference: %08X"), currentRef); + } + + if (ImGui::CollapsingHeader(T("feature.post_processing.do_f.debug", "Debug"))) { + static float debugRescale = .3f; + ImGui::Text(T("feature.post_processing.do_f.debug_distance", "Debug Distance: %f"), debugDistance); + ImGui::Text(T("feature.post_processing.do_f.debug_focus_plane", "Debug Focus Plane: %f"), debugFocusPlane); + ImGui::SliderFloat(T("feature.post_processing.do_f.view_resize", "View Resize"), &debugRescale, 0.f, 1.f); + + BUFFER_VIEWER_NODE(texFocus, 64.0f) + BUFFER_VIEWER_NODE(texPreFocus, 64.0f) + + BUFFER_VIEWER_NODE(texCoC, debugRescale) + BUFFER_VIEWER_NODE(texCoCTileTmp, debugRescale) + BUFFER_VIEWER_NODE(texCoCTileTmp2, debugRescale) + BUFFER_VIEWER_NODE(texCoCTileNeighbor, debugRescale) + BUFFER_VIEWER_NODE(texCoCBlur1, debugRescale) + BUFFER_VIEWER_NODE(texCoCBlur2, debugRescale) + + BUFFER_VIEWER_NODE(texPreBlurred, debugRescale) + BUFFER_VIEWER_NODE(texFarBlurred, debugRescale) + BUFFER_VIEWER_NODE(texNearBlurred, debugRescale) + + BUFFER_VIEWER_NODE(texBlurredFiltered, debugRescale) + BUFFER_VIEWER_NODE(texPostSmooth, debugRescale) + BUFFER_VIEWER_NODE(texPostSmooth2, debugRescale) + } +} + +void DoF::RestoreDefaultSettings() +{ + settings = {}; +} + +void DoF::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void DoF::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void DoF::SetupResources() +{ + auto renderer = globals::game::renderer; + auto device = globals::d3d::device; + + logger::debug("Creating buffers..."); + { + dofCB = eastl::make_unique(ConstantBufferDesc()); + } + + logger::debug("Creating 2D textures..."); + { + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + D3D11_TEXTURE2D_DESC texDesc; + gameTexMainCopy.texture->GetDesc(&texDesc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + texDesc.MipLevels = srvDesc.Texture2D.MipLevels = 1; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + + texOutput = eastl::make_unique(texDesc); + texOutput->CreateSRV(srvDesc); + texOutput->CreateUAV(uavDesc); + + texBlurredFull = eastl::make_unique(texDesc); + texBlurredFull->CreateSRV(srvDesc); + texBlurredFull->CreateUAV(uavDesc); + + texPostSmooth = eastl::make_unique(texDesc); + texPostSmooth->CreateSRV(srvDesc); + texPostSmooth->CreateUAV(uavDesc); + + texPostSmooth2 = eastl::make_unique(texDesc); + texPostSmooth2->CreateSRV(srvDesc); + texPostSmooth2->CreateUAV(uavDesc); + + D3D11_TEXTURE2D_DESC texDescHalf = texDesc; + texDescHalf.Width /= 2; + texDescHalf.Height /= 2; + + texPreBlurred = eastl::make_unique(texDescHalf); + texPreBlurred->CreateSRV(srvDesc); + texPreBlurred->CreateUAV(uavDesc); + + texFarBlurred = eastl::make_unique(texDescHalf); + texFarBlurred->CreateSRV(srvDesc); + texFarBlurred->CreateUAV(uavDesc); + + texNearBlurred = eastl::make_unique(texDescHalf); + texNearBlurred->CreateSRV(srvDesc); + texNearBlurred->CreateUAV(uavDesc); + + texBlurredFiltered = eastl::make_unique(texDescHalf); + texBlurredFiltered->CreateSRV(srvDesc); + texBlurredFiltered->CreateUAV(uavDesc); + + texDesc.Format = DXGI_FORMAT_R32_FLOAT; + texDescHalf.Format = DXGI_FORMAT_R32_FLOAT; + srvDesc.Format = DXGI_FORMAT_R32_FLOAT; + uavDesc.Format = DXGI_FORMAT_R32_FLOAT; + + texCoC = eastl::make_unique(texDesc); + texCoC->CreateSRV(srvDesc); + texCoC->CreateUAV(uavDesc); + + texCoCTileTmp = eastl::make_unique(texDesc); + texCoCTileTmp->CreateSRV(srvDesc); + texCoCTileTmp->CreateUAV(uavDesc); + + texCoCTileTmp2 = eastl::make_unique(texDesc); + texCoCTileTmp2->CreateSRV(srvDesc); + texCoCTileTmp2->CreateUAV(uavDesc); + + texCoCTileNeighbor = eastl::make_unique(texDesc); + texCoCTileNeighbor->CreateSRV(srvDesc); + texCoCTileNeighbor->CreateUAV(uavDesc); + + texCoCBlur1 = eastl::make_unique(texDescHalf); + texCoCBlur1->CreateSRV(srvDesc); + texCoCBlur1->CreateUAV(uavDesc); + + texCoCBlur2 = eastl::make_unique(texDescHalf); + texCoCBlur2->CreateSRV(srvDesc); + texCoCBlur2->CreateUAV(uavDesc); + + texDesc.Width = 1; + texDesc.Height = 1; + + texFocus = eastl::make_unique(texDesc); + texFocus->CreateSRV(srvDesc); + texFocus->CreateUAV(uavDesc); + + texPreFocus = eastl::make_unique(texDesc); + texPreFocus->CreateSRV(srvDesc); + texPreFocus->CreateUAV(uavDesc); + + g_TDM = reinterpret_cast(TDM_API::RequestPluginAPI(TDM_API::InterfaceVersion::V2)); + } + + // Bokeh shapes are loaded by PostProcessing::bokehResources (shared with LensFlare) + + logger::debug("Creating samplers..."); + { + D3D11_SAMPLER_DESC samplerDesc = { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_MIRROR, + .AddressV = D3D11_TEXTURE_ADDRESS_MIRROR, + .AddressW = D3D11_TEXTURE_ADDRESS_MIRROR, + .MaxAnisotropy = 1, + .MinLOD = 0, + .MaxLOD = D3D11_FLOAT32_MAX + }; + DX::ThrowIfFailed(device->CreateSamplerState(&samplerDesc, linearSampler.put())); + } + + CompileComputeShaders(); +} + +void DoF::ClearShaderCache() +{ + const auto shaderPtrs = std::array{ + &UpdateFocusCS, + &CalculateCoCCS, + &CoCTile1CS, + &CoCTile2CS, + &CoCTileNeighbor, + &CoCGaussian1CS, + &CoCGaussian2CS, + &BlurCS, + &FarBlurCS, + &NearBlurCS, + &TentFilterCS, + &CombinerCS, + &PostSmoothing1CS, + &PostSmoothing2AndFocusingCS + }; + + for (auto shader : shaderPtrs) + if ((*shader)) { + (*shader)->Release(); + shader->detach(); + } + + CompileComputeShaders(); +} + +void DoF::CompileComputeShaders() +{ + struct ShaderCompileInfo + { + winrt::com_ptr* programPtr; + std::string_view filename; + std::vector> defines; + std::string entry = "main"; + }; + + std::vector + shaderInfos = { + { &UpdateFocusCS, "dof.cs.hlsl", {}, "CS_UpdateFocus" }, + { &CalculateCoCCS, "dof.cs.hlsl", {}, "CS_CalculateCoC" }, + { &CoCTile1CS, "dof.cs.hlsl", {}, "CS_CoCTile1" }, + { &CoCTile2CS, "dof.cs.hlsl", {}, "CS_CoCTile2" }, + { &CoCTileNeighbor, "dof.cs.hlsl", {}, "CS_CoCTileNeighbor" }, + { &CoCGaussian1CS, "dof.cs.hlsl", {}, "CS_CoCGaussian1" }, + { &CoCGaussian2CS, "dof.cs.hlsl", {}, "CS_CoCGaussian2" }, + { &BlurCS, "dof.cs.hlsl", {}, "CS_Blur" }, + { &FarBlurCS, "dof.cs.hlsl", {}, "CS_FarBlur" }, + { &NearBlurCS, "dof.cs.hlsl", {}, "CS_NearBlur" }, + { &TentFilterCS, "dof.cs.hlsl", {}, "CS_TentFilter" }, + { &CombinerCS, "dof.cs.hlsl", {}, "CS_Combiner" }, + { &PostSmoothing1CS, "dof.cs.hlsl", {}, "CS_PostSmoothing1" }, + { &PostSmoothing2AndFocusingCS, "dof.cs.hlsl", {}, "CS_PostSmoothing2AndFocusing" } + }; + + for (auto& info : shaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\DoF") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } +} + +// Thanks Ershin! +RE::NiPoint3 DoF::GetCameraPos() +{ + auto player = RE::PlayerCharacter::GetSingleton(); + auto playerCamera = RE::PlayerCamera::GetSingleton(); + RE::NiPoint3 ret; + + if (playerCamera->currentState == playerCamera->GetRuntimeData().cameraStates[RE::CameraStates::kFirstPerson] || + playerCamera->currentState == playerCamera->GetRuntimeData().cameraStates[RE::CameraStates::kThirdPerson] || + playerCamera->currentState == playerCamera->GetRuntimeData().cameraStates[RE::CameraStates::kMount]) { + RE::NiNode* root = playerCamera->cameraRoot.get(); + if (root) { + ret.x = root->world.translate.x; + ret.y = root->world.translate.y; + ret.z = root->world.translate.z; + } + } else if (playerCamera->IsInFreeCameraMode()) { + auto freeCameraState = static_cast(playerCamera->currentState.get()); + ret = freeCameraState->translation; + } else { + RE::NiPoint3 playerPos = player->GetLookingAtLocation(); + + ret.z = playerPos.z; + ret.x = player->GetPositionX(); + ret.y = player->GetPositionY(); + } + + return ret; +} + +bool DoF::GetTargetLockEnabled() +{ + return g_TDM && g_TDM->GetCurrentTarget(); +} + +bool DoF::GetInDialogue() +{ + return RE::MenuTopicManager::GetSingleton()->speaker || RE::MenuTopicManager::GetSingleton()->lastSpeaker; +} + +float DoF::GetDistanceToReference(RE::TESObjectREFR* a_ref) +{ + RE::NiPoint3 cameraPosition = GetCameraPos(); + RE::NiPoint3 targetPosition = a_ref->GetPosition(); + if (a_ref->GetFormType() == RE::FormType::ActorCharacter && !a_ref->IsPlayer()) { + auto head = a_ref->GetNodeByName("NPC Head [Head]"); + if (head) { + targetPosition = head->world.translate; + } + } + return cameraPosition.GetDistance(targetPosition); +} + +void DoF::Draw(TextureInfo& inout_tex) +{ + auto state = globals::state; + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + auto* depthSRV = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].depthSRV; + if (!depthSRV) { + return; + } + + float2 res = { (float)texOutput->desc.Width, (float)texOutput->desc.Height }; + + float focusLen = settings.FocalLength; + float nearBlur = settings.NearPlaneMaxBlur; + float manualFocus = settings.ManualFocusPlane / 1000.0f; + debugFocusPlane = manualFocus; + bool autoFocus = settings.AutoFocus; + + if (settings.targetFocus) { + focusLen = 1.0f; + nearBlur = 0.0f; + float targetFocusDistanceGame = 0; + auto targetFocusEnabled = false; + autoFocus = false; + + RE::TESObjectREFR* target = nullptr; + const auto consoleRef = RE::Console::GetSelectedRef(); + if (settings.consoleSelection) + if (consoleRef && !consoleRef->IsDisabled() && !consoleRef->IsDeleted() && consoleRef->Is3DLoaded()) { + currentRef = consoleRef->formID; + target = consoleRef.get(); + targetFocusEnabled = true; + } else { + currentRef = 0; + } + + if (GetTargetLockEnabled()) { + target = g_TDM->GetCurrentTarget().get().get(); + targetFocusEnabled = true; + } + + if (GetInDialogue()) { + if (RE::MenuTopicManager::GetSingleton()->speaker) { + target = RE::MenuTopicManager::GetSingleton()->speaker.get().get(); + } else { + target = RE::MenuTopicManager::GetSingleton()->lastSpeaker.get().get(); + } + targetFocusEnabled = true; + } + if (target) + targetFocusDistanceGame = GetDistanceToReference(target); + debugDistance = targetFocusDistanceGame; + if (targetFocusEnabled) { + nearBlur = settings.NearPlaneMaxBlur; + focusLen = settings.targetFocusFocalLength; + manualFocus = Util::Units::GameUnitsToMeters(targetFocusDistanceGame) * 0.001f; // in KM + } else { + return; + } + } + debugFocusPlane = manualFocus; + state->BeginPerfEvent("Depth of Field"); + DoFCB dofData = { + .TransitionSpeed = settings.TransitionSpeed, + .FocusCoord = settings.FocusCoord, + .ManualFocusPlane = manualFocus, + .FocalLength = focusLen, + .FNumber = settings.FNumber, + .FarPlaneMaxBlur = settings.FarPlaneMaxBlur, + .NearPlaneMaxBlur = nearBlur, + .BlurQuality = settings.BlurQuality, + .NearFarDistanceCompensation = settings.NearFarDistanceCompensation, + .BokehBusyFactor = settings.BokehBusyFactor, + .HighlightBoost = settings.HighlightBoost, + .PostBlurSmoothing = settings.PostBlurSmoothing, + .HighlightShape = (uint)settings.HighlightShape, + .HighlightShapeRotationAngle = settings.HighlightShapeRotationAngle, + .PetzvalStrength = settings.PetzvalStrength, + .AutoFocus = autoFocus + }; + dofCB->Update(dofData); + + std::array srvs = { inout_tex.srv, texPreFocus->srv.get(), depthSRV, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; + std::array uavs = { texOutput->uav.get(), texFocus->uav.get(), texCoC->uav.get() }; + std::array samplers = { linearSampler.get() }; + auto cb = dofCB->CB(); + auto resetViews = [&]() { + srvs.fill(nullptr); + uavs.fill(nullptr); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + }; + + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); + uint dispatchWidth = ((uint)res.x + 7) >> 3; + uint dispatchHeight = ((uint)res.y + 7) >> 3; + uint dispatchWidthBlur = ((uint)(res.x / 2) + 7) >> 3; + uint dispatchHeightBlur = ((uint)(res.y / 2) + 7) >> 3; + + // Update Focus + { + srvs.at(0) = inout_tex.srv; + srvs.at(1) = texPreFocus->srv.get(); + srvs.at(2) = depthSRV; + uavs.at(1) = texFocus->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(UpdateFocusCS.get(), nullptr, 0); + context->Dispatch(1, 1, 1); + } + + resetViews(); + context->CopyResource(texPreFocus->resource.get(), texFocus->resource.get()); + + // Calculate CoC + { + globals::profiler->BeginPass("PostProcessing::DoF::CoC"); + state->BeginPerfEvent("Calculate CoC"); + srvs.at(0) = inout_tex.srv; + srvs.at(1) = texPreFocus->srv.get(); + srvs.at(2) = depthSRV; + uavs.at(2) = texCoC->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(CalculateCoCCS.get(), nullptr, 0); + context->Dispatch(dispatchWidth, dispatchHeight, 1); + state->EndPerfEvent(); + globals::profiler->EndPass(); + } + + resetViews(); + + // CoC Tile + { + globals::profiler->BeginPass("PostProcessing::DoF::CoCTile"); + state->BeginPerfEvent("CoC Tile"); + srvs.at(3) = texCoC->srv.get(); + uavs.at(2) = texCoCTileTmp->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(CoCTile1CS.get(), nullptr, 0); + context->Dispatch(dispatchWidth, dispatchHeight, 1); + + resetViews(); + + srvs.at(3) = texCoCTileTmp->srv.get(); + uavs.at(2) = texCoCTileTmp2->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(CoCTile2CS.get(), nullptr, 0); + context->Dispatch(dispatchWidth, dispatchHeight, 1); + + resetViews(); + + srvs.at(3) = texCoCTileTmp2->srv.get(); + uavs.at(2) = texCoCTileNeighbor->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(CoCTileNeighbor.get(), nullptr, 0); + context->Dispatch(dispatchWidth, dispatchHeight, 1); + + resetViews(); + state->EndPerfEvent(); + globals::profiler->EndPass(); + } + + // CoC Gaussian Blur (coc uses srv3 and uav2) + { + globals::profiler->BeginPass("PostProcessing::DoF::CoCBlur"); + state->BeginPerfEvent("CoC Gaussian Blur"); + srvs.at(3) = texCoCTileNeighbor->srv.get(); + uavs.at(2) = texCoCBlur1->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(CoCGaussian1CS.get(), nullptr, 0); + context->Dispatch(dispatchWidthBlur, dispatchHeightBlur, 1); + + resetViews(); + + srvs.at(3) = texCoCBlur1->srv.get(); + uavs.at(2) = texCoCBlur2->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(CoCGaussian2CS.get(), nullptr, 0); + context->Dispatch(dispatchWidthBlur, dispatchHeightBlur, 1); + + resetViews(); + state->EndPerfEvent(); + globals::profiler->EndPass(); + } + + // Blur + { + globals::profiler->BeginPass("PostProcessing::DoF::PreBlur"); + state->BeginPerfEvent("Pre Blur"); + srvs.at(0) = inout_tex.srv; + srvs.at(3) = texCoC->srv.get(); + srvs.at(4) = texCoCBlur2->srv.get(); + uavs.at(0) = texPreBlurred->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(BlurCS.get(), nullptr, 0); + context->Dispatch(dispatchWidthBlur, dispatchHeightBlur, 1); + + resetViews(); + state->EndPerfEvent(); + globals::profiler->EndPass(); + + globals::profiler->BeginPass("PostProcessing::DoF::FarBlur"); + state->BeginPerfEvent("Far Blur"); + srvs.at(0) = texPreBlurred->srv.get(); + srvs.at(3) = texCoC->srv.get(); + srvs.at(4) = texCoCBlur2->srv.get(); + if (owner) + srvs.at(8) = owner->bokehResources.GetShapeSRV(std::clamp(settings.HighlightShape - 1, 0, BokehResources::NUM_BUILTIN_SHAPES - 1)); + uavs.at(0) = texFarBlurred->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(FarBlurCS.get(), nullptr, 0); + context->Dispatch(dispatchWidthBlur, dispatchHeightBlur, 1); + + resetViews(); + state->EndPerfEvent(); + globals::profiler->EndPass(); + + globals::profiler->BeginPass("PostProcessing::DoF::NearBlur"); + state->BeginPerfEvent("Near Blur"); + srvs.at(0) = texFarBlurred->srv.get(); + srvs.at(3) = texCoCTileNeighbor->srv.get(); + srvs.at(4) = texCoCBlur2->srv.get(); + if (owner) + srvs.at(8) = owner->bokehResources.GetShapeSRV(std::clamp(settings.HighlightShape - 1, 0, BokehResources::NUM_BUILTIN_SHAPES - 1)); + uavs.at(0) = texNearBlurred->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(NearBlurCS.get(), nullptr, 0); + context->Dispatch(dispatchWidthBlur, dispatchHeightBlur, 1); + + resetViews(); + state->EndPerfEvent(); + globals::profiler->EndPass(); + } + + // Tent Filter + { + globals::profiler->BeginPass("PostProcessing::DoF::TentFilter"); + state->BeginPerfEvent("Tent Filter"); + srvs.at(0) = texFarBlurred->srv.get(); + uavs.at(0) = texBlurredFiltered->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(TentFilterCS.get(), nullptr, 0); + context->Dispatch(dispatchWidthBlur, dispatchHeightBlur, 1); + + resetViews(); + state->EndPerfEvent(); + globals::profiler->EndPass(); + } + + // Combiner + { + globals::profiler->BeginPass("PostProcessing::DoF::Combiner"); + state->BeginPerfEvent("Combiner"); + srvs.at(0) = inout_tex.srv; + srvs.at(3) = texCoC->srv.get(); + srvs.at(5) = texBlurredFiltered->srv.get(); + srvs.at(6) = texNearBlurred->srv.get(); + uavs.at(0) = texPostSmooth->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(CombinerCS.get(), nullptr, 0); + context->Dispatch(dispatchWidth, dispatchHeight, 1); + + resetViews(); + state->EndPerfEvent(); + globals::profiler->EndPass(); + } + + // Post Smooth + { + globals::profiler->BeginPass("PostProcessing::DoF::PostSmooth"); + state->BeginPerfEvent("Post Smooth"); + srvs.at(0) = texPostSmooth->srv.get(); + srvs.at(3) = texCoC->srv.get(); + uavs.at(0) = texPostSmooth2->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(PostSmoothing1CS.get(), nullptr, 0); + context->Dispatch(dispatchWidth, dispatchHeight, 1); + + resetViews(); + + srvs.at(0) = texPostSmooth->srv.get(); + srvs.at(3) = texCoC->srv.get(); + srvs.at(7) = texPostSmooth2->srv.get(); + uavs.at(0) = texOutput->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(PostSmoothing2AndFocusingCS.get(), nullptr, 0); + context->Dispatch(dispatchWidth, dispatchHeight, 1); + + resetViews(); + state->EndPerfEvent(); + globals::profiler->EndPass(); + } + + samplers.fill(nullptr); + cb = nullptr; + + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); + context->CSSetShader(nullptr, nullptr, 0); + + inout_tex = { texOutput->resource.get(), texOutput->srv.get() }; + state->EndPerfEvent(); +} diff --git a/src/Features/PostProcessing/DoF.h b/src/Features/PostProcessing/DoF.h new file mode 100644 index 0000000000..aa354c5b98 --- /dev/null +++ b/src/Features/PostProcessing/DoF.h @@ -0,0 +1,118 @@ +#pragma once + +#define TDM_API_COMMONLIB +#include "TDM/TrueDirectionalMovementAPI.h" + +#include "Buffer.h" +#include "PostProcessFeature.h" + +struct DoF : public PostProcessFeature +{ + virtual inline std::string GetType() const override { return "Depth of Field"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.do_f.name", "Depth of Field"); } + virtual inline std::string GetDesc() const override { return T("feature.post_processing.do_f.description", "Depth of Field, based on CinematicDOF by Frans Bouma."); } + virtual inline bool DisableInMainLoadingMenu() const override { return true; } + + struct Settings + { + bool AutoFocus = true; + float TransitionSpeed = 0.5f; + float2 FocusCoord = float2(0.5f, 0.5f); + float ManualFocusPlane = 0.4f; + float FocalLength = 50.0f; + float FNumber = 2.8f; + float FarPlaneMaxBlur = 1.0f; + float NearPlaneMaxBlur = 1.0f; + float BlurQuality = 7.0f; + float NearFarDistanceCompensation = 1.0f; + float BokehBusyFactor = 0.5f; + float HighlightBoost = 0.0f; + float PostBlurSmoothing = 0.0f; + float PetzvalStrength = 0.0f; + int HighlightShape = 0; + float HighlightShapeRotationAngle = 0.0f; + bool targetFocus = false; + float targetFocusFocalLength = 50.0f; + bool consoleSelection = false; + } settings; + + struct alignas(16) DoFCB + { + float TransitionSpeed; + float2 FocusCoord; + float ManualFocusPlane; + float FocalLength; + float FNumber; + float FarPlaneMaxBlur; + float NearPlaneMaxBlur; + float BlurQuality; + float NearFarDistanceCompensation; + float BokehBusyFactor; + float HighlightBoost; + float PostBlurSmoothing; + uint HighlightShape; + float HighlightShapeRotationAngle; + float PetzvalStrength; + uint AutoFocus; + uint pad[3]; + }; + + eastl::unique_ptr dofCB = nullptr; + + eastl::unique_ptr texOutput = nullptr; + eastl::unique_ptr texPreBlurred = nullptr; + eastl::unique_ptr texFarBlurred = nullptr; + eastl::unique_ptr texNearBlurred = nullptr; + eastl::unique_ptr texBlurredFiltered = nullptr; + eastl::unique_ptr texBlurredFull = nullptr; + eastl::unique_ptr texPostSmooth = nullptr; + eastl::unique_ptr texPostSmooth2 = nullptr; + eastl::unique_ptr texFocus = nullptr; + eastl::unique_ptr texPreFocus = nullptr; + eastl::unique_ptr texCoC = nullptr; + eastl::unique_ptr texCoCTileTmp = nullptr; + eastl::unique_ptr texCoCTileTmp2 = nullptr; + eastl::unique_ptr texCoCTileNeighbor = nullptr; + eastl::unique_ptr texCoCBlur1 = nullptr; + eastl::unique_ptr texCoCBlur2 = nullptr; + // Bokeh shapes are provided by PostProcessing::bokehResources (shared with LensFlare) + + winrt::com_ptr UpdateFocusCS = nullptr; + winrt::com_ptr CalculateCoCCS = nullptr; + winrt::com_ptr CoCTile1CS = nullptr; + winrt::com_ptr CoCTile2CS = nullptr; + winrt::com_ptr CoCTileNeighbor = nullptr; + winrt::com_ptr CoCGaussian1CS = nullptr; + winrt::com_ptr CoCGaussian2CS = nullptr; + winrt::com_ptr BlurCS = nullptr; + winrt::com_ptr FarBlurCS = nullptr; + winrt::com_ptr NearBlurCS = nullptr; + winrt::com_ptr TentFilterCS = nullptr; + winrt::com_ptr CombinerCS = nullptr; + winrt::com_ptr PostSmoothing1CS = nullptr; + winrt::com_ptr PostSmoothing2AndFocusingCS = nullptr; + + winrt::com_ptr linearSampler = nullptr; + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json&) override; + virtual void SaveSettings(json&) override; + + virtual void DrawSettings() override; + + virtual void Draw(TextureInfo&) override; + + RE::NiPoint3 GetCameraPos(); + bool GetInDialogue(); + bool GetTargetLockEnabled(); + float GetDistanceToReference(RE::TESObjectREFR* a_ref); + float debugDistance = 0.0f; + float debugFocusPlane = 0.0f; + uint currentRef = 0; + + TDM_API::IVTDM2* g_TDM = nullptr; +}; diff --git a/src/Features/PostProcessing/HistogramAutoExposure.cpp b/src/Features/PostProcessing/HistogramAutoExposure.cpp new file mode 100644 index 0000000000..d237a51615 --- /dev/null +++ b/src/Features/PostProcessing/HistogramAutoExposure.cpp @@ -0,0 +1,360 @@ +#include "HistogramAutoExposure.h" + +#include "I18n/I18n.h" +#include "Menu.h" +#include "State.h" +#include "Util.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + HistogramAutoExposure::Settings, + ExposureCompensation, + AdaptationRange, + AdaptArea, + AdaptSpeed, + PurkinjeStartEV, + PurkinjeMaxEV, + PurkinjeStrength) + +void HistogramAutoExposure::DrawSettings() +{ + ImGui::SliderFloat(T("feature.post_processing.histogram_auto_exposure.exposure_compensation", "Exposure Compensation"), &settings.ExposureCompensation, -5.f, 5.f, "%+.2f EV"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.applying_additional_exposure_adjustment_to_the_image", "Applying additional exposure adjustment to the image.")); + + ImGui::SliderFloat(T("feature.post_processing.histogram_auto_exposure.adaptation_speed", "Adaptation Speed"), &settings.AdaptSpeed, 0.1f, 5.f, "%.2f"); + ImGui::SliderFloat2(T("feature.post_processing.histogram_auto_exposure.focus_area", "Focus Area"), &settings.AdaptArea.x, 0.f, 1.f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.specifies_the_proportion_of_the_area_width_height", "Specifies the proportion of the area [width, height] that auto exposure will adapt to.")); + + ImGui::SliderFloat2(T("feature.post_processing.histogram_auto_exposure.adaptation_range", "Adaptation Range"), &settings.AdaptationRange.x, -10.f, 21.f, "%.2f EV100"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.histogram_auto_exposure.min_max_the_average_scene_luminance_will_be", + "[Min, Max] The average scene luminance will be clamped between them when doing auto exposure." + "Turning up the minimum, for example, makes it adapt less to darkness and therefore prevents over-brightening of dark scenes.")); + + if (ImGui::TreeNodeEx(T("feature.post_processing.histogram_auto_exposure.purkinje_effect", "Purkinje Effect"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::TextWrapped( + T("feature.post_processing.histogram_auto_exposure.the_purkinje_effect_simulates_the_blue_shift_of", + "The Purkinje effect simulates the blue shift of human vision under low light.\n" + "If you don't like the effect, you can set the strength to zero.")); + + ImGui::SliderFloat(T("feature.post_processing.histogram_auto_exposure.max_strength", "Max Strength"), &settings.PurkinjeStrength, 0.f, 5.f, "%.2f"); + ImGui::SliderFloat(T("feature.post_processing.histogram_auto_exposure.fade_in_ev", "Fade In EV100"), &settings.PurkinjeStartEV, -10.f, 3.f, "%.2f EV100"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.the_purkinje_effect_will_start_to_take_place", "The Purkinje effect will start to take place when the average scene luminance falls lower than this.")); + ImGui::SliderFloat(T("feature.post_processing.histogram_auto_exposure.max_effect_ev", "Max Effect EV100"), &settings.PurkinjeMaxEV, -7.f, 3.f, "%.2f EV100"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.from_this_point_onward_the_purkinje_effect_remains", "From this point onward, the Purkinje effect remains the greatest.")); + + ImGui::TreePop(); + } + + if (ImGui::CollapsingHeader(T("feature.post_processing.histogram_auto_exposure.histogram", "Histogram"), ImGuiTreeNodeFlags_DefaultOpen)) { + histogramReadbackRequested = true; + histogramReadbackRequestFrame = ImGui::GetFrameCount(); + + constexpr float kMinEV100 = -10.f; + constexpr float kMaxEV100 = 21.f; + constexpr int kHistogramBins = 256; + constexpr int kFirstLuminanceBin = 1; + constexpr int kLastLuminanceBin = kHistogramBins - 1; + constexpr float kMiddleGray = 0.18f; + + const float adaptedLum = std::max(adaptationValue, 1e-5f); + const float adaptedEV100 = log2(adaptedLum) + 3.0f; + const float compensationEV = settings.ExposureCompensation; + const float compensationScale = exp2(compensationEV); + const float clampedAdaptedLum = std::clamp(adaptedLum, exp2(settings.AdaptationRange.x - 3.0f), exp2(settings.AdaptationRange.y - 3.0f)); + const float compensatedTargetLum = clampedAdaptedLum / std::max(compensationScale, 1e-5f); + const float compensatedTargetEV100 = log2(compensatedTargetLum) + 3.0f; + const float finalExposure = kMiddleGray * compensationScale / clampedAdaptedLum; + const float finalExposureEV = log2(std::max(finalExposure, 1e-5f)); + + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.adapted_luminance_ev", "Adapted Luminance: %.6g (%.2f EV100)"), adaptedLum, adaptedEV100); + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.compensated_target_ev", "Compensated Target: %.6g (%.2f EV100)"), compensatedTargetLum, compensatedTargetEV100); + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.final_global_exposure_ev", "Final Global Exposure: %.6g (%+.2f EV)"), finalExposure, finalExposureEV); + + float maxBin = 1.f; + for (int i = 0; i < kHistogramBins; i++) { + maxBin = std::max(maxBin, static_cast(histogramData[i])); + } + + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.luminance_histogram_ev", "Luminance Histogram (%.0f - %.0f EV100)"), kMinEV100, kMaxEV100); + const ImVec2 canvasPos = ImGui::GetCursorScreenPos(); + const ImVec2 canvasSize = ImVec2(ImGui::GetContentRegionAvail().x, 120.f); + ImGui::InvisibleButton("##histogram_canvas", canvasSize); + + auto* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(canvasPos, ImVec2(canvasPos.x + canvasSize.x, canvasPos.y + canvasSize.y), IM_COL32(18, 18, 18, 255)); + drawList->AddRect(canvasPos, ImVec2(canvasPos.x + canvasSize.x, canvasPos.y + canvasSize.y), IM_COL32(80, 80, 80, 255)); + + const float binWidth = canvasSize.x / static_cast(kHistogramBins); + for (int i = 0; i < kHistogramBins; i++) { + const float binValue = static_cast(histogramData[i]); + const float barHeight = canvasSize.y * std::clamp(binValue / maxBin, 0.f, 1.f); + const float x0 = canvasPos.x + static_cast(i) * binWidth; + const float x1 = canvasPos.x + static_cast(i + 1) * binWidth; + const float y0 = canvasPos.y + canvasSize.y - barHeight; + const ImU32 color = i == 0 ? IM_COL32(90, 90, 90, 180) : IM_COL32(90, 150, 220, 220); + drawList->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, canvasPos.y + canvasSize.y), color); + } + + auto evToX = [&](float ev) { + const float norm = std::clamp((ev - kMinEV100) / (kMaxEV100 - kMinEV100), 0.f, 1.f); + const float bin = static_cast(kFirstLuminanceBin) + norm * static_cast(kLastLuminanceBin - kFirstLuminanceBin); + return canvasPos.x + (bin + 0.5f) * binWidth; + }; + + auto drawMarker = [&](float ev, ImU32 color) { + const float x = evToX(ev); + drawList->AddLine(ImVec2(x, canvasPos.y), ImVec2(x, canvasPos.y + canvasSize.y), color, 2.f); + }; + + drawMarker(settings.AdaptationRange.x, IM_COL32(255, 200, 0, 255)); + drawMarker(settings.AdaptationRange.y, IM_COL32(255, 200, 0, 255)); + drawMarker(adaptedEV100, IM_COL32(0, 255, 0, 255)); + drawMarker(compensatedTargetEV100, IM_COL32(0, 220, 255, 255)); + + if (ImGui::IsItemHovered()) { + const float mouseX = ImGui::GetIO().MousePos.x; + const int bin = std::clamp(static_cast((mouseX - canvasPos.x) / binWidth), 0, kHistogramBins - 1); + ImGui::BeginTooltip(); + if (bin == 0) { + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.bin_0_below_luminance_threshold", "Bin 0: below luminance threshold")); + } else { + const float histogramPos = static_cast(bin - kFirstLuminanceBin) / static_cast(kLastLuminanceBin - kFirstLuminanceBin); + const float ev = histogramPos * (kMaxEV100 - kMinEV100) + kMinEV100; + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.bin", "Bin: %d"), bin); + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.luminance", "Luminance: %.6g"), exp2(ev - 3.0f)); + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.ev", "EV100: %.2f"), ev); + } + ImGui::Text(T("feature.post_processing.histogram_auto_exposure.samples", "Samples: %u"), histogramData[bin]); + ImGui::EndTooltip(); + } + + ImGui::TextColored(ImVec4(0, 1, 0, 1), T("feature.post_processing.histogram_auto_exposure.green_adapted_ev", "Green: Adapted EV100")); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0, 0.86f, 1, 1), T("feature.post_processing.histogram_auto_exposure.cyan_compensation_target", "Cyan: Compensation Target")); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0.8f, 0, 1), T("feature.post_processing.histogram_auto_exposure.yellow_adaptation_range", "Yellow: Adaptation Range")); + } else { + histogramReadbackRequested = false; + } +} + +void HistogramAutoExposure::RestoreDefaultSettings() +{ + settings = {}; +} + +void HistogramAutoExposure::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void HistogramAutoExposure::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void HistogramAutoExposure::SetupResources() +{ + logger::debug("Creating buffers..."); + { + autoExposureCB = std::make_unique(ConstantBufferDesc()); + + histogramSB = std::make_unique(StructuredBufferDesc(256u, false), 256); + histogramSB->CreateUAV(); + + adaptationSB = std::make_unique(StructuredBufferDesc(1u, false), 1); + adaptationSB->CreateSRV(); + adaptationSB->CreateUAV(); + } + + // Create staging buffers for histogram readback + { + auto device = globals::d3d::device; + + D3D11_BUFFER_DESC stagingDesc{}; + stagingDesc.ByteWidth = sizeof(uint32_t) * 256; + stagingDesc.Usage = D3D11_USAGE_STAGING; + stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + stagingDesc.StructureByteStride = sizeof(uint32_t); + stagingDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; + device->CreateBuffer(&stagingDesc, nullptr, histogramStagingBuffer.put()); + + D3D11_BUFFER_DESC adaptDesc{}; + adaptDesc.ByteWidth = sizeof(float); + adaptDesc.Usage = D3D11_USAGE_STAGING; + adaptDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + adaptDesc.StructureByteStride = sizeof(float); + adaptDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; + device->CreateBuffer(&adaptDesc, nullptr, adaptationStagingBuffer.put()); + } + + CompileComputeShaders(); +} + +void HistogramAutoExposure::ClearShaderCache() +{ + const auto shaderPtrs = std::array{ + &histogramCS, &histogramAvgCS + }; + + for (auto shader : shaderPtrs) + if ((*shader)) { + (*shader)->Release(); + shader->detach(); + } + + CompileComputeShaders(); +} + +void HistogramAutoExposure::CompileComputeShaders() +{ + struct ShaderCompileInfo + { + winrt::com_ptr* programPtr; + std::string_view filename; + std::vector> defines; + std::string entry = "main"; + }; + + std::vector + shaderInfos = { + { &histogramCS, "histogram.cs.hlsl", {}, "CS_Histogram" }, + { &histogramAvgCS, "histogram.cs.hlsl", {}, "CS_Average" }, + }; + + for (auto& info : shaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\HistogramAutoExposure") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } +} + +void HistogramAutoExposure::Draw(TextureInfo& inout_tex) +{ + auto context = globals::d3d::context; + auto state = globals::state; + + float exposureCompensation = settings.ExposureCompensation; + float2 adaptationRange = settings.AdaptationRange; + + AutoExposureCB cbData = { + .AdaptArea = settings.AdaptArea, + .AdaptationRange = { exp2(adaptationRange.x - 3.0f), exp2(adaptationRange.y - 3.0f) }, + .AdaptLerp = std::clamp(1.f - exp(-RE::BSTimer::GetSingleton()->realTimeDelta * settings.AdaptSpeed), 0.f, 1.f), + .ExposureCompensation = exp2(exposureCompensation), + .PurkinjeStartEV = settings.PurkinjeStartEV, + .PurkinjeMaxEV = settings.PurkinjeMaxEV, + .PurkinjeStrength = settings.PurkinjeStrength, + }; + autoExposureCB->Update(cbData); + + std::array srvs = { nullptr }; + std::array uavs = { nullptr }; + ID3D11Buffer* cb = autoExposureCB->CB(); + + auto resetViews = [&]() { + srvs.fill(nullptr); + uavs.fill(nullptr); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + }; + + context->CSSetConstantBuffers(1, 1, &cb); + globals::profiler->BeginPass("PostProcessing::HistogramAutoExposure"); + state->BeginPerfEvent("Histogram Auto Exposure"); + + const bool histogramReadbackActive = + Menu::GetSingleton()->IsEnabled && + histogramReadbackRequested && + ImGui::GetCurrentContext() && + histogramReadbackRequestFrame >= ImGui::GetFrameCount() - 1; + if (!histogramReadbackActive) + histogramReadbackRequested = false; + + { + state->BeginPerfEvent("Calculate Histogram"); + srvs[0] = inout_tex.srv; + uavs[0] = histogramSB->UAV(); + uavs[1] = adaptationSB->UAV(); + + context->CSSetShaderResources(0, (UINT)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (UINT)uavs.size(), uavs.data(), nullptr); + + // Calculate histogram + context->CSSetShader(histogramCS.get(), nullptr, 0); + uint texWidth = 0; + uint texHeight = 0; + { + D3D11_TEXTURE2D_DESC desc; + inout_tex.tex->GetDesc(&desc); + texWidth = desc.Width; + texHeight = desc.Height; + } + uint32_t dispatchX = ((texWidth - 1) >> 5) + 1; + uint32_t dispatchY = ((texHeight - 1) >> 5) + 1; + dispatchX = (dispatchX + 7) / 8; + dispatchY = (dispatchY + 7) / 8; + + context->Dispatch(dispatchX, dispatchY, 1); + + if (histogramReadbackActive && histogramStagingBuffer) { + uavs.fill(nullptr); + context->CSSetUnorderedAccessViews(0, (UINT)uavs.size(), uavs.data(), nullptr); + + ID3D11Resource* histResource = nullptr; + histogramSB->UAV()->GetResource(&histResource); + context->CopyResource(histogramStagingBuffer.get(), histResource); + histResource->Release(); + + uavs[0] = histogramSB->UAV(); + uavs[1] = adaptationSB->UAV(); + context->CSSetUnorderedAccessViews(0, (UINT)uavs.size(), uavs.data(), nullptr); + } + + // Calculate average + context->CSSetShader(histogramAvgCS.get(), nullptr, 0); + context->Dispatch(1, 1, 1); + state->EndPerfEvent(); + } + + // Clean up + resetViews(); + cb = nullptr; + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetShader(nullptr, nullptr, 0); + + // NOTE: We intentionally do NOT modify inout_tex here. + // The adaptation result is stored in adaptationSB and will be consumed + // by the Composite pass which applies exposure before color grading. + state->EndPerfEvent(); + globals::profiler->EndPass(); + + // Readback histogram and adaptation data when the histogram panel is open. + // histogramStagingBuffer was copied before CS_Average cleared the GPU histogram. + if (histogramReadbackActive && histogramStagingBuffer) { + D3D11_MAPPED_SUBRESOURCE mapped{}; + if (SUCCEEDED(context->Map(histogramStagingBuffer.get(), 0, D3D11_MAP_READ, 0, &mapped))) { + memcpy(histogramData.data(), mapped.pData, sizeof(uint32_t) * 256); + context->Unmap(histogramStagingBuffer.get(), 0); + } + + if (adaptationStagingBuffer) { + ID3D11Resource* adaptResource = nullptr; + adaptationSB->SRV()->GetResource(&adaptResource); + context->CopyResource(adaptationStagingBuffer.get(), adaptResource); + adaptResource->Release(); + + D3D11_MAPPED_SUBRESOURCE adaptMapped{}; + if (SUCCEEDED(context->Map(adaptationStagingBuffer.get(), 0, D3D11_MAP_READ, 0, &adaptMapped))) { + adaptationValue = *reinterpret_cast(adaptMapped.pData); + context->Unmap(adaptationStagingBuffer.get(), 0); + } + } + } +} diff --git a/src/Features/PostProcessing/HistogramAutoExposure.h b/src/Features/PostProcessing/HistogramAutoExposure.h new file mode 100644 index 0000000000..99d3f8111c --- /dev/null +++ b/src/Features/PostProcessing/HistogramAutoExposure.h @@ -0,0 +1,84 @@ +#pragma once + +#include "PostProcessFeature.h" + +#include "Buffer.h" + +struct HistogramAutoExposure : public PostProcessFeature +{ + virtual inline std::string GetType() const override { return "Histogram Auto Exposure"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.histogram_auto_exposure.name", "Histogram Auto Exposure"); } + virtual inline std::string GetDesc() const override + { + return T("feature.post_processing.histogram_auto_exposure.description", + "Auto exposure and eye adaptation method that uses a histogram to calculate average screen brightness. Expects HDR linear RGB inputs."); + } + virtual inline bool DisableInMainLoadingMenu() const override { return true; } + + /// This feature no longer writes to the main texture. + /// It only computes the adaptation value which is consumed by the Composite pass. + virtual bool WritesToMainTexture() const override { return false; } + + struct Settings + { + float ExposureCompensation = 0.f; + + // auto exposure + float2 AdaptationRange = { -3.f, 5.f }; // EV100 (0 EV100 = 0.125 linear luminance) + float2 AdaptArea = { .6f, .6f }; + + float AdaptSpeed = 1.5f; + + // purkinje + float PurkinjeStartEV = -1.5f; // EV100 (0 EV100 = 0.125 linear luminance) + float PurkinjeMaxEV = -4.f; // EV100 (0 EV100 = 0.125 linear luminance) + float PurkinjeStrength = 0.f; + } settings; + + // buffers + struct alignas(16) AutoExposureCB + { + float2 AdaptArea; + float2 AdaptationRange; + float AdaptLerp; + float ExposureCompensation; + float PurkinjeStartEV; + float PurkinjeMaxEV; + float PurkinjeStrength; + + float pad[3]; + }; + std::unique_ptr autoExposureCB = nullptr; + std::unique_ptr histogramSB = nullptr; + std::unique_ptr adaptationSB = nullptr; + + winrt::com_ptr histogramCS = nullptr; + winrt::com_ptr histogramAvgCS = nullptr; + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json&) override; + virtual void SaveSettings(json&) override; + + virtual void DrawSettings() override; + + virtual void Draw(TextureInfo&) override; + + /// Get the adaptation structured buffer SRV (contains a single float: adapted luminance). + /// Used by the Composite pass to apply exposure. + ID3D11ShaderResourceView* GetAdaptationSRV() const { return adaptationSB ? adaptationSB->SRV() : nullptr; } + + /// Get the constant buffer containing exposure parameters (for Composite pass). + ID3D11Buffer* GetConstantBuffer() const { return autoExposureCB ? autoExposureCB->CB() : nullptr; } + + // Histogram visualization + winrt::com_ptr histogramStagingBuffer = nullptr; + winrt::com_ptr adaptationStagingBuffer = nullptr; + std::array histogramData = {}; + float adaptationValue = 0.f; + bool histogramReadbackRequested = false; + int histogramReadbackRequestFrame = -1; +}; diff --git a/src/Features/PostProcessing/LUT.cpp b/src/Features/PostProcessing/LUT.cpp new file mode 100644 index 0000000000..658941f076 --- /dev/null +++ b/src/Features/PostProcessing/LUT.cpp @@ -0,0 +1,296 @@ +#include "LUT.h" + +#include "State.h" +#include "Util.h" + +#include +#include + +#include "I18n/I18n.h" +#include +#include +#include + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + LUT::Settings, + LutPath, + InputMin, + InputMax) + +void LUT::DrawSettings() +{ + ImGui::TextWrapped(T("feature.post_processing.lut.relative_path_starts_from_game_executable_directory_supports", "Relative path starts from game executable directory. Supports dds/bmp/png format.")); + ImGui::BulletText(T("feature.post_processing.lut.1d_lut_n_x_1_sized_images", "1D LUT: N x 1 sized images.")); + ImGui::BulletText(T("feature.post_processing.lut.3d_lut_in_2d_format_n_r_x", "3D LUT in 2D format: N (R) x N (G) sized images, stacked horizontally along blue axis.")); + ImGui::BulletText(T("feature.post_processing.lut.3d_lut_3d_dds_only", "3D LUT: 3D dds only.")); + + ImGui::InputText(T("feature.post_processing.lut.lut_texture_path", "LUT Texture Path"), &tempPath); + + if (ImGui::Button(T("feature.post_processing.lut.load", "Load"))) + ReadTexture(tempPath); + ImGui::SameLine(); + if (ImGui::Button(T("feature.post_processing.lut.clear", "Clear"))) { + Clear(); + tempPath = ""; + } + if (!errMsg.empty()) { + ImGui::SameLine(); + Util::Text::Error("%s", errMsg.c_str()); + } + + if (LutType == -1) + ImGui::Text(T("feature.post_processing.lut.loaded_texture_none", "Loaded Texture: None")); + else + ImGui::Text(T("feature.post_processing.lut.loaded_texture", "Loaded Texture: %s"), settings.LutPath.c_str()); + + ImGui::Separator(); + + if (LutType == 0 || LutType == 1) + if (ImGui::BeginTable("##1d", 2)) { + ImGui::TableNextColumn(); + ImGui::RadioButton(T("feature.post_processing.lut.map_luma", "Map Luma"), &LutType, 0); + ImGui::TableNextColumn(); + ImGui::RadioButton(T("feature.post_processing.lut.map_per_channel", "Map Per Channel"), &LutType, 1); + ImGui::EndTable(); + } + ImGui::InputFloat3(T("feature.post_processing.lut.input_min", "Input Min"), &settings.InputMin.x); + ImGui::InputFloat3(T("feature.post_processing.lut.input_max", "Input Max"), &settings.InputMax.x); +} + +void LUT::RestoreDefaultSettings() +{ + settings = {}; +} + +void LUT::LoadSettings(json& o_json) +{ + settings = o_json; + + tempPath = settings.LutPath; + logger::info("Loading LUT settings, LUT Path: {}", settings.LutPath); + + try { + if (!tempPath.empty() && !firstLoad) + ReadTexture(tempPath); + else if (firstLoad) + firstLoad = false; + } catch (const std::exception& e) { + logger::warn("Failed to load LUT settings: {}", e.what()); + } +} + +void LUT::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void LUT::SetupResources() +{ + auto renderer = globals::game::renderer; + + if (!settings.LutPath.empty()) + ReadTexture(settings.LutPath); + + logger::debug("Creating buffers..."); + { + lutCB = eastl::make_unique(ConstantBufferDesc()); + } + + logger::debug("Creating 2D textures..."); + { + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + D3D11_TEXTURE2D_DESC texDesc; + gameTexMainCopy.texture->GetDesc(&texDesc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + texDesc.MipLevels = srvDesc.Texture2D.MipLevels = 1; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + + texOutput = eastl::make_unique(texDesc); + texOutput->CreateSRV(srvDesc); + texOutput->CreateUAV(uavDesc); + } + + CompileComputeShaders(); +} + +void LUT::ReadTexture(std::filesystem::path path) +{ + constexpr auto comErrMsg = "Failed to create texture! Error: {}"; + + auto device = globals::d3d::device; + + Clear(); + + auto extension = path.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + + if (extension != ".dds" && extension != ".png" && extension != ".bmp") { + errMsg = std::format("Invalid extension: {}! Only dds/png/bmp are supported.", path.extension().string()); + logger::warn("Invalid extension: {}! Only dds/png/bmp are supported.", path.extension().string()); + return; + } + if (!std::filesystem::exists(path)) { + errMsg = "The file does not exist."; + logger::warn("The file does not exist."); + return; + } + + if (extension == ".dds") { + ID3D11Resource* pRsrc = nullptr; + ID3D11ShaderResourceView* pSrv = nullptr; + try { + DX::ThrowIfFailed(DirectX::CreateDDSTextureFromFile(device, path.c_str(), &pRsrc, &pSrv)); + } catch (std::runtime_error& e) { + errMsg = std::format(comErrMsg, e.what()); + logger::warn(comErrMsg, e.what()); + return; + } + + D3D11_RESOURCE_DIMENSION texType; + pRsrc->GetType(&texType); + if (texType == D3D11_RESOURCE_DIMENSION_TEXTURE2D) { + texLUT2D = eastl::make_unique(reinterpret_cast(pRsrc)); + texLUT2D->srv.attach(pSrv); + LutType = texLUT2D->desc.Height == 1 ? 0 : 2; + } else if (texType == D3D11_RESOURCE_DIMENSION_TEXTURE3D) { + texLUT3D = eastl::make_unique(reinterpret_cast(pRsrc)); + texLUT3D->srv.attach(pSrv); + LutType = 3; + } else { + errMsg = std::format("Invalid texture dimension: {}! Only 2D/3D textures are supported.", magic_enum::enum_name(texType)); + logger::warn("Invalid texture dimension: {}! Only 2D/3D textures are supported.", magic_enum::enum_name(texType)); + return; + } + } else { + DirectX::ScratchImage image; + try { + DX::ThrowIfFailed(DirectX::LoadFromWICFile(path.c_str(), DirectX::WIC_FLAGS_NONE, nullptr, image)); + } catch (std::runtime_error& e) { + errMsg = std::format(comErrMsg, e.what()); + logger::warn(comErrMsg, e.what()); + return; + } + + ID3D11Resource* pRsrc = nullptr; + try { + DX::ThrowIfFailed(CreateTexture(device, image.GetImages(), image.GetImageCount(), image.GetMetadata(), &pRsrc)); + } catch (std::runtime_error& e) { + errMsg = std::format(comErrMsg, e.what()); + logger::warn(comErrMsg, e.what()); + return; + } + + texLUT2D = eastl::make_unique(reinterpret_cast(pRsrc)); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texLUT2D->desc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { + .MostDetailedMip = 0, + .MipLevels = 1 } + }; + texLUT2D->CreateSRV(srvDesc); + + LutType = texLUT2D->desc.Height == 1 ? 0 : 2; + } + + settings.LutPath = path.string(); +} + +void LUT::ClearShaderCache() +{ + const auto shaderPtrs = std::array{ + &lutCS + }; + + for (auto shader : shaderPtrs) + if ((*shader)) { + (*shader)->Release(); + shader->detach(); + } + + CompileComputeShaders(); +} + +void LUT::CompileComputeShaders() +{ + struct ShaderCompileInfo + { + winrt::com_ptr* programPtr; + std::string_view filename; + std::vector> defines = {}; + std::string entry = "main"; + }; + + std::vector + shaderInfos = { + { &lutCS, "lut.cs.hlsl" }, + }; + + for (auto& info : shaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\LUT") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } +} + +void LUT::Draw(TextureInfo& inout_tex) +{ + if (LutType == -1) + return; + + globals::profiler->BeginPass("PostProcessing::LUT"); + auto context = globals::d3d::context; + + float2 res = { (float)texOutput->desc.Width, (float)texOutput->desc.Height }; + res = Util::ConvertToDynamic(res); + + LUTCB data = { + .InputMin = settings.InputMin, + .InputMax = settings.InputMax, + .LutType = LutType + }; + lutCB->Update(data); + + ID3D11ShaderResourceView* srv[3] = { + inout_tex.srv, + LutType == 3 ? nullptr : texLUT2D->srv.get(), + LutType == 3 ? texLUT3D->srv.get() : nullptr + }; + + ID3D11UnorderedAccessView* uav = texOutput->uav.get(); + ID3D11Buffer* cb = lutCB->CB(); + + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShaderResources(0, 3, srv); + context->CSSetShader(lutCS.get(), nullptr, 0); + + context->Dispatch(((uint)res.x + 7) >> 3, ((uint)res.y + 7) >> 3, 1); + + // clean up + std::fill(srv, srv + 3, nullptr); + uav = nullptr; + cb = nullptr; + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShaderResources(0, 3, srv); + context->CSSetConstantBuffers(0, 1, &cb); + context->CSSetShader(nullptr, nullptr, 0); + + inout_tex = { texOutput->resource.get(), texOutput->srv.get() }; + globals::profiler->EndPass(); +} diff --git a/src/Features/PostProcessing/LUT.h b/src/Features/PostProcessing/LUT.h new file mode 100644 index 0000000000..e98369bf93 --- /dev/null +++ b/src/Features/PostProcessing/LUT.h @@ -0,0 +1,65 @@ +#pragma once + +#include "PostProcessFeature.h" + +#include "Buffer.h" + +struct LUT : PostProcessFeature +{ + virtual inline std::string GetType() const override { return "LUT"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.lut.name", "LUT"); } + virtual inline std::string GetDesc() const override { return T("feature.post_processing.lut.description", "Look-up table application."); } + virtual inline bool DrawAfterColorGrading() const override { return true; } + + int LutType = -1; // -1 - null, 0 - 1d luma, 1 - 1d per channel, 2 - 3d in 2d, 3 - 3d + + std::string errMsg = ""; + std::string tempPath = ""; + + struct Settings + { + std::string LutPath = ""; + float3 InputMin{ 0.f }; + float3 InputMax{ 1.f }; + } settings; + + struct alignas(16) LUTCB + { + float3 InputMin; + float pad; + float3 InputMax; + int LutType; + }; + eastl::unique_ptr lutCB = nullptr; + + eastl::unique_ptr texLUT2D = nullptr; + eastl::unique_ptr texLUT3D = nullptr; + eastl::unique_ptr texOutput = nullptr; + + winrt::com_ptr lutCS = nullptr; + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json&) override; + virtual void SaveSettings(json&) override; + + virtual void DrawSettings() override; + void inline Clear() + { + LutType = -1; + settings.LutPath = ""; + errMsg = ""; + if (texLUT2D) + texLUT2D.reset(); + if (texLUT3D) + texLUT3D.reset(); + } + void ReadTexture(std::filesystem::path path); + + virtual void Draw(TextureInfo&) override; + + bool firstLoad = true; +}; diff --git a/src/Features/PostProcessing/LensFlare.cpp b/src/Features/PostProcessing/LensFlare.cpp new file mode 100644 index 0000000000..ab1833d48a --- /dev/null +++ b/src/Features/PostProcessing/LensFlare.cpp @@ -0,0 +1,838 @@ +#include "LensFlare.h" + +#include "Features/PostProcessing.h" +#include "I18n/I18n.h" +#include "State.h" +#include "Util.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + LensFlare::Settings, + Intensity, + ThresholdEV, + ThresholdRange, + GhostStrength, + GhostChromaShift, + GhostModeInt, + FFTResolution, + KernelScale, + FStop, + ApertureBlades, + ApertureRotation, + HaloStrength, + HaloRadius, + HaloWidth, + HaloCompression, + HaloChromaShift, + Tint, + GLocalMask, + Ghosts) + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + LensFlare::GhostSettings, + Color, + Scale, + Enabled, + KernelScale) + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + LensFlare::DebugSettings, + blurIterations, + disableThreshold, + disableGhosts, + disableBlur) + +void LensFlare::DrawSettings() +{ + auto tooltip = [](const char* text) { + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::TextUnformatted(text); + }; + + ImGui::SliderFloat(T("feature.post_processing.lens_flare.intensity", "Intensity"), &settings.Intensity, 0.0f, 1.0f, "%.3f"); + tooltip("Master intensity for the entire lens flare effect"); + + // Threshold + ImGui::Spacing(); + ImGui::Text(T("feature.post_processing.lens_flare.threshold", "Threshold")); + ImGui::Separator(); + ImGui::SliderFloat(T("feature.post_processing.lens_flare.threshold_ev", "Threshold (EV100)"), &settings.ThresholdEV, -7.0f, 23.0f, "%+.2f EV100"); + tooltip("Brightness threshold in EV100 (0 EV100 = 0.125 linear luminance)."); + ImGui::SliderFloat(T("feature.post_processing.lens_flare.threshold_range", "Threshold Range"), &settings.ThresholdRange, 0.01f, 5.0f, "%.3f"); + tooltip("Fade range for the threshold cutoff"); + + // Ghost Settings + ImGui::Spacing(); + ImGui::Text(T("feature.post_processing.lens_flare.ghost_settings", "Ghost Settings")); + ImGui::Separator(); + + { + const char* modeNames[] = { "Fast (Procedural)", "Quality (FFT Bokeh)", "Ultra (Per-Ghost FFT)" }; + ImGui::Combo(T("feature.post_processing.lens_flare.ghost_mode", "Ghost Mode"), &settings.GhostModeInt, modeNames, 3); + tooltip("Fast: procedural radial ghosts (low cost).\nQuality: FFT convolution with bokeh shape.\nUltra: per-ghost kernel sizes via multi-pass FFT (expensive)."); + } + + if (settings.GhostModeInt == static_cast(GhostMode::Quality) || settings.GhostModeInt == static_cast(GhostMode::Ultra)) { + // Procedural aperture settings + ImGui::SliderInt(T("feature.post_processing.lens_flare.aperture_blades", "Aperture Blades"), &settings.ApertureBlades, 3, 10); + tooltip("Number of aperture blades for the procedural bokeh shape."); + + ImGui::SliderFloat(T("feature.post_processing.lens_flare.f_stop", "F-Stop"), &settings.FStop, 1.0f, 22.0f, "F%.1f"); + tooltip("Aperture f-number (e.g. F2.8). Smaller = larger aperture.\nControls the bokeh shape characteristics."); + + ImGui::SliderFloat(T("feature.post_processing.lens_flare.aperture_rotation", "Aperture Rotation"), &settings.ApertureRotation, -180.0f, 180.0f, "%.1f deg"); + tooltip("Rotation of the procedural aperture."); + + // FFT Resolution + { + const char* resNames[] = { "128", "256", "512", "1024" }; + int resValues[] = { 128, 256, 512, 1024 }; + int curIdx = 1; + for (int i = 0; i < 4; i++) + if (resValues[i] == settings.FFTResolution) + curIdx = i; + + if (ImGui::Combo(T("feature.post_processing.lens_flare.fft_resolution", "FFT Resolution"), &curIdx, resNames, 4)) + settings.FFTResolution = resValues[curIdx]; + + tooltip("Resolution of the FFT convolution. Higher = sharper bokeh ghost shapes but more expensive."); + } + + ImGui::SliderFloat(T("feature.post_processing.lens_flare.kernel_scale", "Kernel Scale"), &settings.KernelScale, 0.01f, 0.5f, "%.3f"); + tooltip("Base size of the bokeh kernel relative to FFT resolution.\nPer-ghost scales multiply this value in Ultra mode."); + } + + ImGui::SliderFloat(T("feature.post_processing.lens_flare.ghost_strength", "Ghost Strength"), &settings.GhostStrength, 0.0f, 1.0f, "%.3f"); + ImGui::SliderFloat(T("feature.post_processing.lens_flare.ghost_chroma_shift", "Ghost Chroma Shift"), &settings.GhostChromaShift, 0.0f, 0.1f, "%.4f"); + ImGui::Checkbox(T("feature.post_processing.lens_flare.non_intrusive_ghosts", "Non-intrusive Ghosts"), &settings.GLocalMask); + tooltip("Only apply ghost flaring when looking directly at light sources"); + + if (ImGui::TreeNode(T("feature.post_processing.lens_flare.custom_ghost_colors_scales", "Custom Ghost Colors & Scales"))) { + for (int i = 0; i < NUM_GHOSTS; i++) { + ImGui::PushID(i); + char label[32]; + snprintf(label, sizeof(label), "Ghost %d", i + 1); + if (ImGui::TreeNode(label)) { + ImGui::Checkbox(T("feature.post_processing.lens_flare.enabled", "Enabled"), &settings.Ghosts[i].Enabled); + ImGui::ColorEdit4(T("feature.post_processing.lens_flare.color", "Color"), settings.Ghosts[i].Color.data()); + ImGui::SliderFloat(T("feature.post_processing.lens_flare.scale", "Scale"), &settings.Ghosts[i].Scale, -15.0f, 15.0f, "%.2f"); + if (settings.GhostModeInt == static_cast(GhostMode::Ultra)) { + ImGui::SliderFloat(T("feature.post_processing.lens_flare.kernel_scale_2", "Kernel Scale"), &settings.Ghosts[i].KernelScale, 0.1f, 4.0f, "%.2fx"); + tooltip("Multiplier on global Kernel Scale.\n1x = same as global, <1 = sharper, >1 = softer.\nEach distinct value requires a separate FFT pass."); + } + ImGui::TreePop(); + } + ImGui::PopID(); + } + if (ImGui::Button(T("feature.post_processing.lens_flare.reset_ghosts_to_default", "Reset Ghosts to Default"))) { + GhostSettings defaults[NUM_GHOSTS] = { + { { { 1.0f, 0.8f, 0.4f, 1.0f } }, -1.5f, true, 1.0f }, + { { { 1.0f, 1.0f, 0.6f, 1.0f } }, 2.5f, true, 1.0f }, + { { { 0.8f, 0.8f, 1.0f, 1.0f } }, -5.0f, true, 1.0f }, + { { { 0.5f, 1.0f, 0.4f, 1.0f } }, 10.0f, true, 1.0f }, + { { { 0.5f, 0.8f, 1.0f, 1.0f } }, 0.7f, true, 1.0f }, + { { { 0.9f, 1.0f, 0.8f, 1.0f } }, -0.4f, true, 1.0f }, + { { { 1.0f, 0.8f, 0.4f, 1.0f } }, -0.2f, true, 1.0f }, + { { { 0.9f, 0.7f, 0.7f, 1.0f } }, -0.1f, true, 1.0f }, + }; + std::memcpy(settings.Ghosts.data(), defaults, sizeof(settings.Ghosts)); + } + ImGui::TreePop(); + } + + // Halo Settings + ImGui::Spacing(); + ImGui::Text(T("feature.post_processing.lens_flare.halo_settings", "Halo Settings")); + ImGui::Separator(); + ImGui::SliderFloat(T("feature.post_processing.lens_flare.halo_strength", "Halo Strength"), &settings.HaloStrength, 0.0f, 1.0f, "%.3f"); + ImGui::SliderFloat(T("feature.post_processing.lens_flare.halo_radius", "Halo Radius"), &settings.HaloRadius, 0.0f, 1.0f, "%.3f"); + ImGui::SliderFloat(T("feature.post_processing.lens_flare.halo_width", "Halo Width"), &settings.HaloWidth, 0.0f, 1.0f, "%.3f"); + ImGui::SliderFloat(T("feature.post_processing.lens_flare.halo_compression", "Halo Compression"), &settings.HaloCompression, 0.1f, 2.0f, "%.3f"); + tooltip("Fisheye distortion strength for the halo effect"); + ImGui::SliderFloat(T("feature.post_processing.lens_flare.halo_chroma_shift", "Halo Chroma Shift"), &settings.HaloChromaShift, 0.0f, 0.1f, "%.4f"); + + // Tint + ImGui::Spacing(); + ImGui::Text(T("feature.post_processing.lens_flare.color_tint", "Color Tint")); + ImGui::Separator(); + ImGui::ColorEdit3(T("feature.post_processing.lens_flare.tint", "Tint"), settings.Tint.data()); + tooltip("Radial color gradient applied to the flare effect"); + + // Debug + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::CollapsingHeader(T("feature.post_processing.lens_flare.debug", "Debug"))) { + ImGui::Checkbox(T("feature.post_processing.lens_flare.disable_threshold", "Disable Threshold"), &debugsettings.disableThreshold); + ImGui::Checkbox(T("feature.post_processing.lens_flare.disable_ghosts", "Disable Ghosts"), &debugsettings.disableGhosts); + ImGui::Checkbox(T("feature.post_processing.lens_flare.disable_blur", "Disable Blur"), &debugsettings.disableBlur); + ImGui::SliderInt(T("feature.post_processing.lens_flare.blur_iterations", "Blur Iterations"), &debugsettings.blurIterations, 1, 4); + tooltip("Kawase blur cycles (down+up). 1 = sharp, 2+ = smoother"); + + static float debugRescale = .25f; + ImGui::SliderFloat(T("feature.post_processing.lens_flare.view_resize", "View Resize"), &debugRescale, 0.f, 1.f); + BUFFER_VIEWER_NODE_TITLE(texThreshold, "Threshold (half-res)", debugRescale); + BUFFER_VIEWER_NODE_TITLE(texGhostHalo, "Ghost + Halo (half-res)", debugRescale); + BUFFER_VIEWER_NODE_TITLE(texFlare, "Final Flare Output", debugRescale); + BUFFER_VIEWER_NODE_TITLE(texBlurTemp, "Blur Temp (quarter-res)", debugRescale); + if (settings.GhostModeInt >= (int)GhostMode::Quality) { + BUFFER_VIEWER_NODE_TITLE(texFFTResult, "FFT Convolution Result", debugRescale); + } + } +} + +void LensFlare::RestoreDefaultSettings() +{ + settings = {}; +} + +void LensFlare::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void LensFlare::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void LensFlare::SetupResources() +{ + auto renderer = globals::game::renderer; + auto device = globals::d3d::device; + + logger::debug("LensFlare: Creating buffers..."); + { + lensFlareCB = eastl::make_unique(ConstantBufferDesc()); + } + + logger::debug("LensFlare: Creating 2D textures..."); + { + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + D3D11_TEXTURE2D_DESC baseDesc; + gameTexMainCopy.texture->GetDesc(&baseDesc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = DXGI_FORMAT_R16G16B16A16_FLOAT, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = DXGI_FORMAT_R16G16B16A16_FLOAT, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + auto createTex = [&](eastl::unique_ptr& tex, uint width, uint height) { + D3D11_TEXTURE2D_DESC texDesc = baseDesc; + texDesc.Width = width; + texDesc.Height = height; + texDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; + texDesc.MipLevels = 1; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + tex = eastl::make_unique(texDesc); + tex->CreateSRV(srvDesc); + tex->CreateUAV(uavDesc); + }; + + uint fullW = baseDesc.Width; + uint fullH = baseDesc.Height; + uint halfW = std::max(fullW / 2, 1u); + uint halfH = std::max(fullH / 2, 1u); + uint quarterW = std::max(fullW / 4, 1u); + uint quarterH = std::max(fullH / 4, 1u); + + createTex(texFlare, fullW, fullH); // full resolution (final output) + createTex(texThreshold, halfW, halfH); // half resolution + createTex(texGhostHalo, halfW, halfH); // half resolution + createTex(texBlurTemp, quarterW, quarterH); // quarter resolution + + logger::debug("LensFlare: textures created - full {}x{}, half {}x{}, quarter {}x{}", fullW, fullH, halfW, halfH, quarterW, quarterH); + } + + logger::debug("LensFlare: Creating samplers..."); + { + D3D11_SAMPLER_DESC samplerDesc = { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_CLAMP, + .AddressV = D3D11_TEXTURE_ADDRESS_CLAMP, + .AddressW = D3D11_TEXTURE_ADDRESS_CLAMP, + .MaxAnisotropy = 1, + .MinLOD = 0, + .MaxLOD = D3D11_FLOAT32_MAX + }; + DX::ThrowIfFailed(device->CreateSamplerState(&samplerDesc, colorSampler.put())); + + D3D11_SAMPLER_DESC borderDesc = { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_BORDER, + .AddressV = D3D11_TEXTURE_ADDRESS_BORDER, + .AddressW = D3D11_TEXTURE_ADDRESS_BORDER, + .MaxAnisotropy = 1, + .MinLOD = 0, + .MaxLOD = D3D11_FLOAT32_MAX + }; + DX::ThrowIfFailed(device->CreateSamplerState(&borderDesc, borderSampler.put())); + } + + CompileComputeShaders(); + + // Create initial FFT textures + CreateFFTTextures(std::clamp((uint)settings.FFTResolution, FFT_MIN, FFT_MAX)); +} + +void LensFlare::CreateFFTTextures(uint resolution) +{ + currentFFTResolution = resolution; + bokehFFTDirty = true; + + D3D11_TEXTURE2D_DESC texDesc = { + .Width = resolution, + .Height = resolution, + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_R32G32_FLOAT, + .SampleDesc = { .Count = 1, .Quality = 0 }, + .Usage = D3D11_USAGE_DEFAULT, + .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS, + }; + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + // FFT ping-pong textures (RG32F) + for (int pp = 0; pp < 2; pp++) { + texFFT[pp] = eastl::make_unique(texDesc); + texFFT[pp]->CreateSRV(srvDesc); + texFFT[pp]->CreateUAV(uavDesc); + } + + // Bokeh kernel FFT cache (RG32F) + texBokehFFT = eastl::make_unique(texDesc); + texBokehFFT->CreateSRV(srvDesc); + texBokehFFT->CreateUAV(uavDesc); + + // Scene FFT cache (RG32F) — reused across kernel groups in Ultra mode + texSceneFFT = eastl::make_unique(texDesc); + texSceneFFT->CreateSRV(srvDesc); + texSceneFFT->CreateUAV(uavDesc); + + // FFT result (RGBA16F at FFT resolution) + texDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; + srvDesc.Format = texDesc.Format; + uavDesc.Format = texDesc.Format; + + texFFTResult = eastl::make_unique(texDesc); + texFFTResult->CreateSRV(srvDesc); + texFFTResult->CreateUAV(uavDesc); + + auto context = globals::d3d::context; + const FLOAT clearColor[4] = { 0.f, 0.f, 0.f, 0.f }; + context->ClearUnorderedAccessViewFloat(texFFTResult->uav.get(), clearColor); + + logger::debug("LensFlare: FFT textures created at {}x{}", resolution, resolution); +} + +void LensFlare::ClearShaderCache() +{ + const auto shaderPtrs = std::array{ + &thresholdCS, &ghostHaloCS, &blurDownCS, &blurUpCS, &mixCS, + &fftRowCS, &fftColCS, &fftRowInvCS, &fftColInvCS, &fftMultiplyCS, + &bokehPrepareCS, &fftThresholdCS, &fftGhostComposeCS + }; + + for (auto shader : shaderPtrs) + if ((*shader)) { + (*shader)->Release(); + shader->detach(); + } + + CompileComputeShaders(); +} + +void LensFlare::CompileComputeShaders() +{ + struct ShaderCompileInfo + { + winrt::com_ptr* programPtr; + std::string_view filename; + std::vector> defines = {}; + std::string entry = "main"; + }; + + std::vector shaderInfos = { + { &thresholdCS, "lensflare.cs.hlsl", {}, "CSThreshold" }, + { &ghostHaloCS, "lensflare.cs.hlsl", {}, "CSGhostHalo" }, + { &blurDownCS, "lensflare.cs.hlsl", {}, "CSFlareDown" }, + { &blurUpCS, "lensflare.cs.hlsl", {}, "CSFlareUp" }, + { &mixCS, "lensflare.cs.hlsl", {}, "CSMix" }, + // FFT ghost pipeline shaders + { &bokehPrepareCS, "lensflare_fft.cs.hlsl", {}, "CSBokehPrepare" }, + { &fftThresholdCS, "lensflare_fft.cs.hlsl", {}, "CSFFTThreshold" }, + { &fftGhostComposeCS, "lensflare_fft.cs.hlsl", {}, "CSFFTGhostCompose" }, + }; + + for (auto& info : shaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\LensFlare") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } + + // FFT shaders — self-contained in lensflare_fft.cs.hlsl with LensFlareConstants CB + std::vector fftShaderInfos = { + { &fftRowCS, "lensflare_fft.cs.hlsl", { { "ROW_PASS", "" }, { "FORWARD", "" } }, "CS_FFT" }, + { &fftColCS, "lensflare_fft.cs.hlsl", { { "COL_PASS", "" }, { "FORWARD", "" } }, "CS_FFT" }, + { &fftRowInvCS, "lensflare_fft.cs.hlsl", { { "ROW_PASS", "" }, { "INVERSE", "" } }, "CS_FFT" }, + { &fftColInvCS, "lensflare_fft.cs.hlsl", { { "COL_PASS", "" }, { "INVERSE", "" } }, "CS_FFT" }, + { &fftMultiplyCS, "lensflare_fft.cs.hlsl", {}, "CS_Multiply" }, + }; + + for (auto& info : fftShaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\LensFlare") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } + + if (!thresholdCS || !ghostHaloCS || !mixCS) { + logger::error("Failed to compile lens flare compute shaders!"); + } +} + +void LensFlare::DispatchFFT(ID3D11ComputeShader* shader, Texture2D* input, Texture2D* output, uint resolution) +{ + auto context = globals::d3d::context; + + ID3D11ShaderResourceView* srv = input->srv.get(); + ID3D11UnorderedAccessView* uav = output->uav.get(); + + context->CSSetShaderResources(0, 1, &srv); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShader(shader, nullptr, 0); + context->Dispatch(resolution, 1, 1); + + srv = nullptr; + uav = nullptr; + context->CSSetShaderResources(0, 1, &srv); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); +} + +void LensFlare::PrepareBokehFFT() +{ + auto context = globals::d3d::context; + + // Step 1: Generate procedural aperture kernel → RG32F (real=aperture, imag=0), centered + zero-padded + { + std::array uavs = { texFFT[0]->uav.get() }; + + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + context->CSSetShader(bokehPrepareCS.get(), nullptr, 0); + context->Dispatch((currentFFTResolution + 7) >> 3, (currentFFTResolution + 7) >> 3, 1); + + uavs.fill(nullptr); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + } + + // Step 2: Forward FFT bokeh kernel: texFFT[0] → texFFT[1] → texBokehFFT + DispatchFFT(fftRowCS.get(), texFFT[0].get(), texFFT[1].get(), currentFFTResolution); + DispatchFFT(fftColCS.get(), texFFT[1].get(), texBokehFFT.get(), currentFFTResolution); + + bokehFFTDirty = false; +} + +void LensFlare::DrawFast(TextureInfo& inout_tex, LensFlareCB& data) +{ + std::ignore = inout_tex; + auto context = globals::d3d::context; + uint halfW = texThreshold->desc.Width; + uint halfH = texThreshold->desc.Height; + uint quarterW = texBlurTemp->desc.Width; + uint quarterH = texBlurTemp->desc.Height; + + std::array srvs = { nullptr }; + std::array uavs = { nullptr }; + + auto resetViews = [&]() { + srvs.fill(nullptr); + uavs.fill(nullptr); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + }; + + // === Pass 2: Ghost + Halo — half res → half res === + if (!debugsettings.disableGhosts && ghostHaloCS) { + data.OutputWidth = (float)halfW; + data.OutputHeight = (float)halfH; + data.InputWidth = (float)halfW; + data.InputHeight = (float)halfH; + lensFlareCB->Update(data); + auto cb = lensFlareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + srvs.at(0) = texThreshold->srv.get(); + uavs.at(0) = texGhostHalo->uav.get(); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(ghostHaloCS.get(), nullptr, 0); + context->Dispatch((halfW + 7) >> 3, (halfH + 7) >> 3, 1); + resetViews(); + } + + // === Pass 3: Kawase blur === + if (!debugsettings.disableBlur && blurDownCS && blurUpCS) { + for (int iter = 0; iter < debugsettings.blurIterations; iter++) { + data.OutputWidth = (float)quarterW; + data.OutputHeight = (float)quarterH; + data.InputWidth = (float)halfW; + data.InputHeight = (float)halfH; + lensFlareCB->Update(data); + auto cb = lensFlareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + srvs.at(0) = texGhostHalo->srv.get(); + uavs.at(0) = texBlurTemp->uav.get(); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(blurDownCS.get(), nullptr, 0); + context->Dispatch((quarterW + 7) >> 3, (quarterH + 7) >> 3, 1); + resetViews(); + + data.OutputWidth = (float)halfW; + data.OutputHeight = (float)halfH; + data.InputWidth = (float)quarterW; + data.InputHeight = (float)quarterH; + lensFlareCB->Update(data); + cb = lensFlareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + srvs.at(0) = texBlurTemp->srv.get(); + uavs.at(0) = texGhostHalo->uav.get(); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(blurUpCS.get(), nullptr, 0); + context->Dispatch((halfW + 7) >> 3, (halfH + 7) >> 3, 1); + resetViews(); + } + } +} + +void LensFlare::DrawQuality(TextureInfo& inout_tex, LensFlareCB& data) +{ + std::ignore = inout_tex; + auto context = globals::d3d::context; + uint N = currentFFTResolution; + uint halfW = texThreshold->desc.Width; + uint halfH = texThreshold->desc.Height; + + // Handle FFT resolution change + uint targetRes = std::clamp((uint)settings.FFTResolution, FFT_MIN, FFT_MAX); + if (targetRes != currentFFTResolution) { + CreateFFTTextures(targetRes); + N = targetRes; + } + + data.FFTResolution = N; + GhostMode mode = static_cast(settings.GhostModeInt); + + // === Build kernel groups === + struct KernelGroup + { + float kernelScale; + uint32_t ghostMask; + }; + std::vector groups; + + if (mode == GhostMode::Ultra) { + // Group enabled ghosts by effective kernel scale (merge similar within 0.001) + for (int i = 0; i < NUM_GHOSTS; i++) { + if (!(data.ActiveGhostMask & (1u << i))) + continue; + float ks = settings.KernelScale * settings.Ghosts[i].KernelScale; // effective scale + bool merged = false; + for (auto& g : groups) { + if (std::abs(g.kernelScale - ks) < 0.001f) { + g.ghostMask |= (1u << i); + merged = true; + break; + } + } + if (!merged) { + if ((int)groups.size() < MAX_KERNEL_GROUPS) { + groups.push_back({ ks, 1u << (uint)i }); + } else { + // Overflow: merge into closest existing group + float bestDist = 1e9f; + int bestIdx = 0; + for (int g = 0; g < (int)groups.size(); g++) { + float d = std::abs(groups[g].kernelScale - ks); + if (d < bestDist) { + bestDist = d; + bestIdx = g; + } + } + groups[bestIdx].ghostMask |= (1u << i); + } + } + } + } + + if (groups.empty()) { + // Quality mode or no Ultra groups: single pass with global KernelScale + groups.push_back({ settings.KernelScale, data.ActiveGhostMask }); + } + + // === Clear texGhostHalo for additive compositing === + { + const FLOAT clearColor[4] = { 0.f, 0.f, 0.f, 0.f }; + context->ClearUnorderedAccessViewFloat(texGhostHalo->uav.get(), clearColor); + } + + if (debugsettings.disableGhosts) + return; + + // === Step 1: Threshold scene → FFT format (RG32F, N×N) === + if (fftThresholdCS) { + data.OutputWidth = (float)N; + data.OutputHeight = (float)N; + data.InputWidth = (float)halfW; + data.InputHeight = (float)halfH; + lensFlareCB->Update(data); + auto cb = lensFlareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + ID3D11ShaderResourceView* srv = texThreshold->srv.get(); + ID3D11UnorderedAccessView* uav = texFFT[0]->uav.get(); + context->CSSetShaderResources(0, 1, &srv); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShader(fftThresholdCS.get(), nullptr, 0); + context->Dispatch((N + 7) >> 3, (N + 7) >> 3, 1); + + srv = nullptr; + uav = nullptr; + context->CSSetShaderResources(0, 1, &srv); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + } + + // === Step 2: Forward FFT scene → cache in texSceneFFT === + { + DispatchFFT(fftRowCS.get(), texFFT[0].get(), texFFT[1].get(), N); + DispatchFFT(fftColCS.get(), texFFT[1].get(), texSceneFFT.get(), N); + } + + // === Step 3: Per-group convolution loop === + float originalHaloStrength = data.HaloStrength; + + for (int gi = 0; gi < (int)groups.size(); gi++) { + auto& group = groups[gi]; + + // Update CB for this group + data.KernelScale = group.kernelScale; + data.ActiveGhostMask = group.ghostMask; + if (gi > 0) + data.HaloStrength = 0.f; // Halo only in first group + + // 3a: Prepare bokeh kernel at this group's scale + { + bool needBokehRebuild = bokehFFTDirty; + // Multi-group Ultra: rebuild bokeh each group (different KernelScale) + // Single-group Quality: only rebuild when shape/dirty changes + if (needBokehRebuild || groups.size() > 1) { + lensFlareCB->Update(data); + auto cb = lensFlareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + PrepareBokehFFT(); + } + } + + // 3b: Frequency-domain multiply (scene × bokeh) + if (fftMultiplyCS) { + std::array mulSrvs = { texSceneFFT->srv.get(), texBokehFFT->srv.get() }; + std::array mulUavs = { texFFT[1]->uav.get() }; + + context->CSSetShaderResources(0, (uint)mulSrvs.size(), mulSrvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)mulUavs.size(), mulUavs.data(), nullptr); + context->CSSetShader(fftMultiplyCS.get(), nullptr, 0); + context->Dispatch((N + 7) >> 3, (N + 7) >> 3, 1); + + mulSrvs.fill(nullptr); + mulUavs.fill(nullptr); + context->CSSetShaderResources(0, (uint)mulSrvs.size(), mulSrvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)mulUavs.size(), mulUavs.data(), nullptr); + } + + // 3c: Inverse FFT + DispatchFFT(fftRowInvCS.get(), texFFT[1].get(), texFFT[0].get(), N); + DispatchFFT(fftColInvCS.get(), texFFT[0].get(), texFFT[1].get(), N); + + // 3d: Compose IFFT result → additive into texGhostHalo + if (fftGhostComposeCS) { + data.OutputWidth = (float)halfW; + data.OutputHeight = (float)halfH; + data.InputWidth = (float)N; + data.InputHeight = (float)N; + lensFlareCB->Update(data); + auto cb = lensFlareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + // t0 = IFFT result (RG32F) + std::array srvs = { texFFT[1]->srv.get() }; + std::array uavs = { texGhostHalo->uav.get() }; + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(fftGhostComposeCS.get(), nullptr, 0); + context->Dispatch((halfW + 7) >> 3, (halfH + 7) >> 3, 1); + + srvs.fill(nullptr); + uavs.fill(nullptr); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + } + } + + // Restore original halo strength for subsequent passes + data.HaloStrength = originalHaloStrength; +} + +void LensFlare::Draw(TextureInfo& inout_tex) +{ + auto state = globals::state; + auto context = globals::d3d::context; + + globals::profiler->BeginPass("PostProcessing::LensFlare"); + state->BeginPerfEvent("Lens Flare"); + + uint fullW = texFlare->desc.Width; + uint fullH = texFlare->desc.Height; + uint halfW = texThreshold->desc.Width; + uint halfH = texThreshold->desc.Height; + + // Build base constant buffer data + LensFlareCB data = {}; + data.ThresholdLevel = std::exp2f(settings.ThresholdEV - 3.0f); // EV100 → linear luminance + data.ThresholdRange = settings.ThresholdRange; + data.GhostStrength = settings.GhostStrength; + data.GhostChromaShift = settings.GhostChromaShift; + data.HaloStrength = settings.HaloStrength; + data.HaloRadius = settings.HaloRadius; + data.HaloWidth = settings.HaloWidth; + data.HaloCompression = settings.HaloCompression; + data.HaloChromaShift = settings.HaloChromaShift; + data.Intensity = settings.Intensity; + data.FFTResolution = currentFFTResolution; + std::memcpy(data.Tint, settings.Tint.data(), sizeof(float) * 3); + data.GLocalMask = settings.GLocalMask ? 1 : 0; + + uint enabledMask = 0; + for (int i = 0; i < NUM_GHOSTS; i++) { + std::memcpy(&data.GhostColors[i * 4], settings.Ghosts[i].Color.data(), sizeof(float) * 4); + data.GhostScalesPacked[i] = settings.Ghosts[i].Scale; + // Effective kernel scale = global * per-ghost multiplier + data.GhostKernelScalesPacked[i] = settings.KernelScale * settings.Ghosts[i].KernelScale; + if (settings.Ghosts[i].Enabled) + enabledMask |= (1u << i); + } + data.ActiveGhostMask = enabledMask; + data.KernelScale = settings.KernelScale; + data.AspectRatio = (float)fullW / (float)fullH; + data.ApertureBlades = settings.ApertureBlades; + data.ApertureRotation = settings.ApertureRotation * 3.14159265358979323846f / 180.0f; // degrees → radians + data.ApertureSize = 1.0f / std::max(settings.FStop, 1.0f); + + // Compute PadScale based on mode + GhostMode mode = static_cast(settings.GhostModeInt); + float maxKernelScale = settings.KernelScale; // Quality mode: use global directly + if (mode == GhostMode::Ultra) { + // Ultra: PadScale based on max effective scale across enabled ghosts + maxKernelScale = 0.0f; + for (int i = 0; i < NUM_GHOSTS; i++) { + if (settings.Ghosts[i].Enabled) + maxKernelScale = std::max(maxKernelScale, settings.KernelScale * settings.Ghosts[i].KernelScale); + } + if (maxKernelScale == 0.0f) + maxKernelScale = settings.KernelScale; // fallback if none enabled + } + data.PadScale = std::clamp(1.0f - maxKernelScale, 0.1f, 1.0f); + + std::array srvs = { nullptr }; + std::array uavs = { nullptr }; + std::array samplers = { colorSampler.get(), borderSampler.get() }; + + auto resetViews = [&]() { + srvs.fill(nullptr); + uavs.fill(nullptr); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + }; + + context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); + + // === Pass 1: Threshold — full res input → half res output === + if (!debugsettings.disableThreshold && thresholdCS) { + data.OutputWidth = (float)halfW; + data.OutputHeight = (float)halfH; + data.InputWidth = (float)fullW; + data.InputHeight = (float)fullH; + lensFlareCB->Update(data); + auto cb = lensFlareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + srvs.at(0) = inout_tex.srv; + uavs.at(0) = texThreshold->uav.get(); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(thresholdCS.get(), nullptr, 0); + context->Dispatch((halfW + 7) >> 3, (halfH + 7) >> 3, 1); + resetViews(); + } + + // === Ghost + Halo generation (mode-dependent) === + if ((mode == GhostMode::Quality || mode == GhostMode::Ultra) && fftRowCS && fftColCS && fftMultiplyCS && fftThresholdCS && fftGhostComposeCS) { + DrawQuality(inout_tex, data); + } else { + DrawFast(inout_tex, data); + } + + // === Pass 4: Mix ghost+halo → full res output === + if (mixCS) { + data.OutputWidth = (float)fullW; + data.OutputHeight = (float)fullH; + data.InputWidth = (float)halfW; + data.InputHeight = (float)halfH; + lensFlareCB->Update(data); + auto cb = lensFlareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + srvs.at(0) = texGhostHalo->srv.get(); + uavs.at(0) = texFlare->uav.get(); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(mixCS.get(), nullptr, 0); + context->Dispatch((fullW + 7) >> 3, (fullH + 7) >> 3, 1); + resetViews(); + } + + // Cleanup + resetViews(); + auto nullCB = (ID3D11Buffer*)nullptr; + samplers.fill(nullptr); + context->CSSetConstantBuffers(1, 1, &nullCB); + context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); + context->CSSetShader(nullptr, nullptr, 0); + + inout_tex = { texFlare->resource.get(), texFlare->srv.get() }; + state->EndPerfEvent(); + globals::profiler->EndPass(); +} diff --git a/src/Features/PostProcessing/LensFlare.h b/src/Features/PostProcessing/LensFlare.h new file mode 100644 index 0000000000..0753c502da --- /dev/null +++ b/src/Features/PostProcessing/LensFlare.h @@ -0,0 +1,180 @@ +#pragma once + +#include "Buffer.h" +#include "PostProcessFeature.h" + +struct LensFlare : public PostProcessFeature +{ + virtual inline std::string GetType() const override { return "Lens Flare"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.lens_flare.name", "Lens Flare"); } + virtual inline std::string GetDesc() const override { return T("feature.post_processing.lens_flare.description", "Screen-space lens flare with ghosts and halo. Supports FFT bokeh convolution for physically-shaped ghosts."); } + virtual bool WritesToMainTexture() const override { return false; } + + TextureInfo GetFlareOutput() const; + + static constexpr int NUM_GHOSTS = 8; + static constexpr uint FFT_MIN = 128; + static constexpr uint FFT_MAX = 1024; + static constexpr int MAX_KERNEL_GROUPS = 8; + + enum class GhostMode : int + { + Fast = 0, // Original procedural radial scaling + Quality = 1, // FFT convolution with bokeh shape (single kernel) + Ultra = 2, // FFT convolution with per-ghost kernel sizes + }; + + struct GhostSettings + { + std::array Color = { 1.f, 1.f, 1.f, 1.f }; + float Scale = 1.f; + bool Enabled = true; + float KernelScale = 1.0f; // Multiplier on global KernelScale (Ultra mode only) + }; + + struct Settings + { + float Intensity = 0.1f; + float ThresholdEV = 3.0f; // EV100-based threshold (converted to linear luminance for shader) + float ThresholdRange = 1.0f; + float GhostStrength = 0.3f; + float GhostChromaShift = 0.015f; + int GhostModeInt = 0; // 0 = Fast, 1 = Quality, 2 = Ultra + int FFTResolution = 256; + float KernelScale = 0.1f; // Fraction of FFT resolution for bokeh kernel size + float FStop = 2.8f; // F-number for procedural aperture (e.g. F2.8) + int ApertureBlades = 6; // Number of aperture blades (3-10) + float ApertureRotation = 0.f; // Aperture rotation in degrees + float HaloStrength = 0.2f; + float HaloRadius = 0.5f; + float HaloWidth = 0.5f; + float HaloCompression = 0.65f; + float HaloChromaShift = 0.015f; + std::array Tint = { 1.0f, 0.85f, 0.7f }; + bool GLocalMask = true; + uint8_t pad[3]{}; + std::array Ghosts = { { + { { { 1.0f, 0.8f, 0.4f, 1.0f } }, -1.5f, true, 1.0f }, + { { { 1.0f, 1.0f, 0.6f, 1.0f } }, 2.5f, true, 1.0f }, + { { { 0.8f, 0.8f, 1.0f, 1.0f } }, -5.0f, true, 1.0f }, + { { { 0.5f, 1.0f, 0.4f, 1.0f } }, 10.0f, true, 1.0f }, + { { { 0.5f, 0.8f, 1.0f, 1.0f } }, 0.7f, true, 1.0f }, + { { { 0.9f, 1.0f, 0.8f, 1.0f } }, -0.4f, true, 1.0f }, + { { { 1.0f, 0.8f, 0.4f, 1.0f } }, -0.2f, true, 1.0f }, + { { { 0.9f, 0.7f, 0.7f, 1.0f } }, -0.1f, true, 1.0f }, + } }; + } settings; + + struct alignas(16) LensFlareCB + { + // Per-pass dimensions (set before each dispatch) + float OutputWidth; + float OutputHeight; + float InputWidth; + float InputHeight; + + // Threshold params + float ThresholdLevel; + float ThresholdRange; + float GhostStrength; + float GhostChromaShift; + + // Halo params + float HaloStrength; + float HaloRadius; + float HaloWidth; + float HaloCompression; + + float HaloChromaShift; + float Intensity; + uint FFTResolution; + int GLocalMask; + + float Tint[3]; + float KernelScale; + + float AspectRatio; + int ApertureBlades; + float ApertureRotation; // radians + float PadScale; // 1.0 - maxKernelScale, for zero-padding + + uint ActiveGhostMask; // bitmask of enabled ghosts for current pass + float ApertureSize; // 1.0 / FStop + float pad0[2]{}; + + // Ghost colors as float4[8] = 128 bytes, matches HLSL float4 array + float GhostColors[NUM_GHOSTS * 4]; + // Ghost scales packed as 2 × float4 = 32 bytes, matches HLSL float4[2] + float GhostScalesPacked[8]; + // Per-ghost kernel scales packed as 2 × float4 (Ultra mode) + float GhostKernelScalesPacked[8]; + }; + + struct DebugSettings + { + int blurIterations = 1; + bool disableThreshold = false; + bool disableGhosts = false; + bool disableBlur = false; + uint8_t pad[3]{}; + } debugsettings; + + eastl::unique_ptr lensFlareCB = nullptr; + + eastl::unique_ptr texFlare = nullptr; // full resolution (final output) + eastl::unique_ptr texThreshold = nullptr; // half resolution + eastl::unique_ptr texGhostHalo = nullptr; // half resolution + eastl::unique_ptr texBlurTemp = nullptr; // quarter resolution + + // FFT ghost pipeline textures + eastl::unique_ptr texFFT[2] = {}; // RG32F ping-pong (N×N) + eastl::unique_ptr texBokehFFT = nullptr; // RG32F cached bokeh kernel FFT (N×N) + eastl::unique_ptr texSceneFFT = nullptr; // RG32F cached scene FFT (N×N) + eastl::unique_ptr texFFTResult = nullptr; // RGBA16F FFT convolution result (N×N) + + winrt::com_ptr colorSampler = nullptr; + winrt::com_ptr borderSampler = nullptr; + + // Original pipeline shaders + winrt::com_ptr thresholdCS = nullptr; + winrt::com_ptr ghostHaloCS = nullptr; + winrt::com_ptr blurDownCS = nullptr; + winrt::com_ptr blurUpCS = nullptr; + winrt::com_ptr mixCS = nullptr; + + // FFT ghost pipeline shaders (self-contained in lensflare_fft.cs.hlsl) + winrt::com_ptr fftRowCS = nullptr; + winrt::com_ptr fftColCS = nullptr; + winrt::com_ptr fftRowInvCS = nullptr; + winrt::com_ptr fftColInvCS = nullptr; + winrt::com_ptr fftMultiplyCS = nullptr; + winrt::com_ptr bokehPrepareCS = nullptr; + winrt::com_ptr fftThresholdCS = nullptr; + winrt::com_ptr fftGhostComposeCS = nullptr; + + uint currentFFTResolution = 256; + bool bokehFFTDirty = true; + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + void CreateFFTTextures(uint resolution); + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json&) override; + virtual void SaveSettings(json&) override; + + virtual void DrawSettings() override; + + virtual void Draw(TextureInfo&) override; + + virtual inline void Reset() override { bokehFFTDirty = true; } + +private: + void DispatchFFT(ID3D11ComputeShader* shader, Texture2D* input, Texture2D* output, uint resolution); + void DrawFast(TextureInfo& inout_tex, LensFlareCB& data); + void DrawQuality(TextureInfo& inout_tex, LensFlareCB& data); + void PrepareBokehFFT(); +}; + +inline PostProcessFeature::TextureInfo LensFlare::GetFlareOutput() const { return { texFlare->resource.get(), texFlare->srv.get() }; } diff --git a/src/Features/PostProcessing/LocalExposure.cpp b/src/Features/PostProcessing/LocalExposure.cpp new file mode 100644 index 0000000000..4baa9d4045 --- /dev/null +++ b/src/Features/PostProcessing/LocalExposure.cpp @@ -0,0 +1,430 @@ +#include "LocalExposure.h" + +#include "Features/PostProcessing.h" +#include "HistogramAutoExposure.h" +#include "I18n/I18n.h" +#include "State.h" +#include "Util.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + LocalExposure::Settings, + Exposure, + Shadows, + Highlights, + ExposurePreferenceSigma, + Mip, + DisplayMip, + BoostLocalContrast) + +void LocalExposure::DrawSettings() +{ + ImGui::SliderFloat(T("feature.post_processing.local_exposure.exposure", "Exposure"), &settings.Exposure, 0.f, 4.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.local_exposure.manual_brightness_normalization_used_when_histogram_auto_exposure", "Manual brightness normalization used when Histogram Auto Exposure is disabled. Higher values make the scene behave brighter.")); + + ImGui::SliderFloat(T("feature.post_processing.local_exposure.shadow_recovery", "Shadow Recovery"), &settings.Shadows, 0.f, 4.f, "%.1f EV"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.local_exposure.how_strongly_darker_areas_are_lifted_higher_values", "How strongly darker areas are lifted. Higher values recover more shadow detail.")); + + ImGui::SliderFloat(T("feature.post_processing.local_exposure.highlight_recovery", "Highlight Recovery"), &settings.Highlights, 0.f, 4.f, "%.1f EV"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.local_exposure.how_strongly_bright_areas_are_compressed_higher_values", "How strongly bright areas are compressed. Higher values preserve more highlight detail.")); + + ImGui::SliderFloat(T("feature.post_processing.local_exposure.exposure_preference", "Exposure Preference"), &settings.ExposurePreferenceSigma, 0.f, 10.f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.local_exposure.how_selectively_each_area_chooses_its_best_exposure", "How selectively each area chooses its best exposure. Higher values create stronger local adaptation; lower values blend more softly.")); + + int mipVal = (int)settings.Mip; + ImGui::SliderInt(T("feature.post_processing.local_exposure.coarse_scale_mip", "Coarse Scale (Mip)"), &mipVal, 0, (int)s_MaxMips - 1); + settings.Mip = (uint)std::clamp(mipVal, 0, (int)s_MaxMips - 1); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.local_exposure.largest_image_scale_used_by_the_effect_higher", "Largest image scale used by the effect. Higher values affect broader lighting regions.")); + + int displayMipVal = (int)settings.DisplayMip; + ImGui::SliderInt(T("feature.post_processing.local_exposure.detail_scale_display_mip", "Detail Scale (Display Mip)"), &displayMipVal, 0, (int)s_MaxMips - 1); + settings.DisplayMip = (uint)std::clamp(displayMipVal, 0, (int)settings.Mip); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.local_exposure.finest_pyramid_level_reconstructed_before_full_resolution_upsampling", "Finest pyramid level reconstructed before full-resolution upsampling. Lower values keep smaller local details.")); + + ImGui::Checkbox(T("feature.post_processing.local_exposure.boost_local_contrast", "Boost Local Contrast"), &settings.BoostLocalContrast); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.local_exposure.gives_high_contrast_local_details_more_influence_during", "Gives high-contrast local details more influence during pyramid reconstruction.")); + + if (ImGui::CollapsingHeader(T("feature.post_processing.local_exposure.debug", "Debug"))) { + static float debugRescale = .3f; + ImGui::SliderFloat(T("feature.post_processing.local_exposure.view_resize", "View Resize"), &debugRescale, 0.f, 1.f); + BUFFER_VIEWER_NODE_TITLE(texExposure, "Exposure Map (per-pixel multiplier)", debugRescale); + BUFFER_VIEWER_NODE_TITLE(texExposures, "Synthetic Exposures (RGB=H/M/S)", debugRescale); + BUFFER_VIEWER_NODE_TITLE(texWeights, "Fusion Weights", debugRescale); + BUFFER_VIEWER_NODE_TITLE(texAssemble, "Reconstructed Fusion", debugRescale); + } +} + +void LocalExposure::RestoreDefaultSettings() +{ + settings = {}; +} + +void LocalExposure::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void LocalExposure::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void LocalExposure::SetupResources() +{ + auto renderer = globals::game::renderer; + + // Get screen dimensions from game render target + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + D3D11_TEXTURE2D_DESC mainDesc; + gameTexMainCopy.texture->GetDesc(&mainDesc); + + uint fullW = mainDesc.Width; + uint fullH = mainDesc.Height; + // Calculate mip count for the full-resolution exposure-fusion pyramids. + numMips = 1; + { + uint w = fullW, h = fullH; + while (w > 1 && h > 1 && numMips < s_MaxMips) { + w = (w + 1) / 2; + h = (h + 1) / 2; + numMips++; + } + } + + auto createMipViews = [](Texture2D& texture, + DXGI_FORMAT format, + uint mipCount, + std::array, s_MaxMips>& srvs, + std::array, s_MaxMips>& uavs) { + auto device = globals::d3d::device; + for (uint i = 0; i < mipCount; i++) { + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MostDetailedMip = i; + srvDesc.Texture2D.MipLevels = 1; + DX::ThrowIfFailed(device->CreateShaderResourceView(texture.resource.get(), &srvDesc, srvs[i].put())); + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; + uavDesc.Format = format; + uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D; + uavDesc.Texture2D.MipSlice = i; + DX::ThrowIfFailed(device->CreateUnorderedAccessView(texture.resource.get(), &uavDesc, uavs[i].put())); + } + }; + + // Create exposure luminance and weight pyramids (RGB = highlight, midtone, shadow). + { + D3D11_TEXTURE2D_DESC texDesc = {}; + texDesc.Width = fullW; + texDesc.Height = fullH; + texDesc.MipLevels = numMips; + texDesc.ArraySize = 1; + texDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; + texDesc.SampleDesc.Count = 1; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + + texExposures = eastl::make_unique(texDesc, "LocalExposure Exposures"); + texWeights = eastl::make_unique(texDesc, "LocalExposure Weights"); + + createMipViews(*texExposures, DXGI_FORMAT_R16G16B16A16_FLOAT, numMips, exposureMipSRVs, exposureMipUAVs); + createMipViews(*texWeights, DXGI_FORMAT_R16G16B16A16_FLOAT, numMips, weightMipSRVs, weightMipUAVs); + } + + // Create assembled fusion result pyramid. + { + D3D11_TEXTURE2D_DESC texDesc = {}; + texDesc.Width = fullW; + texDesc.Height = fullH; + texDesc.MipLevels = numMips; + texDesc.ArraySize = 1; + texDesc.Format = DXGI_FORMAT_R16_FLOAT; + texDesc.SampleDesc.Count = 1; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + + texAssemble = eastl::make_unique(texDesc, "LocalExposure Assemble"); + createMipViews(*texAssemble, DXGI_FORMAT_R16_FLOAT, numMips, assembleMipSRVs, assembleMipUAVs); + } + + // Create output exposure texture (full resolution, R16F) + { + D3D11_TEXTURE2D_DESC texDesc = {}; + texDesc.Width = fullW; + texDesc.Height = fullH; + texDesc.MipLevels = 1; + texDesc.ArraySize = 1; + texDesc.Format = DXGI_FORMAT_R16_FLOAT; + texDesc.SampleDesc.Count = 1; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + + texExposure = eastl::make_unique(texDesc, "LocalExposure Output"); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = DXGI_FORMAT_R16_FLOAT; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MostDetailedMip = 0; + srvDesc.Texture2D.MipLevels = 1; + texExposure->CreateSRV(srvDesc); + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; + uavDesc.Format = DXGI_FORMAT_R16_FLOAT; + uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D; + uavDesc.Texture2D.MipSlice = 0; + texExposure->CreateUAV(uavDesc); + } + + // Create linear sampler + { + D3D11_SAMPLER_DESC sampDesc = {}; + sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + sampDesc.MaxLOD = D3D11_FLOAT32_MAX; + + auto device = globals::d3d::device; + DX::ThrowIfFailed(device->CreateSamplerState(&sampDesc, linearSampler.put())); + } + + // Create constant buffer + localExposureCB = std::make_unique(ConstantBufferDesc()); + + CompileComputeShaders(); +} + +void LocalExposure::ClearShaderCache() +{ + const auto shaderPtrs = std::array{ + &setupCS, &downsampleCS, &blendCS, &computeExpCS + }; + + for (auto shader : shaderPtrs) + if ((*shader)) { + (*shader)->Release(); + shader->detach(); + } + + CompileComputeShaders(); +} + +void LocalExposure::CompileComputeShaders() +{ + struct ShaderCompileInfo + { + winrt::com_ptr* programPtr; + std::string_view filename; + std::vector> defines; + std::string entry; + }; + + std::vector shaderInfos = { + { &setupCS, "localexposure.cs.hlsl", {}, "CSSetup" }, + { &downsampleCS, "localexposure.cs.hlsl", {}, "CSDownsample" }, + { &blendCS, "localexposure.cs.hlsl", {}, "CSBlend" }, + { &computeExpCS, "localexposure.cs.hlsl", {}, "CSComputeExposure" }, + }; + + for (auto& info : shaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\LocalExposure") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } +} + +void LocalExposure::Draw(TextureInfo& inout_tex) +{ + auto context = globals::d3d::context; + auto state = globals::state; + + state->BeginPerfEvent("Local Exposure"); + + // Get dimensions + D3D11_TEXTURE2D_DESC mainDesc; + inout_tex.tex->GetDesc(&mainDesc); + uint fullW = mainDesc.Width; + uint fullH = mainDesc.Height; + + uint mipLevel = std::min(settings.Mip, numMips - 1); + uint displayMip = std::min(settings.DisplayMip, mipLevel); + auto* exposure = owner ? owner->GetPipelineFeature(PostProcessing::FeaturePipelineIndex::AutoExposure) : nullptr; + bool useGlobalExposure = exposure && exposure->enabled && exposure->GetAdaptationSRV(); + float exposureCompensation = useGlobalExposure ? exp2(exposure->settings.ExposureCompensation) : 1.f; + float2 adaptationRange = useGlobalExposure ? + float2{ exp2(exposure->settings.AdaptationRange.x - 3.0f), exp2(exposure->settings.AdaptationRange.y - 3.0f) } : + float2{ 1.f, 1.f }; + // Update constant buffer + LocalExposureCB cbData = { + .ManualExposure = settings.Exposure, + .HighlightExposure = exp2(-settings.Highlights), + .ShadowExposure = exp2(settings.Shadows), + .ExposurePreferenceSigmaSq = settings.ExposurePreferenceSigma * settings.ExposurePreferenceSigma, + .InputWidth = fullW, + .InputHeight = fullH, + .MipLevel = mipLevel, + .DisplayMip = displayMip, + .CurrentMip = 0, + .HasCoarserMip = 0, + .BoostLocalContrast = settings.BoostLocalContrast ? 1u : 0u, + .UseGlobalExposure = useGlobalExposure ? 1u : 0u, + .ExposureCompensation = exposureCompensation, + .AdaptationMin = adaptationRange.x, + .AdaptationMax = adaptationRange.y, + .DarkThreshold = 0.007f, + }; + + ID3D11Buffer* cb = localExposureCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + ID3D11SamplerState* sampler = linearSampler.get(); + context->CSSetSamplers(0, 1, &sampler); + + auto updateCB = [&]() { + localExposureCB->Update(cbData); + }; + + auto mipDim = [](uint dim, uint mip) { + return std::max(1u, (dim + ((1u << mip) - 1u)) >> mip); + }; + + // === Pass 1: Compute synthetic exposure luminances and weights === + { + globals::profiler->BeginPass("PostProcessing::LocalExposure::FusionSetup"); + state->BeginPerfEvent("Fusion Setup"); + + updateCB(); + + std::array srvs = { inout_tex.srv, nullptr, nullptr, nullptr, useGlobalExposure ? exposure->GetAdaptationSRV() : nullptr }; + std::array uavs = { exposureMipUAVs[0].get(), weightMipUAVs[0].get() }; + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(setupCS.get(), nullptr, 0); + context->Dispatch((fullW + 7) >> 3, (fullH + 7) >> 3, 1); + + srvs.fill(nullptr); + uavs.fill(nullptr); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + state->EndPerfEvent(); + globals::profiler->EndPass(); + } + + // === Pass 2: Build exposure and weight mip chains === + { + globals::profiler->BeginPass("PostProcessing::LocalExposure::MipChain"); + state->BeginPerfEvent("Mip Chain"); + + for (uint i = 1; i <= mipLevel; i++) { + std::array srvs = { exposureMipSRVs[i - 1].get(), weightMipSRVs[i - 1].get() }; + std::array uavs = { exposureMipUAVs[i].get(), weightMipUAVs[i].get() }; + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(downsampleCS.get(), nullptr, 0); + + uint mipW = mipDim(fullW, i); + uint mipH = mipDim(fullH, i); + context->Dispatch((mipW + 7) >> 3, (mipH + 7) >> 3, 1); + + srvs.fill(nullptr); + uavs.fill(nullptr); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + } + + state->EndPerfEvent(); + globals::profiler->EndPass(); + } + + // === Pass 3: Reconstruct Gaussian/Laplacian exposure-fusion result === + { + globals::profiler->BeginPass("PostProcessing::LocalExposure::FusionBlend"); + state->BeginPerfEvent("Fusion Blend"); + + for (int i = (int)mipLevel; i >= (int)displayMip; i--) { + cbData.CurrentMip = (uint)i; + cbData.HasCoarserMip = i < (int)mipLevel ? 1u : 0u; + updateCB(); + + std::array srvs = { + exposureMipSRVs[i].get(), + weightMipSRVs[i].get(), + cbData.HasCoarserMip ? exposureMipSRVs[i + 1].get() : nullptr, + cbData.HasCoarserMip ? assembleMipSRVs[i + 1].get() : nullptr, + }; + std::array uavs = { nullptr, nullptr, assembleMipUAVs[i].get() }; + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(blendCS.get(), nullptr, 0); + + uint mipW = mipDim(fullW, (uint)i); + uint mipH = mipDim(fullH, (uint)i); + context->Dispatch((mipW + 7) >> 3, (mipH + 7) >> 3, 1); + + srvs.fill(nullptr); + uavs.fill(nullptr); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + } + + state->EndPerfEvent(); + globals::profiler->EndPass(); + } + + // === Pass 4: Guided upsample and output raw-HDR exposure multiplier === + { + globals::profiler->BeginPass("PostProcessing::LocalExposure::ComputeExposure"); + state->BeginPerfEvent("Compute Exposure"); + + cbData.CurrentMip = 0; + cbData.HasCoarserMip = 0; + updateCB(); + + // t0 = full-res raw scene color + // t1 = exposure luminance texture at display_mip + // t2 = assembled fusion result at display_mip + // u2 = output exposure map + std::array srvs = { + inout_tex.srv, + exposureMipSRVs[displayMip].get(), + assembleMipSRVs[displayMip].get(), + nullptr, + useGlobalExposure ? exposure->GetAdaptationSRV() : nullptr, + }; + std::array uavs = { nullptr, nullptr, texExposure->uav.get() }; + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(computeExpCS.get(), nullptr, 0); + context->Dispatch((fullW + 7) >> 3, (fullH + 7) >> 3, 1); + + srvs.fill(nullptr); + uavs.fill(nullptr); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + + state->EndPerfEvent(); + globals::profiler->EndPass(); + } + + // Cleanup + context->CSSetShader(nullptr, nullptr, 0); + cb = nullptr; + context->CSSetConstantBuffers(1, 1, &cb); + sampler = nullptr; + context->CSSetSamplers(0, 1, &sampler); + + // NOTE: We do NOT modify inout_tex. The exposure map is consumed by Composite. + state->EndPerfEvent(); +} diff --git a/src/Features/PostProcessing/LocalExposure.h b/src/Features/PostProcessing/LocalExposure.h new file mode 100644 index 0000000000..5369c88898 --- /dev/null +++ b/src/Features/PostProcessing/LocalExposure.h @@ -0,0 +1,106 @@ +#pragma once + +#include "PostProcessFeature.h" + +#include "Buffer.h" + +/// Local Exposure +/// Generates a per-pixel exposure multiplier using exposure-fusion local +/// tonemapping. Runs before Auto Exposure and is consumed by Composite. +/// The main color texture is not modified by this pass. +/// +/// Reference: +/// https://bartwronski.com/2022/02/28/exposure-fusion-local-tonemapping-for-real-time-rendering/ +/// +/// Algorithm: +/// 1. Normalize raw HDR input with global exposure when available +/// 2. Compute highlight, midtone, and shadow exposure candidates +/// 3. Build luminance and weight pyramids +/// 4. Reconstruct the fused result across the configured mip range +/// 5. Guided-upsample the fused result into a full-resolution multiplier +struct LocalExposure : public PostProcessFeature +{ + virtual inline std::string GetType() const override { return "Local Exposure"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.local_exposure.name", "Local Exposure"); } + virtual inline std::string GetDesc() const override + { + return T("feature.post_processing.local_exposure.description", + "Local Exposure brightens shadows and compresses highlights based on local neighborhood luminance. Runs before Auto Exposure and is applied in the Composite pass."); + } + virtual bool WritesToMainTexture() const override { return false; } + virtual inline bool DisableInMainLoadingMenu() const override { return true; } + + struct Settings + { + float Exposure = 0.7f; // Manual input normalization when Auto Exposure is unavailable + float Shadows = 1.0f; // Shadow recovery EV + float Highlights = 1.5f; // Highlight recovery EV + float ExposurePreferenceSigma = 5.0f; // Exposure selection sharpness + uint Mip = 6; // Coarsest pyramid level used for reconstruction + uint DisplayMip = 2; // Finest reconstructed level before guided upsample + bool BoostLocalContrast = false; // Weight Laplacian bands by local contrast + } settings; + + // Constant buffer for the compute shader + struct alignas(16) LocalExposureCB + { + float ManualExposure; + float HighlightExposure; + float ShadowExposure; + float ExposurePreferenceSigmaSq; + uint InputWidth; + uint InputHeight; + uint MipLevel; + uint DisplayMip; + uint CurrentMip; + uint HasCoarserMip; + uint BoostLocalContrast; + uint UseGlobalExposure; + float ExposureCompensation; + float AdaptationMin; + float AdaptationMax; + float DarkThreshold; + }; + std::unique_ptr localExposureCB = nullptr; + + // Textures + static constexpr uint s_MaxMips = 10; + + eastl::unique_ptr texExposures = nullptr; // RGBA16F, RGB = highlights/midtones/shadows + eastl::unique_ptr texWeights = nullptr; // RGBA16F, normalized synthetic exposure weights + eastl::unique_ptr texAssemble = nullptr; // R16F, reconstructed fusion result + + std::array, s_MaxMips> exposureMipSRVs = {}; + std::array, s_MaxMips> exposureMipUAVs = {}; + std::array, s_MaxMips> weightMipSRVs = {}; + std::array, s_MaxMips> weightMipUAVs = {}; + std::array, s_MaxMips> assembleMipSRVs = {}; + std::array, s_MaxMips> assembleMipUAVs = {}; + uint numMips = 0; + + eastl::unique_ptr texExposure = nullptr; // R16F, full res - the output exposure map + + // Sampler + winrt::com_ptr linearSampler = nullptr; + + // Compute shaders + winrt::com_ptr setupCS = nullptr; // Compute synthetic exposure lums and weights + winrt::com_ptr downsampleCS = nullptr; // Iterative mip downsample + winrt::com_ptr blendCS = nullptr; // Gaussian/Laplacian exposure-fusion reconstruction + winrt::com_ptr computeExpCS = nullptr; // Guided upsample to full-res multiplier + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json&) override; + virtual void SaveSettings(json&) override; + virtual void DrawSettings() override; + + virtual void Draw(TextureInfo&) override; + + /// Get the local exposure texture SRV (R16F, full resolution, per-pixel multiplier). + /// Consumed by the Composite pass. + ID3D11ShaderResourceView* GetExposureSRV() const { return texExposure ? texExposure->srv.get() : nullptr; } +}; diff --git a/src/Features/PostProcessing/MotionBlur.cpp b/src/Features/PostProcessing/MotionBlur.cpp new file mode 100644 index 0000000000..780fc38b25 --- /dev/null +++ b/src/Features/PostProcessing/MotionBlur.cpp @@ -0,0 +1,585 @@ +#include "MotionBlur.h" +#include "Features/Upscaling.h" +#include "ShaderCache.h" +#include "Util.h" + +#pragma warning(disable: 4324) + +// Define serialization for settings +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + MotionBlur::Settings, + VelocityScale, + SampleCount, + ScalePreset) + +void MotionBlur::SetupResources() +{ + auto device = globals::d3d::device; + + // Create samplers + D3D11_SAMPLER_DESC samplerDesc = { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_CLAMP, + .AddressV = D3D11_TEXTURE_ADDRESS_CLAMP, + .AddressW = D3D11_TEXTURE_ADDRESS_CLAMP, + .ComparisonFunc = D3D11_COMPARISON_NEVER, + .MinLOD = 0, + .MaxLOD = D3D11_FLOAT32_MAX + }; + + device->CreateSamplerState(&samplerDesc, linearSampler.put()); + + samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT; + device->CreateSamplerState(&samplerDesc, pointSampler.put()); + + // Compile shaders + CompileComputeShaders(); + + // Initialize constant buffer structs + motionBlurCB = { + .VelocityParams = { GetScaleValueFromPreset(settings.ScalePreset), 1.0f, 1.0f, 0.0f }, + .SampleCount = (settings.SampleCount * 2) & ~1 // Double and ensure it's always even + }; + + reductionPassCB = { + .VelocityParams = { GetScaleValueFromPreset(settings.ScalePreset), 1.0f, 1.0f, 0.0f } + }; + + // Create the actual D3D constant buffers + try { + // Create constant buffers using the ConstantBuffer helper class + blurConstantBufferObj = eastl::make_unique(ConstantBufferDesc()); + reductionPassConstantBufferObj = eastl::make_unique(ConstantBufferDesc()); + + // Initial update + blurConstantBufferObj->Update(motionBlurCB); + reductionPassConstantBufferObj->Update(reductionPassCB); + + // Cache the initial values + lastMotionBlurCB = motionBlurCB; + lastReductionPassCB = reductionPassCB; + } catch (const std::exception& e) { + logger::error("Motion blur error initializing constant buffers: {}", e.what()); + } + + logger::info("Motion blur resources initialized"); +} + +void MotionBlur::CompileComputeShaders() +{ + // Clear existing shaders + horizontalPassShader = nullptr; + verticalPassShader = nullptr; + neighborMaxPassShader = nullptr; + blurPassShader = nullptr; + + struct ShaderInfo + { + winrt::com_ptr* shader; + const char* filename; + }; + + ShaderInfo shaders[] = { + { &horizontalPassShader, "motionblur_horizontalpass.cs.hlsl" }, + { &verticalPassShader, "motionblur_verticalpass.cs.hlsl" }, + { &neighborMaxPassShader, "motionblur_neighborpass.cs.hlsl" }, + { &blurPassShader, "motionblur_blurpass.cs.hlsl" } + }; + + // Compile each shader + for (const auto& info : shaders) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\MotionBlur") / info.filename; + + try { + auto rawPtr = reinterpret_cast( + Util::CompileShader(path.c_str(), {}, "cs_5_0", "main")); + + if (rawPtr) { + info.shader->attach(rawPtr); + logger::info("Compiled shader: {}", info.filename); + } else { + logger::error("Failed to compile shader: {}", info.filename); + } + } catch (const std::exception& e) { + logger::error("Failed to compile {}: {}", info.filename, e.what()); + } + } + + if (!horizontalPassShader || !verticalPassShader || !neighborMaxPassShader || !blurPassShader) { + logger::error("One or more motion blur shaders failed to compile"); + } +} + +void MotionBlur::ClearShaderCache() +{ + // Release resources + horizontalPassShader = nullptr; + verticalPassShader = nullptr; + neighborMaxPassShader = nullptr; + blurPassShader = nullptr; + + horizontalPassTexture = nullptr; + verticalPassTexture = nullptr; + neighborMaxTexture = nullptr; + blurOutputTexture = nullptr; + + // Release constant buffer objects + blurConstantBufferObj = nullptr; + reductionPassConstantBufferObj = nullptr; + + lastWidth = lastHeight = 0; +} + +void MotionBlur::RestoreDefaultSettings() +{ + // Reset to defaults + settings = Settings{}; + settings.VelocityScale = 300.0f; + settings.SampleCount = 8; // 8 base samples = 16 actual samples + settings.ScalePreset = MotionScale::Medium; +} + +void MotionBlur::LoadSettings(json& j) +{ + try { + settings = j; + + // Enforce valid ranges + settings.VelocityScale = std::clamp(settings.VelocityScale, 10.0f, 800.0f); + settings.SampleCount = std::clamp(settings.SampleCount, 8, 16); + + // Ensure valid enum value + int scalePreset = static_cast(settings.ScalePreset); + if (scalePreset < 0 || scalePreset > 4) { + settings.ScalePreset = MotionScale::Medium; + } + } catch (json::exception&) { + RestoreDefaultSettings(); + } +} + +void MotionBlur::SaveSettings(json& j) +{ + j = settings; +} + +void MotionBlur::DrawSettings() +{ + ImGui::Text("Motion Blur Settings"); + + // Motion scale presets + const char* presets[] = { + "Very Short", "Short", "Medium", "Long", "Very Long" + }; + + int preset = static_cast(settings.ScalePreset); + if (ImGui::Combo("Motion Length", &preset, presets, IM_ARRAYSIZE(presets))) { + settings.ScalePreset = static_cast(preset); + } + + // Samples (each UI sample represents 2 actual samples) + ImGui::SliderInt(T("feature.post_processing.motion_blur.samples", "Samples"), &settings.SampleCount, 8, 16, "%d"); + ImGui::SameLine(); + ImGui::TextDisabled(T("feature.post_processing.motion_blur.a", "(?)")); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T("feature.post_processing.motion_blur.sample_count_is_doubled_internally_for_smoother_results", "Sample count is doubled internally for smoother results.\nMore samples = better quality but slower performance")); + } +} + +void MotionBlur::Draw(TextureInfo& inout_tex) +{ + // Skip if disabled + if (!enabled) + return; + + try { + auto renderer = globals::game::renderer; + if (!renderer) { + logger::error("Motion blur error: Renderer is null"); + return; + } + + // First validate that motion vector and post-upscale depth resources exist and are valid + auto& motionVectorTex = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + auto* depthSRV = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].depthSRV; + + // Check that the required resources are valid + if (!motionVectorTex.texture || !motionVectorTex.SRV) { + logger::error("Motion blur error: Motion vector texture is invalid"); + return; + } + + if (!depthSRV) { + logger::error("Motion blur error: Depth texture is invalid"); + return; + } + + // Check that the input texture is valid + if (!inout_tex.tex || !inout_tex.srv) { + logger::error("Motion blur error: Input texture is invalid"); + return; + } + + // Check for resize and update resources if needed + CheckAndResizeResources(inout_tex); + + // Compute dynamic resolution dimensions for dispatch (without overwriting lastWidth/lastHeight + // which are used for resource size tracking) + dynamicWidth = lastWidth; + dynamicHeight = lastHeight; + if (dynamicWidth > 0 && dynamicHeight > 0) { + float2 res = { (float)dynamicWidth, (float)dynamicHeight }; + res = Util::ConvertToDynamic(res); + dynamicWidth = (uint32_t)res.x; + dynamicHeight = (uint32_t)res.y; + } + + // Update constant buffers + UpdateConstantBuffers(); + + // Execute passes + globals::profiler->BeginPass("PostProcessing::MotionBlur"); + ExecuteVerticalPass(); + ExecuteNeighborMaxPass(); + ExecuteBlurPass(inout_tex); + globals::profiler->EndPass(); + } catch (const std::exception& e) { + logger::error("Motion blur error: {}", e.what()); + } catch (...) { + logger::error("Motion blur error: Unknown exception occurred"); + } +} + +bool MotionBlur::CheckAndResizeResources(const TextureInfo& inout_tex) +{ + if (!inout_tex.tex) { + logger::error("Motion blur error: Input texture is null in CheckAndResizeResources"); + return false; + } + + try { + // Get dimensions + D3D11_TEXTURE2D_DESC texDesc; + inout_tex.tex->GetDesc(&texDesc); + uint32_t width = texDesc.Width; + uint32_t height = texDesc.Height; + + // Check if dimensions changed + if (width == lastWidth && height == lastHeight && verticalPassTexture) { + return false; + } + + // Update tracking + lastWidth = width; + lastHeight = height; + + auto device = globals::d3d::device; + if (!device) { + logger::error("Motion blur error: D3D device is null in CheckAndResizeResources"); + return false; + } + + // Fixed grid dimensions + uint32_t gridWidth = FixedGridSize; + uint32_t gridHeight = FixedGridSize; + uint32_t horizontalPassHeight = height; + + // Create texture descriptor + D3D11_TEXTURE2D_DESC gridDesc = { + .Width = gridWidth, + .Height = gridHeight, + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_R16G16B16A16_FLOAT, + .SampleDesc = { 1, 0 }, + .Usage = D3D11_USAGE_DEFAULT, + .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS, + .CPUAccessFlags = 0, + .MiscFlags = 0 + }; + + // Create horizontal pass texture descriptor + D3D11_TEXTURE2D_DESC horizontalDesc = gridDesc; + horizontalDesc.Height = horizontalPassHeight; + + // Release previous resources + verticalPassTexture = nullptr; + neighborMaxTexture = nullptr; + blurOutputTexture = nullptr; + horizontalPassTexture = nullptr; + + // Create textures with error handling + try { + // For grid-based textures (using R16G16B16A16_FLOAT format) + D3D11_SHADER_RESOURCE_VIEW_DESC gridSrvDesc = { + .Format = gridDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC gridUavDesc = { + .Format = gridDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + horizontalPassTexture = eastl::make_unique(horizontalDesc); + horizontalPassTexture->CreateSRV(gridSrvDesc); + horizontalPassTexture->CreateUAV(gridUavDesc); + + verticalPassTexture = eastl::make_unique(gridDesc); + verticalPassTexture->CreateSRV(gridSrvDesc); + verticalPassTexture->CreateUAV(gridUavDesc); + + neighborMaxTexture = eastl::make_unique(gridDesc); + neighborMaxTexture->CreateSRV(gridSrvDesc); + neighborMaxTexture->CreateUAV(gridUavDesc); + + // Create full-resolution output texture with format matching the input texture + D3D11_TEXTURE2D_DESC blurDesc = texDesc; + blurDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + + // Output texture views need to match its format + D3D11_SHADER_RESOURCE_VIEW_DESC blurSrvDesc = { + .Format = blurDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC blurUavDesc = { + .Format = blurDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + blurOutputTexture = eastl::make_unique(blurDesc); + blurOutputTexture->CreateSRV(blurSrvDesc); + blurOutputTexture->CreateUAV(blurUavDesc); + } catch (const std::exception& e) { + logger::error("Motion blur error creating textures: {}", e.what()); + return false; + } + + // Validate that all textures were created successfully + if (!horizontalPassTexture || !verticalPassTexture || !neighborMaxTexture || !blurOutputTexture) { + logger::error("Motion blur error: Failed to create one or more required textures"); + return false; + } + + return true; + } catch (const std::exception& e) { + logger::error("Motion blur resource resize error: {}", e.what()); + return false; + } catch (...) { + logger::error("Motion blur resource resize error: Unknown exception occurred"); + return false; + } +} + +bool MotionBlur::UpdateConstantBuffers() +{ + auto context = globals::d3d::context; + if (!context) { + logger::error("Motion blur error: D3D context is null in UpdateConstantBuffers"); + return false; + } + + if (!blurConstantBufferObj || !reductionPassConstantBufferObj) { + logger::error("Motion blur error: Constant buffers are invalid"); + return false; + } + + bool updated = false; + + // Get actual velocity scale value from preset + float velocityScale = GetScaleValueFromPreset(settings.ScalePreset); + float2 velocityTextureScale = { 1.0f, 1.0f }; + float2 targetResolution = { static_cast(lastWidth), static_cast(lastHeight) }; + + auto& upscaling = globals::features::upscaling; + if (upscaling.loaded && upscaling.IsUpscalingActive()) { + velocityTextureScale.x = std::clamp(upscaling.resolutionScale.x, 0.0f, 1.0f); + velocityTextureScale.y = std::clamp(upscaling.resolutionScale.y, 0.0f, 1.0f); + } + + // Set current values + motionBlurCB = { + .VelocityParams = { velocityScale, velocityTextureScale.x, velocityTextureScale.y, 0.0f }, + .SampleCount = (settings.SampleCount * 2) & ~1 // Double and ensure it's always even + }; + + reductionPassCB = { + .VelocityParams = { velocityScale, velocityTextureScale.x, velocityTextureScale.y, 0.0f }, + .TargetResolution = { targetResolution.x, targetResolution.y, 0.0f, 0.0f } + }; + + // Update blur constant buffer if needed + if (memcmp(&motionBlurCB, &lastMotionBlurCB, sizeof(MotionBlurConstantBuffer)) != 0) { + try { + blurConstantBufferObj->Update(motionBlurCB); + lastMotionBlurCB = motionBlurCB; + updated = true; + } catch (const std::exception& e) { + logger::error("Motion blur error updating blur constant buffer: {}", e.what()); + return false; + } + } + + // Update reduction pass constant buffer if needed + if (memcmp(&reductionPassCB, &lastReductionPassCB, sizeof(ReductionPassConstantBuffer)) != 0) { + try { + reductionPassConstantBufferObj->Update(reductionPassCB); + lastReductionPassCB = reductionPassCB; + updated = true; + } catch (const std::exception& e) { + logger::error("Motion blur error updating reduction constant buffer: {}", e.what()); + return false; + } + } + + return updated; +} + +void MotionBlur::SetupComputePass( + ID3D11ComputeShader* shader, + ID3D11ShaderResourceView** srvs, + uint32_t srvCount, + ID3D11UnorderedAccessView* uav, + ID3D11Buffer* constantBuffer) +{ + auto context = globals::d3d::context; + context->CSSetShader(shader, nullptr, 0); + context->CSSetShaderResources(0, srvCount, srvs); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetConstantBuffers(0, 1, &constantBuffer); +} + +void MotionBlur::ClearComputeResources(uint32_t srvCount) +{ + auto context = globals::d3d::context; + + ID3D11ShaderResourceView* nullSRVs[8] = { nullptr }; + ID3D11UnorderedAccessView* nullUAV[1] = { nullptr }; + ID3D11Buffer* nullCB[1] = { nullptr }; + + context->CSSetShaderResources(0, srvCount, nullSRVs); + context->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr); + context->CSSetConstantBuffers(0, 1, nullCB); +} + +void MotionBlur::ExecuteVerticalPass() +{ + auto context = globals::d3d::context; + + // First do horizontal reduction + ExecuteHorizontalPass(); + + if (!verticalPassTexture || !verticalPassTexture->uav || !horizontalPassTexture || !horizontalPassTexture->srv || !verticalPassShader || !reductionPassConstantBufferObj) + return; + + // Setup vertical pass with horizontal pass texture as input + ID3D11ShaderResourceView* horizontalSRV = horizontalPassTexture->srv.get(); + ID3D11Buffer* reductionCB = reductionPassConstantBufferObj->CB(); + SetupComputePass(verticalPassShader.get(), &horizontalSRV, 1, verticalPassTexture->uav.get(), reductionCB); + + // Dispatch vertical pass: output is [GRID_SIZE × GRID_SIZE], so dispatch covers grid dimensions only + uint32_t dispatchX = (FixedGridSize + 7) / 8; + uint32_t dispatchY = (FixedGridSize + 7) / 8; + context->Dispatch(dispatchX, dispatchY, 1); + + ClearComputeResources(1); +} + +void MotionBlur::ExecuteHorizontalPass() +{ + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + + if (!context || !renderer || !horizontalPassShader || !reductionPassConstantBufferObj) + return; + + // Get motion vectors from engine + auto& motionVectorTex = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + if (!motionVectorTex.texture || !motionVectorTex.SRV || !horizontalPassTexture || !horizontalPassTexture->uav) + return; + + ID3D11ShaderResourceView* velocitySRV = motionVectorTex.SRV; + + // Setup horizontal pass + ID3D11Buffer* reductionCB = reductionPassConstantBufferObj->CB(); + SetupComputePass(horizontalPassShader.get(), &velocitySRV, 1, horizontalPassTexture->uav.get(), reductionCB); + + // Dispatch horizontal pass: output is [GRID_SIZE × height], so dispatch covers grid width and full height + uint32_t dispatchX = (FixedGridSize + 7) / 8; + uint32_t dispatchY = (dynamicHeight + 7) / 8; + context->Dispatch(dispatchX, dispatchY, 1); + + ClearComputeResources(1); +} + +void MotionBlur::ExecuteNeighborMaxPass() +{ + auto context = globals::d3d::context; + + if (!context || !verticalPassTexture || !verticalPassTexture->srv || !neighborMaxTexture || !neighborMaxTexture->uav || !neighborMaxPassShader || !reductionPassConstantBufferObj) + return; + + // Setup neighbor pass + ID3D11ShaderResourceView* verticalPassSRV = verticalPassTexture->srv.get(); + ID3D11Buffer* reductionCB = reductionPassConstantBufferObj->CB(); + SetupComputePass(neighborMaxPassShader.get(), &verticalPassSRV, 1, neighborMaxTexture->uav.get(), reductionCB); + + // Dispatch neighbor pass: operates on [GRID_SIZE × GRID_SIZE] grid + uint32_t dispatchX = (FixedGridSize + 7) / 8; + uint32_t dispatchY = (FixedGridSize + 7) / 8; + context->Dispatch(dispatchX, dispatchY, 1); + + ClearComputeResources(1); +} + +void MotionBlur::ExecuteBlurPass(TextureInfo& inout_tex) +{ + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + + if (!context || !renderer || !blurPassShader || !blurConstantBufferObj) + return; + + // Get engine resources + auto& motionVectorTex = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + auto* depthSRV = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].depthSRV; + + if (!motionVectorTex.SRV || !depthSRV || !neighborMaxTexture || !neighborMaxTexture->srv || !blurOutputTexture || !blurOutputTexture->uav) + return; + + ID3D11ShaderResourceView* velocitySRV = motionVectorTex.SRV; + + // Set samplers + if (!linearSampler || !pointSampler) + return; + + ID3D11SamplerState* samplers[] = { linearSampler.get(), pointSampler.get() }; + context->CSSetSamplers(0, 2, samplers); + + // Setup blur pass + ID3D11ShaderResourceView* srvs[] = { inout_tex.srv, velocitySRV, neighborMaxTexture->srv.get(), depthSRV }; + ID3D11Buffer* blurCB = blurConstantBufferObj->CB(); + + SetupComputePass(blurPassShader.get(), srvs, 4, blurOutputTexture->uav.get(), blurCB); + + // Dispatch blur pass at dynamic resolution (full-screen blur) + uint32_t dispatchX = (dynamicWidth + 7) / 8; + uint32_t dispatchY = (dynamicHeight + 7) / 8; + context->Dispatch(dispatchX, dispatchY, 1); + + // Cleanup + ClearComputeResources(4); + ID3D11SamplerState* nullSamplers[2] = { nullptr, nullptr }; + context->CSSetSamplers(0, 2, nullSamplers); + context->CSSetShader(nullptr, nullptr, 0); + + // Set output + if (blurOutputTexture && blurOutputTexture->resource && blurOutputTexture->srv) { + inout_tex = { blurOutputTexture->resource.get(), blurOutputTexture->srv.get() }; + } +} diff --git a/src/Features/PostProcessing/MotionBlur.h b/src/Features/PostProcessing/MotionBlur.h new file mode 100644 index 0000000000..5ba66e0b2a --- /dev/null +++ b/src/Features/PostProcessing/MotionBlur.h @@ -0,0 +1,144 @@ +#pragma once +// Disable warning about structure padding due to alignment specifier +#pragma warning(disable: 4324) + +/** + * Motion Blur Effect + * + * Three-pass compute shader approach based on CoD:AW: + * 1. Reduction: Horizontal pass to [grid×height], then vertical to [grid×grid] + * 2. Neighbor: Calculate neighborhood max velocities in 3×3 grid groups + * 3. Blur: Apply motion blur using neighborhood velocities and depth comparison + */ + +#include "../../Buffer.h" +#include "PostProcessFeature.h" +#include + +struct MotionBlur : public PostProcessFeature +{ + // Feature interface + inline std::string GetType() const override { return "Motion Blur"; } + inline std::string GetDisplayName() const override { return T("feature.post_processing.motion_blur.name", "Motion Blur"); } + inline std::string GetDesc() const override + { + return T("feature.post_processing.motion_blur.description", "Creates cinematic motion blur based on camera and object movement."); + } + virtual inline bool DisableInMainLoadingMenu() const override { return true; } + + // Constants + static constexpr float MaxBlurRadius = 40.0f; + static constexpr float DepthBiasFactor = 1.5f; + static constexpr int UseDepthBounds = 1; + static constexpr int FixedGridSize = 20; + + // Motion scale presets + enum class MotionScale + { + VeryShort = 0, + Short = 1, + Medium = 2, + Long = 3, + VeryLong = 4 + }; + + // Settings + struct Settings + { + float VelocityScale = 300.0f; // Will be mapped from MotionScale + int SampleCount = 8; // Doubled internally before sending to shader + MotionScale ScalePreset = MotionScale::Medium; + }; + Settings settings; + + // Function to map MotionScale enum to actual velocity scale value + float GetScaleValueFromPreset(MotionScale preset) const + { + switch (preset) { + case MotionScale::VeryShort: + return 100.0f; + case MotionScale::Short: + return 200.0f; + case MotionScale::Medium: + return 300.0f; + case MotionScale::Long: + return 400.0f; + case MotionScale::VeryLong: + return 500.0f; + default: + return 300.0f; + } + } + + // D3D Resources + winrt::com_ptr linearSampler; + winrt::com_ptr pointSampler; + + // Compute shaders + winrt::com_ptr horizontalPassShader; // Pass 1a + winrt::com_ptr verticalPassShader; // Pass 1b + winrt::com_ptr neighborMaxPassShader; // Pass 2 + winrt::com_ptr blurPassShader; // Pass 3 + + // ConstantBuffer wrapper objects + eastl::unique_ptr blurConstantBufferObj; + eastl::unique_ptr reductionPassConstantBufferObj; + + // Textures + eastl::unique_ptr horizontalPassTexture; // [grid x height] + eastl::unique_ptr verticalPassTexture; // [grid x grid] + eastl::unique_ptr neighborMaxTexture; // [grid x grid] + eastl::unique_ptr blurOutputTexture; // Full resolution + + // Dimensions tracking + uint32_t lastWidth = 0; + uint32_t lastHeight = 0; + uint32_t dynamicWidth = 0; // Dynamic resolution width for dispatch (not for resource sizing) + uint32_t dynamicHeight = 0; // Dynamic resolution height for dispatch (not for resource sizing) + + // Constant buffer structs + struct alignas(16) MotionBlurConstantBuffer + { + float4 VelocityParams; // x: velocity scale, yz: motion-vector source/display scale + int32_t SampleCount; + float Padding[3] = {}; + }; + + struct alignas(16) ReductionPassConstantBuffer + { + float4 VelocityParams; // x: velocity scale, yz: motion-vector source/display scale + float4 TargetResolution; // xy: output color resolution + }; + + // CB instances + MotionBlurConstantBuffer motionBlurCB; + ReductionPassConstantBuffer reductionPassCB; + + // Cache for optimization + MotionBlurConstantBuffer lastMotionBlurCB = {}; + ReductionPassConstantBuffer lastReductionPassCB = {}; + + // Interface methods + void SetupResources() override; + void ClearShaderCache() override; + void RestoreDefaultSettings() override; + void LoadSettings(json&) override; + void SaveSettings(json&) override; + void DrawSettings() override; + void Draw(TextureInfo&) override; + + // Helper methods + void CompileComputeShaders(); + bool CheckAndResizeResources(const TextureInfo& inout_tex); + bool UpdateConstantBuffers(); + void SetupComputePass(ID3D11ComputeShader* shader, + ID3D11ShaderResourceView** srvs, + uint32_t srvCount, + ID3D11UnorderedAccessView* uav, + ID3D11Buffer* constantBuffer); + void ClearComputeResources(uint32_t srvCount = 1); + void ExecuteHorizontalPass(); + void ExecuteVerticalPass(); + void ExecuteNeighborMaxPass(); + void ExecuteBlurPass(TextureInfo& inout_tex); +}; diff --git a/src/Features/PostProcessing/OpenDRT.cpp b/src/Features/PostProcessing/OpenDRT.cpp new file mode 100644 index 0000000000..953e2d7598 --- /dev/null +++ b/src/Features/PostProcessing/OpenDRT.cpp @@ -0,0 +1,1273 @@ +#include "OpenDRT.h" +#include "Util.h" + +#include "I18n/I18n.h" +#include + +namespace +{ + enum class LookPreset : int32_t + { + Standard = 0, + Arriba, + Sylvan, + Colorful, + Aery, + Dystopic, + Umbra, + Base, + }; + + enum class TonescalePreset : int32_t + { + UseLookPreset = 0, + LowContrast, + MediumContrast, + HighContrast, + ArribaTonescale, + SylvanTonescale, + ColorfulTonescale, + AeryTonescale, + DystopicTonescale, + UmbraTonescale, + ACES1x, + ACES2, + MarvelousTonescape, + DaGrinchiTonegroan, + }; + + struct OpenDRTPresetSelection + { + LookPreset lookPreset = LookPreset::Standard; + TonescalePreset tonescalePreset = TonescalePreset::UseLookPreset; + }; + + const char* const kInputColorSpaceLabels[] = { + "Linear Rec.709/sRGB", + "ACEScg", + }; + + const char* const kOutputEncodingLabels[] = { + "Linear Rec.709/sRGB", + "Linear Rec.2020", + }; + + const char* const kLookPresetLabels[] = { + "Standard", + "Arriba", + "Sylvan", + "Colorful", + "Aery", + "Dystopic", + "Umbra", + "Base", + }; + + const char* const kTonescalePresetLabels[] = { + "USE LOOK PRESET", + "Low Contrast", + "Medium Contrast", + "High Contrast", + "Arriba Tonescale", + "Sylvan Tonescale", + "Colorful Tonescale", + "Aery Tonescale", + "Dystopic Tonescale", + "Umbra Tonescale", + "ACES-1.x", + "ACES-2.0", + "Marvelous Tonescape", + "DaGrinchi ToneGroan", + }; + + const char* const kCreativeWhitePresetLabels[] = { + "USE LOOK PRESET", + "D93", + "D75", + "D65", + "D60", + "D55", + "D50", + }; + + const char* const kCreativeWhiteLabels[] = { + "D93", + "D75", + "D65", + "D60", + "D55", + "D50", + }; + + const char* const kSurroundLabels[] = { "Dark", "Dim", "Bright" }; + + int ClampIndex(int value, int count) + { + if (value < 0) { + return 0; + } + if (value >= count) { + return count - 1; + } + return value; + } + + bool ComboInt(const char* label, std::int32_t& value, const char* const* items, int count) + { + int current = ClampIndex(static_cast(value), count); + if (ImGui::Combo(label, ¤t, items, count)) { + value = static_cast(current); + return true; + } + return false; + } + + bool CheckboxInt(const char* label, std::int32_t& value) + { + bool current = value != 0; + if (ImGui::Checkbox(label, ¤t)) { + value = current ? 1 : 0; + return true; + } + return false; + } + + template + bool ComboEnum(const char* label, Enum& value, const char* const (&items)[Count]) + { + int current = ClampIndex(static_cast(value), Count); + if (ImGui::Combo(label, ¤t, items, Count)) { + value = static_cast(current); + return true; + } + return false; + } + +} // namespace + +void ApplyLookPreset(OpenDRTSettings& s, LookPreset preset) +{ + switch (preset) { + case LookPreset::Standard: + s.tn_con = 1.66f; + s.tn_sh = 0.5f; + s.tn_toe = 0.003f; + s.tn_off = 0.005f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 0; + s.tn_lcon = 0.0f; + s.tn_lcon_w = 0.5f; + s.cwp = 2; + s.cwp_lm = 0.25f; + s.rs_sa = 0.35f; + s.rs_rw = 0.25f; + s.rs_bw = 0.55f; + s.pt_enable = 1; + s.pt_lml = 0.25f; + s.pt_lml_r = 0.5f; + s.pt_lml_g = 0.0f; + s.pt_lml_b = 0.1f; + s.pt_lmh = 0.25f; + s.pt_lmh_r = 0.5f; + s.pt_lmh_b = 0.0f; + s.ptl_enable = 1; + s.ptl_c = 0.06f; + s.ptl_m = 0.08f; + s.ptl_y = 0.06f; + s.ptm_enable = 1; + s.ptm_low = 0.4f; + s.ptm_low_rng = 0.25f; + s.ptm_low_st = 0.5f; + s.ptm_high = -0.8f; + s.ptm_high_rng = 0.35f; + s.ptm_high_st = 0.4f; + s.brl_enable = 1; + s.brl = 0.0f; + s.brl_r = -2.5f; + s.brl_g = -1.5f; + s.brl_b = -1.5f; + s.brl_rng = 0.5f; + s.brl_st = 0.35f; + s.brlp_enable = 1; + s.brlp = -0.5f; + s.brlp_r = -1.25f; + s.brlp_g = -1.25f; + s.brlp_b = -0.25f; + s.hc_enable = 1; + s.hc_r = 1.0f; + s.hc_r_rng = 0.3f; + s.hs_rgb_enable = 1; + s.hs_r = 0.6f; + s.hs_r_rng = 0.6f; + s.hs_g = 0.35f; + s.hs_g_rng = 1.0f; + s.hs_b = 0.66f; + s.hs_b_rng = 1.0f; + s.hs_cmy_enable = 1; + s.hs_c = 0.25f; + s.hs_c_rng = 1.0f; + s.hs_m = 0.0f; + s.hs_m_rng = 1.0f; + s.hs_y = 0.0f; + s.hs_y_rng = 1.0f; + break; + case LookPreset::Arriba: + s.tn_con = 1.05f; + s.tn_sh = 0.5f; + s.tn_toe = 0.1f; + s.tn_off = 0.01f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 1.5f; + s.tn_lcon_w = 0.2f; + s.cwp = 2; + s.cwp_lm = 0.25f; + s.rs_sa = 0.35f; + s.rs_rw = 0.25f; + s.rs_bw = 0.55f; + s.pt_enable = 1; + s.pt_lml = 0.25f; + s.pt_lml_r = 0.45f; + s.pt_lml_g = 0.0f; + s.pt_lml_b = 0.1f; + s.pt_lmh = 0.25f; + s.pt_lmh_r = 0.25f; + s.pt_lmh_b = 0.0f; + s.ptl_enable = 1; + s.ptl_c = 0.06f; + s.ptl_m = 0.08f; + s.ptl_y = 0.06f; + s.ptm_enable = 1; + s.ptm_low = 1.0f; + s.ptm_low_rng = 0.4f; + s.ptm_low_st = 0.5f; + s.ptm_high = -0.8f; + s.ptm_high_rng = 0.66f; + s.ptm_high_st = 0.6f; + s.brl_enable = 1; + s.brl = 0.0f; + s.brl_r = -2.5f; + s.brl_g = -1.5f; + s.brl_b = -1.5f; + s.brl_rng = 0.5f; + s.brl_st = 0.35f; + s.brlp_enable = 1; + s.brlp = 0.0f; + s.brlp_r = -1.7f; + s.brlp_g = -2.0f; + s.brlp_b = -0.5f; + s.hc_enable = 1; + s.hc_r = 1.0f; + s.hc_r_rng = 0.3f; + s.hs_rgb_enable = 1; + s.hs_r = 0.6f; + s.hs_r_rng = 0.8f; + s.hs_g = 0.35f; + s.hs_g_rng = 1.0f; + s.hs_b = 0.66f; + s.hs_b_rng = 1.0f; + s.hs_cmy_enable = 1; + s.hs_c = 0.15f; + s.hs_c_rng = 1.0f; + s.hs_m = 0.0f; + s.hs_m_rng = 1.0f; + s.hs_y = 0.0f; + s.hs_y_rng = 1.0f; + break; + case LookPreset::Sylvan: + s.tn_con = 1.6f; + s.tn_sh = 0.5f; + s.tn_toe = 0.01f; + s.tn_off = 0.01f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 0.25f; + s.tn_lcon_w = 0.75f; + s.cwp = 2; + s.cwp_lm = 0.25f; + s.rs_sa = 0.25f; + s.rs_rw = 0.25f; + s.rs_bw = 0.55f; + s.pt_enable = 1; + s.pt_lml = 0.15f; + s.pt_lml_r = 0.5f; + s.pt_lml_g = 0.15f; + s.pt_lml_b = 0.1f; + s.pt_lmh = 0.25f; + s.pt_lmh_r = 0.15f; + s.pt_lmh_b = 0.15f; + s.ptl_enable = 1; + s.ptl_c = 0.05f; + s.ptl_m = 0.08f; + s.ptl_y = 0.05f; + s.ptm_enable = 1; + s.ptm_low = 0.5f; + s.ptm_low_rng = 0.5f; + s.ptm_low_st = 0.5f; + s.ptm_high = -0.8f; + s.ptm_high_rng = 0.5f; + s.ptm_high_st = 0.5f; + s.brl_enable = 1; + s.brl = -1.0f; + s.brl_r = -2.0f; + s.brl_g = -2.0f; + s.brl_b = 0.0f; + s.brl_rng = 0.25f; + s.brl_st = 0.25f; + s.brlp_enable = 1; + s.brlp = -1.0f; + s.brlp_r = -0.5f; + s.brlp_g = -0.25f; + s.brlp_b = -0.25f; + s.hc_enable = 1; + s.hc_r = 1.0f; + s.hc_r_rng = 0.4f; + s.hs_rgb_enable = 1; + s.hs_r = 0.6f; + s.hs_r_rng = 1.15f; + s.hs_g = 0.8f; + s.hs_g_rng = 1.25f; + s.hs_b = 0.6f; + s.hs_b_rng = 1.0f; + s.hs_cmy_enable = 1; + s.hs_c = 0.25f; + s.hs_c_rng = 0.25f; + s.hs_m = 0.25f; + s.hs_m_rng = 0.5f; + s.hs_y = 0.35f; + s.hs_y_rng = 0.5f; + break; + case LookPreset::Colorful: + s.tn_con = 1.5f; + s.tn_sh = 0.5f; + s.tn_toe = 0.003f; + s.tn_off = 0.003f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 0.4f; + s.tn_lcon_w = 0.5f; + s.cwp = 2; + s.cwp_lm = 0.25f; + s.rs_sa = 0.35f; + s.rs_rw = 0.25f; + s.rs_bw = 0.55f; + s.pt_enable = 1; + s.pt_lml = 0.5f; + s.pt_lml_r = 1.0f; + s.pt_lml_g = 0.0f; + s.pt_lml_b = 0.5f; + s.pt_lmh = 0.15f; + s.pt_lmh_r = 0.15f; + s.pt_lmh_b = 0.15f; + s.ptl_enable = 1; + s.ptl_c = 0.05f; + s.ptl_m = 0.06f; + s.ptl_y = 0.05f; + s.ptm_enable = 1; + s.ptm_low = 0.8f; + s.ptm_low_rng = 0.5f; + s.ptm_low_st = 0.4f; + s.ptm_high = -0.8f; + s.ptm_high_rng = 0.4f; + s.ptm_high_st = 0.4f; + s.brl_enable = 1; + s.brl = 0.0f; + s.brl_r = -1.25f; + s.brl_g = -1.25f; + s.brl_b = -0.25f; + s.brl_rng = 0.3f; + s.brl_st = 0.5f; + s.brlp_enable = 1; + s.brlp = -0.5f; + s.brlp_r = -1.25f; + s.brlp_g = -1.25f; + s.brlp_b = -0.5f; + s.hc_enable = 1; + s.hc_r = 1.0f; + s.hc_r_rng = 0.4f; + s.hs_rgb_enable = 1; + s.hs_r = 0.5f; + s.hs_r_rng = 0.8f; + s.hs_g = 0.35f; + s.hs_g_rng = 1.0f; + s.hs_b = 0.5f; + s.hs_b_rng = 1.0f; + s.hs_cmy_enable = 1; + s.hs_c = 0.25f; + s.hs_c_rng = 1.0f; + s.hs_m = 0.0f; + s.hs_m_rng = 1.0f; + s.hs_y = 0.25f; + s.hs_y_rng = 1.0f; + break; + case LookPreset::Aery: + s.tn_con = 1.15f; + s.tn_sh = 0.5f; + s.tn_toe = 0.04f; + s.tn_off = 0.006f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 0.0f; + s.tn_hcon_st = 0.5f; + s.tn_lcon_enable = 1; + s.tn_lcon = 0.5f; + s.tn_lcon_w = 2.0f; + s.cwp = 1; + s.cwp_lm = 0.25f; + s.rs_sa = 0.25f; + s.rs_rw = 0.2f; + s.rs_bw = 0.5f; + s.pt_enable = 1; + s.pt_lml = 0.0f; + s.pt_lml_r = 0.5f; + s.pt_lml_g = 0.15f; + s.pt_lml_b = 0.1f; + s.pt_lmh = 0.0f; + s.pt_lmh_r = 0.1f; + s.pt_lmh_b = 0.0f; + s.ptl_enable = 1; + s.ptl_c = 0.05f; + s.ptl_m = 0.08f; + s.ptl_y = 0.05f; + s.ptm_enable = 1; + s.ptm_low = 0.8f; + s.ptm_low_rng = 0.35f; + s.ptm_low_st = 0.5f; + s.ptm_high = -0.9f; + s.ptm_high_rng = 0.5f; + s.ptm_high_st = 0.3f; + s.brl_enable = 1; + s.brl = -3.0f; + s.brl_r = 0.0f; + s.brl_g = 0.0f; + s.brl_b = 1.0f; + s.brl_rng = 0.8f; + s.brl_st = 0.15f; + s.brlp_enable = 1; + s.brlp = -1.0f; + s.brlp_r = -1.0f; + s.brlp_g = -1.0f; + s.brlp_b = 0.0f; + s.hc_enable = 1; + s.hc_r = 0.5f; + s.hc_r_rng = 0.25f; + s.hs_rgb_enable = 1; + s.hs_r = 0.6f; + s.hs_r_rng = 1.0f; + s.hs_g = 0.35f; + s.hs_g_rng = 2.0f; + s.hs_b = 0.5f; + s.hs_b_rng = 1.5f; + s.hs_cmy_enable = 1; + s.hs_c = 0.35f; + s.hs_c_rng = 1.0f; + s.hs_m = 0.25f; + s.hs_m_rng = 1.0f; + s.hs_y = 0.35f; + s.hs_y_rng = 0.5f; + break; + case LookPreset::Dystopic: + s.tn_con = 1.6f; + s.tn_sh = 0.5f; + s.tn_toe = 0.01f; + s.tn_off = 0.008f; + s.tn_hcon_enable = 1; + s.tn_hcon = 0.25f; + s.tn_hcon_pv = 0.0f; + s.tn_hcon_st = 1.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 1.0f; + s.tn_lcon_w = 0.75f; + s.cwp = 3; + s.cwp_lm = 0.25f; + s.rs_sa = 0.2f; + s.rs_rw = 0.25f; + s.rs_bw = 0.55f; + s.pt_enable = 1; + s.pt_lml = 0.15f; + s.pt_lml_r = 0.0f; + s.pt_lml_g = 0.0f; + s.pt_lml_b = 0.0f; + s.pt_lmh = 0.0f; + s.pt_lmh_r = 0.0f; + s.pt_lmh_b = 0.0f; + s.ptl_enable = 1; + s.ptl_c = 0.05f; + s.ptl_m = 0.08f; + s.ptl_y = 0.05f; + s.ptm_enable = 1; + s.ptm_low = 0.25f; + s.ptm_low_rng = 0.25f; + s.ptm_low_st = 0.8f; + s.ptm_high = -0.8f; + s.ptm_high_rng = 0.6f; + s.ptm_high_st = 0.25f; + s.brl_enable = 1; + s.brl = -2.0f; + s.brl_r = -2.0f; + s.brl_g = -2.0f; + s.brl_b = 0.0f; + s.brl_rng = 0.35f; + s.brl_st = 0.35f; + s.brlp_enable = 1; + s.brlp = 0.0f; + s.brlp_r = -1.0f; + s.brlp_g = -1.0f; + s.brlp_b = -1.0f; + s.hc_enable = 1; + s.hc_r = 1.0f; + s.hc_r_rng = 0.25f; + s.hs_rgb_enable = 1; + s.hs_r = 0.7f; + s.hs_r_rng = 1.33f; + s.hs_g = 1.0f; + s.hs_g_rng = 2.0f; + s.hs_b = 0.75f; + s.hs_b_rng = 2.0f; + s.hs_cmy_enable = 1; + s.hs_c = 1.0f; + s.hs_c_rng = 0.5f; + s.hs_m = 1.0f; + s.hs_m_rng = 1.0f; + s.hs_y = 1.0f; + s.hs_y_rng = 0.765f; + break; + case LookPreset::Umbra: + s.tn_con = 1.8f; + s.tn_sh = 0.5f; + s.tn_toe = 0.001f; + s.tn_off = 0.015f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 1.0f; + s.tn_lcon_w = 1.0f; + s.cwp = 5; + s.cwp_lm = 0.25f; + s.rs_sa = 0.35f; + s.rs_rw = 0.25f; + s.rs_bw = 0.55f; + s.pt_enable = 1; + s.pt_lml = 0.0f; + s.pt_lml_r = 0.5f; + s.pt_lml_g = 0.0f; + s.pt_lml_b = 0.15f; + s.pt_lmh = 0.25f; + s.pt_lmh_r = 0.25f; + s.pt_lmh_b = 0.0f; + s.ptl_enable = 1; + s.ptl_c = 0.05f; + s.ptl_m = 0.06f; + s.ptl_y = 0.05f; + s.ptm_enable = 1; + s.ptm_low = 0.4f; + s.ptm_low_rng = 0.35f; + s.ptm_low_st = 0.66f; + s.ptm_high = -0.6f; + s.ptm_high_rng = 0.45f; + s.ptm_high_st = 0.45f; + s.brl_enable = 1; + s.brl = -2.0f; + s.brl_r = -4.5f; + s.brl_g = -3.0f; + s.brl_b = -4.0f; + s.brl_rng = 0.35f; + s.brl_st = 0.3f; + s.brlp_enable = 1; + s.brlp = 0.0f; + s.brlp_r = -2.0f; + s.brlp_g = -1.0f; + s.brlp_b = -0.5f; + s.hc_enable = 1; + s.hc_r = 1.0f; + s.hc_r_rng = 0.35f; + s.hs_rgb_enable = 1; + s.hs_r = 0.66f; + s.hs_r_rng = 1.0f; + s.hs_g = 0.5f; + s.hs_g_rng = 2.0f; + s.hs_b = 0.85f; + s.hs_b_rng = 2.0f; + s.hs_cmy_enable = 1; + s.hs_c = 0.0f; + s.hs_c_rng = 1.0f; + s.hs_m = 0.25f; + s.hs_m_rng = 1.0f; + s.hs_y = 0.66f; + s.hs_y_rng = 0.66f; + break; + case LookPreset::Base: + s.tn_con = 1.66f; + s.tn_sh = 0.5f; + s.tn_toe = 0.003f; + s.tn_off = 0.005f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 0; + s.tn_lcon = 0.0f; + s.tn_lcon_w = 0.5f; + s.cwp = 2; + s.cwp_lm = 0.25f; + s.rs_sa = 0.35f; + s.rs_rw = 0.25f; + s.rs_bw = 0.55f; + s.pt_enable = 1; + s.pt_lml = 0.5f; + s.pt_lml_r = 0.5f; + s.pt_lml_g = 0.15f; + s.pt_lml_b = 0.15f; + s.pt_lmh = 0.8f; + s.pt_lmh_r = 0.5f; + s.pt_lmh_b = 0.0f; + s.ptl_enable = 1; + s.ptl_c = 0.05f; + s.ptl_m = 0.06f; + s.ptl_y = 0.05f; + s.ptm_enable = 0; + s.ptm_low = 0.0f; + s.ptm_low_rng = 0.5f; + s.ptm_low_st = 0.5f; + s.ptm_high = 0.0f; + s.ptm_high_rng = 0.5f; + s.ptm_high_st = 0.5f; + s.brl_enable = 0; + s.brl = 0.0f; + s.brl_r = 0.0f; + s.brl_g = 0.0f; + s.brl_b = 0.0f; + s.brl_rng = 0.5f; + s.brl_st = 0.35f; + s.brlp_enable = 1; + s.brlp = -0.5f; + s.brlp_r = -1.6f; + s.brlp_g = -1.6f; + s.brlp_b = -0.8f; + s.hc_enable = 0; + s.hc_r = 0.0f; + s.hc_r_rng = 0.25f; + s.hs_rgb_enable = 0; + s.hs_r = 0.0f; + s.hs_r_rng = 1.0f; + s.hs_g = 0.0f; + s.hs_g_rng = 1.0f; + s.hs_b = 0.0f; + s.hs_b_rng = 1.0f; + s.hs_cmy_enable = 0; + s.hs_c = 0.0f; + s.hs_c_rng = 1.0f; + s.hs_m = 0.0f; + s.hs_m_rng = 1.0f; + s.hs_y = 0.0f; + s.hs_y_rng = 1.0f; + break; + default: + break; + } +} + +void ApplyTonescalePreset(OpenDRTSettings& s, TonescalePreset preset) +{ + switch (preset) { + case TonescalePreset::LowContrast: + s.tn_con = 1.4f; + s.tn_sh = 0.5f; + s.tn_toe = 0.003f; + s.tn_off = 0.005f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 0; + s.tn_lcon = 0.0f; + s.tn_lcon_w = 0.5f; + break; + case TonescalePreset::MediumContrast: + s.tn_con = 1.66f; + s.tn_sh = 0.5f; + s.tn_toe = 0.003f; + s.tn_off = 0.005f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 0; + s.tn_lcon = 0.0f; + s.tn_lcon_w = 0.5f; + break; + case TonescalePreset::HighContrast: + s.tn_con = 1.4f; + s.tn_sh = 0.5f; + s.tn_toe = 0.003f; + s.tn_off = 0.005f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 1.0f; + s.tn_lcon_w = 0.5f; + break; + case TonescalePreset::ArribaTonescale: + s.tn_con = 1.05f; + s.tn_sh = 0.5f; + s.tn_toe = 0.1f; + s.tn_off = 0.01f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 1.5f; + s.tn_lcon_w = 0.2f; + break; + case TonescalePreset::SylvanTonescale: + s.tn_con = 1.6f; + s.tn_sh = 0.5f; + s.tn_toe = 0.01f; + s.tn_off = 0.01f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 0.25f; + s.tn_lcon_w = 0.75f; + break; + case TonescalePreset::ColorfulTonescale: + s.tn_con = 1.5f; + s.tn_sh = 0.5f; + s.tn_toe = 0.003f; + s.tn_off = 0.003f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 0.4f; + s.tn_lcon_w = 0.5f; + break; + case TonescalePreset::AeryTonescale: + s.tn_con = 1.15f; + s.tn_sh = 0.5f; + s.tn_toe = 0.04f; + s.tn_off = 0.006f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 0.0f; + s.tn_hcon_st = 0.5f; + s.tn_lcon_enable = 1; + s.tn_lcon = 0.5f; + s.tn_lcon_w = 2.0f; + break; + case TonescalePreset::DystopicTonescale: + s.tn_con = 1.6f; + s.tn_sh = 0.5f; + s.tn_toe = 0.01f; + s.tn_off = 0.008f; + s.tn_hcon_enable = 1; + s.tn_hcon = 0.25f; + s.tn_hcon_pv = 0.0f; + s.tn_hcon_st = 1.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 1.0f; + s.tn_lcon_w = 0.75f; + break; + case TonescalePreset::UmbraTonescale: + s.tn_con = 1.8f; + s.tn_sh = 0.5f; + s.tn_toe = 0.001f; + s.tn_off = 0.015f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 1.0f; + s.tn_lcon_w = 1.0f; + break; + case TonescalePreset::ACES1x: + s.tn_con = 1.0f; + s.tn_sh = 0.35f; + s.tn_toe = 0.02f; + s.tn_off = 0.0f; + s.tn_hcon_enable = 1; + s.tn_hcon = 0.55f; + s.tn_hcon_pv = 0.0f; + s.tn_hcon_st = 2.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 1.13f; + s.tn_lcon_w = 1.0f; + break; + case TonescalePreset::ACES2: + s.tn_con = 1.15f; + s.tn_sh = 0.5f; + s.tn_toe = 0.04f; + s.tn_off = 0.0f; + s.tn_hcon_enable = 0; + s.tn_hcon = 1.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 1.0f; + s.tn_lcon_enable = 0; + s.tn_lcon = 1.0f; + s.tn_lcon_w = 0.6f; + break; + case TonescalePreset::MarvelousTonescape: + s.tn_con = 1.5f; + s.tn_sh = 0.5f; + s.tn_toe = 0.003f; + s.tn_off = 0.01f; + s.tn_hcon_enable = 1; + s.tn_hcon = 0.25f; + s.tn_hcon_pv = 0.0f; + s.tn_hcon_st = 4.0f; + s.tn_lcon_enable = 1; + s.tn_lcon = 1.0f; + s.tn_lcon_w = 1.0f; + break; + case TonescalePreset::DaGrinchiTonegroan: + s.tn_con = 1.2f; + s.tn_sh = 0.5f; + s.tn_toe = 0.02f; + s.tn_off = 0.0f; + s.tn_hcon_enable = 0; + s.tn_hcon = 0.0f; + s.tn_hcon_pv = 1.0f; + s.tn_hcon_st = 1.0f; + s.tn_lcon_enable = 0; + s.tn_lcon = 0.0f; + s.tn_lcon_w = 0.6f; + break; + default: + break; + } +} + +void OpenDRTDrawSettings(OpenDRTSettings& s, bool hdrActive, float hdrPaperWhite, float hdrPeakNits) +{ + static OpenDRTPresetSelection presets{}; + + if (ImGui::CollapsingHeader(T("feature.post_processing.open_drt.built_in_presets", "Built-in Presets"), ImGuiTreeNodeFlags_DefaultOpen)) { + ComboEnum(T("feature.post_processing.open_drt.look_preset", "Look Preset"), presets.lookPreset, kLookPresetLabels); + ComboEnum(T("feature.post_processing.open_drt.tonescale_preset", "Tonescale Preset"), presets.tonescalePreset, kTonescalePresetLabels); + + if (ImGui::Button(T("feature.post_processing.open_drt.apply_preset", "Apply Preset"), { -FLT_MIN, 0 })) { + ApplyLookPreset(s, presets.lookPreset); + ApplyTonescalePreset(s, presets.tonescalePreset); + s.clamp = 1; + } + } + + // if (ImGui::CollapsingHeader(T("feature.post_processing.open_drt.input", "Input"), ImGuiTreeNodeFlags_DefaultOpen)) { + // ComboInt(T("feature.post_processing.open_drt.input_color_space", "Input Color Space"), s.input_color_space, kInputColorSpaceLabels, IM_ARRAYSIZE(kInputColorSpaceLabels)); + // ComboInt(T("feature.post_processing.open_drt.output_encoding", "Output Encoding"), s.output_encoding, kOutputEncodingLabels, IM_ARRAYSIZE(kOutputEncodingLabels)); + // } + + if (ImGui::CollapsingHeader(T("feature.post_processing.open_drt.display", "Display"), ImGuiTreeNodeFlags_DefaultOpen)) { + if (hdrActive) + ImGui::BeginDisabled(); + ImGui::SliderFloat(T("feature.post_processing.open_drt.display_peak_luminance", "Display Peak Luminance"), &s.tn_Lp, 100.0f, 1000.0f, "%.0f"); + if (hdrActive) + ImGui::EndDisabled(); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.peak_display_luminance_in_nits_in_sdr_the", + "Peak display luminance in nits.\n" + "In SDR, the max value stays pinned at 1.0. In HDR, this is overridden by HDR Display Peak Brightness.")); + if (hdrActive) { + const float paperWhite = std::max(hdrPaperWhite, 1.0f); + const float peakNits = std::max(hdrPeakNits, paperWhite); + ImGui::TextDisabled(T("feature.post_processing.open_drt.hdr_display_nits_paper_white_nits_peak_linear", "HDR Display: %.0f nits paper white, %.0f nits peak (%.2f linear)"), paperWhite, peakNits, peakNits / paperWhite); + } + ImGui::SliderFloat(T("feature.post_processing.open_drt.hdr_grey_boost", "HDR Grey Boost"), &s.tn_gb, 0.0f, 1.0f, "%.3f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.amount_of_stops_to_boost_grey_luminance_per", + "Amount of stops to boost Grey Luminance, per stop of exposure increase of Peak Luminance.\n" + "For example, if HDR Grey Boost is 0.1, middle grey will be boosted by 0.1 stops per stop of Peak Luminance increase.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hdr_purity", "HDR Purity"), &s.pt_hdr, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.how_much_to_affect_purity_compression_and_hue", + "How much to affect purity compression and hue shift as Peak Luminance increases.\n" + "A value of 0.0 will keep the purity compression and hue shift behavior the same for SDR and HDR.\n" + "A value of 1.0 will preserve more purity as peak luminance increases\n" + "(at the risk of gradient disruptions in high purity high intensity light sources), and will reduce hue shift amount in highlights.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.display_grey_luminance", "Display Grey Luminance"), &s.tn_Lg, 3.0f, 25.0f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.display_luminance_for_middle_grey_0_18_in", + "Display luminance for middle grey (0.18) in nits.\n" + "Sets the target value for middle grey within the available luminance range of the display device.")); + ComboInt(T("feature.post_processing.open_drt.surround", "Surround"), s.tn_su, kSurroundLabels, IM_ARRAYSIZE(kSurroundLabels)); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.opendrt_includes_a_simple_surround_compensation_model_dci", + "OpenDRT includes a simple surround compensation model.\n" + "DCI cinema presets use dark surround. Rec.1886 uses dim surround. And sRGB Display uses bright surround.\n" + "This functionality should provide a better perceptual match between viewing environments.")); + ComboInt(T("feature.post_processing.open_drt.creative_white", "Creative White"), s.cwp, kCreativeWhiteLabels, IM_ARRAYSIZE(kCreativeWhiteLabels)); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.set_the_creative_whitepoint_of_the_display_peak", + "Set the creative whitepoint of the display peak luminance.\n" + "With D65 all channels are equal. With D50, the peak luminance value will match a D50 whitepoint.\n" + "This can be creatively desireable. This adjustment is applied post-tonescale.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.creative_white_limit", "Creative White Limit"), &s.cwp_lm, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.limit_the_intensity_range_affected_by_the_creative", + "Limit the intensity range affected by the Creative Whitepoint.\n" + "At 0.0, the entire intensity range is affected. As the limit is decreased, more of midtones and shadows are kept neutral.\n" + "It can be creatively desireable to keep midtones more neutral while shifting highlights warmer for example.")); + CheckboxInt(T("feature.post_processing.open_drt.clamp", "Clamp"), s.clamp); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.clamp_the_final_image_into_the_final_range", "Clamp the final image into the final range supported by the display device.")); + } + + if (ImGui::CollapsingHeader(T("feature.post_processing.open_drt.tonescale", "Tonescale"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat(T("feature.post_processing.open_drt.contrast", "Contrast"), &s.tn_con, 1.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.adjusts_contrast_or_slope_a_constrained_power_function", + "Adjusts contrast or slope.\n" + "A constrained power function applied in display linear.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.shoulder_clip", "Shoulder Clip"), &s.tn_sh, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.unitless_control_for_the_scene_linear_value_at", + "Unitless control for the scene-linear value at which the tonescale system crosses the peak display linear value (1.0) and clips.\n" + "This is not an exact constraint in order to keep the system simple, but corresponds to roughly 16 at Shoulder Clip = 0 and 1024 at Shoulder Clip = 1")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.toe", "Toe"), &s.tn_toe, 0.0f, 0.1f, "%.3f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.quadratic_toe_compression_strongly_compresses_deep_shadows_helpful", + "Quadratic toe compression.\n" + "Strongly compresses deep shadows. Helpful to have some amount to smooth the transition into display minimum.\n" + "Higher values with a strong positive Offset also valid. Similar to common camera DRT tonescale strategies.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.offset", "Offset"), &s.tn_off, 0.0f, 0.02f, "%.4f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.pre_tonescale_scene_linear_offset_if_0_0", + "Pre-tonescale scene-linear offset.\n" + "If 0.0, scene-linear 0.0 maps to display-linear 0.0 through the tonescale system.\n" + "Many camera imaging pipelines apply a negative offset to set the average of shadow grain at 0.0.\n" + "A positive Offset can be desireable to compensate for this and increase detail in shadows, in addition to being aesthetically desireable.\n" + "Offset should NOT be a negative number (Looking at you ACES 1.x)")); + CheckboxInt(T("feature.post_processing.open_drt.enable_contrast_high", "Enable Contrast High"), s.tn_hcon_enable); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.contrast_high_allows_control_of_the_upper_section", + "Contrast High allows control of the upper section of the tonescale function.\n" + "Off by default, but can be useful if a stronger highlight contrast, or a softer highlight rolloff behavior is desired.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.contrast_high", "Contrast High"), &s.tn_hcon, -1.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.amount_adjust_highlights_positive_values_increase_highlight_exposure", + "Amount adjust highlights.\n" + "Positive values increase highlight exposure, negative values decrease. 0 has no effect.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.contrast_high_pivot", "Contrast High Pivot"), &s.tn_hcon_pv, 0.0f, 4.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.amount_of_stops_above_middle_grey_0_18", "Amount of stops above middle grey (0.18) to start the adjustment.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.contrast_high_strength", "Contrast High Strength"), &s.tn_hcon_st, 0.0f, 4.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.how_quickly_above_the_contrast_high_pivot_the", "How quickly above the Contrast High Pivot the effect begins.")); + CheckboxInt(T("feature.post_processing.open_drt.enable_contrast_low", "Enable Contrast Low"), s.tn_lcon_enable); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.contrast_low_adds_contrast_to_the_midtones_and", + "Contrast Low adds contrast to the midtones and shadows.\n" + "Middle grey (0.18) is un-changed through the adjustment.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.contrast_low", "Contrast Low"), &s.tn_lcon, 0.0f, 3.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.amount_of_contrast_to_add_0_0_has", + "Amount of contrast to add. 0.0 has no effect.\n" + "1.0 will expose down by 1 stop at the origin (0,0)")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.contrast_low_width", "Contrast Low Width"), &s.tn_lcon_w, 0.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.the_width_of_the_adjustment_width_below_0", + "The width of the adjustment.\n" + "Width below 0.5 will mostly affect values between 0 and middle grey.\n" + "Values above 0.5 will increasingly start to increase highlight contrast, which could be desired or not depending on what you are trying to do.")); + } + + if (ImGui::CollapsingHeader(T("feature.post_processing.open_drt.render_space", "Render Space"))) { + ImGui::SliderFloat(T("feature.post_processing.open_drt.render_space_strength", "Render Space Strength"), &s.rs_sa, 0.0f, 0.6f, "%.3f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.render_space_is_the_encoding_in_which_the", + "Render space is the encoding in which the RGB Ratios are taken.\n" + "Strength controls how much to desaturate from P3 gamut. Creatively, the more you desaturate, the more brilliance is increased in the resulting image.\n" + "To be used with caution as this affects every other aspect of the image rendering.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.render_space_weight_r", "Render Space Weight R"), &s.rs_rw, 0.0f, 0.8f, "%.3f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.the_red_weight_of_the_render_space_strength", + "The Red weight of the Render Space Strength.\n" + "Modify with caution as this affects every other part of the image rendering.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.render_space_weight_b", "Render Space Weight B"), &s.rs_bw, 0.0f, 0.8f, "%.3f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.the_blue_weight_of_the_render_space_strength", + "The Blue weight of the Render Space Strength.\n" + "Modify with caution as this affects every other part of the image rendering.")); + } + + if (ImGui::CollapsingHeader(T("feature.post_processing.open_drt.purity", "Purity"))) { + CheckboxInt(T("feature.post_processing.open_drt.enable_purity_compress_high", "Enable Purity Compress High"), s.pt_enable); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.compresses_purity_as_intensity_increases_bare_minimum_functionality", + "Compresses purity as intensity increases.\n" + "Bare minimum functionality for a picture formation.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.purity_limit_low", "Purity Limit Low"), &s.pt_lml, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity", + "Limit the strength of purity compression as intensity decreases, for all hue angles.\n" + "A higher value will compress purity less in midtones and shadows.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.purity_limit_low_r", "Purity Limit Low R"), &s.pt_lml_r, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_2", + "Limit the strength of purity compression as intensity decreases, for reds.\n" + "A higher value will compress purity less in midtones and shadows.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.purity_limit_low_g", "Purity Limit Low G"), &s.pt_lml_g, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_3", + "Limit the strength of purity compression as intensity decreases, for all greens.\n" + "A higher value will compress purity less in midtones and shadows.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.purity_limit_low_b", "Purity Limit Low B"), &s.pt_lml_b, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_4", + "Limit the strength of purity compression as intensity decreases, for blues.\n" + "A higher value will compress purity less in midtones and shadows.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.purity_limit_high", "Purity Limit High"), &s.pt_lmh, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.limit_the_strength_of_purity_compression_as_intensity_5", + "Limit the strength of purity compression as intensity increases.\n" + "Can be helpful to keep some color in high intensity high purity light sources.\n" + "Use with caution as this can cause gradient disruptions.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.purity_limit_high_r", "Purity Limit High R"), &s.pt_lmh_r, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.limit_the_strength_of_red_purity_compression_as", + "Limit the strength of red purity compression as intensity increases.\n" + "Can be helpful to keep some color in high intensity fire and pure red light sources.\n" + "Use with caution as this can cause gradient disruptions.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.purity_limit_high_b", "Purity Limit High B"), &s.pt_lmh_b, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.limit_the_strength_of_blue_purity_compression_as", + "Limit the strength of blue purity compression as intensity increases.\n" + "Can be helpful to keep some color in high intensity pure blue light sources.\n" + "Use with caution as this can cause gradient disruptions.")); + CheckboxInt(T("feature.post_processing.open_drt.enable_purity_softclip", "Enable Purity Softclip"), s.ptl_enable); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.purity_softclip_increases_tonality_and_smoothness_in_extremely", + "Purity Softclip increases tonality and smoothness in extremely pure input values\n" + "that can not be adequately compressed into the display-referred gamut volume.\n" + "The algorithm is tuned for common camera observer colorimetry sources.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.purity_softclip_c", "Purity Softclip C"), &s.ptl_c, 0.0f, 0.25f, "%.4f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.purity_softclip_strength_for_cyan", "Purity Softclip strength for Cyan.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.purity_softclip_m", "Purity Softclip M"), &s.ptl_m, 0.0f, 0.25f, "%.4f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.purity_softclip_strength_for_magenta", "Purity Softclip strength for Magenta.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.purity_softclip_y", "Purity Softclip Y"), &s.ptl_y, 0.0f, 0.25f, "%.4f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.purity_softclip_strength_for_yellow", "Purity Softclip strength for Yellow.")); + CheckboxInt(T("feature.post_processing.open_drt.enable_mid_purity", "Enable Mid Purity"), s.ptm_enable); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.the_mid_purity_module_adjusts_mid_range_purity", + "The Mid Purity module adjusts mid-range purity of midtones and highlights.\n" + "Without this module enabled, it is likely that midtones will not appear colorful enough,\n" + "and highlights will appear too colorful resulting in chaulky pasty looking images especially in yellows and cyans. ")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.mid_purity_low", "Mid Purity Low"), &s.ptm_low, 0.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.amount_to_increase_purity_of_midtones_and_shadows", + "Amount to increase purity of midtones and shadows in mid-range purity areas.\n" + "A value of 0.0 will have no effect. A value of 1.0 is the maximum possible value while preserving smoothness.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.mid_purity_low_range", "Mid Purity Low Range"), &s.ptm_low_rng, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.the_strength_of_the_mid_purity_low_adjustment", + "The strength of the Mid Purity Low adjustment.\n" + "Higher values affect more of the luminance range.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.mid_purity_low_strength", "Mid Purity Low Strength"), &s.ptm_low_st, 0.1f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.the_strength_of_the_mid_purity_low_adjustment_2", + "The strength of the Mid Purity Low adjustment.\n" + "Higher values affect more of the purity range.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.mid_purity_high", "Mid Purity High"), &s.ptm_high, -0.9f, 0.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.amount_to_decrease_purity_of_upper_midtones_and", + "Amount to decrease purity of upper midtones and highlights in mid-range purity areas.\n" + "A value of 0.0 will have no effect. A value of 1.0 is the maximum possible value while preserving smoothness.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.mid_purity_high_range", "Mid Purity High Range"), &s.ptm_high_rng, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.the_strength_of_the_mid_purity_high_adjustment", + "The strength of the Mid Purity High adjustment.\n" + "Higher values affect more of the luminance range.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.mid_purity_high_strength", "Mid Purity High Strength"), &s.ptm_high_st, 0.1f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.the_strength_of_the_mid_purity_high_adjustment_2", + "The strength of the Mid Purity High adjustment.\n" + "Higher values affect more of the purity range.")); + } + + if (ImGui::CollapsingHeader(T("feature.post_processing.open_drt.brilliance", "Brilliance##Header"))) { + CheckboxInt(T("feature.post_processing.open_drt.enable_brilliance", "Enable Brilliance"), s.brl_enable); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.brilliance_scales_the_intensity_of_more_pure_stimuli", + "Brilliance scales the intensity of more pure stimuli.\n" + "The brilliance module is applied before the tonescale is taken for purity compression.\n" + "This means that if you darken reds with this adjustment, the purity compression will also be reduced.\n" + "This behavior is natural and smooth.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.brilliance_2", "Brilliance"), &s.brl, -6.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.global_intensity_scale_of_high_purity_stimuli", "Global intensity scale of high-purity stimuli.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.brilliance_r", "Brilliance R"), &s.brl_r, -6.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.scale_intensity_of_high_purity_reds", "Scale intensity of high-purity reds.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.brilliance_g", "Brilliance G"), &s.brl_g, -6.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.scale_intensity_of_high_purity_greens", "Scale intensity of high-purity greens.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.brilliance_b", "Brilliance B"), &s.brl_b, -6.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.scale_intensity_of_high_purity_blues", "Scale intensity of high-purity blues.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.brilliance_range", "Brilliance Range"), &s.brl_rng, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.as_brilliance_range_is_increased_the_brilliance_adjustments", "As Brilliance Range is increased, the brilliance adjustments affect more the low intensity values of the image data.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.brilliance_strength", "Brilliance Strength"), &s.brl_st, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.as_brilliance_strength_is_increased_the_brilliance_adjustments", "As Brilliance Strength is increased, the brilliance adjustments affect more the low purity values of the image data.")); + CheckboxInt(T("feature.post_processing.open_drt.enable_post_brilliance", "Enable Post Brilliance"), s.brlp_enable); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.post_brilliance_scales_the_intensity_of_more_pure", + "Post Brilliance scales the intensity of more pure stimuli after purity compression hue shifts have been applied.\n" + "With the OpenDRT algorithm it is possible to get high intensity high purity values going out of the top of the display-referred gamut volume,\n" + "which can cause discontinuities in gradients, especially on the RGB primaries.\n" + "This module can help compensate for this.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.brilliance_post", "Brilliance Post"), &s.brlp, -1.0f, 0.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.global_post_purity_compression_brilliance_adjustment", "Global post purity compression brilliance adjustment ")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.post_brilliance_r", "Post Brilliance R"), &s.brlp_r, -3.0f, 0.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.scale_intensity_of_high_purity_red_post_purity", + "Scale intensity of high-purity red, post purity compression.\n" + "This can help reduce rings or halos around high purity light sources.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.post_brilliance_g", "Post Brilliance G"), &s.brlp_g, -3.0f, 0.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.scale_intensity_of_high_purity_green_post_purity", + "Scale intensity of high-purity green, post purity compression.\n" + "This can help reduce rings or halos around high purity light sources.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.post_brilliance_b", "Post Brilliance B"), &s.brlp_b, -3.0f, 0.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.scale_intensity_of_high_purity_blue_post_purity", + "Scale intensity of high-purity blue, post purity compression.\n" + "This can help reduce rings or halos around high purity light sources.")); + } + + if (ImGui::CollapsingHeader(T("feature.post_processing.open_drt.hue", "Hue"))) { + CheckboxInt(T("feature.post_processing.open_drt.enable_hue_contrast", "Enable Hue Contrast"), s.hc_enable); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.hue_contrast_compresses_hue_angle_towards_the_primary", + "Hue Contrast compresses hue angle towards the primary at the bottom end and expands the hue angle towards the secondary as intensity increases.\n" + "It also increases purity as it compresses, and decreases purity as it expands.\n" + "This leads to a nice creatively controllable simulation of this effect from per-channel tonescales.\n" + "For OpenDRT we only keep the red hue angle control since it is the most useful.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hue_contrast_r", "Hue Contrast R"), &s.hc_r, 0.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.amount_to_increase_hue_contrast_at_the_red", "Amount to increase Hue Contrast at the red hue angle.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hue_contrast_r_range", "Hue Contrast R Range"), &s.hc_r_rng, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.hue_contrast_range_control_determines_where_over_the", + "Hue contrast range control: determines where over the intensity range the hue contrast affects.\n" + "Higher values place the crossover point higher in the intensity range.")); + CheckboxInt(T("feature.post_processing.open_drt.enable_hueshift_rgb", "Enable Hueshift RGB"), s.hs_rgb_enable); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.hue_shift_rgb_adds_hue_distortion_to_the", + "Hue Shift RGB adds hue distortion to the red green and blue primary hue angles as intensity increases.\n" + "By default OpenDRT will compress purity in a straight line in RGB/Chromaticity space.\n" + "This can lead to perceived hue shifts due to the Abney Effect, for example a pure blue will perceptually shift towards purple as it desaturates.\n" + "To compensate for this, and to use as a creative tool, this module allows creative control over the path that red green and blue hue angles take as their purity is compressed.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_r", "Hueshift R"), &s.hs_r, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.amount_to_distort_the_red_hue_angle_towards", "Amount to distort the red hue angle towards yellow as intensity increases.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_r_range", "Hueshift R Range"), &s.hs_r_rng, 0.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.range_of_the_red_hueshift_higher_values_affect", "Range of the red hueshift: higher values affect more of the lower intensity range.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_g", "Hueshift G"), &s.hs_g, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.amount_to_distort_the_green_hue_angle_towards", "Amount to distort the green hue angle towards yellow as intensity increases.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_g_range", "Hueshift G Range"), &s.hs_g_rng, 0.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.range_of_the_green_hueshift_higher_values_affect", "Range of the green hueshift: higher values affect more of the lower intensity range.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_b", "Hueshift B"), &s.hs_b, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.amount_to_distort_the_blue_hue_angle_towards", "Amount to distort the blue hue angle towards cyan as intensity increases.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_b_range", "Hueshift B Range"), &s.hs_b_rng, 0.0f, 4.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.range_of_the_blue_hueshift_higher_values_affect", "Range of the blue hueshift: higher values affect more of the lower intensity range.")); + CheckboxInt(T("feature.post_processing.open_drt.enable_hueshift_cmy", "Enable Hueshift CMY"), s.hs_cmy_enable); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.open_drt.hue_shift_cmy_adds_hue_distortion_to_the", + "Hue Shift CMY adds hue distortion to the cyan magenta and yellow secondary hue angles as intensity decreases.\n" + "This module allows some very minimal adjustments of secondary hue angles as a creative tool.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_c", "Hueshift C"), &s.hs_c, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.amount_to_distort_the_cyan_hue_angle_towards", "Amount to distort the cyan hue angle towards blue as intensity decreases.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_c_range", "Hueshift C Range"), &s.hs_c_rng, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.range_of_the_cyan_hueshift_higher_values_affect", "Range of the cyan hueshift: higher values affect more of the upper intensity range.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_m", "Hueshift M"), &s.hs_m, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.amount_to_distort_the_magenta_hue_angle_towards", "Amount to distort the magenta hue angle towards blue as intensity decreases.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_m_range", "Hueshift M Range"), &s.hs_m_rng, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.range_of_the_magenta_hueshift_higher_values_affect", "Range of the magenta hueshift: higher values affect more of the upper intensity range.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_y", "Hueshift Y"), &s.hs_y, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.amount_to_distort_the_yellow_hue_angle_towards", "Amount to distort the yellow hue angle towards red as intensity decreases.")); + ImGui::SliderFloat(T("feature.post_processing.open_drt.hueshift_y_range", "Hueshift Y Range"), &s.hs_y_rng, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.open_drt.range_of_the_yellow_hueshift_higher_values_affect", "Range of the yellow hueshift: higher values affect more of the upper intensity range.")); + } +} diff --git a/src/Features/PostProcessing/OpenDRT.h b/src/Features/PostProcessing/OpenDRT.h new file mode 100644 index 0000000000..20fe307c46 --- /dev/null +++ b/src/Features/PostProcessing/OpenDRT.h @@ -0,0 +1,84 @@ +#pragma once + +#include "Buffer.h" +#include "PostProcessFeature.h" + +// Matches OpenDRTConstants in OpenDRT.hlsl. Use 32-bit integers, not bools. +struct OpenDRTSettings +{ + int32_t input_color_space = 0; + int32_t output_encoding = 0; + int32_t clamp = 1; + int32_t tn_su = 1; + float tn_Lp = 100.0f; + float tn_gb = 0.13f; + float pt_hdr = 0.5f; + float tn_Lg = 10.0f; + float tn_con = 1.66f; + float tn_sh = 0.5f; + float tn_toe = 0.003f; + float tn_off = 0.005f; + int32_t tn_hcon_enable = 0; + float tn_hcon = 0.0f; + float tn_hcon_pv = 1.0f; + float tn_hcon_st = 4.0f; + int32_t tn_lcon_enable = 0; + float tn_lcon = 0.0f; + float tn_lcon_w = 0.5f; + int32_t cwp = 2; + float cwp_lm = 0.25f; + float rs_sa = 0.35f; + float rs_rw = 0.25f; + float rs_bw = 0.55f; + int32_t pt_enable = 1; + float pt_lml = 0.25f; + float pt_lml_r = 0.5f; + float pt_lml_g = 0.0f; + float pt_lml_b = 0.1f; + float pt_lmh = 0.25f; + float pt_lmh_r = 0.5f; + float pt_lmh_b = 0.0f; + int32_t ptl_enable = 1; + float ptl_c = 0.06f; + float ptl_m = 0.08f; + float ptl_y = 0.06f; + int32_t ptm_enable = 1; + float ptm_low = 0.4f; + float ptm_low_rng = 0.25f; + float ptm_low_st = 0.5f; + float ptm_high = -0.8f; + float ptm_high_rng = 0.35f; + float ptm_high_st = 0.4f; + int32_t brl_enable = 1; + float brl = 0.0f; + float brl_r = -2.5f; + float brl_g = -1.5f; + float brl_b = -1.5f; + float brl_rng = 0.5f; + float brl_st = 0.35f; + int32_t brlp_enable = 1; + float brlp = -0.5f; + float brlp_r = -1.25f; + float brlp_g = -1.25f; + float brlp_b = -0.25f; + int32_t hc_enable = 1; + float hc_r = 1.0f; + float hc_r_rng = 0.3f; + int32_t hs_rgb_enable = 1; + float hs_r = 0.6f; + float hs_r_rng = 0.6f; + float hs_g = 0.35f; + float hs_g_rng = 1.0f; + float hs_b = 0.66f; + float hs_b_rng = 1.0f; + int32_t hs_cmy_enable = 1; + float hs_c = 0.25f; + float hs_c_rng = 1.0f; + float hs_m = 0.0f; + float hs_m_rng = 1.0f; + float hs_y = 0.0f; + float hs_y_rng = 1.0f; +}; +static_assert(sizeof(OpenDRTSettings) % 16 == 0); + +void OpenDRTDrawSettings(OpenDRTSettings& s, bool hdrActive = false, float hdrPaperWhite = 203.0f, float hdrPeakNits = 1000.0f); diff --git a/src/Features/PostProcessing/OpenDRTIo.h b/src/Features/PostProcessing/OpenDRTIo.h new file mode 100644 index 0000000000..c1d9583a3c --- /dev/null +++ b/src/Features/PostProcessing/OpenDRTIo.h @@ -0,0 +1,164 @@ +#pragma once + +#include "OpenDRT.h" + +// Helper structs for JSON serialization (split due to macro limitations) +struct OpenDRTSettingsPart1 +{ + int32_t input_color_space = 0; + int32_t output_encoding = 0; + int32_t clamp = 1; + int32_t tn_su = 1; + float tn_Lp = 100.0f; + float tn_gb = 0.13f; + float pt_hdr = 0.5f; + float tn_Lg = 10.0f; + float tn_con = 1.66f; + float tn_sh = 0.5f; + float tn_toe = 0.003f; + float tn_off = 0.005f; + int32_t tn_hcon_enable = 0; + float tn_hcon = 0.0f; + float tn_hcon_pv = 1.0f; + float tn_hcon_st = 4.0f; + int32_t tn_lcon_enable = 0; + float tn_lcon = 0.0f; + float tn_lcon_w = 0.5f; + int32_t cwp = 2; + float cwp_lm = 0.25f; + float rs_sa = 0.35f; + float rs_rw = 0.25f; + float rs_bw = 0.55f; + int32_t pt_enable = 1; + float pt_lml = 0.25f; + float pt_lml_r = 0.5f; + float pt_lml_g = 0.0f; + float pt_lml_b = 0.1f; + float pt_lmh = 0.25f; + float pt_lmh_r = 0.5f; + float pt_lmh_b = 0.0f; + int32_t ptl_enable = 1; + float ptl_c = 0.06f; + float ptl_m = 0.08f; + float ptl_y = 0.06f; + int32_t ptm_enable = 1; + float ptm_low = 0.4f; + float ptm_low_rng = 0.25f; + float ptm_low_st = 0.5f; + float ptm_high = -0.8f; + float ptm_high_rng = 0.35f; + float ptm_high_st = 0.4f; +}; +static_assert(sizeof(OpenDRTSettingsPart1) == offsetof(OpenDRTSettings, brl_enable)); + +struct OpenDRTSettingsPart2 +{ + int32_t brl_enable = 1; + float brl = 0.0f; + float brl_r = -2.5f; + float brl_g = -1.5f; + float brl_b = -1.5f; + float brl_rng = 0.5f; + float brl_st = 0.35f; + int32_t brlp_enable = 1; + float brlp = -0.5f; + float brlp_r = -1.25f; + float brlp_g = -1.25f; + float brlp_b = -0.25f; + int32_t hc_enable = 1; + float hc_r = 1.0f; + float hc_r_rng = 0.3f; + int32_t hs_rgb_enable = 1; + float hs_r = 0.6f; + float hs_r_rng = 0.6f; + float hs_g = 0.35f; + float hs_g_rng = 1.0f; + float hs_b = 0.66f; + float hs_b_rng = 1.0f; + int32_t hs_cmy_enable = 1; + float hs_c = 0.25f; + float hs_c_rng = 1.0f; + float hs_m = 0.0f; + float hs_m_rng = 1.0f; + float hs_y = 0.0f; + float hs_y_rng = 1.0f; +}; +static_assert(sizeof(OpenDRTSettingsPart1) + sizeof(OpenDRTSettingsPart2) == sizeof(OpenDRTSettings)); + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + OpenDRTSettingsPart1, + input_color_space, + output_encoding, + clamp, + tn_su, + tn_Lp, + tn_gb, + pt_hdr, + tn_Lg, + tn_con, + tn_sh, + tn_toe, + tn_off, + tn_hcon_enable, + tn_hcon, + tn_hcon_pv, + tn_hcon_st, + tn_lcon_enable, + tn_lcon, + tn_lcon_w, + cwp, + cwp_lm, + rs_sa, + rs_rw, + rs_bw, + pt_enable, + pt_lml, + pt_lml_r, + pt_lml_g, + pt_lml_b, + pt_lmh, + pt_lmh_r, + pt_lmh_b, + ptl_enable, + ptl_c, + ptl_m, + ptl_y, + ptm_enable, + ptm_low, + ptm_low_rng, + ptm_low_st, + ptm_high, + ptm_high_rng, + ptm_high_st) + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + OpenDRTSettingsPart2, + brl_enable, + brl, + brl_r, + brl_g, + brl_b, + brl_rng, + brl_st, + brlp_enable, + brlp, + brlp_r, + brlp_g, + brlp_b, + hc_enable, + hc_r, + hc_r_rng, + hs_rgb_enable, + hs_r, + hs_r_rng, + hs_g, + hs_g_rng, + hs_b, + hs_b_rng, + hs_cmy_enable, + hs_c, + hs_c_rng, + hs_m, + hs_m_rng, + hs_y, + hs_y_rng) diff --git a/src/Features/PostProcessing/PhysicalGlare.cpp b/src/Features/PostProcessing/PhysicalGlare.cpp new file mode 100644 index 0000000000..e0b0a5162f --- /dev/null +++ b/src/Features/PostProcessing/PhysicalGlare.cpp @@ -0,0 +1,903 @@ +#include "PhysicalGlare.h" + +#include "Features/LinearLighting.h" +#include "Globals.h" +#include "I18n/I18n.h" +#include "State.h" +#include "Util.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + PhysicalGlare::Settings, + ThresholdEV, + Intensity, + ApertureMode, + ApertureBlades, + ApertureRotation, + ScatterStrength, + AdaptSpeed, + FFTResolution, + FresnelExponent, + ChromaticSpread, + FStop, + SphericalAberration, + KernelScale, + PSFSharpness, + PSFNoiseFloor, + PaddingRatio, + EnableEyelashes, + EyelashCount, + EyelashLength, + EyelashCurvature, + ParticleCount, + ParticleSize, + GratingCount, + GratingStrength, + TearFilmStrength, + TearFilmSpeed, + TearFilmComplexity, + SutureBranches, + SutureStrength, + SutureWidth, + StarburstCount, + StarburstStrength, + StarburstIrregularity, + DustCount, + DustSize, + BladeRoughnessFreq, + BladeRoughnessAmp, + ScratchCount, + ScratchOpacity, + ScratchLength, + ScratchWidth) + +void PhysicalGlare::DrawSettings() +{ + ImGui::SliderFloat(T("feature.post_processing.physical_glare.threshold", "Threshold"), &settings.ThresholdEV, -7.f, 23.f, "%+.2f EV100"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.per_channel_brightness_threshold_for_glare_extraction_in", "Per-channel brightness threshold for glare extraction in EV100 (0 EV100 = 0.125 linear luminance).")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.intensity", "Intensity"), &settings.Intensity, 0.f, 2.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.overall_glare_intensity", "Overall glare intensity.")); + + { + const char* modeNames[] = { "Lens (N-polygon)", "Pupil (Circle)" }; + ImGui::Combo(T("feature.post_processing.physical_glare.aperture_mode", "Aperture Mode"), &settings.ApertureMode, modeNames, 2); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.lens_camera_lens_polygon_starburst_pupil_circular_human", "Lens: camera lens polygon starburst. Pupil: circular human eye aperture.")); + } + + if (settings.ApertureMode == 0) { + ImGui::SliderInt(T("feature.post_processing.physical_glare.aperture_blades", "Aperture Blades"), &settings.ApertureBlades, 3, 10); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.number_of_aperture_blades_controls_starburst_pattern", "Number of aperture blades. Controls starburst pattern.")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.f_stop", "F-Stop"), &settings.FStop, 1.0f, 22.0f, "F%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.aperture_f_number_e_g_f2_8_smaller", "Aperture f-number (e.g. F2.8). Smaller = larger aperture = wider diffraction spikes.\nPhysically: aperture radius = 1 / f-number.")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.spherical_aberration", "Spherical Aberration"), &settings.SphericalAberration, 0.f, 100.f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.physical_glare.seidel_spherical_aberration_r_4_wavefront_error_models", + "Seidel spherical aberration (r^4 wavefront error).\n" + "Models lens curvature: outer rays focus at a different point\n" + "than central rays, producing concentric ring structure in the\n" + "PSF and softer glare edges. Physical range: 0-50.")); + + ImGui::SliderInt(T("feature.post_processing.physical_glare.dust_count", "Dust Count"), &settings.DustCount, 0, 500); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.dust_particles_on_lens_element_surfaces_produces_scattered", "Dust particles on lens element surfaces.\nProduces scattered haze via Babinet's principle.")); + + if (settings.DustCount > 0) { + ImGui::SliderFloat(T("feature.post_processing.physical_glare.dust_size", "Dust Size"), &settings.DustSize, 0.5f, 5.f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.radius_of_each_dust_particle_in_pixels", "Radius of each dust particle in pixels.")); + } + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.blade_roughness", "Blade Roughness"), &settings.BladeRoughnessAmp, 0.f, 2.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.micro_serrations_on_aperture_blade_edges_manufacturing_imperfections", "Micro-serrations on aperture blade edges (manufacturing imperfections).\nMakes star spikes slightly fuzzy/irregular. 0 = perfect edges.")); + + if (settings.BladeRoughnessAmp > 0.f) { + ImGui::SliderInt(T("feature.post_processing.physical_glare.roughness_frequency", "Roughness Frequency"), &settings.BladeRoughnessFreq, 5, 100); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.number_of_bumps_per_blade_edge_higher_finer", "Number of bumps per blade edge. Higher = finer serrations.")); + } + + ImGui::SliderInt(T("feature.post_processing.physical_glare.scratch_count", "Scratch Count"), &settings.ScratchCount, 0, 20); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.linear_scratches_on_lens_element_surfaces_each_scratch", "Linear scratches on lens element surfaces.\nEach scratch produces a perpendicular streak in the glare.")); + + if (settings.ScratchCount > 0) { + ImGui::SliderFloat(T("feature.post_processing.physical_glare.scratch_opacity", "Scratch Opacity"), &settings.ScratchOpacity, 0.f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.how_opaque_each_scratch_is_higher_more_visible", "How opaque each scratch is. Higher = more visible streaks.")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.scratch_length", "Scratch Length"), &settings.ScratchLength, 0.2f, 1.5f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.length_of_scratches_relative_to_aperture_size", "Length of scratches relative to aperture size.")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.scratch_width", "Scratch Width"), &settings.ScratchWidth, 0.5f, 4.f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.pixel_width_of_each_scratch", "Pixel width of each scratch.")); + } + } + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.aperture_rotation", "Aperture Rotation"), &settings.ApertureRotation, -180.f, 180.f, "%.1f deg"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.rotation_angle_of_the_aperture", "Rotation angle of the aperture.")); + + if (settings.ApertureMode == 1) { + ImGui::SliderFloat(T("feature.post_processing.physical_glare.scatter_strength", "Scatter Strength"), &settings.ScatterStrength, 0.f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.opacity_of_scatter_particles_in_pupil_mode_paper", "Opacity of scatter particles in pupil mode (paper section 2.4).\n0 = transparent (no scatter), 1 = fully opaque.")); + + ImGui::SliderInt(T("feature.post_processing.physical_glare.particle_count", "Particle Count"), &settings.ParticleCount, 0, 1000); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.number_of_scatter_particles_in_lens_vitreous_ritschel", "Number of scatter particles in lens/vitreous (Ritschel: 750).\nProduces ciliary corona needle pattern via Babinet's principle.")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.particle_size", "Particle Size"), &settings.ParticleSize, 0.5f, 5.f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.radius_of_each_particle_in_pixels", "Radius of each particle in pixels.")); + + ImGui::SliderInt(T("feature.post_processing.physical_glare.grating_count", "Grating Count"), &settings.GratingCount, 0, 400); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.number_of_radial_lens_gratings_paper_section_2", "Number of radial lens gratings (paper section 2.4: Ritschel uses 200).\nProduces lenticular halo via edge diffraction.")); + + if (settings.GratingCount > 0) { + ImGui::SliderFloat(T("feature.post_processing.physical_glare.grating_strength", "Grating Strength"), &settings.GratingStrength, 0.f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.opacity_of_lens_gratings_higher_stronger_lenticular_halo", "Opacity of lens gratings. Higher = stronger lenticular halo.")); + } + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.tear_film_strength", "Tear Film Strength"), &settings.TearFilmStrength, 0.f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.simulates_tear_film_irregularities_on_the_cornea_surface", "Simulates tear film irregularities on the cornea surface.\nProduces flickering, sharp, irregular star spikes.\n0 = disabled (static PSF).")); + + if (settings.TearFilmStrength > 0.f) { + ImGui::SliderFloat(T("feature.post_processing.physical_glare.tear_film_speed", "Tear Film Speed"), &settings.TearFilmSpeed, 0.1f, 8.f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.how_fast_the_tear_film_fluctuates_blink_refresh", "How fast the tear film fluctuates (blink refresh rate ~0.3Hz, breakup ~2-5Hz).")); + + ImGui::SliderInt(T("feature.post_processing.physical_glare.tear_film_complexity", "Tear Film Complexity"), &settings.TearFilmComplexity, 3, 16); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.number_of_angular_harmonics_more_more_spikes_finer", "Number of angular harmonics. More = more spikes, finer detail.")); + } + + ImGui::SliderInt(T("feature.post_processing.physical_glare.suture_branches", "Suture Branches"), &settings.SutureBranches, 0, 8); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.physical_glare.lens_suture_lines_y_shaped_junctions_where_lens", + "Lens suture lines: Y-shaped junctions where lens fiber cells meet.\n" + "3 = young eye (anterior Y + posterior inverted Y = 6 spikes).\n" + "More branches = older/more complex lens. 0 = disabled.")); + + if (settings.SutureBranches > 0) { + ImGui::SliderFloat(T("feature.post_processing.physical_glare.suture_strength", "Suture Strength"), &settings.SutureStrength, 0.f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.opacity_of_suture_lines_higher_stronger_star_spikes", "Opacity of suture lines. Higher = stronger star spikes.")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.suture_width", "Suture Width"), &settings.SutureWidth, 0.5f, 5.f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.pixel_width_of_each_suture_line_thinner_sharper", "Pixel width of each suture line. Thinner = sharper spikes.")); + } + + ImGui::SliderInt(T("feature.post_processing.physical_glare.starburst_spikes", "Starburst Spikes"), &settings.StarburstCount, 0, 128); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.physical_glare.lens_fiber_radial_phase_grating_creates_many_thin", + "Lens fiber radial phase grating.\nCreates many thin, sharp radial star spikes.\n" + "Higher count = more spikes (typical human eye: 20-80). 0 = disabled.")); + + if (settings.StarburstCount > 0) { + ImGui::SliderFloat(T("feature.post_processing.physical_glare.starburst_strength", "Starburst Strength"), &settings.StarburstStrength, 0.f, 2.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.phase_shift_strength_per_fiber_higher_brighter_spikes", "Phase shift strength per fiber. Higher = brighter spikes.")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.starburst_irregularity", "Starburst Irregularity"), &settings.StarburstIrregularity, 0.f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.physical_glare.random_variation_in_fiber_spacing_and_strength_0", + "Random variation in fiber spacing and strength.\n" + "0 = perfectly regular (even spikes).\n" + "1 = maximally irregular (natural look).")); + } + + if (ImGui::CollapsingHeader(T("feature.post_processing.physical_glare.eyelashes", "Eyelashes"))) { + ImGui::Checkbox(T("feature.post_processing.physical_glare.enable_eyelashes", "Enable Eyelashes"), &settings.EnableEyelashes); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.simulate_eyelash_occlusion_for_streak_effects_paper_section", "Simulate eyelash occlusion for streak effects (paper section 3.1).")); + + if (settings.EnableEyelashes) { + ImGui::SliderInt(T("feature.post_processing.physical_glare.eyelash_count", "Eyelash Count"), &settings.EyelashCount, 5, 80); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.total_number_of_eyelash_hairs_upper_lower", "Total number of eyelash hairs (upper + lower).")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.eyelash_length", "Eyelash Length"), &settings.EyelashLength, 0.1f, 0.8f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.length_of_eyelashes_relative_to_aperture_radius", "Length of eyelashes relative to aperture radius.")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.eyelash_curvature", "Eyelash Curvature"), &settings.EyelashCurvature, 0.f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.streak_curvature_via_uv_bending_paper_fig_3", "Streak curvature via UV bending (paper fig 3.7: sin(x) vertical offset).")); + } + } + } + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.adapt_speed", "Adapt Speed"), &settings.AdaptSpeed, 0.5f, 10.f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.how_fast_the_glare_adapts_to_brightness_changes", "How fast the glare adapts to brightness changes.")); + + { + const char* resNames[] = { "128", "256", "512", "1024" }; + int resValues[] = { 128, 256, 512, 1024 }; + int curIdx = 1; + for (int i = 0; i < 4; i++) + if (resValues[i] == settings.FFTResolution) + curIdx = i; + + if (ImGui::Combo(T("feature.post_processing.physical_glare.fft_resolution", "FFT Resolution"), &curIdx, resNames, 4)) + settings.FFTResolution = resValues[curIdx]; + + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.resolution_of_the_fft_convolution_higher_sharper_starburst", "Resolution of the FFT convolution. Higher = sharper starburst but more expensive.")); + } + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.padding_ratio", "Padding Ratio"), &settings.PaddingRatio, 0.f, 0.25f, "%.3f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.physical_glare.zero_padding_per_side_to_prevent_fft_wrap", + "Zero-padding per side to prevent FFT wrap-around.\n" + "0.25 = paper default (50%% effective resolution).\n" + "0.1 = 80%% effective (recommended for high-res).\n" + "0.0 = 100%% (maximum sharpness, may wrap at edges).\n" + "Lower = sharper glare on high-res screens.")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.kernel_scale", "Kernel Scale"), &settings.KernelScale, 0.01f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.physical_glare.scale_of_the_glare_kernel_size_on_screen", + "Scale of the glare kernel size on screen.\n" + "1.0 = default. Smaller = more concentrated glare.\n" + "Does not affect aperture physics.")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.fresnel_exponent", "Fresnel Exponent"), &settings.FresnelExponent, 0.f, 80.f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.fresnel_phase_at_aperture_edge_radians_paper_eq", "Fresnel phase at aperture edge (radians). Paper eq 2.12: e^(i*pi/(lambda*z) * r^2).\nHigher = more Fresnel rings. 0 = pure Fraunhofer (no rings).")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.chromatic_spread", "Chromatic Spread"), &settings.ChromaticSpread, 0.f, 3.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.physical_glare.multiplier_on_wavelength_dependent_uv_scaling_paper_section", "Multiplier on wavelength-dependent UV scaling (paper section 2.3: lambda/575nm).\n1.0 = physically correct. Higher = more rainbow spread. 0 = monochrome.")); + + if (ImGui::CollapsingHeader(T("feature.post_processing.physical_glare.psf_shaping", "PSF Shaping"))) { + ImGui::SliderFloat(T("feature.post_processing.physical_glare.psf_sharpness", "PSF Sharpness"), &settings.PSFSharpness, 0.2f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.physical_glare.dynamic_range_compression_exponent_paper_table_3_9", + "Dynamic range compression exponent (paper Table 3.9: 0.45).\n" + "Lower = wider/softer glare, higher = concentrated near light source.\n" + "Increase if glare looks too blurry/spreads too far.")); + + ImGui::SliderFloat(T("feature.post_processing.physical_glare.psf_noise_floor", "PSF Noise Floor"), &settings.PSFNoiseFloor, 0.f, 0.01f, "%.4f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.physical_glare.threshold_to_remove_low_level_fft_noise_from", + "Threshold to remove low-level FFT noise from the PSF.\n" + "Paper default: 0.001. Higher = cleaner glare wings.")); + } + + if (ImGui::CollapsingHeader(T("feature.post_processing.physical_glare.debug", "Debug"))) { + if (texGlareResult) + ImGui::Image(texGlareResult->srv.get(), { 256.f, 256.f }); + } +} + +void PhysicalGlare::RestoreDefaultSettings() +{ + settings = {}; +} + +void PhysicalGlare::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void PhysicalGlare::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void PhysicalGlare::CreateFFTTextures(uint resolution) +{ + currentFFTResolution = resolution; + psfDirty = true; + + D3D11_TEXTURE2D_DESC texDesc = { + .Width = resolution, + .Height = resolution, + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_R32G32_FLOAT, + .SampleDesc = { .Count = 1, .Quality = 0 }, + .Usage = D3D11_USAGE_DEFAULT, + .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS, + }; + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + // FFT ping-pong textures (RG32F) for 3 channels + for (int ch = 0; ch < 3; ch++) { + for (int pp = 0; pp < 2; pp++) { + texFFT[ch][pp] = eastl::make_unique(texDesc); + texFFT[ch][pp]->CreateSRV(srvDesc); + texFFT[ch][pp]->CreateUAV(uavDesc); + } + } + + // PSF FFT cache (RG32F) for 3 channels + for (int ch = 0; ch < 3; ch++) { + texPSF_FFT[ch] = eastl::make_unique(texDesc); + texPSF_FFT[ch]->CreateSRV(srvDesc); + texPSF_FFT[ch]->CreateUAV(uavDesc); + } + + // Glare result and history (RGBA16F, FFT resolution) + D3D11_TEXTURE2D_DESC glareDesc = texDesc; + glareDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; + srvDesc.Format = glareDesc.Format; + uavDesc.Format = glareDesc.Format; + + texGlareResult = eastl::make_unique(glareDesc); + texGlareResult->CreateSRV(srvDesc); + texGlareResult->CreateUAV(uavDesc); + + texGlarePrev = eastl::make_unique(glareDesc); + texGlarePrev->CreateSRV(srvDesc); + texGlarePrev->CreateUAV(uavDesc); + + // Clear glare history to zero — D3D11 USAGE_DEFAULT textures have undefined content + // which may contain NaN/Inf, poisoning the temporal blend permanently + auto context = globals::d3d::context; + const FLOAT clearColor[4] = { 0.f, 0.f, 0.f, 0.f }; + context->ClearUnorderedAccessViewFloat(texGlareResult->uav.get(), clearColor); + context->ClearUnorderedAccessViewFloat(texGlarePrev->uav.get(), clearColor); +} + +void PhysicalGlare::SetupResources() +{ + auto renderer = globals::game::renderer; + auto device = globals::d3d::device; + + logger::debug("PhysicalGlare: Creating buffers..."); + { + glareCB = eastl::make_unique(ConstantBufferDesc()); + } + + logger::debug("PhysicalGlare: Creating FFT textures..."); + { + currentFFTResolution = std::clamp((uint)settings.FFTResolution, FFT_MIN, FFT_MAX); + CreateFFTTextures(currentFFTResolution); + } + + logger::debug("PhysicalGlare: Creating output texture..."); + { + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + D3D11_TEXTURE2D_DESC texDesc; + gameTexMainCopy.texture->GetDesc(&texDesc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + texDesc.MipLevels = 1; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + + texOutput = eastl::make_unique(texDesc); + texOutput->CreateSRV(srvDesc); + texOutput->CreateUAV(uavDesc); + } + + logger::debug("PhysicalGlare: Creating samplers..."); + { + D3D11_SAMPLER_DESC samplerDesc = { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_CLAMP, + .AddressV = D3D11_TEXTURE_ADDRESS_CLAMP, + .AddressW = D3D11_TEXTURE_ADDRESS_CLAMP, + .MaxAnisotropy = 1, + .MinLOD = 0, + .MaxLOD = D3D11_FLOAT32_MAX + }; + DX::ThrowIfFailed(device->CreateSamplerState(&samplerDesc, linearSampler.put())); + + D3D11_SAMPLER_DESC wrapSamplerDesc = { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_WRAP, + .AddressV = D3D11_TEXTURE_ADDRESS_WRAP, + .AddressW = D3D11_TEXTURE_ADDRESS_WRAP, + .MaxAnisotropy = 1, + .MinLOD = 0, + .MaxLOD = D3D11_FLOAT32_MAX + }; + DX::ThrowIfFailed(device->CreateSamplerState(&wrapSamplerDesc, wrapSampler.put())); + } + + CompileComputeShaders(); +} + +void PhysicalGlare::ClearShaderCache() +{ + auto const shaderPtrs = std::array{ + &thresholdCS, &apertureCS, &psfCS, &fftRowCS, &fftColCS, &fftRowInvCS, &fftColInvCS, &multiplyCS, &compositeCS + }; + + for (auto shader : shaderPtrs) + if ((*shader)) { + (*shader)->Release(); + shader->detach(); + } + + CompileComputeShaders(); +} + +void PhysicalGlare::CompileComputeShaders() +{ + struct ShaderCompileInfo + { + winrt::com_ptr* programPtr; + std::string_view filename; + std::vector> defines; + std::string entry = "main"; + }; + + std::vector + shaderInfos = { + { &thresholdCS, "threshold.cs.hlsl", {}, "CS_Threshold" }, + { &apertureCS, "aperture.cs.hlsl", {}, "CS_Aperture" }, + { &psfCS, "psf.cs.hlsl", {}, "CS_ChromaticBlur" }, + { &fftRowCS, "fft.cs.hlsl", { { "ROW_PASS", "" }, { "FORWARD", "" } }, "CS_FFT" }, + { &fftColCS, "fft.cs.hlsl", { { "COL_PASS", "" }, { "FORWARD", "" } }, "CS_FFT" }, + { &fftRowInvCS, "fft.cs.hlsl", { { "ROW_PASS", "" }, { "INVERSE", "" } }, "CS_FFT" }, + { &fftColInvCS, "fft.cs.hlsl", { { "COL_PASS", "" }, { "INVERSE", "" } }, "CS_FFT" }, + { &multiplyCS, "multiply.cs.hlsl", {}, "CS_Multiply" }, + { &compositeCS, "composite.cs.hlsl", {}, "CS_Composite" }, + }; + + for (auto& info : shaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\PhysicalGlare") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } +} + +bool PhysicalGlare::NeedsPSFRegeneration() const +{ + return psfDirty || + cachedPSFParams.ApertureMode != settings.ApertureMode || + cachedPSFParams.ApertureBlades != settings.ApertureBlades || + cachedPSFParams.ApertureRotation != settings.ApertureRotation || + cachedPSFParams.ScatterStrength != settings.ScatterStrength || + cachedPSFParams.FFTResolution != settings.FFTResolution || + cachedPSFParams.EnableEyelashes != settings.EnableEyelashes || + cachedPSFParams.EyelashCount != settings.EyelashCount || + cachedPSFParams.EyelashLength != settings.EyelashLength || + cachedPSFParams.EyelashCurvature != settings.EyelashCurvature || + cachedPSFParams.FresnelExponent != settings.FresnelExponent || + cachedPSFParams.ChromaticSpread != settings.ChromaticSpread || + cachedPSFParams.FStop != settings.FStop || + cachedPSFParams.PSFSharpness != settings.PSFSharpness || + cachedPSFParams.PSFNoiseFloor != settings.PSFNoiseFloor || + cachedPSFParams.ParticleCount != settings.ParticleCount || + cachedPSFParams.ParticleSize != settings.ParticleSize || + cachedPSFParams.GratingCount != settings.GratingCount || + cachedPSFParams.GratingStrength != settings.GratingStrength || + cachedPSFParams.TearFilmStrength != settings.TearFilmStrength || + cachedPSFParams.TearFilmSpeed != settings.TearFilmSpeed || + cachedPSFParams.TearFilmComplexity != settings.TearFilmComplexity || + cachedPSFParams.SutureBranches != settings.SutureBranches || + cachedPSFParams.SutureStrength != settings.SutureStrength || + cachedPSFParams.SutureWidth != settings.SutureWidth || + cachedPSFParams.StarburstCount != settings.StarburstCount || + cachedPSFParams.StarburstStrength != settings.StarburstStrength || + cachedPSFParams.StarburstIrregularity != settings.StarburstIrregularity || + cachedPSFParams.DustCount != settings.DustCount || + cachedPSFParams.DustSize != settings.DustSize || + cachedPSFParams.BladeRoughnessFreq != settings.BladeRoughnessFreq || + cachedPSFParams.BladeRoughnessAmp != settings.BladeRoughnessAmp || + cachedPSFParams.ScratchCount != settings.ScratchCount || + cachedPSFParams.ScratchOpacity != settings.ScratchOpacity || + cachedPSFParams.ScratchLength != settings.ScratchLength || + cachedPSFParams.ScratchWidth != settings.ScratchWidth || + cachedPSFParams.SphericalAberration != settings.SphericalAberration || + cachedPSFParams.KernelScale != settings.KernelScale || + cachedPSFParams.UseAP1 != (globals::features::linearLighting.settings.enableACEScg && globals::features::linearLighting.settings.enableLinearLighting) || + settings.TearFilmStrength > 0.f; // force per-frame regen when active +} + +void PhysicalGlare::GeneratePSF() +{ + auto context = globals::d3d::context; + + // Build the CB data for PSF generation + GlareCB cbData = { + .Threshold = exp2(settings.ThresholdEV - 3.0f), + .Intensity = settings.Intensity, + .ScatterStrength = settings.ScatterStrength, + .ApertureMode = (uint)settings.ApertureMode, + .ApertureBlades = settings.ApertureBlades, + .ApertureRotation = settings.ApertureRotation * 3.14159265f / 180.f, + .AdaptSpeed = settings.AdaptSpeed, + .DeltaTime = 0.f, + .FFTResolution = currentFFTResolution, + .PaddingRatio = settings.PaddingRatio, + .ScreenWidth = texOutput ? (float)texOutput->desc.Width : 1920.f, + .ScreenHeight = texOutput ? (float)texOutput->desc.Height : 1080.f, + .ChannelIndex = 0, + .FresnelExponent = settings.FresnelExponent, + .ChromaticSpread = settings.ChromaticSpread, + .ApertureSize = 1.0f / std::max(settings.FStop, 1.0f), + .PSFSharpness = settings.PSFSharpness, + .PSFNoiseFloor = settings.PSFNoiseFloor, + .EnableEyelashes = settings.EnableEyelashes ? 1u : 0u, + .EyelashCurvature = settings.EyelashCurvature, + // Eye mode + .EyelashCount = (uint)settings.EyelashCount, + .EyelashLength = settings.EyelashLength, + .ParticleCount = (uint)settings.ParticleCount, + .ParticleSize = settings.ParticleSize, + .GratingCount = (uint)settings.GratingCount, + .GratingStrength = settings.GratingStrength, + .TearFilmStrength = settings.TearFilmStrength, + .TearFilmSpeed = settings.TearFilmSpeed, + .TearFilmComplexity = (uint)settings.TearFilmComplexity, + .TearFilmTime = tearFilmTimeAccum, + .SutureBranches = (uint)settings.SutureBranches, + .SutureStrength = settings.SutureStrength, + .SutureWidth = settings.SutureWidth, + .StarburstCount = (uint)settings.StarburstCount, + .StarburstStrength = settings.StarburstStrength, + .StarburstIrregularity = settings.StarburstIrregularity, + // Lens mode + .DustCount = (uint)settings.DustCount, + .DustSize = settings.DustSize, + .BladeRoughnessFreq = (uint)settings.BladeRoughnessFreq, + .BladeRoughnessAmp = settings.BladeRoughnessAmp, + .ScratchCount = (uint)settings.ScratchCount, + .ScratchOpacity = settings.ScratchOpacity, + .ScratchLength = settings.ScratchLength, + .ScratchWidth = settings.ScratchWidth, + .SphericalAberration = settings.SphericalAberration, + .UseAP1 = (globals::features::linearLighting.settings.enableACEScg && globals::features::linearLighting.settings.enableLinearLighting) ? 1u : 0u, + .KernelScale = settings.KernelScale, + }; + + glareCB->Update(cbData); + ID3D11Buffer* cb = glareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + // ===== Step 1: Render aperture polygon ===== + // Output: texFFT[0][0] (real = aperture value, imag = 0) + { + ID3D11UnorderedAccessView* uav = texFFT[0][0]->uav.get(); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShader(apertureCS.get(), nullptr, 0); + context->Dispatch((currentFFTResolution + 7) >> 3, (currentFFTResolution + 7) >> 3, 1); + + uav = nullptr; + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + } + + // ===== Step 2: FFT aperture (Fraunhofer diffraction) ===== + // texFFT[0][0] -> row FFT -> texFFT[0][1] -> col FFT -> texFFT[0][0] + // Now texFFT[0][0] holds the complex diffraction amplitude F(u,v) + DispatchFFT(fftRowCS.get(), texFFT[0][0].get(), texFFT[0][1].get(), currentFFTResolution); + DispatchFFT(fftColCS.get(), texFFT[0][1].get(), texFFT[0][0].get(), currentFFTResolution); + + // ===== Step 3: Chromatic blur per RGB channel ===== + // Reads texFFT[0][0] (diffraction amplitude, t0), writes texFFT[ch][1] (u0) + // Computes |F|² at wavelength-dependent UV scales with CIE spectral weighting + { + ID3D11SamplerState* sampler = wrapSampler.get(); + context->CSSetSamplers(0, 1, &sampler); + + for (int ch = 0; ch < 3; ch++) { + cbData.ChannelIndex = (uint)ch; + glareCB->Update(cbData); + cb = glareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + ID3D11ShaderResourceView* srv = texFFT[0][0]->srv.get(); + ID3D11UnorderedAccessView* uav = texFFT[ch][1]->uav.get(); + + context->CSSetShaderResources(0, 1, &srv); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShader(psfCS.get(), nullptr, 0); + context->Dispatch((currentFFTResolution + 7) >> 3, (currentFFTResolution + 7) >> 3, 1); + + srv = nullptr; + uav = nullptr; + context->CSSetShaderResources(0, 1, &srv); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + } + + sampler = nullptr; + context->CSSetSamplers(0, 1, &sampler); + } + + // ===== Step 4: FFT each channel's PSF for frequency-domain storage ===== + // texFFT[ch][1] -> row FFT -> texFFT[ch][0] -> col FFT -> texPSF_FFT[ch] + for (int ch = 0; ch < 3; ch++) { + DispatchFFT(fftRowCS.get(), texFFT[ch][1].get(), texFFT[ch][0].get(), currentFFTResolution); + DispatchFFT(fftColCS.get(), texFFT[ch][0].get(), texPSF_FFT[ch].get(), currentFFTResolution); + } + + // Cache parameters + cachedPSFParams.ApertureMode = settings.ApertureMode; + cachedPSFParams.ApertureBlades = settings.ApertureBlades; + cachedPSFParams.ApertureRotation = settings.ApertureRotation; + cachedPSFParams.ScatterStrength = settings.ScatterStrength; + cachedPSFParams.FFTResolution = settings.FFTResolution; + cachedPSFParams.EnableEyelashes = settings.EnableEyelashes; + cachedPSFParams.EyelashCount = settings.EyelashCount; + cachedPSFParams.EyelashLength = settings.EyelashLength; + cachedPSFParams.EyelashCurvature = settings.EyelashCurvature; + cachedPSFParams.FresnelExponent = settings.FresnelExponent; + cachedPSFParams.ChromaticSpread = settings.ChromaticSpread; + cachedPSFParams.FStop = settings.FStop; + cachedPSFParams.ParticleCount = settings.ParticleCount; + cachedPSFParams.ParticleSize = settings.ParticleSize; + cachedPSFParams.GratingCount = settings.GratingCount; + cachedPSFParams.GratingStrength = settings.GratingStrength; + cachedPSFParams.TearFilmStrength = settings.TearFilmStrength; + cachedPSFParams.TearFilmSpeed = settings.TearFilmSpeed; + cachedPSFParams.TearFilmComplexity = settings.TearFilmComplexity; + cachedPSFParams.SutureBranches = settings.SutureBranches; + cachedPSFParams.SutureStrength = settings.SutureStrength; + cachedPSFParams.SutureWidth = settings.SutureWidth; + cachedPSFParams.StarburstCount = settings.StarburstCount; + cachedPSFParams.StarburstStrength = settings.StarburstStrength; + cachedPSFParams.StarburstIrregularity = settings.StarburstIrregularity; + cachedPSFParams.DustCount = settings.DustCount; + cachedPSFParams.DustSize = settings.DustSize; + cachedPSFParams.PSFSharpness = settings.PSFSharpness; + cachedPSFParams.PSFNoiseFloor = settings.PSFNoiseFloor; + cachedPSFParams.BladeRoughnessAmp = settings.BladeRoughnessAmp; + cachedPSFParams.BladeRoughnessFreq = settings.BladeRoughnessFreq; + cachedPSFParams.ScratchCount = settings.ScratchCount; + cachedPSFParams.ScratchOpacity = settings.ScratchOpacity; + cachedPSFParams.ScratchLength = settings.ScratchLength; + cachedPSFParams.ScratchWidth = settings.ScratchWidth; + cachedPSFParams.SphericalAberration = settings.SphericalAberration; + cachedPSFParams.KernelScale = settings.KernelScale; + cachedPSFParams.UseAP1 = globals::features::linearLighting.settings.enableACEScg && globals::features::linearLighting.settings.enableLinearLighting; + psfDirty = false; +} + +void PhysicalGlare::DispatchFFT(ID3D11ComputeShader* shader, Texture2D* input, Texture2D* output, uint resolution) +{ + auto context = globals::d3d::context; + + ID3D11ShaderResourceView* srv = input->srv.get(); + ID3D11UnorderedAccessView* uav = output->uav.get(); + + context->CSSetShaderResources(0, 1, &srv); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShader(shader, nullptr, 0); + context->Dispatch(resolution, 1, 1); + + srv = nullptr; + uav = nullptr; + context->CSSetShaderResources(0, 1, &srv); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); +} + +void PhysicalGlare::Draw(TextureInfo& inout_tex) +{ + auto state = globals::state; + auto context = globals::d3d::context; + + state->BeginPerfEvent("Physical Glare"); + + // Handle FFT resolution change + uint targetRes = std::clamp((uint)settings.FFTResolution, FFT_MIN, FFT_MAX); + if (targetRes != currentFFTResolution) { + CreateFFTTextures(targetRes); + } + + // Accumulate tear film time + if (settings.TearFilmStrength > 0.f) { + tearFilmTimeAccum += *globals::game::deltaTime; + } + + // Update constant buffer + GlareCB cbData = { + .Threshold = exp2(settings.ThresholdEV - 3.0f), + .Intensity = settings.Intensity, + .ScatterStrength = settings.ScatterStrength, + .ApertureMode = (uint)settings.ApertureMode, + .ApertureBlades = settings.ApertureBlades, + .ApertureRotation = settings.ApertureRotation * 3.14159265f / 180.f, + .AdaptSpeed = settings.AdaptSpeed, + .DeltaTime = *globals::game::deltaTime, + .FFTResolution = currentFFTResolution, + .PaddingRatio = settings.PaddingRatio, + .ScreenWidth = (float)texOutput->desc.Width, + .ScreenHeight = (float)texOutput->desc.Height, + .ChannelIndex = 0, + .FresnelExponent = settings.FresnelExponent, + .ChromaticSpread = settings.ChromaticSpread, + .ApertureSize = 1.0f / std::max(settings.FStop, 1.0f), + .PSFSharpness = settings.PSFSharpness, + .PSFNoiseFloor = settings.PSFNoiseFloor, + .EnableEyelashes = settings.EnableEyelashes ? 1u : 0u, + .EyelashCurvature = settings.EyelashCurvature, + // Eye mode + .EyelashCount = (uint)settings.EyelashCount, + .EyelashLength = settings.EyelashLength, + .ParticleCount = (uint)settings.ParticleCount, + .ParticleSize = settings.ParticleSize, + .GratingCount = (uint)settings.GratingCount, + .GratingStrength = settings.GratingStrength, + .TearFilmStrength = settings.TearFilmStrength, + .TearFilmSpeed = settings.TearFilmSpeed, + .TearFilmComplexity = (uint)settings.TearFilmComplexity, + .TearFilmTime = tearFilmTimeAccum, + .SutureBranches = (uint)settings.SutureBranches, + .SutureStrength = settings.SutureStrength, + .SutureWidth = settings.SutureWidth, + .StarburstCount = (uint)settings.StarburstCount, + .StarburstStrength = settings.StarburstStrength, + .StarburstIrregularity = settings.StarburstIrregularity, + // Lens mode + .DustCount = (uint)settings.DustCount, + .DustSize = settings.DustSize, + .BladeRoughnessFreq = (uint)settings.BladeRoughnessFreq, + .BladeRoughnessAmp = settings.BladeRoughnessAmp, + .ScratchCount = (uint)settings.ScratchCount, + .ScratchOpacity = settings.ScratchOpacity, + .ScratchLength = settings.ScratchLength, + .ScratchWidth = settings.ScratchWidth, + .SphericalAberration = settings.SphericalAberration, + .UseAP1 = (globals::features::linearLighting.settings.enableACEScg && globals::features::linearLighting.settings.enableLinearLighting) ? 1u : 0u, + .KernelScale = settings.KernelScale, + }; + glareCB->Update(cbData); + + ID3D11Buffer* cb = glareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + + // ========== Step 1: Regenerate PSF if parameters changed ========== + if (NeedsPSFRegeneration()) { + globals::profiler->BeginPass("PostProcessing::PhysicalGlare::PSF"); + GeneratePSF(); + + // Re-update CB because GeneratePSF() overwrites it with DeltaTime=0 + glareCB->Update(cbData); + cb = glareCB->CB(); + context->CSSetConstantBuffers(1, 1, &cb); + globals::profiler->EndPass(); + } + + // ========== Step 2: Threshold + downsample scene into FFT textures ========== + globals::profiler->BeginPass("PostProcessing::PhysicalGlare::FFT"); + { + // We write R, G, B channels into texFFT[0..2][0] + ID3D11ShaderResourceView* srv = inout_tex.srv; + std::array uavs = { + texFFT[0][0]->uav.get(), + texFFT[1][0]->uav.get(), + texFFT[2][0]->uav.get() + }; + + context->CSSetShaderResources(0, 1, &srv); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(thresholdCS.get(), nullptr, 0); + context->Dispatch((currentFFTResolution + 7) >> 3, (currentFFTResolution + 7) >> 3, 1); + + srv = nullptr; + uavs.fill(nullptr); + context->CSSetShaderResources(0, 1, &srv); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + } + + // ========== Step 3: Forward FFT on scene (per channel) ========== + for (int ch = 0; ch < 3; ch++) { + // Row FFT: texFFT[ch][0] -> texFFT[ch][1] + DispatchFFT(fftRowCS.get(), texFFT[ch][0].get(), texFFT[ch][1].get(), currentFFTResolution); + // Col FFT: texFFT[ch][1] -> texFFT[ch][0] + DispatchFFT(fftColCS.get(), texFFT[ch][1].get(), texFFT[ch][0].get(), currentFFTResolution); + } + + // ========== Step 4: Frequency-domain multiply (scene * PSF) ========== + { + // Input: texFFT[ch][0] (scene FFT), texPSF_FFT[ch] + // Output: texFFT[ch][1] + std::array srvs = { nullptr, nullptr }; + std::array uavs = { nullptr }; + + for (int ch = 0; ch < 3; ch++) { + srvs[0] = texFFT[ch][0]->srv.get(); + srvs[1] = texPSF_FFT[ch]->srv.get(); + uavs[0] = texFFT[ch][1]->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(multiplyCS.get(), nullptr, 0); + context->Dispatch((currentFFTResolution + 7) >> 3, (currentFFTResolution + 7) >> 3, 1); + + srvs.fill(nullptr); + uavs.fill(nullptr); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + } + } + + // ========== Step 5: Inverse FFT (per channel) ========== + for (int ch = 0; ch < 3; ch++) { + // Row IFFT: texFFT[ch][1] -> texFFT[ch][0] + DispatchFFT(fftRowInvCS.get(), texFFT[ch][1].get(), texFFT[ch][0].get(), currentFFTResolution); + // Col IFFT: texFFT[ch][0] -> texFFT[ch][1] + DispatchFFT(fftColInvCS.get(), texFFT[ch][0].get(), texFFT[ch][1].get(), currentFFTResolution); + } + globals::profiler->EndPass(); + + // ========== Step 6: Composite (upsample + add to scene) ========== + globals::profiler->BeginPass("PostProcessing::PhysicalGlare::Composite"); + { + // t0 = scene, t1/t2/t3 = IFFT result R/G/B (texFFT[ch][1]), + // u0 = output + std::array srvs = { + inout_tex.srv, + texFFT[0][1]->srv.get(), + texFFT[1][1]->srv.get(), + texFFT[2][1]->srv.get(), + }; + std::array uavs = { + texOutput->uav.get(), + }; + ID3D11SamplerState* sampler = linearSampler.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetSamplers(0, 1, &sampler); + context->CSSetShader(compositeCS.get(), nullptr, 0); + + context->Dispatch(((uint)texOutput->desc.Width + 7) >> 3, ((uint)texOutput->desc.Height + 7) >> 3, 1); + + srvs.fill(nullptr); + uavs.fill(nullptr); + sampler = nullptr; + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetSamplers(0, 1, &sampler); + } + + // Cleanup + cb = nullptr; + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetShader(nullptr, nullptr, 0); + globals::profiler->EndPass(); + + state->EndPerfEvent(); +} diff --git a/src/Features/PostProcessing/PhysicalGlare.h b/src/Features/PostProcessing/PhysicalGlare.h new file mode 100644 index 0000000000..ad46483d15 --- /dev/null +++ b/src/Features/PostProcessing/PhysicalGlare.h @@ -0,0 +1,275 @@ +#pragma once + +// Physical Glare — Community Shaders / Post Processing +// Author: Jiaye, 2026 +// +// Physically-based glare via FFT convolution with a wavelength-dependent +// point spread function (PSF). Supports dual aperture modes (eye/lens) +// with chromatic dispersion, wavefront aberrations, and anatomical / +// mechanical optical features. +// +// Core pipeline inspired by: +// [1] Delavennat, J. (2021). Physically-based Real-time Glare. +// Master's thesis (LIU-ITN-TEK-A--21/068-SE), Linköping University. +// https://www.diva-portal.org/smash/record.jsf?pid=diva2:1629565 +// [2] Ritschel, T., Eisemann, E., Ha, I., Kim, J. D. K., & Seidel, H.-P. +// (2009). Temporal Glare: Real-Time Dynamic Simulation of the +// Scattering in the Human Eye. Computer Graphics Forum 28(2), 183-192. +// +// Extensions beyond [1] and [2]: +// - Dual aperture mode: camera lens (N-polygon) and anatomical eye (pupil) +// - Seidel spherical aberration (r^4 wavefront error) +// - Tear film phase harmonics with temporal animation +// - Crystalline lens suture lines (Y-pattern phase modulation) +// - Fiber cell starburst (radial phase grating) +// - Lens-mode features: dust particles, blade roughness, surface scratches +// - CIE 1931 spectral weighting with AP1/ACEScg wide-gamut support +// - Energy-conserving composite (bright subtraction + glare redistribution) +// - Catmull-Rom bicubic upsampling from FFT resolution +// - Configurable zero-padding ratio for wrap-around control + +#include "Buffer.h" +#include "PostProcessFeature.h" + +struct PhysicalGlare : public PostProcessFeature +{ + virtual inline std::string GetType() const override { return "Physical Glare"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.physical_glare.name", "Physical Glare"); } + virtual inline std::string GetDesc() const override { return T("feature.post_processing.physical_glare.description", "Physically based glare from aperture diffraction and ocular scattering. FFT convolution with a wavelength-dependent PSF, dual eye/lens modes, wavefront aberrations, and wide-gamut chromatic dispersion."); } + virtual bool WritesToMainTexture() const override { return false; } + + TextureInfo GetGlareOutput() const { return { texOutput->resource.get(), texOutput->srv.get() }; } + + static constexpr uint FFT_MIN = 128; + static constexpr uint FFT_MAX = 1024; + + struct Settings + { + // --- Core --- + float ThresholdEV = 6.0f; // Brightness threshold in EV100 (exp2(EV100 - 3) → linear luminance). Paper: 0.9 linear ≈ 2.85 EV100. + float Intensity = 0.25f; + int ApertureMode = 0; // 0 = Lens (N-polygon), 1 = Pupil (circle) + int ApertureBlades = 6; + float ApertureRotation = 0.f; + float ScatterStrength = 1.f; + float AdaptSpeed = 3.f; + int FFTResolution = 512; + float FresnelExponent = 30.f; + float ChromaticSpread = 1.f; + float FStop = 2.8f; // F-number (e.g. F2.8). ApertureSize = 1.0 / FStop. + float SphericalAberration = 0.f; // Seidel r^4 wavefront error from lens curvature + float KernelScale = 1.0f; // Scale of the convolution kernel (0.01-1.0). Smaller = more concentrated glare. + + // --- PSF shaping --- + float PSFSharpness = 0.45f; // pow() exponent (paper Table 3.9: 0.45). Higher = concentrated. + float PSFNoiseFloor = 0.001f; // noise floor (paper: 0.001). Higher = cleaner wings. + float PaddingRatio = 0.1f; // Zero-padding per side. 0.25=paper(50% effective), 0.1=80%, 0=100%. + + // --- Eye mode --- + bool EnableEyelashes = false; + int EyelashCount = 40; + float EyelashLength = 0.4f; + float EyelashCurvature = 0.3f; + int ParticleCount = 200; + float ParticleSize = 1.5f; + int GratingCount = 200; + float GratingStrength = 0.5f; + float TearFilmStrength = 0.f; + float TearFilmSpeed = 2.f; + int TearFilmComplexity = 8; + int SutureBranches = 3; + float SutureStrength = 0.5f; + float SutureWidth = 2.f; + int StarburstCount = 48; + float StarburstStrength = 0.8f; + float StarburstIrregularity = 0.3f; + + // --- Lens mode --- + int DustCount = 100; + float DustSize = 1.5f; + int BladeRoughnessFreq = 20; + float BladeRoughnessAmp = 0.3f; + int ScratchCount = 5; + float ScratchOpacity = 0.3f; + float ScratchLength = 0.8f; + float ScratchWidth = 1.5f; + } settings; + + struct alignas(16) GlareCB + { + // --- Row 0: Core controls --- + float Threshold; + float Intensity; + float ScatterStrength; + uint ApertureMode; + + // --- Row 1: Geometry + temporal --- + int ApertureBlades; + float ApertureRotation; + float AdaptSpeed; + float DeltaTime; + + // --- Row 2: Resolution + padding --- + uint FFTResolution; + float PaddingRatio; + float ScreenWidth; + float ScreenHeight; + + // --- Row 3: Channel + optical --- + uint ChannelIndex; + float FresnelExponent; + float ChromaticSpread; + float ApertureSize; + + // --- Row 4: PSF shaping + eyelash UV bending --- + float PSFSharpness; + float PSFNoiseFloor; + uint EnableEyelashes; + float EyelashCurvature; + + // --- (threshold/psf/fft/multiply/composite read up to here) --- + + // --- Row 5: Eye - eyelash geometry + scatter particles --- + + uint EyelashCount; + float EyelashLength; + uint ParticleCount; + float ParticleSize; + + // --- Row 6: Eye - gratings + tear film --- + uint GratingCount; + float GratingStrength; + float TearFilmStrength; + float TearFilmSpeed; + + // --- Row 7: Eye - tear film cont. + sutures --- + uint TearFilmComplexity; + float TearFilmTime; + uint SutureBranches; + float SutureStrength; + + // --- Row 8: Eye - suture width + starburst --- + float SutureWidth; + uint StarburstCount; + float StarburstStrength; + float StarburstIrregularity; + + // --- Row 9: Lens - dust + blade roughness --- + uint DustCount; + float DustSize; + uint BladeRoughnessFreq; + float BladeRoughnessAmp; + + // --- Row 10: Lens - scratches --- + uint ScratchCount; + float ScratchOpacity; + float ScratchLength; + float ScratchWidth; + + // --- Row 11: Additional optics --- + float SphericalAberration; + uint UseAP1; + float KernelScale; + float _pad11; + }; + eastl::unique_ptr glareCB = nullptr; + + // FFT work textures - RG32F (real, imaginary) per channel, ping-pong pair + eastl::unique_ptr texFFT[3][2] = {}; // [R/G/B][ping/pong] + + // PSF FFT cache (regenerated only when parameters change) + eastl::unique_ptr texPSF_FFT[3] = {}; // [R/G/B] + + // Glare result (FFT resolution) + eastl::unique_ptr texGlareResult = nullptr; + eastl::unique_ptr texGlarePrev = nullptr; + + // Full-resolution output + eastl::unique_ptr texOutput = nullptr; + + // Sampler for bilinear upsampling + winrt::com_ptr linearSampler = nullptr; + + // Wrap-mode sampler for chromatic blur UV scaling + winrt::com_ptr wrapSampler = nullptr; + + // Compute shaders + winrt::com_ptr thresholdCS = nullptr; + winrt::com_ptr apertureCS = nullptr; + winrt::com_ptr psfCS = nullptr; + winrt::com_ptr fftRowCS = nullptr; + winrt::com_ptr fftColCS = nullptr; + winrt::com_ptr fftRowInvCS = nullptr; + winrt::com_ptr fftColInvCS = nullptr; + winrt::com_ptr multiplyCS = nullptr; + winrt::com_ptr compositeCS = nullptr; + + // PSF parameter cache - regenerate only when changed + struct PSFParams + { + int ApertureMode = 0; + int ApertureBlades = 0; + float ApertureRotation = 0.f; + float ScatterStrength = 0.f; + int FFTResolution = 0; + float FresnelExponent = 0.f; + float ChromaticSpread = 0.f; + float FStop = 0.f; + float PSFSharpness = 0.f; + float PSFNoiseFloor = 0.f; + // Eye mode + bool EnableEyelashes = false; + int EyelashCount = 0; + float EyelashLength = 0.f; + float EyelashCurvature = 0.f; + int ParticleCount = 0; + float ParticleSize = 0.f; + int GratingCount = 0; + float GratingStrength = 0.f; + float TearFilmStrength = 0.f; + float TearFilmSpeed = 0.f; + int TearFilmComplexity = 0; + int SutureBranches = 0; + float SutureStrength = 0.f; + float SutureWidth = 0.f; + int StarburstCount = 0; + float StarburstStrength = 0.f; + float StarburstIrregularity = 0.f; + // Lens mode + int DustCount = 0; + float DustSize = 0.f; + int BladeRoughnessFreq = 0; + float BladeRoughnessAmp = 0.f; + int ScratchCount = 0; + float ScratchOpacity = 0.f; + float ScratchLength = 0.f; + float ScratchWidth = 0.f; + float SphericalAberration = 0.f; + bool UseAP1 = false; + float KernelScale = 0.f; + } cachedPSFParams; + bool psfDirty = true; + float tearFilmTimeAccum = 0.f; + + uint currentFFTResolution = 256; + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + void CreateFFTTextures(uint resolution); + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json&) override; + virtual void SaveSettings(json&) override; + + virtual void DrawSettings() override; + + virtual void Draw(TextureInfo&) override; + + virtual inline void Reset() override { psfDirty = true; } + +private: + void DispatchFFT(ID3D11ComputeShader* shader, Texture2D* input, Texture2D* output, uint resolution); + void GeneratePSF(); + bool NeedsPSFRegeneration() const; +}; diff --git a/src/Features/PostProcessing/PostProcessFeature.cpp b/src/Features/PostProcessing/PostProcessFeature.cpp new file mode 100644 index 0000000000..68f8db51a8 --- /dev/null +++ b/src/Features/PostProcessing/PostProcessFeature.cpp @@ -0,0 +1 @@ +#include "PostProcessFeature.h" diff --git a/src/Features/PostProcessing/PostProcessFeature.h b/src/Features/PostProcessing/PostProcessFeature.h new file mode 100644 index 0000000000..56f64f6705 --- /dev/null +++ b/src/Features/PostProcessing/PostProcessFeature.h @@ -0,0 +1,51 @@ +#pragma once + +#include "Feature.h" + +struct PostProcessing; + +struct PostProcessFeature +{ + virtual ~PostProcessFeature() = default; + + bool enabled = true; + PostProcessing* owner = nullptr; + + virtual std::string GetType() const = 0; + virtual std::string GetDisplayName() const { return GetType(); } + std::string name; + virtual std::string GetDesc() const = 0; + virtual bool DrawBeforeUpscaling() const { return false; } + virtual bool DrawAfterColorGrading() const { return false; } + virtual bool DisableInMainLoadingMenu() const { return false; } + + /// Whether this feature is visible in the menu. Hidden features (e.g. composite passes) return false. + virtual bool IsVisible() const { return true; } + + /// Whether this feature's enabled state is automatically managed based on other features. + virtual bool IsAutoEnabled() const { return false; } + + /// Called each frame for auto-enabled features to update their enabled state. + virtual void UpdateAutoEnabled() {} + + /// Whether this feature writes its result back to the main pipeline texture. + /// If false, the feature performs internal work but does not replace inout_tex. + virtual bool WritesToMainTexture() const { return true; } + + virtual inline void SetupResources() = 0; + virtual void ClearShaderCache() = 0; + virtual void RestoreDefaultSettings() = 0; + + virtual void LoadSettings(json& o_json) = 0; + virtual void SaveSettings(json& o_json) = 0; + virtual void DrawSettings() = 0; + + struct TextureInfo + { + ID3D11Texture2D* tex = nullptr; + ID3D11ShaderResourceView* srv = nullptr; + }; + virtual void Draw(TextureInfo& inout_tex) = 0; // read from last pass, do the thing, and replace it with output texture + + virtual inline void Reset() {}; +}; diff --git a/src/Features/PostProcessing/Vignette.cpp b/src/Features/PostProcessing/Vignette.cpp new file mode 100644 index 0000000000..2d0b020c0a --- /dev/null +++ b/src/Features/PostProcessing/Vignette.cpp @@ -0,0 +1,158 @@ +#include "Vignette.h" + +#include "I18n/I18n.h" +#include "State.h" +#include "Util.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Vignette::Settings, + FocalLength, + Power) + +void Vignette::DrawSettings() +{ + ImGui::SliderFloat(T("feature.post_processing.vignette.focal_length", "Focal Length"), &settings.FocalLength, 0.1f, 2.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.vignette.the_focal_length_of_the_lens_relative_to", "The focal length of the lens, relative to image width.")); + + ImGui::SliderFloat(T("feature.post_processing.vignette.anamorphic_squeeze", "Anamorphic Squeeze"), &settings.Anamorphism, 0.1f, 1.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text(T("feature.post_processing.vignette.how_flat_the_vignette_looks_simulating_anamorphic_lens", "How flat the vignette looks, simulating anamorphic lens.")); + + ImGui::SliderFloat(T("feature.post_processing.vignette.power", "Power"), &settings.Power, 0.f, 4.f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + T("feature.post_processing.vignette.the_natural_vignetting_of_a_camera_follows_the", + "The natural vignetting of a camera follows the fourth law, where the vignette is proportional to the fourth power of the incident angle. " + "The actual power in a camera is usually lower due to designed compensation.")); +} + +void Vignette::RestoreDefaultSettings() +{ + settings = {}; +} + +void Vignette::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void Vignette::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void Vignette::SetupResources() +{ + auto renderer = globals::game::renderer; + + logger::debug("Creating buffers..."); + { + vignetteCB = eastl::make_unique(ConstantBufferDesc()); + } + + logger::debug("Creating 2D textures..."); + { + auto gameTexMainCopy = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + + D3D11_TEXTURE2D_DESC texDesc; + gameTexMainCopy.texture->GetDesc(&texDesc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + texDesc.MipLevels = srvDesc.Texture2D.MipLevels = 1; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + texDesc.MiscFlags = 0; + + texOutput = eastl::make_unique(texDesc); + texOutput->CreateSRV(srvDesc); + texOutput->CreateUAV(uavDesc); + } + + CompileComputeShaders(); +} + +void Vignette::ClearShaderCache() +{ + const auto shaderPtrs = std::array{ + &vignetteCS + }; + + for (auto shader : shaderPtrs) + if ((*shader)) { + (*shader)->Release(); + shader->detach(); + } + + CompileComputeShaders(); +} + +void Vignette::CompileComputeShaders() +{ + struct ShaderCompileInfo + { + winrt::com_ptr* programPtr; + std::string_view filename; + std::vector> defines = {}; + std::string entry = "main"; + }; + + std::vector + shaderInfos = { + { &vignetteCS, "vignette.cs.hlsl" }, + }; + + for (auto& info : shaderInfos) { + auto path = std::filesystem::path("Data\\Shaders\\PostProcessing\\Vignette") / info.filename; + if (auto rawPtr = reinterpret_cast(Util::CompileShader(path.c_str(), info.defines, "cs_5_0", info.entry.c_str()))) + info.programPtr->attach(rawPtr); + } +} + +void Vignette::Draw(TextureInfo& inout_tex) +{ + globals::profiler->BeginPass("PostProcessing::Vignette"); + auto context = globals::d3d::context; + + float2 res = { (float)texOutput->desc.Width, (float)texOutput->desc.Height }; + res = Util::ConvertToDynamic(res); + VignetteCB data = { + .settings = settings, + .AspectRatio = res.y / res.x / settings.Anamorphism, + .RcpDynRes = float2(1.f) / res + }; + vignetteCB->Update(data); + + ID3D11ShaderResourceView* srv = inout_tex.srv; + ID3D11UnorderedAccessView* uav = texOutput->uav.get(); + ID3D11Buffer* cb = vignetteCB->CB(); + + context->CSSetConstantBuffers(1, 1, &cb); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShaderResources(0, 1, &srv); + context->CSSetShader(vignetteCS.get(), nullptr, 0); + + context->Dispatch(((uint)res.x + 7) >> 3, ((uint)res.y + 7) >> 3, 1); + + // clean up + srv = nullptr; + uav = nullptr; + cb = nullptr; + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + context->CSSetShaderResources(0, 1, &srv); + context->CSSetConstantBuffers(0, 1, &cb); + context->CSSetShader(nullptr, nullptr, 0); + + inout_tex = { texOutput->resource.get(), texOutput->srv.get() }; + globals::profiler->EndPass(); +} diff --git a/src/Features/PostProcessing/Vignette.h b/src/Features/PostProcessing/Vignette.h new file mode 100644 index 0000000000..2379a11c98 --- /dev/null +++ b/src/Features/PostProcessing/Vignette.h @@ -0,0 +1,44 @@ +#pragma once + +#include "Buffer.h" +#include "PostProcessFeature.h" + +struct Vignette : public PostProcessFeature +{ + virtual inline std::string GetType() const override { return "Vignette"; } + virtual inline std::string GetDisplayName() const override { return T("feature.post_processing.vignette.name", "Vignette"); } + virtual inline std::string GetDesc() const override { return T("feature.post_processing.vignette.description", "Simulates natural vignetting caused by angled rays hitting the film or sensor array."); } + + struct Settings + { + float FocalLength = 1.f; + float Anamorphism = 1.f; + float Power = 3.f; + } settings; + + struct alignas(16) VignetteCB + { + Settings settings; + float AspectRatio; + + float2 RcpDynRes; + float pad[2]; + }; + eastl::unique_ptr vignetteCB = nullptr; + + eastl::unique_ptr texOutput = nullptr; + + winrt::com_ptr vignetteCS = nullptr; + + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + void CompileComputeShaders(); + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json&) override; + virtual void SaveSettings(json&) override; + + virtual void DrawSettings() override; + + virtual void Draw(TextureInfo&) override; +}; diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 44d703b973..b06292dbce 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -17,6 +17,8 @@ #include #include +#include "Features/PostProcessing.h" + #define I18N_KEY_PREFIX "feature.upscaling." NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( @@ -1678,11 +1680,19 @@ void Upscaling::MenuManagerDrawInterfaceStartHook::thunk(int64_t a1) void Upscaling::Main_PostProcessing::thunk(RE::ImageSpaceManager* a_this, uint32_t a3, RE::RENDER_TARGET a_target, void* a_4, bool a_5) { + auto& postProcessing = globals::features::postProcessing; + if (postProcessing.loaded) { + postProcessing.DrawBeforeUpscaling(); + } + auto& upscaling = globals::features::upscaling; auto upscaleMethod = upscaling.GetUpscaleMethod(); - if (upscaling.ShouldUseFrameGenerationThisFrame()) + if (upscaling.ShouldUseFrameGenerationThisFrame()) { + if (postProcessing.loaded) + postProcessing.ClearBorderMotionVectorsForFrameGen(); upscaling.CopySharedD3D12Resources(); + } if (upscaleMethod != UpscaleMethod::kNONE && upscaleMethod != UpscaleMethod::kTAA) upscaling.PerformUpscaling(); diff --git a/src/Globals.cpp b/src/Globals.cpp index 24016b8e0c..9be528e09d 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -1,6 +1,7 @@ #include "Globals.h" #include "Deferred.h" +#include "Features/CSEditor.h" #include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" #include "Features/Effects11.h" @@ -19,6 +20,7 @@ #include "Features/LightLimitFix.h" #include "Features/LinearLighting.h" #include "Features/PerformanceOverlay.h" +#include "Features/PostProcessing.h" #include "Features/RemoteControl.h" #include "Features/RenderDoc.h" #include "Features/ScreenSpaceGI.h" @@ -37,7 +39,6 @@ #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" -#include "Features/CSEditor.h" #include "Features/WetnessEffects.h" #include "Menu.h" #include "SceneSettingsManager.h" @@ -97,6 +98,7 @@ namespace globals ExponentialHeightFog exponentialHeightFog{}; TruePBR truePBR{}; Skin skin{}; + PostProcessing postProcessing{}; namespace llf { diff --git a/src/Globals.h b/src/Globals.h index 94419d8bbc..9b03e890cf 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -37,6 +37,7 @@ struct CSEditor; struct Effects11; struct ExponentialHeightFog; struct HDRDisplay; +struct PostProcessing; struct ScreenshotFeature; struct Skin; @@ -130,6 +131,7 @@ namespace globals extern ExponentialHeightFog exponentialHeightFog; extern TruePBR truePBR; extern Skin skin; + extern PostProcessing postProcessing; } @@ -260,4 +262,4 @@ namespace globals * @param a_context The D3D11 device context to hook. */ void InstallD3DHooks(ID3D11DeviceContext* a_context); -} \ No newline at end of file +} diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 0b8bf74002..9f5d4168eb 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -13,8 +13,9 @@ #include "Features/Effects11.h" #include "Features/HDRDisplay.h" #include "Features/InteriorSun.h" -#include "Features/ScreenshotFeature.h" #include "Features/LightLimitFix.h" +#include "Features/PostProcessing.h" +#include "Features/ScreenshotFeature.h" #include "Features/Skin.h" #include "Features/SkySync.h" #include "Features/Upscaling.h" @@ -289,11 +290,22 @@ namespace PostProcessingExtensions { static void thunk(RE::ImageSpaceManager* a1, RE::ImageSpaceEffect* a2, uint32_t a3, uint32_t a4, RE::ImageSpaceShaderParam* a5) { - if (!globals::state->IsMainOrLoadingMenuOpen() && - globals::state->HandlePostProcessing( - static_cast(a3), - static_cast(a4))) + auto* state = globals::state; + const auto input = static_cast(a3); + const auto output = static_cast(a4); + + // Effects11 replaces the pass outright; when it does, the vanilla call is skipped + // and HandlePostProcessing fixes up the render-target state the pass would have set. + if (state->HandlePostProcessing(input, output)) return; + + // Post Processing runs its pipeline into kMAIN/kMAIN_COPY, then lets the vanilla + // pass run so ISHDR can take its POSTPROCESS passthrough branch. It also runs when + // the vanilla tonemap owns the frame, since most of its effects are pre-tonemap. + auto& postProcessing = globals::features::postProcessing; + if (postProcessing.loaded) + postProcessing.PreProcess(input); + func(a1, a2, a3, a4, a5); } static inline REL::Relocation func; @@ -476,8 +488,8 @@ struct BSInputDeviceManager_PollInputDevices if (*a_events) { if (auto device = (*a_events)->GetDevice()) { - // Block all devices except gamepad when menu is open - blockedDevice = (device != RE::INPUT_DEVICES::INPUT_DEVICE::kGamepad); + // Block all devices except gamepad when menu is open + blockedDevice = (device != RE::INPUT_DEVICES::INPUT_DEVICE::kGamepad); } } } diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 92341e1942..45e8b97b19 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -5,8 +5,10 @@ #include "BackgroundBlur.h" #include "Fonts.h" #include "I18n/I18n.h" +#include "IconsFontAwesome5.h" #include +#include #include #include #include @@ -147,6 +149,41 @@ namespace } return nullptr; } + + bool MergeFontAwesome5(ImFontAtlas* atlas, ImFont* dstFont, float fontSize) + { + if (!atlas || !dstFont) { + return false; + } + + auto dataIconPath = Util::PathHelpers::GetDataPath() / "Interface" / "CommunityShaders" / "Fonts" / FONT_ICON_FILE_NAME_FAS; + std::array iconPathCandidates = { + dataIconPath, + std::filesystem::path("Data\\Interface\\CommunityShaders\\Fonts") / FONT_ICON_FILE_NAME_FAS + }; + + static const ImWchar iconsRanges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 }; + + ImFontConfig mergeConfig; + mergeConfig.MergeMode = true; + mergeConfig.PixelSnapH = true; + mergeConfig.DstFont = dstFont; + + for (const auto& iconPath : iconPathCandidates) { + if (!std::filesystem::exists(iconPath)) { + continue; + } + + if (atlas->AddFontFromFileTTF(iconPath.string().c_str(), fontSize, &mergeConfig, iconsRanges)) { + return true; + } + } + + logger::warn("Failed to merge Font Awesome 5 icons. Tried: {}; {}", + iconPathCandidates[0].string(), + iconPathCandidates[1].string()); + return false; + } } // Static UI helper methods @@ -385,6 +422,7 @@ bool ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) ImFontConfig cfg = font_config; auto* font = io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), roundedSize, &cfg); if (font) { + MergeFontAwesome5(io.Fonts, font, roundedSize); atlasCache.emplace(cacheKey, font); loadedFont = font; } @@ -415,11 +453,13 @@ bool ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) ImFontConfig cfg = font_config; bodyFont = io.Fonts->AddFontFromFileTTF(defaultPath.string().c_str(), roundedBodySize, &cfg); if (bodyFont) { + MergeFontAwesome5(io.Fonts, bodyFont, roundedBodySize); atlasCache.emplace(cacheKey, bodyFont); } } if (!bodyFont) { bodyFont = io.Fonts->AddFontDefault(); + MergeFontAwesome5(io.Fonts, bodyFont, roundedBodySize); } menu.loadedFontRoles[bodyIndex] = bodyFont; @@ -563,6 +603,7 @@ bool ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) baseFont = io.Fonts->AddFontDefault(); } + MergeFontAwesome5(io.Fonts, baseFont, roleSize); tryMergeGlyphSet(baseFont, roleSize, primaryCJKFontPaths, primaryGlyphRanges, std::format("active locale '{}' glyphs", locale), role); for (const auto& merge : supplementalGlyphMerges) { tryMergeGlyphSet(baseFont, roleSize, merge.fontPaths, merge.glyphRanges.Data, std::format("locale display '{}'", merge.locale), role); @@ -597,6 +638,7 @@ bool ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) io.Fonts->Clear(); MenuFonts::InvalidatePreviewFonts(); ImFont* fallbackFont = io.Fonts->AddFontDefault(); + MergeFontAwesome5(io.Fonts, fallbackFont, fontSize); if (fallbackFont && io.Fonts->Build()) { menu.loadedFontRoles.fill(fallbackFont); io.FontDefault = fallbackFont; @@ -631,6 +673,7 @@ bool ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) io.Fonts->Clear(); MenuFonts::InvalidatePreviewFonts(); ImFont* fallbackFont = io.Fonts->AddFontDefault(); + MergeFontAwesome5(io.Fonts, fallbackFont, fontSize); bool recoverySucceeded = false; if (fallbackFont && io.Fonts->Build()) { diff --git a/src/State.cpp b/src/State.cpp index d3b5d0709e..3da852b28a 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -10,13 +10,13 @@ #include "Features/CloudShadows.h" #include "Features/Effects11.h" #include "Features/ExponentialHeightFog.h" -#include "Features/SkySync.h" #include "Features/HDRDisplay.h" #include "Features/InteriorSun.h" #include "Features/PerformanceOverlay.h" +#include "Features/PostProcessing.h" #include "Features/Skin.h" -#include "Features/Skylighting.h" #include "Features/SkySync.h" +#include "Features/Skylighting.h" #include "Features/TerrainBlending.h" #include "Features/TerrainHelper.h" #include "Features/Upscaling.h" @@ -185,10 +185,42 @@ void State::Debug() } } -bool State::HandlePostProcessing(RE::RENDER_TARGET a_input, RE::RENDER_TARGET a_output) +State::TonemapOwner State::GetTonemapOwner() { + static Util::FrameChecker tonemapOwnerFrameChecker; + static TonemapOwner cachedOwner = TonemapOwner::kVanilla; + + if (!tonemapOwnerFrameChecker.IsNewFrame()) + return cachedOwner; + auto& effects11 = globals::features::effects11; - if (!effects11.loaded || !effects11.HandleTonemapRender(a_input, a_output)) + auto& postProcessing = globals::features::postProcessing; + + // Effects11 does not run over the main menu or loading screen: those composite UI and + // scene into one buffer, so an ENB preset would grade the menu itself. It must not claim + // ownership there either, or Post Processing would lose its tonemap to a pass that never + // renders. + const bool effects11CanRender = !IsMainOrLoadingMenuOpen(); + + // Effects11 wins ties: its effects form a complete ENB preset (tonemap, bloom, lens, + // adaptation) that looks wrong when only partially applied, whereas Post Processing + // degrades gracefully to the vanilla tonemap. + if (effects11.loaded && effects11CanRender && effects11.WantsTonemapOwnership()) + cachedOwner = TonemapOwner::kEffects11; + else if (postProcessing.loaded && postProcessing.WantsTonemapOwnership()) + cachedOwner = TonemapOwner::kPostProcessing; + else + cachedOwner = TonemapOwner::kVanilla; + + return cachedOwner; +} + +bool State::HandlePostProcessing(RE::RENDER_TARGET a_input, RE::RENDER_TARGET a_output) +{ + if (GetTonemapOwner() != TonemapOwner::kEffects11) + return false; + + if (!globals::features::effects11.RenderTonemap(a_input, a_output)) return false; auto renderer = globals::game::renderer; diff --git a/src/State.h b/src/State.h index 93b9eb634e..5d09d0f138 100644 --- a/src/State.h +++ b/src/State.h @@ -62,8 +62,8 @@ class State int drawCalls[RE::BSShader::Type::Total + 1]; // Frame time tracking per shader type (in milliseconds) - float frameTimePerType[RE::BSShader::Type::Total + 1]; ///< Per-type frame time in milliseconds. - float smoothFrameTimePerType[RE::BSShader::Type::Total + 1]; ///< EMA-smoothed per-type frame time in milliseconds. + float frameTimePerType[RE::BSShader::Type::Total + 1]; ///< Per-type frame time in milliseconds. + float smoothFrameTimePerType[RE::BSShader::Type::Total + 1]; ///< EMA-smoothed per-type frame time in milliseconds. // Timing state for per-type frame time tracking using QueryPerformanceCounter LARGE_INTEGER frameTimingFrequency; @@ -87,6 +87,37 @@ class State /** @brief One-time post-D3D setup: creates resources, probes GPU caps, initializes features. */ void Setup(); + /** + * @brief Which feature owns the HDR tonemap pass this frame. + * + * Effects11 and Post Processing both replace the vanilla ISHDR tonemap and cannot + * coexist: Effects11 skips the whole pass, while Post Processing relies on it running + * so ISHDR can take its passthrough branch. Exactly one owner is resolved per frame. + */ + enum class TonemapOwner + { + kVanilla, ///< Vanilla ISHDR tonemap runs unmodified. + kPostProcessing, ///< Post Processing pipeline drives tonemapping. + kEffects11 ///< Effects11 (ENB-compatible) replaces the pass entirely. + }; + + /** + * @brief Resolves the tonemap owner for the current frame (cached once per frame). + * + * Effects11 wins when both features want the pass, because it applies a whole-preset + * look that degrades badly when partially applied. The choice is surfaced in both + * feature UIs so the loser is not silently disabled. + * + * @return The feature owning tonemapping this frame. + */ + TonemapOwner GetTonemapOwner(); + + /** + * @brief Dispatches the HDR tonemap pass to the resolved owner. + * @param a_input Render target holding the scene color to tonemap. + * @param a_output Render target receiving the tonemapped result. + * @return True if the vanilla pass was fully replaced and must not be invoked. + */ bool HandlePostProcessing(RE::RENDER_TARGET a_input, RE::RENDER_TARGET a_output); /** @@ -232,33 +263,33 @@ class State /** * Bitflags describing extra shader-specific properties. */ - + /** * Bitflags describing extra feature-specific properties related to terrain displacement and material models. */ - + /** * Checks whether the main menu or loading menu is cached as open. * @returns true if either the main menu or loading menu is open, false otherwise. */ - + /** * Checks whether the main menu or loading menu is open, querying the UI if provided. * @param ui Pointer to the UI manager; if non-null, performs live menu checks as a fallback. * @returns true if the main menu or loading menu is open, false otherwise. */ - + /** * Updates the shared constant buffer data based on world state and rendering pass. * @param a_inWorld Whether the camera is in world space. * @param a_prepass Whether this is a prepass rendering phase. */ - + /** * Updates sky shader permutation based on the current render pass. * @param a_pass The render pass to inspect. */ - + /** * Checks whether directional shadows are available for the current scene. * @returns true if directional shadows are present, false otherwise. diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 03da3fb30f..d72f0dad71 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -394,6 +394,31 @@ namespace Util }; bool PercentageSlider(const char* label, float* data, float lb = 0.f, float ub = 100.f, const char* format = "%.1f %%"); + + /** + * Draws a 2-4 component float slider. Holding Shift edits all components from the first component. + */ + template + bool ShiftSlider(const char* label, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0) + { + static_assert(Num > 1 && Num < 5); + + if (ImGui::GetIO().KeyShift) { + auto changed = ImGui::SliderFloat(label, v, v_min, v_max, format, flags); + if (changed) + for (int i = 1; i < Num; i++) + v[i] = v[0]; + return changed; + } + + if constexpr (Num == 2) + return ImGui::SliderFloat2(label, v, v_min, v_max, format, flags); + else if constexpr (Num == 3) + return ImGui::SliderFloat3(label, v, v_min, v_max, format, flags); + else + return ImGui::SliderFloat4(label, v, v_min, v_max, format, flags); + } + ImVec2 GetNativeViewportSizeScaled(float scale); // Icon loading functions