How to create instanced mesh in drei with multiple parts

I wanted to improve the perfomance of the display of tiles, while still allowing the player to change the colors of separate pieces of an individual tile. So, I decided to use Instances from drei. When I convert the glb file to jsx using this command npx gltfjsx Tile_Separate_2.glb --keepmeshes --keepmaterials --instanceall, I get this:

import React, { useRef, useMemo, useContext, createContext } from 'react'
import { useGLTF, Merged } from '@react-three/drei'

const context = createContext()
export function Instances({ children, ...props }) {
  const { nodes } = useGLTF('/Tile_Separate_2-transformed.glb')
  const instances = useMemo(
    () => ({
      TileFront: nodes.Tile_Front,
      TileBackFace: nodes.Tile_Back_Face,
      TileFrontFace: nodes.Tile_Front_Face,
      TileFrontRim: nodes.Tile_Front_Rim,
      TileBody: nodes.Tile_Body,
      TileBack: nodes.Tile_Back,
      TileBackRim: nodes.Tile_Back_Rim,
    }),
    [nodes]
  )
  return (
    <Merged meshes={instances} {...props}>
      {(instances) => <context.Provider value={instances} children={children} />}
    </Merged>
  )
}

export function Model(props) {
  const instances = useContext(context)
  return (
    <group {...props} dispose={null}>
      <instances.TileFront />
      <instances.TileBackFace />
      <instances.TileFrontFace />
      <instances.TileFrontRim />
      <instances.TileBody />
      <instances.TileBack />
      <instances.TileBackRim />
    </group>
  )
}

useGLTF.preload('/Tile_Separate_2-transformed.glb')

As you can see, instances is inside of Merged. But from what I’ve heard, unlike Instanced, Merged allows less control over individual instances being animated or changing color. If I wanted to use Instanced instead of Merged, how would I go about that?

Thank you.

Instances in drei are declarative THREE.InstancedMesh. The downside of that class is that it’s a blob, it can’t be nested, instances are not THREE.Object3D’s. Drei/instance fixes that. another downside of THREE.InstancedMesh is that it only takes one geometry. this is what drei/merged fixes, but it is based on drei/instance, it creates one for each geometry+material pair.

here’s a real world example: GLTFJSX Instancing - CodeSandbox

model 3 times re-used in vanilla: 80mb, 6.3 million vertices, 2349 draw calls

and 3 times with gltfjsx/merged: 2.5mb, 4.9m vertices, 12 calls

these 12 are the unique parts of the original glb. gltfjsx uses gltf-transform to find similar chunks of geometry and detects re-use, it created 12 THREE.InstancedMesh via drei/merged. now you can re-use the whole model, and it will always remain at 12 calls.

2 Likes

btw threejs has a new feature called batchedmesh, allows you have multiple geometries with one material, but you can have buffer attributes for individual colors and other properties. BatchedMesh - CodeSandbox *

it’s based on WEBGL_multi_draw, renders everything in a single draw call but single instances can also be frustum culled which is a real benefit over instancedmesh, where the whole thing gets culled. especially for tiles this would probably make a difference, you don’t want to render all tiles at all times even if it happens in one draw call, you want to render only the amount of tiles that the camera can see.

i probably explain it wrong, i will link @gkjohnson who made the threejs abstraction.

  • there is no react abstraction yet that would allow you to have declarative batched meshes with grouping and nesting. that’s a downside, but it will come.
2 Likes

This is right - with the big drawback compared to InstancedMesh is that if you have multiple of the same mesh then the data must be duplicated multiple times (on CPU and GPU) whereas it’s reused with InstancedMesh.

1 Like

So If I wanted to hover over a tile and have the color on a specific mesh from the tile change from this

to this

I could use either Merged, or BatchedMesh? Inside the Model function I could maybe pass down an onMouseOver event on a single piece of the mesh when it goes into these instances.name:

    <group {...props} dispose={null}>
      <instances.TileFront />
      <instances.TileBackFace />
      <instances.TileFrontFace />
      <instances.TileFrontRim />
      <instances.TileBody />
      <instances.TileBack />
      <instances.TileBackRim />
    </group>
const [hovered, hover] = useState(false)
return (
   <group
     {...props}
     dispose={null}
     onPointerOver={(e) => (e.stopPropagation(), hover(true))}
     onPointerOut={() => hover(false)}>
      <instances.TileFront />
      <instances.TileBackFace />
      <instances.TileFrontFace />
      <instances.TileFrontRim color={hovered ? "skyblue" : "orange" />
      <instances.TileBody />
      <instances.TileBack />
      <instances.TileBackRim />
    </group>
)

for now it’s fine, you will have 7 draw calls even with ten thousand tiles. i don’t know how many tiles you plan for but if frustum culling is ever going to become a problem then batched could be a solution.

1 Like