type
status
date
slug
summary
tags
category
icon
password
NX点光和聚光算法
在lowB的furure版本 在vs阶段还有段point light的代码 顶点算点光 效果很low(低画质使用)
此文件讨论的是LightingPass算的(延迟渲染 中高画质)
Low的顶点 点光:

可见 虽然是vs的 但是还是簇 和 裘的两种算法
代码展示


可以看到 依据不同的设备走了不同的算法
我们具体看看:
高性能-基于簇
// 用Clustered的方式去计算点光和聚光灯(PC平台)
void CalcPbrClusteredLight(PixelMaterialParameters MaterialParameters, FGBufferData GBuffer, inout PixelData p)
{
if (MaterialParameters.screen_position.w < u_cluster_depth_info.y)
{ int4 volume = GetClusteredVolume(MaterialParameters.screen_position.xy, MaterialParameters.screen_position.w);
int point_start = volume.x * 256 + volume.y;
int spot_start = point_start + volume.z;
[loop]
for (int i = 0; i < volume.z; i++)
{
int light_index = GetClusteredLightIndex(point_start, i, 2);
float4 light_color = u_cluster_point_list[light_index];
float4 light_pos = u_cluster_point_list[light_index + 1];
half4 dir_intensity = GetPointLightDirIntensity(MaterialParameters.world_position.xyz, light_pos, light_color.a);
half point_shadow = 1.f;
float4x4 trans_mat = float4x4(u_dpsm_views[light_index * 2 + 0], u_dpsm_views[light_index * 2 + 1], u_dpsm_views[light_index * 2 + 2], u_dpsm_views[light_index * 2 + 3]);
CalcPointLightShadow(MaterialParameters.world_position, trans_mat, u_cluster_point_uvoffset_list[light_index + 0], u_cluster_point_uvoffset_list[light_index + 1], (light_index & 0x2) ? u_dpsm_depth_scale[light_index >> 2].zw : u_dpsm_depth_scale[light_index >> 2].xy, point_shadow);
half3 L = dir_intensity.xyz;
half3 common_multiplier = GetLightMultiplier(light_color.rgb, dir_intensity.w, point_shadow);
FDirectLighting Lighting = (FDirectLighting) 0;
CalcDynamicLightingSplitSimple(MaterialParameters, GBuffer, p, Lighting, L);
p.local_diffuse += Lighting.Diffuse * common_multiplier;
//#if HAS_SUBSURFACE
// p.local_diffuse += Lighting.Transmission * common_multiplier
//#endif
#if QUALITY_SUPPORT_HIGH
p.local_specular += Lighting.Specular * common_multiplier;
#endif
}
[loop]
for (int i = 0; i < volume.w; i++)
{
int light_index = GetClusteredLightIndex(spot_start, i, 4);
float4 light_color = u_cluster_spot_list[light_index];
float4 light_pos = u_cluster_spot_list[light_index + 1];
float4 light_dir = u_cluster_spot_list[light_index + 2];
half4 dir_intensity = GetSpotLightIntensity(MaterialParameters.world_position.xyz, light_pos, light_dir, light_color.a);
half spot_shadow = 1.f;
light_index /= 4;
float4x4 trans_mat = float4x4(u_spot_view_projs[light_index * 4 + 0], u_spot_view_projs[light_index * 4 + 1], u_spot_view_projs[light_index * 4 + 2], u_spot_view_projs[light_index * 4 + 3]);
CalcSpotLightShadow(MaterialParameters.world_position, trans_mat, u_cluster_spot_uvoffset_list[light_index], spot_shadow);
half3 L = dir_intensity.xyz;
half3 common_multiplier = GetLightMultiplier(light_color.rgb, dir_intensity.w, spot_shadow);
FDirectLighting Lighting = (FDirectLighting) 0;
CalcDynamicLightingSplitSimple(MaterialParameters, GBuffer, p, Lighting, L);
p.local_diffuse += Lighting.Diffuse * common_multiplier;
//#if HAS_SUBSURFACE
// p.local_diffuse += Lighting.Transmission * common_multiplier
//#endif
#if QUALITY_SUPPORT_HIGH
p.local_specular += Lighting.Specular * common_multiplier;
#endif
}
}
}
解析
这个函数的根本目的是解决一个核心难题: 如何让一个像素只计算那些真正会影响它的光源,而忽略掉所有其他光源? 答案是: 基于簇的延迟光照(Clustered Deferred Lighting)。
这部分可以复习渲染管线(Rending Pipeline)精细笔记关于Cluser Lighting的部分
其核心思想是“ 分而治之 ”,将整个计算过程分为四个大阶段: 1. 空间划分 -> 2. 光源分配 -> 3. 高效查找 -> 4. 精确计算 。前两步通常在CPU端预先完成,后两步在GPU的这个函数中进行。
flowchart TD
A["场景 (无数光源)"] --> B["第1步: 空间划分<br>将视锥体划分为许多3D簇(Cluster)"]
B --> C["第2步: 光源分配(CPU)<br>为每个簇标记出影响它的光源列表"]
C --> D["第3步: 高效查找(GPU)<br>像素快速定位到自己所在的簇"]
D --> E
subgraph E[第4步: 精确计算(GPU)]
E1[获取该簇的光源列表]
E2[循环处理列表中的每个光源]
E2 --> E3[计算光照方向与衰减]
E2 --> E4[计算阴影]
E2 --> E5[执行PBR光照计算]
E2 --> E6[累加贡献]
end
E --> F["输出: 局部漫反射与高光贡献"]
前置知识
这里需要理解一些,每个像素是怎么拿到影响自己的光源的一系列信息的。包括颜色,位置。。。。等等
整个系统看作 一个三级别数据库 :
- 一级索引 :GetClusteredVolume
- 相当于空间哈希函数,输入(屏幕X,Y,深度Z) → 输出簇ID
- 使用3D纹理实现O(1)查找
- 二级索引 :u_cluster_light_indices
簇ID → 该簇的光源索引列表
紧凑存储,解包访问
【簇1的光源a,簇1的光源b,簇2的光源a,簇2的光源b,簇2的光源c。。】
这里的每个元素是光源在u_cluster_point_list的起始index
- 数据存储 :u_cluster_point_list
- 光源ID → 光源详细属性
- 只存储核心属性,按ID直接访问
理解这里是关键:可以看我这篇文档:
这个文档理解了,你才能理解下面的算法
具体实现:
- 筛选 - 只计算一定范围内的
首先,进行计算的像素 都是在一个前提条件下:
if (MaterialParameters.screen_position.w < u_cluster_depth_info.y)
- MaterialParameters.screen_position.w :及顶点的世界空间坐标的Z值(即深度)
- u_cluster_depth_info.y :这是一个预设的 最大计算深度 。可能在更远的地方,光源的影响微乎其微,为了性能直接忽略。
- 原理 :这是一个简单的优化。如果像素的深度远于阈值,就直接跳过所有复杂计算,节省大量性能。这相当于一个粗粒度的“距离剔除”。
- 定位到所在的“簇”
int4 volume = GetClusteredVolume(MaterialParameters.screen_position.xy, MaterialParameters.screen_position.w);
这个函数是为了让每一个像素知道自己属于哪个3D簇(Cluster)。原理是通过根据像素的屏幕位置和深度,构建一个volume_uv ,再通过volume_uv采样一个包含具体簇信息的3d纹理贴图。
这个获得的int4,其中包含了这个像素所在的3D簇(Cluster)的索引信息。这个 volume 变量包含了:
- .x: (通常) : 簇索引的高位字节 (High Byte) 。它代表了该簇在庞大的簇列表中的索引(或块索引)的最重要的部分
- .y: volume.y (通常) : 簇索引的低位字节 (Low Byte) 。它代表了该簇索引的最不重要的部分
- .z:影响这个簇的 点光源数量 。
- .w:影响这个簇的 聚光灯数量 。
volume.x 和 volume.y 合起来代表了该簇在光源索引列表中的起始位置(start_index)
int start_index = volume.x * 256 + volume.y;
256 等价于 2^8,所以 volume.x * 256 就是将高位字节左移8位,然后加上低位字节 volume.y。 这个 start_index 指向了在 u_cluster_light_indices 数组中,存储该簇所包含的所有点光源和聚光灯索引数据的起始位置(在u_cluster_light_indices中的索引,注意单位是打包索引项,不是具体的字节或光源索引)
好,我们现在继续看:
int point_start = volume.x * 256 + volume.y; // start_index
int spot_start = point_start + volume.z; // Start of spot lights = start_index + num_point_lights
我们已经知道了point_start就是 影响这个簇的 点光源索引列表 在u_cluster_light_indices 中的 起始位置。算法上面讲了。
这里为什么是spot_start的计算是point_start + volume.z?因为CPU 在存储每个簇的光源索引时,是 先连续存储所有影响该簇的点光源索引 (volume.z 个), 接着连续存储所有影响该簇的聚光灯索引 (volume.w 个)。
因此,聚光灯索引的起始位置 (spot_start) 紧挨在点光源索引列表的末尾也就是 point_start 加上点光源数量 volume.z。
实际上就是构建了一个简单的算法,来找到这个像素所在簇中,影响该像素的光源在u_cluster_light_indices的索引,再通过该索引的元素,这个元素
3:循环计算每个光源(以下循环要两遍 一遍是点光 一遍是聚光)
以点光为例 如下:
这个是点光的 看循环是 i < volume.z。volume.z上面说了是点光源数量

- 首先是获取光源信息
那么首先,我们需要遍历该簇的影响像素的每个点光源。就要获取当前要处理的光源在全局列表中的确切位置。使用这个 GetClusteredLightIndex 函数。
这个函数的目的是: 从u_cluster_light_indices中,正确地提取出单个光源的索引。 没循环一次,还能依次往后移动,获取下个灯的索引
具体函数解析看 这篇文档: GetClusteredLightIndex-提取光源索引以进行遍历获取光源信息
注意这个存储的index的列表A我们称为光源索引列表 (u_cluster_light_indices) 。这里面记录的元素都是索引,这些索引是用来了遍历列表B的,我们称为u_cluster_point_list(这是点光的 还有聚光的u_cluster_spot_list)。
列表B即u_cluster_point_list,依次排布每个光源的各种信息。点光是两个槽位,聚光是4个槽位。这点至关重要,这个数组的填充是在 CPU端的引擎逻辑中准备并上传到GPU的。
通过该数组索引,我们就可以获得改光源的一系列信息。 light_color light_pos light_dir
另外可以看到。点光是GetClusteredLightIndex(point_start, i, 2); 聚光是(spot_start, i, 4); 这是因为这个参数是size_of_light_attr 。点光是(size_of_light_attr = 2) 。聚光是
(size_of_light_attr = 4)。
因为点光需要的是:
- 颜色和强度 (1个 float4:RGB = 颜色, A = 强度或衰减参数)
- 位置和范围 (1个 float4:XYZ = 世界坐标位置, W = 影响半径)
而聚光在此基础上还需要
- 方向和角度 (1个 float4:XYZ = 归一化的方向向量, W = 聚光角度或衰减指数)
- 投影矩阵或附加参数 (1个 float4:可能存储投影矩阵的索引或其他控制参数)
float4 light_color = u_cluster_point_list[light_index];
float4 light_pos = u_cluster_point_list[light_index + 1];
这里再强调一次。因为u_cluster_spot_list存储了每个光源的各种信息 而且信息是依次排列的 譬如点光源a起始index是0 点光源b起始是index2 点光源c起始index是4 那么u_cluster_spot_list = 【a的light_color ,a的light_pos ,b的light_color ,b的light_pos,c的light_color 。。。。。】
- 然后计算光照方向与衰减
好的,上一步我们已经获得了光源的信息。包括位置啊强度啊范围。
我们现在就可以去具体计算光源和物体的关系了,譬如方向和衰减
half4 dir_intensity = GetPointLightDirIntensity(MaterialParameters.world_position.xyz, light_pos, light_color.a);
这个函数返回一个 half4:.xyz:单位化的光线方向(表面 -> 光源)。
w:计算出的基于距离的衰减系数(在0到1之间)
这个函数的解析看我这个文档:
注意聚光的版本和这个应该有些许差异。要增加了角度衰减
- 计算阴影
half point_shadow = 1.f; // 默认无阴影
... // 复杂的设置阴影计算所需的变换矩阵和参数
CalcPointLightShadow(..., point_shadow); // 计算后,point_shadow变为[0,1]之间的阴影系数
这个部分很复杂。使用的是DPSM 的阴影技术。这个主要还是移动端妥协。
平行光用的阴影是 级联阴影映射(Cascaded Shadow Maps - CSM)和 高度图阴影混合(Heightmap Shadow) 和 屏幕空间接触阴影(Contact Shadow)
- 合成光强乘数 :
half3 common_multiplier = GetLightMultiplier(light_color.rgb, dir_intensity.w, point_shadow);
GetLightMultiplier这个函数的实现就是:
LightColor * LightMask * Shadow;
原理 :这很好理解啊。就是把光源颜色 光源的衰减 和阴影都相乘 最后和 pbr计算出来的结果相乘
- 其计算本质通常是:FinalLight = LightColor * Attenuation * Shadow
- 这个乘数将应用于后续的光照计算结果。
- 执行pbr计算
FDirectLighting Lighting = (FDirectLighting) 0;
CalcDynamicLightingSplitSimple(MaterialParameters, GBuffer, p, Lighting, L);
这个函数非常复杂,包含了各函数的BRDF,这个之后整理为专门的文件
这里我大致看了一下,就是是比较简易的光照计算。
以漫反射为例,其中大部分走的就是
saturate(dot(N, L)) * GBuffer.DiffuseColor
其中这个GBuffer.DiffuseColor在pixel_init的gbuffer填充阶段已经做了处理,可以看 CalcDiffuseColor(GBuffer, p); 这个函数是gbuffer DiffuseColor填充的
可见GBuffer.DiffuseColor = BaseColor * (1 - Metallic)
其实这里就是相当于BRDF的kd即漫反射比例 直接就是(1 - Metallic)
(纯金属:无漫反射 非金属:有漫反射 )
- 累计贡献
p.local_diffuse += Lighting.Diffuse * common_multiplier;
p.local_specular += Lighting.Specular * common_multiplier;
最后合起来,没啥好说的。
聚光和点光不同的地方
低性能 - 基于球协
void CalcPbrSHPointLight(PixelMaterialParameters MaterialParameters, FGBufferData GBuffer, inout PixelData p)
{
half4 voxel_r;
half4 voxel_g;
half4 voxel_b;
GetSHPointValue(MaterialParameters.screen_position, voxel_r, voxel_g, voxel_b);
// 这样求的是E = L * cos
half4 diffuse_sh = CalcDiffuseTransferSH(GBuffer.WorldNormal);
half3 env_diffuse = half3(
dot(voxel_r, diffuse_sh),
dot(voxel_g, diffuse_sh),
dot(voxel_b, diffuse_sh)
);
half shadow = 1.f;
if (u_sh_light_shadow_info != 0)
{
uint light_index = u_sh_light_shadow_info >> 16;
if (u_sh_light_shadow_info & 0x1)
{
float4 light_color = u_cluster_point_list[light_index * 2];
float4 light_pos = u_cluster_point_list[light_index * 2 + 1];
half4 dir_intensity = GetPointLightDirIntensity(MaterialParameters.world_position.xyz, light_pos, light_color.a);
half3 L = dir_intensity.xyz;
float4x4 trans_mat = float4x4(u_dpsm_views[light_index * 4 + 0], u_dpsm_views[light_index * 4 + 1], u_dpsm_views[light_index * 4 + 2], u_dpsm_views[light_index * 4 + 3]);
CalcPointLightShadow(MaterialParameters.world_position, trans_mat, u_cluster_point_uvoffset_list[light_index * 2 + 0], u_cluster_point_uvoffset_list[light_index * 2 + 1], (light_index & 0x1) ? u_dpsm_depth_scale[light_index << 1].zw : u_dpsm_depth_scale[light_index << 1].xy, shadow);
half3 common_multiplier = GetLightMultiplier(light_color.rgb, dir_intensity.w, saturate(u_sh_light_shadow_base - shadow));
env_diffuse -= common_multiplier;
}
else
{
float4 light_color = u_cluster_spot_list[light_index * 4];
float4 light_pos = u_cluster_spot_list[light_index * 4 + 1];
float4 light_dir = u_cluster_spot_list[light_index * 4 + 2];
half4 dir_intensity = GetSpotLightIntensity(MaterialParameters.world_position.xyz, light_pos, light_dir, light_color.a);
half3 L = dir_intensity.xyz;
float4x4 trans_mat = float4x4(u_spot_view_projs[light_index * 4 + 0], u_spot_view_projs[light_index * 4 + 1], u_spot_view_projs[light_index * 4 + 2], u_spot_view_projs[light_index * 4 + 3]);
CalcSpotLightShadow(MaterialParameters.world_position, trans_mat, u_cluster_spot_uvoffset_list[light_index], shadow);
half3 common_multiplier = GetLightMultiplier(light_color.rgb, dir_intensity.w, saturate(u_sh_light_shadow_base - shadow));
env_diffuse -= common_multiplier;
}
}
p.local_diffuse += max(env_diffuse, half3(0.0)) * GBuffer.DiffuseColor;
#if QUALITY_SUPPORT_HIGH
half roughness = GBuffer.Roughness;
float3 dominant_dir = voxel_r.yzw * 0.3 + voxel_g.yzw * 0.59 + voxel_b.yzw * 0.11;
float3 L = normalize(half3(-dominant_dir.zx, dominant_dir.y));
half NoL = saturate(dot(GBuffer.WorldNormal, L));
// 这样求的是L
diffuse_sh = GetSHBasis(MaterialParameters.world_reflect);
half3 env_specular = half3(
dot(voxel_r, diffuse_sh),
dot(voxel_g, diffuse_sh),
dot(voxel_b, diffuse_sh)
);
p.local_specular += max(env_specular, half3(0.0)) * p.ibl_brdf * NoL;
#endif
}
优势

SH算法关键优势:
- 预计算光照 :CPU烘焙场景光照到SH系数
- 恒定开销 :无论场景光源数量多少 不需要循环
- 数学近似 :用低阶函数拟合复杂光照
- 单光源处理 :仅额外计算1个动态光源(可能主角手里的聚光灯啥的)
前置知识:
其实就是采样球鞋贴图t_clustered_map ,然后主角的点光或者聚光还是用簇的方式计算
注意我们从SH贴图获取的是上 各个方向收到光照的 强度 和 颜色 。这就厉害了,也不需要像簇那样,进行各种循环去获取。
另外,相较于传统的brdf这样计算光照。使用SH可以直接通过dot或的光照结果
// SH方案直接计算出总光照结果
half3 totalLighting = dot(SH_coeffs, SH_basis)

可见,cpu在制作sh贴图的时候是 光照采集 + 固定余弦BRDF积分。即
- 固定部分 (与材质无关的余弦项)→ CPU预计算
- 动态部分 (材质颜色/属性)→ GPU实时计算

预计算和还原


可见,漫反射和高光的还原是不同的。为啥呢。其实就是sh贴图在预乘阶段。就提起计算了。譬如这里漫反射是NoL。 镜面反射是vor 。(就是核函数)
然后用dot进行还原(在sh光照中,dot即积分)
积分结果 = dot(SH系数, SH基函数)
这里的数学原理看