godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.12k stars 69 forks source link

Implement dithered shadows for semi-transparent objects #3276

Open atirut-w opened 3 years ago

atirut-w commented 3 years ago

Describe the project you are working on

Not working on anything, but could be useful for rendering foliages where transparency have to be used instead of alpha-scissor/cutout rendering.

Describe the problem or limitation you are having in your project

As of currently(3.x, did not test the master branch), you cannot have semi-transparent shadows and thus are limited to Opaque Pre-Pass if you want shadows for your semi-transparent objects. This obviously won't look correct.

Screenshot from 2021-09-11 09-14-24

Describe the feature / enhancement and how it helps to overcome the problem or limitation

An option to use dithered shadows that try to emulate semi-transparent shadows.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Use the alpha channel to dither between opaque and completely transparent, that's it. Basically how Unity does it.

Example: https://github.com/mrdoob/three.js/issues/10600#issuecomment-471152023

If this enhancement will not be used often, can it be worked around with a few lines of script?

Currently not possible to implement as a script or an addon.

Is there a reason why this should be core and not an add-on in the asset library?

Currently not possible to implement as a script or an addon.

Calinou commented 3 years ago

Not working on anything, but could be useful for rendering foliages where transparency have to be used instead of alpha-scissor/cutout rendering.

Foliage shadows are generally well-covered by alpha-tested (alpha scissor) shadows, since foliage doesn't have partially translucent areas by nature. Dithered shadows would be more useful in situations where you have partial transparency, such as smoothly fading an object in and out along with its shadow.

Nonetheless, I think this feature is worth looking into for Godot 4.x. I'm not sure if it'll be possible to sample the albedo texture's alpha channel though – we may be limited to per-material opacity only (or even per-mesh opacity).

atirut-w commented 3 years ago

Foliage shadows are generally well-covered by alpha-tested (alpha scissor) shadows, since foliage doesn't have partially translucent areas by nature.

And what about clothes and fabrics?

Calinou commented 3 years ago

And what about clothes and fabrics?

Cloth and fabric also rarely has large semi-transparent areas. I think dithered shadows will be more useful for materials such as (stained) glass, in addition to fading objects (e.g. due to LOD or gameplay reasons).

atirut-w commented 3 years ago

(stained) glass

Time for colored shadows? :eyes:

Calinou commented 3 years ago

Time for colored shadows? :eyes:

Lights in 3.x can cast colored or transparent shadows with the Shadow Color property, but this is done on a per-light basis rather than a per-mesh or per-material basis. Also, Light's Shadow Color property has been removed in the master branch for performance reasons. (It may be possible to reintroduce this feature with no performance cost if you don't use it thanks to Vulkan specialization constants, but it probably won't happen in time for 4.0.)

atirut-w commented 3 years ago

By colored shadows, I mean colored, semi-transparent objects casting colored shadows like this: image

Calinou commented 3 years ago

By colored shadows, I mean colored, semi-transparent objects casting colored shadows like this:

For use cases such as this one, you can also use projector textures which are supported for both SpotLights and OmniLights.

atirut-w commented 3 years ago

projector textures

Sounds like a PITA to set up especially because you have to bake the correct colors into the texture, but how do you even do that?

SIsilicon commented 3 years ago

It's actually pretty simple to set up per object transparent shadows in Godot. All you need to do is duplicate the object with the shadow, disable shadow casting on the original, and use a dithering shader on the shadow caster (You must also set the dupe's shadow mode to Shadow Only). Something like this.

shader_type spatial;

uniform float alpha : hint_range(0.0, 1.0);
uniform sampler2D bayer;

void fragment() {
    if (texture(bayer, FRAGCOORD.xy / 4.0).r > alpha) {
        discard;
    }
}

Screenshot 2021-09-11 195412 Screenshot 2021-09-11 195433

It's also worth noting that the effect is absolutely awful without shadow filtering. Screenshot 2021-09-11 195524

Edit: It's also also worth noting that with the spot light, the shadow seems to be more transparent the closer the shadow is to the light source; perhaps it's due to the dithering pattern being more compressed there, but that's just a guess.

Edit: While playing with the shader, I also discovered a disadvantage of this technique in which multiple transparent object shadows don't darken each other as they would IRL.

Image ![image](https://user-images.githubusercontent.com/34734122/132967659-6c8631fa-17b6-4770-a941-82e22ee3821c.png)

I tried solving by giving each object a unique sampling offset of dithering pattern, but it only works in specific cases.

atirut-w commented 3 years ago

use a dithering shader on the shadow caster

Exactly the implementation that I had on my mind.

SIsilicon commented 3 years ago

Also, about coloured shadows, I think one solution would be to have three lights, one for each colour component, and set three object dupes to selectively render in each shadow. I cannot test this theory right now though, 'cause I'm currently limited to GLES2. Even then, the combined lights would have some colour fringing due to one or two of the shadow maps having different resolutions, plus you'd be rendering three shadow maps, which will be expensive in any 3D project.

Frankly, if a pull request were made that implemented what was shown in this article, it would be much more elegant and efficient. https://wickedengine.net/2018/01/18/easy-transparent-shadow-maps/

Edit: I thought I'd mess with the shader some more, and I think caustics would be another great application of this proposal. image

atirut-w commented 3 years ago

caustics

HOW.

I NEED SAUCE.

SIsilicon commented 3 years ago

It's quite simple really. 🙂

shader_type spatial;

uniform float caustic = 5.0; // The higher this is, the more the light is focused in the effect.
uniform sampler2D bayer;

void fragment() {
    if (texture(bayer, FRAGCOORD.xy / 4.0).r > (1.0 - pow(dot(NORMAL, VIEW), caustic))) {
        discard;
    }
}

The trick is to make the assumption that the caustic strength is directly proportional to the angle in which the light shines through the object. I use the same technique to fake caustics in blender eevee.

atirut-w commented 2 years ago

@Calinou any chance of this proposal making it into 4.x?

Calinou commented 2 years ago

@Calinou any chance of this proposal making it into 4.x?

This proposal needs to be discussed in a proposal review meeting first. It won't be considered for 4.0 due to feature freeze, but we need to evaluate whether this makes sense to support in core for a future 4.x release.

However, we are currently not reviewing proposals that are not critical for 4.0 in an effort to focus on releasing 4.0 first. This proposal also already has a workaround available, making it less critical to implement in core.

atirut-w commented 2 years ago

It would still be nice to have this as a built-in "Dithered" transparency mode, though. A QoL thing.

viktor-ferenczi commented 2 years ago

Dithering should use blue noise instead of plain white noise for smoother visual appearance. Please see this good explanation on why.

Calinou commented 2 years ago

Dithering should use blue noise instead of plain white noise for smoother visual appearance.

We already use interleaved gradient noise for distance fade dithering and shadow map rendering. It's a cheap approximation of a noise that focuses on high-frequency patterns (instead of low frequency) :slightly_smiling_face:

viktor-ferenczi commented 2 years ago

I've tried both noise patterns.

Downloaded the Free blue noise textures from this article: http://momentsingraphics.de/BlueNoise.html

Loaded the 512_512/LDR_RGBA_0.png one as the dither_noise texture (512x512 pixels).


uniform sampler2D dither_noise;

// See https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence/
float ign(vec2 px)
{
    float t = floor(mod(TIME * 60.0, 1024.0));
    float x = px.x + 5.588238f * t;
    float y = px.y + 5.588238f * t;
    return mod(52.9829189f * mod(0.06711056f * x + 0.00583715f * y, 1.0f), 1.0f);
}

float noise(vec2 px) 
{
    return texelFetch(dither_noise, (ivec2(px) + ivec2(int(TIME * 397.0) % 8191)) & ivec2(511), 0).r;
}

void fragment() {
    ...
    // Here transparency is the color and transparency (alpha) of the glass material in the mesh
    ALBEDO = transparency.rgb;
        ALPHA = transparency.a >= noise(SCREEN_UV * VIEWPORT_SIZE) ? 1.0 : 0.0;
    ALPHA_SCISSOR_THRESHOLD = 0.5;
}

The noise function gives slightly smoother result than the ign one. Of course it is at the cost of an extra memory read per transparent pixel. Also, I had to tweak the IGN algo a bit to adopt to Godot having only TIME and no FRAME_NUMBER in shaders. TAA works like magic and makes it smooth quite quickly if staying still. But with FXAA ign() gives a smoother result and does not care about movements (no time domain), however at the cost of quite jittery edges.

image

However, it looks completely wrong when animated (slowly rotated) in front of a camera...

viktor-ferenczi commented 2 years ago

Perfected a bit how we use TIME and got it working both with FXAA and TAA with real good results:

const int D = (1 << 13) - 1;  // 13 is prime to have a cyclic group
float ign(vec2 p)
{
    float v = mod(52.9829189f * mod(0.06711056f * p.x + 0.00583715f * p.y, 1.0f), 1.0f);
    return mod(v + float(int(TIME * 11003.0) % D) / float(D), 1.0);  // 11003 is a prime which works well
}

How to use it:

void fragment() 
{
    ...
    if (!opaque) {
        // transmittance is the light can be transmitted through the transparent material
        ALBEDO = 1.0 - transmittance.rgb;
                ALPHA = transmittance.a >= ign(SCREEN_UV * VIEWPORT_SIZE) ? 1.0 : 0.0;
        ALPHA_SCISSOR_THRESHOLD = 0.5;
    }
}

Tested with alpha from 0.1 to 0.9 in 0.1 steps. Under 0.2 it looks really spotty, 0.3 works pretty well as a glass window and above it all good. Best combined with FXAA or TAA, certainly, but even work without. The TAA result is basically the same as real transparency after watching it still for less than a second. FXAA works reasonably well, but jitters a little bit at certain transparency values. I guess the best values can be selected and using only those for in-game materials would minimize the effect.

No smoothing, alpha 0.3: image (the stripes are not visible, they are moving fast and cannot be tracked with the eye, so still look smooth)

FXAA, alpha 0.3: image

Video, play it at 1:1 zoom (1280x960)

eddieataberk commented 9 months ago

any updates on this one?

Calinou commented 9 months ago

any updates on this one?

To my knowledge, nobody is currently working on this.

Hyperspeed1313 commented 1 month ago

It's quite simple really. 🙂

shader_type spatial;

uniform float caustic = 5.0; // The higher this is, the more the light is focused in the effect.
uniform sampler2D bayer;

void fragment() {
  if (texture(bayer, FRAGCOORD.xy / 4.0).r > (1.0 - pow(dot(NORMAL, VIEW), caustic))) {
      discard;
  }
}

The trick is to make the assumption that the caustic strength is directly proportional to the angle in which the light shines through the object. I use the same technique to fake caustics in blender eevee.

As someone who just started using Godot literally today, what do I need to put in the Bayer property? I'm just trying to get this shader to work on a translucent sphere and not seeing it make a real impact. There's a tiny hard edge around the sphere's perimeter but it does nothing to the shadow