Three.js 3D 堆栈
2026-10 月验证:three 0.186,@react-three/fiber 9.8,@react-three/drei 10.7,@react-three/rapier 2.2,@react-three/postprocessing 3.2(+ postprocessing 6.39)。Fiber 9 / Drei 10 / Rapier 2 / postprocessing 3 需要 React 19(fiber 9.8 peer: react >=19 <19.4)。
npm i three @react-three/fiber @react-three/drei
npm i @react-three/rapier # 物理引擎
npm i @react-three/postprocessing postprocessing # 特效
npm i -D @types/three
基础场景(Web)
import { useRef, Suspense } from 'react'
import * as THREE from 'three'
import { Canvas, useFrame } from '@react-three/fiber'
import { OrbitControls, Environment, ContactShadows } from '@react-three/drei'
function Box() {
const ref = useRef<THREE.Mesh>(null!)
useFrame((_, delta) => { ref.current.rotation.y += delta }) // 每帧修改 ref,绝不使用 setState
return (
<mesh ref={ref} castShadow>
<boxGeometry />
<meshStandardMaterial color="#4a9eff" />
</mesh>
)
}
<Canvas camera={{ position: [3, 2, 5], fov: 45 }} dpr={[1, 2]} shadows>
<ambientLight intensity={0.3} />
<directionalLight position={[5, 5, 5]} castShadow />
<Suspense fallback={null}>
<Box />
<Environment preset="city" />
</Suspense>
<ContactShadows position={[0, -0.5, 0]} opacity={0.5} blur={2} />
<OrbitControls />
</Canvas>
规则:
- 所有异步操作(useGLTF、useTexture、Environment、Text 字体、Physics WASM)都应放在
<Suspense>下。 - 首次渲染即需加载的模型,在模块级别使用
useGLTF.preload(url)。 - 场景尺寸保持在 1–10 单位左右;过大或过小的比例会破坏阴影和控制器。
MeshStandardMaterial需要光源或<Environment>;否则渲染为黑色。
Drei(按需选用)
- 相机/控制器:
OrbitControls、CameraControls、PerspectiveCamera makeDefault - 资源:
useGLTF、useTexture、Environment(预设:apartment、city、dawn、forest、lobby、night、park、studio、sunset、warehouse)、Center、Bounds - 布景:
ContactShadows、AccumulativeShadows、Stage、Float、Sparkles、Stars、Cloud - 材质:
MeshTransmissionMaterial(玻璃)、MeshReflectorMaterial(反射地面) - 文字:
Text(SDF,支持任意字体 URL)、Text3D(需要 typeface JSON)、Html(DOM 覆盖层)
Rapier 物理引擎
import { Physics, RigidBody, CuboidCollider, type RapierRigidBody } from '@react-three/rapier'
<Suspense>
<Physics gravity={[0, -9.81, 0]} /* debug */>
<RigidBody colliders="hull" restitution={0.6} ref={bodyRef}>
<mesh><torusGeometry /><meshStandardMaterial /></mesh>
</RigidBody>
<RigidBody type="fixed">
<CuboidCollider args={[10, 0.1, 10]} /> {/* 半长宽高 */}
</RigidBody>
</Physics>
</Suspense>
// bodyRef.current?.applyImpulse({ x: 0, y: 5, z: 0 }, true)
colliders:"cuboid"(默认)、"ball"、"hull"、"trimesh"、false(自行添加碰撞体组件)。type:"dynamic"(默认)、"fixed"、"kinematicPosition"、"kinematicVelocity"。timeStep默认为固定的 1/60。timeStep="vary"不确定。手动步进:useRapier().step(1 / 60)。- 在
<Physics>上添加debug可绘制碰撞体。
后处理特效
import { EffectComposer, Bloom, Vignette, DepthOfField, Noise } from '@react-three/postprocessing'
<EffectComposer>{/* 作为 <Canvas> 的最后一个子元素 */}
<Bloom luminanceThreshold={0.9} intensity={1.2} mipmapBlur />
<Vignette offset={0.1} darkness={0.9} />
</EffectComposer>
- Bloom 仅捕获高于
luminanceThreshold的像素。若要让某个对象发光:使用emissive颜色 +emissiveIntensity> 1,并在其材质上设置toneMapped={false}。 - 每个特效消耗一次全屏渲染;2–4 个特效足矣。
视频场景
Remotion(参见 remotion-markup/3d.md):使用 @remotion/three 中的 <ThreeCanvas width height>;不要使用 useFrame;所有运动来自 useCurrentFrame()。此外:
- 不使用
OrbitControls——从frame移动相机(使用interpolate/spring)。 - 依赖自身时钟动画的 Drei 辅助组件(
Float、Sparkles、Stars、Cloud、MeshWobbleMaterial)在渲染中会闪烁。要么删除,要么用frame重新实现运动。 - 物理引擎:Remotion 跨标签页乱序渲染帧,因此实时模拟无法匹配。需要烘焙:初始化
@dimforge/rapier3d-compat(@react-three/rapier 底层引擎)一次,对每一帧使用world.step()步进,timestep = 1 / fps,存储每个刚体每帧的位置 + 旋转,然后在帧索引frame处渲染变换。
在 Remotion 之外(屏幕录制、GSAP 驱动):useFrame 是可以的;当需要镜头可重复时,使用时间线时间(tl.time())驱动,而非 delta。