InstancedUniformsMesh - set shader uniform values per instance

Hi, threeps! I’ve just released a tool that extends Three’s InstancedMesh to allow setting the values of any float/vector shader uniform for each instance independently. InstancedUniformsMesh adds a single new method, setUniformAt(name, index, value), which when called will seamlessly upgrade the geometry and material behind the scenes to let it be varied per instance.

import { InstancedUniformsMesh } from 'three-instanced-uniforms-mesh'

// ...

const mesh = new InstancedUniformsMesh(someGeometry, someMaterial, count)
for (let i = 0; i < count; i++) {
  mesh.setMatrixAt(i, ...)
  mesh.setUniformAt('someUniform', i, Math.random()) // <-- The magic happens here
}

You can use this with any of the built-in Three.js materials to vary things like emissive, roughness, metalness, etc., but it is particularly useful when used with custom ShaderMaterials or materials with shaders modified by onBeforeCompile. This tool lets you declare configurable values as uniforms in your shader, and then you can use it non-instanced with a regular Mesh or instanced using InstancedUniformsMesh with the exact same shader code.

Examples:

image
Instanced BezierMesh (custom shader uniforms)

image
Instanced metalness/roughness (plain MeshStandardMaterial)

NPM package:

Source code:

I’ve been using this approach for a few years so it should be pretty robust, but if you find any issues please open a ticket.

I hope folks find this as useful as I do!

7 Likes

Hi, great work!
I’d love to know a little more about this. How does it work under the hood? What are the advantages and disadvantages compared to using InstancedBufferAttribute ?

@DolphinIQ thanks for asking! Under the hood it actually just manages extra InstancedBufferAttributes and modifies the shader code to convert uniform to attribute+varying where needed. The end result should be no different than if you wrote your geometry+material using InstancedBufferAttribute manually.

The big advantages IMO are ergonomics and flexibility: materials can be agnostic to whether they’re going to be used with or without instancing. The shader can be written assuming a non-instanced Mesh usage with simple uniform declarations, and then later on any set of those uniforms can be instanced with no extra changes.

Varying the roughness on MeshStandardMaterial is a great example; to do that manually would be quite a bit of work involving modifying the shader and adding an attribute to every geometry using it. With this it’s just a single setUniformAt() call.

As for downsides, I suppose it’s an extra layer of “magic” to debug if something goes wrong. :slight_smile:

1 Like

Great Work and thank you for sharing!
Is there a chance to use it with texture (a texture per instance).
From what i read in the readme it works with float and vec* types at the moment?

No, unfortunately instancing only supports vector types. But if you had a small fixed set of textures you could add them all to the material, perhaps packed like spritesheets, and then use a float uniform to tell each instance which one to use.

1 Like

Thanks @lojjic, my use case is that I load dynamically Mapbox tiles and would like them to be represented by a plane geometry (instance). It is neither small nor fixed set… and it needs to be served sequentially (not as atlas / spritesheet). I guess passing the texture as an InstancedBufferAttribute of instanced mesh geometry would work but not sure how to do it yet.

Would this require modifying the shader? I want to do the exact thing that you have described, but just can’t wrap my head around how the value would be used by the shader.

I tried setting the uniform offset as that’s something I had seen in the draw call for the texture (by default always set to -1), and as a stab in the dark tried uv, but I always end up with the entire spritesheet on every face of every instance.

Do I have to modify this line in the onBeforeCompile callback? If yes, what do I replace it with?

vUv = ( uvTransform * vec3( uv, 1 ) ).xy;