diff --git a/manual/list.json b/manual/list.json index 6bcd490504e223..5a24759f8584bd 100644 --- a/manual/list.json +++ b/manual/list.json @@ -339,6 +339,15 @@ "相关资源": "zh/useful-links", "WebGL兼容性检查": "zh/webgl-compatibility-check" }, + "进阶": { + "动画系统": "zh/animation-system", + "颜色管理": "zh/color-management", + "如何创建VR内容": "zh/how-to-create-vr-content", + "如何释放对象": "zh/how-to-dispose-of-objects", + "如何更新对象": "zh/how-to-update-things", + "如何使用后处理": "zh/how-to-use-post-processing", + "矩阵变换": "zh/matrix-transformations" + }, "---": {}, "基本": { "基础": "zh/fundamentals", @@ -356,7 +365,8 @@ "阴影": "zh/shadows", "雾": "zh/fog", "渲染目标": "zh/rendertargets", - "自定义缓冲几何体": "zh/custom-buffergeometry" + "自定义缓冲几何体": "zh/custom-buffergeometry", + "物理": "zh/physics" }, "技巧": { "按需渲染": "zh/rendering-on-demand", @@ -390,6 +400,10 @@ "体素几何体 (Minecraft)": "zh/voxel-geometry", "来试试做一个游戏吧": "zh/game" }, + "WebGPU": { + "WebGPU渲染器": "zh/webgpurenderer", + "后处理": "zh/webgpu-postprocessing" + }, "WebXR": { "VR - 基础": "zh/webxr-basics", "VR - 用目光进行选择": "zh/webxr-look-to-select", diff --git a/manual/zh/animation-system.html b/manual/zh/animation-system.html new file mode 100644 index 00000000000000..7cf9bb4555da6a --- /dev/null +++ b/manual/zh/animation-system.html @@ -0,0 +1,169 @@ +
+ +
+ 在 three.js 的动画系统中,你可以为模型的多种属性制作动画:
+ 例如蒙皮绑定模型的骨骼、形态目标(morph targets)、不同材质属性
+ (颜色、不透明度、布尔值)、可见性与变换。动画属性可以淡入、
+ 淡出、交叉淡化(crossfade)和变速。即使是同一对象上的多个动画,
+ 或不同对象上的多个动画,也可以独立调整权重和时间缩放,
+ 并进行同步。
+
+ 为了在一个统一系统中实现这些功能,three.js 动画系统在
+ 2015 年[link:https://github.com/mrdoob/three.js/issues/6881 发生了彻底重构]
+ (注意甄别过时资料)。当前架构与 Unity / Unreal Engine 4
+ 更接近。本页将简要介绍该系统的核心组件,以及它们如何协同工作。
+
+
+
+ 当你成功导入一个带动画的 3D 对象后(无论它使用骨骼、形态目标,或两者兼有),
+ 比如通过 [link:https://github.com/KhronosGroup/glTF-Blender-IO glTF Blender 导出器]
+ 从 Blender 导出,再使用 `GLTFLoader` 加载到 three.js 场景中,
+ 返回结果中通常会有一个名为 "animations" 的数组字段,
+ 其中包含该模型的动画片段(下文会列出支持此能力的加载器)。
+
+ 每个 `AnimationClip` 一般表示对象的一种动作数据。以角色模型为例,
+ 可以有一个片段表示走路,第二个表示跳跃,第三个表示侧移,等等。
+
+
+
+ 在 `AnimationClip` 内部,每个被动画驱动的属性都会存储在独立的
+ `KeyframeTrack` 中。假设角色有骨架,那么一条轨道可以记录前臂骨骼
+ 随时间变化的位置数据,另一条记录同一骨骼的旋转变化,第三条记录
+ 其他骨骼的位置、旋转或缩放,依此类推。也就是说,
+ 一个 AnimationClip 通常由大量此类轨道组成。
+
+ 如果模型有形态目标(比如一个表示微笑,另一个表示愤怒),
+ 每条相关轨道会描述某个形态目标在该片段播放过程中,
+ 其影响权重如何随时间变化。
+
+
+ + 这些存储的数据只是动画基础,真正的播放控制由 `AnimationMixer` 完成。 + 你可以把它理解成不只是一个“播放器”,更像一台真实的混音台: + 能够同时控制多个动画,并对它们进行混合与融合。 + +
+ ++ + `AnimationMixer` 本身只有少量通用属性和方法, + 因为它主要通过动画动作来驱动。通过配置 `AnimationAction`, + 你可以决定某个 `AnimationClip` 在某个 mixer 上何时播放、暂停或停止, + 是否循环、循环次数、是否淡入淡出、是否进行时间缩放, + 以及更多高级控制(如交叉淡化与同步)。 + +
+ ++ + 如果你希望一组对象共享同一套动画状态, + 可以使用 `AnimationObjectGroup`。 + +
+ ++ 请注意,并非所有模型格式都包含动画(例如 OBJ 就不包含), + 而且只有部分 three.js 加载器支持 `AnimationClip` 序列。 + 下面这些加载器支持这种动画数据: +
+ ++ 另外,3ds Max 和 Maya 目前还不能直接将多个动画 + (即不在同一时间轴上的动画)导出到同一个文件中。 +
+ +
+let mesh;
+
+// 创建 AnimationMixer,并获取 AnimationClip 列表
+const mixer = new THREE.AnimationMixer( mesh );
+const clips = mesh.animations;
+
+// 每帧更新 mixer
+function update () {
+ mixer.update( deltaSeconds );
+}
+
+// 播放指定动画
+const clip = THREE.AnimationClip.findByName( clips, 'dance' );
+const action = mixer.clipAction( clip );
+action.play();
+
+// 播放全部动画
+clips.forEach( function ( clip ) {
+ mixer.clipAction( clip ).play();
+} );
+
+
+ + 每一种色彩空间,都是一组经过权衡的设计选择。它们共同目标是: + 在满足精度和显示技术限制的前提下,覆盖尽可能大的颜色范围。 + 在创建 3D 资源或把多个 3D 资源组装进同一场景时, + 理解这些属性及其在不同色彩空间之间的关系非常重要。 +
+ +
+ + 来看两个最常见的色彩空间:`SRGBColorSpace`(sRGB)与 + `LinearSRGBColorSpace`(Linear-sRGB)。两者原色和白点相同, + 因此色域一致,也都使用 RGB 模型。它们只在传递函数上不同: + Linear-sRGB 相对于物理光强是线性的;sRGB 使用非线性传递函数, + 更接近人眼感知与常见显示设备的响应特性。 +
+ ++ 这个差异非常关键。光照计算和大多数渲染运算通常必须在线性色彩空间中完成。 + 但线性颜色在图像或帧缓冲中的存储效率较低,且直接显示给人眼时观感不正确。 + 因此,输入纹理与最终输出图像通常会使用非线性的 sRGB 色彩空间。 +
+ +++ ++ ℹ️ 注意:虽然部分现代显示器支持 Display-P3 等更宽色域, + 但 Web 平台图形 API 仍主要基于 sRGB。 + 当前 three.js 应用通常只会使用 sRGB 与 Linear-sRGB。 +
+
+ 现代渲染所需的线性工作流通常会涉及不止一种色彩空间, + 每种色彩空间承担不同职责。线性与非线性色彩空间适用于不同环节, + 如下所示。 +
+ ++ 传入 three.js 的颜色(来自取色器、纹理、3D 模型等)都带有各自色彩空间。 + 凡是不在 Linear-sRGB 工作色彩空间中的输入,都需要转换; + 纹理也必须正确设置 texture.colorSpace。 + 如果在初始化颜色前启用 THREE.ColorManagement, + 某些转换(如十六进制颜色与 CSS sRGB 颜色)会自动处理: +
+ +
+THREE.ColorManagement.enabled = true;
+
+
+ + THREE.ColorManagement 默认已启用。 +
+ +++ ++ ⚠️ 警告:许多 3D 模型格式并未正确或一致地定义色彩空间信息。 + three.js 虽会尽量处理常见情况,但旧格式仍常出现问题。 + 为获得最佳结果,请优先使用 glTF 2.0(`GLTFLoader`), + 并尽早在在线查看器中验证资源本身是否正确。 +
+
+ 渲染、插值及许多其他计算,必须在开域的线性工作色彩空间中进行, + 此时 RGB 分量与物理光照强度成比例。在 three.js 中, + 工作色彩空间是 Linear-sRGB。 +
+ ++ 输出到显示设备、图片或视频时,通常需要将开域 Linear-sRGB + 工作空间转换到目标色彩空间。该转换由 + `WebGLRenderer.outputColorSpace` 定义。 + 使用后处理时,需要 `OutputPass`。 +
+ +++ ++ ⚠️ 警告:渲染目标可使用 sRGB 或 Linear-sRGB。 + sRGB 在有限精度下利用率更高:在闭区间内,sRGB 常用 8-bit 即可, + 而 Linear-sRGB 可能需要至少 16-bit(half float)。 + 若后续管线阶段还要求 Linear-sRGB 输入,额外转换会带来一定性能开销。 +
+
+ 基于 `ShaderMaterial` 和 `RawShaderMaterial` 的自定义材质需要自行实现输出色彩空间转换。 + 对于 `ShaderMaterial`,通常在片元着色器 `main()` 中加入 + `colorspace_fragment` shader chunk 即可。 +
+ ++ 读取或修改 `Color` 的方法默认假设数据已经在 three.js 的工作色彩空间 + (Linear-sRGB)中。RGB 与 HSL 分量都直接对应 `Color` 实例内部数据, + 不会被隐式转换。你可以显式调用 + .convertLinearToSRGB() 或 .convertSRGBToLinear() 进行转换。 +
+ ++// RGB 分量(不发生转换)。 +color.r = color.g = color.b = 0.5; +console.log( color.r ); // → 0.5 + +// 手动转换。 +color.r = 0.5; +color.convertSRGBToLinear(); +console.log( color.r ); // → 0.214041140 ++ +
+ 当设置 ColorManagement.enabled = true(推荐,且默认开启)后, + 某些转换会自动执行。由于十六进制与 CSS 颜色通常属于 sRGB, + `Color` 在 setter 中会把它们从 sRGB 转为 Linear-sRGB; + 在 getter 返回十六进制或 CSS 值时,则会从 Linear-sRGB 转回 sRGB。 +
+ ++// 十六进制转换。 +color.setHex( 0x808080 ); +console.log( color.r ); // → 0.214041140 +console.log( color.getHex() ); // → 0x808080 + +// CSS 颜色转换。 +color.setStyle( 'rgb( 0.5, 0.5, 0.5 )' ); +console.log( color.r ); // → 0.214041140 + +// 通过 'colorSpace' 参数覆盖默认转换。 +color.setHex( 0x808080, LinearSRGBColorSpace ); +console.log( color.r ); // → 0.5 +console.log( color.getHex( LinearSRGBColorSpace ) ); // → 0x808080 +console.log( color.getHex( SRGBColorSpace ) ); // → 0xBCBCBC ++ +
+ 当某个颜色或纹理配置错误时,它看起来会比预期更亮或更暗。 + 当渲染器输出色彩空间配置错误时,整张场景都可能偏暗 + (例如遗漏了到 sRGB 的转换)或偏亮(例如后处理中重复转换到 sRGB)。 + 这类问题通常并非全局线性偏差,单纯增减光照并不能真正解决。 +
+ ++ 更隐蔽的问题是:当输入和输出色彩空间都设置错误时, + 整体亮度看似正常,但颜色会在不同光照下异常变化, + 或明暗层次变得过曝、生硬。两个错误不会相互抵消。 + 务必确保工作色彩空间是线性的(scene referred), + 输出色彩空间是非线性的(display referred)。 +
+ ++ 本指南简要介绍如何使用 three.js 构建一个基于 Web 的 VR 应用, + 以及其中最基础的组成部分。 +
+ ++ 首先,在项目中引入 + [link:https://github.com/mrdoob/three.js/blob/master/examples/jsm/webxr/VRButton.js VRButton.js]。 +
+ +
+import { VRButton } from 'three/addons/webxr/VRButton.js';
+
+
+ + *VRButton.createButton()* 会做两件重要的事:它会创建一个用于指示 + VR 兼容性的按钮;此外,如果用户点击该按钮,它会发起 VR 会话。 + 你只需要在应用中添加下面这行代码。 +
+ ++document.body.appendChild( VRButton.createButton( renderer ) ); ++ +
+ 接下来,需要在 `WebGLRenderer` 实例上启用 XR 渲染。 +
+ ++renderer.xr.enabled = true; ++ +
+ 最后,要调整动画循环。VR 场景中不再使用常见的 + *window.requestAnimationFrame()*,而是使用 `renderer.setAnimationLoop()`。 + 最小示例代码如下: +
+ +
+renderer.setAnimationLoop( function () {
+
+ renderer.render( scene, camera );
+
+} );
+
+
+
+ 可以查看官方 WebXR 示例,了解上述流程在实际项目中的用法。
+
+ [example:webxr_xr_ballshooter WebXR / XR / ballshooter]
+ [example:webxr_xr_cubes WebXR / XR / cubes]
+ [example:webxr_xr_dragging WebXR / XR / dragging]
+ [example:webxr_xr_paint WebXR / XR / paint]
+ [example:webxr_xr_sculpt WebXR / XR / sculpt]
+ [example:webxr_vr_panorama_depth WebXR / VR / panorama_depth]
+ [example:webxr_vr_panorama WebXR / VR / panorama]
+ [example:webxr_vr_rollercoaster WebXR / VR / rollercoaster]
+ [example:webxr_vr_sandbox WebXR / VR / sandbox]
+ [example:webxr_vr_video WebXR / VR / video]
+
+ 为了提升性能并避免内存泄漏,及时释放不再使用的对象非常关键。 + 每当你创建一个 *three.js* 实例,都会分配一定内存。同时,*three.js* + 还会为几何体、材质等对象创建渲染所需的 WebGL 资源(如缓冲区和着色器程序)。 + 这些资源不会自动释放,应用必须通过专用 API 主动清理。 + 本文简要说明这些 API 的使用方式,以及哪些对象需要关注。 +
+ ++ 几何体通常由一组顶点属性组成。*three.js* 会为每个属性在内部创建 + [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLBuffer WebGLBuffer]。 + 这些资源只有在调用 `BufferGeometry.dispose()` 后才会被删除。 + 当几何体不再使用时,应执行该方法释放相关资源。 +
+ ++ 材质决定对象如何被渲染。*three.js* 会根据材质信息构建着色器程序。 + 着色器程序只有在对应材质被释放后才可能删除。出于性能考虑, + *three.js* 会尽量复用已存在的着色器程序,因此只有当相关材质都释放后, + 着色器程序才会真正销毁。释放材质请调用 `Material.dispose()`。 +
+ ++ 释放材质不会影响纹理。纹理需要单独管理,因为一个纹理可能被多个材质共享。 + 每当创建 `Texture`,three.js 会在内部创建 + [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLTexture WebGLTexture]。 + 与缓冲区一样,它只能通过 `Texture.dispose()` 删除。 +
+ ++ 如果纹理数据源是 `ImageBitmap`,你还需要在应用层调用 + [link:https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap/close ImageBitmap.close]() + 来释放 CPU 侧资源。`Texture.dispose()` 无法自动调用该方法, + 因为 `close()` 后图像位图将不可再用,而引擎无法判断它是否还被其他地方使用。 +
+ ++ `WebGLRenderTarget` 不仅会分配 + [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLTexture WebGLTexture], + 还会分配 [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLFramebuffer WebGLFramebuffer] + 和 [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderbuffer WebGLRenderbuffer] + 来支持自定义渲染输出。这些资源只能通过 `WebGLRenderTarget.dispose()` 释放。 +
+ ++ 蒙皮网格通过骨架(skeleton)表示骨骼层级。若不再需要某个蒙皮网格, + 可以对其骨架调用 `Skeleton.dispose()` 释放内部资源。 + 注意骨架可能被多个蒙皮网格共享,只有在确认未被其他活动对象使用时再释放。 +
+ ++ examples 目录中的其他类(如 controls、后处理 pass)也可能提供 `dispose()`, + 用于移除内部事件监听器或渲染目标。通常建议查看类的 API / 文档, + 只要有 `dispose()`,在清理阶段就应调用。 +
+ ++ 这是社区经常提出的问题。核心原因是:*three.js* 无法知道用户创建对象 + (如几何体、材质)的生命周期与作用域,这属于应用层职责。 + 例如某材质当前帧没被使用,下一帧仍可能需要。 + 因此当应用确认对象可删除时,必须调用对应 `dispose()` 通知引擎。 +
+ ++ 不会。你需要显式调用 *dispose()* 释放几何体和材质。 + 同时要注意它们可能被多个 3D 对象共享。 +
+ ++ 可以。查看渲染器的 `renderer.info` 属性即可,它包含显存与渲染流程的统计信息, + 包括当前内部缓存了多少纹理、几何体、着色器程序等。 + 如果应用出现性能问题,调试该属性有助于快速定位内存泄漏。 +
+ ++ 纹理内部资源只有在图像完全加载后才会分配。 + 若在加载前就调用 `dispose()`,通常不会发生任何事; + 因为资源尚未创建,也就无需清理。 +
+ ++ 取决于对象类型。对于几何体、材质、纹理、渲染目标和后处理 pass, + 被删除的内部资源通常可以由引擎重新创建,因此不会直接报运行时错误; + 但当前帧可能有性能损耗,尤其在需要重新编译着色器时。 + + controls 和 renderer 属于例外:调用 `dispose()` 后实例不可继续使用, + 需要重新创建。 +
+ ++ 没有唯一标准答案,取决于具体业务场景。需要强调的是,并非任何时候都必须立即释放。 + 例如多关卡游戏,切关通常是做统一清理的好时机:遍历旧场景并释放失效的材质、 + 几何体和纹理。就像上文所说,即使误释放了仍会被使用的对象, + 一般也不会立刻报错,最坏情况通常是某一帧性能下降。 +
+ ++ 某些情况下,Three.js 会创建一些内部使用的纹理和几何体, + 它们无法通过遍历场景图直接访问到,因此也无法在遍历时释放。 + 所以即便做了完整场景清理,`renderer.info.memory` 仍可能显示这些对象。 + 这通常不代表泄漏,它们会在后续“清理-重建”循环中被复用。 + + 常见相关场景包括使用 `material.envMap`、`scene.background`、 + `scene.environment` 等,会触发引擎创建内部资源。 +
+ +
+ [example:webgl_test_memory WebGL / test / memory]
+ [example:webgl_test_memory2 WebGL / test / memory2]
+
默认情况下,只要对象被添加到场景中,就会自动更新其矩阵:
++const object = new THREE.Object3D(); +scene.add( object ); ++ 或者,它是某个已加入场景对象的子对象: +
+const object1 = new THREE.Object3D(); +const object2 = new THREE.Object3D(); + +object1.add( object2 ); +scene.add( object1 ); // object1 和 object2 都会自动更新它们的矩阵 ++
但如果你确定对象是静态的,可以关闭自动更新,仅在需要时手动更新变换矩阵。
+ ++object.matrixAutoUpdate = false; +object.updateMatrix(); ++ +
+ BufferGeometry 把顶点位置、面索引、法线、颜色、UV 以及自定义属性等信息 + 存在属性缓冲中,也就是 + [link:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Typed_arrays 类型化数组]。 + 这种结构通常比旧版 Geometry 更快,但使用起来通常更复杂。 +
++ 更新 BufferGeometry 时最重要的一点是:不能调整缓冲区大小 + (开销很大,基本等同于新建几何体),但可以更新缓冲区已有内容。 +
++ 这意味着如果你知道某个属性会增长(如顶点数增加), + 必须预先分配足够大的缓冲区来容纳新增数据。 + 同时也意味着 BufferGeometry 必然存在最大容量, + 无法做到无限高效扩展。 +
++ 下面以“运行时不断延长的线段”为例: + 先为 500 个顶点分配空间,但起初只绘制 2 个点, + 通过 `BufferGeometry.drawRange` 控制绘制范围。 +
+
+const MAX_POINTS = 500;
+
+// 几何体
+const geometry = new THREE.BufferGeometry();
+
+// 属性
+const positions = new Float32Array( MAX_POINTS * 3 ); // 每个点使用 3 个浮点数(x、y、z)
+geometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
+
+// 绘制范围
+const drawCount = 2; // 仅绘制前 2 个点
+geometry.setDrawRange( 0, drawCount );
+
+// 材质
+const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
+
+// 线段
+const line = new THREE.Line( geometry, material );
+scene.add( line );
+
+ + 接着按如下方式随机写入线段点位: +
+
+const positionAttribute = line.geometry.getAttribute( 'position' );
+
+let x = 0, y = 0, z = 0;
+
+for ( let i = 0; i < positionAttribute.count; i ++ ) {
+
+ positionAttribute.setXYZ( i, x, y, z );
+
+ x += ( Math.random() - 0.5 ) * 30;
+ y += ( Math.random() - 0.5 ) * 30;
+ z += ( Math.random() - 0.5 ) * 30;
+
+}
+
+ + 如果要在首次渲染后改变绘制点数量,这样做: +
++line.geometry.setDrawRange( 0, newValue ); ++
+ 如果要在首次渲染后修改位置数据,需要设置 `needsUpdate`: +
++positionAttribute.needsUpdate = true; // 首次渲染后修改数据时必须设置 ++ +
+ 首次渲染后修改位置数据时,通常还需要重新计算包围体, + 以保证视锥体裁剪、辅助器等功能正常工作。 +
++line.geometry.computeBoundingBox(); +line.geometry.computeBoundingSphere(); ++ +
+ [link:https://jsfiddle.net/t4m85pLr/1/ 这个 fiddle 示例] + 展示了一个动画线段,你可以按需改造。 +
+ +
+ [example:webgl_custom_attributes WebGL / custom / attributes]
+ [example:webgl_buffergeometry_custom_attributes_particles WebGL / buffergeometry / custom / attributes / particles]
+
所有 uniforms 都可以自由修改(如颜色、纹理、不透明度等),并会在每帧传入着色器。
+ +GL 状态相关参数也可随时修改(如 depthTest、blending、polygonOffset 等)。
+ +以下属性在运行时不易修改(尤其材质至少渲染过一次后):
+这些变化会触发重建着色器程序,你需要设置:
+material.needsUpdate = true
+
+ 注意这一步可能较慢并导致帧率抖动或卡顿(尤其在 Windows 上,DirectX 下编译 shader 往往比 OpenGL 更慢)。
+ +为获得更平滑体验,可以通过“占位值”模拟部分开关效果,比如强度为 0 的灯光、纯白纹理或密度为 0 的雾。
+ +你可以替换几何体分块所用材质,但无法在运行时改变对象按面材质划分分块的方式。
+ +若材质/分块数量较少,可提前分块(例如人物:头发/脸/身体/上衣/裤子;汽车:前/侧/顶/玻璃/轮胎/内饰)。
+ +若数量很大(例如每个面都可能不同),建议改用属性或纹理驱动每面外观,而非大量分块材质。
+ +
+ [example:webgl_materials_car WebGL / materials / car]
+ [example:webgl_postprocessing_dof WebGL / webgl_postprocessing / dof]
+
图像、Canvas、视频和数据纹理若内容有变更,需要设置:
+
+ texture.needsUpdate = true;
+
+ 渲染目标会自动更新。
+ +
+ [example:webgl_materials_video WebGL / materials / video]
+ [example:webgl_rtt WebGL / rtt]
+
相机位置和目标会自动更新。如果你要修改以下参数:
++ 则需要重新计算投影矩阵: +
++camera.aspect = window.innerWidth / window.innerHeight; +camera.updateProjectionMatrix(); ++
+ `InstancedMesh` 用于在 `three.js` 中便捷地进行实例化渲染。 + 视锥体裁剪、射线检测等功能依赖最新包围体(包围盒和包围球)。 + 由于 `InstancedMesh` 的工作方式,它具有自己的 `boundingBox` 与 `boundingSphere`, + 会覆盖几何体级别的包围体。 +
++ 与几何体类似,只要底层数据变化就应重算包围体。 + 对 `InstancedMesh` 来说,常见场景是通过 `setMatrixAt()` 修改实例变换矩阵后, + 再重算包围体。处理方式与几何体相同。 +
++instancedMesh.computeBoundingBox(); +instancedMesh.computeBoundingSphere(); ++ +
+ 在包围体机制上,`SkinnedMesh` 与 `InstancedMesh` 原则相同: + 它拥有自己的 `boundingBox` 与 `boundingSphere`,用于正确包裹动画中的网格。 + 当调用 `computeBoundingBox()` 与 `computeBoundingSphere()` 时, + 会基于当前骨骼变换(即当前动画状态)计算对应包围体。 +
++ 许多 three.js 应用会把 3D 对象直接渲染到屏幕上。但在一些场景中, + 你会希望叠加景深(DOF)、Bloom、胶片颗粒、抗锯齿等视觉效果。后处理(Post Processing) + 是实现这些效果的常见方案:先把场景渲染到一个渲染目标(显存中的图像缓冲区), + 再通过一个或多个后处理 pass 对该缓冲区应用滤镜与效果,最后输出到屏幕。 +
++ three.js 通过 `EffectComposer` 提供了完整的后处理工作流支持。 +
+ ++ 第一步是从 examples 目录导入所需模块。本文默认你使用 three.js 官方 + [link:https://www.npmjs.com/package/three npm 包]。在本教程的基础示例中,我们需要以下文件。 +
+ +
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+import { GlitchPass } from 'three/addons/postprocessing/GlitchPass.js';
+import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
+
+
+ + 导入完成后,把 `WebGLRenderer` 实例传入,创建 composer。 +
+ ++const composer = new EffectComposer( renderer ); ++ +
+ 使用 composer 后,需要调整动画循环:不再调用 `WebGLRenderer.render()`, + 而改为调用 `EffectComposer.render()`。 +
+ +
+function animate() {
+
+ requestAnimationFrame( animate );
+
+ composer.render();
+
+}
+
+
+ + 到这里 composer 已准备就绪,可以配置后处理 pass 链。各 pass 按添加顺序依次执行, + 共同决定最终输出结果。在本例中先执行 `RenderPass`,再执行 `GlitchPass`, + 最后执行 `OutputPass`。链路中最后一个启用的 pass 会自动渲染到屏幕。 + 配置如下: +
+ ++const renderPass = new RenderPass( scene, camera ); +composer.addPass( renderPass ); + +const glitchPass = new GlitchPass(); +composer.addPass( glitchPass ); + +const outputPass = new OutputPass(); +composer.addPass( outputPass ); ++ +
+ `RenderPass` 通常放在链路开头,用于提供场景渲染结果给后续步骤。 + 本例中 `GlitchPass` 会基于图像数据施加故障效果。 + `OutputPass` 一般放在最后,负责 sRGB 色彩空间转换与色调映射(tone mapping)。 + 可参考这个 [link:https://threejs.org/examples/webgl_postprocessing_glitch 在线示例]。 +
+ ++ 引擎提供了大量预置后处理 pass,可在 + [link:https://github.com/mrdoob/three.js/tree/dev/examples/jsm/postprocessing postprocessing] + 目录中找到。 +
+ ++ 如果你要编写自定义后处理着色器并接入 pass 链,可以使用 `ShaderPass`。 + 在导入相关模块和自定义 shader 后,按下述方式添加 pass: +
+ +
+import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
+import { LuminosityShader } from 'three/addons/shaders/LuminosityShader.js';
+
+// 稍后在你的初始化流程中
+
+const luminosityPass = new ShaderPass( LuminosityShader );
+composer.addPass( luminosityPass );
+
+
+ + 仓库中提供的 + [link:https://github.com/mrdoob/three.js/blob/master/examples/jsm/shaders/CopyShader.js CopyShader] + 是编写自定义 shader 的良好起点。`CopyShader` 仅将 `EffectComposer` 的读缓冲内容 + 复制到写缓冲,不附加任何效果。 +
+ ++ Three.js 使用 `matrices`(矩阵)来表示 3D 变换:平移(position)、旋转和缩放。 + 每个 `Object3D` 实例都包含一个 `matrix`,用于存储该对象的位置、旋转与缩放。 + 本页介绍如何更新对象的变换。 +
+ ++ 更新对象变换有两种方式: +
++object.position.copy( start_position ); +object.quaternion.copy( quaternion ); ++ 默认情况下 `matrixAutoUpdate = true`,矩阵会自动重算。 + 如果对象是静态的,或你希望手动控制重算时机,可将其设为 `false` 以获得更好性能: +
+object.matrixAutoUpdate = false; ++ 修改属性后,再手动更新矩阵: +
+object.updateMatrix(); ++
+object.matrix.setRotationFromQuaternion( quaternion ); +object.matrix.setPosition( start_position ); +object.matrixAutoUpdate = false; ++ 注意这种方式下 `matrixAutoUpdate` 必须设为 `false`, + 并且要确保不要调用 `updateMatrix`。 + 调用 `updateMatrix` 会根据 `position`、`scale` 等重新计算矩阵, + 从而覆盖你手动写入的矩阵内容。 +
+ 对象的 `matrix` 存储的是相对于父对象的局部变换; + 若要获取对象在世界坐标中的变换,需要访问对象的世界矩阵。 +
++ 当父对象或子对象变换发生变化时,可以调用 `object.updateMatrixWorld()`, + 触发子对象世界矩阵更新。 +
++ 也可以通过 `applyMatrix4()` 对对象施加变换。注意:该方法底层依赖 + `Matrix4.decompose()`,并非所有矩阵都能这样分解。 + 例如父对象存在非均匀缩放时,子对象世界矩阵可能无法正确分解, + 此时该方法并不适用。 +
+ ++ Three.js 提供两种 3D 旋转表示:欧拉角与四元数,并支持相互转换。 + 欧拉角会受到“万向节锁(gimbal lock)”问题影响, + 在某些姿态下会丢失一个自由度(导致无法绕某一轴旋转)。 + 因此,对象旋转在内部始终存储为四元数。 +
++ 旧版本库曾有 `useQuaternion` 属性,设为 false 时会用欧拉角计算对象矩阵。 + 这种做法已废弃。现在应使用 `object.setRotationFromEuler()`, + 它会同步更新四元数。 +
+ ++ 物理引擎可以在 3D 场景中模拟重力、碰撞、受力等物理现象。 + 在常规 three.js 场景中,我们通常直接修改对象位置和旋转; + 而在使用物理引擎时,会额外维护一个并行的“物理世界”, + 刚体在其中响应力和碰撞。然后每帧把 three.js 网格与物理刚体状态同步, + 从而呈现出“真实物理”效果。 +
+ ++ 需要注意的是,物理引擎不一定要每帧更新。为了保持模拟稳定, + 通常会采用固定时间步。比如游戏循环运行于 60fps, + 物理更新运行于 30fps(1/30 秒), + 同时每帧使用物理引擎的最新状态更新 three.js 网格变换(位置、旋转等)。 +
+ ++ 物理模拟尤其适用于游戏、交互可视化,以及任何需要真实对象行为的应用, + 例如下落、弹跳、滑动等效果。 +
+ ++ 在 three.js 项目中集成物理引擎,主要有三种方式: +
+ ++ Three.js 在 examples/jsm/physics 目录中为多个常见物理引擎提供了封装类。 + 这些插件可简化接入流程,完成物理世界初始化与网格同步。 +
+ ++ 可用插件包括: +
+ ++ 这些插件屏蔽了大量底层复杂性。对于常规需求,它们是最快的入门路径之一。 +
+ ++ 许多物理引擎直接由 JavaScript / TypeScript 编写, + 与 Web 生态集成较容易。像 cannon-es 这类库因轻量且接入简单而常被采用。 +
+ ++ 使用这类库时,你需要自己创建物理世界和刚体, + 并在动画循环中手动把刚体的位置、四元数同步到 three.js 网格。 +
+ + ++ 另外还有一类“看似 JS/TS、实则调用外部引擎”的方案,例如: +
++ 如果你需要更高性能、稳定性和精度(尤其复杂模拟), + 可以选择 C++/Rust 等语言编写并编译为 WebAssembly(WASM)的物理引擎。 + 例如 Ammo.js(Bullet 的移植版)和 Rapier 都属于这一类。 +
+ ++ 这种方式通常功能最完整、性能最好,但接入成本更高, + 需要处理 WASM 内存管理及与物理 API 的直接交互。 +
+ ++ 其中一些跨平台 3D 物理引擎已有可直接使用的 WASM 版本,例如: +
++ `WebGPURenderer` 提供了全新的后处理组件。本文介绍其工作方式和基础用法。 +
+ ++ 旧版 `WebGLRenderer` 的后处理在设计上存在一些限制。由于渲染器支持不足, + 使用 MRT(多渲染目标)较繁琐,同时缺少自动 pass/effect 合并机制, + 难以优化整体性能。 +
+ ++ 新版 `WebGPURenderer` 后处理栈从一开始就面向这些需求设计。 +
++ 下面来看如何在 three.js 应用中接入这套后处理系统。 +
+ ++ 请先阅读 WebGPURenderer 指南并正确配置导入。 + 然后按如下方式创建渲染管线模块实例: +
+ ++const renderPipeline = new THREE.RenderPipeline( renderer ); ++ +
+ `RenderPipeline` 用于替代旧的 `EffectComposer`。 + 要确保最终输出来自该模块,需要把动画循环改成如下形式: +
++- renderer.render( scene, camera ); ++ renderPipeline.render(); ++ +
+ 大多数后处理流程都会先创建一个所谓的场景 pass(scene pass),也称 beauty pass, + 作为原始渲染图像,然后再叠加 Bloom、景深、SSR 等效果。 + 先从 TSL 命名空间导入 `pass()` 并创建该 pass。 +
+
+import { pass } from 'three/tsl';
+
+// 在你的初始化流程中
+
+const scenePass = pass( scene, camera );
+
+ + 节点系统的核心思想是:把材质或后处理效果表示为节点组合。 + 例如要实现 DotScreen 与 RGB Shift,只需创建对应效果节点并串联。 +
+ +
+import { pass } from 'three/tsl';
++ import { dotScreen } from 'three/addons/tsl/display/DotScreenNode.js';
++ import { rgbShift } from 'three/addons/tsl/display/RGBShiftNode.js';
+
+// 在你的初始化流程中
+
+const scenePass = pass( scene, camera );
+
++ const dotScreenPass = dotScreen( scenePass );
++ const rgbShiftPass = rgbShift( dotScreenPass );
+
+
+ + 完成后,把最终节点赋给 `RenderPipeline` 即可。 +
++renderPipeline.outputNode = rgbShiftPass; ++ +
+ 使用后处理时,色调映射与色彩空间转换会在效果链末尾自动执行。 + 但某些场景你可能希望完全控制执行时机与顺序。 + 例如使用 `FXAANode` 做 FXAA,或用 `Lut3DNode` 做调色时, + 可以关闭自动处理,并通过 `renderOutput()` 手动应用。 +
+ +
+import { pass, renderOutput } from 'three/tsl';
+import { fxaa } from 'three/addons/tsl/display/FXAANode.js';
+
+// 在你的初始化流程中
+
+const renderPipeline = new THREE.RenderPipeline( renderer );
+renderPipeline.outputColorTransform = false; // 禁用默认输出色彩变换
+
+const scenePass = pass( scene, camera );
+const outputPass = renderOutput( scenePass ); // 在这里应用色调映射和色彩空间转换
+
+// FXAA 必须在 sRGB 色彩空间中计算
+
+const fxaaPass = fxaa( outputPass );
+renderPipeline.outputNode = fxaaPass;
+
+
+ + `renderOutput()` 不是强制的,你也可以按需求自行实现色调映射与色彩空间转换。 +
+ ++ 新后处理栈内置 MRT,对高级效果非常关键。MRT 允许你在一次渲染 pass 中产生多个输出。 + 例如使用 TRAA 时,你可以按下述配置准备抗锯齿输入。 +
+ +
+import { pass, mrt, output, velocity } from 'three/tsl';
+
+// 在你的初始化流程中
+
+const scenePass = pass( scene, camera );
+scenePass.setMRT( mrt( {
+ output: output,
+ velocity: velocity
+} ) );
+
+ + 传给 `mrt()` 的配置对象用于描述该 pass 的各个输出。 + 本例中我们保存默认输出(场景主图)和速度信息,用于 TRAA。 + 如果还需要深度,一般无需额外作为 MRT 输出配置; + 在默认输出 pass 中按需请求即可获取。 + 若后续效果需要这些结果,可把它们作为纹理节点读取。 +
+ +
+import { traa } from 'three/addons/tsl/display/TRAANode.js';
+
+// 在你的初始化流程中
+
+const scenePassColor = scenePass.getTextureNode( 'output' );
+const scenePassDepth = scenePass.getTextureNode( 'depth' );
+const scenePassVelocity = scenePass.getTextureNode( 'velocity' );
+
+const traaPass = traa( scenePassColor, scenePassDepth, scenePassVelocity, camera );
+renderPipeline.outputNode = traaPass;
+
+
+ + MRT 配置取决于你的具体方案。你可以使用 `output`、`velocity`、`normalView`、 + `emissive` 等 TSL 对象,把片元数据写入不同 attachment。 + 在复杂 MRT 流程中,为提升性能并避免显存压力,必须做好数据打包与格式优化。 + 默认 attachment 精度是 RGBA16(Half-Float),并非所有数据都需要这么高。 + 例如下方把 `diffuseColor` 改为 RGBA8,可将带宽和内存占用减半。 +
+ ++const diffuseTexture = scenePass.getTexture( 'diffuseColor' ); +diffuseTexture.type = THREE.UnsignedByteType; ++ +
+ 下方 SSR(屏幕空间反射)示例把默认 FP16 法线转换为 RGBA8 颜色, + 并将金属度/粗糙度打包到单个 attachment。配合 `sample()` TSL 函数可实现自定义解包, + 本例中会把颜色还原为归一化方向向量。 +
+ +
+scenePass.setMRT( mrt( {
+ output: output,
+ normal: directionToColor( normalView ),
+ metalrough: vec2( metalness, roughness )
+} ) );
+
+// 使用 RGBA8 替代 RGBA16
+
+const normalTexture = scenePass.getTexture( 'normal' );
+normalTexture.type = THREE.UnsignedByteType;
+
+const metalRoughTexture = scenePass.getTexture( 'metalrough' );
+metalRoughTexture.type = THREE.UnsignedByteType;
+
+// 自定义解包。后续效果里请使用得到的 "sceneNormal"
+// 来替代 "scenePassNormal"
+
+const sceneNormal = sample( ( uv ) => {
+
+ return colorToDirection( scenePassNormal.sample( uv ) );
+
+} );
+
+
+ + 后续还会继续增强打包/解包能力,提供更多 MRT 数据组织方式。 + 目前建议参考 + 官方示例, + 了解现有效果和配置模式。 +
++ `WebGPURenderer` 是 three.js 的下一代渲染器。本文会简要介绍它的能力和基本使用方式。 +
+ ++ `WebGPURenderer` 被设计为 `WebGLRenderer` 的现代替代方案。 + 它优先使用 WebGPU(现代高性能图形与计算 API), + 同时也被设计成通用渲染器:若设备/浏览器不支持 WebGPU, + 会自动回退到 WebGL 2 后端。 +
++ 这个回退机制非常关键:应用可以在支持 WebGPU 的平台获得新能力, + 同时不牺牲仅支持 WebGL 2 设备的兼容性。 +
+ ++ 除了接入 WebGPU,`WebGPURenderer` 还提供了以下特性: +
+ +
+ `WebGPURenderer` 使用不同构建入口,因此导入方式需要调整: +
+ ++- import * as THREE from 'three'; ++ import * as THREE from 'three/webgpu'; ++ +
+ 如果你使用 import map,建议改成如下形式(路径按你的工程结构调整): +
+
+ <script type="importmap">
+ {
+ "imports": {
+ "three": "../build/three.webgpu.js",
+ "three/webgpu": "../build/three.webgpu.js",
+ "three/tsl": "../build/three.tsl.js",
+ "three/addons/": "./jsm/"
+ }
+ }
+ </script>
+
+
+ + 创建渲染器实例的方式和 `WebGLRenderer` 类似: +
+ +
+const renderer = new THREE.WebGPURenderer( { antialias: true } );
+renderer.setPixelRatio( window.devicePixelRatio );
+renderer.setSize( window.innerWidth, window.innerHeight );
+renderer.setAnimationLoop( render );
+document.body.appendChild( renderer.domElement );
+
+ + 需要注意,WebGPU 初始化是异步的。因此推荐使用 `setAnimationLoop()`, + 它能确保首次渲染前完成初始化。 + 如果你坚持使用 `window.requestAnimationFrame()` 或需要在初始化阶段直接使用渲染器, + 则要额外调用一行初始化代码。 +
+
+const renderer = new THREE.WebGPURenderer( { antialias: true } );
+renderer.setPixelRatio( window.devicePixelRatio );
+renderer.setSize( window.innerWidth, window.innerHeight );
+renderer.setAnimationLoop( render );
+document.body.appendChild( renderer.domElement );
+
++ await renderer.init();
+
+ + `WebGLRenderer` 中常见的方法(如 `clear()`、`setRenderTarget()`、`dispose()`) + 在 `WebGPURenderer` 中同样可用。完整接口请参考 + API 文档。 +
+ ++ 正如前文所述,`WebGPURenderer` 默认使用 WebGPU,必要时回退 WebGL 2。 + 如果你想在测试中强制 WebGL 2,或出于某些原因禁用 WebGPU, + 可以使用 `forceWebGL` 参数。 +
+
+- const renderer = new THREE.WebGPURenderer( { antialias: true } );
++ const renderer = new THREE.WebGPURenderer( { antialias: true, forceWebGL: true } );
+
+
+ + 准备迁移到 `WebGPURenderer` 时,需要注意以下几点: +
+ +虽然当前研发重点在 `WebGPURenderer`、节点材质和 TSL, + `WebGLRenderer` 仍在维护,且依然是纯 WebGL 2 应用的推荐选择。 + 但请注意,项目已不计划为 `WebGLRenderer` 增加大型新特性,这一点从最近的版本说明(release notes)中也可以明显看出。 + 同时我们也在评估为其加入有限的节点材质支持, + 以便某些项目更平滑地迁移到 `WebGPURenderer`。 +
+ +