Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

How to get Parallax working correctly in multiview surface shader?

Discussion in 'Shaders' started by FireHawkSoftware, Sep 19, 2020.

  1. FireHawkSoftware

    FireHawkSoftware

    Joined:
    Aug 24, 2019
    Posts:
    1
    In multipass I get the desired 3d effect from Parallax just like the standard shader, but in multiview (Occulus Quest) the viewDir seems to be the same for both eyes, which causes it to visually flatten since the same offset is applied to both eyes. I would just use the standard shader, but I get better performance with this from packing Metallic Height and Gloss/Smoothness into one image (also AO but I'm currently not using that).

    I tried using my own vertex shader and a bunch of different macros found in the documentation and various forums (UNITY_VERTEX_OUTPUT_STEREO,
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO, etc) but nothing worked. The most promising thing I saw was using unity_StereoWorldSpaceCameraPos[unity_StereoEyeIndex] to access the eye and calculate the viewDir myself, but the compiler kept telling me unity_StereoWorldSpaceCameraPos was undefined.

    I am using 2020.1.2f1 with standard/built-in rendering pipeline.

    Code (CSharp):
    1. Shader "Custom/MainShader"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    6.         _MetAoHeightGloss("_MetAoHeightGloss (RGBA)", 2D) = "white" {}
    7.         _Normal ("Normal (RGB)", 2D) = "white" {}
    8.     }
    9.     SubShader
    10.     {
    11.         Tags { "RenderType"="Opaque" }
    12.         LOD 200
    13.  
    14.         CGPROGRAM
    15.        
    16.         #pragma target 3.0
    17.         #pragma surface surf Standard
    18.        
    19.         sampler2D _MainTex;
    20.         sampler2D _MetAoHeightGloss;
    21.         sampler2D _Normal;
    22.  
    23.         struct Input
    24.         {
    25.             float2 uv_MainTex;
    26.             float3 viewDir;
    27.         };
    28.  
    29.         void surf (Input IN, inout SurfaceOutputStandard o)
    30.         {
    31.             fixed4 map = tex2D(_MetAoHeightGloss, IN.uv_MainTex);
    32.             IN.uv_MainTex += ParallaxOffset(map.b, .02,  IN.viewDir);// map.b is height
    33.  
    34.             fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
    35.            
    36.             fixed4 normal = tex2D(_Normal, IN.uv_MainTex);
    37.             o.Albedo = c.rgb;
    38.             o.Metallic = map.r;
    39.             o.Smoothness = map.a;
    40.             //o.Occlusion = map.g; not currently using AO
    41.             o.Alpha = c.a;
    42.             o.Normal = UnpackNormal(normal);
    43.            
    44.         }
    45.         ENDCG
    46.     }
    47.     FallBack "Diffuse"
    48. }
    49.  
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    This is a bug with Surface Shaders, and unfortunately because it's a Surface Shader I have zero expectation that it'll get fixed ever as any development on them has basically been abandoned.

    Straight up,
    o.viewDir
    is wrong in the
    surf
    function on the Quest for one eye.

    The bug comes from the fact that the
    viewDir
    is calculated in the fragment shader using
    _WorldSpaceCameraPos
    , which when using Quest's multiview rendering is a macro for
    unity_StereoWorldSpaceCameraPos[unity_StereoEyeIndex]
    . But Surface Shaders don't ever set
    unity_StereoEyeIndex
    in the fragment shader when running on the Quest, so it's always using the left eye position (index 0). The good news is this is also a bug with vertex fragment shaders too! At least in some very specific and arguably broken in other ways setups...

    I found two workarounds for this.

    The "easiest" / obvious one is to not use the built in
    viewDir
    and instead use a custom
    Input
    struct value and set the tangent space view dir manually. I did put "easiest" in quotes for a reason, as that does take some know how. But the good news is the
    _WorldSpaceCameraPos
    is correct in the vertex shader.

    The other option is to set the
    unity_StereoEyeIndex
    in the fragment shader before it's used. Unfortunately this isn't possible since Surface Shaders don't provide any way to do this. But there is a hack!

    Code (csharp):
    1. // likely not needed as if you have another #include above this, it probably includes this, but it must be included before the below code
    2. #include "UnityShaderVariables.cginc"
    3.  
    4. #if defined(UNITY_STEREO_MULTIVIEW_ENABLED) && defined(SHADER_STAGE_FRAGMENT)
    5. // OVR_multiview
    6. // In order to convey this info over the DX compiler, we wrap it into a cbuffer.
    7. #define UNITY_DECLARE_MULTIVIEW(number_of_views) GLOBAL_CBUFFER_START(OVR_multiview) uint gl_ViewID; uint numViews_##number_of_views; GLOBAL_CBUFFER_END
    8. UNITY_DECLARE_MULTIVIEW(2);
    9. #define UNITY_VIEWID gl_ViewID
    10.  
    11. // override _WorldSpaceCameraPos to not use the unity_StereoEyeIndex which we can't set and instead use UNITY_VIEWID which we just defined
    12. #undef _WorldSpaceCameraPos
    13. #define _WorldSpaceCameraPos unity_StereoWorldSpaceCameraPos[UNITY_VIEWID]
    14. #endif

    Some further info on the bug:
    UNITY_VIEWID
    is defined the vertex shader, and
    unity_StereoEyeIndex
    is just a macro for
    UNITY_VIEWID
    , which is in turn a macro for
    gl_ViewID
    . But Unity's code only defines that macro in the vertex shader. For reasons unknown to me, if it's the fragment shader it is not set and
    unity_StereoEyeIndex
    is assumed to come from the stereo instance ID that's passed from the vertex shader to the fragment shader. The issue here is the shader isn't instanced, and even then Surface Shaders don't pass the instance ID to the vertex shader unless there's an instanced property in the
    surf
    function, so there's no isntance ID to use, so it just gets set to 0. However,
    gl_ViewID
    is available in both the vertex and fragment shader stages, Unity just chose not to use it in their code that way. The snippet above fixes that.

    Note, any other camera properties that are similarly using the
    unity_StereoEyeIndex
    in the fragment shader will be similarly broken, and would need to be
    #undef
    ed and
    #define
    d to use
    UNITY_VIEWID
    instead.
     
    Last edited: Sep 22, 2020
  3. Eclectus

    Eclectus

    Joined:
    Oct 29, 2012
    Posts:
    20
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    This thread is explicitly about Surface Shaders, which already call those macros under the hood in some cases, and otherwise don't do anything in the functions exposed to users when they're not. The bug is they were not called when using
    IN.viewDir
    , even though it was needed as Surface Shaders calculate the view direction in the fragment shader. And the above code fixes the issue for the Quest in an efficient way that doesn't require passing the stereo ID via the interpolators.

    Those macros are useful for a vertex fragment shader, though I ended up using the same fix I proposed for Surface Shaders instead for some of my own vertex fragment shaders because it does avoid needing the extra interpolator.