Three.js 运行时更新:矩阵、几何体、材质怎么改才不卡
原文出处:Three.js Manual -- How to update Things
本文基于官方手册「How to update Things」章节整理,用通俗方式带你看懂:对象加进场景后矩阵怎么刷新、改几何体顶点要不要重建、材质哪些能随便改、相机改了参数为什么画面没变------把「更新」这件事一次讲清。
写在前面
写 Three.js 应用,大部分时间都在「动态地改东西」:让物体动起来、随鼠标调整形状、运行时换贴图、玩家靠近时改相机视野......但这些更新不是「改了属性就完事」,不同对象有不同的更新规则。改错了,要么画面不刷新,要么悄悄掉帧。这一篇按对象类型逐一拆解:矩阵、几何体、材质、贴图、相机、InstancedMesh、SkinnedMesh。
一、变换矩阵:默认自动更新,静态对象可手动优化
所有对象一旦被加进场景,它的矩阵默认就会自动更新:
js
const object = new THREE.Object3D();
scene.add( object );
如果它是另一个已被加入场景的对象的子节点,同样会自动更新:
js
const object1 = new THREE.Object3D();
const object2 = new THREE.Object3D();
object1.add( object2 );
scene.add( object1 ); // object1 和 object2 的矩阵都会自动更新
但如果你确定某个对象永远不会动(纯静态),可以把自动更新关掉,只在需要时手动刷新一次,省下每帧的矩阵计算:
js
object.matrixAutoUpdate = false;
object.updateMatrix();
划重点:关掉
matrixAutoUpdate后,你改了position/rotation/scale不会自动反映到矩阵上,必须自己再调一次updateMatrix()。别关了又忘了手动刷。
二、BufferGeometry:缓冲区不能改大小,只能改内容
BufferGeometry 把顶点位置、索引、法线、颜色、UV 等数据存在 typed arrays(类型化数组) 里,比老式 Geometry 更快,但用起来更讲究。
最关键的一条:你不能改变缓冲区的大小。 重新分配大小的开销,基本等同于新建一个几何体。你只能更新缓冲区里已有的内容。
这意味着:如果你预感到某个属性(比如顶点数)会增长,就必须预先分配一个够大的缓冲区 。当然,这也意味着你的几何体有个上限------不存在能无限高效扩张的 BufferGeometry。
看一个「运行时不断变长的线」的例子。先预分配 500 个顶点的空间,但一开始只画前 2 个点,靠 BufferGeometry.drawRange 控制绘制数量:
js
const MAX_POINTS = 500;
// 几何体
const geometry = new THREE.BufferGeometry();
// 属性:每个点 3 个 float(x, y, z)
const positions = new Float32Array( MAX_POINTS * 3 );
geometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
// 绘制范围:只画前 2 个点
const drawCount = 2;
geometry.setDrawRange( 0, drawCount );
// 材质与线
const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
const line = new THREE.Line( geometry, material );
scene.add( line );
之后想往线上加点的套路是这样:
js
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;
}
如果渲染之后想改变绘制点的数量 ,改 drawRange 即可:
js
line.geometry.setDrawRange( 0, newValue );
如果想渲染之后修改位置数据的值,必须打上更新标记,否则 GPU 端的缓冲区不会刷新:
js
positionAttribute.needsUpdate = true; // 首次渲染后修改必须设置
⚠️ 改完位置数据后,通常还要重算包围体,否则视锥剔除(frustum culling)、辅助工具等会算错:
js
line.geometry.computeBoundingBox();
line.geometry.computeBoundingSphere();
官方还提供了一个可拖来改去演示动态线的 fiddle(jsfiddle.net/t4m85pLr/1),可以照着改。
三、Materials:哪些能随便改,哪些要重建着色器
材质的 uniform 值 (颜色、贴图、透明度等)可以随便改 ,每帧都会发给着色器;跟 GL 状态相关的参数(depthTest、blending、polygonOffset 等)也能随时改。
但有这么一批属性,一旦材质被渲染过一次,就不容易在运行时改了:
- uniforms 的数量和类型;
- 以下特性的「有无」:
- texture(贴图)
- fog(雾)
- vertex colors(顶点颜色)
- morphing(形变)
- shadow map(阴影贴图)
- alpha test(透明度测试)
- transparent(透明)
改这些会触发着色器程序重建,需要手动打标记:
js
material.needsUpdate = true;
性能提醒:这会相当慢,可能造成掉帧------尤其 Windows 上(DirectX 的 shader 编译比 OpenGL 慢)。平滑一点的做法是「用假值模拟」:比如零强度的灯光、白色贴图、零浓度的雾,先用着,避免运行时硬切 shader。
另外,你可以自由更换几何体块(chunk)用的材质,但不能改变物体按面材质被切成几块的方式。如果运行时需要多种材质配置:数量少就提前切好(比如人的头发/脸/身体/上衣/裤子),数量大(每个面都可能不同)就考虑用 attribute 或贴图来驱动每面的差异。
四、Textures:改了内容要打 needsUpdate
图片、canvas、视频、数据贴图,改了之后需要打上更新标记:
js
texture.needsUpdate = true;
例外:渲染目标(Render Target)会自己自动更新,不用管。
五、Cameras:改了 fov/aspect 记得重算投影矩阵
相机的位置和朝向是自动更新的。但如果你改了下面这些参数:
fovaspectnearfar
就必须重算投影矩阵,否则画面不会按新参数变化:
js
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
这是个高频坑:窗口 resize 时改了
camera.aspect却忘了updateProjectionMatrix(),结果画面拉伸或不变。Resize 处理里这两行是固定搭档。
六、InstancedMesh:改实例后要重算包围体
InstancedMesh 用于便捷地做实例化渲染。视锥剔除、射线拾取等特性依赖最新的包围体(包围球/包围盒)。由于它的机制特殊,类自己带有 boundingBox 和 boundingSphere,优先级高于几何体层级的包围体。
和你改几何体一样:每当底层数据变了(比如用 setMatrixAt() 变换实例),就要重算:
js
instancedMesh.computeBoundingBox();
instancedMesh.computeBoundingSphere();
七、SkinnedMesh:同理
SkinnedMesh 在包围体这件事上和 InstancedMesh 一个道理------类自己有 boundingBox / boundingSphere 来正确包住动画中的网格。调用 computeBoundingBox() / computeBoundingSphere() 时,会基于**当前的骨骼变换(即当前动画状态)**来算。
总结
一张表记住各类对象的更新规则:
| 对象 | 更新方式 | 关键注意 |
|---|---|---|
| 变换矩阵 | 默认自动(matrixAutoUpdate) |
静态对象可关掉手动 updateMatrix() |
| BufferGeometry 内容 | 改 typed array 后设 needsUpdate |
缓冲区不能改大小,需预分配;改位置后重算包围体 |
| 绘制数量 | setDrawRange(0, n) |
配合预分配缓冲使用 |
| Material uniform/GL 状态 | 随意改 | 改 texture/fog 等特性需 material.needsUpdate = true(重建 shader,慢) |
| Texture | 改后设 texture.needsUpdate = true |
RenderTarget 自动更新 |
| Camera fov/aspect/near/far | 改后 updateProjectionMatrix() |
一起改漏了就画面不对 |
| InstancedMesh / SkinnedMesh | 改数据后 computeBoundingBox/Sphere |
类自带包围体,覆盖几何体层级 |