实机故障:进入游戏后整屏灰紫,主角还是蓝色胶囊占位体,2D UI 与小地图正常。 根因(已实证,非推测)。`EntityStore::mut_despawn()` 在 despawn 的 vid 等于 主角 vid 时把 `m_main_vid` 清零。40250 服务器在 spawn-in 头几秒会对玩家自己的 vid 反复下发 GC_CHARACTER_DEL + GC_CHARACTER_ADD(视野重算,实测约 10 次)。 第一次 DEL 就把玩家永久去主角化:`get_main_vid()` 返回 0 → `_sync_main_character()` 直接 return → 建模代码从未跑过(main_attempts=0)→ 占位胶囊留在 y=0 → 相机跟着它钻到 C1 地形底下 → 满屏灰紫。地图其实一直是好的 (chunks_built 20/20、objects_placed 565)。 参考端怎么做(40250 ClientVS22)。主角身份属于 `CNetworkActorManager::m_dwMainVID`, 只由 `SetMainActorVID()`(GC_MAIN_CHARACTER)写,只在管理器析构时清; `RemoveActor()` 删 actor 行和角色实例,但绝不碰它;重新加入时 `__AppendCharacterManagerActor()` 用 `kCreateData.m_isMain = __IsMainActorVID(dwVID)` 重新推导,并再跑一次 `SetMainCharacterIndex(dwVID)`。 修法(entity_store.cpp): - `mut_despawn()` 只删行、只发 Despawn,不动 m_main_vid;m2dev 分支的 GC_CHARACTER_DEL 同规则。 - `mut_spawn_full()` 在 `e.vid == m_main_vid` 时把 `is_main` 置回 true, 并重新广播 MainSet,对齐参考端的重新推导。 顺带落地的渲染修复(同一次实机排查中定位): - libgr2 `sample_pose()` 增加 `root_offset` 形参;`Metin2AnimPlayer::apply_pose()` 显式传单位阵而不是模型的 InitialPlacement。Metin2 动画 gr2 的根骨轨道自带骨盆 高度,再乘一次 IP 会把整个人抬高一个骨盆高(武 99.4cm / 刺 92.6 / 巫 103.7 / 萨 95.5,正好各自的 IP.z)——即此前的「人物悬空」。`bind_pose()` 语义不变。 - `Metin2Model::_load_hair()` 改用与身体/武器同一条 `make_material()` (specular_disabled)而不是 roughness=1.0 的 StandardMaterial3D,头发不再被 PBR 高光洗成奶白色块。 取证与回归工具: - `project/net_trace.gd`(新):主角 vid 生命周期取证。挂在 AppFlow 之后逐帧读 `get_main_vid()`,归零时打印 `NETTRACE: MAIN_LOST was=.. now=0 reason=..` 加最近 24 条环形事件;`reason` 区分 despawn 与 silent(reset_for_map_change 清空 m_changes,是静默清零)。`MT_NETTRACE=1` 打开逐事件日志。 - `game_scene.gd`:诊断 tick 支持重复采样(分辨主角装配是「没跑」还是「没跑完」), 新增 main_sync_ready / main_loading / main_view_key / main_map_vid / main_retry_in_ms 字段;`MT_DIAG_SHOT=<png>` 让游戏自己存一帧实机画面 (screencapture 拍到的是终端窗口,没用)。 - `project/model_render_test.gd` + `test/rendering/model_baseline.json`(新): 八个种族的模型渲染基线(--bless 重新落盘)。 - `project/char_bench_test.gd`(新):无网络无地图的单角色实验台,五变体 × 六机位出证据图,只产图不做阈值判定。 - `project/package_render_test.gd`(新):导出包内的渲染自检入口 (导出模板不接受 --script,只能走 MT_TEST_MODE)。 - `/project/build/` 加入 .gitignore(渲染用例的落图输出)。 实机复验(真实服务器,非模拟): - 修前 `LIVE_SMOKE FAIL: 主角色 VID 有效`; `NETTRACE: MAIN_LOST was=25910 now=0 reason=despawn t=+7737ms`, 环里紧跟 `spawn vid=25910 is_main=false`。 - 修后 `LIVE_SMOKE RESULT: PASS`,main_vid=25933、entities=19; 40s 实机跑 16 次诊断采样全部 model_built=True / main_attempts=1 / main_sync_ready=True,player_pos 落在 C1 地形上而非 y=0; MT_DIAG_SHOT 存下的帧里地形、建筑、NPC、树木、真实角色模型、HUD、小地图俱全。 回归:gamescene_test / equip_model_test / gamescene_visual_test(feet_gap 0.001373)/ model_render_test(8 races)/ char_select_visual_test / 包内 MT_TEST_MODE=render 全绿;ctest 22/23,唯一失败 net.classic_session 已用 「还原本次改动 → 重跑 → diff 失败集合」确认与本次修改无关(失败集合完全相同)。 注:画面整体偏暗属于光照/色调差距,受 PARITY-GAP.md §0 门禁约束(对照帧未落盘前 不做相机/光照/色调/材质调参),本次不碰。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJugvEJwz3FK4hw9ti3SRb
228 lines
9.4 KiB
C++
228 lines
9.4 KiB
C++
// M0 T5 —— 骨架提取 + bind pose 自洽检查。见 docs/reference/steps/M0-gr2-reader.md
|
||
// 规格:granny_transform.cpp BuildCompositeTransform4x4 / granny_math.cpp
|
||
// MatrixEqualsQuaternion3x3 + MatrixMultiply3x3 /
|
||
// granny_matrix_operations.cpp ColumnMatrixMultiply4x3Impl /
|
||
// granny_world_pose.cpp BuildWorldPoseNoCompositeLOD + granny_bone_operations.cpp
|
||
// BuildFullWorldPoseOnly_Generic
|
||
#include "internal.h"
|
||
#include <cmath>
|
||
#include <cstdio>
|
||
#include <cstdlib>
|
||
#include <cstring>
|
||
|
||
namespace gr2 {
|
||
|
||
using detail::Ref;
|
||
using detail::TypeMember;
|
||
|
||
namespace {
|
||
|
||
// 行主序 4x4:R = A * B(ColumnMatrixMultiply4x3Impl 语义,平移在第 3 行 elem 12..14)
|
||
Mat4 mul4x3(const Mat4& A, const Mat4& B) {
|
||
Mat4 R{};
|
||
for (int i = 0; i < 3; ++i) {
|
||
for (int k = 0; k < 3; ++k)
|
||
R[i * 4 + k] = A[i * 4 + 0] * B[0 * 4 + k]
|
||
+ A[i * 4 + 1] * B[1 * 4 + k]
|
||
+ A[i * 4 + 2] * B[2 * 4 + k];
|
||
R[i * 4 + 3] = 0.0f;
|
||
}
|
||
for (int k = 0; k < 3; ++k)
|
||
R[12 + k] = A[12] * B[0 * 4 + k]
|
||
+ A[13] * B[1 * 4 + k]
|
||
+ A[14] * B[2 * 4 + k]
|
||
+ B[12 + k];
|
||
R[15] = 1.0f;
|
||
return R;
|
||
}
|
||
|
||
// granny_math.cpp MatrixEqualsQuaternion3x3(行主序 3x3)
|
||
void quat_to_m3(const float q[4], float d[9]) {
|
||
const float xx = q[0] * q[0], yy = q[1] * q[1], zz = q[2] * q[2];
|
||
const float xy = q[0] * q[1], xz = q[0] * q[2], yz = q[1] * q[2];
|
||
const float wx = q[3] * q[0], wy = q[3] * q[1], wz = q[3] * q[2];
|
||
d[0] = 1 - 2 * (yy + zz); d[1] = 2 * (xy - wz); d[2] = 2 * (xz + wy);
|
||
d[3] = 2 * (xy + wz); d[4] = 1 - 2 * (xx + zz); d[5] = 2 * (yz - wx);
|
||
d[6] = 2 * (xz - wy); d[7] = 2 * (yz + wx); d[8] = 1 - 2 * (xx + yy);
|
||
}
|
||
|
||
void m3_mul(const float a[9], const float b[9], float r[9]) {
|
||
for (int i = 0; i < 3; ++i)
|
||
for (int j = 0; j < 3; ++j)
|
||
r[i * 3 + j] = a[i * 3 + 0] * b[0 * 3 + j]
|
||
+ a[i * 3 + 1] * b[1 * 3 + j]
|
||
+ a[i * 3 + 2] * b[2 * 3 + j];
|
||
}
|
||
|
||
} // namespace
|
||
|
||
// granny_transform (68B: u32 Flags; f32 Position[3]; f32 Orientation[4]; f32 ScaleShear[3][3])
|
||
// → 行主序 4x4,上 3x3 = transpose(U3),平移在 elem 12..14。BuildCompositeTransform4x4 语义。
|
||
void File::Impl::transform_to_composite(Ref tf, Mat4& out, uint32_t& flags) const {
|
||
const uint8_t* p = at(tf, 68);
|
||
out = Mat4{};
|
||
out[0] = out[5] = out[10] = out[15] = 1.0f;
|
||
flags = 0;
|
||
if (!p) return;
|
||
|
||
auto f32 = [&](size_t off) { float v; std::memcpy(&v, p + off, 4); return v; };
|
||
flags = rd_u32(tf);
|
||
float pos[3] = { f32(4), f32(8), f32(12) };
|
||
float ori[4] = { f32(16), f32(20), f32(24), f32(28) };
|
||
float ss[9];
|
||
for (int i = 0; i < 9; ++i) ss[i] = f32(32 + size_t(i) * 4);
|
||
|
||
float q3[9];
|
||
quat_to_m3(ori, q3);
|
||
float u3[9];
|
||
if (flags & 0x4 /* HasScaleShear */) m3_mul(q3, ss, u3);
|
||
else std::memcpy(u3, q3, sizeof u3);
|
||
|
||
// C 的上 3x3 = transpose(U3)
|
||
out[0] = u3[0]; out[1] = u3[3]; out[2] = u3[6]; out[3] = 0;
|
||
out[4] = u3[1]; out[5] = u3[4]; out[6] = u3[7]; out[7] = 0;
|
||
out[8] = u3[2]; out[9] = u3[5]; out[10] = u3[8]; out[11] = 0;
|
||
out[12] = pos[0]; out[13] = pos[1]; out[14] = pos[2]; out[15] = 1;
|
||
}
|
||
|
||
// skeleton: {String Name; RefToArray Bones(bone[]); int32 LODType; VariantRef ExtendedData}
|
||
// bone: {String Name; int32 ParentIndex; Transform LocalTransform;
|
||
// Real32[16] InverseWorldTransform; Real32 LODError; VariantRef ExtendedData}
|
||
// 全程按类型树取偏移,不硬编码。
|
||
bool File::Impl::extract_skeleton(Ref skel_obj, Ref skel_type, Skeleton& out) {
|
||
out.bones.clear();
|
||
if (!skel_obj.valid() || !skel_type.valid()) return false;
|
||
|
||
const auto& sms = parse_type(skel_type);
|
||
const TypeMember* mBones = find_member(sms, "Bones");
|
||
if (!mBones) return false;
|
||
|
||
Ref bones_base;
|
||
uint32_t bone_count = 0, stride = 0;
|
||
if (!read_ref_to_array(skel_obj, *mBones, bones_base, bone_count, stride)) return false;
|
||
if (bone_count == 0) return false;
|
||
if (bone_count > 8192 || stride == 0) return false;
|
||
|
||
const auto& bms = parse_type(mBones->ref_type);
|
||
const TypeMember* mName = find_member(bms, "Name");
|
||
const TypeMember* mParent = find_member(bms, "ParentIndex");
|
||
const TypeMember* mLocal = find_member(bms, "LocalTransform");
|
||
if (!mLocal) mLocal = find_member(bms, "Transform");
|
||
const TypeMember* mInvW = find_member(bms, "InverseWorldTransform");
|
||
if (!mInvW) mInvW = find_member(bms, "InverseWorld4x4");
|
||
if (!mLocal || !mInvW) return false;
|
||
|
||
out.bones.resize(bone_count);
|
||
for (uint32_t i = 0; i < bone_count; ++i) {
|
||
Ref b = { bones_base.section, bones_base.offset + i * stride };
|
||
Bone& bo = out.bones[i];
|
||
if (mName) bo.name = rd_str(member_slot(b, *mName));
|
||
if (mParent) bo.parent = rd_i32(member_slot(b, *mParent));
|
||
uint32_t flags = 0;
|
||
transform_to_composite(member_slot(b, *mLocal), bo.local_transform, flags);
|
||
bo.lt_flags = flags;
|
||
const uint8_t* iw = at(member_slot(b, *mInvW), 64);
|
||
if (iw) for (int k = 0; k < 16; ++k) std::memcpy(&bo.inverse_world[k], iw + k * 4, 4);
|
||
else bo.inverse_world = Mat4{};
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ── M2:姿势采样(自由函数 = 主 primitive;File::* 是便捷封装)──────────
|
||
namespace {
|
||
void accumulate_world(const Skeleton& sk, const std::vector<Mat4>& local,
|
||
std::vector<Mat4>& world, std::vector<Mat4>& skin,
|
||
const Mat4& root_offset) {
|
||
const size_t n = sk.bones.size();
|
||
const Mat4 I{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
|
||
world.assign(n, I);
|
||
skin.assign(n, I);
|
||
for (size_t i = 0; i < n; ++i) {
|
||
int32_t par = sk.bones[i].parent;
|
||
const Mat4& parent_w = (par < 0 || (size_t)par >= i) ? root_offset : world[par];
|
||
world[i] = mul4x3((i < local.size()) ? local[i] : sk.bones[i].local_transform, parent_w);
|
||
skin[i] = mul4x3(sk.bones[i].inverse_world, world[i]);
|
||
}
|
||
}
|
||
} // namespace
|
||
|
||
void bind_pose(const Skeleton& sk, std::vector<Mat4>& world, std::vector<Mat4>& skin) {
|
||
std::vector<Mat4> local(sk.bones.size());
|
||
for (size_t i = 0; i < sk.bones.size(); ++i) local[i] = sk.bones[i].local_transform;
|
||
accumulate_world(sk, local, world, skin, sk.initial_placement);
|
||
}
|
||
|
||
void sample_pose(const Skeleton& sk, const Animation& an, float t,
|
||
std::vector<Mat4>& world, std::vector<Mat4>& skin,
|
||
const Mat4* root_offset) {
|
||
std::vector<Mat4> track_local;
|
||
an.sample_local(t, track_local);
|
||
|
||
std::vector<Mat4> local(sk.bones.size());
|
||
for (size_t i = 0; i < sk.bones.size(); ++i) local[i] = sk.bones[i].local_transform; // 默认 = bind
|
||
|
||
// track 按骨骼名匹配(bone_index 若已在 anim 提取时对某个 skeleton 映射过,可能不是这个 sk)
|
||
for (size_t ti = 0; ti < an.tracks.size() && ti < track_local.size(); ++ti) {
|
||
const BoneTrack& tr = an.tracks[ti];
|
||
int bi = -1;
|
||
for (size_t k = 0; k < sk.bones.size(); ++k)
|
||
if (sk.bones[k].name == tr.bone_name) { bi = int(k); break; }
|
||
if (bi >= 0) local[bi] = track_local[ti];
|
||
}
|
||
accumulate_world(sk, local, world, skin, root_offset ? *root_offset : sk.initial_placement);
|
||
}
|
||
|
||
bool File::bind_pose(int skeleton, std::vector<Mat4>& world, std::vector<Mat4>& skin) const {
|
||
if (!impl_ || skeleton < 0 || (size_t)skeleton >= impl_->skels_cache.size()) return false;
|
||
gr2::bind_pose(impl_->skels_cache[skeleton], world, skin);
|
||
return true;
|
||
}
|
||
|
||
bool File::sample_pose(int anim, float t, int skeleton,
|
||
std::vector<Mat4>& world, std::vector<Mat4>& skin) const {
|
||
if (!impl_ || skeleton < 0 || (size_t)skeleton >= impl_->skels_cache.size()) return false;
|
||
if (anim < 0 || (size_t)anim >= info_.animations.size())
|
||
return bind_pose(skeleton, world, skin);
|
||
gr2::sample_pose(impl_->skels_cache[skeleton], info_.animations[anim], t, world, skin);
|
||
return true;
|
||
}
|
||
|
||
double File::Impl::skeleton_self_check(int skeleton) const {
|
||
if (skeleton < 0 || (size_t)skeleton >= skels_cache.size()) return 1e9;
|
||
const auto& sk = skels_cache[skeleton];
|
||
const size_t n = sk.bones.size();
|
||
if (n == 0) return 1e9;
|
||
|
||
std::vector<Mat4> world(n);
|
||
double dev = 0.0;
|
||
size_t worst = 0;
|
||
for (size_t i = 0; i < n; ++i) {
|
||
int32_t par = sk.bones[i].parent;
|
||
if (par < 0 || (size_t)par >= i) {
|
||
// 根:world = local * InitialPlacement(BuildWorldPose 的 Offset4x4)
|
||
world[i] = mul4x3(sk.bones[i].local_transform, sk.initial_placement);
|
||
} else {
|
||
world[i] = mul4x3(sk.bones[i].local_transform, world[par]);
|
||
}
|
||
// E = InverseWorld * world ≈ I
|
||
Mat4 E = mul4x3(sk.bones[i].inverse_world, world[i]);
|
||
double bd = 0.0;
|
||
for (int k = 0; k < 16; ++k) {
|
||
float want = (k % 5 == 0) ? 1.0f : 0.0f;
|
||
bd = std::max(bd, (double)std::fabs(E[k] - want));
|
||
}
|
||
if (bd > dev) { dev = bd; worst = i; }
|
||
}
|
||
if (getenv("GR2_DEBUG")) {
|
||
fprintf(stderr, "[skel] self-check dev=%.5f worst bone %zu '%s' parent=%d\n",
|
||
dev, worst, sk.bones[worst].name.c_str(), sk.bones[worst].parent);
|
||
const Mat4& E = mul4x3(sk.bones[worst].inverse_world, world[worst]);
|
||
fprintf(stderr, "[skel] E[worst]:");
|
||
for (float x : E) fprintf(stderr, " %.3f", x);
|
||
fprintf(stderr, "\n");
|
||
}
|
||
return dev;
|
||
}
|
||
|
||
} // namespace gr2
|