Search Unity

Question Why Unity wants a Y-flipped projection matrix when rendering with Direct3D

Discussion in 'Universal Render Pipeline' started by rduret, May 30, 2022.

  1. rduret

    rduret

    Joined:
    Sep 11, 2020
    Posts:
    1
    First of all, hello,
    I have several questions tied together to this title, because I can't summarize all into one good question.
    (Also, it is almost a copy of my StakOverflow question if you already found it there. Since it may be more related to Unity I put it here too.)

    To put the settings, I am using Unity 2020.1.2f1 URP and I am trying to rebuild myself the Unity's projection matrix used with Direct3D 11 in order to fully understand the working of it.
    I know that Unity uses the left-handed system for the object and world spaces, but not for the view space, which still use the OpenGL's old convention of the right-handed one. I could say that the clip space is LH too as the Z axis points towards the screen, but Unity makes me doubt a lot.
    Let me explain : we all know that the handedness is given by the matrix, which is why the projection matrix (column-major here) used by Unity for OpenGL-like APIs looks like that :

    Code (CSharp):
    1. [ x  0  0  0 ] x = cot(fovH/2)  c = (f+n)/(n-f)
    2. [ 0  y  0  0 ] y = cot(fovV/2)  e = (2*f*n)/(n-f)
    3. [ 0  0  c  e ] d = -1
    4. [ 0  0  d  0 ]
    where 'c' and 'e' clip and flip 'z' into the depth buffer from the RH view space to the LH clip space (or NDC once the perspective division is applied), 'w' holds the flipped view depth, and the depth buffer is not reversed.

    With the near plane = 0.3 and the far plane = 100, the Unity's frame debugger confirms that our matrix sent to the shader is equal to 'glstate_matrix_projection' (it's the matrix behind UNITY_MATRIX_P macro in the shader), as well as the projection matrix from the camera itself 'camera.projectionMatrix' since it's the matrix built internally by Unity, following the OpenGL convention. It is even confirmed with 'GL.GetGPUProjectionMatrix()' which tweaks the projection matrix of our camera to match the Graphic API requirements before sending it to the GPU, but changes nothing in this case.

    Code (CSharp):
    1. // _CamProjMat
    2. float n = viewCam.nearClipPlane;
    3. float f = viewCam.farClipPlane;
    4. float fovV = Mathf.Deg2Rad * viewCam.fieldOfView;
    5. float fovH = 2f * Mathf.Atan(Mathf.Tan(fovH / 2f) * viewCam.aspect);
    6. Matrix4x4 projMat = new Matrix4x4();
    7. projMat.m00 = 1f / Mathf.Tan(fovH / 2f);
    8. projMat.m11 = 1f / Mathf.Tan(fovV / 2f);
    9. projMat.m22 = (f + n) / (n - f);
    10. projMat.m23 = 2 * f * n / (n - f);
    11. projMat.m32 = -1f;
    12.  
    13. // _GPUProjMat
    14. Matrix4x4 GPUMat = GL.GetGPUProjectionMatrix(viewCam.projectionMatrix, false);
    15. Shader.SetGlobalMatrix("_GPUProjMat", projMat);
    16.  
    17. // _UnityProjMat
    18. Shader.SetGlobalMatrix("_UnityProjMat", viewCam.projectionMatrix);
    gives us :
    OpenGL_projection_matrix.jpg
    HOWEVER, when I switch to Direct3D11 the 'glstate_matrix_projection' is flipped vertically. I mean that the m11 component of the matrix is negative, which flips the Y axis when applied to the vertex. The projection matrix for Direct3D used in Unity applies the Reversed Z buffer technique, giving us a matrix like :

    Code (CSharp):
    1. [ x  0  0  0 ] x = cot(fovH/2)   c = n/(f-n)
    2. [ 0  y  0  0 ] y = -cot(fovV/2)  e = (f*n)/(f-n)
    3. [ 0  0  c  e ] d = -1
    4. [ 0  0  d  0 ]
    (you'll notice that 'c' and 'e' are respectively the same as f/(n-f) and (f*n)/(n-f) given by Direct3D documentation of D3DXMatrixPerspectiveFovRH() function, with 'f' and 'n' swapped to apply the Reversed Z to the depth buffer. Btw 'y' is negative, we'll se why.)
    From there, there are several concerns :
    - if we try to give a projection matrix to the shader, instead of 'glstate_matrix_projection', using 'GL.GetGPUProjectionMatrix()' specifying false as the second parameter, the matrix won't be correct, the rendered screen will be flipped vertically, which is not wrong given the parameter.
    Direct3D_projection_matrix.jpg
    Indeed, this boolean parameter is to modify the matrix whether the image is rendered into a Render Texture or not, and it is justified since OpenGL vs Direct3D render texture coordinates are like this :
    OGL_vs_D3D_rt_coords.jpg
    In a way that makes sense because the screen space of Direct3D is in pixel coordinates, where the handedness is the same as for render texture coordinates, accessed in the pixel shader through the 'SV_Position' semantic. The clip space is only flipped vertically then, into a right-handed system with the positive Y going down the screen, and the positive Z going towards the screen.
    Nontheless, I render my vertices directly to the screen, and not into any render texture ... is this parameter from 'GL.GetGPUProjectionMatrix()' a trick to set to true when used with Direct3D like APIs ?

    - another concern is that we can guess that, given the clip space, NDC, and screen space are left-handed in OpenGL-like APIs, these spaces are right-handed in Direct3D-like APIs... right ? where am I wrong ? Although nobody never qualified or stated on any topic, documentation, dev blog, etc.. I ever read, the handedness of those doesn't seem to bother anyone. Even the projection matrices provided by the official Direct3D documentation don't flip the Y-axis, why then ? I admit I only tried to render graphics with D3D or OGL only inside Unity, perhaps Unity does black magic again under the coat, as usual heh.

    I hope I explained clearly enough all this mess, thanks to everyone who reach this point ;)
    I really need to find out what's going on here, because Unity's documentation becomes more and more legacy, with poor explanation on precise engine parts.
    Any help is really appreciated !!
     
    ElliotB, wolfand13 and srslylawl like this.