Files

270 lines
12 KiB
C++

#pragma once
#include <godot_cpp/classes/array_mesh.hpp>
#include <godot_cpp/classes/image.hpp>
#include <godot_cpp/classes/image_texture.hpp>
#include <godot_cpp/classes/node3d.hpp>
#include <godot_cpp/classes/ref.hpp>
#include <godot_cpp/templates/hash_map.hpp>
#include <godot_cpp/variant/array.hpp>
#include <godot_cpp/variant/aabb.hpp>
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/variant/string.hpp>
#include <gr2/gr2.h>
#include "gr2_bridge.h"
#include <memory>
#include <optional>
#include <vector>
namespace godot {
class Skeleton3D;
class MeshInstance3D;
} // namespace godot
namespace mtgodot {
// Loads a Metin2 .gr2 and builds a Godot subtree:
// Metin2Model (transform = Z-up/cm -> Y-up/m conversion)
// └─ Skeleton3D (rest pose from gr2 bones)
// └─ MeshInstance3D (ArrayMesh, one surface per gr2 mesh; skinned)
//
// M1: static bind-pose render. M2's Metin2AnimPlayer drives the Skeleton3D.
class Metin2Model : public godot::Node3D {
GDCLASS(Metin2Model, godot::Node3D)
public:
Metin2Model();
~Metin2Model() override;
void _ready() override;
void _process(double delta) override;
// --- inspector properties ---
// §2.10 LOD: loads <base>_lod_01/02/03.gr2 (same 75-bone skeleton, decimated
// meshes) and swaps the rendered mesh by camera distance. `lod_distances` =
// [d1,d2,d3]; >d3 -> LOD3. Skinning is unaffected (shared skeleton).
void set_lod_enabled(bool v);
bool get_lod_enabled() const { return lod_enabled; }
void set_lod_distances(const godot::PackedFloat32Array &d);
godot::PackedFloat32Array get_lod_distances() const { return lod_dist; }
int get_lod_level() const { return lod_level; }
void set_gr2_path(const godot::String &p);
godot::String get_gr2_path() const { return gr2_path; }
void set_texture_dir(const godot::String &p);
godot::String get_texture_dir() const { return texture_dir; }
void set_unit_scale(double s);
double get_unit_scale() const { return unit_scale; }
void set_flip_z(bool v);
bool get_flip_z() const { return flip_z; }
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; }
void set_use_gr2_materials(bool v);
bool get_use_gr2_materials() const { return use_gr2_materials; }
// Sphere-map specular power (PARITY §2.7 / BACKLOG B9). In the original client
// this is per skin-part, driven by the equipped body-armor item_proto
// `bSpecular / 100`; 0 = flat (the base body). No equipment layer here yet, so
// this is a manual hook — 0 keeps output identical to before.
void set_specular_power(double p);
double get_specular_power() const { return specular_power; }
// Explicit per-surface texture path override (index -> absolute .dds path).
void set_surface_texture(int surface, const godot::String &path);
// When gr2_path is a .msm: [{index, model (abs .gr2), target_skin}, ...].
godot::Array get_hair_options() const { return hair_options; }
// Attach a hair .gr2 whose skeleton bone names match the base. Its mesh is
// merged into the CPU-skin path (skinned by the base skeleton's matrices,
// bones remapped by name). "" detaches. GPU-skin path ignores hair for now.
void set_hair_gr2(const godot::String &p);
godot::String get_hair_gr2() const { return hair_gr2; }
// SourceSkin -> TargetSkin recolour (PARITY §2.4 / BACKLOG D3). The client's
// `.msm` HairData ships one hair .gr2 with a `SourceSkin` (the texture baked
// into the gr2 material) and a per-colour `TargetSkin` dds; `SetMaterialImage
// Pointer(part, SourceSkin, load(TargetSkin))` swaps it. Here: when set, the
// hair mesh uses this dds as its albedo instead of the gr2's sibling texture.
// Absolute path (an entry's resolved `target_skin` from get_hair_options()).
void set_hair_skin(const godot::String &p);
godot::String get_hair_skin() const { return hair_skin; }
// Attach a rigid weapon .gr2 to a base-skeleton bone (PARITY §2.1 / BACKLOG
// C5). The client links the weapon model instance to `equip_right_hand`
// (`playersettingmodule.py`, warrior) and drives it with that bone's world
// matrix (`ModelInstanceUpdate.cpp:148` GetBoneMatrixPointer). Here the weapon
// mesh is a child MeshInstance3D whose transform = the bone's world pose from
// gr2::sample_pose each frame. "" detaches. Path may be relative to the base
// gr2 ("d:/ymir work/item/weapon/00040.gr2") or absolute.
void set_weapon_gr2(const godot::String &p);
godot::String get_weapon_gr2() const { return weapon_gr2; }
void set_weapon_bone(const godot::String &b);
godot::String get_weapon_bone() const { return weapon_bone; }
// Off-hand shield (rigid, attaches to equip_left_hand like the weapon).
void set_shield_gr2(const godot::String &p);
godot::String get_shield_gr2() const { return shield_gr2; }
void set_shield_bone(const godot::String &b);
godot::String get_shield_bone() const { return shield_bone; }
// Fed by Metin2AnimPlayer after each gr2::sample_pose: base-skeleton world
// matrices (world_pose output). Repositions attached rigid parts (weapon +
// shield). No-op for a slot with no gr2 or an unresolved bone name.
void update_weapon_pose(const std::vector<gr2::Mat4> &world_pose);
// Rebuild the subtree from the current properties.
void reload();
// One-line summary (bones / meshes / verts / bounds).
godot::String get_info() const;
// Deformed mesh bounds in this node's local GR2 coordinate frame. Unlike
// MeshInstance3D::get_aabb(), this follows the GPU shader pose as well.
godot::AABB get_visual_aabb();
godot::Dictionary get_fly_target_bounds();
// Parent-space Y adjustment that places the bind-pose model's lowest point
// on the actor origin. GR2 is Z-up/cm; this is converted to Godot Y/metres.
double get_ground_offset() const { return ground_offset; }
// --- C++ accessors for Metin2AnimPlayer (same extension) ---
const gr2::Skeleton *gr2_skeleton() const;
godot::Skeleton3D *skeleton_node() const { return skel; }
const gr2::FileInfo *gr2_fileinfo() const;
// Latest full-affine deformer matrices. Initialized to bind pose on reload
// and refreshed by either skinning path; attachments can share this pose.
const std::vector<gr2::Mat4> &current_skin_matrices() const { return current_skin; }
// CPU linear-blend skinning: rewrite ArrayMesh vertex regions from the
// per-bone deformer matrices (gr2::sample_pose's `skin` output). Debug/
// reference path; bypasses Godot's Skeleton3D skinning entirely.
void cpu_skin(const std::vector<gr2::Mat4> &skin);
bool has_cpu_skin_mesh() const { return cpu_mesh.is_valid(); }
void enable_cpu_skin(bool on);
// GPU linear-blend skinning: LBS in a custom vertex shader with the FULL
// per-bone matrices uploaded as a float texture (no Skeleton3D -> shear kept).
// Keeps the static build_mesh() output; just swaps materials + feeds the
// bone texture each frame. MTGODOT_GPUSKIN=1 route.
void enable_gpu_skin(bool on);
void gpu_skin(const std::vector<gr2::Mat4> &skin);
// libgr2 sanity probe (kept from M0').
godot::String probe_gr2(const godot::String &path) const;
protected:
static void _bind_methods();
private:
godot::String gr2_path;
godot::String texture_dir;
double unit_scale = 0.01;
bool flip_z = false;
bool flip_winding = false;
godot::String material_mode = "metin2"; // "metin2" (ShaderMaterial) | "standard"
bool use_gr2_materials = true; // gr2 MaterialBindings -> texture (else: filename heuristic only)
double specular_power = 0.0; // PARITY §2.7; 0 = disabled
godot::Ref<godot::Texture2D> sphere_map; // shared sphere map, lazy-loaded
godot::Ref<godot::Texture2D> _load_sphere_map(); // "ymir work/special/spheremap.jpg"
godot::PackedStringArray surface_tex_override;
godot::String resolved_gr2_dir; // dir of the actually-loaded .gr2 (for .msm)
godot::Array hair_options; // from .msm HairData
godot::String hair_gr2; // attached hair .gr2 (or "")
godot::String hair_skin; // TargetSkin dds override (or "")
std::shared_ptr<std::optional<gr2::File>> hair_file; // loaded hair gr2
std::vector<mtgodot::RenderPart> hair_parts;
std::vector<int> hair_bone_remap; // hair skel idx -> base skel idx (by name)
godot::Ref<godot::Material> hair_mat;
godot::Ref<godot::ImageTexture> hair_tex; // resolved hair albedo (for GPU path)
int gpu_base_surf = 0; // base surfaces before appended GPU hair
void _load_hair();
void _refresh_hair_mesh();
void _build_gpu_mesh(); // base + remapped hair surfaces -> mi->mesh (GPU-skin path, §2.2)
godot::String weapon_gr2; // attached weapon .gr2 (or "")
godot::String weapon_bone = "equip_right_hand"; // base-skeleton attach bone
std::shared_ptr<std::optional<gr2::File>> weapon_file; // loaded weapon gr2
godot::MeshInstance3D *weapon_mi = nullptr; // child of this node
int weapon_bone_idx = -1; // base skeleton index of weapon_bone
// weapon's own bone[0] grip transform (invWorld · local); folded in so the mesh
// aligns to the hand even when it is authored offset from its bone (client:
// GrannySampleModelAnimationsAccelerated on the weapon skeleton). Identity when
// the weapon has no skeleton or a ~identity bone.
gr2::Mat4 weapon_pre{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
godot::String shield_gr2;
godot::String shield_bone = "Bip01 L Hand"; // PC skeletons have no equip_left_hand
std::shared_ptr<std::optional<gr2::File>> shield_file;
godot::MeshInstance3D *shield_mi = nullptr;
int shield_bone_idx = -1;
gr2::Mat4 shield_pre{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
void _load_weapon();
void _load_shield();
// shared rigid-attach loader used by _load_weapon / _load_shield.
void _load_attach(const godot::String &gr2_rel, const godot::String &bone_name,
std::shared_ptr<std::optional<gr2::File>> &slot_file, godot::MeshInstance3D *&slot_mi,
int &slot_bone_idx, gr2::Mat4 &slot_pre, const char *node_name, const char *label);
std::shared_ptr<std::optional<gr2::File>> file; // shared_ptr so accessor stays valid
std::vector<gr2::MaterialInfo> materials;
godot::Skeleton3D *skel = nullptr;
godot::MeshInstance3D *mi = nullptr;
godot::String last_info;
double ground_offset = 0.0;
godot::HashMap<godot::String, godot::Ref<godot::ImageTexture>> tex_cache;
// §2.10 LOD
bool lod_enabled = true;
int lod_level = 0; // 0 = full model, 1..3 = _lod_0N
godot::PackedFloat32Array lod_dist;
std::vector<std::shared_ptr<std::optional<gr2::File>>> lod_files; // [0]=_lod_01 ...
std::vector<std::vector<mtgodot::RenderPart>> lod_parts; // parallel to lod_files
// LOD crossfade: a frozen ghost of the outgoing level fades out while the new
// mesh fades in (LODController::BlendRenderWithOneTexture). ~0.18 s.
godot::MeshInstance3D *lod_prev_mi = nullptr;
double lod_fade_t = -1.0; // <0 = not fading
std::vector<godot::Ref<godot::Material>> lod_ghost_mats;
void _end_lod_fade();
std::vector<mtgodot::RenderPart> base_parts;
void _load_lods(const godot::String &loaded_spec);
void _set_lod(int n);
const gr2::FileInfo *active_fi() const; // base or current LOD (mesh/material data)
std::vector<mtgodot::RenderPart> parts; // surface s -> (gr2 mesh, tri_group, material)
std::vector<godot::Ref<godot::Material>> surf_mats; // resolved once; re-assigned each cpu_skin frame
godot::Ref<godot::ArrayMesh> cpu_mesh; // rebuilt each frame in cpu_skin mode
bool gpu_skin_active = false;
bool gpu_visual_bounds_ready = false;
bool have_visual_bounds = false;
godot::AABB visual_bounds;
std::vector<gr2::Mat4> current_world_pose;
godot::AABB fly_target_bounds;
bool fly_target_bounds_valid = false;
std::vector<gr2::Mat4> current_skin;
godot::Ref<godot::Image> bones_img; // RGBAF 3 x bone_count
godot::Ref<godot::ImageTexture> bones_tex;
void _clear_children();
void _apply_materials();
bool _update_skinned_bounds(const std::vector<gr2::Mat4> &skin);
godot::String _guess_texture_dir() const;
// Resolve a "d:/ymir work/..." path referenced from `base` (.msm/.msa) to an
// absolute file. Passes existing absolute paths through.
static godot::String resolve_rel_gr2(const godot::String &base, const godot::String &spec);
godot::Ref<godot::ImageTexture> _load_dds(const godot::String &path);
godot::Ref<godot::ImageTexture> _resolve_texture(const godot::String &dir,
const godot::String &stem, const godot::String &surface_name,
const godot::PackedStringArray &dds_files);
};
} // namespace mtgodot