M2.5: wire libgr2 dump_materials (opt-in), keep filename heuristic default

- Metin2Model stores gr2::dump_materials() and can resolve surface textures
  via tri_groups[0].material_index -> material.diffuse_texture basename,
  gated behind use_gr2_materials (default off): the mapping is unverified and
  currently regresses warrior (body surfaces pick the face texture), so the
  filename-convention + dir-scan path stays the default.
- Property + setter for use_gr2_materials so it can be A/B'd once a Granny
  oracle exists. Logged: "gr2 materials: N (M with diffuse name)".

warrior/assassin/shaman render unchanged from the heuristic path.
docs/GODOT-POC-PLAN.md §M2.5 T2.5.6 gap list already covers the open item.

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:41:32 +09:00
parent eeffa2a730
commit 83c48f0c14
7 changed files with 72 additions and 1 deletions
+66 -1
View File
@@ -43,6 +43,8 @@ void Metin2Model::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_flip_winding"), &Metin2Model::get_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("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("get_material_mode"), &Metin2Model::get_material_mode);
ClassDB::bind_method(D_METHOD("set_use_gr2_materials", "v"), &Metin2Model::set_use_gr2_materials);
ClassDB::bind_method(D_METHOD("get_use_gr2_materials"), &Metin2Model::get_use_gr2_materials);
ClassDB::bind_method(D_METHOD("set_surface_texture", "surface", "path"), &Metin2Model::set_surface_texture); 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("reload"), &Metin2Model::reload);
ClassDB::bind_method(D_METHOD("get_info"), &Metin2Model::get_info); ClassDB::bind_method(D_METHOD("get_info"), &Metin2Model::get_info);
@@ -58,6 +60,8 @@ void Metin2Model::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_winding"), "set_flip_winding", "get_flip_winding"); 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"), ADD_PROPERTY(PropertyInfo(Variant::STRING, "material_mode", PROPERTY_HINT_ENUM, "metin2,standard"),
"set_material_mode", "get_material_mode"); "set_material_mode", "get_material_mode");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_gr2_materials"),
"set_use_gr2_materials", "get_use_gr2_materials");
} }
void Metin2Model::set_material_mode(const String &m) { void Metin2Model::set_material_mode(const String &m) {
@@ -67,6 +71,13 @@ void Metin2Model::set_material_mode(const String &m) {
} }
} }
void Metin2Model::set_use_gr2_materials(bool v) {
use_gr2_materials = v;
if (is_inside_tree()) {
_apply_materials();
}
}
void Metin2Model::set_surface_texture(int surface, const String &path) { void Metin2Model::set_surface_texture(int surface, const String &path) {
if (surface < 0) { if (surface < 0) {
return; return;
@@ -178,12 +189,24 @@ void Metin2Model::reload() {
mi->set_custom_aabb(bounds.grow(0.01)); mi->set_custom_aabb(bounds.grow(0.01));
} }
materials = gr2::dump_materials(**file);
_apply_materials(); _apply_materials();
int vtot = 0; int vtot = 0;
for (const auto &m : fi.meshes) { for (const auto &m : fi.meshes) {
vtot += (int)m.vertices.size(); vtot += (int)m.vertices.size();
} }
{
int with_tex = 0;
for (const auto &mm : materials) {
if (!mm.diffuse_texture.empty()) {
++with_tex;
}
}
UtilityFunctions::print(vformat("[Metin2Model] gr2 materials: %d (%d with diffuse name)",
(int)materials.size(), with_tex));
}
last_info = vformat("gr2 v%d | bones=%d meshes=%d surfaces=%d verts=%d | bounds=%s", last_info = vformat("gr2 v%d | bones=%d meshes=%d surfaces=%d verts=%d | bounds=%s",
(int)(*file)->format_version(), (int)(*file)->format_version(),
sk ? (int)sk->bones.size() : 0, sk ? (int)sk->bones.size() : 0,
@@ -327,14 +350,56 @@ void Metin2Model::_apply_materials() {
} }
} }
// Surface s corresponds to the s-th gr2 mesh that has geometry (same skip
// rule as build_mesh). Recover that mapping to reach tri_groups/material_index.
const gr2::FileInfo &fi = (*file)->file_info();
std::vector<int> surf_to_mesh;
for (int mi_ = 0; mi_ < (int)fi.meshes.size(); ++mi_) {
if (!fi.meshes[mi_].vertices.empty() && !fi.meshes[mi_].indices.empty()) {
surf_to_mesh.push_back(mi_);
}
}
Ref<ArrayMesh> mesh = mi->get_mesh(); Ref<ArrayMesh> mesh = mi->get_mesh();
int textured = 0; int textured = 0;
for (int s = 0; s < mesh->get_surface_count(); ++s) { for (int s = 0; s < mesh->get_surface_count(); ++s) {
const String sname = mesh->surface_get_name(s); const String sname = mesh->surface_get_name(s);
Ref<ImageTexture> tex; Ref<ImageTexture> tex;
auto mat_texture_name = [&]() -> String {
if (s >= (int)surf_to_mesh.size()) {
return String();
}
const gr2::Mesh &gm = fi.meshes[surf_to_mesh[s]];
int matidx = gm.tri_groups.empty() ? -1 : gm.tri_groups[0].material_index;
if (matidx < 0 || matidx >= (int)materials.size() ||
materials[matidx].diffuse_texture.empty()) {
return String();
}
return String(materials[matidx].diffuse_texture.c_str())
.replace("\\", "/")
.get_file()
.get_basename()
.to_lower();
};
if (s < surface_tex_override.size() && !String(surface_tex_override[s]).is_empty()) { if (s < surface_tex_override.size() && !String(surface_tex_override[s]).is_empty()) {
tex = _load_dds(surface_tex_override[s]); tex = _load_dds(surface_tex_override[s]);
} else { }
// gr2 material name -> texture, only when opted in (unverified vs oracle;
// see GODOT-POC-PLAN.md §M2.5 T2.5.6).
if (tex.is_null() && use_gr2_materials) {
String want = mat_texture_name();
if (!want.is_empty()) {
for (int i = 0; i < dds_files.size(); ++i) {
if (dds_files[i].get_basename().to_lower() == want) {
tex = _load_dds(dir.path_join(dds_files[i]));
break;
}
}
}
}
// filename-convention resolution (default path)
if (tex.is_null()) {
tex = _resolve_texture(dir, stem, sname, dds_files); tex = _resolve_texture(dir, stem, sname, dds_files);
} }
if (tex.is_valid()) { if (tex.is_valid()) {
+6
View File
@@ -11,6 +11,7 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <vector>
namespace godot { namespace godot {
class Skeleton3D; class Skeleton3D;
@@ -54,6 +55,9 @@ public:
void set_material_mode(const godot::String &m); void set_material_mode(const godot::String &m);
godot::String get_material_mode() const { return material_mode; } godot::String get_material_mode() const { return material_mode; }
void set_use_gr2_materials(bool v);
bool get_use_gr2_materials() const { return use_gr2_materials; }
// Explicit per-surface texture path override (index -> absolute .dds path). // Explicit per-surface texture path override (index -> absolute .dds path).
void set_surface_texture(int surface, const godot::String &path); void set_surface_texture(int surface, const godot::String &path);
@@ -81,9 +85,11 @@ private:
bool flip_winding = false; bool flip_winding = false;
godot::String material_mode = "metin2"; // "metin2" (ShaderMaterial) | "standard" godot::String material_mode = "metin2"; // "metin2" (ShaderMaterial) | "standard"
bool use_gr2_materials = false; // drive textures from gr2 material names (unverified)
godot::PackedStringArray surface_tex_override; godot::PackedStringArray surface_tex_override;
std::shared_ptr<std::optional<gr2::File>> file; // shared_ptr so accessor stays valid std::shared_ptr<std::optional<gr2::File>> file; // shared_ptr so accessor stays valid
std::vector<gr2::MaterialInfo> materials;
godot::Skeleton3D *skel = nullptr; godot::Skeleton3D *skel = nullptr;
godot::MeshInstance3D *mi = nullptr; godot::MeshInstance3D *mi = nullptr;
godot::String last_info; godot::String last_info;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB