fix: IBL 全黑根因——RadicalInverseVdC 误用 bit_cast,Hammersley 序列错乱

- bit_cast<f32>(bits) 把反转后的整数当 IEEE754 位型解读(LearnOpenGL
  原文 float(bits) 是整数值转换)→ xi.y 越出 [0,1] → sqrt(负数) →
  BRDF LUT NaN + pre-filter 数据全垃圾 → ambient NaN 整屏黑
- 分离出的积分程序实测:texel(0,2) 样本 10 xi.y=2 → NaN;修后无 NaN
- 加 CPU 侧 fp16 NaN 自检(上传前最后一道防线),数据异常拒绝加载
  走中性兜底(不再解引用空指针)
- 中性兜底 1×1 图只建 1 mip(修 mipLevels=5 validation 警告)
- prefilter mip 拷贝偏移改累积制(此前用当前 dim×序号错位)
- prefilter/irradiance 绑定换带 mip 的环境采样器(shadowSampler
  maxLod=0 把 textureLod clamp 到最锐 mip)
This commit is contained in:
2026-09-23 19:18:06 +08:00
parent 6e107ac2ba
commit bd4ac6aa12
2 changed files with 56 additions and 26 deletions
+52 -23
View File
@@ -120,7 +120,9 @@ namespace Engine
// ---- GGX 重要性采样(split-sum 镜面端)----
// van der Corput 基底反转(位反转技巧)
// van der Corput 基底反转(位反转技巧)。
// 注意是整数值转浮点(static_cast),不是位型重解读——
// bit_cast 会把反转后的整数当 IEEE754 解读,序列完全错乱
f32 RadicalInverseVdC(u32 bits)
{
bits = (bits << 16u) | (bits >> 16u);
@@ -128,7 +130,7 @@ namespace Engine
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return std::bit_cast<f32>(bits) * 2.3283064365386963e-10f;
return static_cast<f32>(bits) * 2.3283064365386963e-10f;
}
// n = 当次积分总采样数(xi.x = i/n 必须覆盖 [0,1] 全程)
@@ -202,7 +204,10 @@ namespace Engine
const f32 g = (nDotV / (nDotV * (1.0f - k) + k)) *
(nDotL / (nDotL * (1.0f - k) + k));
const f32 gVis = g * vDotH / (nDotH * nDotV);
// 高 roughness 时部分样本 H.z 恰为 0(g 也为 0),
// 0/0 = NaN 会累进 LUT——分母钳 epsilon
const f32 gVis = g * vDotH /
glm::max(nDotH * nDotV, 1e-6f);
const f32 fc = 1.0f - vDotH;
const f32 f = fc * fc * fc * fc * fc; // (1-vDotH)⁵,乘法链
@@ -509,6 +514,17 @@ namespace Engine
0, 1, 0, 1}));
}
// fp16 NaN 自检(指数全 1 且尾数非 0):预积分数据上传前的
// 最后一道防线——NaN 进 GPU 会污染整屏输出
bool ContainsNaNHalf(const u16* data, size_t count)
{
for (size_t i = 0; i < count; ++i) {
const u32 abs = data[i] & 0x7FFFu;
if (abs > 0x7C00u) return true;
}
return false;
}
vk::Sampler CreateEnvSampler(Device& device, u32 maxLod)
{
vk::SamplerCreateInfo samplerInfo;
@@ -592,20 +608,27 @@ namespace Engine
}
// ---- 2c. pre-filter cube:GGX 重要性采样 × mip 分级 roughness ----
// 采样数随 mip 递增(高 roughness 分布更宽需要更多样本)
std::vector<u16> prefFaces;
// 采样数随 mip 递增(高 roughness 分布更宽需要更多样本)。
// 注意:必须一次性分配总容量——逐 mip resize 会 realloc,使
// prefMips 里已记录的 data 指针悬空(上传读到垃圾 = NaN 全黑)
u32 prefTotal = 0;
for (u32 mip = 0; mip < kPrefilterMips; ++mip) {
const u32 dim = kPrefilterFaceSize >> mip;
prefTotal += kFaceCount * dim * dim * 4;
}
std::vector<u16> prefFaces(prefTotal);
std::vector<CubeMipData> prefMips;
prefMips.reserve(kPrefilterMips);
size_t prefOffset = 0;
for (u32 mip = 0; mip < kPrefilterMips; ++mip) {
const u32 dim = kPrefilterFaceSize >> mip;
const f32 roughness =
static_cast<f32>(mip) / static_cast<f32>(kPrefilterMips - 1);
const u32 samples = std::min(32u << mip, 256u);
const size_t offset = prefFaces.size();
prefFaces.resize(offset + kFaceCount * dim * dim * 4);
u16* base = prefFaces.data() + prefOffset;
for (u32 f = 0; f < kFaceCount; ++f) {
u16* dst = prefFaces.data() + offset + f * dim * dim * 4;
u16* dst = base + f * dim * dim * 4;
for (u32 y = 0; y < dim; ++y) {
for (u32 x = 0; x < dim; ++x) {
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(dim);
@@ -621,7 +644,8 @@ namespace Engine
}
}
}
prefMips.push_back({prefFaces.data() + offset, dim});
prefMips.push_back({base, dim});
prefOffset += kFaceCount * dim * dim * 4;
}
stbi_image_free(pixels); // 源数据消费完毕
@@ -639,7 +663,19 @@ namespace Engine
}
}
// ---- 3. 上传:env / irradiance / prefilter / LUT ----
// ---- 3. 上传:env / irradiance / prefilter / LUT(NaN 自检)----
if (ContainsNaNHalf(envFaces.data(), envFaces.size()) ||
ContainsNaNHalf(irrFaces.data(), irrFaces.size()) ||
ContainsNaNHalf(prefFaces.data(), prefFaces.size())) {
ENGINE_ERROR("Cubemap: NaN in pre-integrated data, "
"skipping IBL upload");
return nullptr;
}
if (ContainsNaNHalf(lutTexels.data(), lutTexels.size())) {
ENGINE_ERROR("Cubemap: NaN in BRDF LUT");
return nullptr;
}
vk::Image envImage, irrImage, prefImage, lutImage;
auto envAlloc = UploadCube(device, allocator,
{{envFaces.data(), faceSize}}, envImage);
@@ -681,8 +717,9 @@ namespace Engine
MemoryAllocator& allocator,
const glm::vec3& rgb)
{
// 1×1×6 常数 cube(prefilter 各 mip 同值);BRDF LUT 仍为真实
// 积分(纯数学,无环境依赖)——描述符恒有效
// 1×1×6 常数 cube(prefilter 仅 1 mip——1px 图不能建 mip 链,
// 采样越界 lod 会被 view range clamp 到 mip0,效果等同);
// BRDF LUT 仍为真实积分(纯数学,无环境依赖)——描述符恒有效
const u16 px[4] = { FloatToHalf(rgb.r), FloatToHalf(rgb.g),
FloatToHalf(rgb.b), 0x3C00 };
std::vector<u16> faces(kFaceCount * 4, 0);
@@ -690,15 +727,8 @@ namespace Engine
std::memcpy(faces.data() + f * 4, px, sizeof(px));
}
std::vector<u16> prefFaces(kPrefilterMips * kFaceCount * 4, 0);
std::vector<CubeMipData> prefMips;
prefMips.reserve(kPrefilterMips);
for (u32 mip = 0; mip < kPrefilterMips; ++mip) {
const size_t offset = mip * kFaceCount * 4;
std::memcpy(prefFaces.data() + offset, faces.data(),
kFaceCount * 4 * sizeof(u16));
prefMips.push_back({prefFaces.data() + offset, 1});
}
std::vector<u16> prefFaces(kFaceCount * 4, 0);
std::vector<CubeMipData> prefMips = {{prefFaces.data(), 1}};
std::vector<u16> lutTexels(kBrdfLutSize * kBrdfLutSize * 2);
for (u32 y = 0; y < kBrdfLutSize; ++y) {
@@ -722,8 +752,7 @@ namespace Engine
vk::ImageView envView = CreateCubeView(device, envImage, 1);
vk::ImageView irrView = CreateCubeView(device, irrImage, 1);
vk::ImageView prefView = CreateCubeView(device, prefImage,
kPrefilterMips);
vk::ImageView prefView = CreateCubeView(device, prefImage, 1);
vk::ImageView lutView = CreateLutView(device, lutImage);
vk::Sampler sampler = CreateEnvSampler(device, kPrefilterMips - 1);