Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement dithered shadows for semi-transparent objects #3276

Open
atirut-w opened this issue Sep 11, 2021 · 23 comments
Open

Implement dithered shadows for semi-transparent objects #3276

atirut-w opened this issue Sep 11, 2021 · 23 comments

Comments

@atirut-w
Copy link

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: mrdoob/three.js#10600 (comment)

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 Calinou changed the title Dithered shadows for semi-transparent objects Implement dithered shadows for semi-transparent objects Sep 11, 2021
@Calinou
Copy link
Member

Calinou commented Sep 11, 2021

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
Copy link
Author

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
Copy link
Member

Calinou commented Sep 11, 2021

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
Copy link
Author

(stained) glass

Time for colored shadows? 👀

@Calinou
Copy link
Member

Calinou commented Sep 11, 2021

Time for colored shadows? 👀

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
Copy link
Author

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

@Calinou
Copy link
Member

Calinou commented Sep 11, 2021

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
Copy link
Author

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
Copy link

SIsilicon commented Sep 12, 2021

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

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

@atirut-w
Copy link
Author

use a dithering shader on the shadow caster

Exactly the implementation that I had on my mind.

@SIsilicon
Copy link

SIsilicon commented Sep 12, 2021

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
Copy link
Author

caustics

HOW.

I NEED SAUCE.

@SIsilicon
Copy link

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
Copy link
Author

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

@Calinou
Copy link
Member

Calinou commented Aug 22, 2022

@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.

@Calinou Calinou moved this to In Discussion in Godot Proposal Metaverse Aug 22, 2022
@Calinou Calinou moved this from In Discussion to Ready for Review in Godot Proposal Metaverse Aug 22, 2022
@atirut-w
Copy link
Author

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

@viktor-ferenczi
Copy link

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

@Calinou
Copy link
Member

Calinou commented Sep 16, 2022

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) 🙂

@viktor-ferenczi
Copy link

viktor-ferenczi commented Sep 16, 2022

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
Copy link

viktor-ferenczi commented Sep 16, 2022

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
Copy link

any updates on this one?

@Calinou
Copy link
Member

Calinou commented Nov 27, 2023

any updates on this one?

To my knowledge, nobody is currently working on this.

@Hyperspeed1313
Copy link

Hyperspeed1313 commented Jul 28, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants