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 先控制叙事,再近似物理

的关键不是多一个光源类,而是把艺术指导需要调的旋钮放在同一处。它可以让一盏灯只选择某些对象、让 ambient/diffuse/specular 拥有独立权重,也可以故意把阴影移到更能服务构图的位置。

uberlight:一盏可编程的电影灯uberlightmood + storyselection谁被照亮color色相与权重shaping光斑边界shadowing暗部与投影texturing / cookie
cinematic lighting 把物理正确性让位给叙事控制:先决定照谁,再决定光的颜色、形状、阴影与纹理。

可以把控制面拆成五个问题:

控制面它回答的问题一个可见结果
selection哪些对象或材质接收这盏灯?主角亮、背景退后
color光的色相和各分量权重是什么?暖脸、冷背景
shaping光斑落在哪个区域,边缘多软?窗光、门洞光
shadowing谁投影、谁接收,暗部如何着色?轮廓仍保留细节
texturing光束是否带有 cookie 图案?百叶窗、树叶影

selection 与 color:不是只有一个 intensity

可以通过对象标签、材质通道或灯光列表完成。一个简单而实用的做法,是在材质上保留一个 lightMask,让角色的主灯为 1,背景为 0.2;这样不需要把所有灯光都复制成不同的 shader。

也不等于把 RGB 乘到最终颜色。艺术家可能希望暖色只进入 diffuse,让角色皮肤变暖;同时保留较冷的 specular,让眼睛和金属仍有清晰的轮廓。分量独立,才能避免“暖光一开,所有东西都变成橙色”。

2. shaping:从 omni 的圆润衰减到 barn 的窗口光

是很好的起点:计算简单,近处亮、远处暗。但它无法表达一束从窗户斜进来的光。 则把光源前方限制为一个截断的广义金字塔。

从 omni 到 barn:把光限制成叙事形状omni light径向衰减没有方向性边界barn lightsuperellipsewidth / heightsoft edgenear / far 截断window cheatshear → 像门窗不必服从真实灯具
barn light 是一个可被艺术指导的截断体积:宽高、软边、shear 都能把光斑塑造成窗户或门洞。

barn 的横截面可以用 superellipse 表达。令 ab 控制宽高,p 控制从圆到方的过渡:

xap+ybp=1\left|\frac{x}{a}\right|^p+\left|\frac{y}{b}\right|^p=1

p=2p=2 时,边界接近椭圆;提高 pp,中段会变得更平、角落更像矩形。再加上 near/far 截断与 soft edge,就能把边界做得硬或软;shear 则能把它倾斜成一束“从窗外斜进来”的光。

的价值在于它给了艺术家一个连续旋钮,而不是只能在“圆形”和“方形”之间切换。实际 shader 不必直接解几何交点,可以把归一化的 light-space 坐标送进距离函数,再把距离映射成 0 到 1 的软边权重。

3. distance shaping 与 cookie:控制光到哪里结束

负责回答“光到哪里开始变暗、到哪里归零”。一种稳定的思路是分开计算 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:阴影是材质决定的输入

把“是否被挡住”与“遮挡后变成什么”分开。可以选择性地让某些对象投影或接收阴影,也可以把投影位置略微移动,避免真实阴影破坏画面构图。

最常见的误解是:shadow map 为 0 就把漫反射和高光都乘成 0。更可控的组合是:

Cshadow=AwA+D(1s)wD+SwSC_{\text{shadow}}=A\cdot w_A+D\cdot(1-s)\cdot w_D+S\cdot w_S

这里 ss 是 shadow density,wA,wD,wSw_A,w_D,w_S 是独立权重。暗部仍可保留一部分 diffuse;如果高光会让阴影看起来“漏光”,就把 wSw_S 设为 0。阴影的色相也可以通过给 (1s)(1-s) 乘一层冷色或暖色来控制,而不必使用纯黑。

仍然是很实用的实现,因为它简单、灵活,并且容易和 cookie 共享投影坐标。真正需要设计的是“如何消费这条遮挡信息”:选择、偏移、密度和颜色都应当是 shader 的显式参数。

5. 从 light space 到材质输出

顶点准备坐标,片元组合光照贡献surface法线 / 材质light space坐标投影shapebarn / distanceshadowmap / cookieoutputA + D + S阴影里也可保留 diffuse,关闭 specular,或改变 shadow color“被挡住”是输入,不是唯一的视觉结果
顶点阶段准备 light space 坐标,片元阶段把多个艺术控制项合并;shadow map 只是提供遮挡信息,阴影如何影响材质仍由 shader 决定。

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 与距离函数烘焙为查表图;在镜头和对象保持静止时缓存它们,甚至用两张图分别承载不同的衰减维度。

这个优化有三个前提:

  1. 函数要稳定:只有参数不变或变化很慢时,预计算才不会频繁失效。
  2. 坐标要一致:lookup 的 UV 必须和 light space 的 shaping 坐标严格对应,否则边界会漂移。
  3. 先对照画面:lookup 近似应与 analytic 版本比较边界误差、帧时间和纹理采样成本,而不是只看平均 FPS。
同一条衰减曲线:解析计算 vs lookup textureanalytic每个片元重复函数shape + distance + cookielookup map预计算后一次采样静态镜头可缓存并复用pre-bake
把昂贵的解析函数换成纹理查表,能减少片元计算;镜头和对象不动时,还可以缓存 shaping map。

不要把 lookup 当成“永远更快”。动态光源、动态对象或频繁变化的 soft edge 会增加更新纹理的成本;静态镜头中,它才有机会把解析函数从每个片元的热路径移到预计算阶段。

7. 实验:用一盏灯做一次电影化决策

猜一猜:把 barn shaping 的 soft edge 调硬,光斑会先改变边界还是中心亮度?把 shadow density 降低后,为什么画面不是“完全没有阴影”?最后切换 lookup texture,画面应保持近似不变,变化的主要应该是计算路径。

Cinematic Lighting Lab

先预测:硬化 barn 的 soft edge,会先改变边界还是中心亮度?

可交互
screen space previewanalytic shapingLsubject:ambient + diffuse + specularshadow 可改变密度 / 色相,不必简单变黑

观察: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 的衰减控制。
cinematic shadowing
决定投影、接收以及暗部如何保留 diffuse、specular 和色相的阴影控制。
shadow map
提供接收点遮挡答案的光源投影深度纹理。
lookup texture
存放预计算函数值、用采样替代重复解析计算的纹理。

资料与写作方式声明

本章以GPU Gems 1 · Chapter 10. Cinematic Lighting公开完整正文核定章节范围、事实坐标与时代语境,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…