Files
mtgodot-poc/libgr2/src/gr2_anim.cpp
T
shenleiandClaude Opus 5.5 ff0983d3c8 2V3-a: walk with the keyboard through the ported player chain
PythonPlayer, PythonPlayerInput{,Keyboard,Mouse}, PythonPlayerEventHandler,
PythonPlayerSkill, PythonPlayerModule and GameType are verbatim copies; the
`player` stub module and the pending PythonPlayer/GameType stand-ins are gone.
Unported callees (CPythonSkill, CItemData, CItemManager, CPythonItem, ...) are
pending stubs. 64-bit fixes: emotion icon handles use Py_BuildPointer, and
PyDict_Next takes Py_ssize_t.

The actor did not move because GrannyUpdateModelMatrix passed the model matrix
through. Walk/run motions use constant root-motion extraction
(AccumulationFlags=AccumulationExtracted, LoopTranslation e.g. (0,-300,0) for
the warrior run, same as the .msa Accumulation). libgr2 now reads both fields
from TrackGroups[0], and the runtime moves by LoopTranslation over each
control's local-clock span, blended by control weight. VDA extraction is not
implemented.

port.login_flow presses DIK_UP; the actor walks more than 100 cm, and the fake
server parses CG packets and sees CG_MOVE FUNC_MOVE, then FUNC_WAIT at the new
position. port.login_live against the real server walked 132 cm.

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

302 lines
14 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 T7a —— 曲线子类型分类(gr2fuzz 直方图依赖)。T7b(解码)见文末。
// 见 docs/reference/steps/M0-gr2-reader.md
// 规格:granny_track_group.cpp (TrackGroupType/TransformTrackType/Curve2Type) /
// granny_curve.cpp (CurveData*Type,均以 CurveDataHeaderType 开头) /
// granny_curve.h (curve_data_header{ u8 Format; u8 Degree })
#include "internal.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cmath>
#define ADBG(...) do { if (getenv("GR2_DEBUG")) fprintf(stderr, "[anim] " __VA_ARGS__); } while (0)
namespace gr2 {
using detail::Ref;
using detail::TypeMember;
// Metin2 = Granny 2.4:曲线是 OldCurveType(granny_curve.cpp):
// {Int32 Degree; RefToArray Knots(Real32[]); RefToArray Controls(Real32[])} —— 20B,无压缩变体。
// curve_obj 指向 transform_track 里 inline 的那个 curve 对象本体。
// 新版(2.6+)是 curve2{variant CurveData},本函数一并识别、打标签。
std::string File::Impl::classify_curve_obj(Ref curve_obj, Ref curve_type) {
if (!curve_obj.valid() || !curve_type.valid()) return "none";
const auto& ms = parse_type(curve_type);
if (ms.empty()) return "Unknown";
// 老格式:第一个成员是 "Degree"
if (ms[0].name == "Degree") {
const TypeMember* mD = &ms[0];
const TypeMember* mK = find_member(ms, "Knots");
const TypeMember* mC = find_member(ms, "Controls");
int32_t degree = rd_i32(member_slot(curve_obj, *mD));
uint32_t kn = mK ? uint32_t(std::max(0, rd_i32(member_slot(curve_obj, *mK)))) : 0;
uint32_t cn = mC ? uint32_t(std::max(0, rd_i32(member_slot(curve_obj, *mC)))) : 0;
uint32_t dim = kn ? cn / kn : 0;
char buf[64];
std::snprintf(buf, sizeof buf, "Old(d=%d,dim=%u)", degree, dim);
return buf;
}
// 新格式:curve2{variant CurveData}
if (ms[0].name == "CurveData") {
Ref type_ref = follow(member_slot(curve_obj, ms[0])); // variant.Type
if (!type_ref.valid()) return "Identity";
const auto& dm = parse_type(type_ref);
if (dm.empty()) return "Unknown";
const std::string& n = dm[0].name;
const char* pfx = "CurveDataHeader_";
return (n.rfind(pfx, 0) == 0) ? n.substr(std::strlen(pfx)) : n;
}
return "Unknown";
}
// 读一条 OldCurveType 曲线({Int32 Degree; RefToArray Knots; RefToArray Controls})到 Curve。
void File::Impl::read_old_curve(Ref curve_obj, Ref curve_type, Curve& out) {
if (!curve_obj.valid() || !curve_type.valid()) return;
const auto& ms = parse_type(curve_type);
if (ms.empty() || ms[0].name != "Degree") return; // 新格式暂不解
const TypeMember* mD = &ms[0];
const TypeMember* mK = find_member(ms, "Knots");
const TypeMember* mC = find_member(ms, "Controls");
if (!mK || !mC) return;
out.degree = rd_i32(member_slot(curve_obj, *mD));
auto read_farr = [&](const TypeMember& m, std::vector<float>& dst) {
Ref slot = member_slot(curve_obj, m);
int32_t cnt = rd_i32(slot);
if (cnt <= 0 || cnt > (1 << 24)) return;
Ref base = follow({slot.section, slot.offset + 4});
const uint8_t* p = at(base, size_t(cnt) * 4);
if (!p) return;
dst.resize(cnt);
for (int32_t k = 0; k < cnt; ++k) std::memcpy(&dst[k], p + size_t(k) * 4, 4);
};
read_farr(*mK, out.knots);
read_farr(*mC, out.controls);
out.dim = out.knots.empty() ? 0 : uint32_t(out.controls.size() / out.knots.size());
if (out.dim && out.knots.size() * out.dim != out.controls.size()) out.dim = 0; // 不一致 → 弃
// 有些资产的曲线控制点本身是 NaN(如 redthief_general/*_damage.gr2 的 finger track)。
// 不 sanitize 会把 NaN 灌进整条蒙皮链。→ 整条曲线弃(eval 返回空,调用方回退到 bind)。
for (float v : out.knots) if (!std::isfinite(v)) { out.dim = 0; return; }
for (float v : out.controls) if (!std::isfinite(v)) { out.dim = 0; return; }
}
// animation: {String Name; Real32 Duration; Real32 TimeStep; Real32 Oversampling;
// ArrayOfRefs TrackGroups; ...}
// track_group: {String Name; RefToArray VectorTracks; RefToArray TransformTracks; ...}
// transform_track: {String Name; Int32 Flags; Inline OrientationCurve(curve2);
// Inline PositionCurve(curve2); Inline ScaleShearCurve(curve2)}
bool File::Impl::extract_animation(Ref anim_obj, Ref anim_type, const Skeleton* skel, Animation& out) {
if (!anim_obj.valid() || !anim_type.valid()) return false;
const auto& ams = parse_type(anim_type);
if (const TypeMember* m = find_member(ams, "Name"))
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].AccumulationFlags (Int32) + LoopTranslation (Real32[3])
if (!groups.empty() && groups[0].valid()) {
if (const TypeMember* m = find_member(tgms, "AccumulationFlags"))
out.accumulation_flags = rd_i32(member_slot(groups[0], *m));
if (const TypeMember* m = find_member(tgms, "LoopTranslation")) {
Ref r = member_slot(groups[0], *m);
for (int i = 0; i < 3; ++i)
out.loop_translation[i] = rd_f32(Ref{r.section, r.offset + uint32_t(i) * 4u});
}
}
// 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;
const auto& ttms = parse_type(mTT->ref_type);
if (getenv("GR2_DEBUG")) {
fprintf(stderr, "[anim] transform_track type members=%zu:\n", ttms.size());
for (auto& m : ttms)
fprintf(stderr, "[anim] %-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);
}
const TypeMember* mName = find_member(ttms, "Name");
const TypeMember* mOri = find_member(ttms, "OrientationCurve");
const TypeMember* mPos = find_member(ttms, "PositionCurve");
const TypeMember* mScl = find_member(ttms, "ScaleShearCurve");
for (Ref g : groups) {
if (!g.valid()) continue;
Ref base; uint32_t count = 0, stride = 0;
if (!read_ref_to_array(g, *mTT, base, count, stride) || count == 0) continue;
for (uint32_t i = 0; i < count; ++i) {
Ref t = {base.section, base.offset + i * stride};
BoneTrack bt;
if (mName) bt.bone_name = rd_str(member_slot(t, *mName));
if (mPos) {
bt.pos_type = classify_curve_obj(member_slot(t, *mPos), mPos->ref_type);
read_old_curve(member_slot(t, *mPos), mPos->ref_type, bt.position);
}
if (mOri) {
bt.rot_type = classify_curve_obj(member_slot(t, *mOri), mOri->ref_type);
read_old_curve(member_slot(t, *mOri), mOri->ref_type, bt.orientation);
}
if (mScl) {
bt.scale_type = classify_curve_obj(member_slot(t, *mScl), mScl->ref_type);
read_old_curve(member_slot(t, *mScl), mScl->ref_type, bt.scale_shear);
}
if (skel) for (size_t k = 0; k < skel->bones.size(); ++k)
if (skel->bones[k].name == bt.bone_name) { bt.bone_index = int32_t(k); break; }
out.tracks.push_back(std::move(bt));
}
}
return true;
}
// ── T7b:曲线解码 ─────────────────────────────────────────────────────
// Metin2 曲线全是 OldCurveType(B 样条:Degree + Knots[] + Controls[knot*dim])。
// 求值照 granny_bspline.cpp SampleBSpline{0,1,2}xN + granny_bspline_inlines.h
// {Linear,Quadratic}Coefficients。degree 0=常量,1=线性,2=二次 B 样条。
// KnotIndex = "第一个 > t 的 knot"(granny CurveFindKnot),窗口越界按端点 clamp。
//
// dim==4(四元数):blend 之前对本 span 参与的 degree+1 个控制点做
// EnsureQuaternionContinuity(granny_math.cpp)—— 逐点若与前一点点积 < 0 则整体取反。
// 少了这一步,跨半球(q vs -q)的控制点线性混合会收缩到近零,归一后得到一个
// 合法但方向错约 180° 的旋转(表情动作里脖子/头会折叠塌陷)。
void Curve::eval(float t, float* out) const {
const uint32_t n = (uint32_t)knots.size();
if (n == 0 || dim == 0) return;
auto Kc = [&](int i) -> float {
i = i < 0 ? 0 : (i >= (int)n ? (int)n - 1 : i);
return knots[(uint32_t)i];
};
if (n == 1 || degree <= 0) { // 常量
for (uint32_t d = 0; d < dim; ++d) out[d] = controls[d];
return;
}
if (t <= knots[0]) t = knots[0];
if (t >= knots[n - 1]) t = knots[n - 1];
int i = (int)n - 1;
for (uint32_t k = 0; k < n; ++k) if (knots[k] > t) { i = (int)k; break; }
if (i < 1) i = 1;
const int span = (degree == 1) ? 2 : 3; // 本 span 的控制点数 = degree+1
const int first = (degree == 1) ? (i - 1) : (i - 2);
// 拷本 span 的控制点(越界按端点 clamp),四元数做半球连续化
// Metin2 的 ScaleShearCurve 是 9 维;旧的 3*4 缓冲区会在这里发生
// 栈溢出(degree=2 时写入 27 个 float),随后被 __stack_chk_fail 终止。
// 目前支持的曲线维度为 position=3、orientation=4、scale/shear=9。
if (dim > 9) return;
float cp[3 * 9];
for (int s = 0; s < span; ++s) {
int ci = first + s;
ci = ci < 0 ? 0 : (ci >= (int)n ? (int)n - 1 : ci);
for (uint32_t d = 0; d < dim; ++d) cp[s * dim + d] = controls[(uint32_t)ci * dim + d];
}
if (dim == 4) {
for (int s = 1; s < span; ++s) {
const float* p = &cp[(s - 1) * 4];
float* q = &cp[s * 4];
if (p[0]*q[0] + p[1]*q[1] + p[2]*q[2] + p[3]*q[3] < 0.0f)
for (int d = 0; d < 4; ++d) q[d] = -q[d];
}
}
if (degree == 1) {
float denom = Kc(i) - Kc(i - 1);
float c0 = denom != 0.0f ? (t - Kc(i - 1)) / denom : 0.0f;
c0 = c0 < 0 ? 0 : (c0 > 1 ? 1 : c0);
float c1 = 1.0f - c0;
for (uint32_t d = 0; d < dim; ++d)
out[d] = c1 * cp[0 * dim + d] + c0 * cp[1 * dim + d];
} else { // degree >= 2 → 二次(Metin2 无 degree 3)
float ti_2 = Kc(i - 2), ti_1 = Kc(i - 1), ti = Kc(i), ti1 = Kc(i + 1);
auto fdiv = [](float a, float b) { return b != 0.0f ? a / b : 0.0f; };
float L0 = fdiv(t - ti_1, ti - ti_1);
float L1_1 = fdiv(t - ti_2, ti - ti_2);
float L1_2 = fdiv(t - ti_1, ti1 - ti_1);
float tmp = (L1_1 + L0) - L0 * L1_1;
float c0 = L0 * L1_2; // ci
float c1 = tmp - c0; // ci_1
float c2 = 1.0f - tmp; // ci_2
for (uint32_t d = 0; d < dim; ++d)
out[d] = c2 * cp[0 * dim + d] + c1 * cp[1 * dim + d] + c0 * cp[2 * dim + d];
}
if (dim == 4) { // 四元数:归一
float m = std::sqrt(out[0]*out[0] + out[1]*out[1] + out[2]*out[2] + out[3]*out[3]);
if (m > 1e-12f) for (int d = 0; d < 4; ++d) out[d] /= m;
}
}
// 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;
}
}
}
// 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);
}
}
} // namespace gr2