关于 threejs-react-three-fiber
面向使用 React Three Fiber 和 drei 构建 3D 场景的 Three.js 技能。内容涵盖声明式场景组合、Hooks、资源加载、交互处理及 React 3D 项目的性能技巧。
主要功能
- 01用 JSX 组合 Three.js 场景
- 02通过 Hooks 控制渲染循环
- 03加载 GLTF 模型与纹理
- 04处理基于射线投射的指针事件
- 05使用 drei 控制器与场景辅助工具
- 06应用按需渲染和实例化
适用场景
- 01在 React 中构建交互式 3D 视图
- 02加载并展示 GLTF 资源
- 03添加相机控制与场景标签
- 04减少静态场景的渲染工作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cloudai-x/ threejs-skills --skill threejs-react-three-fiber安装后在 Claude Code 中输入/threejs-react-three-fiber确认已加载。
详细介绍
Three.js React Three Fiber
面向 three.js r186。示例使用 WebGLRenderer;WebGPU 和 TSL 部分使用 WebGPURenderer。
快速开始
面向 @react-three/fiber 9.8、React 19 和 @react-three/drei 10.7。Canvas 会创建并管理一个 WebGLRenderer、一个场景和一个摄像机;three.js 对象会变成 JSX 元素。Canvas 会填满其父元素,因此请为父元素设置高度。下面的组件片段会像这样在 Canvas 内渲染;受光照影响的材质需要 Canvas 中有灯光。
import { useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
function Box() {
const ref = useRef();
useFrame((_, delta) => {
ref.current.rotation.y += delta;
});
return (
<mesh ref={ref} position={[0, 0.5, 0]} castShadow>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="#d9915b" />
</mesh>
);
}
export default function App() {
return (
<Canvas shadows="percentage" camera={{ position: [3, 2, 4], fov: 50 }}>
<color attach="background" args={["#111827"]} />
<ambientLight intensity={0.4} />
<directionalLight position={[3, 4, 2]} intensity={2} castShadow />
<Box />
<mesh rotation-x={-Math.PI / 2} receiveShadow>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial color="#20293a" />
</mesh>
<OrbitControls makeDefault />
</Canvas>
);
}
Canvas 属性
export function App() {
return (
<Canvas
shadows="variance"
dpr={[1, 2]}
frameloop="demand"
flat
camera={{ position: [0, 2, 6], fov: 45 }}
gl={{ powerPreference: "high-performance" }}
onCreated={(state) => state.gl.setClearColor("#111827")}
>
{/* scene contents */}
</Canvas>
);
}
camera:默认摄像机的属性,或一个摄像机实例。未设置时,会使用 fov 为 75、near 为 0.1、far 为 1000、位于 z 5 且朝向原点的PerspectiveCamera;fiber 会在每次调整大小时更新 aspect(或在设置orthographic时更新 left/right/top/bottom)和投影矩阵。camera={{ manual: true }}会让投影完全由你控制。dpr:一个数字,或一个根据window.devicePixelRatio限定范围的[min, max];该范围会随显示设备变化而更新。shadows:true会启用阴影贴图,字符串('basic'、'percentage'、'variance')用于选择滤镜类型,对象则会合并到渲染器的shadowMap属性中。true、'soft'和默认值false都会设置 PCFSoftShadowMap,three.js 会将其替换为 PCFShadowMap 并在控制台发出警告(WebGLRenderer 在渲染阴影时警告,WebGPURenderer 在每个 Canvas 中警告);'percentage'使用相同的柔和滤镜,但不会发出警告。flat会设置 NoToneMapping;未设置时,fiber 会应用 ACESFilmicToneMapping——这与纯 three.js 不同,色调映射默认开启。linear会将输出切换为 LinearSRGBColorSpace;默认值为 sRGB。legacy会禁用 three.js 的 ColorManagement。gl:默认new WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance" })的属性、一个渲染器实例,或一个工厂函数(见下文的 WebGPU 路径)。frameloop:'always'会渲染每一帧,'demand'仅在 React 发生变化或调用invalidate()后渲染,'never'会将循环交由你控制(调用模块级advance(timestamp)导出函数并传入以秒为单位的时间;useFrame的 delta 是连续时间戳之差,因此若使用毫秒,数值会大 1000 倍)。events会替换事件管理器(默认管理器会将 DOM 指针事件映射为射线投射);eventSource(附加指针事件的元素,默认为 Canvas 包装元素)和eventPrefix(默认值为'offset';页面滚动或画布变换时可使用'page'、'client'、'layer'、'screen')用于调整指针映射。渲染器创建后会运行onCreated;点击未命中任何对象时会触发onPointerMissed。
以 JSX 表示场景图
three 命名空间中的每个导出都可作为元素使用:小写标签对应首字母大写的类(<mesh> → THREE.Mesh,<meshStandardMaterial> → 对应材质)。属性会应用于实例;凡是 three.js 中有 .set() 的属性都支持简写值:position={[1, 2, 3]}、position={new THREE.Vector3(1, 2, 3)}、position={5}(调用 setScalar)、color="#d9915b"。带连字符的属性会向下传递(rotation-x、material-color、geometry-attributes-position);attach="material-0" 会附加到数组索引。
args 是构造函数参数列表;更改它会从头重建对象。几何体和材质会自动附加到父级网格上——无需设置 attach。
function Torus() {
return (
<mesh position={[0, 1, 0]}>
<torusKnotGeometry args={[0.7, 0.25, 128, 24]} />
<meshStandardMaterial
color="#42699c"
roughness={0.2}
metalness={0.8}
onUpdate={(self) => console.log(self.type)}
/>
</mesh>
);
}
使用 <primitive object={obj} /> 挂载已有对象;每个 primitive 只能有一个对象(fiber 会将每个对象的状态保存在对象本身),复制时请克隆。fiber 不会释放 primitive——其生命周期由你负责。对任意元素设置 dispose={null} 可使其不在卸载时释放。使用 extend 在 three 命名空间之外注册类:
import { extend } from "@react-three/fiber";
import { Water } from "three/addons/objects/Water.js";
extend({ Water });
// then render <water args={[geometry]} /> anywhere in the Canvas
在 TypeScript 中,标签 line、audio、source 和 path 会与 DOM 类型冲突;请写作 threeLine、threeAudio、threeSource、threePath(fiber 会移除前缀并解析对应的 three 类)。
Hooks
useFrame(callback, renderPriority?) 订阅渲染循环;它会在场景渲染前运行:
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
function Spinner({ radius = 2 }) {
const ref = useRef();
useFrame((state, delta) => {
ref.current.rotation.y += delta;
ref.current.position.x = Math.sin(state.clock.elapsedTime) * radius;
});
return (
<mesh ref={ref}>
<sphereGeometry args={[0.4, 32, 16]} />
<meshBasicMaterial color="#e8b4a0" />
</mesh>
);
}
具有正 renderPriority 的订阅者会接管渲染工作——Canvas 会停止渲染,你的回调必须自行调用 state.gl.render(state.scene, state.camera)。使用默认优先级时,自己调用 gl.render 会导致重复渲染。
useThree(selector?) 会以响应式方式读取 Canvas 状态;不提供选择器时,它会返回整个状态(gl、camera、scene、raycaster、pointer、size、viewport、invalidate、setDpr、setFrameloop、controls、performance):
import { useThree } from "@react-three/fiber";
const camera = useThree((state) => state.camera);
const invalidate = useThree((state) => state.invalidate);
在 useFrame 中改用 state 参数——它是相同的状态,但不会触发重新渲染。state.clock 是 fiber 9.8 创建的 THREE.Clock,因此每个 Canvas 都会输出 three 的 Clock 弃用警告;这不是由你的代码导致的。
useLoader(ClassOrInstance, url, extensions?, onProgress?) 会暂停,直到任意 three 加载器完成解析,并按加载器和 URL 缓存;调用它的组件必须位于 Suspense 边界下。extensions 回调会在 fiber 为每个类保留的那个加载器实例上运行,因此它设置的内容(路径、解码器)会应用于之后所有使用该类的 useLoader 调用。useGraph(object) 会根据对象树构建具名查找映射({ nodes, materials, meshes }):
import { useGraph, useLoader } from "@react-three/fiber";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
function Model({ url }) {
const gltf = useLoader(GLTFLoader, url, (loader) => loader.setPath("assets/"));
const { nodes } = useGraph(gltf.scene);
return <primitive object={gltf.scene} />;
}
useLoader.preload(Class, url) 会在 React 外预热缓存;useLoader.clear(Class, url) 会移除缓存条目。
Pointer Events
每次指针事件发生时,都会对带有事件处理器的对象执行射线投射(指针移动只会检测带有 move、over、out、enter 或 leave 处理器的对象);事件处理器会从最近的对象开始触发,并从命中的对象向上冒泡到其父对象。事件包含交点以及辅助信息:object、eventObject(注册了处理器的对象)、point、distance、face、instanceId、intersections、ray、camera、unprojectedPoint、delta(自 pointerdown 起移动的像素数,用于类似点击的事件)、nativeEvent。事件处理器:onClick、onDoubleClick、onContextMenu、onWheel、onPointerUp、onPointerDown、onPointerMove、onPointerOver、onPointerOut、onPointerEnter、onPointerLeave、onPointerMissed(onPointerCancel 和 onLostPointerCapture 虽有类型定义,但 fiber 从不在对象上调用它们)。
import { useState } from "react";
function Pickable() {
const [hovered, setHovered] = useState(false);
return (
<mesh
onClick={(event) => {
event.stopPropagation(); // underlying objects along the ray no longer receive it
console.log(event.object.name, event.distance, event.point, event.instanceId);
}}
onPointerOver={(event) => {
event.stopPropagation();
setHovered(true);
}}
onPointerOut={() => setHovered(false)}
onPointerDown={(event) => {
event.target.setPointerCapture(event.pointerId); // this pointer's events reach the mesh off its surface until release
}}
onPointerUp={(event) => {
event.target.releasePointerCapture(event.pointerId);
}}
onPointerMissed={() => console.log("clicked elsewhere")}
>
<sphereGeometry args={[0.75, 32, 16]} />
<meshStandardMaterial color={hovered ? "#e8b4a0" : "#42699c"} />
</mesh>
);
}
Pointer Events 在任何渲染器下都可使用:默认射线投射器会通过 intersectObject(object, true) 检测 CPU 几何体。传入 raycaster={{ params: { Line: { threshold: 0.1 } } }} 可调整检测阈值。
Loading Assets
加载钩子会暂停;请将边界放在它们上方,加载期间会显示备用内容。
import { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
import { useGLTF, useProgress, Environment } from "@react-three/drei";
function Model({ url }) {
const { scene, animations, nodes, materials } = useGLTF(url);
return <primitive object={scene} />;
}
function Hud() {
const { active, progress, errors } = useProgress(); // tracks three's DefaultLoadingManager
return <div>{active ? `${progress.toFixed(0)}%` : errors.length > 0 ? "failed" : "done"}</div>;
}
export function App() {
return (
<>
<Hud />
<Canvas camera={{ position: [3, 2, 4], fov: 50 }}>
<Suspense fallback={null}>
<Environment preset="city" />
<Model url="model.glb" />
</Suspense>
</Canvas>
</>
);
}
useGLTF(url)使用 drei 随附的 GLTFLoader(three-stdlib)加载资源,默认启用 Draco 和 meshopt;Draco 解码器来自固定版本的 gstatic URL(useGLTF.setDecoderPath可更改其路径,useGLTF(url, false)可禁用它)。useGLTF.preload(url)会预热缓存。返回结果包含scene、animations、cameras以及nodes/materials/meshes查找映射。对于同一蒙皮模型的多个副本,可在每个组件中使用useMemo(() => SkeletonUtils.clone(scene), [scene])克隆一次(import * as SkeletonUtils from "three/addons/utils/SkeletonUtils.js"),并将每个克隆挂载在各自的 primitive 中。useTexture(input)可加载单个 URL、数组(const [a, b] = useTexture([x, y]))或键值对象;fiber 会为通过 props 赋值的 LDR(RGBA8)颜色贴图(包括map、emissiveMap、envMap)设置 sRGB 色彩空间,因此无需手动设置colorSpace,颜色贴图即可正常工作;标量贴图则保持 NoColorSpace。<Preload all />会在挂载时编译每种材质一次(使用all时也包括隐藏对象),并将场景渲染到立方体目标以便上传纹理;这会带来一次性卡顿,但可避免首次查看时的卡顿。请将它放在 Suspense 边界内、资源之后,否则它会在资源加载前运行。useProgress是一个普通 store,因此上方的 Hud 会在 Canvas 外部的 DOM 中读取它。
drei 辅助工具
drei 还提供了许多其他辅助工具;这里仅验证了以下子集,且每个辅助工具都从 @react-three/drei 导入。
控制器:OrbitControls 会构造 drei 打包的控制器类,将其连接到事件源(默认是 Canvas 包装器),在每帧用户回调之前更新它,在发生变化时调用 invalidate()(因此可与 frameloop="demand" 配合使用),并默认启用阻尼。makeDefault 会将它存储在 state.controls 中(Bounds 和其他辅助工具会获取它);拖动时,regress 会降低自适应性能值。属性会直接传递给控制器实例:
import { OrbitControls } from "@react-three/drei";
<OrbitControls
makeDefault
enableDamping
dampingFactor={0.1}
target={[0, 1, 0]}
minDistance={2}
maxDistance={12}
maxPolarAngle={Math.PI / 2}
autoRotate
autoRotateSpeed={0.5}
/>
CameraControls 以相同方式封装 camera-controls 库(如 fitToBox 这样的 ref 方法来自该库,并且会随帧间隔更新)。
场景装饰:
import {
ContactShadows, Environment, Lightformer, Text, Html, Center, Bounds,
} from "@react-three/drei";
export function Dressing({ overlay }) { // overlay: a ref to a DOM element laid over the Canvas
return (
<group>
<Environment resolution={256}>
<Lightformer form="rect" intensity={2} position={[0, 4, 3]} scale={[8, 2, 1]} />
</Environment>
<ContactShadows position={[0, -1, 0]} opacity={0.6} scale={12} blur={2.4} far={4} />
<Bounds fit clip observe margin={1.5}>
<mesh>
<sphereGeometry args={[1, 32, 16]} />
<meshStandardMaterial metalness={0.9} roughness={0.2} />
</mesh>
<Center top position={[2.5, -1, 0]}>
<mesh>
<boxGeometry args={[1, 0.6, 1]} />
<meshStandardMaterial />
</mesh>
</Center>
</Bounds>
<Text position={[0.75, 1.1, 0]} fontSize={0.4} anchorX="center" anchorY="bottom" color="#e8b4a0" outlineWidth={0.02}>
three.js
</Text>
<Html portal={overlay} position={[2.5, 0.2, 0]} center distanceFactor={8} occlude="raycast">
<div>dom overlay anchored to the scene</div>
</Html>
</group>
);
}
ContactShadows会从下方渲染场景到深度目标,并对其模糊处理——在靠近地面的物体下方生成柔和的接触阴影平面,无需设置阴影贴图;对于静态场景,可设置frames={1},仅烘焙一次。Lightformer是一块灯光卡片——一个使用基础材质的网格,form可设为circle、ring、rect或box——设计用于放置在<Environment>的子场景中,以烘焙到反射效果里。Text使用 SDF 文本(troika)进行渲染,支持布局属性maxWidth、textAlign、lineHeight、letterSpacing、characters;字体加载期间会暂停渲染。Html会将 DOM 节点投影到场景中;transform会将其渲染为经过 3D 旋转的叠加层,而不是面向屏幕的叠加层。occlude(true或"raycast")会在几何体阻挡到其锚点的射线时隐藏该元素,因此切勿将其锚定在网格内部;occlude="blending"则通过 ShaderMaterial 根据深度隐藏它(仅限 WebGL)。使用 fiber 9.8、drei 10.7 和 React 19.2 时,与默认(WebGLRenderer)Canvas 在同一次提交中挂载的Html会显示为空;随 Canvas 一起重新挂载的Html也会如此。请将覆盖 Canvas 的 DOM 元素 ref 传给portal,以固定其容器,Dressing 也采用了这种做法:在 Canvas 后、position: relative包装器内添加<div ref={overlay} style={{ position: "absolute", inset: 0, pointerEvents: "none" }} />。Center会根据子元素的包围盒重新居中(top/bottom/left/right/front/back可对齐到边缘,precise={false}会测量每个对象的边界而非每个顶点,onCentered会报告包围盒)。它会测量顶点位置,因此会错误放置 dreiText,因为其几何体是单位四边形;请改用anchorX、anchorY和position放置 Text。Bounds会让相机适配其子元素并围绕它们取景(fit、clip可调整 near/far,observe会在窗口大小变化时重新适配);useBounds()会返回其命令式 API(refresh、reset、moveTo、lookAt、fit、clip)。
性能
按需渲染:没有变化,就不会渲染。
import { useEffect, useRef } from "react";
import { useThree } from "@react-three/fiber";
function OnDemand() {
const ref = useRef();
const invalidate = useThree((state) => state.invalidate);
// under <Canvas frameloop="demand"> imperative changes are invisible to React — request a frame for them
useEffect(() => {
const id = setInterval(() => {
ref.current.rotation.y += 0.5;
invalidate();
}, 100);
return () => clearInterval(id);
}, [invalidate]);
return (
<mesh ref={ref}>
<boxGeometry args={[1, 1, 1]} />
<meshNormalMaterial />
</mesh>
);
}
React 变化(props、state)会自动触发失效。切勿在 useFrame 中调用 setState——像快速入门中那样,通过 refs 修改对象;少数需要 DOM 跟随每帧数值变化的情况,可以使用 store 或 flushSync。
多个相同形状的副本可通过 Instances 共用一次绘制调用;每个 Instance 都是一个声明式变换(以及可选的 color),每帧都会合成到实例化矩阵中:
import { Instance, Instances } from "@react-three/drei";
function Crowd({ count = 200 }) {
return (
<Instances limit={count} castShadow>
<capsuleGeometry args={[0.25, 0.8, 4, 12]} />
<meshStandardMaterial />
{Array.from({ length: count }, (_, i) => (
<Instance
key={i}
position={[(i % 15) - 7, 0.65, Math.floor(i / 15) - 7]}
color={`hsl(${(i * 37) % 360}, 60%, 55%)`}
/>
))}
</Instances>
);
}
Detailed 根据相机距离选择一个子项(三.js 的 LOD),以低成本渲染远处的几何体:子项 i 从 distances[i] 对应的距离开始显示(第一项为 0),hysteresis 是该距离的一个比例,用于延迟切回更精细的细节:
import { Detailed } from "@react-three/drei";
<Detailed distances={[0, 15, 40]} hysteresis={0.1}>
<mesh><sphereGeometry args={[1, 64, 32]} /><meshStandardMaterial /></mesh>
<mesh><sphereGeometry args={[1, 16, 8]} /><meshStandardMaterial /></mesh>
<mesh><sphereGeometry args={[1, 8, 4]} /><meshStandardMaterial /></mesh>
</Detailed>
自适应分辨率:启用 regress 的控件会在拖动时将 performance.current 降至 min(默认值为 0.5),并在 200 ms 后恢复;AdaptiveDpr 会将像素比设为该值乘以 Canvas 首次解析出的像素比,因此每次拖动结束时都会恢复到该初始像素比。PerformanceMonitor 会采样实际帧率并通过回调报告——它本身不会调整像素比。通过 React state 在回调中驱动 Canvas 的 dpr prop,因为 Canvas 每次重新渲染时都会重新应用该 prop,从而撤销在其内部调用的 setDpr;请勿在该 Canvas 中使用 AdaptiveDpr,否则下一次拖动会将像素比恢复到初始值,覆盖已降低的 dpr:
import { useState } from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls, AdaptiveDpr, PerformanceMonitor } from "@react-three/drei";
// lower the resolution while the controls move
export function DragTunedApp() {
return (
<Canvas dpr={[1, 2]}>
<OrbitControls regress />
<AdaptiveDpr pixelated />
{/* scene contents */}
</Canvas>
);
}
// let the measured frame rate pick the resolution through the dpr prop
export function MonitoredApp() {
const [dpr, setDpr] = useState([1, 2]);
return (
<Canvas dpr={dpr}>
<OrbitControls />
<PerformanceMonitor
onDecline={() => setDpr(1)}
onIncline={() => setDpr([1, 2])}
flipflops={3}
onFallback={() => setDpr(1)}
/>
{/* scene contents */}
</Canvas>
);
}
除 fiber 专用工具外:限制 dpr,优先使用 KTX2 纹理,合并几何体或使用实例化,并释放已卸载的资源——参见 threejs-performance。
资源释放
Fiber 会在卸载时(安排在空闲时间)释放它创建的内容:每个 JSX 元素背后的对象,包括作为 JSX 子项声明的几何体、材质和纹理,以及它通过 gl props 或工厂创建的渲染器。通过 props 传入的对象(geometry={...}、map={...})和 primitives 永远不会被释放;dispose={null} 则可让某个元素及其子项免于释放。你传入的渲染器实例不会被释放,但 fiber 会在卸载时强制丢失其上下文,因此无法再次使用。通过 useLoader、useGLTF 和 useTexture 加载的资源会为重新挂载而保留在缓存中;hook 的 clear 只会移除缓存条目,因此不再需要的几何体、材质和纹理需由你自行释放。模块级的 dispose(object) 导出会对该对象及其拥有 dispose() 方法的每个属性调用 dispose()(例如网格的几何体和材质);它不会遍历子项,因此需要自行遍历整个树。
WebGPU 和 TSL
gl 属性接受一个异步工厂函数:使用 fiber 传入的属性构造 WebGPURenderer,等待其初始化完成,然后返回它——fiber 会在 Canvas 内等待该 Promise,并在其解析后渲染子元素(此时 Canvas 外的 Suspense fallback 不会显示)。由于 Canvas 使用普通的 three 命名空间初始化元素目录(其中没有节点材质),因此在使用节点材质元素前,先通过 extend 合并 three/webgpu 命名空间。three/tsl 中的 TSL 节点也以同样的方式通过属性传入,但材质只会编译一次其节点:在模块作用域或使用 useMemo 构建节点,并通过 uniform() 值进行动画处理,因为后续渲染中传入的新节点会被赋值,但不会触发重新编译,除非设置 material.needsUpdate = true:
import * as THREE from "three/webgpu";
import { color } from "three/tsl";
import { Canvas, extend, useFrame } from "@react-three/fiber";
import { useRef } from "react";
extend(THREE); // node materials and WebGPU classes join the element catalogue
function Knot() {
const ref = useRef();
useFrame((_, delta) => {
ref.current.rotation.y += delta;
});
return (
<mesh ref={ref}>
<torusKnotGeometry args={[0.8, 0.3, 128, 24]} />
<meshBasicNodeMaterial colorNode={color(0x42699c)} />
</mesh>
);
}
export default function App() {
return (
<Canvas
camera={{ position: [3, 2, 4], fov: 50 }}
gl={async (props) => {
const renderer = new THREE.WebGPURenderer(props);
await renderer.init();
return renderer;
}}
>
<color attach="background" args={["#111827"]} />
<Knot />
</Canvas>
);
}
fiber 也会将颜色默认值应用于工厂函数创建的渲染器:ACESFilmicToneMapping(使用 flat 时为 NoToneMapping)和 sRGB 输出。WebGPURenderer 会对整帧进行色调映射,包括背景,因此相同的 <color attach="background"> 在 WebGLRenderer 下会显得更亮,除非 Canvas 设置了 flat。
已在 WebGPURenderer 的 WebGPU 后端和启用 forceWebGL: true 的情况下进行渲染测试(fiber 9.8.2,drei 10.7.9)。两个后端均可正常工作:场景图、钩子、事件(raycaster 测试的是 CPU 几何体,因此看不到 positionNode 的位移)、useGLTF(加载的材质会转换为节点材质)、useTexture、Preload、Center、Bounds、Instances、Detailed、OrbitControls、CameraControls、未设置 occlude="blending" 的 Html,以及使用 preset、map 或 .hdr 文件的 Environment;useProgress、AdaptiveDpr 和 PerformanceMonitor 不会调用任何渲染器 API(可阅读其源码)。
不兼容 WebGPU:Text 只会绘制一个没有纹理的四边形,而不是字形(troika 通过 onBeforeCompile 注入其 SDF 着色器);ContactShadows(使用 onBeforeCompile 深度材质和模糊 ShaderMaterial,three 将其报告为不兼容);设置了 occlude="blending" 的 Html;使用 ground 或 .jpg/.webp 增益图 files 的 Environment(两者都通过 ShaderMaterials 绘制;可阅读源码);以及带有子元素的 Environment,它通过立方体相机进行烘焙,在 WebGPU 后端会产生反射,但在 WebGL 2 回退模式下反射会变黑,且不会报错。TSL 材质插槽(colorNode、positionNode)的行为与 threejs-shaders 完全相同——编写 Fn 代码,而不是 GLSL。
常见错误
requestAnimationFrame(tick)→useFrame((state, delta) => { ... }):Canvas 已经运行帧循环;单独的循环会与其争用,并绕过其效果。state.clock.getElapsedTime()→state.clock.elapsedTime:getElapsedTime() 内部会调用 getDelta(),从而缩短 fiber 为下一帧测量的 delta。import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"→import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js":three/addons/在打包器和 three.js 文档中提供的导入映射下都能解析,而examples/jsm路径在这些导入映射下会失效。useFrame(() => { ref.current.rotation.y += 0.01; })→useFrame((_, delta) => { ref.current.rotation.y += delta; }):第二个参数是帧间隔;固定增量在不同刷新率下运行速度不同。useFrame((_, delta) => setRotation((r) => r + delta))→useFrame((_, delta) => { ref.current.rotation.y += delta; }):在帧循环中设置状态会让 React 每帧都重新渲染;应通过 refs 修改对象。useFrame((state, delta) => { ref.current.rotation.y += delta; }, 1)→useFrame((_, delta) => { ref.current.rotation.y += delta; }):正的渲染优先级会让 Canvas 停止渲染;除非回调自行调用state.gl.render(state.scene, state.camera),否则场景会变黑。<mesh position={{ x: 1, y: 0, z: 0 }}>→<mesh position={[1, 0, 0]}>:position是保存 Vector3 的只读属性,因此普通对象会抛出 "Cannot assign to read only property" 错误,导致 Canvas 失效;数组、数字和 Vector3 则会写入现有的 Vector3(fromArray、setScalar、copy)。useEffect(() => { new GLTFLoader().load(url, (gltf) => setScene(gltf.scene)); }, [])→const gltf = useGLTF(url):useGLTF 会挂起渲染,按 URL 缓存解析后的模型以便重新挂载和预加载,并接入 Draco 和 meshopt 解码;而 effect 版本会在每次挂载时重新获取和解析,且没有 Draco 解码器。useGLTF(url).scene.clone()→useMemo(() => SkeletonUtils.clone(scene), [scene]):Object3D.clone() 会共享同一个骨架,因此副本仍绑定到原始对象的骨骼;SkeletonUtils 会为每个副本重新绑定,而 useMemo 会让每个组件保留一个克隆,而不是每次渲染都创建一个。scene.add(gltf.scene)→<primitive object={gltf.scene} />:以命令式方式添加的对象不会接收 fiber 事件属性,并且组件卸载后仍会留在场景中;primitive 会接收事件属性,并随组件添加和移除(其释放仍由你负责)。<water args={[geometry]} />→extend({ Water }); <water args={[geometry]} />:元素只会根据 three 命名空间解析;必须先注册 addon 类。<line>→<threeLine>:裸标签会与 TypeScript 中的 SVG line 元素冲突;fiber 会去掉前缀three并构建 THREE.Line。<Canvas shadows>→<Canvas shadows="percentage">:布尔值会选择 PCFSoftShadowMap,该选项在 r182 中已弃用——three 会回退到 PCF 并发出控制台警告;字符串则会选择类型且不发出警告。<directionalLight intensity={2} />→<directionalLight castShadow intensity={2} />:只有灯光也启用了投射阴影,且设置了 Canvas 的 shadows 标志,网格的 castShadow 才会绘制阴影。ref.current.position.x += 1→ref.current.position.x += 1; invalidate();:在 frameloop="demand" 下,单独修改属性不会触发重绘,因此命令式修改后应调用useThree((state) => state.invalidate)获取的invalidate;而 React 属性变化会自行触发失效。gl={new THREE.WebGPURenderer()}→gl={async (props) => { const renderer = new THREE.WebGPURenderer(props); await renderer.init(); return renderer; }}:未使用 fiber 的 canvas 构建的渲染器会绘制到自己的 canvas 上,而该 canvas 不会显示;在 init() 完成前调用其 render() 也会抛出异常。工厂函数会获取 Canvas 的 canvas,fiber 只会在渲染器初始化完成后开始渲染。new OrbitControls(camera, renderer.domElement)→<OrbitControls makeDefault />:drei 组件会接入逐帧更新、失效处理和释放;原始类在启用阻尼时会卡住,并且永远不会触发按需渲染循环失效。<Environment><Lightformer /></Environment>→<Environment files="env.hdr" />:在 WebGPURenderer 下,子元素形式会通过立方体相机烘焙,但在 WebGL 2 回退后端上反射会变黑,且不会报错;.hdr文件和预设只会设置纹理,因此在两种后端上都能正常工作。
另请参阅
threejs-fundamentals- Canvas 为你管理的渲染器、场景和摄像机概念threejs-loaders- 深入了解加载器配置、压缩和释放threejs-interaction- fiber 指针事件之外的光线投射和控件threejs-shaders- fiber 元素中使用的 TSL 节点材质和插槽threejs-performance- fiber 自适应工具之外的性能分析和优化threejs-asset-pipeline- gltfjsx 组件,以及在 useGLTF 加载模型前对其进行压缩