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
@@ -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()) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 277 KiB |