M1+M2: gr2 -> Skeleton3D/ArrayMesh/Skin + libgr2-driven animation
- gr2_bridge: gr2 Mat4 (row-major) -> Godot Transform3D (4x4 transpose); build_skeleton (rest from local_transform, initial_placement folded into roots), build_skin (bind i -> bone i, pose = inverse_world), build_mesh (surface per gr2 mesh, ARRAY_BONES = skeleton idx via bone_bindings, rigid meshes bound to bone_bindings[0]). - dxt: DDS DXT1/3/5 decoder ported from xrender-poc. - Metin2Model: load_gr2 -> Skeleton3D + MeshInstance3D + StandardMaterial3D (runtime DDS albedo, face/body split by surface name). Z-up cm -> Y-up m conversion on the node transform (unit_scale, flip_z, flip_winding props). - Metin2AnimPlayer: _process samples a (separate) anim .gr2 with gr2::sample_pose(model.skeleton, anim, t) -> set_bone_global_pose per bone; Godot GPU-skins via skin_matrix = global_pose(i) * bind_pose(i) == gr2 deformer matrix. selfcheck() NaN-scans all anims. Verified: warrior_cheongrin renders (75 bones, 5 surfaces, 3324 verts, textured, upright). dance01 (dur 28.3s, 74 tracks) plays, 0 NaN over 25 samples, distinct poses at t=2/9/16/23. Screenshots in test/golden/. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaHYEY9rwLWt21PULiYjeJ
This commit is contained in:
+105
-37
@@ -1,35 +1,45 @@
|
||||
extends Node3D
|
||||
## mtgodot-poc — M0' harness.
|
||||
## mtgodot-poc harness — M1 (static render) + M2 (skeletal animation).
|
||||
##
|
||||
## Builds the minimal scene the plan asks for (orbit camera + DirectionalLight3D
|
||||
## with shadows + WorldEnvironment/sky + placeholder cube + an empty Metin2Model),
|
||||
## then reports whether the GDExtension loaded.
|
||||
## Loads a Metin2 .gr2 via the Metin2Model GDExtension node, renders it with
|
||||
## Godot's Forward+ renderer, and (if an anim .gr2 is given) drives it with
|
||||
## Metin2AnimPlayer.
|
||||
##
|
||||
## M1 will replace the cube with Metin2Model.load_gr2(...) output.
|
||||
## Asset paths default to the sibling m2dev-client-main checkout; override with
|
||||
## MTGODOT_MODEL / MTGODOT_ANIM / MTGODOT_TEXDIR env vars.
|
||||
##
|
||||
## Keys: drag = orbit wheel = zoom Space = play/pause B = bind pose
|
||||
## [ / ] = scrub R = reload F2 = screenshot
|
||||
|
||||
const ORBIT_SPEED := 0.01
|
||||
const ZOOM_STEP := 0.3
|
||||
const ZOOM_STEP := 0.15
|
||||
const PITCH_LIMIT := 1.45
|
||||
|
||||
var _assets_root := ""
|
||||
var _cam: Camera3D
|
||||
var _yaw := 0.6
|
||||
var _pitch := 0.45
|
||||
var _dist := 4.0
|
||||
var _model: Node3D # Metin2Model
|
||||
var _anim: Node3D # Metin2AnimPlayer
|
||||
var _yaw := 0.7
|
||||
var _pitch := 0.25
|
||||
var _dist := 3.0
|
||||
var _target := Vector3(0.0, 1.0, 0.0)
|
||||
var _dragging := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_assets_root = ProjectSettings.globalize_path("res://../../m2dev-client-main/assets")
|
||||
_build_environment()
|
||||
_build_sun()
|
||||
_build_camera()
|
||||
_build_placeholder()
|
||||
_probe_extension()
|
||||
print("[mtgodot] M0' harness ready — drag: orbit, wheel: zoom, F2: screenshot")
|
||||
# Batch capture: MTGODOT_AUTOSHOT=/abs/path.png godot --path project
|
||||
_load_content()
|
||||
_frame_model()
|
||||
print("[mtgodot] harness ready")
|
||||
|
||||
var autoshot := OS.get_environment("MTGODOT_AUTOSHOT")
|
||||
if autoshot != "":
|
||||
await get_tree().create_timer(0.8).timeout
|
||||
# let a few frames render (and the anim settle) before capturing
|
||||
for i in 8:
|
||||
await get_tree().process_frame
|
||||
var img := get_viewport().get_texture().get_image()
|
||||
img.save_png(autoshot)
|
||||
print("[mtgodot] autoshot -> ", autoshot)
|
||||
@@ -44,6 +54,7 @@ func _build_environment() -> void:
|
||||
env.background_mode = Environment.BG_SKY
|
||||
env.sky = sky
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
||||
env.ambient_light_energy = 0.6
|
||||
env.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
||||
we.environment = env
|
||||
add_child(we)
|
||||
@@ -51,39 +62,73 @@ func _build_environment() -> void:
|
||||
|
||||
func _build_sun() -> void:
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.rotation_degrees = Vector3(-45.0, -35.0, 0.0)
|
||||
sun.rotation_degrees = Vector3(-50.0, -40.0, 0.0)
|
||||
sun.shadow_enabled = true
|
||||
sun.light_energy = 1.1
|
||||
add_child(sun)
|
||||
|
||||
|
||||
func _build_camera() -> void:
|
||||
_cam = Camera3D.new()
|
||||
_cam.current = true
|
||||
_cam.fov = 55.0
|
||||
_cam.fov = 45.0
|
||||
_cam.near = 0.01
|
||||
add_child(_cam)
|
||||
_update_camera()
|
||||
|
||||
|
||||
func _build_placeholder() -> void:
|
||||
var cube := MeshInstance3D.new()
|
||||
cube.name = "Placeholder"
|
||||
cube.mesh = BoxMesh.new()
|
||||
cube.position = _target
|
||||
add_child(cube)
|
||||
func _env(name: String, fallback: String) -> String:
|
||||
var v := OS.get_environment(name)
|
||||
return v if v != "" else fallback
|
||||
|
||||
|
||||
func _probe_extension() -> void:
|
||||
func _load_content() -> void:
|
||||
if not ClassDB.class_exists("Metin2Model"):
|
||||
push_error("[mtgodot] Metin2Model NOT registered — GDExtension failed to load")
|
||||
push_error("[mtgodot] Metin2Model not registered — GDExtension failed to load")
|
||||
return
|
||||
var m := ClassDB.instantiate("Metin2Model") as Node3D
|
||||
m.name = "Metin2Model"
|
||||
add_child(m)
|
||||
print("[mtgodot] Metin2Model registered OK — ", m.call("libgr2_info"))
|
||||
# Optional: point MTGODOT_PROBE_GR2 at a real .gr2 to exercise libgr2 end to end.
|
||||
var probe := OS.get_environment("MTGODOT_PROBE_GR2")
|
||||
if probe != "":
|
||||
print("[mtgodot] probe_gr2(", probe, ") -> ", m.call("probe_gr2", probe))
|
||||
|
||||
var model_path := _env("MTGODOT_MODEL",
|
||||
_assets_root.path_join("PC/ymir work/pc/warrior/warrior_cheongrin.gr2"))
|
||||
var tex_dir := _env("MTGODOT_TEXDIR",
|
||||
_assets_root.path_join("PC/ymir work/pc/warrior"))
|
||||
var anim_path := _env("MTGODOT_ANIM",
|
||||
_assets_root.path_join("PC/ymir work/pc/warrior/action/dance_1.gr2"))
|
||||
|
||||
_model = ClassDB.instantiate("Metin2Model")
|
||||
_model.name = "Metin2Model"
|
||||
_model.set("texture_dir", tex_dir)
|
||||
_model.set("gr2_path", model_path)
|
||||
add_child(_model)
|
||||
if _model.has_method("get_info"):
|
||||
print("[mtgodot] model: ", _model.call("get_info"))
|
||||
|
||||
if anim_path != "-" and FileAccess.file_exists(anim_path) and ClassDB.class_exists("Metin2AnimPlayer"):
|
||||
_anim = ClassDB.instantiate("Metin2AnimPlayer")
|
||||
_anim.name = "Metin2AnimPlayer"
|
||||
_anim.set("model_path", _anim.get_path_to(_model) if false else NodePath("../Metin2Model"))
|
||||
_anim.set("anim_path", anim_path)
|
||||
add_child(_anim)
|
||||
if _anim.has_method("get_info"):
|
||||
print("[mtgodot] anim: ", _anim.call("get_info"))
|
||||
if _anim.has_method("selfcheck"):
|
||||
print("[mtgodot] ", _anim.call("selfcheck", 24))
|
||||
_anim.call("reload")
|
||||
var fixed_t := OS.get_environment("MTGODOT_ANIM_T")
|
||||
if fixed_t != "":
|
||||
_anim.set("playing", false)
|
||||
_anim.call("set_time", float(fixed_t))
|
||||
print("[mtgodot] fixed t=", _anim.call("get_time"), " dur=", _anim.call("get_duration"))
|
||||
|
||||
|
||||
func _frame_model() -> void:
|
||||
# Use the model's converted world-space AABB to place the orbit target/dist.
|
||||
var mi := _model.get_node_or_null("MeshInstance3D") if _model else null
|
||||
if mi and mi is VisualInstance3D:
|
||||
var aabb: AABB = (mi as VisualInstance3D).get_aabb()
|
||||
aabb = _model.transform * aabb
|
||||
_target = aabb.get_center()
|
||||
_dist = maxf(1.0, aabb.get_longest_axis_size() * 1.8)
|
||||
_update_camera()
|
||||
|
||||
|
||||
func _update_camera() -> void:
|
||||
@@ -102,20 +147,43 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
_dragging = mb.pressed
|
||||
MOUSE_BUTTON_WHEEL_UP:
|
||||
_dist = maxf(0.5, _dist - ZOOM_STEP)
|
||||
_dist = maxf(0.2, _dist * (1.0 - ZOOM_STEP))
|
||||
_update_camera()
|
||||
MOUSE_BUTTON_WHEEL_DOWN:
|
||||
_dist = minf(50.0, _dist + ZOOM_STEP)
|
||||
_dist = minf(200.0, _dist * (1.0 + ZOOM_STEP))
|
||||
_update_camera()
|
||||
elif event is InputEventMouseMotion and _dragging:
|
||||
var mm := event as InputEventMouseMotion
|
||||
_yaw -= mm.relative.x * ORBIT_SPEED
|
||||
_pitch = clampf(_pitch + mm.relative.y * ORBIT_SPEED, -PITCH_LIMIT, PITCH_LIMIT)
|
||||
_update_camera()
|
||||
elif event is InputEventKey:
|
||||
var k := event as InputEventKey
|
||||
if k.pressed and k.keycode == KEY_F2:
|
||||
elif event is InputEventKey and event.pressed and not event.echo:
|
||||
_on_key(event.keycode)
|
||||
|
||||
|
||||
func _on_key(kc: int) -> void:
|
||||
match kc:
|
||||
KEY_F2:
|
||||
_screenshot()
|
||||
KEY_SPACE:
|
||||
if _anim:
|
||||
_anim.set("playing", not _anim.get("playing"))
|
||||
KEY_B:
|
||||
if _anim:
|
||||
_anim.set("playing", false)
|
||||
_anim.call("set_time", 0.0)
|
||||
KEY_R:
|
||||
if _model:
|
||||
_model.call("reload")
|
||||
if _anim:
|
||||
_anim.call("reload")
|
||||
_frame_model()
|
||||
KEY_BRACKETLEFT:
|
||||
if _anim:
|
||||
_anim.call("set_time", _anim.call("get_time") - 0.05)
|
||||
KEY_BRACKETRIGHT:
|
||||
if _anim:
|
||||
_anim.call("set_time", _anim.call("get_time") + 0.05)
|
||||
|
||||
|
||||
func _screenshot() -> void:
|
||||
|
||||
Reference in New Issue
Block a user