extends Node3D ## mtgodot-poc harness — M1 (static render) + M2 (skeletal animation). ## ## 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. ## ## 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.15 const PITCH_LIMIT := 1.45 var _assets_root := "" var _cam: Camera3D 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() _load_content() _frame_model() print("[mtgodot] harness ready") var autoshot := OS.get_environment("MTGODOT_AUTOSHOT") if autoshot != "": # 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) get_tree().quit() func _build_environment() -> void: var we := WorldEnvironment.new() var env := Environment.new() var sky := Sky.new() sky.sky_material = ProceduralSkyMaterial.new() 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) func _build_sun() -> void: var sun := DirectionalLight3D.new() 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 = 45.0 _cam.near = 0.01 add_child(_cam) _update_camera() func _env(name: String, fallback: String) -> String: var v := OS.get_environment(name) return v if v != "" else fallback func _load_content() -> void: if not ClassDB.class_exists("Metin2Model"): push_error("[mtgodot] Metin2Model not registered — GDExtension failed to load") return 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: var offset := Vector3( cos(_pitch) * sin(_yaw), sin(_pitch), cos(_pitch) * cos(_yaw)) * _dist _cam.position = _target + offset _cam.look_at(_target, Vector3.UP) func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseButton: var mb := event as InputEventMouseButton match mb.button_index: MOUSE_BUTTON_LEFT: _dragging = mb.pressed MOUSE_BUTTON_WHEEL_UP: _dist = maxf(0.2, _dist * (1.0 - ZOOM_STEP)) _update_camera() MOUSE_BUTTON_WHEEL_DOWN: _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 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: var img := get_viewport().get_texture().get_image() var path := "user://shot_%d.png" % Time.get_ticks_msec() img.save_png(path) print("[mtgodot] screenshot -> ", ProjectSettings.globalize_path(path))