本节摘要:相机与灯光是一帧开头的两个数据源——相机产出"从哪看"的视图投影矩阵,灯光产出"照多亮"的光照数据。本节从一次黑屏事故切入,讲两类常用相机的控制模型、四种灯光的每帧成本差异,并给出"先摆相机再打灯"的工作顺序。承接 2.1 的节点树(相机灯光也是树上节点),通向 2.3 的视锥剔除与第3章的着色计算。
新手最崩溃的一幕:代码不报错,场景"渲染成功",画面纯黑。翻遍属性设置,最后发现是两个低级疏忽之一:忘了给场景配灯光——标准材质没有光就没有颜色输出;或者相机藏在几何体内部、朝向反了——树和灯都在,只是"从哪看"出了问题。黑屏本身不可怕,可怕的是不知道检查顺序。这一节把两个数据源的机理讲透,你就能按因果链排查而不是碰运气。
先说相机。它住在场景图上,也是节点,但身份特殊:每帧第4站,引擎取它的位置与朝向算出两个矩阵——视图矩阵把世界"搬到"相机面前,投影矩阵把眼前的东西压进一个视锥。视锥就是一帧的可见范围,像一台从相机出发向前张开的手电筒光锥。下一节讲的剔除,判据正是这个锥体。

两类相机对应两种控制模型。ArcRotateCamera 是"围绕目标转":位置由水平角、垂直角、半径三个参数反算出来,天然适合产品展示、编辑器视角,鼠标拖拽旋转滚轮缩放一行代码就有了。UniversalCamera 是"身处世界走":直接控制位置与朝向,配键盘鼠标控制器就是第一人称视角。选错模型事倍功半——用环绕相机硬做走迷宫,手感永远是错的。
// 环绕相机:展示类场景的首选 const arc = new BABYLON.ArcRotateCamera("orbit", -Math.PI / 2, 1.1, 10, BABYLON.Vector3.Zero(), scene); arc.attachControl(canvas, true); arc.lowerRadiusLimit = 5; // 拉不远过近:防止穿模 arc.upperRadiusLimit = 20; // 也拉不无限远:防止迷失 arc.wheelDeltaPercentage = 0.01; // 滚轮步进改成百分比,手感更顺 // 自由相机:第一人称类场景的首选 const walk = new BABYLON.UniversalCamera("walk", new BABYLON.Vector3(0, 1.7, -8), scene); walk.attachControl(canvas, true); walk.speed = 0.2; // 移动速度 walk.inertia = 0.7; // 惯性:0 到 1,越大越滑 walk.minZ = 0.1; // 近裁剪面别用默认 1,贴墙看物体会被裁
相机切换在应用里很常见:展厅"鸟瞰与走进去"双视角,切换时保留各自参数即可,不必销毁重建——重建会重新编译管线状态,切换瞬间会卡一下。无论哪类相机,三个裁剪与视场参数值得单独记:
| 参数 | 作用 | 经验起点 |
|---|---|---|
| minZ 近裁剪面 | 比这更近的东西不画 | 0.1,贴墙看物不被裁 |
| maxZ 远裁剪面 | 比这更远的东西不画 | 按场景尺度收紧,别留默认大数 |
| fov 视场角 | 视锥张开的角度 | 0.8 弧度接近人眼,越大越"广角" |
四种常用灯光,按每帧开销从低到高排:半球光模拟环境底色,只有方向与两个颜色;方向光是从无穷远照来的平行光,太阳的不二之选;点光源向四周辐射,灯泡;聚光灯带锥角,舞台灯。方向光最便宜,聚光灯最贵,但真正的台阶不在类型,在数量——着色器要逐像素遍历场景里的灯,灯从一盏加到四盏,像素着色的算术量近似翻着涨。
// 一个"一主两辅"的标准布光组合 const sun = new BABYLON.DirectionalLight("sun", new BABYLON.Vector3(-0.4, -1, -0.3), scene); sun.intensity = 1.0; // 主光:定调子,负责影子(第3章) const fill = new BABYLON.HemisphericLight("fill", new BABYLON.Vector3(0, 1, 0), scene); fill.intensity = 0.35; // 辅光:抬暗部,避免背光面死黑 const rim = new BABYLON.PointLight("rim", new BABYLON.Vector3(2, 3, -2), scene); rim.intensity = 0.5; // 轮廓光:把物体从背景里勾出来 rim.range = 12; // 照射范围,超出按距离衰减,也省着色计算
灯只照一部分物体的手法是把网格的 includedOnlyMeshes 或排除列表配好——展台打追光、别处不沾光,靠的就是这个属性,顺带把"这盏灯对哪些像素生效"说清楚,着色器生成时也能更省。
⚠️ 常见坑:灯的 intensity 与 diffuse、specular 颜色相乘生效,两处都调会互相抵消直觉。调亮度只动 intensity,调色温只动颜色,两个旋钮别一起拧。
把开头的事故变成可执行的检查表,按命中概率从高到低:第一查场景是否有灯、灯强度是否为零,标准材质无光即黑;第二查相机是否藏在几何体内部或朝向反了,把环绕半径拉到 30 再看一眼;第三查材质漫反射色是否被设成了纯黑,通道概念在 3.1 节展开;第四查多相机场景的激活相机是否切错;第五查网格是否被缩放到极小或挪出了视锥。五条覆盖九成黑屏,剩下的个别案例是后端抗锯齿与透明画布的配置问题,留到 5.4 节的调试工具里用层视图查。
背景:产品展示页照搬摄影棚思路打了五盏灯,中端手机帧率 34,画面反而发灰——多盏辅光互相补,层次被抹平了。操作分三步:先只留主光,画面立刻出现明暗对比;再补一盏低强度半球光抬暗部;最后用 0.3 强度的点光勾轮廓。结果:三盏灯层次分明,帧率回到 51。解读:灯光的边际收益递减极快,第二盏之后的每一盏往往在"抹平"而非"增加"信息——先做减法再谈加灯,是画面与帧预算共同的正道。变式:以金属和玻璃为主的场景是例外,它们吃反射不吃直射,第3章 PBR 的环境贴图比补灯有效得多,这也是 PBR 一节"环境打底"伏笔的由来。
相机与灯光就位,一帧的"输入数据"齐了:视图投影矩阵圈定可见范围,光照数据等着被像素消费。工作顺序上建议"先摆相机再打灯"——先确定观众从哪看,再决定光从哪来,反着做经常会打出一片观众根本看不到的灯,白花帧预算。下一节钻进绘制名单本身:一个网格到底由哪些数据构成,包围盒如何配合视锥完成剔除。