2V2-e.5b: feed native map height and light into game render
This commit is contained in:
@@ -30,7 +30,6 @@ var map_path := "metin2_map_a1"
|
||||
var _atlas: Array = []
|
||||
var _terrain_attempted := false
|
||||
var _last_terrain_focus := Vector2(INF, INF)
|
||||
var _ground_offset_cm := NAN
|
||||
var _meshes: Array[MeshInstance3D] = []
|
||||
# Materials keyed by texture and state, so an unchanged draw keeps its material from frame to frame.
|
||||
var _materials := {}
|
||||
@@ -84,6 +83,13 @@ static func multiply(a: PackedFloat32Array, b: PackedFloat32Array) -> PackedFloa
|
||||
|
||||
func update_frame() -> void:
|
||||
var draws: Array = Metin2PythonHost.render3d_draws()
|
||||
var native_map: String = Metin2PythonHost.current_map_name()
|
||||
if terrain != null and not native_map.is_empty() and native_map != map_path:
|
||||
terrain.queue_free()
|
||||
terrain = null
|
||||
terrain_report = {}
|
||||
_terrain_attempted = false
|
||||
_last_terrain_focus = Vector2(INF, INF)
|
||||
var shown := 0
|
||||
var camera_set := false
|
||||
var light_set := false
|
||||
@@ -106,9 +112,6 @@ func update_frame() -> void:
|
||||
if not light_set and draw["light0"]:
|
||||
_apply_light(draw, view)
|
||||
light_set = true
|
||||
elif not light_set and not terrain_report.is_empty():
|
||||
_apply_map_light(view)
|
||||
light_set = light.visible
|
||||
var instance := _mesh_instance(shown)
|
||||
instance.transform = d3d_transform(multiply(draw["world"], view))
|
||||
instance.mesh = _build_mesh(draw)
|
||||
@@ -130,7 +133,8 @@ func _load_terrain(draw: Dictionary) -> void:
|
||||
if focus.distance_to(_last_terrain_focus) < 1000.0:
|
||||
return
|
||||
_last_terrain_focus = focus
|
||||
map_path = _map_for_position(focus)
|
||||
var native_map: String = Metin2PythonHost.current_map_name()
|
||||
map_path = native_map if not native_map.is_empty() else _map_for_position(focus)
|
||||
terrain = ClassDB.instantiate("Metin2World")
|
||||
terrain.name = "PackTerrain"
|
||||
terrain.set("auto_load", false)
|
||||
@@ -142,7 +146,9 @@ func _load_terrain(draw: Dictionary) -> void:
|
||||
terrain.call("set_collision_enabled", false)
|
||||
# A skinned mesh's world matrix can be local to a bone. The inverse view
|
||||
# locates the camera beside the actor and selects the correct 3x3 map tiles.
|
||||
terrain.call("set_initial_focus_cm", focus)
|
||||
# Native RenderGame coordinates are map-local; Metin2World's initial_focus_cm
|
||||
# expects global AtlasInfo coordinates, so set the local tile directly.
|
||||
terrain.call("set_focus_tile", Vector2i(int(floor(focus.x / 25600.0)), int(floor(focus.y / 25600.0))))
|
||||
add_child(terrain)
|
||||
if not terrain.call("load_map"):
|
||||
terrain_report = terrain.call("get_load_report")
|
||||
@@ -159,7 +165,6 @@ func _load_terrain(draw: Dictionary) -> void:
|
||||
terrain_report = {}
|
||||
return
|
||||
_terrain_attempted = true
|
||||
_ground_offset_cm = NAN
|
||||
# The map's own character light uses legacy Godot coordinates; the recorded game pass
|
||||
# needs one light in camera space, derived from the same .msenv values below.
|
||||
var map_character_light := terrain.get_node_or_null("CharacterLight") as DirectionalLight3D
|
||||
@@ -190,37 +195,14 @@ func _map_for_position(global_cm: Vector2) -> String:
|
||||
return map_path
|
||||
|
||||
func _place_terrain(view: PackedFloat32Array) -> void:
|
||||
var base: Vector2 = terrain.call("get_map_base_cm")
|
||||
var camera_world := d3d_transform(view).affine_inverse().origin
|
||||
var local_x := (camera_world.x - base.x) / 100.0
|
||||
var local_y := (-camera_world.y - base.y) / 100.0
|
||||
var local_x := camera_world.x / 100.0
|
||||
var local_y := -camera_world.y / 100.0
|
||||
terrain.call("set_focus_position", local_x, local_y)
|
||||
# Until the native CMapManager::GetHeight path is ported, the actor enters at
|
||||
# z=0. Align the map once to its actual sampled starting elevation.
|
||||
if is_nan(_ground_offset_cm):
|
||||
_ground_offset_cm = float(terrain.call("sample_height", local_x, local_y)) * 100.0
|
||||
var from_map := Transform3D(Basis(Vector3(100, 0, 0), Vector3(0, 0, 100),
|
||||
Vector3(0, -100, 0)), Vector3(base.x, -base.y, -_ground_offset_cm))
|
||||
Vector3(0, -100, 0)), Vector3.ZERO)
|
||||
terrain.transform = d3d_transform(view) * from_map
|
||||
|
||||
func _apply_map_light(view: PackedFloat32Array) -> void:
|
||||
if not terrain_report.get("env_ok", false) or not terrain_report.get("character_light_enabled", false):
|
||||
light.visible = false
|
||||
return
|
||||
var direction: Vector3 = d3d_transform(view).basis * terrain_report["character_light_direction"]
|
||||
if direction.length_squared() < 1e-8:
|
||||
light.visible = false
|
||||
return
|
||||
light.visible = true
|
||||
light.transform = Transform3D(Basis.looking_at(direction.normalized(),
|
||||
Vector3.UP if absf(direction.normalized().y) < 0.99 else Vector3.RIGHT), Vector3.ZERO)
|
||||
light.light_color = terrain_report["character_light_diffuse"]
|
||||
light.light_energy = 1.0
|
||||
var world_env := terrain.get_node_or_null("WorldEnv") as WorldEnvironment
|
||||
if world_env and world_env.environment:
|
||||
world_env.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
world_env.environment.ambient_light_color = terrain_report["character_light_ambient"]
|
||||
|
||||
func _apply_projection(proj: PackedFloat32Array) -> void:
|
||||
# D3DXMatrixPerspectiveFovRH: _22 = cot(fovy / 2), _33 = zf / (zn - zf), _43 = zn * zf / (zn - zf).
|
||||
camera.fov = rad_to_deg(2.0 * atan(1.0 / proj[5]))
|
||||
|
||||
@@ -109,9 +109,17 @@ func run() -> void:
|
||||
report["unlit_stand_in_count"] = world.unlit_stand_in_count
|
||||
report["terrain"] = world.terrain_report
|
||||
report["atlas_entries"] = world._atlas.size()
|
||||
_check(world._atlas.size() > 0, "40250 AtlasInfo chooses the current map")
|
||||
_check(not Metin2PythonHost.current_map_name().is_empty(), "native AtlasInfo chooses the current map")
|
||||
if world.terrain:
|
||||
report["terrain_base_cm"] = str(world.terrain.call("get_map_base_cm"))
|
||||
report["native_map_name"] = Metin2PythonHost.current_map_name()
|
||||
_check(world.map_path == Metin2PythonHost.current_map_name(), "terrain uses native Warp map")
|
||||
var first_draws: Array = Metin2PythonHost.render3d_draws()
|
||||
if not first_draws.is_empty():
|
||||
var camera_world: Vector3 = world.d3d_transform(first_draws[0]["view"]).affine_inverse().origin
|
||||
report["camera_world_cm"] = str(camera_world)
|
||||
report["terrain_height_at_camera_cm"] = float(world.terrain.call("sample_height",
|
||||
camera_world.x / 100.0, -camera_world.y / 100.0)) * 100.0
|
||||
var tile_bounds := []
|
||||
for tile in world.terrain.find_children("Terrain*", "MeshInstance3D", true, false):
|
||||
if tile_bounds.size() >= 4:
|
||||
@@ -130,6 +138,33 @@ func run() -> void:
|
||||
report["camera_far"] = world.camera.far
|
||||
var textures := []
|
||||
var draws: Array = Metin2PythonHost.render3d_draws()
|
||||
var native_lit_draws := 0
|
||||
var view_finite := true
|
||||
var actor_foot_cm := INF
|
||||
var foot_xy := Vector2.ZERO
|
||||
for draw in draws:
|
||||
if draw["light0"]:
|
||||
native_lit_draws += 1
|
||||
for value in draw["view"]:
|
||||
if not is_finite(value):
|
||||
view_finite = false
|
||||
var world_xform: Transform3D = world.d3d_transform(draw["world"])
|
||||
for vertex in draw["positions"]:
|
||||
var p: Vector3 = world_xform * vertex
|
||||
if p.z < actor_foot_cm:
|
||||
actor_foot_cm = p.z
|
||||
foot_xy = Vector2(p.x, -p.y)
|
||||
report["native_lit_draws"] = native_lit_draws
|
||||
report["view_finite"] = view_finite
|
||||
_check(view_finite, "native camera view stays finite")
|
||||
if world.terrain and not is_inf(actor_foot_cm):
|
||||
var ground_cm := float(world.terrain.call("sample_height", foot_xy.x / 100.0, foot_xy.y / 100.0)) * 100.0
|
||||
report["actor_foot_cm"] = actor_foot_cm
|
||||
report["ground_at_foot_cm"] = ground_cm
|
||||
_check(absf(actor_foot_cm - ground_cm) < 120.0,
|
||||
"native actor feet meet map height (delta %.1f cm)" % absf(actor_foot_cm - ground_cm))
|
||||
_check(not drawn or native_lit_draws == draws.size(),
|
||||
"native RenderGame light0 comes from map .msenv (%d/%d)" % [native_lit_draws, draws.size()])
|
||||
if not draws.is_empty():
|
||||
report["proj"] = Array(draws[0]["proj"])
|
||||
report["view"] = Array(draws[0]["view"])
|
||||
@@ -193,18 +228,19 @@ func run() -> void:
|
||||
lit += 1
|
||||
report["centre_lit_samples"] = lit
|
||||
_check(lit > 50, "character pixels in the middle of the screen (%d samples)" % lit)
|
||||
var ground_low := 1.0
|
||||
var ground_high := 0.0
|
||||
for y in range(image.get_height() * 3 / 4, image.get_height() - 20, 17):
|
||||
for x in range(30, image.get_width() - 30, 23):
|
||||
var luminance := image.get_pixel(x, y).get_luminance()
|
||||
ground_low = minf(ground_low, luminance)
|
||||
ground_high = maxf(ground_high, luminance)
|
||||
report["ground_luminance_range"] = ground_high - ground_low
|
||||
_check(ground_high - ground_low > 0.08, "textured ground visible in screenshot")
|
||||
# The 3D pass alone, for looking at the character without the UI.
|
||||
ui.visible = false
|
||||
await RenderingServer.frame_post_draw
|
||||
root.get_texture().get_image().save_png(output.path_join("game_3d.png"))
|
||||
var game_image := root.get_texture().get_image()
|
||||
game_image.save_png(output.path_join("game_3d.png"))
|
||||
var textured_pairs := 0
|
||||
for y in range(game_image.get_height() * 3 / 4, game_image.get_height() - 20, 13):
|
||||
for x in range(30, game_image.get_width() - 35, 17):
|
||||
var left := game_image.get_pixel(x, y).get_luminance()
|
||||
var right := game_image.get_pixel(x + 5, y).get_luminance()
|
||||
if absf(left - right) > 0.04:
|
||||
textured_pairs += 1
|
||||
report["ground_texture_pairs"] = textured_pairs
|
||||
_check(textured_pairs > 20, "textured ground visible in screenshot (%d local contrasts)" % textured_pairs)
|
||||
ui.visible = true
|
||||
await _finish(output, report)
|
||||
|
||||
Reference in New Issue
Block a user