Pixel perfect cell shading with normal maps

Godot Version

Godot 4.3

Question

So I want to create a cell shaded effect for my sprites based on the Normal map.

As you can hopefully see in the picture, I can not get my lights to snap to the pixels of the actual sprite.

Shader code:

shader_type canvas_item;

// Number of cell-shading levels (adjust as needed)
uniform int cell_levels : hint_range(1, 10) = 3;

// Normal map texture (assign this in the Godot editor)
uniform sampler2D normal_map : filter_nearest;

void light() {

    // Sample the normal map and convert it to a world-space normal
    vec3 normal_map_value = texture(normal_map, UV).rgb * 2.0 - 1.0;
    vec3 normal = normalize(normal_map_value);

    // Calculate the light direction (from the fragment to the light source)
    vec3 light_dir = normalize(LIGHT_DIRECTION); // Flip the direction

    // Calculate the light intensity using the dot product
    float light_intensity = max(dot(normal, light_dir), 0.0);

    // Posterize the light intensity for cell-shading
    float cell_light = floor(light_intensity * float(cell_levels)) / float(cell_levels - 1);

    // Apply the cell-shaded lighting to the light color
    LIGHT = LIGHT_COLOR * cell_light;
}

I have everything set to filtering: Nearest.
I am not great at shaders and am hoping that I am missing something obvious.

Small recreation of the scene + shader: https://drive.google.com/file/d/1iivEYbMCtGv8orjQD48IXIuIgLXJ1Ftr/view?usp=drive_link

Just a shot in the dark, but it looks like you’re creating a texture (“normal_map_value”), and that may need to be set to nearest filtering, too.
Supposedly you can also set the default project settings to nearest, if you haven’t already. That’s my best guess.

The main issue is likely that your UVs need to be quantized to match your sprite’s pixel grid. Add this function to snap the UV cords:

shader_type canvas_item;

uniform int cell_levels : hint_range(1, 10) = 3;
uniform sampler2D normal_map : filter_nearest;
uniform vec2 texture_size = vec2(64.0, 64.0); // Set this to your sprite's dimensions

vec2 snap_to_pixel(vec2 uv) {
    return floor(uv * texture_size) / texture_size;
}

void light() {
    // Get pixel-perfect UV coordinates
    vec2 pixelated_uv = snap_to_pixel(UV);
    
    // Sample the normal map with pixelated UVs
    vec3 normal_map_value = texture(normal_map, pixelated_uv).rgb * 2.0 - 1.0;
    vec3 normal = normalize(normal_map_value);
    
    // Calculate the light direction
    vec3 light_dir = normalize(LIGHT_DIRECTION);
    
    // Calculate the light intensity using the dot product
    float light_intensity = max(dot(normal, light_dir), 0.0);
    
    // Posterize the light intensity for cell-shading
    float cell_light = floor(light_intensity * float(cell_levels)) / float(cell_levels - 1);
    
    // Apply the cell-shaded lighting to the light color
    LIGHT = LIGHT_COLOR * cell_light;
}

When working with pixel art and normal maps, getting the lighting to perfectly align with the sprite pixels can be tricky. I can find a few vids I like that can help you if you want?

I normally write in another language so some of the built in functions might be a little funky just lmk if it works and i can check em when I get home :slight_smile: