270 lines
12 KiB
C++
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> ¤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<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
|