Problems optimizing r3f scene

I’m trying to optimize an interactive scene I’ve been working using R3F. In this scene, there’s a long list of blocks, and the user can interact with those blocks. After a few dozen blocks the performance of the interactions starts to deteriorate, so I’m trying to fix this. The scene looks like this:

My original implementation was pretty naive. I just grab a list of blocks and render them. In a very simplified version, it looks something like this. Although it has a lot more happening:

const Block = () => {
   return (
    <Box ...args>
      <MeshTransmissionMaterial ...args />
      <Edges ...args />
    </Box>
  );
}
// Canvas.tsx
// ...
return <Canvas>{blocks.map(block => <Block block={block} />)}</Canvas>

I couldn’t wrap my head around re-implementing this with Instances, so I was trying to avoid recreating geometries and materials on every render. The geometry was straightforward. For the material, I’m trying something like this:

// helper.ts
const boxGeometry =  new BoxGeometry(cubeSize, cubeSize, cubeSize);
const materialCache = new Map<string, Material | Material[]>();
// Block.tsx
const Block = ({block}) => {
  const blockRef = useRef<Mesh>(null);
  const blockMaterialId = useMemo(
    () => `block-${block.type}-${block.state}`,
    [block.type, block.state]
  );
  const blockMaterial = materialCache.get(blockMaterialId);
   return (
  // if there's a material for this block type + state, we use it and avoid creating a new instance
    <Box ...args geometry={boxGeometry} {...(blockMaterial && { material: blockMaterial })}>
       {!blockMaterial && (<MeshTransmissionMaterial ...args />)}
      <Edges ...args />
    </Box>
  );
}

This is working fine as long as the block type for a given block doesn’t change. Among the block interactions the user might trigger a change in the block. When this happens, with my optimization, I end up saving a standard material in white instead of the mesh transmission material in blue, and I’m not exactly sure why.
If someone would be able to shed some light. I’m also curious to hear how I could refactor this block component to work with instances. It seems like this would give me the best result in terms of performance, but I’m not sure how to extract the material with the right args to use with the instance component.
Also worth mentioning, I’m using Drei’s Box, Edge and MeshTransmissionMaterial for the rendering, plus react-spring. If you’re curious, this is the live version of the scene: https://stacks-nakamoto-block-simulator.vercel.app/. And the code is here: GitHub - vicnicius/stacks-nakamoto-block-simulator: A Stacks block simulator for educational purposes

My current attempt to improve the performance is here: WIP by vicnicius · Pull Request #16 · vicnicius/stacks-nakamoto-block-simulator · GitHub

Any help or feedback is appreciated.

Thanks in advance!

If you mean literally a few dozen blocks - then there’s likely something essentially wrong with the rendering, maybe you’re using too high of a resolution, or too hardcore postprocessing. You should be easily able to render 1000 cubes, without instancing, at 60fps.

  1. Iirc, by it’s nature <Edges> helper from drei was a bit questionable when it comes to recreating the edge geometries on each state update (ie. it duplicates entire geometry of the original object to create the outline, doubling the amount of workload for the renderer.) I’d first check if removing the edges wouldn’t help.

  2. I’d just skip MeshTransmissionMaterial all together. It looks pretty but it’s slow, and you don’t have anything in the background to show the transmission effect anyways. For what you have in the scene - you should easily be able to just go for MeshLambertMaterial (best), or MeshStandardMaterial (slower, but still faster than transmission material.)

  3. At any given point, only 8-10 cubes are visible. If you’re aiming for 1000s or 100000s of cubes, consider virtualising the scrollable list of cubes.

MTM, in order to have refraction, must create a render pass, which means it renders the full scene (without the mesh that uses it). this then applies to each and every block. MTM is OK for something front and center, a single thing, or two, but if you have multiple objects, there’s no way it could scale.

you might be able to get away with instanced meshes, and MTM, but keep in mind that you wouldn’t see other blocks through any block.

ps i would suggest you read The Big List of three.js Tips and Tricks! | Discover three.js you’ll get an understanding that threejs, as fast as it is, isn’t limitless, and that limits are actually quite tight. you only have a few draw calls until performance goes down, a few materials with varying cost, a few lights. everything on top of that requires optimisations: instancing, sharing materials, sharing geometries, …

2 Likes

Hi @mjurczyk, thanks for the feedback!

I did test the impact of the Edges and it doesn’t seem to be affecting performance that much, but I’ll do a new test with a proper measure to check it again. Moving away from the MeshTrasmissionMaterial is a very good idea. I had it because my first idea was to have a solid block inside a transparent one to create a nice affect, but although it looked very cool I started struggling with performance with (literally) a few dozen blocks. Removing the inside block allowed the scene to get to the 100 mark.