☰
【three.js教程】阴影:从“假阴影“到“阴影贴图“一次讲清
2026/10/8 6:44:55 网站建设 项目流程

Three.js 阴影:从"假阴影"到"阴影贴图"一次讲清

原文出处:Three.js Manual – Shadows
本文基于官方手册 Shadows 章节整理,从最便宜的假阴影讲起,再到真正的阴影贴图(shadow map)怎么调,一篇看懂。

写在前面

前面两篇我们聊了灯光和相机,场景有立体感、也能正确"看"了。但还差最后一部分——阴影。

没有阴影,物体像是飘在半空,怎么看都不真实。three.js 里做阴影有一套完整的机制,但坑也不少。这篇先介绍假阴影方案,再介绍真正的阴影贴图,并说清新手最常卡住的几个点。

一、阴影很贵

three.js 默认用shadow map(阴影贴图)做阴影。它的原理一定要记住这句话:

对于每一盏"投影"的灯,场景里所有"投射阴影"的物体,都要从"灯的角度"再画一遍。

假设你有 20 个物体、5 盏投影灯,整个场景就会被画 6 遍(5 遍从灯的角度、1 遍正式渲染)。更复杂的是,如果是点光源投影,一盏灯就要画 6 遍(立方体六个面各一遍)!

所以现实里常见做法有几种:

  • 多盏灯,但只让其中一盏平行光投影;
  • 用 lightmap / 环境光遮蔽贴图离线烘焙(静态、快,但后面单讲);
  • 直接用假阴影(最简单)。

二、假阴影:一个圆片搞定

最省事的方案:弄一块平面,贴一张灰度"阴影"纹理,放在物体正下方。像《动物森友会》《纪念碑谷》里角色脚下的圆影就是这么干的——便宜又有效。

做法是给每个球建一个Object3D作为"底座",球和阴影都挂它下面,这样移动底座两者一起动:

constbase=newTHREE.Object3D();scene.add(base);constshadowMat=newTHREE.MeshBasicMaterial({map:shadowTexture,transparent:true,// 能透出地面depthWrite:false,// 阴影之间互不打架,不用排序});constshadowMesh=newTHREE.Mesh(shadowGeo,shadowMat);shadowMesh.position.y=0.001;// 略微抬高,避免和地面 z-fightingshadowMesh.rotation.x=Math.PI*-.5;constshadowSize=sphereRadius*4;shadowMesh.scale.set(shadowSize,shadowSize,shadowSize);base.add(shadowMesh);

动画时让球上下弹跳,同时把阴影的透明度跟着调淡——球越高,影子越淡:

constyOff=Math.abs(Math.sin(time*2+ndx));sphereMesh.position.y=y+THREE.MathUtils.lerp(-2,2,yOff);shadowMesh.material.opacity=THREE.MathUtils.lerp(1,.25,yOff);

两个小坑:depthWrite: false是让多个阴影互不遮挡;position.y = 0.001是防止和地面"深度冲突"闪烁(z-fighting)。

三、真正的阴影贴图

当你需要真实阴影时,走这三步:

1. 渲染器开阴影

renderer.shadowMap.enabled=true;

2. 灯光开启投影

constlight=newTHREE.DirectionalLight(color,intensity);light.castShadow=true;

3. 每个物体声明:投影?接收?地面只接收、不投射(反正底下看不见);立方体、球两者都要:

mesh.receiveShadow=true;// 地面mesh.castShadow=true;// 立方体/球mesh.receiveShadow=true;

跑起来——咦,怎么阴影缺了一块?

四、为什么阴影"缺一块"?调阴影相机

原因:阴影贴图是从"灯的角度"渲染的,灯上挂着一台阴影相机,它框定的范围之外就不产生阴影。上面缺一块,是因为这个范围太小了。

把灯的阴影相机可视化出来就一目了然:

constcameraHelper=newTHREE.CameraHelper(light.shadow.camera);scene.add(cameraHelper);

平行光用的是正交相机做阴影相机(回顾相机那篇),靠left/right/top/bottom/near/far/zoom定框。手册用一个DimensionGUIHelper同时设left/right为width、bottom/top为height,再配之前写过的MinMaxGUIHelper调near/far:

functionupdateCamera(){light.target.updateMatrixWorld();helper.update();light.shadow.camera.updateProjectionMatrix();// 改了阴影相机参数必须调这个cameraHelper.update();}updateCamera();

把width、height调到约 30,阴影就完整了。

但别贪大:有人想"那我设成 100 全包住不香吗?"——结果阴影变得马赛克一样糊。因为阴影贴图本质是一张纹理,框得越大,这张纹理被拉伸得越厉害,分辨率就越低。

五、阴影贴图分辨率:mapSize

阴影贴图是张有尺寸的纹理,默认512×512。越大越清晰,但越吃内存越慢。用这两个属性调:

light.shadow.mapSize.width=1024;light.shadow.mapSize.height=1024;

实战经验:阴影清晰度的两个杠杆是——① 阴影相机框尽量小(刚好罩住场景);② mapSize 尽量小但够用。另外注意每台机器的纹理上限renderer.capabilities.maxTextureSize。

六、聚光灯阴影:框由灯自己定

换成SpotLight,它的阴影相机变成透视相机。而且大部分参数你改不了——它的fov直接绑死在聚光灯的angle上,aspect由贴图尺寸自动算。所以调聚光灯阴影,本质就是调angle和penumbra(见灯光那篇)。

angle开太大,阴影贴图被摊到很大一片区域,分辨率照样崩。

七、点光源阴影:最慢的一种

PointLight向四面八方发光,它的阴影相当于6 个聚光灯阴影(立方体六个面各一遍)。所以只有near和far两个参数可调,而且整场景要画 6 遍,最慢。

常见演示是给场景套个盒子(side: THREE.BackSide渲染内侧),让阴影能投到墙和天花板上:

constcubeGeo=newTHREE.BoxGeometry(cubeSize,cubeSize,cubeSize);constcubeMat=newTHREE.MeshPhongMaterial({color:'#CCC',side:THREE.BackSide,});constmesh=newTHREE.Mesh(cubeGeo,cubeMat);mesh.receiveShadow=true;mesh.position.set(0,cubeSize/2-0.1,0);// 略低于地面,避免 z-fightingscene.add(mesh);

拖动点光源位置,能看到阴影落在四周墙上。near以内不投影、far以外永远在阴影里。

总结:怎么选阴影方案?

方案真实度性能适用
假阴影(圆片)低极快移动端、卡通风格、俯视游戏
平行光阴影贴图高中室外主光源
聚光灯阴影贴图高中舞台、手电
点光源阴影贴图高慢(6 遍)室内灯泡,谨慎用

记住四句话:① 投影前renderer.shadowMap.enabled = true+ 灯castShadow+ 物体cast/receiveShadow;② 阴影"缺一块"是阴影相机框太小;③ 框别太大、mapSize 别太高,否则糊或卡;④ 能少用几盏投影灯就少用。

延伸阅读

  • Renderer.shadowMap
  • LightShadow
  • DirectionalLightShadow
  • PointLightShadow
  • 回顾:Lights(灯光) · Cameras(相机)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询