Setting Shader Parameter at runtime?

Godot Version

v4.6.1 Mono

Question

I have a shader that swaps color palette. It takes in two textures, one for source palette and other for target. The shader will then swap every color on the image that maps onto the source palette with the target palette. The shader works as intended.

However, when trying to swap out these textures at run time i have weird issues. It works exactly as intended for my Control nodes that uses TextureRect to display an Icon (that uses the shader, so the colors in the icon are swapped correctly). However, it does not work as intended for Sprite2D, even though im using the shader the same way.

For my Sprite2D, i can only set shader parameters in _Ready, if i try to do it elsewhere it doesnt work, unless i share the same material across all instances (by sett LocalToScene in material to false)

It works for my ItemSlotGui, which is a control node.
public partial class ItemSlotGui : GenericButton
{
TextureRect Icon;
ShaderMaterial _material;

public override void _Ready()
{
    Icon = FindChild("Icon") as TextureRect;     
    _material = (ShaderMaterial)Icon.Material;
    Icon.Material = _material;
}

public void SetItem(Item item, Item compareItem = null)
{
    if (item != null)
    {
        if (item.Material != ItemMaterial.None)
        {
            Texture2D palette = GuiManager.Instance.MaterialPalettes[item.Material]; //Gets a "color palette" texture
            _material.SetShaderParameter("target_palette_texture", palette);
            Icon.Material = _material;
        }
        else
        {
            Icon.Material = null;
        }
    }
}

}

It does not work for ArmorPart, which is a Sprite2D.
public partial class ArmorPart : Sprite2D
{
private ShaderMaterial _material;

public override void _Ready()
{
    _material = (ShaderMaterial)Material;
    var pal = GuiManager.Instance.MaterialPalettes[ItemMaterial.Iron];
    _material.SetShaderParameter("target_palette_texture", pal); //When setting the Parameter in _Ready, it works. 

    GD.Print($"[_Ready] Material RID: {_material.GetRid()} " + " / " + Name); //This prints the same RID as the Print in SetShader.
}

public void SyncFramesFrom(Sprite2D source)
{
    Hframes = source.Hframes;
    Vframes = source.Vframes;
}

public void SetShader(ItemMaterial material)
{
    GD.Print($"Material RID: {Material?.GetRid()}"); //Same RID as below and as in _Ready
    GD.Print($"_material RID: {_material?.GetRid()}"); //Same RID as above and as in _Ready
	
    var pal = GuiManager.Instance.MaterialPalettes[material];
    _material.SetShaderParameter("target_palette_texture", pal); //This doesnt work, even though its the same thing i do in ItemSlotGui and in _Ready.
	
    QueueRedraw(); //Tried doing this, but doesnt change anything
}

}

However, ArmorPart does set the shader correctly if “Local to Scene” is false in the material setting for the scene (ArmorPart scene). However, different armorparts should be able to have different shaders, so “Local to Scene” has to be true.
I also tried doing:
_material = (ShaderMaterial)Material.Duplicate();
Material = _material;
Inside the _Ready for ArmorPart, this gave me the same result (only the _Ready Parameter change matters)

I’d print material in SetShader()

Btw do something about naming. There’s too many “materials”. You have Material, _material and material. Easy to get confused.

I did “GD.Print(" material: " + Material);” in both SetShader and _Ready. The print is the same for the same object in SetShader and _Ready.

Not sure how this helps me. Since the ShaderMaterial is the same, setting the Parameters should work.

It should if the type of the value you’re setting it to is compatible with the uniform type, so I’d start by investigating that value.

Not sure what you mean by that. I am setting the “target_palette_texture” parameter to a Texture2D. I use the same textures in Ready, SetParameter and in ItemSlotGui. It works for ItemSlotGui, but not for ArmorPart, but i use the same textures. Setting in _Ready for ArmorPart works, but not SetParameter, even when using the same Textures.

So doing:

var pal = GuiManager.Instance.MaterialPalettes[ItemMaterial.Iron];
    _material.SetShaderParameter("target_palette_texture", pal);

in _Ready for ArmorPart, makes it so the sprite swaps the colors to the Iron palette. But if i do that exact code snippet in SetShader, the colors are not swapped to Iron palette. Its the same code, same material, same texture, everything same, only difference is where i do it.

It doesn’t look the same in the code you posted. _Ready() does:

var pal = GuiManager.Instance.MaterialPalettes[ItemMaterial.Iron];

while SetShader() does:

var pal = GuiManager.Instance.MaterialPalettes[material];

And you haven’t shown code that calls SetShader()

Well, i have tried many different things, in different combinations.

The code in the OP is how i intend the code to work. An Item has an ItemMaterial, which is just an enum,
The MaterialPalettes in GuiManager is just a dictionary that maps ItemMaterial enums to a Texture2D.

So if the item has ItemMaterial set to Iron, it would functionally be the same as doing:
MaterialPalettes(ItemMaterial.Iron};

However, no matter what ItemMaterial the Item has, it doesnt work. And if i ignore the ItemMaterial in Item, and instead just hard code the material in SetShader,
like so:
var pal = GuiManager.Instance.MaterialPalettes[ItemMaterial.Iron];

It still doesnt work. But if i hardcode it in _Ready, it does work. I cannot apply the “correct” material in _Ready, because “ArmorPart” are instances on the Character that are not created/deleted after Character has been instansiated.
The Sprite/Texture that the ArmorPart uses is instead swapped when player equip an item, and thats when i also want to set the shader.
So SetShader for ArmorPart is called once player Equips an Item. So the same ArmorPart will display different textures with different color palettes for the shader, which is why i need to be able to SetShader after _Ready.

I have tried duplicating the material like so:
_material = (ShaderMaterial)Material.Duplicate();
Material = _material;

I have tried that in combination with “Local to Scene” setting true/false on the ArmorPart material setting (in scene inspector).

The only thing that lets me set the shader parameters after _Ready is having “Local to Scene” = false and NOT duplicate the material. Then it works, HOWEVER, then all ArmorParts share the same material, and thus will have the same color palette swaps.
I need to have eeach ArmorPart have its own Unique material instance, whilst also being able to set parameters after _Ready.

AS a reminder, this all works for ItemSlotGui. There i can change the Shader parameters after _Ready. I can update the shader material a hundred times, it works, the material is not shared between instances. Everything works as intended.
When doing the same thing but for Sprite2D, it doesnt work.

I’ve also tried doing things like setting Material = _material in SetShader.

Even duplicating the material, and applying the duplication fails if “Local to Scene” is false:
SetShader()
…
_material = (ShaderMaterial)Material.Duplicate();
_material.SetShaderParameter(“target_palette_texture”, pal);
Material = _material;
…

The problem is not the texture, or what i am setting the ShaderParameter too. As i said. It works in _Ready. Using the same textures in SetSHader, does not work, UNLESS: “Local to Scene” is false and i dont duplicate materials, however, then all armorparts share the same material, which i dont want.

Looks like a bug somewhere in your setup or code. Can you make a MRP?

I can’t see on your examples the type of data you are assigning to the shader but check that a texture parameter in the shader requires a Texture2D type to be assigned on GDScript.

I was setting up a MRP, but realized i couldnt quite reproduce the issue, and then went back to my code to look at it more thouroughly. Turned out i had a copy of the sprite and applied paramater changes to that copy instead of the actual sprite (due to how generated the nodes, they were not prebuilt in a scene, so couldnt see that there was a copy).

I feel dumb, spent a whole day on it. But thanks for trying to help.

2 Likes

Making a MRP always helps, one way or another :smile:

1 Like