From 20f0035baa46b56c86722b7e1006e0a1a43dc210 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Wed, 30 Sep 2026 00:24:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ImGui=20docking=20Editor=20+=20?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E9=93=BE/=E5=B8=A7=E5=90=8C=E6=AD=A5/?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E8=AF=AD=E4=B9=89=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 作用域(重名不冲突、改名不丢选中) --- App/CMakeLists.txt | 9 +- App/editor/Editor.cpp | 202 ++++++++ App/editor/Editor.h | 89 ++++ App/editor/EditorContext.h | 48 ++ App/editor/HierarchyPanel.cpp | 86 ++++ App/editor/HierarchyPanel.h | 20 + App/editor/InspectorPanel.cpp | 220 ++++++++ App/editor/InspectorPanel.h | 22 + App/editor/StatsPanel.cpp | 173 +++++++ App/editor/StatsPanel.h | 19 + App/editor/ViewportPanel.cpp | 53 ++ App/editor/ViewportPanel.h | 26 + App/main.cpp | 474 +++++------------- Engine/include/Engine/Core/Application.h | 2 +- Engine/include/Engine/Core/Window.h | 13 + Engine/include/Engine/ECS/Components.h | 33 ++ Engine/include/Engine/ECS/Scene.h | 2 +- Engine/include/Engine/ImGui/ImGuiLayer.h | 14 + .../include/Engine/RenderGraph/RenderGraph.h | 8 + Engine/include/Engine/Renderer/Camera.h | 7 +- Engine/include/Engine/Renderer/Material.h | 27 +- Engine/include/Engine/Renderer/Mesh.h | 10 + Engine/include/Engine/Renderer/Model.h | 87 +++- .../include/Engine/Renderer/Passes/BlitPass.h | 72 +++ .../Engine/Renderer/Passes/ForwardPass.h | 6 + .../Engine/Renderer/Passes/FullscreenGpu.h | 1 + .../Engine/Renderer/Passes/ImGuiPass.h | 14 +- .../include/Engine/Renderer/Passes/SceneGpu.h | 30 +- .../Engine/Renderer/Passes/TonemapPass.h | 10 +- Engine/include/Engine/Renderer/Renderer.h | 46 +- .../include/Engine/Renderer/SceneRenderer.h | 24 +- Engine/include/Engine/Renderer/SceneTypes.h | 58 ++- Engine/include/Engine/Renderer/Vertex.h | 22 +- Engine/include/Engine/Vulkan/Buffer.h | 4 +- Engine/include/Engine/Vulkan/Device.h | 4 + Engine/src/Core/Application.cpp | 18 +- Engine/src/Core/Window.cpp | 58 ++- Engine/src/ECS/Scene.cpp | 4 +- Engine/src/ImGui/ImGuiLayer.cpp | 25 + Engine/src/RenderGraph/RenderGraph.cpp | 23 +- Engine/src/Renderer/Camera.cpp | 21 +- Engine/src/Renderer/Model.cpp | 331 ++++++++++-- Engine/src/Renderer/Passes/ForwardPass.cpp | 321 ++++++++---- Engine/src/Renderer/Passes/FullscreenGpu.cpp | 26 + Engine/src/Renderer/Passes/ImGuiPass.cpp | 26 +- Engine/src/Renderer/Passes/SceneGpu.cpp | 96 +++- Engine/src/Renderer/Passes/SkyboxPass.cpp | 6 +- Engine/src/Renderer/Passes/TonemapPass.cpp | 8 +- Engine/src/Renderer/Renderer.cpp | 155 +++++- Engine/src/Renderer/SceneRenderer.cpp | 287 ++++++++++- Engine/src/Vulkan/Device.cpp | 20 +- Engine/src/Vulkan/Swapchain.cpp | 5 +- shaders/shadow.vert | 26 +- shaders/simple_shader.frag | 18 +- shaders/simple_shader.vert | 32 +- shaders/simple_shader_translucent.frag | 238 +++++++++ shaders/tonemap.frag | 10 +- 57 files changed, 3067 insertions(+), 622 deletions(-) create mode 100644 App/editor/Editor.cpp create mode 100644 App/editor/Editor.h create mode 100644 App/editor/EditorContext.h create mode 100644 App/editor/HierarchyPanel.cpp create mode 100644 App/editor/HierarchyPanel.h create mode 100644 App/editor/InspectorPanel.cpp create mode 100644 App/editor/InspectorPanel.h create mode 100644 App/editor/StatsPanel.cpp create mode 100644 App/editor/StatsPanel.h create mode 100644 App/editor/ViewportPanel.cpp create mode 100644 App/editor/ViewportPanel.h create mode 100644 Engine/include/Engine/Renderer/Passes/BlitPass.h create mode 100644 shaders/simple_shader_translucent.frag diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 92d2a9c..efa036b 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -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) diff --git a/App/editor/Editor.cpp b/App/editor/Editor.cpp new file mode 100644 index 0000000..8218d84 --- /dev/null +++ b/App/editor/Editor.cpp @@ -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 +#include // DockBuilder*(内部 API) + +#include + +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(); + m_Inspector = std::make_unique(); + m_Viewport = std::make_unique(); + m_Stats = std::make_unique(); +} + +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(); + } +} diff --git a/App/editor/Editor.h b/App/editor/Editor.h new file mode 100644 index 0000000..e032091 --- /dev/null +++ b/App/editor/Editor.h @@ -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 + +#include + +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 m_Hierarchy; + std::unique_ptr m_Inspector; + std::unique_ptr m_Viewport; + std::unique_ptr 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 diff --git a/App/editor/EditorContext.h b/App/editor/EditorContext.h new file mode 100644 index 0000000..4c6f066 --- /dev/null +++ b/App/editor/EditorContext.h @@ -0,0 +1,48 @@ +// +// EditorContext:Editor 层与 SandboxApp 之间的桥接上下文。 +// +// Editor 是 App 层代码(不属于引擎),它需要的引擎门面与沙盒能力 +// 全部经此聚合注入——面板只认识 EditorContext,不反向依赖 main.cpp。 +// + +#ifndef HELLOVULKAN_EDITORCONTEXT_H +#define HELLOVULKAN_EDITORCONTEXT_H + +#include +#include +#include +#include +#include // kDisplayViewport / kDisplayDirect + +#include + +// 可切换环境(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 getEnvIndex; // 当前环境表下标 + std::function setEnvIndex; + + // 冻结动画时钟(沙盒 spinners 用;保留原 Scene Debug 行为) + bool *pauseAnim = nullptr; +}; + +#endif //HELLOVULKAN_EDITORCONTEXT_H diff --git a/App/editor/HierarchyPanel.cpp b/App/editor/HierarchyPanel.cpp new file mode 100644 index 0000000..c4aae6c --- /dev/null +++ b/App/editor/HierarchyPanel.cpp @@ -0,0 +1,86 @@ +// +// HierarchyPanel.cpp:实体列表实现。 +// + +#include "HierarchyPanel.h" +#include "Engine/ECS/Components.h" +#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle + +#include + +using namespace Engine; + +namespace { + struct ScopedID { + explicit ScopedID(const u64 uuid) { + ImGui::PushID(reinterpret_cast(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().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(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(e)) badge += "[M]"; + if (reg.all_of(e)) badge += "[A]"; + if (reg.all_of(e)) badge += "[C]"; + if (reg.all_of(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(); +} diff --git a/App/editor/HierarchyPanel.h b/App/editor/HierarchyPanel.h new file mode 100644 index 0000000..c38de23 --- /dev/null +++ b/App/editor/HierarchyPanel.h @@ -0,0 +1,20 @@ +// +// HierarchyPanel:实体列表面板。 +// +// 点击选中(写 Editor 的选中状态)、工具栏创建实体、右键删除 +//(延迟销毁,帧尾 flush,遍历安全)。每项尾缀显示组件构成徽标。 +// + +#ifndef HELLOVULKAN_HIERARCHYPANEL_H +#define HELLOVULKAN_HIERARCHYPANEL_H + +#include "EditorContext.h" + +#include + +class HierarchyPanel { +public: + void Draw(const EditorContext& ctx, entt::entity& selected); +}; + +#endif //HELLOVULKAN_HIERARCHYPANEL_H diff --git a/App/editor/InspectorPanel.cpp b/App/editor/InspectorPanel.cpp new file mode 100644 index 0000000..af23193 --- /dev/null +++ b/App/editor/InspectorPanel.cpp @@ -0,0 +1,220 @@ +// +// InspectorPanel.cpp:组件编辑实现。 +// +// Transform 编辑必须走 Set* 入口(标脏 → 帧循环统一 Compose 重算 +// world),直接改私有成员是编译错误——面板天然遵守引擎约束。 +// + +#include "InspectorPanel.h" +#include "Engine/ECS/Components.h" +#include "Engine/Renderer/Model.h" + +#include + +#include +#include + +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(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(selected) && + ImGui::MenuItem("Light")) + { + reg.emplace(selected); + } + if (!reg.all_of(selected) && + ImGui::MenuItem("Camera")) + { + reg.emplace(selected); + } + if (!reg.all_of(selected) && + ImGui::MenuItem("Animation")) + { + reg.emplace(selected); + } + if (!reg.all_of(selected) && + ImGui::MenuItem("Name")) + { + reg.emplace(selected); + } + ImGui::EndPopup(); + } + ImGui::Separator(); + + // ---- Transform ---- + if (auto* t = reg.try_get(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(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(clips.size()), + static_cast(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(selected)) + { + if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen)) + { + int type = static_cast(light->type); + if (ImGui::Combo("type", &type, "Directional\0Point\0")) + { + light->type = static_cast(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(selected)) + { + if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) + { + if (ImGui::Checkbox("primary", &cam->primary) && cam->primary) + { + // primary 互斥:启用本相机时清掉其余实体的 primary + //(引擎取第一个 primary 作为渲染视角) + reg.view().each( + [&](entt::entity e, Engine::ECS::CameraComponent& c) + { + if (e != selected) + { + c.primary = false; + } + }); + } + } + } + + // ---- Animation ---- + if (auto* anim = reg.try_get(selected)) + { + if (ImGui::CollapsingHeader("Animation", + ImGuiTreeNodeFlags_DefaultOpen)) + { + const auto* mr = reg.try_get(selected); + const Engine::Model* model = + mr ? ctx.assets->GetModel(mr->model) : nullptr; + + if (model != nullptr && model->HasAnimations()) + { + const auto& clips = model->GetClips(); + std::vector 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(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(clips.size())) + { + ImGui::Text("t: %.2f / %.2f s", anim->time, + clips[anim->clipIndex].duration); + } + } + else + { + ImGui::TextDisabled("(no animation clips on model)"); + } + } + } + + ImGui::End(); +} diff --git a/App/editor/InspectorPanel.h b/App/editor/InspectorPanel.h new file mode 100644 index 0000000..2b1fa98 --- /dev/null +++ b/App/editor/InspectorPanel.h @@ -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 + +class InspectorPanel { +public: + void Draw(const EditorContext& ctx, entt::entity selected); +}; + +#endif //HELLOVULKAN_INSPECTORPANEL_H diff --git a/App/editor/StatsPanel.cpp b/App/editor/StatsPanel.cpp new file mode 100644 index 0000000..fc67e43 --- /dev/null +++ b/App/editor/StatsPanel.cpp @@ -0,0 +1,173 @@ +// +// StatsPanel.cpp:遥测与全局设置实现(原 main.cpp Scene Debug 搬迁)。 +// + +#include "StatsPanel.h" +#include "Engine/ECS/Components.h" + +#include +#include + +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(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(app.GetFrameCount())); + } + + ImGui::End(); +} diff --git a/App/editor/StatsPanel.h b/App/editor/StatsPanel.h new file mode 100644 index 0000000..79fdc40 --- /dev/null +++ b/App/editor/StatsPanel.h @@ -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 diff --git a/App/editor/ViewportPanel.cpp b/App/editor/ViewportPanel.cpp new file mode 100644 index 0000000..19a5d6c --- /dev/null +++ b/App/editor/ViewportPanel.cpp @@ -0,0 +1,53 @@ +// +// ViewportPanel.cpp:显示模式单选 + ImGui::Image + 分辨率上报。 +// 逻辑照搬原 main.cpp 的 Viewport 窗口(行为不变)。 +// + +#include "ViewportPanel.h" + +#include + +#include // 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(avail.x * fb.x + 0.5f); + const u32 wantH = static_cast(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(); +} diff --git a/App/editor/ViewportPanel.h b/App/editor/ViewportPanel.h new file mode 100644 index 0000000..b366fa1 --- /dev/null +++ b/App/editor/ViewportPanel.h @@ -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 diff --git a/App/main.cpp b/App/main.cpp index 3ac5503..337311e 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -3,12 +3,18 @@ // // SandboxApp:第一个基于 Application 框架的程序。 // -// main 的全部职责:定义一个 Application 派生类并运行它。 -// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理; -// 这里只写"程序逻辑":创建实体、逐帧更新组件、事件响应。 +// main 的职责边界: +// - 定义 Application 派生类并运行它 +// - "程序逻辑":场景搭建(实体/组件)、逐帧更新、事件响应、 +// 异步环境加载 +// - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现 +// 任何 ImGui 控件) +// +// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理。 // #include +#include // SetInputCapture(Application.h 只前置声明) #include #include #include @@ -17,37 +23,20 @@ #include #include #include -#include - -#include +#include #include -#include -#include #include -#include -#include +#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,69 +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{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{gun}); + m_Gun.AddComponent(); + auto& gt = m_Gun.GetComponent(); + 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{gun}); + m_Gun.AddComponent(); + auto& gt = m_Gun.GetComponent(); + 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{model}); - m.GetComponent().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{model}); + m.GetComponent().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); - - // 透明管线验证:两片叠放的半透明平面(前后各一,看混合与排序) - const auto glass1 = GetAssets().CreateTransparentPlane( - 2.0f, 80, 180, 255, 110); - const auto glass2 = GetAssets().CreateTransparentPlane( - 2.0f, 255, 120, 80, 110); - auto g1 = scene.CreateEntity("GlassNear"); - g1.AddComponent(MeshRendererComponent{glass1}); - g1.GetComponent() - .SetPosition(glm::vec3(0.0f, 0.5f, -1.0f)); - auto g2 = scene.CreateEntity("GlassFar"); - g2.AddComponent(MeshRendererComponent{glass2}); - g2.GetComponent() - .SetPosition(glm::vec3(0.0f, 1.0f, -3.0f)); - m_Ground = scene.CreateEntity("Ground"); m_Ground.GetComponent() .SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose m_Ground.AddComponent(MeshRendererComponent{ground}); + */ // 相机实体(primary = 当前渲染视角;位姿存实体 Transform) m_Camera = scene.CreateEntity("MainCamera"); auto& camera = m_Camera.AddComponent(); camera.primary = true; m_Camera.GetComponent() - .SetPosition(glm::vec3(0.0f, 0.0f, 7.0f)); + .SetPosition(glm::vec3(0.0f, 12.0f, 22.0f)); + m_Camera.GetComponent() + .SetRotation(glm::vec3(-25.0f, 0.0f, 0.0f)); // 灯光实体(第一个方向光 = 阴影光源) m_KeyLight = scene.CreateEntity("KeyLight"); @@ -147,6 +145,7 @@ namespace .castShadow = true, }); + /* scene.CreateEntity("FillLight") .AddComponent( LightComponent{ @@ -154,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().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f)); @@ -173,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{ref}); - temple.GetComponent() - .SetScale(glm::vec3{m_TempleScale}); - ENGINE_INFO("[ASYNC] SunTemple ready, entity created"); - } - } - - // 异步环境加载:每帧收割完成的解码结果 - PollEnvironment(); - // 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff // 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到) if (!m_PauseAnim) @@ -216,268 +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()) - { - 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); - } - ImGui::SameLine(); - bool lod = renderer.GetLodEnabled(); - if (ImGui::Checkbox("LOD", &lod)) - { - renderer.SetLodEnabled(lod); // 关 = 恒绘原网格 - } - } - - // ---- 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(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(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 @@ -498,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 m_Editor = std::make_unique(); + EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空 - EntityHandle m_Ground{}; EntityHandle m_KeyLight{}; + EntityHandle m_Ground{}; + EntityHandle m_Gun{}; // 骨骼动画验证实体(M1911) std::vector 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 m_EnvLoadDone{false}; - bool m_EnvLoadBusy = false; - int m_EnvPending = -1; - // ---- 异步模型加载(SunTemple)---- AsyncTask m_TempleTask; f32 m_TempleScale = 1.0f; // cm 尺度资产 → 米(解析后应用) diff --git a/Engine/include/Engine/Core/Application.h b/Engine/include/Engine/Core/Application.h index 65f3b04..01f877e 100644 --- a/Engine/include/Engine/Core/Application.h +++ b/Engine/include/Engine/Core/Application.h @@ -88,7 +88,7 @@ namespace Engine { // 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。 // 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。 - virtual void OnRenderOverlay() {} + virtual void OnImGuiRender() {} // 主循环结束后、析构前(保存、清理场景等) virtual void OnShutdown() {} diff --git a/Engine/include/Engine/Core/Window.h b/Engine/include/Engine/Core/Window.h index f71612e..b02ef94 100644 --- a/Engine/include/Engine/Core/Window.h +++ b/Engine/include/Engine/Core/Window.h @@ -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); diff --git a/Engine/include/Engine/ECS/Components.h b/Engine/include/Engine/ECS/Components.h index 93cedb0..793b349 100644 --- a/Engine/include/Engine/ECS/Components.h +++ b/Engine/include/Engine/ECS/Components.h @@ -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; diff --git a/Engine/include/Engine/ECS/Scene.h b/Engine/include/Engine/ECS/Scene.h index 7425b41..a20b6ef 100644 --- a/Engine/include/Engine/ECS/Scene.h +++ b/Engine/include/Engine/ECS/Scene.h @@ -50,7 +50,7 @@ namespace Engine::ECS { // ---- 实体 ---- // 返回句柄表皮:e.AddComponent() / e.GetComponent() / e.Destroy() - EntityHandle CreateEntity(std::string name = {}); + EntityHandle CreateEntity(std::string name = "new entity"); void DestroyEntity(Entity e); // 延迟:帧尾统一销毁 void FlushDestroyQueue(); // 每帧末尾由 Application 调用 diff --git a/Engine/include/Engine/ImGui/ImGuiLayer.h b/Engine/include/Engine/ImGui/ImGuiLayer.h index 7f87b11..2abc262 100644 --- a/Engine/include/Engine/ImGui/ImGuiLayer.h +++ b/Engine/include/Engine/ImGui/ImGuiLayer.h @@ -18,6 +18,8 @@ #pragma once #include +#include // kMaxFramesInFlight +#include #include 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 diff --git a/Engine/include/Engine/RenderGraph/RenderGraph.h b/Engine/include/Engine/RenderGraph/RenderGraph.h index 03ff4c3..94ef54b 100644 --- a/Engine/include/Engine/RenderGraph/RenderGraph.h +++ b/Engine/include/Engine/RenderGraph/RenderGraph.h @@ -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 管理的资源绑定到外部描述符(如阴影图采样)。 // 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。 diff --git a/Engine/include/Engine/Renderer/Camera.h b/Engine/include/Engine/Renderer/Camera.h index 8b343f3..a708797 100644 --- a/Engine/include/Engine/Renderer/Camera.h +++ b/Engine/include/Engine/Renderer/Camera.h @@ -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 diff --git a/Engine/include/Engine/Renderer/Material.h b/Engine/include/Engine/Renderer/Material.h index 88f632d..bf9c252 100644 --- a/Engine/include/Engine/Renderer/Material.h +++ b/Engine/include/Engine/Renderer/Material.h @@ -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,8 +55,9 @@ 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, diff --git a/Engine/include/Engine/Renderer/Mesh.h b/Engine/include/Engine/Renderer/Mesh.h index f04bbc1..4df3fcb 100644 --- a/Engine/include/Engine/Renderer/Mesh.h +++ b/Engine/include/Engine/Renderer/Mesh.h @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -50,6 +51,15 @@ namespace Engine // 全局连续槽位(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 { glm::vec3 center{0.0f}; diff --git a/Engine/include/Engine/Renderer/Model.h b/Engine/include/Engine/Renderer/Model.h index a6089d0..9b00cf7 100644 --- a/Engine/include/Engine/Renderer/Model.h +++ b/Engine/include/Engine/Renderer/Model.h @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include // 默认参数需完整类型(烘焙节点变换用) #include #include #include @@ -41,10 +43,52 @@ namespace Engine 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 positions; + std::vector rotations; + std::vector scales; + }; + + struct AnimationClip { + std::string name; + f32 duration = 0.0f; // 秒(已按 tps 换算) + f32 tps = 1.0f; // ticks per second(0 按 1,assimp 惯例) + std::vector 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 meshes; + std::vector nodes; + std::vector bones; + std::vector clips; bool Valid() const { return !meshes.empty(); } }; @@ -92,6 +136,13 @@ namespace Engine // 实体级粗剔除用——整个模型在视锥外直接跳过全部 mesh 循环 const Mesh::Bounds& GetBounds() const { return m_Bounds; } + // ---- 骨骼动画运行时数据(无动画模型全部为空,零开销)---- + bool HasAnimations() const { return !m_Clips.empty(); } + u32 GetBoneCount() const { return static_cast(m_Bones.size()); } + const std::vector& GetClips() const { return m_Clips; } + const std::vector& GetNodes() const { return m_Nodes; } + const std::vector& GetBones() const { return m_Bones; } + private: Model(Device& device, MemoryAllocator& allocator, DescriptorManager& descriptorManager, @@ -105,13 +156,33 @@ 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& out); + // 每帧动画的 channel 轨道提取(nodeName → node 表下标) + static void ParseAnimations(const aiScene* scene, + const std::vector& nodes, + std::vector& out); static void ProcessNode(const aiNode* node, const aiScene* scene, std::vector& out, const std::filesystem::path& modelDir, - bool isGltf); + bool isGltf, + std::vector& bones, + std::unordered_map& 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& bones, + std::unordered_map& boneSlots, + const aiMatrix4x4& globalTransform + = aiMatrix4x4()); + // mesh 的 aiBone 权重灌进顶点 + 模型级骨骼表注册去重 + static void CollectMeshBones(const aiMesh* mesh, + std::vector& vertices, + std::vector& bones, + std::unordered_map& boneSlots); void InitDefaultTextures(); // 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除) @@ -121,10 +192,13 @@ namespace Engine // 材质缓存入口:同贴图组合+同因子+同透明语义共享一份 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, - bool transparent = false); + bool transparent = false, + const std::array& + slotOverrides = {}); Device& m_Device; MemoryAllocator& m_Allocator; @@ -142,6 +216,11 @@ namespace Engine Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除) std::string m_ModelPath; bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV + + // ---- 骨骼动画运行时数据(FromParsed/LoadModel 从解析产物 move)---- + std::vector m_Nodes; + std::vector m_Bones; + std::vector m_Clips; }; } // namespace Engine diff --git a/Engine/include/Engine/Renderer/Passes/BlitPass.h b/Engine/include/Engine/Renderer/Passes/BlitPass.h new file mode 100644 index 0000000..16ca223 --- /dev/null +++ b/Engine/include/Engine/Renderer/Passes/BlitPass.h @@ -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 +#include +#include + +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 srcSpan = { + vk::Offset3D{0, 0, 0}, + vk::Offset3D{static_cast(srcExt.width), + static_cast(srcExt.height), 1}}; + const std::array dstSpan = { + vk::Offset3D{0, 0, 0}, + vk::Offset3D{static_cast(dstExt.width), + static_cast(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 diff --git a/Engine/include/Engine/Renderer/Passes/ForwardPass.h b/Engine/include/Engine/Renderer/Passes/ForwardPass.h index d733b1d..c3a2443 100644 --- a/Engine/include/Engine/Renderer/Passes/ForwardPass.h +++ b/Engine/include/Engine/Renderer/Passes/ForwardPass.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; diff --git a/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h b/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h index 9b2ad7f..e784d74 100644 --- a/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h +++ b/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h @@ -56,6 +56,7 @@ namespace Engine { kDebugIrradiance, // IBL 漫反射辐照度(32² 低频) kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级) kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道) + kDebugTransparent, // 仅透明队列(Skybox/Forward 跳过绘制,看透明段内容) kDebugViewCount }; diff --git a/Engine/include/Engine/Renderer/Passes/ImGuiPass.h b/Engine/include/Engine/Renderer/Passes/ImGuiPass.h index f27e2d1..3170fb0 100644 --- a/Engine/include/Engine/Renderer/Passes/ImGuiPass.h +++ b/Engine/include/Engine/Renderer/Passes/ImGuiPass.h @@ -11,6 +11,7 @@ #define HELLOVULKAN_IMGUIPASS_H #include +#include // 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 diff --git a/Engine/include/Engine/Renderer/Passes/SceneGpu.h b/Engine/include/Engine/Renderer/Passes/SceneGpu.h index 8f61f3b..cd7732d 100644 --- a/Engine/include/Engine/Renderer/Passes/SceneGpu.h +++ b/Engine/include/Engine/Renderer/Passes/SceneGpu.h @@ -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, kMaxFramesInFlight> indirectCmdBuffers; + std::array, 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; }; diff --git a/Engine/include/Engine/Renderer/Passes/TonemapPass.h b/Engine/include/Engine/Renderer/Passes/TonemapPass.h index 574dcc1..52e91a7 100644 --- a/Engine/include/Engine/Renderer/Passes/TonemapPass.h +++ b/Engine/include/Engine/Renderer/Passes/TonemapPass.h @@ -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 前) diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index e887173..7ca4e49 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -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; @@ -115,13 +126,32 @@ namespace Engine { 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; @@ -133,6 +163,8 @@ namespace Engine { Scope m_UniformBuffer; // per-slot UBO ring Scope m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO) Scope m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity) + Scope m_SkinOffsetBuffer; // 实例→palette 基址 ring(SSBO,与实例序平行) + Scope m_SkinPaletteBuffer; // 骨骼蒙皮矩阵 palette ring(SSBO,帧首 bump) Scope m_SceneGpu; // 场景类 pass 共享装配 Scope m_FullscreenGpu; // 全屏 pass 共享装配 // 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造 @@ -152,6 +184,7 @@ namespace Engine { // ---- RenderGraph 资源句柄 ---- RG::TextureHandle m_Backbuffer{}; + RG::TextureHandle m_PresentTarget{}; // tonemap 输出(offscreen,ImGui 采样) RG::TextureHandle m_Depth{}; std::array m_ShadowMaps{}; // CSM 级联 RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一 @@ -180,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 diff --git a/Engine/include/Engine/Renderer/SceneRenderer.h b/Engine/include/Engine/Renderer/SceneRenderer.h index 2201d37..eea0677 100644 --- a/Engine/include/Engine/Renderer/SceneRenderer.h +++ b/Engine/include/Engine/Renderer/SceneRenderer.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -36,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; } @@ -59,6 +61,7 @@ namespace Engine { 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 段,每段独立拟合光源正交投影 @@ -66,6 +69,12 @@ 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 后保留容量)---- @@ -74,6 +83,7 @@ namespace Engine { struct InstanceMatrices { glm::mat4 cur; glm::mat4 prev; + u32 skinBase = 0; // palette 区段基址(0 = identity 槽) }; CullResult result; // 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 = @@ -108,6 +118,17 @@ namespace Engine { std::unordered_map m_PrevWorlds; std::unordered_map m_CurWorlds; + // ---- 骨骼动画 scratch(帧首重建/容量复用)---- + // entKey → palette 区段基址(第 2.5 节预采样填,收集段查) + std::unordered_map m_EntitySkinBase; + // 采样层 A(当前 clip)/ B(fade 来源 clip)与 global 累乘 + std::vector m_PosePosA, m_PosePosB; + std::vector m_PoseRotA, m_PoseRotB; + std::vector m_PoseSclA, m_PoseSclB; + std::vector m_TouchedA, m_TouchedB; + std::vector m_Globals; + bool m_SkinTruncWarned = false; // palette 预算溢出只告警一次 + bool m_TruncationWarned = false; // 实例缓冲打满只告警一次 // ---- 运行时开关/调试视图状态(本类是唯一写入者)---- @@ -116,6 +137,7 @@ namespace Engine { bool m_BloomEnabled = true; bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过) int m_DebugView = 0; + int m_DisplayMode = kDisplayViewport; // Editor 视口为默认形态 }; } // namespace Engine diff --git a/Engine/include/Engine/Renderer/SceneTypes.h b/Engine/include/Engine/Renderer/SceneTypes.h index 4b2c31d..88be9e8 100644 --- a/Engine/include/Engine/Renderer/SceneTypes.h +++ b/Engine/include/Engine/Renderer/SceneTypes.h @@ -18,6 +18,7 @@ namespace Engine { class Mesh; + class Material; // GPUDriven:按 (mesh, LOD) 聚合的绘制批——同 mesh 不同 LOD 的实例 // 索引区间不同,不能并成一次 draw @@ -28,10 +29,38 @@ namespace Engine { 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 batches; u32 instanceCount = 0; + std::vector indirectCommands; + std::vector indirectGroups; }; constexpr u32 kMaxLights = 4; @@ -43,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 { @@ -70,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 { @@ -87,6 +131,15 @@ namespace Engine { // 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一 // 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵 std::vector prevInstanceModels; + + // ---- 蒙皮(与 instanceModels 平行的 per-instance 间接索引)---- + // skinOffsets[i] = 第 i 个实例的 palette 区段起点(kMaxSkinMatrices + // 全局数组内下标);无动画实体指向 slot0 identity + std::vector skinOffsets; + // palette 区段池:帧首 bump allocator 按动画实体分配 + // [skinBase .. skinBase+boneCount) = 该实体全部骨骼的蒙皮矩阵 + std::vector skinPalettes; + CullResult mainView; // 相机视锥 → Forward pass // 每级联独立剔除结果(Shadow pass 按 region 各画各的—— // 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉) @@ -102,10 +155,13 @@ namespace Engine { 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 耗时(遥测) }; diff --git a/Engine/include/Engine/Renderer/Vertex.h b/Engine/include/Engine/Renderer/Vertex.h index 03ed1fd..8670767 100644 --- a/Engine/include/Engine/Renderer/Vertex.h +++ b/Engine/include/Engine/Renderer/Vertex.h @@ -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 GetAttributeDescriptions() { - std::array attrs; + static std::array GetAttributeDescriptions() { + std::array 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; } }; diff --git a/Engine/include/Engine/Vulkan/Buffer.h b/Engine/include/Engine/Vulkan/Buffer.h index 24ef335..35c8384 100644 --- a/Engine/include/Engine/Vulkan/Buffer.h +++ b/Engine/include/Engine/Vulkan/Buffer.h @@ -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; diff --git a/Engine/include/Engine/Vulkan/Device.h b/Engine/include/Engine/Vulkan/Device.h index be40a61..81bb415 100644 --- a/Engine/include/Engine/Vulkan/Device.h +++ b/Engine/include/Engine/Vulkan/Device.h @@ -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 m_ValidationLayers = { "VK_LAYER_KHRONOS_validation" }; std::vector m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; diff --git a/Engine/src/Core/Application.cpp b/Engine/src/Core/Application.cpp index 83d5717..196a44a 100644 --- a/Engine/src/Core/Application.cpp +++ b/Engine/src/Core/Application.cpp @@ -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(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( 1e6 / m_TargetFps)); diff --git a/Engine/src/Core/Window.cpp b/Engine/src/Core/Window.cpp index 8001e56..626ac7c 100644 --- a/Engine/src/Core/Window.cpp +++ b/Engine/src/Core/Window.cpp @@ -81,12 +81,19 @@ namespace Engine void Window::WindowFocusCallback(GLFWwindow* window, int focused) { - const auto* self = static_cast(glfwGetWindowUserPointer(window)); + auto* self = static_cast(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(mx - lastX); - dy = static_cast(my - lastY); + dx = static_cast(mx - m_LastMouseX); + dy = static_cast(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; } diff --git a/Engine/src/ECS/Scene.cpp b/Engine/src/ECS/Scene.cpp index 42ebed0..2940715 100644 --- a/Engine/src/ECS/Scene.cpp +++ b/Engine/src/ECS/Scene.cpp @@ -16,9 +16,7 @@ namespace Engine::ECS { // 出生即有:UUID(稳定身份)+ Transform(空间存在性,Unity 语义) m_Registry.emplace(e, IdComponent{UUID::New()}); m_Registry.emplace(e); - if (!name.empty()) { - m_Registry.emplace(e, NameComponent{std::move(name)}); - } + m_Registry.emplace(e, NameComponent{std::move(name)}); return EntityHandle{this, e}; } diff --git a/Engine/src/ImGui/ImGuiLayer.cpp b/Engine/src/ImGui/ImGuiLayer.cpp index b9e91e9..62beb84 100644 --- a/Engine/src/ImGui/ImGuiLayer.cpp +++ b/Engine/src/ImGui/ImGuiLayer.cpp @@ -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(); diff --git a/Engine/src/RenderGraph/RenderGraph.cpp b/Engine/src/RenderGraph/RenderGraph.cpp index 1a8ce31..6baeb8a 100644 --- a/Engine/src/RenderGraph/RenderGraph.cpp +++ b/Engine/src/RenderGraph/RenderGraph.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -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(m_Textures.size()), 1}; + const TextureHandle h{static_cast(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); diff --git a/Engine/src/Renderer/Camera.cpp b/Engine/src/Renderer/Camera.cpp index 621e823..747bee3 100644 --- a/Engine/src/Renderer/Camera.cpp +++ b/Engine/src/Renderer/Camera.cpp @@ -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 相机) diff --git a/Engine/src/Renderer/Model.cpp b/Engine/src/Renderer/Model.cpp index 28c2bde..85c2c2f 100644 --- a/Engine/src/Renderer/Model.cpp +++ b/Engine/src/Renderer/Model.cpp @@ -22,6 +22,19 @@ 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, @@ -76,7 +89,10 @@ 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, @@ -89,12 +105,18 @@ Model::Model(Device& device, MemoryAllocator& allocator, , m_DescriptorManager(descriptorManager) , m_TextureLayout(textureLayout) { - // 纯色贴图(albedo 兜底;alpha 非全 1 时为半透明材质) + // 纯色贴图直供 albedo 槽(alpha 非全 1 时为半透明材质); + // 生命周期挂进纹理缓存(键含 rgba 值,同色共享、异色互不冲突) InitDefaultTextures(); - auto tex = (rgba[3] == 255) + Scope 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; @@ -107,9 +129,13 @@ Model::Model(Device& device, MemoryAllocator& allocator, const std::vector indices = {0, 1, 2, 0, 2, 3}; const std::vector> lodIndices = {indices}; - // 材质/纹理生命周期随模型(缓存持有) + // 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo + // 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训) std::string paths[Material::kSlotCount] = {"", "", "", ""}; - Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, transparent); + std::array overrides{}; + overrides[Material::Albedo] = albedoOverride; + Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, transparent, + overrides); m_Meshes.push_back(CreateScope( m_Device, m_Allocator, vertices, lodIndices, mat)); @@ -164,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 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 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& out) +{ + const u32 idx = static_cast(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(idx), out); + } + return idx; +} + +void Model::ParseAnimations(const aiScene* scene, + const std::vector& nodes, + std::vector& out) +{ + if (scene->mNumAnimations == 0) return; + + // nodeName → node 表下标(channel 的目标必须能在层级里累乘) + std::unordered_map 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(anim->mTicksPerSecond) : 1.0f; + clip.duration = static_cast(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(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(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(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& vertices, + std::vector& bones, + std::unordered_map& 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(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::FromParsed(Device& device, MemoryAllocator& allocator, DescriptorManager& descriptorManager, vk::DescriptorSetLayout textureLayout, @@ -192,6 +375,9 @@ Scope 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) { @@ -214,6 +400,9 @@ 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( @@ -226,14 +415,24 @@ void Model::LoadModel(const std::string& path) void Model::ProcessNode(const aiNode* node, const aiScene* scene, std::vector& out, - const std::filesystem::path& modelDir, bool isGltf) + const std::filesystem::path& modelDir, bool isGltf, + std::vector& bones, + std::unordered_map& 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); } } @@ -264,21 +463,37 @@ Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb) Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount], const MaterialParams& params, - bool transparent) + bool transparent, + const std::array& + slotOverrides) { - const std::string key = Material::MakeCacheKey(paths, params, transparent); + // 程序化覆盖纹理以指针地址入键:同模型内不同颜色的覆盖组合 + //(如两块玻璃板)各得一份材质,不与纯路径键的材质串缓存 + 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(t)) + : ":0"; + } + } + if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) { return it->second.get(); } - // 每槽位解析纹理:空/缺失折到该槽位默认纹理 + // 每槽位解析纹理:程序化覆盖优先,路径次之,空/缺失折到默认纹理 std::array 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(); @@ -294,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& bones, + std::unordered_map& boneSlots, + const aiMatrix4x4& globalTransform) { ParsedMesh out; std::vector& vertices = out.vertices; std::vector 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]) { @@ -328,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) { @@ -355,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 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}, @@ -385,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() ? "" : texPaths[Material::Albedo], @@ -393,6 +637,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene, : texPaths[Material::Normal], texPaths[Material::MetallicRoughness].empty() ? "" : texPaths[Material::MetallicRoughness], + texPaths[Material::Roughness].empty() ? "" + : texPaths[Material::Roughness], texPaths[Material::Emissive].empty() ? "" : texPaths[Material::Emissive], texPaths[Material::Ao].empty() ? "" @@ -419,15 +665,24 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene, 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(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); } } diff --git a/Engine/src/Renderer/Passes/ForwardPass.cpp b/Engine/src/Renderer/Passes/ForwardPass.cpp index 0a4feb2..1ff7411 100644 --- a/Engine/src/Renderer/Passes/ForwardPass.cpp +++ b/Engine/src/Renderer/Passes/ForwardPass.cpp @@ -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 +#include #include #include #include @@ -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 stages = { + vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eFragment + }; + std::array 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(ctx.extent.width), + static_cast(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 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_FALSE, VK_FALSE}); + const vk::ColorBlendEquationEXT direct = MakeDirectEquation(); + cmd.setColorBlendEquationEXT(0, + std::array{direct, direct}); + cmd.setColorWriteMaskEXT(0, + std::array{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{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{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,124 +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 stages = { - vk::ShaderStageFlagBits::eVertex, - vk::ShaderStageFlagBits::eFragment - }; - std::array 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(ctx.extent.width), - static_cast(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{blendEnable, blendEnable}); + std::array{VK_TRUE, VK_FALSE}); cmd.setColorBlendEquationEXT(0, - std::array{blendEquation, blendEquation}); + std::array{premul, direct}); cmd.setColorWriteMaskEXT(0, - std::array{colorWriteMask, colorWriteMask}); + std::array{kColorWriteMask, kColorWriteMask}); - // ---- 不透明批次:无混合直写,深度测试 + 写 ---- - 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.lod); } - // ---- 透明批次:预乘 alpha 混合(线性空间 over),深度只测 - // 不写,远 → 近排序(SceneRenderer 侧已按 batch 平均距离排)。 - // 附件 0(HDR)混合、附件 1(velocity)直写——透明表面的 - // velocity 就是它自身的运动,覆盖自己的像素即正确。 - // alpha 语义:frag 输出未预乘线性色 + 覆盖率 alpha, - // 预乘在 tonemap 段(rgb * a)完成 —— hard constraint。 - if (!m_Frame.translucentView.batches.empty()) { - vk::ColorBlendEquationEXT premul; - premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) - .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha) - .setColorBlendOp(vk::BlendOp::eAdd) - .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) - .setDstAlphaBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha) - .setAlphaBlendOp(vk::BlendOp::eAdd); - - cmd.setColorBlendEnableEXT(0, - std::array{VK_TRUE, VK_FALSE}); - cmd.setColorBlendEquationEXT(0, - std::array{premul, blendEquation}); - cmd.setDepthWriteEnableEXT(VK_FALSE); - - 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.lod); - } - - cmd.setDepthWriteEnableEXT(VK_TRUE); // 恢复(下帧 opaque 段也会重设) - } - 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) diff --git a/Engine/src/Renderer/Passes/FullscreenGpu.cpp b/Engine/src/Renderer/Passes/FullscreenGpu.cpp index 7b98bf1..53bc50b 100644 --- a/Engine/src/Renderer/Passes/FullscreenGpu.cpp +++ b/Engine/src/Renderer/Passes/FullscreenGpu.cpp @@ -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(ctx.extent.width), static_cast(ctx.extent.height), diff --git a/Engine/src/Renderer/Passes/ImGuiPass.cpp b/Engine/src/Renderer/Passes/ImGuiPass.cpp index a65912e..907f7bf 100644 --- a/Engine/src/Renderer/Passes/ImGuiPass.cpp +++ b/Engine/src/Renderer/Passes/ImGuiPass.cpp @@ -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{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 diff --git a/Engine/src/Renderer/Passes/SceneGpu.cpp b/Engine/src/Renderer/Passes/SceneGpu.cpp index 0409096..75c5317 100644 --- a/Engine/src/Renderer/Passes/SceneGpu.cpp +++ b/Engine/src/Renderer/Passes/SceneGpu.cpp @@ -4,8 +4,10 @@ #include #include +#include // IndirectGroup 取数(inline 经 Mesh) #include #include +#include #include #include #include @@ -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 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( + device, allocator, cmdBytes, + vk::BufferUsageFlagBits::eIndirectBuffer, memProps); + indirectCountBuffers[slot] = CreateScope( + 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 重编译变化),绑一次即可。 diff --git a/Engine/src/Renderer/Passes/SkyboxPass.cpp b/Engine/src/Renderer/Passes/SkyboxPass.cpp index 1c0ac87..4aa7d82 100644 --- a/Engine/src/Renderer/Passes/SkyboxPass.cpp +++ b/Engine/src/Renderer/Passes/SkyboxPass.cpp @@ -3,6 +3,7 @@ // #include +#include #include #include #include @@ -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{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 stages = { vk::ShaderStageFlagBits::eVertex, diff --git a/Engine/src/Renderer/Passes/TonemapPass.cpp b/Engine/src/Renderer/Passes/TonemapPass.cpp index ea8c62d..f54845b 100644 --- a/Engine/src/Renderer/Passes/TonemapPass.cpp +++ b/Engine/src/Renderer/Passes/TonemapPass.cpp @@ -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( 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, diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 0d80798..c85c2bf 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,7 @@ #include #include #include +#include #include #include @@ -43,6 +45,7 @@ #include #include +#include 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( + *m_Device, *m_Allocator, + sizeof(u32) * kMaxInstances, kMaxFramesInFlight, + vk::BufferUsageFlagBits::eStorageBuffer); + m_SkinPaletteBuffer = CreateScope( + *m_Device, *m_Allocator, + sizeof(glm::mat4) * kMaxSkinMatrices, kMaxFramesInFlight, + vk::BufferUsageFlagBits::eStorageBuffer); // 3. pass 装配体:布局/shader/采样器/per-slot 描述符 m_SceneGpu = CreateScope( - *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( *m_Device, *m_DescriptorManager, *m_ShaderManager); @@ -191,6 +205,12 @@ namespace Engine 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(); @@ -210,9 +230,9 @@ namespace Engine static const std::vector 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; } @@ -244,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( @@ -317,10 +345,15 @@ namespace Engine m_BloomB)); m_Passes.push_back(CreateScope( *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( + frame, m_PresentTarget, m_Backbuffer)); m_Passes.push_back(CreateScope( - &m_ImGuiLayer, m_Backbuffer)); + &m_ImGuiLayer, frame, m_Backbuffer, m_PresentTarget)); m_Passes.push_back(CreateScope(m_Backbuffer)); for (auto& pass : m_Passes) { @@ -330,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( + *m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount()); + // backbuffer 物理尺寸跟新窗口走(ImGui 渲染区域 = 全窗口) + m_RenderGraph->SetImportedExtent(m_Backbuffer, + m_Swapchain->GetExtent()); + } - // image 数量可能变化 - m_SyncManager = CreateScope( - *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) @@ -386,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(); @@ -417,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; @@ -427,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; } @@ -486,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; diff --git a/Engine/src/Renderer/SceneRenderer.cpp b/Engine/src/Renderer/SceneRenderer.cpp index 66f9378..90880c0 100644 --- a/Engine/src/Renderer/SceneRenderer.cpp +++ b/Engine/src/Renderer/SceneRenderer.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -46,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& 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& 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& 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& pos, + std::vector& rot, + std::vector& scl, + std::vector& 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& pos, + const std::vector& rot, + const std::vector& scl, + const std::vector& touched, + std::vector& 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(nodes[i].parent)] * local + : local; + } + } } void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir) @@ -132,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(); @@ -179,6 +295,7 @@ namespace Engine 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; @@ -250,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( + [&](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(clips.size())) { + anim.clipIndex = 0; + anim.fadeFrom = -1; + if (clips.empty()) return; + } + if (anim.fadeFrom >= static_cast(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( + 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(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(entt::to_integral(entity)); + m_EntitySkinBase[entKey] = base; + }); + // ---- 3. GPUDriven:单趟收集 + 多视图剔除 ---- // 同作用域第二个 zone:ZoneScopedN 变量名固定会重声明,用 ZoneNamedN ZoneNamedN( ___tracy_collect, "GPUDriven Collect", true ); @@ -262,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; @@ -289,6 +509,13 @@ 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])), @@ -359,7 +586,7 @@ namespace Engine // 实例在扁平化后连续——firstInstance 语义的前提 const u32 bi = batchIdx - 1; s.result.batches[bi].count++; - s.batchMats[bi].push_back({t.World(), prevWorld}); + s.batchMats[bi].push_back({t.World(), prevWorld, skinBase}); s.batchDistSum[bi] += dist; // 透明排序用 s.result.instanceCount++; ++totalInstances; @@ -398,6 +625,8 @@ namespace Engine 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; @@ -407,6 +636,12 @@ 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; @@ -466,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(static_cast( + a.mesh->GetVertexBuffer())); + const u64 vb = reinterpret_cast(static_cast( + b.mesh->GetVertexBuffer())); + if (va != vb) return va < vb; + if (byMaterial && a.mesh->GetMaterial() != b.mesh->GetMaterial()) { + return reinterpret_cast(a.mesh->GetMaterial()) < + reinterpret_cast(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(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 diff --git a/Engine/src/Vulkan/Device.cpp b/Engine/src/Vulkan/Device.cpp index 4be5feb..8f70ab7 100644 --- a/Engine/src/Vulkan/Device.cpp +++ b/Engine/src/Vulkan/Device.cpp @@ -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; } // 扩展列表 diff --git a/Engine/src/Vulkan/Swapchain.cpp b/Engine/src/Vulkan/Swapchain.cpp index e91378f..4defcbb 100644 --- a/Engine/src/Vulkan/Swapchain.cpp +++ b/Engine/src/Vulkan/Swapchain.cpp @@ -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) diff --git a/shaders/shadow.vert b/shaders/shadow.vert index 0a74eaa..fe869a2 100644 --- a/shaders/shadow.vert +++ b/shaders/shadow.vert @@ -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); } diff --git a/shaders/simple_shader.frag b/shaders/simple_shader.frag index 0553fff..fb7b9ea 100644 --- a/shaders/simple_shader.frag +++ b/shaders/simple_shader.frag @@ -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; } diff --git a/shaders/simple_shader.vert b/shaders/simple_shader.vert index ac5db07..f2d1482 100644 --- a/shaders/simple_shader.vert +++ b/shaders/simple_shader.vert @@ -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); diff --git a/shaders/simple_shader_translucent.frag b/shaders/simple_shader_translucent.frag new file mode 100644 index 0000000..cde5c8d --- /dev/null +++ b/shaders/simple_shader_translucent.frag @@ -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 排序下近面后画恒通过,混合顺序仍由排序保证。 +} diff --git a/shaders/tonemap.frag b/shaders/tonemap.frag index d3956c1..0550932 100644 --- a/shaders/tonemap.frag +++ b/shaders/tonemap.frag @@ -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: {