From 844673e9248ec4fa9db859cac84faf8ed515d470 Mon Sep 17 00:00:00 2001 From: Ruben Panzich Date: Tue, 23 Jun 2026 11:46:59 +0800 Subject: [PATCH] Add listener-anchored sources --- .../Assets/Runtime/PHASEHelpers.cs | 19 +++++++ .../Assets/Runtime/PHASEListener.cs | 4 ++ .../Assets/Runtime/PHASESource.cs | 51 ++++++++++++++++- plug-ins/Apple.PHASE/Native/PHASEInterface.h | 6 ++ plug-ins/Apple.PHASE/Native/PHASEInterface.mm | 6 ++ .../Native/PHASEWrapper/PHASEWrapper.h | 8 +++ .../Native/PHASEWrapper/PHASEWrapper.mm | 55 ++++++++++++++++++- 7 files changed, 147 insertions(+), 2 deletions(-) diff --git a/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASEHelpers.cs b/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASEHelpers.cs index c5de5d7d..cf4919e2 100644 --- a/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASEHelpers.cs +++ b/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASEHelpers.cs @@ -103,6 +103,14 @@ public enum ReverbPresets /// True on success, false otherwise. [DllImport(PluginDllName)] public static extern bool PHASESetSourceTransform(long inSourceId, Matrix4x4 inTransform); + /// + /// Parents a source under the listener or back under the scene root. + /// + /// The unique ID representing the source. + /// True to parent under the listener, false to parent under root. + /// True on success, false otherwise. + [DllImport(PluginDllName)] public static extern bool PHASESetSourceListenerAnchored(long inSourceId, bool inAnchored); + /// /// Set the gain of the source in the PHASE engine. /// @@ -821,6 +829,17 @@ static public Matrix4x4 GetPhaseTransform(Transform inTransform) return phaseTransform; } + /// + /// Converts a Unity matrix to a PHASE transform (Left-Handed to Right-Handed). + /// + /// Unity based matrix to convert to PHASE coordinates. + /// A Matrix4x4 representing a transform in PHASE coordinates. + static public Matrix4x4 GetPhaseTransform(Matrix4x4 inTransform) + { + // RhConversionMat (S) * M * S, passed transposed because the native side reads it that way. + return RhConversionMat * inTransform.transpose * RhConversionMat; + } + static private Vector3 GetCombinedHierachyScale(Transform inTransform) { Vector3 combinedScale = inTransform.localScale; diff --git a/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASEListener.cs b/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASEListener.cs index 3add6fdc..20379116 100644 --- a/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASEListener.cs +++ b/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASEListener.cs @@ -67,6 +67,10 @@ void CreateListener() { Debug.LogError("Failed to create PHASE Listener"); } + else + { + PHASESource.ReanchorSources(); + } } // Update is called once per frame. diff --git a/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASESource.cs b/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASESource.cs index b18a963b..497b0b1f 100644 --- a/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASESource.cs +++ b/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASESource.cs @@ -63,6 +63,9 @@ enum SourceMode // Active sound event instance on this source. private List _soundEventInstance = new List(); + // When anchored, the local transform is sent to PHASE as the offset from the listener. + private bool _listenerAnchored = false; + // Source id to store. private long _sourceId = Helpers.InvalidId; @@ -243,7 +246,10 @@ protected virtual void ManualUpdate() { if (_transform != null && _sourceId != Helpers.InvalidId) { - Matrix4x4 phaseTransform = Helpers.GetPhaseTransform(_transform); + // An anchored source sends its local transform, read relative to the listener. + Matrix4x4 phaseTransform = _listenerAnchored + ? Helpers.GetPhaseTransform(Matrix4x4.TRS(_transform.localPosition, _transform.localRotation, Vector3.one)) + : Helpers.GetPhaseTransform(_transform); bool result = Helpers.PHASESetSourceTransform(_sourceId, phaseTransform); if (result == false) { @@ -266,6 +272,19 @@ protected internal static void UpdateSources() entry.Value.ManualUpdate(); } } + + // Re-parents anchored sources under a newly created listener. + protected internal static void ReanchorSources() + { + foreach (PHASESource source in _registeredSources.Values) + { + if (source._listenerAnchored) + { + source.SetListenerAnchored(true); + } + } + } + private void UpdateGain() { var result = Helpers.PHASESetSourceGain(_sourceId, _gain); @@ -349,6 +368,35 @@ void SetSendParametersForSpatialMixer(string mixer_name, long instanceId) } } + /// + /// Parents this source under the listener, or back under the scene root. + /// While anchored, the source's local transform is its offset from the listener, + /// so parent the GameObject under the listener's GameObject. + /// + /// True to parent under the listener, false to parent under root. + /// True on success, false otherwise. + public bool SetListenerAnchored(bool anchored) + { + if (_sourceId == Helpers.InvalidId) + { + _listenerAnchored = false; + return false; + } + + bool result = Helpers.PHASESetSourceListenerAnchored(_sourceId, anchored); + _listenerAnchored = anchored && result; + return result; + } + + /// + /// Whether this source is currently anchored to the listener. + /// + /// True if anchored to the listener, false otherwise. + public bool IsListenerAnchored() + { + return _listenerAnchored; + } + /// /// Set a meta parameter of type integer associated with this source's sound event. /// @@ -409,6 +457,7 @@ public void DestroyFromPHASE() Helpers.PHASEDestroySource(_sourceId); _toBeDestroyed = true; _sourceId = Helpers.InvalidId; + _listenerAnchored = false; } // Stop is called when the object stops. diff --git a/plug-ins/Apple.PHASE/Native/PHASEInterface.h b/plug-ins/Apple.PHASE/Native/PHASEInterface.h index 90b1d304..6088f609 100644 --- a/plug-ins/Apple.PHASE/Native/PHASEInterface.h +++ b/plug-ins/Apple.PHASE/Native/PHASEInterface.h @@ -94,6 +94,12 @@ int64_t PHASECreatePointSource(); */ bool PHASESetSourceTransform(int64_t inSourceId, Matrix4x4 inTransform); +/* + Parents a source under the listener or back under the scene root. + Returns true on success, false otherwise. +*/ +bool PHASESetSourceListenerAnchored(int64_t inSourceId, bool inAnchored); + /* Sets the gain linear scale value of a given source, range of [0,1]. Given gain values outside of this range will be clamped. diff --git a/plug-ins/Apple.PHASE/Native/PHASEInterface.mm b/plug-ins/Apple.PHASE/Native/PHASEInterface.mm index eaf3d2de..3d30d368 100644 --- a/plug-ins/Apple.PHASE/Native/PHASEInterface.mm +++ b/plug-ins/Apple.PHASE/Native/PHASEInterface.mm @@ -163,6 +163,12 @@ bool PHASESetSourceTransform(int64_t inSourceId, Matrix4x4 inTransform) return [engineWrapper setSourceTransformWithId:inSourceId transform:sourceTransform]; } +bool PHASESetSourceListenerAnchored(int64_t inSourceId, bool inAnchored) +{ + PHASEEngineWrapper* engineWrapper = [PHASEEngineWrapper sharedInstance]; + return [engineWrapper setSourceListenerAnchoredWithId:inSourceId anchored:inAnchored]; +} + bool PHASESetSourceGain(int64_t inSourceId, double inGain) { PHASEEngineWrapper* engineWrapper = [PHASEEngineWrapper sharedInstance]; diff --git a/plug-ins/Apple.PHASE/Native/PHASEWrapper/PHASEWrapper.h b/plug-ins/Apple.PHASE/Native/PHASEWrapper/PHASEWrapper.h index 340dccda..344465b7 100644 --- a/plug-ins/Apple.PHASE/Native/PHASEWrapper/PHASEWrapper.h +++ b/plug-ins/Apple.PHASE/Native/PHASEWrapper/PHASEWrapper.h @@ -197,6 +197,14 @@ enum CalibrationMode */ - (BOOL)setSourceTransformWithId:(int64_t)sourceId transform:(simd_float4x4)transform; +/*! @method setSourceListenerAnchoredWithId + @abstract Parents a source under the listener or back under the scene root. + @param sourceId source ID to re-parent + @param anchored true to parent under the listener, false to parent under root + @return true on success, false otherwise +*/ +- (BOOL)setSourceListenerAnchoredWithId:(int64_t)sourceId anchored:(BOOL)anchored; + /*! @method setSourceGainWithId @abstract Sets the gain linear scale value of the source, range of [0,1]. Given gain values outside of this range will be clamped. @param sourceId source ID to update gain for diff --git a/plug-ins/Apple.PHASE/Native/PHASEWrapper/PHASEWrapper.mm b/plug-ins/Apple.PHASE/Native/PHASEWrapper/PHASEWrapper.mm index 599acbc1..7d8f2e96 100644 --- a/plug-ins/Apple.PHASE/Native/PHASEWrapper/PHASEWrapper.mm +++ b/plug-ins/Apple.PHASE/Native/PHASEWrapper/PHASEWrapper.mm @@ -337,6 +337,18 @@ - (BOOL)destroyListener return NO; } + // Return any anchored sources to the root before the listener goes away. + NSArray* children = mListener.children; + [mListener removeChildren]; + for (PHASEObject* child in children) + { + NSError* errorRef = nil; + if (![mEngine.rootObject addChild:child error:&errorRef]) + { + NSLog(@"Phase Wrapper: Failed to return PHASE Source to the root %@.", errorRef); + } + } + // Remove from the hierarchy [mEngine.rootObject removeChild:mListener]; @@ -409,6 +421,47 @@ - (BOOL)setSourceTransformWithId:(int64_t)sourceId transform:(simd_float4x4)tran return YES; } +- (BOOL)setSourceListenerAnchoredWithId:(int64_t)sourceId anchored:(BOOL)anchored +{ + PHASESource* source = [mSources objectForKey:[NSNumber numberWithLongLong:sourceId]]; + if (source == nil) + { + NSLog(@"Phase Wrapper: Failed to find PHASE Source to anchor."); + return NO; + } + + if (anchored && mListener == nil) + { + NSLog(@"Phase Wrapper: Listener does not exist."); + return NO; + } + + PHASEObject* newParent = anchored ? mListener : mEngine.rootObject; + if (source.parent == newParent) + { + // Already there. Re-parenting an anchored source again has silenced it on device. + return YES; + } + + // addChild fails if the source already has a parent. + [source.parent removeChild:source]; + + NSError* errorRef = nil; + const BOOL result = [newParent addChild:source error:&errorRef]; + if (!result) + { + NSLog(@"Phase Wrapper: Failed to add PHASE Source to the %@ %@.", anchored ? @"Listener" : @"root", errorRef); + // Put it back under the root rather than leave it orphaned. + NSError* rollbackError = nil; + if (![mEngine.rootObject addChild:source error:&rollbackError]) + { + NSLog(@"Phase Wrapper: Failed to return PHASE Source to the root %@.", rollbackError); + } + return NO; + } + return YES; +} + - (BOOL)setSourceGainWithId:(int64_t)sourceId sourceGain:(double)sourceGain { PHASESource* source = [mSources objectForKey:[NSNumber numberWithLongLong:sourceId]]; @@ -440,7 +493,7 @@ - (void)destroySourceWithId:(int64_t)sourceId if (source != nil) { // Remove from the hierarchy - [mEngine.rootObject removeChild:source]; + [source.parent removeChild:source]; [mSources removeObjectForKey:[NSNumber numberWithLongLong:sourceId]];