LUME x Duncan Fewkes: Complete Implementation Handoff for Codex
LUME is a real-time depth-camera visualization bar. Physical enclosure (500x120x85mm ASA 3D-printed shell) houses an Orbbec Femto Mega depth camera (640x576 @ 30fps), UMA-8 USB microphone array, and a GMKtec K11 mini-PC (Ryzen 9 8945HS, Radeon 780M ~9 TFLOPS, 32GB DDR5, 1TB NVMe). The K11 captures depth + audio, streams over UDP to Unity, which renders interactive particle/fluid visuals on a 1920x440 IPS bar display (60Hz, 500 nits, mini HDMI + USB-C) mounted flush on top of the bar shell.
Full Public Reader
LUME x Duncan Fewkes: Complete Implementation Handoff for Codex
Part 1: What LUME Is
LUME is a real-time depth-camera visualization bar. Physical enclosure (500x120x85mm ASA 3D-printed shell) houses an Orbbec Femto Mega depth camera (640x576 @ 30fps), UMA-8 USB microphone array, and a GMKtec K11 mini-PC (Ryzen 9 8945HS, Radeon 780M ~9 TFLOPS, 32GB DDR5, 1TB NVMe). The K11 captures depth + audio, streams over UDP to Unity, which renders interactive particle/fluid visuals on a 1920x440 IPS bar display (60Hz, 500 nits, mini HDMI + USB-C) mounted flush on top of the bar shell.
The visual aesthetic is directly inspired by Duncan Fewkes (@duncan.fewkes on Instagram), who builds similar installations commercially under the brand HOLOVIS. His target hardware is an A6000 at 60fps. We have 83 of his reels archived (spanning 2025-10-20 to 2026-04-25) with 69 Gemini visual analyses on disk. This document specifies exactly what to build from that corpus, with concrete numeric values for every preset.
Working directory: `Desktop/lume-commerce/software/demo/unity/lume_pcloud/`
---
Part 2: What's Already Built (17 Components, 3 Compute Shaders, 5 Wire Formats)
All source files at `Desktop/lume-commerce/software/demo/unity/lume_pcloud/Assets/`
2A: Core Pipeline Components
| File | Exec Order | What It Does | Key Globals Published |
|---|---|---|---|
| `Scripts/LumeDisplayController.cs` | -100 | Auto-detects 1920x440 bar display in Display.displays[]. Sets camera to 25 deg vertical FOV (gives ~100 deg horizontal at 4.36:1 aspect). Near-black background (0.01, 0.01, 0.02). Two modes: Production (K11 primary, fullscreen) vs Development (Mac4 secondary, windowed preview) | None |
| `Scripts/LumeUdpReceiver.cs` | default | Binds UDP :9700. Dispatches by 4-byte magic: LUME (0x4C554D45) = cloud points, LUMD (0x4C554D44) = raw u16 depth, LUMF (0x4C554D46) = audio FFT, LUMM (0x4C554D4D) = mocopi skeleton, LUMC (0x4C554D43) = reserved | None (feeds other components) |
| `Scripts/LumeDepthReprojector.cs` | default | GPU compute shader dispatch: u16 raw depth buffer to world-space float4 positions via pinhole unproject. Exposes DepthRawBuffer, W, H, Scale, FrameCounter for downstream consumers | `_LumeWorldInv` (Matrix4x4), `_LumeIntrinsics` (Vector4: fx,fy,cx,cy), `_LumeDepthDims` (Vector4: w,h,scale,0) |
| `Scripts/LumePointRenderer.cs` | default | GPU instanced billboard renderer. Uses GraphicsBuffer (NOT ComputeBuffer -- Unity 6 VFX Graph requires GraphicsBuffer). Exposes `LiveCount` property for overlay/VFX consumers to know how many valid points exist. Supports Cloud mode (pre-projected) and Depth mode (GPU-reprojected) | `PositionsBuffer`, `ColorsBuffer` via Shader.SetGlobalBuffer |
| `Scripts/LumeOpticalFlow.cs` | 0 | Two-kernel compute: CSFrameDiff (8x8 atomic accumulation for scalar motion magnitude) + CSLucasKanade (5x5 dense optical flow on linearized depth). Allocates RG16F flow render texture at depth resolution. AsyncGPUReadback for CPU-side scalar. EMA smoothing + decay-on-stall | `_FlowField` (Texture2D RG16F, bilinear clamp), `_InnerSpread` (float, 0-1 scalar motion magnitude), `_FlowLkActive` (float, 0 or 1 gate) |
| `Scripts/LumeFluidSim.cs` | 100 | 2D Eulerian fluid simulation, 256x256 grid. Keijiro StableFluids-style. Kernels: advection, Jacobi pressure solve (20 iterations), curl-noise injection. Reads `_FlowField` RG16F as velocity source. Audio turbulence from `_AudioLevels`. Delta-time multiplied by `_LumeTimeScale` for SuperHot coupling. Has `ApplyPreset(LumeFluidSimPreset)` public API. Three static factory presets: Calm, Reactive, Storm | `_FluidDensity` (Texture2D, global) |
| `Scripts/LumeMotionGate.cs` | 150 | SuperHot mode from Duncan's E568. Reads `_InnerSpread` global, applies EMA-smoothed power-curve remap to [minScale=0.10, maxScale=1.00]. `applyToUnityTimeScale` toggle OFF by default (audio-safe -- don't freeze AudioSource). powerCurve=2.0, smoothing=0.05 | `_LumeTimeScale` (float, 0.1-1.0) |
| `Scripts/LumeAudioFftReceiver.cs` | 200 | LUMF UDP receiver bound to :9701. Parses 84-byte fixed datagram: magic (4B), timestamp (8B), RMS (4B), spectral centroid (4B), onset flag (1B), pad (3B), 4 EQ bands (16B: bass/low-mid/high-mid/treble as float32), 8 MFCC coefficients (32B), reserved (12B). Overrides `_AudioLevels`/`_OutlineFlash`/`_InnerSpread` when fresh, yields silently when stale. Beat-spike + transient-edge boost | `_AudioLevels` (Vector4: rms, centroid, onset, reserved), `_OutlineFlash` (float), overrides `_InnerSpread` |
| `Scripts/LumeAudioReactor.cs` | 0 | Wave 1 fallback: uses AudioSource.GetSpectrumData when LUMF UDP is absent. Public setters: SetInnerSpread(), etc. DO NOT MODIFY this class (parallel session contract) | Same globals as LumeAudioFftReceiver but lower fidelity |
| `Scripts/LumeTransientForcePusher.cs` | 220 | Third audio channel per Duncan's E535. [RequireComponent(VisualEffect)]. Rising-edge onset detection from LumeAudioFftReceiver.TryGetLatest(). Fires `SendEvent("OnTransient")` on VFX Graph + writes decaying Vector3 `ImpulseForce` parameter. Default: magnitude=6, decay=8/s = 125ms half-life. Fallback: `_OutlineFlash` > 1.2 threshold when LUMF absent | Writes to VFX Graph params, not global shader uniforms |
| `Scripts/LumeMocopiReceiver.cs` | 205 | LUMM UDP receiver bound to :9702. Parses 772-byte skeleton datagram: magic (4B), timestamp (8B), 27 bones x (quaternion 16B + position 12B) = 756B. Smoothed via `TryGetSmoothed()` | None (feeds LumeMocopiAnimator) |
| `Scripts/LumeMocopiAnimator.cs` | 210 | Maps 27 mocopi bones to Unity Humanoid avatar rig. Applies transforms each LateUpdate. [DefaultExecutionOrder(210)] ensures it runs after receiver (205) | None |
| `Scripts/LumeSkeletonFluidInjector.cs` | default | Reads bone positions/velocities from LumeMocopiAnimator, injects fluid density at bone positions and velocity at bone velocities into LumeFluidSim | None |
| `Scripts/LumeVfxRuntimeBridge.cs` | 210 | [RequireComponent(VisualEffect)]. Pushes ALL relevant globals into VFX Graph VisualEffect instance every LateUpdate. Caches Has* presence flags on enable. Re-binds Positions buffer only on renderer reallocation. Pushes: _AudioLevels, _OutlineFlash, _InnerSpread, _FlowField, PositionsBuffer, ImpulseForce, LumfActive, FluidDensity | None (writes to VFX Graph, not global) |
| `Scripts/LumeCalibrationPanel.cs` | default | F12 IMGUI panel. 26 tunables across 7 component sections. Runtime reflection on [SerializeField] private fields. Per-install JSON persistence at Application.persistentDataPath/lume-calibration.json. Auto-save on hide/disable, load on enable. Window ID 8120. Draggable | None |
| `Scripts/LumeProceduralTunnel.cs` | default | Generates 6144-point cylindrical helix. Pins Cloud mode. Used for demo backdrop | None |
2B: Preset Infrastructure (Classes Built, Zero .asset Instances)
| File | CreateAssetMenu Path | Fields |
|---|---|---|
| `Scripts/Vfx/LumeVfxPreset.cs` | "LUME/VFX Preset" | `intensity` float [Range(0,3)] default 1, `density` float [Range(0,1)] default 0.5, `count` int [Range(100,200000)] default 30000, `gravity` float [Range(-20,20)] default 0, `initialVelocity` float [Range(0,50)] default 1, `lifetime` float [Range(0.1,30)] default 3, `spawnRate` float [Range(1,5000)] default 500, `baseColor` Color default (0.30,0.35,0.85,1.0) blue-purple, `particleTex` Texture2D nullable, `materialOverride` Material nullable |
| `Scripts/Vfx/LumeFluidSimPreset.cs` | "LUME/Fluid Sim Preset" | `presetName` string, `viscosity` float [Range(0.00001,0.01)], `diffusion` float [Range(0.000001,0.001)], `dt` float [Range(0.001,0.05)], `flowInjectionScale` float [Range(0,5)], `audioTurbulenceScale` float [Range(0,3)]. Static factories: `Calm()` (visc=0.005, diff=0.0001, flow=0.3, audio=0.1), `Reactive()` (visc=0.00005, diff=0.000005, flow=2.0, audio=1.5), `Storm()` (visc=0.00001, diff=0.000001, flow=4.0, audio=2.5, dt=0.025) |
| `Scripts/Vfx/LumeAvatarPreset.cs` | "LUME/Avatar Preset" | Read the file for exact fields -- skeleton/avatar appearance params |
| `Scripts/Vfx/LumeLightingPreset.cs` | "LUME/Lighting Preset" | Read the file for exact fields -- key/fill/rim light setup |
| `Scripts/Vfx/LumeEnvironmentPreset.cs` | "LUME/Environment Preset" | Read the file for exact fields -- skybox/fog/background |
2C: Editor/Operator Panel (scaffold, ugly, empty lists)
File: `Scripts/Vfx/LumeVfxEditor.cs` (~13KB, fully featured scaffold)
- Toggle: backtick key (CHANGE TO F1)
- Hidden by default
- Procedural Canvas built in Awake(): left-side black panel, 340px wide
- 6 Dropdown slots: VFX01, VFX02, Avatar, Lighting, Environment, FluidSim
- Each backed by `List<ScriptableObject>` -- these lists are PUBLIC + SERIALIZED but currently empty
- Auto-cycle toggle: switches VFX01 every N bars when `LumeRuntime.currentBpm > 0`
- `ApplyAll()` dispatches to per-category apply methods:
- VFX: pushes `baseColor` via MaterialPropertyBlock to LumePointRenderer
- FluidSim: sets `LumeRuntime.activeFluidPresetId` (LumeFluidSim reads this)
- Avatar: swaps prefab on "Avatar" GameObject
- Lighting: sets Key/Fill/Rim light colors + intensities
- Environment: sets skybox material + fog color/density
- Load/Save scene buttons (stubbed for Wave 2)
2D: Compute Shaders
| File | Kernels | Grid | Purpose |
|---|---|---|---|
| `Shaders/LumeFluidSim.compute` | Advect, Project (Jacobi x20), InjectFlow, InjectAudioTurbulence | 256x256 | 2D Eulerian fluid. Reads _FlowField, _AudioLevels. Writes density/velocity RTs |
| `Shaders/LumeOpticalFlow.compute` | CSFrameDiff (8x8 atomic accum), CSCopyDepth (memcpy), CSLucasKanade (5x5 dense LK) | depth resolution | Frame-diff scalar + vector flow field. Outputs RG16F flow texture |
| `Shaders/LumeDepthReproject.compute` | CSReproject (8x8) | depth resolution | u16 raw depth buffer to world-space float4 via pinhole intrinsics |
2E: Vertex/Fragment Shaders
| File | Purpose |
|---|---|
| `Shaders/LumePointInstanced.shader` | Instanced billboard renderer. Samples `_FlowField` for vector-localized inner spread + hue bias by atan2(flow). `_FlowLkActive` gates fallback. Duncan-lite dissolve/twinkle pass added |
2F: Python Publishers
| File | Port | Magic | Datagram Size | Purpose |
|---|---|---|---|---|
| `software/demo/pointcloud_pub.py` | :9700 | LUME (cloud) or LUMD (raw depth) | LUME: 16B header + N12B points. LUMD: 40B header + N2B pixels | Depth publisher. `--raw-depth` flag for Wave 2+. `--synthetic` for test data. `--host` and `--port` configurable |
| `software/demo/audio_pub.py` | :9701 | LUMF | 84 bytes fixed | Audio FFT publisher. `--synthetic` for test. `--mic` for real audio. Hann FFT, flux onset detector. 60fps |
| `software/demo/mocopi_bridge.py` | :9702 | LUMM | 772 bytes | Sony mocopi skeleton bridge. Reads binary :12351 from XYN Motion Studio, repacks as LUMM |
| `software/demo/lume_packet_inspector.py` | :9700 + :9701 | all | varies | Debug tool. Binds both ports, prints rolling diagnostics, drop estimates, intrinsics drift |
2G: 17 Global Shader Uniforms
| Uniform | Type | Producer | Consumers |
|---|---|---|---|
| `_AudioLevels` | Vector4 (rms, centroid, onset, reserved) | LumeAudioFftReceiver or LumeAudioReactor | Shaders, VFX Graph |
| `_OutlineFlash` | float | LumeAudioFftReceiver | Shaders (outline brightness) |
| `_InnerSpread` | float (0-1) | LumeOpticalFlow or LumeAudioFftReceiver | LumeMotionGate, shaders |
| `_FlowField` | Texture2D RG16F | LumeOpticalFlow | LumeFluidSim, LumePointInstanced shader |
| `_FlowLkActive` | float (0 or 1) | LumeOpticalFlow | LumePointInstanced shader (fallback gate) |
| `_FluidDensity` | Texture2D | LumeFluidSim | Shaders, VFX Graph |
| `_LumeTimeScale` | float (0.1-1.0) | LumeMotionGate | LumeFluidSim (dt multiplier) |
| `_LumeWorldInv` | Matrix4x4 | LumeDepthReprojector | Shaders (world to depth UV projection) |
| `_LumeIntrinsics` | Vector4 (fx,fy,cx,cy) | LumeDepthReprojector | Shaders (pinhole model) |
| `_LumeDepthDims` | Vector4 (w,h,scale,0) | LumeDepthReprojector | Shaders (buffer dimensions) |
| `PositionsBuffer` | GraphicsBuffer | LumePointRenderer | VFX Graph, LumeFlowParticleOverlay |
| `ColorsBuffer` | GraphicsBuffer | LumePointRenderer | VFX Graph |
2H: Test Surface
From `Desktop/lume-commerce/software/demo/`:
python3 -m pytest tests/ -q # 33 fast tests (sub-second)
python3 -m pytest tests/ -m slow -v # 4 integration tests (~5s)
LUME_RUN_SLOW=1 python3 -m pytest tests/ # all 37Tests cover: struct sizes, magic round-trip, FFT bounds, onset edge cases, end-to-end loopback, LUMF/LUMD/LUME golden wire bytes, packet inspector e2e, integration streams.
---
Part 3: Duncan's 7-Layer Visual System
### Layer 1: Depth Point Cloud -- SHIPPED
Femto Mega captures at 640x576 @ 30fps. Raw u16 depth reprojected to world-space float4 via GPU compute. Rendered as instanced billboards. 92,160 live points per frame (640*576/4 subsampled). GraphicsBuffer path ready for VFX Graph.
### Layer 2: Optical Flow -- SHIPPED
CRITICAL RULE: Depth MUST be linearized BEFORE optical flow. Z-near bias in raw depth kills far-field motion detection (Duncan E460). Our implementation does this correctly in CSCopyDepth.
Lucas-Kanade 5x5 dense flow produces RG16F velocity texture. Scalar magnitude (frame-diff) published as `_InnerSpread`. Cost: ~0.5ms GPU.
### Layer 3: Fluid Simulation -- SHIPPED (needs 6 more presets)
2D Eulerian, 256x256 grid. Advection + Jacobi pressure solve (20 iterations). Flow field injects velocity. Audio injects turbulence. Publishes `_FluidDensity`. Time-step multiplied by `_LumeTimeScale` (SuperHot coupling). `ApplyPreset()` public API exists.
Currently ships 3 presets. Duncan has 6 named presets (see Part 5B).
On the 4.36:1 bar display, the 256x256 square grid sampled in screen space will appear horizontally stretched. This is a known visual artifact -- tune after seeing it on the actual display.
### Layer 4: VFX Graph Particles -- NOT SHIPPED
The particle swarm that makes it look like Duncan. This is the single biggest visual gap. The runtime bridge is fully wired (pushes 8 params + events into VisualEffect). The `.vfx` node graph needs authoring in Unity's VFX Graph editor (GUI-only task, cannot be done from CLI).
Full node graph recipe is at `Assets/VFX/README-LumeFlowParticles.md` with step-by-step instructions for Spawn, Initialize, Update, Output blocks, 7 exposed parameters, and the OnTransient event.
Current workaround: `Assets/Scripts/LumeFlowParticleOverlay.cs` + `Assets/Shaders/LumeFlowParticleOverlay.shader` provide a code-driven 26K particle overlay. This is explicitly labeled TEMPORARY. Do not delete it until the real .vfx is authored and verified.
Layer 5: Three-Channel Audio Reactivity -- SHIPPED
Duncan's canonical three-channel model (defined in E579, refined across E535/E512/E589):
| Channel | Name | What It Drives | Signal Source | Visual Purpose | Our Implementation |
|---|---|---|---|---|---|
| 1 | Outline (form) | Fresnel power multiplied by RMS. Flash = step(beat_threshold, rms). Emissive bright outline around body silhouette | RMS + 4 EQ bands from LUMF | "Where you are" -- shows the user their position | `_OutlineFlash` global float + `_AudioLevels.x` (RMS) |
| 2 | Inner (action) | Motion magnitude from optical flow on linearised depth modulates inverse fresnel. High motion = color spreads inward from silhouette edge | Optical flow scalar from CSFrameDiff | "What you're doing" -- shows the user their movement | `_InnerSpread` global float + `_FlowField` texture |
| 3 | Impulse (kick) | AddForce event in VFX Graph fires on transient onset ONLY (not smoothed RMS). Velocity kick applied to existing particles. Rising-edge detection prevents re-fire | Audio transient detector (peak/onset flag in LUMF byte 20) | "When it hits" -- punctuates the beat | `LumeTransientForcePusher.cs`: SendEvent("OnTransient") + ImpulseForce param |
### Layer 6: SuperHot Motion Gate -- SHIPPED
From Duncan's E568. Sim time scales with body motion: move fast and visuals are alive, stand still and they freeze. His approach: "utility script hooks user motion to fluid sim timescale with a fairly quick smoothing/falloff." Our implementation: `LumeMotionGate.cs`, power curve from `_InnerSpread` to `_LumeTimeScale`.
Default tuning: minScale=0.10, maxScale=1.00, powerCurve=2.0, EMA smoothing=0.05. Tunable via F12 calibration panel.
### Layer 7: Mocopi Skeleton -- SHIPPED (code), NOT TESTED (hardware)
27-bone Sony mocopi skeleton mapped to Unity Humanoid avatar. Bones inject into fluid sim via LumeSkeletonFluidInjector. Blocked on mocopi hardware pairing with K11. The code path is complete and compiles.
---
Part 4: Duncan's Design Rules (MUST Follow -- Violations Produce Visually Wrong Results)
These come directly from Duncan's own reel captions, Inspector dumps, and self-critiques across 72 reels:
1. Audio drives POSITION directly, never force/velocity. "Audio response boosting position directly (rather than feeding into force levels) makes it nice and snappy without blowing up the fluid sim." -- E512
2. Apply EQ BEFORE auto-gain, or use fixed dynamic range. If you auto-gain first, the EQ bands all compress to the same level and you lose the bass/treble separation that drives the visual layering -- E512
3. Bass = large, low-frequency positional offsets (slow wave bulges). Mid+High = small, high-frequency timer-cycled offsets -- E512
4. Three separate audio channels, not one mixed signal. Outline, Inner, and Impulse are independent visual responses. Mixing them into one channel collapses the visual vocabulary -- E579
5. Particles MUST NOT cast shadows. Self-collision/self-shadowing on 100K+ particles is a visual mess and a perf cliff -- E488
6. Pure gravity + collision = visually dead. ALWAYS layer fluid-sim kick + turbulence on top of gravity. "Without the fluid sim the gravity drop just looks like sand falling." -- E488, E598
7. ShortThrow fluid preset for full-screen + multi-person installations. Shorter velocity propagation = better isolation between multiple people in the frame -- E512
8. Every preset enum's last entry MUST be None (off state). This allows cleanly disabling any layer -- E516
9. Coupled sim systems: pick ONE direction or feedback loop goes crazy. "Making the motion of the Wobbly Lad input to fluid sim meant I had to disable the fluid sim force contribution to the motion, otherwise it would feedback and he'd go apeshit." -- E549, E556
10. Per-install calibration is a hard product requirement. "Need to rebalance threshold etc values for the actual usage distance -- probably easiest to expose in settings and debug menu to allow for tweak values for each install individually." -- E568
11. holdDuration = 0.25s for clone snapshots, then ease-in to gravity via AnimationCurve (not constant) -- E491
12. Speed-to-brightness is the "twinkle" parameter. Power curve + threshold, tune by ear. `brightness = pow(speed / maxSpeed, k)` where k = 2-4 -- E490
13. Audio-transient response must read clearly without losing form coherence. "Needs more definite/large change in motion, but without it going crazy and losing coherence." -- E535
14. Depth linearization BEFORE optical flow. Z-near bias in raw depth kills far-field motion detection -- E460
15. Emission Scale range is 0.00 to 0.05, NOT 0 to 1. The entire useful visual range for emissive particles/surfaces is in that tiny band. Going above 0.05 blows out to white -- E593
16. One rotational symmetry is enough; don't compound symmetries. 16-clone radial is the max, and even that can look busy -- E479
17. "Don't quit at the first disappointing test" -- E488. Iterate parameters before declaring a technique failed.
18. The negative-space look (Lighting=None + emissive depth) is its own valid aesthetic. Not a bug, it's EmissiveOnly mode at 160-170fps -- E516
---
Part 5: Concrete Preset Values to Generate
5A: VFX Presets (LumeVfxPreset) -- 13 presets
Create at `Assets/Resources/LumePresets/Vfx/`. Naming: `e###-slug.asset`
Write an editor script at `Assets/Editor/LumePresetGenerator.cs` with menu item `Lume > Generate Duncan Presets` that creates all assets programmatically via:
var preset = ScriptableObject.CreateInstance<LumeVfxPreset>();
// set fields
AssetDatabase.CreateAsset(preset, "Assets/Resources/LumePresets/Vfx/e598-dissolving-clones.asset");---
e598-dissolving-clones.asset
intensity = 2.5f;
density = 0.6f;
count = 150000;
gravity = -2.0f;
initialVelocity = 2.0f;
lifetime = 8.0f;
spawnRate = 3000f;
baseColor = new Color(0.70f, 0.30f, 0.90f, 0.80f); // purple dissolveSource: E598 (4,081 likes, highest engagement reel). Frozen poses captured on beat-trigger (transient channel), layered with live mesh. Gravity eases in over ~2.5s via AnimationCurve (not constant drop). holdDuration = 0.25s for the snapshot. CRITICAL: always layer fluid-sim kick + turbulence on top of gravity or it looks like sand falling. Speed-to-brightness mapping on dissolving particles. Dual-channel palette: red (frozen snapshot layer) + purple/orange (live depth mesh). Particles MUST NOT cast shadows.
---
e568-superhot-cubes.asset
intensity = 2.0f;
density = 0.5f;
count = 150000;
gravity = 0.0f;
initialVelocity = 0.5f;
lifetime = 30.0f; // near-infinite in frozen-buffer mode
spawnRate = 500f;
baseColor = new Color(0.40f, 0.60f, 0.95f, 1.0f); // cool blue baseSource: E568. SuperHot motion-gated timescale. LOD variant uses cube-shaped particles (DepthCubes enum). Per-particle hue cycling: `hue = baseHue + speedScalar * shiftAmount` -- fast particles get hue-shifted, slow stay near base. Frozen velocity buffer mode: velocity field frozen while particles continue flowing through it = laminar streams along etched-in paths. Duncan's Motion Settings for this mode: Optical Flow Imagine X/Y (3-9), Drag Max Intensity X/Y (0.0-0.1+), Drag Scale ~0.006, Image Threshold (0-255, 50 = more concentrated), Image Invert (0/1), Motion Blur (0/1). Per-install calibration is mandatory.
---
e560-frozen-sim-buffers.asset
intensity = 1.5f;
density = 0.4f;
count = 100000;
gravity = 0.0f;
initialVelocity = 1.0f;
lifetime = 30.0f; // infinite (frozen field)
spawnRate = 200f;
baseColor = new Color(0.50f, 0.50f, 0.80f, 0.90f); // inherits palette when frozenSource: E560. "Accidental art" -- discovered when stopping Unity editor playback. The fluid sim stops updating but the density/velocity RenderTextures persist because they exist in the asset database. Particles keep flowing through the frozen velocity field, creating laminar streams along etched-in paths. "Frozen forms in the motion paths that you don't get while the sim is updating." To implement: add a `FreezeVelocityField` bool toggle to LumeFluidSim that stops dispatching the compute shader but keeps the RTs alive.
---
e549-kelp-seagrass.asset
intensity = 1.0f;
density = 0.7f;
count = 40000; // strip/ribbon particles, not quads
gravity = -0.3f;
initialVelocity = 0.5f;
lifetime = 15.0f; // continuous sway
spawnRate = 100f;
baseColor = new Color(0.15f, 0.65f, 0.40f, 0.85f); // green/teal organicSource: E549. VFX Graph particle strips (ribbon particles) with per-strip stiffness and parent-to-child force propagation along the chain. Audio coupling: FFT bin index mapped to Y position on strip -- bass drives bottom leaves, treble drives top leaves. Audio levels must be tuned LOW or the kelp whips around wildly. Fish boids variant: audio params on cruising speed + separation, also tuned low so fish don't zip away. CRITICAL: unidirectional fluid coupling ONLY (body motion -> fluid sim, but NOT fluid sim -> body motion, or it feedback-loops and "goes apeshit"). Self-shadowed HDRP lit material (we'll approximate in URP). Duncan's self-critique: "Pretty huge fail when I finally check some footage of real kelp and realise mine looks bugger all like actual kelp."
---
e545-pinscreen.asset
intensity = 1.5f;
density = 1.0f; // full grid coverage
count = 10000; // instanced cube grid ~100x100
gravity = 0.0f;
initialVelocity = 0.0f; // static grid, Z pushed by depth
lifetime = 30.0f;
spawnRate = 0f; // permanent mesh grid, not ephemeral particles
baseColor = new Color(0.90f, 0.90f, 0.90f, 1.0f); // white plastic frontSource: E545. Not traditional particles -- instanced cube grid locked in XY positions. Z-position pushed by two sources: (1) reprojected depth camera pushes cubes FORWARD, (2) fluid sim dye buffer pushes cubes BACKWARD from mid-plane. Depth camera overrides fluid push (depth > dye). The dye scalar is multi-purpose: drives BOTH geometric z-displacement AND HDRP thickness (transmission/absorption colour). Two-tone material: front-facing = white plastic, rear-facing = gold accent. Transparent variant uses HDRP thickness for transmission color. Less "swarmy", more "architectural." A fundamentally different rendering approach than the other presets.
---
e535-particle-motion-kick.asset
intensity = 2.5f;
density = 0.5f;
count = 120000;
gravity = -0.5f;
initialVelocity = 3.0f;
lifetime = 4.0f;
spawnRate = 2000f;
baseColor = new Color(0.85f, 0.50f, 0.20f, 1.0f); // warm orange sparksSource: E535. THE impulse channel preset. This is where Duncan first tested the third audio channel. forceMagnitude=6, decayPerSec=8 (125ms half-life). Rising-edge detection prevents re-fire (only fires on the onset's leading edge, not while the flag stays high). Impulse bypasses fluid sim entirely -- fires directly into VFX Graph via SendEvent("OnTransient") + ImpulseForce param. "WIP testing quick 'overamp' method for audio transients to add a kick to the particle motion. Not very noticeable so needs more definite/large change in motion, but without it going crazy and losing coherence."
---
e512-swirly-particles.asset
intensity = 1.5f;
density = 0.5f;
count = 150000;
gravity = 0.0f;
initialVelocity = 1.5f;
lifetime = 5.0f;
spawnRate = 1500f;
baseColor = new Color(0.45f, 0.35f, 0.85f, 0.90f); // hue-cycling purple baseSource: E512. THE audio coupling rules reference reel. Floor-spawn vortex particles. 3-layer hue stack (all active simultaneously): (1) global slow cycle = 1 full revolution per 30 seconds applied to ALL particles, (2) per-particle spawn color from RGB luma at body position, (3) per-particle speed-driven shift each frame. Two noise axis modes: `XZ_Twist_Contained` (gentle, contained swirls) vs `Y_Lift_With_LowFreqBulges` (overflowing upward bulges). Speed-to-brightness = "twinkle" (power curve + threshold, tune by ear). ShortThrow fluid preset works better for this at full-screen scale + multiple people.
---
e589-dat-funk.asset
intensity = 2.0f;
density = 0.6f;
count = 120000;
gravity = -0.3f;
initialVelocity = 2.0f;
lifetime = 3.5f;
spawnRate = 2500f;
baseColor = new Color(0.80f, 0.25f, 0.60f, 1.0f); // magenta-pink funkSource: E589. Dance-reactive mode. Surface shader reacts to BOTH audio AND motion simultaneously: brightness flashes on beat (audio channel 1), fresnel pulled down where motion occurs = color spreads from silhouette edge inward (channel 2). Full 3-source hue stack active. Size/glow fade by speed. "Music on/off" toggle proven as a real operator feature in E587 -- audio reactivity should be gatable/toggleable per install. "Outline (form) reacts to audio: brightness flashes on beat -- shows you 'where you are.' Inner color (action) reacts to motion: hue/spread driven by motion vectors -- shows you 'what you're doing.'"
---
e593-blobby-guys.asset
intensity = 2.0f;
density = 0.8f;
count = 80000; // source particles for marching cubes
gravity = 0.0f;
initialVelocity = 0.5f;
lifetime = 10.0f;
spawnRate = 500f;
baseColor = new Color(0.10f, 0.80f, 0.90f, 0.70f); // teal/slimeSource: E593. Marching cubes metaball morph. Source particles feed MC mesh reconstruction -- output is continuous isosurface, not discrete particles. Material variants: slime, water, pink energy, oily, chrome/mercury. Emission HDR colors: bright blue RGB(109,255,255) = (0.43,1.0,1.0), green RGB(133,255,0) = (0.52,1.0,0.0), magenta RGB(255,0,255) = (1.0,0.0,1.0). CRITICAL: Emission Scale operating range is 0.00-0.05 ONLY (not 0-1). Shader: Custom/Thickness_Apply with IOR=1.33 (water), Translucency=0.003, Fresnel Multiplier=0.0 (OFF by default). Inverse mode (E468): alpha proportional to (1-thickness) = neon-edge look. Twin-mesh nested MC (E427): inner gold-leaf shell + outer glass, same source particles but two different ISO thresholds (_InnerThreshold and _OuterThreshold). Compute-thickness-to-alpha with tight power curve: `pow(thickness, k)` where k=6-10. Bistable: letters/surfaces stay dark then suddenly glow when thickness crosses threshold. NOTE: Marching cubes compute shader is NOT YET BUILT in our pipeline. This preset will only affect color/count until LumeMarchingCubes.compute + .cs are implemented.
---
e544-logo-testing.asset
intensity = 1.5f;
density = 0.5f;
count = 60000;
gravity = 0.0f;
initialVelocity = 0.5f;
lifetime = 8.0f;
spawnRate = 500f;
baseColor = new Color(0.90f, 0.85f, 0.70f, 1.0f); // white/gold volumetricSource: E544. Logo letters as simultaneous: blendshape inflation target, reflection probe hit-target, particle spawn surface, SDF for "punch through" reveals. Wordmark feedback loop (E489): BlendShape inflation driven by FFT bands -> inflation generates motion vectors -> motion vectors injected into fluid sim -> fluid sim pushes nearby particles. The wordmark BECOMES part of the audio-reactivity loop. Uses InvertedObstacle fluid preset: fluid flows INSIDE body silhouette only. ShortThrow for cymatics (E528): particles stuck on letters bouncing internally. HolePunch depth mode: renders silhouette on top of all particles (no z-test) -- hurts depth cues but you never lose track of yourself. "Interactive text displayed and reacts volumetrically to the user's movements." NOTE: Wordmark-as-blendshape requires modeling LUME wordmark as a real Mesh (currently just a Blender deboss in CAD). Deferred until product copy finalized.
---
e606-maximalism.asset
intensity = 3.0f;
density = 0.8f;
count = 200000;
gravity = -1.0f;
initialVelocity = 5.0f;
lifetime = 6.0f;
spawnRate = 5000f;
baseColor = new Color(0.60f, 0.20f, 1.0f, 1.0f); // saturated purpleSource: E606. Maximum everything. High particle count, additive blend, saturated colors, all three audio channels active at high gain, fluid sim at Reactive or Storm. Wide FOV vs Narrow FOV comparison. This is the "showcase" preset.
---
e538-motion-sparks.asset
intensity = 2.5f;
density = 0.3f;
count = 80000;
gravity = 0.0f;
initialVelocity = 8.0f;
lifetime = 1.5f; // short bright bursts
spawnRate = 4000f;
baseColor = new Color(1.0f, 0.85f, 0.30f, 1.0f); // bright gold sparksSource: E535-E538. Sparks spawn only on motion edges (where optical flow magnitude is high). High initial velocity, very short lifetime = brief streaks. Speed-to-brightness mapping makes them flash as they fire outward and dim as they slow. The "fireworks on movement" aesthetic.
---
e530-depth-spawn.asset
intensity = 1.0f;
density = 0.9f;
count = 92160; // match depth frame pixel count
gravity = 0.0f;
initialVelocity = 0.0f;
lifetime = 2.0f;
spawnRate = 46000f; // half population per second
baseColor = new Color(0.30f, 0.35f, 0.85f, 1.0f); // default blue-purpleSource: E530+. The raw depth aesthetic. Particles spawn strictly at depth buffer positions with zero post-spawn motion. Minimal processing. This is the "honest" mode that just shows what the camera sees, re-rendered as particles.
---
5B: Fluid Sim Presets (LumeFluidSimPreset) -- 6 presets
Create at `Assets/Resources/LumePresets/FluidSim/`. Duncan names these verbatim in E534:
default.asset
presetName = "Default";
viscosity = 0.0001f;
diffusion = 0.00001f;
dt = 0.016f;
flowInjectionScale = 1.0f;
audioTurbulenceScale = 0.5f;Duncan: "Nice swishy pressure wave, but does sometimes look too large a motion." General purpose starting point.
---
default-smooth.asset
presetName = "Default_Smooth";
viscosity = 0.0005f;
diffusion = 0.00005f;
dt = 0.016f;
flowInjectionScale = 0.8f;
audioTurbulenceScale = 0.3f;Duncan: Smoothed Default for calmer scenes. Higher viscosity damps oscillations.
---
long-throw.asset
presetName = "LongThrow";
viscosity = 0.00002f;
diffusion = 0.000005f;
dt = 0.016f;
flowInjectionScale = 2.5f;
audioTurbulenceScale = 1.0f;Duncan: "Lot of velocity propagation, tends to thin down over distance, lacks vorticity/swirls." Good for dramatic long-arc gestures.
---
mid-throw.asset
presetName = "MidThrow";
viscosity = 0.00005f;
diffusion = 0.00001f;
dt = 0.016f;
flowInjectionScale = 1.5f;
audioTurbulenceScale = 0.8f;Duncan: "Puts more vorticity back, helps it not drop to ground level." Balanced between reach and visual interest.
---
short-throw.asset
presetName = "ShortThrow";
viscosity = 0.001f;
diffusion = 0.0001f;
dt = 0.016f;
flowInjectionScale = 0.5f;
audioTurbulenceScale = 0.3f;Duncan: "Very local, helps leave trails/particle clones lingering in space." BEST for full-screen scale + multiple people (per E512). Good for cymatics-on-logo (particles bounce internally).
---
inverted-obstacle.asset
presetName = "InvertedObstacle";
viscosity = 0.0001f;
diffusion = 0.00001f;
dt = 0.016f;
flowInjectionScale = 1.0f;
audioTurbulenceScale = 0.5f;Duncan: "Keep fluid flow inside the offscreen reprojected depth render." Body silhouette REPELS fluid instead of injecting velocity. Fluid flows only OUTSIDE the body. Requires modifying LumeFluidSim to invert the depth mask when this preset is active. Good for: logo volumetric reveals, negative-space aesthetics.
---
5C: Lighting Presets (LumeLightingPreset) -- 7 presets
Create at `Assets/Resources/LumePresets/Lighting/`. Read `LumeLightingPreset.cs` first to see exact field names.
| Asset Name | Key Light | Fill Light | Rim Light | Fog | Notes |
|---|---|---|---|---|---|
| `fog-spot-left-right-1.asset` | Left spot, warm | Right spot, cool | None | Volumetric fog enabled | Two opposed spotlights with atmospheric haze |
| `fog-spot-left-right-2.asset` | Left spot, warm, tighter cone | Right spot, cool, tighter cone | None | Volumetric fog | Variant: tighter cone angles for more dramatic falloff |
| `fog-spot-right.asset` | None | None | Right spot, dramatic | Light fog | Single-source Rembrandt lighting |
| `fog-spot-overhead.asset` | Overhead spot, neutral white | None | None | Volumetric | Top-down, stage lighting feel |
| `fog-point-orbit-pulse.asset` | Orbiting point light | None | None | Light fog | Point light orbits scene, intensity pulsed by `_AudioLevels.x` (RMS) |
| `bright-room.asset` | Ambient, high intensity | Fill, high intensity | None | No fog | Even illumination, no drama. "Museum" mode |
| `emissive-only.asset` | None | None | None | No fog | NO scene lights. Particles self-illuminate only. 160-170fps (best perf). The "negative space" look -- confirmed by Duncan as a valid aesthetic, not a bug (E516) |
---
5D: Environment Presets (LumeEnvironmentPreset) -- 4 presets
Create at `Assets/Resources/LumePresets/Environment/`. Read `LumeEnvironmentPreset.cs` first.
| Asset Name | Skybox | Fog Color | Fog Density | Notes |
|---|---|---|---|---|
| `studio-sky-sphere.asset` | Neutral grey studio HDRI | Dark grey | 0.01 | Standard studio look |
| `sunset.asset` | Warm sunset gradient | Warm orange | 0.02 | Golden hour mood |
| `bolivian-salt-flats.asset` | Mirror-ground infinite horizon HDRI | White | 0.005 | Infinite reflective plane |
| `test-room-dark.asset` | Solid black | Black | 0.0 | Near-zero ambient. Particles + emissive only. Best for the bar display (dark surround = OLED-like contrast on IPS) |
---
Part 6: Per-Particle Hue Stack (3 Simultaneous Sources)
This is HOW Duncan does color across ALL modes. All three layers stack additively:
### Layer 1: Global Slow Cycle
One full hue revolution per 30 seconds, applied uniformly to ALL particles.
hue += _Time.y * 0.033; // 1/30 = 0.033 rev/sec### Layer 2: Per-Particle Spawn Color
At spawn time, sample the depth camera's color texture at the spawn position. Convert RGB to luminance. Map luminance to hue offset.
float spawnLuma = dot(spawnColor.rgb, float3(0.299, 0.587, 0.114));
hue += spawnLuma * 0.3;### Layer 3: Per-Particle Speed-Driven Shift
Each frame, particle velocity magnitude shifts the hue. Fast particles diverge from base hue, slow particles stay near it.
float speed = length(velocity);
hue += speed * shiftAmount; // shiftAmount ~0.1-0.3The result: particles share a family palette but each drifts uniquely based on where it spawned and how fast it's currently moving. This creates the organic, living quality that distinguishes Duncan's work from static particle presets.
---
Part 7: Runtime Picker UI Specification
### Current State
`LumeVfxEditor.cs` has a procedural Canvas with 6 dropdowns. Works but ugly. Preset lists are empty. Toggle is backtick (conflicts with Unity console).
### What to Build
Replace with IMGUI panel mirroring `LumeCalibrationPanel.cs` pattern. That file demonstrates the exact approach:
- `GUI.Window(windowId, rect, DrawCallback, title)` with draggable title bar
- `GUILayout.BeginScrollView()` / `EndScrollView()` for scrollable content
- `GUILayout.HorizontalSlider()` for values, `GUILayout.Button()` for actions
- `GUILayout.Label()` with section headers
- JSON persistence via `JsonUtility.ToJson()` + `File.WriteAllText()` / `File.ReadAllText()`
Detailed Spec
| Property | Value |
|---|---|
| Toggle key | `KeyCode.F1` (NOT backtick -- backtick opens Unity console) |
| Visibility | Hidden by default. `visibleOnStart = false` |
| Window rect | `new Rect(20, 20, 420, 600)` |
| Window ID | `8121` (calibration panel uses 8120, must not collide) |
| Draggable | Yes, `GUI.DragWindow(new Rect(0, 0, width, 20))` on title bar |
| Tabs | 5 tabs: VFX, Fluid, Lighting, Environment, Avatar |
| Tab rendering | Horizontal row of `GUILayout.Toggle()` styled as toolbar buttons |
| Per tab content | Vertical scrollable list of preset names. Each row: color swatch (16x16 `GUI.DrawTexture` with `baseColor`) + preset name label + "Apply" button |
| Selected highlight | Currently active preset name shown in bold or different color |
| Auto-cycle section | Below the list: Toggle "Auto-cycle" + int field "BPM divisor" with buttons [4] [8] [16] [32] |
| Bottom buttons | "Save" (persist), "Load" (reload), "Hide" (close panel) |
| Status line | `GUILayout.Label("File: " + PersistPath)` showing the JSON save location |
Preset Loading
void OnEnable()
{
_vfxPresets = Resources.LoadAll<LumeVfxPreset>("LumePresets/Vfx");
_fluidPresets = Resources.LoadAll<LumeFluidSimPreset>("LumePresets/FluidSim");
_lightingPresets = Resources.LoadAll<LumeLightingPreset>("LumePresets/Lighting");
_envPresets = Resources.LoadAll<LumeEnvironmentPreset>("LumePresets/Environment");
_avatarPresets = Resources.LoadAll<LumeAvatarPreset>("LumePresets/Avatar");
Load(); // restore last selection from JSON
}### Apply Logic (reuse from LumeVfxEditor.cs)
- VFX: push `baseColor` via `MaterialPropertyBlock` to `LumePointRenderer`. Set count/spawnRate/lifetime on VFX Graph if VisualEffect present
- FluidSim: call `FindObjectOfType<LumeFluidSim>().ApplyPreset(preset)` (API already exists)
- Lighting: set Key/Fill/Rim light colors + intensities on scene lights found by tag or name
- Environment: set `RenderSettings.skybox`, `RenderSettings.fogColor`, `RenderSettings.fogDensity`
- Avatar: swap prefab on GameObject named "Avatar" if it exists
JSON Persistence
[Serializable]
class PickerState
{
public string selectedVfx;
public string selectedFluid;
public string selectedLighting;
public string selectedEnvironment;
public string selectedAvatar;
public bool autoCycle;
public int bpmDivisor;
public string savedAt;
}
string PersistPath => Path.Combine(Application.persistentDataPath, "lume-picker.json");Save on: OnDisable, on selection change, on "Save" button click.
Load on: OnEnable.
### Integration with Auto-Wire
Add to `Assets/Editor/LumeWaveAutoWire.cs`:
// ---- Wave 8: Duncan preset picker ----------------------------
var picker = root.GetComponent<LumeDuncanPicker>();
if (picker == null) { picker = root.AddComponent<LumeDuncanPicker>(); added++; }And add to the Verify menu output.
---
Part 8: VFX Editor Slot Enums (Duncan's Verbatim Names)
These are the exact enum values Duncan uses in his Settings panel. Our presets should map to these naming families:
Depth Modes:
GhostChromatic2, GlassThin, GlassThick, GlassScan1, GlassScan2, GlassScan3,
DepthCubes, HolePunch, None
VFX Slot 01:
ClonesAudioDropBright, ParticleClone, ParticleClonesAudio, SwirlyParticles,
KaleidoscopeRing16, Multiparticle_Spine_FX, None
VFX Slot 02:
ParticleSystem_Spine_Trail, ClonesSnapshotRed, ClonesSnapshotRedPlexus, None
Painting Modes:
SpotsLeftRightWithFill, Bounce_Frosted, Bounce_Plastic, None
Lighting:
FogSpotLeftRight1, FogSpotLeftRight2, FogSpotRight, FogSpotOverhead,
FogPointOrbitPulse, BrightRoom, SpotsLeftRightWithFill, EmissiveOnly, None
Lighting Profile:
EmissiveOnly (160-170fps best perf), SpotShadows (70-110fps, 7x shadow cast cost)
Environment:
Studio_Sky_Sphere, Sunset, BolivianSaltFlats, TestRoomDark, None
Background:
StudioBG_Grey, Gallery2, TestRoomDark, None
Logo:
HOLOVIS_Letters_AudioBlendshape, Off
Fluid Sim:
Default, Default_Smooth, LongThrow, MidThrow, ShortThrow, InvertedObstacle
Body Sim:
Same values as FluidSim (separate body-driven slot)
FX:
ClonesSnapshotRed, ClonesSnapshotRedPlexus, None
Spawn Topology:
FloorPlane, BodyVolume, CeilingPlane, WallPlanes, MidPlane
Noise Axis Mode:
XZ_Twist_Contained, Y_Lift_With_LowFreqBulges---
Part 9: Shader Reference Values (Inspector Dumps)
Duncan's Custom/Thickness_Apply shader (E462 Inspector capture)
IOR: 1.33 (water)
Translucency: 0.003
Fresnel Multiplier: 0.0 (OFF by default -- he rarely uses Fresnel on the blobby material)
Emissions Scale: 0.00-0.05 (THE ENTIRE useful range. NOT 0-1. Going above 0.05 = blown out white)
Surface Type: Transparent/Opaque dual-pass
Emission HDR Colors:
Bright blue: RGB(109, 255, 255) = float3(0.427, 1.000, 1.000)
Green: RGB(133, 255, 0) = float3(0.522, 1.000, 0.000)
Magenta: RGB(255, 0, 255) = float3(1.000, 0.000, 1.000)
Compute thickness power curve: pow(thickness, k) where k = 6-10
Result: bistable -- surfaces stay dark then suddenly glow when thickness crosses thresholdOur LumeCalibrationPanel -- 26 Tunables (current ranges)
Optical Flow (5 tunables):
diffNormMm: 20-2000 (motion sensitivity in mm, 200 = shoulder-shift at arm's reach)
responseGain: 0.1-10 (amplify motion before saturate)
smoothing: 0.05-1.0 (EMA coefficient)
decayPerSec: 0-8 (flow decay rate when no motion)
lkFlowClampPx: 1-32 (max per-component flow in pixels/frame)
Depth Reproject (2 tunables):
minDepthM: 0.01-1.0 (near clip in meters)
maxDepthM: 0.5-16 (far clip in meters)
Audio FFT (6 tunables):
rmsGain: 0.5-16 (RMS amplification)
beatThreshold: 0-1 (beat detection sensitivity)
beatSpike: 0-4 (outline brightness on beat)
transientSpike: 0-4 (additional brightness on transient)
smoothing: 0.05-1.0 (audio EMA)
staleTimeout: 0.1-5 (seconds before declaring LUMF stale)
Impulse Force (3 tunables):
forceMagnitude: 0-50 (kick strength, default 6)
decayPerSec: 0.1-50 (force decay rate, default 8)
outlineThreshold: 0.1-4 (fallback beat threshold when LUMF absent)
Fluid Sim (5 tunables):
viscosity: 0.00001-0.01
diffusion: 0.000001-0.001
dt: 0.001-0.05
flowInjectionScale: 0-5
audioTurbulenceScale: 0-3
Motion Gate (4 tunables):
minScale: 0-0.5 (minimum time scale during stillness)
maxScale: 0.5-1.0 (maximum time scale during motion)
powerCurve: 0.5-4 (remapping curve exponent)
smoothing: 0.01-0.5 (EMA coefficient for time-scale changes)
Bar Display (1 tunable):
barVerticalFov: 10-60 (camera vertical FOV, default 25)---
Part 10: Mac4 State and Sync Procedure
### Current Mac4 Reality (verified 2026-05-02 via SSH)
- Unity 6000.0.72f1 is RUNNING (PID 20180)
- But `Desktop/lume-commerce/` is EMPTY -- no scripts, no git, no assets
- `[home-path]` has a partial sync from Mac1 (never fully applied)
- Zero .cs files anywhere on Mac4's lume-commerce directory
- One tmux session running (`claude`)
- Mac4 reachable from Mac1 via Tailscale at ~7ms
Sync Procedure (run from Mac1)
rsync -avz --progress \
--exclude='.git' \
--exclude='Library/' \
--exclude='Temp/' \
--exclude='Logs/' \
--exclude='obj/' \
--exclude='*.mp4' \
Desktop/lume-commerce/ mac4:Desktop/lume-commerce/After sync, on Mac4:
1. Unity will detect new files and reimport (~2-5 min)
2. Watch Console for compile errors
3. Expected: clean reload, 17 components available in AddComponent menu
4. Run: `Lume > Auto-Wire` to attach all components to LumeMain GameObject
### K11 State
K11 is currently UNREACHABLE (DNS resolution failure on Tailscale). K11 work is blocked until Tailscale connectivity is restored. K11 is second priority -- get Mac4 working first.
---
Part 11: Execution Order for Codex
### Step 1: Sync Mac1 to Mac4
Run the rsync command from Part 10 on Mac1. Wait for Unity reimport on Mac4.
### Step 2: Write Assets/Editor/LumePresetGenerator.cs
Menu item: `Lume > Generate Duncan Presets`
Programmatically creates ALL .asset files from Part 5 using `ScriptableObject.CreateInstance<T>()` + `AssetDatabase.CreateAsset()`.
Directory structure:
Assets/Resources/LumePresets/
Vfx/ -- 13 VfxPreset assets
FluidSim/ -- 6 FluidSimPreset assets
Lighting/ -- 7 LightingPreset assets
Environment/ -- 4 EnvironmentPreset assets
Avatar/ -- (empty for now, avatar presets need humanoid model)
MANIFEST.md -- lists every asset with source reel + description### Step 3: Run Generator in Unity
Menu: `Lume > Generate Duncan Presets`. Should produce 30+ assets.
Verify: `Resources.LoadAll<LumeVfxPreset>("LumePresets/Vfx").Length` should be >= 13.
### Step 4: Write LumeDuncanPicker.cs (IMGUI Picker)
New MonoBehaviour at `Assets/Scripts/LumeDuncanPicker.cs`.
F1 toggle, IMGUI panel, 5 tabs, preset list, auto-cycle, JSON persistence per Part 7 spec.
### Step 5: Wire into Auto-Wire
Add LumeDuncanPicker to `Assets/Editor/LumeWaveAutoWire.cs` (Wave 8 section).
### Step 6: Add FreezeVelocityField to LumeFluidSim
Add a `public bool freezeVelocityField` toggle. When true, skip compute shader dispatch but keep RTs alive. This enables the "Frozen Sim Buffers" mode from E560.
### Step 7: Verify on Mac4
- Start publishers: `pointcloud_pub.py --raw-depth --host [ip]` on :9700, `audio_pub.py --synthetic --host [ip]` on :9701
- Unity Play mode
- F12 shows calibration panel (26 tunables)
- F1 shows Duncan preset picker (5 tabs, 30+ presets)
- Switch VFX presets: point cloud color changes
- Switch Fluid presets: fluid behavior changes (verify via particle motion)
- Toggle freeze: fluid sim freezes but particles keep flowing
- Auto-cycle: presets rotate on BPM divisor
Step 8: Tests
cd Desktop/lume-commerce/software/demo/
python3 -m pytest tests/ -q # existing 33 fast tests still greenUnity edit-mode test at `Assets/Scripts/Editor/Tests/LumePresetTests.cs`:
- `Resources.LoadAll<LumeVfxPreset>("LumePresets/Vfx").Length >= 13`
- `Resources.LoadAll<LumeFluidSimPreset>("LumePresets/FluidSim").Length >= 6`
- Picker JSON round-trip: save state, reload, verify equality
- `LumeFluidSim.ApplyPreset()` accepts each generated FluidSim preset without exception
Step 9: Commit
feat(lume): duncan preset generator -- 13 VfxPreset + 6 FluidSim + 7 Lighting + 4 Environment assets
feat(lume): duncan runtime picker -- F1 IMGUI panel with 5 tabs, auto-cycle, JSON persistence
feat(lume): frozen velocity buffer toggle for FluidSim (E560 mode)---
Part 12: Don't Touch List
| File/System | Why |
|---|---|
| `software/demo/pointcloud_pub.py` | synthpub LaunchAgent on Mac5 depends on its CLI. Adding flags is fine, renaming/removing is breaking |
| `Scripts/LumeUdpReceiver.cs` | Wire format magic-byte dispatch is settled. 5 magic bytes pinned by golden tests |
| `Scripts/LumeAudioReactor.cs` | Wave 1 fallback, parallel session contract. Use the public setters, do not modify internals |
| Wire format magic bytes | LUME=0x4C554D45, LUMD=0x4C554D44, LUMF=0x4C554D46, LUMM=0x4C554D4D, LUMC=0x4C554D43. Pinned by `tests/test_wire_format_golden.py` |
| `Scripts/LumeFlowParticleOverlay.cs` + shader | TEMPORARY substitute for the unauthored .vfx graph. Must stay until real VFX Graph asset is authored and verified. Comment in file labels it temporary |
| Wave 1 cloud rendering path | Must stay bit-preserved as the demo rescue floor |
| `hardware/cad/` | Different workstream (3D printing pipeline). Mac1 territory |
| Active tmux feeds on Mac4 | Don't kill `lume-real-d` or `lume-audio` while testing. Use Unity batchmode for compile checks |
---
Part 13: Reference Material Locations
| What | Path |
|---|---|
| 83-reel index table | `Reference/Duncan/INDEX.md` |
| 69 Gemini analyses | `Reference/Duncan/analyses/E###-*.md` |
| Reel captions (text) | `Reference/Duncan/reels/*.txt` |
| Reel metadata (JSON) | `Reference/Duncan/reels/*.json` |
| Playbook v2 master (513 lines) | `[home-path]` |
| Playbook chunk DV-DU (E485-E570) | `[home-path]` |
| Playbook chunk DT-DS (E475-E521) | `[home-path]` |
| Playbook chunk DR-DQ (E415-E474) | `[home-path]` |
| Gap analysis (16-row table) | `Desktop/lume-commerce/software/demo/DUNCAN-GAP-ANALYSIS.md` |
| Architecture doc | `ARCHITECTURE.md` in Unity project root |
| VFX Graph node recipe | `Assets/VFX/README-LumeFlowParticles.md` |
| Calibration panel (IMGUI pattern) | `Assets/Scripts/LumeCalibrationPanel.cs` |
| Print validation guide | `Desktop/lume-commerce/hardware/cad/PRINT-VALIDATION-GUIDE.md` |
### Top 10 Analyses to Read First
1. `analyses/E598-*.md` -- Dissolving Clones (4,081 likes, highest engagement)
2. `analyses/E568-*.md` -- SuperHot Cubes / Frozen Sim Buffers 5
3. `analyses/E512-*.md` -- Audio coupling engineering rules (CRITICAL)
4. `analyses/E560-*.md` -- Frozen Sim Buffers 1 (accidental art)
5. `analyses/E549-*.md` -- Kelp / Seagrass Strips
6. `analyses/E535-*.md` -- Particle Motion Kick (impulse channel)
7. `analyses/E545-*.md` -- Pinscreen / Cube Grid
8. `analyses/E589-*.md` -- Dat Funk / Motion Sparks
9. `analyses/E593-*.md` -- Blobby Guys (metaball/marching cubes)
10. `analyses/E534-*.md` -- Fluid Presets Demo (verbatim preset names)
---
Part 14: Commit Convention
feat(lume): <what> -- <details>
fix(lume): <what>Examples from git log:
feat(lume): fluid sim presets (Calm/Reactive/Storm) + ApplyPreset API
feat(lume): F12 panel gains FluidSim + MotionGate tunables + K11 CLAUDE.md
fix(lume): OSC packed vs per-bone address collision + 4 OSC decoder tests---
Part 15: Performance Targets
| Platform | Target | Particle Budget |
|---|---|---|
| Duncan's A6000 (reference ceiling) | 60fps | 2x150K = 300K mesh particles |
| Duncan's 3070 laptop (dev) | 30-60fps | 300K total |
| Mac4 M4 16GB (our dev) | 30-60fps | 100-200K particles |
| K11 Radeon 780M ~9 TFLOPS (product) | 60fps | 80-150K particles (estimate, untested) |
| EmissiveOnly lighting mode | 160-170fps | any count (no shadow compute) |
| SpotShadows lighting mode | 70-110fps | reduced (7x shadow cast cost) |
---
This is the complete handoff. The file is also saved at `Desktop/lume-commerce/CODEX-DUNCAN-HANDOFF.md`.
Promotion Decision
Attach run IDs, datasets, metrics, and reproduction commands.
Source Anchor
lume-commerce/CODEX-DUNCAN-HANDOFF.md
Detected Structure
Method · Evaluation · Code Anchors · Architecture