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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
View File
@@ -23,6 +23,9 @@ add_subdirectory("${XRENDER_POC_DIR}/libgr2" "${CMAKE_CURRENT_BINARY_DIR}/libgr2
add_library(mtgodot SHARED add_library(mtgodot SHARED
src/register_types.cpp src/register_types.cpp
src/metin2_model.cpp src/metin2_model.cpp
src/metin2_anim.cpp
src/gr2_bridge.cpp
src/dxt.cpp
) )
target_compile_features(mtgodot PRIVATE cxx_std_20) target_compile_features(mtgodot PRIVATE cxx_std_20)
target_link_libraries(mtgodot PRIVATE godot::cpp xrender::libgr2) target_link_libraries(mtgodot PRIVATE godot::cpp xrender::libgr2)
+160
View File
@@ -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(不写 alphadxt1_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]);
}
}
// DXT3alpha 块(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);
}
}
// DXT5alpha 块(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
+23
View File
@@ -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*4level 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

Some files were not shown because too many files have changed in this diff Show More