// M0 T6 —— 顶点 / 索引 / 三角组 / BoneBindings。见 docs/reference/steps/M0-gr2-reader.md // 规格:granny_mesh.cpp (MeshType/BoneBindingType) / granny_vertex_data.cpp (VertexDataType // + 各顶点格式) / granny_tri_topology.cpp (TriTopologyType/TriMaterialGroupType) // 顶点格式是自描述的(Vertices 是 ReferenceToVariantArray,带 data_type_definition*), // 按成员名(Position/Normal/TextureCoordinates0/1/BoneWeights/BoneIndices)取分量,不硬编码布局。 #include "internal.h" #include #include #include #include #define MDBG(...) do { if (getenv("GR2_DEBUG")) fprintf(stderr, "[mesh] " __VA_ARGS__); } while (0) namespace gr2 { using detail::Ref; using detail::TypeMember; namespace { // member_type → (元素是浮点?, 元素字节数, 是否归一化整型) struct Elem { bool is_float; int bytes; bool normalized; bool is_signed; }; Elem elem_of(int mt) { switch (mt) { case detail::MT_Real32: return {true, 4, false, true}; case detail::MT_Real16: return {false, 2, false, true}; // 半精度,少见,按需扩展 case detail::MT_Int8: return {false, 1, false, true}; case detail::MT_UInt8: return {false, 1, false, false}; case detail::MT_BinormInt8: return {false, 1, true, true}; case detail::MT_NormUInt8: return {false, 1, true, false}; case detail::MT_Int16: return {false, 2, false, true}; case detail::MT_UInt16: return {false, 2, false, false}; case detail::MT_BinormInt16: return {false, 2, true, true}; case detail::MT_NormUInt16: return {false, 2, true, false}; case detail::MT_Int32: return {false, 4, false, true}; case detail::MT_UInt32: return {false, 4, false, false}; default: return {false, 0, false, false}; } } float read_scalar(const uint8_t* p, const Elem& e) { switch (e.bytes) { case 4: if (e.is_float) { float v; std::memcpy(&v, p, 4); return v; } if (e.is_signed) { int32_t v; std::memcpy(&v, p, 4); return float(v); } { uint32_t v; std::memcpy(&v, p, 4); return float(v); } case 2: { if (e.is_signed) { int16_t v; std::memcpy(&v, p, 2); return e.normalized ? v / 32767.0f : float(v); } uint16_t v; std::memcpy(&v, p, 2); return e.normalized ? v / 65535.0f : float(v); } case 1: { if (e.is_signed) { int8_t v = (int8_t)p[0]; return e.normalized ? v / 127.0f : float(v); } return e.normalized ? p[0] / 255.0f : float(p[0]); } default: return 0.0f; } } } // namespace // mesh: {String Name; Reference PrimaryVertexData; RefToArray MorphTargets; // Reference PrimaryTopology; RefToArray MaterialBindings; RefToArray BoneBindings; ...} bool File::Impl::extract_mesh(Ref mesh_obj, Ref mesh_type, const std::vector& skels, Mesh& out) { if (!mesh_obj.valid() || !mesh_type.valid()) return false; const auto& mms = parse_type(mesh_type); if (getenv("GR2_DEBUG")) { fprintf(stderr, "[mesh] mesh_type=(%d,%u) members=%zu:\n", mesh_type.section, mesh_type.offset, mms.size()); for (auto& m : mms) fprintf(stderr, "[mesh] %-20s type=%d aw=%d off=%u size=%u reftype=(%d,%u)\n", m.name.c_str(), m.type, m.array_width, m.offset, m.size, m.ref_type.section, m.ref_type.offset); } if (const TypeMember* mName = find_member(mms, "Name")) out.name = rd_str(member_slot(mesh_obj, *mName)); // ── 顶点 ────────────────────────────────────────────────────────── const TypeMember* mVD = find_member(mms, "PrimaryVertexData"); if (!mVD) { MDBG("no PrimaryVertexData member\n"); return false; } MDBG("PrimaryVertexData m.offset=%u ref_type=(%d,%u)\n", mVD->offset, mVD->ref_type.section, mVD->ref_type.offset); Ref vd = read_reference(mesh_obj, *mVD); if (!vd.valid()) { MDBG("PrimaryVertexData ref -> NULL (slot %d,%u)\n", member_slot(mesh_obj, *mVD).section, member_slot(mesh_obj, *mVD).offset); return false; } const auto& vdms = parse_type(mVD->ref_type); const TypeMember* mVerts = find_member(vdms, "Vertices"); if (!mVerts) { MDBG("no Vertices member (vdms=%zu)\n", vdms.size()); return false; } // ReferenceToVariantArray on-disk (32-bit): {def* Type; int32 Count; void* Ptr} // (granny_data_type_definition.cpp: Ptr32 + int32 + Ptr32) Ref vslot = member_slot(vd, *mVerts); Ref vtype = follow({vslot.section, vslot.offset + 0}); int32_t vcount = rd_i32({vslot.section, vslot.offset + 4}); Ref vdata = follow({vslot.section, vslot.offset + 8}); MDBG("Vertices count=%d vtype=(%d,%u) vdata=(%d,%u)\n", vcount, vtype.section, vtype.offset, vdata.section, vdata.offset); if (vcount <= 0 || vcount > (1 << 24) || !vtype.valid() || !vdata.valid()) return false; const auto& vfmt = parse_type(vtype); uint32_t stride = 0; for (const auto& m : vfmt) stride += m.size; MDBG("vfmt members=%zu stride=%u\n", vfmt.size(), stride); if (stride == 0) return false; struct Comp { const TypeMember* m; Elem e; }; auto comp = [&](const char* n) -> Comp { const TypeMember* m = find_member(vfmt, n); if (!m) return {nullptr, {}}; return {m, elem_of(m->type)}; }; Comp cPos = comp("Position"); Comp cNrm = comp("Normal"); Comp cUv0 = comp("TextureCoordinates0"); Comp cUv1 = comp("TextureCoordinates1"); Comp cBW = comp("BoneWeights"); Comp cBI = comp("BoneIndices"); const bool skinned = (cBW.m && cBI.m); out.rigid = !skinned; if (skinned) { out.source_bone_weight_slots = uint32_t(cBW.m->array_width > 0 ? cBW.m->array_width : 1); out.source_bone_index_slots = (cBI.e.bytes == 4 && cBI.m->array_width <= 1) ? 4u : uint32_t(cBI.m->array_width > 0 ? cBI.m->array_width : 1); } if (skinned && cUv1.m) out.kind = VertexKind::PNT3322_Skinned; else if (skinned) out.kind = VertexKind::PNT332_Skinned; else if (cUv1.m) out.kind = VertexKind::PNT3322; else if (cPos.m && cNrm.m) out.kind = VertexKind::PNT332; else out.kind = VertexKind::Unknown; if ((uint64_t)vdata.offset + (uint64_t)vcount * stride > sections[vdata.section].data.size()) return false; out.vertices.resize(vcount); for (int32_t i = 0; i < vcount; ++i) { const uint8_t* base = sections[vdata.section].data.data() + vdata.offset + size_t(i) * stride; Vertex& v = out.vertices[i]; auto rd = [&](const Comp& c, float* dst, int n) { if (!c.m || !c.e.bytes) return; int aw = c.m->array_width <= 0 ? 1 : c.m->array_width; n = n < aw ? n : aw; for (int k = 0; k < n; ++k) dst[k] = read_scalar(base + c.m->offset + size_t(k) * c.e.bytes, c.e); }; rd(cPos, v.pos, 3); rd(cNrm, v.normal, 3); rd(cUv0, v.uv0, 2); rd(cUv1, v.uv1, 2); if (skinned) { float w[4] = {0,0,0,0}, bi[4] = {0,0,0,0}; rd(cBW, w, 4); // BoneIndices 可能是 UInt8[4] 或 UInt32[N](PWN* 系列是单 uint32 packed 4×u8) if (cBI.e.bytes == 1) { rd(cBI, bi, 4); } else if (cBI.e.bytes == 4 && cBI.e.is_float == false) { uint32_t packed; std::memcpy(&packed, base + cBI.m->offset, 4); for (int k = 0; k < 4; ++k) bi[k] = float((packed >> (k * 8)) & 0xff); } for (int k = 0; k < 4; ++k) { v.bone_index[k] = uint8_t(std::lround(bi[k])); float wn = cBW.e.normalized ? w[k] : w[k]; // 已在 read_scalar 归一 v.bone_weight[k] = uint8_t(std::lround(std::fmin(std::fmax(wn, 0.0f), 1.0f) * 255.0f)); } } } // ── 索引 + 三角组 ──────────────────────────────────────────────── const TypeMember* mTopo = find_member(mms, "PrimaryTopology"); if (mTopo) { Ref tt = read_reference(mesh_obj, *mTopo); if (tt.valid()) { const auto& tms = parse_type(mTopo->ref_type); if (const TypeMember* mG = find_member(tms, "Groups")) { Ref gb; uint32_t gc = 0, gs = 0; if (read_ref_to_array(tt, *mG, gb, gc, gs) && gc) { const auto& gmT = parse_type(mG->ref_type); const TypeMember* mMI = find_member(gmT, "MaterialIndex"); const TypeMember* mTF = find_member(gmT, "TriFirst"); const TypeMember* mTC = find_member(gmT, "TriCount"); for (uint32_t i = 0; i < gc; ++i) { Ref g = {gb.section, gb.offset + i * gs}; TriGroup tg{}; if (mMI) tg.material_index = rd_i32(member_slot(g, *mMI)); if (mTF) tg.tri_first = rd_i32(member_slot(g, *mTF)); if (mTC) tg.tri_count = rd_i32(member_slot(g, *mTC)); out.tri_groups.push_back(tg); } } } auto load_idx = [&](const char* name, int elem_bytes) -> bool { const TypeMember* mI = find_member(tms, name); if (!mI) return false; Ref ib; uint32_t ic = 0, is = 0; if (!read_ref_to_array(tt, *mI, ib, ic, is) || ic == 0) return false; const uint8_t* p = at(ib, size_t(ic) * elem_bytes); if (!p) return false; out.indices.resize(ic); for (uint32_t i = 0; i < ic; ++i) { if (elem_bytes == 2) { uint16_t v; std::memcpy(&v, p + i*2, 2); out.indices[i] = v; } else { uint32_t v; std::memcpy(&v, p + i*4, 4); out.indices[i] = v; } } return true; }; if (!load_idx("Indices", 4)) load_idx("Indices16", 2); } } // ── MaterialBindings:与 tri_group.material_index 平行的贴图名表 ─── // granny_mesh.MaterialBindings = RefToArray of {Reference Material}。 if (const TypeMember* mMB = find_member(mms, "MaterialBindings")) { Ref mb; uint32_t mc = 0, ms = 0; if (read_ref_to_array(mesh_obj, *mMB, mb, mc, ms) && mc) { const auto& bT = parse_type(mMB->ref_type); const TypeMember* mMat = find_member(bT, "Material"); out.material_textures.reserve(mc); for (uint32_t i = 0; i < mc; ++i) { Ref e = {mb.section, mb.offset + i * ms}; std::string tn; if (mMat) { Ref mat = read_reference(e, *mMat); tn = material_texture_name(mat, mMat->ref_type, 0); } out.material_textures.push_back(std::move(tn)); } } } // ── BoneBindings:mesh 局部骨骼槽 → skeleton 骨骼索引 ───────────── // 文件可能带多个 skeleton(本体 + 武器挂点等)。对每个 skeleton 试解析, // 取"悬空最少"的那个。 if (const TypeMember* mBB = find_member(mms, "BoneBindings")) { Ref bb; uint32_t bc = 0, bs = 0; if (read_ref_to_array(mesh_obj, *mBB, bb, bc, bs) && bc) { const auto& bbT = parse_type(mBB->ref_type); const TypeMember* mBN = find_member(bbT, "BoneName"); const TypeMember* mMin = find_member(bbT, "OBBMin"); const TypeMember* mMax = find_member(bbT, "OBBMax"); out.bone_bounds.resize(bc); std::vector names(bc); for (uint32_t i = 0; i < bc; ++i) { Ref e = {bb.section, bb.offset + i * bs}; names[i] = mBN ? rd_str(member_slot(e, *mBN)) : std::string(); auto &bounds = out.bone_bounds[i]; if (mMin && mMax && mMin->type == detail::MT_Real32 && mMax->type == detail::MT_Real32 && mMin->array_width == 3 && mMax->array_width == 3) { Ref lo = member_slot(e, *mMin), hi = member_slot(e, *mMax); bounds.valid = true; for (int axis = 0; axis < 3; ++axis) { bounds.min[axis] = rd_f32({lo.section, lo.offset + uint32_t(axis * 4)}); bounds.max[axis] = rd_f32({hi.section, hi.offset + uint32_t(axis * 4)}); bounds.valid &= std::isfinite(bounds.min[axis]) && std::isfinite(bounds.max[axis]) && bounds.min[axis] <= bounds.max[axis]; } } } std::vector best(bc, -1); int best_dangling = int(bc) + 1; for (const auto& sk : skels) { std::vector cur(bc, -1); int dangling = 0; for (uint32_t i = 0; i < bc; ++i) { for (size_t k = 0; k < sk.bones.size(); ++k) if (sk.bones[k].name == names[i]) { cur[i] = int32_t(k); break; } if (cur[i] < 0) ++dangling; } if (dangling < best_dangling) { best_dangling = dangling; best = cur; } if (dangling == 0) break; } out.bone_bindings = std::move(best); } } return true; } // granny_material {String Name; int MapCount; material_map* Maps; texture* Texture; ...} // granny_material_map {String Usage; material* Material} // granny_texture {String FromFileName; ...} std::string File::Impl::material_texture_name(Ref mat_obj, Ref mat_type, int depth) { if (!mat_obj.valid() || !mat_type.valid() || depth > 8) return {}; const auto& mms = parse_type(mat_type); // 1) 直接挂 texture if (const TypeMember* mTex = find_member(mms, "Texture")) { Ref tex = read_reference(mat_obj, *mTex); if (tex.valid()) { const auto& tms = parse_type(mTex->ref_type); if (const TypeMember* mFN = find_member(tms, "FromFileName")) { std::string s = rd_str(member_slot(tex, *mFN)); if (!s.empty()) return s; } } } // 2) 递归 Maps[].Map(复合材质:granny_material_map = {String Usage; material* Map}) if (const TypeMember* mMaps = find_member(mms, "Maps")) { Ref b; uint32_t c = 0, s = 0; if (read_ref_to_array(mat_obj, *mMaps, b, c, s) && c) { const auto& mapT = parse_type(mMaps->ref_type); const TypeMember* mSub = find_member(mapT, "Map"); if (!mSub) mSub = find_member(mapT, "Material"); for (uint32_t i = 0; i < c && mSub; ++i) { Ref e = {b.section, b.offset + i * s}; Ref sub = read_reference(e, *mSub); std::string r = material_texture_name(sub, mSub->ref_type, depth + 1); if (!r.empty()) return r; } } } return {}; } } // namespace gr2