extends SceneTree const DEFAULT_OUTPUT_PATH := "res://docs/baselines/jajce_lookdev_01.png" const DEFAULT_SCENE_PATH := "res://world/jajce/JajceLookdev.tscn" const CAPTURE_SIZE := Vector2i(1280, 720) const WARMUP_FRAMES := 90 func _initialize() -> void: call_deferred("_run") func _run() -> void: root.size = CAPTURE_SIZE var scene_path := _get_argument_value("--scene=", DEFAULT_SCENE_PATH) var output_path := _get_argument_value("--output=", DEFAULT_OUTPUT_PATH) var scene := load(scene_path) as PackedScene if scene == null: push_error("Could not load %s" % scene_path) quit(1) return var lookdev := scene.instantiate() as Node3D root.add_child(lookdev) await process_frame if not _apply_resource_capture_overrides(): quit(1) return _stabilize_lookdev(lookdev) for _frame in WARMUP_FRAMES: await process_frame var image := root.get_texture().get_image() if image == null or image.is_empty(): push_error("Lookdev capture produced no image") quit(1) return var analysis := _analyze_image(image) if analysis.get("range", 0.0) < 0.05: push_error("Lookdev capture is too uniform: %s" % analysis) quit(1) return var error := image.save_png(output_path) if error != OK: push_error("Could not save %s: %s" % [output_path, error_string(error)]) quit(1) return print("[TOOL] Saved %s | %s" % [output_path, analysis]) quit(0) func _stabilize_lookdev(lookdev: Node3D) -> void: var camera := root.get_camera_3d() if camera != null: camera.set_process(false) var simulation_manager := lookdev.get_node_or_null("SimulationManager") if simulation_manager != null: simulation_manager.set_process(false) var day_night_cycle := lookdev.get_node_or_null("JajceWorld/DayNightCycle") if day_night_cycle == null: return day_night_cycle.set_process(false) day_night_cycle.call("_apply_light_rotation", 0.4) day_night_cycle.call("_apply_environment", 0.4) func _get_argument_value(prefix: String, default_value: String) -> String: for argument in OS.get_cmdline_user_args(): if argument.begins_with(prefix): var value := argument.trim_prefix(prefix) if not value.is_empty(): return value return default_value func _apply_resource_capture_overrides() -> bool: var hide_all_labels := OS.get_cmdline_user_args().has("--hide-debug-labels") var hide_labels := hide_all_labels or OS.get_cmdline_user_args().has("--hide-resource-labels") for resource in ResourceNode.get_all(): if hide_labels: resource.set_debug_label_enabled(false) var hide_animal_labels := ( hide_all_labels or OS.get_cmdline_user_args().has("--hide-animal-labels") ) for animal in AnimalNode.get_all(): if hide_animal_labels: animal.set_debug_label_enabled(false) if hide_all_labels: for site in AnimalRoutineSite.get_all(): site.set_debug_label_enabled(false) for storage in StorageNode.get_all(): storage.set_debug_label_enabled(false) for site in ActivitySite.get_all(): site.set_debug_label_enabled(false) for argument in OS.get_cmdline_user_args(): if argument.begins_with("--animal-hunger="): var animal_override := argument.trim_prefix("--animal-hunger=") var animal_parts := animal_override.split(":", false, 1) if animal_parts.size() != 2 or not animal_parts[1].is_valid_float(): push_error("Invalid animal hunger override: %s" % animal_override) return false var animal := AnimalNode.get_by_id(StringName(animal_parts[0])) if animal == null: push_error("Unknown animal hunger override ID: %s" % animal_parts[0]) return false var animal_state := AnimalStateRecord.create_from_node(animal) animal_state.set_hunger(animal_parts[1].to_float()) if not animal.bind_state(animal_state): push_error("Could not bind capture state for animal: %s" % animal_parts[0]) return false continue if not argument.begins_with("--resource-amount="): continue var override_text := argument.trim_prefix("--resource-amount=") var parts := override_text.split(":", false, 1) if parts.size() != 2 or not parts[1].is_valid_float(): push_error("Invalid resource amount override: %s" % override_text) return false var resource := ResourceNode.get_by_id(StringName(parts[0])) if resource == null: push_error("Unknown resource amount override ID: %s" % parts[0]) return false var state := ResourceStateRecord.create_from_node(resource) state.set_amount_remaining(parts[1].to_float()) if not resource.bind_state(state): push_error("Could not bind capture state for resource: %s" % parts[0]) return false return true func _analyze_image(image: Image) -> Dictionary: var min_luma := INF var max_luma := -INF var total_luma := 0.0 var samples := 0 var step_x = maxi(1, image.get_width() / 64) var step_y = maxi(1, image.get_height() / 36) for y in range(0, image.get_height(), step_y): for x in range(0, image.get_width(), step_x): var color := image.get_pixel(x, y) var luma := color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722 min_luma = minf(min_luma, luma) max_luma = maxf(max_luma, luma) total_luma += luma samples += 1 return { "size": "%sx%s" % [image.get_width(), image.get_height()], "samples": samples, "average_luma": total_luma / samples, "range": max_luma - min_luma, }