Compare commits
63
Commits
74a2044441
...
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 |
+10
-2
@@ -1,12 +1,20 @@
|
||||
# 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/*.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)
|
||||
|
||||
add_executable(HelloVulkan main.cpp ${EDITOR_SOURCES} ${SCRIPT_SOURCES})
|
||||
# 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)
|
||||
|
||||
|
||||
@@ -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
|
||||
+310
-61
@@ -1,19 +1,27 @@
|
||||
//
|
||||
// Editor.cpp:docking 骨架实现。
|
||||
//
|
||||
// 布局方案(首帧 DockBuilder 一次,之后 imgui.ini 接管):
|
||||
// ┌──────────────────────────────┬───────────────┐
|
||||
// │ Hierarchy │ │
|
||||
// ├──────────────────────────────┤ Inspector │
|
||||
// │ Stats │ (28%) │
|
||||
// └──────────────────────────────┴───────────────┘
|
||||
// 默认布局 = UE5 出厂布局(首帧 DockBuilder 一次,之后 imgui.ini 接管):
|
||||
// ┌──────────┬──────────────────────┬───────────┐
|
||||
// │ Outliner │ │ │
|
||||
// │ │ Viewport │ Details │
|
||||
// │ │ (+Stats tab) │ │
|
||||
// ├──────────┴──────────────────────┤ │
|
||||
// │ Content Browser(左+中通栏) │ │
|
||||
// └─────────────────────────────────┴───────────┘
|
||||
//
|
||||
|
||||
#include "Editor.h"
|
||||
#include "HierarchyPanel.h"
|
||||
#include "InspectorPanel.h"
|
||||
#include "ViewportPanel.h"
|
||||
#include "StatsPanel.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>
|
||||
@@ -22,9 +30,8 @@
|
||||
#include <imgui_internal.h> // DockBuilder*(内部 API)
|
||||
#include <ImGuizmo.h>
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h)
|
||||
|
||||
@@ -35,15 +42,158 @@ 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 → 杆可见但永远抓不住(只能看不能拖)
|
||||
@@ -67,12 +217,22 @@ void Editor::Draw()
|
||||
ImGui::PopStyleVar(3);
|
||||
|
||||
DrawMenuBar();
|
||||
DrawToolbar();
|
||||
DrawDockSpace();
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// 常规面板窗口(窗口名 = docking 身份)
|
||||
if (m_ShowHierarchy) m_Hierarchy->Draw(m_Ctx, m_Selected);
|
||||
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)
|
||||
{
|
||||
@@ -82,6 +242,11 @@ void Editor::Draw()
|
||||
}
|
||||
}
|
||||
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();
|
||||
@@ -96,6 +261,11 @@ 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)
|
||||
@@ -132,6 +302,9 @@ void Editor::DrawMenuBar()
|
||||
|
||||
if (ImGui::BeginMenu("File"))
|
||||
{
|
||||
// 运行态禁存取:Save 会把"玩出来的状态"当编辑结果落盘,
|
||||
// Load 会撕掉快照恢复链(UE 运行态同样禁)
|
||||
ImGui::BeginDisabled(IsPlaying());
|
||||
if (ImGui::MenuItem("Save Scene...", nullptr))
|
||||
{
|
||||
m_ShowSaveModal = true;
|
||||
@@ -140,6 +313,7 @@ void Editor::DrawMenuBar()
|
||||
{
|
||||
m_ShowLoadModal = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Quit", "Alt+F4"))
|
||||
{
|
||||
@@ -150,16 +324,120 @@ void Editor::DrawMenuBar()
|
||||
|
||||
if (ImGui::BeginMenu("View"))
|
||||
{
|
||||
ImGui::MenuItem("Hierarchy", nullptr, &m_ShowHierarchy);
|
||||
ImGui::MenuItem("Inspector", nullptr, &m_ShowInspector);
|
||||
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");
|
||||
@@ -174,18 +452,24 @@ void Editor::DrawDockSpace()
|
||||
ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace);
|
||||
ImGui::DockBuilderSetNodeSize(dockspaceId, ImGui::GetMainViewport()->Size);
|
||||
|
||||
ImGuiID dockLeft = 0, dockRight = 0, dockCenter = 0, dockStats = 0;
|
||||
ImGui::DockBuilderSplitNode(dockspaceId, ImGuiDir_Left, 0.20f,
|
||||
&dockLeft, &dockCenter);
|
||||
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Right, 0.28f,
|
||||
// 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(dockLeft, ImGuiDir_Down, 0.38f,
|
||||
&dockStats, &dockLeft);
|
||||
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Down, 0.30f,
|
||||
&dockBottom, &dockCenter);
|
||||
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Left, 0.22f,
|
||||
&dockLeft, &dockCenter);
|
||||
|
||||
ImGui::DockBuilderDockWindow("Hierarchy", dockLeft);
|
||||
ImGui::DockBuilderDockWindow("Stats", dockStats);
|
||||
ImGui::DockBuilderDockWindow("Inspector", dockRight);
|
||||
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);
|
||||
}
|
||||
@@ -303,16 +587,8 @@ void Editor::DoSaveScene()
|
||||
std::filesystem::create_directories(
|
||||
std::filesystem::path(path).parent_path(), ec);
|
||||
|
||||
// 环境路径:引擎序列化器只认字符串,查表由编辑器侧负责
|
||||
std::string envPath;
|
||||
if (m_Ctx.getEnvIndex)
|
||||
{
|
||||
const int idx = m_Ctx.getEnvIndex();
|
||||
if (idx >= 0 && idx < kEnvCount) envPath = kEnvPaths[idx];
|
||||
}
|
||||
|
||||
if (Serialization::SceneSerializer::Serialize(*m_Ctx.scene, *m_Ctx.assets,
|
||||
path, envPath))
|
||||
path))
|
||||
{
|
||||
ENGINE_INFO("Editor: scene saved to '{}'", path);
|
||||
}
|
||||
@@ -330,10 +606,8 @@ void Editor::DoLoadScene()
|
||||
return;
|
||||
}
|
||||
|
||||
std::string envPath;
|
||||
if (!Serialization::SceneSerializer::Deserialize(*m_Ctx.scene,
|
||||
*m_Ctx.assets, path,
|
||||
envPath))
|
||||
*m_Ctx.assets, path))
|
||||
{
|
||||
ENGINE_ERROR("Editor: failed to load scene '{}'", path);
|
||||
return;
|
||||
@@ -342,29 +616,4 @@ void Editor::DoLoadScene()
|
||||
|
||||
// 旧实体已销毁:清选中(连带清描边,见 SelectEntity)
|
||||
SelectEntity(entt::null);
|
||||
|
||||
// 环境恢复:场景带环境且命中环境表才切换(镜像 StatsPanel 同步路径)
|
||||
if (!envPath.empty() && m_Ctx.getEnvIndex && m_Ctx.setEnvIndex)
|
||||
{
|
||||
int found = -1;
|
||||
for (int i = 0; i < kEnvCount; ++i)
|
||||
{
|
||||
if (envPath == kEnvPaths[i]) { found = i; break; }
|
||||
}
|
||||
if (found >= 0)
|
||||
{
|
||||
m_Ctx.setEnvIndex(found);
|
||||
const auto env = m_Ctx.assets->LoadEnvironment(kEnvPaths[found]);
|
||||
if (env.Valid())
|
||||
{
|
||||
m_Ctx.renderer->SetEnvironment(
|
||||
m_Ctx.assets->GetEnvironment(env));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ENGINE_WARN("Editor: scene env '{}' not in env table, "
|
||||
"keeping current", envPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+63
-8
@@ -6,26 +6,34 @@
|
||||
// - 实体选中状态(entt::entity,Hierarchy 写 / Inspector 读)
|
||||
// - 退出确认模态框(窗口关闭 → RequestQuit → Yes 时 Close)
|
||||
//
|
||||
// 面板窗口名(Hierarchy/Inspector/Viewport/Stats)即 docking 身份,
|
||||
// imgui.ini 持久化布局;默认布局只在 dock node 为空时构建一次。
|
||||
// 面板窗口名(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 "ViewportPanel.h" // ViewportInputState(成员按值持有)
|
||||
#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:
|
||||
@@ -54,23 +62,57 @@ public:
|
||||
// 请求退出(弹确认框;窗口关闭事件走这里)
|
||||
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 只画选中实体
|
||||
//(bits = 实体位模式,0 = 清选中)
|
||||
// 同步描边: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
|
||||
? 0u
|
||||
: static_cast<Engine::u32>(entt::to_integral(e)));
|
||||
? 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 窗口内顶栏
|
||||
void DrawMenuBar(); // host 窗口内顶栏(File/View 菜单)
|
||||
void DrawToolbar(); // 菜单栏下方主工具栏(Play/Pause/Stop 居中)
|
||||
void DrawDockSpace(); // 全屏 host + dockspace + 默认布局
|
||||
void DrawQuitModal(); // 退出确认模态框
|
||||
void DrawSceneModals(); // 保存/加载路径模态框(共用一个路径输入)
|
||||
@@ -86,6 +128,11 @@ private:
|
||||
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};
|
||||
|
||||
@@ -94,6 +141,9 @@ private:
|
||||
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;
|
||||
@@ -106,6 +156,11 @@ private:
|
||||
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
|
||||
+18
-16
@@ -9,27 +9,18 @@
|
||||
#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 <functional>
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
// 可切换环境(IBL 动态切换):StatsPanel 的 Combo 与 SandboxApp 的
|
||||
// 加载逻辑共用这一张表(下标即身份)。
|
||||
inline constexpr const char *kEnvPaths[] = {
|
||||
"assets/textures/venice_dawn_1_4k.hdr",
|
||||
"assets/textures/rooitou_park_4k.hdr",
|
||||
"assets/textures/pink_sunrise_4k.hdr",
|
||||
"assets/textures/birchwood_4k.hdr",
|
||||
};
|
||||
inline constexpr const char *kEnvNames[] = {
|
||||
"Venice Dawn", "Rooitou Park", "Pink Sunrise", "Birchwood",
|
||||
};
|
||||
inline constexpr int kEnvCount = 4;
|
||||
inline constexpr int kDefaultEnv = 0;
|
||||
class MaterialEditorPanel; // 面板间联动通道(Details 双击槽位 / Content
|
||||
// Browser 双击 .material → 打开材质编辑器)
|
||||
class ControllerEditorPanel; // Details Controller 槽 Edit → 打开控制器编辑器
|
||||
|
||||
// 沙盒特有状态(面板可选读取,空指针 = 该面板区块不显示)
|
||||
struct EditorContext {
|
||||
@@ -38,12 +29,23 @@ struct EditorContext {
|
||||
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;
|
||||
|
||||
std::function<int()> getEnvIndex; // 当前环境表下标
|
||||
std::function<void(int)> setEnvIndex;
|
||||
// PIE 运行态(Play-In-Editor):Editor 持有,只读指针。
|
||||
// 面板门控用——运行态禁 Gizmo/编辑器热键,渲染源已是 primary 相机
|
||||
const bool *playState = nullptr;
|
||||
// PIE 暂停态(timeScale=0):Editor 持有,只读指针(视口 PAUSED 提示)
|
||||
const bool *pausedState = nullptr;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_EDITORCONTEXT_H
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
//
|
||||
// HierarchyPanel.cpp:实体列表实现。
|
||||
//
|
||||
|
||||
#include "HierarchyPanel.h"
|
||||
#include "Engine/ECS/Components.h"
|
||||
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
using namespace Engine;
|
||||
|
||||
namespace {
|
||||
struct ScopedID {
|
||||
explicit ScopedID(const u64 uuid) {
|
||||
ImGui::PushID(reinterpret_cast<const void*>(uuid));
|
||||
}
|
||||
~ScopedID() {ImGui::PopID(); }
|
||||
};
|
||||
}
|
||||
|
||||
void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
|
||||
{
|
||||
ImGui::Begin("Hierarchy");
|
||||
|
||||
// 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构)
|
||||
if (ImGui::Button("+ Create Entity"))
|
||||
{
|
||||
const auto e = ctx.scene->CreateEntity("Entity");
|
||||
selected = e;
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
|
||||
reg.view<Engine::ECS::NameComponent>().each(
|
||||
[&](const entt::entity e, const Engine::ECS::NameComponent& name)
|
||||
{
|
||||
const bool isSelected = (selected == e);
|
||||
|
||||
u64 rowId = static_cast < uint64_t >( static_cast < uint32_t >(e));
|
||||
if (const auto * id = reg.try_get<Engine::ECS::IdComponent>(e)) {
|
||||
rowId = id->uuid.Value();
|
||||
}
|
||||
const ScopedID scope (rowId);
|
||||
|
||||
const std::string label = name.name.empty() ? "(unnamed)" : name.name;
|
||||
if (ImGui::Selectable(label.c_str(), isSelected))
|
||||
{
|
||||
selected = e;
|
||||
}
|
||||
|
||||
// 右键菜单:删除(延迟销毁,帧尾统一 flush)
|
||||
if (ImGui::BeginPopupContextItem("entity_ctx"))
|
||||
{
|
||||
if (ImGui::MenuItem("Delete"))
|
||||
{
|
||||
ctx.scene->DestroyEntity(e);
|
||||
if (selected == e)
|
||||
{
|
||||
selected = entt::null;
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// 组件徽标 + 悬停清单
|
||||
std::string badge;
|
||||
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e)) badge += "[M]";
|
||||
if (reg.all_of<Engine::ECS::AnimationComponent>(e)) badge += "[A]";
|
||||
if (reg.all_of<Engine::ECS::CameraComponent>(e)) badge += "[C]";
|
||||
if (reg.all_of<Engine::ECS::LightComponent>(e)) badge += "[L]";
|
||||
if (reg.all_of<Engine::ECS::RigidBodyComponent>(e)) badge += "[P]";
|
||||
if (!badge.empty())
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s", badge.c_str());
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::SetTooltip(
|
||||
"M=Mesh A=Animation C=Camera L=Light P=Physics");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
//
|
||||
// HierarchyPanel:实体列表面板。
|
||||
//
|
||||
// 点击选中(写 Editor 的选中状态)、工具栏创建实体、右键删除
|
||||
//(延迟销毁,帧尾 flush,遍历安全)。每项尾缀显示组件构成徽标。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_HIERARCHYPANEL_H
|
||||
#define HELLOVULKAN_HIERARCHYPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
class HierarchyPanel {
|
||||
public:
|
||||
void Draw(const EditorContext& ctx, entt::entity& selected);
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_HIERARCHYPANEL_H
|
||||
@@ -1,675 +0,0 @@
|
||||
//
|
||||
// InspectorPanel.cpp:组件编辑实现。
|
||||
//
|
||||
// Transform 编辑必须走 Set* 入口(标脏 → 帧循环统一 Compose 重算
|
||||
// world),直接改私有成员是编译错误——面板天然遵守引擎约束。
|
||||
//
|
||||
|
||||
#include "InspectorPanel.h"
|
||||
#include "Engine/ECS/Components.h"
|
||||
#include "Engine/Renderer/Model.h"
|
||||
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
|
||||
#include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names)
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
#include <type_traits>
|
||||
|
||||
namespace
|
||||
{
|
||||
// InputText 需要 char 缓冲;提交时写回 std::string
|
||||
bool InputTextString(const char* label, std::string& str)
|
||||
{
|
||||
char buf[256];
|
||||
std::snprintf(buf, sizeof(buf), "%s", str.c_str());
|
||||
if (ImGui::InputText(label, buf, sizeof(buf)))
|
||||
{
|
||||
str = buf;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// UE 风变换行:label + 三轴色 DragFloat(轴色对齐 Gizmo:X 红 /
|
||||
// Y 绿 / Z 蓝)。拖拽粗调;Ctrl+Click 进输入框键入精确值。
|
||||
// 任一轴变化返回 true
|
||||
bool DragVec3Row(const char* label, glm::vec3& v, float speed,
|
||||
float mn = -3.4e38f, float mx = 3.4e38f)
|
||||
{
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::SameLine(72.0f); // 固定标签列宽 → 三行输入框对齐
|
||||
ImGui::PushID(label);
|
||||
|
||||
constexpr ImVec4 axisCol[3] = {
|
||||
{0.72f, 0.24f, 0.24f, 1.00f}, // X 红
|
||||
{0.30f, 0.68f, 0.34f, 1.00f}, // Y 绿
|
||||
{0.24f, 0.40f, 0.88f, 1.00f}, // Z 蓝
|
||||
};
|
||||
const float boxW =
|
||||
(ImGui::GetContentRegionAvail().x
|
||||
- 2.0f * ImGui::GetStyle().ItemInnerSpacing.x) / 3.0f;
|
||||
|
||||
bool changed = false;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, axisCol[i]);
|
||||
ImGui::SetNextItemWidth(boxW);
|
||||
changed |= ImGui::DragFloat(
|
||||
i == 0 ? "##x" : i == 1 ? "##y" : "##z",
|
||||
&v[i], speed, mn, mx, "%.2f");
|
||||
ImGui::PopStyleColor();
|
||||
if (i < 2) ImGui::SameLine();
|
||||
}
|
||||
ImGui::PopID();
|
||||
return changed;
|
||||
}
|
||||
|
||||
// 组件区块头(Unity 语义):折叠标题 + 右缘"启用"勾选与"删除"按钮。
|
||||
// active 传 nullptr = 无启停(结构性组件);removable = 显示删除钮,
|
||||
// 点击时置 *removeFlag,调用方在区块末尾统一移除(避免边画边删)
|
||||
bool ComponentHeader(const char* label, bool* active, bool removable,
|
||||
bool* removeFlag)
|
||||
{
|
||||
ImGui::PushID(label);
|
||||
bool open;
|
||||
if (active != nullptr)
|
||||
{
|
||||
open = ImGui::CollapsingHeader(
|
||||
label, ImGuiTreeNodeFlags_DefaultOpen |
|
||||
ImGuiTreeNodeFlags_AllowOverlap);
|
||||
const float rowH = ImGui::GetItemRectSize().y;
|
||||
float x = ImGui::GetWindowWidth() - 32.0f;
|
||||
if (removable)
|
||||
{
|
||||
ImGui::SameLine(x);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1.0f);
|
||||
if (ImGui::Button("-", ImVec2(24.0f, rowH - 4.0f)))
|
||||
{
|
||||
*removeFlag = true;
|
||||
}
|
||||
x -= 30.0f;
|
||||
}
|
||||
ImGui::SameLine(x);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1.0f);
|
||||
ImGui::Checkbox("##active", active);
|
||||
}
|
||||
else
|
||||
{
|
||||
open = ImGui::CollapsingHeader(label,
|
||||
ImGuiTreeNodeFlags_DefaultOpen);
|
||||
}
|
||||
ImGui::PopID();
|
||||
return open;
|
||||
}
|
||||
}
|
||||
|
||||
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
{
|
||||
ImGui::Begin("Inspector");
|
||||
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
if (selected == entt::null || !reg.valid(selected))
|
||||
{
|
||||
ImGui::TextDisabled("(no entity selected)");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Name ----
|
||||
if (auto* name = reg.try_get<Engine::ECS::NameComponent>(selected))
|
||||
{
|
||||
InputTextString("Name", name->name);
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- Add Component(已挂载项置灰:Unity Add Component 语义)----
|
||||
if (ImGui::Button("Add Component"))
|
||||
{
|
||||
ImGui::OpenPopup("add_component");
|
||||
}
|
||||
if (ImGui::BeginPopup("add_component"))
|
||||
{
|
||||
auto menuItem = [&](auto typeTag, const char* label)
|
||||
{
|
||||
using T = std::remove_pointer_t<decltype(typeTag)>;
|
||||
const bool has = reg.all_of<T>(selected);
|
||||
if (has)
|
||||
{
|
||||
ImGui::BeginDisabled(true);
|
||||
}
|
||||
if (ImGui::MenuItem(label))
|
||||
{
|
||||
reg.emplace<T>(selected);
|
||||
}
|
||||
if (has)
|
||||
{
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
};
|
||||
menuItem(static_cast<Engine::ECS::LightComponent*>(nullptr), "Light");
|
||||
menuItem(static_cast<Engine::ECS::CameraComponent*>(nullptr), "Camera");
|
||||
menuItem(static_cast<Engine::ECS::AnimationComponent*>(nullptr), "Animation");
|
||||
menuItem(static_cast<Engine::ECS::NameComponent*>(nullptr), "Name");
|
||||
menuItem(static_cast<Engine::ECS::RigidBodyComponent*>(nullptr), "RigidBody");
|
||||
menuItem(static_cast<Engine::ECS::JointComponent*>(nullptr), "Joint");
|
||||
menuItem(static_cast<Engine::ECS::CharacterControllerComponent*>(nullptr),
|
||||
"Character Controller");
|
||||
menuItem(static_cast<Engine::ECS::ScriptComponent*>(nullptr), "Script");
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- Transform(结构性组件:不可删、无启停)----
|
||||
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
|
||||
{
|
||||
if (ComponentHeader("Transform", nullptr, false, nullptr))
|
||||
{
|
||||
glm::vec3 pos = t->GetPosition();
|
||||
if (DragVec3Row("position", pos, 0.05f))
|
||||
{
|
||||
t->SetPosition(pos);
|
||||
}
|
||||
glm::vec3 rot = t->GetRotation(); // 派生欧拉(度)
|
||||
if (DragVec3Row("rotation", rot, 0.5f))
|
||||
{
|
||||
t->SetRotation(rot);
|
||||
}
|
||||
glm::vec3 scl = t->GetScale();
|
||||
if (DragVec3Row("scale", scl, 0.02f, 0.001f, 100.0f))
|
||||
{
|
||||
t->SetScale(scl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- MeshRenderer ----
|
||||
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
|
||||
{
|
||||
bool removeMr = false;
|
||||
if (ComponentHeader("MeshRenderer", &mr->active, true, &removeMr))
|
||||
{
|
||||
if (!mr->active)
|
||||
{
|
||||
ImGui::BeginDisabled(true); // 禁用 = 内容置灰(Unity 语义)
|
||||
}
|
||||
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
|
||||
model != nullptr)
|
||||
{
|
||||
const auto& clips = model->GetClips();
|
||||
ImGui::Text("model: valid (%u clips, %u bones)",
|
||||
static_cast<unsigned>(clips.size()),
|
||||
static_cast<unsigned>(model->GetBoneCount()));
|
||||
ImGui::TextDisabled("(mesh asset is read-only here)");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
||||
"model: INVALID (dangling ref)");
|
||||
}
|
||||
if (!mr->active)
|
||||
{
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
if (removeMr)
|
||||
{
|
||||
reg.remove<Engine::ECS::MeshRendererComponent>(selected);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Light ----
|
||||
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
|
||||
{
|
||||
bool removeLight = false;
|
||||
if (ComponentHeader("Light", &light->active, true, &removeLight))
|
||||
{
|
||||
if (!light->active)
|
||||
{
|
||||
ImGui::BeginDisabled(true);
|
||||
}
|
||||
int type = static_cast<int>(light->type);
|
||||
if (ImGui::Combo("type", &type, "Directional\0Point\0"))
|
||||
{
|
||||
light->type = static_cast<Engine::ECS::LightComponent::Type>(type);
|
||||
}
|
||||
ImGui::ColorEdit3("color", &light->color.x);
|
||||
ImGui::DragFloat("intensity", &light->color.a, 0.05f, 0.0f, 100.0f);
|
||||
if (light->type == Engine::ECS::LightComponent::Type::Directional)
|
||||
{
|
||||
ImGui::SliderFloat3("direction", &light->direction.x, -1.0f, 1.0f);
|
||||
ImGui::Checkbox("castShadow", &light->castShadow);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
|
||||
ImGui::TextDisabled("(position = entity Transform)");
|
||||
}
|
||||
if (!light->active)
|
||||
{
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
if (removeLight)
|
||||
{
|
||||
reg.remove<Engine::ECS::LightComponent>(selected);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Camera ----
|
||||
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
|
||||
{
|
||||
bool removeCam = false;
|
||||
if (ComponentHeader("Camera", &cam->active, true, &removeCam))
|
||||
{
|
||||
if (!cam->active)
|
||||
{
|
||||
ImGui::BeginDisabled(true);
|
||||
}
|
||||
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
|
||||
{
|
||||
// primary 互斥:启用本相机时清掉其余实体的 primary
|
||||
//(引擎取第一个 primary 作为游戏形态渲染视角)
|
||||
reg.view<Engine::ECS::CameraComponent>().each(
|
||||
[&](entt::entity e, Engine::ECS::CameraComponent& c)
|
||||
{
|
||||
if (e != selected)
|
||||
{
|
||||
c.primary = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 对齐跳转:把本实体位姿复制给编辑器相机,Scene view 立即
|
||||
// 从该位置观看(只取位姿,不接管渲染——渲染源分派在
|
||||
// SceneRenderer,编辑器形态恒用编辑器相机)
|
||||
if (ImGui::Button("Align Viewport to Camera") && ctx.editorCamera)
|
||||
{
|
||||
if (auto* tr = reg.try_get<Engine::ECS::TransformComponent>(
|
||||
selected))
|
||||
{
|
||||
ctx.editorCamera->AlignTo(*tr);
|
||||
}
|
||||
}
|
||||
if (!cam->active)
|
||||
{
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
if (removeCam)
|
||||
{
|
||||
reg.remove<Engine::ECS::CameraComponent>(selected);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- RigidBody(Jolt 刚体:动态体 Transform 由物理每帧写回)----
|
||||
// type/collider/mass 变更 → PhysicsWorld 重建 body;其余参数即时生效
|
||||
if (auto* rb = reg.try_get<Engine::ECS::RigidBodyComponent>(selected))
|
||||
{
|
||||
bool removeRb = false;
|
||||
if (ComponentHeader("RigidBody", &rb->active, true, &removeRb))
|
||||
{
|
||||
if (!rb->active)
|
||||
{
|
||||
ImGui::BeginDisabled(true);
|
||||
}
|
||||
// 冲突提示:同实体挂 CharacterController 会同时建 body 与
|
||||
// CharacterVirtual,两个"运动主体"互相打架
|
||||
if (reg.all_of<Engine::ECS::CharacterControllerComponent>(selected))
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||
"! conflicts with Character Controller "
|
||||
"(disable or remove one)");
|
||||
}
|
||||
int type = static_cast<int>(rb->type);
|
||||
if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0"))
|
||||
{
|
||||
rb->type = static_cast<Engine::ECS::RigidBodyComponent::BodyType>(type);
|
||||
}
|
||||
int collider = static_cast<int>(rb->collider);
|
||||
if (ImGui::Combo("collider", &collider,
|
||||
"Box\0Sphere\0Mesh\0Capsule\0Cylinder\0Compound\0"))
|
||||
{
|
||||
rb->collider = static_cast<Engine::ECS::RigidBodyComponent::ColliderType>(collider);
|
||||
}
|
||||
if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Box)
|
||||
{
|
||||
ImGui::DragFloat3("halfExtents", &rb->boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
|
||||
}
|
||||
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Sphere)
|
||||
{
|
||||
ImGui::DragFloat("radius", &rb->sphereRadius, 0.05f, 0.01f, 500.0f);
|
||||
}
|
||||
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Capsule)
|
||||
{
|
||||
// 半高 = 圆柱段半高(不含半球端盖),总高 = 2*(halfHeight + radius)
|
||||
ImGui::DragFloat("radius", &rb->capsuleRadius, 0.05f, 0.01f, 500.0f);
|
||||
ImGui::DragFloat("halfHeight", &rb->capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
|
||||
}
|
||||
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Cylinder)
|
||||
{
|
||||
ImGui::DragFloat("radius", &rb->cylinderRadius, 0.05f, 0.01f, 500.0f);
|
||||
ImGui::DragFloat("halfHeight", &rb->cylinderHalfHeight, 0.05f, 0.01f, 500.0f);
|
||||
}
|
||||
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Compound)
|
||||
{
|
||||
// 子形状列表:尺寸/位置变更 → PhysicsWorld 重建 body(既有模式)
|
||||
ImGui::Text("children: %d", static_cast<int>(rb->compound.size()));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("+ add"))
|
||||
{
|
||||
rb->compound.emplace_back();
|
||||
}
|
||||
int remove = -1;
|
||||
for (int i = 0; i < static_cast<int>(rb->compound.size()); ++i)
|
||||
{
|
||||
auto& c = rb->compound[i];
|
||||
ImGui::PushID(i);
|
||||
ImGui::Separator();
|
||||
int sh = static_cast<int>(c.shape);
|
||||
if (ImGui::Combo("shape", &sh, "Box\0Sphere\0Capsule\0"))
|
||||
{
|
||||
c.shape = static_cast<Engine::ECS::RigidBodyComponent::CompoundChild::Shape>(sh);
|
||||
}
|
||||
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Box)
|
||||
{
|
||||
ImGui::DragFloat3("halfExtents", &c.boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::DragFloat("radius", &c.radius, 0.05f, 0.01f, 500.0f);
|
||||
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Capsule)
|
||||
{
|
||||
ImGui::DragFloat("halfHeight", &c.capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
|
||||
}
|
||||
}
|
||||
ImGui::DragFloat3("offset", &c.position.x, 0.05f);
|
||||
if (ImGui::Button("remove"))
|
||||
{
|
||||
remove = i;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (remove >= 0)
|
||||
{
|
||||
rb->compound.erase(rb->compound.begin() + remove);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格,
|
||||
// Dynamic=凸包近似),无尺寸可调
|
||||
ImGui::TextDisabled("follows MeshRenderer geometry");
|
||||
}
|
||||
ImGui::DragFloat("mass", &rb->mass, 0.1f, 0.1f, 1000.0f, "%.1f");
|
||||
ImGui::SliderFloat("gravityScale", &rb->gravityScale, -1.0f, 5.0f);
|
||||
ImGui::SliderFloat("friction", &rb->friction, 0.0f, 2.0f);
|
||||
ImGui::SliderFloat("restitution", &rb->restitution, 0.0f, 1.0f);
|
||||
ImGui::Checkbox("isTrigger", &rb->isTrigger);
|
||||
if (rb->bodyValid &&
|
||||
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic)
|
||||
{
|
||||
ImGui::Text("vel: %.2f, %.2f, %.2f m/s",
|
||||
rb->velocity.x, rb->velocity.y, rb->velocity.z);
|
||||
|
||||
// 驱动调试(冲量瞬态不序列化):直接推一把看响应
|
||||
static glm::vec3 s_debugImpulse{0.0f, 5.0f, 0.0f};
|
||||
ImGui::DragFloat3("impulse", &s_debugImpulse.x, 0.1f);
|
||||
if (ImGui::Button("Apply Impulse"))
|
||||
{
|
||||
ctx.app->GetPhysics().AddImpulse(
|
||||
static_cast<uint32_t>(entt::to_integral(selected)),
|
||||
s_debugImpulse);
|
||||
}
|
||||
}
|
||||
if (!rb->active)
|
||||
{
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
if (removeRb)
|
||||
{
|
||||
reg.remove<Engine::ECS::RigidBodyComponent>(selected);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Joint(约束:两端 body 就绪后由 PhysicsWorld 对账创建)----
|
||||
if (auto* jc = reg.try_get<Engine::ECS::JointComponent>(selected))
|
||||
{
|
||||
bool removeJc = false;
|
||||
if (ComponentHeader("Joint", &jc->active, true, &removeJc))
|
||||
{
|
||||
if (!jc->active)
|
||||
{
|
||||
ImGui::BeginDisabled(true);
|
||||
}
|
||||
// 依赖提示:约束挂在两端刚体上,本体无 RigidBody 无法创建
|
||||
if (!reg.all_of<Engine::ECS::RigidBodyComponent>(selected))
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||
"! requires a RigidBody on this entity");
|
||||
}
|
||||
int type = static_cast<int>(jc->type);
|
||||
if (ImGui::Combo("type", &type, "Fixed\0Hinge\0Ball\0"))
|
||||
{
|
||||
jc->type = static_cast<Engine::ECS::JointComponent::Type>(type);
|
||||
}
|
||||
|
||||
// target 实体下拉:实体名列表,选中态按 UUID 匹配(变更写回)
|
||||
std::string items;
|
||||
std::vector<Engine::UUID> uuids;
|
||||
int current = -1;
|
||||
reg.view<Engine::ECS::IdComponent, Engine::ECS::NameComponent>().each(
|
||||
[&](entt::entity e, const Engine::ECS::IdComponent& id,
|
||||
const Engine::ECS::NameComponent& n)
|
||||
{
|
||||
if (e == selected)
|
||||
{
|
||||
return; // 自连无意义
|
||||
}
|
||||
if (id.uuid == jc->target)
|
||||
{
|
||||
current = static_cast<int>(uuids.size());
|
||||
}
|
||||
uuids.push_back(id.uuid);
|
||||
items.append(n.name);
|
||||
items.push_back('\0');
|
||||
});
|
||||
items.push_back('\0'); // ImGui Combo 双 \0 结尾
|
||||
if (ImGui::Combo("target", ¤t, items.c_str()) && current >= 0)
|
||||
{
|
||||
jc->target = uuids[static_cast<size_t>(current)];
|
||||
}
|
||||
|
||||
ImGui::DragFloat3("anchor A", &jc->anchorA.x, 0.05f);
|
||||
ImGui::DragFloat3("anchor B", &jc->anchorB.x, 0.05f);
|
||||
if (jc->type == Engine::ECS::JointComponent::Type::Hinge)
|
||||
{
|
||||
ImGui::DragFloat3("hinge axis", &jc->hingeAxis.x, 0.05f);
|
||||
ImGui::Checkbox("enable limit", &jc->enableLimit);
|
||||
if (jc->enableLimit)
|
||||
{
|
||||
ImGui::DragFloat("min deg", &jc->limitMin, 1.0f, -180.0f, 0.0f);
|
||||
ImGui::DragFloat("max deg", &jc->limitMax, 1.0f, 0.0f, 180.0f);
|
||||
}
|
||||
}
|
||||
if (!jc->active)
|
||||
{
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
if (removeJc)
|
||||
{
|
||||
reg.remove<Engine::ECS::JointComponent>(selected);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- CharacterController(参数编辑;状态在 PhysicsWorld 运行时)----
|
||||
if (auto* cc = reg.try_get<Engine::ECS::CharacterControllerComponent>(selected))
|
||||
{
|
||||
bool removeCc = false;
|
||||
if (ComponentHeader("Character Controller", &cc->active, true, &removeCc))
|
||||
{
|
||||
if (!cc->active)
|
||||
{
|
||||
ImGui::BeginDisabled(true);
|
||||
}
|
||||
// 冲突提示:同实体挂 RigidBody 会同时建 CharacterVirtual 与 body
|
||||
if (reg.all_of<Engine::ECS::RigidBodyComponent>(selected))
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||
"! conflicts with RigidBody "
|
||||
"(disable or remove one)");
|
||||
}
|
||||
// 实体原点 = 脚底(胶囊底端),总高 = 2*(halfHeight + radius)
|
||||
ImGui::DragFloat("radius", &cc->radius, 0.02f, 0.05f, 5.0f);
|
||||
ImGui::DragFloat("halfHeight", &cc->capsuleHalfHeight, 0.02f, 0.0f, 5.0f);
|
||||
ImGui::DragFloat("mass", &cc->mass, 1.0f, 1.0f, 500.0f, "%.0f");
|
||||
ImGui::DragFloat("max slope deg", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f);
|
||||
ImGui::DragFloat("step height", &cc->stepHeight, 0.02f, 0.0f, 2.0f);
|
||||
|
||||
// 遥测(只读):运行时状态在 PhysicsWorld 的 CharacterController
|
||||
if (auto& phy = ctx.app->GetPhysics(); phy.IsEnabled())
|
||||
{
|
||||
const uint32_t bits =
|
||||
static_cast<uint32_t>(entt::to_integral(selected));
|
||||
ImGui::Text("grounded: %s",
|
||||
phy.IsCharacterGrounded(bits) ? "yes" : "no");
|
||||
const glm::vec3 v = phy.GetCharacterVelocity(bits);
|
||||
ImGui::Text("vel: %.2f, %.2f, %.2f m/s", v.x, v.y, v.z);
|
||||
|
||||
// 跳跃调试按钮
|
||||
if (ImGui::Button("Jump"))
|
||||
{
|
||||
phy.Jump(bits, 5.0f);
|
||||
}
|
||||
}
|
||||
if (!cc->active)
|
||||
{
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
if (removeCc)
|
||||
{
|
||||
reg.remove<Engine::ECS::CharacterControllerComponent>(selected);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Script(按名解析注册表脚本;实例生命周期在 ScriptRuntime)----
|
||||
if (auto* sc = reg.try_get<Engine::ECS::ScriptComponent>(selected))
|
||||
{
|
||||
bool removeSc = false;
|
||||
if (ComponentHeader("Script", &sc->active, true, &removeSc))
|
||||
{
|
||||
if (!sc->active)
|
||||
{
|
||||
ImGui::BeginDisabled(true);
|
||||
}
|
||||
// 下拉列出全部已注册脚本(注册序);"<none>" = 清空(对账自动销毁实例)
|
||||
const auto& names = Engine::Scripting::ScriptRegistry::Names();
|
||||
std::vector<const char*> items;
|
||||
items.push_back("<none>");
|
||||
for (const auto& n : names)
|
||||
{
|
||||
items.push_back(n.c_str());
|
||||
}
|
||||
int cur = 0;
|
||||
for (size_t i = 0; i < names.size(); ++i)
|
||||
{
|
||||
if (names[i] == sc->scriptName)
|
||||
{
|
||||
cur = static_cast<int>(i) + 1;
|
||||
}
|
||||
}
|
||||
if (ImGui::Combo("script", &cur, items.data(),
|
||||
static_cast<int>(items.size())))
|
||||
{
|
||||
sc->scriptName = (cur <= 0) ? "" : names[static_cast<size_t>(cur) - 1];
|
||||
}
|
||||
if (sc->scriptName.empty())
|
||||
{
|
||||
ImGui::TextDisabled("no script assigned");
|
||||
}
|
||||
// 已填名但注册表查无此类:场景加载早于脚本注册/改名都会走到这
|
||||
else if (cur == 0)
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||
"! script not registered: %s",
|
||||
sc->scriptName.c_str());
|
||||
}
|
||||
if (!sc->active)
|
||||
{
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
if (removeSc)
|
||||
{
|
||||
reg.remove<Engine::ECS::ScriptComponent>(selected);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Animation ----
|
||||
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
|
||||
{
|
||||
bool removeAnim = false;
|
||||
if (ComponentHeader("Animation", &anim->active, true, &removeAnim))
|
||||
{
|
||||
if (!anim->active)
|
||||
{
|
||||
ImGui::BeginDisabled(true);
|
||||
}
|
||||
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
|
||||
const Engine::Model* model =
|
||||
mr ? ctx.assets->GetModel(mr->model) : nullptr;
|
||||
|
||||
// 依赖提示:动画作用在 MeshRenderer 的模型骨骼上
|
||||
if (mr == nullptr)
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||
"! requires a MeshRenderer component");
|
||||
}
|
||||
else if (model != nullptr && model->HasAnimations())
|
||||
{
|
||||
const auto& clips = model->GetClips();
|
||||
std::vector<const char*> clipNames;
|
||||
clipNames.reserve(clips.size());
|
||||
for (const auto& c : clips)
|
||||
{
|
||||
clipNames.push_back(c.name.c_str());
|
||||
}
|
||||
int cur = anim->clipIndex;
|
||||
if (ImGui::Combo("clip", &cur, clipNames.data(),
|
||||
static_cast<int>(clipNames.size())))
|
||||
{
|
||||
anim->Play(cur); // 触发 crossfade(fade 秒)
|
||||
}
|
||||
ImGui::Checkbox("playing", &anim->playing);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("loop", &anim->loop);
|
||||
ImGui::SliderFloat("speed", &anim->speed, -2.0f, 2.0f);
|
||||
ImGui::SliderFloat("fade", &anim->fadeDuration, 0.0f, 1.0f);
|
||||
if (anim->clipIndex >= 0 &&
|
||||
anim->clipIndex < static_cast<int>(clips.size()))
|
||||
{
|
||||
ImGui::Text("t: %.2f / %.2f s", anim->time,
|
||||
clips[anim->clipIndex].duration);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("(no animation clips on model)");
|
||||
}
|
||||
if (!anim->active)
|
||||
{
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
if (removeAnim)
|
||||
{
|
||||
reg.remove<Engine::ECS::AnimationComponent>(selected);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -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,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
|
||||
@@ -1,11 +1,15 @@
|
||||
//
|
||||
// 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>
|
||||
@@ -19,21 +23,30 @@ void StatsPanel::Draw(const EditorContext& ctx)
|
||||
auto& renderer = *ctx.renderer;
|
||||
auto& app = *ctx.app;
|
||||
|
||||
// ---- Camera:视图相机位姿(只读遥测;跟随渲染源分派)----
|
||||
// ---- View:看哪里(渲染目标)+ 相机位姿(跟随渲染源分派)----
|
||||
if (ImGui::CollapsingHeader("View", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
if (ctx.renderer->GetDisplayMode() == kDisplayViewport && ctx.editorCamera)
|
||||
// 渲染源分派与 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)))
|
||||
{
|
||||
const glm::vec3& pos = ctx.editorCamera->GetTransform().GetPosition();
|
||||
ImGui::Text("Editor camera pos");
|
||||
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
|
||||
renderer.SetDisplayMode(mode);
|
||||
}
|
||||
else
|
||||
if ((playing || renderer.GetDisplayMode() == kDisplayDirect))
|
||||
{
|
||||
Engine::ECS::TransformComponent* camT = nullptr;
|
||||
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
|
||||
{
|
||||
const glm::vec3& pos = camT->GetPosition();
|
||||
ImGui::Text("Game camera pos");
|
||||
// world 位姿(相机实体可能挂父层级下)
|
||||
const glm::vec3& pos = camT->GetWorldPosition();
|
||||
ImGui::Text("Game Camera Pos");
|
||||
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
|
||||
}
|
||||
else
|
||||
@@ -41,35 +54,30 @@ void StatsPanel::Draw(const EditorContext& ctx)
|
||||
ImGui::TextDisabled("(no primary camera)");
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
static const char* displayTarget[] = {
|
||||
"Fullscreen",
|
||||
"Viewport",
|
||||
};
|
||||
int mode = renderer.GetDisplayMode();
|
||||
if (ImGui::Combo("##DisplayTarget", &mode, displayTarget, IM_ARRAYSIZE(displayTarget))) {
|
||||
renderer.SetDisplayMode(mode);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- Scene:暂停模拟(引擎 Time 门面:动画/物理统一冻结)----
|
||||
else if (ctx.editorCamera)
|
||||
{
|
||||
bool paused = Time::TimeScale() == 0.0f;
|
||||
if (ImGui::Checkbox("Pause Simulation", &paused))
|
||||
{
|
||||
Time::SetTimeScale(paused ? 0.0f : 1.0f);
|
||||
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))
|
||||
if (ImGui::SliderFloat("Exposure", &exposure, 0.1f, 4.0f))
|
||||
{
|
||||
renderer.SetExposure(exposure);
|
||||
}
|
||||
@@ -88,12 +96,13 @@ void StatsPanel::Draw(const EditorContext& ctx)
|
||||
if (bloom)
|
||||
{
|
||||
f32 threshold = renderer.GetBloomThreshold();
|
||||
if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f))
|
||||
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))
|
||||
if (ImGui::SliderFloat("Strength", &strength, 0.0f, 2.0f))
|
||||
{
|
||||
renderer.SetBloomStrength(strength);
|
||||
}
|
||||
@@ -117,22 +126,6 @@ void StatsPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
renderer.SetLodEnabled(lod); // 关 = 恒绘原网格
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Environment:IBL 环境 + 调试视图(决定"看什么")----
|
||||
if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
bool& drawSkybox = ctx.scene->GetSettings().DrawSkybox;
|
||||
ImGui::Checkbox("DrawSkybox", &drawSkybox);
|
||||
if (drawSkybox && ctx.getEnvIndex)
|
||||
{
|
||||
int envIdx = ctx.getEnvIndex();
|
||||
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
|
||||
{
|
||||
ctx.setEnvIndex(envIdx);
|
||||
const auto env = ctx.assets->LoadEnvironment(kEnvPaths[envIdx]);
|
||||
ctx.renderer->SetEnvironment(ctx.assets->GetEnvironment(env));
|
||||
}
|
||||
|
||||
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
|
||||
const auto& viewNames = renderer.GetDebugViewNames();
|
||||
@@ -143,20 +136,14 @@ void StatsPanel::Draw(const EditorContext& ctx)
|
||||
renderer.SetDebugView(view);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::ColorEdit4("ClearColor",
|
||||
glm::value_ptr(ctx.scene->GetSettings().ClearColor));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Physics Debug:物理调试可视化(collider/约束/重力/速度/加速度)----
|
||||
if (ImGui::CollapsingHeader("Physics Debug"))
|
||||
// ---- Physics:调试可视化(collider/约束/重力/速度/加速度)----
|
||||
if (ImGui::CollapsingHeader("Physics"))
|
||||
{
|
||||
auto& physics = app.GetPhysics();
|
||||
u32 bits = physics.GetDebugDrawBits();
|
||||
bool master = bits != 0;
|
||||
if (ImGui::Checkbox("Enable##PhysDebug", &master))
|
||||
if (ImGui::Checkbox("Debug Draw##Phys", &master))
|
||||
{
|
||||
bits = master ? kPhyDbgDefault : 0;
|
||||
}
|
||||
@@ -182,27 +169,8 @@ void StatsPanel::Draw(const EditorContext& ctx)
|
||||
ImGui::Text("contacts (frame): %zu", physics.GetEvents().size());
|
||||
}
|
||||
|
||||
// ---- GPU Timings:只读遥测(默认折叠,不占面板高度)----
|
||||
if (ImGui::CollapsingHeader("GPU Timings"))
|
||||
{
|
||||
ImGui::Text("instances: %u main / %u shadow",
|
||||
renderer.GetMainInstanceCount(),
|
||||
renderer.GetShadowInstanceCount());
|
||||
if (renderer.GetInstancesTruncated())
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
||||
"TRUNCATED: instance buffer full!");
|
||||
}
|
||||
const auto& names = renderer.GetPassNames();
|
||||
const auto& times = renderer.GetPassGpuMs();
|
||||
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
|
||||
{
|
||||
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Frame:帧率控制与显示 ----
|
||||
if (ImGui::CollapsingHeader("Frame", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
// ---- Performance:帧率控制 + GPU 遥测(默认折叠,不占面板高度)----
|
||||
if (ImGui::CollapsingHeader("Performance"))
|
||||
{
|
||||
bool vsync = app.GetVSync();
|
||||
if (ImGui::Checkbox("VSync", &vsync))
|
||||
@@ -217,7 +185,7 @@ void StatsPanel::Draw(const EditorContext& ctx)
|
||||
if (!vsync)
|
||||
{
|
||||
f32 target = app.GetTargetFps();
|
||||
if (ImGui::SliderFloat("target fps", &target, 24.0f, 360.0f, "%.0f"))
|
||||
if (ImGui::SliderFloat("Target FPS", &target, 24.0f, 360.0f, "%.0f"))
|
||||
{
|
||||
app.SetTargetFps(target);
|
||||
}
|
||||
@@ -225,6 +193,42 @@ void StatsPanel::Draw(const EditorContext& ctx)
|
||||
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();
|
||||
@@ -69,12 +69,14 @@ namespace
|
||||
|
||||
// 视图相机分派(必须与 SceneRenderer::BuildFrame 渲染源同一规则,
|
||||
// 否则 Gizmo 杆与画面错位):编辑器形态 = EditorCamera(Scene view
|
||||
// 专用,与相机实体无关);游戏形态 = primary 相机实体。
|
||||
// 专用,与相机实体无关);游戏形态(kDisplayDirect 全屏预览或 PIE
|
||||
// 内嵌播放)= primary 相机实体。
|
||||
// 返回 nullptr = 游戏形态且无 primary 相机(本帧无 Gizmo)
|
||||
Engine::Camera* ViewCamera(const EditorContext& ctx,
|
||||
ECS::TransformComponent*& outT)
|
||||
{
|
||||
if (ctx.renderer->GetDisplayMode() == kDisplayViewport)
|
||||
const bool playing = ctx.playState && *ctx.playState;
|
||||
if (!playing && ctx.renderer->GetDisplayMode() == kDisplayViewport)
|
||||
{
|
||||
outT = &ctx.editorCamera->GetTransform();
|
||||
return &ctx.editorCamera->GetController();
|
||||
@@ -141,45 +143,138 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
else size.y = avail.x / aspect;
|
||||
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
|
||||
|
||||
// ---- 工具热键(UE 语义):W/E/R 切 Gizmo 模式、T 切
|
||||
// local/world、F 焦点选中实体。仅 Viewport 持焦时生效——
|
||||
// 输入框打字(WantTextInput)由 BlockingKeyboard 拦游戏侧,
|
||||
// 但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按住 = 相机
|
||||
// 飞行模式(键盘归相机),工具热键全部抑制——W 前进与
|
||||
// W 平移 Gizmo 的键位冲突就此解耦 ----
|
||||
// ---- 骨骼关节 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;
|
||||
if (out.focused && !ImGui::GetIO().WantTextInput &&
|
||||
// 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;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F) && tr != nullptr) {
|
||||
const bool fCombo = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
|
||||
// F = 焦点选中实体(frame-selection):编辑器相机
|
||||
// 环绕 pivot 设为选中实体位置,沿视线平移对准(环
|
||||
// 绕距离沿用当前值,滚轮可调)
|
||||
ctx.editorCamera->FocusOn(tr->GetPosition());
|
||||
// 绕距离沿用当前值,滚轮可调)。世界位姿(层级
|
||||
// 实体 = 父链组合结果)。裸 F 才触发(组合键排他)
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F) && !fCombo && tr != nullptr) {
|
||||
ctx.editorCamera->FocusOn(tr->GetWorldPosition());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
|
||||
// 选中实体 + 有 Transform 才出杆;矩阵分解直接写回
|
||||
// TransformComponent(与 Inspector 同源,下一帧数值自动跟随)
|
||||
if (tr != nullptr) {
|
||||
// 选中实体 + 有 Transform 才出杆;分解 world 矩阵经
|
||||
// SetWorldPose 写回(与 Inspector 同源,下一帧数值自动跟随)
|
||||
// PIE 运行态不出杆(编辑操作冻结)
|
||||
if (tr != nullptr && !playing) {
|
||||
// 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏
|
||||
// 形态 = primary 相机实体;与 SceneRenderer::BuildFrame
|
||||
// 同一分派),参数与渲染侧一致(aspect = 渲染分辨率、
|
||||
// near/far 0.1/100)
|
||||
// 同一分派),投影参数由相机自身承载(与渲染侧一致)
|
||||
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, 0.1f, 100.0f));
|
||||
cam->GetProjection(aspect));
|
||||
|
||||
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
|
||||
// 杆画在 Image 区域内(等比适配后的 item rect)
|
||||
@@ -259,8 +354,8 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
snapping ? snap : nullptr);
|
||||
gizmo.dragging = ImGuizmo::IsUsing();
|
||||
|
||||
// 拖拽生效:分解 world 矩阵写回 SRT(Set* 标脏 →
|
||||
// UpdateTransforms 重算 → 渲染下帧生效)。旋转用
|
||||
// 拖拽生效:分解 world 矩阵 → world SRT → 沿父链反解
|
||||
// local 写回(SetWorldPose;根实体 = 直写)。旋转用
|
||||
// decompose 出的 quat 直写(UE FTransform 语义)——
|
||||
// 不经欧拉往返,拖拽零漂移、无万向锁
|
||||
if (ImGuizmo::IsUsing()) {
|
||||
@@ -268,23 +363,34 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
glm::quat rot;
|
||||
glm::vec4 persp;
|
||||
if (glm::decompose(m, scale, rot, transl, skew, persp)) {
|
||||
tr->SetPosition(transl)
|
||||
.SetRotationQuat(rot)
|
||||
.SetScale(scale);
|
||||
ctx.scene->SetWorldPose(selected, transl, rot,
|
||||
scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gizmo 状态提示(左上角 drawlist 叠加,原因同下:不占布局行)
|
||||
// PIE 运行态 = 黄色 PLAYING/PAUSED 提示替代编辑状态
|
||||
{
|
||||
char tip[64];
|
||||
std::snprintf(tip, sizeof(tip), "Space: %s [T] Snap: Ctrl",
|
||||
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),
|
||||
IM_COL32(255, 255, 255, 150), tip);
|
||||
playing ? IM_COL32(255, 184, 46, 220)
|
||||
: IM_COL32(255, 255, 255, 150),
|
||||
tip);
|
||||
}
|
||||
|
||||
// Dynamic 刚体提示:物理每帧把姿态写回 Transform,拖拽即刻
|
||||
@@ -292,6 +398,23 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
// 用 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 &&
|
||||
@@ -306,8 +429,9 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
|
||||
// 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。
|
||||
// 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中)——否则按在
|
||||
// 操纵杆上的点击会穿透发拾取请求
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) &&
|
||||
// 操纵杆上的点击会穿透发拾取请求。PIE 运行态不出拾取:
|
||||
// 点击 = 游戏输入(UE Play 语义,编辑操作冻结)
|
||||
if (!playing && ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) &&
|
||||
!ImGuizmo::IsOver() && !ImGuizmo::IsUsing()) {
|
||||
RequestPickAt(ctx, size);
|
||||
}
|
||||
@@ -353,7 +477,7 @@ void ViewportPanel::LogPick(const EditorContext &ctx, entt::entity e) {
|
||||
? "static" : "dynamic",
|
||||
rb->boxHalfExtents.x, rb->boxHalfExtents.y,
|
||||
rb->boxHalfExtents.z, rb->sphereRadius,
|
||||
tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z);
|
||||
tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z);
|
||||
} else {
|
||||
ENGINE_INFO("[pick] '{}' (hitproxy) | (no collider)", name);
|
||||
}
|
||||
@@ -29,8 +29,8 @@ struct ViewportInputState {
|
||||
// op 取 ImGuizmo::OPERATION 位掩码(TRANSLATE=7=三轴位或运算,非 0
|
||||
// 起始枚举;0 = 无任何轴位 → Gizmo 完全不绘制),int 存储避免本
|
||||
// 头文件 include ImGuizmo.h(拖入 imgui 宏依赖顺序)
|
||||
// W/E/R 切换 op、T 切换 local/world 空间(UE 语义);dragging 拉起
|
||||
// Editor::BlockingMouse 门控
|
||||
// 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=模型轴(旋转/缩放语义),
|
||||
+65
-16
@@ -20,14 +20,20 @@
|
||||
#include <Engine/Core/Time.h>
|
||||
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Core/Stats.h>
|
||||
#include <Engine/Renderer/Renderer.h>
|
||||
#include <Engine/Renderer/EditorCamera.h>
|
||||
#include <Engine/Serialization/SceneSerializer.h>
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <Engine/Core/Input.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
// editor/game 编译期分离(HELLOVULKAN_EDITOR CMake option):editor 形态
|
||||
// 挂接 docking Editor;game 形态 = Direct 直渲 + primary 相机,零编辑器代码
|
||||
#ifdef HELLO_EDITOR
|
||||
#include "Editor.h"
|
||||
#include "EditorContext.h" // kEnvPaths / kDefaultEnv(与 StatsPanel 共用)
|
||||
#include "EditorContext.h"
|
||||
#endif
|
||||
|
||||
using namespace Engine;
|
||||
|
||||
@@ -51,6 +57,7 @@ namespace
|
||||
void OnInit() override
|
||||
{
|
||||
Renderer& renderer = GetRenderer();
|
||||
#ifdef HELLO_EDITOR
|
||||
renderer.SetDisplayMode(kDisplayViewport);
|
||||
|
||||
|
||||
@@ -59,46 +66,85 @@ namespace
|
||||
|
||||
// Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
|
||||
InitEditor();
|
||||
#else
|
||||
// game 形态:全窗口 Direct 直渲(Renderer 默认即 kDisplayDirect),
|
||||
// 相机由场景 primary camera 提供,无 UI。
|
||||
// 游戏流:Stopped → Loading(OnUpdate 泵场景加载)→ Running
|
||||
GetGameFlow().RequestStart();
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnUpdate(const FrameTime& frameTime) override
|
||||
{
|
||||
#ifdef HELLO_EDITOR
|
||||
// 编辑器形态:驱动编辑器相机(RMB 飞行 / MMB 焦点环绕 /
|
||||
// 滚轮推拉)。暂停(timeScale=0)时仍可飞行——unscaled dt。
|
||||
// PIE 内嵌播放:渲染源已切 primary 相机,编辑器相机停驱
|
||||
//(否则与游戏脚本抢 WASD,Stop 后视角也被偷跑)。
|
||||
// 游戏形态:相机实体由游戏逻辑驱动(本沙盒无逻辑 = 静止)
|
||||
if (GetRenderer().GetDisplayMode() == kDisplayViewport)
|
||||
if (GetRenderer().GetDisplayMode() == kDisplayViewport &&
|
||||
!m_Editor->IsPlaying())
|
||||
{
|
||||
GetRenderer().GetEditorCamera().Update(GetWindow(),
|
||||
frameTime.UnscaledDt());
|
||||
GetRenderer().GetEditorCamera().Update(frameTime.UnscaledDt());
|
||||
}
|
||||
#else
|
||||
// Loading 泵(同步示例):场景文件存在则加载,不存在 =
|
||||
// 空场景照常进 Running(引擎冒烟路径)。加载完成后必须
|
||||
// CompleteLoading,否则物理/脚本门控永远跳过(卡死 Loading)
|
||||
if (GetGameFlow().IsIn(GameFlowState::Loading))
|
||||
{
|
||||
if (std::filesystem::exists(m_ScenePath))
|
||||
{
|
||||
if (Serialization::SceneSerializer::Deserialize(
|
||||
GetScene(), GetAssets(), m_ScenePath))
|
||||
{
|
||||
ENGINE_INFO("Game: scene loaded from {}", m_ScenePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
ENGINE_ERROR("Game: scene load failed: {}",
|
||||
m_ScenePath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ENGINE_WARN("Game: scene file not found ({}), "
|
||||
"starting empty", m_ScenePath);
|
||||
}
|
||||
GetGameFlow().CompleteLoading();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- UI(全部委托 docking Editor;本文件零 ImGui 控件)----
|
||||
|
||||
void OnImGuiRender() override
|
||||
{
|
||||
#ifdef HELLO_EDITOR
|
||||
m_Editor->Draw();
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- 事件 ----
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
void OnWindowClose(const Event& event) override
|
||||
{
|
||||
// 退出确认走 Editor 的模态框(ImGui 必须可见才有 UI)
|
||||
SetImGuiEnabled(true);
|
||||
m_Editor->RequestQuit();
|
||||
}
|
||||
#endif
|
||||
|
||||
void OnKeyPress(const Event& event) override
|
||||
{
|
||||
if (event.key == GLFW_KEY_GRAVE_ACCENT)
|
||||
// 键码统一走 Input::Key(App 层零 GLFW 依赖)
|
||||
#ifdef HELLO_EDITOR
|
||||
if (event.key == static_cast<int>(Input::Key::GraveAccent))
|
||||
{
|
||||
SetImGuiEnabled(!GetImGuiEnabled());
|
||||
}
|
||||
if (event.key == GLFW_KEY_F1)
|
||||
{
|
||||
STATS.PrintComparison();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +154,7 @@ namespace
|
||||
}
|
||||
|
||||
private:
|
||||
#ifdef HELLO_EDITOR
|
||||
// Editor 注入:门面指针 + 反向能力回调(面板 → 沙盒)
|
||||
void InitEditor()
|
||||
{
|
||||
@@ -116,11 +163,10 @@ namespace
|
||||
ctx.scene = &GetScene();
|
||||
ctx.assets = &GetAssets();
|
||||
ctx.app = this;
|
||||
ctx.flow = &GetGameFlow();
|
||||
ctx.editorCamera = &GetRenderer().GetEditorCamera();
|
||||
ctx.getEnvIndex = [this] { return m_EnvIndex; };
|
||||
ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; };
|
||||
// 继承原沙盒默认:初始暂停(StatsPanel 的 Pause Simulation 开关恢复)
|
||||
Time::SetTimeScale(0.0f);
|
||||
// 编辑器态默认冻结:GameFlow 构造即 timeScale=0(Stopped 语义),
|
||||
// timeScale 权威在 GameFlow,此处不再手设
|
||||
m_Editor->Init(ctx);
|
||||
|
||||
// 输入归属:覆盖 EnableImGui 的全局 WantCapture* 谓词
|
||||
@@ -135,7 +181,10 @@ namespace
|
||||
|
||||
private:
|
||||
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
|
||||
int m_EnvIndex = kDefaultEnv;
|
||||
#else
|
||||
// game 形态:启动场景路径(CWD = bin;编辑器 Save 的产物位置)
|
||||
const char* m_ScenePath = "assets/scenes/sandbox.scene";
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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__)
|
||||
+10
-12
@@ -1,10 +1,12 @@
|
||||
//
|
||||
// Spinner.cpp:C++ 原生脚本示范——Y 轴匀速自转。
|
||||
//
|
||||
// 写脚本三步:
|
||||
// 写脚本四步:
|
||||
// 1. 新建 .cpp 放本目录(CMake 自动进构建)
|
||||
// 2. 继承 ScriptBehavior,覆写 Name/OnCreate/OnUpdate/OnDestroy
|
||||
// 3. 文件级 ENGINE_REGISTER_SCRIPT(类名) 自注册——Inspector 下拉
|
||||
// 3. DeclareParams 声明可调参数(Inspector 自动出控件 + 随场景
|
||||
// 序列化 + 实例化灌值)
|
||||
// 4. 文件级 ENGINE_REGISTER_SCRIPT(类名) 自注册——Inspector 下拉
|
||||
// 与场景序列化按该名解析
|
||||
//
|
||||
|
||||
@@ -18,6 +20,10 @@ namespace {
|
||||
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>()(不存在会炸)
|
||||
}
|
||||
@@ -25,23 +31,15 @@ namespace {
|
||||
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; // 度/秒
|
||||
float m_Speed = 90.0f; // 度/秒(声明默认;实例化期被存档值覆盖)
|
||||
};
|
||||
|
||||
static const bool s_Spinner_registered = [] {
|
||||
::Engine::Scripting::ScriptRegistry::Register(
|
||||
"Spinner", []() -> ::Engine::Scope<::Engine::Scripting::ScriptBehavior> {
|
||||
return ::Engine::CreateScope<Spinner>();
|
||||
});
|
||||
return true;
|
||||
}();
|
||||
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
|
||||
+229
-21
@@ -1,10 +1,17 @@
|
||||
//
|
||||
// WASDMove.cpp:WASD 控制实体移动(相机水平朝向参考,UE gameplay 语义)。
|
||||
// WASDMove.cpp:第三人称移动(WASD/手柄摇杆,相机朝向参考,UE gameplay 语义)。
|
||||
//
|
||||
// 移动语义分派:
|
||||
// 输入走引擎具名动作层(Move/Jump/Run 默认绑定):WASD + 方向键 +
|
||||
// 手柄左摇杆移动(引擎径向死区),Space/手柄 A 跳;跑 = Run 动作
|
||||
//(Shift / 手柄 L3 按下左摇杆)。摇杆模拟量直接驱动动量整形(轻推慢走)。
|
||||
//
|
||||
// 移动语义分派(按实体配置自动选择,优先级从上到下):
|
||||
// - CharacterControllerComponent → 角色控制器路径(扫掠运动学:
|
||||
// 水平喂意图速度 / 重力自动积分 / Space 跳跃,Unity
|
||||
// CharacterController.Move 同构)
|
||||
// - Dynamic 刚体 → 走物理速度门面(每帧喂水平速度,Y 归物理——
|
||||
// 重力/跳跃不受影响;松键喂零速 = 立即停,Unity velocity 语义)
|
||||
// - 无刚体 → 直接平移 Transform
|
||||
// - 都没有 → 直接平移 Transform
|
||||
//
|
||||
// 编辑器形态键位天然不冲突:RMB 按住 = 相机吃 WASD 飞行(脚本也读
|
||||
// 到按键 → 同时在动,测移动时松开 RMB 即可);游戏形态 WASD 常开。
|
||||
@@ -12,58 +19,259 @@
|
||||
|
||||
#include <Engine/Scripting/Script.h>
|
||||
#include <Engine/Core/Application.h>
|
||||
#include <Engine/Core/Window.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 <Engine/Physics/PhysicsWorld.h>
|
||||
|
||||
#include <GLFW/glfw3.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(升降/跳跃归物理或将来扩展)
|
||||
const glm::quat cq = App().GetRenderer()
|
||||
// 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);
|
||||
|
||||
auto& win = App().GetWindow();
|
||||
glm::vec3 move(0.0f);
|
||||
if (win.IsKeyPressed(GLFW_KEY_W)) move += fwd;
|
||||
if (win.IsKeyPressed(GLFW_KEY_S)) move -= fwd;
|
||||
if (win.IsKeyPressed(GLFW_KEY_D)) move += right;
|
||||
if (win.IsKeyPressed(GLFW_KEY_A)) move -= right;
|
||||
// 约定: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;
|
||||
if (glm::length(move) > 1e-4f) {
|
||||
// 只钳超速,不放大——摇杆模拟量(幅度 <1)保留进速度:
|
||||
// 轻推慢走、推满全速(归一化会把幅度吃掉,一动就满速)
|
||||
if (glm::length(move) > 1.0f) {
|
||||
move = glm::normalize(move);
|
||||
}
|
||||
const glm::vec3 vel = move * m_Speed;
|
||||
// 走/跑: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; // 期望速度(输入意图)
|
||||
|
||||
if (auto* rb = TryGetComponent<Engine::ECS::RigidBodyComponent>();
|
||||
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 会抵消重力 → 空中物体悬浮)
|
||||
auto& phy = App().GetPhysics();
|
||||
const glm::vec3 cur = phy.GetLinearVelocity(EntityBits());
|
||||
phy.SetLinearVelocity(EntityBits(), {vel.x, cur.y, vel.z});
|
||||
phy.SetLinearVelocity(
|
||||
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
|
||||
} else {
|
||||
Transform().SetPosition(Transform().GetPosition() + vel * dt);
|
||||
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 = 5.0f; // m/s
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
+12
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(HelloVulkan CXX)
|
||||
project(HelloVulkan C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -31,6 +31,11 @@ set(JPH_USE_CPU_COMPUTE OFF CACHE BOOL "" FORCE)
|
||||
set(JPH_USE_DX12 OFF CACHE BOOL "" FORCE)
|
||||
set(JPH_USE_VK OFF CACHE BOOL "" FORCE)
|
||||
set(JPH_USE_MTL OFF CACHE BOOL "" FORCE)
|
||||
# Jolt GENERATE_DEBUG_SYMBOLS 默认 ON,Release 也带 -g → Jolt 的 DWARF
|
||||
# Debug/RelWithDebInfo 保留符号便于调试
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
set(GENERATE_DEBUG_SYMBOLS OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
add_subdirectory(Lib/JoltPhysics/Build)
|
||||
|
||||
# ---- Assimp(vendored submodule,模型导入;只编译需要的 importer) ----
|
||||
@@ -71,6 +76,12 @@ 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")
|
||||
|
||||
@@ -27,6 +27,13 @@ target_include_directories(ImGuizmo PUBLIC ${IMGUIZMO_DIR})
|
||||
target_compile_definitions(ImGuizmo PUBLIC IMGUI_DEFINE_MATH_OPERATORS)
|
||||
target_link_libraries(ImGuizmo PUBLIC imgui)
|
||||
|
||||
# ufbx(FBX 全版本导入,vendored 单文件 C 库):assimp 的 FBX importer
|
||||
# 只认 FBX 2011+,Mixamo 及大量网上资产是 FBX 6.1 binary(二进制布局
|
||||
# 与 7.x 在对象 ID 层不兼容,assimp DOM 直接拒读)。ufbx 官方支持
|
||||
# FBX 5.x/6.x/7.x 全版本,引擎的 .fbx 解析统一走它(glTF/OBJ 仍 assimp)
|
||||
add_library(ufbx STATIC ${CMAKE_SOURCE_DIR}/Lib/ufbx/ufbx.c)
|
||||
target_include_directories(ufbx PUBLIC ${CMAKE_SOURCE_DIR}/Lib/ufbx)
|
||||
|
||||
# Tracy(CPU/GPU 帧分析)
|
||||
add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp)
|
||||
target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public)
|
||||
@@ -50,6 +57,15 @@ 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 编译
|
||||
@@ -71,6 +87,7 @@ target_link_libraries(Engine PUBLIC
|
||||
spdlog::spdlog
|
||||
stb
|
||||
assimp::assimp
|
||||
ufbx
|
||||
meshoptimizer
|
||||
Jolt::Jolt
|
||||
yaml-cpp::yaml-cpp
|
||||
@@ -79,3 +96,9 @@ target_link_libraries(Engine PUBLIC
|
||||
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/Log.h>
|
||||
#include <Engine/Core/AssetRef.h>
|
||||
#include <Engine/Animation/AnimationAttach.h>
|
||||
#include <Engine/Animation/AnimationController.h>
|
||||
#include <Engine/Assets/IAssetUploader.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/MaterialAsset.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
@@ -109,6 +112,14 @@ namespace Engine
|
||||
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
|
||||
{
|
||||
static const std::string kEmpty;
|
||||
@@ -117,11 +128,28 @@ namespace Engine
|
||||
return (e.generation == h.generation && e.alive) ? e.path : kEmpty;
|
||||
}
|
||||
|
||||
// 按路径查活资产(未加载/已卸载返回 nullptr;不触发加载、
|
||||
// 不对坏句柄 FATAL)——面板展示用
|
||||
T* Find(const std::string& path) const
|
||||
{
|
||||
auto it = m_ByPath.find(path);
|
||||
if (it == m_ByPath.end()) return nullptr;
|
||||
const Entry& e = m_Entries[it->second.index];
|
||||
return (e.alive && e.generation == it->second.generation)
|
||||
? e.asset.get() : nullptr;
|
||||
}
|
||||
|
||||
u32 Count() const
|
||||
{
|
||||
return static_cast<u32>(m_Entries.size()) - static_cast<u32>(m_Free.size());
|
||||
}
|
||||
|
||||
// 路径是否在缓存(含被卸载后路径清除的语义:卸载即不含)
|
||||
bool Contains(const std::string& path) const
|
||||
{
|
||||
return m_ByPath.find(path) != m_ByPath.end();
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry
|
||||
{
|
||||
@@ -154,12 +182,58 @@ namespace Engine
|
||||
ModelRef LoadModel(const std::string& path, ParsedModel&& parsed);
|
||||
Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
|
||||
void UnloadModel(ModelRef h) { m_Models.Unload(h); }
|
||||
// 批量按路径卸载模型(未加载为 no-op)。先 GPU 全停飞再销毁
|
||||
//(在飞命令缓冲可能仍引用顶点缓冲/默认纹理 sampler——
|
||||
// 与 ReloadMaterialAsset 的 WaitDeviceIdle 同一先例);一次
|
||||
// 停飞摊薄整批,调用方攒批降频(脚本碎片回收每帧一批)
|
||||
void UnloadModelsByPaths(const std::vector<std::string>& paths)
|
||||
{
|
||||
if (paths.empty()) return;
|
||||
m_Uploader.WaitDeviceIdle();
|
||||
for (const std::string& p : paths) {
|
||||
m_Models.UnloadByPath(p);
|
||||
}
|
||||
}
|
||||
// 按路径查已加载的模型(未加载返回 null,不触发加载)——
|
||||
// 面板 hover 信息卡用
|
||||
Model* GetModelByPath(const std::string& path) const
|
||||
{
|
||||
return m_Models.Find(path);
|
||||
}
|
||||
|
||||
// ---- 外部动画并入(UE AnimSequence 归属 Skeleton 的简化版)----
|
||||
// 把动画容器文件(无网格 FBX/GLB 等)里的 clip 按骨骼节点名
|
||||
// 重映射进目标模型的 clip 表。前提 = 源/目标骨骼命名同源(如
|
||||
// 同一 DCC 拆分/Mixamo 同角色);名字对不上的轨道跳过并计数。
|
||||
// 幂等:目标 clips 已含该源(sourcePath)时不重复并入。
|
||||
// 本方法只是 facade(取模型 + 解析磁盘),重映射算法在
|
||||
// Animation::AttachClipsToModel
|
||||
using AnimAttachResult = Animation::AttachResult;
|
||||
AnimAttachResult AttachAnimationClips(ModelRef target,
|
||||
const std::string& animPath);
|
||||
// 对称剥离(编辑器 detach):删除目标模型里来自 animPath 的
|
||||
// clip,返回删除数。facade(取模型 + 调 Model 方法)
|
||||
u32 DetachAnimationClips(ModelRef target,
|
||||
const std::string& animPath)
|
||||
{
|
||||
Model* m = GetModel(target);
|
||||
return m ? m->RemoveClipsBySource(animPath) : 0u;
|
||||
}
|
||||
|
||||
// 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。
|
||||
ModelRef CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
|
||||
// 半透明平面(alpha 0-255,透明绘制队列):同参数缓存去重
|
||||
ModelRef CreateTransparentPlane(f32 halfSize,
|
||||
u8 r, u8 g, u8 b, u8 a);
|
||||
// 程序化立方体(halfSize 为半边长):同参数缓存去重。
|
||||
// 尺寸变化走实体 Transform 缩放,勿按尺寸炸缓存
|
||||
ModelRef CreateCube(f32 halfSize, u8 r, u8 g, u8 b);
|
||||
|
||||
// 通用程序化图元(MeshFactory 形状名 + 参数):同形状同参数同色
|
||||
// 缓存去重。几何唯一源 = MeshFactory(见 MeshFactory.h)
|
||||
ModelRef CreatePrimitive(const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b);
|
||||
|
||||
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
|
||||
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
|
||||
@@ -170,6 +244,59 @@ namespace Engine
|
||||
DecodedEquirect&& decoded);
|
||||
Cubemap* GetEnvironment(EnvRef h) const { return m_Environments.Get(h); }
|
||||
|
||||
// 路径缓存查询(面板显示加载状态用;不触发加载)
|
||||
bool IsModelLoaded(const std::string& path) const
|
||||
{
|
||||
return m_Models.Contains(path);
|
||||
}
|
||||
bool IsEnvironmentLoaded(const std::string& path) const
|
||||
{
|
||||
return m_Environments.Contains(path);
|
||||
}
|
||||
|
||||
// ---- 材质资产(.material,YAML → GPU)----
|
||||
// 同步加载(YAML 解析 + 纹理上传,主线程):路径缓存去重,
|
||||
// 失败返回无效句柄。调用点收敛在两处——场景读后预载
|
||||
//(SceneSerializer,游戏形态也走)与编辑器指派/创建覆盖
|
||||
//(Inspector);渲染收集期只查不载(FindMaterialAsset)
|
||||
MaterialAssetRef LoadMaterialAsset(const std::string& path);
|
||||
// 按路径查已加载材质(未加载返回 null,不触发加载)——
|
||||
// 收集期 resolve / 面板展示用
|
||||
MaterialAsset* FindMaterialAsset(const std::string& path) const
|
||||
{
|
||||
return m_Materials.Find(path);
|
||||
}
|
||||
bool IsMaterialLoaded(const std::string& path) const
|
||||
{
|
||||
return m_Materials.Contains(path);
|
||||
}
|
||||
// 编辑器重建路径(纹理槽变化):GPU 全停飞 → 卸载旧资产
|
||||
//(句柄即刻失效,引用方下帧 resolve 自然拿到新实例)→
|
||||
// 重新加载。waitIdle 经 uploader 接缝(本类不感知 Vulkan)
|
||||
MaterialAssetRef ReloadMaterialAsset(const std::string& path);
|
||||
|
||||
// ---- 动画控制器资产(.animctrl,YAML → 状态机数据)----
|
||||
// 模式同 MaterialAsset:加载点收敛两处(场景读后预载 +
|
||||
// 编辑器指派),System 收集期只 Find 不 Load。组件按路径
|
||||
// 引用(controllerPath),句柄仅指派点短暂持有
|
||||
using AnimCtrlRef = AssetRef<Animation::AnimationController>;
|
||||
AnimCtrlRef LoadController(const std::string& path);
|
||||
// 强制重载(.animctrl = 纯文本 YAML,外部编辑器改完后调用;
|
||||
// 失败时缓存已清,Find 落空 → rest 兜底,路径仍可再次重载)
|
||||
AnimCtrlRef ReloadController(const std::string& path)
|
||||
{
|
||||
m_Controllers.UnloadByPath(path);
|
||||
return LoadController(path);
|
||||
}
|
||||
Animation::AnimationController* FindController(const std::string& path) const
|
||||
{
|
||||
return m_Controllers.Find(path);
|
||||
}
|
||||
bool IsControllerLoaded(const std::string& path) const
|
||||
{
|
||||
return m_Controllers.Contains(path);
|
||||
}
|
||||
|
||||
AssetCache<Model>& Models() { return m_Models; }
|
||||
|
||||
// ---- 资产 GUID 注册表(UE AssetRegistry 模式:中央注册表)----
|
||||
@@ -185,6 +312,11 @@ namespace Engine
|
||||
// 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle
|
||||
// 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。
|
||||
|
||||
// 把 dirty 的 GUID 注册表落盘(Application 退出 / 场景保存时调)。
|
||||
// 内部幂等:非 dirty 直接 return。GetOrCreateGUID 只标 dirty 不落盘,
|
||||
// 避免每次新增 GUID 都全量重写 yaml(开发期编辑器卡顿主因之一)
|
||||
void FlushGUIDRegistry();
|
||||
|
||||
private:
|
||||
void LoadGUIDRegistry();
|
||||
void SaveGUIDRegistry() const;
|
||||
@@ -193,10 +325,16 @@ namespace Engine
|
||||
|
||||
AssetCache<Model> m_Models;
|
||||
AssetCache<Cubemap> m_Environments;
|
||||
AssetCache<MaterialAsset> m_Materials;
|
||||
AssetCache<Animation::AnimationController> m_Controllers;
|
||||
|
||||
// GUID 注册表(双向);dirty 即时保存,无析构落盘依赖
|
||||
// GUID 注册表(双向);GetOrCreateGUID 标 dirty,FlushGUIDRegistry 落盘
|
||||
// (Application 退出 / 场景保存时统一 flush,避免每次新增都全量重写 yaml)
|
||||
std::unordered_map<u64, std::string> m_GuidToPath;
|
||||
std::unordered_map<std::string, u64> m_PathToGuid;
|
||||
|
||||
bool m_GuidRegistryDirty = false;
|
||||
|
||||
};
|
||||
} // namespace Assets
|
||||
} // namespace Engine
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/MaterialAsset.h>
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
|
||||
namespace Engine::Assets {
|
||||
@@ -41,12 +42,32 @@ namespace Engine::Assets {
|
||||
virtual Scope<Model> CreateTransparentPlane(f32 halfSize,
|
||||
u8 r, u8 g, u8 b, u8 a) = 0;
|
||||
|
||||
// 程序化纯色立方体(halfSize 为半边长;占位几何/碎片模拟)
|
||||
virtual Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) = 0;
|
||||
|
||||
// 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/
|
||||
// cone/torus 等):形状 + 参数 → 单 mesh 模型。非法形状返回 nullptr
|
||||
virtual Scope<Model> CreatePrimitive(const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b) = 0;
|
||||
|
||||
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
|
||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
|
||||
|
||||
// 异步加载路径:解码已在后台线程完成,此调用只做 GPU 生成(主线程)
|
||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||
DecodedEquirect&& decoded) = 0;
|
||||
|
||||
// ---- 材质资产 GPU 构建(MaterialAsset 专用接缝)----
|
||||
// 数据段 → 纹理上传 + Material 描述符(失败/空槽折共享默认纹理,
|
||||
// 纹理所有权归返回的 MaterialAsset)。失败返回 nullptr
|
||||
virtual Scope<MaterialAsset> CreateMaterial(
|
||||
MaterialAssetData&& data) = 0;
|
||||
|
||||
// GPU 全停飞(编辑器低频重建路径用:材质资产 Reload 前调用,
|
||||
// 保证旧纹理/描述符无在飞引用后销毁——与 Renderer::SetEnvironment
|
||||
// 的 waitIdle 换绑同一先例)
|
||||
virtual void WaitDeviceIdle() = 0;
|
||||
};
|
||||
|
||||
} // namespace Engine::Assets
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Core/Event.h>
|
||||
#include <Engine/Core/GameFlow.h>
|
||||
#include <Engine/Core/Time.h>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
@@ -75,6 +76,7 @@ namespace Engine {
|
||||
Assets::AssetManager& GetAssets() const { return *m_Assets; }
|
||||
PhysicsWorld& GetPhysics() const { return *m_Physics; }
|
||||
Scripting::ScriptRuntime& GetScripts() const { return *m_Scripts; }
|
||||
GameFlow& GetGameFlow() { return m_GameFlow; }
|
||||
u64 GetFrameCount() const { return m_FrameCount; }
|
||||
|
||||
// ImGui相关
|
||||
@@ -127,6 +129,9 @@ namespace Engine {
|
||||
// 其后端销毁需要 Device 与 GLFWwindow 存活)
|
||||
Scope<ImGuiLayer> m_ImGuiLayer;
|
||||
|
||||
// 游戏流状态机(构造即冻结:Stopped → timeScale=0)
|
||||
GameFlow m_GameFlow;
|
||||
|
||||
bool m_Running = false;
|
||||
u64 m_FrameCount = 0;
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace Engine {
|
||||
class Cubemap;
|
||||
class Texture;
|
||||
class Material;
|
||||
class MaterialAsset;
|
||||
}
|
||||
|
||||
namespace Engine {
|
||||
@@ -41,6 +42,7 @@ namespace Engine {
|
||||
// 常用别名(新增资产类型时在这里加一行)
|
||||
using ModelRef = AssetRef<Model>;
|
||||
using EnvRef = AssetRef<Cubemap>; // 环境(IBL 四件套)
|
||||
using MaterialAssetRef = AssetRef<MaterialAsset>; // 材质资产(.material)
|
||||
|
||||
} // 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 AddDeallocation(u64 bytes);
|
||||
|
||||
// ---- 采样进程真实内存 ----
|
||||
void SampleProcessMemory();
|
||||
|
||||
// ---- 打印 ----
|
||||
void Print() const;
|
||||
void PrintDetailed() const;
|
||||
void PrintComparison() const;
|
||||
// ---- 只读快照(面板/遥测消费)----
|
||||
struct Snapshot
|
||||
{
|
||||
u64 textureCount = 0;
|
||||
u64 textureBytes = 0;
|
||||
u64 meshCount = 0;
|
||||
u64 meshBytes = 0;
|
||||
u64 bufferCount = 0;
|
||||
u64 bufferBytes = 0;
|
||||
u64 memoryBlockBytes = 0; // MemoryAllocator 已分配块
|
||||
u64 memoryUsedBytes = 0; // 块内实际占用
|
||||
u64 frameAllocation = 0; // 本帧新增(BeginFrame 清零)
|
||||
u64 frameDeallocation = 0; // 本帧释放
|
||||
u64 totalAllocation = 0;
|
||||
u64 totalDeallocation = 0;
|
||||
};
|
||||
Snapshot TakeSnapshot() const;
|
||||
|
||||
private:
|
||||
Stats() = default;
|
||||
|
||||
struct ProcessMemory
|
||||
{
|
||||
u64 VmSize = 0; // /proc/self/status VmSize
|
||||
u64 Rss = 0; // /proc/self/status VmRSS
|
||||
u64 Pss = 0; // smaps_rollup
|
||||
u64 SharedClean = 0;
|
||||
u64 PrivateDirty = 0;
|
||||
u64 PrivateClean = 0;
|
||||
};
|
||||
|
||||
ProcessMemory ReadProcessMemory() const;
|
||||
|
||||
mutable std::mutex m_Mutex;
|
||||
|
||||
// 资源计数
|
||||
@@ -81,9 +79,6 @@ namespace Engine
|
||||
u64 m_FrameDeallocation = 0;
|
||||
u64 m_TotalAllocation = 0;
|
||||
u64 m_TotalDeallocation = 0;
|
||||
|
||||
// 进程快照
|
||||
ProcessMemory m_ProcessMem{};
|
||||
};
|
||||
|
||||
#define STATS Engine::Stats::Get()
|
||||
|
||||
@@ -17,6 +17,16 @@ struct GLFWwindow;
|
||||
|
||||
namespace Engine {
|
||||
|
||||
// 手柄状态快照(POD,PollGamepad 每帧填充)。
|
||||
// 值 = GLFW 标准游戏布局:axes 下标即 GLFW_GAMEPAD_AXIS_*(左摇杆 X/Y、
|
||||
// 右摇杆 X/Y、LT、RT),buttons 下标即 GLFW_GAMEPAD_BUTTON_*(A=0…),
|
||||
// 调用方不得依赖具体数值(GLFW 头语义)
|
||||
struct GamepadState {
|
||||
bool connected = false;
|
||||
f32 axes[6] = {};
|
||||
bool buttons[15] = {};
|
||||
};
|
||||
|
||||
class Window {
|
||||
public:
|
||||
Window(const std::string& title, u32 width, u32 height);
|
||||
@@ -76,6 +86,11 @@ namespace Engine {
|
||||
void SetCursorCaptured(bool captured);
|
||||
bool IsCursorCaptured() const { return m_CursorCaptured; }
|
||||
|
||||
// 手柄轮询(JOYSTICK_1 标准游戏布局):连接且带标准映射时填充
|
||||
// state 并返回 true;否则置 connected=false 全零并返回 false。
|
||||
// 无分配(GLFW 平台态拉取),每帧调用成本可忽略
|
||||
bool PollGamepad(GamepadState& state) const;
|
||||
|
||||
|
||||
|
||||
private:
|
||||
|
||||
@@ -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,6 +12,9 @@
|
||||
#include <Engine/Core/UUID.h>
|
||||
#include <Engine/Core/AssetRef.h>
|
||||
#include <Engine/Renderer/Camera.h>
|
||||
#include <Engine/Scripting/ScriptParam.h>
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
@@ -19,6 +22,7 @@
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
@@ -40,22 +44,29 @@ namespace Engine::ECS {
|
||||
std::string name;
|
||||
};
|
||||
|
||||
// 世界变换:UE USceneComponent 的 encapsulation 模式——
|
||||
// SRT 分量私有,所有写入口标脏;world 矩阵由帧循环的
|
||||
// Scene::UpdateTransforms() 只对脏的实体重算(transform-update
|
||||
// 阶段),并自动向未来的子层级传播。渲染热路径只读 World()。
|
||||
// 层级(父子变换)留给后续的层级同步系统。
|
||||
// 变换(UE FTransform 语义):TRS 分量 = 相对父的 **local** 值
|
||||
//(无父实体 local 即 world,旧场景零迁移)。分量私有,所有写入口
|
||||
// 标脏;world 缓存(SRT 三分量 + 矩阵)由帧循环的
|
||||
// Scene::UpdateTransforms() 从根 DFS 只对脏子树重算(transform-
|
||||
// update 阶段)。子 world = 父 world SRT 组合而非矩阵乘法——
|
||||
// 非均匀父缩放下组合式仍是干净 SRT(矩阵分解会引入剪切)。
|
||||
// 渲染热路径只读 World();灯光/相机/物理/Gizmo 读 world 三分量。
|
||||
//
|
||||
// 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角):
|
||||
// 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在
|
||||
// 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation
|
||||
// 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。
|
||||
struct TransformComponent {
|
||||
// ---- 读取 ----
|
||||
// ---- local 读取(相对父;UI/游戏逻辑用)----
|
||||
const glm::vec3& GetPosition() const { return m_Position; }
|
||||
const glm::quat& GetRotationQuat() const { return m_Rotation; }
|
||||
const glm::vec3& GetScale() const { return m_Scale; }
|
||||
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 列主序索引:
|
||||
@@ -65,32 +76,53 @@ namespace Engine::ECS {
|
||||
{
|
||||
const glm::mat3 R = glm::mat3_cast(m_Rotation);
|
||||
const f32 sp = std::clamp(-R[2][1], -1.0f, 1.0f); // = sin(pitch)
|
||||
if (std::abs(sp) < 1.0f) {
|
||||
return glm::degrees(glm::vec3(
|
||||
// 万向锁分支阈值 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])));
|
||||
}
|
||||
// ±90° 万向锁:yaw/roll 只剩组合角,约定 roll=0(UE 同策略)
|
||||
// 归入 yaw;两支组合角符号不同
|
||||
return sp > 0.0f
|
||||
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::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);
|
||||
glm::degrees(std::atan2(-R[0][2], -R[1][2])),
|
||||
0.0f));
|
||||
// 显示约定 = UE Details 的 0-360(-90 → 270)。逐轴 fmod wrap
|
||||
// 与 [-180,180] 数学严格等价(每轴旋转周期 360°),矩阵重建类
|
||||
// 消费端(Camera::EulerRotMat 等)不受影响;需要连续域做
|
||||
// clamp/lerp 的调用方自行 wrap 回 [-180,180](如自由视角)
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
euler[i] = std::fmod(euler[i], 360.0f);
|
||||
if (euler[i] < 0.0f) euler[i] += 360.0f;
|
||||
if (euler[i] >= 360.0f) euler[i] = 0.0f; // 浮点边界
|
||||
}
|
||||
return euler;
|
||||
}
|
||||
|
||||
// ---- 写入(标脏,脏实体由 UpdateTransforms 重算)----
|
||||
TransformComponent& SetPosition(const glm::vec3& p) { if (m_Position != p) { m_Position = p; m_Dirty = true; } return *this; }
|
||||
// 欧拉角(度)入口:UI/游戏逻辑用。转 quat 存(q = qy·qx·qz,
|
||||
// 与原 Ry·Rx·Rz 合成严格同构;不能用 glm::quat(euler) ctor——
|
||||
// 那是 ZYX 航向序,语义不同)
|
||||
// 那是 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 SetRotationQuat(qy * qx * qz);
|
||||
return qy * qx * qz;
|
||||
}
|
||||
// 四元数直写(UE FTransform 语义):Gizmo 拖拽/物理写回用,
|
||||
// 不经欧拉往返 → 零漂移、无万向锁
|
||||
@@ -99,63 +131,231 @@ namespace Engine::ECS {
|
||||
|
||||
bool IsDirty() const { return m_Dirty; }
|
||||
|
||||
// 显式标脏:层级系统改父/挂载后触发 world 重算用
|
||||
//(local 值未变但 world 缓存已失效)
|
||||
TransformComponent& MarkDirty() { m_Dirty = true; return *this; }
|
||||
|
||||
// SRT 分量 → world 矩阵(T * R * S),清除脏标记。
|
||||
// 由 Scene::UpdateTransforms 调用(或层级系统),勿手动
|
||||
// 根实体版(world = local);由 Scene::UpdateTransforms 调用,
|
||||
// 勿手动
|
||||
TransformComponent& Compose()
|
||||
{
|
||||
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_Position);
|
||||
const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_Scale);
|
||||
m_World = t * glm::mat4_cast(m_Rotation) * s;
|
||||
m_WorldPosition = m_Position;
|
||||
m_WorldRotation = m_Rotation;
|
||||
m_WorldScale = m_Scale;
|
||||
RebuildWorldMatrix();
|
||||
m_Dirty = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// 子实体版:world = 父 world SRT 组合(UE FTransform 语义):
|
||||
// wPos = pPos + pRot·(pScale⊙lPos) wRot = pRot·lRot wScl = pScale⊙lScl
|
||||
// 组合式(非矩阵分解)保证非均匀父缩放下结果仍是干净 SRT。
|
||||
// 要求父已 Compose(DFS 父先于子保证)。由 Scene 调用,勿手动
|
||||
TransformComponent& ComposeFromParent(const TransformComponent& parent)
|
||||
{
|
||||
m_WorldPosition = parent.m_WorldPosition +
|
||||
parent.m_WorldRotation * (parent.m_WorldScale * m_Position);
|
||||
m_WorldRotation = parent.m_WorldRotation * m_Rotation;
|
||||
m_WorldScale = parent.m_WorldScale * m_Scale;
|
||||
RebuildWorldMatrix();
|
||||
m_Dirty = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
void RebuildWorldMatrix()
|
||||
{
|
||||
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_WorldPosition);
|
||||
const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_WorldScale);
|
||||
m_World = t * glm::mat4_cast(m_WorldRotation) * s;
|
||||
}
|
||||
|
||||
// ---- local TRS(相对父)----
|
||||
glm::vec3 m_Position{0.0f};
|
||||
glm::quat m_Rotation{1.0f, 0.0f, 0.0f, 0.0f}; // 单位四元数(w,x,y,z)
|
||||
glm::vec3 m_Scale{1.0f};
|
||||
// ---- world 缓存(UpdateTransforms 重算)----
|
||||
glm::vec3 m_WorldPosition{0.0f};
|
||||
glm::quat m_WorldRotation{1.0f, 0.0f, 0.0f, 0.0f};
|
||||
glm::vec3 m_WorldScale{1.0f};
|
||||
glm::mat4 m_World{1.0f};
|
||||
bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world
|
||||
};
|
||||
|
||||
// 层级链接(UE Actor attach):父句柄 + 子表。惰性创建——
|
||||
// 只有参与父子关系的实体才有本组件,独立实体零存储成本。
|
||||
// entt 句柄仅在 registry 存续期有效;序列化换 UUID,加载后重链
|
||||
struct HierarchyComponent {
|
||||
entt::entity parent = entt::null; // entt::null = 根实体
|
||||
std::vector<entt::entity> children; // 挂载顺序
|
||||
};
|
||||
|
||||
// 网格渲染体:引用共享的 Model 资产。
|
||||
// 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
|
||||
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
|
||||
//
|
||||
// 材质覆盖(UE Material Slot 语义):key = mesh 下标(Model::GetMeshes
|
||||
// 序),value = .material 资产路径;无该项 = 用模型内建材质(只读)。
|
||||
// 存路径不存句柄:覆盖材质由渲染收集期按路径惰性 resolve(资产
|
||||
// 已被场景预载/编辑器指派,收集只查缓存),组件保持纯数据。
|
||||
// 资产被多实体共享时编辑同时生效;实体独立 = 每实体一份覆盖文件
|
||||
struct MeshRendererComponent {
|
||||
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;
|
||||
};
|
||||
|
||||
// 骨骼动画播放状态(纯数据 + 便捷控制)。
|
||||
// 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者);
|
||||
// ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。
|
||||
// 骨骼动画播放状态(纯数据 + 便捷控制)——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 数量不变,只是求值端多采一组姿态)
|
||||
// crossfade(clip 切换平滑过渡):来源 clip 淡出期间与目标
|
||||
// clip 并行采样,局部 TRS 层混合(palette 数量不变,只是求值
|
||||
// 端多采一组姿态)
|
||||
int fadeFrom = -1; // 来源 clip 下标(-1 = 无过渡)
|
||||
f32 fadeFromTime = 0.0f; // 来源 clip 播放头
|
||||
f32 fadeElapsed = 0.0f; // 已过渡时长
|
||||
f32 fadeDuration = 0.3f; // 过渡总时长(秒)
|
||||
|
||||
// 切换 clip(带 crossfade)。越界/空 clip 保护在消费端。
|
||||
// 按名播放的运行时反指(瞬态,不落盘不序列化):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) return;
|
||||
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 的实体是当前渲染视角。
|
||||
@@ -277,21 +477,57 @@ namespace Engine::ECS {
|
||||
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 习惯
|
||||
};
|
||||
|
||||
// 脚本组件:只存脚本类名(注册表键,POD 可序列化)。C++ 原生脚本经
|
||||
// ENGINE_REGISTER_SCRIPT 自注册;将来 Lua/C# 脚本同键解析。
|
||||
// 脚本组件:多脚本挂载(Unity 语义——一个 GameObject 挂 N 个
|
||||
// MonoBehaviour,各自有 enabled)。每项 = 脚本类名(注册表键,
|
||||
// 将来 Lua/C# 同键解析)+ 独立启停位;组件级 active 是总开关。
|
||||
// 实例生命周期由 ScriptRuntime 对账管理(不进组件,保持可拷贝)
|
||||
struct ScriptComponent {
|
||||
bool active = true; // 禁用 = 实例保留但停 OnUpdate(Unity disable)
|
||||
std::string scriptName;
|
||||
// 单个脚本项
|
||||
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
|
||||
|
||||
#endif //HELLOVULKAN_COMPONENTS_H
|
||||
|
||||
@@ -40,10 +40,10 @@ namespace Engine::ECS {
|
||||
Scene& operator=(const Scene&) = delete;
|
||||
|
||||
// ---- 场景级设置 ----
|
||||
// 环境类设置已迁为环境实体组件(SkyboxComponent/SkyLightComponent,
|
||||
// UE Skybox/SkyLight Actor 语义);仅保留渲Target清屏色
|
||||
struct Settings {
|
||||
glm::vec4 ClearColor = {0.1, 0.1, 0.1, 1.0};
|
||||
glm::vec4 AmbientColor{0.07f, 0.07f, 0.09f, 0.0f};
|
||||
bool DrawSkybox = true;
|
||||
glm::vec4 ClearColor = {0.0, 0.0, 0.0, 1.0};
|
||||
};
|
||||
Settings& GetSettings() { return m_Settings; }
|
||||
const Settings& GetSettings() const { return m_Settings; } // 序列化只读
|
||||
@@ -52,7 +52,8 @@ namespace Engine::ECS {
|
||||
|
||||
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy()
|
||||
EntityHandle CreateEntity(std::string name = "new entity");
|
||||
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
|
||||
// 延迟:帧尾统一销毁。层级级联(UE 语义):销毁父 → 子树随葬
|
||||
void DestroyEntity(Entity e);
|
||||
void FlushDestroyQueue(); // 每帧末尾由 Application 调用
|
||||
|
||||
// 整场景换血(序列化加载):立即销毁全部实体并清空延迟队列。
|
||||
@@ -66,13 +67,40 @@ namespace Engine::ECS {
|
||||
|
||||
// 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update
|
||||
// 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用,
|
||||
// 用户代码永远不需要手动 Compose
|
||||
// 用户代码永远不需要手动 Compose。层级语义:从根 DFS,脏实体
|
||||
// 或脏祖先的后代才重算(父先于子,ComposeFromParent 组合)
|
||||
void UpdateTransforms();
|
||||
bool IsAlive(Entity e) const { return m_Registry.valid(e); }
|
||||
|
||||
// ---- 层级(UE Actor attach 语义)----
|
||||
|
||||
// 改父/挂根。keepWorld=true(编辑器拖拽语义):换算 local 保持
|
||||
// world 位姿不变;false(序列化加载语义):local 已就位只建链。
|
||||
// 防环(新父是 child 后代 → 拒绝);成功返回 true
|
||||
bool SetParent(Entity child, Entity parent, bool keepWorld = true);
|
||||
|
||||
// 父句柄;根/悬空/无效实体返回 entt::null
|
||||
Entity GetParent(Entity e) const;
|
||||
|
||||
// e 是否在 ancestor 的子树内(含相等)。防环检测/面板用
|
||||
bool IsDescendantOf(Entity e, Entity ancestor) const;
|
||||
|
||||
// 世界位姿写入(UE SetActorLocation 语义):world SRT → 沿父链
|
||||
// 反解 local。物理写回/Gizmo 拖拽用
|
||||
void SetWorldPose(Entity e, const glm::vec3& worldPos,
|
||||
const glm::quat& worldRot, const glm::vec3& worldScale);
|
||||
|
||||
// 沿父链自顶向下补算脏 world(层级写读 world 前调用——
|
||||
// UpdateTransforms 之外的时机读取/换算 world 前保证缓存新鲜)
|
||||
void EnsureWorldFresh(Entity e);
|
||||
|
||||
// 按稳定 UUID 查实体(序列化/跨引用场景);找不到返回 entt::null
|
||||
Entity FindEntityByUUID(UUID uuid);
|
||||
|
||||
// 全量重建 UUID → Entity 索引(序列化加载完调一次;运行期勿调,
|
||||
// CreateEntity / FlushDestroyQueue 已增量维护)
|
||||
void RebuildUuidIndex();
|
||||
|
||||
Registry& GetRegistry() { return m_Registry; }
|
||||
const Registry& GetRegistry() const { return m_Registry; } // 序列化等只读遍历
|
||||
|
||||
@@ -113,6 +141,12 @@ namespace Engine::ECS {
|
||||
Registry m_Registry;
|
||||
std::vector<Entity> m_PendingDestroy;
|
||||
Settings m_Settings;
|
||||
|
||||
// UUID → Entity 索引(O(1) FindEntityByUUID;SyncConstraints 每帧调用)
|
||||
// CreateEntity / FlushDestroyQueue 增量维护;序列化加载后由
|
||||
// RebuildUuidIndex() 全量重建一次(SceneSerializer 直接改 IdComponent.uuid
|
||||
// 绕过 CreateEntity,仅靠增量会不同步)
|
||||
std::unordered_map<UUID, Entity> m_UUIDToEntity;
|
||||
};
|
||||
|
||||
} // namespace Engine::ECS
|
||||
|
||||
@@ -48,6 +48,18 @@ namespace Engine {
|
||||
}
|
||||
void UpdateSceneTexture(u32 slot, vk::ImageView view);
|
||||
|
||||
// ---- 材质球预览 atlas(MaterialPreviewPass 输出 → 材质槽 UI;
|
||||
// editor 专属,HELLO_EDITOR 裁剪)----
|
||||
// 注册/生命周期语义与场景纹理完全一致(按槽位各一个描述符)
|
||||
#ifdef HELLO_EDITOR
|
||||
ImTextureID GetPreviewTexture(u32 slot) const
|
||||
{
|
||||
return slot < kMaxFramesInFlight
|
||||
? m_PreviewTextures[slot] : ImTextureID_Invalid;
|
||||
}
|
||||
void UpdatePreviewTexture(u32 slot, vk::ImageView view);
|
||||
#endif
|
||||
|
||||
// ---- 每帧三段式(由 Application::Run 驱动)----
|
||||
|
||||
// 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间,
|
||||
@@ -68,9 +80,13 @@ namespace Engine {
|
||||
// 生命周期保证:Application 成员序中 ImGuiLayer 在 Renderer 之后声明
|
||||
//(先析构),因此 Device 引用在整个层生命周期内有效
|
||||
Device& m_Device;
|
||||
Window& m_Window; // 光标捕获状态查询(NewFrame hover 门控)
|
||||
vk::DescriptorPool m_Pool;
|
||||
bool m_Enabled = true;
|
||||
ImTextureID m_SceneTextures[kMaxFramesInFlight]{};
|
||||
#ifdef HELLO_EDITOR
|
||||
ImTextureID m_PreviewTextures[kMaxFramesInFlight]{};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// InputAction.h:输入动作模型类型。
|
||||
//
|
||||
// 动作层把"游戏语义"(Move/Jump)与"物理输入源"(键/鼠标/手柄)解耦:
|
||||
// - 动作(Action)= 具名游戏语义,Button(按下沿)/Axis1D/Axis2D
|
||||
// - 绑定(Binding)= 一个输入源到动作贡献的映射(code = GLFW 标准码)
|
||||
// 上层(脚本/玩法系统)只认动作名,换键位/换手柄不改代码。
|
||||
//
|
||||
// code 值约定 = GLFW 标准码(同 Input::Key 惯例:Key 即 glf3.h 键码,
|
||||
// GamepadButton 即 GLFW_GAMEPAD_BUTTON_*,GamepadAxis 即
|
||||
// GLFW_GAMEPAD_AXIS_*);调用方不得依赖具体数值。
|
||||
//
|
||||
// component:Axis2D 动作中该绑定贡献哪个分量(0=X,1=Y)。
|
||||
// 例如 W → {Key, KEY_W, scale=-1, component=1}(Y 分量负方向 = 前进),
|
||||
// 左摇杆 Y 轴 → {GamepadAxis, AXIS_LEFT_Y, scale=+1, component=1}
|
||||
//(GLFW 摇杆推上为负,与 W 的 -1 语义自然对齐)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_INPUTACTION_H
|
||||
#define HELLOVULKAN_INPUTACTION_H
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Engine/Core/Types.h"
|
||||
|
||||
namespace Engine {
|
||||
namespace Input {
|
||||
|
||||
// 动作类型
|
||||
enum class ActionKind : u8 {
|
||||
Button, // 0/1 按下态(Down/Pressed/Released 边沿查询)
|
||||
Axis1D, // 标量 [-1,1](键绑定 scale 加权、手柄轴原始值×scale)
|
||||
Axis2D, // 平面向量(2D 径向死区 + 单位圆钳制)
|
||||
};
|
||||
|
||||
// 绑定源类型
|
||||
enum class SourceKind : u8 {
|
||||
Key, // 键盘(code = GLFW 键码)
|
||||
MouseButton, // 鼠标键(0=左 1=右 2=中)
|
||||
GamepadButton, // 手柄按钮(0-14 标准布局)
|
||||
GamepadAxis, // 手柄轴(0-5:左摇杆 X/Y、右摇杆 X/Y、LT、RT)
|
||||
};
|
||||
|
||||
// 单条绑定:一个输入源对一个动作的贡献
|
||||
struct ActionBinding {
|
||||
SourceKind kind;
|
||||
i32 code; // 见头注释(GLFW 标准码)
|
||||
f32 scale; // 贡献权重(方向:键 ±1;轴 ±1)
|
||||
u8 component = 0; // 仅 Axis2D 消费:0=X 分量,1=Y 分量
|
||||
};
|
||||
|
||||
// 动作实例:定义 + 绑定表 + 双帧值(边沿派生用,见 InputMap)
|
||||
struct InputAction {
|
||||
std::string name;
|
||||
ActionKind kind = ActionKind::Button;
|
||||
std::vector<ActionBinding> bindings;
|
||||
|
||||
// prev/cur 双帧值(Button/Axis1D 用 x;Axis2D 用 xy)。
|
||||
// Button 语义:Down = |cur.x|>0.5,Pressed/Released = cur/prev 边沿
|
||||
glm::vec2 prev{0.0f, 0.0f};
|
||||
glm::vec2 cur{0.0f, 0.0f};
|
||||
};
|
||||
|
||||
} // namespace Input
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_INPUTACTION_H
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// InputMap.h:动作表(定义/绑定/采样/重绑定/持久化)。
|
||||
//
|
||||
// 装配期(DefineAction/Bind/SetDefaults/LoadYaml)与运行期(Poll + 查询)
|
||||
// 分离:名字→下标哈希在 Define 期建好,Poll 只写值槽位——热路径零堆分配。
|
||||
//
|
||||
// 采样语义(Poll):
|
||||
// - active=false(GameFlow 非 Running):全部动作值清零且 prev 同步清零,
|
||||
// 恢复激活后按住的键产生一次 Pressed 边(符合游戏语义,无脏边)
|
||||
// - active=true:键鼠经 Window 轮询(自带 UI 捕获门控),手柄经
|
||||
// Window::PollGamepad;连接翻转打一条日志
|
||||
// - Axis2D:各绑定按 component 分量加权求和 → 2D 径向死区(0.15) →
|
||||
// 超出单位圆时归一化(斜向不超速)
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_INPUTMAP_H
|
||||
#define HELLOVULKAN_INPUTMAP_H
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Engine/Core/Types.h"
|
||||
#include "Engine/Input/InputAction.h"
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Window;
|
||||
|
||||
namespace Input {
|
||||
|
||||
class InputMap {
|
||||
public:
|
||||
// ---- 装配期 ----
|
||||
|
||||
// 定义动作(返回动作 id = 下标;重名忽略并返回已有 id)
|
||||
size_t DefineAction(const std::string& name, ActionKind kind);
|
||||
// 追加绑定(动作 id 非法时忽略)
|
||||
void Bind(size_t id, ActionBinding b);
|
||||
// 就地重绑定(下帧采样即生效)。成功返回 true
|
||||
bool Rebind(size_t id, size_t bindingIdx, ActionBinding b);
|
||||
// 内建默认表:Move(Axis2D)=WASD+方向键+左摇杆、Jump(Button)=Space+A
|
||||
void SetDefaults();
|
||||
// 清空全部动作与索引(LoadYaml 重建用)
|
||||
void Clear();
|
||||
|
||||
// ---- 运行期 ----
|
||||
|
||||
// 每帧采样(主循环单点调用:物理/脚本/OnUpdate 之前)
|
||||
void Poll(const Window& win, bool active);
|
||||
|
||||
bool Down(size_t id) const; // Button: 按住;Axis: |值|>0.5
|
||||
bool Pressed(size_t id) const; // 本帧按下边沿
|
||||
bool Released(size_t id) const; // 本帧松开边沿
|
||||
f32 Value(size_t id) const; // Axis1D;Button 返回 0/1
|
||||
glm::vec2 Value2(size_t id) const;// Axis2D;其他 kind 返回 (x,0)
|
||||
|
||||
// 手柄连接态(Poll 时更新;连接翻转打 INFO)
|
||||
bool IsGamepadConnected() const { return m_GamepadConnected; }
|
||||
|
||||
// ---- 持久化(yaml)----
|
||||
|
||||
// 格式见 InputMap.cpp;保存失败(目录不存在等)返回 false
|
||||
bool SaveYaml(const std::string& path) const;
|
||||
// 文件不存在静默失败返回 false(保持现状表);存在则
|
||||
// SetDefaults 后按 yaml 覆盖同名动作的绑定表
|
||||
bool LoadYaml(const std::string& path);
|
||||
|
||||
// ---- 查询辅助 ----
|
||||
size_t Count() const { return m_Actions.size(); }
|
||||
// 名字→id;不存在返回 npos
|
||||
static constexpr size_t npos = static_cast<size_t>(-1);
|
||||
size_t Find(const std::string& name) const;
|
||||
|
||||
private:
|
||||
void PollGamepadConnection(bool connected);
|
||||
|
||||
std::vector<InputAction> m_Actions;
|
||||
std::unordered_map<std::string, size_t> m_ByName;
|
||||
bool m_GamepadConnected = false;
|
||||
};
|
||||
|
||||
} // namespace Input
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_INPUTMAP_H
|
||||
@@ -6,6 +6,11 @@
|
||||
// "看不到"它(无碰撞解算,仅查询)。由 PhysicsWorld 对账创建/销毁并
|
||||
// 在 PhysicsSystem::Update 之后驱动(Jolt 官方建议序)。
|
||||
//
|
||||
// 角色间碰撞(Jolt 设计行为):CharacterVirtual 彼此默认穿透,需经
|
||||
// CharacterVsCharacterCollision 显式互查。PhysicsWorld 持有一份
|
||||
// CharacterVsCharacterCollisionSimple 并注入每个角色(SetCharacterCollision),
|
||||
// 使角色扫掠时也撞其它角色——进/出列表由本类构造/析构自动维护。
|
||||
//
|
||||
// 速度语义(游戏逻辑职责分工):
|
||||
// - 水平移动:游戏逻辑每帧 SetCharacterVelocity 喂意图速度
|
||||
// - 垂直分量:本类自动积分重力(着地清零向下分量);Jump 置垂直速度
|
||||
@@ -36,13 +41,21 @@ namespace Engine {
|
||||
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);
|
||||
~CharacterController() = default; // Ref 引用计数自动释放
|
||||
// 析构:先从角色间碰撞列表摘除(防悬垂指针),Ref 随即释放
|
||||
~CharacterController();
|
||||
|
||||
// 注入角色间碰撞列表(PhysicsWorld 单份共享):登记本角色并打开
|
||||
// 互查。构造后由 PhysicsWorld 创建时调用一次;列表须比本对象活得久
|
||||
void SetCharacterCollision(JPH::CharacterVsCharacterCollisionSimple& list);
|
||||
|
||||
// 每物理步调用:重力积分 + ExtendedUpdate(扫掠滑动 + 贴地 +
|
||||
// 上台阶)。allocator 与 PhysicsSystem::Update 串行复用同一池
|
||||
@@ -59,6 +72,16 @@ namespace Engine {
|
||||
|
||||
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;
|
||||
@@ -67,6 +90,8 @@ namespace Engine {
|
||||
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
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
@@ -78,6 +79,11 @@ namespace Engine {
|
||||
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 力在每步模拟后清零:游戏逻辑每帧持续施加 = 正确用法;
|
||||
// 冲量一次性改变动量(自动唤醒休眠体);速度直写会打断自然
|
||||
@@ -98,6 +104,12 @@ namespace Engine {
|
||||
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)
|
||||
@@ -126,7 +138,7 @@ namespace Engine {
|
||||
ECS::TransformComponent& tr);
|
||||
// 约束对账:owner 孤儿清理 / body 重建传染失效 / 建缺失与变更重建
|
||||
// invalidated = 本帧 SyncScene 中被销毁或重建 body 的实体位模式
|
||||
void SyncConstraints(const std::vector<u32>& invalidated);
|
||||
void SyncConstraints(const std::unordered_set<u32>& invalidated);
|
||||
void CreateConstraint(u32 ownerBits, u32 targetBits,
|
||||
const ECS::JointComponent& jc);
|
||||
void RemoveConstraint(u32 ownerBits);
|
||||
@@ -134,7 +146,8 @@ namespace Engine {
|
||||
// 角色非刚体(不进 PhysicsSystem),无 body 清理负担
|
||||
void SyncCharacters();
|
||||
// 每物理步(PhysicsSystem::Update 之后,Jolt 官方建议序):
|
||||
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)
|
||||
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)+ 合成角色
|
||||
// 碰撞事件(CharacterVirtual 不进 ContactListener,见函数注释)
|
||||
void UpdateCharacters(f32 dt);
|
||||
// Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape /
|
||||
// Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box)
|
||||
@@ -146,6 +159,10 @@ namespace Engine {
|
||||
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(多线程并发,加锁)
|
||||
@@ -161,9 +178,16 @@ namespace Engine {
|
||||
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;
|
||||
|
||||
@@ -183,6 +207,11 @@ namespace Engine {
|
||||
|
||||
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;
|
||||
|
||||
@@ -314,7 +314,10 @@ namespace Engine::RG {
|
||||
void ComputeLifetimes();
|
||||
void PrecomputeBarriers();
|
||||
void AllocatePhysical();
|
||||
void ReleasePhysical();
|
||||
// keepFixedSize = true:固定尺寸资源(desc 显式 width/height,与目标
|
||||
// 渲染尺寸无关)的物理镜像不归还池、保留在 entry 上——重编译后原样
|
||||
// 沿用同一 VkImage,内容存续(材质球预览 atlas 不因视口 resize 清空)
|
||||
void ReleasePhysical(bool keepFixedSize);
|
||||
void PurgePool(vk::Extent2D target);
|
||||
PhysicalImage* AcquirePhysical(vk::Format format, vk::Extent2D extent,
|
||||
vk::ImageUsageFlags usage,
|
||||
|
||||
@@ -26,15 +26,29 @@
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Window;
|
||||
namespace ECS { struct TransformComponent; }
|
||||
|
||||
// ---- 相机参数默认值(每实体相机参数的唯一初始来源;编辑器 UI /
|
||||
// 序列化缺省回退也引用它们,改默认值一处生效)----
|
||||
// 视场角(度,垂直 fov,Unity 语义;UE 默认水平 90° ≈ 垂直 45°@16:9)
|
||||
inline constexpr f32 kDefaultCameraFovDeg = 45.0f;
|
||||
// 裁剪面距离(米):near 主导深度精度,far 拉远到"无限"级不损近处
|
||||
inline constexpr f32 kDefaultCameraNear = 0.1f;
|
||||
inline constexpr f32 kDefaultCameraFar = 10000.0f;
|
||||
// 正交模式半垂直高度(Unity orthographic size 语义):世界单位米
|
||||
inline constexpr f32 kDefaultCameraOrthoSize = 10.0f;
|
||||
|
||||
class Camera {
|
||||
public:
|
||||
// 输入驱动。flyRequiresRMB=true(编辑器模式):移动键仅在 RMB
|
||||
// 按住时生效——解决与 Gizmo W/E/R 工具热键的键位冲突(UE 语义:
|
||||
// 飞行时键盘归相机)。全部写回传入的 transform
|
||||
void Update(Window& window, ECS::TransformComponent& transform,
|
||||
// 投影模式(UE/Unity Camera 语义:透视 = 近大远小;正交 =
|
||||
// 平行投影,工程图纸/2D/等距视角)
|
||||
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,相机沿当前
|
||||
@@ -44,8 +58,27 @@ namespace Engine {
|
||||
// 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose)
|
||||
glm::mat4 GetView(const ECS::TransformComponent& transform) const;
|
||||
|
||||
// Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度
|
||||
glm::mat4 GetProjection(f32 aspect, f32 nearZ, f32 farZ) const;
|
||||
// Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度。
|
||||
// fov/near/far/orthoSize/模式全部走成员(每实体参数),aspect 由
|
||||
// 视口推导不落盘(UE/Unity 同构)
|
||||
glm::mat4 GetProjection(f32 aspect) const;
|
||||
|
||||
// ---- 投影参数(Inspector/序列化直接读写)----
|
||||
void SetFovDeg(f32 fov) { m_FovDeg = glm::clamp(fov, 1.0f, 179.0f); }
|
||||
f32 GetFovDeg() const { return m_FovDeg; }
|
||||
// near/far 成对写:内部保证 far > near(UI 拖动交叉时防穿透)
|
||||
void SetClip(f32 nearZ, f32 farZ)
|
||||
{
|
||||
m_NearZ = glm::max(nearZ, 0.001f);
|
||||
m_FarZ = glm::max(farZ, m_NearZ + 0.01f);
|
||||
}
|
||||
f32 GetNearZ() const { return m_NearZ; }
|
||||
f32 GetFarZ() const { return m_FarZ; }
|
||||
// 正交半垂直高度(>0;aspect 由视口推导出全宽)
|
||||
void SetOrthoSize(f32 s) { m_OrthoSize = glm::max(s, 0.01f); }
|
||||
f32 GetOrthoSize() const { return m_OrthoSize; }
|
||||
void SetProjectionMode(ProjectionMode m) { m_ProjectionMode = m; }
|
||||
ProjectionMode GetProjectionMode() const { return m_ProjectionMode; }
|
||||
|
||||
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
|
||||
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
|
||||
@@ -63,6 +96,13 @@ namespace Engine {
|
||||
f32 m_MouseSensitivity = 0.1f;
|
||||
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};
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Window;
|
||||
|
||||
class EditorCamera {
|
||||
public:
|
||||
EditorCamera()
|
||||
@@ -38,10 +36,10 @@ namespace Engine {
|
||||
// 每帧输入驱动(编辑器形态由 Application 调用;游戏形态不调
|
||||
// 即静止)。flyRequiresRMB 恒 true:编辑器移动键只在 RMB 按住
|
||||
// 时生效(W/E/R 归 Gizmo,UE 语义)。暂停(timeScale=0)时
|
||||
// 仍可飞行——调用方传 unscaled dt
|
||||
void Update(Window& window, f32 deltaTime)
|
||||
// 仍可飞行——调用方传 unscaled dt。键鼠/光标捕获统一走 Input
|
||||
void Update(f32 deltaTime)
|
||||
{
|
||||
m_Controller.Update(window, m_Transform, deltaTime,
|
||||
m_Controller.Update(m_Transform, deltaTime,
|
||||
/*flyRequiresRMB=*/true);
|
||||
m_Transform.Compose(); // World() 是渲染/Gizmo 读源,随写随算
|
||||
}
|
||||
@@ -51,8 +49,9 @@ namespace Engine {
|
||||
// 一次性对齐版)。环绕 pivot 已失效,清除待 MMB 重建
|
||||
void AlignTo(const ECS::TransformComponent& target)
|
||||
{
|
||||
m_Transform.SetPosition(target.GetPosition());
|
||||
m_Transform.SetRotationQuat(target.GetRotationQuat());
|
||||
// world 位姿复制(目标实体可能在层级子树下)
|
||||
m_Transform.SetPosition(target.GetWorldPosition());
|
||||
m_Transform.SetRotationQuat(target.GetWorldRotationQuat());
|
||||
m_Transform.Compose();
|
||||
m_Controller.ResetOrbit();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#define HELLOVULKAN_MATERIAL_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Core/FrameContext.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <array>
|
||||
@@ -42,8 +43,55 @@ namespace Engine {
|
||||
// 材质因子(乘贴图;无贴图时即最终值)
|
||||
// x = metallic 因子 y = roughness 因子
|
||||
// z = 独立粗糙度图开关(1 = 用 Roughness 槽 R 通道替换 MR.g)
|
||||
// 注意:与 shader MaterialParams UBO 严格同序同布局(全 vec4,std140
|
||||
// 无填充歧义),加字段必须双端同步(simple_shader*.frag)
|
||||
struct MaterialParams {
|
||||
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f};
|
||||
// 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 {
|
||||
@@ -76,16 +124,37 @@ namespace Engine {
|
||||
// 透明队列归属(glTF alphaMode=BLEND;CPU 收集侧用)
|
||||
bool IsTransparent() const { return m_Transparent; }
|
||||
|
||||
// ---- 编辑器调参入口(零 GPU 重建路径)----
|
||||
// 参数直写:全 in-flight 槽同步覆盖写,与 ctor 初始化同一绑定
|
||||
// 语义(binding 覆盖写,ring 槽位共用同一 descriptor binding)。
|
||||
// 理论上存在 in-flight 帧读到半新半旧参数的撕裂窗口(动态偏移
|
||||
// ring 是根治演进点)——UI 段低频编辑 + 48B 小块 memcpy 下实践
|
||||
// 无感。纹理槽变化不走这里(那是重建路径,见 MaterialAsset)。
|
||||
// 实现外置(UniformBuffer 头文件内仅前置声明)
|
||||
void UpdateParams(const MaterialParams& params);
|
||||
// 队列归属切换(收集侧 CPU 标志,描述符不变):透明 ↔ 不透明
|
||||
void SetTransparent(bool t) { m_Transparent = t; }
|
||||
|
||||
// 材质缓存键:同贴图组合+同因子+同透明语义共享一份(Model 层使用)
|
||||
static std::string MakeCacheKey(const std::string paths[kSlotCount],
|
||||
const MaterialParams& params,
|
||||
bool transparent);
|
||||
|
||||
// ---- 预览缩略图身份(editor tile 池的缓存键,game 不消费)----
|
||||
// serial:出生唯一号(进程内单调递增,不复用)——裸指针做键有
|
||||
// ABA 风险(旧材质析构后新材质恰好分配到同一地址,池误判命中)
|
||||
u64 GetSerial() const { return m_Serial; }
|
||||
// editVersion:参数编辑代数,UpdateParams 每次 ++
|
||||
//(tile 池以此判定"缓存的球已过期需重渲")
|
||||
u32 GetEditVersion() const { return m_EditVersion; }
|
||||
|
||||
private:
|
||||
std::array<Texture*, kSlotCount> m_Textures{};
|
||||
vk::DescriptorSet m_DescriptorSet;
|
||||
Scope<UniformBuffer> m_ParamsBuffer; // per-slot 参数 ring(创建时写死)
|
||||
bool m_Transparent = false;
|
||||
u64 m_Serial = 0; // ctor 取号(定义见 Material.cpp)
|
||||
u32 m_EditVersion = 0;
|
||||
};
|
||||
} // namespace Engine
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/10/5.
|
||||
//
|
||||
// MaterialAsset:独立材质资产(.material 文件)+ GPU 资源封装。
|
||||
//
|
||||
// UE UMaterial 的资产化对应物。语义约定:
|
||||
// - 模型内建材质(Model 材质缓存持有,随 glTF/FBX 解析生成)只读,
|
||||
// 不存在"原地编辑"路径——改它会波及所有引用该模型的实体
|
||||
// - 可编辑材质 = .material 资产文件,被 MeshRenderer 的覆盖表
|
||||
// (mesh 下标 → 资产路径)按实体引用;资产被多实体共享时编辑
|
||||
// 同时生效(资产级语义),要实体独立就每实体一份文件
|
||||
// - 数据段(MaterialAssetData,磁盘 ↔ 内存纯值)与 GPU 段(纹理 +
|
||||
// Material)分离:GPU 段由 VulkanAssetUploader 构建,资产层与
|
||||
// 本类都不感知 Vulkan——依赖方向与 Model/资产层完全一致
|
||||
//
|
||||
// 扩展点(UV 动效/特效等后续迭代):
|
||||
// - 数据段加字段 + Version 递增,Load 全部 as<T>(default) 缺省
|
||||
// 兼容——旧文件永远可读,新字段对旧文件取默认值
|
||||
// - GPU 段参数经 Material::UpdateParams 直写 UBO,shader 侧
|
||||
// matParams 块同步扩展即可,本类结构不变
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_MATERIALASSET_H
|
||||
#define HELLOVULKAN_MATERIALASSET_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Material.h>
|
||||
// Texture 完整类型(非前置声明):成员持 Scope<Texture> 数组,若仅
|
||||
// 前置声明,所有实例化本类析构/构造的 TU 都需各自看到 Texture 完整
|
||||
// 类型——直接包含一劳永逸。Material.h 已含 vulkan.hpp,无新增重依赖
|
||||
#include <Engine/Vulkan/Texture.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class MemoryAllocator;
|
||||
|
||||
// 材质槽位默认纹理工厂(单点实现防漂移):Model 兜底纹理与
|
||||
// MaterialAsset 构建共用同一组 1x1 兜底语义
|
||||
//(albedo 白 / normal 平 / MR 中性 / emissive 黑 / AO 全遮蔽 / rough 0.5)
|
||||
Scope<Texture> CreateDefaultMaterialTexture(
|
||||
Device& device, MemoryAllocator& allocator, Material::Slot slot);
|
||||
|
||||
// 材质资产数据段(磁盘格式 ↔ 内存值,无 GPU 状态)。
|
||||
// texPaths = 文件系统相对 CWD 路径(与模型解析产物同源,glTF/FBX
|
||||
// 纹理可直接快照);空串 = 该槽用共享默认纹理兜底。
|
||||
// glb 内嵌纹理在模型导入期已抽到 <模型目录>/textures/ 落盘并回写为
|
||||
// 真实路径(Model::ParseFromDisk),故此处与文件纹理同语义——覆盖
|
||||
// 材质可正常继承
|
||||
struct MaterialAssetData {
|
||||
std::array<std::string, Material::kSlotCount> texPaths;
|
||||
MaterialParams params;
|
||||
bool transparent = false;
|
||||
|
||||
bool operator==(const MaterialAssetData&) const = default;
|
||||
};
|
||||
|
||||
class MaterialAsset {
|
||||
public:
|
||||
static constexpr u32 kVersion = 1;
|
||||
|
||||
// ---- 磁盘序列化(.material,YAML)----
|
||||
// 失败(无法打开/解析)返回 false;Load 缺省键兼容
|
||||
static bool Save(const std::string& path, const MaterialAssetData& data);
|
||||
static bool Load(const std::string& path, MaterialAssetData& out);
|
||||
|
||||
// GPU 段注入(仅 VulkanAssetUploader 调用):material = 组装好的
|
||||
// 材质(空槽已由 uploader 绑共享默认纹理)。纹理所有权归 uploader
|
||||
// 的路径缓存(跨材质去重),本资产只持有绑进 Material 的裸指针
|
||||
MaterialAsset(MaterialAssetData&& data, Scope<Material>&& material);
|
||||
// 析构外置(成员为重资源句柄,销毁序与 GPU 生命周期解耦遵循
|
||||
// Model/Material 既有模式)
|
||||
~MaterialAsset();
|
||||
|
||||
const MaterialAssetData& GetData() const { return m_Data; }
|
||||
Material* GetMaterial() const { return m_Material.get(); }
|
||||
|
||||
// ---- 编辑入口(Inspector 调;数据段与 GPU 段同步更新)----
|
||||
// 写盘时机由编辑器决定(参数松手落盘,纹理变化走资产重建)
|
||||
void EditParams(const MaterialParams& params);
|
||||
void SetTransparent(bool t);
|
||||
|
||||
private:
|
||||
MaterialAssetData m_Data;
|
||||
Scope<Material> m_Material;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_MATERIALASSET_H
|
||||
@@ -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,9 +10,11 @@
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Material.h>
|
||||
#include <Engine/Renderer/Mesh.h>
|
||||
#include <Engine/Renderer/MeshFactory.h> // 程序化几何唯一源(MeshData/PrimitiveDesc)
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
@@ -43,6 +45,8 @@ namespace Engine
|
||||
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 的物理数据源与
|
||||
@@ -51,6 +55,18 @@ namespace Engine
|
||||
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; };
|
||||
@@ -71,6 +87,9 @@ namespace Engine
|
||||
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
|
||||
@@ -96,7 +115,14 @@ namespace Engine
|
||||
std::vector<ModelNode> nodes;
|
||||
std::vector<ModelBone> bones;
|
||||
std::vector<AnimationClip> clips;
|
||||
bool Valid() const { return !meshes.empty(); }
|
||||
// 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
|
||||
@@ -112,6 +138,12 @@ namespace Engine
|
||||
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
|
||||
static ParsedModel ParseFromDisk(const std::string& path);
|
||||
|
||||
// 资产显示名(编辑器 UI / clip 命名的唯一来源):取文件 stem;
|
||||
// stem 无信息量(空 / 导入资产库的 "asset")时读同目录 pathname
|
||||
// 文件取原始资产 basename,再回退父目录名。避免 UI 直接显示
|
||||
// 哈希路径(用户看到的是 id)
|
||||
static std::string AssetDisplayName(const std::string& path);
|
||||
|
||||
// GPU 构建(必须主线程):纹理上传 / mesh 缓冲 / 材质描述符。
|
||||
// 解析失败返回 nullptr。
|
||||
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
|
||||
@@ -119,9 +151,12 @@ namespace Engine
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
ParsedModel&& parsed);
|
||||
|
||||
// 程序化模型:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
||||
// size 为半边长(平面总边长 = 2*size)。
|
||||
// 程序化几何标签:几何统一来自 MeshFactory(单一几何源),
|
||||
// 本类只负责把几何 + 纯色 albedo 组装成可渲染 Model
|
||||
struct ProceduralTag {};
|
||||
|
||||
// 程序化纯色平面:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
||||
// size 为半边长(平面总边长 = 2*size)。
|
||||
static Scope<Model> CreatePlane(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
@@ -133,12 +168,36 @@ namespace Engine
|
||||
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(const Model&) = delete;
|
||||
Model& operator=(const Model&) = delete;
|
||||
|
||||
const std::vector<Scope<Mesh>>& GetMeshes() const { return m_Meshes; }
|
||||
// 内建材质源信息(与 GetMeshes() 下标对齐;只读,见结构注释)
|
||||
const std::vector<MeshMaterialInfo>& GetMeshMaterialInfos() const
|
||||
{
|
||||
return m_MeshMaterialInfos;
|
||||
}
|
||||
// 全模型联合包围球(各 mesh bounds 的并集,局部空间):
|
||||
// 实体级粗剔除用——整个模型在视锥外直接跳过全部 mesh 循环
|
||||
const Mesh::Bounds& GetBounds() const { return m_Bounds; }
|
||||
@@ -147,15 +206,40 @@ namespace Engine
|
||||
bool HasAnimations() const { return !m_Clips.empty(); }
|
||||
u32 GetBoneCount() const { return static_cast<u32>(m_Bones.size()); }
|
||||
const std::vector<AnimationClip>& GetClips() const { return m_Clips; }
|
||||
// 外部动画并入(编辑器 attach 操作):追加到 clip 表尾部,
|
||||
// 既有 clipIndex 不受影响。仅运行时内存状态(重启后由场景
|
||||
// 里 Animation.attachedAnimPaths 驱动重放)
|
||||
void AppendClips(std::vector<AnimationClip>&& extra)
|
||||
{
|
||||
m_Clips.insert(m_Clips.end(),
|
||||
std::make_move_iterator(extra.begin()),
|
||||
std::make_move_iterator(extra.end()));
|
||||
}
|
||||
// 外部动画剥离(编辑器 detach 操作):删除 sourcePath 匹配的
|
||||
// clip,返回删除数。clipIndex/fadeFrom 下标悬空由消费端自愈
|
||||
//(AnimationSystem 每帧 clamp)
|
||||
u32 RemoveClipsBySource(const std::string& sourcePath)
|
||||
{
|
||||
const u32 before = static_cast<u32>(m_Clips.size());
|
||||
m_Clips.erase(
|
||||
std::remove_if(m_Clips.begin(), m_Clips.end(),
|
||||
[&](const AnimationClip& c)
|
||||
{ return c.sourcePath == sourcePath; }),
|
||||
m_Clips.end());
|
||||
return before - static_cast<u32>(m_Clips.size());
|
||||
}
|
||||
const std::vector<ModelNode>& GetNodes() const { return m_Nodes; }
|
||||
const std::vector<ModelBone>& GetBones() const { return m_Bones; }
|
||||
|
||||
private:
|
||||
// 程序化几何骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 顶点/索引
|
||||
// 由 MeshFactory 产出(见 .cpp)。name = 编辑器材质槽显示名。
|
||||
// 各具名工厂(CreatePlane/Cube/Primitive)都折到这一个 ctor
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
|
||||
bool transparent);
|
||||
ProceduralTag, MeshData&& geo, const std::array<u8, 4>& rgba,
|
||||
bool transparent, const std::string& name);
|
||||
|
||||
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
@@ -176,6 +260,8 @@ namespace Engine
|
||||
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,
|
||||
@@ -183,6 +269,8 @@ namespace Engine
|
||||
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 权重灌进顶点 + 模型级骨骼表注册去重
|
||||
@@ -192,6 +280,10 @@ namespace Engine
|
||||
std::unordered_map<std::string, u32>& boneSlots);
|
||||
void InitDefaultTextures();
|
||||
|
||||
// GLB 内嵌贴图字节("*N" → 编码数据):解析期拷出,纹理解析期
|
||||
// GetOrLoadTexture 消费(随实例生命周期,键同 texPaths 引用)
|
||||
std::unordered_map<std::string, std::vector<u8>> m_EmbeddedImages;
|
||||
|
||||
// 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除)
|
||||
void RebuildUnionBounds();
|
||||
|
||||
@@ -221,6 +313,9 @@ namespace Engine
|
||||
|
||||
std::vector<Scope<Mesh>> m_Meshes;
|
||||
Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除)
|
||||
// 与 m_Meshes 平行的内建材质快照(每条建 mesh 路径同步填充:
|
||||
// FromParsed / 同步 LoadModel / 程序化平面)
|
||||
std::vector<MeshMaterialInfo> m_MeshMaterialInfos;
|
||||
std::string m_ModelPath;
|
||||
bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV
|
||||
|
||||
|
||||
@@ -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,25 +20,38 @@ namespace Engine::Passes {
|
||||
ForwardPass(SceneGpu& gpu, FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||
RG::TextureHandle depth,
|
||||
RG::TextureHandle sceneColor,
|
||||
const std::array<RG::TextureHandle, kMaxCascades>& shadowMaps)
|
||||
: m_Gpu(gpu), m_Frame(frame),
|
||||
m_HDRColor(hdrColor), m_Velocity(velocity),
|
||||
m_Depth(depth), m_ShadowMaps(shadowMaps) {}
|
||||
m_Depth(depth), m_SceneColor(sceneColor),
|
||||
m_ShadowMaps(shadowMaps) {}
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
||||
|
||||
private:
|
||||
// 两段渲染共享的命令录制(shader/描述符绑定 + 几何/光栅/视口状态)。
|
||||
// translucent=true 绑定 gl_FragDepth=1.0 的透明 frag 变体;
|
||||
// translucent=true 绑定透射变体(折射采样场景色副本);
|
||||
// 深度写与混合状态由各渲染实例作为初始态设置,见 ForwardPass.cpp
|
||||
void RecordSharedState(const RG::RenderGraphContext& ctx,
|
||||
vk::CommandBuffer cmd, bool translucent) const;
|
||||
|
||||
// 透明队列绘制单位总数(= 各批实例数之和;排序后扁平单位序)
|
||||
u32 TransparentUnitCount() const;
|
||||
|
||||
// 绘制透明单位区间 [start,end)(批次内连续实例合并为一次 draw):
|
||||
// 透明渲染实例 + 预乘 alpha 混合 + 只测不写深度的全部状态
|
||||
void DrawTransparentRange(const RG::RenderGraphContext& ctx,
|
||||
vk::CommandBuffer cmd,
|
||||
u32 start, u32 end) const;
|
||||
|
||||
SceneGpu& m_Gpu;
|
||||
FrameSceneData& m_Frame;
|
||||
RG::TextureHandle m_HDRColor;
|
||||
RG::TextureHandle m_Velocity; // TAA 运动向量(第二颜色附件)
|
||||
RG::TextureHandle m_Depth;
|
||||
RG::TextureHandle m_SceneColor; // 场景色副本(透射折射采样源)
|
||||
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps;
|
||||
};
|
||||
|
||||
|
||||
@@ -36,7 +36,10 @@ namespace Engine {
|
||||
struct PostProcessParams {
|
||||
f32 exposure = 0.6f;
|
||||
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; // 叠加强度
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// "HitProxy" —— 拾取请求帧把不透明 + 透明 batch 用 id 管线画进
|
||||
// R32Uint 目标(片元值 = 实体位模式)。深度自测自写
|
||||
// (重清主深度):遮挡语义 = 可见表面胜出;运行在
|
||||
// Forward/ForwardTrans/PhysicsDebug 之后的 WAW 链尾,
|
||||
// Forward/ForwardTransL*/PhysicsDebug 之后的 WAW 链尾,
|
||||
// 主链下游无人再读深度,下帧 Forward 照常重新 Clear
|
||||
// "HitProxyCopy" —— 请求像素 1×1 CopyImageToBuffer 进 host-visible
|
||||
// 暂存。TransferSrc use 让 RG 自动完成
|
||||
@@ -30,6 +30,7 @@ namespace Engine {
|
||||
class Device;
|
||||
class ShaderManager;
|
||||
class DescriptorManager;
|
||||
class UniformBuffer;
|
||||
|
||||
namespace Passes {
|
||||
|
||||
@@ -54,6 +55,13 @@ namespace Engine {
|
||||
//(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;
|
||||
@@ -67,6 +75,15 @@ namespace Engine {
|
||||
// 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
|
||||
|
||||
@@ -26,10 +26,14 @@ namespace Engine {
|
||||
// presentTarget:ImGui::Image 采样的场景纹理(tonemap 输出)。
|
||||
// loadOp 按 displayMode 分支:Direct = Load(blit 已铺场景),
|
||||
// Viewport = Clear(唯一写入者,清"桌面底色")
|
||||
// previewAtlas:材质球预览 SDR atlas(MaterialPreviewPass 写、
|
||||
// ImGui::Image 按槽位采样;声明读 = eSampled usage 推导 +
|
||||
// ColorAttachment→ShaderRead barrier)
|
||||
ImGuiPass(ImGuiLayer** layer, FrameSceneData& frame,
|
||||
RG::TextureHandle backbuffer, RG::TextureHandle presentTarget)
|
||||
RG::TextureHandle backbuffer, RG::TextureHandle presentTarget,
|
||||
RG::TextureHandle previewAtlas)
|
||||
: m_Layer(layer), m_Frame(frame), m_Backbuffer(backbuffer),
|
||||
m_PresentTarget(presentTarget) {}
|
||||
m_PresentTarget(presentTarget), m_PreviewAtlas(previewAtlas) {}
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
|
||||
@@ -38,6 +42,7 @@ namespace Engine {
|
||||
FrameSceneData& m_Frame; // displayMode(帧契约)
|
||||
RG::TextureHandle m_Backbuffer;
|
||||
RG::TextureHandle m_PresentTarget;
|
||||
RG::TextureHandle m_PreviewAtlas;
|
||||
};
|
||||
|
||||
} // 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
|
||||
@@ -1,25 +1,28 @@
|
||||
//
|
||||
// PhysicsDebugPass:物理调试图元叠加(collider 线框/约束/重力/速度/
|
||||
// 加速度箭头)。
|
||||
// PhysicsDebugPass:调试图元叠加 pass(双源)。
|
||||
// 物理源 = PhysicsWorld 收集的 PhysicsDebugFrame(collider 线框/
|
||||
// 约束,Application 注入帧缓冲指针)
|
||||
// 通用源 = Engine::Debug 调试服务(脚本/任意层的 Line/Arrow/Wire*,
|
||||
// Unity Debug.DrawLine 同位;批次自带线宽/深度测试/拓扑)
|
||||
//
|
||||
// 画在 HDRColor(Forward 之后、TAA 之前):调试图元进入场景主链路——
|
||||
// TAA 时域累积抗锯齿、Bloom/Tonemap 正常消费,kDebugTransparent 诊断
|
||||
// 模式下也可见。深度只测不写(被不透明几何正确遮挡),无混合直写
|
||||
// (fullscreen pass 状态泄漏教训)。
|
||||
//
|
||||
// 数据源 = PhysicsWorld 每帧收集的 PhysicsDebugFrame(Application 把
|
||||
// PhysicsWorld 的帧缓冲指针注入 Renderer)。空帧零绘制;debug 开关
|
||||
// 全关时 PhysicsWorld 不收集 → 顶点数为 0 → 本 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 {
|
||||
@@ -44,7 +47,8 @@ namespace Engine {
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor,
|
||||
RG::TextureHandle depth,
|
||||
UniformBuffer& lineVB, UniformBuffer& triVB);
|
||||
UniformBuffer& lineVB, UniformBuffer& triVB,
|
||||
UniformBuffer& debugVB);
|
||||
~PhysicsDebugPass();
|
||||
|
||||
PhysicsDebugPass(const PhysicsDebugPass&) = delete;
|
||||
@@ -52,23 +56,36 @@ namespace Engine {
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
|
||||
// 每帧 Renderer::Render 里(该槽位 fence 等待后)上传当帧图元
|
||||
void PrepareFrame(u32 frameSlot, const PhysicsDebugFrame& src);
|
||||
// 每帧 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_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;
|
||||
};
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ namespace Engine {
|
||||
vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer,
|
||||
// SkinOffsets, SkinPalettes, EntityIds}
|
||||
vk::DescriptorSetLayout textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO}
|
||||
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube}
|
||||
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube,
|
||||
// prefiltered cube, BRDF LUT, 场景色副本}
|
||||
vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO)
|
||||
vk::PipelineLayout shadowLayout; // Set0,push = lightVP 64B
|
||||
|
||||
@@ -65,10 +66,23 @@ namespace Engine {
|
||||
std::vector<vk::DescriptorSet> uboSets;
|
||||
std::vector<vk::DescriptorSet> shadowSets;
|
||||
|
||||
// 材质预览锁定环境集(Set2 同布局,editor 专属——HELLO_EDITOR
|
||||
// 裁剪):级联/场景色副本随 BindShadowMaps/BindSceneColor 与
|
||||
// shadowSets 同步重绑(几何资源,无环境语义);IBL 三件套只在
|
||||
// LockPreviewEnvironment 绑定一次后锁定——材质球观察条件不随
|
||||
// 场景 SetEnvironment 漂移(UE ThumbnailRenderer 语义)
|
||||
#ifdef HELLO_EDITOR
|
||||
std::vector<vk::DescriptorSet> previewEnvSets;
|
||||
#endif
|
||||
|
||||
// 阴影采样器:clamp-to-border 白色 = 光视锥外自动无阴影
|
||||
//(省 shader 出界分支)
|
||||
vk::Sampler shadowSampler;
|
||||
|
||||
// 场景色副本采样器(binding 6):普通线性 clamp
|
||||
//(阴影采样器带硬件比较,不可用于常规 sampler2D)
|
||||
vk::Sampler sceneSampler;
|
||||
|
||||
// ---- MDI per-slot ring ----
|
||||
// 布局:[main | cascade0 | cascade1 | cascade2],每区
|
||||
// kMaxInstances 条命令预算(每实例至多 1 条命令,必不越界)。
|
||||
@@ -86,9 +100,17 @@ namespace Engine {
|
||||
void BindShadowMaps(RG::RenderGraph& rg,
|
||||
const std::array<RG::TextureHandle, kMaxCascades>& maps);
|
||||
|
||||
// RG 重编译后重绑场景色副本(Set2 binding 6,透射采样源)
|
||||
void BindSceneColor(RG::RenderGraph& rg, RG::TextureHandle sceneColor);
|
||||
|
||||
// 环境辐照度(Set2 binding 3):view 全程稳定,绑一次即可
|
||||
void BindEnvironment(const Cubemap& env);
|
||||
|
||||
// 材质预览锁定环境:只绑 previewEnvSets 的 IBL 三件套
|
||||
//(binding 3/4/5),一次调用永久生效;须在 Renderer 初始化
|
||||
//(环境尚为中性兜底)时调用
|
||||
void LockPreviewEnvironment(const Cubemap& env);
|
||||
|
||||
private:
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
|
||||
@@ -35,7 +35,8 @@ namespace Engine {
|
||||
// env 可为空(HDR 资产缺失):仅承担清屏,天空保持清屏色
|
||||
SkyboxPass(Device& device, ShaderManager& shaders,
|
||||
DescriptorManager& descriptors, FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor, const Cubemap* env);
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||
const Cubemap* env);
|
||||
~SkyboxPass();
|
||||
|
||||
SkyboxPass(const SkyboxPass&) = delete;
|
||||
@@ -49,10 +50,11 @@ namespace Engine {
|
||||
DescriptorManager& m_Descriptors;
|
||||
FrameSceneData& m_Frame;
|
||||
RG::TextureHandle m_HDRColor;
|
||||
RG::TextureHandle m_Velocity; // 天空 velocity 附件(TAA)
|
||||
const Cubemap* m_Env; // 可空
|
||||
|
||||
vk::DescriptorSetLayout m_SetLayout; // Set0 = {samplerCube}
|
||||
vk::PipelineLayout m_Layout; // push = invVP 64B
|
||||
vk::PipelineLayout m_Layout; // push = {invVP, prevVP} 128B
|
||||
ShaderManager::ShaderRef m_Vert, m_Frag; // 析构自动销毁
|
||||
std::array<vk::DescriptorSet, kMaxFramesInFlight> m_Sets{};
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Engine::Passes {
|
||||
public:
|
||||
TAAPass(FullscreenGpu& gpu, FrameSceneData& frame, f32 historyAlpha,
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||
RG::TextureHandle taaColor);
|
||||
RG::TextureHandle depth, RG::TextureHandle taaColor);
|
||||
~TAAPass() override = default;
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
@@ -34,6 +34,7 @@ namespace Engine::Passes {
|
||||
f32 m_HistoryAlpha; // 开启时的历史权重
|
||||
RG::TextureHandle m_HDR;
|
||||
RG::TextureHandle m_Velocity;
|
||||
RG::TextureHandle m_Depth; // 场景深度(深度感知 dilation 用)
|
||||
RG::TextureHandle m_TAAColor; // 输出 = 下一帧的历史
|
||||
|
||||
std::vector<vk::DescriptorSet> m_Sets; // per-slot
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Core/FrameContext.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
#include <Engine/Renderer/Passes/MaterialPreviewPass.h> // PreviewTilePool/PreviewDirtyTile(完整类型:成员 vector)
|
||||
#include <Engine/RenderGraph/RenderGraph.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <array>
|
||||
@@ -54,6 +55,7 @@ namespace Engine {
|
||||
namespace Passes {
|
||||
class RenderPass; class SceneGpu; class FullscreenGpu;
|
||||
class PostProcessParams; class PhysicsDebugPass; class HitProxyPass;
|
||||
class EditorGizmoPass;
|
||||
}
|
||||
|
||||
class Renderer {
|
||||
@@ -98,6 +100,30 @@ namespace Engine {
|
||||
// 渲染分辨率(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)----
|
||||
f32 GetExposure() const;
|
||||
void SetExposure(f32 e);
|
||||
@@ -118,6 +144,15 @@ namespace Engine {
|
||||
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;
|
||||
void SetDebugView(int v); // Passes::DebugView 枚举值
|
||||
// 调试视图名表(下标 = DebugView 枚举值,ImGui combo 直接枚举)
|
||||
@@ -142,6 +177,7 @@ namespace Engine {
|
||||
// ---- 显示模式(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 完整类型)
|
||||
@@ -164,9 +200,15 @@ namespace Engine {
|
||||
std::optional<u32> TakePickResult();
|
||||
|
||||
// ---- Editor 选中描边(UE 编辑器选中轮廓)----
|
||||
// UI 段选中变化时调用(bits = 实体位模式,0 = 清选中);
|
||||
// 下一帧 SelectionMask 只画该实体,Outline 在其轮廓叠加描边色
|
||||
void SetSelectedEntity(u32 entityBits);
|
||||
// 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),
|
||||
// 只应在帧 UI 构建段调用(Render 之前)
|
||||
@@ -182,6 +224,9 @@ namespace Engine {
|
||||
void HandleResize(bool recreateSwapchain);
|
||||
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;
|
||||
@@ -205,11 +250,22 @@ namespace Engine {
|
||||
// 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 构造
|
||||
Scope<IblGenerator> m_Ibl;
|
||||
// 中性兜底环境(IBL 四件套):Application 未加载/加载失败时保底;
|
||||
// 声明在 m_Passes 之前 → 析构晚于 pass(引用安全)
|
||||
Scope<Cubemap> m_Environment;
|
||||
// 当前绑定环境的资产路径(帧数据 envPath 差量同步基准;空 = 兜底)
|
||||
std::string m_ActiveEnvPath;
|
||||
Scope<Swapchain> m_Swapchain;
|
||||
Scope<SyncManager> m_SyncManager;
|
||||
Scope<CommandManager> m_CommandManager;
|
||||
@@ -226,6 +282,7 @@ namespace Engine {
|
||||
RG::TextureHandle m_Depth{};
|
||||
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps{}; // CSM 级联
|
||||
RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一
|
||||
RG::TextureHandle m_SceneColor{}; // 不透明段快照(透射折射采样源)
|
||||
RG::TextureHandle m_Velocity{}; // Forward 输出(clip 空间运动向量)
|
||||
RG::TextureHandle m_HitProxyId{}; // 拾取 id 目标(R32Uint,片元写实体位模式)
|
||||
RG::TextureHandle m_SelectionMask{}; // 选中描边 mask(R8Unorm,选中像素=1)
|
||||
@@ -233,6 +290,9 @@ namespace Engine {
|
||||
RG::TextureHandle m_BloomA{}; // 半分辨率:提取 + 垂直模糊结果
|
||||
RG::TextureHandle m_BloomB{}; // 半分辨率:水平模糊中间结果
|
||||
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 层访问器转发)
|
||||
Scope<Passes::PostProcessParams> m_PostParams;
|
||||
@@ -245,6 +305,44 @@ namespace Engine {
|
||||
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 可读)
|
||||
vk::QueryPool m_QueryPool;
|
||||
u32 m_QueriesPerFrame = 0; // 每帧槽位的查询数 = pass 数 × 2
|
||||
@@ -262,6 +360,14 @@ namespace Engine {
|
||||
// ---- 帧首同步的意图缓冲(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;
|
||||
};
|
||||
|
||||
|
||||
@@ -23,10 +23,13 @@
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Mesh.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
#include <Engine/Animation/AnimationSystem.h>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <Engine/Renderer/EditorCamera.h>
|
||||
|
||||
@@ -65,6 +68,19 @@ namespace Engine {
|
||||
void SetLodEnabled(bool b) { m_LodEnabled = b; }
|
||||
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; }
|
||||
@@ -73,14 +89,34 @@ namespace Engine {
|
||||
{
|
||||
m_PendingPick = PickRequest{true, ndc};
|
||||
}
|
||||
// Editor 选中实体(描边):UI 段写入,BuildFrame 落帧并筛出绘制
|
||||
void SetSelectedEntity(u32 entityBits) { m_PendingSelected = entityBits; }
|
||||
// Editor 选中实体(描边):UI 段写入,BuildFrame 落帧并筛出绘制。
|
||||
// nullopt = 清选中(实体 0 号位模式 = 0,不能拿位值兼作哨兵)
|
||||
void SetSelectedEntity(std::optional<u32> entityBits)
|
||||
{
|
||||
m_PendingSelected = entityBits;
|
||||
}
|
||||
|
||||
private:
|
||||
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
|
||||
//(含纹素对齐防阴影边缘闪烁)。写入(Shadow pass push)与采样
|
||||
//(Forward UBO)共用同一组矩阵。
|
||||
void ComputeCascades(const glm::vec3& shadowDir);
|
||||
//(Forward UBO)共用同一组矩阵。camNear/camFar 取自当帧渲染源
|
||||
// 相机(每实体参数)
|
||||
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 区间表。
|
||||
@@ -91,7 +127,7 @@ namespace Engine {
|
||||
FrameSceneData m_Frame;
|
||||
EditorCamera m_EditorCamera; // 编辑器相机(视口 UI 状态,见 GetEditorCamera)
|
||||
PickRequest m_PendingPick{}; // 拾取请求缓冲(BuildFrame 落帧后清除)
|
||||
u32 m_PendingSelected = 0; // 选中实体缓冲(描边)
|
||||
std::optional<u32> m_PendingSelected; // 选中实体缓冲(nullopt = 无选中)
|
||||
|
||||
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
|
||||
struct ViewScratch {
|
||||
@@ -102,24 +138,43 @@ namespace Engine {
|
||||
u32 skinBase = 0; // palette 区段基址(0 = identity 槽)
|
||||
u32 entityBits = 0; // 实体位模式(HitProxy id pass 消费)
|
||||
};
|
||||
// 链表尾哨兵:kMaxInstances 远小于 u32 最大值,安全
|
||||
static constexpr u32 kNoInstance = 0xFFFFFFFFu;
|
||||
CullResult result;
|
||||
// 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 =
|
||||
// firstInstance + 连续区间,实体序收集不满足(多实体共享 mesh
|
||||
// 时实例分散各处),扁平化成连续数组推迟到 appendView
|
||||
std::vector<std::vector<InstanceMatrices>> batchMats;
|
||||
// 每 batch 实例距离累加(透明排序用:平均距离降序 = 远→近)
|
||||
// 扁平实例数组(所有 batch 的实例共用一份连续内存)。
|
||||
// 实体收集序下同 batch 实例会散布(多实体共享 mesh 交错插入),
|
||||
// 组内连接关系由 instanceNext 链表维护——appendView 时按 batch
|
||||
// 聚合拷成连续区间(firstInstance 连续语义的前提)。大场景下
|
||||
// 避免 vector<vector> 的小块分配,每帧 clear() 复用容量
|
||||
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()
|
||||
{
|
||||
result.batches.clear();
|
||||
result.instanceCount = 0;
|
||||
batchMats.clear();
|
||||
instances.clear(); // 容量保留,复用
|
||||
instanceNext.clear();
|
||||
batchHead.clear();
|
||||
batchDistSum.clear();
|
||||
for (auto& e : lookup) e.fill(0); // 容量保留,置零复用
|
||||
ovrLookup.clear();
|
||||
matVariants.clear();
|
||||
}
|
||||
};
|
||||
ViewScratch m_MainScratch;
|
||||
@@ -129,22 +184,19 @@ namespace Engine {
|
||||
// ---- TAA 时域状态 ----
|
||||
u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样)
|
||||
glm::mat4 m_LastView{1.0f}; // 上一帧视图(velocity)
|
||||
glm::mat4 m_LastProjNoJitter{1.0f}; // 上一帧未抖动投影(velocity)
|
||||
glm::mat4 m_LastProjNoJitter{1.0f}; // 上一帧未抖动投影(velocity 用)
|
||||
// 实体的上一帧世界矩阵(key = entt 实体 integral)。
|
||||
// 每帧重建 m_CurWorlds 后与 m_PrevWorlds swap——自动清理销毁实体
|
||||
std::unordered_map<u32, glm::mat4> m_PrevWorlds;
|
||||
std::unordered_map<u32, glm::mat4> m_CurWorlds;
|
||||
// 用 sorted vector + binary_search 替代 unordered_map:
|
||||
// unordered_map 在每帧大量 emplace 时 rehash 抖动明显,
|
||||
// vector 连续内存 + 单次 sort 对 cache 友好且无 rehash
|
||||
std::vector<std::pair<u32, glm::mat4>> m_PrevWorlds;
|
||||
std::vector<std::pair<u32, glm::mat4>> m_CurWorlds;
|
||||
|
||||
// ---- 骨骼动画 scratch(帧首重建/容量复用)----
|
||||
// entKey → palette 区段基址(第 2.5 节预采样填,收集段查)
|
||||
std::unordered_map<u32, u32> m_EntitySkinBase;
|
||||
// 采样层 A(当前 clip)/ B(fade 来源 clip)与 global 累乘
|
||||
std::vector<glm::vec3> m_PosePosA, m_PosePosB;
|
||||
std::vector<glm::quat> m_PoseRotA, m_PoseRotB;
|
||||
std::vector<glm::vec3> m_PoseSclA, m_PoseSclB;
|
||||
std::vector<u8> m_TouchedA, m_TouchedB;
|
||||
std::vector<glm::mat4> m_Globals;
|
||||
bool m_SkinTruncWarned = false; // palette 预算溢出只告警一次
|
||||
// ---- 骨骼动画(调度收口 Animation::AnimationSystem,本类只
|
||||
// 转发调用 + 收集段只读查 frame.entitySkinBases)----
|
||||
Animation::AnimationSystem m_AnimSystem;
|
||||
// 材质资产加载缺失的一次性告警节流(按路径;发现即插表)
|
||||
std::unordered_set<std::string> m_WarnedMaterialMisses;
|
||||
|
||||
bool m_TruncationWarned = false; // 实例缓冲打满只告警一次
|
||||
|
||||
@@ -154,7 +206,22 @@ namespace Engine {
|
||||
bool m_BloomEnabled = true;
|
||||
bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过)
|
||||
int m_DebugView = 0;
|
||||
int m_DisplayMode = kDisplayViewport; // Editor 视口为默认形态
|
||||
// 引擎默认 = 游戏直渲形态(与 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
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
@@ -20,10 +21,13 @@ namespace Engine {
|
||||
class Mesh;
|
||||
class Material;
|
||||
|
||||
// GPUDriven:按 (mesh, LOD) 聚合的绘制批——同 mesh 不同 LOD 的实例
|
||||
// 索引区间不同,不能并成一次 draw
|
||||
// GPUDriven:按 (mesh, 材质, LOD) 聚合的绘制批——同 mesh 不同 LOD
|
||||
// 的实例索引区间不同不能并成一次 draw;同 mesh 不同材质(实体级
|
||||
// 覆盖)描述符集不同,同样必须分批
|
||||
struct InstanceBatch {
|
||||
Mesh* mesh;
|
||||
Material* material = nullptr; // 收集期 resolve 的实际材质
|
||||
//(覆盖材质或 mesh 内建;shadow 组不绑材质但同表记录)
|
||||
u32 first; // 在实例矩阵数组中的起始下标
|
||||
u32 count; // 实例数
|
||||
u32 lod = 0; // 绘制用索引区间(Mesh::Draw)
|
||||
@@ -92,6 +96,47 @@ namespace Engine {
|
||||
// 位模式,永不与有效实体冲突
|
||||
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;
|
||||
@@ -119,9 +164,9 @@ namespace Engine {
|
||||
LightData lights[kMaxLights];
|
||||
|
||||
// ---- TAA(velocity 在 vertex 里算)----
|
||||
glm::mat4 projNoJitter; // 未抖动投影(velocity 当前帧 clip 用)
|
||||
glm::mat4 projNoJitter; // 未抖动投影(天空盒反投影/折射同位采样/静态预览)
|
||||
glm::mat4 prevView; // 上一帧视图(velocity)
|
||||
glm::mat4 prevProj; // 上一帧未抖动投影(velocity)
|
||||
glm::mat4 prevProj; // 上一帧"已抖动"投影(velocity 历史 UV 对齐)
|
||||
glm::vec4 cameraPos; // 相机世界位置(高光 V 向量)
|
||||
};
|
||||
|
||||
@@ -138,13 +183,30 @@ namespace Engine {
|
||||
struct FrameSceneData {
|
||||
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;
|
||||
// 本帧生效的环境贴图资产路径(来自第一个 active 的
|
||||
// SkyboxComponent;空 = 无环境 → Renderer 回中性兜底 cubemap)。
|
||||
// Renderer 据此与当前已绑定环境做差量同步(路径变化才重载)
|
||||
std::string envPath;
|
||||
|
||||
// 天空盒反投影矩阵:inverse(projNoJitter × mat4(mat3(view)))
|
||||
//(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push)
|
||||
glm::mat4 skyInvVP{1.0f};
|
||||
|
||||
// 天空盒 velocity 用:上一帧未抖动 proj × 上一帧视图(去平移)。
|
||||
// 天空是"无限远"表面,只受相机旋转影响:frag 把当前世界方向过
|
||||
// 此矩阵得上一帧屏幕位置,差即天空的运动向量(相机移动时天空/
|
||||
// 背景不再闪烁的根治点——此前天空从不写 velocity = 恒 0)
|
||||
glm::mat4 skyPrevVP{1.0f};
|
||||
|
||||
// 实例矩阵数组 = [主视图段 | 阴影视图段](共享同一 per-slot SSBO)
|
||||
std::vector<glm::mat4> instanceModels;
|
||||
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
|
||||
@@ -158,6 +220,10 @@ namespace Engine {
|
||||
// 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 目标)
|
||||
@@ -167,9 +233,12 @@ namespace Engine {
|
||||
// HitProxy/HitProxyCopy 两 pass 据此决定是否绘制/拷贝)
|
||||
PickRequest pickRequest{};
|
||||
|
||||
// 选中描边(UE 编辑器语义):选中实体的位模式(0 = 无选中)。
|
||||
// 选中描边(UE 编辑器语义):选中实体 + 位模式。位模式不能兼作
|
||||
// "无选中"哨兵——entt 首个实体 0 号的 to_integral == 0("点击
|
||||
// 场景第一个创建的实体没有描边"的根因),有效性与位值解耦。
|
||||
// BuildFrame 据此从实例表筛出 selectionDraws,SelectionMask pass
|
||||
// 只画选中实体 → Outline pass 在 mask 边缘叠加描边色
|
||||
bool selectedEntityValid = false;
|
||||
u32 selectedEntityBits = 0;
|
||||
std::vector<SelectionDraw> selectionDraws;
|
||||
|
||||
@@ -196,6 +265,31 @@ namespace Engine {
|
||||
// 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 耗时(遥测)
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
|
||||
#include <Engine/Assets/IAssetUploader.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
@@ -37,11 +40,36 @@ namespace Engine {
|
||||
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
|
||||
Scope<Model> CreateTransparentPlane(f32 halfSize,
|
||||
u8 r, u8 g, u8 b, u8 a) override;
|
||||
Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) override;
|
||||
Scope<Model> CreatePrimitive(const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b) override;
|
||||
Scope<Cubemap> LoadEnvironment(const std::string& path) override;
|
||||
Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||
DecodedEquirect&& decoded) override;
|
||||
// 析构外置(成员持 Scope<Texture> 数组等重资源,遵循
|
||||
// Model/Material 既有模式)
|
||||
~VulkanAssetUploader() override;
|
||||
|
||||
// ---- 材质资产(.material 数据段 → GPU 段)----
|
||||
// 每槽经路径纹理缓存(跨材质去重)取纹理,同批新纹理合并为一次
|
||||
// submit;空/缺失回退共享默认纹理,再组 Material
|
||||
Scope<MaterialAsset> CreateMaterial(MaterialAssetData&& data) override;
|
||||
// 设备空闲屏障(编辑器资产重建路径:卸载旧 GPU 资源前全停飞)
|
||||
void WaitDeviceIdle() override;
|
||||
|
||||
private:
|
||||
// 共享默认纹理集(全部材质资产共用一份,惰性创建;
|
||||
// 单点实现 = MaterialAsset.cpp 的 CreateDefaultMaterialTexture)
|
||||
void InitSharedDefaultTextures();
|
||||
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures{};
|
||||
|
||||
// 路径纹理缓存:键 = 归一化路径 + sRGB 标记 → 独占纹理。多个
|
||||
// 材质引用同一贴图(仓库 4K 贴图被多材质共享)只解码/上传一次。
|
||||
// 生命周期长于所有 MaterialAsset(uploader 在 assets 之后析构),
|
||||
// 故 Material 可安全持裸指针。DDS 与 stbi 同缓存
|
||||
std::unordered_map<std::string, Scope<Texture>> m_TextureCache;
|
||||
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
DescriptorManager& m_Descriptors;
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
// 基类接口——将来接 Lua/C# 时各实现一种 foreign behavior(内部持
|
||||
// VM 状态),引擎零改动。
|
||||
//
|
||||
// 瘦基类(对齐 Unity6/UE5 现役形态):基类只供生命周期 + 参数 +
|
||||
// 实体访问器,零领域 API——物理住 PhysicsWorld(App().GetPhysics()
|
||||
// 直调),动画住 AnimationComponent(GetComponent 直调,Animator
|
||||
// 同形)。DeclareParams = C# 反射缺失下 UPROPERTY 的等价物:Unity
|
||||
// 里你从不写声明是因为语言替你做了,C++ 里每个引擎都得补这一环
|
||||
//(UE 让你标 UPROPERTY,这里让你写一行 Declare)。
|
||||
//
|
||||
// 组件只存 scriptName 字符串(POD,序列化天然支持:Lua/C# 同样按名
|
||||
// 解析);实例由 ScriptRuntime 对账式管理(照 PhysicsWorld 模式),
|
||||
// 不进组件——组件可拷贝/可比较。
|
||||
@@ -21,10 +28,13 @@
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/ECS/EntityHandle.h> // 须在 Scene.h 之后(内联方法需完整 Scene)
|
||||
#include <Engine/Scripting/ScriptParam.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
@@ -39,11 +49,43 @@ namespace Engine {
|
||||
// 注册表键(= 组件 scriptName),须与 ENGINE_REGISTER_SCRIPT 名一致
|
||||
virtual const char* Name() const = 0;
|
||||
|
||||
// 生命周期(吃 scaled dt:编辑器暂停 timeScale=0 时自然停摆)
|
||||
// 生命周期(吃 scaled dt:编辑器暂停 timeScale=0 时自然停摆)。
|
||||
// 顺序对齐 Unity:OnCreate(= Awake,实例化即调)→ OnEnable
|
||||
// → OnStart(首次 OnUpdate 前一次)→ OnUpdate* → OnDisable →
|
||||
// OnDestroy(可能因实体销毁触发,句柄已失效只做自身清理)。
|
||||
// 跨实体 OnEnable 顺序不保证(实例表无序),与 Unity 一致
|
||||
virtual void OnCreate() {}
|
||||
virtual void OnEnable() {}
|
||||
virtual void OnStart() {}
|
||||
virtual void OnUpdate(f32 dt) {}
|
||||
virtual void OnDisable() {}
|
||||
virtual void OnDestroy() {}
|
||||
|
||||
// 参数声明(实例化灌值与注册表 schema 缓存共用入口;外语绑定
|
||||
// 同键接入)。契约:只写成员指针与常量,禁触场景/应用——
|
||||
// schema 缓存路径用未 Bind 的临时实例调用
|
||||
virtual void DeclareParams(ParamBuilder& pb) { (void)pb; }
|
||||
|
||||
// 参数灌值后回调(外语侧收值接缝:C++ 成员已直写,VM 绑定
|
||||
// 在此按名取值)
|
||||
virtual void OnParamsApplied() {}
|
||||
|
||||
// ---- 碰撞回调(PhysicsWorld::GetEvents 帧首派发,ScriptRuntime
|
||||
// 转发;事件语义 = 最近一次物理步进)----
|
||||
// Enter/Exit 由 Jolt Added/Removed 转发;Persisted 不回调
|
||||
//(最小 API,接触持续期无每帧噪音)。对方是触发器(sensor)
|
||||
// 时同样触发——后续按需拆 OnTriggerEnter/Exit。
|
||||
// other = 对方实体位模式(还原:static_cast<entt::entity>(other),
|
||||
// 回调时对方可能已销毁,用前自行 Scene::IsAlive 校验)。
|
||||
// 派发在 Sync/OnUpdate 之前:回调里 spawn/destroy 安全
|
||||
//(destroy 入延迟队列,spawn 由下一帧对账消化)
|
||||
virtual void OnCollisionEnter(u32 other,
|
||||
const glm::vec3& point,
|
||||
const glm::vec3& normal) {
|
||||
(void)other; (void)point; (void)normal;
|
||||
}
|
||||
virtual void OnCollisionExit(u32 other) { (void)other; }
|
||||
|
||||
protected:
|
||||
// 便捷访问:绑定实体的 TransformComponent
|
||||
ECS::TransformComponent& Transform() {
|
||||
@@ -60,7 +102,8 @@ namespace Engine {
|
||||
return m_Scene->GetRegistry().try_get<T>(m_Entity);
|
||||
}
|
||||
|
||||
// 绑定实体的 entityBits(物理门面按键值寻址)
|
||||
// 绑定实体的 entityBits(PhysicsWorld 按此键寻址:直调
|
||||
// App().GetPhysics() 的各 API 时作第一个参数)
|
||||
u32 EntityBits() const {
|
||||
return static_cast<u32>(entt::to_integral(m_Entity));
|
||||
}
|
||||
@@ -70,6 +113,98 @@ namespace Engine {
|
||||
|
||||
bool Valid() const { return m_Scene != nullptr && m_Entity != entt::null; }
|
||||
|
||||
// ---- 场景实体 spawn/destroy 底座(Unity Instantiate/Destroy
|
||||
// 同位)----
|
||||
// spawn:帧内即时创建(UUID + Transform + Name 出生即有),
|
||||
// 组件由脚本自行 AddComponent 装配;RigidBody/ScriptComponent
|
||||
// 分别由 PhysicsWorld/ScriptRuntime 下一帧对账消化
|
||||
//
|
||||
// 【选型约定:Spawn vs Instantiate】(Unity/UE 同题,靠约定
|
||||
// 治理,判定只有一问):
|
||||
// "这东西的参数要不要让非程序员改?"
|
||||
// 要 → Save As Prefab + Instantiate(数据 = 真理来源)
|
||||
// 不要 → Spawn(代码 = 真理来源:子弹/特效触发器等纯逻辑体)
|
||||
// 三条纪律:
|
||||
// 1. 一个对象只走一条路径——prefab 化后 Spawn 版本撤注册;
|
||||
// 2. Spawn 的脚本构造保持"零参数搭建":发现 Spawn 后手动
|
||||
// set 一堆组件 = 该 prefab 化的信号;
|
||||
// 3. 注册表即白名单(ENGINE_REGISTER_SCRIPT 的类才能按名
|
||||
// spawn),别为绕过约定注册"伪逻辑体"。
|
||||
ECS::EntityHandle Spawn(const std::string& name);
|
||||
// destroy:延迟——入队,帧尾 FlushDestroyQueue 统一执行
|
||||
//(遍历安全);调用方持有的句柄此后悬空
|
||||
void Destroy(ECS::Entity e);
|
||||
|
||||
// ---- Prefab 实例化(Unity Object.Instantiate 同位)----
|
||||
// .prefab 文件 → 整棵子树追加进场景(UUID 全量再生,子树内
|
||||
// Joint.target 自动重映射)。pos/rot = 根实体 local 位姿
|
||||
//(根挂场景根时即世界位姿)。失败返回 entt::null;
|
||||
// RigidBody/ScriptComponent 由 PhysicsWorld/ScriptRuntime
|
||||
// 下一帧对账消化(与 Spawn 同语义)
|
||||
ECS::Entity Instantiate(const std::string& prefabPath,
|
||||
const glm::vec3& pos,
|
||||
const glm::quat& rot = glm::quat{});
|
||||
|
||||
// ---- 组件启停(Unity SetActive / Behaviour.enabled 语义)----
|
||||
// 自身组件总开关(整个脚本组件停 tick,实例保留)
|
||||
void SetActive(bool on) {
|
||||
if (auto* sc = m_Scene->GetRegistry().try_get<ECS::ScriptComponent>(m_Entity)) {
|
||||
sc->active = on;
|
||||
}
|
||||
}
|
||||
bool IsActive() const {
|
||||
const auto* sc =
|
||||
m_Scene->GetRegistry().try_get<ECS::ScriptComponent>(m_Entity);
|
||||
return sc != nullptr && sc->active;
|
||||
}
|
||||
|
||||
// ---- 自身参数(写成员 + 写穿 Item.params:PIE Inspector
|
||||
// 实时反映,Stop 由快照天然回滚;首次写新名一次分配)----
|
||||
// 返回成员直写是否命中(未声明参数 false,存档仍记录——前向
|
||||
// 兼容)。跨脚本/跨实体访问走 App().GetScripts().SetParam/GetParam
|
||||
bool SetParam(const std::string& name, const ParamValue& v) {
|
||||
const bool hit = WriteParam(name, v);
|
||||
if (auto* sc = Valid()
|
||||
? m_Scene->GetRegistry().try_get<ECS::ScriptComponent>(m_Entity)
|
||||
: nullptr) {
|
||||
const auto item = std::find_if(
|
||||
sc->items.begin(), sc->items.end(),
|
||||
[&](const ECS::ScriptComponent::Item& it) {
|
||||
return it.name == Name();
|
||||
});
|
||||
if (item != sc->items.end()) {
|
||||
const auto pit = std::find_if(
|
||||
item->params.begin(), item->params.end(),
|
||||
[&](const ScriptParam& p) { return p.name == name; });
|
||||
if (pit != item->params.end()) {
|
||||
pit->value = v;
|
||||
} else {
|
||||
item->params.push_back({name, v});
|
||||
}
|
||||
}
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
template<typename T>
|
||||
bool SetParam(const std::string& name, const T& v) {
|
||||
return SetParam(name, ParamValue{v});
|
||||
}
|
||||
bool GetParam(const std::string& name, ParamValue& out) const {
|
||||
return ReadParam(name, out);
|
||||
}
|
||||
template<typename T>
|
||||
bool GetParam(const std::string& name, T& out) const {
|
||||
ParamValue v;
|
||||
if (!ReadParam(name, v)) {
|
||||
return false;
|
||||
}
|
||||
if (auto* p = std::get_if<T>(&v.v)) {
|
||||
out = *p;
|
||||
return true;
|
||||
}
|
||||
return false; // 类型不符
|
||||
}
|
||||
|
||||
private:
|
||||
friend class ScriptRuntime;
|
||||
void Bind(ECS::Scene& scene, entt::entity e, Application* app) {
|
||||
@@ -77,10 +212,19 @@ namespace Engine {
|
||||
m_Entity = e;
|
||||
m_App = app;
|
||||
}
|
||||
// 参数绑定表交接(实例化/RefreshParams 时由运行时填充)
|
||||
void SetParamBinds(std::vector<ParamBuilder::Bind> binds) {
|
||||
m_ParamBinds = std::move(binds);
|
||||
}
|
||||
// 按名写/读成员(经绑定表;未声明或类型不符返回 false)。
|
||||
// 写穿 Item.params 由运行时侧完成(需查组件项)
|
||||
bool WriteParam(const std::string& name, const ParamValue& v);
|
||||
bool ReadParam(const std::string& name, ParamValue& out) const;
|
||||
|
||||
ECS::Scene* m_Scene = nullptr;
|
||||
entt::entity m_Entity = entt::null;
|
||||
Application* m_App = nullptr;
|
||||
std::vector<ParamBuilder::Bind> m_ParamBinds;
|
||||
};
|
||||
|
||||
// ---- 按名注册表:脚本类文件级自注册,运行时按名实例化 ----
|
||||
@@ -108,6 +252,28 @@ namespace Engine {
|
||||
return v;
|
||||
}
|
||||
|
||||
// 参数 schema(惰性缓存):临时实例调 DeclareParams 取声明
|
||||
// ——每脚本类一次性(注册先于 main,加载期必就绪)。未注册
|
||||
// 脚本 = 空 schema(读端未知键全忽略,弱前向兼容)
|
||||
static const std::vector<ParamBuilder::Desc>&
|
||||
Schema(const std::string& name) {
|
||||
static const std::vector<ParamBuilder::Desc> kEmpty;
|
||||
static std::unordered_map<std::string,
|
||||
std::vector<ParamBuilder::Desc>>
|
||||
cache;
|
||||
const auto it = cache.find(name);
|
||||
if (it != cache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
std::vector<ParamBuilder::Desc> descs;
|
||||
if (auto behavior = Create(name)) {
|
||||
ParamBuilder pb;
|
||||
behavior->DeclareParams(pb);
|
||||
descs = pb.Descs();
|
||||
}
|
||||
return cache.emplace(name, std::move(descs)).first->second;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::unordered_map<std::string, Factory>& Map() {
|
||||
static std::unordered_map<std::string, Factory> m;
|
||||
@@ -119,7 +285,12 @@ namespace Engine {
|
||||
} // namespace Engine
|
||||
|
||||
// 文件级自注册:脚本 cpp 里写一行即可,Inspector 下拉与序列化按
|
||||
// 类名(字符串)解析。T 须默认可构造并继承 ScriptBehavior
|
||||
// 类名(字符串)解析。T 须默认可构造并继承 ScriptBehavior。
|
||||
//
|
||||
// 注册 = UE 的 UCLASS(Blueprintable) 表态:本类声明为"逻辑体"——
|
||||
// 允许运行时按名 Spawn(脚本 Spawn()/运行时注册表白名单)。需要
|
||||
// 美术调参/编辑器复用的组合体不要注册(走 Save As Prefab +
|
||||
// Instantiate 路径),防止同一对象出现 Spawn/prefab 两条权威链
|
||||
#define ENGINE_REGISTER_SCRIPT(T) \
|
||||
static const bool s_##T##_registered = [] { \
|
||||
::Engine::Scripting::ScriptRegistry::Register( \
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//
|
||||
// ScriptParam.h:脚本参数——按名变体值 + 声明式 schema 构建器。
|
||||
//
|
||||
// 设计本质(UE/Unity 共识):参数数据按名存在序列化层(组件
|
||||
// ScriptComponent::Item.params,随场景持久化),C++ 脚本成员只是运行时
|
||||
// 真值缓存——这条边界正是将来 Lua/C# 外语绑定的接缝:
|
||||
// - C++ 脚本:DeclareParams 里 Field(name, &成员, 默认值) 双写声明
|
||||
// (灌值直写成员,零回收代码);
|
||||
// - 外语脚本:Declare(name, 类型, 默认值) 无指针声明,值经
|
||||
// OnParamsApplied 回收(VM 侧按名取)。
|
||||
//
|
||||
// schema 由注册表按脚本类名惰性缓存(ScriptRegistry::Schema)——描述
|
||||
// 里不含指针,缓存无悬垂;成员指针(Bind)只随实例存亡,不进缓存。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_SCRIPT_PARAM_H
|
||||
#define HELLOVULKAN_SCRIPT_PARAM_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
namespace Scripting {
|
||||
|
||||
// 参数类型(序列化分支 + Inspector 控件选择)。枚举序 = variant
|
||||
// 臂序(ParamValue::Type() 直转);新类型 = 枚举尾插 + 追加臂,
|
||||
// 旧档位前向兼容
|
||||
enum class ParamType : u8 { F32, I32, Bool, String, Vec2, Vec3 };
|
||||
|
||||
// 类型 → 枚举映射(Field 重载与消费端共用)
|
||||
template<typename T> struct ParamTypeOf;
|
||||
template<> struct ParamTypeOf<f32> { static constexpr ParamType value = ParamType::F32; };
|
||||
template<> struct ParamTypeOf<i32> { static constexpr ParamType value = ParamType::I32; };
|
||||
template<> struct ParamTypeOf<bool> { static constexpr ParamType value = ParamType::Bool; };
|
||||
template<> struct ParamTypeOf<std::string> { static constexpr ParamType value = ParamType::String; };
|
||||
template<> struct ParamTypeOf<glm::vec2> { static constexpr ParamType value = ParamType::Vec2; };
|
||||
template<> struct ParamTypeOf<glm::vec3> { static constexpr ParamType value = ParamType::Vec3; };
|
||||
|
||||
// 单参数的存储形态(类型标签由 variant 臂序自持)
|
||||
struct ParamValue {
|
||||
std::variant<f32, i32, bool, std::string, glm::vec2, glm::vec3>
|
||||
v{f32{0.0f}};
|
||||
|
||||
ParamType Type() const { return static_cast<ParamType>(v.index()); }
|
||||
|
||||
template<typename T>
|
||||
static constexpr ParamType TypeOf() { return ParamTypeOf<T>::value; }
|
||||
|
||||
bool operator==(const ParamValue&) const = default;
|
||||
};
|
||||
|
||||
// 单参数(组件 Item.params 的元素,随场景序列化)
|
||||
struct ScriptParam {
|
||||
std::string name;
|
||||
ParamValue value;
|
||||
|
||||
bool operator==(const ScriptParam&) const = default;
|
||||
};
|
||||
|
||||
// 参数声明构建器:DeclareParams(pb) 里逐项 Field/Declare,产出
|
||||
// 描述列表(schema,持久)+ 绑定表(成员指针,瞬态)。
|
||||
// Field 即把声明默认写进成员——未灌值前成员就是默认值
|
||||
class ParamBuilder {
|
||||
public:
|
||||
// ---- C++ 侧:声明 + 绑定成员(灌值直写)----
|
||||
void Field(std::string_view name, f32* dst, f32 def) {
|
||||
*dst = def;
|
||||
AddDesc(name, ParamType::F32, ParamValue{def});
|
||||
m_Binds.push_back({std::string(name), ParamType::F32, dst});
|
||||
}
|
||||
void Field(std::string_view name, i32* dst, i32 def) {
|
||||
*dst = def;
|
||||
AddDesc(name, ParamType::I32, ParamValue{def});
|
||||
m_Binds.push_back({std::string(name), ParamType::I32, dst});
|
||||
}
|
||||
void Field(std::string_view name, bool* dst, bool def) {
|
||||
*dst = def;
|
||||
AddDesc(name, ParamType::Bool, ParamValue{def});
|
||||
m_Binds.push_back({std::string(name), ParamType::Bool, dst});
|
||||
}
|
||||
void Field(std::string_view name, std::string* dst,
|
||||
const std::string& def) {
|
||||
*dst = def;
|
||||
AddDesc(name, ParamType::String, ParamValue{def});
|
||||
m_Binds.push_back({std::string(name), ParamType::String, dst});
|
||||
}
|
||||
void Field(std::string_view name, glm::vec2* dst,
|
||||
const glm::vec2& def) {
|
||||
*dst = def;
|
||||
AddDesc(name, ParamType::Vec2, ParamValue{def});
|
||||
m_Binds.push_back({std::string(name), ParamType::Vec2, dst});
|
||||
}
|
||||
void Field(std::string_view name, glm::vec3* dst,
|
||||
const glm::vec3& def) {
|
||||
*dst = def;
|
||||
AddDesc(name, ParamType::Vec3, ParamValue{def});
|
||||
m_Binds.push_back({std::string(name), ParamType::Vec3, dst});
|
||||
}
|
||||
|
||||
// ---- 外语侧:仅声明不绑定(值经 OnParamsApplied 回收)----
|
||||
void Declare(std::string_view name, ParamType t, ParamValue def) {
|
||||
AddDesc(name, t, std::move(def));
|
||||
}
|
||||
|
||||
// 声明描述(持久——进注册表 schema 缓存,无指针无悬垂)
|
||||
struct Desc {
|
||||
std::string name;
|
||||
ParamType type;
|
||||
ParamValue def;
|
||||
};
|
||||
// 成员绑定(瞬态——指针随声明实例存亡,只进 ScriptBehavior)
|
||||
struct Bind {
|
||||
std::string name;
|
||||
ParamType type;
|
||||
void* ptr;
|
||||
};
|
||||
|
||||
const std::vector<Desc>& Descs() const { return m_Descs; }
|
||||
std::vector<Bind> TakeBinds() { return std::move(m_Binds); }
|
||||
|
||||
// 单绑定写值:类型不符返回 false(SetParam/跨脚本写共用)
|
||||
static bool WriteBind(const Bind& b, const ParamValue& v) {
|
||||
if (b.type != v.Type()) {
|
||||
return false;
|
||||
}
|
||||
switch (b.type) {
|
||||
case ParamType::F32:
|
||||
*static_cast<f32*>(b.ptr) = std::get<f32>(v.v);
|
||||
return true;
|
||||
case ParamType::I32:
|
||||
*static_cast<i32*>(b.ptr) = std::get<i32>(v.v);
|
||||
return true;
|
||||
case ParamType::Bool:
|
||||
*static_cast<bool*>(b.ptr) = std::get<bool>(v.v);
|
||||
return true;
|
||||
case ParamType::String:
|
||||
*static_cast<std::string*>(b.ptr) =
|
||||
std::get<std::string>(v.v);
|
||||
return true;
|
||||
case ParamType::Vec2:
|
||||
*static_cast<glm::vec2*>(b.ptr) = std::get<glm::vec2>(v.v);
|
||||
return true;
|
||||
case ParamType::Vec3:
|
||||
*static_cast<glm::vec3*>(b.ptr) = std::get<glm::vec3>(v.v);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 单绑定读值(按绑定类型出值;GetParam/跨脚本读共用)
|
||||
static bool ReadBind(const Bind& b, ParamValue& out) {
|
||||
switch (b.type) {
|
||||
case ParamType::F32:
|
||||
out = ParamValue{*static_cast<f32*>(b.ptr)};
|
||||
return true;
|
||||
case ParamType::I32:
|
||||
out = ParamValue{*static_cast<i32*>(b.ptr)};
|
||||
return true;
|
||||
case ParamType::Bool:
|
||||
out = ParamValue{*static_cast<bool*>(b.ptr)};
|
||||
return true;
|
||||
case ParamType::String:
|
||||
out = ParamValue{*static_cast<std::string*>(b.ptr)};
|
||||
return true;
|
||||
case ParamType::Vec2:
|
||||
out = ParamValue{*static_cast<glm::vec2*>(b.ptr)};
|
||||
return true;
|
||||
case ParamType::Vec3:
|
||||
out = ParamValue{*static_cast<glm::vec3*>(b.ptr)};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 按名灌值进绑定成员:命中→直写;未绑定(外语声明)→跳过;
|
||||
// 类型不符/多余键→忽略(读端已校正,这里兜底)。
|
||||
// 缺失键不写——Field 已把声明默认写进成员
|
||||
void Apply(const std::vector<ScriptParam>& params) const {
|
||||
for (const auto& p : params) {
|
||||
const auto b = std::find_if(
|
||||
m_Binds.begin(), m_Binds.end(),
|
||||
[&](const Bind& it) { return it.name == p.name; });
|
||||
if (b != m_Binds.end()) {
|
||||
WriteBind(*b, p.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void AddDesc(std::string_view name, ParamType t, ParamValue def) {
|
||||
assert(!name.empty() && "参数名不能为空");
|
||||
assert(std::none_of(m_Descs.begin(), m_Descs.end(),
|
||||
[&](const Desc& d) {
|
||||
return d.name == name;
|
||||
}) &&
|
||||
"参数重名");
|
||||
m_Descs.push_back({std::string(name), t, std::move(def)});
|
||||
}
|
||||
|
||||
std::vector<Desc> m_Descs;
|
||||
std::vector<Bind> m_Binds;
|
||||
};
|
||||
|
||||
} // namespace Scripting
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_SCRIPT_PARAM_H
|
||||
@@ -1,7 +1,14 @@
|
||||
//
|
||||
// ScriptRuntime.h:脚本生命周期驱动。对账式管理(组件加/删/改名 ↔
|
||||
// 实例建/毁),照 PhysicsWorld.SyncScene 模式——每帧对账,编辑器操作
|
||||
// 即时生效,场景加载/实体销毁自动消化。
|
||||
// ScriptRuntime.h:脚本生命周期驱动。对账式管理(组件项加/删/改名 ↔
|
||||
// 实例建/毁),照 PhysicsWorld.SyncScene 模式——Running 中每帧对账,
|
||||
// PIE 中组件增删/改名即时生效,场景加载/实体销毁自动消化。
|
||||
//
|
||||
// Play Mode 语义(Unity/UE PIE 对齐):编辑态不实例化任何脚本——
|
||||
// 实例生命周期 = 一次 PIE 局,Play 后首次 Update 对账重建,Stop 时
|
||||
// EndPlay 统一终局,成员状态不跨局泄漏。
|
||||
//
|
||||
// 多脚本:一个实体可挂任意多个脚本项(ScriptComponent::items),实例
|
||||
// 按名字与 items 对账——重命名/删除走 OnDestroy 清理,未动的实例不重建。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_SCRIPT_RUNTIME_H
|
||||
@@ -12,6 +19,7 @@
|
||||
#include <Engine/Scripting/Script.h>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
@@ -19,10 +27,13 @@ namespace Engine {
|
||||
|
||||
namespace Scripting {
|
||||
|
||||
// 实例簿记:行为对象 + OnCreate 是否已调
|
||||
// 实例簿记:行为对象 + 生命周期位(created = OnCreate 已调;
|
||||
// enabled = 当前启停态,翻转检测基准;started = OnStart 已发)
|
||||
struct ScriptInstance {
|
||||
Scope<ScriptBehavior> behavior;
|
||||
bool created = false;
|
||||
bool enabled = false;
|
||||
bool started = false;
|
||||
};
|
||||
|
||||
class ScriptRuntime {
|
||||
@@ -30,15 +41,40 @@ namespace Engine {
|
||||
explicit ScriptRuntime(ECS::Scene& scene, Application* app = nullptr)
|
||||
: m_Scene(scene), m_App(app) {}
|
||||
|
||||
// 每帧调用(吃 scaled dt):对账 → OnCreate(新实例)→ OnUpdate
|
||||
// 每帧调用(吃 scaled dt):对账 → OnCreate(新实例)→
|
||||
// 启停翻转(OnEnable/OnDisable)→ OnStart(首次)→ OnUpdate。
|
||||
// 仅 Running 态被调(Application 门控)
|
||||
void Update(f32 dt);
|
||||
|
||||
// 终局(GameFlow →Stopped 广播):全部实例 OnDisable →
|
||||
// OnDestroy 并清空簿记。Unity 退出 Play 语义——成员状态
|
||||
// (动量/边沿/当前 clip)不跨局泄漏,下次 Play 对账重建
|
||||
void EndPlay();
|
||||
|
||||
// 参数热刷新(编辑期一次性):实体全部实例重新 DeclareParams
|
||||
// + 按组件存档灌值(Inspector 编辑参数后调)
|
||||
void RefreshParams(u32 entityBits);
|
||||
|
||||
// 跨脚本按名参数访问(本实体兄弟项或跨实体):SetParam 写
|
||||
// 目标实例成员 + 写穿 Item.params(PIE Inspector 实时反映);
|
||||
// GetParam 读目标实例成员。实体无组件/项不存在/实例未建
|
||||
// 返回 false
|
||||
bool SetParam(u32 entityBits, const std::string& scriptName,
|
||||
const std::string& param, const ParamValue& value);
|
||||
bool GetParam(u32 entityBits, const std::string& scriptName,
|
||||
const std::string& param, ParamValue& out) const;
|
||||
|
||||
private:
|
||||
void Sync();
|
||||
// 碰撞事件派发(Update 开头、Sync 之前):PhysicsWorld 最近
|
||||
// 一次步进的事件转发到涉事实例的 OnCollisionEnter/Exit。
|
||||
// Persisted 跳过;实例未建/已禁用不收
|
||||
void DispatchCollisions();
|
||||
|
||||
ECS::Scene& m_Scene;
|
||||
Application* m_App; // 注入脚本上下文
|
||||
std::unordered_map<u32, ScriptInstance> m_Instances; // entityBits
|
||||
// entityBits → 实例列表(成员按名字与 items 对齐,无序)
|
||||
std::unordered_map<u32, std::vector<ScriptInstance>> m_Instances;
|
||||
};
|
||||
|
||||
} // namespace Scripting
|
||||
|
||||
@@ -8,21 +8,24 @@
|
||||
// 场景内资产引用双写 {Guid, Path}:GUID 是主键(重命名/移动资产不断
|
||||
// 引用),Path 是人工可读提示 + 注册表缺失时的解析兜底。
|
||||
//
|
||||
// 引擎层身份:本类不感知编辑器(kEnvPaths 表)与渲染器——
|
||||
// environmentPath 以普通字符串经参数进出,消费方(编辑器/游戏运行时)
|
||||
// 自行解释。一套 API 服务两个消费者:编辑器 File 菜单 与
|
||||
// 游戏运行时动态加载,不存在"另一套序列化"。
|
||||
// 引擎层身份:本类不感知编辑器与渲染器。环境 = 场景实体组件
|
||||
//(SkyboxComponent/SkyLightComponent,UE Skybox/SkyLight Actor 语义),
|
||||
// 随实体序列化,场景级仅存清屏色。
|
||||
//
|
||||
// 前向兼容(Prefab 路线预留):
|
||||
// - 场景 = 实体节点列表,Prefab 复用同一套实体读写函数
|
||||
// - Version 闸:高版本文件拒载并 WARN,低版本宽容
|
||||
// - 未知组件名跳过(Unity 语义:新版场景在旧版引擎打开可玩)
|
||||
// - UUID 重复 → 重新生成(即 Prefab 实例化的 ID 重映射)
|
||||
// - 旧档 v1 环境语义(文档头 Environment + Settings.AmbientColor/
|
||||
// DrawSkybox)→ 自动补建 Environment 实体,视觉等价迁移
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_SCENESERIALIZER_H
|
||||
#define HELLOVULKAN_SCENESERIALIZER_H
|
||||
|
||||
#include <Engine/ECS/Scene.h> // Entity + glm vec3/quat(Prefab API 签名用)
|
||||
#include <Engine/Core/AssetRef.h> // ModelRef(ResolveProcedural 返回值)
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Engine::ECS { class Scene; }
|
||||
@@ -30,31 +33,74 @@ namespace Engine::Assets { class AssetManager; }
|
||||
|
||||
namespace Engine::Serialization {
|
||||
|
||||
// 当前场景文件格式版本(字段/结构变更时递增)
|
||||
inline constexpr int kSceneVersion = 1;
|
||||
// 当前场景文件格式版本(字段/结构变更时递增)。
|
||||
// 2 = 层级(可选键 Parent: <uuid>,缺省 = 根实体,旧档零迁移)
|
||||
inline constexpr int kSceneVersion = 2;
|
||||
|
||||
class SceneSerializer {
|
||||
public:
|
||||
// ---- 保存 ----
|
||||
// scene 序列化到 outPath(YAML)。environmentPath 可空(不写
|
||||
// Environment 字段)。失败(无法写文件)返回 false。
|
||||
// scene 序列化到 outPath(YAML)。失败(无法写文件)返回 false。
|
||||
static bool Serialize(const ECS::Scene& scene,
|
||||
Assets::AssetManager& assets,
|
||||
const std::string& outPath,
|
||||
const std::string& environmentPath = {});
|
||||
const std::string& outPath);
|
||||
|
||||
// ---- 加载 ----
|
||||
// 原子换血:先解析校验到中间结构,全部成功才 scene.Clear() 重建
|
||||
//(解析失败不动现场)。成功时 outEnvironmentPath 带回场景记录的
|
||||
// 环境路径(空 = 场景未带环境,消费方保持当前环境即可)。
|
||||
//(解析失败不动现场)。
|
||||
// 健壮性:单实体损坏跳过继续、缺字段取默认值、未知组件跳过、
|
||||
// UUID 重复重新生成、YAML 异常优雅报错。
|
||||
// 注意:调用方持有的 Entity/EntityHandle 在成功后全部悬空,
|
||||
// 必须重解析(Scene::GetEntityHandle / 按 UUID 查询)。
|
||||
static bool Deserialize(ECS::Scene& scene,
|
||||
Assets::AssetManager& assets,
|
||||
const std::string& inPath);
|
||||
|
||||
// ---- 内存快照(同一套实体读写,不同载体)----
|
||||
// 消费者:编辑器 Play-In-Editor 快照恢复、Prefab、运行时动态
|
||||
// 拼装场景。documentName 仅写入文档头 "Scene" 字段(注释性)。
|
||||
static std::string SerializeToString(
|
||||
const ECS::Scene& scene,
|
||||
Assets::AssetManager& assets,
|
||||
const std::string& documentName = {});
|
||||
|
||||
// 失败(YAML 异常/版本过高)返回 false 且不动现场。
|
||||
// 实体悬空语义同文件版 Deserialize。
|
||||
static bool DeserializeFromString(ECS::Scene& scene,
|
||||
Assets::AssetManager& assets,
|
||||
const std::string& yamlText);
|
||||
|
||||
// ---- Prefab(实体子树存取;与场景共用实体段读写,实现见 .cpp)----
|
||||
|
||||
// 保存 root 子树(根 + 全部后代,DFS 序:父先于子)到 outPath。
|
||||
// 格式 = 场景实体段子集:"Prefab:" 文档头 + Version + Entities
|
||||
// 列表(组件段与场景逐键一致)。首实体 = 根(实例化约定)。
|
||||
// 失败(根无效/无法写文件)返回 false
|
||||
static bool SerializePrefab(const ECS::Scene& scene,
|
||||
Assets::AssetManager& assets,
|
||||
ECS::Entity root,
|
||||
const std::string& outPath);
|
||||
|
||||
// 实例化 Prefab 到 scene(追加式,不换血)。UUID 全量再生——
|
||||
// 同一 prefab 多次实例化、与场景既有实体均不冲突;子树内
|
||||
// Joint.target 按旧→新重映射,指向子树外的引用保持原值。
|
||||
// overridePos/overrideRot 非空时覆写根实体 local 位姿(根挂
|
||||
// 场景根时即世界位姿);空指针 = 保留 prefab 授权位姿。
|
||||
// 返回根实体(= 文件首实体);失败(解析失败/无实体)返回
|
||||
// entt::null——逐实体容错与场景加载同语义,失败前可能已部分
|
||||
// 实例化(单实体损坏只跳过该项)
|
||||
static ECS::Entity InstantiatePrefab(
|
||||
ECS::Scene& scene, Assets::AssetManager& assets,
|
||||
const std::string& inPath,
|
||||
std::string& outEnvironmentPath);
|
||||
const glm::vec3* overridePos = nullptr,
|
||||
const glm::quat* overrideRot = nullptr);
|
||||
|
||||
// procedural:// 伪路径 → 参数反解重建(cube/plane/
|
||||
// transparent-plane)。确定性键必然命中缓存;绝不走 assimp。
|
||||
// 未知/格式坏伪路径返回无效句柄。场景加载与编辑器程序化
|
||||
// 入口共用——键格式唯一定义点
|
||||
static ModelRef ResolveProcedural(
|
||||
Assets::AssetManager& assets, const std::string& path);
|
||||
};
|
||||
|
||||
} // namespace Engine::Serialization
|
||||
|
||||
@@ -45,6 +45,11 @@ public:
|
||||
bool IsSynchronization2Supported() const { return m_Synchronization2Supported; }
|
||||
// MDI:drawCount>1 的 drawIndexedIndirect(Count) 必需(core 特性位)
|
||||
bool IsMultiDrawIndirectSupported() const { return m_MultiDrawIndirectSupported; }
|
||||
// 各向异性过滤:纹理采样器清晰度(掠射角材质锐利度)的硬件开关
|
||||
bool IsSamplerAnisotropySupported() const { return m_SamplerAnisotropySupported; }
|
||||
float MaxSamplerAnisotropy() const { return m_MaxSamplerAnisotropy; }
|
||||
bool IsWideLinesSupported() const { return m_WideLinesSupported; }
|
||||
float MaxLineWidth() const { return m_MaxLineWidth; }
|
||||
|
||||
private:
|
||||
void CreateInstance();
|
||||
@@ -76,6 +81,10 @@ private:
|
||||
bool m_Synchronization2Supported = false;
|
||||
bool m_MultiDrawIndirectSupported = false;
|
||||
bool m_DrawIndirectCountSupported = false;
|
||||
bool m_SamplerAnisotropySupported = false;
|
||||
float m_MaxSamplerAnisotropy = 1.0f;
|
||||
bool m_WideLinesSupported = false;
|
||||
float m_MaxLineWidth = 1.0f; // 设备线宽上限(像素,limits.lineWidthRange[1])
|
||||
|
||||
std::vector<const char*> m_ValidationLayers = { "VK_LAYER_KHRONOS_validation" };
|
||||
std::vector<const char*> m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
|
||||
|
||||
@@ -31,6 +31,10 @@ namespace Engine {
|
||||
|
||||
class Device;
|
||||
|
||||
// 显存估算(按格式字节宽与 mip 链逐级累加;BC 压缩按 4x4 块计)——
|
||||
// 分配器台账与 Texture 资源台账共用同一算法,两边永不漂移
|
||||
u64 EstimateImageBytes(vk::Format fmt, u32 w, u32 h, u32 depth, u32 mips);
|
||||
|
||||
class MemoryAllocator {
|
||||
public:
|
||||
explicit MemoryAllocator(Device& device);
|
||||
|
||||
@@ -35,6 +35,25 @@ namespace Engine {
|
||||
static Scope<Texture> CreateSolidColorRGBA(Device& device, MemoryAllocator& allocator,
|
||||
u8 r, u8 g, u8 b, u8 a, bool srgb = true);
|
||||
|
||||
// 内存编码图像(PNG/JPEG 原始字节)→ 纹理:GLB 内嵌贴图
|
||||
//(assimp "*<index>" 约定)的解析端,复用 InitFromPixels
|
||||
// 公共上传路径。解码失败返回空(调用方折到默认纹理)
|
||||
static Scope<Texture> CreateFromEncodedMemory(
|
||||
Device& device, MemoryAllocator& allocator,
|
||||
const void* data, u32 bytes, bool srgb);
|
||||
|
||||
// 批式上传(一次 submit + 一次 fence 等待服务同批多张贴图,省掉
|
||||
// 每张各自的命令池创建销毁与阻塞等待):解码 + 建 image/view/
|
||||
// sampler + 把拷贝/barrier 录制进调用方 cmd(**不**提交)。staging
|
||||
// 暂存对象内,调用方在批量 submit fence 通过后逐张
|
||||
// ReleasePendingStaging 释放;析构亦兜底释放。DDS(稀有)仍走
|
||||
// 自带提交,不参与批量
|
||||
static Scope<Texture> CreateDeferred(Device& device,
|
||||
MemoryAllocator& allocator,
|
||||
const std::string& path, bool srgb,
|
||||
vk::CommandBuffer cmd);
|
||||
void ReleasePendingStaging();
|
||||
|
||||
~Texture();
|
||||
|
||||
Texture(const Texture&) = delete;
|
||||
@@ -54,9 +73,20 @@ namespace Engine {
|
||||
Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
|
||||
const std::array<u8, 4>& rgba, bool srgb);
|
||||
|
||||
// 公共路径:staging → device local image → view/sampler(RGBA8 单 mip)
|
||||
// 公共路径:staging → device local image → blit 生成完整 mip 链 →
|
||||
// view/sampler(RGBA8,各向异性过滤)。录制进 cmd 后自行提交等待
|
||||
void InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb);
|
||||
|
||||
// 批式路径构造(cmd 由调用方提供,不在本构造内提交)
|
||||
Texture(Device& device, MemoryAllocator& allocator,
|
||||
const std::string& path, bool srgb, vk::CommandBuffer cmd);
|
||||
|
||||
// 公共上传路径核心:建 staging/image/view/sampler + 逐 mip
|
||||
// barrier/copy/blit 录制进 cmd(不提交、不销毁 staging,staging 存
|
||||
// m_PendingStaging)。InitFromPixels 与批式路径共用
|
||||
void RecordPixelsUpload(const void* pixels, u32 width, u32 height,
|
||||
bool srgb, vk::CommandBuffer cmd);
|
||||
|
||||
// DDS 路径:压缩数据直传(BC 硬件采样解码)+ 文件自带 mip 链
|
||||
void InitFromDds(const std::string& path, bool srgb);
|
||||
|
||||
@@ -75,6 +105,10 @@ namespace Engine {
|
||||
|
||||
u64 m_TextureBytes = 0;
|
||||
|
||||
// 批式上传暂存的 staging(submit fence 通过前不可释放)
|
||||
bool m_HasPendingStaging = false;
|
||||
MemoryAllocator::Allocation m_PendingStaging{};
|
||||
|
||||
MemoryAllocator::Allocation m_ImageAllocation;
|
||||
MemoryAllocator& m_Allocator;
|
||||
};
|
||||
|
||||
@@ -41,6 +41,11 @@ namespace Engine {
|
||||
// 写入指定帧槽位的 UBO(仅在该槽位 fence 确认完成后调用)
|
||||
void Update(u32 frameSlot, const void* data, u64 size) const;
|
||||
|
||||
// 偏移写入(同槽位 fence 契约同上)。多段数据共享同一 buffer 时
|
||||
// 用——gizmo 线流 + 骨骼线流拼同一顶点缓冲
|
||||
void UpdateAt(u32 frameSlot, u64 offset, const void* data,
|
||||
u64 size) const;
|
||||
|
||||
private:
|
||||
std::vector<Scope<Buffer>> m_Buffers;
|
||||
u64 m_Size = 0;
|
||||
|
||||
@@ -0,0 +1,638 @@
|
||||
//
|
||||
// AnimationAttach 实现:外部动画资产 → 目标模型并入。
|
||||
//
|
||||
// 五段管线(映射表一次构建后**冻结**,逐 clip 只读消费,杜绝循环内
|
||||
// 惰性登记/反向表重建):
|
||||
//
|
||||
// ① 资产导入(Model.cpp) clip 命名与重定向解耦(文件 stem 真名)
|
||||
// ② 映射表(HumanoidRig) 同名直映 / 52 槽配对 → 冻结 Plan
|
||||
// ③ 根运动通道 骨架上方"承载节点"(未识别的映射骨祖先)
|
||||
// 取最深者 → dst 根,世界增量 Δ·RdstRest
|
||||
// ④ 关节烘焙 两遍式 T-pose 参考搬运(见下数学);手指/
|
||||
// 趾与主干同通道(Unity/UE 语义:指骨即普通
|
||||
// 骨骼,无独立局部增量通道——局部系约定免疫)
|
||||
//
|
||||
// 关键不变量(本次重写的核心,此前版本违反即整体崩坏):
|
||||
// * Plan 构建后不可变;不存在 clip 循环内的 emplace 登记
|
||||
// * 每个 dst 节点至多一条轨道(owner 去重)——杜绝同节点重复轨道
|
||||
// * "承载节点"= 未识别的骨骼祖先(骨架上方),恒不参与同名直搬;
|
||||
// 判定基于**槽位识别**而非"所有映射骨祖先"(后者会把 hips/spine
|
||||
// 等内部骨全部误判成承载节点 → 大半骨架走烘焙而叶走直搬 → 局部
|
||||
// 化基准错位、全身崩坏,即用户反馈"全部是崩溃的"的根因)
|
||||
// * 仅"跨骨架对"(两侧骨名不同)走烘焙;同名对零损耗原样直搬
|
||||
// * 未动画的 src 骨不发旋转键(目标保持 rest,不被 T-pose 零位拉走)
|
||||
//
|
||||
// 数学(旋转,两遍式):
|
||||
// 零位 = src 侧 T-pose 参考 RsT(EstimateHumanoidTPose,两侧对称)
|
||||
// 世界增量 dW = A·(Rsrc(t)·RsT⁻¹)·A⁻¹
|
||||
// 目标世界 RwI = dW·RdT(dst)
|
||||
// 键 = inv(Rw[父])·RwI(父用 anim 后世界旋转)
|
||||
// rest 即 T-pose 的资产自动退化(dW 恒等 → 与旧直搬等价);A 为
|
||||
// src 世界 → dst 世界的行走基对齐(EstimateHumanoidRootAlignment)
|
||||
//
|
||||
// 数学(平移):
|
||||
// Root : dstRest + A·(srcPos - srcRest)·unitScale(整体尺度)
|
||||
// Joint : dstRest + M·(srcPos - srcRest)·ratio(per-骨长比)
|
||||
// M = inv(dstParW)·A·srcParW(局部增量系转换),退化取 unitScale
|
||||
//
|
||||
// 纯算法在 AttachClipsToNodes(headless 可直测);
|
||||
// AttachClipsToModel 是 facade:取模型 nodes/clips + 回填。
|
||||
//
|
||||
|
||||
#include <Engine/Animation/AnimationAttach.h>
|
||||
#include <Engine/Animation/HumanoidRig.h>
|
||||
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u32 kNoNode = ~0u;
|
||||
const glm::quat kIdentity(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
// mat4 → TRS 分解(rest 局部变换;引擎 node 无 shear)
|
||||
struct TRS {
|
||||
glm::vec3 pos{};
|
||||
glm::quat rot{1.0f, 0.0f, 0.0f, 0.0f};
|
||||
glm::vec3 scl{1.0f};
|
||||
};
|
||||
|
||||
TRS Decompose(const glm::mat4& m)
|
||||
{
|
||||
TRS t;
|
||||
t.pos = glm::vec3(m[3]);
|
||||
const glm::vec3 colX(m[0]);
|
||||
const glm::vec3 colY(m[1]);
|
||||
const glm::vec3 colZ(m[2]);
|
||||
t.scl = {glm::length(colX), glm::length(colY), glm::length(colZ)};
|
||||
if (t.scl.x > 1e-8f && t.scl.y > 1e-8f && t.scl.z > 1e-8f) {
|
||||
t.rot = glm::quat_cast(glm::mat3(
|
||||
colX / t.scl.x, colY / t.scl.y, colZ / t.scl.z));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
// 世界矩阵 → 旋转(列归一化抗缩放;退化列取 identity)
|
||||
glm::quat RotOf(const glm::mat4& m)
|
||||
{
|
||||
const glm::vec3 cx(m[0]);
|
||||
const glm::vec3 cy(m[1]);
|
||||
const glm::vec3 cz(m[2]);
|
||||
if (glm::length(cx) < 1e-8f || glm::length(cy) < 1e-8f ||
|
||||
glm::length(cz) < 1e-8f) {
|
||||
return kIdentity;
|
||||
}
|
||||
return glm::quat_cast(glm::mat3(
|
||||
glm::normalize(cx), glm::normalize(cy), glm::normalize(cz)));
|
||||
}
|
||||
|
||||
// rest 全骨架世界矩阵(node 表父先子后,直接链乘)
|
||||
std::vector<glm::mat4> RestGlobals(const std::vector<ModelNode>& nodes)
|
||||
{
|
||||
std::vector<glm::mat4> g(nodes.size());
|
||||
for (size_t i = 0; i < nodes.size(); ++i) {
|
||||
const i32 p = nodes[i].parent;
|
||||
g[i] = (p >= 0 ? g[static_cast<size_t>(p)]
|
||||
: glm::mat4(1.0f)) *
|
||||
nodes[i].localTransform;
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
// ---- 键采样(烘焙用;线性/slerp,rest 兜底)----
|
||||
glm::vec3 SampleV(const std::vector<PosKey>& ks, float t,
|
||||
const glm::vec3& def)
|
||||
{
|
||||
if (ks.empty()) return def;
|
||||
if (t <= ks.front().time) return ks.front().value;
|
||||
for (size_t i = 1; i < ks.size(); ++i) {
|
||||
if (t <= ks[i].time) {
|
||||
const f32 a = (t - ks[i - 1].time) /
|
||||
(ks[i].time - ks[i - 1].time);
|
||||
return glm::mix(ks[i - 1].value, ks[i].value, a);
|
||||
}
|
||||
}
|
||||
return ks.back().value;
|
||||
}
|
||||
|
||||
glm::vec3 SampleV(const std::vector<SclKey>& ks, float t,
|
||||
const glm::vec3& def)
|
||||
{
|
||||
if (ks.empty()) return def;
|
||||
if (t <= ks.front().time) return ks.front().value;
|
||||
for (size_t i = 1; i < ks.size(); ++i) {
|
||||
if (t <= ks[i].time) {
|
||||
const f32 a = (t - ks[i - 1].time) /
|
||||
(ks[i].time - ks[i - 1].time);
|
||||
return glm::mix(ks[i - 1].value, ks[i].value, a);
|
||||
}
|
||||
}
|
||||
return ks.back().value;
|
||||
}
|
||||
|
||||
glm::quat SampleQ(const std::vector<RotKey>& ks, float t,
|
||||
const glm::quat& def)
|
||||
{
|
||||
if (ks.empty()) return def;
|
||||
if (t <= ks.front().time) return ks.front().value;
|
||||
for (size_t i = 1; i < ks.size(); ++i) {
|
||||
if (t <= ks[i].time) {
|
||||
const f32 a = (t - ks[i - 1].time) /
|
||||
(ks[i].time - ks[i - 1].time);
|
||||
return glm::slerp(ks[i - 1].value, ks[i].value, a);
|
||||
}
|
||||
}
|
||||
return ks.back().value;
|
||||
}
|
||||
|
||||
// 源 clip 于时刻 t 的全骨架世界矩阵(层级 T·R·S 链乘)
|
||||
std::vector<glm::mat4> SrcWorldAt(
|
||||
const std::vector<ModelNode>& nodes,
|
||||
const std::vector<const AnimChannel*>& byNode, float t)
|
||||
{
|
||||
std::vector<glm::mat4> world(nodes.size());
|
||||
for (size_t i = 0; i < nodes.size(); ++i) {
|
||||
const TRS rest = Decompose(nodes[i].localTransform);
|
||||
const AnimChannel* ch =
|
||||
i < byNode.size() ? byNode[i] : nullptr;
|
||||
const glm::vec3 p =
|
||||
ch ? SampleV(ch->positions, t, rest.pos) : rest.pos;
|
||||
const glm::quat r =
|
||||
ch ? SampleQ(ch->rotations, t, rest.rot) : rest.rot;
|
||||
const glm::vec3 s =
|
||||
ch ? SampleV(ch->scales, t, rest.scl) : rest.scl;
|
||||
const glm::mat4 local = glm::translate(glm::mat4(1.0f), p) *
|
||||
glm::mat4_cast(r) *
|
||||
glm::scale(glm::mat4(1.0f), s);
|
||||
const i32 pi = nodes[i].parent;
|
||||
world[i] = (pi >= 0 ? world[static_cast<size_t>(pi)]
|
||||
: glm::mat4(1.0f)) * local;
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
bool ChannelEmpty(const AnimChannel& ch)
|
||||
{
|
||||
return ch.positions.empty() && ch.rotations.empty() &&
|
||||
ch.scales.empty();
|
||||
}
|
||||
|
||||
// 槽位识别(未命中 → Count)
|
||||
HumanoidSlot SlotAt(const std::vector<ModelNode>& nodes, u32 i)
|
||||
{
|
||||
Side side = Side::None;
|
||||
bool ok = false;
|
||||
const HumanoidSlot s = MatchHumanoidSlot(nodes[i].name, side, ok);
|
||||
return ok ? s : HumanoidSlot::Count;
|
||||
}
|
||||
|
||||
// ---- 轨道归类 ----
|
||||
enum class Kind : u8 {
|
||||
Skip, // 无对应(道具/辅助骨)
|
||||
Direct, // 同名直搬(同源拆分资产,原样回放)
|
||||
Joint, // 跨骨架关节烘焙(含手指/趾:统一世界增量通道)
|
||||
Root, // 根运动(骨架上方承载节点 → dst 根)
|
||||
};
|
||||
|
||||
struct SrcSlot {
|
||||
Kind kind = Kind::Skip;
|
||||
u32 dst = kNoNode;
|
||||
// 平移烘焙元数据
|
||||
glm::quat M{kIdentity}; // inv(dstParW)·A·srcParW
|
||||
glm::vec3 dstRestPos{};
|
||||
f32 posScale = 1.0f; // 骨长比(Root 用 unitScale)
|
||||
};
|
||||
|
||||
// 冻结的重定向计划(构建后不可变)
|
||||
struct Plan {
|
||||
std::vector<SrcSlot> bySrc; // index = src node
|
||||
std::vector<u32> carriers; // 承载节点(骨架上方,浅→深)
|
||||
std::vector<glm::quat> srcTPose;
|
||||
std::vector<glm::quat> dstTPose;
|
||||
glm::quat align{kIdentity}; // src 世界 → dst 世界
|
||||
f32 unitScale = 1.0f; // src→dst 整体尺度
|
||||
u32 dstRoot = kNoNode;
|
||||
bool needsBake = false;
|
||||
};
|
||||
|
||||
// 骨长尺度(hips→head 世界距离;槽位不全 → 0)
|
||||
f32 SkeletonSpan(const std::vector<ModelNode>& nodes,
|
||||
const std::vector<glm::mat4>& g)
|
||||
{
|
||||
i32 hips = -1;
|
||||
i32 head = -1;
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
const HumanoidSlot slot = SlotAt(nodes, i);
|
||||
if (slot == HumanoidSlot::Hips && hips < 0) {
|
||||
hips = static_cast<i32>(i);
|
||||
} else if (slot == HumanoidSlot::Head && head < 0) {
|
||||
head = static_cast<i32>(i);
|
||||
}
|
||||
}
|
||||
if (hips < 0 || head < 0) return 0.0f;
|
||||
return glm::length(glm::vec3(g[static_cast<size_t>(head)][3]) -
|
||||
glm::vec3(g[static_cast<size_t>(hips)][3]));
|
||||
}
|
||||
|
||||
// 构建冻结计划:归类每个 src 轨道 + 预计算几何补偿元数据
|
||||
Plan BuildPlan(const std::vector<ModelNode>& srcNodes,
|
||||
const std::vector<ModelNode>& dstNodes)
|
||||
{
|
||||
Plan pl;
|
||||
pl.bySrc.assign(srcNodes.size(), SrcSlot{});
|
||||
|
||||
// dst 根(node 0 即 DFS 首节点)
|
||||
for (u32 i = 0; i < dstNodes.size(); ++i) {
|
||||
if (dstNodes[i].parent < 0) {
|
||||
pl.dstRoot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 跨骨架配对(AutoMapHumanoid:同名直映 + 槽位一对一)
|
||||
const auto pairs = AutoMapHumanoid(srcNodes, dstNodes);
|
||||
std::vector<u8> slotBone(srcNodes.size(), 0);
|
||||
for (const auto& [si, di] : pairs) {
|
||||
(void)di;
|
||||
if (SlotAt(srcNodes, si) != HumanoidSlot::Count) {
|
||||
slotBone[si] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 承载节点 = 已识别槽位骨的祖先里**未识别**者(骨架上方根
|
||||
// 运动承载链)。判定必须基于槽位识别而非"所有映射骨祖先"
|
||||
// ——后者把 hips/spine/… 全部内部骨误判进来,导致大半骨架
|
||||
// 走烘焙、叶走直搬,局部化基准错位(整体崩坏根因)
|
||||
std::vector<u8> isCarrier(srcNodes.size(), 0);
|
||||
for (u32 si = 0; si < srcNodes.size(); ++si) {
|
||||
if (!slotBone[si]) continue;
|
||||
for (i32 p = srcNodes[si].parent; p >= 0;
|
||||
p = srcNodes[static_cast<size_t>(p)].parent) {
|
||||
const u32 pu = static_cast<u32>(p);
|
||||
if (!slotBone[pu]) isCarrier[pu] = 1;
|
||||
}
|
||||
}
|
||||
for (u32 i = 0; i < srcNodes.size(); ++i) {
|
||||
if (isCarrier[i]) pl.carriers.push_back(i);
|
||||
}
|
||||
|
||||
// owner:每个 dst 至多一条轨道(跨骨架对优先占用)
|
||||
std::vector<u32> owner(dstNodes.size(), kNoNode);
|
||||
|
||||
// ① 跨骨架对分类(含手指/趾,统一 → Joint 世界增量烘焙)
|
||||
std::vector<std::pair<u32, u32>> directPairs; // (si,di) 同名
|
||||
for (const auto& [si, di] : pairs) {
|
||||
if (isCarrier[si]) continue; // 承载节点另归
|
||||
if (srcNodes[si].name == dstNodes[di].name) {
|
||||
directPairs.emplace_back(si, di); // 待 ② 定夺
|
||||
continue;
|
||||
}
|
||||
if (owner[di] != kNoNode) continue; // 已被占
|
||||
SrcSlot s;
|
||||
s.dst = di;
|
||||
// 手指/趾与主干同走世界增量烘焙——Unity/UE 语义:指骨即
|
||||
// 普通骨骼,无独立通道。世界增量对骨骼局部轴向约定免疫:
|
||||
// 此前的"指根局部增量"以手骨局部系为基准做共轭,而
|
||||
// Mixamo/Rigify/UE 的手骨轴向约定互异(沿 +X vs 沿 +Y),
|
||||
// 共轭后弯曲轴被拧转 → X Bot 指弯向手背、kwy 指呈爪状
|
||||
//(用户实测)。世界增量只用两侧世界旋转 + 行走基对齐 A
|
||||
s.kind = Kind::Joint;
|
||||
owner[di] = si;
|
||||
pl.bySrc[si] = s;
|
||||
}
|
||||
|
||||
// ② 同名对 → 直搬(仅当目标未被跨骨架对占用;承载节点已排除)
|
||||
for (const auto& [si, di] : directPairs) {
|
||||
if (owner[di] != kNoNode) continue;
|
||||
SrcSlot s;
|
||||
s.kind = Kind::Direct;
|
||||
s.dst = di;
|
||||
pl.bySrc[si] = s;
|
||||
owner[di] = si;
|
||||
}
|
||||
|
||||
// ③ 承载节点 → 根运动(恒不直搬)。仅当 dst 根未被映射骨占用
|
||||
// 时才需要独立通道:dst 根若本身是映射骨(Mixamo 的
|
||||
// mixamorig:Hips 即根),其绝对世界增量已含承载链运动,
|
||||
// 独立通道反而与之重复
|
||||
if (pl.dstRoot != kNoNode && owner[pl.dstRoot] == kNoNode) {
|
||||
for (const u32 c : pl.carriers) {
|
||||
SrcSlot s;
|
||||
s.kind = Kind::Root;
|
||||
s.dst = pl.dstRoot;
|
||||
pl.bySrc[c] = s;
|
||||
}
|
||||
}
|
||||
|
||||
// 是否需要几何烘焙(纯同名资产走快速直搬路径,零开销)
|
||||
for (const auto& s : pl.bySrc) {
|
||||
if (s.kind == Kind::Joint || s.kind == Kind::Root) {
|
||||
pl.needsBake = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!pl.needsBake) return pl;
|
||||
|
||||
// ---- 几何补偿元数据(A / T-pose / 平移尺度)----
|
||||
const auto srcG = RestGlobals(srcNodes);
|
||||
const auto dstG = RestGlobals(dstNodes);
|
||||
pl.align = EstimateHumanoidRootAlignment(srcNodes, dstNodes);
|
||||
pl.srcTPose = EstimateHumanoidTPose(srcNodes, srcG);
|
||||
pl.dstTPose = EstimateHumanoidTPose(dstNodes, dstG);
|
||||
const f32 srcSpan = SkeletonSpan(srcNodes, srcG);
|
||||
const f32 dstSpan = SkeletonSpan(dstNodes, dstG);
|
||||
pl.unitScale = srcSpan > 1e-6f ? dstSpan / srcSpan : 1.0f;
|
||||
|
||||
for (u32 si = 0; si < srcNodes.size(); ++si) {
|
||||
SrcSlot& s = pl.bySrc[si];
|
||||
if (s.kind == Kind::Skip) continue;
|
||||
const u32 di = s.dst;
|
||||
|
||||
// 平移元数据
|
||||
const i32 sp = srcNodes[si].parent;
|
||||
const i32 dp = dstNodes[di].parent;
|
||||
const glm::quat srcParW =
|
||||
sp >= 0 ? RotOf(srcG[static_cast<size_t>(sp)]) : kIdentity;
|
||||
const glm::quat dstParW =
|
||||
dp >= 0 ? RotOf(dstG[static_cast<size_t>(dp)]) : kIdentity;
|
||||
s.M = glm::inverse(dstParW) * pl.align * srcParW;
|
||||
s.dstRestPos = Decompose(dstNodes[di].localTransform).pos;
|
||||
if (s.kind == Kind::Root) {
|
||||
s.posScale = pl.unitScale;
|
||||
} else {
|
||||
const f32 srcLen =
|
||||
glm::length(Decompose(srcNodes[si].localTransform).pos);
|
||||
const f32 dstLen =
|
||||
glm::length(s.dstRestPos);
|
||||
s.posScale = srcLen > 1e-6f ? dstLen / srcLen
|
||||
: pl.unitScale;
|
||||
}
|
||||
}
|
||||
return pl;
|
||||
}
|
||||
|
||||
// 唯一 clip 名(同名冲突逐轮加源文件名前缀)
|
||||
std::string UniqueName(const std::string& base,
|
||||
const std::string& stem,
|
||||
const std::vector<AnimationClip>& existing,
|
||||
const std::vector<AnimationClip>& pending)
|
||||
{
|
||||
const auto collides = [&](const std::string& n) {
|
||||
for (const auto& c : existing) {
|
||||
if (c.name == n) return true;
|
||||
}
|
||||
for (const auto& c : pending) {
|
||||
if (c.name == n) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
std::string name = base;
|
||||
while (collides(name)) name = stem + "_" + name;
|
||||
return name;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AttachResult AttachClipsToNodes(
|
||||
const std::vector<ModelNode>& dstNodes,
|
||||
const std::vector<AnimationClip>& existingClips,
|
||||
ParsedModel&& src, const std::string& sourcePath,
|
||||
std::vector<AnimationClip>& outExtra)
|
||||
{
|
||||
AttachResult res;
|
||||
// 幂等:该源文件已并入过(自愈重放/重复 attach 直接返回)
|
||||
for (const auto& c : existingClips) {
|
||||
if (c.sourcePath == sourcePath) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
if (src.clips.empty()) {
|
||||
ENGINE_WARN("AttachClipsToModel: no clips in '{}'", sourcePath);
|
||||
return res;
|
||||
}
|
||||
|
||||
const std::vector<ModelNode>& srcNodes = src.nodes;
|
||||
const Plan pl = BuildPlan(srcNodes, dstNodes);
|
||||
const std::string stem =
|
||||
std::filesystem::path(sourcePath).stem().string();
|
||||
|
||||
for (auto& clip : src.clips) {
|
||||
AnimationClip out;
|
||||
out.name = UniqueName(clip.name, stem, existingClips, outExtra);
|
||||
out.duration = clip.duration;
|
||||
out.tps = clip.tps;
|
||||
out.sourcePath = sourcePath;
|
||||
|
||||
// 每 clip 的 src 轨道索引表(按 nodeIdx)
|
||||
std::vector<const AnimChannel*> srcByNode(srcNodes.size(),
|
||||
nullptr);
|
||||
for (const auto& ch : clip.channels) {
|
||||
if (ch.nodeIdx < srcByNode.size() && !ChannelEmpty(ch)) {
|
||||
srcByNode[ch.nodeIdx] = &ch;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 本 clip 的根运动源:承载节点里最深且有轨道者 ----
|
||||
// (更深者世界变换已含其全部祖先运动,取一个即全包括)
|
||||
u32 rootSrc = kNoNode;
|
||||
for (const u32 c : pl.carriers) { // carriers 浅→深
|
||||
if (srcByNode[c] != nullptr) rootSrc = c;
|
||||
}
|
||||
|
||||
// ---- 目标节点 → 源轨道(可控节点,供烘焙局部化)----
|
||||
std::unordered_map<u32, u32> jointBakeDst; // dst → src(Joint/Root)
|
||||
std::unordered_map<u32, u32> directDst; // dst → src(Direct)
|
||||
for (u32 si = 0; si < srcNodes.size(); ++si) {
|
||||
const SrcSlot& s = pl.bySrc[si];
|
||||
if (srcByNode[si] == nullptr) continue;
|
||||
switch (s.kind) {
|
||||
case Kind::Direct:
|
||||
directDst.emplace(s.dst, si);
|
||||
break;
|
||||
case Kind::Joint:
|
||||
jointBakeDst.emplace(s.dst, si);
|
||||
break;
|
||||
case Kind::Root:
|
||||
if (si == rootSrc) jointBakeDst.emplace(s.dst, si);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 旋转键:两遍式烘焙 ----
|
||||
// 采样时刻 = 全部需烘焙通道(Joint/Root)的旋转键时刻并集;
|
||||
// 逐 t 跑源全骨架 FK → 逐 dst 递推世界旋转 → 局部化写键
|
||||
std::unordered_map<u32, std::vector<RotKey>> baked;
|
||||
{
|
||||
std::vector<f32> times;
|
||||
for (u32 si = 0; si < srcNodes.size(); ++si) {
|
||||
const SrcSlot& s = pl.bySrc[si];
|
||||
const AnimChannel* ch = srcByNode[si];
|
||||
if (ch == nullptr) continue;
|
||||
const bool bakeThis =
|
||||
s.kind == Kind::Joint ||
|
||||
(s.kind == Kind::Root && si == rootSrc);
|
||||
if (!bakeThis || ch->rotations.empty()) continue;
|
||||
for (const auto& k : ch->rotations) times.push_back(k.time);
|
||||
}
|
||||
std::sort(times.begin(), times.end());
|
||||
times.erase(std::unique(times.begin(), times.end()),
|
||||
times.end());
|
||||
|
||||
if (!times.empty()) {
|
||||
std::vector<glm::quat> Rw(dstNodes.size(), kIdentity);
|
||||
for (const f32 t : times) {
|
||||
const auto Wsrc = SrcWorldAt(srcNodes, srcByNode, t);
|
||||
for (u32 i = 0; i < dstNodes.size(); ++i) {
|
||||
const i32 pi = dstNodes[i].parent;
|
||||
const glm::quat Rpar =
|
||||
pi >= 0 ? Rw[static_cast<size_t>(pi)]
|
||||
: kIdentity;
|
||||
glm::quat RwI;
|
||||
const auto jb = jointBakeDst.find(i);
|
||||
if (jb != jointBakeDst.end()) {
|
||||
// 关节/根/手指/趾:世界增量搬运(对两侧
|
||||
// 骨骼的局部轴向约定免疫)
|
||||
const u32 si = jb->second;
|
||||
const glm::quat dW =
|
||||
pl.align *
|
||||
(RotOf(Wsrc[si]) *
|
||||
glm::inverse(pl.srcTPose
|
||||
[static_cast<size_t>(si)])) *
|
||||
glm::inverse(pl.align);
|
||||
RwI = dW * pl.dstTPose[static_cast<size_t>(i)];
|
||||
} else if (auto db = directDst.find(i);
|
||||
db != directDst.end()) {
|
||||
// 直搬骨:anim 后世界 = 父 × 源局部(供其子
|
||||
// 关节局部化用;自身轨道另走原样直搬)
|
||||
const u32 si = db->second;
|
||||
const glm::quat loc = SampleQ(
|
||||
srcByNode[si]->rotations, t,
|
||||
Decompose(srcNodes[si].localTransform).rot);
|
||||
RwI = Rpar * loc;
|
||||
} else {
|
||||
RwI = Rpar * Decompose(
|
||||
dstNodes[i].localTransform).rot;
|
||||
}
|
||||
Rw[static_cast<size_t>(i)] = RwI;
|
||||
if (jb != jointBakeDst.end()) {
|
||||
baked[i].push_back(
|
||||
{t, glm::inverse(Rpar) * RwI});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 组装轨道(每 dst 至多一条;owner 在 Plan 阶段已保证)----
|
||||
// 非 const 引用:同名直搬路径整段 move 源轨道(此后 src 不再
|
||||
// 被读——烘焙已在上面完成)
|
||||
for (auto& ch : clip.channels) {
|
||||
if (ChannelEmpty(ch)) continue;
|
||||
if (ch.nodeIdx >= srcNodes.size()) {
|
||||
++res.channelsSkipped;
|
||||
continue;
|
||||
}
|
||||
const SrcSlot& s = pl.bySrc[ch.nodeIdx];
|
||||
const std::string& boneName = srcNodes[ch.nodeIdx].name;
|
||||
|
||||
if (s.kind == Kind::Skip) {
|
||||
++res.channelsSkipped;
|
||||
continue;
|
||||
}
|
||||
if (s.kind == Kind::Root && ch.nodeIdx != rootSrc) {
|
||||
continue; // 更深的承载节点已承载根运动,其余并入
|
||||
}
|
||||
|
||||
AnimChannel outCh;
|
||||
outCh.nodeIdx = s.dst;
|
||||
|
||||
if (s.kind == Kind::Direct) {
|
||||
// 同名直搬:原样(同资产体系局部空间同源)
|
||||
outCh.positions = std::move(ch.positions);
|
||||
outCh.rotations = std::move(ch.rotations);
|
||||
outCh.scales = std::move(ch.scales);
|
||||
out.channels.push_back(std::move(outCh));
|
||||
++res.channelsMatched;
|
||||
} else {
|
||||
// 平移:局部增量 × 尺度
|
||||
const TRS srcRest =
|
||||
Decompose(srcNodes[ch.nodeIdx].localTransform);
|
||||
outCh.positions.reserve(ch.positions.size());
|
||||
for (const auto& k : ch.positions) {
|
||||
outCh.positions.push_back(
|
||||
{k.time, s.dstRestPos +
|
||||
(s.M * (k.value - srcRest.pos)) *
|
||||
s.posScale});
|
||||
}
|
||||
// 旋转:烘焙结果(多源汇一 dst 时共享同份)
|
||||
if (auto bk = baked.find(s.dst); bk != baked.end()) {
|
||||
outCh.rotations = bk->second;
|
||||
}
|
||||
// 缩放不搬运:跨骨架源的缩放轨道常承载单位换算
|
||||
// (kwy 根节点的 cm 补偿等),原样拷到目标会整体放大
|
||||
// 骨骼;目标保持自身 rest 缩放(人体动画一般无缩放)
|
||||
out.channels.push_back(std::move(outCh));
|
||||
++res.channelsRetargeted;
|
||||
|
||||
const bool newRow = std::none_of(
|
||||
res.retargetRows.begin(), res.retargetRows.end(),
|
||||
[&](const RetargetRow& r)
|
||||
{ return r.srcName == boneName; });
|
||||
if (newRow) {
|
||||
const HumanoidSlot slot = SlotAt(srcNodes, ch.nodeIdx);
|
||||
res.retargetRows.push_back(
|
||||
{s.kind == Kind::Root
|
||||
? "RootMotion"
|
||||
: HumanoidSlotName(slot),
|
||||
boneName, dstNodes[s.dst].name});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (out.channels.empty()) {
|
||||
ENGINE_WARN("AttachClipsToModel: clip '{}' has no matching "
|
||||
"bones in target (skipped)",
|
||||
clip.name);
|
||||
continue;
|
||||
}
|
||||
outExtra.push_back(std::move(out));
|
||||
++res.clipsAdded;
|
||||
}
|
||||
|
||||
if (!outExtra.empty()) {
|
||||
ENGINE_INFO("AttachClipsToModel: '{}' -> {} clips "
|
||||
"({} named, {} retargeted, {} skipped)",
|
||||
sourcePath, res.clipsAdded, res.channelsMatched,
|
||||
res.channelsRetargeted, res.channelsSkipped);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
|
||||
const std::string& sourcePath)
|
||||
{
|
||||
std::vector<AnimationClip> extra;
|
||||
AttachResult res = AttachClipsToNodes(
|
||||
target.GetNodes(), target.GetClips(), std::move(src),
|
||||
sourcePath, extra);
|
||||
if (!extra.empty()) {
|
||||
target.AppendClips(std::move(extra));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace Engine::Animation
|
||||
@@ -0,0 +1,427 @@
|
||||
//
|
||||
// AnimationController 实现:.animctrl YAML 解析。
|
||||
//
|
||||
// 纯资产解析(无运行时状态):字符串条件在这里一次性编译成数值比较
|
||||
// 结构(TransitionCond),System 每帧只做查表比较,零字符串处理。
|
||||
//
|
||||
|
||||
#include <Engine/Animation/AnimationController.h>
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
// 2D 方向混合权重:原点层 1-|v| + 覆盖扇区角度插值 ×min(|v|,1)。
|
||||
// 数学与原 System 内联版一致,抽公共函数后编辑器方向盘可视化
|
||||
// 与运行时采样永不漂移。
|
||||
// kMinBlendWeight:方向层权重低于阈值即不输出——中心附近每个
|
||||
// 方向层都有"角度份额×|v|"的微量残留,不滤的话"读出显示 0.00
|
||||
// 却还在影响姿态"(四舍五入假象)+ 无效微量混合。原点层不受
|
||||
// 此限(它是中心默认动作,v 小时权重接近 1)
|
||||
constexpr f32 kMinBlendWeight = 0.01f;
|
||||
|
||||
void EvaluateBlendTree2D(const BlendTree& tree, f32 x, f32 z,
|
||||
std::vector<std::pair<u32, f32>>& out)
|
||||
{
|
||||
out.clear();
|
||||
const auto& ch = tree.children;
|
||||
const f32 r = glm::min(glm::length(glm::vec2(x, z)), 1.0f);
|
||||
// 原点子节点:权重 = 1-|v| 随模长连续衰减(Unity 2D 语义)——
|
||||
// 待机到方向动作平滑过渡。v=0 时全量,拖到边缘衰减为 0;
|
||||
// 任何实现里原点层都不允许"v≠0 即消失"(那样中心拖动会瞬切)
|
||||
for (u32 i = 0; i < ch.size(); ++i) {
|
||||
if (glm::length(ch[i].pos) < 1e-4f) {
|
||||
if (1.0f - r > 1e-4f) out.push_back({i, 1.0f - r});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (r <= 1e-4f || ch.empty()) return;
|
||||
// 覆盖 v 角度的相邻扇区(角度归一 [0,2π);方向子 ≤16 个,
|
||||
// 栈数组零堆分配)
|
||||
constexpr f32 kTwoPi = 6.2831853f;
|
||||
constexpr u32 kMaxDir = 16;
|
||||
u32 idx[kMaxDir];
|
||||
f32 ang[kMaxDir];
|
||||
u32 nd = 0;
|
||||
for (u32 i = 0; i < ch.size() && nd < kMaxDir; ++i) {
|
||||
if (glm::length(ch[i].pos) < 1e-4f) continue;
|
||||
idx[nd] = i;
|
||||
ang[nd] = glm::mod(glm::atan(ch[i].pos.y, ch[i].pos.x), kTwoPi);
|
||||
++nd;
|
||||
}
|
||||
if (nd == 0) return;
|
||||
// 环序自排序(插入排序,nd≤16 就地零堆分配):扇区数学假设
|
||||
// "数组相邻 = 角度环相邻",输入顺序不可信——编辑器实时改树
|
||||
// 不重跑 Load 期排序,乱序时右侧拖拽会配到错误的扇区邻居
|
||||
for (u32 i = 1; i < nd; ++i) {
|
||||
const u32 ki = idx[i];
|
||||
const f32 ai = ang[i];
|
||||
u32 j = i;
|
||||
while (j > 0 && ang[j - 1] > ai) {
|
||||
idx[j] = idx[j - 1];
|
||||
ang[j] = ang[j - 1];
|
||||
--j;
|
||||
}
|
||||
idx[j] = ki;
|
||||
ang[j] = ai;
|
||||
}
|
||||
const f32 theta = glm::mod(glm::atan(z, x), kTwoPi);
|
||||
if (nd == 1) {
|
||||
// 单方向子:无扇区可插值,全权重归它(否则角度不等于
|
||||
// 该方向时落空退化成纯 idle)
|
||||
out.push_back({idx[0], r});
|
||||
return;
|
||||
}
|
||||
for (u32 i = 0; i < nd; ++i) {
|
||||
const u32 j = (i + 1) % nd;
|
||||
const f32 ai = ang[i];
|
||||
if (j == 0) {
|
||||
// wrap 扇区 [ai,2π)∪[0,ang0]:用 || 连接,&& 会让
|
||||
// theta ∈ [0,ang0) 落空(首方向不在 0° 时拖右侧无匹配)
|
||||
if (theta >= ai || theta <= ang[0]) {
|
||||
const f32 span = ang[0] + kTwoPi - ai;
|
||||
const f32 tt =
|
||||
theta >= ai ? theta - ai : theta + kTwoPi - ai;
|
||||
const f32 t = span > 1e-6f ? tt / span : 0.0f;
|
||||
const f32 wi = (1.0f - t) * r;
|
||||
const f32 wj = t * r;
|
||||
if (wi >= kMinBlendWeight) out.push_back({idx[i], wi});
|
||||
if (wj >= kMinBlendWeight)
|
||||
out.push_back({idx[0], wj});
|
||||
break;
|
||||
}
|
||||
} else if (theta >= ai && theta <= ang[j]) {
|
||||
const f32 span = ang[j] - ai;
|
||||
const f32 t = span > 1e-6f ? (theta - ai) / span : 0.0f;
|
||||
const f32 wi = (1.0f - t) * r;
|
||||
const f32 wj = t * r;
|
||||
if (wi >= kMinBlendWeight) out.push_back({idx[i], wi});
|
||||
if (wj >= kMinBlendWeight) out.push_back({idx[j], wj});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "Speed > 0.1" / "IsJump == true" / ""(恒真)→ 数值比较结构。
|
||||
// 未知参数/坏格式 = invalid(永不触发 + WARN,资产错误不崩运行时)
|
||||
TransitionCond AnimationController::ParseCond(const std::string& s)
|
||||
{
|
||||
TransitionCond c;
|
||||
if (s.empty()) {
|
||||
c.always = true;
|
||||
c.valid = true;
|
||||
return c;
|
||||
}
|
||||
std::istringstream ss(s);
|
||||
std::string param, op, operand;
|
||||
ss >> param >> op >> operand;
|
||||
c.param = param;
|
||||
if (param.empty() || op.empty() || operand.empty()) {
|
||||
ENGINE_WARN("AnimationController: bad cond '{}' (want 'P op X')",
|
||||
s);
|
||||
return c;
|
||||
}
|
||||
if (op == ">") c.op = TransitionCond::Op::Gt;
|
||||
else if (op == "<") c.op = TransitionCond::Op::Lt;
|
||||
else if (op == ">=") c.op = TransitionCond::Op::Ge;
|
||||
else if (op == "<=") c.op = TransitionCond::Op::Le;
|
||||
else if (op == "==") c.op = TransitionCond::Op::Eq;
|
||||
else if (op == "!=") c.op = TransitionCond::Op::Ne;
|
||||
else {
|
||||
ENGINE_WARN("AnimationController: unknown op '{}' in '{}'",
|
||||
op, s);
|
||||
return c;
|
||||
}
|
||||
if (operand == "true") {
|
||||
c.isBool = true;
|
||||
c.boolOperand = true;
|
||||
} else if (operand == "false") {
|
||||
c.isBool = true;
|
||||
c.boolOperand = false;
|
||||
} else {
|
||||
try {
|
||||
size_t consumed = 0;
|
||||
c.numOperand = std::stof(operand, &consumed);
|
||||
if (consumed != operand.size()) throw std::invalid_argument("");
|
||||
} catch (const std::exception&) {
|
||||
ENGINE_WARN("AnimationController: bad operand '{}' in '{}'",
|
||||
operand, s);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
c.valid = true;
|
||||
return c;
|
||||
}
|
||||
|
||||
Scope<AnimationController>
|
||||
AnimationController::Load(const std::string& path)
|
||||
{
|
||||
YAML::Node root;
|
||||
try {
|
||||
root = YAML::LoadFile(path);
|
||||
} catch (const std::exception& e) {
|
||||
ENGINE_ERROR("AnimationController: failed to parse '{}': {}",
|
||||
path, e.what());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto ctrl = CreateScope<AnimationController>();
|
||||
ctrl->m_Path = path;
|
||||
|
||||
// ---- Params(参数声明;类型缺省 float)----
|
||||
if (const YAML::Node params = root["Params"]) {
|
||||
for (const auto& p : params) {
|
||||
ControllerParam d;
|
||||
d.name = p["name"].as<std::string>("");
|
||||
const std::string type =
|
||||
p["type"].as<std::string>("float");
|
||||
if (type == "bool") d.type = ControllerParam::Type::Bool;
|
||||
else if (type == "trigger")
|
||||
d.type = ControllerParam::Type::Trigger;
|
||||
if (d.type == ControllerParam::Type::Bool)
|
||||
d.defBool = p["default"].as<bool>(false);
|
||||
else
|
||||
d.defFloat = p["default"].as<f32>(0.0f);
|
||||
if (!d.name.empty()) {
|
||||
ctrl->m_Params.push_back(std::move(d));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- States(map:state 名 → 定义;文件序即声明序)----
|
||||
if (const YAML::Node states = root["States"]) {
|
||||
for (const auto& it : states) {
|
||||
ControllerState st;
|
||||
st.name = it.first.as<std::string>("");
|
||||
const YAML::Node sn = it.second;
|
||||
if (st.name.empty()) continue;
|
||||
if (const YAML::Node clip = sn["clip"]) {
|
||||
st.tree.type = BlendTree::Type::Single;
|
||||
st.tree.clip = clip.as<std::string>("");
|
||||
}
|
||||
if (const YAML::Node bt = sn["blendTree"]) {
|
||||
const std::string btType =
|
||||
bt["type"].as<std::string>("2DDirectional");
|
||||
st.tree.type =
|
||||
(btType == "1D" || btType == "1DDirectional")
|
||||
? BlendTree::Type::Directional1D
|
||||
: BlendTree::Type::Directional2D;
|
||||
st.tree.xParam = bt["x"].as<std::string>("");
|
||||
if (const YAML::Node z = bt["z"]) {
|
||||
st.tree.zParam = z.as<std::string>("");
|
||||
}
|
||||
if (const YAML::Node children = bt["children"]) {
|
||||
for (const auto& c : children) {
|
||||
BlendTreeChild ch;
|
||||
ch.clip = c["clip"].as<std::string>("");
|
||||
if (const YAML::Node at = c["at"]) {
|
||||
if (at.IsSequence() && at.size() >= 1) {
|
||||
ch.pos.x = at[0].as<f32>(0.0f);
|
||||
if (at.size() >= 2) {
|
||||
ch.pos.y = at[1].as<f32>(0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ch.clip.empty()) {
|
||||
st.tree.children.push_back(std::move(ch));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
st.speed = sn["speed"].as<f32>(1.0f);
|
||||
st.loop = sn["loop"].as<bool>(true);
|
||||
if (const YAML::Node te = sn["transitionsOnEnd"]) {
|
||||
for (const auto& t : te) {
|
||||
st.transitionsOnEnd.push_back(
|
||||
t.as<std::string>(""));
|
||||
}
|
||||
}
|
||||
// BlendTree 子节点排序(Load 期一次,System 每帧直接
|
||||
// 扫描):1D 按阈值升序;2D 按角度升序(原点子节点
|
||||
// 角度未定义留在原位——扇区查找时按 |pos|<ε 排除)
|
||||
if (st.tree.type == BlendTree::Type::Directional1D) {
|
||||
std::sort(st.tree.children.begin(),
|
||||
st.tree.children.end(),
|
||||
[](const BlendTreeChild& a,
|
||||
const BlendTreeChild& b)
|
||||
{ return a.pos.x < b.pos.x; });
|
||||
} else if (st.tree.type ==
|
||||
BlendTree::Type::Directional2D) {
|
||||
std::sort(st.tree.children.begin(),
|
||||
st.tree.children.end(),
|
||||
[](const BlendTreeChild& a,
|
||||
const BlendTreeChild& b) {
|
||||
return std::atan2(a.pos.y, a.pos.x) <
|
||||
std::atan2(b.pos.y, b.pos.x);
|
||||
});
|
||||
}
|
||||
ctrl->m_StateOrder.push_back(st.name);
|
||||
ctrl->m_States.emplace(st.name, std::move(st));
|
||||
}
|
||||
}
|
||||
if (ctrl->m_States.empty()) {
|
||||
ENGINE_ERROR("AnimationController: '{}' has no states", path);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---- Entry(显式键优先,缺省 = 声明序首个)----
|
||||
if (const YAML::Node entry = root["Entry"]) {
|
||||
ctrl->m_Entry = entry.as<std::string>("");
|
||||
}
|
||||
if (ctrl->m_Entry.empty() ||
|
||||
!ctrl->m_States.count(ctrl->m_Entry)) {
|
||||
if (!ctrl->m_Entry.empty()) {
|
||||
ENGINE_WARN("AnimationController: entry '{}' not found in "
|
||||
"'{}', falling back to first state",
|
||||
ctrl->m_Entry, path);
|
||||
}
|
||||
ctrl->m_Entry = ctrl->m_StateOrder.front();
|
||||
}
|
||||
|
||||
// ---- Transitions(目标不存在的行丢弃 + WARN)----
|
||||
if (const YAML::Node trans = root["Transitions"]) {
|
||||
for (const auto& t : trans) {
|
||||
Transition tr;
|
||||
tr.from = t["from"].as<std::string>("*");
|
||||
tr.to = t["to"].as<std::string>("");
|
||||
tr.fade = t["fade"].as<f32>(0.25f);
|
||||
tr.hasExitTime = t["exitTime"].as<bool>(false);
|
||||
tr.cond = ParseCond(t["cond"].as<std::string>(""));
|
||||
if (tr.to.empty() || !ctrl->m_States.count(tr.to)) {
|
||||
ENGINE_WARN("AnimationController: transition to "
|
||||
"unknown state '{}' in '{}'",
|
||||
tr.to, path);
|
||||
continue;
|
||||
}
|
||||
ctrl->m_Transitions.push_back(std::move(tr));
|
||||
}
|
||||
}
|
||||
|
||||
ENGINE_INFO("AnimationController: loaded '{}' ({} states, "
|
||||
"{} params, {} transitions)",
|
||||
path, ctrl->m_States.size(), ctrl->m_Params.size(),
|
||||
ctrl->m_Transitions.size());
|
||||
return ctrl;
|
||||
}
|
||||
|
||||
// Save:与 Load 对齐的 YAML 反序列化(f32 紧凑格式 %.4g;clip/状态
|
||||
// 名加引号——mixamorig: 前缀等含冒号名不破键值解析)
|
||||
static std::string FmtF(f32 v)
|
||||
{
|
||||
char b[32];
|
||||
snprintf(b, sizeof(b), "%.4g", static_cast<double>(v));
|
||||
return b;
|
||||
}
|
||||
|
||||
std::string AnimationController::CondToString(const TransitionCond& c)
|
||||
{
|
||||
const char* op = "=";
|
||||
switch (c.op) {
|
||||
case TransitionCond::Op::Gt: op = ">"; break;
|
||||
case TransitionCond::Op::Lt: op = "<"; break;
|
||||
case TransitionCond::Op::Ge: op = ">="; break;
|
||||
case TransitionCond::Op::Le: op = "<="; break;
|
||||
case TransitionCond::Op::Eq: op = "=="; break;
|
||||
case TransitionCond::Op::Ne: op = "!="; break;
|
||||
}
|
||||
const std::string operand =
|
||||
c.isBool ? (c.boolOperand ? "true" : "false")
|
||||
: FmtF(c.numOperand);
|
||||
return c.param + " " + op + " " + operand;
|
||||
}
|
||||
|
||||
bool AnimationController::Save(const std::string& path) const
|
||||
{
|
||||
std::string y =
|
||||
"# 动画控制器(.animctrl)—— ControllerEditorPanel 保存产物\n"
|
||||
"# 状态/参数/过渡全由本文件声明,引擎零硬编码\n";
|
||||
if (!m_Entry.empty()) {
|
||||
y += "Entry: \"" + m_Entry + "\"\n";
|
||||
}
|
||||
y += "Params:\n";
|
||||
for (const auto& p : m_Params) {
|
||||
y += " - {name: \"" + p.name + "\", type: ";
|
||||
if (p.type == ControllerParam::Type::Float) {
|
||||
y += "float, default: " + FmtF(p.defFloat) + "}";
|
||||
} else if (p.type == ControllerParam::Type::Bool) {
|
||||
y += std::string("bool, default: ") +
|
||||
(p.defBool ? "true" : "false") + "}";
|
||||
} else {
|
||||
y += "trigger}";
|
||||
}
|
||||
y += "\n";
|
||||
}
|
||||
y += "States:\n";
|
||||
for (const auto& n : m_StateOrder) {
|
||||
const auto it = m_States.find(n);
|
||||
if (it == m_States.end()) continue;
|
||||
const auto& s = it->second;
|
||||
y += " \"" + n + "\":\n";
|
||||
y += " speed: " + FmtF(s.speed) + "\n";
|
||||
y += std::string(" loop: ") + (s.loop ? "true" : "false") +
|
||||
"\n";
|
||||
if (!s.transitionsOnEnd.empty()) {
|
||||
y += " transitionsOnEnd: [";
|
||||
for (size_t i = 0; i < s.transitionsOnEnd.size(); ++i) {
|
||||
// 状态名加引号(含冒号/逗号的名不破 flow 序列解析,
|
||||
// 同 States 键与 Transitions from/to 惯例)
|
||||
y += (i ? ", \"" : "\"") + s.transitionsOnEnd[i] + "\"";
|
||||
}
|
||||
y += "]\n";
|
||||
}
|
||||
const auto& t = s.tree;
|
||||
if (t.type == BlendTree::Type::Single) {
|
||||
y += " clip: \"" + t.clip + "\"\n";
|
||||
} else {
|
||||
y += " blendTree:\n type: ";
|
||||
y += (t.type == BlendTree::Type::Directional1D)
|
||||
? "1DDirectional\n"
|
||||
: "2DDirectional\n";
|
||||
y += " x: \"" + t.xParam + "\"\n";
|
||||
if (t.type == BlendTree::Type::Directional2D) {
|
||||
y += " z: \"" + t.zParam + "\"\n";
|
||||
}
|
||||
y += " children:\n";
|
||||
for (const auto& c : t.children) {
|
||||
y += " - {clip: \"" + c.clip + "\", at: [" +
|
||||
FmtF(c.pos.x);
|
||||
if (t.type == BlendTree::Type::Directional2D) {
|
||||
y += ", " + FmtF(c.pos.y);
|
||||
}
|
||||
y += "]}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
y += "Transitions:\n";
|
||||
for (const auto& t : m_Transitions) {
|
||||
y += " - {from: \"" + t.from + "\", to: \"" + t.to + "\"";
|
||||
if (t.hasExitTime) {
|
||||
y += ", exitTime: true";
|
||||
}
|
||||
if (!t.cond.always && t.cond.valid) {
|
||||
y += ", cond: \"" + CondToString(t.cond) + "\"";
|
||||
}
|
||||
y += ", fade: " + FmtF(t.fade) + "}\n";
|
||||
}
|
||||
std::ofstream f(path, std::ios::binary | std::ios::trunc);
|
||||
if (!f) {
|
||||
ENGINE_ERROR("AnimationController: cannot write '{}'", path);
|
||||
return false;
|
||||
}
|
||||
f.write(y.data(),
|
||||
static_cast<std::streamsize>(y.size()));
|
||||
ENGINE_INFO("AnimationController: saved '{}' ({} states, "
|
||||
"{} transitions)",
|
||||
path, m_States.size(), m_Transitions.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Engine::Animation
|
||||
@@ -0,0 +1,247 @@
|
||||
//
|
||||
// AnimationEvaluator 实现:采样内核自 SceneRenderer 原样迁移(数学零
|
||||
// 改动,行为不变),补 ComputeRestPose 供骨骼可视化用。
|
||||
//
|
||||
|
||||
#include <Engine/Animation/AnimationEvaluator.h>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
// 静止 local TRS 分解(列主序 SRT 矩阵;假设无负缩放/剪切)。
|
||||
// 被动画轨道触碰的节点用关键帧重建,未触碰的保持原矩阵——
|
||||
// FBX PreRotation 折在 localTransform 里,拆 TRS 重建会丢
|
||||
void DecomposeTRS(const glm::mat4& m, glm::vec3& pos, glm::quat& rot,
|
||||
glm::vec3& scl)
|
||||
{
|
||||
pos = glm::vec3(m[3]);
|
||||
scl = {glm::length(glm::vec3(m[0])),
|
||||
glm::length(glm::vec3(m[1])),
|
||||
glm::length(glm::vec3(m[2]))};
|
||||
const glm::mat3 r(glm::vec3(m[0]) / scl.x,
|
||||
glm::vec3(m[1]) / scl.y,
|
||||
glm::vec3(m[2]) / scl.z);
|
||||
rot = glm::quat_cast(r);
|
||||
}
|
||||
|
||||
// 关键帧采样:前后键插值(位置/缩放 lerp,旋转 slerp)。
|
||||
// key 数通常几十个,线性扫描对 cache 更友好
|
||||
glm::vec3 SampleKeys(const std::vector<PosKey>& keys, f32 t,
|
||||
const glm::vec3& rest)
|
||||
{
|
||||
if (keys.empty()) return rest;
|
||||
if (t <= keys.front().time) return keys.front().value;
|
||||
for (size_t i = 0; i + 1 < keys.size(); ++i) {
|
||||
if (t < keys[i + 1].time) {
|
||||
const f32 a = keys[i].time, b = keys[i + 1].time;
|
||||
return glm::mix(keys[i].value, keys[i + 1].value,
|
||||
b > a ? (t - a) / (b - a) : 0.0f);
|
||||
}
|
||||
}
|
||||
return keys.back().value;
|
||||
}
|
||||
|
||||
glm::vec3 SampleKeys(const std::vector<SclKey>& keys, f32 t,
|
||||
const glm::vec3& rest)
|
||||
{
|
||||
if (keys.empty()) return rest;
|
||||
if (t <= keys.front().time) return keys.front().value;
|
||||
for (size_t i = 0; i + 1 < keys.size(); ++i) {
|
||||
if (t < keys[i + 1].time) {
|
||||
const f32 a = keys[i].time, b = keys[i + 1].time;
|
||||
return glm::mix(keys[i].value, keys[i + 1].value,
|
||||
b > a ? (t - a) / (b - a) : 0.0f);
|
||||
}
|
||||
}
|
||||
return keys.back().value;
|
||||
}
|
||||
|
||||
glm::quat SampleKeys(const std::vector<RotKey>& keys, f32 t,
|
||||
const glm::quat& rest)
|
||||
{
|
||||
if (keys.empty()) return rest;
|
||||
if (t <= keys.front().time) return keys.front().value;
|
||||
for (size_t i = 0; i + 1 < keys.size(); ++i) {
|
||||
if (t < keys[i + 1].time) {
|
||||
const f32 a = keys[i].time, b = keys[i + 1].time;
|
||||
return glm::slerp(keys[i].value, keys[i + 1].value,
|
||||
b > a ? (t - a) / (b - a) : 0.0f);
|
||||
}
|
||||
}
|
||||
return keys.back().value;
|
||||
}
|
||||
|
||||
// 单 clip 在 time 的全节点局部 TRS(无轨道分量 = 静止分解值)
|
||||
void SampleLocalPose(const Model& model, const AnimationClip& clip,
|
||||
f32 time, LocalPose& out)
|
||||
{
|
||||
const auto& nodes = model.GetNodes();
|
||||
const size_t n = nodes.size();
|
||||
out.pos.resize(n);
|
||||
out.rot.resize(n);
|
||||
out.scl.resize(n);
|
||||
out.touched.assign(n, 0);
|
||||
for (u32 i = 0; i < n; ++i) {
|
||||
DecomposeTRS(nodes[i].localTransform, out.pos[i], out.rot[i],
|
||||
out.scl[i]);
|
||||
}
|
||||
if (clip.duration <= 0.0f) return;
|
||||
const f32 t = glm::mod(time, clip.duration);
|
||||
for (const auto& ch : clip.channels) {
|
||||
if (ch.nodeIdx >= n) continue;
|
||||
out.touched[ch.nodeIdx] = 1;
|
||||
if (!ch.positions.empty())
|
||||
out.pos[ch.nodeIdx] =
|
||||
SampleKeys(ch.positions, t, out.pos[ch.nodeIdx]);
|
||||
if (!ch.rotations.empty())
|
||||
out.rot[ch.nodeIdx] =
|
||||
SampleKeys(ch.rotations, t, out.rot[ch.nodeIdx]);
|
||||
if (!ch.scales.empty())
|
||||
out.scl[ch.nodeIdx] =
|
||||
SampleKeys(ch.scales, t, out.scl[ch.nodeIdx]);
|
||||
}
|
||||
}
|
||||
|
||||
// 局部 TRS → 全节点 global 矩阵(层级累乘;父序 DFS 保证父先算)
|
||||
void ComposeGlobals(const Model& model, const LocalPose& pose,
|
||||
std::vector<glm::mat4>& globals)
|
||||
{
|
||||
const auto& nodes = model.GetNodes();
|
||||
globals.resize(nodes.size());
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
const glm::mat4 local = pose.touched[i]
|
||||
? glm::translate(glm::mat4(1.0f), pose.pos[i]) *
|
||||
glm::mat4_cast(pose.rot[i]) *
|
||||
glm::scale(glm::mat4(1.0f), pose.scl[i])
|
||||
: nodes[i].localTransform;
|
||||
globals[i] = nodes[i].parent >= 0
|
||||
? globals[static_cast<u32>(nodes[i].parent)] * local
|
||||
: local;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AnimationEvaluator::Evaluate(const Model& model,
|
||||
const AnimationClip& cur, f32 curTime,
|
||||
const CrossfadeState& xf,
|
||||
std::vector<glm::mat4>& outGlobals,
|
||||
bool inPlace)
|
||||
{
|
||||
// crossfade 双层 = N 层混合的特例(Direct 模式与 Controller
|
||||
// 过渡共用同一套加权数学)。两权重和恒为 1,渐进归一化后精确
|
||||
// 等价 mix(src, cur, wCur):起点全 src、终点全 cur,全程平滑
|
||||
//(cur 恒 1.0 的话起点就瞬间跳 50%,fade 有效时长减半——割裂感)
|
||||
PoseLayer layers[2];
|
||||
layers[0] = {&cur, curTime, xf.wCur};
|
||||
u32 count = 1;
|
||||
if (xf.src != nullptr) {
|
||||
layers[1] = {xf.src, xf.srcTime, 1.0f - xf.wCur};
|
||||
count = 2;
|
||||
}
|
||||
EvaluateLayers(model, layers, count, outGlobals, inPlace);
|
||||
}
|
||||
|
||||
void AnimationEvaluator::EvaluateLayers(const Model& model,
|
||||
const PoseLayer* layers,
|
||||
u32 count,
|
||||
std::vector<glm::mat4>& outGlobals,
|
||||
bool inPlace)
|
||||
{
|
||||
// ---- 逐层采样(容量跨帧复用;nullptr/零权重层跳过)----
|
||||
if (m_Layers.size() < count) {
|
||||
m_Layers.resize(count); // 只扩不缩,采样循环内元素地址稳定
|
||||
}
|
||||
m_Sampled.clear();
|
||||
for (u32 i = 0; i < count; ++i) {
|
||||
if (layers[i].clip == nullptr || layers[i].weight <= 0.0f) {
|
||||
continue;
|
||||
}
|
||||
SampleLocalPose(model, *layers[i].clip, layers[i].time,
|
||||
m_Layers[m_Sampled.size()]);
|
||||
m_Sampled.push_back({&m_Layers[m_Sampled.size()],
|
||||
layers[i].weight});
|
||||
}
|
||||
|
||||
if (m_Sampled.empty()) {
|
||||
// 无有效层 = rest(m_A untouched = localTransform 原样)
|
||||
const size_t n = model.GetNodes().size();
|
||||
m_A.pos.resize(n);
|
||||
m_A.rot.resize(n);
|
||||
m_A.scl.resize(n);
|
||||
m_A.touched.assign(n, 0);
|
||||
ComposeGlobals(model, m_A, outGlobals);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 加权归一化混合进 m_A:渐进混合(基层 = 首个有效层,
|
||||
// 逐层以 wi/(acc+wi) 混入;对线性量等价 Σwi·xi/Σwi,
|
||||
// 对旋转是顺序稳定的渐进 slerp——层级混合的通行做法)----
|
||||
{
|
||||
const LocalPose& base = *m_Sampled[0].pose;
|
||||
const size_t n = base.pos.size();
|
||||
m_A.pos.resize(n);
|
||||
m_A.rot.resize(n);
|
||||
m_A.scl.resize(n);
|
||||
m_A.touched.resize(n);
|
||||
for (size_t k = 0; k < n; ++k) {
|
||||
m_A.pos[k] = base.pos[k];
|
||||
m_A.rot[k] = base.rot[k];
|
||||
m_A.scl[k] = base.scl[k];
|
||||
m_A.touched[k] = base.touched[k];
|
||||
}
|
||||
f32 acc = m_Sampled[0].weight;
|
||||
for (size_t l = 1; l < m_Sampled.size(); ++l) {
|
||||
const LocalPose& src = *m_Sampled[l].pose;
|
||||
const f32 w =
|
||||
m_Sampled[l].weight / (acc + m_Sampled[l].weight);
|
||||
for (size_t k = 0; k < n; ++k) {
|
||||
// touched = 各层轨道并集(重建集合只扩大)
|
||||
m_A.touched[k] =
|
||||
static_cast<u8>(m_A.touched[k] | src.touched[k]);
|
||||
m_A.pos[k] = glm::mix(m_A.pos[k], src.pos[k], w);
|
||||
m_A.scl[k] = glm::mix(m_A.scl[k], src.scl[k], w);
|
||||
// 旋转必须 slerp(lerp 单位四元数会走样)
|
||||
m_A.rot[k] = glm::slerp(m_A.rot[k], src.rot[k], w);
|
||||
}
|
||||
acc += m_Sampled[l].weight;
|
||||
}
|
||||
}
|
||||
// ---- 根运动裁剪(inPlace,Unity "Apply Root Motion" 关闭同
|
||||
// 语义):动画子树根节点(touched 且父未被触碰)位移回
|
||||
// 静止值,旋转/缩放保留——跑动 clip 不再漂出实体锚点,
|
||||
// 位移归脚本/物理驱动。混合后统一裁剪,crossfade 双层
|
||||
// 与 BlendTree N 层一个入口全覆盖
|
||||
if (inPlace) {
|
||||
const auto& nodes = model.GetNodes();
|
||||
for (size_t k = 0; k < nodes.size(); ++k) {
|
||||
if (!m_A.touched[k]) continue;
|
||||
const i32 parent = nodes[k].parent;
|
||||
if (parent < 0 ||
|
||||
!m_A.touched[static_cast<size_t>(parent)]) {
|
||||
// FBX PreRotation 折在 localTransform 里,静止位移
|
||||
// 直接取平移列(不拆 TRS 重建,避免丢 PreRotation)
|
||||
m_A.pos[k] = glm::vec3(nodes[k].localTransform[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
ComposeGlobals(model, m_A, outGlobals);
|
||||
}
|
||||
|
||||
void AnimationEvaluator::ComputeRestPose(
|
||||
const Model& model, std::vector<glm::mat4>& outGlobals)
|
||||
{
|
||||
const auto& nodes = model.GetNodes();
|
||||
outGlobals.resize(nodes.size());
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
outGlobals[i] = nodes[i].parent >= 0
|
||||
? outGlobals[static_cast<u32>(nodes[i].parent)] *
|
||||
nodes[i].localTransform
|
||||
: nodes[i].localTransform;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Engine::Animation
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user