using System.Collections.Generic; using UnityEngine; using UnityEngine.Profiling; using UnityEngine.SceneManagement; using Wwise.Wooduan.Components; using Wwise.Wooduan.Tools; namespace Wwise.Wooduan.Core { public enum HRTFMode { Auro, Resonance, None } public static class SpatialAudioManager { private static bool IsInitialized; internal static void Initialize() { _buildings = new List(); _scatteredRooms = new List(); IsInitialized = true; } internal static void Terminate() { _buildings = null; _scatteredRooms = null; IsInitialized = false; } #region Building private static List _buildings; internal static void RegisterBuilding(AkBuilding building) { if (IsInitialized && !_buildings.Contains(building)) _buildings.Add(building); } internal static void UnregisterBuilding(AkBuilding building) { if (IsInitialized && _buildings.Contains(building)) _buildings.Remove(building); } #endregion Building #region Cull public static bool CullByEmitter = true; private static int _currentBuildingIndex, _currentRoomIndex; internal static void Update() { if (!IsInitialized) return; Profiler.BeginSample("SpatialAudioManager.Update"); var listenerPosition = ListenerManager.GetListenerLocation(); if (listenerPosition == Vector3.zero) return; if (_currentBuildingIndex < _buildings.Count) { var building = _buildings[_currentBuildingIndex]; if (building) building.TryCull(listenerPosition); _currentBuildingIndex++; } else _currentBuildingIndex = 0; if (!SpatialAudioManager.CullByEmitter) { if (_currentRoomIndex < _scatteredRooms.Count) { var room = _scatteredRooms[_currentRoomIndex]; if (room) room.TryCull(listenerPosition); _currentRoomIndex++; } else _currentRoomIndex = 0; } Profiler.EndSample(); } #endregion Cull #region Room private static List _scatteredRooms; public static AkRoomPortal ClosestPortalToOutdoor; internal static void RegisterScatteredRoom(AkRoom room) { if (IsInitialized && !_scatteredRooms.Contains(room)) _scatteredRooms.Add(room); } internal static void UnregisterScatteredRoom(AkRoom room) { if (IsInitialized && _scatteredRooms.Contains(room)) _scatteredRooms.Remove(room); } internal static void UpdateSpatialAudioRoom(AkGameObj akGameObj) { if (!akGameObj) return; Profiler.BeginSample("UpdateSpatialAudioRoom"); var oldRoom = akGameObj.CurrentRoom; akGameObj.CurrentSpace = FindSpaceAtPosition(akGameObj.GetPosition()); if (akGameObj.IsOwnedByListener && AkGameObj.SelfEmitter) AkGameObj.SelfEmitter.CurrentSpace = akGameObj.CurrentSpace; var newRoom = akGameObj.CurrentRoom; if (newRoom != oldRoom) { // No new room, set to outdoor if (!newRoom) { AkSoundEngine.SetGameObjectInRoom(akGameObj.gameObject, AkRoom.INVALID_ROOM_ID); if (akGameObj.IsOwnedByListener && AkGameObj.SelfEmitter) AkSoundEngine.SetGameObjectInRoom(AkGameObj.SelfEmitter.gameObject, AkRoom.INVALID_ROOM_ID); AkWooduanHelper.DebugToConsole(AudioLogVerbosity.Notification, AudioObjectType.Room, AudioTriggerSource.Code, AudioAction.SetValue, "No Room", akGameObj.gameObject); } else { AkSoundEngine.SetGameObjectInRoom(akGameObj.gameObject, newRoom.GetID()); if (akGameObj.IsOwnedByListener && AkGameObj.SelfEmitter) AkSoundEngine.SetGameObjectInRoom(AkGameObj.SelfEmitter.gameObject, newRoom.GetID()); AkWooduanHelper.DebugToConsole(AudioLogVerbosity.Notification, AudioObjectType.Room, AudioTriggerSource.Code, AudioAction.SetValue, newRoom.name, akGameObj.gameObject, "Bus=" + newRoom.reverbAuxBus?.Name); } } Profiler.EndSample(); } private static AkSpace FindSpaceAtPosition(Vector3 position) { AkSpace resultSpace = AkOutdoor.GetInstance(); if (IsInitialized) { // search buildings foreach (var building in _buildings) { var space = building.FindSpaceAtPosition(position); if (space) return space; } // search scattered rooms var highestPriority = -1; AkSpace foundSpace = null; foreach (var room in _scatteredRooms) { var space = room.FindSpaceAtPosition(position); if (space && room.priority > highestPriority) { foundSpace = space; highestPriority = room.priority; } } if (foundSpace) return foundSpace; } return resultSpace; } internal static AkSpatialAudioRoom FindRoomAtPosition(Vector3 position) { var space = FindSpaceAtPosition(position); if (space) return space.Room; AkSpatialAudioRoom resultRoom = AkOutdoor.GetInstance(); #if UNITY_EDITOR if (!Application.isPlaying) { var highestPriority = -1; var colliders = UnityEngine.Physics.OverlapSphere(position, 0); foreach (var collider in colliders) { space = collider.gameObject.GetComponent(); if (space) { var room = space.Room as AkRoom; if (room && room.priority > highestPriority) { resultRoom = room; highestPriority = room.priority; } } } } #endif return resultRoom; } internal static void CalculateListenerDistanceToOutdoor(AkSpatialAudioRoom listenerRoom, Transform listenerTransform) { if (!listenerRoom) return; if (!listenerRoom.IsOutdoor) { var angleToOutdoor = 180f; ClosestPortalToOutdoor = listenerRoom.FindNearestPortalOpenedToOutdoor(listenerTransform.position, out var distance); if (ClosestPortalToOutdoor) { var portalTransform = ClosestPortalToOutdoor.transform; angleToOutdoor = distance < 1f ? Vector3.Angle(listenerTransform.forward, -portalTransform.forward) : 90f; } AudioManager.SetRTPCValue("Distance_To_Outdoor", distance); AudioManager.SetRTPCValue("Camera_Angle_To_Outdoor", angleToOutdoor); } else { ClosestPortalToOutdoor = null; AudioManager.SetRTPCValue("Distance_To_Outdoor", 0f); AudioManager.SetRTPCValue("Camera_Angle_To_Outdoor", 0f); } } internal static int CalculateFloorDifference(AkGameObj emitter, AkAudioListener listener) { if (!listener) return 0; if (!listener.CurrentSpace || !emitter.CurrentSpace) return 0; if (listener.CurrentSpace is AkOutdoor || emitter.CurrentSpace is AkOutdoor) return 0; if (listener.CurrentSpace is AkBuilding || emitter.CurrentSpace is AkBuilding) return 0; var listenerFloor = listener.GetFloor(); var emitterFloor = emitter.GetFloor(); return emitterFloor - listenerFloor; } #endregion Room #region Portal public static void TogglePortalAtLocation(Vector3 location, bool toggle, PortalType portalType) { var foundPortal = FindPortalAtPosition(location, portalType); if (foundPortal) { foundPortal.TogglePortal(toggle); } else AkWooduanHelper.DebugToConsole(AudioLogVerbosity.Error, AudioObjectType.Portal, AudioTriggerSource.Code, AudioAction.Activate, "", null, "Portal not found"); } private static AkRoomPortal FindPortalAtPosition(Vector3 position, PortalType portalType) { if (!IsInitialized) return null; foreach (var building in _buildings) { if (building.IsPositionWithinSpace(position)) return building.FindPortalAtPosition(position, portalType); } foreach (var room in _scatteredRooms) { if (room.IsCulled) { if (room.GetDistanceToPosition(position) > room.cullDistance) continue; } var portal = room.FindPortalAtPosition(position, portalType); if (portal) return portal; } return null; } #endregion Portal #region OcclusionObstruction public static bool OcclusionEnabled = true; public static bool ObstructionEnabled = false; private const float ObstructionStartAngle = 60f; public static LayerMask OcclusionObstructionLayer => LayerMask.GetMask("NoCollisionWithEntity", "Player"); internal static void UpdateOcclusionObstruction(AkGameObj emitter, out float occValue, out float obsValue) { if (!OcclusionEnabled) { occValue = obsValue = 0f; return; } Profiler.BeginSample("SpatialAudioManager.UpdateOcclusionObstruction"); var listenerPosition = ListenerManager.GetListenerLocation(); var emitterPosition = emitter.GetPosition(); var difference = listenerPosition - emitterPosition; var distEmitterListener = difference.magnitude; occValue = CastOcclusionRays(emitterPosition, difference, distEmitterListener); if (occValue > 0 && ObstructionEnabled && distEmitterListener < 50) { var propagateDirection = difference / distEmitterListener; var maxNumOfRays = Mathf.RoundToInt(60 / Mathf.Clamp(distEmitterListener, 10, 30)); var angleDecrementEmitter = ObstructionStartAngle / maxNumOfRays; var obsAngleEmitter = CastObstructionRaysFromEmitter(emitterPosition, propagateDirection, ObstructionStartAngle, angleDecrementEmitter, distEmitterListener, out var axis) + angleDecrementEmitter / 2f; var startAngleListener = Mathf.Min(90 - obsAngleEmitter, ObstructionStartAngle); var angleDecrementListener = startAngleListener / maxNumOfRays; var obsAngleListener = CastObstructionRayFromListener(listenerPosition, -propagateDirection, axis, obsAngleEmitter, startAngleListener, angleDecrementListener, distEmitterListener) + angleDecrementListener / 2f; obsValue = obsAngleEmitter + obsAngleListener; #if UNITY_EDITOR if (AkWwiseEditorSettings.Instance.showOccObsRays) { var localAxis = Vector3.Cross(-propagateDirection, axis); var newDirection = Quaternion.AngleAxis(obsAngleListener, localAxis) * -propagateDirection; var magnitude = CalculateListenerSideRayLength(distEmitterListener, obsAngleEmitter, obsAngleListener); var intersectPoint = listenerPosition + newDirection * magnitude; Debug.DrawRay(emitterPosition, intersectPoint - emitterPosition, Color.magenta, 1f); Debug.DrawRay(listenerPosition, intersectPoint - listenerPosition, Color.magenta, 1f); } #endif } else obsValue = 0f; Profiler.EndSample(); } private static float CastOcclusionRays(Vector3 startPosition, Vector3 difference, float magnitude) { var direction = difference / magnitude; var hit = new RaycastHit(); var result = Physics.Raycast(startPosition, direction, out hit, magnitude, ~OcclusionObstructionLayer); #if UNITY_EDITOR if (AkWwiseEditorSettings.Instance.showOccObsRays) Debug.DrawRay(startPosition, direction * magnitude, result ? Color.yellow : Color.green, 1f); #endif if (!result) return 0; if (magnitude > 40) return Mathf.Min(magnitude * 2, 100); var hitPointFromEmitter = hit.point; startPosition += difference; result = Physics.Raycast(startPosition, -direction, out hit, magnitude, ~OcclusionObstructionLayer); if (!result) return 0; var hitPointFromListener = hit.point; var obstacleThickness = Vector3.Distance(hitPointFromEmitter ,hitPointFromListener); return Mathf.Clamp(obstacleThickness * 5f, 0f, 100f); } private static float CastObstructionRaysFromEmitter(Vector3 startPoint, Vector3 direction, float angleEmitter, float angleDecrement, float distEmitterListener, out Vector3 axis) { axis = Vector3.up; var angleUp = CastObstructionRayFromEmitter(startPoint, direction, Vector3.up, angleEmitter, angleDecrement, distEmitterListener); var angleLeft = CastObstructionRayFromEmitter(startPoint, direction, Vector3.left, angleUp, angleDecrement, distEmitterListener); if (angleLeft < angleUp) axis = Vector3.left; var angleRight = CastObstructionRayFromEmitter(startPoint, direction, Vector3.right, angleLeft, angleDecrement, distEmitterListener); if (angleRight < angleLeft) axis = Vector3.right; return Mathf.Min(angleRight, ObstructionStartAngle); } private static float CastObstructionRayFromEmitter(Vector3 startPoint, Vector3 rayCastDirection, Vector3 rotateAxis, float angleEmitter, float angleDecrement, float distEmitterListener) { if (angleEmitter < 5) return 0; var magnitude = CalculateEmitterSideRayLength(distEmitterListener, angleEmitter); var localAxis = Vector3.Cross(rayCastDirection, rotateAxis); var newDirection = Quaternion.AngleAxis(angleEmitter, localAxis) * rayCastDirection; var blocked = Physics.Raycast(startPoint, newDirection, magnitude, ~OcclusionObstructionLayer); #if UNITY_EDITOR if (AkWwiseEditorSettings.Instance.showOccObsRays) Debug.DrawRay(startPoint, newDirection * magnitude, blocked ? Color.blue : Color.cyan, 1f); #endif if (blocked) return angleEmitter; return CastObstructionRayFromEmitter(startPoint, rayCastDirection, rotateAxis, angleEmitter - angleDecrement, angleDecrement, distEmitterListener); } private static float CastObstructionRayFromListener(Vector3 startPoint, Vector3 rayCastDirection, Vector3 rotateAxis, float angleEmitter, float angleListener, float angleDecrement, float distEmitterListener) { if (angleEmitter < 5) return 0; var magnitude = CalculateListenerSideRayLength(distEmitterListener, angleEmitter, angleListener); var localAxis = Vector3.Cross(rayCastDirection, rotateAxis); var newDirection = Quaternion.AngleAxis(angleListener, localAxis) * rayCastDirection; var blocked = Physics.Raycast(startPoint, newDirection, magnitude, ~OcclusionObstructionLayer); #if UNITY_EDITOR if (AkWwiseEditorSettings.Instance.showOccObsRays) Debug.DrawRay(startPoint, newDirection * magnitude, blocked ? Color.blue : Color.cyan, 1f); #endif if (blocked) return angleListener; return CastObstructionRayFromListener(startPoint, rayCastDirection, rotateAxis, angleEmitter, angleListener - angleDecrement, angleDecrement, distEmitterListener); } private static float CalculateListenerSideRayLength(float distEmitterListener, float angleEmitter, float angleListener) { var angleRadEmitter = Mathf.Deg2Rad * angleEmitter; var angleRadListener = Mathf.Deg2Rad * angleListener; return distEmitterListener * Mathf.Tan(angleRadEmitter) / (Mathf.Tan(angleRadEmitter) + Mathf.Tan(angleRadListener)) / Mathf.Cos(angleRadListener); } private static float CalculateEmitterSideRayLength(float distEmitterListener, float angleEmitter) { return distEmitterListener * Mathf.Cos(Mathf.Deg2Rad * angleEmitter); } #endregion OcclusionObstruction #region Settings public static bool RoomEnabled = true; public static bool GeometryEnabled = true; public static void ToggleSpatialAudio(bool toggle) { AudioManager.SetState("SpatialAudio", toggle ? "On" : "Off"); ToggleOcclusion(toggle); //ToggleObstruction(toggle); ToggleGeometry(toggle); ToggleRoom(toggle); } public static void ToggleRoom(bool toggle) { if (RoomEnabled != toggle) { RoomEnabled = toggle; RefreshAll(); } } public static void ToggleOcclusion(bool toggle) { OcclusionEnabled = toggle; } public static void ToggleObstruction(bool toggle) { ObstructionEnabled = toggle; } public static void ToggleGeometry(bool toggle) { if (GeometryEnabled != toggle) { GeometryEnabled = toggle; RefreshAll(); } } private static void RefreshAll() { if (_buildings == null) return; foreach (var building in _buildings) { building.Refresh(false); building.Refresh(true); } foreach (var room in _scatteredRooms) { room.Refresh(false); room.Refresh(true); } } public static void SwitchHRTFMode(HRTFMode newMode) { switch (newMode) { case HRTFMode.Auro: { AkChannelConfig config = new AkChannelConfig(); config.SetStandard(AkSoundEngine.AK_SPEAKER_SETUP_AURO_13POINT1_751); AudioManager.SetBusConfig("Binaural", config); AudioManager.SetBusEffect("Binaural", "SSJJ_Auro_Headphone", 0); break; } case HRTFMode.Resonance: { AkChannelConfig config = new AkChannelConfig(); config.SetAmbisonic(16); AudioManager.SetBusConfig("Binaural", config); AudioManager.SetBusEffect("Binaural", "SSJJ_Resonance_Audio_Renderer", 0); break; } default: { AkChannelConfig config = new AkChannelConfig(); config.Clear(); AudioManager.SetBusConfig("Binaural", config); AudioManager.ClearBusEffect("Binaural", 0); break; } } } #endregion Settings } }