crowd markers, unsheathed sword correct
This commit is contained in:
+37
-55
@@ -1,19 +1,16 @@
|
||||
extends MultiMeshInstance3D
|
||||
## A whole stand of cheering spectators rendered as camera-facing billboards in a
|
||||
## single draw call — the performant replacement for dozens of skinned crowd skeletons.
|
||||
## The sprite sheet is baked from the existing crowd figure by tools/bake_crowd_sheet.gd.
|
||||
## A whole stand of cheering spectators rendered as flat billboards in a single draw
|
||||
## call — the performant replacement for dozens of skinned crowd skeletons. The sprite
|
||||
## sheet is baked from the existing crowd figure by tools/bake_crowd_sheet.gd.
|
||||
##
|
||||
## Placement mirrors an existing crowd node (default: the sibling PublikNode) so the
|
||||
## billboards land exactly where the crowd was authored; if that node is missing it
|
||||
## falls back to a generated tribune so the scene still populates.
|
||||
## Placement comes from CrowdMarker nodes the designer drops into the level (collected
|
||||
## via the &"crowd_marker" group): each marker gives one spectator its world position and
|
||||
## a chosen facing (one of six 60°-spaced headings). If no markers are present the scene
|
||||
## falls back to a generated tribune so it still populates.
|
||||
|
||||
const SHEET_PATH := "res://Assets/crowd_clap_sheet.png"
|
||||
const SHADER_PATH := "res://crowd_billboard.gdshader"
|
||||
|
||||
## Node whose descendant figures (names starting with `figure_prefix`) donate their
|
||||
## world positions. Empty = use the generated fallback stand.
|
||||
@export var source_path: NodePath = ^"../PublikNode"
|
||||
@export var figure_prefix: String = "mees"
|
||||
## World height of a spectator billboard, in metres. Sheet aspect sets the width.
|
||||
@export var figure_height: float = 2.0
|
||||
@export var frames: int = 8
|
||||
@@ -36,7 +33,8 @@ var _rng := RandomNumberGenerator.new()
|
||||
|
||||
func _ready() -> void:
|
||||
_rng.seed = hash("bullosseum-crowd") # deterministic layout/phases across runs
|
||||
_build()
|
||||
# Deferred so every CrowdMarker has run _ready and joined the group before we harvest.
|
||||
_build.call_deferred()
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
@@ -58,71 +56,53 @@ func _build() -> void:
|
||||
mat.set_shader_parameter("fps", clap_fps)
|
||||
material_override = mat
|
||||
|
||||
var seats := _seat_positions()
|
||||
var center := _arena_center(seats)
|
||||
# Each seat is a (global feet-position, unit forward) pair.
|
||||
var positions := PackedVector3Array()
|
||||
var facings: Array[Vector3] = []
|
||||
_collect_seats(positions, facings)
|
||||
|
||||
var mm := MultiMesh.new()
|
||||
mm.transform_format = MultiMesh.TRANSFORM_3D
|
||||
mm.use_custom_data = true
|
||||
mm.mesh = quad
|
||||
mm.instance_count = seats.size()
|
||||
mm.instance_count = positions.size()
|
||||
|
||||
for i in seats.size():
|
||||
for i in positions.size():
|
||||
var s := 1.0 + _rng.randf_range(-0.06, 0.06) # slight height variety
|
||||
mm.set_instance_transform(i, Transform3D(_facing_basis(seats[i], center, s), seats[i]))
|
||||
mm.set_instance_transform(i, Transform3D(_upright_basis(facings[i], s), positions[i]))
|
||||
var b := 1.0 + _rng.randf_range(-tint_variation, tint_variation)
|
||||
mm.set_instance_custom_data(i, Color(_rng.randf(), b, b * 0.99, b * 0.98))
|
||||
multimesh = mm
|
||||
|
||||
|
||||
# Orientation for a seat: the sprite's forward (+Z, the quad's face) points horizontally
|
||||
# inward at the arena centre, so each ring segment faces the action instead of the
|
||||
# camera. Upright (world +Y), uniformly scaled by `s`.
|
||||
func _facing_basis(seat: Vector3, center: Vector3, s: float) -> Basis:
|
||||
var look := center - seat
|
||||
# Upright basis whose forward (+Z, the quad's face) points along `look`, uniformly
|
||||
# scaled by `s`. World +Y stays up so spectators never tilt.
|
||||
func _upright_basis(look: Vector3, s: float) -> Basis:
|
||||
look.y = 0.0
|
||||
look = look.normalized() if look.length() > 0.001 else Vector3(0.0, 0.0, -1.0)
|
||||
look = look.normalized() if look.length() > 0.001 else Vector3(0.0, 0.0, 1.0)
|
||||
var up := Vector3.UP
|
||||
var right := up.cross(look).normalized()
|
||||
var basis := Basis()
|
||||
basis.x = right
|
||||
basis.x = up.cross(look).normalized()
|
||||
basis.y = up
|
||||
basis.z = look
|
||||
return basis.scaled(Vector3.ONE * s)
|
||||
|
||||
|
||||
# Horizontal centre the crowd faces — the mean of the seat positions (the ring/stand
|
||||
# centroid), which for an arena crowd is the middle of the pitch.
|
||||
func _arena_center(seats: PackedVector3Array) -> Vector3:
|
||||
if seats.is_empty():
|
||||
return global_position
|
||||
var sum := Vector3.ZERO
|
||||
for p in seats:
|
||||
sum += p
|
||||
return sum / float(seats.size())
|
||||
# Seats from designer-placed CrowdMarker nodes: each contributes its world position and
|
||||
# its chosen 60°-spaced facing. With no markers present, fall back to a generated ring
|
||||
# that faces inward so the scene still shows a crowd.
|
||||
func _collect_seats(positions: PackedVector3Array, facings: Array[Vector3]) -> void:
|
||||
for node in get_tree().get_nodes_in_group(&"crowd_marker"):
|
||||
var marker := node as Node3D
|
||||
if marker == null or not marker.has_method(&"facing_dir"):
|
||||
continue
|
||||
positions.append(marker.global_position)
|
||||
facings.append(marker.facing_dir())
|
||||
if positions.is_empty():
|
||||
_fallback_stand(positions, facings)
|
||||
|
||||
|
||||
# Global feet-positions for every spectator: harvested from the source crowd if present,
|
||||
# otherwise the generated fallback tribune.
|
||||
func _seat_positions() -> PackedVector3Array:
|
||||
var out := PackedVector3Array()
|
||||
var src := get_node_or_null(source_path)
|
||||
if src != null:
|
||||
_collect_figures(src, out)
|
||||
if out.is_empty():
|
||||
_fallback_stand(out)
|
||||
return out
|
||||
|
||||
|
||||
func _collect_figures(node: Node, out: PackedVector3Array) -> void:
|
||||
if node is Node3D and node != self and (node as Node3D).name.begins_with(figure_prefix):
|
||||
out.append((node as Node3D).global_position)
|
||||
return # don't descend into a figure's own rig
|
||||
for c in node.get_children():
|
||||
_collect_figures(c, out)
|
||||
|
||||
|
||||
func _fallback_stand(out: PackedVector3Array) -> void:
|
||||
func _fallback_stand(positions: PackedVector3Array, facings: Array[Vector3]) -> void:
|
||||
var placed := 0
|
||||
for row in fallback_rows:
|
||||
var radius := fallback_inner_radius + float(row) * fallback_row_step
|
||||
@@ -133,5 +113,7 @@ func _fallback_stand(out: PackedVector3Array) -> void:
|
||||
if placed >= fallback_count:
|
||||
return
|
||||
var ang := TAU * float(s) / float(seats_in_row)
|
||||
out.append(Vector3(cos(ang) * radius, y, sin(ang) * radius))
|
||||
var pos := Vector3(cos(ang) * radius, y, sin(ang) * radius)
|
||||
positions.append(pos)
|
||||
facings.append(-pos) # face the ring centre (origin)
|
||||
placed += 1
|
||||
|
||||
Reference in New Issue
Block a user