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