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