
材质和光照系统是three.js里最让新手头疼、也最能拉开画面差距的两个板块。代码逻辑写清楚了几何体也加载进来了但画面要么漆黑一片要么像塑料模型一样假十有八九问题就出在材质和光源的配合上。这篇文章我从零开始把three.js的材质类型、光源体系、参数逻辑和实战布光流程完整过一遍讲清楚每个选择背后的原因和实际踩坑经验适合刚接触three.js、正在为画面效果发愁的开发者参考。1. 先弄明白为什么材质和光照必须放在一起学大家在网上看到的那些炫酷three.js作品背后的场景其实不复杂区别主要在于对材质和光照的理解深度。单独看材质或单独看光照都会陷入参数我都设了效果就是不对的尴尬。1.1 光照是因材质是反应材质决定了一个表面的颜色、粗糙度、金属感、透明度光照则决定这些属性如何被我们看到。用生活化的类比来说材质是物体的皮肤光源是打在皮肤上的光。同一个人在日光灯下和在夕阳下看起来完全不同不是因为皮肤变了而是因为光照变了。反过来同样的光照条件下一块金属和一块布料的反光效果天差地别这就是材质在起作用。three.js遵循的逻辑正是如此渲染器把光源信息传给材质材质根据自身的参数去计算每个像素应该显示什么颜色。所以如果你用了不受光照影响的材质光源的能量再强也没有意义反过来如果你用了依赖光照的材质却不打灯场景就会一片漆黑。1.2 一个被光照参数折磨的真实场景我见过不少新手写的第一段three.js代码长这样创建了一个MeshStandardMaterial标准材质往场景里加了一个AmbientLight环境光然后奇怪为什么物体看起来扁平的没有立体感。还有人加了PointLight点光源但光照强度给了1000画面过曝到只剩白色。这类问题的根子在于光照强度、衰减、材质粗糙度、金属度这些参数是协同工作的单独调某一个往往适得其反。理解了光源类型和材质属性的对应关系之后你会发现自己不再依赖试参数试到满意为止而是能直接根据想要的效果反向推导出该怎么做。这也是这篇博文想帮你达到的状态。2. three.js材质家族从占位符到物理真实three.js的材质体系经历了几代迭代从最初的简单着色模型到现在的基于物理渲染PBR理解这个演进过程有助于你理解每个材质的位置。2.1 材质分类三维度是否受光、是否物理真实、性能开销维度一材质是否对光源产生响应。MeshBasicMaterial完全不理会场景里的任何灯光它显示的就是贴图或颜色本身像一张贴纸。MeshLambertMaterial、MeshPhongMaterial以及后面一系列Standard、Physical材质都会对光源产生反应。这个区别在调试时很关键如果你的物体是黑的先检查是不是用错了Basic材质。维度二是否基于物理真实。MeshStandardMaterial用金属度metalness和粗糙度roughness来描述表面MeshPhysicalMaterial在它基础上扩展了清漆层、透光率等物理属性。而MeshLambertMaterial和MeshPhongMaterial是早期经验模型它们计算快但不够真实。维度三性能开销。Basic最省、Lambert次之、Phong中等、Standard和Physical最贵。这个排序在移动端开发时特别重要同样是展示一个模型不同材质可能带来成倍的帧率差距。2.2 六大材质逐一拆解MeshBasicMaterial它就像一张贴纸不接受光照不会产生阴影只显示颜色或贴图。适合用于调试场景——先用它确认模型加载是否正确再切换到标准材质调整光影效果。也适合制作不真实感的UI特效、辅助线、天空盒子等。MeshLambertMaterial这是最简单的受光材质只计算漫反射diffuse表面光线的亮度会随法线方向变化但不会出现高光镜面反射。因此它适合表现粗糙表面粉笔、混凝土、布料。Lambert材质的性能开销很小在低端设备上如果不需要高光用它会是不错的选择。MeshPhongMaterial在Lambert的基础上增加了镜面反射specular计算可以通过shininess控制高光范围通过specular颜色控制高光色调。这是早期three.js项目最常见的材质。虽然现在有了更真实的Standard材质但在做卡通渲染或需要精确控制高光的风格化效果时Phong反而更好使。MeshStandardMaterial这是当前最主流、也是我做项目默认选择的材质。基于PBR模型核心参数就两个metalness金属度和roughness粗糙度。metalness为0表示非金属为1表示纯金属。roughness从0到10是抛光的镜面1是完全粗糙的哑光表面。用这两个参数可以模拟出绝大部分真实材质的感觉。此外Standard材质的高度贴图bumpMap、法线贴图normalMap、环境遮挡贴图aoMap、自发光贴图emissiveMap等通道为细节表现提供了很大的空间。MeshPhysicalMaterial它是Standard的增强版。除了基础参数额外支持clearcoat清漆层强度、clearcoatRoughness清漆层粗糙度、sheen织物边缘光泽、ior折射率等参数。用来表现汽车漆面、玉石、有镀膜的眼镜镜片等复杂光学效果时Physical Material是首选。它的渲染开销也更大适合对画面质量要求较高且设备性能充裕的场景。MeshToonMaterial卡通渲染专用材质。它的漫反射颜色不是连续的而是通过梯度贴图产生明暗分阶的效果制造出类似漫画的硬阴影边界。做风格化项目时我经常用价格便宜效果也稳定。2.3 材质公共参数与常见坑有几个参数几乎对所有材质都适用但容易被忽略。第一个是side渲染面。默认值是THREE.FrontSide只渲染正面。如果模型面的法线方向错了或者你在做玻璃、纸张这类双面可见的物体就会看到少了一半的现象。这个时候需要设置side: THREE.DoubleSide。第二个是transparent与opacity。很多新手直接设置opacity发现没有效果原因是没有把transparent设为true。需要提醒一下开启透明后three.js要额外进行透明排序会带来一定的性能开销大量透明物体叠加时还可能出现穿插错误所以要尽量少用。第三个是visible。材质继承自Object3D有visible属性。如果在调试时想快速隐藏某个物体不需要删除它直接设material.visible false即可。第四depthWrite。当多个透明物体相互穿插时调整深度写入能解决排序错误的问题但副作用是可能产生绘制顺序异常。这是进阶技巧新手阶段需要了解有这个参数即可。3. 光照系统拆解光源类型与场景适配three.js里光源的种类并不复杂但每种光源的适用场景和计算方式差异很大。布光的本质是在艺术效果和性能代价之间找平衡。3.1 四种基础光源的物理直觉AmbientLight环境光没有方向均匀照亮所有物体相当于一个保底光。它不产生阴影没有衰减作用是防止场景中背光面完全变黑。它的强度不宜过高我一般控制在0.3-0.5左右太强的Environment光会让画面发灰失去立体感。DirectionalLight平行光光的方向是固定的类似于太阳。无论物体离光源多远光照强度不变。平行光可以产生阴影是室外场景的主力光源。它在场景里更适合用做主光。PointLight点光源从某个点向四周辐射像灯泡。它有两个核心参数距离distance和衰减decay。distance控制光照的影响范围超出范围之后光强降为0decay控制光强随距离衰减的速度。点光源的阴影比较柔和但计算量比平行光大。SpotLight聚光灯在点光源的基础上增加了方向性和照射角度。angle控制照射范围锥形角度penumbra控制边缘从亮到暗的过渡范围。聚光灯很适合做舞台灯光、汽车大灯这类效果。3.2 光源参数里最容易被忽略的细节光源的强度单位让人困惑过一阵子。在three.js较早版本和较新版本里光照强度默认单位并不完全一致。现代版本中直接在构造函数里传入强度参数如new THREE.PointLight(0xff0000, 50)。物理单位校准useLegacyLights false之后PointLight强度数值的含义变得更接近实际功率实际操作里不用太纠结单位记住数值越大越亮配合衰减调节就够用。另一个高频问题光源放在场景里没有效果。排查思路很简单——先确认材质是否受光再用DirectionalLight或AmbientLight排除光源类型问题最后检查光源的position是否真的在物体可见范围内。position这个变量也容易忽视。DirectionalLight只关心target也就是光的方向它自身的位置不影响角度计算。而SpotLight和PointLight的position就是发光点的位置如果忘了把position设置到物体附近场景当然一片黑。**HemisphereLight半球光**也值得一提。它模拟天空和地面的环境色配合不同颜色值可以迅速营造出自然的光照氛围。它在室外场景中做辅助光非常好用能有效避免背光面死黑。3.3 阴影光源、材质、渲染器的三方配合阴影是让场景立体感大幅提升的关键同时也是新手最容易卡住的地方。three.js默认不开启阴影你需要做三件事才能让阴影落地第一步renderer.shadowMap.enabled true。这是渲染器层面的总开关。第二步光源设置castShadow true。平行光、点光源、聚光灯都支持投射阴影AmbientLight和HemisphereLight不支持。第三步物体上需要区分投影体和接收体。投射阴影的物体设castShadow true接收阴影的地面或桌面设receiveShadow true。还有一个细节MeshBasicMaterial虽然能看到阴影吗答案是看不到的。因为Basic材质不参与光照计算也不接收阴影投影。换句话说接收阴影的材质必须是能响应光照的材质。阴影效果的细节参数也值得花时间调一调。shadow.mapSize控制阴影贴图分辨率默认是512x512想更精细就设2048或更高但内存和计算开销会上升。shadow.bias用于修正阴影条纹shadow acne问题通过微调可以消除阴影斑点但设得太大又会出现阴影偏移的悬浮感。这些参数不是一次就能调好的多试几组值观察边缘变化直到找到不大不小刚好合适的值。4. 实战从零搭一个看得过去的静物场景理论说再多不如跑一个完整的场景。这一节我用一个桌面静物的示例把材质选择和布光流程完整过一遍。场景很简单一个金属质感的水杯放在粗糙的木桌上背后是柔和的环境余光。这个案例覆盖了金属度、粗糙度、阴影、对比度这几个核心概念。4.1 场景准备先搭好场景、相机和渲染器基础设施。这一部分相信大家已经很熟悉我只强调几个关键选项。const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.shadowMap.enabled true; renderer.shadowMap.type THREE.PCFSoftShadowMap; renderer.outputColorSpace THREE.SRGBColorSpace; renderer.toneMapping THREE.ACESFilmicToneMapping; renderer.toneMappingExposure 1.0;开头几步是关键type选择PCFSoftShadowMap阴影边缘会比较自然地柔和。outputColorSpace设置为sRGB色彩空间避免出现整体偏灰、泛白的问题。这是拿颜色贴图场景一个经常被忽视的细节。toneMapping选择ACESFilmicToneMapping可以提升画面的对比度和色彩层次感让亮部不过曝。这是画面质感提升的一个重要技巧。4.2 材质用参数描摹真实感先做桌面。木桌面不是高反光材质用MeshStandardMaterialroughness设在0.85左右金属度0。这样表面不会有明显高光但纹理细节还是能看出质感。然后是水杯。假设是不锈钢材质metalness设到0.95roughness设到0.2。这样杯身上会出现明显的环境反射和镜面高光。如果杯子表面还想要一层清漆感可以换用MeshPhysicalMaterial给clearcoat设置一个0.3左右的数值效果会更肉一点。const tableMaterial new THREE.MeshStandardMaterial({ color: 0x8b7355, roughness: 0.85, metalness: 0, }); const cupMaterial new THREE.MeshPhysicalMaterial({ color: 0xc0c0c0, metalness: 0.95, roughness: 0.2, clearcoat: 0.3, });贴图的事先放一边纯颜色材质先把光影关系理清楚后续替换贴图只是加一个texture的事。4.3 布光从主光、辅光到轮廓光室内静物场景我习惯的布光顺序是主光→辅光→轮廓光。主光用DirectionalLight模拟窗外进来的阳光位置在物体左上方角度稍微向下。强度设置在2.0到3.0之间打开castShadow。这样的好处是阴影方向单一、清晰便于构建主体的体积感。辅光用HemisphereLight天空颜色偏冷蓝地面颜色偏暖棕强度0.3到0.5。它的作用是填充主光产生的阴影区域防止纸面下黑成一片同时增加环境的氛围色。轮廓光用SpotLight或PointLight放在物体后方偏上用来勾出杯子的边缘亮线让主体从背景中跳出来。强度不需要大0.5到1.0足够太强会喧宾夺主。const mainLight new THREE.DirectionalLight(0xfff5e6, 2.5); mainLight.position.set(4, 6, 3); mainLight.castShadow true; mainLight.shadow.mapSize.set(2048, 2048); scene.add(mainLight); const fillLight new THREE.HemisphereLight(0xaaccff, 0x8b7355, 0.4); scene.add(fillLight); const rimLight new THREE.SpotLight(0xffffff, 1.0); rimLight.position.set(-3, 4, -4); scene.add(rimLight);调参的过程我建议按这个顺序来先单独开主光把整体的明暗方向确定下来再开辅光调整暗部亮度直到细节可见但不抢戏最后加轮廓光只为了提亮边缘。这样做不会出现哪个光源都想要最后画面过曝的情况。5. 调试与避坑我踩过的那些材质光照坑最后这部分直接上干货。我在实际项目里踩过的坑整理成几条规律方便大家对照排查。5.1 画面发灰或过曝多半是色彩空间和色调映射的问题很多新手刚学会打光调了半天灯光画面却总是灰蒙蒙的对比度上不去。原因往往是渲染器的outputColorSpace没有设置默认的线性色彩空间会让颜色显得闷。把outputColorSpace设成SRGBColorSpace之后立刻会通透很多。如果画面高光区域容易糊成一片、缺乏层次加上ACESFilmicToneMapping再把toneMappingExposure调到0.8到1.2之间高光和暗部都能保留更多的细节。想追求不同的风格可以试试ReinhardToneMapping或NeutralToneMapping效果差异明显多做几组对比就会有自己的偏好。5.2 阴影出现条纹、漏光或抖动阴影脏乱多因为两个原因。一个是shadow.bias参数设置得不好。偏小容易出现和物体表面打架的条纹阴影偏大则物体阴影会悬空或歪斜。正确做法是把bias先设成0.01左右观察阴影边缘上下微调直到干净。另一个是阴影相机的范围问题。DirectionalLight的shadow.camera范围默认是-5到5如果你把一个比较大的场景放进去物体可能超出阴影相机范围导致阴影不断跳动或消失。解决办法是根据场景大小设置shadow.camera的left、right、top、bottom值。这里用一张表格总结现象常见原因调整方向阴影表面有黑色小斑点shadow.bias过小增大bias如0.01阴影悬浮、和物体分离shadow.bias过大减小bias远处阴影突然消失shadow.camera范围不够扩大shadow.camera边界阴影边缘太锯齿shadow.mapSize太低提高mapSize到2048或更高5.3 性能优化光源数量和材质档位的取舍最后说性能。点光源和聚光灯的阴影计算非常昂贵。一个场景里同时有三个以上带阴影的PointLight帧率基本会明显下滑。移动端项目里更要克制我有段时间为了画面效果在一个房间里放了六个SpotLight结果手机直接变成幻灯片。优化思路有几个按性价比排序用DirectionalLight替代部分PointLight和SpotLight平行光阴影性价比最高。没有必要的灯光不要开castShadow。固定场景可以把光照信息烘焙到贴图里运行时不再动态计算这是性能提升幅度最大的一招。不用PBR材质的地方就退回到MeshLambertMaterial或MeshPhongMaterial肉眼差别不大性能差异却很可观。环境贴图如HDR环境映射能同时提升Standard材质的反射细节代价只算一次采样往往比多加几个辅助光源更划算。我个人的经验是一般场景建议的配置是1个主光带阴影 1个环境光/半球光辅助 1个轮廓光不带阴影。这个配置既能把物体的立体感和材质质感表现出来性能也不会受到明显拖累。真需要更多灯光氛围时再做针对性妥协。材质与光照的掌握其实没有太多捷径理解PBR参数的含义搞懂光源类型的物理直觉剩下的就是多调多对比。把上面这些参数逐一改变、观察画面变化的规律记在心里以后再遇到任何画面不对的情况你都能很快定位到问题所在。