fix(rendering): complete character visual regression fixes

This commit is contained in:
shen
2026-09-08 13:26:42 +08:00
parent c3a6fb973e
commit cea79fe17c
15 changed files with 380 additions and 35 deletions
+93 -3
View File
@@ -68,6 +68,7 @@ void Metin2Model::_bind_methods() {
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("get_visual_aabb"), &Metin2Model::get_visual_aabb);
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);
@@ -83,6 +84,11 @@ void Metin2Model::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_shield_bone", "b"), &Metin2Model::set_shield_bone);
ClassDB::bind_method(D_METHOD("get_shield_bone"), &Metin2Model::get_shield_bone);
ClassDB::bind_method(D_METHOD("probe_gr2", "path"), &Metin2Model::probe_gr2);
// A renderer consumer may have framed the model before an animation switches
// it from the CPU mesh to GPU skinning. That switch updates the effective
// AABB, so expose a small notification instead of making each scene guess a
// number of frames to wait.
ADD_SIGNAL(MethodInfo("visual_bounds_changed"));
ADD_PROPERTY(PropertyInfo(Variant::STRING, "gr2_path", PROPERTY_HINT_GLOBAL_FILE, "*.gr2,*.msm"),
"set_gr2_path", "get_gr2_path");
@@ -391,6 +397,8 @@ void Metin2Model::reload() {
surf_mats.clear();
cpu_mesh.unref();
gpu_skin_active = false;
gpu_visual_bounds_ready = false;
have_visual_bounds = false;
bones_img.unref();
bones_tex.unref();
current_skin.clear();
@@ -476,6 +484,8 @@ void Metin2Model::reload() {
}
}
mi->set_custom_aabb(bounds.grow(0.01));
visual_bounds = bounds;
have_visual_bounds = true;
}
materials = gr2::dump_materials(**file);
@@ -1369,6 +1379,10 @@ void Metin2Model::cpu_skin(const std::vector<gr2::Mat4> &skin) {
for (uint32_t t = 0; t < ic; ++t) {
iw[t] = (int)m.indices[ib + t];
}
// Match build_mesh(): CPU animation must preserve the configured winding.
if (flip_winding)
for (uint32_t t = 0; t + 2 < ic; t += 3)
std::swap(iw[t + 1], iw[t + 2]);
Array arrays;
arrays.resize(Mesh::ARRAY_MAX);
arrays[Mesh::ARRAY_VERTEX] = mesh_pos[rp.mesh];
@@ -1430,6 +1444,14 @@ void Metin2Model::cpu_skin(const std::vector<gr2::Mat4> &skin) {
nn[2] /= ln;
}
pw[i] = Vector3(p[0], p[1], p[2]);
if (std::isfinite(p[0]) && std::isfinite(p[1]) && std::isfinite(p[2])) {
if (have_animated_bounds) {
animated_bounds = animated_bounds.expand(pw[i]);
} else {
animated_bounds = AABB(pw[i], Vector3());
have_animated_bounds = true;
}
}
nw[i] = Vector3(nn[0], nn[1], nn[2]);
uw[i] = Vector2(v.uv0[0], v.uv0[1]);
}
@@ -1448,6 +1470,9 @@ void Metin2Model::cpu_skin(const std::vector<gr2::Mat4> &skin) {
int32_t *iw = idx.ptrw();
for (uint32_t t = 0; t < ic; ++t)
iw[t] = (int)m.indices[ib + t];
if (flip_winding)
for (uint32_t t = 0; t + 2 < ic; t += 3)
std::swap(iw[t + 1], iw[t + 2]);
Array a;
a.resize(Mesh::ARRAY_MAX);
a[Mesh::ARRAY_VERTEX] = hp[rp.mesh];
@@ -1476,9 +1501,55 @@ void Metin2Model::cpu_skin(const std::vector<gr2::Mat4> &skin) {
mi->set_surface_override_material(body_surfaces + s, hair_mat);
if (mi && have_animated_bounds) {
mi->set_custom_aabb(animated_bounds.grow(0.01));
visual_bounds = animated_bounds;
have_visual_bounds = true;
}
}
bool Metin2Model::_update_skinned_bounds(const std::vector<gr2::Mat4> &skin) {
const gr2::FileInfo *afi = active_fi();
if (!afi) {
return false;
}
static const gr2::Mat4 I{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
AABB bounds;
bool have_bounds = false;
auto expand_mesh = [&](const gr2::FileInfo &fi, const std::vector<int> *remap) {
for (const gr2::Mesh &m : fi.meshes) {
for (const gr2::Vertex &v : m.vertices) {
gr2::Mat4 blend{};
float sum = 0.0f;
for (int k = 0; k < 4; ++k) sum += v.bone_weight[k];
for (int k = 0; k < 4; ++k) {
const float w = sum > 0.0f ? v.bone_weight[k] / sum : (k == 0 ? 1.0f : 0.0f);
if (w == 0.0f) continue;
int bone = (v.bone_index[k] >= 0 && v.bone_index[k] < (int)m.bone_bindings.size())
? m.bone_bindings[v.bone_index[k]] : -1;
if (remap && bone >= 0 && bone < (int)remap->size()) bone = (*remap)[bone];
const gr2::Mat4 &mat = bone >= 0 && bone < (int)skin.size() ? skin[bone] : I;
for (int e = 0; e < 16; ++e) blend[e] += w * mat[e];
}
float p[3];
xform_pt(blend, v.pos, p);
if (!std::isfinite(p[0]) || !std::isfinite(p[1]) || !std::isfinite(p[2])) continue;
const Vector3 point(p[0], p[1], p[2]);
bounds = have_bounds ? bounds.expand(point) : AABB(point, Vector3());
have_bounds = true;
}
}
};
expand_mesh(*afi, nullptr);
if (hair_file && *hair_file && !hair_parts.empty() && !hair_bone_remap.empty()) {
expand_mesh((**hair_file).file_info(), &hair_bone_remap);
}
if (have_bounds) {
visual_bounds = bounds;
have_visual_bounds = true;
if (mi) mi->set_custom_aabb(bounds.grow(0.01));
}
return have_bounds;
}
void Metin2Model::enable_gpu_skin(bool on) {
if (!on || gpu_skin_active || !mi || !file || !*file) {
return;
@@ -1504,9 +1575,8 @@ void Metin2Model::enable_gpu_skin(bool on) {
if (skel) {
skel->set_show_rest_only(true);
}
// The mesh AABB is bind-pose; the shader moves verts, so widen the cull box
// (gr2-skin-space units, ~100/m) or arms-up poses get frustum/shadow-culled.
mi->set_custom_aabb(AABB(Vector3(-400, -400, -400), Vector3(800, 800, 800)));
// gpu_skin() supplies the first actual posed bounds once the animation
// sampler has produced its skin matrices.
gpu_skin_active = true;
_apply_materials(); // rebuild surface materials with the skinned shader + bones_tex
UtilityFunctions::print(vformat("[Metin2Model] GPU-skin mode ON (%d bones)", nbones));
@@ -1517,6 +1587,13 @@ 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)) {
gpu_visual_bounds_ready = true;
emit_signal("visual_bounds_changed");
}
const int nbones = bones_img->get_height();
PackedByteArray buf;
buf.resize(nbones * 3 * 4 * 4); // rows * 3 texels * RGBA * float
@@ -1538,6 +1615,19 @@ String Metin2Model::get_info() const {
return last_info;
}
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);
}
if (have_visual_bounds) {
return visual_bounds;
}
return mi && mi->get_mesh().is_valid() ? mi->get_mesh()->get_aabb() : AABB();
}
String Metin2Model::probe_gr2(const String &path) const {
gr2::LoadError err;
auto f = mtgodot::gr2_from_file(path, &err);
+8
View File
@@ -7,6 +7,7 @@
#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>
@@ -126,6 +127,9 @@ public:
// 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();
// 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; }
@@ -237,12 +241,16 @@ private:
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_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.