阶段性合并:动画重定向、碎裂共享库、第三人称玩法、编辑器面板与 vendored ufbx
- 动画:HumanoidRig 三层架构(standard 标准槽 / dialects 方言映射 / mapper 配对), 指/趾统一走世界增量两遍式烘焙;AnimationAttach 拆为纯算法 + facade;修正 HumanoidBasis 行走基朝向与跨骨架去重、根运动通道 - 物理:CharacterController 角色互碰撞 / shapeOffset / 接触事件;PhysicsWorld 接触合成与 字符冲量判定 - 渲染与材质:Model 多材质按材质拆子 mesh 与贴图路径解析、DDS 直传、GLB 内嵌纹理落盘、 不透明通道 alpha 固定 1.0、glTF FlipUVs 对齐;SceneSerializer 支持 meshIndex 与材质覆盖 - 脚本:FractureLib 抽为共享库(HostView 桥接)、WallFracture 参数直绑、新增 ThirdPersonCamera / Follower / WASDMove 玩法;移除 GameDirector / PlayerController / Target - 编辑器:新增 GamepadPanel、LogPanel(LogRing),改进 Inspector / 材质 / 统计 / 视口面板 - 依赖:vendored ufbx(FBX 5.x/6.x/7.x 全版本解析,替代 assimp 对 FBX 6.1 的拒读)
This commit is contained in:
@@ -19,6 +19,8 @@
|
|||||||
#include "panels/ControllerEditorPanel.h"
|
#include "panels/ControllerEditorPanel.h"
|
||||||
#include "panels/ViewportPanel.h"
|
#include "panels/ViewportPanel.h"
|
||||||
#include "panels/StatsPanel.h"
|
#include "panels/StatsPanel.h"
|
||||||
|
#include "panels/LogPanel.h"
|
||||||
|
#include "panels/GamepadPanel.h"
|
||||||
|
|
||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
#include <Engine/Serialization/SceneSerializer.h>
|
#include <Engine/Serialization/SceneSerializer.h>
|
||||||
@@ -60,6 +62,8 @@ void Editor::Init(const EditorContext& ctx)
|
|||||||
m_Inspector = std::make_unique<InspectorPanel>();
|
m_Inspector = std::make_unique<InspectorPanel>();
|
||||||
m_Viewport = std::make_unique<ViewportPanel>();
|
m_Viewport = std::make_unique<ViewportPanel>();
|
||||||
m_Stats = std::make_unique<StatsPanel>();
|
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_AssetBrowser = std::make_unique<AssetBrowserPanel>();
|
||||||
m_MaterialEditor = std::make_unique<MaterialEditorPanel>();
|
m_MaterialEditor = std::make_unique<MaterialEditorPanel>();
|
||||||
m_Ctx.materialEditor = m_MaterialEditor.get();
|
m_Ctx.materialEditor = m_MaterialEditor.get();
|
||||||
@@ -219,6 +223,8 @@ void Editor::Draw()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m_ShowStats) m_Stats->Draw(m_Ctx);
|
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_ShowAssetBrowser) m_AssetBrowser->Draw(m_Ctx);
|
||||||
if (m_ShowMaterialEditor) m_MaterialEditor->Draw(m_Ctx);
|
if (m_ShowMaterialEditor) m_MaterialEditor->Draw(m_Ctx);
|
||||||
m_ControllerEditor->Draw(m_Ctx); // 面板内部 m_Open 门控
|
m_ControllerEditor->Draw(m_Ctx); // 面板内部 m_Open 门控
|
||||||
@@ -236,6 +242,11 @@ bool Editor::BlockingKeyboard() const
|
|||||||
const ImGuiIO& io = ImGui::GetIO();
|
const ImGuiIO& io = ImGui::GetIO();
|
||||||
if (io.WantTextInput) return true; // 打字中(改名等)不让位快捷键
|
if (io.WantTextInput) return true; // 打字中(改名等)不让位快捷键
|
||||||
|
|
||||||
|
// PIE 运行态:键盘全给游戏——编辑热键/Gizmo 在运行态本就让位
|
||||||
|
//(ViewportPanel !playing 分支),不再因视口失焦拦截。否则点
|
||||||
|
// Play 后焦点停在工具栏,WASD 整局全死(玩家输入无响应的根因)
|
||||||
|
if (IsPlaying()) return false;
|
||||||
|
|
||||||
// 游戏形态(Fullscreen):WASD 全窗口生效,键盘只让位文本输入
|
// 游戏形态(Fullscreen):WASD 全窗口生效,键盘只让位文本输入
|
||||||
if (m_Ctx.renderer &&
|
if (m_Ctx.renderer &&
|
||||||
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
|
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
|
||||||
@@ -298,11 +309,18 @@ void Editor::DrawMenuBar()
|
|||||||
ImGui::MenuItem("Details", nullptr, &m_ShowInspector);
|
ImGui::MenuItem("Details", nullptr, &m_ShowInspector);
|
||||||
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
|
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
|
||||||
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
|
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
|
||||||
|
ImGui::MenuItem("Log", nullptr, &m_ShowLog);
|
||||||
ImGui::MenuItem("Content Browser", nullptr, &m_ShowAssetBrowser);
|
ImGui::MenuItem("Content Browser", nullptr, &m_ShowAssetBrowser);
|
||||||
ImGui::MenuItem("Material Editor", nullptr, &m_ShowMaterialEditor);
|
ImGui::MenuItem("Material Editor", nullptr, &m_ShowMaterialEditor);
|
||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu("Tools"))
|
||||||
|
{
|
||||||
|
ImGui::MenuItem("Gamepad Test", nullptr, &m_Gamepad->m_Open);
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::EndMenuBar();
|
ImGui::EndMenuBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,6 +446,7 @@ void Editor::DrawDockSpace()
|
|||||||
ImGui::DockBuilderDockWindow("Outliner", dockLeft);
|
ImGui::DockBuilderDockWindow("Outliner", dockLeft);
|
||||||
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
|
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
|
||||||
ImGui::DockBuilderDockWindow("Stats", dockCenter); // Viewport 旁 tab
|
ImGui::DockBuilderDockWindow("Stats", dockCenter); // Viewport 旁 tab
|
||||||
|
ImGui::DockBuilderDockWindow("Log", dockBottom); // Content Browser 旁 tab
|
||||||
ImGui::DockBuilderDockWindow("Content Browser", dockBottom);
|
ImGui::DockBuilderDockWindow("Content Browser", dockBottom);
|
||||||
ImGui::DockBuilderDockWindow("Details", dockRight);
|
ImGui::DockBuilderDockWindow("Details", dockRight);
|
||||||
ImGui::DockBuilderDockWindow("Material Editor", dockRight); // Details 旁 tab
|
ImGui::DockBuilderDockWindow("Material Editor", dockRight); // Details 旁 tab
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class HierarchyPanel;
|
|||||||
class InspectorPanel;
|
class InspectorPanel;
|
||||||
class ViewportPanel;
|
class ViewportPanel;
|
||||||
class StatsPanel;
|
class StatsPanel;
|
||||||
|
class LogPanel;
|
||||||
|
class GamepadPanel;
|
||||||
class AssetBrowserPanel;
|
class AssetBrowserPanel;
|
||||||
class MaterialEditorPanel;
|
class MaterialEditorPanel;
|
||||||
class ControllerEditorPanel;
|
class ControllerEditorPanel;
|
||||||
@@ -126,6 +128,8 @@ private:
|
|||||||
std::unique_ptr<InspectorPanel> m_Inspector;
|
std::unique_ptr<InspectorPanel> m_Inspector;
|
||||||
std::unique_ptr<ViewportPanel> m_Viewport;
|
std::unique_ptr<ViewportPanel> m_Viewport;
|
||||||
std::unique_ptr<StatsPanel> m_Stats;
|
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<AssetBrowserPanel> m_AssetBrowser;
|
||||||
std::unique_ptr<MaterialEditorPanel> m_MaterialEditor;
|
std::unique_ptr<MaterialEditorPanel> m_MaterialEditor;
|
||||||
std::unique_ptr<ControllerEditorPanel> m_ControllerEditor;
|
std::unique_ptr<ControllerEditorPanel> m_ControllerEditor;
|
||||||
@@ -137,6 +141,7 @@ private:
|
|||||||
bool m_ShowInspector = true;
|
bool m_ShowInspector = true;
|
||||||
bool m_ShowViewport = true;
|
bool m_ShowViewport = true;
|
||||||
bool m_ShowStats = true;
|
bool m_ShowStats = true;
|
||||||
|
bool m_ShowLog = true;
|
||||||
bool m_ShowAssetBrowser = true;
|
bool m_ShowAssetBrowser = true;
|
||||||
bool m_ShowMaterialEditor = true;
|
bool m_ShowMaterialEditor = true;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -198,6 +198,12 @@ namespace
|
|||||||
|
|
||||||
for (u32 i = 0; i < meshes.size(); ++i)
|
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));
|
ImGui::PushID(static_cast<int>(i));
|
||||||
|
|
||||||
const std::string meshName =
|
const std::string meshName =
|
||||||
@@ -689,13 +695,15 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
ImGui::RadioButton("Plane", &shape, 1);
|
ImGui::RadioButton("Plane", &shape, 1);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::RadioButton("Trans.Plane", &shape, 2);
|
ImGui::RadioButton("Trans.Plane", &shape, 2);
|
||||||
ImGui::SetNextItemWidth(-1.0f);
|
// 固定宽度而非 -1:弹窗是 AlwaysAutoResize,-1(撑满可用
|
||||||
|
// 宽度)会拿上一帧宽度当基准 → 逐帧膨胀直到撑满屏幕
|
||||||
|
ImGui::SetNextItemWidth(180.0f);
|
||||||
ImGui::SliderFloat("half size", &half, 0.01f, 10.0f,
|
ImGui::SliderFloat("half size", &half, 0.01f, 10.0f,
|
||||||
"%.2f");
|
"%.2f");
|
||||||
ImGui::ColorEdit3("color", col);
|
ImGui::ColorEdit3("color", col);
|
||||||
if (shape == 2)
|
if (shape == 2)
|
||||||
{
|
{
|
||||||
ImGui::SetNextItemWidth(-1.0f);
|
ImGui::SetNextItemWidth(180.0f);
|
||||||
ImGui::SliderFloat("alpha", &alpha, 0.0f, 1.0f,
|
ImGui::SliderFloat("alpha", &alpha, 0.0f, 1.0f,
|
||||||
"%.2f");
|
"%.2f");
|
||||||
}
|
}
|
||||||
@@ -1271,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("Radius", &cc->radius, 0.02f, 0.05f, 5.0f);
|
||||||
ImGui::DragFloat("Half Height", &cc->capsuleHalfHeight, 0.02f, 0.0f, 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");
|
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:扫掠运动学参数 ----
|
// ---- Movement:扫掠运动学参数 ----
|
||||||
ImGui::SeparatorText("Movement");
|
ImGui::SeparatorText("Movement");
|
||||||
ImGui::DragFloat("Max Slope (deg)", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f);
|
ImGui::DragFloat("Max Slope (deg)", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f);
|
||||||
@@ -1901,11 +1911,31 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
// 警告跳过)----
|
// 警告跳过)----
|
||||||
ImGui::SeparatorText("Clips");
|
ImGui::SeparatorText("Clips");
|
||||||
const auto& files = AssetModelFiles();
|
const auto& files = AssetModelFiles();
|
||||||
std::vector<const char*> opts;
|
// 显示真名(AssetDisplayName:文件 stem,导入资产库的
|
||||||
opts.push_back("<attach animation file...>");
|
// 哈希 "asset" 读同目录 pathname 取原始名);重名者补
|
||||||
|
// 父目录区分——不再把哈希路径直接摆给用户(= 显示成 id)
|
||||||
|
std::vector<std::string> labels;
|
||||||
|
labels.reserve(files.size());
|
||||||
for (const auto& f : files)
|
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;
|
int sel = 0;
|
||||||
ImGui::SetNextItemWidth(-1.0f);
|
ImGui::SetNextItemWidth(-1.0f);
|
||||||
@@ -1925,7 +1955,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
}
|
}
|
||||||
// attach 遥测(骨骼匹配结果,面板下方显示)
|
// attach 遥测(骨骼匹配结果,面板下方显示)
|
||||||
s_LastAttachFile =
|
s_LastAttachFile =
|
||||||
std::filesystem::path(path).filename().string();
|
Engine::Model::AssetDisplayName(path);
|
||||||
s_LastAttachMatched =
|
s_LastAttachMatched =
|
||||||
res.channelsMatched + res.channelsRetargeted;
|
res.channelsMatched + res.channelsRetargeted;
|
||||||
s_LastAttachRetargeted = res.channelsRetargeted;
|
s_LastAttachRetargeted = res.channelsRetargeted;
|
||||||
@@ -1938,9 +1968,8 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
for (size_t i = 0; i < anim->attachedAnimPaths.size(); ++i)
|
for (size_t i = 0; i < anim->attachedAnimPaths.size(); ++i)
|
||||||
{
|
{
|
||||||
const auto& p = anim->attachedAnimPaths[i];
|
const auto& p = anim->attachedAnimPaths[i];
|
||||||
ImGui::BulletText("%s", std::filesystem::path(p)
|
ImGui::BulletText("%s",
|
||||||
.filename()
|
Engine::Model::AssetDisplayName(p)
|
||||||
.string()
|
|
||||||
.c_str());
|
.c_str());
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::PushID(static_cast<int>(i));
|
ImGui::PushID(static_cast<int>(i));
|
||||||
|
|||||||
@@ -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,6 +173,9 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
|||||||
EditParam(
|
EditParam(
|
||||||
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f));
|
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f));
|
||||||
EditParam(ImGui::ColorEdit4("Base Color", &p.baseColor.x));
|
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("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f));
|
||||||
// 发光 = emissiveFactor × emissive 贴图 × Strength(Emissive 槽
|
// 发光 = emissiveFactor × emissive 贴图 × Strength(Emissive 槽
|
||||||
// 兜底贴图恒黑,无贴图时颜色因子是唯一入口)
|
// 兜底贴图恒黑,无贴图时颜色因子是唯一入口)
|
||||||
@@ -192,6 +195,23 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
|||||||
ImGui::TextDisabled("Transmission/Thickness need Transparent");
|
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 重建)----
|
// ---- 纹理槽(变化 → 落盘 + waitIdle 重建)----
|
||||||
ImGui::SeparatorText("Textures");
|
ImGui::SeparatorText("Textures");
|
||||||
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
|
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
|
||||||
@@ -216,6 +236,13 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
|||||||
}
|
}
|
||||||
if (ImGui::BeginPopup("tex_pick"))
|
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())
|
for (const auto& f : EditorMaterialUtil::AssetTextureFiles())
|
||||||
{
|
{
|
||||||
if (ImGui::Selectable(f.c_str()))
|
if (ImGui::Selectable(f.c_str()))
|
||||||
@@ -313,6 +340,7 @@ void MaterialEditorPanel::DrawBuiltinMode(const EditorContext& ctx)
|
|||||||
ImGui::DragFloat("Metallic", &p.factors.x, 0.01f, 0.0f, 1.0f);
|
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::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f);
|
||||||
ImGui::ColorEdit4("Base Color", &p.baseColor.x);
|
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("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f);
|
||||||
ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x);
|
ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x);
|
||||||
ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f, 0.0f, 100.0f);
|
ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f, 0.0f, 100.0f);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "Engine/ECS/Components.h"
|
#include "Engine/ECS/Components.h"
|
||||||
#include "Engine/Physics/PhysicsWorld.h"
|
#include "Engine/Physics/PhysicsWorld.h"
|
||||||
#include "Engine/Core/Time.h"
|
#include "Engine/Core/Time.h"
|
||||||
|
#include "Engine/Core/Stats.h"
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <glm/gtc/type_ptr.hpp>
|
#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]);
|
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();
|
ImGui::End();
|
||||||
|
|||||||
@@ -234,6 +234,14 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
|||||||
// PIE 运行态:输入归游戏(脚本读 GLFW 轮询),Gizmo/编辑器
|
// PIE 运行态:输入归游戏(脚本读 GLFW 轮询),Gizmo/编辑器
|
||||||
// 热键全部让位(UE Play 语义:Scene view 编辑操作冻结)
|
// 热键全部让位(UE Play 语义:Scene view 编辑操作冻结)
|
||||||
const bool playing = ctx.playState && *ctx.playState;
|
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 &&
|
if (!playing && out.focused && !ImGui::GetIO().WantTextInput &&
|
||||||
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
||||||
// Q = 纯选择模式(UE Q 工具语义):op=0 无任何轴位,
|
// Q = 纯选择模式(UE Q 工具语义):op=0 无任何轴位,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
#include <Engine/Core/Time.h>
|
#include <Engine/Core/Time.h>
|
||||||
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
|
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
|
||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
#include <Engine/Core/Stats.h>
|
|
||||||
#include <Engine/Renderer/Renderer.h>
|
#include <Engine/Renderer/Renderer.h>
|
||||||
#include <Engine/Renderer/EditorCamera.h>
|
#include <Engine/Renderer/EditorCamera.h>
|
||||||
#include <Engine/Serialization/SceneSerializer.h>
|
#include <Engine/Serialization/SceneSerializer.h>
|
||||||
@@ -146,10 +145,6 @@ namespace
|
|||||||
SetImGuiEnabled(!GetImGuiEnabled());
|
SetImGuiEnabled(!GetImGuiEnabled());
|
||||||
}
|
}
|
||||||
#endif
|
#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
|
||||||
+159
-17
@@ -17,8 +17,10 @@
|
|||||||
// 墙脚"的观感。
|
// 墙脚"的观感。
|
||||||
//
|
//
|
||||||
// 用法(宿主 = 挂在可碎实体上的脚本):
|
// 用法(宿主 = 挂在可碎实体上的脚本):
|
||||||
// Fracture::Params p_; // DeclareParams 直绑成员(pb.Field)
|
|
||||||
// Fracture::Burst burst_; // 碎片簿记 + 生命周期
|
// Fracture::Burst burst_; // 碎片簿记 + 生命周期
|
||||||
|
// DeclareParams 直绑 burst_.p 各字段(pb.Field("Count",
|
||||||
|
// &burst_.p.count, ...))——参数必须与碎裂逻辑读的是同一对象,
|
||||||
|
// 绑到独立副本会让 Count/Lifetime 等全部失效
|
||||||
// HostView HV() { // 桥接(脚本自身成员函数内可访问
|
// HostView HV() { // 桥接(脚本自身成员函数内可访问
|
||||||
// return {App(), EntityBits(),// protected 基类设施,库内只走公共 API)
|
// return {App(), EntityBits(),// protected 基类设施,库内只走公共 API)
|
||||||
// [this](auto& n) { return Spawn(n); },
|
// [this](auto& n) { return Spawn(n); },
|
||||||
@@ -27,8 +29,9 @@
|
|||||||
// OnCollisionEnter → burst_.TryFire(HV(), other, point, normal)
|
// OnCollisionEnter → burst_.TryFire(HV(), other, point, normal)
|
||||||
// OnUpdate → burst_.Tick(HV(), dt)
|
// OnUpdate → burst_.Tick(HV(), dt)
|
||||||
// OnDestroy → burst_.Shutdown(HV())
|
// OnDestroy → burst_.Shutdown(HV())
|
||||||
// 外观:Fire 前改 burst_.look(默认砖色不透明;玻璃靶 = 半透明
|
// 外观:默认继承宿主(墙)的生效材质(底色/贴图/透明),碎块与墙面
|
||||||
// + 自发光)
|
// 一致;要独立外观(玻璃靶 = 半透明 + 自发光)Fire 前置
|
||||||
|
// burst_.look.inheritFromHost = false 再填 baseColor/emissive
|
||||||
// HostView 每次调用现填(不存指针:组件存储重排/实例重建均安全)
|
// HostView 每次调用现填(不存指针:组件存储重排/实例重建均安全)
|
||||||
//
|
//
|
||||||
// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按宿主世界
|
// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按宿主世界
|
||||||
@@ -148,23 +151,32 @@ namespace Engine::Fracture {
|
|||||||
std::vector<glm::vec3> cpuPositions;
|
std::vector<glm::vec3> cpuPositions;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline PrismMesh CellToPrism(const std::vector<glm::vec2>& cell, f32 hz) {
|
// 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;
|
PrismMesh m;
|
||||||
const size_t n = cell.size();
|
const size_t n = cell.size();
|
||||||
const size_t topBase = 0;
|
const size_t topBase = 0;
|
||||||
const size_t botBase = n;
|
const size_t botBase = n;
|
||||||
m.vertices.reserve(n * 2);
|
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) {
|
for (const glm::vec2& p : cell) {
|
||||||
const Engine::Vertex v{
|
const Engine::Vertex v{
|
||||||
{p.x, p.y, hz}, {0.0f, 0.0f, 1.0f},
|
{p.x, p.y, hz}, {0.0f, 0.0f, 1.0f},
|
||||||
{p.x, p.y}, {1.0f, 0.0f, 0.0f}};
|
uvOf(p), {1.0f, 0.0f, 0.0f}};
|
||||||
m.vertices.push_back(v);
|
m.vertices.push_back(v);
|
||||||
}
|
}
|
||||||
for (const glm::vec2& p : cell) {
|
for (const glm::vec2& p : cell) {
|
||||||
const Engine::Vertex v{
|
const Engine::Vertex v{
|
||||||
{p.x, p.y, -hz}, {0.0f, 0.0f, -1.0f},
|
{p.x, p.y, -hz}, {0.0f, 0.0f, -1.0f},
|
||||||
{p.x, p.y}, {1.0f, 0.0f, 0.0f}};
|
uvOf(p), {1.0f, 0.0f, 0.0f}};
|
||||||
m.vertices.push_back(v);
|
m.vertices.push_back(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +228,47 @@ namespace Engine::Fracture {
|
|||||||
return m;
|
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 控件)----
|
// ---- 参数(原 WallFracture 全量参数,字段类型随 Inspector 控件)----
|
||||||
struct Params {
|
struct Params {
|
||||||
i32 count = 30; // Voronoi 种子数 = 碎片数
|
i32 count = 30; // Voronoi 种子数 = 碎片数
|
||||||
@@ -230,9 +283,15 @@ namespace Engine::Fracture {
|
|||||||
f32 minImpactSpeed = 2.0f; // 触发阈值(m/s)
|
f32 minImpactSpeed = 2.0f; // 触发阈值(m/s)
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 碎片外观(Fire 时套用到全部碎片网格;默认砖色不透明 =
|
// ---- 碎片外观 ----
|
||||||
// 原 WallFracture 观感)----
|
// 默认 inheritFromHost = true:碎块继承宿主(墙)的"生效材质"——
|
||||||
|
// 底色/贴图/透明/自发光全套沿用,碎掉后与墙面目视一致。宿主材质
|
||||||
|
// 取实体级覆盖(.material 资产)优先、模型内建材质兜底,与
|
||||||
|
// SceneRenderer 收集期的 resolve 同序
|
||||||
|
// 需要独立外观(玻璃靶 = 半透明 + 自发光等)时置 false,改用下面
|
||||||
|
// 的显式字段(Fire 前设置)
|
||||||
struct Appearance {
|
struct Appearance {
|
||||||
|
bool inheritFromHost = true; // true = 材质沿用宿主,忽略下列字段
|
||||||
glm::vec4 baseColor{140.0f / 255.0f, 92.0f / 255.0f,
|
glm::vec4 baseColor{140.0f / 255.0f, 92.0f / 255.0f,
|
||||||
72.0f / 255.0f, 1.0f};
|
72.0f / 255.0f, 1.0f};
|
||||||
bool transparent = false; // 透明绘制队列(玻璃碎片)
|
bool transparent = false; // 透明绘制队列(玻璃碎片)
|
||||||
@@ -254,20 +313,36 @@ namespace Engine::Fracture {
|
|||||||
const glm::vec3& normal) {
|
const glm::vec3& normal) {
|
||||||
if (m_Fired) return false;
|
if (m_Fired) return false;
|
||||||
|
|
||||||
// 对方须是活的 Dynamic 刚体且速度够快(过滤静止贴靠/
|
// 对方须"够快"且是可动的撞击来源,二选一:
|
||||||
// 角色轻碰——子弹/抛体必中)
|
// - 动态刚体(子弹/抛体):用实时线速度
|
||||||
|
// - 角色(CharacterVirtual,非刚体、无 RigidBodyComponent):
|
||||||
|
// 用"扫掠前"速度——扫掠中朝墙面的分量会被 Jolt 抵消,
|
||||||
|
// 实时速度往往已归零,会漏判(见
|
||||||
|
// PhysicsWorld::GetCharacterImpactVelocity)
|
||||||
|
// 速度低于阈值(静止贴靠/轻碰)则忽略
|
||||||
auto& registry = host.app.GetScene().GetRegistry();
|
auto& registry = host.app.GetScene().GetRegistry();
|
||||||
const ECS::Entity otherE = EntityFromBits(other);
|
const ECS::Entity otherE = EntityFromBits(other);
|
||||||
if (!registry.valid(otherE)) return false;
|
if (!registry.valid(otherE)) return false;
|
||||||
const auto* otherRb =
|
f32 speed = 0.0f;
|
||||||
registry.try_get<Engine::ECS::RigidBodyComponent>(otherE);
|
if (const auto* otherRb =
|
||||||
if (!otherRb ||
|
registry.try_get<Engine::ECS::RigidBodyComponent>(
|
||||||
otherRb->type !=
|
otherE)) {
|
||||||
|
if (otherRb->type !=
|
||||||
Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
|
Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
|
||||||
return false;
|
return false; // 静态/运动学体不算撞击
|
||||||
}
|
}
|
||||||
const f32 speed = glm::length(
|
speed = glm::length(
|
||||||
host.app.GetPhysics().GetLinearVelocity(other));
|
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;
|
if (speed < p.minImpactSpeed) return false;
|
||||||
|
|
||||||
m_Fired = true;
|
m_Fired = true;
|
||||||
@@ -335,6 +410,52 @@ namespace Engine::Fracture {
|
|||||||
const f32 hh = 0.5f * wallSize.y;
|
const f32 hh = 0.5f * wallSize.y;
|
||||||
const f32 hz = 0.5f * wallSize.z;
|
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 即裂纹平面坐标
|
// 命中点 → 宿主局部(逆旋转 + 平移),XY 即裂纹平面坐标
|
||||||
const glm::quat invRot = glm::conjugate(
|
const glm::quat invRot = glm::conjugate(
|
||||||
wall.GetWorldRotationQuat());
|
wall.GetWorldRotationQuat());
|
||||||
@@ -391,7 +512,7 @@ namespace Engine::Fracture {
|
|||||||
paths.emplace_back();
|
paths.emplace_back();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
PrismMesh pm = CellToPrism(cell, hz);
|
PrismMesh pm = CellToPrism(cell, hz, hw, hh);
|
||||||
glm::vec2 c2{0.0f};
|
glm::vec2 c2{0.0f};
|
||||||
for (const glm::vec2& q : cell) c2 += q;
|
for (const glm::vec2& q : cell) c2 += q;
|
||||||
c2 /= static_cast<f32>(cell.size());
|
c2 /= static_cast<f32>(cell.size());
|
||||||
@@ -407,11 +528,27 @@ namespace Engine::Fracture {
|
|||||||
mesh.lods.push_back(std::move(pm.indices));
|
mesh.lods.push_back(std::move(pm.indices));
|
||||||
mesh.cpuPositions = std::move(pm.cpuPositions);
|
mesh.cpuPositions = std::move(pm.cpuPositions);
|
||||||
mesh.name = "frag" + std::to_string(s);
|
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.params.baseColor = look.baseColor;
|
||||||
mesh.transparent = look.transparent;
|
mesh.transparent = look.transparent;
|
||||||
mesh.params.emissiveFactor =
|
mesh.params.emissiveFactor =
|
||||||
glm::vec4{look.emissive, 1.0f};
|
glm::vec4{look.emissive, 1.0f};
|
||||||
mesh.params.surface.z = look.emissiveStrength;
|
mesh.params.surface.z = look.emissiveStrength;
|
||||||
|
}
|
||||||
|
|
||||||
const Engine::ModelRef ref =
|
const Engine::ModelRef ref =
|
||||||
assets.LoadModel(parsed.path, std::move(parsed));
|
assets.LoadModel(parsed.path, std::move(parsed));
|
||||||
@@ -436,6 +573,11 @@ namespace Engine::Fracture {
|
|||||||
frag.AddComponent<
|
frag.AddComponent<
|
||||||
Engine::ECS::MeshRendererComponent>();
|
Engine::ECS::MeshRendererComponent>();
|
||||||
fmr.model = refs[s];
|
fmr.model = refs[s];
|
||||||
|
// 宿主墙用 .material 覆盖时,碎块直接复用同一份资产:外观
|
||||||
|
//(含透明通道)与墙完全一致,且不需要引擎改判定
|
||||||
|
if (!hostOverridePath.empty()) {
|
||||||
|
fmr.materialOverrides[0] = hostOverridePath;
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamic MeshCollider → ConvexHull(数据源 = 刚上传
|
// Dynamic MeshCollider → ConvexHull(数据源 = 刚上传
|
||||||
// 的 cpuPositions;Box 字段不参与 Mesh 形状)
|
// 的 cpuPositions;Box 字段不参与 Mesh 形状)
|
||||||
|
|||||||
@@ -1,391 +0,0 @@
|
|||||||
//
|
|
||||||
// GameDirector.cpp:60 秒限时打靶场导演(玩法原型验证侧线)。
|
|
||||||
//
|
|
||||||
// 完整玩法循环(全现有能力,零引擎改动):
|
|
||||||
// 开局 → 程序化搭玻璃靶阵(行对齐网格,居中逐行收缩)→ 60 秒
|
|
||||||
// 倒计时 → 射击碎靶计分(Target 报分,跨脚本参数通道)→ 时间到
|
|
||||||
// 收靶结算 → R 重开(任意时刻可重置)
|
|
||||||
//
|
|
||||||
// 计分协议:Score/AcceptHits = 导演的声明参数——Target 经
|
|
||||||
// ScriptRuntime::GetParam/SetParam 跨实体读写(参数层即按名消息,
|
|
||||||
// 与序列化同一条路)。Score 写穿 Item.params,PIE Inspector 实时
|
|
||||||
// 反映;AcceptHits=false 后 Target 停止报分(双保险,收靶为主)。
|
|
||||||
//
|
|
||||||
// HUD:引擎暂无文字渲染(缺失能力,已记录),倒计时/得分用
|
|
||||||
// Debug 图元在世界空间凑——顶部倒计时条(绿→黄→红随剩余时间)
|
|
||||||
// + 条下一排金色得分点(1 分 = 1 点)。挂在靶阵上方,depthTest
|
|
||||||
// 关闭保证穿墙可见。
|
|
||||||
//
|
|
||||||
// 靶子模型:程序化玻璃立方(ParsedModel 手工装配,半透明 + 自发
|
|
||||||
// 光),缓存键 = 颜色/尺寸参数量化——同配置共享一份 GPU 模型,
|
|
||||||
// 跨回合复用;改参数重开局才重建。碎片几何由 FractureLib 每靶
|
|
||||||
// 现场生成(唯一代键,自管回收)。
|
|
||||||
//
|
|
||||||
// 【演示步骤】任意实体挂 GameDirector;场景 primary 相机实体
|
|
||||||
// (CameraComponent.primary=true,PIE 渲染源)挂 PlayerController +
|
|
||||||
// TestShoot,摆到面向靶阵(如原点朝 -Z 正对 Center 默认位)→ Play:
|
|
||||||
// 光标自动捕捉(Esc 释放、点视口重捕),WASD/Space/Ctrl 移动 +
|
|
||||||
// 鼠标转向,左键射击:靶子玻璃式碎裂 + 得分点亮起,倒计时条流逝,
|
|
||||||
// 时间到结算(控制台输出得分),R 重开。
|
|
||||||
//
|
|
||||||
// 【缺失能力记录(玩法侧线的下一批依据)】
|
|
||||||
// - UI/HUD 文字(倒计时数字/得分数字只得用图元条凑)
|
|
||||||
// - 音效(碎裂/射击/倒计时提示全程无声)
|
|
||||||
// - 准星(靠弹道调试十字凑合)
|
|
||||||
// - 游戏状态机/关卡流(重开 = 脚本内手搓,无引擎级 GameMode)
|
|
||||||
//
|
|
||||||
|
|
||||||
#include <Engine/Scripting/Script.h>
|
|
||||||
#include <Engine/Core/Application.h>
|
|
||||||
#include <Engine/Core/Input.h>
|
|
||||||
#include <Engine/Core/Log.h>
|
|
||||||
#include <Engine/Core/Types.h>
|
|
||||||
#include <Engine/Debug/DebugDraw.h>
|
|
||||||
#include <Engine/Assets/AssetManager.h>
|
|
||||||
#include <Engine/Renderer/Model.h>
|
|
||||||
|
|
||||||
#include "FractureLib.h" // BitsOf/EntityFromBits(碎片簿记同款换算)
|
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using namespace Engine; // Input/Debug 门面 + 基础类型
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// ---- 玻璃立方 ParsedModel 装配(半透明 + 自发光;6 面 quad
|
|
||||||
// 手工展开,外向 CCW:u × v = n 定切向基)----
|
|
||||||
Engine::ParsedModel BuildGlassCube(const glm::vec3& he,
|
|
||||||
const glm::vec3& color,
|
|
||||||
const std::string& path) {
|
|
||||||
Engine::ParsedModel parsed;
|
|
||||||
parsed.path = path;
|
|
||||||
auto& mesh = parsed.meshes.emplace_back();
|
|
||||||
|
|
||||||
// 面:法线 n + 切向 u/v(u×v=n 保证外向 CCW 索引序)
|
|
||||||
static constexpr glm::vec3 kN[6] = {
|
|
||||||
{1, 0, 0}, {-1, 0, 0}, {0, 1, 0},
|
|
||||||
{0, -1, 0}, {0, 0, 1}, {0, 0, -1}};
|
|
||||||
static constexpr glm::vec3 kU[6] = {
|
|
||||||
{0, 0, -1}, {0, 0, 1}, {1, 0, 0},
|
|
||||||
{1, 0, 0}, {1, 0, 0}, {1, 0, 0}};
|
|
||||||
static constexpr glm::vec3 kV[6] = {
|
|
||||||
{0, 1, 0}, {0, 1, 0}, {0, 0, -1},
|
|
||||||
{0, 0, 1}, {0, 1, 0}, {0, 1, 0}};
|
|
||||||
|
|
||||||
// 轴向半长:轴 a 上的偏移 = a ∘ he(分量积;单位轴即取该轴
|
|
||||||
// 分量),面心 = n ∘ he
|
|
||||||
auto along = [&](const glm::vec3& a) {
|
|
||||||
return glm::vec3{a.x * he.x, a.y * he.y, a.z * he.z};
|
|
||||||
};
|
|
||||||
|
|
||||||
mesh.vertices.reserve(24);
|
|
||||||
for (int f = 0; f < 6; ++f) {
|
|
||||||
const u32 base = static_cast<u32>(mesh.vertices.size());
|
|
||||||
const glm::vec3 center = along(kN[f]);
|
|
||||||
// quad 角序:(−u,−v) (+u,−v) (+u,+v) (−u,+v)——CCW
|
|
||||||
for (int c = 0; c < 4; ++c) {
|
|
||||||
const f32 su = (c == 1 || c == 2) ? 1.0f : -1.0f;
|
|
||||||
const f32 sv = (c >= 2) ? 1.0f : -1.0f;
|
|
||||||
const glm::vec3 pos = center + su * along(kU[f]) +
|
|
||||||
sv * along(kV[f]);
|
|
||||||
const Engine::Vertex v{
|
|
||||||
pos, kN[f],
|
|
||||||
{(su + 1.0f) * 0.5f, (sv + 1.0f) * 0.5f}, kU[f]};
|
|
||||||
mesh.vertices.push_back(v);
|
|
||||||
}
|
|
||||||
mesh.lods.emplace_back();
|
|
||||||
auto& idx = mesh.lods.back();
|
|
||||||
idx.insert(idx.end(),
|
|
||||||
{base, base + 1, base + 2,
|
|
||||||
base, base + 2, base + 3});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 玻璃材质:低金属度高光洁 + alpha 混合 + 靶色自发光
|
|
||||||
// (alpha 0.85、自发光 3——曾用 alpha 0.42 近乎全透,亮背景
|
|
||||||
// 下靶子隐形;拉高保靶子在浅色环境里醒目)
|
|
||||||
mesh.name = "target";
|
|
||||||
mesh.params.factors = {0.05f, 0.15f, 0.0f, 0.0f};
|
|
||||||
mesh.params.baseColor = glm::vec4{color, 0.85f};
|
|
||||||
mesh.params.surface = {1.5f, 0.0f, 3.0f, 0.0f};
|
|
||||||
mesh.params.emissiveFactor = glm::vec4{color * 0.85f, 1.0f};
|
|
||||||
mesh.transparent = true;
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 打靶场导演 ----
|
|
||||||
class GameDirector final : public Engine::Scripting::ScriptBehavior {
|
|
||||||
public:
|
|
||||||
const char* Name() const override { return "GameDirector"; }
|
|
||||||
|
|
||||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
|
||||||
// ---- 回合规则 ----
|
|
||||||
pb.Field("RoundTime", &m_RoundTime, 60.0f); // 局时长(秒)
|
|
||||||
pb.Field("TargetCount", &m_TargetCount, 5); // 靶数
|
|
||||||
pb.Field("Score", &m_Score, 0); // 得分(Target 报分写这里)
|
|
||||||
pb.Field("AcceptHits", &m_AcceptHits, true); // 计分门(Target 读)
|
|
||||||
// ---- 靶阵布局(Center = 底行中心)----
|
|
||||||
pb.Field("Center", &m_Center,
|
|
||||||
glm::vec3{0.0f, 1.5f, -6.0f});
|
|
||||||
pb.Field("PerRow", &m_PerRow, 5); // 每行靶数
|
|
||||||
pb.Field("Spacing", &m_Spacing, 1.2f); // 行/列间距 m
|
|
||||||
// ---- 靶子规格 ----
|
|
||||||
pb.Field("TargetSize", &m_TargetSize,
|
|
||||||
glm::vec3{0.7f, 0.7f, 0.2f}); // 单靶尺寸 m(厚 ≥0.2 防高速弹隧穿)
|
|
||||||
pb.Field("TargetColor", &m_TargetColor,
|
|
||||||
glm::vec3{1.0f, 0.3f, 0.25f}); // 玻璃色
|
|
||||||
pb.Field("FragCount", &m_FragCount, 22); // 每靶碎片数(写进 Target)
|
|
||||||
// ---- HUD ----
|
|
||||||
pb.Field("HudHeight", &m_HudHeight, 1.6f); // HUD 高于靶阵中心 m
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnStart() override { StartRound(); }
|
|
||||||
|
|
||||||
void OnUpdate(f32 dt) override {
|
|
||||||
// R 上升沿重开(整局任意时刻可重置)
|
|
||||||
const bool rDown = Input::IsKeyDown(Input::Key::R);
|
|
||||||
const bool restart = rDown && !m_RWasDown;
|
|
||||||
m_RWasDown = rDown;
|
|
||||||
if (restart) {
|
|
||||||
StartRound();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_AcceptHits) {
|
|
||||||
m_TimeLeft -= dt;
|
|
||||||
if (m_TimeLeft <= 0.0f) {
|
|
||||||
m_TimeLeft = 0.0f;
|
|
||||||
EndRound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DrawHud();
|
|
||||||
}
|
|
||||||
|
|
||||||
// PIE stop:玻璃靶模型是本脚本独占资产(参数键缓存),批量
|
|
||||||
// 停飞卸载(同 WallFracture 碎片模型回收先例)
|
|
||||||
void OnDestroy() override {
|
|
||||||
if (!m_ModelKey.empty()) {
|
|
||||||
std::vector<std::string> paths{m_ModelKey};
|
|
||||||
App().GetAssets().UnloadModelsByPaths(paths);
|
|
||||||
m_ModelKey.clear();
|
|
||||||
m_TargetModel = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
// ---- 开局:清场 → 建模型 → spawn 靶阵 → 计时/计分归零 ----
|
|
||||||
void StartRound() {
|
|
||||||
DestroyRemaining();
|
|
||||||
EnsureTargetModel();
|
|
||||||
SpawnTargets();
|
|
||||||
|
|
||||||
m_Score = 0;
|
|
||||||
SetParam("Score", Engine::Scripting::ParamValue{0});
|
|
||||||
m_TimeLeft = m_RoundTime;
|
|
||||||
m_AcceptHits = true;
|
|
||||||
SetParam("AcceptHits", Engine::Scripting::ParamValue{true});
|
|
||||||
ENGINE_INFO("[GameDirector] 回合开始:{} 个靶 / {} 秒",
|
|
||||||
m_TargetCount, m_RoundTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 结算:停计分 → 收掉没打碎的靶 → 报分 ----
|
|
||||||
void EndRound() {
|
|
||||||
m_AcceptHits = false;
|
|
||||||
SetParam("AcceptHits", Engine::Scripting::ParamValue{false});
|
|
||||||
DestroyRemaining();
|
|
||||||
ENGINE_INFO("[GameDirector] 时间到!得分 {}/{}(R 重开)",
|
|
||||||
m_Score, m_TargetCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 靶阵:行对齐网格,每行水平居中,行向上堆 ----
|
|
||||||
void SpawnTargets() {
|
|
||||||
const int count = std::max(1, m_TargetCount);
|
|
||||||
const int perRow = std::max(1, m_PerRow);
|
|
||||||
for (int i = 0; i < count; ++i) {
|
|
||||||
const int row = i / perRow;
|
|
||||||
const int col = i % perRow;
|
|
||||||
const int inRow = std::min(count - row * perRow, perRow);
|
|
||||||
glm::vec3 pos = m_Center;
|
|
||||||
pos.y += static_cast<f32>(row) * m_Spacing;
|
|
||||||
pos.x += static_cast<f32>(col) * m_Spacing -
|
|
||||||
static_cast<f32>(inRow - 1) * m_Spacing * 0.5f;
|
|
||||||
SpawnTarget(pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SpawnTarget(const glm::vec3& pos) {
|
|
||||||
auto t = Spawn("target");
|
|
||||||
auto& tr = t.GetComponent<Engine::ECS::TransformComponent>();
|
|
||||||
tr.SetPosition(pos); // scale 1:尺寸烘焙进模型与碰撞体
|
|
||||||
|
|
||||||
auto& mr =
|
|
||||||
t.AddComponent<Engine::ECS::MeshRendererComponent>();
|
|
||||||
mr.model = m_TargetModel;
|
|
||||||
|
|
||||||
// Static Box:厚 ≥0.2m 时对 25m/s 无 CCD 弹保证检出
|
|
||||||
//(检测带 2*(弹半径0.12+靶半厚0.1)=0.44m > 每步位移0.42m)
|
|
||||||
auto& rb = t.AddComponent<Engine::ECS::RigidBodyComponent>();
|
|
||||||
rb.type = Engine::ECS::RigidBodyComponent::BodyType::Static;
|
|
||||||
rb.collider = Engine::ECS::RigidBodyComponent::ColliderType::Box;
|
|
||||||
rb.boxHalfExtents = m_TargetSize * 0.5f;
|
|
||||||
rb.friction = 0.5f;
|
|
||||||
rb.restitution = 0.1f;
|
|
||||||
|
|
||||||
// Target 脚本项:计分去向 + 靶色/碎片档经参数层传入
|
|
||||||
auto& sc = t.AddComponent<Engine::ECS::ScriptComponent>();
|
|
||||||
Engine::ECS::ScriptComponent::Item item;
|
|
||||||
item.name = "Target";
|
|
||||||
item.params.push_back(
|
|
||||||
{"Director",
|
|
||||||
Engine::Scripting::ParamValue{
|
|
||||||
static_cast<i32>(EntityBits())}});
|
|
||||||
item.params.push_back(
|
|
||||||
{"Color", Engine::Scripting::ParamValue{m_TargetColor}});
|
|
||||||
item.params.push_back(
|
|
||||||
{"Count", Engine::Scripting::ParamValue{m_FragCount}});
|
|
||||||
sc.items.push_back(std::move(item));
|
|
||||||
|
|
||||||
m_TargetBits.push_back(Engine::Fracture::BitsOf(t));
|
|
||||||
}
|
|
||||||
|
|
||||||
void DestroyRemaining() {
|
|
||||||
auto& registry = App().GetScene().GetRegistry();
|
|
||||||
for (const u32 bits : m_TargetBits) {
|
|
||||||
const ECS::Entity e =
|
|
||||||
Engine::Fracture::EntityFromBits(bits);
|
|
||||||
if (registry.valid(e)) {
|
|
||||||
Destroy(e); // 延迟队列,帧尾统一执行
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_TargetBits.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 玻璃靶模型:参数量化缓存键(同配置共享一份 GPU 模型,
|
|
||||||
// 跨回合复用;改色/改尺寸后下次开局重建。注:键按"一个导演
|
|
||||||
// 实例独占"设计,双导演同参数会互踩——原型的合理取舍)----
|
|
||||||
void EnsureTargetModel() {
|
|
||||||
const auto q = [](f32 c) {
|
|
||||||
const int b = static_cast<int>(
|
|
||||||
std::clamp(c, 0.0f, 1.0f) * 255.0f + 0.5f);
|
|
||||||
return std::clamp(b, 0, 255);
|
|
||||||
};
|
|
||||||
// 尺寸量化:分米精度(0.1m 步进;同尺寸档共享模型)
|
|
||||||
const auto qs = [](f32 v) {
|
|
||||||
return std::clamp(
|
|
||||||
static_cast<int>(std::max(v, 0.0f) * 10.0f + 0.5f),
|
|
||||||
0, 999);
|
|
||||||
};
|
|
||||||
char hex[96];
|
|
||||||
std::snprintf(hex, sizeof(hex),
|
|
||||||
"procedural://rangetarget/%02X%02X%02X_"
|
|
||||||
"%d_%d_%d",
|
|
||||||
q(m_TargetColor.r), q(m_TargetColor.g),
|
|
||||||
q(m_TargetColor.b),
|
|
||||||
qs(m_TargetSize.x), qs(m_TargetSize.y),
|
|
||||||
qs(m_TargetSize.z));
|
|
||||||
const std::string key = hex;
|
|
||||||
if (key == m_ModelKey && m_TargetModel.Valid()) return;
|
|
||||||
|
|
||||||
// 配置变了:旧键独占资产先回收
|
|
||||||
if (m_TargetModel.Valid()) {
|
|
||||||
std::vector<std::string> old{m_ModelKey};
|
|
||||||
App().GetAssets().UnloadModelsByPaths(old);
|
|
||||||
m_TargetModel = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto parsed = BuildGlassCube(m_TargetSize * 0.5f,
|
|
||||||
m_TargetColor, key);
|
|
||||||
m_TargetModel = App().GetAssets().LoadModel(
|
|
||||||
key, std::move(parsed));
|
|
||||||
m_ModelKey = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- HUD(图元凑,无文字是已知缺失能力):倒计时条
|
|
||||||
//(绿→黄→红)+ 得分点(1 分 = 1 金点);结算态整条红 ----
|
|
||||||
void DrawHud() {
|
|
||||||
const glm::vec3 base =
|
|
||||||
m_Center + glm::vec3{0.0f, m_HudHeight, 0.0f};
|
|
||||||
constexpr f32 kBarW = 3.0f;
|
|
||||||
const f32 left = base.x - kBarW * 0.5f;
|
|
||||||
const glm::vec3 z{0.0f, 0.0f, base.z};
|
|
||||||
|
|
||||||
const f32 frac = m_AcceptHits
|
|
||||||
? std::clamp(m_TimeLeft / std::max(m_RoundTime, 0.01f),
|
|
||||||
0.0f, 1.0f)
|
|
||||||
: 0.0f;
|
|
||||||
// 颜色:>50% 绿→黄,≤50% 黄→红
|
|
||||||
const glm::vec4 green{0.24f, 0.86f, 0.52f, 1.0f};
|
|
||||||
const glm::vec4 yellow{1.0f, 0.84f, 0.31f, 1.0f};
|
|
||||||
const glm::vec4 red{1.0f, 0.32f, 0.32f, 1.0f};
|
|
||||||
const glm::vec4 col = frac > 0.5f
|
|
||||||
? glm::mix(yellow, green, (frac - 0.5f) * 2.0f)
|
|
||||||
: glm::mix(red, yellow, frac * 2.0f);
|
|
||||||
const glm::vec4 dim{0.25f, 0.25f, 0.25f, 1.0f};
|
|
||||||
|
|
||||||
// 外框 + 端点刻度
|
|
||||||
Debug::Line({left, base.y, base.z}, {left + kBarW, base.y, base.z},
|
|
||||||
dim, 2.0f, 0.0f, /*depthTest*/ false);
|
|
||||||
Debug::Line({left, base.y - 0.08f, base.z},
|
|
||||||
{left, base.y + 0.08f, base.z},
|
|
||||||
dim, 2.0f, 0.0f, false);
|
|
||||||
Debug::Line({left + kBarW, base.y - 0.08f, base.z},
|
|
||||||
{left + kBarW, base.y + 0.08f, base.z},
|
|
||||||
dim, 2.0f, 0.0f, false);
|
|
||||||
|
|
||||||
if (m_AcceptHits) {
|
|
||||||
if (frac > 0.003f) {
|
|
||||||
Debug::Line({left, base.y, base.z},
|
|
||||||
{left + kBarW * frac, base.y, base.z},
|
|
||||||
col, 8.0f, 0.0f, false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 结算态:整条红 + 上方短刻线(与运行态一眼区分)
|
|
||||||
Debug::Line({left, base.y, base.z},
|
|
||||||
{left + kBarW, base.y, base.z},
|
|
||||||
red, 4.0f, 0.0f, false);
|
|
||||||
Debug::Line({left, base.y + 0.14f, base.z},
|
|
||||||
{left + kBarW, base.y + 0.14f, base.z},
|
|
||||||
red, 2.0f, 0.0f, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 得分点:1 分 = 1 金点(封顶 24 防溢出条外)
|
|
||||||
const glm::vec4 gold{1.0f, 0.78f, 0.2f, 1.0f};
|
|
||||||
const int pips = std::min<i32>(m_Score, 24);
|
|
||||||
for (i32 i = 0; i < pips; ++i) {
|
|
||||||
const f32 px = left + 0.18f +
|
|
||||||
static_cast<f32>(i) * 0.24f;
|
|
||||||
Debug::Cross({px, base.y - 0.3f, base.z}, 0.05f,
|
|
||||||
gold, 2.0f, 0.0f, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 回合状态 ----
|
|
||||||
f32 m_RoundTime = 60.0f;
|
|
||||||
i32 m_TargetCount = 5;
|
|
||||||
i32 m_Score = 0;
|
|
||||||
bool m_AcceptHits = true; // 计分门(参数成员,Target 跨脚本读写)
|
|
||||||
f32 m_TimeLeft = 60.0f;
|
|
||||||
bool m_RWasDown = false; // R 上升沿基准
|
|
||||||
|
|
||||||
// ---- 布局/规格 ----
|
|
||||||
glm::vec3 m_Center{0.0f, 1.5f, -6.0f};
|
|
||||||
i32 m_PerRow = 5;
|
|
||||||
f32 m_Spacing = 1.2f;
|
|
||||||
glm::vec3 m_TargetSize{0.7f, 0.7f, 0.2f};
|
|
||||||
glm::vec3 m_TargetColor{1.0f, 0.3f, 0.25f};
|
|
||||||
i32 m_FragCount = 22;
|
|
||||||
f32 m_HudHeight = 1.6f;
|
|
||||||
|
|
||||||
// ---- 靶子簿记 ----
|
|
||||||
std::vector<u32> m_TargetBits; // 本局靶实体(重开收靶用)
|
|
||||||
Engine::ModelRef m_TargetModel; // 玻璃立方(参数键缓存共享)
|
|
||||||
std::string m_ModelKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
ENGINE_REGISTER_SCRIPT(GameDirector)
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
//
|
|
||||||
// PlayerController.cpp:FPS 玩家控制脚本——挂在场景 primary 相机实体
|
|
||||||
// (CameraComponent.primary=true)上,直接驱动该实体的 Transform。
|
|
||||||
//
|
|
||||||
// 为什么驱动实体而不是"编辑器相机":PIE 内嵌播放/游戏形态的渲染源
|
|
||||||
// 是 primary 相机实体(SceneRenderer::BuildFrame 分派,main.cpp 亦
|
|
||||||
// 注明"PIE 内嵌播放:渲染源已切 primary 相机,编辑器相机停驱")。
|
|
||||||
// 实体位姿一动,视口所见即玩家所见——这才是 Unity/UE 的正道;
|
|
||||||
// EditorCamera 是视口 UI 状态,不是场景实体,脚本够不着也不该够。
|
|
||||||
//
|
|
||||||
// 能力(全部走 Input 门面,零引擎改动):
|
|
||||||
// - 鼠标捕捉:OnStart 捕捉(FPS 语义:光标隐入、无限转向);
|
|
||||||
// Esc 释放(交还编辑器交互);未捕捉时点视口重新捕捉
|
|
||||||
// - 视角:GetMouseDelta 增量 → yaw 绕世界 up 左乘 + pitch 绕本地
|
|
||||||
// X 右乘(±89° 钳制,与编辑器相机 Camera::Update 同语义)
|
|
||||||
// - 移动:WASD 水平 + Space 升 / Ctrl 降(飞行式,无重力——
|
|
||||||
// 打靶场微调机位用);Shift 加速
|
|
||||||
//
|
|
||||||
// 注意:捕捉态下编辑器 UI 收不到鼠标——这是游戏体验的代价;
|
|
||||||
// Esc 是唯一退路(释放后可点 Inspector/改参数)。
|
|
||||||
//
|
|
||||||
// 配套:TestShoot(挂同一实体)射击,同样只在捕捉态开火。
|
|
||||||
//
|
|
||||||
|
|
||||||
#include <Engine/Scripting/Script.h>
|
|
||||||
#include <Engine/Core/Input.h>
|
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
|
||||||
#include <glm/gtc/quaternion.hpp>
|
|
||||||
|
|
||||||
using namespace Engine; // Input 门面
|
|
||||||
using Engine::Scripting::ParamBuilder;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
class PlayerController final : public Engine::Scripting::ScriptBehavior {
|
|
||||||
public:
|
|
||||||
const char* Name() const override { return "PlayerController"; }
|
|
||||||
|
|
||||||
void DeclareParams(ParamBuilder& pb) override {
|
|
||||||
pb.Field("LookSensitivity", &m_Sensitivity, 0.12f); // 度/像素
|
|
||||||
pb.Field("MoveSpeed", &m_MoveSpeed, 4.0f); // 平移速度 m/s
|
|
||||||
pb.Field("RunSpeed", &m_RunSpeed, 8.0f); // Shift 加速 m/s
|
|
||||||
pb.Field("CaptureOnStart", &m_CaptureOnStart, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnStart() override {
|
|
||||||
if (m_CaptureOnStart) {
|
|
||||||
Input::SetCursorCaptured(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnUpdate(f32 dt) override {
|
|
||||||
// ---- 捕捉管理:Esc 释放 / 点视口重捕 ----
|
|
||||||
bool captured = Input::IsCursorCaptured();
|
|
||||||
if (captured && Input::IsKeyDown(Input::Key::Escape)) {
|
|
||||||
Input::SetCursorCaptured(false);
|
|
||||||
captured = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool leftDown =
|
|
||||||
Input::IsMouseButtonDown(Input::MouseButton::Left);
|
|
||||||
if (!captured) {
|
|
||||||
// 未捕捉时点击(点在视口内才收得到——ImGui 捕获的
|
|
||||||
// 区域静默)→ 重捕进入操作态
|
|
||||||
if (leftDown && !m_LeftWasDown) {
|
|
||||||
Input::SetCursorCaptured(true);
|
|
||||||
}
|
|
||||||
m_LeftWasDown = leftDown;
|
|
||||||
return; // 释放态:不转向不移动(编辑器交互让路)
|
|
||||||
}
|
|
||||||
m_LeftWasDown = leftDown;
|
|
||||||
|
|
||||||
Look();
|
|
||||||
Move(dt);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void Look() {
|
|
||||||
f32 dx = 0.0f, dy = 0.0f;
|
|
||||||
Input::GetMouseDelta(dx, dy);
|
|
||||||
if (dx == 0.0f && dy == 0.0f) return;
|
|
||||||
|
|
||||||
auto& tr = Transform();
|
|
||||||
const glm::quat cur = tr.GetRotationQuat();
|
|
||||||
|
|
||||||
// yaw:绕世界 up 左乘(不随俯仰改变旋转轴)
|
|
||||||
const glm::quat yaw =
|
|
||||||
glm::angleAxis(glm::radians(-dx * m_Sensitivity),
|
|
||||||
glm::vec3(0.0f, 1.0f, 0.0f));
|
|
||||||
|
|
||||||
// pitch:绕本地 X 右乘,钳制 ±89°(防过顶翻转)
|
|
||||||
const glm::vec3 fwd = cur * glm::vec3(0.0f, 0.0f, -1.0f);
|
|
||||||
const f32 pitch =
|
|
||||||
glm::asin(glm::clamp(fwd.y, -1.0f, 1.0f));
|
|
||||||
const f32 kPitchLimit = glm::radians(89.0f);
|
|
||||||
const f32 pitchTarget =
|
|
||||||
glm::clamp(pitch + glm::radians(-dy * m_Sensitivity),
|
|
||||||
-kPitchLimit, kPitchLimit);
|
|
||||||
const glm::quat pitchStep =
|
|
||||||
glm::angleAxis(pitchTarget - pitch,
|
|
||||||
glm::vec3(1.0f, 0.0f, 0.0f));
|
|
||||||
|
|
||||||
tr.SetRotationQuat(yaw * (cur * pitchStep));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Move(f32 dt) {
|
|
||||||
glm::vec3 axis{0.0f};
|
|
||||||
if (Input::IsKeyDown(Input::Key::W))
|
|
||||||
axis.z += 1.0f;
|
|
||||||
if (Input::IsKeyDown(Input::Key::S))
|
|
||||||
axis.z -= 1.0f;
|
|
||||||
if (Input::IsKeyDown(Input::Key::D))
|
|
||||||
axis.x += 1.0f;
|
|
||||||
if (Input::IsKeyDown(Input::Key::A))
|
|
||||||
axis.x -= 1.0f;
|
|
||||||
if (Input::IsKeyDown(Input::Key::Space))
|
|
||||||
axis.y += 1.0f;
|
|
||||||
if (Input::IsKeyDown(Input::Key::LeftControl))
|
|
||||||
axis.y -= 1.0f;
|
|
||||||
if (glm::dot(axis, axis) < 1e-6f) return;
|
|
||||||
|
|
||||||
const glm::quat r = Transform().GetRotationQuat();
|
|
||||||
const glm::vec3 fwd = r * glm::vec3(0.0f, 0.0f, -1.0f);
|
|
||||||
const glm::vec3 right = r * glm::vec3(1.0f, 0.0f, 0.0f);
|
|
||||||
|
|
||||||
// 水平走视线投影(含俯仰 = 飞行感),升降用世界 Y
|
|
||||||
glm::vec3 move =
|
|
||||||
fwd * axis.z + right * axis.x
|
|
||||||
+ glm::vec3(0.0f, axis.y, 0.0f);
|
|
||||||
const f32 len = glm::length(move);
|
|
||||||
if (len < 1e-6f) return;
|
|
||||||
move /= len;
|
|
||||||
|
|
||||||
const f32 speed = Input::IsKeyDown(Input::Key::LeftShift)
|
|
||||||
? m_RunSpeed
|
|
||||||
: m_MoveSpeed;
|
|
||||||
auto& tr = Transform();
|
|
||||||
tr.SetPosition(tr.GetWorldPosition() + move * speed * dt);
|
|
||||||
}
|
|
||||||
|
|
||||||
f32 m_Sensitivity = 0.12f;
|
|
||||||
f32 m_MoveSpeed = 4.0f;
|
|
||||||
f32 m_RunSpeed = 8.0f;
|
|
||||||
bool m_CaptureOnStart = true;
|
|
||||||
bool m_LeftWasDown = false; // 重捕用的左键上升沿基准
|
|
||||||
};
|
|
||||||
|
|
||||||
ENGINE_REGISTER_SCRIPT(PlayerController)
|
|
||||||
|
|
||||||
} // 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__)
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
//
|
|
||||||
// Target.cpp:打靶场玻璃靶子脚本(侧线"玩法原型验证")。
|
|
||||||
//
|
|
||||||
// 职责(挂靶子实体:MeshRenderer 玻璃立方 + RigidBody Static Box +
|
|
||||||
// 本脚本;通常由 GameDirector 运行时成批 spawn,也可编辑器手动摆):
|
|
||||||
// OnCollisionEnter → 玻璃外观套用(半透明 + 靶色自发光)→
|
|
||||||
// Fracture::Burst::TryFire(一次性保护 + Dynamic 速度校验 +
|
|
||||||
// 藏靶 + Voronoi 切分 + spawn 玻璃碎片群,共享库内完成)
|
|
||||||
// ReportScore:向 Director 实体的 GameDirector 脚本报分(Score +1,
|
|
||||||
// 写穿 Item.params,PIE Inspector 实时反映);Director=0 表示
|
|
||||||
// 独立靶不计分;导演 AcceptHits=false(回合已结束)不报
|
|
||||||
// OnUpdate/OnDestroy → Burst::Tick/Shutdown(碎片施速/寿命/回收)
|
|
||||||
//
|
|
||||||
// 覆盖的引擎设施:跨脚本参数访问(ScriptRuntime::GetParam/SetParam,
|
|
||||||
// GameDirector ↔ Target 的计分通道)、共享碎裂库(FractureLib.h,
|
|
||||||
// 与 WallFracture 同一条碎片路径)、玻璃材质(透明队列 + 自发光)。
|
|
||||||
//
|
|
||||||
// 演示步骤:任意实体挂 GameDirector → Play → 左键射击靶阵:
|
|
||||||
// 靶子玻璃式碎裂 + 计分,60 秒倒计时,时间到结算,R 重开。
|
|
||||||
//
|
|
||||||
|
|
||||||
#include <Engine/Scripting/Script.h>
|
|
||||||
#include <Engine/Scripting/ScriptRuntime.h> // GetScripts() 跨脚本参数访问
|
|
||||||
#include <Engine/Core/Types.h>
|
|
||||||
|
|
||||||
#include "FractureLib.h"
|
|
||||||
|
|
||||||
using namespace Engine; // i32/f32/u32 基础类型(同 WallFracture)
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// ---- 玻璃靶子 ----
|
|
||||||
class Target final : public Engine::Scripting::ScriptBehavior {
|
|
||||||
public:
|
|
||||||
const char* Name() const override { return "Target"; }
|
|
||||||
|
|
||||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
|
||||||
// 击碎报分去向:Director 实体 bits(0 = 独立靶不计分);
|
|
||||||
// GameDirector spawn 时写进脚本项参数
|
|
||||||
pb.Field("Director", &m_Director, 0);
|
|
||||||
// 靶色(玻璃染色 + 自发光基色;碎片同色系)
|
|
||||||
pb.Field("Color", &m_Color,
|
|
||||||
glm::vec3{1.0f, 0.3f, 0.25f});
|
|
||||||
// 碎裂参数(同 WallFracture 语义;靶面小 → 种子更密)
|
|
||||||
pb.Field("Count", &m_Burst.p.count, 22);
|
|
||||||
pb.Field("SeedRadius", &m_Burst.p.seedRadius, 0.35f);
|
|
||||||
pb.Field("MinSeedDist", &m_Burst.p.minSeedDist, 0.07f);
|
|
||||||
pb.Field("BurstSpeed", &m_Burst.p.burstSpeed, 1.6f);
|
|
||||||
pb.Field("Falloff", &m_Burst.p.falloff, 2.0f);
|
|
||||||
pb.Field("NormalPop", &m_Burst.p.normalPop, 0.6f);
|
|
||||||
pb.Field("UpBias", &m_Burst.p.upBias, 0.4f);
|
|
||||||
pb.Field("Lifetime", &m_Burst.p.lifetime, 6.0f);
|
|
||||||
pb.Field("MaxFragments", &m_Burst.p.maxFragments, 96);
|
|
||||||
pb.Field("MinImpactSpeed", &m_Burst.p.minImpactSpeed, 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnCollisionEnter(u32 other, const glm::vec3& point,
|
|
||||||
const glm::vec3& normal) override {
|
|
||||||
// 玻璃外观:半透明 + 靶色自发光(Fire 前套用,碎片沿用;
|
|
||||||
// 每次覆写代价可忽略,且吃 Inspector 实时改色)。
|
|
||||||
// alpha 0.85(与靶体 BuildGlassCube 同步:0.45 时代碎片
|
|
||||||
// 在亮背景里几乎隐形)
|
|
||||||
m_Burst.look.baseColor = glm::vec4{m_Color, 0.85f};
|
|
||||||
m_Burst.look.transparent = true;
|
|
||||||
m_Burst.look.emissive = m_Color * 0.7f;
|
|
||||||
m_Burst.look.emissiveStrength = 3.0f;
|
|
||||||
|
|
||||||
if (m_Burst.TryFire(HV(), other, point, normal)) {
|
|
||||||
ReportScore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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); }};
|
|
||||||
}
|
|
||||||
// 击碎报分:读导演 AcceptHits 门(回合结束不报)→ Score +1。
|
|
||||||
// 参数层 = 跨脚本通信协议(与序列化同一条路,C# 反射缺失下
|
|
||||||
// 的按名消息);导演实例未建(罕见:同帧开局碰撞)时忽略
|
|
||||||
void ReportScore() {
|
|
||||||
if (m_Director == 0) return;
|
|
||||||
auto& scripts = App().GetScripts();
|
|
||||||
const u32 director = static_cast<u32>(m_Director);
|
|
||||||
// 参数层按名访问(ScriptRuntime 只认 ParamValue 变体,
|
|
||||||
// 取值自行 std::get_if 校型)
|
|
||||||
using Engine::Scripting::ParamValue;
|
|
||||||
ParamValue v{true};
|
|
||||||
if (scripts.GetParam(director, "GameDirector",
|
|
||||||
"AcceptHits", v)) {
|
|
||||||
if (const auto* b = std::get_if<bool>(&v.v); b && !*b) {
|
|
||||||
return; // 回合已结束(Director 收靶前的漏网命中)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i32 score = 0;
|
|
||||||
if (scripts.GetParam(director, "GameDirector", "Score", v)) {
|
|
||||||
if (const auto* s = std::get_if<i32>(&v.v)) {
|
|
||||||
score = *s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scripts.SetParam(director, "GameDirector", "Score",
|
|
||||||
ParamValue{static_cast<i32>(score + 1)});
|
|
||||||
}
|
|
||||||
|
|
||||||
i32 m_Director = 0; // 导演实体 bits(0 = 独立靶)
|
|
||||||
glm::vec3 m_Color{1.0f, 0.3f, 0.25f};
|
|
||||||
Engine::Fracture::Burst m_Burst; // 碎片簿记(look 每次命中前套)
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
ENGINE_REGISTER_SCRIPT(Target)
|
|
||||||
+20
-35
@@ -1,16 +1,11 @@
|
|||||||
//
|
//
|
||||||
// TestShoot.cpp:射击脚本(发射源 = 绑定实体位姿,挂谁从谁发)。
|
// TestShoot.cpp:阶段1 验收脚本——脚本侧 spawn/destroy 底座 + 碰撞事件
|
||||||
//
|
// 转发的端到端链路。
|
||||||
// 正确用法:挂到场景 primary 相机实体(CameraComponent.primary=true)
|
|
||||||
// 上——PIE/游戏形态渲染源就是它(SceneRenderer::BuildFrame 分派),
|
|
||||||
// 实体朝向即准星,实体位置 + MuzzleOffset 即枪口。配 PlayerController
|
|
||||||
// (FPS 转向/移动/鼠标捕捉)即完整第一人称射击。
|
|
||||||
//
|
//
|
||||||
// 三个脚本协作(放一个文件方便对照):
|
// 三个脚本协作(放一个文件方便对照):
|
||||||
// TestShoot(挂相机实体):
|
// TestShoot(挂任意实体,如相机实体):
|
||||||
// 光标捕捉态下鼠标左键上升沿 → 从绑定实体沿视线 spawn 子弹实体
|
// 鼠标左键上升沿 → 从编辑器相机沿视线 spawn 子弹实体
|
||||||
// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入);
|
// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入)
|
||||||
// 未捕捉(编辑器交互中)不射——防点 UI 误发
|
|
||||||
// Bullet(子弹实体,由 TestShoot 运行时装配):
|
// Bullet(子弹实体,由 TestShoot 运行时装配):
|
||||||
// OnCreate 施加枪口初速(body 已由当帧 Step 对账建好);
|
// OnCreate 施加枪口初速(body 已由当帧 Step 对账建好);
|
||||||
// OnCollisionEnter → 命中点 spawn ImpactFx + 自毁;
|
// OnCollisionEnter → 命中点 spawn ImpactFx + 自毁;
|
||||||
@@ -18,11 +13,6 @@
|
|||||||
// ImpactFx(特效实体,由 Bullet 运行时装配,空实体 + 调试点标):
|
// ImpactFx(特效实体,由 Bullet 运行时装配,空实体 + 调试点标):
|
||||||
// 扩张十字 + 白→红渐隐,寿命尽自毁——占位特效
|
// 扩张十字 + 白→红渐隐,寿命尽自毁——占位特效
|
||||||
//
|
//
|
||||||
// 历史注:早期版本从编辑器相机(EditorCamera)发射——那是视口 UI
|
|
||||||
// 状态,不是场景实体,PIE 渲染根本不用它(渲染源 = primary 相机
|
|
||||||
// 实体),子弹会从视口外飞出。已改为绑定实体位姿(Unity 组件语义:
|
|
||||||
// 挂谁从谁发)。
|
|
||||||
//
|
|
||||||
// 覆盖的引擎设施:
|
// 覆盖的引擎设施:
|
||||||
// - Spawn()/Destroy():ScriptBehavior 实体底座(Scene::CreateEntity/
|
// - Spawn()/Destroy():ScriptBehavior 实体底座(Scene::CreateEntity/
|
||||||
// DestroyEntity 延迟队列)
|
// DestroyEntity 延迟队列)
|
||||||
@@ -31,9 +21,8 @@
|
|||||||
// - OnCollisionEnter/Exit:PhysicsWorld::GetEvents → ScriptRuntime
|
// - OnCollisionEnter/Exit:PhysicsWorld::GetEvents → ScriptRuntime
|
||||||
// 帧首派发
|
// 帧首派发
|
||||||
//
|
//
|
||||||
// 验收步骤:primary 相机实体挂 PlayerController + 本脚本 → Play →
|
// 验收步骤:编辑器给任意实体挂 TestShoot → Play → RMB 飞行视角瞄准
|
||||||
// 鼠标转视角 → 左键射击:弹道点飞出 → 撞击瞬间原地绽放十字特效、
|
// 墙体 → 左键射击:弹道点飞出 → 撞墙瞬间原地绽放十字特效、子弹消失。
|
||||||
// 子弹消失。
|
|
||||||
//
|
//
|
||||||
|
|
||||||
#include <Engine/Scripting/Script.h>
|
#include <Engine/Scripting/Script.h>
|
||||||
@@ -41,6 +30,8 @@
|
|||||||
#include <Engine/Core/Input.h>
|
#include <Engine/Core/Input.h>
|
||||||
#include <Engine/Physics/PhysicsWorld.h>
|
#include <Engine/Physics/PhysicsWorld.h>
|
||||||
#include <Engine/Debug/DebugDraw.h>
|
#include <Engine/Debug/DebugDraw.h>
|
||||||
|
#include <Engine/Renderer/Renderer.h>
|
||||||
|
#include <Engine/Renderer/EditorCamera.h>
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
@@ -148,7 +139,7 @@ namespace {
|
|||||||
f32 m_Age = 0.0f;
|
f32 m_Age = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 枪口脚本(挂谁从谁发:位姿取绑定实体,正挂 primary 相机)----
|
// ---- 枪口脚本(验收主体:挂到任意实体,位置/朝向取编辑器相机)----
|
||||||
class TestShoot final : public Engine::Scripting::ScriptBehavior {
|
class TestShoot final : public Engine::Scripting::ScriptBehavior {
|
||||||
public:
|
public:
|
||||||
const char* Name() const override { return "TestShoot"; }
|
const char* Name() const override { return "TestShoot"; }
|
||||||
@@ -167,18 +158,10 @@ namespace {
|
|||||||
void OnUpdate(f32 dt) override {
|
void OnUpdate(f32 dt) override {
|
||||||
m_Cooldown -= dt;
|
m_Cooldown -= dt;
|
||||||
|
|
||||||
const bool down =
|
|
||||||
Input::IsMouseButtonDown(Input::MouseButton::Left);
|
|
||||||
|
|
||||||
// 捕捉态门控:光标未捕捉 = 用户在跟编辑器 UI 交互,不射
|
|
||||||
// (m_FireWasDown 照常对账,重捕按下的那一下不误发)
|
|
||||||
if (!Input::IsCursorCaptured()) {
|
|
||||||
m_FireWasDown = down;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 左键上升沿触发(电平查询 + 上帧状态对比,Unity
|
// 左键上升沿触发(电平查询 + 上帧状态对比,Unity
|
||||||
// GetMouseButtonDown 边沿语义的手工等价物)
|
// GetMouseButtonDown 边沿语义的手工等价物)
|
||||||
|
const bool down =
|
||||||
|
Input::IsMouseButtonDown(Input::MouseButton::Left);
|
||||||
const bool fire = down && !m_FireWasDown &&
|
const bool fire = down && !m_FireWasDown &&
|
||||||
m_Cooldown <= 0.0f;
|
m_Cooldown <= 0.0f;
|
||||||
m_FireWasDown = down;
|
m_FireWasDown = down;
|
||||||
@@ -190,18 +173,20 @@ namespace {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void SpawnBullet() {
|
void SpawnBullet() {
|
||||||
// 枪口 = 绑定实体视线前方(挂 primary 相机实体 = 第一人称
|
// 枪口 = 编辑器相机视线前方(编辑形态 RMB 飞行瞄准;
|
||||||
// 射击;PIE 渲染源就是这台相机,所见即所射)
|
// 与 WASDMove 的朝向来源一致)
|
||||||
const glm::quat rot = Transform().GetWorldRotationQuat();
|
const auto& cam =
|
||||||
const glm::vec3 fwd = rot * glm::vec3(0.0f, 0.0f, -1.0f);
|
App().GetRenderer().GetEditorCamera().GetTransform();
|
||||||
|
const glm::vec3 fwd =
|
||||||
|
cam.GetRotationQuat() * glm::vec3(0.0f, 0.0f, -1.0f);
|
||||||
const glm::vec3 muzzle =
|
const glm::vec3 muzzle =
|
||||||
Transform().GetWorldPosition() + fwd * m_MuzzleOffset;
|
cam.GetWorldPosition() + fwd * m_MuzzleOffset;
|
||||||
|
|
||||||
// ---- 子弹实体装配(spawn 底座 + 组件运行时装配演示)----
|
// ---- 子弹实体装配(spawn 底座 + 组件运行时装配演示)----
|
||||||
auto bullet = Spawn("bullet");
|
auto bullet = Spawn("bullet");
|
||||||
auto& tr = bullet.GetComponent<Engine::ECS::TransformComponent>();
|
auto& tr = bullet.GetComponent<Engine::ECS::TransformComponent>();
|
||||||
tr.SetPosition(muzzle);
|
tr.SetPosition(muzzle);
|
||||||
tr.SetRotationQuat(rot);
|
tr.SetRotationQuat(cam.GetRotationQuat());
|
||||||
|
|
||||||
// Dynamic 球刚体:参数给默认档(轻质量/低弹性,命中即停)
|
// Dynamic 球刚体:参数给默认档(轻质量/低弹性,命中即停)
|
||||||
auto& rb = bullet.AddComponent<Engine::ECS::RigidBodyComponent>();
|
auto& rb = bullet.AddComponent<Engine::ECS::RigidBodyComponent>();
|
||||||
|
|||||||
@@ -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 → 角色控制器路径(扫掠运动学:
|
// - CharacterControllerComponent → 角色控制器路径(扫掠运动学:
|
||||||
@@ -19,6 +23,7 @@
|
|||||||
#include <Engine/Core/Input.h>
|
#include <Engine/Core/Input.h>
|
||||||
#include <Engine/Debug/DebugDraw.h>
|
#include <Engine/Debug/DebugDraw.h>
|
||||||
#include "ScriptDebug.h"
|
#include "ScriptDebug.h"
|
||||||
|
#include "ScriptLog.h"
|
||||||
#include <Engine/Renderer/Renderer.h>
|
#include <Engine/Renderer/Renderer.h>
|
||||||
#include <Engine/Renderer/EditorCamera.h>
|
#include <Engine/Renderer/EditorCamera.h>
|
||||||
|
|
||||||
@@ -49,37 +54,68 @@ namespace {
|
|||||||
pb.Field("RunClip", &m_RunClip, std::string("run"));
|
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 {
|
void OnUpdate(float dt) override {
|
||||||
// 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene
|
// 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene
|
||||||
// view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)。
|
// view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)。
|
||||||
// 输入直读按键(Unity Input.GetKey(KeyCode.W) 同形);UI 捕获
|
// 输入 = 具名动作层(Unity6 Input System / UE5 Enhanced Input
|
||||||
// 键盘时门面静默。要手柄/运行时改键再换动作层
|
// 同位):Move 默认混绑 WASD/方向键/左摇杆(引擎做径向死区 +
|
||||||
//(Input::ActionValue2("Move")——Unity6 新 Input System /
|
// 单位圆钳制,斜向不超速),Jump 混绑 Space/手柄 A——键盘
|
||||||
// UE5 Enhanced Input 对应物),本示例保持最简
|
// 手柄一套代码,将来改键/加绑定不进脚本
|
||||||
const glm::quat cq = App().GetRenderer()
|
// 移动参考 = 场景 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()
|
.GetEditorCamera()
|
||||||
.GetTransform()
|
.GetTransform()
|
||||||
.GetRotationQuat();
|
.GetRotationQuat();
|
||||||
|
}
|
||||||
const glm::vec3 fwd = cq * glm::vec3(0.0f, 0.0f, -1.0f);
|
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);
|
const glm::vec3 right = cq * glm::vec3(1.0f, 0.0f, 0.0f);
|
||||||
|
|
||||||
// 约定:X+ = 右(D),Y- = 前(W);方向合成 = fwd*(-Y) + right*(+X)
|
// 约定:X+ = 右(D),Y- = 前(W);方向合成 = fwd*(-Y) + right*(+X)
|
||||||
glm::vec2 mv(0.0f);
|
const glm::vec2 mvRaw = Input::ActionValue2("Move");
|
||||||
if (Input::IsKeyDown(Input::Key::W)) mv.y -= 1.0f;
|
glm::vec2 mv = mvRaw;
|
||||||
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;
|
|
||||||
if (glm::length(mv) > 1.0f) mv = glm::normalize(mv);
|
if (glm::length(mv) > 1.0f) mv = glm::normalize(mv);
|
||||||
|
|
||||||
glm::vec3 move = fwd * (-mv.y) + right * mv.x;
|
glm::vec3 move = fwd * (-mv.y) + right * mv.x;
|
||||||
move.y = 0.0f;
|
move.y = 0.0f;
|
||||||
if (glm::length(move) > 1e-4f) {
|
// 只钳超速,不放大——摇杆模拟量(幅度 <1)保留进速度:
|
||||||
|
// 轻推慢走、推满全速(归一化会把幅度吃掉,一动就满速)
|
||||||
|
if (glm::length(move) > 1.0f) {
|
||||||
move = glm::normalize(move);
|
move = glm::normalize(move);
|
||||||
}
|
}
|
||||||
// 走/跑
|
// 走/跑:Run 动作(默认绑 Shift + 手柄 L3,按下左摇杆冲刺
|
||||||
const bool moving = glm::length(mv) > 0.1f;
|
// 的 Xbox 惯例)。不判"摇杆推满"——键盘 WASD 幅度恒 1.0,
|
||||||
const bool running =
|
// 那样判会把走路误判成跑
|
||||||
moving && Input::IsKeyDown(Input::Key::LeftShift);
|
const bool running = Input::ActionDown("Run");
|
||||||
const float speed = running ? m_RunSpeed : m_Speed;
|
const float speed = running ? m_RunSpeed : m_Speed;
|
||||||
const glm::vec3 desired = move * speed; // 期望速度(输入意图)
|
const glm::vec3 desired = move * speed; // 期望速度(输入意图)
|
||||||
|
|
||||||
@@ -95,6 +131,7 @@ namespace {
|
|||||||
// 松手滑行刹车。加速/转向用 AccelResponse,松手用更硬的
|
// 松手滑行刹车。加速/转向用 AccelResponse,松手用更硬的
|
||||||
// StopResponse(角色要"想停就停");空中操纵性降到 1/4
|
// StopResponse(角色要"想停就停");空中操纵性降到 1/4
|
||||||
//(起跳后不宜急转弯)。指数逼近,帧率无关
|
//(起跳后不宜急转弯)。指数逼近,帧率无关
|
||||||
|
const bool moving = glm::length(mvRaw) > 0.1f;
|
||||||
float response = moving ? m_AccelResponse : m_StopResponse;
|
float response = moving ? m_AccelResponse : m_StopResponse;
|
||||||
if (!grounded) {
|
if (!grounded) {
|
||||||
response *= 0.25f;
|
response *= 0.25f;
|
||||||
@@ -113,15 +150,14 @@ namespace {
|
|||||||
phy.SetCharacterVelocity(
|
phy.SetCharacterVelocity(
|
||||||
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
|
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
|
||||||
|
|
||||||
// 跳跃:着地 + 上升沿触发。IsKeyDown 是电平量(按住
|
// 跳跃:动作层边沿(Pressed = prev 0 → cur 非 0,键盘
|
||||||
// 每帧都真),不设边沿会变成贴地火箭;IsGrounded 防
|
// Space / 手柄 A 都产生一次上升沿)。IsGrounded 防空中连跳
|
||||||
// 空中连跳
|
if (Input::ActionPressed("Jump") &&
|
||||||
const bool jumpDown = Input::IsKeyDown(Input::Key::Space);
|
|
||||||
if (jumpDown && !m_JumpWasDown &&
|
|
||||||
phy.IsCharacterGrounded(EntityBits())) {
|
phy.IsCharacterGrounded(EntityBits())) {
|
||||||
phy.Jump(EntityBits(), m_JumpSpeed);
|
phy.Jump(EntityBits(), m_JumpSpeed);
|
||||||
|
SLOG_INFO("[WASDMove] jump(着地起跳 {:.1f} m/s)",
|
||||||
|
m_JumpSpeed);
|
||||||
}
|
}
|
||||||
m_JumpWasDown = jumpDown;
|
|
||||||
} else if (auto* rb =
|
} else if (auto* rb =
|
||||||
TryGetComponent<Engine::ECS::RigidBodyComponent>();
|
TryGetComponent<Engine::ECS::RigidBodyComponent>();
|
||||||
rb != nullptr && rb->bodyValid &&
|
rb != nullptr && rb->bodyValid &&
|
||||||
@@ -155,11 +191,24 @@ namespace {
|
|||||||
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
|
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 动画:idle/walk/run 三态切换(Direct 模式 crossfade)----
|
// ---- 动画:按真实速度分档(迟滞防临界抖)----
|
||||||
// 目标 clip 是"电平量":Shift 中途按下/松开都要能换,所以
|
// 判据 = 动量整形后的真实水平速度,不是输入原始值:起步
|
||||||
// 每帧重算目标,只在目标与上帧不同才 Play(边沿)。每帧无
|
// 滑行期已是 walk,松手刹车期保持 walk 直到真停——动画与
|
||||||
// 条件 Play 会不停重置播放头,动画永远播不完
|
// 位移观感一致,按键瞬通瞬断不再瞬间换挡。run 双阈值迟滞
|
||||||
const std::string& target = !moving ? m_IdleClip : (running ? m_RunClip : m_WalkClip);
|
//(进 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 (target != m_CurClip) {
|
||||||
if (auto* anim = TryGetComponent<Engine::ECS::AnimationComponent>();
|
if (auto* anim = TryGetComponent<Engine::ECS::AnimationComponent>();
|
||||||
anim != nullptr && anim->Play(target, 0.35f)) {
|
anim != nullptr && anim->Play(target, 0.35f)) {
|
||||||
@@ -221,7 +270,6 @@ namespace {
|
|||||||
std::string m_WalkClip = "walk";
|
std::string m_WalkClip = "walk";
|
||||||
std::string m_RunClip = "run";
|
std::string m_RunClip = "run";
|
||||||
std::string m_CurClip = "idle"; // 当前目标 clip(边沿检测基准)
|
std::string m_CurClip = "idle"; // 当前目标 clip(边沿检测基准)
|
||||||
bool m_JumpWasDown = false; // 上帧跳跃键(上升沿检测)
|
|
||||||
glm::vec3 m_PrevVel{0.0f}; // 上帧测量速度(加速度差分基准)
|
glm::vec3 m_PrevVel{0.0f}; // 上帧测量速度(加速度差分基准)
|
||||||
glm::vec3 m_AccelS{0.0f}; // 平滑测量加速度(EMA 0.35,调试图)
|
glm::vec3 m_AccelS{0.0f}; // 平滑测量加速度(EMA 0.35,调试图)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
//
|
//
|
||||||
// WallFracture.cpp:可碎墙脚本——碎裂逻辑已提取至 FractureLib.h
|
// WallFracture.cpp:可碎墙脚本——碎裂逻辑已提取至 FractureLib.h
|
||||||
//(打靶场 Target 复用同一条路径),本文件只剩宿主职责:
|
//(打靶场 Target 复用同一条路径),本文件只剩宿主职责:
|
||||||
// DeclareParams 直绑 Fracture::Params(参数名/默认值与历史档位
|
// DeclareParams 直绑 Burst::p 各字段(参数名/默认值与历史档位
|
||||||
// 逐一同名同值,旧场景不受影响)
|
// 逐一同名同值,旧场景不受影响)
|
||||||
// OnCollisionEnter → Burst::TryFire(一次性保护 + 对方 Dynamic
|
// OnCollisionEnter → Burst::TryFire(一次性保护 + 对方撞击来源
|
||||||
// 速度校验 + 藏墙 + Voronoi 切分 + spawn 碎片群,库内完成)
|
// 与速度校验 + 藏墙 + Voronoi 切分 + spawn 碎片群,库内完成)。
|
||||||
|
// 撞击来源 = 动态刚体(子弹/抛体)或角色(玩家撞墙)——角色是
|
||||||
|
// CharacterVirtual,其碰撞事件由 PhysicsWorld 从角色接触合成
|
||||||
|
// (Jolt 的 ContactListener 只回调刚体,不覆盖角色)
|
||||||
// OnUpdate → Burst::Tick(施速待办 / 寿命清理 / 模型回收)
|
// OnUpdate → Burst::Tick(施速待办 / 寿命清理 / 模型回收)
|
||||||
// OnDestroy → Burst::Shutdown(独占碎片模型批量停飞卸载)
|
// OnDestroy → Burst::Shutdown(独占碎片模型批量停飞卸载)
|
||||||
//
|
//
|
||||||
// 外观默认 = 砖色不透明(原观感);玻璃/发光碎片换 Target.cpp 的
|
// 外观默认继承墙的生效材质(底色/贴图/透明),碎块与墙面观感一致;
|
||||||
// 用法(Fire 前改 Burst::look)。
|
// 要独立外观(玻璃/发光碎片)置 Burst::look.inheritFromHost = false
|
||||||
|
// 再用 baseColor/emissive 显式指定。
|
||||||
//
|
//
|
||||||
// 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube +
|
// 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube +
|
||||||
// RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里
|
// RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里
|
||||||
@@ -35,25 +39,27 @@ namespace {
|
|||||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
||||||
// Voronoi 种子数 = 碎片数(40% 在命中点半径内加密 → 孔洞
|
// Voronoi 种子数 = 碎片数(40% 在命中点半径内加密 → 孔洞
|
||||||
// 周边碎块小、边缘大块的玻璃碎分布;最小间距防碎片退化)
|
// 周边碎块小、边缘大块的玻璃碎分布;最小间距防碎片退化)
|
||||||
pb.Field("Count", &m_P.count, 30);
|
// 直绑 Burst::p:碎裂逻辑只读 Burst::p,绑到别处(曾经的
|
||||||
pb.Field("SeedRadius", &m_P.seedRadius, 0.9f); // 加密半径 m
|
// 独立 m_P)会让计数器/寿命等全部失效
|
||||||
pb.Field("MinSeedDist", &m_P.minSeedDist, 0.12f); // 种子最小间距 m
|
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_P.burstSpeed, 1.2f); // 近点径向初速 m/s
|
pb.Field("BurstSpeed", &m_Burst.p.burstSpeed, 1.2f); // 近点径向初速 m/s
|
||||||
pb.Field("Falloff", &m_P.falloff, 1.0f); // 距离衰减率 1/m
|
pb.Field("Falloff", &m_Burst.p.falloff, 1.0f); // 距离衰减率 1/m
|
||||||
pb.Field("NormalPop", &m_P.normalPop, 0.4f); // 法线外推 m/s
|
pb.Field("NormalPop", &m_Burst.p.normalPop, 0.4f); // 法线外推 m/s
|
||||||
pb.Field("UpBias", &m_P.upBias, 0.3f); // 上抛偏置 m/s
|
pb.Field("UpBias", &m_Burst.p.upBias, 0.3f); // 上抛偏置 m/s
|
||||||
pb.Field("Lifetime", &m_P.lifetime, 6.0f); // 碎片寿命(秒)
|
pb.Field("Lifetime", &m_Burst.p.lifetime, 6.0f); // 碎片寿命(秒)
|
||||||
pb.Field("MaxFragments", &m_P.maxFragments, 128); // 活碎片上限
|
pb.Field("MaxFragments", &m_Burst.p.maxFragments, 128); // 活碎片上限
|
||||||
pb.Field("MinImpactSpeed", &m_P.minImpactSpeed, 2.0f); // 触发阈值
|
pb.Field("MinImpactSpeed", &m_Burst.p.minImpactSpeed, 2.0f); // 触发阈值
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnCollisionEnter(u32 other, const glm::vec3& point,
|
void OnCollisionEnter(u32 other, const glm::vec3& point,
|
||||||
const glm::vec3& normal) override {
|
const glm::vec3& normal) override {
|
||||||
// 触发条件在库内:对方须是活的 Dynamic 刚体且速度够快
|
// 触发条件在库内:对方须是够快的撞击来源——动态刚体
|
||||||
//(过滤静止贴靠/角色轻碰——子弹/抛体必中)
|
//(子弹/抛体)或角色(玩家撞墙);速度阈值过滤静止贴靠
|
||||||
m_Burst.TryFire(HV(), other, point, normal);
|
m_Burst.TryFire(HV(), other, point, normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +78,8 @@ namespace {
|
|||||||
[this](Engine::ECS::Entity e) { Destroy(e); }};
|
[this](Engine::ECS::Entity e) { Destroy(e); }};
|
||||||
}
|
}
|
||||||
|
|
||||||
Engine::Fracture::Params m_P; // 参数(DeclareParams 直绑)
|
// 碎片簿记 + 生命周期;参数即 m_Burst.p(DeclareParams 直绑其字段)
|
||||||
Engine::Fracture::Burst m_Burst; // 碎片簿记 + 生命周期(look 默认砖色)
|
Engine::Fracture::Burst m_Burst;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
project(HelloVulkan CXX)
|
project(HelloVulkan C CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ target_include_directories(ImGuizmo PUBLIC ${IMGUIZMO_DIR})
|
|||||||
target_compile_definitions(ImGuizmo PUBLIC IMGUI_DEFINE_MATH_OPERATORS)
|
target_compile_definitions(ImGuizmo PUBLIC IMGUI_DEFINE_MATH_OPERATORS)
|
||||||
target_link_libraries(ImGuizmo PUBLIC imgui)
|
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 帧分析)
|
# Tracy(CPU/GPU 帧分析)
|
||||||
add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp)
|
add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp)
|
||||||
target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public)
|
target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public)
|
||||||
@@ -80,6 +87,7 @@ target_link_libraries(Engine PUBLIC
|
|||||||
spdlog::spdlog
|
spdlog::spdlog
|
||||||
stb
|
stb
|
||||||
assimp::assimp
|
assimp::assimp
|
||||||
|
ufbx
|
||||||
meshoptimizer
|
meshoptimizer
|
||||||
Jolt::Jolt
|
Jolt::Jolt
|
||||||
yaml-cpp::yaml-cpp
|
yaml-cpp::yaml-cpp
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#define HELLOVULKAN_ANIMATIONATTACH_H
|
#define HELLOVULKAN_ANIMATIONATTACH_H
|
||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
|
#include <Engine/Renderer/Model.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -51,6 +52,16 @@ namespace Engine {
|
|||||||
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
|
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
|
||||||
const std::string& sourcePath);
|
const std::string& sourcePath);
|
||||||
|
|
||||||
|
// 纯算法核心(headless 探针可直测):轨道重映射进 dstNodes
|
||||||
|
// 坐标系,产出的 clips 追加到 outExtra。幂等/命名冲突检查
|
||||||
|
// 基于 existingClips。AttachClipsToModel 只是
|
||||||
|
// "取 nodes/clips + 回填" 的薄封装
|
||||||
|
AttachResult AttachClipsToNodes(
|
||||||
|
const std::vector<ModelNode>& dstNodes,
|
||||||
|
const std::vector<AnimationClip>& existingClips,
|
||||||
|
ParsedModel&& src, const std::string& sourcePath,
|
||||||
|
std::vector<AnimationClip>& outExtra);
|
||||||
|
|
||||||
} // namespace Animation
|
} // namespace Animation
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -64,15 +64,19 @@ namespace Engine::Animation {
|
|||||||
// outGlobals 尺寸 = model.GetNodes().size(),父序 DFS 累乘。
|
// outGlobals 尺寸 = model.GetNodes().size(),父序 DFS 累乘。
|
||||||
// (crossfade 双层 = EvaluateLayers 的 count==2 特例,同一套
|
// (crossfade 双层 = EvaluateLayers 的 count==2 特例,同一套
|
||||||
// 加权混合数学,消掉历史双份代码)
|
// 加权混合数学,消掉历史双份代码)
|
||||||
|
// inPlace = 丢弃动画子树根节点的位移(Unity in-place 语义:
|
||||||
|
// 根运动开关关闭,原地播放,旋转保留)
|
||||||
void Evaluate(const Model& model, const AnimationClip& cur,
|
void Evaluate(const Model& model, const AnimationClip& cur,
|
||||||
f32 curTime, const CrossfadeState& xf,
|
f32 curTime, const CrossfadeState& xf,
|
||||||
std::vector<glm::mat4>& outGlobals);
|
std::vector<glm::mat4>& outGlobals,
|
||||||
|
bool inPlace = false);
|
||||||
|
|
||||||
// N 层加权混合(BlendTree / 状态机过渡的通用入口):
|
// N 层加权混合(BlendTree / 状态机过渡的通用入口):
|
||||||
// 逐层采样局部 TRS → 权重归一化混合(pos/scl lerp、rot 渐进
|
// 逐层采样局部 TRS → 权重归一化混合(pos/scl lerp、rot 渐进
|
||||||
// slerp)→ global 累乘。全空/全零权重 = rest 姿态
|
// slerp)→ global 累乘。全空/全零权重 = rest 姿态
|
||||||
void EvaluateLayers(const Model& model, const PoseLayer* layers,
|
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 原样累乘。
|
// 绑定姿势:全节点 untouched = localTransform 原样累乘。
|
||||||
// 与渲染 identity palette 的表现一致(无动画/组件禁用实体的
|
// 与渲染 identity palette 的表现一致(无动画/组件禁用实体的
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
//
|
//
|
||||||
// HumanoidRig:Unity Avatar 语义的跨骨架重定向映射。
|
// HumanoidRig:Unity Avatar 语义的跨骨架重定向映射。
|
||||||
//
|
//
|
||||||
// 资产组装现实:角色模型与动画 clip 是两套独立资产,骨骼命名完全
|
// 架构分层(对齐 Unity Mecanim / UE IK Retargeter 的做法):
|
||||||
// 不同源(英文/Mixamo/UE/Blender/MMD 日文)。Unity Humanoid 的解法
|
|
||||||
// 是把任意命名骨架映射到标准人形槽位(Avatar bone mapping),动画
|
|
||||||
// 经槽位转换后即可驱动任意人形角色。
|
|
||||||
//
|
//
|
||||||
// 这里实现其中枢:名字归一化 → 槽位识别(关键词表,en/Mixamo/MMD)
|
// ┌─ standard ─ 标准人形拓扑:HumanoidSlot 52 槽(主干 6 + 臂链
|
||||||
// → 源/目标骨架间生成 node 下标映射对。同名直映是映射的特例。
|
// │ 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
|
#ifndef HELLOVULKAN_HUMANOIDRIG_H
|
||||||
@@ -16,13 +27,24 @@
|
|||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/Renderer/Model.h>
|
#include <Engine/Renderer/Model.h>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine::Animation {
|
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 {
|
enum class HumanoidSlot : u32 {
|
||||||
Hips = 0,
|
Hips = 0,
|
||||||
Spine,
|
Spine,
|
||||||
@@ -34,38 +56,113 @@ namespace Engine::Animation {
|
|||||||
LeftUpperArm,
|
LeftUpperArm,
|
||||||
LeftLowerArm,
|
LeftLowerArm,
|
||||||
LeftHand,
|
LeftHand,
|
||||||
|
LeftThumb1, // 指骨区起点(FingerSlot 算术依赖连续布局)
|
||||||
|
LeftThumb2,
|
||||||
|
LeftThumb3,
|
||||||
|
LeftIndex1,
|
||||||
|
LeftIndex2,
|
||||||
|
LeftIndex3,
|
||||||
|
LeftMiddle1,
|
||||||
|
LeftMiddle2,
|
||||||
|
LeftMiddle3,
|
||||||
|
LeftRing1,
|
||||||
|
LeftRing2,
|
||||||
|
LeftRing3,
|
||||||
|
LeftPinky1,
|
||||||
|
LeftPinky2,
|
||||||
|
LeftPinky3,
|
||||||
|
LeftUpperLeg,
|
||||||
|
LeftLowerLeg,
|
||||||
|
LeftFoot,
|
||||||
|
LeftToes,
|
||||||
RightShoulder,
|
RightShoulder,
|
||||||
RightUpperArm,
|
RightUpperArm,
|
||||||
RightLowerArm,
|
RightLowerArm,
|
||||||
RightHand,
|
RightHand,
|
||||||
LeftUpperLeg,
|
RightThumb1,
|
||||||
LeftLowerLeg,
|
RightThumb2,
|
||||||
LeftFoot,
|
RightThumb3,
|
||||||
|
RightIndex1,
|
||||||
|
RightIndex2,
|
||||||
|
RightIndex3,
|
||||||
|
RightMiddle1,
|
||||||
|
RightMiddle2,
|
||||||
|
RightMiddle3,
|
||||||
|
RightRing1,
|
||||||
|
RightRing2,
|
||||||
|
RightRing3,
|
||||||
|
RightPinky1,
|
||||||
|
RightPinky2,
|
||||||
|
RightPinky3,
|
||||||
RightUpperLeg,
|
RightUpperLeg,
|
||||||
RightLowerLeg,
|
RightLowerLeg,
|
||||||
RightFoot,
|
RightFoot,
|
||||||
|
RightToes,
|
||||||
Count
|
Count
|
||||||
};
|
};
|
||||||
|
|
||||||
// 槽位侧别(左右对称骨骼的识别结果)
|
// 槽位侧别(左右对称骨骼的识别结果)
|
||||||
enum class Side : u8 { None, Left, Right };
|
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);
|
const char* HumanoidSlotName(HumanoidSlot slot);
|
||||||
|
|
||||||
// 名字归一化 + 槽位识别。
|
// ---- mapper:名字归一化 + 槽位识别 ----
|
||||||
|
|
||||||
|
// 名字归一化 + 查方言表识别槽位。
|
||||||
// "mixamorig:LeftForeArm" → {LowerArm, Left};"左ひじ" → {LowerArm, Left};
|
// "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,
|
HumanoidSlot MatchHumanoidSlot(std::string_view name, Side& side,
|
||||||
bool& ok);
|
bool& ok);
|
||||||
|
|
||||||
// 跨骨架自动映射:返回 (srcNodeIdx, dstNodeIdx) 对列表。
|
// 跨骨架自动映射:返回 (srcNodeIdx, dstNodeIdx) 对列表。
|
||||||
// 同名 node 直映优先(同源拆分资产零损耗),余下按槽位匹配;
|
// 同名 node 直映优先(同源拆分资产零损耗),余下按槽位一对一配对;
|
||||||
// 一槽多候选时取语义关键词优先级表中的顺序(先高后低)。
|
// 同槽多候选时取方言表内的优先序(先高后低)。
|
||||||
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
|
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
|
||||||
const std::vector<ModelNode>& srcNodes,
|
const std::vector<ModelNode>& srcNodes,
|
||||||
const std::vector<ModelNode>& dstNodes);
|
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
|
} // namespace Engine::Animation
|
||||||
|
|
||||||
#endif // HELLOVULKAN_HUMANOIDRIG_H
|
#endif // HELLOVULKAN_HUMANOIDRIG_H
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
//
|
||||||
|
// LogRing.h:进程级日志环形缓冲(编辑器 Log Panel 的数据源)。
|
||||||
|
//
|
||||||
|
// RingSink 注册进默认 logger 的 sinks 后,所有日志(引擎 + 脚本经
|
||||||
|
// ScriptLog/ENGINE_* 打印的)自动落缓冲;面板按 version 增量感知、
|
||||||
|
// Snapshot 整体拉取。头文件设施(sink_it_ 内联),无 cpp。
|
||||||
|
//
|
||||||
|
#ifndef HELLOVULKAN_LOGRING_H
|
||||||
|
#define HELLOVULKAN_LOGRING_H
|
||||||
|
|
||||||
|
#include <Engine/Core/Types.h>
|
||||||
|
|
||||||
|
#include <spdlog/sinks/base_sink.h>
|
||||||
|
#include <spdlog/details/log_msg.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Engine::Debug {
|
||||||
|
|
||||||
|
struct LogEntry {
|
||||||
|
spdlog::level::level_enum level;
|
||||||
|
std::string msg; // 整行(已按 ring pattern 格式化,无色码)
|
||||||
|
};
|
||||||
|
|
||||||
|
class LogRingBuffer {
|
||||||
|
public:
|
||||||
|
static constexpr size_t kCap = 2048;
|
||||||
|
|
||||||
|
// 门控:启动期系统初始化日志(Vulkan/UBO/IBL...)不进面板,
|
||||||
|
// 与 Unity Console 语义一致(只收运行期游戏侧日志)。
|
||||||
|
// Application::Run 进入主循环时开闸
|
||||||
|
bool IsEnabled() const {
|
||||||
|
return m_Enabled.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
void SetEnabled(bool v) {
|
||||||
|
m_Enabled.store(v, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Push(spdlog::level::level_enum lvl, std::string msg) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_Mutex);
|
||||||
|
if (m_Entries.size() < kCap) {
|
||||||
|
m_Entries.push_back({lvl, std::move(msg)});
|
||||||
|
} else {
|
||||||
|
m_Entries[m_Head] = {lvl, std::move(msg)};
|
||||||
|
m_Head = (m_Head + 1) % kCap; // 满后覆写最旧
|
||||||
|
}
|
||||||
|
++m_Version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快照(满环时按时间序重排)
|
||||||
|
std::vector<LogEntry> Snapshot() const {
|
||||||
|
std::lock_guard<std::mutex> lk(m_Mutex);
|
||||||
|
if (m_Entries.size() < kCap) {
|
||||||
|
return m_Entries;
|
||||||
|
}
|
||||||
|
std::vector<LogEntry> out;
|
||||||
|
out.reserve(kCap);
|
||||||
|
out.insert(out.end(), m_Entries.begin() + static_cast<long>(m_Head),
|
||||||
|
m_Entries.end());
|
||||||
|
out.insert(out.end(), m_Entries.begin(),
|
||||||
|
m_Entries.begin() + static_cast<long>(m_Head));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Clear() {
|
||||||
|
std::lock_guard<std::mutex> lk(m_Mutex);
|
||||||
|
m_Entries.clear();
|
||||||
|
m_Head = 0;
|
||||||
|
++m_Version;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 Version() const {
|
||||||
|
return m_Version.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::mutex m_Mutex;
|
||||||
|
std::vector<LogEntry> m_Entries;
|
||||||
|
size_t m_Head = 0;
|
||||||
|
std::atomic<u64> m_Version{0};
|
||||||
|
std::atomic<bool> m_Enabled{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
inline LogRingBuffer& LogRing() {
|
||||||
|
static LogRingBuffer inst;
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// spdlog sink 适配:log() 整行入环。pattern 由装配端设置(无色码)
|
||||||
|
class RingSink : public spdlog::sinks::base_sink<std::mutex> {
|
||||||
|
protected:
|
||||||
|
void sink_it_(const spdlog::details::log_msg& msg) override {
|
||||||
|
if (!LogRing().IsEnabled()) {
|
||||||
|
return; // 启动期:只落终端/文件,不进面板
|
||||||
|
}
|
||||||
|
spdlog::memory_buf_t formatted;
|
||||||
|
base_sink<std::mutex>::formatter_->format(msg, formatted);
|
||||||
|
std::string line(formatted.data(), formatted.size());
|
||||||
|
while (!line.empty() &&
|
||||||
|
(line.back() == '\n' || line.back() == '\r')) {
|
||||||
|
line.pop_back();
|
||||||
|
}
|
||||||
|
LogRing().Push(msg.level, std::move(line));
|
||||||
|
}
|
||||||
|
void flush_() override {}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Engine::Debug
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_LOGRING_H
|
||||||
@@ -37,29 +37,27 @@ namespace Engine
|
|||||||
void AddAllocation(u64 bytes);
|
void AddAllocation(u64 bytes);
|
||||||
void AddDeallocation(u64 bytes);
|
void AddDeallocation(u64 bytes);
|
||||||
|
|
||||||
// ---- 采样进程真实内存 ----
|
// ---- 只读快照(面板/遥测消费)----
|
||||||
void SampleProcessMemory();
|
struct Snapshot
|
||||||
|
{
|
||||||
// ---- 打印 ----
|
u64 textureCount = 0;
|
||||||
void Print() const;
|
u64 textureBytes = 0;
|
||||||
void PrintDetailed() const;
|
u64 meshCount = 0;
|
||||||
void PrintComparison() const;
|
u64 meshBytes = 0;
|
||||||
|
u64 bufferCount = 0;
|
||||||
|
u64 bufferBytes = 0;
|
||||||
|
u64 memoryBlockBytes = 0; // MemoryAllocator 已分配块
|
||||||
|
u64 memoryUsedBytes = 0; // 块内实际占用
|
||||||
|
u64 frameAllocation = 0; // 本帧新增(BeginFrame 清零)
|
||||||
|
u64 frameDeallocation = 0; // 本帧释放
|
||||||
|
u64 totalAllocation = 0;
|
||||||
|
u64 totalDeallocation = 0;
|
||||||
|
};
|
||||||
|
Snapshot TakeSnapshot() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Stats() = default;
|
Stats() = default;
|
||||||
|
|
||||||
struct ProcessMemory
|
|
||||||
{
|
|
||||||
u64 VmSize = 0; // /proc/self/status VmSize
|
|
||||||
u64 Rss = 0; // /proc/self/status VmRSS
|
|
||||||
u64 Pss = 0; // smaps_rollup
|
|
||||||
u64 SharedClean = 0;
|
|
||||||
u64 PrivateDirty = 0;
|
|
||||||
u64 PrivateClean = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
ProcessMemory ReadProcessMemory() const;
|
|
||||||
|
|
||||||
mutable std::mutex m_Mutex;
|
mutable std::mutex m_Mutex;
|
||||||
|
|
||||||
// 资源计数
|
// 资源计数
|
||||||
@@ -81,9 +79,6 @@ namespace Engine
|
|||||||
u64 m_FrameDeallocation = 0;
|
u64 m_FrameDeallocation = 0;
|
||||||
u64 m_TotalAllocation = 0;
|
u64 m_TotalAllocation = 0;
|
||||||
u64 m_TotalDeallocation = 0;
|
u64 m_TotalDeallocation = 0;
|
||||||
|
|
||||||
// 进程快照
|
|
||||||
ProcessMemory m_ProcessMem{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#define STATS Engine::Stats::Get()
|
#define STATS Engine::Stats::Get()
|
||||||
|
|||||||
@@ -480,6 +480,9 @@ namespace Engine::ECS {
|
|||||||
// 重力倍率(UE CharacterMovement.GravityScale 同位):真实 9.81
|
// 重力倍率(UE CharacterMovement.GravityScale 同位):真实 9.81
|
||||||
// 对人形角色偏"棉花糖"(滞空 1.2s+),游戏手感普遍 2~3 倍
|
// 对人形角色偏"棉花糖"(滞空 1.2s+),游戏手感普遍 2~3 倍
|
||||||
f32 gravityScale = 2.0f;
|
f32 gravityScale = 2.0f;
|
||||||
|
// 形状附加偏移:叠加在脚底约定(胶囊底端=原点)之上,模型
|
||||||
|
// pivot 不在脚底时对齐视觉网格用(Y+ 胶囊上抬)
|
||||||
|
glm::vec3 shapeOffset{0.0f};
|
||||||
|
|
||||||
bool operator==(const CharacterControllerComponent&) const = default;
|
bool operator==(const CharacterControllerComponent&) const = default;
|
||||||
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
|
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
// "看不到"它(无碰撞解算,仅查询)。由 PhysicsWorld 对账创建/销毁并
|
// "看不到"它(无碰撞解算,仅查询)。由 PhysicsWorld 对账创建/销毁并
|
||||||
// 在 PhysicsSystem::Update 之后驱动(Jolt 官方建议序)。
|
// 在 PhysicsSystem::Update 之后驱动(Jolt 官方建议序)。
|
||||||
//
|
//
|
||||||
|
// 角色间碰撞(Jolt 设计行为):CharacterVirtual 彼此默认穿透,需经
|
||||||
|
// CharacterVsCharacterCollision 显式互查。PhysicsWorld 持有一份
|
||||||
|
// CharacterVsCharacterCollisionSimple 并注入每个角色(SetCharacterCollision),
|
||||||
|
// 使角色扫掠时也撞其它角色——进/出列表由本类构造/析构自动维护。
|
||||||
|
//
|
||||||
// 速度语义(游戏逻辑职责分工):
|
// 速度语义(游戏逻辑职责分工):
|
||||||
// - 水平移动:游戏逻辑每帧 SetCharacterVelocity 喂意图速度
|
// - 水平移动:游戏逻辑每帧 SetCharacterVelocity 喂意图速度
|
||||||
// - 垂直分量:本类自动积分重力(着地清零向下分量);Jump 置垂直速度
|
// - 垂直分量:本类自动积分重力(着地清零向下分量);Jump 置垂直速度
|
||||||
@@ -36,13 +41,21 @@ namespace Engine {
|
|||||||
f32 mass = 70.0f;
|
f32 mass = 70.0f;
|
||||||
f32 maxSlopeAngle = 45.0f; // 度
|
f32 maxSlopeAngle = 45.0f; // 度
|
||||||
f32 stepHeight = 0.4f; // 台阶攀爬高
|
f32 stepHeight = 0.4f; // 台阶攀爬高
|
||||||
|
// 形状附加偏移(相对"胶囊底端钉实体原点"的脚底约定):
|
||||||
|
// 模型 pivot 不在脚底时对齐用(Y+ 上抬)
|
||||||
|
glm::vec3 shapeOffset{0.0f};
|
||||||
|
|
||||||
bool operator==(const Params&) const = default;
|
bool operator==(const Params&) const = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
CharacterController(JPH::PhysicsSystem& system, const Params& params,
|
CharacterController(JPH::PhysicsSystem& system, const Params& params,
|
||||||
const glm::vec3& position);
|
const glm::vec3& position);
|
||||||
~CharacterController() = default; // Ref 引用计数自动释放
|
// 析构:先从角色间碰撞列表摘除(防悬垂指针),Ref 随即释放
|
||||||
|
~CharacterController();
|
||||||
|
|
||||||
|
// 注入角色间碰撞列表(PhysicsWorld 单份共享):登记本角色并打开
|
||||||
|
// 互查。构造后由 PhysicsWorld 创建时调用一次;列表须比本对象活得久
|
||||||
|
void SetCharacterCollision(JPH::CharacterVsCharacterCollisionSimple& list);
|
||||||
|
|
||||||
// 每物理步调用:重力积分 + ExtendedUpdate(扫掠滑动 + 贴地 +
|
// 每物理步调用:重力积分 + ExtendedUpdate(扫掠滑动 + 贴地 +
|
||||||
// 上台阶)。allocator 与 PhysicsSystem::Update 串行复用同一池
|
// 上台阶)。allocator 与 PhysicsSystem::Update 串行复用同一池
|
||||||
@@ -59,6 +72,16 @@ namespace Engine {
|
|||||||
|
|
||||||
const Params& GetParams() const { return m_Params; }
|
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 画不到,须手动
|
// 调试渲染:非 body,PhysicsSystem::DrawBodies 画不到,须手动
|
||||||
// (脚本在物理位姿/脚底原点处画胶囊线框)
|
// (脚本在物理位姿/脚底原点处画胶囊线框)
|
||||||
void DebugDraw(JPH::DebugRenderer& renderer) const;
|
void DebugDraw(JPH::DebugRenderer& renderer) const;
|
||||||
@@ -67,6 +90,8 @@ namespace Engine {
|
|||||||
Params m_Params;
|
Params m_Params;
|
||||||
JPH::Ref<JPH::CharacterVirtual> m_Character;
|
JPH::Ref<JPH::CharacterVirtual> m_Character;
|
||||||
JPH::CharacterVirtual::ExtendedUpdateSettings m_Ext;
|
JPH::CharacterVirtual::ExtendedUpdateSettings m_Ext;
|
||||||
|
JPH::CharacterVsCharacterCollisionSimple* m_VsChar = nullptr; // 非拥有
|
||||||
|
JPH::Vec3 m_SweepVelocity{0.0f, 0.0f, 0.0f}; // 扫掠前速度快照
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ namespace Engine {
|
|||||||
// 角色脚底位置(物理权威世界坐标):调试图元/特效锚点用它,
|
// 角色脚底位置(物理权威世界坐标):调试图元/特效锚点用它,
|
||||||
// 而不是 Transform().GetPosition()——后者在有父级时是本地坐标
|
// 而不是 Transform().GetPosition()——后者在有父级时是本地坐标
|
||||||
glm::vec3 GetCharacterPosition(u32 entityBits) const;
|
glm::vec3 GetCharacterPosition(u32 entityBits) const;
|
||||||
|
// 角色"撞击强度"速度(上一次扫掠前的速度):撞墙/碎墙等判定用它,
|
||||||
|
// 而非 GetCharacterVelocity——后者已被 Jolt 抵消掉朝向墙面的分量
|
||||||
|
glm::vec3 GetCharacterImpactVelocity(u32 entityBits) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// 创建时缓存的参数(变更检测 → 重建 body)
|
// 创建时缓存的参数(变更检测 → 重建 body)
|
||||||
@@ -138,7 +141,8 @@ namespace Engine {
|
|||||||
// 角色非刚体(不进 PhysicsSystem),无 body 清理负担
|
// 角色非刚体(不进 PhysicsSystem),无 body 清理负担
|
||||||
void SyncCharacters();
|
void SyncCharacters();
|
||||||
// 每物理步(PhysicsSystem::Update 之后,Jolt 官方建议序):
|
// 每物理步(PhysicsSystem::Update 之后,Jolt 官方建议序):
|
||||||
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)
|
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)+ 合成角色
|
||||||
|
// 碰撞事件(CharacterVirtual 不进 ContactListener,见函数注释)
|
||||||
void UpdateCharacters(f32 dt);
|
void UpdateCharacters(f32 dt);
|
||||||
// Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape /
|
// Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape /
|
||||||
// Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box)
|
// Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box)
|
||||||
@@ -165,9 +169,16 @@ namespace Engine {
|
|||||||
std::unordered_map<u32, ConstraintRecord> m_Constraints;
|
std::unordered_map<u32, ConstraintRecord> m_Constraints;
|
||||||
|
|
||||||
// ---- 角色(entityBits → CharacterVirtual 封装)----
|
// ---- 角色(entityBits → CharacterVirtual 封装)----
|
||||||
|
// 角色间碰撞互查表:Jolt CharacterVirtual 默认彼此穿透,须显式注入
|
||||||
|
// CharacterVsCharacterCollisionSimple 才互撞。声明在 m_Characters
|
||||||
|
// 之前(析构更晚)——角色析构时会回表 Remove,列表须后于角色释放
|
||||||
|
Scope<JPH::CharacterVsCharacterCollisionSimple> m_VsCharacterCollision;
|
||||||
struct CharacterEntry {
|
struct CharacterEntry {
|
||||||
std::unique_ptr<CharacterController> cc;
|
std::unique_ptr<CharacterController> cc;
|
||||||
glm::vec3 lastPos{0.0f}; // 编辑器拖动检测
|
glm::vec3 lastPos{0.0f}; // 编辑器拖动检测
|
||||||
|
// 上一步活跃接触的"对方实体"集合:本步对比出角色碰撞
|
||||||
|
// Enter/Exit(CharacterVirtual 非刚体,ContactListener 不收)
|
||||||
|
std::unordered_set<u32> contacts;
|
||||||
};
|
};
|
||||||
std::unordered_map<u32, CharacterEntry> m_Characters;
|
std::unordered_map<u32, CharacterEntry> m_Characters;
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,13 @@ namespace Engine {
|
|||||||
// 无填充歧义),加字段必须双端同步(simple_shader*.frag)
|
// 无填充歧义),加字段必须双端同步(simple_shader*.frag)
|
||||||
struct MaterialParams {
|
struct MaterialParams {
|
||||||
// x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
// 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 透明度
|
// glTF baseColorFactor:rgb 乘进 albedo,a 作 BLEND 透明度
|
||||||
glm::vec4 baseColor{1.0f, 1.0f, 1.0f, 1.0f};
|
glm::vec4 baseColor{1.0f, 1.0f, 1.0f, 1.0f};
|
||||||
// glTF 表面扩展(KHR_materials_ior / transmission / volume /
|
// glTF 表面扩展(KHR_materials_ior / transmission / volume /
|
||||||
@@ -55,7 +61,14 @@ namespace Engine {
|
|||||||
// y = 透射率(0 = 普通表面),z = 自发光强度,w = 体积厚度
|
// y = 透射率(0 = 普通表面),z = 自发光强度,w = 体积厚度
|
||||||
//(折射屏幕偏移的世界距离,0 = 无弯曲直透)
|
//(折射屏幕偏移的世界距离,0 = 无弯曲直透)
|
||||||
glm::vec4 surface{1.5f, 0.0f, 1.0f, 0.0f};
|
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};
|
glm::vec4 misc{0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
// glTF emissiveFactor:自发光颜色(rgb;黑 = 不发光)。emissive
|
// glTF emissiveFactor:自发光颜色(rgb;黑 = 不发光)。emissive
|
||||||
// 槽默认兜底纹理是纯黑,无贴图时 Emissive Strength 乘出来恒黑
|
// 槽默认兜底纹理是纯黑,无贴图时 Emissive Strength 乘出来恒黑
|
||||||
@@ -63,8 +76,10 @@ namespace Engine {
|
|||||||
glm::vec4 emissiveFactor{0.0f, 0.0f, 0.0f, 0.0f};
|
glm::vec4 emissiveFactor{0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
// 每槽 KHR_texture_transform(glTF2 每个 textureInfo 独立声明
|
// 每槽 KHR_texture_transform(glTF2 每个 textureInfo 独立声明
|
||||||
// offset/scale/rotation——烘焙单份顶点 UV 无法表达"一材质多槽
|
// offset/scale/rotation——烘焙单份顶点 UV 无法表达"一材质多槽
|
||||||
// 各异变换",收敛为 shader 侧线性式 uv' = A·uv + t,作用在本引擎
|
// 各异变换",收敛为 shader 侧线性式 uv' = A·uv + t,作用在 glTF
|
||||||
// v-up 顶点 UV 空间,换算推导见 Model.cpp 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)。
|
// uvA[slot] = (a11, a12, a21, a22)(行主 2×2),uvT[slot] = (tx, ty, 0, 0)。
|
||||||
// 长度必须 = Material::kSlotCount;std140 vec4 数组步长 16B 与
|
// 长度必须 = Material::kSlotCount;std140 vec4 数组步长 16B 与
|
||||||
// CPU 布局天然对齐
|
// CPU 布局天然对齐
|
||||||
@@ -72,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},
|
||||||
{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] = {};
|
glm::vec4 uvT[6] = {};
|
||||||
|
// albedo 贴图 alpha 作细节叠加遮罩(脸部腮红/卧蚕等局部效果):
|
||||||
|
// rgb = 叠加色(与 baseColor 同款直乘/直用,不做空间换算),
|
||||||
|
// a = 强度(0 = 关闭,mix 恒等无副作用)。alpha 遮罩来自贴图本身,
|
||||||
|
// 无 alpha 或 alpha 恒 1 的贴图开启后即等效整体染色
|
||||||
|
glm::vec4 alphaOverlay{0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
};
|
};
|
||||||
|
|
||||||
class Material {
|
class Material {
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ namespace Engine {
|
|||||||
// 材质资产数据段(磁盘格式 ↔ 内存值,无 GPU 状态)。
|
// 材质资产数据段(磁盘格式 ↔ 内存值,无 GPU 状态)。
|
||||||
// texPaths = 文件系统相对 CWD 路径(与模型解析产物同源,glTF/FBX
|
// texPaths = 文件系统相对 CWD 路径(与模型解析产物同源,glTF/FBX
|
||||||
// 纹理可直接快照);空串 = 该槽用共享默认纹理兜底。
|
// 纹理可直接快照);空串 = 该槽用共享默认纹理兜底。
|
||||||
// glb 内嵌纹理无独立文件路径 → 对 glb 模型 Create Override 时
|
// glb 内嵌纹理在模型导入期已抽到 <模型目录>/textures/ 落盘并回写为
|
||||||
// 纹理槽为空(回退默认纹理),参数/透明照常可编辑。
|
// 真实路径(Model::ParseFromDisk),故此处与文件纹理同语义——覆盖
|
||||||
|
// 材质可正常继承
|
||||||
struct MaterialAssetData {
|
struct MaterialAssetData {
|
||||||
std::array<std::string, Material::kSlotCount> texPaths;
|
std::array<std::string, Material::kSlotCount> texPaths;
|
||||||
MaterialParams params;
|
MaterialParams params;
|
||||||
|
|||||||
@@ -137,6 +137,12 @@ namespace Engine
|
|||||||
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
|
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
|
||||||
static ParsedModel ParseFromDisk(const std::string& path);
|
static ParsedModel ParseFromDisk(const std::string& path);
|
||||||
|
|
||||||
|
// 资产显示名(编辑器 UI / clip 命名的唯一来源):取文件 stem;
|
||||||
|
// stem 无信息量(空 / 导入资产库的 "asset")时读同目录 pathname
|
||||||
|
// 文件取原始资产 basename,再回退父目录名。避免 UI 直接显示
|
||||||
|
// 哈希路径(用户看到的是 id)
|
||||||
|
static std::string AssetDisplayName(const std::string& path);
|
||||||
|
|
||||||
// GPU 构建(必须主线程):纹理上传 / mesh 缓冲 / 材质描述符。
|
// GPU 构建(必须主线程):纹理上传 / mesh 缓冲 / 材质描述符。
|
||||||
// 解析失败返回 nullptr。
|
// 解析失败返回 nullptr。
|
||||||
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
|
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ namespace Engine {
|
|||||||
|
|
||||||
class Device;
|
class Device;
|
||||||
|
|
||||||
|
// 显存估算(按格式字节宽与 mip 链逐级累加;BC 压缩按 4x4 块计)——
|
||||||
|
// 分配器台账与 Texture 资源台账共用同一算法,两边永不漂移
|
||||||
|
u64 EstimateImageBytes(vk::Format fmt, u32 w, u32 h, u32 depth, u32 mips);
|
||||||
|
|
||||||
class MemoryAllocator {
|
class MemoryAllocator {
|
||||||
public:
|
public:
|
||||||
explicit MemoryAllocator(Device& device);
|
explicit MemoryAllocator(Device& device);
|
||||||
|
|||||||
@@ -1,17 +1,42 @@
|
|||||||
//
|
//
|
||||||
// AnimationAttach 实现:外部动画资产 → 目标模型并入。
|
// AnimationAttach 实现:外部动画资产 → 目标模型并入。
|
||||||
//
|
//
|
||||||
// 轨道映射两级:
|
// 五段管线(映射表一次构建后**冻结**,逐 clip 只读消费,杜绝循环内
|
||||||
// ① 同名直映——同源拆分资产(角色/动画同一骨架导出)零损耗;
|
// 惰性登记/反向表重建):
|
||||||
// ② Humanoid 槽位重定向——名字不同源的组装资产(角色 FBX +
|
|
||||||
// 动画 FBX 各自制作),经 Unity Avatar 语义的标准人形槽位配对,
|
|
||||||
// 逐 key 烘焙"rest→anim 的增量变换"到目标骨架局部空间:
|
|
||||||
// 平移 out.pos = dstRestPos + (srcPos - srcRestPos)
|
|
||||||
// 旋转 Δ = srcRot * inv(srcRestRot);out.rot = Δ * dstRestRot
|
|
||||||
// (Δ 父空间左乘 = 骨骼自身摆动在两套骨架间等价传递)
|
|
||||||
// 烘焙后 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>
|
#include <Engine/Animation/AnimationAttach.h>
|
||||||
@@ -28,11 +53,15 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine::Animation {
|
namespace Engine::Animation {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
constexpr u32 kNoNode = ~0u;
|
||||||
|
const glm::quat kIdentity(1.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
|
||||||
// mat4 → TRS 分解(rest 局部变换;引擎 node 无 shear)
|
// mat4 → TRS 分解(rest 局部变换;引擎 node 无 shear)
|
||||||
struct TRS {
|
struct TRS {
|
||||||
glm::vec3 pos{};
|
glm::vec3 pos{};
|
||||||
@@ -47,8 +76,7 @@ namespace Engine::Animation {
|
|||||||
const glm::vec3 colX(m[0]);
|
const glm::vec3 colX(m[0]);
|
||||||
const glm::vec3 colY(m[1]);
|
const glm::vec3 colY(m[1]);
|
||||||
const glm::vec3 colZ(m[2]);
|
const glm::vec3 colZ(m[2]);
|
||||||
t.scl = {glm::length(colX), glm::length(colY),
|
t.scl = {glm::length(colX), glm::length(colY), glm::length(colZ)};
|
||||||
glm::length(colZ)};
|
|
||||||
if (t.scl.x > 1e-8f && t.scl.y > 1e-8f && t.scl.z > 1e-8f) {
|
if (t.scl.x > 1e-8f && t.scl.y > 1e-8f && t.scl.z > 1e-8f) {
|
||||||
t.rot = glm::quat_cast(glm::mat3(
|
t.rot = glm::quat_cast(glm::mat3(
|
||||||
colX / t.scl.x, colY / t.scl.y, colZ / t.scl.z));
|
colX / t.scl.x, colY / t.scl.y, colZ / t.scl.z));
|
||||||
@@ -56,14 +84,336 @@ namespace Engine::Animation {
|
|||||||
return t;
|
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
|
} // namespace
|
||||||
|
|
||||||
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
|
AttachResult AttachClipsToNodes(
|
||||||
const std::string& sourcePath)
|
const std::vector<ModelNode>& dstNodes,
|
||||||
|
const std::vector<AnimationClip>& existingClips,
|
||||||
|
ParsedModel&& src, const std::string& sourcePath,
|
||||||
|
std::vector<AnimationClip>& outExtra)
|
||||||
{
|
{
|
||||||
AttachResult res;
|
AttachResult res;
|
||||||
// 幂等:该源文件已并入过(自愈重放/重复 attach 直接返回)
|
// 幂等:该源文件已并入过(自愈重放/重复 attach 直接返回)
|
||||||
for (const auto& c : target.GetClips()) {
|
for (const auto& c : existingClips) {
|
||||||
if (c.sourcePath == sourcePath) {
|
if (c.sourcePath == sourcePath) {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -73,150 +423,215 @@ namespace Engine::Animation {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<ModelNode>& dstNodes = target.GetNodes();
|
|
||||||
const std::vector<ModelNode>& srcNodes = src.nodes;
|
const std::vector<ModelNode>& srcNodes = src.nodes;
|
||||||
|
const Plan pl = BuildPlan(srcNodes, dstNodes);
|
||||||
// ① 同名直映表
|
|
||||||
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 std::string stem =
|
const std::string stem =
|
||||||
std::filesystem::path(sourcePath).stem().string();
|
std::filesystem::path(sourcePath).stem().string();
|
||||||
std::vector<AnimationClip> extra;
|
|
||||||
extra.reserve(src.clips.size());
|
|
||||||
for (auto& clip : src.clips) {
|
for (auto& clip : src.clips) {
|
||||||
AnimationClip out;
|
AnimationClip out;
|
||||||
out.name = clip.name;
|
out.name = UniqueName(clip.name, stem, existingClips, outExtra);
|
||||||
out.duration = clip.duration;
|
out.duration = clip.duration;
|
||||||
out.tps = clip.tps;
|
out.tps = clip.tps;
|
||||||
out.sourcePath = sourcePath;
|
out.sourcePath = sourcePath;
|
||||||
// 同名冲突(内嵌 clip 或其它源):源文件名循环加前缀直到
|
|
||||||
// 唯一。单轮前缀在"前缀名也撞"(如两次 attach 同名资产)时
|
// 每 clip 的 src 轨道索引表(按 nodeIdx)
|
||||||
// 仍会产出重复名——Controller/脚本按 clip 名引用,重名即歧义
|
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) {
|
std::vector<f32> times;
|
||||||
for (const auto& c : target.GetClips()) {
|
for (u32 si = 0; si < srcNodes.size(); ++si) {
|
||||||
if (c.name == n) return true;
|
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) {
|
std::sort(times.begin(), times.end());
|
||||||
if (c.name == n) return true;
|
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;
|
Rw[static_cast<size_t>(i)] = RwI;
|
||||||
};
|
if (jb != jointBakeDst.end()) {
|
||||||
while (collides(out.name)) {
|
baked[i].push_back(
|
||||||
out.name = stem + "_" + out.name;
|
{t, glm::inverse(Rpar) * RwI});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 组装轨道(每 dst 至多一条;owner 在 Plan 阶段已保证)----
|
||||||
|
// 非 const 引用:同名直搬路径整段 move 源轨道(此后 src 不再
|
||||||
|
// 被读——烘焙已在上面完成)
|
||||||
for (auto& ch : clip.channels) {
|
for (auto& ch : clip.channels) {
|
||||||
if (ch.positions.empty() && ch.rotations.empty() &&
|
if (ChannelEmpty(ch)) continue;
|
||||||
ch.scales.empty()) {
|
|
||||||
continue; // 空轨道无用
|
|
||||||
}
|
|
||||||
if (ch.nodeIdx >= srcNodes.size()) {
|
if (ch.nodeIdx >= srcNodes.size()) {
|
||||||
++res.channelsSkipped;
|
++res.channelsSkipped;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const SrcSlot& s = pl.bySrc[ch.nodeIdx];
|
||||||
const std::string& boneName = srcNodes[ch.nodeIdx].name;
|
const std::string& boneName = srcNodes[ch.nodeIdx].name;
|
||||||
|
|
||||||
AnimChannel remapped;
|
if (s.kind == Kind::Skip) {
|
||||||
if (auto it = nameToIdx.find(boneName);
|
++res.channelsSkipped;
|
||||||
it != nameToIdx.end()) {
|
continue;
|
||||||
// ① 同名直搬
|
}
|
||||||
remapped.nodeIdx = it->second;
|
if (s.kind == Kind::Root && ch.nodeIdx != rootSrc) {
|
||||||
remapped.positions = std::move(ch.positions);
|
continue; // 更深的承载节点已承载根运动,其余并入
|
||||||
remapped.rotations = std::move(ch.rotations);
|
}
|
||||||
remapped.scales = std::move(ch.scales);
|
|
||||||
out.channels.push_back(std::move(remapped));
|
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;
|
++res.channelsMatched;
|
||||||
continue;
|
} else {
|
||||||
}
|
// 平移:局部增量 × 尺度
|
||||||
|
const TRS srcRest =
|
||||||
// ② 槽位重定向:增量烘焙到目标骨架局部空间。
|
Decompose(srcNodes[ch.nodeIdx].localTransform);
|
||||||
// 平移增量按骨长比缩放(|dstRest| / |srcRest|)——
|
outCh.positions.reserve(ch.positions.size());
|
||||||
// 两资产单位/比例不同源(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());
|
|
||||||
for (const auto& k : ch.positions) {
|
for (const auto& k : ch.positions) {
|
||||||
remapped.positions.push_back(
|
outCh.positions.push_back(
|
||||||
{k.time, dstRest.pos +
|
{k.time, s.dstRestPos +
|
||||||
(k.value - srcRest.pos) * ratio});
|
(s.M * (k.value - srcRest.pos)) *
|
||||||
|
s.posScale});
|
||||||
}
|
}
|
||||||
remapped.rotations.reserve(ch.rotations.size());
|
// 旋转:烘焙结果(多源汇一 dst 时共享同份)
|
||||||
for (const auto& k : ch.rotations) {
|
if (auto bk = baked.find(s.dst); bk != baked.end()) {
|
||||||
const glm::quat delta =
|
outCh.rotations = bk->second;
|
||||||
k.value * glm::inverse(srcRest.rot);
|
|
||||||
remapped.rotations.push_back(
|
|
||||||
{k.time, delta * dstRest.rot});
|
|
||||||
}
|
}
|
||||||
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;
|
++res.channelsRetargeted;
|
||||||
|
|
||||||
// 明细行(同骨骼多 clip 去重)
|
|
||||||
const bool newRow = std::none_of(
|
const bool newRow = std::none_of(
|
||||||
res.retargetRows.begin(), res.retargetRows.end(),
|
res.retargetRows.begin(), res.retargetRows.end(),
|
||||||
[&](const RetargetRow& r)
|
[&](const RetargetRow& r)
|
||||||
{ return r.srcName == boneName; });
|
{ return r.srcName == boneName; });
|
||||||
if (newRow) {
|
if (newRow) {
|
||||||
|
const HumanoidSlot slot = SlotAt(srcNodes, ch.nodeIdx);
|
||||||
res.retargetRows.push_back(
|
res.retargetRows.push_back(
|
||||||
{HumanoidSlotName(retargetSlot[ch.nodeIdx]),
|
{s.kind == Kind::Root
|
||||||
boneName, dstNodes[mapIt->second].name});
|
? "RootMotion"
|
||||||
|
: HumanoidSlotName(slot),
|
||||||
|
boneName, dstNodes[s.dst].name});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 全部轨道落不上 = 无可驱动骨骼,clip 不入表
|
}
|
||||||
|
|
||||||
if (out.channels.empty()) {
|
if (out.channels.empty()) {
|
||||||
ENGINE_WARN("AttachClipsToModel: clip '{}' has no matching "
|
ENGINE_WARN("AttachClipsToModel: clip '{}' has no matching "
|
||||||
"bones in target (skipped)",
|
"bones in target (skipped)",
|
||||||
clip.name);
|
clip.name);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
extra.push_back(std::move(out));
|
outExtra.push_back(std::move(out));
|
||||||
++res.clipsAdded;
|
++res.clipsAdded;
|
||||||
}
|
}
|
||||||
if (!extra.empty()) {
|
|
||||||
target.AppendClips(std::move(extra));
|
if (!outExtra.empty()) {
|
||||||
}
|
|
||||||
ENGINE_INFO("AttachClipsToModel: '{}' -> {} clips "
|
ENGINE_INFO("AttachClipsToModel: '{}' -> {} clips "
|
||||||
"({} named, {} retargeted, {} skipped)",
|
"({} named, {} retargeted, {} skipped)",
|
||||||
sourcePath, res.clipsAdded, res.channelsMatched,
|
sourcePath, res.clipsAdded, res.channelsMatched,
|
||||||
res.channelsRetargeted, res.channelsSkipped);
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,25 +44,62 @@ namespace Engine::Animation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (r <= 1e-4f || ch.empty()) return;
|
if (r <= 1e-4f || ch.empty()) return;
|
||||||
// 覆盖 v 角度的相邻扇区(角度归一 [0,2π),wrap 补全;方向子
|
// 覆盖 v 角度的相邻扇区(角度归一 [0,2π);方向子 ≤16 个,
|
||||||
// ≤16 个,栈数组零堆分配)
|
// 栈数组零堆分配)
|
||||||
constexpr f32 kTwoPi = 6.2831853f;
|
constexpr f32 kTwoPi = 6.2831853f;
|
||||||
const f32 theta = glm::mod(glm::atan(z, x), kTwoPi);
|
constexpr u32 kMaxDir = 16;
|
||||||
u32 idx[16];
|
u32 idx[kMaxDir];
|
||||||
f32 ang[16];
|
f32 ang[kMaxDir];
|
||||||
u32 nd = 0;
|
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;
|
if (glm::length(ch[i].pos) < 1e-4f) continue;
|
||||||
idx[nd] = i;
|
idx[nd] = i;
|
||||||
ang[nd] = glm::mod(glm::atan(ch[i].pos.y, ch[i].pos.x), kTwoPi);
|
ang[nd] = glm::mod(glm::atan(ch[i].pos.y, ch[i].pos.x), kTwoPi);
|
||||||
++nd;
|
++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) {
|
for (u32 i = 0; i < nd; ++i) {
|
||||||
const u32 j = (i + 1) % nd;
|
const u32 j = (i + 1) % nd;
|
||||||
const f32 ai = ang[i];
|
const f32 ai = ang[i];
|
||||||
const f32 aj = (j == 0) ? ang[0] + kTwoPi : ang[j];
|
if (j == 0) {
|
||||||
if (theta >= ai && theta <= aj) {
|
// wrap 扇区 [ai,2π)∪[0,ang0]:用 || 连接,&& 会让
|
||||||
const f32 span = aj - ai;
|
// 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 t = span > 1e-6f ? (theta - ai) / span : 0.0f;
|
||||||
const f32 wi = (1.0f - t) * r;
|
const f32 wi = (1.0f - t) * r;
|
||||||
const f32 wj = t * r;
|
const f32 wj = t * r;
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ namespace Engine::Animation {
|
|||||||
void AnimationEvaluator::Evaluate(const Model& model,
|
void AnimationEvaluator::Evaluate(const Model& model,
|
||||||
const AnimationClip& cur, f32 curTime,
|
const AnimationClip& cur, f32 curTime,
|
||||||
const CrossfadeState& xf,
|
const CrossfadeState& xf,
|
||||||
std::vector<glm::mat4>& outGlobals)
|
std::vector<glm::mat4>& outGlobals,
|
||||||
|
bool inPlace)
|
||||||
{
|
{
|
||||||
// crossfade 双层 = N 层混合的特例(Direct 模式与 Controller
|
// crossfade 双层 = N 层混合的特例(Direct 模式与 Controller
|
||||||
// 过渡共用同一套加权数学)。两权重和恒为 1,渐进归一化后精确
|
// 过渡共用同一套加权数学)。两权重和恒为 1,渐进归一化后精确
|
||||||
@@ -141,13 +142,14 @@ namespace Engine::Animation {
|
|||||||
layers[1] = {xf.src, xf.srcTime, 1.0f - xf.wCur};
|
layers[1] = {xf.src, xf.srcTime, 1.0f - xf.wCur};
|
||||||
count = 2;
|
count = 2;
|
||||||
}
|
}
|
||||||
EvaluateLayers(model, layers, count, outGlobals);
|
EvaluateLayers(model, layers, count, outGlobals, inPlace);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AnimationEvaluator::EvaluateLayers(const Model& model,
|
void AnimationEvaluator::EvaluateLayers(const Model& model,
|
||||||
const PoseLayer* layers,
|
const PoseLayer* layers,
|
||||||
u32 count,
|
u32 count,
|
||||||
std::vector<glm::mat4>& outGlobals)
|
std::vector<glm::mat4>& outGlobals,
|
||||||
|
bool inPlace)
|
||||||
{
|
{
|
||||||
// ---- 逐层采样(容量跨帧复用;nullptr/零权重层跳过)----
|
// ---- 逐层采样(容量跨帧复用;nullptr/零权重层跳过)----
|
||||||
if (m_Layers.size() < count) {
|
if (m_Layers.size() < count) {
|
||||||
@@ -208,6 +210,24 @@ namespace Engine::Animation {
|
|||||||
acc += m_Sampled[l].weight;
|
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);
|
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);
|
PushPalette(entity, model);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -493,7 +494,8 @@ namespace Engine::Animation {
|
|||||||
m_Evaluator.EvaluateLayers(*model, m_LayerScratch.data(),
|
m_Evaluator.EvaluateLayers(*model, m_LayerScratch.data(),
|
||||||
static_cast<u32>(
|
static_cast<u32>(
|
||||||
m_LayerScratch.size()),
|
m_LayerScratch.size()),
|
||||||
m_Globals);
|
m_Globals,
|
||||||
|
!anim.applyRootMotion);
|
||||||
PushPalette(entity, model);
|
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 <Engine/Animation/HumanoidRig.h>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine::Animation {
|
namespace Engine::Animation {
|
||||||
|
|
||||||
namespace {
|
// ==================================================================
|
||||||
|
// standard:槽位结构化访问
|
||||||
|
// (概念分层:公共 API 在 Engine::Animation 作用域,与头文件声明
|
||||||
|
// 一致;真正的 C++ 内层 namespace 只用于 dialects/mapper 内部表)
|
||||||
|
// ==================================================================
|
||||||
|
|
||||||
// ---- 名字归一化:拆 (body, side) ----
|
Side SideOfSlot(HumanoidSlot slot)
|
||||||
//
|
|
||||||
// 规则:
|
|
||||||
// 1) 去 "mixamorig:" 等前缀
|
|
||||||
// 2) 小写;分隔符(空格/_/.-:)一律删除
|
|
||||||
// 3) left/right(Mixamo "LeftArm")与 左/右(MMD "左腕")统一
|
|
||||||
// 抽成 side,body 里剔除;.L/.R、_L/_R 后缀同上
|
|
||||||
//
|
|
||||||
// body 用**精确相等**匹配关键词(非子串)——"lowerarm"
|
|
||||||
// 不会误命中 "arm"
|
|
||||||
|
|
||||||
Side ExtractSide(std::string& body)
|
|
||||||
{
|
{
|
||||||
Side s = Side::None;
|
const u32 s = static_cast<u32>(slot);
|
||||||
auto cut = [&](std::string_view token, Side which)
|
if (s < 6) return Side::None; // 主干
|
||||||
{
|
return s < 29 ? Side::Left : Side::Right;
|
||||||
size_t pos = 0;
|
|
||||||
while ((pos = body.find(token, pos)) != std::string::npos) {
|
|
||||||
body.erase(pos, token.size());
|
|
||||||
if (s == Side::None) s = which;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
const u32 s = static_cast<u32>(slot);
|
||||||
cut("right", Side::Right);
|
return s < kNames.size() ? kNames[s] : "?";
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ① 前缀剥离 + 小写(分隔符**保留**——后缀式 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" 依赖分隔符存在,必须先于分隔符删除识别)
|
// ".l"/"_l" 依赖分隔符存在,必须先于分隔符删除识别)
|
||||||
std::string NormalizeLower(std::string_view name)
|
std::string NormalizeLower(std::string_view name)
|
||||||
{
|
{
|
||||||
@@ -63,10 +370,11 @@ namespace Engine::Animation {
|
|||||||
c = static_cast<char>(std::tolower(
|
c = static_cast<char>(std::tolower(
|
||||||
static_cast<unsigned char>(c)));
|
static_cast<unsigned char>(c)));
|
||||||
}
|
}
|
||||||
|
FoldFullWidthDigits(s);
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ② 分隔符删除(在 ExtractSide 之后调用;CJK 多字节原样过)
|
// ② 分隔符删除(side 抽取之后调用;CJK 多字节原样过)
|
||||||
std::string StripSeparators(const std::string& s)
|
std::string StripSeparators(const std::string& s)
|
||||||
{
|
{
|
||||||
std::string out;
|
std::string out;
|
||||||
@@ -80,105 +388,99 @@ namespace Engine::Animation {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- body → 左侧槽位 关键词表(右侧 = 左槽 + 段偏移)----
|
// 窄 side 抽取:side 只出现在 Mixamo 前缀(LeftHand...)、
|
||||||
// 精确相等匹配;顺序即优先级(先命中先用)
|
// MMD 前缀(左手首)与 Blender 末尾后缀(..._L/.L)。
|
||||||
struct BodyRule {
|
// 不用任意位置 cut——Rigify "f_ring" 中间的 "_r" 会被误当
|
||||||
std::string_view body;
|
// 右侧后缀删掉(body 变 "fing" → Ring 指骨全军覆没的历史
|
||||||
HumanoidSlot left;
|
// 陷阱),末尾匹配天然规避
|
||||||
};
|
Side ExtractSideNarrow(std::string& body)
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
if (side != Side::Right) return leftSlot;
|
Side s = Side::None;
|
||||||
const u32 s = static_cast<u32>(leftSlot);
|
if (body.rfind("left", 0) == 0) {
|
||||||
const u32 legStart = static_cast<u32>(HumanoidSlot::LeftUpperLeg);
|
body.erase(0, 4);
|
||||||
return static_cast<HumanoidSlot>(s + (s >= legStart ? 3 : 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
|
// 宽 side 抽取(fallback):任意位置删 token,兜底命名习惯
|
||||||
|
// 不在首尾的资产。窄规则先行,本函数只在查表失败后启用
|
||||||
const char* HumanoidSlotName(HumanoidSlot slot)
|
Side ExtractSideWide(std::string& body)
|
||||||
{
|
{
|
||||||
static constexpr std::array<const char*, 20> kNames{
|
Side s = Side::None;
|
||||||
"Hips", "Spine", "Chest", "UpperChest", "Neck", "Head",
|
auto cut = [&](std::string_view token, Side which)
|
||||||
"LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand",
|
{
|
||||||
"RightShoulder", "RightUpperArm", "RightLowerArm", "RightHand",
|
size_t pos = 0;
|
||||||
"LeftUpperLeg", "LeftLowerLeg", "LeftFoot",
|
while ((pos = body.find(token, pos)) != std::string::npos) {
|
||||||
"RightUpperLeg", "RightLowerLeg", "RightFoot",
|
body.erase(pos, token.size());
|
||||||
|
if (s == Side::None) s = which;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const u32 s = static_cast<u32>(slot);
|
cut("left", Side::Left);
|
||||||
return s < kNames.size() ? kNames[s] : "?";
|
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,
|
HumanoidSlot MatchHumanoidSlot(std::string_view name, Side& side,
|
||||||
bool& ok)
|
bool& ok)
|
||||||
{
|
{
|
||||||
side = Side::None;
|
side = Side::None;
|
||||||
ok = false;
|
ok = false;
|
||||||
std::string body = NormalizeLower(name);
|
const std::string lower = mapper::NormalizeLower(name);
|
||||||
side = ExtractSide(body);
|
for (int pass = 0; pass < 2; ++pass) {
|
||||||
body = StripSeparators(body);
|
std::string body = lower;
|
||||||
if (body.empty()) return HumanoidSlot::Hips;
|
const Side s = pass == 0
|
||||||
|
? mapper::ExtractSideNarrow(body)
|
||||||
for (const BodyRule& r : kBodyRules) {
|
: mapper::ExtractSideWide(body);
|
||||||
if (body == r.body) {
|
body = mapper::StripSeparators(body);
|
||||||
|
if (body.empty()) {
|
||||||
|
return HumanoidSlot::Hips;
|
||||||
|
}
|
||||||
|
HumanoidSlot slot;
|
||||||
|
if (dialects::Lookup(body, slot)) {
|
||||||
|
side = s;
|
||||||
ok = true;
|
ok = true;
|
||||||
return ToSide(r.left, side);
|
// 表存左槽名:右 side 经镜像换槽(主干槽镜像原样)
|
||||||
|
return s == Side::Right ? MirrorSlot(slot) : slot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return HumanoidSlot::Hips;
|
return HumanoidSlot::Hips;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跨骨架自动映射:同名直映 + 52 槽一对一配对(指/趾骨同为主干
|
||||||
|
// 槽位走统一通道,无独立特判)
|
||||||
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
|
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
|
||||||
const std::vector<ModelNode>& srcNodes,
|
const std::vector<ModelNode>& srcNodes,
|
||||||
const std::vector<ModelNode>& dstNodes)
|
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);
|
constexpr u32 kSlotCount = static_cast<u32>(HumanoidSlot::Count);
|
||||||
auto slotOf = [](const std::vector<ModelNode>& nodes,
|
auto slotOf = [](const std::vector<ModelNode>& nodes,
|
||||||
std::array<i32, kSlotCount>& out)
|
std::array<i32, kSlotCount>& out)
|
||||||
@@ -218,7 +520,7 @@ namespace Engine::Animation {
|
|||||||
}
|
}
|
||||||
const u32 s = static_cast<u32>(slot);
|
const u32 s = static_cast<u32>(slot);
|
||||||
if (out[s] < 0) {
|
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;
|
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
|
} // namespace Engine::Animation
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include <Engine/Core/Window.h>
|
#include <Engine/Core/Window.h>
|
||||||
#include <Engine/Core/Input.h>
|
#include <Engine/Core/Input.h>
|
||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
|
#include <Engine/Core/LogRing.h>
|
||||||
|
#include <Engine/Core/Stats.h>
|
||||||
#include <Engine/ECS/Scene.h>
|
#include <Engine/ECS/Scene.h>
|
||||||
#include <Engine/Physics/PhysicsWorld.h>
|
#include <Engine/Physics/PhysicsWorld.h>
|
||||||
#include <Engine/Scripting/ScriptRuntime.h>
|
#include <Engine/Scripting/ScriptRuntime.h>
|
||||||
@@ -112,6 +114,12 @@ namespace Engine {
|
|||||||
|
|
||||||
void Application::Run()
|
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");
|
ENGINE_INFO("Application starting");
|
||||||
|
|
||||||
m_Running = true;
|
m_Running = true;
|
||||||
@@ -133,6 +141,8 @@ namespace Engine {
|
|||||||
while (m_Running) {
|
while (m_Running) {
|
||||||
const auto frameStart = std::chrono::steady_clock::now();
|
const auto frameStart = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
STATS.BeginFrame(); // 帧内资源分配清零(面板帧流失真修正)
|
||||||
|
|
||||||
m_Window->PollEvents();
|
m_Window->PollEvents();
|
||||||
|
|
||||||
auto now = std::chrono::steady_clock::now();
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Created by atdunbg on 2026/9/20.
|
// Created by atdunbg on 2026/9/20.
|
||||||
//
|
//
|
||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
|
#include <Engine/Core/LogRing.h>
|
||||||
|
|
||||||
#include "spdlog/sinks/basic_file_sink.h"
|
#include "spdlog/sinks/basic_file_sink.h"
|
||||||
#include "spdlog/sinks/stdout_color_sinks.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);
|
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");
|
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
|
// 多sink 组合默认 logger
|
||||||
|
|
||||||
// auto logger = std::make_shared<spdlog::logger>("hello", spdlog::sinks_init_list{console, file});
|
// 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->set_level(spdlog::level::trace);
|
||||||
logger->flush_on(spdlog::level::warn); // warn 及以上立即 flush,避免崩溃丢日志
|
logger->flush_on(spdlog::level::warn); // warn 及以上立即 flush,避免崩溃丢日志
|
||||||
|
|
||||||
|
|||||||
+16
-196
@@ -4,10 +4,6 @@
|
|||||||
#include <Engine/Core/Stats.h>
|
#include <Engine/Core/Stats.h>
|
||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
#include <sstream>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace Engine
|
namespace Engine
|
||||||
{
|
{
|
||||||
Stats& Stats::Get()
|
Stats& Stats::Get()
|
||||||
@@ -125,200 +121,24 @@ namespace Engine
|
|||||||
m_TotalDeallocation += bytes;
|
m_TotalDeallocation += bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 进程内存采样 ============
|
// ============ 只读快照 ============
|
||||||
|
|
||||||
Stats::ProcessMemory Stats::ReadProcessMemory() const
|
Stats::Snapshot Stats::TakeSnapshot() 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()
|
|
||||||
{
|
{
|
||||||
std::lock_guard lock(m_Mutex);
|
std::lock_guard lock(m_Mutex);
|
||||||
m_ProcessMem = ReadProcessMemory();
|
Snapshot s;
|
||||||
}
|
s.textureCount = m_TextureCount;
|
||||||
|
s.textureBytes = m_TextureBytes;
|
||||||
// ============ 打印 ============
|
s.meshCount = m_MeshCount;
|
||||||
|
s.meshBytes = m_MeshBytes;
|
||||||
static double MB(const u64 bytes) { return bytes / (1024.0 * 1024.0); }
|
s.bufferCount = m_BufferCount;
|
||||||
|
s.bufferBytes = m_BufferBytes;
|
||||||
void Stats::Print() const
|
s.memoryBlockBytes = m_MemoryBlockBytes;
|
||||||
{
|
s.memoryUsedBytes = m_MemoryUsedBytes;
|
||||||
std::lock_guard lock(m_Mutex);
|
s.frameAllocation = m_FrameAllocation;
|
||||||
|
s.frameDeallocation = m_FrameDeallocation;
|
||||||
const auto proc = ReadProcessMemory();
|
s.totalAllocation = m_TotalAllocation;
|
||||||
|
s.totalDeallocation = m_TotalDeallocation;
|
||||||
ENGINE_INFO("========== Resource Stats ==========");
|
return s;
|
||||||
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("================================================");
|
|
||||||
}
|
}
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -23,10 +23,14 @@ namespace Engine {
|
|||||||
constexpr i32 kKeyUp = 265, kKeyDown = 264,
|
constexpr i32 kKeyUp = 265, kKeyDown = 264,
|
||||||
kKeyLeft = 263, kKeyRight = 262;
|
kKeyLeft = 263, kKeyRight = 262;
|
||||||
constexpr i32 kKeySpace = 32;
|
constexpr i32 kKeySpace = 32;
|
||||||
|
constexpr i32 kKeyLShift = 340; // GLFW_KEY_LEFT_SHIFT
|
||||||
// 手柄标准布局
|
// 手柄标准布局
|
||||||
constexpr i32 kPadBtnA = 0; // GLFW_GAMEPAD_BUTTON_A
|
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 kPadAxisLeftX = 0; // GLFW_GAMEPAD_AXIS_LEFT_X
|
||||||
constexpr i32 kPadAxisLeftY = 1; // GLFW_GAMEPAD_AXIS_LEFT_Y
|
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;
|
constexpr f32 kRadialDeadZone = 0.15f;
|
||||||
|
|
||||||
@@ -139,6 +143,19 @@ namespace Engine {
|
|||||||
const size_t jump = DefineAction("Jump", ActionKind::Button);
|
const size_t jump = DefineAction("Jump", ActionKind::Button);
|
||||||
Bind(jump, {SourceKind::Key, kKeySpace, 1.0f, kX});
|
Bind(jump, {SourceKind::Key, kKeySpace, 1.0f, kX});
|
||||||
Bind(jump, {SourceKind::GamepadButton, kPadBtnA, 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(
|
s.mShape = new JPH::CapsuleShape(
|
||||||
std::max(params.radius, 0.01f),
|
std::max(params.radius, 0.01f),
|
||||||
std::max(params.halfHeight, 0.0f));
|
std::max(params.halfHeight, 0.0f));
|
||||||
s.mShapeOffset = JPH::Vec3(0.0f,
|
s.mShapeOffset = JPH::Vec3(
|
||||||
params.halfHeight + params.radius, 0.0f);
|
params.shapeOffset.x,
|
||||||
|
params.shapeOffset.y + params.halfHeight + params.radius,
|
||||||
|
params.shapeOffset.z);
|
||||||
|
|
||||||
m_Character = new JPH::CharacterVirtual(
|
m_Character = new JPH::CharacterVirtual(
|
||||||
&s, JPH::RVec3(position.x, position.y, position.z),
|
&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);
|
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,
|
void CharacterController::Update(const f32 dt, const JPH::Vec3 &gravity,
|
||||||
JPH::TempAllocator &allocator) {
|
JPH::TempAllocator &allocator) {
|
||||||
// 重力积分:着地时清掉向下分量(防贴地累积穿透速度),悬空时累加
|
// 重力积分:着地时清掉向下分量(防贴地累积穿透速度),悬空时累加
|
||||||
@@ -50,6 +66,10 @@ namespace Engine {
|
|||||||
m_Character->SetLinearVelocity(v + gravity * dt);
|
m_Character->SetLinearVelocity(v + gravity * dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 扫掠前速度快照:撞墙判定用。ExtendedUpdate 会抵消朝向陡面/
|
||||||
|
// 墙面的速度分量,扫掠后再读就归零了(见头文件说明)
|
||||||
|
m_SweepVelocity = m_Character->GetLinearVelocity();
|
||||||
|
|
||||||
// 扫掠推进(撞墙滑动 + 斜坡限制 + StickToFloor 贴地 + WalkStairs
|
// 扫掠推进(撞墙滑动 + 斜坡限制 + StickToFloor 贴地 + WalkStairs
|
||||||
// 上台阶)。过滤器默认全放行:角色与静/动层全碰
|
// 上台阶)。过滤器默认全放行:角色与静/动层全碰
|
||||||
m_Character->ExtendedUpdate(dt, gravity, m_Ext,
|
m_Character->ExtendedUpdate(dt, gravity, m_Ext,
|
||||||
@@ -78,6 +98,16 @@ namespace Engine {
|
|||||||
return m_Character->IsSupported();
|
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 {
|
glm::vec3 CharacterController::GetPosition() const {
|
||||||
const JPH::RVec3 p = m_Character->GetPosition();
|
const JPH::RVec3 p = m_Character->GetPosition();
|
||||||
return {static_cast<f32>(p.GetX()), static_cast<f32>(p.GetY()),
|
return {static_cast<f32>(p.GetX()), static_cast<f32>(p.GetY()),
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ namespace Engine {
|
|||||||
// 直接成员会先于 call_once 构造 → 函数指针未注册 → SIGSEGV at 0x0
|
// 直接成员会先于 call_once 构造 → 函数指针未注册 → SIGSEGV at 0x0
|
||||||
m_DebugDraw = CreateScope<PhysicsDebugDraw>();
|
m_DebugDraw = CreateScope<PhysicsDebugDraw>();
|
||||||
|
|
||||||
|
// 角色间碰撞互查表(内部 Array 走 JPH 分配器,须在 call_once 之后)
|
||||||
|
m_VsCharacterCollision =
|
||||||
|
CreateScope<JPH::CharacterVsCharacterCollisionSimple>();
|
||||||
|
|
||||||
m_TempAllocator = CreateScope<JPH::TempAllocatorImpl>(16 * 1024 * 1024);
|
m_TempAllocator = CreateScope<JPH::TempAllocatorImpl>(16 * 1024 * 1024);
|
||||||
const u32 threads = std::max(1u, std::thread::hardware_concurrency() - 1);
|
const u32 threads = std::max(1u, std::thread::hardware_concurrency() - 1);
|
||||||
m_JobSystem = CreateScope<JPH::JobSystemThreadPool>(
|
m_JobSystem = CreateScope<JPH::JobSystemThreadPool>(
|
||||||
@@ -208,6 +212,7 @@ namespace Engine {
|
|||||||
// 释放烘焙几何——必须先于 UnregisterTypes
|
// 释放烘焙几何——必须先于 UnregisterTypes
|
||||||
m_DebugDraw.reset();
|
m_DebugDraw.reset();
|
||||||
m_Bodies.clear();
|
m_Bodies.clear();
|
||||||
|
m_Characters.clear(); // 角色析构回 m_VsCharacterCollision 摘除,先于 Jolt 反注册
|
||||||
m_ContactListener.reset(); // 监听器先于 system 析构(持 m_Owner 引用)
|
m_ContactListener.reset(); // 监听器先于 system 析构(持 m_Owner 引用)
|
||||||
m_System.reset();
|
m_System.reset();
|
||||||
// 单世界假设:进程退出时统一反注册(多世界时改为引用计数)
|
// 单世界假设:进程退出时统一反注册(多世界时改为引用计数)
|
||||||
@@ -695,20 +700,24 @@ namespace Engine {
|
|||||||
p.mass = cc.mass;
|
p.mass = cc.mass;
|
||||||
p.maxSlopeAngle = cc.maxSlopeAngle;
|
p.maxSlopeAngle = cc.maxSlopeAngle;
|
||||||
p.stepHeight = cc.stepHeight;
|
p.stepHeight = cc.stepHeight;
|
||||||
|
p.shapeOffset = cc.shapeOffset;
|
||||||
|
|
||||||
auto found = m_Characters.find(bits);
|
auto found = m_Characters.find(bits);
|
||||||
if (found == m_Characters.end()) {
|
if (found == m_Characters.end()) {
|
||||||
CharacterEntry entry;
|
CharacterEntry entry;
|
||||||
entry.cc = std::make_unique<CharacterController>(
|
entry.cc = std::make_unique<CharacterController>(
|
||||||
*m_System, p, tr.GetWorldPosition());
|
*m_System, p, tr.GetWorldPosition());
|
||||||
|
entry.cc->SetCharacterCollision(*m_VsCharacterCollision);
|
||||||
entry.lastPos = tr.GetWorldPosition();
|
entry.lastPos = tr.GetWorldPosition();
|
||||||
m_Characters.emplace(bits, std::move(entry));
|
m_Characters.emplace(bits, std::move(entry));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (found->second.cc->GetParams() != p) {
|
if (found->second.cc->GetParams() != p) {
|
||||||
// 参数变更 → 重建(位置沿用当前 Transform)
|
// 参数变更 → 重建(位置沿用当前 Transform);旧实例析构
|
||||||
|
// 自动摘除旧条目,新实例重新登记
|
||||||
found->second.cc = std::make_unique<CharacterController>(
|
found->second.cc = std::make_unique<CharacterController>(
|
||||||
*m_System, p, tr.GetWorldPosition());
|
*m_System, p, tr.GetWorldPosition());
|
||||||
|
found->second.cc->SetCharacterCollision(*m_VsCharacterCollision);
|
||||||
found->second.lastPos = tr.GetWorldPosition();
|
found->second.lastPos = tr.GetWorldPosition();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -742,6 +751,42 @@ namespace Engine {
|
|||||||
static_cast<entt::entity>(bits), p,
|
static_cast<entt::entity>(bits), p,
|
||||||
tr->GetWorldRotationQuat(), tr->GetWorldScale());
|
tr->GetWorldRotationQuat(), tr->GetWorldScale());
|
||||||
entry.lastPos = p;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1124,6 +1169,14 @@ namespace Engine {
|
|||||||
: glm::vec3{0.0f};
|
: 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)----
|
// ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)----
|
||||||
|
|
||||||
void PhysicsWorld::Writeback() {
|
void PhysicsWorld::Writeback() {
|
||||||
|
|||||||
@@ -88,10 +88,17 @@ namespace Engine
|
|||||||
",e:" + std::to_string(params.emissiveFactor.x) +
|
",e:" + std::to_string(params.emissiveFactor.x) +
|
||||||
"," + std::to_string(params.emissiveFactor.y) +
|
"," + std::to_string(params.emissiveFactor.y) +
|
||||||
"," + std::to_string(params.emissiveFactor.z);
|
"," + std::to_string(params.emissiveFactor.z);
|
||||||
// per-slot UV 变换 + unlit 入键:同一张贴图配不同变换是不同材质
|
// per-slot UV 变换 + unlit + UV 镜像入键:同一张贴图配不同变换是
|
||||||
//(KHR_texture_transform 测试模型:一张棋盘格被 82 个材质以不同
|
// 不同材质(KHR_texture_transform 测试模型:一张棋盘格被 82 个材质
|
||||||
// 变换共享,键漏了会串成同一份)
|
// 以不同变换共享,键漏了会串成同一份)
|
||||||
key += ",m:" + std::to_string(params.misc.x);
|
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) {
|
for (u32 slot = 0; slot < kSlotCount; ++slot) {
|
||||||
key += ",u" + std::to_string(slot) + ":" +
|
key += ",u" + std::to_string(slot) + ":" +
|
||||||
std::to_string(params.uvA[slot].x) + "," +
|
std::to_string(params.uvA[slot].x) + "," +
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ namespace Engine
|
|||||||
EmitVec4(out, data.params.emissiveFactor);
|
EmitVec4(out, data.params.emissiveFactor);
|
||||||
out << YAML::Key << "Misc" << YAML::Value;
|
out << YAML::Key << "Misc" << YAML::Value;
|
||||||
EmitVec4(out, data.params.misc);
|
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
|
// per-slot UV 变换(KHR_texture_transform):6 槽各 A(4 分量) + T
|
||||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||||
out << YAML::Key << ("UvA" + std::to_string(slot)) << YAML::Value;
|
out << YAML::Key << ("UvA" + std::to_string(slot)) << YAML::Value;
|
||||||
@@ -156,6 +158,8 @@ namespace Engine
|
|||||||
m["EmissiveFactor"], MaterialParams{}.emissiveFactor);
|
m["EmissiveFactor"], MaterialParams{}.emissiveFactor);
|
||||||
out.params.misc = ReadVec4(
|
out.params.misc = ReadVec4(
|
||||||
m["Misc"], MaterialParams{}.misc);
|
m["Misc"], MaterialParams{}.misc);
|
||||||
|
out.params.alphaOverlay = ReadVec4(
|
||||||
|
m["AlphaOverlay"], MaterialParams{}.alphaOverlay);
|
||||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||||
out.params.uvA[slot] = ReadVec4(
|
out.params.uvA[slot] = ReadVec4(
|
||||||
m["UvA" + std::to_string(slot)],
|
m["UvA" + std::to_string(slot)],
|
||||||
|
|||||||
+912
-70
File diff suppressed because it is too large
Load Diff
@@ -529,6 +529,10 @@ namespace Engine::Serialization {
|
|||||||
<< cc->stepHeight;
|
<< cc->stepHeight;
|
||||||
out << YAML::Key << "GravityScale" << YAML::Value
|
out << YAML::Key << "GravityScale" << YAML::Value
|
||||||
<< cc->gravityScale;
|
<< cc->gravityScale;
|
||||||
|
out << YAML::Key << "ShapeOffset" << YAML::Value
|
||||||
|
<< YAML::Flow << YAML::BeginSeq << cc->shapeOffset.x
|
||||||
|
<< cc->shapeOffset.y << cc->shapeOffset.z
|
||||||
|
<< YAML::EndSeq;
|
||||||
out << YAML::Key << "Active" << YAML::Value << cc->active;
|
out << YAML::Key << "Active" << YAML::Value << cc->active;
|
||||||
out << YAML::EndMap;
|
out << YAML::EndMap;
|
||||||
}
|
}
|
||||||
@@ -681,6 +685,32 @@ namespace Engine::Serialization {
|
|||||||
it.as<std::string>(""));
|
it.as<std::string>(""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 外部动画并入(幂等重放):动画 clip 本体不落盘,只在
|
||||||
|
// 此按 Attached 列表把动画容器并入共享 Model 资产。过去
|
||||||
|
// 仅由 Inspector 选中实体时重放 → 不点击实体/开局直跑
|
||||||
|
// 就永远不加载。放在共享的 ApplyComponents 里,场景加载
|
||||||
|
// 与 Prefab 实例化两条路径同时覆盖。
|
||||||
|
// 时序保证:MeshRenderer 分支(上方)先于本分支解析,
|
||||||
|
// 故此处 mr.model 已 resolve 就绪
|
||||||
|
if (!a.attachedAnimPaths.empty() &&
|
||||||
|
handle.HasComponent<ECS::MeshRendererComponent>()) {
|
||||||
|
const auto& mr =
|
||||||
|
handle.GetComponent<ECS::MeshRendererComponent>();
|
||||||
|
if (Model* model =
|
||||||
|
mr.model.Valid() ? assets.GetModel(mr.model)
|
||||||
|
: nullptr) {
|
||||||
|
for (const auto& p : a.attachedAnimPaths) {
|
||||||
|
const bool done = std::any_of(
|
||||||
|
model->GetClips().begin(),
|
||||||
|
model->GetClips().end(),
|
||||||
|
[&](const AnimationClip& c)
|
||||||
|
{ return c.sourcePath == p; });
|
||||||
|
if (!done) {
|
||||||
|
assets.AttachAnimationClips(mr.model, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const YAML::Node n = comps["Camera"]) {
|
if (const YAML::Node n = comps["Camera"]) {
|
||||||
@@ -798,6 +828,11 @@ namespace Engine::Serialization {
|
|||||||
cc.maxSlopeAngle = n["MaxSlopeAngle"].as<f32>(cc.maxSlopeAngle);
|
cc.maxSlopeAngle = n["MaxSlopeAngle"].as<f32>(cc.maxSlopeAngle);
|
||||||
cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight);
|
cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight);
|
||||||
cc.gravityScale = n["GravityScale"].as<f32>(cc.gravityScale);
|
cc.gravityScale = n["GravityScale"].as<f32>(cc.gravityScale);
|
||||||
|
if (const YAML::Node so = n["ShapeOffset"]) {
|
||||||
|
cc.shapeOffset = glm::vec3(so[0].as<f32>(0.0f),
|
||||||
|
so[1].as<f32>(0.0f),
|
||||||
|
so[2].as<f32>(0.0f));
|
||||||
|
}
|
||||||
cc.active = n["Active"].as<bool>(true);
|
cc.active = n["Active"].as<bool>(true);
|
||||||
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
|
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,55 @@
|
|||||||
|
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "Engine/Core/Stats.h"
|
#include "Engine/Core/Stats.h"
|
||||||
|
|
||||||
namespace Engine {
|
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
|
namespace
|
||||||
{
|
{
|
||||||
// props → VMA 创建参数:
|
// props → VMA 创建参数:
|
||||||
@@ -88,6 +132,7 @@ namespace Engine {
|
|||||||
out.MappedPtr = info.pMappedData; // 非 MAPPED 标记时为 null
|
out.MappedPtr = info.pMappedData; // 非 MAPPED 标记时为 null
|
||||||
|
|
||||||
STATS.AddMemoryUsed(size);
|
STATS.AddMemoryUsed(size);
|
||||||
|
STATS.AddMemoryBlock(size); // 1:1 VMA 分配:块 == 占用(无子分配)
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,11 +153,15 @@ namespace Engine {
|
|||||||
|
|
||||||
Allocation out;
|
Allocation out;
|
||||||
out.Image = image;
|
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.Handle = handle;
|
||||||
out.Valid = true;
|
out.Valid = true;
|
||||||
|
|
||||||
STATS.AddMemoryUsed(out.Size);
|
STATS.AddMemoryUsed(out.Size);
|
||||||
|
STATS.AddMemoryBlock(out.Size);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +177,7 @@ namespace Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
STATS.RemoveMemoryUsed(alloc.Size);
|
STATS.RemoveMemoryUsed(alloc.Size);
|
||||||
|
STATS.RemoveMemoryBlock(alloc.Size);
|
||||||
alloc = {};
|
alloc = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,19 @@ namespace Engine
|
|||||||
|
|
||||||
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb)
|
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_Width = width;
|
||||||
m_Height = height;
|
m_Height = height;
|
||||||
m_Format = srgb ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm;
|
m_Format = srgb ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm;
|
||||||
@@ -299,8 +312,12 @@ namespace Engine
|
|||||||
m_Device.GetDevice().destroyCommandPool(pool);
|
m_Device.GetDevice().destroyCommandPool(pool);
|
||||||
m_Allocator.Destroy(stagingAlloc);
|
m_Allocator.Destroy(stagingAlloc);
|
||||||
|
|
||||||
// 显存占用 = 宽 * 高 * 4 字节(RGBA8)
|
// 显存占用按真实格式 + mip 链估算(与分配器台账同一算法)
|
||||||
m_TextureBytes = static_cast<u64>(m_Width) * m_Height * 4;
|
if (m_TextureBytes != 0) {
|
||||||
|
STATS.RemoveTexture(m_TextureBytes); // 重建防双计
|
||||||
|
}
|
||||||
|
m_TextureBytes = EstimateImageBytes(m_Format, m_Width, m_Height, 1,
|
||||||
|
m_MipLevels);
|
||||||
STATS.AddTexture(m_TextureBytes);
|
STATS.AddTexture(m_TextureBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +509,9 @@ namespace Engine
|
|||||||
m_Device.GetDevice().destroyCommandPool(pool);
|
m_Device.GetDevice().destroyCommandPool(pool);
|
||||||
m_Allocator.Destroy(stagingAlloc);
|
m_Allocator.Destroy(stagingAlloc);
|
||||||
|
|
||||||
|
if (m_TextureBytes != 0) {
|
||||||
|
STATS.RemoveTexture(m_TextureBytes); // 重建防双计
|
||||||
|
}
|
||||||
m_TextureBytes = total;
|
m_TextureBytes = total;
|
||||||
STATS.AddTexture(m_TextureBytes);
|
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,19 +45,25 @@ layout(set = 1, binding = 6) uniform MaterialParams {
|
|||||||
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
||||||
vec4 baseColor; // glTF baseColorFactor:rgb 调色 albedo,a = BLEND 透明度
|
vec4 baseColor; // glTF baseColorFactor:rgb 调色 albedo,a = BLEND 透明度
|
||||||
vec4 surface; // x = ior(F0 换算),y = transmission(不透明段恒 0 语义),z = 自发光强度,w = 体积厚度
|
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 emissiveFactor; // 自发光颜色(rgb × emissiveMap × surface.z;黑 = 不发光)
|
||||||
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22),槽序 = binding
|
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22),槽序 = binding
|
||||||
vec4 uvT[6]; // 每槽 UV 平移 (tx, ty, 0, 0)
|
vec4 uvT[6]; // 每槽 UV 平移 (tx, ty, 0, 0)
|
||||||
|
vec4 alphaOverlay; // rgb = albedo alpha 叠加色,a = 强度(0 = 关闭)
|
||||||
} matParams;
|
} matParams;
|
||||||
|
|
||||||
// 每槽 UV 变换:uv' = A·uv + t(identity 时退化为直通)。slot 必须
|
// 每槽 UV 变换:uv' = A·uv + t(identity 时退化为直通)。slot 必须
|
||||||
// 字面量调用点(编译期展开索引);6 = Material::kSlotCount
|
// 字面量调用点(编译期展开索引);6 = Material::kSlotCount
|
||||||
|
// 末尾叠加材质级镜像(misc.y = 左右,misc.z = 上下),作用全部槽:
|
||||||
|
// 纠正源资产 UV 朝向(部分角色 rip 与 glTF 上左原点约定相反)
|
||||||
vec2 ApplyUv(int slot, vec2 uv) {
|
vec2 ApplyUv(int slot, vec2 uv) {
|
||||||
vec4 a = matParams.uvA[slot];
|
vec4 a = matParams.uvA[slot];
|
||||||
vec4 t = matParams.uvT[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);
|
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)
|
// CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
|
||||||
@@ -154,9 +160,13 @@ void main() {
|
|||||||
// alphaMode=MASK 镂空(表针/栅栏等硬边贴图):低于阈值的纹素直接丢弃
|
// alphaMode=MASK 镂空(表针/栅栏等硬边贴图):低于阈值的纹素直接丢弃
|
||||||
if (matParams.factors.w > 0.5 && albedoTex.a < 0.5) discard;
|
if (matParams.factors.w > 0.5 && albedoTex.a < 0.5) discard;
|
||||||
vec3 albedo = albedoTex.rgb * matParams.baseColor.rgb;
|
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 直出
|
// KHR_materials_unlit:无光照语义(文字标签/示意图),albedo 直出
|
||||||
if (matParams.misc.x > 0.5) {
|
if (matParams.misc.x > 0.5) {
|
||||||
outColor = vec4(albedo, albedoTex.a * matParams.baseColor.a);
|
outColor = vec4(albedo, 1.0);
|
||||||
outVelocity = fragVelocity;
|
outVelocity = fragVelocity;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -259,9 +269,11 @@ void main() {
|
|||||||
vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb
|
vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb
|
||||||
* mix(0.4, 1.0, shade0); // 阴影区环境同步衰减(简化遮蔽)
|
* mix(0.4, 1.0, shade0); // 阴影区环境同步衰减(简化遮蔽)
|
||||||
|
|
||||||
// alpha = albedo 贴图覆盖率 × baseColorFactor.a(glTF alphaMode=BLEND
|
// 不透明段覆盖率恒 1:本 pass 只画 alphaMode=OPAQUE/MASK 表面,像素必
|
||||||
// 混合源);不透明资产两者恒 1 行为不变。预乘在 tonemap 段完成。
|
// 被完全覆盖。绝不能拿 albedo 贴图 alpha 当覆盖率——HI3/Genshin 脸部
|
||||||
outColor = vec4(ambient + Lo + emissive, albedoTex.a * matParams.baseColor.a);
|
// 贴图的 alpha 是腮红/卧蚕遮罩(大面积 0),写进 HDR.a 后会被 tonemap
|
||||||
|
// 的预乘 (rgb * a) 拉成纯黑。贴图 alpha 的细节用法见 alphaOverlay。
|
||||||
|
outColor = vec4(ambient + Lo + emissive, 1.0);
|
||||||
// ==== DEBUG B1: 纯 albedo 直出(验证贴图绑定/UV 采样)====
|
// ==== DEBUG B1: 纯 albedo 直出(验证贴图绑定/UV 采样)====
|
||||||
// outColor = vec4(albedo, 1.0);
|
// outColor = vec4(albedo, 1.0);
|
||||||
outVelocity = fragVelocity;
|
outVelocity = fragVelocity;
|
||||||
|
|||||||
@@ -45,18 +45,23 @@ layout(set = 1, binding = 6) uniform MaterialParams {
|
|||||||
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
||||||
vec4 baseColor; // glTF baseColorFactor:rgb 调色 albedo,a = BLEND 透明度
|
vec4 baseColor; // glTF baseColorFactor:rgb 调色 albedo,a = BLEND 透明度
|
||||||
vec4 surface; // x = ior,y = transmission(KHR_materials_transmission),z = 自发光强度,w = 体积厚度
|
vec4 surface; // x = ior,y = transmission(KHR_materials_transmission),z = 自发光强度,w = 体积厚度
|
||||||
vec4 misc; // x = unlit(透射材质不会是 unlit,仅为与 simple_shader 布局一致)
|
vec4 misc; // x = unlit(透射材质不会是 unlit,仅为与 simple_shader 布局一致),y = UV 左右镜像,z = UV 上下镜像
|
||||||
vec4 emissiveFactor; // 自发光颜色(rgb × emissiveMap × surface.z;黑 = 不发光)
|
vec4 emissiveFactor; // 自发光颜色(rgb × emissiveMap × surface.z;黑 = 不发光)
|
||||||
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22),槽序 = binding
|
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22),槽序 = binding
|
||||||
vec4 uvT[6]; // 每槽 UV 平移 (tx, ty, 0, 0)
|
vec4 uvT[6]; // 每槽 UV 平移 (tx, ty, 0, 0)
|
||||||
|
vec4 alphaOverlay; // rgb = albedo alpha 叠加色,a = 强度(0 = 关闭)
|
||||||
} matParams;
|
} matParams;
|
||||||
|
|
||||||
// 每槽 UV 变换:uv' = A·uv + t(与 simple_shader.frag 同款,6 = kSlotCount)
|
// 每槽 UV 变换:uv' = A·uv + t(与 simple_shader.frag 同款,6 = kSlotCount)
|
||||||
|
// 末尾叠加材质级镜像(misc.y = 左右,misc.z = 上下),作用全部槽
|
||||||
vec2 ApplyUv(int slot, vec2 uv) {
|
vec2 ApplyUv(int slot, vec2 uv) {
|
||||||
vec4 a = matParams.uvA[slot];
|
vec4 a = matParams.uvA[slot];
|
||||||
vec4 t = matParams.uvT[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);
|
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)
|
// CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
|
||||||
@@ -152,6 +157,9 @@ vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
|
|||||||
void main() {
|
void main() {
|
||||||
vec4 albedoTex = texture(albedoMap, ApplyUv(0, fragUV));
|
vec4 albedoTex = texture(albedoMap, ApplyUv(0, fragUV));
|
||||||
vec3 albedo = albedoTex.rgb * matParams.baseColor.rgb;
|
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));
|
vec4 mr = texture(metallicRoughnessMap, ApplyUv(2, fragUV));
|
||||||
// 粗糙度源按 factors.z 切换:glTF 合图工作流读 MR.g;
|
// 粗糙度源按 factors.z 切换:glTF 合图工作流读 MR.g;
|
||||||
// FBX/OBJ specular 工作流(z=1)换用独立粗糙度图 R 通道
|
// FBX/OBJ specular 工作流(z=1)换用独立粗糙度图 R 通道
|
||||||
|
|||||||
Reference in New Issue
Block a user