diff --git a/AGENTS.md b/AGENTS.md index 4be7875..ae8ed7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c4c031f..b96683a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/sv-recovery-plan.md b/docs/sv-recovery-plan.md index fc5bc3b..07e5a5e 100644 --- a/docs/sv-recovery-plan.md +++ b/docs/sv-recovery-plan.md @@ -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 @@ -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. diff --git a/project.godot b/project.godot index 86ed77a..a66d5f0 100644 --- a/project.godot +++ b/project.godot @@ -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") diff --git a/scripts/rig.gd b/scripts/rig.gd index 4d444af..94faa4b 100644 --- a/scripts/rig.gd +++ b/scripts/rig.gd @@ -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 @@ -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] = [] @@ -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] = [] @@ -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: @@ -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 @@ -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 diff --git a/scripts/show/bar_pixels.gd b/scripts/show/bar_pixels.gd new file mode 100644 index 0000000..91e63dd --- /dev/null +++ b/scripts/show/bar_pixels.gd @@ -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 diff --git a/scripts/show/bar_pixels.gd.uid b/scripts/show/bar_pixels.gd.uid new file mode 100644 index 0000000..e3f34f7 --- /dev/null +++ b/scripts/show/bar_pixels.gd.uid @@ -0,0 +1 @@ +uid://c2f1m0vvt8d0d diff --git a/scripts/show/director.gd b/scripts/show/director.gd index 4270586..f8daed4 100644 --- a/scripts/show/director.gd +++ b/scripts/show/director.gd @@ -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)) diff --git a/shaders/batten_pixel.gdshader b/shaders/batten_pixel.gdshader new file mode 100644 index 0000000..a031149 --- /dev/null +++ b/shaders/batten_pixel.gdshader @@ -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; +} diff --git a/shaders/batten_pixel.gdshader.uid b/shaders/batten_pixel.gdshader.uid new file mode 100644 index 0000000..0af84d9 --- /dev/null +++ b/shaders/batten_pixel.gdshader.uid @@ -0,0 +1 @@ +uid://b4gydj4x6q4tb diff --git a/tools/build.sh b/tools/build.sh index 2c2df4c..80f9923 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -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 diff --git a/tools/test_bar_pixels.gd b/tools/test_bar_pixels.gd new file mode 100644 index 0000000..9fedfd0 --- /dev/null +++ b/tools/test_bar_pixels.gd @@ -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() diff --git a/tools/test_bar_pixels.gd.uid b/tools/test_bar_pixels.gd.uid new file mode 100644 index 0000000..c900e17 --- /dev/null +++ b/tools/test_bar_pixels.gd.uid @@ -0,0 +1 @@ +uid://ca45lxes3yr1f