Files
modbison/scripts/tools/screenshot_rig.gd
T
daddy 3fb45f0ef2
checkpoint / checkpoint (push) Successful in 1m47s
Avatar: tune Roblox proportions + walk cycle from real rendered screenshots
Now that cloudbison can render (Radeon GPU via Xvfb, or llvmpipe software),
added scripts/tools/screenshot_rig.gd + scenes/dev/ so a scene can be booted,
run a few frames, and its rendered frame saved to PNG -- caught three things
the headless smoke never could:

- limbs floated detached from the torso: widened the torso and overlapped the
  arm/leg boxes into it, and pulled limb depth in behind the torso front so
  they stop z-fighting.
- proportions were scrawny: reworked to Roblox R6 ratios (2-stud torso, 2-stud
  arms/legs, 1-stud head) with the head clearly pegged on top.
- the walk swing was a 40-deg lunge: STRIDE 0.7->0.42 rad, separate 0.32 arm
  swing, KNEE 1.0->0.5. Reads as a walk now, not a crouch.

test_level lighting was near-black on the shaded side (ambient was disabled) --
colour ambient at 0.8 + a stronger key light, so the avatar reads in-world.

avatar_preview.tscn (AGP_PREVIEW_WALK=1) renders the assembled avatar walking
in place for quick visual checks.

Verified: avatar smoke 14/14 source + exported pack; fresh-clone boot PASS;
eyeballed in rendered screenshots (game scene + walk sequence).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
2026-09-06 14:06:46 +00:00

73 lines
2.6 KiB
GDScript

extends Node
## Dev-only screenshot harness. Boots a target scene inside the real (GPU)
## renderer, lets it run a few frames, then reads the rendered frame back off
## the viewport and writes it to disk. Nothing in the shipped game touches this.
##
## Driven entirely by environment variables so it needs no CLI parsing:
## AGP_SHOT_SCENE res:// path of the scene to capture (default: game.tscn)
## AGP_SHOT_OUT output PNG path (absolute) (default: /tmp/agp_shot.png)
## AGP_SHOT_FRAMES frames to settle before the first shot (default: 45)
## AGP_SHOT_SEQ "count:gap" -> take <count> shots <gap> frames apart,
## suffixing the filename _01.._NN (default: single shot)
##
## Launch (hardware Vulkan on cloudbison's Radeon, via Xvfb):
## Xvfb :99 -screen 0 1280x720x24 &
## sg render -c "sg video -c 'DISPLAY=:99 AGP_SHOT_SCENE=res://scenes/game.tscn \
## AGP_SHOT_OUT=/abs/shot.png ~/godot/godot --path . \
## --rendering-driver vulkan --gpu-index 0 --resolution 1280x720 \
## res://scenes/dev/screenshot_rig.tscn'"
func _ready() -> void:
_run.call_deferred()
func _run() -> void:
var target := OS.get_environment("AGP_SHOT_SCENE")
if target.is_empty():
target = "res://scenes/game.tscn"
var out := OS.get_environment("AGP_SHOT_OUT")
if out.is_empty():
out = "/tmp/agp_shot.png"
var settle := int(OS.get_environment("AGP_SHOT_FRAMES"))
if settle <= 0:
settle = 45
var seq_count := 1
var seq_gap := 0
var seq_env := OS.get_environment("AGP_SHOT_SEQ")
if seq_env.contains(":"):
var parts := seq_env.split(":")
seq_count = maxi(1, int(parts[0]))
seq_gap = maxi(1, int(parts[1]))
var packed: PackedScene = load(target)
if packed == null:
push_error("screenshot_rig: cannot load %s" % target)
get_tree().quit(1)
return
var inst: Node = packed.instantiate()
get_tree().root.add_child(inst)
get_tree().current_scene = inst
for i in settle:
await get_tree().process_frame
for shot in seq_count:
# get_image() on the viewport texture is a GPU->CPU readback of the
# frame that was just rendered -- no window presentation needed, which
# is why this works over Xvfb (no DRI3).
var img: Image = get_viewport().get_texture().get_image()
var path := out
if seq_count > 1:
path = "%s_%02d.png" % [out.get_basename(), shot + 1]
var err := img.save_png(path)
if err != OK:
push_error("screenshot_rig: save_png(%s) failed: %d" % [path, err])
else:
print("SHOT %s (%dx%d)" % [path, img.get_width(), img.get_height()])
if shot < seq_count - 1:
for j in seq_gap:
await get_tree().process_frame
get_tree().quit()