Search Unity

Rendering Object Thickness/Volume with Two-Pass Shader

Discussion in 'Shaders' started by StickFigs, Sep 15, 2015.

  1. StickFigs

    StickFigs

    Joined:
    Apr 3, 2014
    Posts:
    23
    I'm trying to implement this method for rendering thick glass into Unity:

    http://prideout.net/blog/?p=51

    ( Note that I do not want Light Absorption or Fresnel in my shader! I'm just trying to do depth for now. )

    My idea was to add a secondary camera to the scene which is parented to the main camera and renders to a RenderTexture using a shader like the one described in the article. I would exclude the glass object layer from the main camera and include only the glass layer in the secondary camera. So the RenderTexture would look something like this:



    Then I would just overlay the RenderTexture result on top of the main camera view with additive blending and color it if I'd like.

    The issue I'm having is with writing the shader. I don't really know what I'm doing when it comes to transforming my vert positions with the matrices, this is what I've cobbled together from other people's code. Here is the best I've got so far:

    ]
    Code (CSharp):
    1. Shader "Thickness"
    2. {
    3.     Properties
    4.     {
    5.     }
    6.  
    7.     SubShader
    8.     {
    9.         Tags{ "RenderType" = "Opaque" }
    10.         //LOD 200
    11.  
    12.         Pass
    13.         {
    14.             Lighting Off
    15.             Fog{ Mode Off }
    16.  
    17.             CGPROGRAM
    18.             #pragma vertex vert
    19.             #pragma fragment frag
    20.             #pragma fragmentoption ARB_precision_hint_fastest
    21.  
    22.             struct a2v
    23.             {
    24.                 float4 vertex : POSITION;
    25.                 fixed4 color : COLOR;
    26.             };
    27.  
    28.             struct v2f
    29.             {
    30.                 float4 pos : SV_POSITION;
    31.                 half dist : TEXCOORD0;
    32.             };
    33.  
    34.             v2f vert(a2v v)
    35.             {
    36.                 v2f o;
    37.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    38.                 float4 temp = mul(UNITY_MATRIX_IT_MV, v.vertex);
    39.                 o.dist = temp.z;
    40.                 return o;
    41.             }
    42.  
    43.             fixed4 frag(v2f i) : COLOR
    44.             {            
    45.                 float depth = i.dist;
    46.                 return half4(depth, depth, depth, 1);
    47.             }
    48.             ENDCG
    49.         }
    50.  
    51.         Pass
    52.         {
    53.             //BlendOp Sub
    54.             //Blend One One
    55.             Lighting Off
    56.             Fog{ Mode Off }
    57.             Cull Front
    58.             ZTest Greater /* This was needed to render the back faces on top of the front faces */
    59.  
    60.             CGPROGRAM
    61.             #pragma vertex vert
    62.             #pragma fragment frag
    63.             #pragma fragmentoption ARB_precision_hint_fastest
    64.  
    65.             struct a2v
    66.             {
    67.                 float4 vertex : POSITION;
    68.                 fixed4 color : COLOR;
    69.             };
    70.  
    71.             struct v2f
    72.             {
    73.                 float4 pos : SV_POSITION;
    74.                 half dist : TEXCOORD0;
    75.             };
    76.  
    77.             v2f vert(a2v v)
    78.             {
    79.                 v2f o;
    80.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    81.                 float4 temp = mul(UNITY_MATRIX_IT_MV, v.vertex);
    82.                 o.dist = temp.z;
    83.                 return o;
    84.             }
    85.  
    86.             fixed4 frag(v2f i) : COLOR
    87.             {
    88.                 float depth = 1 - i.dist;
    89.                 return half4(depth, depth, depth, 1);
    90.             }
    91.             ENDCG
    92.         }
    93.     }
    94.  
    95.     FallBack Off
    96. }

    What I have here is my attempt at a 2-pass shader. First a normal Back face culled pass where the pixels are colored by depth. Then the 2nd pass the Front faces are Culled and the depth reversed. Then you can see I tried to change the blending mode to have the 2nd pass's dark pixels SUBTRACT from the lightness of the first pass's pixels. I was unsuccessful in figuring out how to do this.

    Basically I want the shader to render the front faces colored according to the depth. The closer to the camera the whiter the pixels should be. I also want the depth to be scaled according to the object's boundaries so that the furthest back pixel is pure black and the closest is pure white, I don't know how to do this. I basically want the depth not to be dependent on the camera proximity.

    The second pass should work the same but with front faces culled and the depth inversed so black is closer and white is further away. Then I want this 2nd pass to blend with the first pass in such a way that the white pixels don't change the destination pixel color but the darker pixels should darken the pixel colors of the first pass.

    I'm trying to do exactly what the shader in the article I linked does. So can anyone help me figure out how to pull this together?

    Thanks in advance!
     
  2. chrismarch

    chrismarch

    Joined:
    Jul 24, 2013
    Posts:
    472
    Instead of this
    Code (CSharp):
    1. Tags{"RenderType"="Opaque"}
    try this:
    Code (CSharp):
    1. Tags { "Queue" = "Transparent" }
    Instead of this
    Code (CSharp):
    1. Lighting Off
    try this:
    Code (CSharp):
    1.  
    2. Lighting Off
    3. Blend One One
    4.  
    as described in the shader writing manual, blending section.

    Instead of this
    Code (CSharp):
    1. float4 temp = mul(UNITY_MATRIX_IT_MV, v.vertex);
    2. o.dist = temp.z;
    try this:
    Code (CSharp):
    1.  
    2. COMPUTE_EYEDEPTH(o.dist);
    3.  
    Instead of this
    Code (CSharp):
    1. float depth = 1 - i.dist;
    try this:
    Code (CSharp):
    1. float depth = -i.dist;
    although it might be more optimal, or at least better practice, to do the negation in the vertex program.
     
    Last edited: Sep 17, 2015
  3. StickFigs

    StickFigs

    Joined:
    Apr 3, 2014
    Posts:
    23
    Tried this. The first pass ends up looking better except that the shading is still affected by the camera distance AND angle from the object.

    Problem with the 2nd pass though, it seems to just render solid black:

    Code (CSharp):
    1. Pass
    2.             {
    3.                 Lighting Off
    4.                 Fog{ Mode Off }
    5.                 Cull Front
    6.  
    7.                 CGPROGRAM
    8.                 #pragma vertex vert
    9.                 #pragma fragment frag
    10.                 #pragma fragmentoption ARB_precision_hint_fastest
    11.                 #include "UnityCG.cginc"
    12.  
    13.                 struct a2v
    14.                 {
    15.                     float4 vertex : POSITION;
    16.                     fixed4 color : COLOR;
    17.                 };
    18.  
    19.                 struct v2f
    20.                 {
    21.                     float4 pos : SV_POSITION;
    22.                     half dist : TEXCOORD0;
    23.                 };
    24.  
    25.                 v2f vert(a2v v)
    26.                 {
    27.                     v2f o;
    28.                     o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    29.  
    30.                     COMPUTE_EYEDEPTH(o.dist);
    31.                     return o;
    32.                 }
    33.  
    34.                 fixed4 frag(v2f i) : COLOR
    35.                 {
    36.                     float depth = -i.dist;
    37.                     return half4(depth, depth, depth, 1);
    38.                 }
    39.                 ENDCG
    40.             }
    I think the camera distance thing can be fixed by making the depth values linear? I don't know how to do this though.
     
  4. chrismarch

    chrismarch

    Joined:
    Jul 24, 2013
    Posts:
    472
    you'll need Blend One One on the second pass if you want the additive effect.

    for the linear depth values, you can try passing the depth to LinearEyeDepth (if you want to see what the macros do, download the built in shaders zip from unity for your version, and look CGIncludes\UnityCG.cginc for LinearEyeDepth.)
     
  5. chrismarch

    chrismarch

    Joined:
    Jul 24, 2013
    Posts:
    472
    However, if you read the built in shader code, it is common to compare the result of COMPUTE_EYEDEPTH from a vertex program and sent to the frag program with a LinearEyeDepth from a depth texture, so I suspect COMPUTE_EYEDEPTH is already linear, and that is what I do in my shaders (I don't pass the value of COMPUTE_EYEDEPTH into LinearEyeDepth, I just assume it is already linear.)
     
  6. StickFigs

    StickFigs

    Joined:
    Apr 3, 2014
    Posts:
    23
    If it's true that COMPUTE_EYEDEPTH makes the result linear then why does my camera distance still affect the shader?

    Here's what the result is, showing just the first pass as an example:

     
  7. chrismarch

    chrismarch

    Joined:
    Jul 24, 2013
    Posts:
    472
    If I understand your gif correctly, and you are moving the camera closer and farther from the geometry rendered, then I would expect the eye depth value to change, as it reflects the distance from the vertex to the camera in view space. So, if you look at only one pass while you move the camera, it will change.

    However, if you succeed with both passes, the result should be the difference between the back and front face depths, and then it would not appear to be different when you move the camera
     
  8. StickFigs

    StickFigs

    Joined:
    Apr 3, 2014
    Posts:
    23
    I get the same result as the GIF even if I enable the second pass. Here's what my shader looks like right now:

    Code (CSharp):
    1. Shader "Thickness"
    2. {
    3.     Properties
    4.     {
    5.     }
    6.  
    7.     SubShader
    8.     {
    9.         Tags{ "Queue" = "Transparent" }
    10.  
    11.         Pass
    12.         {
    13.             Lighting Off
    14.             Fog{ Mode Off }
    15.  
    16.             CGPROGRAM
    17.             #pragma vertex vert
    18.             #pragma fragment frag
    19.             #pragma fragmentoption ARB_precision_hint_fastest
    20.             #include "UnityCG.cginc"
    21.  
    22.             struct a2v
    23.             {
    24.                 float4 vertex : POSITION;
    25.                 fixed4 color : COLOR;
    26.             };
    27.  
    28.             struct v2f
    29.             {
    30.                 float4 pos : SV_POSITION;
    31.                 half dist : TEXCOORD0;
    32.             };
    33.  
    34.             v2f vert(a2v v)
    35.             {
    36.                 v2f o;
    37.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    38.  
    39.                 COMPUTE_EYEDEPTH(o.dist);
    40.  
    41.                 return o;
    42.             }
    43.  
    44.             fixed4 frag(v2f i) : COLOR
    45.             {
    46.                 float depth = i.dist;
    47.                 return half4(depth, depth, depth, 1);
    48.             }
    49.                 ENDCG
    50.         }
    51.  
    52.         Pass
    53.             {
    54.                 Lighting Off
    55.                 Fog{ Mode Off }
    56.                 Cull Front
    57.                 Blend One One
    58.                 ZTest Always
    59.  
    60.                 CGPROGRAM
    61. #pragma vertex vert
    62. #pragma fragment frag
    63. #pragma fragmentoption ARB_precision_hint_fastest
    64. #include "UnityCG.cginc"
    65.  
    66.                 struct a2v
    67.                 {
    68.                     float4 vertex : POSITION;
    69.                     fixed4 color : COLOR;
    70.                 };
    71.  
    72.                 struct v2f
    73.                 {
    74.                     float4 pos : SV_POSITION;
    75.                     half dist : TEXCOORD0;
    76.                 };
    77.  
    78.                 v2f vert(a2v v)
    79.                 {
    80.                     v2f o;
    81.                     o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    82.  
    83.                     COMPUTE_EYEDEPTH(o.dist);
    84.  
    85.                     return o;
    86.                 }
    87.  
    88.                 fixed4 frag(v2f i) : COLOR
    89.                 {
    90.                     float depth = -i.dist;
    91.                     return half4(depth, depth, depth, 1);
    92.                 }
    93.                     ENDCG
    94.             }
    95.     }
    96.  
    97.     FallBack Off
    98. }
     
  9. chrismarch

    chrismarch

    Joined:
    Jul 24, 2013
    Posts:
    472
    what's your rendertexture format? You'll need a floating point version for this technique, as the author described in the page you linked. RenderTextureFormat.RHalf is probably optimal, or RFloat, either way just render to red
     
  10. StickFigs

    StickFigs

    Joined:
    Apr 3, 2014
    Posts:
    23
    I'm not even using a rendertexture at this point. It's just the shader applied to a normal sphere primitive. The material is just the shader.
     
  11. chrismarch

    chrismarch

    Joined:
    Jul 24, 2013
    Posts:
    472
    oh ok, well negative colors won't work without a floating point render target:
    I'm not sure if this is sufficient, but you could try turning on HDR on your cameras, as a quick test, and see if that floating point buffer is going to work with this technique. The intent of the author on the page you linked though was to use a rendertexture format like RHalf or RFloat
     
  12. StickFigs

    StickFigs

    Joined:
    Apr 3, 2014
    Posts:
    23
    I turned HDR on with both cameras, separated the sphere object to its own display layer, rendered the second camera to a RFloat render texture, and here was the result:



    Same problem as before except now it's red instead of white. When I move the camera closer the center of the sphere becomes more transparent like in the GIF which is the opposite of what I would expect. I would expect the center to be more opaque.
     
  13. chrismarch

    chrismarch

    Joined:
    Jul 24, 2013
    Posts:
    472
    The inspector won't be able to draw the RFloat or RHalf depth difference texture in the colors you expect. You have to decode it with an image effect, as in the blog post. Here is screen of a demo project I put together, note the camera that renders to the RHalf texture clears solid color to black. Also, once I had things setup correctly, I didn't see any red in the inspector for the RHalf texture:


    In the project view of the screen above, you can see a few assets you can use to get these effects. Here is the image effect that goes on your main camera:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. [ExecuteInEditMode]
    4. public class ThickGlassImageEffect : MonoBehaviour {
    5.     public RenderTexture thicknessRenderTexture;
    6.     public Material material;
    7.     void Awake () { material.SetTexture("_ThicknessTex", thicknessRenderTexture); }
    8.    
    9.     // Postprocess the image
    10.     void OnRenderImage (RenderTexture source, RenderTexture destination) { Graphics.Blit (source, destination, material); }
    11. }
    here is the shader the image effect uses:
    Shader "ThickGlassImageEffect"
    {
    Properties
    {
    _ThicknessTex("Thickness Texture", 2D) = "white" {}
    _Color("Color", Color) = (1, 1, 1, 1)
    _Sigma("Sigma", Float) = 1.0
    }

    SubShader
    {
    // No culling or depth
    Cull Off ZWrite Off ZTest Always

    Pass
    {
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"

    struct appdata
    {
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    };

    struct v2f
    {
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    };

    v2f vert (appdata v)
    {
    v2f o;
    o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
    o.uv = v.uv;

    // see http://docs.unity3d.com/Manual/SL-PlatformDifferences.html
    #if UNITY_UV_STARTS_AT_TOP
    o.uv.y = 1 - o.uv.y;
    #endif

    return o;
    }

    sampler2D _ThicknessTex;
    fixed3 _Color;
    float _Sigma;

    fixed4 frag (v2f i) : SV_Target
    {
    // adapted from http://prideout.net/blog/?p=51
    float thickness = abs(tex2D(_ThicknessTex, i.uv).r);
    if (thickness <= 0.0)
    {
    discard;
    }

    float intensity = exp(-_Sigma * thickness);
    fixed4 col = fixed4(intensity * _Color, 1);

    return col;
    }
    ENDCG
    }
    }
    }
     
  14. Flailer

    Flailer

    Joined:
    Apr 1, 2014
    Posts:
    66
  15. StickFigs

    StickFigs

    Joined:
    Apr 3, 2014
    Posts:
    23
    I will try this ASAP. Although I noticed that your Game view is showing the same problem with the shader as my shader where the thicker parts of the mesh are being rendered as darker and the thinner parts are lighter. Given the way the passes are written its expected that the opposite would be true?

    I read this before while looking for a depth rendering technique but it won't work for my use case because this technique requires that you have pre-baked local thickness maps. I can't do this because my shader is going to be applied to dynamic meshes which change shape at runtime.
     
  16. chrismarch

    chrismarch

    Joined:
    Jul 24, 2013
    Posts:
    472
  17. chrismarch

    chrismarch

    Joined:
    Jul 24, 2013
    Posts:
    472
    Yes! It is inverted after you use the sigma step. If you just render the thickness in the image effect shader, the thicker parts are lighter, similar to the blog post.
     
  18. StickFigs

    StickFigs

    Joined:
    Apr 3, 2014
    Posts:
    23
    After starting over from scratch in a new project I managed to get it to work!

    Some notes:
    • HDR was NOT required on either camera.
    • For some reason disabling antialiasing in project quality settings causes the composited main camera view to not render correctly. It just shows the rendertexture and doesnt show the skybox or any objects in its culling layers.
    • To get the desired effect that I wanted (depth rendered to look like gas or a liquid volume) in the ThickGlassImageEffect shader I changed `return col;` to `return 1 - col;` to invert the output colors. And then I changed the ThickGlassImageEffect to use 'Blend OneMinusDstColor One' (Soft Additive blending).
    And here's the result!:



    Exactly what I was trying to do! Thanks for all the help! I didn't realize how much I was not picking up on from the thick glass technique implementation.
     
    sepee67, glitchers and chrismarch like this.
  19. Reanimate_L

    Reanimate_L

    Joined:
    Oct 10, 2009
    Posts:
    2,788
    Sorry for replying to old topic,
    But you can also use VFACE to get object thickness directly in one pass just using the same technique as in this link
    Screenshot_4.png
     
    glitchers likes this.
  20. StickFigs

    StickFigs

    Joined:
    Apr 3, 2014
    Posts:
    23
    @rea Can you explain how to do this?
     
  21. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    If rendering to a target with enough precision and support for negative numbers, you can just output the distance for back faces and -distance for the front faces. Blending one one and backface culling disabled of course. This is probably the simplest approach there is to do it in a single pass. There is no need to convert anything back to linear in this case, so distance is just the distance between the camera and the current pixel in world space. This even takes cavities into account, but needs a perfectly closed model that is not intersected by anything. (Every entry into the models also needs an backface exit.)

    I've also seen approaches that changed the blending operator from add to min/max. Probably min on color and then max on alpha. That would get you the minimum and maximum distance in one pass. Cavities are of course ignored in this case.
     
  22. dreamerflyer

    dreamerflyer

    Joined:
    Jun 11, 2011
    Posts:
    927
    how do u get that?I use your upward codes, not work! depth.jpg
     
  23. Piflik

    Piflik

    Joined:
    Sep 11, 2011
    Posts:
    293
    I know this thread is old, but if anyone is still looking for this shader as a working example, here is my version, using the VFACE semantic mentioned earlier. I use it to visualize X-Ray imaging, that's why my absorption can go negative, to emulate empty space (like lungs). All in all it is a good approximation for X-Ray.

    Code (CSharp):
    1.  
    2. Shader "Custom/Thickness" {
    3.    
    4.     Properties {
    5.         _Absorption("Absorption", Range(-10, 10)) = 1
    6.     }
    7.    
    8.     SubShader {
    9.         Tags {
    10.             "Queue" = "Transparent"
    11.             "IgnoreProjector" = "True"
    12.             "RenderType" = "Transparent"
    13.         }
    14.  
    15.        
    16.         Pass {
    17.             Cull Off
    18.             Blend One One
    19.             Lighting Off
    20.             ZWrite Off
    21.             Fog { Mode Off }
    22.            
    23.             CGPROGRAM
    24.             #pragma vertex vert
    25.             #pragma fragment frag
    26.  
    27.             #include "UnityCG.cginc"
    28.  
    29.             struct a2v {
    30.                 float4 vertex : POSITION;
    31.             };
    32.  
    33.             struct v2f {
    34.                 float4 pos : SV_POSITION;
    35.                 half dist : TEXCOORD0;
    36.             };
    37.  
    38.             v2f vert(a2v v) {
    39.                 v2f o;
    40.                 o.pos = UnityObjectToClipPos(v.vertex);
    41.                 COMPUTE_EYEDEPTH(o.dist);
    42.                 return o;
    43.             }
    44.  
    45.             float _Absorption;
    46.  
    47.             fixed4 frag(v2f i, fixed facing : VFACE) : COLOR {
    48.                 float depth = -_Absorption * sign(facing) * i.dist;
    49.                 return half4(depth, depth, depth, 1);
    50.             }
    51.             ENDCG
    52.         }
    53.     }
    54. }
     
    smortBlob likes this.
  24. smortBlob

    smortBlob

    Joined:
    Oct 26, 2021
    Posts:
    5
    I freakin love you!!!
    Thank you so much. You won't believe how long i was looking for this.
    I knew it was something very simple but unfortunately i was not able to achieve it.

    Regarding to the negative Absorbtion, I have it the other way around. The thick areas are represented dark
     
    Last edited: Apr 14, 2022