From fff26e4fcff0e22785271137830cf2b174a769a4 Mon Sep 17 00:00:00 2001 From: SlipstreamSurfer <277415410+SlipstreamSurfer@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:18:13 +0100 Subject: [PATCH 1/3] Move every head on the beat and add more beat patterns Heads no longer turn on a free clock: each passage picks a beat-locked step motion (scissors, nod, fan, ladder, wave), two beats a step, one late in a peak, a bar in breaks growing with the build, with a small nod on claps. The lead's patterns gain fill, pump, sparkle, bounce and split, in every genre, and supporting groups pump with the kick. Beam rain is removed. Version 0.61.0-test4. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 12 +++++++ project.godot | 2 +- scripts/show/director.gd | 42 +++++++++++++--------- scripts/show/passage.gd | 77 +++++++++++++++++++++++----------------- 4 files changed, 83 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f7bb09..2c7edbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.61.0-test4 — Every move on the beat + +- Heads no longer turn, drift or circle on a clock of their own. Every head moves in steps on the beat: a step + lands on the kick and holds until the next one. Each passage picks a motion: scissors (odd and even cross), + a nod of the whole rig, a fan that opens and closes, a ladder that travels along and up, or a wave. +- Two beats a step, every beat in the second half of a peak; in a break a step a bar, growing with the build. + A clap nods the rig a little. Gobos turn with the tempo. +- Beam rain is removed. +- More rhythm in the light, in every genre: besides the chases the lead can fill up beat by beat, pump with the + kick, sparkle on the sixteenths, bounce between the edges and the middle, or split left and right. The supporting + groups pump with the kick under it (not with gentle strobes). + ## 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. diff --git a/project.godot b/project.godot index 39d4b69..fe3617f 100644 --- a/project.godot +++ b/project.godot @@ -5,7 +5,7 @@ config_version=5 [application] config/name="Stage Visualizer" -config/version="0.61.0-test3" +config/version="0.61.0-test4" run/main_scene="res://main.tscn" config/features=PackedStringArray("4.7", "Forward Plus") diff --git a/scripts/show/director.gd b/scripts/show/director.gd index 299a6ed..a97d6e4 100644 --- a/scripts/show/director.gd +++ b/scripts/show/director.gd @@ -730,9 +730,10 @@ func _direct(dt: float) -> void: _passage_seen = passage.index _frozen = {"m": _motion, "spin": _spin, "phase": sig.phase_clock} var passages: bool = not mood.flow and not brk - # Techno: the heads stand still and the rhythm comes from light moving between them, as in a depth chase. - var still: bool = passages and mood.family in ["Techno", "Hard"] - passage.stepped = still + # A figure changes its place only in steps, in the dark; on top of it every head moves in steps on the beat, and + # the rhythm travels through the light: the lead plays a pattern, the support pumps with the kick. + var still: bool = passages # every genre: the lead plays a pattern on the beat, the support pumps with the kick + passage.stepped = passages _groove(dt, sig.phase_clock, brk) # the DJ asked for the next look: a new scene now, with the next look and formation @@ -792,7 +793,7 @@ func _direct(dt: float) -> void: rig.blinder = blind * master # ----- the moving heads ----- - var k := {"m": _frozen.m if still else _motion, "spin": _frozen.spin if still else _spin, "phase": _frozen.phase if still else sig.phase_clock, "build": sig.build, "hangar_look": _scene % 3} + var k := {"m": _frozen.m, "spin": _frozen.spin, "phase": _frozen.phase, "build": sig.build, "hangar_look": _scene % 3} _hangar_looks(tier, brk) var look: Array = L.HEAD_LOOKS[_head_look] var stage_z := Rig.TRUSS_Z[0] - 8.0 # over the booth @@ -820,10 +821,15 @@ func _direct(dt: float) -> void: var e_moving: float = ease.call(0.1) var e_settle: float = ease.call(0.7) var e_gate: float = ease.call(0.65 if safe else 0.22) + # Every head moves in steps on the beat: two beats a step, every beat late in a peak; in a break the steps slow + # to a bar and grow with the build. A clap nods the rig a little. + var step_beats: float = 4.0 if brk or mood.flow else 1.0 if sec == "peak" and passage.progress >= 0.5 else 2.0 + var clap_nod: float = 0.0 if safe or brk else float(groove.clap) * 0.05 + var step_size: float = (0.25 + bld * 0.75 if brk else 0.35 if mood.flow else 1.0) * (0.5 if safe else 1.0) var cue := "%d:%d:%d" % [_scene, floori((_beat - 1) / 16.0), floori(passage.progress * 4.0) if passage.stepped else 0] # a step of a still figure moves in the dark # what is the same for every head this frame, worked out once var stack := _stack(look, tier) - var look_move: String = "still" if still else stack[0] + var look_move: String = "still" var look_gate: String = stack[1] var look_colour: String = stack[2] var look_beam: Array = L.BEAMS[stack[3]] @@ -854,7 +860,6 @@ 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)) @@ -867,6 +872,8 @@ func _direct(dt: float) -> void: else: var pt := passage.stage_target(f.group, f.row, f.mount, h.xn, h.u, k.m) if passages else Vector3.INF a = _program_aim("break" if brk else P.name, h.hanging, h, k) if pt == Vector3.INF else _toward(p, _inside(pt)) + # a break holds the figure it came in with; the build is in the light, not in the movement + if brk and h.aimed: a = h.aim # crossfade the asked aim as well as limiting the motors; a head dims while it travels far if not h.aimed: h.aim = _toward(p, p + f.dir) @@ -878,33 +885,32 @@ 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 rain: h.aim = a # a falling beam follows its path itself; the motors still limit it - elif _t - h.move_at > 0.18: + if _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 + f.target = p + _dir(h.aim + passage.step(h.slot, h.row, h.u, h.xn, sig.phase_clock, step_beats, step_size) + Vector2(0.0, clap_nod)) * 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"] and not rain: + if chase_gate and not row and role == "overhead" and prole in ["", "lead"]: 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 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") and not rain: I *= dropk * 0.9 + if not brk and not row and h.lit_key > lit_share and not (prole != "" and role == "stage"): 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") and not rain: I *= phrasing.gain(bank, f.mount, dropk) + if not (prole != "" and role == "stage"): 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" or rain): gate_name = "all" + if prole != "" and (role == "stage" or prole != "lead"): 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) @@ -914,8 +920,12 @@ 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 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 + if still and prole == "lead": + var lit := passage.chase(f.group, f.row, h.u, sig.phase_clock, sec) + I *= lit + (1.0 - lit) * dropk + elif still and prole == "support" and not safe: + var pumped := ShowPassage.pump(sig.phase_clock) + I *= pumped + (1.0 - pumped) * 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" @@ -942,7 +952,7 @@ func _direct(dt: float) -> void: if f.kind == Rig.SPOT: gobo = 1.0 f.zoom = z f.gobo = gobo - f.spin = 0.06 if gobo > 0.0 else 0.0 + f.spin = 0.06 if gobo > 0.0 else 0.0 # turns per beat: the gobos turn with the tempo # ----- washes and bars ----- # Washes are the colour under the beams: a steady level for the part of the track that changes over a couple of diff --git a/scripts/show/passage.gd b/scripts/show/passage.gd index 13f1241..39be499 100644 --- a/scripts/show/passage.gd +++ b/scripts/show/passage.gd @@ -15,12 +15,7 @@ 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} @@ -40,9 +35,12 @@ var _hall_on := true var breath := 0.0 # the bass: the figure widens with it var stepped := false # techno: heads stand still and change position only in steps (four per passage), in the dark 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"] +const ROUTES := ["across", "centre", "ladder", "alternate", "pingpong", "fill", "pump", "sparkle", "bounce", "split"] +# How the heads move in a passage: in steps on the beat, never on a clock of their own. Each step lands on a kick +# (the move takes the first fifth of the step) and holds until the next one. +const MOTIONS := ["scissors", "nod", "fan", "ladder", "wave"] +var motion := "scissors" 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: @@ -51,7 +49,6 @@ 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") @@ -88,7 +85,6 @@ 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]: @@ -99,6 +95,7 @@ func _start(at: float, section: String) -> void: figure = pick.get_slice(":", 0) mirror = float(pick.get_slice(":", 1)) route = ROUTES[_rng.randi_range(0, ROUTES.size() - 1)] + motion = MOTIONS[_rng.randi_range(0, MOTIONS.size() - 1)] _recent.push_front(pick) if _recent.size() > 4: _recent.resize(4) @@ -110,9 +107,8 @@ 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 +# How strong a group plays in this passage. +func level(_group: String, part: String) -> float: return LEVEL[part] func stage_leads() -> bool: @@ -146,12 +142,47 @@ func chase(group: String, row: int, u: float, grid: float, section: String) -> f "pingpong": var at := posmod(step, 14) on = station == (at if at < 8 else 14 - at) + "fill": # one by one on with every beat until all are lit, then from the start again + on = station <= posmod(step, 8) + "pump": # dark on the kick, swelling back between: the sidechain of the track, in light + return pump(grid * rate) + "sparkle": # a scatter on the sixteenths, a new one every time + var tick := floori(grid * 4.0) + on = posmod(hash("%d:%d:%d" % [tick, station, row]), 4) == 0 + if on: return 1.0 - smoothstep(0.0, 0.25, fposmod(grid * 4.0, 1.0)) * 0.8 + "bounce": # from both edges to the middle and back + var at := posmod(step, 6) + on = mini(station, 7 - station) == (at if at < 4 else 6 - at) + "split": # left and right trade places on every beat + on = (u < 0.5) == (posmod(step, 2) == 0) if not on: return 0.04 return 1.0 - smoothstep(0.6, 0.98, fposmod(grid * rate, 1.0)) * 0.7 +# The supporting groups' rhythm: dark on the kick, back up before the next one. +static func pump(grid: float) -> float: + return 0.25 + 0.75 * smoothstep(0.05, 0.55, fposmod(grid, 1.0)) + func describe() -> String: var def: Array = FIGURES[figure] - return "%s (%s leads, %s, %d%%)" % [figure.replace("_", " "), def[0], route, roundi(progress * 100.0)] + return "%s (%s leads, %s, %s, %d%%)" % [figure.replace("_", " "), def[0], route, motion, roundi(progress * 100.0)] + +# A head's move on the beat, as [pan, tilt] in radians on top of its figure. index: the head's place in its group. +# beats: how many beats a step lasts. size: how far the steps reach (0 holds still). +func step(index: int, row: int, u: float, xn: float, grid: float, beats: float, size: float) -> Vector2: + if size <= 0.0: return Vector2.ZERO + var n := floori(grid / beats) + var land := smoothstep(0.0, 0.2, fposmod(grid / beats, 1.0)) # the move lands in the first fifth of the step + return _step_at(n - 1, index, row, u, xn).lerp(_step_at(n, index, row, u, xn), land) * size + +func _step_at(n: int, index: int, row: int, u: float, xn: float) -> Vector2: + var side := 1.0 if posmod(n, 2) == 0 else -1.0 + match motion: + "scissors": return Vector2(0.22 * side * (1.0 if index % 2 == 0 else -1.0) * mirror, 0.0) # odd and even cross on the beat + "nod": return Vector2(0.0, 0.16 * side) # the rig nods together + "fan": return Vector2(0.3 * xn * side, 0.0) # the fan opens and closes + "ladder": return Vector2(0.0, (float(posmod(n + row + floori(u * 4.0), 4)) - 1.5) * 0.1) # a step travels along and up + "wave": return Vector2(sin((float(n) + u * 4.0) * PI * 0.5) * 0.24 * mirror, 0.0) # a wave steps across + return Vector2.ZERO # Where a stage head aims in this passage, as a point in the hall. m is the show's slow motion clock. # Returns null-like Vector3.INF when the figure leaves the head to the director's own program. @@ -161,7 +192,6 @@ 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) @@ -212,25 +242,6 @@ 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) From d0dbe29e35aa021c84a2a1abb3bcbffa72458a8b Mon Sep 17 00:00:00 2001 From: SlipstreamSurfer <277415410+SlipstreamSurfer@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:28:37 +0100 Subject: [PATCH 2/3] Give the main stage its own set and remove the far-wall pieces LED frames round the centre screen, vertical tubes in a V between the towers and a ring grid on the back wall, each playing a beat-locked figure per passage. The far-wall dot grid and the rear-truss rings are removed. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 ++- scripts/rig.gd | 98 ++++++++++++++++++++++------------- scripts/show/director.gd | 27 ++++++++-- scripts/show/stage_set.gd | 46 ++++++++++++++++ scripts/show/stage_set.gd.uid | 1 + 5 files changed, 137 insertions(+), 41 deletions(-) create mode 100644 scripts/show/stage_set.gd create mode 100644 scripts/show/stage_set.gd.uid diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c7edbc..078844b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,11 @@ a nod of the whole rig, a fan that opens and closes, a ladder that travels along and up, or a wave. - Two beats a step, every beat in the second half of a peak; in a break a step a bar, growing with the build. A clap nods the rig a little. Gobos turn with the tempo. -- Beam rain is removed. +- Beam rain is removed, and so are the dot grid on the far wall and the rings under the rear truss. +- A stage set in the main stage: two LED frames round the centre screen, six vertical LED tubes between the towers + in a V (tall at the outside) and a grid of LED rings on the back wall. The rings swell on the kick; per passage + the tubes rise, drop, pulse or alternate and the frames orbit, take turns or hit on the kick; in quiet moments + the tubes flash on the melody, in a break they fill with the build, and a drop lights the whole set. - More rhythm in the light, in every genre: besides the chases the lead can fill up beat by beat, pump with the kick, sparkle on the sixteenths, bounce between the edges and the middle, or split left and right. The supporting groups pump with the kick under it (not with gentle strobes). diff --git a/scripts/rig.gd b/scripts/rig.gd index cde49ad..82d906d 100644 --- a/scripts/rig.gd +++ b/scripts/rig.gd @@ -97,17 +97,20 @@ 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. +# The stage set: a grid of LED rings on the back wall, vertical LED tubes between the towers (a V, tall at the +# outside), and two LED frames round the centre screen. The show sets every level (0..1) and the colours; the set +# lights only itself, so it costs no lights. Tube and frame pixels share one batch. var ring_levels := PackedFloat32Array() -var dot_levels := PackedFloat32Array() var ring_at: Array[Vector3] = [] -var dot_at: Array[Vector3] = [] +var set_pixels: Array[Vector3] = [] +var set_part := PackedInt32Array() # per pixel: 0..5 a tube (left to right), 10 the inner frame, 11 the outer frame +var set_place := PackedFloat32Array() # per pixel: up a tube 0..1, or round a frame 0..1 (from the bottom middle) +var set_levels := PackedFloat32Array() var piece_color := Color.WHITE +var frame_color := Color.WHITE var _rings: MultiMesh -var _dots: MultiMesh -const DOT_COLUMNS := 18 -const DOT_ROWS := 7 +var _set: MultiMesh +const TUBES := [[-18.0, 13.4], [-12.0, 11.8], [-6.0, 10.2], [6.0, 10.2], [12.0, 11.8], [18.0, 13.4]] # x, top var _blinder_glow := 0.0 var _by_kind := [[], [], []] var _mm := [] # per kind: {base, yoke, head, lens, beam} @@ -510,36 +513,61 @@ func _yoke_mesh(r: float) -> ArrayMesh: 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 wall := Hall.BACK + 0.45 + var material := Kit.shader("res://shaders/batten_pixel.gdshader", {"gain": 6.0}) + # rings on the back wall in the gaps between the towers, low and high + for x in [-18.0, -12.0, -6.0, 6.0, 12.0, 18.0]: + for y in [3.6, 13.9]: ring_at.append(Vector3(x, y, wall)) var torus := TorusMesh.new() - torus.inner_radius = 0.5 - torus.outer_radius = 0.6 + torus.inner_radius = 0.55 + torus.outer_radius = 0.66 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])) + for i in ring_at.size(): _rings.set_instance_transform(i, Transform3D(Basis(Vector3.RIGHT, PI / 2.0), ring_at[i])) ring_levels.resize(ring_at.size()) - dot_levels.resize(dot_at.size()) + # tubes: one pixel every 40 cm from the deck up + for t in TUBES.size(): + var bottom := Hall.DECK + 3.0 + 0.3 + var top: float = TUBES[t][1] + var n := int((top - bottom) / 0.4) + 1 + for i in n: + set_pixels.append(Vector3(TUBES[t][0], bottom + i * 0.4, wall + 0.05)) + set_part.append(t) + set_place.append(float(i) / float(n - 1)) + # frames round the screen (5 × 7 m, centre 7.9 m up): one close round it, one taller and narrower outside it + var centre := Vector3(0.0, Hall.DECK + 6.3, -36.05) + for f in 2: + var size := Vector2(5.7, 7.8) if f == 0 else Vector2(6.9, 10.6) + var perimeter := 2.0 * (size.x + size.y) + var n := int(perimeter / 0.35) + for i in n: + var d := float(i) / float(n) * perimeter + # from the bottom middle round to the right, up, across the top and down the left + var at := Vector2.ZERO + var hx := size.x / 2.0 + var hy := size.y / 2.0 + if d < hx: at = Vector2(d, -hy) + elif d < hx + size.y: at = Vector2(hx, -hy + (d - hx)) + elif d < hx + size.y + size.x: at = Vector2(hx - (d - hx - size.y), hy) + elif d < hx + 2.0 * size.y + size.x: at = Vector2(-hx, hy - (d - hx - size.y - size.x)) + else: at = Vector2(-hx + (d - hx - 2.0 * size.y - size.x), -hy) + set_pixels.append(centre + Vector3(at.x, at.y, 0.02 * f)) + set_part.append(10 + f) + set_place.append(float(i) / float(n)) + var pixel := BoxMesh.new() + pixel.size = Vector3(0.16, 0.3, 0.08) + _set = _multimesh(pixel, set_pixels.size(), material, true, false, false) + for i in set_pixels.size(): + var frame_side := set_part[i] >= 10 + var basis := Basis.IDENTITY + # a frame's pixels lie along their edge + if frame_side: + var local := set_pixels[i] - centre + var size := Vector2(5.7, 7.8) if set_part[i] == 10 else Vector2(6.9, 10.6) + if absf(absf(local.y) - size.y / 2.0) < 0.01: basis = Basis(Vector3.BACK, PI / 2.0) + _set.set_instance_transform(i, Transform3D(basis, set_pixels[i])) + set_levels.resize(set_pixels.size()) func _batten_batches() -> void: var body := BoxMesh.new() @@ -675,9 +703,9 @@ func update(dt: float, beat: float) -> void: 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 i in set_pixels.size(): + var c := (frame_color if set_part[i] >= 10 else piece_color) * set_levels[i] + _set.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 diff --git a/scripts/show/director.gd b/scripts/show/director.gd index a97d6e4..2dbe58c 100644 --- a/scripts/show/director.gd +++ b/scripts/show/director.gd @@ -1005,11 +1005,14 @@ 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 ----- + # ----- the stage set: rings swell on the kick; tubes and frames play a figure per passage on the beat; in a quiet + # moment the tubes flash on the melody instead; a break fills the tubes with the build; a drop lights it all ----- 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 + rig.frame_color = rig.piece_color.lerp(Color.WHITE, 0.35) 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) + var kick_k: float = sig.kick * (0.3 if safe else 1.0) + var swell: float = sig.build * 0.25 if brk else kick_k 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 @@ -1019,9 +1022,23 @@ func _direct(dt: float) -> void: _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 + var block := passage.index if passages else floori(sig.phase_clock / 64.0) + var tubes := ShowSet.tube_mode(block) + var frames := ShowSet.frame_mode(block) + var grid: float = sig.phase_clock + for i in rig.set_pixels.size(): + var part: int = rig.set_part[i] + var place: float = rig.set_place[i] + var v: float + if part >= 10: + v = ShowSet.frame(frames, part - 10, place, grid, kick_k) if not brk else 0.06 + 0.25 * sig.build + elif brk: + v = 1.0 if place <= sig.build else ShowSet.DIM + else: + v = ShowSet.tube(tubes, part, place, grid, kick_k) + if melodic and part < 10 and posmod(hash("%d:%d" % [part, _dot_pick]), 3) != 0: v = maxf(v, _dot_flash) + if safe: v = minf(v, 0.4) + rig.set_levels[i] = _lamp((v * 0.8 + sig.drop * 0.4) * 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 diff --git a/scripts/show/stage_set.gd b/scripts/show/stage_set.gd new file mode 100644 index 0000000..95d3017 --- /dev/null +++ b/scripts/show/stage_set.gd @@ -0,0 +1,46 @@ +class_name ShowSet +extends RefCounted +# How the stage set plays: the tubes and the frames each pick a figure per passage, locked to the beat grid. +# Levels are 0..1 before the master and the desk. + +const TUBE_MODES := ["rise", "drops", "pulse", "alternate"] +const FRAME_MODES := ["orbit", "nest", "beat"] +const DIM := 0.04 + +static func tube_mode(passage: int) -> String: + return TUBE_MODES[posmod(hash("%d:tubes" % passage), TUBE_MODES.size())] + +static func frame_mode(passage: int) -> String: + return FRAME_MODES[posmod(hash("%d:frames" % passage), FRAME_MODES.size())] + +# A tube pixel: tube 0..5 left to right, place 0..1 from the bottom up. +static func tube(mode: String, tube_index: int, place: float, grid: float, kick: float) -> float: + match mode: + "rise": # fills from the bottom up once a bar, then starts again + return 1.0 if place <= fposmod(grid / 4.0, 1.0) else DIM + "drops": # a drop falls down each tube in two beats, every tube on its own moment + var own := fposmod(sin(tube_index * 12.9898) * 43758.5453, 1.0) + var head := 1.0 - fposmod(grid / 2.0 + own, 1.0) + var trail := place - head + return 1.0 - trail / 0.2 if trail >= 0.0 and trail < 0.2 else DIM + "pulse": # every tube on the kick + return DIM + (1.0 - DIM) * kick + "alternate": # odd and even tubes trade on the beat + var on := posmod(tube_index, 2) == posmod(floori(grid), 2) + return 1.0 - smoothstep(0.5, 1.0, fposmod(grid, 1.0)) * 0.7 if on else DIM + return DIM + +# A frame pixel: frame 0 the inner one, 1 the outer one; place 0..1 round it. +static func frame(mode: String, frame_index: int, place: float, grid: float, kick: float) -> float: + match mode: + "orbit": # a light runs round each frame once a bar, the two the opposite way + var at := fposmod(grid / 4.0, 1.0) + if frame_index == 1: at = 1.0 - at + var d := fposmod((place - at) * (1.0 if frame_index == 0 else -1.0), 1.0) + return 1.0 - d / 0.15 if d < 0.15 else DIM * 1.5 + "nest": # inner and outer frame take turns on the beat + var on := frame_index == posmod(floori(grid), 2) + return 1.0 - smoothstep(0.4, 1.0, fposmod(grid, 1.0)) * 0.8 if on else DIM + "beat": # both frames on the kick + return 0.08 + 0.92 * kick + return DIM diff --git a/scripts/show/stage_set.gd.uid b/scripts/show/stage_set.gd.uid new file mode 100644 index 0000000..0acbc1e --- /dev/null +++ b/scripts/show/stage_set.gd.uid @@ -0,0 +1 @@ +uid://cj6133yj6m08v From 300f2c84b895782a8a9f40bae48f04fd18eae6aa Mon Sep 17 00:00:00 2001 From: SlipstreamSurfer <277415410+SlipstreamSurfer@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:54:00 +0100 Subject: [PATCH 3/3] Add a second place: a room with only the DJ and light all round Settings > Show > Place switches between the hangar and a closed room round the booth. The room has its own rig in the same groups (ceiling grid, walls, floor row, corners, roof spots, LED bars), its own stage set (tubes on every wall, rings and frames round a screen on the wall ahead), haze and cameras. The hangar keeps its rig and set. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 + scripts/hall.gd | 43 ++++++++ scripts/main.gd | 7 +- scripts/rig.gd | 215 ++++++++++++++++++++++++++++----------- scripts/settings.gd | 17 +++- scripts/show/director.gd | 6 +- scripts/view.gd | 4 + 7 files changed, 231 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 078844b..f1a7395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ a nod of the whole rig, a fan that opens and closes, a ladder that travels along and up, or a wave. - Two beats a step, every beat in the second half of a peak; in a break a step a bar, growing with the build. A clap nods the rig a little. Gobos turn with the tempo. +- A second place, Room (Settings → Show → Place): only the DJ and the booth in a closed 16 × 16 m room full of + light. A grid of beams in the ceiling, three rows of beams and spots on the wall behind the DJ, a row of floor + beams in front of the booth, beams on the wall ahead, on both side walls and in two corner towers, spots round + the ceiling, LED tubes floor to ceiling on every wall, rings and two LED frames round a screen on the wall + ahead, and LED bars along the foot of the walls. The same show plays it; cameras 3–5 stay inside the room. - Beam rain is removed, and so are the dot grid on the far wall and the rings under the rear truss. - A stage set in the main stage: two LED frames round the centre screen, six vertical LED tubes between the towers in a V (tall at the outside) and a grid of LED rings on the back wall. The rings swell on the kick; per passage diff --git a/scripts/hall.gd b/scripts/hall.gd index 1dee858..2889258 100644 --- a/scripts/hall.gd +++ b/scripts/hall.gd @@ -14,6 +14,13 @@ const BOOTH_Z := -33.0 # the front of the DJ table const RISER := 0.4 const TABLE := DECK + RISER + 1.05 const BARRIER := -24.0 +# The place: "hangar" (the festival hall) or "room" (only the DJ, in a closed room full of light). +static var venue := "hangar" +# The room: 16 × 16 m round the booth, its floor at the deck's height so the booth and the cameras stay put. +const ROOM_X := 8.0 +const ROOM_Z0 := -42.4 +const ROOM_Z1 := -26.4 +const ROOM_TOP := DECK + 7.0 @export_enum("Two decks:2", "Festival — four decks:4") var booth_decks := 2 var screens: Array[ShaderMaterial] = [] @@ -29,6 +36,9 @@ var stage_screen_material: ShaderMaterial func build() -> void: var black := Kit.mat(Color(0.018, 0.018, 0.02), 0.8) + if venue == "room": + _room(black) + return var frame := _hall_material(Color(0.17, 0.20, 0.19), 0.73, 0.12, 0.3) var boxes := Kit.Batch.new(Kit.unit_box(), frame) var dark := Kit.Batch.new(Kit.unit_box(), black) @@ -79,8 +89,35 @@ func build() -> void: boxes.build(self) dark.build(self) +# A closed room: dark concrete floor, walls and ceiling that take the colour of the light, the booth in the middle, +# a screen on the wall in front of the DJ, and haze that fills it. +func _room(black: Material) -> void: + var size := Vector3(ROOM_X * 2.0, ROOM_TOP - DECK, ROOM_Z1 - ROOM_Z0) + var middle := Vector3(0.0, DECK, (ROOM_Z0 + ROOM_Z1) / 2.0) + var floor_mesh := MeshInstance3D.new() + var plane := PlaneMesh.new() + plane.size = Vector2(size.x, size.z) + floor_mesh.mesh = plane + floor_mesh.material_override = _concrete() + floor_mesh.position = middle + Vector3(0.0, 0.001, 0.0) + add_child(floor_mesh) + var walls := Kit.Batch.new(Kit.unit_box(), _hall_material(Color(0.07, 0.07, 0.075), 0.9, 0.0, 0.45)) + walls.add(Kit.block(Vector3(size.x, DECK, size.z), middle - Vector3(0.0, DECK / 2.0, 0.0))) # the slab under the floor + walls.add(Kit.block(Vector3(size.x + 0.6, 0.3, size.z + 0.6), middle + Vector3(0.0, size.y + 0.15, 0.0))) + for side in [-1.0, 1.0]: + walls.add(Kit.block(Vector3(0.3, size.y, size.z), middle + Vector3(side * (ROOM_X + 0.15), size.y / 2.0, 0.0))) + walls.add(Kit.block(Vector3(size.x, size.y, 0.3), middle + Vector3(0.0, size.y / 2.0, side * (size.z / 2.0 + 0.15)))) + walls.build(self).name = "RoomWalls" + var dark := Kit.Batch.new(Kit.unit_box(), black) + dark.add(Kit.block(Vector3(6, RISER, 3.2), Vector3(0, DECK + RISER / 2, BOOTH_Z - 0.6))) + dark.build(self) + _stage_screen() + _booth(black) + _fog("RoomHaze", middle + Vector3(0.0, size.y / 2.0, 0.0), size - Vector3(0.4, 0.4, 0.4), 0.016) + func _stage_screen() -> void: var center := Vector3(0, DECK + 2.8 + 3.5, -36.3) + if venue == "room": center = Vector3(0, DECK + 3.6, ROOM_Z1 - 0.12) var frame := Kit.mat(Color(0.012, 0.012, 0.014), 0.8) stage_screen = Node3D.new() stage_screen.name = "StageScreen" @@ -95,6 +132,12 @@ func _stage_screen() -> void: face.material_override = stage_screen_material face.position = center stage_screen.add_child(face) + if venue == "room": + # on the wall in front of the DJ, smaller, turned to face the booth + for child in stage_screen.get_children(): + child.position = center + (child.position - center) * Vector3(0.55, 0.55, -0.55) # its frame behind it + child.scale = Vector3(0.55, 0.55, 0.55) + child.rotation.y = PI # Two floor hazers at the rear, with recessed outlets facing the crowd. func _hazers(boxes: Kit.Batch, dark: Kit.Batch) -> void: diff --git a/scripts/main.gd b/scripts/main.gd index 35a84b7..d144e2e 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -98,6 +98,7 @@ func _ready() -> void: if args.has("seed"): seed(int(args.seed)) for n in ["sv_beat", "sv_energy", "sv_hands", "sv_haze", "sv_phones"]: RenderingServer.global_shader_parameter_add(n, RenderingServer.GLOBAL_VAR_TYPE_FLOAT, 1.0 if n == "sv_haze" else 0.0) + Hall.venue = str(args.get("venue", settings.values.get("venue", "hangar"))) hall = Hall.new() add_child(hall) hall.build() @@ -106,7 +107,7 @@ func _ready() -> void: rig.build() crowd = Crowd.new() add_child(crowd) - crowd.build(3000) + crowd.build(0 if Hall.venue == "room" else 3000) # the room has no crowd: only the DJ _environment() view = View.new() add_child(view) @@ -282,6 +283,10 @@ func _hud() -> void: # The source and the graphics, from the settings screen (and once at the start). func _apply_settings(options: Dictionary) -> void: + # another place is another build: the scene loads again, in the new place + if str(options.get("venue", Hall.venue)) != Hall.venue and not args.has("venue"): + get_tree().call_deferred("reload_current_scene") + return if hall.stage_screen: hall.stage_screen.visible = bool(options.get("stage_screen", true)) show.genre = options.get("genre", show.genre) show.safe = args.has("safe") or bool(options.get("safe", show.safe)) diff --git a/scripts/rig.gd b/scripts/rig.gd index 82d906d..c7816a1 100644 --- a/scripts/rig.gd +++ b/scripts/rig.gd @@ -103,7 +103,10 @@ var beam_strength := 1.0 var ring_levels := PackedFloat32Array() var ring_at: Array[Vector3] = [] var set_pixels: Array[Vector3] = [] -var set_part := PackedInt32Array() # per pixel: 0..5 a tube (left to right), 10 the inner frame, 11 the outer frame +var set_part := PackedInt32Array() # per pixel: a tube (0, 1, ...), or FRAME the inner frame, FRAME + 1 the outer one +const FRAME := 100 +var _frame_sizes: Array = [] +var _frame_centre := Vector3.ZERO var set_place := PackedFloat32Array() # per pixel: up a tube 0..1, or round a frame 0..1 (from the bottom middle) var set_levels := PackedFloat32Array() var piece_color := Color.WHITE @@ -185,6 +188,51 @@ func build() -> void: for a in OS.get_cmdline_user_args(): if a.begins_with("--trusses="): shape = a.get_slice("=", 1) + if Hall.venue == "room": _room_rig(chords, braces, plates, feet) + else: _hangar_rig(chords, braces, plates, mounts, safeties, feet, F) + + chords.build(self) + braces.build(self) + plates.build(self) + mounts.build(self).name = "DelayMounts" + safeties.build(self).name = "DelaySafeties" + feet.build(self).name = "TowerFeet" + + var room := Hall.venue == "room" + var x_half := Hall.ROOM_X if room else 21.0 + var z_near := Hall.ROOM_Z0 if room else Hall.FRONT + var z_far := Hall.ROOM_Z1 if room else 26.0 + for f in fixtures: + f.x01 = clampf((f.mount.x + x_half) / (2.0 * x_half), 0.0, 1.0) + f.z01 = clampf((f.mount.z - z_near) / (z_far - z_near), 0.0, 1.0) + f.stage = f.group in ["front", "back", "roof"] + for b in battens: + b.x01 = clampf((b.mount.x + x_half) / (2.0 * x_half), 0.0, 1.0) + b.z01 = clampf((b.mount.z - z_near) / (z_far - z_near), 0.0, 1.0) + for i in fixtures.size(): + fixtures[i].idx = i + fixtures[i].rp = randf() + 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 + var lamps := [Vector3(0, Hall.ROOM_TOP - 1.0, -34.4), Vector3(0, Hall.DECK + 2.5, -28.0)] if room else [Vector3(0, 9, Hall.BACK + 2), Vector3(0, 3, F - 0.5), Vector3(0, 10.5, -8), Vector3(0, 10, 22)] + for p in lamps: + var o := OmniLight3D.new() + o.position = p + o.omni_range = 30 + o.light_energy = 0.0 + o.light_color = Color(0.92, 0.95, 1.0) + o.light_volumetric_fog_energy = 0.12 + o.visible = false + add_child(o) + _strobe_lights.append(o) + +# Two omega rails under each fixed delay head, clamped round the crossbar's upper chords. +# The closed safety loop joins the base to the bar independently of those rails. +func _hangar_rig(chords: Kit.Batch, braces: Kit.Batch, plates: Kit.Batch, mounts: Kit.Batch, safeties: Kit.Batch, feet: Kit.Batch, F: float) -> void: # front: beams along the edge of the deck, with a gap where the PA hangs, battens between them for k in 30: var x := -20.3 + k * 1.4 @@ -226,8 +274,8 @@ func build() -> void: t.position = Vector3(0, TRUSS_Y[i], TRUSS_Z[i]) add_child(t) trusses.append(t) - var c := Kit.Batch.new(Kit.unit_tube(), alu) - var b := Kit.Batch.new(Kit.unit_tube(), alu) + var c := Kit.Batch.new(Kit.unit_tube(), chords.material) + var b := Kit.Batch.new(Kit.unit_tube(), chords.material) if shape == "v": Kit.truss(c, b, Vector3(-19, 0, V_DEPTH), Vector3(0, 0, 0)) Kit.truss(c, b, Vector3(0, 0, 0), Vector3(19, 0, V_DEPTH)) @@ -296,42 +344,68 @@ func build() -> void: f.xn = s _batten("side", Vector3(x - s * 0.22, 5.25, z), -1, Vector3.UP, Vector3(-s, 0, 0), (z + 19.0) / 40.0) - chords.build(self) - braces.build(self) - plates.build(self) - mounts.build(self).name = "DelayMounts" - safeties.build(self).name = "DelaySafeties" - feet.build(self).name = "TowerFeet" - for f in fixtures: - f.x01 = clampf((f.mount.x + 21.0) / 42.0, 0.0, 1.0) - f.z01 = clampf((f.mount.z - Hall.FRONT) / (26.0 - Hall.FRONT), 0.0, 1.0) - f.stage = f.group in ["front", "back", "roof"] - for b in battens: - b.x01 = clampf((b.mount.x + 21.0) / 42.0, 0.0, 1.0) - b.z01 = clampf((b.mount.z - Hall.FRONT) / (26.0 - Hall.FRONT), 0.0, 1.0) - for i in fixtures.size(): - fixtures[i].idx = i - fixtures[i].rp = randf() - 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)]: - var o := OmniLight3D.new() - o.position = p - o.omni_range = 30 - o.light_energy = 0.0 - o.light_color = Color(0.92, 0.95, 1.0) - o.light_volumetric_fog_energy = 0.12 - o.visible = false - add_child(o) - _strobe_lights.append(o) +# The room: light on every surface round the DJ. A grid of beams in the ceiling (the "hall" group), three rows of +# beams and a row of spots on the wall behind the DJ ("back"), a row of beams on the floor in front of the booth +# ("front"), beams on the wall the DJ faces ("rear"), on both side walls ("side"), in the two far corners +# ("delay"), spots round the ceiling's edge ("roof"), washes in the ceiling, and LED bars along the foot of the walls. +func _room_rig(chords: Kit.Batch, braces: Kit.Batch, plates: Kit.Batch, feet: Kit.Batch) -> void: + var top := Hall.ROOM_TOP + var base_y := Hall.DECK + var x0 := Hall.ROOM_X + var z0 := Hall.ROOM_Z0 + var z1 := Hall.ROOM_Z1 + # a ceiling grid of pipes, the beams hanging from it + for i in 4: + var z := z0 + 2.5 + i * 3.8 + chords.add(Kit.tube(Vector3(-x0 + 0.5, top - 0.25, z), Vector3(x0 - 0.5, top - 0.25, z), 0.025)) + for k in 5: + var x := -6.0 + k * 3.0 + _add(BEAM, "hall", Vector3(x, top - 0.45, z), Vector3.DOWN, -1, k / 4.0, i) + for x in [-4.5, 4.5]: _add(WASH, "hall", Vector3(x, top - 0.45, z + 1.9), Vector3.DOWN, -1, 0.5 + x / 9.0, i) + # behind the DJ: three rows of beams and a row of spots on the wall + for k in 6: + var x := -6.25 + k * 2.5 + for j in 3: _add(BEAM, "back", Vector3(x, base_y + 1.4 + j * 1.6, z0 + 0.35), Vector3.BACK, -1, k / 5.0, j) + _add(SPOT, "back", Vector3(x, top - 0.9, z0 + 0.4), Vector3.BACK, -1, k / 5.0, 3) + # on the base_y in front of the booth, a row of beams pointing up + for k in 12: + var x := -6.6 + k * 1.2 + _add(BEAM, "front", Vector3(x, base_y, -29.6), Vector3.UP, -1, k / 11.0, 0) + plates.add(Kit.block(Vector3(0.45, 0.04, 0.45), Vector3(x, base_y + 0.02, -29.6))) + # the wall in front of the DJ: beams in a row under the ceiling, looking back into the room + for k in 10: + var x := -6.75 + k * 1.5 + _add(BEAM, "rear", Vector3(x, top - 1.2, z1 - 0.35), Vector3.FORWARD, -1, k / 9.0, 0) + # both side walls: two heights + for s in [-1.0, 1.0]: + for i in 4: + var z := z0 + 2.0 + i * 4.0 + for j in 2: + var f := _add(BEAM, "side", Vector3(s * (x0 - 0.35), base_y + 2.0 + j * 2.6, z), Vector3(-s, 0, 0), -1, i / 3.0, j) + f.xn = s + # the two far corners: a column of beams each, on a short tower + for s in [-1.0, 1.0]: + var x: float = s * (x0 - 0.8) + var z := z1 - 0.8 + Kit.truss(chords, braces, Vector3(x, base_y, z), Vector3(x, base_y + 4.5, z), 0.3) + feet.add(Kit.block(Vector3(0.6, 0.05, 0.6), Vector3(x, base_y + 0.025, z))) + for j in 4: + var f := _add(BEAM, "delay", Vector3(x + (j - 1.5) * 0.35, base_y + 4.7, z), Vector3.UP, -1, j / 3.0, 0) + f.xn = s + # spots round the edge of the ceiling + for k in 8: + var a := TAU * (k + 0.5) / 8.0 + _add(SPOT, "roof", Vector3(sin(a) * (x0 - 1.2), top - 0.45, -34.4 + cos(a) * 6.8), Vector3.DOWN, -1, k / 7.0, 0) + # LED bars along the foot of all four walls, facing into the room + for s in [-1.0, 1.0]: + for i in 7: + var z := z0 + 1.6 + i * 2.2 + _batten("side", Vector3(s * (x0 - 0.12), base_y + 0.25, z), -1, Vector3(0, 0, 1), Vector3(-s, 0, 0), i / 6.0) + for i in 7: + var x := -6.6 + i * 2.2 + _batten("back" if s < 0 else "rear", Vector3(x, base_y + 0.25, (z0 + 0.12) if s < 0 else (z1 - 0.12)), -1, Vector3.RIGHT, Vector3.BACK if s < 0 else Vector3.FORWARD, i / 6.0) -# Two omega rails under each fixed delay head, clamped round the crossbar's upper chords. -# The closed safety loop joins the base to the bar independently of those rails. func _delay_mount(mounts: Kit.Batch, safeties: Kit.Batch, at: Vector3) -> void: for dx in [-0.105, 0.105]: mounts.add(Kit.tube(at + Vector3(dx, -0.015, -0.19), at + Vector3(dx, -0.015, 0.19), 0.015)) @@ -513,32 +587,56 @@ func _yoke_mesh(r: float) -> ArrayMesh: return st.commit() func _set_pieces() -> void: + var room := Hall.venue == "room" var wall := Hall.BACK + 0.45 var material := Kit.shader("res://shaders/batten_pixel.gdshader", {"gain": 6.0}) - # rings on the back wall in the gaps between the towers, low and high - for x in [-18.0, -12.0, -6.0, 6.0, 12.0, 18.0]: - for y in [3.6, 13.9]: ring_at.append(Vector3(x, y, wall)) + var facing_z := Basis(Vector3.RIGHT, PI / 2.0) # a torus lies flat; stood up, it faces along Z + var ring_bases: Array[Basis] = [] + if room: + # rings on the wall in front of the DJ, either side of the screen, low and high + for x in [-6.1, -3.5, 3.5, 6.1]: + for y in [Hall.DECK + 1.6, Hall.DECK + 5.6]: + ring_at.append(Vector3(x, y, Hall.ROOM_Z1 - 0.15)) + ring_bases.append(facing_z) + else: + # rings on the back wall in the gaps between the towers, low and high + for x in [-18.0, -12.0, -6.0, 6.0, 12.0, 18.0]: + for y in [3.6, 13.9]: + ring_at.append(Vector3(x, y, wall)) + ring_bases.append(facing_z) var torus := TorusMesh.new() - torus.inner_radius = 0.55 - torus.outer_radius = 0.66 + torus.inner_radius = 0.42 if room else 0.55 + torus.outer_radius = 0.5 if room else 0.66 torus.rings = 48 torus.ring_segments = 8 _rings = _multimesh(torus, ring_at.size(), material, true, false, false) - for i in ring_at.size(): _rings.set_instance_transform(i, Transform3D(Basis(Vector3.RIGHT, PI / 2.0), ring_at[i])) + for i in ring_at.size(): _rings.set_instance_transform(i, Transform3D(ring_bases[i], ring_at[i])) ring_levels.resize(ring_at.size()) - # tubes: one pixel every 40 cm from the deck up - for t in TUBES.size(): - var bottom := Hall.DECK + 3.0 + 0.3 - var top: float = TUBES[t][1] - var n := int((top - bottom) / 0.4) + 1 + # tubes: [bottom, top] of each, one pixel every 40 cm; in the room they stand on every wall, floor to ceiling + var tubes: Array = [] + if room: + var low := Hall.DECK + 0.6 + var high := Hall.ROOM_TOP - 0.5 + for x in [-5.0, -2.5, 0.0, 2.5, 5.0]: tubes.append([Vector3(x, low, Hall.ROOM_Z0 + 0.15), high]) + for side in [-1.0, 1.0]: + for z in [-38.4, -34.4, -30.4]: tubes.append([Vector3(side * (Hall.ROOM_X - 0.15), low, z), high]) + for x in [-7.4, -4.8, -2.2, 2.2, 4.8, 7.4]: tubes.append([Vector3(x, low, Hall.ROOM_Z1 - 0.15), high - 1.6]) + else: + for t in TUBES: tubes.append([Vector3(t[0], Hall.DECK + 3.3, wall + 0.05), t[1]]) + for t in tubes.size(): + var bottom: Vector3 = tubes[t][0] + var top: float = tubes[t][1] + var n := int((top - bottom.y) / 0.4) + 1 for i in n: - set_pixels.append(Vector3(TUBES[t][0], bottom + i * 0.4, wall + 0.05)) + set_pixels.append(bottom + Vector3(0.0, i * 0.4, 0.0)) set_part.append(t) set_place.append(float(i) / float(n - 1)) - # frames round the screen (5 × 7 m, centre 7.9 m up): one close round it, one taller and narrower outside it - var centre := Vector3(0.0, Hall.DECK + 6.3, -36.05) + # frames round the screen: one close round it, one taller and wider outside it + var centre := Vector3(0.0, Hall.DECK + 3.6, Hall.ROOM_Z1 - 0.2) if room else Vector3(0.0, Hall.DECK + 6.3, -36.05) + _frame_sizes = [Vector2(3.15, 4.3), Vector2(3.8, 5.8)] if room else [Vector2(5.7, 7.8), Vector2(6.9, 10.6)] + _frame_centre = centre for f in 2: - var size := Vector2(5.7, 7.8) if f == 0 else Vector2(6.9, 10.6) + var size: Vector2 = _frame_sizes[f] var perimeter := 2.0 * (size.x + size.y) var n := int(perimeter / 0.35) for i in n: @@ -552,19 +650,18 @@ func _set_pieces() -> void: elif d < hx + size.y + size.x: at = Vector2(hx - (d - hx - size.y), hy) elif d < hx + 2.0 * size.y + size.x: at = Vector2(-hx, hy - (d - hx - size.y - size.x)) else: at = Vector2(-hx + (d - hx - 2.0 * size.y - size.x), -hy) - set_pixels.append(centre + Vector3(at.x, at.y, 0.02 * f)) - set_part.append(10 + f) + set_pixels.append(centre + Vector3(at.x, at.y, (-0.02 if room else 0.02) * f)) + set_part.append(FRAME + f) set_place.append(float(i) / float(n)) var pixel := BoxMesh.new() pixel.size = Vector3(0.16, 0.3, 0.08) _set = _multimesh(pixel, set_pixels.size(), material, true, false, false) for i in set_pixels.size(): - var frame_side := set_part[i] >= 10 var basis := Basis.IDENTITY # a frame's pixels lie along their edge - if frame_side: - var local := set_pixels[i] - centre - var size := Vector2(5.7, 7.8) if set_part[i] == 10 else Vector2(6.9, 10.6) + if set_part[i] >= FRAME: + var local := set_pixels[i] - _frame_centre + var size: Vector2 = _frame_sizes[set_part[i] - FRAME] if absf(absf(local.y) - size.y / 2.0) < 0.01: basis = Basis(Vector3.BACK, PI / 2.0) _set.set_instance_transform(i, Transform3D(basis, set_pixels[i])) set_levels.resize(set_pixels.size()) @@ -704,7 +801,7 @@ func update(dt: float, beat: float) -> void: var c := piece_color * ring_levels[i] _rings.set_instance_color(i, Color(c.r, c.g, c.b, 1.0)) for i in set_pixels.size(): - var c := (frame_color if set_part[i] >= 10 else piece_color) * set_levels[i] + var c := (frame_color if set_part[i] >= FRAME else piece_color) * set_levels[i] _set.set_instance_color(i, Color(c.r, c.g, c.b, 1.0)) for o in _strobe_lights: o.visible = strobe > 0.02 diff --git a/scripts/settings.gd b/scripts/settings.gd index 38d00cc..6c4b38c 100644 --- a/scripts/settings.gd +++ b/scripts/settings.gd @@ -6,10 +6,12 @@ signal applied(options: Dictionary) signal closed const DEFAULTS := {"source": "both", "device": "", "track": "", "latency": 0.08, "quality": "auto", "genre": "auto", "osc_port": 4460, - "rekordbox_local": false, "setup_done": false, "safe": false, "stage_screen": true, "screen": -1, "fullscreen": false, "fps_limit": 60, "camera": 1} + "rekordbox_local": false, "setup_done": false, "safe": false, "stage_screen": true, "screen": -1, "fullscreen": false, "fps_limit": 60, "camera": 1, "venue": "hangar"} const SOURCES := ["system", "both", "rekordbox", "file"] const SOURCE_LABELS := ["System audio", "System audio + rekordbox", "Rekordbox", "Music file"] const QUALITIES := ["auto", "low", "light", "normal", "high", "ultra"] +const VENUES := ["hangar", "room"] +const VENUE_LABELS := ["Hangar: a festival stage", "Room: only you and the light"] var values := DEFAULTS.duplicate() var config_path := "user://settings.cfg" @@ -18,6 +20,7 @@ var _audio_state := {} var _root: Control var _source: OptionButton var _genre: OptionButton +var _venue: OptionButton var _device: OptionButton var _track: LineEdit var _latency: SpinBox @@ -95,8 +98,9 @@ func _save_config(config: ConfigFile) -> Error: static func _validated(options: Dictionary) -> Dictionary: var result := DEFAULTS.duplicate() - for key in ["source", "device", "track", "quality", "genre"]: + for key in ["source", "device", "track", "quality", "genre", "venue"]: if options.get(key) is String: result[key] = options[key] + if result.venue not in VENUES: result.venue = DEFAULTS.venue if result.genre not in Show.GENRE_ORDER: result.genre = DEFAULTS.genre if result.source not in SOURCES: result.source = DEFAULTS.source if result.quality not in QUALITIES: result.quality = DEFAULTS.quality @@ -193,6 +197,12 @@ func _ready() -> void: _genre.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS for id in Show.GENRE_ORDER: _genre.add_item(Show.genre_name(id)) _row(show_options, "Genre", _genre) + _venue = OptionButton.new() + _venue.fit_to_longest_item = false + _venue.custom_minimum_size.x = 280 + _venue.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + for text in VENUE_LABELS: _venue.add_item(text) + _row(show_options, "Place", _venue) _source.item_selected.connect(func(_index: int): _source_rows()) _device = OptionButton.new() _device.fit_to_longest_item = false @@ -287,6 +297,7 @@ func _row(parent: VBoxContainer, caption: String, control: Control) -> HBoxConta func _sync() -> void: _source.select(SOURCES.find(values.source)) _genre.select(Show.GENRE_ORDER.find(values.genre)) + _venue.select(maxi(0, VENUES.find(values.get("venue", "hangar")))) _device.clear() _device.add_item("Automatic / default") _device.set_item_metadata(0, "") @@ -330,7 +341,7 @@ func _source_rows() -> void: func _apply() -> void: var options := values.duplicate() - options.merge({"source": SOURCES[_source.selected], "genre": Show.GENRE_ORDER[_genre.selected], "device": _device.get_selected_metadata(), + options.merge({"source": SOURCES[_source.selected], "genre": Show.GENRE_ORDER[_genre.selected], "venue": VENUES[_venue.selected], "device": _device.get_selected_metadata(), "track": _track.text.strip_edges(), "latency": _latency.value / 1000.0, "quality": QUALITIES[_quality.selected], "osc_port": int(_port.value), "rekordbox_local": _rekordbox_page.same_computer, "safe": _safe.button_pressed, "stage_screen": _stage_screen.button_pressed, "fullscreen": _fullscreen.button_pressed, diff --git a/scripts/show/director.gd b/scripts/show/director.gd index 2dbe58c..8787794 100644 --- a/scripts/show/director.gd +++ b/scripts/show/director.gd @@ -1030,13 +1030,13 @@ func _direct(dt: float) -> void: var part: int = rig.set_part[i] var place: float = rig.set_place[i] var v: float - if part >= 10: - v = ShowSet.frame(frames, part - 10, place, grid, kick_k) if not brk else 0.06 + 0.25 * sig.build + if part >= Rig.FRAME: + v = ShowSet.frame(frames, part - Rig.FRAME, place, grid, kick_k) if not brk else 0.06 + 0.25 * sig.build elif brk: v = 1.0 if place <= sig.build else ShowSet.DIM else: v = ShowSet.tube(tubes, part, place, grid, kick_k) - if melodic and part < 10 and posmod(hash("%d:%d" % [part, _dot_pick]), 3) != 0: v = maxf(v, _dot_flash) + if melodic and part < Rig.FRAME and posmod(hash("%d:%d" % [part, _dot_pick]), 3) != 0: v = maxf(v, _dot_flash) if safe: v = minf(v, 0.4) rig.set_levels[i] = _lamp((v * 0.8 + sig.drop * 0.4) * master) * pieces diff --git a/scripts/view.gd b/scripts/view.gd index 90f99d4..9ee93fd 100644 --- a/scripts/view.gd +++ b/scripts/view.gd @@ -44,6 +44,10 @@ func update(dt: float, show: Show) -> void: elif _camera >= 3: pos = [Vector3(0, 2.1, -8), Vector3(0, 2.8, 25), Vector3(12, 4.6, Hall.BOOTH_Z - 1.0)][_camera - 3] look = Vector3(0, Hall.DECK + 3.0, Hall.BOOTH_Z + (5.0 if _camera == 5 else 0.0)) + if Hall.venue == "room": + # in the room: from the far wall, from a high corner and from the side, all inside its walls + pos = [Vector3(0, Hall.DECK + 2.0, Hall.ROOM_Z1 - 1.2), Vector3(6.6, Hall.ROOM_TOP - 1.0, Hall.ROOM_Z1 - 1.0), Vector3(6.8, Hall.DECK + 3.0, Hall.BOOTH_Z - 1.0)][_camera - 3] + look = Vector3(0, Hall.DECK + 2.2, Hall.BOOTH_Z - 1.0) fov = 72.0 if _camera == 3 else 68.0 if _camera == 5 else 60.0 else: # a person standing: never quite still, a little give in the knees on the beat