M2.5: ShaderMaterial + texture resolution + fog; multi-skeleton verified
- m2_material: Metin2-style ShaderMaterial. blend_mix shader (opaque /
alpha-blend / alpha-test-cutout via `mode` uniform) + blend_add shader
(additive glow). modulate(tex, COORD) * COLOR, diffuse_lambert.
- Metin2Model: material_mode prop ("metin2" ShaderMaterial | "standard");
_resolve_texture does filename-convention + case-insensitive dir scan of
*.dds (face/hair keyword slots, stem match, fallback); set_surface_texture
override; guess_blend heuristic (hair->cutout, cape->alpha, effect->add).
Texture cache. Fixed opaque path to NOT alpha-discard (was hiding bodies).
- harness: AABB-based camera framing, engine depth fog (MTGODOT_FOG=1),
MTGODOT_ANIM_T fixed-pose capture (needs explicit anim reload first),
MTGODOT_AUTOSHOT batch screenshot.
Verified: warrior 5/5 surfaces textured under the shader (t=9 dance pose,
lit, fogged). assassin 17/17 textured via dir scan. shaman_lord (gr2 v7)
renders. Screenshots test/golden/m25-*.png, m2c-t*.png.
Gap (M2.5 gate not closed): libgr2 POD API exposes no material/texture-name/
blend data, so multi-slot -> texture/blend mapping is heuristic only. Needs
a libgr2 material API (gr2_material.cpp walking the type tree) — same gap as
xrender-poc M1 T5. Documented in docs/GODOT-POC-PLAN.md §M2.5 T2.5.6.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaHYEY9rwLWt21PULiYjeJ
This commit is contained in:
+172
-37
@@ -2,13 +2,16 @@
|
||||
|
||||
#include "dxt.h"
|
||||
#include "gr2_bridge.h"
|
||||
#include "m2_material.h"
|
||||
|
||||
#include <godot_cpp/classes/array_mesh.hpp>
|
||||
#include <godot_cpp/classes/base_material3d.hpp>
|
||||
#include <godot_cpp/classes/dir_access.hpp>
|
||||
#include <godot_cpp/classes/file_access.hpp>
|
||||
#include <godot_cpp/classes/image.hpp>
|
||||
#include <godot_cpp/classes/image_texture.hpp>
|
||||
#include <godot_cpp/classes/mesh_instance3d.hpp>
|
||||
#include <godot_cpp/classes/shader_material.hpp>
|
||||
#include <godot_cpp/classes/skeleton3d.hpp>
|
||||
#include <godot_cpp/classes/skin.hpp>
|
||||
#include <godot_cpp/classes/standard_material3d.hpp>
|
||||
@@ -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<godot::ImageTexture> Metin2Model::_load_dds(const String &path) {
|
||||
Ref<ImageTexture> 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<godot::Image> 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<godot::ImageTexture> 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<ImageTexture> {
|
||||
for (int i = 0; i < dds_files.size(); ++i) {
|
||||
if (dds_files[i].to_lower().find(needle) != -1) {
|
||||
Ref<ImageTexture> t = _load_dds(dir.path_join(dds_files[i]));
|
||||
if (t.is_valid()) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ref<ImageTexture>();
|
||||
};
|
||||
|
||||
// 1. surface-name keyword slots
|
||||
if (sn.find("face") != -1) {
|
||||
Ref<ImageTexture> 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<ImageTexture> 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<ImageTexture> t = _load_dds(dir.path_join(surface_name + String(".dds")));
|
||||
if (t.is_valid()) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
// 3. exact stem
|
||||
Ref<ImageTexture> 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<ImageTexture> any = _load_dds(dir.path_join(dds_files[i]));
|
||||
if (any.is_valid()) {
|
||||
return any;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ref<ImageTexture>();
|
||||
}
|
||||
|
||||
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<ImageTexture> {
|
||||
Ref<ImageTexture> t;
|
||||
if (!FileAccess::file_exists(path)) {
|
||||
return t;
|
||||
PackedStringArray dds_files;
|
||||
{
|
||||
Ref<DirAccess> 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<godot::Image> 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<ImageTexture> tex_main = load_tex(dir.path_join(stem + String(".dds")));
|
||||
Ref<ImageTexture> 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<ArrayMesh> 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<ImageTexture> tex = (sname.find("face") != -1 && tex_face.is_valid()) ? tex_face : tex_main;
|
||||
|
||||
Ref<StandardMaterial3D> 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<ImageTexture> 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<StandardMaterial3D> 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 {
|
||||
|
||||
Reference in New Issue
Block a user