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 Implement DDY Drop Shadow/Outline Effect from Bad North

Discussion in 'Shaders' started by hardcorebadger, Nov 22, 2021.

  1. hardcorebadger

    hardcorebadger

    Joined:
    Nov 28, 2016
    Posts:
    31
    Hey there, this is my first dip back into shaders in a couple years. I'm trying to implement a trick outlined here where DDY is used to create a drop shadow / edge / outline-ish effect along the bottom of a color. I know from the video that DDY is used to do this in 1 pass, and I get the basics of what DDY is, but I'm struggling to understand how to put the pieces together. Any help would be greatly appreciated - also agnostic as to how I would build this in either code (which I'm more familiar with) or the new shader graph (which looks cool!)
    Thanks!
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Oskar's stuff is amazing, but he's hand waving away a lot of the implementation details here.

    ddy()
    , and the related function
    ddx()
    and
    fwidth()
    are screen space partial pixel derivative functions. Which is a complicated way of saying it's a function that says how much a value is changing from one pixel to the one next to it. In the case of
    ddy()
    it's the difference between the pixel and the one above or below it.
    ddx()
    is the one to the left or right, and
    fwidth()
    is
    abs(ddx(n)) + abs(ddy(n))
    . Now I say "above or below" and "left or right", and you might be thinking "okay, how can it be or?" All modern GPUs break up the screen into a grid of 2x2 pixels, known as a pixel quad. This is the smallest region a GPU can render; if the GPU is rendering 1 pixel of something, it's always actually rendering 4 pixels in parallel. For Direct3D, calling the
    ddx()
    or
    ddy()
    function returns the difference between the "first" pixel (top left*) and the pixel to its right or bellow it, and all pixels within the pixel quad get that same value. This means the bottom right pixel's value is actually ignored, so the derivatives are quite coarse. There are also
    ddx_fine()
    and
    ddy_fine()
    functions when using shader model 5.0 (DirectX 11,
    #pragma target 5.0
    ) that get the difference between the pixel values for that row or column if you need more fine grained derivatives, as well as
    ddx_coarse()
    and
    ddy_coarse()
    , but these are equivalent to the base derivative functions. For OpenGL, Metal, and Vulkan whether or not the default derivative functions return the fine or coarse derivatives is implementation specific (meaning it can change from GPU to GPU), but generally it is always the fine derivative.


    So, that explains the DDY part of things, but doesn't explain how the effect works. There's no built in shader value you can look at to say if you're close to the edge of some geometry, which is what you'd need. Indeed the fragment shader is running at all 4 pixels within the pixel quad regardless of if the triangle being rendered covers all 4 pixels! The shader has no idea if it's visible or not! What I suspect Oskar actually did was there's a gradient of some kind, maybe a vertex color that's baked into the mesh, that is how "close" to the grass the ground is. Then he's using
    ddy()
    to measure how much the gradient value is changing on the horizontal axis of the screen to change how wide the drawn outline is. Since
    ddy()
    is a signed value, you can also check which direction the gradient is going so it only shows on the bottom edge.


    * Unity does a lot of weird things when rendering, like rendering everything upside down so certain things match how OpenGL renders, then flips the image when finally showing it on screen. Specifically OpenGL texture and screen coordinates start at the bottom left whereas all other graphics APIs, including Direct3D, start at the top left. So the "first" pixel is actually the bottom left one for what appears on screen at the end.
     
    PutridEx likes this.
  3. hardcorebadger

    hardcorebadger

    Joined:
    Nov 28, 2016
    Posts:
    31
    Thanks for the reply and solid detail on the topic! Based on that, I went ahead and tried to sample the albedo texture and pull the DDY. Did some math with that to do a color replacement on the bottom edge of the color.

    Only issue here is, my texture resolution is basically dictating the effect's thickness. It's a simple boundary between color & white. I assume if I wanted a shader property for the thickness I would need some sort of secondary gradient texture to sample where I can adjust the clipping via the property?
     
  4. hardcorebadger

    hardcorebadger

    Joined:
    Nov 28, 2016
    Posts:
    31
    Alright so some progress here. Basically I didn't love the idea of having a whole separate value/mesh data to sample just to do this. So this version uses just the albedo texture, which now is higher resolution with a slightly blurry edge between colors. the gradient on the edge basically gives the DDY some more space to pick up value from. After that you just push that initial DDY value through some clamping and multiplication to get the desired power of darkening. You can interestingly also invert the property to get an internal box shadow (just like a drop shadow in css!)



    This is cool but the texture still basically controls the inevitable thickness. I'm still not 100% convinced this is the way Oskar is doing it. But if anyone wants to continue the investigation with me I found another clue here - I'm not quite understanding what he's saying, but he has some more detailed math based on the texture drop-off in the comments to this tweet
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    The texture controls the max thickness, you’re limited to the area the gradient covers. But generally you use derivatives just for 1, maybe 2 pixel wide lines. Anything beyond that and you’re increasing the chance for the inevitable artifacts of the technique to show through.

    And this is absolutely the technique Oskar is using.
     
  6. hardcorebadger

    hardcorebadger

    Joined:
    Nov 28, 2016
    Posts:
    31
    Alright I think we can put this one to bed. After reading more into that twitter thread I realized this is the same technique used in SDF Font rendering.

    Here's the money shot!




    TLDR; you start with a regular old texture (with crisp edges), then generate a singed distance field (SDF) and bake it into the alpha channel of the texture. The DDY function samples the alpha channel which contains the SDF, which you can clamp and filter into a variable width outline (or bottom edge in this case).

    Here's the longform explanation for anyone who wants to do this in the future.

    Texture Asset Pipeline
    (This asset pipeline can probably be optimized if someone can figure out how to just export one texture with color & alpha from photoshop).

    1. Start with a your basic texture, crisp edges, whatever colors you want. Export that.
    2. Export another version of that texture for alpha where the "background" (in this case the cliff) is transparent (0.0) and the "foreground" (the grass) is opaque (1.0). Doesn't matter what the colors are, you just need the alpha channel
    3. Create an SDF Texture using the alpha texture you just exported. You can do that in unity with this asset. Basically the distance is going to be the max width of the outline, I have mine set to inside since the shader is just going to subtract to get the outline color, but you can implement that piece a couple different ways, so up to you here.

    Here's the 2 textures I exported plus the generated SDF:


    Signed Distance Field - Background
    Some background on SDFs. Basically this new texture you have assigns a value to each pixel for how far away it is from the "edge" and stores it in the alpha channel. This is perfect for DDY because it creates a gradient, which derivative functions can pick up value from.

    Heres a visual example of what is going on with the example of a simple line as our "grass"


    The Shader
    1. Make a shader, set it up however you want, you want to at least pass through the albedo texture, but you can add any other lighting or whatever at this point. Here's mine - it's a custom diffuse shader with a texture.

    2. Here's the juicy part that actually does the outline. Sample the SDF Texture's alpha, run it through DDY, and bam there it is. If you set up the SDF Texture like I did (inside) you can just subtract this from your albedo to display the darkened outline effect.

    3. Then remove the "top" edge which is inverted so we don't get a lightening effect on the top edge.
    4. Then implement darkness and thickness settings with some math functions before applying it to your albedo.

    Heres my full subgraph for the DDY Edge (including clamping out the top edge, as well as thickness filtering and darkness multiplier.


    Here's the full shader (the diffuse/ambient lighting in off screen to the bottom)
     

    Attached Files:

    Last edited: Nov 24, 2021
  7. hardcorebadger

    hardcorebadger

    Joined:
    Nov 28, 2016
    Posts:
    31
    Quick follow up, I went ahead and tried to replicate a couple of the other lighting, outline, etc effects from bad north.



    DDY Edge Effect (Above)
    Inverse Hull Outer Outlines (This requires multi pass in URP which is nontrivial to set up but this video helps explain)
    Reflection via Inverted Mesh (literally just duplicate and scale the mesh negatively in the y direction)
    Transparent Water (with no texture, but with depth. Used shaders from this asset)

    Mesh: Just a low poly mesh with smooth shading (and of course the grass/cliff texture painted - gotta line it up with the tris)
    Lighting: Basically, it's overcast lighting. Ambient light is super bright, directional light is super dim. Point the light straight down to avoid noticeable shading on sides of the island while retaining sharper edges between tops and sides. Then add a super low intensity, super high radius SSAO to create that smoother lighting on the sides. (Oskar implemented his own AO based on a voxel space but this is a quick hack for it)

    If anyone's interested I can post more detail on the rest of these. Gunna move onto the water foam! (and make some better models for this)