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
+182
View File
@@ -0,0 +1,182 @@
#include "gr2_bridge.h"
#include <godot_cpp/classes/array_mesh.hpp>
#include <godot_cpp/classes/mesh.hpp>
#include <godot_cpp/classes/skeleton3d.hpp>
#include <godot_cpp/classes/skin.hpp>
#include <godot_cpp/core/math.hpp>
#include <godot_cpp/core/memory.hpp>
#include <godot_cpp/variant/packed_float32_array.hpp>
#include <godot_cpp/variant/packed_int32_array.hpp>
#include <godot_cpp/variant/packed_vector2_array.hpp>
#include <godot_cpp/variant/packed_vector3_array.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <vector>
using namespace godot;
namespace mtgodot {
Transform3D gr2_to_godot(const gr2::Mat4 &m) {
// gr2 row-major, row-vector: basis columns are (m0,m1,m2),(m4,m5,m6),(m8,m9,m10);
// translation is the 4th row (m12,m13,m14).
Basis b(
Vector3(m[0], m[1], m[2]),
Vector3(m[4], m[5], m[6]),
Vector3(m[8], m[9], m[10]));
return Transform3D(b, Vector3(m[12], m[13], m[14]));
}
Transform3D make_conv(float unit_scale, bool flip_z) {
// Z-up -> Y-up: rotate -90 deg about X. Then uniform scale. Optional Z flip.
Basis b;
b = b.rotated(Vector3(1, 0, 0), Math::deg_to_rad(-90.0));
b = b.scaled(Vector3(unit_scale, unit_scale, flip_z ? -unit_scale : unit_scale));
return Transform3D(b, Vector3());
}
Skeleton3D *build_skeleton(const gr2::Skeleton &sk) {
if (sk.bones.empty()) {
return nullptr;
}
Skeleton3D *skel = memnew(Skeleton3D);
skel->set_name("Skeleton3D");
const Transform3D ip = gr2_to_godot(sk.initial_placement);
for (size_t i = 0; i < sk.bones.size(); ++i) {
skel->add_bone(String(sk.bones[i].name.c_str()));
}
for (size_t i = 0; i < sk.bones.size(); ++i) {
const gr2::Bone &bn = sk.bones[i];
skel->set_bone_parent((int)i, bn.parent);
Transform3D rest = gr2_to_godot(bn.local_transform);
if (bn.parent < 0) {
rest = ip * rest; // fold initial_placement into root bones
}
skel->set_bone_rest((int)i, rest);
skel->reset_bone_pose((int)i);
}
return skel;
}
Ref<Skin> build_skin(const gr2::Skeleton &sk) {
Ref<Skin> skin;
skin.instantiate();
for (size_t i = 0; i < sk.bones.size(); ++i) {
skin->add_bind((int)i, gr2_to_godot(sk.bones[i].inverse_world));
skin->set_bind_name((int)i, StringName(sk.bones[i].name.c_str()));
}
return skin;
}
Ref<ArrayMesh> build_mesh(const gr2::FileInfo &fi, bool flip_winding, AABB &out_bounds) {
Ref<ArrayMesh> am;
am.instantiate();
am->set_name("ArrayMesh");
bool have_bounds = false;
for (size_t mi = 0; mi < fi.meshes.size(); ++mi) {
const gr2::Mesh &m = fi.meshes[mi];
if (m.vertices.empty() || m.indices.empty()) {
continue;
}
const int vcount = (int)m.vertices.size();
PackedVector3Array pos;
PackedVector3Array nrm;
PackedVector2Array uv;
PackedInt32Array bones;
PackedFloat32Array weights;
pos.resize(vcount);
nrm.resize(vcount);
uv.resize(vcount);
bones.resize(vcount * 4);
weights.resize(vcount * 4);
Vector3 *pos_w = pos.ptrw();
Vector3 *nrm_w = nrm.ptrw();
Vector2 *uv_w = uv.ptrw();
int32_t *bn_w = bones.ptrw();
float *wt_w = weights.ptrw();
auto slot_to_bone = [&](int slot) -> int {
if (slot < 0 || slot >= (int)m.bone_bindings.size()) {
return 0;
}
int b = m.bone_bindings[slot];
return (b < 0) ? 0 : b;
};
const int rigid_bone = m.rigid ? slot_to_bone(m.bone_bindings.empty() ? -1 : 0) : 0;
for (int i = 0; i < vcount; ++i) {
const gr2::Vertex &v = m.vertices[i];
pos_w[i] = Vector3(v.pos[0], v.pos[1], v.pos[2]);
nrm_w[i] = Vector3(v.normal[0], v.normal[1], v.normal[2]);
uv_w[i] = Vector2(v.uv0[0], v.uv0[1]);
if (!have_bounds) {
out_bounds.position = pos_w[i];
out_bounds.size = Vector3();
have_bounds = true;
} else {
out_bounds = out_bounds.expand(pos_w[i]);
}
if (m.rigid) {
bn_w[i * 4 + 0] = rigid_bone;
bn_w[i * 4 + 1] = bn_w[i * 4 + 2] = bn_w[i * 4 + 3] = 0;
wt_w[i * 4 + 0] = 1.0f;
wt_w[i * 4 + 1] = wt_w[i * 4 + 2] = wt_w[i * 4 + 3] = 0.0f;
continue;
}
float wsum = 0.0f;
for (int k = 0; k < 4; ++k) {
wsum += v.bone_weight[k];
}
for (int k = 0; k < 4; ++k) {
bn_w[i * 4 + k] = slot_to_bone(v.bone_index[k]);
wt_w[i * 4 + k] = (wsum > 0.0f) ? (v.bone_weight[k] / (float)wsum) : (k == 0 ? 1.0f : 0.0f);
}
}
PackedInt32Array idx;
idx.resize((int)m.indices.size());
int32_t *idx_w = idx.ptrw();
if (flip_winding) {
for (size_t t = 0; t + 2 < m.indices.size(); t += 3) {
idx_w[t + 0] = (int)m.indices[t + 0];
idx_w[t + 1] = (int)m.indices[t + 2];
idx_w[t + 2] = (int)m.indices[t + 1];
}
} else {
for (size_t t = 0; t < m.indices.size(); ++t) {
idx_w[t] = (int)m.indices[t];
}
}
Array arrays;
arrays.resize(Mesh::ARRAY_MAX);
arrays[Mesh::ARRAY_VERTEX] = pos;
arrays[Mesh::ARRAY_NORMAL] = nrm;
arrays[Mesh::ARRAY_TEX_UV] = uv;
arrays[Mesh::ARRAY_BONES] = bones;
arrays[Mesh::ARRAY_WEIGHTS] = weights;
arrays[Mesh::ARRAY_INDEX] = idx;
am->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrays);
am->surface_set_name(am->get_surface_count() - 1,
m.name.empty() ? String("surf_") + itos((int)mi) : String(m.name.c_str()));
}
if (!have_bounds) {
out_bounds = AABB();
}
return am;
}
} // namespace mtgodot