Implement playable Mac client and rendering validation
This commit is contained in:
@@ -69,6 +69,7 @@ void Metin2Model::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("reload"), &Metin2Model::reload);
|
||||
ClassDB::bind_method(D_METHOD("get_info"), &Metin2Model::get_info);
|
||||
ClassDB::bind_method(D_METHOD("get_visual_aabb"), &Metin2Model::get_visual_aabb);
|
||||
ClassDB::bind_method(D_METHOD("get_fly_target_bounds"), &Metin2Model::get_fly_target_bounds);
|
||||
ClassDB::bind_method(D_METHOD("get_ground_offset"), &Metin2Model::get_ground_offset);
|
||||
ClassDB::bind_method(D_METHOD("get_hair_options"), &Metin2Model::get_hair_options);
|
||||
ClassDB::bind_method(D_METHOD("set_hair_gr2", "p"), &Metin2Model::set_hair_gr2);
|
||||
@@ -274,14 +275,26 @@ void Metin2Model::_set_lod(int n) {
|
||||
lod_prev_mi->set_name("LodGhost");
|
||||
lod_prev_mi->set_layer_mask(1u << 1);
|
||||
lod_prev_mi->set_mesh(mi->get_mesh());
|
||||
lod_prev_mi->set_custom_aabb(mi->get_custom_aabb());
|
||||
add_child(lod_prev_mi);
|
||||
lod_prev_mi->set_transform(mi->get_transform());
|
||||
std::vector<Ref<Material>> ghost_mats;
|
||||
for (int s = 0; s < (int)surf_mats.size(); ++s) {
|
||||
Ref<Material> src = surf_mats[s];
|
||||
// A frozen GPU ghost must not share the live, updated bone texture.
|
||||
Ref<ImageTexture> ghost_bones;
|
||||
if (gpu_skin_active && bones_img.is_valid()) {
|
||||
Ref<godot::Image> snapshot = bones_img->duplicate();
|
||||
ghost_bones = ImageTexture::create_from_image(snapshot);
|
||||
}
|
||||
for (int s = 0; s < mi->get_mesh()->get_surface_count(); ++s) {
|
||||
// CPU hair overrides are not necessarily present in surf_mats.
|
||||
Ref<Material> src = mi->get_active_material(s);
|
||||
Ref<Material> dup;
|
||||
if (src.is_valid()) {
|
||||
dup = src->duplicate();
|
||||
Ref<ShaderMaterial> shader = dup;
|
||||
if (shader.is_valid() && ghost_bones.is_valid()) {
|
||||
shader->set_shader_parameter("bones_tex", ghost_bones);
|
||||
}
|
||||
}
|
||||
lod_prev_mi->set_surface_override_material(s, dup);
|
||||
ghost_mats.push_back(dup);
|
||||
@@ -298,7 +311,11 @@ void Metin2Model::_set_lod(int n) {
|
||||
_end_lod_fade();
|
||||
return;
|
||||
}
|
||||
if (cpu_mesh.is_valid()) {
|
||||
if (gpu_skin_active) {
|
||||
// Keep attachments and the body/hair material boundary across LODs.
|
||||
_build_gpu_mesh();
|
||||
gpu_visual_bounds_ready = false;
|
||||
} else if (cpu_mesh.is_valid()) {
|
||||
AABB b;
|
||||
cpu_mesh = build_mesh(*afi, parts, flip_winding, b);
|
||||
mi->set_mesh(cpu_mesh);
|
||||
@@ -310,6 +327,9 @@ void Metin2Model::_set_lod(int n) {
|
||||
}
|
||||
}
|
||||
_apply_materials();
|
||||
if (gpu_skin_active && !current_skin.empty()) {
|
||||
gpu_skin(current_skin);
|
||||
}
|
||||
if (cpu_mesh.is_valid() && !current_skin.empty()) {
|
||||
cpu_skin(current_skin);
|
||||
}
|
||||
@@ -372,6 +392,8 @@ void Metin2Model::_clear_children() {
|
||||
}
|
||||
|
||||
void Metin2Model::reload() {
|
||||
current_world_pose.clear();
|
||||
fly_target_bounds_valid = false;
|
||||
_clear_children();
|
||||
*file = std::nullopt;
|
||||
last_info = "";
|
||||
@@ -595,24 +617,37 @@ void Metin2Model::_load_hair() {
|
||||
UtilityFunctions::print(vformat("[Metin2Model] hair: %s %d/%d bones matched, %d parts",
|
||||
hair_gr2.get_file(), matched, (int)hbones.size(), (int)hair_parts.size()));
|
||||
|
||||
if (has_cpu_skin_mesh() && !current_skin.empty())
|
||||
}
|
||||
|
||||
void Metin2Model::_refresh_hair_mesh() {
|
||||
if (gpu_skin_active) {
|
||||
_build_gpu_mesh();
|
||||
_apply_materials();
|
||||
gpu_visual_bounds_ready = false;
|
||||
if (!current_skin.empty()) gpu_skin(current_skin);
|
||||
} else if (has_cpu_skin_mesh() && !current_skin.empty()) {
|
||||
cpu_skin(current_skin);
|
||||
}
|
||||
}
|
||||
|
||||
void Metin2Model::set_hair_gr2(const String &p) {
|
||||
if (p == hair_gr2)
|
||||
return;
|
||||
hair_gr2 = p;
|
||||
if (file && *file)
|
||||
if (file && *file) {
|
||||
_load_hair();
|
||||
_refresh_hair_mesh();
|
||||
}
|
||||
}
|
||||
|
||||
void Metin2Model::set_hair_skin(const String &p) {
|
||||
if (p == hair_skin)
|
||||
return;
|
||||
hair_skin = p;
|
||||
if (file && *file && !hair_gr2.is_empty())
|
||||
if (file && *file && !hair_gr2.is_empty()) {
|
||||
_load_hair();
|
||||
_refresh_hair_mesh();
|
||||
}
|
||||
}
|
||||
|
||||
void Metin2Model::set_weapon_gr2(const String &p) {
|
||||
@@ -793,6 +828,7 @@ void Metin2Model::_load_attach(const String &gr2_rel, const String &bone_name,
|
||||
}
|
||||
|
||||
void Metin2Model::update_weapon_pose(const std::vector<gr2::Mat4> &world_pose) {
|
||||
current_world_pose = world_pose;
|
||||
// weapon local = gripPre · handWorld (client: weaponBone[0] composed onto the
|
||||
// parent attach-bone matrix). gripPre is identity for near-origin weapons, so
|
||||
// this is backward-compatible with the ones that already looked right.
|
||||
@@ -804,6 +840,38 @@ void Metin2Model::update_weapon_pose(const std::vector<gr2::Mat4> &world_pose) {
|
||||
mtgodot::mul4x3(shield_pre, world_pose[shield_bone_idx])));
|
||||
}
|
||||
|
||||
namespace { inline void xform_pt(const gr2::Mat4 &M, const float *v, float *o); }
|
||||
|
||||
Dictionary Metin2Model::get_fly_target_bounds() {
|
||||
// Legacy caches its sphere until the model is reset. Do not let particles,
|
||||
// hair, weapons, LOD ghosts or subsequent animation frames shift the centre.
|
||||
if (!fly_target_bounds_valid) {
|
||||
const gr2::FileInfo *fi = active_fi();
|
||||
bool found = false;
|
||||
AABB bounds;
|
||||
if (fi) for (const auto &mesh : fi->meshes) {
|
||||
for (size_t slot = 0; slot < mesh.bone_bounds.size(); ++slot) {
|
||||
const auto &obb = mesh.bone_bounds[slot];
|
||||
const int bone = slot < mesh.bone_bindings.size() ? mesh.bone_bindings[slot] : -1;
|
||||
if (!obb.valid || bone < 0 || bone >= (int)current_world_pose.size()) continue;
|
||||
for (int corner = 0; corner < 8; ++corner) {
|
||||
float point[3], transformed[3];
|
||||
for (int axis = 0; axis < 3; ++axis) point[axis] = (corner & (1 << axis)) ? obb.max[axis] : obb.min[axis];
|
||||
xform_pt(current_world_pose[bone], point, transformed);
|
||||
const Vector3 p(transformed[0], transformed[1], transformed[2]);
|
||||
bounds = found ? bounds.expand(p) : AABB(p, Vector3());
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (found) { fly_target_bounds = bounds; fly_target_bounds_valid = true; }
|
||||
}
|
||||
Dictionary result;
|
||||
result["valid"] = fly_target_bounds_valid;
|
||||
result["bounds"] = fly_target_bounds;
|
||||
return result;
|
||||
}
|
||||
|
||||
String Metin2Model::_guess_texture_dir() const {
|
||||
if (!texture_dir.is_empty()) {
|
||||
return texture_dir;
|
||||
@@ -1137,6 +1205,7 @@ void Metin2Model::_apply_materials() {
|
||||
md.albedo = hair_tex;
|
||||
md.blend = mtgodot::BlendMode::AlphaTest;
|
||||
md.alpha_scissor = 0.5f;
|
||||
md.two_sided = true; // Match CPU hair cards when viewed from behind.
|
||||
md.skinned = true;
|
||||
md.bones_tex = bones_tex;
|
||||
Ref<Material> hm = mtgodot::make_material(md);
|
||||
@@ -1591,10 +1660,11 @@ void Metin2Model::gpu_skin(const std::vector<gr2::Mat4> &skin) {
|
||||
return;
|
||||
}
|
||||
current_skin = skin;
|
||||
// The shader owns deformation after this point. Sample the posed bounds
|
||||
// once for culling and initial camera framing; doing the full vertex walk on
|
||||
// every GPU frame would erase the CPU saving this path is intended to make.
|
||||
if (!gpu_visual_bounds_ready && _update_skinned_bounds(skin)) {
|
||||
// Culling must follow every pose, including offscreen actors and one-shots.
|
||||
// Only the initial ready notification reframes selection cameras; emitting
|
||||
// it on every pose would make the camera follow the breathing animation.
|
||||
const bool bounds_updated = _update_skinned_bounds(skin);
|
||||
if (!gpu_visual_bounds_ready && bounds_updated) {
|
||||
gpu_visual_bounds_ready = true;
|
||||
emit_signal("visual_bounds_changed");
|
||||
}
|
||||
@@ -1620,12 +1690,7 @@ String Metin2Model::get_info() const {
|
||||
}
|
||||
|
||||
AABB Metin2Model::get_visual_aabb() {
|
||||
// Consumers such as CharSelect ask only while framing. Refresh on demand
|
||||
// so their final camera fit reflects the current GPU pose without adding a
|
||||
// per-frame CPU vertex walk to every world actor.
|
||||
if (gpu_skin_active && !current_skin.empty()) {
|
||||
_update_skinned_bounds(current_skin);
|
||||
}
|
||||
// Both animation paths publish bounds when sampling, before camera queries.
|
||||
if (have_visual_bounds) {
|
||||
return visual_bounds;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user