ps1 filter variables, crowds
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user