fix: 视锥剔除平面归一化(透视因数误剔)+ 材质去重 + 描述符池可增长

- Frustum::FromMatrix 平面归一化:Gribb-Hartmann 提取的 left/right/
  bottom/top 法线模长含 1/tan(fov/2) 因子(>1),未归一化时包围球
  距离测试被放大,视锥边缘物体被误剔(SunTemple 全场景可见)——
  修正注释:必须传 [0,1] 重映射矩阵
- 材质去重:Model 新增 MaterialCache(按贴图路径共享 Material),
  Mesh 改持 Material* 非拥有指针。1056 mesh 渲染只需 48 个描述符集
  (对齐 UE/Unity 材质实例化思路,大场景第一道闸门)
- DescriptorManager 池改可增长分块(chunk=4096,OutOfPool/Fragmented
  自动续块):材质集数量不可预估(1G 资产可能上万),固定上限迟早爆
This commit is contained in:
2026-09-23 14:32:35 +08:00
parent a0c92af5db
commit 8253c31ad5
7 changed files with 116 additions and 51 deletions
+14 -3
View File
@@ -6,7 +6,10 @@
// 平面从 view-proj 矩阵的行组合中直接抽出(无需分解参数),
// 用于按视图剔除(相机视图 / 光源视图各自独立一份)。
// 注意:near/far 平面按 Vulkan [0,1] 深度约定提取
//(z_clip ∈ [0,1]:near = row2,far = row3 - row2)。
//(z_clip ∈ [0,1]:near = row2,far = row3 - row2)——
// 因此必须传已做 [0,1] 重映射的矩阵(相机 proj / lightSpace 均是)。
// 平面在提取后归一化:包围球测试的 radius 是欧氏长度,
// 未归一化的平面距离会被透视因数放大,导致视锥边缘误剔。
//
#ifndef HELLOVULKAN_FRUSTUM_H
@@ -20,11 +23,11 @@ namespace Engine {
struct Frustum {
// 依次:left, right, bottom, top, near, far
// xyz = 平面法线(未归一化,仅做符号/点积测试),w = 偏移
// xyz = 单位法线,w = 原点到平面的有符号距离
std::array<glm::vec4, 6> planes{};
// 从 view-proj 矩阵提取 6 平面(列主序 glm 矩阵的"行"组合)。
// 注意必须传未做 [0,1] 深度重映射的矩阵,提取公式才成立。
// 必须传 [0,1] 深度重映射后的矩阵。
static Frustum FromMatrix(const glm::mat4& m)
{
Frustum f;
@@ -43,6 +46,14 @@ namespace Engine {
f.planes[3] = r3 - r1; // top: y' <= w
f.planes[4] = r2; // near: z' >= 0(Vulkan [0,1])
f.planes[5] = r3 - r2; // far: z' <= w
// 归一化:法线长度把透视/缩放因数从距离里除掉
for (auto& p : f.planes) {
const f32 len = glm::length(glm::vec3(p));
if (len > 1e-8f) {
p /= len;
}
}
return f;
}
+4 -3
View File
@@ -21,10 +21,11 @@ namespace Engine
class Mesh
{
public:
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices,
const std::vector<u32>& indices,
Scope<Material> material);
Material* material);
~Mesh();
Mesh(const Mesh&) = delete;
@@ -34,7 +35,7 @@ namespace Engine
// 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1, u32 firstInstance = 0) const;
Material* GetMaterial() const { return m_Material.get(); }
Material* GetMaterial() const { return m_Material; }
u32 GetIndexCount() const { return m_IndexCount; }
// 局部空间包围球(构造时从顶点算出),视锥剔除用
@@ -47,7 +48,7 @@ namespace Engine
private:
Scope<Buffer> m_VertexBuffer;
Scope<Buffer> m_IndexBuffer;
Scope<Material> m_Material;
Material* m_Material;
u32 m_IndexCount = 0;
Bounds m_Bounds;
+11 -4
View File
@@ -25,6 +25,7 @@ namespace Engine
class MemoryAllocator;
class DescriptorManager;
class Texture;
class Material;
class Model
{
@@ -61,17 +62,23 @@ namespace Engine
Texture* GetOrLoadTexture(const std::string& path);
// 材质缓存入口:同贴图路径共享同一 Material(一个描述符集),
// 空路径/缺文件折到兜底材质。路径已做反斜杠归一化。
Material* GetOrCreateMaterial(const std::string& path);
Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_DescriptorManager;
vk::DescriptorSetLayout m_TextureLayout;
std::vector<Scope<Mesh>> m_Meshes;
std::string m_ModelPath;
// 纹理缓存:同一路径只加载一次
// 缓存声明在 m_Meshes 之前:mesh 持 Material* 裸指针,
// 析构序 = meshes 先于材质/纹理缓存
std::unordered_map<std::string, Scope<Material>> m_MaterialCache;
std::unordered_map<std::string, Scope<Texture>> m_TextureCache;
Scope<Texture> m_DefaultTexture;
std::vector<Scope<Mesh>> m_Meshes;
std::string m_ModelPath;
};
} // namespace Engine
@@ -54,12 +54,17 @@ namespace Engine {
private:
Device& m_Device;
vk::DescriptorPool m_Pool;
// 描述符池容量:需 ≥ 场景全部材质集合数 + 每帧集合。
// SunTemple(984 mesh 材质)把 1000 打爆过;材质去重(同贴图共享
// Material)属于材质系统任务,在那之前留足余量
static constexpr u32 kMaxSets = 4096;
// 可增长描述符池(分块):场景材质集数量不可预估(1G 资产场景
// 可能上万),固定上限迟早打爆。耗尽时追加一块继续分配——
// 对齐 UE/Unity 的"growable pool/bindless 堆"思路的 Vulkan 版。
// 材质去重(同贴图共享 Material)仍是大场景的第一道闸门。
std::vector<vk::DescriptorPool> m_Pools;
// 每块的容量(set 数;各类型描述符数同值)
static constexpr u32 kChunkSets = 4096;
vk::DescriptorPool CreateChunk();
};
} // namespace Engine