Simple Shader to Change Color

Godot Version

4.51 stable

Question

I created a simple shader to change color. If I add a texture rectangle to the scene and set the material in the inspector everything works fine. But I've spent the last four hours trying to get it to work at runtime. I switched from the original project to just isolate the problem but am unable to find it.

O created a scene it has a Panel at the top level. The panel has two child texture rectangles each with the shader applied one to turn red blue, and one to turn red green.

In the scene’s ready method I try and add a new texture rectangle and add the ShaderMaterial that is used by the other texture rectangles that were configured in the editor. I’m able to set the parameter values for the shader, the values being the same as the first texture rectangle that was added to the panel, but it has no effect. For reference see the attached image, the blue desk and green chair are configured in the editor. The red cabinet is set to the same shader values as the red desk that is blue in the final image, however it remains untouched.

Here’s the code in question:

public override void _Ready()
{
    TextureRect rect = new TextureRect();
    ShaderMaterial material = ResourceLoader.Load<ShaderMaterial>("res://color_material.tres");
    
    material.SetShaderParameter("target_color", new Color(255, 0, 0)); // Example: Red
    material.SetShaderParameter("replacement_color", new Color(0, 0, 255));
    material.SetShaderParameter("tolerance", 0.1);
    Texture2D texture = ResourceLoader.Load<Texture2D>("res://cabinet.png");
    
    rect.Texture = texture;
    rect.SetMaterial(material); //= material;
    rect.Position = new Vector2(128,128);
    
    AddChild(rect);
    
    var m = (ShaderMaterial)rect.Material;
    
    var x = m.GetShaderParameter("target_color");
    GD.Print("Target Color:" + x);
    
    var y = m.GetShaderParameter("replacement_color");
    GD.Print("Replacement Color:" + y);
    
    var z = m.GetShaderParameter("tolerance");
    GD.Print("Tolerance:" + z);
    
}

Note the print statements show the correct values when the project is run.

In case it matters, here’s the shader:

shader_type canvas_item;

uniform vec4 target_color : source_color;
uniform vec4 replacement_color : source_color;
uniform float tolerance : hint_range(0.0, 1.0) = 0.1;

void fragment() {
vec4 current_color = texture(TEXTURE, UV);
// Calculate distance between current pixel and target color
if (distance(current_color.rgb, target_color.rgb) <= tolerance) {
// Replace color while maintaining transparency
COLOR = vec4(replacement_color.rgb, current_color.a);
} else {
COLOR = current_color;
}
}

//void light() {
//	// Called for every pixel for every light affecting the CanvasItem.
//	// Uncomment to replace the default light processing function with this one.
//}

Any help would be greatly appreciated as I have spent a long time on this and tried many things.

Run the scene and click on the remote-tab in the editor and inspect your newly created texture-rect

grafik

To look for what?

If I examine the texture rectangle it looks fine. It’s Material property shows the proper resource path for the material. The shader property of the Material object also shows the right resouce path and the Code property is correct in the debugger.

using the remote tab and inspecting the Material property shows it as the same settings as the blue desk. The only difference I can find is that Material is listed by objectid for the two rectangles created in the editor, but shows the resource file as the Material in the created version

An image of what the Material property looks like when examined in remote.

I also notice that when in remote mode if I try to change the replacement color for the newly created texture rectangle it cause the texture rectangle to loose it’s texture.

If I try to change the replacement color in either of the two rectangles created in the editor it does not update the shader. Is that something to do with the shader or the material perhaps?

Further Edit:

So I created a scene with one item a texture rectangle. Set the material and ran the scene it looked okay. I then changed _Ready() to be the following:

// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
//Texture = ResourceLoader.Load(“res://cabinet.png”);
ShaderMaterial m = (ShaderMaterial)Material;
m.SetShaderParameter(“target_color”, new Color(255, 0, 0)); // Example: Red
m.SetShaderParameter(“replacement_color”, new Color(0, 0, 255));
m.SetShaderParameter(“tolerance”, 0.1);

}

With that code in _Ready() the shader doesn’t work anymore. It also stops working if I instantiate the scene and change the texture.

Instead of loading the shadermaterial, create a new one in code and only load the shader and assign it to the newly created material and try again.

The issue might be due to local_to_scene having odd behaviour with shader-material

Resource.resource_local_to_scene only works for instanced scenes. If the Resource is used in multiple places of the same scene it will be shared.

You can use per-instance uniforms to use the same material and modify uniforms per-instance.

herrspaten

I tried that, I also tried creating the code for the shader at runtime, and it still doesn’t work. I also tried a singleton that loads the material, and I used Duplicate(true) but it’s still the same behavior.

mrcdk

Even when I create an instanced scene with only a texture rectangle, and the shader, when I instantiate the scene it works as long as the texture used in the scene doesn’t change. If the I change the texture in the texture rectangle of the instantiated scene the shader stops working.

I also tried setting the uniforms to instance, and had the exact same behavior, only difference was how the values showed up in the inspector. I spent almost 8 hours yesterday trying everything I could think of.

Color() constructor goes from 0.0 to 1.0. If you want to use 0 to 255 you need to use Color.from_rgba8()

instance colors have to be converted from srgb to linear (or the otherway around, i forgot)

So you tried it without “local_to_scene” active?

I started over from scratch and have something that almost works. I just can’t set the parameters at runtime.

Here’s my code:

public partial class PanelChild : TextureRect
{
    private Shader shader;

    public PanelChild()
    {
        shader = new Shader();

        shader.Code = """
                      shader_type canvas_item;
                      
                      uniform vec4 target_color : source_color;
                      uniform vec4 replacement_color : source_color;
                      uniform float tolerance : hint_range(0.0, 1.0) = 0.1;
                      
                      void fragment() {
                          vec4 current_color = texture(TEXTURE, UV);
                          // Calculate distance between current pixel and target color
                          if (distance(current_color.rgb, target_color.rgb) <= tolerance) {
                              // Replace color while maintaining transparency
                              COLOR = vec4(replacement_color.rgb, current_color.a);
                          } else {
                              COLOR = current_color;
                          }
                      }
                      
                      //void light() {
                      //    // Called for every pixel for every light affecting the CanvasItem.
                      //    // Uncomment to replace the default light processing function with this one.
                      //}
                      


                      """;
        ShaderMaterial material = new ShaderMaterial();
        material.Shader = shader;
        SetMaterial(material);
    }

    public void setCustomColor()
    {
        ShaderMaterial material = (ShaderMaterial)Material;
        material.SetShaderParameter("target_color", new Color(255, 0, 0)); // Example: Red
        material.SetShaderParameter("replacement_color", new Color(0, 0, 255));
        material.SetShaderParameter("tolerance", 0.1);  
        
    }
    public override void _Ready()
    {
        /*
        ShaderMaterial material = new ShaderMaterial();
        material.Shader = shader;
        SetMaterial(material); */
      
    }

    // Called every frame. 'delta' is the elapsed time since the previous frame.
    public override void _Process(double delta)
    {
        //QueueRedraw();
    }
}

If I use this code to load it, then when accessed through remote I can change the Shader parameter values and they work correctly.

PanelChild texRect = new PanelChild();
Texture2D cabinet = ResourceLoader.Load<Texture2D>("res://desk.png");
texRect.SetTexture(cabinet);
texRect.Position = new Vector2(200,128);
AddChild(texRect);

If I add the following line to the code to manually set the parameters, not only do they not work, when the object is accessed in remote it no longer responds to changes in the shader parameter values.

texRect.setCustomColor();

Thanks for all the help so far.

Edit:

It does look like it’s the way I set the color values. I changed the setCustomColor method to be like this and it works sort of:

  ShaderMaterial material = (ShaderMaterial)Material;
  //  material.SetShaderParameter("target_color", new Color(255, 0, 0)); // Example: Red
//    material.SetShaderParameter("replacement_color", new Color(0, 0, 255));
    Color red = Color.Color8(255, 0, 0, 1);
    Color green = Color.Color8(0, 255, 0, 1);
    material.SetShaderParameter("target_color", red);
    material.SetShaderParameter("replacement_color", green);
    material.SetShaderParameter("tolerance", 0.1);  

Using Color 8 works, but in remote mode the values don’t show properly though changing them still works.

Final Edit:

That was indeed it. Changed my color constructors and all is working properly. Just have to figure out how to use the color picker now, Many thanks for all the help.