// 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 #include #include #include #include #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& 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& 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