Skip to content

139. react-three-fiber:组件化开发 3D 场景

Published:

前面我们都是在 react 项目里用原生 three.js api 来操作 3D 场景。

其实还有一个专门的 react 渲染器叫做 react-three-fiber:

image.png

它可以用组件的方式来写 3D 场景,和 react 结合更紧密。

比如创建一个 Box 组件:

image.png

mesh、geometry、material 都是组件的形式写在 jsx 里。

我们来试一下:

npx create-vite react-three-fiber-test

image.png

创建 vite + react 的 js 项目。

进入项目,安装依赖:

pnpm install
pnpm install --save three
pnpm install --save-dev @types/three

然后安装 react-three-fiber

pnpm install --save @react-three/fiber

去掉 index.js 和 StrictMode

image.png

改下 App.jsx

import { OrbitControls } from '@react-three/drei'
import { Canvas } from '@react-three/fiber'

function App() {
  return <Canvas camera={{
    position: [0, 500, 500]
  }} style={{
      width: window.innerWidth,
      height: window.innerHeight
  }}>
    <ambientLight/>
    <axesHelper args={[1000]}/>
    <directionalLight position={[500, 400, 300]}/>
    <OrbitControls/>
    <mesh>
      <dodecahedronGeometry args={[100]}/>
      <meshPhongMaterial color={'orange'}/>
    </mesh>
  </Canvas>
}

export default App

threejs 场景会渲染到 canvas 上,所以我们用 Canvas 作为根组件。

canvas 指定宽高为窗口宽高。

指定 camera 的位置。

创建一个环境光,一个平行光。

加一个 axesHelper,参数在 args 里通过数组传入。

然后加一个 OrbitControls。

场景里加一个 mesh,mesh 的子组件指定 geometry 和 material。

这里我们创建了一个十二面体。

注意,这里 OrbitControls 是从 @react-three/drei 这个包导入的,扩展的一些 api 都放在这个包里。

pnpm install --save @react-three/drei

跑下试试:

npm run dev

image.png

2025-07-10 12.09.28.gif

这样,3D 场景就绘制出来了。

如果你熟悉 threejs 的原生 api,切换到 react-three-fiber 也很快就能上手了。

但我们之前写代码是有渲染循环的:

image.png

现在如果想在渲染循环里执行一些逻辑怎么办呢?

react-three-fiber 提供了一个 hook,叫 useFrame

它的作用就是每次渲染循环执行一些逻辑。

加一下试试;

image.png

用 useRef 拿到 mesh 的 ref,每一帧渲染改变一下 rotation.y

import { OrbitControls } from '@react-three/drei'
import { Canvas, useFrame } from '@react-three/fiber'
import { useRef } from 'react';

function App() {

  const meshRef =  useRef();

  useFrame((state, delta) => {
    meshRef.current.rotation.y += 0.1;
  });

  return <Canvas camera={{
    position: [0, 500, 500]
  }} style={{
      width: window.innerWidth,
      height: window.innerHeight
  }}>
    <ambientLight/>
    <axesHelper args={[1000]}/>
    <directionalLight position={[500, 400, 300]}/>
    <OrbitControls/>
    <mesh ref={meshRef}>
      <dodecahedronGeometry args={[100]}/>
      <meshPhongMaterial color={'orange'}/>
    </mesh>
  </Canvas>
}

export default App

跑起来之后控制台报错了:

image.png

useFrame 只能用在 Canvas 的子组件里。

我们把 mesh 封装一下:

image.png

import { OrbitControls } from '@react-three/drei'
import { Canvas, useFrame } from '@react-three/fiber'
import { useRef } from 'react';

function Mesh() {
  const meshRef =  useRef();

  useFrame((state, delta) => {
    meshRef.current.rotation.y += 0.1;
  });

  return <mesh ref={meshRef}>
    <dodecahedronGeometry args={[100]}/>
    <meshPhongMaterial color={'orange'}/>
  </mesh>
}

function App() {
  return <Canvas camera={{
    position: [0, 500, 500]
  }} style={{
      width: window.innerWidth,
      height: window.innerHeight
  }}>
    <ambientLight/>
    <axesHelper args={[1000]}/>
    <directionalLight position={[500, 400, 300]}/>
    <OrbitControls/>
    <Mesh/>
  </Canvas>
}

export default App

封装 Mesh 组件,在里面调用 useFrame

看下效果:

2025-07-10 16.36.43.gif

这样就好了。

而且用了 react-three-fiber 之后处理鼠标事件就更简单了。

之前是这样写:

image.png

用 RayCaster 发出一条射线,判断和对象是否相交。

而现在只需要这样写:

image.png

function clickHandler() {
    meshRef.current.material.color.set('blue');
}
onClick={clickHandler}

绑定事件写起来和 dom 是不是一模一样?

react-three-fiber 帮你封装好了,不需要自己调用 RayCaster 的 api。

试一下:

2025-07-10 16.46.45.gif

我们写 3D 场景,经常需要加载 gltf 模型,在 react-three-fiber 里也有对应的 hook:useLoader

试一下:

从 sketchfab.com 找个模型:

https://sketchfab.com/3d-models/naruto-shippuden-naruto-ea2e5dff481243b9973f2bb34a384031

image.png

下载下来放到 public 目录下:

image.png

代码里用一下:

image.png

<Suspense fallback={null}>
  <Naruto/>
</Suspense>
function Naruto() {
  const gltf = useLoader(GLTFLoader, 'naruto.glb')
  console.log(gltf);

  gltf.scene.scale.setScalar(200);
  return <primitive object={gltf.scene}/>
}

Suspense 是 React 内置组件,用于异步加载子组件。

这样当渲染这个组件的时候才会去下载模型。

看一下:

2025-07-10 17.05.20.gif

这样,模型就加载进来了。

此外,还有一个 hook 也可能用到,就是 useThree

之前我们用 useFrame 的时候,第一个参数 state 就可以拿到 camera、controls 这些上下文信息:

image.png

useThree 就是拿到这个上下文:

image.png

const size = useThree(state => state.size);
console.log(size);

const camera = useThree(state => state.camera);
gsap.to(camera.position, {
    x: 0,
    y: 500,
    z: 200,
    duration: 1
});

比如用 useThree 拿到 canvas 的 size

image.png

或者拿到 camera 做个相机动画。

这里用到了 gsap 做缓动动画,安装下:

pnpm install --save gsap

2025-07-10 17.17.24.gif

这样,react-three-fiber 的各种 api 我们就都会用了。

案例代码上传了小册仓库

总结

这节我们学了下 react-three-fiber,它是用组件的方式来写 3D 场景。

核心 api 在 @react-three/fiber 这个包,扩展的 api 比如 OrbitControls 在 @react-three/drei 这个包。

最外层根组件是 Canvas,其余的 light、mesh 等都用组件的方式写。

我们用了 useLoader、useThree、useFrame 这几个 hook:

而且 mesh 绑定点击事件等直接写 onClick 就行,不用自己调用 RayCaster 的 api,r3f 内部做了封装。

用组件的方式来写 3D 场景,确实更符合 react 的开发习惯,有 three.js 和 react 基础,上手还是很快的。

评论