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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ GODOT=/path/to/Godot_v4.7.2-stable
"$GODOT" --headless --path . --script tools/test_show_checks.gd
"$GODOT" --headless --path . --script tools/test_feedback.gd
"$GODOT" --headless --path . --script tools/test_choreography.gd
"$GODOT" --headless --path . --script tools/test_bar_pixels.gd
"$GODOT" --headless --path . --script tools/test_session.gd
"$GODOT" --headless --path . --script tools/test_rekordbox.gd
"$GODOT" --headless --path . --script tools/test_listen_ear.gd
Expand Down
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# Changelog

## 0.60.0-test2 — Independently programmed LED bars

- Every batten has twelve independently set dimmer and colour cells, still rendered in one batch.
- Phrase-held bar figures include solid, 50/50, odd/even, centre, end and travelling-window states.
- Alternating LED colours follow the active show palette; monochrome cues keep all cells in one colour.
- Carries forward the symmetric back towers, saved source continuity, calmer stage row and the test1 show design.
- New roof bars and a second head row on the back towers remain follow-up work.

## 0.60.0-test1 — Symmetric stage towers and show source continuity

- Back towers now use eight mirrored positions at an even 6 m spacing, centred around the LED screen.
- Launching with a saved audio device no longer silently changes the saved source to System + rekordbox.
- Carries forward the current candidate's calmer stage row, preserved light-desk levels, integrated scene accents,
longer evolving depth chase, screen visuals and optional local debug feedback.
- This is a local review candidate. Bars do not yet have independent LED pixel states, and new roof bars or a second
head row on the back towers are not included in this build.

## Unreleased — Show balance, spatial patterns and debug feedback

Expand Down
10 changes: 8 additions & 2 deletions docs/sv-recovery-plan.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SV: voorgesteld herstel- en ontwikkelplan

Status: uitvoering gestart na expliciete opdracht. De wijzigingen worden als lokale kandidaten in afzonderlijke stappen opgeleverd; er wordt niets publiek gepubliceerd zonder aparte goedkeuring. Lees daarnaast `choreography-blueprint.md` en `pattern-design.md`.
Status: uitvoering gestart; test1 is als publieke prerelease uitgebracht na expliciete opdracht. Volgende stappen komen in afzonderlijke kandidaten. Lees daarnaast `choreography-blueprint.md` en `pattern-design.md`.

## Richting

Expand Down Expand Up @@ -174,6 +174,12 @@ Lever per stap één onveranderlijke lokale kandidaat met een kleine, duidelijke
- De volledige Godot 4.7.2-checksuite slaagt voor deze kandidaat.
- De al aanwezige roof-spot-, sequencer-, depth-chase-, scherm- en feedbackwijzigingen zijn meegenomen in deze lokale kandidaat.

## Uitgevoerd in kandidaat 0.60.0-test2

- Elke batten heeft twaalf afzonderlijke dimmer- en kleurwaarden binnen één gebatchte pixelmesh.
- De barpatronen omvatten aangehouden 50/50-vlakken, even/oneven cellen, midden, uiteinden en een langzaam verplaatsend venster. Afwisselende kleuren volgen het bestaande showpalet en monochrome scènes blijven monochroom.
- `tools/test_bar_pixels.gd` controleert het aantal cellen, patroonvormen, determinisme en de gebatchte renderstructuur.

## Resterende uitvoering

Volgende stappen zijn pixelbars met onafhankelijke segmentwaarden, de visuele beoordeling van de towergeometrie, een roof-spotpassage die zonder Stage row en Hall beams leesbaar blijft, daarna pas nieuwe roof-bars en een extra spotrij op de back towers. Vervolgens volgen samenhang, comfortlogging, prestaties, scherm/zaal/camera, langere praktijkbeoordeling en lokale iteraties. De candidate status is geen artistieke goedkeuring.
Volgende stappen zijn de visuele beoordeling van pixelbars en towergeometrie, een roof-spotpassage die zonder Stage row en Hall beams leesbaar blijft, daarna pas nieuwe roof-bars en een extra spotrij op de back towers. Vervolgens volgen samenhang, comfortlogging, prestaties, scherm/zaal/camera, langere praktijkbeoordeling en lokale iteraties. De candidate status is geen artistieke goedkeuring.
2 changes: 1 addition & 1 deletion project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ config_version=5
[application]

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

Expand Down
28 changes: 25 additions & 3 deletions scripts/rig.gd
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Fixture:
var ifx_phase := 0.0

class Batten:
const PIXELS := BarPixels.COUNT
var mount := Vector3.ZERO
var truss := -1
var axis := Vector3.RIGHT
Expand All @@ -76,6 +77,8 @@ class Batten:
var x01 := 0.5
var z01 := 0.0
var ifx_phase := 0.0
var pixel_levels := PackedFloat32Array()
var pixel_colors: Array[Color] = []

var fixtures: Array[Fixture] = []
var battens: Array[Batten] = []
Expand All @@ -100,6 +103,7 @@ var _mm := [] # per kind: {base, yoke, head, lens, beam}
var _mm_nodes := {} # MultiMesh -> the node that draws it
var _batten_body: MultiMesh
var _batten_line: MultiMesh
var _batten_pixels: MultiMeshInstance3D
var _blinder_lamps: MultiMesh
var _blinder_xf: Array[Transform3D] = []
var _blinder_lights: Array[SpotLight3D] = []
Expand Down Expand Up @@ -404,6 +408,10 @@ func _batten(group: String, mount: Vector3, truss: int, axis: Vector3, face: Vec
b.axis = axis
b.face = face
b.u = u
b.pixel_levels.resize(Batten.PIXELS)
b.pixel_levels.fill(1.0)
b.pixel_colors.resize(Batten.PIXELS)
b.pixel_colors.fill(Color.BLACK)
battens.append(b)

func _blinder(at: Vector3, towards: Vector3) -> void:
Expand Down Expand Up @@ -491,6 +499,18 @@ func _batten_batches() -> void:
line.size = Vector3(1.0, 0.05, 0.01)
_batten_body = _multimesh(body, battens.size(), _body)
_batten_line = _multimesh(line, battens.size(), Kit.shader("res://shaders/batten.gdshader"), true, true, false)
var led := BoxMesh.new()
led.size = Vector3(0.061, 0.07, 0.025)
var pixel_mm := _multimesh(led, battens.size() * Batten.PIXELS, Kit.shader("res://shaders/batten_pixel.gdshader"), true, false, false)
_batten_pixels = _mm_nodes[pixel_mm] as MultiMeshInstance3D
for i in battens.size():
var b: Batten = battens[i]
var at := _hung_at(b.mount, b.truss)
var y := b.face.cross(b.axis)
var bb := Basis(b.axis, y, b.face)
for pixel in Batten.PIXELS:
var offset := (float(pixel) + 0.5) / Batten.PIXELS - 0.5
_batten_pixels.multimesh.set_instance_transform(i * Batten.PIXELS + pixel, Transform3D(bb, at + b.axis * offset))
var lamp := CylinderMesh.new()
lamp.top_radius = 0.12
lamp.bottom_radius = 0.12
Expand Down Expand Up @@ -598,9 +618,11 @@ func update(dt: float, beat: float) -> void:
var bb := Basis(b.axis, y, b.face)
_batten_body.set_instance_transform(i, Transform3D(bb, at))
_batten_line.set_instance_transform(i, Transform3D(bb, at + b.face * 0.085))
var c := b.color * b.level
_batten_line.set_instance_color(i, Color(c.r, c.g, c.b, 1.0))
_batten_line.set_instance_custom_data(i, Color(b.strobe, b.chase, b.chase_amount, 0))
_batten_line.set_instance_color(i, Color.BLACK)
_batten_line.set_instance_custom_data(i, Color(b.strobe, 0.0, 0.0, 0.0))
for pixel in Batten.PIXELS:
var pixel_color: Color = b.pixel_colors[pixel] * b.pixel_levels[pixel] * b.level
_batten_pixels.multimesh.set_instance_color(i * Batten.PIXELS + pixel, Color(pixel_color.r, pixel_color.g, pixel_color.b, 1.0))
for o in _strobe_lights:
o.visible = strobe > 0.02
o.light_energy = 16.0 * strobe
Expand Down
27 changes: 27 additions & 0 deletions scripts/show/bar_pixels.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class_name BarPixels
extends RefCounted
# Twelve independently driven LED cells per batten. Shapes hold for whole phrases.
const COUNT := 12
const PATTERNS := ["solid", "left_half", "right_half", "odd", "even", "centre", "ends", "window"]

static func pattern_for(phrase: int, fixture: int, group: String, gentle: bool) -> String:
var choices := 2 if gentle else PATTERNS.size()
var offset := posmod(fixture + hash(group), choices)
return PATTERNS[posmod(phrase + offset, choices)]

static func levels(pattern: String, phase := 0) -> PackedFloat32Array:
var result := PackedFloat32Array()
result.resize(COUNT)
for i in COUNT:
var on := true
match pattern:
"left_half": on = i < COUNT / 2
"right_half": on = i >= COUNT / 2
"odd": on = i % 2 == 1
"even": on = i % 2 == 0
"centre": on = i in [4, 5, 6, 7]
"ends": on = i in [0, 1, 10, 11]
"window": on = posmod(i - phase, COUNT) < 3
_: pass
result[i] = 1.0 if on else 0.0
return result
1 change: 1 addition & 0 deletions scripts/show/bar_pixels.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c2f1m0vvt8d0d
8 changes: 8 additions & 0 deletions scripts/show/director.gd
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,14 @@ func _direct(dt: float) -> void:
bt.level = _lamp(maxf(maxf(maxf(_bar_level[i], breathe), maxf(run, hit)) * _w.bars, maxf(sig.drop, sig.lift * 0.45)) * master) * float(tune.groups.get("batten", 1.0))
bt.color = mono_colour(_blk_index + _palette * 137) if multi and _mono_block() else pal[i % pal.size()] if multi else col2 if i % 2 else col
if single_colour: bt.color = col
var bar_phrase := floori(sig.phase_clock / 32.0)
var bar_pattern := BarPixels.pattern_for(bar_phrase, i, bt.group, safe)
bt.pixel_levels = BarPixels.levels(bar_pattern, floori(sig.phase_clock / 16.0))
for pixel in Rig.Batten.PIXELS:
var pixel_color := bt.color
if bar_pattern in ["odd", "even"] and not single_colour and not (multi and _mono_block()) and pixel % 2 == 0:
pixel_color = col2 if not multi else pal[(i + 1) % pal.size()]
bt.pixel_colors[pixel] = pixel_color
bt.level *= phrasing.gain(2 if bt.group in ["front", "back"] else 1, bt.mount, dropk)
if safe and not manual.strobe:
var glow: float = (0.04 + sig.mid * 0.08) * (0.8 + 0.2 * sin(sig.phase_clock * TAU / 32.0 + bt.u))
Expand Down
9 changes: 9 additions & 0 deletions shaders/batten_pixel.gdshader
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
shader_type spatial;
render_mode unshaded, shadows_disabled;

uniform float gain = 8.0;

void fragment() {
ALBEDO = COLOR.rgb * COLOR.a;
EMISSION = COLOR.rgb * COLOR.a * gain;
}
1 change: 1 addition & 0 deletions shaders/batten_pixel.gdshader.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://b4gydj4x6q4tb
2 changes: 1 addition & 1 deletion tools/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ if [[ ! -d "$root/.godot" ]]; then
fi
fi
run import "$godot_bin" --headless --path "$root" --editor --import
for check in test_app test_controls test_held_input test_light_desk test_show_checks test_feedback test_choreography test_sequencer test_stage_detail test_crowd_detail test_pa test_haze test_session test_rekordbox test_listen_ear; do
for check in test_app test_controls test_held_input test_light_desk test_show_checks test_feedback test_choreography test_sequencer test_bar_pixels test_stage_detail test_crowd_detail test_pa test_haze test_session test_rekordbox test_listen_ear; do
run "$check" "$godot_bin" --headless --path "$root" --script "tools/$check.gd"
done
if "$test_only"; then
Expand Down
30 changes: 30 additions & 0 deletions tools/test_bar_pixels.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
extends SceneTree

var failures: Array[String] = []

func _initialize() -> void:
for pattern in BarPixels.PATTERNS:
var levels := BarPixels.levels(pattern)
assert(levels.size() == 12, "Every bar pattern must address twelve LED cells")
for level in levels: assert(level == 0.0 or level == 1.0, "Pixel output must stay within its dimmer range")
assert(BarPixels.levels("left_half").count(1.0) == 6, "50/50 bars must light exactly six cells")
assert(BarPixels.levels("right_half").count(1.0) == 6, "Mirrored 50/50 bars must light six cells")
assert(BarPixels.levels("odd").count(1.0) == 6 and BarPixels.levels("even").count(1.0) == 6, "Odd/even bars must alternate six cells")
assert(BarPixels.levels("centre").count(1.0) == 4, "The centre pattern must remain local")
assert(BarPixels.levels("ends").count(1.0) == 4, "The end pattern must remain local")
assert(BarPixels.levels("window", 3).count(1.0) == 3, "A moving pixel window must contain three cells")
assert(BarPixels.pattern_for(9, 4, "back", false) == BarPixels.pattern_for(9, 4, "back", false), "Pattern selection must replay deterministically")
var rig := Rig.new()
root.add_child(rig)
rig.build()
assert(rig._batten_pixels.multimesh.instance_count == rig.battens.size() * 12, "All batten pixels must share one batched draw")
var first: Rig.Batten = rig.battens[0]
first.color = Color.RED
first.pixel_levels = BarPixels.levels("left_half")
first.pixel_colors.fill(Color.RED)
assert(first.pixel_levels[0] == 1.0 and first.pixel_levels[11] == 0.0, "Each cell keeps its own dimmer value")
assert(first.pixel_colors.size() == 12 and first.pixel_colors[0] == Color.RED, "Each cell keeps its own colour value")
assert(rig._batten_pixels.multimesh.use_colors, "The batched pixel mesh must read per-instance colour")
rig.free()
print("bar pixels: twelve per fixture, eight held patterns, independent dimmer cells, one batch")
quit()
1 change: 1 addition & 0 deletions tools/test_bar_pixels.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://ca45lxes3yr1f
Loading