@tool extends Node3D const RENDER_SIZE := 1024 const WORKING_SIZE := 256 const RUNTIME_SIZE := 64 const DIRECTIONS: PackedStringArray = ["north", "east", "south", "west"] const ROTATIONS: PackedFloat64Array = [0.0, 90.0, 180.0, 270.0] @export var model_scene: PackedScene @export var output_name: String = "wall_structural" @export var outline_width_px: int = 4 # at 256 = 1px at 64 @export var outline_color: Color = Color(0.2, 0.2, 0.25, 1.0) @export var render_now: bool = false: set(value): if value and model_scene: _execute_render() render_now = false @onready var viewport: SubViewport = $SubViewport @onready var model_root: Node3D = $SubViewport/ModelRoot func _ready() -> void: if Engine.is_editor_hint(): return # CLI mode: godot --path client/ res://render_scene.tscn -- wall_structural var args := OS.get_cmdline_user_args() if args.size() == 0: return var model_name := args[0] var model_path := "res://models/%s.tscn" % model_name if not ResourceLoader.exists(model_path): push_error("Model not found: %s" % model_path) get_tree().quit(1) return model_scene = load(model_path) output_name = model_name # Wait for viewport to initialize await get_tree().process_frame await get_tree().process_frame await _execute_render() get_tree().quit() func _execute_render() -> void: if not model_scene: push_error("No model scene assigned") return print("Starting sprite render for: %s" % output_name) # Clear any existing model for child in model_root.get_children(): child.queue_free() # Instantiate the model var model_instance = model_scene.instantiate() model_root.add_child(model_instance) # Ensure output directory exists var output_dir := "res://output" if not DirAccess.dir_exists_absolute(output_dir): DirAccess.make_dir_recursive_absolute(output_dir) # Render each cardinal direction for i in range(DIRECTIONS.size()): var direction := DIRECTIONS[i] var rotation := ROTATIONS[i] print(" Rendering direction: %s (%.1f°)" % [direction, rotation]) # Rotate model root model_root.rotation_degrees.y = rotation # Force viewport to render viewport.render_target_update_mode = SubViewport.UPDATE_ONCE await RenderingServer.frame_post_draw # Capture the viewport texture var img := viewport.get_texture().get_image() # Save 1024x1024 source var path_1024 := "%s/%s_%s_1024.png" % [output_dir, output_name, direction] img.save_png(path_1024) print(" Saved: %s" % path_1024) # Downscale to 256x256 var img_256 := Image.create_from_data(img.get_width(), img.get_height(), false, img.get_format(), img.get_data()) img_256.resize(WORKING_SIZE, WORKING_SIZE, Image.INTERPOLATE_BILINEAR) # Apply outline at 256x256 var img_256_outlined := _apply_outline(img_256, outline_width_px, outline_color) # Save 256x256 with outline var path_256 := "%s/%s_%s_256.png" % [output_dir, output_name, direction] img_256_outlined.save_png(path_256) print(" Saved: %s" % path_256) # Downscale to 64x64 var img_64 := Image.create_from_data(img_256_outlined.get_width(), img_256_outlined.get_height(), false, img_256_outlined.get_format(), img_256_outlined.get_data()) img_64.resize(RUNTIME_SIZE, RUNTIME_SIZE, Image.INTERPOLATE_BILINEAR) # Save 64x64 var path_64 := "%s/%s_%s_64.png" % [output_dir, output_name, direction] img_64.save_png(path_64) print(" Saved: %s" % path_64) print("Render complete! Generated %d sprites." % (DIRECTIONS.size() * 3)) func _apply_outline(img: Image, width: int, color: Color) -> Image: """Apply outline by dilating the alpha mask and drawing outline color.""" var result := Image.create(img.get_width(), img.get_height(), false, Image.FORMAT_RGBA8) result.blit_rect(img, Rect2i(0, 0, img.get_width(), img.get_height()), Vector2i(0, 0)) var w := img.get_width() var h := img.get_height() # Create dilated alpha mask var dilated_alpha := PackedByteArray() dilated_alpha.resize(w * h) for y in range(h): for x in range(w): var max_alpha := 0.0 # Check all pixels within outline_width radius for dy in range(-width, width + 1): for dx in range(-width, width + 1): var check_x := x + dx var check_y := y + dy if check_x >= 0 and check_x < w and check_y >= 0 and check_y < h: var pixel := img.get_pixel(check_x, check_y) max_alpha = max(max_alpha, pixel.a) dilated_alpha[y * w + x] = int(max_alpha * 255) # Draw outline where dilated exceeds original alpha for y in range(h): for x in range(w): var original_pixel := img.get_pixel(x, y) var dilated_a := float(dilated_alpha[y * w + x]) / 255.0 if dilated_a > original_pixel.a: # This pixel is in the outline region var outline_pixel := Color(color.r, color.g, color.b, dilated_a) result.set_pixel(x, y, outline_pixel) return result