You don’t normalize w, it is the normalizing term in itself. And you don’t ever want to “normalize” it in the vertex shader.
The xyzw values of clip space are all in a range defined by the current w value itself. So you “normalize” the value by dividing by the w, resulting in a -1 to 1 range for all on screen positions. They key is on screen positions.
The vertex shader isn’t confined by the frustum of the current projection matrix. That is to say the vertex shader runs on all vertices, not just the ones that end up visible on screen. The vertex shader is actually part of how the GPU determines if something is visible on screen or not by transforming the vertex position into the clip space position. So you’ll have clip space positions in the vertex shader that are far, far, outside the frustum bounds, and that’s okay. A vertex that is 300 units above the camera and far out of view may still be part of a triangle that is in view, so it still needs to be calculated. Most real time rendering engines make use of CPU side frustum culling to skip rendering of objects that are fully outside of the view frustum to avoid calculating meshes that for sure no triangle of which will be seen, but you can’t do that on a per vertex level. That vertex that’s 300 units out of view may be part of a triangle that’s 1000 units across and which you’re looking at the dead center off. Thus all 3 vertices of that triangle aren’t anywhere near that “-w to w” range, but are still needed.
As for why you don’t want to do the normalization in the vertex shader, it’s because the values are linearly interpolated in screen space. If you just pass the normalized positions it doesn’t interpolate properly, which is the entire point of using a float4 to begin with.
Here’s an easy example. Take a shader that just renders a texture using the normalized xy clip space positions as its UVs. First try dividing by w in the vertex shader. If the object is something like a view facing quad, it’ll look perfectly fine.
But try taking that quad and rotating it so it’s not facing the view and the texture will start to warp wildly.
But if you do the divide in the fragment shader, after the interpolation, everything is correct.
If you look closely at those last two images you’ll notice the UV positions at each vertex is the same spot on the texture, but everything in between is wrong. This is because the interpolated values aren’t correctly taking into account the perspective when you do the divide in the vertex shader.
Perspective Divide Test shader
Shader "Custom/Perspective Divide Test"
{
Properties {
_MainTex ("Texture", 2D) = "white" {}
[KeywordEnum(Vertex, Fragment)] _Do_Perspective_Divide_In ("Do Perspective Divide in:", Float) = 0
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma shader_feature _ _DO_PERSPECTIVE_DIVIDE_IN_FRAGMENT
struct v2f {
float4 pos : SV_Position;
float4 screenPos : TEXCOORD0;
};
sampler2D _MainTex;
v2f vert(appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.screenPos = o.pos;
#if !defined(_DO_PERSPECTIVE_DIVIDE_IN_FRAGMENT)
o.screenPos /= o.screenPos.w;
#endif
return o;
}
half4 frag(v2f i) : SV_Target
{
#if defined(_DO_PERSPECTIVE_DIVIDE_IN_FRAGMENT)
i.screenPos /= i.screenPos.w;
#endif
return tex2D(_MainTex, i.screenPos.xy);
}
ENDCG
}
}
}