This site is currently in read-only mode during migration to a new platform.
You cannot post questions, answers or comments, as they would be lost during the migration otherwise.
+1 vote

Basically, i need to make a system similar to street fighter and games like that where when you select a character you can change the character's colour, BUT i dont want to have 50000 of the same sprites, so is there a way maybe through shaders to Make it so one a specific area turns one colour, or somehow godot looks for a certain colour on the character and changes it when i ask it to?

i dont want to use the getpixel() and setpixel() methods because it's highly inefficiant for fighting games(Mainly cause i have like 600 sprites for one character), i need to be able to select bulk of colour and change it, but i dont know how to do this

in Engine by (483 points)

3 Answers

+6 votes
Best answer

The only way I have in mind is by doing a shader like this:

uniform texture in_tex; //input texture
uniform color test_col; //the color to test against
uniform color new_col; //the target color
uniform float threshold; 

//get the texture color
vec4 out_col = tex(in_tex, UV);

//calculate the difference between our color and test color
vec3 diff = out_col.rgb - test_col.rgb;

//if the difference is less than our threshold
if(abs(length(diff)) < threshold)
    //the new texture color is now  new_color * diff
    out_col.rgb = new_col.rgb * (vec3(1.0,1.0,1.0) - diff);
COLOR = out_col;

where test_col is a mask color that you use only for mask-testing, for instance (1.0,0.0,1.0) and new_col is the color you will replace the mask with.
I'm mot sure if you can actually use this shader per-instance, but might be useful anyway..


To everyone finding this post useful and willing to use this code, feel free to do so as you wish!

by (109 points)
edited by

looks like something went wrong when inserting code, some underscores got automatically changed as italic so it might need modifications to work

Thank you so much, this is exactly what i was looking for ;-;

Wait...ughhh, is there a way to do it for multiple different colours, cause i can only get it to work with 1 colour

This shader does the same thing greenscreen does in videos, you could use different shades and change what's happening into the if to tweak the result.
You could also add another pseudo-pass by copy-pasting the code before COLOR and adding a new mask color and testing color etc.
But I would not recommend it:
I don't know how shaders internally works in Godot but keep in mind that normally the "fragment" or "pixel" are calculated per-pixel(called on each single pixel each frame!!) and most of all, using if in shaders is normally a bad, bad thing(slow).

I think your solution still remains in the shader side but you have to figure out if you actually want a color "rotation" or actually change the palette (by trashing my shader and thinking coordinates)

@Alex\doc: You can write underscore with \_ or you can use some other advice from this thread

@Alex\doc, I fixed it for you ;) Next time use backticks (`) instead of <code>, and indentation instead of <pre><code>

Thank you, I'll do!

Thank you Alex\doc for this shader code, it works exactly as advertised. To your knowledge, is there any way to smooth the edges of the color-replaced pixels, or maybe even overlay/screen the new color over the target color? I used your shader code in a texture progress control and wrote briefly about it in a thread on the Godot developers forum here, and even included a gif of it in action. The shader works great, but the color-replaced pixels are jagged and rough around the edges. Here's a screenshot:
enter image description here

Sorry for the long time, I've tried without success to smooth those edges.
You should play around with if(abs(length(diff)) < threshold) and its contents, I think the problem comes from the if condition which is not "fuzzy". You should try to act directly without the if statement...

+1 vote

If you are doing 2d game the simplest way to do this would be to use modulate property of Sprite node.
(edit:) What I have seen in many games can be achieved very easy by combining two sprites, a base one on the bottom and 'color changer' on the top, then you modulate only top one.

by (1,299 points)
edited by

The problem is i cant use modulate due to it colouring the entire sprite, i have a preset thing of colour palettes, but i dont know how i would apply it

Well don't know your exact textures and the effect you want to achieve, but what I have seen in many games can be achieved very easy by combining two sprites, a base one on the bottom -> and 'color change' on the top, and modulating only top one.

+4 votes

UPDATE, Now this work perfect on Intel GPUS, blame bad precision on intel gpus for uniform float vars.
Also use ASEPRITE for generate the greyscale mask, Photoshop alters the palette on save

After playing a bit with godot I found maybe the best solution for this problem without using if for each color or a loop:

This solution only requires a greyscale mask for each frame and the palette textures which saved on the correct format are a minimal memory overhead

First you need a mask which represent the areas that change color for map on the palette, on my example these are like this:
enter image description here
They are greyscale images without alpha where each color is represented by a correlative number and then this number represents the color index on the palette:
On the example image:
megaman black color is 0,0,0 which is index 0 on palette
megaman blue color is 1,1,1 which is index 1 on palette
megaman cyan color is 2,2,2 which is index 2 on palette
white color 255,255,255 is ignored

And then the palettes, because I need only 3 colors I created my palette of 4x1 pixels, more colors to map, the bigger the palette.

Then the shader where the magic happens:

uniform texture palette;
uniform float palette_size;
uniform texture mask;
vec4 mask_color = tex(mask,UV);
vec4 output = tex(TEXTURE,UV);
if(mask_color.r != 1.0)
    output = tex(palette,vec2((mask_color.r*255.0)/(palette_size-0.001),0.0));
COLOR.rgba = output;

This shader receives the current palette
The total size of the palette (image width)
And the mask for current frame

Then for each non white pixel (255,255,255) on the image I apply the mask vs palette translation
Note: I only use the red channel for my calculations because on a greyscale images the three channels are the same value.
And for the translation basically the formula (mask_color.r/(palette_size/255)) is transform 0.0/1.0 values to palette UV.x ones, also remember UV values are between 0.0 and 1.0 too.
for Example:
the current mask color = 2,2,2
mask_color.r = 0,007843137254902 (remember godot shaders return normalized (0.0-1.0) colors, this number here is the "2" from current_color normalized automatically by godot (2/255))
palette_size = 4 (This value needs to be normalized for match the godot color values between 0.0 and 1.0, that is the /255 on the formula)
So the formula:
0,007843137254902/(4/255) = 0,5000000000000007 ~ 0,5 which results on the color two of our palette (look at the image on top is cyan)

All this looks good now, but what about the implications of change textures on the fly from disk on the shader, for example an animation player directly change textures?
For that I created a simple script to automatically update the animation frames with animation frame mask using a SpriteFrames, so is the same process as loading the frames for animated sprite, a requirement is those needs to match, so if one of your animations has 5 frames, you need the 5 same frames masked. Also the script does the same for palettes. With this you already loaded the required textures so no more disk read.
Here the piece of code which does that:

extends AnimatedSprite
export(SpriteFrames) var color_mask = null
export(SpriteFrames) var palettes = null
export(int) var palette_index = 0 setget _set_palette_index
var palette_size = 0 setget _set_palette_size

#When the frame change, change the mask reference on the shader too
func _on_AnimatedSprite_frame_changed():
    if(color_mask != null && get_frame() < color_mask.get_frame_count()):

#If the palette index change, reflect the change on the shader and update the width of the new palette
func _set_palette_index(value):
    if(palettes != null && palette_index < palettes.get_frame_count()  && palette_index != value):
        palette_index = value
        var frame_mask = palettes.get_frame(palette_index)
        self.palette_size = frame_mask.get_width()

#Update the width of the palette
func _set_palette_size(value):
    if(palette_size != value):
        palette_size = value

An image which resume how the algorithm works:
And the demo project with the concepts explained here:
If somebody has an optimization feel free to comment it here, but this could be the fastest solution on performance with a minimal memory overhead on GLES2.0

by (80 points)
edited by


Yeah he's always running, the problem i was having before is basically, you know his basic colours? Like under the mask? They dont Show up when i press play (When i remove the code you told me to add) It's just flashing with a solid black colour, and neither of the palettes seem to show up

Ok I manage to reproduce the bug on my notebook with an Intel HD Graphics 4000 and a Amd 8870M.
With the amd all is fine
With Intel same result as you.

This should be a bug, so I will open a issue with the info. When is resolved this script should work on all computers. Btw what are your specs?

Intel® Core™ i5-3320M CPU
64Bit Zorin OS 10 (Linux)
4gb DDR3 Ram
Intel-4000 Graphics (I think?)

Hey I Patched the issue, Now the project should work on Intel, download it again from the first reply and use ASEPRITE for generate the greyscale mask

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read Frequently asked questions and How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to [email protected] with your username.