Babylon.js 材质与光照实战:告别塑料感,掌握 StandardMaterial 与 PBR 调参心法
如果你已经跟着前面的教程在 Babylon.js 里搭出了自己的第一个场景接下来最打击你的往往不是代码报错而是画面效果模型明明加载出来了材质也拖上去了灯光也放了但物体看起来就是一股塑料味。尤其是转起来之后高光跟着镜头乱闪暗部死黑一片反射更是完全没有逻辑。别急着怀疑自己的美术水平问题大概率出在材质和光的配合方式上。作为系列第三篇这篇教材专门讲材质与光先拆最常用的 StandardMaterial再讲真正能出氛围的 PBR 材质接着是四种灯光的性格差异最后聊环境贴图和几个我实际踩过不少次才想通的细节。适合所有已经能创建场景、加载模型的 Babylon.js 开发者。材质不是贴图光也不是一盏灯它俩是同一张蜘蛛网的两端怎么牵一发动全身下面细说。1. 告别塑料感先把 StandardMaterial 调明白1.1 默认材质为什么一股“清漆味”如果你只是 new 了一个 StandardMaterial 就直接赋给模型很容易发现物体表面像刷了一层清漆。很多引擎为了“让你不至于什么都看不见”默认会给比较强的镜面高光响应。于是灯光一打物体表面出现一块亮斑其他地方却很素净。这种强烈反差会让模型看起来像塑料玩具而不是真实世界里的质感。应急做法很简单把高光压下去。比如设置material.specularColor new BABYLON.Color3(0.05, 0.05, 0.05)或者调高material.specularPower 64油亮感会明显减轻。但如果你想把金属、木材、塑料区分开只靠这一招完全不够。你必须理解 StandardMaterial 上每个参数到底在模拟什么否则就会出现“这个材质调了和没调一样”的情况。1.2 四个颜色参数各管什么StandardMaterial 常用的颜色参数有四个diffuseColor、specularColor、emissiveColor、ambientColor。初学者最容易把它们都当成“颜色选择器”乱调结果越调越乱。参数作用常见误用diffuseColor / diffuseTexture材质的基础颜色也就是光线被表面吸收后再次散射出来的颜色以为贴图贴上去就完事忽略光照颜色对它的影响specularColor / specularPower镜面高光的颜色和锐度把高光调成纯黑结果金属表面也变得死板emissiveColor / emissiveTexture自发光颜色不受灯光影响把所有需要亮的区域都塞进 emissive导致画面像荧光棒ambientColor表面对场景环境光的响应程度盲目调高导致暗部发灰、阴影发飘用一句话理解你看到物体本质是光打上去之后一部分被吸收、一部分被反射回眼睛。diffuse 是材料分子吸收后散射出来的色光specular 是表面直接反射的灯光颜色。所以一个红苹果diffuse 应该偏红specular 则应该接近灯光的颜色。如果你把高光也设成红色苹果看起来会像一块软糖完全没有真实的缓冲感。我记得有次做产品展示客户一定要“更鲜艳”于是我把 diffuse 的颜色饱和度拉得很高。结果产品就像被人用滤镜硬刷过一层怎么看怎么假。后来才意识到真实感来自于材质对光的响应是否合理而不是把颜色本身调得有多重。鲜艳的是光不是物体本身。1.3 贴图贴不上的常见检查链材质调色只是一半另一半是贴图。很多刚入门的人把贴图赋给diffuseTexture之后发现物体表面是纯色或者整张图挤在一个角落里。先检查三件事模型有没有展 UV贴图是否成功加载纹理的 wrap 模式是不是默认的 CLAMP。展 UV 是建模阶段的事Babylon.js 帮不了你但贴图加载和 wrap 是可以从代码里查的。const mat new BABYLON.StandardMaterial(mat, scene); const tex new BABYLON.Texture(textures/wall.jpg, scene); mat.diffuseTexture tex; // 让贴图在 U、V 方向上重复两次 tex.uScale 2; tex.vScale 2; // 确保重复时不拉伸模糊 tex.wrapU BABYLON.Texture.WRAP_ADDRESSMODE; tex.wrapV BABYLON.Texture.WRAP_ADDRESSMODE;如果你发现贴图方向不对比如木纹横着贴成了竖着先不要急着旋转贴图优先检查模型在建模软件里的 UV 布局。Babylon.js 这边能给到的是uOffset、vOffset、uRotation这些参数但它们都是用来做细节修正的不是用来救一个完全没展 UV 的模型的。2. PBR 材质真实感靠的不是参数多而是物理模型2.1 为什么 PBR 调起来反而简单StandardMaterial 的参数看起来很直观但一多就会互相打架。你今天把高光调低了漫反射颜色又会发闷你把环境光调高了整个场景又变成灰蒙蒙一片。这种“拧一个螺丝整扇门歪掉”的感觉就是传统材质模型最让人头疼的地方。PBR 的思路完全不同。它不让你猜表面怎么反射而是把材质拆成几个物理量albedo基底色、metallic金属度、roughness粗糙度。你不需要关心高光具体是什么颜色因为高光的颜色和强度会由光的能量守恒自动推算出来。金属表面反射的是有色光非金属表面反射的是白色高光这些都由 metallic 一锤定音。const pbr new BABYLON.PBRMaterial(pbr, scene); pbr.albedoColor new BABYLON.Color3(0.8, 0.6, 0.45); pbr.metallic 0; pbr.roughness 0.6; box.material pbr;从 StandardMaterial 切到 PBR最直观的感受是你不再花时间“修”多余的反射而是把注意力放在“这个表面到底属于什么材料”上。材质真实感的第一步不是写更多代码而是去掉那些不符合物理直觉的参数。2.2 metallic 和 roughness 的物理含义metallic 只有 0 和 1 两端的意义真实中间值通常是给美术做“假混合”用的。实际物理世界里一个表面要么是金属要么不是。你可以做 0.3 的“半金属”但如果你真的想让一层金属上有锈斑正确做法是给锈斑区域一个 0 的 metallic而不是把整体调成 0.5。roughness 控制的是微表面平整度。roughness 为 0 时表面绝对光滑像一面完整镜面roughness 为 1 时表面极度粗糙像砂纸。实际项目里可以参照这个表起步材质metallicroughness说明未上漆木桌00.75木纹表面有一定粗糙感磨砂塑料00.4有微弱的镜面反射光面陶瓷00.2高光较锐利拉丝金属10.55金属但表面有拉丝纹理抛光金属10.1接近镜面反射每次新建 PBR 材质先想清楚你是要“金属”还是“非金属”然后再决定粗糙度的高低。这两个值一旦定下来光照和环境贴图才会顺着这个方向正确响应。2.3 从单值到贴图地表材质的实际做法只靠一个 roughness 值整块地会均匀得像一张新塑料板。实际上真实地面一定有裂缝、磨损、干湿不一致的区域。这时候就要上粗糙度贴图了。通常我会加载三张贴图albedo 贴图、metallic 贴图、normal法线贴图。其中 metal/rough 可以合并到一张贴图里Babylon.js 的 PBR 材质约定metallicTexture 的蓝色通道负责金属度绿色通道负责粗糙度红色通道可以被遗漏或者用来存环境光遮蔽。pbr.albedoTexture new BABYLON.Texture(textures/stone_albedo.jpg, scene); pbr.metallicTexture new BABYLON.Texture(textures/stone_metal_rough.jpg, scene); pbr.bumpTexture new BABYLON.Texture(textures/stone_normal.jpg, scene); pbr.metallic 0; // 如果贴图里已经包含金属度这里可以只给兜底值 pbr.roughness 0.8; pbr.invertNormalMapY true; // 根据项目坐标约定调整这里最常见的错误是只给 albedoTexture剩下的 metallic 和 roughness 全部用默认值。结果就是地面虽然有一块一块的花纹但反光完全不对一个没有湿润感的水泥地被高光打得像打了蜡。贴图不是越清晰越好它和参数组合起来才算一个完整的材质。3. 灯选对了材质才叫材质3.1 灯光亮度、颜色和方向先有全局概念有些人喜欢一盏灯打天下还有的人则是哪里暗就加灯光最后场景里摆了大大小小二十盏灯亮度叠在一起所有材质都白成一片。灯光不是越多越好关键是你需要几种不同“性格”的光。Babylon.js 提供了四类基础光源它们的物理行为不同使用场景也不同。光源构造函数简化生活中最像什么典型用途HemisphericLightBABYLON.HemisphericLight(name, direction, scene)阴天里整个天空的漫反射光基础底光模拟天空和地面环境DirectionalLightBABYLON.DirectionalLight(name, direction, scene)太阳光线近似平行室外硬阴影、强调方向PointLightBABYLON.PointLight(name, position, scene)灯泡或蜡烛室内局部补光、氛围光SpotLightBABYLON.SpotLight(name, position, direction, angle, exponent, scene)射灯、手电筒突出某个展示物体你会发现它们不是用来一比一还原现实世界的光源而是用来“分层”的。HemisphericLight 先给场景一个均匀的底DirectionalLight 再给出明确的方向感PointLight 或 SpotLight 只需要在最需要强调的地方出现。我的习惯是一个场景最少一盏半球光一盏主方向光其他灯光按需添加。3.2 四类灯光的创建和调参思路HemisphericLight 的 direction 表示“天空的朝上方向”。比如想让光从上方均匀洒下来用new BABYLON.Vector3(0, 1, 0)它的强度通常是 0.3 到 0.6太高会把阴影都冲淡。const hemi new BABYLON.HemisphericLight(hemi, new BABYLON.Vector3(0, 1, 0), scene); hemi.intensity 0.4; hemi.diffuse new BABYLON.Color3(1, 0.95, 0.9); // 暖色底光DirectionalLight 是平行光它的 direction 表示光线照向场景的方向和太阳角度正好相反。比如太阳在东南方向那么 direction 就大约是new BABYLON.Vector3(-1, -1, 0)。平行光不会因为距离衰减所以position更多是为了控制阴影的投影范围。const sun new BABYLON.DirectionalLight(sun, new BABYLON.Vector3(-1, -1, 0), scene); sun.position new BABYLON.Vector3(10, 20, 10); sun.intensity 1.2; sun.diffuse new BABYLON.Color3(1, 0.85, 0.7);PointLight 和 SpotLight 更依赖位置和衰减。PointLight 就是一颗灯泡你去设置它的position它会向四周均匀发光。SpotLight 比 PointLight 多了一个锥形方向和内角适合用来打亮一个展台或者强调一把椅子。const spot new BABYLON.SpotLight( spot, new BABYLON.Vector3(0, 5, 0), // 光源位置 new BABYLON.Vector3(0, -1, 0), // 朝下照射 Math.PI / 4, // 光锥角度 2, // 光锥衰减速度 scene ); spot.intensity 1.5;点光源和聚光灯都有距离限制。一盏点光理论上是无限远都能照到但实际项目里你会希望远处不受到干扰。Babylon.js 里设置spot.range或者手动给一个衰减半径能减少很多“灯光污染”。这不是一个必须打开的物理功能而是控制性能和环境干净度的重要习惯。3.3 阴影加错还不如不加阴影是材质和光“交响曲”里最容易被搞砸的一段。新手加阴影经常遇到两种情况一是阴影边缘又硬又黑二是地面上出现一堆麻点状的条纹。阴影的基础用法是这样const shadowGen new BABYLON.ShadowGenerator(1024, sun); shadowGen.useBlurExponentialShadowMap true; shadowGen.bias 0.0005; shadowGen.normalOffsetScale 1; mesh.castShadows true; ground.receiveShadows true;注意这里bias是防自阴影条纹的关键。如果没有 bias模型自身的表面在光照角度极低时会产生大量“自我遮挡”的伪影画面看上去就像表面长了疹子。normalOffsetScale也能帮助偏移但调得太大阴影会脱离物体底部看起来像物体在悬空。阴影贴图分辨率不是越高越好。1024 是一个比较均衡的起点放到 4096 除了让显存和帧率难受很多时候你看不出区别。还要注意只有投射阴影的 mesh 需要被shadowGen.addShadowCaster登记接收阴影的 mesh 只需要开启receiveShadows true。如果所有物体都在列表里阴影生成的开销会飙升。我记得有个项目里会议室场景加了六盏点光每盏都开了阴影结果帧率直接掉到十几。后来把次要的点光全部改成不投影只保留主方向光投影画面没有太大变化帧率却翻了倍。阴影是空间关系的线索不是装饰品。4. 环境贴图才是“真实感”的隐藏层4.1 没有环境贴图的 PBR像没有镜子的化妆室PBR 材质比 StandardMaterial 更依赖“周围有什么”。你仔细想一个金属球体它为什么看起来是金属因为它会反射它面前所有东西的形状和颜色。如果场景里没有任何环境信息它就只能反射纯黑或者纯白看起来就像一颗涂了漆的塑料球。环境贴图就是用来解决这个问题的。它是一张模拟房间、天空、树林、摄影棚等场景的全景贴图Babylon.js 会把它当作“从四面八方照进来的光”。它同时影响漫反射和镜面反射漫反射让暗部不那么死黑镜面反射让高光区域有具体的形状和颜色。所以当你用了 PBR 材质却觉得不对劲时先别急着调 metallic 和 roughness先问一句我的场景里有环境贴图吗4.2 接上 HDR 环境贴图的三步操作接入环境贴图最通用的是 HDR 格式。你可以去 Poly Haven 这类网站下载 HDR 文件然后直接加载为 Babylon.js 的 HDRCubeTexture。const hdr new BABYLON.HDRCubeTexture( textures/studio.hdr, scene, 512 // 内部 cube 面尺寸不用太大 ); scene.environmentTexture hdr; scene.environmentIntensity 0.5;这里的三步分别是创建 HDRCubeTexture、赋值给scene.environmentTexture、调整environmentIntensity。第三步最容易忘也最容易让场景瞬间“曝光过度”。因为 HDR 文件里的亮度范围很大如果不控制环境强度PBR 材质会自动反射出非常亮的高光整个画面会像开了 HDR 滤镜一样。对于更偏向性能的 Web 项目可以把 HDR 转成.env格式Babylon.js 对.env的压缩更友好。不过从学习角度先用一个.hdr跑通流程比纠结压缩格式重要得多。4.3 调节顺序先环境、再曝光、最后灯光如果你同时调环境贴图、曝光值、灯光亮度和材质参数你会很快陷入“调完一个又得回头调另一个”的循环。我现在的固定流程是第一把场景里所有灯光先关掉或者强度降到 0只留环境贴图。这样你能看到材质在均匀环境光下最原始的状态判断 albedo 和 roughness 是否合理。第二调整scene.environmentIntensity。我自己的起点是 0.4 到 0.6。这个值是环境整体的能量调得太高材质过亮调得太低暗部又会塌陷。第三调整scene.imageProcessingConfiguration.exposure和 toneMapping。先不用管太多细节把 exposure 从 1 调到 1.1 到 1.3能明显改善高光溢出的问题。之后再开灯光灯光强度只需要作为“补充”出现而不是主要照明来源。有了环境贴图之后你会发现之前调得半死不活的金属材质突然有了“能照出东西”的反射。这个瞬间很像按下乐器合奏的开关材质和光终于不再各唱各的调了。5. 调材质最容易翻车的四个细节5.1 颜色空间为什么我贴的木纹是灰的很多人在贴图之后发现颜色发暗、发灰。原因可能不是贴图本身而是颜色空间处理错了。简单理解一张木纹照片是 sRGBgamma空间而金属度、粗糙度、法线这些数据贴图是线性数据不应该做 sRGB 转换。Babylon.js 的 Texture 对象有gammaSpace属性。颜色贴图默认按 gamma 空间处理这是对的但当你加载法线、粗糙度这类贴图时如果忘记把gammaSpace设为false表面细节会被系统错误地“调亮”或“调暗”原本立体的纹路变成一片平板。const normalTex new BABYLON.Texture(textures/normal.png, scene); normalTex.gammaSpace false; // 数据贴图不要走 sRGB 转换 pbr.bumpTexture normalTex;这属于那种“代码看着没问题画面就是不对”的玄学问题。先检查颜色空间再检查灯光能省下非常多调试时间。5.2 法线贴图的“方向”会骗你法线贴图在 Substance Painter、Blender 里看着正常导入 Babylon.js 后凹凸感却像反过来。这不是加载失败也不是模型坏了大概率是坐标方向约定不同。Babylon.js 的 PBR 材质提供invertNormalMapX和invertNormalMapY两个开关一个是横向一个是纵向。通常我遇到凹凸方向反了先切invertNormalMapY试试因为多数软件以 Y 轴作为 OpenGL 风格法线如果问题还是存在再考虑 X 轴。千万不要在 Photoshop 里粗暴地把绿色通道提亮或者反转那样会破坏切线空间的平衡导致光照在边缘出现奇怪的断层。这个问题的根源是不同 DCC 工具之间的坐标习惯差异。建议在项目启动时就约定所有法线贴图的通道标准并且写一个统一的加载函数把gammaSpace false和invertNormalMapY这类设置集中处理省得每个材质都重复踩一遍。5.3 透明物体别只调 Alphamaterial.alpha 0.5是最快的透明方案但如果你做玻璃、水面、塑料膜只调 alpha 会遇到排序问题。两个透明面片叠在一起时谁在前谁在后引擎是不知道的于是会出现透明穿帮、边缘重叠、颜色漂移。Babylon.js 的 PBR 材质里透明有专门的模式比如pbr.transparencyMode BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND同时还需要配合环境贴图和高光响应否则玻璃就是一个“半透明的塑料片”。想要做出真正的玻璃核心思路不是让物体变透明而是让透明表面同时又能反射周围环境。没有环境贴图的时候玻璃看起来永远是脏脏的。如果只是做树叶、铁丝网这种带孔洞的物体用透明度贴图或者 alpha test 会更稳定避免半透明排序带来的性能消耗。记住一点全屏半透明粒子也好、玻璃瓶也好透明材质是特殊的渲染队列不能和普通材质一样“无脑赋值”。5.4 材质复用性能优化第一课性能问题很多时候不来自引擎而来自 coding 习惯。比如你复制了 100 个箱子每个箱子都new一个材质就算贴图一样DrawCall 也会成倍上涨。正确做法是如果它们长得一样就共享同一个材质实例需要不同颜色时考虑用实例化 mesh 或顶点颜色而不是新建一堆材质。还有贴图尺寸。一个用于很小物件的 4096 贴图在美术眼里很清晰在 GPU 眼里却是巨大的显存负担。我的经验是大件地面 2048中小件 1024 到 512环境贴图 cube 面 512 已经足够。不要什么都堆在最高清画面真实感的核心不是贴图有多清晰而是光照、反射、材质三者有没有统一。Babylon.js 自带的 Scene Inspector 是个强力调试工具场景里点一下 mesh就能实时改材质参数和灯光亮度比反复改代码刷新页面高效得多。调材质时别急着关页面先打开 Inspector 把参数拖一遍再回代码里固化。说回文章开头那个塑料感场景。我一开始也怀疑是不是模型没做好后来发现 90% 的情况是材质高光、环境贴图、灯光三层没配合好。现在我的习惯是先搭一个“标准环境”一张 HDR、一块地面、一盏方向光把材质放进这个环境里看先别急着调灯。也就是说光与材质是一套系统不是你加一盏灯、我调一个高光就能解决。想通这一点Babylon.js 的材质和光就算入门了。后续再玩后处理、粒子、天空盒你会发现底层还是这套相互作用。