Compute eye space from window space

From OpenGL.org
Revision as of 18:32, 25 January 2013 by Alfonse (Talk | contribs) (Derivation)

Jump to: navigation, search

This page will explain how to recompute eye-space vertex positions given window-space vertex positions. This will be shown for two cases. Case 1 uses gl_FragCoord​ in its entirety. Case 2 uses only gl_FragCoord.xyz​. Both require access to the projection matrix.

Definitions

Before we begin, we need to define some symbols:

Symbol Meaning
M The projection matrix
P The eye-space position, 4D vector
C The clip-space position, 4D vector
N The normalized device coordinate space position, 3D vector
W The window-space position, 3D vector
Vx, y The X and Y values passed to glViewport
Vw, h The width and height values passed to glViewport
Dn, f The near and far values passed to glDepthRange

From gl_FragCoord

gl_FragCoord.xyz​ is the window-space position W, a 3D vector quantity. gl_FragCoord.w​ contains the inverse of the clip-space W: gl\_FragCoord_{w}={\tfrac  {1}{C_{w}}}.

Given these values, we have a fairly simple system of equations:

{\begin{aligned}{\vec  N}&={\begin{bmatrix}{\tfrac  {(2*W_{x})-(2*V_{x})}{V_{w}}}-1\\{\tfrac  {(2*W_{y})-(2*V_{y})}{V_{h}}}-1\\{\tfrac  {(2*W_{z})-D_{f}-D_{n}}{D_{f}-D_{n}}}-1\end{bmatrix}}\\{\vec  C}_{{xyz}}&={\frac  {{\vec  N}}{gl\_FragCoord_{w}}}\\C_{{w}}&={\frac  {1}{gl\_FragCoord_{w}}}\\{\vec  P}&=M^{{-1}}{\vec  C}\end{aligned}}

In a GLSL fragment shader, the code would be as follows:

vec4 ndcPos;
ndcPos.xy = ((2.0 * gl_FragCoord.xy) - (2.0 * viewport.xy)) / (viewport.zw) - 1;
ndcPos.z = (2.0 * gl_FragCoord.z - gl_DepthRange.near - gl_DepthRange.far) /
    (gl_DepthRange.far - gl_DepthRange.near);
ndcPos.w = 1.0;
 
vec4 clipPos = ndcPos / gl_FragCoord.w;
vec4 eyePos = invPersMatrix * clipPos;

This assumes the presence of a uniform called viewport​, which is a vec4​, matching the parameters to glViewport, in the order passed to that function. Also, this assumes that invPersMatrix​ is the inverse of the perspective projection matrix (it is a really bad idea to compute this in the fragment shader). Note that gl_DepthRange​ is a built-in variable available to the fragment shader.

From XYZ of gl_FragCoord

This case is mostly useful for deferred rendering techniques. In deferred rendering, we render the material parameters of our objects to images. Then, we make several passes over these images, loading those material parameters and performing lighting computations on them.

In the light pass, we need to reconstruct the eye-space vertex position in order to do lighting. However, we do not actually have gl_FragCoord​; not for the fragment that produced the material parameters. Instead, we have the window-space X and Y position, from gl_FragCoord.xy​, and we have the window-space depth, sampled by accessing the depth buffer, which was also saved from the deferred pass.

What we are missing is the original window-space W coordinate.

Therefore, we must find a way to compute it from the window-space XYZ coordinate and the perspective projection matrix. This discussion will assume your perspective projection matrix is of the following form:

[ xx  xx  xx  xx ]
[ xx  xx  xx  xx ]
[ 0   0   T1  T2 ]
[ 0   0   E1   0 ]

The xx​ mean "anything;" they can be any value you use in your projection. The 0's must be zeros in your projection matrix. T1​, T2​, and E1​ can be any arbitrary terms, depending on how your projection matrix works.

If your projection matrix does not fit this form, then the following code will get a lot more complicated.

From window to ndc

We have the XYZ of window space:

{\vec  W}={\begin{bmatrix}gl\_FragCoord.x\\gl\_FragCoord.y\\fromDepthTexture\end{bmatrix}}

Computing the NDC space from window space is the same as the above:

{\vec  N}={\begin{bmatrix}{\tfrac  {(2*W_{x})-(2*V_{x})}{V_{w}}}-1\\{\tfrac  {(2*W_{y})-(2*V_{y})}{V_{h}}}-1\\{\tfrac  {(2*W_{z})-D_{f}-D_{n}}{D_{f}-D_{n}}}-1\end{bmatrix}}

Just remember: the viewport and depth range parameters are, in this case, the parameters that were used to render the original scene. The viewport should not have changed of course, but the depth range certainly could (assuming you even have a depth range in the lighting pass of a deferred renderer).

From NDC to clip

For the sake of simplicity, here are the equations for going from NDC space to clip space:

{\begin{aligned}C_{w}&={\tfrac  {T2}{N_{z}-{\tfrac  {T1}{E1}}}}\\{\vec  C}_{{xyz}}&={\vec  N}*C_{w}\end{aligned}}

Derivation

Deriving those two equiations is very non-trivial; it's a pretty big stumbling block. Let's start with what we know.

We can convert from clip space to NDC space, so we can go back:

{\begin{aligned}{\vec  N}&={\tfrac  {{\vec  C}}{C_{w}}}\\{\vec  C}&={\vec  N}*C_{w}\end{aligned}}

The problem is that we don't have Cw. We were able to use gl_FragCoord.w​ to compute it before, but that's not available when we're doing this after the fact in a deferred lighting pass.

So how do we compute it? Well, we know that the clip space position was originally computed like this:

{\vec  C}=M*{\vec  P}

Therefore, we know that Cw was computed by the dot-product of P with the fourth row of M. And given our above definition of the fourth row of M, we can conclude:

{\begin{aligned}C_{w}&=E1*P_{z}\\{\vec  N}&={\tfrac  {{\vec  C}}{E1*P_{z}}}\end{aligned}}

Of course, this just trades one unknown for another. But we can use this. It turns out that Nz has something in common with this:

N_{z}={\tfrac  {C_{z}}{E1*P_{z}}}

It's interesting to look at where Cz comes from. As before, we know that it was computed by the dot-product of P with the third row of M. And again, given our above definition for M, we can conclude:

{\begin{aligned}C_{z}&=T1*P_{z}+T2*P_{w}\\N_{z}&={\tfrac  {T1*P_{z}+T2*P_{w}}{E1*P_{z}}}\end{aligned}}

We still have two unknown values here, Pz and Pw. However, we can assume that Pw is 1.0, as this is usually the case for eye space positions. Given that assumption, we only have one unknown, Pz, which we can solve for:

{\begin{aligned}P_{w}&=1.0\\N_{z}&={\tfrac  {T1*P_{z}+T2}{E1*P_{z}}}\\N_{z}&={\tfrac  {T1}{E1}}+{\tfrac  {T2}{E1*P_{z}}}\\N_{z}-{\tfrac  {T1}{E1}}&={\tfrac  {T2}{E1*P_{z}}}\\E1*P_{z}&={\tfrac  {T2}{N_{z}-{\tfrac  {T1}{E1}}}}\\P_{z}&={\tfrac  {T2}{E1*(N_{z}-{\tfrac  {T1}{E1}})}}\\P_{z}&={\tfrac  {T2}{E1*N_{z}-T1}}\end{aligned}}

Now armed with Pz, we can compute Cw:

{\begin{aligned}C_{w}&=E1*P_{z}\\C_{w}&={\tfrac  {T2}{N_{z}-{\tfrac  {T1}{E1}}}}\end{aligned}}

And thus, we can compute the rest of C from this:

{\begin{aligned}{\vec  C}_{{xyz}}&={\vec  N}*C_{w}\\{\vec  C}_{{xyz}}&={\vec  N}*({\tfrac  {T2}{N_{z}-{\tfrac  {T1}{E1}}}})\end{aligned}}

From clip to eye

With the full 4D vector C computed, we can compute P just as before:

{\vec  P}=M^{{-1}}{\vec  C}

GLSL example

Here is some GLSL sample code for what this would look like:

uniform mat4 persMatrix;
uniform mat4 invPersMatrix;
uniform vec4 viewport;
uniform vec2 depthrange;
 
vec4 CalcEyeFromWindow(vec3 windowSpace)
{
	vec3 ndcPos;
	ndcPos.xy = ((2.0 * windowSpace.xy) - (2.0 * viewport.xy)) / (viewport.zw) - 1;
	ndcPos.z = (2.0 * windowSpace.z - depthrange.x - depthrange.y) /
    (depthrange.y - depthrange.x);
 
	vec4 clipPos;
	clipPos.w = persMatrix[3][3] / (ndcPos.z - (persMatrix[4][3] / persMatrix[3][4]));
	clipPos.xyz = ndcPos * clipPos.w;
 
	vec4 eyePos = invPersMatrix * clipPos;
}

viewport​ is a vector containing the viewport parameters. depthrange​ is a 2D vector containing the glDepthRange parameters.