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:
@@ -0,0 +1,160 @@
|
||||
// engine/dxt —— DDS(DXT1/3/5) + 未压缩 BGRA8 → RGBA8 软解。M1 T3。
|
||||
#include "dxt.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace mtgodot {
|
||||
namespace {
|
||||
|
||||
inline uint32_t rd_le32(const uint8_t* p) {
|
||||
return uint32_t(p[0]) | (uint32_t(p[1]) << 8) | (uint32_t(p[2]) << 16) | (uint32_t(p[3]) << 24);
|
||||
}
|
||||
|
||||
// 5:6:5 → r,g,b (0..255)
|
||||
inline void unpack565(uint16_t c, int& r, int& g, int& b) {
|
||||
r = ((c >> 11) & 0x1f); r = (r << 3) | (r >> 2);
|
||||
g = ((c >> 5) & 0x3f); g = (g << 2) | (g >> 4);
|
||||
b = (c & 0x1f); b = (b << 3) | (b >> 2);
|
||||
}
|
||||
|
||||
// 解一个 DXT1 颜色块(8B)→ 16 像素 RGB(不写 alpha;dxt1_alpha=true 时按 1-bit alpha 写)
|
||||
void decode_color_block(const uint8_t* blk, uint8_t out[16][4], bool dxt1_alpha) {
|
||||
uint16_t c0 = uint16_t(blk[0] | (blk[1] << 8));
|
||||
uint16_t c1 = uint16_t(blk[2] | (blk[3] << 8));
|
||||
int r[4], g[4], b[4], a[4] = {255, 255, 255, 255};
|
||||
unpack565(c0, r[0], g[0], b[0]);
|
||||
unpack565(c1, r[1], g[1], b[1]);
|
||||
if (c0 > c1 || !dxt1_alpha) {
|
||||
r[2] = (2 * r[0] + r[1]) / 3; g[2] = (2 * g[0] + g[1]) / 3; b[2] = (2 * b[0] + b[1]) / 3;
|
||||
r[3] = (r[0] + 2 * r[1]) / 3; g[3] = (g[0] + 2 * g[1]) / 3; b[3] = (b[0] + 2 * b[1]) / 3;
|
||||
} else {
|
||||
r[2] = (r[0] + r[1]) / 2; g[2] = (g[0] + g[1]) / 2; b[2] = (b[0] + b[1]) / 2;
|
||||
r[3] = g[3] = b[3] = 0; a[3] = 0; // 透明
|
||||
}
|
||||
uint32_t bits = rd_le32(blk + 4);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
int idx = (bits >> (i * 2)) & 3;
|
||||
out[i][0] = uint8_t(r[idx]); out[i][1] = uint8_t(g[idx]);
|
||||
out[i][2] = uint8_t(b[idx]); out[i][3] = uint8_t(a[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
// DXT3:alpha 块(8B)= 16 个 4-bit alpha,直接展开
|
||||
void decode_dxt3_alpha(const uint8_t* blk, uint8_t out[16][4]) {
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
int a0 = blk[i] & 0x0f, a1 = (blk[i] >> 4) & 0x0f;
|
||||
out[i * 2 + 0][3] = uint8_t(a0 * 17); // 0..15 → 0..255
|
||||
out[i * 2 + 1][3] = uint8_t(a1 * 17);
|
||||
}
|
||||
}
|
||||
|
||||
// DXT5:alpha 块(8B)= 2 端点 + 16×3-bit 索引
|
||||
void decode_dxt5_alpha(const uint8_t* blk, uint8_t out[16][4]) {
|
||||
int a0 = blk[0], a1 = blk[1];
|
||||
int a[8];
|
||||
a[0] = a0; a[1] = a1;
|
||||
if (a0 > a1) {
|
||||
for (int i = 1; i < 7; ++i) a[i + 1] = ((7 - i) * a0 + i * a1) / 7;
|
||||
} else {
|
||||
for (int i = 1; i < 5; ++i) a[i + 1] = ((5 - i) * a0 + i * a1) / 5;
|
||||
a[6] = 0; a[7] = 255;
|
||||
}
|
||||
uint64_t bits = 0;
|
||||
for (int i = 0; i < 6; ++i) bits |= uint64_t(blk[2 + i]) << (8 * i);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
int idx = int((bits >> (i * 3)) & 7);
|
||||
out[i][3] = uint8_t(a[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
enum Fmt { F_NONE, F_DXT1, F_DXT3, F_DXT5, F_BGRA8 };
|
||||
|
||||
Image decode(const uint8_t* d, size_t len) {
|
||||
Image img;
|
||||
if (len < 128 || std::memcmp(d, "DDS ", 4) != 0) return img;
|
||||
uint32_t hsize = rd_le32(d + 4);
|
||||
if (hsize != 124) return img;
|
||||
uint32_t h = rd_le32(d + 12);
|
||||
uint32_t w = rd_le32(d + 16);
|
||||
uint32_t pf_flags = rd_le32(d + 80);
|
||||
const uint8_t* fourcc = d + 84;
|
||||
uint32_t rgb_bitcount = rd_le32(d + 88);
|
||||
|
||||
Fmt fmt = F_NONE;
|
||||
if (pf_flags & 0x4) { // DDPF_FOURCC
|
||||
if (!std::memcmp(fourcc, "DXT1", 4)) fmt = F_DXT1;
|
||||
else if (!std::memcmp(fourcc, "DXT3", 4)) fmt = F_DXT3;
|
||||
else if (!std::memcmp(fourcc, "DXT5", 4)) fmt = F_DXT5;
|
||||
} else if ((pf_flags & 0x40) && rgb_bitcount == 32) { // DDPF_RGB, 32bpp
|
||||
fmt = F_BGRA8; // Metin2 的非压缩 DDS 一般是 B8G8R8A8
|
||||
}
|
||||
if (fmt == F_NONE || w == 0 || h == 0 || w > 8192 || h > 8192) return img;
|
||||
|
||||
const uint8_t* src = d + 128;
|
||||
size_t avail = len - 128;
|
||||
img.w = uint16_t(w);
|
||||
img.h = uint16_t(h);
|
||||
img.rgba.assign(size_t(w) * h * 4, 0);
|
||||
|
||||
if (fmt == F_BGRA8) {
|
||||
if (avail < size_t(w) * h * 4) { img = Image{}; return img; }
|
||||
for (size_t i = 0; i < size_t(w) * h; ++i) {
|
||||
img.rgba[i * 4 + 0] = src[i * 4 + 2];
|
||||
img.rgba[i * 4 + 1] = src[i * 4 + 1];
|
||||
img.rgba[i * 4 + 2] = src[i * 4 + 0];
|
||||
img.rgba[i * 4 + 3] = src[i * 4 + 3];
|
||||
}
|
||||
img.format = "BGRA8";
|
||||
return img;
|
||||
}
|
||||
|
||||
const int block_bytes = (fmt == F_DXT1) ? 8 : 16;
|
||||
const size_t bx = (w + 3) / 4, by = (h + 3) / 4;
|
||||
if (avail < bx * by * block_bytes) { img = Image{}; return img; }
|
||||
|
||||
for (size_t byi = 0; byi < by; ++byi) {
|
||||
for (size_t bxi = 0; bxi < bx; ++bxi) {
|
||||
const uint8_t* blk = src + (byi * bx + bxi) * block_bytes;
|
||||
uint8_t px[16][4];
|
||||
if (fmt == F_DXT1) {
|
||||
decode_color_block(blk, px, /*dxt1_alpha=*/true);
|
||||
} else {
|
||||
decode_color_block(blk + 8, px, /*dxt1_alpha=*/false);
|
||||
if (fmt == F_DXT3) decode_dxt3_alpha(blk, px);
|
||||
else decode_dxt5_alpha(blk, px);
|
||||
}
|
||||
for (int py = 0; py < 4; ++py) {
|
||||
size_t y = byi * 4 + py;
|
||||
if (y >= h) break;
|
||||
for (int pxx = 0; pxx < 4; ++pxx) {
|
||||
size_t x = bxi * 4 + pxx;
|
||||
if (x >= w) break;
|
||||
uint8_t* o = &img.rgba[(y * w + x) * 4];
|
||||
const uint8_t* s = px[py * 4 + pxx];
|
||||
o[0] = s[0]; o[1] = s[1]; o[2] = s[2]; o[3] = s[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
img.format = (fmt == F_DXT1) ? "DXT1" : (fmt == F_DXT3) ? "DXT3" : "DXT5";
|
||||
return img;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Image load_dds(const uint8_t* bytes, size_t len) { return decode(bytes, len); }
|
||||
|
||||
Image load_dds_path(const char* path) {
|
||||
FILE* f = std::fopen(path, "rb");
|
||||
if (!f) return {};
|
||||
std::fseek(f, 0, SEEK_END);
|
||||
long n = std::ftell(f);
|
||||
std::fseek(f, 0, SEEK_SET);
|
||||
std::vector<uint8_t> buf(n > 0 ? size_t(n) : 0);
|
||||
size_t rd = buf.empty() ? 0 : std::fread(buf.data(), 1, buf.size(), f);
|
||||
std::fclose(f);
|
||||
if (rd != buf.size()) return {};
|
||||
return decode(buf.data(), buf.size());
|
||||
}
|
||||
|
||||
} // namespace mtgodot
|
||||
@@ -0,0 +1,23 @@
|
||||
// dxt.{h,cpp} — ported verbatim from xrender-poc/engine/dxt.{h,cpp}
|
||||
// (namespace engine -> mtgodot). DDS DXT1/3/5 + BGRA8 -> RGBA8, level 0.
|
||||
// engine/dxt —— DDS(DXT1/3/5) → RGBA8 软解。见 docs/steps/M1-static-render.md T3
|
||||
// 自己写 ~150 行绕开 reuse/EterImageLib 的 windows.h 依赖(M1 风险表允许)。
|
||||
// 对拍时两侧都喂软解 RGBA,不被 GPU S3TC 的 bit 级差异污染。
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace mtgodot {
|
||||
|
||||
struct Image {
|
||||
uint16_t w = 0, h = 0;
|
||||
std::vector<uint8_t> rgba; // w*h*4,level 0
|
||||
const char* format = ""; // "DXT1" / "DXT3" / "DXT5" / "BGRA8" / ""
|
||||
bool ok() const { return w && h && rgba.size() == size_t(w) * h * 4; }
|
||||
};
|
||||
|
||||
// 读整个 .dds 文件字节,解 level 0 到 RGBA8。失败返回 !ok()。
|
||||
Image load_dds(const uint8_t* bytes, size_t len);
|
||||
Image load_dds_path(const char* path);
|
||||
|
||||
} // namespace mtgodot
|
||||
@@ -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
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
// gr2_bridge — libgr2 POD views -> Godot scene objects.
|
||||
//
|
||||
// Coordinate handling (the one place it lives; see docs/GODOT-POC-PLAN.md §02):
|
||||
// - gr2 Mat4 is row-major, row-vector math, translation in elements 12..14.
|
||||
// - A plain 4x4 transpose maps it to a Godot Transform3D (column-vector,
|
||||
// translation in .origin). gr2_to_godot() does exactly that.
|
||||
// - Z-up(cm) -> Y-up(m) and optional handedness flip are NOT applied here;
|
||||
// they go on the Metin2Model node transform (make_conv()).
|
||||
|
||||
#include <godot_cpp/classes/ref.hpp>
|
||||
#include <godot_cpp/variant/aabb.hpp>
|
||||
#include <godot_cpp/variant/transform3d.hpp>
|
||||
|
||||
#include <gr2/gr2.h>
|
||||
|
||||
namespace godot {
|
||||
class Skeleton3D;
|
||||
class Skin;
|
||||
class ArrayMesh;
|
||||
} // namespace godot
|
||||
|
||||
namespace mtgodot {
|
||||
|
||||
// 4x4 transpose: gr2 row-major/row-vector -> Godot Transform3D.
|
||||
godot::Transform3D gr2_to_godot(const gr2::Mat4 &m);
|
||||
|
||||
// Z-up cm -> Y-up m (+ optional Z flip for LH->RH content).
|
||||
godot::Transform3D make_conv(float unit_scale, bool flip_z);
|
||||
|
||||
// Build a Skeleton3D (rest pose from bone local transforms; initial_placement
|
||||
// folded into root bones). Returns nullptr if the skeleton has no bones.
|
||||
godot::Skeleton3D *build_skeleton(const gr2::Skeleton &sk);
|
||||
|
||||
// Skin whose bind list is parallel to the skeleton bones:
|
||||
// bind i -> bone i, pose = gr2_to_godot(bone[i].inverse_world)
|
||||
godot::Ref<godot::Skin> build_skin(const gr2::Skeleton &sk);
|
||||
|
||||
// One ArrayMesh with a surface per gr2 Mesh that has geometry.
|
||||
// ARRAY_BONES values are skeleton bone indices (mesh slot -> bone via
|
||||
// mesh.bone_bindings). Rigid meshes are bound rigidly to bone_bindings[0].
|
||||
// Fills out_bounds with the untransformed vertex AABB.
|
||||
godot::Ref<godot::ArrayMesh> build_mesh(const gr2::FileInfo &fi, bool flip_winding,
|
||||
godot::AABB &out_bounds);
|
||||
|
||||
} // namespace mtgodot
|
||||
@@ -0,0 +1,176 @@
|
||||
#include "metin2_anim.h"
|
||||
|
||||
#include "gr2_bridge.h"
|
||||
#include "metin2_model.h"
|
||||
|
||||
#include <godot_cpp/classes/skeleton3d.hpp>
|
||||
#include <godot_cpp/core/class_db.hpp>
|
||||
#include <godot_cpp/variant/utility_functions.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
using namespace godot;
|
||||
|
||||
namespace mtgodot {
|
||||
|
||||
Metin2AnimPlayer::Metin2AnimPlayer() {}
|
||||
Metin2AnimPlayer::~Metin2AnimPlayer() = default;
|
||||
|
||||
void Metin2AnimPlayer::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("set_anim_path", "path"), &Metin2AnimPlayer::set_anim_path);
|
||||
ClassDB::bind_method(D_METHOD("get_anim_path"), &Metin2AnimPlayer::get_anim_path);
|
||||
ClassDB::bind_method(D_METHOD("set_model_path", "path"), &Metin2AnimPlayer::set_model_path);
|
||||
ClassDB::bind_method(D_METHOD("get_model_path"), &Metin2AnimPlayer::get_model_path);
|
||||
ClassDB::bind_method(D_METHOD("set_playing", "v"), &Metin2AnimPlayer::set_playing);
|
||||
ClassDB::bind_method(D_METHOD("get_playing"), &Metin2AnimPlayer::get_playing);
|
||||
ClassDB::bind_method(D_METHOD("set_time_scale", "s"), &Metin2AnimPlayer::set_time_scale);
|
||||
ClassDB::bind_method(D_METHOD("get_time_scale"), &Metin2AnimPlayer::get_time_scale);
|
||||
ClassDB::bind_method(D_METHOD("set_time", "t"), &Metin2AnimPlayer::set_time);
|
||||
ClassDB::bind_method(D_METHOD("get_time"), &Metin2AnimPlayer::get_time);
|
||||
ClassDB::bind_method(D_METHOD("get_duration"), &Metin2AnimPlayer::get_duration);
|
||||
ClassDB::bind_method(D_METHOD("reload"), &Metin2AnimPlayer::reload);
|
||||
ClassDB::bind_method(D_METHOD("get_info"), &Metin2AnimPlayer::get_info);
|
||||
ClassDB::bind_method(D_METHOD("selfcheck", "samples"), &Metin2AnimPlayer::selfcheck, DEFVAL(24));
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::STRING, "anim_path", PROPERTY_HINT_GLOBAL_FILE, "*.gr2"),
|
||||
"set_anim_path", "get_anim_path");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "model_path", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Node3D"),
|
||||
"set_model_path", "get_model_path");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "playing"), "set_playing", "get_playing");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "time_scale", PROPERTY_HINT_RANGE, "0,4,0.01"),
|
||||
"set_time_scale", "get_time_scale");
|
||||
}
|
||||
|
||||
void Metin2AnimPlayer::set_anim_path(const String &p) {
|
||||
anim_path = p;
|
||||
if (is_inside_tree()) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
void Metin2AnimPlayer::set_model_path(const NodePath &p) {
|
||||
model_path = p;
|
||||
}
|
||||
void Metin2AnimPlayer::set_time(double t) {
|
||||
time = t;
|
||||
if (is_inside_tree()) {
|
||||
apply_pose(time);
|
||||
}
|
||||
}
|
||||
|
||||
Metin2Model *Metin2AnimPlayer::resolve_model() const {
|
||||
if (model_path.is_empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
Node *n = get_node_or_null(model_path);
|
||||
return Object::cast_to<Metin2Model>(n);
|
||||
}
|
||||
|
||||
void Metin2AnimPlayer::_ready() {
|
||||
set_process(true);
|
||||
reload();
|
||||
}
|
||||
|
||||
void Metin2AnimPlayer::reload() {
|
||||
anim_file = std::nullopt;
|
||||
duration = 0.0;
|
||||
time = 0.0;
|
||||
last_info = "";
|
||||
if (anim_path.is_empty()) {
|
||||
return;
|
||||
}
|
||||
gr2::LoadError err;
|
||||
const std::string p(anim_path.utf8().get_data());
|
||||
auto f = gr2::File::load_path(p, &err);
|
||||
if (!f) {
|
||||
UtilityFunctions::push_error(String("[Metin2AnimPlayer] anim load failed [") +
|
||||
String(err.stage.c_str()) + "]: " + String(err.message.c_str()));
|
||||
return;
|
||||
}
|
||||
const gr2::FileInfo &fi = f->file_info();
|
||||
if (fi.animations.empty()) {
|
||||
UtilityFunctions::push_error("[Metin2AnimPlayer] no animations in " + anim_path);
|
||||
return;
|
||||
}
|
||||
anim_file = std::move(f);
|
||||
const gr2::Animation &an = anim_file->file_info().animations[0];
|
||||
duration = an.duration;
|
||||
last_info = vformat("anim '%s' dur=%.3f tracks=%d anims=%d",
|
||||
String(an.name.c_str()), duration, (int)an.tracks.size(),
|
||||
(int)anim_file->file_info().animations.size());
|
||||
UtilityFunctions::print("[Metin2AnimPlayer] ", last_info);
|
||||
apply_pose(0.0);
|
||||
}
|
||||
|
||||
void Metin2AnimPlayer::apply_pose(double t) {
|
||||
if (!anim_file) {
|
||||
return;
|
||||
}
|
||||
Metin2Model *model = resolve_model();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
const gr2::Skeleton *sk = model->gr2_skeleton();
|
||||
Skeleton3D *skel = model->skeleton_node();
|
||||
if (!sk || !skel) {
|
||||
return;
|
||||
}
|
||||
const gr2::Animation &an = anim_file->file_info().animations[0];
|
||||
|
||||
double tt = t;
|
||||
if (duration > 0.0) {
|
||||
tt = std::fmod(t, duration);
|
||||
if (tt < 0.0) {
|
||||
tt += duration;
|
||||
}
|
||||
}
|
||||
|
||||
gr2::sample_pose(*sk, an, (float)tt, world_buf, skin_buf);
|
||||
|
||||
const int n = (int)std::min<size_t>(world_buf.size(), (size_t)skel->get_bone_count());
|
||||
for (int i = 0; i < n; ++i) {
|
||||
skel->set_bone_global_pose(i, gr2_to_godot(world_buf[i]));
|
||||
}
|
||||
}
|
||||
|
||||
void Metin2AnimPlayer::_process(double delta) {
|
||||
if (!playing || !anim_file) {
|
||||
return;
|
||||
}
|
||||
time += delta * time_scale;
|
||||
apply_pose(time);
|
||||
}
|
||||
|
||||
String Metin2AnimPlayer::selfcheck(int samples) {
|
||||
if (!anim_file) {
|
||||
return "no anim loaded";
|
||||
}
|
||||
Metin2Model *model = resolve_model();
|
||||
const gr2::Skeleton *sk = model ? model->gr2_skeleton() : nullptr;
|
||||
if (!sk) {
|
||||
return "no model/skeleton";
|
||||
}
|
||||
const auto &anims = anim_file->file_info().animations;
|
||||
int total = 0;
|
||||
int nan_hits = 0;
|
||||
std::vector<gr2::Mat4> w, s;
|
||||
for (const auto &an : anims) {
|
||||
double dur = an.duration > 0.0 ? an.duration : 1.0;
|
||||
for (int k = 0; k <= samples; ++k) {
|
||||
float tt = (float)(dur * k / samples);
|
||||
gr2::sample_pose(*sk, an, tt, w, s);
|
||||
for (const auto &mtx : w) {
|
||||
for (float v : mtx) {
|
||||
++total;
|
||||
if (std::isnan(v) || std::isinf(v)) {
|
||||
++nan_hits;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return vformat("selfcheck: anims=%d samples/anim=%d floats=%d NaN/Inf=%d",
|
||||
(int)anims.size(), samples + 1, total, nan_hits);
|
||||
}
|
||||
|
||||
} // namespace mtgodot
|
||||
@@ -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
|
||||
+226
-12
@@ -1,39 +1,253 @@
|
||||
#include "metin2_model.h"
|
||||
|
||||
#include "dxt.h"
|
||||
#include "gr2_bridge.h"
|
||||
|
||||
#include <godot_cpp/classes/array_mesh.hpp>
|
||||
#include <godot_cpp/classes/base_material3d.hpp>
|
||||
#include <godot_cpp/classes/file_access.hpp>
|
||||
#include <godot_cpp/classes/image.hpp>
|
||||
#include <godot_cpp/classes/image_texture.hpp>
|
||||
#include <godot_cpp/classes/mesh_instance3d.hpp>
|
||||
#include <godot_cpp/classes/skeleton3d.hpp>
|
||||
#include <godot_cpp/classes/skin.hpp>
|
||||
#include <godot_cpp/classes/standard_material3d.hpp>
|
||||
#include <godot_cpp/core/class_db.hpp>
|
||||
#include <godot_cpp/variant/packed_byte_array.hpp>
|
||||
#include <godot_cpp/variant/utility_functions.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <gr2/gr2.h> // libgr2 (xrender-poc)
|
||||
|
||||
using namespace godot;
|
||||
|
||||
namespace mtgodot {
|
||||
|
||||
Metin2Model::Metin2Model() = default;
|
||||
Metin2Model::Metin2Model() {
|
||||
file = std::make_shared<std::optional<gr2::File>>();
|
||||
}
|
||||
Metin2Model::~Metin2Model() = default;
|
||||
|
||||
void Metin2Model::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("libgr2_info"), &Metin2Model::libgr2_info);
|
||||
ClassDB::bind_method(D_METHOD("set_gr2_path", "path"), &Metin2Model::set_gr2_path);
|
||||
ClassDB::bind_method(D_METHOD("get_gr2_path"), &Metin2Model::get_gr2_path);
|
||||
ClassDB::bind_method(D_METHOD("set_texture_dir", "path"), &Metin2Model::set_texture_dir);
|
||||
ClassDB::bind_method(D_METHOD("get_texture_dir"), &Metin2Model::get_texture_dir);
|
||||
ClassDB::bind_method(D_METHOD("set_unit_scale", "s"), &Metin2Model::set_unit_scale);
|
||||
ClassDB::bind_method(D_METHOD("get_unit_scale"), &Metin2Model::get_unit_scale);
|
||||
ClassDB::bind_method(D_METHOD("set_flip_z", "v"), &Metin2Model::set_flip_z);
|
||||
ClassDB::bind_method(D_METHOD("get_flip_z"), &Metin2Model::get_flip_z);
|
||||
ClassDB::bind_method(D_METHOD("set_flip_winding", "v"), &Metin2Model::set_flip_winding);
|
||||
ClassDB::bind_method(D_METHOD("get_flip_winding"), &Metin2Model::get_flip_winding);
|
||||
ClassDB::bind_method(D_METHOD("reload"), &Metin2Model::reload);
|
||||
ClassDB::bind_method(D_METHOD("get_info"), &Metin2Model::get_info);
|
||||
ClassDB::bind_method(D_METHOD("probe_gr2", "path"), &Metin2Model::probe_gr2);
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::STRING, "gr2_path", PROPERTY_HINT_GLOBAL_FILE, "*.gr2"),
|
||||
"set_gr2_path", "get_gr2_path");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::STRING, "texture_dir", PROPERTY_HINT_GLOBAL_DIR),
|
||||
"set_texture_dir", "get_texture_dir");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "unit_scale", PROPERTY_HINT_RANGE, "0.0001,1,0.0001"),
|
||||
"set_unit_scale", "get_unit_scale");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_z"), "set_flip_z", "get_flip_z");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_winding"), "set_flip_winding", "get_flip_winding");
|
||||
}
|
||||
|
||||
String Metin2Model::libgr2_info() const {
|
||||
// Forces a reference to a libgr2 symbol so the link is real, not dead-stripped.
|
||||
return String("libgr2 linked; gr2::Magic enum size=") + itos((int)sizeof(gr2::Magic));
|
||||
void Metin2Model::set_gr2_path(const String &p) {
|
||||
gr2_path = p;
|
||||
if (is_inside_tree()) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
void Metin2Model::set_texture_dir(const String &p) {
|
||||
texture_dir = p;
|
||||
if (is_inside_tree()) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
void Metin2Model::set_unit_scale(double s) {
|
||||
unit_scale = s;
|
||||
if (is_inside_tree()) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
void Metin2Model::set_flip_z(bool v) {
|
||||
flip_z = v;
|
||||
if (is_inside_tree()) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
void Metin2Model::set_flip_winding(bool v) {
|
||||
flip_winding = v;
|
||||
if (is_inside_tree()) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
void Metin2Model::_ready() {
|
||||
if (!gr2_path.is_empty()) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
void Metin2Model::_clear_children() {
|
||||
skel = nullptr;
|
||||
mi = nullptr;
|
||||
TypedArray<Node> kids = get_children();
|
||||
for (int i = 0; i < kids.size(); ++i) {
|
||||
Node *n = Object::cast_to<Node>(kids[i]);
|
||||
if (n) {
|
||||
remove_child(n);
|
||||
n->queue_free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Metin2Model::reload() {
|
||||
_clear_children();
|
||||
*file = std::nullopt;
|
||||
last_info = "";
|
||||
|
||||
if (gr2_path.is_empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
gr2::LoadError err;
|
||||
const std::string p(gr2_path.utf8().get_data());
|
||||
auto loaded = gr2::File::load_path(p, &err);
|
||||
if (!loaded) {
|
||||
UtilityFunctions::push_error(String("[Metin2Model] gr2 load failed [") +
|
||||
String(err.stage.c_str()) + "]: " + String(err.message.c_str()) + " (" + gr2_path + ")");
|
||||
return;
|
||||
}
|
||||
*file = std::move(loaded);
|
||||
const gr2::FileInfo &fi = (*file)->file_info();
|
||||
|
||||
set_transform(make_conv((float)unit_scale, flip_z));
|
||||
|
||||
// Skeleton (optional).
|
||||
const gr2::Skeleton *sk = fi.skeletons.empty() ? nullptr : &fi.skeletons[0];
|
||||
if (sk) {
|
||||
skel = build_skeleton(*sk);
|
||||
if (skel) {
|
||||
add_child(skel);
|
||||
skel->set_owner(get_owner() ? get_owner() : this);
|
||||
}
|
||||
}
|
||||
|
||||
// Mesh.
|
||||
AABB bounds;
|
||||
Ref<ArrayMesh> mesh = build_mesh(fi, flip_winding, bounds);
|
||||
if (mesh->get_surface_count() > 0) {
|
||||
mi = memnew(MeshInstance3D);
|
||||
mi->set_name("MeshInstance3D");
|
||||
mi->set_mesh(mesh);
|
||||
add_child(mi);
|
||||
mi->set_owner(get_owner() ? get_owner() : this);
|
||||
if (skel && sk) {
|
||||
mi->set_skeleton_path(mi->get_path_to(skel));
|
||||
mi->set_skin(build_skin(*sk));
|
||||
}
|
||||
mi->set_custom_aabb(bounds.grow(0.01));
|
||||
}
|
||||
|
||||
_apply_materials();
|
||||
|
||||
int vtot = 0;
|
||||
for (const auto &m : fi.meshes) {
|
||||
vtot += (int)m.vertices.size();
|
||||
}
|
||||
last_info = vformat("gr2 v%d | bones=%d meshes=%d surfaces=%d verts=%d | bounds=%s",
|
||||
(int)(*file)->format_version(),
|
||||
sk ? (int)sk->bones.size() : 0,
|
||||
(int)fi.meshes.size(),
|
||||
mesh->get_surface_count(),
|
||||
vtot,
|
||||
String(bounds));
|
||||
UtilityFunctions::print("[Metin2Model] ", last_info);
|
||||
}
|
||||
|
||||
String Metin2Model::_guess_texture_dir() const {
|
||||
if (!texture_dir.is_empty()) {
|
||||
return texture_dir;
|
||||
}
|
||||
return gr2_path.get_base_dir();
|
||||
}
|
||||
|
||||
void Metin2Model::_apply_materials() {
|
||||
if (!mi || !file || !*file) {
|
||||
return;
|
||||
}
|
||||
const gr2::FileInfo &fi = (*file)->file_info();
|
||||
const String dir = _guess_texture_dir();
|
||||
const String stem = gr2_path.get_file().get_basename();
|
||||
|
||||
// Cache: texture path -> ImageTexture.
|
||||
auto load_tex = [&](const String &path) -> Ref<ImageTexture> {
|
||||
Ref<ImageTexture> t;
|
||||
if (!FileAccess::file_exists(path)) {
|
||||
return t;
|
||||
}
|
||||
mtgodot::Image dds = mtgodot::load_dds_path(path.utf8().get_data());
|
||||
if (!dds.ok()) {
|
||||
return t;
|
||||
}
|
||||
PackedByteArray bytes;
|
||||
bytes.resize((int)dds.rgba.size());
|
||||
memcpy(bytes.ptrw(), dds.rgba.data(), dds.rgba.size());
|
||||
Ref<godot::Image> img = godot::Image::create_from_data(
|
||||
dds.w, dds.h, false, godot::Image::FORMAT_RGBA8, bytes);
|
||||
if (img.is_null()) {
|
||||
return t;
|
||||
}
|
||||
img->generate_mipmaps();
|
||||
t = ImageTexture::create_from_image(img);
|
||||
return t;
|
||||
};
|
||||
|
||||
Ref<ImageTexture> tex_main = load_tex(dir.path_join(stem + String(".dds")));
|
||||
Ref<ImageTexture> tex_face = load_tex(dir.path_join(stem + String("_face.dds")));
|
||||
if (tex_face.is_null()) {
|
||||
tex_face = load_tex(dir.path_join(String("warrior_face.dds")));
|
||||
}
|
||||
|
||||
Ref<ArrayMesh> mesh = mi->get_mesh();
|
||||
for (int s = 0; s < mesh->get_surface_count(); ++s) {
|
||||
String sname = mesh->surface_get_name(s).to_lower();
|
||||
Ref<ImageTexture> tex = (sname.find("face") != -1 && tex_face.is_valid()) ? tex_face : tex_main;
|
||||
|
||||
Ref<StandardMaterial3D> mat;
|
||||
mat.instantiate();
|
||||
mat->set_cull_mode(BaseMaterial3D::CULL_DISABLED); // M1: winding unverified
|
||||
if (tex.is_valid()) {
|
||||
mat->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, tex);
|
||||
} else {
|
||||
mat->set_albedo(Color(0.8, 0.8, 0.82));
|
||||
}
|
||||
mi->set_surface_override_material(s, mat);
|
||||
}
|
||||
}
|
||||
|
||||
const gr2::Skeleton *Metin2Model::gr2_skeleton() const {
|
||||
if (!file || !*file) {
|
||||
return nullptr;
|
||||
}
|
||||
const gr2::FileInfo &fi = (*file)->file_info();
|
||||
return fi.skeletons.empty() ? nullptr : &fi.skeletons[0];
|
||||
}
|
||||
|
||||
String Metin2Model::get_info() const {
|
||||
return last_info;
|
||||
}
|
||||
|
||||
String Metin2Model::probe_gr2(const String &path) const {
|
||||
gr2::LoadError err;
|
||||
const std::string p(path.utf8().get_data());
|
||||
auto file = gr2::File::load_path(p, &err);
|
||||
if (!file) {
|
||||
return vformat("gr2 load FAILED [%s]: %s",
|
||||
String(err.stage.c_str()), String(err.message.c_str()));
|
||||
auto f = gr2::File::load_path(p, &err);
|
||||
if (!f) {
|
||||
return vformat("gr2 load FAILED [%s]: %s", String(err.stage.c_str()), String(err.message.c_str()));
|
||||
}
|
||||
return vformat("gr2 OK: format_version=%d sections=%d",
|
||||
(int)file->format_version(), (int)file->sections().size());
|
||||
(int)f->format_version(), (int)f->sections().size());
|
||||
}
|
||||
|
||||
} // namespace mtgodot
|
||||
|
||||
@@ -3,14 +3,24 @@
|
||||
#include <godot_cpp/classes/node3d.hpp>
|
||||
#include <godot_cpp/variant/string.hpp>
|
||||
|
||||
#include <gr2/gr2.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace godot {
|
||||
class Skeleton3D;
|
||||
class MeshInstance3D;
|
||||
} // namespace godot
|
||||
|
||||
namespace mtgodot {
|
||||
|
||||
// M0' placeholder node.
|
||||
// Loads a Metin2 .gr2 and builds a Godot subtree:
|
||||
// Metin2Model (transform = Z-up/cm -> Y-up/m conversion)
|
||||
// └─ Skeleton3D (rest pose from gr2 bones)
|
||||
// └─ MeshInstance3D (ArrayMesh, one surface per gr2 mesh; skinned)
|
||||
//
|
||||
// Purpose at this milestone: prove the GDExtension registers a class the Godot
|
||||
// editor can instantiate, and that libgr2 is linked in and callable from here.
|
||||
// M1 replaces the body with real Skeleton3D / ArrayMesh construction
|
||||
// (see docs/GODOT-POC-PLAN.md §M1).
|
||||
// M1: static bind-pose render. M2's Metin2AnimPlayer drives the Skeleton3D.
|
||||
class Metin2Model : public godot::Node3D {
|
||||
GDCLASS(Metin2Model, godot::Node3D)
|
||||
|
||||
@@ -18,14 +28,55 @@ public:
|
||||
Metin2Model();
|
||||
~Metin2Model() override;
|
||||
|
||||
// One-line tag proving libgr2 symbols resolved at link time.
|
||||
godot::String libgr2_info() const;
|
||||
void _ready() override;
|
||||
|
||||
// Parse a .gr2 with libgr2 and report a couple of counts (no scene build yet).
|
||||
// --- inspector properties ---
|
||||
void set_gr2_path(const godot::String &p);
|
||||
godot::String get_gr2_path() const { return gr2_path; }
|
||||
|
||||
void set_texture_dir(const godot::String &p);
|
||||
godot::String get_texture_dir() const { return texture_dir; }
|
||||
|
||||
void set_unit_scale(double s);
|
||||
double get_unit_scale() const { return unit_scale; }
|
||||
|
||||
void set_flip_z(bool v);
|
||||
bool get_flip_z() const { return flip_z; }
|
||||
|
||||
void set_flip_winding(bool v);
|
||||
bool get_flip_winding() const { return flip_winding; }
|
||||
|
||||
// Rebuild the subtree from the current properties.
|
||||
void reload();
|
||||
|
||||
// One-line summary (bones / meshes / verts / bounds).
|
||||
godot::String get_info() const;
|
||||
|
||||
// --- C++ accessors for Metin2AnimPlayer (same extension) ---
|
||||
const gr2::Skeleton *gr2_skeleton() const;
|
||||
godot::Skeleton3D *skeleton_node() const { return skel; }
|
||||
|
||||
// libgr2 sanity probe (kept from M0').
|
||||
godot::String probe_gr2(const godot::String &path) const;
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
||||
private:
|
||||
godot::String gr2_path;
|
||||
godot::String texture_dir;
|
||||
double unit_scale = 0.01;
|
||||
bool flip_z = false;
|
||||
bool flip_winding = false;
|
||||
|
||||
std::shared_ptr<std::optional<gr2::File>> file; // shared_ptr so accessor stays valid
|
||||
godot::Skeleton3D *skel = nullptr;
|
||||
godot::MeshInstance3D *mi = nullptr;
|
||||
godot::String last_info;
|
||||
|
||||
void _clear_children();
|
||||
void _apply_materials();
|
||||
godot::String _guess_texture_dir() const;
|
||||
};
|
||||
|
||||
} // namespace mtgodot
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <godot_cpp/core/defs.hpp>
|
||||
#include <godot_cpp/godot.hpp>
|
||||
|
||||
#include "metin2_anim.h"
|
||||
#include "metin2_model.h"
|
||||
|
||||
using namespace godot;
|
||||
@@ -13,6 +14,7 @@ void initialize_mtgodot_module(ModuleInitializationLevel p_level) {
|
||||
return;
|
||||
}
|
||||
GDREGISTER_CLASS(mtgodot::Metin2Model);
|
||||
GDREGISTER_CLASS(mtgodot::Metin2AnimPlayer);
|
||||
}
|
||||
|
||||
void uninitialize_mtgodot_module(ModuleInitializationLevel p_level) {
|
||||
|
||||
Reference in New Issue
Block a user