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;
}
Tags
pixel, retro, texel space lighting, texture space
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from tentabrobpy

Related shaders

guest

12 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Skooma_man
25 days ago

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;
}


forgeworksdev
24 days ago

How does one apply this shader??

Alex
Alex
21 days ago

What is the STANDARD_LIGHTING function? And where can I find standard_lighting.gdshaderinc?

Axi
Axi
18 days ago

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.

Axi
Axi
9 days ago
Reply to  tentabrobpy

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.

Purpbatboi2i
15 days ago

hm, per-texel eh.. i wonder if you could make it look like if its per-vertex

Ian
Ian
5 days ago

How would you adapt this to work with a triplanar texture?