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
