// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. // Film Grain post-process shader v1.1 // Martins Upitis (martinsh) devlog-martinsh.blogspot.com // 2013 // // -------------------------- // This work is licensed under a Creative Commons Attribution 3.0 Unported License. // So you are free to share, modify and adapt it for your needs, and even use it for commercial use. // I would also love to hear about a project you are using it. // // Have fun, // Martins // -------------------------- // // Perlin noise shader by toneburst: // http://machinesdontcare.wordpress.com/2009/06/25/3d-perlin-noise-sphere-vertex-shader-sourcecode/ // // Lens flares by John Chapman: //https://john-chapman.github.io/2017/11/05/pseudo-lens-flare.html // #include "./Flax/Common.hlsl" #include "./Flax/Random.hlsl" #include "./Flax/GammaCorrectionCommon.hlsl" #define GB_RADIUS 6 #define GB_KERNEL_SIZE (GB_RADIUS * 2 + 1) #ifndef NO_GRADING_LUT #define NO_GRADING_LUT 0 #endif #ifndef USE_VOLUME_LUT #define USE_VOLUME_LUT 0 #endif META_CB_BEGIN(0, Data) float BloomLimit; float BloomThreshold; float BloomMagnitude; float BloomBlurSigma; float3 VignetteColor; float VignetteShapeFactor; float2 InputSize; float InputAspect; float GrainAmount; float GrainTime; float GrainParticleSize; int Ghosts; float HaloWidth; float HaloIntensity; float Distortion; float GhostDispersal; float LensFlareIntensity; float2 LensInputDistortion; float LensScale; float LensBias; float2 InvInputSize; float ChromaticDistortion; float Time; float Dummy1; float PostExposure; float VignetteIntensity; float LensDirtIntensity; float4 ScreenFadeColor; float4x4 LensFlareStarMat; META_CB_END META_CB_BEGIN(1, GaussianBlurData) float2 Size; float Dummy3; float Dummy4; float4 GaussianBlurCache[GB_KERNEL_SIZE]; // x-weight, y-offset META_CB_END // Film Grain static const float permTexUnit = 1.0 / 256.0; // Perm texture texel-size static const float permTexUnitHalf = 0.5 / 256.0; // Half perm texture texel-size // Input textures Texture2D Input0 : register(t0); Texture2D Input1 : register(t1); Texture2D Input2 : register(t2); Texture2D Input3 : register(t3); Texture2D LensDirt : register(t4); Texture2D LensStar : register(t5); Texture2D LensColor : register(t6); #if USE_VOLUME_LUT Texture3D ColorGradingLUT : register(t7); #else Texture2D ColorGradingLUT : register(t7); #endif static const float LUTSize = 32; half3 ColorLookupTable(half3 linearColor) { // Move from linear color to encoded LUT color space //float3 encodedColor = linearColor; // Default float3 encodedColor = LinearToLog(linearColor + LogToLinear(0)); // Log float3 uvw = encodedColor * ((LUTSize - 1) / LUTSize) + (0.5f / LUTSize); #if USE_VOLUME_LUT half3 color = ColorGradingLUT.Sample(SamplerLinearClamp, uvw).rgb; #else half3 color = SampleUnwrappedTexture3D(ColorGradingLUT, SamplerLinearClamp, uvw, LUTSize).rgb; #endif return color; } // A random texture generator float4 rnmRGBA(in float2 tc, in float time) { float noise = sin(dot(tc + float2(time, time), float2(12.9898, 78.233))) * 43758.5453; float noiseR = frac(noise) * 2.0 - 1.0; float noiseG = frac(noise * 1.2154) * 2.0 - 1.0; float noiseB = frac(noise * 1.3453) * 2.0 - 1.0; float noiseA = frac(noise * 1.3647) * 2.0 - 1.0; return float4(noiseR, noiseG, noiseB, noiseA); } float3 rnmRGB(in float2 tc, in float time) { float noise = sin(dot(tc + float2(time, time), float2(12.9898, 78.233))) * 43758.5453; float noiseR = frac(noise) * 2.0 - 1.0; float noiseG = frac(noise * 1.2154) * 2.0 - 1.0; float noiseB = frac(noise * 1.3453) * 2.0 - 1.0; return float3(noiseR, noiseG, noiseB); } float2 rnmRG(in float2 tc, in float time) { float noise = sin(dot(tc + float2(time, time), float2(12.9898, 78.233))) * 43758.5453; float noiseR = frac(noise) * 2.0 - 1.0; float noiseG = frac(noise * 1.2154) * 2.0 - 1.0; return float2(noiseR, noiseG); } float rnmA(in float2 tc, in float time) { float noise = sin(dot(tc + float2(time, time), float2(12.9898, 78.233))) * 43758.5453; float noiseA = frac(noise * 1.3647) * 2.0 - 1.0; return noiseA; } float pnoise3D(in float3 p, in float time) { // Integer part, scaled so +1 moves permTexUnit texel float3 pi = permTexUnit * floor(p) + permTexUnitHalf; // and offset 1/2 texel to sample texel centers. Fractional part for interpolation float3 pf = frac(p); // Noise contributions from (x=0, y=0), z=0 and z=1 float perm00 = rnmA(pi.xy, time); float3 grad000 = rnmRGB(float2(perm00, pi.z), time) * 4.0 - 1.0; float n000 = dot(grad000, pf); float3 grad001 = rnmRGB(float2(perm00, pi.z + permTexUnit), time) * 4.0 - 1.0; float n001 = dot(grad001, pf - float3(0.0, 0.0, 1.0)); // Noise contributions from (x=0, y=1), z=0 and z=1 float perm01 = rnmA(pi.xy + float2(0.0, permTexUnit), time); float3 grad010 = rnmRGB(float2(perm01, pi.z), time) * 4.0 - 1.0; float n010 = dot(grad010, pf - float3(0.0, 1.0, 0.0)); float3 grad011 = rnmRGB(float2(perm01, pi.z + permTexUnit), time) * 4.0 - 1.0; float n011 = dot(grad011, pf - float3(0.0, 1.0, 1.0)); // Noise contributions from (x=1, y=0), z=0 and z=1 float perm10 = rnmA(pi.xy + float2(permTexUnit, 0.0), time); float3 grad100 = rnmRGB(float2(perm10, pi.z), time) * 4.0 - 1.0; float n100 = dot(grad100, pf - float3(1.0, 0.0, 0.0)); float3 grad101 = rnmRGB(float2(perm10, pi.z + permTexUnit), time) * 4.0 - 1.0; float n101 = dot(grad101, pf - float3(1.0, 0.0, 1.0)); // Noise contributions from (x=1, y=1), z=0 and z=1 float perm11 = rnmA(pi.xy + float2(permTexUnit, permTexUnit), time); float3 grad110 = rnmRGB(float2(perm11, pi.z), time) * 4.0 - 1.0; float n110 = dot(grad110, pf - float3(1.0, 1.0, 0.0)); float3 grad111 = rnmRGB(float2(perm11, pi.z + permTexUnit), time) * 4.0 - 1.0; float n111 = dot(grad111, pf - float3(1.0, 1.0, 1.0)); // Blend contributions along x float4 n_x = lerp(float4(n000, n001, n010, n011), float4(n100, n101, n110, n111), PerlinRamp(pf.x)); // Blend contributions along y float2 n_xy = lerp(n_x.xy, n_x.zw, PerlinRamp(pf.y)); // Blend contributions along z float n_xyz = lerp(n_xy.x, n_xy.y, PerlinRamp(pf.z)); // We're done, return the final noise value return n_xyz; } float pnoise2D(in float2 p, in float time) { // Integer part, scaled so +1 moves permTexUnit texel float2 pi = permTexUnit * floor(p) + permTexUnitHalf; // and offset 1/2 texel to sample texel centers. Fractional part for interpolation float2 pf = frac(p); // Noise contributions from (x=0, y=0) float perm00 = rnmA(pi.xy, time); float2 grad000 = rnmRG(float2(perm00, 0), time) * 4.0 - 1.0; float n000 = dot(grad000, pf); // Noise contributions from (x=0, y=1) float perm01 = rnmA(pi.xy + float2(0.0, permTexUnit), time); float2 grad010 = rnmRG(float2(perm01, 0), time) * 4.0 - 1.0; float n010 = dot(grad010, pf - float2(0.0, 1.0)); // Noise contributions from (x=1, y=0) float perm10 = rnmA(pi.xy + float2(permTexUnit, 0.0), time); float2 grad100 = rnmRG(float2(perm10, 0), time) * 4.0 - 1.0; float n100 = dot(grad100, pf - float2(1.0, 0.0)); // Noise contributions from (x=1, y=1) float perm11 = rnmA(pi.xy + float2(permTexUnit, permTexUnit), time); float2 grad110 = rnmRG(float2(perm11, 0), time) * 4.0 - 1.0; float n110 = dot(grad110, pf - float2(1.0, 1.0)); // Blend contributions along x float2 n_x = lerp(float2(n000, n010), float2(n100, n110), PerlinRamp(pf.x)); // Blend contributions along y float n_xy = lerp(n_x.x, n_x.y, PerlinRamp(pf.y)); // We're done, return the final noise value return n_xy; } // 2d coordinate orientation thing float2 coordRot(in float2 tc, in float angle) { float rotX = ((tc.x * 2.0 - 1.0) * InputAspect * cos(angle)) - ((tc.y * 2.0 - 1.0) * sin(angle)); float rotY = ((tc.y * 2.0 - 1.0) * cos(angle)) + ((tc.x * 2.0 - 1.0) * InputAspect * sin(angle)); rotX = ((rotX / InputAspect) * 0.5 + 0.5); rotY = rotY * 0.5 + 0.5; return float2(rotX, rotY); } // Uses a lower exposure to produce a value suitable for a bloom pass META_PS(true, FEATURE_LEVEL_ES2) float4 PS_Threshold(Quad_VS2PS input) : SV_Target { float4 color = Input0.SampleLevel(SamplerLinearClamp, input.TexCoord, 0); return clamp(color - BloomThreshold, 0, BloomLimit); } // Uses hw bilinear filtering for upscaling or downscaling META_PS(true, FEATURE_LEVEL_ES2) float4 PS_Scale(Quad_VS2PS input) : SV_Target { // TODO: we could use quality switch for bloom effect return Input0.SampleLevel(SamplerLinearClamp, input.TexCoord, 0); /* float3 color; // TODO: use gather for dx11 and dx12?? color = Input0.SampleLevel(SamplerLinearClamp, input.TexCoord, 0, int2( 0, 0)).rgb; color += Input0.SampleLevel(SamplerLinearClamp, input.TexCoord, 0, int2( 0, 1)).rgb; color += Input0.SampleLevel(SamplerLinearClamp, input.TexCoord, 0, int2( 0,-1)).rgb; color += Input0.SampleLevel(SamplerLinearClamp, input.TexCoord, 0, int2(-1, 0)).rgb; color += Input0.SampleLevel(SamplerLinearClamp, input.TexCoord, 0, int2( 1, 0)).rgb; color *= (1.0f / 5.0f); return float4(color, 1); */ } // Horizontal gaussian blur META_PS(true, FEATURE_LEVEL_ES2) float4 PS_GaussainBlurH(Quad_VS2PS input) : SV_Target { float4 color = 0; UNROLL for (int i = 0; i < GB_KERNEL_SIZE; i++) { color += Input0.Sample(SamplerLinearClamp, input.TexCoord + float2(GaussianBlurCache[i].y, 0.0)) * GaussianBlurCache[i].x; } return color; } // Vertical gaussian blur META_PS(true, FEATURE_LEVEL_ES2) float4 PS_GaussainBlurV(Quad_VS2PS input) : SV_Target { float4 color = 0; UNROLL for (int i = 0; i < GB_KERNEL_SIZE; i++) { color += Input0.Sample(SamplerLinearClamp, input.TexCoord + float2(0.0, GaussianBlurCache[i].y)) * GaussianBlurCache[i].x; } return color; } // Generate 'ghosts' for lens flare META_PS(true, FEATURE_LEVEL_ES2) float4 PS_Ghosts(Quad_VS2PS input) : SV_Target { // Temporary data int i = 0; float weight; float2 offset; float2 haloFrac; float3 color; float3 result = 0; // Flip texcoordoords float2 texcoord = input.TexCoord * -1 + float2(1.0, 1.0); // Ahost vector to image centre float2 ghostVec = (float2(0.5, 0.5) - texcoord) * GhostDispersal;// TODO: optimize to MAD instruction float2 ghostVecnNorm = normalize(ghostVec); float2 haloVec = ghostVecnNorm * HaloWidth; // Calculate distortion vector float3 distortion = float3(LensInputDistortion.x, 0.0, LensInputDistortion.y); // Sample 'ghosts' // TODO: use uniform amount of ghosts and unroll loop LOOP for(; i < Ghosts; i++) { // Calculate ghost offset offset = frac(texcoord + ghostVec * (float)i); // Calculate ghost weight weight = pow(1.0 - length(float2(0.5, 0.5) - offset) / length(float2(0.71, 0.6)), 10.0); // Sample distored lens downsampled/threshold texture color = float3( Input3.Sample(SamplerLinearClamp, offset + ghostVecnNorm * distortion.r).r, Input3.Sample(SamplerLinearClamp, offset + ghostVecnNorm * distortion.g).g, Input3.Sample(SamplerLinearClamp, offset + ghostVecnNorm * distortion.b).b); color = clamp(color + LensBias, 0, 10) * (LensScale * weight); // Accumulate color result += color; } // Apply lens color result *= LensColor.Sample(SamplerLinearWrap, float2(length(float2(0.5, 0.5) - texcoord) / length(float2(0.5, 0.5)), 0)).rgb; // Add halo haloFrac = frac(texcoord + haloVec); weight = length(float2(0.5, 0.5) - haloFrac) / length(float2(0.5, 0.5)); weight = pow(1.0 - weight, 5.0) * HaloIntensity; color = float3( Input3.Sample(SamplerLinearClamp, haloFrac + ghostVecnNorm * distortion.r).r, Input3.Sample(SamplerLinearClamp, haloFrac + ghostVecnNorm * distortion.g).g, Input3.Sample(SamplerLinearClamp, haloFrac + ghostVecnNorm * distortion.b).b); result += clamp((color + LensBias) * (LensScale * weight), 0, 8); return float4(result, 1); } float remap(float t, float a, float b) { return clamp((t - a) / (b - a), 0.0, 1.0); } float2 remap(float2 t, float2 a, float2 b) { return clamp((t - a) / (b - a), 0.0, 1.0); } float2 radialdistort(float2 coord, float2 amt) { float2 cc = coord - 0.5; return coord + 2.0 * cc * amt; } float2 distort(float2 uv, float t, float2 min_distort, float2 max_distort) { float2 dist = lerp(min_distort, max_distort, t); float2 cc = uv - 0.5; return uv + 4.0 * cc * dist; } float3 spectrum_offset(float t) { float t0 = 3.0 * t - 1.5; return clamp(float3( -t0, 1.0 - abs(t0), t0), 0.0, 1.0); } float nrand(float2 n) { return frac(sin(dot(n.xy, float2(12.9898, 78.233)))* 43758.5453); } // Applies exposure, color grading and tone mapping to the input. // Combines it with the results of the bloom pass and other postFx. META_PS(true, FEATURE_LEVEL_ES2) META_PERMUTATION_1(NO_GRADING_LUT=1) META_PERMUTATION_1(USE_VOLUME_LUT=1) META_PERMUTATION_1(USE_VOLUME_LUT=0) float4 PS_Composite(Quad_VS2PS input) : SV_Target { float2 uv = input.TexCoord; float3 lensLight = 0; float4 color; // Chromatic Abberation if (ChromaticDistortion > 0) { const float MAX_DIST_PX = 24.0; float max_distort_px = MAX_DIST_PX * ChromaticDistortion; float2 max_distort = InvInputSize * max_distort_px; float2 min_distort = 0.5 * max_distort; float2 oversiz = distort(float2(1.0, 1.0), 1.0, min_distort, max_distort); uv = remap(uv, 1.0 - oversiz, oversiz); int iterations = (int)lerp(3, 10, ChromaticDistortion); float stepsiz = 1.0 / (float(iterations) - 1.0); float rnd = nrand(uv + Time); float t = rnd * stepsiz; float4 sumcol = 0; float4 sumw = 0; for (int i = 0; i < iterations; i++) { float4 w = float4(spectrum_offset(t), 1); sumw += w; float2 uvd = distort(uv, t, min_distort, max_distort); sumcol += Input0.Sample(SamplerLinearClamp, uvd) * w; t += stepsiz; } sumcol /= sumw; color = sumcol + (rnd / 255.0); } else { color = Input0.Sample(SamplerLinearClamp, uv); } // Lens Flares BRANCH if (LensFlareIntensity > 0) { // Get lens flare color float3 lensFlares = Input3.Sample(SamplerLinearClamp, uv).rgb * LensFlareIntensity; // Get lens star color and mix it with lens flares float2 lensStarTexcoord = uv - 0.5; lensStarTexcoord = mul(lensStarTexcoord, (float2x2)LensFlareStarMat).xy; lensStarTexcoord += 0.5; float3 lensStar = LensStar.Sample(SamplerLinearClamp, lensStarTexcoord).rgb; lensFlares *= lensStar * 2 + 0.5; // Accumulate final lens flares lght lensLight += lensFlares * 1.5f; color.rgb += lensFlares; } // Bloom BRANCH if (BloomMagnitude > 0) { // Sample the bloom float3 bloom = Input2.SampleLevel(SamplerLinearClamp, uv, 0).rgb; bloom = bloom * BloomMagnitude; // Accumulate final bloom lght lensLight += max(0, bloom * 3.0f + (- 1.0f * 3.0f)); color.rgb += bloom; } // Lens Dirt float3 lensDirt = LensDirt.SampleLevel(SamplerLinearClamp, uv, 0).rgb; color.rgb += lensDirt * (lensLight * LensDirtIntensity); // Eye Adaptation post exposure color.rgb *= PostExposure; // Color Grading and Tone Mapping #if !NO_GRADING_LUT color.rgb = ColorLookupTable(color.rgb); #endif // Film Grain BRANCH if (GrainAmount > 0) { // Calculate noise float2 rotCoordsR = coordRot(uv, GrainTime); float noise = pnoise2D(rotCoordsR * (InputSize / GrainParticleSize), GrainTime); // Noisiness response curve based on scene luminance float luminance = Luminance(saturate(color.rgb)); luminance += smoothstep(0.2, 0.0, luminance); // Add noise to the final color noise = lerp(noise, 0, min(pow(luminance, 4.0), 100)); color.rgb += noise * GrainAmount; } // Vignette BRANCH if (VignetteIntensity > 0) { float2 uvCircle = uv * (1 - uv); float uvCircleScale = uvCircle.x * uvCircle.y * 16.0f; float mask = lerp(1, pow(uvCircleScale, VignetteShapeFactor), VignetteIntensity); color.rgb = lerp(VignetteColor, color.rgb, mask); } // Screen fade color.rgb = lerp(color.rgb, ScreenFadeColor.rgb, ScreenFadeColor.a); // Saturate color since it will be rendered to the screen color.rgb = saturate(color.rgb); // Return final pixel color (preserve input alpha) return color; }