ps1 filter variables, crowds

This commit is contained in:
2026-07-30 15:12:22 +03:00
parent 3defc9da95
commit 271de1b045
31 changed files with 872 additions and 17 deletions
+186
View File
@@ -0,0 +1,186 @@
extends SceneTree
## Bakes an animated-billboard sprite sheet from the existing clapping crowd figure
## (Animations/mees_plaksutab.glb, clip "ArmatureAction") so a whole stand of
## spectators can render as camera-facing quads in one draw call instead of 72 skinned
## skeletons. Renders FRAMES evenly-spaced poses from a front orthographic camera on a
## transparent background and packs them into a single-row atlas.
##
## Needs a display — script mode renders to a real window (headless is blank):
## godot --script tools/bake_crowd_sheet.gd
##
## Output: res://Assets/crowd_clap_sheet.png — FRAMES cells wide, TWO rows: the top row
## is the front view, the bottom row is the back view. The crowd shader picks the row
## with FRONT_FACING so spectators show their backs when the camera is behind them.
const SRC := "res://Animations/mees_plaksutab.glb"
const CLIP := &"ArmatureAction"
const OUT := "res://Assets/crowd_clap_sheet.png"
const FRAMES := 8
const CELL_H := 192 # sprite cell height in px; width derived from the figure aspect
const PAD := 1.12 # framing margin around the figure's bounds
const FACE_Z := 6.0 # camera distance in front (+Z looks toward -Z)
func _init() -> void:
_run.call_deferred()
func _run() -> void:
var ps := load(SRC) as PackedScene
if ps == null:
push_error("bake_crowd_sheet: cannot load %s" % SRC)
quit(1)
return
var fig: Node3D = ps.instantiate()
var vp := SubViewport.new()
vp.own_world_3d = true
vp.transparent_bg = true
vp.msaa_3d = Viewport.MSAA_4X
vp.render_target_update_mode = SubViewport.UPDATE_ALWAYS
root.add_child(vp)
var world := World3D.new()
var env := Environment.new()
env.background_mode = Environment.BG_CLEAR_COLOR
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
env.ambient_light_color = Color(1, 1, 1)
env.ambient_light_energy = 0.7
world.environment = env
vp.world_3d = world
var key := DirectionalLight3D.new()
key.rotation_degrees = Vector3(-30, 18, 0)
key.light_energy = 1.1
vp.add_child(key)
vp.add_child(fig)
var ap := _find_anim(fig)
if ap == null:
push_error("bake_crowd_sheet: no AnimationPlayer in %s" % SRC)
quit(1)
return
ap.play(CLIP)
ap.pause()
var length := ap.get_animation(CLIP).length
await process_frame
await process_frame
# Frame the figure generously from its bind bounds (get_aabb() is the rest pose, so
# arms read wide); the tight crop comes from the rendered alpha afterwards.
var bounds := _visual_aabb(fig)
var center := bounds.get_center()
var cap_h := bounds.size.y * 1.25
var cap_w := maxf(bounds.size.x, bounds.size.y) * 1.15
var vw := int(round(CELL_H * (cap_w / cap_h)))
vp.size = Vector2i(vw, CELL_H)
var cam := Camera3D.new()
cam.projection = Camera3D.PROJECTION_ORTHOGONAL
cam.size = cap_h # KEEP_HEIGHT: vertical world extent
cam.current = true
vp.add_child(cam)
# Render the loop from the front (camera at +Z, a Camera3D looks down its own -Z) and
# from the back (camera at -Z, yawed 180° to look back at the figure).
cam.position = center + Vector3(0, 0, FACE_Z)
cam.rotation = Vector3.ZERO
var fronts: Array = await _grab_frames(vp, ap, length)
cam.position = center + Vector3(0, 0, -FACE_Z)
cam.rotation = Vector3(0, PI, 0)
var backs: Array = await _grab_frames(vp, ap, length)
# One shared tight crop across every front and back pose so the cells line up.
var all := fronts.duplicate()
all.append_array(backs)
var box := _alpha_bounds(all, 0.12)
if box.size == Vector2i.ZERO:
push_error("bake_crowd_sheet: rendered frames are fully transparent — check framing/facing")
quit(1)
return
# One pixel of transparent margin so filtering doesn't bleed a hard edge.
box = box.grow(1).intersection(Rect2i(0, 0, vw, CELL_H))
var cw := box.size.x
var ch := box.size.y
var sheet := Image.create(cw * FRAMES, ch * 2, false, Image.FORMAT_RGBA8)
sheet.fill(Color(0, 0, 0, 0))
for i in FRAMES:
sheet.blit_rect(fronts[i], box, Vector2i(i * cw, 0)) # top row = front
sheet.blit_rect(backs[i], box, Vector2i(i * cw, ch)) # bottom row = back
var err := sheet.save_png(OUT)
if err != OK:
push_error("bake_crowd_sheet: save_png failed (%d)" % err)
quit(1)
return
print("baked %s %dx%d (%d frames x 2 rows, cell %dx%d, aspect %.3f, figure %.2fm tall)" % [
OUT, sheet.get_width(), sheet.get_height(), FRAMES, cw, ch,
float(cw) / float(ch), bounds.size.y])
quit(0)
# Render every clap pose from the currently-positioned camera into an RGBA8 image array.
func _grab_frames(vp: SubViewport, ap: AnimationPlayer, length: float) -> Array:
var out: Array[Image] = []
for i in FRAMES:
ap.seek((float(i) / FRAMES) * length, true)
await process_frame
await RenderingServer.frame_post_draw
var img := vp.get_texture().get_image()
img.convert(Image.FORMAT_RGBA8)
out.append(img)
return out
# Union bounding box of pixels with alpha above `thresh` across every frame — the
# tight crop that holds the whole clap loop.
func _alpha_bounds(frames: Array[Image], thresh: float) -> Rect2i:
var w := frames[0].get_width()
var h := frames[0].get_height()
var min_x := w
var min_y := h
var max_x := -1
var max_y := -1
for img in frames:
for y in h:
for x in w:
if img.get_pixel(x, y).a > thresh:
min_x = mini(min_x, x)
min_y = mini(min_y, y)
max_x = maxi(max_x, x)
max_y = maxi(max_y, y)
if max_x < 0:
return Rect2i()
return Rect2i(min_x, min_y, max_x - min_x + 1, max_y - min_y + 1)
func _find_anim(n: Node) -> AnimationPlayer:
if n is AnimationPlayer:
return n as AnimationPlayer
for c in n.get_children():
var r := _find_anim(c)
if r != null:
return r
return null
func _visual_aabb(n: Node) -> AABB:
var acc := AABB()
var have := false
for mi in _all_mesh_instances(n):
var waabb: AABB = (mi as MeshInstance3D).global_transform * (mi as MeshInstance3D).get_aabb()
acc = waabb if not have else acc.merge(waabb)
have = true
return acc
func _all_mesh_instances(n: Node) -> Array:
var out: Array = []
if n is MeshInstance3D:
out.append(n)
for c in n.get_children():
out.append_array(_all_mesh_instances(c))
return out