ddx/ddy always return zero when the input value is face normal(object space)

hello, i am a newbee to unity, i am now working with edge detection problem.
I find that using the difference(ddx, ddy) of face nomal(object space) can easily generate a plausible result. And the object space normal is rotation invariant.


object-space face normal

But when i use ddx/ddy to generate difference of face normal, it always return zero value, why would this happen?
Here is the code snippet of my shader, i generate a face normal in geometry shader.

struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float2 uvLM : TEXCOORD1;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct Varyings
{
float2 uv : TEXCOORD0;
float2 uvLM : TEXCOORD1;
float4 positionWSAndFogFactor : TEXCOORD2;
half3 normalWS : TEXCOORD3;
#if _NORMALMAP
half3 tangentWS : TEXCOORD4;
half3 bitangentWS : TEXCOORD5;
#endif

#ifdef _MAIN_LIGHT_SHADOWS
float4 shadowCoord : TEXCOORD6;
#endif
float3 normalOS : TEXCOORD7;
float4 positionCS : SV_POSITION;
};

Varyings vert(Attributes input)
{
Varyings output;
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
VertexNormalInputs vertexNormalInput = GetVertexNormalInputs(input.normalOS.xyz, input.tangentOS);
float fogFactor = ComputeFogFactor(vertexInput.positionCS.z);

// output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
output.uv = input.uv;
output.uvLM = input.uvLM.xy * unity_LightmapST.xy + unity_LightmapST.zw;

output.positionWSAndFogFactor = float4(vertexInput.positionWS, fogFactor);
output.normalWS = vertexNormalInput.normalWS;

#ifdef _NORMALMAP
output.tangentWS = vertexNormalInput.tangentWS;
output.bitangentWS = vertexNormalInput.bitangentWS;
#endif

#ifdef _MAIN_LIGHT_SHADOWS
output.shadowCoord = GetShadowCoord(vertexInput);
#endif
output.positionCS = vertexInput.positionCS;
// store the object space position
output.normalOS = input.positionOS.xyz;
return output;
}

[maxvertexcount(3)]
void geom(triangle Varyings input[3], inout TriangleStream<Varyings> outputStream)
{
Varyings output = (Varyings)0;
Varyings top[3];

float3 dir[3];
[unroll(3)]
for (int k = 0; k < 3; ++k)
{
dir[k] = input[(k + 1) % 3].normalOS.xyz - input[k].normalOS.xyz;
}

// generate a face normal
float3 faceNorm = normalize(cross(dir[0], -dir[2]));

[unroll(3)]
for (int i = 0; i < 3; ++i)
{
top _= input*;*_
<em>_top*.normalOS = faceNorm;*_</em>
<em><em>_outputStream.Append(top*);*_</em></em>
<em><em>_*}*_</em></em>
<em><em>_*outputStream.RestartStrip();*_</em></em>
<em><em>_*}*_</em></em>
<em><em><em>*half4 frag(Varyings input) : SV_Target*</em></em></em>
<em><em>_*{*_</em></em>
<em><em>_*// draw out line*_</em></em>
<em><em>_*float3 diff = fwidth(input.normalOS.xyz);*_</em></em>
<em><em>_*float intensity = min(diff.x + diff.y + diff.z, 1.0);*_</em></em>
<em><em>_*intensity = 1.0 - intensity;*_</em></em>
<em><em>_*return half4(intensity, intensity, intensity, 1.0);*_</em></em>
<em><em>_*}*_</em></em>
<em><em>_*ENDHLSL*_</em></em>
<em><em>_*```*_</em></em>
<em><em>_*![8454488--1121864--blank-white.PNG|547x547](upload://fDQjB9gbYONH68sE74O495p0Xnn.png)*_</em></em> 
<em><em>_*result of my shader.*_</em></em>

Pixel derivative functions like ddx() and ddy() are comparing screen space data within a single triangle.

Short primer on how GPUs work.

CPU sends a draw call to the GPU via the graphics API. This draw call generally includes a mesh, shader, and the relevant material data (textures, material properties, transform and camera matrices, etc). The GPU then takes the vertex data from the mesh and runs the vertex shader on every vertex to determine the clip space position. In your case, you also have a geometry shader which runs on every primitive of the mesh you’ve setup your geometry shader to have as an input, each triangle here, and the output of that is a new set of triangles and vertices that are in clip space. The GPU then figures out which triangles are visible and in which screen pixels. The triangles that are visible are then rendered by the fragment shader for each pixel they’re visible at.

Here’s where the important part of all of this comes in. GPUs don’t render one pixel at a time. They always render 4 pixels at a time in what’s known as a pixel quad, or a 2x2 block of pixels. The render target is broken up into a grid of these pixels quads, and if the GPU knows a triangle is visible in any of the pixels within a pixel quad, the entire pixel quad is rendered. That means if a triangle is only covering a single pixel of that pixel quad, all 4 pixels are rendered concurrently. This is where ddx() comes in. Because all 4 pixels are being calculated at the same time, derivative functions can request data from other pixels in the pixel quad. Functions like ddx() and ddy() are getting the difference between the left and right pixels, or top and bottom pixels, of the quad. Depending on the graphics API in use or the GPU, this might be the pixels of that row / column or it could always be comparing against the first pixel in the pixel quad against the pixel immediately next to it on either axis within the pixel quad. For pixels where the triangle isn’t visible, the fragment shader is still running as if it was, so there are still valid derivative values.

Why do GPUs do this? For texture mip mapping. All GPUs use pixel derivates to calculate the difference in the texture UVs to calculate the appropriate mip level to use when sampling a texture. You need to be able to calculate this even if a single pixel is visible, and this was the easiest and most efficient way to do that.

So coming back to the issue you’re having. In your geometry shader you’re calculating a single common normal for the entire triangle. You’re then using ddx() and ddy() to find out how much that normal is changing across that single triangle. But as the normal is the same for the entire triangle, there is no difference.

Presumably what you’re looking to do is to find the difference between the current triangle and the neighboring triangles, but the fragment shader doesn’t know anything about neighboring triangles. By design they can’t! And that’s even ignoring the fact you’re using a geometry shader that’s spitting out single triangles at a time further divorcing any possible knowledge it might have. Though it should be noted geometry shaders also don’t have knowledge of neighboring triangles, so they’re not of any extra help here either.

2 Likes

Thanks a lot! You really solve my problem, i think i need to precompute a favorite group of edges, or perform this pass in post-processing