Search Unity

MVP matrix, why this doesn't work? [Solved: a Unity flaw]

Discussion in 'Shaders' started by kdkd, Nov 14, 2018.

  1. kdkd

    kdkd

    Joined:
    Nov 21, 2017
    Posts:
    26
    For a pp effect I'm writing, I need to construct a MVP matrix myself. here's the test code. It doesn't work well. Am I misunderstood something?

    Shader: really straight forward, just compute the screen pos using custom MVP.
    I include a toggle to switch between the builtin UnityObjectToClipPos and custom MVP computation.

    Code (CSharp):
    1. Shader "Custom/TestScreenPos"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.         [Toggle]_UseCustomMVP("Use custom mvp", Float) = 0
    7.     }
    8.     SubShader
    9.     {
    10.         Tags { "RenderType"="Opaque" }
    11.         LOD 100
    12.  
    13.         Pass
    14.         {
    15.             CGPROGRAM
    16.             #pragma vertex vert
    17.             #pragma fragment frag
    18.             // make fog work
    19.             #pragma multi_compile_fog
    20.        
    21.             #include "UnityCG.cginc"
    22.  
    23.             struct appdata
    24.             {
    25.                 float4 vertex : POSITION;
    26.                 float2 uv : TEXCOORD0;
    27.             };
    28.  
    29.             struct v2f
    30.             {
    31.                 float2 uv : TEXCOORD0;
    32.                 float4 vertex : SV_POSITION;
    33.                 float4 p : TEXCOORD1;
    34.             };
    35.  
    36.             sampler2D _MainTex;
    37.             float4 _MainTex_ST;
    38.             float4x4 _RenderMVP;
    39.             float _UseCustomMVP;
    40.        
    41.             v2f vert (appdata v)
    42.             {
    43.                 v2f o;
    44.                 o.vertex = UnityObjectToClipPos(v.vertex);
    45.                 o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    46.                 o.p = v.vertex;
    47.                 return o;
    48.             }
    49.        
    50.             float4 frag (v2f i) : SV_Target
    51.             {
    52.                 float4 clipPos;
    53.                 if(_UseCustomMVP) {
    54.                     clipPos = mul(_RenderMVP, half4(i.p.xyz, 1) );
    55.                 }
    56.                 else {
    57.                     clipPos = UnityObjectToClipPos(i.p.xyz);
    58.                 }
    59.                 float2 kuv = clipPos.xy / clipPos.w * 0.5 + 0.5;
    60.                 #if UNITY_UV_STARTS_AT_TOP
    61.                     kuv.y = 1-kuv.y;
    62.                 #endif
    63.                 float4 col = tex2D(_MainTex, kuv);
    64.                 return col;
    65.             }
    66.             ENDCG
    67.         }
    68.     }
    69. }
    70.  
    C# part: just setting up the MVP matrix:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class TestScreenPosRunner : MonoBehaviour {
    6.  
    7.     void Start () {
    8.         //material = GetComponent<Renderer>().sharedMaterial;
    9.     }
    10.  
    11.     void Update () {
    12.         Camera cam = Camera.main;
    13.         //Matrix4x4 VP = cam.projectionMatrix * cam.worldToCameraMatrix;
    14.         Matrix4x4 MVP = cam.projectionMatrix * cam.worldToCameraMatrix
    15.             * transform.localToWorldMatrix;
    16.         Shader.SetGlobalMatrix("_RenderMVP", MVP);
    17.         //Shader.SetGlobalMatrix("_RenderVP", VP);
    18.     }
    19. }
    20.  

    When I use UnityObjectToClipPos, every thing works [Pic below]
    right.JPG


    However when I use custom MVP, it gives a a complete garbage:
    problem.JPG settings.JPG


    What's the magic here?



    Update:
    Solution: see #10 and #13
     
    Last edited: Nov 17, 2018
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Lets go through these:
    cam.projectionMatrix - Read the documentation on that one:
    https://docs.unity3d.com/ScriptReference/Camera-projectionMatrix.html
    transform.localToWorldMatrix - And the documenation for this one too:
    https://docs.unity3d.com/ScriptReference/Transform-localToWorldMatrix.html
    cam.worldToCameraMatrix - That one is fine, it's the only matrix you're using correctly actually.

    So, in the end, it should be:
    Matrix4x4 MVP = GL.GetGPUProjectionMatrix(cam.projectionMatrix, false) * cam.worldToCameraMatrix * transform.GetComponent<Renderer>().localToWorldMatrix;
     
    pjhk5797 and shegway like this.
  3. kdkd

    kdkd

    Joined:
    Nov 21, 2017
    Posts:
    26
    thanks for the insight, but this is what the new code gives:
    issue2.JPG
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
  5. kdkd

    kdkd

    Joined:
    Nov 21, 2017
    Posts:
    26
    No it's not. and I tried both options.
    Left, RT=false, right, RT=true
    rt-false.JPG rt-true.JPG

    The calculated VP matrix is nothing equal to UNITY_MATRIX_VP
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Hmm... no idea then. That code should produce an identical MVP matrix to what Unity uses. Well, technically the function Unity's shaders use does the local to world (unity_ObjectToWorld) and world to clip (UNITY_MATRIX_VP) separately, but for this it shouldn't matter.
     
  7. kdkd

    kdkd

    Joined:
    Nov 21, 2017
    Posts:
    26
    pls unity, shed some light? it's not good if we can be frustrated by such simple need.
     
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Is more than one object in the screne using this shader? If so, the object might be batched in which case the per object local to world isn't valid. There's no way to detect this from script. You can turn off batching on the shader by adding "DisableBatching"="True" in the subshader tags.

    But I'm not sure that's it.

    There are several threads here and on answers from people asking how to recreate the MVP or VP matrices, and I can't see anything wrong.
     
  9. kdkd

    kdkd

    Joined:
    Nov 21, 2017
    Posts:
    26
    Perhaps that's not the reason.
    I uploaded this tiny test scene in the attachment "MVPTest.rar".
    Take a look?

    custommvp.JPG
    unitybuiltin.JPG
     

    Attached Files:

  10. kdkd

    kdkd

    Joined:
    Nov 21, 2017
    Posts:
    26
    Solution found.

    I found this is affected by a tricky Unity flaw or bug.
    I added a line "public Camera cam;" in the TestScreenPosRunner.cs, assign the main camera to it. Then, delete that line. The net effect is no code or inspector changes. After this. It works mostly, aside from that the V coordinate is upside down comparing to Unity's builtin variable.
    Then, flip the renderToTexture switch to "true" GL.GetGPUProjectionMatrix(cam.projectionMatrix, true);
    Everything works.

    It appears, without a proper Camera property at least once, Unity will somehow clip away the Camera matrix computation out of user scripts and caches the decision, without ANY WARNING.
    Current (2018.2) workaround: Either add a Camera property, or add-then-delete the Camera property with compilation at least once.


    This behavior is not documented. If my guessing is correct, Unity staffs should take serious steps about the engine's compilation stability issue.
     
    Last edited: Nov 16, 2018
    MSQTobi likes this.
  11. kdkd

    kdkd

    Joined:
    Nov 21, 2017
    Posts:
    26
    Remaining issue:

    Both Camera.main and Camera.current are unable to provide previews simultaneously in Scene view and Game view, where Unity's builtin function does that with no problems.
     
  12. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Camera.main is literally just looking for a camera on a game object named MainCamera. If you don't have one, or have more than one camera with that name, it won't work well. And it'll never work for the scene view.

    Camera.current isn't valid during Update, only very specific functions that happen per camera. Update happens once per frame, and you need something that happens once per camera, like OnPreRender.

    There are two versions of OnPreRender. One requires your script is on the same game object as the camera, which works fine, but won't work for the scene view. The other is a static delegate which Unity will call for all cameras.

    https://docs.unity3d.com/ScriptReference/Camera-onPreRender.html

    It passes the camera being rendered to the function directly, so there's no need to use Camera.main or Camera.current.
     
    Last edited: Nov 16, 2018
    BergOnTheJob and kdkd like this.
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [ExecuteInEditMode]
    6. public class TestScreenPosRunner : MonoBehaviour {
    7.  
    8.     void OnEnable () {
    9.         Camera.onPreRender += UpdateMVP;
    10.     }
    11.  
    12.     void OnDisable () {
    13.         Camera.onPreRender -= UpdateMVP;
    14.     }
    15.  
    16.     void UpdateMVP (Camera cam) {
    17.         Matrix4x4 M = transform.GetComponent<Renderer>().localToWorldMatrix;
    18.         Matrix4x4 V = cam.worldToCameraMatrix;
    19.         Matrix4x4 P = GL.GetGPUProjectionMatrix(cam.projectionMatrix, true);
    20.         Matrix4x4 MVP = P*V*M;
    21.         Shader.SetGlobalMatrix("_RenderMVP", MVP);
    22.         Shader.SetGlobalMatrix("_RenderM", M);
    23.         Shader.SetGlobalMatrix("_RenderV", V);
    24.         Shader.SetGlobalMatrix("_RenderP", P);
    25.     }
    26. }
    This code works in the editor for both the scene and main view. However if you want to support multiple objects with something like this, I'd recommend only passing in the VP matrix and using the built in unity_ObjectToWorld matrix to transform the mesh into world space, or applying the matrix to only the specific mesh the script is attached to (using a MaterialPropertyBlock). Otherwise if multiple objects are using this script the MVP matrix that they'll all use will simply be the last one that got updated before rendering actually begins.
     
  14. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    I just had to hit play once. Your script as you had it won't run unless you're in play mode, then it worked fine. For the above example script I added [ExecuteInEditMode] so that it runs when not in play mode.

    Yeah, this part is curious. It seems like, at least in the editor, Unity is always render to a render texture? I'm too lazy to build to standalone to see if it's still flipped or not.
     
  15. kdkd

    kdkd

    Joined:
    Nov 21, 2017
    Posts:
    26
    This is brilliant, it solves the preview thing.

    If everyone's millage can be different, that's a big problem.
    For me, I have to recompile with Camera property. Otherwise, it'll look the pics I uploaded with the test project.


    Actually, since it's the screen coordinates to be used with texture space sampling, it has to be always true. I have run the build on all APIs, and it confirms.