Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.61.0-test3 — Rings, dots and beam rain

- LED rings under the rear truss and on the back towers: a full circle that swells on every kick.
- A grid of dots on the far wall that flashes on the melody in quiet moments and breaks.
- Beam rain (Techno and Hard): the rear truss and the delay towers' beams each fall from 45° up to 45° down in
about two and a half seconds, every beam on its own moment, and go back up in the dark. The hall steps back.

## 0.61.0-test2 — A generic club booth

- The players and the mixer are generic club gear in the familiar layout, without real brand or model names.
Expand Down
2 changes: 1 addition & 1 deletion project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ config_version=5
[application]

config/name="Stage Visualizer"
config/version="0.61.0-test2"
config/version="0.61.0-test3"
run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.7", "Forward Plus")

Expand Down
50 changes: 50 additions & 0 deletions scripts/rig.gd
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ var laser_spread := 0.6
var laser_tilt := 0.0
var laser_pan := 0.0
var beam_strength := 1.0
# Set pieces: LED rings that swell on the kick, and a grid of dots that flashes on the melody. The show sets their
# levels (0..1) and one colour; they light only themselves, so they cost no lights.
var ring_levels := PackedFloat32Array()
var dot_levels := PackedFloat32Array()
var ring_at: Array[Vector3] = []
var dot_at: Array[Vector3] = []
var piece_color := Color.WHITE
var _rings: MultiMesh
var _dots: MultiMesh
const DOT_COLUMNS := 18
const DOT_ROWS := 7
var _blinder_glow := 0.0
var _by_kind := [[], [], []]
var _mm := [] # per kind: {base, yoke, head, lens, beam}
Expand Down Expand Up @@ -302,6 +313,7 @@ func build() -> void:
fixtures[i].rt = randf()
for k in 3: _mm.append(_kind_batches(k))
_batten_batches()
_set_pieces()
if LASERS: _laser_batches()
# the strobes light the hall through a few shared lamps
for p in [Vector3(0, 9, Hall.BACK + 2), Vector3(0, 3, F - 0.5), Vector3(0, 10.5, -8), Vector3(0, 10, 22)]:
Expand Down Expand Up @@ -497,6 +509,38 @@ func _yoke_mesh(r: float) -> ArrayMesh:
st.append_from(b, 0, Transform3D(Basis.IDENTITY, part[1]))
return st.commit()

func _set_pieces() -> void:
var facing_stage := Basis(Vector3.RIGHT, PI / 2.0) # a torus lies flat; stood up, it faces along Z
var xfs: Array[Transform3D] = []
# under the rear truss, between its beams, facing the stage and the DJ
for k in 8:
ring_at.append(Vector3(-15.75 + k * 4.5, 9.75, 26.3))
# on the back towers, below their beams, facing the hall
for k in 8:
ring_at.append(Vector3(-21.0 + k * 6.0, 4.4, Hall.BACK + 1.35))
for at in ring_at: xfs.append(Transform3D(facing_stage, at))
var torus := TorusMesh.new()
torus.inner_radius = 0.5
torus.outer_radius = 0.6
torus.rings = 48
torus.ring_segments = 8
var material := Kit.shader("res://shaders/batten_pixel.gdshader", {"gain": 6.0})
_rings = _multimesh(torus, ring_at.size(), material, true, false, false)
for i in xfs.size(): _rings.set_instance_transform(i, xfs[i])
# a grid of dots on the far wall, over the crowd, seen from the booth
for r in DOT_ROWS:
for c in DOT_COLUMNS:
dot_at.append(Vector3(-20.4 + c * 2.4, 4.0 + r * 1.35, Hall.D / 2.0 - 0.35))
var dot := SphereMesh.new()
dot.radius = 0.14
dot.height = 0.28
dot.radial_segments = 10
dot.rings = 5
_dots = _multimesh(dot, dot_at.size(), material, true, false, false)
for i in dot_at.size(): _dots.set_instance_transform(i, Transform3D(Basis.IDENTITY, dot_at[i]))
ring_levels.resize(ring_at.size())
dot_levels.resize(dot_at.size())

func _batten_batches() -> void:
var body := BoxMesh.new()
body.size = Vector3(1.03, 0.13, 0.16)
Expand Down Expand Up @@ -628,6 +672,12 @@ func update(dt: float, beat: float) -> void:
for pixel in Batten.PIXELS:
var pixel_color: Color = b.pixel_colors[pixel] * b.pixel_levels[pixel] * b.level
_batten_pixels.multimesh.set_instance_color(i * Batten.PIXELS + pixel, Color(pixel_color.r, pixel_color.g, pixel_color.b, 1.0))
for i in ring_at.size():
var c := piece_color * ring_levels[i]
_rings.set_instance_color(i, Color(c.r, c.g, c.b, 1.0))
for i in dot_at.size():
var c := piece_color * dot_levels[i]
_dots.set_instance_color(i, Color(c.r, c.g, c.b, 1.0))
for o in _strobe_lights:
o.visible = strobe > 0.02
o.light_energy = 16.0 * strobe
Expand Down
41 changes: 33 additions & 8 deletions scripts/show/director.gd
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,10 @@ var phrasing := ShowPhrasing.new()
var passage := ShowPassage.new()
var _passage_seen := -1
# The groove: what the parts of the track do, each with its own place in the rig (see _groove).
var groove := {"clap": 0.0, "hats": 0.0, "body": 0.0, "bass": 0.0, "claps": 0, "hat_count": 0, "bodies": 0}
var groove := {"clap": 0.0, "hats": 0.0, "body": 0.0, "bass": 0.0, "claps": 0, "hat_count": 0, "bodies": 0, "leads": 0}
var _dot_flash := 0.0
var _dot_seen := 0
var _dot_pick := 0
var _grid_step := -1
var _frozen := {"m": 0.0, "spin": 0.0, "phase": 0.0} # where the movement stood when the passage began (techno holds it)
var gestures := ShowGestures.new()
Expand Down Expand Up @@ -851,6 +854,7 @@ func _direct(dt: float) -> void:
var row: bool = h.role == "row"
var role: String = "stage" if row else h.role
var prole: String = passage.role(f.group) if passages and not row else ""
var rain: bool = passages and passage.figure == "rain" and f.group in ShowPassage.RAIN_GROUPS
var a: Vector2
if h.role == "overhead":
if brk: a = _toward(p, _break_target(h, p, k, s, sig) if multi else L.floor_target(h, p, k, _beat, sec, sig.build, sig.energy, sig.drop))
Expand All @@ -874,32 +878,33 @@ func _direct(dt: float) -> void:
h.cue = cue
var moving: bool = _t - h.move_at < 5.0 and gap > 0.2
h.move_level += ((0.2 if moving else 1.0) - h.move_level) * (e_moving if moving else e_settle)
if _t - h.move_at > 0.18:
if rain: h.aim = a # a falling beam follows its path itself; the motors still limit it
elif _t - h.move_at > 0.18:
h.aim.x += pan_gap * e25
h.aim.y += (a.y - h.aim.y) * e25
f.target = p + _dir(h.aim) * 30.0
var I: float = I_base if prole == "" else I_still + kick_term * ShowPassage.PULSE[prole] / 0.55
var pg := 1.0
if chase_gate and not row and role == "overhead" and prole in ["", "lead"]:
if chase_gate and not row and role == "overhead" and prole in ["", "lead"] and not rain:
pg = 0.1 + 0.9 * pow(maxf(0.0, cos(h.u * TAU * 1.5 - sig.phase_clock * TAU / 4.0 + h.row * 1.1)), 3.0)
I = (I * (_row_w if row else _w[role] if prole == "" else ShowPassage.LEVEL[prole]) * pg + I_drop) * master
I = (I * (_row_w if row else _w[role] if prole == "" else passage.level(f.group, prole)) * pg + I_drop) * master
# give the centre, the sides and the stage turns in the foreground; a drop opens the full rig
var bank: int = h.bank
if not brk and accent_bank == bank: I += I_accent
# the genre's share of lit heads: the rest stay dark outside a drop
if not brk and not row and h.lit_key > lit_share and not (prole != "" and role == "stage"): I *= dropk * 0.9
if not brk and not row and h.lit_key > lit_share and not (prole != "" and role == "stage") and not rain: I *= dropk * 0.9
var foreground: bool = bank == foreground_bank
var lv: float = 1.0 if foreground or prole != "" else 0.28 if calm or brk else 0.45
h.level += (lv - h.level) * e18
I *= h.level + (1.0 - h.level) * dropk
if not (prole != "" and role == "stage"): I *= phrasing.gain(bank, f.mount, dropk)
if not (prole != "" and role == "stage") and not rain: I *= phrasing.gain(bank, f.mount, dropk)
I *= 1.0 - _screen_focus * (0.6 if row or role == "stage" else 0.3) * (1.0 - dropk)
# hold each figure for 32 kicks; builds leave a small group near the stage
var build_on: bool = absf(h.xn) <= 1.0 - bld * 0.85 and (bld < 0.65 or bank == 2 or h.row % 3 == 0)
var gate_name: String = look_gate if h.role == "overhead" else row_gate if row else "patterns"
if safe and not row: gate_name = "all"
# in a passage the figure owns the stage's selection, and only the lead plays the look's gate
if prole != "" and (role == "stage" or prole != "lead"): gate_name = "all"
if prole != "" and (role == "stage" or prole != "lead" or rain): gate_name = "all"
var on: bool = build_on if brk else L.gate(gate_name, h, counted, pattern)
if prole != "" and role == "stage": on = passage.stage_on(f.group, f.row)
if row and dropk < 0.5: on = on and posmod(h.slot, 4) == posmod(floori(sig.phase_clock / 8.0) + _scene, 4)
Expand All @@ -909,7 +914,8 @@ func _direct(dt: float) -> void:
I *= h.move_level + (1.0 - h.move_level) * dropk

# the lead's rhythm: light travels between its fixtures on the beat
if still and prole == "lead": I *= passage.chase(f.group, f.row, h.u, sig.phase_clock, sec) + (1.0 - passage.chase(f.group, f.row, h.u, sig.phase_clock, sec)) * dropk
if rain: I *= passage.rain_level(f.mount)
elif still and prole == "lead": I *= passage.chase(f.group, f.row, h.u, sig.phase_clock, sec) + (1.0 - passage.chase(f.group, f.row, h.u, sig.phase_clock, sec)) * dropk
f.level = _lamp(I) * float(faders.get(f.group, 1.0))
# the look spreads the two colours, through a slow colour fade
var cname: String = look_colour if h.role == "overhead" else row_colour if row else "sides"
Expand Down Expand Up @@ -989,6 +995,24 @@ func _direct(dt: float) -> void:
bt.chase_amount = (0.55 if safe else 0.85) * (1.0 - dropk) if gestures.name == "spatial_sequence" and gestures.mix > 0.5 and not calm and not brk else 0.0
bt.strobe = flash * strobe_k * master * float(tune.groups.get("batten", 1.0))

# ----- set pieces: the rings swell on the kick, the dot grid flashes on the melody in a quiet moment -----
var piece: Color = mono_colour(_colour_block() + _palette * 137) if multi and _mono_block() else col
rig.piece_color = col if single_colour else piece
var pieces: float = float(tune.groups.get("batten", 1.0))
var swell: float = sig.build * 0.25 if brk else sig.kick * (0.3 if safe else 1.0)
for i in rig.ring_at.size():
rig.ring_levels[i] = _lamp((0.05 + swell * 0.9 + sig.drop * 0.4) * master) * pieces
var melodic: bool = brk or sig.energy < 0.45
if int(groove.leads) != _dot_seen:
_dot_seen = int(groove.leads)
if melodic:
_dot_flash = 1.0
_dot_pick += 1
_dot_flash *= exp(-dt * (4.0 if safe else 8.0))
for i in rig.dot_at.size():
var lit: bool = posmod(hash("%d:%d" % [i, _dot_pick]), 3) != 0
rig.dot_levels[i] = _lamp((0.02 + (_dot_flash * (0.4 if safe else 1.0) if lit else 0.0) + sig.drop * 0.3) * master) * pieces

# ----- the trusses on their winches -----
# Heavy trusses hold a formation for minutes. In a break they sink further as the build grows, in a measured
# wave, and level out; the drop takes them back up to the formation.
Expand Down Expand Up @@ -1032,6 +1056,7 @@ func _groove(dt: float, grid: float, brk: bool) -> void:
groove.claps = p.hits[4]
groove.hat_count = p.hits[5]
groove.bodies = p.hits[2]
groove.leads = p.hits[3]
groove.bass = p.level[1] * p.present[1]
if p.came_in[3] or p.came_in[2]: passage.open()
elif not brk and _activity > 0.3:
Expand Down
33 changes: 33 additions & 0 deletions scripts/show/passage.gd
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ const FIGURES := {
"roof_cross": ["roof", ["back", "bars"]], # the roof spots weave: neighbours lean opposite ways, reaching out into the hall
"roof_fan": ["roof", ["back"]], # a mirrored fan from the roof that widens and reaches further
"overhead": ["hall", ["back"]], # the hall's own looks lead; the stage holds a quiet figure
"rain": ["rear", ["delay"]], # beam rain: every beam falls on its own from 45° up to 45° down
}
# Beam rain: one fall takes RAIN_FALL of a cycle of RAIN_BEATS (about 2.5 s at 128 BPM); the beam goes back up dark.
const RAIN_BEATS := 8.0
const RAIN_FALL := 0.7
const RAIN_GROUPS := ["rear", "delay"]
const LEVEL := {"lead": 1.0, "support": 0.6, "hold": 0.3}
# How much of the kick a role may show: the lead breathes with it, the rest barely.
const PULSE := {"lead": 0.75, "support": 0.3, "hold": 0.08}
Expand All @@ -37,6 +42,7 @@ var stepped := false # techno: heads stand still and change position only i
var route := "across" # the passage's rhythm: how the lead's light travels from fixture to fixture on the beat
const ROUTES := ["across", "centre", "ladder", "alternate", "pingpong"]
var _open := 0.0
var _clock := 0.0 # the beat grid now, for figures that move on their own
var _grid := 0.0 # a new layer (a lead) came in: the figure opens up, then settles

func open() -> void:
Expand All @@ -45,6 +51,7 @@ func open() -> void:
# Called every frame on the beat grid. Returns true when a new passage starts.
func update(grid: float, section: String, dropped: bool, faders: Dictionary, identity: String) -> bool:
_hall_on = float(faders.get("hall", 1.0)) > 0.3
_clock = grid
if identity != _identity or grid < started - 8.0:
_identity = identity
_rng.seed = hash(identity + ":passage")
Expand Down Expand Up @@ -81,6 +88,7 @@ func _start(at: float, section: String) -> void:
length = 128.0 if quiet else [128.0, 128.0, 64.0][_rng.randi_range(0, 2)]
var pool: Array = FIGURES.keys()
if not _hall_on: pool.erase("overhead") # the hall is off on the desk: the stage carries every passage
if not stepped: pool.erase("rain") # rain is a techno figure: the other heads stand still meanwhile
var options := []
for name in pool:
for lean in [-1.0, 1.0]:
Expand All @@ -102,6 +110,11 @@ func role(group: String) -> String:
if figure == "overhead" and group in ["rear", "side"]: return "support"
return "hold"

# How strong a group plays in this passage; in the rain the hall steps back almost to dark, so the rain reads.
func level(group: String, part: String) -> float:
if figure == "rain" and group == "hall": return 0.05
return LEVEL[part]

func stage_leads() -> bool:
return figure != "overhead"

Expand Down Expand Up @@ -148,6 +161,7 @@ func stage_target(group: String, row: int, mount: Vector3, xn: float, u: float,
var b := begin()
var side := -1.0 if mount.x < 0.0 else 1.0
var drift := sin(m * 0.18 + u * 0.9) * 0.8
if figure == "rain" and group in RAIN_GROUPS: return _rain(mount)
match group:
"back":
if row == 3: return _back_spot(mount, u, d, m)
Expand Down Expand Up @@ -198,6 +212,25 @@ func stage_target(group: String, row: int, mount: Vector3, xn: float, u: float,
return Vector3(-side * 6.0, mount.y + 1.5, mount.z + (u - 0.5) * 4.0 + mirror * d * 3.0)
return Vector3.INF

# Where a falling beam aims: from 45° up to 45° down towards the stage, each beam on its own moment, in its own lane.
func _rain(mount: Vector3) -> Vector3:
var t := _rain_t(mount)
var a := deg_to_rad(lerpf(45.0, -45.0, minf(t / RAIN_FALL, 1.0)))
var d := Vector3(0.0, sin(a), -cos(a))
# aim at the point where the ray meets the roof or the floor, so the angle survives the hall's limits
var reach := (14.4 - mount.y) / d.y if d.y > 0.01 else (mount.y - 0.2) / -d.y if d.y < -0.01 else 30.0
return mount + d * clampf(reach, 1.0, 30.0)

# How lit a falling beam is: it fades in at the top, out near the bottom, and goes back up in the dark.
func rain_level(mount: Vector3) -> float:
var f := _rain_t(mount) / RAIN_FALL
if f >= 1.0: return 0.0
return smoothstep(0.0, 0.1, f) * (1.0 - smoothstep(0.75, 1.0, f))

func _rain_t(mount: Vector3) -> float:
var own := fposmod(sin(mount.x * 12.9898 + mount.y * 78.233 + mount.z * 3.7) * 43758.5453, 1.0)
return fposmod(_clock / RAIN_BEATS + own, 1.0)

# Across the hall without piling up at a wall: a soft limit keeps every head's order and some of its spacing.
static func _lane(x: float) -> float:
return 23.0 * tanh(x / 23.0)
Expand Down
Loading