Add CoolSaber demo: PSVR2 lightsaber duel with SharePlay - #17
Open
miogds wants to merge 15 commits into
Open
Conversation
added 15 commits
July 25, 2026 15:27
CoolSaber is a render-extension plugin drawing HDR additive saber blades and clash sparks from the PSVR2 Sense controller poses, plus a visionOS example app with SharePlay duels (group immersive space), CoreHaptics clash feedback, and synthesized saber audio.
- Window the blade halo to exactly zero inside the billboard bounds and tighten the glow falloff, so only the light around the core is visible, never the quad itself; alpha now lives almost entirely in the core so the halo no longer dims passthrough. - Read each wand's trigger directly from its GCController (SaberWandInput) instead of the engine's merged GameControllerState, whose side-prefixed element-name assumption doesn't hold on real Sense wands — each saber now ignites independently. Hand inference by vendor name or element prefixes, shared with haptics; wand element names are logged on connect.
On visionOS the audio engine can report started before IO cycles begin; AVAudioPlayerNode.play() then throws the uncatchable 'player did not see an IO cycle' NSException and kills the app at immersive-space open. The source-node callback only renders when IO actually runs, so the failure mode is gone by construction: hum voices are resampled per frame (rate replaces the time-pitch unit), one-shots are mixed in the same callback, and engine start retries while session activation lags.
The Sense wand's trigger reaches GameController with a Button A alias, so the engine's merged aPressed fired on every trigger pull and the toggle-both fallback overrode per-hand ignite. Remove that fallback; SaberWandInput now also binds each wand's A/Cross alias to its own hand, dedupes alias elements by identity, and debounces per controller so one physical press yields exactly one toggle.
Blade axis was the controller's forward (-Z), reading as an arm extension. Now mostly +Y with a slight forward tilt (~73 deg from forward), starting just above the controller ring.
Once the immersive space closed (crown press, SharePlay transition), reopening it hit the xr-already-set guard and produced a dead space that never rendered — 'Enter the Duel Arena' then appeared to do nothing. When the render loop exits on layer invalidation, the game and engine now tear down (shutdownUntoldEngineXR) so the next open rebuilds cleanly. Entering the arena also lights each saber the first time its wand is tracked — mixed immersion shows no change at all otherwise — and the open-space result is logged.
Blade axis leans ~29 deg forward now. The control window shows a live status line (arena open state, last openImmersiveSpace result, PSVR2 connection and per-wand tracking) to pinpoint where 'Enter the Duel Arena' stalls on device. Audio session activation now retries with the engine, and the plist declares the window as the preferred default scene role like the engine template.
Device log showed the arena opens and renders fine from the button; the
'nothing happens' was silence — AVAudioSession activated during the
scene transition comes up dead ('Session lookup failed'), the engine
reports running with no IO, and sound only appeared once SharePlay
forced a live session into the process. Session activation errors are
now retryable instead of swallowed, the first attempt waits half a
second for the transition to settle, and a watchdog restarts the engine
whenever it claims to run but the render callback stalls. Haptic
engines no longer start eagerly at attach (same transition caused a
CHHapticEngine startup timeout); they start on demand per pulse.
Device log showed the failing entry path: the PSVR2 accessory provider loads slowly and arrives after the immersive space's first ARKit provider run, forcing a re-run that can leave world tracking paused — no wand tracking, no blades. Touching the input system at app launch starts wand discovery and accessory loading long before the space opens, so the first provider run already includes the accessory provider (the ordering that empirically works; the SharePlay flow only 'fixed' this by adding time). If tracking still delivers nothing after 2 s in the arena, blades ignite at the resting pose so the arena is never silently empty — frozen blades read as 'tracking dead' at a glance. Blade tilt increased to ~37 deg forward.
Iterating the blade angle by rebuild was going in circles; the tilt (0 = out of the fist, 90 = along the controller) is now a slider applied live to both blades, defaulting to 60 deg per the user's reference sketch. Once the right angle is found on device it gets baked in as the default. Also lands the arena loading cue that was in the working tree: while wands are connected but not yet tracked, a pair of sparks orbits ahead of the user (replacing the frozen-blades fallback, which read as broken), and the control window shows a progress row until the first wand tracks and the blades auto-ignite.
…sisted Tilt confirmed at 68 deg on device and baked as the default. The blade origin was still off from other viewpoints, so the control window now has a Saber Fit panel — tilt plus side/height/forward offsets in controller space — applied live to both blades and stored per device via AppStorage, so each duel participant tunes the fit of their own controllers once and keeps it. Reset button restores defaults.
Tilt only rotates the blade in the up/forward plane, so a sideways misalignment with the handle could be masked from one viewpoint with offsets but never fixed. Lean swings the tilted direction left/right around the controller's up axis, completing the direction's degrees of freedom. Defaults updated to the values tuned on device (tilt 83, side -1.5 cm, height -0.5 cm, forward 5.5 cm).
The fit sliders pushed the duel buttons below the window edge. The fit panel is now a DisclosureGroup (collapsed by default), the window body scrolls, and the default height grew to 640.
The right wand's tuned fit didn't hold for the left one, so fit values (tilt/lean/offsets) are now stored and applied per hand: the fit panel gains a Left/Right wand picker, each hand persists its own settings, and the left defaults to the right's verified values with the sideways components (lean, side offset) mirrored.
Author
|
this is the first version so you can test the controller and the fix in the engine. Still I need to keep working on the share play session. (multiplayer) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CoolSaber — PSVR2 lightsaber duel demo
A new demo showcasing the engine's PSVR2 Sense controller support on Apple Vision Pro (visionOS 26 accessory tracking): each controller is a saber hilt — pull the trigger to ignite a glowing blade — and via SharePlay two players on a FaceTime call can duel, with blade-clash sparks, per-wand haptics, and synthesized saber audio.
Plugin package (
CoolSaber/).beforePostProcessdraws up to 4 blades (local + remote, left/right) and 8 clash sparks in a single draw call. Vertex-ID procedural billboards, capsule-SDF fragment glow (hot core + halo windowed to zero inside the quad), HDR values feed the engine bloom. First use of the engine'sblendMode: .additive.swift test), including a shader-ABI stride guard and a metallib function check.Example app (
CoolSaber/Examples/CoolSaberVisionOS)GCController(the Sense wands expose unprefixed element names — "Trigger", "Button A" — with handedness only in the vendor name, so the merged engine controller state can't drive per-hand actions). Each saber ignites independently.@AppStorage) so every player aligns the blades to their own grip once. Right-wand defaults verified on device (tilt 73°, lean 6°).GroupActivity+ system coordinator (group immersive space,.conversationaltemplate), unreliable 45 Hz pose stream + reliable ignite/clash events, lock-guarded mailbox between the main-actor session and the XR render thread, opponent-anchor fallback when a participant isn't spatial. Solo practice works without a session.AVAudioSourceNode(player nodes crash with an uncatchable "player did not see an IO cycle" exception when the session comes up dead during the immersive-space transition), with retryable session activation and an IO-stall watchdog.Verified on device
Blade rendering/glow, per-hand ignite, clash sparks + haptics, audio, saber fit. Not yet verified: the two-headset SharePlay duel (needs two Vision Pros on a FaceTime call).
Known engine issues found while building this (fixes tracked separately)