ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Three.js材质进阶:从PBR参数到ShaderMaterial自定义实践

Three.js材质进阶:从PBR参数到ShaderMaterial自定义实践 入坑Three.js一年多的时候我接到一个任务把一套机器人模型调成可以用来做产品宣传的程度。我当时觉得很简单不就是贴图、调颜色嘛结果折腾了大半个月渲染出来还是一股子塑料味。后来我慢慢搞清楚材质这事不是给模型刷个颜色而是你写下的每一个参数最终都会变成GPU里一段着色器运算的输入。这篇博客我不打算复读官方文档的参数表而是把Three.js材质这条线从头捋一遍先讲清楚它到底是什么再讲怎么从内置材质一路进阶到自定义ShaderMaterial最后聊聊真实项目里性能、工程和资源管理的坑。适合已经会搭场景、却总是被渲染效果劝退的Three.js开发者。1. 从填色工具到着色器封装先建立材质心智模型1.1 你在操作的不是材质是着色器的配置入口我知道很多人第一次接触Three.js是从MeshBasicMaterial开始的颜色一贴上去立刻变了于是想当然地认为材质是一个填色对象。绕到GPU这一侧看Three.js的材质其实是一份给WebGL的着色器方案渲染器拿到材质对象根据它的类型判断该用哪些attribute和uniform再拼出一套顶点着色器和片元着色器最后交给GPU执行。你改一个roughness改的并不是某个孤立数字而是片元着色器里PBR方程的一个输入系数。你换一张normalMap本质是改变了片元着色器里法线干扰的计算逻辑。理解这一层你才不会在材质效果不对的时候一头雾水——很多看起来像是bug的问题其实是着色器分支和数据格式出了岔子。到了进阶阶段你会发现一切高级效果最终都会走向同一个方向要么直接使用ShaderMaterial自定义着色逻辑要么通过onBeforeCompile去改造内置材质。前者适合做火焰、流体、描边这种完全非真实感的特效后者适合那些你在内置材质上摸得到却改不透的定制需求。所以我的建议是别急着背内置材质的属性名先把材质是着色器的配置入口这个心智模型刻在脑子里后面所有排查思路都会顺很多。1.2 内置材质六兄弟选型其实很有讲究Three.js内置材质按渲染能力排序基本就是一条从几乎不要钱到画质好但性能吃紧的效果曲线。很多人一上来就直接上MeshPhysicalMaterial还以为参数越多越高级结果项目里全是它帧率掉了30帧还不明白怎么回事。实际情况是90%以上的静态模型用MeshStandardMaterial就够只有透明折射、清漆、布艺反射这些特殊诉求才需要升级到MeshPhysicalMaterial。材质受光高光/PBR性能典型场景MeshBasicMaterial否无极高纯色调试、线框、天空球、不需要光照的界面元素MeshLambertMaterial漫反射无高低模演示、移动端极限优化MeshPhongMaterial漫反射高光高光中旧项目、简单塑料/玻璃质感MeshStandardMaterialPBRmetalness/roughness中低通用材质适合绝大多数模型MeshPhysicalMaterialPBR增强clearcoat/transmission/sheen低车漆、镜片、布料、水面ShaderMaterial全自定义全自定义取决于shader写法火焰、流光、特殊水面、非真实渲染我见过有同事把几十个不同零件的材质全部替换成MeshPhysicalMaterial只为了贪一个sheen参数结果移动端直接卡成PPT。材质选型的第一原则是够用就好先想清楚这个物体在场景里的视觉地位再决定要不要上高级材质。背景板用BasicMaterial普通道具用StandardMaterial主角且需要高光学细节的局部再用PhysicalMaterial这套分配方案在绝大多数项目里都很稳。1.3 如果你懂UE5可以顺手把概念翻译过来不少前端同学对ue5材质的印象停留在蓝色节点连线上觉得那是另一个世界的知识。实际上UE5的材质编辑器是可视化着色器编辑器和Three.js的声明式材质描述是同一套底层逻辑UE5里的BaseColor对应Three.js的map或colorMetallic对应metalnessRoughness对应roughnessNormal对应normalMapEmissive Color对应emissive。这个映射关系很有用。你在Substance Painter里烘焙好的PBR贴图从Blender导出到glTF再到Three.js贴图资源本身可以原封不动搬过来思路几乎不用改。真正要注意的是色彩空间颜色贴图要在新版本Three.js里设置为texture.colorSpace THREE.SRGBColorSpace而金属度、粗糙度、法线、AO这类数据贴图要保持NoColorSpace。这个细节我放在后面的问题排查章节再展开但你现在就要有个印象——很多为什么颜色发灰的问题根源根本不是材质参数而是色彩空间没有配对。2. 材质背后的三条线光照、UV与法线少一条都翻车2.1 没有光材质就是一块黑布我之前把一只机器人的glTF模型拖进页面结果全黑第一反应是代码写错了。后来翻场景发现里面根本没有加任何灯光。MeshBasicMaterial是唯一完全不吃光照的材质其他受光材质都需要场景里有光源否则就是一块黑坨。MeshStandardMaterial这类PBR材质对光照的要求更高它不光需要直接光还需要环境光来提供间接反射。尤其是金属感材质metalness越接近1越依赖环境贴图贡献反射内容。你只打一盏平行光一个金属球的大半部分会黑得彻底因为它反射的是环境信息场景里没有环境它就没有东西可反射。我强烈建议所有PBR场景都把环境贴图加上最简单的做法是用Three.js官方的RoomEnvironmentimport { RoomEnvironment } from three/examples/jsm/environments/RoomEnvironment.js; const pmremGenerator new THREE.PMREMGenerator(renderer); scene.environment pmremGenerator.fromScene(new RoomEnvironment()).texture;这行代码能瞬间让金属、粗糙表面都活过来。很多人调材质调半天调不出立体感缺的不是参数而是一张合适的环境贴图。2.2 UV和贴图的拧巴关系如果你在SketchUp 2024里也遇到过材质贴图时无法识别贴图文件的情况那你应该对贴图路径和格式的坑有心理准备了。到了Three.js贴图文件加载失败通常不会报错模型会静默变黑排查起来更隐蔽。UV是三维表面展开到二维纹理的坐标系Three.js不会帮你自动生成UV每个BufferGeometry只是在构造时内置了一套默认UV。如果你自己手拼几何体却忘了写uv属性贴上纹理后就是一片黑而且控制台完全没报错。另一个高频坑是glTF模型的纹理Y轴方向和部分DCC工具预览相反在某些模型上需要texture.flipY false才能和源软件里看到的一致。我的排查顺序固定是这样先看贴图URL能不能直接访问再看纹理是否异步加载完成最后查UV是否存在和正确。如果这三个都没问题才考虑更隐蔽的colorSpace和材质参数问题。顺序反了很容易在正常代码里白白消耗一下午。2.3 法线贴图与切线空间低模和高模之间的魔术法线贴图是让低模拥有高模表面细节的核心手段但它也是最容易翻车的贴图类型。法线贴图里的颜色值并不描述最终朝向而是描述相对于当前表面切线空间的方向偏移所以当模型本身的切线信息丢失或方向错误时贴出来的光影就会很奇怪。三点需要注意第一外部导入的FBX/glTF模型通常自带切线直接贴法线贴图没问题第二手写BufferGeometry的模型往往没有切线需要在加载后调用BufferGeometryUtils.computeMikkTSpaceTangents来计算第三法线贴图的强度不是越大越好超出合理范围会出现边缘过黑、高光过爆的假面感。import { computeMikkTSpaceTangents } from three/examples/jsm/utils/BufferGeometryUtils.js; computeMikkTSpaceTangents(geometry);这个函数在需要时调用一次就能补上切线数据。很多新手做法线贴图效果不对第一反应是图坏了其实十有八九是模型压根没有切线信息。3. 实操进阶从看得过去到有质感的三个实战3.1 PBR参数不应该背应该调以机器人模型为例肉眼可见的质感来自三种材质的配合金属外壳、深灰机身和玻璃感眼部。很多人拿到模型第一件事是疯狂拖roughness和metalness但参数之间是会互相影响的调了半天只是在原地打转。我的调参顺序固定是先保证环境贴图在场再调baseColor确定固有颜色然后定 金属还是非金属metalness取1还是0接着调roughness控制模糊程度最后才考虑normalMap、clearcoat这种锦上添花的项。下面是几个我实测过的经验区间材质类型metalnessroughness额外参数抛光金属件0.9~1.00.1~0.3需要环境贴图否则会发黑磨砂金属0.8~1.00.4~0.7可叠加AO贴图增加层次塑料/清漆面0~0.20.2~0.5clearcoat 可设0.5~1.0普通布料00.7~1.0可配sheen模拟绒毛玻璃/透镜00~0.1transmission 设为1为什么先说环境再调参数因为在高光反射严重的模型上环境贴图直接决定了反光形状和颜色参数只是在这个基础上做微调。很多塑料感问题其实是环境太弱、roughness又太高材质缺少高光响应导致的。3.2 让材质动起来火箭尾焰的ShaderMaterial实测做three.js 3d火箭发射动画特效这类效果时新手最容易犯的错是用一个发光圆柱体来充当尾焰。实际上尾焰是半透明材质和粒子系统的组合。这里我先写一个十分简化的ShaderMaterial用于生成一团有闪烁、渐变的喷射火焰代码短但足够说明核心思路。顶点着色器只需要把UV传递给片元着色器varying vec2 vUv; void main() { vUv uv; gl_Position projectionMatrix * modelViewMatrix * vec4(position, 1.0); }片元着色器用UV和时间变量模拟火焰形态uniform float uTime; varying vec2 vUv; void main() { // 尾部亮越往上越暗 vec3 color mix(vec3(1.0, 0.7, 0.2), vec3(1.0, 0.2, 0.05), smoothstep(0.0, 0.8, vUv.y)); // 随时间闪烁 float flicker sin(vUv.y * 20.0 uTime * 10.0) * 0.5 0.5; float alpha (1.0 - vUv.y) * (0.4 0.6 * flicker); // 边缘透明中心最实 float edge smoothstep(0.1, 0.45, abs(vUv.x - 0.5)); alpha * 1.0 - edge; gl_FragColor vec4(color, alpha); }JS侧的关键配置是transparent: true、depthWrite: false、blending: THREE.AdditiveBlending这样多个粒子叠在一起时会叠加发光而不是互相遮挡变黑。动画循环里更新uniform的值即可flameMaterial.uniforms.uTime.value delta;这个shader虽然离商业项目还很远但已经把尾焰的灰阶渐变、时间闪烁和边缘透明三个核心元素都覆盖了。真要做复杂版本可以在片元着色器里加入噪波函数让形状随机扭曲再配合粒子系统增加烟雾层次。3.3 让河流流动起来动态纹理与UV偏移在cesium河流材质的实现里水流效果通常是通过多张纹理叠加和UV流动来模拟的这个思路在Three.js里完全可以复刻。核心代码其实简单到让人吃惊把纹理的wrapS和wrapT设为RepeatWrapping然后在动画循环里让offset随时间缓慢变化。// 初始化 waterTexture.wrapS waterTexture.wrapT THREE.RepeatWrapping; waterTexture.repeat.set(1, 1); // 动画循环 waterTexture.offset.x - speed * delta;如果只做简单的河流平面这一行offset修改就够用了。想要更精细的水面效果可以用两张法线贴图叠加在ShaderMaterial里分别采样两个UV然后按时间做混合最后和基础颜色纹理混合。这样流水的方向感、波光粼粼感都会强很多。这里有个容易忽略的坑纹理的wrapS/T如果还是默认的ClampToEdgeWrappingUV坐标一旦超出[0,1]范围采样结果会卡在边缘色上产生一条明显的拉伸线而不是无缝平铺。所以做UV流动前第一件事永远是设置RepeatWrapping。4. 材质工程的另一半性能、资源与Vue2集成4.1 一百个机器人不卡材质共享与实例化真实项目里经常出现地图上有几百个机器人这样的需求。如果每个网格都new一个材质对象渲染器会对同一份shader重复编译draw call数量也会跟着爆炸。正确做法是让所有机器人共享同一个材质实例只在需要区分的部分用少量独立材质覆盖。如果几百个机器人只有颜色不同更好的选择是InstancedMesh。它允许GPU一次渲染所有实例配合instanceColor属性给每个实例设置独立颜色既保留了PBR材质效果又把draw call数量压到最低。需要注意的是当你用InstancedMesh修改单个实例颜色时使用的是实例颜色而不是修改共享材质本身的color否则所有机器人会一起变色。const mesh new THREE.InstancedMesh(geometry, sharedMaterial, count); mesh.instanceColor.setXYZ(index, r, g, b); // 按实例设置颜色这种方法我在实际项目里把draw call从三百多降到了四十以内帧率直接翻了一倍多。材质共享的核心思想是能复用就复用除非某个实例确实需要完全不同的着色逻辑否则不要轻易让每个实例各持有一份材质。4.2 纹理压缩与资源释放别让内存悄悄爆掉材质质量上去了贴图体积也会跟着膨胀。一张4K的PBR纹理动辄十几MB加载到移动端会显著拖慢启动速度。目前比较主流的解决方案是KTX2 Basis Universal压缩GPU可以直接解码内存占用和带宽压力都小很多。在实际项目里我更关心另一个容易被忽略的问题资源释放。组件切换、路由跳转之后旧的几何体和材质如果没有显式调用disposeGPU内存会一直被占着越跑越卡。下面这个函数可以递归遍历场景里的物体并释放资源function disposeObject(obj) { if (obj.geometry) obj.geometry.dispose(); if (obj.material) { const materials Array.isArray(obj.material) ? obj.material : [obj.material]; materials.forEach((m) { for (const key in m) { const value m[key]; if (value value.isTexture) value.dispose(); } m.dispose(); }); } if (obj.children) obj.children.forEach(disposeObject); }写SPA的时候这行代码能救命。不然你在页面上反复进出几个三维场景标签页迟早卡到想摔键盘。4.3 Vue2里集成Three.js别让响应式坑了你的材质Vue2 Three.js是老组合了但有一个很隐蔽的性能坑不要把Three.js对象塞进组件的data响应式系统。比如把material放进data动画循环里每次更新uniform都会触发Vue的响应式追踪白白消耗大量CPU。正确做法是材质、渲染器、场景这些对象挂到this上或者用普通对象保存只有页面需要展示的参数才放进data。另外组件销毁前一定要清理渲染循环和GPU资源否则切换路由后旧的requestAnimationFrame还在跑画面叠在一起像鬼影现场。beforeDestroy() { cancelAnimationFrame(this.animationId); this.renderer.dispose(); disposeObject(this.scene); }这个disposeObject就是上一小节那个递归释放函数。Vue2的生命周期钩子正好对应挂载和卸载两个时机mounted里创建渲染器和场景beforeDestroy里做彻底清理整个材质生命周期才算闭环。4.4 把材质沉淀成资产库一次配置多项目复用做多了项目以后我习惯把材质沉淀成JSON资产库。一个机器人、一堵墙、一条河本质上都是哪些贴图 什么参数的组合。用JSON描述材质类型和参数运行时再根据类型创建对应材质设计师改配色和质感时不需要碰代码。{ type: MeshPhysicalMaterial, baseColor: #3a3f47, metalness: 0.1, roughness: 0.45, clearcoat: 0.8, map: textures/body_baseColor.webp, normalMap: textures/body_normal.webp }这个做法最大的好处是团队协作时减少互相踩脚。三维美术同学调完参数我直接更新JSON前端页面刷新就能看到新效果不用走一次重新打包流程。材质资产库也需要像代码一样维护版本和命名规范否则时间一长几十个JSON文件谁也不敢动。5. 问题速查我把踩过的坑都列在这儿了5.1 快速定位材质问题的速查表症状可能原因排查与解决模型全黑没有光照 / 法线朝内 / 没有环境贴图加灯光检查法线方向给场景加scene.environment贴图显示黑色纹理未加载完成就赋给材质或路径不对用onLoad回调或Promise先确认URL可访问半透明材质的透明效果失效忘了设置transparent: true设置transparent: true必要时配合opacity颜色发灰或过曝色彩空间不对颜色贴图设SRGBColorSpace数据贴图设NoColorSpace法线贴图效果怪异几何体缺少切线数据调用computeMikkTSpaceTangents计算切线粒子/火焰叠加处发黑深度写入导致互相遮挡depthWrite: false使用AdditiveBlendingUV动画出现边缘拉伸纹理没有设置平铺设置wrapS wrapT RepeatWrapping这张表基本覆盖了我这几年遇到的高频问题。值得强调的是第一行模型全黑里有一半案例是法线方向朝内。外部导入模型时如果一个面从外面看是黑的、从里面看正常那基本就是法线翻转了用建模软件重新翻转法线再导出不要试图在Three.js里硬修。5.2 三个亲测有效的排查习惯第一个习惯先换MeshBasicMaterial验证模型和贴图。如果BasicMaterial加贴图正常切回StandardMaterial却全黑问题一定出在光照、UV或PBR参数上而不是模型文件坏了。这个隔离排查法能帮你快速锁定范围。第二个习惯把待调材质放到一个固定的材质测试台上测试。我用一个小球和一个小方块组合固定角度打光不同材质之间对比差距一目了然。不要在复杂的场景里调材质因为你看到的最终效果混合了光照、相机角度和后期处理很难判断是材质的锅还是其他因素的锅。第三个习惯调PBR材质前先把环境贴图挂上然后按baseColor-metalness-roughness的顺序调参。顺序一旦乱了很容易在错误的基础上反复试错。环境贴图对金属感的影响远大于你手拖的那几个参数很多材质假的问题其实是场景里少了一张能提供丰富反射信息的环境图。最后再回一句经常被问的问题WebGL/Three.js这种岗位需求多吗我前后在几家公司带过三维项目体感是国内这块岗位需求确实在涨但团队要的往往不是会调材质、会摆场景的demo选手而是能解决渲染性能、能写shader、能跟美术配合把效果调对的人。材质这条线把软件里几十个参数和GPU背后的数学概念串在一起先把材质这一关过了再去谈岗位多少也不迟。我个人调材质的体会是永远不要一上来就怀疑是库的bug先怀疑光照再怀疑UV再怀疑参数。这套排查顺序救了我无数个加班的夜晚。
返回列表