Texel Space Quantization
The finest in gay furry technology, also taking inspiration from this snippet with the formatting. Mainly useful for faux-retro pixelated shadows. You can either copy these functions directly into your shader or separate them out as their own shader include.
// Calculate screen-space displacement from the center of the current texel
vec2 screen_delta(vec2 uv, sampler2D tex) {
vec2 tex_size = vec2(textureSize(tex, 0));
vec2 texel_center = (floor(uv * tex_size) + 0.5) / tex_size;
vec2 offset = texel_center - uv;
mat2 screen_to_uv = mat2(dFdx(uv), dFdy(uv));
mat2 uv_to_screen = inverse(screen_to_uv);
return uv_to_screen * offset;
}
// Snap vec_type to extrapolated value at the center of the current texel
float texel_snap(float v, vec2 delta) {
return v + dFdx(v) * delta.x + dFdy(v) * delta.y;
}
vec2 texel_snap(vec2 v, vec2 delta) {
return v + dFdx(v) * delta.x + dFdy(v) * delta.y;
}
vec3 texel_snap(vec3 v, vec2 delta) {
return v + dFdx(v) * delta.x + dFdy(v) * delta.y;
}
vec4 texel_snap(vec4 v, vec2 delta) {
return v + dFdx(v) * delta.x + dFdy(v) * delta.y;
}
To use these functions you first get a screen_delta from your UV coordinates and texture, then feed that into texel_snap along with whatever value you want to quantize.
vec2 delta = screen_delta(UV, albedo_map);
LIGHT_VERTEX = texel_snap(VERTEX, delta);
NORMAL = texel_snap(NORMAL, delta);
The shader code below uses my standard lighting shader include and celyk’s accumulation hack to create a posterized PBR effect.
Shader code
shader_type spatial;
#include "texel_snap.gdshaderinc"
// https://godotshaders.com/shader/standard-lighting-shader-include/
#define DIFFUSE_LAMBERT
#define SPECULAR_SCHLICK_GGX
#include "standard_lighting.gdshaderinc"
uniform int posterize_levels = 16;
uniform sampler2D albedo_map : source_color, filter_nearest_mipmap;
uniform sampler2D normal_map : hint_normal, filter_nearest_mipmap;
uniform float roughness : hint_range(0.0, 1.0) = 1.0;
uniform float metallic : hint_range(0.0, 1.0) = 0.0;
uniform vec2 uv_tiling = vec2(1.0);
uniform vec2 uv_offset = vec2(0.0);
void vertex() {
UV = (UV + uv_offset) * uv_tiling;
}
void fragment() {
// Snap LIGHT_VERTEX and NORMAL for pixelated shadows
vec2 delta = screen_delta(UV, albedo_map);
LIGHT_VERTEX = texel_snap(VERTEX, delta);
NORMAL = texel_snap(NORMAL, delta);
ALBEDO = texture(albedo_map, UV).rgb;
NORMAL_MAP = texture(normal_map, UV).xyz;
ROUGHNESS = roughness;
METALLIC = metallic;
// Specular reflections produce filtering artifacts with this method, so disable them
RADIANCE = vec4(vec3(0.0), 1.0);
}
void light() {
vec3 d = vec3(0.0), s = vec3(0.0);
STANDARD_LIGHTING(d, s);
DIFFUSE_LIGHT += ALBEDO * d + s;
float l = float(posterize_levels);
SPECULAR_LIGHT = floor(DIFFUSE_LIGHT * l) / l;
SPECULAR_LIGHT -= ALBEDO * DIFFUSE_LIGHT;
}
That is awesome! Been looking for something like this for a long time.
Here is this shader merged with a generic standardMaterial + normal map
// NOTE: Shader automatically converted from Godot Engine 4.5.1.stable.mono's StandardMaterial3D. shader_type spatial; render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx; uniform vec4 albedo : source_color; uniform sampler2D texture_albedo : source_color, filter_nearest_mipmap, repeat_enable; uniform ivec2 albedo_texture_size; uniform float point_size : hint_range(0.1, 128.0, 0.1); uniform float roughness : hint_range(0.0, 1.0); uniform sampler2D texture_metallic : hint_default_white, filter_nearest_mipmap, repeat_enable; uniform vec4 metallic_texture_channel; uniform sampler2D texture_roughness : hint_roughness_r, filter_nearest_mipmap, repeat_enable; uniform float specular : hint_range(0.0, 1.0, 0.01); uniform float metallic : hint_range(0.0, 1.0, 0.01); uniform vec3 uv1_scale; uniform vec3 uv1_offset; uniform vec3 uv2_scale; uniform vec3 uv2_offset; void vertex() { UV = UV * uv1_scale.xy + uv1_offset.xy; } // Calculate screen-space displacement to the center of the current texel vec2 screen_delta(vec2 uv, sampler2D tex) { vec2 tex_size = vec2(textureSize(tex, 0)); vec2 texel_center = (floor(uv * tex_size) + 0.5) / tex_size; vec2 uv_delta = texel_center - uv; mat2 screen_to_uv = mat2(dFdx(uv), dFdy(uv)); mat2 uv_to_screen = inverse(screen_to_uv); return uv_to_screen * uv_delta; } // Snap vector to the value at the center of the current texel // Note this works for any vec type (float, vec2, vec3, vec4) vec3 texel_snap(vec3 v, vec2 delta) { return v + dFdx(v) * delta.x + dFdy(v) * delta.y; } // Simple posterized lighting void light() { #define levels 12.0 float n_dot_l = clamp(dot(NORMAL, LIGHT), 0.0, 1.0); float i = n_dot_l * ATTENUATION; i = floor(i * levels) / levels; DIFFUSE_LIGHT += LIGHT_COLOR / PI * i; } uniform sampler2D normal_map: hint_normal; void fragment() { vec2 base_uv = UV; vec4 albedo_tex = texture(texture_albedo, base_uv); ALBEDO = albedo.rgb * albedo_tex.rgb; vec2 delta = screen_delta(UV, texture_albedo); LIGHT_VERTEX = texel_snap(VERTEX, delta); NORMAL = texture(normal_map, UV).xyz; NORMAL = texel_snap(NORMAL, delta); ALBEDO = texture(texture_albedo, UV).rgb; float metallic_tex = dot(texture(texture_metallic, base_uv), metallic_texture_channel); METALLIC = metallic_tex * metallic; SPECULAR = specular; vec4 roughness_texture_channel = vec4(1.0, 0.0, 0.0, 0.0); float roughness_tex = dot(texture(texture_roughness, base_uv), roughness_texture_channel); ROUGHNESS = roughness_tex * roughness; }Nice, you’ll wanna take out the custom light function though or else the PBR values won’t have any effect 😅
How does one apply this shader??
You apply it as a ShaderMaterial on objects you want to have pixelated shading. If you’re looking to apply it as a post-processing effect, that’s not really possible in Godot’s default renderer without some very complicated setup.
What is the STANDARD_LIGHTING function? And where can I find standard_lighting.gdshaderinc?
You can find it here. I’ll add the URL as a comment in the shader so it’s more clear.
Is there a way to separate the texel size from UV side? I’m trying to not scale my texture, just the lighting “sub” texel to have smaller, pixelated lighting.
You could change the screen_delta function so it takes in a vec2 tex_size and pass some multiple of your texture resolution
Thank you, that put me on the right path.
What about if I wanted to rotate the texel grid off of the UV’s axis?
I’m trying to get the texel orientation aligned with the world xyz, or otherwise capable of manual rotation to accomplish that.
That would involve rotating your UV coordinates (there are various tutorials online) before passing them to the screen delta function. For snapping to world XYZ you could actually do something as simple as:
Although, that tends to have self-shadowing artifacts so it might not be ideal.
hm, per-texel eh.. i wonder if you could make it look like if its per-vertex
How would you adapt this to work with a triplanar texture?