阶段性合并:动画重定向、碎裂共享库、第三人称玩法、编辑器面板与 vendored ufbx

- 动画:HumanoidRig 三层架构(standard 标准槽 / dialects 方言映射 / mapper 配对),
  指/趾统一走世界增量两遍式烘焙;AnimationAttach 拆为纯算法 + facade;修正 HumanoidBasis
  行走基朝向与跨骨架去重、根运动通道
- 物理:CharacterController 角色互碰撞 / shapeOffset / 接触事件;PhysicsWorld 接触合成与
  字符冲量判定
- 渲染与材质:Model 多材质按材质拆子 mesh 与贴图路径解析、DDS 直传、GLB 内嵌纹理落盘、
  不透明通道 alpha 固定 1.0、glTF FlipUVs 对齐;SceneSerializer 支持 meshIndex 与材质覆盖
- 脚本:FractureLib 抽为共享库(HostView 桥接)、WallFracture 参数直绑、新增
  ThirdPersonCamera / Follower / WASDMove 玩法;移除 GameDirector / PlayerController / Target
- 编辑器:新增 GamepadPanel、LogPanel(LogRing),改进 Inspector / 材质 / 统计 / 视口面板
- 依赖:vendored ufbx(FBX 5.x/6.x/7.x 全版本解析,替代 assimp 对 FBX 6.1 的拒读)
This commit is contained in:
2026-10-11 22:22:19 +08:00
parent 334819bf97
commit beba491bcb
57 changed files with 43200 additions and 1378 deletions
+165 -23
View File
@@ -17,8 +17,10 @@
// 墙脚"的观感。
//
// 用法(宿主 = 挂在可碎实体上的脚本):
// Fracture::Params p_; // DeclareParams 直绑成员(pb.Field)
// 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); },
@@ -27,8 +29,9 @@
// OnCollisionEnter → burst_.TryFire(HV(), other, point, normal)
// OnUpdate → burst_.Tick(HV(), dt)
// OnDestroy → burst_.Shutdown(HV())
// 外观:Fire 前改 burst_.look(默认砖色不透明;玻璃靶 = 半透明
// + 自发光)
// 外观:默认继承宿主(墙)的生效材质(底色/贴图/透明),碎块与墙面
// 一致;要独立外观(玻璃靶 = 半透明 + 自发光)Fire 前置
// burst_.look.inheritFromHost = false 再填 baseColor/emissive
// HostView 每次调用现填(不存指针:组件存储重排/实例重建均安全)
//
// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按宿主世界
@@ -148,23 +151,32 @@ namespace Engine::Fracture {
std::vector<glm::vec3> cpuPositions;
};
inline PrismMesh CellToPrism(const std::vector<glm::vec2>& cell, f32 hz) {
// hw/hh = 宿主半尺寸:顶/底面 UV 由 cell 的墙局部坐标归一化到 [0,1]
//(墙面矩形 UV 约定,与立方体面 UV 对齐)——继承墙材质贴图时,碎片
// 贴图与墙面连续,不会各碎片自成一格
inline PrismMesh CellToPrism(const std::vector<glm::vec2>& cell, f32 hz,
f32 hw, f32 hh) {
PrismMesh m;
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},
{p.x, p.y}, {1.0f, 0.0f, 0.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},
{p.x, p.y}, {1.0f, 0.0f, 0.0f}};
uvOf(p), {1.0f, 0.0f, 0.0f}};
m.vertices.push_back(v);
}
@@ -216,6 +228,47 @@ namespace Engine::Fracture {
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 种子数 = 碎片数
@@ -230,9 +283,15 @@ namespace Engine::Fracture {
f32 minImpactSpeed = 2.0f; // 触发阈值(m/s)
};
// ---- 碎片外观(Fire 时套用到全部碎片网格;默认砖色不透明 =
// 原 WallFracture 观感)----
// ---- 碎片外观 ----
// 默认 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; // 透明绘制队列(玻璃碎片)
@@ -254,20 +313,36 @@ namespace Engine::Fracture {
const glm::vec3& normal) {
if (m_Fired) return false;
// 对方须是活的 Dynamic 刚体且速度够快(过滤静止贴靠/
// 角色轻碰——子弹/抛体必中)
// 对方须"够快"且是可动的撞击来源,二选一:
// - 动态刚体(子弹/抛体):用实时线速度
// - 角色(CharacterVirtual,非刚体、无 RigidBodyComponent):
// 用"扫掠前"速度——扫掠中朝墙面的分量会被 Jolt 抵消,
// 实时速度往往已归零,会漏判(见
// PhysicsWorld::GetCharacterImpactVelocity)
// 速度低于阈值(静止贴靠/轻碰)则忽略
auto& registry = host.app.GetScene().GetRegistry();
const ECS::Entity otherE = EntityFromBits(other);
if (!registry.valid(otherE)) return false;
const auto* otherRb =
registry.try_get<Engine::ECS::RigidBodyComponent>(otherE);
if (!otherRb ||
otherRb->type !=
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;
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 实体)
}
const f32 speed = glm::length(
host.app.GetPhysics().GetLinearVelocity(other));
if (speed < p.minImpactSpeed) return false;
m_Fired = true;
@@ -335,6 +410,52 @@ namespace Engine::Fracture {
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());
@@ -391,7 +512,7 @@ namespace Engine::Fracture {
paths.emplace_back();
continue;
}
PrismMesh pm = CellToPrism(cell, hz);
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());
@@ -407,11 +528,27 @@ namespace Engine::Fracture {
mesh.lods.push_back(std::move(pm.indices));
mesh.cpuPositions = std::move(pm.cpuPositions);
mesh.name = "frag" + std::to_string(s);
mesh.params.baseColor = look.baseColor;
mesh.transparent = look.transparent;
mesh.params.emissiveFactor =
glm::vec4{look.emissive, 1.0f};
mesh.params.surface.z = look.emissiveStrength;
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));
@@ -436,6 +573,11 @@ namespace Engine::Fracture {
frag.AddComponent<
Engine::ECS::MeshRendererComponent>();
fmr.model = refs[s];
// 宿主墙用 .material 覆盖时,碎块直接复用同一份资产:外观
//(含透明通道)与墙完全一致,且不需要引擎改判定
if (!hostOverridePath.empty()) {
fmr.materialOverrides[0] = hostOverridePath;
}
// Dynamic MeshCollider → ConvexHull(数据源 = 刚上传
// 的 cpuPositions;Box 字段不参与 Mesh 形状)