GPU Gems 1 · Chapter 10. Cinematic Lighting
用 uberlight 把选择、颜色、光斑形状、阴影与投影纹理变成可编程的电影化光照控制。
学习目标
- 能说明 cinematic lighting 为什么允许艺术家独立控制选择、颜色、形状、阴影和纹理
- 能解释 omni light 与 barn light 的几何差异,并用 superellipse 调整光斑边界
- 能修改实验中的光强、soft edge、shadow density、cookie 和 specular,预测画面变化
- 能把 uberlight 的 light-space 坐标、形状衰减、shadow map 与材质输出串成一条 shader 管线,并说明 lookup texture 的优化价值
直觉引入:把一盏灯当成摄影师的手
普通光照模型更关心“现实中这束光会怎样传播”。电影灯光更先问:“观众应该看见谁?我想让这块墙变暖,还是让眼睛里留下一个小高光?”因此,一盏可用的实时灯不只是位置、颜色和强度,还要能控制它照谁、照成什么形状、被挡住后留下什么,以及投影什么纹理。
本章把这组控制集中到一个简化的 uberlight shader 中。它不是一个固定的灯具类型,而是一套可组合的控制面:顶点阶段把对象送进 light space,片元阶段依次计算 shaping、距离衰减、cookie 和 shadow,再独立混合 ambient、diffuse、specular。这样“物理输入”和“艺术决定”之间有了清晰的接口。
1. cinematic lighting 先控制叙事,再近似物理
↡把一盏灯的选择、颜色、形状、阴影和投影纹理集中到同一套 shader 控制中的可编程光源 的关键不是多一个光源类,而是把艺术指导需要调的旋钮放在同一处。它可以让一盏灯只选择某些对象、让 ambient/diffuse/specular 拥有独立权重,也可以故意把阴影移到更能服务构图的位置。
可以把控制面拆成五个问题:
| 控制面 | 它回答的问题 | 一个可见结果 |
|---|---|---|
| selection | 哪些对象或材质接收这盏灯? | 主角亮、背景退后 |
| color | 光的色相和各分量权重是什么? | 暖脸、冷背景 |
| shaping | 光斑落在哪个区域,边缘多软? | 窗光、门洞光 |
| shadowing | 谁投影、谁接收,暗部如何着色? | 轮廓仍保留细节 |
| texturing | 光束是否带有 cookie 图案? | 百叶窗、树叶影 |
selection 与 color:不是只有一个 intensity
↡决定哪些对象、材质或光照通道参与某盏灯计算的选择控制 可以通过对象标签、材质通道或灯光列表完成。一个简单而实用的做法,是在材质上保留一个 lightMask,让角色的主灯为 1,背景为 0.2;这样不需要把所有灯光都复制成不同的 shader。
↡把光的色相以及 ambient、diffuse、specular 等分量权重分开调节的光照控制 也不等于把 RGB 乘到最终颜色。艺术家可能希望暖色只进入 diffuse,让角色皮肤变暖;同时保留较冷的 specular,让眼睛和金属仍有清晰的轮廓。分量独立,才能避免“暖光一开,所有东西都变成橙色”。
2. shaping:从 omni 的圆润衰减到 barn 的窗口光
↡只按光源到表面的距离向四周衰减、缺少方向性边界的点状光形状 是很好的起点:计算简单,近处亮、远处暗。但它无法表达一束从窗户斜进来的光。↡用截断体积和可调横截面把光限制成窗口、门洞等方向性光斑的形状 则把光源前方限制为一个截断的广义金字塔。
barn 的横截面可以用 superellipse 表达。令 a、b 控制宽高,p 控制从圆到方的过渡:
当 时,边界接近椭圆;提高 ,中段会变得更平、角落更像矩形。再加上 near/far 截断与 soft edge,就能把边界做得硬或软;shear 则能把它倾斜成一束“从窗外斜进来”的光。
↡描述 superellipse 边界圆润或方正程度的指数;指数越大,横截面越接近矩形 的价值在于它给了艺术家一个连续旋钮,而不是只能在“圆形”和“方形”之间切换。实际 shader 不必直接解几何交点,可以把归一化的 light-space 坐标送进距离函数,再把距离映射成 0 到 1 的软边权重。
3. distance shaping 与 cookie:控制光到哪里结束
↡在光斑边界和光源远近处分别设置衰减、截断与 soft edge 的形状控制 负责回答“光到哪里开始变暗、到哪里归零”。一种稳定的思路是分开计算 near、far 和横截面边界:
float edgeWeight(float distanceToEdge, float softness) {
return smoothstep(0.0, max(softness, 0.0001), distanceToEdge);
}
float distanceWeight(float z, float nearZ, float farZ, float softZ) {
float nearFade = smoothstep(nearZ, nearZ + softZ, z);
float farFade = 1.0 - smoothstep(farZ - softZ, farZ, z);
return nearFade * farFade;
}
float barnWeight(vec3 lightPos, vec4 shape) {
float boundary = 1.0 - length(vec2(lightPos.x, lightPos.y) / shape.xy);
return edgeWeight(boundary, shape.w) * distanceWeight(
lightPos.z, shape.z, shape.z + 1.0, shape.w
);
}这里的数值只是教学版。工程实现可以把圆形距离换成 superellipse 距离,把 near/far 的范围放到统一的光源参数中。重要的是:先得到一个可解释的 shaping 权重,再让灯光师知道每个旋钮到底改变了哪一段边缘。
↡把一张投影纹理采样到光源空间,用来模拟百叶窗、树叶或图案化光束的纹理 是 shaping 的二维补充。它不负责真实遮挡,而是负责“光束上有什么图案”。因此,cookie 可以与 shadow map 同时使用:shadow map 提供遮挡,cookie 提供颜色或形状。两者都通过 light space 坐标采样,但语义不同,调试时应分开显示。
4. shadowing:阴影是材质决定的输入
↡决定投影者、接收者以及被遮挡区域如何保留 diffuse、specular 和色相的阴影控制 把“是否被挡住”与“遮挡后变成什么”分开。可以选择性地让某些对象投影或接收阴影,也可以把投影位置略微移动,避免真实阴影破坏画面构图。
最常见的误解是:shadow map 为 0 就把漫反射和高光都乘成 0。更可控的组合是:
这里 是 shadow density, 是独立权重。暗部仍可保留一部分 diffuse;如果高光会让阴影看起来“漏光”,就把 设为 0。阴影的色相也可以通过给 乘一层冷色或暖色来控制,而不必使用纯黑。
↡把对象送进光源投影坐标并采样深度遮挡信息的纹理;它提供遮挡答案,不规定最终阴影颜色 仍然是很实用的实现,因为它简单、灵活,并且容易和 cookie 共享投影坐标。真正需要设计的是“如何消费这条遮挡信息”:选择、偏移、密度和颜色都应当是 shader 的显式参数。
5. 从 light space 到材质输出
uberlight 的顶点阶段主要准备坐标,片元阶段才组合形状、距离、cookie 与 shadow。下面是压缩后的 Cg/GLSL 风格伪代码,重点是数据流而非某个 API 的精确语法:
// vertex
out vec3 lightPos;
out vec4 shadowCoord;
out vec2 cookieCoord;
void vertexMain(vec3 position) {
vec4 p = lightMatrix * vec4(position, 1.0);
lightPos = p.xyz / p.w;
shadowCoord = shadowMatrix * vec4(position, 1.0);
cookieCoord = p.xy / p.w * 0.5 + 0.5;
}// fragment
float shape = barnWeight(lightPos, shapeParams);
float cookie = texture(cookieMap, cookieCoord).r;
float blocked = texture(shadowMap, shadowCoord.xyz).r;
float shadow = mix(0.0, shadowDensity, blocked);
vec3 ambientPart = ambientColor * ambientWeight;
vec3 diffusePart = diffuseLambert() * shape * cookie * (1.0 - shadow);
vec3 specularPart = showSpecular ? specularBRDF() * specularWeight : vec3(0.0);
return ambientPart + diffusePart + specularPart;代码中的 blocked 只是一个教学占位:真实 shadow map 需要比较接收点深度与采样深度,并处理 bias、过滤和光源类型。即使遮挡判定换了,后面的艺术控制仍然可以保持不变。这样调试时可以独立问:“是 shadow map 错了,还是 shadow color / density 的组合错了?”
6. 把解析函数换成 lookup texture
↡把 shaping、距离等昂贵函数预计算成纹理,用一次或少量采样替代片元中的重复计算 可以把性能问题变成纹理带宽问题。章节给出的方向是:把解析的 shaping 与距离函数烘焙为查表图;在镜头和对象保持静止时缓存它们,甚至用两张图分别承载不同的衰减维度。
这个优化有三个前提:
- 函数要稳定:只有参数不变或变化很慢时,预计算才不会频繁失效。
- 坐标要一致:lookup 的 UV 必须和 light space 的 shaping 坐标严格对应,否则边界会漂移。
- 先对照画面:lookup 近似应与 analytic 版本比较边界误差、帧时间和纹理采样成本,而不是只看平均 FPS。
不要把 lookup 当成“永远更快”。动态光源、动态对象或频繁变化的 soft edge 会增加更新纹理的成本;静态镜头中,它才有机会把解析函数从每个片元的热路径移到预计算阶段。
7. 实验:用一盏灯做一次电影化决策
猜一猜:把 barn shaping 的 soft edge 调硬,光斑会先改变边界还是中心亮度?把 shadow density 降低后,为什么画面不是“完全没有阴影”?最后切换 lookup texture,画面应保持近似不变,变化的主要应该是计算路径。
Cinematic Lighting Lab
先预测:硬化 barn 的 soft edge,会先改变边界还是中心亮度?
观察:soft edge 主要改变边界,shadow density 改变暗部强度,lookup 只改变计算路径。
实验中,omni 用径向范围表达光,barn 用方向性截断体积表达光斑。soft edge 只改变边界的过渡宽度;shadow density 只改变遮挡贡献的权重;cookie 在光源空间中加入图案;关闭 specular 则模拟“阴影里不要出现会抢戏的高光”。最后两个按钮把“艺术选择”和“性能选择”并列出来:lookup 不应该改变你已经调好的光型。
小结:把光照变成可解释的控制面
- uberlight 把 selection、color、shaping、shadowing 和 texturing 放进同一套可编程接口。
- omni 适合径向衰减;barn shaping 用 near/far、宽高、soft edge、superellipse 和 shear 造出方向性光斑。
- shadow map 只提供遮挡信息;暗部仍可保留 ambient、diffuse,或关闭 specular。
- light space 坐标同时服务 shaping、cookie 和 shadow map,但三者语义要分开调试。
- lookup texture 能把稳定的解析函数移出片元热路径;动态场景要权衡缓存更新成本。
练习
问题 1|改 Demo 代码 把实验从 barn 切换为 omni,再逐步把 soft edge 调到 0 和 1。说明哪个参数改变了光斑边界,哪个参数改变了中心亮度。
问题 2|推导题 某接收点的 ambient、diffuse、specular 分量分别为 (0.08, 0.30, 0.18),shadow density 为 0.6,并决定阴影中关闭 specular。按本章的组合式,写出它的相对贡献。
问题 3|独立实现题 为一个静态窗光实现 analytic 与 lookup 两条路径。除了截图,你会记录哪些指标来确认优化没有改变视觉结果?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- uberlight
- 把选择、颜色、形状、阴影和投影纹理集中到同一套 shader 控制中的可编程光源。
- light selection
- 决定哪些对象、材质或光照通道参与某盏灯计算的选择控制。
- light color
- 把色相以及 ambient、diffuse、specular 等分量权重分开调节的控制。
- omni light
- 按距离向四周衰减、缺少方向性边界的点状光形状。
- barn shaping
- 用截断体积和可调横截面把光限制成窗口、门洞等方向性光斑的形状。
- superellipse
- 控制横截面从圆润到方正过渡的指数化边界。
- distance shaping
- 设置光斑近端、远端、截断和 soft edge 的衰减控制。
- cookie
- 投影到光源空间中的纹理,用来模拟图案化的光束。
- cinematic shadowing
- 决定投影、接收以及暗部如何保留 diffuse、specular 和色相的阴影控制。
- shadow map
- 提供接收点遮挡答案的光源投影深度纹理。
- lookup texture
- 存放预计算函数值、用采样替代重复解析计算的纹理。