Compare commits

...
6 Commits
Author SHA1 Message Date
atdunbg bfbb18f1a4 build: meshoptimizer 改为 git submodule(--depth=1)
Lib 依赖一律 submodule 不入库;历史中的在树源码记录已用 filter-branch 全量清洗
2026-09-30 09:22:30 +08:00
atdunbg 20f0035baa feat: ImGui docking Editor + 显示链/帧同步/输入语义重构
- App/editor: Hierarchy/Inspector/Viewport/Stats 四面板 + DockBuilder 默认布局,main.cpp 瘦身为场景搭建与生命周期
- 显示链: Tonemap→PresentTarget 经 ImGui::Image 呈现(零色偏),backbuffer 仅承载 ImGui;Direct/Viewport 双显示模式 + 面板分辨率上报
- 帧首同步 BeginFrameSync: resize/vsync/尺寸/模式意图统一帧首落定(修描述符悬垂);修复 vsync 意图未落 swapchain 导致的每帧重建交换链
- 输入: Editor 每帧判定键盘/鼠标归属(打字让位、Viewport hover/focus 放行、Fullscreen 全放行)
- 材质: roughness 独立槽位 + 非 glTF 金属/粗糙贴图映射;透明队列 depthWrite=FALSE + translucent 独立 shader + scratch 清帧
- FBX: 非蒙皮网格烘焙节点链全局变换(Z-up 旋转/单位换算落顶点)
- Hierarchy 行 ImGui ID 以 UUID 作用域(重名不冲突、改名不丢选中)
2026-09-30 00:24:53 +08:00
atdunbg b25fa29fe5 feat: 透明材质 alpha blending——独立队列 + 线性 over 混合 + 距离排序
透明来源:glTF alphaMode=BLEND(解析期读取,OBJ/FBX 恒不透明——
资产格式定义写死);MASK 后续演进。Material 加透明语义(入缓存键),
程序化测试资产 CreateTransparentPlane(RGBA 纯色纹理)。

SceneRenderer:透明材质实例收集进独立 scratch(仅主视图,不投
阴影——alpha 阴影后续演进),batch 按平均距离降序排序(远→近,
batchMats 同步重排),SSBO 扁平化顺序 = 排序后顺序。

ForwardPass 两段绘制:不透明(无混合 + 深度写)→ 透明(Shader
Object 动态状态切 SRC_ALPHA/ONE_MINUS_SRC_ALPHA 线性 over + 深度
只测不写);velocity 附件透明段直写(透明表面 velocity 即自身运动)。
frag alpha = albedo 贴图 a 直通,预乘在 tonemap 段(hard constraint
不变)。已知边界:TAA alpha 时域混合(透明边缘几帧渐显);同 batch
多实例不做实例级排序。
2026-09-25 16:57:27 +08:00
atdunbg 8dbfb9f554 feat: LOD 运行时开关(ImGui Features 横排,关 = 实例收集恒选 LOD0) 2026-09-25 16:39:16 +08:00
atdunbg a975845214 perf: BuildFrame 收集段优化——实体级粗剔除 + 数组化 batch 查找 + 合并矩阵写
定位数据(100 背包 + SunTemple,RTX 5060):GPU 全帧仅 ~3ms
(Forward 1.91 + Shadow 0.82),CPU BuildFrame 收集段 5.94ms 才是
帧率瓶颈。三段细分测量:pre 0.2 / each+cull 5.0 / flatten 0.4。

优化(collect 5.94 → 4.6ms,-23%):
1. Mesh 全局连续槽位(GetSlot);ViewScratch batch 查找 hash →
   数组寻址(slot × kMeshLodCount 槽)
2. Model 级联合包围球(各 mesh bounds 并集):实体在四视图全部
   不可见 → 跳过整个 mesh 循环(多 mesh 模型的粗剔除)
3. worlds/prevWorlds 合并为 stride-2 InstanceMatrices 单次写
   (每实例每视图 2 次随机堆访问 → 1 次,cache miss 减半)

遥测:Renderer::GetCollectMs 暴露收集段耗时。剩余 4.6ms 的进一步
压缩(并行收集 / GPU-driven culling)单独立项。
2026-09-25 08:08:51 +08:00
atdunbg d9b1742327 feat: LOD 网格分级——meshopt 解析期简化 + 按距离/级联选级
vendored meshoptimizer 1.2(submodule 环境故障改直接 vendor)。Mesh
多 LOD:各级索引连续拼进同一 index buffer(共享顶点,LodRange 区间
选择),解析期 meshopt_simplify 生成链(LOD0=原始,每级目标减半,
相对误差 1e-2,小网格复用上级);都在 worker 线程,加载零额外卡顿。

SceneRenderer 按 (mesh, LOD) 聚合 batch(lookup key = 指针<<8|lod):
主视图按屏占比近似 radius/dist 选级(阈值 10/20 倍半径);阴影级联
按级联序降级(越远越简)。Draw 按 batch.lod 选索引区间。

A/B 对比(100 背包 10×10,RTX 5060 Laptop,vsync off):
- 纯背包:LOD 开 93-95 fps / 关 90-107 fps —— 无显著差异
- 加 SunTemple:LOD 开 52-56 / 关 51-55 —— 无显著差异
结论:当前场景瓶颈不在顶点数(5060 顶点吞吐富余),LOD 基础设施
保留(几何复杂资产/低端 GPU/更大场景生效),帧率问题转向 Tracy
GPU 时间线定位真实瓶颈。

坑:LodRange 记录"LOD 不足复用最后一级"时 first 不得继续累加
(buffer 无重复数据,累加必越界——VUID-08798 抓的)。
2026-09-24 20:53:19 +08:00
70 changed files with 3447 additions and 637 deletions
+3
View File
@@ -23,3 +23,6 @@
[submodule "Lib/tracy"]
path = Lib/tracy
url = https://github.com/wolfpld/tracy.git
[submodule "Lib/meshoptimizer"]
path = Lib/meshoptimizer
url = https://github.com/zeux/meshoptimizer.git
+8 -1
View File
@@ -1,4 +1,11 @@
add_executable(HelloVulkan main.cpp)
# App 层源文件:main + editor/(docking 编辑器,纯 App 代码)
file(GLOB EDITOR_SOURCES CONFIGURE_DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.h)
add_executable(HelloVulkan main.cpp ${EDITOR_SOURCES})
target_include_directories(HelloVulkan PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/editor)
target_link_libraries(HelloVulkan PRIVATE Engine)
+202
View File
@@ -0,0 +1,202 @@
//
// Editor.cpp:docking 骨架实现。
//
// 布局方案(首帧 DockBuilder 一次,之后 imgui.ini 接管):
// ┌──────────────────────────────┬───────────────┐
// │ Hierarchy │ │
// ├──────────────────────────────┤ Inspector │
// │ Stats │ (28%) │
// └──────────────────────────────┴───────────────┘
//
#include "Editor.h"
#include "HierarchyPanel.h"
#include "InspectorPanel.h"
#include "ViewportPanel.h"
#include "StatsPanel.h"
#include <imgui.h>
#include <imgui_internal.h> // DockBuilder*(内部 API)
#include <GLFW/glfw3.h>
using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h)
Editor::Editor() = default;
Editor::~Editor() = default;
void Editor::Init(const EditorContext& ctx)
{
m_Ctx = ctx;
m_Hierarchy = std::make_unique<HierarchyPanel>();
m_Inspector = std::make_unique<InspectorPanel>();
m_Viewport = std::make_unique<ViewportPanel>();
m_Stats = std::make_unique<StatsPanel>();
}
void Editor::Draw()
{
// host 窗口:铺满主视口,承载菜单栏 + DockSpace
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->Pos);
ImGui::SetNextWindowSize(viewport->Size);
ImGui::SetNextWindowViewport(viewport->ID);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin("##EditorDockHost", nullptr,
ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus);
ImGui::PopStyleVar(3);
DrawMenuBar();
DrawDockSpace();
ImGui::End();
// 常规面板窗口(窗口名 = docking 身份)
if (m_ShowHierarchy) m_Hierarchy->Draw(m_Ctx, m_Selected);
if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected);
if (m_ShowViewport) m_Viewport->Draw(m_Ctx, m_VpInput);
if (m_ShowStats) m_Stats->Draw(m_Ctx);
DrawQuitModal();
}
// 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里
// 只决定"剩余输入给不给游戏侧"。UI 被隐藏(反引号)时不会走到
// Draw → m_VpInput 停在旧值,谓词调用方(SandboxApp)须先查
// GetImGuiEnabled() 再问这里。
bool Editor::BlockingKeyboard() const
{
const ImGuiIO& io = ImGui::GetIO();
if (io.WantTextInput) return true; // 打字中(改名等)不让位快捷键
// 游戏形态(Fullscreen):WASD 全窗口生效,键盘只让位文本输入
if (m_Ctx.renderer &&
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
{
return false;
}
// 编辑器形态:Viewport 面板持焦才拥有键盘(UE Scene view 语义)
return !m_VpInput.focused;
}
bool Editor::BlockingMouse() const
{
// 游戏形态:沿用 ImGui 全局判定(UI 隐藏时 WantCaptureMouse=false)
if (m_Ctx.renderer &&
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
{
return ImGui::GetIO().WantCaptureMouse;
}
// 编辑器形态:悬停 Viewport 才放行(模态框遮挡时 hovered=false)
return !m_VpInput.hovered;
}
void Editor::DrawMenuBar()
{
if (!ImGui::BeginMenuBar())
{
return;
}
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("Quit", "Alt+F4"))
{
RequestQuit();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View"))
{
ImGui::MenuItem("Hierarchy", nullptr, &m_ShowHierarchy);
ImGui::MenuItem("Inspector", nullptr, &m_ShowInspector);
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
void Editor::DrawDockSpace()
{
const ImGuiID dockspaceId = ImGui::GetID("EditorDockSpace");
// 默认布局:仅当 dock node 不存在时构建一次
//(imgui.ini 已保存过布局 → DockBuilderGetNode 非空 → 跳过)
if (!m_LayoutBuilt && ImGui::DockBuilderGetNode(dockspaceId) == nullptr)
{
m_LayoutBuilt = true;
ImGui::DockBuilderRemoveNode(dockspaceId);
ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspaceId, ImGui::GetMainViewport()->Size);
ImGuiID dockLeft = 0, dockRight = 0, dockCenter = 0, dockStats = 0;
ImGui::DockBuilderSplitNode(dockspaceId, ImGuiDir_Left, 0.20f,
&dockLeft, &dockCenter);
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Right, 0.28f,
&dockRight, &dockCenter);
ImGui::DockBuilderSplitNode(dockLeft, ImGuiDir_Down, 0.38f,
&dockStats, &dockLeft);
ImGui::DockBuilderDockWindow("Hierarchy", dockLeft);
ImGui::DockBuilderDockWindow("Stats", dockStats);
ImGui::DockBuilderDockWindow("Inspector", dockRight);
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
ImGui::DockBuilderFinish(dockspaceId);
}
ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f),
ImGuiDockNodeFlags_None);
}
void Editor::DrawQuitModal()
{
if (m_ShowQuitConfirm)
{
ImGui::OpenPopup("Should quit?");
m_ShowQuitConfirm = false;
}
ImGui::SetNextWindowPos(
ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Should quit?", nullptr,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("Quit the application?");
ImGui::Separator();
if (ImGui::Button("Yes", ImVec2(120, 0)))
{
m_QuitConfirmed = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("No", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (m_QuitConfirmed)
{
m_QuitConfirmed = false;
m_Ctx.app->Close();
}
}
+89
View File
@@ -0,0 +1,89 @@
//
// Editor:docking 编辑器骨架。
//
// 职责:
// - 顶栏菜单(File/View)+ 全屏 DockSpace + 首帧默认布局(DockBuilder)
// - 实体选中状态(entt::entity,Hierarchy 写 / Inspector 读)
// - 退出确认模态框(窗口关闭 → RequestQuit → Yes 时 Close)
//
// 面板窗口名(Hierarchy/Inspector/Viewport/Stats)即 docking 身份,
// imgui.ini 持久化布局;默认布局只在 dock node 为空时构建一次。
//
#ifndef HELLOVULKAN_EDITOR_H
#define HELLOVULKAN_EDITOR_H
#include "EditorContext.h"
#include "ViewportPanel.h" // ViewportInputState(成员按值持有)
#include <entt/entt.hpp>
#include <memory>
class HierarchyPanel;
class InspectorPanel;
class ViewportPanel;
class StatsPanel;
class Editor {
public:
Editor();
~Editor();
Editor(const Editor&) = delete;
Editor& operator=(const Editor&) = delete;
// 注入上下文(OnInit 阶段,指针必须全部有效)
void Init(const EditorContext& ctx);
// 每帧 UI 构建(OnImGuiRender 唯一入口)
void Draw();
// ---- 输入归属判定(Window 层输入捕获谓词;UE/Unity 的
// "viewport 持焦才拥有输入" 语义,ImGui 侧经自身后端
// 链式回调永远正常收输入)----
// 键盘:编辑器形态 = Viewport 持焦且不在文本输入中;游戏形态
// (Fullscreen)只在文本输入中让位(打字不触发快捷键)
bool BlockingKeyboard() const;
// 鼠标:编辑器形态 = 悬停 Viewport 才放行;游戏形态沿用
// WantCaptureMouse(UI 隐藏时自动放行)
bool BlockingMouse() const;
// 请求退出(弹确认框;窗口关闭事件走这里)
void RequestQuit() { m_ShowQuitConfirm = true; }
// ---- 选中状态 ----
void SelectEntity(entt::entity e) { m_Selected = e; }
entt::entity GetSelected() const { return m_Selected; }
private:
void DrawMenuBar(); // host 窗口内顶栏
void DrawDockSpace(); // 全屏 host + dockspace + 默认布局
void DrawQuitModal(); // 退出确认模态框
EditorContext m_Ctx{};
ViewportInputState m_VpInput{}; // Viewport 面板 hover/focus(每帧刷新)
std::unique_ptr<HierarchyPanel> m_Hierarchy;
std::unique_ptr<InspectorPanel> m_Inspector;
std::unique_ptr<ViewportPanel> m_Viewport;
std::unique_ptr<StatsPanel> m_Stats;
entt::entity m_Selected{entt::null};
// 面板显隐(View 菜单)
bool m_ShowHierarchy = true;
bool m_ShowInspector = true;
bool m_ShowViewport = true;
bool m_ShowStats = true;
// 默认布局只建一次(imgui.ini 已有布局则跳过)
bool m_LayoutBuilt = false;
// 退出确认链
bool m_ShowQuitConfirm = false;
bool m_QuitConfirmed = false;
};
#endif //HELLOVULKAN_EDITOR_H
+48
View File
@@ -0,0 +1,48 @@
//
// EditorContext:Editor 层与 SandboxApp 之间的桥接上下文。
//
// Editor 是 App 层代码(不属于引擎),它需要的引擎门面与沙盒能力
// 全部经此聚合注入——面板只认识 EditorContext,不反向依赖 main.cpp。
//
#ifndef HELLOVULKAN_EDITORCONTEXT_H
#define HELLOVULKAN_EDITORCONTEXT_H
#include <Engine/Core/Application.h>
#include <Engine/ECS/Scene.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect
#include <functional>
// 可切换环境(IBL 动态切换):StatsPanel 的 Combo 与 SandboxApp 的
// 加载逻辑共用这一张表(下标即身份)。
inline constexpr const char *kEnvPaths[] = {
"assets/textures/venice_dawn_1_4k.hdr",
"assets/textures/rooitou_park_4k.hdr",
"assets/textures/pink_sunrise_4k.hdr",
"assets/textures/birchwood_4k.hdr",
};
inline constexpr const char *kEnvNames[] = {
"Venice Dawn", "Rooitou Park", "Pink Sunrise", "Birchwood",
};
inline constexpr int kEnvCount = 4;
inline constexpr int kDefaultEnv = 0;
// 沙盒特有状态(面板可选读取,空指针 = 该面板区块不显示)
struct EditorContext {
// 引擎门面
Engine::Renderer *renderer = nullptr;
Engine::ECS::Scene *scene = nullptr;
Engine::Assets::AssetManager *assets = nullptr;
Engine::Application *app = nullptr;
std::function<int()> getEnvIndex; // 当前环境表下标
std::function<void(int)> setEnvIndex;
// 冻结动画时钟(沙盒 spinners 用;保留原 Scene Debug 行为)
bool *pauseAnim = nullptr;
};
#endif //HELLOVULKAN_EDITORCONTEXT_H
+86
View File
@@ -0,0 +1,86 @@
//
// HierarchyPanel.cpp:实体列表实现。
//
#include "HierarchyPanel.h"
#include "Engine/ECS/Components.h"
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
#include <imgui.h>
using namespace Engine;
namespace {
struct ScopedID {
explicit ScopedID(const u64 uuid) {
ImGui::PushID(reinterpret_cast<const void*>(uuid));
}
~ScopedID() {ImGui::PopID(); }
};
}
void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
{
ImGui::Begin("Hierarchy");
// 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构)
if (ImGui::Button("+ Create Entity"))
{
const auto e = ctx.scene->CreateEntity("Entity");
selected = e;
}
ImGui::Separator();
auto& reg = ctx.scene->GetRegistry();
reg.view<Engine::ECS::NameComponent>().each(
[&](const entt::entity e, const Engine::ECS::NameComponent& name)
{
const bool isSelected = (selected == e);
u64 rowId = static_cast < uint64_t >( static_cast < uint32_t >(e));
if (const auto * id = reg.try_get<Engine::ECS::IdComponent>(e)) {
rowId = id->uuid.Value();
}
const ScopedID scope (rowId);
const std::string label = name.name.empty() ? "(unnamed)" : name.name;
if (ImGui::Selectable(label.c_str(), isSelected))
{
selected = e;
}
// 右键菜单:删除(延迟销毁,帧尾统一 flush)
if (ImGui::BeginPopupContextItem("entity_ctx"))
{
if (ImGui::MenuItem("Delete"))
{
ctx.scene->DestroyEntity(e);
if (selected == e)
{
selected = entt::null;
}
}
ImGui::EndPopup();
}
// 组件徽标 + 悬停清单
std::string badge;
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e)) badge += "[M]";
if (reg.all_of<Engine::ECS::AnimationComponent>(e)) badge += "[A]";
if (reg.all_of<Engine::ECS::CameraComponent>(e)) badge += "[C]";
if (reg.all_of<Engine::ECS::LightComponent>(e)) badge += "[L]";
if (!badge.empty())
{
ImGui::SameLine();
ImGui::TextDisabled("%s", badge.c_str());
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(
"M=Mesh A=Animation C=Camera L=Light");
}
}
});
ImGui::End();
}
+20
View File
@@ -0,0 +1,20 @@
//
// HierarchyPanel:实体列表面板。
//
// 点击选中(写 Editor 的选中状态)、工具栏创建实体、右键删除
//(延迟销毁,帧尾 flush,遍历安全)。每项尾缀显示组件构成徽标。
//
#ifndef HELLOVULKAN_HIERARCHYPANEL_H
#define HELLOVULKAN_HIERARCHYPANEL_H
#include "EditorContext.h"
#include <entt/entt.hpp>
class HierarchyPanel {
public:
void Draw(const EditorContext& ctx, entt::entity& selected);
};
#endif //HELLOVULKAN_HIERARCHYPANEL_H
+220
View File
@@ -0,0 +1,220 @@
//
// InspectorPanel.cpp:组件编辑实现。
//
// Transform 编辑必须走 Set* 入口(标脏 → 帧循环统一 Compose 重算
// world),直接改私有成员是编译错误——面板天然遵守引擎约束。
//
#include "InspectorPanel.h"
#include "Engine/ECS/Components.h"
#include "Engine/Renderer/Model.h"
#include <imgui.h>
#include <cstdio>
#include <vector>
namespace
{
// InputText 需要 char 缓冲;提交时写回 std::string
bool InputTextString(const char* label, std::string& str)
{
char buf[256];
std::snprintf(buf, sizeof(buf), "%s", str.c_str());
if (ImGui::InputText(label, buf, sizeof(buf)))
{
str = buf;
return true;
}
return false;
}
}
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
ImGui::Begin("Inspector");
auto& reg = ctx.scene->GetRegistry();
if (selected == entt::null || !reg.valid(selected))
{
ImGui::TextDisabled("(no entity selected)");
ImGui::End();
return;
}
// ---- Name ----
if (auto* name = reg.try_get<Engine::ECS::NameComponent>(selected))
{
InputTextString("Name", name->name);
}
ImGui::Separator();
// ---- Add Component ----
if (ImGui::Button("Add Component"))
{
ImGui::OpenPopup("add_component");
}
if (ImGui::BeginPopup("add_component"))
{
if (!reg.all_of<Engine::ECS::LightComponent>(selected) &&
ImGui::MenuItem("Light"))
{
reg.emplace<Engine::ECS::LightComponent>(selected);
}
if (!reg.all_of<Engine::ECS::CameraComponent>(selected) &&
ImGui::MenuItem("Camera"))
{
reg.emplace<Engine::ECS::CameraComponent>(selected);
}
if (!reg.all_of<Engine::ECS::AnimationComponent>(selected) &&
ImGui::MenuItem("Animation"))
{
reg.emplace<Engine::ECS::AnimationComponent>(selected);
}
if (!reg.all_of<Engine::ECS::NameComponent>(selected) &&
ImGui::MenuItem("Name"))
{
reg.emplace<Engine::ECS::NameComponent>(selected);
}
ImGui::EndPopup();
}
ImGui::Separator();
// ---- Transform ----
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
{
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
{
glm::vec3 pos = t->GetPosition();
if (ImGui::DragFloat3("position", &pos.x, 0.05f))
{
t->SetPosition(pos);
}
glm::vec3 rot = t->GetRotation(); // 欧拉角(度)
if (ImGui::DragFloat3("rotation", &rot.x, 0.5f))
{
t->SetRotation(rot);
}
glm::vec3 scl = t->GetScale();
if (ImGui::DragFloat3("scale", &scl.x, 0.02f, 0.001f, 100.0f))
{
t->SetScale(scl);
}
}
}
// ---- MeshRenderer ----
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
{
if (ImGui::CollapsingHeader("MeshRenderer",
ImGuiTreeNodeFlags_DefaultOpen))
{
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
model != nullptr)
{
const auto& clips = model->GetClips();
ImGui::Text("model: valid (%u clips, %u bones)",
static_cast<unsigned>(clips.size()),
static_cast<unsigned>(model->GetBoneCount()));
ImGui::TextDisabled("(mesh asset is read-only here)");
}
else
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"model: INVALID (dangling ref)");
}
}
}
// ---- Light ----
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
{
if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen))
{
int type = static_cast<int>(light->type);
if (ImGui::Combo("type", &type, "Directional\0Point\0"))
{
light->type = static_cast<Engine::ECS::LightComponent::Type>(type);
}
ImGui::ColorEdit3("color", &light->color.x);
ImGui::DragFloat("intensity", &light->color.a, 0.05f, 0.0f, 100.0f);
if (light->type == Engine::ECS::LightComponent::Type::Directional)
{
ImGui::SliderFloat3("direction", &light->direction.x, -1.0f, 1.0f);
ImGui::Checkbox("castShadow", &light->castShadow);
}
else
{
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
ImGui::TextDisabled("(position = entity Transform)");
}
}
}
// ---- Camera ----
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
{
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen))
{
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
{
// primary 互斥:启用本相机时清掉其余实体的 primary
//(引擎取第一个 primary 作为渲染视角)
reg.view<Engine::ECS::CameraComponent>().each(
[&](entt::entity e, Engine::ECS::CameraComponent& c)
{
if (e != selected)
{
c.primary = false;
}
});
}
}
}
// ---- Animation ----
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
{
if (ImGui::CollapsingHeader("Animation",
ImGuiTreeNodeFlags_DefaultOpen))
{
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
const Engine::Model* model =
mr ? ctx.assets->GetModel(mr->model) : nullptr;
if (model != nullptr && model->HasAnimations())
{
const auto& clips = model->GetClips();
std::vector<const char*> clipNames;
clipNames.reserve(clips.size());
for (const auto& c : clips)
{
clipNames.push_back(c.name.c_str());
}
int cur = anim->clipIndex;
if (ImGui::Combo("clip", &cur, clipNames.data(),
static_cast<int>(clipNames.size())))
{
anim->Play(cur); // 触发 crossfade(fade 秒)
}
ImGui::Checkbox("playing", &anim->playing);
ImGui::SameLine();
ImGui::Checkbox("loop", &anim->loop);
ImGui::SliderFloat("speed", &anim->speed, -2.0f, 2.0f);
ImGui::SliderFloat("fade", &anim->fadeDuration, 0.0f, 1.0f);
if (anim->clipIndex >= 0 &&
anim->clipIndex < static_cast<int>(clips.size()))
{
ImGui::Text("t: %.2f / %.2f s", anim->time,
clips[anim->clipIndex].duration);
}
}
else
{
ImGui::TextDisabled("(no animation clips on model)");
}
}
}
ImGui::End();
}
+22
View File
@@ -0,0 +1,22 @@
//
// InspectorPanel:选中实体的组件检查/编辑面板。
//
// 泛化编辑:任何实体(不是沙盒特例)的 Name / Transform /
// MeshRenderer / Light / Camera / Animation 全部在此编辑。
// 沙盒原 Scene Debug 里的 KeyLight、动画 clip、gun scale 控件
// 全部由这里的通用控件替代。
//
#ifndef HELLOVULKAN_INSPECTORPANEL_H
#define HELLOVULKAN_INSPECTORPANEL_H
#include "EditorContext.h"
#include <entt/entt.hpp>
class InspectorPanel {
public:
void Draw(const EditorContext& ctx, entt::entity selected);
};
#endif //HELLOVULKAN_INSPECTORPANEL_H
+173
View File
@@ -0,0 +1,173 @@
//
// StatsPanel.cpp:遥测与全局设置实现(原 main.cpp Scene Debug 搬迁)。
//
#include "StatsPanel.h"
#include "Engine/ECS/Components.h"
#include <imgui.h>
#include <glm/gtc/type_ptr.hpp>
using namespace Engine; // f32(Engine/Core/Types.h)
void StatsPanel::Draw(const EditorContext& ctx)
{
ImGui::Begin("Stats");
auto& renderer = *ctx.renderer;
auto& app = *ctx.app;
// ---- Camera:主相机位姿(只读遥测;位姿编辑走 Inspector)----
{
Engine::ECS::TransformComponent* camT = nullptr;
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
{
const glm::vec3& pos = camT->GetPosition();
ImGui::Text("Camera pos");
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
}
else
{
ImGui::TextDisabled("(no primary camera)");
}
ImGui::Separator();
}
// ---- Scene:动画时钟冻结(沙盒 spinners 用)----
if (ctx.pauseAnim != nullptr)
{
ImGui::Checkbox("Pause Animation", ctx.pauseAnim);
}
// ---- Rendering:渲染管线参数与开关 ----
if (ImGui::CollapsingHeader("Rendering", ImGuiTreeNodeFlags_DefaultOpen))
{
f32 exposure = renderer.GetExposure();
if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f))
{
renderer.SetExposure(exposure);
}
bool aces = renderer.GetUseACES();
if (ImGui::Checkbox("ACES", &aces))
{
renderer.SetUseACES(aces);
}
// Bloom:开关即显隐——勾上才显示参数
bool bloom = renderer.GetBloomEnabled();
if (ImGui::Checkbox("Bloom", &bloom))
{
renderer.SetBloomEnabled(bloom);
}
if (bloom)
{
f32 threshold = renderer.GetBloomThreshold();
if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f))
{
renderer.SetBloomThreshold(threshold);
}
f32 strength = renderer.GetBloomStrength();
if (ImGui::SliderFloat("strength", &strength, 0.0f, 2.0f))
{
renderer.SetBloomStrength(strength);
}
}
// 无参数的纯开关:横排平铺
bool shadows = renderer.GetShadowsEnabled();
if (ImGui::Checkbox("Shadows", &shadows))
{
renderer.SetShadowsEnabled(shadows);
}
ImGui::SameLine();
bool taa = renderer.GetTAAEnabled();
if (ImGui::Checkbox("TAA", &taa))
{
renderer.SetTAAEnabled(taa);
}
ImGui::SameLine();
bool lod = renderer.GetLodEnabled();
if (ImGui::Checkbox("LOD", &lod))
{
renderer.SetLodEnabled(lod); // 关 = 恒绘原网格
}
}
// ---- Environment:IBL 环境 + 调试视图(决定"看什么")----
if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen))
{
bool& drawSkybox = ctx.scene->GetSettings().DrawSkybox;
ImGui::Checkbox("DrawSkybox", &drawSkybox);
if (drawSkybox && ctx.getEnvIndex)
{
int envIdx = ctx.getEnvIndex();
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
{
ctx.setEnvIndex(envIdx);
const auto env = ctx.assets->LoadEnvironment(kEnvPaths[envIdx]);
ctx.renderer->SetEnvironment(ctx.assets->GetEnvironment(env));
}
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
const auto& viewNames = renderer.GetDebugViewNames();
int view = renderer.GetDebugView();
if (ImGui::Combo("Debug View", &view, viewNames.data(),
static_cast<int>(viewNames.size())))
{
renderer.SetDebugView(view);
}
}
else
{
ImGui::ColorEdit4("ClearColor",
glm::value_ptr(ctx.scene->GetSettings().ClearColor));
}
}
// ---- GPU Timings:只读遥测(默认折叠,不占面板高度)----
if (ImGui::CollapsingHeader("GPU Timings"))
{
ImGui::Text("instances: %u main / %u shadow",
renderer.GetMainInstanceCount(),
renderer.GetShadowInstanceCount());
if (renderer.GetInstancesTruncated())
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"TRUNCATED: instance buffer full!");
}
const auto& names = renderer.GetPassNames();
const auto& times = renderer.GetPassGpuMs();
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
{
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
}
}
// ---- Frame:帧率控制与显示 ----
if (ImGui::CollapsingHeader("Frame", ImGuiTreeNodeFlags_DefaultOpen))
{
bool vsync = app.GetVSync();
if (ImGui::Checkbox("VSync", &vsync))
{
app.SetVSync(vsync);
if (!vsync)
{
const f32 target = app.GetTargetFps();
app.SetTargetFps(target);
}
}
if (!vsync)
{
f32 target = app.GetTargetFps();
if (ImGui::SliderFloat("target fps", &target, 24.0f, 360.0f, "%.0f"))
{
app.SetTargetFps(target);
}
}
ImGui::Text("fps: %.1f frame: %llu",
1.0f / app.GetDeltaTime(),
static_cast<unsigned long long>(app.GetFrameCount()));
}
ImGui::End();
}
+19
View File
@@ -0,0 +1,19 @@
//
// StatsPanel:遥测与全局设置面板(原 Scene Debug 拆入)。
//
// Camera 位姿 / Rendering 开关 / Environment(IBL+DebugView+ClearColor) /
// GPU Timings / Frame(VSync、fps)。KeyLight、动画 clip、gun scale
// 等沙盒特例编辑已泛化进 Inspector,此处不再重复。
//
#ifndef HELLOVULKAN_STATSPANEL_H
#define HELLOVULKAN_STATSPANEL_H
#include "EditorContext.h"
class StatsPanel {
public:
void Draw(const EditorContext& ctx);
};
#endif //HELLOVULKAN_STATSPANEL_H
+53
View File
@@ -0,0 +1,53 @@
//
// ViewportPanel.cpp:显示模式单选 + ImGui::Image + 分辨率上报。
// 逻辑照搬原 main.cpp 的 Viewport 窗口(行为不变)。
//
#include "ViewportPanel.h"
#include <imgui.h>
#include <vulkan/vulkan.h> // vk::Extent2D 经 Renderer 头已带入,显式无妨
using namespace Engine; // u32 / f32(Engine/Core/Types.h)
void ViewportPanel::Draw(const EditorContext &ctx, ViewportInputState &out) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
{
ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoCollapse);
// 输入焦点上报(悬停计入遮挡:模态框盖住时不误报)
out.hovered = ImGui::IsWindowHovered();
out.focused = ImGui::IsWindowFocused();
auto &renderer = *ctx.renderer;
const int mode = renderer.GetDisplayMode();
const void *sceneTex = renderer.GetSceneTexture();
if (sceneTex != nullptr && mode == kDisplayViewport) {
// 面板可用区域(物理像素)上报为渲染分辨率 → 画面精确填满
// 面板;下一帧帧首 BeginFrameSync 生效(一帧延迟,不可见)
const ImVec2 avail = ImGui::GetContentRegionAvail();
const ImVec2 fb = ImGui::GetIO().DisplayFramebufferScale;
const u32 wantW = static_cast<u32>(avail.x * fb.x + 0.5f);
const u32 wantH = static_cast<u32>(avail.y * fb.y + 0.5f);
if (wantW >= 16 && wantH >= 16) {
renderer.SetViewportSize(wantW, wantH);
}
// 纹理 = 渲染分辨率:等比适配面板可用区域
const vk::Extent2D ext = renderer.GetRenderExtent();
const f32 aspect =
ext.height != 0 ? f32(ext.width) / f32(ext.height) : 1.0f;
ImVec2 size = avail;
if (avail.x / avail.y > aspect) size.x = avail.y * aspect;
else size.y = avail.x / aspect;
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
} else {
ImGui::TextDisabled("(scene texture not ready)");
}
ImGui::End();
}
ImGui::PopStyleVar();
}
+26
View File
@@ -0,0 +1,26 @@
//
// ViewportPanel:场景渲染输出面板。
//
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
//
#ifndef HELLOVULKAN_VIEWPORTPANEL_H
#define HELLOVULKAN_VIEWPORTPANEL_H
#include "EditorContext.h"
// Viewport 面板每帧上报的输入焦点状态(Hazel 模式:编辑器据此决定
// 游戏侧输入是否放行——悬停/持焦 Viewport 才拥有 WASD/鼠标)
struct ViewportInputState {
bool hovered = false; // 鼠标悬停在 Viewport 面板上
bool focused = false; // Viewport 面板持有键盘焦点
};
class ViewportPanel {
public:
void Draw(const EditorContext& ctx, ViewportInputState& out);
};
#endif //HELLOVULKAN_VIEWPORTPANEL_H
+118 -336
View File
@@ -3,12 +3,18 @@
//
// SandboxApp:第一个基于 Application 框架的程序。
//
// main 的全部职责:定义一个 Application 派生类并运行它。
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理;
// 这里只写"程序逻辑":创建实体、逐帧更新组件、事件响应。
// main 的职责边界:
// - 定义 Application 派生类并运行它
// - "程序逻辑":场景搭建(实体/组件)、逐帧更新、事件响应、
// 异步环境加载
// - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现
// 任何 ImGui 控件)
//
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理。
//
#include <Engine/Core/Application.h>
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
#include <Engine/Core/Log.h>
#include <Engine/Core/Stats.h>
#include <Engine/Core/AsyncTask.h>
@@ -17,37 +23,20 @@
#include <Engine/ECS/Components.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/IblGenerator.h>
#include <imgui.h>
#include <Engine/Renderer/Model.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <GLFW/glfw3.h>
#include <atomic>
#include <thread>
#include "Editor.h"
#include "EditorContext.h" // kEnvPaths / kDefaultEnv(与 StatsPanel 共用)
using namespace Engine;
using namespace Engine::ECS;
namespace
{
// 可切换环境(IBL 动态切换演示):路径 → AssetManager 缓存去重,
// 重复选择同一环境零成本;首选路径触发一次 compute 预积分
constexpr const char* kEnvPaths[] = {
"assets/textures/venice_dawn_1_4k.hdr",
"assets/textures/rooitou_park_4k.hdr",
"assets/textures/pink_sunrise_4k.hdr",
"assets/textures/birchwood_4k.hdr",
};
constexpr const char* kEnvNames[] = {
"Venice Dawn", "Rooitou Park", "Pink Sunrise", "Birchwood",
};
constexpr int kEnvCount = 4;
constexpr int kDefaultEnv = 0;
constexpr const char* kTemplePath = "assets/models/SunTemple/SunTemple.fbx";
class SandboxApp final : public Application
@@ -60,11 +49,6 @@ namespace
~SandboxApp() override
{
// worker 只做 CPU 解码(不碰 Vulkan),在任何 GPU 资源
// 析构之前 join 即安全;派生析构先于基类成员析构执行
if (m_EnvWorker.joinable()) {
m_EnvWorker.join();
}
}
protected:
@@ -73,55 +57,83 @@ namespace
void OnInit() override
{
Scene& scene = GetScene();
Renderer& renderer = GetRenderer();
renderer.SetDisplayMode(kDisplayViewport);
// ImGui 叠加层(框架只做管线桥接,UI 内容全部在这里定义)
// ImGui 叠加层(框架只做管线桥接;UI 内容 = docking Editor)
EnableImGui();
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级),
// 其余环境在 ImGui 里按需 lazy 加载后切换
const auto env = GetAssets().LoadEnvironment(
kEnvPaths[kDefaultEnv]);
if (env.Valid()) {
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env));
// Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
InitEditor();
SetEnv();
const auto suntemple = GetAssets().LoadModel("assets/models/demo/asset.fbx");
auto entityhandle = scene.CreateEntity("SunTemple");
entityhandle.AddComponent<MeshRendererComponent>(MeshRendererComponent{suntemple});
scene.CreateEntity();
scene.CreateEntity();
scene.CreateEntity();
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
if (gun.Valid()) {
m_Gun = scene.CreateEntity("M1911");
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
m_Gun.AddComponent<AnimationComponent>();
auto& gt = m_Gun.GetComponent<TransformComponent>();
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
} else {
ENGINE_WARN("m1911 model not loaded, animation test skipped");
}
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
// 大模型异步加载:assimp 解析在 worker 线程(SunTemple 约 3s),
// 完成后 OnUpdate 收割 → GPU 构建 → 建实体(cm 尺度 → 0.001)
m_TempleTask.Run([] { return Model::ParseFromDisk(kTemplePath); });
m_TempleScale = 0.001f;
/*
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
if (gun.Valid()) {
m_Gun = scene.CreateEntity("M1911");
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
m_Gun.AddComponent<AnimationComponent>();
auto& gt = m_Gun.GetComponent<TransformComponent>();
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
} else {
ENGINE_WARN("m1911 model not loaded, animation test skipped");
}
int countSize = 0;
for (int i = 0; i < countSize; i ++)
{
for (int j = 0; j < countSize; j ++)
if (countSize) {
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
for (int i = 0; i < countSize; i ++)
{
auto m = scene.CreateEntity();
m.AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
m.GetComponent<TransformComponent>().SetPosition(glm::vec3{(i - countSize * 0.5) * 2, 0, (j - countSize * 0.5) * 2});
m_Spinners.push_back(m);
for (int j = 0; j < countSize; j ++)
{
auto m = scene.CreateEntity();
m.AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
m.GetComponent<TransformComponent>().SetPosition(glm::vec3{(i - countSize * 0.5) * 2, 0, (j - countSize * 0.5) * 2});
m_Spinners.push_back(m);
}
}
}
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
m_Ground = scene.CreateEntity("Ground");
m_Ground.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
*/
// 相机实体(primary = 当前渲染视角;位姿存实体 Transform)
m_Camera = scene.CreateEntity("MainCamera");
auto& camera = m_Camera.AddComponent<CameraComponent>();
camera.primary = true;
m_Camera.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f));
.SetPosition(glm::vec3(0.0f, 12.0f, 22.0f));
m_Camera.GetComponent<TransformComponent>()
.SetRotation(glm::vec3(-25.0f, 0.0f, 0.0f));
// 灯光实体(第一个方向光 = 阴影光源)
m_KeyLight = scene.CreateEntity("KeyLight");
@@ -133,6 +145,7 @@ namespace
.castShadow = true,
});
/*
scene.CreateEntity("FillLight")
.AddComponent<LightComponent>(
LightComponent{
@@ -140,7 +153,9 @@ namespace
.direction = glm::vec3(0.5f, -0.5f, 0.3f),
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.5f)
});
*/
/*
// 点光源(位置 = 实体 Transform,出生即有;无阴影,距离衰减)
auto fire = scene.CreateEntity("FirePitLight");
fire.GetComponent<TransformComponent>().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f));
@@ -159,28 +174,11 @@ namespace
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.2f), // 冷蓝
.radius = 10.0f,
});
*/
}
void OnUpdate(f32 deltaTime) override
{
// 异步模型收割:解析完成 → GPU 构建 → 建实体
if (m_TempleTask.Done()) {
ParsedModel parsed = m_TempleTask.Take();
const auto ref = GetAssets().LoadModel(kTemplePath,
std::move(parsed));
if (ref.Valid()) {
auto temple = GetScene().CreateEntity("SunTemple");
temple.AddComponent<MeshRendererComponent>(
MeshRendererComponent{ref});
temple.GetComponent<TransformComponent>()
.SetScale(glm::vec3{m_TempleScale});
ENGINE_INFO("[ASYNC] SunTemple ready, entity created");
}
}
// 异步环境加载:每帧收割完成的解码结果
PollEnvironment();
// 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff
// 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到)
if (!m_PauseAnim)
@@ -202,262 +200,20 @@ namespace
}
}
// ---- UI(全部委托 docking Editor;本文件零 ImGui 控件)----
void OnImGuiRender() override
{
m_Editor->Draw();
}
// ---- 事件 ----
// ---- 异步环境加载(decode 后台线程 / GPU 生成主线程)----
// decode 占切换耗时大头(4k ≈ 130ms);worker 只产出纯 CPU
// 像素数据,Vulkan 生成回主线程(设备非线程安全)。
// 同一时刻最多一个 in-flight,忙碌期的新请求记为 pending 接力。
void RequestEnvironment(int idx)
{
if (m_EnvLoadBusy)
{
m_EnvPending = idx;
return;
}
StartEnvDecode(idx);
}
void StartEnvDecode(int idx)
{
m_EnvLoadBusy = true;
m_EnvLoadDone.store(false);
m_EnvLoadingPath = kEnvPaths[idx];
m_EnvWorker = std::thread([this, idx]() {
m_EnvDecoded = IblGenerator::DecodeHdr(kEnvPaths[idx]);
m_EnvLoadDone.store(true); // release:解码结果对主线程可见
});
}
void PollEnvironment()
{
if (!m_EnvLoadBusy || !m_EnvLoadDone.load())
{
return;
}
m_EnvWorker.join(); // join = 同步点,解码结果此后归主线程
m_EnvLoadBusy = false;
if (m_EnvDecoded.Valid())
{
const auto ref = GetAssets().LoadEnvironment(
m_EnvLoadingPath, std::move(m_EnvDecoded));
if (ref.Valid())
{
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(ref));
}
}
m_EnvDecoded = {};
// 忙碌期间的最新请求接力(rapid switch 只保留最终意图)
if (m_EnvPending >= 0)
{
const int next = m_EnvPending;
m_EnvPending = -1;
StartEnvDecode(next);
}
}
// ---- 叠加层 UI(唯一允许出现 ImGui 控件的地方)----
void OnRenderOverlay() override
{
ImGui::Begin("Scene Debug");
auto& renderer = GetRenderer();
// ---- Scene:场景内容(灯/动画) ----
if (ImGui::CollapsingHeader("Scene", ImGuiTreeNodeFlags_DefaultOpen))
{
// 阴影光源(无开关,控件平铺聚拢)
if (auto* light = m_KeyLight.TryGetComponent<LightComponent>())
{
ImGui::Text("Key Light (shadow caster)");
ImGui::SliderFloat3("direction",
&light->direction.x, -1.0f, 1.0f);
ImGui::SliderFloat("intensity", &light->color.a, 0.0f, 20.0f);
if (ImGui::Button("reset"))
{
light->direction = glm::vec3(-0.5f, -0.6f, -0.35f);
light->color.a = 2.0f;
}
}
ImGui::Checkbox("Pause Animation", &m_PauseAnim);
}
// ---- Rendering:渲染管线参数与开关 ----
if (ImGui::CollapsingHeader("Rendering", ImGuiTreeNodeFlags_DefaultOpen))
{
// Tonemap(无开关,平铺)
f32 exposure = renderer.GetExposure();
if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f))
{
renderer.SetExposure(exposure);
}
bool aces = renderer.GetUseACES();
if (ImGui::Checkbox("ACES", &aces))
{
renderer.SetUseACES(aces);
}
// Bloom:开关即显隐——勾上才显示参数
bool bloom = renderer.GetBloomEnabled();
if (ImGui::Checkbox("Bloom", &bloom))
{
renderer.SetBloomEnabled(bloom);
}
if (bloom)
{
f32 threshold = renderer.GetBloomThreshold();
if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f))
{
renderer.SetBloomThreshold(threshold);
}
f32 strength = renderer.GetBloomStrength();
if (ImGui::SliderFloat("strength", &strength, 0.0f, 2.0f))
{
renderer.SetBloomStrength(strength);
}
}
// 无参数的纯开关:横排平铺
bool shadows = renderer.GetShadowsEnabled();
if (ImGui::Checkbox("Shadows", &shadows))
{
renderer.SetShadowsEnabled(shadows);
}
ImGui::SameLine();
bool taa = renderer.GetTAAEnabled();
if (ImGui::Checkbox("TAA", &taa))
{
renderer.SetTAAEnabled(taa);
}
}
// ---- Environment:IBL 环境 + 调试视图(决定"看什么") ----
if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen))
{
// 动态环境切换(Assets 层加载 → Renderer 换绑;缓存去重)
ImGui::Text("Environment (IBL)");
bool& DrawSkybox = GetScene().GetSettings().DrawSkybox;
ImGui::Checkbox("DrawSkybox", &DrawSkybox);
if (DrawSkybox)
{
int envIdx = m_EnvIndex;
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
{
// 异步:解码后台线程,完成后 OnUpdate 里换绑
m_EnvIndex = envIdx;
RequestEnvironment(envIdx);
}
ImGui::Text(m_EnvLoadBusy ? "(decoding...)" :
"(switch = compute pre-integrate, ms-level)");
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
const auto& viewNames = renderer.GetDebugViewNames();
int view = renderer.GetDebugView();
if (ImGui::Combo("Debug View", &view, viewNames.data(),
static_cast<int>(viewNames.size())))
{
renderer.SetDebugView(view);
}
}else
{
ImGui::ColorEdit4("ClearColor", glm::value_ptr(GetScene().GetSettings().ClearColor));
}
}
// ---- GPU Timings:只读遥测(默认折叠,不占面板高度) ----
if (ImGui::CollapsingHeader("GPU Timings"))
{
// Pass 级 GPU 耗时(RG timestamp)
ImGui::Text("instances: %u main / %u shadow",
renderer.GetMainInstanceCount(),
renderer.GetShadowInstanceCount());
if (renderer.GetInstancesTruncated()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"TRUNCATED: instance buffer full!");
}
const auto& names = renderer.GetPassNames();
const auto& times = renderer.GetPassGpuMs();
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
{
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
}
}
// ---- Frame:帧率控制与显示 ----
if (ImGui::CollapsingHeader("Frame", ImGuiTreeNodeFlags_DefaultOpen))
{
auto& app = *this;
bool vsync = app.GetVSync();
if (ImGui::Checkbox("VSync", &vsync))
{
app.SetVSync(vsync);
if (!vsync)
{
const f32 target = app.GetTargetFps();
app.SetTargetFps(target);
}
}
if (!vsync)
{
f32 target = app.GetTargetFps();
if (ImGui::SliderFloat("target fps", &target, 24.0f, 360.0f, "%.0f"))
{
app.SetTargetFps(target);
}
}
ImGui::Text("fps: %.1f frame: %llu",
1.0f / GetDeltaTime(),
static_cast<unsigned long long>(GetFrameCount()));
}
ImGui::End();
// 自定义重载事件: 重载关闭事件模态框
{
if (m_ShowQuitConfirm)
{
ImGui::OpenPopup("Should quit?");
m_ShowQuitConfirm = false;
}
ImGui::SetNextWindowPos(
ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Should quit?", nullptr,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("Quit the application?");
ImGui::Separator();
if (ImGui::Button("Yes", ImVec2(120, 0)))
{
m_QuitConfirmed = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("No", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (m_QuitConfirmed)
{
m_QuitConfirmed = false;
Close();
}
}
}
void OnWindowClose(const Event& event) override
{
// 退出确认走 Editor 的模态框(ImGui 必须可见才有 UI)
SetImGuiEnabled(true);
m_ShowQuitConfirm = true;
m_Editor->RequestQuit();
}
void OnKeyPress(const Event& event) override
@@ -478,25 +234,51 @@ namespace
ENGINE_INFO("App: window resized to {}x{}", event.width, event.height);
}
void SetEnv(int index = 0) {
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级),
// 其余环境在 Stats 面板里按需 lazy 加载后切换
const auto env = GetAssets().LoadEnvironment(kEnvPaths[index]);
if (env.Valid()) {
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env));
}
}
private:
bool m_ShowQuitConfirm = false;
bool m_QuitConfirmed = false;
// Editor 注入:门面指针 + 反向能力回调(面板 → 沙盒)
void InitEditor()
{
EditorContext ctx;
ctx.renderer = &GetRenderer();
ctx.scene = &GetScene();
ctx.assets = &GetAssets();
ctx.app = this;
ctx.getEnvIndex = [this] { return m_EnvIndex; };
ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; };
ctx.pauseAnim = &m_PauseAnim;
m_Editor->Init(ctx);
// 输入归属:覆盖 EnableImGui 的全局 WantCapture* 谓词
//(全屏 dock host 让它恒真,输入全死)。改为 Editor 按帧
// 判定——Viewport hover/focus 拥有输入(Hazel/UE 语义);
// UI 隐藏(反引号)时 Draw 不再执行,直接全放行
GetWindow().SetInputCapture(
[this] { return GetImGuiEnabled() && m_Editor->BlockingKeyboard(); },
[this] { return GetImGuiEnabled() && m_Editor->BlockingMouse(); });
}
private:
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空
EntityHandle m_Ground{};
EntityHandle m_KeyLight{};
EntityHandle m_Ground{};
EntityHandle m_Gun{}; // 骨骼动画验证实体(M1911)
std::vector<EntityHandle> m_Spinners;
f32 m_Time = 0.0f;
bool m_PauseAnim = true; // 冻结动画时钟(帧历史 diff 验证用)
bool m_PauseAnim = true; // 冻结动画时钟(Editor 面板可开关)
int m_EnvIndex = kDefaultEnv;
// ---- 异步环境加载状态(单 in-flight + pending 接力)----
std::thread m_EnvWorker;
std::string m_EnvLoadingPath;
DecodedEquirect m_EnvDecoded; // worker 写 → join 后主线程读
std::atomic<bool> m_EnvLoadDone{false};
bool m_EnvLoadBusy = false;
int m_EnvPending = -1;
// ---- 异步模型加载(SunTemple)----
AsyncTask<ParsedModel> m_TempleTask;
f32 m_TempleScale = 1.0f; // cm 尺度资产 → 米(解析后应用)
+6
View File
@@ -47,4 +47,10 @@ target_compile_definitions(Engine PUBLIC TRACY_ON_DEMAND)
find_package(Threads REQUIRED)
target_link_libraries(Engine PUBLIC Threads::Threads)
# meshoptimizer(vendored,LOD 网格简化;submodule 机制故障改为直接 vendor)
file(GLOB MESHOPT_SOURCES ${CMAKE_SOURCE_DIR}/Lib/meshoptimizer/src/*.cpp)
add_library(meshopt STATIC ${MESHOPT_SOURCES})
target_include_directories(meshopt PUBLIC ${CMAKE_SOURCE_DIR}/Lib/meshoptimizer/src)
target_link_libraries(Engine PUBLIC meshopt)
target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
@@ -157,6 +157,9 @@ namespace Engine
// 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。
ModelRef CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
// 半透明平面(alpha 0-255,透明绘制队列):同参数缓存去重
ModelRef CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a);
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
@@ -37,6 +37,10 @@ namespace Engine::Assets {
// 程序化纯色平面(size 为半边长)
virtual Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0;
// 半透明纯色平面(alpha 0-255,透明绘制队列)
virtual Scope<Model> CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a) = 0;
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
+1 -1
View File
@@ -88,7 +88,7 @@ namespace Engine {
// 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。
// 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。
virtual void OnRenderOverlay() {}
virtual void OnImGuiRender() {}
// 主循环结束后、析构前(保存、清理场景等)
virtual void OnShutdown() {}
+13
View File
@@ -63,8 +63,15 @@ namespace Engine {
// 输入事件系统
bool IsKeyPressed(int key) const;
// UI 捕获鼠标(ImGui 悬停)时返回 false——面板上的按键归 ImGui
bool IsMouseButtonPressed(int button) const;
void GetMouseDelta(float& dx, float& dy) const;
// 光标捕获(自由视角):DISABLED 隐藏光标出无界增量,NORMAL 恢复。
// 重复设置同值无操作;切换后鼠标增量参考点重置(防跳变)
void SetCursorCaptured(bool captured);
bool IsCursorCaptured() const { return m_CursorCaptured; }
private:
@@ -80,6 +87,12 @@ namespace Engine {
CaptureFn m_KeyboardCapture;
CaptureFn m_MouseCapture;
// 光标捕获状态与鼠标增量参考点(GetMouseDelta 为 const,用 mutable)
bool m_CursorCaptured = false;
mutable bool m_FirstMouse = true;
mutable double m_LastMouseX = 0.0;
mutable double m_LastMouseY = 0.0;
// GLFW 静态回调(通过 window user pointer 找回 this)
static void FramebufferResizeCallback(GLFWwindow* window, int width, int height);
static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
+33
View File
@@ -27,6 +27,8 @@ namespace Engine::ECS {
// ECS 内部遍历不用它(用 index+generation 句柄,性能)。
struct IdComponent {
UUID uuid;
operator UUID() const { return uuid; }
};
// 实体名(调试/查询用;大规模场景建议演进为哈希 id)
@@ -83,6 +85,37 @@ namespace Engine::ECS {
ModelRef model;
};
// 骨骼动画播放状态(纯数据 + 便捷控制)。
// 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者);
// ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。
struct AnimationComponent {
int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
bool playing = true;
bool loop = true;
f32 speed = 1.0f;
f32 time = 0.0f; // 当前 clip 播放头(秒,wrap 由消费端处理)
// ---- crossfade(clip 切换平滑过渡)----
// 来源 clip 淡出期间与目标 clip 并行采样,局部 TRS 层混合
//(palette 数量不变,只是求值端多采一组姿态)
int fadeFrom = -1; // 来源 clip 下标(-1 = 无过渡)
f32 fadeFromTime = 0.0f; // 来源 clip 播放头
f32 fadeElapsed = 0.0f; // 已过渡时长
f32 fadeDuration = 0.3f; // 过渡总时长(秒)
// 切换 clip(带 crossfade)。越界/空 clip 保护在消费端。
void Play(int index, f32 fadeSeconds = 0.3f)
{
if (index == clipIndex) return;
fadeFrom = clipIndex;
fadeFromTime = time;
fadeElapsed = 0.0f;
fadeDuration = fadeSeconds;
clipIndex = index;
time = 0.0f;
}
};
// 相机。一个场景中 primary=true 的实体是当前渲染视角。
struct CameraComponent {
Camera camera;
+1 -1
View File
@@ -50,7 +50,7 @@ namespace Engine::ECS {
// ---- 实体 ----
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy()
EntityHandle CreateEntity(std::string name = {});
EntityHandle CreateEntity(std::string name = "new entity");
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
void FlushDestroyQueue(); // 每帧末尾由 Application 调用
+14
View File
@@ -18,6 +18,8 @@
#pragma once
#include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h> // kMaxFramesInFlight
#include <imgui.h>
#include <vulkan/vulkan.hpp>
namespace Engine {
@@ -35,6 +37,17 @@ namespace Engine {
ImGuiLayer(const ImGuiLayer&) = delete;
ImGuiLayer& operator=(const ImGuiLayer&) = delete;
// ---- 场景显示纹理(RG PresentTarget → ImGui::Image 采样)----
// RG 物理镜像按帧槽位各一份,每槽位各注册一个描述符;
// 重编译(首次/resize)后 view 变化,Renderer 调 Update 重注册。
// 返回值直接传给 ImGui::Image(无场景纹理时为 ImTextureID_Invalid)
ImTextureID GetSceneTexture(u32 slot) const
{
return slot < kMaxFramesInFlight
? m_SceneTextures[slot] : ImTextureID_Invalid;
}
void UpdateSceneTexture(u32 slot, vk::ImageView view);
// ---- 每帧三段式(由 Application::Run 驱动)----
// 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间,
@@ -57,6 +70,7 @@ namespace Engine {
Device& m_Device;
vk::DescriptorPool m_Pool;
bool m_Enabled = true;
ImTextureID m_SceneTextures[kMaxFramesInFlight]{};
};
} // namespace Engine
@@ -98,6 +98,8 @@ namespace Engine::RG {
// 按句柄取当前帧槽位的物理资源(image/view)
vk::ImageView View(TextureHandle h) const;
vk::Image Image(TextureHandle h) const;
// 物理尺寸(导入资源 = SetImportedExtent 设定值;创建资源 = 物理镜像)
vk::Extent2D Extent(TextureHandle h) const;
// 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。
// 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。
@@ -129,6 +131,7 @@ namespace Engine::RG {
// 导入资源每帧绑定
vk::Image importedImage{};
vk::ImageView importedView{};
vk::Extent2D importedExtent{}; // 物理尺寸(swapchain 重建后更新)
// 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体)
struct SlotState {
@@ -207,6 +210,11 @@ namespace Engine::RG {
// 每帧 acquire 后调用,把当帧 swapchain image 绑到导入资源
void SetImportedImage(TextureHandle h, vk::Image image, vk::ImageView view);
// 导入资源的物理尺寸(swapchain 重建后同步更新)。pass 渲染区域
// 跟实际附件尺寸走:backbuffer = 窗口尺寸,与目标尺寸(场景渲染
// 分辨率)解耦——Editor 视口模式下二者不同
void SetImportedExtent(TextureHandle h, vk::Extent2D extent);
// 指定帧槽位的物理镜像 view(Compile 后有效)。
// 用途:把 RG 管理的资源绑定到外部描述符(如阴影图采样)。
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
+4 -3
View File
@@ -22,7 +22,7 @@ namespace Engine {
class Camera {
public:
// 输入驱动:WASD/Space/Shift 移动 + 鼠标视角(需 SetMouseLookEnabled)。
// 输入驱动:WASD/Space/Shift 移动 + 按住右键鼠标视角。
// 全部写回传入的 transform,有变更才 Compose。
void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime);
@@ -35,14 +35,15 @@ namespace Engine {
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
// 鼠标视角默认关闭(需要先做 cursor capture 再打开)
// 鼠标视角总开关(默认开)。交互 = 按住右键自由视角(光标捕获),
// 松开释放光标;右键落在 ImGui 面板上时归 UI 不触发相机
void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; }
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
private:
f32 m_MoveSpeed = 2.5f;
f32 m_MouseSensitivity = 0.1f;
bool m_MouseLookEnabled = false;
bool m_MouseLookEnabled = true;
};
} // namespace Engine
+26 -12
View File
@@ -7,13 +7,19 @@
// 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性)
// 2 metallicRoughness(线性:G=粗糙度,B=金属度,glTF 约定)
// 3 emissive(自发光,sRGB) 4 ao(环境光遮蔽,线性)
// 5 材质参数 UBO(fragment,kParamsBinding)
// 5 roughness(独立粗糙度图,R 通道——FBX/OBJ specular 工作流,
// glTF 合图工作流不用此槽)
// 6 材质参数 UBO(fragment,kParamsBinding)
//
// 纹理非拥有(Model 的纹理缓存持有);缺槽由调用方传默认纹理。
// 因子(metallicFactor/roughnessFactor)乘在 MR 贴图上:
// glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1)
// OBJ(specular 工作流,独立 roughness 图或无图)→ metallic=0,
// roughness 因子按贴图有无/Ns 换算——金属度强制 0
// 因子(metallicFactor/roughnessFactor)乘在贴图上:
// glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1),
// factors.z=0 → shader 直接读 MR.g 当粗糙度
// FBX(metalness 独立图 + roughness 独立图)→ metal 图落 MR 槽
// (灰度 R=G=B,.b 读金属度恰好正确),roughness 图落 Roughness
// 槽,factors.z=1 → shader 用 Roughness.r 替换 MR.g
// OBJ(specular 工作流)→ metallic=0,独立 roughness 图同 FBX;
// 无图时 roughness 因子按 Ns 换算
//
#ifndef HELLOVULKAN_MATERIAL_H
@@ -33,9 +39,11 @@ namespace Engine {
class Texture;
class UniformBuffer;
// 材质因子(乘 MR 贴图;无贴图时即最终值)
// 材质因子(乘贴图;无贴图时即最终值)
// x = metallic 因子 y = roughness 因子
// z = 独立粗糙度图开关(1 = 用 Roughness 槽 R 通道替换 MR.g)
struct MaterialParams {
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f}; // x=metallic y=roughness
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f};
};
class Material {
@@ -47,15 +55,17 @@ namespace Engine {
MetallicRoughness = 2,
Emissive = 3,
Ao = 4,
kSlotCount = 5, // 纹理槽数
kParamsBinding = 5, // 参数 UBO binding
Roughness = 5, // 独立粗糙度图(R 通道)
kSlotCount = 6, // 纹理槽数
kParamsBinding = 6, // 参数 UBO binding
};
Material(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout layout,
const std::array<Texture*, kSlotCount>& textures,
const MaterialParams& params);
const MaterialParams& params,
bool transparent = false);
~Material();
Material(const Material&) = delete;
@@ -63,15 +73,19 @@ namespace Engine {
vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; }
Texture* GetTexture(Slot slot) const { return m_Textures[slot]; }
// 透明队列归属(glTF alphaMode=BLEND;CPU 收集侧用)
bool IsTransparent() const { return m_Transparent; }
// 材质缓存键:同贴图组合+同因子共享一份(Model 层使用)
// 材质缓存键:同贴图组合+同因子+同透明语义共享一份(Model 层使用)
static std::string MakeCacheKey(const std::string paths[kSlotCount],
const MaterialParams& params);
const MaterialParams& params,
bool transparent);
private:
std::array<Texture*, kSlotCount> m_Textures{};
vk::DescriptorSet m_DescriptorSet;
Scope<UniformBuffer> m_ParamsBuffer; // per-slot 参数 ring(创建时写死)
bool m_Transparent = false;
};
} // namespace Engine
+34 -5
View File
@@ -8,6 +8,7 @@
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Vertex.h>
#include <Engine/Vulkan/Buffer.h>
#include <vulkan/vulkan.hpp>
#include <vector>
@@ -18,13 +19,18 @@ namespace Engine
class Buffer;
class Material;
// LOD 级数:LOD0 = 原始网格,越高越简(解析期 meshopt_simplify 生成,
// 共享顶点数据,只换索引区间)
inline constexpr u32 kMeshLodCount = 3;
class Mesh
{
public:
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
// lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成
Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices,
const std::vector<u32>& indices,
const std::vector<std::vector<u32>>& lods,
Material* material);
~Mesh();
@@ -32,11 +38,27 @@ namespace Engine
Mesh& operator=(const Mesh&) = delete;
// instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后,
// 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1, u32 firstInstance = 0) const;
// 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取。
// lod:索引区间选择(越界取最高级)
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1,
u32 firstInstance = 0, u32 lod = 0) const;
Material* GetMaterial() const { return m_Material; }
u32 GetIndexCount() const { return m_IndexCount; }
u32 GetLodIndexCount(u32 lod) const
{
return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].indexCount;
}
// 全局连续槽位(SceneRenderer 数组化 batch 查找用,避免逐实例 hash)
u32 GetSlot() const { return m_Slot; }
// ---- MDI 兼容取数(per-mesh buffer:偏移恒 0,firstIndex 即全局)----
vk::Buffer GetVertexBuffer() const { return m_VertexBuffer->Get(); }
vk::Buffer GetIndexBuffer() const { return m_IndexBuffer->Get(); }
u32 GetVertexOffset() const { return 0; }
u32 GetGlobalFirstIndex(u32 lod) const
{
return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].firstIndex;
}
// 局部空间包围球(构造时从顶点算出),视锥剔除用
struct Bounds {
@@ -46,10 +68,17 @@ namespace Engine
const Bounds& GetBounds() const { return m_Bounds; }
private:
// 同一 index buffer 内连续排布的各级索引区间
struct LodRange {
u32 firstIndex = 0;
u32 indexCount = 0;
};
Scope<Buffer> m_VertexBuffer;
Scope<Buffer> m_IndexBuffer;
std::array<LodRange, kMeshLodCount> m_Lods{};
Material* m_Material;
u32 m_IndexCount = 0;
u32 m_Slot = 0;
Bounds m_Bounds;
u64 m_VertexBytes = 0;
+104 -7
View File
@@ -10,7 +10,9 @@
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Mesh.h>
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp>
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
#include <array>
#include <filesystem>
#include <string>
@@ -34,14 +36,59 @@ namespace Engine
// worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。
struct ParsedMesh {
std::vector<Vertex> vertices;
std::vector<u32> indices;
// LOD 链([0] = 原始索引,越往后越简):解析期 meshopt_simplify
// 生成(worker 线程),消费端共享顶点只换索引
std::vector<std::vector<u32>> lods;
std::array<std::string, Material::kSlotCount> texPaths;
MaterialParams params;
bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列)
};
// ---- 骨骼动画数据(assimp aiNode/aiAnimation/aiBone 的引擎侧镜像)----
// 关键帧(time 单位 = clip 原生 tick,运行时按 tps 换算秒)
struct PosKey { f32 time; glm::vec3 value; };
struct RotKey { f32 time; glm::quat value; };
struct SclKey { f32 time; glm::vec3 value; };
// 一条节点动画轨道:作用于 node 表中下标为 nodeIdx 的节点;
// 无轨道的分量运行时用节点静止姿态(localTransform)
struct AnimChannel {
u32 nodeIdx = 0;
std::vector<PosKey> positions;
std::vector<RotKey> rotations;
std::vector<SclKey> scales;
};
struct AnimationClip {
std::string name;
f32 duration = 0.0f; // 秒(已按 tps 换算)
f32 tps = 1.0f; // ticks per second(0 按 1,assimp 惯例)
std::vector<AnimChannel> channels;
};
// aiNode 树拍平表:骨骼 global 变换的累乘骨架(解析期 ProcessNode
// 原本丢弃 mTransformation,骨骼层级必须保留)
struct ModelNode {
std::string name;
i32 parent = -1; // 根节点 = -1
glm::mat4 localTransform{1.0f}; // 静止姿态 local TRS(assimp 行主→glm 列主已转置)
};
// 骨骼 = palette 槽位 ↔ 节点 ↔ 逆绑定矩阵
// palette[i] = global(bone.nodeIdx) × invBind
struct ModelBone {
std::string name;
u32 nodeIdx = 0;
glm::mat4 invBind{1.0f}; // aiBone mOffsetMatrix(绑定姿势全局逆)
};
struct ParsedModel {
std::string path;
bool isGltf = false;
std::vector<ParsedMesh> meshes;
std::vector<ModelNode> nodes;
std::vector<ModelBone> bones;
std::vector<AnimationClip> clips;
bool Valid() const { return !meshes.empty(); }
};
@@ -72,6 +119,12 @@ namespace Engine
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b);
// 半透明纯色平面(alpha 0-255,预乘 alpha 链):透明管线验证用
static Scope<Model> CreateTransparentPlane(
Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b, u8 a);
~Model();
@@ -79,12 +132,23 @@ namespace Engine
Model& operator=(const Model&) = delete;
const std::vector<Scope<Mesh>>& GetMeshes() const { return m_Meshes; }
// 全模型联合包围球(各 mesh bounds 的并集,局部空间):
// 实体级粗剔除用——整个模型在视锥外直接跳过全部 mesh 循环
const Mesh::Bounds& GetBounds() const { return m_Bounds; }
// ---- 骨骼动画运行时数据(无动画模型全部为空,零开销)----
bool HasAnimations() const { return !m_Clips.empty(); }
u32 GetBoneCount() const { return static_cast<u32>(m_Bones.size()); }
const std::vector<AnimationClip>& GetClips() const { return m_Clips; }
const std::vector<ModelNode>& GetNodes() const { return m_Nodes; }
const std::vector<ModelBone>& GetBones() const { return m_Bones; }
private:
Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
ProceduralTag, f32 halfSize, const std::array<u8, 3>& color);
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
bool transparent);
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
Model(Device& device, MemoryAllocator& allocator,
@@ -92,22 +156,49 @@ namespace Engine
vk::DescriptorSetLayout textureLayout);
void LoadModel(const std::string& path);
// aiNode 树拍平成 node 表(DFS 序,parent 下标回填)
static u32 FlattenNodes(const aiNode* node, i32 parent,
std::vector<ModelNode>& out);
// 每帧动画的 channel 轨道提取(nodeName → node 表下标)
static void ParseAnimations(const aiScene* scene,
const std::vector<ModelNode>& nodes,
std::vector<AnimationClip>& out);
static void ProcessNode(const aiNode* node, const aiScene* scene,
std::vector<ParsedMesh>& out,
const std::filesystem::path& modelDir,
bool isGltf);
bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& parentGlobal
= aiMatrix4x4());
static ParsedMesh ProcessMesh(const aiMesh* mesh, const aiScene* scene,
const std::filesystem::path& modelDir,
bool isGltf);
bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& globalTransform
= aiMatrix4x4());
// mesh 的 aiBone 权重灌进顶点 + 模型级骨骼表注册去重
static void CollectMeshBones(const aiMesh* mesh,
std::vector<Vertex>& vertices,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots);
void InitDefaultTextures();
// 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除)
void RebuildUnionBounds();
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
// 材质缓存入口:同贴图组合+因子共享同一 Material(一个描述符集),
// 材质缓存入口:同贴图组合+同因子+同透明语义共享一份 Material,
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
// paths 下标 = Material::Slot;params = metallic/roughness 因子
// paths 下标 = Material::Slot;params = metallic/roughness 因子;
// slotOverrides = 程序化模型的槽位纹理直供(非空项优先于路径/默认)
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params);
const MaterialParams& params,
bool transparent = false,
const std::array<Texture*, Material::kSlotCount>&
slotOverrides = {});
Device& m_Device;
MemoryAllocator& m_Allocator;
@@ -122,8 +213,14 @@ namespace Engine
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
std::vector<Scope<Mesh>> m_Meshes;
Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除)
std::string m_ModelPath;
bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV
// ---- 骨骼动画运行时数据(FromParsed/LoadModel 从解析产物 move)----
std::vector<ModelNode> m_Nodes;
std::vector<ModelBone> m_Bones;
std::vector<AnimationClip> m_Clips;
};
} // namespace Engine
@@ -0,0 +1,72 @@
//
// Created by atdunbg on 2026/9/29.
//
// BlitPass:显示模式适配器(kDisplayDirect 专用)。
// Direct 模式:vkCmdBlitImage 把 PresentTarget(tonemap 输出)拷进
// backbuffer,ImGui 随后 loadOp=Load 叠加 —— 场景铺满窗口的游戏形态。
// Viewport(Editor)模式:空转,backbuffer 由 ImGui 清屏独占,场景
// 经 ImGui::Image 呈现。
//
// 用 vkCmdBlitImage 而非着色器:同格式全屏拷贝零 shader 成本(声明
// TransferSrc/TransferDst use,layout/barrier 由 RG 推导)。Blit 不在
// rendering scope 内,直接录在 cmd 上,合法。
//
// 注:Blit 只在 Direct 模式运行,此时 PresentTarget 与 backbuffer 同为
// 目标尺寸(1:1),src/dst offset 均取本 pass 渲染区域即可。
//
#ifndef HELLOVULKAN_BLITPASS_H
#define HELLOVULKAN_BLITPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <array>
namespace Engine::Passes {
class BlitPass final : public RenderPass {
public:
BlitPass(FrameSceneData& frame, RG::TextureHandle src,
RG::TextureHandle dst)
: m_Frame(frame), m_Src(src), m_Dst(dst) {}
void Register(RG::RenderGraph& rg) override
{
rg.AddPass("Blit", [this](RG::RenderGraphContext& ctx) {
if (m_Frame.displayMode != kDisplayDirect) return; // Editor 模式空转
vk::ImageBlit region;
region.setSrcSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1})
.setDstSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1});
// src/dst 各按自己附件的物理尺寸取 offset(Direct 模式下
// 二者同为窗口尺寸;尺寸解耦时此写法也各自正确)
const vk::Extent2D srcExt = ctx.Extent(m_Src);
const vk::Extent2D dstExt = ctx.Extent(m_Dst);
const std::array<vk::Offset3D, 2> srcSpan = {
vk::Offset3D{0, 0, 0},
vk::Offset3D{static_cast<i32>(srcExt.width),
static_cast<i32>(srcExt.height), 1}};
const std::array<vk::Offset3D, 2> dstSpan = {
vk::Offset3D{0, 0, 0},
vk::Offset3D{static_cast<i32>(dstExt.width),
static_cast<i32>(dstExt.height), 1}};
region.setSrcOffsets(srcSpan).setDstOffsets(dstSpan);
ctx.cmd.blitImage(
ctx.Image(m_Src), vk::ImageLayout::eTransferSrcOptimal,
ctx.Image(m_Dst), vk::ImageLayout::eTransferDstOptimal,
{region}, vk::Filter::eLinear);
})
.Read(m_Src, RG::ResourceUse::TransferSrc)
.Write(m_Dst, RG::ResourceUse::TransferDst);
}
private:
FrameSceneData& m_Frame; // displayMode(帧契约)
RG::TextureHandle m_Src; // PresentTarget(tonemap 输出)
RG::TextureHandle m_Dst; // backbuffer
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_BLITPASS_H
@@ -28,6 +28,12 @@ namespace Engine::Passes {
void Register(RG::RenderGraph& rg) override;
private:
// 两段渲染共享的命令录制(shader/描述符绑定 + 几何/光栅/视口状态)。
// translucent=true 绑定 gl_FragDepth=1.0 的透明 frag 变体;
// 深度写与混合状态由各渲染实例作为初始态设置,见 ForwardPass.cpp
void RecordSharedState(const RG::RenderGraphContext& ctx,
vk::CommandBuffer cmd, bool translucent) const;
SceneGpu& m_Gpu;
FrameSceneData& m_Frame;
RG::TextureHandle m_HDRColor;
@@ -56,6 +56,7 @@ namespace Engine {
kDebugIrradiance, // IBL 漫反射辐照度(32² 低频)
kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级)
kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道)
kDebugTransparent, // 仅透明队列(Skybox/Forward 跳过绘制,看透明段内容)
kDebugViewCount
};
@@ -11,6 +11,7 @@
#define HELLOVULKAN_IMGUIPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h> // FrameSceneData(displayMode 帧契约)
namespace Engine {
@@ -21,15 +22,22 @@ namespace Engine {
class ImGuiPass final : public RenderPass {
public:
// 二级指针:Renderer::SetImGuiLayer 在 Register 之后才可能被调
//(EnableImGui 时机由派生类决定),执行期解引用取最新值
ImGuiPass(ImGuiLayer** layer, RG::TextureHandle backbuffer)
: m_Layer(layer), m_Backbuffer(backbuffer) {}
//(EnableImGui 时机由派生类决定),执行期解引用取最新值。
// presentTarget:ImGui::Image 采样的场景纹理(tonemap 输出)。
// loadOp 按 displayMode 分支:Direct = Load(blit 已铺场景),
// Viewport = Clear(唯一写入者,清"桌面底色")
ImGuiPass(ImGuiLayer** layer, FrameSceneData& frame,
RG::TextureHandle backbuffer, RG::TextureHandle presentTarget)
: m_Layer(layer), m_Frame(frame), m_Backbuffer(backbuffer),
m_PresentTarget(presentTarget) {}
void Register(RG::RenderGraph& rg) override;
private:
ImGuiLayer** m_Layer;
FrameSceneData& m_Frame; // displayMode(帧契约)
RG::TextureHandle m_Backbuffer;
RG::TextureHandle m_PresentTarget;
};
} // namespace Passes
@@ -25,22 +25,36 @@ namespace Engine {
class ShaderManager;
class UniformBuffer;
class Cubemap;
class Buffer;
class MemoryAllocator;
namespace Passes {
class SceneGpu {
public:
SceneGpu(Device& device, DescriptorManager& descriptors,
SceneGpu(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
ShaderManager& shaders,
UniformBuffer& ubo, UniformBuffer& objectBuffer,
UniformBuffer& prevObjectBuffer);
UniformBuffer& prevObjectBuffer,
UniformBuffer& skinOffsetBuffer,
UniformBuffer& skinPaletteBuffer);
~SceneGpu();
SceneGpu(const SceneGpu&) = delete;
SceneGpu& operator=(const SceneGpu&) = delete;
// ---- MDI 间接绘制 ----
// CPU 打包的命令/count 落进本槽位 ring(Renderer 在 fence
// 等待后调用——与 UBO/SSBO 上传同一安全窗口)
void UploadIndirect(u32 frameSlot, const FrameSceneData& frame);
// 一次组绘制:bind 共享 megabuffer + drawIndexedIndirectCount
void DrawIndirectGroup(vk::CommandBuffer cmd, u32 frameSlot,
u32 viewIndex, const IndirectGroup& g);
// ---- set 布局(shader 契约,改绑定必须同步 shaders/)----
vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer}
vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer,
// SkinOffsets, SkinPalettes}
vk::DescriptorSetLayout textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO}
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube}
vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO)
@@ -54,7 +68,16 @@ namespace Engine {
//(省 shader 出界分支)
vk::Sampler shadowSampler;
// ---- MDI per-slot ring ----
// 布局:[main | cascade0 | cascade1 | cascade2],每区
// kMaxInstances 条命令预算(每实例至多 1 条命令,必不越界)。
// count buffer 同分区(每组一个 u32,下标 = 组首命令下标)
std::array<Scope<Buffer>, kMaxFramesInFlight> indirectCmdBuffers;
std::array<Scope<Buffer>, kMaxFramesInFlight> indirectCountBuffers;
vk::ShaderEXT forwardVert{}, forwardFrag{};
// 透明变体:同 simple_shader + gl_FragDepth=1.0(见 frag 注释)
vk::ShaderEXT translucentFrag{};
vk::ShaderEXT shadowVert{}, shadowFrag{};
// RG 重编译后重绑阴影图(物理镜像按槽位各一份)
@@ -66,6 +89,7 @@ namespace Engine {
private:
Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors;
};
@@ -1,9 +1,10 @@
//
// Created by atdunbg on 2026/9/23.
//
// TonemapPass:HDR(+bloom)→ 显示空间,全屏三角写 Backbuffer。
// TonemapPass:HDR(+bloom)→ 显示空间,全屏三角写 PresentTarget。
// 显示变换在此显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。
// Backbuffer 从此只接收"显示空间"颜色:tonemap 直接写、ImGui 叠加。
// PresentTarget 是 offscreen 可采样纹理:由 ImGui::Image 呈现(输出位置
// 可自由摆放),backbuffer 不再接收场景画面。
//
#ifndef HELLOVULKAN_TONEMAPPASS_H
@@ -28,7 +29,8 @@ namespace Engine {
PostProcessParams& params,
FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
RG::TextureHandle temporalDiff, RG::TextureHandle backbuffer,
RG::TextureHandle temporalDiff,
RG::TextureHandle presentTarget,
RG::TextureHandle rawHdr, RG::TextureHandle velocity,
RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap1,
RG::TextureHandle shadowMap2);
@@ -46,7 +48,7 @@ namespace Engine {
RG::TextureHandle m_HDR;
RG::TextureHandle m_A;
RG::TextureHandle m_TemporalDiff;
RG::TextureHandle m_Backbuffer;
RG::TextureHandle m_Target; // PresentTarget(offscreen,ImGui 采样呈现)
// ---- 调试视图源(binding 3-7,Register 时声明读防止剔除)----
// 注:TAAColor 视图直接用 binding0(主输入即 TAA 输出),无需重复绑
RG::TextureHandle m_RawHDR; // Forward 直出(TAA 前)
+44 -4
View File
@@ -58,7 +58,7 @@ namespace Engine {
Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete;
void Render(ECS::Scene& scene, Assets::AssetManager& assets);
void Render(ECS::Scene& scene, Assets::AssetManager& assets, f32 dt);
// ---- 组合根(Application)装配其他模块时需要的访问器 ----
Device& GetDevice() const { return *m_Device; }
@@ -72,7 +72,18 @@ namespace Engine {
// 可选叠加层(ImGui):不设置则 RG 里的 ImGui pass 为空转。
// 生命周期:层必须先于 Renderer 析构(Application 成员序保证)。
void SetImGuiLayer(ImGuiLayer* layer) { m_ImGuiLayer = layer; }
void SetImGuiLayer(ImGuiLayer* layer)
{
m_ImGuiLayer = layer;
if (layer) RefreshSceneTextures(); // 后端就绪:注册场景纹理
}
// ---- 场景显示纹理(tonemap 输出,ImGui::Image 可采样显示)----
// 按 m_CurrentFrame 取当帧槽位的描述符(UI 构建段与紧随的
// Render 同槽位)。无 ImGui 层时返回 nullptr
void* GetSceneTexture() const;
// 渲染分辨率(PresentTarget 尺寸,等比适配用)
vk::Extent2D GetRenderExtent() const { return m_Extent; }
// ---- 后处理参数(ImGui 可调,转发到 pass 共享的 PostProcessParams)----
f32 GetExposure() const;
@@ -92,6 +103,8 @@ namespace Engine {
void SetTAAEnabled(bool b);
bool GetBloomEnabled() const;
void SetBloomEnabled(bool b);
bool GetLodEnabled() const;
void SetLodEnabled(bool b); // 关 = 全部强制 LOD0
int GetDebugView() const;
void SetDebugView(int v); // Passes::DebugView 枚举值
// 调试视图名表(下标 = DebugView 枚举值,ImGui combo 直接枚举)
@@ -111,15 +124,34 @@ namespace Engine {
u32 GetMainInstanceCount() const;
u32 GetShadowInstanceCount() const;
bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字)
f32 GetCollectMs() const; // CPU 收集+剔除段耗时
// ---- 显示模式(Editor 视口 ↔ 全屏直出;帧首同步生效)----
int GetDisplayMode() const { return m_DesiredDisplayMode; }
void SetDisplayMode(int mode); // cpp 内定义(SceneRenderer 前置声明)
// Editor 视口的期望渲染尺寸(像素;UI 面板每帧上报,帧首统一生效)
void SetViewportSize(u32 w, u32 h)
{
m_DesiredViewport = vk::Extent2D{w, h};
}
// 垂直同步开关(present mode 切换会重建 swapchain),
// 只应在帧 UI 构建段调用(Render 之前)
void SetVSync(bool vsync);
void SetVSync(bool vsync) { m_DesiredVSync = vsync; } // 帧首同步生效
// 帧首同步:窗口 resize / 视口尺寸 / 显示模式 / vsync 意图统一在
// UI 构建之前落定(waitIdle → 重编译 → 重绑描述符)。必须先于
// UI 构建段调用:本帧 draw data 引用的描述符与物理资源由此保证
// 全程新鲜(resize 中途换描述符会绑出已销毁句柄)
void BeginFrameSync();
private:
void BuildGraph(); // 声明 RG 资源 + 注册 pass 对象
void HandleResize();
void HandleResize(bool recreateSwapchain);
void ReadGpuTimes(u32 frameSlot);
void RefreshSceneTextures(); // 物理 view → ImGui 描述符(重建后刷新)
// 期望渲染分辨率:Editor 视口 = 面板尺寸(未上报时回退 swapchain);
// Direct = swapchain 尺寸
vk::Extent2D DesiredRenderExtent() const;
Window& m_Window;
@@ -131,6 +163,8 @@ namespace Engine {
Scope<UniformBuffer> m_UniformBuffer; // per-slot UBO ring
Scope<UniformBuffer> m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO)
Scope<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity)
Scope<UniformBuffer> m_SkinOffsetBuffer; // 实例→palette 基址 ring(SSBO,与实例序平行)
Scope<UniformBuffer> m_SkinPaletteBuffer; // 骨骼蒙皮矩阵 palette ring(SSBO,帧首 bump)
Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
// 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造
@@ -150,6 +184,7 @@ namespace Engine {
// ---- RenderGraph 资源句柄 ----
RG::TextureHandle m_Backbuffer{};
RG::TextureHandle m_PresentTarget{}; // tonemap 输出(offscreen,ImGui 采样)
RG::TextureHandle m_Depth{};
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps{}; // CSM 级联
RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一
@@ -178,6 +213,11 @@ namespace Engine {
vk::Extent2D m_Extent{};
u32 m_CurrentFrame = 0;
// ---- 帧首同步的意图缓冲(UI 段写入,下一帧帧首落定)----
int m_DesiredDisplayMode = kDisplayDirect; // 默认直接渲染, 视口渲染需要手动指定一下
vk::Extent2D m_DesiredViewport{}; // Editor 视口渲染分辨率(面板上报)
bool m_DesiredVSync = true;
};
} // namespace Engine
+42 -7
View File
@@ -21,8 +21,11 @@
#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 <unordered_map>
namespace Engine {
@@ -34,8 +37,9 @@ namespace Engine {
public:
// 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。
// targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。
// dt 驱动骨骼动画采样推进(crossfade 计时)。
bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
vk::Extent2D targetExtent);
vk::Extent2D targetExtent, f32 dt);
FrameSceneData& Frame() { return m_Frame; }
@@ -55,7 +59,9 @@ namespace Engine {
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
private:
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
@@ -63,29 +69,45 @@ namespace Engine {
//(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;
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
struct ViewScratch {
// cur + prev 合并写:两次随机堆访问 → 一次(cache miss 减半)
struct InstanceMatrices {
glm::mat4 cur;
glm::mat4 prev;
u32 skinBase = 0; // palette 区段基址(0 = identity 槽)
};
CullResult result;
// 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 =
// firstInstance + 连续区间,实体序收集不满足(多实体共享 mesh
// 时实例分散各处),扁平化成连续数组推迟到 appendView
std::vector<std::vector<glm::mat4>> batchWorlds;
std::vector<std::vector<glm::mat4>> batchPrevWorlds;
std::unordered_map<Mesh*, u32> lookup;
std::vector<std::vector<InstanceMatrices>> batchMats;
// 每 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;
batchWorlds.clear();
batchPrevWorlds.clear();
lookup.clear();
batchMats.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 采样)
@@ -96,13 +118,26 @@ namespace Engine {
std::unordered_map<u32, glm::mat4> m_PrevWorlds;
std::unordered_map<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
+64 -2
View File
@@ -18,18 +18,49 @@
namespace Engine {
class Mesh;
class Material;
// GPUDriven:按 mesh 聚合的绘制批
// GPUDriven:按 (mesh, LOD) 聚合的绘制批——同 mesh 不同 LOD 的实例
// 索引区间不同,不能并成一次 draw
struct InstanceBatch {
Mesh* mesh;
u32 first; // 在实例矩阵数组中的起始下标
u32 count; // 实例数
u32 lod = 0; // 绘制用索引区间(Mesh::Draw)
};
// 一个渲染视图的剔除结果(相机视图 / 光源视图各一份)
// MDI:一次 vkCmdDrawIndexedIndirectCount 消费的命令(CPU 侧镜像,
// 内存布局 = VkDrawIndexedIndirectCommand,20B)
struct IndirectDrawCmd {
u32 indexCount;
u32 instanceCount;
u32 firstIndex; // megabuffer 里的全局索引下标
u32 vertexOffset; // megabuffer 里的顶点元素偏移
u32 firstInstance; // 实例矩阵数组下标
};
static_assert(sizeof(IndirectDrawCmd) == 20, "布局必须镜像 VkDrawIndexedIndirectCommand");
// MDI:共享同一 megabuffer(同 Model)的一组命令——
// 一次 bindVertex/IndexBuffer +(Forward)一次材质 Set1 绑定 +
// 一次 drawIndexedIndirectCount。组内命令在 indirect buffer 中连续
struct IndirectGroup {
const Mesh* mesh = nullptr; // 取共享 VB/IB 句柄(组内同一份)
Material* material = nullptr; // shadow 组为 null(depth-only 无材质)
u32 firstCommand = 0; // 命令区起始下标(count 同下标)
u32 commandCount = 0;
};
// indirect buffer 的视图分区:[main | cascade0 | cascade1 | cascade2],
// 每区 kMaxInstances 条命令预算(每实例至多 1 条,必不越界)
constexpr u32 kIndirectViewMain = 0;
// 一个渲染视图的剔除结果(相机视图 / 光源视图各一份)。
// batches = 逐实例批次(透明队列仍逐批 draw);indirect = MDI 打包产物
struct CullResult {
std::vector<InstanceBatch> batches;
u32 instanceCount = 0;
std::vector<IndirectDrawCmd> indirectCommands;
std::vector<IndirectGroup> indirectGroups;
};
constexpr u32 kMaxLights = 4;
@@ -41,6 +72,13 @@ namespace Engine {
constexpr u32 kMaxCascades = 3; // CSM 级联数
constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长
// 蒙皮矩阵预算(palette 总槽位数/帧):每动画实体一段(按实体分配,
// 不按模型去重——同模型两实体可不同播放状态)。4096×64B = 256KB/槽位
constexpr u32 kMaxSkinMatrices = 4096;
// palette slot 0 固定 identity:无骨骼顶点(weights=(1,0,0,0), idx=0)
// 直通它 → 静态模型蒙皮零影响
constexpr u32 kIdentityPaletteSlot = 0;
// 镜像 shader 侧 UBO 布局的灯光项(std140,每灯 64B)。
// 字段未用部分置零——shader 按类型分支读取
struct LightData {
@@ -68,6 +106,14 @@ namespace Engine {
glm::vec4 cameraPos; // 相机世界位置(高光 V 向量)
};
// 显示模式(帧契约 displayMode 字段;ImGui 经 Renderer 门面写入):
// Direct = 全屏直出:Blit pass 把 PresentTarget 拷进 backbuffer,
// ImGui 叠加其上(游戏形态)
// Viewport = Editor 视口:backbuffer 只画 ImGui(清屏),场景经
// ImGui::Image 呈现;渲染分辨率跟随 ImGui 面板尺寸
inline constexpr int kDisplayDirect = 0;
inline constexpr int kDisplayViewport = 1;
// 每帧从场景收集的渲染数据(SceneRenderer 生产,pass 消费)。
// 复用成员容量:clear() 不缩容,稳态零每帧堆分配。
struct FrameSceneData {
@@ -85,21 +131,37 @@ namespace Engine {
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
// 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵
std::vector<glm::mat4> prevInstanceModels;
// ---- 蒙皮(与 instanceModels 平行的 per-instance 间接索引)----
// skinOffsets[i] = 第 i 个实例的 palette 区段起点(kMaxSkinMatrices
// 全局数组内下标);无动画实体指向 slot0 identity
std::vector<u32> skinOffsets;
// palette 区段池:帧首 bump allocator 按动画实体分配
// [skinBase .. skinBase+boneCount) = 该实体全部骨骼的蒙皮矩阵
std::vector<glm::mat4> skinPalettes;
CullResult mainView; // 相机视锥 → Forward pass
// 每级联独立剔除结果(Shadow pass 按 region 各画各的——
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
CullResult shadowCascades[kMaxCascades];
// 透明批次(预乘 alpha 混合,深度只测不写,远→近排序);
// 不参与阴影投射
CullResult translucentView;
// ---- 运行时开关/调试视图(ImGui 经 Renderer 门面写入,
// SceneRenderer 唯一落帧,各 pass 只读;每帧确定性)----
bool shadowsEnabled = true; // ShadowPass 空转 + frag ShadowFactor 短路
bool taaEnabled = true; // TAAPass 直通(alpha=0) + 停投影抖动
bool bloomEnabled = true; // BloomPass 子 pass 空转 + tonemap 强度归零
bool lodEnabled = true; // 关 = 实例收集恒选 LOD0(绘制原网格)
int debugView = 0; // Passes::DebugView 枚举(tonemap 路由上屏)
int displayMode = kDisplayViewport; // 显示模式(Blit 路由 + ImGui loadOp)
// 实例预算溢出(kMaxInstances):本轮收集有实例被静默丢弃——
// 必须遥测可见,"只告警一次"的大场景截断等于没有告警
bool instancesTruncated = false;
// palette 预算溢出(kMaxSkinMatrices):动画实体 palette 被截断
bool skinPaletteTruncated = false;
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
};
+20 -2
View File
@@ -16,6 +16,14 @@ namespace Engine {
glm::vec2 uv;
glm::vec3 tangent; // 切线(法线贴图 TBN 用;assimp CalcTangentSpace)
// ---- 骨骼蒙皮属性 ----
// 无骨骼资产保持默认值(weights=(1,0,0,0) + palette[0]=identity)
// → skin 矩阵 = identity,蒙皮路径对静态模型零影响。
// weights 必须带默认初始化器:聚合初始化 Vertex{pos,normal,uv,tangent}
// 会把未列出的成员零初始化 → skin=mat4(0) → 顶点全部塌缩到原点
glm::uvec4 boneIds{0, 0, 0, 0};
glm::vec4 weights{1.0f, 0.0f, 0.0f, 0.0f};
static vk::VertexInputBindingDescription2EXT GetBindingDescription() {
vk::VertexInputBindingDescription2EXT binding;
binding.setBinding(0)
@@ -25,8 +33,8 @@ namespace Engine {
return binding;
}
static std::array<vk::VertexInputAttributeDescription2EXT, 4> GetAttributeDescriptions() {
std::array<vk::VertexInputAttributeDescription2EXT, 4> attrs;
static std::array<vk::VertexInputAttributeDescription2EXT, 6> GetAttributeDescriptions() {
std::array<vk::VertexInputAttributeDescription2EXT, 6> attrs;
attrs[0].setLocation(0)
.setBinding(0)
@@ -48,6 +56,16 @@ namespace Engine {
.setFormat(vk::Format::eR32G32B32Sfloat)
.setOffset(offsetof(Vertex, tangent));
attrs[4].setLocation(4)
.setBinding(0)
.setFormat(vk::Format::eR32G32B32A32Uint)
.setOffset(offsetof(Vertex, boneIds));
attrs[5].setLocation(5)
.setBinding(0)
.setFormat(vk::Format::eR32G32B32A32Sfloat)
.setOffset(offsetof(Vertex, weights));
return attrs;
}
};
@@ -35,6 +35,8 @@ namespace Engine {
Scope<Model> LoadModel(const std::string& path,
ParsedModel&& parsed) override;
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
Scope<Model> CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a) override;
Scope<Cubemap> LoadEnvironment(const std::string& path) override;
Scope<Cubemap> LoadEnvironment(const std::string& path,
DecodedEquirect&& decoded) override;
+3 -1
View File
@@ -26,7 +26,9 @@ namespace Engine {
Buffer& operator=(const Buffer&) = delete;
vk::Buffer Get() const { return m_Buffer; }
vk::DeviceSize GetSize() const { return m_Size; }
vk::DeviceSize Size() const { return m_Size; }
// 持久映射指针(非 host visible 为 nullptr;诊断读回用)
void* MappedPtr() const { return m_MappedPtr; }
// 只有 HOST_VISIBLE 的 Buffer 能调
void Upload(const void* data, vk::DeviceSize size, vk::DeviceSize offset = 0) const;
+4
View File
@@ -43,6 +43,8 @@ public:
bool IsDynamicRenderingSupported() const { return m_DynamicRenderingSupported; }
bool IsShaderObjectSupported() const { return m_ShaderObjectSupported; }
bool IsSynchronization2Supported() const { return m_Synchronization2Supported; }
// MDI:drawCount>1 的 drawIndexedIndirect(Count) 必需(core 特性位)
bool IsMultiDrawIndirectSupported() const { return m_MultiDrawIndirectSupported; }
private:
void CreateInstance();
@@ -72,6 +74,8 @@ private:
bool m_DynamicRenderingSupported = false;
bool m_ShaderObjectSupported = false;
bool m_Synchronization2Supported = false;
bool m_MultiDrawIndirectSupported = false;
bool m_DrawIndirectCountSupported = false;
std::vector<const char*> m_ValidationLayers = { "VK_LAYER_KHRONOS_validation" };
std::vector<const char*> m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
+3
View File
@@ -31,6 +31,9 @@ namespace Engine {
struct ProceduralTag {};
static Scope<Texture> CreateSolidColor(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b, bool srgb = true);
// 带 alpha 的纯色纹理(透明管线验证等)
static Scope<Texture> CreateSolidColorRGBA(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b, u8 a, bool srgb = true);
~Texture();
+12
View File
@@ -41,6 +41,18 @@ namespace Engine::Assets {
[&](const std::string&) { return m_Uploader.CreatePlane(halfSize, r, g, b); });
}
ModelRef AssetManager::CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a)
{
std::string key = "procedural://transparent-plane/" +
std::to_string(halfSize) + "/" +
std::to_string(r) + "," + std::to_string(g) + "," +
std::to_string(b) + "," + std::to_string(a);
return m_Models.Load(key, [&](const std::string&) {
return m_Uploader.CreateTransparentPlane(halfSize, r, g, b, a);
});
}
EnvRef AssetManager::LoadEnvironment(const std::string& path)
{
// GPU 生成(compute 预积分)经接口发生在渲染层实现里
+14 -4
View File
@@ -61,7 +61,10 @@ namespace Engine {
void Application::EnableImGui()
{
if (m_ImGuiLayer) return;
if (m_ImGuiLayer) {
ENGINE_WARN("ImGui layer already enabled, do not enable twice!");
return;
}
m_ImGuiLayer = CreateScope<ImGuiLayer>(GetWindow(), *m_Renderer);
m_Renderer->SetImGuiLayer(m_ImGuiLayer.get());
@@ -108,24 +111,31 @@ namespace Engine {
m_Scene->FlushDestroyQueue();
m_Scene->UpdateTransforms(); // UE transform-update 阶段:渲染前保证 world 新鲜
// 帧首同步:窗口 resize / 渲染尺寸(面板跟随)/ 显示模式 / vsync
// 统一在 UI 构建"之前"落定——此处可能重建 swapchain 与 RG 目标
// 并重注册 ImGui 场景纹理;若挪到 UI 构建之后,本帧 draw data
// 会引用刚被销毁的描述符(validation: Invalid VkDescriptorSet)
m_Renderer->BeginFrameSync();
// ---- UI 构建段:OnRenderOverlay 只"声明"控件,不直接碰 GPU ----
if (m_ImGuiLayer && m_ImGuiLayer->IsEnabled())
{
const auto uiStart = std::chrono::steady_clock::now();
m_ImGuiLayer->NewFrame();
OnRenderOverlay();
OnImGuiRender();
m_ImGuiLayer->BuildDrawData();
const auto uiEnd = std::chrono::steady_clock::now();
}
// ---- 主渲染(框架行为):RG 顺序 Forward → ImGui(loadOp=Load) →
// Present,draw data 在 ImGui pass 里才真正提交 GPU ----
m_Renderer->Render(*m_Scene, *m_Assets);
m_Renderer->Render(*m_Scene, *m_Assets, m_DeltaTime);
// ---- 帧率控制:vsync 开启时 present 自带节奏;否则按目标限帧 ----
// 策略:先 sleep 到剩 ~2ms,再自旋——精度高且不烧 CPU
if (!m_VSync) {
// TargetFps <= 0 表示不限帧(跳过 sleep,避免 1e6/0 除零 UB)
if (!m_VSync && m_TargetFps > 0) {
const auto deadline = frameStart +
std::chrono::microseconds(static_cast<long long>(
1e6 / m_TargetFps));
+41 -17
View File
@@ -81,12 +81,19 @@ namespace Engine
void Window::WindowFocusCallback(GLFWwindow* window, int focused)
{
const auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window));
auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window));
if (!self->m_EventCallback) return;
Event e;
e.type = focused ? EventType::WindowFocus : EventType::WindowLostFocus;
self->m_EventCallback(e);
// 失焦时释放光标捕获(切窗口不会留下隐形被捕获的光标;
// 相机下一帧按右键状态自行决定是否重新捕获)
if (!focused && self->m_CursorCaptured)
{
self->SetCursorCaptured(false);
}
}
void Window::WindowCloseCallback(GLFWwindow* window)
@@ -120,7 +127,7 @@ namespace Engine
glfwTerminate();
return;
}
glfwSetWindowOpacity(m_Window, 0.5f);
glfwSetWindowOpacity(m_Window, 0.0f);
ENGINE_INFO("Window created: {} ({}x{})", title, width, height);
@@ -191,32 +198,49 @@ namespace Engine
return glfwGetKey(m_Window, key) == GLFW_PRESS;
}
bool Window::IsMouseButtonPressed(const int button) const
{
// UI 捕获鼠标时(ImGui 悬停)不占用:面板上的点击归 ImGui。
// 已捕获光标(自由视角中)时绕过——虚拟光标位置无意义
if (!m_CursorCaptured && m_MouseCapture && m_MouseCapture()) return false;
return glfwGetMouseButton(m_Window, button) == GLFW_PRESS;
}
void Window::SetCursorCaptured(const bool captured)
{
if (m_CursorCaptured == captured) return;
m_CursorCaptured = captured;
glfwSetInputMode(m_Window, GLFW_CURSOR,
captured ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL);
// DISABLED 模式光标是虚拟无界坐标,与上次普通坐标不连续——
// 重置参考点,下一帧只建立基准不出增量(防视角跳变)
m_FirstMouse = true;
}
void Window::GetMouseDelta(float& dx, float& dy) const
{
double mx, my;
glfwGetCursorPos(m_Window, &mx, &my);
static double lastX = mx;
static double lastY = my;
static bool first = true;
if (first)
// 首帧(或捕获切换后):只建立参考点
if (m_FirstMouse)
{
lastX = mx;
lastY = my;
first = false;
m_LastMouseX = mx;
m_LastMouseY = my;
m_FirstMouse = false;
dx = dy = 0.0f;
return;
}
dx = static_cast<float>(mx - lastX);
dy = static_cast<float>(my - lastY);
dx = static_cast<float>(mx - m_LastMouseX);
dy = static_cast<float>(my - m_LastMouseY);
m_LastMouseX = mx;
m_LastMouseY = my;
lastX = mx;
lastY = my;
// UI 捕获鼠标时 delta 归零;lastX/Y 照常更新,避免释放后跳变
if (m_MouseCapture && m_MouseCapture())
// UI 捕获鼠标(ImGui 悬停)时 delta 归零;参考点照常更新,避免
// 释放后跳变。光标被相机捕获时绕过——虚拟光标可能悬在 ImGui
// 面板上方,若被归零视角会卡死
if (!m_CursorCaptured && m_MouseCapture && m_MouseCapture())
{
dx = dy = 0.0f;
}
+1 -3
View File
@@ -16,9 +16,7 @@ namespace Engine::ECS {
// 出生即有:UUID(稳定身份)+ Transform(空间存在性,Unity 语义)
m_Registry.emplace<IdComponent>(e, IdComponent{UUID::New()});
m_Registry.emplace<TransformComponent>(e);
if (!name.empty()) {
m_Registry.emplace<NameComponent>(e, NameComponent{std::move(name)});
}
m_Registry.emplace<NameComponent>(e, NameComponent{std::move(name)});
return EntityHandle{this, e};
}
+25
View File
@@ -95,6 +95,11 @@ namespace Engine {
// 后端销毁自己的管线/字体资源前先等 GPU 空闲(层先于 Renderer 析构)
m_Device.GetDevice().waitIdle();
// 场景纹理描述符先归还(后端 shutdown 前必须完成)
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
UpdateSceneTexture(slot, vk::ImageView{});
}
ImGui_ImplVulkan_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
@@ -104,6 +109,26 @@ namespace Engine {
ENGINE_INFO("ImGuiLayer shut down");
}
void ImGuiLayer::UpdateSceneTexture(u32 slot, vk::ImageView view)
{
if (slot >= kMaxFramesInFlight) return;
// 旧描述符先归还(池带 eFreeDescriptorSet;resize 重建时 GPU 已
// waitIdle,无在飞引用)
if (m_SceneTextures[slot]) {
ImGui_ImplVulkan_RemoveTexture(
(VkDescriptorSet)(intptr_t)m_SceneTextures[slot]);
m_SceneTextures[slot] = ImTextureID_Invalid;
}
if (view) {
// 后端自有采样器(linear):只需提供 view 与采样期 layout——
// RG 已保证 ImGuiPass 执行期处于 eShaderReadOnlyOptimal
m_SceneTextures[slot] = (ImTextureID)(intptr_t)
ImGui_ImplVulkan_AddTexture(
(VkImageView)view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
}
}
void ImGuiLayer::NewFrame()
{
ImGui_ImplVulkan_NewFrame();
+20 -3
View File
@@ -7,6 +7,7 @@
#include <Engine/Core/Log.h>
#include <algorithm>
#include <ranges>
#include <set>
#include <stdexcept>
@@ -41,8 +42,8 @@ namespace Engine::RG {
ReleasePhysical();
const auto& vkDevice = m_Device.GetDevice();
for (auto& [key, images] : m_Pool) {
for (auto& img : images) {
for (auto &images: m_Pool | std::views::values) {
for (const auto& img : images) {
if (!img->valid) continue;
vkDevice.destroyImageView(img->view);
m_Allocator.Destroy(img->allocation); // image 资源与显存成对释放
@@ -57,7 +58,7 @@ namespace Engine::RG {
{
CheckMutable();
TextureHandle h{static_cast<u32>(m_Textures.size()), 1};
const TextureHandle h{static_cast<u32>(m_Textures.size()), 1};
TextureEntry entry;
entry.name = std::move(name);
@@ -101,6 +102,15 @@ namespace Engine::RG {
entry.importedView = view;
}
void RenderGraph::SetImportedExtent(TextureHandle h, vk::Extent2D extent)
{
auto& entry = GetEntry(h);
if (!entry.imported) {
ENGINE_FATAL("RenderGraph: SetImportedExtent on non-imported resource '{}'", entry.name);
}
entry.importedExtent = extent;
}
// ---------- Pass 声明 ----------
RenderGraph::PassBuilder RenderGraph::AddPass(std::string name, ExecuteFn exec)
@@ -832,6 +842,13 @@ namespace Engine::RG {
return entry.physical[frame.frameSlot]->image;
}
vk::Extent2D RenderGraphContext::Extent(TextureHandle h) const
{
const auto& entry = graph->GetEntry(h);
if (entry.imported) return entry.importedExtent;
return entry.physical[frame.frameSlot]->extent;
}
u32 RenderGraphContext::HistorySlot(u32 frameSlot) const
{
return graph->HistorySlot(frameSlot);
+18 -3
View File
@@ -14,21 +14,36 @@ namespace Engine {
void Camera::Update(Window& window, ECS::TransformComponent& t, f32 deltaTime)
{
// 鼠标旋转:直接写实体旋转(yaw → rotation.y,pitch → rotation.x)
if (m_MouseLookEnabled)
// 鼠标视角(编辑器习惯):按住右键捕获光标进入自由视角,
// 松开释放光标(ImGui 面板可正常交互)。m_MouseLookEnabled 总开关
const bool wantLook = m_MouseLookEnabled &&
window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT);
if (wantLook)
{
if (!window.IsCursorCaptured())
{
window.SetCursorCaptured(true);
}
f32 mouseDX = 0, mouseDY = 0;
window.GetMouseDelta(mouseDX, mouseDY);
if (mouseDX != 0 || mouseDY != 0)
{
glm::vec3 rot = t.GetRotation();
rot.y += mouseDX * m_MouseSensitivity;
// 右手系绕 Y 正角度 = 向左转,鼠标右移(DX>0)应看向右
// → yaw 取负。pitch 正角度 = 抬头,鼠标下移(DY>0)低头
// → rot.x 直接用负号,两者符号已核对一致
rot.y -= mouseDX * m_MouseSensitivity;
rot.x = glm::clamp(
rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f);
t.SetRotation(rot);
}
}
else if (window.IsCursorCaptured())
{
window.SetCursorCaptured(false);
}
// 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X,
// 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机)
+6 -2
View File
@@ -17,8 +17,10 @@ namespace Engine
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout layout,
const std::array<Texture*, kSlotCount>& textures,
const MaterialParams& params)
const MaterialParams& params,
bool transparent)
: m_Textures(textures)
, m_Transparent(transparent)
{
m_DescriptorSet = descriptorManager.AllocateDescriptorSet(layout);
@@ -44,7 +46,8 @@ namespace Engine
Material::~Material() = default;
std::string Material::MakeCacheKey(const std::string paths[kSlotCount],
const MaterialParams& params)
const MaterialParams& params,
bool transparent)
{
std::string key;
for (u32 slot = 0; slot < kSlotCount; ++slot) {
@@ -53,6 +56,7 @@ namespace Engine
}
key += "f:" + std::to_string(params.factors.x) +
"," + std::to_string(params.factors.y);
key += transparent ? ":t" : ":o"; // 透明语义入键(描述符相同但队列不同)
return key;
}
} // namespace Engine
+40 -10
View File
@@ -10,20 +10,28 @@
#include <Engine/Core/Log.h>
#include <limits>
#include <atomic>
#include "Engine/Core/Stats.h"
namespace Engine
{
namespace {
// 全局连续 mesh 槽位:SceneRenderer 的 batch 查找用数组寻址
//(每实例每视图一次 hash 的代价在大场景下是 5ms 级)
std::atomic<u32> s_nextMeshSlot{0};
}
Mesh::Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices,
const std::vector<u32>& indices,
const std::vector<std::vector<u32>>& lods,
Material* material)
: m_Material(material)
, m_Slot(s_nextMeshSlot.fetch_add(1, std::memory_order_relaxed))
{
if (vertices.empty() || indices.empty())
if (vertices.empty() || lods.empty() || lods[0].empty())
{
ENGINE_ERROR("Mesh: empty vertices or indices");
ENGINE_ERROR("Mesh: empty vertices or LOD0 indices");
return;
}
@@ -35,15 +43,31 @@ namespace Engine
vk::MemoryPropertyFlagBits::eHostCoherent);
m_VertexBuffer->Upload(vertices.data(), vbSize);
u64 ibSize = sizeof(u32) * indices.size();
// 各级索引连续拼进同一 index buffer(共享顶点,只换索引区间)
std::vector<u32> allIndices;
for (const auto& idx : lods) {
allIndices.insert(allIndices.end(), idx.begin(), idx.end());
}
u64 ibSize = sizeof(u32) * allIndices.size();
m_IndexBuffer = CreateScope<Buffer>(
device, allocator, ibSize,
vk::BufferUsageFlagBits::eIndexBuffer,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent);
m_IndexBuffer->Upload(indices.data(), ibSize);
m_IndexBuffer->Upload(allIndices.data(), ibSize);
m_IndexCount = static_cast<u32>(indices.size());
// 记录各级区间。LOD 不足的级复用最后一级的"已有区间"
//(first 不得继续累加——buffer 里没有重复数据,累加必越界)
u32 first = 0;
for (u32 lod = 0; lod < kMeshLodCount; ++lod) {
if (lod < lods.size()) {
m_Lods[lod] = {first, static_cast<u32>(lods[lod].size())};
first += m_Lods[lod].indexCount;
} else {
m_Lods[lod] = m_Lods[lods.size() - 1];
}
}
// 局部空间包围球:顶点包围盒 → 中心 + 最大半径(视锥剔除用)
{
@@ -61,10 +85,12 @@ namespace Engine
}
}
ENGINE_INFO("Mesh created: {} vertices, {} indices", vertices.size(), indices.size());
ENGINE_INFO("Mesh created: {} vertices, LOD0 {} indices", vertices.size(), lods[0].size());
m_VertexBytes = sizeof(Vertex) * vertices.size();
m_IndexBytes = sizeof(u32) * indices.size();
for (const auto& idx : lods) {
m_IndexBytes += sizeof(u32) * idx.size();
}
STATS.AddMesh(m_VertexBytes, m_IndexBytes);
}
@@ -73,10 +99,14 @@ namespace Engine
STATS.RemoveMesh(m_VertexBytes, m_IndexBytes);
}
void Mesh::Draw(vk::CommandBuffer cmd, u32 instanceCount, u32 firstInstance) const
void Mesh::Draw(vk::CommandBuffer cmd, u32 instanceCount, u32 firstInstance,
u32 lod) const
{
const LodRange& range =
m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1];
cmd.bindVertexBuffers(0, m_VertexBuffer->Get(), {0});
cmd.bindIndexBuffer(m_IndexBuffer->Get(), 0, vk::IndexType::eUint32);
cmd.drawIndexed(m_IndexCount, instanceCount, 0, 0, firstInstance);
cmd.drawIndexed(range.indexCount, instanceCount, range.firstIndex,
0, firstInstance);
}
} // namespace Engine
+377 -45
View File
@@ -13,12 +13,28 @@
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <assimp/GltfMaterial.h> // AI_MATKEY_GLTF_ALPHAMODE
#include <meshoptimizer.h>
#include <filesystem>
#include <algorithm>
namespace Engine {
namespace {
// assimp 行主矩阵 → glm 列主(转置搬运;骨骼 node TRS / invBind 共用)
glm::mat4 ToGlm(const aiMatrix4x4& m)
{
glm::mat4 out;
out[0][0] = m.a1; out[1][0] = m.a2; out[2][0] = m.a3; out[3][0] = m.a4;
out[0][1] = m.b1; out[1][1] = m.b2; out[2][1] = m.b3; out[3][1] = m.b4;
out[0][2] = m.c1; out[1][2] = m.c2; out[2][2] = m.c3; out[3][2] = m.c4;
out[0][3] = m.d1; out[1][3] = m.d2; out[2][3] = m.d3; out[3][3] = m.d4;
return out;
}
}
Model::Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
@@ -43,6 +59,23 @@ Model::Model(Device& device, MemoryAllocator& allocator,
Model::~Model() = default;
void Model::RebuildUnionBounds()
{
if (m_Meshes.empty()) {
m_Bounds = {};
return;
}
// 各 mesh 包围球 → AABB 并集 → 外接球(保守,宁多画不漏画)
glm::vec3 mn(1e9f), mx(-1e9f);
for (const auto& mesh : m_Meshes) {
const auto& b = mesh->GetBounds();
mn = glm::min(mn, b.center - glm::vec3(b.radius));
mx = glm::max(mx, b.center + glm::vec3(b.radius));
}
m_Bounds.center = (mn + mx) * 0.5f;
m_Bounds.radius = 0.5f * glm::length(mx - mn);
}
void Model::InitDefaultTextures()
{
// 各材质槽位的兜底纹理(1x1,开销可忽略):
@@ -56,21 +89,34 @@ void Model::InitDefaultTextures()
m_DefaultTextures[Material::Emissive] =
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0);
m_DefaultTextures[Material::Ao] =
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 255, 255, false);
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 0, 0, false);
// 独立粗糙度槽兜底:0.5 灰(无图资产 = 中性粗糙度)
m_DefaultTextures[Material::Roughness] =
Texture::CreateSolidColor(m_Device, m_Allocator, 128, 128, 128, false);
}
Model::Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
ProceduralTag, f32 halfSize, const std::array<u8, 3>& color)
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
bool transparent)
: m_Device(device)
, m_Allocator(allocator)
, m_DescriptorManager(descriptorManager)
, m_TextureLayout(textureLayout)
{
// 纯色 1x1 贴图(albedo 兜底)
// 纯色贴图直供 albedo 槽(alpha 非全 1 时为半透明材质);
// 生命周期挂进纹理缓存(键含 rgba 值,同色共享、异色互不冲突)
InitDefaultTextures();
auto tex = Texture::CreateSolidColor(device, allocator, color[0], color[1], color[2]);
Scope<Texture> tex = (rgba[3] == 255)
? Texture::CreateSolidColor(device, allocator, rgba[0], rgba[1], rgba[2])
: Texture::CreateSolidColorRGBA(device, allocator,
rgba[0], rgba[1], rgba[2], rgba[3]);
Texture* albedoOverride = tex.get();
const std::string texKey = "proc:" + std::to_string(rgba[0]) + ',' +
std::to_string(rgba[1]) + ',' + std::to_string(rgba[2]) + ',' +
std::to_string(rgba[3]);
m_TextureCache.emplace(texKey, std::move(tex));
// XZ 平面,法线 +Y,从上往下看 CCW 绕序
const f32 h = halfSize;
@@ -81,13 +127,19 @@ Model::Model(Device& device, MemoryAllocator& allocator,
{{ h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}},
};
const std::vector<u32> indices = {0, 1, 2, 0, 2, 3};
const std::vector<std::vector<u32>> lodIndices = {indices};
// 材质/纹理生命周期随模型(缓存持有)
// 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo
// 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训)
std::string paths[Material::kSlotCount] = {"", "", "", ""};
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}); // 全默认材质
std::array<Texture*, Material::kSlotCount> overrides{};
overrides[Material::Albedo] = albedoOverride;
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, transparent,
overrides);
m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, vertices, indices, mat));
m_Device, m_Allocator, vertices, lodIndices, mat));
RebuildUnionBounds();
}
Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
@@ -97,7 +149,20 @@ Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
{
return Scope<Model>(new Model(device, allocator, descriptorManager,
textureLayout, ProceduralTag{},
halfSize, std::array<u8, 3>{r, g, b}));
halfSize, std::array<u8, 4>{r, g, b, 255},
false));
}
Scope<Model> Model::CreateTransparentPlane(
Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b, u8 a)
{
return Scope<Model>(new Model(device, allocator, descriptorManager,
textureLayout, ProceduralTag{},
halfSize, std::array<u8, 4>{r, g, b, a},
true));
}
ParsedModel Model::ParseFromDisk(const std::string& path)
@@ -125,20 +190,177 @@ ParsedModel Model::ParseFromDisk(const std::string& path)
(out.isGltf ? 0 : aiProcess_FlipUVs) |
aiProcess_GenSmoothNormals |
aiProcess_CalcTangentSpace | // 法线贴图 TBN
aiProcess_JoinIdenticalVertices);
aiProcess_JoinIdenticalVertices |
aiProcess_LimitBoneWeights); // 每顶点限 4 权重(= 顶点格式 boneIds 容量)
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
if (!scene->mNumMeshes) {
ENGINE_WARN("Assimp warn: model dose not have mesh assets\nmodel has following asset:");
ENGINE_WARN("\tMeshes Count: {}", scene->mNumMeshes);
ENGINE_WARN("\tAnimations Count: {}", scene->mNumAnimations);
ENGINE_WARN("\tCameras Count: {}", scene->mNumCameras);
ENGINE_WARN("\tLights Count: {}", scene->mNumLights);
ENGINE_WARN("\tMaterials Count: {}", scene->mNumMaterials);
ENGINE_WARN("\tSkeletons Count: {}", scene->mNumSkeletons);
ENGINE_WARN("\tTextures Count: {}", scene->mNumTextures);
}
ENGINE_ERROR("Assimp error: {}", importer.GetErrorString());
return out; // meshes 空 = 无效
}
// 骨骼动画三件套:node 表(global 累乘骨架)→ 动画轨道 → 骨骼注册
//(DFS 序;ProcessNode 的 mesh 遍历序与其独立,nodeIdx 靠 name 查表)
FlattenNodes(scene->mRootNode, -1, out.nodes);
ParseAnimations(scene, out.nodes, out.clips);
const std::filesystem::path modelDir =
std::filesystem::path(path).parent_path();
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf);
ENGINE_INFO("Model parsed: {} ({} meshes)", path, out.meshes.size());
// 骨骼注册表(boneName → palette slot):多 mesh 共享骨架去重
std::unordered_map<std::string, u32> boneSlots;
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf,
out.bones, boneSlots);
// 骨骼 nodeIdx 回填(注册发生在各 mesh 的 ProcessMesh 内,
// 这里按 node 表查 name 定位累乘路径)
if (!out.bones.empty()) {
std::unordered_map<std::string, u32> nodeByName;
nodeByName.reserve(out.nodes.size());
for (u32 i = 0; i < out.nodes.size(); ++i) {
nodeByName.emplace(out.nodes[i].name, i);
}
for (auto& bone : out.bones) {
if (auto it = nodeByName.find(bone.name); it != nodeByName.end()) {
bone.nodeIdx = it->second;
} else {
ENGINE_WARN("Bone '{}' has no matching node, palette entry static",
bone.name);
}
}
}
ENGINE_INFO("Model parsed: {} ({} meshes, {} bones, {} anims)",
path, out.meshes.size(), out.bones.size(), out.clips.size());
return out;
}
u32 Model::FlattenNodes(const aiNode* node, i32 parent,
std::vector<ModelNode>& out)
{
const u32 idx = static_cast<u32>(out.size());
ModelNode n;
n.name = node->mName.C_Str();
n.parent = parent;
// 完整 mTransformation:FBX 的 PreRotation/单位换算都折在这里,
// 骨骼 global 累乘必须用全量矩阵,不能拆 TRS 重建
n.localTransform = ToGlm(node->mTransformation);
out.push_back(std::move(n));
for (u32 i = 0; i < node->mNumChildren; ++i) {
FlattenNodes(node->mChildren[i], static_cast<i32>(idx), out);
}
return idx;
}
void Model::ParseAnimations(const aiScene* scene,
const std::vector<ModelNode>& nodes,
std::vector<AnimationClip>& out)
{
if (scene->mNumAnimations == 0) return;
// nodeName → node 表下标(channel 的目标必须能在层级里累乘)
std::unordered_map<std::string, u32> nodeByName;
nodeByName.reserve(nodes.size());
for (u32 i = 0; i < nodes.size(); ++i) {
nodeByName.emplace(nodes[i].name, i);
}
for (u32 a = 0; a < scene->mNumAnimations; ++a) {
const aiAnimation* anim = scene->mAnimations[a];
AnimationClip clip;
clip.name = anim->mName.length > 0 ? anim->mName.C_Str()
: ("clip_" + std::to_string(a));
// tps=0 按 1(assimp 对部分导出器的惯例缺省)
clip.tps = anim->mTicksPerSecond > 0.0
? static_cast<f32>(anim->mTicksPerSecond) : 1.0f;
clip.duration = static_cast<f32>(anim->mDuration) / clip.tps;
for (u32 c = 0; c < anim->mNumChannels; ++c) {
const aiNodeAnim* ch = anim->mChannels[c];
auto it = nodeByName.find(ch->mNodeName.C_Str());
if (it == nodeByName.end()) {
ENGINE_WARN("Anim '{}': channel node '{}' not in hierarchy, skipped",
clip.name, ch->mNodeName.C_Str());
continue;
}
AnimChannel chan;
chan.nodeIdx = it->second;
// 关键帧 time 统一换算成秒(运行时采样免除法)
chan.positions.reserve(ch->mNumPositionKeys);
for (u32 k = 0; k < ch->mNumPositionKeys; ++k) {
const auto& key = ch->mPositionKeys[k];
chan.positions.push_back({
static_cast<f32>(key.mTime) / clip.tps,
{key.mValue.x, key.mValue.y, key.mValue.z}});
}
chan.rotations.reserve(ch->mNumRotationKeys);
for (u32 k = 0; k < ch->mNumRotationKeys; ++k) {
const auto& key = ch->mRotationKeys[k];
// aiQuaternion 存储序 (w,x,y,z) → glm 构造序一致
chan.rotations.push_back({
static_cast<f32>(key.mTime) / clip.tps,
glm::quat(key.mValue.w, key.mValue.x,
key.mValue.y, key.mValue.z)});
}
chan.scales.reserve(ch->mNumScalingKeys);
for (u32 k = 0; k < ch->mNumScalingKeys; ++k) {
const auto& key = ch->mScalingKeys[k];
chan.scales.push_back({
static_cast<f32>(key.mTime) / clip.tps,
{key.mValue.x, key.mValue.y, key.mValue.z}});
}
clip.channels.push_back(std::move(chan));
}
out.push_back(std::move(clip));
}
}
void Model::CollectMeshBones(const aiMesh* mesh,
std::vector<Vertex>& vertices,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots)
{
for (u32 b = 0; b < mesh->mNumBones; ++b) {
const aiBone* bone = mesh->mBones[b];
const char* name = bone->mName.C_Str();
u32 slot;
if (auto it = boneSlots.find(name); it != boneSlots.end()) {
slot = it->second; // 多 mesh 共享同一骨架(身体/附件分区)
} else {
slot = static_cast<u32>(bones.size());
boneSlots.emplace(name, slot);
// nodeIdx 由 ParseFromDisk 统一回填(node 表此时已就绪)
bones.push_back({name, 0, ToGlm(bone->mOffsetMatrix)});
}
// 顶点权重累加:LimitBoneWeights 保证每顶点 ≤4 根骨,逐槽找空位;
// 4 槽满的理论残留权重直接忽略(量化误差级)
for (u32 w = 0; w < bone->mNumWeights; ++w) {
const aiVertexWeight& vw = bone->mWeights[w];
if (vw.mVertexId >= vertices.size() || vw.mWeight <= 0.0f) continue;
Vertex& v = vertices[vw.mVertexId];
for (u32 k = 0; k < 4; ++k) {
if (v.weights[k] == 0.0f) {
v.boneIds[k] = slot;
v.weights[k] = vw.mWeight;
break;
}
}
}
}
}
Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
@@ -153,15 +375,19 @@ Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
descriptorManager, textureLayout));
model->m_ModelPath = parsed.path;
model->m_IsGltf = parsed.isGltf;
model->m_Nodes = std::move(parsed.nodes);
model->m_Bones = std::move(parsed.bones);
model->m_Clips = std::move(parsed.clips);
// 材质缓存去重 + 纹理上传 + mesh 缓冲(全部 GPU,主线程)
for (auto& pm : parsed.meshes) {
Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params);
model->m_Meshes.push_back(CreateScope<Mesh>(
device, allocator, std::move(pm.vertices), std::move(pm.indices), mat));
device, allocator, pm.vertices, pm.lods, mat));
}
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
parsed.path, model->m_Meshes.size(), model->m_TextureCache.size());
model->RebuildUnionBounds();
return model;
}
@@ -174,25 +400,39 @@ void Model::LoadModel(const std::string& path)
}
m_ModelPath = parsed.path;
m_IsGltf = parsed.isGltf;
m_Nodes = std::move(parsed.nodes);
m_Bones = std::move(parsed.bones);
m_Clips = std::move(parsed.clips);
for (auto& pm : parsed.meshes) {
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, std::move(pm.vertices), std::move(pm.indices), mat));
m_Device, m_Allocator, pm.vertices, pm.lods, mat));
}
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
path, m_Meshes.size(), m_TextureCache.size());
RebuildUnionBounds();
}
void Model::ProcessNode(const aiNode* node, const aiScene* scene,
std::vector<ParsedMesh>& out,
const std::filesystem::path& modelDir, bool isGltf)
const std::filesystem::path& modelDir, bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& parentGlobal)
{
// 累乘节点链全局变换:Blender/Sketchfab 导出的 FBX(未勾 Apply
// Transform)把物体旋转(含 Z-up→Y-up)与 cm→m 单位换算全折在
// 节点变换上,忽略会让每个零件按原始局部空间乱画(朝向丢失、
// 尺度差百倍,车都不成车)
const aiMatrix4x4 global = parentGlobal * node->mTransformation;
for (u32 i = 0; i < node->mNumMeshes; ++i) {
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
out.push_back(ProcessMesh(mesh, scene, modelDir, isGltf));
out.push_back(ProcessMesh(mesh, scene, modelDir, isGltf,
bones, boneSlots, global));
}
for (u32 i = 0; i < node->mNumChildren; ++i) {
ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf);
ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf,
bones, boneSlots, global);
}
}
@@ -222,21 +462,38 @@ Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb)
}
Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params)
const MaterialParams& params,
bool transparent,
const std::array<Texture*, Material::kSlotCount>&
slotOverrides)
{
const std::string key = Material::MakeCacheKey(paths, params);
// 程序化覆盖纹理以指针地址入键:同模型内不同颜色的覆盖组合
//(如两块玻璃板)各得一份材质,不与纯路径键的材质串缓存
std::string key = Material::MakeCacheKey(paths, params, transparent);
bool hasOverride = false;
for (const Texture* t : slotOverrides) {
if (t) { hasOverride = true; break; }
}
if (hasOverride) {
key += "@ovr";
for (const Texture* t : slotOverrides) {
key += t ? ":" + std::to_string(reinterpret_cast<uintptr_t>(t))
: ":0";
}
}
if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) {
return it->second.get();
}
// 每槽位解析纹理:空/缺失折到该槽位默认纹理
// 每槽位解析纹理:程序化覆盖优先,路径次之,空/缺失折到默认纹理
std::array<Texture*, Material::kSlotCount> textures{};
// 槽位 sRGB 语义:颜色图 true,数据图 false
const bool slotSrgb[Material::kSlotCount] = {true, false, false, true};
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
Texture* tex = nullptr;
if (!paths[slot].empty()) {
Texture* tex = slotOverrides[slot];
if (!tex && !paths[slot].empty()) {
tex = GetOrLoadTexture(paths[slot], slotSrgb[slot]);
}
textures[slot] = tex ? tex : m_DefaultTextures[slot].get();
@@ -244,7 +501,7 @@ Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCoun
auto mat = CreateScope<Material>(
m_Device, m_Allocator, m_DescriptorManager, m_TextureLayout,
textures, params);
textures, params, transparent);
Material* ptr = mat.get();
m_MaterialCache.emplace(key, std::move(mat));
return ptr;
@@ -252,29 +509,47 @@ Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCoun
ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
const std::filesystem::path& modelDir,
bool isGltf)
bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& globalTransform)
{
ParsedMesh out;
std::vector<Vertex>& vertices = out.vertices;
std::vector<u32>& indices = out.indices;
std::vector<u32> indices;
vertices.reserve(mesh->mNumVertices);
// 有骨骼的 mesh:顶点权重先清零再由 CollectMeshBones 灌入——
// 默认 w=(1,0,0,0) 的槽 0 若不清,palette[0] 的 identity 会以
// 权重 1 混进蒙皮矩阵(顶点永远钉在原点)
const bool hasBones = mesh->mNumBones > 0;
// 非蒙皮 mesh:烘焙节点链全局变换(旋转/单位换算折进顶点);
// 蒙皮 mesh 保持 mesh 空间——palette × offsetMatrix 的蒙皮
// 约定依赖原空间,烘焙会与动画路径打架
const bool bakeTransform = !hasBones;
const aiMatrix3x3 normalMat(globalTransform);
for (u32 i = 0; i < mesh->mNumVertices; ++i) {
Vertex vertex{};
vertex.position = {
mesh->mVertices[i].x,
mesh->mVertices[i].y,
mesh->mVertices[i].z
};
if (bakeTransform) {
const aiVector3D p = globalTransform * mesh->mVertices[i];
vertex.position = {p.x, p.y, p.z};
} else {
vertex.position = {
mesh->mVertices[i].x,
mesh->mVertices[i].y,
mesh->mVertices[i].z
};
}
if (mesh->HasNormals()) {
vertex.normal = {
mesh->mNormals[i].x,
mesh->mNormals[i].y,
mesh->mNormals[i].z
};
const aiVector3D n = bakeTransform
? normalMat * mesh->mNormals[i]
: mesh->mNormals[i];
const glm::vec3 nn{n.x, n.y, n.z};
vertex.normal = bakeTransform ? glm::normalize(nn) : nn;
}
if (mesh->mTextureCoords[0]) {
@@ -286,16 +561,23 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
// 切线(法线贴图 TBN;无切线数据置零,shader 侧退回几何法线)
if (mesh->mTangents) {
vertex.tangent = {
mesh->mTangents[i].x,
mesh->mTangents[i].y,
mesh->mTangents[i].z
};
const aiVector3D t = bakeTransform
? normalMat * mesh->mTangents[i]
: mesh->mTangents[i];
const glm::vec3 tt{t.x, t.y, t.z};
vertex.tangent = bakeTransform ? glm::normalize(tt) : tt;
}
if (hasBones) {
vertex.weights = glm::vec4(0.0f);
}
vertices.push_back(vertex);
}
// 骨骼权重灌顶点(无骨骼 mesh 顶点保持默认 idx0/w1 → identity 直通)
CollectMeshBones(mesh, vertices, bones, boneSlots);
for (u32 i = 0; i < mesh->mNumFaces; ++i) {
aiFace face = mesh->mFaces[i];
for (u32 j = 0; j < face.mNumIndices; ++j) {
@@ -313,16 +595,19 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
// glTF2 贴图在 assimp 5.x 的落点:MR 合图 → METALNESS(15)/
// DIFFUSE_ROUGHNESS(16)(同图双槽);baseColor → BASE_COLOR(12)。
// OBJ:map_Ns → SHININESS(独立 roughness 图)、map_Ka 常绑 AO。
// 先到先得,数组顺序即优先级
// 先到先得,数组顺序即优先级。
// 注意:FBX 独立 metalness/roughness 是两张图——metal 落 MR 槽
//(灰度 R=G=B,shader 读 .b 当金属度恰好正确),roughness 落
// 独立 Roughness 槽(factors.z=1 启用),不再挤占 MR 槽的 .g
const std::pair<aiTextureType, u32> slots[] = {
{aiTextureType_DIFFUSE, Material::Albedo},
{aiTextureType_BASE_COLOR, Material::Albedo},
{aiTextureType_NORMALS, Material::Normal},
{aiTextureType_HEIGHT, Material::Normal}, // OBJ map_Bump 落点
{aiTextureType_METALNESS, Material::MetallicRoughness},
{aiTextureType_DIFFUSE_ROUGHNESS, Material::MetallicRoughness},
{aiTextureType_DIFFUSE_ROUGHNESS, Material::Roughness},
{aiTextureType_SHININESS, Material::Roughness}, // OBJ map_Ns
{aiTextureType_UNKNOWN, Material::MetallicRoughness},
{aiTextureType_SHININESS, Material::MetallicRoughness},
{aiTextureType_AMBIENT_OCCLUSION, Material::Ao},
{aiTextureType_LIGHTMAP, Material::Ao},
{aiTextureType_AMBIENT, Material::Ao},
@@ -343,7 +628,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
// 材质加载可见性:贴图槽缺失/路径解析错误会静默退化成默认
// 纹理(金属全没 → 塑料感),这里留下诊断锚点
ENGINE_INFO("Material '{}': albedo={} normal={} MR={} emissive={} ao={}",
ENGINE_INFO("Material '{}': albedo={} normal={} MR={} roughness={} "
"emissive={} ao={}",
material->GetName().C_Str(),
texPaths[Material::Albedo].empty() ? "<none>"
: texPaths[Material::Albedo],
@@ -351,6 +637,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
: texPaths[Material::Normal],
texPaths[Material::MetallicRoughness].empty() ? "<none>"
: texPaths[Material::MetallicRoughness],
texPaths[Material::Roughness].empty() ? "<none>"
: texPaths[Material::Roughness],
texPaths[Material::Emissive].empty() ? "<none>"
: texPaths[Material::Emissive],
texPaths[Material::Ao].empty() ? "<none>"
@@ -368,16 +656,33 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness);
params.factors = glm::vec4(static_cast<f32>(metallic),
static_cast<f32>(roughness), 0.0f, 0.0f);
// glTF alphaMode:BLEND = 半透明(透明绘制队列 + 深度只测不写)。
// MASK(alpha cutout)暂按不透明——需要 alpha test,后续演进。
// OBJ/FBX 无此键 → 恒不透明(资产格式定义写死)
aiString alphaMode;
if (material->Get(AI_MATKEY_GLTF_ALPHAMODE, alphaMode) == AI_SUCCESS) {
out.transparent = (alphaMode == aiString("BLEND"));
}
} else {
// FBX/OBJ 独立贴图工作流
const bool hasSepRough = !texPaths[Material::Roughness].empty();
f32 roughnessFactor = 1.0f;
if (texPaths[Material::MetallicRoughness].empty()) {
if (!hasSepRough && texPaths[Material::MetallicRoughness].empty()) {
// 无粗糙度贴图 → Blinn-Phong Ns 换算 R=sqrt(2/(Ns+2)),
// 补偿默认 MR 贴图 G=0.5 → 因子 = 2R
ai_real shininess = 10.0f;
material->Get(AI_MATKEY_SHININESS, shininess);
const f32 rough =
std::sqrt(2.0f / (static_cast<f32>(shininess) + 2.0f));
roughnessFactor = 2.0f * glm::clamp(rough, 0.05f, 1.0f);
}
params.factors = glm::vec4(0.0f, roughnessFactor, 0.0f, 0.0f);
// 金属度图落 MR 槽(灰度 .b 即金属度语义):
// 有图 metallic 因子 = 1,无图 = 0(OBJ specular 工作流无金属概念)
const f32 metallicFactor =
texPaths[Material::MetallicRoughness].empty() ? 0.0f : 1.0f;
params.factors = glm::vec4(metallicFactor, roughnessFactor,
hasSepRough ? 1.0f : 0.0f, 0.0f);
}
}
@@ -386,6 +691,33 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
out.texPaths[slot] = std::move(texPaths[slot]);
}
out.params = params;
// out.transparent 已在 glTF 分支读取
// ---- LOD 链(解析期生成,worker 线程免费时间):----
// LOD0 = 原始;每级目标索引数减半,相对误差 1e-2。顶点共享,
// 简化只重排索引(meshopt_simplify 从 LOD0 单次起点逐级向下)
out.lods.push_back(std::move(indices));
for (u32 lod = 1; lod < kMeshLodCount; ++lod) {
const size_t target = out.lods[0].size() >> lod;
// 太小的网格不简化:简化收益低于误差风险,直接复用上一级
if (target < 96 || out.lods[0].size() < 192) {
out.lods.push_back(out.lods.back());
continue;
}
std::vector<u32> dest(out.lods[0].size());
float error = 0.0f;
const size_t count = meshopt_simplify(
dest.data(),
out.lods[0].data(), out.lods[0].size(),
&out.vertices[0].position.x, out.vertices.size(), sizeof(Vertex),
target, 1e-2f, 0, &error);
if (count == 0) {
out.lods.push_back(out.lods.back()); // 简化失败兜底
continue;
}
dest.resize(count);
out.lods.push_back(std::move(dest));
}
return out;
}
+220 -75
View File
@@ -1,8 +1,28 @@
//
// Created by atdunbg on 2026/9/23.
//
// ForwardPass:相机视角渲染到 HDR 颜色图(线性光照值)。
// 显示变换不在本 pass——HDRColor 经 Tonemap 归一后才写 Backbuffer
// (对齐 UE/Unity:SceneColor(HDR) → tonemapper → UI)。
//
// 透明管线对齐 UE/Unity 标准语义:
// - 不透明("Forward"):深度测试 + 写,无混合直写
// - 透明("ForwardTrans"):独立渲染实例,深度测试(对不透明)+
// 深度写入关闭,HDR/velocity/depth 全部 Load 承接不透明段结果,
// 预乘 alpha 混合(线性空间 over),远 → 近排序(SceneRenderer
// 侧按 batch 平均距离排)
//
// NVIDIA shader-object 实测注意(见 .trae/documents 性能档案):
// 早期实验称 depthWriteEnable=FALSE 有灾难病态(~5fps),但该实验
// 是在 frag 仍写 gl_FragDepth 的时期做的(写 gl_FragDepth 会禁用
// early-Z,归因很可能在它而非 FALSE 本身)。gl_FragDepth 已删除,
// 透明段现为 depthWrite=FALSE;若病态复现,先排查二者再下结论。
// 另:勿用 gl_FragDepth=far 模拟"只测不写"(eLess 下片元自身
// 永远测试失败,透明队列全灭)。
//
#include <Engine/Renderer/Passes/ForwardPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Renderer/Vertex.h>
#include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/Material.h>
@@ -12,8 +32,90 @@
namespace Engine::Passes
{
namespace
{
// 直写方程:(ONE, ZERO) → dst = src,数学上等价于无混合
vk::ColorBlendEquationEXT MakeDirectEquation()
{
vk::ColorBlendEquationEXT eq;
eq.setSrcColorBlendFactor(vk::BlendFactor::eOne)
.setDstColorBlendFactor(vk::BlendFactor::eZero)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd);
return eq;
}
constexpr vk::ColorComponentFlags kColorWriteMask =
vk::ColorComponentFlagBits::eR |
vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB |
vk::ColorComponentFlagBits::eA;
} // namespace
// 两段渲染共享的命令录制:shader/描述符绑定 + 几何/光栅/视口状态。
// translucent=true 时绑定真实深度写入的透明 frag 变体。
// 深度写与混合状态由各渲染实例作为初始态设置(见文件头注释)。
void ForwardPass::RecordSharedState(const RG::RenderGraphContext& ctx,
vk::CommandBuffer cmd,
bool translucent) const
{
// 绑定 shader object
std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment
};
std::array<vk::ShaderEXT, 2> shaders = {
m_Gpu.forwardVert,
translucent ? m_Gpu.translucentFrag : m_Gpu.forwardFrag};
cmd.bindShadersEXT(stages, shaders);
// Set 0:本帧槽位的 UBO;Set 2:本帧槽位的阴影图
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 0,
m_Gpu.uboSets[ctx.frame.frameSlot], {});
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 2,
m_Gpu.shadowSets[ctx.frame.frameSlot], {});
cmd.setRasterizerDiscardEnable(VK_FALSE);
cmd.setDepthTestEnable(VK_TRUE);
cmd.setDepthCompareOp(vk::CompareOp::eLess);
cmd.setDepthBoundsTestEnable(VK_FALSE);
cmd.setStencilTestEnable(VK_FALSE);
cmd.setDepthBiasEnable(VK_FALSE);
cmd.setPolygonModeEXT(vk::PolygonMode::eFill);
cmd.setCullMode(vk::CullModeFlagBits::eNone);
cmd.setFrontFace(vk::FrontFace::eCounterClockwise);
cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
cmd.setPrimitiveRestartEnable(VK_FALSE);
auto binding = Vertex::GetBindingDescription();
auto attributes = Vertex::GetAttributeDescriptions();
cmd.setVertexInputEXT(binding, attributes);
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
vk::SampleMask sampleMask = 0xFFFFFFFF;
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE);
vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(ctx.extent.width),
static_cast<float>(ctx.extent.height),
0.0f, 1.0f);
cmd.setViewportWithCount(viewport);
vk::Rect2D scissor({0, 0}, ctx.extent);
cmd.setScissorWithCount(scissor);
}
void ForwardPass::Register(RG::RenderGraph& rg)
{
// ---- 不透明段:深度测试 + 写,无混合直写 ----
rg.AddPass("Forward", [this](RG::RenderGraphContext& ctx) {
const vk::CommandBuffer cmd = ctx.cmd;
@@ -42,6 +144,96 @@ namespace Engine::Passes
depthAttachment.setImageView(ctx.View(m_Depth))
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
// 透明段要 Load 本段写出的深度做测试,必须 store
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(vk::ClearValue(
vk::ClearDepthStencilValue(1.0f, 0)));
std::array<vk::RenderingAttachmentInfo, 2> colorArray = {
colorAttachments[0], colorAttachments[1]};
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
.setLayerCount(1)
.setColorAttachments(colorArray)
.setPDepthAttachment(&depthAttachment);
cmd.beginRendering(renderingInfo);
RecordSharedState(ctx, cmd, false);
cmd.setDepthWriteEnable(VK_TRUE);
// 两个颜色附件(HDR + velocity)都是无混合直写
cmd.setColorBlendEnableEXT(0,
std::array<vk::Bool32, 2>{VK_FALSE, VK_FALSE});
const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
cmd.setColorBlendEquationEXT(0,
std::array<vk::ColorBlendEquationEXT, 2>{direct, direct});
cmd.setColorWriteMaskEXT(0,
std::array<vk::ColorComponentFlags, 2>{kColorWriteMask, kColorWriteMask});
// 调试视图 Transparent:只看透明队列——跳过不透明绘制,
// HDR 颜色 = 清屏色 + 透明段内容(Skybox 同步跳过背景)
if (m_Frame.debugView != Passes::kDebugTransparent) {
for (const auto& batch : m_Frame.mainView.batches) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 1,
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
}
}
cmd.endRendering();
})
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
.Write(m_Velocity, RG::ResourceUse::ColorAttachment)
.Write(m_Depth, RG::ResourceUse::DepthAttachment)
.Read(m_ShadowMaps[0], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_ShadowMaps[1], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_ShadowMaps[2], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment);
// ---- 透明段:独立渲染实例,UE/Unity 标准语义 ----
// 深度测试对不透明深度(eLess)+ 深度写入关闭——玻璃之间
// 互相不遮挡(谁先画都留在画面上,先后只影响 over 混合的
// 颜色数学),交叉半透明不再整块消失。
// 对不透明深度的 RAW 由 depth 的 Write use 链保证排在
// 不透明段之后(WAW 边)。
// alpha 语义:frag 输出未预乘线性色 + 覆盖率 alpha,
// 预乘在 tonemap 段(rgb * a)完成 —— hard constraint。
rg.AddPass("ForwardTrans", [this](RG::RenderGraphContext& ctx) {
if (m_Frame.translucentView.batches.empty()) return;
const vk::CommandBuffer cmd = ctx.cmd;
vk::ClearValue clearValue;
clearValue.color = vk::ClearColorValue(
std::array<float, 4>{0.1f, 0.1f, 0.1f, 0.0f});
vk::RenderingAttachmentInfo colorAttachments[2];
colorAttachments[0].setImageView(ctx.View(m_HDRColor))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
// velocity:Load 不透明段写出的运动向量,玻璃只覆盖自己的像素
vk::ClearValue velocityClear;
velocityClear.color = vk::ClearColorValue(std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
colorAttachments[1].setImageView(ctx.View(m_Velocity))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(velocityClear);
vk::RenderingAttachmentInfo depthAttachment;
depthAttachment.setImageView(ctx.View(m_Depth))
// 深度只读测试源:Load 承接不透明段深度,
// 透明段 depthWrite=FALSE 不产生写入,
// storeOp DontCare(下帧 entry 重新 Clear)
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eDontCare)
.setClearValue(vk::ClearValue(
vk::ClearDepthStencilValue(1.0f, 0)));
@@ -56,96 +248,49 @@ namespace Engine::Passes
.setPDepthAttachment(&depthAttachment);
cmd.beginRendering(renderingInfo);
RecordSharedState(ctx, cmd, true);
// UE/Unity 语义:透明段只测不写深度。写深度会让"先画的玻璃"
// 剔掉后画但实际在它前面的玻璃(交叉半透明整块消失)。
// 早期 depthWrite=FALSE 引发 NVIDIA ~5fps 病态的实验是在
// frag 仍写 gl_FragDepth 的时期做的(禁 early-Z),归因存疑;
// gl_FragDepth 已删除,FALSE 若复现病态再另行隔离。
cmd.setDepthWriteEnable(VK_FALSE);
// 绑定 shader object
std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment
};
std::array<vk::ShaderEXT, 2> shaders = {m_Gpu.forwardVert, m_Gpu.forwardFrag};
cmd.bindShadersEXT(stages, shaders);
vk::ColorBlendEquationEXT premul;
premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
.setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
.setColorBlendOp(vk::BlendOp::eAdd)
// alpha 穿透:玻璃只做场景内 rgb over(透出背景色),
// 不修改像素覆盖率——HDR 的 a 是"窗口透过率"(tonemap
// rgb *= a 预乘 + 透明窗口桌面合成),归背景专属。若让
// 玻璃 over 混入 a(旧写法),a<1 背景上的玻璃会被拖进
// 桌面混色链路 → 预乘拉黑
.setSrcAlphaBlendFactor(vk::BlendFactor::eZero)
.setDstAlphaBlendFactor(vk::BlendFactor::eOne)
.setAlphaBlendOp(vk::BlendOp::eAdd);
// Set 0:本帧槽位的 UBO;Set 2:本帧槽位的阴影图
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 0,
m_Gpu.uboSets[ctx.frame.frameSlot], {});
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 2,
m_Gpu.shadowSets[ctx.frame.frameSlot], {});
// ---- 动态状态 ----
cmd.setRasterizerDiscardEnable(VK_FALSE);
cmd.setDepthTestEnable(VK_TRUE);
cmd.setDepthWriteEnable(VK_TRUE);
cmd.setDepthCompareOp(vk::CompareOp::eLess);
cmd.setDepthBoundsTestEnable(VK_FALSE);
cmd.setStencilTestEnable(VK_FALSE);
cmd.setDepthBiasEnable(VK_FALSE);
cmd.setPolygonModeEXT(vk::PolygonMode::eFill);
cmd.setCullMode(vk::CullModeFlagBits::eNone);
cmd.setFrontFace(vk::FrontFace::eCounterClockwise);
cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
cmd.setPrimitiveRestartEnable(VK_FALSE);
auto binding = Vertex::GetBindingDescription();
auto attributes = Vertex::GetAttributeDescriptions();
cmd.setVertexInputEXT(binding, attributes);
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
vk::SampleMask sampleMask = 0xFFFFFFFF;
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE);
vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(ctx.extent.width),
static_cast<float>(ctx.extent.height),
0.0f, 1.0f);
cmd.setViewportWithCount(viewport);
vk::Rect2D scissor({0, 0}, ctx.extent);
cmd.setScissorWithCount(scissor);
vk::Bool32 blendEnable = VK_FALSE;
vk::ColorBlendEquationEXT blendEquation;
blendEquation.setSrcColorBlendFactor(vk::BlendFactor::eOne)
.setDstColorBlendFactor(vk::BlendFactor::eZero)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd);
vk::ColorComponentFlags colorWriteMask =
vk::ColorComponentFlagBits::eR |
vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB |
vk::ColorComponentFlagBits::eA;
// 两个颜色附件(HDR + velocity)都是无混合直写
const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
cmd.setColorBlendEnableEXT(0,
std::array<vk::Bool32, 2>{blendEnable, blendEnable});
std::array<vk::Bool32, 2>{VK_TRUE, VK_FALSE});
cmd.setColorBlendEquationEXT(0,
std::array<vk::ColorBlendEquationEXT, 2>{blendEquation, blendEquation});
std::array<vk::ColorBlendEquationEXT, 2>{premul, direct});
cmd.setColorWriteMaskEXT(0,
std::array<vk::ColorComponentFlags, 2>{colorWriteMask, colorWriteMask});
std::array<vk::ColorComponentFlags, 2>{kColorWriteMask, kColorWriteMask});
// ---- 绘制:GPUDriven 按 mesh 分 batch(相机视锥剔除后)----
// 实例矩阵已由 Renderer 写入 ObjectBuffer(per-slot SSBO);
// 同 mesh 的 N 个可见实体合并为一次 drawIndexed(instanceCount=N),
// draw 数 = 可见唯一 mesh 数,与实体数量解耦
for (const auto& batch : m_Frame.mainView.batches) {
for (const auto& batch : m_Frame.translucentView.batches) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 1,
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
batch.mesh->Draw(cmd, batch.count, batch.first);
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
}
cmd.endRendering();
})
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
.Write(m_Velocity, RG::ResourceUse::ColorAttachment)
// depth 走写 use(WAW 边):保证透明段排在不透明段之后,
// barrier 覆盖透明段对不透明深度的测试读(Read-Write hazard
// 由 src=Write 的内存域保证可见性)
.Write(m_Depth, RG::ResourceUse::DepthAttachment)
.Read(m_ShadowMaps[0], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
@@ -174,6 +174,32 @@ namespace Engine::Passes
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE);
// ---- 颜色混合必须显式直写 ----
// shader-object 路径混合是动态状态,会从上一个 pass(ForwardTrans
// 的 premul 混合)残留:tonemap 输出被二次混合 → rgb = color×a²
//(拖低 alpha 平方级变黑),且 alpha 被残留方程与 clear(a=1) 混成
// 恒 1 → 透明窗口永远不生效
constexpr vk::Bool32 kBlendOff = VK_FALSE;
constexpr vk::ColorComponentFlags kWriteAll =
vk::ColorComponentFlagBits::eR |
vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB |
vk::ColorComponentFlagBits::eA;
// 直写方程:(ONE, ZERO) → dst = src(无混合),alpha 同样直写
static const vk::ColorBlendEquationEXT kDirect = [] {
vk::ColorBlendEquationEXT eq;
eq.setSrcColorBlendFactor(vk::BlendFactor::eOne)
.setDstColorBlendFactor(vk::BlendFactor::eZero)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd);
return eq;
}();
cmd.setColorBlendEnableEXT(0, kBlendOff);
cmd.setColorBlendEquationEXT(0, kDirect);
cmd.setColorWriteMaskEXT(0, kWriteAll);
vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(ctx.extent.width),
static_cast<float>(ctx.extent.height),
+22 -4
View File
@@ -12,14 +12,28 @@ namespace Engine::Passes
rg.AddPass("ImGui", [this](RG::RenderGraphContext& ctx) {
const vk::CommandBuffer cmd = ctx.cmd;
// loadOp 按显示模式分支:Direct = blit 已铺满场景(Load 叠加);
// Viewport = 本 pass 是唯一写入者(Clear 为"桌面底色")
const bool direct = (m_Frame.displayMode == kDisplayDirect);
vk::ClearValue clearValue;
clearValue.color = vk::ClearColorValue(
std::array<float, 4>{0.04f, 0.045f, 0.06f, 1.0f});
vk::RenderingAttachmentInfo colorAttachment;
colorAttachment.setImageView(ctx.View(m_Backbuffer))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad) // 关键:叠加
.setStoreOp(vk::AttachmentStoreOp::eStore);
.setLoadOp(direct ? vk::AttachmentLoadOp::eLoad
: vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
// 渲染区域 = backbuffer 物理尺寸(窗口大小),不是 ctx.extent
//(= RG 目标尺寸 = 场景渲染分辨率)。Editor 视口模式下场景链
// 按面板尺寸渲染,而 backbuffer 是全窗口——若跟随目标尺寸,
// 整个 ImGui 会被裁剪到面板大小
renderingInfo.setRenderArea({{0, 0}, ctx.Extent(m_Backbuffer)})
.setLayerCount(1)
.setColorAttachments(colorAttachment);
@@ -30,7 +44,11 @@ namespace Engine::Passes
}
cmd.endRendering();
}).Write(m_Backbuffer, RG::ResourceUse::ColorAttachment);
}).Write(m_Backbuffer, RG::ResourceUse::ColorAttachment)
// 场景纹理在本 pass 内被 ImGui::Image 的描述符采样:声明读让 RG
// 补 ColorAttachment→ShaderRead barrier,并推导 eSampled usage
.Read(m_PresentTarget, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment);
}
} // namespace Engine::Passes
+92 -4
View File
@@ -4,8 +4,10 @@
#include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Mesh.h> // IndirectGroup 取数(inline 经 Mesh)
#include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/Buffer.h>
#include <Engine/Vulkan/DescriptorManager.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <Engine/Vulkan/UniformBuffer.h>
@@ -15,14 +17,18 @@
namespace Engine::Passes
{
SceneGpu::SceneGpu(Device& device, DescriptorManager& descriptors,
SceneGpu::SceneGpu(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
ShaderManager& shaders,
UniformBuffer& ubo, UniformBuffer& objectBuffer,
UniformBuffer& prevObjectBuffer)
: m_Device(device), m_Descriptors(descriptors)
UniformBuffer& prevObjectBuffer,
UniformBuffer& skinOffsetBuffer,
UniformBuffer& skinPaletteBuffer)
: m_Device(device), m_Allocator(allocator), m_Descriptors(descriptors)
{
// Set 0: UBO + GPUDriven 实例数据(SSBO,vertex 读取)
// + 上一帧实例矩阵(TAA velocity,vertex 读取)
// + 骨骼蒙皮:SkinOffsets(实例→palette 基址)+ SkinPalettes(骨骼矩阵)
std::vector<DescriptorBinding> uboBindings = {
{0, vk::DescriptorType::eUniformBuffer,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
@@ -30,6 +36,10 @@ namespace Engine::Passes
vk::ShaderStageFlagBits::eVertex},
{2, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
{3, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
{4, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
};
uboSetLayout = descriptors.CreateLayout(uboBindings);
@@ -81,6 +91,11 @@ namespace Engine::Passes
shaderDir + "simple_shader.frag.spv",
vk::ShaderStageFlagBits::eFragment,
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0);
// 透明变体:同一套布局,frag 末尾写 gl_FragDepth=1.0
auto transFrag = shaders.CreateFromFile(
shaderDir + "simple_shader_translucent.frag.spv",
vk::ShaderStageFlagBits::eFragment,
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0);
// shadow shader:depth-only,push = lightVP
auto shadowVertShader = shaders.CreateFromFile(
@@ -96,10 +111,12 @@ namespace Engine::Passes
forwardVert = vert.Handle;
forwardFrag = frag.Handle;
translucentFrag = transFrag.Handle;
shadowVert = shadowVertShader.Handle;
shadowFrag = shadowFragShader.Handle;
if (!forwardVert || !forwardFrag || !shadowVert || !shadowFrag) {
if (!forwardVert || !forwardFrag || !translucentFrag ||
!shadowVert || !shadowFrag) {
ENGINE_FATAL("SceneGpu: failed to create shader objects");
}
@@ -132,10 +149,34 @@ namespace Engine::Passes
descriptors.UpdateStorageBuffer(
uboSets[slot], 2, prevObjectBuffer.Get(slot),
prevObjectBuffer.GetSize());
descriptors.UpdateStorageBuffer(
uboSets[slot], 3, skinOffsetBuffer.Get(slot),
skinOffsetBuffer.GetSize());
descriptors.UpdateStorageBuffer(
uboSets[slot], 4, skinPaletteBuffer.Get(slot),
skinPaletteBuffer.GetSize());
shadowSets[slot] = descriptors.AllocateDescriptorSet(shadowSetLayout);
}
// shadowSets 的图片绑定在 Compile 后由 BindShadowMap 完成
// ---- MDI per-slot ring:命令 + count(CPU 直写,host visible)----
// 布局 [main | casc0 | casc1 | casc2] × kMaxInstances 命令预算;
// 两者 usage 都要 eIndirectBuffer(count buffer 的 spec 要求)
const vk::DeviceSize cmdBytes = vk::DeviceSize(kIndirectViewMain + 1 + kMaxCascades) *
kMaxInstances * sizeof(IndirectDrawCmd);
const vk::DeviceSize cntBytes = vk::DeviceSize(kIndirectViewMain + 1 + kMaxCascades) *
kMaxInstances * sizeof(u32);
constexpr auto memProps = vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent;
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
indirectCmdBuffers[slot] = CreateScope<Buffer>(
device, allocator, cmdBytes,
vk::BufferUsageFlagBits::eIndirectBuffer, memProps);
indirectCountBuffers[slot] = CreateScope<Buffer>(
device, allocator, cntBytes,
vk::BufferUsageFlagBits::eIndirectBuffer, memProps);
}
}
SceneGpu::~SceneGpu()
@@ -144,6 +185,7 @@ namespace Engine::Passes
if (forwardVert) vkDevice.destroyShaderEXT(forwardVert);
if (forwardFrag) vkDevice.destroyShaderEXT(forwardFrag);
if (translucentFrag) vkDevice.destroyShaderEXT(translucentFrag);
if (shadowVert) vkDevice.destroyShaderEXT(shadowVert);
if (shadowFrag) vkDevice.destroyShaderEXT(shadowFrag);
@@ -168,6 +210,52 @@ namespace Engine::Passes
}
}
// ---- MDI:per-slot 上传 + 组绘制 ----
void SceneGpu::UploadIndirect(u32 frameSlot, const FrameSceneData& frame)
{
auto uploadView = [&](u32 viewIdx, const CullResult& v) {
if (v.indirectCommands.empty()) return;
const vk::DeviceSize base =
vk::DeviceSize(viewIdx) * kMaxInstances;
indirectCmdBuffers[frameSlot]->Upload(
v.indirectCommands.data(),
v.indirectCommands.size() * sizeof(IndirectDrawCmd),
base * sizeof(IndirectDrawCmd));
// 每组一个 count,下标 = 组首命令下标(drawIndexedIndirectCount
// 从此读取实际执行条数——GPU 剔除切片接管时只换写入者)
for (const auto& g : v.indirectGroups) {
indirectCountBuffers[frameSlot]->Upload(
&g.commandCount, sizeof(u32),
(base + g.firstCommand) * sizeof(u32));
}
};
uploadView(kIndirectViewMain, frame.mainView);
for (u32 c = 0; c < kMaxCascades; ++c) {
uploadView(kIndirectViewMain + 1 + c, frame.shadowCascades[c]);
}
}
void SceneGpu::DrawIndirectGroup(vk::CommandBuffer cmd, u32 frameSlot,
u32 viewIndex, const IndirectGroup& g)
{
// 组内全部命令共享同一 megabuffer:bind 一次,逐 mesh 定位由
// 命令里的 firstIndex/vertexOffset 承担
cmd.bindVertexBuffers(0, g.mesh->GetVertexBuffer(), {vk::DeviceSize(0)});
cmd.bindIndexBuffer(g.mesh->GetIndexBuffer(), vk::DeviceSize(0),
vk::IndexType::eUint32);
const vk::DeviceSize base =
vk::DeviceSize(viewIndex) * kMaxInstances;
cmd.drawIndexedIndirectCount(
indirectCmdBuffers[frameSlot]->Get(),
base * sizeof(IndirectDrawCmd),
indirectCountBuffers[frameSlot]->Get(),
(base + g.firstCommand) * sizeof(u32),
g.commandCount, // maxDrawCount = CPU 打包的组大小
sizeof(IndirectDrawCmd));
}
void SceneGpu::BindEnvironment(const Cubemap& env)
{
// IBL 资源 view 全程稳定(不随 RG 重编译变化),绑一次即可。
+1 -1
View File
@@ -101,7 +101,7 @@ namespace Engine::Passes
&m_Frame.ubo.lightSpace[c]);
for (const auto& batch : m_Frame.shadowCascades[c].batches) {
batch.mesh->Draw(cmd, batch.count, batch.first);
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
}
cmd.endRendering();
+5 -1
View File
@@ -3,6 +3,7 @@
//
#include <Engine/Renderer/Passes/SkyboxPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/DescriptorManager.h>
@@ -96,6 +97,8 @@ namespace Engine::Passes
const glm::vec4& cc = m_Frame.ClearColor;
vk::ClearValue clear;
// alpha 语义 = 窗口透过率(透明窗口与桌面合成;tonemap 段
// rgb *= a 预乘)。滑条由用户控制:a<1 = 背景透出桌面
clear.color = vk::ClearColorValue(
std::array<float, 4>{cc.r, cc.g, cc.b, cc.a});
@@ -113,7 +116,8 @@ namespace Engine::Passes
cmd.beginRendering(renderingInfo);
if (m_Env && m_Frame.DrawSkybox) {
if (m_Env && m_Frame.DrawSkybox &&
m_Frame.debugView != Passes::kDebugTransparent) {
// 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写)
const std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
+4 -4
View File
@@ -14,7 +14,7 @@ namespace Engine::Passes
FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
RG::TextureHandle temporalDiff,
RG::TextureHandle backbuffer,
RG::TextureHandle presentTarget,
RG::TextureHandle rawHdr,
RG::TextureHandle velocity,
RG::TextureHandle shadowMap0,
@@ -22,7 +22,7 @@ namespace Engine::Passes
RG::TextureHandle shadowMap2)
: m_Gpu(gpu), m_Descriptors(descriptors), m_Params(params), m_Frame(frame),
m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff),
m_Backbuffer(backbuffer),
m_Target(presentTarget),
m_RawHDR(rawHdr), m_Velocity(velocity),
m_ShadowMaps{shadowMap0, shadowMap1, shadowMap2}
{
@@ -38,7 +38,7 @@ namespace Engine::Passes
{
rg.AddPass("Tonemap",
[this](RG::RenderGraphContext& ctx) {
m_Gpu.RecordFullscreen(ctx, ctx.View(m_Backbuffer),
m_Gpu.RecordFullscreen(ctx, ctx.View(m_Target),
m_Gpu.tonemapFrag,
m_Sets[ctx.frame.frameSlot],
FullscreenPush{m_Params.exposure,
@@ -49,7 +49,7 @@ namespace Engine::Passes
static_cast<f32>(
m_Frame.debugView)});
})
.Write(m_Backbuffer, RG::ResourceUse::ColorAttachment)
.Write(m_Target, RG::ResourceUse::ColorAttachment)
.Read(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_A, RG::ResourceUse::ShaderRead,
+135 -27
View File
@@ -24,6 +24,7 @@
#include <Engine/Renderer/Passes/TemporalDiffPass.h>
#include <Engine/Renderer/Passes/ImGuiPass.h>
#include <Engine/Renderer/Passes/PresentPass.h>
#include <Engine/Renderer/Passes/BlitPass.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/MemoryAllocator.h>
#include <Engine/Vulkan/DescriptorManager.h>
@@ -34,6 +35,7 @@
#include <Engine/Vulkan/UniformBuffer.h>
#include <Engine/Core/Window.h>
#include <Engine/Core/Log.h>
#include <Engine/ImGui/ImGuiLayer.h>
#include <Engine/ECS/Scene.h>
#include <Engine/Assets/AssetManager.h>
@@ -43,6 +45,7 @@
#include <array>
#include <limits>
#include <string>
namespace Engine
{
@@ -67,11 +70,22 @@ namespace Engine
*m_Device, *m_Allocator,
sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
// 骨骼蒙皮:offsets 与实例序平行(每实例一个 palette 基址);
// palette 每实体一个 slot(帧首 bump 分配),容量 kMaxSkinMatrices
m_SkinOffsetBuffer = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
sizeof(u32) * kMaxInstances, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
m_SkinPaletteBuffer = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
sizeof(glm::mat4) * kMaxSkinMatrices, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
// 3. pass 装配体:布局/shader/采样器/per-slot 描述符
m_SceneGpu = CreateScope<Passes::SceneGpu>(
*m_Device, *m_DescriptorManager, *m_ShaderManager,
*m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer);
*m_Device, *m_Allocator, *m_DescriptorManager, *m_ShaderManager,
*m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer,
*m_SkinOffsetBuffer, *m_SkinPaletteBuffer);
m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>(
*m_Device, *m_DescriptorManager, *m_ShaderManager);
@@ -186,9 +200,17 @@ namespace Engine
return m_SceneRenderer->Frame().bloomEnabled;
}
void Renderer::SetBloomEnabled(bool b) { m_SceneRenderer->SetBloomEnabled(b); }
bool Renderer::GetLodEnabled() const { return m_SceneRenderer->Frame().lodEnabled; }
void Renderer::SetLodEnabled(bool b) { m_SceneRenderer->SetLodEnabled(b); }
int Renderer::GetDebugView() const { return m_SceneRenderer->Frame().debugView; }
void Renderer::SetDebugView(int v) { m_SceneRenderer->SetDebugView(v); }
void Renderer::SetDisplayMode(int mode)
{
m_DesiredDisplayMode = mode;
m_SceneRenderer->SetDisplayMode(mode);
}
void Renderer::SetEnvironment(const Cubemap* env)
{
if (!env) env = m_Environment.get();
@@ -208,9 +230,9 @@ namespace Engine
static const std::vector<const char*> kNames = {
"Final", "TemporalDiff", "HDR", "TAAColor", "Bloom",
"Velocity", "ShadowMap0", "ShadowMap1", "ShadowMap2",
"EnvCube", "Irradiance", "Prefilter", "BrdfLut",
"EnvCube", "Irradiance", "Prefilter", "BrdfLut", "Transparent",
};
static_assert(int(Passes::kDebugViewCount) == 13,
static_assert(int(Passes::kDebugViewCount) == 14,
"DebugView 枚举与名表同步");
return kNames;
}
@@ -225,6 +247,11 @@ namespace Engine
return m_SceneRenderer->Frame().instancesTruncated;
}
f32 Renderer::GetCollectMs() const
{
return m_SceneRenderer->GetCollectMs();
}
u32 Renderer::GetShadowInstanceCount() const
{
return m_SceneRenderer->GetShadowInstanceCount();
@@ -237,9 +264,17 @@ namespace Engine
// ---- 资源声明(命名资源;pass 经句柄引用,数据流由 uses 决定)----
// 导入 swapchain 颜色附件
// 导入 swapchain 颜附件(物理尺寸 = 窗口,与场景渲染分辨率解耦)
m_Backbuffer = m_RenderGraph->ImportSwapchain(
"Backbuffer", m_Swapchain->GetFormat());
m_RenderGraph->SetImportedExtent(m_Backbuffer, m_Swapchain->GetExtent());
// 显示目标:tonemap 写这里(offscreen),ImGui::Image 采样后自由
// 摆放;backbuffer 只承载 ImGui。格式随 swapchain(Unorm:tonemap
// 自带 gamma 编码,采样 → ImGui 写回全程无二次编解码,零色偏)
m_PresentTarget = m_RenderGraph->CreateTexture(
"PresentTarget",
RG::TextureDesc{m_Swapchain->GetFormat()});
// 深度图:目标尺寸,格式由 Device 能力决定
const vk::Format depthFormat = m_Device->FindSupportedFormat(
@@ -310,10 +345,15 @@ namespace Engine
m_BloomB));
m_Passes.push_back(CreateScope<Passes::TonemapPass>(
*m_FullscreenGpu, *m_DescriptorManager, *m_PostParams, frame,
m_TAAColor, m_BloomA, m_TemporalDiff, m_Backbuffer, m_HDRColor,
m_TAAColor, m_BloomA, m_TemporalDiff, m_PresentTarget, m_HDRColor,
m_Velocity, m_ShadowMaps[0], m_ShadowMaps[1], m_ShadowMaps[2]));
// Direct 模式适配器:PresentTarget → backbuffer(Viewport 模式空转)。
// 必须注册在 ImGui 之前:两者都写 backbuffer,WAW 边按声明序
// 连接(前 writer → 后 writer),反序会让 blit 盖掉 UI
m_Passes.push_back(CreateScope<Passes::BlitPass>(
frame, m_PresentTarget, m_Backbuffer));
m_Passes.push_back(CreateScope<Passes::ImGuiPass>(
&m_ImGuiLayer, m_Backbuffer));
&m_ImGuiLayer, frame, m_Backbuffer, m_PresentTarget));
m_Passes.push_back(CreateScope<Passes::PresentPass>(m_Backbuffer));
for (auto& pass : m_Passes) {
@@ -323,29 +363,88 @@ namespace Engine
m_RenderGraph->SetTargetExtent(m_Extent); // 触发首次 Compile
}
void Renderer::SetVSync(bool vsync)
// ---- 帧首同步:所有"会换物理资源/描述符"的意图统一在这里落定 ----
vk::Extent2D Renderer::DesiredRenderExtent() const
{
if (m_Swapchain->GetVSync() == vsync) return;
m_Swapchain->SetVSync(vsync);
HandleResize(); // present mode 变化需要重建 swapchain
// Editor 视口模式:渲染分辨率跟随 ImGui 面板(面板未上报/关闭时
// 回退 swapchain 尺寸);Direct 模式:全窗口
return (m_DesiredDisplayMode == kDisplayViewport && m_DesiredViewport.width)
? m_DesiredViewport
: m_Swapchain->GetExtent();
}
void Renderer::HandleResize()
void Renderer::BeginFrameSync()
{
const bool vsyncChanged = (m_DesiredVSync != m_Swapchain->GetVSync());
const bool windowResized = m_Window.WasResized();
const vk::Extent2D want = DesiredRenderExtent();
const bool extentChanged =
(want.width != m_Extent.width || want.height != m_Extent.height);
if (!vsyncChanged && !windowResized && !extentChanged) return;
if (windowResized) m_Window.ResetResizedFlag();
// swapchain 重建仅由窗口 resize / vsync 切换触发;
// 面板拖拽只重编译 RG(swapchain 与渲染分辨率解耦)
HandleResize(windowResized || vsyncChanged);
}
// GPU 空闲 → (可选)重建 swapchain → RG 重编译到期望渲染分辨率 →
// 各 pass 重绑输入描述符 → 场景纹理重新注册到 ImGui。
// 调用点:BeginFrameSync(UI 构建前)与帧中止路径(acquire/present
// 失败)——都不存在"已构建 draw data 引用旧描述符"的窗口
void Renderer::HandleResize(bool recreateSwapchain)
{
m_Device->GetDevice().waitIdle();
m_Swapchain->Recreate(m_Window);
m_Extent = m_Swapchain->GetExtent();
if (recreateSwapchain) {
// 先把 vsync 意图落进 swapchain(Create 按 m_VSync 选 present
// mode 且从不自行修改它)——否则 vsyncChanged 每帧恒真,
// swapchain 被无限重建,present mode 也永远切不过去
m_Swapchain->SetVSync(m_DesiredVSync);
m_Swapchain->Recreate(m_Window);
// image 数量可能变化
m_SyncManager = CreateScope<SyncManager>(
*m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount());
// backbuffer 物理尺寸跟新窗口走(ImGui 渲染区域 = 全窗口)
m_RenderGraph->SetImportedExtent(m_Backbuffer,
m_Swapchain->GetExtent());
}
// image 数量可能变化
m_SyncManager = CreateScope<SyncManager>(
*m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount());
m_Extent = DesiredRenderExtent(); // 重建后取(swapchain 尺寸已新鲜)
// RG 重编译:物理镜像按新尺寸轮换,各 pass 重绑输入描述符
m_RenderGraph->SetTargetExtent(m_Extent);
for (auto& pass : m_Passes) {
pass->OnTargetsRebuilt(*m_RenderGraph);
}
RefreshSceneTextures();
}
// ---- 场景显示纹理(PresentTarget → ImGui::Image)----
void* Renderer::GetSceneTexture() const
{
// UI 构建段时 m_CurrentFrame 已指向本帧 Render 将使用的槽位
//(上一帧 Render 尾部推进),UI 与渲染同槽位
void* tex = nullptr;
if (m_ImGuiLayer) {
tex = (void*)(intptr_t)
m_ImGuiLayer->GetSceneTexture(m_CurrentFrame);
}
return tex;
}
// RG 物理镜像按帧槽位各一份:每槽位注册一个 ImGui 描述符。
// 首次编译后(SetImGuiLayer)与每次重编译后(HandleResize)调用
void Renderer::RefreshSceneTextures()
{
if (!m_ImGuiLayer || !m_PresentTarget.Valid()) return;
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_ImGuiLayer->UpdateSceneTexture(
slot, m_RenderGraph->GetPhysicalView(m_PresentTarget, slot));
}
}
void Renderer::ReadGpuTimes(u32 frameSlot)
@@ -379,19 +478,16 @@ namespace Engine
}
}
void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets)
void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets, f32 dt)
{
ZoneScoped; // CPU 侧:UBO/SSBO 上传 + 录制 + 提交(GPU 耗时见 RG timestamp)
if (m_Window.WasResized()) {
m_Window.ResetResizedFlag();
HandleResize();
}
// resize 已在 BeginFrameSync(UI 构建前)处理,此处无需再查
const u32 frameSlot = m_CurrentFrame;
const auto& vkDevice = m_Device->GetDevice();
// 1. 场景 → 当帧渲染数据(相机/灯光/双视图剔除;纯 CPU,不碰 GPU)
if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent)) {
if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent, dt)) {
return; // 无 primary 相机:跳帧
}
const FrameSceneData& frame = m_SceneRenderer->Frame();
@@ -410,6 +506,18 @@ namespace Engine
frame.prevInstanceModels.size() *
sizeof(glm::mat4));
}
// 蒙皮数据与实例矩阵同窗口落槽位 ring(fence 已等,写-读竞争关闭)
if (!frame.skinOffsets.empty()) {
m_SkinOffsetBuffer->Update(frameSlot, frame.skinOffsets.data(),
frame.skinOffsets.size() * sizeof(u32));
}
if (!frame.skinPalettes.empty()) {
m_SkinPaletteBuffer->Update(frameSlot, frame.skinPalettes.data(),
frame.skinPalettes.size() *
sizeof(glm::mat4));
}
// MDI 命令/count 同窗口落槽位 ring(fence 已等,写-读竞争关闭)
m_SceneGpu->UploadIndirect(frameSlot, frame);
// 3. acquire swapchain image
u32 imageIndex = 0;
@@ -420,13 +528,13 @@ namespace Engine
m_SyncManager->GetImageAvailable(frameSlot));
if (result.result == vk::Result::eErrorOutOfDateKHR) {
HandleResize();
HandleResize(true); // 帧中止:无 draw data 引用旧资源,安全
return;
}
imageIndex = result.value;
}
catch (const vk::OutOfDateKHRError&) {
HandleResize();
HandleResize(true);
return;
}
@@ -479,11 +587,11 @@ namespace Engine
try {
const auto result = m_Device->GetPresentQueue().presentKHR(presentInfo);
if (result == vk::Result::eSuboptimalKHR) {
HandleResize();
HandleResize(true);
}
}
catch (const vk::OutOfDateKHRError&) {
HandleResize();
HandleResize(true);
}
m_CurrentFrame = (m_CurrentFrame + 1) % kMaxFramesInFlight;
+396 -26
View File
@@ -14,6 +14,10 @@
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <algorithm>
#include <numeric>
#include <tracy/Tracy.hpp>
@@ -43,6 +47,121 @@ namespace Engine
}
return r;
}
// ---- 骨骼动画采样(BuildFrame 第 2.5 节专用)----
// 静止 local TRS 分解(列主序 SRT 矩阵;假设无负缩放/剪切)。
// 被动画轨道触碰的节点用关键帧重建,未触碰的保持原矩阵——
// FBX PreRotation 折在 localTransform 里,拆 TRS 重建会丢
void DecomposeTRS(const glm::mat4& m, glm::vec3& pos, glm::quat& rot,
glm::vec3& scl)
{
pos = glm::vec3(m[3]);
scl = {glm::length(glm::vec3(m[0])),
glm::length(glm::vec3(m[1])),
glm::length(glm::vec3(m[2]))};
const glm::mat3 r(glm::vec3(m[0]) / scl.x,
glm::vec3(m[1]) / scl.y,
glm::vec3(m[2]) / scl.z);
rot = glm::quat_cast(r);
}
// 关键帧采样:前后键插值(位置/缩放 lerp,旋转 slerp)。
// key 数通常几十个,线性扫描对 cache 更友好
glm::vec3 SampleKeys(const std::vector<PosKey>& keys, f32 t,
const glm::vec3& rest)
{
if (keys.empty()) return rest;
if (t <= keys.front().time) return keys.front().value;
for (size_t i = 0; i + 1 < keys.size(); ++i) {
if (t < keys[i + 1].time) {
const f32 a = keys[i].time, b = keys[i + 1].time;
return glm::mix(keys[i].value, keys[i + 1].value,
b > a ? (t - a) / (b - a) : 0.0f);
}
}
return keys.back().value;
}
glm::vec3 SampleKeys(const std::vector<SclKey>& keys, f32 t,
const glm::vec3& rest)
{
if (keys.empty()) return rest;
if (t <= keys.front().time) return keys.front().value;
for (size_t i = 0; i + 1 < keys.size(); ++i) {
if (t < keys[i + 1].time) {
const f32 a = keys[i].time, b = keys[i + 1].time;
return glm::mix(keys[i].value, keys[i + 1].value,
b > a ? (t - a) / (b - a) : 0.0f);
}
}
return keys.back().value;
}
glm::quat SampleKeys(const std::vector<RotKey>& keys, f32 t,
const glm::quat& rest)
{
if (keys.empty()) return rest;
if (t <= keys.front().time) return keys.front().value;
for (size_t i = 0; i + 1 < keys.size(); ++i) {
if (t < keys[i + 1].time) {
const f32 a = keys[i].time, b = keys[i + 1].time;
return glm::slerp(keys[i].value, keys[i + 1].value,
b > a ? (t - a) / (b - a) : 0.0f);
}
}
return keys.back().value;
}
// 单 clip 在 time 的全节点局部 TRS(无轨道分量 = 静止分解值)。
// touched = 被任意轨道触碰的节点标记(ComposeGlobals 决定重建或原样)
void SampleLocalPose(const Model& model, const AnimationClip& clip,
f32 time, std::vector<glm::vec3>& pos,
std::vector<glm::quat>& rot,
std::vector<glm::vec3>& scl,
std::vector<u8>& touched)
{
const auto& nodes = model.GetNodes();
pos.resize(nodes.size());
rot.resize(nodes.size());
scl.resize(nodes.size());
touched.assign(nodes.size(), 0);
for (u32 i = 0; i < nodes.size(); ++i) {
DecomposeTRS(nodes[i].localTransform, pos[i], rot[i], scl[i]);
}
if (clip.duration <= 0.0f) return;
const f32 t = glm::mod(time, clip.duration);
for (const auto& ch : clip.channels) {
if (ch.nodeIdx >= nodes.size()) continue;
touched[ch.nodeIdx] = 1;
if (!ch.positions.empty())
pos[ch.nodeIdx] = SampleKeys(ch.positions, t, pos[ch.nodeIdx]);
if (!ch.rotations.empty())
rot[ch.nodeIdx] = SampleKeys(ch.rotations, t, rot[ch.nodeIdx]);
if (!ch.scales.empty())
scl[ch.nodeIdx] = SampleKeys(ch.scales, t, scl[ch.nodeIdx]);
}
}
// 局部 TRS → 全节点 global 矩阵(层级累乘;父序 DFS 保证父先算)
void ComposeGlobals(const Model& model, const std::vector<glm::vec3>& pos,
const std::vector<glm::quat>& rot,
const std::vector<glm::vec3>& scl,
const std::vector<u8>& touched,
std::vector<glm::mat4>& globals)
{
const auto& nodes = model.GetNodes();
globals.resize(nodes.size());
for (u32 i = 0; i < nodes.size(); ++i) {
const glm::mat4 local = touched[i]
? glm::translate(glm::mat4(1.0f), pos[i]) *
glm::mat4_cast(rot[i]) * glm::scale(glm::mat4(1.0f), scl[i])
: nodes[i].localTransform;
globals[i] = nodes[i].parent >= 0
? globals[static_cast<u32>(nodes[i].parent)] * local
: local;
}
}
}
void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir)
@@ -129,7 +248,7 @@ namespace Engine
}
bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
vk::Extent2D targetExtent)
vk::Extent2D targetExtent, f32 dt)
{
ZoneScopedN("BuildFrame");
const auto collectStart = std::chrono::steady_clock::now();
@@ -174,7 +293,9 @@ namespace Engine
m_Frame.shadowsEnabled = m_ShadowsEnabled;
m_Frame.taaEnabled = m_TAAEnabled;
m_Frame.bloomEnabled = m_BloomEnabled;
m_Frame.lodEnabled = m_LodEnabled;
m_Frame.debugView = m_DebugView;
m_Frame.displayMode = m_DisplayMode;
ubo.cascadeSplits.w = m_ShadowsEnabled ? 1.0f : 0.0f;
// 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass;
@@ -246,6 +367,107 @@ namespace Engine
ComputeCascades(shadowDir);
ubo.cameraPos = glm::vec4(camTransform->GetPosition(), 1.0f);
// ---- 2.5 骨骼动画推进 + 姿态采样(与可见性无关:播放头恒走,
// 否则被剔除的实体回屏时动画跳变)----
// palette 槽 0 固定 identity:静态模型/无动画实例的蒙皮直通
m_Frame.skinPalettes.clear();
m_Frame.skinPalettes.push_back(glm::mat4(1.0f));
m_Frame.skinOffsets.clear();
m_Frame.skinPaletteTruncated = false;
m_EntitySkinBase.clear();
scene.Each<MeshRendererComponent, AnimationComponent>(
[&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) {
const Model* model = assets.GetModel(mr.model);
if (!model || !model->HasAnimations()) return;
const auto& clips = model->GetClips();
// clip 下标合法性(模型资产可能已换)
if (anim.clipIndex < 0 ||
anim.clipIndex >= static_cast<int>(clips.size())) {
anim.clipIndex = 0;
anim.fadeFrom = -1;
if (clips.empty()) return;
}
if (anim.fadeFrom >= static_cast<int>(clips.size())) {
anim.fadeFrom = -1;
}
// ---- 播放头推进(wrap/clamp)----
if (anim.playing && anim.speed != 0.0f) {
const f32 d = dt * anim.speed;
anim.time += d;
if (anim.fadeFrom >= 0) anim.fadeFromTime += d;
}
const auto wrapTime = [&](f32 t, const AnimationClip& c) {
if (c.duration <= 0.0f) return 0.0f;
return anim.loop ? glm::mod(t, c.duration)
: glm::clamp(t, 0.0f, c.duration - 1e-3f);
};
const AnimationClip& cur = clips[anim.clipIndex];
anim.time = wrapTime(anim.time, cur);
const AnimationClip* src = nullptr;
f32 srcTime = 0.0f;
f32 wCur = 1.0f; // 当前 clip 权重(crossfade 0→1)
if (anim.fadeFrom >= 0) {
// 淡出计时(暂停时冻结,与播放头一致)
if (anim.playing) anim.fadeElapsed += dt;
const f32 f = glm::min(
anim.fadeElapsed / glm::max(anim.fadeDuration, 1e-4f),
1.0f);
wCur = f * f * (3.0f - 2.0f * f); // smoothstep
if (f >= 1.0f) {
anim.fadeFrom = -1; // 过渡完成
} else {
src = &clips[anim.fadeFrom];
anim.fadeFromTime = wrapTime(anim.fadeFromTime, *src);
srcTime = anim.fadeFromTime;
}
}
// ---- 采样 + 局部 TRS 层混合 → global → palette ----
SampleLocalPose(*model, cur, anim.time,
m_PosePosA, m_PoseRotA, m_PoseSclA, m_TouchedA);
if (src) {
SampleLocalPose(*model, *src, srcTime,
m_PosePosB, m_PoseRotB, m_PoseSclB,
m_TouchedB);
const size_t n = m_PosePosA.size();
for (size_t i = 0; i < n; ++i) {
// touched = 两层轨道的并集(重建集合)
m_TouchedA[i] = static_cast<u8>(
m_TouchedA[i] | m_TouchedB[i]);
m_PosePosA[i] = glm::mix(m_PosePosB[i], m_PosePosA[i], wCur);
m_PoseSclA[i] = glm::mix(m_PoseSclB[i], m_PoseSclA[i], wCur);
// 旋转必须 slerp(lerp 单位四元数会走样)
m_PoseRotA[i] = glm::slerp(m_PoseRotB[i], m_PoseRotA[i], wCur);
}
}
ComposeGlobals(*model, m_PosePosA, m_PoseRotA, m_PoseSclA,
m_TouchedA, m_Globals);
// palette bump 分配(按实体,不按模型去重——同模型两实体
// 可不同播放状态)。预算打满 → 该实体退回绑定姿势
const auto& bones = model->GetBones();
const u32 base = static_cast<u32>(m_Frame.skinPalettes.size());
if (base + bones.size() > kMaxSkinMatrices) {
m_Frame.skinPaletteTruncated = true;
if (!m_SkinTruncWarned) {
m_SkinTruncWarned = true;
ENGINE_WARN("GPUDriven: skin palette full ({}), "
"animated entities fall back to bind pose",
kMaxSkinMatrices);
}
return;
}
for (const auto& b : bones) {
const u32 ni = b.nodeIdx < m_Globals.size()
? b.nodeIdx : 0;
m_Frame.skinPalettes.push_back(m_Globals[ni] * b.invBind);
}
const u32 entKey = static_cast<u32>(entt::to_integral(entity));
m_EntitySkinBase[entKey] = base;
});
// ---- 3. GPUDriven:单趟收集 + 多视图剔除 ----
// 同作用域第二个 zone:ZoneScopedN 变量名固定会重声明,用 ZoneNamedN
ZoneNamedN( ___tracy_collect, "GPUDriven Collect", true );
@@ -258,8 +480,10 @@ namespace Engine
// batch.first 拼接时加上段基址,保证段内区间连续。
m_Frame.instanceModels.clear();
m_Frame.prevInstanceModels.clear();
m_Frame.skinOffsets.clear();
m_MainScratch.Clear();
for (auto& s : m_CascScratch) s.Clear();
m_TranslucentScratch.Clear(); // 透明队列每帧重排批次,必须整帧重建
u32 totalInstances = 0;
bool instanceTruncated = false;
@@ -285,19 +509,57 @@ namespace Engine
}
m_CurWorlds.emplace(entKey, t.World());
// palette 基址(第 2.5 节预采样;无动画实体 = identity 槽)
u32 skinBase = kIdentityPaletteSlot;
if (auto it = m_EntitySkinBase.find(entKey);
it != m_EntitySkinBase.end()) {
skinBase = it->second;
}
// 实例世界缩放(矩阵列向量最大长度,包围球半径随缩放)
const f32 worldScale = glm::max(
glm::length(glm::vec3(t.World()[0])),
glm::max(glm::length(glm::vec3(t.World()[1])),
glm::length(glm::vec3(t.World()[2]))));
// ---- 实体级粗剔除(联合包围球):四视图全部看不到 →
// 跳过整个 mesh 循环(多 mesh 模型的大头优化)----
const Mesh::Bounds& mb = model->GetBounds();
const glm::vec3 modelCenter =
glm::vec3(t.World() * glm::vec4(mb.center, 1.0f));
const f32 modelRadius = mb.radius * worldScale;
const bool mainVis =
mainFrustum.IntersectsSphere(modelCenter, modelRadius);
bool cascVis[kMaxCascades];
bool anyVis = mainVis;
for (u32 c = 0; c < kMaxCascades; ++c) {
cascVis[c] =
cascFrustum[c].IntersectsSphere(modelCenter, modelRadius);
anyVis |= cascVis[c];
}
if (!anyVis) return;
for (auto& mesh : model->GetMeshes()) {
const auto& bounds = mesh->GetBounds();
const glm::vec3 worldCenter =
glm::vec3(t.World() * glm::vec4(bounds.center, 1.0f));
const f32 worldRadius = bounds.radius * worldScale;
auto tryAppend = [&](ViewScratch& s, const Frustum& frustum) {
// LOD 选级:屏占比近似 = radius/距离(主视图按实例距离;
// 阈值 10/20 倍半径,可调)。开关关闭 = 恒 LOD0
const f32 dist =
glm::length(worldCenter - camTransform->GetPosition());
u32 mainLod = 0;
if (m_LodEnabled) {
if (dist >= worldRadius * 20.0f) {
mainLod = 2;
} else if (dist >= worldRadius * 10.0f) {
mainLod = 1;
}
}
auto tryAppend = [&](ViewScratch& s, const Frustum& frustum,
u32 lod) {
if (!frustum.IntersectsSphere(worldCenter, worldRadius)) {
return;
}
@@ -305,30 +567,47 @@ namespace Engine
instanceTruncated = true;
return;
}
// lookup 值 = batches 索引(此前误存 worlds 索引:
// 单实体时两序号恰好恒等,多实体共享 mesh 后分叉
// → batches[worldsIdx] 越界)。batch.first(GPU
// firstInstance 基址)才是 worlds 索引,语义别混
const auto [it, inserted] = s.lookup.emplace(
mesh.get(), static_cast<u32>(s.result.batches.size()));
if (inserted) {
s.result.batches.push_back({mesh.get(), 0, 0});
s.batchWorlds.emplace_back();
s.batchPrevWorlds.emplace_back();
// batch 查找 = 数组寻址(Mesh::GetSlot 槽位 + LOD 槽)
// batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch)
const u32 slot = mesh->GetSlot();
if (slot >= s.lookup.size()) {
s.lookup.resize(slot + 1);
}
auto& lodSlots = s.lookup[slot];
u32 batchIdx = lodSlots[lod];
if (batchIdx == 0) {
batchIdx = static_cast<u32>(s.result.batches.size()) + 1;
lodSlots[lod] = batchIdx;
s.result.batches.push_back({mesh.get(), 0, 0, lod});
s.batchMats.emplace_back();
s.batchDistSum.push_back(0.0f);
}
// 实例归入自己的 batch(mesh 分组),保证同 batch
// 实例在扁平化后连续——firstInstance 语义的前提
const u32 batchIdx = it->second;
s.result.batches[batchIdx].count++;
s.batchWorlds[batchIdx].push_back(t.World());
s.batchPrevWorlds[batchIdx].push_back(prevWorld);
const u32 bi = batchIdx - 1;
s.result.batches[bi].count++;
s.batchMats[bi].push_back({t.World(), prevWorld, skinBase});
s.batchDistSum[bi] += dist; // 透明排序用
s.result.instanceCount++;
++totalInstances;
};
tryAppend(m_MainScratch, mainFrustum);
for (u32 c = 0; c < kMaxCascades; ++c) {
tryAppend(m_CascScratch[c], cascFrustum[c]);
// 透明材质 → 独立队列(主视图,混合绘制 + 排序;
// 不进阴影——alpha 阴影复杂度高收益低,注释后续演进)
const bool isTransparentMesh =
mesh->GetMaterial()->IsTransparent();
ViewScratch& mainTarget = isTransparentMesh
? m_TranslucentScratch : m_MainScratch;
tryAppend(mainTarget, mainFrustum, mainLod);
if (!isTransparentMesh) {
for (u32 c = 0; c < kMaxCascades; ++c) {
if (!cascVis[c]) continue; // 粗剔除已判不可见
// 级联降级:越远的级联覆盖范围越大,用越低的 LOD
//(阴影本来就是低频信息,误差可接受);开关关闭 = 恒 LOD0
tryAppend(m_CascScratch[c], cascFrustum[c],
m_LodEnabled ? std::min(c, kMeshLodCount - 1) : 0u);
}
}
}
});
@@ -343,13 +622,12 @@ namespace Engine
b.first = base; // 扁平化后的连续区间基址
base += b.count;
dst.batches.push_back(b);
m_Frame.instanceModels.insert(m_Frame.instanceModels.end(),
s.batchWorlds[i].begin(),
s.batchWorlds[i].end());
m_Frame.prevInstanceModels.insert(
m_Frame.prevInstanceModels.end(),
s.batchPrevWorlds[i].begin(),
s.batchPrevWorlds[i].end());
for (const auto& m : s.batchMats[i]) {
m_Frame.instanceModels.push_back(m.cur);
m_Frame.prevInstanceModels.push_back(m.prev);
// 与实例序平行对齐(shader 按 gl_InstanceIndex 取)
m_Frame.skinOffsets.push_back(m.skinBase);
}
}
dst.instanceCount = s.result.instanceCount;
};
@@ -358,6 +636,50 @@ namespace Engine
appendView(m_Frame.shadowCascades[c], m_CascScratch[c]);
}
// ---- 4.5 MDI 打包(已下线:移动视角下存在几何消失缺陷,待隔离修复)----
// PackIndirect(m_Frame.mainView, true);
// for (u32 c = 0; c < kMaxCascades; ++c) {
// PackIndirect(m_Frame.shadowCascades[c], false);
// }
// ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排
{
auto& tb = m_TranslucentScratch.result.batches;
if (tb.size() > 1) {
std::vector<u32> order(tb.size());
std::iota(order.begin(), order.end(), 0);
std::sort(order.begin(), order.end(), [&](u32 a, u32 b) {
return m_TranslucentScratch.batchDistSum[a] / tb[a].count >
m_TranslucentScratch.batchDistSum[b] / tb[b].count;
});
CullResult sorted;
sorted.instanceCount = m_TranslucentScratch.result.instanceCount;
std::vector<ViewScratch::InstanceMatrices> sortedMats;
std::vector<f32> sortedDist;
for (const u32 i : order) {
sorted.batches.push_back(tb[i]);
sortedDist.push_back(m_TranslucentScratch.batchDistSum[i]);
for (auto& m : m_TranslucentScratch.batchMats[i]) {
sortedMats.push_back(std::move(m));
}
}
m_TranslucentScratch.result = std::move(sorted);
m_TranslucentScratch.batchDistSum = std::move(sortedDist);
// batchMats 按新分组的 count 顺序切分重装(保留容量)
m_TranslucentScratch.batchMats.assign(
m_TranslucentScratch.result.batches.size(), {});
u32 offset = 0;
for (size_t i = 0; i < m_TranslucentScratch.result.batches.size(); ++i) {
const u32 n = m_TranslucentScratch.result.batches[i].count;
m_TranslucentScratch.batchMats[i].assign(
std::make_move_iterator(sortedMats.begin() + offset),
std::make_move_iterator(sortedMats.begin() + offset + n));
offset += n;
}
}
}
appendView(m_Frame.translucentView, m_TranslucentScratch);
// 帧末滚动时域状态:cur → prev(每帧重建,销毁的实体自动清出)
m_PrevWorlds.swap(m_CurWorlds);
m_CurWorlds.clear();
@@ -379,4 +701,52 @@ namespace Engine
return true;
}
void SceneRenderer::PackIndirect(CullResult& view, bool byMaterial)
{
view.indirectCommands.clear();
view.indirectGroups.clear();
if (view.batches.empty()) return;
// 组键排序:megabuffer(VB 句柄)→ 材质(可选)→ mesh 槽位。
// 同 Model 的全部 mesh 共享同一 megabuffer,材质是主分组轴;
// 槽位兜底保证排序稳定(组内顺序无语义,防逐帧抖动)
std::sort(view.batches.begin(), view.batches.end(),
[byMaterial](const InstanceBatch& a, const InstanceBatch& b) {
const u64 va = reinterpret_cast<u64>(static_cast<VkBuffer>(
a.mesh->GetVertexBuffer()));
const u64 vb = reinterpret_cast<u64>(static_cast<VkBuffer>(
b.mesh->GetVertexBuffer()));
if (va != vb) return va < vb;
if (byMaterial && a.mesh->GetMaterial() != b.mesh->GetMaterial()) {
return reinterpret_cast<u64>(a.mesh->GetMaterial()) <
reinterpret_cast<u64>(b.mesh->GetMaterial());
}
return a.mesh->GetSlot() < b.mesh->GetSlot();
});
for (const auto& b : view.batches) {
const vk::Buffer vb = b.mesh->GetVertexBuffer();
Material* mat = byMaterial ? b.mesh->GetMaterial() : nullptr;
bool newGroup = view.indirectGroups.empty();
if (!newGroup) {
const IndirectGroup& g = view.indirectGroups.back();
newGroup = g.mesh->GetVertexBuffer() != vb ||
g.material != mat;
}
if (newGroup) {
view.indirectGroups.push_back({b.mesh, mat,
static_cast<u32>(view.indirectCommands.size()), 0});
}
view.indirectCommands.push_back({
b.mesh->GetLodIndexCount(b.lod),
b.count,
b.mesh->GetGlobalFirstIndex(b.lod),
b.mesh->GetVertexOffset(),
b.first});
++view.indirectGroups.back().commandCount;
}
}
} // namespace Engine
@@ -29,6 +29,14 @@ namespace Engine {
m_TextureLayout, halfSize, r, g, b);
}
Scope<Model> VulkanAssetUploader::CreateTransparentPlane(
f32 halfSize, u8 r, u8 g, u8 b, u8 a)
{
return Model::CreateTransparentPlane(m_Device, m_Allocator,
m_Descriptors, m_TextureLayout,
halfSize, r, g, b, a);
}
Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path)
{
// compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT
+18 -2
View File
@@ -166,6 +166,11 @@ void Device::PickPhysicalDevice()
m_DynamicRenderingSupported = features13.dynamicRendering;
m_Synchronization2Supported = features13.synchronization2;
// core 特性:MDI(drawCount>1)必需
m_MultiDrawIndirectSupported = features2.features.multiDrawIndirect == VK_TRUE;
// 1.2 特性:drawIndexedIndirectCount 必需(VUID-04445)
m_DrawIndirectCountSupported = features12.drawIndirectCount == VK_TRUE;
// 1.4 的新特性
bool localReadSupported = features14.dynamicRenderingLocalRead;
bool pushDescriptorSupported = features14.pushDescriptor;
@@ -183,6 +188,10 @@ void Device::PickPhysicalDevice()
ENGINE_INFO(" Dynamic Rendering Local Read (1.4): {}", localReadSupported ? "Yes" : "No");
ENGINE_INFO(" Push Descriptor (1.4): {}", pushDescriptorSupported ? "Yes" : "No");
ENGINE_INFO(" Shader Object (EXT): {}", m_ShaderObjectSupported ? "Yes" : "No");
ENGINE_INFO(" Multi Draw Indirect (1.0 core): {}",
m_MultiDrawIndirectSupported ? "Yes" : "No");
ENGINE_INFO(" Draw Indirect Count (1.2): {}",
m_DrawIndirectCountSupported ? "Yes" : "No");
}
bool Device::IsDeviceSuitable(vk::PhysicalDevice device)
@@ -244,6 +253,8 @@ void Device::CreateLogicalDevice()
}
vk::PhysicalDeviceFeatures features;
// MDI:按材质分组的 drawIndexedIndirectCount,drawCount>1 必需
features.setMultiDrawIndirect(m_MultiDrawIndirectSupported ? VK_TRUE : VK_FALSE);
// ========================================================
// 启用 1.4 特性链
@@ -254,23 +265,28 @@ void Device::CreateLogicalDevice()
vk::PhysicalDeviceVulkan14Features features14{};
vk::PhysicalDeviceVulkan13Features features13{};
vk::PhysicalDeviceVulkan12Features features12{};
// 启用 1.3 的特性
features13.dynamicRendering = VK_TRUE;
features13.synchronization2 = VK_TRUE;
// 启用 1.2 的特性:MDI 的 drawIndexedIndirectCount(VUID-04445)
features12.drawIndirectCount = m_DrawIndirectCountSupported ? VK_TRUE : VK_FALSE;
// 启用 1.4 的新特性
features14.dynamicRenderingLocalRead = VK_TRUE;
features14.pushDescriptor = VK_TRUE;
// 链接:1.4 → 1.3 → (shader object)
// 链接:1.4 → 1.3 → 1.2 → (shader object)
features14.pNext = &features13;
features13.pNext = &features12;
// Shader Object 特性(如果支持)
vk::PhysicalDeviceShaderObjectFeaturesEXT shaderObjFeatures{};
if (m_ShaderObjectSupported) {
shaderObjFeatures.shaderObject = VK_TRUE;
features13.pNext = &shaderObjFeatures;
features12.pNext = &shaderObjFeatures;
}
// 扩展列表
+4 -1
View File
@@ -72,7 +72,10 @@ namespace Engine
.setImageColorSpace(surfaceFormat.colorSpace)
.setImageExtent(extent)
.setImageArrayLayers(1)
.setImageUsage(vk::ImageUsageFlagBits::eColorAttachment)
// COLOR_ATTACHMENT:ImGui 后端渲染;TRANSFER_DST:
// Direct 模式 BlitPass 直拷场景到 backbuffer
.setImageUsage(vk::ImageUsageFlagBits::eColorAttachment |
vk::ImageUsageFlagBits::eTransferDst)
.setImageSharingMode(vk::SharingMode::eExclusive)
.setPreTransform(caps.currentTransform)
.setCompositeAlpha(compositeAlpha)
+7
View File
@@ -64,6 +64,13 @@ namespace Engine
{r, g, b, 255}, srgb));
}
Scope<Texture> Texture::CreateSolidColorRGBA(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b, u8 a, bool srgb)
{
return Scope<Texture>(new Texture(device, allocator, ProceduralTag{},
{r, g, b, a}, srgb));
}
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb)
{
m_Width = width;
+1
Submodule Lib/meshoptimizer added at 4c203430ca
+24 -2
View File
@@ -3,17 +3,39 @@
// Shadow pass vertex:光源视角写深度。
// GPUDriven:模型矩阵经 gl_InstanceIndex 从实例缓冲取(与 Forward
// 共用同一份数据),push 只带 lightVP(全 pass 一次)。
// 骨骼蒙皮与 Forward 同一套数据(binding 3/4),阴影与主体形变一致。
layout(location = 0) in vec3 inPosition;
layout(location = 4) in uvec4 inBoneIds; // 骨骼槽(palette 下标)
layout(location = 5) in vec4 inWeights; // 蒙皮权重(无骨骼顶点 = (1,0,0,0))
layout(std430, set = 0, binding = 1) readonly buffer ObjectBuffer {
mat4 models[];
} objects;
// 骨骼蒙皮:与 simple_shader.vert 同布局(Set0 binding 3/4)
layout(std430, set = 0, binding = 3) readonly buffer SkinOffsetBuffer {
uint skinBases[];
} skinOffsets;
layout(std430, set = 0, binding = 4) readonly buffer SkinPaletteBuffer {
mat4 bones[];
} skinPalettes;
layout(push_constant) uniform PushConstants {
mat4 lightVP;
} pc;
void main() {
gl_Position = pc.lightVP * objects.models[gl_InstanceIndex] * vec4(inPosition, 1.0);
mat4 SkinMatrix() {
uint base = skinOffsets.skinBases[gl_InstanceIndex];
return skinPalettes.bones[base + inBoneIds.x] * inWeights.x +
skinPalettes.bones[base + inBoneIds.y] * inWeights.y +
skinPalettes.bones[base + inBoneIds.z] * inWeights.z +
skinPalettes.bones[base + inBoneIds.w] * inWeights.w;
}
void main() {
mat4 skin = SkinMatrix();
vec3 skinnedPos = (skin * vec4(inPosition, 1.0)).xyz;
gl_Position = pc.lightVP * objects.models[gl_InstanceIndex] * vec4(skinnedPos, 1.0);
}
+13 -5
View File
@@ -40,8 +40,9 @@ layout(set = 1, binding = 1) uniform sampler2D normalMap;
layout(set = 1, binding = 2) uniform sampler2D metallicRoughnessMap; // G=粗糙度 B=金属度
layout(set = 1, binding = 3) uniform sampler2D emissiveMap;
layout(set = 1, binding = 4) uniform sampler2D aoMap; // 环境光遮蔽(R 通道)
layout(set = 1, binding = 5) uniform MaterialParams {
vec4 factors; // x = metallic 因子,y = roughness 因子
layout(set = 1, binding = 5) uniform sampler2D roughnessMap; // 独立粗糙度图(R 通道,specular 工作流)
layout(set = 1, binding = 6) uniform MaterialParams {
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关
} matParams;
// CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
@@ -134,9 +135,14 @@ vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
}
void main() {
vec3 albedo = texture(albedoMap, fragUV).rgb;
vec4 albedoTex = texture(albedoMap, fragUV);
vec3 albedo = albedoTex.rgb;
vec4 mr = texture(metallicRoughnessMap, fragUV);
float roughness = clamp(mr.g * matParams.factors.y, 0.045, 1.0);
// 粗糙度源按 factors.z 切换:glTF 合图工作流读 MR.g;
// FBX/OBJ specular 工作流(z=1)换用独立粗糙度图 R 通道
float roughness = clamp(mix(mr.g, texture(roughnessMap, fragUV).r,
matParams.factors.z) * matParams.factors.y,
0.045, 1.0);
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
vec3 emissive = texture(emissiveMap, fragUV).rgb;
@@ -217,6 +223,8 @@ void main() {
vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb
* mix(0.4, 1.0, shade0); // 阴影区环境同步衰减(简化遮蔽)
outColor = vec4(ambient + Lo + emissive, 1.0);
// alpha = albedo 贴图覆盖率(glTF alphaMode=BLEND 混合源);
// 不透明资产贴图 a=1 行为不变。预乘在 tonemap 段完成。
outColor = vec4(ambient + Lo + emissive, albedoTex.a);
outVelocity = fragVelocity;
}
+28 -4
View File
@@ -13,6 +13,8 @@ layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inUV;
layout(location = 3) in vec3 inTangent;
layout(location = 4) in uvec4 inBoneIds; // 骨骼槽(palette 下标,非全局节点号)
layout(location = 5) in vec4 inWeights; // 蒙皮权重(无骨骼顶点 = (1,0,0,0))
layout(location = 0) out vec2 fragUV;
layout(location = 1) out vec3 fragNormal;
@@ -33,6 +35,15 @@ layout(std430, set = 0, binding = 2) readonly buffer PrevObjectBuffer {
mat4 prevModels[];
} prevObjects;
// 骨骼蒙皮:每实例一个 palette 基址(与 models 平行),骨骼矩阵全局扁平
layout(std430, set = 0, binding = 3) readonly buffer SkinOffsetBuffer {
uint skinBases[]; // 实例 → palette 基址
} skinOffsets;
layout(std430, set = 0, binding = 4) readonly buffer SkinPaletteBuffer {
mat4 bones[]; // 全局 palette(槽 0 = identity,静态模型直通)
} skinPalettes;
layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 view;
mat4 proj;
@@ -46,16 +57,28 @@ layout(set = 0, binding = 0) uniform UniformBufferObject {
vec4 cameraPos;
} ubo;
// 4 权重混合的蒙皮矩阵。无骨骼顶点 weights=(1,0,0,0)+ids=0 → identity。
mat4 SkinMatrix() {
uint base = skinOffsets.skinBases[gl_InstanceIndex];
return skinPalettes.bones[base + inBoneIds.x] * inWeights.x +
skinPalettes.bones[base + inBoneIds.y] * inWeights.y +
skinPalettes.bones[base + inBoneIds.z] * inWeights.z +
skinPalettes.bones[base + inBoneIds.w] * inWeights.w;
}
void main() {
mat4 model = objects.models[gl_InstanceIndex];
mat4 skin = SkinMatrix();
vec4 worldPos = model * vec4(inPosition, 1.0);
vec3 skinnedPos = (skin * vec4(inPosition, 1.0)).xyz;
vec4 worldPos = model * vec4(skinnedPos, 1.0);
gl_Position = ubo.proj * ubo.view * worldPos;
// ---- TAA velocity:当前帧与上一帧的 clip 空间位置差(UV 空间)----
// 抖动只进光栅化 gl_Position,不进 velocity(否则抖动本身被当成运动)
// v1:蒙皮形变不进 velocity(prev 用同一蒙皮姿态),边缘 ghosting 接受
vec4 currClip = ubo.projNoJitter * ubo.view * worldPos;
vec4 prevWorldPos = prevObjects.prevModels[gl_InstanceIndex] * vec4(inPosition, 1.0);
vec4 prevWorldPos = prevObjects.prevModels[gl_InstanceIndex] * vec4(skinnedPos, 1.0);
vec4 prevClip = ubo.prevProj * ubo.prevView * prevWorldPos;
fragVelocity = (currClip.xy / currClip.w - prevClip.xy / prevClip.w) * 0.5;
@@ -64,9 +87,10 @@ void main() {
fragViewDepth = -(ubo.view * worldPos).z;
mat3 normalMatrix = mat3(transpose(inverse(model)));
fragNormal = normalize(normalMatrix * inNormal);
// 法线先过蒙皮旋转(形变方向),再过模型逆变换
fragNormal = normalize(normalMatrix * (mat3(skin) * inNormal));
// 切线随模型变换(法线贴图 TBN;无切线数据保持零,frag 侧退回几何法线)
fragTangent = normalize(mat3(model) * inTangent);
fragTangent = normalize(mat3(model) * (mat3(skin) * inTangent));
// 零向量 normalize 产生 NaN,这里直接透传零,frag 判长度
fragTangent = dot(inTangent, inTangent) > 0.0 ? fragTangent : vec3(0.0);
+238
View File
@@ -0,0 +1,238 @@
#version 450
const int MAX_LIGHTS = 4;
const float PI = 3.14159265359;
const int kCascadeRes = 1024; // CSM 级联图边长(与引擎 SceneTypes.h 同步)
struct Light {
vec4 color; // rgb = 颜色,a = 强度
vec4 direction; // 方向光:xyz = 方向(已归一)
vec4 position; // 点光:xyz = 世界位置
vec4 params; // x = 类型(0=方向,1=点),y = 衰减半径
};
layout(location = 0) in vec2 fragUV;
layout(location = 1) in vec3 fragNormal;
layout(location = 2) in vec3 fragWorldPos;
layout(location = 3) in float fragViewDepth;
layout(location = 4) in vec2 fragVelocity;
layout(location = 5) in vec3 fragTangent;
layout(location = 0) out vec4 outColor;
layout(location = 1) out vec2 outVelocity; // TAA velocity 附件
layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 view;
mat4 proj;
mat4 lightSpace[3]; // CSM 各级联光空间 VP
vec4 cascadeSplits; // xyz = 级联 0..2 远边界(视图深度)
vec4 ambientColor;
Light lights[MAX_LIGHTS];
mat4 projNoJitter;
mat4 prevView;
mat4 prevProj;
vec4 cameraPos;
} ubo;
// 材质纹理槽(binding = Material::Slot)
layout(set = 1, binding = 0) uniform sampler2D albedoMap;
layout(set = 1, binding = 1) uniform sampler2D normalMap;
layout(set = 1, binding = 2) uniform sampler2D metallicRoughnessMap; // G=粗糙度 B=金属度
layout(set = 1, binding = 3) uniform sampler2D emissiveMap;
layout(set = 1, binding = 4) uniform sampler2D aoMap; // 环境光遮蔽(R 通道)
layout(set = 1, binding = 5) uniform sampler2D roughnessMap; // 独立粗糙度图(R 通道,specular 工作流)
layout(set = 1, binding = 6) uniform MaterialParams {
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关
} matParams;
// CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
layout(set = 2, binding = 0) uniform sampler2DShadow shadowMap0; // CSM 级联 0(最近)
layout(set = 2, binding = 1) uniform sampler2DShadow shadowMap1; // 级联 1
layout(set = 2, binding = 2) uniform sampler2DShadow shadowMap2; // 级联 2(最远)
layout(set = 2, binding = 3) uniform samplerCube irradianceMap; // 漫反射环境光(IBL 阶段②)
layout(set = 2, binding = 4) uniform samplerCube prefilteredMap; // 镜面环境(mip 分 roughness,阶段③)
layout(set = 2, binding = 5) uniform sampler2D brdfLut; // BRDF 积分表 (NdotV, roughness) → (scale, bias)
// 第 0 盏方向光(KeyLight)的 CSM 阴影因子。
// 级联按视图深度选择;3×3 纹素偏移采样(每 tap 硬件 2×2 PCF,
// 等效 36 次比较)→ 软阴影边缘;采样器 clamp-to-border 白色:
// 光视锥外自动 1.0(无阴影),省出界分支
float ShadowFactor(vec3 N, float viewDepth) {
// 阴影总开关(ubo.cascadeSplits.w 由 CPU 每帧写入):关闭即全亮,
// 配合 ShadowPass 空转(深度图无内容也读不到这里)
if (ubo.cascadeSplits.w < 0.5) return 1.0;
int cascade = 2;
for (int i = 0; i < 3; ++i) {
if (viewDepth < ubo.cascadeSplits[i]) { cascade = i; break; }
}
vec4 ls = ubo.lightSpace[cascade] * vec4(fragWorldPos, 1.0);
vec3 proj = ls.xyz / ls.w;
vec2 uv = proj.xy * 0.5 + 0.5;
vec3 L = normalize(-ubo.lights[0].direction.xyz);
float bias = max(0.004 * (1.0 - dot(N, L)), 0.0012);
float texel = 1.0 / float(kCascadeRes); // 与引擎 kCascadeRes 同步
float shadow = 0.0;
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
float lit;
if (cascade == 0)
lit = texture(shadowMap0, vec3(uv + vec2(x, y) * texel,
proj.z - bias));
else if (cascade == 1)
lit = texture(shadowMap1, vec3(uv + vec2(x, y) * texel,
proj.z - bias));
else
lit = texture(shadowMap2, vec3(uv + vec2(x, y) * texel,
proj.z - bias));
shadow += mix(0.3, 1.0, lit); // 影中保留 0.3 环境感
}
}
return shadow / 9.0;
}
// 法线贴图 → 世界空间法线(TBN 构建)。
// 无切线数据(零向量)或平法线贴图时退回几何法线。
vec3 MappedNormal(vec3 geomN, vec3 T, vec2 uv) {
if (dot(T, T) < 1e-4) return geomN;
vec3 N = geomN;
vec3 nT = normalize(T - N * dot(N, T)); // 正交化
vec3 B = cross(N, nT);
vec3 sampled = texture(normalMap, uv).xyz * 2.0 - 1.0;
if (dot(sampled, sampled) < 1e-4) return geomN;
return normalize(nT * sampled.x + B * sampled.y + N * sampled.z);
}
// ---- Cook-Torrance BRDF 各项 ----
float DistributionGGX(float NdotH, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
return a2 / (PI * d * d);
}
float GeometrySmith(float NdotV, float NdotL, float roughness) {
float r = roughness + 1.0;
float k = (r * r) / 8.0; // 解析光源用此 k(IBL 才用 IBL 版本)
float gv = NdotV / (NdotV * (1.0 - k) + k);
float gl = NdotL / (NdotL * (1.0 - k) + k);
return gv * gl;
}
vec3 FresnelSchlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
// 粗糙度感知的 Fresnel(IBL 用):粗糙表面掠射边缘的 F 上升更平缓
vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
vec3 Fmax = max(vec3(1.0 - roughness), F0);
return F0 + (Fmax - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
void main() {
vec4 albedoTex = texture(albedoMap, fragUV);
vec3 albedo = albedoTex.rgb;
vec4 mr = texture(metallicRoughnessMap, fragUV);
// 粗糙度源按 factors.z 切换:glTF 合图工作流读 MR.g;
// FBX/OBJ specular 工作流(z=1)换用独立粗糙度图 R 通道
float roughness = clamp(mix(mr.g, texture(roughnessMap, fragUV).r,
matParams.factors.z) * matParams.factors.y,
0.045, 1.0);
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
vec3 emissive = texture(emissiveMap, fragUV).rgb;
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, fragUV);
vec3 V = normalize(ubo.cameraPos.xyz - fragWorldPos);
float NdotV = max(dot(N, V), 1e-4);
vec3 F0 = mix(vec3(0.04), albedo, metallic); // 电介质基准反射率 4%
vec3 Lo = vec3(0.0);
float shade0 = 1.0f; // 第 0 盏光的阴影因子(环境光同步衰减用)
for (int i = 0; i < MAX_LIGHTS; ++i) {
// 按类型分支:方向光无衰减;点光方向指向光源 + 平方距离衰减
vec3 L;
float atten = 1.0;
if (ubo.lights[i].params.x > 0.5) {
vec3 toLight = ubo.lights[i].position.xyz - fragWorldPos;
float d = length(toLight);
if (d >= ubo.lights[i].params.y) continue;
L = toLight / d;
float a = clamp(1.0 - (d * d) /
(ubo.lights[i].params.y * ubo.lights[i].params.y),
0.0, 1.0);
atten = a * a;
} else {
L = normalize(-ubo.lights[i].direction.xyz);
}
vec3 radiance = ubo.lights[i].color.rgb * ubo.lights[i].color.a * atten;
float NdotL = max(dot(N, L), 0.0);
if (NdotL <= 0.0) continue;
// Cook-Torrance 镜面项
vec3 H = normalize(V + L);
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = max(dot(H, V), 0.0);
float D = DistributionGGX(NdotH, roughness);
float G = GeometrySmith(NdotV, NdotL, roughness);
vec3 F = FresnelSchlick(NdotH2, F0);
vec3 kS = F;
vec3 kD = (1.0 - kS) * (1.0 - metallic); // 金属不参与漫反射
vec3 specular = (D * G * F) /
max(4.0 * NdotV * NdotL, 1e-4);
// 阴影只来自第 0 盏方向光(CSM)
float shade = (i == 0 && ubo.lights[0].params.x < 0.5)
? ShadowFactor(N, fragViewDepth) : 1.0;
if (i == 0) shade0 = shade;
// 漫反射影中保留 0.3(间接光近似);镜面项是"光源的镜面像",
// 光源被遮挡即完全消失——按 shade 重映射(0.3→0,1.0→1)。
// 否则掠射角下 spec(HDR 8)×0.3 仍是白块,影子比天空还亮
float shadeSpec = max((shade - 0.3f) / 0.7f, 0.0f);
Lo += (kD * albedo / PI) * radiance * NdotL * shade
+ specular * radiance * NdotL * shadeSpec;
}
// 环境光:IBL split-sum(阶段②③)。
// 漫反射:N 采样 irradiance cube(半球 Lambert 预卷积)
// 镜面:R 采样 pre-filtered cube(mip = roughness 分级)
// × BRDF LUT 查表得 F 缩放/偏置
// ambientColor = 用户可调强度/色调乘子
vec3 F_amb = FresnelSchlickRoughness(NdotV, F0, roughness);
vec3 kD_amb = (1.0 - F_amb) * (1.0 - metallic);
vec3 irradiance = texture(irradianceMap, N).rgb;
float ao = texture(aoMap, fragUV).r;
vec3 diffuse = kD_amb * irradiance * albedo * ao;
vec3 R = reflect(-V, N);
const float kMaxPrefilterMip = 4.0; // 与 Cubemap::kPrefilterMips - 1 同步
vec3 prefilteredColor = textureLod(prefilteredMap, R,
roughness * kMaxPrefilterMip).rgb;
vec2 envBRDF = texture(brdfLut, vec2(NdotV, roughness)).rg;
vec3 specular = prefilteredColor * (F_amb * envBRDF.x + envBRDF.y);
vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb
* mix(0.4, 1.0, shade0); // 阴影区环境同步衰减(简化遮蔽)
// alpha = albedo 贴图覆盖率(glTF alphaMode=BLEND 混合源);
// 不透明资产贴图 a=1 行为不变。预乘在 tonemap 段完成。
outColor = vec4(ambient + Lo + emissive, albedoTex.a);
outVelocity = fragVelocity;
// 深度语义:真实深度测试 + 真实写入(depthWrite 状态保持 TRUE——
// 绕开 NVIDIA shader-object "depthWrite=FALSE 下绘制" 的灾难病态)。
// 早期版本在此写 gl_FragDepth=1.0 模拟"只测不写",但深度比较符是
// eLess:片元自身的 1.0 对清屏值 1.0 与不透明深度(更小)永远测试
// 失败,透明队列一个像素都出不来(Transparent 调试视图实锤)。
// 透明段是帧内最后一个 pass 且深度每帧清空,真实写入无副作用;
// back-to-front 排序下近面后画恒通过,混合顺序仍由排序保证。
}
+8 -2
View File
@@ -1,6 +1,7 @@
#version 450
// Tonemap pass:HDR(+bloom)→ 显示空间,兼调试视图路由上屏。
// Tonemap pass:HDR(+bloom)→ 显示空间,写 PresentTarget(offscreen,
// 由 ImGui::Image 呈现;输出位置可自由摆放)。
// 这是显示变换的显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。
// ACES = Narkowicz 拟合(线性域映射到 [0,1]),可经 push constant 关闭
// 退回纯 gamma(旧观感)。
@@ -40,6 +41,7 @@ const int VIEW_ENV_CUBE = 9;
const int VIEW_IRRADIANCE = 10;
const int VIEW_PREFILTER = 11;
const int VIEW_BRDF_LUT = 12;
const int VIEW_TRANSPARENT = 13;
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)
layout(push_constant) uniform PushConstants {
@@ -90,7 +92,11 @@ void main() {
outColor = vec4(pow(d, vec3(1.0 / 2.2)), 1.0);
return;
}
case VIEW_RAW_HDR: outColor = vec4(tonemap(texture(rawHdrImage, fragUV).rgb), 1.0); return;
// Transparent 视图:场景侧已跳过天空盒/不透明绘制,
// raw HDR = 清屏色 + 透明队列内容(与 RAW_HDR 同一显示路径)
case VIEW_RAW_HDR:
case VIEW_TRANSPARENT:
outColor = vec4(tonemap(texture(rawHdrImage, fragUV).rgb), 1.0); return;
case VIEW_TAA: outColor = vec4(tonemap(texture(taaImage, fragUV).rgb), 1.0); return;
case VIEW_BLOOM: outColor = vec4(tonemap(texture(bloomImage, fragUV).rgb * 4.0), 1.0); return;
case VIEW_VELOCITY: {