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
+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);
}
}