Compare commits
104
Commits
99e6712cdf
..
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 |
@@ -1,4 +1,8 @@
|
|||||||
.idea/
|
.idea/
|
||||||
|
.trae/
|
||||||
|
|
||||||
|
# 运行日志(工作目录生成)
|
||||||
|
HelloVulkan.log
|
||||||
|
|
||||||
cmake-build-*/
|
cmake-build-*/
|
||||||
out/
|
out/
|
||||||
|
|||||||
+12
@@ -23,3 +23,15 @@
|
|||||||
[submodule "Lib/tracy"]
|
[submodule "Lib/tracy"]
|
||||||
path = Lib/tracy
|
path = Lib/tracy
|
||||||
url = https://github.com/wolfpld/tracy.git
|
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
|
||||||
|
|||||||
+19
-1
@@ -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)
|
target_link_libraries(HelloVulkan PRIVATE Engine)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
+212
-521
@@ -1,521 +1,212 @@
|
|||||||
//
|
//
|
||||||
// Created by atdunbg on 2026/9/20.
|
// Created by atdunbg on 2026/9/20.
|
||||||
//
|
//
|
||||||
// SandboxApp:第一个基于 Application 框架的程序。
|
// SandboxApp:基于 Application 框架的编辑器宿主程序。
|
||||||
//
|
//
|
||||||
// main 的全部职责:定义一个 Application 派生类并运行它。
|
// main 的职责边界:
|
||||||
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理;
|
// - 定义 Application 派生类并运行它
|
||||||
// 这里只写"程序逻辑":创建实体、逐帧更新组件、事件响应。
|
// - 编辑器形态驱动 EditorCamera(Scene view 专用相机,非场景对象)
|
||||||
//
|
// - 异步环境加载、全局热键、输入归属门控
|
||||||
|
// - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现
|
||||||
#include <Engine/Core/Application.h>
|
// 任何 ImGui 控件)
|
||||||
#include <Engine/Core/Log.h>
|
//
|
||||||
#include <Engine/Core/Stats.h>
|
// 场景内容(实体/组件)由编辑器 File 菜单 Save/Load 场景文件管理,
|
||||||
#include <Engine/Core/AsyncTask.h>
|
// 相机实体属于场景数据、只在游戏形态参与渲染(UE 语义)。
|
||||||
#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/Core/Application.h>
|
||||||
#include <Engine/Renderer/IblGenerator.h>
|
#include <Engine/Core/Time.h>
|
||||||
|
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
|
||||||
#include <imgui.h>
|
#include <Engine/Core/Log.h>
|
||||||
|
#include <Engine/Renderer/Renderer.h>
|
||||||
#include <glm/glm.hpp>
|
#include <Engine/Renderer/EditorCamera.h>
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <Engine/Serialization/SceneSerializer.h>
|
||||||
#include <glm/gtc/type_ptr.hpp>
|
|
||||||
|
#include <Engine/Core/Input.h>
|
||||||
#include <GLFW/glfw3.h>
|
|
||||||
|
#include <filesystem>
|
||||||
#include <atomic>
|
|
||||||
#include <thread>
|
// editor/game 编译期分离(HELLOVULKAN_EDITOR CMake option):editor 形态
|
||||||
|
// 挂接 docking Editor;game 形态 = Direct 直渲 + primary 相机,零编辑器代码
|
||||||
using namespace Engine;
|
#ifdef HELLO_EDITOR
|
||||||
using namespace Engine::ECS;
|
#include "Editor.h"
|
||||||
|
#include "EditorContext.h"
|
||||||
namespace
|
#endif
|
||||||
{
|
|
||||||
// 可切换环境(IBL 动态切换演示):路径 → AssetManager 缓存去重,
|
using namespace Engine;
|
||||||
// 重复选择同一环境零成本;首选路径触发一次 compute 预积分
|
|
||||||
constexpr const char* kEnvPaths[] = {
|
namespace
|
||||||
"assets/textures/venice_dawn_1_4k.hdr",
|
{
|
||||||
"assets/textures/rooitou_park_4k.hdr",
|
class SandboxApp final : public Application
|
||||||
"assets/textures/pink_sunrise_4k.hdr",
|
{
|
||||||
"assets/textures/birchwood_4k.hdr",
|
public:
|
||||||
};
|
SandboxApp()
|
||||||
constexpr const char* kEnvNames[] = {
|
: Application("HelloVulkan", 1920, 1080)
|
||||||
"Venice Dawn", "Rooitou Park", "Pink Sunrise", "Birchwood",
|
{
|
||||||
};
|
}
|
||||||
constexpr int kEnvCount = 4;
|
|
||||||
constexpr int kDefaultEnv = 0;
|
~SandboxApp() override
|
||||||
constexpr const char* kTemplePath = "assets/models/SunTemple/SunTemple.fbx";
|
{
|
||||||
|
}
|
||||||
class SandboxApp final : public Application
|
|
||||||
{
|
protected:
|
||||||
public:
|
// ---- 生命周期 ----
|
||||||
SandboxApp()
|
|
||||||
: Application("HelloVulkan", 800, 600)
|
void OnInit() override
|
||||||
{
|
{
|
||||||
}
|
Renderer& renderer = GetRenderer();
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
~SandboxApp() override
|
renderer.SetDisplayMode(kDisplayViewport);
|
||||||
{
|
|
||||||
// worker 只做 CPU 解码(不碰 Vulkan),在任何 GPU 资源
|
|
||||||
// 析构之前 join 即安全;派生析构先于基类成员析构执行
|
// ImGui 叠加层(框架只做管线桥接;UI 内容 = docking Editor)
|
||||||
if (m_EnvWorker.joinable()) {
|
EnableImGui();
|
||||||
m_EnvWorker.join();
|
|
||||||
}
|
// Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
|
||||||
}
|
InitEditor();
|
||||||
|
#else
|
||||||
protected:
|
// game 形态:全窗口 Direct 直渲(Renderer 默认即 kDisplayDirect),
|
||||||
// ---- 生命周期 ----
|
// 相机由场景 primary camera 提供,无 UI。
|
||||||
|
// 游戏流:Stopped → Loading(OnUpdate 泵场景加载)→ Running
|
||||||
void OnInit() override
|
GetGameFlow().RequestStart();
|
||||||
{
|
#endif
|
||||||
Scene& scene = GetScene();
|
}
|
||||||
|
|
||||||
// ImGui 叠加层(框架只做管线桥接,UI 内容全部在这里定义)
|
void OnUpdate(const FrameTime& frameTime) override
|
||||||
EnableImGui();
|
{
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级),
|
// 编辑器形态:驱动编辑器相机(RMB 飞行 / MMB 焦点环绕 /
|
||||||
// 其余环境在 ImGui 里按需 lazy 加载后切换
|
// 滚轮推拉)。暂停(timeScale=0)时仍可飞行——unscaled dt。
|
||||||
const auto env = GetAssets().LoadEnvironment(
|
// PIE 内嵌播放:渲染源已切 primary 相机,编辑器相机停驱
|
||||||
kEnvPaths[kDefaultEnv]);
|
//(否则与游戏脚本抢 WASD,Stop 后视角也被偷跑)。
|
||||||
if (env.Valid()) {
|
// 游戏形态:相机实体由游戏逻辑驱动(本沙盒无逻辑 = 静止)
|
||||||
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env));
|
if (GetRenderer().GetDisplayMode() == kDisplayViewport &&
|
||||||
}
|
!m_Editor->IsPlaying())
|
||||||
|
{
|
||||||
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
|
GetRenderer().GetEditorCamera().Update(frameTime.UnscaledDt());
|
||||||
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
|
}
|
||||||
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
|
#else
|
||||||
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
|
// Loading 泵(同步示例):场景文件存在则加载,不存在 =
|
||||||
|
// 空场景照常进 Running(引擎冒烟路径)。加载完成后必须
|
||||||
// 大模型异步加载:assimp 解析在 worker 线程(SunTemple 约 3s),
|
// CompleteLoading,否则物理/脚本门控永远跳过(卡死 Loading)
|
||||||
// 完成后 OnUpdate 收割 → GPU 构建 → 建实体(cm 尺度 → 0.001)
|
if (GetGameFlow().IsIn(GameFlowState::Loading))
|
||||||
m_TempleTask.Run([] { return Model::ParseFromDisk(kTemplePath); });
|
{
|
||||||
m_TempleScale = 0.001f;
|
if (std::filesystem::exists(m_ScenePath))
|
||||||
|
{
|
||||||
|
if (Serialization::SceneSerializer::Deserialize(
|
||||||
int countSize = 0;
|
GetScene(), GetAssets(), m_ScenePath))
|
||||||
for (int i = 0; i < countSize; i ++)
|
{
|
||||||
{
|
ENGINE_INFO("Game: scene loaded from {}", m_ScenePath);
|
||||||
for (int j = 0; j < countSize; j ++)
|
}
|
||||||
{
|
else
|
||||||
auto m = scene.CreateEntity();
|
{
|
||||||
m.AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
|
ENGINE_ERROR("Game: scene load failed: {}",
|
||||||
m.GetComponent<TransformComponent>().SetPosition(glm::vec3{(i - countSize * 0.5) * 2, 0, (j - countSize * 0.5) * 2});
|
m_ScenePath);
|
||||||
m_Spinners.push_back(m);
|
}
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
|
ENGINE_WARN("Game: scene file not found ({}), "
|
||||||
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
|
"starting empty", m_ScenePath);
|
||||||
|
}
|
||||||
m_Ground = scene.CreateEntity("Ground");
|
GetGameFlow().CompleteLoading();
|
||||||
m_Ground.GetComponent<TransformComponent>()
|
}
|
||||||
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
|
#endif
|
||||||
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
|
}
|
||||||
|
|
||||||
// 相机实体(primary = 当前渲染视角;位姿存实体 Transform)
|
// ---- UI(全部委托 docking Editor;本文件零 ImGui 控件)----
|
||||||
m_Camera = scene.CreateEntity("MainCamera");
|
|
||||||
auto& camera = m_Camera.AddComponent<CameraComponent>();
|
void OnImGuiRender() override
|
||||||
camera.primary = true;
|
{
|
||||||
m_Camera.GetComponent<TransformComponent>()
|
#ifdef HELLO_EDITOR
|
||||||
.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f));
|
m_Editor->Draw();
|
||||||
|
#endif
|
||||||
// 灯光实体(第一个方向光 = 阴影光源)
|
}
|
||||||
m_KeyLight = scene.CreateEntity("KeyLight");
|
|
||||||
m_KeyLight.AddComponent<LightComponent>(
|
// ---- 事件 ----
|
||||||
LightComponent{
|
|
||||||
.type = LightComponent::Type::Directional,
|
#ifdef HELLO_EDITOR
|
||||||
.direction = glm::vec3(-0.5f, -0.6f, -0.35f),
|
void OnWindowClose(const Event& event) override
|
||||||
.color = glm::vec4(1.0f, 0.95f, 0.9f, 5.0f),
|
{
|
||||||
.castShadow = true,
|
// 退出确认走 Editor 的模态框(ImGui 必须可见才有 UI)
|
||||||
});
|
SetImGuiEnabled(true);
|
||||||
|
m_Editor->RequestQuit();
|
||||||
scene.CreateEntity("FillLight")
|
}
|
||||||
.AddComponent<LightComponent>(
|
#endif
|
||||||
LightComponent{
|
|
||||||
.type = LightComponent::Type::Directional,
|
void OnKeyPress(const Event& event) override
|
||||||
.direction = glm::vec3(0.5f, -0.5f, 0.3f),
|
{
|
||||||
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.5f)
|
// 键码统一走 Input::Key(App 层零 GLFW 依赖)
|
||||||
});
|
#ifdef HELLO_EDITOR
|
||||||
|
if (event.key == static_cast<int>(Input::Key::GraveAccent))
|
||||||
// 点光源(位置 = 实体 Transform,出生即有;无阴影,距离衰减)
|
{
|
||||||
auto fire = scene.CreateEntity("FirePitLight");
|
SetImGuiEnabled(!GetImGuiEnabled());
|
||||||
fire.GetComponent<TransformComponent>().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f));
|
}
|
||||||
fire.AddComponent<LightComponent>(
|
#endif
|
||||||
LightComponent{
|
}
|
||||||
.type = LightComponent::Type::Point,
|
|
||||||
.color = glm::vec4(1.0f, 0.6f, 0.3f, 1.0f), // 暖橙
|
|
||||||
.radius = 8.0f,
|
void OnWindowResize(const Event& event) override
|
||||||
});
|
{
|
||||||
|
ENGINE_INFO("App: window resized to {}x{}", event.width, event.height);
|
||||||
auto cool = scene.CreateEntity("CoolFillLight");
|
}
|
||||||
cool.GetComponent<TransformComponent>().SetPosition(glm::vec3(-2.5f, 2.0f, -2.0f));
|
|
||||||
cool.AddComponent<LightComponent>(
|
private:
|
||||||
LightComponent{
|
#ifdef HELLO_EDITOR
|
||||||
.type = LightComponent::Type::Point,
|
// Editor 注入:门面指针 + 反向能力回调(面板 → 沙盒)
|
||||||
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.2f), // 冷蓝
|
void InitEditor()
|
||||||
.radius = 10.0f,
|
{
|
||||||
});
|
EditorContext ctx;
|
||||||
}
|
ctx.renderer = &GetRenderer();
|
||||||
|
ctx.scene = &GetScene();
|
||||||
void OnUpdate(f32 deltaTime) override
|
ctx.assets = &GetAssets();
|
||||||
{
|
ctx.app = this;
|
||||||
// 异步模型收割:解析完成 → GPU 构建 → 建实体
|
ctx.flow = &GetGameFlow();
|
||||||
if (m_TempleTask.Done()) {
|
ctx.editorCamera = &GetRenderer().GetEditorCamera();
|
||||||
ParsedModel parsed = m_TempleTask.Take();
|
// 编辑器态默认冻结:GameFlow 构造即 timeScale=0(Stopped 语义),
|
||||||
const auto ref = GetAssets().LoadModel(kTemplePath,
|
// timeScale 权威在 GameFlow,此处不再手设
|
||||||
std::move(parsed));
|
m_Editor->Init(ctx);
|
||||||
if (ref.Valid()) {
|
|
||||||
auto temple = GetScene().CreateEntity("SunTemple");
|
// 输入归属:覆盖 EnableImGui 的全局 WantCapture* 谓词
|
||||||
temple.AddComponent<MeshRendererComponent>(
|
//(全屏 dock host 让它恒真,输入全死)。改为 Editor 按帧
|
||||||
MeshRendererComponent{ref});
|
// 判定——Viewport hover/focus 拥有输入(Hazel/UE 语义);
|
||||||
temple.GetComponent<TransformComponent>()
|
// UI 隐藏(反引号)时 Draw 不再执行,直接全放行
|
||||||
.SetScale(glm::vec3{m_TempleScale});
|
GetWindow().SetInputCapture(
|
||||||
ENGINE_INFO("[ASYNC] SunTemple ready, entity created");
|
[this] { return GetImGuiEnabled() && m_Editor->BlockingKeyboard(); },
|
||||||
}
|
[this] { return GetImGuiEnabled() && m_Editor->BlockingMouse(); });
|
||||||
}
|
|
||||||
|
}
|
||||||
// 异步环境加载:每帧收割完成的解码结果
|
|
||||||
PollEnvironment();
|
private:
|
||||||
|
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
|
||||||
// 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff
|
#else
|
||||||
// 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到)
|
// game 形态:启动场景路径(CWD = bin;编辑器 Save 的产物位置)
|
||||||
if (!m_PauseAnim)
|
const char* m_ScenePath = "assets/scenes/sandbox.scene";
|
||||||
{
|
#endif
|
||||||
m_Time += deltaTime;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 相机(WASD/Space/Shift 移动;位姿读写实体 Transform)
|
int main()
|
||||||
m_Camera.GetComponent<CameraComponent>()
|
{
|
||||||
.camera.Update(GetWindow(),
|
Logger::Initialize();
|
||||||
m_Camera.GetComponent<TransformComponent>(),
|
|
||||||
deltaTime);
|
{
|
||||||
|
SandboxApp app;
|
||||||
// 旋转动画:编辑 Transform 分量后 Compose 重算 world
|
|
||||||
for (size_t i = 0; i < m_Spinners.size(); ++i)
|
try
|
||||||
{
|
{
|
||||||
auto& t = m_Spinners[i].GetComponent<TransformComponent>();
|
app.Run();
|
||||||
t.SetRotation(glm::vec3(0.0f, m_Time * 60.0f + static_cast<f32>(i) * 40.0f, 0.0f));
|
}
|
||||||
}
|
catch (const std::exception& e)
|
||||||
}
|
{
|
||||||
|
ENGINE_FATAL("Exception: {}", e.what());
|
||||||
// ---- 事件 ----
|
Logger::Shutdown();
|
||||||
|
return -1;
|
||||||
// ---- 异步环境加载(decode 后台线程 / GPU 生成主线程)----
|
}
|
||||||
// decode 占切换耗时大头(4k ≈ 130ms);worker 只产出纯 CPU
|
} // app 析构:成员(Device/Renderer/Window…)全部销毁完毕
|
||||||
// 像素数据,Vulkan 生成回主线程(设备非线程安全)。
|
|
||||||
// 同一时刻最多一个 in-flight,忙碌期的新请求记为 pending 接力。
|
Logger::Shutdown();
|
||||||
void RequestEnvironment(int idx)
|
return 0;
|
||||||
{
|
}
|
||||||
if (m_EnvLoadBusy)
|
|
||||||
{
|
|
||||||
m_EnvPending = idx;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
StartEnvDecode(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
void StartEnvDecode(int idx)
|
|
||||||
{
|
|
||||||
m_EnvLoadBusy = true;
|
|
||||||
m_EnvLoadDone.store(false);
|
|
||||||
m_EnvLoadingPath = kEnvPaths[idx];
|
|
||||||
m_EnvWorker = std::thread([this, idx]() {
|
|
||||||
m_EnvDecoded = IblGenerator::DecodeHdr(kEnvPaths[idx]);
|
|
||||||
m_EnvLoadDone.store(true); // release:解码结果对主线程可见
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void PollEnvironment()
|
|
||||||
{
|
|
||||||
if (!m_EnvLoadBusy || !m_EnvLoadDone.load())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_EnvWorker.join(); // join = 同步点,解码结果此后归主线程
|
|
||||||
m_EnvLoadBusy = false;
|
|
||||||
|
|
||||||
if (m_EnvDecoded.Valid())
|
|
||||||
{
|
|
||||||
const auto ref = GetAssets().LoadEnvironment(
|
|
||||||
m_EnvLoadingPath, std::move(m_EnvDecoded));
|
|
||||||
if (ref.Valid())
|
|
||||||
{
|
|
||||||
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(ref));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_EnvDecoded = {};
|
|
||||||
|
|
||||||
// 忙碌期间的最新请求接力(rapid switch 只保留最终意图)
|
|
||||||
if (m_EnvPending >= 0)
|
|
||||||
{
|
|
||||||
const int next = m_EnvPending;
|
|
||||||
m_EnvPending = -1;
|
|
||||||
StartEnvDecode(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 叠加层 UI(唯一允许出现 ImGui 控件的地方)----
|
|
||||||
|
|
||||||
void OnRenderOverlay() override
|
|
||||||
{
|
|
||||||
ImGui::Begin("Scene Debug");
|
|
||||||
auto& renderer = GetRenderer();
|
|
||||||
|
|
||||||
// ---- Scene:场景内容(灯/动画) ----
|
|
||||||
if (ImGui::CollapsingHeader("Scene", ImGuiTreeNodeFlags_DefaultOpen))
|
|
||||||
{
|
|
||||||
// 阴影光源(无开关,控件平铺聚拢)
|
|
||||||
if (auto* light = m_KeyLight.TryGetComponent<LightComponent>())
|
|
||||||
{
|
|
||||||
ImGui::Text("Key Light (shadow caster)");
|
|
||||||
ImGui::SliderFloat3("direction",
|
|
||||||
&light->direction.x, -1.0f, 1.0f);
|
|
||||||
ImGui::SliderFloat("intensity", &light->color.a, 0.0f, 20.0f);
|
|
||||||
if (ImGui::Button("reset"))
|
|
||||||
{
|
|
||||||
light->direction = glm::vec3(-0.5f, -0.6f, -0.35f);
|
|
||||||
light->color.a = 2.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::Checkbox("Pause Animation", &m_PauseAnim);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Rendering:渲染管线参数与开关 ----
|
|
||||||
if (ImGui::CollapsingHeader("Rendering", ImGuiTreeNodeFlags_DefaultOpen))
|
|
||||||
{
|
|
||||||
// Tonemap(无开关,平铺)
|
|
||||||
f32 exposure = renderer.GetExposure();
|
|
||||||
if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f))
|
|
||||||
{
|
|
||||||
renderer.SetExposure(exposure);
|
|
||||||
}
|
|
||||||
bool aces = renderer.GetUseACES();
|
|
||||||
if (ImGui::Checkbox("ACES", &aces))
|
|
||||||
{
|
|
||||||
renderer.SetUseACES(aces);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bloom:开关即显隐——勾上才显示参数
|
|
||||||
bool bloom = renderer.GetBloomEnabled();
|
|
||||||
if (ImGui::Checkbox("Bloom", &bloom))
|
|
||||||
{
|
|
||||||
renderer.SetBloomEnabled(bloom);
|
|
||||||
}
|
|
||||||
if (bloom)
|
|
||||||
{
|
|
||||||
f32 threshold = renderer.GetBloomThreshold();
|
|
||||||
if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f))
|
|
||||||
{
|
|
||||||
renderer.SetBloomThreshold(threshold);
|
|
||||||
}
|
|
||||||
f32 strength = renderer.GetBloomStrength();
|
|
||||||
if (ImGui::SliderFloat("strength", &strength, 0.0f, 2.0f))
|
|
||||||
{
|
|
||||||
renderer.SetBloomStrength(strength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无参数的纯开关:横排平铺
|
|
||||||
bool shadows = renderer.GetShadowsEnabled();
|
|
||||||
if (ImGui::Checkbox("Shadows", &shadows))
|
|
||||||
{
|
|
||||||
renderer.SetShadowsEnabled(shadows);
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
bool taa = renderer.GetTAAEnabled();
|
|
||||||
if (ImGui::Checkbox("TAA", &taa))
|
|
||||||
{
|
|
||||||
renderer.SetTAAEnabled(taa);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Environment:IBL 环境 + 调试视图(决定"看什么") ----
|
|
||||||
if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen))
|
|
||||||
{
|
|
||||||
// 动态环境切换(Assets 层加载 → Renderer 换绑;缓存去重)
|
|
||||||
ImGui::Text("Environment (IBL)");
|
|
||||||
bool& DrawSkybox = GetScene().GetSettings().DrawSkybox;
|
|
||||||
ImGui::Checkbox("DrawSkybox", &DrawSkybox);
|
|
||||||
if (DrawSkybox)
|
|
||||||
{
|
|
||||||
int envIdx = m_EnvIndex;
|
|
||||||
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
|
|
||||||
{
|
|
||||||
// 异步:解码后台线程,完成后 OnUpdate 里换绑
|
|
||||||
m_EnvIndex = envIdx;
|
|
||||||
RequestEnvironment(envIdx);
|
|
||||||
}
|
|
||||||
ImGui::Text(m_EnvLoadBusy ? "(decoding...)" :
|
|
||||||
"(switch = compute pre-integrate, ms-level)");
|
|
||||||
|
|
||||||
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
|
|
||||||
const auto& viewNames = renderer.GetDebugViewNames();
|
|
||||||
int view = renderer.GetDebugView();
|
|
||||||
if (ImGui::Combo("Debug View", &view, viewNames.data(),
|
|
||||||
static_cast<int>(viewNames.size())))
|
|
||||||
{
|
|
||||||
renderer.SetDebugView(view);
|
|
||||||
}
|
|
||||||
}else
|
|
||||||
{
|
|
||||||
ImGui::ColorEdit4("ClearColor", glm::value_ptr(GetScene().GetSettings().ClearColor));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- GPU Timings:只读遥测(默认折叠,不占面板高度) ----
|
|
||||||
if (ImGui::CollapsingHeader("GPU Timings"))
|
|
||||||
{
|
|
||||||
// Pass 级 GPU 耗时(RG timestamp)
|
|
||||||
ImGui::Text("instances: %u main / %u shadow",
|
|
||||||
renderer.GetMainInstanceCount(),
|
|
||||||
renderer.GetShadowInstanceCount());
|
|
||||||
if (renderer.GetInstancesTruncated()) {
|
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
|
||||||
"TRUNCATED: instance buffer full!");
|
|
||||||
}
|
|
||||||
const auto& names = renderer.GetPassNames();
|
|
||||||
const auto& times = renderer.GetPassGpuMs();
|
|
||||||
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
|
|
||||||
{
|
|
||||||
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Frame:帧率控制与显示 ----
|
|
||||||
if (ImGui::CollapsingHeader("Frame", ImGuiTreeNodeFlags_DefaultOpen))
|
|
||||||
{
|
|
||||||
auto& app = *this;
|
|
||||||
bool vsync = app.GetVSync();
|
|
||||||
if (ImGui::Checkbox("VSync", &vsync))
|
|
||||||
{
|
|
||||||
app.SetVSync(vsync);
|
|
||||||
if (!vsync)
|
|
||||||
{
|
|
||||||
const f32 target = app.GetTargetFps();
|
|
||||||
app.SetTargetFps(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!vsync)
|
|
||||||
{
|
|
||||||
f32 target = app.GetTargetFps();
|
|
||||||
if (ImGui::SliderFloat("target fps", &target, 24.0f, 360.0f, "%.0f"))
|
|
||||||
{
|
|
||||||
app.SetTargetFps(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::Text("fps: %.1f frame: %llu",
|
|
||||||
1.0f / GetDeltaTime(),
|
|
||||||
static_cast<unsigned long long>(GetFrameCount()));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::End();
|
|
||||||
|
|
||||||
|
|
||||||
// 自定义重载事件: 重载关闭事件模态框
|
|
||||||
{
|
|
||||||
if (m_ShowQuitConfirm)
|
|
||||||
{
|
|
||||||
ImGui::OpenPopup("Should quit?");
|
|
||||||
m_ShowQuitConfirm = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SetNextWindowPos(
|
|
||||||
ImGui::GetMainViewport()->GetCenter(),
|
|
||||||
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
|
||||||
|
|
||||||
if (ImGui::BeginPopupModal("Should quit?", nullptr,
|
|
||||||
ImGuiWindowFlags_AlwaysAutoResize))
|
|
||||||
{
|
|
||||||
ImGui::TextUnformatted("Quit the application?");
|
|
||||||
ImGui::Separator();
|
|
||||||
if (ImGui::Button("Yes", ImVec2(120, 0)))
|
|
||||||
{
|
|
||||||
m_QuitConfirmed = true;
|
|
||||||
ImGui::CloseCurrentPopup();
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (ImGui::Button("No", ImVec2(120, 0)))
|
|
||||||
{
|
|
||||||
ImGui::CloseCurrentPopup();
|
|
||||||
}
|
|
||||||
ImGui::EndPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_QuitConfirmed)
|
|
||||||
{
|
|
||||||
m_QuitConfirmed = false;
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnWindowClose(const Event& event) override
|
|
||||||
{
|
|
||||||
SetImGuiEnabled(true);
|
|
||||||
m_ShowQuitConfirm = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnKeyPress(const Event& event) override
|
|
||||||
{
|
|
||||||
if (event.key == GLFW_KEY_GRAVE_ACCENT)
|
|
||||||
{
|
|
||||||
SetImGuiEnabled(!GetImGuiEnabled());
|
|
||||||
}
|
|
||||||
if (event.key == GLFW_KEY_F1)
|
|
||||||
{
|
|
||||||
STATS.PrintComparison();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void OnWindowResize(const Event& event) override
|
|
||||||
{
|
|
||||||
ENGINE_INFO("App: window resized to {}x{}", event.width, event.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool m_ShowQuitConfirm = false;
|
|
||||||
bool m_QuitConfirmed = false;
|
|
||||||
EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空
|
|
||||||
EntityHandle m_Ground{};
|
|
||||||
EntityHandle m_KeyLight{};
|
|
||||||
std::vector<EntityHandle> m_Spinners;
|
|
||||||
f32 m_Time = 0.0f;
|
|
||||||
bool m_PauseAnim = true; // 冻结动画时钟(帧历史 diff 验证用)
|
|
||||||
int m_EnvIndex = kDefaultEnv;
|
|
||||||
|
|
||||||
// ---- 异步环境加载状态(单 in-flight + pending 接力)----
|
|
||||||
std::thread m_EnvWorker;
|
|
||||||
std::string m_EnvLoadingPath;
|
|
||||||
DecodedEquirect m_EnvDecoded; // worker 写 → join 后主线程读
|
|
||||||
std::atomic<bool> m_EnvLoadDone{false};
|
|
||||||
bool m_EnvLoadBusy = false;
|
|
||||||
int m_EnvPending = -1;
|
|
||||||
|
|
||||||
// ---- 异步模型加载(SunTemple)----
|
|
||||||
AsyncTask<ParsedModel> m_TempleTask;
|
|
||||||
f32 m_TempleScale = 1.0f; // cm 尺度资产 → 米(解析后应用)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
SandboxApp app;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
app.Run();
|
|
||||||
}
|
|
||||||
catch (const std::exception& e)
|
|
||||||
{
|
|
||||||
ENGINE_FATAL("Exception: {}", e.what());
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
+95
-52
@@ -1,52 +1,95 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
project(HelloVulkan CXX)
|
project(HelloVulkan C CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
set(CMAKE_BINARY_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
set(CMAKE_BINARY_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
||||||
|
|
||||||
# 第三方库
|
# 第三方库
|
||||||
add_subdirectory(Lib/glfw)
|
add_subdirectory(Lib/glfw)
|
||||||
add_subdirectory(Lib/glm)
|
add_subdirectory(Lib/glm)
|
||||||
add_subdirectory(Lib/spdlog)
|
add_subdirectory(Lib/spdlog)
|
||||||
add_subdirectory(Lib/stb)
|
add_subdirectory(Lib/stb)
|
||||||
|
# meshoptimizer(submodule,LOD 网格简化;demo/gltfpack 默认关闭,install 不需要)
|
||||||
# ---- Assimp(vendored submodule,模型导入;只编译需要的 importer) ----
|
set(MESHOPT_INSTALL OFF CACHE BOOL "" FORCE)
|
||||||
set(BUILD_SHARED_LIBS OFF) # 静态链接
|
add_subdirectory(Lib/meshoptimizer)
|
||||||
set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
|
||||||
set(ASSIMP_BUILD_ASSIMP_TOOLS OFF CACHE BOOL "" FORCE) # 不编 assimp_cmd 等工具
|
# ---- Jolt Physics(submodule v5.6,刚体/软体/角色/载具模拟)----
|
||||||
set(ASSIMP_INSTALL OFF CACHE BOOL "" FORCE)
|
if(MSVC)
|
||||||
set(ASSIMP_WARNINGS_AS_ERRORS OFF CACHE BOOL "" FORCE) # assimp 源码警告多
|
set(USE_STATIC_MSVC_RUNTIME_LIBRARY OFF CACHE BOOL "" FORCE)
|
||||||
set(ASSIMP_INJECT_DEBUG_POSTFIX OFF CACHE BOOL "" FORCE)
|
endif ()
|
||||||
set(ASSIMP_NO_EXPORT ON CACHE BOOL "" FORCE) # 彻底移除导出代码
|
# 顶层 CMake 在 Build/ 子目录;只编 jolt 静态库,示例/测试目标默认全关
|
||||||
set(ASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT OFF CACHE BOOL "" FORCE) # 全关,按需开
|
set(JPH_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||||
set(ASSIMP_BUILD_DRACO OFF CACHE BOOL "" FORCE)
|
set(INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE) # LTO 拖慢开发迭代
|
||||||
# 用 assimp 自带 contrib zlib(而非系统 zlib)。原因:系统 zlib 会使 assimp 走
|
set(OVERRIDE_CXX_FLAGS OFF CACHE BOOL "" FORCE) # 不动全局优化参数
|
||||||
# pkg-config 找系统 minizip,而 Arch 的 minizip 头文件在 /usr/include/minizip/
|
set(FLOATING_POINT_EXCEPTIONS_ENABLED OFF CACHE BOOL "" FORCE) # NaN 进物理不炸,仅报警
|
||||||
# 子目录,与 assimp 的 #include <unzip.h> 不符 → ZipArchiveIOSystem 编译失败。
|
# JPH_USE_CPU_COMPUTE 默认 ON,需要 DXC 把 Shaders/*.hlsl 编成 .spv(本机无
|
||||||
# 自带 zlib 后 ASSIMP_BUILD_MINIZIP=1,跳过 pkg-config,直接用 contrib/unzip。
|
# 工具链);我们不使用 Jolt 的 GPU compute 路径,连同后端 shader 一并关掉
|
||||||
set(ASSIMP_BUILD_ZLIB ON CACHE BOOL "" FORCE)
|
set(JPH_USE_CPU_COMPUTE OFF CACHE BOOL "" FORCE)
|
||||||
# 清除首次 configure 残留的 UNZIP_FOUND 缓存(INTERNAL=1 会持续生效,
|
set(JPH_USE_DX12 OFF CACHE BOOL "" FORCE)
|
||||||
# 使 assimp 误判系统 minizip 可用而不把 contrib/unzip 加进 include 路径)。
|
set(JPH_USE_VK OFF CACHE BOOL "" FORCE)
|
||||||
set(UNZIP_FOUND OFF CACHE INTERNAL "" FORCE)
|
set(JPH_USE_MTL OFF CACHE BOOL "" FORCE)
|
||||||
# 需要的导入格式(按需增减,每关一个省编译时间):
|
# Jolt GENERATE_DEBUG_SYMBOLS 默认 ON,Release 也带 -g → Jolt 的 DWARF
|
||||||
set(ASSIMP_BUILD_OBJ_IMPORTER ON CACHE BOOL "" FORCE)
|
# Debug/RelWithDebInfo 保留符号便于调试
|
||||||
set(ASSIMP_BUILD_GLTF_IMPORTER ON CACHE BOOL "" FORCE) # glTF1+2 同一选项
|
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||||
set(ASSIMP_BUILD_FBX_IMPORTER ON CACHE BOOL "" FORCE)
|
set(GENERATE_DEBUG_SYMBOLS OFF CACHE BOOL "" FORCE)
|
||||||
set(ASSIMP_BUILD_STL_IMPORTER ON CACHE BOOL "" FORCE)
|
endif()
|
||||||
set(ASSIMP_BUILD_PLY_IMPORTER ON CACHE BOOL "" FORCE)
|
add_subdirectory(Lib/JoltPhysics/Build)
|
||||||
add_subdirectory(Lib/assimp)
|
|
||||||
|
# ---- Assimp(vendored submodule,模型导入;只编译需要的 importer) ----
|
||||||
|
set(BUILD_SHARED_LIBS OFF) # 静态链接
|
||||||
|
set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
# Vulkan
|
set(ASSIMP_BUILD_ASSIMP_TOOLS OFF CACHE BOOL "" FORCE) # 不编 assimp_cmd 等工具
|
||||||
find_package(Vulkan REQUIRED)
|
set(ASSIMP_INSTALL OFF CACHE BOOL "" FORCE)
|
||||||
|
set(ASSIMP_WARNINGS_AS_ERRORS OFF CACHE BOOL "" FORCE) # assimp 源码警告多
|
||||||
# Engine 和 App
|
set(ASSIMP_INJECT_DEBUG_POSTFIX OFF CACHE BOOL "" FORCE)
|
||||||
add_subdirectory(Engine)
|
set(ASSIMP_NO_EXPORT ON CACHE BOOL "" FORCE) # 彻底移除导出代码
|
||||||
add_subdirectory(App)
|
set(ASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT OFF CACHE BOOL "" FORCE) # 全关,按需开
|
||||||
|
set(ASSIMP_BUILD_DRACO OFF CACHE BOOL "" FORCE)
|
||||||
file(GLOB ASSETS "assets")
|
# 用 assimp 自带 contrib zlib(而非系统 zlib)。原因:系统 zlib 会使 assimp 走
|
||||||
file(COPY ${ASSETS} DESTINATION ${CMAKE_BINARY_DIR})
|
# pkg-config 找系统 minizip,而 Arch 的 minizip 头文件在 /usr/include/minizip/
|
||||||
|
# 子目录,与 assimp 的 #include <unzip.h> 不符 → ZipArchiveIOSystem 编译失败。
|
||||||
|
# 自带 zlib 后 ASSIMP_BUILD_MINIZIP=1,跳过 pkg-config,直接用 contrib/unzip。
|
||||||
|
set(ASSIMP_BUILD_ZLIB ON CACHE BOOL "" FORCE)
|
||||||
|
# 清除首次 configure 残留的 UNZIP_FOUND 缓存(INTERNAL=1 会持续生效,
|
||||||
|
# 使 assimp 误判系统 minizip 可用而不把 contrib/unzip 加进 include 路径)。
|
||||||
|
set(UNZIP_FOUND OFF CACHE INTERNAL "" FORCE)
|
||||||
|
# 需要的导入格式(按需增减,每关一个省编译时间):
|
||||||
|
set(ASSIMP_BUILD_OBJ_IMPORTER ON CACHE BOOL "" FORCE)
|
||||||
|
set(ASSIMP_BUILD_GLTF_IMPORTER ON CACHE BOOL "" FORCE) # glTF1+2 同一选项
|
||||||
|
set(ASSIMP_BUILD_FBX_IMPORTER ON CACHE BOOL "" FORCE)
|
||||||
|
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)
|
||||||
|
|
||||||
|
file(GLOB ASSETS "assets")
|
||||||
|
file(COPY ${ASSETS} DESTINATION ${CMAKE_BINARY_DIR})
|
||||||
|
|||||||
+104
-47
@@ -1,47 +1,104 @@
|
|||||||
file(GLOB_RECURSE ENGINE_SOURCES
|
# ============ 第三方库目标(Engine 的依赖) ============
|
||||||
src/*.cpp
|
|
||||||
)
|
# ImGui(docking 分支,vendored submodule)
|
||||||
|
set(IMGUI_DIR ${CMAKE_SOURCE_DIR}/Lib/imgui)
|
||||||
# ImGui(docking 分支,vendored submodule)
|
add_library(imgui STATIC
|
||||||
set(IMGUI_DIR ${CMAKE_SOURCE_DIR}/Lib/imgui)
|
${IMGUI_DIR}/imgui.cpp
|
||||||
set(IMGUI_SOURCES
|
${IMGUI_DIR}/imgui_draw.cpp
|
||||||
${IMGUI_DIR}/imgui.cpp
|
${IMGUI_DIR}/imgui_tables.cpp
|
||||||
${IMGUI_DIR}/imgui_draw.cpp
|
${IMGUI_DIR}/imgui_widgets.cpp
|
||||||
${IMGUI_DIR}/imgui_tables.cpp
|
# ${IMGUI_DIR}/imgui_demo.cpp # ShowDemoWindow 的实现(调试期保留)
|
||||||
${IMGUI_DIR}/imgui_widgets.cpp
|
${IMGUI_DIR}/backends/imgui_impl_glfw.cpp
|
||||||
# ${IMGUI_DIR}/imgui_demo.cpp # ShowDemoWindow 的实现(调试期保留)
|
${IMGUI_DIR}/backends/imgui_impl_vulkan.cpp
|
||||||
${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 头
|
||||||
add_library(Engine STATIC ${ENGINE_SOURCES} ${IMGUI_SOURCES})
|
target_link_libraries(imgui PUBLIC glfw Vulkan::Vulkan)
|
||||||
|
|
||||||
|
# ImGuizmo(视口操纵杆,vendored submodule;新版仓库源码在 src/,
|
||||||
target_include_directories(Engine PUBLIC
|
# 只编译 ImGuizmo.cpp,GraphEditor/ImCurveEdit 等示例模块不用)
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
# IMGUI_DEFINE_MATH_OPERATORS 必须 PUBLIC:ImGuizmo.cpp 与任何
|
||||||
${CMAKE_SOURCE_DIR}/Lib/entt/src
|
# include 其头文件的 TU 都要在 imgui.h 之前看到该宏(ImVec2 运算符)
|
||||||
${CMAKE_SOURCE_DIR}/Lib/VulkanMemoryAllocator/include
|
set(IMGUIZMO_DIR ${CMAKE_SOURCE_DIR}/Lib/ImGuizmo/src)
|
||||||
${IMGUI_DIR}
|
add_library(ImGuizmo STATIC ${IMGUIZMO_DIR}/ImGuizmo.cpp)
|
||||||
${IMGUI_DIR}/backends
|
target_include_directories(ImGuizmo PUBLIC ${IMGUIZMO_DIR})
|
||||||
)
|
target_compile_definitions(ImGuizmo PUBLIC IMGUI_DEFINE_MATH_OPERATORS)
|
||||||
|
target_link_libraries(ImGuizmo PUBLIC imgui)
|
||||||
# 链接第三方库
|
|
||||||
target_link_libraries(Engine PUBLIC
|
# ufbx(FBX 全版本导入,vendored 单文件 C 库):assimp 的 FBX importer
|
||||||
glfw
|
# 只认 FBX 2011+,Mixamo 及大量网上资产是 FBX 6.1 binary(二进制布局
|
||||||
Vulkan::Vulkan
|
# 与 7.x 在对象 ID 层不兼容,assimp DOM 直接拒读)。ufbx 官方支持
|
||||||
spdlog::spdlog
|
# FBX 5.x/6.x/7.x 全版本,引擎的 .fbx 解析统一走它(glTF/OBJ 仍 assimp)
|
||||||
stb
|
add_library(ufbx STATIC ${CMAKE_SOURCE_DIR}/Lib/ufbx/ufbx.c)
|
||||||
assimp::assimp
|
target_include_directories(ufbx PUBLIC ${CMAKE_SOURCE_DIR}/Lib/ufbx)
|
||||||
)
|
|
||||||
|
# Tracy(CPU/GPU 帧分析)
|
||||||
# Tracy(CPU/GPU 帧分析):宏关闭时全部退化空操作,零开销
|
add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp)
|
||||||
set(TRACY_DIR ${CMAKE_SOURCE_DIR}/Lib/tracy)
|
target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public)
|
||||||
target_include_directories(Engine PUBLIC ${TRACY_DIR}/public)
|
target_compile_definitions(tracy PUBLIC
|
||||||
target_sources(Engine PRIVATE ${TRACY_DIR}/public/TracyClient.cpp)
|
TRACY_ENABLE
|
||||||
target_compile_definitions(Engine PUBLIC TRACY_ENABLE)
|
# 按需剖析:GPU context 宣告(GpuNewContext)与连接绑定——GUI 永远晚于
|
||||||
|
# 程序启动连接,没有这个宏宣告消息被丢弃,GPU 时间线永远空白
|
||||||
find_package(Threads REQUIRED)
|
TRACY_ON_DEMAND)
|
||||||
target_link_libraries(Engine PUBLIC Threads::Threads)
|
find_package(Threads REQUIRED)
|
||||||
|
if(WIN32)
|
||||||
target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
|
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
|
||||||
|
${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
|
||||||
@@ -19,8 +19,11 @@
|
|||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
#include <Engine/Core/AssetRef.h>
|
#include <Engine/Core/AssetRef.h>
|
||||||
|
#include <Engine/Animation/AnimationAttach.h>
|
||||||
|
#include <Engine/Animation/AnimationController.h>
|
||||||
#include <Engine/Assets/IAssetUploader.h>
|
#include <Engine/Assets/IAssetUploader.h>
|
||||||
#include <Engine/Renderer/Model.h>
|
#include <Engine/Renderer/Model.h>
|
||||||
|
#include <Engine/Renderer/MaterialAsset.h>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -109,6 +112,14 @@ namespace Engine
|
|||||||
m_Free.push_back(h.index);
|
m_Free.push_back(h.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 按路径卸载(未加载为 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
|
const std::string& Path(AssetRef<T> h) const
|
||||||
{
|
{
|
||||||
static const std::string kEmpty;
|
static const std::string kEmpty;
|
||||||
@@ -117,11 +128,28 @@ namespace Engine
|
|||||||
return (e.generation == h.generation && e.alive) ? e.path : kEmpty;
|
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
|
u32 Count() const
|
||||||
{
|
{
|
||||||
return static_cast<u32>(m_Entries.size()) - static_cast<u32>(m_Free.size());
|
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:
|
private:
|
||||||
struct Entry
|
struct Entry
|
||||||
{
|
{
|
||||||
@@ -154,9 +182,58 @@ namespace Engine
|
|||||||
ModelRef LoadModel(const std::string& path, ParsedModel&& parsed);
|
ModelRef LoadModel(const std::string& path, ParsedModel&& parsed);
|
||||||
Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
|
Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
|
||||||
void UnloadModel(ModelRef h) { m_Models.Unload(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 为半边长。带缓存,同参数共享一份。
|
// 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。
|
||||||
ModelRef 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 预积分四件套)。路径缓存去重,
|
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
|
||||||
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
|
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
|
||||||
@@ -167,16 +244,97 @@ namespace Engine
|
|||||||
DecodedEquirect&& decoded);
|
DecodedEquirect&& decoded);
|
||||||
Cubemap* GetEnvironment(EnvRef h) const { return m_Environments.Get(h); }
|
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; }
|
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
|
// 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle
|
||||||
// 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。
|
// 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。
|
||||||
|
|
||||||
|
// 把 dirty 的 GUID 注册表落盘(Application 退出 / 场景保存时调)。
|
||||||
|
// 内部幂等:非 dirty 直接 return。GetOrCreateGUID 只标 dirty 不落盘,
|
||||||
|
// 避免每次新增 GUID 都全量重写 yaml(开发期编辑器卡顿主因之一)
|
||||||
|
void FlushGUIDRegistry();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void LoadGUIDRegistry();
|
||||||
|
void SaveGUIDRegistry() const;
|
||||||
|
|
||||||
IAssetUploader& m_Uploader;
|
IAssetUploader& m_Uploader;
|
||||||
|
|
||||||
AssetCache<Model> m_Models;
|
AssetCache<Model> m_Models;
|
||||||
AssetCache<Cubemap> m_Environments;
|
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 Assets
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/Renderer/Model.h>
|
#include <Engine/Renderer/Model.h>
|
||||||
|
#include <Engine/Renderer/MaterialAsset.h>
|
||||||
#include <Engine/Renderer/Cubemap.h>
|
#include <Engine/Renderer/Cubemap.h>
|
||||||
|
|
||||||
namespace Engine::Assets {
|
namespace Engine::Assets {
|
||||||
@@ -37,12 +38,36 @@ namespace Engine::Assets {
|
|||||||
// 程序化纯色平面(size 为半边长)
|
// 程序化纯色平面(size 为半边长)
|
||||||
virtual Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0;
|
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)
|
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
|
||||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
|
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
|
||||||
|
|
||||||
// 异步加载路径:解码已在后台线程完成,此调用只做 GPU 生成(主线程)
|
// 异步加载路径:解码已在后台线程完成,此调用只做 GPU 生成(主线程)
|
||||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path,
|
virtual Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||||
DecodedEquirect&& decoded) = 0;
|
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
|
} // namespace Engine::Assets
|
||||||
|
|||||||
@@ -9,13 +9,14 @@
|
|||||||
// 和事件钩子(OnEvent + 类型化事件)定义程序行为;
|
// 和事件钩子(OnEvent + 类型化事件)定义程序行为;
|
||||||
// runtime、editor 等不同程序形态只是不同的派生类。
|
// runtime、editor 等不同程序形态只是不同的派生类。
|
||||||
//
|
//
|
||||||
// 主循环固定为:PollEvents → OnUpdate(dt) → [NewFrame → OnRenderOverlay →
|
// 主循环固定为:PollEvents → OnUpdate(FrameTime) → [NewFrame → OnRenderOverlay →
|
||||||
// BuildDrawData] → 主渲染(Renderer.Render)→ 判退出。
|
// BuildDrawData] → 主渲染(Renderer.Render)→ 判退出。
|
||||||
// UI 构建与 GPU 提交分离:OnRenderOverlay 只"声明"控件,真正的 draw data
|
// UI 构建与 GPU 提交分离:OnRenderOverlay 只"声明"控件,真正的 draw data
|
||||||
// 提交发生在主渲染管线中的 ImGui pass(Forward 之后、Present 之前,
|
// 提交发生在主渲染管线中的 ImGui pass(Forward 之后、Present 之前,
|
||||||
// loadOp=Load 叠加)。主渲染是框架行为,派生类不可覆盖也不会丢失;
|
// loadOp=Load 叠加)。主渲染是框架行为,派生类不可覆盖也不会丢失;
|
||||||
// 框架自身永远不绘制任何 ImGui 控件——UI 内容全部由派生类定义。
|
// 框架自身永远不绘制任何 ImGui 控件——UI 内容全部由派生类定义。
|
||||||
// dt 由框架统一计时(含首帧与卡顿钳制),派生层无需自己管理。
|
// 时间由框架单源采集:帧首 Time::BeginFrame 落定 FrameTime(含卡顿钳制),
|
||||||
|
// 派生层经 OnUpdate(frame) 或 Time 门面只读,不再自己碰 chrono。
|
||||||
//
|
//
|
||||||
|
|
||||||
#ifndef HELLOVULKAN_APPLICATION_H
|
#ifndef HELLOVULKAN_APPLICATION_H
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/Core/Event.h>
|
#include <Engine/Core/Event.h>
|
||||||
|
#include <Engine/Core/GameFlow.h>
|
||||||
|
#include <Engine/Core/Time.h>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -33,6 +36,8 @@ namespace Engine {
|
|||||||
class Window;
|
class Window;
|
||||||
class Renderer;
|
class Renderer;
|
||||||
class ImGuiLayer;
|
class ImGuiLayer;
|
||||||
|
class PhysicsWorld;
|
||||||
|
namespace Scripting { class ScriptRuntime; }
|
||||||
|
|
||||||
namespace ECS { class Scene; }
|
namespace ECS { class Scene; }
|
||||||
namespace Assets { class AssetManager; }
|
namespace Assets { class AssetManager; }
|
||||||
@@ -69,7 +74,9 @@ namespace Engine {
|
|||||||
Renderer& GetRenderer() const { return *m_Renderer; }
|
Renderer& GetRenderer() const { return *m_Renderer; }
|
||||||
ECS::Scene& GetScene() const { return *m_Scene; }
|
ECS::Scene& GetScene() const { return *m_Scene; }
|
||||||
Assets::AssetManager& GetAssets() const { return *m_Assets; }
|
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; }
|
u64 GetFrameCount() const { return m_FrameCount; }
|
||||||
|
|
||||||
// ImGui相关
|
// ImGui相关
|
||||||
@@ -83,12 +90,13 @@ namespace Engine {
|
|||||||
// 框架装配完成(Window/Renderer 可用),派生类做初始化(建场景等)
|
// 框架装配完成(Window/Renderer 可用),派生类做初始化(建场景等)
|
||||||
virtual void OnInit() {}
|
virtual void OnInit() {}
|
||||||
|
|
||||||
// 每帧逻辑更新(输入、动画、物理……)
|
// 每帧逻辑更新(输入、动画、物理……)。frame 当帧只读:
|
||||||
virtual void OnUpdate(f32 deltaTime) {}
|
// Dt() = scaled(游戏逻辑),UnscaledDt() = 真实(编辑器相机/UI)
|
||||||
|
virtual void OnUpdate(const FrameTime& frame) {}
|
||||||
|
|
||||||
// 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。
|
// 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。
|
||||||
// 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。
|
// 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。
|
||||||
virtual void OnRenderOverlay() {}
|
virtual void OnImGuiRender() {}
|
||||||
|
|
||||||
// 主循环结束后、析构前(保存、清理场景等)
|
// 主循环结束后、析构前(保存、清理场景等)
|
||||||
virtual void OnShutdown() {}
|
virtual void OnShutdown() {}
|
||||||
@@ -115,12 +123,16 @@ namespace Engine {
|
|||||||
Scope<ECS::Scene> m_Scene;
|
Scope<ECS::Scene> m_Scene;
|
||||||
Scope<Renderer> m_Renderer;
|
Scope<Renderer> m_Renderer;
|
||||||
Scope<Assets::AssetManager> m_Assets;
|
Scope<Assets::AssetManager> m_Assets;
|
||||||
|
Scope<PhysicsWorld> m_Physics; // Jolt 物理(Run 起循环时创建)
|
||||||
|
Scope<Scripting::ScriptRuntime> m_Scripts; // 脚本生命周期(Run 起循环时创建)
|
||||||
// 注意成员序:ImGuiLayer 必须后声明(先于 Renderer/Window 析构,
|
// 注意成员序:ImGuiLayer 必须后声明(先于 Renderer/Window 析构,
|
||||||
// 其后端销毁需要 Device 与 GLFWwindow 存活)
|
// 其后端销毁需要 Device 与 GLFWwindow 存活)
|
||||||
Scope<ImGuiLayer> m_ImGuiLayer;
|
Scope<ImGuiLayer> m_ImGuiLayer;
|
||||||
|
|
||||||
|
// 游戏流状态机(构造即冻结:Stopped → timeScale=0)
|
||||||
|
GameFlow m_GameFlow;
|
||||||
|
|
||||||
bool m_Running = false;
|
bool m_Running = false;
|
||||||
f32 m_DeltaTime = 0.0f;
|
|
||||||
u64 m_FrameCount = 0;
|
u64 m_FrameCount = 0;
|
||||||
|
|
||||||
// 帧率控制
|
// 帧率控制
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ namespace Engine {
|
|||||||
class Cubemap;
|
class Cubemap;
|
||||||
class Texture;
|
class Texture;
|
||||||
class Material;
|
class Material;
|
||||||
|
class MaterialAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
@@ -41,6 +42,7 @@ namespace Engine {
|
|||||||
// 常用别名(新增资产类型时在这里加一行)
|
// 常用别名(新增资产类型时在这里加一行)
|
||||||
using ModelRef = AssetRef<Model>;
|
using ModelRef = AssetRef<Model>;
|
||||||
using EnvRef = AssetRef<Cubemap>; // 环境(IBL 四件套)
|
using EnvRef = AssetRef<Cubemap>; // 环境(IBL 四件套)
|
||||||
|
using MaterialAssetRef = AssetRef<MaterialAsset>; // 材质资产(.material)
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 AddAllocation(u64 bytes);
|
||||||
void AddDeallocation(u64 bytes);
|
void AddDeallocation(u64 bytes);
|
||||||
|
|
||||||
// ---- 采样进程真实内存 ----
|
// ---- 只读快照(面板/遥测消费)----
|
||||||
void SampleProcessMemory();
|
struct Snapshot
|
||||||
|
{
|
||||||
// ---- 打印 ----
|
u64 textureCount = 0;
|
||||||
void Print() const;
|
u64 textureBytes = 0;
|
||||||
void PrintDetailed() const;
|
u64 meshCount = 0;
|
||||||
void PrintComparison() const;
|
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:
|
private:
|
||||||
Stats() = default;
|
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;
|
mutable std::mutex m_Mutex;
|
||||||
|
|
||||||
// 资源计数
|
// 资源计数
|
||||||
@@ -81,9 +79,6 @@ namespace Engine
|
|||||||
u64 m_FrameDeallocation = 0;
|
u64 m_FrameDeallocation = 0;
|
||||||
u64 m_TotalAllocation = 0;
|
u64 m_TotalAllocation = 0;
|
||||||
u64 m_TotalDeallocation = 0;
|
u64 m_TotalDeallocation = 0;
|
||||||
|
|
||||||
// 进程快照
|
|
||||||
ProcessMemory m_ProcessMem{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#define STATS Engine::Stats::Get()
|
#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 {
|
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 {
|
class Window {
|
||||||
public:
|
public:
|
||||||
Window(const std::string& title, u32 width, u32 height);
|
Window(const std::string& title, u32 width, u32 height);
|
||||||
@@ -63,8 +73,24 @@ namespace Engine {
|
|||||||
// 输入事件系统
|
// 输入事件系统
|
||||||
|
|
||||||
bool IsKeyPressed(int key) const;
|
bool IsKeyPressed(int key) const;
|
||||||
|
// UI 捕获鼠标(ImGui 悬停)时返回 false——面板上的按键归 ImGui
|
||||||
|
bool IsMouseButtonPressed(int button) const;
|
||||||
void GetMouseDelta(float& dx, float& dy) 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;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -80,6 +106,13 @@ namespace Engine {
|
|||||||
CaptureFn m_KeyboardCapture;
|
CaptureFn m_KeyboardCapture;
|
||||||
CaptureFn m_MouseCapture;
|
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)
|
// GLFW 静态回调(通过 window user pointer 找回 this)
|
||||||
static void FramebufferResizeCallback(GLFWwindow* window, int width, int height);
|
static void FramebufferResizeCallback(GLFWwindow* window, int width, int height);
|
||||||
static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
|
static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -12,10 +12,18 @@
|
|||||||
#include <Engine/Core/UUID.h>
|
#include <Engine/Core/UUID.h>
|
||||||
#include <Engine/Core/AssetRef.h>
|
#include <Engine/Core/AssetRef.h>
|
||||||
#include <Engine/Renderer/Camera.h>
|
#include <Engine/Renderer/Camera.h>
|
||||||
|
#include <Engine/Scripting/ScriptParam.h>
|
||||||
|
|
||||||
|
#include <entt/entt.hpp>
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
class Model;
|
class Model;
|
||||||
@@ -27,6 +35,8 @@ namespace Engine::ECS {
|
|||||||
// ECS 内部遍历不用它(用 index+generation 句柄,性能)。
|
// ECS 内部遍历不用它(用 index+generation 句柄,性能)。
|
||||||
struct IdComponent {
|
struct IdComponent {
|
||||||
UUID uuid;
|
UUID uuid;
|
||||||
|
|
||||||
|
operator UUID() const { return uuid; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// 实体名(调试/查询用;大规模场景建议演进为哈希 id)
|
// 实体名(调试/查询用;大规模场景建议演进为哈希 id)
|
||||||
@@ -34,57 +44,323 @@ namespace Engine::ECS {
|
|||||||
std::string name;
|
std::string name;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 世界变换:UE USceneComponent 的 encapsulation 模式——
|
// 变换(UE FTransform 语义):TRS 分量 = 相对父的 **local** 值
|
||||||
// SRT 分量私有,所有写入口标脏;world 矩阵由帧循环的
|
//(无父实体 local 即 world,旧场景零迁移)。分量私有,所有写入口
|
||||||
// Scene::UpdateTransforms() 只对脏的实体重算(transform-update
|
// 标脏;world 缓存(SRT 三分量 + 矩阵)由帧循环的
|
||||||
// 阶段),并自动向未来的子层级传播。渲染热路径只读 World()。
|
// Scene::UpdateTransforms() 从根 DFS 只对脏子树重算(transform-
|
||||||
// 层级(父子变换)留给后续的层级同步系统。
|
// update 阶段)。子 world = 父 world SRT 组合而非矩阵乘法——
|
||||||
|
// 非均匀父缩放下组合式仍是干净 SRT(矩阵分解会引入剪切)。
|
||||||
|
// 渲染热路径只读 World();灯光/相机/物理/Gizmo 读 world 三分量。
|
||||||
|
//
|
||||||
|
// 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角):
|
||||||
|
// 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在
|
||||||
|
// 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation
|
||||||
|
// 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。
|
||||||
struct TransformComponent {
|
struct TransformComponent {
|
||||||
// ---- 读取 ----
|
// ---- local 读取(相对父;UI/游戏逻辑用)----
|
||||||
const glm::vec3& GetPosition() const { return m_Position; }
|
const glm::vec3& GetPosition() const { return m_Position; }
|
||||||
const glm::vec3& GetRotation() const { return m_Rotation; } // 欧拉角(度)
|
const glm::quat& GetRotationQuat() const { return m_Rotation; }
|
||||||
const glm::vec3& GetScale() const { return m_Scale; }
|
const glm::vec3& GetScale() const { return m_Scale; }
|
||||||
const glm::mat4& World() const { return m_World; } // 要求已由 UpdateTransforms 计算
|
|
||||||
|
// ---- 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; }
|
||||||
|
|
||||||
|
// 欧拉角(度)视图:从 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 重算)----
|
// ---- 写入(标脏,脏实体由 UpdateTransforms 重算)----
|
||||||
TransformComponent& SetPosition(const glm::vec3& p) { if (m_Position != p) { m_Position = p; m_Dirty = true; } return *this; }
|
TransformComponent& SetPosition(const glm::vec3& p) { if (m_Position != p) { m_Position = p; m_Dirty = true; } return *this; }
|
||||||
TransformComponent& SetRotation(const glm::vec3& r) { if (m_Rotation != r) { m_Rotation = r; 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; }
|
TransformComponent& SetScale(const glm::vec3& s) { if (m_Scale != s) { m_Scale = s; m_Dirty = true; } return *this; }
|
||||||
|
|
||||||
bool IsDirty() const { return m_Dirty; }
|
bool IsDirty() const { return m_Dirty; }
|
||||||
|
|
||||||
|
// 显式标脏:层级系统改父/挂载后触发 world 重算用
|
||||||
|
//(local 值未变但 world 缓存已失效)
|
||||||
|
TransformComponent& MarkDirty() { m_Dirty = true; return *this; }
|
||||||
|
|
||||||
// SRT 分量 → world 矩阵(T * R * S),清除脏标记。
|
// SRT 分量 → world 矩阵(T * R * S),清除脏标记。
|
||||||
// 由 Scene::UpdateTransforms 调用(或层级系统),勿手动
|
// 根实体版(world = local);由 Scene::UpdateTransforms 调用,
|
||||||
|
// 勿手动
|
||||||
TransformComponent& Compose()
|
TransformComponent& Compose()
|
||||||
{
|
{
|
||||||
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_Position);
|
m_WorldPosition = m_Position;
|
||||||
const glm::mat4 r =
|
m_WorldRotation = m_Rotation;
|
||||||
glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation.y), glm::vec3(0, 1, 0)) *
|
m_WorldScale = m_Scale;
|
||||||
glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation.x), glm::vec3(1, 0, 0)) *
|
RebuildWorldMatrix();
|
||||||
glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation.z), glm::vec3(0, 0, 1));
|
m_Dirty = false;
|
||||||
const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_Scale);
|
return *this;
|
||||||
m_World = t * r * s;
|
}
|
||||||
|
|
||||||
|
// 子实体版: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;
|
m_Dirty = false;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
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::vec3 m_Position{0.0f};
|
||||||
glm::vec3 m_Rotation{0.0f};
|
glm::quat m_Rotation{1.0f, 0.0f, 0.0f, 0.0f}; // 单位四元数(w,x,y,z)
|
||||||
glm::vec3 m_Scale{1.0f};
|
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};
|
glm::mat4 m_World{1.0f};
|
||||||
bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world
|
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 资产。
|
// 网格渲染体:引用共享的 Model 资产。
|
||||||
// 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
|
// 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
|
||||||
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
|
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
|
||||||
|
//
|
||||||
|
// 材质覆盖(UE Material Slot 语义):key = mesh 下标(Model::GetMeshes
|
||||||
|
// 序),value = .material 资产路径;无该项 = 用模型内建材质(只读)。
|
||||||
|
// 存路径不存句柄:覆盖材质由渲染收集期按路径惰性 resolve(资产
|
||||||
|
// 已被场景预载/编辑器指派,收集只查缓存),组件保持纯数据。
|
||||||
|
// 资产被多实体共享时编辑同时生效;实体独立 = 每实体一份覆盖文件
|
||||||
struct MeshRendererComponent {
|
struct MeshRendererComponent {
|
||||||
ModelRef 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 的实体是当前渲染视角。
|
// 相机。一个场景中 primary=true 的实体是当前渲染视角。
|
||||||
struct CameraComponent {
|
struct CameraComponent {
|
||||||
|
bool active = true; // 禁用 = 退出 primary 候选
|
||||||
Camera camera;
|
Camera camera;
|
||||||
bool primary = false;
|
bool primary = false;
|
||||||
};
|
};
|
||||||
@@ -93,6 +369,7 @@ namespace Engine::ECS {
|
|||||||
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
|
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
|
||||||
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
|
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
|
||||||
struct LightComponent {
|
struct LightComponent {
|
||||||
|
bool active = true; // 禁用 = 不进光照
|
||||||
enum class Type : u8 { Directional = 0, Point = 1 };
|
enum class Type : u8 { Directional = 0, Point = 1 };
|
||||||
Type type = Type::Directional;
|
Type type = Type::Directional;
|
||||||
glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光
|
glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光
|
||||||
@@ -101,6 +378,156 @@ namespace Engine::ECS {
|
|||||||
bool castShadow = false; // 方向光可投影(引擎取第一个 castShadow 方向光为 CSM 光源)
|
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
|
} // namespace Engine::ECS
|
||||||
|
|
||||||
#endif //HELLOVULKAN_COMPONENTS_H
|
#endif //HELLOVULKAN_COMPONENTS_H
|
||||||
|
|||||||
@@ -40,30 +40,69 @@ namespace Engine::ECS {
|
|||||||
Scene& operator=(const Scene&) = delete;
|
Scene& operator=(const Scene&) = delete;
|
||||||
|
|
||||||
// ---- 场景级设置 ----
|
// ---- 场景级设置 ----
|
||||||
|
// 环境类设置已迁为环境实体组件(SkyboxComponent/SkyLightComponent,
|
||||||
|
// UE Skybox/SkyLight Actor 语义);仅保留渲Target清屏色
|
||||||
struct Settings {
|
struct Settings {
|
||||||
glm::vec4 ClearColor = {0.1, 0.1, 0.1, 1.0};
|
glm::vec4 ClearColor = {0.0, 0.0, 0.0, 1.0};
|
||||||
glm::vec4 AmbientColor{0.07f, 0.07f, 0.09f, 0.0f};
|
|
||||||
bool DrawSkybox = true;
|
|
||||||
};
|
};
|
||||||
Settings& GetSettings() { return m_Settings; }
|
Settings& GetSettings() { return m_Settings; }
|
||||||
|
const Settings& GetSettings() const { return m_Settings; } // 序列化只读
|
||||||
|
|
||||||
// ---- 实体 ----
|
// ---- 实体 ----
|
||||||
|
|
||||||
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy()
|
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy()
|
||||||
EntityHandle CreateEntity(std::string name = {});
|
EntityHandle CreateEntity(std::string name = "new entity");
|
||||||
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
|
// 延迟:帧尾统一销毁。层级级联(UE 语义):销毁父 → 子树随葬
|
||||||
|
void DestroyEntity(Entity e);
|
||||||
void FlushDestroyQueue(); // 每帧末尾由 Application 调用
|
void FlushDestroyQueue(); // 每帧末尾由 Application 调用
|
||||||
|
|
||||||
|
// 整场景换血(序列化加载):立即销毁全部实体并清空延迟队列。
|
||||||
|
// 仅用于"校验通过后的原子替换"——调用方持有的 Entity/EntityHandle
|
||||||
|
// 全部悬空,必须自行重解析(GetEntityHandle 可从裸 entity 重建)
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
// 裸 entity → 句柄表皮(EntityHandle 构造私有 + friend Scene;
|
||||||
|
// 序列化加载后应用层重解析悬空句柄用)。无效 entity 返回空句柄
|
||||||
|
EntityHandle GetEntityHandle(Entity e);
|
||||||
|
|
||||||
// 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update
|
// 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update
|
||||||
// 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用,
|
// 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用,
|
||||||
// 用户代码永远不需要手动 Compose
|
// 用户代码永远不需要手动 Compose。层级语义:从根 DFS,脏实体
|
||||||
|
// 或脏祖先的后代才重算(父先于子,ComposeFromParent 组合)
|
||||||
void UpdateTransforms();
|
void UpdateTransforms();
|
||||||
bool IsAlive(Entity e) const { return m_Registry.valid(e); }
|
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
|
// 按稳定 UUID 查实体(序列化/跨引用场景);找不到返回 entt::null
|
||||||
Entity FindEntityByUUID(UUID uuid);
|
Entity FindEntityByUUID(UUID uuid);
|
||||||
|
|
||||||
|
// 全量重建 UUID → Entity 索引(序列化加载完调一次;运行期勿调,
|
||||||
|
// CreateEntity / FlushDestroyQueue 已增量维护)
|
||||||
|
void RebuildUuidIndex();
|
||||||
|
|
||||||
Registry& GetRegistry() { return m_Registry; }
|
Registry& GetRegistry() { return m_Registry; }
|
||||||
|
const Registry& GetRegistry() const { return m_Registry; } // 序列化等只读遍历
|
||||||
|
|
||||||
// ---- 组件便捷接口(转发 entt)----
|
// ---- 组件便捷接口(转发 entt)----
|
||||||
|
|
||||||
@@ -102,6 +141,12 @@ namespace Engine::ECS {
|
|||||||
Registry m_Registry;
|
Registry m_Registry;
|
||||||
std::vector<Entity> m_PendingDestroy;
|
std::vector<Entity> m_PendingDestroy;
|
||||||
Settings m_Settings;
|
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
|
} // namespace Engine::ECS
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
|
#include <Engine/Core/FrameContext.h> // kMaxFramesInFlight
|
||||||
|
#include <imgui.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
@@ -35,6 +37,29 @@ namespace Engine {
|
|||||||
ImGuiLayer(const ImGuiLayer&) = delete;
|
ImGuiLayer(const ImGuiLayer&) = delete;
|
||||||
ImGuiLayer& operator=(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 驱动)----
|
// ---- 每帧三段式(由 Application::Run 驱动)----
|
||||||
|
|
||||||
// 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间,
|
// 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间,
|
||||||
@@ -55,8 +80,13 @@ namespace Engine {
|
|||||||
// 生命周期保证:Application 成员序中 ImGuiLayer 在 Renderer 之后声明
|
// 生命周期保证:Application 成员序中 ImGuiLayer 在 Renderer 之后声明
|
||||||
//(先析构),因此 Device 引用在整个层生命周期内有效
|
//(先析构),因此 Device 引用在整个层生命周期内有效
|
||||||
Device& m_Device;
|
Device& m_Device;
|
||||||
|
Window& m_Window; // 光标捕获状态查询(NewFrame hover 门控)
|
||||||
vk::DescriptorPool m_Pool;
|
vk::DescriptorPool m_Pool;
|
||||||
bool m_Enabled = true;
|
bool m_Enabled = true;
|
||||||
|
ImTextureID m_SceneTextures[kMaxFramesInFlight]{};
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
|
ImTextureID m_PreviewTextures[kMaxFramesInFlight]{};
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine
|
} // 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
|
||||||
@@ -98,6 +98,8 @@ namespace Engine::RG {
|
|||||||
// 按句柄取当前帧槽位的物理资源(image/view)
|
// 按句柄取当前帧槽位的物理资源(image/view)
|
||||||
vk::ImageView View(TextureHandle h) const;
|
vk::ImageView View(TextureHandle h) const;
|
||||||
vk::Image Image(TextureHandle h) const;
|
vk::Image Image(TextureHandle h) const;
|
||||||
|
// 物理尺寸(导入资源 = SetImportedExtent 设定值;创建资源 = 物理镜像)
|
||||||
|
vk::Extent2D Extent(TextureHandle h) const;
|
||||||
|
|
||||||
// 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。
|
// 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。
|
||||||
// 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。
|
// 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。
|
||||||
@@ -129,6 +131,7 @@ namespace Engine::RG {
|
|||||||
// 导入资源每帧绑定
|
// 导入资源每帧绑定
|
||||||
vk::Image importedImage{};
|
vk::Image importedImage{};
|
||||||
vk::ImageView importedView{};
|
vk::ImageView importedView{};
|
||||||
|
vk::Extent2D importedExtent{}; // 物理尺寸(swapchain 重建后更新)
|
||||||
|
|
||||||
// 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体)
|
// 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体)
|
||||||
struct SlotState {
|
struct SlotState {
|
||||||
@@ -207,6 +210,11 @@ namespace Engine::RG {
|
|||||||
// 每帧 acquire 后调用,把当帧 swapchain image 绑到导入资源
|
// 每帧 acquire 后调用,把当帧 swapchain image 绑到导入资源
|
||||||
void SetImportedImage(TextureHandle h, vk::Image image, vk::ImageView view);
|
void SetImportedImage(TextureHandle h, vk::Image image, vk::ImageView view);
|
||||||
|
|
||||||
|
// 导入资源的物理尺寸(swapchain 重建后同步更新)。pass 渲染区域
|
||||||
|
// 跟实际附件尺寸走:backbuffer = 窗口尺寸,与目标尺寸(场景渲染
|
||||||
|
// 分辨率)解耦——Editor 视口模式下二者不同
|
||||||
|
void SetImportedExtent(TextureHandle h, vk::Extent2D extent);
|
||||||
|
|
||||||
// 指定帧槽位的物理镜像 view(Compile 后有效)。
|
// 指定帧槽位的物理镜像 view(Compile 后有效)。
|
||||||
// 用途:把 RG 管理的资源绑定到外部描述符(如阴影图采样)。
|
// 用途:把 RG 管理的资源绑定到外部描述符(如阴影图采样)。
|
||||||
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
|
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
|
||||||
@@ -260,6 +268,24 @@ namespace Engine::RG {
|
|||||||
// pool 由调用方创建(容量 ≥ framesInFlight × queriesPerFrame),
|
// pool 由调用方创建(容量 ≥ framesInFlight × queriesPerFrame),
|
||||||
// 读取由调用方在本槽位 fence 等待之后进行。
|
// 读取由调用方在本槽位 fence 等待之后进行。
|
||||||
void SetQueryPool(vk::QueryPool pool, u32 queriesPerFrame);
|
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;
|
std::vector<std::string> GetPassNames() const;
|
||||||
|
|
||||||
// ---- 统计 ----
|
// ---- 统计 ----
|
||||||
@@ -270,6 +296,14 @@ namespace Engine::RG {
|
|||||||
u32 GetBarrierCount() const;
|
u32 GetBarrierCount() const;
|
||||||
u32 GetPooledImageCount() const;
|
u32 GetPooledImageCount() const;
|
||||||
|
|
||||||
|
// GPU timestamp:指定槽位上一轮实际写入的查询数(active pass × 2)。
|
||||||
|
// 剔除使 active 数 < 总 pass 数——读侧必须按此数取结果,按满额
|
||||||
|
// 阻塞读会等永远无人写入的查询(死锁)
|
||||||
|
u32 GetWrittenQueries(u32 frameSlot) const
|
||||||
|
{
|
||||||
|
return frameSlot < 4 ? m_WrittenQueries[frameSlot] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend struct RenderGraphContext;
|
friend struct RenderGraphContext;
|
||||||
|
|
||||||
@@ -280,7 +314,10 @@ namespace Engine::RG {
|
|||||||
void ComputeLifetimes();
|
void ComputeLifetimes();
|
||||||
void PrecomputeBarriers();
|
void PrecomputeBarriers();
|
||||||
void AllocatePhysical();
|
void AllocatePhysical();
|
||||||
void ReleasePhysical();
|
// keepFixedSize = true:固定尺寸资源(desc 显式 width/height,与目标
|
||||||
|
// 渲染尺寸无关)的物理镜像不归还池、保留在 entry 上——重编译后原样
|
||||||
|
// 沿用同一 VkImage,内容存续(材质球预览 atlas 不因视口 resize 清空)
|
||||||
|
void ReleasePhysical(bool keepFixedSize);
|
||||||
void PurgePool(vk::Extent2D target);
|
void PurgePool(vk::Extent2D target);
|
||||||
PhysicalImage* AcquirePhysical(vk::Format format, vk::Extent2D extent,
|
PhysicalImage* AcquirePhysical(vk::Format format, vk::Extent2D extent,
|
||||||
vk::ImageUsageFlags usage,
|
vk::ImageUsageFlags usage,
|
||||||
@@ -314,6 +351,8 @@ namespace Engine::RG {
|
|||||||
// GPU timestamp(可选注入)
|
// GPU timestamp(可选注入)
|
||||||
vk::QueryPool m_QueryPool{};
|
vk::QueryPool m_QueryPool{};
|
||||||
u32 m_QueriesPerFrame = 0;
|
u32 m_QueriesPerFrame = 0;
|
||||||
|
u32 m_WrittenQueries[4] = {}; // 每槽位上一轮实际写入数(active×2)
|
||||||
|
IGpuProfiler* m_GpuProfiler = nullptr; // 非拥有(Renderer 持有)
|
||||||
|
|
||||||
// 物理资源池:key = (format, extent, usage),value = 空闲镜像列表
|
// 物理资源池:key = (format, extent, usage),value = 空闲镜像列表
|
||||||
struct PoolKey {
|
struct PoolKey {
|
||||||
|
|||||||
@@ -4,9 +4,18 @@
|
|||||||
// Camera:独立的相机模块。
|
// Camera:独立的相机模块。
|
||||||
//
|
//
|
||||||
// 设计(Transform 单一事实源):相机的位置与朝向完全由实体上的
|
// 设计(Transform 单一事实源):相机的位置与朝向完全由实体上的
|
||||||
// TransformComponent 承载——Update 把输入直接写回 transform(末尾
|
// TransformComponent 承载——Update 把输入直接写回 transform,视图
|
||||||
// Compose),视图矩阵 = 实体世界矩阵的逆(本地 -Z 为视线方向)。
|
// 矩阵 = 实体世界矩阵的逆(本地 -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
|
#ifndef HELLOVULKAN_CAMERA_H
|
||||||
@@ -17,32 +26,88 @@
|
|||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
class Window;
|
|
||||||
namespace ECS { struct TransformComponent; }
|
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 {
|
class Camera {
|
||||||
public:
|
public:
|
||||||
// 输入驱动:WASD/Space/Shift 移动 + 鼠标视角(需 SetMouseLookEnabled)。
|
// 投影模式(UE/Unity Camera 语义:透视 = 近大远小;正交 =
|
||||||
// 全部写回传入的 transform,有变更才 Compose。
|
// 平行投影,工程图纸/2D/等距视角)
|
||||||
void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime);
|
enum class ProjectionMode : u8 { Perspective = 0, Orthographic = 1 };
|
||||||
|
|
||||||
|
// 输入驱动(键鼠查询统一走 Input 门面,零 Window 依赖)。
|
||||||
|
// flyRequiresRMB=true(编辑器模式):移动键仅在 RMB 按住时生效
|
||||||
|
// ——解决与 Gizmo W/E/R 工具热键的键位冲突(UE 语义:飞行时
|
||||||
|
// 键盘归相机)。全部写回传入的 transform
|
||||||
|
void Update(ECS::TransformComponent& transform,
|
||||||
|
f32 deltaTime, bool flyRequiresRMB = false);
|
||||||
|
|
||||||
|
// 焦点选中(UE frame-selection):pivot 设为 point,相机沿当前
|
||||||
|
// 视线平移对准;环绕距离沿用当前值(初始 5m,滚轮可改)
|
||||||
|
void FocusOn(ECS::TransformComponent& transform, const glm::vec3& point);
|
||||||
|
|
||||||
// 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose)
|
// 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose)
|
||||||
glm::mat4 GetView(const ECS::TransformComponent& transform) const;
|
glm::mat4 GetView(const ECS::TransformComponent& transform) const;
|
||||||
|
|
||||||
// Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度
|
// Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度。
|
||||||
glm::mat4 GetProjection(f32 aspect, f32 nearZ, f32 farZ) const;
|
// 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 SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
|
||||||
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
|
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; }
|
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
|
||||||
|
|
||||||
|
// 清除环绕 pivot(位姿被外部整体改写后 pivot 已失效——下一次
|
||||||
|
// MMB 环绕按新视线自动重建)
|
||||||
|
void ResetOrbit() { m_HasPivot = false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
f32 m_MoveSpeed = 2.5f;
|
f32 m_MoveSpeed = 2.5f;
|
||||||
f32 m_MouseSensitivity = 0.1f;
|
f32 m_MouseSensitivity = 0.1f;
|
||||||
bool m_MouseLookEnabled = false;
|
bool m_MouseLookEnabled = true;
|
||||||
|
|
||||||
|
// ---- 投影参数(每实体;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
|
} // namespace Engine
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/Renderer/Cubemap.h> // DecodedEquirect(解码产物数据契约)
|
#include <Engine/Renderer/Cubemap.h> // DecodedEquirect(解码产物数据契约)
|
||||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||||
|
#include <Engine/Vulkan/ShaderManager.h>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -36,7 +37,6 @@ namespace Engine {
|
|||||||
|
|
||||||
class Device;
|
class Device;
|
||||||
class DescriptorManager;
|
class DescriptorManager;
|
||||||
class ShaderManager;
|
|
||||||
class Cubemap;
|
class Cubemap;
|
||||||
|
|
||||||
class IblGenerator {
|
class IblGenerator {
|
||||||
@@ -95,10 +95,11 @@ namespace Engine {
|
|||||||
vk::PipelineLayout m_IblLayoutPush;
|
vk::PipelineLayout m_IblLayoutPush;
|
||||||
vk::PipelineLayout m_LutLayout;
|
vk::PipelineLayout m_LutLayout;
|
||||||
|
|
||||||
vk::ShaderEXT m_ToCubeShader{};
|
// compute shader(引用计数句柄,析构自动销毁)
|
||||||
vk::ShaderEXT m_IrrShader{};
|
ShaderManager::ShaderRef m_ToCubeShader;
|
||||||
vk::ShaderEXT m_PrefShader{};
|
ShaderManager::ShaderRef m_IrrShader;
|
||||||
vk::ShaderEXT m_LutShader{};
|
ShaderManager::ShaderRef m_PrefShader;
|
||||||
|
ShaderManager::ShaderRef m_LutShader;
|
||||||
|
|
||||||
vk::Sampler m_EquirectSampler; // S=repeat(经度环绕)T=clamp
|
vk::Sampler m_EquirectSampler; // S=repeat(经度环绕)T=clamp
|
||||||
vk::CommandPool m_Pool; // 持久复用,每次生成 reset
|
vk::CommandPool m_Pool; // 持久复用,每次生成 reset
|
||||||
|
|||||||
@@ -7,19 +7,26 @@
|
|||||||
// 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性)
|
// 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性)
|
||||||
// 2 metallicRoughness(线性:G=粗糙度,B=金属度,glTF 约定)
|
// 2 metallicRoughness(线性:G=粗糙度,B=金属度,glTF 约定)
|
||||||
// 3 emissive(自发光,sRGB) 4 ao(环境光遮蔽,线性)
|
// 3 emissive(自发光,sRGB) 4 ao(环境光遮蔽,线性)
|
||||||
// 5 材质参数 UBO(fragment,kParamsBinding)
|
// 5 roughness(独立粗糙度图,R 通道——FBX/OBJ specular 工作流,
|
||||||
|
// glTF 合图工作流不用此槽)
|
||||||
|
// 6 材质参数 UBO(fragment,kParamsBinding)
|
||||||
//
|
//
|
||||||
// 纹理非拥有(Model 的纹理缓存持有);缺槽由调用方传默认纹理。
|
// 纹理非拥有(Model 的纹理缓存持有);缺槽由调用方传默认纹理。
|
||||||
// 因子(metallicFactor/roughnessFactor)乘在 MR 贴图上:
|
// 因子(metallicFactor/roughnessFactor)乘在贴图上:
|
||||||
// glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1)
|
// glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1),
|
||||||
// OBJ(specular 工作流,独立 roughness 图或无图)→ metallic=0,
|
// factors.z=0 → shader 直接读 MR.g 当粗糙度
|
||||||
// roughness 因子按贴图有无/Ns 换算——金属度强制 0
|
// 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
|
#ifndef HELLOVULKAN_MATERIAL_H
|
||||||
#define HELLOVULKAN_MATERIAL_H
|
#define HELLOVULKAN_MATERIAL_H
|
||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
|
#include <Engine/Core/FrameContext.h>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -33,9 +40,58 @@ namespace Engine {
|
|||||||
class Texture;
|
class Texture;
|
||||||
class UniformBuffer;
|
class UniformBuffer;
|
||||||
|
|
||||||
// 材质因子(乘 MR 贴图;无贴图时即最终值)
|
// 材质因子(乘贴图;无贴图时即最终值)
|
||||||
|
// x = metallic 因子 y = roughness 因子
|
||||||
|
// z = 独立粗糙度图开关(1 = 用 Roughness 槽 R 通道替换 MR.g)
|
||||||
|
// 注意:与 shader MaterialParams UBO 严格同序同布局(全 vec4,std140
|
||||||
|
// 无填充歧义),加字段必须双端同步(simple_shader*.frag)
|
||||||
struct MaterialParams {
|
struct MaterialParams {
|
||||||
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f}; // x=metallic y=roughness
|
// 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 {
|
class Material {
|
||||||
@@ -47,15 +103,17 @@ namespace Engine {
|
|||||||
MetallicRoughness = 2,
|
MetallicRoughness = 2,
|
||||||
Emissive = 3,
|
Emissive = 3,
|
||||||
Ao = 4,
|
Ao = 4,
|
||||||
kSlotCount = 5, // 纹理槽数
|
Roughness = 5, // 独立粗糙度图(R 通道)
|
||||||
kParamsBinding = 5, // 参数 UBO binding
|
kSlotCount = 6, // 纹理槽数
|
||||||
|
kParamsBinding = 6, // 参数 UBO binding
|
||||||
};
|
};
|
||||||
|
|
||||||
Material(Device& device, MemoryAllocator& allocator,
|
Material(Device& device, MemoryAllocator& allocator,
|
||||||
DescriptorManager& descriptorManager,
|
DescriptorManager& descriptorManager,
|
||||||
vk::DescriptorSetLayout layout,
|
vk::DescriptorSetLayout layout,
|
||||||
const std::array<Texture*, kSlotCount>& textures,
|
const std::array<Texture*, kSlotCount>& textures,
|
||||||
const MaterialParams& params);
|
const MaterialParams& params,
|
||||||
|
bool transparent = false);
|
||||||
~Material();
|
~Material();
|
||||||
|
|
||||||
Material(const Material&) = delete;
|
Material(const Material&) = delete;
|
||||||
@@ -63,15 +121,40 @@ namespace Engine {
|
|||||||
|
|
||||||
vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; }
|
vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; }
|
||||||
Texture* GetTexture(Slot slot) const { return m_Textures[slot]; }
|
Texture* GetTexture(Slot slot) const { return m_Textures[slot]; }
|
||||||
|
// 透明队列归属(glTF alphaMode=BLEND;CPU 收集侧用)
|
||||||
|
bool IsTransparent() const { return m_Transparent; }
|
||||||
|
|
||||||
// 材质缓存键:同贴图组合+同因子共享一份(Model 层使用)
|
// ---- 编辑器调参入口(零 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],
|
static std::string MakeCacheKey(const std::string paths[kSlotCount],
|
||||||
const MaterialParams& params);
|
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:
|
private:
|
||||||
std::array<Texture*, kSlotCount> m_Textures{};
|
std::array<Texture*, kSlotCount> m_Textures{};
|
||||||
vk::DescriptorSet m_DescriptorSet;
|
vk::DescriptorSet m_DescriptorSet;
|
||||||
Scope<UniformBuffer> m_ParamsBuffer; // per-slot 参数 ring(创建时写死)
|
Scope<UniformBuffer> m_ParamsBuffer; // per-slot 参数 ring(创建时写死)
|
||||||
|
bool m_Transparent = false;
|
||||||
|
u64 m_Serial = 0; // ctor 取号(定义见 Material.cpp)
|
||||||
|
u32 m_EditVersion = 0;
|
||||||
};
|
};
|
||||||
} // namespace Engine
|
} // 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/Core/Types.h>
|
||||||
#include <Engine/Renderer/Vertex.h>
|
#include <Engine/Renderer/Vertex.h>
|
||||||
|
#include <Engine/Vulkan/Buffer.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -18,25 +19,49 @@ namespace Engine
|
|||||||
class Buffer;
|
class Buffer;
|
||||||
class Material;
|
class Material;
|
||||||
|
|
||||||
|
// LOD 级数:LOD0 = 原始网格,越高越简(解析期 meshopt_simplify 生成,
|
||||||
|
// 共享顶点数据,只换索引区间)
|
||||||
|
inline constexpr u32 kMeshLodCount = 3;
|
||||||
|
|
||||||
class Mesh
|
class Mesh
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
|
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
|
||||||
|
// lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成
|
||||||
|
// cpuPositions 非空(蒙皮 mesh)= 物理数据源与包围球改用它
|
||||||
|
//(绑定姿态渲染空间,与视觉同源;vertices.position 是蒙皮原始空间)
|
||||||
Mesh(Device& device, MemoryAllocator& allocator,
|
Mesh(Device& device, MemoryAllocator& allocator,
|
||||||
const std::vector<Vertex>& vertices,
|
const std::vector<Vertex>& vertices,
|
||||||
const std::vector<u32>& indices,
|
const std::vector<std::vector<u32>>& lods,
|
||||||
Material* material);
|
Material* material,
|
||||||
|
const std::vector<glm::vec3>* cpuPositions = nullptr);
|
||||||
~Mesh();
|
~Mesh();
|
||||||
|
|
||||||
Mesh(const Mesh&) = delete;
|
Mesh(const Mesh&) = delete;
|
||||||
Mesh& operator=(const Mesh&) = delete;
|
Mesh& operator=(const Mesh&) = delete;
|
||||||
|
|
||||||
// instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后,
|
// instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后,
|
||||||
// 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取
|
// 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取。
|
||||||
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1, u32 firstInstance = 0) const;
|
// lod:索引区间选择(越界取最高级)
|
||||||
|
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1,
|
||||||
|
u32 firstInstance = 0, u32 lod = 0) const;
|
||||||
|
|
||||||
Material* GetMaterial() const { return m_Material; }
|
Material* GetMaterial() const { return m_Material; }
|
||||||
u32 GetIndexCount() const { return m_IndexCount; }
|
u32 GetLodIndexCount(u32 lod) const
|
||||||
|
{
|
||||||
|
return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].indexCount;
|
||||||
|
}
|
||||||
|
// 全局连续槽位(SceneRenderer 数组化 batch 查找用,避免逐实例 hash)
|
||||||
|
u32 GetSlot() const { return m_Slot; }
|
||||||
|
|
||||||
|
// ---- MDI 兼容取数(per-mesh buffer:偏移恒 0,firstIndex 即全局)----
|
||||||
|
vk::Buffer GetVertexBuffer() const { return m_VertexBuffer->Get(); }
|
||||||
|
vk::Buffer GetIndexBuffer() const { return m_IndexBuffer->Get(); }
|
||||||
|
u32 GetVertexOffset() const { return 0; }
|
||||||
|
u32 GetGlobalFirstIndex(u32 lod) const
|
||||||
|
{
|
||||||
|
return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].firstIndex;
|
||||||
|
}
|
||||||
|
|
||||||
// 局部空间包围球(构造时从顶点算出),视锥剔除用
|
// 局部空间包围球(构造时从顶点算出),视锥剔除用
|
||||||
struct Bounds {
|
struct Bounds {
|
||||||
@@ -45,11 +70,27 @@ namespace Engine
|
|||||||
};
|
};
|
||||||
const Bounds& GetBounds() const { return m_Bounds; }
|
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:
|
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_VertexBuffer;
|
||||||
Scope<Buffer> m_IndexBuffer;
|
Scope<Buffer> m_IndexBuffer;
|
||||||
|
std::array<LodRange, kMeshLodCount> m_Lods{};
|
||||||
Material* m_Material;
|
Material* m_Material;
|
||||||
u32 m_IndexCount = 0;
|
u32 m_Slot = 0;
|
||||||
Bounds m_Bounds;
|
Bounds m_Bounds;
|
||||||
|
|
||||||
u64 m_VertexBytes = 0;
|
u64 m_VertexBytes = 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
|
||||||
@@ -10,7 +10,11 @@
|
|||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/Renderer/Material.h>
|
#include <Engine/Renderer/Material.h>
|
||||||
#include <Engine/Renderer/Mesh.h>
|
#include <Engine/Renderer/Mesh.h>
|
||||||
|
#include <Engine/Renderer/MeshFactory.h> // 程序化几何唯一源(MeshData/PrimitiveDesc)
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
|
||||||
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -34,15 +38,91 @@ namespace Engine
|
|||||||
// worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。
|
// worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。
|
||||||
struct ParsedMesh {
|
struct ParsedMesh {
|
||||||
std::vector<Vertex> vertices;
|
std::vector<Vertex> vertices;
|
||||||
std::vector<u32> indices;
|
// LOD 链([0] = 原始索引,越往后越简):解析期 meshopt_simplify
|
||||||
|
// 生成(worker 线程),消费端共享顶点只换索引
|
||||||
|
std::vector<std::vector<u32>> lods;
|
||||||
std::array<std::string, Material::kSlotCount> texPaths;
|
std::array<std::string, Material::kSlotCount> texPaths;
|
||||||
MaterialParams params;
|
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 {
|
struct ParsedModel {
|
||||||
std::string path;
|
std::string path;
|
||||||
bool isGltf = false;
|
bool isGltf = false;
|
||||||
std::vector<ParsedMesh> meshes;
|
std::vector<ParsedMesh> meshes;
|
||||||
bool Valid() const { return !meshes.empty(); }
|
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
|
class Model
|
||||||
@@ -58,6 +138,12 @@ namespace Engine
|
|||||||
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
|
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
|
||||||
static ParsedModel ParseFromDisk(const std::string& path);
|
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 缓冲 / 材质描述符。
|
// GPU 构建(必须主线程):纹理上传 / mesh 缓冲 / 材质描述符。
|
||||||
// 解析失败返回 nullptr。
|
// 解析失败返回 nullptr。
|
||||||
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
|
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
|
||||||
@@ -65,13 +151,41 @@ namespace Engine
|
|||||||
vk::DescriptorSetLayout textureLayout,
|
vk::DescriptorSetLayout textureLayout,
|
||||||
ParsedModel&& parsed);
|
ParsedModel&& parsed);
|
||||||
|
|
||||||
// 程序化模型:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
// 程序化几何标签:几何统一来自 MeshFactory(单一几何源),
|
||||||
// size 为半边长(平面总边长 = 2*size)。
|
// 本类只负责把几何 + 纯色 albedo 组装成可渲染 Model
|
||||||
struct ProceduralTag {};
|
struct ProceduralTag {};
|
||||||
|
|
||||||
|
// 程序化纯色平面:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
||||||
|
// size 为半边长(平面总边长 = 2*size)。
|
||||||
static Scope<Model> CreatePlane(Device& device, MemoryAllocator& allocator,
|
static Scope<Model> CreatePlane(Device& device, MemoryAllocator& allocator,
|
||||||
DescriptorManager& descriptorManager,
|
DescriptorManager& descriptorManager,
|
||||||
vk::DescriptorSetLayout textureLayout,
|
vk::DescriptorSetLayout textureLayout,
|
||||||
f32 halfSize, u8 r, u8 g, u8 b);
|
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();
|
~Model();
|
||||||
|
|
||||||
@@ -79,12 +193,53 @@ namespace Engine
|
|||||||
Model& operator=(const Model&) = delete;
|
Model& operator=(const Model&) = delete;
|
||||||
|
|
||||||
const std::vector<Scope<Mesh>>& GetMeshes() const { return m_Meshes; }
|
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:
|
private:
|
||||||
|
// 程序化几何骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 顶点/索引
|
||||||
|
// 由 MeshFactory 产出(见 .cpp)。name = 编辑器材质槽显示名。
|
||||||
|
// 各具名工厂(CreatePlane/Cube/Primitive)都折到这一个 ctor
|
||||||
Model(Device& device, MemoryAllocator& allocator,
|
Model(Device& device, MemoryAllocator& allocator,
|
||||||
DescriptorManager& descriptorManager,
|
DescriptorManager& descriptorManager,
|
||||||
vk::DescriptorSetLayout textureLayout,
|
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 由调用方填
|
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
|
||||||
Model(Device& device, MemoryAllocator& allocator,
|
Model(Device& device, MemoryAllocator& allocator,
|
||||||
@@ -92,22 +247,57 @@ namespace Engine
|
|||||||
vk::DescriptorSetLayout textureLayout);
|
vk::DescriptorSetLayout textureLayout);
|
||||||
|
|
||||||
void LoadModel(const std::string& path);
|
void LoadModel(const std::string& path);
|
||||||
|
// aiNode 树拍平成 node 表(DFS 序,parent 下标回填)
|
||||||
|
static u32 FlattenNodes(const aiNode* node, i32 parent,
|
||||||
|
std::vector<ModelNode>& out);
|
||||||
|
// 每帧动画的 channel 轨道提取(nodeName → node 表下标)
|
||||||
|
static void ParseAnimations(const aiScene* scene,
|
||||||
|
const std::vector<ModelNode>& nodes,
|
||||||
|
std::vector<AnimationClip>& out);
|
||||||
static void ProcessNode(const aiNode* node, const aiScene* scene,
|
static void ProcessNode(const aiNode* node, const aiScene* scene,
|
||||||
std::vector<ParsedMesh>& out,
|
std::vector<ParsedMesh>& out,
|
||||||
const std::filesystem::path& modelDir,
|
const std::filesystem::path& modelDir,
|
||||||
bool isGltf);
|
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,
|
static ParsedMesh ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||||
const std::filesystem::path& modelDir,
|
const std::filesystem::path& modelDir,
|
||||||
bool isGltf);
|
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();
|
void InitDefaultTextures();
|
||||||
|
|
||||||
|
// 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);
|
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
|
||||||
|
|
||||||
// 材质缓存入口:同贴图组合+因子共享同一 Material(一个描述符集),
|
// 材质缓存入口:同贴图组合+同因子+同透明语义共享一份 Material,
|
||||||
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
|
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
|
||||||
// paths 下标 = Material::Slot;params = metallic/roughness 因子
|
// paths 下标 = Material::Slot;params = metallic/roughness 因子;
|
||||||
|
// slotOverrides = 程序化模型的槽位纹理直供(非空项优先于路径/默认)
|
||||||
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
|
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
|
||||||
const MaterialParams& params);
|
const MaterialParams& params,
|
||||||
|
bool transparent = false,
|
||||||
|
const std::array<Texture*, Material::kSlotCount>&
|
||||||
|
slotOverrides = {});
|
||||||
|
|
||||||
Device& m_Device;
|
Device& m_Device;
|
||||||
MemoryAllocator& m_Allocator;
|
MemoryAllocator& m_Allocator;
|
||||||
@@ -122,8 +312,17 @@ namespace Engine
|
|||||||
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
|
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
|
||||||
|
|
||||||
std::vector<Scope<Mesh>> m_Meshes;
|
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;
|
std::string m_ModelPath;
|
||||||
bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV
|
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
|
} // 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,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
|
||||||
@@ -20,19 +20,38 @@ namespace Engine::Passes {
|
|||||||
ForwardPass(SceneGpu& gpu, FrameSceneData& frame,
|
ForwardPass(SceneGpu& gpu, FrameSceneData& frame,
|
||||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||||
RG::TextureHandle depth,
|
RG::TextureHandle depth,
|
||||||
|
RG::TextureHandle sceneColor,
|
||||||
const std::array<RG::TextureHandle, kMaxCascades>& shadowMaps)
|
const std::array<RG::TextureHandle, kMaxCascades>& shadowMaps)
|
||||||
: m_Gpu(gpu), m_Frame(frame),
|
: m_Gpu(gpu), m_Frame(frame),
|
||||||
m_HDRColor(hdrColor), m_Velocity(velocity),
|
m_HDRColor(hdrColor), m_Velocity(velocity),
|
||||||
m_Depth(depth), m_ShadowMaps(shadowMaps) {}
|
m_Depth(depth), m_SceneColor(sceneColor),
|
||||||
|
m_ShadowMaps(shadowMaps) {}
|
||||||
|
|
||||||
void Register(RG::RenderGraph& rg) override;
|
void Register(RG::RenderGraph& rg) override;
|
||||||
|
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
||||||
|
|
||||||
private:
|
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;
|
SceneGpu& m_Gpu;
|
||||||
FrameSceneData& m_Frame;
|
FrameSceneData& m_Frame;
|
||||||
RG::TextureHandle m_HDRColor;
|
RG::TextureHandle m_HDRColor;
|
||||||
RG::TextureHandle m_Velocity; // TAA 运动向量(第二颜色附件)
|
RG::TextureHandle m_Velocity; // TAA 运动向量(第二颜色附件)
|
||||||
RG::TextureHandle m_Depth;
|
RG::TextureHandle m_Depth;
|
||||||
|
RG::TextureHandle m_SceneColor; // 场景色副本(透射折射采样源)
|
||||||
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps;
|
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/RenderGraph/RenderGraph.h>
|
#include <Engine/RenderGraph/RenderGraph.h>
|
||||||
|
#include <Engine/Vulkan/ShaderManager.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
class Device;
|
class Device;
|
||||||
class DescriptorManager;
|
class DescriptorManager;
|
||||||
class ShaderManager;
|
|
||||||
|
|
||||||
namespace Passes {
|
namespace Passes {
|
||||||
|
|
||||||
@@ -36,7 +36,10 @@ namespace Engine {
|
|||||||
struct PostProcessParams {
|
struct PostProcessParams {
|
||||||
f32 exposure = 0.6f;
|
f32 exposure = 0.6f;
|
||||||
bool useACES = true;
|
bool useACES = true;
|
||||||
f32 bloomThreshold = 1.0f; // HDR 域亮度阈值
|
// HDR 域亮度阈值。注意不能设 1.0:光滑金属反射亮天空的 HDR 值
|
||||||
|
// 普遍 2~50,阈值为 1 会让整片金属表面进 bloom → 白色泛光糊满
|
||||||
|
//(怀表铜壳"纯白"元凶)。3.0 ≈ ACES 后接近白的真高光才泛光
|
||||||
|
f32 bloomThreshold = 3.0f;
|
||||||
f32 bloomStrength = 0.8f; // 叠加强度
|
f32 bloomStrength = 0.8f; // 叠加强度
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,6 +59,7 @@ namespace Engine {
|
|||||||
kDebugIrradiance, // IBL 漫反射辐照度(32² 低频)
|
kDebugIrradiance, // IBL 漫反射辐照度(32² 低频)
|
||||||
kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级)
|
kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级)
|
||||||
kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道)
|
kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道)
|
||||||
|
kDebugTransparent, // 仅透明队列(Skybox/Forward 跳过绘制,看透明段内容)
|
||||||
kDebugViewCount
|
kDebugViewCount
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,7 +67,7 @@ namespace Engine {
|
|||||||
public:
|
public:
|
||||||
FullscreenGpu(Device& device, DescriptorManager& descriptors,
|
FullscreenGpu(Device& device, DescriptorManager& descriptors,
|
||||||
ShaderManager& shaders);
|
ShaderManager& shaders);
|
||||||
~FullscreenGpu();
|
~FullscreenGpu(); // 只清 sampler/layout(shader 随引用自动销毁)
|
||||||
|
|
||||||
FullscreenGpu(const FullscreenGpu&) = delete;
|
FullscreenGpu(const FullscreenGpu&) = delete;
|
||||||
FullscreenGpu& operator=(const FullscreenGpu&) = delete;
|
FullscreenGpu& operator=(const FullscreenGpu&) = delete;
|
||||||
@@ -71,9 +75,10 @@ namespace Engine {
|
|||||||
vk::DescriptorSetLayout setLayout; // Set0 = {输入0, 输入1, 调试输入}
|
vk::DescriptorSetLayout setLayout; // Set0 = {输入0, 输入1, 调试输入}
|
||||||
vk::PipelineLayout layout; // push 16B(FullscreenPush)
|
vk::PipelineLayout layout; // push 16B(FullscreenPush)
|
||||||
vk::Sampler sampler;
|
vk::Sampler sampler;
|
||||||
vk::ShaderEXT vert{}, extractFrag{}, blurFrag{}, tonemapFrag{};
|
// shader 引用计数句柄(析构自动销毁,零手工清理)
|
||||||
vk::ShaderEXT diffFrag{}; // temporal_diff(帧历史调试)
|
ShaderManager::ShaderRef vert, extractFrag, blurFrag, tonemapFrag;
|
||||||
vk::ShaderEXT taaFrag{}; // taa(时域抗锯齿 resolve)
|
ShaderManager::ShaderRef diffFrag; // temporal_diff(帧历史调试)
|
||||||
|
ShaderManager::ShaderRef taaFrag; // taa(时域抗锯齿 resolve)
|
||||||
|
|
||||||
// 各 pass 自己持有 per-slot set;分配统一走这里
|
// 各 pass 自己持有 per-slot set;分配统一走这里
|
||||||
vk::DescriptorSet AllocateSet();
|
vk::DescriptorSet AllocateSet();
|
||||||
@@ -86,7 +91,8 @@ namespace Engine {
|
|||||||
// 全屏 pass 公共录制:动态状态/draw 全一致,仅附件 view、
|
// 全屏 pass 公共录制:动态状态/draw 全一致,仅附件 view、
|
||||||
// 输入 set、push 值不同
|
// 输入 set、push 值不同
|
||||||
void RecordFullscreen(RG::RenderGraphContext& ctx, vk::ImageView view,
|
void RecordFullscreen(RG::RenderGraphContext& ctx, vk::ImageView view,
|
||||||
vk::ShaderEXT frag, const vk::DescriptorSet& set,
|
const ShaderManager::ShaderRef& frag,
|
||||||
|
const vk::DescriptorSet& set,
|
||||||
const FullscreenPush& push);
|
const FullscreenPush& push);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
#define HELLOVULKAN_IMGUIPASS_H
|
#define HELLOVULKAN_IMGUIPASS_H
|
||||||
|
|
||||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||||
|
#include <Engine/Renderer/SceneTypes.h> // FrameSceneData(displayMode 帧契约)
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
@@ -21,15 +22,27 @@ namespace Engine {
|
|||||||
class ImGuiPass final : public RenderPass {
|
class ImGuiPass final : public RenderPass {
|
||||||
public:
|
public:
|
||||||
// 二级指针:Renderer::SetImGuiLayer 在 Register 之后才可能被调
|
// 二级指针:Renderer::SetImGuiLayer 在 Register 之后才可能被调
|
||||||
//(EnableImGui 时机由派生类决定),执行期解引用取最新值
|
//(EnableImGui 时机由派生类决定),执行期解引用取最新值。
|
||||||
ImGuiPass(ImGuiLayer** layer, RG::TextureHandle backbuffer)
|
// presentTarget:ImGui::Image 采样的场景纹理(tonemap 输出)。
|
||||||
: m_Layer(layer), m_Backbuffer(backbuffer) {}
|
// 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;
|
void Register(RG::RenderGraph& rg) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ImGuiLayer** m_Layer;
|
ImGuiLayer** m_Layer;
|
||||||
|
FrameSceneData& m_Frame; // displayMode(帧契约)
|
||||||
RG::TextureHandle m_Backbuffer;
|
RG::TextureHandle m_Backbuffer;
|
||||||
|
RG::TextureHandle m_PresentTarget;
|
||||||
|
RG::TextureHandle m_PreviewAtlas;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Passes
|
} // namespace Passes
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <Engine/Core/FrameContext.h>
|
#include <Engine/Core/FrameContext.h>
|
||||||
#include <Engine/Renderer/SceneTypes.h>
|
#include <Engine/Renderer/SceneTypes.h>
|
||||||
#include <Engine/RenderGraph/RenderGraph.h>
|
#include <Engine/RenderGraph/RenderGraph.h>
|
||||||
|
#include <Engine/Vulkan/ShaderManager.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -22,27 +23,42 @@ namespace Engine {
|
|||||||
|
|
||||||
class Device;
|
class Device;
|
||||||
class DescriptorManager;
|
class DescriptorManager;
|
||||||
class ShaderManager;
|
|
||||||
class UniformBuffer;
|
class UniformBuffer;
|
||||||
class Cubemap;
|
class Cubemap;
|
||||||
|
class Buffer;
|
||||||
|
class MemoryAllocator;
|
||||||
|
|
||||||
namespace Passes {
|
namespace Passes {
|
||||||
|
|
||||||
class SceneGpu {
|
class SceneGpu {
|
||||||
public:
|
public:
|
||||||
SceneGpu(Device& device, DescriptorManager& descriptors,
|
SceneGpu(Device& device, MemoryAllocator& allocator,
|
||||||
|
DescriptorManager& descriptors,
|
||||||
ShaderManager& shaders,
|
ShaderManager& shaders,
|
||||||
UniformBuffer& ubo, UniformBuffer& objectBuffer,
|
UniformBuffer& ubo, UniformBuffer& objectBuffer,
|
||||||
UniformBuffer& prevObjectBuffer);
|
UniformBuffer& prevObjectBuffer,
|
||||||
~SceneGpu();
|
UniformBuffer& skinOffsetBuffer,
|
||||||
|
UniformBuffer& skinPaletteBuffer,
|
||||||
|
UniformBuffer& entityIdBuffer);
|
||||||
|
~SceneGpu(); // 只清 sampler/layout/set(shader 随引用自动销毁)
|
||||||
|
|
||||||
SceneGpu(const SceneGpu&) = delete;
|
SceneGpu(const SceneGpu&) = delete;
|
||||||
SceneGpu& operator=(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/)----
|
// ---- set 布局(shader 契约,改绑定必须同步 shaders/)----
|
||||||
vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer}
|
vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer,
|
||||||
|
// SkinOffsets, SkinPalettes, EntityIds}
|
||||||
vk::DescriptorSetLayout textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO}
|
vk::DescriptorSetLayout textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO}
|
||||||
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube}
|
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube,
|
||||||
|
// prefiltered cube, BRDF LUT, 场景色副本}
|
||||||
vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO)
|
vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO)
|
||||||
vk::PipelineLayout shadowLayout; // Set0,push = lightVP 64B
|
vk::PipelineLayout shadowLayout; // Set0,push = lightVP 64B
|
||||||
|
|
||||||
@@ -50,22 +66,54 @@ namespace Engine {
|
|||||||
std::vector<vk::DescriptorSet> uboSets;
|
std::vector<vk::DescriptorSet> uboSets;
|
||||||
std::vector<vk::DescriptorSet> shadowSets;
|
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 白色 = 光视锥外自动无阴影
|
// 阴影采样器:clamp-to-border 白色 = 光视锥外自动无阴影
|
||||||
//(省 shader 出界分支)
|
//(省 shader 出界分支)
|
||||||
vk::Sampler shadowSampler;
|
vk::Sampler shadowSampler;
|
||||||
|
|
||||||
vk::ShaderEXT forwardVert{}, forwardFrag{};
|
// 场景色副本采样器(binding 6):普通线性 clamp
|
||||||
vk::ShaderEXT shadowVert{}, shadowFrag{};
|
//(阴影采样器带硬件比较,不可用于常规 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 重编译后重绑阴影图(物理镜像按槽位各一份)
|
// RG 重编译后重绑阴影图(物理镜像按槽位各一份)
|
||||||
void BindShadowMaps(RG::RenderGraph& rg,
|
void BindShadowMaps(RG::RenderGraph& rg,
|
||||||
const std::array<RG::TextureHandle, kMaxCascades>& maps);
|
const std::array<RG::TextureHandle, kMaxCascades>& maps);
|
||||||
|
|
||||||
|
// RG 重编译后重绑场景色副本(Set2 binding 6,透射采样源)
|
||||||
|
void BindSceneColor(RG::RenderGraph& rg, RG::TextureHandle sceneColor);
|
||||||
|
|
||||||
// 环境辐照度(Set2 binding 3):view 全程稳定,绑一次即可
|
// 环境辐照度(Set2 binding 3):view 全程稳定,绑一次即可
|
||||||
void BindEnvironment(const Cubemap& env);
|
void BindEnvironment(const Cubemap& env);
|
||||||
|
|
||||||
|
// 材质预览锁定环境:只绑 previewEnvSets 的 IBL 三件套
|
||||||
|
//(binding 3/4/5),一次调用永久生效;须在 Renderer 初始化
|
||||||
|
//(环境尚为中性兜底)时调用
|
||||||
|
void LockPreviewEnvironment(const Cubemap& env);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Device& m_Device;
|
Device& m_Device;
|
||||||
|
MemoryAllocator& m_Allocator;
|
||||||
DescriptorManager& m_Descriptors;
|
DescriptorManager& m_Descriptors;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// SelectionMaskPass:选中描边 mask pass(UE CustomStencil 语义)。
|
||||||
|
//
|
||||||
|
// 只绘制选中实体(FrameSceneData.selectionDraws,BuildFrame 从实例表
|
||||||
|
// 筛出)到 R8Unorm mask:复用 SceneGpu 的 forwardVert(静态/蒙皮同源
|
||||||
|
// 变换)+ 恒 1 片元。无深度测试 → mask = 完整投影轮廓(透墙可见,
|
||||||
|
// 且前方遮挡物不会在 mask 上挖洞——挖洞会让边缘检测把洞描出来)。
|
||||||
|
//
|
||||||
|
// 无选中帧只 clear mask 不绘制(SideEffect 保活——拷贝终点在 RG 外
|
||||||
|
// 的 pass 会被引用计数剔除,见 HitProxy 教训)。
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef HELLOVULKAN_SELECTIONMASKPASS_H
|
||||||
|
#define HELLOVULKAN_SELECTIONMASKPASS_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 SceneGpu;
|
||||||
|
|
||||||
|
class SelectionMaskPass final : public RenderPass {
|
||||||
|
public:
|
||||||
|
SelectionMaskPass(ShaderManager& shaders,
|
||||||
|
SceneGpu& gpu, FrameSceneData& frame,
|
||||||
|
RG::TextureHandle mask);
|
||||||
|
|
||||||
|
SelectionMaskPass(const SelectionMaskPass&) = delete;
|
||||||
|
SelectionMaskPass& operator=(const SelectionMaskPass&) = delete;
|
||||||
|
|
||||||
|
void Register(RG::RenderGraph& rg) override;
|
||||||
|
void OnTargetsRebuilt(RG::RenderGraph& rg) override {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
SceneGpu& m_Gpu;
|
||||||
|
FrameSceneData& m_Frame;
|
||||||
|
RG::TextureHandle m_Mask;
|
||||||
|
ShaderManager::ShaderRef m_Frag; // 析构自动销毁
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Passes
|
||||||
|
} // namespace Engine
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_SELECTIONMASKPASS_H
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <Engine/Core/FrameContext.h>
|
#include <Engine/Core/FrameContext.h>
|
||||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||||
#include <Engine/Renderer/SceneTypes.h>
|
#include <Engine/Renderer/SceneTypes.h>
|
||||||
|
#include <Engine/Vulkan/ShaderManager.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
@@ -34,7 +35,8 @@ namespace Engine {
|
|||||||
// env 可为空(HDR 资产缺失):仅承担清屏,天空保持清屏色
|
// env 可为空(HDR 资产缺失):仅承担清屏,天空保持清屏色
|
||||||
SkyboxPass(Device& device, ShaderManager& shaders,
|
SkyboxPass(Device& device, ShaderManager& shaders,
|
||||||
DescriptorManager& descriptors, FrameSceneData& frame,
|
DescriptorManager& descriptors, FrameSceneData& frame,
|
||||||
RG::TextureHandle hdrColor, const Cubemap* env);
|
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||||
|
const Cubemap* env);
|
||||||
~SkyboxPass();
|
~SkyboxPass();
|
||||||
|
|
||||||
SkyboxPass(const SkyboxPass&) = delete;
|
SkyboxPass(const SkyboxPass&) = delete;
|
||||||
@@ -48,11 +50,12 @@ namespace Engine {
|
|||||||
DescriptorManager& m_Descriptors;
|
DescriptorManager& m_Descriptors;
|
||||||
FrameSceneData& m_Frame;
|
FrameSceneData& m_Frame;
|
||||||
RG::TextureHandle m_HDRColor;
|
RG::TextureHandle m_HDRColor;
|
||||||
|
RG::TextureHandle m_Velocity; // 天空 velocity 附件(TAA)
|
||||||
const Cubemap* m_Env; // 可空
|
const Cubemap* m_Env; // 可空
|
||||||
|
|
||||||
vk::DescriptorSetLayout m_SetLayout; // Set0 = {samplerCube}
|
vk::DescriptorSetLayout m_SetLayout; // Set0 = {samplerCube}
|
||||||
vk::PipelineLayout m_Layout; // push = invVP 64B
|
vk::PipelineLayout m_Layout; // push = {invVP, prevVP} 128B
|
||||||
vk::ShaderEXT m_Vert{}, m_Frag{};
|
ShaderManager::ShaderRef m_Vert, m_Frag; // 析构自动销毁
|
||||||
std::array<vk::DescriptorSet, kMaxFramesInFlight> m_Sets{};
|
std::array<vk::DescriptorSet, kMaxFramesInFlight> m_Sets{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace Engine::Passes {
|
|||||||
public:
|
public:
|
||||||
TAAPass(FullscreenGpu& gpu, FrameSceneData& frame, f32 historyAlpha,
|
TAAPass(FullscreenGpu& gpu, FrameSceneData& frame, f32 historyAlpha,
|
||||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||||
RG::TextureHandle taaColor);
|
RG::TextureHandle depth, RG::TextureHandle taaColor);
|
||||||
~TAAPass() override = default;
|
~TAAPass() override = default;
|
||||||
|
|
||||||
void Register(RG::RenderGraph& rg) override;
|
void Register(RG::RenderGraph& rg) override;
|
||||||
@@ -34,6 +34,7 @@ namespace Engine::Passes {
|
|||||||
f32 m_HistoryAlpha; // 开启时的历史权重
|
f32 m_HistoryAlpha; // 开启时的历史权重
|
||||||
RG::TextureHandle m_HDR;
|
RG::TextureHandle m_HDR;
|
||||||
RG::TextureHandle m_Velocity;
|
RG::TextureHandle m_Velocity;
|
||||||
|
RG::TextureHandle m_Depth; // 场景深度(深度感知 dilation 用)
|
||||||
RG::TextureHandle m_TAAColor; // 输出 = 下一帧的历史
|
RG::TextureHandle m_TAAColor; // 输出 = 下一帧的历史
|
||||||
|
|
||||||
std::vector<vk::DescriptorSet> m_Sets; // per-slot
|
std::vector<vk::DescriptorSet> m_Sets; // per-slot
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
//
|
//
|
||||||
// Created by atdunbg on 2026/9/23.
|
// Created by atdunbg on 2026/9/23.
|
||||||
//
|
//
|
||||||
// TonemapPass:HDR(+bloom)→ 显示空间,全屏三角写 Backbuffer。
|
// TonemapPass:HDR(+bloom)→ 显示空间,全屏三角写 PresentTarget。
|
||||||
// 显示变换在此显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。
|
// 显示变换在此显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。
|
||||||
// Backbuffer 从此只接收"显示空间"颜色:tonemap 直接写、ImGui 叠加。
|
// PresentTarget 是 offscreen 可采样纹理:由 ImGui::Image 呈现(输出位置
|
||||||
|
// 可自由摆放),backbuffer 不再接收场景画面。
|
||||||
//
|
//
|
||||||
|
|
||||||
#ifndef HELLOVULKAN_TONEMAPPASS_H
|
#ifndef HELLOVULKAN_TONEMAPPASS_H
|
||||||
@@ -28,7 +29,8 @@ namespace Engine {
|
|||||||
PostProcessParams& params,
|
PostProcessParams& params,
|
||||||
FrameSceneData& frame,
|
FrameSceneData& frame,
|
||||||
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
|
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
|
||||||
RG::TextureHandle temporalDiff, RG::TextureHandle backbuffer,
|
RG::TextureHandle temporalDiff,
|
||||||
|
RG::TextureHandle presentTarget,
|
||||||
RG::TextureHandle rawHdr, RG::TextureHandle velocity,
|
RG::TextureHandle rawHdr, RG::TextureHandle velocity,
|
||||||
RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap1,
|
RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap1,
|
||||||
RG::TextureHandle shadowMap2);
|
RG::TextureHandle shadowMap2);
|
||||||
@@ -46,7 +48,7 @@ namespace Engine {
|
|||||||
RG::TextureHandle m_HDR;
|
RG::TextureHandle m_HDR;
|
||||||
RG::TextureHandle m_A;
|
RG::TextureHandle m_A;
|
||||||
RG::TextureHandle m_TemporalDiff;
|
RG::TextureHandle m_TemporalDiff;
|
||||||
RG::TextureHandle m_Backbuffer;
|
RG::TextureHandle m_Target; // PresentTarget(offscreen,ImGui 采样呈现)
|
||||||
// ---- 调试视图源(binding 3-7,Register 时声明读防止剔除)----
|
// ---- 调试视图源(binding 3-7,Register 时声明读防止剔除)----
|
||||||
// 注:TAAColor 视图直接用 binding0(主输入即 TAA 输出),无需重复绑
|
// 注:TAAColor 视图直接用 binding0(主输入即 TAA 输出),无需重复绑
|
||||||
RG::TextureHandle m_RawHDR; // Forward 直出(TAA 前)
|
RG::TextureHandle m_RawHDR; // Forward 直出(TAA 前)
|
||||||
|
|||||||
@@ -23,9 +23,11 @@
|
|||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/Core/FrameContext.h>
|
#include <Engine/Core/FrameContext.h>
|
||||||
#include <Engine/Renderer/SceneTypes.h>
|
#include <Engine/Renderer/SceneTypes.h>
|
||||||
|
#include <Engine/Renderer/Passes/MaterialPreviewPass.h> // PreviewTilePool/PreviewDirtyTile(完整类型:成员 vector)
|
||||||
#include <Engine/RenderGraph/RenderGraph.h>
|
#include <Engine/RenderGraph/RenderGraph.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <optional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
@@ -40,14 +42,21 @@ namespace Engine {
|
|||||||
class CommandManager;
|
class CommandManager;
|
||||||
class UniformBuffer;
|
class UniformBuffer;
|
||||||
class SceneRenderer;
|
class SceneRenderer;
|
||||||
|
class EditorCamera;
|
||||||
class VulkanAssetUploader;
|
class VulkanAssetUploader;
|
||||||
class Cubemap;
|
class Cubemap;
|
||||||
class IblGenerator;
|
class IblGenerator;
|
||||||
class ImGuiLayer;
|
class ImGuiLayer;
|
||||||
|
class TracyGpuProfiler;
|
||||||
|
struct PhysicsDebugFrame; // PhysicsDebugTypes.h(物理调试图元契约)
|
||||||
|
|
||||||
namespace ECS { class Scene; }
|
namespace ECS { class Scene; }
|
||||||
namespace Assets { class AssetManager; class IAssetUploader; }
|
namespace Assets { class AssetManager; class IAssetUploader; }
|
||||||
namespace Passes { class RenderPass; class SceneGpu; class FullscreenGpu; class PostProcessParams; }
|
namespace Passes {
|
||||||
|
class RenderPass; class SceneGpu; class FullscreenGpu;
|
||||||
|
class PostProcessParams; class PhysicsDebugPass; class HitProxyPass;
|
||||||
|
class EditorGizmoPass;
|
||||||
|
}
|
||||||
|
|
||||||
class Renderer {
|
class Renderer {
|
||||||
public:
|
public:
|
||||||
@@ -57,7 +66,7 @@ namespace Engine {
|
|||||||
Renderer(const Renderer&) = delete;
|
Renderer(const Renderer&) = delete;
|
||||||
Renderer& operator=(const Renderer&) = delete;
|
Renderer& operator=(const Renderer&) = delete;
|
||||||
|
|
||||||
void Render(ECS::Scene& scene, Assets::AssetManager& assets);
|
void Render(ECS::Scene& scene, Assets::AssetManager& assets, f32 dt);
|
||||||
|
|
||||||
// ---- 组合根(Application)装配其他模块时需要的访问器 ----
|
// ---- 组合根(Application)装配其他模块时需要的访问器 ----
|
||||||
Device& GetDevice() const { return *m_Device; }
|
Device& GetDevice() const { return *m_Device; }
|
||||||
@@ -71,7 +80,49 @@ namespace Engine {
|
|||||||
|
|
||||||
// 可选叠加层(ImGui):不设置则 RG 里的 ImGui pass 为空转。
|
// 可选叠加层(ImGui):不设置则 RG 里的 ImGui pass 为空转。
|
||||||
// 生命周期:层必须先于 Renderer 析构(Application 成员序保证)。
|
// 生命周期:层必须先于 Renderer 析构(Application 成员序保证)。
|
||||||
void SetImGuiLayer(ImGuiLayer* layer) { m_ImGuiLayer = layer; }
|
void SetImGuiLayer(ImGuiLayer* layer)
|
||||||
|
{
|
||||||
|
m_ImGuiLayer = layer;
|
||||||
|
if (layer) RefreshSceneTextures(); // 后端就绪:注册场景纹理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 物理调试数据源(PhysicsWorld 每帧收集的图元缓冲;不拥有)。
|
||||||
|
// Application 装配时注入;nullptr = 无物理调试
|
||||||
|
void SetPhysicsDebugSource(const PhysicsDebugFrame* src)
|
||||||
|
{
|
||||||
|
m_PhysicsDebug = src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 场景显示纹理(tonemap 输出,ImGui::Image 可采样显示)----
|
||||||
|
// 按 m_CurrentFrame 取当帧槽位的描述符(UI 构建段与紧随的
|
||||||
|
// Render 同槽位)。无 ImGui 层时返回 nullptr
|
||||||
|
void* GetSceneTexture() const;
|
||||||
|
// 渲染分辨率(PresentTarget 尺寸,等比适配用)
|
||||||
|
vk::Extent2D GetRenderExtent() const { return m_Extent; }
|
||||||
|
|
||||||
|
// ---- 材质球预览(editor 专属,HELLO_EDITOR 裁剪;game 构建无此 API)----
|
||||||
|
// UI 构建段只对可见行发幂等请求(UE FAssetThumbnailPool 语义):
|
||||||
|
// tile 池按 Material serial+参数版本判缓存,渲染一次跨帧持久,
|
||||||
|
// 脏 tile 走每帧预算。返回 tile:>=0 = 内容可显示(UV 子矩形取
|
||||||
|
// 图);-1 = 首渲未完成/池满(UI 显示占位)
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
|
i32 RequestMaterialPreview(Material* material);
|
||||||
|
// 材质实例销毁/重建前调用(ReloadMaterialAsset 等):清池内
|
||||||
|
// 脏表引用与槽位——脏 tile 本帧不再解引用已销毁材质(UAF)
|
||||||
|
void DropMaterialPreview(const Material* material);
|
||||||
|
void* GetMaterialPreviewTexture() const;
|
||||||
|
|
||||||
|
// ---- 材质球缩略图烘焙(GPU→CPU 异步回读,无 waitIdle)----
|
||||||
|
// UI 段对"需要静态缩略图"的材质逐帧幂等请求(userKey 由调用方
|
||||||
|
// 定义,Renderer 不解释、原样回传;同 Material serial 去重)。渲染
|
||||||
|
// 段把该材质渲染进预览 tile 后当帧拷进 host 暂存,待该帧槽位
|
||||||
|
// fence 通过(两帧后同槽位再入图)再取走——不阻塞主线程
|
||||||
|
void RequestMaterialPreviewBake(Material* material, u64 userKey);
|
||||||
|
// UI 段轮询:有完成的烘焙则取走(一次性)。像素 = RGBA8,
|
||||||
|
// kPreviewTileSize²(tonemap 后的 SDR,可直接当缩略图)
|
||||||
|
bool PollMaterialPreviewBake(u64& outKey, std::vector<u8>& outPixels,
|
||||||
|
u32& outWidth, u32& outHeight);
|
||||||
|
#endif
|
||||||
|
|
||||||
// ---- 后处理参数(ImGui 可调,转发到 pass 共享的 PostProcessParams)----
|
// ---- 后处理参数(ImGui 可调,转发到 pass 共享的 PostProcessParams)----
|
||||||
f32 GetExposure() const;
|
f32 GetExposure() const;
|
||||||
@@ -91,6 +142,17 @@ namespace Engine {
|
|||||||
void SetTAAEnabled(bool b);
|
void SetTAAEnabled(bool b);
|
||||||
bool GetBloomEnabled() const;
|
bool GetBloomEnabled() const;
|
||||||
void SetBloomEnabled(bool b);
|
void SetBloomEnabled(bool b);
|
||||||
|
bool GetLodEnabled() const;
|
||||||
|
void SetLodEnabled(bool b); // 关 = 全部强制 LOD0
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
|
// ---- 骨骼可视化(转发 SceneRenderer;编辑形态专属)----
|
||||||
|
bool GetBoneWiresVisible() const;
|
||||||
|
void SetBoneWiresVisible(bool b);
|
||||||
|
bool GetBoneAllNodes() const;
|
||||||
|
void SetBoneAllNodes(bool b);
|
||||||
|
f32 GetBoneJointPx() const;
|
||||||
|
void SetBoneJointPx(f32 px);
|
||||||
|
#endif
|
||||||
int GetDebugView() const;
|
int GetDebugView() const;
|
||||||
void SetDebugView(int v); // Passes::DebugView 枚举值
|
void SetDebugView(int v); // Passes::DebugView 枚举值
|
||||||
// 调试视图名表(下标 = DebugView 枚举值,ImGui combo 直接枚举)
|
// 调试视图名表(下标 = DebugView 枚举值,ImGui combo 直接枚举)
|
||||||
@@ -110,15 +172,64 @@ namespace Engine {
|
|||||||
u32 GetMainInstanceCount() const;
|
u32 GetMainInstanceCount() const;
|
||||||
u32 GetShadowInstanceCount() const;
|
u32 GetShadowInstanceCount() const;
|
||||||
bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字)
|
bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字)
|
||||||
|
f32 GetCollectMs() const; // CPU 收集+剔除段耗时
|
||||||
|
|
||||||
|
// ---- 显示模式(Editor 视口 ↔ 全屏直出;帧首同步生效)----
|
||||||
|
int GetDisplayMode() const { return m_DesiredDisplayMode; }
|
||||||
|
void SetDisplayMode(int mode); // cpp 内定义(SceneRenderer 前置声明)
|
||||||
|
void SetGameView(bool gameView); // PIE 内嵌播放(cpp 转发 SceneRenderer)
|
||||||
|
// 编辑器相机(Scene view 恒用,与场景相机实体无关)。转发
|
||||||
|
// SceneRenderer 持有的实例:Application 驱动 Update,面板读写位姿
|
||||||
|
EditorCamera& GetEditorCamera(); // cpp 内定义(SceneRenderer 完整类型)
|
||||||
|
// 当帧场景数据(只读直通)。CPU 消费如视口拾取:反投影矩阵必须
|
||||||
|
// 与渲染同一台相机同一投影(含 [0,1] 深度语义),禁止自行重算
|
||||||
|
const FrameSceneData& GetFrameSceneData() const; // cpp 内定义
|
||||||
|
// Editor 视口的期望渲染尺寸(像素;UI 面板每帧上报,帧首统一生效)
|
||||||
|
void SetViewportSize(u32 w, u32 h)
|
||||||
|
{
|
||||||
|
m_DesiredViewport = vk::Extent2D{w, h};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Editor 视口拾取(UE 式 GPU HitProxy:id pass + 点击 readback)----
|
||||||
|
// UI 段点击时调用:下一帧 HitProxy 渲染 + 请求像素拷贝(Vulkan
|
||||||
|
// NDC,y 向下,与渲染同构)
|
||||||
|
void RequestPick(const glm::vec2& ndc);
|
||||||
|
// UI 段轮询:拷贝结果就绪则取走(一次性;nullopt = 暂无结果)。
|
||||||
|
// 返回 kPickMiss = 未命中(天空/空隙),有效值 = 实体位模式。
|
||||||
|
// 内部 waitIdle 保证提交完成(点击低频,单帧 hitch 不可感知)
|
||||||
|
std::optional<u32> TakePickResult();
|
||||||
|
|
||||||
|
// ---- Editor 选中描边(UE 编辑器选中轮廓)----
|
||||||
|
// UI 段选中变化时调用;下一帧 SelectionMask 只画该实体,Outline
|
||||||
|
// 在其轮廓叠加描边色。nullopt = 清选中(实体 0 号位模式 = 0,
|
||||||
|
// 不能拿位值兼作哨兵)
|
||||||
|
void SetSelectedEntity(std::optional<u32> entityBits);
|
||||||
|
|
||||||
|
// ---- 帧截图(调试 / 无 UI 自动化验证)----
|
||||||
|
// 请求 delayFrames 帧后把 PresentTarget(tonemap+outline 后最终
|
||||||
|
// 画面)拷出存 PNG。触发帧内部 waitIdle;仅调试用途
|
||||||
|
void RequestScreenshot(const std::string& path, u32 delayFrames = 60);
|
||||||
|
|
||||||
// 垂直同步开关(present mode 切换会重建 swapchain),
|
// 垂直同步开关(present mode 切换会重建 swapchain),
|
||||||
// 只应在帧 UI 构建段调用(Render 之前)
|
// 只应在帧 UI 构建段调用(Render 之前)
|
||||||
void SetVSync(bool vsync);
|
void SetVSync(bool vsync) { m_DesiredVSync = vsync; } // 帧首同步生效
|
||||||
|
|
||||||
|
// 帧首同步:窗口 resize / 视口尺寸 / 显示模式 / vsync 意图统一在
|
||||||
|
// UI 构建之前落定(waitIdle → 重编译 → 重绑描述符)。必须先于
|
||||||
|
// UI 构建段调用:本帧 draw data 引用的描述符与物理资源由此保证
|
||||||
|
// 全程新鲜(resize 中途换描述符会绑出已销毁句柄)
|
||||||
|
void BeginFrameSync();
|
||||||
private:
|
private:
|
||||||
void BuildGraph(); // 声明 RG 资源 + 注册 pass 对象
|
void BuildGraph(); // 声明 RG 资源 + 注册 pass 对象
|
||||||
void HandleResize();
|
void HandleResize(bool recreateSwapchain);
|
||||||
void ReadGpuTimes(u32 frameSlot);
|
void ReadGpuTimes(u32 frameSlot);
|
||||||
|
void RefreshSceneTextures(); // 物理 view → ImGui 描述符(重建后刷新)
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
|
void CollectBakeResults(u32 frameSlot); // 槽位 fence 后取走回读结果
|
||||||
|
#endif
|
||||||
|
// 期望渲染分辨率:Editor 视口 = 面板尺寸(未上报时回退 swapchain);
|
||||||
|
// Direct = swapchain 尺寸
|
||||||
|
vk::Extent2D DesiredRenderExtent() const;
|
||||||
|
|
||||||
Window& m_Window;
|
Window& m_Window;
|
||||||
|
|
||||||
@@ -130,13 +241,31 @@ namespace Engine {
|
|||||||
Scope<UniformBuffer> m_UniformBuffer; // per-slot UBO ring
|
Scope<UniformBuffer> m_UniformBuffer; // per-slot UBO ring
|
||||||
Scope<UniformBuffer> m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO)
|
Scope<UniformBuffer> m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO)
|
||||||
Scope<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity)
|
Scope<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity)
|
||||||
|
Scope<UniformBuffer> m_SkinOffsetBuffer; // 实例→palette 基址 ring(SSBO,与实例序平行)
|
||||||
|
Scope<UniformBuffer> m_SkinPaletteBuffer; // 骨骼蒙皮矩阵 palette ring(SSBO,帧首 bump)
|
||||||
|
Scope<UniformBuffer> m_EntityIdBuffer; // 实例→实体位模式 ring(SSBO,HitProxy 拾取)
|
||||||
Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
|
Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
|
||||||
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
|
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
|
||||||
|
// 物理调试图元 ring VB(eVertexBuffer;per-slot fence 语义复用
|
||||||
|
// UniformBuffer——PhysicsDebugPass::PrepareFrame 上传,pass 内绑定)
|
||||||
|
Scope<UniformBuffer> m_PhysicsLineVB;
|
||||||
|
Scope<UniformBuffer> m_PhysicsTriVB;
|
||||||
|
Scope<UniformBuffer> m_DebugLineVB; // 通用调试源(Engine::Debug 服务)
|
||||||
|
// Editor gizmo 图元 ring VB(editor 专属,game 构建不分配):
|
||||||
|
// 图标 billboard / 视锥线框 / 拾取代理壳三流——代理壳由
|
||||||
|
// HitProxyPass 经 AttachGizmoProxy 消费(同 VB 换 shader 重画)
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
|
Scope<UniformBuffer> m_GizmoIconVB;
|
||||||
|
Scope<UniformBuffer> m_GizmoWireVB;
|
||||||
|
Scope<UniformBuffer> m_GizmoProxyVB;
|
||||||
|
#endif
|
||||||
// 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造
|
// 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造
|
||||||
Scope<IblGenerator> m_Ibl;
|
Scope<IblGenerator> m_Ibl;
|
||||||
// 中性兜底环境(IBL 四件套):Application 未加载/加载失败时保底;
|
// 中性兜底环境(IBL 四件套):Application 未加载/加载失败时保底;
|
||||||
// 声明在 m_Passes 之前 → 析构晚于 pass(引用安全)
|
// 声明在 m_Passes 之前 → 析构晚于 pass(引用安全)
|
||||||
Scope<Cubemap> m_Environment;
|
Scope<Cubemap> m_Environment;
|
||||||
|
// 当前绑定环境的资产路径(帧数据 envPath 差量同步基准;空 = 兜底)
|
||||||
|
std::string m_ActiveEnvPath;
|
||||||
Scope<Swapchain> m_Swapchain;
|
Scope<Swapchain> m_Swapchain;
|
||||||
Scope<SyncManager> m_SyncManager;
|
Scope<SyncManager> m_SyncManager;
|
||||||
Scope<CommandManager> m_CommandManager;
|
Scope<CommandManager> m_CommandManager;
|
||||||
@@ -149,14 +278,21 @@ namespace Engine {
|
|||||||
|
|
||||||
// ---- RenderGraph 资源句柄 ----
|
// ---- RenderGraph 资源句柄 ----
|
||||||
RG::TextureHandle m_Backbuffer{};
|
RG::TextureHandle m_Backbuffer{};
|
||||||
|
RG::TextureHandle m_PresentTarget{}; // tonemap 输出(offscreen,ImGui 采样)
|
||||||
RG::TextureHandle m_Depth{};
|
RG::TextureHandle m_Depth{};
|
||||||
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps{}; // CSM 级联
|
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps{}; // CSM 级联
|
||||||
RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一
|
RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一
|
||||||
|
RG::TextureHandle m_SceneColor{}; // 不透明段快照(透射折射采样源)
|
||||||
RG::TextureHandle m_Velocity{}; // Forward 输出(clip 空间运动向量)
|
RG::TextureHandle m_Velocity{}; // Forward 输出(clip 空间运动向量)
|
||||||
|
RG::TextureHandle m_HitProxyId{}; // 拾取 id 目标(R32Uint,片元写实体位模式)
|
||||||
|
RG::TextureHandle m_SelectionMask{}; // 选中描边 mask(R8Unorm,选中像素=1)
|
||||||
RG::TextureHandle m_TAAColor{}; // TAA 累积输出(自引用历史),后处理源
|
RG::TextureHandle m_TAAColor{}; // TAA 累积输出(自引用历史),后处理源
|
||||||
RG::TextureHandle m_BloomA{}; // 半分辨率:提取 + 垂直模糊结果
|
RG::TextureHandle m_BloomA{}; // 半分辨率:提取 + 垂直模糊结果
|
||||||
RG::TextureHandle m_BloomB{}; // 半分辨率:水平模糊中间结果
|
RG::TextureHandle m_BloomB{}; // 半分辨率:水平模糊中间结果
|
||||||
RG::TextureHandle m_TemporalDiff{}; // 半分辨率:帧历史差异(调试)
|
RG::TextureHandle m_TemporalDiff{}; // 半分辨率:帧历史差异(调试)
|
||||||
|
RG::TextureHandle m_PreviewHDR{}; // 材质球 atlas(线性 HDR,固定 1024²)
|
||||||
|
RG::TextureHandle m_PreviewVelocity{}; // 材质球 velocity 附件(forward.frag 双输出契约,无人读)
|
||||||
|
RG::TextureHandle m_PreviewSDR{}; // 材质球 atlas(tonemap 输出,ImGui 采样)
|
||||||
|
|
||||||
// pass 共享的可调参数(Renderer 持有,App 层访问器转发)
|
// pass 共享的可调参数(Renderer 持有,App 层访问器转发)
|
||||||
Scope<Passes::PostProcessParams> m_PostParams;
|
Scope<Passes::PostProcessParams> m_PostParams;
|
||||||
@@ -164,6 +300,49 @@ namespace Engine {
|
|||||||
// 可选叠加层(不拥有;Application 持有)
|
// 可选叠加层(不拥有;Application 持有)
|
||||||
ImGuiLayer* m_ImGuiLayer = nullptr;
|
ImGuiLayer* m_ImGuiLayer = nullptr;
|
||||||
|
|
||||||
|
// 物理调试(非拥有指针:pass 在 m_Passes 内,数据源由 Application 注入)
|
||||||
|
Passes::PhysicsDebugPass* m_PhysicsDebugPass = nullptr;
|
||||||
|
const PhysicsDebugFrame* m_PhysicsDebug = nullptr;
|
||||||
|
// HitProxy 拾取(非拥有指针:pass 在 m_Passes 内)
|
||||||
|
Passes::HitProxyPass* m_HitProxyPass = nullptr;
|
||||||
|
// 材质球预览(editor 专属;HELLO_EDITOR 裁剪,game 构建零预览
|
||||||
|
// 代码路径——pass 不创建、atlas 不创建、请求池不存在)
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
|
// 非拥有指针:pass 在 m_Passes 内,构造时持有 m_PreviewRenders 引用
|
||||||
|
Passes::MaterialPreviewPass* m_PreviewPass = nullptr;
|
||||||
|
// tile 池:UI 段 Request 标脏(收集活指针),Render 段
|
||||||
|
// TakeRenders 取预算内的脏 tile 换入 m_PreviewRenders(pass 只读)
|
||||||
|
Passes::PreviewTilePool m_PreviewPool;
|
||||||
|
std::vector<Passes::PreviewDirtyTile> m_PreviewRenders;
|
||||||
|
// 上一轮注册到 ImGui 的预览 atlas 物理 view:重编译后若镜像真的
|
||||||
|
// 换新(固定尺寸资源跨视口 resize 会保留同一镜像)才需让池重画,
|
||||||
|
// 否则预览球内容存续、不闪烁
|
||||||
|
vk::ImageView m_PreviewViews[kMaxFramesInFlight]{};
|
||||||
|
|
||||||
|
// ---- 材质球缩略图烘焙(GPU→CPU 异步回读)----
|
||||||
|
struct PreviewBakeReq
|
||||||
|
{
|
||||||
|
u64 key = 0; // 调用方令牌(App 侧 path → key)
|
||||||
|
u64 serial = 0; // Material serial(去重/失效判据)
|
||||||
|
Material* material = nullptr; // 仅 !copied 期间被解引用
|
||||||
|
i32 frameTile = -1; // 本帧 RequestBake 返回的槽位
|
||||||
|
bool copied = false; // 拷贝已录制,等待回读
|
||||||
|
u64 lastRequestFrame = 0; // UI keepalive 帧号(老化丢弃判据)
|
||||||
|
};
|
||||||
|
std::vector<PreviewBakeReq> m_BakeReqs; // 跨帧存活至回读完成
|
||||||
|
u64 m_FrameIndex = 0; // 单调帧号(请求老化判据)
|
||||||
|
std::vector<Passes::PreviewBakeTile> m_PreviewBakes; // 当帧(pass 只读)
|
||||||
|
// 每槽位当帧录制的回读 key(帧 N 录,帧 N+2 同槽位 fence 后取走)
|
||||||
|
std::array<std::vector<u64>, kMaxFramesInFlight> m_BakeRecorded;
|
||||||
|
struct PreviewBakeResult
|
||||||
|
{
|
||||||
|
u64 key = 0;
|
||||||
|
std::vector<u8> pixels;
|
||||||
|
u32 width = 0;
|
||||||
|
u32 height = 0;
|
||||||
|
};
|
||||||
|
std::vector<PreviewBakeResult> m_BakeDone;
|
||||||
|
#endif
|
||||||
// GPU timestamp(pass 级耗时,ImGui 可读)
|
// GPU timestamp(pass 级耗时,ImGui 可读)
|
||||||
vk::QueryPool m_QueryPool;
|
vk::QueryPool m_QueryPool;
|
||||||
u32 m_QueriesPerFrame = 0; // 每帧槽位的查询数 = pass 数 × 2
|
u32 m_QueriesPerFrame = 0; // 每帧槽位的查询数 = pass 数 × 2
|
||||||
@@ -172,8 +351,24 @@ namespace Engine {
|
|||||||
std::vector<f32> m_PassGpuMs; // 最近一帧各 pass 耗时
|
std::vector<f32> m_PassGpuMs; // 最近一帧各 pass 耗时
|
||||||
bool m_TimesPrimed[2] = {false, false}; // 每槽位首帧跳过读取
|
bool m_TimesPrimed[2] = {false, false}; // 每槽位首帧跳过读取
|
||||||
|
|
||||||
|
// Tracy GPU 时间线(pass zone;TRACY_ENABLE 关闭时空实现)
|
||||||
|
Scope<TracyGpuProfiler> m_TracyProfiler;
|
||||||
|
|
||||||
vk::Extent2D m_Extent{};
|
vk::Extent2D m_Extent{};
|
||||||
u32 m_CurrentFrame = 0;
|
u32 m_CurrentFrame = 0;
|
||||||
|
|
||||||
|
// ---- 帧首同步的意图缓冲(UI 段写入,下一帧帧首落定)----
|
||||||
|
int m_DesiredDisplayMode = kDisplayDirect; // 默认直接渲染, 视口渲染需要手动指定一下
|
||||||
|
vk::Extent2D m_DesiredViewport{}; // Editor 视口渲染分辨率(面板上报)
|
||||||
|
|
||||||
|
// 截图意图:framesLeft 递减到 0 的那一帧末尾拷贝 PresentTarget 存 PNG
|
||||||
|
struct ScreenshotRequest
|
||||||
|
{
|
||||||
|
std::string path;
|
||||||
|
u32 framesLeft = 0;
|
||||||
|
};
|
||||||
|
std::optional<ScreenshotRequest> m_PendingShot;
|
||||||
|
bool m_DesiredVSync = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -21,9 +21,17 @@
|
|||||||
#define HELLOVULKAN_SCENERENDERER_H
|
#define HELLOVULKAN_SCENERENDERER_H
|
||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
|
#include <Engine/Renderer/Mesh.h>
|
||||||
#include <Engine/Renderer/SceneTypes.h>
|
#include <Engine/Renderer/SceneTypes.h>
|
||||||
|
#include <Engine/Animation/AnimationSystem.h>
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
#include <array>
|
||||||
|
#include <optional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#include <Engine/Renderer/EditorCamera.h>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
@@ -32,10 +40,12 @@ namespace Engine {
|
|||||||
|
|
||||||
class SceneRenderer {
|
class SceneRenderer {
|
||||||
public:
|
public:
|
||||||
// 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。
|
// 从场景构建当帧渲染数据。游戏形态无 primary 相机返回 false
|
||||||
|
//(渲染跳帧;编辑器形态恒用编辑器相机,不跳)。
|
||||||
// targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。
|
// targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。
|
||||||
|
// dt 驱动骨骼动画采样推进(crossfade 计时)。
|
||||||
bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
|
bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
|
||||||
vk::Extent2D targetExtent);
|
vk::Extent2D targetExtent, f32 dt);
|
||||||
|
|
||||||
FrameSceneData& Frame() { return m_Frame; }
|
FrameSceneData& Frame() { return m_Frame; }
|
||||||
|
|
||||||
@@ -55,46 +65,138 @@ namespace Engine {
|
|||||||
void SetShadowsEnabled(bool b) { m_ShadowsEnabled = b; }
|
void SetShadowsEnabled(bool b) { m_ShadowsEnabled = b; }
|
||||||
void SetTAAEnabled(bool b) { m_TAAEnabled = b; }
|
void SetTAAEnabled(bool b) { m_TAAEnabled = b; }
|
||||||
void SetBloomEnabled(bool b) { m_BloomEnabled = b; }
|
void SetBloomEnabled(bool b) { m_BloomEnabled = b; }
|
||||||
|
void SetLodEnabled(bool b) { m_LodEnabled = b; }
|
||||||
void SetDebugView(int v) { m_DebugView = v; }
|
void SetDebugView(int v) { m_DebugView = v; }
|
||||||
|
void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport
|
||||||
|
// PIE 内嵌播放(UE Play-In-Editor 语义):viewport 模式下也用
|
||||||
|
// primary 相机渲染进 sceneTex(Play 后 Scene view 显示游戏画面,
|
||||||
|
// 编辑器 UI 保持可用)。kDisplayDirect 恒为游戏形态不受此影响
|
||||||
|
void SetGameView(bool g) { m_GameView = g; }
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
|
// ---- 骨骼可视化设置(Editor 面板 → Renderer 门面 → 落帧)----
|
||||||
|
void SetBoneWiresVisible(bool b) { m_ShowBoneWires = b; }
|
||||||
|
void SetBoneAllNodes(bool b) { m_BoneAllNodes = b; }
|
||||||
|
void SetBoneJointPx(f32 px) { m_BoneJointPx = px; }
|
||||||
|
bool GetBoneWiresVisible() const { return m_ShowBoneWires; }
|
||||||
|
bool GetBoneAllNodes() const { return m_BoneAllNodes; }
|
||||||
|
f32 GetBoneJointPx() const { return m_BoneJointPx; }
|
||||||
|
#endif
|
||||||
|
// 编辑器相机(视口 UI 状态,SceneRenderer 持有 = 渲染源在此分派);
|
||||||
|
// Application 驱动 Update,ViewportPanel/Inspector 经 Renderer 门面访问
|
||||||
|
EditorCamera& GetEditorCamera() { return m_EditorCamera; }
|
||||||
|
// Editor 视口拾取请求(UI 段写入,下一次 BuildFrame 落帧后清除)
|
||||||
|
void SetPickRequest(const glm::vec2& ndc)
|
||||||
|
{
|
||||||
|
m_PendingPick = PickRequest{true, ndc};
|
||||||
|
}
|
||||||
|
// Editor 选中实体(描边):UI 段写入,BuildFrame 落帧并筛出绘制。
|
||||||
|
// nullopt = 清选中(实体 0 号位模式 = 0,不能拿位值兼作哨兵)
|
||||||
|
void SetSelectedEntity(std::optional<u32> entityBits)
|
||||||
|
{
|
||||||
|
m_PendingSelected = entityBits;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
|
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
|
||||||
//(含纹素对齐防阴影边缘闪烁)。写入(Shadow pass push)与采样
|
//(含纹素对齐防阴影边缘闪烁)。写入(Shadow pass push)与采样
|
||||||
//(Forward UBO)共用同一组矩阵。
|
//(Forward UBO)共用同一组矩阵。camNear/camFar 取自当帧渲染源
|
||||||
void ComputeCascades(const glm::vec3& shadowDir);
|
// 相机(每实体参数)
|
||||||
|
void ComputeCascades(const glm::vec3& shadowDir,
|
||||||
|
f32 camNear, f32 camFar);
|
||||||
|
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
|
// Editor gizmo 可视化收集(billboard 图标 / 相机视锥线框 /
|
||||||
|
// HitProxy 代理壳)——只读场景写 FrameSceneData editor 段。
|
||||||
|
// camPos/right/up 供 billboard 面向展开;aspect 供视锥角点;
|
||||||
|
// viewHeightPx 供代理 ribbon 恒定像素屏宽换算;viewTanHalf 为
|
||||||
|
// 编辑器视角相机的 tan(fov/2)(billboard 尺寸基准)
|
||||||
|
void CollectEditorGizmos(ECS::Scene& scene,
|
||||||
|
const glm::vec3& camPos,
|
||||||
|
const glm::vec3& camRight,
|
||||||
|
const glm::vec3& camUp,
|
||||||
|
f32 aspect, f32 viewHeightPx,
|
||||||
|
f32 viewTanHalf);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// MDI 打包:batch 按组键排序(megabuffer + 可选材质)后压成
|
||||||
|
// IndirectDrawCmd 连续数组 + IndirectGroup 区间表。
|
||||||
|
// byMaterial=true(Forward):每材质一次 bind + 一次 MDI;
|
||||||
|
// byMaterial=false(Shadow):只按 megabuffer 分组(depth-only)
|
||||||
|
static void PackIndirect(CullResult& view, bool byMaterial);
|
||||||
|
|
||||||
FrameSceneData m_Frame;
|
FrameSceneData m_Frame;
|
||||||
|
EditorCamera m_EditorCamera; // 编辑器相机(视口 UI 状态,见 GetEditorCamera)
|
||||||
|
PickRequest m_PendingPick{}; // 拾取请求缓冲(BuildFrame 落帧后清除)
|
||||||
|
std::optional<u32> m_PendingSelected; // 选中实体缓冲(nullopt = 无选中)
|
||||||
|
|
||||||
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
|
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
|
||||||
struct ViewScratch {
|
struct ViewScratch {
|
||||||
|
// cur + prev 合并写:两次随机堆访问 → 一次(cache miss 减半)
|
||||||
|
struct InstanceMatrices {
|
||||||
|
glm::mat4 cur;
|
||||||
|
glm::mat4 prev;
|
||||||
|
u32 skinBase = 0; // palette 区段基址(0 = identity 槽)
|
||||||
|
u32 entityBits = 0; // 实体位模式(HitProxy id pass 消费)
|
||||||
|
};
|
||||||
|
// 链表尾哨兵:kMaxInstances 远小于 u32 最大值,安全
|
||||||
|
static constexpr u32 kNoInstance = 0xFFFFFFFFu;
|
||||||
CullResult result;
|
CullResult result;
|
||||||
// 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 =
|
// 扁平实例数组(所有 batch 的实例共用一份连续内存)。
|
||||||
// firstInstance + 连续区间,实体序收集不满足(多实体共享 mesh
|
// 实体收集序下同 batch 实例会散布(多实体共享 mesh 交错插入),
|
||||||
// 时实例分散各处),扁平化成连续数组推迟到 appendView
|
// 组内连接关系由 instanceNext 链表维护——appendView 时按 batch
|
||||||
std::vector<std::vector<glm::mat4>> batchWorlds;
|
// 聚合拷成连续区间(firstInstance 连续语义的前提)。大场景下
|
||||||
std::vector<std::vector<glm::mat4>> batchPrevWorlds;
|
// 避免 vector<vector> 的小块分配,每帧 clear() 复用容量
|
||||||
std::unordered_map<Mesh*, u32> lookup;
|
std::vector<InstanceMatrices> instances;
|
||||||
|
std::vector<u32> instanceNext; // 与 instances 平行:同 batch 下一实例
|
||||||
|
// 每 batch 实例链头(与 result.batches 平行;kNoInstance = 空)
|
||||||
|
std::vector<u32> batchHead;
|
||||||
|
// 每 batch 实例视图深度累加(透明排序用:平均深度降序 = 远→近;
|
||||||
|
// 透明队列逐实例成批故 count=1,等价逐实例深度)
|
||||||
|
std::vector<f32> batchDistSum;
|
||||||
|
// batch 查找:数组寻址(下标 = Mesh::GetSlot(),值 = 每 LOD 的
|
||||||
|
// batchIdx+1,0 = 未用)——逐实例 hash 在大场景是毫秒级开销
|
||||||
|
std::vector<std::array<u32, kMeshLodCount>> lookup;
|
||||||
|
// 覆盖材质的 batch 查找(内建材质走 lookup 数组快路径):
|
||||||
|
// key = meshSlot<<32 | matVariant(matVariant = 覆盖材质的
|
||||||
|
// 帧内序号,指针仅帧内有效),value = 每 LOD 的 batchIdx+1
|
||||||
|
std::unordered_map<u64, std::array<u32, kMeshLodCount>> ovrLookup;
|
||||||
|
// 帧内覆盖材质 → 序号分配表(指针仅帧内有效——材质资产
|
||||||
|
// Reload 会在帧间换代,禁止跨帧缓存)
|
||||||
|
std::unordered_map<Material*, u32> matVariants;
|
||||||
|
|
||||||
void Clear()
|
void Clear()
|
||||||
{
|
{
|
||||||
result.batches.clear();
|
result.batches.clear();
|
||||||
result.instanceCount = 0;
|
result.instanceCount = 0;
|
||||||
batchWorlds.clear();
|
instances.clear(); // 容量保留,复用
|
||||||
batchPrevWorlds.clear();
|
instanceNext.clear();
|
||||||
lookup.clear();
|
batchHead.clear();
|
||||||
|
batchDistSum.clear();
|
||||||
|
for (auto& e : lookup) e.fill(0); // 容量保留,置零复用
|
||||||
|
ovrLookup.clear();
|
||||||
|
matVariants.clear();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ViewScratch m_MainScratch;
|
ViewScratch m_MainScratch;
|
||||||
ViewScratch m_CascScratch[kMaxCascades];
|
ViewScratch m_CascScratch[kMaxCascades];
|
||||||
|
ViewScratch m_TranslucentScratch; // 透明材质(仅主视图,不投阴影)
|
||||||
|
|
||||||
// ---- TAA 时域状态 ----
|
// ---- TAA 时域状态 ----
|
||||||
u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样)
|
u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样)
|
||||||
glm::mat4 m_LastView{1.0f}; // 上一帧视图(velocity)
|
glm::mat4 m_LastView{1.0f}; // 上一帧视图(velocity)
|
||||||
glm::mat4 m_LastProjNoJitter{1.0f}; // 上一帧未抖动投影(velocity)
|
glm::mat4 m_LastProjNoJitter{1.0f}; // 上一帧未抖动投影(velocity 用)
|
||||||
// 实体的上一帧世界矩阵(key = entt 实体 integral)。
|
// 实体的上一帧世界矩阵(key = entt 实体 integral)。
|
||||||
// 每帧重建 m_CurWorlds 后与 m_PrevWorlds swap——自动清理销毁实体
|
// 用 sorted vector + binary_search 替代 unordered_map:
|
||||||
std::unordered_map<u32, glm::mat4> m_PrevWorlds;
|
// unordered_map 在每帧大量 emplace 时 rehash 抖动明显,
|
||||||
std::unordered_map<u32, glm::mat4> m_CurWorlds;
|
// vector 连续内存 + 单次 sort 对 cache 友好且无 rehash
|
||||||
|
std::vector<std::pair<u32, glm::mat4>> m_PrevWorlds;
|
||||||
|
std::vector<std::pair<u32, glm::mat4>> m_CurWorlds;
|
||||||
|
|
||||||
|
// ---- 骨骼动画(调度收口 Animation::AnimationSystem,本类只
|
||||||
|
// 转发调用 + 收集段只读查 frame.entitySkinBases)----
|
||||||
|
Animation::AnimationSystem m_AnimSystem;
|
||||||
|
// 材质资产加载缺失的一次性告警节流(按路径;发现即插表)
|
||||||
|
std::unordered_set<std::string> m_WarnedMaterialMisses;
|
||||||
|
|
||||||
bool m_TruncationWarned = false; // 实例缓冲打满只告警一次
|
bool m_TruncationWarned = false; // 实例缓冲打满只告警一次
|
||||||
|
|
||||||
@@ -102,7 +204,24 @@ namespace Engine {
|
|||||||
bool m_ShadowsEnabled = true;
|
bool m_ShadowsEnabled = true;
|
||||||
bool m_TAAEnabled = true;
|
bool m_TAAEnabled = true;
|
||||||
bool m_BloomEnabled = true;
|
bool m_BloomEnabled = true;
|
||||||
|
bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过)
|
||||||
int m_DebugView = 0;
|
int m_DebugView = 0;
|
||||||
|
// 引擎默认 = 游戏直渲形态(与 Renderer::m_DesiredDisplayMode 的
|
||||||
|
// 默认一致)。编辑器形态在 App OnInit 显式切 Viewport——若这里
|
||||||
|
// 默认 Viewport,game 形态没人调 SetDisplayMode:Blit 恒空转 +
|
||||||
|
// ImGuiPass 清屏 → 纯黑屏(game 形态黑屏的根因)
|
||||||
|
int m_DisplayMode = kDisplayDirect;
|
||||||
|
bool m_GameView = false; // PIE 内嵌播放(Editor Play/Stop 驱动)
|
||||||
|
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
|
bool m_GizmoTruncWarned = false; // gizmo 顶点预算溢出只告警一次
|
||||||
|
// 骨骼可视化设置(唯一持有者,BuildFrame 落帧)。默认关闭:
|
||||||
|
// 骨架是调试可视化(Inspector Animation 节手动打开),常显
|
||||||
|
// 会污染编辑视图
|
||||||
|
bool m_ShowBoneWires = false;
|
||||||
|
bool m_BoneAllNodes = false;
|
||||||
|
f32 m_BoneJointPx = 5.0f;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -13,23 +13,66 @@
|
|||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
class Mesh;
|
class Mesh;
|
||||||
|
class Material;
|
||||||
|
|
||||||
// GPUDriven:按 mesh 聚合的绘制批
|
// GPUDriven:按 (mesh, 材质, LOD) 聚合的绘制批——同 mesh 不同 LOD
|
||||||
|
// 的实例索引区间不同不能并成一次 draw;同 mesh 不同材质(实体级
|
||||||
|
// 覆盖)描述符集不同,同样必须分批
|
||||||
struct InstanceBatch {
|
struct InstanceBatch {
|
||||||
Mesh* mesh;
|
Mesh* mesh;
|
||||||
|
Material* material = nullptr; // 收集期 resolve 的实际材质
|
||||||
|
//(覆盖材质或 mesh 内建;shadow 组不绑材质但同表记录)
|
||||||
u32 first; // 在实例矩阵数组中的起始下标
|
u32 first; // 在实例矩阵数组中的起始下标
|
||||||
u32 count; // 实例数
|
u32 count; // 实例数
|
||||||
|
u32 lod = 0; // 绘制用索引区间(Mesh::Draw)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 一个渲染视图的剔除结果(相机视图 / 光源视图各一份)
|
// MDI:一次 vkCmdDrawIndexedIndirectCount 消费的命令(CPU 侧镜像,
|
||||||
|
// 内存布局 = VkDrawIndexedIndirectCommand,20B)
|
||||||
|
struct IndirectDrawCmd {
|
||||||
|
u32 indexCount;
|
||||||
|
u32 instanceCount;
|
||||||
|
u32 firstIndex; // megabuffer 里的全局索引下标
|
||||||
|
u32 vertexOffset; // megabuffer 里的顶点元素偏移
|
||||||
|
u32 firstInstance; // 实例矩阵数组下标
|
||||||
|
};
|
||||||
|
static_assert(sizeof(IndirectDrawCmd) == 20, "布局必须镜像 VkDrawIndexedIndirectCommand");
|
||||||
|
|
||||||
|
// MDI:共享同一 megabuffer(同 Model)的一组命令——
|
||||||
|
// 一次 bindVertex/IndexBuffer +(Forward)一次材质 Set1 绑定 +
|
||||||
|
// 一次 drawIndexedIndirectCount。组内命令在 indirect buffer 中连续
|
||||||
|
struct IndirectGroup {
|
||||||
|
const Mesh* mesh = nullptr; // 取共享 VB/IB 句柄(组内同一份)
|
||||||
|
Material* material = nullptr; // shadow 组为 null(depth-only 无材质)
|
||||||
|
u32 firstCommand = 0; // 命令区起始下标(count 同下标)
|
||||||
|
u32 commandCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// indirect buffer 的视图分区:[main | cascade0 | cascade1 | cascade2],
|
||||||
|
// 每区 kMaxInstances 条命令预算(每实例至多 1 条,必不越界)
|
||||||
|
constexpr u32 kIndirectViewMain = 0;
|
||||||
|
|
||||||
|
// 一个渲染视图的剔除结果(相机视图 / 光源视图各一份)。
|
||||||
|
// batches = 逐实例批次(透明队列仍逐批 draw);indirect = MDI 打包产物
|
||||||
struct CullResult {
|
struct CullResult {
|
||||||
std::vector<InstanceBatch> batches;
|
std::vector<InstanceBatch> batches;
|
||||||
u32 instanceCount = 0;
|
u32 instanceCount = 0;
|
||||||
|
std::vector<IndirectDrawCmd> indirectCommands;
|
||||||
|
std::vector<IndirectGroup> indirectGroups;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选中描边:单实例绘制指令(SelectionMask pass 逐条 draw)。
|
||||||
|
// instance = 该实例在实例矩阵数组中的绝对下标
|
||||||
|
struct SelectionDraw {
|
||||||
|
Mesh* mesh;
|
||||||
|
u32 instance;
|
||||||
|
u32 lod;
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr u32 kMaxLights = 4;
|
constexpr u32 kMaxLights = 4;
|
||||||
@@ -41,6 +84,65 @@ namespace Engine {
|
|||||||
constexpr u32 kMaxCascades = 3; // CSM 级联数
|
constexpr u32 kMaxCascades = 3; // CSM 级联数
|
||||||
constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长
|
constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长
|
||||||
|
|
||||||
|
// 蒙皮矩阵预算(palette 总槽位数/帧):每动画实体一段(按实体分配,
|
||||||
|
// 不按模型去重——同模型两实体可不同播放状态)。4096×64B = 256KB/槽位
|
||||||
|
constexpr u32 kMaxSkinMatrices = 4096;
|
||||||
|
// palette slot 0 固定 identity:无骨骼顶点(weights=(1,0,0,0), idx=0)
|
||||||
|
// 直通它 → 静态模型蒙皮零影响
|
||||||
|
constexpr u32 kIdentityPaletteSlot = 0;
|
||||||
|
|
||||||
|
// ---- Editor 视口拾取(UE 式 GPU HitProxy)----
|
||||||
|
// R32Uint id 目标的清屏色 = 未命中(天空/空隙);恰为 entt::null
|
||||||
|
// 位模式,永不与有效实体冲突
|
||||||
|
inline constexpr u32 kPickMiss = 0xFFFFFFFFu;
|
||||||
|
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
|
// ---- Editor gizmo 可视化契约(SceneRenderer 生产,EditorGizmoPass /
|
||||||
|
// HitProxyPass 消费;game 构建整段不存在)----
|
||||||
|
// 顶点预算:图标 ≈680 个(4096/6),线框覆盖 ~200 相机视锥 + 半径圆
|
||||||
|
inline constexpr u32 kMaxGizmoIconVerts = 4096;
|
||||||
|
inline constexpr u32 kMaxGizmoWireVerts = 16384;
|
||||||
|
inline constexpr u32 kMaxGizmoProxyVerts = 32768;
|
||||||
|
|
||||||
|
// billboard/拾取代理 quad 顶点(48B):pos 已由收集期面向相机展开,
|
||||||
|
// aux.x = 形状 id(shader SDF 分支),aux.yz = quad 本地 uv(-1..1)
|
||||||
|
struct GizmoQuadVertex {
|
||||||
|
glm::vec3 pos;
|
||||||
|
glm::vec4 color; // rgb 线性 HDR 主色,a = 不透明度
|
||||||
|
glm::vec4 aux;
|
||||||
|
u32 entityId; // HitProxy 位模式(拾取 shader 顶点直读)
|
||||||
|
};
|
||||||
|
static_assert(sizeof(GizmoQuadVertex) == 48);
|
||||||
|
|
||||||
|
// 线框顶点(28B):布局与 debug_line.vert 输入同构 → 复用其 shader
|
||||||
|
struct GizmoWireVertex {
|
||||||
|
glm::vec3 pos;
|
||||||
|
glm::vec4 color;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(GizmoWireVertex) == 28);
|
||||||
|
|
||||||
|
// 选中高亮色(与 OutlinePass 描边同款,UE 语义统一)。gizmo 收集
|
||||||
|
// 与 Animation 骨骼可视化共用一份定义
|
||||||
|
inline const glm::vec4 kGizmoSelectedColor{1.0f, 0.62f, 0.05f, 1.0f};
|
||||||
|
// 非骨骼节点的暗色(骨骼线亮橙 / 普通节点线暗灰,层级一眼分层;
|
||||||
|
// LineList 无混合,alpha 无效,只能实色区分)
|
||||||
|
inline const glm::vec4 kGizmoNodeDimColor{0.42f, 0.42f, 0.46f, 1.0f};
|
||||||
|
|
||||||
|
// 骨骼关节标记(AnimationSystem 产出,ViewportPanel 投影到屏幕画
|
||||||
|
// 圆点 + tooltip 骨骼名——Endfield-Poser overlay 范式)
|
||||||
|
struct BoneJointMark {
|
||||||
|
glm::vec3 pos; // 关节世界坐标
|
||||||
|
u32 nodeIdx; // 查 Model::GetNodes()[nodeIdx].name 用
|
||||||
|
u8 isBone; // 1 = 骨骼(bone 表成员),0 = 普通节点
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 点击拾取请求(UI 段写入 → SceneRenderer 落帧 → HitProxy pass 只读)
|
||||||
|
struct PickRequest {
|
||||||
|
bool active = false;
|
||||||
|
glm::vec2 ndc{0.0f}; // 点击点 NDC(Vulkan y 向下,与渲染同构)
|
||||||
|
};
|
||||||
|
|
||||||
// 镜像 shader 侧 UBO 布局的灯光项(std140,每灯 64B)。
|
// 镜像 shader 侧 UBO 布局的灯光项(std140,每灯 64B)。
|
||||||
// 字段未用部分置零——shader 按类型分支读取
|
// 字段未用部分置零——shader 按类型分支读取
|
||||||
struct LightData {
|
struct LightData {
|
||||||
@@ -62,44 +164,131 @@ namespace Engine {
|
|||||||
LightData lights[kMaxLights];
|
LightData lights[kMaxLights];
|
||||||
|
|
||||||
// ---- TAA(velocity 在 vertex 里算)----
|
// ---- TAA(velocity 在 vertex 里算)----
|
||||||
glm::mat4 projNoJitter; // 未抖动投影(velocity 当前帧 clip 用)
|
glm::mat4 projNoJitter; // 未抖动投影(天空盒反投影/折射同位采样/静态预览)
|
||||||
glm::mat4 prevView; // 上一帧视图(velocity)
|
glm::mat4 prevView; // 上一帧视图(velocity)
|
||||||
glm::mat4 prevProj; // 上一帧未抖动投影(velocity)
|
glm::mat4 prevProj; // 上一帧"已抖动"投影(velocity 历史 UV 对齐)
|
||||||
glm::vec4 cameraPos; // 相机世界位置(高光 V 向量)
|
glm::vec4 cameraPos; // 相机世界位置(高光 V 向量)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 显示模式(帧契约 displayMode 字段;ImGui 经 Renderer 门面写入):
|
||||||
|
// Direct = 全屏直出:Blit pass 把 PresentTarget 拷进 backbuffer,
|
||||||
|
// ImGui 叠加其上(游戏形态)
|
||||||
|
// Viewport = Editor 视口:backbuffer 只画 ImGui(清屏),场景经
|
||||||
|
// ImGui::Image 呈现;渲染分辨率跟随 ImGui 面板尺寸
|
||||||
|
inline constexpr int kDisplayDirect = 0;
|
||||||
|
inline constexpr int kDisplayViewport = 1;
|
||||||
|
|
||||||
// 每帧从场景收集的渲染数据(SceneRenderer 生产,pass 消费)。
|
// 每帧从场景收集的渲染数据(SceneRenderer 生产,pass 消费)。
|
||||||
// 复用成员容量:clear() 不缩容,稳态零每帧堆分配。
|
// 复用成员容量:clear() 不缩容,稳态零每帧堆分配。
|
||||||
struct FrameSceneData {
|
struct FrameSceneData {
|
||||||
UniformBufferObject ubo{};
|
UniformBufferObject ubo{};
|
||||||
|
|
||||||
glm::vec4 ClearColor{0.1, 0.1, 0.1, 1.0};
|
// 相机裁剪面 + 投影模式(TAA 去遮挡检测用:把 [0,1] 深度线性化为
|
||||||
|
// 视图空间距离再比较。透视深度非线性,需 n·f/(f−d(f−n));正交
|
||||||
|
// 深度本身线性,用 cameraOrtho 分支)
|
||||||
|
f32 cameraNearZ = 0.1f;
|
||||||
|
f32 cameraFarZ = 10000.0f;
|
||||||
|
bool cameraOrtho = false;
|
||||||
|
|
||||||
|
glm::vec4 ClearColor{0.0, 0.0, 0.0, 1.0};
|
||||||
bool DrawSkybox = true;
|
bool DrawSkybox = true;
|
||||||
|
// 本帧生效的环境贴图资产路径(来自第一个 active 的
|
||||||
|
// SkyboxComponent;空 = 无环境 → Renderer 回中性兜底 cubemap)。
|
||||||
|
// Renderer 据此与当前已绑定环境做差量同步(路径变化才重载)
|
||||||
|
std::string envPath;
|
||||||
|
|
||||||
// 天空盒反投影矩阵:inverse(projNoJitter × mat4(mat3(view)))
|
// 天空盒反投影矩阵:inverse(projNoJitter × mat4(mat3(view)))
|
||||||
//(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push)
|
//(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push)
|
||||||
glm::mat4 skyInvVP{1.0f};
|
glm::mat4 skyInvVP{1.0f};
|
||||||
|
|
||||||
|
// 天空盒 velocity 用:上一帧未抖动 proj × 上一帧视图(去平移)。
|
||||||
|
// 天空是"无限远"表面,只受相机旋转影响:frag 把当前世界方向过
|
||||||
|
// 此矩阵得上一帧屏幕位置,差即天空的运动向量(相机移动时天空/
|
||||||
|
// 背景不再闪烁的根治点——此前天空从不写 velocity = 恒 0)
|
||||||
|
glm::mat4 skyPrevVP{1.0f};
|
||||||
|
|
||||||
// 实例矩阵数组 = [主视图段 | 阴影视图段](共享同一 per-slot SSBO)
|
// 实例矩阵数组 = [主视图段 | 阴影视图段](共享同一 per-slot SSBO)
|
||||||
std::vector<glm::mat4> instanceModels;
|
std::vector<glm::mat4> instanceModels;
|
||||||
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
|
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
|
||||||
// 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵
|
// 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵
|
||||||
std::vector<glm::mat4> prevInstanceModels;
|
std::vector<glm::mat4> prevInstanceModels;
|
||||||
|
|
||||||
|
// ---- 蒙皮(与 instanceModels 平行的 per-instance 间接索引)----
|
||||||
|
// skinOffsets[i] = 第 i 个实例的 palette 区段起点(kMaxSkinMatrices
|
||||||
|
// 全局数组内下标);无动画实体指向 slot0 identity
|
||||||
|
std::vector<u32> skinOffsets;
|
||||||
|
// palette 区段池:帧首 bump allocator 按动画实体分配
|
||||||
|
// [skinBase .. skinBase+boneCount) = 该实体全部骨骼的蒙皮矩阵
|
||||||
|
std::vector<glm::mat4> skinPalettes;
|
||||||
|
// 实体(entt integral)→ 该实体 palette 区段基址。AnimationSystem
|
||||||
|
// 预采样填,收集段查表(查不到 = 无动画/组件禁用 → identity 槽
|
||||||
|
// = 绑定姿势)。渲染侧对动画零逻辑,只消费查表结果
|
||||||
|
std::unordered_map<u32, u32> entitySkinBases;
|
||||||
|
|
||||||
|
// per-instance 实体 id(与 instanceModels 平行对齐;
|
||||||
|
// HitProxy id pass 按 gl_InstanceIndex 取,片元写入 R32Uint 目标)
|
||||||
|
std::vector<u32> instanceEntityIds;
|
||||||
|
|
||||||
|
// 本帧拾取请求(UI 段点击,SceneRenderer 落帧后自动清除;
|
||||||
|
// HitProxy/HitProxyCopy 两 pass 据此决定是否绘制/拷贝)
|
||||||
|
PickRequest pickRequest{};
|
||||||
|
|
||||||
|
// 选中描边(UE 编辑器语义):选中实体 + 位模式。位模式不能兼作
|
||||||
|
// "无选中"哨兵——entt 首个实体 0 号的 to_integral == 0("点击
|
||||||
|
// 场景第一个创建的实体没有描边"的根因),有效性与位值解耦。
|
||||||
|
// BuildFrame 据此从实例表筛出 selectionDraws,SelectionMask pass
|
||||||
|
// 只画选中实体 → Outline pass 在 mask 边缘叠加描边色
|
||||||
|
bool selectedEntityValid = false;
|
||||||
|
u32 selectedEntityBits = 0;
|
||||||
|
std::vector<SelectionDraw> selectionDraws;
|
||||||
|
|
||||||
CullResult mainView; // 相机视锥 → Forward pass
|
CullResult mainView; // 相机视锥 → Forward pass
|
||||||
// 每级联独立剔除结果(Shadow pass 按 region 各画各的——
|
// 每级联独立剔除结果(Shadow pass 按 region 各画各的——
|
||||||
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
|
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
|
||||||
CullResult shadowCascades[kMaxCascades];
|
CullResult shadowCascades[kMaxCascades];
|
||||||
|
// 透明批次(预乘 alpha 混合,深度只测不写,远→近排序);
|
||||||
|
// 不参与阴影投射
|
||||||
|
CullResult translucentView;
|
||||||
|
|
||||||
// ---- 运行时开关/调试视图(ImGui 经 Renderer 门面写入,
|
// ---- 运行时开关/调试视图(ImGui 经 Renderer 门面写入,
|
||||||
// SceneRenderer 唯一落帧,各 pass 只读;每帧确定性)----
|
// SceneRenderer 唯一落帧,各 pass 只读;每帧确定性)----
|
||||||
bool shadowsEnabled = true; // ShadowPass 空转 + frag ShadowFactor 短路
|
bool shadowsEnabled = true; // ShadowPass 空转 + frag ShadowFactor 短路
|
||||||
bool taaEnabled = true; // TAAPass 直通(alpha=0) + 停投影抖动
|
bool taaEnabled = true; // TAAPass 直通(alpha=0) + 停投影抖动
|
||||||
bool bloomEnabled = true; // BloomPass 子 pass 空转 + tonemap 强度归零
|
bool bloomEnabled = true; // BloomPass 子 pass 空转 + tonemap 强度归零
|
||||||
|
bool lodEnabled = true; // 关 = 实例收集恒选 LOD0(绘制原网格)
|
||||||
int debugView = 0; // Passes::DebugView 枚举(tonemap 路由上屏)
|
int debugView = 0; // Passes::DebugView 枚举(tonemap 路由上屏)
|
||||||
|
int displayMode = kDisplayViewport; // 显示模式(Blit 路由 + ImGui loadOp)
|
||||||
|
|
||||||
// 实例预算溢出(kMaxInstances):本轮收集有实例被静默丢弃——
|
// 实例预算溢出(kMaxInstances):本轮收集有实例被静默丢弃——
|
||||||
// 必须遥测可见,"只告警一次"的大场景截断等于没有告警
|
// 必须遥测可见,"只告警一次"的大场景截断等于没有告警
|
||||||
bool instancesTruncated = false;
|
bool instancesTruncated = false;
|
||||||
|
// palette 预算溢出(kMaxSkinMatrices):动画实体 palette 被截断
|
||||||
|
bool skinPaletteTruncated = false;
|
||||||
|
|
||||||
|
#ifdef HELLO_EDITOR
|
||||||
|
// ---- Editor gizmo 可视化(收集门控:kDisplayViewport 且非 PIE
|
||||||
|
// 时非空;每帧 clear() 复用容量,稳态零堆分配)----
|
||||||
|
std::vector<GizmoQuadVertex> gizmoIconVerts; // 视觉 billboard(三角表)
|
||||||
|
std::vector<GizmoWireVertex> gizmoWireVerts; // 线段对(LineList)
|
||||||
|
// 选中实体骨骼线框(AnimationSystem 产出,EditorGizmoPass 与
|
||||||
|
// gizmoWireVerts 拼同一 LineList 绘制)。动画实体跟随当帧姿态,
|
||||||
|
// 非动画/禁用实体 = 绑定姿势(与渲染 identity palette 一致)
|
||||||
|
std::vector<GizmoWireVertex> boneWireVerts;
|
||||||
|
std::vector<GizmoQuadVertex> gizmoProxyVerts; // HitProxy 代理壳
|
||||||
|
|
||||||
|
// ---- 骨骼可视化设置(ImGui 经 Renderer 门面写入)----
|
||||||
|
bool showBoneWires = true; // 总开关(3D 骨架线 + 关节圆点)
|
||||||
|
bool pieActive = false; // PIE 运行态(Play/Stop 驱动):骨骼
|
||||||
|
// 线/关节圆点全部停产出——PIE 画面
|
||||||
|
// 是 primary 相机渲染的"游戏",编辑器
|
||||||
|
// 可视化叠上去必然错位
|
||||||
|
bool boneAllNodes = false; // true = 全节点层级(普通节点暗色);
|
||||||
|
// false = 仅骨骼链(bone 表成员)
|
||||||
|
f32 boneJointPx = 5.0f; // 关节圆点半径(屏幕像素,overlay 画)
|
||||||
|
// 关节标记(EmitSelectedSkeleton 产出,每帧重建;ViewportPanel
|
||||||
|
// 用编辑器相机投影画 overlay 圆点)
|
||||||
|
std::vector<BoneJointMark> boneJoints;
|
||||||
|
#endif
|
||||||
|
|
||||||
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
|
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// Created by atdunbg on 2026/9/24.
|
||||||
|
//
|
||||||
|
// TracyGpuProfiler:RG::IGpuProfiler 的 Tracy Vulkan 实现。
|
||||||
|
//
|
||||||
|
// 每个 RG pass 在 GPU 时间线里是一个 zone(TracyVkZoneTransient,运行时
|
||||||
|
// 名字),时间戳在命令录制期写入当帧 cmd,TracyVkCollect 在帧尾解析。
|
||||||
|
// TRACY_ENABLE 关闭时退化为空实现(零开销)。
|
||||||
|
//
|
||||||
|
// 时间戳未做 CPU/GPU 校准(TracyVkContext 非 calibrated 变体):
|
||||||
|
// Tracy 按收集批次对齐,长时间运行的漂移可后续升级 Calibrated 变体。
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef HELLOVULKAN_TRACYGPUPROFILER_H
|
||||||
|
#define HELLOVULKAN_TRACYGPUPROFILER_H
|
||||||
|
|
||||||
|
#include <Engine/RenderGraph/RenderGraph.h>
|
||||||
|
|
||||||
|
namespace Engine {
|
||||||
|
|
||||||
|
class Device;
|
||||||
|
|
||||||
|
class TracyGpuProfiler final : public RG::RenderGraph::IGpuProfiler {
|
||||||
|
public:
|
||||||
|
explicit TracyGpuProfiler(Device& device);
|
||||||
|
~TracyGpuProfiler() override;
|
||||||
|
|
||||||
|
TracyGpuProfiler(const TracyGpuProfiler&) = delete;
|
||||||
|
TracyGpuProfiler& operator=(const TracyGpuProfiler&) = delete;
|
||||||
|
|
||||||
|
[[nodiscard]] Scope<IScope> BeginPass(const char* name,
|
||||||
|
vk::CommandBuffer cmd) override;
|
||||||
|
void EndFrame(vk::CommandBuffer cmd) override;
|
||||||
|
private:
|
||||||
|
class ScopeImpl;
|
||||||
|
void* m_Ctx{}; // tracy::VkCtx*(TracyVulkan.hpp 只在实现内 include)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Engine
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_TRACYGPUPROFILER_H
|
||||||
@@ -16,6 +16,14 @@ namespace Engine {
|
|||||||
glm::vec2 uv;
|
glm::vec2 uv;
|
||||||
glm::vec3 tangent; // 切线(法线贴图 TBN 用;assimp CalcTangentSpace)
|
glm::vec3 tangent; // 切线(法线贴图 TBN 用;assimp CalcTangentSpace)
|
||||||
|
|
||||||
|
// ---- 骨骼蒙皮属性 ----
|
||||||
|
// 无骨骼资产保持默认值(weights=(1,0,0,0) + palette[0]=identity)
|
||||||
|
// → skin 矩阵 = identity,蒙皮路径对静态模型零影响。
|
||||||
|
// weights 必须带默认初始化器:聚合初始化 Vertex{pos,normal,uv,tangent}
|
||||||
|
// 会把未列出的成员零初始化 → skin=mat4(0) → 顶点全部塌缩到原点
|
||||||
|
glm::uvec4 boneIds{0, 0, 0, 0};
|
||||||
|
glm::vec4 weights{1.0f, 0.0f, 0.0f, 0.0f};
|
||||||
|
|
||||||
static vk::VertexInputBindingDescription2EXT GetBindingDescription() {
|
static vk::VertexInputBindingDescription2EXT GetBindingDescription() {
|
||||||
vk::VertexInputBindingDescription2EXT binding;
|
vk::VertexInputBindingDescription2EXT binding;
|
||||||
binding.setBinding(0)
|
binding.setBinding(0)
|
||||||
@@ -25,8 +33,8 @@ namespace Engine {
|
|||||||
return binding;
|
return binding;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::array<vk::VertexInputAttributeDescription2EXT, 4> GetAttributeDescriptions() {
|
static std::array<vk::VertexInputAttributeDescription2EXT, 6> GetAttributeDescriptions() {
|
||||||
std::array<vk::VertexInputAttributeDescription2EXT, 4> attrs;
|
std::array<vk::VertexInputAttributeDescription2EXT, 6> attrs;
|
||||||
|
|
||||||
attrs[0].setLocation(0)
|
attrs[0].setLocation(0)
|
||||||
.setBinding(0)
|
.setBinding(0)
|
||||||
@@ -48,6 +56,16 @@ namespace Engine {
|
|||||||
.setFormat(vk::Format::eR32G32B32Sfloat)
|
.setFormat(vk::Format::eR32G32B32Sfloat)
|
||||||
.setOffset(offsetof(Vertex, tangent));
|
.setOffset(offsetof(Vertex, tangent));
|
||||||
|
|
||||||
|
attrs[4].setLocation(4)
|
||||||
|
.setBinding(0)
|
||||||
|
.setFormat(vk::Format::eR32G32B32A32Uint)
|
||||||
|
.setOffset(offsetof(Vertex, boneIds));
|
||||||
|
|
||||||
|
attrs[5].setLocation(5)
|
||||||
|
.setBinding(0)
|
||||||
|
.setFormat(vk::Format::eR32G32B32A32Sfloat)
|
||||||
|
.setOffset(offsetof(Vertex, weights));
|
||||||
|
|
||||||
return attrs;
|
return attrs;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user