diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f7bb09..f1a7395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # 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. +- 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 + 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). + ## 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/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 cde49ad..c7816a1 100644 --- a/scripts/rig.gd +++ b/scripts/rig.gd @@ -97,17 +97,23 @@ 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: 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 +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} @@ -182,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 @@ -223,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)) @@ -293,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)) @@ -510,36 +587,84 @@ 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 room := Hall.venue == "room" + var wall := Hall.BACK + 0.45 + var material := Kit.shader("res://shaders/batten_pixel.gdshader", {"gain": 6.0}) + 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.5 - torus.outer_radius = 0.6 + 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 - 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(ring_bases[i], ring_at[i])) ring_levels.resize(ring_at.size()) - dot_levels.resize(dot_at.size()) + # 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(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: 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 = _frame_sizes[f] + 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 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 basis := Basis.IDENTITY + # a frame's pixels lie along their edge + 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()) func _batten_batches() -> void: var body := BoxMesh.new() @@ -675,9 +800,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] >= 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 o.light_energy = 16.0 * strobe 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 299a6ed..8787794 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 @@ -995,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 @@ -1009,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 >= 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 < 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 # ----- 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/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) 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 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