Files
qbz/crates/qbz-qt/cxx/plasma_item.h
T
vicrodh 931cb06d38 feat(qt): ease the visualizers on the pulse, and make Plasma react
TWO FINDINGS, both of which falsified something that was being assumed.

FLUIDITY IS NOT FRAME RATE. Driven at QBZ_PULSE_MS=10 the shell presented ~100
frames/s — qt-batches confirmed it, and the script now prints the pulse it ran
with so two logs cannot be confused — and the bars looked exactly as they had at
30. They would: every published frame was applied as a STEP, so the extra
presents repainted the same discrete values three times over. What the Slint
build does differently is not rate, it is `animate bar-h { 90ms }` — it moves
BETWEEN values.

So the interpolation is back, ON THE PULSE, and it costs nothing. The old
interpolator was expensive because of its private 16 ms Timer, not because it
interpolated: it presented at its own rate, unsynchronised with everything else.
Easing on the pulse presents exactly as often as stepping on the pulse did — one
repaint per period either way, only the value differs. Self-parking survives: a
converged ease writes nothing and schedules no repaint. The price is lag, and it
is the honest one — k = 0.45 is ~83% there in three pulses (~100 ms), matching
the reference's 90 ms, against 46 ms of FFT window and 77 ms of Rust smoothing
already in the chain.

The from/to/progress triple is gone with it, and its consumers moved to `cur`.
Note for the next reader: `SpectrumBand` drove all three of its modes from
`Connections` on those properties, and a Connections handler naming a property
that no longer exists is NOT a compile error — QML warns at runtime and simply
never fires. That is how the whole band went dead at once; grep for
`on<Name>Changed` when removing a property, not only for direct reads.

PLASMA WAS DRIVEN BY THE SLOWEST SIGNALS IN THE PIPELINE while the fast ones
travelled beside it unconnected. `transientAmp`, `bandsLo` and `bandsHi` were
published, parsed and stored, and never handed to the item — the shader declared
them and received zeros — so everything ran off the energy bands, which decay
x0.85 per frame (~473 ms) against the bars' x0.65 (~178 ms).

Worse, `beat` stops being a beat on dense material: it decays x0.88 per tick, so
above ~3 hits/s it never falls far enough to come back as a spike and settles
into a shallow ripple around 0.7. A splat multiplied by it becomes a steady
glow, and the busier the music the flatter the picture — the most frenetic input
produced the least motion. `beatAc` subtracts a slow density floor, so a hit
reads as a hit at any tempo.

The scene itself was rebuilt around the reference the owner filmed (Butterchurn
on a percussion-heavy track). Its recipe is three parts: one thin annulus
injected per frame that the feedback warp marches inward, so the concentric
rings ARE the last N frames of audio laid out in space; retention high enough
for that to stack (this held 0.85, where a trail died in ~15 frames and nothing
ever accumulated); and angular structure from reading the spectrum BY ANGLE.

But a CLOSED ring was wrong, and the owner named why: a perfect inner
circumference reads as a drawn shape, hard-edges the field, and at this
retention successive rings pack into one solid disc — which is what killed the
ambient feel. The annulus is gated by angle now, so a hit paints arcs the
conveyor carries inward, the circle never closes, and the gaps stay open for the
background. Noise bends, frays and thins those arcs so they are made of plasma
rather than of compass.

The worms are back at full strength with tighter cores and a contrast curve — a
raw gaussian is all shoulder and reads as haze — and their orbits now reach past
the arc band instead of being trapped inside it. Colour comes from ONE
album-derived triad, so everything used to be a shade of the same hue; a
Rodrigues hue turn splits it into worms, arcs and an ambient wash that sit apart
whatever album is playing. That wash is what stops the background being void for
the foreground to be a cut-out against. A soft limiter replaces the hard clamp:
at 0.945 retention a clamp pins the middle to white and the structure vanishes.

Release build clean, offscreen smoke green, 421/421.

cc
2026-08-19 13:53:57 -06:00

67 lines
2.7 KiB
C++

// Plasma (immersive shader scene, mode 1) — the QQuickRhiItem behind the
// feedback fluid. Block A2 of the 2026-08-15 immersive-completion contract
// (spec 01 §2.1). Second hand-written C++ item after LineBedItem
// (cxx/linebed_item.h carries the why-C++ rationale: a ShaderEffect cannot
// own a feedback texture pair).
//
// QML properties are MEMBER-only (no NOTIFY): the QML wrapper
// (PlasmaScene.qml) writes them on the pulse edge and calls update()
// itself — the same cadence contract as LineBedItem.
//
// Registered as QML type `PlasmaItem` in `com.blitzfc.qbz` 1.0 (see the
// .cpp).
#pragma once
#include <QtGui/QColor>
#include <QtGui/QVector4D>
#include <QtQuick/QQuickRhiItem>
class PlasmaItem : public QQuickRhiItem
{
Q_OBJECT
Q_PROPERTY(float time MEMBER m_time)
Q_PROPERTY(float beat MEMBER m_beat)
Q_PROPERTY(float level MEMBER m_level)
Q_PROPERTY(float levelSmooth MEMBER m_levelSmooth)
Q_PROPERTY(QVector4D energyLo MEMBER m_energyLo)
Q_PROPERTY(QVector4D energyHi MEMBER m_energyHi)
// THE FAST HALF OF THE PACK, which used to arrive zeroed.
//
// `plasma.frag` has always declared `transientAmp`, `bandsLo` and
// `bandsHi` in its std140 block and the pack has always published them —
// `ShaderSceneLayer` even parses and stores all three. They simply never
// reached this item, so the shader read zeros and ran entirely off the
// ENERGY bands, which decay x0.85 per frame (~473 ms to a tenth) against
// the bars' x0.65 (~178 ms). Plasma was being driven by the slowest
// signals in the pipeline while the fast ones travelled beside them
// unconnected.
Q_PROPERTY(float transientAmp MEMBER m_transientAmp)
Q_PROPERTY(QVector4D bandsLo MEMBER m_bandsLo)
Q_PROPERTY(QVector4D bandsHi MEMBER m_bandsHi)
Q_PROPERTY(QColor primary MEMBER m_primary)
Q_PROPERTY(QColor secondary MEMBER m_secondary)
Q_PROPERTY(QColor accent MEMBER m_accent)
public:
explicit PlasmaItem(QQuickItem *parent = nullptr) : QQuickRhiItem(parent) {}
QQuickRhiItemRenderer *createRenderer() override;
// Member snapshots the renderer reads in synchronize(). Defaults are the
// Slint palette (shader_underlay.rs:121-129) so a pre-art track still
// looks right.
float m_time = 0.0f;
float m_beat = 0.0f;
float m_level = 0.0f;
float m_levelSmooth = 0.0f;
QVector4D m_energyLo{ 0.0f, 0.0f, 0.0f, 0.0f };
QVector4D m_energyHi{ 0.0f, 0.0f, 0.0f, 0.0f };
float m_transientAmp = 0.0f;
QVector4D m_bandsLo{ 0.0f, 0.0f, 0.0f, 0.0f };
QVector4D m_bandsHi{ 0.0f, 0.0f, 0.0f, 0.0f };
QColor m_primary{ 0, 220, 200 };
QColor m_secondary{ 150, 50, 255 };
QColor m_accent{ 63, 217, 200 };
};