Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
211 lines
7.7 KiB
GDScript
211 lines
7.7 KiB
GDScript
extends Node3D
|
|
## Godot/Unreal comparison stage: identical portable assets, cameras and lighting.
|
|
## Captures close-up + game-camera stills, a walk cycle, and 1/30/90 load measurements.
|
|
|
|
const CAPTURES := "res://docs/screenshots/compare"
|
|
var viewport: SubViewport
|
|
var stage: Node3D
|
|
var camera: Camera3D
|
|
var figures: Array[Node3D] = []
|
|
var results: Array = []
|
|
|
|
func _ready() -> void:
|
|
call_deferred("run")
|
|
|
|
func _load_glb(path: String) -> Node3D:
|
|
var packed := load(path) as PackedScene
|
|
if packed == null:
|
|
push_error("Missing compare asset: " + path)
|
|
return Node3D.new()
|
|
return packed.instantiate() as Node3D
|
|
|
|
func _shot(name: String) -> void:
|
|
for n: int in range(8):
|
|
await get_tree().process_frame
|
|
await RenderingServer.frame_post_draw
|
|
viewport.get_texture().get_image().save_png(CAPTURES + "/" + name + ".png")
|
|
print("SHOT: ", name)
|
|
|
|
func _aim(pos: Vector3, target: Vector3, size: float) -> void:
|
|
camera.size = size
|
|
camera.position = pos
|
|
camera.look_at(target, Vector3.UP)
|
|
await get_tree().process_frame
|
|
await get_tree().process_frame
|
|
|
|
func run() -> void:
|
|
Soundscape.enabled = false
|
|
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
|
|
get_window().size = Vector2i(960, 540)
|
|
viewport = SubViewport.new()
|
|
viewport.own_world_3d = true
|
|
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
|
viewport.msaa_3d = Viewport.MSAA_4X
|
|
viewport.size = Vector2i(1920, 1080)
|
|
add_child(viewport)
|
|
stage = Node3D.new()
|
|
viewport.add_child(stage)
|
|
var display := TextureRect.new()
|
|
display.texture = viewport.get_texture()
|
|
display.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
display.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
add_child(display)
|
|
|
|
var env := WorldEnvironment.new()
|
|
env.environment = Environment.new()
|
|
preload("res://scripts/presentation/lighting.gd").configure(env.environment, true)
|
|
stage.add_child(env)
|
|
var light := DirectionalLight3D.new()
|
|
light.rotation_degrees = Vector3(-48, -36, 0)
|
|
light.light_color = Color("ffd4a0")
|
|
light.light_energy = 1.7
|
|
light.shadow_enabled = true
|
|
stage.add_child(light)
|
|
var fill := DirectionalLight3D.new()
|
|
fill.rotation_degrees = Vector3(-35, 150, 0)
|
|
fill.light_color = Color("a8c4e8")
|
|
fill.light_energy = .55
|
|
stage.add_child(fill)
|
|
|
|
# Solid unshaded stage floor so studio light cannot blow it out.
|
|
var floor := MeshInstance3D.new()
|
|
var plane := PlaneMesh.new()
|
|
plane.size = Vector2(24, 24)
|
|
floor.mesh = plane
|
|
var floor_mat := StandardMaterial3D.new()
|
|
floor_mat.albedo_color = Color("243038")
|
|
floor_mat.roughness = 1.0
|
|
floor_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
floor.material_override = floor_mat
|
|
stage.add_child(floor)
|
|
# Arena tile as a modest platform under the figures.
|
|
var tile: Node3D = _load_glb("res://assets/compare/compare-arena-tile.glb")
|
|
tile.position = Vector3(0, .01, .35)
|
|
tile.scale = Vector3(0.85, .35, 0.7)
|
|
stage.add_child(tile)
|
|
|
|
var rock: Node3D = _load_glb("res://assets/compare/compare-rock.glb")
|
|
rock.position = Vector3(-2.8, 0, -1.0)
|
|
rock.rotation.y = .45
|
|
rock.scale = Vector3.ONE * 1.0
|
|
stage.add_child(rock)
|
|
var tree: Node3D = _load_glb("res://assets/compare/compare-tree.glb")
|
|
tree.position = Vector3(3.0, 0, -1.5)
|
|
tree.rotation.y = -.35
|
|
tree.scale = Vector3.ONE * .9
|
|
stage.add_child(tree)
|
|
|
|
var ring: Node3D = AssetLibrary.instantiate_fighter("w04")
|
|
ring.position = Vector3(-0.6, 0, 0.55)
|
|
ring.rotation.y = .2
|
|
stage.add_child(ring)
|
|
figures.append(ring)
|
|
var worker: Node3D = AssetLibrary.instantiate_fighter("worker")
|
|
worker.position = Vector3(0.9, 0, 0.7)
|
|
worker.rotation.y = -.45
|
|
stage.add_child(worker)
|
|
figures.append(worker)
|
|
var core: Node3D = AssetLibrary.instantiate_fighter("core_team0")
|
|
core.position = Vector3(0.1, 0, -1.8)
|
|
stage.add_child(core)
|
|
|
|
camera = Camera3D.new()
|
|
camera.projection = Camera3D.PROJECTION_ORTHOGONAL
|
|
camera.size = 2.4
|
|
stage.add_child(camera)
|
|
await get_tree().process_frame
|
|
|
|
# 1) Close-up of pattern figure + worker.
|
|
await _aim(Vector3(1.6, 1.6, 2.6), Vector3(0.1, 1.0, 0.2), 2.4)
|
|
print("CAM closeup size=", camera.size, " pos=", camera.position)
|
|
await _shot("closeup")
|
|
# 2) Game camera: high and wide so rock, tree, core and figures all read.
|
|
await _aim(Vector3(2.0, 14.0, 12.5), Vector3(0, 0.4, -0.2), 10.0)
|
|
print("CAM game size=", camera.size, " pos=", camera.position)
|
|
await _shot("game-camera")
|
|
# 3) Movement pass on the game camera.
|
|
for i in range(30):
|
|
var t: float = i / 10.0
|
|
if ring.has_method("preview"):
|
|
ring.preview("walk", fmod(t, ring.clip_length("walk")))
|
|
if worker.has_method("update_work"):
|
|
worker.update_work(1.0 / 10.0, 0.0)
|
|
await get_tree().process_frame
|
|
if i in [0, 12, 24]:
|
|
await RenderingServer.frame_post_draw
|
|
viewport.get_texture().get_image().save_png(CAPTURES + "/move-%02d.png" % i)
|
|
print("SHOT: move-", i)
|
|
await _shot("move-end")
|
|
|
|
# Load measurements 1 / 30 / 90 at 1080p and 1440p.
|
|
for resolution: Vector2i in [Vector2i(1920, 1080), Vector2i(2560, 1440)]:
|
|
viewport.size = resolution
|
|
for count: int in [1, 30, 90]:
|
|
for figure: Node3D in figures:
|
|
if is_instance_valid(figure):
|
|
figure.free()
|
|
figures.clear()
|
|
var columns: int = ceili(sqrt(count))
|
|
for i: int in range(count):
|
|
var figure: Node3D = AssetLibrary.instantiate_fighter("w04" if i % 2 == 0 else "worker")
|
|
figure.position = Vector3((i % columns - columns * .5) * 2.2, 0, (i / float(columns) - columns * .5) * 2.2 + 2.5)
|
|
stage.add_child(figure)
|
|
figures.append(figure)
|
|
await _aim(Vector3(6, 16, 20), Vector3(0, 0.8, 1.5), maxf(4, columns * 2.5))
|
|
var times: Array[float] = []
|
|
var started: int = Time.get_ticks_usec()
|
|
var previous: int = started
|
|
while Time.get_ticks_usec() - started < 8000000:
|
|
var elapsed: float = (Time.get_ticks_usec() - started) / 1000000.0
|
|
for i: int in range(figures.size()):
|
|
var figure: Node3D = figures[i]
|
|
if figure.has_method("preview"):
|
|
figure.preview("walk", fmod(elapsed + i * .05, 1.4))
|
|
elif figure.has_method("update_work"):
|
|
figure.update_work(1.0 / 60.0, i * .1)
|
|
await get_tree().process_frame
|
|
var now: int = Time.get_ticks_usec()
|
|
if elapsed > 2:
|
|
times.append((now - previous) / 1000.0)
|
|
previous = now
|
|
var total: float = 0
|
|
for t: float in times:
|
|
total += t
|
|
times.sort()
|
|
var record := {
|
|
"resolution": str(resolution),
|
|
"instances": count,
|
|
"mean_frame_ms": total / maxf(1, times.size()),
|
|
"p95_frame_ms": times[int(times.size() * .95)] if not times.is_empty() else 0.0,
|
|
"fps": 1000.0 / maxf(0.01, total / maxf(1, times.size())),
|
|
"draw_calls": Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME),
|
|
"video_memory_bytes": Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED),
|
|
}
|
|
results.append(record)
|
|
print("COMPARE_BENCH: ", record)
|
|
|
|
var report := {
|
|
"engine": "Godot " + Engine.get_version_info().get("string", ""),
|
|
"device": RenderingServer.get_video_adapter_name(),
|
|
"renderer": RenderingServer.get_current_rendering_method(),
|
|
"vsync": false,
|
|
"msaa": "4x",
|
|
"warmup_seconds": 2,
|
|
"sample_seconds": 6,
|
|
"scope": "Comparison stage: pattern figure, worker, core, rock, tree, arena tile. Not a full match.",
|
|
"camera": {
|
|
"projection": "orthogonal",
|
|
"closeup": {"position": [1.4, 1.5, 2.4], "look_at": [0.1, 0.95, 0.2], "size": 2.4},
|
|
"game": {"position": [1.5, 10.5, 9.5], "look_at": [0, 0.5, 0], "size": 7.0, "yaw": 0.2},
|
|
},
|
|
"lighting": {
|
|
"key": {"rotation_degrees": [-48, -36, 0], "color": "#ffd4a0", "energy": 1.7},
|
|
"fill": {"rotation_degrees": [-35, 150, 0], "color": "#a8c4e8", "energy": 0.55},
|
|
"tonemap": "ACES",
|
|
},
|
|
"captures": ["closeup", "game-camera", "move-00", "move-12", "move-24", "move-end"],
|
|
"results": results,
|
|
}
|
|
FileAccess.open("res://docs/compare-godot.json", FileAccess.WRITE).store_string(JSON.stringify(report, "\t"))
|
|
print("COMPARE_STAGE_PASS")
|
|
await Soundscape.quit_game()
|