feat: IBL 预积分 GPU compute 化 + 环境资产入 Assets 层(动态切换)

CPU 逐像素卷积(venice 4k 阻塞 7.36s)→ 4 个 compute shader 一次性
提交:equirect→cube / irradiance / prefilter×5mip / BRDF LUT。
Cubemap 瘦身为纯资源持有类,生成职责移交 IblGenerator;
环境经 AssetManager 缓存(EnvRef),Renderer::SetEnvironment
动态换绑(RenderPass::OnEnvironmentChanged 广播)。
调试视图 +4:EnvCube / Irradiance / Prefilter / BRDF LUT。

性能前后对比(RTX 5060 Laptop,venice_dawn_1_4k.hdr):
- 改造前 CPU:7.36 s(主线程阻塞)
- 改造后 GPU:HDR decode 127-134ms + 资源 22-35ms + compute 17-19ms
  ≈ 165-190ms(约 40×);Neutral 兜底 ~6ms
- 运行中动态切换环境同量级(ms 级 GPU,decode 占大头)

冒烟:两构建目录 0 警告;运行期 0 validation(含两次环境切换 +
调试视图 9-12 轮播自动化验证)。
This commit is contained in:
2026-09-24 13:23:59 +08:00
parent 705250df76
commit 1d690067bd
30 changed files with 1162 additions and 824 deletions
+54
View File
@@ -0,0 +1,54 @@
#version 450
// IBL ④:BRDF 积分 LUT(2D,纯数学,与环境内容无关)。
// (NdotV, roughness) → (F 缩放 A, F 偏置 B):specular = prefiltered·(F·A + B)。
// 与环境切换无关,但随生成链一起跑(GPU 亚毫秒级),省去跨环境缓存。
#include "ibl_common.glsl"
layout(local_size_x = 8, local_size_y = 8) in;
layout(set = 0, binding = 0, rg16f) uniform restrict writeonly image2D outLut;
const uint SAMPLES = 128;
void main()
{
ivec2 gid = ivec2(gl_GlobalInvocationID);
if (gid.x >= imageSize(outLut).x || gid.y >= imageSize(outLut).y) return;
vec2 uv = (vec2(gid) + 0.5) / vec2(imageSize(outLut));
float nDotV = uv.x;
float roughness = uv.y;
// V 在以 N 为 +Z 的切线空间里(积分只依赖这两角度)
vec3 V = vec3(sqrt(1.0 - nDotV * nDotV), 0.0, nDotV);
vec3 Nz = vec3(0.0, 0.0, 1.0);
float a = 0.0, b = 0.0;
float k = roughness * roughness * 0.5; // IBL 版 Smith k
for (uint i = 0u; i < SAMPLES; ++i) {
vec3 H = iblImportanceSampleGGX(iblHammersley(i, SAMPLES), Nz, roughness);
vec3 L = normalize(2.0 * dot(V, H) * H - V);
float lDotN = L.z;
float nDotL = max(lDotN, 0.0);
float nDotH = max(H.z, 0.0);
float vDotH = max(dot(V, H), 0.0);
if (nDotL <= 0.0) continue;
float g = (nDotV / (nDotV * (1.0 - k) + k)) *
(nDotL / (nDotL * (1.0 - k) + k));
// 高 roughness 时部分样本 H.z 恰为 0(g 也为 0),0/0 = NaN
// 会累进 LUT——分母钳 epsilon(与 CPU 版一致)
float gVis = g * vDotH / max(nDotH * nDotV, 1e-6);
float fc = 1.0 - vDotH;
float f = fc * fc * fc * fc * fc; // (1-vDotH)⁵
a += (1.0 - f) * gVis;
b += f * gVis;
}
imageStore(outLut, gid, vec4(a / float(SAMPLES), b / float(SAMPLES), 0.0, 1.0));
}
+60
View File
@@ -0,0 +1,60 @@
// ibl_common:IBL compute 预积分共享数学(与原 CPU 实现逐式对齐)。
//
// 被 4 个 .comp 包含(glslc 相对 include)。改造自 Cubemap.cpp 的
// CPU 积分路径——公式一致保证视觉无回归,可直接对比验证。
#ifndef IBL_COMMON_GLSL
#define IBL_COMMON_GLSL
const float IBL_PI = 3.14159265358979;
// ---- GL/Vulkan cube 面方向表(与 GL spec 立方体选择表互逆)----
// (u,v)∈[0,1](u 向右 v 向下)→ s,t∈[-1,1] → 单位方向
vec3 iblFaceDir(uint face, float u, float v)
{
float s = u * 2.0 - 1.0;
float t = v * 2.0 - 1.0;
vec3 dir;
if (face == 0u) dir = vec3( 1.0, -t, -s); // +X(面片顶行 = +Y)
else if (face == 1u) dir = vec3(-1.0, -t, s); // -X
else if (face == 2u) dir = vec3( s, 1.0, t); // +Y
else if (face == 3u) dir = vec3( s,-1.0, -t); // -Y
else if (face == 4u) dir = vec3( s, -t, 1.0); // +Z
else dir = vec3(-s, -t,-1.0); // -Z
return normalize(dir); // equirect 查表要求单位方向
}
// 单位方向 → 等距柱状 uv。行 0 = 天顶(+Y):v = acos(y)/π,
// 与 stbi 行序一致;u 经度环绕交给采样器 S = eRepeat 处理接缝
vec2 iblDirToEquirectUv(vec3 dir)
{
float u = atan(dir.z, dir.x) / (2.0 * IBL_PI) + 0.5;
float v = acos(clamp(dir.y, -1.0, 1.0)) / IBL_PI;
return vec2(u, v);
}
// van der Corput 基底反转。硬件位反转指令替代 CPU 手写交换链;
// 整数值转浮点(不是位型重解读),序列与 CPU 版一致
vec2 iblHammersley(uint i, uint n)
{
return vec2(float(i) / float(n),
float(bitfieldReverse(i)) * 2.3283064365386963e-10);
}
// GGX NDF 按概率密度取半程向量 H(切线空间 → 世界)
vec3 iblImportanceSampleGGX(vec2 xi, vec3 N, float roughness)
{
float a = roughness * roughness;
float phi = 2.0 * IBL_PI * xi.x;
float cosTheta = sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y));
float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);
vec3 up = abs(N.z) > 0.999 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0);
vec3 T = normalize(cross(up, N));
vec3 B = cross(N, T);
return normalize(T * H.x + B * H.y + N * H.z);
}
#endif // IBL_COMMON_GLSL
+27
View File
@@ -0,0 +1,27 @@
#version 450
// IBL ①:等距柱状 HDR → 环境 cube(6 面重采样)。
// 替代原 CPU 逐像素 SampleEquirect 双线性查表——GPU 上直接用硬件
// 线性过滤采样(经度接缝由 S=eRepeat 处理),观感与 CPU 版一致。
// 输出为 image2DArray(layer = face),图像本体带 CUBE_COMPATIBLE,
// 采样端另建 eCube view。
#include "ibl_common.glsl"
layout(local_size_x = 8, local_size_y = 8) in;
layout(set = 0, binding = 0) uniform sampler2D equirect;
layout(set = 0, binding = 1, rgba16f) uniform restrict writeonly image2DArray outCube;
void main()
{
ivec3 gid = ivec3(gl_GlobalInvocationID);
ivec2 size = imageSize(outCube).xy;
if (gid.x >= size.x || gid.y >= size.y) return;
vec2 uv = (vec2(gid.xy) + 0.5) / vec2(size);
vec3 dir = iblFaceDir(uint(gid.z), uv.x, uv.y);
vec3 rgb = texture(equirect, iblDirToEquirectUv(dir)).rgb;
imageStore(outCube, gid, vec4(rgb, 1.0));
}
+48
View File
@@ -0,0 +1,48 @@
#version 450
// IBL ②:环境 cube → 漫反射辐照度 cube(半球 Lambert 卷积)。
// E = ∫ L·cosθ·sinθ dθ dφ,切线基 up×N 构造。64×32 = 2048 采样/像素
// (与 CPU 版同级精度),32² 低频输出上 GPU 耗时可忽略。
#include "ibl_common.glsl"
layout(local_size_x = 8, local_size_y = 8) in;
layout(set = 0, binding = 0) uniform samplerCube env;
layout(set = 0, binding = 1, rgba16f) uniform restrict writeonly image2DArray outCube;
const int PHI_STEPS = 64;
const int THETA_STEPS = 32;
void main()
{
ivec3 gid = ivec3(gl_GlobalInvocationID);
ivec2 size = imageSize(outCube).xy;
if (gid.x >= size.x || gid.y >= size.y) return;
vec2 uv = (vec2(gid.xy) + 0.5) / vec2(size);
vec3 N = iblFaceDir(uint(gid.z), uv.x, uv.y);
vec3 up = abs(N.y) > 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(0.0, 1.0, 0.0);
vec3 T = normalize(cross(up, N));
vec3 B = cross(N, T);
float dPhi = 2.0 * IBL_PI / float(PHI_STEPS);
float dTheta = 0.5 * IBL_PI / float(THETA_STEPS);
vec3 irradiance = vec3(0.0);
for (int p = 0; p < PHI_STEPS; ++p) {
float phi = (float(p) + 0.5) * dPhi;
for (int t = 0; t < THETA_STEPS; ++t) {
float theta = (float(t) + 0.5) * dTheta;
float sinT = sin(theta);
float cosT = cos(theta);
vec3 sampleDir = T * (sinT * cos(phi)) +
B * (sinT * sin(phi)) +
N * cosT;
irradiance += texture(env, sampleDir).rgb * (cosT * sinT);
}
}
imageStore(outCube, gid, vec4(irradiance * (dPhi * dTheta), 1.0));
}
+44
View File
@@ -0,0 +1,44 @@
#version 450
// IBL ③:环境 cube → pre-filtered cube(GGX 重要性采样,split-sum 镜面端)。
// R(N) ≈ Σ L·NdotL / Σ NdotL。每 mip 一次 dispatch(storage view 必须
// 单 mip),roughness 经 push constant 传入;256 采样统一(高 roughness
// 分布更宽,取 CPU 版采样数上限保证收敛)。
#include "ibl_common.glsl"
layout(local_size_x = 8, local_size_y = 8) in;
layout(set = 0, binding = 0) uniform samplerCube env;
layout(set = 0, binding = 1, rgba16f) uniform restrict writeonly image2DArray outCube;
layout(push_constant) uniform Push {
float roughness;
} pc;
const uint SAMPLES = 256;
void main()
{
ivec3 gid = ivec3(gl_GlobalInvocationID);
ivec2 size = imageSize(outCube).xy;
if (gid.x >= size.x || gid.y >= size.y) return;
vec2 uv = (vec2(gid.xy) + 0.5) / vec2(size);
vec3 N = iblFaceDir(uint(gid.z), uv.x, uv.y);
vec3 V = N; // split-sum 约定:V 沿 N
vec3 acc = vec3(0.0);
float weight = 0.0;
for (uint i = 0u; i < SAMPLES; ++i) {
vec3 H = iblImportanceSampleGGX(iblHammersley(i, SAMPLES), N, pc.roughness);
vec3 L = normalize(2.0 * dot(V, H) * H - V);
float nDotL = max(dot(N, L), 0.0);
if (nDotL > 0.0) {
acc += texture(env, L).rgb * nDotL;
weight += nDotL;
}
}
imageStore(outCube, gid, vec4(weight > 0.0 ? acc / weight : vec3(0.0), 1.0));
}
+31
View File
@@ -20,6 +20,11 @@ layout(set = 0, binding = 4) uniform sampler2D velocityImage;
layout(set = 0, binding = 5) uniform sampler2D shadowMap0Image; // D32 深度(r 分量)
layout(set = 0, binding = 6) uniform sampler2D shadowMap1Image;
layout(set = 0, binding = 7) uniform sampler2D shadowMap2Image;
// 环境调试视图源(随环境切换重绑,静态绑定超集)
layout(set = 0, binding = 8) uniform samplerCube envCubeImage;
layout(set = 0, binding = 9) uniform samplerCube irradianceImage;
layout(set = 0, binding = 10) uniform samplerCube prefilterImage;
layout(set = 0, binding = 11) uniform sampler2D brdfLutImage;
// 与 Passes::DebugView 枚举同步(FullscreenGpu.h)
const int VIEW_FINAL = 0;
@@ -31,6 +36,10 @@ const int VIEW_VELOCITY = 5;
const int VIEW_SHADOW0 = 6;
const int VIEW_SHADOW1 = 7;
const int VIEW_SHADOW2 = 8;
const int VIEW_ENV_CUBE = 9;
const int VIEW_IRRADIANCE = 10;
const int VIEW_PREFILTER = 11;
const int VIEW_BRDF_LUT = 12;
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)
layout(push_constant) uniform PushConstants {
@@ -56,6 +65,19 @@ vec3 depthView(sampler2D depthImage) {
return vec3(pow(1.0 - d, 2.2));
}
// 全屏 uv → 球面方向(cube 调试视图的经纬展开:横向经度、纵向极角)
vec3 latLongDir(vec2 uv) {
float phi = (uv.x * 2.0 - 1.0) * 3.14159265358979; // -π..π 经度
float theta = (1.0 - uv.y) * 3.14159265358979; // 0..π 极角(顶=天顶)
return vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
}
// cube 视图统一走 tonemap(HDR 域,观感与场景一致);
// prefilter 纵向映射 mip 链(上锐下糙,直观验证 roughness 分级)
vec3 cubeView(samplerCube cube, float lod) {
return tonemap(textureLod(cube, latLongDir(fragUV), lod).rgb);
}
void main() {
int view = int(pc.debugView + 0.5);
@@ -81,6 +103,15 @@ void main() {
case VIEW_SHADOW0: outColor = vec4(depthView(shadowMap0Image), 1.0); return;
case VIEW_SHADOW1: outColor = vec4(depthView(shadowMap1Image), 1.0); return;
case VIEW_SHADOW2: outColor = vec4(depthView(shadowMap2Image), 1.0); return;
case VIEW_ENV_CUBE: outColor = vec4(cubeView(envCubeImage, 0.0), 1.0); return;
case VIEW_IRRADIANCE: outColor = vec4(cubeView(irradianceImage, 0.0), 1.0); return;
case VIEW_PREFILTER: outColor = vec4(cubeView(prefilterImage, fragUV.y * 4.0), 1.0); return;
case VIEW_BRDF_LUT: {
// LUT 是 (F 缩放, F 偏置) 双通道,R/G 直读可视化
vec2 ab = texture(brdfLutImage, fragUV).rg;
outColor = vec4(ab, 0.0, 1.0);
return;
}
}
}