// Copyright © 2015-2022 Pico Technology Co., Ltd. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using PXR_Audio.Spatializer; using UnityEditor; using UnityEngine; [RequireComponent(typeof(PXR_Audio_Spatializer_SceneMaterial))] public class PXR_Audio_Spatializer_SceneGeometry : MonoBehaviour { [Tooltip("Whether include meshes in children game objects as the shape of this acoustic geometry.")] [SerializeField] private bool includeChildren = false; [Tooltip("Whether visualize meshes in current scene that are included as the shape of this acoustic geometry.\n" + " - non-static meshes are visualized using white wire frames;\n" + " - static meshes are visualized using green wire frames.")] [SerializeField] private bool visualizeMeshInEditor = false; [Tooltip("Baked static mesh used as the shape of this acoustic geometry.")] [SerializeField] private Mesh bakedStaticMesh; #region EDITOR-ONLY SerializedFields #if UNITY_EDITOR [SerializeField] private LayerMask meshBakingLayerMask = ~0; [SerializeField, HideInInspector] private string currentBakedStaticMeshAssetPath = null; #endif #endregion public bool isStaticMeshBaked => bakedStaticMesh != null; private int geometryId = -1; public int GeometryId { get => geometryId; } private int staticGeometryID = -1; public int StaticGeometryId => staticGeometryID; private PXR_Audio_Spatializer_SceneMaterial material; public PXR_Audio_Spatializer_SceneMaterial Material { get { if (material == null) { material = GetComponent(); } return material; } } private MeshConfig meshConfig; private uint propertyMask = 0; private int currentContextUuid = -2; private void OnEnable() { if (PXR_Audio_Spatializer_Context.Instance == null) return; // If geometries are added after context is initialized if (PXR_Audio_Spatializer_Context.Instance.UUID != currentContextUuid) { var ret = SubmitMeshToContext(); var staticRet = SubmitStaticMeshToContext(); } else { meshConfig = new MeshConfig(true, Material, transform.localToWorldMatrix); if (geometryId >= 0) PXR_Audio_Spatializer_Context.Instance.SetMeshConfig(geometryId, ref meshConfig, (uint)MeshProperty.All); if (staticGeometryID >= 0) PXR_Audio_Spatializer_Context.Instance.SetMeshConfig(staticGeometryID, ref meshConfig, (uint)MeshProperty.All); } } private void OnDisable() { if (PXR_Audio_Spatializer_Context.Instance == null) return; if (PXR_Audio_Spatializer_Context.Instance.UUID != currentContextUuid) return; meshConfig.enabled = false; if (geometryId >= 0) PXR_Audio_Spatializer_Context.Instance.SetMeshConfig(geometryId, ref meshConfig, (uint)MeshProperty.Enabled); if (staticGeometryID >= 0) PXR_Audio_Spatializer_Context.Instance.SetMeshConfig(staticGeometryID, ref meshConfig, (uint)MeshProperty.Enabled); } private void OnDestroy() { RemoveMeshFromContext(); } private void RemoveMeshFromContext() { if (PXR_Audio_Spatializer_Context.Instance == null) return; if (PXR_Audio_Spatializer_Context.Instance.UUID != currentContextUuid) return; if (geometryId >= 0) { PXR_Audio_Spatializer_Context.Instance.RemoveMesh(geometryId); Debug.LogFormat("Removed geometry #{0}, gameObject name is {1}", geometryId.ToString(), name); } if (staticGeometryID >= 0) { PXR_Audio_Spatializer_Context.Instance.RemoveMesh(staticGeometryID); Debug.LogFormat("Removed static geometry #{0}, gameObject name is {1}", staticGeometryID.ToString(), name); } } private void Update() { if (PXR_Audio_Spatializer_Context.Instance == null) return; // // If geometries are added after context is initialized // if (PXR_Audio_Spatializer_Context.Instance.UUID != currentContextUuid) // { // var ret = SubmitMeshToContext(); // var staticRet = SubmitStaticMeshToContext(); // } if (transform.hasChanged) { meshConfig.SetTransformMatrix4x4(transform.localToWorldMatrix); propertyMask |= (uint)MeshProperty.ToWorldTransform; transform.hasChanged = false; } if (propertyMask > 0) { if (geometryId >= 0) PXR_Audio_Spatializer_Context.Instance.SetMeshConfig(geometryId, ref meshConfig, propertyMask); if (staticGeometryID >= 0) PXR_Audio_Spatializer_Context.Instance.SetMeshConfig(staticGeometryID, ref meshConfig, propertyMask); propertyMask = 0; } } public void UpdateAbsorptionMultiband(float[] absorptions) { meshConfig.materialType = AcousticsMaterial.Custom; meshConfig.absorption.v0 = Material.absorption[0] = absorptions[0]; meshConfig.absorption.v1 = Material.absorption[1] = absorptions[1]; meshConfig.absorption.v2 = Material.absorption[2] = absorptions[2]; meshConfig.absorption.v3 = Material.absorption[3] = absorptions[3]; propertyMask |= (uint)MeshProperty.Material | (uint)MeshProperty.Absorption; } public void UpdateScattering(float scattering) { meshConfig.materialType = AcousticsMaterial.Custom; meshConfig.scattering = Material.scattering = scattering; propertyMask |= (uint)MeshProperty.Material | (uint)MeshProperty.Scattering; } public void UpdateTransmission(float transmission) { meshConfig.materialType = AcousticsMaterial.Custom; meshConfig.transmission = Material.transmission = transmission; propertyMask |= (uint)MeshProperty.Material | (uint)MeshProperty.Transmission; } public void UpdateMaterialType(PXR_Audio.Spatializer.AcousticsMaterial materialType) { meshConfig.materialType = materialType; propertyMask |= (uint)MeshProperty.Material; } private void GetAllMeshFilter(Transform transform, bool includeChildren, List meshFilterList, bool isStatic, LayerMask layerMask) { if (includeChildren) { int childCount = transform.childCount; for (int i = 0; i < childCount; i++) { var childTransform = transform.GetChild(i); if (childTransform.GetComponent() == null) { GetAllMeshFilter(childTransform.transform, includeChildren, meshFilterList, isStatic, layerMask); } } } // Gather this mesh only when // 1. Its isStatic flag is equal to our requirement // 2. Its layer belongs to layerMask set if (((1 << transform.gameObject.layer) & layerMask) != 0) { var meshFilterArray = transform.GetComponents(); // cases we don't add to mesh filter list // 1. meshFilter.sharedmesh == null // 2. meshFilter.sharedmesh.isReadable == false if (meshFilterArray != null) { foreach (var meshFilter in meshFilterArray) { if (meshFilter != null && meshFilter.sharedMesh != null && ( (isStatic && (transform.gameObject.isStatic || !meshFilter.sharedMesh.isReadable)) || (!isStatic && (!transform.gameObject.isStatic && meshFilter.sharedMesh.isReadable)) )) { meshFilterList.Add(meshFilter); } } } } } private static Mesh CombineMeshes(List meshFilterList, Transform rootTransform) { if (meshFilterList.Count == 1) return meshFilterList[0].mesh; Mesh combinedMesh = new Mesh { name = "combined meshes", indexFormat = UnityEngine.Rendering.IndexFormat.UInt32 }; var combinedVertices = Array.Empty(); var combinedIndices = Array.Empty(); // Accumulate combined vertices buffer size foreach (var meshFilter in meshFilterList) { int vertexOffset = combinedVertices.Length; combinedVertices = combinedVertices.Concat(meshFilter.sharedMesh.vertices).ToArray(); int vertexSegmentEnd = combinedVertices.Length; var toWorld = rootTransform.worldToLocalMatrix * meshFilter.transform.localToWorldMatrix; for (int i = vertexOffset; i < vertexSegmentEnd; ++i) { combinedVertices[i] = toWorld.MultiplyPoint3x4(combinedVertices[i]); } var trianglesStartIdx = combinedIndices.Length; combinedIndices = combinedIndices.Concat(meshFilter.sharedMesh.triangles).ToArray(); var trianglesEndIdx = combinedIndices.Length; for (var i = trianglesStartIdx; i < trianglesEndIdx; ++i) { combinedIndices[i] += vertexOffset; } } combinedMesh.vertices = combinedVertices; combinedMesh.triangles = combinedIndices; combinedMesh.RecalculateNormals(); return combinedMesh; } private static float[] FlattenVerticesBuffer(Vector3[] verticesBuffer) { float[] vertices = new float[verticesBuffer.Length * 3]; int index = 0; foreach (Vector3 vertex in verticesBuffer) { vertices[index++] = vertex.x; vertices[index++] = vertex.y; vertices[index++] = vertex.z; } return vertices; } /// /// Submit non-static mesh of this geometry and its material into spatializer engine context /// /// Result of static mesh submission public PXR_Audio.Spatializer.Result SubmitMeshToContext(bool showLog = true) { // find all meshes var meshFilterList = new List(); GetAllMeshFilter(transform, includeChildren, meshFilterList, false, ~0); // Combine all meshes Mesh combinedMesh = CombineMeshes(meshFilterList, transform); // flatten vertices buffer into a float array float[] vertices = FlattenVerticesBuffer(combinedMesh.vertices); meshConfig = new MeshConfig(enabled, Material, transform.localToWorldMatrix); // Submit all meshes PXR_Audio.Spatializer.Result result = PXR_Audio_Spatializer_Context.Instance.SubmitMeshWithConfig( vertices, vertices.Length / 3, combinedMesh.triangles, combinedMesh.triangles.Length / 3, ref meshConfig, ref geometryId); if (showLog) { if (result != Result.Success) Debug.LogError("Failed to submit audio mesh: " + gameObject.name + ", Error code is: " + result); else Debug.LogFormat("Submitted geometry #{0}, gameObject name is {1}", geometryId.ToString(), name); } if (result == Result.Success) currentContextUuid = PXR_Audio_Spatializer_Context.Instance.UUID; return result; } /// /// Submit static mesh of this geometry and its material into spatializer engine context /// /// Result of static mesh submission public PXR_Audio.Spatializer.Result SubmitStaticMeshToContext(bool showLog = true) { PXR_Audio.Spatializer.Result result = Result.Success; if (bakedStaticMesh != null) { float[] tempVertices = FlattenVerticesBuffer(bakedStaticMesh.vertices); meshConfig = new MeshConfig(enabled, Material, transform.localToWorldMatrix); result = PXR_Audio_Spatializer_Context.Instance.SubmitMeshWithConfig(tempVertices, bakedStaticMesh.vertices.Length, bakedStaticMesh.triangles, bakedStaticMesh.triangles.Length / 3, ref meshConfig, ref staticGeometryID); if (showLog) { if (result != Result.Success) Debug.LogError("Failed to submit static audio mesh: " + gameObject.name + ", Error code is: " + result); else Debug.LogFormat("Submitted static geometry #{0}, gameObject name is {1}", staticGeometryID.ToString(), name); } } if (result == Result.Success) currentContextUuid = PXR_Audio_Spatializer_Context.Instance.UUID; return result; } public Result UpdateMeshInContext() { // find all meshes var meshFilterList = new List(); GetAllMeshFilter(transform, includeChildren, meshFilterList, false, ~0); // Combine all meshes Mesh combinedMesh = CombineMeshes(meshFilterList, transform); // flatten vertices buffer into a float array float[] vertices = FlattenVerticesBuffer(combinedMesh.vertices); meshConfig = new MeshConfig(enabled, Material, transform.localToWorldMatrix); // Submit all meshes Result result = PXR_Audio_Spatializer_Context.Instance.UpdateMesh(geometryId, vertices, vertices.Length / 3, combinedMesh.triangles, combinedMesh.triangles.Length / 3, ref meshConfig, ref geometryId); if (result == Result.Success) currentContextUuid = PXR_Audio_Spatializer_Context.Instance.UUID; return result; } #if UNITY_EDITOR public int BakeStaticMesh(LayerMask layerMask) { List meshList = new List(); GetAllMeshFilter(transform, includeChildren, meshList, true, meshBakingLayerMask); SerializedObject serializedObject = new SerializedObject(this); if (meshList.Count == 0) { bakedStaticMesh = null; } else { bakedStaticMesh = CombineMeshes(meshList, transform); bakedStaticMesh.name = "baked mesh for ygg"; } serializedObject.FindProperty("bakedStaticMesh").objectReferenceValue = bakedStaticMesh; if (bakedStaticMesh != null) { System.IO.Directory.CreateDirectory("Assets/Resources/PxrAudioSpatializerBakedSceneMeshes/"); if (!string.IsNullOrEmpty(currentBakedStaticMeshAssetPath)) { AssetDatabase.DeleteAsset(currentBakedStaticMeshAssetPath); } currentBakedStaticMeshAssetPath = "Assets/Resources/PxrAudioSpatializerBakedSceneMeshes/" + name + "_" + GetInstanceID() + "_" + System.DateTime.UtcNow.ToBinary() + ".yggmesh"; serializedObject.FindProperty("currentBakedStaticMeshAssetPath").stringValue = currentBakedStaticMeshAssetPath; AssetDatabase.CreateAsset(bakedStaticMesh, currentBakedStaticMeshAssetPath); AssetDatabase.SaveAssets(); } serializedObject.ApplyModifiedProperties(); return meshList.Count; } public void ClearBakeStaticMesh() { SerializedObject serializedObject = new SerializedObject(this); bakedStaticMesh = null; serializedObject.FindProperty("bakedStaticMesh").objectReferenceValue = null; if (!string.IsNullOrEmpty(currentBakedStaticMeshAssetPath)) { AssetDatabase.DeleteAsset(currentBakedStaticMeshAssetPath); currentBakedStaticMeshAssetPath = null; serializedObject.FindProperty("currentBakedStaticMeshAssetPath").stringValue = currentBakedStaticMeshAssetPath; } serializedObject.ApplyModifiedProperties(); } #endif public void OnDrawGizmos() { if (visualizeMeshInEditor) { // Visualize non-static meshes // find all MeshFilter var meshFilterList = new List(); GetAllMeshFilter(transform, includeChildren, meshFilterList, false, ~0); for (int i = 0; i < meshFilterList.Count; i++) { var mesh = meshFilterList[i].sharedMesh; var transform = meshFilterList[i].transform; Gizmos.DrawWireMesh(mesh, transform.position, transform.rotation, transform.localScale); } // Visualize baked static meshes if (isStaticMeshBaked) { Color colorBackUp = Gizmos.color; Color c; c.r = 0.0f; c.g = 0.7f; c.b = 0.0f; c.a = 1.0f; Gizmos.color = c; var gizmosMatrixBackup = Gizmos.matrix; Gizmos.matrix = transform.localToWorldMatrix; Gizmos.DrawWireMesh(bakedStaticMesh); Gizmos.color = colorBackUp; Gizmos.matrix = gizmosMatrixBackup; } } } }