M1+M2: gr2 -> Skeleton3D/ArrayMesh/Skin + libgr2-driven animation

- gr2_bridge: gr2 Mat4 (row-major) -> Godot Transform3D (4x4 transpose);
  build_skeleton (rest from local_transform, initial_placement folded into
  roots), build_skin (bind i -> bone i, pose = inverse_world), build_mesh
  (surface per gr2 mesh, ARRAY_BONES = skeleton idx via bone_bindings,
  rigid meshes bound to bone_bindings[0]).
- dxt: DDS DXT1/3/5 decoder ported from xrender-poc.
- Metin2Model: load_gr2 -> Skeleton3D + MeshInstance3D + StandardMaterial3D
  (runtime DDS albedo, face/body split by surface name). Z-up cm -> Y-up m
  conversion on the node transform (unit_scale, flip_z, flip_winding props).
- Metin2AnimPlayer: _process samples a (separate) anim .gr2 with
  gr2::sample_pose(model.skeleton, anim, t) -> set_bone_global_pose per bone;
  Godot GPU-skins via skin_matrix = global_pose(i) * bind_pose(i) == gr2
  deformer matrix. selfcheck() NaN-scans all anims.

Verified: warrior_cheongrin renders (75 bones, 5 surfaces, 3324 verts,
textured, upright). dance01 (dur 28.3s, 74 tracks) plays, 0 NaN over 25
samples, distinct poses at t=2/9/16/23. Screenshots in test/golden/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaHYEY9rwLWt21PULiYjeJ
This commit is contained in:
Claude
2026-08-29 09:23:29 +09:00
parent 67109990ec
commit 27e39993f7
121 changed files with 1058 additions and 57 deletions
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include <godot_cpp/classes/node3d.hpp>
#include <godot_cpp/variant/node_path.hpp>
#include <godot_cpp/variant/string.hpp>
#include <gr2/gr2.h>
#include <optional>
#include <vector>
namespace mtgodot {
class Metin2Model;
// Drives a Metin2Model's Skeleton3D each frame by sampling a (possibly
// separate) animation .gr2 with libgr2, then writing per-bone global poses.
//
// world[i] = gr2::sample_pose(model.skeleton, anim, t)
// skel.set_bone_global_pose(i, gr2_to_godot(world[i]))
//
// Godot's renderer then computes skin matrix = global_pose(i) * bind_pose(i)
// which equals gr2's deformer matrix (see gr2_bridge.h).
class Metin2AnimPlayer : public godot::Node3D {
GDCLASS(Metin2AnimPlayer, godot::Node3D)
public:
Metin2AnimPlayer();
~Metin2AnimPlayer() override;
void _ready() override;
void _process(double delta) override;
void set_anim_path(const godot::String &p);
godot::String get_anim_path() const { return anim_path; }
void set_model_path(const godot::NodePath &p);
godot::NodePath get_model_path() const { return model_path; }
void set_playing(bool v) { playing = v; }
bool get_playing() const { return playing; }
void set_time_scale(double s) { time_scale = s; }
double get_time_scale() const { return time_scale; }
void set_time(double t);
double get_time() const { return time; }
double get_duration() const { return duration; }
void reload();
godot::String get_info() const { return last_info; }
// NaN-scan every animation in anim_path across [0,dur]; returns a report string.
godot::String selfcheck(int samples = 24);
protected:
static void _bind_methods();
private:
godot::String anim_path;
godot::NodePath model_path;
bool playing = true;
double time_scale = 1.0;
double time = 0.0;
double duration = 0.0;
godot::String last_info;
std::optional<gr2::File> anim_file;
std::vector<gr2::Mat4> world_buf;
std::vector<gr2::Mat4> skin_buf;
Metin2Model *resolve_model() const;
void apply_pose(double t);
};
} // namespace mtgodot