Load unique shader instance

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Billy the Boy

I would like to load a tscn with a material attached. This material has a shader, which I would like to manipulate. However, the manipulated values seem to be shared across all shader (material) instances.

After some search I found following solution which does not work for me:

self.set_material(self.get_material().duplicate(true))

Making the material unique, doesn’t work either:

enter image description here

Furthermore, marking the shader directly as unique, doesn’t work either

enter image description here

Another try was to set a ShaderMaterial and the Shader directly within the GoDot Editor, marking their resources as Local To Scene. That didn’t work either.

enter image description here

The shader resource itself is also set to Local to Scene

enter image description here

I did play around with load/preload and duplicate on nearly everything, without any change. This is how I create the component with the material + shader attached

var shockWave = load("res://Components/ShockWaveComponent.tscn").instance()
get_tree().get_root().add_child(shockWave)
shockWave.explode(get_parent().position, 0.51, 0.15, 0.22, 0.02, 5000)

and this is the components ready function

func _ready():
    _shockWave_Material = self.get_material().duplicate(true)
    self.set_material(_shockWave_Material)

What else did I try: Loading the material with preload and setting the sprite’s material property afterwards with: preloadedmaterial.duplicate(), sadly with the same result.

GoDot Version:

Godot Engine v3.2.3.stable.official - https://godotengine.org
OpenGL ES 3.0 Renderer: GeForce RTX 3070/PCIe/SSE2

Tested in Godot 4:

It has already been answered but I’d like to give more precision on embedded vs external resource and Make Unique, for those who don’t need to share materials a lot.

Creating a brand new embedded ShaderMaterial for each new node (e.g. AnimatedSprite2D) will guarantee uniqueness, so you’ll have nothing to add in code. You can then save the material externally, but you must make sure not to share it with other entities (unfortunately, that defeats a little the purpose of saving it as external resource, as each entity will need its own material resource; except if the goal is to have clean commits indicating changes affecting materials only). Or, if you do share it, you’ll have to either Make it Unique again (whether kept embedded or then saved), or duplicate it at runtime as suggested by the answers.

In your case, the material was already an external asset, so you’d have to Make it Unique on every other entity using it. If you only clicked Make Unique on the entity from which you saved the resource as file, it changed nothing. If you have many other entities already using the material though, duplicating it at runtime seems your best bet, rather than selecting each of them one by one to Make it Unique (esp. if you need to propagate future changes to those entities).

Hyper Sonic | 2023-01-26 17:32

:bust_in_silhouette: Reply From: Kyle Szklenski

I would first off save your material out to its own file. Then I’d load it dynamically in ready. I’d duplicate() it after that, then just do material = duplicatedMaterial afterward. No need to call the setter directly like that, nor the getter. If you’re manipulating the material in code, you can probably just do:

material = material.duplicate()

Although I haven’t tried that specifically.

Sadly that doesn’t change anything. Still all shaders are affected while manipulating the parameters. Here is what you did suggest:

var _shader = load("res://Shaders/DonutWave.shader").duplicate(true)
_shockWave_Material = ShaderMaterial.new()
_shockWave_Material.shader = _shader
material = _shockWave_Material

So the material is not only unique, but new for each tscn instance. The shader has been loaded and is also a duplicate. I did use the property directly, instead of get_ set_ (is there a difference?)

Billy the Boy | 2020-12-06 18:16

I did update my question, as there were some new findings. (Local To Scene) But the issue is still present.

Billy the Boy | 2020-12-06 18:40

:bust_in_silhouette: Reply From: automatrio

Oh, man, I ran into the same issue and did the exact same stuff you did!

The curious thing is that, in 2D, the Duplicate() thing did actually work:

C#:

_sprite.Material = new ShaderMaterial() { Shader = (_sprite.Material as ShaderMaterial).Shader.Duplicate() as Shader };

But in 3D, I had to instantiate a new ShaderMaterial with a Duplicate() of the shader (it could’ve been an Instance(), for that matter) when the effect was called, and then again a new ShaderMaterial without a Shader when not.

C#:

public void Highlight(object sender, EventArgs args)
{
    if(LobbyGlobals.ObjectHoveredByMouse == (this as Node))
    {
        meshInstance.GetSurfaceMaterial(0).NextPass = new ShaderMaterial()
        {
            Shader = (Shader)highlightShader.Duplicate()
        };
    }
    else
    {
         meshInstance.GetSurfaceMaterial(0).NextPass = null;
    }
}

We should definitely post this as a ticket on GitHub, especially because the workarounds are never obvious and very cumbersome, if not performance-heavy.

But if I do find any better solution, I’ll get back to you.

:bust_in_silhouette: Reply From: Bowalt

Hi there. I have run into same issue as you, but I could make it work somehow. What helped me was to set Material resource - local to scene (same for shader as you are showing in pics) and save this as separate scene.

To not get confused calling everything “scene”, lets assume your instances with shader are called “Enemies” and your main scene is called “Level”

Now the crucial step (as I think this is the reason, why it works for me) is NOT to duplicate this “Enemies” to your “Level” using duplicate (cmd+d or ctrl+d) BUT by drag and dropping your instances from FileSystem → this did trick for me.

I didn’t tried to add “enemies” programatically so I am not sure if that is working, but for me it worked nicely - I tried to apply wind shader on AtlasTexture, where I needed to adjust displacement by wind depending the current frame I need to use.

If you are still interested, I hope it will help.

Also here is link to youtube video, where was covered the topic (approximately at 3:30) by ACB_Gamez: https://www.youtube.com/watch?v=tCcKzEUXkIw

It did work for me on ver 3.3

haigiodhh | 2021-06-21 16:07