Fix cull mode + isolate head-collapse to a libgr2 curve bug
Rendering fixes (real): - m2_material shaders: cull_disabled -> cull_back (inside-of-mesh faces were z-winning and darkening/hiding the head under the ShaderMaterial path). - drop `* COLOR` from the shader (meshes carry no ARRAY_COLOR). Animation: convert gr2 global poses to Godot *local* bone poses and set via set_bone_pose (was set_bone_global_pose). MTGODOT_VERIFY=1 confirms get_bone_global_pose(i) == conv(world[i]) for all 75 bones (<=2.6e-5). Head/upper-armor collapse on dance_1 is NOT a Godot-route bug: - reproduced identically in xrender-poc's bgfx demo at the same anim/t - reproduced by the new MTGODOT_CPUSKIN=1 reference path (same LBS math as xrender's validated skin_mesh) - `general/wait` / `run` render correctly in all three => upstream libgr2 Curve::eval (degree-2 quaternion B-spline) vs Granny. Demo default animation switched to general/wait; documented in MIDREVIEW §4 and README. Debug scaffolding gated behind MTGODOT_VERIFY / MTGODOT_CPUSKIN. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaHYEY9rwLWt21PULiYjeJ
@@ -26,8 +26,11 @@
|
|||||||
(`test/golden/m1-bindpose.png`)。多骨架:`warrior_lord`(v6,74)、`assassin`(v6,95,17 表面)、
|
(`test/golden/m1-bindpose.png`)。多骨架:`warrior_lord`(v6,74)、`assassin`(v6,95,17 表面)、
|
||||||
`shaman_lord`(**v7**,93) 均正确加载渲染。
|
`shaman_lord`(**v7**,93) 均正确加载渲染。
|
||||||
- [x] **M2** — 骨骼动画。`Metin2AnimPlayer` `_process` 里 `gr2::sample_pose`(跨文件)
|
- [x] **M2** — 骨骼动画。`Metin2AnimPlayer` `_process` 里 `gr2::sample_pose`(跨文件)
|
||||||
→ `Skeleton3D.set_bone_global_pose`,Godot GPU 蒙皮。`dance01`(28.3s / 74 track)
|
→ 逐骨 `Skeleton3D.set_bone_pose`(局部),Godot GPU 蒙皮。`general/wait` 头 / 护甲
|
||||||
播放,`selfcheck` 25 采样 0 NaN,t=2/9/16/23 姿势各异(`test/golden/m2c-*.png`)。
|
对齐正确(`test/golden/m2-wait.png`);`get_bone_global_pose` vs `conv(world)` 全骨 ≤2.6e-5;
|
||||||
|
CPU-LBS 参考路径(`MTGODOT_CPUSKIN=1`)与 GPU 蒙皮一致;`selfcheck` 0 NaN。
|
||||||
|
⚠️ `dance_1` 等表情动作头 / 颈塌陷 —— **libgr2 曲线解码 bug,xrender-poc bgfx demo 同样复现**
|
||||||
|
(见 `docs/MIDREVIEW.md` §4)。
|
||||||
- [~] **M2.5** — 材质与观感保真。`ShaderMaterial`(modulate tex×COLOR×light、
|
- [~] **M2.5** — 材质与观感保真。`ShaderMaterial`(modulate tex×COLOR×light、
|
||||||
opaque/alpha/alpha-test/add 四模式)+ 目录扫描贴图解析 + 方向光阴影 + procedural sky
|
opaque/alpha/alpha-test/add 四模式)+ 目录扫描贴图解析 + 方向光阴影 + procedural sky
|
||||||
+ 引擎雾(`test/golden/m25-*.png`)。**门禁未闭**:多材质槽 → 贴图/混合的精确映射需
|
+ 引擎雾(`test/golden/m25-*.png`)。**门禁未闭**:多材质槽 → 贴图/混合的精确映射需
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **M0'** 脚手架 | ✅ | GDExtension 加载、`Metin2Model`/`Metin2AnimPlayer` 注册、`libgr2` 静态链接、Godot 4.7 工程 Metal Forward+ 出画 |
|
| **M0'** 脚手架 | ✅ | GDExtension 加载、`Metin2Model`/`Metin2AnimPlayer` 注册、`libgr2` 静态链接、Godot 4.7 工程 Metal Forward+ 出画 |
|
||||||
| **M1** 静态渲染 | ✅ | `warrior_cheongrin`:75 骨 / 5 表面 / 3324 顶点,正立、朝 +Z、`unit_scale=0.01`、`flip_z/flip_winding=false`、贴图正确(`test/golden/m1-bindpose.png`)。多骨架:`warrior_lord`(v6,74)、`assassin`(v6,95,17 表面)、`shaman_lord`(**v7**,93) 均加载渲染正确 |
|
| **M1** 静态渲染 | ✅ | `warrior_cheongrin`:75 骨 / 5 表面 / 3324 顶点,正立、朝 +Z、`unit_scale=0.01`、`flip_z/flip_winding=false`、贴图正确(`test/golden/m1-bindpose.png`)。多骨架:`warrior_lord`(v6,74)、`assassin`(v6,95,17 表面)、`shaman_lord`(**v7**,93) 均加载渲染正确 |
|
||||||
| **M2** 骨骼动画 | ✅ | `dance01`(28.3s / 74 track)经 `gr2::sample_pose`(跨文件)→ `Skeleton3D.set_bone_global_pose` → Godot GPU 蒙皮播放。`selfcheck` 25 采样 **0 NaN/Inf**。t=9/16 姿势正确、无蒙皮撕裂(`test/golden/m2c-*.png`、`m25-warrior-shader.png`) |
|
| **M2** 骨骼动画 | ✅(渲染路径)/ ⚠️(一条 upstream bug) | `general/wait`(2.67s / 75 track)经 `gr2::sample_pose`(跨文件)→ 逐骨 Godot **局部 pose**(`set_bone_pose`)→ GPU 蒙皮,头 / 护甲对齐正确(`test/golden/m2-wait.png`)。**数值验证**:`get_bone_global_pose(i)` vs `conv(world[i])` 全 75 骨 ≤ **2.6e-5**;CPU-LBS 参考路径(`MTGODOT_CPUSKIN=1`,与 xrender `skin_mesh` 同式)与 GPU 蒙皮同结果。`selfcheck` 0 NaN。**但**:`dance_1` 等表情动作会让头 / 颈塌陷 —— **libgr2 曲线解码 bug,在 xrender-poc 的 bgfx demo 里同样复现**(见 §4) |
|
||||||
| **M2.5** 材质与观感 | 🟡 门禁未闭 | `ShaderMaterial`(modulate tex×COLOR×light;opaque/alpha/alpha-test/add 四模式)+ 目录扫描贴图解析(warrior 5/5、assassin 17/17、shaman 1/1 表面命中)+ 方向光阴影 + procedural sky + 引擎雾(`test/golden/m25-*.png`)。`libgr2::dump_materials()` 已加(见 §4) |
|
| **M2.5** 材质与观感 | 🟡 门禁未闭 | `ShaderMaterial`(modulate tex×COLOR×light;opaque/alpha/alpha-test/add 四模式)+ 目录扫描贴图解析(warrior 5/5、assassin 17/17、shaman 1/1 表面命中)+ 方向光阴影 + procedural sky + 引擎雾(`test/golden/m25-*.png`)。`libgr2::dump_materials()` 已加(见 §4) |
|
||||||
|
|
||||||
## 2. 桥接层工程量(实测)
|
## 2. 桥接层工程量(实测)
|
||||||
@@ -40,7 +40,8 @@
|
|||||||
|
|
||||||
| 项 | 影响 | 出路 |
|
| 项 | 影响 | 出路 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **M2.5 材质精确映射未闭**(唯一门禁开口)| `libgr2::dump_materials()` 已能取材质名 + `FromFileName`(warrior 4 材质 / 2 有 diffuse 名),但 material_index→贴图 的映射**未对拍 Granny**,实测驱动 warrior 时 body 表面误取 face 贴图。当前默认走文件名+目录扫描启发式(观感正确),material 路径 `use_gr2_materials` 开关默认关 | 需要 Granny oracle 逐材质对拍(与 xrender-poc M1 T5 同一缺口,共用修法)。这是 Phase 1 收尾的最后一项 |
|
| **libgr2 曲线解码 bug —— 部分动画头 / 颈塌陷**(`dance_1` 等表情,`rot:Old(d=2,dim=4)` 二次四元数 B 样条)。**这不是 Godot route 的问题** —— 已用 xrender-poc 的 bgfx demo 在同一动画 / 同一 t 复现完全一致的塌陷(`/tmp/xr-*.png`),且 mtgodot 的 CPU-LBS 参考路径(与 xrender 同式)也复现。`wait` / `run` 等动画正常。xrender-poc M2「对拍 Granny ≤4.6e-5」的 23 用例未覆盖到这个可视化用例。 | 表情动作不可用;idle / 移动动画正常。当前 demo 默认 `general/wait` | libgr2 `gr2_anim.cpp` 的 `Curve::eval` degree-2 分支 vs Granny `OldCurve` B 样条基对拍修正(需 Granny oracle)。两条 route 共用修法。**属 xrender-poc/libgr2 的活,不阻塞 Godot route 结论** |
|
||||||
|
| **M2.5 材质精确映射未闭**(M2.5 门禁开口)| `libgr2::dump_materials()` 已能取材质名 + `FromFileName`(warrior 4 材质 / 2 有 diffuse 名),但 material_index→贴图 的映射**未对拍 Granny**,实测驱动 warrior 时 body 表面误取 face 贴图。当前默认走文件名+目录扫描启发式(观感正确),material 路径 `use_gr2_materials` 开关默认关 | 需要 Granny oracle 逐材质对拍(与 xrender-poc M1 T5 同一缺口,共用修法)。这是 Phase 1 收尾的最后一项 |
|
||||||
| **对拍未接线** | 与 xrender-poc bgfx demo / oracle 的逐帧几何/骨骼矩阵对拍脚本(`test/compare.py`)未写 | 需先构建 xrender-poc bgfx demo(首次 ~10 min)。中期评审后、进 Phase 2 前补 |
|
| **对拍未接线** | 与 xrender-poc bgfx demo / oracle 的逐帧几何/骨骼矩阵对拍脚本(`test/compare.py`)未写 | 需先构建 xrender-poc bgfx demo(首次 ~10 min)。中期评审后、进 Phase 2 前补 |
|
||||||
| **性能数字被节流** | 后台窗口下 Godot 限 30fps,1–50 角色帧时间都贴在 33ms 天花板;100 角色破顶到 52ms。资产构建 **~165ms/角色**(单线程,无跨模型缓存),100 角色加载 16.5s(`test/godot-macos-stress.json`) | 真机 / 前台窗口下重测(Phase 2)。构建成本需 Phase 2 做多线程 + eterpack 共享贴图 |
|
| **性能数字被节流** | 后台窗口下 Godot 限 30fps,1–50 角色帧时间都贴在 33ms 天花板;100 角色破顶到 52ms。资产构建 **~165ms/角色**(单线程,无跨模型缓存),100 角色加载 16.5s(`test/godot-macos-stress.json`) | 真机 / 前台窗口下重测(Phase 2)。构建成本需 Phase 2 做多线程 + eterpack 共享贴图 |
|
||||||
| **多材质槽内拆分** | `build_mesh` 目前一个 gr2 mesh = 一个 surface,未按 `tri_groups[].material_index` 再拆 | 有 material API 对拍后一起做 |
|
| **多材质槽内拆分** | `build_mesh` 目前一个 gr2 mesh = 一个 surface,未按 `tri_groups[].material_index` 再拆 | 有 material API 对拍后一起做 |
|
||||||
@@ -52,9 +53,12 @@
|
|||||||
|
|
||||||
理由:Phase 1 要证明的三件事里,①gr2→Godot 场景对象、②动画运行时移植,**已用可运行代码 + 截图 + 0-NaN 自检证明可行且工程量小**;③观感还原的基础设施(自定义 shader、阴影、sky、fog、多骨架)已就位,只差"材质精确映射"这一处需 oracle 的收尾,不构成方案性风险。
|
理由:Phase 1 要证明的三件事里,①gr2→Godot 场景对象、②动画运行时移植,**已用可运行代码 + 截图 + 0-NaN 自检证明可行且工程量小**;③观感还原的基础设施(自定义 shader、阴影、sky、fog、多骨架)已就位,只差"材质精确映射"这一处需 oracle 的收尾,不构成方案性风险。
|
||||||
|
|
||||||
Phase 2 前的收尾清单(1–1.5 周):
|
Phase 2 前的收尾清单(1.5–2 周):
|
||||||
1. 写 `test/compare.py`,与 xrender-poc bgfx demo 同机位 / 同 t 对拍几何 + 骨骼矩阵。
|
1. 写 `test/compare.py`,与 xrender-poc bgfx demo 同机位 / 同 t 对拍几何 + 骨骼矩阵。
|
||||||
2. 补 `libgr2::dump_materials` 的 Granny oracle 对拍,闭合 M2.5 门禁。
|
2. 补 `libgr2::dump_materials` 的 Granny oracle 对拍,闭合 M2.5 门禁。
|
||||||
3. 前台窗口 / 真机重测性能,给 Phase 2 的动画 A/B 一个可信基线。
|
3. **修 libgr2 `Curve::eval` degree-2 四元数 B 样条**(对拍 Granny oracle)—— 这条其实是 xrender-poc/libgr2 的活,两条 route 共享收益;修好后表情动作即可用。
|
||||||
|
4. 前台窗口 / 真机重测性能,给 Phase 2 的动画 A/B 一个可信基线。
|
||||||
|
|
||||||
|
> Godot route 本身的桥接层已验证正确(`wait` 渲染与 xrender bgfx demo 一致,骨骼矩阵 ≤2.6e-5,CPU/GPU 蒙皮一致)。#3 是把已知的 upstream 曲线 bug 补上,不是 Godot route 的缺陷。
|
||||||
|
|
||||||
不建议现在做的:地形、`.mse` 特效、eterpack、UI/Python —— 均按计划留到正式移植。
|
不建议现在做的:地形、`.mse` 特效、eterpack、UI/Python —— 均按计划留到正式移植。
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace {
|
|||||||
|
|
||||||
// blend_mix shader: covers opaque (mode 0, alpha-scissor) and alpha (mode 1).
|
// blend_mix shader: covers opaque (mode 0, alpha-scissor) and alpha (mode 1).
|
||||||
const char *SRC_MIX = R"(shader_type spatial;
|
const char *SRC_MIX = R"(shader_type spatial;
|
||||||
render_mode blend_mix, depth_draw_opaque, cull_disabled, diffuse_lambert, specular_disabled;
|
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_disabled;
|
||||||
|
|
||||||
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap, repeat_enable;
|
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap, repeat_enable;
|
||||||
uniform bool use_texture = true;
|
uniform bool use_texture = true;
|
||||||
@@ -22,7 +22,7 @@ uniform int mode = 0; // 0 opaque | 1 alpha-blend | 2 alpha-test (cutout)
|
|||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec4 c = use_texture ? texture(albedo_tex, UV) : vec4(1.0);
|
vec4 c = use_texture ? texture(albedo_tex, UV) : vec4(1.0);
|
||||||
c *= modulate * COLOR;
|
c *= modulate; // no vertex color: Metin2 PC meshes carry none (ARRAY_COLOR absent)
|
||||||
if (mode == 2 && c.a < alpha_scissor) {
|
if (mode == 2 && c.a < alpha_scissor) {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ void fragment() {
|
|||||||
|
|
||||||
// blend_add shader: additive glow (effects, enchant overlays).
|
// blend_add shader: additive glow (effects, enchant overlays).
|
||||||
const char *SRC_ADD = R"(shader_type spatial;
|
const char *SRC_ADD = R"(shader_type spatial;
|
||||||
render_mode blend_add, depth_draw_opaque, depth_test_disabled, cull_disabled, unshaded;
|
render_mode blend_add, depth_draw_opaque, depth_test_disabled, cull_back, unshaded;
|
||||||
|
|
||||||
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap, repeat_enable;
|
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap, repeat_enable;
|
||||||
uniform bool use_texture = true;
|
uniform bool use_texture = true;
|
||||||
@@ -41,7 +41,7 @@ uniform vec4 modulate : source_color = vec4(1.0);
|
|||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec4 c = use_texture ? texture(albedo_tex, UV) : vec4(1.0);
|
vec4 c = use_texture ? texture(albedo_tex, UV) : vec4(1.0);
|
||||||
c *= modulate * COLOR;
|
c *= modulate; // no vertex color: Metin2 PC meshes carry none (ARRAY_COLOR absent)
|
||||||
ALBEDO = c.rgb * c.a;
|
ALBEDO = c.rgb * c.a;
|
||||||
ALPHA = 1.0;
|
ALPHA = 1.0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,18 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <godot_cpp/classes/mesh_instance3d.hpp>
|
||||||
|
#include <godot_cpp/classes/os.hpp>
|
||||||
|
#include <godot_cpp/classes/skin.hpp>
|
||||||
|
|
||||||
|
static double _tdiff(const godot::Transform3D &a, const godot::Transform3D &b) {
|
||||||
|
double d = 0.0;
|
||||||
|
for (int r = 0; r < 3; ++r)
|
||||||
|
for (int c = 0; c < 3; ++c)
|
||||||
|
d = godot::Math::max(d, (double)godot::Math::abs(a.basis[r][c] - b.basis[r][c]));
|
||||||
|
return godot::Math::max(d, (double)(a.origin - b.origin).length());
|
||||||
|
}
|
||||||
|
|
||||||
using namespace godot;
|
using namespace godot;
|
||||||
|
|
||||||
namespace mtgodot {
|
namespace mtgodot {
|
||||||
@@ -95,6 +107,31 @@ void Metin2AnimPlayer::reload() {
|
|||||||
anim_file = std::move(f);
|
anim_file = std::move(f);
|
||||||
const gr2::Animation &an = anim_file->file_info().animations[0];
|
const gr2::Animation &an = anim_file->file_info().animations[0];
|
||||||
duration = an.duration;
|
duration = an.duration;
|
||||||
|
|
||||||
|
// Diagnostic: how many anim tracks map to a model-skeleton bone by name?
|
||||||
|
if (Metin2Model *model = resolve_model()) {
|
||||||
|
if (const gr2::Skeleton *sk = model->gr2_skeleton()) {
|
||||||
|
int matched = 0;
|
||||||
|
godot::String unmatched;
|
||||||
|
for (const auto &tr : an.tracks) {
|
||||||
|
bool hit = false;
|
||||||
|
for (const auto &b : sk->bones) {
|
||||||
|
if (b.name == tr.bone_name) {
|
||||||
|
hit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hit) {
|
||||||
|
++matched;
|
||||||
|
} else if (unmatched.length() < 300) {
|
||||||
|
unmatched += String(tr.bone_name.c_str()) + " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UtilityFunctions::print(vformat(
|
||||||
|
"[Metin2AnimPlayer] track->bone match: %d/%d (skel bones=%d) unmatched: %s",
|
||||||
|
matched, (int)an.tracks.size(), (int)sk->bones.size(), unmatched));
|
||||||
|
}
|
||||||
|
}
|
||||||
last_info = vformat("anim '%s' dur=%.3f tracks=%d anims=%d",
|
last_info = vformat("anim '%s' dur=%.3f tracks=%d anims=%d",
|
||||||
String(an.name.c_str()), duration, (int)an.tracks.size(),
|
String(an.name.c_str()), duration, (int)an.tracks.size(),
|
||||||
(int)anim_file->file_info().animations.size());
|
(int)anim_file->file_info().animations.size());
|
||||||
@@ -127,9 +164,43 @@ void Metin2AnimPlayer::apply_pose(double t) {
|
|||||||
|
|
||||||
gr2::sample_pose(*sk, an, (float)tt, world_buf, skin_buf);
|
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());
|
if (godot::OS::get_singleton()->get_environment("MTGODOT_CPUSKIN") == "1") {
|
||||||
|
model->enable_cpu_skin(true);
|
||||||
|
model->cpu_skin(skin_buf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert gr2 global poses -> Godot *local* bone poses and set those.
|
||||||
|
// (Setting global poses directly hits a parent-ordering hazard in
|
||||||
|
// Skeleton3D::set_bone_global_pose that detaches head/upper-body meshes
|
||||||
|
// once bones leave the bind pose.)
|
||||||
|
const int n = (int)std::min<size_t>(world_buf.size(),
|
||||||
|
std::min<size_t>(sk->bones.size(), (size_t)skel->get_bone_count()));
|
||||||
for (int i = 0; i < n; ++i) {
|
for (int i = 0; i < n; ++i) {
|
||||||
skel->set_bone_global_pose(i, gr2_to_godot(world_buf[i]));
|
const godot::Transform3D t_i = gr2_to_godot(world_buf[i]);
|
||||||
|
const int parent = sk->bones[i].parent;
|
||||||
|
if (parent < 0 || parent >= n) {
|
||||||
|
skel->set_bone_pose(i, t_i);
|
||||||
|
} else {
|
||||||
|
skel->set_bone_pose(i, gr2_to_godot(world_buf[parent]).affine_inverse() * t_i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verified_ && OS::get_singleton()->get_environment("MTGODOT_VERIFY") == "1") {
|
||||||
|
verified_ = true;
|
||||||
|
skel->force_update_all_bone_transforms();
|
||||||
|
double d_pose = 0.0;
|
||||||
|
int w_pose = -1;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
double dp = _tdiff(skel->get_bone_global_pose(i), gr2_to_godot(world_buf[i]));
|
||||||
|
if (dp > d_pose) {
|
||||||
|
d_pose = dp;
|
||||||
|
w_pose = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UtilityFunctions::print(vformat(
|
||||||
|
"[Metin2AnimPlayer] verify: max |global_pose - conv(world)| = %.6f @ bone %d '%s'",
|
||||||
|
d_pose, w_pose, w_pose >= 0 ? String(sk->bones[w_pose].name.c_str()) : String()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ private:
|
|||||||
double time_scale = 1.0;
|
double time_scale = 1.0;
|
||||||
double time = 0.0;
|
double time = 0.0;
|
||||||
double duration = 0.0;
|
double duration = 0.0;
|
||||||
|
bool verified_ = false;
|
||||||
godot::String last_info;
|
godot::String last_info;
|
||||||
|
|
||||||
std::optional<gr2::File> anim_file;
|
std::optional<gr2::File> anim_file;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <godot_cpp/classes/image.hpp>
|
#include <godot_cpp/classes/image.hpp>
|
||||||
#include <godot_cpp/classes/image_texture.hpp>
|
#include <godot_cpp/classes/image_texture.hpp>
|
||||||
#include <godot_cpp/classes/mesh_instance3d.hpp>
|
#include <godot_cpp/classes/mesh_instance3d.hpp>
|
||||||
|
#include <godot_cpp/classes/os.hpp>
|
||||||
#include <godot_cpp/classes/shader_material.hpp>
|
#include <godot_cpp/classes/shader_material.hpp>
|
||||||
#include <godot_cpp/classes/skeleton3d.hpp>
|
#include <godot_cpp/classes/skeleton3d.hpp>
|
||||||
#include <godot_cpp/classes/skin.hpp>
|
#include <godot_cpp/classes/skin.hpp>
|
||||||
@@ -184,8 +185,12 @@ void Metin2Model::reload() {
|
|||||||
mi->set_owner(get_owner() ? get_owner() : this);
|
mi->set_owner(get_owner() ? get_owner() : this);
|
||||||
if (skel && sk) {
|
if (skel && sk) {
|
||||||
mi->set_skeleton_path(mi->get_path_to(skel));
|
mi->set_skeleton_path(mi->get_path_to(skel));
|
||||||
|
if (OS::get_singleton()->get_environment("MTGODOT_AUTOSKIN") == "1") {
|
||||||
|
// let Godot derive the skin from skeleton rest poses
|
||||||
|
} else {
|
||||||
mi->set_skin(build_skin(*sk));
|
mi->set_skin(build_skin(*sk));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
mi->set_custom_aabb(bounds.grow(0.01));
|
mi->set_custom_aabb(bounds.grow(0.01));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +440,124 @@ const gr2::Skeleton *Metin2Model::gr2_skeleton() const {
|
|||||||
return fi.skeletons.empty() ? nullptr : &fi.skeletons[0];
|
return fi.skeletons.empty() ? nullptr : &fi.skeletons[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gr2::FileInfo *Metin2Model::gr2_fileinfo() const {
|
||||||
|
return (file && *file) ? &(*file)->file_info() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// row-major 4x4 (translation elem 12..14), row-vector: out = [v 1] * M
|
||||||
|
inline void xform_pt(const gr2::Mat4 &M, const float *v, float *o) {
|
||||||
|
o[0] = v[0] * M[0] + v[1] * M[4] + v[2] * M[8] + M[12];
|
||||||
|
o[1] = v[0] * M[1] + v[1] * M[5] + v[2] * M[9] + M[13];
|
||||||
|
o[2] = v[0] * M[2] + v[1] * M[6] + v[2] * M[10] + M[14];
|
||||||
|
}
|
||||||
|
inline void xform_dir(const gr2::Mat4 &M, const float *v, float *o) {
|
||||||
|
o[0] = v[0] * M[0] + v[1] * M[4] + v[2] * M[8];
|
||||||
|
o[1] = v[0] * M[1] + v[1] * M[5] + v[2] * M[9];
|
||||||
|
o[2] = v[0] * M[2] + v[1] * M[6] + v[2] * M[10];
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Metin2Model::enable_cpu_skin(bool on) {
|
||||||
|
if (!mi || !file || !*file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (on && cpu_mesh.is_null()) {
|
||||||
|
AABB b;
|
||||||
|
cpu_mesh = build_mesh((*file)->file_info(), flip_winding, b);
|
||||||
|
cpu_surf_mesh.clear();
|
||||||
|
const gr2::FileInfo &fi = (*file)->file_info();
|
||||||
|
for (int i = 0; i < (int)fi.meshes.size(); ++i) {
|
||||||
|
if (!fi.meshes[i].vertices.empty() && !fi.meshes[i].indices.empty()) {
|
||||||
|
cpu_surf_mesh.push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mi->set_skeleton_path(NodePath());
|
||||||
|
mi->set_skin(Ref<Skin>());
|
||||||
|
mi->set_mesh(cpu_mesh);
|
||||||
|
if (skel) {
|
||||||
|
skel->set_show_rest_only(true);
|
||||||
|
}
|
||||||
|
UtilityFunctions::print("[Metin2Model] CPU-skin mode ON");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Metin2Model::cpu_skin(const std::vector<gr2::Mat4> &skin) {
|
||||||
|
if (cpu_mesh.is_null() || !file || !*file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gr2::FileInfo &fi = (*file)->file_info();
|
||||||
|
static const gr2::Mat4 I{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
|
||||||
|
|
||||||
|
cpu_mesh->clear_surfaces();
|
||||||
|
for (int s = 0; s < (int)cpu_surf_mesh.size(); ++s) {
|
||||||
|
const gr2::Mesh &m = fi.meshes[cpu_surf_mesh[s]];
|
||||||
|
const int nv = (int)m.vertices.size();
|
||||||
|
PackedVector3Array pos;
|
||||||
|
PackedVector3Array nrm;
|
||||||
|
PackedVector2Array uv;
|
||||||
|
PackedInt32Array idx;
|
||||||
|
pos.resize(nv);
|
||||||
|
nrm.resize(nv);
|
||||||
|
uv.resize(nv);
|
||||||
|
idx.resize((int)m.indices.size());
|
||||||
|
Vector3 *pw = pos.ptrw();
|
||||||
|
Vector3 *nw = nrm.ptrw();
|
||||||
|
Vector2 *uw = uv.ptrw();
|
||||||
|
int32_t *iw = idx.ptrw();
|
||||||
|
auto bone_of = [&](int slot) -> const gr2::Mat4 & {
|
||||||
|
if (slot < 0 || slot >= (int)m.bone_bindings.size()) {
|
||||||
|
return I;
|
||||||
|
}
|
||||||
|
int b = m.bone_bindings[slot];
|
||||||
|
return (b < 0 || b >= (int)skin.size()) ? I : skin[b];
|
||||||
|
};
|
||||||
|
for (int i = 0; i < nv; ++i) {
|
||||||
|
const gr2::Vertex &v = m.vertices[i];
|
||||||
|
float wq[4];
|
||||||
|
float sw = 0;
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
sw += v.bone_weight[k];
|
||||||
|
}
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
wq[k] = (sw > 0) ? v.bone_weight[k] / sw : (k == 0 ? 1.f : 0.f);
|
||||||
|
}
|
||||||
|
gr2::Mat4 M{};
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
const gr2::Mat4 &bm = bone_of(v.bone_index[k]);
|
||||||
|
for (int e = 0; e < 16; ++e) {
|
||||||
|
M[e] += wq[k] * bm[e];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gr2::Mat4 M3 = M;
|
||||||
|
M3[12] = M3[13] = M3[14] = 0;
|
||||||
|
float p[3], nn[3];
|
||||||
|
xform_pt(M, v.pos, p);
|
||||||
|
xform_dir(M3, v.normal, nn);
|
||||||
|
float ln = std::sqrt(nn[0] * nn[0] + nn[1] * nn[1] + nn[2] * nn[2]);
|
||||||
|
if (ln > 1e-8f) {
|
||||||
|
nn[0] /= ln;
|
||||||
|
nn[1] /= ln;
|
||||||
|
nn[2] /= ln;
|
||||||
|
}
|
||||||
|
pw[i] = Vector3(p[0], p[1], p[2]);
|
||||||
|
nw[i] = Vector3(nn[0], nn[1], nn[2]);
|
||||||
|
uw[i] = Vector2(v.uv0[0], v.uv0[1]);
|
||||||
|
}
|
||||||
|
for (size_t t = 0; t < m.indices.size(); ++t) {
|
||||||
|
iw[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_INDEX] = idx;
|
||||||
|
cpu_mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrays);
|
||||||
|
}
|
||||||
|
_apply_materials();
|
||||||
|
}
|
||||||
|
|
||||||
String Metin2Model::get_info() const {
|
String Metin2Model::get_info() const {
|
||||||
return last_info;
|
return last_info;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <godot_cpp/classes/array_mesh.hpp>
|
||||||
#include <godot_cpp/classes/image_texture.hpp>
|
#include <godot_cpp/classes/image_texture.hpp>
|
||||||
#include <godot_cpp/classes/node3d.hpp>
|
#include <godot_cpp/classes/node3d.hpp>
|
||||||
#include <godot_cpp/classes/ref.hpp>
|
#include <godot_cpp/classes/ref.hpp>
|
||||||
@@ -16,7 +17,6 @@
|
|||||||
namespace godot {
|
namespace godot {
|
||||||
class Skeleton3D;
|
class Skeleton3D;
|
||||||
class MeshInstance3D;
|
class MeshInstance3D;
|
||||||
class ImageTexture;
|
|
||||||
} // namespace godot
|
} // namespace godot
|
||||||
|
|
||||||
namespace mtgodot {
|
namespace mtgodot {
|
||||||
@@ -70,6 +70,14 @@ public:
|
|||||||
// --- C++ accessors for Metin2AnimPlayer (same extension) ---
|
// --- C++ accessors for Metin2AnimPlayer (same extension) ---
|
||||||
const gr2::Skeleton *gr2_skeleton() const;
|
const gr2::Skeleton *gr2_skeleton() const;
|
||||||
godot::Skeleton3D *skeleton_node() const { return skel; }
|
godot::Skeleton3D *skeleton_node() const { return skel; }
|
||||||
|
const gr2::FileInfo *gr2_fileinfo() const;
|
||||||
|
|
||||||
|
// CPU linear-blend skinning: rewrite ArrayMesh vertex regions from the
|
||||||
|
// per-bone deformer matrices (gr2::sample_pose's `skin` output). Debug/
|
||||||
|
// reference path; bypasses Godot's Skeleton3D skinning entirely.
|
||||||
|
void cpu_skin(const std::vector<gr2::Mat4> &skin);
|
||||||
|
bool has_cpu_skin_mesh() const { return cpu_mesh.is_valid(); }
|
||||||
|
void enable_cpu_skin(bool on);
|
||||||
|
|
||||||
// libgr2 sanity probe (kept from M0').
|
// libgr2 sanity probe (kept from M0').
|
||||||
godot::String probe_gr2(const godot::String &path) const;
|
godot::String probe_gr2(const godot::String &path) const;
|
||||||
@@ -95,6 +103,9 @@ private:
|
|||||||
godot::String last_info;
|
godot::String last_info;
|
||||||
godot::HashMap<godot::String, godot::Ref<godot::ImageTexture>> tex_cache;
|
godot::HashMap<godot::String, godot::Ref<godot::ImageTexture>> tex_cache;
|
||||||
|
|
||||||
|
godot::Ref<godot::ArrayMesh> cpu_mesh; // rebuilt each frame in cpu_skin mode
|
||||||
|
std::vector<int> cpu_surf_mesh; // surface -> gr2 mesh index
|
||||||
|
|
||||||
void _clear_children();
|
void _clear_children();
|
||||||
void _apply_materials();
|
void _apply_materials();
|
||||||
godot::String _guess_texture_dir() const;
|
godot::String _guess_texture_dir() const;
|
||||||
|
|||||||
@@ -106,8 +106,11 @@ func _load_content() -> void:
|
|||||||
_assets_root.path_join("PC/ymir work/pc/warrior/warrior_cheongrin.gr2"))
|
_assets_root.path_join("PC/ymir work/pc/warrior/warrior_cheongrin.gr2"))
|
||||||
var tex_dir := _env("MTGODOT_TEXDIR",
|
var tex_dir := _env("MTGODOT_TEXDIR",
|
||||||
_assets_root.path_join("PC/ymir work/pc/warrior"))
|
_assets_root.path_join("PC/ymir work/pc/warrior"))
|
||||||
|
# Default to an idle: dance_1 (and other emotes) hit a libgr2 curve-decode
|
||||||
|
# bug that collapses the head/neck — reproduced in xrender-poc's bgfx demo
|
||||||
|
# too, i.e. upstream, not a Godot-route issue. See docs/MIDREVIEW.md.
|
||||||
var anim_path := _env("MTGODOT_ANIM",
|
var anim_path := _env("MTGODOT_ANIM",
|
||||||
_assets_root.path_join("PC/ymir work/pc/warrior/action/dance_1.gr2"))
|
_assets_root.path_join("PC/ymir work/pc/warrior/general/wait.gr2"))
|
||||||
|
|
||||||
_model = ClassDB.instantiate("Metin2Model")
|
_model = ClassDB.instantiate("Metin2Model")
|
||||||
_model.name = "Metin2Model"
|
_model.name = "Metin2Model"
|
||||||
@@ -144,6 +147,9 @@ func _frame_model() -> void:
|
|||||||
_target = Vector3(aabb.get_center().x, aabb.position.y + h * 0.5, aabb.get_center().z)
|
_target = Vector3(aabb.get_center().x, aabb.position.y + h * 0.5, aabb.get_center().z)
|
||||||
_dist = h * 3.2
|
_dist = h * 3.2
|
||||||
_pitch = 0.12
|
_pitch = 0.12
|
||||||
|
var yenv := OS.get_environment("MTGODOT_YAW")
|
||||||
|
if yenv != "":
|
||||||
|
_yaw = float(yenv)
|
||||||
print("[mtgodot] frame: aabb=", aabb, " target=", _target, " dist=", _dist)
|
print("[mtgodot] frame: aabb=", aabb, " target=", _target, " dist=", _dist)
|
||||||
_update_camera()
|
_update_camera()
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 265 KiB |
|
Before Width: | Height: | Size: 279 KiB |