Search Unity

Outline Shader in VR

Discussion in 'Scripting' started by skowronski, Jan 17, 2018.

  1. skowronski

    skowronski

    Joined:
    Jan 17, 2018
    Posts:
    28
    Hi,
    I have a problem with implementing an outline shader (based on this tutorial: https://willweissman.wordpress.com/tutorials/shaders/unity-shaderlab-object-outlines/) in VR. What is basically does is render the scene to a texture, then render selected objects to another texture and outline those and at the end combine both textures.

    When running the game without an attached Vive, everything displays correctly:

    As you can see the block in the front is correctly outlined.

    When I run it in VR, two things happen:
    1) When I look at the selected object I see the outline twice
    2) If I look away from the object, I see the outline only once, but at the wrong position.

    This seems to be due to the shader being performed twice (maybe only on the screen space?) and therefore not correctly displaying the shader in VR. Can anyone clarify and explain how something like this can be solved? Here's my sample code:
    Script attached to the camera:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. public class PostEffect : MonoBehaviour
    4. {
    5.     Camera AttachedCamera;
    6.     public Shader Post_Outline;
    7.     public Shader DrawSimple;
    8.     Camera TempCam;
    9.     Material Post_Mat;
    10.     void Start ()
    11.     {
    12.         AttachedCamera = GetComponent<Camera>();
    13.         TempCam = new GameObject().AddComponent<Camera>();
    14.         TempCam.enabled = false;
    15.         Post_Mat = new Material(Post_Outline);
    16.     }
    17.     void OnRenderImage(RenderTexture source, RenderTexture destination)
    18.     {
    19.         //set up a temporary camera
    20.         TempCam.CopyFrom(AttachedCamera);
    21.         TempCam.clearFlags = CameraClearFlags.Color;
    22.         TempCam.backgroundColor = Color.black;
    23.         //cull any layer that isn't the outline
    24.         TempCam.cullingMask = 1 << LayerMask.NameToLayer("Outline");
    25.         //make the temporary rendertexture
    26.         RenderTexture TempRT = new RenderTexture(source.width, source.height, 0, RenderTextureFormat.ARGB32);
    27.         //put it to video memory
    28.         TempRT.Create();
    29.         //set the camera's target texture when rendering
    30.         TempCam.targetTexture = TempRT;
    31.        
    32.         //flip the camera, since VR image is somehow upside down
    33.         TempCam.ResetWorldToCameraMatrix();
    34.         TempCam.ResetProjectionMatrix();
    35.         TempCam.projectionMatrix = TempCam.projectionMatrix * Matrix4x4.Scale(new Vector3(1, -1, 1));
    36.  
    37.         //render all objects this camera can render, but with our custom shader.
    38.         TempCam.RenderWithShader(DrawSimple,"");
    39.  
    40.         Graphics.Blit(source, destination);
    41.         //copy the temporary RT to the final image
    42.         Graphics.Blit(TempRT, destination, Post_Mat);
    43.         //release the temporary RT
    44.         TempRT.Release();
    45.     }
    46. }
    and this is the code that just renders the selected object in white to be outlined later. Here the effect is already visible:

    Code (CSharp):
    1. //This shader goes on the objects themselves. It just draws the object as white, and has the "Outline" tag.
    2. Shader "Custom/DrawSimple"
    3. {
    4.     SubShader
    5.     {
    6.         ZWrite Off
    7.         ZTest Always
    8.         Lighting Off
    9.         Pass
    10.         {
    11.             CGPROGRAM
    12.             #pragma vertex VShader
    13.             #pragma fragment FShader
    14.             struct VertexToFragment
    15.             {
    16.                 float4 pos:POSITION;
    17.             };
    18.             //just get the position correct
    19.             VertexToFragment VShader(VertexToFragment i)
    20.             {
    21.                 VertexToFragment o;
    22.                 o.pos=UnityObjectToClipPos(i.pos);
    23.                 return o;
    24.             }
    25.             //return white
    26.             fixed4 FShader():COLOR0
    27.             {
    28.                 return fixed4(1,1,1,1);
    29.             }
    30.             ENDCG
    31.         }
    32.     }
    33. }
    and lastly the post-processing that is used to outline the created white objects:

    Code (CSharp):
    1. Shader "Custom/Post Outline"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex("Main Texture",2D)="white"{}
    6.     }
    7.     SubShader
    8.     {
    9.     Blend SrcAlpha OneMinusSrcAlpha
    10.         Pass
    11.         {
    12.             CGPROGRAM
    13.    
    14.             sampler2D _MainTex;
    15.             //<SamplerName>_TexelSize is a float2 that says how much screen space a texel occupies.
    16.             float2 _MainTex_TexelSize;
    17.             #pragma vertex vert
    18.             #pragma fragment frag
    19.             #include "UnityCG.cginc"
    20.            
    21.             struct v2f
    22.             {
    23.                 float4 pos : POSITION;
    24.                 float2 uvs : TEXCOORD0;
    25.             };
    26.            
    27.             v2f vert (appdata_base v)
    28.             {
    29.                 v2f o;
    30.                
    31.                 //Despite the fact that we are only drawing a quad to the screen, Unity requires us to multiply vertices by our MVP matrix, presumably to keep things working when inexperienced people try copying code from other shaders.
    32.                 o.pos = UnityObjectToClipPos(v.vertex);
    33.                
    34.                 //Also, we need to fix the UVs to match our screen space coordinates. There is a Unity define for this that should normally be used.
    35.                 o.uvs = o.pos.xy / 2 + 0.5;
    36.                
    37.                 return o;
    38.             }
    39.            
    40.            
    41.             half4 frag(v2f i) : COLOR
    42.             {
    43.                 //arbitrary number of iterations for now
    44.                 int NumberOfIterations=5;
    45.                 //split texel size into smaller words
    46.                 float TX_x=_MainTex_TexelSize.x;
    47.                 float TX_y=_MainTex_TexelSize.y;
    48.                 //and a final intensity that increments based on surrounding intensities.
    49.                 float ColorIntensityInRadius;
    50.                 //if something already exists underneath the fragment, discard the fragment.
    51.                 if(tex2D(_MainTex,i.uvs.xy).r>0)
    52.                 {
    53.                     discard;
    54.                 }
    55.                 //for every iteration we need to do horizontally
    56.                 for(int k=0;k<NumberOfIterations;k+=1)
    57.                 {
    58.                     //for every iteration we need to do vertically
    59.                     for(int j=0;j<NumberOfIterations;j+=1)
    60.                     {
    61.                         //increase our output color by the pixels in the area
    62.                         ColorIntensityInRadius+=tex2D(
    63.                                                       _MainTex,
    64.                                                       i.uvs.xy+float2
    65.                                                                    (
    66.                                                                         (k-NumberOfIterations/2)*TX_x,
    67.                                                                         (j-NumberOfIterations/2)*TX_y
    68.                                                                    )
    69.                                                      ).r;
    70.                     }
    71.                 }
    72.                 //output some intensity of teal
    73.                 return ColorIntensityInRadius*half4(0,1,1,1);
    74.             }
    75.            
    76.             ENDCG
    77.         }
    78.         //end pass      
    79.     }
    80.     //end subshader
    81. }
    82. //end shader
     
  2. Boz0r

    Boz0r

    Joined:
    Feb 27, 2014
    Posts:
    419
    How would you expect this to look? In VR you've got two separate screen-spaces, from two different angles, so the outline won't be the same.

    EDIT: Generally, most screen-space effects don't really work properly in stereoscopic 3D.
     
    Last edited: Jan 17, 2018
  3. skowronski

    skowronski

    Joined:
    Jan 17, 2018
    Posts:
    28
    I would expect it to look as it does in the screenshot in my post. However, in VR, if I look at the outlined object, the outline appears twice (once for left and right eye) with a slight offset from the object
     
  4. ThermalFusion

    ThermalFusion

    Joined:
    May 1, 2011
    Posts:
    906
    Are you using any of the single pass or single pass instanced VR techniques?
    They need your shaders to have additional properties to function in this mode.

    Does the behaviour change if you do not touch the camera matrix? I've experienced that touching the matrix breaks the vr functionality of the camera.
     
  5. Boz0r

    Boz0r

    Joined:
    Feb 27, 2014
    Posts:
    419
    Does the effect look correct for each individual eye?
     
  6. skowronski

    skowronski

    Joined:
    Jan 17, 2018
    Posts:
    28
    In the XR settings I've chosen multipass. Is there anything that else that could be set wrong?


    Will check both of that later
     
  7. Boz0r

    Boz0r

    Joined:
    Feb 27, 2014
    Posts:
    419
    Cool. Did my point about the problem of screen-space effects in 3D make sense?

    EDIT: I made a really pretty illustration in Paint:



    The red bars represent the outline of the box. In 2D it works fine because there's only one camera, and thus only one screen-space. In 3D there's two cameras and two screen-spaces from two different angles. Thus, the outline gets drawn from two different angles. This affects most screen-space effects.
     
    Last edited: Jan 18, 2018
    adhocdown and whileBreak like this.
  8. skowronski

    skowronski

    Joined:
    Jan 17, 2018
    Posts:
    28
  9. krasner

    krasner

    Joined:
    Sep 9, 2014
    Posts:
    7
    Hey, I found a way to make it work. On Unity 2018.2.
    The following changes are needed:

    1) Don't flip the camera in your PostEffect.cs

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. public class PostEffect : MonoBehaviour
    4. {
    5.     Camera AttachedCamera;
    6.     public Shader Post_Outline;
    7.     public Shader DrawSimple;
    8.     Camera TempCam;
    9.     Material Post_Mat;
    10.  
    11.     void Start()
    12.     {
    13.         AttachedCamera = GetComponent<Camera>();
    14.         Post_Outline = Shader.Find("Custom/Post Outline");
    15.         DrawSimple = Shader.Find("Custom/DrawSimple");
    16.         TempCam = new GameObject().AddComponent<Camera>();
    17.         //set up a temporary camera
    18.         TempCam.CopyFrom(AttachedCamera);
    19.         TempCam.clearFlags = CameraClearFlags.Color;
    20.         TempCam.backgroundColor = Color.black;
    21.         TempCam.renderingPath = RenderingPath.Forward;
    22.         //cull any layer that isn't the outline
    23.         TempCam.cullingMask = 1 << LayerMask.NameToLayer("Outline");
    24.  
    25.         TempCam.gameObject.transform.parent = AttachedCamera.transform;
    26.         TempCam.allowHDR = false;
    27.         TempCam.enabled = false;
    28.         Post_Mat = new Material(Post_Outline);
    29.     }
    30.     void OnRenderImage(RenderTexture source, RenderTexture destination)
    31.     {
    32.         //make the temporary rendertexture
    33.         RenderTexture TempRT = new RenderTexture(source.width, source.height, 0, RenderTextureFormat.ARGB32);
    34.         //put it to video memory
    35.         TempRT.Create();
    36.         //set the camera's target texture when rendering
    37.         TempCam.targetTexture = TempRT;
    38.  
    39.         //render all objects this camera can render, but with our custom shader.
    40.         TempCam.RenderWithShader(DrawSimple, "");
    41.  
    42.         Graphics.Blit(source, destination);
    43.         //copy the temporary RT to the final image
    44.         Graphics.Blit(TempRT, destination, Post_Mat);
    45.         //release the temporary RT
    46.         TempRT.Release();
    47.     }
    48. }
    2. In the Post Outline shader frag function, use UnityStereoTransformScreenSpaceTex() function to apply the necessary transformation for each eye to the rendered texture. I modified the shader by using the #pragma vertex vert_img also, but the frag function is the important change.

    Code (CSharp):
    1. Shader "Custom/Post Outline"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex("Main Texture",2D)="white"{}
    6.     }
    7.     SubShader
    8.     {
    9.     Blend SrcAlpha OneMinusSrcAlpha
    10.         Pass
    11.         {
    12.             CGPROGRAM
    13.  
    14.  
    15. #pragma vertex vert_img
    16. #pragma fragment frag
    17. #include "UnityCG.cginc"
    18.                      
    19.             sampler2D _MainTex;
    20.             half4 _MainTex_ST;
    21.             //<SamplerName>_TexelSize is a float2 that says how much screen space a texel occupies.
    22.             float2 _MainTex_TexelSize;
    23.          
    24.             half4 frag(v2f_img i) : SV_Target
    25.             {
    26.                 //arbitrary number of iterations for now
    27.                 int NumberOfIterations=2;
    28.                 //split texel size into smaller words
    29.                 float TX_x=_MainTex_TexelSize.x;
    30.                 float TX_y=_MainTex_TexelSize.y;
    31.                 //and a final intensity that increments based on surrounding intensities.
    32.                 float ColorIntensityInRadius;
    33.  
    34.                 fixed4 original = tex2D(_MainTex, UnityStereoTransformScreenSpaceTex(i.uv));
    35.  
    36.                 //if something already exists underneath the fragment, discard the fragment.
    37.                 if(original.r>0)
    38.                 {
    39.                     discard;
    40.                 }
    41.                 //for every iteration we need to do horizontally
    42.                 for(int k=0;k<NumberOfIterations;k+=1)
    43.                 {
    44.                     //for every iteration we need to do vertically
    45.                     for(int j=0;j<NumberOfIterations;j+=1)
    46.                     {
    47.                         float4 radiusTex = tex2D(_MainTex,  UnityStereoTransformScreenSpaceTex(i.uv + float2
    48.                                                                    (
    49.                                                                         (k-NumberOfIterations/2)*TX_x,
    50.                                                                         (j-NumberOfIterations/2)*TX_y
    51.                                                                    )
    52.                                                      ));
    53.                         //increase our output color by the pixels in the area
    54.                         ColorIntensityInRadius+=radiusTex;
    55.                     }
    56.                 }
    57.                 //output some intensity of teal
    58.                 return ColorIntensityInRadius*half4(0,1,1,1);
    59.             }
    60.          
    61.             ENDCG
    62.         }
    63.         //end pass    
    64.     }
    65.     //end subshader
    66. }
    67. //end shader
    Really hope this helps!
     
  10. krasner

    krasner

    Joined:
    Sep 9, 2014
    Posts:
    7
    It's not letting me edit my comment, but to add to the above, it's using Single Pass stereo rendering..
     
  11. dantair

    dantair

    Joined:
    Apr 8, 2018
    Posts:
    1
    Hola, tengo el mismo problema pero con este tutorial
    .

    cuando lo juego y tengo el oculus conectado, me muestra una imagen en cada ojo. ¿Alguna solución?
     
  12. R1PFake

    R1PFake

    Joined:
    Aug 7, 2015
    Posts:
    540
    The Oculus pack contains a outline shader (it's in the distance grab example folder). The code will work with any VR headset. Im not sure if you are "allowed" to use the code for any other headsets, but im guess they will not care if you copy one of their example pack shaders, so you can just download the package and uncheck everything but the outline shader.