Search code examples
three.js

InstancedMesh with unique texture per instance


I am looking for a way to draw several objects with unique textures. I came across this old question about instancedMesh where someone got the multiple instances with different textures but on desktop, textures have weird artifacts. Initially I thought something must be wrong with that demo but everything seems fine to me, I also tried to use mix functions in place of conditionals but textures still have artifacts.

I have been looking for different ways to draw multiple unique geometries so merging geometries isn't an option, but most results I get are for multiple objects with merged geometry. Would be great if someone can offer some guidance.

var camera, scene, renderer, stats;

var mesh;
var amount = parseInt(window.location.search.substr(1)) || 10;
var count = Math.pow(amount, 3);

var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2(1, 1);

var rotationTheta = 0.1;
var rotationMatrix = new THREE.Matrix4().makeRotationY(rotationTheta);
var instanceMatrix = new THREE.Matrix4();
var matrix = new THREE.Matrix4();

init();
animate();

function init() {

  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 10000);
  camera.position.set(amount, amount, amount);
  camera.lookAt(0, 0, 0);

  scene = new THREE.Scene();

  var light = new THREE.HemisphereLight(0xffffff, 0x000088);
  light.position.set(-1, 1.5, 1);
  scene.add(light);

  var light = new THREE.HemisphereLight(0xffffff, 0x880000, 0.5);
  light.position.set(-1, -1.5, -1);
  scene.add(light);

  var geometry = new THREE.BoxBufferGeometry(.5, .5, .5, 1, 1, 1);

  var material = [
    new THREE.MeshStandardMaterial({
      map: new THREE.TextureLoader().load('https://threejs.org/examples/textures/square-outline-textured.png')
    }),
    new THREE.MeshStandardMaterial({
      map: new THREE.TextureLoader().load('https://threejs.org/examples/textures/golfball.jpg')
    }),
    new THREE.MeshStandardMaterial({
      map: new THREE.TextureLoader().load('https://threejs.org/examples/textures/metal.jpg')
    }),
    new THREE.MeshStandardMaterial({
      map: new THREE.TextureLoader().load('https://threejs.org/examples/textures/roughness_map.jpg')
    }),
    new THREE.MeshStandardMaterial({
      map: new THREE.TextureLoader().load('https://threejs.org/examples/textures/tri_pattern.jpg')
    }),
    new THREE.MeshStandardMaterial({
      map: new THREE.TextureLoader().load('https://threejs.org/examples/textures/water.jpg')
    }),
  ];

  material.forEach((m, side) => {
    if (side != 2) return;

    m.onBeforeCompile = (shader) => {

      shader.uniforms.textures = {
        'type': 'tv',
        value: [
          new THREE.TextureLoader().load('https://threejs.org/examples/textures/crate.gif'),
          new THREE.TextureLoader().load('https://threejs.org/examples/textures/equirectangular.png'),
          new THREE.TextureLoader().load('https://threejs.org/examples/textures/colors.png')
        ]
      };


      shader.vertexShader = shader.vertexShader.replace(
        '#define STANDARD',
        `#define STANDARD
                        varying vec3 vTint;
                        varying float vTextureIndex;`
      ).replace(
        '#include <common>',
        `#include <common>
                    attribute vec3 tint;
                    attribute float textureIndex;`
      ).replace(
        '#include <project_vertex>',
        `#include <project_vertex>
                    vTint = tint;
                    vTextureIndex=textureIndex;`
      );

      shader.fragmentShader = shader.fragmentShader.replace(
          '#define STANDARD',
          `#define STANDARD
                        uniform sampler2D textures[3];
                        varying vec3 vTint;
                        varying float vTextureIndex;`
        )
        .replace(
          '#include <fog_fragment>',
          `#include <fog_fragment>
                    int texIdx = int(vTextureIndex);
                    vec4 col;
                    if (texIdx == 0) {
                            col = texture2D(textures[0], vUv );
                        } else if ( texIdx==1) {
                            col = texture2D(textures[1], vUv );
                        } else if ( texIdx==2) {
                                col = texture2D(textures[2], vUv );
                            }

                            gl_FragColor = col;
                    //      gl_FragColor.rgb *= vTint;`

        );
    }
  });

  mesh = new THREE.InstancedMesh(geometry, material, count);

  var i = 0;
  var offset = (amount - 1) / 2;

  var transform = new THREE.Object3D();
  var textures = [];

  for (var x = 0; x < amount; x++) {

    for (var y = 0; y < amount; y++) {

      for (var z = 0; z < amount; z++) {

        transform.position.set(offset - x, offset - y, offset - z);
        transform.updateMatrix();

        mesh.setMatrixAt(i++, transform.matrix);


        textures.push(Math.random() < 0.3 ? 0 : (Math.random() < 0.5 ? 1 : 2));
      }

    }

  }

  geometry.setAttribute('textureIndex',
    new THREE.InstancedBufferAttribute(new Float32Array(textures), 1));

  scene.add(mesh);


  renderer = new THREE.WebGLRenderer({
    antialias: false
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  new THREE.OrbitControls(camera, renderer.domElement);

  stats = new Stats();
  stats.showPanel(1); // 0: fps, 1: ms, 2: mb, 3+: custom

  stats.domElement.classList.add("statsDom");
  document.body.appendChild(stats.domElement);

  window.addEventListener('resize', onWindowResize, false);
  document.addEventListener('mousemove', onMouseMove, false);

}

function onWindowResize() {

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);

}

function onMouseMove(event) {

  event.preventDefault();

  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

}

function animate() {

  requestAnimationFrame(animate);

  render();

}

function render() {

  raycaster.setFromCamera(mouse, camera);

  var intersection = raycaster.intersectObject(mesh);
  // console.log('intersection', intersection.length);
  if (intersection.length > 0) {

    mesh.getMatrixAt(intersection[0].instanceId, instanceMatrix);
    matrix.multiplyMatrices(instanceMatrix, rotationMatrix);

    mesh.setMatrixAt(intersection[0].instanceId, matrix);
    mesh.instanceMatrix.needsUpdate = true;

  }

  renderer.render(scene, camera);

  stats.update();

}
.statsDom {
  position: fixed;
  top: 0;
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stats.js/r16/Stats.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.9/dat.gui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.min.js"></script>

<div></div>


Solution

  • I believe your issue comes from converting a float to an int, and then using that to create branches. This bug shows up only in a few GPUs, not all of them. I got it to work by keeping vTextureIndex as float, sampling all 3 textures and multiplying each by 1 if the textureIndex matches, or multiplying by 0 if the textureIndex does not match.

    I basically replaced these lines:

    int texIdx = int(vTextureIndex);
    vec4 col;
    if (texIdx == 0) {
        col = texture2D(textures[0], vUv );
    } else if ( texIdx==1) {
        col = texture2D(textures[1], vUv );
    } else if ( texIdx==2) {
        col = texture2D(textures[2], vUv );
    }
    

    with this approach:

    float x = vTextureIndex;
    vec4 col;
    col = texture2D(textures[0], vUv ) * step(-0.1, x) * step(x, 0.1);
    col += texture2D(textures[1], vUv ) * step(0.9, x) * step(x, 1.1);
    col += texture2D(textures[2], vUv ) * step(1.9, x) * step(x, 2.1);
    
    • If textureIndex is 0, the first texture is multiplied by 1, the others by 0
    • If textureIndex is 1, the second texture is multiplied by 1, the others by 0
    • If textureIndex is 2, the third texture is multiplied by 1, the others by 0

    enter image description here

    var camera, scene, renderer, stats;
    
        var mesh;
        var amount = parseInt( window.location.search.substr( 1 ) ) || 10;
        var count = Math.pow( amount, 3 );
    
        var raycaster = new THREE.Raycaster();
        var mouse = new THREE.Vector2( 1, 1 );
    
        var rotationTheta = 0.1;
        var rotationMatrix = new THREE.Matrix4().makeRotationY( rotationTheta );
        var instanceMatrix = new THREE.Matrix4();
        var matrix = new THREE.Matrix4();
    
        init();
        animate();
    
        function init() {
    
            camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 10000 );
            camera.position.set( amount, amount, amount );
            camera.lookAt( 0, 0, 0 );
    
            scene = new THREE.Scene();
    
            var light = new THREE.HemisphereLight( 0xffffff, 0x666666 );
            light.position.set( - 1, 1.5, 1 );
            scene.add( light );
    
            var light = new THREE.HemisphereLight( 0xffffff, 0x666666, 0.5 );
            light.position.set( - 1, - 1.5, - 1 );
            scene.add( light );
    
            var geometry = new THREE.BoxBufferGeometry( .5, .5, .5, 1, 1, 1 );
    
            var material = [
                new THREE.MeshStandardMaterial({color: 0xff9900}),
                new THREE.MeshStandardMaterial({color: 0xff0099}),
                new THREE.MeshStandardMaterial( { map: new THREE.TextureLoader().load( 'https://threejs.org/examples/textures/metal.jpg' ) } ),
                new THREE.MeshStandardMaterial({color: 0x9900ff}),
                new THREE.MeshStandardMaterial({color: 0x0099ff}),
                new THREE.MeshStandardMaterial({color: 0x99ff00}),
            ];
    
            material.forEach((m,side)=>{
                if ( side!=2 ) return;
    
                m.onBeforeCompile = ( shader ) => {
    
                    shader.uniforms.textures = { 'type': 'tv', value: [
                        new THREE.TextureLoader().load( 'https://threejs.org/examples/textures/crate.gif' ),
                        new THREE.TextureLoader().load( 'https://threejs.org/examples/textures/sprite0.png' ),
                        new THREE.TextureLoader().load( 'https://threejs.org/examples/textures/sprite.png' )
                     ] };
    
    
                    shader.vertexShader = shader.vertexShader.replace(
                            '#define STANDARD',
                            `#define STANDARD
                            varying vec3 vTint;
                            varying float vTextureIndex;`
                    ).replace(
                        '#include <common>',
                        `#include <common>
                        attribute vec3 tint;
                        attribute float textureIndex;`
                    ).replace(
                        '#include <project_vertex>',
                        `#include <project_vertex>
                        vTint = tint;
                        vTextureIndex=textureIndex;`
                    );
    
                    shader.fragmentShader = shader.fragmentShader.replace(
                            '#define STANDARD',
                            `#define STANDARD
                            uniform sampler2D textures[3];
                            varying vec3 vTint;
                            varying float vTextureIndex;`
                    )
                    .replace(
                        '#include <fog_fragment>',
                        `#include <fog_fragment>
                        float x = vTextureIndex;
                        vec4 col;
                        col = texture2D(textures[0], vUv ) * step(-0.1, x) * step(x, 0.1);
                        col += texture2D(textures[1], vUv ) * step(0.9, x) * step(x, 1.1);
                        col += texture2D(textures[2], vUv ) * step(1.9, x) * step(x, 2.1);
    
                        gl_FragColor = col;
                        `
    
                    )
                    ;
                }
            });
    
            mesh = new THREE.InstancedMesh( geometry, material, count );
    
            var i = 0;
            var offset = ( amount - 1 ) / 2;
    
            var transform = new THREE.Object3D();
            var textures = [];
    
            for ( var x = 0; x < amount; x ++ ) {
    
                for ( var y = 0; y < amount; y ++ ) {
    
                    for ( var z = 0; z < amount; z ++ ) {
    
                        transform.position.set( offset - x, offset - y, offset - z );
                        transform.updateMatrix();
    
                        mesh.setMatrixAt( i ++, transform.matrix );
    
    
                        textures.push(Math.random()<0.3 ? 0 : (Math.random()<0.5 ? 1 : 2));
                    }
    
                }
    
            }
    
            geometry.setAttribute( 'textureIndex',
                  new THREE.InstancedBufferAttribute( new Float32Array(textures), 1 ) );
    
            scene.add( mesh );
    
    
            renderer = new THREE.WebGLRenderer( { antialias: false } );
            renderer.setPixelRatio( window.devicePixelRatio );
            renderer.setSize( window.innerWidth, window.innerHeight );
            document.body.appendChild( renderer.domElement );
    
            new THREE.OrbitControls( camera, renderer.domElement );
    
            stats = new Stats();
            document.body.appendChild( stats.dom );
    
            window.addEventListener( 'resize', onWindowResize, false );
            document.addEventListener( 'mousemove', onMouseMove, false );
    
        }
    
        function onWindowResize() {
    
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
    
            renderer.setSize( window.innerWidth, window.innerHeight );
    
        }
    
        function onMouseMove( event ) {
    
            event.preventDefault();
    
            mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
            mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
    
        }
    
        function animate() {
    
            requestAnimationFrame( animate );
    
            render();
    
        }
    
        function render() {
    
            raycaster.setFromCamera( mouse, camera );
    
            var intersection = raycaster.intersectObject( mesh );
    // console.log('intersection', intersection.length);
            if ( intersection.length > 0 ) {
    
                mesh.getMatrixAt( intersection[ 0 ].instanceId, instanceMatrix );
                matrix.multiplyMatrices( instanceMatrix, rotationMatrix );
    
                mesh.setMatrixAt( intersection[ 0 ].instanceId, matrix );
                mesh.instanceMatrix.needsUpdate = true;
    
            }
    
            renderer.render( scene, camera );
    
            stats.update();
    
        }
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/libs/stats.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>