refactor(assets): move sprite renderer to standalone project at renderer/

Separates the 3D-to-2D render pipeline from the game client into its
own minimal Godot project. No autoloads, no plugins, no game code —
just the render scene, models, textures, and export script. Eliminates
SimBridge parse errors and gdUnit4 scanning during renders.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 23:26:16 +01:00
co-authored by Claude Opus 4.6
parent 8a1b10786f
commit 921d2aef0d
11 changed files with 38 additions and 20 deletions
+15
View File
@@ -0,0 +1,15 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Texture2D" path="res://textures/wall_bar_green_panels.png" id="1_wall_texture"]
[sub_resource type="BoxMesh" id="BoxMesh_wall"]
size = Vector3(1.0, 0.8, 0.2)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wall"]
albedo_texture = ExtResource("1_wall_texture")
metallic = 0.0
roughness = 0.9
[node name="WallBarGreen" type="MeshInstance3D"]
mesh = SubResource("BoxMesh_wall")
surface_material_override/0 = SubResource("StandardMaterial3D_wall")
+15
View File
@@ -0,0 +1,15 @@
[gd_scene load_steps=3 format=3 uid="uid://cyh0xf1nv2j3e"]
[ext_resource type="Texture2D" path="res://textures/wall_institutional_era1.png" id="1_wall_texture"]
[sub_resource type="BoxMesh" id="BoxMesh_wall"]
size = Vector3(1.0, 0.8, 0.2)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wall"]
albedo_texture = ExtResource("1_wall_texture")
metallic = 0.0
roughness = 0.9
[node name="WallStructural" type="MeshInstance3D"]
mesh = SubResource("BoxMesh_wall")
surface_material_override/0 = SubResource("StandardMaterial3D_wall")
View File
+14
View File
@@ -0,0 +1,14 @@
; Sprite Renderer — standalone Godot project for 3D-to-2D sprite pipeline.
; No game logic, no autoloads, no plugins. Just rendering.
config_version=5
[application]
config/name="Settled Reach Sprite Renderer"
config/features=PackedStringArray("4.6", "GL Compatibility")
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
+151
View File
@@ -0,0 +1,151 @@
@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
+27
View File
@@ -0,0 +1,27 @@
[gd_scene load_steps=2 format=3 uid="uid://cqvfv4uhbgq6h"]
[ext_resource type="Script" path="res://render_export.gd" id="1_render_script"]
[node name="RenderScene" type="Node3D"]
script = ExtResource("1_render_script")
[node name="SubViewport" type="SubViewport" parent="."]
transparent_bg = true
size = Vector2i(1024, 1024)
render_target_update_mode = 1
own_world_3d = true
[node name="Camera3D" type="Camera3D" parent="SubViewport"]
transform = Transform3D(1, 0, 0, 0, 0.309017, 0.951057, 0, -0.951057, 0.309017, 0, 3, 1)
projection = 1
size = 1.4
near = 0.01
far = 10.0
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="SubViewport"]
transform = Transform3D(1, 0, 0, 0, 0.309017, 0.951057, 0, -0.951057, 0.309017, 0, 3, 1)
light_color = Color(1, 1, 1, 1)
light_energy = 1.0
shadow_enabled = false
[node name="ModelRoot" type="Node3D" parent="SubViewport"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 KiB