Files
mtgodot-poc/libgr2/src/gr2_mesh.cpp
T
shenleiandClaude Opus 5.5 668f48d6ec port 2V2-d.2: Granny runtime on libgr2, main character model animates
Implement the Granny SDK 2.11 API the client uses on top of libgr2
(platform/EterGrnLib/GrannyRuntime.cpp, shim granny.h declared from the
SDK header): file sections, type-definition vertex conversion, material
textures by Usage, mesh/bone bindings, deformer, control clock/loop/ease
and weighted SRT sampling. Port EterGrnLib (LODController, Material, Mesh,
Model, ModelInstance*, Motion, Thing, ThingInstance, Util) plus EterLib
GrpObjectInstance/GrpCollisionObject/CollisionData verbatim and drop their
platform skeletons and pending stand-ins; register the gr2 resource
factory; vertex/index buffers are CPU-side until the 2V2-e render adapter.

app.UpdateGame/RenderGame now run 40250's bodies, reaching the
CPythonApplication members through their singletons (the platform has no
app object); CPythonGraphic joins GameSingletons in member order, and
GetMousePosition reads the host cursor.

port.login_flow asserts the main instance's PART_MAIN model has meshes,
its diffuse image loaded and the WAIT motion moves the bones between
frames; offline and live pass, port_gate macos PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 19:43:27 +09:00

380 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#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<Skeleton>& 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.vertex_stride = stride;
for (const auto& m : vfmt)
out.vertex_members.push_back({m.name, m.type, m.array_width, m.offset});
{
const uint8_t* vb = sections[vdata.section].data.data() + vdata.offset;
out.raw_vertices.assign(vb, vb + size_t(vcount) * stride);
}
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;
int32_t mi = -1;
if (mMat) {
Ref mat = read_reference(e, *mMat);
tn = material_texture_name(mat, mMat->ref_type, 0);
mi = intern_material(mat, mMat->ref_type);
}
out.material_textures.push_back(std::move(tn));
out.material_bindings.push_back(mi);
}
}
}
// ── 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<std::string> 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<int32_t> best(bc, -1);
int best_dangling = int(bc) + 1;
for (const auto& sk : skels) {
std::vector<int32_t> 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);
out.bone_binding_names = std::move(names);
}
}
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 {};
}
// granny_material {String Name; RefToArray Maps(material_map{String Usage; Reference Material});
// Reference Texture; VariantRef ExtendedData}
int32_t File::Impl::intern_material(Ref mat_obj, Ref mat_type, int depth) {
if (!mats_out || !mat_obj.valid() || !mat_type.valid() || depth > 8) return -1;
for (size_t k = 0; k < mat_refs.size(); ++k)
if (mat_refs[k].section == mat_obj.section && mat_refs[k].offset == mat_obj.offset)
return int32_t(k);
const int32_t idx = int32_t(mats_out->size());
mats_out->emplace_back();
mat_refs.push_back(mat_obj);
const auto& mms = parse_type(mat_type);
Material mat;
if (const TypeMember* m = find_member(mms, "Name")) mat.name = rd_str(member_slot(mat_obj, *m));
mat.diffuse_texture = material_texture_name(mat_obj, mat_type, 0);
if (const TypeMember* mTex = find_member(mms, "Texture")) {
Ref tex = read_reference(mat_obj, *mTex);
if (tex.valid())
if (const TypeMember* mFN = find_member(parse_type(mTex->ref_type), "FromFileName"))
mat.texture = rd_str(member_slot(tex, *mFN));
}
if (const TypeMember* mE = find_member(mms, "ExtendedData"))
mat.has_two_sided = extended_i32(member_slot(mat_obj, *mE), "Two-sided", mat.two_sided);
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 && s) {
const auto& mapT = parse_type(mMaps->ref_type);
const TypeMember* mU = find_member(mapT, "Usage");
const TypeMember* mSub = find_member(mapT, "Material");
if (!mSub) mSub = find_member(mapT, "Map");
for (uint32_t i = 0; i < c && i < 64; ++i) {
Ref e = {b.section, b.offset + i * s};
MaterialMap mm;
if (mU) mm.usage = rd_str(member_slot(e, *mU));
if (mSub) mm.material = intern_material(read_reference(e, *mSub), mSub->ref_type, depth + 1);
mat.maps.push_back(std::move(mm));
}
}
}
(*mats_out)[idx] = std::move(mat);
return idx;
}
// VariantRef 在 32 位文件里 = {ptr Type; ptr Object}。
bool File::Impl::extended_i32(Ref slot, const char* name, int32_t& out) {
Ref type = follow(slot);
Ref obj = follow({slot.section, slot.offset + 4});
if (!type.valid() || !obj.valid()) return false;
const TypeMember* m = find_member(parse_type(type), name);
if (!m || (m->type != detail::MT_Int32 && m->type != detail::MT_UInt32)) return false;
out = rd_i32(member_slot(obj, *m));
return true;
}
} // namespace gr2