#pragma once #include #include #include #include #include #include #include #include #include #include #include #include "gr2_bridge.h" #include #include #include 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 _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 &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 ¤t_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 &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 &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 sphere_map; // shared sphere map, lazy-loaded godot::Ref _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> hair_file; // loaded hair gr2 std::vector hair_parts; std::vector hair_bone_remap; // hair skel idx -> base skel idx (by name) godot::Ref hair_mat; godot::Ref 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> 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> 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> &slot_file, godot::MeshInstance3D *&slot_mi, int &slot_bone_idx, gr2::Mat4 &slot_pre, const char *node_name, const char *label); std::shared_ptr> file; // shared_ptr so accessor stays valid std::vector materials; godot::Skeleton3D *skel = nullptr; godot::MeshInstance3D *mi = nullptr; godot::String last_info; double ground_offset = 0.0; godot::HashMap> 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>> lod_files; // [0]=_lod_01 ... std::vector> 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> lod_ghost_mats; void _end_lod_fade(); std::vector 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 parts; // surface s -> (gr2 mesh, tri_group, material) std::vector> surf_mats; // resolved once; re-assigned each cpu_skin frame godot::Ref 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 current_world_pose; godot::AABB fly_target_bounds; bool fly_target_bounds_valid = false; std::vector current_skin; godot::Ref bones_img; // RGBAF 3 x bone_count godot::Ref bones_tex; void _clear_children(); void _apply_materials(); bool _update_skinned_bounds(const std::vector &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 _load_dds(const godot::String &path); godot::Ref _resolve_texture(const godot::String &dir, const godot::String &stem, const godot::String &surface_name, const godot::PackedStringArray &dds_files); }; } // namespace mtgodot