<h2>tl;dr: perspective-correct interpolation of NDCs doesn't work; they need to be <em>linearly</em> interpolated in screen-space instead.</h2>
<p>This answer uses math notation from <a href="https://www.comp.nus.edu.sg/%7Elowkl/publications/lowk_persp_interp_techrep.pdf" rel="nofollow noreferrer">this paper</a>, <em>section 3: interpolating vertex attributes</em>. (<em>I highly recommend reading the entire paper</em> to understand how perspective-correct interpolation of vertex attributes works; the following discussion is based on it.)</p>
<hr />
<h2>Why does the code in the tutorial work?</h2>
<p>The hardware does perspective-correct interpolation of vertex clip-space coordinates to yield a pixel's clip-space coordinates. Then the perspective divide (by the interpolated <em>w</em>-coordinate) in the pixel shader transforms that into NDC.</p>
<p>But let's dive a bit deeper and see what the interpolation really does under the hood:</p>
<p>Let <span class="math-container">$I_1$</span> and <span class="math-container">$I_2$</span> be <em>clip-space</em> coordinates of two vertices, and let <span class="math-container">$Z_1$</span> and <span class="math-container">$Z_2$</span> be their respective view-space <em>z</em>-coordinate (or clip-space <em>w</em>-coordinate).
We want to find a pixel's NDCs, or mathematically: <span class="math-container">$I_t/Z_t$</span> where <span class="math-container">$I_t$</span> is its clip-space coordinates and <span class="math-container">$Z_t$</span> is the view-space <em>z</em>-coordinate.</p>
<p>Now recall that NDCs are coordinates of a 3D object <em>after projecting it on the 2D projection window</em> (screen-space). So to find a pixel's NDCs in screen-space, it suffices to linearly interpolate the vertices' NDCs <em>in screen-space</em>. Mathematically:</p>
<p><span class="math-container">$$
\tag{*}\label{*}
\frac{I_t}{Z_t}=\frac{I_1}{Z_1}+s\left(\frac{I_2}{Z_2}-\frac{I_1}{Z_1}\right)
$$</span></p>
<p>(<span class="math-container">$s$</span> is a screen-space barycentric coordinate of a triangle, see in the paper linked above).</p>
<p>To get this, we first let the hardware do perspective-correct interpolation of the vertex <em>clip-space</em> coordinates. This is <strong>equation (16)</strong> in the paper and it gives us <span class="math-container">$I_t$</span> - the pixel's <em>clip-space</em> coords:
<span class="math-container">$$
I_t=\left[\frac{I_1}{Z_1}+s\left(\frac{I_2}{Z_2}-\frac{I_1}{Z_1}\right)\right]Z_t
$$</span>
Then in the pixel shader we divide by <em>w</em> which is simply the <span class="math-container">$Z_t$</span> of a pixel (remember that <em>w</em> gets interpolated as well), which gives us <span class="math-container">$I_t/Z_t$</span> (NDC) as desired.</p>
<p>So <em>the hardware does the perspective divide and interpolation for us</em>, which gives the pixel's NDCs, but it also multiplies by <span class="math-container">$Z_t$</span> which transforms from NDC back to clip-space. And what the perspective divide in the pixel shader really does is transform back to NDC by cancelling out the <span class="math-container">$Z_t$</span>.</p>
<hr />
<h2>What if we do the division in the <em>vertex shader</em> instead?</h2>
<p>The vertex NDCs are <span class="math-container">$I_1/Z_1$</span> and <span class="math-container">$I_2/Z_2$</span> and the perspective-correct interpolation equation gives:
<span class="math-container">$$
\left[\frac{I_1}{Z_1^2}+s\left(\frac{I_2}{Z_2^2}-\frac{I_1}{Z_1^2}\right)\right]Z_t
$$</span>
which is clearly not <span class="math-container">$I_t/Z_t$</span> as defined in <span class="math-container">$\eqref{*}$</span>.
The reason this equation doesn't work in this case is that its derivation requires the attribute to <em>vary linearly across the triangle in 3D space</em> (view space). But screen-space NDC coords don't vary linearly in 3D space because of the perspective divide, so the premise is broken. (Clip-space coords <em>do</em> vary linearly in view-space because they are the result of a linear transform.)
<strong>Perspective-correct interpolation of values that have undergone perspective divide doesn't work</strong>. They are not in 3D space anymore, rather in 2D projection space and thus require <strong>linear interpolation in screen-space</strong> to get correct results. In practice it's accomplished using perspective-correct interpolation of clip-space coords, plus perspective divide per-pixel.</p>
<hr />
<p>Fun fact: <span class="math-container">$Z_{ndc}$</span> has the form <span class="math-container">$Z_{ndc}=A+\frac{B}{z}$</span> (where <span class="math-container">$z$</span> is in view space) and is also linearly interpolated in screen-space for z-buffering:</p>
<ol>
<li>Perspective-correct interpolation of clip-space <em>z</em> of the form <span class="math-container">$z'=Az+B$</span>:
<span class="math-container">$$
\left[\frac{Az_1+B}{z_1}+s\left(\frac{Az_2+B}{z_2}-\frac{Az_1+B}{z_1}\right)\right]z_t =
\left\{A+\frac{B}{z_1}+s\left[A+\frac{B}{z_2}-\left(A+\frac{B}{z_1}\right)\right]\right\}z_t
$$</span></li>
<li>Perspective divide per-pixel (div by <span class="math-container">$z_t$</span>):
<span class="math-container">$$
A+\frac{B}{z_1}+s\left[A+\frac{B}{z_2}-\left(A+\frac{B}{z_1}\right)\right]
$$</span></li>
</ol>
<p>Thus both steps amount to a lerp of <span class="math-container">$Z_{ndc}$</span>.</p>

CC BY-SA 4.0
2024-07-09T16:20:23.273Z
2024-09-14T22:17:30.757Z
2024-09-14T22:17:30.757Z
Daniel
<p>I've just worked my way through <a href="https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping" rel="nofollow noreferrer">this OpenGL shadow mapping tutorial</a>.
While I understand the basic algorithm, one thing puzzles me:
During the 2nd render pass all vertices are transformed into the clip space of the light source. This is done by multiplying them with the light's view-projection matrix in the vertex shader:</p>

<pre><code>vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);
</code></pre>

<p>However, for texture lookup into the shadow map a perspective division is needed. This is done in the fragment shader:</p>

<pre><code>float ShadowCalculation(vec4 fragPosLightSpace)
{
    // perform perspective divide
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    //continue w. texture lookup
    [...]
}
</code></pre>

<p>So my question is - why can't I perform the perspective division in the vertex shader? I did try moving the division from fragment to vertex shader in my otherwise finished shadow mapping code, and ended up with some really weird artifacts. So I guess it has something to do with the interpolation performed by the rasterizer, but I would like a more detailed explanation if possible.</p>

CC BY-SA 4.0
2019-11-24T14:46:35.863Z
"*This is done in the fragment shader*" That's silly. OpenGL has [dedicated `Proj` texture accessing functions that do the divide for you](https://www.khronos.org/opengl/wiki/Sampler_(GLSL)#Projective_texture_access). I'm surprised that the tutorial doesn't bother to use them, since letting you know they exist is half the point of such a tutorial in the first place.
Nicol Bolas
CC BY-SA 4.0
2024-09-14T22:24:31.217Z
If you do the perspective divide and then interpolate, you will not get the same values if you interpolate and then divide. The end points of the interpolation will be correct, but every point in between will be divided by the w value of the end point, not the interpolated w value.  Doing the division in the vertex shader is the divide then interpolate case, doing the division in the fragment shader is the interpolate then divide case.
pmw1234
<opengl><glsl><shadow-mapping>
Projective Texture / Shadow Mapping -- Why is the perspective division performed in the fragment shader?
rapunzel
# Projective Texture / Shadow Mapping -- Why is the perspective division performed in the fragment shader?

(Asked by: username_0 on 2019-11-24)

I've just worked my way through [this OpenGL shadow mapping tutorial](https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping).
While I understand the basic algorithm, one thing puzzles me:
During the 2nd render pass all vertices are transformed into the clip space of the light source. This is done by multiplying them with the light's view-projection matrix in the vertex shader:

```
vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);

```

However, for texture lookup into the shadow map a perspective division is needed. This is done in the fragment shader:

```
float ShadowCalculation(vec4 fragPosLightSpace)
{
    // perform perspective divide
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    //continue w. texture lookup
    [...]
}

```

So my question is - why can't I perform the perspective division in the vertex shader? I did try moving the division from fragment to vertex shader in my otherwise finished shadow mapping code, and ended up with some really weird artifacts. So I guess it has something to do with the interpolation performed by the rasterizer, but I would like a more detailed explanation if possible.

**Tags:** opengl, glsl, shadow-mapping

> **Comments:**
> **username_1**: "*This is done in the fragment shader*" That's silly. OpenGL has [dedicated `Proj` texture accessing functions that do the divide for you](https://www.khronos.org/opengl/wiki/Sampler_(GLSL)#Projective_texture_access). I'm surprised that the tutorial doesn't bother to use them, since letting you know they exist is half the point of such a tutorial in the first place.  
> **username_2**: If you do the perspective divide and then interpolate, you will not get the same values if you interpolate and then divide. The end points of the interpolation will be correct, but every point in between will be divided by the w value of the end point, not the interpolated w value.  Doing the division in the vertex shader is the divide then interpolate case, doing the division in the fragment shader is the interpolate then divide case.  

---

## 1 answers

### Answer by username_3 on 2024-07-09. Score: 0

## tl;dr: perspective-correct interpolation of NDCs doesn't work; they need to be *linearly* interpolated in screen-space instead.

This answer uses math notation from [this paper](https://www.comp.nus.edu.sg/%7Elowkl/publications/lowk_persp_interp_techrep.pdf), *section 3: interpolating vertex attributes*. (*I highly recommend reading the entire paper* to understand how perspective-correct interpolation of vertex attributes works; the following discussion is based on it.)

---

## Why does the code in the tutorial work?

The hardware does perspective-correct interpolation of vertex clip-space coordinates to yield a pixel's clip-space coordinates. Then the perspective divide (by the interpolated *w*-coordinate) in the pixel shader transforms that into NDC.

But let's dive a bit deeper and see what the interpolation really does under the hood:

Let $I\_1$ and $I\_2$ be *clip-space* coordinates of two vertices, and let $Z\_1$ and $Z\_2$ be their respective view-space *z*-coordinate (or clip-space *w*-coordinate).
We want to find a pixel's NDCs, or mathematically: $I\_t/Z\_t$ where $I\_t$ is its clip-space coordinates and $Z\_t$ is the view-space *z*-coordinate.

Now recall that NDCs are coordinates of a 3D object *after projecting it on the 2D projection window* (screen-space). So to find a pixel's NDCs in screen-space, it suffices to linearly interpolate the vertices' NDCs *in screen-space*. Mathematically:

$$
\tag{\*}\label{\*}
\frac{I\_t}{Z\_t}=\frac{I\_1}{Z\_1}+s\left(\frac{I\_2}{Z\_2}-\frac{I\_1}{Z\_1}\right)
$$

($s$ is a screen-space barycentric coordinate of a triangle, see in the paper linked above).

To get this, we first let the hardware do perspective-correct interpolation of the vertex *clip-space* coordinates. This is **equation (16)** in the paper and it gives us $I\_t$ - the pixel's *clip-space* coords:
$$
I\_t=\left[\frac{I\_1}{Z\_1}+s\left(\frac{I\_2}{Z\_2}-\frac{I\_1}{Z\_1}\right)\right]Z\_t
$$
Then in the pixel shader we divide by *w* which is simply the $Z\_t$ of a pixel (remember that *w* gets interpolated as well), which gives us $I\_t/Z\_t$ (NDC) as desired.

So *the hardware does the perspective divide and interpolation for us*, which gives the pixel's NDCs, but it also multiplies by $Z\_t$ which transforms from NDC back to clip-space. And what the perspective divide in the pixel shader really does is transform back to NDC by cancelling out the $Z\_t$.

---

## What if we do the division in the *vertex shader* instead?

The vertex NDCs are $I\_1/Z\_1$ and $I\_2/Z\_2$ and the perspective-correct interpolation equation gives:
$$
\left[\frac{I\_1}{Z\_1^2}+s\left(\frac{I\_2}{Z\_2^2}-\frac{I\_1}{Z\_1^2}\right)\right]Z\_t
$$
which is clearly not $I\_t/Z\_t$ as defined in $\eqref{\*}$.
The reason this equation doesn't work in this case is that its derivation requires the attribute to *vary linearly across the triangle in 3D space* (view space). But screen-space NDC coords don't vary linearly in 3D space because of the perspective divide, so the premise is broken. (Clip-space coords *do* vary linearly in view-space because they are the result of a linear transform.)
**Perspective-correct interpolation of values that have undergone perspective divide doesn't work**. They are not in 3D space anymore, rather in 2D projection space and thus require **linear interpolation in screen-space** to get correct results. In practice it's accomplished using perspective-correct interpolation of clip-space coords, plus perspective divide per-pixel.

---

Fun fact: $Z\_{ndc}$ has the form $Z\_{ndc}=A+\frac{B}{z}$ (where $z$ is in view space) and is also linearly interpolated in screen-space for z-buffering:

1. Perspective-correct interpolation of clip-space *z* of the form $z'=Az+B$:
   $$
   \left[\frac{Az\_1+B}{z\_1}+s\left(\frac{Az\_2+B}{z\_2}-\frac{Az\_1+B}{z\_1}\right)\right]z\_t =
   \left\{A+\frac{B}{z\_1}+s\left[A+\frac{B}{z\_2}-\left(A+\frac{B}{z\_1}\right)\right]\right\}z\_t
   $$
2. Perspective divide per-pixel (div by $z\_t$):
   $$
   A+\frac{B}{z\_1}+s\left[A+\frac{B}{z\_2}-\left(A+\frac{B}{z\_1}\right)\right]
   $$

Thus both steps amount to a lerp of $Z\_{ndc}$.


---

