- Fix client sprite naming in README (files are _64.png, not .png) - Clarify --path working directory (renderer/ from repo root) - Remove /sprite-gen reference (skill not in branch yet) - Add D-043 citation on outline color in render_export.gd - Add uid to wall_bar_green.tscn for reproducible imports - Note wall_bar_green has no rendered sprites yet - Note npc_generic capsule symmetry is by design (D-044) - Add outline + D-033 tint compatibility guidance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
152 lines
4.7 KiB
GDScript
152 lines
4.7 KiB
GDScript
@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) # #333340 per D-043
|
|
@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
|