Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Custom HDRP Pass AfterOpaqueAndNormal - how to WRITE normal?

Discussion in 'Shaders' started by KYL3R, Nov 18, 2021.

  1. KYL3R

    KYL3R

    Joined:
    Nov 16, 2012
    Posts:
    128
    I created a Custom Pass Volume and created a FullScreenPass Shader that simply offsets the Normal for testing purpose.
    I want to read & write the normal.

    When I select the injection point "After Post Process" and simply return a float4 in my Pass, I see the normal COLOR. Great for debugging I guess. But I want to WRITE the normal.

    How? In a normal Shader, I'd use:

    Code (CSharp):
    1. EncodeIntoNormalBuffer(normalData, output.gBuffer1);
    where "output" is a struct:

    Code (CSharp):
    1. //FeatureName   Standard
    2.             //GBuffer0      baseColor.r,    baseColor.g,    baseColor.b,    specularOcclusion
    3.             //GBuffer1      normal.xy (1212),   perceptualRoughness
    4.             //GBuffer2      f0.r,   f0.g,   f0.b,   featureID(3) / coatMask(5)
    5.             //GBuffer3      bakedDiffuseLighting.rgb
    6.             struct FragmentOutput {
    7.                 float4 gBuffer0 : SV_Target0;
    8.                 float4 gBuffer1 : SV_Target1;
    9.                 float4 gBuffer2 : SV_Target2;
    10.                 float4 gBuffer3 : SV_Target3;
    11.             };    
    I think I got those comments from the Forum or the Documentation of HDRP.

    Anyway, returning a float4 shows me the Color in "After Post Process" Injection Point - as it doesn't expect any normals. But in "AfterOpaqueAndNormal" Injection Point it doesn't show any change, not even when I return my FragmentOutput struct with the SV_Targets - I was thinking I would be targetting the gbuffer for the normal Data...

    The documentation even says this for AfterOpaqueDepthAndNormal:

    Code (CSharp):
    1. Buffers will contain all opaque objects. Here you can modify the normal, roughness and depth buffer, it will be taken in account in the lighting and the depth pyramid. Note that normals and roughness are in the same buffer, you can use DecodeFromNormalBuffer and EncodeIntoNormalBuffer functions to read/write normal and roughness data
    - Note the last Part: "you can use ... EncodeIntoNormalBuffer..." - but HOW? by returning a struct? I do I need some inout/out variable as a parameter? Or maybe a special Tag in my Pass? This is no where doucmented - or I can't find it.

    @antoinel_unity has some examples on his GitHub, but I don't see any GBuffer Tags or similar in the code... I only see the depth being written by a "inout depth" parameter.

    How do I write Normals in a Fullscreen Custom Pass in HDRP, at the "AfterOpaqueDepthAndNormal" Injection Point.

    Thanks!
     
  2. KYL3R

    KYL3R

    Joined:
    Nov 16, 2012
    Posts:
    128
    I figured it out. You need to create a C# custom Pass as well. There you can define the render targets and call your custom Pass to render. The shader itself actually just returns float4, but you can then write it to the normalBuffer in the Execute Method:

    Code (CSharp):
    1. CoreUtils.SetRenderTarget(ctx.cmd, ctx.cameraNormalBuffer, ClearFlag.None);
    2. CoreUtils.DrawFullScreen(ctx.cmd, fullscreenPassMat, ctx.propertyBlock, shaderPassId: 0);
     
    Egad_McDad likes this.
  3. camerondus

    camerondus

    Joined:
    Dec 15, 2018
    Posts:
    29
    hello, could you provide more info on this? when i write a custom pass to try to write to the normal buffer, it completely corrupts it. all i want to do is write to the normal/roughness buffer using a fullscreen shader.
     
  4. KYL3R

    KYL3R

    Joined:
    Nov 16, 2012
    Posts:
    128


    Hm, a "human readable" normal is the "decoded" version, the gbuffer wants the "encoded" version, that actually looks like the normals got scrambled. Have a look at the Frame debugger what a "HDRP/Lit" shader does to the normals.

    Decoded, human readable version:


    For Comparison, Encoded GBuffer version using "EncodeIntoNormalBuffer":


    But I had some issues with the buffer format, ended up using "R32G32B32A32_SFloat", the other variants in the comment are formats that I tried before... Maybe that helps

    Code (CSharp):
    1. myCustomBuffer = RTHandles.Alloc(
    2.             Vector2.one, TextureXR.slices, dimension: TextureXR.dimension,
    3.             colorFormat: GraphicsFormat.R32G32B32A32_SFloat,  // colorFormat: GraphicsFormat.R8G8B8A8_SRGB,GraphicsFormat.R8G8B8A8_UNorm,
    4.             useDynamicScale: true, name: "custom normal buffer 2"
    5.         );

    This is how I used EncodeIntoNormalBuffer:

    Code (CSharp):
    1.  
    2. FragmentOutput GBufferFragment(VertexOutput input) : SV_TARGET{
    3.                 UNITY_SETUP_INSTANCE_ID(input);
    4.                 float3 albedo = col;
    5.                 float4 specular_roughness = float4(_SpecularRGB.r, _SpecularRGB.g, _SpecularRGB.b, _RoughnessValue);
    6.                 float4 normal_roughness = float4(input.worldNormal.xy, 0, 0);
    7.                 normal_roughness = float4(0, 0, 0, 0);
    8.  
    9.  
    10.                 NormalData normalData;
    11.                 normalData.normalWS = input.worldNormal.xyz;
    12.                 normalData.perceptualRoughness = 0.6f;
    13.                 float4 positionSS = input.clipPos;
    14.                 EncodeIntoNormalBuffer(normalData, positionSS, output.gBuffer1);
    15.  
    16.  
    17.                 output.gBuffer0.rgba = float4(albedo.rgb, _SpecularOcclusion); // Diffuse (RGB), specularOcclusion (A)
    18.                 output.gBuffer1.rgba = normal_roughness; // normal.xy(1212), perceptualRoughness
    19.  
    20.                 float3 shadow_color = albedo.rgb * 0.8;
    21.                 shadow_color.r += 0.4f;
    22.                 shadow_color.b += 0.45f;
    23.                 output.gBuffer2.rgba = float4(shadow_color, 0); // f0.r, f0.g, f0.b, featureID(3) / coatMask(5)
    24.                 output.gBuffer3.rgba = float4(0, 0, 0, 0); // bakedDiffuseLighting.rgb
    25.  
    26.                 return output;
    27.             }
    (I brightened the shadows, but you get the point) If you bake lights you might get a black mesh with this one.


    To make it work, and because I couldn't get the #includes to work (clashed with "UnityCG.cginc") I pasted the relevant parts:


    Code (CSharp):
    1. struct NormalData // from "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/NormalBuffer.hlsl"
    2.             {
    3.                 float3 normalWS;
    4.                 float  perceptualRoughness;
    5.             };
    6.             // Inserts the bits indicated by 'mask' from 'src' into 'dst'.
    7.             uint BitFieldInsert(uint mask, uint src, uint dst)
    8.             {
    9.                 return (src & mask) | (dst & ~mask);
    10.             }
    11.             // Composes a floating point value with the magnitude of 'x' and the sign of 's'.
    12.             // See the comment about FastSign() below.
    13.             float CopySign(float x, float s, bool ignoreNegZero = true)
    14.             {
    15. #if !defined(SHADER_API_GLES)
    16.                 if (ignoreNegZero)
    17.                 {
    18.                     return (s >= 0) ? abs(x) : -abs(x);
    19.                 }
    20.                 else
    21.                 {
    22.                     uint negZero = 0x80000000u;
    23.                     uint signBit = negZero & asuint(s);
    24.                     return asfloat(BitFieldInsert(negZero, signBit, asuint(x)));
    25.                 }
    26. #else
    27.                 return (s >= 0) ? abs(x) : -abs(x);
    28. #endif
    29.             }
    30.             // Ref: http://jcgt.org/published/0003/02/01/paper.pdf
    31.             // Encode with Oct, this function work with any size of output
    32.             // return float between [-1, 1]
    33.             float2 PackNormalOctQuadEncode(float3 n)
    34.             {
    35.                 //float l1norm    = dot(abs(n), 1.0);
    36.                 //float2 res0     = n.xy * (1.0 / l1norm);
    37.  
    38.                 //float2 val      = 1.0 - abs(res0.yx);
    39.                 //return (n.zz < float2(0.0, 0.0) ? (res0 >= 0.0 ? val : -val) : res0);
    40.  
    41.                 // Optimized version of above code:
    42.                 n *= rcp(dot(abs(n), 1.0));
    43.                 float t = saturate(-n.z);
    44.                 return n.xy + (n.xy >= 0.0 ? t : -t);
    45.             }
    46.             // Pack float2 (each of 12 bit) in 888
    47.             float3 PackFloat2To888(float2 f)
    48.             {
    49.                 uint2 i = (uint2)(f * 4095.5);
    50.                 uint2 hi = i >> 8;
    51.                 uint2 lo = i & 255;
    52.                 // 8 bit in lo, 4 bit in hi
    53.                 uint3 cb = uint3(lo, hi.x | (hi.y << 4));
    54.  
    55.                 return cb / 255.0;
    56.             }
    57.             void EncodeIntoNormalBuffer(NormalData normalData, uint2 positionSS, out float4 outNormalBuffer0)
    58.             {
    59.                 // The sign of the Z component of the normal MUST round-trip through the G-Buffer, otherwise
    60.                 // the reconstruction of the tangent frame for anisotropic GGX creates a seam along the Z axis.
    61.                 // The constant was eye-balled to not cause artifacts.
    62.                 // TODO: find a proper solution. E.g. we could re-shuffle the faces of the octahedron
    63.                 // s.t. the sign of the Z component round-trips.
    64.                 const float seamThreshold = 1.0 / 1024.0;
    65.                 normalData.normalWS.z = CopySign(max(seamThreshold, abs(normalData.normalWS.z)), normalData.normalWS.z);
    66.  
    67.                 // RT1 - 8:8:8:8
    68.                 // Our tangent encoding is based on our normal.
    69.                 float2 octNormalWS = PackNormalOctQuadEncode(normalData.normalWS);
    70.                 float3 packNormalWS = PackFloat2To888(saturate(octNormalWS * 0.5 + 0.5));
    71.                 // We store perceptualRoughness instead of roughness because it is perceptually linear.
    72.                 outNormalBuffer0 = float4(packNormalWS, normalData.perceptualRoughness);
    73.             }
    hope it helps
     
    Last edited: Jun 22, 2023