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
577 lines
17 KiB
C++
577 lines
17 KiB
C++
#include "metin2_model.h"
|
|
|
|
#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/os.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>
|
|
#include <godot_cpp/core/class_db.hpp>
|
|
#include <godot_cpp/variant/packed_byte_array.hpp>
|
|
#include <godot_cpp/variant/utility_functions.hpp>
|
|
|
|
#include <string>
|
|
|
|
using namespace godot;
|
|
|
|
namespace mtgodot {
|
|
|
|
Metin2Model::Metin2Model() {
|
|
file = std::make_shared<std::optional<gr2::File>>();
|
|
}
|
|
Metin2Model::~Metin2Model() = default;
|
|
|
|
void Metin2Model::_bind_methods() {
|
|
ClassDB::bind_method(D_METHOD("set_gr2_path", "path"), &Metin2Model::set_gr2_path);
|
|
ClassDB::bind_method(D_METHOD("get_gr2_path"), &Metin2Model::get_gr2_path);
|
|
ClassDB::bind_method(D_METHOD("set_texture_dir", "path"), &Metin2Model::set_texture_dir);
|
|
ClassDB::bind_method(D_METHOD("get_texture_dir"), &Metin2Model::get_texture_dir);
|
|
ClassDB::bind_method(D_METHOD("set_unit_scale", "s"), &Metin2Model::set_unit_scale);
|
|
ClassDB::bind_method(D_METHOD("get_unit_scale"), &Metin2Model::get_unit_scale);
|
|
ClassDB::bind_method(D_METHOD("set_flip_z", "v"), &Metin2Model::set_flip_z);
|
|
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_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("reload"), &Metin2Model::reload);
|
|
ClassDB::bind_method(D_METHOD("get_info"), &Metin2Model::get_info);
|
|
ClassDB::bind_method(D_METHOD("probe_gr2", "path"), &Metin2Model::probe_gr2);
|
|
|
|
ADD_PROPERTY(PropertyInfo(Variant::STRING, "gr2_path", PROPERTY_HINT_GLOBAL_FILE, "*.gr2"),
|
|
"set_gr2_path", "get_gr2_path");
|
|
ADD_PROPERTY(PropertyInfo(Variant::STRING, "texture_dir", PROPERTY_HINT_GLOBAL_DIR),
|
|
"set_texture_dir", "get_texture_dir");
|
|
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "unit_scale", PROPERTY_HINT_RANGE, "0.0001,1,0.0001"),
|
|
"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");
|
|
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_gr2_materials"),
|
|
"set_use_gr2_materials", "get_use_gr2_materials");
|
|
}
|
|
|
|
void Metin2Model::set_material_mode(const String &m) {
|
|
material_mode = m;
|
|
if (is_inside_tree()) {
|
|
reload();
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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) {
|
|
gr2_path = p;
|
|
if (is_inside_tree()) {
|
|
reload();
|
|
}
|
|
}
|
|
void Metin2Model::set_texture_dir(const String &p) {
|
|
texture_dir = p;
|
|
if (is_inside_tree()) {
|
|
reload();
|
|
}
|
|
}
|
|
void Metin2Model::set_unit_scale(double s) {
|
|
unit_scale = s;
|
|
if (is_inside_tree()) {
|
|
reload();
|
|
}
|
|
}
|
|
void Metin2Model::set_flip_z(bool v) {
|
|
flip_z = v;
|
|
if (is_inside_tree()) {
|
|
reload();
|
|
}
|
|
}
|
|
void Metin2Model::set_flip_winding(bool v) {
|
|
flip_winding = v;
|
|
if (is_inside_tree()) {
|
|
reload();
|
|
}
|
|
}
|
|
|
|
void Metin2Model::_ready() {
|
|
if (!gr2_path.is_empty()) {
|
|
reload();
|
|
}
|
|
}
|
|
|
|
void Metin2Model::_clear_children() {
|
|
skel = nullptr;
|
|
mi = nullptr;
|
|
TypedArray<Node> kids = get_children();
|
|
for (int i = 0; i < kids.size(); ++i) {
|
|
Node *n = Object::cast_to<Node>(kids[i]);
|
|
if (n) {
|
|
remove_child(n);
|
|
n->queue_free();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Metin2Model::reload() {
|
|
_clear_children();
|
|
*file = std::nullopt;
|
|
last_info = "";
|
|
|
|
if (gr2_path.is_empty()) {
|
|
return;
|
|
}
|
|
|
|
gr2::LoadError err;
|
|
const std::string p(gr2_path.utf8().get_data());
|
|
auto loaded = gr2::File::load_path(p, &err);
|
|
if (!loaded) {
|
|
UtilityFunctions::push_error(String("[Metin2Model] gr2 load failed [") +
|
|
String(err.stage.c_str()) + "]: " + String(err.message.c_str()) + " (" + gr2_path + ")");
|
|
return;
|
|
}
|
|
*file = std::move(loaded);
|
|
const gr2::FileInfo &fi = (*file)->file_info();
|
|
|
|
set_transform(make_conv((float)unit_scale, flip_z));
|
|
|
|
// Skeleton (optional).
|
|
const gr2::Skeleton *sk = fi.skeletons.empty() ? nullptr : &fi.skeletons[0];
|
|
if (sk) {
|
|
skel = build_skeleton(*sk);
|
|
if (skel) {
|
|
add_child(skel);
|
|
skel->set_owner(get_owner() ? get_owner() : this);
|
|
}
|
|
}
|
|
|
|
// Mesh.
|
|
AABB bounds;
|
|
Ref<ArrayMesh> mesh = build_mesh(fi, flip_winding, bounds);
|
|
if (mesh->get_surface_count() > 0) {
|
|
mi = memnew(MeshInstance3D);
|
|
mi->set_name("MeshInstance3D");
|
|
mi->set_mesh(mesh);
|
|
add_child(mi);
|
|
mi->set_owner(get_owner() ? get_owner() : this);
|
|
if (skel && sk) {
|
|
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_custom_aabb(bounds.grow(0.01));
|
|
}
|
|
|
|
materials = gr2::dump_materials(**file);
|
|
|
|
_apply_materials();
|
|
|
|
int vtot = 0;
|
|
for (const auto &m : fi.meshes) {
|
|
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",
|
|
(int)(*file)->format_version(),
|
|
sk ? (int)sk->bones.size() : 0,
|
|
(int)fi.meshes.size(),
|
|
mesh->get_surface_count(),
|
|
vtot,
|
|
String(bounds));
|
|
UtilityFunctions::print("[Metin2Model] ", last_info);
|
|
}
|
|
|
|
String Metin2Model::_guess_texture_dir() const {
|
|
if (!texture_dir.is_empty()) {
|
|
return texture_dir;
|
|
}
|
|
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 String dir = _guess_texture_dir();
|
|
const String stem = gr2_path.get_file().get_basename();
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
int textured = 0;
|
|
for (int s = 0; s < mesh->get_surface_count(); ++s) {
|
|
const String sname = mesh->surface_get_name(s);
|
|
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()) {
|
|
tex = _load_dds(surface_tex_override[s]);
|
|
}
|
|
// 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);
|
|
}
|
|
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));
|
|
}
|
|
}
|
|
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 {
|
|
if (!file || !*file) {
|
|
return nullptr;
|
|
}
|
|
const gr2::FileInfo &fi = (*file)->file_info();
|
|
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 {
|
|
return last_info;
|
|
}
|
|
|
|
String Metin2Model::probe_gr2(const String &path) const {
|
|
gr2::LoadError err;
|
|
const std::string p(path.utf8().get_data());
|
|
auto f = gr2::File::load_path(p, &err);
|
|
if (!f) {
|
|
return vformat("gr2 load FAILED [%s]: %s", String(err.stage.c_str()), String(err.message.c_str()));
|
|
}
|
|
return vformat("gr2 OK: format_version=%d sections=%d",
|
|
(int)f->format_version(), (int)f->sections().size());
|
|
}
|
|
|
|
} // namespace mtgodot
|