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
+20 -1
View File
@@ -238,7 +238,7 @@ func _build_stage() -> void:
root.add_child(we)
_pivot = Node3D.new()
_pivot.rotation.y = -PI * 0.5
_pivot.rotation.y = PI * 0.5
root.add_child(_pivot)
_cam = Camera3D.new()
@@ -254,10 +254,20 @@ func _frame_model() -> void:
return
var aabb := AABB()
var first := true
# GPU skinning happens in the vertex shader, so MeshInstance3D's mesh AABB
# remains in bind pose. Prefer the extension's matching deformed bounds for
# the main body; accessories continue through their own mesh bounds below.
if _pv.model and _pv.model.has_method("get_visual_aabb"):
var posed: AABB = _pv.model.call("get_visual_aabb")
if posed.size.length_squared() > 0.0:
aabb = (_pv.model as Node3D).global_transform * posed
first = false
for mi in (_pv as Node).find_children("*", "MeshInstance3D", true, false):
var m := mi as MeshInstance3D
if m.mesh == null:
continue
if m.get_parent() == _pv.model and m.name == "MeshInstance3D":
continue # already covered by Metin2Model.get_visual_aabb()
var wb: AABB = m.get_global_transform() * m.get_aabb()
aabb = wb if first else aabb.merge(wb)
first = false
@@ -274,6 +284,13 @@ func _frame_model() -> void:
_cam.position = target + Vector3(0, dist * sin(deg_to_rad(6.0)), dist * cos(deg_to_rad(6.0)))
_cam.look_at(target, Vector3.UP)
# Metin2AnimPlayer enables the optional GPU skin path on its first animation
# sample. Its material/mesh swap changes the MeshInstance AABB after the
# initial CPU framing pass, so frame again after the extension confirms it.
func _on_model_visual_bounds_changed(pv: Node) -> void:
if pv == _pv and is_instance_valid(pv):
call_deferred("_frame_model")
func _rebuild_model() -> void:
if _pivot == null:
return
@@ -290,6 +307,8 @@ func _rebuild_model() -> void:
_pivot.add_child(pv)
if pv.build(_assets, job, pump):
_pv = pv
if pv.model and pv.model.has_signal("visual_bounds_changed"):
pv.model.visual_bounds_changed.connect(_on_model_visual_bounds_changed.bind(pv))
if _proto == null and ClassDB.class_exists("Metin2Proto"):
_proto = ClassDB.instantiate("Metin2Proto")
add_child(_proto)
+10 -2
View File
@@ -346,11 +346,19 @@ func _resolve_spec_asset(e: Dictionary, rel: String) -> String:
var byp := _resolve_asset(String(e.get("path", "")) + rel)
if byp != "":
return byp
# 按 basename 在 pc/<cls>/ 和 pc2/<cls>/ 下找
# 规格覆盖层(例如 root/msm/warrior_m.msm)可能只给 Model 名而没有
# PathName。按 basename 回退时除职业根目录外,还必须查 hair/;否则
# HairIndex 1001..1012 能解析却加载失败,并静默保留 PlayerView 的
# hair_1_1 默认模型,造成选角发型遮脸。
var bn := rel.get_file()
if race >= 0:
var cls: String = CLASS_OF[race & 3]
for sub in ["PC/ymir work/pc/%s/%s" % [cls, bn], "pc2/ymir work/pc2/%s/%s" % [cls, bn]]:
for sub in [
"PC/ymir work/pc/%s/%s" % [cls, bn],
"PC/ymir work/pc/%s/hair/%s" % [cls, bn],
"pc2/ymir work/pc2/%s/%s" % [cls, bn],
"pc2/ymir work/pc2/%s/hair/%s" % [cls, bn],
]:
var q := _resolve_asset(sub)
if q != "":
return q
+2
View File
@@ -43,6 +43,8 @@ func build(assets_root: String, race: int, pump := Callable()) -> bool:
model = ClassDB.instantiate("Metin2Model")
model.name = "Metin2Model"
# PC GR2 surfaces need the same outward winding in bind, CPU and GPU poses.
model.set("flip_winding", true)
model.set("texture_dir", base)
if pump.is_valid(): pump.call()
var body := _first_existing([base.path_join("%s_novice.gr2" % cls), base.path_join("%s.gr2" % cls)])
+6 -1
View File
@@ -339,7 +339,12 @@ func _swap(local_a: int, local_b: int) -> void:
func _build(parent: Node) -> void:
_root = Control.new()
_root.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
_root.position = Vector2(-SLOTS_PER_PAGE * 23, -54)
# Give the root a real extent. A zero-sized Control happened to work with
# some viewport sizes, but makes anchoring and hit testing fragile on Retina
# windows. The old HUD's empty row then made this look like missing skills.
_root.size = Vector2(400, 56)
_root.position = Vector2(-200, -62)
_root.z_index = 10
parent.add_child(_root)
var prev := Button.new()
prev.text = ""
+8 -2
View File
@@ -57,7 +57,9 @@ func shape(idx: int) -> Dictionary:
if e.is_empty():
return {}
e = e.duplicate()
e["path"] = String(e.get("special_path", shape_path))
e["path"] = String(e.get("special_path", ""))
if e["path"] == "":
e["path"] = shape_path
e["spec_dir"] = _dir
return e
@@ -66,7 +68,9 @@ func hair(idx: int) -> Dictionary:
if e.is_empty():
return {}
e = e.duplicate()
e["path"] = String(e.get("special_path", hair_path))
e["path"] = String(e.get("special_path", ""))
if e["path"] == "":
e["path"] = hair_path
e["spec_dir"] = _dir
return e
@@ -150,6 +154,8 @@ func _parse_block(toks: Array, i: Array, top := false) -> Dictionary:
return d
func _tokval(t):
if t is String and t.begins_with(String.chr(1)):
return t.substr(1)
if t is Dictionary and t.has("str"):
return t["str"]
return str(t)