前面我们都是在 react 项目里用原生 three.js api 来操作 3D 场景。
其实还有一个专门的 react 渲染器叫做 react-three-fiber:

它可以用组件的方式来写 3D 场景,和 react 结合更紧密。
比如创建一个 Box 组件:

mesh、geometry、material 都是组件的形式写在 jsx 里。
我们来试一下:
npx create-vite react-three-fiber-test

创建 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

改下 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


这样,3D 场景就绘制出来了。
如果你熟悉 threejs 的原生 api,切换到 react-three-fiber 也很快就能上手了。
但我们之前写代码是有渲染循环的:

现在如果想在渲染循环里执行一些逻辑怎么办呢?
react-three-fiber 提供了一个 hook,叫 useFrame
它的作用就是每次渲染循环执行一些逻辑。
加一下试试;

用 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
跑起来之后控制台报错了:

useFrame 只能用在 Canvas 的子组件里。
我们把 mesh 封装一下:

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
看下效果:

这样就好了。
而且用了 react-three-fiber 之后处理鼠标事件就更简单了。
之前是这样写:

用 RayCaster 发出一条射线,判断和对象是否相交。
而现在只需要这样写:

function clickHandler() {
meshRef.current.material.color.set('blue');
}
onClick={clickHandler}
绑定事件写起来和 dom 是不是一模一样?
react-three-fiber 帮你封装好了,不需要自己调用 RayCaster 的 api。
试一下:

我们写 3D 场景,经常需要加载 gltf 模型,在 react-three-fiber 里也有对应的 hook:useLoader
试一下:
从 sketchfab.com 找个模型:
https://sketchfab.com/3d-models/naruto-shippuden-naruto-ea2e5dff481243b9973f2bb34a384031

下载下来放到 public 目录下:

代码里用一下:

<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 内置组件,用于异步加载子组件。
这样当渲染这个组件的时候才会去下载模型。
看一下:

这样,模型就加载进来了。
此外,还有一个 hook 也可能用到,就是 useThree
之前我们用 useFrame 的时候,第一个参数 state 就可以拿到 camera、controls 这些上下文信息:

useThree 就是拿到这个上下文:

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

或者拿到 camera 做个相机动画。
这里用到了 gsap 做缓动动画,安装下:
pnpm install --save gsap

这样,react-three-fiber 的各种 api 我们就都会用了。
案例代码上传了小册仓库
总结
这节我们学了下 react-three-fiber,它是用组件的方式来写 3D 场景。
核心 api 在 @react-three/fiber 这个包,扩展的 api 比如 OrbitControls 在 @react-three/drei 这个包。
最外层根组件是 Canvas,其余的 light、mesh 等都用组件的方式写。
我们用了 useLoader、useThree、useFrame 这几个 hook:
- useFrame:在每帧的渲染循环里执行一些逻辑,可以通过第一个参数 state 拿到上下文
- useLoader: 加载 gltf 模型,结合 react 的 Suspense 组件实现异步加载
- useThree:拿到 state 上下文,比如 size、camera 等。
而且 mesh 绑定点击事件等直接写 onClick 就行,不用自己调用 RayCaster 的 api,r3f 内部做了封装。
用组件的方式来写 3D 场景,确实更符合 react 的开发习惯,有 three.js 和 react 基础,上手还是很快的。