mcpField
T
threejs-react-three-fiber

threejs-react-three-fiber

github.com/cloudai-x开发3.8k1.9k 次安装1 周前更新
代码仓库

关于 threejs-react-three-fiber

面向使用 React Three Fiber 和 drei 构建 3D 场景的 Three.js 技能。内容涵盖声明式场景组合、Hooks、资源加载、交互处理及 React 3D 项目的性能技巧。

主要功能

  1. 01用 JSX 组合 Three.js 场景
  2. 02通过 Hooks 控制渲染循环
  3. 03加载 GLTF 模型与纹理
  4. 04处理基于射线投射的指针事件
  5. 05使用 drei 控制器与场景辅助工具
  6. 06应用按需渲染和实例化

适用场景

  1. 01在 React 中构建交互式 3D 视图
  2. 02加载并展示 GLTF 资源
  3. 03添加相机控制与场景标签
  4. 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 会报告包围盒)。它会测量顶点位置,因此会错误放置 drei Text,因为其几何体是单位四边形;请改用 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 加载模型前对其进行压缩

评论

支持 Markdown