Combining Vanilla threejs and R3F

Hi,

i’ve got the following code to display a custom geometry in a mesh from R3F:

import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";


var geom = new THREE.BufferGeometry()

export const ConeExtrude = () => {

  const dracoLoader = new DRACOLoader();
  dracoLoader.setDecoderPath("./draco/");
  const loader = new GLTFLoader();
  loader.setDRACOLoader(dracoLoader);

  const v = new THREE.Vector3(1, 0, 0);
  const w = new THREE.Vector3();

  loader.load("./models/Fräsung.glb", (glb) => {
    const meshPos = glb.scene.children[0].geometry.getAttribute("position");

    for (let i = 0; i < meshPos.count; i++) {
      w.set(meshPos.getX(i), meshPos.getY(i), meshPos.getZ(i));

      w.x += Math.sign(Math.round(100 * w.x)) * v.x * 4;
      w.y += Math.sign(Math.round(100 * w.y)) * v.y * 4;
      w.z += Math.sign(Math.round(100 * w.z)) * v.z * 4;

      meshPos.setXYZ(i, w.x, w.y, w.z);
    }
    meshPos.needsUpdate = true;

    geom = glb.scene.children[0].geometry;
  });


  return (
    <>
      <mesh geometry={geom} position-x={0}>
        <meshStandardMaterial color={"#23a44e"} />
      </mesh>
    </>
  );
};

It works if i refresh the page as expected, the mesh gets displayed. If i just save the file nothing is showing anymore (I am using vite, normally svaing files refreshes the dev server). Is the way i use the geometry wrong? Is there a better way to get the geometry from vanilla threejs?

I don’t use R3F so I’m unaware of specifics, but here are a few things:

  • R3F can handle loading models, don’t use your own code to do that - the whole point of using this kind of library is to make your life easier by not having to write that sort of thing

-R3F is React, so you need to make sure you understand how React works.

  loader.load("./models/Fräsung.glb", (glb) => {
    ...
    geom = glb.scene.children[0].geometry;
  });
...
      <mesh geometry={geom} position-x={0}>

is something that wouldn’t work in normal React so it won’t work with R3F. The geom variable would need to be assigned using setState, and the load function should be called in useEffect.

you are re-loading the model every time the component renders, this is called a side effect, you use useEffect, useLayoutEffect and useMemo. the only thing that should be inside your render function is hooks, and the return, nothing else.

export function ConeExtrude(props) {
  const { scene } = useGLTF("./models/Fräsung.glb")
  const geo = useMemo(() => {
    const meshPos = scene.children[0].geometry.getAttribute("position")
    for (let i = 0; i < meshPos.count; i++) {
      w.set(meshPos.getX(i), meshPos.getY(i), meshPos.getZ(i))
      w.x += Math.sign(Math.round(100 * w.x)) * v.x * 4
      w.y += Math.sign(Math.round(100 * w.y)) * v.y * 4
      w.z += Math.sign(Math.round(100 * w.z)) * v.z * 4
      meshPos.setXYZ(i, w.x, w.y, w.z)
    }
    meshPos.needsUpdate = true
    return scene.children[0].geometry
  }, [model])
  return (
    <mesh geometry={geo} {...props}>
      <meshStandardMaterial color="#23a44e" />
    </mesh>
  )
}

you can of course run all this vanilla code, though it would be a waste, but run it inside a useEffect, so that it executes only once. but do use drei/useGLTF, it takes care of draco automatically and it would also give you “nodes”, so you would not even have to traverse or grab into children like that. it is dirty and imo also error prone since the order of children can change since gltfloader is async.

const { nodes } = useGLTF("./models/Fräsung.glb")
useLayoutEffect(() => {
  // Mutate the geometry safely after the React component has rendered, but before Threejs
  const meshPos = nodes.WhateverTheNameIs.geometry.getAttribute("position")
  ...
}, [nodes])
return (
  <mesh geometry={nodes.WhateverTheNameIs.geometry} {...props}>

also check out GitHub - pmndrs/gltfjsx: 🎮 Turns GLTFs into JSX components

ps, why useLoader/useGLTF instead of just GLTFLoader directly? because you cannot orchestrate GLTFLoader, nothing from outside can operate on the result. in react loading is a first class primitive, called suspense.

this is what everything in the threejs eco system in react is based upon, see https://twitter.com/0xca0a/status/1653168029755219970

That works perfectly thanks!! I am working a bit now with r3f but such things are still not always clear unfortunatly…:slight_smile: One question, in your useLayoutEffect you have “model” as a dependency what should i put there? If i put nodes.Cube as a dependency and save, the code in the uLE runs again and again. In my understanding if i want the uLE to run only once i provide an empty dependency array right?

import { useGLTF } from "@react-three/drei"
import { useLayoutEffect } from "react";
import * as THREE from "three"

const v = new THREE.Vector3(1, 0, 0);
const w = new THREE.Vector3();

export function ConeExtrude2() {
  const { nodes } = useGLTF("./models/Fräsung.glb")
useLayoutEffect(() => {
  // Mutate the geometry safely after the React component has rendered, but before Threejs
  const meshPos = nodes.Cube.geometry.getAttribute("position")

  for (let i = 0; i < meshPos.count; i++) {
    w.set(meshPos.getX(i), meshPos.getY(i), meshPos.getZ(i));

    w.x += Math.sign(Math.round(100 * w.x)) * v.x * 4;
    w.y += Math.sign(Math.round(100 * w.y)) * v.y * 4;
    w.z += Math.sign(Math.round(100 * w.z)) * v.z * 4;

    meshPos.setXYZ(i, w.x, w.y, w.z);
  }
  meshPos.needsUpdate = true;

}, [])
  return (
    <mesh geometry={nodes.Cube.geometry}>
      <meshStandardMaterial color="#23a44e" />
    </mesh>
  )
}

it meant to say [nodes]. it’s the dependencies array, if any one of the dependencies has changed the effect will trigger.

  • useMemo runs immediately, executes if dependencies change, returns a result - it is used for memoization
  • useEffect executes if dependencies change, runs after react renders the view (turns the jsx into threejs nodes), and after the view has been painted (after three renders to the screen), this is a classical side effect
  • useLayoutEffect executes if dependencies change, runs after react renders, but before three has painted, this is a side effect also, but you can use it to change/measure/… things before it ends up on the screen

If i put nodes.Cube as a dependency and save, the code in the uLE runs again and again

there might be a bigger problem in your app. check if the component itself re-mounts every render.

useEffect(() => {
  console.log("mount")
  return () => console.log("unmount")
}, [])

if that’s the case you need to find the cause for that. a component unmounts when the parent doesn’t render it, through suspense, or mistakes, like defining a component function inside another function.

if it’s fine then check if you change nodes.Cube because that reference is guaranteed to be stable, it should absolutely be permissable to use it as a dependency in the array, though , [nodes]) should be good enough.

1 Like

Ahh okay thought so, so i used nodes.Cube! Yeah you are right there is something weird in my code, if i change something in the Experience (my scene) the ConeExtrude logs “unmount” and “mount”. It should just mount once right? Maybe i setup a sandbox to debug

components should only mount once. if you edit stuff in your editor then hot reload can of course cause a remount, but if nothing from outside changes the source and the components re-render for no good reason you have a bug.

1 Like