← ALL ARTICLES

直接光照

全库目录221
本文目录16

给灯光增加强度以后,地面亮了,高光和阴影却未必按同样的比例变化。因为一个表面的直接光照同时取决于光源发出了多少光、光怎样到达表面,以及材质把它反射到哪里。光源单位约束输入,距离和可见性约束传播,BRDF约束表面响应;三者需要分别求值后组合。

光源模型

Directional Light

方向光假设光源无限远,所有点使用同一个入射方向,不随位置衰减。太阳在场景尺度下常用方向光近似。

它通常影响整个相机可见区域。Forward Rendering 会在物体材质中计算;Deferred Rendering 常用一次 Fullscreen Pass 或 Compute Pass。

Point Light

点光源从一个位置向所有方向发光。在与光线垂直的接收面上,各向同性点光的照度随距离平方衰减;倾斜面还需乘入射余弦。若总光通量为 Φ\Phi,均匀分到 4π4\pi 立体角,则光强 I=Φ/(4π)I=\Phi/(4\pi),距离 rr 处的法向照度为 I/r2I/r^2:

E∝1r2E\propto\frac{1}{r^2}

r=0r=0 时会发散,引擎需要最小半径、光源尺寸或平滑衰减。为了性能,还会设置有限影响范围,在边界前平滑降到零。

只使用线性衰减虽然容易调,但材质和灯光在不同尺度下不稳定。物理单位工作流应结合引擎的 Light Unit 和曝光约定。

Spot Light

聚光灯在点光源基础上增加方向和锥角。通常有 Inner Cone 和 Outer Cone,在两者之间做平滑过渡。

设灯轴指向灯外,qq 是灯到着色点的单位方向,c=q⋅axisc=q\cdot axis。内外半角满足 θinner<θouter\theta_{inner}<\theta_{outer},所以 cinner>couterc_{inner}>c_{outer};可取 t=saturate[(c−couter)/(cinner−couter)]t=\mathrm{saturate}[(c-c_{outer})/(c_{inner}-c_{outer})],用 t2(3−2t)t^2(3-2t) 平滑过渡。内外角相同时使用硬阈值,避免零分母。这个锥权重只定义形状;保持总光通量还需按角分布重新归一化。

光照贡献可以拆成:

text
距离衰减 × 角度衰减 × 可见性 × BRDF × NdotL

每一项都应单独可视化,方便判断问题来自灯光范围、阴影还是材质。

Area Light

真实面光源的每个表面点都能发光,需要对光源面积积分。它会产生随距离变化的软阴影和具有形状的高光。

实时近似包括:

  • Lightmap 烘焙;
  • 多点光源近似;
  • Linearly Transformed Cosines(LTC)估计多边形光源对微表面 BRDF 的积分;
  • 光线追踪直接采样面积光;
  • 特定形状的解析近似。

仅把 Point Light 的阴影模糊,并不能得到完整面积光响应。高光形状和能量分布也要改变。

LTC 的执行过程

线性变换余弦(Linearly Transformed Cosines,LTC)用一个经过线性变换的余弦分布近似 GGX 镜面波瓣。余弦分布对多边形的积分有高效解析形式,因此运行时可以把矩形灯变换到 LTC 空间后做边积分。

离线阶段按 Roughness 和 N⋅VN\cdot V 拟合两张 LUT:

  • 逆变换矩阵的参数;
  • 幅度、Fresnel 和几何修正参数。

运行时流程是:

  1. 用 N\mathbf N 和 V\mathbf V 建立局部正交基。
  2. 以 Roughness、N⋅VN\cdot V 查询 LTC LUT。
  3. 把矩形灯四个顶点变换到着色点局部空间。
  4. 应用 LTC 逆矩阵,把 GGX 波瓣变成余弦波瓣。
  5. 对归一化后的多边形边做积分,并乘回幅度和 Fresnel。

为使表约定可检查,以下HLSL使用公开LTC演示中的64×64纹理布局:matrixParams 解码为按行构造的矩阵

M−1=(px0pz010py0pw).M^{-1}=\begin{pmatrix}p_x&0&p_z\\0&1&0\\p_y&0&p_w\end{pmatrix}.

GLSL的三向量矩阵构造按列,直接照抄到HLSL会转置。这里用数学矩阵和 mul(Minv, vector) 固定语义。幅度表的两个通道按原拟合约定组合为 F0a+(1−F0)bF_0 a+(1-F_0)b,表资产、拟合BRDF和参数映射需成套使用。

多边形积分的主体如下。输入 vertices 已从着色点出发、变到局部基底并乘过 M−1M^{-1},仍保留长度;输入是非退化、共面的凸四边形,四点按边界顺序排列;可逆线性变换保持这些性质。单平面裁剪至多产生五个顶点,因此下方数组容量为 5。代码先裁剪 z≥0z\ge0,再积分球面边。它采用双面发光,单面光源由调用端根据原灯面法线和着色点方向剔除。

hlsl
float IntegrateLTCCosine(float3 vertices[4])
{
    float3 clipped[5];
    uint count = 0;
    for (uint i=0; i<4; ++i) {
        float3 a=vertices[i], b=vertices[(i+1)%4];
        bool inA=a.z>=0.0, inB=b.z>=0.0;
        if (inA) clipped[count++]=a;
        if (inA != inB) {
            float t=a.z/(a.z-b.z);
            clipped[count++]=lerp(a,b,t);
        }
    }
    if (count<3) return 0.0;
    for (uint j=0; j<count; ++j) {
        float len=length(clipped[j]);
        if (len<1e-7) return 0.0; // 灯面穿过着色点,超出本近似的有效输入
        clipped[j]/=len;
    }
    float integral=0.0;
    for (uint k=0; k<count; ++k) {
        float3 a=clipped[k], b=clipped[(k+1)%count];
        float3 c=cross(a,b);
        float sine=length(c);
        float cosine=clamp(dot(a,b),-1.0,1.0);
        if (sine<1e-7) {
            if (cosine<0.0) return 0.0; // 近反向边需另行处理灯面接触
            continue;
        }
        integral+=c.z*atan2(sine,cosine)/sine;
    }
    return abs(integral)/(2.0*3.14159265359);
}

对于归一化相邻方向 a,ba,b,边的球面角为 θ=atan2(∣a×b∣,a⋅b)\theta=\mathrm{atan2}(|a\times b|,a\cdot b)。每条边贡献 (a×b)zθ/sin⁡θ(a\times b)_z\theta/\sin\theta,总和除以 2π2\pi 得归一化余弦分布的积分。近平行同向边的贡献趋于零;反向边和着色点接触灯面的情况需要专门的近场策略,代码的零值回退不代表真实照度为零。

构造局部基底时,先把 VV 投影到切平面:T=V−N(N⋅V)T=V-N(N\cdot V)。若其长度接近零,选一个与 NN 不平行的参考轴生成 TT;归一化后取 B=N×TB=N\times T。每个灯顶点 QQ 的局部向量为 (T⋅(Q−P),B⋅(Q−P),N⋅(Q−P))(T\cdot(Q-P),B\cdot(Q-P),N\cdot(Q-P)),随后应用上述逆矩阵。LTC所得积分已包含接收面余弦与面积投影,不应再乘一次 N⋅LN\cdot L 或统一的距离平方衰减;最后乘灯的出射辐射亮度和拟合幅度。

LTC不自动计算场景遮挡。另加单一中心阴影也只是可见性近似,精确面积光遮挡需要在积分中区分灯上各点的可见性。

Horizon Clipping 与 LUT 边界

变换后的灯光多边形可能跨过着色点的切平面。局部空间中 z≤0z\leq0 的方向位于地平线以下,不应参与上半球余弦积分。Horizon Clipping 会逐边判断端点符号,在边穿过 z=0z=0 时插入交点,再对剩余的 3–5 个顶点做球面边积分。完全位于地平线下时贡献为 0;裁剪后顶点接近重合时要避免归一化零向量。

本例LUT坐标为 (roughness,sqrt(1-saturate(dot(N,V)))),使用 uv*(63/64)+0.5/64 映射到首尾纹素中心。LUT 的两个输入是 Roughness 与 N⋅VN\cdot V 的约定映射。运行时需要 Clamp 到拟合域,并使用半纹素缩放和偏移落在纹素中心。Roughness 为 0 或 N⋅VN\cdot V 接近 0 时最容易碰到拟合边界和数值尖峰,生产实现常设置与离线拟合一致的最小 Roughness。可把灯光缓慢移过地平线:若没有裁剪,贡献会在背面翻转或突然爆亮;若 LUT 地址错误,高光会在掠射角出现整条边界色带。

单位与空间分布

光的单位和曝光

现代引擎可能使用:

  • Lumen:光通量;
  • Candela:单位立体角光强;
  • Lux:表面照度;
  • Nit/cd/m²:显示或发光表面亮度。

数值只有在单位、引擎实现和曝光一致时才可比较。把灯光强度乘十后“看起来没变”,可能是 Auto Exposure 抵消了变化。

光源单位换算

同样填入 1000,Point Light 的 Lumen、Spot Light 的 Lumen/Candela 和 Area Light 的 Lumen/Nit 可能得到完全不同的场景亮度。Spot Cone 变窄时,如果总光通量保持不变,中心方向光强会提高。

引擎有时提供“保持亮度”或“保持总能量”的不同选项。修改光源尺寸、Cone Angle 后,需要确认哪个量被保持。

Light Cookie 用纹理调制光源的空间分布,可以表现窗格、云影或投影图案。它会增加纹理采样,也可能需要额外 Atlas 和更新管理。

IES Profile 描述现实灯具按方向测得的光强分布。它不是一张普通装饰纹理,使用时要配合正确的光源单位和朝向。

Cookie/IES 只改变光源分布。材质 BRDF、阴影可见性和距离衰减仍然独立计算。

光源尺寸的影响

增加光源半径或面积通常会同时影响:

  • 阴影半影;
  • 镜面高光形状和宽度;
  • 近距离照度分布;
  • 光追或随机采样的方差。

如果只让阴影变软,高光仍保持点状,就不是完整的面积光模型,而是视觉近似。项目可以接受这种近似,但文档要明确。

各向同性点光总光通量1000流明,光强为1000/(4π)≈79.58坎德拉。与光线垂直的接收面距灯2米时照度约19.89勒克斯,4米时约4.97勒克斯;表面法线与来光夹60°则再乘0.5。固定曝光以后,这份四倍距离平方关系可以在光源传播项中直接检查。

如果1000流明被重新分配到更窄的聚光角域,总光通量保持而中心方向光强增加;若接口保持坎德拉,缩角减少总光通量。两种行为都可合理,参数单位决定哪个量应保持。不能把点光的79.58直接当作面积光的辐亮度输入LTC。

表面求值与光源组织

直接光照的典型计算

对一个光源,简化形式:

Lo=visibility⋅attenuation⋅Li⋅(fd+fs)⋅max⁡(0,N⋅L)L_o=visibility\cdot attenuation\cdot L_i\cdot (f_d+f_s)\cdot\max(0,\mathbf N\cdot\mathbf L)
  • visibility 来自阴影;
  • attenuation 来自距离和灯光形状;
  • 对点光,Li⋅attenuationL_i\cdot attenuation 代表离散方向贡献,如 I/r2I/r^2,它不能被当成一个有限立体角上的普通辐射亮度样本;对面积光则必须做面积或方向积分;
  • fdf_d、fsf_s 是材质 BRDF。

多光源成本

Forward Rendering 中,一个物体可能对每盏影响它的灯重复计算,甚至重复 Draw。

Deferred/Forward+ 常先筛选哪些灯影响当前屏幕区域:

  • Directional Light:全屏;
  • Point/Spot Light:Light Volume;
  • Tiled/Clustered:每个 Tile/Cluster 保存灯光列表。

因此“有 GBuffer”并不会让 100 盏灯自动只算一次。每个像素仍要累加影响它的灯,只是材质几何信息不必为每盏灯重复生成。

光源剔除

CPU 可以先用 Bounds 筛选物体和灯。GPU Tiled/Clustered Culling 进一步把灯分配到屏幕区域。

灯光列表过长时,成本仍然很高。需要限制灯光范围、重叠数量、阴影灯数量和每 Cluster 上限,并记录溢出行为。

验证方法

  • 固定曝光后比较不同 Light Unit。
  • 单独显示距离和角度衰减。
  • 用多个重叠小灯测试 Tile/Cluster 灯光列表。
  • 在帧捕获中查看逐灯 Pass、Light Volume 或 Compute Light List。
  • 分别关闭阴影、Diffuse 和 Specular,确认各项归属。

test_gi.py 用独立的面积数值积分核对矩形余弦边积分,并检验尺度不变性、地平线裁剪与全背面情况。正文LTC函数通过DXC编译;拟合表误差、阴影可见性与目标GPU画面尚未验证。

相关主题

参考资料