feat: complete native client rendering parity updates
This commit is contained in:
+164
-29
@@ -12,11 +12,13 @@
|
||||
# in the recorded D3D camera's coordinate frame, and its .msenv supplies the character light.
|
||||
extends Node3D
|
||||
|
||||
const AssetRoot = preload("res://asset_root.gd")
|
||||
const UiAssets = preload("res://ui/ui_assets.gd")
|
||||
|
||||
# D3DCULL / D3DBLEND / D3DCMPFUNC values used below (D3D8Types.h).
|
||||
const D3DCULL_NONE := 1
|
||||
const D3DCULL_CW := 2
|
||||
const D3DCULL_CCW := 3
|
||||
const D3DBLEND_ONE := 2
|
||||
const D3DBLEND_SRCALPHA := 5
|
||||
const D3DBLEND_INVSRCALPHA := 6
|
||||
@@ -31,6 +33,11 @@ var _atlas: Array = []
|
||||
var _terrain_attempted := false
|
||||
var _last_terrain_focus := Vector2(INF, INF)
|
||||
var _meshes: Array[MeshInstance3D] = []
|
||||
var _geometry_cache := {}
|
||||
var _geometry_frame := 0
|
||||
var _bg_mesh: MeshInstance3D
|
||||
var _bg_mat: StandardMaterial3D
|
||||
var _bg_quad: QuadMesh
|
||||
# Materials keyed by texture and state, so an unchanged draw keeps its material from frame to frame.
|
||||
var _materials := {}
|
||||
|
||||
@@ -39,6 +46,7 @@ var draw_count := 0
|
||||
var unlit_stand_in_count := 0 # Compatibility report field; real lighting keeps this at zero.
|
||||
|
||||
func _ready() -> void:
|
||||
get_viewport().transparent_bg = true
|
||||
camera = Camera3D.new()
|
||||
camera.name = "GameCamera"
|
||||
camera.keep_aspect = Camera3D.KEEP_HEIGHT
|
||||
@@ -53,8 +61,7 @@ func _ready() -> void:
|
||||
add_child(light)
|
||||
environment = WorldEnvironment.new()
|
||||
environment.environment = Environment.new()
|
||||
environment.environment.background_mode = Environment.BG_COLOR
|
||||
environment.environment.background_color = Color.BLACK
|
||||
environment.environment.background_mode = Environment.BG_CLEAR_COLOR
|
||||
environment.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
environment.environment.ambient_light_color = Color.BLACK
|
||||
environment.environment.tonemap_mode = Environment.TONE_MAPPER_LINEAR
|
||||
@@ -62,7 +69,13 @@ func _ready() -> void:
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if Metin2PythonHost.is_running():
|
||||
var profile_start := Time.get_ticks_usec() if _profile_frame else 0
|
||||
update_frame()
|
||||
if profile_start != 0:
|
||||
profile_update_ms = float(Time.get_ticks_usec() - profile_start) / 1000.0
|
||||
|
||||
var _profile_frame := OS.has_environment("MT_PROFILE_FRAME")
|
||||
var profile_update_ms := 0.0
|
||||
|
||||
# D3D row-vector matrix (v' = v * M) as a Godot Transform3D (v' = T * v): the rows of M are the basis
|
||||
# columns, row 3 is the origin.
|
||||
@@ -70,18 +83,8 @@ static func d3d_transform(m: PackedFloat32Array) -> Transform3D:
|
||||
return Transform3D(Basis(Vector3(m[0], m[1], m[2]), Vector3(m[4], m[5], m[6]), Vector3(m[8], m[9], m[10])),
|
||||
Vector3(m[12], m[13], m[14]))
|
||||
|
||||
static func multiply(a: PackedFloat32Array, b: PackedFloat32Array) -> PackedFloat32Array:
|
||||
var out := PackedFloat32Array()
|
||||
out.resize(16)
|
||||
for r in 4:
|
||||
for c in 4:
|
||||
var sum := 0.0
|
||||
for k in 4:
|
||||
sum += a[r * 4 + k] * b[k * 4 + c]
|
||||
out[r * 4 + c] = sum
|
||||
return out
|
||||
|
||||
func update_frame() -> void:
|
||||
_geometry_frame += 1
|
||||
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:
|
||||
@@ -93,6 +96,7 @@ func update_frame() -> void:
|
||||
var shown := 0
|
||||
var camera_set := false
|
||||
var light_set := false
|
||||
var terrain_view := PackedFloat32Array()
|
||||
for draw in draws:
|
||||
# XYZRHW (screen-space) draws belong to the 2D pass; lines are debug geometry.
|
||||
if draw["pretransformed"] or draw["lines"] or draw["indices"].is_empty():
|
||||
@@ -102,21 +106,25 @@ func update_frame() -> void:
|
||||
if proj[11] != -1.0:
|
||||
continue
|
||||
if not camera_set:
|
||||
_apply_projection(proj)
|
||||
_apply_projection(proj, draw)
|
||||
camera_set = true
|
||||
var view: PackedFloat32Array = draw["view"]
|
||||
if terrain == null and not _terrain_attempted:
|
||||
_load_terrain(draw)
|
||||
if terrain != null:
|
||||
_place_terrain(view)
|
||||
terrain_view = view
|
||||
if not light_set and draw["light0"]:
|
||||
_apply_light(draw, view)
|
||||
light_set = true
|
||||
var instance := _mesh_instance(shown)
|
||||
instance.transform = d3d_transform(multiply(draw["world"], view))
|
||||
instance.mesh = _build_mesh(draw)
|
||||
instance.transform = d3d_transform(view) * d3d_transform(draw["world"])
|
||||
instance.mesh = _mesh_for_draw(draw)
|
||||
instance.material_override = _material(draw)
|
||||
instance.sorting_offset = float(shown) * 0.01
|
||||
instance.visible = true
|
||||
shown += 1
|
||||
if terrain != null and not terrain_view.is_empty():
|
||||
_place_terrain(terrain_view)
|
||||
for i in range(shown, _meshes.size()):
|
||||
_meshes[i].visible = false
|
||||
_meshes[i].mesh = null
|
||||
@@ -124,17 +132,75 @@ func update_frame() -> void:
|
||||
light.visible = false
|
||||
draw_count = shown
|
||||
unlit_stand_in_count = 0
|
||||
_update_background()
|
||||
if _geometry_frame % 120 == 0:
|
||||
for key in _geometry_cache.keys():
|
||||
if _geometry_frame - int(_geometry_cache[key][2]) > 120:
|
||||
_geometry_cache.erase(key)
|
||||
|
||||
func _update_background() -> void:
|
||||
var bg_cmd: Dictionary = {}
|
||||
for cmd in Metin2PythonHost.ui_render_commands_batched():
|
||||
if cmd.get("behind_3d", false) and cmd.get("kind", "") == "image":
|
||||
bg_cmd = cmd
|
||||
break
|
||||
if bg_cmd.is_empty():
|
||||
if _bg_mesh != null:
|
||||
_bg_mesh.visible = false
|
||||
return
|
||||
|
||||
if _bg_mesh == null:
|
||||
_bg_mesh = MeshInstance3D.new()
|
||||
_bg_mesh.name = "BackgroundQuad"
|
||||
_bg_mesh.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
_bg_quad = QuadMesh.new()
|
||||
_bg_mesh.mesh = _bg_quad
|
||||
_bg_mat = StandardMaterial3D.new()
|
||||
_bg_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
_bg_mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_ALWAYS
|
||||
_bg_mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
_bg_mesh.material_override = _bg_mat
|
||||
add_child(_bg_mesh)
|
||||
|
||||
var bg_tex_name: String = bg_cmd.get("text", "")
|
||||
var tex := UiAssets.load_tex(AssetRoot.path(), bg_tex_name)
|
||||
if tex is AtlasTexture:
|
||||
var at: AtlasTexture = tex
|
||||
_bg_mat.albedo_texture = at.atlas
|
||||
var asize: Vector2 = at.atlas.get_size()
|
||||
var r: Rect2 = at.region
|
||||
_bg_mat.uv1_scale = Vector3(r.size.x / asize.x, r.size.y / asize.y, 1.0)
|
||||
_bg_mat.uv1_offset = Vector3(r.position.x / asize.x, r.position.y / asize.y, 0.0)
|
||||
else:
|
||||
_bg_mat.albedo_texture = tex
|
||||
if bg_cmd.has("uv"):
|
||||
var uvs: PackedVector2Array = bg_cmd["uv"]
|
||||
_bg_mat.uv1_scale = Vector3(uvs[3].x - uvs[0].x, uvs[3].y - uvs[0].y, 1.0)
|
||||
_bg_mat.uv1_offset = Vector3(uvs[0].x, uvs[0].y, 0.0)
|
||||
else:
|
||||
_bg_mat.uv1_scale = Vector3(1.0, 1.0, 1.0)
|
||||
_bg_mat.uv1_offset = Vector3(0.0, 0.0, 0.0)
|
||||
|
||||
var vp_size := get_viewport().get_visible_rect().size
|
||||
var dist := 2800.0
|
||||
var v_size := 2.0 * dist * tan(deg_to_rad(camera.fov) / 2.0)
|
||||
var h_size := v_size * (float(vp_size.x) / float(vp_size.y))
|
||||
_bg_quad.size = Vector2(h_size, v_size)
|
||||
_bg_mesh.transform = Transform3D(Basis(), Vector3(0.0, 0.0, -dist))
|
||||
_bg_mesh.visible = true
|
||||
|
||||
func _load_terrain(draw: Dictionary) -> void:
|
||||
if not ClassDB.class_exists("Metin2World"):
|
||||
return
|
||||
var native_map: String = Metin2PythonHost.current_map_name()
|
||||
if native_map.is_empty():
|
||||
return
|
||||
var camera_world := d3d_transform(draw["view"]).affine_inverse().origin
|
||||
var focus := Vector2(camera_world.x, -camera_world.y)
|
||||
if focus.distance_to(_last_terrain_focus) < 1000.0:
|
||||
return
|
||||
_last_terrain_focus = focus
|
||||
var native_map: String = Metin2PythonHost.current_map_name()
|
||||
map_path = native_map if not native_map.is_empty() else _map_for_position(focus)
|
||||
map_path = native_map
|
||||
terrain = ClassDB.instantiate("Metin2World")
|
||||
terrain.name = "PackTerrain"
|
||||
terrain.set("auto_load", false)
|
||||
@@ -212,11 +278,38 @@ func _place_terrain(view: PackedFloat32Array) -> void:
|
||||
Vector3(0, -100, 0)), Vector3.ZERO)
|
||||
terrain.transform = d3d_transform(view) * from_map
|
||||
|
||||
func _apply_projection(proj: PackedFloat32Array) -> void:
|
||||
func _apply_projection(proj: PackedFloat32Array, draw: Dictionary = {}) -> 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]))
|
||||
camera.near = proj[14] / proj[10]
|
||||
camera.far = proj[14] / (proj[10] + 1.0)
|
||||
var fov_deg := rad_to_deg(2.0 * atan(1.0 / proj[5]))
|
||||
var zn: float = proj[14] / proj[10]
|
||||
var zf: float = proj[14] / (proj[10] + 1.0)
|
||||
camera.fov = fov_deg
|
||||
camera.near = zn
|
||||
camera.far = zf
|
||||
|
||||
var vp_offset := Vector2.ZERO
|
||||
if draw.has("viewport"):
|
||||
var vp: PackedFloat32Array = draw["viewport"]
|
||||
var vp_x: float = vp[0]
|
||||
var vp_y: float = vp[1]
|
||||
var vp_w: float = vp[2]
|
||||
var vp_h: float = vp[3]
|
||||
var vp_size := get_viewport().get_visible_rect().size
|
||||
if vp_w > 0.0 and vp_h > 0.0 and (vp_w < vp_size.x or vp_h < vp_size.y or vp_x > 0.0 or vp_y > 0.0):
|
||||
var offset_px_x: float = (vp_x + vp_w / 2.0) - (vp_size.x / 2.0)
|
||||
var offset_px_y: float = (vp_y + vp_h / 2.0) - (vp_size.y / 2.0)
|
||||
if absf(offset_px_x) > 0.5 or absf(offset_px_y) > 0.5:
|
||||
var v_size_near := 2.0 * zn * tan(deg_to_rad(fov_deg) / 2.0)
|
||||
var h_size_near := v_size_near * (vp_size.x / vp_size.y)
|
||||
var off_near_x := (offset_px_x / vp_size.x) * h_size_near
|
||||
var off_near_y := -(offset_px_y / vp_size.y) * v_size_near
|
||||
vp_offset = Vector2(-off_near_x, off_near_y)
|
||||
|
||||
var v_size_near := 2.0 * zn * tan(deg_to_rad(fov_deg) / 2.0)
|
||||
if vp_offset != Vector2.ZERO:
|
||||
camera.set_frustum(v_size_near, vp_offset, zn, zf)
|
||||
else:
|
||||
camera.set_perspective(fov_deg, zn, zf)
|
||||
|
||||
func _apply_light(draw: Dictionary, view: PackedFloat32Array) -> void:
|
||||
var view_basis := d3d_transform(view).basis
|
||||
@@ -253,6 +346,19 @@ func _mesh_instance(index: int) -> MeshInstance3D:
|
||||
_meshes.append(instance)
|
||||
return _meshes[index]
|
||||
|
||||
func _mesh_for_draw(draw: Dictionary) -> ArrayMesh:
|
||||
var key: int = draw.get("geometry_key", 0)
|
||||
if key == 0:
|
||||
return _build_mesh(draw)
|
||||
var revision: int = draw.get("geometry_revision", 0)
|
||||
var cached: Array = _geometry_cache.get(key, [])
|
||||
if not cached.is_empty() and int(cached[0]) == revision:
|
||||
cached[2] = _geometry_frame
|
||||
return cached[1]
|
||||
var mesh := _build_mesh(draw)
|
||||
_geometry_cache[key] = [revision, mesh, _geometry_frame]
|
||||
return mesh
|
||||
|
||||
func _build_mesh(draw: Dictionary) -> ArrayMesh:
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
@@ -266,15 +372,27 @@ func _build_mesh(draw: Dictionary) -> ArrayMesh:
|
||||
arrays[Mesh.ARRAY_INDEX] = draw["indices"]
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
mesh.surface_set_material(0, _material(draw))
|
||||
return mesh
|
||||
|
||||
func _material(draw: Dictionary) -> StandardMaterial3D:
|
||||
var lit: bool = draw["lighting"]
|
||||
var z_write: int = draw.get("z_write", 1)
|
||||
var tf: int = int(draw.get("texture_factor", 0xFFFFFFFF))
|
||||
var tf_a: float = float((tf >> 24) & 0xFF) / 255.0
|
||||
var tf_r: float = float((tf >> 16) & 0xFF) / 255.0
|
||||
var tf_g: float = float((tf >> 8) & 0xFF) / 255.0
|
||||
var tf_b: float = float(tf & 0xFF) / 255.0
|
||||
var factor_color := Color(tf_r, tf_g, tf_b, tf_a)
|
||||
|
||||
var uses_tf: bool = bool(draw.get("uses_tf", false))
|
||||
var has_tf: bool = uses_tf and (tf != 0xFFFFFFFF and tf != -1)
|
||||
var tf_key := "%02x%02x%02x%02x" % [int(tf_r * 63.0), int(tf_g * 63.0), int(tf_b * 63.0), int(tf_a * 63.0)] if has_tf else ""
|
||||
var has_diffuse: bool = draw.has("diffuse")
|
||||
var is_alpha: bool = draw["alpha_blend"] or (has_tf and factor_color.a < 0.99)
|
||||
# alpha_blend / alpha_test / lit are bools: %d needs them as ints.
|
||||
var key := "%s|%d|%d|%d|%d|%d|%d|%d|%s|%s" % [draw["texture0"], int(draw["alpha_blend"]), draw["src_blend"],
|
||||
var key := "%s|%d|%d|%d|%d|%d|%d|%d|%d|%d|%s|%s|%s" % [draw["texture0"], int(draw["alpha_blend"]), draw["src_blend"],
|
||||
draw["dest_blend"], int(draw["alpha_test"]), draw["alpha_ref"], draw["cull_mode"], int(lit),
|
||||
draw["material_diffuse"], draw["material_emissive"]]
|
||||
int(has_diffuse), z_write, draw["material_diffuse"], draw["material_emissive"], tf_key]
|
||||
if _materials.has(key):
|
||||
return _materials[key]
|
||||
var material := StandardMaterial3D.new()
|
||||
@@ -282,7 +400,14 @@ func _material(draw: Dictionary) -> StandardMaterial3D:
|
||||
if not texture_name.is_empty():
|
||||
# The ported client reads its models from 40250's packs; the texture must come from there too.
|
||||
material.albedo_texture = UiAssets.load_pack_tex(texture_name)
|
||||
material.albedo_color = draw["material_diffuse"] if lit else Color.WHITE
|
||||
|
||||
var base_diffuse: Color = draw["material_diffuse"] if lit else Color.WHITE
|
||||
if has_tf:
|
||||
material.albedo_color = Color(base_diffuse.r * factor_color.r, base_diffuse.g * factor_color.g,
|
||||
base_diffuse.b * factor_color.b, base_diffuse.a * factor_color.a)
|
||||
else:
|
||||
material.albedo_color = base_diffuse
|
||||
|
||||
material.emission_enabled = lit and draw["material_emissive"] != Color(0, 0, 0, 0)
|
||||
if material.emission_enabled:
|
||||
material.emission = draw["material_emissive"]
|
||||
@@ -296,18 +421,28 @@ func _material(draw: Dictionary) -> StandardMaterial3D:
|
||||
else BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.specular_mode = BaseMaterial3D.SPECULAR_DISABLED
|
||||
material.roughness = 1.0
|
||||
material.vertex_color_use_as_albedo = draw.has("diffuse")
|
||||
material.vertex_color_use_as_albedo = has_diffuse
|
||||
match int(draw["cull_mode"]):
|
||||
D3DCULL_NONE:
|
||||
material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
D3DCULL_CW:
|
||||
material.cull_mode = BaseMaterial3D.CULL_BACK
|
||||
D3DCULL_CCW:
|
||||
material.cull_mode = BaseMaterial3D.CULL_FRONT
|
||||
_:
|
||||
material.cull_mode = BaseMaterial3D.CULL_BACK
|
||||
if draw["alpha_blend"]:
|
||||
if z_write == 0:
|
||||
material.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
|
||||
elif is_alpha:
|
||||
material.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_ALWAYS
|
||||
else:
|
||||
material.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_OPAQUE_ONLY
|
||||
if is_alpha:
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
if int(draw["dest_blend"]) == D3DBLEND_ONE:
|
||||
material.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
||||
else:
|
||||
material.blend_mode = BaseMaterial3D.BLEND_MODE_MIX
|
||||
elif draw["alpha_test"]:
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
|
||||
material.alpha_scissor_threshold = float(draw["alpha_ref"]) / 255.0
|
||||
|
||||
Reference in New Issue
Block a user