Search Unity

Passing _ShadowMapTexture to Material and sampling it OnPostRender

Discussion in 'Shaders' started by svizcay, Apr 5, 2019.

  1. svizcay

    svizcay

    Joined:
    Oct 26, 2015
    Posts:
    28
    Hi,
    I would like to sample the screen space shadow map generated by Unity when I render a full quad in OnPostRender.

    I have a custom shader/material that I set its parameters each frame with material.SetTexture, material.SetFloat, etc and I would like first to pass Unity's internal _ShadowMapTexture to my shader and then sampling from it in the correct way.

    I have read some comments about how to do it but I think the info is not well documented in general or has been deprecated.

    What I have right now, is a very simple shader with the tag: { "RenderType"="Opaque" "LightMode" = "ForwardBase" } (I don't know why i need to set the lightmode tag)

    Then after specifying the pragmas for the vertex and fragment shader and including UnityCG.cginc
    i have the following:
    Code (csharp):
    1.  
    2. #include "Lighting.cginc"
    3. #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight
    4. #pragma multi_compile _ SHADOWS_SCREEN
    5. #include "AutoLight.cginc"
    6. UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture); // I included this because _ShadowMapTexture was still undefined
    7.  
    I don't know if the order matters here or if I'm overriding things.

    then in the v2f struct I added the macro
    SHADOW_COORDS(4)

    in the vertex shader i also added
    TRANSFER_SHADOW(o)

    and in the fragment shader i try to access it with
    tex2D(_ShadowMapTexture, i.uv);

    the result when I check the frame debugger, the texture is not passed to the shader, the tex2D sampling function says there is no matching 2 parameter intrinsic function and with tex2Dproj and passing a float4, i got the same error.

    The only thing that I care is about reading that texture in my own full screen space shader. I'm not trying to write a shader for a shadow caster or to use the shader as a material for a gameObject, just read the value and do some processing with it.

    Thanks in advance
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    During OnPostRender, the light mode isn’t going to matter. Are you drawing a full screen quad using a Blit? If not, why? If you want access to the screen space shadow texture, you need to make a copy of it using a command buffer & a CopyTexture call, then assign that copy to your material with another texture name, or set the existing texture as a global texture during a time the screen space texture is valid (assuming Unity doesn’t clear it before OnPostRender, which it hopefully shouldn’t). After that, in a Blit(), you should be able to sample the texture just be the Blit quad’s UVs.
     
  3. svizcay

    svizcay

    Joined:
    Oct 26, 2015
    Posts:
    28
    For the quad, i set the material's parameters for that frame, I activate its pass and then i set identity matrices to GL's MVP stacks and draw 2 triangles by specifying the vertices in the corners of a quad. I don't know if by using graphics.blit is different but the result should be the same i guess (I used blit in OnRenderImage to pass the resulting rendertexture to the main screen buffer though).

    I'll give it a try to the command buffer. I've never used them before.
    I would like to try the original one though rather than making a copy. For doing "set the existing texture as a global texture", what do you mean? do I need to declare within my custom shader a sampler2D exactly with the same name (_ShadowMapTexture) and it will receive automatically the right texture by unity? or do i need to call Shader.SetGlobalTexure? if that's the case, I don't have the Texture reference to pass as second parameter.

    Thank you very much for your help
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    Yep, it is effectively the same as a Blit(). If you're doing it manually, is suggest using a single full screen triangle instead of a quad. Slightly more efficient, but not really required.

    The command buffer version of SetGlobalTexture does not actually take a texture object as one of its inputs.
    https://docs.unity3d.com/ScriptReference/Rendering.CommandBuffer.SetGlobalTexture.html

    Instead it takes a "render target identifier". Several built in textures have identifiers you can use directly, like for the depth texture or gbuffers, etc.
    https://docs.unity3d.com/ScriptReference/Rendering.BuiltinRenderTextureType.html

    The screen space shadow texture is not one of these, but you can run a command buffer while the screen space shadow texture is the "current active" render target with the right event.
    https://docs.unity3d.com/ScriptReference/Rendering.LightEvent.AfterScreenspaceMask.html

    Now, you'll want to use a texture property name that's not the same as the built in one, as Unity intentionally clears that property after rendering the opaque queue. So use "_MyShadowTexture" or something like that. Make a command buffer which only sets the global texture of that name using the current active render target, and add the command buffer to the light on the appropriate event. Then you should be able to sample it using your quad's UVs during your psuedo blit.
     
  5. svizcay

    svizcay

    Joined:
    Oct 26, 2015
    Posts:
    28
    Thank you so much bgolus!

    So what I did was the following:
    * Create a rendertarget having the same resolution and format as the screen space shadow map (Camera.main.pixelWidht, Camera.main.pixelHeight, ARGB32)
    * Create a commandBuffer on OnEnable and add it to the main direction light's LightEvent.AfterScreenspaceMask event.
    * On Update (not sure if this is the right event to do it), i clear the commandBuffer and submit a CopyTexture command passing BuiltinRenderTexturetype.CurrentActive as first parameter and my rendertarget as second one.

    The only problem is that I get an error when I play the scene from the editor. Somewhere in between Update and OnPostRender, when the CopyTexture gets execute, I got an error saying that there is a mismatch in the texture size. The source rendertarget has a resolution that I think it matches the one of the editor's scene view. There is no error when I build the app.

    Is there any way to get rid off of this error?

    Thanks again!
     
  6. flogelz

    flogelz

    Joined:
    Aug 10, 2018
    Posts:
    142
    I'm just posting this here again! So I was also searching for a way to sample the shadow texture with a commandbuffer and noticed, that they use this technique in the 3D GameKit from Unity! And it just seems to work straight out of the box this way! I saw many questions about this and nobody ever posted his working code, so here ya go!

    Code that should be attached to the main directional light:
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3. using UnityEngine.Rendering;
    4.  
    5. namespace Gamekit3D
    6. {
    7.     [ExecuteInEditMode]
    8.     public class CopyShadowMap : MonoBehaviour
    9.     {
    10.         CommandBuffer cb = null;
    11.  
    12.         void OnEnable()
    13.         {
    14.             var light = GetComponent<Light>();
    15.             if (light)
    16.             {
    17.                 cb = new CommandBuffer();
    18.                 cb.name = "CopyShadowMap";
    19.                 cb.SetGlobalTexture("_DirectionalShadowMask", new RenderTargetIdentifier(BuiltinRenderTextureType.CurrentActive));
    20.                 light.AddCommandBuffer(UnityEngine.Rendering.LightEvent.AfterScreenspaceMask, cb);
    21.             }
    22.         }
    23.  
    24.         void OnDisable()
    25.         {
    26.             var light = GetComponent<Light>();
    27.             if (light)
    28.             {
    29.                 light.RemoveCommandBuffer(UnityEngine.Rendering.LightEvent.AfterScreenspaceMask, cb);
    30.             }
    31.         }
    32.     }
    33. }
    And in the shader, access the map likes this:
    Code (CSharp):
    1. sampler2D _DirectionalShadowMask;
    and in the surf/fragment shader:
    Code (CSharp):
    1. float shadowmask = tex2D(_DirectionalShadowMask,screenUV).b;
    Theres also this Macro written there, but I'm not sure for what it's used:
    Code (CSharp):
    1. UNITY_DECLARE_SHADOWMAP(_DirectionalShadowMap);
    (I just didnt used it and it still worked)

    Anyways, I hope this helps someone!
     
    bgolus likes this.
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    That's for defining a shadow map texture object ... which technically the screen space shadow texture is not so can be ignored. You've already defined it as a sampler2D, which is what it is.
     
    flogelz likes this.
  8. flogelz

    flogelz

    Joined:
    Aug 10, 2018
    Posts:
    142
    Ah sweet! Thanks for clearing this up!
     
  9. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    Also, one thing to note about that is that is copying the screen space shadows, not the actual shadow map. This means any object you want to receive shadows must still be rendered as part of the depth texture pass, and any shader you want to be able to actually sample the shadow map will not be able to since, again, that's not the shadow map.

    There's an excellent interior mapping example released on github which actually grabs the real main directional shadow map and allows transparent objects to sample from it using a custom shader.
    https://github.com/Gaxil/Unity-InteriorMapping
     
    Masaka_Yogi and flogelz like this.
  10. flogelz

    flogelz

    Joined:
    Aug 10, 2018
    Posts:
    142
    Two usecases would be a custom Imageffect or removing one reason to use a custom lighting function, since you now have access to the attenuation value outside of the function.

    And this one looks super interesting! Gotta take a look into that!
     
  11. flogelz

    flogelz

    Joined:
    Aug 10, 2018
    Posts:
    142
    Bgolus...how. Just how-
    ShadowsOnTransparent_1.png
     
  12. flogelz

    flogelz

    Joined:
    Aug 10, 2018
    Posts:
    142
    It just works- I kind of can't process, that this is just working- and just with two lines of code in the surface shader- (Yes, the main stuff gets calculated in a custom shadow.cginc, but still, this is so easy to setup-) THIS IS AMAZING!!
     
    AlejUb likes this.
  13. flogelz

    flogelz

    Joined:
    Aug 10, 2018
    Posts:
    142
    I was testing different stuff, because it just seems too good to be true, but i didn't catch any downside until now!?
    And Transparent Object can throw shadows on transparent Objects and you can still see the Shadows of each of them, when you look through them in a row!!!!! AAAAHHH //fangirling
     
  14. flogelz

    flogelz

    Joined:
    Aug 10, 2018
    Posts:
    142
    But what are the downsides?? Doesn't this surpass Unitys build-in shadows by length??
     
  15. flogelz

    flogelz

    Joined:
    Aug 10, 2018
    Posts:
    142
    Ok, so the one thing, thats missing in comparison to Unitys build in system, are semitransparent shadows, but this could probably be solved with a dither in the shadowpass, if this were a problem.
     
  16. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    The downside is it can be slow. The whole reason Unity's shadows work they way they do is because it was more efficient to render the shadows that way, both for opaque shadow receiving, and in keeping transparent shader simpler. This is (potentially significantly) increasing the cost of transparent shaders that use it due to the additional texture samples and code. There's no technical limitation preventing shaders from directly sampling a shadow map, indeed any shadow casting point, spot, or additional direction light on opaque object sample their light's shadow maps directly in the shader rather than using the screen space shadow mask approach.

    Of course, this also only works with the main directional light. The only reason Unity never allowed shadow casting on transparent surfaces to begin with was because they choose to disable it for said performance reasons. That code works because Unity stopped clearing some values they used to clear in older versions of Unity, AFAIK it would not have work in Unity 5.4 for example. It seems when they made some optimizations to the backend they stopped clearing those values after the shadow gathering pass.
     
  17. AlejUb

    AlejUb

    Joined:
    Mar 11, 2019
    Posts:
    25
    Thanks a lot
    Spent a full day hitting roadblocks on this and the fact that the light directional lights use the screenmask event flew by me completely.
    Can finally either sample the Depth Shadow map from the light or the ShadowScreenMask final filtered shadows on transparent queue, post processing, etc
     
    flogelz likes this.