// oracle_diff —— libgr2 vs oracle 逐字段对拍(M2 门禁·主)。见 oracle/FORMAT.md、docs/steps/00-oracle.md // // oracle_diff [|-] [--t ] [--mesh N] // // oracle_dump.bin = `oracle dump` 产出(层① 世界矩阵 + mesh N 蒙皮顶点)。 // 本工具用 libgr2 对同一 (model, anim, t) 算一遍,报 max|Δ|(矩阵)+ max‖Δ‖(顶点)。 #include "gr2/gr2.h" #include #include #include #include #include #include namespace { struct Reader { const uint8_t* p; const uint8_t* end; bool ok = true; uint32_t u32() { if (p + 4 > end) { ok = false; return 0; } uint32_t v; std::memcpy(&v, p, 4); p += 4; return v; } float f32() { if (p + 4 > end) { ok = false; return 0; } float v; std::memcpy(&v, p, 4); p += 4; return v; } std::string str() { uint32_t n = u32(); if (!ok || p + n > end) { ok = false; return {}; } std::string s((const char*)p, n); p += n; return s; } void skip(size_t n) { if (p + n > end) ok = false; else p += n; } }; // 行主序 4x4 A*B(ColumnMatrixMultiply4x3 语义,平移 elem 12..14) void mul4x3(const float* A, const float* B, float* 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; } } // namespace int main(int argc, char** argv) { if (argc < 4) { std::fprintf(stderr, "usage: oracle_diff [--t ] [--mesh N]\n"); return 2; } const char* model_path = argv[1]; const char* anim_path = std::strcmp(argv[2], "-") == 0 ? nullptr : argv[2]; const char* bin_path = argv[3]; float t_override = -1.0f; int mesh_index = 0; for (int i = 4; i < argc; ++i) { if (!std::strcmp(argv[i], "--t") && i + 1 < argc) t_override = float(std::atof(argv[++i])); else if (!std::strcmp(argv[i], "--mesh") && i + 1 < argc) mesh_index = std::atoi(argv[++i]); } // ── 读 oracle dump ────────────────────────────────────────────── FILE* f = std::fopen(bin_path, "rb"); if (!f) { std::fprintf(stderr, "cannot open %s\n", bin_path); return 1; } std::fseek(f, 0, SEEK_END); long n = std::ftell(f); std::fseek(f, 0, SEEK_SET); std::vector buf(n > 0 ? size_t(n) : 0); if (std::fread(buf.data(), 1, buf.size(), f) != buf.size()) { std::fclose(f); return 1; } std::fclose(f); Reader r{buf.data(), buf.data() + buf.size()}; if (std::memcmp(r.p, "XORC", 4) != 0) { std::fprintf(stderr, "bad magic\n"); return 1; } r.skip(4); uint32_t ver = r.u32(), kind = r.u32(), coord = r.u32(); char gv[16]; std::memcpy(gv, r.p, 16); r.skip(16); std::string omodel = r.str(), oanim = r.str(); float ot = r.f32(); (void)ver; (void)coord; std::printf("oracle dump: kind=%u granny=%.15s t=%.6f\n model=%s\n anim =%s\n", kind, gv, ot, omodel.c_str(), oanim.c_str()); uint32_t obone_count = r.u32(); std::vector omat(size_t(obone_count) * 16); for (uint32_t b = 0; b < obone_count; ++b) for (int k = 0; k < 16; ++k) omat[b*16+k] = r.f32(); uint32_t overt_count = r.u32(); std::vector overt(size_t(overt_count) * 3); for (uint32_t v = 0; v < overt_count; ++v) for (int k = 0; k < 3; ++k) overt[v*3+k] = r.f32(); if (!r.ok) { std::fprintf(stderr, "dump truncated\n"); return 1; } std::printf(" bones=%u skinned_verts=%u\n", obone_count, overt_count); // ── libgr2 侧算一遍 ──────────────────────────────────────────── gr2::LoadError e; auto model = gr2::File::load_path(model_path, &e); if (!model) { std::fprintf(stderr, "libgr2 model load: %s\n", e.message.c_str()); return 1; } if (model->file_info().skeletons.empty()) { std::fprintf(stderr, "no skeleton\n"); return 1; } const gr2::Skeleton& sk = model->file_info().skeletons[0]; std::vector world, skin; float t = t_override >= 0 ? t_override : ot; if (anim_path) { auto af = gr2::File::load_path(anim_path, &e); if (!af || af->file_info().animations.empty()) { std::fprintf(stderr, "libgr2 anim load: %s\n", e.message.c_str()); return 1; } gr2::sample_pose(sk, af->file_info().animations[0], t, world, skin); } else { gr2::bind_pose(sk, world, skin); } // ── 门禁 1:骨骼世界矩阵 ────────────────────────────────────────── if (world.size() != obone_count) { std::fprintf(stderr, "bone count mismatch: libgr2=%zu oracle=%u\n", world.size(), obone_count); return 1; } double mat_max = 0.0; int mat_worst = -1; for (uint32_t b = 0; b < obone_count; ++b) { double d = 0.0; for (int k = 0; k < 16; ++k) d = std::fmax(d, std::fabs((double)world[b][k] - omat[b*16+k])); if (d > mat_max) { mat_max = d; mat_worst = int(b); } } std::printf("\n[gate 1] 骨骼世界矩阵 vs oracle 层①:\n"); std::printf(" max|Δ| = %.6g (worst bone %d '%s')\n", mat_max, mat_worst, mat_worst >= 0 ? sk.bones[mat_worst].name.c_str() : ""); // ── 门禁 2:蒙皮顶点 ──────────────────────────────────────────── double vtx_max = 0.0; bool vtx_checked = false; if (overt_count) { const auto& meshes = model->file_info().meshes; if (mesh_index < (int)meshes.size()) { const auto& me = meshes[mesh_index]; vtx_checked = true; // libgr2 侧手算 LBS(与 engine/skinning.cpp 同公式,不引 engine 依赖) int vtx_worst = -1; uint32_t nv = std::min(overt_count, (uint32_t)me.vertices.size()); for (uint32_t i = 0; i < nv; ++i) { const auto& sv = me.vertices[i]; float w[4]; float sw = 0; for (int k = 0; k < 4; ++k) { w[k] = sv.bone_weight[k] / 255.0f; sw += w[k]; } if (sw > 1e-6f) for (int k = 0; k < 4; ++k) w[k] /= sw; else w[0] = 1.0f; float M[16] = {0}; for (int k = 0; k < 4; ++k) { int slot = sv.bone_index[k]; int bone = (slot >= 0 && slot < (int)me.bone_bindings.size()) ? me.bone_bindings[slot] : -1; const float* S = (bone >= 0 && bone < (int)skin.size()) ? skin[bone].data() : nullptr; if (!S) continue; for (int j = 0; j < 16; ++j) M[j] += S[j] * w[k]; } float o[3]; o[0] = sv.pos[0]*M[0] + sv.pos[1]*M[4] + sv.pos[2]*M[8] + M[12]; o[1] = sv.pos[0]*M[1] + sv.pos[1]*M[5] + sv.pos[2]*M[9] + M[13]; o[2] = sv.pos[0]*M[2] + sv.pos[1]*M[6] + sv.pos[2]*M[10] + M[14]; double dx = o[0]-overt[i*3+0], dy = o[1]-overt[i*3+1], dz = o[2]-overt[i*3+2]; double d = std::sqrt(dx*dx + dy*dy + dz*dz); if (d > vtx_max) { vtx_max = d; vtx_worst = int(i); } } std::printf("\n[gate 2] mesh %d 蒙皮顶点 vs oracle:\n", mesh_index); std::printf(" verts compared = %u\n", nv); std::printf(" max‖Δ‖ = %.6g (worst vert %d)\n", vtx_max, vtx_worst); } } (void)mul4x3; // 阈值:noise_floor.json 就绪前用硬编码 1e-3(远高于实测的 ~1e-4,留浮点累积余量) const double kMatEps = 1e-3, kVtxEps = 1e-3; bool pass = mat_max < kMatEps && (!vtx_checked || vtx_max < kVtxEps); std::printf("\n[verdict] granny=%.15s vs libgr2 : %s (mat %.2g < %.0e, vtx %.2g < %.0e)\n", gv, pass ? "PASS" : "FAIL", mat_max, kMatEps, vtx_checked ? vtx_max : 0.0, kVtxEps); return pass ? 0 : 1; }