3D Color Range

3D sprites, especially those with Billboard settings, are a rather niche and somewhat overlooked area. This is my attempt to adapt a 2D shader found here for those edge cases. The shader takes a specified range of colors and replaces them with a different color, without affecting the rest of the spectrum. It provides best results when your original sprite features a distinct color to be keyed out, such as bright cyan or magenta. In theory you should be able to swap multiple color ranges, too; instructions are provided in the shader comments.

See this thread for the specifics of applying shaders to Sprite3Ds; the process with animated sprites is similar but involves reapplying the shader on every frame, not just on texture change.

Key variables to be set with “shader properties” or via script:

_min and _max (0.0 – 1.0 range): these specify which hue you want to erase (look at the HSV scale for reference; cyan is 0.5). The muddier your initial color, the broader the range that you’ll need to cover.
color: the color that you want to apply. Brightness is preserved, so you can get darker shades by darkening choice spots on the original sprite
sprite_texture: reference to the texture of your sprite (or to a frame, if it’s animated)
billboard: set to true for billboard mode sprites. Just beware of potential lighting issues (some of those will get fixed in 4.3)

art credit: Konst. Evans

Shader code
// Color range swap shader for Godot 4; Sprite3D version by Sithoid
// Based on 2D shader by nonunknown https://godotshaders.com/shader/color-range-swap/
// 3d lifehacks by Anonzs https://www.reddit.com/r/godot/comments/11dklv0/sprite3d_shader/
// Billboard projection by mrdunk https://ask.godotengine.org/152606/how-to-do-i-make-a-shader-a-billboard-face-the-player

shader_type spatial;
render_mode depth_draw_opaque, depth_prepass_alpha; // Prepass is needed to cast a shadow

// Set this parameter to your actual texture in script, e.g. with
// material_override.set_shader_parameter("sprite_texture", texture)
uniform sampler2D sprite_texture : source_color, filter_nearest;

// Hue on a HSV scale (0 to 1) that will be keyed out. Defaults are set to key out bright cyan
uniform float _min = 0.49;
uniform float _max = 0.5;
// Target color (RGBA) that will appear instead of the mask (it will respect brightness)
uniform vec4 color : source_color = vec4(0.59, 0.12, 0.32, 1.0); // Dark pink by default

uniform bool billboard = false; // Toggle billboard mode (set this in script)

vec3 rgb2hsv(vec3 c) {
	vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
	vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
	vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));

	float d = q.x - min(q.w, q.y);
	float e = 1.0e-10;
	return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

// All components are in the range [0…1], including hue.
vec3 hsv2rgb(vec3 c) {
	vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
	vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
	return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

// ===== nonunknown got those from: https://gamedev.stackexchange.com/a/75928
vec4 to_gray(vec4 tex) {
	float avg = (tex.r + tex.g + tex.b) / 3.0;
	return vec4(vec3(avg),tex.a);
}

vec4 to_color(vec4 gray, vec4 col) {
	return gray * col;
}
// ===== end

// == Billboard projection by mrdunk

void vertex() {
	if (billboard) {
		mat4 modified_model_view = VIEW_MATRIX * mat4(
			INV_VIEW_MATRIX[0],
			INV_VIEW_MATRIX[1],
			INV_VIEW_MATRIX[2],
			MODEL_MATRIX[3]
		);
		MODELVIEW_MATRIX = modified_model_view;
	}
}

// end ===

void fragment() {
	vec4 tex = texture(sprite_texture, UV);
	vec3 hsv = rgb2hsv(tex.rgb);
	
	// the .r here represents HUE, .g is SATURATION, .b is LUMINANCE
	if (hsv.r >= _min && hsv.r <= _max) {
		tex = to_gray(tex);
		tex = to_color(tex, color);
	}
	// To replace multiple colors, just copy this "if" statement
	// and repeat it with different variables (such as _min1, _min2 and color2)
	ALBEDO = tex.rgb;
	ALPHA = tex.a;
}
Tags
3d sprite, billboard, color range, color swap, godot 4, key, keying, palette swap, sprite3D
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.

Related shaders

SDF Range Rings (3D)

Texture-Based Color Swapper

Godot 4.x Color Swap for 3D Mesh Models

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments