【three.js教程】Three.js 运行时更新:矩阵、几何体、材质怎么改才不卡

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 状态相关的参数(depthTestblendingpolygonOffset 等)也能随时改。

但有这么一批属性,一旦材质被渲染过一次,就不容易在运行时改了:

  • 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 记得重算投影矩阵

相机的位置和朝向是自动更新的。但如果你改了下面这些参数:

  • fov
  • aspect
  • near
  • far

就必须重算投影矩阵,否则画面不会按新参数变化:

js 复制代码
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

这是个高频坑:窗口 resize 时改了 camera.aspect 却忘了 updateProjectionMatrix(),结果画面拉伸或不变。Resize 处理里这两行是固定搭档。

六、InstancedMesh:改实例后要重算包围体

InstancedMesh 用于便捷地做实例化渲染。视锥剔除、射线拾取等特性依赖最新的包围体(包围球/包围盒)。由于它的机制特殊,类自己带有 boundingBoxboundingSphere,优先级高于几何体层级的包围体。

和你改几何体一样:每当底层数据变了(比如用 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 类自带包围体,覆盖几何体层级

延伸阅读

相关推荐
默_笙1 小时前
🌃 HTTP 不认识你:JWT 登录鉴权的完整"酒店入住"指南
前端·javascript
奋斗吧程序媛1 小时前
CSV文件导入功能
开发语言·javascript·ecmascript
梨想橙汁2 小时前
JS 核心语法:运算符流程控制、函数、数组与对象实战详解
前端·javascript
七牛开发者2 小时前
拆解 dsh:Turn 与 Step 如何组织 Agent 主循环
前端·javascript·人工智能
空想兔2 小时前
前端网络请求的前世今生:从 Ajax、XHR 到 Fetch 和 Axios
javascript
梨想橙汁2 小时前
JavaScript 零基础入门:引入方式、变量与数据类型,吃透原始与引用类型
前端·javascript
WebInfra2 小时前
Rslib 1.0 正式发布:面向多场景的 JavaScript 库开发工具
前端·javascript·github
汉堡大王95273 小时前
面试官:讲讲归并排序 —— 为什么它能在面试里反复出现?
前端·javascript·面试
kyriewen3 小时前
别再只给人写页面了:AI 已经开始自己点你的按钮、填你的表单
前端·javascript·人工智能