Compare commits

..
18 Commits
Author SHA1 Message Date
atdunbg beba491bcb 阶段性合并:动画重定向、碎裂共享库、第三人称玩法、编辑器面板与 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 的拒读)
2026-10-11 22:22:19 +08:00
atdunbg 334819bf97 修复打靶场射击链路:发射源/玩家控制走 primary 相机实体
- TestShoot 发射源从 EditorCamera(视口 UI 状态,PIE 渲染不用它)
  改为绑定实体位姿,挂 primary 相机实体即第一人称枪口;
  仅光标捕捉态开火,防编辑器交互误射
- 新增 PlayerController:FPS 控制(鼠标捕捉/Esc 释放/点视口重捕、
  yaw+pitch 转向、WASD+Space/Ctrl 移动+Shift 加速),驱动 primary
  相机实体 Transform,PIE 所见即玩家所见
- 靶子/碎片玻璃 alpha 0.42/0.45→0.85、自发光 2→3:低不透明度在
  亮背景下靶子几乎隐形(用户误认 HUD 横条为靶子)
2026-10-09 13:13:52 +08:00
atdunbg f15b104e77 60 秒限时打靶场玩法原型:碎裂逻辑提取 FractureLib 共享库(HostView 桥接 protected 底座)+ 玻璃靶 Target(跨脚本参数计分)+ GameDirector(靶阵/倒计时/重开/图元 HUD) 2026-10-09 13:00:18 +08:00
atdunbg 6854d53f11 per-mesh 引用:MeshRendererComponent.meshIndex(-1=整模型)贯穿渲染收集/物理 Mesh 碰撞(自动跟随)/场景序列化(缺省键向前兼容)/Inspector mesh 下拉 2026-10-09 12:38:05 +08:00
atdunbg 505217ff7e Spawn/Instantiate 选型约定落进基类与注册宏文档(判定问题+三条纪律;注册=Blueprintable 表态,UE 同题靠约定治理) 2026-10-09 12:31:33 +08:00
atdunbg f14aba4238 修自发光回归:glTF 导入端写 emissiveFactor(EMISSIVE_COLOR,贴图无 factor 时设白);Emissive 兜底纹理黑→白(黑兜底会把 factor/strength 一起乘没) 2026-10-09 00:12:35 +08:00
atdunbg 6447b5c0fa 材质表面参数接通:新增 emissiveFactor(无 emissive 贴图时的发光入口,glTF 语义)入 UBO/YAML/缓存键/两 shader;Transmission/Thickness 未勾 Transparent 时禁用编辑(不透明段 shader 不读) 2026-10-09 00:04:55 +08:00
atdunbg 235482733c 修渲染两处:透明段剔除背面(闭合体正反面乱序 over 混合=深度错乱,单面玻璃 UE/Unity 语义);prefilter mip0 128→256(roughness 0 镜面反射块状马赛克) 2026-10-08 23:50:11 +08:00
atdunbg c43adc339d 资产系统侧线第一波:ResolveProcedural 提为公共静态(键格式唯一定义点);Inspector 程序化模型入口;修 Prefab hover 误导性 not loaded;Content Browser 模型拖拽 → Outliner 创建实体 2026-10-08 23:36:08 +08:00
atdunbg 2cf8d6e076 修复碎片模型卸载的在飞 GPU 引用(vkDestroyBuffer in-use):门面批量卸载先 WaitDeviceIdle 再销毁(与 ReloadMaterialAsset 同一先例) 2026-10-08 23:23:15 +08:00
atdunbg affa1a7a56 碎片形状随机化:唯一代键替代命中点量化缓存键(每次命中重新随机 Voronoi),配套碎片模型生命周期回收(销毁次帧卸载 + OnDestroy 兜底) 2026-10-08 23:18:35 +08:00
atdunbg d4d37b4b28 修复 PIE 快照丢相机组件(序列化写出键 Attached ≠ 读侧 Camera)与子弹同帧多接触重复特效(Bullet 命中防重入) 2026-10-08 23:06:18 +08:00
atdunbg 21da38db92 WallFracture 碎片几何升级:网格方块 → 2D Voronoi 不规则凸棱柱(命中点周边种子加密,MeshCollider 凸包,运行时 ParsedModel 程序化资产) 2026-10-08 22:58:40 +08:00
atdunbg f20578bc4b TestShoot 弹头半径 0.08→0.12 反隧穿:离散检测下薄板碰撞窗口须大于每步位移(无 CCD) 2026-10-08 22:48:20 +08:00
atdunbg bca50976f3 WallFracture 碎裂手感修正:爆炸飞散 → 玻璃式碎裂(初速距离衰减 + 法线剥落外推 + 网格切细至 8×6) 2026-10-08 22:43:36 +08:00
atdunbg d2701a3d9b 新增程序化立方体与可碎墙验收:Model::CreateCube 五层链路 + WallFracture 碎片刚体群(命中碎裂/径向飞散/寿命清理) 2026-10-08 22:28:49 +08:00
atdunbg ed7cb417bd 新增 Prefab 系统:实体子树存取 + 编辑器 Save As Prefab/拖入实例化 + 脚本 Instantiate 2026-10-08 22:01:26 +08:00
atdunbg b89f431896 新增脚本实体底座与碰撞事件:Spawn/Destroy + OnCollisionEnter/Exit + TestShoot 验收脚本
- ScriptBehavior::Spawn/Destroy:Scene::CreateEntity / 延迟销毁队列直达(实现下沉 ScriptRuntime.cpp,避 Application 不完整类型)
- ScriptRuntime::DispatchCollisions:帧首把 PhysicsWorld::GetEvents 派发到涉事实例(Persisted 跳过、禁用不收;回调内 spawn/destroy 安全)
- TestShoot:左键 spawn 动态球刚体子弹,OnCollisionEnter 命中点生成 ImpactFx 特效实体并自毁,超时防泄漏
2026-10-08 21:45:10 +08:00
70 changed files with 45354 additions and 1025 deletions
+19
View File
@@ -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
+5
View File
@@ -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;
+59 -14
View File
@@ -7,6 +7,7 @@
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器 #include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <imgui.h> #include <imgui.h>
@@ -27,10 +28,12 @@ namespace {
// 类型过滤(下标即身份):0 = All // 类型过滤(下标即身份):0 = All
constexpr const char* kTypeNames[] = {"All", "Models", constexpr const char* kTypeNames[] = {"All", "Models",
"Textures", "Scenes", "Textures", "Scenes",
"Materials", "Controllers"}; "Materials", "Controllers",
"Prefabs"};
// 资产类别:按扩展名分派(过滤用) // 资产类别:按扩展名分派(过滤用)
enum class Kind { Model, Texture, Scene, Material, AnimClip, Other }; enum class Kind { Model, Texture, Scene, Material, AnimClip, Prefab,
Other };
Kind KindOf(const std::filesystem::path& p) Kind KindOf(const std::filesystem::path& p)
{ {
@@ -59,6 +62,10 @@ namespace {
{ {
return Kind::AnimClip; return Kind::AnimClip;
} }
if (ext == ".prefab")
{
return Kind::Prefab;
}
return Kind::Other; return Kind::Other;
} }
@@ -71,6 +78,7 @@ namespace {
case 3: return kind == Kind::Scene; case 3: return kind == Kind::Scene;
case 4: return kind == Kind::Material; case 4: return kind == Kind::Material;
case 5: return kind == Kind::AnimClip; case 5: return kind == Kind::AnimClip;
case 6: return kind == Kind::Prefab;
default: return true; default: return true;
} }
} }
@@ -85,6 +93,7 @@ namespace {
case Kind::Scene: return "[S]"; case Kind::Scene: return "[S]";
case Kind::Material: return "[Ma]"; case Kind::Material: return "[Ma]";
case Kind::AnimClip: return "[An]"; case Kind::AnimClip: return "[An]";
case Kind::Prefab: return "[Pf]";
default: return "[ ]"; default: return "[ ]";
} }
} }
@@ -121,7 +130,7 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
} }
ImGui::SameLine(); ImGui::SameLine();
ImGui::SetNextItemWidth(110.0f); ImGui::SetNextItemWidth(110.0f);
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 6); ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 7);
ImGui::SameLine(); ImGui::SameLine();
ImGui::SetNextItemWidth(140.0f); ImGui::SetNextItemWidth(140.0f);
ImGui::InputTextWithHint("##name", "filter", m_NameFilter, ImGui::InputTextWithHint("##name", "filter", m_NameFilter,
@@ -296,6 +305,26 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
ImGui::EndDragDropSource(); ImGui::EndDragDropSource();
} }
// .prefab 拖拽源:拖到 Outliner(节点=挂其下 / 空白区=挂根)
// 实例化整棵子树(kPrefabDragPayload)
if (item.kind == Kind::Prefab && ImGui::BeginDragDropSource())
{
ImGui::SetDragDropPayload(kPrefabDragPayload,
assetPath.c_str(), assetPath.size() + 1);
ImGui::TextUnformatted(name.c_str());
ImGui::EndDragDropSource();
}
// 模型拖拽源:拖到 Outliner 落点创建带 MeshRenderer 的实体
//(kModelDragPayload;实体名 = 文件名)
if (item.kind == Kind::Model && ImGui::BeginDragDropSource())
{
ImGui::SetDragDropPayload(kModelDragPayload,
assetPath.c_str(), assetPath.size() + 1);
ImGui::TextUnformatted(name.c_str());
ImGui::EndDragDropSource();
}
// 双击:.material 打开 Material Editor;.animctrl 打开 // 双击:.material 打开 Material Editor;.animctrl 打开
// Controller Editor;model/HDR 进缓存预热(路径去重,重复 // Controller Editor;model/HDR 进缓存预热(路径去重,重复
// 双击零成本);场景加载含环境恢复/选中清理流程,提示走 // 双击零成本);场景加载含环境恢复/选中清理流程,提示走
@@ -328,6 +357,16 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})", ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
assetPath); assetPath);
} }
else if (item.kind == Kind::Prefab && ctx.scene && ctx.assets)
{
// 双击 = 快捷实例化(授权位姿落点,选中交给 Outliner
// 手动改);拖拽实例化见 Outliner 落点
const auto root = Engine::Serialization::SceneSerializer::
InstantiatePrefab(*ctx.scene, *ctx.assets, assetPath);
ENGINE_INFO("AssetBrowser: prefab '{}' instantiated "
"(valid={})",
assetPath, root != entt::null);
}
} }
// hover 信息卡(UE Content Browser 式):类型/大小/修改时间/加载状态 // hover 信息卡(UE Content Browser 式):类型/大小/修改时间/加载状态
@@ -336,11 +375,12 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
ImGui::BeginTooltip(); ImGui::BeginTooltip();
ImGui::TextUnformatted(name.c_str()); ImGui::TextUnformatted(name.c_str());
ImGui::Separator(); ImGui::Separator();
// 与 Kind 枚举逐位对齐(Kind::Other = 5)——漏项 = tooltip // 与 Kind 枚举逐位对齐(Kind::Other = 6)——漏项 = tooltip
// 数组越界读 // 数组越界读
static const char* kKindNames[] = {"Model", "Texture/Environment", static const char* kKindNames[] = {"Model", "Texture/Environment",
"Scene", "Material", "Scene", "Material",
"Controller", "File"}; "Controller", "Prefab",
"File"};
ImGui::Text("type: %s", kKindNames[static_cast<int>(item.kind)]); ImGui::Text("type: %s", kKindNames[static_cast<int>(item.kind)]);
namespace fs = std::filesystem; namespace fs = std::filesystem;
std::error_code ec; std::error_code ec;
@@ -374,21 +414,26 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
std::localtime(&tt)); std::localtime(&tt));
ImGui::Text("modified: %s", tmBuf); ImGui::Text("modified: %s", tmBuf);
} }
// 加载状态(只查询不触发加载;model/HDR 进过缓存才显示 loaded) // 加载状态:只对"加载"型资产有意义(查询不触发加载)。
// Prefab 是实例化模板、Scene 走 File 菜单——显示 "not
// loaded" 会误导(曾让用户当成 bug)
if (ctx.assets) if (ctx.assets)
{ {
const char* state = "not loaded"; if (item.kind == Kind::Model ||
if (item.kind == Kind::Model && item.kind == Kind::Texture)
ctx.assets->IsModelLoaded(assetPath))
{ {
state = "loaded"; const bool loaded =
(item.kind == Kind::Model)
? ctx.assets->IsModelLoaded(assetPath)
: ctx.assets->IsEnvironmentLoaded(assetPath);
ImGui::Text("state: %s", loaded ? "loaded"
: "not loaded");
} }
else if (item.kind == Kind::Texture && else if (item.kind == Kind::Prefab)
ctx.assets->IsEnvironmentLoaded(assetPath))
{ {
state = "loaded"; ImGui::TextDisabled(
"template — drag into scene / double-click");
} }
ImGui::Text("state: %s", state);
// 已加载的 model 展开资产数据概览(meshes=0 即纯动画 // 已加载的 model 展开资产数据概览(meshes=0 即纯动画
// 容器;未加载不显示——查询不触发加载) // 容器;未加载不显示——查询不触发加载)
if (item.kind == Kind::Model) if (item.kind == Kind::Model)
+9
View File
@@ -18,6 +18,15 @@
#include <filesystem> #include <filesystem>
#include <string> #include <string>
// Prefab 资产拖拽 payload(Content Browser 拖 .prefab → Outliner 落点
// 实例化;字符串 = 资产相对路径)。对齐 kMaterialDragPayload 模式:
// 常量住源面板头
inline constexpr const char* kPrefabDragPayload = "PREFAB_PATH";
// 模型资产拖拽 payload(Content Browser 拖 .glb/.fbx/... → Outliner
// 落点创建带 MeshRenderer 的实体;字符串 = 资产相对路径)
inline constexpr const char* kModelDragPayload = "MODEL_PATH";
class AssetBrowserPanel { class AssetBrowserPanel {
public: public:
void Draw(const EditorContext& ctx); void Draw(const EditorContext& ctx);
+199
View File
@@ -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();
}
+21
View File
@@ -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
+110 -2
View File
@@ -7,12 +7,16 @@
// //
#include "HierarchyPanel.h" #include "HierarchyPanel.h"
#include "AssetBrowserPanel.h" // kPrefabDragPayload(Content Browser 源)
#include "Engine/ECS/Components.h" #include "Engine/ECS/Components.h"
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle #include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
#include "Engine/Serialization/SceneSerializer.h"
#include "Engine/Core/Log.h"
#include <imgui.h> #include <imgui.h>
#include <cstring> #include <cstring>
#include <filesystem>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -33,6 +37,58 @@ namespace {
return "(unnamed)"; return "(unnamed)";
} }
// Prefab 拖入实例化(树节点/空白区落点共用)。根实体挂在场景根、
// 保留授权位姿;返回根实体(失败 entt::null)
entt::entity InstantiatePrefabDrop(const EditorContext& ctx,
const char* path)
{
if (!ctx.scene || !ctx.assets || !path) {
return entt::null;
}
return Engine::Serialization::SceneSerializer::InstantiatePrefab(
*ctx.scene, *ctx.assets, path);
}
// 模型拖入落点:创建带 MeshRenderer 的单实体(名字 = 文件名
// stem,默认位姿;模型句柄 = LoadModel 磁盘路径)。拖到节点下 =
// SetParent keepWorld;空白区 = 场景根。加载失败不建空实体
entt::entity SpawnModelDrop(const EditorContext& ctx, const char* path)
{
if (!ctx.scene || !ctx.assets || !path) {
return entt::null;
}
const auto model = ctx.assets->LoadModel(path);
if (!model.Valid()) {
return entt::null;
}
std::string name =
std::filesystem::path(path).stem().string();
if (name.empty()) {
name = "model entity";
}
auto e = ctx.scene->CreateEntity(name);
e.AddComponent<Engine::ECS::MeshRendererComponent>().model = model;
ENGINE_INFO("Hierarchy: model entity '{}' created from '{}'",
name, path);
return static_cast<entt::entity>(e);
}
// 实体名 → 合法文件名(路径分隔符/保留符净化;空名兜底)
std::string PrefabFileName(std::string name)
{
for (char& c : name) {
if (c == '/' || c == '\\' || c == ':' || c == '*' ||
c == '?' || c == '"' || c == '<' || c == '>' ||
c == '|') {
c = '_';
}
}
if (name.empty() || name == "(unnamed)") {
name = "prefab";
}
return name;
}
// 递归绘制单节点(含子树)。返回值无意义,选中写 selected // 递归绘制单节点(含子树)。返回值无意义,选中写 selected
void DrawEntityNode(const EditorContext& ctx, entt::entity e, void DrawEntityNode(const EditorContext& ctx, entt::entity e,
entt::entity& selected) entt::entity& selected)
@@ -78,7 +134,9 @@ namespace {
ImGui::EndDragDropSource(); ImGui::EndDragDropSource();
} }
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变)---- // ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变);
// Prefab 拖入 = 实例化子树后挂到本实体下(keepWorld 保持
// 授权世界位姿)----
if (ImGui::BeginDragDropTarget()) { if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* pl = if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kHierPayload)) { ImGui::AcceptDragDropPayload(kHierPayload)) {
@@ -91,6 +149,26 @@ namespace {
ctx.scene->SetParent(dragged, e); ctx.scene->SetParent(dragged, e);
} }
} }
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
if (const entt::entity root =
InstantiatePrefabDrop(ctx,
static_cast<const char*>(
pl->Data));
root != entt::null) {
ctx.scene->SetParent(root, e); // keepWorld 默认 true
selected = root;
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
if (const entt::entity me = SpawnModelDrop(
ctx, static_cast<const char*>(pl->Data));
me != entt::null) {
ctx.scene->SetParent(me, e); // keepWorld 默认 true
selected = me;
}
}
ImGui::EndDragDropTarget(); ImGui::EndDragDropTarget();
} }
@@ -102,6 +180,19 @@ namespace {
ctx.scene->SetParent(child, e); ctx.scene->SetParent(child, e);
selected = child; selected = child;
} }
if (ImGui::MenuItem("Save As Prefab")) {
// 固定目录 + 实体名派生文件名(无文件对话框库,与场景
// File 菜单同策略;重名覆盖,改名走文件系统)
const std::string path =
"assets/prefabs/" +
PrefabFileName(DisplayName(reg, e)) + ".prefab";
std::error_code ec;
std::filesystem::create_directories("assets/prefabs", ec);
if (Serialization::SceneSerializer::SerializePrefab(
*ctx.scene, *ctx.assets, e, path)) {
ENGINE_INFO("Hierarchy: prefab saved to '{}'", path);
}
}
if (ctx.scene->GetParent(e) != entt::null && if (ctx.scene->GetParent(e) != entt::null &&
ImGui::MenuItem("Detach to Root")) { ImGui::MenuItem("Detach to Root")) {
ctx.scene->SetParent(e, entt::null); ctx.scene->SetParent(e, entt::null);
@@ -194,7 +285,8 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
DrawEntityNode(ctx, e, selected); DrawEntityNode(ctx, e, selected);
}); });
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层)---- // ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层);
// Prefab 拖入 = 实例化到场景根(授权位姿)----
ImVec2 rem = ImGui::GetContentRegionAvail(); ImVec2 rem = ImGui::GetContentRegionAvail();
if (rem.y > 4.0f) { if (rem.y > 4.0f) {
ImGui::InvisibleButton("##hier_drop_root", rem); ImGui::InvisibleButton("##hier_drop_root", rem);
@@ -206,6 +298,22 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
ctx.scene->SetParent(static_cast<entt::entity>(bits), ctx.scene->SetParent(static_cast<entt::entity>(bits),
entt::null); entt::null);
} }
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
if (const entt::entity root = InstantiatePrefabDrop(
ctx, static_cast<const char*>(pl->Data));
root != entt::null) {
selected = root;
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
if (const entt::entity me = SpawnModelDrop(
ctx, static_cast<const char*>(pl->Data));
me != entt::null) {
selected = me;
}
}
ImGui::EndDragDropTarget(); ImGui::EndDragDropTarget();
} }
} }
+137 -8
View File
@@ -18,6 +18,7 @@
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面) #include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
#include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names) #include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names)
#include "Engine/Scripting/ScriptRuntime.h" // 参数编辑 → RefreshParams #include "Engine/Scripting/ScriptRuntime.h" // 参数编辑 → RefreshParams
#include "Engine/Serialization/SceneSerializer.h" // ResolveProcedural(程序化入口)
#include "Engine/Core/Log.h" // Create Override 失败告警 #include "Engine/Core/Log.h" // Create Override 失败告警
#include <imgui.h> #include <imgui.h>
@@ -197,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 =
@@ -670,6 +677,69 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
mr->materialOverrides.clear(); mr->materialOverrides.clear();
} }
ImGui::Spacing(); ImGui::Spacing();
// 程序化模型入口:伪资产不在磁盘(下拉框列不到),由
// 弹窗组键生成 → ResolveProcedural(与场景反解同一
// 键格式唯一定义点;确定性键必然命中缓存)
if (ImGui::SmallButton("Procedural..."))
{
ImGui::OpenPopup("##proc_model");
}
if (ImGui::BeginPopup("##proc_model"))
{
static int shape = 0; // 0=cube 1=plane 2=transparent plane
static float half = 0.5f;
static float col[3] = {0.55f, 0.36f, 0.28f};
static float alpha = 0.5f;
ImGui::RadioButton("Cube", &shape, 0);
ImGui::SameLine();
ImGui::RadioButton("Plane", &shape, 1);
ImGui::SameLine();
ImGui::RadioButton("Trans.Plane", &shape, 2);
// 固定宽度而非 -1:弹窗是 AlwaysAutoResize,-1(撑满可用
// 宽度)会拿上一帧宽度当基准 → 逐帧膨胀直到撑满屏幕
ImGui::SetNextItemWidth(180.0f);
ImGui::SliderFloat("half size", &half, 0.01f, 10.0f,
"%.2f");
ImGui::ColorEdit3("color", col);
if (shape == 2)
{
ImGui::SetNextItemWidth(180.0f);
ImGui::SliderFloat("alpha", &alpha, 0.0f, 1.0f,
"%.2f");
}
if (ImGui::Button("Create & Assign"))
{
const int r = int(col[0] * 255.0f + 0.5f);
const int g = int(col[1] * 255.0f + 0.5f);
const int b = int(col[2] * 255.0f + 0.5f);
char key[96];
if (shape == 2)
{
const int a = int(alpha * 255.0f + 0.5f);
std::snprintf(
key, sizeof(key),
"procedural://transparent-plane/%.3g/%d,%d,%d,%d",
half, r, g, b, a);
}
else
{
std::snprintf(
key, sizeof(key),
"procedural://%s/%.3g/%d,%d,%d",
shape == 0 ? "cube" : "plane", half, r, g, b);
}
if (auto ref = Engine::Serialization::
SceneSerializer::ResolveProcedural(
*ctx.assets, key);
ref.Valid())
{
mr->model = ref;
mr->materialOverrides.clear();
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
} }
// 无效句柄不进 GetModel(缓存对无效/悬空句柄 FATAL,是 bug 信号) // 无效句柄不进 GetModel(缓存对无效/悬空句柄 FATAL,是 bug 信号)
if (mr->model.Valid()) if (mr->model.Valid())
@@ -677,6 +747,40 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
if (const Engine::Model* model = ctx.assets->GetModel(mr->model); if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
model != nullptr) model != nullptr)
{ {
// 单 mesh 子引用下拉(-1 = 整模型全部):名字用
// GetMeshMaterialInfos(材质名优先,空回退 Slot N)
{
const auto& infos = model->GetMeshMaterialInfos();
const int sel = mr->meshIndex + 1; // 0 = All
std::string preview = "All meshes";
if (sel > 0 &&
static_cast<size_t>(sel - 1) < infos.size()) {
preview = infos[static_cast<size_t>(sel) - 1]
.name.empty()
? "Slot " + std::to_string(sel - 1)
: infos[static_cast<size_t>(sel) - 1].name;
}
if (ImGui::BeginCombo("mesh", preview.c_str()))
{
if (ImGui::Selectable("All meshes", sel <= 0)) {
mr->meshIndex = -1;
}
for (size_t i = 0; i < infos.size(); ++i)
{
const std::string label =
(infos[i].name.empty()
? "Slot " + std::to_string(i)
: infos[i].name) +
"##" + std::to_string(i);
const bool cur =
mr->meshIndex == static_cast<int>(i);
if (ImGui::Selectable(label.c_str(), cur)) {
mr->meshIndex = static_cast<int>(i);
}
}
ImGui::EndCombo();
}
}
const auto& clips = model->GetClips(); const auto& clips = model->GetClips();
if (model->GetMeshes().empty()) if (model->GetMeshes().empty())
{ {
@@ -1175,6 +1279,8 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::DragFloat("Radius", &cc->radius, 0.02f, 0.05f, 5.0f); ImGui::DragFloat("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);
@@ -1805,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);
@@ -1829,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;
@@ -1842,10 +1968,9 @@ 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));
const bool doDetach = ImGui::SmallButton("x"); const bool doDetach = ImGui::SmallButton("x");
@@ -1951,6 +2076,10 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
// CrossFade 与 Direct 的 crossfade 共用 fade) // CrossFade 与 Direct 的 crossfade 共用 fade)
ImGui::SliderFloat("Speed", &anim->speed, -2.0f, 2.0f); ImGui::SliderFloat("Speed", &anim->speed, -2.0f, 2.0f);
ImGui::SliderFloat("Fade", &anim->fadeDuration, 0.0f, 1.0f); ImGui::SliderFloat("Fade", &anim->fadeDuration, 0.0f, 1.0f);
// 根运动(Unity Animator 同名开关):关 = 原地播放,
// 位移归脚本/物理驱动(CharacterController 角色用)
ImGui::Checkbox("Apply Root Motion",
&anim->applyRootMotion);
// 动画数据表:每个 clip 的时长与有效轨道数(attach 进来的 // 动画数据表:每个 clip 的时长与有效轨道数(attach 进来的
// 外部动画在此核对骨骼匹配结果——轨道数明显偏少 = 骨骼 // 外部动画在此核对骨骼匹配结果——轨道数明显偏少 = 骨骼
// 命名不同源被跳过) // 命名不同源被跳过)
+152
View File
@@ -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();
}
+26
View File
@@ -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
+49 -3
View File
@@ -173,13 +173,44 @@ 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));
EditParam(ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f, // 发光 = emissiveFactor × emissive 贴图 × Strength(Emissive 槽
1.0f)); // 兜底贴图恒黑,无贴图时颜色因子是唯一入口)
EditParam(ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x));
EditParam(ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f, EditParam(ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f,
0.0f, 100.0f)); 0.0f, 100.0f));
// transmission/thickness 只有不透明段不读——透明 shader 专属
//(未勾 Transparent 时禁用编辑,防"拖了没反应"的困惑)
ImGui::BeginDisabled(!trans);
EditParam(ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f,
1.0f));
EditParam(ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f, EditParam(ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f,
10.0f)); 10.0f));
ImGui::EndDisabled();
if (!trans)
{
ImGui::TextDisabled("Transmission/Thickness need Transparent");
}
// UV 镜像(材质级开关,作用于全部纹理槽):源资产 UV 朝向与引擎
// 上左原点约定相反时贴图整体翻转,这里一键纠正。写入 misc.y/z,
// 旧 .material 无该分量 → 缺省 0 = 关闭
{
bool flipU = p.misc.y > 0.5f;
const bool c = ImGui::Checkbox("Flip U (left-right)", &flipU);
if (c) { p.misc.y = flipU ? 1.0f : 0.0f; }
EditParam(c);
}
ImGui::SameLine();
{
bool flipV = p.misc.z > 0.5f;
const bool c = ImGui::Checkbox("Flip V (up-down)", &flipV);
if (c) { p.misc.z = flipV ? 1.0f : 0.0f; }
EditParam(c);
}
// ---- 纹理槽(变化 → 落盘 + waitIdle 重建)---- // ---- 纹理槽(变化 → 落盘 + waitIdle 重建)----
ImGui::SeparatorText("Textures"); ImGui::SeparatorText("Textures");
@@ -205,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()))
@@ -302,10 +340,18 @@ 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::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f, 1.0f); ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x);
ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f, 0.0f, 100.0f); ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f, 0.0f, 100.0f);
ImGui::BeginDisabled(!trans);
ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f, 1.0f);
ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f, 10.0f); ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f, 10.0f);
ImGui::EndDisabled();
if (!trans)
{
ImGui::TextDisabled("Transmission/Thickness need Transparent");
}
ImGui::SeparatorText("Textures"); ImGui::SeparatorText("Textures");
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = { static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
"Albedo", "Normal", "MetallicRoughness", "Albedo", "Normal", "MetallicRoughness",
+21
View File
@@ -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();
+8
View File
@@ -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 无任何轴位,
-5
View File
@@ -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();
}
} }
+203
View File
@@ -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
+704
View File
@@ -0,0 +1,704 @@
//
// FractureLib.h:共享碎裂库(自 WallFracture.cpp 提取,行为逐行保持)。
//
// 碎裂语义 = 玻璃/陶瓷式("碎掉"),非爆炸飞散,碎片几何 = 2D
// Voronoi 分割:
// 碎裂瞬间在面上撒种子点(命中点周边加密 → 孔洞周围碎片小、
// 远处碎片大,玻璃碎的典型分布),对每颗种子做垂直平分线半平面
// 交集得到不规则凸多边形 cell,沿面厚挤出成凸棱柱——碎片在碎裂
// 前一刻完美拼合原面,碎开后沿裂纹分离。碰撞形状不走 Box,直接
// 用 MeshCollider(Dynamic → Jolt ConvexHull,数据源 = 网格 CPU
// 位置副本,PhysicsWorld::BuildMeshShape)→ 不规则碎片连碰撞体
// 都是真的,落地翻滚/堆积接触自然。
//
// 初速 = 径向 × 距离指数衰减(只有命中点周边的碎片被推动,远处
// 几乎原地悬置)+ 沿命中法线轻微外推(朝弹侧剥落)+ 微弱上抛
// ——重力是塌落的主导,整体呈"裂纹分离 → 孔洞塌落 → 碎块堆在
// 墙脚"的观感。
//
// 用法(宿主 = 挂在可碎实体上的脚本):
// Fracture::Burst burst_; // 碎片簿记 + 生命周期
// DeclareParams 直绑 burst_.p 各字段(pb.Field("Count",
// &burst_.p.count, ...))——参数必须与碎裂逻辑读的是同一对象,
// 绑到独立副本会让 Count/Lifetime 等全部失效
// HostView HV() { // 桥接(脚本自身成员函数内可访问
// return {App(), EntityBits(),// protected 基类设施,库内只走公共 API)
// [this](auto& n) { return Spawn(n); },
// [this](auto e) { Destroy(e); }};
// }
// OnCollisionEnter → burst_.TryFire(HV(), other, point, normal)
// OnUpdate → burst_.Tick(HV(), dt)
// OnDestroy → burst_.Shutdown(HV())
// 外观:默认继承宿主(墙)的生效材质(底色/贴图/透明),碎块与墙面
// 一致;要独立外观(玻璃靶 = 半透明 + 自发光)Fire 前置
// burst_.look.inheritFromHost = false 再填 baseColor/emissive
// HostView 每次调用现填(不存指针:组件存储重排/实例重建均安全)
//
// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按宿主世界
// 尺寸烘焙、碎片实体复用宿主 world pos/rot(scale 恒 1,防
// ConvexHull 顶点二次缩放)——宿主须无旋转摆放。
//
#ifndef HELLOVULKAN_FRACTURE_LIB_H
#define HELLOVULKAN_FRACTURE_LIB_H
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Types.h>
#include <Engine/Assets/AssetManager.h> // LoadModel(门面只前向声明)
#include <Engine/Renderer/Model.h> // ParsedModel/ParsedMesh(程序化碎片几何)
#include <Engine/Renderer/Vertex.h> // Vertex 聚合布局
#include <Engine/Physics/PhysicsWorld.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <algorithm>
#include <cmath>
#include <functional>
#include <random>
namespace Engine::Fracture {
// bits(PhysicsWorld 键)→ 实体(还原位模式;用前自行 valid 校验)
inline ECS::Entity EntityFromBits(u32 bits) {
return static_cast<ECS::Entity>(bits);
}
// 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记
// 需要任意实体的同款换算)
inline u32 BitsOf(const ECS::EntityHandle& e) {
return static_cast<u32>(
entt::to_integral(static_cast<entt::entity>(e)));
}
inline f32 Rand(f32 lo, f32 hi) {
thread_local std::mt19937 rng{std::random_device{}()};
return std::uniform_real_distribution<f32>(lo, hi)(rng);
}
// 碎片模型代序号(全局递增):每代碎片一组唯一缓存键 → 每次
// 命中都重新随机生成形状(命中点量化键会让同位置重复命中
// 复用首次几何——缓存去重把新几何丢弃,碎片就"固定"了)
inline u32& GenSeq() {
static u32 s = 0;
return s;
}
// ---- 宿主桥接(脚本侧现填;库内只走公共 API)----
// spawn/destroy 走脚本成员(内部才能访问 ScriptBehavior 的
// protected 底座),app/实体走数据。std::function 捕 this 无
// 分配(小对象优化),随调用栈存亡,无悬垂面
struct HostView {
Application& app;
u32 entityBits = 0;
ECS::Entity entity() const { return EntityFromBits(entityBits); }
std::function<ECS::EntityHandle(const std::string&)> spawn;
std::function<void(ECS::Entity)> destroy;
};
// ---- 2D Voronoi(碎裂裂纹)----
//
// cell = 相对其它种子的垂直平分线半平面交集,初始域 = 墙面矩形。
// Sutherland–Hodgman 凸多边形裁剪:保留靠近 p 的一侧
//(dot(a,x) <= b,a = normalize(q-p),b = dot(a,p)+|q-p|/2)。
// 输入多边形须 CCW(初始矩形即 CCW,裁剪保序)。
inline std::vector<glm::vec2> ClipByBisector(std::vector<glm::vec2> poly,
const glm::vec2& p,
const glm::vec2& q) {
const glm::vec2 d = q - p;
const f32 len = glm::length(d);
if (len < 1e-6f) return poly;
const glm::vec2 a = d / len;
const f32 b = glm::dot(a, p) + 0.5f * len;
std::vector<glm::vec2> out;
out.reserve(poly.size() + 1);
for (size_t i = 0; i < poly.size(); ++i) {
const glm::vec2& c = poly[i];
const glm::vec2& n = poly[(i + 1) % poly.size()];
const f32 dc = glm::dot(a, c) - b;
const f32 dn = glm::dot(a, n) - b;
if (dc <= 0.0f) out.push_back(c);
if ((dc < 0.0f) != (dn < 0.0f)) { // 边跨越裁剪线 → 求交点
const f32 t = dc / (dc - dn);
out.push_back(c + t * (n - c));
}
}
return out;
}
// 种子 p 的 Voronoi cell(域 = [-hw,hw]×[-hh,hh] 的墙面矩形)
inline std::vector<glm::vec2> VoronoiCell(const glm::vec2& p,
const std::vector<glm::vec2>& seeds,
f32 hw, f32 hh) {
std::vector<glm::vec2> poly = {
{-hw, -hh}, {hw, -hh}, {hw, hh}, {-hw, hh}};
for (const glm::vec2& q : seeds) {
if (glm::dot(q - p, q - p) < 1e-10f) continue; // 自身
poly = ClipByBisector(std::move(poly), p, q);
if (poly.size() < 3) return {}; // 退化(最小间距保护,罕见)
}
return poly;
}
// cell(CCW 凸多边形,墙局部 XY)沿 z ∈ [-hz, +hz] 挤出成棱柱:
// 顶/底面 fan 三角化 + 侧面逐边 quad,per-face 法线。返回 mesh
// 数据;cpuPositions 单独回传(ConvexHull 物理数据源)。
struct PrismMesh {
std::vector<Engine::Vertex> vertices;
std::vector<u32> indices;
std::vector<glm::vec3> cpuPositions;
};
// hw/hh = 宿主半尺寸:顶/底面 UV 由 cell 的墙局部坐标归一化到 [0,1]
//(墙面矩形 UV 约定,与立方体面 UV 对齐)——继承墙材质贴图时,碎片
// 贴图与墙面连续,不会各碎片自成一格
inline PrismMesh CellToPrism(const std::vector<glm::vec2>& cell, f32 hz,
f32 hw, f32 hh) {
PrismMesh m;
const size_t n = cell.size();
const size_t topBase = 0;
const size_t botBase = n;
m.vertices.reserve(n * 2);
const f32 invW = hw > 1e-6f ? 0.5f / hw : 0.0f;
const f32 invH = hh > 1e-6f ? 0.5f / hh : 0.0f;
const auto uvOf = [&](const glm::vec2& p) {
return glm::vec2{p.x * invW + 0.5f, p.y * invH + 0.5f};
};
for (const glm::vec2& p : cell) {
const Engine::Vertex v{
{p.x, p.y, hz}, {0.0f, 0.0f, 1.0f},
uvOf(p), {1.0f, 0.0f, 0.0f}};
m.vertices.push_back(v);
}
for (const glm::vec2& p : cell) {
const Engine::Vertex v{
{p.x, p.y, -hz}, {0.0f, 0.0f, -1.0f},
uvOf(p), {1.0f, 0.0f, 0.0f}};
m.vertices.push_back(v);
}
// top fan(CCW 从 +Z 看朝外)+ bottom fan(反向)
for (size_t i = 1; i + 1 < n; ++i) {
m.indices.push_back(topBase);
m.indices.push_back(topBase + i);
m.indices.push_back(topBase + i + 1);
m.indices.push_back(botBase);
m.indices.push_back(botBase + i + 1);
m.indices.push_back(botBase + i);
}
// 侧面:(i, N+i, N+i+1, i+1)——外法线 = CCW 边的右侧法向
for (size_t i = 0; i < n; ++i) {
const size_t j = (i + 1) % n;
const glm::vec2 e = cell[j] - cell[i];
const f32 el = glm::length(e);
const glm::vec3 sn = el > 1e-6f
? glm::vec3{e.y / el, -e.x / el, 0.0f}
: glm::vec3{0.0f, -1.0f, 0.0f};
const size_t b = m.vertices.size();
const Engine::Vertex q0{
{cell[i].x, cell[i].y, hz}, sn,
{0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}};
const Engine::Vertex q1{
{cell[i].x, cell[i].y, -hz}, sn,
{0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}};
const Engine::Vertex q2{
{cell[j].x, cell[j].y, -hz}, sn,
{1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}};
const Engine::Vertex q3{
{cell[j].x, cell[j].y, hz}, sn,
{1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}};
m.vertices.push_back(q0);
m.vertices.push_back(q1);
m.vertices.push_back(q2);
m.vertices.push_back(q3);
m.indices.insert(m.indices.end(),
{u32(b), u32(b + 1), u32(b + 2),
u32(b), u32(b + 2), u32(b + 3)});
}
// 物理凸包数据源(BuildMeshShape Dynamic 分支逐顶点收集)
m.cpuPositions.reserve(m.vertices.size());
for (const auto& v : m.vertices) {
m.cpuPositions.push_back(v.position);
}
return m;
}
// ---- 程序化纯色还原(引擎零改动:颜色只存在于伪路径里)----
// 程序化 Cube/Plane 把颜色烘进 1x1 sRGB 纹理,MaterialParams 留白、
// 内建材质快照也没有颜色字段,故从伪路径尾段 "r,g,b[,a]" 还原线性
// 底色——与 GPU 采样 sRGB 纹理的硬件线性化同式,直乘 baseColor 才等价
inline f32 SrgbByteToLinear(u8 c) {
const f32 s = static_cast<f32>(c) / 255.0f;
return s <= 0.04045f ? s / 12.92f
: std::pow((s + 0.055f) / 1.055f, 2.4f);
}
// "procedural://cube/0.5/140,92,72" → 取最后一个 '/' 之后的纯数字
// 逗号段;段数 < 3 或含非数字(如 halfSize 段)判失败
inline bool ParseProceduralColor(const std::string& path, glm::vec4& out) {
if (path.rfind("procedural://", 0) != 0) return false;
const size_t slash = path.find_last_of('/');
if (slash == std::string::npos) return false;
const std::string tail = path.substr(slash + 1);
int c[3] = {0, 0, 0};
int n = 0;
size_t pos = 0;
while (n < 3 && pos < tail.size()) {
const size_t comma = tail.find(',', pos);
const size_t end =
(comma == std::string::npos) ? tail.size() : comma;
if (end == pos) return false;
int v = 0;
for (size_t i = pos; i < end; ++i) {
const char ch = tail[i];
if (ch < '0' || ch > '9') return false;
v = v * 10 + (ch - '0');
}
c[n++] = v;
if (comma == std::string::npos) break;
pos = comma + 1;
}
if (n < 3) return false;
out = glm::vec4{SrgbByteToLinear(static_cast<u8>(c[0])),
SrgbByteToLinear(static_cast<u8>(c[1])),
SrgbByteToLinear(static_cast<u8>(c[2])), 1.0f};
return true;
}
// ---- 参数(原 WallFracture 全量参数,字段类型随 Inspector 控件)----
struct Params {
i32 count = 30; // Voronoi 种子数 = 碎片数
f32 seedRadius = 0.9f; // 加密半径 m
f32 minSeedDist = 0.12f; // 种子最小间距 m
f32 burstSpeed = 1.2f; // 近点径向初速 m/s
f32 falloff = 1.0f; // 距离衰减率 1/m
f32 normalPop = 0.4f; // 法线外推 m/s
f32 upBias = 0.3f; // 上抛偏置 m/s
f32 lifetime = 6.0f; // 碎片寿命(秒)
i32 maxFragments = 128; // 活碎片上限
f32 minImpactSpeed = 2.0f; // 触发阈值(m/s)
};
// ---- 碎片外观 ----
// 默认 inheritFromHost = true:碎块继承宿主(墙)的"生效材质"——
// 底色/贴图/透明/自发光全套沿用,碎掉后与墙面目视一致。宿主材质
// 取实体级覆盖(.material 资产)优先、模型内建材质兜底,与
// SceneRenderer 收集期的 resolve 同序
// 需要独立外观(玻璃靶 = 半透明 + 自发光等)时置 false,改用下面
// 的显式字段(Fire 前设置)
struct Appearance {
bool inheritFromHost = true; // true = 材质沿用宿主,忽略下列字段
glm::vec4 baseColor{140.0f / 255.0f, 92.0f / 255.0f,
72.0f / 255.0f, 1.0f};
bool transparent = false; // 透明绘制队列(玻璃碎片)
glm::vec3 emissive{0.0f}; // 自发光颜色(黑 = 不发光)
f32 emissiveStrength = 1.0f;
};
// ---- 碎裂执行体(宿主脚本持有;一次 Fire 管一整批碎片)----
class Burst {
public:
Params p; // 宿主 DeclareParams 直绑
Appearance look; // 碎片外观(Fire 前设置)
// 触发流程一次做完:一次性保护 → 对方校验(活 Dynamic +
// 速度阈值)→ 藏宿主 → 切分 → spawn 碎片群。未触发返回
// false(速度不够/重复触发),宿主无需再写判定
bool TryFire(const HostView& host,
u32 other, const glm::vec3& point,
const glm::vec3& normal) {
if (m_Fired) return false;
// 对方须"够快"且是可动的撞击来源,二选一:
// - 动态刚体(子弹/抛体):用实时线速度
// - 角色(CharacterVirtual,非刚体、无 RigidBodyComponent):
// 用"扫掠前"速度——扫掠中朝墙面的分量会被 Jolt 抵消,
// 实时速度往往已归零,会漏判(见
// PhysicsWorld::GetCharacterImpactVelocity)
// 速度低于阈值(静止贴靠/轻碰)则忽略
auto& registry = host.app.GetScene().GetRegistry();
const ECS::Entity otherE = EntityFromBits(other);
if (!registry.valid(otherE)) return false;
f32 speed = 0.0f;
if (const auto* otherRb =
registry.try_get<Engine::ECS::RigidBodyComponent>(
otherE)) {
if (otherRb->type !=
Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
return false; // 静态/运动学体不算撞击
}
speed = glm::length(
host.app.GetPhysics().GetLinearVelocity(other));
} else if (registry.all_of<
Engine::ECS::CharacterControllerComponent>(
otherE)) {
// 玩家/角色撞击:角色碰撞事件由 PhysicsWorld 合成
// (CharacterVirtual 不进 ContactListener)
speed = glm::length(
host.app.GetPhysics().GetCharacterImpactVelocity(other));
} else {
return false; // 无物理来源(纯 Transform 实体)
}
if (speed < p.minImpactSpeed) return false;
m_Fired = true;
Fire(host, point, normal);
return true;
}
// 每帧簿记(宿主 OnUpdate 直调;未 Fire 过无簿记可理)
void Tick(const HostView& host, f32 dt) {
if (!m_Fired) return;
ApplyPendingUnload(host);
ApplyPendingVelocities(host);
AgeAndCleanup(host, dt);
}
// 宿主销毁(PIE stop 换血 / 手动删除):碎片实体由场景清亡
// 处理,独占碎片模型统一批量回收(模型不被快照恢复的场景
// 引用——唯一代键;批量接口内部 GPU 停飞后再销毁)
void Shutdown(const HostView& host) {
if (!m_Fired) return; // 从未 Fire:无碎片无模型
std::vector<std::string> paths;
paths.reserve(m_Fragments.size() + m_PendingUnload.size());
for (const Fragment& f : m_Fragments) {
if (!f.modelPath.empty()) paths.push_back(f.modelPath);
}
m_Fragments.clear();
paths.insert(paths.end(),
std::make_move_iterator(m_PendingUnload.begin()),
std::make_move_iterator(m_PendingUnload.end()));
m_PendingUnload.clear();
if (!paths.empty()) {
host.app.GetAssets().UnloadModelsByPaths(paths);
}
}
bool Fired() const { return m_Fired; }
private:
// ---- 触发碎裂:藏宿主 + Voronoi 切分 + spawn 碎片群 ----
void Fire(const HostView& host,
const glm::vec3& hitPoint, const glm::vec3& hitNormal) {
auto& registry = host.app.GetScene().GetRegistry();
const ECS::Entity self = host.entity();
// 藏宿主:渲染/拾取跳过 + body 对账销毁(组件级 active,
// 与 Unity Renderer.enabled + Rigidbody 摘除同语义)
if (auto* mr =
registry.try_get<Engine::ECS::MeshRendererComponent>(
self)) {
mr->active = false;
}
if (auto* rb =
registry.try_get<Engine::ECS::RigidBodyComponent>(
self)) {
rb->active = false;
}
// 宿主几何:碎片网格顶点按宿主世界尺寸烘焙(面 = XY 平面,
// 厚度沿 Z;宿主须无旋转摆放,见头注释占位局限)
const auto& wall = registry.get<Engine::ECS::TransformComponent>(
self);
const glm::vec3 wallPos = wall.GetWorldPosition();
const glm::vec3 wallSize = wall.GetWorldScale();
const f32 hw = 0.5f * wallSize.x;
const f32 hh = 0.5f * wallSize.y;
const f32 hz = 0.5f * wallSize.z;
// ---- 碎片外观:解析宿主(墙)的生效材质 ----
// 与 SceneRenderer 收集期 resolve 同序:实体级覆盖
//(.material 资产,按 mesh 下标查表)优先,模型内建材质兜底
//(MeshMaterialInfo 为权威只读描述)。不继承则碎片只剩默认
// 砖色,与墙面目视不符
Engine::MaterialAssetData effMat;
glm::vec4 effSolid{0.0f}; // 程序化纯色 albedo(a>0 = 有效)
std::string hostOverridePath; // 宿主 .material 覆盖(空 = 内建材质)
bool hasEffMat = false;
if (look.inheritFromHost) {
const auto* mr = registry.try_get<
Engine::ECS::MeshRendererComponent>(self);
const int slot =
(mr && mr->meshIndex >= 0) ? mr->meshIndex : 0;
if (mr) {
const auto oIt =
mr->materialOverrides.find(static_cast<u32>(slot));
if (oIt != mr->materialOverrides.end()) {
if (auto* asset =
host.app.GetAssets().FindMaterialAsset(
oIt->second)) {
effMat = asset->GetData();
hostOverridePath = oIt->second;
hasEffMat = true;
}
}
}
if (!hasEffMat && mr && mr->model.Valid()) {
if (auto* model =
host.app.GetAssets().GetModel(mr->model)) {
const auto& infos = model->GetMeshMaterialInfos();
if (slot < static_cast<int>(infos.size())) {
effMat.texPaths = infos[slot].texPaths;
effMat.params = infos[slot].params;
effMat.transparent = infos[slot].transparent;
hasEffMat = true;
}
}
// 程序化纯色墙:颜色在生成的 1x1 纹理里(快照无颜色
// 字段),改由脚本从模型伪路径还原线性底色
ParseProceduralColor(
host.app.GetAssets().Models().Path(mr->model),
effSolid);
}
}
// 命中点 → 宿主局部(逆旋转 + 平移),XY 即裂纹平面坐标
const glm::quat invRot = glm::conjugate(
wall.GetWorldRotationQuat());
const glm::vec3 localHit = invRot * (hitPoint - wallPos);
const glm::vec2 uvHit{localHit.x, localHit.y};
// ---- 种子:40% 命中点半径内加密 + 60% 全面均匀,最小
// 间距拒绝采样(防碎片退化为细条)----
const u32 count = std::clamp(p.count, 4, 96);
const u32 nearCount = count * 2 / 5;
std::vector<glm::vec2> seeds;
seeds.reserve(count);
const f32 minD2 = p.minSeedDist * p.minSeedDist;
auto tryPush = [&](const glm::vec2& s) {
if (s.x < -hw || s.x > hw || s.y < -hh || s.y > hh) return;
for (const glm::vec2& e : seeds) {
if (glm::dot(e - s, e - s) < minD2) return;
}
seeds.push_back(s);
};
// 命中点周边:均匀盘采样(sqrt(Rand) 面积均匀)
for (u32 i = 0; i < nearCount * 12 && seeds.size() < nearCount;
++i) {
const f32 a = Rand(0.0f, glm::two_pi<f32>());
const f32 r = p.seedRadius * std::sqrt(Rand(0.0f, 1.0f));
tryPush(uvHit + r * glm::vec2{std::cos(a), std::sin(a)});
}
// 全面均匀补足(同款拒绝采样)
for (u32 i = 0; i < count * 12 && seeds.size() < count; ++i) {
tryPush({Rand(-hw, hw), Rand(-hh, hh)});
}
if (seeds.size() < 4) return; // 面太小采样失败:不碎
// ---- 每种子 → cell → 棱柱 mesh → ParsedModel ----
// 唯一代键(见 GenSeq 注释):形状每次随机;模型回收走
// Fragment.modelPath + 待卸载队列(ApplyPendingUnload)
const std::string keyBase =
"procedural://vorfrag/" +
std::to_string(GenSeq()++) + '/';
auto& assets = host.app.GetAssets();
std::vector<Engine::ModelRef> refs;
std::vector<glm::vec3> centroids; // 碎片质心(世界系,初速径向用)
std::vector<std::string> paths; // 模型缓存键(回收用,与 refs 对齐)
refs.reserve(seeds.size());
centroids.reserve(seeds.size());
paths.reserve(seeds.size());
for (size_t s = 0; s < seeds.size(); ++s) {
const std::vector<glm::vec2> cell =
VoronoiCell(seeds[s], seeds, hw, hh);
if (cell.size() < 3) {
refs.push_back({}); // 退化 cell:占位,spawn 跳过
centroids.emplace_back();
paths.emplace_back();
continue;
}
PrismMesh pm = CellToPrism(cell, hz, hw, hh);
glm::vec2 c2{0.0f};
for (const glm::vec2& q : cell) c2 += q;
c2 /= static_cast<f32>(cell.size());
centroids.push_back(
wallPos + wall.GetWorldRotationQuat() *
glm::vec3{c2.x, c2.y, 0.0f});
Engine::ParsedModel parsed;
parsed.path = keyBase + std::to_string(s);
Engine::ParsedMesh& mesh =
parsed.meshes.emplace_back();
mesh.vertices = std::move(pm.vertices);
mesh.lods.push_back(std::move(pm.indices));
mesh.cpuPositions = std::move(pm.cpuPositions);
mesh.name = "frag" + std::to_string(s);
if (hasEffMat) {
// 继承墙的生效材质:底色/贴图/UV 变换/透明全套沿用,
// 碎块与墙面目视一致(UV 已在 CellToPrism 归一到墙面)
mesh.params = effMat.params;
mesh.texPaths = effMat.texPaths;
mesh.transparent = effMat.transparent;
// 程序化纯色墙(无贴图路径):用脚本解析出的线性底色当
// baseColor——碎片 albedo 走默认白纹理,直乘即该色;
// 不走这步会退回 MaterialParams 的白色默认值
if (effSolid.a > 0.0f) {
mesh.params.baseColor = effSolid;
}
} else {
// 无宿主材质可继承(或 look.inheritFromHost=false):
// 用显式外观(玻璃靶 = 半透明 + 自发光)
mesh.params.baseColor = look.baseColor;
mesh.transparent = look.transparent;
mesh.params.emissiveFactor =
glm::vec4{look.emissive, 1.0f};
mesh.params.surface.z = look.emissiveStrength;
}
const Engine::ModelRef ref =
assets.LoadModel(parsed.path, std::move(parsed));
refs.push_back(ref);
paths.push_back(parsed.path);
}
// ---- spawn 碎片实体:pos/rot 复用宿主位姿(mesh 已真
// 尺寸,scale 恒 1 防 ConvexHull 顶点二次缩放),碎裂
// 瞬间完美拼合原面,动起来后沿裂纹自然分离 ----
for (size_t s = 0; s < refs.size(); ++s) {
if (!refs[s].Valid()) continue;
auto frag = host.spawn("fragment");
auto& tr = frag.GetComponent<
Engine::ECS::TransformComponent>();
tr.SetPosition(wallPos);
tr.SetRotationQuat(wall.GetWorldRotationQuat());
tr.SetScale(glm::vec3{1.0f});
auto& fmr =
frag.AddComponent<
Engine::ECS::MeshRendererComponent>();
fmr.model = refs[s];
// 宿主墙用 .material 覆盖时,碎块直接复用同一份资产:外观
//(含透明通道)与墙完全一致,且不需要引擎改判定
if (!hostOverridePath.empty()) {
fmr.materialOverrides[0] = hostOverridePath;
}
// Dynamic MeshCollider → ConvexHull(数据源 = 刚上传
// 的 cpuPositions;Box 字段不参与 Mesh 形状)
auto& frb =
frag.AddComponent<
Engine::ECS::RigidBodyComponent>();
frb.type =
Engine::ECS::RigidBodyComponent::BodyType::Dynamic;
frb.collider =
Engine::ECS::RigidBodyComponent::ColliderType::Mesh;
frb.mass = 0.3f;
frb.friction = 0.7f;
frb.restitution = 0.05f;
// 碎裂初速(玻璃式,非爆炸):径向 = 碎片质心 - 命中
// 点,离命中点越远越接近静止(指数衰减)——只有孔洞
// 周边的碎片被冲击波推动;法线外推让碎片朝弹侧剥出
glm::vec3 dir = centroids[s] - hitPoint;
const f32 len = glm::length(dir);
if (len < 1e-3f) {
dir = glm::vec3{Rand(-1.0f, 1.0f), 0.6f,
Rand(-1.0f, 1.0f)};
} else {
dir /= len;
}
const f32 falloff = std::exp(-len * p.falloff);
const glm::vec3 vel =
dir * p.burstSpeed * falloff *
Rand(0.4f, 1.1f) +
hitNormal * p.normalPop * Rand(0.3f, 0.8f) +
glm::vec3{0.0f, p.upBias * Rand(0.0f, 0.5f),
0.0f};
// 总量上限:超限立即回收最老碎片(防泄漏的兜底闸;
// 寿命清理才是常态回收)。模型延迟一帧卸载
const u32 bits = BitsOf(frag);
if (m_Fragments.size() >=
static_cast<size_t>(p.maxFragments)) {
host.destroy(EntityFromBits(m_Fragments.front().bits));
if (!m_Fragments.front().modelPath.empty()) {
m_PendingUnload.push_back(
std::move(m_Fragments.front().modelPath));
}
m_Fragments.erase(m_Fragments.begin());
}
m_Fragments.push_back(
Fragment{bits, vel, 0.0f, false, paths[s]});
}
}
// ---- 模型回收:延迟一帧 + 批量停飞卸载(Destroy 是帧尾延迟,
// 碎片实体本帧渲染收集仍会引用模型;下一帧实体已灭,但上一帧
// 提交的命令缓冲仍可能引用顶点缓冲/sampler——批量接口先
// WaitDeviceIdle 再销毁,与资产重建换绑同一引擎先例)----
void ApplyPendingUnload(const HostView& host) {
if (m_PendingUnload.empty()) return;
host.app.GetAssets().UnloadModelsByPaths(m_PendingUnload);
m_PendingUnload.clear();
}
// ---- 施速待办:body 建好(bodyValid)即注入,勿重复 ----
void ApplyPendingVelocities(const HostView& host) {
auto& registry = host.app.GetScene().GetRegistry();
for (auto it = m_Fragments.begin();
it != m_Fragments.end();) {
if (it->applied) { ++it; continue; }
const ECS::Entity e = EntityFromBits(it->bits);
if (!registry.valid(e)) { // 实体没了(外部销毁)
// 模型是独占资源(一代一键),实体没了模型也回收
if (!it->modelPath.empty()) {
m_PendingUnload.push_back(
std::move(it->modelPath));
}
it = m_Fragments.erase(it);
continue;
}
const auto* rb = registry.try_get<
Engine::ECS::RigidBodyComponent>(e);
if (rb && rb->bodyValid) {
host.app.GetPhysics().SetLinearVelocity(
it->bits, it->velocity);
it->applied = true;
}
++it; // body 未建:留待下一帧(对账晚一帧必到)
}
}
// ---- 寿命清理:到期 Destroy(延迟队列,遍历安全)----
void AgeAndCleanup(const HostView& host, f32 dt) {
auto& registry = host.app.GetScene().GetRegistry();
for (auto it = m_Fragments.begin();
it != m_Fragments.end();) {
it->age += dt;
if (it->age < p.lifetime) { ++it; continue; }
const ECS::Entity e = EntityFromBits(it->bits);
if (registry.valid(e)) {
host.destroy(e);
}
// 模型延迟一帧卸载(本帧渲染收集还引用它)
if (!it->modelPath.empty()) {
m_PendingUnload.push_back(std::move(it->modelPath));
}
it = m_Fragments.erase(it);
}
}
struct Fragment {
u32 bits = 0; // PhysicsWorld 键(实体位模式)
glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入)
f32 age = 0.0f; // 存活秒数(寿命清理用)
bool applied = false; // 初速已注入
std::string modelPath; // 独占模型缓存键(回收时按路径卸载)
};
bool m_Fired = false; // 一次性触发保护
std::vector<Fragment> m_Fragments; // 活碎片簿记
// 待卸载模型路径(碎片销毁的下一帧执行——Destroy 帧尾延迟,
// 本帧渲染收集仍引用模型,同帧卸载会渲染悬空指针)
std::vector<std::string> m_PendingUnload;
};
} // namespace Engine::Fracture
#endif //HELLOVULKAN_FRACTURE_LIB_H
+88
View File
@@ -0,0 +1,88 @@
//
// PrefabTest.cpp:阶段2 验收脚本——脚本侧 Instantiate(prefab) 运行时
// 实例化链路。
//
// PrefabTest(挂任意空实体,作为实例化锚点):
// OnStart 按参数路径实例化一次 prefab;Interval > 0 时周期性再实例化
// (多实例验收:每次 UUID 全量再生、物理体独立、沿 X 向排开下落)。
//
// 覆盖的引擎设施:
// - ScriptBehavior::Instantiate(path, pos, rot)(Unity Object.
// Instantiate 同位)→ SceneSerializer::InstantiatePrefab
// - prefab 子树含 RigidBody/ScriptComponent 时由 PhysicsWorld/
// ScriptRuntime 下一帧对账消化(与 Spawn 同语义)
//
// 验收步骤:
// 1. 编辑器搭一个实体子树(如 Dynamic 盒刚体 + MeshRenderer 的箱子,
// 可再挂个子实体验证层级一起走)→ Outliner 右键 Save As Prefab
// (落盘 assets/prefabs/<名字>.prefab)
// 2. 建空实体挂 PrefabTest,参数 PrefabPath 填该路径 → Play:
// 锚点处出现实例;Interval 设 1.0 可看连续多实例物理下落互不串扰
// 3. 编辑态也可从 Content Browser 把 .prefab 拖进 Outliner(节点=
// 挂其下,空白区=挂根)或双击直接实例化——UUID 独立、资产引用有效
//
#include <Engine/Scripting/Script.h>
#include <glm/glm.hpp>
using namespace Engine; // f32 / ScriptBehavior 等免前缀(对齐 TestShoot)
namespace {
using Engine::Scripting::ParamBuilder;
class PrefabTest final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "PrefabTest"; }
void DeclareParams(ParamBuilder& pb) override {
// prefab 资产路径(工作目录相对,同场景/资产管理约定)
pb.Field("PrefabPath", &m_PrefabPath,
std::string{"assets/prefabs/crate.prefab"});
// 周期实例化间隔(秒;<=0 = 仅 OnStart 实例化一次)
pb.Field("Interval", &m_Interval, 0.0f);
// 相邻实例的 X 向排开间距(多实例不叠在一起)
pb.Field("SpreadX", &m_SpreadX, 2.0f);
}
void OnStart() override {
m_Timer = m_Interval;
SpawnInstance();
}
void OnUpdate(f32 dt) override {
if (m_Interval <= 0.0f) {
return;
}
m_Timer -= dt;
if (m_Timer <= 0.0f) {
m_Timer = m_Interval;
SpawnInstance();
}
}
private:
void SpawnInstance() {
// 锚点实体位姿 + 排开偏移;旋转 = 锚点自身旋转
const auto& tr = Transform();
const glm::vec3 pos =
tr.GetPosition() + glm::vec3{m_SpreadX * f32(m_Count),
0.0f, 0.0f};
const glm::quat rot = tr.GetRotationQuat();
if (Instantiate(m_PrefabPath, pos, rot) == entt::null) {
return; // 路径无效/解析失败:日志已在序列化器里打过
}
++m_Count;
}
std::string m_PrefabPath;
f32 m_Interval = 0.0f;
f32 m_SpreadX = 2.0f;
f32 m_Timer = 0.0f;
int m_Count = 0; // 已实例化份数(排开偏移基准)
};
ENGINE_REGISTER_SCRIPT(PrefabTest)
} // namespace
+17
View File
@@ -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__)
+226
View File
@@ -0,0 +1,226 @@
//
// TestShoot.cpp:阶段1 验收脚本——脚本侧 spawn/destroy 底座 + 碰撞事件
// 转发的端到端链路。
//
// 三个脚本协作(放一个文件方便对照):
// TestShoot(挂任意实体,如相机实体):
// 鼠标左键上升沿 → 从编辑器相机沿视线 spawn 子弹实体
// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入)
// Bullet(子弹实体,由 TestShoot 运行时装配):
// OnCreate 施加枪口初速(body 已由当帧 Step 对账建好);
// OnCollisionEnter → 命中点 spawn ImpactFx + 自毁;
// 超时自毁防飞出地图泄漏;存活期画调试十字 = 弹道可视化
// ImpactFx(特效实体,由 Bullet 运行时装配,空实体 + 调试点标):
// 扩张十字 + 白→红渐隐,寿命尽自毁——占位特效
//
// 覆盖的引擎设施:
// - Spawn()/Destroy():ScriptBehavior 实体底座(Scene::CreateEntity/
// DestroyEntity 延迟队列)
// - 组件运行时装配:RigidBody(PhysicsWorld 下一帧对账建 body)、
// ScriptComponent(ScriptRuntime 下一帧对账建实例)
// - OnCollisionEnter/Exit:PhysicsWorld::GetEvents → ScriptRuntime
// 帧首派发
//
// 验收步骤:编辑器给任意实体挂 TestShoot → Play → RMB 飞行视角瞄准
// 墙体 → 左键射击:弹道点飞出 → 撞墙瞬间原地绽放十字特效、子弹消失。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Input.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
#include <glm/glm.hpp>
using namespace Engine; // Input / Debug 门面
namespace {
// ---- 子弹脚本(TestShoot 运行时挂载;不手动挂场景)----
class Bullet final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "Bullet"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
// 枪口初速(世界系 m/s):TestShoot spawn 时按视线方向写进
// 脚本项参数——数据经参数层传递,与序列化同一条路
pb.Field("Velocity", &m_Velocity, glm::vec3{0.0f});
pb.Field("Lifetime", &m_Lifetime, 5.0f); // 超时自毁(秒)
}
void OnCreate() override {
// body 已由本帧 Step 的 SyncScene 建好(spawn 发生在上帧
// OnUpdate):这里施加初速,物理从下步起按弹道积分
if (glm::length(m_Velocity) > 1e-4f) {
App().GetPhysics().SetLinearVelocity(EntityBits(), m_Velocity);
}
}
void OnUpdate(f32 dt) override {
m_Age += dt;
if (m_Age >= m_Lifetime) {
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
return;
}
// 弹道可视化:命中点即弹着点(橙十字 = 弹头,速度线 = 尾迹)
const glm::vec3 pos = Transform().GetPosition();
const glm::vec3 vel =
App().GetPhysics().GetLinearVelocity(EntityBits());
Debug::Cross(pos, 0.09f, Debug::Rgb(0xFF8C1A),
/*width*/ 2.0f, /*duration*/ 0.0f,
/*depthTest*/ false);
if (glm::length(vel) > 0.5f) {
Debug::Arrow(pos, pos + vel * 0.03f, Debug::Rgb(0xFFB366),
/*headSize*/ 0.04f, /*width*/ 2.0f,
/*duration*/ 0.0f, /*depthTest*/ false);
}
}
void OnCollisionEnter(u32 other, const glm::vec3& point,
const glm::vec3& normal) override {
// 防重入:Destroy 是帧尾延迟执行,命中帧内子弹与碎片群
// 可产生多对接触事件(碎片云一块挨一块),无标志会一帧
// 爆出 N 个特效(同 WallFracture m_Fired 的一次性触发模式)
if (m_Hit) return;
m_Hit = true;
(void)other; (void)normal; // 法线留给将来定向溅射
// 命中特效:空实体锚在命中点,ImpactFx 脚本项由下一帧对账
// 实例化(Spawn 即时建实体,脚本实例化天然晚一帧)
auto fx = Spawn("impact");
fx.GetComponent<Engine::ECS::TransformComponent>()
.SetPosition(point);
auto& sc = fx.AddComponent<Engine::ECS::ScriptComponent>();
Engine::ECS::ScriptComponent::Item item;
item.name = "ImpactFx";
sc.items.push_back(std::move(item));
// 自毁:延迟队列,帧尾统一执行(事件派发循环安全)
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
}
private:
glm::vec3 m_Velocity{0.0f};
f32 m_Lifetime = 5.0f;
f32 m_Age = 0.0f;
bool m_Hit = false; // 首次命中置位(防同帧多次接触重复特效)
};
// ---- 命中特效脚本(Bullet 运行时挂载;空实体 + 调试点标占位)----
class ImpactFx final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "ImpactFx"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Duration", &m_Duration, 0.4f); // 特效寿命(秒)
}
void OnUpdate(f32 dt) override {
m_Age += dt;
if (m_Age >= m_Duration) {
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
return;
}
// 扩张十字 + 白→红渐隐(占位:将来换 VFX/粒子系统)
const f32 t = m_Age / m_Duration;
const glm::vec3 pos = Transform().GetPosition();
Debug::Cross(pos, 0.12f + t * 0.55f,
glm::vec4{1.0f, 1.0f - t, 1.0f - t, 1.0f},
/*width*/ 3.0f, /*duration*/ 0.0f,
/*depthTest*/ false);
}
private:
f32 m_Duration = 0.4f;
f32 m_Age = 0.0f;
};
// ---- 枪口脚本(验收主体:挂到任意实体,位置/朝向取编辑器相机)----
class TestShoot final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "TestShoot"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("BulletSpeed", &m_BulletSpeed, 25.0f); // 枪口初速 m/s
pb.Field("MuzzleOffset", &m_MuzzleOffset, 0.6f); // 出膛距离 m
pb.Field("FireCooldown", &m_FireCooldown, 0.15f); // 最小射击间隔 s
pb.Field("BulletRadius", &m_BulletRadius, 0.12f); // 弹头碰撞半径 m
// 半径 0.12 的反隧穿账:物理离散检测(无 CCD),检测带 =
// 半径 + 障碍半厚;薄板(如 0.2m 墙)窗口 2*(0.12+0.1)=0.44m
// > 每步进位移 0.42m(25 m/s @ 1/60),垂直命中保证检出。
// 半径越小高速弹越易整步跨过薄碰撞体(无接触=无事件)
}
void OnUpdate(f32 dt) override {
m_Cooldown -= dt;
// 左键上升沿触发(电平查询 + 上帧状态对比,Unity
// GetMouseButtonDown 边沿语义的手工等价物)
const bool down =
Input::IsMouseButtonDown(Input::MouseButton::Left);
const bool fire = down && !m_FireWasDown &&
m_Cooldown <= 0.0f;
m_FireWasDown = down;
if (fire) {
SpawnBullet();
m_Cooldown = m_FireCooldown;
}
}
private:
void SpawnBullet() {
// 枪口 = 编辑器相机视线前方(编辑形态 RMB 飞行瞄准;
// 与 WASDMove 的朝向来源一致)
const auto& cam =
App().GetRenderer().GetEditorCamera().GetTransform();
const glm::vec3 fwd =
cam.GetRotationQuat() * glm::vec3(0.0f, 0.0f, -1.0f);
const glm::vec3 muzzle =
cam.GetWorldPosition() + fwd * m_MuzzleOffset;
// ---- 子弹实体装配(spawn 底座 + 组件运行时装配演示)----
auto bullet = Spawn("bullet");
auto& tr = bullet.GetComponent<Engine::ECS::TransformComponent>();
tr.SetPosition(muzzle);
tr.SetRotationQuat(cam.GetRotationQuat());
// Dynamic 球刚体:参数给默认档(轻质量/低弹性,命中即停)
auto& rb = bullet.AddComponent<Engine::ECS::RigidBodyComponent>();
rb.type = Engine::ECS::RigidBodyComponent::BodyType::Dynamic;
rb.collider =
Engine::ECS::RigidBodyComponent::ColliderType::Sphere;
rb.sphereRadius = m_BulletRadius;
rb.mass = 0.2f;
rb.friction = 0.3f;
rb.restitution = 0.1f;
// Bullet 脚本项:初速/寿命经参数层传入(与序列化同一条路,
// 下一帧 ScriptRuntime 对账实例化)
auto& sc = bullet.AddComponent<Engine::ECS::ScriptComponent>();
Engine::ECS::ScriptComponent::Item item;
item.name = "Bullet";
item.params.push_back(
{"Velocity",
Engine::Scripting::ParamValue{fwd * m_BulletSpeed}});
item.params.push_back(
{"Lifetime", Engine::Scripting::ParamValue{5.0f}});
sc.items.push_back(std::move(item));
}
f32 m_BulletSpeed = 25.0f;
f32 m_MuzzleOffset = 0.6f;
f32 m_FireCooldown = 0.15f;
f32 m_BulletRadius = 0.08f;
f32 m_Cooldown = 0.0f;
bool m_FireWasDown = false; // 左键上升沿检测基准
};
ENGINE_REGISTER_SCRIPT(Bullet)
ENGINE_REGISTER_SCRIPT(ImpactFx)
ENGINE_REGISTER_SCRIPT(TestShoot)
} // namespace
+188
View File
@@ -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
+79 -31
View File
@@ -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 相机(第三人称相机脚本每帧写它
.GetEditorCamera() // 的朝向——玩家看到的视角就是移动参考);无相机实体时
.GetTransform() //(纯独立形态)回退编辑器相机
.GetRotationQuat(); glm::quat cq;
bool refCam = false;
auto camView = App().GetScene()
.GetRegistry()
.view<Engine::ECS::CameraComponent>();
for (auto e : camView) {
const auto& cam =
camView.get<Engine::ECS::CameraComponent>(e);
if (cam.primary && cam.active) {
cq = App().GetScene()
.GetRegistry()
.get<Engine::ECS::TransformComponent>(e)
.GetRotationQuat();
refCam = true;
break;
}
}
if (!refCam) {
cq = App().GetRenderer()
.GetEditorCamera()
.GetTransform()
.GetRotationQuat();
}
const glm::vec3 fwd = cq * glm::vec3(0.0f, 0.0f, -1.0f); const glm::vec3 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,调试图)
}; };
+87
View File
@@ -0,0 +1,87 @@
//
// WallFracture.cpp:可碎墙脚本——碎裂逻辑已提取至 FractureLib.h
//(打靶场 Target 复用同一条路径),本文件只剩宿主职责:
// DeclareParams 直绑 Burst::p 各字段(参数名/默认值与历史档位
// 逐一同名同值,旧场景不受影响)
// OnCollisionEnter → Burst::TryFire(一次性保护 + 对方撞击来源
// 与速度校验 + 藏墙 + Voronoi 切分 + spawn 碎片群,库内完成)。
// 撞击来源 = 动态刚体(子弹/抛体)或角色(玩家撞墙)——角色是
// CharacterVirtual,其碰撞事件由 PhysicsWorld 从角色接触合成
// (Jolt 的 ContactListener 只回调刚体,不覆盖角色)
// OnUpdate → Burst::Tick(施速待办 / 寿命清理 / 模型回收)
// OnDestroy → Burst::Shutdown(独占碎片模型批量停飞卸载)
//
// 外观默认继承墙的生效材质(底色/贴图/透明),碎块与墙面观感一致;
// 要独立外观(玻璃/发光碎片)置 Burst::look.inheritFromHost = false
// 再用 baseColor/emissive 显式指定。
//
// 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube +
// RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里
// 任意实体挂 TestShoot → Play → RMB 飞行瞄准墙体 → 左键射击:
// 墙沿裂纹碎成不规则碎片,孔洞周边碎块小、飞散明显,边缘大块
// 塌落堆积在墙脚,若干秒后清空。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Types.h>
#include "FractureLib.h"
using namespace Engine; // u32/f32/i32 基础类型(历史脚本同款)
namespace {
// ---- 可碎墙(挂墙实体;触发后由 Burst 管一整批碎片)----
class WallFracture final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "WallFracture"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
// Voronoi 种子数 = 碎片数(40% 在命中点半径内加密 → 孔洞
// 周边碎块小、边缘大块的玻璃碎分布;最小间距防碎片退化)
// 直绑 Burst::p:碎裂逻辑只读 Burst::p,绑到别处(曾经的
// 独立 m_P)会让计数器/寿命等全部失效
pb.Field("Count", &m_Burst.p.count, 30);
pb.Field("SeedRadius", &m_Burst.p.seedRadius, 0.9f); // 加密半径 m
pb.Field("MinSeedDist", &m_Burst.p.minSeedDist, 0.12f); // 种子最小间距 m
// 玻璃/陶瓷式碎裂(裂纹分离 + 重力塌落),非爆炸飞散:
// 初速 = 命中点径向 × 距离指数衰减(近动远不动)+ 法线
// 轻微外推(剥落)+ 微弱上抛;重力是塌落的主导
pb.Field("BurstSpeed", &m_Burst.p.burstSpeed, 1.2f); // 近点径向初速 m/s
pb.Field("Falloff", &m_Burst.p.falloff, 1.0f); // 距离衰减率 1/m
pb.Field("NormalPop", &m_Burst.p.normalPop, 0.4f); // 法线外推 m/s
pb.Field("UpBias", &m_Burst.p.upBias, 0.3f); // 上抛偏置 m/s
pb.Field("Lifetime", &m_Burst.p.lifetime, 6.0f); // 碎片寿命(秒)
pb.Field("MaxFragments", &m_Burst.p.maxFragments, 128); // 活碎片上限
pb.Field("MinImpactSpeed", &m_Burst.p.minImpactSpeed, 2.0f); // 触发阈值
}
void OnCollisionEnter(u32 other, const glm::vec3& point,
const glm::vec3& normal) override {
// 触发条件在库内:对方须是够快的撞击来源——动态刚体
//(子弹/抛体)或角色(玩家撞墙);速度阈值过滤静止贴靠
m_Burst.TryFire(HV(), other, point, normal);
}
void OnUpdate(f32 dt) override { m_Burst.Tick(HV(), dt); }
// 墙销毁(PIE stop 换血 / 手动删除):碎片实体由场景清亡处理,
// 独占碎片模型统一批量回收
void OnDestroy() override { m_Burst.Shutdown(HV()); }
private:
// 库桥接:本成员函数内可访问 protected 基类设施(App/Spawn/
// Destroy),库内只走公共 API——见 FractureLib.h HostView 注
Engine::Fracture::HostView HV() {
return {App(), EntityBits(),
[this](const std::string& n) { return Spawn(n); },
[this](Engine::ECS::Entity e) { Destroy(e); }};
}
// 碎片簿记 + 生命周期;参数即 m_Burst.p(DeclareParams 直绑其字段)
Engine::Fracture::Burst m_Burst;
};
} // namespace
ENGINE_REGISTER_SCRIPT(WallFracture)
+1 -1
View File
@@ -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)
+8
View File
@@ -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 的表现一致(无动画/组件禁用实体的
+112 -15
View File
@@ -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
@@ -182,6 +182,18 @@ namespace Engine
ModelRef LoadModel(const std::string& path, ParsedModel&& parsed); ModelRef LoadModel(const std::string& path, ParsedModel&& parsed);
Model* GetModel(ModelRef h) const { return m_Models.Get(h); } Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
void UnloadModel(ModelRef h) { m_Models.Unload(h); } void UnloadModel(ModelRef h) { m_Models.Unload(h); }
// 批量按路径卸载模型(未加载为 no-op)。先 GPU 全停飞再销毁
//(在飞命令缓冲可能仍引用顶点缓冲/默认纹理 sampler——
// 与 ReloadMaterialAsset 的 WaitDeviceIdle 同一先例);一次
// 停飞摊薄整批,调用方攒批降频(脚本碎片回收每帧一批)
void UnloadModelsByPaths(const std::vector<std::string>& paths)
{
if (paths.empty()) return;
m_Uploader.WaitDeviceIdle();
for (const std::string& p : paths) {
m_Models.UnloadByPath(p);
}
}
// 按路径查已加载的模型(未加载返回 null,不触发加载)—— // 按路径查已加载的模型(未加载返回 null,不触发加载)——
// 面板 hover 信息卡用 // 面板 hover 信息卡用
Model* GetModelByPath(const std::string& path) const Model* GetModelByPath(const std::string& path) const
@@ -213,6 +225,9 @@ namespace Engine
// 半透明平面(alpha 0-255,透明绘制队列):同参数缓存去重 // 半透明平面(alpha 0-255,透明绘制队列):同参数缓存去重
ModelRef CreateTransparentPlane(f32 halfSize, ModelRef CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a); u8 r, u8 g, u8 b, u8 a);
// 程序化立方体(halfSize 为半边长):同参数缓存去重。
// 尺寸变化走实体 Transform 缩放,勿按尺寸炸缓存
ModelRef CreateCube(f32 halfSize, u8 r, u8 g, u8 b);
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重, // 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
// 重复 Load 同一路径零成本(动态切换场景直接反复调用) // 重复 Load 同一路径零成本(动态切换场景直接反复调用)
@@ -42,6 +42,9 @@ namespace Engine::Assets {
virtual Scope<Model> CreateTransparentPlane(f32 halfSize, virtual Scope<Model> CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a) = 0; u8 r, u8 g, u8 b, u8 a) = 0;
// 程序化纯色立方体(halfSize 为半边长;占位几何/碎片模拟)
virtual Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) = 0;
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr) // 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0; virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
+113
View File
@@ -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
+17 -22
View File
@@ -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()
+12
View File
@@ -203,6 +203,10 @@ namespace Engine::ECS {
struct MeshRendererComponent { struct MeshRendererComponent {
ModelRef model; ModelRef model;
bool active = true; // 禁用 = 渲染/拾取跳过(Unity enabled / UE SetActive) bool active = true; // 禁用 = 渲染/拾取跳过(Unity enabled / UE SetActive)
// 单 mesh 子引用:-1 = 整模型全部;N = 只画第 N 个
//(GetMeshes 下标)。物理 Mesh 碰撞体自动跟随(BuildMeshShape
// 读本组件)。序列化缺省键 = -1 向前兼容
int meshIndex = -1;
std::unordered_map<u32, std::string> materialOverrides; std::unordered_map<u32, std::string> materialOverrides;
}; };
@@ -222,6 +226,11 @@ namespace Engine::ECS {
bool playing = true; bool playing = true;
bool loop = true; bool loop = true;
f32 speed = 1.0f; f32 speed = 1.0f;
// 根运动开关(Unity Animator "Apply Root Motion" 同语义):
// true = 保留根节点位移轨道(跑动 clip 自带位移会漂出锚点);
// false = 原地播放(丢弃动画子树根节点位移,旋转保留)——
// 位移归脚本/物理驱动的角色(CharacterController 等)用 false
bool applyRootMotion = true;
// ---- 播放头 / crossfade(瞬态,不落盘——重启从 0 重播)---- // ---- 播放头 / crossfade(瞬态,不落盘——重启从 0 重播)----
f32 time = 0.0f; // 当前 clip 播放头(秒,wrap 由消费端处理) f32 time = 0.0f; // 当前 clip 播放头(秒,wrap 由消费端处理)
@@ -471,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
+12 -1
View File
@@ -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;
+28 -4
View File
@@ -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,12 +61,25 @@ 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
// 槽默认兜底纹理是纯黑,无贴图时 Emissive Strength 乘出来恒黑
// ——无贴图想要发光就调这里(rgb × 贴图 × surface.z)
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 布局天然对齐
@@ -68,6 +87,11 @@ namespace Engine {
{1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f},
{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;
+22
View File
@@ -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,
@@ -158,6 +164,15 @@ namespace Engine
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b, u8 a); f32 halfSize, u8 r, u8 g, u8 b, u8 a);
// 程序化立方体(6 面 24 顶点纯色贴图,halfSize = 半边长):
// 通用占位几何(碎片模拟的墙/块等)。尺寸变化走实体 Transform
// 缩放——同 halfSize 同色共享一份缓存模型
struct CubeTag {};
static Scope<Model> CreateCube(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b);
~Model(); ~Model();
Model(const Model&) = delete; Model(const Model&) = delete;
@@ -209,6 +224,13 @@ namespace Engine
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba, ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
bool transparent); bool transparent);
// 程序化立方体骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 由
// 立方体路径填(6 面,顶点法线朝外)
Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
CubeTag, f32 halfSize, const std::array<u8, 4>& rgba);
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填 // 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
Model(Device& device, MemoryAllocator& allocator, Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
@@ -37,6 +37,7 @@ namespace Engine {
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override; Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
Scope<Model> CreateTransparentPlane(f32 halfSize, Scope<Model> CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a) override; u8 r, u8 g, u8 b, u8 a) override;
Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) override;
Scope<Cubemap> LoadEnvironment(const std::string& path) override; Scope<Cubemap> LoadEnvironment(const std::string& path) override;
Scope<Cubemap> LoadEnvironment(const std::string& path, Scope<Cubemap> LoadEnvironment(const std::string& path,
DecodedEquirect&& decoded) override; DecodedEquirect&& decoded) override;
+55 -1
View File
@@ -28,6 +28,7 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/ECS/Components.h> #include <Engine/ECS/Components.h>
#include <Engine/ECS/Scene.h> #include <Engine/ECS/Scene.h>
#include <Engine/ECS/EntityHandle.h> // 须在 Scene.h 之后(内联方法需完整 Scene)
#include <Engine/Scripting/ScriptParam.h> #include <Engine/Scripting/ScriptParam.h>
#include <string> #include <string>
@@ -69,6 +70,22 @@ namespace Engine {
// 在此按名取值) // 在此按名取值)
virtual void OnParamsApplied() {} virtual void OnParamsApplied() {}
// ---- 碰撞回调(PhysicsWorld::GetEvents 帧首派发,ScriptRuntime
// 转发;事件语义 = 最近一次物理步进)----
// Enter/Exit 由 Jolt Added/Removed 转发;Persisted 不回调
//(最小 API,接触持续期无每帧噪音)。对方是触发器(sensor)
// 时同样触发——后续按需拆 OnTriggerEnter/Exit。
// other = 对方实体位模式(还原:static_cast<entt::entity>(other),
// 回调时对方可能已销毁,用前自行 Scene::IsAlive 校验)。
// 派发在 Sync/OnUpdate 之前:回调里 spawn/destroy 安全
//(destroy 入延迟队列,spawn 由下一帧对账消化)
virtual void OnCollisionEnter(u32 other,
const glm::vec3& point,
const glm::vec3& normal) {
(void)other; (void)point; (void)normal;
}
virtual void OnCollisionExit(u32 other) { (void)other; }
protected: protected:
// 便捷访问:绑定实体的 TransformComponent // 便捷访问:绑定实体的 TransformComponent
ECS::TransformComponent& Transform() { ECS::TransformComponent& Transform() {
@@ -96,6 +113,38 @@ namespace Engine {
bool Valid() const { return m_Scene != nullptr && m_Entity != entt::null; } bool Valid() const { return m_Scene != nullptr && m_Entity != entt::null; }
// ---- 场景实体 spawn/destroy 底座(Unity Instantiate/Destroy
// 同位)----
// spawn:帧内即时创建(UUID + Transform + Name 出生即有),
// 组件由脚本自行 AddComponent 装配;RigidBody/ScriptComponent
// 分别由 PhysicsWorld/ScriptRuntime 下一帧对账消化
//
// 【选型约定:Spawn vs Instantiate】(Unity/UE 同题,靠约定
// 治理,判定只有一问):
// "这东西的参数要不要让非程序员改?"
// 要 → Save As Prefab + Instantiate(数据 = 真理来源)
// 不要 → Spawn(代码 = 真理来源:子弹/特效触发器等纯逻辑体)
// 三条纪律:
// 1. 一个对象只走一条路径——prefab 化后 Spawn 版本撤注册;
// 2. Spawn 的脚本构造保持"零参数搭建":发现 Spawn 后手动
// set 一堆组件 = 该 prefab 化的信号;
// 3. 注册表即白名单(ENGINE_REGISTER_SCRIPT 的类才能按名
// spawn),别为绕过约定注册"伪逻辑体"。
ECS::EntityHandle Spawn(const std::string& name);
// destroy:延迟——入队,帧尾 FlushDestroyQueue 统一执行
//(遍历安全);调用方持有的句柄此后悬空
void Destroy(ECS::Entity e);
// ---- Prefab 实例化(Unity Object.Instantiate 同位)----
// .prefab 文件 → 整棵子树追加进场景(UUID 全量再生,子树内
// Joint.target 自动重映射)。pos/rot = 根实体 local 位姿
//(根挂场景根时即世界位姿)。失败返回 entt::null;
// RigidBody/ScriptComponent 由 PhysicsWorld/ScriptRuntime
// 下一帧对账消化(与 Spawn 同语义)
ECS::Entity Instantiate(const std::string& prefabPath,
const glm::vec3& pos,
const glm::quat& rot = glm::quat{});
// ---- 组件启停(Unity SetActive / Behaviour.enabled 语义)---- // ---- 组件启停(Unity SetActive / Behaviour.enabled 语义)----
// 自身组件总开关(整个脚本组件停 tick,实例保留) // 自身组件总开关(整个脚本组件停 tick,实例保留)
void SetActive(bool on) { void SetActive(bool on) {
@@ -236,7 +285,12 @@ namespace Engine {
} // namespace Engine } // namespace Engine
// 文件级自注册:脚本 cpp 里写一行即可,Inspector 下拉与序列化按 // 文件级自注册:脚本 cpp 里写一行即可,Inspector 下拉与序列化按
// 类名(字符串)解析。T 须默认可构造并继承 ScriptBehavior // 类名(字符串)解析。T 须默认可构造并继承 ScriptBehavior。
//
// 注册 = UE 的 UCLASS(Blueprintable) 表态:本类声明为"逻辑体"——
// 允许运行时按名 Spawn(脚本 Spawn()/运行时注册表白名单)。需要
// 美术调参/编辑器复用的组合体不要注册(走 Save As Prefab +
// Instantiate 路径),防止同一对象出现 Spawn/prefab 两条权威链
#define ENGINE_REGISTER_SCRIPT(T) \ #define ENGINE_REGISTER_SCRIPT(T) \
static const bool s_##T##_registered = [] { \ static const bool s_##T##_registered = [] { \
::Engine::Scripting::ScriptRegistry::Register( \ ::Engine::Scripting::ScriptRegistry::Register( \
@@ -66,6 +66,10 @@ namespace Engine {
private: private:
void Sync(); void Sync();
// 碰撞事件派发(Update 开头、Sync 之前):PhysicsWorld 最近
// 一次步进的事件转发到涉事实例的 OnCollisionEnter/Exit。
// Persisted 跳过;实例未建/已禁用不收
void DispatchCollisions();
ECS::Scene& m_Scene; ECS::Scene& m_Scene;
Application* m_App; // 注入脚本上下文 Application* m_App; // 注入脚本上下文
@@ -23,6 +23,9 @@
#ifndef HELLOVULKAN_SCENESERIALIZER_H #ifndef HELLOVULKAN_SCENESERIALIZER_H
#define HELLOVULKAN_SCENESERIALIZER_H #define HELLOVULKAN_SCENESERIALIZER_H
#include <Engine/ECS/Scene.h> // Entity + glm vec3/quat(Prefab API 签名用)
#include <Engine/Core/AssetRef.h> // ModelRef(ResolveProcedural 返回值)
#include <string> #include <string>
namespace Engine::ECS { class Scene; } namespace Engine::ECS { class Scene; }
@@ -66,6 +69,38 @@ namespace Engine::Serialization {
static bool DeserializeFromString(ECS::Scene& scene, static bool DeserializeFromString(ECS::Scene& scene,
Assets::AssetManager& assets, Assets::AssetManager& assets,
const std::string& yamlText); const std::string& yamlText);
// ---- Prefab(实体子树存取;与场景共用实体段读写,实现见 .cpp)----
// 保存 root 子树(根 + 全部后代,DFS 序:父先于子)到 outPath。
// 格式 = 场景实体段子集:"Prefab:" 文档头 + Version + Entities
// 列表(组件段与场景逐键一致)。首实体 = 根(实例化约定)。
// 失败(根无效/无法写文件)返回 false
static bool SerializePrefab(const ECS::Scene& scene,
Assets::AssetManager& assets,
ECS::Entity root,
const std::string& outPath);
// 实例化 Prefab 到 scene(追加式,不换血)。UUID 全量再生——
// 同一 prefab 多次实例化、与场景既有实体均不冲突;子树内
// Joint.target 按旧→新重映射,指向子树外的引用保持原值。
// overridePos/overrideRot 非空时覆写根实体 local 位姿(根挂
// 场景根时即世界位姿);空指针 = 保留 prefab 授权位姿。
// 返回根实体(= 文件首实体);失败(解析失败/无实体)返回
// entt::null——逐实体容错与场景加载同语义,失败前可能已部分
// 实例化(单实体损坏只跳过该项)
static ECS::Entity InstantiatePrefab(
ECS::Scene& scene, Assets::AssetManager& assets,
const std::string& inPath,
const glm::vec3* overridePos = nullptr,
const glm::quat* overrideRot = nullptr);
// procedural:// 伪路径 → 参数反解重建(cube/plane/
// transparent-plane)。确定性键必然命中缓存;绝不走 assimp。
// 未知/格式坏伪路径返回无效句柄。场景加载与编辑器程序化
// 入口共用——键格式唯一定义点
static ModelRef ResolveProcedural(
Assets::AssetManager& assets, const std::string& path);
}; };
} // namespace Engine::Serialization } // namespace Engine::Serialization
@@ -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);
+542 -127
View File
@@ -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);
const auto collides = [&](const std::string& n) { for (const auto& ch : clip.channels) {
for (const auto& c : target.GetClips()) { if (ch.nodeIdx < srcByNode.size() && !ChannelEmpty(ch)) {
if (c.name == n) return true; srcByNode[ch.nodeIdx] = &ch;
}
for (const auto& c : extra) {
if (c.name == n) return true;
}
return false;
};
while (collides(out.name)) {
out.name = stem + "_" + out.name;
} }
} }
for (auto& ch : clip.channels) {
if (ch.positions.empty() && ch.rotations.empty() && // ---- 本 clip 的根运动源:承载节点里最深且有轨道者 ----
ch.scales.empty()) { // (更深者世界变换已含其全部祖先运动,取一个即全包括)
continue; // 空轨道无用 u32 rootSrc = kNoNode;
for (const u32 c : pl.carriers) { // carriers 浅→深
if (srcByNode[c] != nullptr) rootSrc = c;
}
// ---- 目标节点 → 源轨道(可控节点,供烘焙局部化)----
std::unordered_map<u32, u32> jointBakeDst; // dst → src(Joint/Root)
std::unordered_map<u32, u32> directDst; // dst → src(Direct)
for (u32 si = 0; si < srcNodes.size(); ++si) {
const SrcSlot& s = pl.bySrc[si];
if (srcByNode[si] == nullptr) continue;
switch (s.kind) {
case Kind::Direct:
directDst.emplace(s.dst, si);
break;
case Kind::Joint:
jointBakeDst.emplace(s.dst, si);
break;
case Kind::Root:
if (si == rootSrc) jointBakeDst.emplace(s.dst, si);
break;
default:
break;
} }
}
// ---- 旋转键:两遍式烘焙 ----
// 采样时刻 = 全部需烘焙通道(Joint/Root)的旋转键时刻并集;
// 逐 t 跑源全骨架 FK → 逐 dst 递推世界旋转 → 局部化写键
std::unordered_map<u32, std::vector<RotKey>> baked;
{
std::vector<f32> times;
for (u32 si = 0; si < srcNodes.size(); ++si) {
const SrcSlot& s = pl.bySrc[si];
const AnimChannel* ch = srcByNode[si];
if (ch == nullptr) continue;
const bool bakeThis =
s.kind == Kind::Joint ||
(s.kind == Kind::Root && si == rootSrc);
if (!bakeThis || ch->rotations.empty()) continue;
for (const auto& k : ch->rotations) times.push_back(k.time);
}
std::sort(times.begin(), times.end());
times.erase(std::unique(times.begin(), times.end()),
times.end());
if (!times.empty()) {
std::vector<glm::quat> Rw(dstNodes.size(), kIdentity);
for (const f32 t : times) {
const auto Wsrc = SrcWorldAt(srcNodes, srcByNode, t);
for (u32 i = 0; i < dstNodes.size(); ++i) {
const i32 pi = dstNodes[i].parent;
const glm::quat Rpar =
pi >= 0 ? Rw[static_cast<size_t>(pi)]
: kIdentity;
glm::quat RwI;
const auto jb = jointBakeDst.find(i);
if (jb != jointBakeDst.end()) {
// 关节/根/手指/趾:世界增量搬运(对两侧
// 骨骼的局部轴向约定免疫)
const u32 si = jb->second;
const glm::quat dW =
pl.align *
(RotOf(Wsrc[si]) *
glm::inverse(pl.srcTPose
[static_cast<size_t>(si)])) *
glm::inverse(pl.align);
RwI = dW * pl.dstTPose[static_cast<size_t>(i)];
} else if (auto db = directDst.find(i);
db != directDst.end()) {
// 直搬骨:anim 后世界 = 父 × 源局部(供其子
// 关节局部化用;自身轨道另走原样直搬)
const u32 si = db->second;
const glm::quat loc = SampleQ(
srcByNode[si]->rotations, t,
Decompose(srcNodes[si].localTransform).rot);
RwI = Rpar * loc;
} else {
RwI = Rpar * Decompose(
dstNodes[i].localTransform).rot;
}
Rw[static_cast<size_t>(i)] = RwI;
if (jb != jointBakeDst.end()) {
baked[i].push_back(
{t, glm::inverse(Rpar) * RwI});
}
}
}
}
}
// ---- 组装轨道(每 dst 至多一条;owner 在 Plan 阶段已保证)----
// 非 const 引用:同名直搬路径整段 move 源轨道(此后 src 不再
// 被读——烘焙已在上面完成)
for (auto& ch : clip.channels) {
if (ChannelEmpty(ch)) continue;
if (ch.nodeIdx >= srcNodes.size()) { 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);
outCh.positions.reserve(ch.positions.size());
for (const auto& k : ch.positions) {
outCh.positions.push_back(
{k.time, s.dstRestPos +
(s.M * (k.value - srcRest.pos)) *
s.posScale});
}
// 旋转:烘焙结果(多源汇一 dst 时共享同份)
if (auto bk = baked.find(s.dst); bk != baked.end()) {
outCh.rotations = bk->second;
}
// 缩放不搬运:跨骨架源的缩放轨道常承载单位换算
// (kwy 根节点的 cm 补偿等),原样拷到目标会整体放大
// 骨骼;目标保持自身 rest 缩放(人体动画一般无缩放)
out.channels.push_back(std::move(outCh));
++res.channelsRetargeted;
// ② 槽位重定向:增量烘焙到目标骨架局部空间。 const bool newRow = std::none_of(
// 平移增量按骨长比缩放(|dstRest| / |srcRest|)—— res.retargetRows.begin(), res.retargetRows.end(),
// 两资产单位/比例不同源(FBX 常为 cm、glTF 为 m)时 [&](const RetargetRow& r)
// 不缩放会把模型拽飞;rest≈0 的根骨骼比例取 1 { return r.srcName == boneName; });
const auto mapIt = retargetMap.find(ch.nodeIdx); if (newRow) {
if (mapIt == retargetMap.end()) { const HumanoidSlot slot = SlotAt(srcNodes, ch.nodeIdx);
++res.channelsSkipped; // 两级都落不上(手指/道具等) res.retargetRows.push_back(
continue; {s.kind == Kind::Root
} ? "RootMotion"
const TRS srcRest = Decompose( : HumanoidSlotName(slot),
srcNodes[ch.nodeIdx].localTransform); boneName, dstNodes[s.dst].name});
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) {
remapped.positions.push_back(
{k.time, dstRest.pos +
(k.value - srcRest.pos) * ratio});
}
remapped.rotations.reserve(ch.rotations.size());
for (const auto& k : ch.rotations) {
const glm::quat delta =
k.value * glm::inverse(srcRest.rot);
remapped.rotations.push_back(
{k.time, delta * dstRest.rot});
}
remapped.scales = std::move(ch.scales);
out.channels.push_back(std::move(remapped));
++res.channelsRetargeted;
// 明细行(同骨骼多 clip 去重)
const bool newRow = std::none_of(
res.retargetRows.begin(), res.retargetRows.end(),
[&](const RetargetRow& r)
{ return r.srcName == boneName; });
if (newRow) {
res.retargetRows.push_back(
{HumanoidSlotName(retargetSlot[ch.nodeIdx]),
boneName, dstNodes[mapIt->second].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 (!outExtra.empty()) {
ENGINE_INFO("AttachClipsToModel: '{}' -> {} clips "
"({} named, {} retargeted, {} skipped)",
sourcePath, res.clipsAdded, res.channelsMatched,
res.channelsRetargeted, res.channelsSkipped);
}
return res;
}
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
const std::string& sourcePath)
{
std::vector<AnimationClip> extra;
AttachResult res = AttachClipsToNodes(
target.GetNodes(), target.GetClips(), std::move(src),
sourcePath, extra);
if (!extra.empty()) { if (!extra.empty()) {
target.AppendClips(std::move(extra)); target.AppendClips(std::move(extra));
} }
ENGINE_INFO("AttachClipsToModel: '{}' -> {} clips "
"({} named, {} retargeted, {} skipped)",
sourcePath, res.clipsAdded, res.channelsMatched,
res.channelsRetargeted, res.channelsSkipped);
return res; return res;
} }
+46 -9
View File
@@ -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;
+23 -3
View File
@@ -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);
} }
+4 -2
View File
@@ -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);
}); });
+619 -117
View File
@@ -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)
// {
// 规则: const u32 s = static_cast<u32>(slot);
// 1) 去 "mixamorig:" 等前缀 if (s < 6) return Side::None; // 主干
// 2) 小写;分隔符(空格/_/.-:)一律删除 return s < 29 ? Side::Left : Side::Right;
// 3) left/right(Mixamo "LeftArm")与 左/右(MMD "左腕")统一 }
// 抽成 side,body 里剔除;.L/.R、_L/_R 后缀同上
//
// body 用**精确相等**匹配关键词(非子串)——"lowerarm"
// 不会误命中 "arm"
Side ExtractSide(std::string& body) 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",
};
const u32 s = static_cast<u32>(slot);
return s < kNames.size() ? kNames[s] : "?";
}
// ==================================================================
// 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()
{ {
Side s = Side::None; static const std::vector<BodySlot> kTable = [] {
auto cut = [&](std::string_view token, Side which) std::vector<BodySlot> t;
{ t.reserve(80);
size_t pos = 0;
while ((pos = body.find(token, pos)) != std::string::npos) { // Mixamo:handthumb1 ... handpinky3
body.erase(pos, token.size()); constexpr std::array<std::string_view, 5> kMixamo{
if (s == Side::None) s = which; "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)});
}
} }
};
cut("left", Side::Left); // Rigify:defthumb01 ... deffpinky03(+ 无前缀 master 变体)
cut("right", Side::Right); constexpr std::array<std::string_view, 5> kRigify{
cut("左", Side::Left); // MMD 前缀 "thumb", "findex", "fmiddle", "fring", "fpinky"};
cut("右", Side::Right); for (const auto kindU : {0u, 1u, 2u, 3u, 4u}) {
cut(".l", Side::Left); // blender 惯例后缀 const auto slot = static_cast<u32>(
cut(".r", Side::Right); FingerSlot(Side::Left,
cut("_l", Side::Left); static_cast<FingerKind>(kindU),
cut("_r", Side::Right); 1));
return s; 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;
} }
// ① 前缀剥离 + 小写(分隔符**保留**——后缀式 side 标记 // 查表:主干优先,指骨兜底(两表键空间无交集)
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,兜底命名习惯
// 不在首尾的资产。窄规则先行,本函数只在查表失败后启用
Side ExtractSideWide(std::string& body)
{
Side s = Side::None;
auto cut = [&](std::string_view token, Side which)
{
size_t pos = 0;
while ((pos = body.find(token, pos)) != std::string::npos) {
body.erase(pos, token.size());
if (s == Side::None) s = which;
}
};
cut("left", Side::Left);
cut("right", Side::Right);
cut("左", Side::Left);
cut("右", Side::Right);
cut(".l", Side::Left);
cut(".r", Side::Right);
cut("_l", Side::Left);
cut("_r", Side::Right);
return s;
}
const char* HumanoidSlotName(HumanoidSlot slot) } // namespace mapper
{
static constexpr std::array<const char*, 20> kNames{
"Hips", "Spine", "Chest", "UpperChest", "Neck", "Head",
"LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand",
"RightShoulder", "RightUpperArm", "RightLowerArm", "RightHand",
"LeftUpperLeg", "LeftLowerLeg", "LeftFoot",
"RightUpperLeg", "RightLowerLeg", "RightFoot",
};
const u32 s = static_cast<u32>(slot);
return s < kNames.size() ? kNames[s] : "?";
}
// 名字归一化 + 方言查表识别。两轮:窄 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
+10
View File
@@ -202,6 +202,16 @@ namespace Engine::Assets {
}); });
} }
ModelRef AssetManager::CreateCube(f32 halfSize, u8 r, u8 g, u8 b)
{
// 程序化资产复用同一缓存:键 = 伪路径 + 参数(与 plane 同约定)
std::string key = "procedural://cube/" + std::to_string(halfSize) +
"/" + std::to_string(r) + "," + std::to_string(g) +
"," + std::to_string(b);
return m_Models.Load(key,
[&](const std::string&) { return m_Uploader.CreateCube(halfSize, r, g, b); });
}
EnvRef AssetManager::LoadEnvironment(const std::string& path) EnvRef AssetManager::LoadEnvironment(const std::string& path)
{ {
// GPU 生成(compute 预积分)经接口发生在渲染层实现里 // GPU 生成(compute 预积分)经接口发生在渲染层实现里
+10
View File
@@ -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();
+8 -1
View File
@@ -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
View File
@@ -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
+17
View File
@@ -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});
} }
// ---- 运行期 ---- // ---- 运行期 ----
+32 -2
View File
@@ -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()),
+71 -6
View File
@@ -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);
} }
} }
@@ -767,8 +812,14 @@ namespace Engine {
// 动态/运动体 → ConvexHull(MeshShape 三角网格太贵,Jolt 不允许动) // 动态/运动体 → ConvexHull(MeshShape 三角网格太贵,Jolt 不允许动)
if (rb.type != ECS::RigidBodyComponent::BodyType::Static) { if (rb.type != ECS::RigidBodyComponent::BodyType::Static) {
JPH::ConvexHullShapeSettings cs; JPH::ConvexHullShapeSettings cs;
for (const auto &mesh: model->GetMeshes()) { const auto &meshes = model->GetMeshes();
for (const auto &p: mesh->GetCpuPositions()) { for (size_t mi = 0; mi < meshes.size(); ++mi) {
// 单 mesh 子引用:碰撞只取渲染可见的同一 mesh(语义跟随)
if (mc->meshIndex >= 0 &&
static_cast<int>(mi) != mc->meshIndex) {
continue;
}
for (const auto &p: meshes[mi]->GetCpuPositions()) {
cs.mPoints.emplace_back(p.x * s.x, p.y * s.y, p.z * s.z); cs.mPoints.emplace_back(p.x * s.x, p.y * s.y, p.z * s.z);
} }
} }
@@ -788,9 +839,15 @@ namespace Engine {
JPH::VertexList verts; JPH::VertexList verts;
JPH::IndexedTriangleList tris; JPH::IndexedTriangleList tris;
u32 base = 0; u32 base = 0;
for (const auto &mesh: model->GetMeshes()) { const auto &meshes = model->GetMeshes();
const auto &pos = mesh->GetCpuPositions(); for (size_t mi = 0; mi < meshes.size(); ++mi) {
const auto &idx = mesh->GetCpuIndices(); // 单 mesh 子引用:碰撞只取渲染可见的同一 mesh(语义跟随)
if (mc->meshIndex >= 0 &&
static_cast<int>(mi) != mc->meshIndex) {
continue;
}
const auto &pos = meshes[mi]->GetCpuPositions();
const auto &idx = meshes[mi]->GetCpuIndices();
verts.reserve(verts.size() + pos.size()); verts.reserve(verts.size() + pos.size());
for (const auto &p: pos) { for (const auto &p: pos) {
verts.push_back(JPH::Float3(p.x * s.x, p.y * s.y, p.z * s.z)); verts.push_back(JPH::Float3(p.x * s.x, p.y * s.y, p.z * s.z));
@@ -1112,6 +1169,14 @@ namespace Engine {
: glm::vec3{0.0f}; : glm::vec3{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() {
+3 -1
View File
@@ -32,7 +32,9 @@ namespace Engine
{ {
constexpr u32 kFaceCount = 6; constexpr u32 kFaceCount = 6;
constexpr u32 kIrradianceFaceSize = 32; // 漫反射辐照度面边长(低频) constexpr u32 kIrradianceFaceSize = 32; // 漫反射辐照度面边长(低频)
constexpr u32 kPrefilterFaceSize = 128; // pre-filter 面边长(mip0) // pre-filter 面边长(mip0):roughness→0 的镜面反射采 mip0,
// 128²/面放大到全屏 = 块状马赛克;256 = 无 SSR 时的清晰度底线
constexpr u32 kPrefilterFaceSize = 256;
constexpr u32 kPrefilterMips = 5; // mip 分级 roughness 数 constexpr u32 kPrefilterMips = 5; // mip 分级 roughness 数
constexpr u32 kBrdfLutSize = 128; // BRDF LUT 边长 constexpr u32 kBrdfLutSize = 128; // BRDF LUT 边长
constexpr u32 kGroupSize = 8; // compute local_size(shader 同步) constexpr u32 kGroupSize = 8; // compute local_size(shader 同步)
+15 -5
View File
@@ -84,11 +84,21 @@ namespace Engine
",s:" + std::to_string(params.surface.x) + ",s:" + std::to_string(params.surface.x) +
"," + std::to_string(params.surface.y) + "," + std::to_string(params.surface.y) +
"," + std::to_string(params.surface.z) + "," + std::to_string(params.surface.z) +
"," + std::to_string(params.surface.w); "," + std::to_string(params.surface.w) +
// per-slot UV 变换 + unlit 入键:同一张贴图配不同变换是不同材质 ",e:" + std::to_string(params.emissiveFactor.x) +
//(KHR_texture_transform 测试模型:一张棋盘格被 82 个材质以不同 "," + std::to_string(params.emissiveFactor.y) +
// 变换共享,键漏了会串成同一份) "," + std::to_string(params.emissiveFactor.z);
key += ",m:" + std::to_string(params.misc.x); // per-slot UV 变换 + unlit + UV 镜像入键:同一张贴图配不同变换是
// 不同材质(KHR_texture_transform 测试模型:一张棋盘格被 82 个材质
// 以不同变换共享,键漏了会串成同一份)
key += ",m:" + std::to_string(params.misc.x) +
"," + std::to_string(params.misc.y) +
"," + std::to_string(params.misc.z);
// alpha 叠加遮罩入键:同贴图不同叠加色是不同材质
key += ",o:" + std::to_string(params.alphaOverlay.x) +
"," + std::to_string(params.alphaOverlay.y) +
"," + std::to_string(params.alphaOverlay.z) +
"," + std::to_string(params.alphaOverlay.w);
for (u32 slot = 0; slot < kSlotCount; ++slot) { 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) + "," +
+14 -1
View File
@@ -29,7 +29,12 @@ namespace Engine
return Texture::CreateSolidColor(device, allocator, return Texture::CreateSolidColor(device, allocator,
0, 128, 0, false); 0, 128, 0, false);
case Material::Emissive: case Material::Emissive:
return Texture::CreateSolidColor(device, allocator, 0, 0, 0); // 兜底白(非黑):emissive = 贴图 × factor × strength——黑
// 兜底会把无贴图材质的 factor/strength 一起乘没(调了恒黑
// 的困惑源);白兜底 = 无贴图时 factor 即发光色,有贴图时
// glTF 语义不变(factor 黑的普通模型依旧不发光)
return Texture::CreateSolidColor(device, allocator,
255, 255, 255);
case Material::Ao: case Material::Ao:
return Texture::CreateSolidColor(device, allocator, return Texture::CreateSolidColor(device, allocator,
255, 0, 0, false); 255, 0, 0, false);
@@ -84,8 +89,12 @@ namespace Engine
EmitVec4(out, data.params.baseColor); EmitVec4(out, data.params.baseColor);
out << YAML::Key << "Surface" << YAML::Value; out << YAML::Key << "Surface" << YAML::Value;
EmitVec4(out, data.params.surface); EmitVec4(out, data.params.surface);
out << YAML::Key << "EmissiveFactor" << YAML::Value;
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;
@@ -145,8 +154,12 @@ namespace Engine
m["BaseColor"], MaterialParams{}.baseColor); m["BaseColor"], MaterialParams{}.baseColor);
out.params.surface = ReadVec4( out.params.surface = ReadVec4(
m["Surface"], MaterialParams{}.surface); m["Surface"], MaterialParams{}.surface);
out.params.emissiveFactor = ReadVec4(
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)],
File diff suppressed because it is too large Load Diff
@@ -279,6 +279,11 @@ namespace Engine::Passes
// frag 仍写 gl_FragDepth 的时期做的(禁 early-Z),归因存疑; // frag 仍写 gl_FragDepth 的时期做的(禁 early-Z),归因存疑;
// gl_FragDepth 已删除,FALSE 若复现病态再另行隔离。 // gl_FragDepth 已删除,FALSE 若复现病态再另行隔离。
cmd.setDepthWriteEnable(VK_FALSE); cmd.setDepthWriteEnable(VK_FALSE);
// 单面玻璃:透明段剔除背面。不透明段 cullNone + depthWrite
// 无所谓(深度裁决),透明段 cullNone 会让闭合体(cube)的
// 正反面片按 mesh 顺序乱序 over 混合 = "深度错乱";cull back
// 每像素至多一面。双面透明(树叶卡片)将来加材质开关
cmd.setCullMode(vk::CullModeFlagBits::eBack);
vk::ColorBlendEquationEXT premul; vk::ColorBlendEquationEXT premul;
premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
+6
View File
@@ -757,6 +757,12 @@ namespace Engine
const auto& meshes = model->GetMeshes(); const auto& meshes = model->GetMeshes();
for (size_t meshIdx = 0; meshIdx < meshes.size(); ++meshIdx) { for (size_t meshIdx = 0; meshIdx < meshes.size(); ++meshIdx) {
// 单 mesh 子引用:只收集指派的那一个(覆盖表/物理
// 均按原模型下标,语义不变)
if (mr.meshIndex >= 0 &&
static_cast<int>(meshIdx) != mr.meshIndex) {
continue;
}
auto& mesh = meshes[meshIdx]; auto& mesh = meshes[meshIdx];
const auto& bounds = mesh->GetBounds(); const auto& bounds = mesh->GetBounds();
const glm::vec3 worldCenter = const glm::vec3 worldCenter =
@@ -95,6 +95,12 @@ namespace Engine {
halfSize, r, g, b, a); halfSize, r, g, b, a);
} }
Scope<Model> VulkanAssetUploader::CreateCube(f32 halfSize, u8 r, u8 g, u8 b)
{
return Model::CreateCube(m_Device, m_Allocator, m_Descriptors,
m_TextureLayout, halfSize, r, g, b);
}
Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path) Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path)
{ {
// compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT // compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT
+63
View File
@@ -7,6 +7,10 @@
#include <Engine/Core/Application.h> #include <Engine/Core/Application.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/ECS/Components.h> #include <Engine/ECS/Components.h>
#include <Engine/ECS/EntityHandle.h>
#include <Engine/Physics/PhysicsEvents.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <algorithm> #include <algorithm>
@@ -19,6 +23,25 @@ namespace Engine {
return *m_App; return *m_App;
} }
// ---- 场景实体 spawn/destroy 底座(声明在 Script.h,实现下沉
// 此处——Application 完整类型只在 cpp 可见)----
ECS::EntityHandle ScriptBehavior::Spawn(const std::string& name) {
return App().GetScene().CreateEntity(name);
}
void ScriptBehavior::Destroy(ECS::Entity e) {
App().GetScene().DestroyEntity(e);
}
// Prefab 实例化(声明在 Script.h,实现下沉此处——同 Spawn/
// Destroy 模式;序列化器与 Application 门面都在引擎内可达)
ECS::Entity ScriptBehavior::Instantiate(const std::string& prefabPath,
const glm::vec3& pos,
const glm::quat& rot) {
return Serialization::SceneSerializer::InstantiatePrefab(
App().GetScene(), App().GetAssets(), prefabPath, &pos, &rot);
}
// ---- 参数按名读写(ScriptBehavior 声明,经绑定表;写穿 // ---- 参数按名读写(ScriptBehavior 声明,经绑定表;写穿
// Item.params 由运行时 SetParam 完成)---- // Item.params 由运行时 SetParam 完成)----
bool ScriptBehavior::WriteParam(const std::string& name, bool ScriptBehavior::WriteParam(const std::string& name,
@@ -41,7 +64,47 @@ namespace Engine {
return false; return false;
} }
// ---- 碰撞事件派发(Update 开头、Sync 之前):PhysicsWorld 最近
// 一次步进收集的事件转发到涉事实例。时序保证:Step 先于
// Update(帧循环序)→ 此处读到的是本帧物理结果;回调里
// spawn/destroy 安全——destroy 入延迟队列(帧尾 flush),
// spawn 不改 m_Instances(下一帧 Sync 对账建实例)。禁用
// 实例不收(Unity enabled 语义对齐);Persisted 每步都来,
// 直接跳过(Enter/Exit 已够 gameplay 用)
void ScriptRuntime::DispatchCollisions() {
if (!m_App) {
return;
}
const std::vector<CollisionEvent>& events =
m_App->GetPhysics().GetEvents();
for (const auto& ev : events) {
if (ev.kind == CollisionEvent::Kind::Persisted) {
continue;
}
const bool enter = ev.kind == CollisionEvent::Kind::Added;
for (const u32 self : {ev.a, ev.b}) {
const u32 other = (self == ev.a) ? ev.b : ev.a;
const auto it = m_Instances.find(self);
if (it == m_Instances.end()) {
continue;
}
for (auto& inst : it->second) {
if (!inst.behavior || !inst.enabled) {
continue;
}
if (enter) {
inst.behavior->OnCollisionEnter(
other, ev.point, ev.normal);
} else {
inst.behavior->OnCollisionExit(other);
}
}
}
}
}
void ScriptRuntime::Update(f32 dt) { void ScriptRuntime::Update(f32 dt) {
DispatchCollisions();
Sync(); Sync();
auto& reg = m_Scene.GetRegistry(); auto& reg = m_Scene.GetRegistry();
File diff suppressed because it is too large Load Diff
+51 -1
View File
@@ -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 = {};
} }
+22 -2
View File
@@ -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);
} }
+45
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+6062
View File
File diff suppressed because it is too large Load Diff
+24 -9
View File
@@ -45,18 +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 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)
@@ -153,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;
} }
@@ -166,8 +177,10 @@ void main() {
matParams.factors.z) * matParams.factors.y, matParams.factors.z) * matParams.factors.y,
0.045, 1.0); 0.045, 1.0);
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0); float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
// 自发光强度(KHR_materials_emissive_strength,缺省 1) // 自发光强度(KHR_materials_emissive_strength,缺省 1);
vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb * matParams.surface.z; // emissiveFactor = 颜色因子(无贴图时发光的入口——兜底贴图恒黑)
vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb
* matParams.emissiveFactor.rgb * matParams.surface.z;
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, ApplyUv(1, fragUV)); vec3 N = MappedNormal(normalize(fragNormal), fragTangent, ApplyUv(1, fragUV));
vec3 V = normalize(ubo.cameraPos.xyz - fragWorldPos); vec3 V = normalize(ubo.cameraPos.xyz - fragWorldPos);
@@ -256,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;
+15 -5
View File
@@ -45,17 +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 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22) vec4 emissiveFactor; // 自发光颜色(rgb × emissiveMap × surface.z;黑 = 不发光)
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22),槽序 = binding
vec4 uvT[6]; // 每槽 UV 平移 (tx, ty, 0, 0) vec4 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)
@@ -151,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 通道
@@ -159,7 +168,8 @@ void main() {
0.045, 1.0); 0.045, 1.0);
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0); float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
// 自发光强度(KHR_materials_emissive_strength,缺省 1) // 自发光强度(KHR_materials_emissive_strength,缺省 1)
vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb * matParams.surface.z; vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb
* matParams.emissiveFactor.rgb * matParams.surface.z;
// glTF 表面扩展:透射率(KHR_materials_transmission)/ // glTF 表面扩展:透射率(KHR_materials_transmission)/
// 折射率(ior,缺省 1.5)/ 体积厚度(KHR_materials_volume) // 折射率(ior,缺省 1.5)/ 体积厚度(KHR_materials_volume)