godot icon indicating copy to clipboard operation
godot copied to clipboard

Directional shadows broken for Distance Fade objects with Pixel Dither or Object Dither

Open puchik opened this issue 5 years ago • 14 comments

Godot version:

3.2.1 stable

OS/device including version:

Windows 10 Build 1909, Nvidia RTX 2070 Super, Driver version 445.87, GLES3

Issue description:

An object with a material that has Distance Fade (with either Pixel Dither or Object Dither mode) with a far-clip fade out (e.g. Min 60, Max 20) casts odd shadows (or not at all).

For example.... Shadows disappear when a large object exists in a scene? bigfloor

Banding/inconsistent opacity when moving camera pan

Shadow fading out before object does (Is it supposed to fade out at all?) zoom

Point and spot shadows look fine. Near-clip distance fade directional shadows (e.g. Min 20, Max 60) work fine. It also seems to be dependent on camera angle... there's one angle at which none of the above bugs show up. normal

Steps to reproduce:

  1. Make a new project. Add one small "floor" cube, another very large "floor" cube
  2. Add a cube and place it on top of the small "floor".
  3. Add a material to the directional cube. Set the Distance Fade to either "Pixel Dither" or "Object Dither". Make sure the min is larger than the max so it fades out when going further, not closer.
  4. Add a directional light with shadows enabled. Observe issues listed above (see gifs): 4.a Enable and disable the large floor. Notice shadows disappear when it's visible 4.b Pan or zoom camera and observe banding and fading issues
  5. Try to do the same with Omni and Spot lights. Shadows look fine.

Minimal reproduction project:

distance-fade-shadows.zip


Am I doing something wrong by making the min and max reversed? Is fading out at a far distance not supported? Not sure if this was never supposed to work in the first place 🙂

puchik avatar Jun 14 '20 04:06 puchik

Not sure if there is a nice solution here. Pixel fadeout is based on a) screen space position of the pixel and b) the distance from the camera to the pixel.

For computing the shadow buffer these values don't make sense. Shadows fade out based on the distance between the object and the light and shadow buffer pixels don't turn into singular screen space pixels.

What I would do is turn shadows off on that cube and then have a proxy cube that is set to shadows only.

clayjohn avatar Jun 14 '20 05:06 clayjohn

The only thing I can think of off the top of my head is rendering the shadows as normal but have them all fade out (reading) according to distance in the camera view, at about the same distance as the distance fade for the objects.

As clayjohn say, it is a difficult problem, and as with visibility determination, shadows need to be considered at the same time as building the system, as shadows can be equally as difficult / or even more difficult to deal with.

lawnjelly avatar Jun 14 '20 05:06 lawnjelly

What's weird, though, is this only happens if min and max distances are reversed. If max > min, then shadows work properly. But if min > max, these bugs happen. That's why I'm not sure if this is the intended way of getting them to fade out with far distance as opposed to near distance, since min > max doesn't intuitively make sense (might be a usability issue)

puchik avatar Jun 14 '20 06:06 puchik

But if min > max, these bugs happen.

Missed that bit. Yeah, it's fairly common that things won't work as expected when a min is larger than a maximum. Maybe there could be a warning, or preventing the values being set to this in the UI. :+1:

Irrespective the point stands, fading shadow maps might not work as intended, both in terms of the distance from the light being different from the distance from camera, and the all or nothing nature of shadow maps, and the shadow map texels being potentially a lot larger than pixels on screen.

lawnjelly avatar Jun 14 '20 06:06 lawnjelly

The shadow doesn't fade at all if max > min or all those other "working" cases, but I think that's another issue.


Here's another thing I noticed: image image So if the distance < min, it should appear normally? The fading should be done between min and max, and fully invisible past max.... but what actually happens is it's faded out when it's under min and visible past max...

You can easily flip those two values in the code to make the text match, at least, but the issue remains. Directional shadows are broken when it's fading out by far distance, regardless of min > max or max > min in the variables.

puchik avatar Jun 14 '20 08:06 puchik

I'v got the same issue with shader, that scales objects by distance. Has someone already found a solution to this problem?

https://youtu.be/1q8zVpsQR4c

William-Godwin avatar Sep 15 '20 16:09 William-Godwin

And I think the problem is here: float fade_distance = abs((INV_CAMERA_MATRIX * WORLD_MATRIX[3]).z);

If you use uniforms or TIME to scale or dither objects, then you will get normal shadow behavior.

William-Godwin avatar Sep 15 '20 17:09 William-Godwin

So, some more interesting information. If you use distance() to calculate fade or scale for objects shadows are working correctly. But any calculations, that use camera_matrix and inv_camera_matrix reproduce this bug.

distance(world_obj_pos, CAMERA_MATRIX[3].xyz) //______OR____ abs((INV_CAMERA_MATRIX * WORLD_MATRIX[3]).z)

William-Godwin avatar Sep 15 '20 19:09 William-Godwin

I'm not getting shadows when using a dithered fade with gles3. It seems to be related to this line:

https://github.com/dsrw/godot/blob/95f93b5f0f10f20eaa258faab057cae7adf5ee6b/drivers/gles3/rasterizer_scene_gles3.cpp#L2337

Removing && !p_material->shader->spatial.uses_discard from the condition fixes the issue for me.

Is there a better fix for this that doesn't require changing the code? Can I force a material for the depth pass?

dsrw avatar Oct 19 '20 23:10 dsrw

Cool. But the one question still: why when using matrices to get the position of the vertices and their scaling, the shadows also disappear?

William-Godwin avatar Oct 30 '20 06:10 William-Godwin

As a workaround, you can duplicate your mesh, use a separate opaque material for it and change its shadow rendering mode in the GeometryInstance section to Shadows Only. Then make your other dithered mesh not cast any shadows in the same GeometryInstance section. (You can reuse the same opaque material for all shadow-only meshes.)

Calinou avatar Oct 30 '20 09:10 Calinou

https://github.com/godotengine/godot-proposals/issues/3276 and https://github.com/godotengine/godot-proposals/issues/4443 should allow resolving this once either of these is implemented.

Calinou avatar Aug 23 '22 16:08 Calinou

I think I've found a satisfactory workaround for this issue after converting this effect to shader code. Establishing an upper boundary for fade_distance can make the object visible to the directional light, restoring its shadow. To minimize that distance while keeping the object out of sight before it "reappears" for its shadow projection, I might opt for the "pixel dither" form of the effect and base the value on the far distance of the camera. So far, that works smoothly.

This trick works for camera view distances up to a few hundred units, until fade_distance seems to top out (a little under 512?) and the shadow disappears again. I wasn't able to narrow down what affects that limit.

    uniform float camera_far = 100.0;
    
    ...
    
    // Pixel dither
    float fade_distance = -VERTEX.z;    
    float fade = clamp(smoothstep(distance_fade_min, distance_fade_max, fade_distance), 0.0, 1.0);
    // Restore opacity to cast shadow
    if (fade_distance > camera_far) fade = 1.0;
    
    int x = int(FRAGCOORD.x) % 4;
    int y = int(FRAGCOORD.y) % 4;
    etc...

Wolfe2x7 avatar Nov 01 '22 19:11 Wolfe2x7

Another workaround is to use a shader global for the world camera position. Instead of using length(VERTEX) as the distance, use the distance between the world camera position global (not the built-in!) and the current vertex/pixel position in world space.

This calculation can be done in the vertex shader to make it run faster. The world camera position global variable will have to be updated from some script every frame. (As far as I know shader globals were introduced in 4.0)

WrobotGames avatar May 31 '24 23:05 WrobotGames