Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
beba491bcb | ||
|
|
334819bf97 | ||
|
|
f15b104e77 | ||
|
|
6854d53f11 | ||
|
|
505217ff7e | ||
|
|
f14aba4238 | ||
|
|
6447b5c0fa | ||
|
|
235482733c | ||
|
|
c43adc339d | ||
|
|
2cf8d6e076 | ||
|
|
affa1a7a56 | ||
|
|
d4d37b4b28 | ||
|
|
21da38db92 | ||
|
|
f20578bc4b | ||
|
|
bca50976f3 | ||
|
|
d2701a3d9b | ||
|
|
ed7cb417bd | ||
|
|
b89f431896 |
@@ -19,6 +19,8 @@
|
||||
#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>
|
||||
@@ -60,6 +62,8 @@ void Editor::Init(const EditorContext& ctx)
|
||||
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();
|
||||
@@ -219,6 +223,8 @@ 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 门控
|
||||
@@ -236,6 +242,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)
|
||||
@@ -298,11 +309,18 @@ void Editor::DrawMenuBar()
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -428,6 +446,7 @@ void Editor::DrawDockSpace()
|
||||
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
|
||||
|
||||
@@ -29,6 +29,8 @@ class HierarchyPanel;
|
||||
class InspectorPanel;
|
||||
class ViewportPanel;
|
||||
class StatsPanel;
|
||||
class LogPanel;
|
||||
class GamepadPanel;
|
||||
class AssetBrowserPanel;
|
||||
class MaterialEditorPanel;
|
||||
class ControllerEditorPanel;
|
||||
@@ -126,6 +128,8 @@ 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;
|
||||
@@ -137,6 +141,7 @@ 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;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Serialization/SceneSerializer.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
@@ -27,10 +28,12 @@ namespace {
|
||||
// 类型过滤(下标即身份):0 = All
|
||||
constexpr const char* kTypeNames[] = {"All", "Models",
|
||||
"Textures", "Scenes",
|
||||
"Materials", "Controllers"};
|
||||
"Materials", "Controllers",
|
||||
"Prefabs"};
|
||||
|
||||
// 资产类别:按扩展名分派(过滤用)
|
||||
enum class Kind { Model, Texture, Scene, Material, AnimClip, Other };
|
||||
enum class Kind { Model, Texture, Scene, Material, AnimClip, Prefab,
|
||||
Other };
|
||||
|
||||
Kind KindOf(const std::filesystem::path& p)
|
||||
{
|
||||
@@ -59,6 +62,10 @@ namespace {
|
||||
{
|
||||
return Kind::AnimClip;
|
||||
}
|
||||
if (ext == ".prefab")
|
||||
{
|
||||
return Kind::Prefab;
|
||||
}
|
||||
return Kind::Other;
|
||||
}
|
||||
|
||||
@@ -71,6 +78,7 @@ namespace {
|
||||
case 3: return kind == Kind::Scene;
|
||||
case 4: return kind == Kind::Material;
|
||||
case 5: return kind == Kind::AnimClip;
|
||||
case 6: return kind == Kind::Prefab;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
@@ -85,6 +93,7 @@ namespace {
|
||||
case Kind::Scene: return "[S]";
|
||||
case Kind::Material: return "[Ma]";
|
||||
case Kind::AnimClip: return "[An]";
|
||||
case Kind::Prefab: return "[Pf]";
|
||||
default: return "[ ]";
|
||||
}
|
||||
}
|
||||
@@ -121,7 +130,7 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(110.0f);
|
||||
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 6);
|
||||
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 7);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(140.0f);
|
||||
ImGui::InputTextWithHint("##name", "filter", m_NameFilter,
|
||||
@@ -296,6 +305,26 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// .prefab 拖拽源:拖到 Outliner(节点=挂其下 / 空白区=挂根)
|
||||
// 实例化整棵子树(kPrefabDragPayload)
|
||||
if (item.kind == Kind::Prefab && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kPrefabDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// 模型拖拽源:拖到 Outliner 落点创建带 MeshRenderer 的实体
|
||||
//(kModelDragPayload;实体名 = 文件名)
|
||||
if (item.kind == Kind::Model && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kModelDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// 双击:.material 打开 Material Editor;.animctrl 打开
|
||||
// Controller Editor;model/HDR 进缓存预热(路径去重,重复
|
||||
// 双击零成本);场景加载含环境恢复/选中清理流程,提示走
|
||||
@@ -328,6 +357,16 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
|
||||
assetPath);
|
||||
}
|
||||
else if (item.kind == Kind::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);
|
||||
}
|
||||
}
|
||||
|
||||
// hover 信息卡(UE Content Browser 式):类型/大小/修改时间/加载状态
|
||||
@@ -336,11 +375,12 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::Separator();
|
||||
// 与 Kind 枚举逐位对齐(Kind::Other = 5)——漏项 = tooltip
|
||||
// 与 Kind 枚举逐位对齐(Kind::Other = 6)——漏项 = tooltip
|
||||
// 数组越界读
|
||||
static const char* kKindNames[] = {"Model", "Texture/Environment",
|
||||
"Scene", "Material",
|
||||
"Controller", "File"};
|
||||
"Controller", "Prefab",
|
||||
"File"};
|
||||
ImGui::Text("type: %s", kKindNames[static_cast<int>(item.kind)]);
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
@@ -374,21 +414,26 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||
std::localtime(&tt));
|
||||
ImGui::Text("modified: %s", tmBuf);
|
||||
}
|
||||
// 加载状态(只查询不触发加载;model/HDR 进过缓存才显示 loaded)
|
||||
// 加载状态:只对"加载"型资产有意义(查询不触发加载)。
|
||||
// Prefab 是实例化模板、Scene 走 File 菜单——显示 "not
|
||||
// loaded" 会误导(曾让用户当成 bug)
|
||||
if (ctx.assets)
|
||||
{
|
||||
const char* state = "not loaded";
|
||||
if (item.kind == Kind::Model &&
|
||||
ctx.assets->IsModelLoaded(assetPath))
|
||||
if (item.kind == Kind::Model ||
|
||||
item.kind == Kind::Texture)
|
||||
{
|
||||
state = "loaded";
|
||||
const bool loaded =
|
||||
(item.kind == Kind::Model)
|
||||
? ctx.assets->IsModelLoaded(assetPath)
|
||||
: ctx.assets->IsEnvironmentLoaded(assetPath);
|
||||
ImGui::Text("state: %s", loaded ? "loaded"
|
||||
: "not loaded");
|
||||
}
|
||||
else if (item.kind == Kind::Texture &&
|
||||
ctx.assets->IsEnvironmentLoaded(assetPath))
|
||||
else if (item.kind == Kind::Prefab)
|
||||
{
|
||||
state = "loaded";
|
||||
ImGui::TextDisabled(
|
||||
"template — drag into scene / double-click");
|
||||
}
|
||||
ImGui::Text("state: %s", state);
|
||||
// 已加载的 model 展开资产数据概览(meshes=0 即纯动画
|
||||
// 容器;未加载不显示——查询不触发加载)
|
||||
if (item.kind == Kind::Model)
|
||||
|
||||
@@ -18,6 +18,15 @@
|
||||
#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);
|
||||
|
||||
@@ -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
|
||||
@@ -7,12 +7,16 @@
|
||||
//
|
||||
|
||||
#include "HierarchyPanel.h"
|
||||
#include "AssetBrowserPanel.h" // kPrefabDragPayload(Content Browser 源)
|
||||
#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>
|
||||
|
||||
@@ -33,6 +37,58 @@ namespace {
|
||||
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);
|
||||
}
|
||||
|
||||
// 实体名 → 合法文件名(路径分隔符/保留符净化;空名兜底)
|
||||
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)
|
||||
@@ -78,7 +134,9 @@ namespace {
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变)----
|
||||
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变);
|
||||
// Prefab 拖入 = 实例化子树后挂到本实体下(keepWorld 保持
|
||||
// 授权世界位姿)----
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kHierPayload)) {
|
||||
@@ -91,6 +149,26 @@ namespace {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -102,6 +180,19 @@ namespace {
|
||||
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);
|
||||
@@ -194,7 +285,8 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
|
||||
DrawEntityNode(ctx, e, selected);
|
||||
});
|
||||
|
||||
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层)----
|
||||
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层);
|
||||
// Prefab 拖入 = 实例化到场景根(授权位姿)----
|
||||
ImVec2 rem = ImGui::GetContentRegionAvail();
|
||||
if (rem.y > 4.0f) {
|
||||
ImGui::InvisibleButton("##hier_drop_root", rem);
|
||||
@@ -206,6 +298,22 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
|
||||
#include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names)
|
||||
#include "Engine/Scripting/ScriptRuntime.h" // 参数编辑 → RefreshParams
|
||||
#include "Engine/Serialization/SceneSerializer.h" // ResolveProcedural(程序化入口)
|
||||
#include "Engine/Core/Log.h" // Create Override 失败告警
|
||||
|
||||
#include <imgui.h>
|
||||
@@ -197,6 +198,12 @@ namespace
|
||||
|
||||
for (u32 i = 0; i < meshes.size(); ++i)
|
||||
{
|
||||
// 「mesh」下拉的过滤:选了具体子 mesh 就只列该槽位(与渲染/
|
||||
// 物理的子引用语义一致);All meshes(-1)= 全列
|
||||
if (mr.meshIndex >= 0 && static_cast<int>(i) != mr.meshIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
|
||||
const std::string meshName =
|
||||
@@ -670,6 +677,69 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
mr->materialOverrides.clear();
|
||||
}
|
||||
ImGui::Spacing();
|
||||
// 程序化模型入口:伪资产不在磁盘(下拉框列不到),由
|
||||
// 弹窗组键生成 → ResolveProcedural(与场景反解同一
|
||||
// 键格式唯一定义点;确定性键必然命中缓存)
|
||||
if (ImGui::SmallButton("Procedural..."))
|
||||
{
|
||||
ImGui::OpenPopup("##proc_model");
|
||||
}
|
||||
if (ImGui::BeginPopup("##proc_model"))
|
||||
{
|
||||
static int shape = 0; // 0=cube 1=plane 2=transparent plane
|
||||
static float half = 0.5f;
|
||||
static float col[3] = {0.55f, 0.36f, 0.28f};
|
||||
static float alpha = 0.5f;
|
||||
ImGui::RadioButton("Cube", &shape, 0);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton("Plane", &shape, 1);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton("Trans.Plane", &shape, 2);
|
||||
// 固定宽度而非 -1:弹窗是 AlwaysAutoResize,-1(撑满可用
|
||||
// 宽度)会拿上一帧宽度当基准 → 逐帧膨胀直到撑满屏幕
|
||||
ImGui::SetNextItemWidth(180.0f);
|
||||
ImGui::SliderFloat("half size", &half, 0.01f, 10.0f,
|
||||
"%.2f");
|
||||
ImGui::ColorEdit3("color", col);
|
||||
if (shape == 2)
|
||||
{
|
||||
ImGui::SetNextItemWidth(180.0f);
|
||||
ImGui::SliderFloat("alpha", &alpha, 0.0f, 1.0f,
|
||||
"%.2f");
|
||||
}
|
||||
if (ImGui::Button("Create & Assign"))
|
||||
{
|
||||
const int r = int(col[0] * 255.0f + 0.5f);
|
||||
const int g = int(col[1] * 255.0f + 0.5f);
|
||||
const int b = int(col[2] * 255.0f + 0.5f);
|
||||
char key[96];
|
||||
if (shape == 2)
|
||||
{
|
||||
const int a = int(alpha * 255.0f + 0.5f);
|
||||
std::snprintf(
|
||||
key, sizeof(key),
|
||||
"procedural://transparent-plane/%.3g/%d,%d,%d,%d",
|
||||
half, r, g, b, a);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::snprintf(
|
||||
key, sizeof(key),
|
||||
"procedural://%s/%.3g/%d,%d,%d",
|
||||
shape == 0 ? "cube" : "plane", half, r, g, b);
|
||||
}
|
||||
if (auto ref = Engine::Serialization::
|
||||
SceneSerializer::ResolveProcedural(
|
||||
*ctx.assets, key);
|
||||
ref.Valid())
|
||||
{
|
||||
mr->model = ref;
|
||||
mr->materialOverrides.clear();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
// 无效句柄不进 GetModel(缓存对无效/悬空句柄 FATAL,是 bug 信号)
|
||||
if (mr->model.Valid())
|
||||
@@ -677,6 +747,40 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
|
||||
model != nullptr)
|
||||
{
|
||||
// 单 mesh 子引用下拉(-1 = 整模型全部):名字用
|
||||
// GetMeshMaterialInfos(材质名优先,空回退 Slot N)
|
||||
{
|
||||
const auto& infos = model->GetMeshMaterialInfos();
|
||||
const int sel = mr->meshIndex + 1; // 0 = All
|
||||
std::string preview = "All meshes";
|
||||
if (sel > 0 &&
|
||||
static_cast<size_t>(sel - 1) < infos.size()) {
|
||||
preview = infos[static_cast<size_t>(sel) - 1]
|
||||
.name.empty()
|
||||
? "Slot " + std::to_string(sel - 1)
|
||||
: infos[static_cast<size_t>(sel) - 1].name;
|
||||
}
|
||||
if (ImGui::BeginCombo("mesh", preview.c_str()))
|
||||
{
|
||||
if (ImGui::Selectable("All meshes", sel <= 0)) {
|
||||
mr->meshIndex = -1;
|
||||
}
|
||||
for (size_t i = 0; i < infos.size(); ++i)
|
||||
{
|
||||
const std::string label =
|
||||
(infos[i].name.empty()
|
||||
? "Slot " + std::to_string(i)
|
||||
: infos[i].name) +
|
||||
"##" + std::to_string(i);
|
||||
const bool cur =
|
||||
mr->meshIndex == static_cast<int>(i);
|
||||
if (ImGui::Selectable(label.c_str(), cur)) {
|
||||
mr->meshIndex = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
const auto& clips = model->GetClips();
|
||||
if (model->GetMeshes().empty())
|
||||
{
|
||||
@@ -1175,6 +1279,8 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
ImGui::DragFloat("Radius", &cc->radius, 0.02f, 0.05f, 5.0f);
|
||||
ImGui::DragFloat("Half Height", &cc->capsuleHalfHeight, 0.02f, 0.0f, 5.0f);
|
||||
ImGui::DragFloat("Mass", &cc->mass, 1.0f, 1.0f, 500.0f, "%.0f");
|
||||
// 形状附加偏移:叠加在脚底约定之上(模型 pivot 对齐用)
|
||||
ImGui::DragFloat3("Shape Offset", &cc->shapeOffset.x, 0.02f, -5.0f, 5.0f);
|
||||
// ---- Movement:扫掠运动学参数 ----
|
||||
ImGui::SeparatorText("Movement");
|
||||
ImGui::DragFloat("Max Slope (deg)", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f);
|
||||
@@ -1805,11 +1911,31 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
// 警告跳过)----
|
||||
ImGui::SeparatorText("Clips");
|
||||
const auto& files = AssetModelFiles();
|
||||
std::vector<const char*> opts;
|
||||
opts.push_back("<attach animation file...>");
|
||||
// 显示真名(AssetDisplayName:文件 stem,导入资产库的
|
||||
// 哈希 "asset" 读同目录 pathname 取原始名);重名者补
|
||||
// 父目录区分——不再把哈希路径直接摆给用户(= 显示成 id)
|
||||
std::vector<std::string> labels;
|
||||
labels.reserve(files.size());
|
||||
for (const auto& f : files)
|
||||
{
|
||||
opts.push_back(f.c_str());
|
||||
std::string label = Engine::Model::AssetDisplayName(f);
|
||||
if (std::find(labels.begin(), labels.end(), label) !=
|
||||
labels.end())
|
||||
{
|
||||
label += " (" +
|
||||
std::filesystem::path(f)
|
||||
.parent_path()
|
||||
.filename()
|
||||
.string() +
|
||||
")";
|
||||
}
|
||||
labels.push_back(std::move(label));
|
||||
}
|
||||
std::vector<const char*> opts;
|
||||
opts.push_back("<attach animation file...>");
|
||||
for (const auto& l : labels)
|
||||
{
|
||||
opts.push_back(l.c_str());
|
||||
}
|
||||
int sel = 0;
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
@@ -1829,7 +1955,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
}
|
||||
// attach 遥测(骨骼匹配结果,面板下方显示)
|
||||
s_LastAttachFile =
|
||||
std::filesystem::path(path).filename().string();
|
||||
Engine::Model::AssetDisplayName(path);
|
||||
s_LastAttachMatched =
|
||||
res.channelsMatched + res.channelsRetargeted;
|
||||
s_LastAttachRetargeted = res.channelsRetargeted;
|
||||
@@ -1842,9 +1968,8 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
for (size_t i = 0; i < anim->attachedAnimPaths.size(); ++i)
|
||||
{
|
||||
const auto& p = anim->attachedAnimPaths[i];
|
||||
ImGui::BulletText("%s", std::filesystem::path(p)
|
||||
.filename()
|
||||
.string()
|
||||
ImGui::BulletText("%s",
|
||||
Engine::Model::AssetDisplayName(p)
|
||||
.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
@@ -1951,6 +2076,10 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
// CrossFade 与 Direct 的 crossfade 共用 fade)
|
||||
ImGui::SliderFloat("Speed", &anim->speed, -2.0f, 2.0f);
|
||||
ImGui::SliderFloat("Fade", &anim->fadeDuration, 0.0f, 1.0f);
|
||||
// 根运动(Unity Animator 同名开关):关 = 原地播放,
|
||||
// 位移归脚本/物理驱动(CharacterController 角色用)
|
||||
ImGui::Checkbox("Apply Root Motion",
|
||||
&anim->applyRootMotion);
|
||||
// 动画数据表:每个 clip 的时长与有效轨道数(attach 进来的
|
||||
// 外部动画在此核对骨骼匹配结果——轨道数明显偏少 = 骨骼
|
||||
// 命名不同源被跳过)
|
||||
|
||||
@@ -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
|
||||
@@ -173,13 +173,44 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
||||
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));
|
||||
EditParam(ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f,
|
||||
1.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);
|
||||
}
|
||||
|
||||
// ---- 纹理槽(变化 → 落盘 + waitIdle 重建)----
|
||||
ImGui::SeparatorText("Textures");
|
||||
@@ -205,6 +236,13 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
||||
}
|
||||
if (ImGui::BeginPopup("tex_pick"))
|
||||
{
|
||||
// 清除入口(Unity/UE 语义):该槽置空 → 回退兜底贴图
|
||||
if (ImGui::Selectable("<none>"))
|
||||
{
|
||||
pendingSlot = static_cast<int>(s);
|
||||
pendingPath.clear();
|
||||
}
|
||||
ImGui::Separator();
|
||||
for (const auto& f : EditorMaterialUtil::AssetTextureFiles())
|
||||
{
|
||||
if (ImGui::Selectable(f.c_str()))
|
||||
@@ -302,10 +340,18 @@ void MaterialEditorPanel::DrawBuiltinMode(const EditorContext& ctx)
|
||||
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::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f, 1.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",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#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>
|
||||
@@ -208,6 +209,26 @@ void StatsPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
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();
|
||||
|
||||
@@ -234,6 +234,14 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
// 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 无任何轴位,
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#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>
|
||||
@@ -146,10 +145,6 @@ namespace
|
||||
SetImGuiEnabled(!GetImGuiEnabled());
|
||||
}
|
||||
#endif
|
||||
if (event.key == static_cast<int>(Input::Key::F1))
|
||||
{
|
||||
STATS.PrintComparison();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,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,17 @@
|
||||
//
|
||||
// ScriptLog.h:脚本日志门面(ScriptDbg 的文字版同位物)。
|
||||
//
|
||||
// 脚本打日志统一走这里——直接转发引擎 spdlog 默认 logger(终端
|
||||
// + HelloVulkan.log + 编辑器 Log Panel 三处同收)。约定:消息开头
|
||||
// 写 [脚本名] 前缀(面板过滤好定位):
|
||||
// ScriptLog::Info("[WASDMove] jump");
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#define SLOG_TRACE(...) ::spdlog::trace(__VA_ARGS__)
|
||||
#define SLOG_DEBUG(...) ::spdlog::debug(__VA_ARGS__)
|
||||
#define SLOG_INFO(...) ::spdlog::info(__VA_ARGS__)
|
||||
#define SLOG_WARN(...) ::spdlog::warn(__VA_ARGS__)
|
||||
#define SLOG_ERROR(...) ::spdlog::error(__VA_ARGS__)
|
||||
@@ -0,0 +1,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,188 @@
|
||||
//
|
||||
// ThirdPersonCamera.cpp:第三人称环绕相机(UE SpringArm 同位)。
|
||||
//
|
||||
// 挂 primary 相机实体。鼠标增量 → yaw/pitch,相机位置 = 目标实体
|
||||
// (按名解析,默认 "player")+ 球面偏移,朝向锁定目标。WASDMove 的
|
||||
// 移动参考 = 本相机朝向——WASD 即"相机水平朝向的前后左右"(UE
|
||||
// gameplay 语义)。枢轴水平硬跟、竖直带阻尼(FollowResponse):XZ
|
||||
// 与角色零滞后(跑动不错位),Y 滤掉脚底每物理步的微弹跳(踩碎块/
|
||||
// 撞墙)避免画面抖动。
|
||||
//
|
||||
// OnStart 从编辑器摆的机位反推 yaw/pitch/dist:编辑器摆哪,游戏就
|
||||
// 从哪个角度开场。Esc 释放光标(调参),左键重捕继续玩。
|
||||
//
|
||||
|
||||
#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 <entt/entt.hpp>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using namespace Engine;
|
||||
|
||||
namespace {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
auto& camT = Transform();
|
||||
camT.SetPosition(pivot + dir * m_Distance);
|
||||
|
||||
// ---- 朝向锁定目标 ----
|
||||
// 引擎 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; // 首帧吸附基准
|
||||
float m_Yaw = 0.0f;
|
||||
float m_Pitch = 20.0f;
|
||||
entt::entity m_Target = entt::null;
|
||||
};
|
||||
|
||||
ENGINE_REGISTER_SCRIPT(ThirdPersonCamera)
|
||||
|
||||
} // namespace
|
||||
+76
-28
@@ -1,5 +1,9 @@
|
||||
//
|
||||
// WASDMove.cpp:WASD 控制实体移动(相机水平朝向参考,UE gameplay 语义)。
|
||||
// WASDMove.cpp:第三人称移动(WASD/手柄摇杆,相机朝向参考,UE gameplay 语义)。
|
||||
//
|
||||
// 输入走引擎具名动作层(Move/Jump/Run 默认绑定):WASD + 方向键 +
|
||||
// 手柄左摇杆移动(引擎径向死区),Space/手柄 A 跳;跑 = Run 动作
|
||||
//(Shift / 手柄 L3 按下左摇杆)。摇杆模拟量直接驱动动量整形(轻推慢走)。
|
||||
//
|
||||
// 移动语义分派(按实体配置自动选择,优先级从上到下):
|
||||
// - CharacterControllerComponent → 角色控制器路径(扫掠运动学:
|
||||
@@ -19,6 +23,7 @@
|
||||
#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>
|
||||
|
||||
@@ -49,37 +54,68 @@ namespace {
|
||||
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(升降/跳跃归物理或将来扩展)。
|
||||
// 输入直读按键(Unity Input.GetKey(KeyCode.W) 同形);UI 捕获
|
||||
// 键盘时门面静默。要手柄/运行时改键再换动作层
|
||||
//(Input::ActionValue2("Move")——Unity6 新 Input System /
|
||||
// UE5 Enhanced Input 对应物),本示例保持最简
|
||||
const glm::quat cq = App().GetRenderer()
|
||||
// 输入 = 具名动作层(Unity6 Input System / UE5 Enhanced Input
|
||||
// 同位):Move 默认混绑 WASD/方向键/左摇杆(引擎做径向死区 +
|
||||
// 单位圆钳制,斜向不超速),Jump 混绑 Space/手柄 A——键盘
|
||||
// 手柄一套代码,将来改键/加绑定不进脚本
|
||||
// 移动参考 = 场景 primary 相机(第三人称相机脚本每帧写它
|
||||
// 的朝向——玩家看到的视角就是移动参考);无相机实体时
|
||||
//(纯独立形态)回退编辑器相机
|
||||
glm::quat cq;
|
||||
bool refCam = false;
|
||||
auto camView = App().GetScene()
|
||||
.GetRegistry()
|
||||
.view<Engine::ECS::CameraComponent>();
|
||||
for (auto e : camView) {
|
||||
const auto& cam =
|
||||
camView.get<Engine::ECS::CameraComponent>(e);
|
||||
if (cam.primary && cam.active) {
|
||||
cq = App().GetScene()
|
||||
.GetRegistry()
|
||||
.get<Engine::ECS::TransformComponent>(e)
|
||||
.GetRotationQuat();
|
||||
refCam = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!refCam) {
|
||||
cq = App().GetRenderer()
|
||||
.GetEditorCamera()
|
||||
.GetTransform()
|
||||
.GetRotationQuat();
|
||||
}
|
||||
const glm::vec3 fwd = cq * glm::vec3(0.0f, 0.0f, -1.0f);
|
||||
const glm::vec3 right = cq * glm::vec3(1.0f, 0.0f, 0.0f);
|
||||
|
||||
// 约定:X+ = 右(D),Y- = 前(W);方向合成 = fwd*(-Y) + right*(+X)
|
||||
glm::vec2 mv(0.0f);
|
||||
if (Input::IsKeyDown(Input::Key::W)) mv.y -= 1.0f;
|
||||
if (Input::IsKeyDown(Input::Key::S)) mv.y += 1.0f;
|
||||
if (Input::IsKeyDown(Input::Key::A)) mv.x -= 1.0f;
|
||||
if (Input::IsKeyDown(Input::Key::D)) mv.x += 1.0f;
|
||||
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 bool moving = glm::length(mv) > 0.1f;
|
||||
const bool running =
|
||||
moving && Input::IsKeyDown(Input::Key::LeftShift);
|
||||
// 走/跑: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; // 期望速度(输入意图)
|
||||
|
||||
@@ -95,6 +131,7 @@ namespace {
|
||||
// 松手滑行刹车。加速/转向用 AccelResponse,松手用更硬的
|
||||
// StopResponse(角色要"想停就停");空中操纵性降到 1/4
|
||||
//(起跳后不宜急转弯)。指数逼近,帧率无关
|
||||
const bool moving = glm::length(mvRaw) > 0.1f;
|
||||
float response = moving ? m_AccelResponse : m_StopResponse;
|
||||
if (!grounded) {
|
||||
response *= 0.25f;
|
||||
@@ -113,15 +150,14 @@ namespace {
|
||||
phy.SetCharacterVelocity(
|
||||
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
|
||||
|
||||
// 跳跃:着地 + 上升沿触发。IsKeyDown 是电平量(按住
|
||||
// 每帧都真),不设边沿会变成贴地火箭;IsGrounded 防
|
||||
// 空中连跳
|
||||
const bool jumpDown = Input::IsKeyDown(Input::Key::Space);
|
||||
if (jumpDown && !m_JumpWasDown &&
|
||||
// 跳跃:动作层边沿(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);
|
||||
}
|
||||
m_JumpWasDown = jumpDown;
|
||||
} else if (auto* rb =
|
||||
TryGetComponent<Engine::ECS::RigidBodyComponent>();
|
||||
rb != nullptr && rb->bodyValid &&
|
||||
@@ -155,11 +191,24 @@ namespace {
|
||||
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
|
||||
}
|
||||
|
||||
// ---- 动画:idle/walk/run 三态切换(Direct 模式 crossfade)----
|
||||
// 目标 clip 是"电平量":Shift 中途按下/松开都要能换,所以
|
||||
// 每帧重算目标,只在目标与上帧不同才 Play(边沿)。每帧无
|
||||
// 条件 Play 会不停重置播放头,动画永远播不完
|
||||
const std::string& target = !moving ? m_IdleClip : (running ? m_RunClip : m_WalkClip);
|
||||
// ---- 动画:按真实速度分档(迟滞防临界抖)----
|
||||
// 判据 = 动量整形后的真实水平速度,不是输入原始值:起步
|
||||
// 滑行期已是 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)) {
|
||||
@@ -221,7 +270,6 @@ namespace {
|
||||
std::string m_WalkClip = "walk";
|
||||
std::string m_RunClip = "run";
|
||||
std::string m_CurClip = "idle"; // 当前目标 clip(边沿检测基准)
|
||||
bool m_JumpWasDown = false; // 上帧跳跃键(上升沿检测)
|
||||
glm::vec3 m_PrevVel{0.0f}; // 上帧测量速度(加速度差分基准)
|
||||
glm::vec3 m_AccelS{0.0f}; // 平滑测量加速度(EMA 0.35,调试图)
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
+1
-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)
|
||||
|
||||
@@ -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)
|
||||
@@ -80,6 +87,7 @@ target_link_libraries(Engine PUBLIC
|
||||
spdlog::spdlog
|
||||
stb
|
||||
assimp::assimp
|
||||
ufbx
|
||||
meshoptimizer
|
||||
Jolt::Jolt
|
||||
yaml-cpp::yaml-cpp
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#define HELLOVULKAN_ANIMATIONATTACH_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -51,6 +52,16 @@ namespace Engine {
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -64,15 +64,19 @@ namespace Engine::Animation {
|
||||
// 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);
|
||||
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);
|
||||
u32 count, std::vector<glm::mat4>& outGlobals,
|
||||
bool inPlace = false);
|
||||
|
||||
// 绑定姿势:全节点 untouched = localTransform 原样累乘。
|
||||
// 与渲染 identity palette 的表现一致(无动画/组件禁用实体的
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
//
|
||||
// HumanoidRig:Unity Avatar 语义的跨骨架重定向映射。
|
||||
//
|
||||
// 资产组装现实:角色模型与动画 clip 是两套独立资产,骨骼命名完全
|
||||
// 不同源(英文/Mixamo/UE/Blender/MMD 日文)。Unity Humanoid 的解法
|
||||
// 是把任意命名骨架映射到标准人形槽位(Avatar bone mapping),动画
|
||||
// 经槽位转换后即可驱动任意人形角色。
|
||||
// 架构分层(对齐 Unity Mecanim / UE IK Retargeter 的做法):
|
||||
//
|
||||
// 这里实现其中枢:名字归一化 → 槽位识别(关键词表,en/Mixamo/MMD)
|
||||
// → 源/目标骨架间生成 node 下标映射对。同名直映是映射的特例。
|
||||
// ┌─ 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
|
||||
@@ -16,13 +27,24 @@
|
||||
#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,
|
||||
@@ -34,38 +56,113 @@ namespace Engine::Animation {
|
||||
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,
|
||||
LeftUpperLeg,
|
||||
LeftLowerLeg,
|
||||
LeftFoot,
|
||||
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 };
|
||||
|
||||
// 槽位显示名("Hips"/"LeftUpperArm"...;调试映射表用)
|
||||
// 指别(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}。识别失败 ok=false。
|
||||
// "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 直映优先(同源拆分资产零损耗),余下按槽位匹配;
|
||||
// 一槽多候选时取语义关键词优先级表中的顺序(先高后低)。
|
||||
// 同名 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
|
||||
|
||||
@@ -182,6 +182,18 @@ 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
|
||||
@@ -213,6 +225,9 @@ namespace Engine
|
||||
// 半透明平面(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);
|
||||
|
||||
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
|
||||
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
|
||||
|
||||
@@ -42,6 +42,9 @@ 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;
|
||||
|
||||
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
|
||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -203,6 +203,10 @@ namespace Engine::ECS {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -222,6 +226,11 @@ namespace Engine::ECS {
|
||||
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 由消费端处理)
|
||||
@@ -471,6 +480,9 @@ namespace Engine::ECS {
|
||||
// 重力倍率(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 习惯
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -102,6 +102,9 @@ namespace Engine {
|
||||
// 角色脚底位置(物理权威世界坐标):调试图元/特效锚点用它,
|
||||
// 而不是 Transform().GetPosition()——后者在有父级时是本地坐标
|
||||
glm::vec3 GetCharacterPosition(u32 entityBits) const;
|
||||
// 角色"撞击强度"速度(上一次扫掠前的速度):撞墙/碎墙等判定用它,
|
||||
// 而非 GetCharacterVelocity——后者已被 Jolt 抵消掉朝向墙面的分量
|
||||
glm::vec3 GetCharacterImpactVelocity(u32 entityBits) const;
|
||||
|
||||
private:
|
||||
// 创建时缓存的参数(变更检测 → 重建 body)
|
||||
@@ -138,7 +141,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)
|
||||
@@ -165,9 +169,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;
|
||||
|
||||
|
||||
@@ -47,7 +47,13 @@ namespace Engine {
|
||||
// 无填充歧义),加字段必须双端同步(simple_shader*.frag)
|
||||
struct MaterialParams {
|
||||
// x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
||||
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f};
|
||||
// 默认 = 电介质表面推荐值(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 /
|
||||
@@ -55,12 +61,25 @@ namespace Engine {
|
||||
// y = 透射率(0 = 普通表面),z = 自发光强度,w = 体积厚度
|
||||
//(折射屏幕偏移的世界距离,0 = 无弯曲直透)
|
||||
glm::vec4 surface{1.5f, 0.0f, 1.0f, 0.0f};
|
||||
// 杂项语义:x = unlit(KHR_materials_unlit,shader 早退直出 albedo)
|
||||
// 杂项语义:
|
||||
// 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,作用在本引擎
|
||||
// v-up 顶点 UV 空间,换算推导见 Model.cpp glTF 分支注释)。
|
||||
// 各异变换",收敛为 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 布局天然对齐
|
||||
@@ -68,6 +87,11 @@ namespace Engine {
|
||||
{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 {
|
||||
|
||||
@@ -47,8 +47,9 @@ namespace Engine {
|
||||
// 材质资产数据段(磁盘格式 ↔ 内存值,无 GPU 状态)。
|
||||
// texPaths = 文件系统相对 CWD 路径(与模型解析产物同源,glTF/FBX
|
||||
// 纹理可直接快照);空串 = 该槽用共享默认纹理兜底。
|
||||
// glb 内嵌纹理无独立文件路径 → 对 glb 模型 Create Override 时
|
||||
// 纹理槽为空(回退默认纹理),参数/透明照常可编辑。
|
||||
// glb 内嵌纹理在模型导入期已抽到 <模型目录>/textures/ 落盘并回写为
|
||||
// 真实路径(Model::ParseFromDisk),故此处与文件纹理同语义——覆盖
|
||||
// 材质可正常继承
|
||||
struct MaterialAssetData {
|
||||
std::array<std::string, Material::kSlotCount> texPaths;
|
||||
MaterialParams params;
|
||||
|
||||
@@ -137,6 +137,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,
|
||||
@@ -158,6 +164,15 @@ namespace Engine
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
f32 halfSize, u8 r, u8 g, u8 b, u8 a);
|
||||
|
||||
// 程序化立方体(6 面 24 顶点纯色贴图,halfSize = 半边长):
|
||||
// 通用占位几何(碎片模拟的墙/块等)。尺寸变化走实体 Transform
|
||||
// 缩放——同 halfSize 同色共享一份缓存模型
|
||||
struct CubeTag {};
|
||||
static Scope<Model> CreateCube(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
f32 halfSize, u8 r, u8 g, u8 b);
|
||||
|
||||
~Model();
|
||||
|
||||
Model(const Model&) = delete;
|
||||
@@ -209,6 +224,13 @@ namespace Engine
|
||||
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
|
||||
bool transparent);
|
||||
|
||||
// 程序化立方体骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 由
|
||||
// 立方体路径填(6 面,顶点法线朝外)
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
CubeTag, f32 halfSize, const std::array<u8, 4>& rgba);
|
||||
|
||||
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
|
||||
@@ -37,6 +37,7 @@ 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<Cubemap> LoadEnvironment(const std::string& path) override;
|
||||
Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||
DecodedEquirect&& decoded) override;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#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>
|
||||
@@ -69,6 +70,22 @@ namespace Engine {
|
||||
// 在此按名取值)
|
||||
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() {
|
||||
@@ -96,6 +113,38 @@ 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) {
|
||||
@@ -236,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( \
|
||||
|
||||
@@ -66,6 +66,10 @@ namespace Engine {
|
||||
|
||||
private:
|
||||
void Sync();
|
||||
// 碰撞事件派发(Update 开头、Sync 之前):PhysicsWorld 最近
|
||||
// 一次步进的事件转发到涉事实例的 OnCollisionEnter/Exit。
|
||||
// Persisted 跳过;实例未建/已禁用不收
|
||||
void DispatchCollisions();
|
||||
|
||||
ECS::Scene& m_Scene;
|
||||
Application* m_App; // 注入脚本上下文
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
#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; }
|
||||
@@ -66,6 +69,38 @@ namespace Engine::Serialization {
|
||||
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,
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,17 +1,42 @@
|
||||
//
|
||||
// AnimationAttach 实现:外部动画资产 → 目标模型并入。
|
||||
//
|
||||
// 轨道映射两级:
|
||||
// ① 同名直映——同源拆分资产(角色/动画同一骨架导出)零损耗;
|
||||
// ② Humanoid 槽位重定向——名字不同源的组装资产(角色 FBX +
|
||||
// 动画 FBX 各自制作),经 Unity Avatar 语义的标准人形槽位配对,
|
||||
// 逐 key 烘焙"rest→anim 的增量变换"到目标骨架局部空间:
|
||||
// 平移 out.pos = dstRestPos + (srcPos - srcRestPos)
|
||||
// 旋转 Δ = srcRot * inv(srcRestRot);out.rot = Δ * dstRestRot
|
||||
// (Δ 父空间左乘 = 骨骼自身摆动在两套骨架间等价传递)
|
||||
// 烘焙后 clip 即目标骨架空间数据,评估器零改动。
|
||||
// 五段管线(映射表一次构建后**冻结**,逐 clip 只读消费,杜绝循环内
|
||||
// 惰性登记/反向表重建):
|
||||
//
|
||||
// AssetManager 是 facade:取模型 + 解析磁盘,纯算法在这里。
|
||||
// ① 资产导入(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>
|
||||
@@ -28,11 +53,15 @@
|
||||
#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{};
|
||||
@@ -47,8 +76,7 @@ namespace Engine::Animation {
|
||||
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)};
|
||||
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));
|
||||
@@ -56,14 +84,336 @@ namespace Engine::Animation {
|
||||
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 AttachClipsToModel(Model& target, ParsedModel&& src,
|
||||
const std::string& sourcePath)
|
||||
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 : target.GetClips()) {
|
||||
for (const auto& c : existingClips) {
|
||||
if (c.sourcePath == sourcePath) {
|
||||
return res;
|
||||
}
|
||||
@@ -73,150 +423,215 @@ namespace Engine::Animation {
|
||||
return res;
|
||||
}
|
||||
|
||||
const std::vector<ModelNode>& dstNodes = target.GetNodes();
|
||||
const std::vector<ModelNode>& srcNodes = src.nodes;
|
||||
|
||||
// ① 同名直映表
|
||||
std::unordered_map<std::string, u32> nameToIdx;
|
||||
nameToIdx.reserve(dstNodes.size());
|
||||
for (u32 i = 0; i < dstNodes.size(); ++i) {
|
||||
nameToIdx.emplace(dstNodes[i].name, i);
|
||||
}
|
||||
// ② Humanoid 槽位映射表(src nodeIdx → dst nodeIdx)
|
||||
std::unordered_map<u32, u32> retargetMap;
|
||||
std::unordered_map<u32, HumanoidSlot> retargetSlot;
|
||||
{
|
||||
const auto pairs = AutoMapHumanoid(srcNodes, dstNodes);
|
||||
for (const auto& [si, di] : pairs) {
|
||||
retargetMap.emplace(si, di);
|
||||
// 槽位回查(明细行显示用):源骨骼名识别即可
|
||||
Side side;
|
||||
bool ok = false;
|
||||
retargetSlot.emplace(si, MatchHumanoidSlot(
|
||||
srcNodes[si].name, side, ok));
|
||||
}
|
||||
}
|
||||
// 重定向骨架的 rest(逐 nodeIdx 缓存,避免每 key 重分解)
|
||||
std::unordered_map<u32, TRS> dstRestCache;
|
||||
dstRestCache.reserve(retargetMap.size());
|
||||
for (const auto& [si, di] : retargetMap) {
|
||||
dstRestCache.emplace(si, Decompose(dstNodes[di].localTransform));
|
||||
}
|
||||
|
||||
const Plan pl = BuildPlan(srcNodes, dstNodes);
|
||||
const std::string stem =
|
||||
std::filesystem::path(sourcePath).stem().string();
|
||||
std::vector<AnimationClip> extra;
|
||||
extra.reserve(src.clips.size());
|
||||
|
||||
for (auto& clip : src.clips) {
|
||||
AnimationClip out;
|
||||
out.name = clip.name;
|
||||
out.name = UniqueName(clip.name, stem, existingClips, outExtra);
|
||||
out.duration = clip.duration;
|
||||
out.tps = clip.tps;
|
||||
out.sourcePath = sourcePath;
|
||||
// 同名冲突(内嵌 clip 或其它源):源文件名循环加前缀直到
|
||||
// 唯一。单轮前缀在"前缀名也撞"(如两次 attach 同名资产)时
|
||||
// 仍会产出重复名——Controller/脚本按 clip 名引用,重名即歧义
|
||||
|
||||
// 每 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;
|
||||
{
|
||||
const auto collides = [&](const std::string& n) {
|
||||
for (const auto& c : target.GetClips()) {
|
||||
if (c.name == n) return true;
|
||||
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);
|
||||
}
|
||||
for (const auto& c : extra) {
|
||||
if (c.name == n) return true;
|
||||
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;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
while (collides(out.name)) {
|
||||
out.name = stem + "_" + out.name;
|
||||
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 (ch.positions.empty() && ch.rotations.empty() &&
|
||||
ch.scales.empty()) {
|
||||
continue; // 空轨道无用
|
||||
}
|
||||
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;
|
||||
|
||||
AnimChannel remapped;
|
||||
if (auto it = nameToIdx.find(boneName);
|
||||
it != nameToIdx.end()) {
|
||||
// ① 同名直搬
|
||||
remapped.nodeIdx = it->second;
|
||||
remapped.positions = std::move(ch.positions);
|
||||
remapped.rotations = std::move(ch.rotations);
|
||||
remapped.scales = std::move(ch.scales);
|
||||
out.channels.push_back(std::move(remapped));
|
||||
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;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ② 槽位重定向:增量烘焙到目标骨架局部空间。
|
||||
// 平移增量按骨长比缩放(|dstRest| / |srcRest|)——
|
||||
// 两资产单位/比例不同源(FBX 常为 cm、glTF 为 m)时
|
||||
// 不缩放会把模型拽飞;rest≈0 的根骨骼比例取 1
|
||||
const auto mapIt = retargetMap.find(ch.nodeIdx);
|
||||
if (mapIt == retargetMap.end()) {
|
||||
++res.channelsSkipped; // 两级都落不上(手指/道具等)
|
||||
continue;
|
||||
}
|
||||
const TRS srcRest = Decompose(
|
||||
srcNodes[ch.nodeIdx].localTransform);
|
||||
const TRS& dstRest = dstRestCache[ch.nodeIdx];
|
||||
remapped.nodeIdx = mapIt->second;
|
||||
const f32 srcLen = glm::length(srcRest.pos);
|
||||
const f32 ratio =
|
||||
(srcLen > 1e-6f) ? (glm::length(dstRest.pos) / srcLen)
|
||||
: 1.0f;
|
||||
|
||||
remapped.positions.reserve(ch.positions.size());
|
||||
} else {
|
||||
// 平移:局部增量 × 尺度
|
||||
const TRS srcRest =
|
||||
Decompose(srcNodes[ch.nodeIdx].localTransform);
|
||||
outCh.positions.reserve(ch.positions.size());
|
||||
for (const auto& k : ch.positions) {
|
||||
remapped.positions.push_back(
|
||||
{k.time, dstRest.pos +
|
||||
(k.value - srcRest.pos) * ratio});
|
||||
outCh.positions.push_back(
|
||||
{k.time, s.dstRestPos +
|
||||
(s.M * (k.value - srcRest.pos)) *
|
||||
s.posScale});
|
||||
}
|
||||
remapped.rotations.reserve(ch.rotations.size());
|
||||
for (const auto& k : ch.rotations) {
|
||||
const glm::quat delta =
|
||||
k.value * glm::inverse(srcRest.rot);
|
||||
remapped.rotations.push_back(
|
||||
{k.time, delta * dstRest.rot});
|
||||
// 旋转:烘焙结果(多源汇一 dst 时共享同份)
|
||||
if (auto bk = baked.find(s.dst); bk != baked.end()) {
|
||||
outCh.rotations = bk->second;
|
||||
}
|
||||
remapped.scales = std::move(ch.scales);
|
||||
out.channels.push_back(std::move(remapped));
|
||||
// 缩放不搬运:跨骨架源的缩放轨道常承载单位换算
|
||||
// (kwy 根节点的 cm 补偿等),原样拷到目标会整体放大
|
||||
// 骨骼;目标保持自身 rest 缩放(人体动画一般无缩放)
|
||||
out.channels.push_back(std::move(outCh));
|
||||
++res.channelsRetargeted;
|
||||
|
||||
// 明细行(同骨骼多 clip 去重)
|
||||
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(
|
||||
{HumanoidSlotName(retargetSlot[ch.nodeIdx]),
|
||||
boneName, dstNodes[mapIt->second].name});
|
||||
{s.kind == Kind::Root
|
||||
? "RootMotion"
|
||||
: HumanoidSlotName(slot),
|
||||
boneName, dstNodes[s.dst].name});
|
||||
}
|
||||
}
|
||||
// 全部轨道落不上 = 无可驱动骨骼,clip 不入表
|
||||
}
|
||||
|
||||
if (out.channels.empty()) {
|
||||
ENGINE_WARN("AttachClipsToModel: clip '{}' has no matching "
|
||||
"bones in target (skipped)",
|
||||
clip.name);
|
||||
continue;
|
||||
}
|
||||
extra.push_back(std::move(out));
|
||||
outExtra.push_back(std::move(out));
|
||||
++res.clipsAdded;
|
||||
}
|
||||
if (!extra.empty()) {
|
||||
target.AppendClips(std::move(extra));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,25 +44,62 @@ namespace Engine::Animation {
|
||||
}
|
||||
}
|
||||
if (r <= 1e-4f || ch.empty()) return;
|
||||
// 覆盖 v 角度的相邻扇区(角度归一 [0,2π),wrap 补全;方向子
|
||||
// ≤16 个,栈数组零堆分配)
|
||||
// 覆盖 v 角度的相邻扇区(角度归一 [0,2π);方向子 ≤16 个,
|
||||
// 栈数组零堆分配)
|
||||
constexpr f32 kTwoPi = 6.2831853f;
|
||||
const f32 theta = glm::mod(glm::atan(z, x), kTwoPi);
|
||||
u32 idx[16];
|
||||
f32 ang[16];
|
||||
constexpr u32 kMaxDir = 16;
|
||||
u32 idx[kMaxDir];
|
||||
f32 ang[kMaxDir];
|
||||
u32 nd = 0;
|
||||
for (u32 i = 0; i < ch.size() && nd < 16; ++i) {
|
||||
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];
|
||||
const f32 aj = (j == 0) ? ang[0] + kTwoPi : ang[j];
|
||||
if (theta >= ai && theta <= aj) {
|
||||
const f32 span = aj - ai;
|
||||
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;
|
||||
|
||||
@@ -128,7 +128,8 @@ namespace Engine::Animation {
|
||||
void AnimationEvaluator::Evaluate(const Model& model,
|
||||
const AnimationClip& cur, f32 curTime,
|
||||
const CrossfadeState& xf,
|
||||
std::vector<glm::mat4>& outGlobals)
|
||||
std::vector<glm::mat4>& outGlobals,
|
||||
bool inPlace)
|
||||
{
|
||||
// crossfade 双层 = N 层混合的特例(Direct 模式与 Controller
|
||||
// 过渡共用同一套加权数学)。两权重和恒为 1,渐进归一化后精确
|
||||
@@ -141,13 +142,14 @@ namespace Engine::Animation {
|
||||
layers[1] = {xf.src, xf.srcTime, 1.0f - xf.wCur};
|
||||
count = 2;
|
||||
}
|
||||
EvaluateLayers(model, layers, count, outGlobals);
|
||||
EvaluateLayers(model, layers, count, outGlobals, inPlace);
|
||||
}
|
||||
|
||||
void AnimationEvaluator::EvaluateLayers(const Model& model,
|
||||
const PoseLayer* layers,
|
||||
u32 count,
|
||||
std::vector<glm::mat4>& outGlobals)
|
||||
std::vector<glm::mat4>& outGlobals,
|
||||
bool inPlace)
|
||||
{
|
||||
// ---- 逐层采样(容量跨帧复用;nullptr/零权重层跳过)----
|
||||
if (m_Layers.size() < count) {
|
||||
@@ -208,6 +210,24 @@ namespace Engine::Animation {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,8 @@ namespace Engine::Animation {
|
||||
}
|
||||
|
||||
// ---- 姿态求值(双层混合在求值器内部)----
|
||||
m_Evaluator.Evaluate(*model, cur, anim.time, xf, m_Globals);
|
||||
m_Evaluator.Evaluate(*model, cur, anim.time, xf, m_Globals,
|
||||
!anim.applyRootMotion);
|
||||
PushPalette(entity, model);
|
||||
});
|
||||
|
||||
@@ -493,7 +494,8 @@ namespace Engine::Animation {
|
||||
m_Evaluator.EvaluateLayers(*model, m_LayerScratch.data(),
|
||||
static_cast<u32>(
|
||||
m_LayerScratch.size()),
|
||||
m_Globals);
|
||||
m_Globals,
|
||||
!anim.applyRootMotion);
|
||||
PushPalette(entity, model);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,52 +1,359 @@
|
||||
//
|
||||
// HumanoidRig 实现:关键词表 + 归一化 + 自动映射。
|
||||
// HumanoidRig 实现:standard / dialects / mapper 三层 + 行走基估计。
|
||||
//
|
||||
// 分层职责(Unity Avatar 语义,见头文件架构说明):
|
||||
// standard — 槽位结构化访问(侧别/镜像/指骨算术/显示名)
|
||||
// dialects — 命名方言数据表(主干统一表 + 指骨三约定展开表)
|
||||
// mapper — 归一化 + 查表识别 + 跨骨架配对
|
||||
// 行走基/T-pose 估计 — rest 姿态几何推导,与命名无关
|
||||
//
|
||||
|
||||
#include <Engine/Animation/HumanoidRig.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
namespace {
|
||||
// ==================================================================
|
||||
// standard:槽位结构化访问
|
||||
// (概念分层:公共 API 在 Engine::Animation 作用域,与头文件声明
|
||||
// 一致;真正的 C++ 内层 namespace 只用于 dialects/mapper 内部表)
|
||||
// ==================================================================
|
||||
|
||||
// ---- 名字归一化:拆 (body, side) ----
|
||||
//
|
||||
// 规则:
|
||||
// 1) 去 "mixamorig:" 等前缀
|
||||
// 2) 小写;分隔符(空格/_/.-:)一律删除
|
||||
// 3) left/right(Mixamo "LeftArm")与 左/右(MMD "左腕")统一
|
||||
// 抽成 side,body 里剔除;.L/.R、_L/_R 后缀同上
|
||||
//
|
||||
// body 用**精确相等**匹配关键词(非子串)——"lowerarm"
|
||||
// 不会误命中 "arm"
|
||||
|
||||
Side ExtractSide(std::string& body)
|
||||
Side SideOfSlot(HumanoidSlot slot)
|
||||
{
|
||||
Side s = Side::None;
|
||||
auto cut = [&](std::string_view token, Side which)
|
||||
{
|
||||
size_t pos = 0;
|
||||
while ((pos = body.find(token, pos)) != std::string::npos) {
|
||||
body.erase(pos, token.size());
|
||||
if (s == Side::None) s = which;
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
if (s < 6) return Side::None; // 主干
|
||||
return s < 29 ? Side::Left : Side::Right;
|
||||
}
|
||||
|
||||
HumanoidSlot MirrorSlot(HumanoidSlot slot)
|
||||
{
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
// 布局:主干 [0..5] 无镜像;左区 [6..28] 与右区 [29..51]
|
||||
// 完全同构(4 臂 + 15 指 + 4 腿趾),偏移 23
|
||||
if (s < 6 || s >= static_cast<u32>(HumanoidSlot::Count)) {
|
||||
return slot;
|
||||
}
|
||||
return static_cast<HumanoidSlot>(s < 29 ? s + 23 : s - 23);
|
||||
}
|
||||
|
||||
HumanoidSlot FingerSlot(Side side, FingerKind kind, u32 seg)
|
||||
{
|
||||
// 布局:LeftThumb1=10 起,5 指 × 3 段连续;右侧同构 +23
|
||||
const u32 base =
|
||||
side == Side::Left ? 10 : 10 + 23;
|
||||
return static_cast<HumanoidSlot>(
|
||||
base + static_cast<u32>(kind) * 3 + (seg - 1));
|
||||
}
|
||||
|
||||
bool SlotFinger(HumanoidSlot slot, Side& side, FingerKind& kind,
|
||||
u32& seg)
|
||||
{
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
const bool inL = s >= 10 && s <= 24;
|
||||
const bool inR = s >= 33 && s <= 47;
|
||||
if (!inL && !inR) return false;
|
||||
side = inL ? Side::Left : Side::Right;
|
||||
const u32 rel = s - (inL ? 10u : 33u);
|
||||
kind = static_cast<FingerKind>(rel / 3);
|
||||
seg = rel % 3 + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* HumanoidSlotName(HumanoidSlot slot)
|
||||
{
|
||||
static constexpr std::array<const char*, 52> kNames{
|
||||
"Hips", "Spine", "Chest", "UpperChest", "Neck", "Head",
|
||||
"LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand",
|
||||
"LeftThumb1", "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",
|
||||
};
|
||||
cut("left", Side::Left);
|
||||
cut("right", Side::Right);
|
||||
cut("左", Side::Left); // MMD 前缀
|
||||
cut("右", Side::Right);
|
||||
cut(".l", Side::Left); // blender 惯例后缀
|
||||
cut(".r", Side::Right);
|
||||
cut("_l", Side::Left);
|
||||
cut("_r", Side::Right);
|
||||
return s;
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
return s < kNames.size() ? kNames[s] : "?";
|
||||
}
|
||||
|
||||
// ① 前缀剥离 + 小写(分隔符**保留**——后缀式 side 标记
|
||||
// ==================================================================
|
||||
// dialects:命名方言数据表(按"命名规定 / convention"分区)
|
||||
//
|
||||
// 识别 = 归一化 body 精确查表(Unity 的 token 规则表同思想)。
|
||||
// 覆盖的命名规定:
|
||||
// 通用英文 Unity / Blender / 多数 DCC 导出的英文名
|
||||
// Mixamo Adobe Mixamo(导入前已剥 "mixamorig:" 前缀)
|
||||
// MMD / PMX MikuMikuDance 標準ボーン(日文名 + 全角数字)
|
||||
// Rigify Blender Rigify 变形骨(DEF- 前缀)
|
||||
// UE Unreal Mannequin / IK Rig(段词 Proximal…)
|
||||
// 3ds Max 3ds Max Biped(bip01)
|
||||
// 主干(trunk)与指/趾(finger)各一张表:两表键空间不重叠,
|
||||
// 查表主干优先、指骨兜底。各分区键名互不重叠,查表取首个命中,
|
||||
// 分区顺序仅为归类、无功能含义。
|
||||
// ==================================================================
|
||||
namespace dialects {
|
||||
|
||||
// ---- 主干表(body = side 抽取 + 分隔符剥离后的左槽名)----
|
||||
struct TrunkEntry {
|
||||
std::string_view body;
|
||||
HumanoidSlot slot;
|
||||
};
|
||||
|
||||
constexpr std::array<TrunkEntry, 61> kTrunk{{
|
||||
// ==== 通用英文(Unity / Blender / DCC)====
|
||||
// 躯干:spine…/chest/spine1 与 upperchest/spine2 同义梯队
|
||||
{"hips", HumanoidSlot::Hips},
|
||||
{"pelvis", HumanoidSlot::Hips},
|
||||
{"spine", HumanoidSlot::Spine},
|
||||
{"chest", HumanoidSlot::Chest},
|
||||
{"spine1", HumanoidSlot::Chest},
|
||||
{"upperchest", HumanoidSlot::UpperChest},
|
||||
{"spine2", HumanoidSlot::UpperChest},
|
||||
{"neck", HumanoidSlot::Neck},
|
||||
{"head", HumanoidSlot::Head},
|
||||
// 臂:arm/upperarm/bicep 同义;lowerarm/forearm/elbow 同义
|
||||
{"shoulder", HumanoidSlot::LeftShoulder},
|
||||
{"clavicle", HumanoidSlot::LeftShoulder},
|
||||
{"arm", HumanoidSlot::LeftUpperArm},
|
||||
{"upperarm", HumanoidSlot::LeftUpperArm},
|
||||
{"bicep", HumanoidSlot::LeftUpperArm},
|
||||
{"lowerarm", HumanoidSlot::LeftLowerArm},
|
||||
{"forearm", HumanoidSlot::LeftLowerArm},
|
||||
{"elbow", HumanoidSlot::LeftLowerArm},
|
||||
{"hand", HumanoidSlot::LeftHand},
|
||||
// 腿:upperleg/upleg/thigh 同义;lowerleg/leg/knee/calf 同义
|
||||
{"upperleg", HumanoidSlot::LeftUpperLeg},
|
||||
{"upleg", HumanoidSlot::LeftUpperLeg},
|
||||
{"thigh", HumanoidSlot::LeftUpperLeg},
|
||||
{"lowerleg", HumanoidSlot::LeftLowerLeg},
|
||||
{"leg", HumanoidSlot::LeftLowerLeg}, // Mixamo LeftLeg = 小腿
|
||||
{"knee", HumanoidSlot::LeftLowerLeg},
|
||||
{"calf", HumanoidSlot::LeftLowerLeg},
|
||||
{"foot", HumanoidSlot::LeftFoot},
|
||||
{"ankle", HumanoidSlot::LeftFoot},
|
||||
{"toebase", HumanoidSlot::LeftToes},
|
||||
{"toes", HumanoidSlot::LeftToes},
|
||||
{"toe", HumanoidSlot::LeftToes},
|
||||
|
||||
// ==== MMD / PMX 標準ボーン ====
|
||||
// 下半身=骨盆、上半身链=spine→chest→upperchest、腕=上臂、
|
||||
// ひじ/肘=肘、手首=腕关、足=大腿、ひざ=膝、足首=踝、つま先=趾。
|
||||
// センター(根运动控制骨)与下半身同为 Hips 候选,实际取
|
||||
// DFS 首个命中者(下半身),センター 落为承载节点走根运动
|
||||
{"下半身", HumanoidSlot::Hips},
|
||||
{"センター", HumanoidSlot::Hips},
|
||||
{"上半身", HumanoidSlot::Spine},
|
||||
{"上半身1", HumanoidSlot::Chest},
|
||||
{"上半身2", HumanoidSlot::UpperChest},
|
||||
{"首", HumanoidSlot::Neck},
|
||||
{"頭", HumanoidSlot::Head},
|
||||
{"肩", HumanoidSlot::LeftShoulder},
|
||||
{"腕", HumanoidSlot::LeftUpperArm},
|
||||
{"ひじ", HumanoidSlot::LeftLowerArm},
|
||||
{"肘", HumanoidSlot::LeftLowerArm},
|
||||
{"手首", HumanoidSlot::LeftHand},
|
||||
{"足", HumanoidSlot::LeftUpperLeg},
|
||||
{"ひざ", HumanoidSlot::LeftLowerLeg},
|
||||
{"足首", HumanoidSlot::LeftFoot},
|
||||
{"つま先", HumanoidSlot::LeftToes},
|
||||
|
||||
// ==== Rigify(Blender)DEF- 变形骨 ====
|
||||
// 前缀保留参与精确匹配——裸 "spine" 在 Mixamo(脊柱段)与
|
||||
// Rigify(DEF-spine=骨盆变形骨)间语义冲突,独立键名避免
|
||||
// 误伤既有约定。段号实测自 Lumine(.003 挂肩=胸上段、.005
|
||||
// 颈、.006 头——眼/牙/发均挂其下)
|
||||
{"defspine", HumanoidSlot::Hips},
|
||||
{"defspine001", HumanoidSlot::Spine},
|
||||
{"defspine002", HumanoidSlot::Chest},
|
||||
{"defspine003", HumanoidSlot::UpperChest},
|
||||
{"defspine005", HumanoidSlot::Neck},
|
||||
{"defspine006", HumanoidSlot::Head},
|
||||
{"defshoulder", HumanoidSlot::LeftShoulder},
|
||||
{"defupperarm", HumanoidSlot::LeftUpperArm},
|
||||
{"defforearm", HumanoidSlot::LeftLowerArm},
|
||||
{"defhand", HumanoidSlot::LeftHand},
|
||||
{"defthigh", HumanoidSlot::LeftUpperLeg},
|
||||
{"defshin", HumanoidSlot::LeftLowerLeg},
|
||||
{"deffoot", HumanoidSlot::LeftFoot},
|
||||
{"deftoe", HumanoidSlot::LeftToes},
|
||||
|
||||
// ==== 3ds Max Biped ====
|
||||
{"bip01", HumanoidSlot::Hips},
|
||||
}};
|
||||
|
||||
// ---- 指骨展开表(按命名规定分区)----
|
||||
// 四种段号约定(实测来源):
|
||||
// Mixamo LeftHandThumb1..3 → hand + 指名 + 裸数字
|
||||
// Rigify DEF-thumb.01..03 → (def)? + 指名 + 零填充两位;
|
||||
// 四指带 f_ 前缀(findex...),
|
||||
// 拇指没有;master 裸名同构
|
||||
// UE/kwy Thumb Proximal_L 等 → 指名 + 段词
|
||||
// (Proximal/Intermediate/Distal = 近/中/末节,
|
||||
// 小指叫 little 非 pinky);趾为复数 Toes
|
||||
// MMD/PMX 親指0..2 / 人指1..3 / 中指1..3 / 薬指1..3 / 小指1..3
|
||||
// → 指名 + 数字(拇指 0 起、其余 1 起;全角数字已由
|
||||
// mapper::FoldFullWidthDigits 折半)
|
||||
// 表键为左侧 body(side 由归一化抽取),右槽经 MirrorSlot。
|
||||
// 各约定的「…先 / 段 4」指尖均不展开——标准拓扑无对应槽位,
|
||||
// 源轨道识别失败由 attach 侧 skip(与旧行为一致)。
|
||||
struct BodySlot {
|
||||
std::string body;
|
||||
HumanoidSlot slot;
|
||||
};
|
||||
|
||||
const std::vector<BodySlot>& FingerPatterns()
|
||||
{
|
||||
static const std::vector<BodySlot> kTable = [] {
|
||||
std::vector<BodySlot> t;
|
||||
t.reserve(80);
|
||||
|
||||
// Mixamo:handthumb1 ... handpinky3
|
||||
constexpr std::array<std::string_view, 5> kMixamo{
|
||||
"thumb", "index", "middle", "ring", "pinky"};
|
||||
for (const auto kindU : {0u, 1u, 2u, 3u, 4u}) {
|
||||
const auto slot = static_cast<u32>(
|
||||
FingerSlot(Side::Left,
|
||||
static_cast<FingerKind>(kindU),
|
||||
1));
|
||||
for (u32 seg = 1; seg <= 3; ++seg) {
|
||||
t.push_back({std::string("hand") +
|
||||
std::string(kMixamo[kindU]) +
|
||||
std::to_string(seg),
|
||||
static_cast<HumanoidSlot>(
|
||||
slot + seg - 1)});
|
||||
}
|
||||
}
|
||||
|
||||
// Rigify:defthumb01 ... deffpinky03(+ 无前缀 master 变体)
|
||||
constexpr std::array<std::string_view, 5> kRigify{
|
||||
"thumb", "findex", "fmiddle", "fring", "fpinky"};
|
||||
for (const auto kindU : {0u, 1u, 2u, 3u, 4u}) {
|
||||
const auto slot = static_cast<u32>(
|
||||
FingerSlot(Side::Left,
|
||||
static_cast<FingerKind>(kindU),
|
||||
1));
|
||||
for (u32 seg = 1; seg <= 3; ++seg) {
|
||||
const std::string z =
|
||||
std::to_string(100 + seg).substr(1); // 01..03
|
||||
const std::string core =
|
||||
std::string(kRigify[kindU]) + z;
|
||||
t.push_back({"def" + core,
|
||||
static_cast<HumanoidSlot>(
|
||||
slot + seg - 1)});
|
||||
t.push_back({core,
|
||||
static_cast<HumanoidSlot>(
|
||||
slot + seg - 1)});
|
||||
}
|
||||
}
|
||||
|
||||
// UE:thumbproximal ... littledistal
|
||||
constexpr std::array<std::string_view, 5> kUe{
|
||||
"thumb", "index", "middle", "ring", "little"};
|
||||
constexpr std::array<std::string_view, 3> kUeSeg{
|
||||
"proximal", "intermediate", "distal"};
|
||||
for (const auto kindU : {0u, 1u, 2u, 3u, 4u}) {
|
||||
const auto slot = static_cast<u32>(
|
||||
FingerSlot(Side::Left,
|
||||
static_cast<FingerKind>(kindU),
|
||||
1));
|
||||
for (u32 seg = 1; seg <= 3; ++seg) {
|
||||
t.push_back({std::string(kUe[kindU]) +
|
||||
std::string(kUeSeg[seg - 1]),
|
||||
static_cast<HumanoidSlot>(
|
||||
slot + seg - 1)});
|
||||
}
|
||||
}
|
||||
// MMD/PMX:親指0..2 / 人指1..3 / 中指1..3 / 薬指1..3 /
|
||||
// 小指1..3(拇指 0 起、其余 1 起;数字已折半角)
|
||||
{
|
||||
constexpr std::array<std::string_view, 5> kMmd{
|
||||
"親指", "人指", "中指", "薬指", "小指"};
|
||||
for (u32 kindU = 0; kindU < 5; ++kindU) {
|
||||
const u32 numBase = kindU == 0 ? 0u : 1u;
|
||||
for (u32 j = 0; j < 3; ++j) {
|
||||
const auto slot = static_cast<u32>(
|
||||
FingerSlot(Side::Left,
|
||||
static_cast<FingerKind>(kindU),
|
||||
j + 1));
|
||||
t.push_back({std::string(kMmd[kindU]) +
|
||||
std::to_string(numBase + j),
|
||||
static_cast<HumanoidSlot>(slot)});
|
||||
}
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}();
|
||||
return kTable;
|
||||
}
|
||||
|
||||
// 查表:主干优先,指骨兜底(两表键空间无交集)
|
||||
bool Lookup(const std::string& body, HumanoidSlot& out)
|
||||
{
|
||||
for (const auto& e : kTrunk) {
|
||||
if (body == e.body) {
|
||||
out = e.slot;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const auto& e : FingerPatterns()) {
|
||||
if (body == e.body) {
|
||||
out = e.slot;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dialects
|
||||
|
||||
// ==================================================================
|
||||
// mapper:归一化 + 识别 + 配对
|
||||
// ==================================================================
|
||||
namespace mapper {
|
||||
|
||||
// ⓪ 全角数字折半角:U+FF10..U+FF19 → '0'..'9'(UTF-8 EF BC 90..99)。
|
||||
// MMD 規範骨常写全角数字(親指0/中指1…),折半后各命名规定可在
|
||||
// 表里统一用半角书写。仅折数字,全角字母(如「つま先IK」)不动
|
||||
// ——避免 IK 骨误命中「つま先」
|
||||
void FoldFullWidthDigits(std::string& s)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (size_t i = 0; i < s.size();) {
|
||||
if (i + 3 <= s.size() &&
|
||||
static_cast<unsigned char>(s[i]) == 0xEF &&
|
||||
static_cast<unsigned char>(s[i + 1]) == 0xBC) {
|
||||
const auto b2 = static_cast<unsigned char>(s[i + 2]);
|
||||
if (b2 >= 0x90 && b2 <= 0x99) {
|
||||
out.push_back(
|
||||
static_cast<char>('0' + (b2 - 0x90)));
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push_back(s[i++]);
|
||||
}
|
||||
s.swap(out);
|
||||
}
|
||||
|
||||
// ① 前缀剥离 + 小写(分隔符**保留**——末尾式 side 标记
|
||||
// ".l"/"_l" 依赖分隔符存在,必须先于分隔符删除识别)
|
||||
std::string NormalizeLower(std::string_view name)
|
||||
{
|
||||
@@ -63,10 +370,11 @@ namespace Engine::Animation {
|
||||
c = static_cast<char>(std::tolower(
|
||||
static_cast<unsigned char>(c)));
|
||||
}
|
||||
FoldFullWidthDigits(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ② 分隔符删除(在 ExtractSide 之后调用;CJK 多字节原样过)
|
||||
// ② 分隔符删除(side 抽取之后调用;CJK 多字节原样过)
|
||||
std::string StripSeparators(const std::string& s)
|
||||
{
|
||||
std::string out;
|
||||
@@ -80,105 +388,99 @@ namespace Engine::Animation {
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- body → 左侧槽位 关键词表(右侧 = 左槽 + 段偏移)----
|
||||
// 精确相等匹配;顺序即优先级(先命中先用)
|
||||
struct BodyRule {
|
||||
std::string_view body;
|
||||
HumanoidSlot left;
|
||||
};
|
||||
|
||||
constexpr std::array<BodyRule, 43> kBodyRules{{
|
||||
// 主干(MMD 语义:下半身=hips、上半身链=spine→chest)
|
||||
{"hips", HumanoidSlot::Hips},
|
||||
{"pelvis", HumanoidSlot::Hips},
|
||||
{"bip01", HumanoidSlot::Hips},
|
||||
{"下半身", HumanoidSlot::Hips},
|
||||
{"センター", HumanoidSlot::Hips},
|
||||
{"spine", HumanoidSlot::Spine},
|
||||
{"上半身", HumanoidSlot::Spine},
|
||||
{"chest", HumanoidSlot::Chest},
|
||||
{"spine1", HumanoidSlot::Chest},
|
||||
{"上半身1", HumanoidSlot::Chest},
|
||||
{"upperchest", HumanoidSlot::UpperChest},
|
||||
{"spine2", HumanoidSlot::UpperChest},
|
||||
{"上半身2", HumanoidSlot::UpperChest},
|
||||
{"neck", HumanoidSlot::Neck},
|
||||
{"首", HumanoidSlot::Neck},
|
||||
{"head", HumanoidSlot::Head},
|
||||
{"頭", HumanoidSlot::Head},
|
||||
// 臂段(MMD:腕=upper arm、ひじ/肘=elbow、手首=wrist)
|
||||
{"shoulder", HumanoidSlot::LeftShoulder},
|
||||
{"clavicle", HumanoidSlot::LeftShoulder},
|
||||
{"肩", HumanoidSlot::LeftShoulder},
|
||||
{"arm", HumanoidSlot::LeftUpperArm},
|
||||
{"upperarm", HumanoidSlot::LeftUpperArm},
|
||||
{"bicep", HumanoidSlot::LeftUpperArm},
|
||||
{"腕", HumanoidSlot::LeftUpperArm},
|
||||
{"lowerarm", HumanoidSlot::LeftLowerArm},
|
||||
{"forearm", HumanoidSlot::LeftLowerArm},
|
||||
{"elbow", HumanoidSlot::LeftLowerArm},
|
||||
{"ひじ", HumanoidSlot::LeftLowerArm},
|
||||
{"肘", HumanoidSlot::LeftLowerArm},
|
||||
{"hand", HumanoidSlot::LeftHand},
|
||||
{"手首", HumanoidSlot::LeftHand},
|
||||
// 腿段(MMD:足=thigh、ひざ=knee、足首=ankle)
|
||||
{"upperleg", HumanoidSlot::LeftUpperLeg},
|
||||
{"upleg", HumanoidSlot::LeftUpperLeg},
|
||||
{"thigh", HumanoidSlot::LeftUpperLeg},
|
||||
{"足", HumanoidSlot::LeftUpperLeg},
|
||||
{"lowerleg", HumanoidSlot::LeftLowerLeg},
|
||||
{"leg", HumanoidSlot::LeftLowerLeg}, // Mixamo:LeftLeg=小腿(大腿是 UpLeg)
|
||||
{"knee", HumanoidSlot::LeftLowerLeg},
|
||||
{"calf", HumanoidSlot::LeftLowerLeg},
|
||||
{"ひざ", HumanoidSlot::LeftLowerLeg},
|
||||
{"foot", HumanoidSlot::LeftFoot},
|
||||
{"ankle", HumanoidSlot::LeftFoot},
|
||||
{"足首", HumanoidSlot::LeftFoot},
|
||||
}};
|
||||
|
||||
// 左→右槽位偏移:臂/肩段(Shoulder..Hand)+4,腿段(UpperLeg..Foot)+3
|
||||
HumanoidSlot ToSide(HumanoidSlot leftSlot, Side side)
|
||||
// 窄 side 抽取:side 只出现在 Mixamo 前缀(LeftHand...)、
|
||||
// MMD 前缀(左手首)与 Blender 末尾后缀(..._L/.L)。
|
||||
// 不用任意位置 cut——Rigify "f_ring" 中间的 "_r" 会被误当
|
||||
// 右侧后缀删掉(body 变 "fing" → Ring 指骨全军覆没的历史
|
||||
// 陷阱),末尾匹配天然规避
|
||||
Side ExtractSideNarrow(std::string& body)
|
||||
{
|
||||
if (side != Side::Right) return leftSlot;
|
||||
const u32 s = static_cast<u32>(leftSlot);
|
||||
const u32 legStart = static_cast<u32>(HumanoidSlot::LeftUpperLeg);
|
||||
return static_cast<HumanoidSlot>(s + (s >= legStart ? 3 : 4));
|
||||
Side s = Side::None;
|
||||
if (body.rfind("left", 0) == 0) {
|
||||
body.erase(0, 4);
|
||||
s = Side::Left;
|
||||
} else if (body.rfind("right", 0) == 0) {
|
||||
body.erase(0, 5);
|
||||
s = Side::Right;
|
||||
} else if (body.rfind("左", 0) == 0) {
|
||||
body.erase(0, strlen("左")); // UTF-8 3 字节
|
||||
s = Side::Left;
|
||||
} else if (body.rfind("右", 0) == 0) {
|
||||
body.erase(0, strlen("右"));
|
||||
s = Side::Right;
|
||||
}
|
||||
const auto cutSuffix = [&](std::string_view token,
|
||||
Side which) {
|
||||
if (body.size() >= token.size() &&
|
||||
body.compare(body.size() - token.size(),
|
||||
token.size(), token) == 0) {
|
||||
body.erase(body.size() - token.size(), token.size());
|
||||
if (s == Side::None) s = which;
|
||||
}
|
||||
};
|
||||
cutSuffix(".l", Side::Left);
|
||||
cutSuffix(".r", Side::Right);
|
||||
cutSuffix("_l", Side::Left);
|
||||
cutSuffix("_r", Side::Right);
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* HumanoidSlotName(HumanoidSlot slot)
|
||||
// 宽 side 抽取(fallback):任意位置删 token,兜底命名习惯
|
||||
// 不在首尾的资产。窄规则先行,本函数只在查表失败后启用
|
||||
Side ExtractSideWide(std::string& body)
|
||||
{
|
||||
static constexpr std::array<const char*, 20> kNames{
|
||||
"Hips", "Spine", "Chest", "UpperChest", "Neck", "Head",
|
||||
"LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand",
|
||||
"RightShoulder", "RightUpperArm", "RightLowerArm", "RightHand",
|
||||
"LeftUpperLeg", "LeftLowerLeg", "LeftFoot",
|
||||
"RightUpperLeg", "RightLowerLeg", "RightFoot",
|
||||
Side s = Side::None;
|
||||
auto cut = [&](std::string_view token, Side which)
|
||||
{
|
||||
size_t pos = 0;
|
||||
while ((pos = body.find(token, pos)) != std::string::npos) {
|
||||
body.erase(pos, token.size());
|
||||
if (s == Side::None) s = which;
|
||||
}
|
||||
};
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
return s < kNames.size() ? kNames[s] : "?";
|
||||
cut("left", Side::Left);
|
||||
cut("right", Side::Right);
|
||||
cut("左", Side::Left);
|
||||
cut("右", Side::Right);
|
||||
cut(".l", Side::Left);
|
||||
cut(".r", Side::Right);
|
||||
cut("_l", Side::Left);
|
||||
cut("_r", Side::Right);
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace mapper
|
||||
|
||||
// 名字归一化 + 方言查表识别。两轮:窄 side 抽取优先(避开
|
||||
// f_ring 类中间子串陷阱),查表失败再用宽抽取兜底
|
||||
HumanoidSlot MatchHumanoidSlot(std::string_view name, Side& side,
|
||||
bool& ok)
|
||||
{
|
||||
side = Side::None;
|
||||
ok = false;
|
||||
std::string body = NormalizeLower(name);
|
||||
side = ExtractSide(body);
|
||||
body = StripSeparators(body);
|
||||
if (body.empty()) return HumanoidSlot::Hips;
|
||||
|
||||
for (const BodyRule& r : kBodyRules) {
|
||||
if (body == r.body) {
|
||||
const std::string lower = mapper::NormalizeLower(name);
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
std::string body = lower;
|
||||
const Side s = pass == 0
|
||||
? mapper::ExtractSideNarrow(body)
|
||||
: mapper::ExtractSideWide(body);
|
||||
body = mapper::StripSeparators(body);
|
||||
if (body.empty()) {
|
||||
return HumanoidSlot::Hips;
|
||||
}
|
||||
HumanoidSlot slot;
|
||||
if (dialects::Lookup(body, slot)) {
|
||||
side = s;
|
||||
ok = true;
|
||||
return ToSide(r.left, side);
|
||||
// 表存左槽名:右 side 经镜像换槽(主干槽镜像原样)
|
||||
return s == Side::Right ? MirrorSlot(slot) : slot;
|
||||
}
|
||||
}
|
||||
return HumanoidSlot::Hips;
|
||||
}
|
||||
|
||||
// 跨骨架自动映射:同名直映 + 52 槽一对一配对(指/趾骨同为主干
|
||||
// 槽位走统一通道,无独立特判)
|
||||
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
|
||||
const std::vector<ModelNode>& srcNodes,
|
||||
const std::vector<ModelNode>& dstNodes)
|
||||
@@ -202,7 +504,7 @@ namespace Engine::Animation {
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 槽位匹配:两侧各自识别 (slot → nodeIdx),槽位一对一配对
|
||||
// 2) 槽位配对:两侧各自识别 (slot → nodeIdx),槽位一对一
|
||||
constexpr u32 kSlotCount = static_cast<u32>(HumanoidSlot::Count);
|
||||
auto slotOf = [](const std::vector<ModelNode>& nodes,
|
||||
std::array<i32, kSlotCount>& out)
|
||||
@@ -218,7 +520,7 @@ namespace Engine::Animation {
|
||||
}
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
if (out[s] < 0) {
|
||||
out[s] = static_cast<i32>(i); // 关键词先命中优先
|
||||
out[s] = static_cast<i32>(i); // 方言表先命中优先
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -243,4 +545,204 @@ namespace Engine::Animation {
|
||||
return pairs;
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// 行走基 / T-pose 估计(rest 姿态几何推导,与命名方言无关)
|
||||
// ==================================================================
|
||||
namespace {
|
||||
|
||||
// 槽位 → 节点下标(第一个命中;无则 -1)
|
||||
i32 FindSlotNode(const std::vector<ModelNode>& nodes,
|
||||
HumanoidSlot slot)
|
||||
{
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
Side side;
|
||||
bool ok = false;
|
||||
if (MatchHumanoidSlot(nodes[i].name, side, ok) == slot &&
|
||||
ok) {
|
||||
return static_cast<i32>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 脚趾节点(按 Toes 槽位 + 侧别匹配;无则 -1)。走槽位而非
|
||||
// 名字含 "toe"——否则 MMD「つま先」等 CJK 趾骨检测不到
|
||||
i32 FindToeNode(const std::vector<ModelNode>& nodes, Side want)
|
||||
{
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
Side side;
|
||||
bool ok = false;
|
||||
const HumanoidSlot slot =
|
||||
MatchHumanoidSlot(nodes[i].name, side, ok);
|
||||
if (!ok || SideOfSlot(slot) != want) {
|
||||
continue;
|
||||
}
|
||||
if (slot == HumanoidSlot::LeftToes ||
|
||||
slot == HumanoidSlot::RightToes) {
|
||||
return static_cast<i32>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// rest 姿态的行走基(right/up/fwd,右手系)。up = 头 - 双脚
|
||||
// 中点;right = 臂差(armR - armL)去 up 分量;fwd = up × right。
|
||||
// 脚趾朝向消歧:fwd 与脚尖反向时 right/fwd 绕 up 同翻 180°
|
||||
// (镜像/腿型特异防呆,同翻保持右手系)。槽位不足 → false
|
||||
bool HumanoidBasis(const std::vector<ModelNode>& nodes,
|
||||
const std::vector<glm::mat4>& globals,
|
||||
glm::vec3& right, glm::vec3& up, glm::vec3& fwd)
|
||||
{
|
||||
const i32 head = FindSlotNode(nodes, HumanoidSlot::Head);
|
||||
const i32 footL = FindSlotNode(nodes, HumanoidSlot::LeftFoot);
|
||||
const i32 footR = FindSlotNode(nodes, HumanoidSlot::RightFoot);
|
||||
const i32 armL = FindSlotNode(nodes, HumanoidSlot::LeftUpperArm);
|
||||
const i32 armR = FindSlotNode(nodes, HumanoidSlot::RightUpperArm);
|
||||
if (head < 0 || footL < 0 || footR < 0 || armL < 0 || armR < 0) {
|
||||
return false;
|
||||
}
|
||||
const auto pos = [&](i32 i) {
|
||||
return glm::vec3(globals[static_cast<size_t>(i)][3]);
|
||||
};
|
||||
const glm::vec3 midFoot = 0.5f * (pos(footL) + pos(footR));
|
||||
const glm::vec3 lat = pos(armR) - pos(armL);
|
||||
up = pos(head) - midFoot;
|
||||
if (glm::length(up) < 1e-6f || glm::length(lat) < 1e-6f) {
|
||||
return false;
|
||||
}
|
||||
up = glm::normalize(up);
|
||||
right = lat - up * glm::dot(lat, up); // 去除 up 分量
|
||||
if (glm::length(right) < 1e-6f) {
|
||||
return false;
|
||||
}
|
||||
right = glm::normalize(right);
|
||||
// fwd = up × right(右手系:面朝 fwd 时 right 恒在体右侧)。
|
||||
// 原 right × up 恒指向背后,正常资产(X Bot/Mixamo:脚尖
|
||||
// +Z、右臂 −X)必触发下方脚趾消歧的 180° 翻转——翻转虽把
|
||||
// fwd 纠正,却把 right 连带翻成解剖学左,行走基被镜像污染;
|
||||
// 与未翻转的目标骨架(Lumine 等)对齐后,臂部增量被解释成
|
||||
// 朝身体内侧——两臂交缠的根因。腿/躯干增量前后对称,镜像
|
||||
// 下不显形,故此前未暴露
|
||||
fwd = glm::cross(up, right);
|
||||
const i32 toeL = FindToeNode(nodes, Side::Left);
|
||||
const i32 toeR = FindToeNode(nodes, Side::Right);
|
||||
if (toeL >= 0 && toeR >= 0) {
|
||||
const glm::vec3 toeDir =
|
||||
0.5f * (pos(toeL) + pos(toeR)) - midFoot;
|
||||
if (glm::dot(fwd, toeDir) < 0.0f) {
|
||||
right = -right;
|
||||
fwd = -fwd;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
glm::quat EstimateHumanoidRootAlignment(
|
||||
const std::vector<ModelNode>& srcNodes,
|
||||
const std::vector<ModelNode>& dstNodes)
|
||||
{
|
||||
const glm::quat kIdentity(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
// rest 世界变换(node 表父先子后,直接链乘)
|
||||
auto 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;
|
||||
};
|
||||
|
||||
const auto srcG = restGlobals(srcNodes);
|
||||
const auto dstG = restGlobals(dstNodes);
|
||||
|
||||
glm::vec3 sr, su, sf, dr, du, df;
|
||||
if (!HumanoidBasis(srcNodes, srcG, sr, su, sf) ||
|
||||
!HumanoidBasis(dstNodes, dstG, dr, du, df)) {
|
||||
return kIdentity;
|
||||
}
|
||||
// 列 = [right | up | fwd];A = Bd × Bsᵀ(src 世界 → dst 世界)
|
||||
const glm::mat3 Bs(sr, su, sf);
|
||||
const glm::mat3 Bd(dr, du, df);
|
||||
return glm::quat_cast(Bd * glm::transpose(Bs));
|
||||
}
|
||||
|
||||
std::vector<glm::quat> EstimateHumanoidTPose(
|
||||
const std::vector<ModelNode>& nodes,
|
||||
const std::vector<glm::mat4>& restGlobals)
|
||||
{
|
||||
// T-pose 参考姿势(每节点世界旋转):
|
||||
// 人形动画资产(Mixamo 等)的摆动量以 T-pose 为零位,目标骨架
|
||||
// rest 是 A-pose 时直接搬运"旋转变化"会过摆(臂从已下垂 45°
|
||||
// 再摆 T-pose 基准的全幅)——手越过中线交叉。此处为目标估计
|
||||
// T-pose 参考:臂链(upper_arm 子树)绕肩刚体摆到水平侧向,
|
||||
// 指骨按 seg1 方向伸直(kwy 等 rest 半握骨架与 Mixamo 等
|
||||
// rest 伸直骨架的手指零位不同源,见下)。其余骨取 rest。
|
||||
// rest 已是 T-pose 的资产(臂≈水平、指伸直)摆动量≈0,
|
||||
// 自动退化。
|
||||
auto rotOf = [](const glm::mat4& m) {
|
||||
return glm::quat_cast(glm::mat3(
|
||||
glm::normalize(glm::vec3(m[0])),
|
||||
glm::normalize(glm::vec3(m[1])),
|
||||
glm::normalize(glm::vec3(m[2]))));
|
||||
};
|
||||
std::vector<glm::quat> g(nodes.size(), glm::quat(1.0f, 0.0f, 0.0f,
|
||||
0.0f));
|
||||
for (size_t i = 0; i < nodes.size(); ++i) {
|
||||
g[i] = rotOf(restGlobals[i]);
|
||||
}
|
||||
glm::vec3 right, up, fwd;
|
||||
if (!HumanoidBasis(nodes, restGlobals, right, up, fwd)) {
|
||||
return g; // 行走基不可用 → 全 rest(不补偿)
|
||||
}
|
||||
const auto posOf = [&](i32 i) {
|
||||
return glm::vec3(restGlobals[static_cast<size_t>(i)][3]);
|
||||
};
|
||||
for (const Side side : {Side::Left, Side::Right}) {
|
||||
const HumanoidSlot uaSlot = side == Side::Left
|
||||
? HumanoidSlot::LeftUpperArm : HumanoidSlot::RightUpperArm;
|
||||
const HumanoidSlot faSlot = side == Side::Left
|
||||
? HumanoidSlot::LeftLowerArm : HumanoidSlot::RightLowerArm;
|
||||
const i32 ua = FindSlotNode(nodes, uaSlot);
|
||||
const i32 fa = FindSlotNode(nodes, faSlot);
|
||||
if (ua < 0 || fa < 0) {
|
||||
continue;
|
||||
}
|
||||
const glm::vec3 v =
|
||||
glm::normalize(posOf(fa) - posOf(ua)); // 上臂 rest 指向
|
||||
glm::vec3 vlat = v - up * glm::dot(v, up) -
|
||||
fwd * glm::dot(v, fwd);
|
||||
if (glm::length(vlat) < 1e-6f) {
|
||||
continue; // 臂纯沿 up(退化)→ 不摆
|
||||
}
|
||||
vlat = glm::normalize(vlat);
|
||||
const float d = glm::clamp(glm::dot(v, vlat), -1.0f, 1.0f);
|
||||
glm::vec3 axis = glm::cross(v, vlat);
|
||||
const glm::quat swing =
|
||||
glm::length(axis) < 1e-6f
|
||||
? glm::quat(1.0f, 0.0f, 0.0f, 0.0f)
|
||||
: glm::angleAxis(glm::acos(d),
|
||||
glm::normalize(axis));
|
||||
// upper_arm 子树刚体摆动(保持链内相对姿态;含自身)
|
||||
for (size_t j = 0; j < nodes.size(); ++j) {
|
||||
for (i32 p = static_cast<i32>(j); p >= 0;
|
||||
p = nodes[static_cast<size_t>(p)].parent) {
|
||||
if (p == ua) {
|
||||
g[j] = swing * g[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 手指不参与 T-pose 零位:指根扇形编码在 rest 平移里(旋转
|
||||
// 搬运无法复刻扇形),绝对复刻语义会让 dst 指根方向偏转 +
|
||||
// 跨手型穿插。手指走 rest 零位局部增量通道(Unity muscle
|
||||
// 语义,见 AnimationAttach),各资产保留自然手型,此处取 rest。
|
||||
return g;
|
||||
}
|
||||
|
||||
} // namespace Engine::Animation
|
||||
|
||||
@@ -202,6 +202,16 @@ namespace Engine::Assets {
|
||||
});
|
||||
}
|
||||
|
||||
ModelRef AssetManager::CreateCube(f32 halfSize, u8 r, u8 g, u8 b)
|
||||
{
|
||||
// 程序化资产复用同一缓存:键 = 伪路径 + 参数(与 plane 同约定)
|
||||
std::string key = "procedural://cube/" + std::to_string(halfSize) +
|
||||
"/" + std::to_string(r) + "," + std::to_string(g) +
|
||||
"," + std::to_string(b);
|
||||
return m_Models.Load(key,
|
||||
[&](const std::string&) { return m_Uploader.CreateCube(halfSize, r, g, b); });
|
||||
}
|
||||
|
||||
EnvRef AssetManager::LoadEnvironment(const std::string& path)
|
||||
{
|
||||
// GPU 生成(compute 预积分)经接口发生在渲染层实现里
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <Engine/Core/Window.h>
|
||||
#include <Engine/Core/Input.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Core/LogRing.h>
|
||||
#include <Engine/Core/Stats.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/Physics/PhysicsWorld.h>
|
||||
#include <Engine/Scripting/ScriptRuntime.h>
|
||||
@@ -112,6 +114,12 @@ namespace Engine {
|
||||
|
||||
void Application::Run()
|
||||
{
|
||||
// Log Panel 开闸:到此为止之前的启动日志(窗口/Vulkan/UBO/
|
||||
// IBL/网格构建)只落终端与文件——Unity Console 语义,面板只
|
||||
// 收运行期日志(场景/物理/脚本),启动噪音不进面板
|
||||
Engine::Debug::LogRing().SetEnabled(true);
|
||||
ENGINE_DEBUG("[APP] 主循环启动(Log Panel 开启;此行为 DEBUG 级,"
|
||||
"勾选 Trace 过滤可见青色 DEBUG 徽标)");
|
||||
ENGINE_INFO("Application starting");
|
||||
|
||||
m_Running = true;
|
||||
@@ -133,6 +141,8 @@ namespace Engine {
|
||||
while (m_Running) {
|
||||
const auto frameStart = std::chrono::steady_clock::now();
|
||||
|
||||
STATS.BeginFrame(); // 帧内资源分配清零(面板帧流失真修正)
|
||||
|
||||
m_Window->PollEvents();
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Created by atdunbg on 2026/9/20.
|
||||
//
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Core/LogRing.h>
|
||||
|
||||
#include "spdlog/sinks/basic_file_sink.h"
|
||||
#include "spdlog/sinks/stdout_color_sinks.h"
|
||||
@@ -26,10 +27,16 @@ namespace Engine::Logger
|
||||
auto file = std::make_shared<spdlog::sinks::basic_file_sink_mt>("HelloVulkan.log", true);
|
||||
file->set_pattern("[%Y-%m-%d %T.%e] [%l] [%t] %v");
|
||||
|
||||
// 环形缓冲(编辑器 Log Panel 数据源):时间 + 消息——级别词
|
||||
// 不进文本,由面板按 msg.level 画彩色徽标列(文本里重复一遍
|
||||
// 级别既冗余又抢徽标的视觉作用)
|
||||
auto ring = std::make_shared<Engine::Debug::RingSink>();
|
||||
ring->set_pattern("[%H:%M:%S.%e] %v");
|
||||
|
||||
// 多sink 组合默认 logger
|
||||
|
||||
// auto logger = std::make_shared<spdlog::logger>("hello", spdlog::sinks_init_list{console, file});
|
||||
auto logger = std::make_shared<spdlog::logger>("APP", spdlog::sinks_init_list{console, file});
|
||||
auto logger = std::make_shared<spdlog::logger>("APP", spdlog::sinks_init_list{console, file, ring});
|
||||
logger->set_level(spdlog::level::trace);
|
||||
logger->flush_on(spdlog::level::warn); // warn 及以上立即 flush,避免崩溃丢日志
|
||||
|
||||
|
||||
+16
-196
@@ -4,10 +4,6 @@
|
||||
#include <Engine/Core/Stats.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace Engine
|
||||
{
|
||||
Stats& Stats::Get()
|
||||
@@ -125,200 +121,24 @@ namespace Engine
|
||||
m_TotalDeallocation += bytes;
|
||||
}
|
||||
|
||||
// ============ 进程内存采样 ============
|
||||
// ============ 只读快照 ============
|
||||
|
||||
Stats::ProcessMemory Stats::ReadProcessMemory() const
|
||||
{
|
||||
ProcessMemory result;
|
||||
|
||||
// ---- 1. 从 /proc/self/status 读 VmRSS 和 VmSize ----
|
||||
// 这是内核报告的权威 RSS。smaps_rollup 会漏掉某些 NVIDIA 显存映射,
|
||||
// 但 status 的 VmRSS 包含所有 VMA。
|
||||
{
|
||||
std::ifstream status("/proc/self/status");
|
||||
std::string line;
|
||||
while (std::getline(status, line))
|
||||
{
|
||||
if (line.rfind("VmRSS:", 0) == 0)
|
||||
{
|
||||
std::istringstream iss(line.substr(6));
|
||||
u64 kb = 0;
|
||||
iss >> kb;
|
||||
result.Rss = kb * 1024;
|
||||
}
|
||||
else if (line.rfind("VmSize:", 0) == 0)
|
||||
{
|
||||
std::istringstream iss(line.substr(7));
|
||||
u64 kb = 0;
|
||||
iss >> kb;
|
||||
result.VmSize = kb * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 2. 从 /proc/self/smaps_rollup 读 PSS 和分项 ----
|
||||
// 注意:Rss 不从这里读,用上面的。
|
||||
{
|
||||
std::ifstream smaps("/proc/self/smaps_rollup");
|
||||
std::string line;
|
||||
while (std::getline(smaps, line))
|
||||
{
|
||||
std::istringstream iss(line);
|
||||
std::string key;
|
||||
u64 value = 0;
|
||||
std::string unit;
|
||||
|
||||
iss >> key;
|
||||
if (key.empty()) continue;
|
||||
if (key.back() == ':') key.pop_back();
|
||||
|
||||
if (key == "Pss")
|
||||
{
|
||||
iss >> value >> unit;
|
||||
result.Pss = value * 1024;
|
||||
}
|
||||
else if (key == "Shared_Clean")
|
||||
{
|
||||
iss >> value >> unit;
|
||||
result.SharedClean = value * 1024;
|
||||
}
|
||||
else if (key == "Private_Dirty")
|
||||
{
|
||||
iss >> value >> unit;
|
||||
result.PrivateDirty = value * 1024;
|
||||
}
|
||||
else if (key == "Private_Clean")
|
||||
{
|
||||
iss >> value >> unit;
|
||||
result.PrivateClean = value * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void Stats::SampleProcessMemory()
|
||||
Stats::Snapshot Stats::TakeSnapshot() const
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
m_ProcessMem = ReadProcessMemory();
|
||||
}
|
||||
|
||||
// ============ 打印 ============
|
||||
|
||||
static double MB(const u64 bytes) { return bytes / (1024.0 * 1024.0); }
|
||||
|
||||
void Stats::Print() const
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
|
||||
const auto proc = ReadProcessMemory();
|
||||
|
||||
ENGINE_INFO("========== Resource Stats ==========");
|
||||
ENGINE_INFO("Textures: {:>4} ({:.2f} MB)", m_TextureCount, MB(m_TextureBytes));
|
||||
ENGINE_INFO("Meshes: {:>4} ({:.2f} MB)", m_MeshCount, MB(m_MeshBytes));
|
||||
ENGINE_INFO("Buffers: {:>4} ({:.2f} MB)", m_BufferCount, MB(m_BufferBytes));
|
||||
ENGINE_INFO("--- GPU MemoryAllocator ---");
|
||||
ENGINE_INFO("Allocated: {:.2f} MB", MB(m_MemoryBlockBytes));
|
||||
ENGINE_INFO("Used: {:.2f} MB", MB(m_MemoryUsedBytes));
|
||||
ENGINE_INFO("Wasted: {:.2f} MB", MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
|
||||
ENGINE_INFO("--- Process Memory ---");
|
||||
ENGINE_INFO("VmSize: {:.2f} MB", MB(proc.VmSize));
|
||||
ENGINE_INFO("RSS (resident): {:.2f} MB", MB(proc.Rss));
|
||||
ENGINE_INFO("PSS: {:.2f} MB", MB(proc.Pss));
|
||||
ENGINE_INFO(" Shared Clean: {:.2f} MB", MB(proc.SharedClean));
|
||||
ENGINE_INFO(" Private Dirty: {:.2f} MB", MB(proc.PrivateDirty));
|
||||
ENGINE_INFO(" Private Clean: {:.2f} MB", MB(proc.PrivateClean));
|
||||
ENGINE_INFO("====================================");
|
||||
}
|
||||
|
||||
void Stats::PrintDetailed() const
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
|
||||
auto proc = ReadProcessMemory();
|
||||
|
||||
ENGINE_INFO("==================== Detailed Memory Stats ====================");
|
||||
|
||||
ENGINE_INFO(" Resources");
|
||||
ENGINE_INFO(" Textures: {:>6} {:>10.2f} MB", m_TextureCount, MB(m_TextureBytes));
|
||||
ENGINE_INFO(" Meshes: {:>6} {:>10.2f} MB", m_MeshCount, MB(m_MeshBytes));
|
||||
ENGINE_INFO(" Buffers: {:>6} {:>10.2f} MB", m_BufferCount, MB(m_BufferBytes));
|
||||
ENGINE_INFO(" Total: {:>10.2f} MB",
|
||||
MB(m_TextureBytes + m_MeshBytes + m_BufferBytes));
|
||||
|
||||
ENGINE_INFO(" GPU MemoryAllocator");
|
||||
ENGINE_INFO(" Allocated: {:>10.2f} MB", MB(m_MemoryBlockBytes));
|
||||
ENGINE_INFO(" Used: {:>10.2f} MB", MB(m_MemoryUsedBytes));
|
||||
ENGINE_INFO(" Wasted: {:>10.2f} MB",
|
||||
MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
|
||||
|
||||
ENGINE_INFO(" Allocation Churn");
|
||||
ENGINE_INFO(" This frame: +{:.2f} MB / -{:.2f} MB",
|
||||
MB(m_FrameAllocation), MB(m_FrameDeallocation));
|
||||
ENGINE_INFO(" Total: +{:.2f} MB / -{:.2f} MB",
|
||||
MB(m_TotalAllocation), MB(m_TotalDeallocation));
|
||||
|
||||
ENGINE_INFO(" Process Memory (/proc/self/status + smaps_rollup)");
|
||||
ENGINE_INFO(" VmSize: {:>10.2f} MB", MB(proc.VmSize));
|
||||
ENGINE_INFO(" RSS: {:>10.2f} MB", MB(proc.Rss));
|
||||
ENGINE_INFO(" PSS: {:>10.2f} MB", MB(proc.Pss));
|
||||
ENGINE_INFO(" Shared Clean: {:>10.2f} MB", MB(proc.SharedClean));
|
||||
ENGINE_INFO(" Private Dirty: {:>10.2f} MB", MB(proc.PrivateDirty));
|
||||
ENGINE_INFO(" Private Clean: {:>10.2f} MB", MB(proc.PrivateClean));
|
||||
|
||||
ENGINE_INFO(" Unaccounted (RSS - GPU Blocks)");
|
||||
u64 engineTotal = m_MemoryBlockBytes;
|
||||
if (proc.Rss > engineTotal)
|
||||
{
|
||||
ENGINE_INFO(" {:.2f} MB (Vulkan driver, libs, stack, heap, ...)",
|
||||
MB(proc.Rss - engineTotal));
|
||||
}
|
||||
else
|
||||
{
|
||||
ENGINE_INFO(" (GPU blocks are the bulk of RSS)");
|
||||
}
|
||||
|
||||
ENGINE_INFO("===============================================================");
|
||||
}
|
||||
|
||||
void Stats::PrintComparison() const
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
|
||||
auto proc = ReadProcessMemory();
|
||||
|
||||
u64 resourceTotal = m_TextureBytes + m_MeshBytes + m_BufferBytes;
|
||||
|
||||
ENGINE_INFO("=============== Memory Comparison ===============");
|
||||
ENGINE_INFO("");
|
||||
ENGINE_INFO("[Engine 自己统计]");
|
||||
ENGINE_INFO(" Textures {:>10.2f} MB", MB(m_TextureBytes));
|
||||
ENGINE_INFO(" Meshes {:>10.2f} MB", MB(m_MeshBytes));
|
||||
ENGINE_INFO(" Buffers {:>10.2f} MB", MB(m_BufferBytes));
|
||||
ENGINE_INFO(" ─────────────────────────────────────");
|
||||
ENGINE_INFO(" 资源合计 {:>10.2f} MB", MB(resourceTotal));
|
||||
ENGINE_INFO("");
|
||||
ENGINE_INFO(" Allocator Used {:>10.2f} MB", MB(m_MemoryUsedBytes));
|
||||
ENGINE_INFO(" Allocator Allocated {:>10.2f} MB", MB(m_MemoryBlockBytes));
|
||||
ENGINE_INFO(" Allocator Wasted {:>10.2f} MB",
|
||||
MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
|
||||
ENGINE_INFO("");
|
||||
ENGINE_INFO("[系统 /proc 读出的]");
|
||||
ENGINE_INFO(" VmRSS {:>10.2f} MB", MB(proc.Rss));
|
||||
ENGINE_INFO(" VmSize {:>10.2f} MB", MB(proc.VmSize));
|
||||
ENGINE_INFO(" PSS {:>10.2f} MB", MB(proc.Pss));
|
||||
ENGINE_INFO(" Shared Clean {:>10.2f} MB", MB(proc.SharedClean));
|
||||
ENGINE_INFO(" Private Dirty {:>10.2f} MB", MB(proc.PrivateDirty));
|
||||
ENGINE_INFO(" Private Clean {:>10.2f} MB", MB(proc.PrivateClean));
|
||||
ENGINE_INFO("");
|
||||
ENGINE_INFO("[差值]");
|
||||
ENGINE_INFO(" RSS - Allocator {:>10.2f} MB (Vulkan 驱动 + libs + 栈堆)",
|
||||
MB(proc.Rss > m_MemoryBlockBytes ? proc.Rss - m_MemoryBlockBytes : 0));
|
||||
ENGINE_INFO(" RSS - 资源合计 {:>10.2f} MB (含分配器碎片)",
|
||||
MB(proc.Rss > resourceTotal ? proc.Rss - resourceTotal : 0));
|
||||
ENGINE_INFO(" Allocated - Used {:>10.2f} MB (分配器空闲块)",
|
||||
MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
|
||||
ENGINE_INFO("================================================");
|
||||
Snapshot s;
|
||||
s.textureCount = m_TextureCount;
|
||||
s.textureBytes = m_TextureBytes;
|
||||
s.meshCount = m_MeshCount;
|
||||
s.meshBytes = m_MeshBytes;
|
||||
s.bufferCount = m_BufferCount;
|
||||
s.bufferBytes = m_BufferBytes;
|
||||
s.memoryBlockBytes = m_MemoryBlockBytes;
|
||||
s.memoryUsedBytes = m_MemoryUsedBytes;
|
||||
s.frameAllocation = m_FrameAllocation;
|
||||
s.frameDeallocation = m_FrameDeallocation;
|
||||
s.totalAllocation = m_TotalAllocation;
|
||||
s.totalDeallocation = m_TotalDeallocation;
|
||||
return s;
|
||||
}
|
||||
} // namespace Engine
|
||||
|
||||
@@ -23,10 +23,14 @@ namespace Engine {
|
||||
constexpr i32 kKeyUp = 265, kKeyDown = 264,
|
||||
kKeyLeft = 263, kKeyRight = 262;
|
||||
constexpr i32 kKeySpace = 32;
|
||||
constexpr i32 kKeyLShift = 340; // GLFW_KEY_LEFT_SHIFT
|
||||
// 手柄标准布局
|
||||
constexpr i32 kPadBtnA = 0; // GLFW_GAMEPAD_BUTTON_A
|
||||
constexpr i32 kPadBtnL3 = 9; // GLFW_GAMEPAD_BUTTON_LEFT_THUMB
|
||||
constexpr i32 kPadAxisLeftX = 0; // GLFW_GAMEPAD_AXIS_LEFT_X
|
||||
constexpr i32 kPadAxisLeftY = 1; // GLFW_GAMEPAD_AXIS_LEFT_Y
|
||||
constexpr i32 kPadAxisRightX = 2; // GLFW_GAMEPAD_AXIS_RIGHT_X
|
||||
constexpr i32 kPadAxisRightY = 3; // GLFW_GAMEPAD_AXIS_RIGHT_Y
|
||||
|
||||
constexpr f32 kRadialDeadZone = 0.15f;
|
||||
|
||||
@@ -139,6 +143,19 @@ namespace Engine {
|
||||
const size_t jump = DefineAction("Jump", ActionKind::Button);
|
||||
Bind(jump, {SourceKind::Key, kKeySpace, 1.0f, kX});
|
||||
Bind(jump, {SourceKind::GamepadButton, kPadBtnA, 1.0f, kX});
|
||||
|
||||
// Run:Shift + 手柄 L3(按下左摇杆 = 冲刺,Xbox 惯例)。
|
||||
// 注意不能用"摇杆推满"判跑——键盘 WASD 经死区/钳制后幅度
|
||||
// 恒为 1.0,会把走路误判成跑(真实踩坑:WASD 一动就 run)
|
||||
const size_t run = DefineAction("Run", ActionKind::Button);
|
||||
Bind(run, {SourceKind::Key, kKeyLShift, 1.0f, kX});
|
||||
Bind(run, {SourceKind::GamepadButton, kPadBtnL3, 1.0f, kX});
|
||||
|
||||
// Look:右摇杆(相机环绕)。鼠标是相对增量不走动作层,
|
||||
// 相机脚本里 GetMouseDelta 叠加
|
||||
const size_t look = DefineAction("Look", ActionKind::Axis2D);
|
||||
Bind(look, {SourceKind::GamepadAxis, kPadAxisRightX, +1.0f, kX});
|
||||
Bind(look, {SourceKind::GamepadAxis, kPadAxisRightY, +1.0f, kY});
|
||||
}
|
||||
|
||||
// ---- 运行期 ----
|
||||
|
||||
@@ -25,8 +25,10 @@ namespace Engine {
|
||||
s.mShape = new JPH::CapsuleShape(
|
||||
std::max(params.radius, 0.01f),
|
||||
std::max(params.halfHeight, 0.0f));
|
||||
s.mShapeOffset = JPH::Vec3(0.0f,
|
||||
params.halfHeight + params.radius, 0.0f);
|
||||
s.mShapeOffset = JPH::Vec3(
|
||||
params.shapeOffset.x,
|
||||
params.shapeOffset.y + params.halfHeight + params.radius,
|
||||
params.shapeOffset.z);
|
||||
|
||||
m_Character = new JPH::CharacterVirtual(
|
||||
&s, JPH::RVec3(position.x, position.y, position.z),
|
||||
@@ -37,6 +39,20 @@ namespace Engine {
|
||||
m_Ext.mWalkStairsStepUp = JPH::Vec3(0.0f, std::max(params.stepHeight, 0.0f), 0.0f);
|
||||
}
|
||||
|
||||
CharacterController::~CharacterController() {
|
||||
// 先摘除再释放:列表持裸 CharacterVirtual*,否则悬垂
|
||||
if (m_VsChar != nullptr) {
|
||||
m_VsChar->Remove(m_Character.GetPtr());
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterController::SetCharacterCollision(
|
||||
JPH::CharacterVsCharacterCollisionSimple &list) {
|
||||
m_VsChar = &list;
|
||||
m_Character->SetCharacterVsCharacterCollision(&list);
|
||||
list.Add(m_Character.GetPtr());
|
||||
}
|
||||
|
||||
void CharacterController::Update(const f32 dt, const JPH::Vec3 &gravity,
|
||||
JPH::TempAllocator &allocator) {
|
||||
// 重力积分:着地时清掉向下分量(防贴地累积穿透速度),悬空时累加
|
||||
@@ -50,6 +66,10 @@ namespace Engine {
|
||||
m_Character->SetLinearVelocity(v + gravity * dt);
|
||||
}
|
||||
|
||||
// 扫掠前速度快照:撞墙判定用。ExtendedUpdate 会抵消朝向陡面/
|
||||
// 墙面的速度分量,扫掠后再读就归零了(见头文件说明)
|
||||
m_SweepVelocity = m_Character->GetLinearVelocity();
|
||||
|
||||
// 扫掠推进(撞墙滑动 + 斜坡限制 + StickToFloor 贴地 + WalkStairs
|
||||
// 上台阶)。过滤器默认全放行:角色与静/动层全碰
|
||||
m_Character->ExtendedUpdate(dt, gravity, m_Ext,
|
||||
@@ -78,6 +98,16 @@ namespace Engine {
|
||||
return m_Character->IsSupported();
|
||||
}
|
||||
|
||||
const JPH::CharacterVirtual::ContactList&
|
||||
CharacterController::GetActiveContacts() const {
|
||||
return m_Character->GetActiveContacts();
|
||||
}
|
||||
|
||||
glm::vec3 CharacterController::GetSweepVelocity() const {
|
||||
return {m_SweepVelocity.GetX(), m_SweepVelocity.GetY(),
|
||||
m_SweepVelocity.GetZ()};
|
||||
}
|
||||
|
||||
glm::vec3 CharacterController::GetPosition() const {
|
||||
const JPH::RVec3 p = m_Character->GetPosition();
|
||||
return {static_cast<f32>(p.GetX()), static_cast<f32>(p.GetY()),
|
||||
|
||||
@@ -176,6 +176,10 @@ namespace Engine {
|
||||
// 直接成员会先于 call_once 构造 → 函数指针未注册 → SIGSEGV at 0x0
|
||||
m_DebugDraw = CreateScope<PhysicsDebugDraw>();
|
||||
|
||||
// 角色间碰撞互查表(内部 Array 走 JPH 分配器,须在 call_once 之后)
|
||||
m_VsCharacterCollision =
|
||||
CreateScope<JPH::CharacterVsCharacterCollisionSimple>();
|
||||
|
||||
m_TempAllocator = CreateScope<JPH::TempAllocatorImpl>(16 * 1024 * 1024);
|
||||
const u32 threads = std::max(1u, std::thread::hardware_concurrency() - 1);
|
||||
m_JobSystem = CreateScope<JPH::JobSystemThreadPool>(
|
||||
@@ -208,6 +212,7 @@ namespace Engine {
|
||||
// 释放烘焙几何——必须先于 UnregisterTypes
|
||||
m_DebugDraw.reset();
|
||||
m_Bodies.clear();
|
||||
m_Characters.clear(); // 角色析构回 m_VsCharacterCollision 摘除,先于 Jolt 反注册
|
||||
m_ContactListener.reset(); // 监听器先于 system 析构(持 m_Owner 引用)
|
||||
m_System.reset();
|
||||
// 单世界假设:进程退出时统一反注册(多世界时改为引用计数)
|
||||
@@ -695,20 +700,24 @@ namespace Engine {
|
||||
p.mass = cc.mass;
|
||||
p.maxSlopeAngle = cc.maxSlopeAngle;
|
||||
p.stepHeight = cc.stepHeight;
|
||||
p.shapeOffset = cc.shapeOffset;
|
||||
|
||||
auto found = m_Characters.find(bits);
|
||||
if (found == m_Characters.end()) {
|
||||
CharacterEntry entry;
|
||||
entry.cc = std::make_unique<CharacterController>(
|
||||
*m_System, p, tr.GetWorldPosition());
|
||||
entry.cc->SetCharacterCollision(*m_VsCharacterCollision);
|
||||
entry.lastPos = tr.GetWorldPosition();
|
||||
m_Characters.emplace(bits, std::move(entry));
|
||||
continue;
|
||||
}
|
||||
if (found->second.cc->GetParams() != p) {
|
||||
// 参数变更 → 重建(位置沿用当前 Transform)
|
||||
// 参数变更 → 重建(位置沿用当前 Transform);旧实例析构
|
||||
// 自动摘除旧条目,新实例重新登记
|
||||
found->second.cc = std::make_unique<CharacterController>(
|
||||
*m_System, p, tr.GetWorldPosition());
|
||||
found->second.cc->SetCharacterCollision(*m_VsCharacterCollision);
|
||||
found->second.lastPos = tr.GetWorldPosition();
|
||||
continue;
|
||||
}
|
||||
@@ -742,6 +751,42 @@ namespace Engine {
|
||||
static_cast<entt::entity>(bits), p,
|
||||
tr->GetWorldRotationQuat(), tr->GetWorldScale());
|
||||
entry.lastPos = p;
|
||||
|
||||
// ---- 角色碰撞事件合成(Enter/Exit)----
|
||||
// CharacterVirtual 非刚体,Jolt 的 ContactListener 不会为角色
|
||||
// 回调——脚本因此收不到"玩家撞墙"。这里读角色本步活跃接触,
|
||||
// 与上一步集合对比出边沿,合成与刚体同款的 CollisionEvent
|
||||
// (a = 角色实体,b = 被撞实体)。只处理刚体接触(mBodyB 有效):
|
||||
// 角色↔角色由 CharacterVsCharacterCollision 的物理互查处理,
|
||||
// 不发脚本事件(对方是角色时 mBodyB 无效,天然跳过)
|
||||
std::unordered_set<u32> now;
|
||||
now.reserve(entry.contacts.size() + 4);
|
||||
for (const JPH::CharacterContact& c :
|
||||
entry.cc->GetActiveContacts()) {
|
||||
if (c.mBodyB.IsInvalid()) continue; // 对方非刚体(角色)
|
||||
if (c.mWasDiscarded) continue; // 被丢弃/传感器
|
||||
if (c.mDistance > 0.0f) continue; // >0 = 预测接触,未真相交
|
||||
u32 other = 0;
|
||||
if (!TryBitsByBodyId(c.mBodyB, other)) continue;
|
||||
now.insert(other);
|
||||
if (entry.contacts.count(other) == 0) {
|
||||
const glm::vec3 pt(
|
||||
static_cast<f32>(c.mPosition.GetX()),
|
||||
static_cast<f32>(c.mPosition.GetY()),
|
||||
static_cast<f32>(c.mPosition.GetZ()));
|
||||
const glm::vec3 n(c.mContactNormal.GetX(),
|
||||
c.mContactNormal.GetY(),
|
||||
c.mContactNormal.GetZ());
|
||||
QueueEvent({bits, other, CollisionEvent::Kind::Added, pt, n});
|
||||
}
|
||||
}
|
||||
for (const u32 prev : entry.contacts) {
|
||||
if (now.count(prev) == 0) {
|
||||
QueueEvent(
|
||||
{bits, prev, CollisionEvent::Kind::Removed, {}, {}});
|
||||
}
|
||||
}
|
||||
entry.contacts = std::move(now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,8 +812,14 @@ namespace Engine {
|
||||
// 动态/运动体 → ConvexHull(MeshShape 三角网格太贵,Jolt 不允许动)
|
||||
if (rb.type != ECS::RigidBodyComponent::BodyType::Static) {
|
||||
JPH::ConvexHullShapeSettings cs;
|
||||
for (const auto &mesh: model->GetMeshes()) {
|
||||
for (const auto &p: mesh->GetCpuPositions()) {
|
||||
const auto &meshes = model->GetMeshes();
|
||||
for (size_t mi = 0; mi < meshes.size(); ++mi) {
|
||||
// 单 mesh 子引用:碰撞只取渲染可见的同一 mesh(语义跟随)
|
||||
if (mc->meshIndex >= 0 &&
|
||||
static_cast<int>(mi) != mc->meshIndex) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &p: meshes[mi]->GetCpuPositions()) {
|
||||
cs.mPoints.emplace_back(p.x * s.x, p.y * s.y, p.z * s.z);
|
||||
}
|
||||
}
|
||||
@@ -788,9 +839,15 @@ namespace Engine {
|
||||
JPH::VertexList verts;
|
||||
JPH::IndexedTriangleList tris;
|
||||
u32 base = 0;
|
||||
for (const auto &mesh: model->GetMeshes()) {
|
||||
const auto &pos = mesh->GetCpuPositions();
|
||||
const auto &idx = mesh->GetCpuIndices();
|
||||
const auto &meshes = model->GetMeshes();
|
||||
for (size_t mi = 0; mi < meshes.size(); ++mi) {
|
||||
// 单 mesh 子引用:碰撞只取渲染可见的同一 mesh(语义跟随)
|
||||
if (mc->meshIndex >= 0 &&
|
||||
static_cast<int>(mi) != mc->meshIndex) {
|
||||
continue;
|
||||
}
|
||||
const auto &pos = meshes[mi]->GetCpuPositions();
|
||||
const auto &idx = meshes[mi]->GetCpuIndices();
|
||||
verts.reserve(verts.size() + pos.size());
|
||||
for (const auto &p: pos) {
|
||||
verts.push_back(JPH::Float3(p.x * s.x, p.y * s.y, p.z * s.z));
|
||||
@@ -1112,6 +1169,14 @@ namespace Engine {
|
||||
: glm::vec3{0.0f};
|
||||
}
|
||||
|
||||
glm::vec3
|
||||
PhysicsWorld::GetCharacterImpactVelocity(const u32 entityBits) const {
|
||||
const auto found = m_Characters.find(entityBits);
|
||||
return found != m_Characters.end()
|
||||
? found->second.cc->GetSweepVelocity()
|
||||
: glm::vec3{0.0f};
|
||||
}
|
||||
|
||||
// ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)----
|
||||
|
||||
void PhysicsWorld::Writeback() {
|
||||
|
||||
@@ -32,7 +32,9 @@ namespace Engine
|
||||
{
|
||||
constexpr u32 kFaceCount = 6;
|
||||
constexpr u32 kIrradianceFaceSize = 32; // 漫反射辐照度面边长(低频)
|
||||
constexpr u32 kPrefilterFaceSize = 128; // pre-filter 面边长(mip0)
|
||||
// pre-filter 面边长(mip0):roughness→0 的镜面反射采 mip0,
|
||||
// 128²/面放大到全屏 = 块状马赛克;256 = 无 SSR 时的清晰度底线
|
||||
constexpr u32 kPrefilterFaceSize = 256;
|
||||
constexpr u32 kPrefilterMips = 5; // mip 分级 roughness 数
|
||||
constexpr u32 kBrdfLutSize = 128; // BRDF LUT 边长
|
||||
constexpr u32 kGroupSize = 8; // compute local_size(shader 同步)
|
||||
|
||||
@@ -84,11 +84,21 @@ namespace Engine
|
||||
",s:" + std::to_string(params.surface.x) +
|
||||
"," + std::to_string(params.surface.y) +
|
||||
"," + std::to_string(params.surface.z) +
|
||||
"," + std::to_string(params.surface.w);
|
||||
// per-slot UV 变换 + unlit 入键:同一张贴图配不同变换是不同材质
|
||||
//(KHR_texture_transform 测试模型:一张棋盘格被 82 个材质以不同
|
||||
// 变换共享,键漏了会串成同一份)
|
||||
key += ",m:" + std::to_string(params.misc.x);
|
||||
"," + std::to_string(params.surface.w) +
|
||||
",e:" + std::to_string(params.emissiveFactor.x) +
|
||||
"," + std::to_string(params.emissiveFactor.y) +
|
||||
"," + std::to_string(params.emissiveFactor.z);
|
||||
// per-slot UV 变换 + unlit + UV 镜像入键:同一张贴图配不同变换是
|
||||
// 不同材质(KHR_texture_transform 测试模型:一张棋盘格被 82 个材质
|
||||
// 以不同变换共享,键漏了会串成同一份)
|
||||
key += ",m:" + std::to_string(params.misc.x) +
|
||||
"," + std::to_string(params.misc.y) +
|
||||
"," + std::to_string(params.misc.z);
|
||||
// alpha 叠加遮罩入键:同贴图不同叠加色是不同材质
|
||||
key += ",o:" + std::to_string(params.alphaOverlay.x) +
|
||||
"," + std::to_string(params.alphaOverlay.y) +
|
||||
"," + std::to_string(params.alphaOverlay.z) +
|
||||
"," + std::to_string(params.alphaOverlay.w);
|
||||
for (u32 slot = 0; slot < kSlotCount; ++slot) {
|
||||
key += ",u" + std::to_string(slot) + ":" +
|
||||
std::to_string(params.uvA[slot].x) + "," +
|
||||
|
||||
@@ -29,7 +29,12 @@ namespace Engine
|
||||
return Texture::CreateSolidColor(device, allocator,
|
||||
0, 128, 0, false);
|
||||
case Material::Emissive:
|
||||
return Texture::CreateSolidColor(device, allocator, 0, 0, 0);
|
||||
// 兜底白(非黑):emissive = 贴图 × factor × strength——黑
|
||||
// 兜底会把无贴图材质的 factor/strength 一起乘没(调了恒黑
|
||||
// 的困惑源);白兜底 = 无贴图时 factor 即发光色,有贴图时
|
||||
// glTF 语义不变(factor 黑的普通模型依旧不发光)
|
||||
return Texture::CreateSolidColor(device, allocator,
|
||||
255, 255, 255);
|
||||
case Material::Ao:
|
||||
return Texture::CreateSolidColor(device, allocator,
|
||||
255, 0, 0, false);
|
||||
@@ -84,8 +89,12 @@ namespace Engine
|
||||
EmitVec4(out, data.params.baseColor);
|
||||
out << YAML::Key << "Surface" << YAML::Value;
|
||||
EmitVec4(out, data.params.surface);
|
||||
out << YAML::Key << "EmissiveFactor" << YAML::Value;
|
||||
EmitVec4(out, data.params.emissiveFactor);
|
||||
out << YAML::Key << "Misc" << YAML::Value;
|
||||
EmitVec4(out, data.params.misc);
|
||||
out << YAML::Key << "AlphaOverlay" << YAML::Value;
|
||||
EmitVec4(out, data.params.alphaOverlay);
|
||||
// per-slot UV 变换(KHR_texture_transform):6 槽各 A(4 分量) + T
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
out << YAML::Key << ("UvA" + std::to_string(slot)) << YAML::Value;
|
||||
@@ -145,8 +154,12 @@ namespace Engine
|
||||
m["BaseColor"], MaterialParams{}.baseColor);
|
||||
out.params.surface = ReadVec4(
|
||||
m["Surface"], MaterialParams{}.surface);
|
||||
out.params.emissiveFactor = ReadVec4(
|
||||
m["EmissiveFactor"], MaterialParams{}.emissiveFactor);
|
||||
out.params.misc = ReadVec4(
|
||||
m["Misc"], MaterialParams{}.misc);
|
||||
out.params.alphaOverlay = ReadVec4(
|
||||
m["AlphaOverlay"], MaterialParams{}.alphaOverlay);
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
out.params.uvA[slot] = ReadVec4(
|
||||
m["UvA" + std::to_string(slot)],
|
||||
|
||||
+1012
-70
File diff suppressed because it is too large
Load Diff
@@ -279,6 +279,11 @@ namespace Engine::Passes
|
||||
// frag 仍写 gl_FragDepth 的时期做的(禁 early-Z),归因存疑;
|
||||
// gl_FragDepth 已删除,FALSE 若复现病态再另行隔离。
|
||||
cmd.setDepthWriteEnable(VK_FALSE);
|
||||
// 单面玻璃:透明段剔除背面。不透明段 cullNone + depthWrite
|
||||
// 无所谓(深度裁决),透明段 cullNone 会让闭合体(cube)的
|
||||
// 正反面片按 mesh 顺序乱序 over 混合 = "深度错乱";cull back
|
||||
// 每像素至多一面。双面透明(树叶卡片)将来加材质开关
|
||||
cmd.setCullMode(vk::CullModeFlagBits::eBack);
|
||||
|
||||
vk::ColorBlendEquationEXT premul;
|
||||
premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
|
||||
|
||||
@@ -757,6 +757,12 @@ namespace Engine
|
||||
|
||||
const auto& meshes = model->GetMeshes();
|
||||
for (size_t meshIdx = 0; meshIdx < meshes.size(); ++meshIdx) {
|
||||
// 单 mesh 子引用:只收集指派的那一个(覆盖表/物理
|
||||
// 均按原模型下标,语义不变)
|
||||
if (mr.meshIndex >= 0 &&
|
||||
static_cast<int>(meshIdx) != mr.meshIndex) {
|
||||
continue;
|
||||
}
|
||||
auto& mesh = meshes[meshIdx];
|
||||
const auto& bounds = mesh->GetBounds();
|
||||
const glm::vec3 worldCenter =
|
||||
|
||||
@@ -95,6 +95,12 @@ namespace Engine {
|
||||
halfSize, r, g, b, a);
|
||||
}
|
||||
|
||||
Scope<Model> VulkanAssetUploader::CreateCube(f32 halfSize, u8 r, u8 g, u8 b)
|
||||
{
|
||||
return Model::CreateCube(m_Device, m_Allocator, m_Descriptors,
|
||||
m_TextureLayout, halfSize, r, g, b);
|
||||
}
|
||||
|
||||
Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path)
|
||||
{
|
||||
// compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
#include <Engine/Core/Application.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/ECS/EntityHandle.h>
|
||||
#include <Engine/Physics/PhysicsEvents.h>
|
||||
#include <Engine/Physics/PhysicsWorld.h>
|
||||
#include <Engine/Serialization/SceneSerializer.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -19,6 +23,25 @@ namespace Engine {
|
||||
return *m_App;
|
||||
}
|
||||
|
||||
// ---- 场景实体 spawn/destroy 底座(声明在 Script.h,实现下沉
|
||||
// 此处——Application 完整类型只在 cpp 可见)----
|
||||
ECS::EntityHandle ScriptBehavior::Spawn(const std::string& name) {
|
||||
return App().GetScene().CreateEntity(name);
|
||||
}
|
||||
|
||||
void ScriptBehavior::Destroy(ECS::Entity e) {
|
||||
App().GetScene().DestroyEntity(e);
|
||||
}
|
||||
|
||||
// Prefab 实例化(声明在 Script.h,实现下沉此处——同 Spawn/
|
||||
// Destroy 模式;序列化器与 Application 门面都在引擎内可达)
|
||||
ECS::Entity ScriptBehavior::Instantiate(const std::string& prefabPath,
|
||||
const glm::vec3& pos,
|
||||
const glm::quat& rot) {
|
||||
return Serialization::SceneSerializer::InstantiatePrefab(
|
||||
App().GetScene(), App().GetAssets(), prefabPath, &pos, &rot);
|
||||
}
|
||||
|
||||
// ---- 参数按名读写(ScriptBehavior 声明,经绑定表;写穿
|
||||
// Item.params 由运行时 SetParam 完成)----
|
||||
bool ScriptBehavior::WriteParam(const std::string& name,
|
||||
@@ -41,7 +64,47 @@ namespace Engine {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- 碰撞事件派发(Update 开头、Sync 之前):PhysicsWorld 最近
|
||||
// 一次步进收集的事件转发到涉事实例。时序保证:Step 先于
|
||||
// Update(帧循环序)→ 此处读到的是本帧物理结果;回调里
|
||||
// spawn/destroy 安全——destroy 入延迟队列(帧尾 flush),
|
||||
// spawn 不改 m_Instances(下一帧 Sync 对账建实例)。禁用
|
||||
// 实例不收(Unity enabled 语义对齐);Persisted 每步都来,
|
||||
// 直接跳过(Enter/Exit 已够 gameplay 用)
|
||||
void ScriptRuntime::DispatchCollisions() {
|
||||
if (!m_App) {
|
||||
return;
|
||||
}
|
||||
const std::vector<CollisionEvent>& events =
|
||||
m_App->GetPhysics().GetEvents();
|
||||
for (const auto& ev : events) {
|
||||
if (ev.kind == CollisionEvent::Kind::Persisted) {
|
||||
continue;
|
||||
}
|
||||
const bool enter = ev.kind == CollisionEvent::Kind::Added;
|
||||
for (const u32 self : {ev.a, ev.b}) {
|
||||
const u32 other = (self == ev.a) ? ev.b : ev.a;
|
||||
const auto it = m_Instances.find(self);
|
||||
if (it == m_Instances.end()) {
|
||||
continue;
|
||||
}
|
||||
for (auto& inst : it->second) {
|
||||
if (!inst.behavior || !inst.enabled) {
|
||||
continue;
|
||||
}
|
||||
if (enter) {
|
||||
inst.behavior->OnCollisionEnter(
|
||||
other, ev.point, ev.normal);
|
||||
} else {
|
||||
inst.behavior->OnCollisionExit(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptRuntime::Update(f32 dt) {
|
||||
DispatchCollisions();
|
||||
Sync();
|
||||
|
||||
auto& reg = m_Scene.GetRegistry();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,55 @@
|
||||
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <stdexcept>
|
||||
#include <algorithm>
|
||||
|
||||
#include "Engine/Core/Stats.h"
|
||||
|
||||
namespace Engine {
|
||||
|
||||
u64 EstimateImageBytes(const vk::Format fmt, const u32 w, const u32 h,
|
||||
const u32 depth, const u32 mips)
|
||||
{
|
||||
// 线性格式 = 字节/像素;BC 压缩 = 字节/4x4 块(按块向上取整)。
|
||||
// 未识别格式按 RGBA8 保守估算。逐 mip 累加(mip 链约占 1/3 额外)
|
||||
f32 bpp = 0;
|
||||
u32 blockBytes = 0;
|
||||
switch (fmt) {
|
||||
case vk::Format::eR8Unorm:
|
||||
case vk::Format::eR8Srgb: bpp = 1; break;
|
||||
case vk::Format::eR8G8Unorm:
|
||||
case vk::Format::eR8G8Srgb: bpp = 2; break;
|
||||
case vk::Format::eR16G16B16A16Sfloat: bpp = 8; break;
|
||||
case vk::Format::eR32G32B32A32Sfloat: bpp = 16; break;
|
||||
case vk::Format::eB10G11R11UfloatPack32: bpp = 4; break;
|
||||
case vk::Format::eBc1RgbaUnormBlock:
|
||||
case vk::Format::eBc1RgbaSrgbBlock:
|
||||
case vk::Format::eBc4UnormBlock: blockBytes = 8; break;
|
||||
case vk::Format::eBc2SrgbBlock:
|
||||
case vk::Format::eBc2UnormBlock:
|
||||
case vk::Format::eBc3SrgbBlock:
|
||||
case vk::Format::eBc3UnormBlock:
|
||||
case vk::Format::eBc5UnormBlock:
|
||||
case vk::Format::eBc6HUfloatBlock:
|
||||
case vk::Format::eBc7SrgbBlock:
|
||||
case vk::Format::eBc7UnormBlock: blockBytes = 16; break;
|
||||
default: bpp = 4; break;
|
||||
}
|
||||
u64 total = 0;
|
||||
const u32 levels = std::max(1u, mips);
|
||||
for (u32 mip = 0; mip < levels; ++mip) {
|
||||
const u64 mw = std::max(1u, w >> mip);
|
||||
const u64 mh = std::max(1u, h >> mip);
|
||||
if (blockBytes != 0) {
|
||||
total += static_cast<u64>((mw + 3) / 4) *
|
||||
static_cast<u64>((mh + 3) / 4) * blockBytes * depth;
|
||||
} else {
|
||||
total += mw * mh * static_cast<u64>(bpp) * depth;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// props → VMA 创建参数:
|
||||
@@ -88,6 +132,7 @@ namespace Engine {
|
||||
out.MappedPtr = info.pMappedData; // 非 MAPPED 标记时为 null
|
||||
|
||||
STATS.AddMemoryUsed(size);
|
||||
STATS.AddMemoryBlock(size); // 1:1 VMA 分配:块 == 占用(无子分配)
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -108,11 +153,15 @@ namespace Engine {
|
||||
|
||||
Allocation out;
|
||||
out.Image = image;
|
||||
out.Size = ii.extent.depth * ii.extent.height * ii.extent.width; // 近似值(统计用)
|
||||
// 按真实格式 + mip 链估算(原来 w*h*d 近似值对 HDR/压缩全错)
|
||||
out.Size = EstimateImageBytes(imageInfo.format,
|
||||
ii.extent.width, ii.extent.height,
|
||||
ii.extent.depth, imageInfo.mipLevels);
|
||||
out.Handle = handle;
|
||||
out.Valid = true;
|
||||
|
||||
STATS.AddMemoryUsed(out.Size);
|
||||
STATS.AddMemoryBlock(out.Size);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -128,6 +177,7 @@ namespace Engine {
|
||||
}
|
||||
|
||||
STATS.RemoveMemoryUsed(alloc.Size);
|
||||
STATS.RemoveMemoryBlock(alloc.Size);
|
||||
alloc = {};
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,19 @@ namespace Engine
|
||||
|
||||
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb)
|
||||
{
|
||||
// 重入安全:CreateFromEncodedMemory 先经 ProceduralTag ctor 落 1x1
|
||||
// 占位(只为确定 device/allocator 成员),再调本函数传真实像素。
|
||||
// 必须先成对释放占位资源,否则 1x1 的 image/view/sampler 泄漏 →
|
||||
// allocator 析构时 VMA 块非空断言崩溃(GLB 内嵌贴图模型关闭必崩)
|
||||
if (m_Image) {
|
||||
const auto& vkDevice = m_Device.GetDevice();
|
||||
vkDevice.destroySampler(m_Sampler);
|
||||
vkDevice.destroyImageView(m_View);
|
||||
m_Allocator.Destroy(m_ImageAllocation);
|
||||
m_Sampler = nullptr;
|
||||
m_View = nullptr;
|
||||
m_Image = nullptr;
|
||||
}
|
||||
m_Width = width;
|
||||
m_Height = height;
|
||||
m_Format = srgb ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm;
|
||||
@@ -299,8 +312,12 @@ namespace Engine
|
||||
m_Device.GetDevice().destroyCommandPool(pool);
|
||||
m_Allocator.Destroy(stagingAlloc);
|
||||
|
||||
// 显存占用 = 宽 * 高 * 4 字节(RGBA8)
|
||||
m_TextureBytes = static_cast<u64>(m_Width) * m_Height * 4;
|
||||
// 显存占用按真实格式 + mip 链估算(与分配器台账同一算法)
|
||||
if (m_TextureBytes != 0) {
|
||||
STATS.RemoveTexture(m_TextureBytes); // 重建防双计
|
||||
}
|
||||
m_TextureBytes = EstimateImageBytes(m_Format, m_Width, m_Height, 1,
|
||||
m_MipLevels);
|
||||
STATS.AddTexture(m_TextureBytes);
|
||||
}
|
||||
|
||||
@@ -492,6 +509,9 @@ namespace Engine
|
||||
m_Device.GetDevice().destroyCommandPool(pool);
|
||||
m_Allocator.Destroy(stagingAlloc);
|
||||
|
||||
if (m_TextureBytes != 0) {
|
||||
STATS.RemoveTexture(m_TextureBytes); // 重建防双计
|
||||
}
|
||||
m_TextureBytes = total;
|
||||
STATS.AddTexture(m_TextureBytes);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 ufbx-python contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
This project includes ufbx (https://github.com/ufbx/ufbx), which is licensed under the MIT License:
|
||||
|
||||
Copyright (c) 2020-2023 Samuli Raivio
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+33096
File diff suppressed because it is too large
Load Diff
+6062
File diff suppressed because it is too large
Load Diff
@@ -45,18 +45,25 @@ layout(set = 1, binding = 6) uniform MaterialParams {
|
||||
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
||||
vec4 baseColor; // glTF baseColorFactor:rgb 调色 albedo,a = BLEND 透明度
|
||||
vec4 surface; // x = ior(F0 换算),y = transmission(不透明段恒 0 语义),z = 自发光强度,w = 体积厚度
|
||||
vec4 misc; // x = unlit(KHR_materials_unlit,1 = 早退直出 albedo)
|
||||
vec4 misc; // x = unlit(KHR_materials_unlit,1 = 早退直出 albedo),y = UV 左右镜像,z = UV 上下镜像
|
||||
vec4 emissiveFactor; // 自发光颜色(rgb × emissiveMap × surface.z;黑 = 不发光)
|
||||
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22),槽序 = binding
|
||||
vec4 uvT[6]; // 每槽 UV 平移 (tx, ty, 0, 0)
|
||||
vec4 alphaOverlay; // rgb = albedo alpha 叠加色,a = 强度(0 = 关闭)
|
||||
} matParams;
|
||||
|
||||
// 每槽 UV 变换:uv' = A·uv + t(identity 时退化为直通)。slot 必须
|
||||
// 字面量调用点(编译期展开索引);6 = Material::kSlotCount
|
||||
// 末尾叠加材质级镜像(misc.y = 左右,misc.z = 上下),作用全部槽:
|
||||
// 纠正源资产 UV 朝向(部分角色 rip 与 glTF 上左原点约定相反)
|
||||
vec2 ApplyUv(int slot, vec2 uv) {
|
||||
vec4 a = matParams.uvA[slot];
|
||||
vec4 t = matParams.uvT[slot];
|
||||
return vec2(a.x * uv.x + a.y * uv.y + t.x,
|
||||
vec2 o = vec2(a.x * uv.x + a.y * uv.y + t.x,
|
||||
a.z * uv.x + a.w * uv.y + t.y);
|
||||
if (matParams.misc.y > 0.5) o.x = 1.0 - o.x;
|
||||
if (matParams.misc.z > 0.5) o.y = 1.0 - o.y;
|
||||
return o;
|
||||
}
|
||||
|
||||
// CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
|
||||
@@ -153,9 +160,13 @@ void main() {
|
||||
// alphaMode=MASK 镂空(表针/栅栏等硬边贴图):低于阈值的纹素直接丢弃
|
||||
if (matParams.factors.w > 0.5 && albedoTex.a < 0.5) discard;
|
||||
vec3 albedo = albedoTex.rgb * matParams.baseColor.rgb;
|
||||
// albedo 贴图 alpha 作细节叠加遮罩(脸部腮红/卧蚕等局部效果):
|
||||
// 强度 0 = mix 恒等,无副作用
|
||||
albedo = mix(albedo, matParams.alphaOverlay.rgb,
|
||||
albedoTex.a * matParams.alphaOverlay.a);
|
||||
// KHR_materials_unlit:无光照语义(文字标签/示意图),albedo 直出
|
||||
if (matParams.misc.x > 0.5) {
|
||||
outColor = vec4(albedo, albedoTex.a * matParams.baseColor.a);
|
||||
outColor = vec4(albedo, 1.0);
|
||||
outVelocity = fragVelocity;
|
||||
return;
|
||||
}
|
||||
@@ -166,8 +177,10 @@ void main() {
|
||||
matParams.factors.z) * matParams.factors.y,
|
||||
0.045, 1.0);
|
||||
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
|
||||
// 自发光强度(KHR_materials_emissive_strength,缺省 1)
|
||||
vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb * matParams.surface.z;
|
||||
// 自发光强度(KHR_materials_emissive_strength,缺省 1);
|
||||
// emissiveFactor = 颜色因子(无贴图时发光的入口——兜底贴图恒黑)
|
||||
vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb
|
||||
* matParams.emissiveFactor.rgb * matParams.surface.z;
|
||||
|
||||
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, ApplyUv(1, fragUV));
|
||||
vec3 V = normalize(ubo.cameraPos.xyz - fragWorldPos);
|
||||
@@ -256,9 +269,11 @@ void main() {
|
||||
vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb
|
||||
* mix(0.4, 1.0, shade0); // 阴影区环境同步衰减(简化遮蔽)
|
||||
|
||||
// alpha = albedo 贴图覆盖率 × baseColorFactor.a(glTF alphaMode=BLEND
|
||||
// 混合源);不透明资产两者恒 1 行为不变。预乘在 tonemap 段完成。
|
||||
outColor = vec4(ambient + Lo + emissive, albedoTex.a * matParams.baseColor.a);
|
||||
// 不透明段覆盖率恒 1:本 pass 只画 alphaMode=OPAQUE/MASK 表面,像素必
|
||||
// 被完全覆盖。绝不能拿 albedo 贴图 alpha 当覆盖率——HI3/Genshin 脸部
|
||||
// 贴图的 alpha 是腮红/卧蚕遮罩(大面积 0),写进 HDR.a 后会被 tonemap
|
||||
// 的预乘 (rgb * a) 拉成纯黑。贴图 alpha 的细节用法见 alphaOverlay。
|
||||
outColor = vec4(ambient + Lo + emissive, 1.0);
|
||||
// ==== DEBUG B1: 纯 albedo 直出(验证贴图绑定/UV 采样)====
|
||||
// outColor = vec4(albedo, 1.0);
|
||||
outVelocity = fragVelocity;
|
||||
|
||||
@@ -45,17 +45,23 @@ layout(set = 1, binding = 6) uniform MaterialParams {
|
||||
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
||||
vec4 baseColor; // glTF baseColorFactor:rgb 调色 albedo,a = BLEND 透明度
|
||||
vec4 surface; // x = ior,y = transmission(KHR_materials_transmission),z = 自发光强度,w = 体积厚度
|
||||
vec4 misc; // x = unlit(透射材质不会是 unlit,仅为与 simple_shader 布局一致)
|
||||
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22)
|
||||
vec4 misc; // x = unlit(透射材质不会是 unlit,仅为与 simple_shader 布局一致),y = UV 左右镜像,z = UV 上下镜像
|
||||
vec4 emissiveFactor; // 自发光颜色(rgb × emissiveMap × surface.z;黑 = 不发光)
|
||||
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22),槽序 = binding
|
||||
vec4 uvT[6]; // 每槽 UV 平移 (tx, ty, 0, 0)
|
||||
vec4 alphaOverlay; // rgb = albedo alpha 叠加色,a = 强度(0 = 关闭)
|
||||
} matParams;
|
||||
|
||||
// 每槽 UV 变换:uv' = A·uv + t(与 simple_shader.frag 同款,6 = kSlotCount)
|
||||
// 末尾叠加材质级镜像(misc.y = 左右,misc.z = 上下),作用全部槽
|
||||
vec2 ApplyUv(int slot, vec2 uv) {
|
||||
vec4 a = matParams.uvA[slot];
|
||||
vec4 t = matParams.uvT[slot];
|
||||
return vec2(a.x * uv.x + a.y * uv.y + t.x,
|
||||
vec2 o = vec2(a.x * uv.x + a.y * uv.y + t.x,
|
||||
a.z * uv.x + a.w * uv.y + t.y);
|
||||
if (matParams.misc.y > 0.5) o.x = 1.0 - o.x;
|
||||
if (matParams.misc.z > 0.5) o.y = 1.0 - o.y;
|
||||
return o;
|
||||
}
|
||||
|
||||
// CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
|
||||
@@ -151,6 +157,9 @@ vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
|
||||
void main() {
|
||||
vec4 albedoTex = texture(albedoMap, ApplyUv(0, fragUV));
|
||||
vec3 albedo = albedoTex.rgb * matParams.baseColor.rgb;
|
||||
// albedo 贴图 alpha 作细节叠加遮罩(与 simple_shader 同款,0 = 恒等)
|
||||
albedo = mix(albedo, matParams.alphaOverlay.rgb,
|
||||
albedoTex.a * matParams.alphaOverlay.a);
|
||||
vec4 mr = texture(metallicRoughnessMap, ApplyUv(2, fragUV));
|
||||
// 粗糙度源按 factors.z 切换:glTF 合图工作流读 MR.g;
|
||||
// FBX/OBJ specular 工作流(z=1)换用独立粗糙度图 R 通道
|
||||
@@ -159,7 +168,8 @@ void main() {
|
||||
0.045, 1.0);
|
||||
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
|
||||
// 自发光强度(KHR_materials_emissive_strength,缺省 1)
|
||||
vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb * matParams.surface.z;
|
||||
vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb
|
||||
* matParams.emissiveFactor.rgb * matParams.surface.z;
|
||||
|
||||
// glTF 表面扩展:透射率(KHR_materials_transmission)/
|
||||
// 折射率(ior,缺省 1.5)/ 体积厚度(KHR_materials_volume)
|
||||
|
||||
Reference in New Issue
Block a user