three.jsglslwebgluv-mappinggeometry-instancing

Per instance UV texture mapping in Three.js InstancedBufferGeometry


I have a InstancedBufferGeometry made up of a single Plane:

const plane = new THREE.PlaneBufferGeometry(100, 100, 1, 1);
const geometry = new THREE.InstancedBufferGeometry();
geometry.maxInstancedCount = 100;
geometry.attributes.position = plane.attributes.position;

geometry.index = plane.index;
geometry.attributes.uv = plane.attributes.uv;

geometry.addAttribute( 'offset', new THREE.InstancedBufferAttribute( new Float32Array( offsets ), 3 ) ); // an offset position

I am applying a texture to each plane, which is working as expected, however I wish to apply a different region of the texture to each instance, and I'm not sure about the correct approach.

At the moment I have tried to build up uv's per instance, based on the structure of the uv's for a single plane:

let uvs = [];

for (let i = 0; i < 100; i++) {
    const tl = [0, 1];
    const tr = [1, 1];
    const bl = [0, 0];
    const br = [1, 0];
    uvs = uvs.concat(tl, tr, bl, br);
}

...

geometry.addAttribute( 'uv', new THREE.InstancedBufferAttribute( new Float32Array( uvs ), 2) );

When I do this, I don't have any errors, but every instance is just a single colour (all instances are the the same colour). I have tried changing the instance size, and also the meshes per attribute (which I don't fully understand, struggling to find a good explanation in the docs).

I feel like I'm close, but I'm missing something, so a point in the right direction would be fantastic!

(For reference, here are my shaders):

const vertexShader = `
    precision mediump float;

    uniform vec3 color;
    uniform sampler2D tPositions;

    uniform mat4 modelViewMatrix;
    uniform mat4 projectionMatrix;

    attribute vec2 uv;
    attribute vec2 dataUv;
    attribute vec3 position;
    attribute vec3 offset;
    attribute vec3 particlePosition;
    attribute vec4 orientationStart;
    attribute vec4 orientationEnd;

    varying vec3 vPosition;
    varying vec3 vColor;
    varying vec2 vUv;

    void main(){
        vPosition = position;
        vec4 orientation = normalize( orientationStart );
        vec3 vcV = cross( orientation.xyz, vPosition );
        vPosition = vcV * ( 2.0 * orientation.w ) + ( cross( orientation.xyz, vcV ) * 2.0 + vPosition );

        vec4 data = texture2D( tPositions, vec2(dataUv.x, 0.0));
        vec3 particlePosition = (data.xyz - 0.5) * 1000.0;
        vUv = uv;

        vColor = data.xyz;

        gl_Position = projectionMatrix * modelViewMatrix * vec4(  position + particlePosition + offset, 1.0 );
    }
`;

const fragmentShader = `
    precision mediump float;

    uniform sampler2D map;

    varying vec3 vPosition;
    varying vec3 vColor;
    varying vec2 vUv;

    void main() {
        vec3 color = texture2D(map, vUv).xyz;

        gl_FragColor = vec4(color, 1.0);
    }
`;

Solution

  • As all my instances need to take the same size rectangular area, but offset (like a sprite sheet), I have added a UV offset and UV scale attribute to each instance, and use this to define which area of the map to use:

    const uvOffsets = [];
    for (let i = 0; i < INSTANCES; i++) {
        const u = i % textureWidthHeight;
        const v = ~~ (i / textureWidthHeight);
        uvOffsets.push(u, v);
    }
    
    ...
    
    geometry.attributes.uv = plane.attributes.uv;
    geometry.addAttribute( 'uvOffsets', new THREE.InstancedBufferAttribute( new Float32Array( uvOffsets ), 2 ) );
    
    uniforms: {
        ...
        uUvScale: { value: 1 / textureWidthHeight }
    }
    

    And in the fragment shader:

    void main() {
        vec4 color = texture2D(map, (vUv * uUvScale) + (vUvOffsets * uUvScale));
        gl_FragColor = vec4(1.0, 1.0, 1.0, color.a);
    }
    

    \o/