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()