Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Custom Shadow Mapping to mask out rain effects from the top down

Discussion in 'Shaders' started by Mister-Royzo, Jun 3, 2023.

  1. Mister-Royzo

    Mister-Royzo

    Joined:
    Apr 2, 2013
    Posts:
    11
    Hello! I'm creating some rain effects for my game, and I'm trying to achieve an interesting effect. I want the environment to get wet and shiny in the rain, and to stay dry underneath overhangs. Have a look at this:

    upload_2023-6-3_14-34-17.png

    As you can see I have something that sort of works, it's just a bit duct-taped together and I'd like to optimize it / make it handle some edge cases I have. Basically I want to make the system more robust.

    My game is using Deferred Rendering and the Built-in Render Pipeline. Here's my current approach: I have a dedicated rain camera that is orthographic and renders the scene from the top down into a Render Texture that only has the Red channel. I use the Replacement Shaders feature to make it render the environment with a custom "height" shader that returns the world-space height as a value from 0 to 1, with 0 being the rain camera's near clipping plane, and 1 being the camera's far clipping plane.

    Then in a deferred rendering pass (using command buffers) I alter the diffuse, specular and smoothness channels of the GBuffers to appear darker and shinier, to make them look wet. Now here's the tricky part: I get the world position of the fragment (reconstructed from the main camera's depth buffer) and then I use it to sample the rain camera's "height texture". Now I can check: is this world position lower than the highest position the rain camera can see? If so, it is "seen" by the rain and should be wet, if not, it is sheltered from the rain and should be dry. Then you end up with a mask like this, as displayed in green:

    upload_2023-6-3_14-47-33.png
    (note the square "shadow" directly underneath the platform. The actual light shadow is slightly offset because the directional light is at an angle rather than straight down)

    Now the observant reader may have noticed that this is basically just shadow mapping, but worse! Primarily because this rain height approach doesn't have the slew of features that Unity implements to filter the texture and make it look nice. Another reason why this approach is worse is because some of my objects use cutout shaders and erode in interesting patterns by discarding fragments. They have custom ShadowCaster passes to make sure this cutout transparency is also respected when its depth is rendered so shadows work correctly.

    So my problem is this: I cannot for the life of me seem to access this second camera's depth texture while I'm rendering the Main Camera and have to resort to rendering it myself, and as you can imagine I'm not doing it nearly as well as the native depth rendering. Ideally this second camera would only render the depth and nothing else. I don't need its color information. Really, for all intents and purposes this rain is a directional light with shadows. For maximum fidelity / compatibility I would like to re-use the approach Unity uses for directional lights as much as possible.

    I've tried getting this second orthographic rain camera to render the scene only using any ShadowCaster passes available in the material's shaders. Having read the documentation on Shader Replacement they mention that (sometimes?) Shader Replacement is used to render depth textures, but I'm not sure how. As far as I understand it all you can do by using Shader Replacement on a camera is render the scene with your specified override shader. What I would need is: if the object has a pass with a LightMode tag with value ShadowCaster, then we render the object using that pass. Otherwise we do not render it. I don't want to specify a shader it should use, it should use the object's original shader. That doesn't seem to be what Shader Replacement does.


    This post is a bit dry and technical so let me show you how nice the effect is that I'm trying to make.

    EDIT: Not sure why the linked .gif doesn't work, it seems to mess with the URL. Here it is though

    Another possible approach I haven't tried yet that I'm thinking of as I'm typing this: maybe disable the rain camera, use command buffers to make it so that before the main camera renders, we explicitly render this rain camera, maybe use command buffers to blit its depth texture (maybe we do have access to it at that time because it's technically the 'active camera' at that time?) to a buffer, and then we use that buffer later during the deferred rain pass. Although this only solves part of the problem because this might then indeed render the depth of the scene but it would presumably also render the colour information, which is costly and useless data. My current approach, hacky as it is, does not bother with colour and only renders height and is presumably a lot faster.

    I would also be willing to compromise and keep using my current setup if I could somehow make it respect the ShadowCaster pass of objects so partially eroded cutout materials correctly let the rain through, though I currently have no idea how I would do that... Could really use some pointers here. Many thanks in advance!


    As a last-ditch effort I will try to summon bgolus:

    upload_2023-6-3_15-22-33.png

    Oh gods of the Unity Forum, hear my plea. Please send me Benjamin Golus in my time of need and channel His knowledge and wisdom into me such that I may render my scene in His image; efficient and well-engineered and using Unity's features to the fullest. Amen.
     
    Olmi likes this.
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,221
    Short answer: there's no perfect solution when using the built in renderer. There's no way to render only the depth texture for an arbitrary camera view with the built in renderer using the existing shadow caster pass!

    Full stop. That's the end. And it sucks. This was a topic that came up several times over the last several years, and the eventual solution that was given was the Scriptable Render Pipeline.

    That hacky feeling solution of using replacement shaders remains the "best" solution, which again sucks.

    The other options are:
    1. Have your secondary camera setup to render from the top, and set the depthTextureMode to render a depth texture. You then have to use a command buffer to copy that to a new texture that you keep around. However you still have to render the whole scene from that point of view! There's no way to only render the depth texture and not the rest. This is obviously extremely wasteful. And you can't use a replacement shader pass with an empty shader to skip this as cameras don't render the depth texture when you render using a replacement shader.

    2. Use a directional light with shadows enabled, use a command buffer to copy the shadow map that's generated and keep it around. However the light has to actually be visible and get used by another camera. You can set it to some extremely low brightness, but you're paying the cost of rendering that light to the scene still. You could use light culling masks to keep it from rendering on everything in the scene, but it'll only render the shadows for objects in the same mask so that's not an option. And you can't use a secondary dummy camera to trick it unless that secondary camera is also doing the full rendering of everything.

    So, those two options are a bust.

    And that brings us back to replacement shaders. The really are your only option. For handling cutout materials, you have to give them a RenderType tag and a matching pass in the replacement shader. If you're doing anything fancy with your shaders (vertex movement, anything more than using the base texture's alpha for the cutout mask) then you'll need a unique RenderType and pass in the replacement shader.

    For soft shadows, most of the heavy lifting is done in the shader that samples the shadow map rather than in the shadow map itself. And it's really not anything more complicated than any other blur shader. Sample the shadow map multiple times with offset positions, and take the average.

    https://github.com/TwoTailsGames/Un...ob/master/CGIncludes/UnityShadowLibrary.cginc

    For something like what you're doing, you might want to look into VSM (variance shadow maps).
    https://github.com/gkjohnson/unity-custom-shadow-experiments

    You can render the replacement shader pass using a lower resolution MSAA pass, blur the resulting texture, and then sample that for surprisingly cheap soft shadows. The downside is there's sometimes some "shadow leaking" on overlapping objects, but that might work out okay for a wetness map.
     
    Olmi likes this.
  3. Mister-Royzo

    Mister-Royzo

    Joined:
    Apr 2, 2013
    Posts:
    11
    An informative and complete answer as always, thanks so much Ben!

    I think I'm going to have to do that custom RenderType you mentioned and I'll also look into those filtering techniques you shared, sounds like the setup will stay a little hacky but I think it'll be a totally workable solution.


    Then there's one more thing I'm curious about: I make stylized games and I like writing my own shaders via code, and I quite like the workflow of the Built-in Render Pipeline, it's pretty elegant. That said, I do sometimes bump into brick walls like this where I wish the rendering pipeline was a bit more extensible. I'm a bit hesitant to make the leap to URP though, it seems very much geared towards shader graph users and my colleagues tell me there's quite a significant overhead on low-end hardware and have cautioned me to stick with BRP if possible (I would like to target the Nintendo Switch).

    In your personal opinion, has URP matured enough to be the best rendering solution for stylized games? Or is it worth sticking with BRP for a few more years, given that edge cases such as this thread are few and far between?

    I wonder what your thoughts are on that.
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,221
    URP has a lot of nice features. But also a lot of annoying aspects. Technically it’s more extensible, but only if you create a copy of URP and edit the code directly.

    I know lots of games are shipping on it now, but so far I’ve stuck with BIRP.
     
  5. Mister-Royzo

    Mister-Royzo

    Joined:
    Apr 2, 2013
    Posts:
    11
    Might stick with BIRP for a bit too then, I've got a pretty nice workflow going now.

    But for the sake of being thorough I'll do a bit of a test case to evaluate URP before I start the next project. Thanks!