Files
mtgodot-poc/project/python_3d_surface.gd
T
shenleiandClaude Opus 5.5 ac4c2aa4e0 2V2-e.3: live smoke of the Godot render adapter
Fix the material cache key: bools given to %d failed to format, so every
draw shared the first material (the face texture). The render test now
checks one texture per texture name. Offline and live (192.168.21.203)
show the dressed main character; live also shows the nearby mobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:42:24 +09:00

210 lines
8.7 KiB
GDScript

# 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