From textbooks, NDC space is clip space / w. However, I found some weird code in Core.hlsl in package com.unity.render-pipelines.universal.
struct VertexPositionInputs
{
float3 positionWS; // World space position
float3 positionVS; // View space position
float4 positionCS; // Homogeneous clip space position
float4 positionNDC;// Homogeneous normalized device coordinates
};
NDC is not just clipSpace.xy / clipSpace.w. Homogeneous clip spaceâs x & y have a -w to w range (for whatâs in view), but NDCâs x & y have a 0.0 to 1.0 range. Theyâre essentially screen space UVs. Dividing homogeneous clip space by its w just makes it non-homogeneous clip space, not NDC. NDC is closer to (clipSpace.xy / clipSpace.w) * 0.5 + 0.5. So the above code is basically solving that equation a little differently by doing:
Only, itâs not doing the divide by w, so it rescales the xy values to a 0.0 to w range.
But why not divide by w?
The key here is that âhomogeneousâ term. Note the comment for positionNDC refers to it as âHomogeneous normalized device coordinatesâ, and not just ânormalized device coordinatesâ. Thatâs not a mistake. The term homogeneous here refers to the fact itâs a coordinate in a projective space. Essentially is the value multiplied by w, which for a perspective projection happens to be the linear depth. If you want to dig into exactly what homogeneous coordinates are, be my guest, I honestly still canât chew through it. But the key thing is having values multiplied by the w allows those values to be correct when being linearly interpolated in a perspective projection space by dividing by w afterwards.
Basically, if you divide by w in the vertex shader, then try to use the value to sample a screen space texture, it wonât line up any more and instead will warp mid triangle. If youâre familiar with non-perspective correct texture mapping, like the original PS1, thatâs the kind of thing itâll look like.
So, if you dig deep enough in the shader code, youâll find the few places it does actually use that float4 version of the positionNDC, it divides by w in the fragment shader, converting the value from homogeneous NDC to âregularâ NDC.
Hello,bgolus. you mean that multiplying vertices attribution by w in the vertex shader should cancel out the perspective correction in the subsequent rasterization stage, right? make affine effect happen?
Hello, bgolus. since u pointed out the positionNDC.xy is essentially the screen space UV. I am wondering whatâs the difference between normalized screen space uv and screen space uv(positionNDC.xy)
I tested with GetNormalizedScreenSpaceUV() function. The color seems slightly brighter with positionNDC.xy
just wondering the usage scenarios of the two different ways and if it is possible to get the normalzied screen space with this positionNDC.xy instead of using that function
THANKS to Bgolus. Hereâs my understanding (I hope I understand it right >.<)
Although the variable âGetVertexPositionInputs().positionNDCâ contains âpositionNDCâ in its name, it is actually ăHomogeneous Normalized Device Coordinatesă (not strictly NDC coordinates), and it can be divided by w to obtain screen space UV.
It can be divided by w to obtain screen space UV â then shouldnât it be called âHomogeneous Screen Space Coordinatesâ? WhateverâŚNDC space and screen space only differ by a remapping from [-1,+1] to [0,+1], maybe itâs better not to dwell on this point.
In summary, this calculation transforms the vertex from clip space to a new space, where the xy components are remapped from range [-w,+w] to [0,+w] (The new space seems to be equivalent to one quadrant of the original clip space?). Later in the fragment shader, dividing it by w , and you get the screen space UV within the range [0,+1].
What if I want to calculate the vertex in the NDC space, what should I name it?
Actually there is no need to worry about the NDC space.
During the Vertex Shader stage, we only need to calculate positionCS and assign it to a field with the SV_POSITION semantic. We donât need to worry about space transformations like clip space â NDC space â screen space. During the Rasterization stage, the GPU handles the transformation from clip space to screen space and passes the screen space fragment data to the Fragment Shader stage.
The reason you consider NDC space important is that most books explaining space transformations mention the concept of NDC space. It helps to understand the transformation from clip space to screen space. However, in practice, shaders execute from the Vertex Shader to Rasterization and then to the Fragment Shader. When writing shaders, we are actually working on the Vertex Shader and Fragment Shader, and these two stages do not use or require NDC space.
In conclusion, when writing shaders, positionCS is all we need in the Vertex Shader stage; thereâs no need to compute vertex coordinate in NDC space.
The important question is why cannot I calculate the screenUV in the vertex shader, and pass it to the fragment shader?
Because every attribute you access in the Fragment Shader is calculated from âperspective correct barycentric interpolationâ. A useful link here: https://www.comp.nus.edu.sg/~lowkl/publications/lowk_persp_interp_techrep.pdf
(ι, β, γ) is the barycentric coordinate of that fragment(pixel) in the triangle ABC.
I_A, I_B, and I_C are the attribute values of the vertex A, B and C.
Z_A, Z_B, and Z_C are the camera space depth of the vertex A, B and C.
Z_t is the camera space depth of that fragment(pixel).
I_t is the interpolated value of that fragment(pixel).
Assuming I_A is the positionNDC of vertex A, dividing it by the camera space depth of point A yields the new attribute Q_A. Obviously Q_A is indeed the screenUV of vertex A. Therefore, performing barycentric interpolation between Q_A, Q_B, and Q_C, the fragment will naturally result in screenUV as well.
If you pass vertex Aâs screenUV as I_A into the above formula, the result you receive in the fragment shader stage will be the interpolated value of (screenUV/depth), not screenUV.