How to optimise changing materials on a large BufferGeometry in THREE.js

566 Views Asked by At

Requirements

Render a very large geometry (>1 million triangles) via webgl, and via user interaction change the color of some of the triangles. 60fps should be maintained. The geometry cannot be simplified, should be rendered as is, with all the triangles.

The problem

The render cycle takes too much time, sometimes up to 100ms.

What I've tried

I've tried rendering the scene via THREE.js buffered geometry, grouping the triangles which share the same colour (as long as their indices are in a row) and reusing the materials via materialIndex of the groups.

//Declare three.js variables
let camera, scene, renderer, mesh

//assign three.js objects to each variable
function init() {

  //camera
  camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight);
  camera.position.z = 2000;
  //scene
  scene = new THREE.Scene();
  //renderer
  renderer = new THREE.WebGLRenderer({
    antialias: true
  });
  //set the size of the renderer
  renderer.setSize(window.innerWidth, window.innerHeight);

  //add the renderer to the html document body
  document.querySelector('.webgl').appendChild(renderer.domElement);
}


function addMesh() {
  const bufferGeometries = []
  for (var i = 0; i < 50000; i++) {
    var geo = new THREE.BoxGeometry(15, 15, 15)
    geo.applyMatrix4(new THREE.Matrix4().makeTranslation(Math.random() * 1500 - 500, Math.random() * 1500 - 500, 0));
    geo.rotateX(Math.random() * 1)
    geo.rotateY(Math.random() * 1)
    bufferGeometries.push(new THREE.BufferGeometry().fromGeometry(geo))
  }
  const mergedBufferGeometries = mergeBufferGeometries(bufferGeometries, true)
  mesh = new THREE.Mesh(mergedBufferGeometries, new THREE.MeshNormalMaterial());
  
  mesh.material = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((value) => new THREE.MeshPhongMaterial({
    emissive: new THREE.Color(`hsl(${value * i}, 100%, 50%)`),
    side: THREE.DoubleSide,
    polygonOffset: true,
    polygonOffsetFactor: 1,
    polygonOffsetUnits: 1,
    transparent: false,
    depthWrite: false
  }))
  mesh.geometry.groups = mesh.geometry.groups.map(group => ({
    ...group,
    materialIndex: 0
  })) // assign the first material too all the groups
  scene.add(mesh);
}

window.animate = function() {
  mesh.geometry.groups = mesh.geometry.groups.map((group, i) =>
    i < 100 ? // assign random materials to the first 1,00 items
    ({
      ...group,
      materialIndex: Math.floor(Math.random() * 10)
    }) :
    group
  )


  performance.mark('a');
  render()
  performance.measure('duration', 'a')
  const entries = performance.getEntriesByType("measure")
  document.getElementById('time').innerText = Math.round(entries[0].duration)
  performance.clearMeasures()
  performance.clearMarks()
}

function render() {
  //render the scene
  renderer.render(scene, camera);
}

init();
addMesh();
render();
window.animate();// assign random colors one first time

// below is copied from https://github.com/mrdoob/three.js/blob/dev/examples/js/utils/BufferGeometryUtils.js
function mergeBufferAttributes(attributes) {

  var TypedArray;
  var itemSize;
  var normalized;
  var arrayLength = 0;

  for (var i = 0; i < attributes.length; ++i) {

    var attribute = attributes[i];

    if (attribute.isInterleavedBufferAttribute) {

      console.error('THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. InterleavedBufferAttributes are not supported.');
      return null;

    }

    if (TypedArray === undefined) TypedArray = attribute.array.constructor;
    if (TypedArray !== attribute.array.constructor) {

      console.error('THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.');
      return null;

    }

    if (itemSize === undefined) itemSize = attribute.itemSize;
    if (itemSize !== attribute.itemSize) {

      console.error('THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.');
      return null;

    }

    if (normalized === undefined) normalized = attribute.normalized;
    if (normalized !== attribute.normalized) {

      console.error('THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.');
      return null;

    }

    arrayLength += attribute.array.length;

  }

  var array = new TypedArray(arrayLength);
  var offset = 0;

  for (var i = 0; i < attributes.length; ++i) {

    array.set(attributes[i].array, offset);

    offset += attributes[i].array.length;

  }

  return new THREE.BufferAttribute(array, itemSize, normalized);

}


function mergeBufferGeometries(geometries, useGroups) {

  var isIndexed = geometries[0].index !== null;

  var attributesUsed = new Set(Object.keys(geometries[0].attributes));
  var morphAttributesUsed = new Set(Object.keys(geometries[0].morphAttributes));

  var attributes = {};
  var morphAttributes = {};

  var morphTargetsRelative = geometries[0].morphTargetsRelative;

  var mergedGeometry = new THREE.BufferGeometry();

  var offset = 0;

  for (var i = 0; i < geometries.length; ++i) {

    var geometry = geometries[i];
    var attributesCount = 0;

    // ensure that all geometries are indexed, or none

    if (isIndexed !== (geometry.index !== null)) {

      console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.');
      return null;

    }

    // gather attributes, exit early if they're different

    for (var name in geometry.attributes) {

      if (!attributesUsed.has(name)) {

        console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure "' + name + '" attribute exists among all geometries, or in none of them.');
        return null;

      }

      if (attributes[name] === undefined) attributes[name] = [];

      attributes[name].push(geometry.attributes[name]);

      attributesCount++;

    }

    // ensure geometries have the same number of attributes

    if (attributesCount !== attributesUsed.size) {

      console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. Make sure all geometries have the same number of attributes.');
      return null;

    }

    // gather morph attributes, exit early if they're different

    if (morphTargetsRelative !== geometry.morphTargetsRelative) {

      console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. .morphTargetsRelative must be consistent throughout all geometries.');
      return null;

    }

    for (var name in geometry.morphAttributes) {

      if (!morphAttributesUsed.has(name)) {

        console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '.  .morphAttributes must be consistent throughout all geometries.');
        return null;

      }

      if (morphAttributes[name] === undefined) morphAttributes[name] = [];

      morphAttributes[name].push(geometry.morphAttributes[name]);

    }

    // gather .userData

    mergedGeometry.userData.mergedUserData = mergedGeometry.userData.mergedUserData || [];
    mergedGeometry.userData.mergedUserData.push(geometry.userData);

    if (useGroups) {

      var count;

      if (isIndexed) {

        count = geometry.index.count;

      } else if (geometry.attributes.position !== undefined) {

        count = geometry.attributes.position.count;

      } else {

        console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. The geometry must have either an index or a position attribute');
        return null;

      }

      mergedGeometry.addGroup(offset, count, i);

      offset += count;

    }

  }

  // merge indices

  if (isIndexed) {

    var indexOffset = 0;
    var mergedIndex = [];

    for (var i = 0; i < geometries.length; ++i) {

      var index = geometries[i].index;

      for (var j = 0; j < index.count; ++j) {

        mergedIndex.push(index.getX(j) + indexOffset);

      }

      indexOffset += geometries[i].attributes.position.count;

    }

    mergedGeometry.setIndex(mergedIndex);

  }

  // merge attributes

  for (var name in attributes) {

    var mergedAttribute = mergeBufferAttributes(attributes[name]);

    if (!mergedAttribute) {

      console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed while trying to merge the ' + name + ' attribute.');
      return null;

    }

    mergedGeometry.setAttribute(name, mergedAttribute);

  }

  // merge morph attributes

  for (var name in morphAttributes) {

    var numMorphTargets = morphAttributes[name][0].length;

    if (numMorphTargets === 0) break;

    mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {};
    mergedGeometry.morphAttributes[name] = [];

    for (var i = 0; i < numMorphTargets; ++i) {

      var morphAttributesToMerge = [];

      for (var j = 0; j < morphAttributes[name].length; ++j) {

        morphAttributesToMerge.push(morphAttributes[name][j][i]);

      }

      var mergedMorphAttribute = mergeBufferAttributes(morphAttributesToMerge);

      if (!mergedMorphAttribute) {

        console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed while trying to merge the ' + name + ' morphAttribute.');
        return null;

      }

      mergedGeometry.morphAttributes[name].push(mergedMorphAttribute);

    }

  }

  return mergedGeometry;

}
html, body {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
}

.webgl {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: -1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r119/three.min.js"></script>
<div class="webgl"></div>
<button onClick='window.animate()'>refresh colors</button>
<div style='color: white'>requied time <span id='time'>ms</span></div>

Ideal solution

Rerender only the triangles/pixels needed, where the colour changes. I've tried to read through THREE.js documentation for partial rendering, but to no avail. I've only found setDrawRange which renders only certain indices, but actually "unrenders" the rest which isn't what I want.

0

There are 0 best solutions below