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>
This commit is contained in:
shenlei
2026-09-23 19:43:27 +09:00
co-authored by Claude Opus 5.5
parent 5266b00c6e
commit 668f48d6ec
74 changed files with 11404 additions and 2356 deletions
+53
View File
@@ -12,6 +12,17 @@ namespace gr2 {
using Mat4 = std::array<float, 16>; // 行主序,Granny 原生。坐标约定见 docs/reference/steps/M0-gr2-reader.md「约定」
// granny_transform(SRT 分量原样;Flags: 1=HasPosition 2=HasOrientation 4=HasScaleShear)。
// 动画混合要在分量上做(GrannySampleModelAnimations 累加 SRT 再归一),不能在 4x4 上做。
struct Transform {
uint32_t flags = 0;
float position[3] = {0, 0, 0};
float orientation[4] = {0, 0, 0, 1}; // x,y,z,w
float scale_shear[9] = {1,0,0, 0,1,0, 0,0,1}; // 行主序 3x3
};
// BuildCompositeTransform4x4:上 3x3 = transpose(R3·SS3),平移在 elem 12..14。
Mat4 compose(const Transform& t);
// ── 骨架 ──────────────────────────────────────────────────────────────
struct Bone {
std::string name;
@@ -19,6 +30,7 @@ struct Bone {
Mat4 local_transform{}; // granny_transform SRT → 4x4(组合顺序照 granny_transform.cpp)
Mat4 inverse_world{}; // InverseWorld4x4,来自文件
uint32_t lt_flags = 0; // granny_transform.Flags:哪部分非单位
Transform local; // LocalTransform 的 SRT 分量(local_transform = compose(local))
};
struct Skeleton {
@@ -44,6 +56,14 @@ struct TriGroup {
int32_t tri_count;
};
// 顶点类型的一个成员(granny_data_type_definition 的平铺子集;type = granny_member_type 序数)。
struct VertexMember {
std::string name;
int32_t type = 0;
int32_t array_width = 0; // 0 => 1
uint32_t offset = 0; // 在顶点里的字节偏移(打包)
};
struct Mesh {
struct BoneBounds {
float min[3] = {}, max[3] = {};
@@ -63,6 +83,12 @@ struct Mesh {
// tri_groups[i].material_index 索引本表。
std::vector<std::string> material_textures;
bool rigid = false; // GrannyMeshIsRigid
std::vector<std::string> bone_binding_names; // granny_bone_binding.BoneName,与 bone_bounds 平行
std::vector<int32_t> material_bindings; // granny_mesh.MaterialBindings[i].Material → FileInfo::materials 索引(-1 = 空)
// 原始顶点:文件声明的顶点类型 + 按它打包的字节(Granny 运行时按名字做布局转换用)
std::vector<VertexMember> vertex_members;
uint32_t vertex_stride = 0;
std::vector<uint8_t> raw_vertices; // vertices.size() * vertex_stride
};
// ── 动画 ──────────────────────────────────────────────────────────────
@@ -82,6 +108,9 @@ struct Curve {
void eval(float t, float* out) const;
};
struct TextTrackEntry { float time_stamp = 0.0f; std::string text; };
struct TextTrack { std::string name; std::vector<TextTrackEntry> entries; };
struct BoneTrack {
std::string bone_name;
int32_t bone_index = -1; // 映射到 skeleton(未映射 = -1)
@@ -91,11 +120,17 @@ struct BoneTrack {
Curve position; // dim 3
Curve orientation; // dim 4(四元数 x,y,z,w)
Curve scale_shear; // dim 9(3x3)或 dim 3
// t 处的 SRT(缺的曲线取单位分量,flags 只标出有曲线的分量)。sample_local 的 4x4 = compose(这个)。
void sample(float t, Transform& out) const;
};
struct Animation {
std::string name;
float duration = 0.0f;
float time_step = 0.0f;
float oversampling = 1.0f;
int32_t track_group_count = 0;
std::vector<TextTrack> text_tracks; // TrackGroups[0].TextTracks
std::vector<BoneTrack> tracks;
// t 处每个 track 的局部 4x4(granny_transform 组合语义,行主序)。
// out 按 tracks 顺序;bone_index 见各 track。
@@ -103,9 +138,26 @@ struct Animation {
};
// 顶层 granny_material(driven by root Materials[])。name + 解析出的漫反射贴图。
// root Materials[] 之外、只经 Maps[] / mesh MaterialBindings 可达的子材质追加在后面。
struct MaterialMap {
std::string usage; // granny_material_map.Usage("Diffuse Color" 等)
int32_t material = -1; // FileInfo::materials 索引
};
struct Material {
std::string name;
std::string diffuse_texture; // granny_texture.FromFileName 原样
std::string texture; // 本材质自己的 Texture.FromFileName(空 = 无 Texture)
std::vector<MaterialMap> maps; // granny_material.Maps
int32_t two_sided = 0; // ExtendedData 的 Int32 "Two-sided"
bool has_two_sided = false;
};
// granny_model:Skeleton + InitialPlacement + MeshBindings。
struct Model {
std::string name;
int32_t skeleton = -1; // FileInfo::skeletons 索引
Transform initial_placement;
std::vector<int32_t> mesh_bindings; // FileInfo::meshes 索引(-1 = 未解析)
};
// ── FileInfo 汇总(T4)────────────────────────────────────────────────
@@ -115,6 +167,7 @@ struct FileInfo {
std::vector<Mesh> meshes;
std::vector<Animation> animations;
std::vector<Material> materials;
std::vector<Model> models;
uint32_t material_count = 0;
uint32_t texture_count = 0;
uint32_t model_count = 0;
+57 -41
View File
@@ -98,11 +98,47 @@ bool File::Impl::extract_animation(Ref anim_obj, Ref anim_type, const Skeleton*
out.name = rd_str(member_slot(anim_obj, *m));
if (const TypeMember* m = find_member(ams, "Duration"))
out.duration = rd_f32(member_slot(anim_obj, *m));
if (const TypeMember* m = find_member(ams, "TimeStep"))
out.time_step = rd_f32(member_slot(anim_obj, *m));
if (const TypeMember* m = find_member(ams, "Oversampling"))
out.oversampling = rd_f32(member_slot(anim_obj, *m));
const TypeMember* mTG = find_member(ams, "TrackGroups");
if (!mTG) return true;
auto groups = read_array_of_refs(anim_obj, *mTG);
out.track_group_count = int32_t(groups.size());
const auto& tgms = parse_type(mTG->ref_type);
// TrackGroups[0].TextTracks: RefToArray text_track{String Name; RefToArray Entries(
// text_track_entry{Real32 TimeStamp; String Text})}(motion event 用)
if (!groups.empty() && groups[0].valid())
if (const TypeMember* mTx = find_member(tgms, "TextTracks")) {
Ref xb; uint32_t xc = 0, xs = 0;
if (read_ref_to_array(groups[0], *mTx, xb, xc, xs) && xc && xs) {
const auto& xms = parse_type(mTx->ref_type);
const TypeMember* mXN = find_member(xms, "Name");
const TypeMember* mXE = find_member(xms, "Entries");
for (uint32_t i = 0; i < xc && i < 4096; ++i) {
Ref x = {xb.section, xb.offset + i * xs};
TextTrack tt;
if (mXN) tt.name = rd_str(member_slot(x, *mXN));
Ref eb; uint32_t ec = 0, es = 0;
if (mXE && read_ref_to_array(x, *mXE, eb, ec, es) && ec && es) {
const auto& ems = parse_type(mXE->ref_type);
const TypeMember* mTS = find_member(ems, "TimeStamp");
const TypeMember* mTT = find_member(ems, "Text");
for (uint32_t k = 0; k < ec && k < 65536; ++k) {
Ref e = {eb.section, eb.offset + k * es};
TextTrackEntry te;
if (mTS) te.time_stamp = rd_f32(member_slot(e, *mTS));
if (mTT) te.text = rd_str(member_slot(e, *mTT));
tt.entries.push_back(std::move(te));
}
}
out.text_tracks.push_back(std::move(tt));
}
}
}
const TypeMember* mTT = find_member(tgms, "TransformTracks");
if (!mTT) return true;
@@ -225,49 +261,29 @@ void Curve::eval(float t, float* out) const {
}
}
// t 处每个 track 的局部 4x4(BuildCompositeTransform4x4 语义:C 上 3x3 = transpose(R3*SS3),
// 平移在 elem 12..14)。position 缺省 (0,0,0),orientation 缺省单位四元数,scaleshear 缺省 I。
void Animation::sample_local(float t, std::vector<Mat4>& out) const {
out.assign(tracks.size(), Mat4{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1});
for (size_t ti = 0; ti < tracks.size(); ++ti) {
const BoneTrack& tr = tracks[ti];
float pos[3] = {0, 0, 0};
float ori[4] = {0, 0, 0, 1};
float ss[9] = {1,0,0, 0,1,0, 0,0,1};
if (!tr.position.empty()) tr.position.eval(t, pos);
if (!tr.orientation.empty()) tr.orientation.eval(t, ori);
bool have_ss = false;
if (!tr.scale_shear.empty()) {
if (tr.scale_shear.dim == 9) { tr.scale_shear.eval(t, ss); have_ss = true; }
else if (tr.scale_shear.dim == 3) {
float s[3]; tr.scale_shear.eval(t, s);
ss[0] = s[0]; ss[4] = s[1]; ss[8] = s[2]; have_ss = true;
}
// t 处的 SRT。position 缺省 (0,0,0),orientation 缺省单位四元数,scaleshear 缺省 I。
void BoneTrack::sample(float t, Transform& out) const {
out = Transform{};
if (!position.empty()) { position.eval(t, out.position); out.flags |= 0x1; }
if (!orientation.empty()) { orientation.eval(t, out.orientation); out.flags |= 0x2; }
if (!scale_shear.empty()) {
if (scale_shear.dim == 9) { scale_shear.eval(t, out.scale_shear); out.flags |= 0x4; }
else if (scale_shear.dim == 3) {
float s[3]; scale_shear.eval(t, s);
out.scale_shear[0] = s[0]; out.scale_shear[4] = s[1]; out.scale_shear[8] = s[2];
out.flags |= 0x4;
}
}
}
// R3 (行主序) from quat
const float xx = ori[0]*ori[0], yy = ori[1]*ori[1], zz = ori[2]*ori[2];
const float xy = ori[0]*ori[1], xz = ori[0]*ori[2], yz = ori[1]*ori[2];
const float wx = ori[3]*ori[0], wy = ori[3]*ori[1], wz = ori[3]*ori[2];
float r3[9] = {
1 - 2*(yy+zz), 2*(xy-wz), 2*(xz+wy),
2*(xy+wz), 1 - 2*(xx+zz), 2*(yz-wx),
2*(xz-wy), 2*(yz+wx), 1 - 2*(xx+yy)
};
float u3[9];
if (have_ss) {
for (int r = 0; r < 3; ++r)
for (int c = 0; c < 3; ++c)
u3[r*3+c] = r3[r*3+0]*ss[0*3+c] + r3[r*3+1]*ss[1*3+c] + r3[r*3+2]*ss[2*3+c];
} else {
for (int k = 0; k < 9; ++k) u3[k] = r3[k];
}
Mat4& m = out[ti];
m = Mat4{};
m[0] = u3[0]; m[1] = u3[3]; m[2] = u3[6]; m[3] = 0;
m[4] = u3[1]; m[5] = u3[4]; m[6] = u3[7]; m[7] = 0;
m[8] = u3[2]; m[9] = u3[5]; m[10] = u3[8]; m[11] = 0;
m[12] = pos[0]; m[13] = pos[1]; m[14] = pos[2]; m[15] = 1;
// t 处每个 track 的局部 4x4(BuildCompositeTransform4x4 语义:C 上 3x3 = transpose(R3*SS3),
// 平移在 elem 12..14)。
void Animation::sample_local(float t, std::vector<Mat4>& out) const {
out.resize(tracks.size());
Transform x;
for (size_t ti = 0; ti < tracks.size(); ++ti) {
tracks[ti].sample(t, x);
out[ti] = compose(x);
}
}
+45 -11
View File
@@ -35,18 +35,12 @@ bool File::Impl::build_fileinfo(FileInfo& out, LoadError* err) {
out.material_count = count_of("Materials");
out.model_count = count_of("Models");
// ── 顶层材质表(调试 / 备用查找)────────────────────────────────────
// ── 材质图:root Materials[] 先入表(索引 = 文件顺序),子材质 / 只经 mesh 可达的追加 ──
mats_out = &out.materials;
mat_refs.clear();
if (const TypeMember* mMa = find_member(rms, "Materials")) {
auto refs = read_array_of_refs(root_obj, *mMa);
const auto& mams = parse_type(mMa->ref_type);
const TypeMember* mMN = find_member(mams, "Name");
for (Ref r : refs) {
if (!r.valid()) continue;
Material mat;
if (mMN) mat.name = rd_str(member_slot(r, *mMN));
mat.diffuse_texture = material_texture_name(r, mMa->ref_type, 0);
out.materials.push_back(std::move(mat));
}
for (Ref r : refs) intern_material(r, mMa->ref_type);
}
// ── 骨架(T5)────────────────────────────────────────────────────
@@ -88,6 +82,7 @@ bool File::Impl::build_fileinfo(FileInfo& out, LoadError* err) {
out.skeletons = skels_cache;
// ── 网格(T6)────────────────────────────────────────────────────
std::vector<Ref> mesh_refs; // 与 out.meshes 平行(model.MeshBindings 关联用)
if (const TypeMember* mMe = find_member(rms, "Meshes")) {
auto refs = read_array_of_refs(root_obj, *mMe);
for (Ref r : refs) {
@@ -97,7 +92,45 @@ bool File::Impl::build_fileinfo(FileInfo& out, LoadError* err) {
fprintf(stderr, "[gr2] mesh ref (%d,%u) -> %s name=%s verts=%zu idx=%zu\n",
r.section, r.offset, ok ? "OK" : "FAIL", me.name.c_str(),
me.vertices.size(), me.indices.size());
if (ok) out.meshes.push_back(std::move(me));
if (ok) { out.meshes.push_back(std::move(me)); mesh_refs.push_back(r); }
}
}
// ── 模型:granny_model{String Name; Reference Skeleton; Transform InitialPlacement;
// RefToArray MeshBindings(model_mesh_binding{Reference Mesh}); VariantRef ExtendedData}
if (const TypeMember* mMo = find_member(rms, "Models")) {
auto refs = read_array_of_refs(root_obj, *mMo);
const auto& moms = parse_type(mMo->ref_type);
const TypeMember* mN = find_member(moms, "Name");
const TypeMember* mSk = find_member(moms, "Skeleton");
const TypeMember* mIP = find_member(moms, "InitialPlacement");
const TypeMember* mMB = find_member(moms, "MeshBindings");
auto same = [](Ref a, Ref b) { return a.section == b.section && a.offset == b.offset; };
for (Ref r : refs) {
if (!r.valid()) continue;
Model mo;
if (mN) mo.name = rd_str(member_slot(r, *mN));
if (mSk) {
Ref sref = read_reference(r, *mSk);
for (size_t k = 0; k < skel_refs.size(); ++k)
if (same(skel_refs[k], sref)) { mo.skeleton = int32_t(k); break; }
}
if (mIP) read_transform(member_slot(r, *mIP), mo.initial_placement);
Ref bb; uint32_t bc = 0, bs = 0;
if (mMB && read_ref_to_array(r, *mMB, bb, bc, bs) && bc && bs) {
const auto& bms = parse_type(mMB->ref_type);
const TypeMember* mMesh = find_member(bms, "Mesh");
for (uint32_t i = 0; i < bc && i < 4096; ++i) {
int32_t idx = -1;
if (mMesh) {
Ref me = read_reference({bb.section, bb.offset + i * bs}, *mMesh);
for (size_t k = 0; k < mesh_refs.size(); ++k)
if (same(mesh_refs[k], me)) { idx = int32_t(k); break; }
}
mo.mesh_bindings.push_back(idx);
}
}
out.models.push_back(std::move(mo));
}
}
@@ -113,6 +146,7 @@ bool File::Impl::build_fileinfo(FileInfo& out, LoadError* err) {
}
}
mats_out = nullptr;
return true;
}
+66
View File
@@ -134,6 +134,13 @@ bool File::Impl::extract_mesh(Ref mesh_obj, Ref mesh_type,
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;
@@ -220,11 +227,14 @@ bool File::Impl::extract_mesh(Ref mesh_obj, Ref mesh_type,
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);
}
}
}
@@ -269,6 +279,7 @@ bool File::Impl::extract_mesh(Ref mesh_obj, Ref mesh_type,
if (dangling == 0) break;
}
out.bone_bindings = std::move(best);
out.bone_binding_names = std::move(names);
}
}
return true;
@@ -310,4 +321,59 @@ std::string File::Impl::material_texture_name(Ref mat_obj, Ref mat_type, int dep
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
+29 -18
View File
@@ -57,32 +57,43 @@ void m3_mul(const float a[9], const float b[9], float r[9]) {
} // 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 {
void File::Impl::read_transform(Ref tf, Transform& out) const {
out = Transform{};
const uint8_t* p = at(tf, 68);
out = Mat4{};
out[0] = out[5] = out[10] = out[15] = 1.0f;
flags = 0;
if (!p) return;
std::memcpy(&out.flags, p, 4);
std::memcpy(out.position, p + 4, 12);
std::memcpy(out.orientation, p + 16, 16);
std::memcpy(out.scale_shear, p + 32, 36);
}
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);
// → 行主序 4x4,上 3x3 = transpose(U3),平移在 elem 12..14。BuildCompositeTransform4x4 语义。
Mat4 compose(const Transform& t) {
float q3[9];
quat_to_m3(ori, q3);
quat_to_m3(t.orientation, q3);
float u3[9];
if (flags & 0x4 /* HasScaleShear */) m3_mul(q3, ss, u3);
if (t.flags & 0x4 /* HasScaleShear */) m3_mul(q3, t.scale_shear, u3);
else std::memcpy(u3, q3, sizeof u3);
Mat4 out{};
// 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;
out[12] = t.position[0]; out[13] = t.position[1]; out[14] = t.position[2]; out[15] = 1;
return out;
}
void File::Impl::transform_to_composite(Ref tf, Mat4& out, uint32_t& flags) const {
Transform t;
read_transform(tf, t);
if (!at(tf, 68)) {
out = Mat4{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
flags = 0;
return;
}
out = compose(t);
flags = t.flags;
}
// skeleton: {String Name; RefToArray Bones(bone[]); int32 LODType; VariantRef ExtendedData}
@@ -118,9 +129,9 @@ bool File::Impl::extract_skeleton(Ref skel_obj, Ref skel_type, Skeleton& out) {
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;
read_transform(member_slot(b, *mLocal), bo.local);
bo.local_transform = compose(bo.local);
bo.lt_flags = bo.local.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{};
+9
View File
@@ -125,6 +125,15 @@ struct File::Impl {
// granny_transform (68B) → 行主序 4x4(BuildCompositeTransform4x4 语义)
void transform_to_composite(Ref tf, Mat4& out, uint32_t& flags) const;
void read_transform(Ref tf, Transform& out) const; // 68B → SRT 分量
// granny_material 图:按对象 Ref 去重,缺的追加到 mats_out(子材质 / 只经 mesh 可达的材质)。
// 返回 FileInfo::materials 索引;无效 / 递归太深 = -1。
int32_t intern_material(Ref mat_obj, Ref mat_type, int depth = 0);
std::vector<Material>* mats_out = nullptr;
std::vector<Ref> mat_refs; // 与 *mats_out 平行
// ExtendedData(VariantRef = {type ptr; object ptr})里取名为 name 的 Int32。
bool extended_i32(Ref variant_slot, const char* name, int32_t& out);
std::vector<Skeleton> skels_cache; // self-check 复用
std::vector<Ref> skel_refs; // 与 skels_cache 平行:各 skeleton 对象的 Ref(model 关联用)
+28
View File
@@ -2,6 +2,7 @@
#include <cmath>
#include <cstdio>
#include <vector>
int main() {
// ScaleShearCurve uses 9 values per control point. A quadratic span reads
@@ -24,5 +25,32 @@ int main() {
return 1;
}
}
// BoneTrack::sample fills SRT components and flags; sample_local = compose(sample).
gr2::BoneTrack track;
track.position.degree = 1;
track.position.dim = 3;
track.position.knots = {0.0f, 1.0f};
track.position.controls = {0.0f, 0.0f, 0.0f, 2.0f, 4.0f, 6.0f};
track.orientation.degree = 0;
track.orientation.dim = 4;
track.orientation.knots = {0.0f};
track.orientation.controls = {0.0f, 0.0f, 0.70710678f, 0.70710678f}; // 90 deg about Z
gr2::Transform x;
track.sample(0.5f, x);
if (x.flags != 3 || std::fabs(x.position[0] - 1.0f) > 1e-5f || std::fabs(x.position[2] - 3.0f) > 1e-5f) {
std::fprintf(stderr, "BoneTrack::sample flags=%u pos=%f,%f,%f\n", x.flags, x.position[0], x.position[1], x.position[2]);
return 1;
}
gr2::Animation anim;
anim.tracks.push_back(track);
std::vector<gr2::Mat4> local;
anim.sample_local(0.5f, local);
const gr2::Mat4 m = gr2::compose(x);
// Row-major composite: row 0 is the rotated X axis (0,1,0); translation in 12..14.
if (local.size() != 1 || local[0] != m || std::fabs(m[1] - 1.0f) > 1e-5f || std::fabs(m[12] - 1.0f) > 1e-5f) {
std::fprintf(stderr, "compose mismatch m[1]=%f m[12]=%f\n", m[1], m[12]);
return 1;
}
return 0;
}