diff --git a/.cache/clangd/index/dxt.cpp.974FAAEF0207427F.idx b/.cache/clangd/index/dxt.cpp.974FAAEF0207427F.idx new file mode 100644 index 00000000..21c5d402 Binary files /dev/null and b/.cache/clangd/index/dxt.cpp.974FAAEF0207427F.idx differ diff --git a/.cache/clangd/index/dxt.h.3425394495729F40.idx b/.cache/clangd/index/dxt.h.3425394495729F40.idx new file mode 100644 index 00000000..bfc549cd Binary files /dev/null and b/.cache/clangd/index/dxt.h.3425394495729F40.idx differ diff --git a/.cache/clangd/index/gr2_bridge.cpp.A7F116F78CD637EA.idx b/.cache/clangd/index/gr2_bridge.cpp.A7F116F78CD637EA.idx new file mode 100644 index 00000000..7a5ce36d Binary files /dev/null and b/.cache/clangd/index/gr2_bridge.cpp.A7F116F78CD637EA.idx differ diff --git a/.cache/clangd/index/gr2_bridge.h.67D793DE305C05F1.idx b/.cache/clangd/index/gr2_bridge.h.67D793DE305C05F1.idx new file mode 100644 index 00000000..8ceb5997 Binary files /dev/null and b/.cache/clangd/index/gr2_bridge.h.67D793DE305C05F1.idx differ diff --git a/.cache/clangd/index/m2_material.cpp.0F98125235A4876E.idx b/.cache/clangd/index/m2_material.cpp.0F98125235A4876E.idx new file mode 100644 index 00000000..b863ed1c Binary files /dev/null and b/.cache/clangd/index/m2_material.cpp.0F98125235A4876E.idx differ diff --git a/.cache/clangd/index/m2_material.h.E2737E7F8903D417.idx b/.cache/clangd/index/m2_material.h.E2737E7F8903D417.idx new file mode 100644 index 00000000..d8999e8d Binary files /dev/null and b/.cache/clangd/index/m2_material.h.E2737E7F8903D417.idx differ diff --git a/.cache/clangd/index/metin2_anim.cpp.E1BCEBCDA5BD2788.idx b/.cache/clangd/index/metin2_anim.cpp.E1BCEBCDA5BD2788.idx new file mode 100644 index 00000000..1e903b07 Binary files /dev/null and b/.cache/clangd/index/metin2_anim.cpp.E1BCEBCDA5BD2788.idx differ diff --git a/.cache/clangd/index/metin2_anim.h.AC61FB6D4081763E.idx b/.cache/clangd/index/metin2_anim.h.AC61FB6D4081763E.idx new file mode 100644 index 00000000..2c5d2c3f Binary files /dev/null and b/.cache/clangd/index/metin2_anim.h.AC61FB6D4081763E.idx differ diff --git a/.cache/clangd/index/metin2_model.h.7BE7F24D98D14274.idx b/.cache/clangd/index/metin2_model.h.7BE7F24D98D14274.idx index 6aeea36c..2953810d 100644 Binary files a/.cache/clangd/index/metin2_model.h.7BE7F24D98D14274.idx and b/.cache/clangd/index/metin2_model.h.7BE7F24D98D14274.idx differ diff --git a/README.md b/README.md index 95c4fc66..fa72365b 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,22 @@ ## 现状 -- [~] **M0'** — 脚手架(macOS)。本机已验证: - - **T0.1** GDExtension 加载、`Metin2Model` 注册成功 - (`[mtgodot] Metin2Model registered OK`)。 - - **T0.2** `libgr2` 链接进扩展且可调用:`probe_gr2(warrior_cheongrin.gr2) - → gr2 OK: format_version=6 sections=8`。 - - **T0.3** Godot 4.7 工程(orbit 相机 + `DirectionalLight3D` 阴影 + - `WorldEnvironment` procedural sky + 占位 cube)经 Metal Forward+ 出画 - (`test/golden/m0-scaffold.png`)。 - - **T0.4** `export_presets.cfg`(macOS)就位;实际 `export` 待安装 4.7.1 - 导出模板(`godot --headless --install-export-templates`)。 +- [x] **M0'** — 脚手架。GDExtension 加载 + `Metin2Model` 注册 + `libgr2` 链接可调用 + + Godot 4.7 工程经 Metal Forward+ 出画。T0.4 实际 `export` 待装 4.7.1 导出模板。 +- [x] **M1** — 静态渲染。`gr2` → `Skeleton3D` + `ArrayMesh` + `Skin` + 材质。 + `warrior_cheongrin`(75 骨 / 5 表面 / 3324 顶点)正立、朝 +Z、贴图正确 + (`test/golden/m1-bindpose.png`)。多骨架:`warrior_lord`(v6,74)、`assassin`(v6,95,17 表面)、 + `shaman_lord`(**v7**,93) 均正确加载渲染。 +- [x] **M2** — 骨骼动画。`Metin2AnimPlayer` `_process` 里 `gr2::sample_pose`(跨文件) + → `Skeleton3D.set_bone_global_pose`,Godot GPU 蒙皮。`dance01`(28.3s / 74 track) + 播放,`selfcheck` 25 采样 0 NaN,t=2/9/16/23 姿势各异(`test/golden/m2c-*.png`)。 +- [~] **M2.5** — 材质与观感保真。`ShaderMaterial`(modulate tex×COLOR×light、 + opaque/alpha/alpha-test/add 四模式)+ 目录扫描贴图解析 + 方向光阴影 + procedural sky + + 引擎雾(`test/golden/m25-*.png`)。**门禁未闭**:多材质槽 → 贴图/混合的精确映射需 + 给 libgr2 加材质 API(见 `docs/GODOT-POC-PLAN.md` §M2.5 T2.5.6 差距清单)。 +- [ ] **中期评审** — 待 M2.5 门禁 + 对拍报告 + +对拍(与 `xrender-poc` bgfx demo / oracle 逐帧)尚未接线,是中期评审前的下一步。 --- diff --git a/docs/GODOT-POC-PLAN.md b/docs/GODOT-POC-PLAN.md index 8192e193..02865cda 100644 --- a/docs/GODOT-POC-PLAN.md +++ b/docs/GODOT-POC-PLAN.md @@ -294,5 +294,17 @@ mtgodot-poc/ ## 选型变更记录 - 2026-08-29 · 范围收敛为 **Phase 1 = macOS only**;新增 **M2.5 材质与观感保真** 作为 Phase 1 核心门禁;动画 A/B 决策推迟到 Phase 2;Godot 版本钉死 4.7.1 / godot-cpp 4.7。 -- *(待 M1 T1.3 填写:贴图走 `.dds` 原生导入 还是运行时 `dds_loader`)* -- *(待 M2.5 T2.5.6 填写:材质无法 1:1 还原项清单)* +- 2026-08-29 · **M1 T1.3**:贴图走 **运行时 `dds_loader`**(`src/dxt.cpp`,端口自 xrender-poc)。理由:Godot 4 的 `Image.load()` 不支持 `.dds`,且资产从 `res://` 外的绝对路径加载(`.gr2` 走 `.gdignore` 同理)。DXT1/3/5 + BGRA8,level 0 + `generate_mipmaps()`。 +- 2026-08-29 · **动画方案**:确认走 **B 方案** —— `_process` 里 `gr2::sample_pose` → `Skeleton3D.set_bone_global_pose(i, gr2_to_godot(world[i]))`,Godot GPU 蒙皮 `skin_matrix = global_pose(i) * bind_pose(i)` 恒等于 gr2 deformer 矩阵(`gr2_bridge.h` 有推导)。A 方案(烘 `Animation`)仍推迟到 Phase 2 按多角色 CPU 采样决定。 +- 2026-08-29 · **坐标/单位**:`gr2_to_godot()` = gr2 行主序 Mat4 的 4×4 转置 → Godot `Transform3D`;Z-up cm → Y-up m(`rotate(-90°,X)` + `scale(0.01)`)放在 `Metin2Model` 节点自身 transform(`make_conv`)。warrior/warrior_lord/assassin/shaman_lord 实测 `flip_z=false`、`flip_winding=false`、`unit_scale=0.01` 正立、朝 +Z、无镜像。 + +### M2.5 T2.5.6 — 材质无法 1:1 还原 / 待办项清单 + +| 项 | 现状 | 影响 | 出路 | +|---|---|---|---| +| **libgr2 POD API 不暴露材质数据** —— 只有 `material_count`/`texture_count`,无材质名、无贴图文件名、无 texture-stage op、无 `D3DBLEND_*`/alpha-test 标志 | 贴图靠**文件名约定 + 目录大小写不敏感扫描**(`_resolve_texture`);blend 靠**表面名关键字启发式**(`guess_blend`:hair→alpha-test、cape/skirt→alpha、effect/glow→add,其余 opaque) | warrior 5/5、assassin 17/17、shaman 1/1 表面命中贴图,但**多材质槽 → 具体贴图/混合的精确映射做不到**;换皮/多贴图变体、精确 alpha-test 阈值、MODULATE2X/ADDSIGNED 等 stage op 无法复现 | **需要给 libgr2 加材质 API**(新文件 `gr2_material.cpp`,走类型树取 `Materials[].Name` + `Maps[].Texture.FromFileName` + shader/blend hint)。这也正是 xrender-poc M1 T5「多 stage 材质对拍待 oracle」的同一缺口,两条 route 共用修法。 | +| **顶点色** | `gr2::Vertex` POD 无 color 字段;shader 已留 `COLOR` 通道,默认白 | Metin2 PC 模型基本不用顶点色,影响小;特效/地形会用 | libgr2 若暴露则 `build_mesh` 补 `ARRAY_COLOR` | +| **`TriGroup.material_index` 未接线** | libgr2 已解析每个三角组的 material_index,但 `build_mesh` 目前一个 gr2 mesh = 一个 surface(未按 material_index 再拆分) | 单 mesh 内多材质的模型会用同一张贴图 | `build_mesh` 按 `tri_groups` 的 material_index 分 surface(有材质 API 后一起做) | +| **ShaderMaterial 静态 `Shader` 退出泄漏** | `shader_for()` 用 function-static `Ref`,Godot 退出时报 "1 shader never freed" | 仅退出时一条 warning,无功能影响 | 改成每材质自持 Shader,或注册 cleanup | +| **alpha 排序 corner case** | 半透明/additive 用 `render_priority` 粗排,未做逐三角/OIT | 多层半透明部件可能穿插 | 记录,不追求 100%(Phase 2 M2.5 打磨) | +| **法线未做逆转置** | 走 Godot GPU 蒙皮,其法线处理与 Granny runtime 的「上 3×3 不逆转置」近似不完全一致 | 非均匀缩放骨骼下法线略有差异;warrior 类资产无明显问题 | Phase 2 逐帧对拍时量化 | diff --git a/extension/CMakeLists.txt b/extension/CMakeLists.txt index 7cbd58f9..9362ba63 100644 --- a/extension/CMakeLists.txt +++ b/extension/CMakeLists.txt @@ -25,6 +25,7 @@ add_library(mtgodot SHARED src/metin2_model.cpp src/metin2_anim.cpp src/gr2_bridge.cpp + src/m2_material.cpp src/dxt.cpp ) target_compile_features(mtgodot PRIVATE cxx_std_20) diff --git a/extension/src/m2_material.cpp b/extension/src/m2_material.cpp new file mode 100644 index 00000000..1380d905 --- /dev/null +++ b/extension/src/m2_material.cpp @@ -0,0 +1,95 @@ +#include "m2_material.h" + +#include +#include +#include +#include + +using namespace godot; + +namespace mtgodot { +namespace { + +// blend_mix shader: covers opaque (mode 0, alpha-scissor) and alpha (mode 1). +const char *SRC_MIX = R"(shader_type spatial; +render_mode blend_mix, depth_draw_opaque, cull_disabled, diffuse_lambert, specular_disabled; + +uniform sampler2D albedo_tex : source_color, filter_linear_mipmap, repeat_enable; +uniform bool use_texture = true; +uniform vec4 modulate : source_color = vec4(1.0); +uniform float alpha_scissor : hint_range(0.0, 1.0) = 0.5; +uniform int mode = 0; // 0 opaque | 1 alpha-blend | 2 alpha-test (cutout) + +void fragment() { + vec4 c = use_texture ? texture(albedo_tex, UV) : vec4(1.0); + c *= modulate * COLOR; + if (mode == 2 && c.a < alpha_scissor) { + discard; + } + ALBEDO = c.rgb; + ALPHA = (mode == 1) ? c.a : 1.0; +} +)"; + +// blend_add shader: additive glow (effects, enchant overlays). +const char *SRC_ADD = R"(shader_type spatial; +render_mode blend_add, depth_draw_opaque, depth_test_disabled, cull_disabled, unshaded; + +uniform sampler2D albedo_tex : source_color, filter_linear_mipmap, repeat_enable; +uniform bool use_texture = true; +uniform vec4 modulate : source_color = vec4(1.0); + +void fragment() { + vec4 c = use_texture ? texture(albedo_tex, UV) : vec4(1.0); + c *= modulate * COLOR; + ALBEDO = c.rgb * c.a; + ALPHA = 1.0; +} +)"; + +Ref shader_for(bool additive) { + static Ref s_mix; + static Ref s_add; + Ref &slot = additive ? s_add : s_mix; + if (slot.is_null()) { + slot.instantiate(); + slot->set_code(additive ? SRC_ADD : SRC_MIX); + } + return slot; +} + +} // namespace + +Ref make_material(const MaterialDesc &d) { + Ref m; + m.instantiate(); + + const bool additive = (d.blend == BlendMode::Add); + m->set_shader(shader_for(additive)); + + const bool has_tex = d.albedo.is_valid(); + m->set_shader_parameter("use_texture", has_tex); + if (has_tex) { + m->set_shader_parameter("albedo_tex", d.albedo); + } + m->set_shader_parameter("modulate", has_tex ? Color(1, 1, 1, 1) : Color(0.8, 0.8, 0.82, 1)); + + if (!additive) { + m->set_shader_parameter("alpha_scissor", d.alpha_scissor); + int mode = 0; + if (d.blend == BlendMode::Alpha) { + mode = 1; + } else if (d.blend == BlendMode::AlphaTest) { + mode = 2; + } + m->set_shader_parameter("mode", mode); + } + if (d.blend == BlendMode::Alpha) { + m->set_render_priority(1); + } else if (additive) { + m->set_render_priority(2); + } + return m; +} + +} // namespace mtgodot diff --git a/extension/src/m2_material.h b/extension/src/m2_material.h new file mode 100644 index 00000000..8b782b0d --- /dev/null +++ b/extension/src/m2_material.h @@ -0,0 +1,35 @@ +#pragma once +// m2_material — Metin2-style forward material as a Godot ShaderMaterial. +// +// M2.5 infrastructure. The gr2 fixed-function texture-stage / blend data is not +// yet exposed by libgr2's POD API (see docs/GODOT-POC-PLAN.md §M2.5 gap list), +// so callers pick the blend mode heuristically for now. The shader itself +// already covers the common Metin2 cases: modulate(tex, vertex_color) + light, +// alpha-test, alpha-blend, additive. + +#include + +namespace godot { +class ShaderMaterial; +class Texture2D; +} // namespace godot + +namespace mtgodot { + +enum class BlendMode { + Opaque, // blend_mix, ALPHA=1, no discard + Alpha, // blend_mix, sorted, uses texture alpha + AlphaTest, // blend_mix, discard below alpha_scissor (cutout) + Add, // blend_add (glow / effects) +}; + +struct MaterialDesc { + godot::Ref albedo; // null -> flat white + BlendMode blend = BlendMode::Opaque; + float alpha_scissor = 0.5f; + bool two_sided = true; // winding not yet verified per-model +}; + +godot::Ref make_material(const MaterialDesc &d); + +} // namespace mtgodot diff --git a/extension/src/metin2_model.cpp b/extension/src/metin2_model.cpp index 632806df..d55936bd 100644 --- a/extension/src/metin2_model.cpp +++ b/extension/src/metin2_model.cpp @@ -2,13 +2,16 @@ #include "dxt.h" #include "gr2_bridge.h" +#include "m2_material.h" #include #include +#include #include #include #include #include +#include #include #include #include @@ -38,6 +41,9 @@ void Metin2Model::_bind_methods() { 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("set_material_mode", "m"), &Metin2Model::set_material_mode); + ClassDB::bind_method(D_METHOD("get_material_mode"), &Metin2Model::get_material_mode); + ClassDB::bind_method(D_METHOD("set_surface_texture", "surface", "path"), &Metin2Model::set_surface_texture); 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); @@ -50,6 +56,28 @@ void Metin2Model::_bind_methods() { "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"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "material_mode", PROPERTY_HINT_ENUM, "metin2,standard"), + "set_material_mode", "get_material_mode"); +} + +void Metin2Model::set_material_mode(const String &m) { + material_mode = m; + if (is_inside_tree()) { + reload(); + } +} + +void Metin2Model::set_surface_texture(int surface, const String &path) { + if (surface < 0) { + return; + } + while (surface_tex_override.size() <= surface) { + surface_tex_override.push_back(String()); + } + surface_tex_override[surface] = path; + if (is_inside_tree()) { + _apply_materials(); + } } void Metin2Model::set_gr2_path(const String &p) { @@ -173,58 +201,165 @@ String Metin2Model::_guess_texture_dir() const { return gr2_path.get_base_dir(); } +Ref Metin2Model::_load_dds(const String &path) { + Ref t; + if (tex_cache.has(path)) { + return tex_cache[path]; + } + if (FileAccess::file_exists(path)) { + mtgodot::Image dds = mtgodot::load_dds_path(path.utf8().get_data()); + if (dds.ok()) { + PackedByteArray bytes; + bytes.resize((int)dds.rgba.size()); + memcpy(bytes.ptrw(), dds.rgba.data(), dds.rgba.size()); + Ref img = godot::Image::create_from_data( + dds.w, dds.h, false, godot::Image::FORMAT_RGBA8, bytes); + if (img.is_valid()) { + img->generate_mipmaps(); + t = ImageTexture::create_from_image(img); + } + } + } + tex_cache[path] = t; + return t; +} + +// Best-effort texture pick for a surface. libgr2's POD API gives no material or +// texture names yet (see GODOT-POC-PLAN.md §M2.5 gap list), so this is filename +// convention + a case-insensitive directory scan. +Ref Metin2Model::_resolve_texture(const String &dir, const String &stem, + const String &surface_name, const PackedStringArray &dds_files) { + String sn = surface_name.to_lower(); + + auto scan = [&](const String &needle) -> Ref { + for (int i = 0; i < dds_files.size(); ++i) { + if (dds_files[i].to_lower().find(needle) != -1) { + Ref t = _load_dds(dir.path_join(dds_files[i])); + if (t.is_valid()) { + return t; + } + } + } + return Ref(); + }; + + // 1. surface-name keyword slots + if (sn.find("face") != -1) { + Ref t = _load_dds(dir.path_join(stem + String("_face.dds"))); + if (t.is_null()) { + t = scan("face"); + } + if (t.is_valid()) { + return t; + } + } + if (sn.find("hair") != -1) { + Ref t = _load_dds(dir.path_join(stem + String("_hair.dds"))); + if (t.is_null()) { + t = scan("hair"); + } + if (t.is_valid()) { + return t; + } + } + // 2. surface name used verbatim as a texture basename + if (!sn.is_empty()) { + Ref t = _load_dds(dir.path_join(surface_name + String(".dds"))); + if (t.is_valid()) { + return t; + } + } + // 3. exact stem + Ref t = _load_dds(dir.path_join(stem + String(".dds"))); + if (t.is_valid()) { + return t; + } + // 4. any dds whose name contains the stem, then any non-face/hair dds + t = scan(stem); + if (t.is_valid()) { + return t; + } + for (int i = 0; i < dds_files.size(); ++i) { + String f = dds_files[i].to_lower(); + if (f.find("face") == -1 && f.find("hair") == -1 && f.find("_lod") == -1) { + Ref any = _load_dds(dir.path_join(dds_files[i])); + if (any.is_valid()) { + return any; + } + } + } + return Ref(); +} + +static mtgodot::BlendMode guess_blend(const String &surface_name) { + String sn = surface_name.to_lower(); + if (sn.find("effect") != -1 || sn.find("glow") != -1 || sn.find("aura") != -1) { + return mtgodot::BlendMode::Add; + } + if (sn.find("hair") != -1) { + return mtgodot::BlendMode::AlphaTest; // Metin2 hair = alpha-test cutout + } + if (sn.find("cloak") != -1 || sn.find("cape") != -1 || + sn.find("skirt") != -1 || sn.find("ribbon") != -1) { + return mtgodot::BlendMode::Alpha; + } + return mtgodot::BlendMode::Opaque; +} + 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 { - Ref t; - if (!FileAccess::file_exists(path)) { - return t; + PackedStringArray dds_files; + { + Ref da = DirAccess::open(dir); + if (da.is_valid()) { + da->list_dir_begin(); + for (String f = da->get_next(); !f.is_empty(); f = da->get_next()) { + if (!da->current_is_dir() && f.to_lower().ends_with(".dds")) { + dds_files.push_back(f); + } + } + da->list_dir_end(); } - 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 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 tex_main = load_tex(dir.path_join(stem + String(".dds"))); - Ref 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 mesh = mi->get_mesh(); + int textured = 0; for (int s = 0; s < mesh->get_surface_count(); ++s) { - String sname = mesh->surface_get_name(s).to_lower(); - Ref tex = (sname.find("face") != -1 && tex_face.is_valid()) ? tex_face : tex_main; - - Ref mat; - mat.instantiate(); - mat->set_cull_mode(BaseMaterial3D::CULL_DISABLED); // M1: winding unverified - if (tex.is_valid()) { - mat->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, tex); + const String sname = mesh->surface_get_name(s); + Ref tex; + if (s < surface_tex_override.size() && !String(surface_tex_override[s]).is_empty()) { + tex = _load_dds(surface_tex_override[s]); } else { - mat->set_albedo(Color(0.8, 0.8, 0.82)); + tex = _resolve_texture(dir, stem, sname, dds_files); + } + if (tex.is_valid()) { + ++textured; + } + + if (material_mode == String("standard")) { + Ref mat; + mat.instantiate(); + mat->set_cull_mode(BaseMaterial3D::CULL_DISABLED); + 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); + } else { + mtgodot::MaterialDesc md; + md.albedo = tex; + md.blend = guess_blend(sname); + mi->set_surface_override_material(s, mtgodot::make_material(md)); } - mi->set_surface_override_material(s, mat); } + UtilityFunctions::print(vformat("[Metin2Model] materials: %d/%d surfaces textured (dir=%s, %d dds)", + textured, mesh->get_surface_count(), dir, (int)dds_files.size())); } const gr2::Skeleton *Metin2Model::gr2_skeleton() const { diff --git a/extension/src/metin2_model.h b/extension/src/metin2_model.h index 14fee5f0..026be84f 100644 --- a/extension/src/metin2_model.h +++ b/extension/src/metin2_model.h @@ -1,6 +1,10 @@ #pragma once +#include #include +#include +#include +#include #include #include @@ -11,6 +15,7 @@ namespace godot { class Skeleton3D; class MeshInstance3D; +class ImageTexture; } // namespace godot namespace mtgodot { @@ -46,6 +51,12 @@ public: void set_flip_winding(bool v); bool get_flip_winding() const { return flip_winding; } + void set_material_mode(const godot::String &m); + godot::String get_material_mode() const { return material_mode; } + + // Explicit per-surface texture path override (index -> absolute .dds path). + void set_surface_texture(int surface, const godot::String &path); + // Rebuild the subtree from the current properties. void reload(); @@ -69,14 +80,22 @@ private: bool flip_z = false; bool flip_winding = false; + godot::String material_mode = "metin2"; // "metin2" (ShaderMaterial) | "standard" + godot::PackedStringArray surface_tex_override; + std::shared_ptr> file; // shared_ptr so accessor stays valid godot::Skeleton3D *skel = nullptr; godot::MeshInstance3D *mi = nullptr; godot::String last_info; + godot::HashMap> tex_cache; void _clear_children(); void _apply_materials(); godot::String _guess_texture_dir() const; + godot::Ref _load_dds(const godot::String &path); + godot::Ref _resolve_texture(const godot::String &dir, + const godot::String &stem, const godot::String &surface_name, + const godot::PackedStringArray &dds_files); }; } // namespace mtgodot diff --git a/project/main.gd b/project/main.gd index ed31ef2e..5585411a 100644 --- a/project/main.gd +++ b/project/main.gd @@ -56,6 +56,15 @@ func _build_environment() -> void: env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY env.ambient_light_energy = 0.6 env.tonemap_mode = Environment.TONE_MAPPER_FILMIC + # T2.5.4 — distance fog via the engine (not per-shader). + if OS.get_environment("MTGODOT_FOG") == "1": + env.fog_enabled = true + env.fog_mode = Environment.FOG_MODE_DEPTH + env.fog_light_color = Color(0.55, 0.55, 0.62) + env.fog_sun_scatter = 0.1 + env.fog_density = 0.0 + env.fog_depth_begin = 3.0 + env.fog_depth_end = 12.0 we.environment = env add_child(we) @@ -121,13 +130,15 @@ func _load_content() -> void: func _frame_model() -> void: - # Use the model's converted world-space AABB to place the orbit target/dist. + # Place the orbit target/dist from the model's converted world-space AABB. var mi := _model.get_node_or_null("MeshInstance3D") if _model else null if mi and mi is VisualInstance3D: - var aabb: AABB = (mi as VisualInstance3D).get_aabb() - aabb = _model.transform * aabb - _target = aabb.get_center() - _dist = maxf(1.0, aabb.get_longest_axis_size() * 1.8) + var aabb: AABB = _model.transform * (mi as VisualInstance3D).get_aabb() + var h := maxf(aabb.size.y, 0.5) + _target = Vector3(aabb.get_center().x, aabb.position.y + h * 0.5, aabb.get_center().z) + _dist = h * 3.2 + _pitch = 0.12 + print("[mtgodot] frame: aabb=", aabb, " target=", _target, " dist=", _dist) _update_camera() diff --git a/test/golden/m1-warrior.png b/test/golden/m1-warrior.png index 4baa9d26..a95462d1 100644 Binary files a/test/golden/m1-warrior.png and b/test/golden/m1-warrior.png differ diff --git a/test/golden/m2-dance-t0.0.png b/test/golden/m2-dance-t0.0.png deleted file mode 100644 index e391738a..00000000 Binary files a/test/golden/m2-dance-t0.0.png and /dev/null differ diff --git a/test/golden/m2-dance-t12.0.png b/test/golden/m2-dance-t12.0.png deleted file mode 100644 index e391738a..00000000 Binary files a/test/golden/m2-dance-t12.0.png and /dev/null differ diff --git a/test/golden/m2-dance-t3.5.png b/test/golden/m2-dance-t3.5.png deleted file mode 100644 index e391738a..00000000 Binary files a/test/golden/m2-dance-t3.5.png and /dev/null differ diff --git a/test/golden/m2-dance-t7.0.png b/test/golden/m2-dance-t7.0.png deleted file mode 100644 index e391738a..00000000 Binary files a/test/golden/m2-dance-t7.0.png and /dev/null differ diff --git a/test/golden/m25-assassin.png b/test/golden/m25-assassin.png new file mode 100644 index 00000000..a7145f0e Binary files /dev/null and b/test/golden/m25-assassin.png differ diff --git a/test/golden/m25-shaman.png b/test/golden/m25-shaman.png new file mode 100644 index 00000000..cea431dc Binary files /dev/null and b/test/golden/m25-shaman.png differ diff --git a/test/golden/m25-warrior-fog.png b/test/golden/m25-warrior-fog.png new file mode 100644 index 00000000..998edddb Binary files /dev/null and b/test/golden/m25-warrior-fog.png differ diff --git a/test/golden/m25-warrior-shader.png b/test/golden/m25-warrior-shader.png new file mode 100644 index 00000000..133234cf Binary files /dev/null and b/test/golden/m25-warrior-shader.png differ diff --git a/test/golden/m2b-t16.0.png b/test/golden/m2b-t16.0.png deleted file mode 100644 index e391738a..00000000 Binary files a/test/golden/m2b-t16.0.png and /dev/null differ diff --git a/test/golden/m2b-t2.0.png b/test/golden/m2b-t2.0.png deleted file mode 100644 index e391738a..00000000 Binary files a/test/golden/m2b-t2.0.png and /dev/null differ diff --git a/test/golden/m2b-t9.0.png b/test/golden/m2b-t9.0.png deleted file mode 100644 index e391738a..00000000 Binary files a/test/golden/m2b-t9.0.png and /dev/null differ