Re-use Shader with almost identical parameters

I’m working on a demo which uses a shader to draw to a texture on a series of planes. Each plane shows something slightly different, but, this is controlled by a single value. Everything else is identical.

The issue I have is, I can’t see a way to share that shader between all the planes and just pass in a reference to the different value…

It would seem that the easiest way for me to deal with this tidily, is to have a class for each plane, which would create a new version of each shader which would then have this identifying value.

But, is there a way of reusing one shader?

What I would try is to set a custom attribute (or more, if needed) on the plane geometry and use the attribute’s value in the shader. So, you would share the same material across all your planes. I don’t know how many planes you are planning to have, but this even works with InstancedMesh.

For example, I’m using this method to display thousands of labels, I created a custom InstancedSprite, and the custom attribute controls the offset on the texture atlas of the labels.

(I probably just need to try this out - hopefully I’ll have some time tomorrow)

The bit I can’t quite ‘see’ at the moment is how I’d update the shader per texture, it seems like I’d have to have a loop the uniform in a loop?

Uniforms are ‘global’, if you change it, it changes everywhere. That’s why you need the custom attribute, which is unique for every plane in your case.

You can access the attributes in the vertex shader and pass them to the fragment shader using varying variables, if needed.

Why can’t you just set a uniform?

But, is there a way of reusing one shader?

Yes. absolutely.

You can have multiple instances of ShaderMaterial each with it’s own uniforms object but reusing the same vertex/fragment shaders. This is how built in materials work.
Uniforms are global per draw call, each draw call (mesh + material) can have a different uniform value, at the same address.

Yes, absolutely, maybe I was not clear, you are right.
I went one step further, suggesting to just have one material with the shader and use the same material on every plane, which would be more optimized. But of course in this case uniforms would not work on a per plane basis, that’s what I meant by ‘global’, as in per material.

If you just use the same shader with different materials, uniforms are perfectly fine.

Actually it would not be optimized. It’s the same effect as having as many material instances as you have meshes. An optimization would be to merge or instance all of the planes and then draw it with one draw call.

My advice is to install the spector extension, you can then capture a frame and see what threejs does in terms of webGL. (Eg you’ll see very few calls to update uniforms between each draw call)

1 Like

This extension might help for that, to handle per-mesh uniforms, so you can change the uniforms before each drawcall.

material.clone(), so I have my own shader instance with its own uniforms for each plane. Since the positions of the mesh vertices are global, I can simultaneously define my own global uv but have control for each plane with its own uniforms

const mesh = new THREE.Mesh( geometry, material.clone() );

If you now access the individual uniforms with “mesh.material.uniforms.custom.value = …” and change them, this will only affect your current plane and nothing else.

A warning in advance if you want to use MeshBasicNodeMaterial (WebGPU). Then I realized that cloning didn’t work. The parameters in it are apparently not cloned.

My favorite pattern is:

class MyShaderMaterial extends ShaderMaterial {....}

const material0 = new MyShaderMaterial(0)
const material1 = new MyShaderMaterial(1)