Compare commits
159
Commits
4fd88248fc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84cf547704 | ||
|
|
2df6e57b61 | ||
|
|
f107854236 | ||
|
|
22309d24f0 | ||
|
|
e7711134fe | ||
|
|
2024772bac | ||
|
|
beba491bcb | ||
|
|
334819bf97 | ||
|
|
f15b104e77 | ||
|
|
6854d53f11 | ||
|
|
505217ff7e | ||
|
|
f14aba4238 | ||
|
|
6447b5c0fa | ||
|
|
235482733c | ||
|
|
c43adc339d | ||
|
|
2cf8d6e076 | ||
|
|
affa1a7a56 | ||
|
|
d4d37b4b28 | ||
|
|
21da38db92 | ||
|
|
f20578bc4b | ||
|
|
bca50976f3 | ||
|
|
d2701a3d9b | ||
|
|
ed7cb417bd | ||
|
|
b89f431896 | ||
|
|
26e222a9cf | ||
|
|
36b1fc0566 | ||
|
|
ff955b9e63 | ||
|
|
f327f44269 | ||
|
|
f73e52926f | ||
|
|
cc837bc605 | ||
|
|
a941207ae3 | ||
|
|
6c80693618 | ||
|
|
5d88ed2057 | ||
|
|
a3037cd401 | ||
|
|
641b255cb2 | ||
|
|
c083ee5cb7 | ||
|
|
1a278d5945 | ||
|
|
5ca5ac2a1e | ||
|
|
432089747f | ||
|
|
b7d6f53591 | ||
|
|
42757bab36 | ||
|
|
dec171cd1e | ||
|
|
e038cb39fa | ||
|
|
3e85285e41 | ||
|
|
2268fa5f76 | ||
|
|
c1854453e5 | ||
|
|
b78d9fb4c9 | ||
|
|
c8a1cd90bf | ||
|
|
ee5ab8e94e | ||
|
|
97408e506c | ||
|
|
230df67db7 | ||
|
|
13055cc8ac | ||
|
|
175ce35def | ||
|
|
2577809ee4 | ||
|
|
c949e8684b | ||
|
|
90097a756e | ||
|
|
4498bc7535 | ||
|
|
4fc48c21ac | ||
|
|
90bf63e641 | ||
|
|
69671cf4eb | ||
|
|
7d02d344e5 | ||
|
|
fd2d8f9b34 | ||
|
|
ab7a43a686 | ||
|
|
74a2044441 | ||
|
|
134812dae3 | ||
|
|
29c413579d | ||
|
|
b6d0868e15 | ||
|
|
cad0481bc0 | ||
|
|
3403822a52 | ||
|
|
ea6beee593 | ||
|
|
d5a86c170e | ||
|
|
e1cef53158 | ||
|
|
82910c41e8 | ||
|
|
a744b332eb | ||
|
|
6c81bfd70d | ||
|
|
fae6614e70 | ||
|
|
e2a9c21e68 | ||
|
|
007d175314 | ||
|
|
ffe33e9967 | ||
|
|
9ff08651f0 | ||
|
|
c9d4eed043 | ||
|
|
21b33d7d7f | ||
|
|
a130850276 | ||
|
|
1d360369c9 | ||
|
|
af241857af | ||
|
|
5cc9b14d73 | ||
|
|
83c1600947 | ||
|
|
e5161439dd | ||
|
|
74d2f6cdaa | ||
|
|
ce5e07879c | ||
|
|
3ec96acc0f | ||
|
|
56bb31638e | ||
|
|
b0d5ed4550 | ||
|
|
47dd879977 | ||
|
|
1449bea86d | ||
|
|
1c4b6f37ff | ||
|
|
bfbb18f1a4 | ||
|
|
20f0035baa | ||
|
|
b25fa29fe5 | ||
|
|
8dbfb9f554 | ||
|
|
a975845214 | ||
|
|
d9b1742327 | ||
|
|
34b593f734 | ||
|
|
3bfe28b027 | ||
|
|
99e6712cdf | ||
|
|
6df3be2846 | ||
|
|
fa9681aa41 | ||
|
|
25a365fce7 | ||
|
|
641f79d89e | ||
|
|
08439ed015 | ||
|
|
b65856b8a5 | ||
|
|
1d690067bd | ||
|
|
705250df76 | ||
|
|
a1c6ad20be | ||
|
|
708d714459 | ||
|
|
d3c3d0cdd9 | ||
|
|
42fd9d5f30 | ||
|
|
4d3963e291 | ||
|
|
8c0a11d2e8 | ||
|
|
960bdf2662 | ||
|
|
2b875c9284 | ||
|
|
9cc36af3f0 | ||
|
|
d2b878c077 | ||
|
|
bd4ac6aa12 | ||
|
|
6e107ac2ba | ||
|
|
9269afaa18 | ||
|
|
fa25cb3348 | ||
|
|
4b5990e6c8 | ||
|
|
89bf4256e8 | ||
|
|
2c4efc5165 | ||
|
|
d8c481ce48 | ||
|
|
b79df3c60e | ||
|
|
b6982d426f | ||
|
|
7d137fa9b1 | ||
|
|
57ab8901a9 | ||
|
|
1bc44e261b | ||
|
|
019c17c7e7 | ||
|
|
0ed7b4c5b8 | ||
|
|
c54f60c5ea | ||
|
|
8253c31ad5 | ||
|
|
a0c92af5db | ||
|
|
0328d4aaeb | ||
|
|
27b7f8785c | ||
|
|
a29aed2803 | ||
|
|
c75832e73e | ||
|
|
0de8498350 | ||
|
|
6f10fd624d | ||
|
|
718a658f5d | ||
|
|
91880d88fe | ||
|
|
fa9082706b | ||
|
|
df4871a49b | ||
|
|
d8274e8bdc | ||
|
|
cef32ea1ba | ||
|
|
6a3fcac072 | ||
|
|
7fca721a24 | ||
|
|
41e4cc85d5 | ||
|
|
908e688178 | ||
|
|
601fd8997c | ||
|
|
952ba8135d |
@@ -1,4 +1,8 @@
|
||||
.idea/
|
||||
.trae/
|
||||
|
||||
# 运行日志(工作目录生成)
|
||||
HelloVulkan.log
|
||||
|
||||
cmake-build-*/
|
||||
out/
|
||||
@@ -6,3 +10,5 @@ build/
|
||||
|
||||
# 目前只上传代码,暂不上传assets
|
||||
assets/
|
||||
# 本地文档暂不上传
|
||||
Docs/
|
||||
|
||||
+18
@@ -17,3 +17,21 @@
|
||||
path = Lib/imgui
|
||||
url = https://github.com/ocornut/imgui.git
|
||||
branch = docking
|
||||
[submodule "Lib/VulkanMemoryAllocator"]
|
||||
path = Lib/VulkanMemoryAllocator
|
||||
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
|
||||
[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
|
||||
[submodule "Lib/JoltPhysics"]
|
||||
path = Lib/JoltPhysics
|
||||
url = https://github.com/jrouwe/JoltPhysics.git
|
||||
[submodule "Lib/ImGuizmo"]
|
||||
path = Lib/ImGuizmo
|
||||
url = https://github.com/CedricGuillemet/ImGuizmo.git
|
||||
[submodule "Lib/yaml-cpp"]
|
||||
path = Lib/yaml-cpp
|
||||
url = https://github.com/jbeder/yaml-cpp.git
|
||||
|
||||
+21
-2
@@ -1,4 +1,22 @@
|
||||
add_executable(HelloVulkan main.cpp)
|
||||
# App 层源文件:main + editor/(docking 编辑器)+ scripts/(C++ 原生脚本)
|
||||
file(GLOB EDITOR_SOURCES CONFIGURE_DEPENDS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/editor/panels/*.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/editor/panels/*.h)
|
||||
|
||||
file(GLOB SCRIPT_SOURCES CONFIGURE_DEPENDS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/*.cpp)
|
||||
|
||||
# editor 形态(HELLOVULKAN_EDITOR=ON)编入 editor/(docking 编辑器);
|
||||
# game 形态只有 main + scripts(编辑器源码整体不进产物)
|
||||
if(HELLOVULKAN_EDITOR)
|
||||
add_executable(HelloVulkan main.cpp ${EDITOR_SOURCES} ${SCRIPT_SOURCES})
|
||||
else()
|
||||
add_executable(HelloVulkan main.cpp ${SCRIPT_SOURCES})
|
||||
endif()
|
||||
|
||||
target_include_directories(HelloVulkan PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/editor)
|
||||
|
||||
target_link_libraries(HelloVulkan PRIVATE Engine)
|
||||
|
||||
@@ -33,7 +51,8 @@ file(GLOB_RECURSE SHADER_SOURCES
|
||||
CONFIGURE_DEPENDS # 新增 shader 时自动重配
|
||||
RELATIVE ${SHADER_SRC_DIR}
|
||||
${SHADER_SRC_DIR}/*.vert
|
||||
${SHADER_SRC_DIR}/*.frag)
|
||||
${SHADER_SRC_DIR}/*.frag
|
||||
${SHADER_SRC_DIR}/*.comp)
|
||||
|
||||
foreach (f ${SHADER_SOURCES})
|
||||
auto_compile_shader(${f})
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
//
|
||||
// AssetItemView.cpp:资产条目绘制单元实现。
|
||||
//
|
||||
|
||||
#include "AssetItemView.h"
|
||||
|
||||
#include "AssetKind.h"
|
||||
#include "EditorColors.h"
|
||||
#include "ThumbnailCache.h"
|
||||
#include "panels/AssetBrowserPanel.h" // kPrefabDragPayload / kModelDragPayload
|
||||
#include "panels/ControllerEditorPanel.h" // kControllerDragPayload
|
||||
#include "panels/MaterialEditorPanel.h" // kMaterialDragPayload / kTextureDragPayload
|
||||
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/MaterialAsset.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kBoxRounding = 3.0f;
|
||||
|
||||
// 类别色压暗背板 + 描边(p0/p1 来自上一个 item rect)
|
||||
void DrawBoxChrome(const ImVec2& p0, const ImVec2& p1,
|
||||
const ImVec4& accent)
|
||||
{
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(p0, p1,
|
||||
ImGui::GetColorU32(ImVec4(accent.x * 0.25f,
|
||||
accent.y * 0.25f,
|
||||
accent.z * 0.25f, 1.0f)),
|
||||
kBoxRounding);
|
||||
dl->AddRect(p0, p1, ImGui::GetColorU32(accent), kBoxRounding);
|
||||
}
|
||||
|
||||
// 名字按可用宽度截断(UTF-8 边界安全)
|
||||
std::string TruncateToWidth(const std::string& s, float maxW)
|
||||
{
|
||||
if (ImGui::CalcTextSize(s.c_str()).x <= maxW) return s;
|
||||
std::string out = s;
|
||||
const char* ell = "..";
|
||||
while (!out.empty() &&
|
||||
ImGui::CalcTextSize((out + ell).c_str()).x > maxW)
|
||||
{
|
||||
out.pop_back();
|
||||
while (!out.empty() && (static_cast<unsigned char>(out.back()) &
|
||||
0xC0) == 0x80)
|
||||
{
|
||||
out.pop_back(); // 退到 UTF-8 起始字节
|
||||
}
|
||||
}
|
||||
return out + ell;
|
||||
}
|
||||
|
||||
// 已尝试按需加载的材质资产路径:加载失败不再每帧重试(资产缓存接管
|
||||
// 成功路径,重复调用是缓存命中)
|
||||
std::set<std::string>& MaterialLoadTried()
|
||||
{
|
||||
static std::set<std::string> s;
|
||||
return s;
|
||||
}
|
||||
|
||||
// 材质加载闸门:大目录(上百个材质)逐个同步加载(.material 解析 +
|
||||
// GPU 纹理上传——Texture 构造内含 submit + fence 阻塞等待,单个材质
|
||||
// 可含多张大贴图)会顶爆单帧 → "卡成 PPT"。闸门 = 每帧至多 1 个 +
|
||||
// 按上次实测耗时冷却(1 帧≈16ms;重载 ≈ 冷却相应帧数),把加载占用
|
||||
// 压到 ≤ ~50% 帧预算,剩余帧时间还给渲染
|
||||
struct MaterialLoadGate
|
||||
{
|
||||
bool Allow()
|
||||
{
|
||||
const int frame = ImGui::GetFrameCount();
|
||||
return frame != m_LastLoadFrame && frame >= m_CooldownUntil;
|
||||
}
|
||||
void Charge(double elapsedMs)
|
||||
{
|
||||
const int frame = ImGui::GetFrameCount();
|
||||
m_LastLoadFrame = frame;
|
||||
const int cool = static_cast<int>(elapsedMs / 16.0);
|
||||
m_CooldownUntil = frame + 1 + std::min(cool, 30);
|
||||
}
|
||||
private:
|
||||
int m_LastLoadFrame = -1;
|
||||
int m_CooldownUntil = 0;
|
||||
};
|
||||
|
||||
MaterialLoadGate& MaterialLoad()
|
||||
{
|
||||
static MaterialLoadGate s_Gate;
|
||||
return s_Gate;
|
||||
}
|
||||
|
||||
// 已烘焙材质缩略图对应的 (serial, editVersion)。资产重载(新 serial)
|
||||
// 或参数编辑(editVersion++)后与当前不符 = 缩略图过期,须作废重烘
|
||||
//(否则浏览器永远停在旧图)——记录时机 = 发起烘焙请求那一帧
|
||||
std::unordered_map<std::string, std::pair<Engine::u64, Engine::u32>>&
|
||||
BakedIdentity()
|
||||
{
|
||||
static std::unordered_map<std::string,
|
||||
std::pair<Engine::u64, Engine::u32>> s;
|
||||
return s;
|
||||
}
|
||||
|
||||
// 材质球缩略图全部走编辑器缩略图缓存:引擎把预览球渲染一次后 GPU→CPU
|
||||
// 回读,App 注入缓存(与贴图缩略图同一条出图路径),浏览器不再占用
|
||||
// 引擎预览 tile 池——大目录滚动/材质编辑器预览互不饿死
|
||||
void DrawCachedThumb(const ThumbView& v, float size, const ImVec2& p0)
|
||||
{
|
||||
float fw = size, fh = size;
|
||||
if (v.w > 0 && v.h > 0)
|
||||
{
|
||||
if (v.w >= v.h) fh = size * static_cast<float>(v.h) / v.w;
|
||||
else fw = size * static_cast<float>(v.w) / v.h;
|
||||
}
|
||||
const ImVec2 fp0(p0.x + (size - fw) * 0.5f, p0.y + (size - fh) * 0.5f);
|
||||
ImGui::GetWindowDrawList()->AddImage(
|
||||
v.tex, fp0, ImVec2(fp0.x + fw, fp0.y + fh));
|
||||
}
|
||||
|
||||
// 缩略图/占位块:一个命中项 + 绘制层。
|
||||
// interactive=true → InvisibleButton(带 ID,可承载拖拽源/点击;Dummy
|
||||
// 无 ID,在其上 BeginDragDropSource 会断言);false → Dummy(行内小图
|
||||
// 纯展示,不抢行 Selectable 的点击)。
|
||||
// 出图时**不铺任何背板**——透明像素会被类别色染色;背板只在未出图时
|
||||
// 作为占位(类别色压暗 + 短标签)。仅可见项才请求缩略图,离屏项不入
|
||||
// 解码队列(大目录不再一次性排队,出图延迟可控)
|
||||
void DrawThumbBox(const EditorContext& ctx,
|
||||
const std::filesystem::path& path, float size,
|
||||
EditorAsset::AssetKind kind, bool interactive)
|
||||
{
|
||||
const bool visible = ImGui::IsRectVisible(ImVec2(size, size));
|
||||
if (interactive)
|
||||
{
|
||||
ImGui::InvisibleButton("##thumb", ImVec2(size, size));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Dummy(ImVec2(size, size));
|
||||
}
|
||||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||||
const ImVec4 accent = EditorColors::AssetColor(kind);
|
||||
|
||||
bool drawn = false;
|
||||
if (visible && kind == EditorAsset::AssetKind::Material && ctx.assets)
|
||||
{
|
||||
// 材质资产缩略图 = 引擎烘焙的静态材质球(GPU→CPU 回读 → 缩略图
|
||||
// 缓存;与贴图缩略图同一条出图路径)
|
||||
auto& cache = EditorThumbnailCache::Get();
|
||||
const std::string assetPath = path.generic_string();
|
||||
|
||||
// 当前材质身份:已加载才有;未加载时无法判新旧
|
||||
Engine::Material* curMat = nullptr;
|
||||
if (ctx.assets->IsMaterialLoaded(assetPath))
|
||||
{
|
||||
Engine::MaterialAsset* a =
|
||||
ctx.assets->FindMaterialAsset(assetPath);
|
||||
curMat = a ? a->GetMaterial() : nullptr;
|
||||
}
|
||||
|
||||
const ThumbView v = cache.Peek(assetPath);
|
||||
if (v.state == ThumbView::State::Ready)
|
||||
{
|
||||
// 失效检测:缓存缩略图对应的材质身份 vs 当前身份——资产
|
||||
// 重载(新 serial,换贴图槽)/ 参数编辑(editVersion++)后
|
||||
// 不符 = 过期,作废缓存让下一帧重烘(否则浏览器永远旧图)
|
||||
bool stale = false;
|
||||
if (curMat)
|
||||
{
|
||||
auto it = BakedIdentity().find(assetPath);
|
||||
if (it != BakedIdentity().end() &&
|
||||
(it->second.first != curMat->GetSerial() ||
|
||||
it->second.second != curMat->GetEditVersion()))
|
||||
{
|
||||
stale = true;
|
||||
}
|
||||
}
|
||||
if (!stale)
|
||||
{
|
||||
DrawCachedThumb(v, size, p0);
|
||||
drawn = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
cache.Invalidate(assetPath);
|
||||
BakedIdentity().erase(assetPath);
|
||||
}
|
||||
}
|
||||
else if (v.state != ThumbView::State::Pending && ctx.renderer)
|
||||
{
|
||||
if (curMat)
|
||||
{
|
||||
// 未烘焙/已失效:请求烘焙并登记当前身份(key 幂等
|
||||
// keepalive,回读完成即进缓存)
|
||||
ctx.renderer->RequestMaterialPreviewBake(
|
||||
curMat, cache.BakeKeyForPath(assetPath));
|
||||
BakedIdentity()[assetPath] = {
|
||||
curMat->GetSerial(), curMat->GetEditVersion()};
|
||||
}
|
||||
else if (!MaterialLoadTried().count(assetPath) &&
|
||||
MaterialLoad().Allow())
|
||||
{
|
||||
// 按需加载材质(闸门限流,避免大目录同帧卡顿)
|
||||
MaterialLoadTried().insert(assetPath);
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
ctx.assets->LoadMaterialAsset(assetPath);
|
||||
const double ms = std::chrono::duration<double,
|
||||
std::milli>(std::chrono::steady_clock::now() - t0)
|
||||
.count();
|
||||
MaterialLoad().Charge(ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!drawn && visible && EditorAsset::IsThumbnailable(path))
|
||||
{
|
||||
const ThumbView v =
|
||||
EditorThumbnailCache::Get().Request(path.generic_string());
|
||||
if (v.state == ThumbView::State::Ready)
|
||||
{
|
||||
DrawCachedThumb(v, size, p0);
|
||||
drawn = true;
|
||||
}
|
||||
}
|
||||
if (!drawn)
|
||||
{
|
||||
// 占位:类别色压暗背板 + 描边;大框叠加短标签
|
||||
DrawBoxChrome(p0, p1, accent);
|
||||
if (size >= 28.0f)
|
||||
{
|
||||
const char* label = EditorAsset::KindShortLabel(kind);
|
||||
const ImVec2 ts = ImGui::CalcTextSize(label);
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(p0.x + (size - ts.x) * 0.5f,
|
||||
p0.y + (size - ts.y) * 0.5f),
|
||||
ImGui::GetColorU32(accent), label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文件夹命中项(黄色)+ 短标签
|
||||
void DrawFolderBox(float size, bool interactive)
|
||||
{
|
||||
if (interactive)
|
||||
{
|
||||
ImGui::InvisibleButton("##dir", ImVec2(size, size));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Dummy(ImVec2(size, size));
|
||||
}
|
||||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||||
DrawBoxChrome(p0, p1, EditorColors::kFolder);
|
||||
if (size >= 28.0f)
|
||||
{
|
||||
const ImVec2 ts = ImGui::CalcTextSize("DIR");
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(p0.x + (size - ts.x) * 0.5f,
|
||||
p0.y + (size - ts.y) * 0.5f),
|
||||
ImGui::GetColorU32(EditorColors::kFolder), "DIR");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace EditorAssetView {
|
||||
|
||||
void DrawDragSource(const std::filesystem::path& path,
|
||||
const std::string& name)
|
||||
{
|
||||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||||
const char* payload = nullptr;
|
||||
switch (kind)
|
||||
{
|
||||
case EditorAsset::AssetKind::Material: payload = kMaterialDragPayload; break;
|
||||
case EditorAsset::AssetKind::AnimClip: payload = kControllerDragPayload; break;
|
||||
case EditorAsset::AssetKind::Prefab: payload = kPrefabDragPayload; break;
|
||||
case EditorAsset::AssetKind::Model: payload = kModelDragPayload; break;
|
||||
case EditorAsset::AssetKind::Texture: payload = kTextureDragPayload; break;
|
||||
default: return;
|
||||
}
|
||||
if (ImGui::BeginDragDropSource())
|
||||
{
|
||||
const std::string assetPath = path.generic_string();
|
||||
ImGui::SetDragDropPayload(payload, assetPath.c_str(),
|
||||
assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawTooltip(const EditorContext& ctx,
|
||||
const std::filesystem::path& path)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
const std::string name = path.filename().string();
|
||||
const std::string assetPath = path.generic_string();
|
||||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::Text("type: %s", EditorAsset::KindLabel(kind));
|
||||
|
||||
std::error_code ec;
|
||||
const auto sz = fs::file_size(path, ec);
|
||||
if (!ec)
|
||||
{
|
||||
char szBuf[32];
|
||||
if (sz >= 1024ull * 1024ull)
|
||||
{
|
||||
std::snprintf(szBuf, sizeof(szBuf), "%.1f MB",
|
||||
static_cast<double>(sz) / (1024.0 * 1024.0));
|
||||
}
|
||||
else
|
||||
{
|
||||
std::snprintf(szBuf, sizeof(szBuf), "%.1f KB",
|
||||
static_cast<double>(sz) / 1024.0);
|
||||
}
|
||||
ImGui::Text("size: %s", szBuf);
|
||||
}
|
||||
|
||||
const auto ftime = fs::last_write_time(path, ec);
|
||||
if (!ec)
|
||||
{
|
||||
// file_clock → system_clock 近似转换(C++17 无官方转换)
|
||||
const auto sctp = std::chrono::time_point_cast<
|
||||
std::chrono::system_clock::duration>(
|
||||
ftime - fs::file_time_type::clock::now() +
|
||||
std::chrono::system_clock::now());
|
||||
const std::time_t tt = std::chrono::system_clock::to_time_t(sctp);
|
||||
char tmBuf[32];
|
||||
std::strftime(tmBuf, sizeof(tmBuf), "%Y-%m-%d %H:%M",
|
||||
std::localtime(&tt));
|
||||
ImGui::Text("modified: %s", tmBuf);
|
||||
}
|
||||
|
||||
if (ctx.assets)
|
||||
{
|
||||
if (kind == EditorAsset::AssetKind::Model ||
|
||||
kind == EditorAsset::AssetKind::Texture)
|
||||
{
|
||||
const bool loaded =
|
||||
(kind == EditorAsset::AssetKind::Model)
|
||||
? ctx.assets->IsModelLoaded(assetPath)
|
||||
: ctx.assets->IsEnvironmentLoaded(assetPath);
|
||||
ImGui::Text("state: %s", loaded ? "loaded" : "not loaded");
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::Prefab)
|
||||
{
|
||||
ImGui::TextDisabled(
|
||||
"template — drag into scene / double-click");
|
||||
}
|
||||
if (kind == EditorAsset::AssetKind::Model)
|
||||
{
|
||||
if (const Engine::Model* m =
|
||||
ctx.assets->GetModelByPath(assetPath);
|
||||
m != nullptr)
|
||||
{
|
||||
ImGui::Text("meshes: %u, bones: %u, clips: %u",
|
||||
static_cast<unsigned>(m->GetMeshes().size()),
|
||||
m->GetBoneCount(),
|
||||
static_cast<unsigned>(m->GetClips().size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::TextDisabled("%s", assetPath.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
ItemResult DrawRow(const EditorContext& ctx,
|
||||
const std::filesystem::path& path, bool selected)
|
||||
{
|
||||
ItemResult res;
|
||||
const std::string name = path.filename().string();
|
||||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||||
|
||||
ImGui::PushID(name.c_str());
|
||||
|
||||
// 内联小缩略图 / 类别色占位块(非交互:行点击归 Selectable)
|
||||
DrawThumbBox(ctx, path, ImGui::GetTextLineHeight(), kind, false);
|
||||
ImGui::SameLine();
|
||||
|
||||
// 名称(类别色提亮)+ 类别色 hover/选中高亮
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind));
|
||||
ImGui::PushStyleColor(ImGuiCol_Header,
|
||||
EditorColors::AssetHighlightColor(kind));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
|
||||
EditorColors::AssetHighlightColor(kind));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive,
|
||||
EditorColors::AssetHighlightColor(kind));
|
||||
res.clicked = ImGui::Selectable(name.c_str(), selected,
|
||||
ImGuiSelectableFlags_AllowDoubleClick);
|
||||
ImGui::PopStyleColor(4);
|
||||
if (res.clicked && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
res.doubleClicked = true;
|
||||
}
|
||||
|
||||
DrawDragSource(path, name);
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
DrawTooltip(ctx, path);
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemResult DrawTile(const EditorContext& ctx,
|
||||
const std::filesystem::path& path,
|
||||
float tileSize, bool selected)
|
||||
{
|
||||
ItemResult res;
|
||||
const std::string name = path.filename().string();
|
||||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||||
const ImVec4 accent = EditorColors::AssetColor(kind);
|
||||
|
||||
ImGui::PushID(name.c_str());
|
||||
ImGui::BeginGroup();
|
||||
|
||||
DrawThumbBox(ctx, path, tileSize, kind, true);
|
||||
|
||||
// 选中/hover 描边(叠在缩略图上)
|
||||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
if (selected || hovered)
|
||||
{
|
||||
ImGui::GetWindowDrawList()->AddRect(
|
||||
p0, p1, ImGui::GetColorU32(accent), kBoxRounding, 0,
|
||||
selected ? 2.0f : 1.0f);
|
||||
}
|
||||
res.clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||
if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
res.doubleClicked = true;
|
||||
}
|
||||
|
||||
DrawDragSource(path, name);
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
DrawTooltip(ctx, path);
|
||||
}
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind));
|
||||
ImGui::TextUnformatted(TruncateToWidth(name, tileSize).c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::EndGroup();
|
||||
ImGui::PopID();
|
||||
return res;
|
||||
}
|
||||
|
||||
bool DrawFolderRow(const std::filesystem::path& dir)
|
||||
{
|
||||
const std::string dname = dir.filename().string();
|
||||
ImGui::PushID(dname.c_str());
|
||||
DrawFolderBox(ImGui::GetTextLineHeight(), false);
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder);
|
||||
const bool clicked = ImGui::Selectable(dname.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopID();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
bool DrawFolderTile(const std::filesystem::path& dir, float tileSize,
|
||||
bool selected)
|
||||
{
|
||||
const std::string dname = dir.filename().string();
|
||||
ImGui::PushID(dname.c_str());
|
||||
ImGui::BeginGroup();
|
||||
|
||||
DrawFolderBox(tileSize, true);
|
||||
|
||||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||||
if (selected || ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::GetWindowDrawList()->AddRect(
|
||||
p0, p1, ImGui::GetColorU32(EditorColors::kFolder), kBoxRounding,
|
||||
0, selected ? 2.0f : 1.0f);
|
||||
}
|
||||
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder);
|
||||
ImGui::TextUnformatted(TruncateToWidth(dname, tileSize).c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::EndGroup();
|
||||
ImGui::PopID();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
} // namespace EditorAssetView
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// AssetItemView:资产条目的绘制单元(列表行 / 网格瓦片 / 目录 / 拖拽源 /
|
||||
// tooltip)。Content Browser 与材质纹理选择器共用同一套视觉与交互。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASSETITEMVIEW_H
|
||||
#define HELLOVULKAN_ASSETITEMVIEW_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace EditorAssetView {
|
||||
|
||||
// 单击 / 双击结果(调用方决定选中与打开语义)
|
||||
struct ItemResult {
|
||||
bool clicked = false;
|
||||
bool doubleClicked = false;
|
||||
};
|
||||
|
||||
// 拖拽源:按资产类别发出对应 payload(材质/控制器/预制体/模型/纹理)
|
||||
void DrawDragSource(const std::filesystem::path& path,
|
||||
const std::string& name);
|
||||
|
||||
// 统一 tooltip(类型/大小/修改时间/加载状态)
|
||||
void DrawTooltip(const EditorContext& ctx,
|
||||
const std::filesystem::path& path);
|
||||
|
||||
// 列表行:内联小缩略图 + 彩色短标签 + 类别色名称
|
||||
ItemResult DrawRow(const EditorContext& ctx,
|
||||
const std::filesystem::path& path, bool selected);
|
||||
|
||||
// 网格瓦片:缩略图/类别色占位块 + 名称(截断到瓦片宽)
|
||||
ItemResult DrawTile(const EditorContext& ctx,
|
||||
const std::filesystem::path& path,
|
||||
float tileSize, bool selected);
|
||||
|
||||
// 目录(文件夹色):返回是否被点击(选中/进入)
|
||||
bool DrawFolderRow(const std::filesystem::path& dir);
|
||||
bool DrawFolderTile(const std::filesystem::path& dir, float tileSize,
|
||||
bool selected);
|
||||
|
||||
} // namespace EditorAssetView
|
||||
|
||||
#endif //HELLOVULKAN_ASSETITEMVIEW_H
|
||||
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// AssetKind.h:资产分类的单一事实来源(Content Browser / 编辑器共用)。
|
||||
//
|
||||
// 原先分类逻辑散在 AssetBrowserPanel.cpp 匿名 ns(Kind/KindOf/KindTag),
|
||||
// 现在上提为头文件:面板、拖拽源、缩略图缓存按同一份扩展名表分派。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASSETKIND_H
|
||||
#define HELLOVULKAN_ASSETKIND_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace EditorAsset {
|
||||
|
||||
// 资产类别(按扩展名分派;同时是类型过滤的身份)
|
||||
enum class AssetKind {
|
||||
Model, Texture, Scene, Material, AnimClip, Prefab, Other
|
||||
};
|
||||
|
||||
// 统一小写扩展名(含点,如 ".png";无扩展名 = 空串)
|
||||
inline std::string LowerExt(const std::filesystem::path& p)
|
||||
{
|
||||
std::string ext = p.extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
return ext;
|
||||
}
|
||||
|
||||
// 纹理扩展名表(单一事实来源:分类 / 缩略图 / 材质纹理槽选择器共用)。
|
||||
// 口径 = 引擎 Texture 可直接加载者:
|
||||
// stb_image 8-bit 系 png/jpg/tga/bmp/psd/gif/pnm
|
||||
// + .hdr(环境贴图,stbi_loadf / stbi_load 走内置 hdr→ldr)
|
||||
// + .dds(引擎 DdsLoader,BC 压缩直传)
|
||||
inline bool IsTexturePath(const std::filesystem::path& p)
|
||||
{
|
||||
static const char* kExts[] = {
|
||||
".png", ".jpg", ".jpeg", ".tga", ".bmp", ".psd",
|
||||
".gif", ".ppm", ".pgm", ".pnm", ".hdr", ".dds"};
|
||||
const std::string ext = LowerExt(p);
|
||||
for (const char* e : kExts)
|
||||
{
|
||||
if (ext == e) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 扩展名 → 类别
|
||||
inline AssetKind KindOfPath(const std::filesystem::path& p)
|
||||
{
|
||||
const std::string ext = LowerExt(p);
|
||||
if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" ||
|
||||
ext == ".obj")
|
||||
{
|
||||
return AssetKind::Model;
|
||||
}
|
||||
if (IsTexturePath(p))
|
||||
{
|
||||
return AssetKind::Texture;
|
||||
}
|
||||
if (ext == ".scene")
|
||||
{
|
||||
return AssetKind::Scene;
|
||||
}
|
||||
if (ext == ".material")
|
||||
{
|
||||
return AssetKind::Material;
|
||||
}
|
||||
if (ext == ".animctrl")
|
||||
{
|
||||
return AssetKind::AnimClip;
|
||||
}
|
||||
if (ext == ".prefab")
|
||||
{
|
||||
return AssetKind::Prefab;
|
||||
}
|
||||
return AssetKind::Other;
|
||||
}
|
||||
|
||||
// 全称标签(tooltip / 网格标题;替代原下标对齐数组)
|
||||
inline const char* KindLabel(AssetKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case AssetKind::Model: return "Model";
|
||||
case AssetKind::Texture: return "Texture/Environment";
|
||||
case AssetKind::Scene: return "Scene";
|
||||
case AssetKind::Material: return "Material";
|
||||
case AssetKind::AnimClip: return "Controller";
|
||||
case AssetKind::Prefab: return "Prefab";
|
||||
default: return "File";
|
||||
}
|
||||
}
|
||||
|
||||
// 列表类型列的短标签(替代原 "[M]"/"[T]" 前缀)
|
||||
inline const char* KindShortLabel(AssetKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case AssetKind::Model: return "MDL";
|
||||
case AssetKind::Texture: return "TEX";
|
||||
case AssetKind::Scene: return "SCN";
|
||||
case AssetKind::Material: return "MAT";
|
||||
case AssetKind::AnimClip: return "CTRL";
|
||||
case AssetKind::Prefab: return "PFB";
|
||||
default: return "FILE";
|
||||
}
|
||||
}
|
||||
|
||||
// 可解码为 8-bit 缩略图的扩展名:stbi_load 原生支持者(含 .hdr ——
|
||||
// stbi_load 内部走 hdr→ldr 色调映射)。仅 .dds 例外(BC 压缩块,需
|
||||
// 专用解码器)→ 占位块
|
||||
inline bool IsThumbnailable(const std::filesystem::path& p)
|
||||
{
|
||||
return IsTexturePath(p) && LowerExt(p) != ".dds";
|
||||
}
|
||||
|
||||
} // namespace EditorAsset
|
||||
|
||||
#endif //HELLOVULKAN_ASSETKIND_H
|
||||
@@ -0,0 +1,619 @@
|
||||
//
|
||||
// Editor.cpp:docking 骨架实现。
|
||||
//
|
||||
// 默认布局 = UE5 出厂布局(首帧 DockBuilder 一次,之后 imgui.ini 接管):
|
||||
// ┌──────────┬──────────────────────┬───────────┐
|
||||
// │ Outliner │ │ │
|
||||
// │ │ Viewport │ Details │
|
||||
// │ │ (+Stats tab) │ │
|
||||
// ├──────────┴──────────────────────┤ │
|
||||
// │ Content Browser(左+中通栏) │ │
|
||||
// └─────────────────────────────────┴───────────┘
|
||||
//
|
||||
|
||||
#include "Editor.h"
|
||||
#include "ThumbnailCache.h" // 缩略图缓存(每帧 BeginFrame)
|
||||
#include "panels/AssetBrowserPanel.h"
|
||||
#include "panels/HierarchyPanel.h"
|
||||
#include "panels/InspectorPanel.h"
|
||||
#include "panels/MaterialEditorPanel.h"
|
||||
#include "panels/ControllerEditorPanel.h"
|
||||
#include "panels/ViewportPanel.h"
|
||||
#include "panels/StatsPanel.h"
|
||||
#include "panels/LogPanel.h"
|
||||
#include "panels/GamepadPanel.h"
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Serialization/SceneSerializer.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h> // DockBuilder*(内部 API)
|
||||
#include <ImGuizmo.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h)
|
||||
|
||||
Editor::Editor() = default;
|
||||
|
||||
Editor::~Editor() = default;
|
||||
|
||||
void Editor::Init(const EditorContext& ctx)
|
||||
{
|
||||
m_Ctx = ctx;
|
||||
// PIE 运行/暂停镜像指针:指向本对象成员(ctx 拷贝后补接,面板只读;
|
||||
// 权威 = GameFlow,镜像在 Draw 开头同步——面板接口保持 const bool*)
|
||||
m_Ctx.playState = &m_PlayingMirror;
|
||||
m_Ctx.pausedState = &m_PausedMirror;
|
||||
|
||||
// PIE 转换副作用挂监听(GameFlow 不知场景;快照/恢复是编辑器职责)
|
||||
if (m_Ctx.flow)
|
||||
{
|
||||
m_Ctx.flow->AddListener(
|
||||
[this](GameFlowState, GameFlowState to) {
|
||||
if (to == GameFlowState::Loading) OnFlowEnterLoading();
|
||||
else if (to == GameFlowState::Stopped) OnFlowEnterStopped();
|
||||
// →Running/→Paused:timeScale 由 GameFlow 权威处理,
|
||||
// 编辑器无额外副作用
|
||||
});
|
||||
}
|
||||
|
||||
m_Hierarchy = std::make_unique<HierarchyPanel>();
|
||||
m_Inspector = std::make_unique<InspectorPanel>();
|
||||
m_Viewport = std::make_unique<ViewportPanel>();
|
||||
m_Stats = std::make_unique<StatsPanel>();
|
||||
m_Log = std::make_unique<LogPanel>();
|
||||
m_Gamepad = std::make_unique<GamepadPanel>();
|
||||
m_AssetBrowser = std::make_unique<AssetBrowserPanel>();
|
||||
m_MaterialEditor = std::make_unique<MaterialEditorPanel>();
|
||||
m_Ctx.materialEditor = m_MaterialEditor.get();
|
||||
m_ControllerEditor = std::make_unique<ControllerEditorPanel>();
|
||||
m_Ctx.controllerEditor = m_ControllerEditor.get();
|
||||
m_Ctx.selected = &m_Selected;
|
||||
|
||||
// 旧布局一次性迁移:面板改名(Hierarchy→Outliner / Inspector→Details /
|
||||
// Asset Browser→Content Browser)= docking 身份全变,旧 imgui.ini 对
|
||||
// 新窗口名失效(新窗口首启浮动,而默认布局又因 dock node 非空不再
|
||||
// 重建)。检测"有旧名无新名" → 删 ini 让 UE 默认布局重建一次。
|
||||
// 此刻 ImGui 首帧 NewFrame 还未读盘(NewFrame 内才 LoadIniSettings),
|
||||
// 删文件干净无副作用;新式 ini 或无 ini 不动
|
||||
if (ImGui::GetCurrentContext() && ImGui::GetIO().IniFilename)
|
||||
{
|
||||
const char* path = ImGui::GetIO().IniFilename;
|
||||
std::ifstream file(path);
|
||||
if (file)
|
||||
{
|
||||
const std::string content((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
if (content.find("[Window][Hierarchy]") != std::string::npos &&
|
||||
content.find("[Window][Outliner]") == std::string::npos)
|
||||
{
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
ENGINE_INFO("Editor: legacy imgui.ini removed, "
|
||||
"UE5 default layout will be rebuilt");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- PIE(Play-In-Editor)----
|
||||
|
||||
// →Loading 监听器:Play 半程。当前场景 → 内存 YAML 快照 → 切 gameView
|
||||
//(Viewport 面板内嵌 primary 相机渲染,UE PIE 语义——编辑器 UI 保持
|
||||
// 可用,不切全屏)→ 同步加载完成即 CompleteLoading(快照是同步操作,
|
||||
// 不完成的 Loading 会卡死主循环的物理/脚本门控)。快照失败 → RequestStop
|
||||
// 回滚(现场不动)
|
||||
void Editor::OnFlowEnterLoading()
|
||||
{
|
||||
if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer || !m_Ctx.flow) return;
|
||||
|
||||
m_PlaySnapshot = Serialization::SceneSerializer::SerializeToString(
|
||||
*m_Ctx.scene, *m_Ctx.assets, "PlaySnapshot");
|
||||
if (m_PlaySnapshot.empty())
|
||||
{
|
||||
ENGINE_ERROR("PIE: snapshot failed, play aborted");
|
||||
m_Ctx.flow->RequestStop(); // 回滚 → 触发 →Stopped 监听器(空快照跳过恢复)
|
||||
return;
|
||||
}
|
||||
|
||||
m_Gizmo.dragging = false; // 编辑态拖杆中点 Play:杆运行态不出,复位
|
||||
m_Ctx.renderer->SetGameView(true);
|
||||
m_Ctx.flow->CompleteLoading(); // 同步加载完成 → Running(解冻)
|
||||
ENGINE_INFO("PIE: playing (scene snapshot {} bytes)",
|
||||
m_PlaySnapshot.size());
|
||||
}
|
||||
|
||||
// →Stopped 监听器:Stop 半程。快照原子换血恢复 → 回编辑器渲染。
|
||||
// 换血后所有 Entity 句柄悬空:清选中(描边同步清)、Gizmo 拖拽态
|
||||
// 复位。物理 body/约束/角色由对账系统随实体重建/清理,无需干预。
|
||||
// 恢复失败(快照损坏,理论罕见)也必须回编辑态,现场留给用户
|
||||
// File→Load 或退出前 Save。空快照 = 快照失败回滚(未真 Play 过),跳过恢复
|
||||
void Editor::OnFlowEnterStopped()
|
||||
{
|
||||
if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return;
|
||||
|
||||
if (!m_PlaySnapshot.empty())
|
||||
{
|
||||
if (!Serialization::SceneSerializer::DeserializeFromString(
|
||||
*m_Ctx.scene, *m_Ctx.assets, m_PlaySnapshot))
|
||||
{
|
||||
ENGINE_ERROR("PIE: snapshot restore failed (scene is now the "
|
||||
"played-out state)");
|
||||
}
|
||||
m_PlaySnapshot.clear();
|
||||
}
|
||||
|
||||
m_Ctx.renderer->SetGameView(false);
|
||||
SelectEntity(entt::null);
|
||||
m_Gizmo.dragging = false;
|
||||
ENGINE_INFO("PIE: stopped, editor scene restored");
|
||||
}
|
||||
|
||||
void Editor::TogglePlay()
|
||||
{
|
||||
if (!m_Ctx.flow) return;
|
||||
IsPlaying() ? m_Ctx.flow->RequestStop() : m_Ctx.flow->RequestStart();
|
||||
}
|
||||
|
||||
// PIE 暂停(UE Pause 语义):Running ↔ Paused。运行态才有意义
|
||||
//(编辑器态本来就冻结,Pause 按钮不出现)
|
||||
void Editor::TogglePause()
|
||||
{
|
||||
if (!m_Ctx.flow) return;
|
||||
IsPaused() ? m_Ctx.flow->RequestResume() : m_Ctx.flow->RequestPause();
|
||||
}
|
||||
|
||||
void Editor::Draw()
|
||||
{
|
||||
// 缩略图缓存推进(解码限量 / 上传就绪 / LRU 淘汰):ImGui::NewFrame
|
||||
// 已由框架 ImGuiLayer 完成,此处处于面板绘制之前
|
||||
EditorThumbnailCache::Get().BeginFrame();
|
||||
|
||||
// 材质球缩略图烘焙:排空引擎异步回读结果 → 注入缩略图缓存(浏览器
|
||||
// 材质条目随后从缓存出图,不再占用引擎预览 tile 池)
|
||||
if (m_Ctx.renderer)
|
||||
{
|
||||
Engine::u64 key = 0;
|
||||
std::vector<unsigned char> pixels;
|
||||
Engine::u32 w = 0, h = 0;
|
||||
while (m_Ctx.renderer->PollMaterialPreviewBake(key, pixels, w, h))
|
||||
{
|
||||
EditorThumbnailCache::Get().CompleteBake(
|
||||
key, pixels.data(), static_cast<int>(w), static_cast<int>(h));
|
||||
}
|
||||
}
|
||||
|
||||
// PIE 镜像同步(面板只读指针消费;权威 = GameFlow,此处适配)
|
||||
if (m_Ctx.flow)
|
||||
{
|
||||
m_PlayingMirror = m_Ctx.flow->IsIn(GameFlowState::Running) ||
|
||||
m_Ctx.flow->IsIn(GameFlowState::Paused);
|
||||
m_PausedMirror = m_Ctx.flow->IsIn(GameFlowState::Paused);
|
||||
}
|
||||
|
||||
// ImGuizmo 每帧首调(官方约定):内部会清 mbOverGizmoHotspot 等
|
||||
// 帧级状态——不调则 hover 过一次杆后 hotspot 永久置位,GetMoveType
|
||||
// 恒返 MT_NONE → 杆可见但永远抓不住(只能看不能拖)
|
||||
ImGuizmo::BeginFrame();
|
||||
|
||||
// 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();
|
||||
DrawToolbar();
|
||||
DrawDockSpace();
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// 常规面板窗口(窗口名 = docking 身份)
|
||||
if (m_ShowHierarchy)
|
||||
{
|
||||
const entt::entity prev = m_Selected;
|
||||
m_Hierarchy->Draw(m_Ctx, m_Selected);
|
||||
// 树点选同步描边(面板直写 m_Selected,不走 Viewport 的
|
||||
// SelectEntity 返回值通道;对比前后变化触发)
|
||||
if (m_Selected != prev) {
|
||||
SelectEntity(m_Selected);
|
||||
}
|
||||
}
|
||||
if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected);
|
||||
if (m_ShowViewport)
|
||||
{
|
||||
if (auto picked = m_Viewport->Draw(m_Ctx, m_VpInput, m_Gizmo,
|
||||
m_Selected)) {
|
||||
SelectEntity(*picked); // Viewport 点击拾取 → 选中联动 Inspector/Hierarchy
|
||||
}
|
||||
}
|
||||
if (m_ShowStats) m_Stats->Draw(m_Ctx);
|
||||
if (m_ShowLog) m_Log->Draw(m_Ctx);
|
||||
m_Gamepad->Draw(m_Ctx); // Tools 弹窗(内部 m_Open 门控)
|
||||
if (m_ShowAssetBrowser) m_AssetBrowser->Draw(m_Ctx);
|
||||
if (m_ShowMaterialEditor) m_MaterialEditor->Draw(m_Ctx);
|
||||
m_ControllerEditor->Draw(m_Ctx); // 面板内部 m_Open 门控
|
||||
|
||||
DrawQuitModal();
|
||||
DrawSceneModals();
|
||||
}
|
||||
|
||||
// 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里
|
||||
// 只决定"剩余输入给不给游戏侧"。UI 被隐藏(反引号)时不会走到
|
||||
// Draw → m_VpInput 停在旧值,谓词调用方(SandboxApp)须先查
|
||||
// GetImGuiEnabled() 再问这里。
|
||||
bool Editor::BlockingKeyboard() const
|
||||
{
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
if (io.WantTextInput) return true; // 打字中(改名等)不让位快捷键
|
||||
|
||||
// PIE 运行态:键盘全给游戏——编辑热键/Gizmo 在运行态本就让位
|
||||
//(ViewportPanel !playing 分支),不再因视口失焦拦截。否则点
|
||||
// Play 后焦点停在工具栏,WASD 整局全死(玩家输入无响应的根因)
|
||||
if (IsPlaying()) return false;
|
||||
|
||||
// 游戏形态(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
|
||||
{
|
||||
// Gizmo 拖拽中:鼠标归操纵杆,不给游戏侧相机(松手自动恢复)
|
||||
if (m_Gizmo.dragging) return true;
|
||||
|
||||
// 游戏形态:沿用 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"))
|
||||
{
|
||||
// 运行态禁存取:Save 会把"玩出来的状态"当编辑结果落盘,
|
||||
// Load 会撕掉快照恢复链(UE 运行态同样禁)
|
||||
ImGui::BeginDisabled(IsPlaying());
|
||||
if (ImGui::MenuItem("Save Scene...", nullptr))
|
||||
{
|
||||
m_ShowSaveModal = true;
|
||||
}
|
||||
if (ImGui::MenuItem("Load Scene...", nullptr))
|
||||
{
|
||||
m_ShowLoadModal = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Quit", "Alt+F4"))
|
||||
{
|
||||
RequestQuit();
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("View"))
|
||||
{
|
||||
ImGui::MenuItem("Outliner", nullptr, &m_ShowHierarchy);
|
||||
ImGui::MenuItem("Details", nullptr, &m_ShowInspector);
|
||||
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
|
||||
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
|
||||
ImGui::MenuItem("Log", nullptr, &m_ShowLog);
|
||||
ImGui::MenuItem("Content Browser", nullptr, &m_ShowAssetBrowser);
|
||||
ImGui::MenuItem("Material Editor", nullptr, &m_ShowMaterialEditor);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Tools"))
|
||||
{
|
||||
ImGui::MenuItem("Gamepad Test", nullptr, &m_Gamepad->m_Open);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::EndMenuBar();
|
||||
}
|
||||
|
||||
// 主工具栏(UE Main Toolbar 语义):菜单栏下方独立一行,Play 组水平
|
||||
// 居中。编辑态 = [Play](绿);PIE 运行态 = [Pause][Stop](暂停高亮黄 /
|
||||
// 停止橙红)。kDisplayDirect 全屏预览是发布形态预览,不在此处
|
||||
void Editor::DrawToolbar()
|
||||
{
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
// Ctrl+P 全局热键:编辑态进 PIE,运行态停回编辑器(双向)。
|
||||
// 文本输入中(改名/路径框)不触发,防打字误切
|
||||
if (io.KeyCtrl && !io.WantTextInput &&
|
||||
ImGui::IsKeyPressed(ImGuiKey_P, false))
|
||||
{
|
||||
TogglePlay();
|
||||
}
|
||||
|
||||
// 按钮加大触感(工具栏行高 = 一个按钮高;DockSpace avail 自适应)
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f, 4.0f));
|
||||
const ImGuiStyle& st = ImGui::GetStyle();
|
||||
const char* pauseLabel = IsPaused() ? "Resume" : "Pause";
|
||||
|
||||
// Play 组宽度 → 水平居中(编辑态 1 钮 / 运行态 2 钮)
|
||||
float groupW = 0.0f;
|
||||
if (IsPlaying())
|
||||
{
|
||||
groupW = ImGui::CalcTextSize(pauseLabel).x +
|
||||
ImGui::CalcTextSize("Stop").x +
|
||||
4.0f * st.FramePadding.x + st.ItemSpacing.x;
|
||||
}
|
||||
else
|
||||
{
|
||||
groupW = ImGui::CalcTextSize("Play").x + 2.0f * st.FramePadding.x;
|
||||
}
|
||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - groupW) * 0.5f);
|
||||
|
||||
if (IsPlaying())
|
||||
{
|
||||
// Pause/Resume:暂停中亮黄常驻(UE 按下态),运行中暗黄
|
||||
ImGui::PushStyleColor(ImGuiCol_Button,
|
||||
IsPaused() ? ImVec4(0.85f, 0.62f, 0.12f, 1.0f)
|
||||
: ImVec4(0.55f, 0.42f, 0.10f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
IsPaused() ? ImVec4(0.92f, 0.70f, 0.18f, 1.0f)
|
||||
: ImVec4(0.65f, 0.50f, 0.14f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||||
ImVec4(0.95f, 0.76f, 0.24f, 1.0f));
|
||||
if (ImGui::Button(pauseLabel))
|
||||
{
|
||||
TogglePause();
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::SetTooltip(IsPaused() ? "Resume simulation"
|
||||
: "Pause simulation");
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
// Stop:橙红(沿用视口 PLAYING 提示色系)
|
||||
ImGui::PushStyleColor(ImGuiCol_Button,
|
||||
ImVec4(0.75f, 0.45f, 0.10f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImVec4(0.85f, 0.55f, 0.15f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||||
ImVec4(0.90f, 0.62f, 0.20f, 1.0f));
|
||||
if (ImGui::Button("Stop"))
|
||||
{
|
||||
TogglePlay();
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::SetTooltip("Stop and restore editor scene (Ctrl+P)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Play:绿(UE Play 按钮语义)
|
||||
ImGui::PushStyleColor(ImGuiCol_Button,
|
||||
ImVec4(0.22f, 0.48f, 0.26f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImVec4(0.28f, 0.58f, 0.32f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||||
ImVec4(0.34f, 0.66f, 0.38f, 1.0f));
|
||||
if (ImGui::Button("Play"))
|
||||
{
|
||||
TogglePlay();
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::SetTooltip("Play in editor (Ctrl+P)");
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// UE5 出厂布局:Details 右列通栏 → Content Browser 底部通栏
|
||||
//(左+中)→ Outliner 左列 → 剩余全给 Viewport(Stats 作其 tab)
|
||||
ImGuiID dockRight = 0, dockBottom = 0, dockLeft = 0, dockCenter = 0;
|
||||
ImGui::DockBuilderSplitNode(dockspaceId, ImGuiDir_Right, 0.26f,
|
||||
&dockRight, &dockCenter);
|
||||
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Down, 0.30f,
|
||||
&dockBottom, &dockCenter);
|
||||
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Left, 0.22f,
|
||||
&dockLeft, &dockCenter);
|
||||
|
||||
ImGui::DockBuilderDockWindow("Outliner", dockLeft);
|
||||
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
|
||||
ImGui::DockBuilderDockWindow("Stats", dockCenter); // Viewport 旁 tab
|
||||
ImGui::DockBuilderDockWindow("Log", dockBottom); // Content Browser 旁 tab
|
||||
ImGui::DockBuilderDockWindow("Content Browser", dockBottom);
|
||||
ImGui::DockBuilderDockWindow("Details", dockRight);
|
||||
ImGui::DockBuilderDockWindow("Material Editor", dockRight); // Details 旁 tab
|
||||
ImGui::DockBuilderDockWindow("Controller Editor", dockRight); // Details 旁 tab
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::DrawSceneModals()
|
||||
{
|
||||
// 保存/加载共用一个路径输入(UE 同款:默认路径记住上次输入)
|
||||
if (m_ShowSaveModal)
|
||||
{
|
||||
ImGui::OpenPopup("Save Scene");
|
||||
m_ShowSaveModal = false;
|
||||
}
|
||||
if (m_ShowLoadModal)
|
||||
{
|
||||
ImGui::OpenPopup("Load Scene");
|
||||
m_ShowLoadModal = false;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(
|
||||
ImGui::GetMainViewport()->GetCenter(),
|
||||
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
|
||||
if (ImGui::BeginPopupModal("Save Scene", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize))
|
||||
{
|
||||
ImGui::TextUnformatted("Scene file path (relative to working dir):");
|
||||
ImGui::SetNextItemWidth(400.0f);
|
||||
ImGui::InputText("##SavePath", m_ScenePathBuf, sizeof(m_ScenePathBuf));
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Save", ImVec2(120, 0)))
|
||||
{
|
||||
DoSaveScene();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(120, 0)))
|
||||
{
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopupModal("Load Scene", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize))
|
||||
{
|
||||
ImGui::TextUnformatted("Scene file path (relative to working dir):");
|
||||
ImGui::SetNextItemWidth(400.0f);
|
||||
ImGui::InputText("##LoadPath", m_ScenePathBuf, sizeof(m_ScenePathBuf));
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Load", ImVec2(120, 0)))
|
||||
{
|
||||
DoLoadScene();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(120, 0)))
|
||||
{
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::DoSaveScene()
|
||||
{
|
||||
const std::string path = m_ScenePathBuf;
|
||||
if (path.empty() || !m_Ctx.scene || !m_Ctx.assets)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// scenes/ 目录不存在时自动创建(空父目录/已存在均合法)
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(
|
||||
std::filesystem::path(path).parent_path(), ec);
|
||||
|
||||
if (Serialization::SceneSerializer::Serialize(*m_Ctx.scene, *m_Ctx.assets,
|
||||
path))
|
||||
{
|
||||
ENGINE_INFO("Editor: scene saved to '{}'", path);
|
||||
}
|
||||
else
|
||||
{
|
||||
ENGINE_ERROR("Editor: failed to save scene to '{}'", path);
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::DoLoadScene()
|
||||
{
|
||||
const std::string path = m_ScenePathBuf;
|
||||
if (path.empty() || !m_Ctx.scene || !m_Ctx.assets)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Serialization::SceneSerializer::Deserialize(*m_Ctx.scene,
|
||||
*m_Ctx.assets, path))
|
||||
{
|
||||
ENGINE_ERROR("Editor: failed to load scene '{}'", path);
|
||||
return;
|
||||
}
|
||||
ENGINE_INFO("Editor: scene loaded from '{}'", path);
|
||||
|
||||
// 旧实体已销毁:清选中(连带清描边,见 SelectEntity)
|
||||
SelectEntity(entt::null);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
//
|
||||
// Editor:docking 编辑器骨架。
|
||||
//
|
||||
// 职责:
|
||||
// - 顶栏菜单(File/View)+ 全屏 DockSpace + 首帧默认布局(DockBuilder)
|
||||
// - 实体选中状态(entt::entity,Hierarchy 写 / Inspector 读)
|
||||
// - 退出确认模态框(窗口关闭 → RequestQuit → Yes 时 Close)
|
||||
//
|
||||
// 面板窗口名(Outliner/Details/Viewport/Stats/Content Browser)即
|
||||
// docking 身份,imgui.ini 持久化布局;默认布局只在 dock node 为空时
|
||||
// 构建一次(旧名 ini 由 Init 一次性迁移删除,见 Editor.cpp)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_EDITOR_H
|
||||
#define HELLOVULKAN_EDITOR_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
#include "panels/ViewportPanel.h" // ViewportInputState(成员按值持有)
|
||||
|
||||
#include <Engine/Core/Types.h> // u32(SelectEntity 位模式转换)
|
||||
#include <Engine/Core/GameFlow.h> // PIE 状态权威(IsPlaying 直查)
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
class HierarchyPanel;
|
||||
class InspectorPanel;
|
||||
class ViewportPanel;
|
||||
class StatsPanel;
|
||||
class LogPanel;
|
||||
class GamepadPanel;
|
||||
class AssetBrowserPanel;
|
||||
class MaterialEditorPanel;
|
||||
class ControllerEditorPanel;
|
||||
|
||||
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 才放行;Gizmo 拖拽中一律拦截
|
||||
// (抓杆时相机不能吃鼠标);游戏形态沿用 WantCaptureMouse
|
||||
bool BlockingMouse() const;
|
||||
|
||||
// 请求退出(弹确认框;窗口关闭事件走这里)
|
||||
void RequestQuit() { m_ShowQuitConfirm = true; }
|
||||
|
||||
// ---- PIE(Play-In-Editor,UE 主工具栏语义)----
|
||||
// 状态权威 = GameFlow(EditorContext::flow);编辑器只是转换的
|
||||
// 发起者(Request*)与监听器(快照/恢复挂在 →Loading/→Stopped)
|
||||
bool IsPlaying() const
|
||||
{
|
||||
return m_Ctx.flow &&
|
||||
(m_Ctx.flow->IsIn(Engine::GameFlowState::Running) ||
|
||||
m_Ctx.flow->IsIn(Engine::GameFlowState::Paused));
|
||||
}
|
||||
bool IsPaused() const
|
||||
{
|
||||
return m_Ctx.flow &&
|
||||
m_Ctx.flow->IsIn(Engine::GameFlowState::Paused);
|
||||
}
|
||||
// Play:RequestStart → →Loading 监听器做快照 + 切 gameView(Viewport
|
||||
// 内嵌 primary 相机渲染,UE PIE 语义;不切全屏)→ 同步加载完成
|
||||
// CompleteLoading → Running 解冻;Stop:RequestStop → →Stopped 监听器
|
||||
// 快照原子换血恢复 → 回编辑器渲染。编辑器态默认冻结(GameFlow 构造
|
||||
// 即 timeScale=0),Play 解冻由 GameFlow 权威处理
|
||||
void TogglePlay();
|
||||
// PIE 暂停切换(UE Pause 语义):Running ↔ Paused(timeScale 由
|
||||
// GameFlow 权威处理)。运行态才有意义(编辑器态本来就冻结,
|
||||
// Pause 按钮不出现)
|
||||
void TogglePause();
|
||||
|
||||
private:
|
||||
// GameFlow 监听器(Init 注册):编辑器快照/恢复挂在此处——
|
||||
// GameFlow 不知场景,转换的副作用由消费端自己挂
|
||||
void OnFlowEnterLoading(); // →Loading:快照 + 切 gameView + 同步完成加载
|
||||
void OnFlowEnterStopped(); // →Stopped:快照换血恢复 + 回编辑器渲染
|
||||
|
||||
// ---- 选中状态 ----
|
||||
// 同步描边:Renderer 落帧后 SelectionMask/Outline 只画选中实体。
|
||||
// nullopt = 清选中——实体 0 号(场景首个创建)位模式 = 0,
|
||||
// 不能拿位值兼作哨兵("第一个模型无描边"的根因)
|
||||
void SelectEntity(entt::entity e)
|
||||
{
|
||||
m_Selected = e;
|
||||
if (m_Ctx.renderer) {
|
||||
m_Ctx.renderer->SetSelectedEntity(
|
||||
e == entt::null
|
||||
? std::optional<Engine::u32>{}
|
||||
: std::optional<Engine::u32>(
|
||||
static_cast<Engine::u32>(entt::to_integral(e))));
|
||||
}
|
||||
}
|
||||
entt::entity GetSelected() const { return m_Selected; }
|
||||
|
||||
private:
|
||||
void DrawMenuBar(); // host 窗口内顶栏(File/View 菜单)
|
||||
void DrawToolbar(); // 菜单栏下方主工具栏(Play/Pause/Stop 居中)
|
||||
void DrawDockSpace(); // 全屏 host + dockspace + 默认布局
|
||||
void DrawQuitModal(); // 退出确认模态框
|
||||
void DrawSceneModals(); // 保存/加载路径模态框(共用一个路径输入)
|
||||
void DoSaveScene(); // 模态确认动作:目录创建 + 序列化落盘
|
||||
void DoLoadScene(); // 模态确认动作:反序列化 + 选中清空 + 环境恢复 + 回调
|
||||
|
||||
EditorContext m_Ctx{};
|
||||
|
||||
ViewportInputState m_VpInput{}; // Viewport 面板 hover/focus(每帧刷新)
|
||||
GizmoState m_Gizmo{}; // 视口操纵杆状态(op/拖拽中)
|
||||
|
||||
std::unique_ptr<HierarchyPanel> m_Hierarchy;
|
||||
std::unique_ptr<InspectorPanel> m_Inspector;
|
||||
std::unique_ptr<ViewportPanel> m_Viewport;
|
||||
std::unique_ptr<StatsPanel> m_Stats;
|
||||
std::unique_ptr<LogPanel> m_Log;
|
||||
std::unique_ptr<GamepadPanel> m_Gamepad;
|
||||
std::unique_ptr<AssetBrowserPanel> m_AssetBrowser;
|
||||
std::unique_ptr<MaterialEditorPanel> m_MaterialEditor;
|
||||
std::unique_ptr<ControllerEditorPanel> m_ControllerEditor;
|
||||
|
||||
entt::entity m_Selected{entt::null};
|
||||
|
||||
// 面板显隐(View 菜单)
|
||||
bool m_ShowHierarchy = true;
|
||||
bool m_ShowInspector = true;
|
||||
bool m_ShowViewport = true;
|
||||
bool m_ShowStats = true;
|
||||
bool m_ShowLog = true;
|
||||
bool m_ShowAssetBrowser = true;
|
||||
bool m_ShowMaterialEditor = true;
|
||||
|
||||
// 默认布局只建一次(imgui.ini 已有布局则跳过)
|
||||
bool m_LayoutBuilt = false;
|
||||
|
||||
// 退出确认链
|
||||
bool m_ShowQuitConfirm = false;
|
||||
bool m_QuitConfirmed = false;
|
||||
|
||||
// 场景保存/加载(File 菜单 → 路径模态框;无文件对话框库,InputText 手输)
|
||||
bool m_ShowSaveModal = false;
|
||||
bool m_ShowLoadModal = false;
|
||||
char m_ScenePathBuf[256] = "scenes/sandbox.scene";
|
||||
|
||||
// PIE 镜像态(面板只读指针消费;权威 = GameFlow,Draw 开头同步)
|
||||
bool m_PlayingMirror = false;
|
||||
bool m_PausedMirror = false;
|
||||
std::string m_PlaySnapshot;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_EDITOR_H
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// EditorColors.h:编辑器面板配色的单一事实来源。
|
||||
//
|
||||
// 资产浏览器条目、Outliner 实体名/组件徽标的着色集中在此:面板不再
|
||||
// 各自散落硬编码 ImVec4。ImVec4 构造为 constexpr(imgui.h),故可用
|
||||
// inline constexpr 常量。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_EDITORCOLORS_H
|
||||
#define HELLOVULKAN_EDITORCOLORS_H
|
||||
|
||||
#include "AssetKind.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace EditorColors {
|
||||
|
||||
// ---- 资产类别色(列表标签 / 名称 / 网格瓦片 / 选中高亮)----
|
||||
inline constexpr ImVec4 kModel = ImVec4(0.36f, 0.62f, 0.90f, 1.0f); // 蓝
|
||||
inline constexpr ImVec4 kTexture = ImVec4(0.34f, 0.78f, 0.52f, 1.0f); // 绿
|
||||
inline constexpr ImVec4 kScene = ImVec4(0.62f, 0.50f, 0.90f, 1.0f); // 紫
|
||||
inline constexpr ImVec4 kMaterial = ImVec4(0.92f, 0.60f, 0.25f, 1.0f); // 橙
|
||||
inline constexpr ImVec4 kAnim = ImVec4(0.30f, 0.76f, 0.76f, 1.0f); // 青
|
||||
inline constexpr ImVec4 kPrefab = ImVec4(0.42f, 0.70f, 0.95f, 1.0f); // 亮蓝
|
||||
inline constexpr ImVec4 kOther = ImVec4(0.72f, 0.72f, 0.75f, 1.0f); // 灰
|
||||
inline constexpr ImVec4 kFolder = ImVec4(0.88f, 0.74f, 0.34f, 1.0f); // 黄
|
||||
|
||||
inline ImVec4 AssetColor(EditorAsset::AssetKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case EditorAsset::AssetKind::Model: return kModel;
|
||||
case EditorAsset::AssetKind::Texture: return kTexture;
|
||||
case EditorAsset::AssetKind::Scene: return kScene;
|
||||
case EditorAsset::AssetKind::Material: return kMaterial;
|
||||
case EditorAsset::AssetKind::AnimClip: return kAnim;
|
||||
case EditorAsset::AssetKind::Prefab: return kPrefab;
|
||||
default: return kOther;
|
||||
}
|
||||
}
|
||||
|
||||
// 向白提亮(保可读性);t = 0 原色,1 全白
|
||||
inline ImVec4 Lighten(const ImVec4& c, float t)
|
||||
{
|
||||
return ImVec4(c.x + (1.0f - c.x) * t, c.y + (1.0f - c.y) * t,
|
||||
c.z + (1.0f - c.z) * t, c.w);
|
||||
}
|
||||
|
||||
// 名称色:类别色提亮(行/瓦片名用)
|
||||
inline ImVec4 AssetNameColor(EditorAsset::AssetKind kind)
|
||||
{
|
||||
return Lighten(AssetColor(kind), 0.40f);
|
||||
}
|
||||
|
||||
// 选中/hover 高亮底色:类别色压暗(ImGuiCol_Header/HeaderHovered)
|
||||
inline ImVec4 AssetHighlightColor(EditorAsset::AssetKind kind)
|
||||
{
|
||||
return ImVec4(AssetColor(kind).x * 0.42f, AssetColor(kind).y * 0.42f,
|
||||
AssetColor(kind).z * 0.42f, 1.0f);
|
||||
}
|
||||
|
||||
// ---- Outliner 组件徽标色 ----
|
||||
inline constexpr ImVec4 kMesh = ImVec4(0.36f, 0.72f, 0.95f, 1.0f);
|
||||
inline constexpr ImVec4 kAnimB = ImVec4(0.95f, 0.72f, 0.35f, 1.0f);
|
||||
inline constexpr ImVec4 kCamera = ImVec4(0.70f, 0.55f, 0.95f, 1.0f);
|
||||
inline constexpr ImVec4 kLight = ImVec4(0.96f, 0.86f, 0.36f, 1.0f);
|
||||
inline constexpr ImVec4 kPhysics = ImVec4(0.45f, 0.85f, 0.55f, 1.0f);
|
||||
inline constexpr ImVec4 kScript = ImVec4(0.90f, 0.55f, 0.76f, 1.0f);
|
||||
inline constexpr ImVec4 kSky = ImVec4(0.46f, 0.80f, 0.92f, 1.0f);
|
||||
// 无组件/兜底实体名色(近白)
|
||||
inline constexpr ImVec4 kEntityNeutral = ImVec4(0.86f, 0.86f, 0.90f, 1.0f);
|
||||
|
||||
} // namespace EditorColors
|
||||
|
||||
#endif //HELLOVULKAN_EDITORCOLORS_H
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// EditorContext:Editor 层与 SandboxApp 之间的桥接上下文。
|
||||
//
|
||||
// Editor 是 App 层代码(不属于引擎),它需要的引擎门面与沙盒能力
|
||||
// 全部经此聚合注入——面板只认识 EditorContext,不反向依赖 main.cpp。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_EDITORCONTEXT_H
|
||||
#define HELLOVULKAN_EDITORCONTEXT_H
|
||||
|
||||
#include <Engine/Core/Application.h>
|
||||
#include <Engine/Core/GameFlow.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Renderer/Renderer.h>
|
||||
#include <Engine/Renderer/EditorCamera.h>
|
||||
#include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
class MaterialEditorPanel; // 面板间联动通道(Details 双击槽位 / Content
|
||||
// Browser 双击 .material → 打开材质编辑器)
|
||||
class ControllerEditorPanel; // Details Controller 槽 Edit → 打开控制器编辑器
|
||||
|
||||
// 沙盒特有状态(面板可选读取,空指针 = 该面板区块不显示)
|
||||
struct EditorContext {
|
||||
// 引擎门面
|
||||
Engine::Renderer *renderer = nullptr;
|
||||
Engine::ECS::Scene *scene = nullptr;
|
||||
Engine::Assets::AssetManager *assets = nullptr;
|
||||
Engine::Application *app = nullptr;
|
||||
// 游戏流状态机(PIE 状态权威;Editor::Init 注册快照/恢复监听)
|
||||
Engine::GameFlow *flow = nullptr;
|
||||
// 编辑器相机(Scene view 专用,归 Renderer 持有——与场景相机实体
|
||||
// 无关;面板经此做 F 焦点/Gizmo 视图/对齐跳转)
|
||||
Engine::EditorCamera *editorCamera = nullptr;
|
||||
// 材质编辑器面板(Editor 持有;指派/编辑分离的联动入口)
|
||||
MaterialEditorPanel *materialEditor = nullptr;
|
||||
// 控制器编辑器面板(Editor 持有;指派/编辑分离的联动入口)
|
||||
ControllerEditorPanel *controllerEditor = nullptr;
|
||||
// 当前选中实体(Controller 编辑器 Preview 的驱动目标)
|
||||
entt::entity *selected = nullptr;
|
||||
|
||||
// PIE 运行态(Play-In-Editor):Editor 持有,只读指针。
|
||||
// 面板门控用——运行态禁 Gizmo/编辑器热键,渲染源已是 primary 相机
|
||||
const bool *playState = nullptr;
|
||||
// PIE 暂停态(timeScale=0):Editor 持有,只读指针(视口 PAUSED 提示)
|
||||
const bool *pausedState = nullptr;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_EDITORCONTEXT_H
|
||||
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// MaterialAssetUtil.h:材质资产编辑共用工具(Inspector / Material Editor
|
||||
// 面板共享)。
|
||||
//
|
||||
// 单一事实来源(原 InspectorPanel.cpp 匿名 ns 实现上提):
|
||||
// - .material 文件枚举缓存(inline 变量跨 TU 唯一——面板各自创建
|
||||
// Override 后缓存失效即时可见)
|
||||
// - Create Override:内建材质快照 → assets/materials/*.material →
|
||||
// GUID 登记 → 同步加载 → 挂实体覆盖表
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_MATERIALASSETUTIL_H
|
||||
#define HELLOVULKAN_MATERIALASSETUTIL_H
|
||||
|
||||
#include "AssetKind.h" // EditorAsset::IsTexturePath(纹理扩展名单一事实来源)
|
||||
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Renderer/MaterialAsset.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace EditorMaterialUtil {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// .material 文件枚举(会话级缓存 + 脏标)。inline 变量 = 所有 TU
|
||||
// 共享同一份(Create Override 新建文件后置脏重扫)
|
||||
inline std::vector<std::string> s_MaterialFiles;
|
||||
inline bool s_MaterialFilesDirty = true;
|
||||
|
||||
inline void InvalidateMaterialFileList() { s_MaterialFilesDirty = true; }
|
||||
|
||||
// 递归收集 assets/ 下纹理文件(材质编辑器纹理槽选择器;纹理文件
|
||||
// 不被编辑器创建,无需脏标)。会话级缓存
|
||||
inline const std::vector<std::string>& AssetTextureFiles()
|
||||
{
|
||||
static std::vector<std::string> s_Files;
|
||||
static bool s_Cached = false;
|
||||
if (!s_Cached)
|
||||
{
|
||||
s_Cached = true;
|
||||
if (fs::exists("assets"))
|
||||
{
|
||||
for (const auto& entry :
|
||||
fs::recursive_directory_iterator("assets"))
|
||||
{
|
||||
if (!entry.is_regular_file())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (EditorAsset::IsTexturePath(entry.path()))
|
||||
{
|
||||
s_Files.push_back(entry.path().generic_string());
|
||||
}
|
||||
}
|
||||
std::sort(s_Files.begin(), s_Files.end());
|
||||
}
|
||||
}
|
||||
return s_Files;
|
||||
}
|
||||
|
||||
inline const std::vector<std::string>& AssetMaterialFiles()
|
||||
{
|
||||
if (s_MaterialFilesDirty)
|
||||
{
|
||||
s_MaterialFilesDirty = false;
|
||||
s_MaterialFiles.clear();
|
||||
if (fs::exists("assets"))
|
||||
{
|
||||
for (const auto& entry :
|
||||
fs::recursive_directory_iterator("assets"))
|
||||
{
|
||||
if (!entry.is_regular_file())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::string ext = entry.path().extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c)
|
||||
{ return std::tolower(c); });
|
||||
if (ext == ".material")
|
||||
{
|
||||
s_MaterialFiles.push_back(
|
||||
entry.path().generic_string());
|
||||
}
|
||||
}
|
||||
std::sort(s_MaterialFiles.begin(), s_MaterialFiles.end());
|
||||
}
|
||||
}
|
||||
return s_MaterialFiles;
|
||||
}
|
||||
|
||||
// Create Override:内建材质快照 → assets/materials/<model>_<mesh>.material
|
||||
// → GUID 登记 → 同步加载 → 挂实体覆盖表。失败返回空串(不挂槽)。
|
||||
// 数据段 = 内建材质只读快照(纹理路径 + 因子 + 透明语义),此后
|
||||
// 该资产独立演化,内建材质不受影响
|
||||
inline std::string CreateMaterialOverride(
|
||||
const std::string& modelPath,
|
||||
const Engine::MeshMaterialInfo& info,
|
||||
Engine::u32 meshIdx,
|
||||
Engine::ECS::MeshRendererComponent& mr,
|
||||
Engine::Assets::AssetManager& assets)
|
||||
{
|
||||
// 目标文件名:<modelStem>_<meshName|idx>.material;mesh 名非
|
||||
// [A-Za-z0-9] 字符折叠 '_',空名回退下标;重名追加 _2/_3/...
|
||||
std::string stem = fs::path(modelPath).stem().string();
|
||||
std::string leaf = info.name;
|
||||
if (leaf.empty())
|
||||
{
|
||||
leaf = std::to_string(meshIdx);
|
||||
}
|
||||
for (char& c : leaf)
|
||||
{
|
||||
if (!std::isalnum(static_cast<unsigned char>(c)))
|
||||
{
|
||||
c = '_';
|
||||
}
|
||||
}
|
||||
|
||||
const fs::path dir = "assets/materials";
|
||||
std::error_code ec;
|
||||
fs::create_directories(dir, ec);
|
||||
if (ec)
|
||||
{
|
||||
ENGINE_WARN("MaterialUtil: cannot create {}: {}", dir.string(),
|
||||
ec.message());
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string base = stem + "_" + leaf;
|
||||
std::string rel = (dir / (base + ".material")).generic_string();
|
||||
for (int n = 2; fs::exists(rel); ++n)
|
||||
{
|
||||
rel = (dir / (base + "_" + std::to_string(n) + ".material"))
|
||||
.generic_string();
|
||||
}
|
||||
|
||||
Engine::MaterialAssetData data;
|
||||
data.texPaths = info.texPaths;
|
||||
data.params = info.params;
|
||||
data.transparent = info.transparent;
|
||||
if (!Engine::MaterialAsset::Save(rel, data))
|
||||
{
|
||||
ENGINE_WARN("MaterialUtil: failed to save material override '{}'",
|
||||
rel);
|
||||
return {};
|
||||
}
|
||||
|
||||
assets.GetOrCreateGUID(rel);
|
||||
if (!assets.LoadMaterialAsset(rel).Valid())
|
||||
{
|
||||
ENGINE_WARN("MaterialUtil: override '{}' saved but failed to load",
|
||||
rel);
|
||||
return {};
|
||||
}
|
||||
mr.materialOverrides[meshIdx] = rel;
|
||||
InvalidateMaterialFileList(); // 新文件进下拉列表
|
||||
return rel;
|
||||
}
|
||||
|
||||
} // namespace EditorMaterialUtil
|
||||
|
||||
#endif //HELLOVULKAN_MATERIALASSETUTIL_H
|
||||
@@ -0,0 +1,341 @@
|
||||
//
|
||||
// ThumbnailCache.cpp:缩略图解碼 + 降采样 + ImGui 用户纹理上传 + LRU 淘汰。
|
||||
//
|
||||
// 生命周期(依赖 vendored ImGui 1.93.0-WIP 语义,与后端/主循环逐帧对齐):
|
||||
// 帧 N :Request 入队 → BeginFrame 解码 → ImTextureData::Create +
|
||||
// RegisterUserTexture(Status=WantCreate,TexID=Invalid)
|
||||
// 帧 N 末:UpdateTexturesEndFrame 把用户纹理并入 PlatformIO.Textures;
|
||||
// ImGui_ImplVulkan_RenderDrawData 内 UpdateTexture 建 VkImage +
|
||||
// 描述符,置 TexID、Status=OK
|
||||
// 帧 N+1:BeginFrame 见 TexID != Invalid → Ready(当帧注册、次帧出图)
|
||||
// 淘汰 :WantDestroyNextFrame=true → 后端 UnusedFrames>=ImageCount 时
|
||||
// 销毁并清 BackendUserData → 下一帧见 Destroyed/无 BackendUserData
|
||||
// → UnregisterUserTexture + IM_DELETE
|
||||
// 关机 :本单例是进程级 function-local static,析构晚于 main 内的
|
||||
// SandboxApp(含 ImGuiLayer)——ImGui_ImplVulkan_Shutdown 已对
|
||||
// RefCount==1 的用户纹理统一释放 GPU;析构仅 IM_DELETE(只
|
||||
// IM_FREE(Pixels),不触任何已销毁的 ImGui 上下文)。
|
||||
//
|
||||
|
||||
#include "ThumbnailCache.h"
|
||||
|
||||
#include <imgui_internal.h> // RegisterUserTexture / UnregisterUserTexture
|
||||
|
||||
#include <stb/stb_image.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kThumbMaxDim = 128; // 缩略图最长边
|
||||
constexpr size_t kMaxEntries = 256; // 缓存上限(≤ 描述符池)
|
||||
constexpr int kMaxDecodesPerFrame = 2; // 每帧最多解码张数
|
||||
constexpr long long kMaxSourcePixels = 4096ll * 4096ll; // 超大源图跳过
|
||||
constexpr int kReadyTimeoutFrames = 3; // 上传超时 → Failed
|
||||
|
||||
// box filter 等比降采样(RGBA8):输出像素在源块内最多取
|
||||
// kMaxSamplesPerAxis² 个采样点(按步长抽样)。大图降采样时避免对
|
||||
// 整块(如 4096² → 128² 时每像素 1024 次读)全量累加——那是单张
|
||||
// 百毫秒级的卡顿源
|
||||
void ResampleBoxRGBA8(const unsigned char* src, int sw, int sh,
|
||||
unsigned char* dst, int dw, int dh)
|
||||
{
|
||||
constexpr int kMaxSamplesPerAxis = 8;
|
||||
for (int y = 0; y < dh; ++y)
|
||||
{
|
||||
const int sy0 = y * sh / dh;
|
||||
int sy1 = (y + 1) * sh / dh;
|
||||
if (sy1 <= sy0) sy1 = sy0 + 1;
|
||||
const int syStep = std::max(
|
||||
1, (sy1 - sy0 + kMaxSamplesPerAxis - 1) / kMaxSamplesPerAxis);
|
||||
for (int x = 0; x < dw; ++x)
|
||||
{
|
||||
const int sx0 = x * sw / dw;
|
||||
int sx1 = (x + 1) * sw / dw;
|
||||
if (sx1 <= sx0) sx1 = sx0 + 1;
|
||||
const int sxStep = std::max(
|
||||
1,
|
||||
(sx1 - sx0 + kMaxSamplesPerAxis - 1) / kMaxSamplesPerAxis);
|
||||
unsigned int r = 0, g = 0, b = 0, a = 0, n = 0;
|
||||
for (int sy = sy0; sy < sy1; sy += syStep)
|
||||
{
|
||||
const unsigned char* row =
|
||||
src + static_cast<size_t>(sy) * sw * 4;
|
||||
for (int sx = sx0; sx < sx1; sx += sxStep)
|
||||
{
|
||||
const unsigned char* p =
|
||||
row + static_cast<size_t>(sx) * 4;
|
||||
r += p[0]; g += p[1]; b += p[2]; a += p[3]; ++n;
|
||||
}
|
||||
}
|
||||
unsigned char* q = dst + (static_cast<size_t>(y) * dw + x) * 4;
|
||||
q[0] = static_cast<unsigned char>(r / n);
|
||||
q[1] = static_cast<unsigned char>(g / n);
|
||||
q[2] = static_cast<unsigned char>(b / n);
|
||||
q[3] = static_cast<unsigned char>(a / n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorThumbnailCache& EditorThumbnailCache::Get()
|
||||
{
|
||||
static EditorThumbnailCache s_Cache;
|
||||
return s_Cache;
|
||||
}
|
||||
|
||||
EditorThumbnailCache::~EditorThumbnailCache()
|
||||
{
|
||||
// 仅回收 CPU 侧(见文件头注释)。勿调 UnregisterUserTexture——
|
||||
// 此刻 ImGui 上下文已随 ImGuiLayer 销毁
|
||||
for (auto& [path, e] : m_Entries)
|
||||
{
|
||||
if (e.tex) IM_DELETE(e.tex);
|
||||
}
|
||||
for (ImTextureData* tex : m_Retire) IM_DELETE(tex);
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::BeginFrame()
|
||||
{
|
||||
++m_Frame;
|
||||
|
||||
DrainRetired();
|
||||
|
||||
// 就绪推进:注册后等后端上传(次帧可见)
|
||||
for (auto& [path, e] : m_Entries)
|
||||
{
|
||||
if (e.state != ThumbView::State::Pending || !e.tex) continue;
|
||||
++e.pendingFrames;
|
||||
if (e.tex->TexID != ImTextureID_Invalid)
|
||||
{
|
||||
e.state = ThumbView::State::Ready;
|
||||
}
|
||||
else if (e.pendingFrames > kReadyTimeoutFrames &&
|
||||
e.tex->Status == ImTextureStatus_Destroyed)
|
||||
{
|
||||
e.state = ThumbView::State::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
// 限量解码(未解码 = tex 为空)
|
||||
int decodes = 0;
|
||||
while (decodes < kMaxDecodesPerFrame && !m_Queue.empty())
|
||||
{
|
||||
const std::string p = m_Queue.front();
|
||||
m_Queue.pop_front();
|
||||
auto it = m_Entries.find(p);
|
||||
if (it == m_Entries.end()) continue; // 已被淘汰
|
||||
if (it->second.tex) continue; // 已解码(防御)
|
||||
DecodeOne(p, it->second);
|
||||
++decodes;
|
||||
}
|
||||
|
||||
// 超上限 → 淘汰最久未用
|
||||
while (m_Entries.size() > kMaxEntries)
|
||||
{
|
||||
if (!RetireOne()) break; // 全在用:本帧放弃
|
||||
}
|
||||
}
|
||||
|
||||
ThumbView EditorThumbnailCache::Request(const std::string& assetPath)
|
||||
{
|
||||
ThumbView v;
|
||||
if (assetPath.empty())
|
||||
{
|
||||
v.state = ThumbView::State::Failed;
|
||||
return v;
|
||||
}
|
||||
|
||||
auto it = m_Entries.find(assetPath);
|
||||
if (it == m_Entries.end())
|
||||
{
|
||||
Entry e;
|
||||
e.lastUseFrame = m_Frame;
|
||||
m_Entries.emplace(assetPath, e);
|
||||
m_Queue.push_back(assetPath);
|
||||
return v; // Pending
|
||||
}
|
||||
|
||||
Entry& e = it->second;
|
||||
e.lastUseFrame = m_Frame;
|
||||
v.state = e.state;
|
||||
v.w = e.w;
|
||||
v.h = e.h;
|
||||
if (e.state == ThumbView::State::Ready && e.tex)
|
||||
{
|
||||
v.tex = e.tex->GetTexRef();
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::UploadRGBA8(const unsigned char* src, int sw, int sh,
|
||||
Entry& e)
|
||||
{
|
||||
// 等比缩放到最长边 ≤ kThumbMaxDim
|
||||
int dw = sw, dh = sh;
|
||||
if (sw >= sh && sw > kThumbMaxDim)
|
||||
{
|
||||
dw = kThumbMaxDim;
|
||||
dh = std::max(1, sh * kThumbMaxDim / sw);
|
||||
}
|
||||
else if (sh > sw && sh > kThumbMaxDim)
|
||||
{
|
||||
dh = kThumbMaxDim;
|
||||
dw = std::max(1, sw * kThumbMaxDim / sh);
|
||||
}
|
||||
|
||||
ImTextureData* tex = IM_NEW(ImTextureData)();
|
||||
tex->Create(ImTextureFormat_RGBA32, dw, dh);
|
||||
if (dw == sw && dh == sh)
|
||||
{
|
||||
std::memcpy(tex->GetPixels(), src, static_cast<size_t>(dw) * dh * 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
ResampleBoxRGBA8(src, sw, sh,
|
||||
static_cast<unsigned char*>(tex->GetPixels()), dw, dh);
|
||||
}
|
||||
|
||||
ImGui::RegisterUserTexture(tex);
|
||||
tex->SetStatus(ImTextureStatus_WantCreate);
|
||||
|
||||
e.tex = tex;
|
||||
e.w = dw;
|
||||
e.h = dh;
|
||||
e.state = ThumbView::State::Pending;
|
||||
e.pendingFrames = 0;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::DecodeOne(const std::string& path, Entry& e)
|
||||
{
|
||||
int sw = 0, sh = 0, ch = 0;
|
||||
if (!stbi_info(path.c_str(), &sw, &sh, &ch) || sw <= 0 || sh <= 0 ||
|
||||
static_cast<long long>(sw) * sh > kMaxSourcePixels)
|
||||
{
|
||||
e.state = ThumbView::State::Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned char* src = stbi_load(path.c_str(), &sw, &sh, &ch, 4);
|
||||
if (!src)
|
||||
{
|
||||
e.state = ThumbView::State::Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
UploadRGBA8(src, sw, sh, e);
|
||||
stbi_image_free(src);
|
||||
e.lastUseFrame = m_Frame;
|
||||
}
|
||||
|
||||
ThumbView EditorThumbnailCache::Peek(const std::string& assetPath)
|
||||
{
|
||||
ThumbView v;
|
||||
auto it = m_Entries.find(assetPath);
|
||||
if (it == m_Entries.end())
|
||||
{
|
||||
v.state = ThumbView::State::Failed; // 缺省 = 未注入
|
||||
return v;
|
||||
}
|
||||
Entry& e = it->second;
|
||||
e.lastUseFrame = m_Frame;
|
||||
v.state = e.state;
|
||||
v.w = e.w;
|
||||
v.h = e.h;
|
||||
if (e.state == ThumbView::State::Ready && e.tex)
|
||||
{
|
||||
v.tex = e.tex->GetTexRef();
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::Invalidate(const std::string& assetPath)
|
||||
{
|
||||
auto it = m_Entries.find(assetPath);
|
||||
if (it == m_Entries.end()) return;
|
||||
if (it->second.tex)
|
||||
{
|
||||
// 交回回收流程(与 LRU 淘汰同一路径:WantDestroyNextFrame →
|
||||
// 后端释放 → DrainRetired 注销 + 回收)
|
||||
it->second.tex->WantDestroyNextFrame = true;
|
||||
m_Retire.push_back(it->second.tex);
|
||||
}
|
||||
m_Entries.erase(it);
|
||||
}
|
||||
|
||||
unsigned long long EditorThumbnailCache::BakeKeyForPath(const std::string& path)
|
||||
{
|
||||
auto it = m_PathToBakeKey.find(path);
|
||||
if (it != m_PathToBakeKey.end()) return it->second;
|
||||
const unsigned long long key = ++m_BakeKeyCounter;
|
||||
m_PathToBakeKey.emplace(path, key);
|
||||
m_BakeKeyToPath.emplace(key, path);
|
||||
return key;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::CompleteBake(unsigned long long key,
|
||||
const unsigned char* rgba, int w, int h)
|
||||
{
|
||||
auto it = m_BakeKeyToPath.find(key);
|
||||
if (it == m_BakeKeyToPath.end()) return; // 已注销(重复回读)
|
||||
const std::string path = it->second;
|
||||
m_BakeKeyToPath.erase(it);
|
||||
m_PathToBakeKey.erase(path);
|
||||
InjectRGBA8(path, rgba, w, h);
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::InjectRGBA8(const std::string& path,
|
||||
const unsigned char* rgba, int w, int h)
|
||||
{
|
||||
if (!rgba || w <= 0 || h <= 0) return;
|
||||
Entry& e = m_Entries[path]; // 材质路径不经解码队列,缺省即新建
|
||||
if (e.tex) return; // 已有纹理(重复注入):忽略
|
||||
UploadRGBA8(rgba, w, h, e);
|
||||
e.lastUseFrame = m_Frame;
|
||||
}
|
||||
|
||||
bool EditorThumbnailCache::RetireOne()
|
||||
{
|
||||
auto worst = m_Entries.end();
|
||||
for (auto it = m_Entries.begin(); it != m_Entries.end(); ++it)
|
||||
{
|
||||
if (it->second.lastUseFrame == m_Frame) continue; // 本帧在用
|
||||
if (worst == m_Entries.end() ||
|
||||
it->second.lastUseFrame < worst->second.lastUseFrame)
|
||||
{
|
||||
worst = it;
|
||||
}
|
||||
}
|
||||
if (worst == m_Entries.end()) return false;
|
||||
|
||||
if (worst->second.tex)
|
||||
{
|
||||
worst->second.tex->WantDestroyNextFrame = true;
|
||||
m_Retire.push_back(worst->second.tex);
|
||||
}
|
||||
m_Entries.erase(worst);
|
||||
return true;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::DrainRetired()
|
||||
{
|
||||
for (size_t i = 0; i < m_Retire.size();)
|
||||
{
|
||||
ImTextureData* tex = m_Retire[i];
|
||||
// 后端销毁后清 BackendUserData 并置 Destroyed → 可安全回收
|
||||
if (tex->BackendUserData == nullptr &&
|
||||
tex->Status == ImTextureStatus_Destroyed)
|
||||
{
|
||||
ImGui::UnregisterUserTexture(tex);
|
||||
IM_DELETE(tex);
|
||||
m_Retire[i] = m_Retire.back();
|
||||
m_Retire.pop_back();
|
||||
}
|
||||
else
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// ThumbnailCache.h:编辑器缩略图缓存(Content Browser / 材质纹理槽共用)。
|
||||
//
|
||||
// 逐帧限量 CPU 解码 + box 降采样 → ImGui 用户纹理(ImTextureData +
|
||||
// RegisterUserTexture)→ 后端建/传 VkImage。零引擎改动。
|
||||
//
|
||||
// 生命周期(详见 .cpp 注释):进程级静态单例,析构晚于 ImGuiLayer,
|
||||
// CPU 侧由静态析构回收、GPU 侧由 ImGui_ImplVulkan_Shutdown 回收。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_THUMBNAILCACHE_H
|
||||
#define HELLOVULKAN_THUMBNAILCACHE_H
|
||||
|
||||
#include <imgui.h> // ImTextureData / ImTextureRef
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// 一次请求的结果:State==Ready 时 tex 可用于 ImGui::Image(tex, size)
|
||||
struct ThumbView {
|
||||
enum class State { Pending, Ready, Failed };
|
||||
State state = State::Pending;
|
||||
ImTextureRef tex{};
|
||||
int w = 0; // 缩略图原始像素尺寸(按比例显示用)
|
||||
int h = 0;
|
||||
};
|
||||
|
||||
class EditorThumbnailCache {
|
||||
public:
|
||||
// 进程级单例(编辑器首帧 BeginFrame 时惰性构造)
|
||||
static EditorThumbnailCache& Get();
|
||||
|
||||
// 每帧一次:Editor::Draw 开头(ImGui::NewFrame 之后、面板绘制之前)
|
||||
void BeginFrame();
|
||||
|
||||
// 幂等请求:命中且就绪即返回;未命中则入队,本帧返回 Pending
|
||||
ThumbView Request(const std::string& assetPath);
|
||||
|
||||
// 只读查状态(**不**触发解码入队):缺省 → Failed。材质球烘焙用——
|
||||
// 材质路径不走 stbi 解码,注入前必须能区分"未注入"与"上传中"
|
||||
//(Pending 表示已注入、等后端建纹理)
|
||||
ThumbView Peek(const std::string& assetPath);
|
||||
|
||||
// 作废某条目的缓存纹理(材质重载/参数编辑后缩略图过期 → 重烘)。
|
||||
// 丢弃 CPU 记录、纹理交回回收流程;随后 Peek 缺省 → Failed
|
||||
void Invalidate(const std::string& assetPath);
|
||||
|
||||
// ---- 材质球烘焙(GPU 回读结果注入)----
|
||||
// path ↔ key 幂等映射:Renderer 只接受/回传 u64 令牌,App 侧登记
|
||||
unsigned long long BakeKeyForPath(const std::string& path);
|
||||
// 回读结果注入:RGBA8 像素(w×h)→ 缩略图纹理(内部等比降到最长边
|
||||
// ≤ kThumbMaxDim)。key 未登记则忽略
|
||||
void CompleteBake(unsigned long long key, const unsigned char* rgba,
|
||||
int w, int h);
|
||||
|
||||
EditorThumbnailCache(const EditorThumbnailCache&) = delete;
|
||||
EditorThumbnailCache& operator=(const EditorThumbnailCache&) = delete;
|
||||
|
||||
private:
|
||||
EditorThumbnailCache() = default;
|
||||
~EditorThumbnailCache();
|
||||
|
||||
struct Entry {
|
||||
ImTextureData* tex = nullptr; // 非拥有(删由回收流程)
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
ThumbView::State state = ThumbView::State::Pending;
|
||||
unsigned long long lastUseFrame = 0;
|
||||
int pendingFrames = 0; // 注册后等待后端上传的帧数
|
||||
};
|
||||
|
||||
void DecodeOne(const std::string& path, Entry& e); // 解码→降采样→注册
|
||||
// 源像素(RGBA8,sw×sh)→ 等比降采样 → ImTextureData 注册(解码与
|
||||
// 烘焙回读注入共用同一条上传路径)
|
||||
static void UploadRGBA8(const unsigned char* src, int sw, int sh, Entry& e);
|
||||
void InjectRGBA8(const std::string& path, const unsigned char* rgba,
|
||||
int w, int h);
|
||||
bool RetireOne(); // 淘汰最久未用(返回是否淘汰)
|
||||
void DrainRetired(); // 后端已释放→回收
|
||||
|
||||
std::unordered_map<std::string, Entry> m_Entries;
|
||||
std::deque<std::string> m_Queue; // 待解码(重复请求命 entries,天然去重)
|
||||
std::vector<ImTextureData*> m_Retire; // 已标 WantDestroy,等后端释放
|
||||
unsigned long long m_Frame = 0;
|
||||
|
||||
// 材质球烘焙 path ↔ key 映射(幂等分配;回读完成即注销)
|
||||
unsigned long long m_BakeKeyCounter = 0;
|
||||
std::unordered_map<std::string, unsigned long long> m_PathToBakeKey;
|
||||
std::unordered_map<unsigned long long, std::string> m_BakeKeyToPath;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_THUMBNAILCACHE_H
|
||||
@@ -0,0 +1,378 @@
|
||||
//
|
||||
// AssetBrowserPanel.cpp:项目资产浏览器实现。
|
||||
//
|
||||
// 条目视觉/拖拽/tooltip 单元住 AssetItemView(列表行与网格瓦片共用同一
|
||||
// 套类别色 + 缩略图);本文件只负责:根切换 / 过滤 / 目录树 / 内容区
|
||||
// 铺排 / 选中态 / 双击分派(打开或实例化)。
|
||||
//
|
||||
|
||||
#include "AssetBrowserPanel.h"
|
||||
|
||||
#include "AssetItemView.h" // 列表行 / 网格瓦片 / 目录条目
|
||||
#include "AssetKind.h" // EditorAsset::AssetKind / KindOfPath
|
||||
#include "MaterialEditorPanel.h" // kMaterialDragPayload + 双击打开材质编辑器
|
||||
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Serialization/SceneSerializer.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
// 固定根:下标与 Editor 无耦合,仅面板内部使用
|
||||
constexpr const char* kRootNames[] = {"Assets", "Scenes"};
|
||||
constexpr const char* kRootPaths[] = {"assets", "scenes"};
|
||||
|
||||
// 类型过滤(下标即身份):0 = All;其余对齐 AssetKind 顺序
|
||||
constexpr const char* kTypeNames[] = {"All", "Models",
|
||||
"Textures", "Scenes",
|
||||
"Materials", "Controllers",
|
||||
"Prefabs"};
|
||||
|
||||
bool PassFilter(int typeFilter, EditorAsset::AssetKind kind)
|
||||
{
|
||||
switch (typeFilter)
|
||||
{
|
||||
case 1: return kind == EditorAsset::AssetKind::Model;
|
||||
case 2: return kind == EditorAsset::AssetKind::Texture;
|
||||
case 3: return kind == EditorAsset::AssetKind::Scene;
|
||||
case 4: return kind == EditorAsset::AssetKind::Material;
|
||||
case 5: return kind == EditorAsset::AssetKind::AnimClip;
|
||||
case 6: return kind == EditorAsset::AssetKind::Prefab;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 相对路径规范化为正斜杠(AssetManager 键与场景文件统一 POSIX 风格)
|
||||
std::string ToAssetPath(const std::filesystem::path& p)
|
||||
{
|
||||
std::string s = p.generic_string();
|
||||
std::replace(s.begin(), s.end(), '\\', '/');
|
||||
return s;
|
||||
}
|
||||
|
||||
// 双击分派:材质/控制器 → 打开编辑器;模型/环境 → 缓存预热;
|
||||
// 场景 → 提示走 File 菜单;预制体 → 快捷实例化(授权位姿落点)
|
||||
void DispatchOpen(const EditorContext& ctx,
|
||||
const std::filesystem::path& path,
|
||||
EditorAsset::AssetKind kind)
|
||||
{
|
||||
const std::string assetPath = path.generic_string();
|
||||
if (kind == EditorAsset::AssetKind::Material && ctx.materialEditor)
|
||||
{
|
||||
ctx.materialEditor->OpenAsset(assetPath);
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::AnimClip && ctx.controllerEditor)
|
||||
{
|
||||
ctx.controllerEditor->Open(assetPath);
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::Model && ctx.assets)
|
||||
{
|
||||
const auto ref = ctx.assets->LoadModel(assetPath);
|
||||
ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})",
|
||||
assetPath, ref.Valid());
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::Texture && ctx.assets)
|
||||
{
|
||||
const auto ref = ctx.assets->LoadEnvironment(assetPath);
|
||||
ENGINE_INFO("AssetBrowser: env '{}' loaded (valid={})",
|
||||
assetPath, ref.Valid());
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::Scene)
|
||||
{
|
||||
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
|
||||
assetPath);
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::Prefab && ctx.scene && ctx.assets)
|
||||
{
|
||||
// 双击 = 快捷实例化(授权位姿落点,选中交给 Outliner 手动改);
|
||||
// 拖拽实例化见 Outliner 落点
|
||||
const auto root = Engine::Serialization::SceneSerializer::
|
||||
InstantiatePrefab(*ctx.scene, *ctx.assets, assetPath);
|
||||
ENGINE_INFO("AssetBrowser: prefab '{}' instantiated (valid={})",
|
||||
assetPath, root != entt::null);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
if (!ImGui::Begin("Content Browser"))
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 顶栏:根切换 + 类型过滤 + 名字过滤 ----
|
||||
if (ImGui::BeginTabBar("##roots"))
|
||||
{
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
if (ImGui::BeginTabItem(kRootNames[i]))
|
||||
{
|
||||
m_Root = i;
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(110.0f);
|
||||
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 7);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(140.0f);
|
||||
ImGui::InputTextWithHint("##name", "filter", m_NameFilter,
|
||||
sizeof(m_NameFilter));
|
||||
ImGui::Separator();
|
||||
|
||||
const std::filesystem::path root(kRootPaths[m_Root]);
|
||||
// 根切换 / 首帧:当前目录回根。前缀比较手写(path::starts_with
|
||||
// 是 libstdc++ 11 才补齐的成员,本机 GCC 没有);补 '/' 分隔校验
|
||||
// 防 "assets_x" 之类误匹配
|
||||
const std::string rootStr = root.generic_string();
|
||||
const std::string curStr = m_CurrentDir.generic_string();
|
||||
const bool under = curStr == rootStr ||
|
||||
(curStr.size() > rootStr.size() &&
|
||||
curStr.compare(0, rootStr.size(), rootStr) == 0 &&
|
||||
curStr[rootStr.size()] == '/');
|
||||
if (m_CurrentDir.empty() || !under)
|
||||
{
|
||||
m_CurrentDir = root;
|
||||
}
|
||||
|
||||
// ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)+ 视图切换 ----
|
||||
if (ImGui::Button("Root"))
|
||||
{
|
||||
m_CurrentDir = root;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
std::string rel = std::filesystem::relative(m_CurrentDir, root)
|
||||
.generic_string();
|
||||
ImGui::TextDisabled("%s/%s", kRootNames[m_Root],
|
||||
rel.empty() ? "" : rel.c_str());
|
||||
|
||||
// 视图切换(列表 / 网格)+ 网格瓦片尺寸
|
||||
ImGui::SameLine();
|
||||
const bool listMode = m_ViewMode == ViewMode::List;
|
||||
if (ImGui::RadioButton("List", listMode))
|
||||
{
|
||||
m_ViewMode = ViewMode::List;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::RadioButton("Grid", !listMode))
|
||||
{
|
||||
m_ViewMode = ViewMode::Grid;
|
||||
}
|
||||
if (m_ViewMode == ViewMode::Grid)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::SliderFloat("##tile", &m_TileSize, 48.0f, 160.0f, "%.0f px");
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- 左:目录树 | 右:当前目录内容(列表 / 网格)----
|
||||
const float treeWidth = 180.0f;
|
||||
ImGui::BeginChild("##tree", ImVec2(treeWidth, 0.0f),
|
||||
ImGuiChildFlags_ResizeX | ImGuiChildFlags_Borders);
|
||||
if (std::filesystem::exists(root))
|
||||
{
|
||||
// 根节点也画进树(可点击选中)
|
||||
ImGui::SetNextItemOpen(true, ImGuiCond_Once);
|
||||
if (ImGui::TreeNodeEx(kRootNames[m_Root],
|
||||
ImGuiTreeNodeFlags_OpenOnArrow))
|
||||
{
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
m_CurrentDir = root;
|
||||
}
|
||||
DrawDirTree(ctx, root);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("(no '%s' dir)", kRootPaths[m_Root]);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginChild("##files", ImVec2(0.0f, 0.0f),
|
||||
ImGuiChildFlags_Borders);
|
||||
if (std::filesystem::exists(m_CurrentDir))
|
||||
{
|
||||
DrawContent(ctx, m_CurrentDir);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void AssetBrowserPanel::DrawDirTree(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir)
|
||||
{
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir))
|
||||
{
|
||||
if (!entry.is_directory())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::string name = entry.path().filename().string();
|
||||
// OpenOnArrow:单击行 = 选中(列表切换),箭头 = 折叠
|
||||
const bool open = ImGui::TreeNodeEx(
|
||||
name.c_str(), ImGuiTreeNodeFlags_OpenOnArrow |
|
||||
ImGuiTreeNodeFlags_SpanFullWidth);
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
m_CurrentDir = entry.path();
|
||||
}
|
||||
if (open)
|
||||
{
|
||||
DrawDirTree(ctx, entry.path());
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AssetBrowserPanel::DrawContent(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// 收集:目录(不受类型过滤影响)+ 过滤后的文件;目录迭代序不稳定 → 排序
|
||||
std::vector<fs::path> dirs;
|
||||
std::vector<std::pair<fs::path, EditorAsset::AssetKind>> files;
|
||||
const std::string filter = m_NameFilter;
|
||||
std::error_code ec;
|
||||
for (const auto& entry : fs::directory_iterator(dir, ec))
|
||||
{
|
||||
if (entry.is_directory())
|
||||
{
|
||||
dirs.push_back(entry.path());
|
||||
continue;
|
||||
}
|
||||
const EditorAsset::AssetKind kind =
|
||||
EditorAsset::KindOfPath(entry.path());
|
||||
if (!PassFilter(m_TypeFilter, kind))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!filter.empty())
|
||||
{
|
||||
const std::string name = entry.path().filename().string();
|
||||
if (name.find(filter) == std::string::npos)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
files.emplace_back(entry.path(), kind);
|
||||
}
|
||||
std::sort(dirs.begin(), dirs.end(),
|
||||
[](const fs::path& a, const fs::path& b)
|
||||
{
|
||||
return a.filename().string() < b.filename().string();
|
||||
});
|
||||
std::sort(files.begin(), files.end(),
|
||||
[](const auto& a, const auto& b)
|
||||
{
|
||||
return a.first.filename().string() <
|
||||
b.first.filename().string();
|
||||
});
|
||||
|
||||
// 双击打开延迟到循环外:Immediate GUI 语义——绘制循环内不改世界状态
|
||||
//(prefab 实例化会改场景,边遍历边打开会让后续条目的项状态错乱)
|
||||
bool pendingOpen = false;
|
||||
std::filesystem::path pendingOpenPath;
|
||||
EditorAsset::AssetKind pendingOpenKind = EditorAsset::AssetKind::Other;
|
||||
|
||||
if (m_ViewMode == ViewMode::List)
|
||||
{
|
||||
for (const auto& d : dirs)
|
||||
{
|
||||
if (EditorAssetView::DrawFolderRow(d))
|
||||
{
|
||||
m_CurrentDir = d;
|
||||
m_SelectedPath.clear();
|
||||
}
|
||||
}
|
||||
for (const auto& [path, kind] : files)
|
||||
{
|
||||
const std::string assetPath = ToAssetPath(path);
|
||||
const auto res = EditorAssetView::DrawRow(
|
||||
ctx, path, assetPath == m_SelectedPath);
|
||||
if (res.clicked)
|
||||
{
|
||||
m_SelectedPath = assetPath;
|
||||
}
|
||||
if (res.doubleClicked)
|
||||
{
|
||||
pendingOpen = true;
|
||||
pendingOpenPath = path;
|
||||
pendingOpenKind = kind;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// ---- 网格视图:固定列宽表格铺排(列数按可用宽度自适应)----
|
||||
const float tile = m_TileSize;
|
||||
const float cellW = tile + ImGui::GetStyle().CellPadding.x * 2.0f;
|
||||
int cols = static_cast<int>(ImGui::GetContentRegionAvail().x / cellW);
|
||||
if (cols < 1)
|
||||
{
|
||||
cols = 1;
|
||||
}
|
||||
if (ImGui::BeginTable("##grid", cols, ImGuiTableFlags_SizingFixedFit))
|
||||
{
|
||||
for (int i = 0; i < cols; ++i)
|
||||
{
|
||||
ImGui::TableSetupColumn("##c",
|
||||
ImGuiTableColumnFlags_WidthFixed,
|
||||
cellW);
|
||||
}
|
||||
for (const auto& d : dirs)
|
||||
{
|
||||
ImGui::TableNextColumn();
|
||||
const std::string assetPath = ToAssetPath(d);
|
||||
if (EditorAssetView::DrawFolderTile(
|
||||
d, tile, assetPath == m_SelectedPath))
|
||||
{
|
||||
m_CurrentDir = d;
|
||||
m_SelectedPath.clear();
|
||||
}
|
||||
}
|
||||
for (const auto& [path, kind] : files)
|
||||
{
|
||||
ImGui::TableNextColumn();
|
||||
const std::string assetPath = ToAssetPath(path);
|
||||
const auto res = EditorAssetView::DrawTile(
|
||||
ctx, path, tile, assetPath == m_SelectedPath);
|
||||
if (res.clicked)
|
||||
{
|
||||
m_SelectedPath = assetPath;
|
||||
}
|
||||
if (res.doubleClicked)
|
||||
{
|
||||
pendingOpen = true;
|
||||
pendingOpenPath = path;
|
||||
pendingOpenKind = kind;
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingOpen)
|
||||
{
|
||||
DispatchOpen(ctx, pendingOpenPath, pendingOpenKind);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// AssetBrowserPanel:项目资产浏览器(Unity Project 窗口简化版)。
|
||||
//
|
||||
// 左侧目录树 + 右侧文件列表(类型过滤 + 名字过滤)。与 AssetManager
|
||||
// 的集成点是"双击预热":model/HDR 进缓存去重(重复双击零成本),
|
||||
// .scene 提示走 File 菜单(加载含环境恢复/选中清理等流程,不在面板
|
||||
// 重复)。拖拽到视口/Inspector 属后续迭代。
|
||||
//
|
||||
// 目录每帧扫描:项目级目录条目有限(几十量级),filesystem 迭代开销
|
||||
// 可忽略;资产规模上来后再加缓存脏标。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASSETBROWSERPANEL_H
|
||||
#define HELLOVULKAN_ASSETBROWSERPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
// Prefab 资产拖拽 payload(Content Browser 拖 .prefab → Outliner 落点
|
||||
// 实例化;字符串 = 资产相对路径)。对齐 kMaterialDragPayload 模式:
|
||||
// 常量住源面板头
|
||||
inline constexpr const char* kPrefabDragPayload = "PREFAB_PATH";
|
||||
|
||||
// 模型资产拖拽 payload(Content Browser 拖 .glb/.fbx/... → Outliner
|
||||
// 落点创建带 MeshRenderer 的实体;字符串 = 资产相对路径)
|
||||
inline constexpr const char* kModelDragPayload = "MODEL_PATH";
|
||||
|
||||
class AssetBrowserPanel {
|
||||
public:
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
private:
|
||||
// 视图模式:列表(行,含内联小缩略图)/ 网格(缩略图瓦片)
|
||||
enum class ViewMode { List, Grid };
|
||||
|
||||
// 目录树节点(递归);右侧内容区画 dir 下的目录+文件
|
||||
void DrawDirTree(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir);
|
||||
void DrawContent(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir);
|
||||
|
||||
// 固定根(下标即身份):项目资产 / 场景文件
|
||||
int m_Root = 0;
|
||||
// 当前浏览目录(点目录树切换;内容区显示这里的条目)
|
||||
std::filesystem::path m_CurrentDir;
|
||||
// 类型过滤(0 = All;其余见 kTypeNames)
|
||||
int m_TypeFilter = 0;
|
||||
// 名字子串过滤(空 = 不过滤)
|
||||
char m_NameFilter[64] = "";
|
||||
// 视图模式 + 网格瓦片尺寸(像素)
|
||||
ViewMode m_ViewMode = ViewMode::List;
|
||||
float m_TileSize = 96.0f;
|
||||
// 当前选中条目(资产相对路径;空 = 无选中)
|
||||
std::string m_SelectedPath;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_ASSETBROWSERPANEL_H
|
||||
@@ -0,0 +1,786 @@
|
||||
//
|
||||
// ControllerEditorPanel 实现:见头文件注释。
|
||||
//
|
||||
|
||||
#include "ControllerEditorPanel.h"
|
||||
|
||||
#include <Engine/Animation/AnimationController.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
ImVec4 WarnCol() { return ImVec4(1.0f, 0.75f, 0.2f, 1.0f); }
|
||||
|
||||
// 模型首个 clip 名(children/树新建时的默认引用;无模型 = 空)
|
||||
std::string FirstClipName(const Engine::Model* model)
|
||||
{
|
||||
if (model == nullptr || model->GetClips().empty()) return "";
|
||||
return model->GetClips()[0].name;
|
||||
}
|
||||
|
||||
// clip 引用编辑:选中实体有模型 → combo(模型 clip 列表);
|
||||
// 无模型 → InputText 手填(clip 缺失 = 该层 rest 兜底,不崩)
|
||||
bool ClipRefEdit(const char* label, std::string& clip,
|
||||
const Engine::Model* model)
|
||||
{
|
||||
if (model == nullptr || model->GetClips().empty())
|
||||
{
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "%s", clip.c_str());
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (ImGui::InputText(label, buf, sizeof(buf)))
|
||||
{
|
||||
clip = buf;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::vector<const char*> names;
|
||||
names.reserve(model->GetClips().size());
|
||||
int sel = -1;
|
||||
for (const auto& c : model->GetClips())
|
||||
{
|
||||
names.push_back(c.name.c_str());
|
||||
if (c.name == clip) sel = static_cast<int>(names.size()) - 1;
|
||||
}
|
||||
if (sel < 0) sel = 0;
|
||||
if (ImGui::Combo(label, &sel, names.data(),
|
||||
static_cast<int>(names.size())))
|
||||
{
|
||||
clip = model->GetClips()[static_cast<size_t>(sel)].name;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ControllerEditorPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
if (!m_Open || m_Path.empty()) return;
|
||||
|
||||
Engine::Animation::AnimationController* ctrl =
|
||||
ctx.assets->FindController(m_Path);
|
||||
if (!ImGui::Begin("Controller Editor", &m_Open))
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
if (ctrl == nullptr)
|
||||
{
|
||||
ImGui::TextColored(WarnCol(), "'%s' failed to load",
|
||||
m_Path.c_str());
|
||||
if (ImGui::Button("Reload"))
|
||||
{
|
||||
ctx.assets->ReloadController(m_Path);
|
||||
}
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 头部:路径 + Save(落盘 + Reload 重编译条件)----
|
||||
ImGui::TextUnformatted(m_Path.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Save"))
|
||||
{
|
||||
if (ctrl->Save(m_Path))
|
||||
{
|
||||
ctx.assets->ReloadController(m_Path);
|
||||
ctrl = ctx.assets->FindController(m_Path);
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("edits apply live; Save writes the file");
|
||||
ImGui::Separator();
|
||||
|
||||
DrawPreview(ctx);
|
||||
ImGui::Separator();
|
||||
DrawStates(ctx, ctrl);
|
||||
ImGui::Separator();
|
||||
DrawTransitions(ctx, ctrl);
|
||||
ImGui::Separator();
|
||||
DrawParams(ctx, ctrl);
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ---- Preview:方向盘(拖动 = SetFloat 到选中实体,摇杆回中)----
|
||||
void ControllerEditorPanel::DrawPreview(const EditorContext& ctx)
|
||||
{
|
||||
// 选中实体的 AnimationComponent(Controller 模式才驱动)
|
||||
Engine::ECS::AnimationComponent* anim = nullptr;
|
||||
const Engine::Model* model = nullptr;
|
||||
if (ctx.selected != nullptr && ctx.scene != nullptr &&
|
||||
ctx.scene->GetRegistry().valid(*ctx.selected))
|
||||
{
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
if (auto* a =
|
||||
reg.try_get<Engine::ECS::AnimationComponent>(*ctx.selected))
|
||||
{
|
||||
anim = a;
|
||||
if (auto* mr =
|
||||
reg.try_get<Engine::ECS::MeshRendererComponent>(
|
||||
*ctx.selected))
|
||||
{
|
||||
if (mr->model.Valid())
|
||||
{
|
||||
model = ctx.assets->GetModel(mr->model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (anim == nullptr)
|
||||
{
|
||||
ImGui::TextDisabled("select an entity with an Animation component "
|
||||
"to preview");
|
||||
return;
|
||||
}
|
||||
|
||||
Engine::Animation::AnimationController* ctrl =
|
||||
anim->controllerPath.empty()
|
||||
? nullptr
|
||||
: ctx.assets->FindController(anim->controllerPath);
|
||||
if (ctrl == nullptr) return;
|
||||
|
||||
// 面板选中状态的 2D 树(预览读出;拖动写的是实体参数)
|
||||
const Engine::Animation::ControllerState* padState = nullptr;
|
||||
const auto& order = ctrl->GetStateOrder();
|
||||
if (m_SelState >= 0 && m_SelState < static_cast<int>(order.size()))
|
||||
{
|
||||
const auto& s = ctrl->GetStates().at(order[static_cast<size_t>(
|
||||
m_SelState)]);
|
||||
if (s.tree.type ==
|
||||
Engine::Animation::BlendTree::Type::Directional2D)
|
||||
{
|
||||
padState = &s;
|
||||
}
|
||||
}
|
||||
if (padState == nullptr)
|
||||
{
|
||||
ImGui::TextDisabled("preview: select a 2DDirectional state");
|
||||
return;
|
||||
}
|
||||
|
||||
const float S = 190.0f;
|
||||
const float rad = S * 0.5f - 14.0f;
|
||||
const ImVec2 org = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 ctr(org.x + S * 0.5f, org.y + S * 0.5f);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddCircleFilled(ctr, rad, IM_COL32(30, 30, 34, 255));
|
||||
dl->AddCircle(ctr, rad, IM_COL32(90, 90, 100, 255), 48, 1.5f);
|
||||
dl->AddLine(ImVec2(ctr.x - rad, ctr.y), ImVec2(ctr.x + rad, ctr.y),
|
||||
IM_COL32(60, 60, 68, 255));
|
||||
dl->AddLine(ImVec2(ctr.x, ctr.y - rad), ImVec2(ctr.x, ctr.y + rad),
|
||||
IM_COL32(60, 60, 68, 255));
|
||||
const auto& ch = padState->tree.children;
|
||||
for (const auto& c : ch)
|
||||
{
|
||||
const bool isOrigin =
|
||||
c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f;
|
||||
const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad);
|
||||
dl->AddCircleFilled(p, isOrigin ? 5.0f : 4.0f,
|
||||
isOrigin ? IM_COL32(90, 140, 255, 255)
|
||||
: IM_COL32(220, 220, 225, 255));
|
||||
}
|
||||
// mode=Direct 的实体不跑状态机——参数写进去没有任何效果,
|
||||
// 直接禁用方向盘(Unity:Animator 组件没挂 Controller 时
|
||||
// 参数面板不可编辑,同语义)
|
||||
const bool isCtrlMode =
|
||||
anim->mode == Engine::ECS::AnimationComponent::AnimMode::Controller;
|
||||
if (!isCtrlMode)
|
||||
{
|
||||
ImGui::TextColored(WarnCol(),
|
||||
"entity is in Direct mode - switch Mode to "
|
||||
"Controller to preview");
|
||||
return;
|
||||
}
|
||||
ImGui::InvisibleButton("##dirpad", ImVec2(S, S));
|
||||
const auto getF = [&](const std::string& n) -> float {
|
||||
auto it = anim->floatParams.find(n);
|
||||
return it != anim->floatParams.end() ? it->second : 0.0f;
|
||||
};
|
||||
float cx = getF(padState->tree.xParam);
|
||||
float cz = getF(padState->tree.zParam);
|
||||
if (ImGui::IsItemActivated())
|
||||
{
|
||||
// 预览闭环(Unity Animator 窗口语义:选中状态即可预览该
|
||||
// 状态)——实体停在别的状态时拖盘自动切到面板选中的 2D
|
||||
// 树状态,否则 SetFloat 对 Single 状态完全无效
|
||||
if (anim->currentState != order[static_cast<size_t>(m_SelState)])
|
||||
{
|
||||
anim->CrossFade(order[static_cast<size_t>(m_SelState)], 0.1f);
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemActive())
|
||||
{
|
||||
const ImVec2 m = ImGui::GetIO().MousePos;
|
||||
float vx = (m.x - ctr.x) / rad;
|
||||
float vz = -(m.y - ctr.y) / rad;
|
||||
const float len = glm::length(glm::vec2(vx, vz));
|
||||
if (len > 1.0f)
|
||||
{
|
||||
vx /= len;
|
||||
vz /= len;
|
||||
}
|
||||
anim->SetFloat(padState->tree.xParam, vx);
|
||||
anim->SetFloat(padState->tree.zParam, vz);
|
||||
cx = vx;
|
||||
cz = vz;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
anim->SetFloat(padState->tree.xParam, 0.0f);
|
||||
anim->SetFloat(padState->tree.zParam, 0.0f);
|
||||
cx = cz = 0.0f;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
const ImVec2 m = ImGui::GetIO().MousePos;
|
||||
for (const auto& c : ch)
|
||||
{
|
||||
const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad);
|
||||
const float dx = m.x - p.x;
|
||||
const float dy = m.y - p.y;
|
||||
if (dx * dx + dy * dy < 64.0f)
|
||||
{
|
||||
ImGui::SetTooltip("%s", c.clip.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const ImVec2 cur(ctr.x + cx * rad, ctr.y - cz * rad);
|
||||
dl->AddCircleFilled(cur, 6.0f, IM_COL32(255, 170, 60, 255));
|
||||
dl->AddCircle(cur, 9.0f, IM_COL32(255, 170, 60, 160), 0, 1.5f);
|
||||
|
||||
// 右侧读出:当前参数 + 选中状态树的权重(运行时同一数学)
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::Text("preview: %s", anim->currentState.c_str());
|
||||
ImGui::Text("%.2f / %.2f", cx, cz);
|
||||
std::vector<std::pair<Engine::u32, float>> ws;
|
||||
Engine::Animation::EvaluateBlendTree2D(padState->tree, cx, cz, ws);
|
||||
std::sort(ws.begin(), ws.end(),
|
||||
[](const auto& a, const auto& b)
|
||||
{ return a.second > b.second; });
|
||||
for (const auto& [idx, w] : ws)
|
||||
{
|
||||
ImGui::Text("%s %.2f", ch[idx].clip.c_str(), w);
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
|
||||
// ---- States:列表(entry ★)+ 选中状态详情 + children 编辑 ----
|
||||
void ControllerEditorPanel::DrawStates(
|
||||
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
|
||||
{
|
||||
if (!ImGui::TreeNodeEx("States", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto& order = ctrl->StateOrderMut();
|
||||
auto& states = ctrl->StatesMut();
|
||||
|
||||
// 选中实体模型(children 的 clip 下拉来源)
|
||||
const Engine::Model* model = nullptr;
|
||||
if (ctx.selected != nullptr && ctx.scene != nullptr &&
|
||||
ctx.scene->GetRegistry().valid(*ctx.selected))
|
||||
{
|
||||
if (auto* mr =
|
||||
ctx.scene->GetRegistry()
|
||||
.try_get<Engine::ECS::MeshRendererComponent>(
|
||||
*ctx.selected))
|
||||
{
|
||||
if (mr->model.Valid())
|
||||
{
|
||||
model = ctx.assets->GetModel(mr->model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 状态列表 + Add
|
||||
int delIdx = -1;
|
||||
for (int i = 0; i < static_cast<int>(order.size()); ++i)
|
||||
{
|
||||
const bool isEntry = order[static_cast<size_t>(i)] ==
|
||||
ctrl->GetEntryState();
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::Selectable(
|
||||
(order[static_cast<size_t>(i)] +
|
||||
std::string(isEntry ? " [entry]" : ""))
|
||||
.c_str(),
|
||||
i == m_SelState))
|
||||
{
|
||||
m_SelState = i;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
{
|
||||
static char s_NewName[64] = "";
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputText("##new_state", s_NewName, sizeof(s_NewName));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0' &&
|
||||
states.find(s_NewName) == states.end())
|
||||
{
|
||||
const std::string n = s_NewName;
|
||||
states.emplace(n, Engine::Animation::ControllerState{n});
|
||||
order.push_back(n);
|
||||
m_SelState = static_cast<int>(order.size()) - 1;
|
||||
s_NewName[0] = '\0';
|
||||
}
|
||||
}
|
||||
if (m_SelState >= static_cast<int>(order.size()))
|
||||
{
|
||||
m_SelState = static_cast<int>(order.size()) - 1;
|
||||
}
|
||||
if (m_SelState < 0 || order.empty())
|
||||
{
|
||||
ImGui::TreePop();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 选中状态详情 ----
|
||||
const std::string selName = order[static_cast<size_t>(m_SelState)];
|
||||
auto it = states.find(selName);
|
||||
if (it == states.end())
|
||||
{
|
||||
ImGui::TreePop();
|
||||
return;
|
||||
}
|
||||
auto& st = it->second;
|
||||
ImGui::Indent();
|
||||
|
||||
ImGui::TextUnformatted("editing:");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(selName.c_str());
|
||||
if (selName != ctrl->GetEntryState() &&
|
||||
ImGui::SmallButton("Set as Entry"))
|
||||
{
|
||||
ctrl->SetEntry(selName);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::PushID("delstate");
|
||||
if (ImGui::SmallButton("x Delete"))
|
||||
{
|
||||
delIdx = m_SelState;
|
||||
}
|
||||
ImGui::PopID();
|
||||
// 重命名(级联:transitions / transitionsOnEnd / entry 引用同步)
|
||||
{
|
||||
static char s_Rename[64] = "";
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputText("##rename", s_Rename, sizeof(s_Rename));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Rename") && s_Rename[0] != '\0' &&
|
||||
states.find(s_Rename) == states.end())
|
||||
{
|
||||
const std::string oldN = selName;
|
||||
const std::string newN = s_Rename;
|
||||
auto node = states.extract(oldN);
|
||||
if (node)
|
||||
{
|
||||
node.key() = newN;
|
||||
states.insert(std::move(node));
|
||||
order[static_cast<size_t>(m_SelState)] = newN;
|
||||
for (auto& t : ctrl->TransitionsMut())
|
||||
{
|
||||
if (t.from == oldN) t.from = newN;
|
||||
if (t.to == oldN) t.to = newN;
|
||||
}
|
||||
for (auto& [n, s] : states)
|
||||
{
|
||||
for (auto& e : s.transitionsOnEnd)
|
||||
{
|
||||
if (e == oldN) e = newN;
|
||||
}
|
||||
}
|
||||
if (ctrl->GetEntryState() == oldN)
|
||||
{
|
||||
ctrl->SetEntry(newN);
|
||||
}
|
||||
s_Rename[0] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 播放分组(speed/loop/播完出口)----
|
||||
if (ImGui::TreeNode("Playback"))
|
||||
{
|
||||
ImGui::DragFloat("speed", &st.speed, 0.05f, 0.1f, 10.0f, "%.2f");
|
||||
ImGui::Checkbox("loop", &st.loop);
|
||||
if (!st.loop)
|
||||
{
|
||||
// 播完出口列表(按序首个可用)+ 下拉追加
|
||||
for (size_t ei = 0; ei < st.transitionsOnEnd.size(); ++ei)
|
||||
{
|
||||
ImGui::PushID(static_cast<int>(ei) + 1000);
|
||||
ImGui::BulletText("%s",
|
||||
st.transitionsOnEnd[ei].c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x"))
|
||||
{
|
||||
st.transitionsOnEnd.erase(
|
||||
st.transitionsOnEnd.begin() +
|
||||
static_cast<long>(ei));
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (!order.empty())
|
||||
{
|
||||
static int s_OnEndSel = 0;
|
||||
s_OnEndSel = std::clamp(
|
||||
s_OnEndSel, 0,
|
||||
static_cast<int>(order.size()) - 1);
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::Combo(
|
||||
"##onend", &s_OnEndSel,
|
||||
[](void* d, int i) -> const char* {
|
||||
return (*static_cast<std::vector<std::string>*>(
|
||||
d))[static_cast<size_t>(i)]
|
||||
.c_str();
|
||||
},
|
||||
&order, static_cast<int>(order.size()));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ exit"))
|
||||
{
|
||||
st.transitionsOnEnd.push_back(
|
||||
order[static_cast<size_t>(s_OnEndSel)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// ---- 混合树分组 ----
|
||||
if (ImGui::TreeNode("Blend Tree"))
|
||||
{
|
||||
using BTType = Engine::Animation::BlendTree::Type;
|
||||
// 树类型:Single 单 clip / 1D 阈值 / 2D 平面方向
|
||||
int tt = static_cast<int>(st.tree.type);
|
||||
if (ImGui::Combo("type", &tt,
|
||||
"Single\0Directional 1D\0Directional 2D\0"))
|
||||
{
|
||||
st.tree.type = static_cast<BTType>(tt);
|
||||
st.tree.children.clear();
|
||||
if (st.tree.type == BTType::Single)
|
||||
{
|
||||
if (st.tree.clip.empty())
|
||||
{
|
||||
st.tree.clip = FirstClipName(model);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认一个原点 child(clip 取模型首个;无模型 = 手填)
|
||||
st.tree.children.push_back(
|
||||
{FirstClipName(model), glm::vec2{0.0f, 0.0f}});
|
||||
}
|
||||
}
|
||||
// x/z 参数绑定(float 参数下拉)
|
||||
{
|
||||
std::vector<const char*> pnames;
|
||||
for (const auto& p : ctrl->GetParams())
|
||||
{
|
||||
if (p.type ==
|
||||
Engine::Animation::ControllerParam::Type::Float)
|
||||
{
|
||||
pnames.push_back(p.name.c_str());
|
||||
}
|
||||
}
|
||||
if (!pnames.empty())
|
||||
{
|
||||
if (st.tree.type != BTType::Single)
|
||||
{
|
||||
int xi = 0;
|
||||
for (int k = 0; k < static_cast<int>(pnames.size());
|
||||
++k)
|
||||
{
|
||||
if (st.tree.xParam ==
|
||||
pnames[static_cast<size_t>(k)])
|
||||
xi = k;
|
||||
}
|
||||
ImGui::SetNextItemWidth(140.0f);
|
||||
if (ImGui::Combo("x param", &xi, pnames.data(),
|
||||
static_cast<int>(pnames.size())))
|
||||
{
|
||||
st.tree.xParam = pnames[static_cast<size_t>(xi)];
|
||||
}
|
||||
}
|
||||
if (st.tree.type == BTType::Directional2D)
|
||||
{
|
||||
int zi = 0;
|
||||
for (int k = 0; k < static_cast<int>(pnames.size());
|
||||
++k)
|
||||
{
|
||||
if (st.tree.zParam ==
|
||||
pnames[static_cast<size_t>(k)])
|
||||
zi = k;
|
||||
}
|
||||
ImGui::SetNextItemWidth(140.0f);
|
||||
if (ImGui::Combo("z param", &zi, pnames.data(),
|
||||
static_cast<int>(pnames.size())))
|
||||
{
|
||||
st.tree.zParam = pnames[static_cast<size_t>(zi)];
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (st.tree.type != BTType::Single)
|
||||
{
|
||||
ImGui::TextColored(
|
||||
WarnCol(), "need a float param for the blend axis");
|
||||
}
|
||||
}
|
||||
if (st.tree.type == BTType::Single)
|
||||
{
|
||||
ClipRefEdit("clip", st.tree.clip, model);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 原点点缺失警告:v 回到中心时方向层权重全被最小阈值滤
|
||||
// 掉,层空 → 引擎侧 fallback 到首个有效 child 单层(不是
|
||||
// T-pose),但"中心=默认动作"的直觉语义要靠原点点承载
|
||||
if (st.tree.type == BTType::Directional2D)
|
||||
{
|
||||
bool hasOrigin = false;
|
||||
for (const auto& c : st.tree.children)
|
||||
{
|
||||
if (c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f)
|
||||
hasOrigin = true;
|
||||
}
|
||||
if (!hasOrigin)
|
||||
{
|
||||
ImGui::TextColored(
|
||||
WarnCol(), "! no origin child (at 0,0) - center "
|
||||
"falls back to first child");
|
||||
}
|
||||
}
|
||||
// children 表:clip + 定位 + 删除(2D = (x,y) 平面定位点;
|
||||
// 1D = x 即阈值)。编辑即时生效(System 下帧直读缓存实例)
|
||||
int delChild = -1;
|
||||
for (int ci = 0;
|
||||
ci < static_cast<int>(st.tree.children.size()); ++ci)
|
||||
{
|
||||
auto& c = st.tree.children[static_cast<size_t>(ci)];
|
||||
ImGui::PushID(ci);
|
||||
ClipRefEdit("##clip", c.clip, model);
|
||||
if (st.tree.type == BTType::Directional2D)
|
||||
{
|
||||
ImGui::SetNextItemWidth(70.0f);
|
||||
ImGui::DragFloat("##atx", &c.pos.x, 0.01f, -1.0f,
|
||||
1.0f, "x %.2f");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(70.0f);
|
||||
ImGui::DragFloat("##aty", &c.pos.y, 0.01f, -1.0f,
|
||||
1.0f, "y %.2f");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::DragFloat("##thr", &c.pos.x, 0.05f, -10.0f,
|
||||
10.0f, "thr %.2f");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x")) delChild = ci;
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (delChild >= 0)
|
||||
{
|
||||
st.tree.children.erase(
|
||||
st.tree.children.begin() + delChild);
|
||||
}
|
||||
if (ImGui::SmallButton("+ Add Child"))
|
||||
{
|
||||
st.tree.children.push_back(
|
||||
{FirstClipName(model),
|
||||
st.tree.type == BTType::Directional2D
|
||||
? glm::vec2{1.0f, 0.0f}
|
||||
: glm::vec2{0.0f, 0.0f}});
|
||||
}
|
||||
}
|
||||
ImGui::TreePop(); // Blend Tree
|
||||
}
|
||||
ImGui::Unindent();
|
||||
|
||||
if (delIdx >= 0)
|
||||
{
|
||||
const std::string name = order[static_cast<size_t>(delIdx)];
|
||||
states.erase(name);
|
||||
order.erase(order.begin() + delIdx);
|
||||
auto& trs = ctrl->TransitionsMut();
|
||||
for (int i = static_cast<int>(trs.size()) - 1; i >= 0; --i)
|
||||
{
|
||||
if (trs[static_cast<size_t>(i)].from == name ||
|
||||
trs[static_cast<size_t>(i)].to == name)
|
||||
{
|
||||
trs.erase(trs.begin() + i);
|
||||
}
|
||||
}
|
||||
for (auto& [n, s] : states)
|
||||
{
|
||||
std::erase_if(s.transitionsOnEnd,
|
||||
[&](const std::string& e)
|
||||
{ return e == name; });
|
||||
}
|
||||
if (ctrl->GetEntryState() == name)
|
||||
{
|
||||
ctrl->SetEntry(order.empty() ? "" : order[0]);
|
||||
}
|
||||
m_SelState = 0;
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// ---- Transitions:行编辑(from/to 下拉 + cond 字符串 Enter 编译)----
|
||||
void ControllerEditorPanel::DrawTransitions(
|
||||
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!ImGui::TreeNode("Transitions")) return;
|
||||
const auto& order = ctrl->GetStateOrder();
|
||||
std::vector<const char*> snames;
|
||||
snames.reserve(order.size() + 1);
|
||||
snames.push_back("*"); // AnyState
|
||||
for (const auto& n : order) snames.push_back(n.c_str());
|
||||
auto stateCombo = [&](const char* label, std::string& cur) {
|
||||
int sel = 0;
|
||||
for (int k = 0; k < static_cast<int>(snames.size()); ++k)
|
||||
{
|
||||
if (cur == snames[static_cast<size_t>(k)]) sel = k;
|
||||
}
|
||||
ImGui::SetNextItemWidth(110.0f);
|
||||
if (ImGui::Combo(label, &sel, snames.data(),
|
||||
static_cast<int>(snames.size())))
|
||||
{
|
||||
cur = snames[static_cast<size_t>(sel)];
|
||||
}
|
||||
};
|
||||
|
||||
int delIdx = -1;
|
||||
int ti = 0;
|
||||
for (auto& t : ctrl->TransitionsMut())
|
||||
{
|
||||
ImGui::PushID(ti++);
|
||||
// 行 1:来源 -> 目标(+ 删除)
|
||||
stateCombo("##from", t.from);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("->");
|
||||
ImGui::SameLine();
|
||||
stateCombo("##to", t.to);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x")) delIdx = ti - 1;
|
||||
// 行 2:条件 + fade + exitTime(缩进——同一过渡的属性组)
|
||||
ImGui::Indent(18.0f);
|
||||
// cond:反推字符串 → Enter(DeactivatedAfterEdit)就地重编译
|
||||
{
|
||||
const std::string s =
|
||||
t.cond.always ? "" :
|
||||
Engine::Animation::AnimationController::CondToString(
|
||||
t.cond);
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof(buf), "%s", s.c_str());
|
||||
ImGui::SetNextItemWidth(170.0f);
|
||||
if (ImGui::InputText("##cond", buf, sizeof(buf),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue) ||
|
||||
ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
t.cond = Engine::Animation::AnimationController::ParseCond(
|
||||
buf);
|
||||
}
|
||||
if (!t.cond.always && !t.cond.valid)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(WarnCol(), "!");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(60.0f);
|
||||
ImGui::DragFloat("##fade", &t.fade, 0.02f, 0.0f, 5.0f, "%.2f");
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("##exit", &t.hasExitTime);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("exit time");
|
||||
ImGui::Unindent(18.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (delIdx >= 0)
|
||||
{
|
||||
auto& trs = ctrl->TransitionsMut();
|
||||
trs.erase(trs.begin() + delIdx);
|
||||
}
|
||||
if (ImGui::SmallButton("+ Add") && !order.empty())
|
||||
{
|
||||
Engine::Animation::Transition tr;
|
||||
tr.from = order[0];
|
||||
tr.to = order[order.size() > 1 ? 1 : 0];
|
||||
ctrl->TransitionsMut().push_back(tr);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// ---- Params:参数声明编辑(禁改名——被 tree/cond 引用,改了断链)----
|
||||
void ControllerEditorPanel::DrawParams(
|
||||
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!ImGui::TreeNode("Params")) return;
|
||||
auto& ps = ctrl->ParamsMut();
|
||||
int delIdx = -1;
|
||||
int pi = 0;
|
||||
for (auto& p : ps)
|
||||
{
|
||||
ImGui::PushID(pi++);
|
||||
ImGui::TextUnformatted(p.name.c_str());
|
||||
ImGui::SameLine();
|
||||
int ty = static_cast<int>(p.type);
|
||||
ImGui::SetNextItemWidth(80.0f);
|
||||
if (ImGui::Combo("##ty", &ty, "float\0bool\0trigger\0"))
|
||||
{
|
||||
p.type =
|
||||
static_cast<Engine::Animation::ControllerParam::Type>(ty);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (p.type ==
|
||||
Engine::Animation::ControllerParam::Type::Float)
|
||||
{
|
||||
ImGui::SetNextItemWidth(90.0f);
|
||||
ImGui::DragFloat("##def", &p.defFloat, 0.05f);
|
||||
}
|
||||
else if (p.type ==
|
||||
Engine::Animation::ControllerParam::Type::Bool)
|
||||
{
|
||||
ImGui::Checkbox("##def", &p.defBool);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x")) delIdx = pi - 1;
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (delIdx >= 0) ps.erase(ps.begin() + delIdx);
|
||||
{
|
||||
static char s_NewName[64] = "";
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputText("##new_param", s_NewName, sizeof(s_NewName));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0')
|
||||
{
|
||||
bool dup = false;
|
||||
for (const auto& p : ps)
|
||||
{
|
||||
if (p.name == s_NewName) dup = true;
|
||||
}
|
||||
if (!dup)
|
||||
{
|
||||
Engine::Animation::ControllerParam d;
|
||||
d.name = s_NewName;
|
||||
ps.push_back(d);
|
||||
s_NewName[0] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// ControllerEditorPanel:动画控制器编辑器窗口(Unity Animator 状态机
|
||||
// 面板范式,交互对齐 MaterialEditorPanel 的"指派/编辑分离")。
|
||||
//
|
||||
// Details 的 Controller 区只做指派(槽/Create/双击打开本面板);
|
||||
// 结构编辑全在这里:
|
||||
// - Preview:方向盘(2D 混合树可视化驱动)+ 权重实时读出,拖动
|
||||
// SetFloat 到当前选中实体(Controller 模式)实时预览
|
||||
// - States:状态列表(增删/重命名/entry),选中状态详情 = 播放
|
||||
// 参数 + 混合树类型 + children 逐行编辑(clip 指定/定位/删除)
|
||||
// - Transitions / Params:列表编辑,cond 字符串 Enter 就地编译
|
||||
//
|
||||
// 编辑直接写缓存资产实例(System 下一帧生效,无需保存);Save 落盘
|
||||
// + ReloadController 重编译(实例换新,本面板每帧重解析不持指针)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_CONTROLLEREDITORPANEL_H
|
||||
#define HELLOVULKAN_CONTROLLEREDITORPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// .animctrl 拖拽 payload(AssetBrowser → Details Controller 槽指派)
|
||||
inline constexpr char kControllerDragPayload[] = "CTRL_PATH";
|
||||
|
||||
class ControllerEditorPanel {
|
||||
public:
|
||||
// 打开 .animctrl 资产(Details 槽位 Edit 按钮 / Content Browser
|
||||
// 双击入口)
|
||||
void Open(const std::string& path)
|
||||
{
|
||||
m_Path = path;
|
||||
m_Open = true;
|
||||
m_SelState = 0;
|
||||
}
|
||||
bool IsOpen() const { return m_Open; }
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
private:
|
||||
void DrawPreview(const EditorContext& ctx);
|
||||
void DrawStates(const EditorContext& ctx,
|
||||
Engine::Animation::AnimationController* ctrl);
|
||||
void DrawTransitions(const EditorContext& ctx,
|
||||
Engine::Animation::AnimationController* ctrl);
|
||||
void DrawParams(const EditorContext& ctx,
|
||||
Engine::Animation::AnimationController* ctrl);
|
||||
|
||||
bool m_Open = false;
|
||||
std::string m_Path;
|
||||
int m_SelState = 0; // 选中状态(StateOrder 下标)
|
||||
std::vector<char> m_CondBuf; // cond 输入缓冲(按行复用)
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_CONTROLLEREDITORPANEL_H
|
||||
@@ -0,0 +1,199 @@
|
||||
//
|
||||
// GamepadPanel.cpp:手柄输入可视化回显(极简手柄视图)。
|
||||
//
|
||||
// 一块画布 = 手柄俯视图:顶部扳机条 + 肩键,左上左摇杆、左下十字键、
|
||||
// 右上 ABXY(Xbox 语义色)、右下右摇杆,中央 Back/Guide/Start 小键。
|
||||
// 按下即亮蓝(面键亮语义色),无数值无卡片——位置即实体,一眼对上
|
||||
// 手里的手柄。数据源 = Window::PollGamepad(GLFW 原始轴值/按键)。
|
||||
//
|
||||
|
||||
#include "GamepadPanel.h"
|
||||
|
||||
#include <Engine/Core/Window.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
// GLFW 标准布局下标
|
||||
enum PadBtn {
|
||||
kA = 0, kB = 1, kX = 2, kY = 3,
|
||||
kLB = 4, kRB = 5, kBack = 6, kStart = 7, kGuide = 8,
|
||||
kLS = 9, kRS = 10, kUp = 11, kRight = 12, kDown = 13, kLeft = 14,
|
||||
};
|
||||
|
||||
constexpr ImU32 kLit = IM_COL32(90, 170, 250, 255); // 按下亮蓝
|
||||
constexpr ImU32 kFill = IM_COL32(52, 52, 60, 255); // 暗态填充
|
||||
constexpr ImU32 kEdge = IM_COL32(104, 104, 114, 255); // 暗态描边
|
||||
constexpr ImU32 kTxt = IM_COL32(215, 215, 220, 255); // 标签文字
|
||||
|
||||
// 块状键(肩键 / Back / Start)
|
||||
void Block(ImDrawList* dl, const ImVec2& c, float w, float h,
|
||||
const char* label, bool pressed)
|
||||
{
|
||||
const ImVec2 mn(c.x - w * 0.5f, c.y - h * 0.5f);
|
||||
const ImVec2 mx(c.x + w * 0.5f, c.y + h * 0.5f);
|
||||
dl->AddRectFilled(mn, mx, pressed ? kLit : kFill, 5.0f);
|
||||
dl->AddRect(mn, mx, pressed ? IM_COL32(160, 210, 255, 255) : kEdge,
|
||||
5.0f);
|
||||
const ImVec2 ts = ImGui::CalcTextSize(label);
|
||||
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f), kTxt, label);
|
||||
}
|
||||
|
||||
// 圆键(Guide)
|
||||
void Circle(ImDrawList* dl, const ImVec2& c, float r, const char* label,
|
||||
bool pressed)
|
||||
{
|
||||
dl->AddCircleFilled(c, r, pressed ? kLit : kFill);
|
||||
dl->AddCircle(c, r, pressed ? IM_COL32(160, 210, 255, 255) : kEdge, 0,
|
||||
1.5f);
|
||||
const ImVec2 ts = ImGui::CalcTextSize(label);
|
||||
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f), kTxt, label);
|
||||
}
|
||||
|
||||
// Xbox 语义色面键
|
||||
void Face(ImDrawList* dl, const ImVec2& c, float r, char label,
|
||||
bool pressed)
|
||||
{
|
||||
ImU32 col;
|
||||
switch (label)
|
||||
{
|
||||
case 'A': col = IM_COL32(32, 150, 40, 255); break;
|
||||
case 'B': col = IM_COL32(200, 45, 45, 255); break;
|
||||
case 'X': col = IM_COL32(50, 110, 220, 255); break;
|
||||
default: col = IM_COL32(225, 185, 40, 255); break;
|
||||
}
|
||||
dl->AddCircleFilled(c, r, pressed ? col : IM_COL32(col & 0xFF,
|
||||
(col >> 8) & 0xFF,
|
||||
(col >> 16) & 0xFF,
|
||||
60));
|
||||
dl->AddCircle(c, r, pressed ? col : kEdge, 0, 2.0f);
|
||||
const ImVec2 ts = ImGui::CalcTextSize(&label, &label + 1);
|
||||
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f),
|
||||
pressed ? IM_COL32(255, 255, 255, 255) : kTxt, &label);
|
||||
}
|
||||
|
||||
// 摇杆画布(底座 + 死区圈 + 轴点)
|
||||
void Stick(ImDrawList* dl, const ImVec2& c, float r, float ax, float ay,
|
||||
float deadzone)
|
||||
{
|
||||
dl->AddCircleFilled(c, r, IM_COL32(32, 32, 37, 255));
|
||||
dl->AddCircle(c, r, kEdge, 0, 1.5f);
|
||||
dl->AddCircle(c, r * deadzone, IM_COL32(90, 140, 210, 110), 0, 1.2f);
|
||||
dl->AddLine(ImVec2(c.x - r * 0.93f, c.y), ImVec2(c.x + r * 0.93f, c.y),
|
||||
IM_COL32(50, 50, 58, 255), 1.0f);
|
||||
dl->AddLine(ImVec2(c.x, c.y - r * 0.93f), ImVec2(c.x, c.y + r * 0.93f),
|
||||
IM_COL32(50, 50, 58, 255), 1.0f);
|
||||
const float mag = std::sqrt(ax * ax + ay * ay);
|
||||
const float cl = (mag > 1.0f) ? 1.0f / mag : 1.0f;
|
||||
const ImVec2 pt(c.x + ax * cl * (r - 8.0f),
|
||||
c.y + ay * cl * (r - 8.0f));
|
||||
if (mag > deadzone)
|
||||
{
|
||||
dl->AddCircleFilled(pt, 10.0f, IM_COL32(90, 170, 250, 70));
|
||||
}
|
||||
dl->AddCircleFilled(pt, 7.0f,
|
||||
mag > deadzone ? kLit
|
||||
: IM_COL32(140, 140, 148, 255));
|
||||
}
|
||||
|
||||
// 十字键(真十字形,按下臂亮)
|
||||
void Dpad(ImDrawList* dl, const ImVec2& c, float arm, float w,
|
||||
const bool* b)
|
||||
{
|
||||
const ImVec2 l0(c.x - arm, c.y - w * 0.5f);
|
||||
const ImVec2 l1(c.x, c.y + w * 0.5f);
|
||||
const ImVec2 r0(c.x, c.y - w * 0.5f);
|
||||
const ImVec2 r1(c.x + arm, c.y + w * 0.5f);
|
||||
const ImVec2 u0(c.x - w * 0.5f, c.y - arm);
|
||||
const ImVec2 u1(c.x + w * 0.5f, c.y);
|
||||
const ImVec2 d0(c.x - w * 0.5f, c.y);
|
||||
const ImVec2 d1(c.x + w * 0.5f, c.y + arm);
|
||||
dl->AddRectFilled(l0, l1, b[kLeft] ? kLit : kFill, 4.0f);
|
||||
dl->AddRectFilled(r0, r1, b[kRight] ? kLit : kFill, 4.0f);
|
||||
dl->AddRectFilled(u0, u1, b[kUp] ? kLit : kFill, 4.0f);
|
||||
dl->AddRectFilled(d0, d1, b[kDown] ? kLit : kFill, 4.0f);
|
||||
dl->AddRect(l0, l1, kEdge, 4.0f);
|
||||
dl->AddRect(r0, r1, kEdge, 4.0f);
|
||||
dl->AddRect(u0, u1, kEdge, 4.0f);
|
||||
dl->AddRect(d0, d1, kEdge, 4.0f);
|
||||
}
|
||||
|
||||
// 线性扳机条(GLFW -1..1 → 0..1 填充)
|
||||
void Trigger(ImDrawList* dl, const ImVec2& mn, const ImVec2& mx,
|
||||
float raw)
|
||||
{
|
||||
dl->AddRectFilled(mn, mx, IM_COL32(32, 32, 37, 255), 4.0f);
|
||||
const float t = (raw + 1.0f) * 0.5f;
|
||||
if (t > 0.02f)
|
||||
{
|
||||
dl->AddRectFilled(mn, ImVec2(mn.x + (mx.x - mn.x) * t, mx.y),
|
||||
IM_COL32(235, 120, 55, 255), 4.0f);
|
||||
}
|
||||
dl->AddRect(mn, mx, kEdge, 4.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void GamepadPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
if (!m_Open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!ImGui::Begin("Gamepad", &m_Open))
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
Engine::GamepadState gp;
|
||||
const bool connected =
|
||||
ctx.app && ctx.app->GetWindow().PollGamepad(gp);
|
||||
|
||||
if (!connected)
|
||||
{
|
||||
ImGui::TextColored(ImVec4(0.60f, 0.60f, 0.60f, 1.0f),
|
||||
"○ Not connected (JOYSTICK_1)");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
ImGui::TextColored(ImVec4(0.40f, 0.85f, 0.40f, 1.0f), "● Connected");
|
||||
ImGui::Spacing();
|
||||
|
||||
// ---- 手柄画布(固定尺寸,元素 = 手柄真实位置;无背景板)----
|
||||
constexpr float kW = 440.0f, kH = 262.0f;
|
||||
constexpr float kDead = 0.15f; // = InputMap kRadialDeadZone
|
||||
ImGui::BeginChild("pad", ImVec2(kW, kH), false);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 o = ImGui::GetWindowPos();
|
||||
auto C = [&](float x, float y) { return ImVec2(o.x + x, o.y + y); };
|
||||
|
||||
// 顶部:线性扳机条 + 肩键
|
||||
Trigger(dl, C(52, 8), C(122, 20), gp.axes[4]); // LT
|
||||
Trigger(dl, C(kW - 122, 8), C(kW - 52, 20), gp.axes[5]); // RT
|
||||
Block(dl, C(78, 40), 76.0f, 20.0f, "LB", gp.buttons[kLB]);
|
||||
Block(dl, C(kW - 78, 40), 76.0f, 20.0f, "RB", gp.buttons[kRB]);
|
||||
|
||||
// 左上:左摇杆|左下:十字键(向右错开——真实手柄摇杆与十字不对齐)
|
||||
Stick(dl, C(76, 118), 46.0f, gp.axes[0], gp.axes[1], kDead);
|
||||
Dpad(dl, C(128, 210), 36.0f, 24.0f, gp.buttons);
|
||||
|
||||
// 中央:Back / Guide / Start
|
||||
Block(dl, C(184, 74), 40.0f, 16.0f, "Back", gp.buttons[kBack]);
|
||||
Circle(dl, C(220, 58), 13.0f, "G", gp.buttons[kGuide]);
|
||||
Block(dl, C(256, 74), 40.0f, 16.0f, "Start", gp.buttons[kStart]);
|
||||
|
||||
// 右上:ABXY 菱形|右下:右摇杆(对称错开,向中心收)
|
||||
const ImVec2 fc = C(360, 118);
|
||||
Face(dl, ImVec2(fc.x, fc.y - 24), 17.0f, 'Y', gp.buttons[kY]);
|
||||
Face(dl, ImVec2(fc.x - 24, fc.y), 17.0f, 'X', gp.buttons[kX]);
|
||||
Face(dl, ImVec2(fc.x + 24, fc.y), 17.0f, 'B', gp.buttons[kB]);
|
||||
Face(dl, ImVec2(fc.x, fc.y + 24), 17.0f, 'A', gp.buttons[kA]);
|
||||
Stick(dl, C(312, 210), 46.0f, gp.axes[2], gp.axes[3], kDead);
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// GamepadPanel:手柄输入可视化回显(Tools 菜单弹出窗,非 dock 常驻)。
|
||||
//
|
||||
// 三块:摇杆画布(原始轴值 + 引擎死区参考圈)/ 按键网格(按下高亮)/
|
||||
// 动作层回显(引擎死区+钳制后脚本真正拿到的 Move/Look/Jump)。
|
||||
// 测试用途:不接手柄时显示未连接状态。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_GAMEPADPANEL_H
|
||||
#define HELLOVULKAN_GAMEPADPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
class GamepadPanel {
|
||||
public:
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
bool m_Open = false; // Tools 菜单开关(窗口 X 也能关)
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_GAMEPADPANEL_H
|
||||
@@ -0,0 +1,361 @@
|
||||
//
|
||||
// HierarchyPanel.cpp:实体层级树实现(UE Outliner 语义)。
|
||||
//
|
||||
// 树形展示(根 = 无有效父链接的实体,子 = HierarchyComponent 挂载序)、
|
||||
// 点击选中、拖拽改父/拖到空白区挂根(keepWorld = 挂载前后世界位姿
|
||||
// 不变)、右键 Create Child / Detach / Delete(级联随葬,延迟销毁)。
|
||||
//
|
||||
|
||||
#include "HierarchyPanel.h"
|
||||
#include "AssetBrowserPanel.h" // kPrefabDragPayload(Content Browser 源)
|
||||
#include "EditorColors.h" // 实体名/组件徽标配色
|
||||
#include "Engine/ECS/Components.h"
|
||||
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
|
||||
#include "Engine/Serialization/SceneSerializer.h"
|
||||
#include "Engine/Core/Log.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace Engine;
|
||||
|
||||
namespace {
|
||||
|
||||
// 拖拽 payload 类型名(Editor 内部约定;实体位模式 4 字节)
|
||||
constexpr const char* kHierPayload = "HIER_ENTITY";
|
||||
|
||||
// 实体显示名
|
||||
std::string DisplayName(const entt::registry& reg, entt::entity e)
|
||||
{
|
||||
if (const auto* n = reg.try_get<Engine::ECS::NameComponent>(e);
|
||||
n && !n->name.empty()) {
|
||||
return n->name;
|
||||
}
|
||||
return "(unnamed)";
|
||||
}
|
||||
|
||||
// Prefab 拖入实例化(树节点/空白区落点共用)。根实体挂在场景根、
|
||||
// 保留授权位姿;返回根实体(失败 entt::null)
|
||||
entt::entity InstantiatePrefabDrop(const EditorContext& ctx,
|
||||
const char* path)
|
||||
{
|
||||
if (!ctx.scene || !ctx.assets || !path) {
|
||||
return entt::null;
|
||||
}
|
||||
return Engine::Serialization::SceneSerializer::InstantiatePrefab(
|
||||
*ctx.scene, *ctx.assets, path);
|
||||
}
|
||||
|
||||
// 模型拖入落点:创建带 MeshRenderer 的单实体(名字 = 文件名
|
||||
// stem,默认位姿;模型句柄 = LoadModel 磁盘路径)。拖到节点下 =
|
||||
// SetParent keepWorld;空白区 = 场景根。加载失败不建空实体
|
||||
entt::entity SpawnModelDrop(const EditorContext& ctx, const char* path)
|
||||
{
|
||||
if (!ctx.scene || !ctx.assets || !path) {
|
||||
return entt::null;
|
||||
}
|
||||
const auto model = ctx.assets->LoadModel(path);
|
||||
if (!model.Valid()) {
|
||||
return entt::null;
|
||||
}
|
||||
std::string name =
|
||||
std::filesystem::path(path).stem().string();
|
||||
if (name.empty()) {
|
||||
name = "model entity";
|
||||
}
|
||||
auto e = ctx.scene->CreateEntity(name);
|
||||
e.AddComponent<Engine::ECS::MeshRendererComponent>().model = model;
|
||||
ENGINE_INFO("Hierarchy: model entity '{}' created from '{}'",
|
||||
name, path);
|
||||
return static_cast<entt::entity>(e);
|
||||
}
|
||||
|
||||
// 主组件 → 实体名颜色(优先级高者胜,用于 Outliner 可读性)
|
||||
ImVec4 PrimaryColor(const entt::registry& reg, entt::entity e)
|
||||
{
|
||||
using namespace Engine::ECS;
|
||||
if (reg.all_of<CameraComponent>(e)) return EditorColors::kCamera;
|
||||
if (reg.all_of<LightComponent>(e)) return EditorColors::kLight;
|
||||
if (reg.all_of<SkyboxComponent>(e) ||
|
||||
reg.all_of<SkyLightComponent>(e)) return EditorColors::kSky;
|
||||
if (reg.all_of<MeshRendererComponent>(e)) return EditorColors::kMesh;
|
||||
if (reg.all_of<AnimationComponent>(e)) return EditorColors::kAnimB;
|
||||
if (reg.all_of<ScriptComponent>(e)) return EditorColors::kScript;
|
||||
if (reg.all_of<RigidBodyComponent>(e) ||
|
||||
reg.all_of<CharacterControllerComponent>(e))
|
||||
{
|
||||
return EditorColors::kPhysics;
|
||||
}
|
||||
return EditorColors::kEntityNeutral;
|
||||
}
|
||||
|
||||
// 实体名 → 合法文件名(路径分隔符/保留符净化;空名兜底)
|
||||
std::string PrefabFileName(std::string name)
|
||||
{
|
||||
for (char& c : name) {
|
||||
if (c == '/' || c == '\\' || c == ':' || c == '*' ||
|
||||
c == '?' || c == '"' || c == '<' || c == '>' ||
|
||||
c == '|') {
|
||||
c = '_';
|
||||
}
|
||||
}
|
||||
if (name.empty() || name == "(unnamed)") {
|
||||
name = "prefab";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// 递归绘制单节点(含子树)。返回值无意义,选中写 selected
|
||||
void DrawEntityNode(const EditorContext& ctx, entt::entity e,
|
||||
entt::entity& selected)
|
||||
{
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
|
||||
// 稳定行 ID(UUID):折叠态跨帧保持 + 与 Inspector PushID 隔离
|
||||
u64 rowId = static_cast<u64>(static_cast<u32>(e));
|
||||
if (const auto* id = reg.try_get<Engine::ECS::IdComponent>(e)) {
|
||||
rowId = id->uuid.Value();
|
||||
}
|
||||
ImGui::PushID(reinterpret_cast<const void*>(rowId));
|
||||
|
||||
// 子表(过滤悬空 + 无 Name 的防御性兜底)
|
||||
const auto* h = reg.try_get<Engine::ECS::HierarchyComponent>(e);
|
||||
std::vector<entt::entity> kids;
|
||||
if (h) {
|
||||
for (entt::entity c : h->children) {
|
||||
if (reg.valid(c) && reg.all_of<Engine::ECS::NameComponent>(c)) {
|
||||
kids.push_back(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string label = DisplayName(reg, e);
|
||||
const ImGuiTreeNodeFlags flags =
|
||||
ImGuiTreeNodeFlags_OpenOnArrow |
|
||||
ImGuiTreeNodeFlags_SpanAvailWidth |
|
||||
(kids.empty() ? ImGuiTreeNodeFlags_Leaf : 0) |
|
||||
(selected == e ? ImGuiTreeNodeFlags_Selected : 0);
|
||||
|
||||
// 实体名按主组件着色(无组件 = 中性色)
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, PrimaryColor(reg, e));
|
||||
const bool opened = ImGui::TreeNodeEx(label.c_str(), flags);
|
||||
ImGui::PopStyleColor();
|
||||
const bool toggled = ImGui::IsItemToggledOpen();
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && !toggled) {
|
||||
selected = e;
|
||||
}
|
||||
|
||||
// ---- 拖拽源:拖动本实体去改父 ----
|
||||
if (ImGui::BeginDragDropSource()) {
|
||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||
ImGui::SetDragDropPayload(kHierPayload, &bits, sizeof(bits));
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变);
|
||||
// Prefab 拖入 = 实例化子树后挂到本实体下(keepWorld 保持
|
||||
// 授权世界位姿)----
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kHierPayload)) {
|
||||
u32 bits = 0;
|
||||
std::memcpy(&bits, pl->Data, sizeof(bits));
|
||||
const entt::entity dragged = static_cast<entt::entity>(bits);
|
||||
// 拒挂自身/自身后代(SetParent 内部同样防环,这里提前
|
||||
// 静默拦截,避免"看起来挂上了"的错觉)
|
||||
if (dragged != e && !ctx.scene->IsDescendantOf(dragged, e)) {
|
||||
ctx.scene->SetParent(dragged, e);
|
||||
}
|
||||
}
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
|
||||
if (const entt::entity root =
|
||||
InstantiatePrefabDrop(ctx,
|
||||
static_cast<const char*>(
|
||||
pl->Data));
|
||||
root != entt::null) {
|
||||
ctx.scene->SetParent(root, e); // keepWorld 默认 true
|
||||
selected = root;
|
||||
}
|
||||
}
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
|
||||
if (const entt::entity me = SpawnModelDrop(
|
||||
ctx, static_cast<const char*>(pl->Data));
|
||||
me != entt::null) {
|
||||
ctx.scene->SetParent(me, e); // keepWorld 默认 true
|
||||
selected = me;
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
// ---- 右键菜单 ----
|
||||
if (ImGui::BeginPopupContextItem("hier_ctx")) {
|
||||
if (ImGui::MenuItem("Create Child")) {
|
||||
// keepWorld=true:新实体出现在父的位置(UE attach 语义)
|
||||
const auto child = ctx.scene->CreateEntity("child entity");
|
||||
ctx.scene->SetParent(child, e);
|
||||
selected = child;
|
||||
}
|
||||
if (ImGui::MenuItem("Save As Prefab")) {
|
||||
// 固定目录 + 实体名派生文件名(无文件对话框库,与场景
|
||||
// File 菜单同策略;重名覆盖,改名走文件系统)
|
||||
const std::string path =
|
||||
"assets/prefabs/" +
|
||||
PrefabFileName(DisplayName(reg, e)) + ".prefab";
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories("assets/prefabs", ec);
|
||||
if (Serialization::SceneSerializer::SerializePrefab(
|
||||
*ctx.scene, *ctx.assets, e, path)) {
|
||||
ENGINE_INFO("Hierarchy: prefab saved to '{}'", path);
|
||||
}
|
||||
}
|
||||
if (ctx.scene->GetParent(e) != entt::null &&
|
||||
ImGui::MenuItem("Detach to Root")) {
|
||||
ctx.scene->SetParent(e, entt::null);
|
||||
}
|
||||
if (ImGui::MenuItem("Delete")) {
|
||||
// 级联随葬:选中若是被删子树的一部分,一并清除
|
||||
const bool selInSubtree = ctx.scene->IsDescendantOf(selected, e);
|
||||
ctx.scene->DestroyEntity(e);
|
||||
if (selInSubtree) {
|
||||
selected = entt::null;
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// ---- 组件徽标(逐组件着色,与行内同级显示)----
|
||||
bool anyBadge = false;
|
||||
auto badge = [&](const char* text, const ImVec4& col)
|
||||
{
|
||||
ImGui::SameLine(0.0f, 6.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, col);
|
||||
ImGui::TextUnformatted(text);
|
||||
ImGui::PopStyleColor();
|
||||
anyBadge = true;
|
||||
};
|
||||
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e))
|
||||
badge("M", EditorColors::kMesh);
|
||||
if (reg.all_of<Engine::ECS::AnimationComponent>(e))
|
||||
badge("A", EditorColors::kAnimB);
|
||||
if (reg.all_of<Engine::ECS::CameraComponent>(e))
|
||||
badge("C", EditorColors::kCamera);
|
||||
if (reg.all_of<Engine::ECS::LightComponent>(e))
|
||||
badge("L", EditorColors::kLight);
|
||||
if (reg.all_of<Engine::ECS::RigidBodyComponent>(e) ||
|
||||
reg.all_of<Engine::ECS::CharacterControllerComponent>(e))
|
||||
badge("P", EditorColors::kPhysics);
|
||||
if (reg.all_of<Engine::ECS::ScriptComponent>(e))
|
||||
badge("S", EditorColors::kScript);
|
||||
if (reg.all_of<Engine::ECS::SkyboxComponent>(e) ||
|
||||
reg.all_of<Engine::ECS::SkyLightComponent>(e))
|
||||
badge("K", EditorColors::kSky);
|
||||
if (anyBadge && ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"M=Mesh A=Animation C=Camera L=Light P=Physics "
|
||||
"S=Script K=Sky");
|
||||
}
|
||||
|
||||
if (opened) {
|
||||
for (entt::entity c : kids) {
|
||||
DrawEntityNode(ctx, c, selected);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
|
||||
{
|
||||
ImGui::Begin("Outliner");
|
||||
|
||||
// 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构)。
|
||||
// Place Actors 简化语义:常用类型一键带组件,其余 Add Component 补
|
||||
if (ImGui::Button("+ Create"))
|
||||
{
|
||||
ImGui::OpenPopup("create_entity");
|
||||
}
|
||||
if (ImGui::BeginPopup("create_entity"))
|
||||
{
|
||||
if (ImGui::MenuItem("Empty"))
|
||||
{
|
||||
selected = ctx.scene->CreateEntity("Entity");
|
||||
}
|
||||
if (ImGui::MenuItem("Light"))
|
||||
{
|
||||
auto e = ctx.scene->CreateEntity("Light");
|
||||
e.AddComponent<Engine::ECS::LightComponent>();
|
||||
selected = e;
|
||||
}
|
||||
if (ImGui::MenuItem("Camera"))
|
||||
{
|
||||
auto e = ctx.scene->CreateEntity("Camera");
|
||||
e.AddComponent<Engine::ECS::CameraComponent>();
|
||||
selected = e;
|
||||
}
|
||||
if (ImGui::MenuItem("Environment"))
|
||||
{
|
||||
// UE 新关卡环境集简化:Skybox(背景)+ SkyLight(环境光)一体
|
||||
auto e = ctx.scene->CreateEntity("Environment");
|
||||
e.AddComponent<Engine::ECS::SkyboxComponent>();
|
||||
e.AddComponent<Engine::ECS::SkyLightComponent>();
|
||||
selected = e;
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
|
||||
// 树形遍历:根 = 无有效父链接的实体(creation 序),子按挂载序递归
|
||||
reg.view<Engine::ECS::NameComponent>().each(
|
||||
[&](const entt::entity e, const Engine::ECS::NameComponent&)
|
||||
{
|
||||
if (ctx.scene->GetParent(e) != entt::null) return; // 非根
|
||||
DrawEntityNode(ctx, e, selected);
|
||||
});
|
||||
|
||||
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层);
|
||||
// Prefab 拖入 = 实例化到场景根(授权位姿)----
|
||||
ImVec2 rem = ImGui::GetContentRegionAvail();
|
||||
if (rem.y > 4.0f) {
|
||||
ImGui::InvisibleButton("##hier_drop_root", rem);
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kHierPayload)) {
|
||||
u32 bits = 0;
|
||||
std::memcpy(&bits, pl->Data, sizeof(bits));
|
||||
ctx.scene->SetParent(static_cast<entt::entity>(bits),
|
||||
entt::null);
|
||||
}
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
|
||||
if (const entt::entity root = InstantiatePrefabDrop(
|
||||
ctx, static_cast<const char*>(pl->Data));
|
||||
root != entt::null) {
|
||||
selected = root;
|
||||
}
|
||||
}
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
|
||||
if (const entt::entity me = SpawnModelDrop(
|
||||
ctx, static_cast<const char*>(pl->Data));
|
||||
me != entt::null) {
|
||||
selected = me;
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// HierarchyPanel:实体层级树面板。
|
||||
//
|
||||
// 树形展示父子层级(UE Outliner)、点击选中、拖拽改父/拖到空白挂根
|
||||
//(keepWorld)、右键 Create Child / Detach / Delete(级联随葬,帧尾
|
||||
// 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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,152 @@
|
||||
//
|
||||
// LogPanel.cpp:运行时日志面板实现。
|
||||
//
|
||||
// 数据源 = Engine::Debug::LogRing(spdlog ring sink 落的整行)。
|
||||
// 级别着色(trace 灰 / debug 青 / info 浅白 / warn 黄 / error 红 /
|
||||
// fatal 亮红带背景条)+ 级别过滤 + 文本过滤 + Auto-scroll(滚动条
|
||||
// 贴底才自动拉,上翻看历史不被拽回)。
|
||||
//
|
||||
|
||||
#include "LogPanel.h"
|
||||
|
||||
#include <Engine/Core/LogRing.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
void LogPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!ImGui::Begin("Log"))
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 工具栏:清空 / 自动滚动 / 文本过滤 / 级别过滤 ----
|
||||
if (ImGui::Button("Clear"))
|
||||
{
|
||||
Engine::Debug::LogRing().Clear();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Auto-scroll", &m_AutoScroll);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
ImGui::InputTextWithHint("##filter", "filter...", m_Filter, sizeof(m_Filter));
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Trace", &m_ShowTrace);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Info", &m_ShowInfo);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Warn", &m_ShowWarn);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Error", &m_ShowError);
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- 版本感知:有新日志且滚动条贴底 → 拉底(上翻历史不拽回)----
|
||||
const Engine::u64 ver = Engine::Debug::LogRing().Version();
|
||||
bool scrollToBottom = false;
|
||||
if (ver != m_LastVersion)
|
||||
{
|
||||
m_LastVersion = ver;
|
||||
if (m_AutoScroll)
|
||||
{
|
||||
scrollToBottom = ImGui::GetScrollY() >=
|
||||
ImGui::GetScrollMaxY() - 24.0f;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::BeginChild("loglist", {0.0f, 0.0f}, false,
|
||||
ImGuiWindowFlags_HorizontalScrollbar))
|
||||
{
|
||||
const auto entries = Engine::Debug::LogRing().Snapshot();
|
||||
for (const auto& e : entries)
|
||||
{
|
||||
using lv = spdlog::level::level_enum;
|
||||
bool show = false;
|
||||
switch (e.level)
|
||||
{
|
||||
case lv::trace:
|
||||
case lv::debug: show = m_ShowTrace; break;
|
||||
case lv::info: show = m_ShowInfo; break;
|
||||
case lv::warn: show = m_ShowWarn; break;
|
||||
case lv::err:
|
||||
case lv::critical: show = m_ShowError; break;
|
||||
default: show = m_ShowInfo; break;
|
||||
}
|
||||
if (!show) continue;
|
||||
if (m_Filter[0] != '\0')
|
||||
{
|
||||
// 简单子串过滤(大小写不敏感)
|
||||
std::string lower;
|
||||
lower.reserve(e.msg.size());
|
||||
for (char c : e.msg)
|
||||
{
|
||||
lower.push_back(static_cast<char>(
|
||||
std::tolower(static_cast<unsigned char>(c))));
|
||||
}
|
||||
std::string needle;
|
||||
needle.reserve(strlen(m_Filter));
|
||||
for (const char* p = m_Filter; *p; ++p)
|
||||
{
|
||||
needle.push_back(static_cast<char>(
|
||||
std::tolower(static_cast<unsigned char>(*p))));
|
||||
}
|
||||
if (lower.find(needle) == std::string::npos)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 渲染:级别徽标(恒彩色,固定列)+ 消息本体 ----
|
||||
// 消息色只有 warn/error/fatal 染色(info 白是惯例),颜色
|
||||
// 辨识主要靠徽标列——UE Output Log 的 Verbosity 列同位
|
||||
ImVec4 tagCol, msgCol;
|
||||
const char* tag = "INFO";
|
||||
bool bg = false;
|
||||
switch (e.level)
|
||||
{
|
||||
case lv::trace:
|
||||
tag = "TRACE"; tagCol = {0.55f, 0.55f, 0.55f, 1.0f};
|
||||
msgCol = {0.55f, 0.55f, 0.55f, 1.0f}; break;
|
||||
case lv::debug:
|
||||
tag = "DEBUG"; tagCol = {0.40f, 0.80f, 0.90f, 1.0f};
|
||||
msgCol = {0.70f, 0.70f, 0.70f, 1.0f}; break;
|
||||
case lv::info:
|
||||
tag = "INFO"; tagCol = {0.45f, 0.72f, 0.95f, 1.0f};
|
||||
msgCol = {0.90f, 0.90f, 0.90f, 1.0f}; break;
|
||||
case lv::warn:
|
||||
tag = "WARN"; tagCol = {1.00f, 0.78f, 0.20f, 1.0f};
|
||||
msgCol = {1.00f, 0.78f, 0.20f, 1.0f}; break;
|
||||
case lv::err:
|
||||
tag = "ERROR"; tagCol = {1.00f, 0.35f, 0.35f, 1.0f};
|
||||
msgCol = {1.00f, 0.35f, 0.35f, 1.0f}; break;
|
||||
default:
|
||||
tag = "FATAL"; tagCol = {1.00f, 0.15f, 0.15f, 1.0f};
|
||||
msgCol = {1.00f, 0.15f, 0.15f, 1.0f};
|
||||
bg = true; break;
|
||||
}
|
||||
if (bg)
|
||||
{
|
||||
// fatal:整行暗红背景条提级(先画背景再画文本)
|
||||
const ImVec2 p0 = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 p1(p0.x + ImGui::GetContentRegionAvail().x,
|
||||
p0.y + ImGui::GetTextLineHeight());
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(
|
||||
p0, p1, IM_COL32(90, 10, 10, 160));
|
||||
}
|
||||
ImGui::TextColored(tagCol, "%s", tag);
|
||||
ImGui::SameLine(40.0f); // 徽标固定列宽,消息对齐成两列
|
||||
ImGui::TextColored(msgCol, "%s", e.msg.c_str());
|
||||
}
|
||||
if (scrollToBottom)
|
||||
{
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// LogPanel:运行时日志面板(环形缓冲渲染,级别着色 + 过滤)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_LOGPANEL_H
|
||||
#define HELLOVULKAN_LOGPANEL_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
class LogPanel {
|
||||
public:
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
private:
|
||||
bool m_ShowTrace = false;
|
||||
bool m_ShowInfo = true;
|
||||
bool m_ShowWarn = true;
|
||||
bool m_ShowError = true;
|
||||
bool m_AutoScroll = true;
|
||||
char m_Filter[128] = "";
|
||||
Engine::u64 m_LastVersion = 0;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_LOGPANEL_H
|
||||
@@ -0,0 +1,499 @@
|
||||
//
|
||||
// MaterialEditorPanel.cpp:独立材质编辑器实现。
|
||||
//
|
||||
// 编辑语义原样承接自原 Inspector "Edit Material" 内嵌块(行为不变的
|
||||
// 搬家):参数 EditParams 直写 UBO + IsItemDeactivatedAfterEdit 落盘;
|
||||
// 纹理槽改动落盘 + ReloadMaterialAsset 重建(旧指针即刻悬空,本帧
|
||||
// 不再触碰 asset)。
|
||||
//
|
||||
|
||||
#include "MaterialEditorPanel.h"
|
||||
#include "MaterialAssetUtil.h"
|
||||
#include "AssetItemView.h" // 纹理选择器网格(缩略图瓦片)
|
||||
#include "EditorColors.h"
|
||||
#include "ThumbnailCache.h"
|
||||
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
// InputText 需要 char 缓冲;提交时写回 std::string(与 Inspector 同款)
|
||||
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;
|
||||
}
|
||||
|
||||
constexpr float kPreviewSize = 128.0f; // 编辑器顶部大球(Inspector 槽位为 48²)
|
||||
|
||||
// 纹理槽缩略图(同时是拖拽落点):Content Browser 拖纹理资产落入即
|
||||
// 指派该槽(kTextureDragPayload)。空槽显示 "nil" 占位
|
||||
void SlotThumb(const std::string& texPath, float sz, int slotIdx,
|
||||
int& pendingSlot, std::string& pendingPath)
|
||||
{
|
||||
const ImVec4 accent = EditorColors::kTexture;
|
||||
ImGui::Dummy(ImVec2(sz, sz));
|
||||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
bool drawn = false;
|
||||
if (!texPath.empty())
|
||||
{
|
||||
const ThumbView v = EditorThumbnailCache::Get().Request(texPath);
|
||||
if (v.state == ThumbView::State::Ready)
|
||||
{
|
||||
float fw = sz, fh = sz;
|
||||
if (v.w > 0 && v.h > 0)
|
||||
{
|
||||
if (v.w >= v.h) fh = sz * static_cast<float>(v.h) / v.w;
|
||||
else fw = sz * static_cast<float>(v.w) / v.h;
|
||||
}
|
||||
const ImVec2 f0(p0.x + (sz - fw) * 0.5f,
|
||||
p0.y + (sz - fh) * 0.5f);
|
||||
dl->AddImage(v.tex, f0, ImVec2(f0.x + fw, f0.y + fh));
|
||||
drawn = true;
|
||||
}
|
||||
}
|
||||
if (!drawn)
|
||||
{
|
||||
// 背板只在未出图时铺:出图时叠加会污染透明像素
|
||||
dl->AddRectFilled(
|
||||
p0, p1,
|
||||
ImGui::GetColorU32(ImVec4(accent.x * 0.25f, accent.y * 0.25f,
|
||||
accent.z * 0.25f, 1.0f)),
|
||||
3.0f);
|
||||
const char* lbl = texPath.empty() ? "nil" : "TEX";
|
||||
const ImVec2 ts = ImGui::CalcTextSize(lbl);
|
||||
dl->AddText(ImVec2(p0.x + (sz - ts.x) * 0.5f,
|
||||
p0.y + (sz - ts.y) * 0.5f),
|
||||
ImGui::GetColorU32(texPath.empty()
|
||||
? EditorColors::kOther
|
||||
: accent),
|
||||
lbl);
|
||||
}
|
||||
dl->AddRect(p0, p1,
|
||||
ImGui::GetColorU32(hovered ? EditorColors::Lighten(accent,
|
||||
0.3f)
|
||||
: accent),
|
||||
3.0f);
|
||||
|
||||
if (ImGui::BeginDragDropTarget())
|
||||
{
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kTextureDragPayload))
|
||||
{
|
||||
pendingSlot = slotIdx;
|
||||
pendingPath = std::string(static_cast<const char*>(pl->Data));
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
}
|
||||
|
||||
// 纹理选择器内容(弹窗内):<none> 清除入口 + 缩略图网格。
|
||||
// 返回 true = 本次有选择,选中路径写入 out(空串 = <none>)
|
||||
bool TexturePickerContent(const EditorContext& ctx, std::string& out)
|
||||
{
|
||||
bool picked = false;
|
||||
if (ImGui::Selectable("<none>"))
|
||||
{
|
||||
out.clear();
|
||||
picked = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
const auto& files = EditorMaterialUtil::AssetTextureFiles();
|
||||
if (files.empty())
|
||||
{
|
||||
ImGui::TextDisabled("(no textures under assets/)");
|
||||
return picked;
|
||||
}
|
||||
|
||||
constexpr float kTile = 64.0f;
|
||||
const float cellW = kTile + ImGui::GetStyle().CellPadding.x * 2.0f;
|
||||
int cols = static_cast<int>(360.0f / cellW);
|
||||
if (cols < 1)
|
||||
{
|
||||
cols = 1;
|
||||
}
|
||||
ImGui::BeginChild("##texgrid", ImVec2(360.0f, 260.0f),
|
||||
ImGuiChildFlags_Borders);
|
||||
if (ImGui::BeginTable("##grid", cols, ImGuiTableFlags_SizingFixedFit))
|
||||
{
|
||||
for (int i = 0; i < cols; ++i)
|
||||
{
|
||||
ImGui::TableSetupColumn("##c",
|
||||
ImGuiTableColumnFlags_WidthFixed,
|
||||
cellW);
|
||||
}
|
||||
for (const auto& f : files)
|
||||
{
|
||||
ImGui::TableNextColumn();
|
||||
const auto r =
|
||||
EditorAssetView::DrawTile(ctx, f, kTile, false);
|
||||
if (r.clicked)
|
||||
{
|
||||
out = f;
|
||||
picked = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
return picked;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void MaterialEditorPanel::OpenAsset(const std::string& path)
|
||||
{
|
||||
m_AssetMode = true;
|
||||
m_Path = path;
|
||||
m_LoadTried = false;
|
||||
m_Owner = entt::null;
|
||||
m_Open = true;
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::OpenBuiltin(entt::entity owner, Engine::u32 meshIdx,
|
||||
const std::string& displayName)
|
||||
{
|
||||
m_AssetMode = false;
|
||||
m_Owner = owner;
|
||||
m_MeshIdx = meshIdx;
|
||||
m_DisplayName = displayName;
|
||||
m_Path.clear();
|
||||
m_Open = true;
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
if (!m_Open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!ImGui::Begin("Material Editor", &m_Open))
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_AssetMode)
|
||||
{
|
||||
DrawAssetMode(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawBuiltinMode(ctx);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// 顶部预览球(tile 池请求幂等——窗口开着才请求,池按可见项换页)
|
||||
void MaterialEditorPanel::DrawPreview(const EditorContext& ctx,
|
||||
Engine::Material* mat)
|
||||
{
|
||||
Engine::i32 tile = -1;
|
||||
void* atlasTex = nullptr;
|
||||
if (ctx.renderer && mat)
|
||||
{
|
||||
tile = ctx.renderer->RequestMaterialPreview(mat);
|
||||
atlasTex = ctx.renderer->GetMaterialPreviewTexture();
|
||||
}
|
||||
if (atlasTex && tile >= 0)
|
||||
{
|
||||
const float uvStep = 1.0f / float(Engine::Passes::kPreviewTilesPerSide);
|
||||
const float u0 =
|
||||
float(tile % Engine::Passes::kPreviewTilesPerSide) * uvStep;
|
||||
const float v0 =
|
||||
float(tile / Engine::Passes::kPreviewTilesPerSide) * uvStep;
|
||||
ImGui::Image(atlasTex, ImVec2(kPreviewSize, kPreviewSize),
|
||||
ImVec2(u0, v0), ImVec2(u0 + uvStep, v0 + uvStep));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Dummy(ImVec2(kPreviewSize, kPreviewSize));
|
||||
}
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
||||
{
|
||||
if (!ctx.assets)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 每帧重解析:ReloadMaterialAsset 换实例 / 未加载自动 Load 一次
|
||||
Engine::MaterialAsset* asset = ctx.assets->FindMaterialAsset(m_Path);
|
||||
if (!asset && !m_LoadTried)
|
||||
{
|
||||
m_LoadTried = true;
|
||||
ctx.assets->LoadMaterialAsset(m_Path);
|
||||
asset = ctx.assets->FindMaterialAsset(m_Path);
|
||||
}
|
||||
if (!asset)
|
||||
{
|
||||
ImGui::TextDisabled("material not loaded: %s", m_Path.c_str());
|
||||
if (ImGui::Button("Retry Load"))
|
||||
{
|
||||
m_LoadTried = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string& path = m_Path;
|
||||
const std::string name =
|
||||
std::filesystem::path(path).filename().generic_string();
|
||||
|
||||
// ---- 头部:预览球 + 资产名/路径 ----
|
||||
DrawPreview(ctx, asset->GetMaterial());
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::TextDisabled("%s", path.c_str());
|
||||
ImGui::TextDisabled("shared asset: edits apply to all entities "
|
||||
"referencing it");
|
||||
ImGui::EndGroup();
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- 透明队列归属(收集侧 CPU 标志)----
|
||||
bool trans = asset->GetMaterial()->IsTransparent();
|
||||
if (ImGui::Checkbox("Transparent", &trans))
|
||||
{
|
||||
asset->SetTransparent(trans);
|
||||
Engine::MaterialAsset::Save(path, asset->GetData());
|
||||
}
|
||||
|
||||
// ---- 参数(变化直写 UBO 实时预览;松手落盘,拖拽中不产生每帧 IO)----
|
||||
Engine::MaterialParams p = asset->GetData().params;
|
||||
auto EditParam = [&](bool changed)
|
||||
{
|
||||
if (changed)
|
||||
{
|
||||
asset->EditParams(p);
|
||||
}
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
Engine::MaterialAsset::Save(path, asset->GetData());
|
||||
}
|
||||
};
|
||||
EditParam(
|
||||
ImGui::DragFloat("Metallic", &p.factors.x, 0.01f, 0.0f, 1.0f));
|
||||
EditParam(
|
||||
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f));
|
||||
EditParam(ImGui::ColorEdit4("Base Color", &p.baseColor.x));
|
||||
// albedo 贴图 alpha 作细节叠加遮罩(脸部腮红/卧蚕等):rgb 叠加色,
|
||||
// a 强度(0 = 关闭);需 albedo 贴图自带 alpha 遮罩才有局部效果
|
||||
EditParam(ImGui::ColorEdit4("Alpha Overlay", &p.alphaOverlay.x));
|
||||
EditParam(ImGui::DragFloat("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f));
|
||||
// 发光 = emissiveFactor × emissive 贴图 × Strength(Emissive 槽
|
||||
// 兜底贴图恒黑,无贴图时颜色因子是唯一入口)
|
||||
EditParam(ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x));
|
||||
EditParam(ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f,
|
||||
0.0f, 100.0f));
|
||||
// transmission/thickness 只有不透明段不读——透明 shader 专属
|
||||
//(未勾 Transparent 时禁用编辑,防"拖了没反应"的困惑)
|
||||
ImGui::BeginDisabled(!trans);
|
||||
EditParam(ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f,
|
||||
1.0f));
|
||||
EditParam(ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f,
|
||||
10.0f));
|
||||
ImGui::EndDisabled();
|
||||
if (!trans)
|
||||
{
|
||||
ImGui::TextDisabled("Transmission/Thickness need Transparent");
|
||||
}
|
||||
|
||||
// UV 镜像(材质级开关,作用于全部纹理槽):源资产 UV 朝向与引擎
|
||||
// 上左原点约定相反时贴图整体翻转,这里一键纠正。写入 misc.y/z,
|
||||
// 旧 .material 无该分量 → 缺省 0 = 关闭
|
||||
{
|
||||
bool flipU = p.misc.y > 0.5f;
|
||||
const bool c = ImGui::Checkbox("Flip U (left-right)", &flipU);
|
||||
if (c) { p.misc.y = flipU ? 1.0f : 0.0f; }
|
||||
EditParam(c);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
{
|
||||
bool flipV = p.misc.z > 0.5f;
|
||||
const bool c = ImGui::Checkbox("Flip V (up-down)", &flipV);
|
||||
if (c) { p.misc.z = flipV ? 1.0f : 0.0f; }
|
||||
EditParam(c);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
{
|
||||
// DirectX 法线绿通道翻转(UE 打包 Specular 资产由导入器默认为开;
|
||||
// 方向反了在此逐材质纠正)。写 misc.w,旧 .material 缺省 0 = 关闭
|
||||
bool flipG = p.misc.w > 0.5f;
|
||||
const bool c = ImGui::Checkbox("Flip Green (DirectX)", &flipG);
|
||||
if (c) { p.misc.w = flipG ? 1.0f : 0.0f; }
|
||||
EditParam(c);
|
||||
}
|
||||
|
||||
// ---- 纹理槽(变化 → 落盘 + waitIdle 重建)----
|
||||
ImGui::SeparatorText("Textures");
|
||||
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
|
||||
"Albedo", "Normal", "MetallicRoughness",
|
||||
"Emissive", "Ao", "Roughness"};
|
||||
int pendingSlot = -1;
|
||||
std::string pendingPath;
|
||||
const float thumbSz = ImGui::GetTextLineHeight() * 1.8f;
|
||||
for (Engine::u32 s = 0; s < Engine::Material::kSlotCount; ++s)
|
||||
{
|
||||
std::string tp = asset->GetData().texPaths[s];
|
||||
ImGui::PushID(static_cast<int>(s));
|
||||
|
||||
// 缩略图块(落点:拖入纹理资产即指派该槽)
|
||||
SlotThumb(tp, thumbSz, static_cast<int>(s), pendingSlot, pendingPath);
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
InputTextString(kSlotNames[s], tp);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
pendingSlot = static_cast<int>(s);
|
||||
pendingPath = tp;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("..."))
|
||||
{
|
||||
ImGui::OpenPopup("tex_pick");
|
||||
}
|
||||
if (ImGui::BeginPopup("tex_pick"))
|
||||
{
|
||||
// 弹窗内只做选择;pending 统一在循环外落盘(避免 popup 内
|
||||
// 立即重建导致 asset 指针悬空)
|
||||
std::string chosen;
|
||||
if (TexturePickerContent(ctx, chosen))
|
||||
{
|
||||
pendingSlot = static_cast<int>(s);
|
||||
pendingPath = chosen;
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (pendingSlot >= 0)
|
||||
{
|
||||
// 拷出数据段 → 落盘 → 重建资产(旧指针即刻悬空:本帧到此不再
|
||||
// 触碰 asset,下帧 Find 取新实例)。旧材质已在本帧 Request 进
|
||||
// 预览池脏表(新 tile 未画满三槽位时每帧重入队)——先 drop 再
|
||||
// 销毁,否则 Render 段解引用悬空指针
|
||||
Engine::MaterialAssetData d = asset->GetData();
|
||||
d.texPaths[static_cast<Engine::u32>(pendingSlot)] = pendingPath;
|
||||
Engine::MaterialAsset::Save(path, d);
|
||||
if (ctx.renderer)
|
||||
{
|
||||
ctx.renderer->DropMaterialPreview(asset->GetMaterial());
|
||||
}
|
||||
ctx.assets->ReloadMaterialAsset(path);
|
||||
}
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::DrawBuiltinMode(const EditorContext& ctx)
|
||||
{
|
||||
// 每帧重解析:实体可销毁(PIE Stop 换血)/ 模型可卸载
|
||||
if (!ctx.scene || !ctx.assets)
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
auto* mr = reg.valid(m_Owner)
|
||||
? reg.try_get<Engine::ECS::MeshRendererComponent>(m_Owner)
|
||||
: nullptr;
|
||||
const Engine::Model* model =
|
||||
(mr && mr->model.Valid()) ? ctx.assets->GetModel(mr->model) : nullptr;
|
||||
const Engine::MeshMaterialInfo* info =
|
||||
(model && m_MeshIdx < model->GetMeshMaterialInfos().size())
|
||||
? &model->GetMeshMaterialInfos()[m_MeshIdx]
|
||||
: nullptr;
|
||||
if (!mr || !model || !info)
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
||||
"built-in material no longer available");
|
||||
ImGui::TextDisabled("(entity destroyed or model unloaded)");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string meshName =
|
||||
(!info->name.empty())
|
||||
? info->name
|
||||
: "Slot " + std::to_string(m_MeshIdx);
|
||||
|
||||
// ---- 头部:预览球 + 名称/来源 ----
|
||||
DrawPreview(ctx, model->GetMeshes()[m_MeshIdx]->GetMaterial());
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::TextUnformatted(meshName.c_str());
|
||||
ImGui::TextDisabled("built-in material of %s",
|
||||
std::filesystem::path(
|
||||
ctx.assets->Models().Path(mr->model))
|
||||
.filename()
|
||||
.generic_string()
|
||||
.c_str());
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f),
|
||||
"read-only: built-in materials belong to the model");
|
||||
ImGui::EndGroup();
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- 一键转可编辑:Create Override → 自动切到资产模式 ----
|
||||
if (ImGui::Button("Create Override"))
|
||||
{
|
||||
const std::string rel = EditorMaterialUtil::CreateMaterialOverride(
|
||||
ctx.assets->Models().Path(mr->model), *info, m_MeshIdx, *mr,
|
||||
*ctx.assets);
|
||||
if (!rel.empty())
|
||||
{
|
||||
OpenAsset(rel); // 数据已快照进资产,无缝进入编辑
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- 只读展示(与资产模式同款控件,锁编辑看值)----
|
||||
ImGui::BeginDisabled(true);
|
||||
bool trans = info->transparent;
|
||||
ImGui::Checkbox("Transparent", &trans);
|
||||
Engine::MaterialParams p = info->params;
|
||||
ImGui::DragFloat("Metallic", &p.factors.x, 0.01f, 0.0f, 1.0f);
|
||||
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f);
|
||||
ImGui::ColorEdit4("Base Color", &p.baseColor.x);
|
||||
ImGui::ColorEdit4("Alpha Overlay", &p.alphaOverlay.x);
|
||||
ImGui::DragFloat("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f);
|
||||
ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x);
|
||||
ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f, 0.0f, 100.0f);
|
||||
ImGui::BeginDisabled(!trans);
|
||||
ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f, 1.0f);
|
||||
ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f, 10.0f);
|
||||
ImGui::EndDisabled();
|
||||
if (!trans)
|
||||
{
|
||||
ImGui::TextDisabled("Transmission/Thickness need Transparent");
|
||||
}
|
||||
ImGui::SeparatorText("Textures");
|
||||
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
|
||||
"Albedo", "Normal", "MetallicRoughness",
|
||||
"Emissive", "Ao", "Roughness"};
|
||||
for (Engine::u32 s = 0; s < Engine::Material::kSlotCount; ++s)
|
||||
{
|
||||
std::string tp = info->texPaths[s];
|
||||
ImGui::PushID(static_cast<int>(s));
|
||||
InputTextString(kSlotNames[s], tp);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// MaterialEditorPanel:独立材质编辑器窗口(UE Material Editor 语义)。
|
||||
//
|
||||
// 与 Details 面板职责分离:Details 材质槽行只做"指派"(换绑/还原/拖拽
|
||||
// 覆盖资产),双击槽位预览球或 Content Browser 的 .material 条目 →
|
||||
// 本面板打开做"编辑"(参数全平铺 + 纹理槽)。
|
||||
//
|
||||
// 两种模式:
|
||||
// - 资产模式(可编辑):编辑 .material 资产。参数走
|
||||
// EditParams 直写 UBO(松手落盘),纹理槽变化走 ReloadMaterialAsset
|
||||
// (waitIdle 全停重建,低频路径)——与原 Inspector 内嵌编辑完全
|
||||
// 同一套语义,此处原样搬运
|
||||
// - 内建模式(只读):Model 内建材质无 CPU 参数副本(直写 GPU UBO),
|
||||
// 只读展示 MeshMaterialInfo 快照 + 站内一键 Create Override(生成
|
||||
// 资产后自动切换到资产模式编辑)
|
||||
//
|
||||
// 打开目标每帧重解析(实体可销毁 / 模型可卸载 / 资产可 Reload 换实例),
|
||||
// 面板不跨帧持有任何指针。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_MATERIALEDITORPANEL_H
|
||||
#define HELLOVULKAN_MATERIALEDITORPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
#include <string>
|
||||
|
||||
// 材质资产拖拽 payload(Content Browser 拖 .material → Details 槽位)
|
||||
inline constexpr const char* kMaterialDragPayload = "MAT_PATH";
|
||||
|
||||
// 纹理资产拖拽 payload(Content Browser 拖 .png/.jpg/... → 材质纹理槽)
|
||||
inline constexpr const char* kTextureDragPayload = "TEXTURE_PATH";
|
||||
|
||||
class MaterialEditorPanel {
|
||||
public:
|
||||
// 打开 .material 资产(可编辑模式)
|
||||
void OpenAsset(const std::string& path);
|
||||
// 打开内建材质(只读模式):owner 实体 + mesh 槽位每帧重解析
|
||||
void OpenBuiltin(entt::entity owner, Engine::u32 meshIdx,
|
||||
const std::string& displayName);
|
||||
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
private:
|
||||
void DrawPreview(const EditorContext& ctx, Engine::Material* mat);
|
||||
void DrawAssetMode(const EditorContext& ctx);
|
||||
void DrawBuiltinMode(const EditorContext& ctx);
|
||||
|
||||
bool m_Open = false;
|
||||
bool m_AssetMode = false;
|
||||
// 资产模式目标
|
||||
std::string m_Path;
|
||||
bool m_LoadTried = false; // 未加载时自动 Load 一次(防每帧重试)
|
||||
// 内建模式目标
|
||||
entt::entity m_Owner{entt::null};
|
||||
Engine::u32 m_MeshIdx = 0;
|
||||
std::string m_DisplayName;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_MATERIALEDITORPANEL_H
|
||||
@@ -0,0 +1,235 @@
|
||||
//
|
||||
// StatsPanel.cpp:遥测与全局设置实现(原 main.cpp Scene Debug 搬迁)。
|
||||
// 面板按 View / Scene / Rendering / Physics / Performance 分组——
|
||||
// 环境贴图/环境光已实体化(Skybox/SkyLight 组件,Details 配置),
|
||||
// 此处只留场景级 ClearColor。
|
||||
//
|
||||
|
||||
#include "StatsPanel.h"
|
||||
#include "Engine/ECS/Components.h"
|
||||
#include "Engine/Physics/PhysicsWorld.h"
|
||||
#include "Engine/Core/Time.h"
|
||||
#include "Engine/Core/Stats.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;
|
||||
|
||||
// ---- View:看哪里(渲染目标)+ 相机位姿(跟随渲染源分派)----
|
||||
if (ImGui::CollapsingHeader("View", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
// 渲染源分派与 SceneRenderer::BuildFrame 同规则:PIE 内嵌播放
|
||||
// 或 kDisplayDirect 全屏预览 = primary 相机,否则编辑器相机
|
||||
const bool playing = ctx.playState && *ctx.playState;
|
||||
static const char* displayTarget[] = {
|
||||
"Fullscreen",
|
||||
"Viewport",
|
||||
};
|
||||
int mode = renderer.GetDisplayMode();
|
||||
if (ImGui::Combo("Display Target", &mode, displayTarget,
|
||||
IM_ARRAYSIZE(displayTarget)))
|
||||
{
|
||||
renderer.SetDisplayMode(mode);
|
||||
}
|
||||
if ((playing || renderer.GetDisplayMode() == kDisplayDirect))
|
||||
{
|
||||
Engine::ECS::TransformComponent* camT = nullptr;
|
||||
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
|
||||
{
|
||||
// world 位姿(相机实体可能挂父层级下)
|
||||
const glm::vec3& pos = camT->GetWorldPosition();
|
||||
ImGui::Text("Game Camera Pos");
|
||||
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("(no primary camera)");
|
||||
}
|
||||
}
|
||||
else if (ctx.editorCamera)
|
||||
{
|
||||
const glm::vec3& pos = ctx.editorCamera->GetTransform().GetPosition();
|
||||
ImGui::Text("Editor Camera Pos");
|
||||
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("(no camera)");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Scene:模拟控制 + 场景级设置。PIE 暂停在主工具栏(UE 语义)----
|
||||
if (ImGui::CollapsingHeader("Scene", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
ImGui::ColorEdit4("Clear Color",
|
||||
glm::value_ptr(ctx.scene->GetSettings().ClearColor));
|
||||
}
|
||||
|
||||
// ---- 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, 8.0f,
|
||||
"%.1f", ImGuiSliderFlags_Logarithmic))
|
||||
{
|
||||
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); // 关 = 恒绘原网格
|
||||
}
|
||||
|
||||
// 调试视图:单看某个 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);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Physics:调试可视化(collider/约束/重力/速度/加速度)----
|
||||
if (ImGui::CollapsingHeader("Physics"))
|
||||
{
|
||||
auto& physics = app.GetPhysics();
|
||||
u32 bits = physics.GetDebugDrawBits();
|
||||
bool master = bits != 0;
|
||||
if (ImGui::Checkbox("Debug Draw##Phys", &master))
|
||||
{
|
||||
bits = master ? kPhyDbgDefault : 0;
|
||||
}
|
||||
if (master)
|
||||
{
|
||||
ImGui::Indent();
|
||||
auto toggle = [&bits](const char* label, u32 bit) {
|
||||
bool on = (bits & bit) != 0;
|
||||
if (ImGui::Checkbox(label, &on))
|
||||
{
|
||||
bits = on ? (bits | bit) : (bits & ~bit);
|
||||
}
|
||||
};
|
||||
toggle("Colliders", kPhyDbgColliders);
|
||||
toggle("Constraints", kPhyDbgConstraints);
|
||||
toggle("Gravity", kPhyDbgGravity);
|
||||
toggle("Velocity", kPhyDbgVelocity);
|
||||
toggle("Acceleration", kPhyDbgAccel);
|
||||
ImGui::Unindent();
|
||||
}
|
||||
physics.SetDebugDrawBits(bits);
|
||||
// 碰撞事件计数(最近一次步进;掉落物触地时应 > 0)
|
||||
ImGui::Text("contacts (frame): %zu", physics.GetEvents().size());
|
||||
}
|
||||
|
||||
// ---- Performance:帧率控制 + GPU 遥测(默认折叠,不占面板高度)----
|
||||
if (ImGui::CollapsingHeader("Performance"))
|
||||
{
|
||||
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 / Time::Current().UnscaledDt(),
|
||||
static_cast<unsigned long long>(app.GetFrameCount()));
|
||||
|
||||
ImGui::Separator();
|
||||
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]);
|
||||
}
|
||||
|
||||
// ---- GPU 资源台账(Stats 记账中枢的唯一消费者)----
|
||||
const auto st = Engine::Stats::Get().TakeSnapshot();
|
||||
auto MB = [](u64 bytes)
|
||||
{ return static_cast<double>(bytes) / (1024.0 * 1024.0); };
|
||||
ImGui::Separator();
|
||||
ImGui::Text("textures: %llu (%.2f MB)",
|
||||
static_cast<unsigned long long>(st.textureCount),
|
||||
MB(st.textureBytes));
|
||||
ImGui::Text("meshes: %llu (%.2f MB)",
|
||||
static_cast<unsigned long long>(st.meshCount),
|
||||
MB(st.meshBytes));
|
||||
ImGui::Text("buffers: %llu (%.2f MB)",
|
||||
static_cast<unsigned long long>(st.bufferCount),
|
||||
MB(st.bufferBytes));
|
||||
ImGui::Text("allocator (VMA 1:1): %.2f MB",
|
||||
MB(st.memoryUsedBytes));
|
||||
ImGui::Text("churn: %.2f MB / %.2f MB (total %.2f / %.2f)",
|
||||
MB(st.frameAllocation), MB(st.frameDeallocation),
|
||||
MB(st.totalAllocation), MB(st.totalDeallocation));
|
||||
}
|
||||
|
||||
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,484 @@
|
||||
//
|
||||
// ViewportPanel:场景渲染输出面板。
|
||||
//
|
||||
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
|
||||
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
|
||||
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
|
||||
//
|
||||
// 点击拾取(UE 式 GPU HitProxy):左键单击 → 鼠标 NDC 经
|
||||
// Renderer::RequestPick 请求下一帧 id 渲染 + 像素拷贝;Draw 每帧
|
||||
// 轮询 TakePickResult 取走结果。像素级精度,与视觉画面一致
|
||||
//(视觉几何语义:与 Collider 无关,无 collider 实体同样可选中)。
|
||||
//
|
||||
|
||||
#include "ViewportPanel.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <ImGuizmo.h>
|
||||
|
||||
#include <Engine/ECS/Components.h> // MeshRenderer / Name / RigidBody(日志对照)
|
||||
#include <Engine/Renderer/Renderer.h> // RequestPick / TakePickResult 门面
|
||||
#include <Engine/Renderer/SceneTypes.h> // kPickMiss
|
||||
#include <Engine/Core/Log.h> // 命中打印
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解,
|
||||
// 旋转以四元数输出,直接写回 TransformComponent)
|
||||
#define GLM_ENABLE_EXPERIMENTAL
|
||||
#include <glm/gtx/matrix_decompose.hpp>
|
||||
|
||||
using namespace Engine; // u32 / f32(Engine/Core/Types.h)
|
||||
|
||||
namespace
|
||||
{
|
||||
// Ctrl 吸附步长(UE Grid Snap 语义:按住 Ctrl 拖拽才吸附)
|
||||
constexpr float kSnapTranslate = 0.25f; // 米/格
|
||||
constexpr float kSnapRotateDeg = 15.0f; // 度
|
||||
constexpr float kSnapScale = 0.1f; // 缩放步进
|
||||
|
||||
// 平移拖拽灵敏度下限:sinθ(轴与"相机→物体"连线夹角)。低于该值
|
||||
// 时拖拽投影系数 <0.25 且激活平面与鼠标射线近共面——视觉上
|
||||
// "看得见、抓了没反应"。≈14.5°,略宽于库默认可见角(≈11.5°),
|
||||
// 只藏真正病态的轴,不动可用轴
|
||||
constexpr float kMoveMinSin = 0.25f;
|
||||
|
||||
// 缩放退化轴裁剪(防误拖动)阈值。库内缩放 hover/拖拽依赖
|
||||
// "法线=轴"平面求交,两种病态几何:
|
||||
// - 轴⊥"相机→物体"连线(正对视角无横偏):交点分子 numer =
|
||||
// -dist·dot(camToObj, 轴) ≡ 0 → 杆抓不到、数值随机猛跳
|
||||
// - 轴∥视线(轴指向相机):屏幕零位移 → 拖了数值不变
|
||||
constexpr float kScaleMinDot = 0.1f; // ≈84.3°
|
||||
constexpr float kScaleMaxFwdDot = 0.98f; // ≈11.5°,与库藏杆角一致
|
||||
|
||||
// Vulkan proj → ImGuizmo 期望的 OpenGL 形式(Y-up、Z∈[-1,1]):
|
||||
// Y 行翻回(抵消 Vulkan clip Y 向下翻转);z 行 = 2*z - w 即
|
||||
// 深度 [0,1] → [-1,1]。glm 列主序 m[col][row],逐列改 row1/row2
|
||||
glm::mat4 ProjVulkanToGL(const glm::mat4& projVulkan)
|
||||
{
|
||||
glm::mat4 p = projVulkan;
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
p[c][1] *= -1.0f;
|
||||
p[c][2] = 2.0f * p[c][2] - p[c][3];
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// 视图相机分派(必须与 SceneRenderer::BuildFrame 渲染源同一规则,
|
||||
// 否则 Gizmo 杆与画面错位):编辑器形态 = EditorCamera(Scene view
|
||||
// 专用,与相机实体无关);游戏形态(kDisplayDirect 全屏预览或 PIE
|
||||
// 内嵌播放)= primary 相机实体。
|
||||
// 返回 nullptr = 游戏形态且无 primary 相机(本帧无 Gizmo)
|
||||
Engine::Camera* ViewCamera(const EditorContext& ctx,
|
||||
ECS::TransformComponent*& outT)
|
||||
{
|
||||
const bool playing = ctx.playState && *ctx.playState;
|
||||
if (!playing && ctx.renderer->GetDisplayMode() == kDisplayViewport)
|
||||
{
|
||||
outT = &ctx.editorCamera->GetTransform();
|
||||
return &ctx.editorCamera->GetController();
|
||||
}
|
||||
auto* cc = ctx.scene->GetPrimaryCamera(&outT);
|
||||
return cc ? &cc->camera : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
ViewportInputState &out,
|
||||
GizmoState &gizmo,
|
||||
const entt::entity selected) const {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
std::optional<entt::entity> picked;
|
||||
{
|
||||
ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
|
||||
// 输入焦点上报(悬停计入遮挡:模态框盖住时不误报)
|
||||
out.hovered = ImGui::IsWindowHovered();
|
||||
out.focused = ImGui::IsWindowFocused();
|
||||
|
||||
auto &renderer = *ctx.renderer;
|
||||
const int mode = renderer.GetDisplayMode();
|
||||
|
||||
// ---- 拾取结果轮询(先于新请求:先取走上帧结果再发新请求)。
|
||||
// 拷贝随上一帧命令缓冲提交,TakePickResult 内 waitIdle 保证
|
||||
// 暂存可见(仅拷贝就绪帧阻塞,点击低频不可感知)----
|
||||
if (auto bits = renderer.TakePickResult()) {
|
||||
auto ® = ctx.scene->GetRegistry();
|
||||
const entt::entity hit = static_cast<entt::entity>(*bits);
|
||||
if (*bits != kPickMiss && reg.valid(hit)) {
|
||||
picked = hit;
|
||||
LogPick(ctx, hit);
|
||||
} else {
|
||||
// 空白(kPickMiss)或悬空位模式:清选中(与旧语义一致)
|
||||
picked = entt::null;
|
||||
if (*bits == kPickMiss) {
|
||||
ENGINE_INFO("[pick] miss (sky/empty)");
|
||||
} else {
|
||||
ENGINE_WARN("[pick] dangling entity bits {:#x}", *bits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// ---- 骨骼关节 overlay(Endfield-Poser 范式):3D 骨架线
|
||||
// 给结构,屏幕圆点给可读性。世界→屏幕与渲染同相机,
|
||||
// Vulkan NDC y 向下与 ImGui 屏幕坐标同构 → 线性映射
|
||||
// image 矩形即可。hover 关节 = 白圈高亮 + 骨骼名
|
||||
{
|
||||
const auto &frameData = renderer.GetFrameSceneData();
|
||||
if (frameData.showBoneWires && !frameData.pieActive &&
|
||||
!frameData.boneJoints.empty() &&
|
||||
ctx.editorCamera != nullptr) {
|
||||
const ImVec2 rmin = ImGui::GetItemRectMin();
|
||||
const ImVec2 rmax = ImGui::GetItemRectMax();
|
||||
const glm::mat4 vp =
|
||||
ctx.editorCamera->GetController().GetProjection(aspect) *
|
||||
ctx.editorCamera->GetView();
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||
dl->PushClipRect(rmin, rmax, true);
|
||||
const f32 r = frameData.boneJointPx;
|
||||
const ImGuiIO &io = ImGui::GetIO();
|
||||
const ImVec2 mouse = io.MousePos;
|
||||
int hovered = -1;
|
||||
float hoveredD = 0.0f;
|
||||
for (size_t ji = 0; ji < frameData.boneJoints.size(); ++ji) {
|
||||
const auto &jm = frameData.boneJoints[ji];
|
||||
const glm::vec4 c = vp * glm::vec4(jm.pos, 1.0f);
|
||||
if (c.w <= 0.0f) continue; // 相机身后
|
||||
const ImVec2 p(
|
||||
rmin.x + (c.x / c.w * 0.5f + 0.5f) * size.x,
|
||||
rmin.y + (c.y / c.w * 0.5f + 0.5f) * size.y);
|
||||
// 骨骼 = 蓝(Endfield humanoid 色),普通节点 = 青
|
||||
const ImU32 fill = jm.isBone
|
||||
? IM_COL32(70, 160, 255, 230)
|
||||
: IM_COL32(70, 190, 190, 190);
|
||||
dl->AddCircleFilled(p, r, fill);
|
||||
dl->AddCircle(p, r + 1.5f,
|
||||
IM_COL32(255, 255, 255, 140), 0, 1.2f);
|
||||
// hover 命中(半径 +3px 容差,取最近)
|
||||
const float dx = p.x - mouse.x, dy = p.y - mouse.y;
|
||||
const float d = dx * dx + dy * dy;
|
||||
const float hit = (r + 3.0f) * (r + 3.0f);
|
||||
if (d <= hit && (hovered < 0 || d < hoveredD)) {
|
||||
hovered = static_cast<int>(ji);
|
||||
hoveredD = d;
|
||||
}
|
||||
}
|
||||
// hover 高亮圈 + 骨骼名 tooltip(名字查选中实体模型
|
||||
// 节点表;查不到 = 显示节点下标)
|
||||
if (hovered >= 0 && ImGui::IsItemHovered()) {
|
||||
const auto &jm = frameData.boneJoints[hovered];
|
||||
const glm::vec4 c = vp * glm::vec4(jm.pos, 1.0f);
|
||||
const ImVec2 p(
|
||||
rmin.x + (c.x / c.w * 0.5f + 0.5f) * size.x,
|
||||
rmin.y + (c.y / c.w * 0.5f + 0.5f) * size.y);
|
||||
dl->AddCircle(p, r + 3.5f,
|
||||
IM_COL32(255, 255, 255, 255), 0, 2.0f);
|
||||
const char *name = nullptr;
|
||||
if (selected != entt::null) {
|
||||
// 本段在 reg 声明之前,就地取 registry
|
||||
const auto *mrSel =
|
||||
ctx.scene->GetRegistry().try_get<
|
||||
Engine::ECS::MeshRendererComponent>(
|
||||
selected);
|
||||
if (mrSel && mrSel->model.Valid()) {
|
||||
const Model *mdl =
|
||||
ctx.assets->GetModel(mrSel->model);
|
||||
if (mdl && jm.nodeIdx < mdl->GetNodes().size())
|
||||
name = mdl->GetNodes()[jm.nodeIdx]
|
||||
.name.c_str();
|
||||
}
|
||||
}
|
||||
ImGui::SetTooltip(
|
||||
"%s%s", name ? name : "(node)",
|
||||
jm.isBone ? "" : " [non-bone]");
|
||||
}
|
||||
dl->PopClipRect();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 工具热键(UE 语义):Q 纯选择(不出杆)、W/E/R 切
|
||||
// Gizmo 模式、T 切 local/world、F 焦点选中实体。仅 Viewport
|
||||
// 持焦时生效——输入框打字(WantTextInput)由 BlockingKeyboard
|
||||
// 拦游戏侧,但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按
|
||||
// 住 = 相机飞行模式(键盘归相机),工具热键全部抑制——W 前
|
||||
// 进与 W 平移 Gizmo 的键位冲突就此解耦 ----
|
||||
auto ® = ctx.scene->GetRegistry();
|
||||
ECS::TransformComponent *tr =
|
||||
selected != entt::null
|
||||
? reg.try_get<ECS::TransformComponent>(selected)
|
||||
: nullptr;
|
||||
// PIE 运行态:输入归游戏(脚本读 GLFW 轮询),Gizmo/编辑器
|
||||
// 热键全部让位(UE Play 语义:Scene view 编辑操作冻结)
|
||||
const bool playing = ctx.playState && *ctx.playState;
|
||||
// PIE 启动瞬间:视口抢焦点(UE Play 语义——游戏视角接管)。
|
||||
// 不抢的话焦点停在工具栏 Play 按钮上,编辑器相机飞行等
|
||||
// 持焦操作下一局全失灵
|
||||
static bool s_wasPlaying = false;
|
||||
if (playing && !s_wasPlaying) {
|
||||
ImGui::SetWindowFocus();
|
||||
}
|
||||
s_wasPlaying = playing;
|
||||
if (!playing && out.focused && !ImGui::GetIO().WantTextInput &&
|
||||
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
||||
// Q = 纯选择模式(UE Q 工具语义):op=0 无任何轴位,
|
||||
// ImGuizmo 不绘制不响应——选中实体只高亮不出杆
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Q)) gizmo.op = 0;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_W)) gizmo.op = ImGuizmo::TRANSLATE;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_T)) gizmo.localSpace = !gizmo.localSpace;
|
||||
const bool fCombo = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
|
||||
// F = 焦点选中实体(frame-selection):编辑器相机
|
||||
// 环绕 pivot 设为选中实体位置,沿视线平移对准(环
|
||||
// 绕距离沿用当前值,滚轮可调)。世界位姿(层级
|
||||
// 实体 = 父链组合结果)。裸 F 才触发(组合键排他)
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F) && !fCombo && tr != nullptr) {
|
||||
ctx.editorCamera->FocusOn(tr->GetWorldPosition());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
|
||||
// 选中实体 + 有 Transform 才出杆;分解 world 矩阵经
|
||||
// SetWorldPose 写回(与 Inspector 同源,下一帧数值自动跟随)
|
||||
// PIE 运行态不出杆(编辑操作冻结)
|
||||
if (tr != nullptr && !playing) {
|
||||
// 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏
|
||||
// 形态 = primary 相机实体;与 SceneRenderer::BuildFrame
|
||||
// 同一分派),投影参数由相机自身承载(与渲染侧一致)
|
||||
ECS::TransformComponent *camT = nullptr;
|
||||
if (Engine::Camera *cam = ViewCamera(ctx, camT);
|
||||
cam != nullptr && camT != nullptr) {
|
||||
const glm::mat4 view = cam->GetView(*camT);
|
||||
const glm::mat4 projGL = ProjVulkanToGL(
|
||||
cam->GetProjection(aspect));
|
||||
|
||||
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
|
||||
// 杆画在 Image 区域内(等比适配后的 item rect)
|
||||
const ImVec2 mn = ImGui::GetItemRectMin();
|
||||
ImGuizmo::SetRect(mn.x, mn.y, size.x, size.y);
|
||||
|
||||
// 不调 SetPlaneLimit/SetAxisLimit——可见度保持库默认
|
||||
|
||||
glm::mat4 m = tr->World();
|
||||
// 操作空间:local(模型轴,旋转/缩放随物体朝向直观)/
|
||||
// world(T 键切换)。无父层级时两种模式 world 矩阵同源,
|
||||
// decompose 写回链路不变
|
||||
const ImGuizmo::MODE mode =
|
||||
gizmo.localSpace ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
|
||||
const glm::mat3 rotM(m);
|
||||
|
||||
// 退化轴裁剪(防误拖动):被裁轴不可见、不可激活,
|
||||
// 转个视角即可用。三操作病态几何不同,按 op 分派:
|
||||
// - 平移:拖拽灵敏度 = sinθ(轴与相机连线夹角),θ→0
|
||||
// 时鼠标位移在轴上投影→0("数值不变")
|
||||
// - 缩放:库内 hover/拖拽依赖"法线=轴"平面求交——
|
||||
// 轴⊥相机连线(正对无横偏)时交点分子 numer≡0
|
||||
// (抓不到/数值乱跳);轴∥视线时屏幕零位移(数值
|
||||
// 不变)。两判据并集裁剪
|
||||
// - 旋转:屏幕空间角度数学,无退化,不裁
|
||||
const bool isTranslate = (gizmo.op
|
||||
& int(ImGuizmo::TRANSLATE)) == int(ImGuizmo::TRANSLATE);
|
||||
const bool isScale = (gizmo.op
|
||||
& int(ImGuizmo::SCALE)) == int(ImGuizmo::SCALE);
|
||||
if (isTranslate || isScale) {
|
||||
bool maskAxis[3] = {false, false, false};
|
||||
const glm::vec3 camPos(glm::inverse(view)[3]);
|
||||
glm::vec3 camToObj = glm::vec3(m[3]) - camPos;
|
||||
const float camDist = glm::length(camToObj);
|
||||
if (camDist > 1e-4f) {
|
||||
camToObj /= camDist;
|
||||
const glm::vec3 viewFwd = -glm::normalize(
|
||||
glm::vec3(view[0][2], view[1][2],
|
||||
view[2][2]));
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const glm::vec3 axis = gizmo.localSpace
|
||||
? glm::normalize(rotM[i])
|
||||
: glm::vec3(float(i == 0), float(i == 1),
|
||||
float(i == 2));
|
||||
maskAxis[i] = isScale
|
||||
? glm::abs(glm::dot(camToObj, axis))
|
||||
< kScaleMinDot ||
|
||||
glm::abs(glm::dot(viewFwd, axis))
|
||||
> kScaleMaxFwdDot
|
||||
: glm::length(glm::cross(camToObj, axis))
|
||||
< kMoveMinSin;
|
||||
}
|
||||
}
|
||||
ImGuizmo::SetAxisMask(maskAxis[0], maskAxis[1],
|
||||
maskAxis[2]);
|
||||
}
|
||||
|
||||
// Ctrl 吸附(UE Grid Snap 语义)。库的约定
|
||||
//(docs/documentation.md):平移 = 世界单位 float[3]
|
||||
// 逐轴;旋转 = snap[0] 按度(库内自行 DEG2RAD,传弧
|
||||
// 度会被解释成极小步进);缩放 = 只取 snap[0]
|
||||
float snap[3] = {0.0f, 0.0f, 0.0f};
|
||||
const bool snapping = ImGui::GetIO().KeyCtrl;
|
||||
if (snapping) {
|
||||
if (gizmo.op == ImGuizmo::TRANSLATE) {
|
||||
snap[0] = snap[1] = snap[2] = kSnapTranslate;
|
||||
} else if (gizmo.op == ImGuizmo::ROTATE) {
|
||||
snap[0] = kSnapRotateDeg; // 度,非弧度
|
||||
} else if (gizmo.op == ImGuizmo::SCALE) {
|
||||
snap[0] = kSnapScale;
|
||||
}
|
||||
}
|
||||
|
||||
ImGuizmo::Manipulate(&view[0][0], &projGL[0][0],
|
||||
static_cast<ImGuizmo::OPERATION>(gizmo.op),
|
||||
mode, &m[0][0], nullptr,
|
||||
snapping ? snap : nullptr);
|
||||
gizmo.dragging = ImGuizmo::IsUsing();
|
||||
|
||||
// 拖拽生效:分解 world 矩阵 → world SRT → 沿父链反解
|
||||
// local 写回(SetWorldPose;根实体 = 直写)。旋转用
|
||||
// decompose 出的 quat 直写(UE FTransform 语义)——
|
||||
// 不经欧拉往返,拖拽零漂移、无万向锁
|
||||
if (ImGuizmo::IsUsing()) {
|
||||
glm::vec3 scale, transl, skew;
|
||||
glm::quat rot;
|
||||
glm::vec4 persp;
|
||||
if (glm::decompose(m, scale, rot, transl, skew, persp)) {
|
||||
ctx.scene->SetWorldPose(selected, transl, rot,
|
||||
scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gizmo 状态提示(左上角 drawlist 叠加,原因同下:不占布局行)
|
||||
// PIE 运行态 = 黄色 PLAYING/PAUSED 提示替代编辑状态
|
||||
{
|
||||
char tip[128];
|
||||
if (playing) {
|
||||
const bool paused = ctx.pausedState && *ctx.pausedState;
|
||||
std::snprintf(tip, sizeof(tip),
|
||||
paused ? "PAUSED - Ctrl+P to stop"
|
||||
: "PLAYING - Ctrl+P to stop");
|
||||
} else {
|
||||
std::snprintf(tip, sizeof(tip),
|
||||
"Q/W/E/R: Sel/Move/Rot/Scale T: %s "
|
||||
"Snap: Ctrl F: Focus",
|
||||
gizmo.localSpace ? "Local" : "World");
|
||||
}
|
||||
const ImVec2 imgMin = ImGui::GetItemRectMin();
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(imgMin.x + 8.0f, imgMin.y + 8.0f),
|
||||
playing ? IM_COL32(255, 184, 46, 220)
|
||||
: IM_COL32(255, 255, 255, 150),
|
||||
tip);
|
||||
}
|
||||
|
||||
// Dynamic 刚体提示:物理每帧把姿态写回 Transform,拖拽即刻
|
||||
// 被覆盖(Static/Kinematic 编辑器权威,可直接拖)。
|
||||
// 用 drawlist 叠加而非 ImGui::TextDisabled:后者在 Image 下方
|
||||
// 占一行布局 → 滚动条出现/消失循环 → avail 抖动 → 渲染分辨率
|
||||
// 每帧重报(视口疯狂顶动);且默认字体无中文字形(显示 ?)
|
||||
if (playing) {
|
||||
// 运行态无 primary 相机实体:渲染回退编辑器视角(保持帧
|
||||
// 管线节拍),提示补相机
|
||||
ECS::TransformComponent *camT = nullptr;
|
||||
if (ViewCamera(ctx, camT) == nullptr) {
|
||||
const ImVec2 imgMin = ImGui::GetItemRectMin();
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(imgMin.x + 8.0f,
|
||||
imgMin.y + size.y * 0.5f),
|
||||
IM_COL32(255, 120, 80, 230),
|
||||
"No primary camera entity - showing editor "
|
||||
"view. Add a Camera component and check "
|
||||
"'Primary'");
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic 刚体提示(drawlist 叠加,理由同上)
|
||||
if (selected != entt::null) {
|
||||
if (const auto *rb = reg.try_get<ECS::RigidBodyComponent>(selected);
|
||||
rb != nullptr &&
|
||||
rb->type == ECS::RigidBodyComponent::BodyType::Dynamic) {
|
||||
const ImVec2 imgMin = ImGui::GetItemRectMin();
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(imgMin.x + 8.0f, imgMin.y + size.y - 20.0f),
|
||||
IM_COL32(255, 255, 255, 160),
|
||||
"(Dynamic body: transform driven by physics)");
|
||||
}
|
||||
}
|
||||
|
||||
// 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。
|
||||
// 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中)——否则按在
|
||||
// 操纵杆上的点击会穿透发拾取请求。PIE 运行态不出拾取:
|
||||
// 点击 = 游戏输入(UE Play 语义,编辑操作冻结)
|
||||
if (!playing && ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) &&
|
||||
!ImGuizmo::IsOver() && !ImGuizmo::IsUsing()) {
|
||||
RequestPickAt(ctx, size);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("(scene texture not ready)");
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
return picked;
|
||||
}
|
||||
|
||||
void ViewportPanel::RequestPickAt(const EditorContext &ctx,
|
||||
const ImVec2 &size) const {
|
||||
// 鼠标在 Image 内的位置 → NDC。Vulkan NDC y 与屏幕 v 同向
|
||||
//(向下为正,与渲染同构;skybox.frag 反投影交叉验证过方向)
|
||||
const ImVec2 mouse = ImGui::GetMousePos();
|
||||
const ImVec2 mn = ImGui::GetItemRectMin();
|
||||
const glm::vec2 ndc{
|
||||
(mouse.x - mn.x) / size.x * 2.0f - 1.0f,
|
||||
(mouse.y - mn.y) / size.y * 2.0f - 1.0f,
|
||||
};
|
||||
|
||||
// UE 式 GPU HitProxy:下一帧 id 渲染 + 请求像素 1×1 拷贝
|
||||
ctx.renderer->RequestPick(ndc);
|
||||
}
|
||||
|
||||
void ViewportPanel::LogPick(const EditorContext &ctx, entt::entity e) {
|
||||
// 命中详情:实体名;有物理体则附 collider 对照(诊断"物理盒 ≠
|
||||
// 视觉"的错位,如 M1911 的默认 0.5 半尺寸盒)
|
||||
auto ® = ctx.scene->GetRegistry();
|
||||
const auto &name = reg.get<ECS::NameComponent>(e).name;
|
||||
if (const auto *rb = reg.try_get<ECS::RigidBodyComponent>(e)) {
|
||||
const auto &tr = reg.get<ECS::TransformComponent>(e);
|
||||
ENGINE_INFO("[pick] '{}' (hitproxy) | collider={} type={} "
|
||||
"boxHalf=({:.2f},{:.2f},{:.2f}) sphereR={:.2f} "
|
||||
"bodyPos=({:.2f},{:.2f},{:.2f})",
|
||||
name,
|
||||
rb->collider == ECS::RigidBodyComponent::ColliderType::Box
|
||||
? "box" : "sphere",
|
||||
rb->type == ECS::RigidBodyComponent::BodyType::Static
|
||||
? "static" : "dynamic",
|
||||
rb->boxHalfExtents.x, rb->boxHalfExtents.y,
|
||||
rb->boxHalfExtents.z, rb->sphereRadius,
|
||||
tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z);
|
||||
} else {
|
||||
ENGINE_INFO("[pick] '{}' (hitproxy) | (no collider)", name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// ViewportPanel:场景渲染输出面板。
|
||||
//
|
||||
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
|
||||
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
|
||||
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
|
||||
//
|
||||
// 点击拾取(UE 式 GPU HitProxy):单击 → NDC 请求 id 渲染;结果
|
||||
// 由 Draw 每帧轮询取走(像素级,与视觉画面一致)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_VIEWPORTPANEL_H
|
||||
#define HELLOVULKAN_VIEWPORTPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <imgui.h> // ImVec2(RequestPickAt 签名)
|
||||
#include <entt/entt.hpp>
|
||||
#include <optional>
|
||||
|
||||
// Viewport 面板每帧上报的输入焦点状态(Hazel 模式:编辑器据此决定
|
||||
// 游戏侧输入是否放行——悬停/持焦 Viewport 才拥有 WASD/鼠标)
|
||||
struct ViewportInputState {
|
||||
bool hovered = false; // 鼠标悬停在 Viewport 面板上
|
||||
bool focused = false; // Viewport 面板持有键盘焦点
|
||||
};
|
||||
|
||||
// Gizmo 交互状态(Editor 持有、ViewportPanel 写):
|
||||
// op 取 ImGuizmo::OPERATION 位掩码(TRANSLATE=7=三轴位或运算,非 0
|
||||
// 起始枚举;0 = 无任何轴位 → Gizmo 完全不绘制),int 存储避免本
|
||||
// 头文件 include ImGuizmo.h(拖入 imgui 宏依赖顺序)
|
||||
// Q 纯选择(op=0 不出杆)、W/E/R 切换 op、T 切换 local/world 空间
|
||||
// (UE 语义);dragging 拉起 Editor::BlockingMouse 门控
|
||||
struct GizmoState {
|
||||
int op = 7; // ImGuizmo::TRANSLATE(TRANSLATE_X|Y|Z)
|
||||
bool localSpace = true; // 操纵空间:true=模型轴(旋转/缩放语义),
|
||||
// T 键切 world
|
||||
bool dragging = false; // 拖拽中(IsUsing)→ 鼠标归 Gizmo 不给游戏侧
|
||||
};
|
||||
|
||||
class ViewportPanel {
|
||||
public:
|
||||
// 返回值:本帧拾取命中的实体(无点击/未命中 = nullopt 或 null);
|
||||
// Editor 据此 SelectEntity(面板不持有选中状态)
|
||||
std::optional<entt::entity> Draw(const EditorContext& ctx,
|
||||
ViewportInputState& out,
|
||||
GizmoState& gizmo,
|
||||
entt::entity selected) const;
|
||||
|
||||
private:
|
||||
// Image 区域内的鼠标位置 → NDC → Renderer::RequestPick
|
||||
//(结果下一帧由 Draw 轮询 TakePickResult 取走)
|
||||
void RequestPickAt(const EditorContext& ctx, const ImVec2& size) const;
|
||||
|
||||
// 命中打印:实体名 + collider 参数对照(诊断物理盒 ≠ 视觉用)
|
||||
static void LogPick(const EditorContext& ctx, entt::entity e);
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_VIEWPORTPANEL_H
|
||||
+147
-112
@@ -1,36 +1,53 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/20.
|
||||
//
|
||||
// SandboxApp:第一个基于 Application 框架的程序。
|
||||
// SandboxApp:基于 Application 框架的编辑器宿主程序。
|
||||
//
|
||||
// main 的全部职责:定义一个 Application 派生类并运行它。
|
||||
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理;
|
||||
// 这里只写"程序逻辑":创建实体、逐帧更新组件、事件响应。
|
||||
// main 的职责边界:
|
||||
// - 定义 Application 派生类并运行它
|
||||
// - 编辑器形态驱动 EditorCamera(Scene view 专用相机,非场景对象)
|
||||
// - 异步环境加载、全局热键、输入归属门控
|
||||
// - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现
|
||||
// 任何 ImGui 控件)
|
||||
//
|
||||
// 场景内容(实体/组件)由编辑器 File 菜单 Save/Load 场景文件管理,
|
||||
// 相机实体属于场景数据、只在游戏形态参与渲染(UE 语义)。
|
||||
//
|
||||
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理。
|
||||
//
|
||||
|
||||
#include <Engine/Core/Application.h>
|
||||
#include <Engine/Core/Time.h>
|
||||
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Core/Stats.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/ECS/EntityHandle.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Renderer/Renderer.h>
|
||||
#include <Engine/Renderer/EditorCamera.h>
|
||||
#include <Engine/Serialization/SceneSerializer.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <Engine/Core/Input.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <filesystem>
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
// editor/game 编译期分离(HELLOVULKAN_EDITOR CMake option):editor 形态
|
||||
// 挂接 docking Editor;game 形态 = Direct 直渲 + primary 相机,零编辑器代码
|
||||
#ifdef HELLO_EDITOR
|
||||
#include "Editor.h"
|
||||
#include "EditorContext.h"
|
||||
#endif
|
||||
|
||||
using namespace Engine;
|
||||
using namespace Engine::ECS;
|
||||
|
||||
namespace
|
||||
{
|
||||
class SandboxApp final : public Application
|
||||
{
|
||||
public:
|
||||
SandboxApp()
|
||||
: Application("HelloVulkan", 800, 600)
|
||||
: Application("HelloVulkan", 1920, 1080)
|
||||
{
|
||||
}
|
||||
|
||||
~SandboxApp() override
|
||||
{
|
||||
}
|
||||
|
||||
@@ -39,139 +56,157 @@ protected:
|
||||
|
||||
void OnInit() override
|
||||
{
|
||||
Scene& scene = GetScene();
|
||||
Renderer& renderer = GetRenderer();
|
||||
#ifdef HELLO_EDITOR
|
||||
renderer.SetDisplayMode(kDisplayViewport);
|
||||
|
||||
// ImGui 叠加层(框架只做管线桥接,UI 内容全部在这里定义)
|
||||
|
||||
// ImGui 叠加层(框架只做管线桥接;UI 内容 = docking Editor)
|
||||
EnableImGui();
|
||||
|
||||
// 模型资产(AssetManager 负责加载/缓存/生命周期,返回句柄)
|
||||
auto backpack = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
|
||||
|
||||
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
|
||||
auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
|
||||
|
||||
m_Ground = scene.CreateEntity("Ground");
|
||||
m_Ground.AddComponent<TransformComponent>()
|
||||
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f))
|
||||
.Compose();
|
||||
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
|
||||
|
||||
// 相机实体(primary = 当前渲染视角)
|
||||
m_Camera = scene.CreateEntity("MainCamera");
|
||||
auto& camera = m_Camera.AddComponent<CameraComponent>();
|
||||
camera.primary = true;
|
||||
camera.camera.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f));
|
||||
|
||||
// 背包实体 × 3(悬浮在地面上方)
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
auto e = scene.CreateEntity("Backpack" + std::to_string(i));
|
||||
|
||||
e.AddComponent<TransformComponent>()
|
||||
.SetPosition(glm::vec3((i - 1) * 1.5f, 0.0f, 0.0f))
|
||||
.SetScale(glm::vec3(0.5f))
|
||||
.Compose();
|
||||
|
||||
e.AddComponent<MeshRendererComponent>(MeshRendererComponent{backpack});
|
||||
|
||||
m_Spinners.push_back(e);
|
||||
// Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
|
||||
InitEditor();
|
||||
#else
|
||||
// game 形态:全窗口 Direct 直渲(Renderer 默认即 kDisplayDirect),
|
||||
// 相机由场景 primary camera 提供,无 UI。
|
||||
// 游戏流:Stopped → Loading(OnUpdate 泵场景加载)→ Running
|
||||
GetGameFlow().RequestStart();
|
||||
#endif
|
||||
}
|
||||
|
||||
// 灯光实体(第一个灯 = 阴影光源)
|
||||
m_KeyLight = scene.CreateEntity("KeyLight");
|
||||
m_KeyLight.AddComponent<LightComponent>(
|
||||
LightComponent{glm::vec3(-0.5f, -0.6f, -0.35f),
|
||||
glm::vec4(1.0f, 0.95f, 0.9f, 1.0f)});
|
||||
|
||||
scene.CreateEntity("FillLight")
|
||||
.AddComponent<LightComponent>(
|
||||
LightComponent{glm::vec3(0.5f, -0.5f, 0.3f),
|
||||
glm::vec4(0.3f, 0.5f, 1.0f, 0.8f)});
|
||||
}
|
||||
|
||||
void OnUpdate(f32 deltaTime) override
|
||||
void OnUpdate(const FrameTime& frameTime) override
|
||||
{
|
||||
m_Time += deltaTime;
|
||||
|
||||
// 相机(WASD/Space/Shift 移动)
|
||||
m_Camera.GetComponent<CameraComponent>()
|
||||
.camera.Update(GetWindow(), deltaTime);
|
||||
|
||||
// 旋转动画:编辑 Transform 分量后 Compose 重算 world
|
||||
for (size_t i = 0; i < m_Spinners.size(); ++i) {
|
||||
auto& t = m_Spinners[i].GetComponent<TransformComponent>();
|
||||
t.position = glm::vec3((static_cast<f32>(i) - 1.0f) * 1.5f, 0.0f, 0.0f);
|
||||
t.rotation.y = m_Time * 60.0f + static_cast<f32>(i) * 40.0f;
|
||||
t.scale = glm::vec3(0.5f);
|
||||
t.Compose();
|
||||
#ifdef HELLO_EDITOR
|
||||
// 编辑器形态:驱动编辑器相机(RMB 飞行 / MMB 焦点环绕 /
|
||||
// 滚轮推拉)。暂停(timeScale=0)时仍可飞行——unscaled dt。
|
||||
// PIE 内嵌播放:渲染源已切 primary 相机,编辑器相机停驱
|
||||
//(否则与游戏脚本抢 WASD,Stop 后视角也被偷跑)。
|
||||
// 游戏形态:相机实体由游戏逻辑驱动(本沙盒无逻辑 = 静止)
|
||||
if (GetRenderer().GetDisplayMode() == kDisplayViewport &&
|
||||
!m_Editor->IsPlaying())
|
||||
{
|
||||
GetRenderer().GetEditorCamera().Update(frameTime.UnscaledDt());
|
||||
}
|
||||
#else
|
||||
// Loading 泵(同步示例):场景文件存在则加载,不存在 =
|
||||
// 空场景照常进 Running(引擎冒烟路径)。加载完成后必须
|
||||
// CompleteLoading,否则物理/脚本门控永远跳过(卡死 Loading)
|
||||
if (GetGameFlow().IsIn(GameFlowState::Loading))
|
||||
{
|
||||
if (std::filesystem::exists(m_ScenePath))
|
||||
{
|
||||
if (Serialization::SceneSerializer::Deserialize(
|
||||
GetScene(), GetAssets(), m_ScenePath))
|
||||
{
|
||||
ENGINE_INFO("Game: scene loaded from {}", m_ScenePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
ENGINE_ERROR("Game: scene load failed: {}",
|
||||
m_ScenePath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ENGINE_WARN("Game: scene file not found ({}), "
|
||||
"starting empty", m_ScenePath);
|
||||
}
|
||||
GetGameFlow().CompleteLoading();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- UI(全部委托 docking Editor;本文件零 ImGui 控件)----
|
||||
|
||||
void OnImGuiRender() override
|
||||
{
|
||||
#ifdef HELLO_EDITOR
|
||||
m_Editor->Draw();
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- 事件 ----
|
||||
|
||||
// ---- 叠加层 UI(唯一允许出现 ImGui 控件的地方)----
|
||||
|
||||
void OnRenderOverlay() override
|
||||
#ifdef HELLO_EDITOR
|
||||
void OnWindowClose(const Event& event) override
|
||||
{
|
||||
if (!m_OverlayVisible) return;
|
||||
|
||||
ImGui::Begin("Scene Debug");
|
||||
|
||||
// 阴影光源方向:拖动滑条实时看影子变化
|
||||
if (auto* light = m_KeyLight.TryGetComponent<LightComponent>()) {
|
||||
ImGui::Text("Key Light (shadow caster)");
|
||||
ImGui::SliderFloat3("direction",
|
||||
&light->direction.x, -1.0f, 1.0f);
|
||||
if (ImGui::Button("reset")) {
|
||||
light->direction = glm::vec3(-0.5f, -0.6f, -0.35f);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("fps: %.1f frame: %llu",
|
||||
1.0f / GetDeltaTime(),
|
||||
static_cast<unsigned long long>(GetFrameCount()));
|
||||
|
||||
ImGui::End();
|
||||
// 退出确认走 Editor 的模态框(ImGui 必须可见才有 UI)
|
||||
SetImGuiEnabled(true);
|
||||
m_Editor->RequestQuit();
|
||||
}
|
||||
#endif
|
||||
|
||||
void OnKeyPress(const Event& event) override
|
||||
{
|
||||
if (event.key == GLFW_KEY_GRAVE_ACCENT) {
|
||||
m_OverlayVisible = !m_OverlayVisible; // ` 反折叠加层
|
||||
}
|
||||
if (event.key == GLFW_KEY_F1) {
|
||||
STATS.PrintComparison();
|
||||
}
|
||||
if (event.key == GLFW_KEY_ESCAPE) {
|
||||
Close();
|
||||
// 键码统一走 Input::Key(App 层零 GLFW 依赖)
|
||||
#ifdef HELLO_EDITOR
|
||||
if (event.key == static_cast<int>(Input::Key::GraveAccent))
|
||||
{
|
||||
SetImGuiEnabled(!GetImGuiEnabled());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void OnWindowResize(const Event& event) override
|
||||
{
|
||||
ENGINE_INFO("App: window resized to {}x{}", event.width, event.height);
|
||||
}
|
||||
|
||||
private:
|
||||
EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空
|
||||
EntityHandle m_Ground{};
|
||||
EntityHandle m_KeyLight{};
|
||||
std::vector<EntityHandle> m_Spinners;
|
||||
bool m_OverlayVisible = true;
|
||||
f32 m_Time = 0.0f;
|
||||
#ifdef HELLO_EDITOR
|
||||
// Editor 注入:门面指针 + 反向能力回调(面板 → 沙盒)
|
||||
void InitEditor()
|
||||
{
|
||||
EditorContext ctx;
|
||||
ctx.renderer = &GetRenderer();
|
||||
ctx.scene = &GetScene();
|
||||
ctx.assets = &GetAssets();
|
||||
ctx.app = this;
|
||||
ctx.flow = &GetGameFlow();
|
||||
ctx.editorCamera = &GetRenderer().GetEditorCamera();
|
||||
// 编辑器态默认冻结:GameFlow 构造即 timeScale=0(Stopped 语义),
|
||||
// timeScale 权威在 GameFlow,此处不再手设
|
||||
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>();
|
||||
#else
|
||||
// game 形态:启动场景路径(CWD = bin;编辑器 Save 的产物位置)
|
||||
const char* m_ScenePath = "assets/scenes/sandbox.scene";
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
Logger::Initialize();
|
||||
|
||||
{
|
||||
SandboxApp app;
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
app.Run();
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
ENGINE_FATAL("Exception: {}", e.what());
|
||||
Logger::Shutdown();
|
||||
return -1;
|
||||
}
|
||||
} // app 析构:成员(Device/Renderer/Window…)全部销毁完毕
|
||||
|
||||
Logger::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// Follower.cpp:跟随者行为(NPC 自动跟随主角,UE AIController 的简化位)。
|
||||
//
|
||||
// 挂在每个 NPC 实体上(各自 CharacterController + Animation)。主角由
|
||||
// TargetName 指定(默认 "player",按 NameComponent 名称匹配——引擎无实体
|
||||
// 引用参数类型,名称匹配是标准做法)。逻辑分四层,与 WASDMove 同一套手感:
|
||||
// - 距离闸门(迟滞):拉开到 StopDistance+ResumeGap 才起步,靠近到
|
||||
// StopDistance 即停——"到附近就停止跟随",边界不抖动
|
||||
// - 速度分档(迟滞):距离超过 RunDistance 改跑,回落到 0.85× 才退回
|
||||
// 走——"距离越大走/跑越快"
|
||||
// - 动量整形:真实速度逐帧逼近期望速度(起步渐快、到点刹停),帧率无关
|
||||
// - 动画按真实速度分档(idle/walk/run),迟滞防抖
|
||||
// 移动驱动走 PhysicsWorld 的 CharacterController 门面(水平喂意图速度,
|
||||
// Y 归物理重力);无 CC 时回退直接平移。多个跟随者用 Offset 错开站位。
|
||||
//
|
||||
|
||||
#include <Engine/Scripting/Script.h>
|
||||
#include <Engine/Core/Application.h>
|
||||
#include <Engine/Physics/PhysicsWorld.h>
|
||||
|
||||
#include "ScriptLog.h"
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using namespace Engine;
|
||||
|
||||
namespace {
|
||||
|
||||
class Follower final : public Engine::Scripting::ScriptBehavior {
|
||||
public:
|
||||
const char* Name() const override { return "Follower"; }
|
||||
|
||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
||||
pb.Field("TargetName", &m_TargetName, std::string("player"));
|
||||
// 相对目标的站位偏移(世界系;多个跟随者错开,防叠一起)
|
||||
pb.Field("Offset", &m_Offset, glm::vec3{0.0f});
|
||||
pb.Field("WalkSpeed", &m_WalkSpeed, 3.0f); // 走路 m/s
|
||||
pb.Field("RunSpeed", &m_RunSpeed, 6.0f); // 跑步 m/s
|
||||
pb.Field("StopDistance", &m_StopDistance, 2.5f); // 到点停(米)
|
||||
pb.Field("ResumeGap", &m_ResumeGap, 1.5f); // 再起步的距离余量
|
||||
pb.Field("RunDistance", &m_RunDistance, 10.0f); // 超过即改跑(米)
|
||||
pb.Field("TurnResponse", &m_TurnResponse, 10.0f);// 转向阻尼
|
||||
pb.Field("AccelResponse", &m_AccelResponse, 6.0f); // 加速响应
|
||||
pb.Field("StopResponse", &m_StopResponse, 12.0f); // 制动响应
|
||||
// clip 按名引用(Unity anim.Play("Walk") 语义)
|
||||
pb.Field("IdleClip", &m_IdleClip, std::string("idle"));
|
||||
pb.Field("WalkClip", &m_WalkClip, std::string("walk"));
|
||||
pb.Field("RunClip", &m_RunClip, std::string("run"));
|
||||
}
|
||||
|
||||
void OnStart() override {
|
||||
if (ResolveTarget()) {
|
||||
SLOG_INFO("[Follower] 跟随目标 '{}' 就绪", m_TargetName);
|
||||
} else {
|
||||
SLOG_WARN("[Follower] 未找到目标 '{}'(按 NameComponent 匹配)",
|
||||
m_TargetName);
|
||||
}
|
||||
}
|
||||
|
||||
void OnUpdate(float dt) override {
|
||||
auto& reg = App().GetScene().GetRegistry();
|
||||
|
||||
// 目标失效(被销毁/未就绪)→ 重解析;仍无则静止待命
|
||||
if (m_Target == entt::null || !reg.valid(m_Target)) {
|
||||
m_Target = entt::null;
|
||||
if (!ResolveTarget()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const glm::vec3 tp =
|
||||
reg.get<Engine::ECS::TransformComponent>(m_Target)
|
||||
.GetPosition();
|
||||
const glm::vec3 self = Transform().GetPosition();
|
||||
glm::vec3 delta = (tp + m_Offset) - self;
|
||||
delta.y = 0.0f; // 只取水平面(角色不俯仰)
|
||||
const float dist = glm::length(delta);
|
||||
|
||||
// ---- 距离闸门(迟滞):到点停、拉开距离再起步 ----
|
||||
if (!m_Moving) {
|
||||
if (dist > m_StopDistance + m_ResumeGap) {
|
||||
m_Moving = true;
|
||||
}
|
||||
} else if (dist <= m_StopDistance) {
|
||||
m_Moving = false;
|
||||
}
|
||||
|
||||
// ---- 速度分档(迟滞):远则跑、近则走 ----
|
||||
glm::vec3 desired{0.0f};
|
||||
if (m_Moving && dist > 1e-4f) {
|
||||
if (!m_Sprinting && dist >= m_RunDistance) {
|
||||
m_Sprinting = true;
|
||||
} else if (m_Sprinting && dist <= m_RunDistance * 0.85f) {
|
||||
m_Sprinting = false;
|
||||
}
|
||||
const float speed = m_Sprinting ? m_RunSpeed : m_WalkSpeed;
|
||||
desired = (delta / dist) * speed;
|
||||
}
|
||||
|
||||
// ---- 动量整形:真实速度逐帧逼近期望(起步渐快/到点刹停)----
|
||||
const float response =
|
||||
glm::length(desired) > 0.01f ? m_AccelResponse : m_StopResponse;
|
||||
m_PlanarVel +=
|
||||
(desired - m_PlanarVel) * (1.0f - std::exp(-response * dt));
|
||||
if (glm::length(desired - m_PlanarVel) < 0.02f) {
|
||||
m_PlanarVel = desired; // 贴齐,防低速蠕动
|
||||
}
|
||||
|
||||
// ---- 驱动:角色控制器喂水平速度(Y 归物理);无则直接平移 ----
|
||||
auto& phy = App().GetPhysics();
|
||||
const bool hasCC =
|
||||
TryGetComponent<Engine::ECS::CharacterControllerComponent>() !=
|
||||
nullptr;
|
||||
if (hasCC) {
|
||||
const glm::vec3 cur = phy.GetCharacterVelocity(EntityBits());
|
||||
phy.SetCharacterVelocity(
|
||||
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
|
||||
} else {
|
||||
Transform().SetPosition(self + m_PlanarVel * dt);
|
||||
}
|
||||
|
||||
// ---- 朝向:面向真实速度方向(模型默认 +Z),低速锁定 ----
|
||||
if (glm::length(m_PlanarVel) > 0.3f) {
|
||||
const float targetYaw =
|
||||
std::atan2(m_PlanarVel.x, m_PlanarVel.z);
|
||||
const glm::vec3 modelFwd =
|
||||
Transform().GetRotationQuat() * glm::vec3(0.0f, 0.0f, 1.0f);
|
||||
const float curYaw = std::atan2(modelFwd.x, modelFwd.z);
|
||||
const float dyaw = std::atan2(std::sin(targetYaw - curYaw),
|
||||
std::cos(targetYaw - curYaw));
|
||||
const float k = 1.0f - std::exp(-m_TurnResponse * dt);
|
||||
Transform().SetRotationQuat(
|
||||
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
|
||||
}
|
||||
|
||||
// ---- 动画:按真实速度分档(迟滞防临界抖)----
|
||||
const float planarSpeed = glm::length(m_PlanarVel);
|
||||
std::string target;
|
||||
if (planarSpeed < 0.15f) {
|
||||
target = m_IdleClip;
|
||||
} else if (m_CurClip == m_RunClip) {
|
||||
target = (planarSpeed >= m_RunSpeed * 0.7f) ? m_RunClip
|
||||
: m_WalkClip;
|
||||
} else {
|
||||
target = (planarSpeed >= m_RunSpeed * 0.9f) ? m_RunClip
|
||||
: m_WalkClip;
|
||||
}
|
||||
if (target != m_CurClip) {
|
||||
if (auto* anim =
|
||||
TryGetComponent<Engine::ECS::AnimationComponent>();
|
||||
anim != nullptr && anim->Play(target, 0.35f)) {
|
||||
m_CurClip = target; // 播放成功才记账(模型未就绪下帧重试)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// 按 NameComponent 名称解析目标(跳过自身);失败返回 false
|
||||
bool ResolveTarget() {
|
||||
auto& reg = App().GetScene().GetRegistry();
|
||||
auto view = reg.view<Engine::ECS::NameComponent>();
|
||||
for (auto e : view) {
|
||||
if (static_cast<u32>(entt::to_integral(e)) == EntityBits()) {
|
||||
continue;
|
||||
}
|
||||
if (view.get<Engine::ECS::NameComponent>(e).name ==
|
||||
m_TargetName) {
|
||||
m_Target = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string m_TargetName = "player";
|
||||
glm::vec3 m_Offset{0.0f};
|
||||
float m_WalkSpeed = 3.0f;
|
||||
float m_RunSpeed = 6.0f;
|
||||
float m_StopDistance = 2.5f;
|
||||
float m_ResumeGap = 1.5f;
|
||||
float m_RunDistance = 10.0f;
|
||||
float m_TurnResponse = 10.0f;
|
||||
float m_AccelResponse = 6.0f;
|
||||
float m_StopResponse = 12.0f;
|
||||
std::string m_IdleClip = "idle";
|
||||
std::string m_WalkClip = "walk";
|
||||
std::string m_RunClip = "run";
|
||||
|
||||
entt::entity m_Target = entt::null; // 运行时状态(非参数)
|
||||
bool m_Moving = false; // 距离闸门状态(迟滞)
|
||||
bool m_Sprinting = false; // 速度档状态(迟滞)
|
||||
glm::vec3 m_PlanarVel{0.0f}; // 平滑后的水平真实速度
|
||||
std::string m_CurClip = "idle"; // 当前 clip(边沿检测基准)
|
||||
};
|
||||
|
||||
ENGINE_REGISTER_SCRIPT(Follower)
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,704 @@
|
||||
//
|
||||
// FractureLib.h:共享碎裂库(自 WallFracture.cpp 提取,行为逐行保持)。
|
||||
//
|
||||
// 碎裂语义 = 玻璃/陶瓷式("碎掉"),非爆炸飞散,碎片几何 = 2D
|
||||
// Voronoi 分割:
|
||||
// 碎裂瞬间在面上撒种子点(命中点周边加密 → 孔洞周围碎片小、
|
||||
// 远处碎片大,玻璃碎的典型分布),对每颗种子做垂直平分线半平面
|
||||
// 交集得到不规则凸多边形 cell,沿面厚挤出成凸棱柱——碎片在碎裂
|
||||
// 前一刻完美拼合原面,碎开后沿裂纹分离。碰撞形状不走 Box,直接
|
||||
// 用 MeshCollider(Dynamic → Jolt ConvexHull,数据源 = 网格 CPU
|
||||
// 位置副本,PhysicsWorld::BuildMeshShape)→ 不规则碎片连碰撞体
|
||||
// 都是真的,落地翻滚/堆积接触自然。
|
||||
//
|
||||
// 初速 = 径向 × 距离指数衰减(只有命中点周边的碎片被推动,远处
|
||||
// 几乎原地悬置)+ 沿命中法线轻微外推(朝弹侧剥落)+ 微弱上抛
|
||||
// ——重力是塌落的主导,整体呈"裂纹分离 → 孔洞塌落 → 碎块堆在
|
||||
// 墙脚"的观感。
|
||||
//
|
||||
// 用法(宿主 = 挂在可碎实体上的脚本):
|
||||
// Fracture::Burst burst_; // 碎片簿记 + 生命周期
|
||||
// DeclareParams 直绑 burst_.p 各字段(pb.Field("Count",
|
||||
// &burst_.p.count, ...))——参数必须与碎裂逻辑读的是同一对象,
|
||||
// 绑到独立副本会让 Count/Lifetime 等全部失效
|
||||
// HostView HV() { // 桥接(脚本自身成员函数内可访问
|
||||
// return {App(), EntityBits(),// protected 基类设施,库内只走公共 API)
|
||||
// [this](auto& n) { return Spawn(n); },
|
||||
// [this](auto e) { Destroy(e); }};
|
||||
// }
|
||||
// OnCollisionEnter → burst_.TryFire(HV(), other, point, normal)
|
||||
// OnUpdate → burst_.Tick(HV(), dt)
|
||||
// OnDestroy → burst_.Shutdown(HV())
|
||||
// 外观:默认继承宿主(墙)的生效材质(底色/贴图/透明),碎块与墙面
|
||||
// 一致;要独立外观(玻璃靶 = 半透明 + 自发光)Fire 前置
|
||||
// burst_.look.inheritFromHost = false 再填 baseColor/emissive
|
||||
// HostView 每次调用现填(不存指针:组件存储重排/实例重建均安全)
|
||||
//
|
||||
// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按宿主世界
|
||||
// 尺寸烘焙、碎片实体复用宿主 world pos/rot(scale 恒 1,防
|
||||
// ConvexHull 顶点二次缩放)——宿主须无旋转摆放。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_FRACTURE_LIB_H
|
||||
#define HELLOVULKAN_FRACTURE_LIB_H
|
||||
|
||||
#include <Engine/Scripting/Script.h>
|
||||
#include <Engine/Core/Application.h>
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Assets/AssetManager.h> // LoadModel(门面只前向声明)
|
||||
#include <Engine/Renderer/Model.h> // ParsedModel/ParsedMesh(程序化碎片几何)
|
||||
#include <Engine/Renderer/Vertex.h> // Vertex 聚合布局
|
||||
#include <Engine/Physics/PhysicsWorld.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <random>
|
||||
|
||||
namespace Engine::Fracture {
|
||||
|
||||
// bits(PhysicsWorld 键)→ 实体(还原位模式;用前自行 valid 校验)
|
||||
inline ECS::Entity EntityFromBits(u32 bits) {
|
||||
return static_cast<ECS::Entity>(bits);
|
||||
}
|
||||
|
||||
// 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记
|
||||
// 需要任意实体的同款换算)
|
||||
inline u32 BitsOf(const ECS::EntityHandle& e) {
|
||||
return static_cast<u32>(
|
||||
entt::to_integral(static_cast<entt::entity>(e)));
|
||||
}
|
||||
|
||||
inline f32 Rand(f32 lo, f32 hi) {
|
||||
thread_local std::mt19937 rng{std::random_device{}()};
|
||||
return std::uniform_real_distribution<f32>(lo, hi)(rng);
|
||||
}
|
||||
|
||||
// 碎片模型代序号(全局递增):每代碎片一组唯一缓存键 → 每次
|
||||
// 命中都重新随机生成形状(命中点量化键会让同位置重复命中
|
||||
// 复用首次几何——缓存去重把新几何丢弃,碎片就"固定"了)
|
||||
inline u32& GenSeq() {
|
||||
static u32 s = 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---- 宿主桥接(脚本侧现填;库内只走公共 API)----
|
||||
// spawn/destroy 走脚本成员(内部才能访问 ScriptBehavior 的
|
||||
// protected 底座),app/实体走数据。std::function 捕 this 无
|
||||
// 分配(小对象优化),随调用栈存亡,无悬垂面
|
||||
struct HostView {
|
||||
Application& app;
|
||||
u32 entityBits = 0;
|
||||
ECS::Entity entity() const { return EntityFromBits(entityBits); }
|
||||
std::function<ECS::EntityHandle(const std::string&)> spawn;
|
||||
std::function<void(ECS::Entity)> destroy;
|
||||
};
|
||||
|
||||
// ---- 2D Voronoi(碎裂裂纹)----
|
||||
//
|
||||
// cell = 相对其它种子的垂直平分线半平面交集,初始域 = 墙面矩形。
|
||||
// Sutherland–Hodgman 凸多边形裁剪:保留靠近 p 的一侧
|
||||
//(dot(a,x) <= b,a = normalize(q-p),b = dot(a,p)+|q-p|/2)。
|
||||
// 输入多边形须 CCW(初始矩形即 CCW,裁剪保序)。
|
||||
inline std::vector<glm::vec2> ClipByBisector(std::vector<glm::vec2> poly,
|
||||
const glm::vec2& p,
|
||||
const glm::vec2& q) {
|
||||
const glm::vec2 d = q - p;
|
||||
const f32 len = glm::length(d);
|
||||
if (len < 1e-6f) return poly;
|
||||
const glm::vec2 a = d / len;
|
||||
const f32 b = glm::dot(a, p) + 0.5f * len;
|
||||
|
||||
std::vector<glm::vec2> out;
|
||||
out.reserve(poly.size() + 1);
|
||||
for (size_t i = 0; i < poly.size(); ++i) {
|
||||
const glm::vec2& c = poly[i];
|
||||
const glm::vec2& n = poly[(i + 1) % poly.size()];
|
||||
const f32 dc = glm::dot(a, c) - b;
|
||||
const f32 dn = glm::dot(a, n) - b;
|
||||
if (dc <= 0.0f) out.push_back(c);
|
||||
if ((dc < 0.0f) != (dn < 0.0f)) { // 边跨越裁剪线 → 求交点
|
||||
const f32 t = dc / (dc - dn);
|
||||
out.push_back(c + t * (n - c));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 种子 p 的 Voronoi cell(域 = [-hw,hw]×[-hh,hh] 的墙面矩形)
|
||||
inline std::vector<glm::vec2> VoronoiCell(const glm::vec2& p,
|
||||
const std::vector<glm::vec2>& seeds,
|
||||
f32 hw, f32 hh) {
|
||||
std::vector<glm::vec2> poly = {
|
||||
{-hw, -hh}, {hw, -hh}, {hw, hh}, {-hw, hh}};
|
||||
for (const glm::vec2& q : seeds) {
|
||||
if (glm::dot(q - p, q - p) < 1e-10f) continue; // 自身
|
||||
poly = ClipByBisector(std::move(poly), p, q);
|
||||
if (poly.size() < 3) return {}; // 退化(最小间距保护,罕见)
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
|
||||
// cell(CCW 凸多边形,墙局部 XY)沿 z ∈ [-hz, +hz] 挤出成棱柱:
|
||||
// 顶/底面 fan 三角化 + 侧面逐边 quad,per-face 法线。返回 mesh
|
||||
// 数据;cpuPositions 单独回传(ConvexHull 物理数据源)。
|
||||
struct PrismMesh {
|
||||
std::vector<Engine::Vertex> vertices;
|
||||
std::vector<u32> indices;
|
||||
std::vector<glm::vec3> cpuPositions;
|
||||
};
|
||||
|
||||
// hw/hh = 宿主半尺寸:顶/底面 UV 由 cell 的墙局部坐标归一化到 [0,1]
|
||||
//(墙面矩形 UV 约定,与立方体面 UV 对齐)——继承墙材质贴图时,碎片
|
||||
// 贴图与墙面连续,不会各碎片自成一格
|
||||
inline PrismMesh CellToPrism(const std::vector<glm::vec2>& cell, f32 hz,
|
||||
f32 hw, f32 hh) {
|
||||
PrismMesh m;
|
||||
const size_t n = cell.size();
|
||||
const size_t topBase = 0;
|
||||
const size_t botBase = n;
|
||||
m.vertices.reserve(n * 2);
|
||||
const f32 invW = hw > 1e-6f ? 0.5f / hw : 0.0f;
|
||||
const f32 invH = hh > 1e-6f ? 0.5f / hh : 0.0f;
|
||||
const auto uvOf = [&](const glm::vec2& p) {
|
||||
return glm::vec2{p.x * invW + 0.5f, p.y * invH + 0.5f};
|
||||
};
|
||||
|
||||
for (const glm::vec2& p : cell) {
|
||||
const Engine::Vertex v{
|
||||
{p.x, p.y, hz}, {0.0f, 0.0f, 1.0f},
|
||||
uvOf(p), {1.0f, 0.0f, 0.0f}};
|
||||
m.vertices.push_back(v);
|
||||
}
|
||||
for (const glm::vec2& p : cell) {
|
||||
const Engine::Vertex v{
|
||||
{p.x, p.y, -hz}, {0.0f, 0.0f, -1.0f},
|
||||
uvOf(p), {1.0f, 0.0f, 0.0f}};
|
||||
m.vertices.push_back(v);
|
||||
}
|
||||
|
||||
// top fan(CCW 从 +Z 看朝外)+ bottom fan(反向)
|
||||
for (size_t i = 1; i + 1 < n; ++i) {
|
||||
m.indices.push_back(topBase);
|
||||
m.indices.push_back(topBase + i);
|
||||
m.indices.push_back(topBase + i + 1);
|
||||
m.indices.push_back(botBase);
|
||||
m.indices.push_back(botBase + i + 1);
|
||||
m.indices.push_back(botBase + i);
|
||||
}
|
||||
|
||||
// 侧面:(i, N+i, N+i+1, i+1)——外法线 = CCW 边的右侧法向
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const size_t j = (i + 1) % n;
|
||||
const glm::vec2 e = cell[j] - cell[i];
|
||||
const f32 el = glm::length(e);
|
||||
const glm::vec3 sn = el > 1e-6f
|
||||
? glm::vec3{e.y / el, -e.x / el, 0.0f}
|
||||
: glm::vec3{0.0f, -1.0f, 0.0f};
|
||||
const size_t b = m.vertices.size();
|
||||
const Engine::Vertex q0{
|
||||
{cell[i].x, cell[i].y, hz}, sn,
|
||||
{0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}};
|
||||
const Engine::Vertex q1{
|
||||
{cell[i].x, cell[i].y, -hz}, sn,
|
||||
{0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}};
|
||||
const Engine::Vertex q2{
|
||||
{cell[j].x, cell[j].y, -hz}, sn,
|
||||
{1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}};
|
||||
const Engine::Vertex q3{
|
||||
{cell[j].x, cell[j].y, hz}, sn,
|
||||
{1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}};
|
||||
m.vertices.push_back(q0);
|
||||
m.vertices.push_back(q1);
|
||||
m.vertices.push_back(q2);
|
||||
m.vertices.push_back(q3);
|
||||
m.indices.insert(m.indices.end(),
|
||||
{u32(b), u32(b + 1), u32(b + 2),
|
||||
u32(b), u32(b + 2), u32(b + 3)});
|
||||
}
|
||||
|
||||
// 物理凸包数据源(BuildMeshShape Dynamic 分支逐顶点收集)
|
||||
m.cpuPositions.reserve(m.vertices.size());
|
||||
for (const auto& v : m.vertices) {
|
||||
m.cpuPositions.push_back(v.position);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
// ---- 程序化纯色还原(引擎零改动:颜色只存在于伪路径里)----
|
||||
// 程序化 Cube/Plane 把颜色烘进 1x1 sRGB 纹理,MaterialParams 留白、
|
||||
// 内建材质快照也没有颜色字段,故从伪路径尾段 "r,g,b[,a]" 还原线性
|
||||
// 底色——与 GPU 采样 sRGB 纹理的硬件线性化同式,直乘 baseColor 才等价
|
||||
inline f32 SrgbByteToLinear(u8 c) {
|
||||
const f32 s = static_cast<f32>(c) / 255.0f;
|
||||
return s <= 0.04045f ? s / 12.92f
|
||||
: std::pow((s + 0.055f) / 1.055f, 2.4f);
|
||||
}
|
||||
// "procedural://cube/0.5/140,92,72" → 取最后一个 '/' 之后的纯数字
|
||||
// 逗号段;段数 < 3 或含非数字(如 halfSize 段)判失败
|
||||
inline bool ParseProceduralColor(const std::string& path, glm::vec4& out) {
|
||||
if (path.rfind("procedural://", 0) != 0) return false;
|
||||
const size_t slash = path.find_last_of('/');
|
||||
if (slash == std::string::npos) return false;
|
||||
const std::string tail = path.substr(slash + 1);
|
||||
int c[3] = {0, 0, 0};
|
||||
int n = 0;
|
||||
size_t pos = 0;
|
||||
while (n < 3 && pos < tail.size()) {
|
||||
const size_t comma = tail.find(',', pos);
|
||||
const size_t end =
|
||||
(comma == std::string::npos) ? tail.size() : comma;
|
||||
if (end == pos) return false;
|
||||
int v = 0;
|
||||
for (size_t i = pos; i < end; ++i) {
|
||||
const char ch = tail[i];
|
||||
if (ch < '0' || ch > '9') return false;
|
||||
v = v * 10 + (ch - '0');
|
||||
}
|
||||
c[n++] = v;
|
||||
if (comma == std::string::npos) break;
|
||||
pos = comma + 1;
|
||||
}
|
||||
if (n < 3) return false;
|
||||
out = glm::vec4{SrgbByteToLinear(static_cast<u8>(c[0])),
|
||||
SrgbByteToLinear(static_cast<u8>(c[1])),
|
||||
SrgbByteToLinear(static_cast<u8>(c[2])), 1.0f};
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 参数(原 WallFracture 全量参数,字段类型随 Inspector 控件)----
|
||||
struct Params {
|
||||
i32 count = 30; // Voronoi 种子数 = 碎片数
|
||||
f32 seedRadius = 0.9f; // 加密半径 m
|
||||
f32 minSeedDist = 0.12f; // 种子最小间距 m
|
||||
f32 burstSpeed = 1.2f; // 近点径向初速 m/s
|
||||
f32 falloff = 1.0f; // 距离衰减率 1/m
|
||||
f32 normalPop = 0.4f; // 法线外推 m/s
|
||||
f32 upBias = 0.3f; // 上抛偏置 m/s
|
||||
f32 lifetime = 6.0f; // 碎片寿命(秒)
|
||||
i32 maxFragments = 128; // 活碎片上限
|
||||
f32 minImpactSpeed = 2.0f; // 触发阈值(m/s)
|
||||
};
|
||||
|
||||
// ---- 碎片外观 ----
|
||||
// 默认 inheritFromHost = true:碎块继承宿主(墙)的"生效材质"——
|
||||
// 底色/贴图/透明/自发光全套沿用,碎掉后与墙面目视一致。宿主材质
|
||||
// 取实体级覆盖(.material 资产)优先、模型内建材质兜底,与
|
||||
// SceneRenderer 收集期的 resolve 同序
|
||||
// 需要独立外观(玻璃靶 = 半透明 + 自发光等)时置 false,改用下面
|
||||
// 的显式字段(Fire 前设置)
|
||||
struct Appearance {
|
||||
bool inheritFromHost = true; // true = 材质沿用宿主,忽略下列字段
|
||||
glm::vec4 baseColor{140.0f / 255.0f, 92.0f / 255.0f,
|
||||
72.0f / 255.0f, 1.0f};
|
||||
bool transparent = false; // 透明绘制队列(玻璃碎片)
|
||||
glm::vec3 emissive{0.0f}; // 自发光颜色(黑 = 不发光)
|
||||
f32 emissiveStrength = 1.0f;
|
||||
};
|
||||
|
||||
// ---- 碎裂执行体(宿主脚本持有;一次 Fire 管一整批碎片)----
|
||||
class Burst {
|
||||
public:
|
||||
Params p; // 宿主 DeclareParams 直绑
|
||||
Appearance look; // 碎片外观(Fire 前设置)
|
||||
|
||||
// 触发流程一次做完:一次性保护 → 对方校验(活 Dynamic +
|
||||
// 速度阈值)→ 藏宿主 → 切分 → spawn 碎片群。未触发返回
|
||||
// false(速度不够/重复触发),宿主无需再写判定
|
||||
bool TryFire(const HostView& host,
|
||||
u32 other, const glm::vec3& point,
|
||||
const glm::vec3& normal) {
|
||||
if (m_Fired) return false;
|
||||
|
||||
// 对方须"够快"且是可动的撞击来源,二选一:
|
||||
// - 动态刚体(子弹/抛体):用实时线速度
|
||||
// - 角色(CharacterVirtual,非刚体、无 RigidBodyComponent):
|
||||
// 用"扫掠前"速度——扫掠中朝墙面的分量会被 Jolt 抵消,
|
||||
// 实时速度往往已归零,会漏判(见
|
||||
// PhysicsWorld::GetCharacterImpactVelocity)
|
||||
// 速度低于阈值(静止贴靠/轻碰)则忽略
|
||||
auto& registry = host.app.GetScene().GetRegistry();
|
||||
const ECS::Entity otherE = EntityFromBits(other);
|
||||
if (!registry.valid(otherE)) return false;
|
||||
f32 speed = 0.0f;
|
||||
if (const auto* otherRb =
|
||||
registry.try_get<Engine::ECS::RigidBodyComponent>(
|
||||
otherE)) {
|
||||
if (otherRb->type !=
|
||||
Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
|
||||
return false; // 静态/运动学体不算撞击
|
||||
}
|
||||
speed = glm::length(
|
||||
host.app.GetPhysics().GetLinearVelocity(other));
|
||||
} else if (registry.all_of<
|
||||
Engine::ECS::CharacterControllerComponent>(
|
||||
otherE)) {
|
||||
// 玩家/角色撞击:角色碰撞事件由 PhysicsWorld 合成
|
||||
// (CharacterVirtual 不进 ContactListener)
|
||||
speed = glm::length(
|
||||
host.app.GetPhysics().GetCharacterImpactVelocity(other));
|
||||
} else {
|
||||
return false; // 无物理来源(纯 Transform 实体)
|
||||
}
|
||||
if (speed < p.minImpactSpeed) return false;
|
||||
|
||||
m_Fired = true;
|
||||
Fire(host, point, normal);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 每帧簿记(宿主 OnUpdate 直调;未 Fire 过无簿记可理)
|
||||
void Tick(const HostView& host, f32 dt) {
|
||||
if (!m_Fired) return;
|
||||
ApplyPendingUnload(host);
|
||||
ApplyPendingVelocities(host);
|
||||
AgeAndCleanup(host, dt);
|
||||
}
|
||||
|
||||
// 宿主销毁(PIE stop 换血 / 手动删除):碎片实体由场景清亡
|
||||
// 处理,独占碎片模型统一批量回收(模型不被快照恢复的场景
|
||||
// 引用——唯一代键;批量接口内部 GPU 停飞后再销毁)
|
||||
void Shutdown(const HostView& host) {
|
||||
if (!m_Fired) return; // 从未 Fire:无碎片无模型
|
||||
std::vector<std::string> paths;
|
||||
paths.reserve(m_Fragments.size() + m_PendingUnload.size());
|
||||
for (const Fragment& f : m_Fragments) {
|
||||
if (!f.modelPath.empty()) paths.push_back(f.modelPath);
|
||||
}
|
||||
m_Fragments.clear();
|
||||
paths.insert(paths.end(),
|
||||
std::make_move_iterator(m_PendingUnload.begin()),
|
||||
std::make_move_iterator(m_PendingUnload.end()));
|
||||
m_PendingUnload.clear();
|
||||
if (!paths.empty()) {
|
||||
host.app.GetAssets().UnloadModelsByPaths(paths);
|
||||
}
|
||||
}
|
||||
|
||||
bool Fired() const { return m_Fired; }
|
||||
|
||||
private:
|
||||
// ---- 触发碎裂:藏宿主 + Voronoi 切分 + spawn 碎片群 ----
|
||||
void Fire(const HostView& host,
|
||||
const glm::vec3& hitPoint, const glm::vec3& hitNormal) {
|
||||
auto& registry = host.app.GetScene().GetRegistry();
|
||||
const ECS::Entity self = host.entity();
|
||||
|
||||
// 藏宿主:渲染/拾取跳过 + body 对账销毁(组件级 active,
|
||||
// 与 Unity Renderer.enabled + Rigidbody 摘除同语义)
|
||||
if (auto* mr =
|
||||
registry.try_get<Engine::ECS::MeshRendererComponent>(
|
||||
self)) {
|
||||
mr->active = false;
|
||||
}
|
||||
if (auto* rb =
|
||||
registry.try_get<Engine::ECS::RigidBodyComponent>(
|
||||
self)) {
|
||||
rb->active = false;
|
||||
}
|
||||
|
||||
// 宿主几何:碎片网格顶点按宿主世界尺寸烘焙(面 = XY 平面,
|
||||
// 厚度沿 Z;宿主须无旋转摆放,见头注释占位局限)
|
||||
const auto& wall = registry.get<Engine::ECS::TransformComponent>(
|
||||
self);
|
||||
const glm::vec3 wallPos = wall.GetWorldPosition();
|
||||
const glm::vec3 wallSize = wall.GetWorldScale();
|
||||
const f32 hw = 0.5f * wallSize.x;
|
||||
const f32 hh = 0.5f * wallSize.y;
|
||||
const f32 hz = 0.5f * wallSize.z;
|
||||
|
||||
// ---- 碎片外观:解析宿主(墙)的生效材质 ----
|
||||
// 与 SceneRenderer 收集期 resolve 同序:实体级覆盖
|
||||
//(.material 资产,按 mesh 下标查表)优先,模型内建材质兜底
|
||||
//(MeshMaterialInfo 为权威只读描述)。不继承则碎片只剩默认
|
||||
// 砖色,与墙面目视不符
|
||||
Engine::MaterialAssetData effMat;
|
||||
glm::vec4 effSolid{0.0f}; // 程序化纯色 albedo(a>0 = 有效)
|
||||
std::string hostOverridePath; // 宿主 .material 覆盖(空 = 内建材质)
|
||||
bool hasEffMat = false;
|
||||
if (look.inheritFromHost) {
|
||||
const auto* mr = registry.try_get<
|
||||
Engine::ECS::MeshRendererComponent>(self);
|
||||
const int slot =
|
||||
(mr && mr->meshIndex >= 0) ? mr->meshIndex : 0;
|
||||
if (mr) {
|
||||
const auto oIt =
|
||||
mr->materialOverrides.find(static_cast<u32>(slot));
|
||||
if (oIt != mr->materialOverrides.end()) {
|
||||
if (auto* asset =
|
||||
host.app.GetAssets().FindMaterialAsset(
|
||||
oIt->second)) {
|
||||
effMat = asset->GetData();
|
||||
hostOverridePath = oIt->second;
|
||||
hasEffMat = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasEffMat && mr && mr->model.Valid()) {
|
||||
if (auto* model =
|
||||
host.app.GetAssets().GetModel(mr->model)) {
|
||||
const auto& infos = model->GetMeshMaterialInfos();
|
||||
if (slot < static_cast<int>(infos.size())) {
|
||||
effMat.texPaths = infos[slot].texPaths;
|
||||
effMat.params = infos[slot].params;
|
||||
effMat.transparent = infos[slot].transparent;
|
||||
hasEffMat = true;
|
||||
}
|
||||
}
|
||||
// 程序化纯色墙:颜色在生成的 1x1 纹理里(快照无颜色
|
||||
// 字段),改由脚本从模型伪路径还原线性底色
|
||||
ParseProceduralColor(
|
||||
host.app.GetAssets().Models().Path(mr->model),
|
||||
effSolid);
|
||||
}
|
||||
}
|
||||
|
||||
// 命中点 → 宿主局部(逆旋转 + 平移),XY 即裂纹平面坐标
|
||||
const glm::quat invRot = glm::conjugate(
|
||||
wall.GetWorldRotationQuat());
|
||||
const glm::vec3 localHit = invRot * (hitPoint - wallPos);
|
||||
const glm::vec2 uvHit{localHit.x, localHit.y};
|
||||
|
||||
// ---- 种子:40% 命中点半径内加密 + 60% 全面均匀,最小
|
||||
// 间距拒绝采样(防碎片退化为细条)----
|
||||
const u32 count = std::clamp(p.count, 4, 96);
|
||||
const u32 nearCount = count * 2 / 5;
|
||||
std::vector<glm::vec2> seeds;
|
||||
seeds.reserve(count);
|
||||
const f32 minD2 = p.minSeedDist * p.minSeedDist;
|
||||
auto tryPush = [&](const glm::vec2& s) {
|
||||
if (s.x < -hw || s.x > hw || s.y < -hh || s.y > hh) return;
|
||||
for (const glm::vec2& e : seeds) {
|
||||
if (glm::dot(e - s, e - s) < minD2) return;
|
||||
}
|
||||
seeds.push_back(s);
|
||||
};
|
||||
// 命中点周边:均匀盘采样(sqrt(Rand) 面积均匀)
|
||||
for (u32 i = 0; i < nearCount * 12 && seeds.size() < nearCount;
|
||||
++i) {
|
||||
const f32 a = Rand(0.0f, glm::two_pi<f32>());
|
||||
const f32 r = p.seedRadius * std::sqrt(Rand(0.0f, 1.0f));
|
||||
tryPush(uvHit + r * glm::vec2{std::cos(a), std::sin(a)});
|
||||
}
|
||||
// 全面均匀补足(同款拒绝采样)
|
||||
for (u32 i = 0; i < count * 12 && seeds.size() < count; ++i) {
|
||||
tryPush({Rand(-hw, hw), Rand(-hh, hh)});
|
||||
}
|
||||
if (seeds.size() < 4) return; // 面太小采样失败:不碎
|
||||
|
||||
// ---- 每种子 → cell → 棱柱 mesh → ParsedModel ----
|
||||
// 唯一代键(见 GenSeq 注释):形状每次随机;模型回收走
|
||||
// Fragment.modelPath + 待卸载队列(ApplyPendingUnload)
|
||||
const std::string keyBase =
|
||||
"procedural://vorfrag/" +
|
||||
std::to_string(GenSeq()++) + '/';
|
||||
|
||||
auto& assets = host.app.GetAssets();
|
||||
std::vector<Engine::ModelRef> refs;
|
||||
std::vector<glm::vec3> centroids; // 碎片质心(世界系,初速径向用)
|
||||
std::vector<std::string> paths; // 模型缓存键(回收用,与 refs 对齐)
|
||||
refs.reserve(seeds.size());
|
||||
centroids.reserve(seeds.size());
|
||||
paths.reserve(seeds.size());
|
||||
for (size_t s = 0; s < seeds.size(); ++s) {
|
||||
const std::vector<glm::vec2> cell =
|
||||
VoronoiCell(seeds[s], seeds, hw, hh);
|
||||
if (cell.size() < 3) {
|
||||
refs.push_back({}); // 退化 cell:占位,spawn 跳过
|
||||
centroids.emplace_back();
|
||||
paths.emplace_back();
|
||||
continue;
|
||||
}
|
||||
PrismMesh pm = CellToPrism(cell, hz, hw, hh);
|
||||
glm::vec2 c2{0.0f};
|
||||
for (const glm::vec2& q : cell) c2 += q;
|
||||
c2 /= static_cast<f32>(cell.size());
|
||||
centroids.push_back(
|
||||
wallPos + wall.GetWorldRotationQuat() *
|
||||
glm::vec3{c2.x, c2.y, 0.0f});
|
||||
|
||||
Engine::ParsedModel parsed;
|
||||
parsed.path = keyBase + std::to_string(s);
|
||||
Engine::ParsedMesh& mesh =
|
||||
parsed.meshes.emplace_back();
|
||||
mesh.vertices = std::move(pm.vertices);
|
||||
mesh.lods.push_back(std::move(pm.indices));
|
||||
mesh.cpuPositions = std::move(pm.cpuPositions);
|
||||
mesh.name = "frag" + std::to_string(s);
|
||||
if (hasEffMat) {
|
||||
// 继承墙的生效材质:底色/贴图/UV 变换/透明全套沿用,
|
||||
// 碎块与墙面目视一致(UV 已在 CellToPrism 归一到墙面)
|
||||
mesh.params = effMat.params;
|
||||
mesh.texPaths = effMat.texPaths;
|
||||
mesh.transparent = effMat.transparent;
|
||||
// 程序化纯色墙(无贴图路径):用脚本解析出的线性底色当
|
||||
// baseColor——碎片 albedo 走默认白纹理,直乘即该色;
|
||||
// 不走这步会退回 MaterialParams 的白色默认值
|
||||
if (effSolid.a > 0.0f) {
|
||||
mesh.params.baseColor = effSolid;
|
||||
}
|
||||
} else {
|
||||
// 无宿主材质可继承(或 look.inheritFromHost=false):
|
||||
// 用显式外观(玻璃靶 = 半透明 + 自发光)
|
||||
mesh.params.baseColor = look.baseColor;
|
||||
mesh.transparent = look.transparent;
|
||||
mesh.params.emissiveFactor =
|
||||
glm::vec4{look.emissive, 1.0f};
|
||||
mesh.params.surface.z = look.emissiveStrength;
|
||||
}
|
||||
|
||||
const Engine::ModelRef ref =
|
||||
assets.LoadModel(parsed.path, std::move(parsed));
|
||||
refs.push_back(ref);
|
||||
paths.push_back(parsed.path);
|
||||
}
|
||||
|
||||
// ---- spawn 碎片实体:pos/rot 复用宿主位姿(mesh 已真
|
||||
// 尺寸,scale 恒 1 防 ConvexHull 顶点二次缩放),碎裂
|
||||
// 瞬间完美拼合原面,动起来后沿裂纹自然分离 ----
|
||||
for (size_t s = 0; s < refs.size(); ++s) {
|
||||
if (!refs[s].Valid()) continue;
|
||||
|
||||
auto frag = host.spawn("fragment");
|
||||
auto& tr = frag.GetComponent<
|
||||
Engine::ECS::TransformComponent>();
|
||||
tr.SetPosition(wallPos);
|
||||
tr.SetRotationQuat(wall.GetWorldRotationQuat());
|
||||
tr.SetScale(glm::vec3{1.0f});
|
||||
|
||||
auto& fmr =
|
||||
frag.AddComponent<
|
||||
Engine::ECS::MeshRendererComponent>();
|
||||
fmr.model = refs[s];
|
||||
// 宿主墙用 .material 覆盖时,碎块直接复用同一份资产:外观
|
||||
//(含透明通道)与墙完全一致,且不需要引擎改判定
|
||||
if (!hostOverridePath.empty()) {
|
||||
fmr.materialOverrides[0] = hostOverridePath;
|
||||
}
|
||||
|
||||
// Dynamic MeshCollider → ConvexHull(数据源 = 刚上传
|
||||
// 的 cpuPositions;Box 字段不参与 Mesh 形状)
|
||||
auto& frb =
|
||||
frag.AddComponent<
|
||||
Engine::ECS::RigidBodyComponent>();
|
||||
frb.type =
|
||||
Engine::ECS::RigidBodyComponent::BodyType::Dynamic;
|
||||
frb.collider =
|
||||
Engine::ECS::RigidBodyComponent::ColliderType::Mesh;
|
||||
frb.mass = 0.3f;
|
||||
frb.friction = 0.7f;
|
||||
frb.restitution = 0.05f;
|
||||
|
||||
// 碎裂初速(玻璃式,非爆炸):径向 = 碎片质心 - 命中
|
||||
// 点,离命中点越远越接近静止(指数衰减)——只有孔洞
|
||||
// 周边的碎片被冲击波推动;法线外推让碎片朝弹侧剥出
|
||||
glm::vec3 dir = centroids[s] - hitPoint;
|
||||
const f32 len = glm::length(dir);
|
||||
if (len < 1e-3f) {
|
||||
dir = glm::vec3{Rand(-1.0f, 1.0f), 0.6f,
|
||||
Rand(-1.0f, 1.0f)};
|
||||
} else {
|
||||
dir /= len;
|
||||
}
|
||||
const f32 falloff = std::exp(-len * p.falloff);
|
||||
const glm::vec3 vel =
|
||||
dir * p.burstSpeed * falloff *
|
||||
Rand(0.4f, 1.1f) +
|
||||
hitNormal * p.normalPop * Rand(0.3f, 0.8f) +
|
||||
glm::vec3{0.0f, p.upBias * Rand(0.0f, 0.5f),
|
||||
0.0f};
|
||||
|
||||
// 总量上限:超限立即回收最老碎片(防泄漏的兜底闸;
|
||||
// 寿命清理才是常态回收)。模型延迟一帧卸载
|
||||
const u32 bits = BitsOf(frag);
|
||||
if (m_Fragments.size() >=
|
||||
static_cast<size_t>(p.maxFragments)) {
|
||||
host.destroy(EntityFromBits(m_Fragments.front().bits));
|
||||
if (!m_Fragments.front().modelPath.empty()) {
|
||||
m_PendingUnload.push_back(
|
||||
std::move(m_Fragments.front().modelPath));
|
||||
}
|
||||
m_Fragments.erase(m_Fragments.begin());
|
||||
}
|
||||
m_Fragments.push_back(
|
||||
Fragment{bits, vel, 0.0f, false, paths[s]});
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 模型回收:延迟一帧 + 批量停飞卸载(Destroy 是帧尾延迟,
|
||||
// 碎片实体本帧渲染收集仍会引用模型;下一帧实体已灭,但上一帧
|
||||
// 提交的命令缓冲仍可能引用顶点缓冲/sampler——批量接口先
|
||||
// WaitDeviceIdle 再销毁,与资产重建换绑同一引擎先例)----
|
||||
void ApplyPendingUnload(const HostView& host) {
|
||||
if (m_PendingUnload.empty()) return;
|
||||
host.app.GetAssets().UnloadModelsByPaths(m_PendingUnload);
|
||||
m_PendingUnload.clear();
|
||||
}
|
||||
|
||||
// ---- 施速待办:body 建好(bodyValid)即注入,勿重复 ----
|
||||
void ApplyPendingVelocities(const HostView& host) {
|
||||
auto& registry = host.app.GetScene().GetRegistry();
|
||||
for (auto it = m_Fragments.begin();
|
||||
it != m_Fragments.end();) {
|
||||
if (it->applied) { ++it; continue; }
|
||||
const ECS::Entity e = EntityFromBits(it->bits);
|
||||
if (!registry.valid(e)) { // 实体没了(外部销毁)
|
||||
// 模型是独占资源(一代一键),实体没了模型也回收
|
||||
if (!it->modelPath.empty()) {
|
||||
m_PendingUnload.push_back(
|
||||
std::move(it->modelPath));
|
||||
}
|
||||
it = m_Fragments.erase(it);
|
||||
continue;
|
||||
}
|
||||
const auto* rb = registry.try_get<
|
||||
Engine::ECS::RigidBodyComponent>(e);
|
||||
if (rb && rb->bodyValid) {
|
||||
host.app.GetPhysics().SetLinearVelocity(
|
||||
it->bits, it->velocity);
|
||||
it->applied = true;
|
||||
}
|
||||
++it; // body 未建:留待下一帧(对账晚一帧必到)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 寿命清理:到期 Destroy(延迟队列,遍历安全)----
|
||||
void AgeAndCleanup(const HostView& host, f32 dt) {
|
||||
auto& registry = host.app.GetScene().GetRegistry();
|
||||
for (auto it = m_Fragments.begin();
|
||||
it != m_Fragments.end();) {
|
||||
it->age += dt;
|
||||
if (it->age < p.lifetime) { ++it; continue; }
|
||||
const ECS::Entity e = EntityFromBits(it->bits);
|
||||
if (registry.valid(e)) {
|
||||
host.destroy(e);
|
||||
}
|
||||
// 模型延迟一帧卸载(本帧渲染收集还引用它)
|
||||
if (!it->modelPath.empty()) {
|
||||
m_PendingUnload.push_back(std::move(it->modelPath));
|
||||
}
|
||||
it = m_Fragments.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
struct Fragment {
|
||||
u32 bits = 0; // PhysicsWorld 键(实体位模式)
|
||||
glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入)
|
||||
f32 age = 0.0f; // 存活秒数(寿命清理用)
|
||||
bool applied = false; // 初速已注入
|
||||
std::string modelPath; // 独占模型缓存键(回收时按路径卸载)
|
||||
};
|
||||
|
||||
bool m_Fired = false; // 一次性触发保护
|
||||
std::vector<Fragment> m_Fragments; // 活碎片簿记
|
||||
// 待卸载模型路径(碎片销毁的下一帧执行——Destroy 帧尾延迟,
|
||||
// 本帧渲染收集仍引用模型,同帧卸载会渲染悬空指针)
|
||||
std::vector<std::string> m_PendingUnload;
|
||||
};
|
||||
|
||||
} // namespace Engine::Fracture
|
||||
|
||||
#endif //HELLOVULKAN_FRACTURE_LIB_H
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// OrbitMove.cpp:参数管线验证样本——实体绕中心点圆周运动。
|
||||
//
|
||||
// 参数全链路:DeclareParams 声明(Speed/Radius/Center)→ Inspector
|
||||
// 自动出控件 → 随场景序列化 → 实例化/RefreshParams 灌值。PIE 中拖
|
||||
// Radius 当帧生效;Stop 由快照天然回滚。
|
||||
// 未声明成员(m_Angle)= 纯运行时状态,不进 Inspector 不落盘。
|
||||
//
|
||||
|
||||
#include <Engine/Scripting/Script.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
class OrbitMove final : public Engine::Scripting::ScriptBehavior {
|
||||
public:
|
||||
const char* Name() const override { return "OrbitMove"; }
|
||||
|
||||
// 参数声明(Inspector 控件 + 序列化 + 灌值共用 schema)
|
||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
||||
pb.Field("Speed", &m_Speed, 1.0f); // 角速度(弧度/秒)
|
||||
pb.Field("Radius", &m_Radius, 3.0f); // 轨道半径
|
||||
pb.Field("Center", &m_Center, glm::vec3{0.0f}); // 轨道中心
|
||||
}
|
||||
|
||||
void OnUpdate(float dt) override {
|
||||
m_Angle += m_Speed * dt;
|
||||
// 直接驱动 Transform(相对父空间的圆周;无父 = 世界)
|
||||
Transform().SetPosition(
|
||||
m_Center + glm::vec3(std::cos(m_Angle) * m_Radius, 0.0f,
|
||||
std::sin(m_Angle) * m_Radius));
|
||||
}
|
||||
|
||||
private:
|
||||
float m_Speed = 1.0f;
|
||||
float m_Radius = 3.0f;
|
||||
glm::vec3 m_Center{0.0f};
|
||||
float m_Angle = 0.0f; // 运行时相位(非参数)
|
||||
};
|
||||
|
||||
ENGINE_REGISTER_SCRIPT(OrbitMove);
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// PrefabTest.cpp:阶段2 验收脚本——脚本侧 Instantiate(prefab) 运行时
|
||||
// 实例化链路。
|
||||
//
|
||||
// PrefabTest(挂任意空实体,作为实例化锚点):
|
||||
// OnStart 按参数路径实例化一次 prefab;Interval > 0 时周期性再实例化
|
||||
// (多实例验收:每次 UUID 全量再生、物理体独立、沿 X 向排开下落)。
|
||||
//
|
||||
// 覆盖的引擎设施:
|
||||
// - ScriptBehavior::Instantiate(path, pos, rot)(Unity Object.
|
||||
// Instantiate 同位)→ SceneSerializer::InstantiatePrefab
|
||||
// - prefab 子树含 RigidBody/ScriptComponent 时由 PhysicsWorld/
|
||||
// ScriptRuntime 下一帧对账消化(与 Spawn 同语义)
|
||||
//
|
||||
// 验收步骤:
|
||||
// 1. 编辑器搭一个实体子树(如 Dynamic 盒刚体 + MeshRenderer 的箱子,
|
||||
// 可再挂个子实体验证层级一起走)→ Outliner 右键 Save As Prefab
|
||||
// (落盘 assets/prefabs/<名字>.prefab)
|
||||
// 2. 建空实体挂 PrefabTest,参数 PrefabPath 填该路径 → Play:
|
||||
// 锚点处出现实例;Interval 设 1.0 可看连续多实例物理下落互不串扰
|
||||
// 3. 编辑态也可从 Content Browser 把 .prefab 拖进 Outliner(节点=
|
||||
// 挂其下,空白区=挂根)或双击直接实例化——UUID 独立、资产引用有效
|
||||
//
|
||||
|
||||
#include <Engine/Scripting/Script.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
using namespace Engine; // f32 / ScriptBehavior 等免前缀(对齐 TestShoot)
|
||||
|
||||
namespace {
|
||||
|
||||
using Engine::Scripting::ParamBuilder;
|
||||
|
||||
class PrefabTest final : public Engine::Scripting::ScriptBehavior {
|
||||
public:
|
||||
const char* Name() const override { return "PrefabTest"; }
|
||||
|
||||
void DeclareParams(ParamBuilder& pb) override {
|
||||
// prefab 资产路径(工作目录相对,同场景/资产管理约定)
|
||||
pb.Field("PrefabPath", &m_PrefabPath,
|
||||
std::string{"assets/prefabs/crate.prefab"});
|
||||
// 周期实例化间隔(秒;<=0 = 仅 OnStart 实例化一次)
|
||||
pb.Field("Interval", &m_Interval, 0.0f);
|
||||
// 相邻实例的 X 向排开间距(多实例不叠在一起)
|
||||
pb.Field("SpreadX", &m_SpreadX, 2.0f);
|
||||
}
|
||||
|
||||
void OnStart() override {
|
||||
m_Timer = m_Interval;
|
||||
SpawnInstance();
|
||||
}
|
||||
|
||||
void OnUpdate(f32 dt) override {
|
||||
if (m_Interval <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
m_Timer -= dt;
|
||||
if (m_Timer <= 0.0f) {
|
||||
m_Timer = m_Interval;
|
||||
SpawnInstance();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void SpawnInstance() {
|
||||
// 锚点实体位姿 + 排开偏移;旋转 = 锚点自身旋转
|
||||
const auto& tr = Transform();
|
||||
const glm::vec3 pos =
|
||||
tr.GetPosition() + glm::vec3{m_SpreadX * f32(m_Count),
|
||||
0.0f, 0.0f};
|
||||
const glm::quat rot = tr.GetRotationQuat();
|
||||
if (Instantiate(m_PrefabPath, pos, rot) == entt::null) {
|
||||
return; // 路径无效/解析失败:日志已在序列化器里打过
|
||||
}
|
||||
++m_Count;
|
||||
}
|
||||
|
||||
std::string m_PrefabPath;
|
||||
f32 m_Interval = 0.0f;
|
||||
f32 m_SpreadX = 2.0f;
|
||||
f32 m_Timer = 0.0f;
|
||||
int m_Count = 0; // 已实例化份数(排开偏移基准)
|
||||
};
|
||||
|
||||
ENGINE_REGISTER_SCRIPT(PrefabTest)
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// ScriptDebug.h:脚本侧力学调试封装(Unity Gizmos/Debug.DrawRay 的
|
||||
// 游戏内同位物)。把调试惯例固化成默认值,脚本不用记参数序:
|
||||
// - width = 像素(≥1),默认 3px
|
||||
// - duration 恒 0(OnUpdate 每帧重画,事件类需求再裸调 Debug::)
|
||||
// - depthTest 恒 false(跟角色跑的线被模型遮挡就失去意义)
|
||||
// - headSize = 世界单位(箭头翼头空间尺寸),默认 0.08
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Engine/Debug/DebugDraw.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace ScriptDbg {
|
||||
|
||||
// 基础箭头:colorHex 语义色见下(重力红/支撑黄/意图蓝/真实绿)
|
||||
inline void Arrow(const glm::vec3& from, const glm::vec3& to,
|
||||
Engine::u32 colorHex, float width = 3.0f) {
|
||||
Engine::Debug::Arrow(from, to, Engine::Debug::Rgb(colorHex),
|
||||
/*headSize*/ 0.08f, /*width*/ width,
|
||||
/*duration*/ 0.0f, /*depthTest*/ false);
|
||||
}
|
||||
|
||||
// 重力:anchor=脚底,len=视觉长度。建议 ∝ gravityScale,且与支撑
|
||||
// 力同长——静置时红(下)黄(上)等长 = 力平衡,一眼可读
|
||||
inline void Gravity(const glm::vec3& anchor, float len) {
|
||||
Arrow(anchor, anchor - glm::vec3(0.0f, len, 0.0f), 0xDC3C3C);
|
||||
}
|
||||
|
||||
// 接触反力(法向支撑):只在有接触时存在——空中传 len=0 内部直接
|
||||
// 不画(力学语义:离地即失支撑)
|
||||
inline void Support(const glm::vec3& anchor, float len) {
|
||||
if (len > 1e-3f) {
|
||||
Arrow(anchor, anchor + glm::vec3(0.0f, len, 0.0f), 0xD9C33C);
|
||||
}
|
||||
}
|
||||
|
||||
// 加速度(深橙):脚本侧差分出的测量加速度(F=ma 的 a)。scale =
|
||||
// m/s² → 世界长度换算;mag 低于 minMag 视为静置/匀速("没有线"=
|
||||
// 没有加速度,本身就是信息),len 钳到 maxLen 防冲击尖峰爆屏
|
||||
inline void Accel(const glm::vec3& anchor, const glm::vec3& accel,
|
||||
float scale = 0.05f, float maxLen = 2.0f) {
|
||||
const float mag = glm::length(accel);
|
||||
if (mag > 0.1f) {
|
||||
const float len = glm::min(mag * scale, maxLen);
|
||||
Arrow(anchor, anchor + (accel / mag) * len, 0xE0640A);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ScriptDbg
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// ScriptLog.h:脚本日志门面(ScriptDbg 的文字版同位物)。
|
||||
//
|
||||
// 脚本打日志统一走这里——直接转发引擎 spdlog 默认 logger(终端
|
||||
// + HelloVulkan.log + 编辑器 Log Panel 三处同收)。约定:消息开头
|
||||
// 写 [脚本名] 前缀(面板过滤好定位):
|
||||
// ScriptLog::Info("[WASDMove] jump");
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#define SLOG_TRACE(...) ::spdlog::trace(__VA_ARGS__)
|
||||
#define SLOG_DEBUG(...) ::spdlog::debug(__VA_ARGS__)
|
||||
#define SLOG_INFO(...) ::spdlog::info(__VA_ARGS__)
|
||||
#define SLOG_WARN(...) ::spdlog::warn(__VA_ARGS__)
|
||||
#define SLOG_ERROR(...) ::spdlog::error(__VA_ARGS__)
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// Spinner.cpp:C++ 原生脚本示范——Y 轴匀速自转。
|
||||
//
|
||||
// 写脚本四步:
|
||||
// 1. 新建 .cpp 放本目录(CMake 自动进构建)
|
||||
// 2. 继承 ScriptBehavior,覆写 Name/OnCreate/OnUpdate/OnDestroy
|
||||
// 3. DeclareParams 声明可调参数(Inspector 自动出控件 + 随场景
|
||||
// 序列化 + 实例化灌值)
|
||||
// 4. 文件级 ENGINE_REGISTER_SCRIPT(类名) 自注册——Inspector 下拉
|
||||
// 与场景序列化按该名解析
|
||||
//
|
||||
|
||||
#include <Engine/Scripting/Script.h>
|
||||
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
class Spinner final : public Engine::Scripting::ScriptBehavior {
|
||||
public:
|
||||
const char* Name() const override { return "Spinner"; }
|
||||
|
||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
||||
pb.Field("Speed", &m_Speed, 90.0f); // 度/秒
|
||||
}
|
||||
|
||||
void OnCreate() override {
|
||||
// 实体与组件已就位;需要别的组件时用 Get<T>()(不存在会炸)
|
||||
}
|
||||
|
||||
void OnUpdate(float dt) override {
|
||||
// 四元数复合绕 Y(避免每帧欧拉↔四元数往返累积失真)
|
||||
|
||||
const glm::quat step = glm::angleAxis(
|
||||
glm::radians(m_Speed * dt), glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
Transform().SetRotationQuat(step * Transform().GetRotationQuat());
|
||||
}
|
||||
|
||||
private:
|
||||
float m_Speed = 90.0f; // 度/秒(声明默认;实例化期被存档值覆盖)
|
||||
};
|
||||
|
||||
ENGINE_REGISTER_SCRIPT(Spinner);
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// TestShoot.cpp:阶段1 验收脚本——脚本侧 spawn/destroy 底座 + 碰撞事件
|
||||
// 转发的端到端链路。
|
||||
//
|
||||
// 三个脚本协作(放一个文件方便对照):
|
||||
// TestShoot(挂任意实体,如相机实体):
|
||||
// 鼠标左键上升沿 → 从编辑器相机沿视线 spawn 子弹实体
|
||||
// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入)
|
||||
// Bullet(子弹实体,由 TestShoot 运行时装配):
|
||||
// OnCreate 施加枪口初速(body 已由当帧 Step 对账建好);
|
||||
// OnCollisionEnter → 命中点 spawn ImpactFx + 自毁;
|
||||
// 超时自毁防飞出地图泄漏;存活期画调试十字 = 弹道可视化
|
||||
// ImpactFx(特效实体,由 Bullet 运行时装配,空实体 + 调试点标):
|
||||
// 扩张十字 + 白→红渐隐,寿命尽自毁——占位特效
|
||||
//
|
||||
// 覆盖的引擎设施:
|
||||
// - Spawn()/Destroy():ScriptBehavior 实体底座(Scene::CreateEntity/
|
||||
// DestroyEntity 延迟队列)
|
||||
// - 组件运行时装配:RigidBody(PhysicsWorld 下一帧对账建 body)、
|
||||
// ScriptComponent(ScriptRuntime 下一帧对账建实例)
|
||||
// - OnCollisionEnter/Exit:PhysicsWorld::GetEvents → ScriptRuntime
|
||||
// 帧首派发
|
||||
//
|
||||
// 验收步骤:编辑器给任意实体挂 TestShoot → Play → RMB 飞行视角瞄准
|
||||
// 墙体 → 左键射击:弹道点飞出 → 撞墙瞬间原地绽放十字特效、子弹消失。
|
||||
//
|
||||
|
||||
#include <Engine/Scripting/Script.h>
|
||||
#include <Engine/Core/Application.h>
|
||||
#include <Engine/Core/Input.h>
|
||||
#include <Engine/Physics/PhysicsWorld.h>
|
||||
#include <Engine/Debug/DebugDraw.h>
|
||||
#include <Engine/Renderer/Renderer.h>
|
||||
#include <Engine/Renderer/EditorCamera.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
using namespace Engine; // Input / Debug 门面
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- 子弹脚本(TestShoot 运行时挂载;不手动挂场景)----
|
||||
class Bullet final : public Engine::Scripting::ScriptBehavior {
|
||||
public:
|
||||
const char* Name() const override { return "Bullet"; }
|
||||
|
||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
||||
// 枪口初速(世界系 m/s):TestShoot spawn 时按视线方向写进
|
||||
// 脚本项参数——数据经参数层传递,与序列化同一条路
|
||||
pb.Field("Velocity", &m_Velocity, glm::vec3{0.0f});
|
||||
pb.Field("Lifetime", &m_Lifetime, 5.0f); // 超时自毁(秒)
|
||||
}
|
||||
|
||||
void OnCreate() override {
|
||||
// body 已由本帧 Step 的 SyncScene 建好(spawn 发生在上帧
|
||||
// OnUpdate):这里施加初速,物理从下步起按弹道积分
|
||||
if (glm::length(m_Velocity) > 1e-4f) {
|
||||
App().GetPhysics().SetLinearVelocity(EntityBits(), m_Velocity);
|
||||
}
|
||||
}
|
||||
|
||||
void OnUpdate(f32 dt) override {
|
||||
m_Age += dt;
|
||||
if (m_Age >= m_Lifetime) {
|
||||
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
|
||||
return;
|
||||
}
|
||||
// 弹道可视化:命中点即弹着点(橙十字 = 弹头,速度线 = 尾迹)
|
||||
const glm::vec3 pos = Transform().GetPosition();
|
||||
const glm::vec3 vel =
|
||||
App().GetPhysics().GetLinearVelocity(EntityBits());
|
||||
Debug::Cross(pos, 0.09f, Debug::Rgb(0xFF8C1A),
|
||||
/*width*/ 2.0f, /*duration*/ 0.0f,
|
||||
/*depthTest*/ false);
|
||||
if (glm::length(vel) > 0.5f) {
|
||||
Debug::Arrow(pos, pos + vel * 0.03f, Debug::Rgb(0xFFB366),
|
||||
/*headSize*/ 0.04f, /*width*/ 2.0f,
|
||||
/*duration*/ 0.0f, /*depthTest*/ false);
|
||||
}
|
||||
}
|
||||
|
||||
void OnCollisionEnter(u32 other, const glm::vec3& point,
|
||||
const glm::vec3& normal) override {
|
||||
// 防重入:Destroy 是帧尾延迟执行,命中帧内子弹与碎片群
|
||||
// 可产生多对接触事件(碎片云一块挨一块),无标志会一帧
|
||||
// 爆出 N 个特效(同 WallFracture m_Fired 的一次性触发模式)
|
||||
if (m_Hit) return;
|
||||
m_Hit = true;
|
||||
|
||||
(void)other; (void)normal; // 法线留给将来定向溅射
|
||||
|
||||
// 命中特效:空实体锚在命中点,ImpactFx 脚本项由下一帧对账
|
||||
// 实例化(Spawn 即时建实体,脚本实例化天然晚一帧)
|
||||
auto fx = Spawn("impact");
|
||||
fx.GetComponent<Engine::ECS::TransformComponent>()
|
||||
.SetPosition(point);
|
||||
auto& sc = fx.AddComponent<Engine::ECS::ScriptComponent>();
|
||||
Engine::ECS::ScriptComponent::Item item;
|
||||
item.name = "ImpactFx";
|
||||
sc.items.push_back(std::move(item));
|
||||
|
||||
// 自毁:延迟队列,帧尾统一执行(事件派发循环安全)
|
||||
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
|
||||
}
|
||||
|
||||
private:
|
||||
glm::vec3 m_Velocity{0.0f};
|
||||
f32 m_Lifetime = 5.0f;
|
||||
f32 m_Age = 0.0f;
|
||||
bool m_Hit = false; // 首次命中置位(防同帧多次接触重复特效)
|
||||
};
|
||||
|
||||
// ---- 命中特效脚本(Bullet 运行时挂载;空实体 + 调试点标占位)----
|
||||
class ImpactFx final : public Engine::Scripting::ScriptBehavior {
|
||||
public:
|
||||
const char* Name() const override { return "ImpactFx"; }
|
||||
|
||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
||||
pb.Field("Duration", &m_Duration, 0.4f); // 特效寿命(秒)
|
||||
}
|
||||
|
||||
void OnUpdate(f32 dt) override {
|
||||
m_Age += dt;
|
||||
if (m_Age >= m_Duration) {
|
||||
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
|
||||
return;
|
||||
}
|
||||
// 扩张十字 + 白→红渐隐(占位:将来换 VFX/粒子系统)
|
||||
const f32 t = m_Age / m_Duration;
|
||||
const glm::vec3 pos = Transform().GetPosition();
|
||||
Debug::Cross(pos, 0.12f + t * 0.55f,
|
||||
glm::vec4{1.0f, 1.0f - t, 1.0f - t, 1.0f},
|
||||
/*width*/ 3.0f, /*duration*/ 0.0f,
|
||||
/*depthTest*/ false);
|
||||
}
|
||||
|
||||
private:
|
||||
f32 m_Duration = 0.4f;
|
||||
f32 m_Age = 0.0f;
|
||||
};
|
||||
|
||||
// ---- 枪口脚本(验收主体:挂到任意实体,位置/朝向取编辑器相机)----
|
||||
class TestShoot final : public Engine::Scripting::ScriptBehavior {
|
||||
public:
|
||||
const char* Name() const override { return "TestShoot"; }
|
||||
|
||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
||||
pb.Field("BulletSpeed", &m_BulletSpeed, 25.0f); // 枪口初速 m/s
|
||||
pb.Field("MuzzleOffset", &m_MuzzleOffset, 0.6f); // 出膛距离 m
|
||||
pb.Field("FireCooldown", &m_FireCooldown, 0.15f); // 最小射击间隔 s
|
||||
pb.Field("BulletRadius", &m_BulletRadius, 0.12f); // 弹头碰撞半径 m
|
||||
// 半径 0.12 的反隧穿账:物理离散检测(无 CCD),检测带 =
|
||||
// 半径 + 障碍半厚;薄板(如 0.2m 墙)窗口 2*(0.12+0.1)=0.44m
|
||||
// > 每步进位移 0.42m(25 m/s @ 1/60),垂直命中保证检出。
|
||||
// 半径越小高速弹越易整步跨过薄碰撞体(无接触=无事件)
|
||||
}
|
||||
|
||||
void OnUpdate(f32 dt) override {
|
||||
m_Cooldown -= dt;
|
||||
|
||||
// 左键上升沿触发(电平查询 + 上帧状态对比,Unity
|
||||
// GetMouseButtonDown 边沿语义的手工等价物)
|
||||
const bool down =
|
||||
Input::IsMouseButtonDown(Input::MouseButton::Left);
|
||||
const bool fire = down && !m_FireWasDown &&
|
||||
m_Cooldown <= 0.0f;
|
||||
m_FireWasDown = down;
|
||||
if (fire) {
|
||||
SpawnBullet();
|
||||
m_Cooldown = m_FireCooldown;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void SpawnBullet() {
|
||||
// 枪口 = 编辑器相机视线前方(编辑形态 RMB 飞行瞄准;
|
||||
// 与 WASDMove 的朝向来源一致)
|
||||
const auto& cam =
|
||||
App().GetRenderer().GetEditorCamera().GetTransform();
|
||||
const glm::vec3 fwd =
|
||||
cam.GetRotationQuat() * glm::vec3(0.0f, 0.0f, -1.0f);
|
||||
const glm::vec3 muzzle =
|
||||
cam.GetWorldPosition() + fwd * m_MuzzleOffset;
|
||||
|
||||
// ---- 子弹实体装配(spawn 底座 + 组件运行时装配演示)----
|
||||
auto bullet = Spawn("bullet");
|
||||
auto& tr = bullet.GetComponent<Engine::ECS::TransformComponent>();
|
||||
tr.SetPosition(muzzle);
|
||||
tr.SetRotationQuat(cam.GetRotationQuat());
|
||||
|
||||
// Dynamic 球刚体:参数给默认档(轻质量/低弹性,命中即停)
|
||||
auto& rb = bullet.AddComponent<Engine::ECS::RigidBodyComponent>();
|
||||
rb.type = Engine::ECS::RigidBodyComponent::BodyType::Dynamic;
|
||||
rb.collider =
|
||||
Engine::ECS::RigidBodyComponent::ColliderType::Sphere;
|
||||
rb.sphereRadius = m_BulletRadius;
|
||||
rb.mass = 0.2f;
|
||||
rb.friction = 0.3f;
|
||||
rb.restitution = 0.1f;
|
||||
|
||||
// Bullet 脚本项:初速/寿命经参数层传入(与序列化同一条路,
|
||||
// 下一帧 ScriptRuntime 对账实例化)
|
||||
auto& sc = bullet.AddComponent<Engine::ECS::ScriptComponent>();
|
||||
Engine::ECS::ScriptComponent::Item item;
|
||||
item.name = "Bullet";
|
||||
item.params.push_back(
|
||||
{"Velocity",
|
||||
Engine::Scripting::ParamValue{fwd * m_BulletSpeed}});
|
||||
item.params.push_back(
|
||||
{"Lifetime", Engine::Scripting::ParamValue{5.0f}});
|
||||
sc.items.push_back(std::move(item));
|
||||
}
|
||||
|
||||
f32 m_BulletSpeed = 25.0f;
|
||||
f32 m_MuzzleOffset = 0.6f;
|
||||
f32 m_FireCooldown = 0.15f;
|
||||
f32 m_BulletRadius = 0.08f;
|
||||
f32 m_Cooldown = 0.0f;
|
||||
bool m_FireWasDown = false; // 左键上升沿检测基准
|
||||
};
|
||||
|
||||
ENGINE_REGISTER_SCRIPT(Bullet)
|
||||
ENGINE_REGISTER_SCRIPT(ImpactFx)
|
||||
ENGINE_REGISTER_SCRIPT(TestShoot)
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,233 @@
|
||||
//
|
||||
// ThirdPersonCamera.cpp:第三人称环绕相机(UE SpringArm 同位)。
|
||||
//
|
||||
// 挂 primary 相机实体。鼠标增量 → yaw/pitch,相机位置 = 目标实体
|
||||
// (按名解析,默认 "player")+ 球面偏移,朝向锁定目标。WASDMove 的
|
||||
// 移动参考 = 本相机朝向——WASD 即"相机水平朝向的前后左右"(UE
|
||||
// gameplay 语义)。枢轴水平硬跟、竖直带阻尼(FollowResponse):XZ
|
||||
// 与角色零滞后(跑动不错位),Y 滤掉脚底每物理步的微弹跳(踩碎块/
|
||||
// 撞墙)避免画面抖动。
|
||||
//
|
||||
// OnStart 从编辑器摆的机位反推 yaw/pitch/dist:编辑器摆哪,游戏就
|
||||
// 从哪个角度开场。Esc 释放光标(调参),左键重捕继续玩。
|
||||
//
|
||||
// 防穿墙:枢轴→期望机位做静态几何射线(UE SpringArm 同位),命中则拉近;
|
||||
// 拉近瞬时、拉远平滑,贴墙最小距离可配。
|
||||
//
|
||||
|
||||
#include <Engine/Scripting/Script.h>
|
||||
#include <Engine/Core/Application.h>
|
||||
#include <Engine/Core/Input.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/Physics/PhysicsRay.h> // RayHit(防穿墙射线结果)
|
||||
#include <Engine/Physics/PhysicsWorld.h> // App().GetPhysics().RayCastStatic 门面
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
using namespace Engine;
|
||||
|
||||
namespace {
|
||||
|
||||
// 拉远回弹响应(1/s,指数逼近):离开墙角后镜头平滑归位。
|
||||
// 拉近是瞬时的(见 OnUpdate)——遮挡晚一帧就会穿墙,不能平滑
|
||||
constexpr float kCameraReturnResponse = 8.0f;
|
||||
|
||||
class ThirdPersonCamera final : public Engine::Scripting::ScriptBehavior {
|
||||
public:
|
||||
const char* Name() const override { return "ThirdPersonCamera"; }
|
||||
|
||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
||||
pb.Field("Sensitivity", &m_Sensitivity, 1.0f);
|
||||
pb.Field("Distance", &m_Distance, 0.0f); // 0 = 吸编辑器机位
|
||||
pb.Field("HeightOffset", &m_HeightOffset, 1.2f);
|
||||
pb.Field("PitchMinDeg", &m_PitchMin, -35.0f);
|
||||
pb.Field("PitchMaxDeg", &m_PitchMax, 75.0f);
|
||||
pb.Field("GamepadTurnSpeed", &m_PadTurnSpeed, 180.0f);
|
||||
// 跟随目标按名锁定(默认 "player"):场景有多个角色控制器
|
||||
// 实体(如跟随 NPC)时,按名解析保证相机锁主角而非随机命中
|
||||
pb.Field("TargetName", &m_TargetName, std::string("player"));
|
||||
// 竖直跟随阻尼(1/s,指数逼近;0 = 硬跟=旧行为)。水平 XZ
|
||||
// 恒硬跟(保证跑动时相机/角色不错位、无拖影——全轴阻尼会
|
||||
// 让角色在高速移动时被"甩"出画面并抖动);仅 Y 阻尼吸收
|
||||
// 角色脚底每物理步的微弹跳(踩碎块/撞墙),帧率无关
|
||||
pb.Field("FollowResponse", &m_FollowResponse, 12.0f);
|
||||
// 目标 Y 瞬移超过该距离(PIE 传送/回合复位)→ 直接吸附
|
||||
pb.Field("SnapDistance", &m_SnapDistance, 3.0f);
|
||||
// 相机防穿墙(UE SpringArm 同位):枢轴→期望机位之间若有静态
|
||||
// 几何,把相机拉到命中点前。关掉 = 旧行为(可穿墙)
|
||||
pb.Field("CameraCollision", &m_Collision, true);
|
||||
// 贴墙时相机离枢轴的最小距离(再近会切进角色/近平面)
|
||||
pb.Field("MinDistance", &m_MinDistance, 0.35f);
|
||||
// 命中点回退余量(防止相机面贴墙 → z-fighting/切面)
|
||||
pb.Field("CollisionSkin", &m_CollisionSkin, 0.25f);
|
||||
}
|
||||
|
||||
void OnStart() override {
|
||||
auto& reg = App().GetScene().GetRegistry();
|
||||
|
||||
// ---- 找目标:优先按名(TargetName)命中主角 ----
|
||||
auto byName = reg.view<Engine::ECS::NameComponent>();
|
||||
for (auto e : byName) {
|
||||
if (byName.get<Engine::ECS::NameComponent>(e).name ==
|
||||
m_TargetName) {
|
||||
m_Target = e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 名称未命中(未命名/改名)→ 回退第一个角色控制器实体
|
||||
if (m_Target == entt::null) {
|
||||
auto view = reg.view<Engine::ECS::CharacterControllerComponent>();
|
||||
if (view.empty()) {
|
||||
ENGINE_WARN(
|
||||
"[ThirdPersonCamera] 场景无 CharacterController 实体,相机静止");
|
||||
return;
|
||||
}
|
||||
m_Target = *view.begin();
|
||||
}
|
||||
|
||||
// ---- 从编辑器机位反推 yaw/pitch/dist(摆哪开场在哪)----
|
||||
const auto& camT = Transform();
|
||||
const auto& tgtT =
|
||||
App().GetScene().GetRegistry().get<
|
||||
Engine::ECS::TransformComponent>(m_Target);
|
||||
const glm::vec3 offset =
|
||||
camT.GetPosition() -
|
||||
(tgtT.GetPosition() +
|
||||
glm::vec3(0.0f, m_HeightOffset, 0.0f));
|
||||
const float dist = glm::length(offset);
|
||||
if (dist > 1e-4f && m_Distance <= 0.0f) {
|
||||
m_Distance = dist;
|
||||
m_Pitch =
|
||||
glm::degrees(std::asin(std::clamp(offset.y / dist,
|
||||
-1.0f, 1.0f)));
|
||||
m_Yaw = glm::degrees(std::atan2(offset.x, offset.z));
|
||||
}
|
||||
ENGINE_INFO(
|
||||
"[ThirdPersonCamera] 目标已锁定,dist={:.2f} yaw={:.1f} pitch={:.1f}",
|
||||
m_Distance, m_Yaw, m_Pitch);
|
||||
|
||||
Input::SetCursorCaptured(true);
|
||||
}
|
||||
|
||||
void OnUpdate(float dt) override {
|
||||
if (m_Target == entt::null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 光标管理:Esc 释放(Inspector 调参),左键重捕。
|
||||
// 手柄玩家不碰鼠标——光标保持捕获态不影响摇杆输入
|
||||
if (Input::IsKeyDown(Input::Key::Escape)) {
|
||||
Input::SetCursorCaptured(false);
|
||||
}
|
||||
if (Input::IsMouseButtonDown(Input::MouseButton::Left)) {
|
||||
Input::SetCursorCaptured(true);
|
||||
}
|
||||
|
||||
// ---- 鼠标增量 + 右摇杆 → 球面角 ----
|
||||
// 鼠标 = 相对增量(GetMouseDelta);右摇杆 = 持续量
|
||||
//(Look 动作,满推 m_PadTurnSpeed 度/秒),两者叠加
|
||||
float dx = 0.0f, dy = 0.0f;
|
||||
Input::GetMouseDelta(dx, dy);
|
||||
const glm::vec2 padLook = Input::ActionValue2("Look");
|
||||
const float k = 0.12f * m_Sensitivity;
|
||||
m_Yaw -= dx * k + padLook.x * m_PadTurnSpeed * dt;
|
||||
m_Pitch += dy * k + padLook.y * m_PadTurnSpeed * dt;
|
||||
m_Pitch = std::clamp(m_Pitch, m_PitchMin, m_PitchMax);
|
||||
m_Yaw = std::fmod(m_Yaw, 360.0f);
|
||||
|
||||
// ---- 位置 = 目标枢轴 + 球面偏移 ----
|
||||
// 水平 XZ 硬跟(与角色零滞后——跑动时相机/角色不错位、无
|
||||
// 拖影),仅 Y 经阻尼逼近(FollowResponse):吸收目标脚底
|
||||
// 每物理步的微弹跳(踩碎块/撞墙),相机竖直方向不再 1:1
|
||||
// 抖动。首帧、瞬移(超 SnapDistance)或阻尼为 0 时直接吸附
|
||||
// = 旧硬跟语义
|
||||
const auto& tgtT =
|
||||
App().GetScene().GetRegistry().get<
|
||||
Engine::ECS::TransformComponent>(m_Target);
|
||||
const glm::vec3 rawPivot =
|
||||
tgtT.GetPosition() +
|
||||
glm::vec3(0.0f, m_HeightOffset, 0.0f);
|
||||
if (!m_FollowInit || m_FollowResponse <= 0.0f ||
|
||||
std::abs(rawPivot.y - m_FollowPivotY) > m_SnapDistance) {
|
||||
m_FollowPivotY = rawPivot.y;
|
||||
m_FollowInit = true;
|
||||
} else {
|
||||
m_FollowPivotY +=
|
||||
(rawPivot.y - m_FollowPivotY) *
|
||||
(1.0f - std::exp(-m_FollowResponse * dt));
|
||||
}
|
||||
glm::vec3 pivot = rawPivot; // XZ 硬跟
|
||||
pivot.y = m_FollowPivotY; // Y 阻尼
|
||||
const float yawR = glm::radians(m_Yaw);
|
||||
const float pitchR = glm::radians(m_Pitch);
|
||||
const glm::vec3 dir( // 枢轴 → 相机(yaw 绕 Y,pitch 抬升)
|
||||
std::sin(yawR) * std::cos(pitchR),
|
||||
std::sin(pitchR),
|
||||
std::cos(yawR) * std::cos(pitchR));
|
||||
// ---- 相机防穿墙:枢轴 → 期望机位射线拉近 ----
|
||||
// 只打静态几何(碎块/动态物不当遮挡,避免镜头被飘过的碎片
|
||||
// 乱拉);枢轴在角色胶囊内,角色是 CharacterVirtual 不进射线,
|
||||
// 不会自己挡自己。拉近瞬时(遮挡晚一帧就穿墙),拉远平滑
|
||||
float wantDist = m_Distance;
|
||||
if (m_Collision) {
|
||||
RayHit hit;
|
||||
if (App().GetPhysics().RayCastStatic(
|
||||
pivot, dir, m_Distance + m_CollisionSkin, hit)) {
|
||||
wantDist = std::max(hit.distance - m_CollisionSkin,
|
||||
m_MinDistance);
|
||||
}
|
||||
}
|
||||
if (!m_DistInit) { // 首帧吸附(无回弹动画)
|
||||
m_CurDist = wantDist;
|
||||
m_DistInit = true;
|
||||
} else if (wantDist < m_CurDist) {
|
||||
m_CurDist = wantDist; // 拉近:瞬时
|
||||
} else {
|
||||
m_CurDist += (wantDist - m_CurDist) *
|
||||
(1.0f - std::exp(-kCameraReturnResponse * dt));
|
||||
}
|
||||
|
||||
auto& camT = Transform();
|
||||
camT.SetPosition(pivot + dir * m_CurDist);
|
||||
|
||||
// ---- 朝向锁定目标 ----
|
||||
// 引擎 YXZ 序(R = Ry·Rx)视向 = (-cosP sinY, sinP, -cosP cosY)
|
||||
// 反解:pitch = asin(vd.y),yaw = atan2(-vd.x, -vd.z)
|
||||
const glm::vec3 vd = glm::normalize(pivot - camT.GetPosition());
|
||||
const float p = glm::degrees(std::asin(std::clamp(vd.y, -1.0f, 1.0f)));
|
||||
const float y = glm::degrees(std::atan2(-vd.x, -vd.z));
|
||||
camT.SetRotation(glm::vec3(p, y, 0.0f));
|
||||
}
|
||||
|
||||
private:
|
||||
float m_Sensitivity = 1.0f;
|
||||
float m_Distance = 0.0f; // 0 = OnStart 吸编辑器机位
|
||||
float m_HeightOffset = 1.2f; // 枢轴高度(瞄胸口)
|
||||
float m_PitchMin = -35.0f;
|
||||
float m_PitchMax = 75.0f;
|
||||
float m_PadTurnSpeed = 180.0f; // 右摇杆满推角速度(度/秒)
|
||||
std::string m_TargetName = "player"; // 目标实体名(NameComponent)
|
||||
float m_FollowResponse = 12.0f; // 竖直跟随阻尼 1/s(0 = 硬跟)
|
||||
float m_SnapDistance = 3.0f; // Y 瞬移吸附阈值 m
|
||||
float m_FollowPivotY = 0.0f; // 阻尼后的枢轴高度(XZ 硬跟)
|
||||
bool m_FollowInit = false; // 首帧吸附基准
|
||||
bool m_Collision = true; // 相机防穿墙开关
|
||||
float m_MinDistance = 0.35f; // 贴墙最小距离
|
||||
float m_CollisionSkin = 0.25f; // 命中点回退余量
|
||||
float m_CurDist = 0.0f; // 防穿墙后的当前距离(拉近平滑态)
|
||||
bool m_DistInit = false; // 距离首帧吸附基准
|
||||
float m_Yaw = 0.0f;
|
||||
float m_Pitch = 20.0f;
|
||||
entt::entity m_Target = entt::null;
|
||||
};
|
||||
|
||||
ENGINE_REGISTER_SCRIPT(ThirdPersonCamera)
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,279 @@
|
||||
//
|
||||
// WASDMove.cpp:第三人称移动(WASD/手柄摇杆,相机朝向参考,UE gameplay 语义)。
|
||||
//
|
||||
// 输入走引擎具名动作层(Move/Jump/Run 默认绑定):WASD + 方向键 +
|
||||
// 手柄左摇杆移动(引擎径向死区),Space/手柄 A 跳;跑 = Run 动作
|
||||
//(Shift / 手柄 L3 按下左摇杆)。摇杆模拟量直接驱动动量整形(轻推慢走)。
|
||||
//
|
||||
// 移动语义分派(按实体配置自动选择,优先级从上到下):
|
||||
// - CharacterControllerComponent → 角色控制器路径(扫掠运动学:
|
||||
// 水平喂意图速度 / 重力自动积分 / Space 跳跃,Unity
|
||||
// CharacterController.Move 同构)
|
||||
// - Dynamic 刚体 → 走物理速度门面(每帧喂水平速度,Y 归物理——
|
||||
// 重力/跳跃不受影响;松键喂零速 = 立即停,Unity velocity 语义)
|
||||
// - 都没有 → 直接平移 Transform
|
||||
//
|
||||
// 编辑器形态键位天然不冲突:RMB 按住 = 相机吃 WASD 飞行(脚本也读
|
||||
// 到按键 → 同时在动,测移动时松开 RMB 即可);游戏形态 WASD 常开。
|
||||
//
|
||||
|
||||
#include <Engine/Scripting/Script.h>
|
||||
#include <Engine/Core/Application.h>
|
||||
#include <Engine/Physics/PhysicsWorld.h>
|
||||
#include <Engine/Core/Input.h>
|
||||
#include <Engine/Debug/DebugDraw.h>
|
||||
#include "ScriptDebug.h"
|
||||
#include "ScriptLog.h"
|
||||
#include <Engine/Renderer/Renderer.h>
|
||||
#include <Engine/Renderer/EditorCamera.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using namespace Engine; // Input 门面(统一输入入口)
|
||||
|
||||
namespace {
|
||||
|
||||
class WASDMove final : public Engine::Scripting::ScriptBehavior {
|
||||
public:
|
||||
const char* Name() const override { return "WASDMove"; }
|
||||
|
||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
||||
pb.Field("Speed", &m_Speed, 3.0f); // 走路 m/s
|
||||
pb.Field("RunSpeed", &m_RunSpeed, 6.0f); // 跑步 m/s
|
||||
pb.Field("TurnResponse", &m_TurnResponse, 12.0f); // 转向阻尼
|
||||
pb.Field("AccelResponse", &m_AccelResponse, 8.0f); // 加速/转向响应
|
||||
pb.Field("StopResponse", &m_StopResponse, 14.0f); // 松手制动响应
|
||||
pb.Field("JumpSpeed", &m_JumpSpeed, 6.0f); // 起跳初速 m/s
|
||||
// clip 按名引用(Unity anim.Play("Walk") 语义)——名字在
|
||||
// Inspector 里改,attach 顺序变了脚本零改动
|
||||
pb.Field("IdleClip", &m_IdleClip, std::string("idle"));
|
||||
pb.Field("WalkClip", &m_WalkClip, std::string("walk"));
|
||||
pb.Field("RunClip", &m_RunClip, std::string("run"));
|
||||
}
|
||||
|
||||
void OnStart() override {
|
||||
SLOG_TRACE("hello");
|
||||
SLOG_INFO("hello");
|
||||
SLOG_DEBUG("hello");
|
||||
SLOG_WARN("hello");
|
||||
SLOG_ERROR("hello");
|
||||
SLOG_INFO("[WASDMove] 就绪:走 {:.1f} m/s / 跑 {:.1f} m/s / 跳 {:.1f} m/s",
|
||||
m_Speed, m_RunSpeed, m_JumpSpeed);
|
||||
}
|
||||
|
||||
void OnUpdate(float dt) override {
|
||||
// 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene
|
||||
// view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)。
|
||||
// 输入 = 具名动作层(Unity6 Input System / UE5 Enhanced Input
|
||||
// 同位):Move 默认混绑 WASD/方向键/左摇杆(引擎做径向死区 +
|
||||
// 单位圆钳制,斜向不超速),Jump 混绑 Space/手柄 A——键盘
|
||||
// 手柄一套代码,将来改键/加绑定不进脚本
|
||||
// 移动参考 = 场景 primary 相机(第三人称相机脚本每帧写它
|
||||
// 的朝向——玩家看到的视角就是移动参考);无相机实体时
|
||||
//(纯独立形态)回退编辑器相机
|
||||
glm::quat cq;
|
||||
bool refCam = false;
|
||||
auto camView = App().GetScene()
|
||||
.GetRegistry()
|
||||
.view<Engine::ECS::CameraComponent>();
|
||||
for (auto e : camView) {
|
||||
const auto& cam =
|
||||
camView.get<Engine::ECS::CameraComponent>(e);
|
||||
if (cam.primary && cam.active) {
|
||||
cq = App().GetScene()
|
||||
.GetRegistry()
|
||||
.get<Engine::ECS::TransformComponent>(e)
|
||||
.GetRotationQuat();
|
||||
refCam = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!refCam) {
|
||||
cq = App().GetRenderer()
|
||||
.GetEditorCamera()
|
||||
.GetTransform()
|
||||
.GetRotationQuat();
|
||||
}
|
||||
const glm::vec3 fwd = cq * glm::vec3(0.0f, 0.0f, -1.0f);
|
||||
const glm::vec3 right = cq * glm::vec3(1.0f, 0.0f, 0.0f);
|
||||
|
||||
// 约定:X+ = 右(D),Y- = 前(W);方向合成 = fwd*(-Y) + right*(+X)
|
||||
const glm::vec2 mvRaw = Input::ActionValue2("Move");
|
||||
glm::vec2 mv = mvRaw;
|
||||
if (glm::length(mv) > 1.0f) mv = glm::normalize(mv);
|
||||
|
||||
glm::vec3 move = fwd * (-mv.y) + right * mv.x;
|
||||
move.y = 0.0f;
|
||||
// 只钳超速,不放大——摇杆模拟量(幅度 <1)保留进速度:
|
||||
// 轻推慢走、推满全速(归一化会把幅度吃掉,一动就满速)
|
||||
if (glm::length(move) > 1.0f) {
|
||||
move = glm::normalize(move);
|
||||
}
|
||||
// 走/跑:Run 动作(默认绑 Shift + 手柄 L3,按下左摇杆冲刺
|
||||
// 的 Xbox 惯例)。不判"摇杆推满"——键盘 WASD 幅度恒 1.0,
|
||||
// 那样判会把走路误判成跑
|
||||
const bool running = Input::ActionDown("Run");
|
||||
const float speed = running ? m_RunSpeed : m_Speed;
|
||||
const glm::vec3 desired = move * speed; // 期望速度(输入意图)
|
||||
|
||||
auto& phy = App().GetPhysics();
|
||||
const bool hasCC =
|
||||
TryGetComponent<Engine::ECS::CharacterControllerComponent>() !=
|
||||
nullptr;
|
||||
const bool grounded =
|
||||
hasCC ? phy.IsCharacterGrounded(EntityBits()) : true;
|
||||
|
||||
// ---- 动量整形(惯性的本体,纯脚本):真实速度逐帧逼近期
|
||||
// 望速度——起步渐快、转向沿弧线漂移、折返经零减速再反向、
|
||||
// 松手滑行刹车。加速/转向用 AccelResponse,松手用更硬的
|
||||
// StopResponse(角色要"想停就停");空中操纵性降到 1/4
|
||||
//(起跳后不宜急转弯)。指数逼近,帧率无关
|
||||
const bool moving = glm::length(mvRaw) > 0.1f;
|
||||
float response = moving ? m_AccelResponse : m_StopResponse;
|
||||
if (!grounded) {
|
||||
response *= 0.25f;
|
||||
}
|
||||
m_PlanarVel +=
|
||||
(desired - m_PlanarVel) * (1.0f - std::exp(-response * dt));
|
||||
if (glm::length(desired - m_PlanarVel) < 0.02f) {
|
||||
m_PlanarVel = desired; // 贴齐,防低速蠕动
|
||||
}
|
||||
|
||||
// ---- 路径 1:角色控制器(挂了 CharacterControllerComponent)----
|
||||
// 水平喂动量整形后的真实速度;Y 保留现值——重力由控制器每
|
||||
// 步自动积分,每帧覆写 Y 会把下落速度清零(空中变悬浮)
|
||||
if (hasCC) {
|
||||
const glm::vec3 cur = phy.GetCharacterVelocity(EntityBits());
|
||||
phy.SetCharacterVelocity(
|
||||
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
|
||||
|
||||
// 跳跃:动作层边沿(Pressed = prev 0 → cur 非 0,键盘
|
||||
// Space / 手柄 A 都产生一次上升沿)。IsGrounded 防空中连跳
|
||||
if (Input::ActionPressed("Jump") &&
|
||||
phy.IsCharacterGrounded(EntityBits())) {
|
||||
phy.Jump(EntityBits(), m_JumpSpeed);
|
||||
SLOG_INFO("[WASDMove] jump(着地起跳 {:.1f} m/s)",
|
||||
m_JumpSpeed);
|
||||
}
|
||||
} else if (auto* rb =
|
||||
TryGetComponent<Engine::ECS::RigidBodyComponent>();
|
||||
rb != nullptr && rb->bodyValid &&
|
||||
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
|
||||
// 物理驱动:只覆写水平速度,Y 保留物理积分离散结果
|
||||
//(整体喂 0 会抵消重力 → 空中物体悬浮)
|
||||
const glm::vec3 cur = phy.GetLinearVelocity(EntityBits());
|
||||
phy.SetLinearVelocity(
|
||||
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
|
||||
} else {
|
||||
Transform().SetPosition(Transform().GetPosition() +
|
||||
m_PlanarVel * dt);
|
||||
}
|
||||
|
||||
// ---- 朝向:面向"真实速度"方向(不是输入方向,模型默认 +Z)----
|
||||
// 这是消"平移感"的关键:动量整形后速度滞后于输入,若面朝
|
||||
// 输入方向,身体朝前脚却在侧滑——正是割裂感的来源。面朝
|
||||
// 真实速度 = 脚底朝向与位移一致,转向呈现弧线漂移。速度
|
||||
// 低于阈值(起步瞬间/折返过零)锁定上帧朝向:折返表现成
|
||||
// U 弯回转而不是原地瞬转。只取水平面(角色不俯仰翻滚)
|
||||
if (glm::length(m_PlanarVel) > 0.3f) {
|
||||
const float targetYaw =
|
||||
std::atan2(m_PlanarVel.x, m_PlanarVel.z);
|
||||
const glm::vec3 modelFwd =
|
||||
Transform().GetRotationQuat() * glm::vec3(0.0f, 0.0f, 1.0f);
|
||||
const float curYaw = std::atan2(modelFwd.x, modelFwd.z);
|
||||
const float dyaw = std::atan2(std::sin(targetYaw - curYaw),
|
||||
std::cos(targetYaw - curYaw));
|
||||
const float k = 1.0f - std::exp(-m_TurnResponse * dt);
|
||||
Transform().SetRotationQuat(
|
||||
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
|
||||
}
|
||||
|
||||
// ---- 动画:按真实速度分档(迟滞防临界抖)----
|
||||
// 判据 = 动量整形后的真实水平速度,不是输入原始值:起步
|
||||
// 滑行期已是 walk,松手刹车期保持 walk 直到真停——动画与
|
||||
// 位移观感一致,按键瞬通瞬断不再瞬间换挡。run 双阈值迟滞
|
||||
//(进 0.9×RunSpeed / 退 0.7×RunSpeed):摇杆在阈值附近晃
|
||||
// 不抖档。引擎 Play(名) 自带相位对齐 + crossfade,切档
|
||||
// 突兀感只剩判据本身的问题
|
||||
const float planarSpeed = glm::length(m_PlanarVel);
|
||||
std::string target;
|
||||
if (planarSpeed < 0.15f) {
|
||||
target = m_IdleClip;
|
||||
} else if (m_CurClip == m_RunClip) {
|
||||
target = (planarSpeed >= m_RunSpeed * 0.7f) ? m_RunClip
|
||||
: m_WalkClip;
|
||||
} else {
|
||||
target = (planarSpeed >= m_RunSpeed * 0.9f) ? m_RunClip
|
||||
: m_WalkClip;
|
||||
}
|
||||
if (target != m_CurClip) {
|
||||
if (auto* anim = TryGetComponent<Engine::ECS::AnimationComponent>();
|
||||
anim != nullptr && anim->Play(target, 0.35f)) {
|
||||
// fade 0.35s:locomotion 混合的经验值(Unity 默认
|
||||
// 0.25 太短,0.5+ 显拖沓);相位同步在引擎侧做
|
||||
m_CurClip = target; // 播放成功才记账(模型未就绪下帧重试)
|
||||
}
|
||||
}
|
||||
|
||||
const glm::vec3 anchor =
|
||||
(hasCC ? phy.GetCharacterPosition(EntityBits())
|
||||
: Transform().GetPosition()) +
|
||||
glm::vec3(0.0f, 0.05f, 0.0f);
|
||||
|
||||
// ---- 运动线(ScriptDbg 封装:像素线宽 3px / 当帧瞬时 /
|
||||
// 透视直显已固化为默认值,不用记参数序):蓝 = 输入期
|
||||
// 望速度,绿 = 动量整形后的真实速度——同一起点,夹角
|
||||
// 即惯性滞量(转向弧线/U 弯/刹车可视化)
|
||||
ScriptDbg::Arrow(anchor, anchor + desired * 0.2f, 0x2B5FD0);
|
||||
ScriptDbg::Arrow(anchor, anchor + m_PlanarVel * 0.2f, 0x1E9E42);
|
||||
|
||||
// ---- 力学线:重力(红,下)+ 接触反力(黄,上,仅着地)。
|
||||
// 长度 ∝ gravityScale:静置时红黄等长 = 力平衡;起跳
|
||||
// 离地黄箭头消失 = 离地即失支撑;gravityScale 调大两
|
||||
// 箭同步变长(重量感可视化)
|
||||
float gs = 1.0f;
|
||||
if (auto* cc = TryGetComponent<
|
||||
Engine::ECS::CharacterControllerComponent>()) {
|
||||
gs = cc->gravityScale;
|
||||
}
|
||||
const float gLen = 0.5f * gs;
|
||||
ScriptDbg::Gravity(anchor, gLen);
|
||||
ScriptDbg::Support(anchor, grounded ? gLen : 0.0f);
|
||||
|
||||
const glm::vec3 vel = hasCC
|
||||
? phy.GetCharacterVelocity(EntityBits())
|
||||
: m_PlanarVel;
|
||||
if (dt > 1e-5f) {
|
||||
m_AccelS = glm::mix(m_AccelS, (vel - m_PrevVel) / dt, 0.35f);
|
||||
}
|
||||
m_PrevVel = vel;
|
||||
ScriptDbg::Accel(anchor, m_AccelS);
|
||||
|
||||
}
|
||||
|
||||
void DebugShow() {
|
||||
|
||||
}
|
||||
|
||||
private:
|
||||
float m_Speed = 3.0f; // 走路 m/s(声明默认;存档值覆盖)
|
||||
float m_RunSpeed = 6.0f; // 跑步 m/s(Shift 按住)
|
||||
float m_TurnResponse = 12.0f; // 转向阻尼强度(越大越跟手)
|
||||
float m_AccelResponse = 8.0f; // 加速/转向响应(动量整形,越小越"重")
|
||||
float m_StopResponse = 14.0f; // 松手制动响应(越大刹车越硬)
|
||||
glm::vec3 m_PlanarVel{0.0f}; // 平滑后的水平真实速度(动量状态)
|
||||
float m_JumpSpeed = 6.0f; // 起跳初速(配合组件 gravityScale 调手感)
|
||||
std::string m_IdleClip = "idle"; // clip 名(Inspector 可改)
|
||||
std::string m_WalkClip = "walk";
|
||||
std::string m_RunClip = "run";
|
||||
std::string m_CurClip = "idle"; // 当前目标 clip(边沿检测基准)
|
||||
glm::vec3 m_PrevVel{0.0f}; // 上帧测量速度(加速度差分基准)
|
||||
glm::vec3 m_AccelS{0.0f}; // 平滑测量加速度(EMA 0.35,调试图)
|
||||
};
|
||||
|
||||
ENGINE_REGISTER_SCRIPT(WASDMove)
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// WallFracture.cpp:可碎墙脚本——碎裂逻辑已提取至 FractureLib.h
|
||||
//(打靶场 Target 复用同一条路径),本文件只剩宿主职责:
|
||||
// DeclareParams 直绑 Burst::p 各字段(参数名/默认值与历史档位
|
||||
// 逐一同名同值,旧场景不受影响)
|
||||
// OnCollisionEnter → Burst::TryFire(一次性保护 + 对方撞击来源
|
||||
// 与速度校验 + 藏墙 + Voronoi 切分 + spawn 碎片群,库内完成)。
|
||||
// 撞击来源 = 动态刚体(子弹/抛体)或角色(玩家撞墙)——角色是
|
||||
// CharacterVirtual,其碰撞事件由 PhysicsWorld 从角色接触合成
|
||||
// (Jolt 的 ContactListener 只回调刚体,不覆盖角色)
|
||||
// OnUpdate → Burst::Tick(施速待办 / 寿命清理 / 模型回收)
|
||||
// OnDestroy → Burst::Shutdown(独占碎片模型批量停飞卸载)
|
||||
//
|
||||
// 外观默认继承墙的生效材质(底色/贴图/透明),碎块与墙面观感一致;
|
||||
// 要独立外观(玻璃/发光碎片)置 Burst::look.inheritFromHost = false
|
||||
// 再用 baseColor/emissive 显式指定。
|
||||
//
|
||||
// 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube +
|
||||
// RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里
|
||||
// 任意实体挂 TestShoot → Play → RMB 飞行瞄准墙体 → 左键射击:
|
||||
// 墙沿裂纹碎成不规则碎片,孔洞周边碎块小、飞散明显,边缘大块
|
||||
// 塌落堆积在墙脚,若干秒后清空。
|
||||
//
|
||||
|
||||
#include <Engine/Scripting/Script.h>
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include "FractureLib.h"
|
||||
|
||||
using namespace Engine; // u32/f32/i32 基础类型(历史脚本同款)
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- 可碎墙(挂墙实体;触发后由 Burst 管一整批碎片)----
|
||||
class WallFracture final : public Engine::Scripting::ScriptBehavior {
|
||||
public:
|
||||
const char* Name() const override { return "WallFracture"; }
|
||||
|
||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
||||
// Voronoi 种子数 = 碎片数(40% 在命中点半径内加密 → 孔洞
|
||||
// 周边碎块小、边缘大块的玻璃碎分布;最小间距防碎片退化)
|
||||
// 直绑 Burst::p:碎裂逻辑只读 Burst::p,绑到别处(曾经的
|
||||
// 独立 m_P)会让计数器/寿命等全部失效
|
||||
pb.Field("Count", &m_Burst.p.count, 30);
|
||||
pb.Field("SeedRadius", &m_Burst.p.seedRadius, 0.9f); // 加密半径 m
|
||||
pb.Field("MinSeedDist", &m_Burst.p.minSeedDist, 0.12f); // 种子最小间距 m
|
||||
// 玻璃/陶瓷式碎裂(裂纹分离 + 重力塌落),非爆炸飞散:
|
||||
// 初速 = 命中点径向 × 距离指数衰减(近动远不动)+ 法线
|
||||
// 轻微外推(剥落)+ 微弱上抛;重力是塌落的主导
|
||||
pb.Field("BurstSpeed", &m_Burst.p.burstSpeed, 1.2f); // 近点径向初速 m/s
|
||||
pb.Field("Falloff", &m_Burst.p.falloff, 1.0f); // 距离衰减率 1/m
|
||||
pb.Field("NormalPop", &m_Burst.p.normalPop, 0.4f); // 法线外推 m/s
|
||||
pb.Field("UpBias", &m_Burst.p.upBias, 0.3f); // 上抛偏置 m/s
|
||||
pb.Field("Lifetime", &m_Burst.p.lifetime, 6.0f); // 碎片寿命(秒)
|
||||
pb.Field("MaxFragments", &m_Burst.p.maxFragments, 128); // 活碎片上限
|
||||
pb.Field("MinImpactSpeed", &m_Burst.p.minImpactSpeed, 2.0f); // 触发阈值
|
||||
}
|
||||
|
||||
void OnCollisionEnter(u32 other, const glm::vec3& point,
|
||||
const glm::vec3& normal) override {
|
||||
// 触发条件在库内:对方须是够快的撞击来源——动态刚体
|
||||
//(子弹/抛体)或角色(玩家撞墙);速度阈值过滤静止贴靠
|
||||
m_Burst.TryFire(HV(), other, point, normal);
|
||||
}
|
||||
|
||||
void OnUpdate(f32 dt) override { m_Burst.Tick(HV(), dt); }
|
||||
|
||||
// 墙销毁(PIE stop 换血 / 手动删除):碎片实体由场景清亡处理,
|
||||
// 独占碎片模型统一批量回收
|
||||
void OnDestroy() override { m_Burst.Shutdown(HV()); }
|
||||
|
||||
private:
|
||||
// 库桥接:本成员函数内可访问 protected 基类设施(App/Spawn/
|
||||
// Destroy),库内只走公共 API——见 FractureLib.h HostView 注
|
||||
Engine::Fracture::HostView HV() {
|
||||
return {App(), EntityBits(),
|
||||
[this](const std::string& n) { return Spawn(n); },
|
||||
[this](Engine::ECS::Entity e) { Destroy(e); }};
|
||||
}
|
||||
|
||||
// 碎片簿记 + 生命周期;参数即 m_Burst.p(DeclareParams 直绑其字段)
|
||||
Engine::Fracture::Burst m_Burst;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ENGINE_REGISTER_SCRIPT(WallFracture)
|
||||
+44
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(HelloVulkan CXX)
|
||||
project(HelloVulkan C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -12,6 +12,31 @@ add_subdirectory(Lib/glfw)
|
||||
add_subdirectory(Lib/glm)
|
||||
add_subdirectory(Lib/spdlog)
|
||||
add_subdirectory(Lib/stb)
|
||||
# meshoptimizer(submodule,LOD 网格简化;demo/gltfpack 默认关闭,install 不需要)
|
||||
set(MESHOPT_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(Lib/meshoptimizer)
|
||||
|
||||
# ---- Jolt Physics(submodule v5.6,刚体/软体/角色/载具模拟)----
|
||||
if(MSVC)
|
||||
set(USE_STATIC_MSVC_RUNTIME_LIBRARY OFF CACHE BOOL "" FORCE)
|
||||
endif ()
|
||||
# 顶层 CMake 在 Build/ 子目录;只编 jolt 静态库,示例/测试目标默认全关
|
||||
set(JPH_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE) # LTO 拖慢开发迭代
|
||||
set(OVERRIDE_CXX_FLAGS OFF CACHE BOOL "" FORCE) # 不动全局优化参数
|
||||
set(FLOATING_POINT_EXCEPTIONS_ENABLED OFF CACHE BOOL "" FORCE) # NaN 进物理不炸,仅报警
|
||||
# JPH_USE_CPU_COMPUTE 默认 ON,需要 DXC 把 Shaders/*.hlsl 编成 .spv(本机无
|
||||
# 工具链);我们不使用 Jolt 的 GPU compute 路径,连同后端 shader 一并关掉
|
||||
set(JPH_USE_CPU_COMPUTE OFF CACHE BOOL "" FORCE)
|
||||
set(JPH_USE_DX12 OFF CACHE BOOL "" FORCE)
|
||||
set(JPH_USE_VK OFF CACHE BOOL "" FORCE)
|
||||
set(JPH_USE_MTL OFF CACHE BOOL "" FORCE)
|
||||
# Jolt GENERATE_DEBUG_SYMBOLS 默认 ON,Release 也带 -g → Jolt 的 DWARF
|
||||
# Debug/RelWithDebInfo 保留符号便于调试
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
set(GENERATE_DEBUG_SYMBOLS OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
add_subdirectory(Lib/JoltPhysics/Build)
|
||||
|
||||
# ---- Assimp(vendored submodule,模型导入;只编译需要的 importer) ----
|
||||
set(BUILD_SHARED_LIBS OFF) # 静态链接
|
||||
@@ -39,11 +64,29 @@ set(ASSIMP_BUILD_STL_IMPORTER ON CACHE BOOL "" FORCE)
|
||||
set(ASSIMP_BUILD_PLY_IMPORTER ON CACHE BOOL "" FORCE)
|
||||
add_subdirectory(Lib/assimp)
|
||||
|
||||
# ---- yaml-cpp(submodule,场景序列化;只编静态库本体)----
|
||||
set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "" FORCE)
|
||||
set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
|
||||
set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(YAML_CPP_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(Lib/yaml-cpp)
|
||||
|
||||
|
||||
|
||||
# Vulkan
|
||||
find_package(Vulkan REQUIRED)
|
||||
|
||||
# ---- editor/game 编译期分离(UE WITH_EDITOR / Unity UNITY_EDITOR 模式)----
|
||||
# ON(默认):HELLO_EDITOR=1,材质球预览 tile 池 + App/editor 编辑器 UI 全量编译
|
||||
# OFF:game 构建——预览 pass/atlas/请求池、previewEnvSets、App/editor 源码
|
||||
# 整体不进编译产物(宏裁剪,非运行时判断)
|
||||
option(HELLOVULKAN_EDITOR "Build editor modules (material preview, inspector UI)" ON)
|
||||
|
||||
|
||||
if(MINGW)
|
||||
set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -Wl,-Bdynamic -lucrt")
|
||||
endif()
|
||||
|
||||
# Engine 和 App
|
||||
add_subdirectory(Engine)
|
||||
add_subdirectory(App)
|
||||
|
||||
+79
-11
@@ -1,36 +1,104 @@
|
||||
file(GLOB_RECURSE ENGINE_SOURCES
|
||||
src/*.cpp
|
||||
)
|
||||
# ============ 第三方库目标(Engine 的依赖) ============
|
||||
|
||||
# ImGui(docking 分支,vendored submodule)
|
||||
set(IMGUI_DIR ${CMAKE_SOURCE_DIR}/Lib/imgui)
|
||||
set(IMGUI_SOURCES
|
||||
add_library(imgui STATIC
|
||||
${IMGUI_DIR}/imgui.cpp
|
||||
${IMGUI_DIR}/imgui_draw.cpp
|
||||
${IMGUI_DIR}/imgui_tables.cpp
|
||||
${IMGUI_DIR}/imgui_widgets.cpp
|
||||
# ${IMGUI_DIR}/imgui_demo.cpp # ShowDemoWindow 的实现(调试期保留)
|
||||
${IMGUI_DIR}/backends/imgui_impl_glfw.cpp
|
||||
${IMGUI_DIR}/backends/imgui_impl_vulkan.cpp
|
||||
)
|
||||
target_include_directories(imgui PUBLIC
|
||||
${IMGUI_DIR}
|
||||
${IMGUI_DIR}/backends)
|
||||
# 两个 backend 头文件分别依赖 GLFW / Vulkan 头
|
||||
target_link_libraries(imgui PUBLIC glfw Vulkan::Vulkan)
|
||||
|
||||
# 编译成静态库
|
||||
add_library(Engine STATIC ${ENGINE_SOURCES} ${IMGUI_SOURCES})
|
||||
# ImGuizmo(视口操纵杆,vendored submodule;新版仓库源码在 src/,
|
||||
# 只编译 ImGuizmo.cpp,GraphEditor/ImCurveEdit 等示例模块不用)
|
||||
# IMGUI_DEFINE_MATH_OPERATORS 必须 PUBLIC:ImGuizmo.cpp 与任何
|
||||
# include 其头文件的 TU 都要在 imgui.h 之前看到该宏(ImVec2 运算符)
|
||||
set(IMGUIZMO_DIR ${CMAKE_SOURCE_DIR}/Lib/ImGuizmo/src)
|
||||
add_library(ImGuizmo STATIC ${IMGUIZMO_DIR}/ImGuizmo.cpp)
|
||||
target_include_directories(ImGuizmo PUBLIC ${IMGUIZMO_DIR})
|
||||
target_compile_definitions(ImGuizmo PUBLIC IMGUI_DEFINE_MATH_OPERATORS)
|
||||
target_link_libraries(ImGuizmo PUBLIC imgui)
|
||||
|
||||
# ufbx(FBX 全版本导入,vendored 单文件 C 库):assimp 的 FBX importer
|
||||
# 只认 FBX 2011+,Mixamo 及大量网上资产是 FBX 6.1 binary(二进制布局
|
||||
# 与 7.x 在对象 ID 层不兼容,assimp DOM 直接拒读)。ufbx 官方支持
|
||||
# FBX 5.x/6.x/7.x 全版本,引擎的 .fbx 解析统一走它(glTF/OBJ 仍 assimp)
|
||||
add_library(ufbx STATIC ${CMAKE_SOURCE_DIR}/Lib/ufbx/ufbx.c)
|
||||
target_include_directories(ufbx PUBLIC ${CMAKE_SOURCE_DIR}/Lib/ufbx)
|
||||
|
||||
# Tracy(CPU/GPU 帧分析)
|
||||
add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp)
|
||||
target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public)
|
||||
target_compile_definitions(tracy PUBLIC
|
||||
TRACY_ENABLE
|
||||
# 按需剖析:GPU context 宣告(GpuNewContext)与连接绑定——GUI 永远晚于
|
||||
# 程序启动连接,没有这个宏宣告消息被丢弃,GPU 时间线永远空白
|
||||
TRACY_ON_DEMAND)
|
||||
find_package(Threads REQUIRED)
|
||||
if(WIN32)
|
||||
target_link_libraries(tracy PUBLIC
|
||||
Threads::Threads
|
||||
ws2_32
|
||||
dbghelp
|
||||
secur32)
|
||||
else()
|
||||
target_link_libraries(tracy PUBLIC Threads::Threads)
|
||||
endif()
|
||||
|
||||
|
||||
# ============ Engine ============
|
||||
|
||||
file(GLOB_RECURSE ENGINE_SOURCES CONFIGURE_DEPENDS src/*.cpp)
|
||||
|
||||
# game 构建(HELLOVULKAN_EDITOR=OFF):editor 专属 pass 整 TU 不进产物
|
||||
#(材质球预览 / Editor gizmo——对应头文件只含声明,无符号产出)
|
||||
if(NOT HELLOVULKAN_EDITOR)
|
||||
list(REMOVE_ITEM ENGINE_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Renderer/Passes/MaterialPreviewPass.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Renderer/Passes/EditorGizmoPass.cpp)
|
||||
endif()
|
||||
|
||||
add_library(Engine STATIC ${ENGINE_SOURCES})
|
||||
|
||||
# PhysicsDebugDraw 派生 Jolt::DebugRendererSimple:Jolt 以 -fno-rtti 编译
|
||||
#(有 key function 的基类 typeinfo 符号不存在),该 TU 必须同步关闭 RTTI,
|
||||
# 否则派生类 typeinfo 引用悬空符号(链接期 undefined reference)
|
||||
set_source_files_properties(src/Physics/PhysicsDebugDraw.cpp PROPERTIES
|
||||
COMPILE_OPTIONS "-fno-rtti")
|
||||
|
||||
target_include_directories(Engine PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
# header-only 库:纯 include 路径
|
||||
${CMAKE_SOURCE_DIR}/Lib/entt/src
|
||||
${IMGUI_DIR}
|
||||
${IMGUI_DIR}/backends
|
||||
)
|
||||
${CMAKE_SOURCE_DIR}/Lib/VulkanMemoryAllocator/include)
|
||||
|
||||
# 链接第三方库
|
||||
target_link_libraries(Engine PUBLIC
|
||||
glfw
|
||||
glm::glm
|
||||
Vulkan::Vulkan
|
||||
spdlog::spdlog
|
||||
stb
|
||||
assimp::assimp
|
||||
)
|
||||
ufbx
|
||||
meshoptimizer
|
||||
Jolt::Jolt
|
||||
yaml-cpp::yaml-cpp
|
||||
imgui
|
||||
ImGuizmo
|
||||
tracy)
|
||||
|
||||
target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
|
||||
|
||||
# editor/game 编译期分离:HELLO_EDITOR=1 时预览子系统 + 编辑器消费端编译
|
||||
#(PUBLIC——App 层 main.cpp 的编辑器装配同样按此宏裁剪)
|
||||
if(HELLOVULKAN_EDITOR)
|
||||
target_compile_definitions(Engine PUBLIC HELLO_EDITOR=1)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// AnimationAttach:外部动画资产 → 目标模型并入(编辑器 attach 语义,
|
||||
// 对应 UE "给 SkeletalMesh 指派 AnimSequence" 的跨资产绑定)。
|
||||
//
|
||||
// 资产语义:纯动画 FBX(只有骨骼动画无网格)= 引擎的 AnimSequence
|
||||
// 等价物。轨道映射两级:① 骨骼名同名直映(同源拆分资产零损耗);
|
||||
// ② 名字不同源时走 HumanoidRig 槽位重定向(Unity Avatar 语义,
|
||||
// 对每个映射骨骼烘焙 rest→anim 的旋转增量到目标骨架局部空间)。
|
||||
// 两级都落不上的轨道跳过并计数。
|
||||
//
|
||||
// 并入只改运行时内存状态:持久化走场景里 Animation.attachedAnimPaths
|
||||
//(重启后按列表幂等重放 attach),模型资产本身永不被回写。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ANIMATIONATTACH_H
|
||||
#define HELLOVULKAN_ANIMATIONATTACH_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Model;
|
||||
struct ParsedModel;
|
||||
|
||||
namespace Animation {
|
||||
|
||||
// 重定向明细行(调试:哪个源骨骼经哪个槽位映射到哪个目标骨骼)。
|
||||
// slot 为静态字符串(HumanoidSlotName 表),名字拷贝
|
||||
struct RetargetRow {
|
||||
const char* slot = nullptr;
|
||||
std::string srcName;
|
||||
std::string dstName;
|
||||
};
|
||||
|
||||
// attach 结果遥测(Inspector "匹配/跳过" 显示用;
|
||||
// 幂等命中/解析失败 = 全 0)
|
||||
struct AttachResult {
|
||||
u32 clipsAdded = 0;
|
||||
u32 channelsMatched = 0; // 同名直映轨道数
|
||||
u32 channelsRetargeted = 0; // 槽位重定向轨道数
|
||||
u32 channelsSkipped = 0;
|
||||
std::vector<RetargetRow> retargetRows; // 重定向明细(调试)
|
||||
};
|
||||
|
||||
// 把 src 的 clips 按目标模型 node 表重映射后追加。
|
||||
// 幂等:sourcePath 已并入过(自愈重放/重复 attach)→ 零操作。
|
||||
// src 数据被消费(move 语义);无 GPU 依赖,纯 CPU
|
||||
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
|
||||
const std::string& sourcePath);
|
||||
|
||||
// 纯算法核心(headless 探针可直测):轨道重映射进 dstNodes
|
||||
// 坐标系,产出的 clips 追加到 outExtra。幂等/命名冲突检查
|
||||
// 基于 existingClips。AttachClipsToModel 只是
|
||||
// "取 nodes/clips + 回填" 的薄封装
|
||||
AttachResult AttachClipsToNodes(
|
||||
const std::vector<ModelNode>& dstNodes,
|
||||
const std::vector<AnimationClip>& existingClips,
|
||||
ParsedModel&& src, const std::string& sourcePath,
|
||||
std::vector<AnimationClip>& outExtra);
|
||||
|
||||
} // namespace Animation
|
||||
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// AnimationController:动画状态机资产(.animctrl,YAML)——Unity
|
||||
// AnimatorController / UE AnimBlueprint 数据侧的对应物。
|
||||
//
|
||||
// 职责:纯数据资产(参数声明 / 状态表 / 混合树 / 过渡规则),零求值
|
||||
// 逻辑零运行时状态。clip 按"名字"引用(不持骨骼数据)——同一
|
||||
// Controller 可复用到任何 clip 名匹配的模型(缺 clip 降级跳过该层,
|
||||
// 不崩)。加载走 AssetManager 缓存(模式同 MaterialAsset)。
|
||||
//
|
||||
// 与求值的接缝:AnimationSystem 每帧把"当前状态 + 组件参数值"翻译成
|
||||
// PoseLayer[](本资产只提供规则查询),N 层混合在 AnimationEvaluator。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ANIMATIONCONTROLLER_H
|
||||
#define HELLOVULKAN_ANIMATIONCONTROLLER_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
// 混合树子节点:clip 名 + 2D 平面定位点
|
||||
struct BlendTreeChild {
|
||||
std::string clip; // clip 名(运行时按名查 Model::GetClips)
|
||||
glm::vec2 pos{0.0f}; // 2D 平面定位(1D 时 x=阈值,y 忽略)
|
||||
};
|
||||
|
||||
// 状态的运动源:单 clip 直采 or 混合树
|
||||
struct BlendTree {
|
||||
enum class Type : u8 {
|
||||
Single = 0, // 直接播一个 clip
|
||||
Directional1D = 1,// 一轴阈值插值(Speed 站走跑)
|
||||
Directional2D = 2,// 2D 方向混合(MoveX/MoveZ → 8 方向)
|
||||
};
|
||||
Type type = Type::Single;
|
||||
std::string clip; // Single:clip 名
|
||||
std::string xParam; // 1D/2D:横轴参数名
|
||||
std::string zParam; // 2D:纵轴参数名
|
||||
std::vector<BlendTreeChild> children;
|
||||
};
|
||||
|
||||
// 过渡条件(Load 期解析 cond 字符串,运行时只做数值比较)
|
||||
struct TransitionCond {
|
||||
enum class Op : u8 { Gt, Lt, Ge, Le, Eq, Ne };
|
||||
std::string param;
|
||||
Op op = Op::Gt;
|
||||
f32 numOperand = 0.0f; // float 比较
|
||||
bool boolOperand = false; // bool 比较(true/false)
|
||||
bool isBool = false; // bool 语义(含 trigger 名命中即真)
|
||||
bool always = false; // cond 空 = 恒真(无条件出边)
|
||||
bool valid = false; // 解析失败 = false(永不触发)
|
||||
};
|
||||
|
||||
struct Transition {
|
||||
std::string from; // "*" = AnyState
|
||||
std::string to;
|
||||
TransitionCond cond;
|
||||
f32 fade = 0.25f; // 过渡时长(秒)
|
||||
bool hasExitTime = false; // true = 当前状态非循环播完一轮才评估
|
||||
};
|
||||
|
||||
struct ControllerState {
|
||||
std::string name;
|
||||
BlendTree tree;
|
||||
f32 speed = 1.0f; // 状态内播放速度倍率(乘组件 speed)
|
||||
bool loop = true; // false = 播完一轮即停(transitionsOnEnd 生效)
|
||||
std::vector<std::string> transitionsOnEnd; // 播完出口(按序首个可用)
|
||||
};
|
||||
|
||||
// 参数声明(类型 + 默认值;实例值在 AnimationComponent)
|
||||
struct ControllerParam {
|
||||
enum class Type : u8 { Float = 0, Bool = 1, Trigger = 2 };
|
||||
std::string name;
|
||||
Type type = Type::Float;
|
||||
f32 defFloat = 0.0f;
|
||||
bool defBool = false;
|
||||
};
|
||||
|
||||
// 2D 方向混合树权重求解(单一事实源:System 运行时采样与编辑器
|
||||
// 方向盘可视化共用同一套数学)——原点子(|pos|<ε)权重 = 1-|v|,
|
||||
// 方向子找覆盖 v 角度的相邻扇区做角度插值 ×min(|v|,1)。只回有权重
|
||||
// >0 的子节点(下标入 children + 权重)。依赖 children 已按 Load
|
||||
// 期角度排序
|
||||
void EvaluateBlendTree2D(const BlendTree& tree, f32 x, f32 z,
|
||||
std::vector<std::pair<u32, f32>>& outWeights);
|
||||
|
||||
class AnimationController {
|
||||
public:
|
||||
// .animctrl YAML 解析;失败返回 nullptr + ERROR 日志。
|
||||
// Version 缺省键向后兼容(同 MaterialAsset 惯例)
|
||||
static Scope<AnimationController> Load(const std::string& path);
|
||||
// 写回 .animctrl(编辑器 Save 用:与 Load 对齐的 YAML 反序列化;
|
||||
// 失败返回 false + ERROR 日志,缓存实例不动)
|
||||
bool Save(const std::string& path) const;
|
||||
// 条件字符串 <-> 编译结构(面板编辑 cond 输入框 Enter 时就地
|
||||
// 重编译,Save 时反推字符串)
|
||||
static TransitionCond ParseCond(const std::string& s);
|
||||
static std::string CondToString(const TransitionCond& c);
|
||||
const std::vector<ControllerParam>& GetParams() const
|
||||
{
|
||||
return m_Params;
|
||||
}
|
||||
const std::unordered_map<std::string, ControllerState>&
|
||||
GetStates() const { return m_States; }
|
||||
// 状态声明序(编辑器 UI 稳定枚举 + entry 兜底;unordered_map
|
||||
// 迭代无序,UI/初始化一律走这里)
|
||||
const std::vector<std::string>& GetStateOrder() const
|
||||
{
|
||||
return m_StateOrder;
|
||||
}
|
||||
// 入口状态:显式 Entry 键优先,缺省 = states 声明序首个
|
||||
const std::string& GetEntryState() const { return m_Entry; }
|
||||
const std::vector<Transition>& GetTransitions() const
|
||||
{
|
||||
return m_Transitions;
|
||||
}
|
||||
const std::string& GetPath() const { return m_Path; }
|
||||
|
||||
// ---- 编辑器可变访问(ControllerEditorPanel 专用;改完
|
||||
// Save 写回 + AssetManager::ReloadController 刷新缓存)----
|
||||
std::vector<ControllerParam>& ParamsMut() { return m_Params; }
|
||||
std::unordered_map<std::string, ControllerState>& StatesMut()
|
||||
{
|
||||
return m_States;
|
||||
}
|
||||
std::vector<std::string>& StateOrderMut() { return m_StateOrder; }
|
||||
std::vector<Transition>& TransitionsMut() { return m_Transitions; }
|
||||
void SetEntry(const std::string& e) { m_Entry = e; }
|
||||
|
||||
private:
|
||||
std::vector<ControllerParam> m_Params;
|
||||
std::unordered_map<std::string, ControllerState> m_States;
|
||||
std::vector<std::string> m_StateOrder;
|
||||
std::vector<Transition> m_Transitions;
|
||||
std::string m_Entry;
|
||||
std::string m_Path;
|
||||
};
|
||||
|
||||
} // namespace Engine::Animation
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// AnimationEvaluator:骨骼动画姿态求值器(UE FAnimInstance 单节点求值 /
|
||||
// Unity PlayableGraph 求值语义的极简对应物)。
|
||||
//
|
||||
// 动画系统职责分层(2026-10-06 重构,对齐 UE/Unity):
|
||||
// Model = 资产层:node/bone/clip 静态数据(assimp 解析产物)
|
||||
// AnimationComponent = 状态层:播放头/播放设置/crossfade 计时(纯数据)
|
||||
// AnimationEvaluator = 求值层:clip 时间 → 全节点 global 矩阵(本文件)
|
||||
// AnimationSystem = 调度层:逐实体推进状态 + 调求值器 + 产 palette
|
||||
// SceneRenderer = 消费层:只读 palette/entitySkinBases 填实例
|
||||
//
|
||||
// 求值器无调度、无渲染依赖:同一姿态可在任意时刻重复求值(渲染主循环
|
||||
// 与编辑器骨骼可视化复用同一套数学,保证"所见即所渲")。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ANIMATIONEVALUATOR_H
|
||||
#define HELLOVULKAN_ANIMATIONEVALUATOR_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
// 单层采样的全节点局部 TRS(Layer 语义)。
|
||||
// touched = 被任意轨道触碰的节点标记(ComposeGlobals 决定重建或
|
||||
// 原样——FBX PreRotation 折在 localTransform 里,拆 TRS 重建会丢,
|
||||
// 未触碰节点必须保持原矩阵)。容量跨帧复用,稳态零堆分配
|
||||
struct LocalPose {
|
||||
std::vector<glm::vec3> pos;
|
||||
std::vector<glm::quat> rot;
|
||||
std::vector<glm::vec3> scl;
|
||||
std::vector<u8> touched;
|
||||
};
|
||||
|
||||
// crossfade 求值快照(纯数据):src = 淡出中的来源 clip
|
||||
//(nullptr = 无过渡 / 过渡已结束),wCur = 当前 clip 权重 0..1
|
||||
//(smoothstep 已算好)。计时推进与 fadeFrom 状态翻转归 AnimationSystem
|
||||
struct CrossfadeState {
|
||||
const AnimationClip* src = nullptr;
|
||||
f32 srcTime = 0.0f;
|
||||
f32 wCur = 1.0f;
|
||||
};
|
||||
|
||||
// 单个混合层:一个 clip 采样实例 + 权重。权重语义 = 归一化前的
|
||||
// 相对权重(求值器内部按有效层权重和归一化;全部为 0 = rest)
|
||||
struct PoseLayer {
|
||||
const AnimationClip* clip = nullptr; // nullptr = 该层跳过
|
||||
f32 time = 0.0f;
|
||||
f32 weight = 1.0f;
|
||||
};
|
||||
|
||||
// 姿态求值器。内部持有 N 层 LocalPose scratch(跨帧复用容量),
|
||||
// 单实例非线程安全——动画推进收敛在 AnimationSystem 单点调用
|
||||
class AnimationEvaluator {
|
||||
public:
|
||||
// cur = 当前 clip,curTime = 当前播放头(秒)。
|
||||
// xf.src 非空时双层局部 TRS 混合(位置/缩放 lerp、旋转 slerp,
|
||||
// 权重 = xf.wCur)→ global 累乘。
|
||||
// outGlobals 尺寸 = model.GetNodes().size(),父序 DFS 累乘。
|
||||
// (crossfade 双层 = EvaluateLayers 的 count==2 特例,同一套
|
||||
// 加权混合数学,消掉历史双份代码)
|
||||
// inPlace = 丢弃动画子树根节点的位移(Unity in-place 语义:
|
||||
// 根运动开关关闭,原地播放,旋转保留)
|
||||
void Evaluate(const Model& model, const AnimationClip& cur,
|
||||
f32 curTime, const CrossfadeState& xf,
|
||||
std::vector<glm::mat4>& outGlobals,
|
||||
bool inPlace = false);
|
||||
|
||||
// N 层加权混合(BlendTree / 状态机过渡的通用入口):
|
||||
// 逐层采样局部 TRS → 权重归一化混合(pos/scl lerp、rot 渐进
|
||||
// slerp)→ global 累乘。全空/全零权重 = rest 姿态
|
||||
void EvaluateLayers(const Model& model, const PoseLayer* layers,
|
||||
u32 count, std::vector<glm::mat4>& outGlobals,
|
||||
bool inPlace = false);
|
||||
|
||||
// 绑定姿势:全节点 untouched = localTransform 原样累乘。
|
||||
// 与渲染 identity palette 的表现一致(无动画/组件禁用实体的
|
||||
// 姿态)——骨骼可视化对非动画实体用它,保证可视化 = 渲染
|
||||
void ComputeRestPose(const Model& model,
|
||||
std::vector<glm::mat4>& outGlobals);
|
||||
|
||||
private:
|
||||
// 混合循环的(pose, weight)配对视图:m_Layers 是采样存储,
|
||||
// m_Sampled 指向其中的有效层并携带原权重
|
||||
struct SampledLayer {
|
||||
const LocalPose* pose;
|
||||
f32 weight;
|
||||
};
|
||||
LocalPose m_A; // 混合累积层(= 直接模式当前层)
|
||||
std::vector<LocalPose> m_Layers; // N 层采样 scratch(容量跨帧复用)
|
||||
std::vector<SampledLayer> m_Sampled; // 有效层配对 scratch
|
||||
};
|
||||
|
||||
} // namespace Engine::Animation
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// AnimationSystem:骨骼动画调度器(UE AnimInstance 求值管线 / Unity
|
||||
// Animator Update 的分层对应物)。
|
||||
//
|
||||
// Update 每帧做三件事:
|
||||
// 1. 逐实体推进 AnimationComponent 播放头 / crossfade 计时(状态层,
|
||||
// 组件是唯一持久状态,System 只改写这些字段)
|
||||
// 2. 调 AnimationEvaluator 把 clip 时间变成全节点 global 矩阵(求值层)
|
||||
// 3. palette bump 分配写进 FrameSceneData(渲染消费契约)
|
||||
//
|
||||
// 与渲染的单向契约:System 写 skinPalettes / entitySkinBases /
|
||||
// skinPaletteTruncated(+ 编辑形态 boneWireVerts),SceneRenderer 收集段
|
||||
// 只读查表。动画推进与可见性无关——被剔除实体播放头恒走,回屏不跳变。
|
||||
//
|
||||
// 无状态机、无混合树(按需再演进):crossfade 就是全部过渡语义,
|
||||
// 与既有 Play(index, fadeSeconds) 接口一一对应。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ANIMATIONSYSTEM_H
|
||||
#define HELLOVULKAN_ANIMATIONSYSTEM_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Animation/AnimationEvaluator.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
namespace ECS { class Scene; }
|
||||
namespace Assets { class AssetManager; }
|
||||
|
||||
namespace Animation {
|
||||
|
||||
class AnimationSystem {
|
||||
public:
|
||||
// dt 单位秒。scene 只读遍历(组件状态字段除外——播放头
|
||||
// 推进是 System 的本职写入);frame 是唯一产出出口
|
||||
void Update(ECS::Scene& scene, Assets::AssetManager& assets,
|
||||
f32 dt, FrameSceneData& frame);
|
||||
|
||||
private:
|
||||
#ifdef HELLO_EDITOR
|
||||
// 选中实体骨骼线框(Part B 可视化,仅编辑形态存在):
|
||||
// 动画实体用当帧求值姿态,非动画/禁用实体用绑定姿势。
|
||||
// 产出 frame.boneWireVerts,EditorGizmoPass 拼线绘制
|
||||
void EmitSelectedSkeleton(ECS::Scene& scene,
|
||||
Assets::AssetManager& assets,
|
||||
FrameSceneData& frame);
|
||||
#endif
|
||||
|
||||
AnimationEvaluator m_Evaluator;
|
||||
std::vector<glm::mat4> m_Globals; // 求值输出 scratch
|
||||
// Controller 模式混合层(BlendTree / 状态机过渡翻译产物)
|
||||
std::vector<PoseLayer> m_LayerScratch;
|
||||
std::vector<PoseLayer> m_FromLayers; // 过渡来源状态层
|
||||
std::vector<std::pair<u32, f32>> m_BlendWeights; // 2D 树权重 scratch
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
// 选中实体当帧姿态暂存(求值循环内命中即拷,供
|
||||
// EmitSelectedSkeleton 用;未命中/非动画 → 绑定姿势路径)
|
||||
std::vector<glm::mat4> m_SelGlobals;
|
||||
bool m_SelHasPose = false;
|
||||
#endif
|
||||
|
||||
bool m_TruncWarned = false; // palette 预算溢出只告警一次
|
||||
};
|
||||
|
||||
} // namespace Animation
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,168 @@
|
||||
//
|
||||
// HumanoidRig:Unity Avatar 语义的跨骨架重定向映射。
|
||||
//
|
||||
// 架构分层(对齐 Unity Mecanim / UE IK Retargeter 的做法):
|
||||
//
|
||||
// ┌─ standard ─ 标准人形拓扑:HumanoidSlot 52 槽(主干 6 + 臂链
|
||||
// │ 4×2 + 指骨 15×2 + 腿链 3×2 + 趾 2),带侧别/镜像/
|
||||
// │ 指骨结构化访问。重定向的"标准空间"。
|
||||
// ├─ dialects ─ 命名方言数据表:各来源(Mixamo/Rigify/UE/MMD)的
|
||||
// │ "槽位 → 归一化骨名"候选模式,纯数据无特判。命名
|
||||
// │ 习惯差异全部收敛在这层(Unity 把 token 规则编成
|
||||
// │ 数据表、UE 用链名映射,同思想:识别=查表)。
|
||||
// └─ mapper ── 归一化(NormalizeLower → side 抽取 → 分隔符剥离)
|
||||
// + 查表识别(MatchHumanoidSlot)+ 跨骨架配对
|
||||
// (AutoMapHumanoid:同名直映优先,余下槽位一对一)。
|
||||
//
|
||||
// 动画重定向本身在 AnimationAttach:src 动画经映射对烘焙到 dst
|
||||
// 骨架(两遍式 T-pose 参考搬运),本模块只负责"名字 → 槽位 → 配对"。
|
||||
//
|
||||
// 资产组装现实:角色模型与动画 clip 是两套独立资产,骨骼命名完全
|
||||
// 不同源。同名直映是映射的特例(同源拆分资产零损耗)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_HUMANOIDRIG_H
|
||||
#define HELLOVULKAN_HUMANOIDRIG_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
// ---- standard:标准人形槽位(Unity Avatar 52 骨拓扑)----
|
||||
//
|
||||
// 布局保证算术可推导(镜像/指骨反查都走位运算友好的分段):
|
||||
// [0..5] 主干:Hips→Spine→Chest→UpperChest→Neck→Head
|
||||
// [6..9] 左臂链:Shoulder→UpperArm→LowerArm→Hand
|
||||
// [10..24] 左指骨:5 指 × 3 段(Thumb/Index/Middle/Ring/Pinky,
|
||||
// 段 1=近节 → 3=末节,与 Mixamo HandThumb1-3、Rigify
|
||||
// .01-.03、UE Proximal/Distal 对齐)
|
||||
// [25..28] 左腿链:UpperLeg→LowerLeg→Foot→Toes
|
||||
// [29..51] 右侧镜像(+23 偏移,镜像布局与左完全同构)
|
||||
enum class HumanoidSlot : u32 {
|
||||
Hips = 0,
|
||||
Spine,
|
||||
Chest,
|
||||
UpperChest,
|
||||
Neck,
|
||||
Head,
|
||||
LeftShoulder,
|
||||
LeftUpperArm,
|
||||
LeftLowerArm,
|
||||
LeftHand,
|
||||
LeftThumb1, // 指骨区起点(FingerSlot 算术依赖连续布局)
|
||||
LeftThumb2,
|
||||
LeftThumb3,
|
||||
LeftIndex1,
|
||||
LeftIndex2,
|
||||
LeftIndex3,
|
||||
LeftMiddle1,
|
||||
LeftMiddle2,
|
||||
LeftMiddle3,
|
||||
LeftRing1,
|
||||
LeftRing2,
|
||||
LeftRing3,
|
||||
LeftPinky1,
|
||||
LeftPinky2,
|
||||
LeftPinky3,
|
||||
LeftUpperLeg,
|
||||
LeftLowerLeg,
|
||||
LeftFoot,
|
||||
LeftToes,
|
||||
RightShoulder,
|
||||
RightUpperArm,
|
||||
RightLowerArm,
|
||||
RightHand,
|
||||
RightThumb1,
|
||||
RightThumb2,
|
||||
RightThumb3,
|
||||
RightIndex1,
|
||||
RightIndex2,
|
||||
RightIndex3,
|
||||
RightMiddle1,
|
||||
RightMiddle2,
|
||||
RightMiddle3,
|
||||
RightRing1,
|
||||
RightRing2,
|
||||
RightRing3,
|
||||
RightPinky1,
|
||||
RightPinky2,
|
||||
RightPinky3,
|
||||
RightUpperLeg,
|
||||
RightLowerLeg,
|
||||
RightFoot,
|
||||
RightToes,
|
||||
Count
|
||||
};
|
||||
|
||||
// 槽位侧别(左右对称骨骼的识别结果)
|
||||
enum class Side : u8 { None, Left, Right };
|
||||
|
||||
// 指别(0=拇指 … 4=小指;方言表里 Mixamo pinky / UE little 同槽)
|
||||
enum class FingerKind : u32 { Thumb = 0, Index, Middle, Ring, Pinky };
|
||||
|
||||
// ---- standard:槽位结构化访问 ----
|
||||
|
||||
// 槽位侧别(主干槽返回 None;镜像侧与枚举名一致)
|
||||
Side SideOfSlot(HumanoidSlot slot);
|
||||
|
||||
// 左右镜像(主干槽原样返回)
|
||||
HumanoidSlot MirrorSlot(HumanoidSlot slot);
|
||||
|
||||
// 指骨槽位合成:(side, 指, 段 1..3) → 槽位
|
||||
HumanoidSlot FingerSlot(Side side, FingerKind kind, u32 seg);
|
||||
|
||||
// 指骨槽位反解(非指骨槽位返回 false)
|
||||
bool SlotFinger(HumanoidSlot slot, Side& side, FingerKind& kind,
|
||||
u32& seg);
|
||||
|
||||
// 槽位显示名(Unity 语义:"Hips"/"LeftUpperArm"/"LeftHandThumb2"...;
|
||||
// 调试映射表/Inspector 明细行用)
|
||||
const char* HumanoidSlotName(HumanoidSlot slot);
|
||||
|
||||
// ---- mapper:名字归一化 + 槽位识别 ----
|
||||
|
||||
// 名字归一化 + 查方言表识别槽位。
|
||||
// "mixamorig:LeftForeArm" → {LowerArm, Left};"左ひじ" → {LowerArm, Left};
|
||||
// "Upper Arm_L" → {UpperArm, Left};"DEF-thumb.01.L" → {Thumb1, Left};
|
||||
// "Thumb Proximal_R" → {Thumb1, Right}。识别失败 ok=false。
|
||||
HumanoidSlot MatchHumanoidSlot(std::string_view name, Side& side,
|
||||
bool& ok);
|
||||
|
||||
// 跨骨架自动映射:返回 (srcNodeIdx, dstNodeIdx) 对列表。
|
||||
// 同名 node 直映优先(同源拆分资产零损耗),余下按槽位一对一配对;
|
||||
// 同槽多候选时取方言表内的优先序(先高后低)。
|
||||
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
|
||||
const std::vector<ModelNode>& srcNodes,
|
||||
const std::vector<ModelNode>& dstNodes);
|
||||
|
||||
// 估计源/目标骨架的根对齐旋转(rest 姿态 basis 对齐,src 世界 →
|
||||
// dst 世界)。跨骨架 retarget 时两套骨架常不同朝向(kwy DummyDoll
|
||||
// Z-up vs X Bot Y-up),增量变换须经该旋转共轭才能跨空间传递。
|
||||
// up = 头 - 双脚中点;right = 右臂 - 左臂(去 up 分量);
|
||||
// fwd = up × right,脚趾朝向消歧。槽位不全(缺头/双脚/双臂)
|
||||
// 或 rest 退化时返回 identity。
|
||||
glm::quat EstimateHumanoidRootAlignment(
|
||||
const std::vector<ModelNode>& srcNodes,
|
||||
const std::vector<ModelNode>& dstNodes);
|
||||
|
||||
// 估计骨架的 T-pose 参考姿势(每节点**世界**旋转)。
|
||||
// 人形动画资产的摆动量以 T-pose 为零位;rest 是 A-pose(臂下垂)
|
||||
// 时,旋转增量直接搬运会过摆(臂越过中线)。此处把臂链
|
||||
// (upper_arm 子树)绕肩刚体摆到水平侧向作为参考,其余骨取 rest
|
||||
// (手指/趾走 rest 零位局部增量通道,见 AnimationAttach——指根
|
||||
// 扇形编码在 rest 平移里,不参与世界系零位搬运);rest 本为
|
||||
// T-pose 的资产摆动量≈0 自动退化。行走基不可用时全 rest。
|
||||
std::vector<glm::quat> EstimateHumanoidTPose(
|
||||
const std::vector<ModelNode>& nodes,
|
||||
const std::vector<glm::mat4>& restGlobals);
|
||||
|
||||
} // namespace Engine::Animation
|
||||
|
||||
#endif // HELLOVULKAN_HUMANOIDRIG_H
|
||||
@@ -1,37 +0,0 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/22.
|
||||
//
|
||||
// AssetHandle:资产句柄 = (池内索引, 生成号),与 Entity/RG 句柄同一哲学。
|
||||
//
|
||||
// 组件、场景数据只存句柄;所有权归 AssetManager。
|
||||
// 悬空句柄(generation 不匹配)会被 Get 显式拒绝,杜绝 use-after-free。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASSETHANDLE_H
|
||||
#define HELLOVULKAN_ASSETHANDLE_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
// Model 前向声明(句柄不依赖完整类型)
|
||||
namespace Engine { class Model; }
|
||||
|
||||
namespace Engine::Assets {
|
||||
|
||||
inline constexpr u32 kInvalidAssetIndex = 0xFFFFFFFF;
|
||||
|
||||
template<typename T>
|
||||
struct AssetHandle {
|
||||
u32 index = kInvalidAssetIndex;
|
||||
u32 generation = 0;
|
||||
|
||||
bool Valid() const { return index != kInvalidAssetIndex; }
|
||||
explicit operator bool() const { return Valid(); }
|
||||
bool operator==(const AssetHandle& o) const = default;
|
||||
};
|
||||
|
||||
// 常用别名
|
||||
using ModelHandle = AssetHandle<Engine::Model>;
|
||||
|
||||
} // namespace Engine::Assets
|
||||
|
||||
#endif //HELLOVULKAN_ASSETHANDLE_H
|
||||
@@ -3,14 +3,9 @@
|
||||
//
|
||||
// AssetManager:引擎资产层。
|
||||
//
|
||||
// 职责划分(对照 Renderer):
|
||||
// - 资产层:路径 → 解析/加载 → 缓存去重 → 句柄分发 + 生命周期管理
|
||||
// - 渲染层:拿已解析资产创建/绑定 GPU 资源
|
||||
//
|
||||
// Model 的构造仍需要 GPU 上下文(buffer/image upload),这是现阶段的
|
||||
// 折中:资产层"使用"GPU 层的接口,但 Renderer 不再知道磁盘和缓存。
|
||||
// 后续演进:把 Model 拆成 CPU 解析数据 + GPU 上传两段,实现真正的
|
||||
// 异步加载(IO 线程解析 → 主线程/上传队列提交),本文件结构已为此留好位置。
|
||||
// 职责:路径 → 解析/加载 → 缓存去重 → 句柄分发 + 生命周期管理。
|
||||
// GPU 创建不在这里:经 IAssetUploader 接口(渲染层提供实现)完成,
|
||||
// 本类不感知 Vulkan——依赖方向 Assets → 接口,实现由组合根注入。
|
||||
//
|
||||
// 组件存句柄不存指针的理由:
|
||||
// shared_ptr 会引入共享所有权 → 原子引用计数(swap-remove 拷贝时)
|
||||
@@ -23,50 +18,56 @@
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Assets/AssetHandle.h>
|
||||
#include <Engine/Core/AssetRef.h>
|
||||
#include <Engine/Animation/AnimationAttach.h>
|
||||
#include <Engine/Animation/AnimationController.h>
|
||||
#include <Engine/Assets/IAssetUploader.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/MaterialAsset.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class MemoryAllocator;
|
||||
class DescriptorManager;
|
||||
|
||||
namespace Assets {
|
||||
|
||||
namespace Engine
|
||||
{
|
||||
namespace Assets
|
||||
{
|
||||
// ---------- 通用资产缓存(不感知 Vulkan / 磁盘细节由 loader 决定) ----------
|
||||
|
||||
template <typename T>
|
||||
class AssetCache {
|
||||
class AssetCache
|
||||
{
|
||||
public:
|
||||
// loader:path → 资产实例(失败返回 nullptr)
|
||||
using LoaderFn = std::function<Scope<T>(const std::string&)>;
|
||||
|
||||
// 加载(带路径去重)。失败返回无效句柄。
|
||||
AssetHandle<T> Load(const std::string& path, const LoaderFn& loader)
|
||||
AssetRef<T> Load(const std::string& path, const LoaderFn& loader)
|
||||
{
|
||||
if (auto it = m_ByPath.find(path); it != m_ByPath.end())
|
||||
{
|
||||
if (auto it = m_ByPath.find(path); it != m_ByPath.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
Scope<T> asset = loader(path);
|
||||
if (!asset) {
|
||||
if (!asset)
|
||||
{
|
||||
ENGINE_ERROR("AssetCache: failed to load '{}'", path);
|
||||
return {};
|
||||
}
|
||||
|
||||
AssetHandle<T> h;
|
||||
if (!m_Free.empty()) {
|
||||
AssetRef<T> h;
|
||||
if (!m_Free.empty())
|
||||
{
|
||||
h.index = m_Free.back();
|
||||
m_Free.pop_back();
|
||||
h.generation = m_Entries[h.index].generation;
|
||||
m_Entries[h.index] = Entry{path, h.generation, std::move(asset), true};
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
h.index = static_cast<u32>(m_Entries.size());
|
||||
h.generation = 1;
|
||||
m_Entries.push_back(Entry{path, h.generation, std::move(asset), true});
|
||||
@@ -77,14 +78,16 @@ namespace Engine {
|
||||
}
|
||||
|
||||
// 句柄 → 资产指针。悬空句柄 FATAL(bug 信号);已卸载返回 nullptr。
|
||||
T* Get(AssetHandle<T> h) const
|
||||
T* Get(AssetRef<T> h) const
|
||||
{
|
||||
if (!h.Valid() || h.index >= m_Entries.size())
|
||||
{
|
||||
if (!h.Valid() || h.index >= m_Entries.size()) {
|
||||
ENGINE_FATAL("AssetCache::Get: invalid handle (index={}, gen={})",
|
||||
h.index, h.generation);
|
||||
}
|
||||
const Entry& e = m_Entries[h.index];
|
||||
if (e.generation != h.generation) {
|
||||
if (e.generation != h.generation)
|
||||
{
|
||||
ENGINE_FATAL("AssetCache::Get: stale handle for '{}' (gen {} != {})",
|
||||
e.path, h.generation, e.generation);
|
||||
}
|
||||
@@ -94,7 +97,7 @@ namespace Engine {
|
||||
// 卸载:句柄即刻失效(generation 递增),路径缓存清除。
|
||||
// 注意:GPU 侧何时安全释放由上层(Renderer fence)保证——
|
||||
// 本类只管所有权,不管 GPU 生命周期。
|
||||
void Unload(AssetHandle<T> h)
|
||||
void Unload(AssetRef<T> h)
|
||||
{
|
||||
if (!h.Valid() || h.index >= m_Entries.size()) return;
|
||||
|
||||
@@ -109,7 +112,15 @@ namespace Engine {
|
||||
m_Free.push_back(h.index);
|
||||
}
|
||||
|
||||
const std::string& Path(AssetHandle<T> h) const
|
||||
// 按路径卸载(未加载为 no-op;编辑器资产重建用——
|
||||
// 手上只有路径没有句柄的场景)
|
||||
void UnloadByPath(const std::string& path)
|
||||
{
|
||||
auto it = m_ByPath.find(path);
|
||||
if (it != m_ByPath.end()) Unload(it->second);
|
||||
}
|
||||
|
||||
const std::string& Path(AssetRef<T> h) const
|
||||
{
|
||||
static const std::string kEmpty;
|
||||
if (!h.Valid() || h.index >= m_Entries.size()) return kEmpty;
|
||||
@@ -117,13 +128,31 @@ namespace Engine {
|
||||
return (e.generation == h.generation && e.alive) ? e.path : kEmpty;
|
||||
}
|
||||
|
||||
// 按路径查活资产(未加载/已卸载返回 nullptr;不触发加载、
|
||||
// 不对坏句柄 FATAL)——面板展示用
|
||||
T* Find(const std::string& path) const
|
||||
{
|
||||
auto it = m_ByPath.find(path);
|
||||
if (it == m_ByPath.end()) return nullptr;
|
||||
const Entry& e = m_Entries[it->second.index];
|
||||
return (e.alive && e.generation == it->second.generation)
|
||||
? e.asset.get() : nullptr;
|
||||
}
|
||||
|
||||
u32 Count() const
|
||||
{
|
||||
return static_cast<u32>(m_Entries.size()) - static_cast<u32>(m_Free.size());
|
||||
}
|
||||
|
||||
// 路径是否在缓存(含被卸载后路径清除的语义:卸载即不含)
|
||||
bool Contains(const std::string& path) const
|
||||
{
|
||||
return m_ByPath.find(path) != m_ByPath.end();
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
struct Entry
|
||||
{
|
||||
std::string path;
|
||||
u32 generation = 1;
|
||||
Scope<T> asset;
|
||||
@@ -132,44 +161,181 @@ namespace Engine {
|
||||
|
||||
std::vector<Entry> m_Entries;
|
||||
std::vector<u32> m_Free;
|
||||
std::unordered_map<std::string, AssetHandle<T>> m_ByPath;
|
||||
std::unordered_map<std::string, AssetRef<T>> m_ByPath;
|
||||
};
|
||||
|
||||
// ---------- AssetManager:全引擎资产入口 ----------
|
||||
// (ModelHandle 别名来自 AssetHandle.h)
|
||||
|
||||
class AssetManager {
|
||||
class AssetManager
|
||||
{
|
||||
public:
|
||||
// GPU 上下文由组合根(Application)在 Renderer 装配完成后注入
|
||||
AssetManager(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptors,
|
||||
vk::DescriptorSetLayout textureLayout);
|
||||
// 上传服务由组合根(Application)注入:渲染层实现接口,
|
||||
// 资产层不感知 Vulkan 具体类
|
||||
explicit AssetManager(IAssetUploader& uploader);
|
||||
~AssetManager() = default;
|
||||
|
||||
AssetManager(const AssetManager&) = delete;
|
||||
AssetManager& operator=(const AssetManager&) = delete;
|
||||
|
||||
ModelHandle LoadModel(const std::string& path);
|
||||
Model* GetModel(ModelHandle h) const { return m_Models.Get(h); }
|
||||
void UnloadModel(ModelHandle h) { m_Models.Unload(h); }
|
||||
ModelRef LoadModel(const std::string& path);
|
||||
// 异步路径:解析已在后台线程完成(路径已缓存则产物丢弃去重)
|
||||
ModelRef LoadModel(const std::string& path, ParsedModel&& parsed);
|
||||
Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
|
||||
void UnloadModel(ModelRef h) { m_Models.Unload(h); }
|
||||
// 批量按路径卸载模型(未加载为 no-op)。先 GPU 全停飞再销毁
|
||||
//(在飞命令缓冲可能仍引用顶点缓冲/默认纹理 sampler——
|
||||
// 与 ReloadMaterialAsset 的 WaitDeviceIdle 同一先例);一次
|
||||
// 停飞摊薄整批,调用方攒批降频(脚本碎片回收每帧一批)
|
||||
void UnloadModelsByPaths(const std::vector<std::string>& paths)
|
||||
{
|
||||
if (paths.empty()) return;
|
||||
m_Uploader.WaitDeviceIdle();
|
||||
for (const std::string& p : paths) {
|
||||
m_Models.UnloadByPath(p);
|
||||
}
|
||||
}
|
||||
// 按路径查已加载的模型(未加载返回 null,不触发加载)——
|
||||
// 面板 hover 信息卡用
|
||||
Model* GetModelByPath(const std::string& path) const
|
||||
{
|
||||
return m_Models.Find(path);
|
||||
}
|
||||
|
||||
// ---- 外部动画并入(UE AnimSequence 归属 Skeleton 的简化版)----
|
||||
// 把动画容器文件(无网格 FBX/GLB 等)里的 clip 按骨骼节点名
|
||||
// 重映射进目标模型的 clip 表。前提 = 源/目标骨骼命名同源(如
|
||||
// 同一 DCC 拆分/Mixamo 同角色);名字对不上的轨道跳过并计数。
|
||||
// 幂等:目标 clips 已含该源(sourcePath)时不重复并入。
|
||||
// 本方法只是 facade(取模型 + 解析磁盘),重映射算法在
|
||||
// Animation::AttachClipsToModel
|
||||
using AnimAttachResult = Animation::AttachResult;
|
||||
AnimAttachResult AttachAnimationClips(ModelRef target,
|
||||
const std::string& animPath);
|
||||
// 对称剥离(编辑器 detach):删除目标模型里来自 animPath 的
|
||||
// clip,返回删除数。facade(取模型 + 调 Model 方法)
|
||||
u32 DetachAnimationClips(ModelRef target,
|
||||
const std::string& animPath)
|
||||
{
|
||||
Model* m = GetModel(target);
|
||||
return m ? m->RemoveClipsBySource(animPath) : 0u;
|
||||
}
|
||||
|
||||
// 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。
|
||||
ModelHandle CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
|
||||
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);
|
||||
// 程序化立方体(halfSize 为半边长):同参数缓存去重。
|
||||
// 尺寸变化走实体 Transform 缩放,勿按尺寸炸缓存
|
||||
ModelRef CreateCube(f32 halfSize, u8 r, u8 g, u8 b);
|
||||
|
||||
// 通用程序化图元(MeshFactory 形状名 + 参数):同形状同参数同色
|
||||
// 缓存去重。几何唯一源 = MeshFactory(见 MeshFactory.h)
|
||||
ModelRef CreatePrimitive(const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b);
|
||||
|
||||
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
|
||||
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
|
||||
EnvRef LoadEnvironment(const std::string& path);
|
||||
// 异步路径:解码已在后台线程完成(若路径已缓存,解码产物被
|
||||
// 丢弃去重——上层可先查缓存再起 worker,此处不做强约束)
|
||||
EnvRef LoadEnvironment(const std::string& path,
|
||||
DecodedEquirect&& decoded);
|
||||
Cubemap* GetEnvironment(EnvRef h) const { return m_Environments.Get(h); }
|
||||
|
||||
// 路径缓存查询(面板显示加载状态用;不触发加载)
|
||||
bool IsModelLoaded(const std::string& path) const
|
||||
{
|
||||
return m_Models.Contains(path);
|
||||
}
|
||||
bool IsEnvironmentLoaded(const std::string& path) const
|
||||
{
|
||||
return m_Environments.Contains(path);
|
||||
}
|
||||
|
||||
// ---- 材质资产(.material,YAML → GPU)----
|
||||
// 同步加载(YAML 解析 + 纹理上传,主线程):路径缓存去重,
|
||||
// 失败返回无效句柄。调用点收敛在两处——场景读后预载
|
||||
//(SceneSerializer,游戏形态也走)与编辑器指派/创建覆盖
|
||||
//(Inspector);渲染收集期只查不载(FindMaterialAsset)
|
||||
MaterialAssetRef LoadMaterialAsset(const std::string& path);
|
||||
// 按路径查已加载材质(未加载返回 null,不触发加载)——
|
||||
// 收集期 resolve / 面板展示用
|
||||
MaterialAsset* FindMaterialAsset(const std::string& path) const
|
||||
{
|
||||
return m_Materials.Find(path);
|
||||
}
|
||||
bool IsMaterialLoaded(const std::string& path) const
|
||||
{
|
||||
return m_Materials.Contains(path);
|
||||
}
|
||||
// 编辑器重建路径(纹理槽变化):GPU 全停飞 → 卸载旧资产
|
||||
//(句柄即刻失效,引用方下帧 resolve 自然拿到新实例)→
|
||||
// 重新加载。waitIdle 经 uploader 接缝(本类不感知 Vulkan)
|
||||
MaterialAssetRef ReloadMaterialAsset(const std::string& path);
|
||||
|
||||
// ---- 动画控制器资产(.animctrl,YAML → 状态机数据)----
|
||||
// 模式同 MaterialAsset:加载点收敛两处(场景读后预载 +
|
||||
// 编辑器指派),System 收集期只 Find 不 Load。组件按路径
|
||||
// 引用(controllerPath),句柄仅指派点短暂持有
|
||||
using AnimCtrlRef = AssetRef<Animation::AnimationController>;
|
||||
AnimCtrlRef LoadController(const std::string& path);
|
||||
// 强制重载(.animctrl = 纯文本 YAML,外部编辑器改完后调用;
|
||||
// 失败时缓存已清,Find 落空 → rest 兜底,路径仍可再次重载)
|
||||
AnimCtrlRef ReloadController(const std::string& path)
|
||||
{
|
||||
m_Controllers.UnloadByPath(path);
|
||||
return LoadController(path);
|
||||
}
|
||||
Animation::AnimationController* FindController(const std::string& path) const
|
||||
{
|
||||
return m_Controllers.Find(path);
|
||||
}
|
||||
bool IsControllerLoaded(const std::string& path) const
|
||||
{
|
||||
return m_Controllers.Contains(path);
|
||||
}
|
||||
|
||||
AssetCache<Model>& Models() { return m_Models; }
|
||||
|
||||
// ---- 资产 GUID 注册表(UE AssetRegistry 模式:中央注册表)----
|
||||
// 磁盘路径资产持久映射 GUID→路径(存 assets/asset_registry.yaml),
|
||||
// 场景文件以 GUID 为主键引用资产——重命名/移动资产不断引用,
|
||||
// Path 仅作人工可读提示与解析兜底。procedural:// 伪路径(参数
|
||||
// 确定性生成,无需稳定身份)不参与
|
||||
// 查询/分配(新路径分配 GUID 并立即持久化,崩溃安全)
|
||||
u64 GetOrCreateGUID(const std::string& path);
|
||||
// GUID → 路径;未登记返回空串
|
||||
std::string ResolveGUID(u64 guid) const;
|
||||
|
||||
// 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle
|
||||
// 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。
|
||||
|
||||
// 把 dirty 的 GUID 注册表落盘(Application 退出 / 场景保存时调)。
|
||||
// 内部幂等:非 dirty 直接 return。GetOrCreateGUID 只标 dirty 不落盘,
|
||||
// 避免每次新增 GUID 都全量重写 yaml(开发期编辑器卡顿主因之一)
|
||||
void FlushGUIDRegistry();
|
||||
|
||||
private:
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
DescriptorManager& m_Descriptors;
|
||||
vk::DescriptorSetLayout m_TextureLayout;
|
||||
void LoadGUIDRegistry();
|
||||
void SaveGUIDRegistry() const;
|
||||
|
||||
IAssetUploader& m_Uploader;
|
||||
|
||||
AssetCache<Model> m_Models;
|
||||
};
|
||||
AssetCache<Cubemap> m_Environments;
|
||||
AssetCache<MaterialAsset> m_Materials;
|
||||
AssetCache<Animation::AnimationController> m_Controllers;
|
||||
|
||||
// GUID 注册表(双向);GetOrCreateGUID 标 dirty,FlushGUIDRegistry 落盘
|
||||
// (Application 退出 / 场景保存时统一 flush,避免每次新增都全量重写 yaml)
|
||||
std::unordered_map<u64, std::string> m_GuidToPath;
|
||||
std::unordered_map<std::string, u64> m_PathToGuid;
|
||||
|
||||
bool m_GuidRegistryDirty = false;
|
||||
|
||||
};
|
||||
} // namespace Assets
|
||||
} // namespace Engine
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// DDS 文件解析(最小实现,不解码压缩块)。
|
||||
//
|
||||
// 设计:GPU 原生支持 BC 压缩格式——DDS 的压缩数据**直接上传**为
|
||||
// BC1/BC2/BC3/BC4/BC5/BC6H/BC7 纹理由硬件采样解码,不做 CPU 解码。
|
||||
// 输出中性 DdsFormat(本层禁 Vulkan 头),vk::Format 映射与 sRGB
|
||||
// 语义由调用方(Texture,渲染侧)决定。
|
||||
//
|
||||
// 支持:
|
||||
// - legacy fourcc:DXT1/DXT3/DXT5/ATI1/BC4U/ATI2/BC5U
|
||||
// - DX10 扩展头:BC1..BC7、R8G8B8A8、B8G8R8A8
|
||||
// - 未压缩 32bpp(DDPF_RGB | DDPF_ALPHAPIXELS)
|
||||
// - 2D + mip 链(cubemap 面标志暂不支持,报错)
|
||||
//
|
||||
// 输出:Mips[r] = 第 r 级完整数据(BC 按块行紧凑排布:
|
||||
// bytes = ceil(w/4)*ceil(h/4)*blockBytes)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_DDSLOADER_H
|
||||
#define HELLOVULKAN_DDSLOADER_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Assets::Dds {
|
||||
|
||||
// 中性格式(Assets 层不感知 Vulkan;映射在渲染侧 Texture)
|
||||
enum class DdsFormat {
|
||||
BC1, BC2, BC3, BC4, BC5, BC6H, BC7,
|
||||
RGBA8,
|
||||
BGRA8, // 按字节序上传后视作 RGBA(近似,触发警告)
|
||||
};
|
||||
|
||||
struct DdsData {
|
||||
u32 Width = 0;
|
||||
u32 Height = 0;
|
||||
u32 MipLevels = 1;
|
||||
DdsFormat Format = DdsFormat::RGBA8;
|
||||
std::vector<std::vector<u8>> Mips; // 每级完整数据(紧凑排布)
|
||||
bool BlockCompressed = false; // BC 格式(rowPitch 按块计算)
|
||||
};
|
||||
|
||||
// 解析 DDS 文件;失败返回 false 并写 errMsg
|
||||
bool Load(const std::string& path, DdsData& out, std::string& errMsg);
|
||||
|
||||
// 指定 mip 级的单行字节跨度(BC = 块行;RGBA8 = w*4)
|
||||
u32 RowPitch(const DdsData& data, u32 mipLevel);
|
||||
|
||||
} // namespace Engine::Assets::Dds
|
||||
|
||||
#endif //HELLOVULKAN_DDSLOADER_H
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// IAssetUploader:资产层 → GPU 的上传接口。
|
||||
//
|
||||
// 资产层(AssetManager/AssetCache)只认这个接口,不感知 Vulkan 具体类
|
||||
// (Device/Allocator/DescriptorManager)。实现由渲染层提供
|
||||
// (VulkanAssetUploader),组合根(Renderer)装配后注入。
|
||||
//
|
||||
// 对齐 UE:资产系统(AssetManager/StreamableManager)与渲染资源创建
|
||||
// (rendering thread / RHI)分离,资产层只拿到"可用的渲染资源"。
|
||||
//
|
||||
// 后续演进点:接口加返回 future / 回调即为异步加载
|
||||
// (IO 线程解析 → 上传队列提交),本接口已是正确的接缝。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_IASSETUPLOADER_H
|
||||
#define HELLOVULKAN_IASSETUPLOADER_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/MaterialAsset.h>
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
|
||||
namespace Engine::Assets {
|
||||
|
||||
class IAssetUploader {
|
||||
public:
|
||||
virtual ~IAssetUploader() = default;
|
||||
|
||||
// 磁盘模型 → 解析 + GPU 上传(失败返回 nullptr)
|
||||
virtual Scope<Model> LoadModel(const std::string& path) = 0;
|
||||
|
||||
// 异步路径:解析已在后台线程完成,此调用只做 GPU 构建(主线程)
|
||||
virtual Scope<Model> LoadModel(const std::string& path,
|
||||
ParsedModel&& parsed) = 0;
|
||||
|
||||
// 程序化纯色平面(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;
|
||||
|
||||
// 程序化纯色立方体(halfSize 为半边长;占位几何/碎片模拟)
|
||||
virtual Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) = 0;
|
||||
|
||||
// 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/
|
||||
// cone/torus 等):形状 + 参数 → 单 mesh 模型。非法形状返回 nullptr
|
||||
virtual Scope<Model> CreatePrimitive(const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b) = 0;
|
||||
|
||||
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
|
||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
|
||||
|
||||
// 异步加载路径:解码已在后台线程完成,此调用只做 GPU 生成(主线程)
|
||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||
DecodedEquirect&& decoded) = 0;
|
||||
|
||||
// ---- 材质资产 GPU 构建(MaterialAsset 专用接缝)----
|
||||
// 数据段 → 纹理上传 + Material 描述符(失败/空槽折共享默认纹理,
|
||||
// 纹理所有权归返回的 MaterialAsset)。失败返回 nullptr
|
||||
virtual Scope<MaterialAsset> CreateMaterial(
|
||||
MaterialAssetData&& data) = 0;
|
||||
|
||||
// GPU 全停飞(编辑器低频重建路径用:材质资产 Reload 前调用,
|
||||
// 保证旧纹理/描述符无在飞引用后销毁——与 Renderer::SetEnvironment
|
||||
// 的 waitIdle 换绑同一先例)
|
||||
virtual void WaitDeviceIdle() = 0;
|
||||
};
|
||||
|
||||
} // namespace Engine::Assets
|
||||
|
||||
#endif //HELLOVULKAN_IASSETUPLOADER_H
|
||||
@@ -9,13 +9,14 @@
|
||||
// 和事件钩子(OnEvent + 类型化事件)定义程序行为;
|
||||
// runtime、editor 等不同程序形态只是不同的派生类。
|
||||
//
|
||||
// 主循环固定为:PollEvents → OnUpdate(dt) → [NewFrame → OnRenderOverlay →
|
||||
// 主循环固定为:PollEvents → OnUpdate(FrameTime) → [NewFrame → OnRenderOverlay →
|
||||
// BuildDrawData] → 主渲染(Renderer.Render)→ 判退出。
|
||||
// UI 构建与 GPU 提交分离:OnRenderOverlay 只"声明"控件,真正的 draw data
|
||||
// 提交发生在主渲染管线中的 ImGui pass(Forward 之后、Present 之前,
|
||||
// loadOp=Load 叠加)。主渲染是框架行为,派生类不可覆盖也不会丢失;
|
||||
// 框架自身永远不绘制任何 ImGui 控件——UI 内容全部由派生类定义。
|
||||
// dt 由框架统一计时(含首帧与卡顿钳制),派生层无需自己管理。
|
||||
// 时间由框架单源采集:帧首 Time::BeginFrame 落定 FrameTime(含卡顿钳制),
|
||||
// 派生层经 OnUpdate(frame) 或 Time 门面只读,不再自己碰 chrono。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_APPLICATION_H
|
||||
@@ -23,21 +24,27 @@
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Core/Event.h>
|
||||
#include <Engine/Core/GameFlow.h>
|
||||
#include <Engine/Core/Time.h>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
#include "Engine/ImGui/ImGuiLayer.h"
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Window;
|
||||
class Renderer;
|
||||
class ImGuiLayer;
|
||||
class PhysicsWorld;
|
||||
namespace Scripting { class ScriptRuntime; }
|
||||
|
||||
namespace ECS { class Scene; }
|
||||
namespace Assets { class AssetManager; }
|
||||
|
||||
class Application {
|
||||
public:
|
||||
Application(std::string title, u32 width, u32 height);
|
||||
Application(const std::string& title, u32 width, u32 height);
|
||||
virtual ~Application();
|
||||
|
||||
Application(const Application&) = delete;
|
||||
@@ -54,26 +61,42 @@ namespace Engine {
|
||||
// 主渲染(ImGui pass 把 draw data 叠加到场景上)。
|
||||
void EnableImGui();
|
||||
|
||||
// ---- 帧率控制 ----
|
||||
// vsync 开启时 present 自带节奏,手动限帧不生效;
|
||||
// 限帧目标最低 24fps(防止过低刷新导致交互卡死),无上限
|
||||
void SetVSync(bool vsync);
|
||||
bool GetVSync() const { return m_VSync; }
|
||||
void SetTargetFps(f32 fps) { m_TargetFps = std::max(24.0f, fps); }
|
||||
f32 GetTargetFps() const { return m_TargetFps; }
|
||||
|
||||
// ---- 供派生类访问的模块 ----
|
||||
Window& GetWindow() const { return *m_Window; }
|
||||
Renderer& GetRenderer() const { return *m_Renderer; }
|
||||
ECS::Scene& GetScene() const { return *m_Scene; }
|
||||
Assets::AssetManager& GetAssets() const { return *m_Assets; }
|
||||
f32 GetDeltaTime() const { return m_DeltaTime; }
|
||||
PhysicsWorld& GetPhysics() const { return *m_Physics; }
|
||||
Scripting::ScriptRuntime& GetScripts() const { return *m_Scripts; }
|
||||
GameFlow& GetGameFlow() { return m_GameFlow; }
|
||||
u64 GetFrameCount() const { return m_FrameCount; }
|
||||
|
||||
// ImGui相关
|
||||
void SetImGuiEnabled(const bool enable) { if (m_ImGuiLayer) m_ImGuiLayer->SetEnabled(enable); }
|
||||
// 理论上ImGuiLayer不应该会空,暂时静默处理
|
||||
bool GetImGuiEnabled() const { return m_ImGuiLayer ? m_ImGuiLayer->IsEnabled() : false; }
|
||||
|
||||
protected:
|
||||
// ---- 生命周期钩子 ----
|
||||
|
||||
// 框架装配完成(Window/Renderer 可用),派生类做初始化(建场景等)
|
||||
virtual void OnInit() {}
|
||||
|
||||
// 每帧逻辑更新(输入、动画、物理……)
|
||||
virtual void OnUpdate(f32 deltaTime) {}
|
||||
// 每帧逻辑更新(输入、动画、物理……)。frame 当帧只读:
|
||||
// Dt() = scaled(游戏逻辑),UnscaledDt() = 真实(编辑器相机/UI)
|
||||
virtual void OnUpdate(const FrameTime& frame) {}
|
||||
|
||||
// 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。
|
||||
// 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。
|
||||
virtual void OnRenderOverlay() {}
|
||||
virtual void OnImGuiRender() {}
|
||||
|
||||
// 主循环结束后、析构前(保存、清理场景等)
|
||||
virtual void OnShutdown() {}
|
||||
@@ -100,14 +123,22 @@ namespace Engine {
|
||||
Scope<ECS::Scene> m_Scene;
|
||||
Scope<Renderer> m_Renderer;
|
||||
Scope<Assets::AssetManager> m_Assets;
|
||||
Scope<PhysicsWorld> m_Physics; // Jolt 物理(Run 起循环时创建)
|
||||
Scope<Scripting::ScriptRuntime> m_Scripts; // 脚本生命周期(Run 起循环时创建)
|
||||
// 注意成员序:ImGuiLayer 必须后声明(先于 Renderer/Window 析构,
|
||||
// 其后端销毁需要 Device 与 GLFWwindow 存活)
|
||||
Scope<ImGuiLayer> m_ImGuiLayer;
|
||||
|
||||
// 游戏流状态机(构造即冻结:Stopped → timeScale=0)
|
||||
GameFlow m_GameFlow;
|
||||
|
||||
bool m_Running = false;
|
||||
f32 m_DeltaTime = 0.0f;
|
||||
u64 m_FrameCount = 0;
|
||||
|
||||
// 帧率控制
|
||||
bool m_VSync = true; // 垂直同步(present 自带节奏)
|
||||
f32 m_TargetFps = 60.0f; // 限帧目标(≥24)
|
||||
|
||||
std::chrono::steady_clock::time_point m_LastFrameTime;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// AssetRef:中性资产引用 = (池内索引, 生成号),与 Entity/RG 句柄同一哲学。
|
||||
//
|
||||
// 放在 Core 层(不依赖 Assets/Vulkan):ECS 组件、序列化数据只存这个
|
||||
// 纯值类型。对齐 UE/Unity 的做法——组件持有"资产引用",但不感知资产
|
||||
// 系统内部:Unity 的 InstanceID、UE 的 FSoftObjectPath 本质都是中性引用
|
||||
// 值,解析归资产系统(AssetManager)。
|
||||
// 悬空引用(generation 不匹配)由 AssetCache::Get 显式拒绝。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASSETREF_H
|
||||
#define HELLOVULKAN_ASSETREF_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
// 资产类型前向声明(必须放全局层:namespace 内前向声明会创建
|
||||
// Engine::Assets::Engine::Model 这类错误嵌套命名空间)
|
||||
namespace Engine {
|
||||
class Model;
|
||||
class Cubemap;
|
||||
class Texture;
|
||||
class Material;
|
||||
class MaterialAsset;
|
||||
}
|
||||
|
||||
namespace Engine {
|
||||
|
||||
inline constexpr u32 kInvalidAssetIndex = 0xFFFFFFFF;
|
||||
|
||||
template<typename T>
|
||||
struct AssetRef {
|
||||
u32 index = kInvalidAssetIndex;
|
||||
u32 generation = 0;
|
||||
|
||||
bool Valid() const { return index != kInvalidAssetIndex; }
|
||||
explicit operator bool() const { return Valid(); }
|
||||
bool operator==(const AssetRef& o) const = default;
|
||||
};
|
||||
|
||||
// 常用别名(新增资产类型时在这里加一行)
|
||||
using ModelRef = AssetRef<Model>;
|
||||
using EnvRef = AssetRef<Cubemap>; // 环境(IBL 四件套)
|
||||
using MaterialAssetRef = AssetRef<MaterialAsset>; // 材质资产(.material)
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_ASSETREF_H
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/24.
|
||||
//
|
||||
// AsyncTask:单 in-flight 异步任务的通用收割工具(header-only)。
|
||||
//
|
||||
// 异步加载的公共骨架(已验证两次:环境 DecodeHdr / 模型 ParseFromDisk):
|
||||
// worker 线程跑纯 CPU 任务 → 原子标志通知 → 主线程每帧轮询收割。
|
||||
// Vulkan 提交/描述符等 GPU 工作全部留在主线程(设备非线程安全)。
|
||||
//
|
||||
// AsyncTask<ParsedModel> task;
|
||||
// task.Run([] { return Model::ParseFromDisk(path); }); // 发起
|
||||
// if (task.Done()) { auto p = task.Take(); ... } // 每帧收割
|
||||
// // 析构自动 join(worker 只做纯 CPU 工作,join 恒安全)
|
||||
//
|
||||
// 语义:Run 在忙碌时先 join 并丢弃旧结果(覆盖式);"快速连切只保留
|
||||
// 最终意图"的 pending 接力由调用方持有(参照 main.cpp 环境加载)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASYNCTASK_H
|
||||
#define HELLOVULKAN_ASYNCTASK_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
template <typename T>
|
||||
class AsyncTask {
|
||||
public:
|
||||
AsyncTask() = default;
|
||||
~AsyncTask() { Join(); }
|
||||
|
||||
AsyncTask(const AsyncTask&) = delete;
|
||||
AsyncTask& operator=(const AsyncTask&) = delete;
|
||||
|
||||
// 发起任务。忙碌时先等待旧任务完成并放弃其结果。
|
||||
void Run(std::function<T()> job)
|
||||
{
|
||||
Join();
|
||||
m_Busy = true;
|
||||
m_Done.store(false);
|
||||
m_Worker = std::thread([this, j = std::move(job)]() {
|
||||
m_Result = j();
|
||||
m_Done.store(true); // release:结果对主线程 acquire 可见
|
||||
});
|
||||
}
|
||||
|
||||
bool Busy() const { return m_Busy; }
|
||||
bool Done() const { return m_Busy && m_Done.load(); }
|
||||
|
||||
// 收割:join worker 并移交结果。只在 Done() 为 true 时调用。
|
||||
T Take()
|
||||
{
|
||||
m_Worker.join(); // join = 同步点,此后结果归主线程
|
||||
m_Busy = false;
|
||||
T out = std::move(m_Result);
|
||||
m_Result = T{};
|
||||
return out;
|
||||
}
|
||||
|
||||
// 关闭前兜底等待(析构自动调用)
|
||||
void Join()
|
||||
{
|
||||
if (m_Worker.joinable()) {
|
||||
m_Worker.join();
|
||||
}
|
||||
m_Busy = false;
|
||||
}
|
||||
|
||||
private:
|
||||
std::thread m_Worker;
|
||||
std::atomic<bool> m_Done{false};
|
||||
T m_Result{};
|
||||
bool m_Busy = false;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_ASYNCTASK_H
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// GameFlow.h:游戏流状态机(引擎层唯一的"游戏在干什么"权威)。
|
||||
//
|
||||
// 状态:Stopped(编辑态/未启动)→ Loading(加载中)→ Running ⇄ Paused,
|
||||
// 任意活动态可 RequestStop 回 Stopped。转换矩阵外的请求拒绝 + warn。
|
||||
//
|
||||
// 职责边界:
|
||||
// - timeScale 单一权威:每次转换统一设 Time::SetTimeScale
|
||||
// (Running/Loading→1,Paused/Stopped→0)。上层(编辑器/游戏)
|
||||
// 不再自行碰 timeScale——冻结语义由状态推导,不再散落
|
||||
// - 状态转换广播:AddListener 注册监听(组合根/编辑器 Init 期注册,
|
||||
// 非每帧路径)。编辑器在 →Loading 挂场景快照、→Stopped 挂恢复;
|
||||
// 脚本/系统将来同样可订阅(跨系统解耦)
|
||||
// - 不知场景不知资产:加载本身(读文件/反序列化)是调用方的事,
|
||||
// 本类只管状态与转换纪律(CompleteLoading 由加载方显式调)
|
||||
//
|
||||
// 与主循环的契约(Application::Run):
|
||||
// - Input::Poll 仅 Running 激活(Paused/Stopped 动作静默)
|
||||
// - 物理步进仅 Loading 跳过(编辑态对账照跑,停摆靠 dt=0)
|
||||
// - 脚本仅 Running 执行(Unity Play Mode / UE PIE 语义——脚本副
|
||||
// 作用不依赖 dt,编辑态不得运行);→Stopped 由监听 EndPlay 终
|
||||
// 局脚本实例(组合根注册,先于编辑器回滚监听)
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_GAMEFLOW_H
|
||||
#define HELLOVULKAN_GAMEFLOW_H
|
||||
|
||||
#include "Engine/Core/Types.h"
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
enum class GameFlowState : u8 {
|
||||
Stopped, // 编辑态 / 未启动(timeScale=0)
|
||||
Loading, // 加载中(物理/脚本步进跳过;timeScale=1 无消费方)
|
||||
Running, // 游戏运行(timeScale=1)
|
||||
Paused, // 暂停(timeScale=0;编辑器 Pause / 游戏菜单挂起)
|
||||
};
|
||||
|
||||
class GameFlow {
|
||||
public:
|
||||
// (from, to);注册期 = 组合根/编辑器 Init,非每帧路径
|
||||
using Listener = std::function<void(GameFlowState, GameFlowState)>;
|
||||
|
||||
GameFlow(); // 构造即冻结(Stopped 语义:timeScale=0)
|
||||
|
||||
void AddListener(Listener l) { m_Listeners.push_back(std::move(l)); }
|
||||
|
||||
GameFlowState State() const { return m_State; }
|
||||
bool IsIn(GameFlowState s) const { return m_State == s; }
|
||||
|
||||
// ---- 转换请求(矩阵外拒绝 + warn,返回是否生效)----
|
||||
bool RequestStart(); // Stopped → Loading
|
||||
void CompleteLoading(); // Loading → Running(加载方显式调)
|
||||
bool RequestPause(); // Running → Paused
|
||||
bool RequestResume(); // Paused → Running
|
||||
bool RequestStop(); // Loading/Running/Paused → Stopped
|
||||
|
||||
private:
|
||||
// 立即生效转换:设 timeScale + 广播(同既有 DoPlay 同步语义)
|
||||
void Transition(GameFlowState to);
|
||||
|
||||
GameFlowState m_State = GameFlowState::Stopped;
|
||||
std::vector<Listener> m_Listeners;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_GAMEFLOW_H
|
||||
@@ -0,0 +1,140 @@
|
||||
//
|
||||
// Input.h:统一输入门面(脚本/游戏逻辑唯一入口)。
|
||||
//
|
||||
// 目的:
|
||||
// 1. 上层代码(脚本、SandboxApp、编辑器相机)不再直接依赖 GLFW——
|
||||
// 只依赖本枚举与门面,Window 后端将来可换(SDL/平台原生)
|
||||
// 2. 嵌入脚本时暴露的也是这一套(Lua/Python 绑定包一层即可)
|
||||
//
|
||||
// 键码值 = GLFW 物理键码(glf3.h 标准值),强转 int 可直接传给
|
||||
// Window::IsKeyPressed;但调用方不得依赖这一点。
|
||||
//
|
||||
// 语义:轮询接口沿用 Window 的门控规则——UI 捕获键盘(编辑器文本框
|
||||
// 输入中)时 IsKeyDown 返回 false,游戏侧静默;鼠标捕获同理。
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "Engine/Core/Types.h"
|
||||
#include "Engine/Input/InputAction.h"
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Window;
|
||||
|
||||
namespace Input {
|
||||
|
||||
enum class Key : int {
|
||||
// ---- 可打印区(值 = ASCII)----
|
||||
Space = 32,
|
||||
Apostrophe = 39,
|
||||
Comma = 44,
|
||||
Minus = 45,
|
||||
Period = 46,
|
||||
Slash = 47,
|
||||
Num0 = 48, Num1 = 49, Num2 = 50, Num3 = 51, Num4 = 52,
|
||||
Num5 = 53, Num6 = 54, Num7 = 55, Num8 = 56, Num9 = 57,
|
||||
Semicolon = 59,
|
||||
Equal = 61,
|
||||
A = 65, B = 66, C = 67, D = 68, E = 69, F = 70, G = 71,
|
||||
H = 72, I = 73, J = 74, K = 75, L = 76, M = 77, N = 78,
|
||||
O = 79, P = 80, Q = 81, R = 82, S = 83, T = 84, U = 85,
|
||||
V = 86, W = 87, X = 88, Y = 89, Z = 90,
|
||||
BracketLeft = 91,
|
||||
Backslash = 92,
|
||||
BracketRight = 93,
|
||||
GraveAccent = 96,
|
||||
// ---- 控制键(值 = GLFW 键码)----
|
||||
Escape = 256,
|
||||
Enter = 257,
|
||||
Tab = 258,
|
||||
Backspace = 259,
|
||||
Insert = 260,
|
||||
Delete = 261,
|
||||
Right = 262,
|
||||
Left = 263,
|
||||
Down = 264,
|
||||
Up = 265,
|
||||
PageUp = 266,
|
||||
PageDown = 267,
|
||||
Home = 268,
|
||||
End = 269,
|
||||
F1 = 290, F2 = 291, F3 = 292, F4 = 293, F5 = 294, F6 = 295,
|
||||
F7 = 296, F8 = 297, F9 = 298, F10 = 299, F11 = 300,
|
||||
F12 = 301,
|
||||
// ---- 修饰键 ----
|
||||
LeftShift = 340,
|
||||
LeftControl = 341,
|
||||
LeftAlt = 342,
|
||||
LeftSuper = 343,
|
||||
RightShift = 344,
|
||||
RightControl = 345,
|
||||
RightAlt = 346,
|
||||
RightSuper = 347,
|
||||
};
|
||||
|
||||
enum class MouseButton : int {
|
||||
Left = 0, // GLFW_MOUSE_BUTTON_1
|
||||
Right = 1,
|
||||
Middle = 2,
|
||||
};
|
||||
|
||||
// 装配:Application 构造时自动调用(后端 Window 就绪后)。
|
||||
// 非 const:门面含状态设置(光标捕获),不只是查询
|
||||
void Init(Window& window);
|
||||
|
||||
// 轮询查询(UI 捕获对应输入时返回 false——游戏侧静默)
|
||||
bool IsKeyDown(Key key);
|
||||
bool IsMouseButtonDown(MouseButton button);
|
||||
|
||||
// ---- 光标捕获(自由视角三件套:隐藏光标 + 无界鼠标增量 +
|
||||
// 滚轮增量)。语义沿 Window 后端:捕获时光标隐藏、位置锁定
|
||||
// 窗口中心,GetMouseDelta 出无界增量;重复设置同值无操作 ----
|
||||
|
||||
// 隐藏并锁定光标(自由视角/环绕中);false 恢复正常光标
|
||||
void SetCursorCaptured(bool captured);
|
||||
bool IsCursorCaptured();
|
||||
|
||||
// 帧间光标增量(无界,捕获态下才有意义;窗口尺寸/模式变化
|
||||
// 由后端参考点重置防跳变)
|
||||
void GetMouseDelta(f32& dx, f32& dy);
|
||||
|
||||
// 滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标
|
||||
//(面板悬停)时不累积——滚轮归 ImGui
|
||||
f32 ConsumeScrollDelta();
|
||||
|
||||
// ---- 输入动作层(具名动作,键鼠+手柄混绑;InputMap 藏在 cpp)----
|
||||
// 采样:主循环单点调用 Poll(物理/脚本/OnUpdate 之前),整帧
|
||||
// 一致。active=false(GameFlow 非 Running)时全部动作静默清零。
|
||||
// 默认绑定表在 Init 时内建(Move=Axis2D、Jump=Button)
|
||||
|
||||
// 装配期(编辑器/游戏配置阶段调用;运行中 Define 也可,索引重建)
|
||||
void DefineAction(const std::string& name, ActionKind kind);
|
||||
// 追加绑定:绑定到具名动作(动作不存在则忽略)
|
||||
void BindAction(const std::string& name, const ActionBinding& b);
|
||||
// 就地重绑定(bindingIdx = 该动作绑定表下标;下帧生效)
|
||||
bool RebindAction(const std::string& name, size_t bindingIdx,
|
||||
const ActionBinding& b);
|
||||
|
||||
// 持久化(yaml;Load 文件不存在静默失败保持现状表)
|
||||
bool SaveActionMap(const std::string& path);
|
||||
bool LoadActionMap(const std::string& path);
|
||||
|
||||
// 每帧采样(Application::Run 专用单点;上层勿调)
|
||||
void Poll(bool active);
|
||||
|
||||
// 查询(按动作名;未定义动作返回 false/0)。Button 用
|
||||
// Pressed/Down/Released 边沿语义,Axis 用 Value/Value2
|
||||
bool ActionPressed(const std::string& name);
|
||||
bool ActionDown(const std::string& name);
|
||||
bool ActionReleased(const std::string& name);
|
||||
f32 ActionValue(const std::string& name);
|
||||
glm::vec2 ActionValue2(const std::string& name);
|
||||
|
||||
// 手柄连接态(Poll 时更新)
|
||||
bool IsGamepadConnected();
|
||||
}
|
||||
|
||||
} // namespace Engine
|
||||
@@ -11,6 +11,12 @@
|
||||
|
||||
namespace Engine {
|
||||
|
||||
namespace Logger
|
||||
{
|
||||
void Initialize();
|
||||
void Shutdown();
|
||||
}
|
||||
|
||||
#define ENGINE_TRACE(...) ::spdlog::trace(__VA_ARGS__)
|
||||
#define ENGINE_INFO(...) ::spdlog::info(__VA_ARGS__)
|
||||
#define ENGINE_DEBUG(...) ::spdlog::debug(__VA_ARGS__)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// LogRing.h:进程级日志环形缓冲(编辑器 Log Panel 的数据源)。
|
||||
//
|
||||
// RingSink 注册进默认 logger 的 sinks 后,所有日志(引擎 + 脚本经
|
||||
// ScriptLog/ENGINE_* 打印的)自动落缓冲;面板按 version 增量感知、
|
||||
// Snapshot 整体拉取。头文件设施(sink_it_ 内联),无 cpp。
|
||||
//
|
||||
#ifndef HELLOVULKAN_LOGRING_H
|
||||
#define HELLOVULKAN_LOGRING_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <spdlog/sinks/base_sink.h>
|
||||
#include <spdlog/details/log_msg.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Debug {
|
||||
|
||||
struct LogEntry {
|
||||
spdlog::level::level_enum level;
|
||||
std::string msg; // 整行(已按 ring pattern 格式化,无色码)
|
||||
};
|
||||
|
||||
class LogRingBuffer {
|
||||
public:
|
||||
static constexpr size_t kCap = 2048;
|
||||
|
||||
// 门控:启动期系统初始化日志(Vulkan/UBO/IBL...)不进面板,
|
||||
// 与 Unity Console 语义一致(只收运行期游戏侧日志)。
|
||||
// Application::Run 进入主循环时开闸
|
||||
bool IsEnabled() const {
|
||||
return m_Enabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
void SetEnabled(bool v) {
|
||||
m_Enabled.store(v, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Push(spdlog::level::level_enum lvl, std::string msg) {
|
||||
std::lock_guard<std::mutex> lk(m_Mutex);
|
||||
if (m_Entries.size() < kCap) {
|
||||
m_Entries.push_back({lvl, std::move(msg)});
|
||||
} else {
|
||||
m_Entries[m_Head] = {lvl, std::move(msg)};
|
||||
m_Head = (m_Head + 1) % kCap; // 满后覆写最旧
|
||||
}
|
||||
++m_Version;
|
||||
}
|
||||
|
||||
// 快照(满环时按时间序重排)
|
||||
std::vector<LogEntry> Snapshot() const {
|
||||
std::lock_guard<std::mutex> lk(m_Mutex);
|
||||
if (m_Entries.size() < kCap) {
|
||||
return m_Entries;
|
||||
}
|
||||
std::vector<LogEntry> out;
|
||||
out.reserve(kCap);
|
||||
out.insert(out.end(), m_Entries.begin() + static_cast<long>(m_Head),
|
||||
m_Entries.end());
|
||||
out.insert(out.end(), m_Entries.begin(),
|
||||
m_Entries.begin() + static_cast<long>(m_Head));
|
||||
return out;
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
std::lock_guard<std::mutex> lk(m_Mutex);
|
||||
m_Entries.clear();
|
||||
m_Head = 0;
|
||||
++m_Version;
|
||||
}
|
||||
|
||||
u64 Version() const {
|
||||
return m_Version.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex m_Mutex;
|
||||
std::vector<LogEntry> m_Entries;
|
||||
size_t m_Head = 0;
|
||||
std::atomic<u64> m_Version{0};
|
||||
std::atomic<bool> m_Enabled{false};
|
||||
};
|
||||
|
||||
inline LogRingBuffer& LogRing() {
|
||||
static LogRingBuffer inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
// spdlog sink 适配:log() 整行入环。pattern 由装配端设置(无色码)
|
||||
class RingSink : public spdlog::sinks::base_sink<std::mutex> {
|
||||
protected:
|
||||
void sink_it_(const spdlog::details::log_msg& msg) override {
|
||||
if (!LogRing().IsEnabled()) {
|
||||
return; // 启动期:只落终端/文件,不进面板
|
||||
}
|
||||
spdlog::memory_buf_t formatted;
|
||||
base_sink<std::mutex>::formatter_->format(msg, formatted);
|
||||
std::string line(formatted.data(), formatted.size());
|
||||
while (!line.empty() &&
|
||||
(line.back() == '\n' || line.back() == '\r')) {
|
||||
line.pop_back();
|
||||
}
|
||||
LogRing().Push(msg.level, std::move(line));
|
||||
}
|
||||
void flush_() override {}
|
||||
};
|
||||
|
||||
} // namespace Engine::Debug
|
||||
|
||||
#endif //HELLOVULKAN_LOGRING_H
|
||||
@@ -37,29 +37,27 @@ namespace Engine
|
||||
void AddAllocation(u64 bytes);
|
||||
void AddDeallocation(u64 bytes);
|
||||
|
||||
// ---- 采样进程真实内存 ----
|
||||
void SampleProcessMemory();
|
||||
|
||||
// ---- 打印 ----
|
||||
void Print() const;
|
||||
void PrintDetailed() const;
|
||||
void PrintComparison() const;
|
||||
// ---- 只读快照(面板/遥测消费)----
|
||||
struct Snapshot
|
||||
{
|
||||
u64 textureCount = 0;
|
||||
u64 textureBytes = 0;
|
||||
u64 meshCount = 0;
|
||||
u64 meshBytes = 0;
|
||||
u64 bufferCount = 0;
|
||||
u64 bufferBytes = 0;
|
||||
u64 memoryBlockBytes = 0; // MemoryAllocator 已分配块
|
||||
u64 memoryUsedBytes = 0; // 块内实际占用
|
||||
u64 frameAllocation = 0; // 本帧新增(BeginFrame 清零)
|
||||
u64 frameDeallocation = 0; // 本帧释放
|
||||
u64 totalAllocation = 0;
|
||||
u64 totalDeallocation = 0;
|
||||
};
|
||||
Snapshot TakeSnapshot() const;
|
||||
|
||||
private:
|
||||
Stats() = default;
|
||||
|
||||
struct ProcessMemory
|
||||
{
|
||||
u64 VmSize = 0; // /proc/self/status VmSize
|
||||
u64 Rss = 0; // /proc/self/status VmRSS
|
||||
u64 Pss = 0; // smaps_rollup
|
||||
u64 SharedClean = 0;
|
||||
u64 PrivateDirty = 0;
|
||||
u64 PrivateClean = 0;
|
||||
};
|
||||
|
||||
ProcessMemory ReadProcessMemory() const;
|
||||
|
||||
mutable std::mutex m_Mutex;
|
||||
|
||||
// 资源计数
|
||||
@@ -81,9 +79,6 @@ namespace Engine
|
||||
u64 m_FrameDeallocation = 0;
|
||||
u64 m_TotalAllocation = 0;
|
||||
u64 m_TotalDeallocation = 0;
|
||||
|
||||
// 进程快照
|
||||
ProcessMemory m_ProcessMem{};
|
||||
};
|
||||
|
||||
#define STATS Engine::Stats::Get()
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// Time:帧时间系统(单源采集 → 帧契约 → 静态门面)。
|
||||
//
|
||||
// 数据流(镜像 FrameSceneData 的落帧模式):
|
||||
// Application::Run 帧首采集真实 dt(steady_clock 单源,钳制卡顿)→
|
||||
// Time::BeginFrame 落定当帧 FrameTime(业务当帧只读)→ 三轨消费:
|
||||
// 游戏轨 Dt() = unscaledDt × timeScale(OnUpdate/动画/粒子)
|
||||
// 真实轨 UnscaledDt() (编辑器相机/UI/FPS,暂停不受影响)
|
||||
// 物理轨 accumulator (吃 Dt(),scale=0 天然冻结,编辑器对账仍跑)
|
||||
//
|
||||
// 对齐 UE/Unity:
|
||||
// Time::Delta/UnscaledDelta ≈ Unity Time.deltaTime / unscaledDeltaTime
|
||||
// Time::SetTimeScale(0) ≈ Unity timeScale=0 / UE GlobalTimeDilation=0
|
||||
// FrameTime.elapsed ≈ Unity Time.time(scaled 累计)
|
||||
// Time::Realtime() ≈ Unity realtimeSinceStartup
|
||||
// TimerManager(UE SetTimer 语义)留二期,有真实消费场景再上。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_TIME_H
|
||||
#define HELLOVULKAN_TIME_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
// 帧时间契约:Run 帧首落定,当帧只读
|
||||
struct FrameTime {
|
||||
f64 unscaledDt = 0.0; // 真实帧时长(秒,f64 防 long-run 误差)
|
||||
f32 timeScale = 1.0f; // 全局缩放(0 = 暂停)
|
||||
u64 frameCount = 0;
|
||||
f64 elapsed = 0.0; // 游戏累计(scaled)
|
||||
f64 unscaledElapsed = 0.0; // 真实累计
|
||||
|
||||
f32 Dt() const { return static_cast<f32>(unscaledDt) * timeScale; }
|
||||
f32 UnscaledDt() const { return static_cast<f32>(unscaledDt); }
|
||||
f32 Milliseconds() const { return Dt() * 1000.0f; }
|
||||
f32 UnscaledMilliseconds() const { return UnscaledDt() * 1000.0f; }
|
||||
};
|
||||
|
||||
// 静态门面:任意深度代码免穿引用拿 Application(Unity Time 风格)
|
||||
class Time {
|
||||
public:
|
||||
// ---- 消费端 ----
|
||||
static const FrameTime& Current();
|
||||
static f32 Delta(); // scaled dt(当帧)
|
||||
static f32 UnscaledDelta();
|
||||
static f64 Elapsed(); // scaled 累计(秒)
|
||||
static f64 Realtime(); // 进程启动至今(真实秒,性能测量统一入口)
|
||||
static f32 TimeScale();
|
||||
static u64 FrameCount();
|
||||
|
||||
// ---- 控制意图(下一帧生效)----
|
||||
static void SetTimeScale(f32 scale); // 0 = 暂停;负值钳为 0
|
||||
|
||||
// ---- 框架内部:Application::Run 帧首调用 ----
|
||||
static void BeginFrame(f64 unscaledDt, u64 frameCount);
|
||||
|
||||
private:
|
||||
static FrameTime s_Current;
|
||||
static f32 s_PendingScale;
|
||||
static const std::chrono::steady_clock::time_point s_Startup;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_TIME_H
|
||||
@@ -17,6 +17,16 @@ struct GLFWwindow;
|
||||
|
||||
namespace Engine {
|
||||
|
||||
// 手柄状态快照(POD,PollGamepad 每帧填充)。
|
||||
// 值 = GLFW 标准游戏布局:axes 下标即 GLFW_GAMEPAD_AXIS_*(左摇杆 X/Y、
|
||||
// 右摇杆 X/Y、LT、RT),buttons 下标即 GLFW_GAMEPAD_BUTTON_*(A=0…),
|
||||
// 调用方不得依赖具体数值(GLFW 头语义)
|
||||
struct GamepadState {
|
||||
bool connected = false;
|
||||
f32 axes[6] = {};
|
||||
bool buttons[15] = {};
|
||||
};
|
||||
|
||||
class Window {
|
||||
public:
|
||||
Window(const std::string& title, u32 width, u32 height);
|
||||
@@ -33,7 +43,18 @@ namespace Engine {
|
||||
using EventCallbackFn = std::function<void(const Event&)>;
|
||||
void SetEventCallback(EventCallbackFn callback) { m_EventCallback = std::move(callback); }
|
||||
|
||||
vk::SurfaceKHR CreateSurface(vk::Instance instance);
|
||||
// 输入捕获:返回 true = 该类输入被 UI 占用(如 ImGui 窗口交互)。
|
||||
// 生效范围:类型化事件回调(Key/Mouse*)+ 轮询接口
|
||||
//(IsKeyPressed/GetMouseDelta)。窗口类事件(Resize/Close/Focus)不受影响。
|
||||
// 未设置时全部放行(行为与无 UI 时一致)。
|
||||
using CaptureFn = std::function<bool()>;
|
||||
void SetInputCapture(CaptureFn keyboard, CaptureFn mouse)
|
||||
{
|
||||
m_KeyboardCapture = std::move(keyboard);
|
||||
m_MouseCapture = std::move(mouse);
|
||||
}
|
||||
|
||||
vk::SurfaceKHR CreateSurface(vk::Instance instance) const;
|
||||
void DestroySurface(vk::Instance instance, vk::SurfaceKHR surface);
|
||||
|
||||
u32 GetWidth() const { return m_Width; }
|
||||
@@ -52,7 +73,23 @@ namespace Engine {
|
||||
// 输入事件系统
|
||||
|
||||
bool IsKeyPressed(int key) const;
|
||||
void GetMouseDelta(float& dx, float& dy);
|
||||
// UI 捕获鼠标(ImGui 悬停)时返回 false——面板上的按键归 ImGui
|
||||
bool IsMouseButtonPressed(int button) const;
|
||||
void GetMouseDelta(float& dx, float& dy) const;
|
||||
|
||||
// 轮询式滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标
|
||||
//(面板悬停)时不累积——滚轮归 ImGui
|
||||
float ConsumeScrollDelta();
|
||||
|
||||
// 光标捕获(自由视角):DISABLED 隐藏光标出无界增量,NORMAL 恢复。
|
||||
// 重复设置同值无操作;切换后鼠标增量参考点重置(防跳变)
|
||||
void SetCursorCaptured(bool captured);
|
||||
bool IsCursorCaptured() const { return m_CursorCaptured; }
|
||||
|
||||
// 手柄轮询(JOYSTICK_1 标准游戏布局):连接且带标准映射时填充
|
||||
// state 并返回 true;否则置 connected=false 全零并返回 false。
|
||||
// 无分配(GLFW 平台态拉取),每帧调用成本可忽略
|
||||
bool PollGamepad(GamepadState& state) const;
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +102,17 @@ namespace Engine {
|
||||
bool m_FramebufferResized = false;
|
||||
EventCallbackFn m_EventCallback;
|
||||
|
||||
// UI 输入捕获(ImGui 接管时置位)
|
||||
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;
|
||||
mutable float m_ScrollAccum = 0.0f; // 滚轮累积(ConsumeScrollDelta)
|
||||
|
||||
// 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);
|
||||
@@ -72,6 +120,8 @@ namespace Engine {
|
||||
static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos);
|
||||
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset);
|
||||
static void WindowFocusCallback(GLFWwindow* window, int focused);
|
||||
|
||||
static void WindowCloseCallback(GLFWwindow* window);
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// DebugDraw:引擎级通用调试图元服务(Unity Debug.DrawLine / UE
|
||||
// DrawDebugHelpers 的对应物)。
|
||||
//
|
||||
// 分层定位:全局静态 API,任何层(脚本/物理/动画/AI)调用即收集;
|
||||
// Renderer 每帧取走缓冲,PhysicsDebugPass 统一上传叠加渲染。引擎侧
|
||||
// 只写这一次,之后一切新可视化 = 调用方加几行,不再碰引擎代码。
|
||||
//
|
||||
// 语义对齐 Unity/UE:
|
||||
// duration <= 0 → 只画当前帧(调用方每帧重调,Unity DrawLine 同)
|
||||
// duration > 0 → 存活 duration 秒(内部持久列表,期间无需重调;
|
||||
// 固定步长系统用 2×步长 即可跨渲染帧无缝续接)
|
||||
// depthTest=false → 透视遮挡直显(UE DrawDebugLine bPersistent 位)
|
||||
// width → 线宽(需 wideLines 特性,设备不支持时钳回 1.0)
|
||||
//
|
||||
// 线程模型:主线程单写单读(脚本 OnUpdate 收集 → Renderer 上传 →
|
||||
// EndFrame 翻页)。
|
||||
// 预算:kMaxVerts 截断 + 一次告警(调试图元打满是滥用信号)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_DEBUGDRAW_H
|
||||
#define HELLOVULKAN_DEBUGDRAW_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Debug {
|
||||
|
||||
// 顶点(28B):与 PhysicsDebugVertex 同布局——渲染 pass 复用同一
|
||||
// 顶点输入态(pos vec3 + color vec4)
|
||||
struct Vertex {
|
||||
glm::vec3 pos;
|
||||
glm::vec4 color;
|
||||
};
|
||||
|
||||
// 渲染批次:同 (拓扑, 线宽, 深度测试) 的连续顶点段。调用方按
|
||||
// 参数自动分组,draw 侧每批一次状态切换(动态状态零管线对象)
|
||||
struct Batch {
|
||||
bool tris; // true = triangleList / false = lineList
|
||||
f32 width;
|
||||
bool depthTest;
|
||||
u32 first; // 顶点偏移(顶点数,非字节)
|
||||
u32 count;
|
||||
};
|
||||
|
||||
// 单帧收集结果(Renderer 每帧消费,勿手动修改)
|
||||
struct DebugFrame {
|
||||
std::vector<Vertex> verts;
|
||||
std::vector<Batch> batches;
|
||||
|
||||
void Clear() { verts.clear(); batches.clear(); }
|
||||
bool Empty() const { return batches.empty(); }
|
||||
};
|
||||
|
||||
// 顶点预算(每渲染帧):调试线框足够用,溢出截断 + 告警一次
|
||||
inline constexpr u32 kMaxVerts = 1 << 16;
|
||||
|
||||
// ---- 全局开关 ----
|
||||
void SetEnabled(bool on);
|
||||
bool IsEnabled();
|
||||
void Clear(); // 清空瞬时 + 持久图元(PIE 停止/场景切换时调用)
|
||||
|
||||
// ---- 线类(全部世界空间)----
|
||||
void Line(const glm::vec3& a, const glm::vec3& b,
|
||||
const glm::vec4& color,
|
||||
f32 width = 1.0f, f32 duration = 0.0f,
|
||||
bool depthTest = true);
|
||||
|
||||
// Ray:from 出发沿单位方向 dirNorm 画 length 长(Unity DrawRay 同形)
|
||||
void Ray(const glm::vec3& from, const glm::vec3& dirNorm, f32 length,
|
||||
const glm::vec4& color,
|
||||
f32 width = 1.0f, f32 duration = 0.0f, bool depthTest = true);
|
||||
|
||||
// 箭头:主干 + 两翼头(正交基自动构建,零向量方向安全退化成点)
|
||||
void Arrow(const glm::vec3& from, const glm::vec3& to,
|
||||
const glm::vec4& color, f32 headSize = 0.12f,
|
||||
f32 width = 1.0f, f32 duration = 0.0f,
|
||||
bool depthTest = true);
|
||||
|
||||
// 十字标记(Unity Debug.DrawCross 同位)
|
||||
void Cross(const glm::vec3& pos, f32 size, const glm::vec4& color,
|
||||
f32 width = 1.0f, f32 duration = 0.0f,
|
||||
bool depthTest = true);
|
||||
|
||||
// ---- 形状线框 ----
|
||||
|
||||
// 三大圆经线圈(UE DrawDebugSphere 同形)
|
||||
void WireSphere(const glm::vec3& center, f32 radius,
|
||||
const glm::vec4& color, int segments = 16,
|
||||
f32 width = 1.0f, f32 duration = 0.0f,
|
||||
bool depthTest = true);
|
||||
|
||||
// 朝向 normal 的平面圆
|
||||
void WireCircle(const glm::vec3& center, f32 radius,
|
||||
const glm::vec3& normal, const glm::vec4& color,
|
||||
int segments = 24,
|
||||
f32 width = 1.0f, f32 duration = 0.0f,
|
||||
bool depthTest = true);
|
||||
|
||||
// 轴对齐 / 旋转盒(rot 单位四元数 = AABB;UE DrawDebugBox 同形)
|
||||
void WireBox(const glm::vec3& center, const glm::vec3& halfExtents,
|
||||
const glm::vec4& color,
|
||||
const glm::quat& rot = glm::quat(1.0f, 0.0f, 0.0f, 0.0f),
|
||||
f32 width = 1.0f, f32 duration = 0.0f,
|
||||
bool depthTest = true);
|
||||
|
||||
// 胶囊线框:轴向 local +Y(Jolt 惯例),halfHeight = 圆柱段半长
|
||||
//(总高 = 2×(halfHeight+radius),与引擎 CharacterController 参数一致)
|
||||
void WireCapsule(const glm::vec3& center, f32 radius, f32 halfHeight,
|
||||
const glm::vec4& color,
|
||||
const glm::quat& rot = glm::quat(1.0f, 0.0f, 0.0f, 0.0f),
|
||||
f32 width = 1.0f, f32 duration = 0.0f,
|
||||
bool depthTest = true);
|
||||
|
||||
// ---- 便捷色:0xRRGGBB → 线性 HDR vec4(可 >1 过 tonemap 依旧醒目)----
|
||||
constexpr glm::vec4 Rgb(u32 hex, f32 alpha = 1.0f)
|
||||
{
|
||||
constexpr f32 kInv = 1.0f / 255.0f;
|
||||
return {(static_cast<f32>((hex >> 16) & 0xFF)) * kInv,
|
||||
(static_cast<f32>((hex >> 8) & 0xFF)) * kInv,
|
||||
(static_cast<f32>(hex & 0xFF)) * kInv,
|
||||
alpha};
|
||||
}
|
||||
|
||||
// ---- Renderer 消费端(业务代码勿调用)----
|
||||
|
||||
// 取当前帧缓冲(禁用/空 = Empty)。Renderer 上传窗口调用
|
||||
const DebugFrame& Frame();
|
||||
|
||||
// 翻页:清瞬时缓冲 + 清过期持久项。Renderer 上传完成后调用
|
||||
void EndFrame();
|
||||
|
||||
} // namespace Engine::Debug
|
||||
|
||||
#endif //HELLOVULKAN_DEBUGDRAW_H
|
||||
@@ -10,12 +10,20 @@
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Core/UUID.h>
|
||||
#include <Engine/Core/AssetRef.h>
|
||||
#include <Engine/Renderer/Camera.h>
|
||||
#include <Engine/Assets/AssetHandle.h>
|
||||
#include <Engine/Scripting/ScriptParam.h>
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
class Model;
|
||||
@@ -27,6 +35,8 @@ namespace Engine::ECS {
|
||||
// ECS 内部遍历不用它(用 index+generation 句柄,性能)。
|
||||
struct IdComponent {
|
||||
UUID uuid;
|
||||
|
||||
operator UUID() const { return uuid; }
|
||||
};
|
||||
|
||||
// 实体名(调试/查询用;大规模场景建议演进为哈希 id)
|
||||
@@ -34,49 +44,488 @@ namespace Engine::ECS {
|
||||
std::string name;
|
||||
};
|
||||
|
||||
// 世界变换:可编辑的 SRT 分量 + 派生的世界矩阵。
|
||||
// 编辑分量后调用 Compose() 重算 world;渲染热路径只读 world。
|
||||
// 层级(父子变换)留给后续的层级同步系统。
|
||||
// 变换(UE FTransform 语义):TRS 分量 = 相对父的 **local** 值
|
||||
//(无父实体 local 即 world,旧场景零迁移)。分量私有,所有写入口
|
||||
// 标脏;world 缓存(SRT 三分量 + 矩阵)由帧循环的
|
||||
// Scene::UpdateTransforms() 从根 DFS 只对脏子树重算(transform-
|
||||
// update 阶段)。子 world = 父 world SRT 组合而非矩阵乘法——
|
||||
// 非均匀父缩放下组合式仍是干净 SRT(矩阵分解会引入剪切)。
|
||||
// 渲染热路径只读 World();灯光/相机/物理/Gizmo 读 world 三分量。
|
||||
//
|
||||
// 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角):
|
||||
// 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在
|
||||
// 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation
|
||||
// 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。
|
||||
struct TransformComponent {
|
||||
glm::vec3 position{0.0f};
|
||||
glm::vec3 rotation{0.0f}; // 欧拉角(度),应用顺序 Y→X→Z
|
||||
glm::vec3 scale{1.0f};
|
||||
glm::mat4 world{1.0f};
|
||||
// ---- local 读取(相对父;UI/游戏逻辑用)----
|
||||
const glm::vec3& GetPosition() const { return m_Position; }
|
||||
const glm::quat& GetRotationQuat() const { return m_Rotation; }
|
||||
const glm::vec3& GetScale() const { return m_Scale; }
|
||||
|
||||
TransformComponent& SetPosition(const glm::vec3& p) { position = p; return *this; }
|
||||
TransformComponent& SetRotation(const glm::vec3& r) { rotation = r; return *this; }
|
||||
TransformComponent& SetScale(const glm::vec3& s) { scale = s; return *this; }
|
||||
// ---- world 缓存读取(要求已由 UpdateTransforms 计算)----
|
||||
const glm::mat4& World() const { return m_World; }
|
||||
const glm::vec3& GetWorldPosition() const { return m_WorldPosition; }
|
||||
const glm::quat& GetWorldRotationQuat() const { return m_WorldRotation; }
|
||||
const glm::vec3& GetWorldScale() const { return m_WorldScale; }
|
||||
|
||||
// 分量 → world 矩阵(T * R * S)
|
||||
// 欧拉角(度)视图:从 quat 派生(仅 UI 显示/编辑用,勿做存储)。
|
||||
// 按引擎 YXZ 序(R = Ry·Rx·Rz)从旋转矩阵反解;glm 列主序索引:
|
||||
// x = asin(-R[2][1])、y = atan2(R[2][0], R[2][2])、z = atan2(R[0][1], R[1][1])。
|
||||
// cx 在 atan2 分子分母中约掉,pitch 贴 ±90° 依然精确
|
||||
glm::vec3 GetRotation() const
|
||||
{
|
||||
const glm::mat3 R = glm::mat3_cast(m_Rotation);
|
||||
const f32 sp = std::clamp(-R[2][1], -1.0f, 1.0f); // = sin(pitch)
|
||||
// 万向锁分支阈值 1e-6:asin 在 |sin|→1 处导数发散(sin90 的
|
||||
// float 表示差 6e-8 → asin 放大成 0.02° 显示噪声,用户实测
|
||||
// 90 → 89.98)。贴锁区改走组合角 atan2(分子分母同源缩放,
|
||||
// 比值稳定)→ 手输 90.0 显示 90.0。落入该窄区(±0.00057°)
|
||||
// 的真实 yaw/roll 被归并为组合角,UI 输入不可达,可接受
|
||||
glm::vec3 euler = std::abs(sp) < 1.0f - 1e-6f
|
||||
? glm::degrees(glm::vec3(
|
||||
std::asin(sp),
|
||||
std::atan2(R[2][0], R[2][2]),
|
||||
std::atan2(R[0][1], R[1][1])))
|
||||
: (sp > 0.0f
|
||||
? glm::vec3(90.0f,
|
||||
glm::degrees(std::atan2(-R[0][2], R[1][2])),
|
||||
0.0f)
|
||||
: glm::vec3(-90.0f,
|
||||
glm::degrees(std::atan2(-R[0][2], -R[1][2])),
|
||||
0.0f));
|
||||
// 显示约定 = UE Details 的 0-360(-90 → 270)。逐轴 fmod wrap
|
||||
// 与 [-180,180] 数学严格等价(每轴旋转周期 360°),矩阵重建类
|
||||
// 消费端(Camera::EulerRotMat 等)不受影响;需要连续域做
|
||||
// clamp/lerp 的调用方自行 wrap 回 [-180,180](如自由视角)
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
euler[i] = std::fmod(euler[i], 360.0f);
|
||||
if (euler[i] < 0.0f) euler[i] += 360.0f;
|
||||
if (euler[i] >= 360.0f) euler[i] = 0.0f; // 浮点边界
|
||||
}
|
||||
return euler;
|
||||
}
|
||||
|
||||
// ---- 写入(标脏,脏实体由 UpdateTransforms 重算)----
|
||||
TransformComponent& SetPosition(const glm::vec3& p) { if (m_Position != p) { m_Position = p; m_Dirty = true; } return *this; }
|
||||
// 欧拉角(度)入口:UI/游戏逻辑用。转 quat 存(q = qy·qx·qz,
|
||||
// 与原 Ry·Rx·Rz 合成严格同构;不能用 glm::quat(euler) ctor——
|
||||
// 那是 ZYX 航向序,语义不同)。输入域无限制:三角函数周期性
|
||||
// 自动归一(720 ≡ -45 ≡ 315),编辑器拖动累计值直接喂
|
||||
TransformComponent& SetRotation(const glm::vec3& r)
|
||||
{
|
||||
return SetRotationQuat(QuatFromEuler(r));
|
||||
}
|
||||
// 欧拉(度)→ quat 的唯一转换点(qy·qx·qz)。static 供编辑器
|
||||
// mirror 的旋转等价检测复用(同一公式构造 → dot 判定严格)
|
||||
static glm::quat QuatFromEuler(const glm::vec3& r)
|
||||
{
|
||||
const glm::quat qx = glm::angleAxis(glm::radians(r.x), glm::vec3(1, 0, 0));
|
||||
const glm::quat qy = glm::angleAxis(glm::radians(r.y), glm::vec3(0, 1, 0));
|
||||
const glm::quat qz = glm::angleAxis(glm::radians(r.z), glm::vec3(0, 0, 1));
|
||||
return qy * qx * qz;
|
||||
}
|
||||
// 四元数直写(UE FTransform 语义):Gizmo 拖拽/物理写回用,
|
||||
// 不经欧拉往返 → 零漂移、无万向锁
|
||||
TransformComponent& SetRotationQuat(const glm::quat& q) { if (m_Rotation != q) { m_Rotation = q; m_Dirty = true; } return *this; }
|
||||
TransformComponent& SetScale(const glm::vec3& s) { if (m_Scale != s) { m_Scale = s; m_Dirty = true; } return *this; }
|
||||
|
||||
bool IsDirty() const { return m_Dirty; }
|
||||
|
||||
// 显式标脏:层级系统改父/挂载后触发 world 重算用
|
||||
//(local 值未变但 world 缓存已失效)
|
||||
TransformComponent& MarkDirty() { m_Dirty = true; return *this; }
|
||||
|
||||
// SRT 分量 → world 矩阵(T * R * S),清除脏标记。
|
||||
// 根实体版(world = local);由 Scene::UpdateTransforms 调用,
|
||||
// 勿手动
|
||||
TransformComponent& Compose()
|
||||
{
|
||||
glm::mat4 t = glm::translate(glm::mat4(1.0f), position);
|
||||
glm::mat4 r =
|
||||
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.y), glm::vec3(0, 1, 0)) *
|
||||
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.x), glm::vec3(1, 0, 0)) *
|
||||
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.z), glm::vec3(0, 0, 1));
|
||||
glm::mat4 s = glm::scale(glm::mat4(1.0f), scale);
|
||||
world = t * r * s;
|
||||
m_WorldPosition = m_Position;
|
||||
m_WorldRotation = m_Rotation;
|
||||
m_WorldScale = m_Scale;
|
||||
RebuildWorldMatrix();
|
||||
m_Dirty = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// 子实体版:world = 父 world SRT 组合(UE FTransform 语义):
|
||||
// wPos = pPos + pRot·(pScale⊙lPos) wRot = pRot·lRot wScl = pScale⊙lScl
|
||||
// 组合式(非矩阵分解)保证非均匀父缩放下结果仍是干净 SRT。
|
||||
// 要求父已 Compose(DFS 父先于子保证)。由 Scene 调用,勿手动
|
||||
TransformComponent& ComposeFromParent(const TransformComponent& parent)
|
||||
{
|
||||
m_WorldPosition = parent.m_WorldPosition +
|
||||
parent.m_WorldRotation * (parent.m_WorldScale * m_Position);
|
||||
m_WorldRotation = parent.m_WorldRotation * m_Rotation;
|
||||
m_WorldScale = parent.m_WorldScale * m_Scale;
|
||||
RebuildWorldMatrix();
|
||||
m_Dirty = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
void RebuildWorldMatrix()
|
||||
{
|
||||
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_WorldPosition);
|
||||
const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_WorldScale);
|
||||
m_World = t * glm::mat4_cast(m_WorldRotation) * s;
|
||||
}
|
||||
|
||||
// ---- local TRS(相对父)----
|
||||
glm::vec3 m_Position{0.0f};
|
||||
glm::quat m_Rotation{1.0f, 0.0f, 0.0f, 0.0f}; // 单位四元数(w,x,y,z)
|
||||
glm::vec3 m_Scale{1.0f};
|
||||
// ---- world 缓存(UpdateTransforms 重算)----
|
||||
glm::vec3 m_WorldPosition{0.0f};
|
||||
glm::quat m_WorldRotation{1.0f, 0.0f, 0.0f, 0.0f};
|
||||
glm::vec3 m_WorldScale{1.0f};
|
||||
glm::mat4 m_World{1.0f};
|
||||
bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world
|
||||
};
|
||||
|
||||
// 层级链接(UE Actor attach):父句柄 + 子表。惰性创建——
|
||||
// 只有参与父子关系的实体才有本组件,独立实体零存储成本。
|
||||
// entt 句柄仅在 registry 存续期有效;序列化换 UUID,加载后重链
|
||||
struct HierarchyComponent {
|
||||
entt::entity parent = entt::null; // entt::null = 根实体
|
||||
std::vector<entt::entity> children; // 挂载顺序
|
||||
};
|
||||
|
||||
// 网格渲染体:引用共享的 Model 资产。
|
||||
// 存句柄不存指针/Ref:所有权归 AssetManager,悬空由句柄 generation 拦截。
|
||||
// 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
|
||||
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
|
||||
//
|
||||
// 材质覆盖(UE Material Slot 语义):key = mesh 下标(Model::GetMeshes
|
||||
// 序),value = .material 资产路径;无该项 = 用模型内建材质(只读)。
|
||||
// 存路径不存句柄:覆盖材质由渲染收集期按路径惰性 resolve(资产
|
||||
// 已被场景预载/编辑器指派,收集只查缓存),组件保持纯数据。
|
||||
// 资产被多实体共享时编辑同时生效;实体独立 = 每实体一份覆盖文件
|
||||
struct MeshRendererComponent {
|
||||
Assets::ModelHandle model;
|
||||
ModelRef model;
|
||||
bool active = true; // 禁用 = 渲染/拾取跳过(Unity enabled / UE SetActive)
|
||||
// 单 mesh 子引用:-1 = 整模型全部;N = 只画第 N 个
|
||||
//(GetMeshes 下标)。物理 Mesh 碰撞体自动跟随(BuildMeshShape
|
||||
// 读本组件)。序列化缺省键 = -1 向前兼容
|
||||
int meshIndex = -1;
|
||||
std::unordered_map<u32, std::string> materialOverrides;
|
||||
};
|
||||
|
||||
// 骨骼动画播放状态(纯数据 + 便捷控制)——Unity Animator 组件 /
|
||||
// UE SkeletalMeshComponent 动画侧的对应物。
|
||||
//
|
||||
// 职责分层(2026-10-06 重构,对齐 UE/Unity):
|
||||
// Model(资产:node/bone/clip)→ AnimationComponent(状态,本结构)
|
||||
// → Animation::AnimationSystem(调度:推进播放头 + 姿态求值 +
|
||||
// palette 产出)→ SceneRenderer(渲染只读查表消费)。
|
||||
// 本组件不含任何求值逻辑;ImGui 面板经 Play()/字段直接驱动。
|
||||
// 无此组件 = 按绑定姿势渲染。
|
||||
struct AnimationComponent {
|
||||
// ---- 播放设置(序列化落盘)----
|
||||
bool active = true; // 禁用 = 暂停采样(按当前帧姿态定格)
|
||||
int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
|
||||
bool playing = true;
|
||||
bool loop = true;
|
||||
f32 speed = 1.0f;
|
||||
// 根运动开关(Unity Animator "Apply Root Motion" 同语义):
|
||||
// true = 保留根节点位移轨道(跑动 clip 自带位移会漂出锚点);
|
||||
// false = 原地播放(丢弃动画子树根节点位移,旋转保留)——
|
||||
// 位移归脚本/物理驱动的角色(CharacterController 等)用 false
|
||||
bool applyRootMotion = true;
|
||||
|
||||
// ---- 播放头 / crossfade(瞬态,不落盘——重启从 0 重播)----
|
||||
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; // 过渡总时长(秒)
|
||||
|
||||
// 按名播放的运行时反指(瞬态,不落盘不序列化):AnimationSystem
|
||||
// 每帧回填(同 RigidBody::bodyValid 模式)。空 = 模型未就绪
|
||||
const Model* runtimeModel = nullptr;
|
||||
|
||||
// ---- 资产引用(序列化落盘)----
|
||||
// 外部动画资产(编辑器 attach 的动画容器文件,随场景持久化)。
|
||||
// clips 本体运行时并入 Model 资产(共享);重启后由 Inspector
|
||||
// 按此列表幂等重放
|
||||
std::vector<std::string> attachedAnimPaths;
|
||||
|
||||
// ---- Controller 模式(Unity Animator 语义)----
|
||||
// Direct = 现状单 clip 直控(clipIndex/playing 等);Controller =
|
||||
// .animctrl 状态机驱动(参数 + 过渡 + BlendTree)。两模式字段
|
||||
// 独立,切换互不清除(来回切配置不丢)
|
||||
enum class AnimMode : u8 { Direct = 0, Controller = 1 };
|
||||
AnimMode mode = AnimMode::Direct; // 落盘;缺键回退 Direct
|
||||
|
||||
std::string controllerPath; // .animctrl 资产路径(落盘)
|
||||
// 参数实例值(落盘 = 设计意图:调好的值重启还在;缺键 = 用
|
||||
// Controller 声明的默认值)。Trigger 是一次性信号不落盘
|
||||
std::unordered_map<std::string, float> floatParams;
|
||||
std::unordered_map<std::string, bool> boolParams;
|
||||
std::vector<std::string> pendingTriggers; // 消费即清(瞬态)
|
||||
|
||||
// 阻尼参数推进状态(瞬态,不落盘——重启从 floatParams 当前值
|
||||
// 重新起步):current 逐帧指数逼近 target,写回 floatParams 供
|
||||
// 状态机条件统一读取
|
||||
struct DampedParam {
|
||||
float current = 0.0f;
|
||||
float target = 0.0f;
|
||||
float response = 8.0f; // 逼近速率(1/s,越大越快)
|
||||
};
|
||||
std::unordered_map<std::string, DampedParam> dampedParams;
|
||||
|
||||
// 状态机瞬态(不落盘,重启从 entry state 开始)
|
||||
std::string currentState; // 空 = 未初始化(System 首帧置 entry)
|
||||
f32 stateTime = 0.0f; // 当前状态播放头(秒)
|
||||
std::string transFrom; // 过渡来源状态(空 = 无过渡)
|
||||
f32 transFromTime = 0.0f; // 来源状态播放头(过渡期间继续推进)
|
||||
f32 transElapsed = 0.0f; // 已过渡时长
|
||||
f32 transFade = 0.25f; // 过渡总时长(秒)
|
||||
|
||||
// ---- 脚本/编辑器共用 API(Unity Animator.SetXxx 语义)----
|
||||
void SetFloat(const std::string& name, float value)
|
||||
{
|
||||
floatParams[name] = value;
|
||||
}
|
||||
// 阻尼变体(Unity SetFloat(name, target, dampTime) 同构):登记
|
||||
// 目标值,AnimationSystem Controller 驱动期逐帧指数逼近(帧率
|
||||
// 无关)。首次登记从 floatParams 当前值起步(无则取目标),
|
||||
// 避免跳变。与 SetFloat 同名混用 = 直接 set 会被下一帧阻尼值
|
||||
// 覆盖,别混
|
||||
void SetFloatDamped(const std::string& name, float target, float response)
|
||||
{
|
||||
auto [it, inserted] = dampedParams.try_emplace(name);
|
||||
it->second.target = target;
|
||||
it->second.response = glm::max(response, 0.01f);
|
||||
if (inserted) {
|
||||
const auto fp = floatParams.find(name);
|
||||
it->second.current =
|
||||
fp != floatParams.end() ? fp->second : target;
|
||||
}
|
||||
}
|
||||
void SetBool(const std::string& name, bool value)
|
||||
{
|
||||
boolParams[name] = value;
|
||||
}
|
||||
// 一次性信号:被状态机条件评估消费后清除(同名去重——误配资产
|
||||
// 无消费条件时防每帧 SetTrigger 无限堆积)
|
||||
void SetTrigger(const std::string& name)
|
||||
{
|
||||
if (std::find(pendingTriggers.begin(), pendingTriggers.end(),
|
||||
name) == pendingTriggers.end()) {
|
||||
pendingTriggers.push_back(name);
|
||||
}
|
||||
}
|
||||
// 强制切状态(CrossFade 语义:来源状态淡出、目标状态淡入,
|
||||
// fade<=0 = 硬切)。编辑器下拉与脚本共用
|
||||
void CrossFade(const std::string& stateName, f32 fadeSeconds)
|
||||
{
|
||||
if (stateName == currentState) return;
|
||||
transFrom = currentState;
|
||||
transFromTime = stateTime;
|
||||
transElapsed = 0.0f;
|
||||
transFade = glm::max(fadeSeconds, 1e-3f);
|
||||
currentState = stateName;
|
||||
stateTime = 0.0f;
|
||||
}
|
||||
|
||||
// 切换 clip 并开始播放(Unity Animator.Play 语义:指派即播,
|
||||
// 编辑态选中 clip 直接预览;crossfade 越界/空 clip 保护在消费端)。
|
||||
// 脚本 API 与编辑器 Inspector 共用同一入口
|
||||
void Play(int index, f32 fadeSeconds = 0.3f)
|
||||
{
|
||||
if (index == clipIndex) {
|
||||
playing = true; // 重复指派 = 确保在播
|
||||
return;
|
||||
}
|
||||
fadeFrom = clipIndex;
|
||||
fadeFromTime = time;
|
||||
fadeElapsed = 0.0f;
|
||||
fadeDuration = fadeSeconds;
|
||||
clipIndex = index;
|
||||
time = 0.0f;
|
||||
playing = true;
|
||||
}
|
||||
|
||||
// 按名播放(Unity Animator.Play("Walk") 同形,推荐):clip 名经
|
||||
// runtimeModel 的 clip 表解析,消掉下标硬耦合(attach 顺序变了
|
||||
// 脚本不改)。未命中/模型未就绪返回 false。实现住
|
||||
// AnimationSystem.cpp——需要 Model 完整类型,组件头保持零资产
|
||||
// 依赖(Model.h 拖 vulkan/assimp,进本头会污染全部包含者)
|
||||
bool Play(const std::string& clipName, f32 fadeSeconds = 0.3f);
|
||||
};
|
||||
|
||||
// 相机。一个场景中 primary=true 的实体是当前渲染视角。
|
||||
struct CameraComponent {
|
||||
bool active = true; // 禁用 = 退出 primary 候选
|
||||
Camera camera;
|
||||
bool primary = false;
|
||||
};
|
||||
|
||||
// 方向光
|
||||
// 灯光。类型:
|
||||
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
|
||||
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
|
||||
struct LightComponent {
|
||||
glm::vec3 direction{0.0f, -1.0f, 0.0f};
|
||||
glm::vec4 color{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
bool active = true; // 禁用 = 不进光照
|
||||
enum class Type : u8 { Directional = 0, Point = 1 };
|
||||
Type type = Type::Directional;
|
||||
glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光
|
||||
glm::vec4 color{1.0f, 1.0f, 1.0f, 1.0f}; // rgb = 颜色,a = 强度
|
||||
f32 radius = 10.0f; // 仅点光:衰减半径
|
||||
bool castShadow = false; // 方向光可投影(引擎取第一个 castShadow 方向光为 CSM 光源)
|
||||
};
|
||||
|
||||
// 刚体(Jolt 后端)。纯数据 POD——头文件零 Jolt 依赖,
|
||||
// bodyId 存 JPH::BodyID 的裸位模式,翻译在 PhysicsWorld。
|
||||
// 生命周期:PhysicsWorld::SyncScene 每帧对账(创建/销毁/参数同步),
|
||||
// 动态体的 Transform 由物理写回(Writeback 标脏),静态/运动学的
|
||||
// Transform 由编辑器驱动同步进 body。
|
||||
// 扩展路线:软体/布料 = SoftBodyComponent(Jolt SoftBodySharedSettings),
|
||||
// 角色 = CharacterControllerComponent(CharacterVirtual),载具 = VehicleComponent
|
||||
// (VehicleConstraint)——都走 PhysicsWorld 门面,不复用本组件。
|
||||
struct RigidBodyComponent {
|
||||
bool active = true; // 禁用 = body 销毁/不建(对账自然消化)
|
||||
enum class BodyType : u8 { Static = 0, Dynamic = 1, Kinematic = 2 };
|
||||
// Mesh:用实体 MeshRenderer 的视觉网格建碰撞(Unity MeshCollider
|
||||
// 语义)——Static → Jolt MeshShape(精确三角网格,不可移动);
|
||||
// Dynamic/Kinematic → ConvexHullShape(凸包近似,MeshShape 太贵
|
||||
// 不能动)。数据源 Mesh 的 CPU 位置副本,无 MeshRenderer 回退 Box
|
||||
enum class ColliderType : u8 {
|
||||
Box = 0, Sphere = 1, Mesh = 2,
|
||||
Capsule = 3, Cylinder = 4, Compound = 5
|
||||
};
|
||||
|
||||
// Compound 子形状:局部 pos/rot 相对刚体原点,不递归嵌套
|
||||
//(child 不能再是 Compound)。尺寸语义同顶层(× Transform 缩放烘焙)
|
||||
struct CompoundChild {
|
||||
enum class Shape : u8 { Box = 0, Sphere = 1, Capsule = 2 };
|
||||
Shape shape = Shape::Box;
|
||||
glm::vec3 boxHalfExtents{0.25f, 0.25f, 0.25f};
|
||||
f32 radius = 0.25f; // Sphere 半径 / Capsule 半径
|
||||
f32 capsuleHalfHeight = 0.25f; // 仅 Capsule:圆柱段半高
|
||||
glm::vec3 position{0.0f};
|
||||
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
|
||||
bool operator==(const CompoundChild&) const = default;
|
||||
};
|
||||
|
||||
BodyType type = BodyType::Dynamic; // Unity 语义:加 Rigidbody 即受物理驱动
|
||||
ColliderType collider = ColliderType::Box;
|
||||
|
||||
// 碰撞体尺寸(× Transform 缩放,Unity BoxCollider.size 语义)——
|
||||
// 程序化模型顶点烘焙尺寸,不能从 scale 反推。
|
||||
// Capsule/Cylinder 的"半高"指圆柱段半高(不含半球端盖),总高 =
|
||||
// 2*(halfHeight + radius);Jolt 两 ctor 参数序相反见 PhysicsWorld
|
||||
glm::vec3 boxHalfExtents{0.5f, 0.5f, 0.5f};
|
||||
f32 sphereRadius = 0.5f;
|
||||
f32 capsuleRadius = 0.5f; // Unity CapsuleCollider 默认值同构
|
||||
f32 capsuleHalfHeight = 0.5f;
|
||||
f32 cylinderRadius = 0.5f;
|
||||
f32 cylinderHalfHeight = 0.5f;
|
||||
std::vector<CompoundChild> compound; // 仅 ColliderType::Compound
|
||||
|
||||
// 动态体参数
|
||||
f32 mass = 1.0f;
|
||||
f32 gravityScale = 1.0f;
|
||||
f32 friction = 0.5f;
|
||||
f32 restitution = 0.2f;
|
||||
|
||||
// 触发器(Unity isTrigger / UE Overlap Events 语义):只产生
|
||||
// Added/Removed 事件,无碰撞解算;底层 Jolt sensor
|
||||
bool isTrigger = false;
|
||||
|
||||
// 运行时(PhysicsWorld 维护,勿手改)
|
||||
u32 bodyId = 0; // JPH::BodyID 裸位
|
||||
bool bodyValid = false;
|
||||
glm::vec3 velocity{0.0f}; // 只读调试视图:线速度(m/s)
|
||||
};
|
||||
|
||||
// 约束/关节(Jolt TwoBodyConstraint 家族)。纯数据 POD 零 Jolt 依赖;
|
||||
// 创建/对账在 PhysicsWorld::SyncConstraints(kPhyDbgConstraints 出图元)。
|
||||
// target 存 UUID(跨存档稳定),惰性解析——SyncScene 每帧按 UUID 反查
|
||||
// 实体:序列化无需两遍解析,UUID 再生后自然失联,加载顺序无关
|
||||
struct JointComponent {
|
||||
bool active = true; // 禁用 = 约束销毁/不建(对账自然消化)
|
||||
enum class Type : u8 { Fixed = 0, Hinge = 1, Ball = 2 };
|
||||
Type type = Type::Fixed;
|
||||
UUID target{}; // 目标实体(0 = 未设置,不建约束)
|
||||
|
||||
// 锚点:各体 Transform 局部系(Unity connectedAnchor 语义,
|
||||
// × 本体缩放烘焙)。Fixed/Hinge/Ball 都连这两点
|
||||
glm::vec3 anchorA{0.0f}; // 本体(挂组件的实体)
|
||||
glm::vec3 anchorB{0.0f}; // 目标体
|
||||
|
||||
// Hinge 专用:转轴(本体局部系,方向量不吃缩放)+ 角度限位(度)
|
||||
glm::vec3 hingeAxis{1.0f, 0.0f, 0.0f};
|
||||
bool enableLimit = false;
|
||||
f32 limitMin = -90.0f; // Jolt 语义 min∈[-180,0] max∈[0,180]
|
||||
f32 limitMax = 90.0f;
|
||||
|
||||
bool operator==(const JointComponent&) const = default;
|
||||
};
|
||||
|
||||
// 角色控制器参数(Jolt CharacterVirtual 扫掠运动学,非刚体——不受
|
||||
// 物理模拟驱动,游戏逻辑经 PhysicsWorld 门面喂速度/跳跃)。本组件
|
||||
// 只存参数(序列化:存参数不存状态),速度/着地等运行时状态在
|
||||
// PhysicsWorld 的 CharacterController 里(对账重建)
|
||||
struct CharacterControllerComponent {
|
||||
bool active = true; // 禁用 = 角色移除(重启用按当前 Transform 重建)
|
||||
f32 radius = 0.3f; // 胶囊半径
|
||||
f32 capsuleHalfHeight = 0.4f; // 圆柱段半高(总高 = 2*(hh+r))
|
||||
f32 mass = 70.0f;
|
||||
f32 maxSlopeAngle = 45.0f; // 度:可站立的最大坡角
|
||||
f32 stepHeight = 0.4f; // 台阶自动攀爬高(WalkStairs)
|
||||
// 重力倍率(UE CharacterMovement.GravityScale 同位):真实 9.81
|
||||
// 对人形角色偏"棉花糖"(滞空 1.2s+),游戏手感普遍 2~3 倍
|
||||
f32 gravityScale = 2.0f;
|
||||
// 形状附加偏移:叠加在脚底约定(胶囊底端=原点)之上,模型
|
||||
// pivot 不在脚底时对齐视觉网格用(Y+ 胶囊上抬)
|
||||
glm::vec3 shapeOffset{0.0f};
|
||||
|
||||
bool operator==(const CharacterControllerComponent&) const = default;
|
||||
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
|
||||
};
|
||||
|
||||
// 脚本组件:多脚本挂载(Unity 语义——一个 GameObject 挂 N 个
|
||||
// MonoBehaviour,各自有 enabled)。每项 = 脚本类名(注册表键,
|
||||
// 将来 Lua/C# 同键解析)+ 独立启停位;组件级 active 是总开关。
|
||||
// 实例生命周期由 ScriptRuntime 对账管理(不进组件,保持可拷贝)
|
||||
struct ScriptComponent {
|
||||
// 单个脚本项
|
||||
struct Item {
|
||||
std::string name;
|
||||
bool active = true; // 禁用 = 实例保留但停 OnUpdate
|
||||
// 参数(权威数据,随场景序列化)。脚本成员 = 运行时真值,
|
||||
// 实例化期由 ScriptRuntime 按 schema 灌入:键缺失 → 声明
|
||||
// 默认,多余键忽略;运行时 SetParam 写穿回这里(PIE 实时
|
||||
// 反映,Stop 由快照天然回滚)
|
||||
std::vector<Engine::Scripting::ScriptParam> params;
|
||||
|
||||
bool operator==(const Item&) const = default;
|
||||
};
|
||||
|
||||
bool active = true; // 组件总开关(全部停 tick,实例保留)
|
||||
std::vector<Item> items;
|
||||
|
||||
bool operator==(const ScriptComponent&) const = default;
|
||||
};
|
||||
|
||||
// 天空盒/环境背景(UE Skybox Actor 语义:环境是场景里的 Actor +
|
||||
// 组件,不是常驻全局设置)。envPath = 环境贴图资产(.hdr,经
|
||||
// AssetManager 加载、Renderer 预积分后进 IBL/Skybox pass);空 =
|
||||
// 只清屏(ClearColor 露出)。多实例取第一个 active(entt 视图序 =
|
||||
// 组件创建序,首个命中即稳定)
|
||||
struct SkyboxComponent {
|
||||
bool active = true; // 禁用 = 不画天空(清屏色露出)
|
||||
std::string envPath;
|
||||
};
|
||||
|
||||
// 环境光(UE SkyLight 语义:全局漫射环境照明)。多实例取第一个 active
|
||||
struct SkyLightComponent {
|
||||
bool active = true;
|
||||
glm::vec4 ambientColor{1.0f}; // 旧 Settings 默认
|
||||
};
|
||||
|
||||
} // namespace Engine::ECS
|
||||
|
||||
@@ -40,23 +40,69 @@ namespace Engine::ECS {
|
||||
Scene& operator=(const Scene&) = delete;
|
||||
|
||||
// ---- 场景级设置 ----
|
||||
// 环境类设置已迁为环境实体组件(SkyboxComponent/SkyLightComponent,
|
||||
// UE Skybox/SkyLight Actor 语义);仅保留渲Target清屏色
|
||||
struct Settings {
|
||||
glm::vec4 ambientColor{0.15f, 0.15f, 0.2f, 0.0f};
|
||||
glm::vec4 ClearColor = {0.0, 0.0, 0.0, 1.0};
|
||||
};
|
||||
Settings& GetSettings() { return m_Settings; }
|
||||
const Settings& GetSettings() const { return m_Settings; } // 序列化只读
|
||||
|
||||
// ---- 实体 ----
|
||||
|
||||
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy()
|
||||
EntityHandle CreateEntity(std::string name = {});
|
||||
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
|
||||
EntityHandle CreateEntity(std::string name = "new entity");
|
||||
// 延迟:帧尾统一销毁。层级级联(UE 语义):销毁父 → 子树随葬
|
||||
void DestroyEntity(Entity e);
|
||||
void FlushDestroyQueue(); // 每帧末尾由 Application 调用
|
||||
|
||||
// 整场景换血(序列化加载):立即销毁全部实体并清空延迟队列。
|
||||
// 仅用于"校验通过后的原子替换"——调用方持有的 Entity/EntityHandle
|
||||
// 全部悬空,必须自行重解析(GetEntityHandle 可从裸 entity 重建)
|
||||
void Clear();
|
||||
|
||||
// 裸 entity → 句柄表皮(EntityHandle 构造私有 + friend Scene;
|
||||
// 序列化加载后应用层重解析悬空句柄用)。无效 entity 返回空句柄
|
||||
EntityHandle GetEntityHandle(Entity e);
|
||||
|
||||
// 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update
|
||||
// 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用,
|
||||
// 用户代码永远不需要手动 Compose。层级语义:从根 DFS,脏实体
|
||||
// 或脏祖先的后代才重算(父先于子,ComposeFromParent 组合)
|
||||
void UpdateTransforms();
|
||||
bool IsAlive(Entity e) const { return m_Registry.valid(e); }
|
||||
|
||||
// ---- 层级(UE Actor attach 语义)----
|
||||
|
||||
// 改父/挂根。keepWorld=true(编辑器拖拽语义):换算 local 保持
|
||||
// world 位姿不变;false(序列化加载语义):local 已就位只建链。
|
||||
// 防环(新父是 child 后代 → 拒绝);成功返回 true
|
||||
bool SetParent(Entity child, Entity parent, bool keepWorld = true);
|
||||
|
||||
// 父句柄;根/悬空/无效实体返回 entt::null
|
||||
Entity GetParent(Entity e) const;
|
||||
|
||||
// e 是否在 ancestor 的子树内(含相等)。防环检测/面板用
|
||||
bool IsDescendantOf(Entity e, Entity ancestor) const;
|
||||
|
||||
// 世界位姿写入(UE SetActorLocation 语义):world SRT → 沿父链
|
||||
// 反解 local。物理写回/Gizmo 拖拽用
|
||||
void SetWorldPose(Entity e, const glm::vec3& worldPos,
|
||||
const glm::quat& worldRot, const glm::vec3& worldScale);
|
||||
|
||||
// 沿父链自顶向下补算脏 world(层级写读 world 前调用——
|
||||
// UpdateTransforms 之外的时机读取/换算 world 前保证缓存新鲜)
|
||||
void EnsureWorldFresh(Entity e);
|
||||
|
||||
// 按稳定 UUID 查实体(序列化/跨引用场景);找不到返回 entt::null
|
||||
Entity FindEntityByUUID(UUID uuid);
|
||||
|
||||
// 全量重建 UUID → Entity 索引(序列化加载完调一次;运行期勿调,
|
||||
// CreateEntity / FlushDestroyQueue 已增量维护)
|
||||
void RebuildUuidIndex();
|
||||
|
||||
Registry& GetRegistry() { return m_Registry; }
|
||||
const Registry& GetRegistry() const { return m_Registry; } // 序列化等只读遍历
|
||||
|
||||
// ---- 组件便捷接口(转发 entt)----
|
||||
|
||||
@@ -86,13 +132,21 @@ namespace Engine::ECS {
|
||||
|
||||
// ---- 常用查询 ----
|
||||
|
||||
// primary 相机(渲染视角来源);无则返回 nullptr
|
||||
CameraComponent* GetPrimaryCamera();
|
||||
// 主相机(primary=true,渲染视角来源);无则返回 nullptr。
|
||||
// outTransform 可选:带出该实体的 TransformComponent
|
||||
//(相机位姿的单一事实源)
|
||||
CameraComponent* GetPrimaryCamera(TransformComponent** outTransform = nullptr);
|
||||
|
||||
private:
|
||||
Registry m_Registry;
|
||||
std::vector<Entity> m_PendingDestroy;
|
||||
Settings m_Settings;
|
||||
|
||||
// UUID → Entity 索引(O(1) FindEntityByUUID;SyncConstraints 每帧调用)
|
||||
// CreateEntity / FlushDestroyQueue 增量维护;序列化加载后由
|
||||
// RebuildUuidIndex() 全量重建一次(SceneSerializer 直接改 IdComponent.uuid
|
||||
// 绕过 CreateEntity,仅靠增量会不同步)
|
||||
std::unordered_map<UUID, Entity> m_UUIDToEntity;
|
||||
};
|
||||
|
||||
} // namespace Engine::ECS
|
||||
|
||||
@@ -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,29 @@ 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);
|
||||
|
||||
// ---- 材质球预览 atlas(MaterialPreviewPass 输出 → 材质槽 UI;
|
||||
// editor 专属,HELLO_EDITOR 裁剪)----
|
||||
// 注册/生命周期语义与场景纹理完全一致(按槽位各一个描述符)
|
||||
#ifdef HELLO_EDITOR
|
||||
ImTextureID GetPreviewTexture(u32 slot) const
|
||||
{
|
||||
return slot < kMaxFramesInFlight
|
||||
? m_PreviewTextures[slot] : ImTextureID_Invalid;
|
||||
}
|
||||
void UpdatePreviewTexture(u32 slot, vk::ImageView view);
|
||||
#endif
|
||||
|
||||
// ---- 每帧三段式(由 Application::Run 驱动)----
|
||||
|
||||
// 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间,
|
||||
@@ -55,8 +80,13 @@ namespace Engine {
|
||||
// 生命周期保证:Application 成员序中 ImGuiLayer 在 Renderer 之后声明
|
||||
//(先析构),因此 Device 引用在整个层生命周期内有效
|
||||
Device& m_Device;
|
||||
Window& m_Window; // 光标捕获状态查询(NewFrame hover 门控)
|
||||
vk::DescriptorPool m_Pool;
|
||||
bool m_Enabled = true;
|
||||
ImTextureID m_SceneTextures[kMaxFramesInFlight]{};
|
||||
#ifdef HELLO_EDITOR
|
||||
ImTextureID m_PreviewTextures[kMaxFramesInFlight]{};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// InputAction.h:输入动作模型类型。
|
||||
//
|
||||
// 动作层把"游戏语义"(Move/Jump)与"物理输入源"(键/鼠标/手柄)解耦:
|
||||
// - 动作(Action)= 具名游戏语义,Button(按下沿)/Axis1D/Axis2D
|
||||
// - 绑定(Binding)= 一个输入源到动作贡献的映射(code = GLFW 标准码)
|
||||
// 上层(脚本/玩法系统)只认动作名,换键位/换手柄不改代码。
|
||||
//
|
||||
// code 值约定 = GLFW 标准码(同 Input::Key 惯例:Key 即 glf3.h 键码,
|
||||
// GamepadButton 即 GLFW_GAMEPAD_BUTTON_*,GamepadAxis 即
|
||||
// GLFW_GAMEPAD_AXIS_*);调用方不得依赖具体数值。
|
||||
//
|
||||
// component:Axis2D 动作中该绑定贡献哪个分量(0=X,1=Y)。
|
||||
// 例如 W → {Key, KEY_W, scale=-1, component=1}(Y 分量负方向 = 前进),
|
||||
// 左摇杆 Y 轴 → {GamepadAxis, AXIS_LEFT_Y, scale=+1, component=1}
|
||||
//(GLFW 摇杆推上为负,与 W 的 -1 语义自然对齐)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_INPUTACTION_H
|
||||
#define HELLOVULKAN_INPUTACTION_H
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Engine/Core/Types.h"
|
||||
|
||||
namespace Engine {
|
||||
namespace Input {
|
||||
|
||||
// 动作类型
|
||||
enum class ActionKind : u8 {
|
||||
Button, // 0/1 按下态(Down/Pressed/Released 边沿查询)
|
||||
Axis1D, // 标量 [-1,1](键绑定 scale 加权、手柄轴原始值×scale)
|
||||
Axis2D, // 平面向量(2D 径向死区 + 单位圆钳制)
|
||||
};
|
||||
|
||||
// 绑定源类型
|
||||
enum class SourceKind : u8 {
|
||||
Key, // 键盘(code = GLFW 键码)
|
||||
MouseButton, // 鼠标键(0=左 1=右 2=中)
|
||||
GamepadButton, // 手柄按钮(0-14 标准布局)
|
||||
GamepadAxis, // 手柄轴(0-5:左摇杆 X/Y、右摇杆 X/Y、LT、RT)
|
||||
};
|
||||
|
||||
// 单条绑定:一个输入源对一个动作的贡献
|
||||
struct ActionBinding {
|
||||
SourceKind kind;
|
||||
i32 code; // 见头注释(GLFW 标准码)
|
||||
f32 scale; // 贡献权重(方向:键 ±1;轴 ±1)
|
||||
u8 component = 0; // 仅 Axis2D 消费:0=X 分量,1=Y 分量
|
||||
};
|
||||
|
||||
// 动作实例:定义 + 绑定表 + 双帧值(边沿派生用,见 InputMap)
|
||||
struct InputAction {
|
||||
std::string name;
|
||||
ActionKind kind = ActionKind::Button;
|
||||
std::vector<ActionBinding> bindings;
|
||||
|
||||
// prev/cur 双帧值(Button/Axis1D 用 x;Axis2D 用 xy)。
|
||||
// Button 语义:Down = |cur.x|>0.5,Pressed/Released = cur/prev 边沿
|
||||
glm::vec2 prev{0.0f, 0.0f};
|
||||
glm::vec2 cur{0.0f, 0.0f};
|
||||
};
|
||||
|
||||
} // namespace Input
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_INPUTACTION_H
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// InputMap.h:动作表(定义/绑定/采样/重绑定/持久化)。
|
||||
//
|
||||
// 装配期(DefineAction/Bind/SetDefaults/LoadYaml)与运行期(Poll + 查询)
|
||||
// 分离:名字→下标哈希在 Define 期建好,Poll 只写值槽位——热路径零堆分配。
|
||||
//
|
||||
// 采样语义(Poll):
|
||||
// - active=false(GameFlow 非 Running):全部动作值清零且 prev 同步清零,
|
||||
// 恢复激活后按住的键产生一次 Pressed 边(符合游戏语义,无脏边)
|
||||
// - active=true:键鼠经 Window 轮询(自带 UI 捕获门控),手柄经
|
||||
// Window::PollGamepad;连接翻转打一条日志
|
||||
// - Axis2D:各绑定按 component 分量加权求和 → 2D 径向死区(0.15) →
|
||||
// 超出单位圆时归一化(斜向不超速)
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_INPUTMAP_H
|
||||
#define HELLOVULKAN_INPUTMAP_H
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Engine/Core/Types.h"
|
||||
#include "Engine/Input/InputAction.h"
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Window;
|
||||
|
||||
namespace Input {
|
||||
|
||||
class InputMap {
|
||||
public:
|
||||
// ---- 装配期 ----
|
||||
|
||||
// 定义动作(返回动作 id = 下标;重名忽略并返回已有 id)
|
||||
size_t DefineAction(const std::string& name, ActionKind kind);
|
||||
// 追加绑定(动作 id 非法时忽略)
|
||||
void Bind(size_t id, ActionBinding b);
|
||||
// 就地重绑定(下帧采样即生效)。成功返回 true
|
||||
bool Rebind(size_t id, size_t bindingIdx, ActionBinding b);
|
||||
// 内建默认表:Move(Axis2D)=WASD+方向键+左摇杆、Jump(Button)=Space+A
|
||||
void SetDefaults();
|
||||
// 清空全部动作与索引(LoadYaml 重建用)
|
||||
void Clear();
|
||||
|
||||
// ---- 运行期 ----
|
||||
|
||||
// 每帧采样(主循环单点调用:物理/脚本/OnUpdate 之前)
|
||||
void Poll(const Window& win, bool active);
|
||||
|
||||
bool Down(size_t id) const; // Button: 按住;Axis: |值|>0.5
|
||||
bool Pressed(size_t id) const; // 本帧按下边沿
|
||||
bool Released(size_t id) const; // 本帧松开边沿
|
||||
f32 Value(size_t id) const; // Axis1D;Button 返回 0/1
|
||||
glm::vec2 Value2(size_t id) const;// Axis2D;其他 kind 返回 (x,0)
|
||||
|
||||
// 手柄连接态(Poll 时更新;连接翻转打 INFO)
|
||||
bool IsGamepadConnected() const { return m_GamepadConnected; }
|
||||
|
||||
// ---- 持久化(yaml)----
|
||||
|
||||
// 格式见 InputMap.cpp;保存失败(目录不存在等)返回 false
|
||||
bool SaveYaml(const std::string& path) const;
|
||||
// 文件不存在静默失败返回 false(保持现状表);存在则
|
||||
// SetDefaults 后按 yaml 覆盖同名动作的绑定表
|
||||
bool LoadYaml(const std::string& path);
|
||||
|
||||
// ---- 查询辅助 ----
|
||||
size_t Count() const { return m_Actions.size(); }
|
||||
// 名字→id;不存在返回 npos
|
||||
static constexpr size_t npos = static_cast<size_t>(-1);
|
||||
size_t Find(const std::string& name) const;
|
||||
|
||||
private:
|
||||
void PollGamepadConnection(bool connected);
|
||||
|
||||
std::vector<InputAction> m_Actions;
|
||||
std::unordered_map<std::string, size_t> m_ByName;
|
||||
bool m_GamepadConnected = false;
|
||||
};
|
||||
|
||||
} // namespace Input
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_INPUTMAP_H
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// CharacterController:单实体角色控制器(Jolt CharacterVirtual 后端)。
|
||||
//
|
||||
// 非刚体、扫掠运动学:CharacterVirtual 不注册进 PhysicsSystem,位置由
|
||||
// 本类每步扫掠推进(撞墙滑动/斜坡限制/贴地/上台阶),其他物理对象
|
||||
// "看不到"它(无碰撞解算,仅查询)。由 PhysicsWorld 对账创建/销毁并
|
||||
// 在 PhysicsSystem::Update 之后驱动(Jolt 官方建议序)。
|
||||
//
|
||||
// 角色间碰撞(Jolt 设计行为):CharacterVirtual 彼此默认穿透,需经
|
||||
// CharacterVsCharacterCollision 显式互查。PhysicsWorld 持有一份
|
||||
// CharacterVsCharacterCollisionSimple 并注入每个角色(SetCharacterCollision),
|
||||
// 使角色扫掠时也撞其它角色——进/出列表由本类构造/析构自动维护。
|
||||
//
|
||||
// 速度语义(游戏逻辑职责分工):
|
||||
// - 水平移动:游戏逻辑每帧 SetCharacterVelocity 喂意图速度
|
||||
// - 垂直分量:本类自动积分重力(着地清零向下分量);Jump 置垂直速度
|
||||
// (Unity CharacterController.Move + gravity 手动积分同构)
|
||||
//
|
||||
// 实体原点 = 脚底(胶囊底端经 mShapeOffset 抬升到原点上方)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_CHARACTERCONTROLLER_H
|
||||
#define HELLOVULKAN_CHARACTERCONTROLLER_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Physics/PhysicsSystem.h>
|
||||
#include <Jolt/Physics/Character/CharacterVirtual.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class CharacterController {
|
||||
public:
|
||||
// 创建参数快照(变更检测 → 重建 CharacterVirtual)
|
||||
struct Params {
|
||||
f32 radius = 0.3f; // 胶囊半径
|
||||
f32 halfHeight = 0.4f; // 圆柱段半高
|
||||
f32 mass = 70.0f;
|
||||
f32 maxSlopeAngle = 45.0f; // 度
|
||||
f32 stepHeight = 0.4f; // 台阶攀爬高
|
||||
// 形状附加偏移(相对"胶囊底端钉实体原点"的脚底约定):
|
||||
// 模型 pivot 不在脚底时对齐用(Y+ 上抬)
|
||||
glm::vec3 shapeOffset{0.0f};
|
||||
|
||||
bool operator==(const Params&) const = default;
|
||||
};
|
||||
|
||||
CharacterController(JPH::PhysicsSystem& system, const Params& params,
|
||||
const glm::vec3& position);
|
||||
// 析构:先从角色间碰撞列表摘除(防悬垂指针),Ref 随即释放
|
||||
~CharacterController();
|
||||
|
||||
// 注入角色间碰撞列表(PhysicsWorld 单份共享):登记本角色并打开
|
||||
// 互查。构造后由 PhysicsWorld 创建时调用一次;列表须比本对象活得久
|
||||
void SetCharacterCollision(JPH::CharacterVsCharacterCollisionSimple& list);
|
||||
|
||||
// 每物理步调用:重力积分 + ExtendedUpdate(扫掠滑动 + 贴地 +
|
||||
// 上台阶)。allocator 与 PhysicsSystem::Update 串行复用同一池
|
||||
void Update(f32 dt, const JPH::Vec3& gravity,
|
||||
JPH::TempAllocator& allocator);
|
||||
|
||||
// ---- 游戏逻辑门面(PhysicsWorld 转发)----
|
||||
void SetLinearVelocity(const glm::vec3& velocity);
|
||||
glm::vec3 GetLinearVelocity() const;
|
||||
void Jump(f32 speed); // 垂直速度置 speed(着地方向才有效)
|
||||
bool IsGrounded() const;
|
||||
glm::vec3 GetPosition() const;
|
||||
void Teleport(const glm::vec3& position) const; // 编辑器拖动
|
||||
|
||||
const Params& GetParams() const { return m_Params; }
|
||||
|
||||
// ---- 碰撞事件支持(PhysicsWorld 合成 Enter/Exit 用)----
|
||||
// 本步活跃接触列表(CharacterVirtual 直通;含对方体 ID/命中点/
|
||||
// 法线/距离)。CharacterVirtual 非刚体,Jolt 的 ContactListener
|
||||
// 不会为它回调——由 PhysicsWorld 读本列表对比出事件边沿
|
||||
const JPH::CharacterVirtual::ContactList& GetActiveContacts() const;
|
||||
// 上一次 Update 扫掠"之前"的速度(撞击强度判定)。Jolt 在扫掠
|
||||
// 中会抵消朝向墙面/陡面的速度分量(CancelVelocityTowardsSteepSlopes),
|
||||
// 扫掠后再读 GetLinearVelocity 往往已归零——撞墙判定必须用这里
|
||||
glm::vec3 GetSweepVelocity() const;
|
||||
|
||||
// 调试渲染:非 body,PhysicsSystem::DrawBodies 画不到,须手动
|
||||
// (脚本在物理位姿/脚底原点处画胶囊线框)
|
||||
void DebugDraw(JPH::DebugRenderer& renderer) const;
|
||||
|
||||
private:
|
||||
Params m_Params;
|
||||
JPH::Ref<JPH::CharacterVirtual> m_Character;
|
||||
JPH::CharacterVirtual::ExtendedUpdateSettings m_Ext;
|
||||
JPH::CharacterVsCharacterCollisionSimple* m_VsChar = nullptr; // 非拥有
|
||||
JPH::Vec3 m_SweepVelocity{0.0f, 0.0f, 0.0f}; // 扫掠前速度快照
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_CHARACTERCONTROLLER_H
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// PhysicsDebugDraw:Jolt 调试图元收集器(JPH::DebugRendererSimple 派生)。
|
||||
//
|
||||
// 职责只有"收集":Jolt 各调试源(BodyManager::Draw 的形状线框/速度箭头、
|
||||
// ConstraintManager::DrawConstraints、PhysicsWorld 手绘的重力/加速度箭头)
|
||||
// 触发的 DrawLine/DrawTriangle 回调全部收进顶点数组;渲染侧
|
||||
// (PhysicsDebugPass)每帧上传到 ring VB 绘制。收集器不理解 Vulkan,
|
||||
// 渲染 pass 不理解 Jolt——图元契约见 PhysicsDebugTypes.h。
|
||||
//
|
||||
// 生命周期:PhysicsWorld 持有并安装为 JPH::DebugRenderer::sInstance;
|
||||
// DebugRendererSimple 的构造即 DebugRenderer::Initialize()(预烘焙
|
||||
// box/sphere/cylinder 等通用几何 batch),NextFrame() 每帧释放不再
|
||||
// 引用的缓存几何。
|
||||
//
|
||||
// 扩展性(物理调试的唯一图元出口,新调试源零新通道):
|
||||
// 软体布料 → DrawSettings 的 mDrawSoftBody* 开关原生生效
|
||||
// 角色控制器 → CharacterVirtual::DebugDraw 走同一 sInstance
|
||||
// 载具控制器 → VehicleConstraint::Draw 走同一 sInstance
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSDEBUGDRAW_H
|
||||
#define HELLOVULKAN_PHYSICSDEBUGDRAW_H
|
||||
|
||||
#include <Engine/Physics/PhysicsDebugTypes.h>
|
||||
|
||||
#ifdef JPH_DEBUG_RENDERER
|
||||
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Renderer/DebugRendererSimple.h>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class PhysicsDebugDraw final : public JPH::DebugRendererSimple {
|
||||
public:
|
||||
// 析构交给基类:~DebugRenderer 自己断言并清 sInstance(JPH_ASSERT(sInstance == this)),
|
||||
// 派生类手动清会让基类断言看到 nullptr 而失败
|
||||
|
||||
PhysicsDebugFrame& Frame() { return m_Frame; }
|
||||
|
||||
// 每次收集前调用:清空上一帧图元(容量保留,稳态零堆分配)
|
||||
void BeginCollect() { m_Frame.Clear(); }
|
||||
|
||||
// ---- DebugRenderer 回调(基类其余绘制原语全部路由到这里)----
|
||||
void DrawLine(JPH::RVec3Arg inFrom, JPH::RVec3Arg inTo,
|
||||
JPH::ColorArg inColor) override;
|
||||
void DrawTriangle(JPH::RVec3Arg inV1, JPH::RVec3Arg inV2,
|
||||
JPH::RVec3Arg inV3, JPH::ColorArg inColor,
|
||||
JPH::DebugRenderer::ECastShadow inCastShadow) override;
|
||||
void DrawText3D(JPH::RVec3Arg, const JPH::string_view&,
|
||||
JPH::ColorArg, float) override {
|
||||
// 3D 文本暂不支持(遮挡 UI 噪音,需要时再挂 ImGui 调试层)
|
||||
}
|
||||
|
||||
private:
|
||||
static glm::vec3 ToGlm(JPH::RVec3Arg v)
|
||||
{
|
||||
return {static_cast<f32>(v.GetX()), static_cast<f32>(v.GetY()),
|
||||
static_cast<f32>(v.GetZ())};
|
||||
}
|
||||
static glm::vec4 ToGlm(JPH::ColorArg c)
|
||||
{
|
||||
constexpr f32 kInv = 1.0f / 255.0f;
|
||||
return {c.r * kInv, c.g * kInv, c.b * kInv, c.a * kInv};
|
||||
}
|
||||
|
||||
PhysicsDebugFrame m_Frame;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#else // !JPH_DEBUG_RENDERER
|
||||
|
||||
// Distribution 构建:Jolt 调试渲染被整体裁剪(DEBUG_RENDERER_IN_* 均关),
|
||||
// 收集器退化为空实现——所有消费方照常编译,零绘制零开销
|
||||
namespace Engine {
|
||||
|
||||
class PhysicsDebugDraw {
|
||||
public:
|
||||
PhysicsDebugFrame& Frame() { return m_Frame; }
|
||||
void BeginCollect() { m_Frame.Clear(); }
|
||||
void NextFrame() {}
|
||||
|
||||
private:
|
||||
PhysicsDebugFrame m_Frame;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif // JPH_DEBUG_RENDERER
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSDEBUGDRAW_H
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// PhysicsDebugTypes:物理调试可视化的数据契约(Jolt 零依赖)。
|
||||
//
|
||||
// PhysicsDebugDraw(收集器,Physics 层)生产、PhysicsDebugPass(渲染)
|
||||
// 消费;渲染侧只 include 本头文件,不接触任何 Jolt 类型——分层约束
|
||||
// 与 RigidBodyComponent 的"零 Jolt 依赖"一致。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSDEBUGTYPES_H
|
||||
#define HELLOVULKAN_PHYSICSDEBUGTYPES_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
// 图元顶点(28B):世界空间位置 + 线性 HDR 颜色(可 >1,过 tonemap
|
||||
// 依旧醒目)。线段/三角形共用同一顶点格式,各走一条动态 VB
|
||||
struct PhysicsDebugVertex {
|
||||
glm::vec3 pos;
|
||||
glm::vec4 color;
|
||||
};
|
||||
|
||||
// 单帧收集结果:PhysicsWorld 每个 Step 调用尾按 debugBits 重新收集
|
||||
//(渲染帧率,收集间隔内数据保持稳定,多渲染帧复用同一份图元)
|
||||
struct PhysicsDebugFrame {
|
||||
std::vector<PhysicsDebugVertex> lines; // 线段顶点(成对)
|
||||
std::vector<PhysicsDebugVertex> triangles; // 三角形顶点(三张一副)
|
||||
|
||||
void Clear() { lines.clear(); triangles.clear(); }
|
||||
bool Empty() const { return lines.empty() && triangles.empty(); }
|
||||
};
|
||||
|
||||
// 可视化位掩码(ImGui 经 PhysicsWorld 门面读写;0 = 全关)
|
||||
enum PhysicsDebugBits : u32 {
|
||||
kPhyDbgColliders = 1 << 0, // collider 线框(Jolt BodyManager::Draw)
|
||||
kPhyDbgConstraints = 1 << 1, // 约束(关节/软体约束,接入后生效)
|
||||
kPhyDbgGravity = 1 << 2, // 重力箭头(手绘,Jolt 无现成 API)
|
||||
kPhyDbgVelocity = 1 << 3, // 速度箭头(Jolt DrawSettings.mDrawVelocity)
|
||||
kPhyDbgAccel = 1 << 4, // 加速度箭头(手绘,速度差分/定步长)
|
||||
kPhyDbgDefault = kPhyDbgColliders | kPhyDbgGravity |
|
||||
kPhyDbgVelocity | kPhyDbgAccel,
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSDEBUGTYPES_H
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// PhysicsEvents:碰撞事件数据契约(Jolt 零依赖)。
|
||||
//
|
||||
// PhysicsWorld 内部的 ContactListenerImpl 在步进中收集、帧尾入队;
|
||||
// 消费方(App/游戏逻辑)只 include 本头文件,不接触任何 Jolt 类型
|
||||
// ——分层约束与 RigidBodyComponent 的"零 Jolt 依赖"一致。
|
||||
//
|
||||
// 事件语义 = 最近一次物理步进(Step 开头清空队列,消费方每帧读取)。
|
||||
// a/b 存实体位模式(entt::to_integral 的裸值),消费方用
|
||||
// static_cast<entt::entity>(bits) 还原,并自行 registry.valid() 校验
|
||||
// ——实体可能在事件产生后、消费前被删除。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSEVENTS_H
|
||||
#define HELLOVULKAN_PHYSICSEVENTS_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
struct CollisionEvent {
|
||||
enum class Kind : u8 { Added = 0, Persisted = 1, Removed = 2 };
|
||||
|
||||
u32 a = 0; // 实体位模式(Body1;Jolt 保证 BodyID 1 < 2)
|
||||
u32 b = 0; // 实体位模式(Body2)
|
||||
Kind kind = Kind::Added;
|
||||
glm::vec3 point{0.0f}; // 世界空间首个接触点(Removed 时无效)
|
||||
glm::vec3 normal{0.0f}; // 世界空间法线(body2 沿此方向推出;Removed 时无效)
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSEVENTS_H
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// PhysicsRay:射线查询数据契约(Jolt 零依赖)。
|
||||
//
|
||||
// PhysicsWorld::RayCast(BodyInterface/NarrowPhaseQuery 门面)生产;
|
||||
// 消费方(游戏查询/编辑器拾取)只 include 本头文件,不接触任何 Jolt
|
||||
// 类型——分层约束与 PhysicsEvents.h 一致。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSRAY_H
|
||||
#define HELLOVULKAN_PHYSICSRAY_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
// 射线命中结果(最近命中单结果;编辑器拾取/点击选择够用,
|
||||
// 穿透列表走 CastRayCollector 扩展,本契约保持最小)
|
||||
struct RayHit {
|
||||
u32 entityBits = 0; // 命中实体的位模式(消费方自行 valid 校验)
|
||||
glm::vec3 point{0.0f}; // 世界空间命中点
|
||||
glm::vec3 normal{0.0f}; // 世界空间表面法线(命中子形状)
|
||||
f32 distance = 0.0f; // 命中距离(origin 起算)
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSRAY_H
|
||||
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// PhysicsWorld:物理世界门面(Jolt 后端)。
|
||||
//
|
||||
// 镜像 SceneRenderer 的单向数据流:
|
||||
// SyncScene(读 ECS 对账 body)→ PhysicsSystem::Update → Writeback(动态体 → Transform 标脏)
|
||||
// 定步长 accumulator(1/60,最多 4 子步,积压丢弃防螺旋死亡)。
|
||||
//
|
||||
// 分层约束:
|
||||
// - 组件层(Components.h)零 Jolt 依赖:bodyId 存 JPH::BodyID 裸位模式
|
||||
// - App/编辑器只经 Application::GetPhysics() 访问开关,不接触 Jolt 头
|
||||
//
|
||||
// 可扩展路线(本门面是唯一入口,feature 各自分文件演进,禁止互相耦合):
|
||||
// 软体/布料 → Jolt SoftBodySharedSettings/SoftBodyCreationSettings(原生支持),
|
||||
// 扩展为 SyncSoftBodies/WritebackSoft(网格引用走 AssetRef)
|
||||
// 角色控制器 → CharacterVirtual(运动学扫掠,非刚体,独立 Controller 类持有)
|
||||
// 载具控制器 → VehicleConstraint(挂动态刚体 chassis 上,独立 Controller 类)
|
||||
// 碰撞事件 → ContactListenerImpl(本类嵌套,已实现)+ GetEvents 帧尾队列
|
||||
// 射线查询 → BodyInterface::RayCast 门面(编辑器拾取/游戏查询)
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSWORLD_H
|
||||
#define HELLOVULKAN_PHYSICSWORLD_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Physics/PhysicsDebugDraw.h>
|
||||
#include <Engine/Physics/PhysicsEvents.h>
|
||||
#include <Engine/Physics/PhysicsRay.h>
|
||||
#include <Engine/Physics/CharacterController.h>
|
||||
#include <Engine/ECS/Components.h> // BodyRecord 快照存 CompoundChild(完整类型)
|
||||
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Physics/PhysicsSystem.h>
|
||||
#include <Jolt/Physics/Body/BodyID.h>
|
||||
#include <Jolt/Physics/Constraints/Constraint.h> // JPH::Ref<Constraint>
|
||||
#include <Jolt/Core/JobSystemThreadPool.h>
|
||||
#include <Jolt/Core/TempAllocator.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
namespace ECS { class Scene; struct RigidBodyComponent; struct TransformComponent; }
|
||||
namespace Assets { class AssetManager; }
|
||||
|
||||
class PhysicsWorld {
|
||||
public:
|
||||
// assets:Mesh 碰撞体解析 modelRef(MeshShape/ConvexHull 数据源)
|
||||
explicit PhysicsWorld(ECS::Scene& scene, Assets::AssetManager* assets);
|
||||
~PhysicsWorld();
|
||||
|
||||
PhysicsWorld(const PhysicsWorld&) = delete;
|
||||
PhysicsWorld& operator=(const PhysicsWorld&) = delete;
|
||||
|
||||
// 帧循环调用(OnUpdate 之前,Unity FixedUpdate→Update 语义)
|
||||
void Step(f32 dt);
|
||||
|
||||
void SetEnabled(bool enabled) { m_Enabled = enabled; }
|
||||
bool IsEnabled() const { return m_Enabled; }
|
||||
|
||||
// ---- 物理调试可视化(ImGui 开关;位掩码见 PhysicsDebugBits)----
|
||||
// Step 尾部按位收集图元(渲染帧率),Renderer 每帧上传绘制
|
||||
void SetDebugDrawBits(u32 bits) { m_DebugBits = bits; }
|
||||
u32 GetDebugDrawBits() const { return m_DebugBits; }
|
||||
PhysicsDebugFrame& GetDebugFrame() { return m_DebugDraw->Frame(); }
|
||||
|
||||
// ---- 碰撞事件(语义 = 最近一次步进;Step 开头清空)----
|
||||
// 主线程在 Step 之外读取,与多线程回调互不重叠,无需加锁
|
||||
const std::vector<CollisionEvent>& GetEvents() const {
|
||||
return m_PendingEvents;
|
||||
}
|
||||
|
||||
// ---- 射线查询(游戏查询/编辑器拾取;主线程调用,暂停时也可用)----
|
||||
// dir 会被内部归一化;maxDist 限制射程。命中返回 true 并填 outHit
|
||||
bool RayCast(const glm::vec3& origin, const glm::vec3& dir,
|
||||
f32 maxDist, RayHit& outHit) const;
|
||||
|
||||
// 仅命中静态(非移动层)几何:忽略碎块/动态物/NPC。相机防穿墙用它
|
||||
// ——动态碎块撞进枢轴→相机射线会把镜头乱拉(抖动),静态语义才稳
|
||||
bool RayCastStatic(const glm::vec3& origin, const glm::vec3& dir,
|
||||
f32 maxDist, RayHit& outHit) const;
|
||||
|
||||
// ---- 刚体驱动(游戏逻辑/编辑器调试;按实体位模式寻址)----
|
||||
// Jolt 力在每步模拟后清零:游戏逻辑每帧持续施加 = 正确用法;
|
||||
// 冲量一次性改变动量(自动唤醒休眠体);速度直写会打断自然
|
||||
// 模拟(Unity 同语义)。body 不存在或类型不符时静默忽略:
|
||||
// 力/扭矩/冲量仅动态体,速度直写动态+运动学体
|
||||
void AddForce(u32 entityBits, const glm::vec3& force);
|
||||
void AddImpulse(u32 entityBits, const glm::vec3& impulse);
|
||||
void AddTorque(u32 entityBits, const glm::vec3& torque);
|
||||
void SetLinearVelocity(u32 entityBits, const glm::vec3& velocity);
|
||||
void SetAngularVelocity(u32 entityBits, const glm::vec3& angularVelocity);
|
||||
// 无 body / 静态体返回零向量
|
||||
glm::vec3 GetLinearVelocity(u32 entityBits) const;
|
||||
|
||||
// ---- 角色控制器(游戏逻辑门面;实体无 CharacterControllerComponent
|
||||
// 或未就绪时静默忽略)----
|
||||
// 速度直写喂意图(水平移动);垂直分量由 Update 自动积分重力
|
||||
void SetCharacterVelocity(u32 entityBits, const glm::vec3& velocity);
|
||||
void Jump(u32 entityBits, f32 speed); // 垂直速度置 speed
|
||||
bool IsCharacterGrounded(u32 entityBits) const;
|
||||
glm::vec3 GetCharacterVelocity(u32 entityBits) const;
|
||||
// 角色脚底位置(物理权威世界坐标):调试图元/特效锚点用它,
|
||||
// 而不是 Transform().GetPosition()——后者在有父级时是本地坐标
|
||||
glm::vec3 GetCharacterPosition(u32 entityBits) const;
|
||||
// 角色"撞击强度"速度(上一次扫掠前的速度):撞墙/碎墙等判定用它,
|
||||
// 而非 GetCharacterVelocity——后者已被 Jolt 抵消掉朝向墙面的分量
|
||||
glm::vec3 GetCharacterImpactVelocity(u32 entityBits) const;
|
||||
|
||||
private:
|
||||
// 创建时缓存的参数(变更检测 → 重建 body)
|
||||
struct BodyRecord {
|
||||
JPH::BodyID id{};
|
||||
u8 type = 0; // RigidBodyComponent::BodyType
|
||||
u8 collider = 0; // RigidBodyComponent::ColliderType
|
||||
glm::vec3 colliderSize{0.0f}; // 碰撞体尺寸缓存(box 半尺寸)
|
||||
f32 radius = 0.0f; // 球/Capsule/Cylinder 半径(collider 决定语义)
|
||||
f32 halfHeight = 0.0f; // Capsule/Cylinder 圆柱段半高
|
||||
std::vector<ECS::RigidBodyComponent::CompoundChild> compound; // 子形状快照
|
||||
f32 mass = 0.0f;
|
||||
bool isTrigger = false; // 触发器标记(变更 → 重建 body)
|
||||
glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测
|
||||
glm::quat lastRot{1.0f, 0.0f, 0.0f, 0.0f}; // 同上(quat 直比,无欧拉往返)
|
||||
// 加速度箭头用:上一步速度(定步长差分)与平滑后的加速度
|
||||
glm::vec3 prevVel{0.0f};
|
||||
glm::vec3 lastAccel{0.0f};
|
||||
};
|
||||
|
||||
void SyncScene(); // 对账:孤儿清理 / 创建 / 参数同步
|
||||
void Writeback(); // 动态体 → TransformComponent(标脏)+ lastPos/lastRot 对账基线
|
||||
void UpdateAccels(f32 dt); // 速度差分 → BodyRecord.lastAccel(step 率)
|
||||
void CollectDebugData(); // 按 m_DebugBits 收集调试图元(渲染帧率)
|
||||
void CreateBody(u32 entityBits, ECS::RigidBodyComponent& rb,
|
||||
ECS::TransformComponent& tr);
|
||||
// 约束对账:owner 孤儿清理 / body 重建传染失效 / 建缺失与变更重建
|
||||
// invalidated = 本帧 SyncScene 中被销毁或重建 body 的实体位模式
|
||||
void SyncConstraints(const std::unordered_set<u32>& invalidated);
|
||||
void CreateConstraint(u32 ownerBits, u32 targetBits,
|
||||
const ECS::JointComponent& jc);
|
||||
void RemoveConstraint(u32 ownerBits);
|
||||
// 角色对账:孤儿清理 / 参数变更重建 / 编辑器拖动传送。
|
||||
// 角色非刚体(不进 PhysicsSystem),无 body 清理负担
|
||||
void SyncCharacters();
|
||||
// 每物理步(PhysicsSystem::Update 之后,Jolt 官方建议序):
|
||||
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)+ 合成角色
|
||||
// 碰撞事件(CharacterVirtual 不进 ContactListener,见函数注释)
|
||||
void UpdateCharacters(f32 dt);
|
||||
// Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape /
|
||||
// Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box)
|
||||
JPH::RefConst<JPH::Shape> BuildMeshShape(u32 entityBits,
|
||||
ECS::RigidBodyComponent& rb,
|
||||
ECS::TransformComponent& tr);
|
||||
// Compound 碰撞体:子形状列表 → Jolt StaticCompoundShape。
|
||||
// 空 children 或构建失败返回 null(回退 Box)
|
||||
JPH::RefConst<JPH::Shape> BuildCompoundShape(ECS::RigidBodyComponent& rb,
|
||||
ECS::TransformComponent& tr);
|
||||
void RemoveBody(u32 entityBits);
|
||||
// RayCast / RayCastStatic 共用实现:layerFilter 为空 = 命中所有层
|
||||
bool RayCastImpl(const glm::vec3& origin, const glm::vec3& dir,
|
||||
f32 maxDist, RayHit& outHit,
|
||||
const JPH::ObjectLayerFilter* layerFilter) const;
|
||||
const BodyRecord* FindRecord(u32 entityBits) const; // 刚体驱动寻址
|
||||
CharacterController* FindCharacter(u32 entityBits); // 角色门面寻址
|
||||
void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁)
|
||||
bool TryBitsByBodyId(const JPH::BodyID& id, u32& outBits) const;
|
||||
|
||||
// ---- 约束(JointComponent → Jolt TwoBodyConstraint;owner 实体寻址)----
|
||||
// target 惰性解析 UUID;body 重建/销毁 → 引用它的约束失效重建
|
||||
struct ConstraintRecord {
|
||||
JPH::Ref<JPH::Constraint> constraint;
|
||||
ECS::JointComponent snapshot; // 变更检测 → 移除重建
|
||||
u32 targetBits = 0;
|
||||
};
|
||||
std::unordered_map<u32, ConstraintRecord> m_Constraints;
|
||||
|
||||
// ---- 角色(entityBits → CharacterVirtual 封装)----
|
||||
// 角色间碰撞互查表:Jolt CharacterVirtual 默认彼此穿透,须显式注入
|
||||
// CharacterVsCharacterCollisionSimple 才互撞。声明在 m_Characters
|
||||
// 之前(析构更晚)——角色析构时会回表 Remove,列表须后于角色释放
|
||||
Scope<JPH::CharacterVsCharacterCollisionSimple> m_VsCharacterCollision;
|
||||
struct CharacterEntry {
|
||||
std::unique_ptr<CharacterController> cc;
|
||||
glm::vec3 lastPos{0.0f}; // 编辑器拖动检测
|
||||
// 上一步活跃接触的"对方实体"集合:本步对比出角色碰撞
|
||||
// Enter/Exit(CharacterVirtual 非刚体,ContactListener 不收)
|
||||
std::unordered_set<u32> contacts;
|
||||
};
|
||||
std::unordered_map<u32, CharacterEntry> m_Characters;
|
||||
|
||||
ECS::Scene& m_Scene;
|
||||
Assets::AssetManager* m_Assets = nullptr; // Mesh 碰撞体解析 modelRef
|
||||
bool m_Enabled = true;
|
||||
f32 m_Accumulator = 0.0f;
|
||||
|
||||
// 层配置实现类在 cpp;接口必须比 PhysicsSystem 活得久
|
||||
Scope<JPH::BroadPhaseLayerInterface> m_BPLayerInterface;
|
||||
Scope<JPH::ObjectVsBroadPhaseLayerFilter> m_ObjectVsBPLayerFilter;
|
||||
Scope<JPH::ObjectLayerPairFilter> m_ObjectVsObjectLayerFilter;
|
||||
|
||||
Scope<JPH::TempAllocator> m_TempAllocator;
|
||||
Scope<JPH::JobSystem> m_JobSystem;
|
||||
Scope<JPH::PhysicsSystem> m_System;
|
||||
|
||||
std::unordered_map<u32, BodyRecord> m_Bodies; // entity 位模式 → body
|
||||
|
||||
// BodyID 序号 → entity 位模式(O(1) 反查;ContactListener 回调用)
|
||||
// 与 m_Bodies 同生共死:CreateBody 同插,RemoveBody/孤儿清理同 erase。
|
||||
// 主线程在 SyncScene 写、Step 内 Update 阶段不写 → 回调线程只读,无需加锁
|
||||
std::unordered_map<u32, u32> m_BodyToEntity;
|
||||
|
||||
// ---- 碰撞事件收集(多线程回调入队,主线程消费)----
|
||||
class ContactListenerImpl; // cpp 定义(JPH::ContactListener 派生)
|
||||
Scope<ContactListenerImpl> m_ContactListener;
|
||||
std::mutex m_EventMutex; // 保护 m_PendingEvents(回调并发)
|
||||
std::vector<CollisionEvent> m_PendingEvents; // 最近一次步进的事件
|
||||
|
||||
// ---- 物理调试可视化 ----
|
||||
// 堆上延迟构造:DebugRendererSimple 构造即预烘焙通用几何(走 JPH
|
||||
// 分配器),必须等 ctor 内 call_once 注册完分配器,见 PhysicsWorld.cpp
|
||||
Scope<PhysicsDebugDraw> m_DebugDraw; // Jolt 全局调试渲染后端(收集器)
|
||||
u32 m_DebugBits = 0; // PhysicsDebugBits(默认关,StatsPanel 开关)
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSWORLD_H
|
||||
@@ -16,7 +16,6 @@
|
||||
//
|
||||
// 已知边界(后续演进点):
|
||||
// - 暂不支持异步 compute queue / 跨队列信号量
|
||||
// - 暂不支持内存别名(需 VMA)与帧历史资源(TAA 等)
|
||||
// - 同一 pass 内对同一资源"读+写"(input attachment / RWM)未建模
|
||||
//
|
||||
|
||||
@@ -70,8 +69,11 @@ namespace Engine::RG {
|
||||
|
||||
struct TextureDesc {
|
||||
vk::Format format = vk::Format::eUndefined;
|
||||
u32 width = 0; // 0 = 跟随目标(swapchain)尺寸
|
||||
u32 width = 0; // 0 = 跟随目标(swapchain)尺寸 × sizeScale
|
||||
u32 height = 0;
|
||||
f32 sizeScale = 1.0f; // 相对尺寸(后处理降采样用,如 bloom 0.5)
|
||||
// 附加 usage(在 pass 读写推导之外;如调试截图需要 TRANSFER_SRC)
|
||||
vk::ImageUsageFlags extraUsage{};
|
||||
};
|
||||
|
||||
// ---------- 物理镜像 ----------
|
||||
@@ -96,6 +98,12 @@ 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, 历史槽位) 预绑(同阴影图模式)。
|
||||
u32 HistorySlot(u32 frameSlot) const;
|
||||
|
||||
class RenderGraph* graph = nullptr;
|
||||
};
|
||||
@@ -123,6 +131,7 @@ namespace Engine::RG {
|
||||
// 导入资源每帧绑定
|
||||
vk::Image importedImage{};
|
||||
vk::ImageView importedView{};
|
||||
vk::Extent2D importedExtent{}; // 物理尺寸(swapchain 重建后更新)
|
||||
|
||||
// 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体)
|
||||
struct SlotState {
|
||||
@@ -130,6 +139,10 @@ namespace Engine::RG {
|
||||
};
|
||||
std::vector<SlotState> slots;
|
||||
|
||||
// 是否存在帧历史读(Compile 期校验:历史资源必须有同帧正常使用,
|
||||
// 否则没有物理镜像可读)
|
||||
bool hasHistoryUse = false;
|
||||
|
||||
// Compile 期状态链解算用(执行序上的"当前"状态)
|
||||
vk::ImageLayout chainLayout = vk::ImageLayout::eUndefined;
|
||||
vk::PipelineStageFlags2 chainStage{};
|
||||
@@ -149,22 +162,31 @@ namespace Engine::RG {
|
||||
u32 resIndex;
|
||||
ResourceUse use;
|
||||
vk::ShaderStageFlags readStage; // ShaderRead 时的消费 stage
|
||||
bool history = false; // 帧历史读:读上一帧槽位的物理镜像
|
||||
};
|
||||
std::vector<Use> uses;
|
||||
ExecuteFn exec;
|
||||
bool sideEffect = false; // 无读写但必须执行(compute 写 buffer 等)
|
||||
bool culled = false;
|
||||
vk::Extent2D extent{}; // 本 pass 的渲染尺寸(写入附件的最大分辨率,Compile 期解析)
|
||||
|
||||
// Compile 期预算好的 barrier:每个资源两个变体
|
||||
// steady = 该槽位已被使用过(从 resting 状态出发)
|
||||
// first = 该槽位首次使用(从 Undefined 出发)
|
||||
// imported 资源恒用 first(acquire 后 layout 语义上为 Undefined)
|
||||
// history = 帧历史读(操作上一帧槽位的镜像,steady 变体带跨帧可见性同步)
|
||||
struct BarrierPair {
|
||||
u32 resIndex;
|
||||
vk::ImageMemoryBarrier2 steady;
|
||||
vk::ImageMemoryBarrier2 first;
|
||||
bool history = false;
|
||||
};
|
||||
std::vector<BarrierPair> barriers;
|
||||
|
||||
// 历史读的出口 barrier(exec 之后执行):历史镜像被临时转进读
|
||||
// layout 采样,读完必须恢复 resting layout,否则下一帧该槽位
|
||||
// entry barrier 的 oldLayout 与实际 layout 脱节
|
||||
std::vector<BarrierPair> postBarriers;
|
||||
};
|
||||
|
||||
// ---------- RenderGraph ----------
|
||||
@@ -188,11 +210,21 @@ 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 管理的资源绑定到外部描述符(如阴影图采样)。
|
||||
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
|
||||
vk::ImageView GetPhysicalView(TextureHandle h, u32 frameSlot) const;
|
||||
|
||||
// 帧历史槽位 = 上一帧使用的槽位:(slot - 1 + F) mod F
|
||||
u32 HistorySlot(u32 frameSlot) const {
|
||||
return (frameSlot + m_FramesInFlight - 1) % m_FramesInFlight;
|
||||
}
|
||||
|
||||
// ---- Pass 声明 ----
|
||||
|
||||
class PassBuilder {
|
||||
@@ -200,6 +232,12 @@ namespace Engine::RG {
|
||||
PassBuilder& Write(TextureHandle h, ResourceUse use);
|
||||
PassBuilder& Read(TextureHandle h, ResourceUse use,
|
||||
vk::ShaderStageFlags stage = vk::ShaderStageFlagBits::eFragment);
|
||||
// 帧历史读:读上一帧槽位的物理镜像(TAA/运动模糊/时域滤波用)。
|
||||
// - 不产生图内边:跨帧依赖由同队列提交序 + 历史 barrier 保证
|
||||
// - 资源必须有同帧正常使用(否则无物理镜像),Compile 期校验
|
||||
// - 采样描述符在 pass 内用 GetPhysicalView(h, HistorySlot(slot)) 预绑
|
||||
PassBuilder& ReadHistory(TextureHandle h, ResourceUse use,
|
||||
vk::ShaderStageFlags stage = vk::ShaderStageFlagBits::eFragment);
|
||||
PassBuilder& SideEffect();
|
||||
|
||||
private:
|
||||
@@ -225,6 +263,31 @@ namespace Engine::RG {
|
||||
// 每帧执行:按编译好的顺序录命令。要求 cmd 处于 recording 状态。
|
||||
void Execute(const FrameContext& frame);
|
||||
|
||||
// GPU timestamp(可选):注入 pool 后,每个 pass 前后各写一枚
|
||||
// 时间戳,索引 = frameSlot * queriesPerFrame + 执行序位置 * 2。
|
||||
// pool 由调用方创建(容量 ≥ framesInFlight × queriesPerFrame),
|
||||
// 读取由调用方在本槽位 fence 等待之后进行。
|
||||
void SetQueryPool(vk::QueryPool pool, u32 queriesPerFrame);
|
||||
|
||||
// GPU profiler(可选):pass 边界的 GPU zone 钩子(Tracy 等工具的
|
||||
// 接缝,RG 本体不感知任何 profiler 实现)。BeginPass 返回的 scope
|
||||
// 析构 = 结束时间戳;注入者须保证 scope 生命周期覆盖 pass lambda。
|
||||
// EndFrame 在全部 pass 录制完后调用(仍在当帧 cmd 内,提交前)。
|
||||
class IGpuProfiler {
|
||||
public:
|
||||
virtual ~IGpuProfiler() = default;
|
||||
class IScope {
|
||||
public:
|
||||
virtual ~IScope() = default;
|
||||
};
|
||||
[[nodiscard]] virtual Scope<IScope> BeginPass(
|
||||
const char* name, vk::CommandBuffer cmd) = 0;
|
||||
virtual void EndFrame(vk::CommandBuffer cmd) = 0;
|
||||
};
|
||||
void SetGpuProfiler(IGpuProfiler* profiler) { m_GpuProfiler = profiler; }
|
||||
|
||||
std::vector<std::string> GetPassNames() const;
|
||||
|
||||
// ---- 统计 ----
|
||||
|
||||
u32 GetPassCount() const;
|
||||
@@ -233,6 +296,14 @@ namespace Engine::RG {
|
||||
u32 GetBarrierCount() const;
|
||||
u32 GetPooledImageCount() const;
|
||||
|
||||
// GPU timestamp:指定槽位上一轮实际写入的查询数(active pass × 2)。
|
||||
// 剔除使 active 数 < 总 pass 数——读侧必须按此数取结果,按满额
|
||||
// 阻塞读会等永远无人写入的查询(死锁)
|
||||
u32 GetWrittenQueries(u32 frameSlot) const
|
||||
{
|
||||
return frameSlot < 4 ? m_WrittenQueries[frameSlot] : 0;
|
||||
}
|
||||
|
||||
private:
|
||||
friend struct RenderGraphContext;
|
||||
|
||||
@@ -243,7 +314,10 @@ namespace Engine::RG {
|
||||
void ComputeLifetimes();
|
||||
void PrecomputeBarriers();
|
||||
void AllocatePhysical();
|
||||
void ReleasePhysical();
|
||||
// keepFixedSize = true:固定尺寸资源(desc 显式 width/height,与目标
|
||||
// 渲染尺寸无关)的物理镜像不归还池、保留在 entry 上——重编译后原样
|
||||
// 沿用同一 VkImage,内容存续(材质球预览 atlas 不因视口 resize 清空)
|
||||
void ReleasePhysical(bool keepFixedSize);
|
||||
void PurgePool(vk::Extent2D target);
|
||||
PhysicalImage* AcquirePhysical(vk::Format format, vk::Extent2D extent,
|
||||
vk::ImageUsageFlags usage,
|
||||
@@ -274,6 +348,12 @@ namespace Engine::RG {
|
||||
// 拓扑排序后的执行序(存 pass 下标)
|
||||
std::vector<u32> m_ExecutionOrder;
|
||||
|
||||
// GPU timestamp(可选注入)
|
||||
vk::QueryPool m_QueryPool{};
|
||||
u32 m_QueriesPerFrame = 0;
|
||||
u32 m_WrittenQueries[4] = {}; // 每槽位上一轮实际写入数(active×2)
|
||||
IGpuProfiler* m_GpuProfiler = nullptr; // 非拥有(Renderer 持有)
|
||||
|
||||
// 物理资源池:key = (format, extent, usage),value = 空闲镜像列表
|
||||
struct PoolKey {
|
||||
u32 format;
|
||||
|
||||
@@ -2,8 +2,20 @@
|
||||
// Created by atdunbg on 2026/9/22.
|
||||
//
|
||||
// Camera:独立的相机模块。
|
||||
// 职责:输入驱动的运动更新 + 视图/投影矩阵计算。
|
||||
// 不再依赖具体渲染器——Renderer/RenderGraph 只消费它的矩阵输出。
|
||||
//
|
||||
// 设计(Transform 单一事实源):相机的位置与朝向完全由实体上的
|
||||
// TransformComponent 承载——Update 把输入直接写回 transform,视图
|
||||
// 矩阵 = 实体世界矩阵的逆(本地 -Z 为视线方向)。相机类自身只保存
|
||||
// 焦点环绕(pivot orbit)状态(pivot/距离),杜绝位姿两套来源。
|
||||
//
|
||||
// 交互(UE Scene view 语义):
|
||||
// RMB 按住 自由视角:鼠标转向 + WASD/Space/Shift 飞行
|
||||
// (编辑器模式下 RMB 按住期间键盘归相机,W/E/R 不切
|
||||
// Gizmo——键位冲突的解法与 UE 一致)
|
||||
// MMB 拖拽 焦点环绕:绕 pivot 转向,距离不变
|
||||
// Shift+MMB 屏幕平面平移(相机与 pivot 同步移动)
|
||||
// 滚轮 推拉:改环绕距离(有 pivot 时指数步进)
|
||||
// F(面板侧) 焦点选中实体(ViewportPanel 调 FocusOn)
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_CAMERA_H
|
||||
@@ -14,51 +26,88 @@
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Window;
|
||||
namespace ECS { struct TransformComponent; }
|
||||
|
||||
// ---- 相机参数默认值(每实体相机参数的唯一初始来源;编辑器 UI /
|
||||
// 序列化缺省回退也引用它们,改默认值一处生效)----
|
||||
// 视场角(度,垂直 fov,Unity 语义;UE 默认水平 90° ≈ 垂直 45°@16:9)
|
||||
inline constexpr f32 kDefaultCameraFovDeg = 45.0f;
|
||||
// 裁剪面距离(米):near 主导深度精度,far 拉远到"无限"级不损近处
|
||||
inline constexpr f32 kDefaultCameraNear = 0.1f;
|
||||
inline constexpr f32 kDefaultCameraFar = 10000.0f;
|
||||
// 正交模式半垂直高度(Unity orthographic size 语义):世界单位米
|
||||
inline constexpr f32 kDefaultCameraOrthoSize = 10.0f;
|
||||
|
||||
class Camera {
|
||||
public:
|
||||
// 每帧调用:读取窗口输入,更新位置与朝向
|
||||
void Update(Window& window, f32 deltaTime);
|
||||
// 投影模式(UE/Unity Camera 语义:透视 = 近大远小;正交 =
|
||||
// 平行投影,工程图纸/2D/等距视角)
|
||||
enum class ProjectionMode : u8 { Perspective = 0, Orthographic = 1 };
|
||||
|
||||
glm::mat4 GetView() const;
|
||||
// 输入驱动(键鼠查询统一走 Input 门面,零 Window 依赖)。
|
||||
// flyRequiresRMB=true(编辑器模式):移动键仅在 RMB 按住时生效
|
||||
// ——解决与 Gizmo W/E/R 工具热键的键位冲突(UE 语义:飞行时
|
||||
// 键盘归相机)。全部写回传入的 transform
|
||||
void Update(ECS::TransformComponent& transform,
|
||||
f32 deltaTime, bool flyRequiresRMB = false);
|
||||
|
||||
// Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度
|
||||
glm::mat4 GetProjection(f32 aspect, f32 nearZ, f32 farZ) const;
|
||||
// 焦点选中(UE frame-selection):pivot 设为 point,相机沿当前
|
||||
// 视线平移对准;环绕距离沿用当前值(初始 5m,滚轮可改)
|
||||
void FocusOn(ECS::TransformComponent& transform, const glm::vec3& point);
|
||||
|
||||
glm::vec3 GetPosition() const { return m_Position; }
|
||||
void SetPosition(const glm::vec3& pos) { m_Position = pos; }
|
||||
// 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose)
|
||||
glm::mat4 GetView(const ECS::TransformComponent& transform) const;
|
||||
|
||||
void SetYaw(f32 yaw) { m_Yaw = yaw; }
|
||||
void SetPitch(f32 pitch) { m_Pitch = pitch; }
|
||||
f32 GetYaw() const { return m_Yaw; }
|
||||
f32 GetPitch() const { return m_Pitch; }
|
||||
// Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度。
|
||||
// fov/near/far/orthoSize/模式全部走成员(每实体参数),aspect 由
|
||||
// 视口推导不落盘(UE/Unity 同构)
|
||||
glm::mat4 GetProjection(f32 aspect) const;
|
||||
|
||||
// ---- 投影参数(Inspector/序列化直接读写)----
|
||||
void SetFovDeg(f32 fov) { m_FovDeg = glm::clamp(fov, 1.0f, 179.0f); }
|
||||
f32 GetFovDeg() const { return m_FovDeg; }
|
||||
// near/far 成对写:内部保证 far > near(UI 拖动交叉时防穿透)
|
||||
void SetClip(f32 nearZ, f32 farZ)
|
||||
{
|
||||
m_NearZ = glm::max(nearZ, 0.001f);
|
||||
m_FarZ = glm::max(farZ, m_NearZ + 0.01f);
|
||||
}
|
||||
f32 GetNearZ() const { return m_NearZ; }
|
||||
f32 GetFarZ() const { return m_FarZ; }
|
||||
// 正交半垂直高度(>0;aspect 由视口推导出全宽)
|
||||
void SetOrthoSize(f32 s) { m_OrthoSize = glm::max(s, 0.01f); }
|
||||
f32 GetOrthoSize() const { return m_OrthoSize; }
|
||||
void SetProjectionMode(ProjectionMode m) { m_ProjectionMode = m; }
|
||||
ProjectionMode GetProjectionMode() const { return m_ProjectionMode; }
|
||||
|
||||
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
|
||||
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
|
||||
|
||||
// 鼠标视角默认关闭(需要先做 cursor capture 再打开)
|
||||
void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; }
|
||||
// 相机输入总开关(默认开)。关闭后转向/环绕/推拉/飞行全部静止
|
||||
void SetMouseLookEnabled(const bool enabled) { m_MouseLookEnabled = enabled; }
|
||||
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
|
||||
|
||||
glm::vec3 GetForward() const { return m_Forward; }
|
||||
glm::vec3 GetRight() const { return m_Right; }
|
||||
glm::vec3 GetUp() const { return m_Up; }
|
||||
// 清除环绕 pivot(位姿被外部整体改写后 pivot 已失效——下一次
|
||||
// MMB 环绕按新视线自动重建)
|
||||
void ResetOrbit() { m_HasPivot = false; }
|
||||
|
||||
private:
|
||||
void UpdateBasis();
|
||||
|
||||
glm::vec3 m_Position{0.0f, 0.0f, 7.0f};
|
||||
f32 m_Yaw = -90.0f; // 朝向 -Z
|
||||
f32 m_Pitch = 0.0f;
|
||||
f32 m_MoveSpeed = 2.5f;
|
||||
f32 m_MouseSensitivity = 0.1f;
|
||||
bool m_MouseLookEnabled = false;
|
||||
bool m_MouseLookEnabled = true;
|
||||
|
||||
// 缓存的方向基向量,Update 时刷新,避免每帧三角函数重复计算
|
||||
glm::vec3 m_Forward{0.0f, 0.0f, -1.0f};
|
||||
glm::vec3 m_Right{1.0f, 0.0f, 0.0f};
|
||||
glm::vec3 m_Up{0.0f, 1.0f, 0.0f};
|
||||
// ---- 投影参数(每实体;Transform 只管位姿,这里只管投影)----
|
||||
f32 m_FovDeg = kDefaultCameraFovDeg;
|
||||
f32 m_NearZ = kDefaultCameraNear;
|
||||
f32 m_FarZ = kDefaultCameraFar;
|
||||
f32 m_OrthoSize = kDefaultCameraOrthoSize;
|
||||
ProjectionMode m_ProjectionMode = ProjectionMode::Perspective;
|
||||
|
||||
// 焦点环绕状态:pivot + 距离。首次环绕无 pivot 时自动取当前
|
||||
// 视线上 m_OrbitDistance 处;FocusOn 显式设置
|
||||
glm::vec3 m_OrbitPivot{0.0f};
|
||||
f32 m_OrbitDistance = 5.0f;
|
||||
bool m_HasPivot = false;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// Cubemap:环境立方体贴图 + IBL 预积分资源(split-sum 四件套)。
|
||||
//
|
||||
// 纯 GPU 资源持有类(对齐 Model 的定位):只管 image/view/采样器的
|
||||
// 同生共死,不做任何生成/上传。预积分由 IblGenerator(compute 链)
|
||||
// 产出后注入——环境切换的入口在 Assets 层(AssetManager::LoadEnvironment)。
|
||||
//
|
||||
// env cube —— 重采样 6 面(Skybox 背景)
|
||||
// irradiance cube —— 半球 Lambert 积分(PBR 漫反射 ambient)
|
||||
// prefilter cube —— GGX 重要性采样 × mip 分级 roughness(镜面 IBL)
|
||||
// BRDF LUT —— 2D 积分表(纯数学,与环境内容无关)
|
||||
// 全部 fp16,运行时采样零积分开销。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_CUBEMAP_H
|
||||
#define HELLOVULKAN_CUBEMAP_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
|
||||
// 环境的 CPU 源数据(等距柱状 HDR 解码产物,RGBA float 行优先)。
|
||||
// 纯数据无线程状态:异步加载时由后台线程产出(DecodeHdr),
|
||||
// 主线程消费(FromDecoded → GPU 生成)。decode 是切换耗时大头,
|
||||
// 搬出主线程后运行时切换只剩 GPU 段(~40ms)。
|
||||
struct DecodedEquirect {
|
||||
std::vector<f32> pixels;
|
||||
u32 w = 0;
|
||||
u32 h = 0;
|
||||
bool Valid() const { return !pixels.empty() && w > 0 && h > 0; }
|
||||
};
|
||||
|
||||
// GPU 纹理资源三元组(RHI 惯例):image/view/显存分配同生共死,
|
||||
// 析构统一释放——杜绝"建了 image 丢了 view"类半套泄漏。
|
||||
// 上传 helper 直接以此为单位产出,消费端只拿引用计数语义的整单元
|
||||
struct EnvTexture {
|
||||
vk::Image image{};
|
||||
vk::ImageView view{};
|
||||
MemoryAllocator::Allocation allocation;
|
||||
};
|
||||
|
||||
class Cubemap {
|
||||
public:
|
||||
~Cubemap();
|
||||
|
||||
Cubemap(const Cubemap&) = delete;
|
||||
Cubemap& operator=(const Cubemap&) = delete;
|
||||
|
||||
// ---- 消费端契约(SkyboxPass:背景;SceneGpu:PBR ambient)----
|
||||
vk::ImageView GetView() const { return m_Env.view; } // 环境 cube
|
||||
vk::ImageView GetIrradianceView() const { return m_Irr.view; } // 漫反射
|
||||
vk::ImageView GetPrefilteredView() const { return m_Pref.view; } // 镜面
|
||||
vk::ImageView GetBrdfLutView() const { return m_Lut.view; } // LUT
|
||||
|
||||
// cube 资源共享采样器(maxLod 覆盖 pre-filter mip 链)
|
||||
vk::Sampler GetSampler() const { return m_EnvSampler; }
|
||||
// LUT 专用采样器(clamp-to-edge):共享的 border 白采样器会在
|
||||
// uv=1.0 处混入 50% 白色 border,导致高 roughness 镜面偏大
|
||||
vk::Sampler GetBrdfLutSampler() const { return m_LutSampler; }
|
||||
|
||||
private:
|
||||
friend class IblGenerator; // 生成端唯一构造入口
|
||||
|
||||
Cubemap(Device& device, MemoryAllocator& allocator,
|
||||
EnvTexture env, EnvTexture irr, EnvTexture pref,
|
||||
EnvTexture lut, vk::Sampler envSampler, vk::Sampler lutSampler)
|
||||
: m_Device(device), m_Allocator(allocator),
|
||||
m_Env(env), m_Irr(irr), m_Pref(pref), m_Lut(lut),
|
||||
m_EnvSampler(envSampler), m_LutSampler(lutSampler) {}
|
||||
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
|
||||
EnvTexture m_Env; // 环境 cube(Skybox 背景)
|
||||
EnvTexture m_Irr; // 漫反射辐照度 cube
|
||||
EnvTexture m_Pref; // pre-filtered cube(mip 分 roughness)
|
||||
EnvTexture m_Lut; // BRDF 积分 LUT(2D)
|
||||
vk::Sampler m_EnvSampler;
|
||||
vk::Sampler m_LutSampler;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_CUBEMAP_H
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/10/3.
|
||||
//
|
||||
// EditorCamera:Scene view 专用相机(UE FEditorViewportClient 语义)。
|
||||
//
|
||||
// 与场景相机实体是两个东西——它是视口 UI 状态,不属于场景:
|
||||
// - 位姿自有(内嵌 TransformComponent,无父层级),空场景/无相机
|
||||
// 实体也可用,永在不可删
|
||||
// - 交互复用 Camera 控制器(RMB 飞行 / MMB 环绕 / 滚轮推拉),输入
|
||||
// 直接写回自有 transform——位姿单一事实源不变(transform 即位姿)
|
||||
// - 渲染源分派在 SceneRenderer::BuildFrame:编辑器形态恒用本类,
|
||||
// 游戏形态用场景 primary 相机实体(游戏逻辑所有,本类不参与,
|
||||
// 不 Update 即静止)
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_EDITORCAMERA_H
|
||||
#define HELLOVULKAN_EDITORCAMERA_H
|
||||
|
||||
#include <Engine/Renderer/Camera.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class EditorCamera {
|
||||
public:
|
||||
EditorCamera()
|
||||
{
|
||||
// 出场位姿:俯视场中央(与旧沙盒 MainCamera 出场一致,
|
||||
// 空场景也有可看的初始视角)
|
||||
m_Transform.SetPosition({0.0f, 12.0f, 22.0f});
|
||||
m_Transform.SetRotation({-25.0f, 0.0f, 0.0f});
|
||||
m_Transform.Compose();
|
||||
}
|
||||
|
||||
// 每帧输入驱动(编辑器形态由 Application 调用;游戏形态不调
|
||||
// 即静止)。flyRequiresRMB 恒 true:编辑器移动键只在 RMB 按住
|
||||
// 时生效(W/E/R 归 Gizmo,UE 语义)。暂停(timeScale=0)时
|
||||
// 仍可飞行——调用方传 unscaled dt。键鼠/光标捕获统一走 Input
|
||||
void Update(f32 deltaTime)
|
||||
{
|
||||
m_Controller.Update(m_Transform, deltaTime,
|
||||
/*flyRequiresRMB=*/true);
|
||||
m_Transform.Compose(); // World() 是渲染/Gizmo 读源,随写随算
|
||||
}
|
||||
|
||||
// 跳转(对齐视口):把相机实体(或任意实体)位姿复制过来,
|
||||
// Scene view 立即从该位置观看(UE "Pilot This Camera" 的
|
||||
// 一次性对齐版)。环绕 pivot 已失效,清除待 MMB 重建
|
||||
void AlignTo(const ECS::TransformComponent& target)
|
||||
{
|
||||
// world 位姿复制(目标实体可能在层级子树下)
|
||||
m_Transform.SetPosition(target.GetWorldPosition());
|
||||
m_Transform.SetRotationQuat(target.GetWorldRotationQuat());
|
||||
m_Transform.Compose();
|
||||
m_Controller.ResetOrbit();
|
||||
}
|
||||
|
||||
// 焦点选中(F 键):pivot 设为 point,沿当前视线平移对准
|
||||
void FocusOn(const glm::vec3& point)
|
||||
{
|
||||
m_Controller.FocusOn(m_Transform, point);
|
||||
}
|
||||
|
||||
// 视图 = 自有 transform 世界矩阵的逆(要求已 Compose——
|
||||
// Update/AlignTo/ctor 三个写入口都收尾 Compose)
|
||||
glm::mat4 GetView() const { return m_Controller.GetView(m_Transform); }
|
||||
|
||||
ECS::TransformComponent& GetTransform() { return m_Transform; }
|
||||
Camera& GetController() { return m_Controller; }
|
||||
|
||||
private:
|
||||
ECS::TransformComponent m_Transform; // 自有位姿(非场景实体)
|
||||
Camera m_Controller; // 交互控制器(环绕状态在此)
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_EDITORCAMERA_H
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// Frustum:Gribb-Hartmann 视锥平面提取 + 包围球相交测试。
|
||||
//
|
||||
// 平面从 view-proj 矩阵的行组合中直接抽出(无需分解参数),
|
||||
// 用于按视图剔除(相机视图 / 光源视图各自独立一份)。
|
||||
// 注意:near/far 平面按 Vulkan [0,1] 深度约定提取
|
||||
//(z_clip ∈ [0,1]:near = row2,far = row3 - row2)——
|
||||
// 因此必须传已做 [0,1] 重映射的矩阵(相机 proj / lightSpace 均是)。
|
||||
// 平面在提取后归一化:包围球测试的 radius 是欧氏长度,
|
||||
// 未归一化的平面距离会被透视因数放大,导致视锥边缘误剔。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_FRUSTUM_H
|
||||
#define HELLOVULKAN_FRUSTUM_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <array>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
struct Frustum {
|
||||
// 依次:left, right, bottom, top, near, far
|
||||
// xyz = 单位法线,w = 原点到平面的有符号距离
|
||||
std::array<glm::vec4, 6> planes{};
|
||||
|
||||
// 从 view-proj 矩阵提取 6 平面(列主序 glm 矩阵的"行"组合)。
|
||||
// 必须传 [0,1] 深度重映射后的矩阵。
|
||||
static Frustum FromMatrix(const glm::mat4& m)
|
||||
{
|
||||
Frustum f;
|
||||
// 列主序下的"行 i": (m[0][i], m[1][i], m[2][i], m[3][i])
|
||||
const auto row = [&m](u32 i) {
|
||||
return glm::vec4(m[0][i], m[1][i], m[2][i], m[3][i]);
|
||||
};
|
||||
const glm::vec4 r0 = row(0);
|
||||
const glm::vec4 r1 = row(1);
|
||||
const glm::vec4 r2 = row(2);
|
||||
const glm::vec4 r3 = row(3);
|
||||
|
||||
f.planes[0] = r3 + r0; // left: x' >= -w
|
||||
f.planes[1] = r3 - r0; // right: x' <= w
|
||||
f.planes[2] = r3 + r1; // bottom: y' >= -w
|
||||
f.planes[3] = r3 - r1; // top: y' <= w
|
||||
f.planes[4] = r2; // near: z' >= 0(Vulkan [0,1])
|
||||
f.planes[5] = r3 - r2; // far: z' <= w
|
||||
|
||||
// 归一化:法线长度把透视/缩放因数从距离里除掉
|
||||
for (auto& p : f.planes) {
|
||||
const f32 len = glm::length(glm::vec3(p));
|
||||
if (len > 1e-8f) {
|
||||
p /= len;
|
||||
}
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
// 包围球与视锥相交测试(外接球近似,略保守——宁多画不漏画)。
|
||||
// 对每个平面:球心在平面负侧且距离超过 radius 才算外。
|
||||
bool IntersectsSphere(const glm::vec3& center, f32 radius) const
|
||||
{
|
||||
for (const auto& p : planes) {
|
||||
if (glm::dot(glm::vec3(p), center) + p.w < -radius) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_FRUSTUM_H
|
||||
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/24.
|
||||
//
|
||||
// IblGenerator:IBL 预积分的 compute 生成端(原 Cubemap CPU 积分路径的
|
||||
// GPU 化,"动态环境切换零卡顿"第一步)。
|
||||
//
|
||||
// 管线:等距柱状 HDR → 上传 equirect 中间纹理(免 CPU 逐像素转换)
|
||||
// → 一次性提交四段 dispatch:
|
||||
// ① equirect_to_cube(硬件过滤重采样 6 面)
|
||||
// ② irradiance(半球 Lambert 卷积,采样 env cube)
|
||||
// ③ prefilter × mip(GGX 重要性采样,push roughness)
|
||||
// ④ BRDF LUT(纯数学)
|
||||
// → fence 等待 → 产出 Cubemap(四件套 fp16 资源)。
|
||||
//
|
||||
// 为什么不进 RenderGraph:RG 管理的是帧内 2D 瞬时资源(无 cube/数组层/
|
||||
// storage 写/条件执行),而预积分是"资产加载时跑一次"的持久资源——
|
||||
// 独立一次性提交与原上传路径同形态,侵入最小。残留卡顿 = fence 等待
|
||||
// (毫秒级),异步 compute queue / IO 线程解码是后续演进点。
|
||||
//
|
||||
// 产出与消费端契约(SkyboxPass / SceneGpu set2 binding3-5)不变:
|
||||
// view 形态与原 CPU 路径完全一致,绑定代码零改动。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_IBLGENERATOR_H
|
||||
#define HELLOVULKAN_IBLGENERATOR_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Cubemap.h> // DecodedEquirect(解码产物数据契约)
|
||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class DescriptorManager;
|
||||
class Cubemap;
|
||||
|
||||
class IblGenerator {
|
||||
public:
|
||||
IblGenerator(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptors, ShaderManager& shaders);
|
||||
~IblGenerator();
|
||||
|
||||
IblGenerator(const IblGenerator&) = delete;
|
||||
IblGenerator& operator=(const IblGenerator&) = delete;
|
||||
|
||||
// ---- 同步便捷路径(等价 DecodeHdr + FromDecoded)----
|
||||
// 等距柱状 HDR → 环境四件套(失败返回 nullptr,上层走 Neutral 兜底)
|
||||
Scope<Cubemap> FromHdrEquirect(const std::string& path, u32 faceSize);
|
||||
|
||||
// ---- 异步加载两段式(decode 是切换耗时大头,可搬后台线程)----
|
||||
// 纯 CPU:HDR 文件 → 像素数据。无线程状态,worker 线程可安全调用。
|
||||
static DecodedEquirect DecodeHdr(const std::string& path);
|
||||
|
||||
// GPU 生成(必须主线程):预解码数据 → 四件套。debugName 仅用于日志。
|
||||
Scope<Cubemap> FromDecoded(const DecodedEquirect& decoded, u32 faceSize,
|
||||
const std::string& debugName);
|
||||
|
||||
// 中性兜底:常数色环境 + 真实 BRDF LUT(描述符恒有效)
|
||||
Scope<Cubemap> Neutral(const glm::vec3& rgb);
|
||||
|
||||
private:
|
||||
// cube 兼容 image(2D array 写 + cube 采样双 view)
|
||||
struct CubeTarget {
|
||||
vk::Image image{};
|
||||
std::vector<vk::ImageView> storageViews; // 每 mip 一个(storage 写必须单 mip)
|
||||
vk::ImageView cubeView{}; // eCube 全 mip(采样端)
|
||||
MemoryAllocator::Allocation allocation;
|
||||
};
|
||||
|
||||
// 从 HDR 像素数据生成(Neutral 走同一链,仅 equirect 为 1×1 常数)
|
||||
Scope<Cubemap> Generate(const f32* pixels, u32 w, u32 h,
|
||||
u32 faceSize, const std::string& debugName);
|
||||
|
||||
CubeTarget CreateCubeTarget(u32 size, u32 mips) const;
|
||||
void ReleaseStorageViews(const CubeTarget& t) const;
|
||||
|
||||
vk::Sampler CreateEnvSampler(u32 maxLod) const;
|
||||
vk::Sampler CreateLutSampler() const;
|
||||
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
DescriptorManager& m_Descriptors;
|
||||
ShaderManager& m_Shaders;
|
||||
|
||||
// set 布局:A = {0 采样输入, 1 storage 输出},B = {0 storage}
|
||||
vk::DescriptorSetLayout m_IblSetLayout;
|
||||
vk::DescriptorSetLayout m_LutSetLayout;
|
||||
// 管线布局:无 push(equirect/irradiance/LUT)与 4B push(prefilter)
|
||||
vk::PipelineLayout m_IblLayout;
|
||||
vk::PipelineLayout m_IblLayoutPush;
|
||||
vk::PipelineLayout m_LutLayout;
|
||||
|
||||
// compute shader(引用计数句柄,析构自动销毁)
|
||||
ShaderManager::ShaderRef m_ToCubeShader;
|
||||
ShaderManager::ShaderRef m_IrrShader;
|
||||
ShaderManager::ShaderRef m_PrefShader;
|
||||
ShaderManager::ShaderRef m_LutShader;
|
||||
|
||||
vk::Sampler m_EquirectSampler; // S=repeat(经度环绕)T=clamp
|
||||
vk::CommandPool m_Pool; // 持久复用,每次生成 reset
|
||||
vk::CommandBuffer m_Cmd;
|
||||
vk::Fence m_Fence;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_IBLGENERATOR_H
|
||||
@@ -1,38 +1,160 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/21.
|
||||
//
|
||||
// Material:PBR 材质 = 纹理槽组 + 因子参数 UBO + 一个描述符集。
|
||||
//
|
||||
// 槽位(binding = 槽位序号,与 simple_shader.frag 契约一致):
|
||||
// 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性)
|
||||
// 2 metallicRoughness(线性:G=粗糙度,B=金属度,glTF 约定)
|
||||
// 3 emissive(自发光,sRGB) 4 ao(环境光遮蔽,线性)
|
||||
// 5 roughness(独立粗糙度图,R 通道——FBX/OBJ specular 工作流,
|
||||
// glTF 合图工作流不用此槽)
|
||||
// 6 材质参数 UBO(fragment,kParamsBinding)
|
||||
//
|
||||
// 纹理非拥有(Model 的纹理缓存持有);缺槽由调用方传默认纹理。
|
||||
// 因子(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
|
||||
#define HELLOVULKAN_MATERIAL_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Core/FrameContext.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
namespace Engine
|
||||
{
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class MemoryAllocator;
|
||||
class DescriptorManager;
|
||||
class Texture;
|
||||
class UniformBuffer;
|
||||
|
||||
class Material
|
||||
{
|
||||
// 材质因子(乘贴图;无贴图时即最终值)
|
||||
// x = metallic 因子 y = roughness 因子
|
||||
// z = 独立粗糙度图开关(1 = 用 Roughness 槽 R 通道替换 MR.g)
|
||||
// 注意:与 shader MaterialParams UBO 严格同序同布局(全 vec4,std140
|
||||
// 无填充歧义),加字段必须双端同步(simple_shader*.frag)
|
||||
struct MaterialParams {
|
||||
// x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
||||
// 默认 = 电介质表面推荐值(Unity/UE 同款):
|
||||
// metallic = 0 皮肤/头发/布料等非金属的正确值
|
||||
// roughness = 1.0 因子语义是"乘贴图",1.0 = 贴图值直用(同 glTF
|
||||
// roughnessFactor 缺省);配合兜底 MR 纹理 G=128
|
||||
// 得 effective roughness ≈ 0.5,不再是 ×0.5 后的
|
||||
// 0.25 假高光(ufbx 路径不写 factors,全靠这里兜底)
|
||||
glm::vec4 factors{0.0f, 1.0f, 0.0f, 0.0f};
|
||||
// glTF baseColorFactor:rgb 乘进 albedo,a 作 BLEND 透明度
|
||||
glm::vec4 baseColor{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
// glTF 表面扩展(KHR_materials_ior / transmission / volume /
|
||||
// emissive_strength):x = 折射率(F0 = ((ior-1)/(ior+1))²),
|
||||
// y = 透射率(0 = 普通表面),z = 自发光强度,w = 体积厚度
|
||||
//(折射屏幕偏移的世界距离,0 = 无弯曲直透)
|
||||
glm::vec4 surface{1.5f, 0.0f, 1.0f, 0.0f};
|
||||
// 杂项语义:
|
||||
// x = unlit(KHR_materials_unlit,shader 早退直出 albedo)
|
||||
// y = UV 左右镜像(u → 1-u,1 = 开启)
|
||||
// z = UV 上下镜像(v → 1-v,1 = 开启)
|
||||
// w = 保留
|
||||
// 镜像作用于材质全部纹理槽,叠加在每槽 UV 变换(uvA/uvT)之后:
|
||||
// 用于纠正源资产 UV 朝向(部分角色 rip 的 UV 与 glTF 上左原点
|
||||
// 约定相反,贴图整体翻转)。材质级语义,非单槽
|
||||
glm::vec4 misc{0.0f, 0.0f, 0.0f, 0.0f};
|
||||
// glTF emissiveFactor:自发光颜色(rgb;黑 = 不发光)。emissive
|
||||
// 槽默认兜底纹理是纯黑,无贴图时 Emissive Strength 乘出来恒黑
|
||||
// ——无贴图想要发光就调这里(rgb × 贴图 × surface.z)
|
||||
glm::vec4 emissiveFactor{0.0f, 0.0f, 0.0f, 0.0f};
|
||||
// 每槽 KHR_texture_transform(glTF2 每个 textureInfo 独立声明
|
||||
// offset/scale/rotation——烘焙单份顶点 UV 无法表达"一材质多槽
|
||||
// 各异变换",收敛为 shader 侧线性式 uv' = A·uv + t,作用在 glTF
|
||||
// 规范 UV 空间(v=0 = 图像顶);顶点 UV 经 aiProcess_FlipUVs 已
|
||||
// 归一至该空间,A/t 即规范式 off + R(rot)·S(scale)·uv 的摊平,
|
||||
// 换算推导见 Model.cpp glTF 分支注释)。
|
||||
// uvA[slot] = (a11, a12, a21, a22)(行主 2×2),uvT[slot] = (tx, ty, 0, 0)。
|
||||
// 长度必须 = Material::kSlotCount;std140 vec4 数组步长 16B 与
|
||||
// CPU 布局天然对齐
|
||||
glm::vec4 uvA[6] = {{1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f},
|
||||
{1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f},
|
||||
{1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f}};
|
||||
glm::vec4 uvT[6] = {};
|
||||
// albedo 贴图 alpha 作细节叠加遮罩(脸部腮红/卧蚕等局部效果):
|
||||
// rgb = 叠加色(与 baseColor 同款直乘/直用,不做空间换算),
|
||||
// a = 强度(0 = 关闭,mix 恒等无副作用)。alpha 遮罩来自贴图本身,
|
||||
// 无 alpha 或 alpha 恒 1 的贴图开启后即等效整体染色
|
||||
glm::vec4 alphaOverlay{0.0f, 0.0f, 0.0f, 0.0f};
|
||||
};
|
||||
|
||||
class Material {
|
||||
public:
|
||||
Material(Device& device,
|
||||
// 槽位序号(与 binding 对齐)
|
||||
enum Slot : u32 {
|
||||
Albedo = 0,
|
||||
Normal = 1,
|
||||
MetallicRoughness = 2,
|
||||
Emissive = 3,
|
||||
Ao = 4,
|
||||
Roughness = 5, // 独立粗糙度图(R 通道)
|
||||
kSlotCount = 6, // 纹理槽数
|
||||
kParamsBinding = 6, // 参数 UBO binding
|
||||
};
|
||||
|
||||
Material(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout layout,
|
||||
Texture* texture);
|
||||
const std::array<Texture*, kSlotCount>& textures,
|
||||
const MaterialParams& params,
|
||||
bool transparent = false);
|
||||
~Material();
|
||||
|
||||
Material(const Material&) = delete;
|
||||
Material& operator=(const Material&) = delete;
|
||||
|
||||
vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; }
|
||||
Texture* GetTexture() const { return m_Texture; }
|
||||
Texture* GetTexture(Slot slot) const { return m_Textures[slot]; }
|
||||
// 透明队列归属(glTF alphaMode=BLEND;CPU 收集侧用)
|
||||
bool IsTransparent() const { return m_Transparent; }
|
||||
|
||||
// ---- 编辑器调参入口(零 GPU 重建路径)----
|
||||
// 参数直写:全 in-flight 槽同步覆盖写,与 ctor 初始化同一绑定
|
||||
// 语义(binding 覆盖写,ring 槽位共用同一 descriptor binding)。
|
||||
// 理论上存在 in-flight 帧读到半新半旧参数的撕裂窗口(动态偏移
|
||||
// ring 是根治演进点)——UI 段低频编辑 + 48B 小块 memcpy 下实践
|
||||
// 无感。纹理槽变化不走这里(那是重建路径,见 MaterialAsset)。
|
||||
// 实现外置(UniformBuffer 头文件内仅前置声明)
|
||||
void UpdateParams(const MaterialParams& params);
|
||||
// 队列归属切换(收集侧 CPU 标志,描述符不变):透明 ↔ 不透明
|
||||
void SetTransparent(bool t) { m_Transparent = t; }
|
||||
|
||||
// 材质缓存键:同贴图组合+同因子+同透明语义共享一份(Model 层使用)
|
||||
static std::string MakeCacheKey(const std::string paths[kSlotCount],
|
||||
const MaterialParams& params,
|
||||
bool transparent);
|
||||
|
||||
// ---- 预览缩略图身份(editor tile 池的缓存键,game 不消费)----
|
||||
// serial:出生唯一号(进程内单调递增,不复用)——裸指针做键有
|
||||
// ABA 风险(旧材质析构后新材质恰好分配到同一地址,池误判命中)
|
||||
u64 GetSerial() const { return m_Serial; }
|
||||
// editVersion:参数编辑代数,UpdateParams 每次 ++
|
||||
//(tile 池以此判定"缓存的球已过期需重渲")
|
||||
u32 GetEditVersion() const { return m_EditVersion; }
|
||||
|
||||
private:
|
||||
Texture* m_Texture = nullptr;
|
||||
std::array<Texture*, kSlotCount> m_Textures{};
|
||||
vk::DescriptorSet m_DescriptorSet;
|
||||
Scope<UniformBuffer> m_ParamsBuffer; // per-slot 参数 ring(创建时写死)
|
||||
bool m_Transparent = false;
|
||||
u64 m_Serial = 0; // ctor 取号(定义见 Material.cpp)
|
||||
u32 m_EditVersion = 0;
|
||||
};
|
||||
} // namespace Engine
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/10/5.
|
||||
//
|
||||
// MaterialAsset:独立材质资产(.material 文件)+ GPU 资源封装。
|
||||
//
|
||||
// UE UMaterial 的资产化对应物。语义约定:
|
||||
// - 模型内建材质(Model 材质缓存持有,随 glTF/FBX 解析生成)只读,
|
||||
// 不存在"原地编辑"路径——改它会波及所有引用该模型的实体
|
||||
// - 可编辑材质 = .material 资产文件,被 MeshRenderer 的覆盖表
|
||||
// (mesh 下标 → 资产路径)按实体引用;资产被多实体共享时编辑
|
||||
// 同时生效(资产级语义),要实体独立就每实体一份文件
|
||||
// - 数据段(MaterialAssetData,磁盘 ↔ 内存纯值)与 GPU 段(纹理 +
|
||||
// Material)分离:GPU 段由 VulkanAssetUploader 构建,资产层与
|
||||
// 本类都不感知 Vulkan——依赖方向与 Model/资产层完全一致
|
||||
//
|
||||
// 扩展点(UV 动效/特效等后续迭代):
|
||||
// - 数据段加字段 + Version 递增,Load 全部 as<T>(default) 缺省
|
||||
// 兼容——旧文件永远可读,新字段对旧文件取默认值
|
||||
// - GPU 段参数经 Material::UpdateParams 直写 UBO,shader 侧
|
||||
// matParams 块同步扩展即可,本类结构不变
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_MATERIALASSET_H
|
||||
#define HELLOVULKAN_MATERIALASSET_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Material.h>
|
||||
// Texture 完整类型(非前置声明):成员持 Scope<Texture> 数组,若仅
|
||||
// 前置声明,所有实例化本类析构/构造的 TU 都需各自看到 Texture 完整
|
||||
// 类型——直接包含一劳永逸。Material.h 已含 vulkan.hpp,无新增重依赖
|
||||
#include <Engine/Vulkan/Texture.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class MemoryAllocator;
|
||||
|
||||
// 材质槽位默认纹理工厂(单点实现防漂移):Model 兜底纹理与
|
||||
// MaterialAsset 构建共用同一组 1x1 兜底语义
|
||||
//(albedo 白 / normal 平 / MR 中性 / emissive 黑 / AO 全遮蔽 / rough 0.5)
|
||||
Scope<Texture> CreateDefaultMaterialTexture(
|
||||
Device& device, MemoryAllocator& allocator, Material::Slot slot);
|
||||
|
||||
// 材质资产数据段(磁盘格式 ↔ 内存值,无 GPU 状态)。
|
||||
// texPaths = 文件系统相对 CWD 路径(与模型解析产物同源,glTF/FBX
|
||||
// 纹理可直接快照);空串 = 该槽用共享默认纹理兜底。
|
||||
// glb 内嵌纹理在模型导入期已抽到 <模型目录>/textures/ 落盘并回写为
|
||||
// 真实路径(Model::ParseFromDisk),故此处与文件纹理同语义——覆盖
|
||||
// 材质可正常继承
|
||||
struct MaterialAssetData {
|
||||
std::array<std::string, Material::kSlotCount> texPaths;
|
||||
MaterialParams params;
|
||||
bool transparent = false;
|
||||
|
||||
bool operator==(const MaterialAssetData&) const = default;
|
||||
};
|
||||
|
||||
class MaterialAsset {
|
||||
public:
|
||||
static constexpr u32 kVersion = 1;
|
||||
|
||||
// ---- 磁盘序列化(.material,YAML)----
|
||||
// 失败(无法打开/解析)返回 false;Load 缺省键兼容
|
||||
static bool Save(const std::string& path, const MaterialAssetData& data);
|
||||
static bool Load(const std::string& path, MaterialAssetData& out);
|
||||
|
||||
// GPU 段注入(仅 VulkanAssetUploader 调用):material = 组装好的
|
||||
// 材质(空槽已由 uploader 绑共享默认纹理)。纹理所有权归 uploader
|
||||
// 的路径缓存(跨材质去重),本资产只持有绑进 Material 的裸指针
|
||||
MaterialAsset(MaterialAssetData&& data, Scope<Material>&& material);
|
||||
// 析构外置(成员为重资源句柄,销毁序与 GPU 生命周期解耦遵循
|
||||
// Model/Material 既有模式)
|
||||
~MaterialAsset();
|
||||
|
||||
const MaterialAssetData& GetData() const { return m_Data; }
|
||||
Material* GetMaterial() const { return m_Material.get(); }
|
||||
|
||||
// ---- 编辑入口(Inspector 调;数据段与 GPU 段同步更新)----
|
||||
// 写盘时机由编辑器决定(参数松手落盘,纹理变化走资产重建)
|
||||
void EditParams(const MaterialParams& params);
|
||||
void SetTransparent(bool t);
|
||||
|
||||
private:
|
||||
MaterialAssetData m_Data;
|
||||
Scope<Material> m_Material;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_MATERIALASSET_H
|
||||
@@ -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,28 +19,79 @@ namespace Engine
|
||||
class Buffer;
|
||||
class Material;
|
||||
|
||||
// LOD 级数:LOD0 = 原始网格,越高越简(解析期 meshopt_simplify 生成,
|
||||
// 共享顶点数据,只换索引区间)
|
||||
inline constexpr u32 kMeshLodCount = 3;
|
||||
|
||||
class Mesh
|
||||
{
|
||||
public:
|
||||
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
|
||||
// lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成
|
||||
// cpuPositions 非空(蒙皮 mesh)= 物理数据源与包围球改用它
|
||||
//(绑定姿态渲染空间,与视觉同源;vertices.position 是蒙皮原始空间)
|
||||
Mesh(Device& device, MemoryAllocator& allocator,
|
||||
const std::vector<Vertex>& vertices,
|
||||
const std::vector<u32>& indices,
|
||||
Scope<Material> material);
|
||||
const std::vector<std::vector<u32>>& lods,
|
||||
Material* material,
|
||||
const std::vector<glm::vec3>* cpuPositions = nullptr);
|
||||
~Mesh();
|
||||
|
||||
Mesh(const Mesh&) = delete;
|
||||
Mesh& operator=(const Mesh&) = delete;
|
||||
|
||||
void Draw(vk::CommandBuffer cmd) const;
|
||||
// instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后,
|
||||
// 同 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.get(); }
|
||||
u32 GetIndexCount() const { return m_IndexCount; }
|
||||
Material* GetMaterial() const { return m_Material; }
|
||||
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 {
|
||||
glm::vec3 center{0.0f};
|
||||
f32 radius = 0.0f;
|
||||
};
|
||||
const Bounds& GetBounds() const { return m_Bounds; }
|
||||
|
||||
// ---- CPU 位置副本(物理 MeshShape/ConvexHull 数据源)----
|
||||
// GPU 上传后完整 Vertex 即弃,但物理碰撞仍需三角形:构造时抽
|
||||
// 位置-only(12B/顶点,约为完整 Vertex 的 1/5)+ LOD0 索引。
|
||||
const std::vector<glm::vec3>& GetCpuPositions() const { return m_CpuPositions; }
|
||||
const std::vector<u32>& GetCpuIndices() const { return m_CpuIndices; }
|
||||
|
||||
private:
|
||||
// 物理碰撞数据源(MeshShape 三角网格 / ConvexHull 点云)
|
||||
std::vector<glm::vec3> m_CpuPositions;
|
||||
std::vector<u32> m_CpuIndices; // LOD0(原始精度)
|
||||
// 同一 index buffer 内连续排布的各级索引区间
|
||||
struct LodRange {
|
||||
u32 firstIndex = 0;
|
||||
u32 indexCount = 0;
|
||||
};
|
||||
|
||||
Scope<Buffer> m_VertexBuffer;
|
||||
Scope<Buffer> m_IndexBuffer;
|
||||
Scope<Material> m_Material;
|
||||
u32 m_IndexCount = 0;
|
||||
std::array<LodRange, kMeshLodCount> m_Lods{};
|
||||
Material* m_Material;
|
||||
u32 m_Slot = 0;
|
||||
Bounds m_Bounds;
|
||||
|
||||
u64 m_VertexBytes = 0;
|
||||
u64 m_IndexBytes = 0;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/10/18.
|
||||
//
|
||||
// MeshFactory:预制图元的 CPU 几何生成端(唯一几何源)。
|
||||
//
|
||||
// 定位对齐 UE 的 Engine\Source\Runtime\Engine\...(内置基础网格)思路:
|
||||
// 引擎只保留一套"参数化图元 → 顶点/索引"的生成器,程序化 Model、
|
||||
// 物理形状、脚本与编辑器下拉全部复用它,避免各路径各写一份立方体。
|
||||
//
|
||||
// 契约:
|
||||
// - 所有图元中心在局部原点(平面在 y=0 的 XZ 面),尺寸为世界单位,
|
||||
// 尺寸变化统一走实体 Transform 缩放,不按尺寸炸资产缓存;
|
||||
// - 面法线朝外、CCW 从外正视,可被背面剔除;
|
||||
// - 切线已按 UV 正交化(法线贴图 TBN 可用),退化为任一与 N 正交向量;
|
||||
// - 输出 LOD0 单级索引(图元顶点少,无简化链价值)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_MESHFACTORY_H
|
||||
#define HELLOVULKAN_MESHFACTORY_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Vertex.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
// 图元参数(形状无关的标量集):各形状只读取自己关心的字段。
|
||||
// 默认值给"半径/半边长 0.5、高 1"的规格化尺寸,贴合单位立方体约定。
|
||||
struct PrimitiveDesc {
|
||||
f32 size = 0.5f; // 半边长(cube/plane)/ 半径(sphere/cylinder/cone)
|
||||
f32 height = 1.0f; // 圆柱/圆锥高(沿 +Y,几何中心在原点)
|
||||
f32 majorRadius = 0.5f; // 圆环主半径
|
||||
f32 minorRadius = 0.15f; // 圆环管半径
|
||||
u32 segments = 32; // 环绕细分(球/柱/锥/圆环主环)
|
||||
u32 rings = 16; // 纵向细分(球)/ 圆环管环细分
|
||||
u32 subdivisions = 1; // 平面每轴网格细分
|
||||
};
|
||||
|
||||
// 预制图元的 CPU 几何(渲染 Mesh 与物理/脚本共用)。
|
||||
struct MeshData {
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<u32> indices; // LOD0(三角形列表)
|
||||
};
|
||||
|
||||
class MeshFactory {
|
||||
public:
|
||||
// 按名字取形状(ShapeNames 中任一);未知名字返回空 MeshData。
|
||||
static MeshData Make(const std::string& shape, const PrimitiveDesc& desc);
|
||||
|
||||
// ---- 具名构造(编辑器下拉 / 物理形状 / 脚本可直接调用)----
|
||||
static MeshData Cube(f32 halfSize);
|
||||
static MeshData Plane(f32 halfSize, u32 subdivisions = 1);
|
||||
static MeshData Sphere(f32 radius, u32 segments = 32, u32 rings = 16);
|
||||
static MeshData Cylinder(f32 radius, f32 height, u32 segments = 32);
|
||||
static MeshData Cone(f32 radius, f32 height, u32 segments = 32);
|
||||
static MeshData Torus(f32 majorRadius, f32 minorRadius,
|
||||
u32 majorSegments = 32, u32 minorSegments = 16);
|
||||
|
||||
// 已知图元名(UI 下拉 / 伪路径校验的唯一定义点)
|
||||
static const std::vector<std::string>& ShapeNames();
|
||||
static bool IsKnownShape(const std::string& shape);
|
||||
|
||||
private:
|
||||
// 逐三角面积加权累积切线 → 对 N 正交化 → 归一(退化取正交兜底)
|
||||
static void ComputeTangents(MeshData& mesh);
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_MESHFACTORY_H
|
||||
@@ -8,9 +8,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Material.h>
|
||||
#include <Engine/Renderer/Mesh.h>
|
||||
#include <Engine/Renderer/MeshFactory.h> // 程序化几何唯一源(MeshData/PrimitiveDesc)
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -25,22 +31,161 @@ namespace Engine
|
||||
class MemoryAllocator;
|
||||
class DescriptorManager;
|
||||
class Texture;
|
||||
class Material;
|
||||
|
||||
// ---- CPU 解析产物(异步加载的跨线程交棒数据)----
|
||||
// 纯数据:几何 + 材质槽位引用(路径 + 因子),无任何 GPU 资源。
|
||||
// worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。
|
||||
struct ParsedMesh {
|
||||
std::vector<Vertex> vertices;
|
||||
// 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(透明绘制队列)
|
||||
bool skinned = false; // 有骨骼(渲染走 palette 蒙皮)
|
||||
std::string name; // UI 名字:材质名(AI_MATKEY_NAME)优先,
|
||||
// 回退 aiMesh mName(可空 → UI 回退 Slot N)
|
||||
|
||||
// 蒙皮 mesh 专属:绑定姿态渲染空间的位置副本(palette(bind) 按
|
||||
// 顶点权重混合,与 shader 同式)。非空 = Mesh 的物理数据源与
|
||||
// 包围球改用它——FBX 单位换算折在骨骼链上(bind palette ≠ I),
|
||||
// 直接用原始顶点会让物理凸包大一个数量级(M1911 实测 10×)
|
||||
std::vector<glm::vec3> cpuPositions;
|
||||
};
|
||||
|
||||
// 内建材质源信息快照(per-mesh,与 GetMeshes() 下标对齐)。
|
||||
// 只读语义:这是"模型固有材质"的权威描述——编辑器用它展示内建
|
||||
// 材质并生成覆盖材质快照(Create Override),**永不回写**:
|
||||
// 内建材质属于模型资产,原地修改会波及所有引用该模型的实体。
|
||||
// 可编辑材质走 MaterialAsset(.material 文件)+ 实体级覆盖表
|
||||
struct MeshMaterialInfo {
|
||||
std::string name; // UI 名字:材质名优先,回退 mesh 名(空 → Slot N)
|
||||
std::array<std::string, Material::kSlotCount> texPaths;
|
||||
MaterialParams params;
|
||||
bool transparent = false;
|
||||
};
|
||||
|
||||
// ---- 骨骼动画数据(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;
|
||||
// 外部动画文件来源路径(运行时并入的 clip 标记;空 = 模型内嵌)。
|
||||
// 用于幂等去重:同一源文件不重复并入
|
||||
std::string sourcePath;
|
||||
};
|
||||
|
||||
// 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;
|
||||
// GLB 内嵌贴图(assimp "*<index>" 约定):解析期从 aiScene 拷出
|
||||
// 原始编码字节(PNG/JPEG),实例期 GetOrLoadTexture 解码上传。
|
||||
// 解析线程无 GPU 上下文,只带数据不建纹理
|
||||
std::unordered_map<std::string, std::vector<u8>> embeddedImages;
|
||||
// 解析有效性:mesh 与 clip 至少其一非空。纯动画 FBX(无网格只有
|
||||
// 骨骼动画,UE 的 Animation Sequence 资产同理)是合法资产——
|
||||
// 过去只认 meshes 导致动画源文件在缓存层被整体拒载
|
||||
bool Valid() const { return !meshes.empty() || !clips.empty(); }
|
||||
};
|
||||
|
||||
class Model
|
||||
{
|
||||
public:
|
||||
// 同步便捷路径(等价 ParseFromDisk + FromParsed,启动期小模型可用)
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
const std::string& path);
|
||||
|
||||
// 程序化模型:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
||||
// size 为半边长(平面总边长 = 2*size)。
|
||||
// ---- 异步加载两段式(assimp 解析占大模型耗时大头,可搬后台线程)----
|
||||
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
|
||||
static ParsedModel ParseFromDisk(const std::string& path);
|
||||
|
||||
// 资产显示名(编辑器 UI / clip 命名的唯一来源):取文件 stem;
|
||||
// stem 无信息量(空 / 导入资产库的 "asset")时读同目录 pathname
|
||||
// 文件取原始资产 basename,再回退父目录名。避免 UI 直接显示
|
||||
// 哈希路径(用户看到的是 id)
|
||||
static std::string AssetDisplayName(const std::string& path);
|
||||
|
||||
// GPU 构建(必须主线程):纹理上传 / mesh 缓冲 / 材质描述符。
|
||||
// 解析失败返回 nullptr。
|
||||
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
ParsedModel&& parsed);
|
||||
|
||||
// 程序化几何标签:几何统一来自 MeshFactory(单一几何源),
|
||||
// 本类只负责把几何 + 纯色 albedo 组装成可渲染 Model
|
||||
struct ProceduralTag {};
|
||||
|
||||
// 程序化纯色平面:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
||||
// size 为半边长(平面总边长 = 2*size)。
|
||||
static Scope<Model> CreatePlane(Device& device, MemoryAllocator& allocator,
|
||||
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);
|
||||
|
||||
// 程序化立方体(halfSize = 半边长,几何走 MeshFactory::Cube):
|
||||
// 通用占位几何(碎片模拟的墙/块等)。尺寸变化走实体 Transform
|
||||
// 缩放——同 halfSize 同色共享一份缓存模型
|
||||
static Scope<Model> CreateCube(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
f32 halfSize, u8 r, u8 g, u8 b);
|
||||
|
||||
// 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/
|
||||
// cone/torus 等):几何由 MeshFactory::Make 生成,纯色 albedo。
|
||||
// 非法形状名返回 nullptr(调用方回退)
|
||||
static Scope<Model> CreatePrimitive(Device& device,
|
||||
MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b);
|
||||
|
||||
~Model();
|
||||
|
||||
@@ -48,30 +193,136 @@ namespace Engine
|
||||
Model& operator=(const Model&) = delete;
|
||||
|
||||
const std::vector<Scope<Mesh>>& GetMeshes() const { return m_Meshes; }
|
||||
// 内建材质源信息(与 GetMeshes() 下标对齐;只读,见结构注释)
|
||||
const std::vector<MeshMaterialInfo>& GetMeshMaterialInfos() const
|
||||
{
|
||||
return m_MeshMaterialInfos;
|
||||
}
|
||||
// 全模型联合包围球(各 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; }
|
||||
// 外部动画并入(编辑器 attach 操作):追加到 clip 表尾部,
|
||||
// 既有 clipIndex 不受影响。仅运行时内存状态(重启后由场景
|
||||
// 里 Animation.attachedAnimPaths 驱动重放)
|
||||
void AppendClips(std::vector<AnimationClip>&& extra)
|
||||
{
|
||||
m_Clips.insert(m_Clips.end(),
|
||||
std::make_move_iterator(extra.begin()),
|
||||
std::make_move_iterator(extra.end()));
|
||||
}
|
||||
// 外部动画剥离(编辑器 detach 操作):删除 sourcePath 匹配的
|
||||
// clip,返回删除数。clipIndex/fadeFrom 下标悬空由消费端自愈
|
||||
//(AnimationSystem 每帧 clamp)
|
||||
u32 RemoveClipsBySource(const std::string& sourcePath)
|
||||
{
|
||||
const u32 before = static_cast<u32>(m_Clips.size());
|
||||
m_Clips.erase(
|
||||
std::remove_if(m_Clips.begin(), m_Clips.end(),
|
||||
[&](const AnimationClip& c)
|
||||
{ return c.sourcePath == sourcePath; }),
|
||||
m_Clips.end());
|
||||
return before - static_cast<u32>(m_Clips.size());
|
||||
}
|
||||
const std::vector<ModelNode>& GetNodes() const { return m_Nodes; }
|
||||
const std::vector<ModelBone>& GetBones() const { return m_Bones; }
|
||||
|
||||
private:
|
||||
// 程序化几何骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 顶点/索引
|
||||
// 由 MeshFactory 产出(见 .cpp)。name = 编辑器材质槽显示名。
|
||||
// 各具名工厂(CreatePlane/Cube/Primitive)都折到这一个 ctor
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
ProceduralTag, f32 halfSize, const std::array<u8, 3>& color);
|
||||
ProceduralTag, MeshData&& geo, const std::array<u8, 4>& rgba,
|
||||
bool transparent, const std::string& name);
|
||||
|
||||
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout);
|
||||
|
||||
void LoadModel(const std::string& path);
|
||||
void ProcessNode(aiNode* node, const aiScene* scene);
|
||||
Scope<Mesh> ProcessMesh(aiMesh* mesh, const aiScene* scene);
|
||||
// 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,
|
||||
std::vector<ModelBone>& bones,
|
||||
std::unordered_map<std::string, u32>& boneSlots,
|
||||
std::unordered_map<std::string,
|
||||
std::vector<u8>>& embeddedImages,
|
||||
const aiMatrix4x4& parentGlobal
|
||||
= aiMatrix4x4());
|
||||
static ParsedMesh ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
const std::filesystem::path& modelDir,
|
||||
bool isGltf,
|
||||
std::vector<ModelBone>& bones,
|
||||
std::unordered_map<std::string, u32>& boneSlots,
|
||||
std::unordered_map<std::string,
|
||||
std::vector<u8>>& embeddedImages,
|
||||
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();
|
||||
|
||||
Texture* GetOrLoadTexture(const std::string& path);
|
||||
// GLB 内嵌贴图字节("*N" → 编码数据):解析期拷出,纹理解析期
|
||||
// GetOrLoadTexture 消费(随实例生命周期,键同 texPaths 引用)
|
||||
std::unordered_map<std::string, std::vector<u8>> m_EmbeddedImages;
|
||||
|
||||
// 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除)
|
||||
void RebuildUnionBounds();
|
||||
|
||||
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
|
||||
|
||||
// 材质缓存入口:同贴图组合+同因子+同透明语义共享一份 Material,
|
||||
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
|
||||
// paths 下标 = Material::Slot;params = metallic/roughness 因子;
|
||||
// slotOverrides = 程序化模型的槽位纹理直供(非空项优先于路径/默认)
|
||||
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
|
||||
const MaterialParams& params,
|
||||
bool transparent = false,
|
||||
const std::array<Texture*, Material::kSlotCount>&
|
||||
slotOverrides = {});
|
||||
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
DescriptorManager& m_DescriptorManager;
|
||||
vk::DescriptorSetLayout m_TextureLayout;
|
||||
|
||||
std::vector<Scope<Mesh>> m_Meshes;
|
||||
std::string m_ModelPath;
|
||||
|
||||
// 纹理缓存:同一路径只加载一次
|
||||
// 缓存声明在 m_Meshes 之前:mesh 持 Material* 裸指针,
|
||||
// 析构序 = meshes 先于材质/纹理缓存
|
||||
std::unordered_map<std::string, Scope<Material>> m_MaterialCache;
|
||||
std::unordered_map<std::string, Scope<Texture>> m_TextureCache;
|
||||
Scope<Texture> m_DefaultTexture;
|
||||
// 各槽位默认纹理(材质缺图时兜底):白 / 平法线 / 黑高光 / 黑自发光
|
||||
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
|
||||
|
||||
std::vector<Scope<Mesh>> m_Meshes;
|
||||
Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除)
|
||||
// 与 m_Meshes 平行的内建材质快照(每条建 mesh 路径同步填充:
|
||||
// FromParsed / 同步 LoadModel / 程序化平面)
|
||||
std::vector<MeshMaterialInfo> m_MeshMaterialInfos;
|
||||
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
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// BloomPass:Extract(HDR 软阈值)→ BlurH → BlurV,全部半分辨率。
|
||||
// 一个"效果"对应多个 RG pass 的例子(Unity 的一个 ScriptableRenderPass
|
||||
// 常含多个 PassEvent 阶段,同构)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_BLOOMPASS_H
|
||||
#define HELLOVULKAN_BLOOMPASS_H
|
||||
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Passes {
|
||||
|
||||
class BloomPass final : public RenderPass {
|
||||
public:
|
||||
BloomPass(FullscreenGpu& gpu, PostProcessParams& params,
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor,
|
||||
RG::TextureHandle bloomA, RG::TextureHandle bloomB);
|
||||
~BloomPass() override = default;
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
||||
|
||||
private:
|
||||
FullscreenGpu& m_Gpu;
|
||||
PostProcessParams& m_Params;
|
||||
FrameSceneData& m_Frame; // bloomEnabled 开关(帧契约)
|
||||
RG::TextureHandle m_HDR;
|
||||
RG::TextureHandle m_A; // 提取 + 垂直模糊结果
|
||||
RG::TextureHandle m_B; // 水平模糊中间结果
|
||||
|
||||
// per-slot 输入描述符
|
||||
std::vector<vk::DescriptorSet> m_ExtractSets;
|
||||
std::vector<vk::DescriptorSet> m_BlurHSets;
|
||||
std::vector<vk::DescriptorSet> m_BlurVSets;
|
||||
};
|
||||
|
||||
} // namespace Engine::Passes
|
||||
|
||||
#endif //HELLOVULKAN_BLOOMPASS_H
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// EditorGizmoPass:Editor gizmo 可视化叠加(billboard 图标 + 视锥线框)。
|
||||
// editor 专属(HELLO_EDITOR):cpp TU 被 game 构建整体剔除
|
||||
//(Engine/CMakeLists REMOVE_ITEM),本头文件只含声明、不引用任何
|
||||
// 编辑器专属类型,game 构建可安全 include(零符号产出)。
|
||||
//
|
||||
// 与 PhysicsDebugPass 同构:
|
||||
// - 写 HDRColor(Load 承接 Forward/PhysicsDebug 结果,WAW 链排其后,
|
||||
// TAA 之前——gizmo 进 TAA/Bloom/Tonemap 主链路)
|
||||
// - 深度只测不写(LessOrEqual,被场景几何正确遮挡),DontCare 不污染
|
||||
// - Write(m_Depth) 声明深度 WAW 边:把本 pass 固定排在 HitProxy
|
||||
// (深度附件 loadOp Clear)之前——gizmo 深度测试用的是场景深度
|
||||
// - 无 PrepareFrame:Renderer 在上传窗口直接把 FrameSceneData 的
|
||||
// gizmo 顶点灌进 ring VB(本 pass 只绑定绘制)
|
||||
// - 空帧短路:两流顶点数全 0 直接 return,零 GPU 工作
|
||||
//
|
||||
// 两流两状态(Shader Object 动态状态跨 pass 残留教训:公共状态无条件
|
||||
// 直写,差异状态各流内直写):
|
||||
// - 线流(GizmoWireVertex):LineList + lineWidth(1.0) + 无混合直写,
|
||||
// 复用 debug_line.vert/frag(世界空间线段 + push viewProj 同契约)
|
||||
// - 图标流(GizmoQuadVertex):TriangleList + 预乘 alpha 混合
|
||||
// (ONE, ONE_MINUS_SRC_ALPHA),gizmo_billboard.vert/frag SDF 造型
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_EDITORGIZMOPASS_H
|
||||
#define HELLOVULKAN_EDITORGIZMOPASS_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class DescriptorManager;
|
||||
class UniformBuffer;
|
||||
|
||||
namespace Passes {
|
||||
|
||||
class EditorGizmoPass final : public RenderPass {
|
||||
public:
|
||||
EditorGizmoPass(Device& device, ShaderManager& shaders,
|
||||
DescriptorManager& descriptors,
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor,
|
||||
RG::TextureHandle depth,
|
||||
UniformBuffer& iconVB, UniformBuffer& wireVB);
|
||||
~EditorGizmoPass();
|
||||
|
||||
EditorGizmoPass(const EditorGizmoPass&) = delete;
|
||||
EditorGizmoPass& operator=(const EditorGizmoPass&) = delete;
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
|
||||
private:
|
||||
Device& m_Device;
|
||||
DescriptorManager& m_Descriptors;
|
||||
FrameSceneData& m_Frame;
|
||||
RG::TextureHandle m_HDRColor;
|
||||
RG::TextureHandle m_Depth;
|
||||
UniformBuffer& m_IconVB; // per-slot ring(Renderer 持有生命周期)
|
||||
UniformBuffer& m_WireVB;
|
||||
|
||||
vk::PipelineLayout m_Layout; // 无描述符,push = viewProj 64B
|
||||
ShaderManager::ShaderRef m_WireVert, m_WireFrag; // debug_line 复用
|
||||
ShaderManager::ShaderRef m_IconVert, m_IconFrag; // gizmo_billboard
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_EDITORGIZMOPASS_H
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// ForwardPass:相机视角渲染到 HDR 颜色图(线性光照值)。
|
||||
// 显示变换不在本 pass——HDRColor 经 Tonemap 归一后才写 Backbuffer
|
||||
// (对齐 UE/Unity:SceneColor(HDR) → tonemapper → UI)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_FORWARDPASS_H
|
||||
#define HELLOVULKAN_FORWARDPASS_H
|
||||
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
#include <Engine/Renderer/Passes/SceneGpu.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
|
||||
namespace Engine::Passes {
|
||||
|
||||
class ForwardPass final : public RenderPass {
|
||||
public:
|
||||
ForwardPass(SceneGpu& gpu, FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||
RG::TextureHandle depth,
|
||||
RG::TextureHandle sceneColor,
|
||||
const std::array<RG::TextureHandle, kMaxCascades>& shadowMaps)
|
||||
: m_Gpu(gpu), m_Frame(frame),
|
||||
m_HDRColor(hdrColor), m_Velocity(velocity),
|
||||
m_Depth(depth), m_SceneColor(sceneColor),
|
||||
m_ShadowMaps(shadowMaps) {}
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
||||
|
||||
private:
|
||||
// 两段渲染共享的命令录制(shader/描述符绑定 + 几何/光栅/视口状态)。
|
||||
// translucent=true 绑定透射变体(折射采样场景色副本);
|
||||
// 深度写与混合状态由各渲染实例作为初始态设置,见 ForwardPass.cpp
|
||||
void RecordSharedState(const RG::RenderGraphContext& ctx,
|
||||
vk::CommandBuffer cmd, bool translucent) const;
|
||||
|
||||
// 透明队列绘制单位总数(= 各批实例数之和;排序后扁平单位序)
|
||||
u32 TransparentUnitCount() const;
|
||||
|
||||
// 绘制透明单位区间 [start,end)(批次内连续实例合并为一次 draw):
|
||||
// 透明渲染实例 + 预乘 alpha 混合 + 只测不写深度的全部状态
|
||||
void DrawTransparentRange(const RG::RenderGraphContext& ctx,
|
||||
vk::CommandBuffer cmd,
|
||||
u32 start, u32 end) const;
|
||||
|
||||
SceneGpu& m_Gpu;
|
||||
FrameSceneData& m_Frame;
|
||||
RG::TextureHandle m_HDRColor;
|
||||
RG::TextureHandle m_Velocity; // TAA 运动向量(第二颜色附件)
|
||||
RG::TextureHandle m_Depth;
|
||||
RG::TextureHandle m_SceneColor; // 场景色副本(透射折射采样源)
|
||||
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps;
|
||||
};
|
||||
|
||||
} // namespace Engine::Passes
|
||||
|
||||
#endif //HELLOVULKAN_FORWARDPASS_H
|
||||
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// FullscreenGpu:全屏后处理共享装配(Tonemap/Bloom 系 pass 共用)。
|
||||
//
|
||||
// 全屏 pass 的共性收口在这里:Set0 = {输入0, 输入1}、push 16B 通用结构
|
||||
// (规避 VUID-08878 的"range 定义必须一致")、无顶点输入全屏三角、
|
||||
// 公共录制函数。新增全屏效果 = 复用本类 + 新增一个 frag shader。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_FULLSCREENGPU_H
|
||||
#define HELLOVULKAN_FULLSCREENGPU_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/RenderGraph/RenderGraph.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class DescriptorManager;
|
||||
|
||||
namespace Passes {
|
||||
|
||||
// 全屏 pass 通用 push(16B):所有全屏 pass 统一声明与推送
|
||||
struct FullscreenPush {
|
||||
f32 exposure;
|
||||
f32 param0; // 各 pass 自定语义(tonemap: aces 开关)
|
||||
f32 param1;
|
||||
f32 param2;
|
||||
};
|
||||
|
||||
// 后处理数值参数(ImGui 可调;Renderer 持有,pass 按引用读取)。
|
||||
// 运行时开关/调试视图不在这里——统一进 FrameSceneData(帧契约)
|
||||
struct PostProcessParams {
|
||||
f32 exposure = 0.6f;
|
||||
bool useACES = true;
|
||||
// HDR 域亮度阈值。注意不能设 1.0:光滑金属反射亮天空的 HDR 值
|
||||
// 普遍 2~50,阈值为 1 会让整片金属表面进 bloom → 白色泛光糊满
|
||||
//(怀表铜壳"纯白"元凶)。3.0 ≈ ACES 后接近白的真高光才泛光
|
||||
f32 bloomThreshold = 3.0f;
|
||||
f32 bloomStrength = 0.8f; // 叠加强度
|
||||
};
|
||||
|
||||
// 调试视图枚举:TonemapPass 按值把对应中间目标路由上屏。
|
||||
// 与 Renderer 的 DebugViewNames 表下标一一对应
|
||||
enum DebugView : int {
|
||||
kDebugFinal = 0, // 正常画面(tonemap 输出)
|
||||
kDebugTemporalDiff, // 帧历史差异(TAA 链路验证)
|
||||
kDebugHDR, // Forward 原始 HDR(无 bloom/TAA)
|
||||
kDebugTAAColor, // TAA resolve 后
|
||||
kDebugBloom, // bloom 模糊结果
|
||||
kDebugVelocity, // 运动向量(|uv| 增益显示)
|
||||
kDebugShadowMap0, // CSM 级联 0 深度(近=白)
|
||||
kDebugShadowMap1, // 级联 1
|
||||
kDebugShadowMap2, // 级联 2
|
||||
kDebugEnvCube, // IBL 环境 cube(经纬展开)
|
||||
kDebugIrradiance, // IBL 漫反射辐照度(32² 低频)
|
||||
kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级)
|
||||
kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道)
|
||||
kDebugTransparent, // 仅透明队列(Skybox/Forward 跳过绘制,看透明段内容)
|
||||
kDebugViewCount
|
||||
};
|
||||
|
||||
class FullscreenGpu {
|
||||
public:
|
||||
FullscreenGpu(Device& device, DescriptorManager& descriptors,
|
||||
ShaderManager& shaders);
|
||||
~FullscreenGpu(); // 只清 sampler/layout(shader 随引用自动销毁)
|
||||
|
||||
FullscreenGpu(const FullscreenGpu&) = delete;
|
||||
FullscreenGpu& operator=(const FullscreenGpu&) = delete;
|
||||
|
||||
vk::DescriptorSetLayout setLayout; // Set0 = {输入0, 输入1, 调试输入}
|
||||
vk::PipelineLayout layout; // push 16B(FullscreenPush)
|
||||
vk::Sampler sampler;
|
||||
// shader 引用计数句柄(析构自动销毁,零手工清理)
|
||||
ShaderManager::ShaderRef vert, extractFrag, blurFrag, tonemapFrag;
|
||||
ShaderManager::ShaderRef diffFrag; // temporal_diff(帧历史调试)
|
||||
ShaderManager::ShaderRef taaFrag; // taa(时域抗锯齿 resolve)
|
||||
|
||||
// 各 pass 自己持有 per-slot set;分配统一走这里
|
||||
vk::DescriptorSet AllocateSet();
|
||||
|
||||
// 输入图片绑定(RG 重编译后重绑;物理镜像按槽位各一份)
|
||||
void UpdateSetInput(vk::DescriptorSet set, u32 binding,
|
||||
RG::RenderGraph& rg, RG::TextureHandle input,
|
||||
u32 slot);
|
||||
|
||||
// 全屏 pass 公共录制:动态状态/draw 全一致,仅附件 view、
|
||||
// 输入 set、push 值不同
|
||||
void RecordFullscreen(RG::RenderGraphContext& ctx, vk::ImageView view,
|
||||
const ShaderManager::ShaderRef& frag,
|
||||
const vk::DescriptorSet& set,
|
||||
const FullscreenPush& push);
|
||||
|
||||
private:
|
||||
Device& m_Device;
|
||||
DescriptorManager& m_Descriptors;
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_FULLSCREENGPU_H
|
||||
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// HitProxyPass:UE 式 GPU 拾取(像素级 id pass + 点击 readback)。
|
||||
//
|
||||
// 两个 RG pass:
|
||||
// "HitProxy" —— 拾取请求帧把不透明 + 透明 batch 用 id 管线画进
|
||||
// R32Uint 目标(片元值 = 实体位模式)。深度自测自写
|
||||
// (重清主深度):遮挡语义 = 可见表面胜出;运行在
|
||||
// Forward/ForwardTransL*/PhysicsDebug 之后的 WAW 链尾,
|
||||
// 主链下游无人再读深度,下帧 Forward 照常重新 Clear
|
||||
// "HitProxyCopy" —— 请求像素 1×1 CopyImageToBuffer 进 host-visible
|
||||
// 暂存。TransferSrc use 让 RG 自动完成
|
||||
// eColorAttachmentOptimal → eTransferSrcOptimal 迁移
|
||||
//
|
||||
// 无请求帧零绘制零拷贝(barrier 照常插入,GPU 无实际工作)。
|
||||
// 结果流:Renderer::TakePickResult(waitIdle 保证提交完成)→ 映射暂存。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_HITPROXYPASS_H
|
||||
#define HELLOVULKAN_HITPROXYPASS_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
#include <Engine/Vulkan/Buffer.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class ShaderManager;
|
||||
class DescriptorManager;
|
||||
class UniformBuffer;
|
||||
|
||||
namespace Passes {
|
||||
|
||||
class SceneGpu;
|
||||
|
||||
class HitProxyPass final : public RenderPass {
|
||||
public:
|
||||
HitProxyPass(Device& device, MemoryAllocator& allocator,
|
||||
ShaderManager& shaders, DescriptorManager& descriptors,
|
||||
SceneGpu& gpu, FrameSceneData& frame,
|
||||
RG::TextureHandle idTarget, RG::TextureHandle depth);
|
||||
~HitProxyPass();
|
||||
|
||||
HitProxyPass(const HitProxyPass&) = delete;
|
||||
HitProxyPass& operator=(const HitProxyPass&) = delete;
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
|
||||
// 有待取结果(拷贝已录制进当帧命令缓冲)
|
||||
bool HasPendingResult() const { return m_ResultValid; }
|
||||
// 取走拷贝结果(一次性)。调用方保证 GPU 已 idle
|
||||
//(Renderer::TakePickResult 内 waitIdle)
|
||||
bool TakeResult(u32& outBits);
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
// Editor gizmo 拾取代理壳接入(editor 专属;Renderer
|
||||
// BuildGraph 内 Attach——共享 gizmo 代理 ring VB,本 pass
|
||||
// 用 gizmo_pick 管线重画进 R32Uint,与 mesh 同一深度解算)
|
||||
void AttachGizmoProxy(UniformBuffer& vb) { m_GizmoProxyVB = &vb; }
|
||||
#endif
|
||||
|
||||
private:
|
||||
Device& m_Device;
|
||||
SceneGpu& m_Gpu;
|
||||
FrameSceneData& m_Frame;
|
||||
RG::TextureHandle m_IdTarget;
|
||||
RG::TextureHandle m_Depth;
|
||||
|
||||
vk::PipelineLayout m_Layout; // Set0 = uboSetLayout,无 push
|
||||
ShaderManager::ShaderRef m_Vert, m_Frag; // 析构自动销毁
|
||||
|
||||
// 1×1 像素暂存(host visible,持久映射);拷贝录制时置位
|
||||
Scope<Buffer> m_Staging;
|
||||
bool m_ResultValid = false;
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
// gizmo 代理壳拾取(editor 专属,game 构建零成员):独立
|
||||
// layout(无描述符,push = viewProj)+ gizmo_pick shader 对 +
|
||||
// 代理 ring VB 非拥有指针(Renderer 持有生命周期)
|
||||
vk::PipelineLayout m_GizmoLayout;
|
||||
ShaderManager::ShaderRef m_GizmoVert, m_GizmoFrag;
|
||||
UniformBuffer* m_GizmoProxyVB = nullptr;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_HITPROXYPASS_H
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// ImGuiPass:叠加层(loadOp=Load,不清屏)。
|
||||
// 框架只负责把 draw data 录进 cmd;UI 内容全部由派生类在
|
||||
// OnRenderOverlay() 里构建,框架自身不画任何 ImGui 控件。
|
||||
// 未启用叠加层时空转(begin/end 一次空 rendering region)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_IMGUIPASS_H
|
||||
#define HELLOVULKAN_IMGUIPASS_H
|
||||
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
#include <Engine/Renderer/SceneTypes.h> // FrameSceneData(displayMode 帧契约)
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class ImGuiLayer;
|
||||
|
||||
namespace Passes {
|
||||
|
||||
class ImGuiPass final : public RenderPass {
|
||||
public:
|
||||
// 二级指针:Renderer::SetImGuiLayer 在 Register 之后才可能被调
|
||||
//(EnableImGui 时机由派生类决定),执行期解引用取最新值。
|
||||
// presentTarget:ImGui::Image 采样的场景纹理(tonemap 输出)。
|
||||
// loadOp 按 displayMode 分支:Direct = Load(blit 已铺场景),
|
||||
// Viewport = Clear(唯一写入者,清"桌面底色")
|
||||
// previewAtlas:材质球预览 SDR atlas(MaterialPreviewPass 写、
|
||||
// ImGui::Image 按槽位采样;声明读 = eSampled usage 推导 +
|
||||
// ColorAttachment→ShaderRead barrier)
|
||||
ImGuiPass(ImGuiLayer** layer, FrameSceneData& frame,
|
||||
RG::TextureHandle backbuffer, RG::TextureHandle presentTarget,
|
||||
RG::TextureHandle previewAtlas)
|
||||
: m_Layer(layer), m_Frame(frame), m_Backbuffer(backbuffer),
|
||||
m_PresentTarget(presentTarget), m_PreviewAtlas(previewAtlas) {}
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
|
||||
private:
|
||||
ImGuiLayer** m_Layer;
|
||||
FrameSceneData& m_Frame; // displayMode(帧契约)
|
||||
RG::TextureHandle m_Backbuffer;
|
||||
RG::TextureHandle m_PresentTarget;
|
||||
RG::TextureHandle m_PreviewAtlas;
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_IMGUIPASS_H
|
||||
@@ -0,0 +1,346 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/10/5.
|
||||
//
|
||||
// MaterialPreviewPass:材质球预览缩略图(editor 专属,HELLO_EDITOR 裁剪)。
|
||||
//
|
||||
// 架构对齐 UE FAssetThumbnailPool / Unity AssetPreview:
|
||||
// - 渲染一次缓存:tile 内容跨帧持久(HDR atlas loadOp=Load),只有
|
||||
// "脏 tile"(新材质 / 参数版本变化 / 镜像失效)消耗每帧渲染预算。
|
||||
// RG 物理镜像按帧槽位各一份(in-flight 三缓冲),tile 完成标准 =
|
||||
// 三个槽位的镜像都画过(paintedSlots 位掩码)——否则 ImGui 按当前
|
||||
// 槽位采样会 1/3 帧有图 2/3 帧是垃圾(按帧率闪烁)。完成后干净帧
|
||||
// 两 pass 零 GPU 工作
|
||||
// - 只请求可见项:UI 对滚进视口的行发幂等 keepalive 请求,池按
|
||||
// Material 身份(serial + editVersion)判缓存命中/标脏
|
||||
// - 失效模型:Material::UpdateParams bump editVersion → 下帧重渲该
|
||||
// tile(拖滑块期间单 tile 实时,其余零 GPU 工作);材质销毁/换绑
|
||||
// 靠 lastUseFrame 老化 LRU 驱逐,池内不跨帧持有 Material 指针
|
||||
//
|
||||
// 渲染本体(与场景零复制的 WYSIWYG 契约,原样保留):
|
||||
// - 几何 = 程序化 UV 球(世界空间原点,无模型矩阵——预览 set0 的
|
||||
// ObjectBuffer 恒 identity,与 simple_shader.vert 的 GPUDriven
|
||||
// 契约兼容:gl_InstanceIndex=0 → models[0]=I)
|
||||
// - 光照 = 与场景完全相同的 simple_shader.vert/frag(同一 SPIR-V、
|
||||
// 同一描述符布局):set1 直接绑目标材质的描述符集
|
||||
// - set2 绑 SceneGpu::previewEnvSets(IBL 三件套锁定中性环境——UE
|
||||
// ThumbnailRenderer 语义:观察条件恒定,金属球不随场景环境变黑;
|
||||
// 级联/场景色副本与场景同步)
|
||||
// - 固定棚拍布光(自建 set0 UBO,内容恒定只写一次)
|
||||
//
|
||||
// 两个固定尺寸 atlas(不随渲染分辨率缩放):
|
||||
// PreviewHDR(R16G16B16A16Sfloat)← 本 pass 按脏 tile 清矩形 + 绘制
|
||||
// PreviewSDR(R8G8B8A8Unorm) ← tonemap(仅有渲染的帧执行),
|
||||
// ImGuiPass 声明读,ImGui 按槽位采样
|
||||
//
|
||||
// 本 pass 消费 Renderer 每帧换入的脏 tile 表(PreviewTilePool::TakeRenders
|
||||
// 产出,预算 kPreviewRendersPerFrame);表空 = 两 pass 零 GPU 工作。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_MATERIALPREVIEWPASS_H
|
||||
#define HELLOVULKAN_MATERIALPREVIEWPASS_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Material.h> // 完整类型:池内联函数读 serial/editVersion
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class MemoryAllocator;
|
||||
class DescriptorManager;
|
||||
class UniformBuffer;
|
||||
class Mesh;
|
||||
class Material;
|
||||
class Cubemap;
|
||||
class Buffer;
|
||||
|
||||
namespace Passes {
|
||||
|
||||
class SceneGpu;
|
||||
class FullscreenGpu;
|
||||
|
||||
// atlas 布局:5×5 tile × 256²。tile 总数 = 并发池容量(不是槽位
|
||||
// 上限):UI 只对可见行请求,池按 LRU 换页支撑任意材质数
|
||||
inline constexpr u32 kPreviewAtlasSize = 1280;
|
||||
inline constexpr u32 kPreviewTileSize = 256;
|
||||
inline constexpr u32 kPreviewTilesPerSide = 5;
|
||||
inline constexpr u32 kPreviewTileCount =
|
||||
kPreviewTilesPerSide * kPreviewTilesPerSide;
|
||||
// 单 tile 回读字节数(SDR atlas 为 RGBA8)
|
||||
inline constexpr u32 kPreviewTileBytes =
|
||||
kPreviewTileSize * kPreviewTileSize * 4;
|
||||
|
||||
// 本帧待渲染的脏 tile(Material* 来自本帧 UI 请求,活指针)
|
||||
struct PreviewDirtyTile {
|
||||
Material* material = nullptr;
|
||||
u32 tile = 0;
|
||||
};
|
||||
|
||||
// 本帧待回读烘焙的 tile(key = 调用方令牌,Renderer 不解释)。
|
||||
// 只收录"本帧确实被渲染"的 tile,故条数 ≤ 渲染预算
|
||||
struct PreviewBakeTile {
|
||||
u64 key = 0;
|
||||
u32 tile = 0;
|
||||
};
|
||||
|
||||
// 编辑器 tile 池(Renderer 持有,UE FAssetThumbnailPool 语义)。
|
||||
// 缓存键 = Material serial(出生唯一号,防指针地址复用的 ABA)
|
||||
// + editVersion(参数代数)。游戏构建整体编译裁剪,无运行时成本
|
||||
class PreviewTilePool {
|
||||
public:
|
||||
// 每帧渲染预算:脏 tile 多时逐帧消化(浏览器缩略图烘焙 / 滚动
|
||||
// 列表渐进出图)。预览球是 128 三角形的 256² 直绘,8 个/帧的
|
||||
// GPU 成本可忽略;烘焙吞吐 = 预算(回读只收当帧渲染过的 tile)
|
||||
static constexpr u32 kPreviewRendersPerFrame = 8;
|
||||
|
||||
// UI 构建段逐可见行调用(幂等 keepalive,刷新 LRU + 脏检测)。
|
||||
// 返回 tile:>=0 = 三槽位镜像内容齐备可显示(含重渲期间的旧
|
||||
// 内容——拖滑块先显示旧球,三帧内全部槽位收敛到新球,无闪烁);
|
||||
// -1 = 池满不可驱逐 / 尚未画满三槽位(UI 显示占位)
|
||||
i32 Request(Material* material)
|
||||
{
|
||||
if (!material) return -1;
|
||||
const u64 serial = material->GetSerial();
|
||||
|
||||
Entry* slot = nullptr;
|
||||
for (auto& e : m_Entries) {
|
||||
if (e.serial == serial) { slot = &e; break; }
|
||||
}
|
||||
if (!slot) {
|
||||
// 分配:空槽优先;满则驱逐最久未用(仅限本帧未使用
|
||||
// 者——可见行并发数 < 容量时不发生)
|
||||
u64 oldest = ~0ull;
|
||||
for (auto& e : m_Entries) {
|
||||
if (e.serial == 0) { slot = &e; break; }
|
||||
if (e.lastUseFrame < m_Frame &&
|
||||
e.lastUseFrame < oldest) {
|
||||
oldest = e.lastUseFrame;
|
||||
slot = &e;
|
||||
}
|
||||
}
|
||||
if (!slot) return -1;
|
||||
slot->serial = serial;
|
||||
slot->paintedSlots = 0;
|
||||
slot->trackedVersion = 0;
|
||||
}
|
||||
|
||||
const u32 tileIdx =
|
||||
static_cast<u32>(slot - m_Entries.data());
|
||||
slot->lastUseFrame = m_Frame;
|
||||
|
||||
const u32 ver = material->GetEditVersion();
|
||||
const bool displayable =
|
||||
slot->paintedSlots == kAllSlotsMask;
|
||||
if (!displayable || slot->trackedVersion != ver) {
|
||||
// 标脏(同 tile 本帧去重——同一材质可被多行共享)。
|
||||
// 未画满三槽位的 tile 每帧重入队,直到逐槽位补齐;
|
||||
// 重入队刷新活指针(跳帧滞留也不会引用已销毁材质)
|
||||
bool queued = false;
|
||||
for (auto& d : m_FrameDirty) {
|
||||
if (d.tile == tileIdx) {
|
||||
d.material = material;
|
||||
queued = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!queued) {
|
||||
m_FrameDirty.push_back({material, tileIdx});
|
||||
}
|
||||
}
|
||||
return displayable ? static_cast<i32>(tileIdx) : -1;
|
||||
}
|
||||
|
||||
// 烘焙请求(浏览器缩略图回读):强制标脏——即使 tile 已
|
||||
// displayable 也重渲一次,保证"当帧渲染 → 当帧拷贝回读"闭环
|
||||
// (回读只收当帧渲染过的 tile)。返回槽位 tile 索引;-1 =
|
||||
// 池满无槽(调用方下帧重试)。不参与 paintedSlots 记账
|
||||
i32 RequestBake(Material* material)
|
||||
{
|
||||
if (!material) return -1;
|
||||
const u64 serial = material->GetSerial();
|
||||
Entry* slot = nullptr;
|
||||
for (auto& e : m_Entries) {
|
||||
if (e.serial == serial) { slot = &e; break; }
|
||||
}
|
||||
if (!slot) {
|
||||
u64 oldest = ~0ull;
|
||||
for (auto& e : m_Entries) {
|
||||
if (e.serial == 0) { slot = &e; break; }
|
||||
if (e.lastUseFrame < m_Frame &&
|
||||
e.lastUseFrame < oldest) {
|
||||
oldest = e.lastUseFrame;
|
||||
slot = &e;
|
||||
}
|
||||
}
|
||||
if (!slot) return -1;
|
||||
slot->serial = serial;
|
||||
slot->paintedSlots = 0;
|
||||
slot->trackedVersion = 0;
|
||||
}
|
||||
slot->lastUseFrame = m_Frame;
|
||||
const u32 tileIdx =
|
||||
static_cast<u32>(slot - m_Entries.data());
|
||||
bool queued = false;
|
||||
for (auto& d : m_FrameDirty) {
|
||||
if (d.tile == tileIdx) { d.material = material; queued = true; break; }
|
||||
}
|
||||
if (!queued) m_FrameDirty.push_back({material, tileIdx});
|
||||
return static_cast<i32>(tileIdx);
|
||||
}
|
||||
|
||||
// Render 段取本帧预算内的脏 tile(frameSlot = 本帧渲染槽位,
|
||||
// 该槽位镜像在本帧被写入)。渲染语义(清矩形+画球)由 pass
|
||||
// 执行后该槽位内容即有效。剩余脏 tile 保持未取——下帧可见行
|
||||
// keepalive 请求会以新活指针重新入队,池绝不跨帧持有指针
|
||||
const std::vector<PreviewDirtyTile>& TakeRenders(u32 frameSlot)
|
||||
{
|
||||
++m_Frame;
|
||||
m_Active.clear();
|
||||
const u32 budget = std::min<u32>(
|
||||
kPreviewRendersPerFrame,
|
||||
static_cast<u32>(m_FrameDirty.size()));
|
||||
for (u32 i = 0; i < budget; ++i) {
|
||||
const auto& d = m_FrameDirty[i];
|
||||
Entry& e = m_Entries[d.tile];
|
||||
e.trackedVersion = d.material->GetEditVersion();
|
||||
e.paintedSlots |= (1u << frameSlot);
|
||||
m_Active.push_back(d);
|
||||
}
|
||||
m_FrameDirty.clear();
|
||||
return m_Active;
|
||||
}
|
||||
|
||||
// 材质实例即将销毁/重建(ReloadMaterialAsset):清脏表引用
|
||||
// + 按 serial 清槽位。同 UI 段"Request → Reload"顺序下,脏
|
||||
// 表里的裸指针当帧 Render 段会被解引用——必须先 drop
|
||||
void DropMaterial(const Material* material)
|
||||
{
|
||||
if (!material) return;
|
||||
const u64 serial = material->GetSerial();
|
||||
std::erase_if(m_FrameDirty,
|
||||
[&](const PreviewDirtyTile& d)
|
||||
{ return d.material == material; });
|
||||
for (auto& e : m_Entries) {
|
||||
if (e.serial == serial) {
|
||||
e.serial = 0;
|
||||
e.paintedSlots = 0;
|
||||
e.trackedVersion = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RG 重编译后物理镜像全部换新(三槽位内容清零)——池记录的
|
||||
// paintedSlots 全部失效。可见行 keepalive 请求自然重入队补画
|
||||
void InvalidateAll()
|
||||
{
|
||||
for (auto& e : m_Entries) {
|
||||
e.paintedSlots = 0;
|
||||
e.trackedVersion = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
u64 serial = 0; // 0 = 空槽(Material serial 从 1 起)
|
||||
u32 trackedVersion = 0; // atlas 内容对应的参数版本
|
||||
u64 lastUseFrame = 0; // LRU / 老化驱逐判据
|
||||
u32 paintedSlots = 0; // 已画过的帧槽位镜像位掩码
|
||||
};
|
||||
// 全槽位掩码:三份 in-flight 镜像都画过 = 内容可显示
|
||||
static constexpr u32 kAllSlotsMask =
|
||||
(1u << kMaxFramesInFlight) - 1;
|
||||
std::array<Entry, kPreviewTileCount> m_Entries;
|
||||
std::vector<PreviewDirtyTile> m_FrameDirty; // 本帧标脏(UI 段)
|
||||
std::vector<PreviewDirtyTile> m_Active; // 预算内(Render 段)
|
||||
u64 m_Frame = 0;
|
||||
};
|
||||
|
||||
class MaterialPreviewPass final : public RenderPass {
|
||||
public:
|
||||
// dirtyTiles = Renderer 持有的当帧脏表(Render 段换入,只读)
|
||||
// bakeTiles = Renderer 持有的当帧回读表(只读;条数 ≤ 渲染预算)
|
||||
MaterialPreviewPass(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptors,
|
||||
SceneGpu& gpu, FullscreenGpu& fullscreen,
|
||||
RG::TextureHandle hdrAtlas,
|
||||
RG::TextureHandle velocityAtlas,
|
||||
RG::TextureHandle sdrAtlas,
|
||||
const std::vector<PreviewDirtyTile>&
|
||||
dirtyTiles,
|
||||
const std::vector<PreviewBakeTile>&
|
||||
bakeTiles);
|
||||
~MaterialPreviewPass() override;
|
||||
|
||||
// 本槽位的回读暂存(host-visible,持久映射):Renderer 在该
|
||||
// 槽位 fence 通过后 memcpy 取走烘焙像素
|
||||
Buffer* BakeStaging(u32 frameSlot) const
|
||||
{
|
||||
return frameSlot < kMaxFramesInFlight
|
||||
? m_BakeStaging[frameSlot].get()
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
||||
// 调试视图 cube 槽(binding 8-11)绑真环境资源——cube 槽类型
|
||||
// 必须 Cube,2D atlas 占位会触发 VUID-07752
|
||||
void OnEnvironmentChanged(const Cubemap* env) override;
|
||||
|
||||
private:
|
||||
// 单个材质球:tile 视口/裁剪 + 材质 set1 + 混合态 + 球绘制
|
||||
void RecordBall(vk::CommandBuffer cmd, u32 tile,
|
||||
const Material& material) const;
|
||||
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
DescriptorManager& m_Descriptors;
|
||||
SceneGpu& m_Gpu;
|
||||
FullscreenGpu& m_Fullscreen;
|
||||
|
||||
RG::TextureHandle m_HDRAtlas;
|
||||
RG::TextureHandle m_VelocityAtlas;
|
||||
RG::TextureHandle m_SDRAtlas;
|
||||
const std::vector<PreviewDirtyTile>& m_DirtyTiles;
|
||||
const std::vector<PreviewBakeTile>& m_BakeTiles;
|
||||
|
||||
// 回读暂存:每帧槽位一份(容量 = 渲染预算 × 单 tile 字节)。
|
||||
// 拷贝录制在该槽位命令缓冲,Renderer 在该槽位 fence 通过后读
|
||||
std::array<Scope<Buffer>, kMaxFramesInFlight> m_BakeStaging;
|
||||
|
||||
// ---- 预览专用 set0(与 uboSetLayout 同布局,内容恒定)----
|
||||
// UBO = 固定棚拍相机/布光;SSBO = GPUDriven 契约的最小占位
|
||||
//(1 实例 identity 矩阵 + skin slot0 identity + 实体 id 占位,
|
||||
// vert 按 gl_InstanceIndex=0 读取)
|
||||
Scope<UniformBuffer> m_PreviewUbo;
|
||||
Scope<UniformBuffer> m_ObjectBuffer;
|
||||
Scope<UniformBuffer> m_PrevObjectBuffer;
|
||||
Scope<UniformBuffer> m_SkinOffsetBuffer;
|
||||
Scope<UniformBuffer> m_SkinPaletteBuffer;
|
||||
Scope<UniformBuffer> m_EntityIdBuffer;
|
||||
vk::DescriptorSet m_Set0;
|
||||
|
||||
// 程序化 UV 球(世界空间原点半径 1;材质非拥有 = nullptr,
|
||||
// 材质在绘制时经 set1 绑定,不进 Mesh)
|
||||
Scope<Mesh> m_Sphere;
|
||||
|
||||
// tonemap per-slot set(FullscreenGpu 布局:binding0 = HDR atlas,
|
||||
// binding1 同源占位——tonemap.frag 终值路径恒采样 bloom 图,
|
||||
// 强度 0 丢弃)
|
||||
std::array<vk::DescriptorSet, kMaxFramesInFlight> m_TonemapSets;
|
||||
|
||||
// 当前环境(cube 调试槽源;注册时与 SetEnvironment 时由
|
||||
// Renderer 通知)
|
||||
const Cubemap* m_Env = nullptr;
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_MATERIALPREVIEWPASS_H
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// OutlinePass:选中描边后处理(UE 编辑器选中轮廓语义)。
|
||||
//
|
||||
// 对 SelectionMask 做边缘检测,alpha 混合叠加在 Tonemap 之后的
|
||||
// PresentTarget 上——描边是恒定 LDR UI 色,不进 bloom/曝光/TAA。
|
||||
// 无选中帧跳过绘制(SideEffect 保活,见 HitProxy 教训)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_OUTLINEPASS_H
|
||||
#define HELLOVULKAN_OUTLINEPASS_H
|
||||
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
namespace Passes {
|
||||
|
||||
class FullscreenGpu;
|
||||
|
||||
class OutlinePass final : public RenderPass {
|
||||
public:
|
||||
// color:描边色(LDR);widthPx:描边宽度(物理像素)
|
||||
OutlinePass(FullscreenGpu& gpu, ShaderManager& shaders,
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle mask, RG::TextureHandle present,
|
||||
const glm::vec3& color = glm::vec3(1.0f, 0.62f, 0.05f),
|
||||
f32 widthPx = 6.0f);
|
||||
|
||||
OutlinePass(const OutlinePass&) = delete;
|
||||
OutlinePass& operator=(const OutlinePass&) = delete;
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
||||
|
||||
private:
|
||||
FullscreenGpu& m_Gpu;
|
||||
FrameSceneData& m_Frame;
|
||||
RG::TextureHandle m_Mask;
|
||||
RG::TextureHandle m_Present;
|
||||
glm::vec3 m_Color;
|
||||
f32 m_WidthPx;
|
||||
ShaderManager::ShaderRef m_Frag; // 析构自动销毁
|
||||
std::vector<vk::DescriptorSet> m_Sets; // per-frame slot
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_OUTLINEPASS_H
|
||||
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// PhysicsDebugPass:调试图元叠加 pass(双源)。
|
||||
// 物理源 = PhysicsWorld 收集的 PhysicsDebugFrame(collider 线框/
|
||||
// 约束,Application 注入帧缓冲指针)
|
||||
// 通用源 = Engine::Debug 调试服务(脚本/任意层的 Line/Arrow/Wire*,
|
||||
// Unity Debug.DrawLine 同位;批次自带线宽/深度测试/拓扑)
|
||||
//
|
||||
// 画在 HDRColor(Forward 之后、TAA 之前):调试图元进入场景主链路——
|
||||
// TAA 时域累积抗锯齿、Bloom/Tonemap 正常消费,kDebugTransparent 诊断
|
||||
// 模式下也可见。深度只测不写(被不透明几何正确遮挡),无混合直写
|
||||
// (fullscreen pass 状态泄漏教训)。
|
||||
//
|
||||
// 空帧零绘制:两源顶点数均 0 时提前短路。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSDEBUGPASS_H
|
||||
#define HELLOVULKAN_PHYSICSDEBUGPASS_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Debug/DebugDraw.h>
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
#include <Engine/Vulkan/UniformBuffer.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
#include <vector>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
namespace Engine {
|
||||
struct PhysicsDebugFrame; // PhysicsDebugTypes.h(Jolt 零依赖契约)
|
||||
|
||||
class Device;
|
||||
class ShaderManager;
|
||||
class DescriptorManager;
|
||||
|
||||
namespace Passes {
|
||||
|
||||
class PhysicsDebugPass final : public RenderPass {
|
||||
public:
|
||||
// 顶点预算(每帧槽位,按顶点数):物理调试几十个 body 的
|
||||
// 线框 + 箭头远用不满;溢出截断并告警一次
|
||||
static constexpr u32 kMaxLineVerts = 1 << 16;
|
||||
static constexpr u32 kMaxTriVerts = 1 << 16;
|
||||
|
||||
PhysicsDebugPass(Device& device, MemoryAllocator& allocator,
|
||||
ShaderManager& shaders,
|
||||
DescriptorManager& descriptors,
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor,
|
||||
RG::TextureHandle depth,
|
||||
UniformBuffer& lineVB, UniformBuffer& triVB,
|
||||
UniformBuffer& debugVB);
|
||||
~PhysicsDebugPass();
|
||||
|
||||
PhysicsDebugPass(const PhysicsDebugPass&) = delete;
|
||||
PhysicsDebugPass& operator=(const PhysicsDebugPass&) = delete;
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
|
||||
// 每帧 Renderer::Render 里(该槽位 fence 等待后)上传当帧
|
||||
// 图元。dbg 为 nullptr = 通用调试源禁用/空
|
||||
void PrepareFrame(u32 frameSlot, const PhysicsDebugFrame& src,
|
||||
const Debug::DebugFrame* dbg);
|
||||
|
||||
private:
|
||||
// 一个可绘制段:独立拓扑/线宽/深度测试(动态状态逐批设置)
|
||||
struct Span {
|
||||
bool tris;
|
||||
f32 width;
|
||||
bool depthTest;
|
||||
u32 first; // 顶点偏移(对应 VB 内顶点序号)
|
||||
u32 count;
|
||||
};
|
||||
|
||||
Device& m_Device;
|
||||
DescriptorManager& m_Descriptors;
|
||||
FrameSceneData& m_Frame;
|
||||
RG::TextureHandle m_HDRColor;
|
||||
RG::TextureHandle m_Depth;
|
||||
UniformBuffer& m_LineVB; // per-slot ring(物理源,Renderer 持有)
|
||||
UniformBuffer& m_TriVB;
|
||||
UniformBuffer& m_DebugVB; // 通用调试源 ring(Renderer 持有)
|
||||
|
||||
vk::PipelineLayout m_Layout; // 无描述符,push = viewProj 64B
|
||||
ShaderManager::ShaderRef m_Vert, m_Frag; // 析构自动销毁
|
||||
|
||||
std::array<u32, kMaxFramesInFlight> m_LineCounts{};
|
||||
std::array<u32, kMaxFramesInFlight> m_TriCounts{};
|
||||
std::array<std::vector<Span>, kMaxFramesInFlight> m_Spans;
|
||||
bool m_TruncationWarned = false;
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSDEBUGPASS_H
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// PresentPass:transition 到 ePresentSrcKHR。
|
||||
// layout 转换由 RG 的 barrier 完成,实际 vkQueuePresentKHR 由 Renderer 驱动。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PRESENTPASS_H
|
||||
#define HELLOVULKAN_PRESENTPASS_H
|
||||
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
|
||||
namespace Engine::Passes {
|
||||
|
||||
class PresentPass final : public RenderPass {
|
||||
public:
|
||||
explicit PresentPass(RG::TextureHandle backbuffer)
|
||||
: m_Backbuffer(backbuffer) {}
|
||||
|
||||
void Register(RG::RenderGraph& rg) override
|
||||
{
|
||||
rg.AddPass("Present", [](RG::RenderGraphContext&) {})
|
||||
.Read(m_Backbuffer, RG::ResourceUse::Present);
|
||||
}
|
||||
|
||||
private:
|
||||
RG::TextureHandle m_Backbuffer;
|
||||
};
|
||||
|
||||
} // namespace Engine::Passes
|
||||
|
||||
#endif //HELLOVULKAN_PRESENTPASS_H
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// RenderPass:渲染 pass 对象基类(Unity ScriptableRenderPass 思路)。
|
||||
//
|
||||
// pass 自己负责两件事,框架(Renderer)不再手写"超级函数":
|
||||
// Register —— 向 RenderGraph 声明资源使用 + 录制 lambda;
|
||||
// 执行序由 RG 拓扑排序决定(writer 必先于 reader),
|
||||
// 与注册顺序无关
|
||||
// OnTargetsRebuilt —— RG 重编译(resize / vsync 切换)后重绑描述符
|
||||
// (物理镜像按槽位可能全部换新)
|
||||
//
|
||||
// pass 之间不互相依赖:数据流只经 RG 资源句柄。新增效果 = 新增一个
|
||||
// RenderPass 子类 + Renderer::BuildGraph 里注册一行,其它代码不动。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_RENDERPASS_H
|
||||
#define HELLOVULKAN_RENDERPASS_H
|
||||
|
||||
#include <Engine/RenderGraph/RenderGraph.h>
|
||||
|
||||
namespace Engine {
|
||||
class Cubemap;
|
||||
}
|
||||
|
||||
namespace Engine::Passes {
|
||||
|
||||
class RenderPass {
|
||||
public:
|
||||
virtual ~RenderPass() = default;
|
||||
|
||||
virtual void Register(RG::RenderGraph& rg) = 0;
|
||||
virtual void OnTargetsRebuilt(RG::RenderGraph& rg) { (void)rg; }
|
||||
|
||||
// 环境资产换绑(动态切换):引用环境描述符的 pass 重绑自己的 set。
|
||||
// 默认无环境影响;env 恒非空(Renderer 有中性兜底)
|
||||
virtual void OnEnvironmentChanged(const Cubemap* /*env*/) {}
|
||||
};
|
||||
|
||||
} // namespace Engine::Passes
|
||||
|
||||
#endif //HELLOVULKAN_RENDERPASS_H
|
||||
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// SceneGpu:场景级 GPU 共享装配(Shadow/Forward 及未来几何类 pass 共用)。
|
||||
//
|
||||
// 从 Renderer 拆出的装配体:set 布局 / pipeline 布局 / shader object /
|
||||
// 采样器 / per-slot 描述符集合。新增几何 pass(延迟、调试线框等)直接
|
||||
// 复用本装配,不必碰 Renderer。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_SCENEGPU_H
|
||||
#define HELLOVULKAN_SCENEGPU_H
|
||||
|
||||
#include <Engine/Core/FrameContext.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
#include <Engine/RenderGraph/RenderGraph.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class DescriptorManager;
|
||||
class UniformBuffer;
|
||||
class Cubemap;
|
||||
class Buffer;
|
||||
class MemoryAllocator;
|
||||
|
||||
namespace Passes {
|
||||
|
||||
class SceneGpu {
|
||||
public:
|
||||
SceneGpu(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptors,
|
||||
ShaderManager& shaders,
|
||||
UniformBuffer& ubo, UniformBuffer& objectBuffer,
|
||||
UniformBuffer& prevObjectBuffer,
|
||||
UniformBuffer& skinOffsetBuffer,
|
||||
UniformBuffer& skinPaletteBuffer,
|
||||
UniformBuffer& entityIdBuffer);
|
||||
~SceneGpu(); // 只清 sampler/layout/set(shader 随引用自动销毁)
|
||||
|
||||
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,
|
||||
// SkinOffsets, SkinPalettes, EntityIds}
|
||||
vk::DescriptorSetLayout textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO}
|
||||
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube,
|
||||
// prefiltered cube, BRDF LUT, 场景色副本}
|
||||
vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO)
|
||||
vk::PipelineLayout shadowLayout; // Set0,push = lightVP 64B
|
||||
|
||||
// per-slot 描述符:Set0(UBO+实例 SSBO)与 Set2(阴影图采样)
|
||||
std::vector<vk::DescriptorSet> uboSets;
|
||||
std::vector<vk::DescriptorSet> shadowSets;
|
||||
|
||||
// 材质预览锁定环境集(Set2 同布局,editor 专属——HELLO_EDITOR
|
||||
// 裁剪):级联/场景色副本随 BindShadowMaps/BindSceneColor 与
|
||||
// shadowSets 同步重绑(几何资源,无环境语义);IBL 三件套只在
|
||||
// LockPreviewEnvironment 绑定一次后锁定——材质球观察条件不随
|
||||
// 场景 SetEnvironment 漂移(UE ThumbnailRenderer 语义)
|
||||
#ifdef HELLO_EDITOR
|
||||
std::vector<vk::DescriptorSet> previewEnvSets;
|
||||
#endif
|
||||
|
||||
// 阴影采样器:clamp-to-border 白色 = 光视锥外自动无阴影
|
||||
//(省 shader 出界分支)
|
||||
vk::Sampler shadowSampler;
|
||||
|
||||
// 场景色副本采样器(binding 6):普通线性 clamp
|
||||
//(阴影采样器带硬件比较,不可用于常规 sampler2D)
|
||||
vk::Sampler sceneSampler;
|
||||
|
||||
// ---- 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;
|
||||
|
||||
// shader 引用计数句柄(析构自动销毁,零手工清理)
|
||||
ShaderManager::ShaderRef forwardVert, forwardFrag;
|
||||
// 透明变体:同 simple_shader + gl_FragDepth=1.0(见 frag 注释)
|
||||
ShaderManager::ShaderRef translucentFrag;
|
||||
ShaderManager::ShaderRef shadowVert, shadowFrag;
|
||||
|
||||
// RG 重编译后重绑阴影图(物理镜像按槽位各一份)
|
||||
void BindShadowMaps(RG::RenderGraph& rg,
|
||||
const std::array<RG::TextureHandle, kMaxCascades>& maps);
|
||||
|
||||
// RG 重编译后重绑场景色副本(Set2 binding 6,透射采样源)
|
||||
void BindSceneColor(RG::RenderGraph& rg, RG::TextureHandle sceneColor);
|
||||
|
||||
// 环境辐照度(Set2 binding 3):view 全程稳定,绑一次即可
|
||||
void BindEnvironment(const Cubemap& env);
|
||||
|
||||
// 材质预览锁定环境:只绑 previewEnvSets 的 IBL 三件套
|
||||
//(binding 3/4/5),一次调用永久生效;须在 Renderer 初始化
|
||||
//(环境尚为中性兜底)时调用
|
||||
void LockPreviewEnvironment(const Cubemap& env);
|
||||
|
||||
private:
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
DescriptorManager& m_Descriptors;
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_SCENEGPU_H
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user