Skip to main content

Chapter 27: Local Illumination Models

局部光照模型回答一个直接问题:给定一个 draw call 中的网格片元、材质参数、灯光参数和相机位置,fragment shader 应怎样把这些输入变成当前像素的直接光照颜色。读完本章后,读者应能定位一个基础光照 shader 中每个向量、颜色项和强度项的来源,判断黑面、过曝、高光漂移、法线贴图反向和阴影过暗这类视觉结果来自哪条数据路径。

本章使用一个贯穿材料:一个 forward rendering draw call 正在绘制带有法线贴图的石质球体,场景中有一个点光源、一台相机、一张 albedo 纹理和一个可选 shadow term。这个 draw call 的 fragment shader 对每个 fragment 计算局部光照:从插值后的世界空间位置得到 light vector,从相机位置得到 view vector,从几何法线或法线贴图得到 normal,再把 diffuse、specular、ambient 和 emission 合成为输出颜色。

这里的“局部”指 shader 主要使用当前着色点附近可直接取得的数据。它计算光源到当前点、当前点到相机、当前点表面朝向之间的关系。全局光照、多次反弹、环境反射和基于微表面的物理材质模型会在后续章节展开。本章先建立最小可复查路径:一个 fragment 的输入是什么,公式中的量在什么坐标空间,输出颜色为什么会变亮、变暗、偏色或产生高光。

为了让后续排查有稳定入口,本章把局部光照拆成三层。第一层是数据层:材质、灯光、相机、法线和阴影可见性。第二层是数学层:点积、归一化、反射向量、半角向量、距离衰减和指数高光。第三层是工程层:uniform 或 buffer 如何进入 shader,纹理如何在线性颜色空间参与计算,多灯循环如何组织,以及工具捕获中应观察哪些资源绑定和 shader 输入。

27.1 灯光模型的基本构成与目的

灯光模型的目的,是在 fragment shader 内把“表面受到多少光、朝向相机呈现什么颜色”转成数值。它的输入来自多个管线位置:顶点阶段提供世界空间位置和插值属性,材质系统提供 albedo、roughness 或 shininess 这类参数,灯光系统提供位置、方向、颜色和强度,相机系统提供 view vector,阴影 pass 或可见性系统提供 shadow term。局部光照模型把这些输入压缩成当前像素的直接光照估计。

贯穿材料中的石质球体可以被看成一组 fragment。每个 fragment 至少需要四类量:表面位置 P、表面法线 N、光照方向 L 和视线方向 VP 通常是世界空间位置,来自 vertex shader 输出的插值结果。N 可以来自模型法线,也可以由 normal map 在 tangent space 中解码后通过 TBN 矩阵转到世界空间。L 从当前点指向光源或使用方向光的反向方向。V 从当前点指向相机。只要这四个量所在坐标空间一致,点积和反射计算才有稳定意义。

下面的图描述本章讨论的 draw call 数据路径。它只覆盖局部直接光照,不展开 shadow map 生成、反射探针、屏幕空间反射和全局光照缓存。

这条路径的关键点在 fragment shader。Mesh attributes 只提供几何基础,Material textures 提供表面参数,Light buffer 提供光源参数,Camera data 提供观察方向,Shadow term 提供光源可见性。fragment shader 在同一个坐标空间中组织这些输入,然后写入 render target。如果捕获工具显示 albedo 纹理绑定正确、灯光 buffer 正确、fragment shader 输出仍然全黑,排查入口应先落到 NL、shadow term 和颜色空间上。

light vector 是从着色点指向光源的方向。对点光源,L = normalize(lightPosition - P);对方向光,L 通常直接来自灯光方向的反向。这个向量回答“光从表面的哪一侧到达”。当 dot(N, L) 为正,表面朝向光源;当结果小于或等于 0,表面背向光源,基础漫反射贡献为 0。

view vector 是从着色点指向相机的方向。对透视相机,V = normalize(cameraPosition - P);对正交相机,也可以使用统一 view direction。它参与 specular 计算,因为高光取决于观察方向。若相机位置使用了 object space,法线和灯光使用了 world space,高光会随物体移动产生漂移,这类问题在静态截图中可能表现为亮斑跟着模型坐标走。

normal 是当前 fragment 的表面朝向。几何法线来自顶点属性并经过插值,normal map 提供每个像素的细节朝向。插值后的法线和 normal map 解码后的法线都要重新归一化。原因很具体:线性插值会改变向量长度,normal map 的存储范围从 [0, 1] 映射到 [-1, 1] 后也会受纹理压缩和采样影响。点积假设参与计算的是单位向量,长度偏差会直接改变亮度。

albedo 是材质在漫反射路径中的基础颜色。它描述表面在接受白光时反射出的颜色比例。石质球体的 albedo 纹理如果在 sRGB 格式中存储,采样进入 shader 后应以线性颜色参与光照计算。线性空间中的乘法才对应光能量比例;把 sRGB 数值直接相乘会让暗部和中间调出现偏差,最后再输出到 sRGB render target 或经过 tone mapping。

diffuse 表示表面朝各方向散射的基础光照贡献。Lambert 漫反射把这个贡献近似为 max(dot(N, L), 0) 乘以 albedo、灯光颜色、灯光强度和距离衰减。它适合表达粉笔、石头、未上光木材这类高光较弱的表面基础明暗。

specular 表示表面沿特定观察方向出现的镜面高光贡献。Phong 和 Blinn-Phong 使用反射向量或半角向量估计高光强度。specular 的视觉结果受视线方向、法线方向、灯光方向和 shininess 指数共同控制。shininess 越大,高光区域越窄;shininess 越小,高光区域越宽。

ambient 在基础局部光照模型中通常是一个低频补光项。它常用于让未被直接光照覆盖的区域保留可见形体。ambient 可以是常量、环境颜色、球谐光照或环境贴图预估;在本章的最小模型中,它是 ambientColor * albedo。它承担的工程作用是稳定基础可见性,同时也容易掩盖法线、阴影或曝光错误。排查直接光照时,可以临时关闭 ambient,观察 diffuse 和 specular 的真实贡献。

emission 是材质主动发光项。它通常直接加到输出颜色中,不依赖 NLV。屏幕上的霓虹标牌、魔法纹路和 UI 发光材质会使用 emission。局部光照模型中的 emission 只改变当前表面颜色;它要真正照亮周围物体,还需要后续全局光照、light injection、screen-space effect 或手动添加灯光。

这几个构成项可以按稳定顺序检查。先确认坐标空间一致,再确认向量已归一化,再确认 albedo 在线性空间,再确认 diffuse 和 specular 各自独立输出正确,最后叠加 shadow、ambient、emission 和曝光处理。这个顺序能把“公式错了”和“资源绑定错了”分开,也能把“数学量错误”和“颜色管线错误”分开。

27.2 Phong / Blinn-Phong 模型数学与视觉效果

Phong 和 Blinn-Phong 都是经验型局部高光模型。它们提供的是可控的视觉高光,输入少,计算路径短,适合教学、调试 shader、简单材质和非物理化风格渲染。它们不会描述真实微表面能量分布,也不会自动满足基于物理的能量守恒;本章使用它们建立 specular 的方向关系和 shader 实现路径。

Phong 模型使用 reflection vector。对当前 fragment,先把从点到光源的 L 和表面法线 N 放在同一空间,并归一化。入射方向可以写成 -L,反射向量 R = reflect(-L, N)。然后计算 max(dot(R, V), 0),再把结果提升到 shininess 指数。行内公式可以写成 specular = pow(max(dot(R, V), 0), shininess)。当相机方向 V 接近反射方向 R 时,高光强;当两者偏离时,高光快速下降。

这个模型的可视化结果很直观。把石质球体换成光滑塑料球,在点光源靠近相机的位置观察,Phong 高光会出现在法线反射方向接近视线的区域。移动相机时,高光会沿球面移动。提高 shininess 指数,高光会收缩并更集中;降低 shininess 指数,高光会扩散成大面积柔亮区域。若法线没有归一化,dot(R, V) 的分布会变形,高光可能被拉宽、压扁或在接缝处闪烁。

Blinn-Phong 使用 half vector。它先计算 H = normalize(L + V),这个向量表示光线方向和视线方向的中间方向。高光项用 max(dot(N, H), 0) 的指数形式计算,即 specular = pow(max(dot(N, H), 0), shininess)。当表面法线接近半角方向时,当前点就位于高光区域。这个写法减少了显式反射向量参与的直观复杂度,也常用于历史上的实时管线和简单 shader。

Phong 和 Blinn-Phong 的 shininess 数值没有一一对应关系。相同指数下,Blinn-Phong 的高光宽度通常和 Phong 不完全一致。工程中迁移模型时,应以视觉目标和调参范围重新标定。若旧 shader 使用 shininess = 32 的 Phong 高光,新 shader 改成 Blinn-Phong 后直接沿用数值,画面可能变得更亮或高光范围变化。稳定做法是固定测试球、固定相机、固定灯光距离,在同一曝光条件下比较高光宽度和峰值。

高光颜色也需要明确来源。传统模型常用一个 specularColorspecularStrength 控制高光强度。非金属材质通常有较低且接近白色的 specular,金属材质的高光颜色会更接近材质颜色。这里先采用简单参数:specularColor * lightColor * lightIntensity * specularTerm。后续 BRDF 章节会把 F0、roughness、metallic 和 Fresnel 放入更完整的材质系统。

Phong 和 Blinn-Phong 对输入方向的符号非常敏感。假设 L 定义为“从点指向光源”,GLSL 中 reflect(I, N)I 应表示入射方向,因此常写 reflect(-L, N)。若写成 reflect(L, N),高光会出现在错误半球,视觉上可能像光从背后照来。排查时可以直接输出 R * 0.5 + 0.5H * 0.5 + 0.5 到屏幕,观察向量场是否随灯光和相机按预期变化。

下面的简化片段展示 Phong 和 Blinn-Phong 的核心差异。代码假设 NLV 已经在世界空间归一化,albedo 和灯光颜色已经在线性空间。

float computePhongSpecular(vec3 N, vec3 L, vec3 V, float shininess)
{
vec3 R = reflect(-L, N);
return pow(max(dot(R, V), 0.0), shininess);
}

float computeBlinnPhongSpecular(vec3 N, vec3 L, vec3 V, float shininess)
{
vec3 H = normalize(L + V);
return pow(max(dot(N, H), 0.0), shininess);
}

这段代码回答的是“高光方向由哪个向量关系决定”。它没有处理距离衰减、阴影、能量归一化和多灯循环。将它接入真实 shader 时,需要把 specular term 乘以灯光颜色、强度、材质 specular 参数、attenuation 和 shadow visibility。若 shader 捕获中 V 长度接近 0,通常说明相机位置和 fragment 位置相同或坐标空间被混用;若 H 在某些像素上变成零向量,说明 LV 方向接近相反,需要在实现中做数值保护。

这两个模型的视觉边界也要写清。它们能表达方向性高光和基础明暗,适合低成本实时渲染和 stylized 材质。它们对粗糙度、Fresnel、几何遮蔽和多次散射的表达有限。对于金属、车漆、湿润皮肤和复杂透明材质,Phong 系列模型只适合作为调试或风格化近似。把它用于 PBR 材质时,应明确它承担的是“可控高光”角色,而 PBR 章节中的微表面 BRDF 才承担更严格的物理参数解释。

27.3 Lambert 漫反射与简单阴影贡献

Lambert 漫反射把表面亮度和光线入射角联系起来。核心量是 dot(N, L):当法线正对光源时结果接近 1,当法线与光线垂直时结果接近 0,当光源在表面背侧时结果为负。shader 中常用 max(dot(N, L), 0)clamp(dot(N, L), 0, 1) 取得可见漫反射贡献。这个值是局部光照中最稳定的基础诊断量。

在石质球体例子中,如果点光源位于球体右前上方,球面朝向该点的区域会获得较大的 NdotL,背向区域的 NdotL 接近 0。将 fragment shader 临时改为输出 vec3(NdotL),屏幕应显示一个平滑的灰度明暗分布。若灰度分布随相机旋转而变化,通常说明 NL 使用了 view space 与 world space 的混合路径;若灰度出现棋盘状断裂,入口通常在法线贴图采样、TBN 矩阵或 UV 接缝。

Lambert 的基础计算可以写成 diffuse = albedo * lightColor * lightIntensity * max(dot(N, L), 0)。这里每个乘法都有明确含义。albedo 决定表面颜色,lightColor 决定光源色,lightIntensity 决定光通量尺度的美术近似,NdotL 决定入射角贡献。这个式子应在线性颜色空间执行。若 albedo 纹理已经被 GPU 以 sRGB 方式解码,shader 中取得的是线性值;若纹理格式配置为普通 UNORM,shader 需要明确项目是否手动做了转换。

点光源还需要距离衰减。常见实时渲染写法使用 attenuation = 1 / (constant + linear * d + quadratic * d * d),其中 d 是光源到着色点的距离。这个模型方便美术控制,但参数组合会显著影响亮度范围。较大的 quadratic 项会让光快速衰减,较大的 constant 项会抬高近处和远处的整体亮度。为了数值稳定,分母通常需要一个很小的下限,例如 0.0001

物理上点光源强度会按距离平方衰减,实时项目常在艺术控制、曝光范围和场景尺度之间取舍。工程判断应看三个证据:灯光半径内的亮度梯度是否符合美术目标,远处表面是否仍有不合理亮度,HDR buffer 中是否出现过大的峰值。若 bloom 后处理被少量点光源触发成大片白斑,问题可能来自光强单位、衰减参数、tone mapping 或曝光,而非 Lambert 公式本身。

shadow term 表示当前点从光源视角看是否可见。为了让本章聚焦局部合成,可以把它看成一个范围在 [0, 1] 的可见性系数。1 表示光源完全可见,0 表示被遮挡,介于两者之间表示经过 PCF、软阴影或透明遮挡近似后的部分可见。简单直接光照可写成 (diffuse + specular) * attenuation * shadowVisibility。ambient 和 emission 是否乘 shadow 取决于项目风格;常见基础做法是 shadow 只影响直接光照。

这个乘法顺序的可观察结果很清楚。将 shadowVisibility 固定为 1,球体只受角度和距离影响。将它固定为 0.5,直接光照减半但 ambient 仍保留形体。将它固定为 0,直接漫反射和高光消失,若 ambient 存在,物体仍有低频可见度。这个测试能把“阴影计算过暗”和“灯光公式过暗”拆开。

在真实 frame 中,shadow term 可能来自 shadow map 深度比较、屏幕空间阴影、体积阴影或离线烘焙可见性。对本章的局部模型来说,需要它最终进入 fragment shader 时的语义清楚:它表示直接光源的可见性,和 albedo、AO、最终曝光处于不同层级。若 AO 和 shadow 都乘到 direct light、ambient 和 albedo 上,暗部可能被重复压低。排查时应分别输出 shadowVisibility、AO、ambient 和 direct light。

Lambert 与 shadow 的最小实现可以写成下面的片段。代码展示计算顺序:先得到方向和距离,再算角度贡献,再算距离衰减,再乘 shadow visibility。

vec3 computeLambertDirect(
vec3 worldPos,
vec3 N,
vec3 albedo,
vec3 lightPos,
vec3 lightColor,
float lightIntensity,
vec3 attenuationTerms,
float shadowVisibility)
{
vec3 toLight = lightPos - worldPos;
float distanceToLight = length(toLight);
vec3 L = toLight / max(distanceToLight, 0.0001);

float NdotL = max(dot(N, L), 0.0);
float d = distanceToLight;
float denom = attenuationTerms.x + attenuationTerms.y * d + attenuationTerms.z * d * d;
float attenuation = 1.0 / max(denom, 0.0001);

vec3 directDiffuse = albedo * lightColor * lightIntensity * NdotL;
return directDiffuse * attenuation * shadowVisibility;
}

这段代码的输入边界需要明确。N 应为归一化后的世界空间法线;albedolightColor 和返回值应在线性颜色空间;shadowVisibility 已经由其他路径算好;attenuationTerms 使用项目定义的单位。若把这段函数用于方向光,应去掉距离计算和 attenuation,直接使用固定方向 L。若用于聚光灯,还需要额外乘以 spotlight cone factor。

一个稳定的 Lambert 排查顺序是:输出 N * 0.5 + 0.5 检查法线方向;输出 L * 0.5 + 0.5 检查光线方向;输出 NdotL 检查入射角;输出 attenuation 检查距离范围;输出 shadowVisibility 检查可见性;最后输出 directDiffuse 检查颜色和强度。这个顺序把几何方向、灯光参数、阴影资源和颜色合成分开,能快速定位大多数基础光照错误。

27.4 在 Shader 中实现基本光照模型

在 shader 中实现基本局部光照,要把数学公式放回 GPU 资源和 API 状态中。一个 forward pass 通常把 per-object 数据放在 uniform buffer 或 push constants 中,把 per-frame 相机数据放在 camera buffer 中,把灯光数组放在 light buffer 中,把 albedo、normal、emission 等纹理绑定到 descriptor、argument buffer 或 sampler binding。fragment shader 对每个 fragment 读取这些资源并执行局部光照合成。

统一坐标空间是实现入口。常见做法是在 vertex shader 中输出 world position、world normal、world tangent 和 UV。fragment shader 从这些插值量重建 TBN,把 normal map 解码到 world space,然后用 world-space light position 和 camera position 计算 LV。另一种做法是在 view space 中完成所有计算。两种路径都可行,关键是 PNLV 同属一个空间,且代码命名反映这个空间,例如 worldNormalworldPosviewDir

normal map 的实现需要把纹理数据转成可参与点积的单位向量。采样得到的 RGB 通常在 [0, 1] 范围,解码为 normalTS = normalize(sample.rgb * 2.0 - 1.0)。其中 TS 表示 tangent space。再使用 TBN * normalTS 转为 world normal。若模型 tangent 的 handedness、bitangent 方向或纹理 green channel 约定不同,凹凸会反向,表现为光从上方照射时细节像被压入或鼓出方向反了。工具中可以检查 tangent attribute、normal map 格式和 shader 解码分支。

颜色空间同样属于 shader 实现的一部分。albedo 纹理和 emission 纹理通常是 sRGB 内容,normal、roughness、metallic、AO 和 mask 通常是线性数据。采样 albedo 后参与光照的值应为线性颜色,lighting buffer 也应保存线性结果。最终输出到 swapchain 前再交给 tone mapping 和 sRGB 编码。若把 normal map 标成 sRGB,解码后的法线方向会被 gamma 曲线扭曲,表现为表面细节亮度异常。

下面的 GLSL 风格片段展示一个最小 forward 局部光照函数。它包含 albedo、normal map、Lambert diffuse、Blinn-Phong specular、attenuation 和 shadowVisibility。ambient 与 emission 放到外层合成,便于多灯循环保持清楚。代码为教学型最小实现,真实引擎会把结构体布局、绑定位置、灯光裁剪和材质分支放入更完整的框架。

struct Light
{
vec3 position;
vec3 color;
float intensity;
vec3 attenuation;
};

struct Material
{
vec3 albedo;
vec3 specularColor;
vec3 emission;
float shininess;
};

vec3 decodeWorldNormal(vec3 normalSample, mat3 TBN)
{
vec3 normalTS = normalize(normalSample * 2.0 - 1.0);
return normalize(TBN * normalTS);
}

vec3 applyDirectLight(
vec3 worldPos,
vec3 worldNormal,
vec3 cameraPos,
Material material,
Light light,
float shadowVisibility)
{
vec3 N = normalize(worldNormal);
vec3 toLight = light.position - worldPos;
float d = length(toLight);
vec3 L = toLight / max(d, 0.0001);
vec3 V = normalize(cameraPos - worldPos);
vec3 H = normalize(L + V);

float NdotL = max(dot(N, L), 0.0);
float specTerm = pow(max(dot(N, H), 0.0), material.shininess);

float denom = light.attenuation.x + light.attenuation.y * d + light.attenuation.z * d * d;
float attenuation = 1.0 / max(denom, 0.0001);

vec3 diffuse = material.albedo * light.color * light.intensity * NdotL;
vec3 specular = material.specularColor * light.color * light.intensity * specTerm;
return (diffuse + specular) * attenuation * shadowVisibility;
}

这段函数的关键点是数据归属清楚。worldPosworldNormal 来自当前 draw call 的插值输出;cameraPos 来自 frame 级 uniform;Material 来自材质常量和纹理采样;Light 来自灯光 buffer;shadowVisibility 来自阴影路径;返回值是线性空间的直接光照颜色。若某一项来自错误资源,视觉问题会沿这条归属关系暴露。比如 specular 全屏发亮,优先看 specTermshininessspecularColor;diffuse 方向错误,优先看 NL 和坐标空间。

多灯循环把单灯函数重复应用到灯光数组。forward 渲染中最直接的形式是 for 循环累加每个影响当前物体或当前 tile 的灯光贡献。灯光数量较少时,这种写法清楚直接;灯光数量较多时,fragment shader 的 ALU、uniform 读取和分支成本会上升。工程实现常用 per-object light list、tiled forward、clustered forward 或 deferred shading 控制每个 fragment 需要遍历的灯光数量。本章要求读者能看懂单灯到多灯的合成关系。

vec3 accumulateLights(
vec3 worldPos,
vec3 worldNormal,
vec3 cameraPos,
Material material,
Light lights[MAX_LIGHTS],
int lightCount)
{
vec3 color = material.albedo * vec3(0.03) + material.emission;

for (int i = 0; i < lightCount; ++i)
{
float shadowVisibility = 1.0;
color += applyDirectLight(worldPos, worldNormal, cameraPos, material, lights[i], shadowVisibility);
}

return color;
}

这个示例把 ambient 和 emission 放在循环外,使它们不会随灯光数量重复累加。若每个灯光都有 shadowVisibility,阴影采样应和对应灯光绑定;若只有主光有阴影,其它灯光的 shadowVisibility 可以固定为 1。多灯输出过亮时,检查顺序是灯光数量、每灯强度、attenuation、HDR buffer 峰值、tone mapping 和 bloom threshold。

shader 资源布局应配合调试。相机 buffer 应能在捕获工具中看到 camera position 或 view matrix;light buffer 应能看到位置、颜色、强度和衰减参数;材质绑定应能区分 sRGB albedo 与线性 normal map;pipeline state 应能显示当前 fragment shader、render target 格式和 blend 状态。若工具支持 pixel history 或 shader debug,可以选中异常像素,跟踪 albedo、NdotL、specTerm、attenuation 和 shadowVisibility 的中间值。

API 差异主要体现在绑定模型和坐标约定。OpenGL 和 WebGL 常通过 uniform、UBO 与 texture unit 组织资源;Vulkan 和 Direct3D 12 常通过 descriptor set、root signature 或 descriptor heap 组织资源;Metal 常通过 argument buffer、buffer index 和 texture index 组织资源。shader 代码中的数学关系保持相同,但资源生命周期、绑定频率、坐标深度范围和纹理格式声明需要按 API 检查。把某个 API 的默认状态直接带到另一个 API,容易出现 normal matrix、sRGB、depth 和 sampler 行为差异。

基本光照 shader 的性能风险集中在三类路径。第一类是纹理采样,尤其是 albedo、normal、specular、emission 多张纹理叠加时的带宽和 cache 行为。第二类是 per-fragment 多灯循环,灯光数量和阴影采样会放大成本。第三类是分支和变体,例如是否启用 normal map、是否启用 emission、是否启用 shadow。小型项目可以用分支保持材质灵活,大型项目通常会使用 shader variant、material key 或 clustered light list 控制运行时成本。

一个可复用的实现检查顺序是:先固定一个白色 albedo 和一个方向明确的灯光,确认 Lambert 灰度正确;再加入真实 albedo,确认颜色空间正确;再加入 specular,确认高光随相机移动;再加入 normal map,确认细节方向正确;再加入 attenuation,确认距离梯度正确;再加入 shadow,确认直接光被可见性调制;最后加入多灯循环和 emission,确认累加路径没有重复项。这个顺序能把每个视觉结果绑定到一个新增输入或状态变化。

本章建立的是局部光照的基础工作模型。它让一个 fragment 的颜色可由 PNLV、材质、灯光和可见性逐步复查。后续 BRDF、Fresnel、subsurface scattering 和 shadowing 章节会扩展材质与光传输模型,但它们仍然需要这条基础路径:数据进入 shader,向量关系决定角度项,资源和颜色空间决定最终输出,工具证据帮助定位错误层级。

最小自检任务

给定一个 forward pass 中的单个 fragment,所有值已经在线性颜色空间:N = (0, 0, 1)L = (0, 0, 1)V = (0, 0, 1)albedo = (0.8, 0.4, 0.2)lightColor = (1, 1, 1)lightIntensity = 2.0attenuation = 0.25shadowVisibility = 0.5ambientColor = (0.03, 0.03, 0.03)specularColor = (0.04, 0.04, 0.04),Blinn-Phong 的 HN 相同,shininess = 32emission = (0, 0, 0)。请计算该 fragment 的输出线性颜色,并说明如果结果在屏幕上全黑,应按什么顺序排查。

答案要点

dot(N, L) = 1,Lambert 漫反射角度贡献为 1。直接 diffuse 为 albedo * lightColor * lightIntensity * attenuation * shadowVisibility,也就是 (0.8, 0.4, 0.2) * 2.0 * 0.25 * 0.5 = (0.2, 0.1, 0.05)。Blinn-Phong 中 dot(N, H) = 1,因此 pow(1, 32) = 1,直接 specular 为 specularColor * lightColor * lightIntensity * attenuation * shadowVisibility = (0.01, 0.01, 0.01)。ambient 为 albedo * ambientColor = (0.024, 0.012, 0.006)。emission 为 0,最终线性颜色为 (0.234, 0.122, 0.066)

若屏幕显示全黑,排查顺序应从最接近 fragment shader 的可观察量开始。先输出常量颜色确认 draw call、pipeline 和 render target 有写入;再输出 albedo 确认纹理绑定与 sRGB 解码;再输出 N * 0.5 + 0.5L * 0.5 + 0.5dot(N, L) 确认坐标空间与归一化;再固定 shadowVisibility = 1attenuation = 1,确认阴影与距离衰减没有压低直接光;再单独输出 diffuse、specular、ambient 和 emission,确认合成项没有被重复乘暗;最后检查 tone mapping、曝光、blend state 和 render target 格式。

这个任务对应的管线阶段是 fragment shader,资源状态包括材质纹理、相机数据、灯光数据、阴影可见性和 render target。数学关系集中在 dot(N, L)dot(N, H)、attenuation 与 shadowVisibility。可观察结果应是一块带有暖色 albedo 倾向的非黑输出;若输出全白,优先检查 lightIntensity、attenuation、HDR 范围和 tone mapping;若高光位置错误,优先检查 VH、normal map 解码和坐标空间。

本章知识点总结

  • 局部光照:局部光照模型用当前 fragment 的表面、灯光、相机和材质输入估计直接光照颜色。
  • 坐标空间PNLV 必须处在同一坐标空间,点积和反射计算才有稳定意义。
  • 法线归一:插值法线和 normal map 解码结果都应重新归一化,向量长度偏差会直接改变亮度。
  • Albedo:albedo 表示漫反射路径的基础反射颜色,应在线性颜色空间参与光照乘法。
  • Lambert:Lambert 漫反射用 max(dot(N, L), 0) 表达表面朝向光源的程度。
  • Phong:Phong 高光使用 reflection vector 和 view vector 的夹角估计镜面亮斑。
  • Blinn-Phong:Blinn-Phong 使用 half vector 与 normal 的夹角估计高光,shininess 控制高光宽度。
  • 距离衰减:点光源 attenuation 把灯光距离转成亮度梯度,参数会影响 HDR 峰值和视觉半径。
  • 阴影项:shadowVisibility 表示直接光源可见性,通常调制 diffuse 和 specular 的直接光贡献。
  • Ambient:ambient 提供低频补光,可在调试时临时关闭以观察 direct light 的真实贡献。
  • Emission:emission 直接加入当前表面输出颜色,它本身不会照亮周围物体。
  • Shader实现:基本光照 shader 应明确资源来源、颜色空间、normal map 解码、多灯累加和输出 render target。
  • 排查顺序:稳定排查从常量输出开始,依次检查 albedo、法线、灯光方向、角度项、衰减、阴影和最终合成。