# 3D adapter for the ported game render (docs/PORT-PLAN.md 2V2-e): every frame it turns the draw calls # CPythonApplication::RenderGame issued through CStateManager (Metin2PythonHost.render3d_draws, recorded # by the platform IDirect3DDevice8) into meshes under a camera. # # The draws carry 40250's own D3D matrices. CGraphicBase::SetPerspective builds a right-handed # projection (D3DXMatrixPerspectiveFovRH), so 40250's view space already is Godot's camera space # (x right, y up, looking down -z): each mesh gets world * view as its transform and the Camera3D stays # at the origin with the fov/near/far read back from the projection. No axis conversion is involved. # D3D's default front face (clockwise on screen, D3DCULL_CCW culls the rest) is also Godot's. # # PORT: until the map is ported, CMapManager has no environment data (LoadMap → SetEnvironmentData), # so CPythonBackground::SetCharacterDirLight sets no light and D3DRS_AMBIENT stays 0: D3D would draw # every lit mesh black. Such draws (lighting on, no light, no ambient, no emissive) are shown unshaded, # texture only, and counted in unlit_stand_in_count. Delete this with the environment port. extends Node3D 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 D3DBLEND_ONE := 2 const D3DBLEND_SRCALPHA := 5 const D3DBLEND_INVSRCALPHA := 6 var camera: Camera3D var light: DirectionalLight3D var environment: WorldEnvironment var _meshes: Array[MeshInstance3D] = [] # Materials keyed by texture and state, so an unchanged draw keeps its material from frame to frame. var _materials := {} # The draws the last update_frame() showed (for tests and reports). var draw_count := 0 # How many of them used the no-environment stand-in (see the note at the top). var unlit_stand_in_count := 0 func _ready() -> void: camera = Camera3D.new() camera.name = "GameCamera" camera.keep_aspect = Camera3D.KEEP_HEIGHT camera.current = true add_child(camera) # 40250's directional light (CPythonBackground::SetCharacterDirLight) arrives with the draws in # world space; it is placed in camera space each frame like the meshes. light = DirectionalLight3D.new() light.name = "Light0" light.visible = false 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.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR environment.environment.ambient_light_color = Color.BLACK environment.environment.tonemap_mode = Environment.TONE_MAPPER_LINEAR add_child(environment) func _process(_delta: float) -> void: if Metin2PythonHost.is_running(): update_frame() # 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. 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: var draws: Array = Metin2PythonHost.render3d_draws() var shown := 0 var stand_ins := 0 var camera_set := false var light_set := false 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(): continue var proj: PackedFloat32Array = draw["proj"] # _34 == -1: a perspective projection (the game pass). Ortho draws are UI-space. if proj[11] != -1.0: continue if not camera_set: _apply_projection(proj) camera_set = true var view: PackedFloat32Array = draw["view"] if not light_set and draw["light0"]: _apply_light(draw, view) light_set = true if _environment_missing(draw): stand_ins += 1 var instance := _mesh_instance(shown) instance.transform = d3d_transform(multiply(draw["world"], view)) instance.mesh = _build_mesh(draw) instance.visible = true shown += 1 for i in range(shown, _meshes.size()): _meshes[i].visible = false _meshes[i].mesh = null if not light_set: light.visible = false draw_count = shown unlit_stand_in_count = stand_ins 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])) camera.near = proj[14] / proj[10] camera.far = proj[14] / (proj[10] + 1.0) func _apply_light(draw: Dictionary, view: PackedFloat32Array) -> void: var view_basis := d3d_transform(view).basis var direction: Vector3 = view_basis * draw["light0_direction"] if direction.length_squared() < 1e-8: light.visible = false return light.visible = true # DirectionalLight3D shines along its -Z. light.transform = Transform3D(Basis.looking_at(direction.normalized(), Vector3.UP if absf(direction.normalized().y) < 0.99 else Vector3.RIGHT), Vector3.ZERO) var diffuse: Color = draw["light0_diffuse"] light.light_color = Color(diffuse.r, diffuse.g, diffuse.b) light.light_energy = 1.0 # D3D lighting: ambient = D3DRS_AMBIENT + light ambient, both times the material ambient. var ambient: Color = draw["ambient"] var light_ambient: Color = draw["light0_ambient"] environment.environment.ambient_light_color = Color(minf(ambient.r + light_ambient.r, 1.0), minf(ambient.g + light_ambient.g, 1.0), minf(ambient.b + light_ambient.b, 1.0)) static func _environment_missing(draw: Dictionary) -> bool: var ambient: Color = draw["ambient"] var emissive: Color = draw["material_emissive"] return draw["lighting"] and not draw["light0"] and ambient.r + ambient.g + ambient.b == 0.0 \ and emissive.r + emissive.g + emissive.b == 0.0 func _mesh_instance(index: int) -> MeshInstance3D: while _meshes.size() <= index: var instance := MeshInstance3D.new() instance.name = "Draw%d" % _meshes.size() instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF add_child(instance) _meshes.append(instance) return _meshes[index] func _build_mesh(draw: Dictionary) -> ArrayMesh: var arrays := [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = draw["positions"] if draw.has("normals"): arrays[Mesh.ARRAY_NORMAL] = draw["normals"] if draw.has("uv0"): arrays[Mesh.ARRAY_TEX_UV] = draw["uv0"] if draw.has("diffuse"): arrays[Mesh.ARRAY_COLOR] = draw["diffuse"] 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"] and not _environment_missing(draw) # 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"], draw["dest_blend"], int(draw["alpha_test"]), draw["alpha_ref"], draw["cull_mode"], int(lit), draw["material_diffuse"], draw["material_emissive"]] if _materials.has(key): return _materials[key] var material := StandardMaterial3D.new() var texture_name: String = draw["texture0"] 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 material.emission_enabled = lit and draw["material_emissive"] != Color(0, 0, 0, 0) if material.emission_enabled: material.emission = draw["material_emissive"] material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX if lit \ else BaseMaterial3D.SHADING_MODE_UNSHADED material.specular_mode = BaseMaterial3D.SPECULAR_DISABLED material.roughness = 1.0 material.vertex_color_use_as_albedo = draw.has("diffuse") match int(draw["cull_mode"]): D3DCULL_NONE: material.cull_mode = BaseMaterial3D.CULL_DISABLED D3DCULL_CW: material.cull_mode = BaseMaterial3D.CULL_FRONT _: material.cull_mode = BaseMaterial3D.CULL_BACK if draw["alpha_blend"]: material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA if int(draw["dest_blend"]) == D3DBLEND_ONE: material.blend_mode = BaseMaterial3D.BLEND_MODE_ADD elif draw["alpha_test"]: material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR material.alpha_scissor_threshold = float(draw["alpha_ref"]) / 255.0 _materials[key] = material return material