尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

WebGPU与TSL实战:从Compute Shader到生产级3D可视化

WebGPU与TSL实战:从Compute Shader到生产级3D可视化 1. WebGPU 和 TSL社区这波热度到底从哪来1.1 WebGL 时代压了十几年的两个天花板先交代背景。Three.js 圈子里WebGL 统治浏览器 3D 十几年大家早就习惯了调调材质、摆摆场景、打一个灯光这套工作流。但这两年我明显感到越来越多老项目开始卡住不是美术不够好而是渲染性能摸到了天花板。这个天花板有两个具体表现。第一是 Draw Call。WebGL 的 API 继承自 OpenGL ES 2.0设计年代很早一个物体从绑定到绘制要做一堆状态切换三千个独立物体的场景就能把主线程压得很满。所以社区里的高级技巧几乎都在做同一件事用 InstancedMesh 合并实例、用 BufferGeometry 合并顶点、用纹理图集减少切换本质都是绕开 API 本身的限制。这些技巧很有效但写完之后自己看着代码都嫌啰嗦。第二是GPU 只能画不能算。WebGL 没有通用的计算着色器想做大规模粒子、流体、布料、软体物理只能把数据塞进纹理或者顶点缓冲用渲染管线顺路算一下。能算但极绕很多算法根本没法表达。这就导致浏览器里做实时物理模拟长期处于可以演示、很难落地的状态。WebGPU 正好把这两个问题都接住了。它新的 API 设计把状态切换成本压得很低同时提供了独立的 Compute Shader 管线GPU 终于可以先算后画而且画的数据可以直接来源于算的结果。社区里很多憋了很久的玩法百万粒子、流体、PBD 布料一下就出来了。TSL 在这个时间点出现又把怎么写 shader这件事从字符串时代拉到了函数式时代。两件事撞在一起这波热度自然起飞。1.2 TSL 革命的地方不在语法在写代码的方式很多朋友第一次看到 TSLThree.js Shading Language都会问这不就是把 GLSL 换成 JavaScript 风格吗语法不一样而已。我一开始也这么想实际用了一周之后看法变了TSL 真正改变的是着色器代码的生命周期。WebGL 时代最常见的写法是 ShaderMaterial 里贴一段字符串const material new THREE.ShaderMaterial({ vertexShader: varying vec3 vColor; void main() { vColor position; gl_Position projectionMatrix * modelViewMatrix * vec4(position, 1.0); } , fragmentShader: varying vec3 vColor; void main() { gl_FragColor vec4(vColor, 1.0); } , });这段代码的问题不在于 GLSL 难学而在于它是一段游离在工程结构之外的字符串。IDE 不会给你提示变量名拼错了要到运行时才报错跨版本升级时gl_Position和 Three.js 内部绑定的名字一变整段就要重写。最痛苦的是想在两个材质之间抽取公共函数你只能继续做字符串拼接。字符串拼接 shader在稍微正规一点的工程里几乎是灾难。TSL 的逻辑是把着色器程序当成普通 TypeScript 函数写返回值代表某个输出挂点import * as THREE from three/webgpu; import { Fn, positionLocal, sin, uv, vec4 } from three/tsl; const material new THREE.NodeMaterial(); material.positionNode Fn(() { return positionLocal; // 顶点位置 })(); material.colorNode Fn(() { return vec4(sin(uv().x * 10.0), uv().y, 0.0, 1.0); // 片元颜色 })();这里的每个数学运算都是一个节点对象Fn(() {...})()把函数体转成可复用的着色器图。编译器three/tsl 内部那套会把它编译成 WGSL 或者 GLSL所以同一份代码在 WebGPU 和 WebGL 后端都能跑。对工程来说收益是实打实的IDE 类型检查、单元测试、函数抽取、按模块拆分全都恢复了。这在我接手过的几个项目里是革命性的。注意TSL 在 Three.js r16x 版本迭代很快早期叫tslFn现在一般统一为Fn部分挂点名也会变。下面代码以我记录时的常见写法为例你上手前先看一眼当前版本迁移说明别直接复制旧教程到新版本。1.3 已有项目该不该现在换这个问题我每周都会被问。我的态度很明确新项目直接考虑 WebGPU TSL老项目按模块搬别搞周末全部重写这种事。WebGL 到现在还是最稳的浏览器 3D 方案普及率最高。如果你的产品是数据大屏、走版演示、稳定运营的 WebGL 系统现在强行切 WebGPU 可能给自己找一堆兼容性麻烦。反过来只要项目满足下面一个条件就值得开一块儿试验田需要上万级别的粒子、流体、布料WebGL 要绕路才能做的要写多个自定义材质并且打算把它们做成内部可复用组件主线程被 Draw Call 卡到没脾气用 Instance 合并已经救不回来全新项目、无历史包袱可以直接把three/webgpu作为默认入口。我个人的做法是先把渲染器换成 WebGPURenderer 跑通原有场景再挑一个高频页面/模块用 TSL 重写材质验证性能收益最后决定是不是全面铺开。这样风险可控也有一个能拿得出手的对比数据。2. 社区最火的 10 个 WebGPU TSL 演示案例逐帧拆解下面这十个是我在社区里高频见到的项目类型也都实际拉下来跑过或者改过。每个案例我会说三件事为什么大家会点开看、核心在炫什么技术、以及你能从中拿走什么。2.1 百万粒子系统第一个被 WebGPU 点爆的作品粒子永远是新技术的最佳广告。原因很朴素效果好肉眼可见工作量不大。WebGL 时代十万粒子就需要各种优化技巧到 WebGPU 这里思路完全变了——把粒子位置和速度放在 Storage Buffer 里用 Compute Shader 在 GPU 上做物理积分然后直接把这块存储区当顶点属性绘制整个过程 CPU 只参与了最初的数据初始化。我跑过的一个社区 demo200 万粒子在空间里做引力汇聚和相互排斥帧率稳在 60。这里的关键是把积分逻辑搬进了计算着色器而不是在 JS 里for循环更新位置。复现路径大概是这样的先创建一个存储缓冲属性把粒子的位置、速度、质量都注册成 Storage Buffer Attribute用一条计算节点做积分每帧调用renderer.compute()。从第二帧开始所有位置更新都在 GPU 里完成CPU 每帧只发一次 dispatch不读回任何数据。这种算完直接画的数据流模式是做 WebGPU 里所有高级效果的基础。2.2 实时水体从贴图欺骗到频谱模拟水体是另一个传播量很高的类型。WebGL 时代海洋效果基本两条路多层法线贴图滚动或者 CPU 端做简易 Gerstner 波。WebGPU 社区的经典方案是 FFT 海洋用计算着色器把海浪频谱做逆傅里叶变换生成高度图和法线图网格顶点位移和法线采样都来自这张图。这个方案在 GPU 里跑的每一步都依赖 compute频谱求解、IFFT、消散项处理。放到 TSL 里你写的不是一段完整的 WGSL而是一个返回高度偏移的函数然后把它挂到material.positionNode上法线是同一个函数求导之后的副产品可以挂到material.normalNode。一个很讨喜的细节是WebGPU 的 compute 和绘制可以共用同一块 Storage Buffer海浪高度图算完之后不需要拷贝一份给顶点着色器直接引用就行。这类跨管线资源复用在 WebGL 里几乎要写一堆 hack到 WebGPU 变成了基础语法。2.3 体积雾与云层Raymarch 终于可以放手写云和雾过去在 Web 端常用的做法是叠加一堆半透明粒子和片层用方向 透明度骗过眼睛。预算紧的时候没问题但相机角度稍微刁钻一点就会穿帮。WebGPU 的案例用的是 Raymarch 思路在片元着色器里从相机位置沿视线发射射线步进采样一个 3D 噪声纹理累积密度和颜色。表面看就是个循环但在 WebGL 里我们不太敢写长循环因为分支和循环的开销收不住。WebGPU 因为驱动层的整理分支和循环的相对开销变低了再加上 compute 可以做噪声纹理的预烘焙体积云这类效果在浏览器里第一次有了可实时交互的质感。TSL 的Fn非常适合封装 Raymarch 的步进逻辑你可以把采样噪声累积密度光照散射拆成三个函数像调库一样组合。这是社区里体积渲染类 demo 密度特别高的原因——还是那句话WebGPU 给了表达空间TSL 给了表达效率。2.4 实时反射与正方体摄像机效果正方体摄像机CubeCamera在 WebGL 时代就是做反射的标配工具在物体位置放一个 Camera向六个面各渲染一次场景生成立方体纹理再赋给物体做环境反射。WebGPU 社区里这个效果被反复拿来复刻因为大家对反射效果太熟悉了一眼就能看出渲染品质变化。核心代码其实是老面孔import * as THREE from three/webgpu; // 注意不同 Three.js 版本对立方体渲染目标命名有差异 // 老版本通常叫 WebGLCubeRenderTarget新版本留意 CubeRenderTarget按本地类型定义来选。 const cubeTarget new THREE.WebGLCubeRenderTarget(256); const cubeCamera new THREE.CubeCamera(0.1, 100, cubeTarget); scene.add(cubeCamera); // 每帧渲染前先把镜面物体藏起来避免反射到自身 reflectiveMesh.visible false; cubeCamera.update(renderer, scene); reflectiveMesh.visible true; reflectiveMesh.material.envMap cubeTarget.texture;在 WebGPU 下跑这个经典流程我第一次就踩了坑CubeCamera 的渲染目标大小、纹理 flipY 行为、深度缓冲格式都和 WebGL 时代不完全一致导致反射画面发暗或者上下颠倒。后面我在第 4 节单独展开这里先记住一个结论老 API 的流程能复用但纹理选项不能照抄得逐个核。2.5 PBR 材质天花板车漆、金属、次表面车漆、各向异性金属、清漆层Clearcoat这类效果在 WebGL 里要么靠 MeshPhysicalMaterial 现成参数要么靠某位大神把 BRDF 公式翻译成长字符串 GLSL。到 TSL 时代自定义 BRDF 变成了改材质图里的某个分支函数你可以直接改高光项、改清漆层衰减因子、改次表面散射的半径。社区里最出圈的一类是虚拟展厅一辆汽车放在旋转台上周围是 HDR 环境光车身反射环境贴图清漆层负责那层高亮的反光。单独看这种 demo 的技术含量不如粒子系统但它胜在用户刚需。电商、广告、汽车官网都需要这种 Web 端 3D 展示所以传播量一直很高。这部分最值得抄的不是某个材质参数而是材质组件化的思路把车身漆面、玻璃、轮胎橡胶分别封装成 TSL 函数在项目里像积木一样拼。这比我过去维护一大段 GLSL 舒服了不止一个量级。2.6 程序化地形与 GPU 植被实例化地形生成在 WebGL 时代也是大热门但要实时改变地形CPU 要不停更新顶点缓冲。WebGPU 社区的地形 demo 通常把高度图生成放进 compute网格顶点采样噪声函数再叠加侵蚀算法植被部分则是把草的每一个叶片当成一个实例位置和朝向全部由 compute 按地形高度生成。复现的关键点在instanceIndex。在计算着色器里每个线程负责一个实例通过索引号加上噪声公式算出它长在哪里、多高、往哪歪再配上 LOD细节层次远处植被自动降级为简版模型。这样做的好处是植被数量从几千提到几十万CPU 依然零负担。如果你对GPU 驱动场景这个概念一直有点模糊这个案例是最好的入门教材。2.7 音频可视化FFT 数据直通着色器音频可视化常年是创意编程社区的最爱WebGPU 版本的玩法也没变用 Web Audio API 的 AnalyserNode 拿到频域数据上传给着色器然后由 TSL 节点把低频、中频、高频映射成几何波动、粒子爆发或颜色渐变。这个案例的教学价值很高因为它同时用到了 uniform 上传和 compute 处理两条链路。频域数据从 CPU 传到 GPUTypedArray 可以直接传给 uniform如果你要做更复杂的响应比如按频谱分频段驱动不同粒子群再用 compute 做一次数据整理。TSL 里定义一个 uniform 数组再配合instanceIndex取值这段逻辑非常直观。我的体感是音频可视化特别适合作为 TSL 练手项目输入数据现成、反馈即时、做出来又很适合拿去炫。2.8 布料与柔体PBD 约束求解Position Based DynamicsPBD在 WebGL 时代就有 CPU 版本但仿真规模撑不起来。社区里的 WebGPU 版本把 PBD 的三步——位置预测、碰撞约束、速度更新——全部写进 compute。布料逐顶点做约束求解一张几千顶点的布料可以实时挂在交互手柄上摆动这在 WebGL 里做CPU 会非常吃力。它让我真正理解了一句话compute 不是用来画东西的是用来算东西的。算完的结果可以画成布料也可以拿去做场景物体的位置动画。只要你脑海里把Draw Call和Compute Dispatch拆成两条独立的调度线整个 WebGPU 的思维就建立起来了。后面你再去看粒子、水体、地形会发现全都是一套计算模式在不同业务场景里的重复。2.9 程序化城市与建筑生长动画从一片空地到一片发光城市这类视频在社区传播量极高因为它有很强的叙事性。实现上并不复杂每栋建筑是一个挤出体高度由噪声决定生长动画由时间变量控制挤出顶点的高度。TSL 里这个玩法尤其简单把positionLocal.y乘以一个由time和instanceIndex共同决定的生长因子即可。这个案例对我的启示是WebGPU 不只是堆性能很多特效在 TSL 里做起来顺手会直接改变创意实现的意愿。过去想到一个效果先要盘算这段 GLSL 怎么写、跨版本会不会崩现在写起来像普通业务代码创意的摩擦成本低了。程序化城市这类项目也特别适合拿来给团队做 TSL 入门培训代码量不大效果足够震撼能快速建立信心。2.10 后处理全家桶泛光、景深、动态模糊的 TSL 化最后一个类型是后处理。WebGL 时代大家习惯用 EffectComposer 加一堆现成 Pass。到 WebGPU 时代社区开始用 TSL 自己组装后处理节点原因无非还是复用和类型安全。泛光Bloom可以从亮度提取 高斯模糊 合成三个小函数开始动态模糊用上一帧的深度和投影矩阵反推像素运动。这类 demo 不一定视觉效果最炸但它最接近生产可复用。我见到的不少正式项目最后都会沉淀出一套自己的后处理管线TSL 化之后这套管线可以作为 npm 包在多个项目间共享这是传统字符串 GLSL 办不到的。如果你所在团队做可视化产品我建议从后处理入手做第一波 TSL 技术储备性价比很高。3. 十个案例背后共同的三个技术底座案例虽然类型不同但拆到底你会发现它们全都在用同一套底层能力。理解了这三个底座看任何 WebGPU/TSL 项目的源码都不会再晕。3.1 Compute Shader把循环搬进 GPU传统渲染管线的思路是顶点进、像素出每次绘制是一个固定的处理流程。Compute Shader 则是完全通用的一组并行计算任务你规定每个线程做什么GPU 会开成千上万个线程同时执行。它和 CPU 循环最大的差别不是算得快而是并发数量大而且计算过程不需要和绘制绑定。在 Three.js 里你甚至不需要手写一行 WGSL。定义一个 TSL 函数作为计算内核把它传给compute()方法每帧调用renderer.compute(computeNode)执行。函数里通过instanceIndex获取当前线程编号读取存储缓冲里的数据、做更新、写回。数据就在原地更新不需要离开 GPU。这个模式放到业务里最常见的就是动画系统数据在 GPU 里流动位置、速度、生命周期这些状态不再每帧从 GPU 读回 CPU而是持续留在显存里被 compute 迭代。一旦你想通这一点WebGPU 项目的架构思路基本就过关了。3.2 Storage Buffer 与资源复用共享、序列化到底指什么社区热搜里有three.js 共享 序列化很多人不理解。其实在我们做项目时绕不开两类问题资源复用和跨线程/跨页面传输。先说资源复用。同一场景里很多物体可以共享同一份几何体BufferGeometry和纹理Texture。Three.js 的 Material 也可以多个 Mesh 共用。这里有个容易踩的雷如果你用复制 geometry.array 再改的方式给不同物体差异数据因为共享引用改一个会全部变化。正确的做法是先geometry.toNonIndexed()或者把 attribute 的 array 再拷贝一份确保每个物体拥有独立的array引用。再说序列化。保存场景、跨页面恢复、Web Worker 里加载模型都需要把数据打包带走。Three.js 场景可以直接scene.toJSON()模型的常用格式是 GLTF/GLB二进制体积小加载快。但注意GPU 侧的 GPUBuffer、GPUTexture 这类资源是不能被直接序列化的它活在显存里结构上属于 GPU 上下文。你只能把原始数据ArrayBuffer 或 TypedArray从 CPU 侧序列化出去到新环境重新上传。所以做多场景共享时我更建议共享数据源而不是共享GPU 资源维护一份 ArrayBuffer 作为顶点数据源头每个场景各自上传自己的 GPU Buffer。如果用了 Worker OffscreenCanvas跨线程传的就是 Transferable 的 ArrayBuffer浏览器零拷贝转移不要用结构化克隆去传一个大数组那是双重拷贝卡到怀疑人生。3.3 Node 材质材质就是一张有向无环图TSL 表面上是语法糖底层其实是节点图。节点图做过设计软件的都不陌生把几个输入节点接进一个数学节点再接到输出节点中间每一步都可以被复用和修改。TSL 的函数语法是人类友好的节点图编辑器。Three.js 的 NodeMaterial 提供了一堆挂点colorNode片元颜色、positionNode顶点位置、normalNode法线、diffuseNode漫反射、emissiveNode自发光等。普通 MeshStandardMaterial 本质也是一棵树只是用参数化界面封装了。所以你在 TSL 里改一个material.colorNode等价于把标准材质某个环节替换成自己的子图其余部分还是走引擎默认逻辑。这也是我坚持推荐 TSL 给业务项目的原因你不用从零写一个完整材质只需要在标准材质上打补丁式地改一个点。生产系统最怕的就是从 WebGL 字符串蔓延到全项目的不可控魔法代码节点图把这种魔法变成了可检查、可测试的工程结构。4. 我把三个热搜效果搬进 WebGPU 管线的完整记录光看别人 demo 不过瘾我自己动手把它们搬到 WebGPU 管线里跑通过程不算顺利但踩过的坑都值得说。4.1 正方体摄像机CubeCamera反射效果实战场景很简单一个亮面地面 一个金属球球体要反射周围场景。动手前我以为换皮肤就行实际第一步就卡住了。WebGPURenderer 的初始化必须先 awaitimport * as THREE from three/webgpu; const renderer new THREE.WebGPURenderer({ antialias: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); await renderer.init(); // 关键初始化是异步的再创建 CubeCamera这里需要特别检查渲染目标格式const cubeTarget new THREE.WebGLCubeRenderTarget(256, { type: THREE.HalfFloatType, // 用半浮点减小带宽颜色精度也够 }); const cubeCamera new THREE.CubeCamera(0.1, 100, cubeTarget); scene.add(cubeCamera);动画循环里注意顺序function animate() { requestAnimationFrame(animate); // 先隐藏被反射物体避免反射到自己 reflectiveMesh.visible false; cubeCamera.update(renderer, scene); reflectiveMesh.visible true; renderer.render(scene, camera); }跑起来之后遇到两个很典型的坑反射图像发暗原因大概率是渲染目标 colorSpace 或纹理flipY设置和 WebGL 默认值不同解决方法是创建 target 时手动检查texture.colorSpace、texture.flipY和目标纹理的后处理设置对齐。反射画面像贴纸一样跟着摄像机动检查 CubeCamera 的 position 是否每帧都紧跟被反射物体我一开始只设置了一次物体一移动反射就穿帮。提示CubeCamera 本质是每帧渲染 6 个面开销等于给场景多渲染几次。场景很重的话建议降低分辨率128 或 256并隔帧更新视觉上差别不大性能立刻好一截。4.2 雨、雪、雾三件套天气系统的 TSL 写法three.js雨雪雾怎么实现在搜索里一直很热。核心思路其实不复杂老 WebGL 项目里我用 Particles 加自定义顶点动画搞定到 TSL 里同样可以做而且代码更干净。雾最简单用现成的 FogExp2 就行scene.fog new THREE.FogExp2(0x9db6c0, 0.018);雨和雪的核心是粒子系统。雨滴用多个细长的线状粒子表达雪用圆点加飘落偏移。用 TSL 控制顶点动画import { Fn, uniform, positionLocal, instanceIndex, time, vec3 } from three/tsl; const material new THREE.NodeMaterial(); material.positionNode Fn(() { // 雨滴的垂直下落与水平风偏移 const offsetY positionLocal.y.add(time.mul(30.0)); const offsetX positionLocal.x.add(time.mul(1.5)); return vec3( offsetX.mod(200.0).sub(100.0), offsetY.mod(200.0).sub(100.0), positionLocal.z ); })();雪片则在 y 方向下落的同时在 x、z 方向加一个正弦摆偏移每个粒子的相位可以用instanceIndex生成一个伪随机数import { sin } from three/tsl; material.positionNode Fn(() { const t time.mul(2.0).add(instanceIndex.toFloat().mul(0.01)); const swayX sin(t).mul(1.5); const swayZ sin(t.mul(1.3)).mul(1.0); const fallY positionLocal.y.sub(time.mul(4.0)); return vec3(positionLocal.x.add(swayX), fallY, positionLocal.z.add(swayZ)); })();这段代码跑起来一万片雪花也只是一个大 Points 的绘制调用。注意几个细节粒子最好复用同一缓冲重生用取模mod完成材质要开 transparent 和 depthWrite: false否则雨滴会把雪地糊成一片如果粒子数量超过十万别再在 CPU 每帧写位置直接上 compute 方案。4.3 WebGL 项目迁移 WebGPU 的注意清单迁移不是换一个渲染器类那么简单下面是列好的清单每一条都是我或身边朋友实际踩过的事项WebGL 时代WebGPU 时代注意点导入路径threethree/webgpu混着导入会导致两个渲染器体系并存内存和类型都会乱渲染器WebGLRendererWebGPURenderer初始化是异步的必须await renderer.init()自定义材质ShaderMaterialNodeMaterialGLSL 字符串不再直接用要转 TSL 节点阴影PCF 为主支持 PCF/VSM 等阴影参数兼容但表现有差异需逐个验证纹理flipY 默认 true默认值有差异上传纹理前确认 flipY/colorSpace抗锯齿看显卡默认 MSAA 可选用antialias参数或手动 setMSAA还有一个最容易被忽略的点兼容性检测。上线前必须写一段兜底逻辑async function checkGPU() { if (!(gpu in navigator)) { return fallback; // 当前浏览器不支持 WebGPU } try { const adapter await navigator.gpu.requestAdapter(); return adapter ? webgpu : fallback; } catch (e) { return fallback; } }我的经验是把 WebGL 渲染器保留一套作为 WebGPU 不支持时的保底路径。两套渲染逻辑在业务层尽量抽象成同一调用比如renderer.render(scene, camera)语义一致页面在启动时按检测结果选择。虽然维护两套听着麻烦但实际改动面很小换来的是产品稳定性。5. 生产级落地Vue3 Three.js TypeScript 的机房可视化聊完炫的聊点更接地气的。很多团队做机房/数据中心可视化技术栈选 Vue3 TypeScript 的概率很高这块也是社区热搜里一直有的话题。我以一个实际做过的项目为例说说 WebGPU/TSL 到来之前这套架构该怎么组织以及它未来可以怎么演化。5.1 为什么选 Vue3 TS 而不是纯 JS 页面机房可视化本质是一个长期维护的业务系统不是一次性演示页所以工程性比花哨更重要。Vue3 的响应式系统和组件化非常契合选中设备、弹出面板、更新状态这类交互TypeScript 则保住了 Three.js 场景对象Mesh、Camera、Light的类型边界。我的目录结构大致是这样src/ views/ // 页面组件一块一块的场景挂在组件里 components/ // UI 组件设备面板、告警列表 three/ // Three.js 场景封装 scene.ts // 初始化 renderer、camera、scene models.ts // 模型加载和实例化 interaction.ts// 射线拾取、鼠标交互 data.ts // 接收 WebSocket 数据并驱动场景 store/ // Pinia保存选中设备、筛选条件、告警状态关键点是场景不写在组件里。Three.js 的 renderer 实例、动画循环、事件监听都绑定在组件声明周期上直接在 Vue 组件里new THREE.Scene()很容易在组件销毁时忘了释放资源。我把它们都封装成类组件只负责创建实例和调用更新方法。这一点对新手来说特别重要很多人第一个坑就是页面切换几次后浏览器内存暴涨。5.2 机房场景的建模、加载与实例化机房场景我建议分三类资源处理建筑外壳、装饰物用 Blender 建模导出 GLB走 DRACOLoader 压缩加载一次。机柜、服务器、空调数量大、重复多用 InstancedMesh。一个机柜是几百个部件但整个机房两百个机柜可以做成一份几何体 两百组实例矩阵Draw Call 从几百降到一两条。灯带、指示点用 Points 或 Sprite一张小贴图反复实例。实例化机柜的代码骨架import { InstancedMesh, Matrix4, Object3D } from three; const rackGroup new Object3D(); const matrix new Matrix4(); // 在每个机柜位置摆一个实例 for (let i 0; i rackCount; i) { matrix.makeTranslation(xArr[i], yArr[i], zArr[i]); rackGroup.matrixWorld.copy(matrix); mesh.setMatrixAt(i, rackGroup.matrixWorld); } mesh.instanceMatrix.needsUpdate true;这段代码很多人第一次看会奇怪为什么要临时组一个 Object3D 再取 matrixWorld因为 InstancedMesh 的矩阵是相对世界坐标的直接传一个临时矩阵容易出偏移。我吃过这个亏后来养成了先构造 Matrix4再显式复制进实例矩阵槽位的习惯并确保每个实例的世界矩阵计算一致。5.3 拾取、联动与实时数据面板机房可视化的交互核心是鼠标移到机柜上高亮点击弹出设备详情左侧表格点某一行场景里对应机柜闪烁。这里有一条不能省的流程raycaster 拾取。import { Raycaster, Vector2 } from three; const raycaster new Raycaster(); const pointer new Vector2(); function onPointerMove(event) { pointer.x (event.clientX / window.innerWidth) * 2 - 1; pointer.y -(event.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(pointer, camera); const hits raycaster.intersectObject(rackInstances); // 高亮命中的实例用 instanceColor 做染色 }高亮用 InstancedMesh 的 instanceColor 很顺手给每个实例一个颜色属性命中时把颜色改成高亮黄离开时还原。实时数据温度、功耗、告警从 WebSocket 推过来的时候我只更新一个 Int32Array 或 Float32Array再把这个数组映射成 instanceColor 或者某个 scale 参数完全不重建场景。现场经验是数据驱动的动画机柜发热变红、空调功率调大叶片转动要尽量用改实例属性和改颜色来解决不要动辄重建 Mesh 或重新加载模型。机房场景在规模化之后全是性能的持久战能省一次 draw call 就省一次。5.4 这套架构到 WebGPU 会怎么演化我对这套技术栈的判断是先把底层的 WebGL 依赖从业务代码里剥离开WebGPU 成熟后逐步替换底层渲染器。业务层的组件、Pinia、拾取逻辑基本不用动因为 Three.js 的语义Scene、Mesh、Raycaster、InstancedMesh在 WebGPU 后端保持一致。能明显受益的是两个模块大规模状态可视化几百个柜子的温度场如果用 TSL compute每个机柜的温度粒子可以实时演算散热动画不再依赖 CPU 每帧改几十个关节位置。告警扩散效果按机柜位置生成脉冲光效果原来是逐个材质改 emissive换成 TSL 后可以用一个全局脉冲函数挂到全部实例上代码量少一半。6. 现场翻车记录7 天跑通 TSL 后踩过的坑与排查思路这一节是实操里最想分享的部分。社区教程永远写得顺但真实环境里报错才是常态。6.1 TSL 编译报错的高频场景第一类Fn(() {...})()外层少了括号。TSL 里函数体转节点需要一个求值动作Fn(() {...})只是定义了函数()才是把它变成节点。少一个括号挂到 colorNode 上的就是一个函数对象而不是节点对象。第二类向量和标量混算。GLSL 里vec3 float有自动转换TSL 的类型要求严格得多vec3.add(float)有时需要手动float()包一层或者用.toVec4(1.0)补一位。报错信息往往很长但开头几个关键词就是缺少 cast 的地方。第三类混着导入three和three/webgpu。一旦项目里同时存在两个 Three.js 体系单例判断就会出各种诡异现象比如renderer.isWebGPURenderer是 undefined排查起来非常痛苦。迁移阶段一定要全项目统一一个入口。6.2 WebGPU 兼容性陷阱我在一个客户现场遇到的情况是开发机 Chrome 跑得好好的测试机的环境不同直接白屏。排查下来就是 WebGPU 不可用或者底层图形栈不支持。这类问题的第一道防线就是特性检测前面给的checkGPU兜底代码一定要在生产环境加上。浏览器支持矩阵建议翻 caniuse 或浏览器官方说明确认我的体感是 Chromium 系最稳其他浏览器的支持节奏不一样。线上项目想稳妥就做双后端降级别赌用户环境。另外await renderer.init()这个异步初始化千万不能漏。漏掉的话第一帧画面可能是黑屏而且在低端显卡上会表现为时好时坏必须在启动逻辑里同步处理。6.3 性能排查瓶颈在 CPU 还是 GPUWebGPU 项目性能出问题先别急着开骂。我用一个老土但有效的办法定位先看renderer.info里的 object 数和 draw call 数量。如果 Draw Call 高、切换频繁瓶颈在 CPU 调用如果 Draw Call 不多但帧率低大概率是 GPU 侧过载先降pixelRatio、降纹理尺寸、降低后处理步骤。再打开浏览器 DevTools 的 Performance 面板看主线程是否有大块 JS 开销。如果 JS 每帧只做几十毫秒的更新但帧数还是上不去那多半是 GPU 渲染超时重点检查着色器复杂度。我见过一个项目粒子数量只有 20 万但初始化时忘了给存储缓冲做对齐处理导致每帧 CPU 都要做一次重排性能直接减半这种问题只有看 profile 才能发现。提示排查 TSL 相关渲染异常时先把material.colorNode换成最简单的常量返回验证管线是否通。如果常量颜色能显示再逐步把噪声、采样这些环节加回来二分定位问题。这一条是拯救我无数次翻车的万能套路。最后再分享一个我个人的习惯写 TSL 前先画数据流图不用搞得很复杂纸上写下谁产生数据、谁消耗数据、数据从哪个 buffer 来、最终挂到哪个 node比直接写代码容易发现问题得多。WebGPU 项目里绝大多数 bug 不是语法问题而是数据流的持有关系搞错了。这个习惯从 WebGL 时代带到现在一直很好用。
返回列表