Files
HelloVulkan/Engine/include/Engine/Renderer/SceneRenderer.h
T
atdunbg dec171cd1e Fix selection outline never showing for the first entity
entt entity 0's integral bits are 0, colliding with the "no selection"
sentinel: u32 selected == 0 meant "clear selection", so the scene's
first-created entity could never be outlined. Replaced with
std::optional<u32> + FrameSceneData::selectedEntityValid.

Also restore per-batch instance contiguity broken by the batchMats
flattening (e038cb3): entities sharing a mesh scatter across the flat
instance array in collection order. ViewScratch now tracks per-batch
singly-linked lists and appendView aggregates each batch into a
contiguous range, preserving the flat-array performance intent.

Hierarchy click selection now routes through SelectEntity so outline
stays in sync when picking from the panel.
2026-10-05 00:39:48 +08:00

179 lines
8.8 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Created by atdunbg on 2026/9/23.
//
// SceneRenderer:场景 → 渲染数据的每帧翻译层(UE FSceneRenderer /
// Unity CullingResults 的对应物)。
//
// 职责(全部是"读场景、产数据",不碰 GPU、不碰 Vulkan):
// 1. 相机/灯光/环境色 → UniformBufferObject 语义组装(含阴影光空间矩阵)
// 2. 单趟实例收集 + 双视图剔除(相机视锥/光源视锥各自独立可见列表——
// 相机外实体仍可能投影子进视野,shadow popping 的教训)
// Renderer 拿 FrameSceneData 写 buffer、驱动 pass;场景理解全部收口在此。
//
// 扩展性:
// - 多光源:lights 数组已就位(kMaxLights),每灯各自 lightSpace 即 CSM 前置
// - 级联阴影:BuildFrame 内按级联多出几份 lightSpace + shadowView,
// pass 侧按索引消费,本类结构不变
// - 遮挡剔除/GPU 剔除:只改本类收集逻辑,下游无感知
//
#ifndef HELLOVULKAN_SCENERENDERER_H
#define HELLOVULKAN_SCENERENDERER_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/SceneTypes.h>
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp>
#include <array>
#include <optional>
#include <unordered_map>
#include <Engine/Renderer/EditorCamera.h>
namespace Engine {
namespace ECS { class Scene; }
namespace Assets { class AssetManager; }
class SceneRenderer {
public:
// 从场景构建当帧渲染数据。游戏形态无 primary 相机返回 false
//(渲染跳帧;编辑器形态恒用编辑器相机,不跳)。
// targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。
// dt 驱动骨骼动画采样推进(crossfade 计时)。
bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
vk::Extent2D targetExtent, f32 dt);
FrameSceneData& Frame() { return m_Frame; }
// ---- 遥测 ----
u32 GetMainInstanceCount() const { return m_Frame.mainView.instanceCount; }
u32 GetShadowInstanceCount() const
{
u32 sum = 0;
for (const auto& c : m_Frame.shadowCascades) sum += c.instanceCount;
return sum;
}
f32 GetCollectMs() const { return m_Frame.collectMs; }
// ---- 运行时开关/调试视图(ImGui 经 Renderer 门面调用)。
// 状态唯一持有者在此,BuildFrame 每帧落入 FrameSceneData;
// TAA 开关同时控制投影抖动,同一持有者天然同步 ----
void SetShadowsEnabled(bool b) { m_ShadowsEnabled = b; }
void SetTAAEnabled(bool b) { m_TAAEnabled = b; }
void SetBloomEnabled(bool b) { m_BloomEnabled = b; }
void SetLodEnabled(bool b) { m_LodEnabled = b; }
void SetDebugView(int v) { m_DebugView = v; }
void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport
// 编辑器相机(视口 UI 状态,SceneRenderer 持有 = 渲染源在此分派);
// Application 驱动 Update,ViewportPanel/Inspector 经 Renderer 门面访问
EditorCamera& GetEditorCamera() { return m_EditorCamera; }
// Editor 视口拾取请求(UI 段写入,下一次 BuildFrame 落帧后清除)
void SetPickRequest(const glm::vec2& ndc)
{
m_PendingPick = PickRequest{true, ndc};
}
// Editor 选中实体(描边):UI 段写入,BuildFrame 落帧并筛出绘制。
// nullopt = 清选中(实体 0 号位模式 = 0,不能拿位值兼作哨兵)
void SetSelectedEntity(std::optional<u32> entityBits)
{
m_PendingSelected = entityBits;
}
private:
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
//(含纹素对齐防阴影边缘闪烁)。写入(Shadow pass push)与采样
//(Forward UBO)共用同一组矩阵。
void ComputeCascades(const glm::vec3& shadowDir);
// MDI 打包:batch 按组键排序(megabuffer + 可选材质)后压成
// IndirectDrawCmd 连续数组 + IndirectGroup 区间表。
// byMaterial=true(Forward):每材质一次 bind + 一次 MDI;
// byMaterial=false(Shadow):只按 megabuffer 分组(depth-only)
static void PackIndirect(CullResult& view, bool byMaterial);
FrameSceneData m_Frame;
EditorCamera m_EditorCamera; // 编辑器相机(视口 UI 状态,见 GetEditorCamera)
PickRequest m_PendingPick{}; // 拾取请求缓冲(BuildFrame 落帧后清除)
std::optional<u32> m_PendingSelected; // 选中实体缓冲(nullopt = 无选中)
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
struct ViewScratch {
// cur + prev 合并写:两次随机堆访问 → 一次(cache miss 减半)
struct InstanceMatrices {
glm::mat4 cur;
glm::mat4 prev;
u32 skinBase = 0; // palette 区段基址(0 = identity 槽)
u32 entityBits = 0; // 实体位模式(HitProxy id pass 消费)
};
// 链表尾哨兵:kMaxInstances 远小于 u32 最大值,安全
static constexpr u32 kNoInstance = 0xFFFFFFFFu;
CullResult result;
// 扁平实例数组(所有 batch 的实例共用一份连续内存)。
// 实体收集序下同 batch 实例会散布(多实体共享 mesh 交错插入),
// 组内连接关系由 instanceNext 链表维护——appendView 时按 batch
// 聚合拷成连续区间(firstInstance 连续语义的前提)。大场景下
// 避免 vector<vector> 的小块分配,每帧 clear() 复用容量
std::vector<InstanceMatrices> instances;
std::vector<u32> instanceNext; // 与 instances 平行:同 batch 下一实例
// 每 batch 实例链头(与 result.batches 平行;kNoInstance = 空)
std::vector<u32> batchHead;
// 每 batch 实例距离累加(透明排序用:平均距离降序 = 远→近)
std::vector<f32> batchDistSum;
// batch 查找:数组寻址(下标 = Mesh::GetSlot(),值 = 每 LOD 的
// batchIdx+1,0 = 未用)——逐实例 hash 在大场景是毫秒级开销
std::vector<std::array<u32, kMeshLodCount>> lookup;
void Clear()
{
result.batches.clear();
result.instanceCount = 0;
instances.clear(); // 容量保留,复用
instanceNext.clear();
batchHead.clear();
batchDistSum.clear();
for (auto& e : lookup) e.fill(0); // 容量保留,置零复用
}
};
ViewScratch m_MainScratch;
ViewScratch m_CascScratch[kMaxCascades];
ViewScratch m_TranslucentScratch; // 透明材质(仅主视图,不投阴影)
// ---- TAA 时域状态 ----
u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样)
glm::mat4 m_LastView{1.0f}; // 上一帧视图(velocity)
glm::mat4 m_LastProjNoJitter{1.0f}; // 上一帧未抖动投影(velocity)
// 实体的上一帧世界矩阵(key = entt 实体 integral)。
// 用 sorted vector + binary_search 替代 unordered_map:
// unordered_map 在每帧大量 emplace 时 rehash 抖动明显,
// vector 连续内存 + 单次 sort 对 cache 友好且无 rehash
std::vector<std::pair<u32, glm::mat4>> m_PrevWorlds;
std::vector<std::pair<u32, glm::mat4>> m_CurWorlds;
// ---- 骨骼动画 scratch(帧首重建/容量复用)----
// entKey → palette 区段基址(第 2.5 节预采样填,收集段查)
std::unordered_map<u32, u32> m_EntitySkinBase;
// 采样层 A(当前 clip)/ B(fade 来源 clip)与 global 累乘
std::vector<glm::vec3> m_PosePosA, m_PosePosB;
std::vector<glm::quat> m_PoseRotA, m_PoseRotB;
std::vector<glm::vec3> m_PoseSclA, m_PoseSclB;
std::vector<u8> m_TouchedA, m_TouchedB;
std::vector<glm::mat4> m_Globals;
bool m_SkinTruncWarned = false; // palette 预算溢出只告警一次
bool m_TruncationWarned = false; // 实例缓冲打满只告警一次
// ---- 运行时开关/调试视图状态(本类是唯一写入者)----
bool m_ShadowsEnabled = true;
bool m_TAAEnabled = true;
bool m_BloomEnabled = true;
bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过)
int m_DebugView = 0;
int m_DisplayMode = kDisplayViewport; // Editor 视口为默认形态
};
} // namespace Engine
#endif //HELLOVULKAN_SCENERENDERER_H