godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
90.1k stars 21.15k forks source link

Vulkan: Attenuation looks pretty grainy when creating custom lighting shaders due to soft shadow dithering #60360

Open hidemat opened 2 years ago

hidemat commented 2 years ago

Godot version

Godot 4 alpha 6

System information

Windows 10 NVIDIA GeForce GTX 1050

Issue description

When creating custom lighting in a shader, the attenuation looks very grainy. I've been trying to implement a toon shader in godot 4. Everything looks good until I enable attenuation. I wish I was able to obtain a clean line on the shadow attenuation, since this is a toon shader.

I decided to report this as an issue, because it is definitely something that would deter me from using godot 4 in future projects, since I'm a big fan of cel shaders.

Without ATTENUATION: image

With ATTENUATION: image

This is the shader code:

shader_type spatial;

//render_mode ambient_light_disabled;

uniform vec4 albedo : hint_color = vec4(1.0f);
uniform sampler2D albedo_texture : hint_albedo;
uniform bool clamp_diffuse_to_max = false;

uniform int cuts : hint_range(1, 8) = 3;
uniform float wrap : hint_range(-2.0f, 2.0f) = 0.0f;
uniform float steepness : hint_range(1.0f, 8.0f) = 1.0f;

uniform bool use_attenuation = true;

uniform bool use_specular = true;
uniform float specular_strength : hint_range(0.0f, 1.0f) = 1.0f;
uniform float specular_shininess : hint_range(0.0f, 32.0f) = 16.0f;
uniform sampler2D specular_map : hint_albedo;

uniform bool use_rim = true;
uniform float rim_width : hint_range(0.0f, 1.2) = 1.0f;
uniform vec4 rim_color : hint_color = vec4(1.0f);

uniform bool use_ramp = false;
uniform sampler2D ramp : hint_albedo;

uniform bool use_borders = false;
uniform float border_width = 0.01f;

float split_specular(float specular) {
    return step(0.5f, specular);
}

void fragment() {
    ALBEDO = albedo.rgb * texture(albedo_texture, UV).rgb;
}

void light() {
    // Attenuation.
    float attenuation = 1.0f;
    if (use_attenuation) {
        attenuation = ATTENUATION;
    }

    // Diffuse lighting.
    float NdotL = dot(NORMAL, LIGHT);
    float diffuse_amount = NdotL + (attenuation - 1.0) + wrap;
    //float diffuse_amount = NdotL * attenuation + wrap;
    diffuse_amount *= steepness;
    float cuts_inv = 1.0f / float(cuts);
    float diffuse_stepped = clamp(diffuse_amount + mod(1.0f - diffuse_amount, cuts_inv), 0.0f, 1.0f);

    // Calculate borders.
    float border = 0.0f;
    if (use_borders) {
        float corr_border_width = length(cross(NORMAL, LIGHT)) * border_width * steepness;
        border = step(diffuse_stepped - corr_border_width, diffuse_amount)
                 - step(1.0 - corr_border_width, diffuse_amount);
    }

    // Apply diffuse result to different styles.
    vec3 diffuse = ALBEDO.rgb * LIGHT_COLOR / PI;
    if (use_ramp) {
        diffuse *= texture(ramp, vec2(diffuse_stepped * (1.0f - border), 0.0f)).rgb;
    } else {
        diffuse *= diffuse_stepped * (1.0f - border);
    }

    if (clamp_diffuse_to_max) {
        // Clamp diffuse to max for multiple light sources.
        DIFFUSE_LIGHT = max(DIFFUSE_LIGHT, diffuse);
    } else {
        DIFFUSE_LIGHT += diffuse;
    }

    // Specular lighting.
    if (use_specular) {
        vec3 H = normalize(LIGHT + VIEW);
        float NdotH = dot(NORMAL, H);
        float specular_amount = max(pow(NdotH, specular_shininess*specular_shininess), 0.0f)
                                * texture(specular_map, UV).r
                                * attenuation;
        specular_amount = split_specular(specular_amount);
        SPECULAR_LIGHT += specular_strength * specular_amount * LIGHT_COLOR;
    }

    // Simple rim lighting.
    if (use_rim) {
        float rim_dot = 1.0 - dot(NORMAL, VIEW);
        float rim_light = rim_dot * NdotL;
        rim_light = smoothstep(rim_width - 0.01, rim_width + 0.01, rim_light);

        DIFFUSE_LIGHT += rim_light * rim_color.rgb * rim_color.a * LIGHT_COLOR;
    }
}

Steps to reproduce

Open the minimal reproduction project and check it out.

Minimal reproduction project

First3D.zip

Calinou commented 2 years ago

This is caused by soft shadow dithering, which is enabled by default. The fix is to set the soft shadow to Hard in the Project Settings, which will also improve performance:

Default shadow settings

2022-04-19_00 29 02

Hard shadow settings (more suited for cel shading)

2022-04-19_00 28 58

Remember to also do this with the Soft Shadow Quality setting, which affects point light shadows.

We can document this caveat of soft shadow filtering in the shader reference in godot-docs (as well as the 3D Lights and shadows page).


PS: When opening the project on master 3639c27cf, this is what I see:

https://user-images.githubusercontent.com/180032/163887308-31498bc8-d524-4834-867e-d9de1eff997c.mp4

If I decrease the DirectionalLight's Shadow Max Distance property to increase shadow texel density, it looks like this:

image

The Shadow Max Distance property you're using in the project (100) is likely too high. It looks even worse when the player scene is opened directly as Godot uses a Shadow Max Distance of 250 by default (at least until https://github.com/godotengine/godot/pull/49736 is merged).

sj2305 commented 2 years ago

agree