/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /************************************************************************************ * Filename : MetaXRAcousticGeometry.cs * Content : Geometry Functions Attach to a game object with meshes and material scripts to create geometry NOTE: ensure that Oculus Spatialization is enabled for AudioSource components ***********************************************************************************/ #define INCLUDE_TERRAIN_TREES using Meta.XR.Acoustics; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Profiling; using UnityEngine.Serialization; using Native = MetaXRAcousticNativeInterface; /// \brief Class that allows mesh analysis to precompute an Acoustic Geometry /// \see MetaXRAcousticNativeInterface internal class MetaXRAcousticGeometry : MonoBehaviour { internal static bool AUTO_VALIDATE = true; internal const string FILE_EXTENSION = "xrageo"; internal static int EnabledGeometryCount = 0; internal static event Action OnAnyGeometryEnabled = () => { }; //------- // PUBLIC [SerializeField] [FormerlySerializedAs("relativeFilePath_")] private string relativeFilePath = ""; #if UNITY_EDITOR internal string RelativeFilePath => string.IsNullOrEmpty(relativeFilePath) ? GenerateSuggestedPath() : relativeFilePath; #else internal string RelativeFilePath => relativeFilePath; #endif /// \brief Absolute path to the serialized mesh file that holds the preprocessed mesh geometry. /// This path should be absolute and somewhere inside Application.dataPath directory. internal string AbsoluteFilePath { get => Path.GetFullPath(Path.Combine(Application.dataPath, RelativeFilePath)); set { string sanitizedPath = value.Replace('\\', '/'); // Make the path relative to the Assets directory. if (sanitizedPath.StartsWith(Application.dataPath)) relativeFilePath = sanitizedPath.Substring(Application.dataPath.Length + 1); else Debug.LogError($"invalid path {value}, outside application path {Application.dataPath}", gameObject); } } /// \brief If this is set, then the serialized acoustic geometry file (.xrageo) is used as the mesh data source. [SerializeField] internal bool FileEnabled = true; /// \brief This button chooses whether or not child meshes of the GameObject where the geometry script is attached are included in the acoustic geometry. /// This option can be used to automatically combine all meshes within an object hierarchy into a single optimized acoustic geometry. /// This will be faster for ray tracing and produce better quality diffraction than many smaller meshes. This is typically used for the static meshes in a scene. [SerializeField] internal bool IncludeChildMeshes = true; /// The flags for how the mesh is computed. [SerializeField] internal MeshFlags Flags = MeshFlags.ENABLE_SIMPLIFICATION; internal bool EnableSimplification { get => (Flags & MeshFlags.ENABLE_SIMPLIFICATION) != 0; set { if (value) Flags |= MeshFlags.ENABLE_SIMPLIFICATION; else Flags &= ~MeshFlags.ENABLE_SIMPLIFICATION; } } /// \brief A button that chooses whether or not diffraction information should be computed for a mesh. This enables sound to propagate around that mesh. /// If disabled, no diffraction will occur around the mesh and the direct sound will be completely occluded when the source is not visible. /// It may be useful to only enable diffraction on large meshes (e.g. scene environment geometry), but to disable it on small props and clutter objects. /// This can improve the performance by reducing the total number of edges in the scene. internal bool EnableDiffraction { get => (Flags & MeshFlags.ENABLE_DIFFRACTION) != 0; set { if (value) Flags |= MeshFlags.ENABLE_DIFFRACTION; else Flags &= ~MeshFlags.ENABLE_DIFFRACTION; } } /// \brief The maximum allowed error for the automatic acoustic geometry simplification. /// This control specifies an error threshold in meters (regardless of which units the game engine uses). /// A relatively large error threshold can be used to reduce the geometry complexity (memory size and runtime ray tracing cost). /// The default error threshold is 0.1, i.e. 10 cm. The threshold may be increased further (up to around 0.5 meters) without any problems in most cases. [SerializeField] private float maxSimplifyError = 0.1f; internal float MaxSimplifyError { get => maxSimplifyError; set { #if UNITY_EDITOR if (value < 0) throw new ArgumentOutOfRangeException("Maximum simplification error must be >= 0."); #endif maxSimplifyError = Math.Max(value, 0.0f); } } /// \brief The minimum angle (degrees) that there must be between two adjacent face normals for their edge to be marked as diffracting. [SerializeField] private float minDiffractionEdgeAngle = 1.0f; internal float MinDiffractionEdgeAngle { get => minDiffractionEdgeAngle; set { #if UNITY_EDITOR if (value < 0) throw new ArgumentOutOfRangeException("Minimum diffraction edge angle must be >= 0."); if (value > 180.0) throw new ArgumentOutOfRangeException("Minimum diffraction edge angle must be <= 180."); #endif minDiffractionEdgeAngle = Math.Clamp(value, 0.0f, 180.0f); } } /// \brief The minimum length in meters that an edge should have for it to be marked as diffracting. [SerializeField] private float minDiffractionEdgeLength = 0.01f; internal float MinDiffractionEdgeLength { get => minDiffractionEdgeLength; set { #if UNITY_EDITOR if (value < 0) throw new ArgumentOutOfRangeException("Minimum diffraction edge length must be >= 0."); #endif minDiffractionEdgeLength = Math.Max(value, 0.0f); } } /// \brief The maximum distance in meters that a diffraction flag extends out from the edge. [SerializeField] private float flagLength = 1.0f; internal float FlagLength { get => flagLength; set { #if UNITY_EDITOR if (value < 0) throw new ArgumentOutOfRangeException("flag length must be greater than 0."); #endif flagLength = value; } } /// \brief The Level of Detail to use for acoustics, the higher the LOD the less polygons. Typically the highest LOD will be sufficient and the most efficient. [SerializeField] private int lodSelection = 0; internal int LodSelection { get => lodSelection; set => lodSelection = value; } /// \brief If enabled the acoustic geometry will be computed using the physics Mesh Colliders. If enabled, Meta XR Acoustic Material scripts will be ignored. /// The mapping between Mesh Colliders and Meta XR Acoustic Material Properties can be configured in **Project Settings > Meta XR Acoustics** [SerializeField] private bool useColliders = false; internal bool UseColliders { get => useColliders; set => useColliders = value; } /// \brief If enabled, the overrideExcludeTags will be used to exclude objects from the baked Acoustic Geometry instead of the project setting Exclude Tags [SerializeField] private bool overrideExcludeTagsEnabled = false; internal bool OverrideExcludeTagsEnabled { get => overrideExcludeTagsEnabled; set => overrideExcludeTagsEnabled = value; } /// \brief The list of tags to be used for excluding objects from the baked Acoustic Geometry instead of the project setting Exclude Tags. [SerializeField] private string[] overrideExcludeTags = null; internal string[] OverrideExcludeTags { get => overrideExcludeTags; set => overrideExcludeTags = value; } internal string[] ExcludeTags => OverrideExcludeTagsEnabled ? OverrideExcludeTags : MetaXRAcousticSettings.Instance.ExcludeTags; //------- // PRIVATE [NonSerialized] internal IntPtr geometryHandle = IntPtr.Zero; [NonSerialized] private bool isLoaded = false; internal bool IsLoaded => isLoaded; [NonSerialized] private int vertexCount = -1; internal int VertexCount => vertexCount; // Disable unused variable warning that occurs in builds. // This preserves serialization layout, while using UNITY_EDITOR // to remove the variable would break the serialization layout. #pragma warning disable 0169 [SerializeField] private Color[] materialColors; /// \brief A hash code of the game objects contributing to the current serialized acoustic geometry, for detecting changes. [SerializeField] private Hash128 HierarchyHash; #pragma warning restore 0169 #if UNITY_EDITOR [NonSerialized] private IMaterialDataProvider[] gizmoMaterialMapping; [NonSerialized] private int[] gizmoVertexMaterialIndices; [NonSerialized] private uint[] materialIndices; #endif //------- // PUBLIC STATIC internal const int Success = 0; /// /// If script is attached to a gameobject, it will try to create geometry /// void Awake() { StartInternal(); } internal bool StartInternal() { if (!CreatePropagationGeometry()) return false; // Make sure the geometry has current transform matrix. ApplyTransform(); return true; } /// /// Call this function to create geometry handle /// internal bool CreatePropagationGeometry() { if (geometryHandle != IntPtr.Zero) { Debug.LogWarning("Tried to initialize geometry twice, destroying stale copy", gameObject); DestroyPropagationGeometry(); } if (geometryHandle != IntPtr.Zero) { Debug.LogError("Unable to clean up stale geometry", gameObject); return false; } if (Native.Interface.CreateAudioGeometry(out geometryHandle) != Success) { Debug.LogError("Unable to create geometry handle", gameObject); return false; } #if UNITY_EDITOR if (!Application.isPlaying) { if (!GatherGeometryEditor(geometryHandle, gameObject, gameObject.transform.worldToLocalMatrix)) return false; } else # endif if (FileEnabled) { if (string.IsNullOrEmpty(relativeFilePath)) { Debug.LogError("No file set, make sure to Bake Mesh to File", gameObject); return false; } else { if (!ReadFile()) return false; } } else { if (gameObject.isStatic) { Debug.LogError("Static geometry requires \"File Enabled\"", gameObject); return false; } else { if (!GatherGeometryRuntime()) return false; } } return true; } void IncrementEnabledGeometryCount() { ++EnabledGeometryCount; if (EnabledGeometryCount == 1) OnAnyGeometryEnabled(); } void DecrementEnabledGeometryCount() => --EnabledGeometryCount; /// Called when enabled. void OnEnable() { if (geometryHandle == IntPtr.Zero || (!isLoaded && FileEnabled)) return; Debug.Log($"Enabling Geometry: {relativeFilePath}", gameObject); Native.Interface.AudioGeometrySetObjectFlag(geometryHandle, ObjectFlags.ENABLED, true); ApplyTransform(); Native.Interface.AudioGeometrySetObjectFlag(geometryHandle, ObjectFlags.STATIC, gameObject.isStatic); IncrementEnabledGeometryCount(); } /// Called when disabled. void OnDisable() { if (geometryHandle == IntPtr.Zero) return; Debug.Log($"Disabling Geometry: {relativeFilePath}", gameObject); Native.Interface.AudioGeometrySetObjectFlag(geometryHandle, ObjectFlags.ENABLED, false); ApplyTransform(); Native.Interface.AudioGeometrySetObjectFlag(geometryHandle, ObjectFlags.STATIC, gameObject.isStatic); DecrementEnabledGeometryCount(); } /// Update the world transform. /// Do this in LateUpdate() instead of Update() because it works better with animations. private void LateUpdate() { if (geometryHandle == IntPtr.Zero) return; if (transform.hasChanged) { ApplyTransform(); // Reset dirty bit. transform.hasChanged = false; } } private void ApplyTransform() { if (geometryHandle == IntPtr.Zero) return; Native.Interface.AudioGeometrySetTransform(geometryHandle, transform.localToWorldMatrix); } /// /// Call when destroyed /// private void OnDestroy() { DestroyInternal(); } internal bool DestroyInternal() { if (!DestroyPropagationGeometry()) return false; return true; } private bool DestroyPropagationGeometry() { lock (this) { if (geometryHandle != IntPtr.Zero && Native.Interface.DestroyAudioGeometry(geometryHandle) != Success) { Debug.LogError("Unable to destroy geometry", gameObject); return false; } geometryHandle = IntPtr.Zero; return true; } } // // FUNCTIONS FOR UPLOADING MESHES VIA GAME OBJECT // static int terrainDecimation = 4; private struct MeshMaterial { internal Mesh mesh; internal Transform meshTransform; internal IMaterialDataProvider[] meshMaterials; } private struct TerrainMaterial { internal Terrain terrain; internal IMaterialDataProvider[] terrainMaterials; internal Mesh[] treePrototypeMeshes; } internal interface ITransformVisitor { System.Object visit(Transform transform, System.Object userData); } interface IGatherer : ITransformVisitor { List Meshes { get; } List Terrains { get; } } class MeshGatherer : IGatherer { internal MeshGatherer(bool ignoreStatic) { this.ignoreStatic = ignoreStatic; } public System.Object visit(Transform transform, System.Object parentData) { var currentMaterials = parentData as IMaterialDataProvider[]; MeshFilter[] meshes = transform.GetComponents(); Terrain[] terrains = transform.GetComponents(); var activeMaterials = transform.GetComponents().Where(x => x.enabled); IMaterialDataProvider[] materials = Array.ConvertAll(activeMaterials.ToArray(), x => x); // Initialize the current material array to a new array if there are any new materials. if (materials != null && materials.Length > 0) { // Determine the length of the material array. int maxLength = materials.Length; if (currentMaterials != null && currentMaterials.Length > maxLength) maxLength = currentMaterials.Length; Meta.XR.Acoustics.IMaterialDataProvider[] newMaterials = new Meta.XR.Acoustics.IMaterialDataProvider[maxLength]; // Copy the previous materials into the new array. if (currentMaterials != null) { for (int i = materials.Length; i < maxLength; i++) newMaterials[i] = currentMaterials[i]; } currentMaterials = newMaterials; // Copy the current materials. for (int i = 0; i < materials.Length; i++) currentMaterials[i] = materials[i]; } // Gather the meshes. foreach (MeshFilter meshFilter in meshes) { Mesh sharedMesh = meshFilter.sharedMesh; if (sharedMesh == null) continue; if (ignoreStatic && (!sharedMesh.isReadable || transform.gameObject.isStatic)) { Debug.LogError($"Mesh: {meshFilter.gameObject.name} not readable. Use \"File Enabled\" for static geometry", transform); ++ignoredMeshCount; continue; } this.meshes.Add(new MeshMaterial() { mesh = sharedMesh, meshTransform = transform, meshMaterials = currentMaterials }); } // Gather the terrains. foreach (Terrain terrain in terrains) this.terrains.Add(new TerrainMaterial() { terrain = terrain, terrainMaterials = currentMaterials }); return currentMaterials; } private List meshes = new List(); public List Meshes { get => meshes; } private List terrains = new List(); public List Terrains { get => terrains; } internal int ignoredMeshCount = 0; internal bool ignoreStatic; } class ColliderGatherer : IGatherer { public System.Object visit(Transform transform, System.Object parentData) { var activeMaterials = transform.GetComponents().Where(x => x.enabled); IMaterialDataProvider[] materials = Array.ConvertAll(activeMaterials.ToArray(), x => x); MeshCollider[] colliders = transform.GetComponents(); foreach (MeshCollider mc in colliders) { if (mc.sharedMesh == null) continue; if (materials.Length == 0) { // No MaterialComponents found, see if there is a mapping for PhysicMaterials MetaXRAcousticMaterialProperties mat = MetaXRAcousticMaterialMapping.Instance.findAcousticMaterial(mc.sharedMaterial); if (mat != null) { materials = new IMaterialDataProvider[] { mat }; #if META_XR_ACOUSTIC_INFO Debug.Log($"Found PhysicMaterial {mc.sharedMaterial?.name} => {mat.name}"); #endif } } meshes.Add(new MeshMaterial() { mesh = mc.sharedMesh, meshTransform = transform, meshMaterials = materials }); } BoxCollider[] boxColliders = transform.GetComponents(); foreach (BoxCollider bc in boxColliders) { Mesh box = new Mesh(); Vector3[] verts = new Vector3[8]; verts[0] = bc.center + Vector3.Scale(bc.size * 0.5f, new Vector3(1f, 1f, 1f)); verts[1] = bc.center + Vector3.Scale(bc.size * 0.5f, new Vector3(1f, 1f, -1f)); verts[2] = bc.center + Vector3.Scale(bc.size * 0.5f, new Vector3(1f, -1f, 1f)); verts[3] = bc.center + Vector3.Scale(bc.size * 0.5f, new Vector3(1f, -1f, -1f)); verts[4] = bc.center + Vector3.Scale(bc.size * 0.5f, new Vector3(-1f, 1f, 1f)); verts[5] = bc.center + Vector3.Scale(bc.size * 0.5f, new Vector3(-1f, 1f, -1f)); verts[6] = bc.center + Vector3.Scale(bc.size * 0.5f, new Vector3(-1f, -1f, 1f)); verts[7] = bc.center + Vector3.Scale(bc.size * 0.5f, new Vector3(-1f, -1f, -1f)); int[] indices = new int[24]{ 1,0,2,3, // left 0,4,6,2, // front 4,5,7,6, // right 5,1,3,7, // back 1,5,4,0, // top 2,6,7,3 // bottom }; box.vertices = verts; box.SetIndices(indices, MeshTopology.Quads, 0); if (materials.Length == 0) { // No MaterialComponents found, see if there is a mapping for PhysicMaterials MetaXRAcousticMaterialProperties mat = MetaXRAcousticMaterialMapping.Instance.findAcousticMaterial(bc.sharedMaterial); if (mat != null) { materials = new IMaterialDataProvider[] { mat }; #if META_XR_ACOUSTIC_INFO Debug.Log($"Found PhysicMaterial {bc.sharedMaterial?.name} => {mat.name}"); #endif } } meshes.Add(new MeshMaterial() { mesh = box, meshTransform = transform, meshMaterials = materials }); } return null; } private List meshes = new List(); public List Meshes { get => meshes; } private List terrains = new List(); public List Terrains { get => terrains; } } // Search an LOD group to determine if an object is used by any renderer it contains. private static bool isObjectUsedByLODGroup(GameObject obj, LODGroup lod) { LOD[] lods = lod.GetLODs(); for (int i = 0; i < lods.Length; i++) { Renderer[] lodRenderers = lods[i].renderers; for (int j = 0; j < lodRenderers.Length; j++) { if (lodRenderers[j].gameObject == obj) { return true; } } } return false; } private static void traverseMeshHierarchy(GameObject obj, bool includeChildren, string[] excludeTags, bool parentWasExcluded, int lodSelection, LODGroup parentLOD, ITransformVisitor visitor, System.Object parentData = null) { if (!obj.activeInHierarchy) { #if META_XR_ACOUSTIC_INFO Debug.Log($"Skipping inactive Object {obj.name}", obj); #endif return; } // Check for LOD. If present, use only the highest LOD and don't recurse to children. // Without this, we can accidentally get all LODs merged together. LODGroup lodGroup = obj.GetComponent(typeof(LODGroup)) as LODGroup; if (lodGroup != null) { #if META_XR_ACOUSTIC_INFO Debug.Log($"LOD Group detected during acoustic geometry traversal"); #endif LOD[] lods = lodGroup.GetLODs(); if (lods.Length > 0) { bool isLodTrivial = (lods.Length == 1 && lods[0].renderers.Length == 1); if (isLodTrivial) { obj = lods[0].renderers[0].gameObject; } else { // Get renderers for user selected LOD. Note they can select any value, so clamp to the known range int lodGroupToUse = Mathf.Clamp(lodSelection, 0, lods.Length - 1); Renderer[] lodRenderers = lods[lodGroupToUse].renderers; #if META_XR_ACOUSTIC_INFO Debug.Log($"Using LOD Group {lodGroupToUse} for acoustic geometry which has {lodRenderers.Length} renderers", obj); #endif // Get and add the game object for every renderer at this LOD level // Some meshes split their mesh into multiple cells, which is why we can't just use a single renderer for (int i = 0; i < lodRenderers.Length; i++) { if (lodRenderers[i] != null) { if (lodRenderers[i].gameObject == obj) continue; // avoid infinite recursion traverseMeshHierarchy(lodRenderers[i].gameObject, includeChildren, excludeTags, parentWasExcluded, lodSelection, null, visitor, parentData); } } } parentLOD = lodGroup; } } // Determine if this object should be visited. // Objects can be excluded based on tags, parent exclusion, or if the object is part of an LOD group. bool shouldVisit = true; bool nonLODMesh = (parentLOD != lodGroup) && (parentLOD != null && isObjectUsedByLODGroup(obj, parentLOD)); if (excludeTags.Contains(obj.tag) || parentWasExcluded || nonLODMesh) { MetaXRAcousticMaterial mat = obj.GetComponent(); if (mat == null || !mat.enabled) { #if META_XR_ACOUSTIC_INFO Debug.Log($"Skipping Object {obj.name} based on exclude tag: {obj.tag}", obj); #endif shouldVisit = false; } else { #if META_XR_ACOUSTIC_INFO Debug.Log($"Override exclude tag {obj.tag} due to presence of acoustic material in child {mat.gameObject.name}", obj); #endif shouldVisit = true; } } if (shouldVisit) parentData = visitor.visit(obj.transform, parentData); // Traverse the child transforms. if (includeChildren) { foreach (Transform child in obj.transform) { if (child.GetComponent() == null) traverseMeshHierarchy(child.gameObject, includeChildren, excludeTags, !shouldVisit, lodSelection, parentLOD, visitor, parentData); #if META_XR_ACOUSTIC_INFO else Debug.Log($"Skipping child: {child.name}, it has it's own {nameof(MetaXRAcousticGeometry)} component"); #endif } } } #if UNITY_EDITOR private bool GatherGeometryEditor(IntPtr geometryHandle, GameObject meshObject, Matrix4x4 worldToLocal) { return GatherGeometryInternal(geometryHandle, meshObject, worldToLocal, false, out int unused); } #endif private bool GatherGeometryInternal(IntPtr geometryHandle, GameObject meshObject, Matrix4x4 worldToLocal, bool ignoreStatic, out int ignoredMeshCount) { ignoredMeshCount = 0; // Get the child mesh objects. IGatherer gatherer; if (useColliders) gatherer = new ColliderGatherer(); else gatherer = new MeshGatherer(ignoreStatic); traverseMeshHierarchy(meshObject, IncludeChildMeshes, ExcludeTags, false, lodSelection, null, gatherer); //*********************************************************************** // Count the number of vertices and indices. int totalVertexCount = 0; uint totalIndexCount = 0; int totalFaceCount = 0; int totalMaterialCount = 0; foreach (MeshMaterial m in gatherer.Meshes) updateCountsForMesh(ref totalVertexCount, ref totalIndexCount, ref totalFaceCount, ref totalMaterialCount, m.mesh); IMaterialDataProvider[] treeMaterials = new IMaterialDataProvider[1]; for (int i = 0; i < gatherer.Terrains.Count; ++i) { TerrainMaterial t = gatherer.Terrains[i]; TerrainData terrain = t.terrain.terrainData; #if UNITY_2019_3_OR_NEWER int w = terrain.heightmapResolution; int h = terrain.heightmapResolution; #else int w = terrain.heightmapWidth; int h = terrain.heightmapHeight; #endif int wRes = (w - 1) / terrainDecimation + 1; int hRes = (h - 1) / terrainDecimation + 1; int vertexCount = wRes * hRes; int indexCount = (wRes - 1) * (hRes - 1) * 6; totalMaterialCount++; totalVertexCount += vertexCount; totalIndexCount += (uint)indexCount; totalFaceCount += indexCount / 3; #if INCLUDE_TERRAIN_TREES TreePrototype[] treePrototypes = terrain.treePrototypes; if (treePrototypes.Length != 0) { if (treeMaterials[0] == null) { // Use last material attached to terrain for foliage treeMaterials[0] = t.terrainMaterials.Last(); } t.treePrototypeMeshes = new Mesh[treePrototypes.Length]; // Assume the sharedMesh with the lowest vertex is the lowest LOD for (int j = 0; j < treePrototypes.Length; ++j) { GameObject prefab = treePrototypes[j].prefab; MeshFilter[] meshFilters = prefab.GetComponentsInChildren(); int minVertexCount = int.MaxValue; int index = -1; for (int k = 0; k < meshFilters.Length; ++k) { int count = meshFilters[k].sharedMesh.vertexCount; if (count < minVertexCount) { minVertexCount = count; index = k; } } t.treePrototypeMeshes[j] = meshFilters[index].sharedMesh; } TreeInstance[] trees = terrain.treeInstances; foreach (TreeInstance tree in trees) { updateCountsForMesh(ref totalVertexCount, ref totalIndexCount, ref totalFaceCount, ref totalMaterialCount, t.treePrototypeMeshes[tree.prototypeIndex]); } gatherer.Terrains[i] = t; } #endif } //*********************************************************************** // Copy the mesh data. List tempVertices = new List(); List tempIndices = new List(); MeshGroup[] groups = new MeshGroup[totalMaterialCount]; float[] vertices = new float[totalVertexCount * 3]; int[] indices = new int[totalIndexCount]; int vertexOffset = 0; int indexOffset = 0; int groupOffset = 0; foreach (MeshMaterial m in gatherer.Meshes) { // Compute the combined transform to go from mesh-local to geometry-local space. Matrix4x4 matrix = worldToLocal * m.meshTransform.localToWorldMatrix; if (!uploadMeshFilter(tempVertices, tempIndices, groups, vertices, indices, ref vertexOffset, ref indexOffset, ref groupOffset, m.mesh, m.meshMaterials, matrix)) return false; } foreach (TerrainMaterial t in gatherer.Terrains) { TerrainData terrain = t.terrain.terrainData; // Compute the combined transform to go from mesh-local to geometry-local space. Matrix4x4 matrix = worldToLocal * t.terrain.gameObject.transform.localToWorldMatrix; #if UNITY_2019_3_OR_NEWER int w = terrain.heightmapResolution; int h = terrain.heightmapResolution; #else int w = terrain.heightmapWidth; int h = terrain.heightmapHeight; #endif float[,] tData = terrain.GetHeights(0, 0, w, h); Vector3 meshScale = terrain.size; meshScale = new Vector3(meshScale.x / (w - 1) * terrainDecimation, meshScale.y, meshScale.z / (h - 1) * terrainDecimation); int wRes = (w - 1) / terrainDecimation + 1; int hRes = (h - 1) / terrainDecimation + 1; int vertexCount = wRes * hRes; int triangleCount = (wRes - 1) * (hRes - 1) * 2; // Initialize the group. groups[groupOffset].faceType = FaceType.TRIANGLES; groups[groupOffset].faceCount = (UIntPtr)triangleCount; groups[groupOffset].indexOffset = (UIntPtr)indexOffset; if (t.terrainMaterials != null && 0 < t.terrainMaterials.Length) groups[groupOffset].material = MetaXRAcousticMaterial.CreateMaterialNativeHandle(t.terrainMaterials[0].Data); else groups[groupOffset].material = IntPtr.Zero; // Build vertices and UVs for (int y = 0; y < hRes; y++) { for (int x = 0; x < wRes; x++) { int offset = (vertexOffset + y * wRes + x) * 3; Vector3 v = matrix.MultiplyPoint3x4(Vector3.Scale(meshScale, new Vector3(y, tData[x * terrainDecimation, y * terrainDecimation], x))); vertices[offset + 0] = v.x; vertices[offset + 1] = v.y; vertices[offset + 2] = v.z; } } // Build triangle indices: 3 indices into vertex array for each triangle for (int y = 0; y < hRes - 1; y++) { for (int x = 0; x < wRes - 1; x++) { // For each grid cell output two triangles indices[indexOffset + 0] = (vertexOffset + (y * wRes) + x); indices[indexOffset + 1] = (vertexOffset + ((y + 1) * wRes) + x); indices[indexOffset + 2] = (vertexOffset + (y * wRes) + x + 1); indices[indexOffset + 3] = (vertexOffset + ((y + 1) * wRes) + x); indices[indexOffset + 4] = (vertexOffset + ((y + 1) * wRes) + x + 1); indices[indexOffset + 5] = (vertexOffset + (y * wRes) + x + 1); indexOffset += 6; } } vertexOffset += vertexCount; groupOffset++; #if INCLUDE_TERRAIN_TREES TreeInstance[] trees = terrain.treeInstances; foreach (TreeInstance tree in trees) { Vector3 pos = Vector3.Scale(tree.position, terrain.size); Matrix4x4 treeLocalToWorldMatrix = t.terrain.gameObject.transform.localToWorldMatrix; treeLocalToWorldMatrix.SetColumn(3, treeLocalToWorldMatrix.GetColumn(3) + new Vector4(pos.x, pos.y, pos.z, 0.0f)); // TODO: tree rotation Matrix4x4 treeMatrix = worldToLocal * treeLocalToWorldMatrix; if (!uploadMeshFilter(tempVertices, tempIndices, groups, vertices, indices, ref vertexOffset, ref indexOffset, ref groupOffset, t.treePrototypeMeshes[tree.prototypeIndex], treeMaterials, treeMatrix)) return false; } #endif } if (totalVertexCount == 0) { string path = ((gameObject.scene != null) ? gameObject.scene.name : "") + ":" + string.Join("/", gameObject.GetComponentsInParent().Select(t => t.name).Reverse().ToArray()); Debug.LogError($"Geometry unable to upload mesh, vertex count is zero {path}", gameObject); return false; } Debug.Log($"Uploading mesh {name} with {totalVertexCount} vertices"); // Gather the mesh simplification parameters to pass to the upload MeshSimplification simplification = new MeshSimplification(); simplification.thisSize = (UIntPtr)Marshal.SizeOf(typeof(MeshSimplification)); simplification.flags = Flags; simplification.unitScale = 1; // Unity always uses 1 unit equals 1 meter simplification.maxError = MaxSimplifyError; simplification.minDiffractionEdgeAngle = MinDiffractionEdgeAngle; simplification.minDiffractionEdgeLength = MinDiffractionEdgeLength; simplification.flagLength = FlagLength; #if UNITY_EDITOR simplification.threadCount = (UIntPtr)0; // Use as many threads as CPUs #else simplification.threadCount = (UIntPtr)1; // Don't create any threads if not in editor. #endif // Upload mesh data int result = Native.Interface.AudioGeometryUploadSimplifiedMeshArrays(geometryHandle, vertices, totalVertexCount, indices, indices.Length, groups, groups.Length, ref simplification); Native.Interface.AudioGeometrySetObjectFlag(geometryHandle, ObjectFlags.ENABLED, isActiveAndEnabled); Native.Interface.AudioGeometrySetObjectFlag(geometryHandle, ObjectFlags.STATIC, gameObject.isStatic); // Clean up native handles foreach (var group in groups) { if (group.material != null) MetaXRAcousticMaterial.DestroyMaterialNativeHandle(group.material); } if (result == Success) { // TODO: add an empty mat for var materials = new List(); foreach (MeshMaterial m in gatherer.Meshes) { int added = 0; int subMeshCount = m.mesh.subMeshCount; int numRealMaterials = m.meshMaterials == null ? 0 : m.meshMaterials.Length; if (numRealMaterials != 0) { int amountToAdd = Mathf.Min(numRealMaterials, subMeshCount); for (added = 0; added < amountToAdd; ++added) materials.Add(m.meshMaterials[added]); // splat the last material on remaining submeshes for (added = amountToAdd; added < subMeshCount; ++added) materials.Add(m.meshMaterials[numRealMaterials - 1]); } else { for (int i = 0; i < subMeshCount; ++i) materials.Add(null); // default material } } foreach (TerrainMaterial t in gatherer.Terrains) { if (t.terrainMaterials != null && t.terrainMaterials.Length != 0) materials.AddRange(t.terrainMaterials); } #if UNITY_EDITOR gizmoMaterialMapping = materials.ToArray(); UpdateGizmoMesh(geometryHandle); HierarchyHash = ComputeHash(); #endif return true; } else { return false; } } private static bool uploadMeshFilter(List tempVertices, List tempIndices, MeshGroup[] groups, float[] vertices, int[] indices, ref int vertexOffset, ref int indexOffset, ref int groupOffset, Mesh mesh, IMaterialDataProvider[] materials, Matrix4x4 matrix) { // Get the mesh vertices. tempVertices.Clear(); mesh.GetVertices(tempVertices); // Copy the Vector3 vertices into a packed array of floats for the API. int meshVertexCount = tempVertices.Count; for (int i = 0; i < meshVertexCount; i++) { // Transform into the parent space. Vector3 v = matrix.MultiplyPoint3x4(tempVertices[i]); int offset = (vertexOffset + i) * 3; vertices[offset + 0] = v.x; vertices[offset + 1] = v.y; vertices[offset + 2] = v.z; } // Copy the data for each submesh. for (int i = 0; i < mesh.subMeshCount; i++) { MeshTopology topology = mesh.GetTopology(i); if (topology == MeshTopology.Triangles || topology == MeshTopology.Quads) { // Get the submesh indices. tempIndices.Clear(); mesh.GetIndices(tempIndices, i); int subMeshIndexCount = tempIndices.Count; // Copy and adjust the indices. for (int j = 0; j < subMeshIndexCount; j++) indices[indexOffset + j] = tempIndices[j] + vertexOffset; // Initialize the group. if (topology == MeshTopology.Triangles) { groups[groupOffset + i].faceType = FaceType.TRIANGLES; groups[groupOffset + i].faceCount = (UIntPtr)(subMeshIndexCount / 3); } else if (topology == MeshTopology.Quads) { groups[groupOffset + i].faceType = FaceType.QUADS; groups[groupOffset + i].faceCount = (UIntPtr)(subMeshIndexCount / 4); } groups[groupOffset + i].indexOffset = (UIntPtr)indexOffset; if (materials != null && materials.Length != 0) { int matIndex = i; if (matIndex >= materials.Length) matIndex = materials.Length - 1; groups[groupOffset + i].material = MetaXRAcousticMaterial.CreateMaterialNativeHandle(materials[matIndex].Data); } else { groups[groupOffset + i].material = IntPtr.Zero; } indexOffset += subMeshIndexCount; } } vertexOffset += meshVertexCount; groupOffset += mesh.subMeshCount; return true; } private static void updateCountsForMesh(ref int totalVertexCount, ref uint totalIndexCount, ref int totalFaceCount, ref int totalMaterialCount, Mesh mesh) { totalMaterialCount += mesh.subMeshCount; totalVertexCount += mesh.vertexCount; for (int i = 0; i < mesh.subMeshCount; i++) { MeshTopology topology = mesh.GetTopology(i); if (topology == MeshTopology.Triangles || topology == MeshTopology.Quads) { uint meshIndexCount = mesh.GetIndexCount(i); totalIndexCount += meshIndexCount; if (topology == MeshTopology.Triangles) totalFaceCount += (int)meshIndexCount / 3; else if (topology == MeshTopology.Quads) totalFaceCount += (int)meshIndexCount / 4; } } } internal bool GatherGeometryRuntime() { Debug.Log("Gathering geometry"); if (!GatherGeometryInternal(geometryHandle, gameObject, gameObject.transform.worldToLocalMatrix, ignoreStatic: Application.isPlaying, out int ignoredMeshCount)) return false; if (ignoredMeshCount != 0) { Debug.LogWarning( $"Failed to upload meshes, {ignoredMeshCount} static meshes ignored. Turn on \"File Enabled\" to process static meshes offline", gameObject); } return true; } #if UNITY_EDITOR internal void FixPathCaseMismatch() { string caseSensitivePath = MetaXRAudioUtils.GetCaseSensitivePathForFile(AbsoluteFilePath); if (AbsoluteFilePath != caseSensitivePath) { int trim = Application.dataPath.Length + 1; Debug.LogWarning($"File path case mismatch detected!\n old: {AbsoluteFilePath}\n new: {caseSensitivePath}"); AbsoluteFilePath = caseSensitivePath.Replace('\\', '/'); } } void OnValidate() { // GameObject is a non-instanced prefab, skip if (UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this)) return; if (vertexCount == -1 && !UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode && !UnityEditor.BuildPipeline.isBuildingPlayer) { vertexCount = 0; // set numVertices so we don't spin here if (AUTO_VALIDATE) { StartInternal(); DestroyInternal(); } } FixPathCaseMismatch(); } [NonSerialized] Mesh gizmoMesh; internal Mesh GizmoMesh => gizmoMesh; // exposed for tests internal Color DEFAULT_MATERIAL_COLOR1 => DEFAULT_MATERIAL_COLOR; private void UpdateGizmoMesh() => UpdateGizmoMesh(geometryHandle); readonly Color DEFAULT_MATERIAL_COLOR = Color.yellow; private void UpdateGizmoMeshColors() { if (gizmoMaterialMapping != null) materialColors = Array.ConvertAll(gizmoMaterialMapping.ToArray(), x => (x?.Data != null) ? x.Data.color : DEFAULT_MATERIAL_COLOR); Color[] vertexColors = new Color[gizmoVertexMaterialIndices.Length]; for (int i = 0; i < gizmoVertexMaterialIndices.Length; i++) { if (gizmoVertexMaterialIndices[i] >= materialColors.Length || i >= vertexColors.Length || i >= gizmoVertexMaterialIndices.Length) { Debug.LogError($"out of bounds: i={i}/{vertexColors.Length} ({gizmoVertexMaterialIndices.Length}) - [{i}]={gizmoVertexMaterialIndices[i]}"); return; } vertexColors[i] = materialColors[gizmoVertexMaterialIndices[i]]; } gizmoMesh.colors = vertexColors; } private void UpdateGizmoMesh(IntPtr handle) { gizmoMesh = null; if (handle == IntPtr.Zero) { Debug.LogError("Unable to update Gizmo: Geometry not loaded", gameObject); return; } if (Native.Interface.AudioGeometryGetSimplifiedMesh(handle, out float[] vertices, out uint[] indices, out materialIndices) != 0) return; vertexCount = vertices.Length / 3; if (vertexCount != 0) { gizmoMesh = new Mesh(); UnityEngine.Rendering.VertexAttributeDescriptor[] attributes = new UnityEngine.Rendering.VertexAttributeDescriptor[1] { new UnityEngine.Rendering.VertexAttributeDescriptor(UnityEngine.Rendering.VertexAttribute.Position, UnityEngine.Rendering.VertexAttributeFormat.Float32, 3) }; int vertexCount = vertices.Length / 3; gizmoMesh.SetVertexBufferParams(vertexCount, attributes); gizmoMesh.SetVertexBufferData(vertices, 0, 0, vertices.Length); gizmoMesh.SetIndexBufferParams(indices.Length, UnityEngine.Rendering.IndexFormat.UInt32); gizmoMesh.SetIndexBufferData(indices, 0, 0, indices.Length, UnityEngine.Rendering.MeshUpdateFlags.Default); UnityEngine.Rendering.SubMeshDescriptor desc = new UnityEngine.Rendering.SubMeshDescriptor(); desc.indexCount = indices.Length; desc.topology = MeshTopology.Triangles; desc.vertexCount = vertexCount; int triangleCount = indices.Length / 3; gizmoVertexMaterialIndices = new int[vertexCount]; for (int i = 0; i < triangleCount; i++) { gizmoVertexMaterialIndices[indices[(i * 3) + 0]] = (int)materialIndices[i]; gizmoVertexMaterialIndices[indices[(i * 3) + 1]] = (int)materialIndices[i]; gizmoVertexMaterialIndices[indices[(i * 3) + 2]] = (int)materialIndices[i]; } UpdateGizmoMeshColors(); gizmoMesh.SetSubMesh(0, desc); gizmoMesh.RecalculateNormals(); Debug.Log($"Simplified mesh with {vertexCount} vertices", gameObject); } } /// Draw the editor debug view of the control. [UnityEditor.DrawGizmo(UnityEditor.GizmoType.NotInSelectionHierarchy | UnityEditor.GizmoType.Pickable | UnityEditor.GizmoType.Selected)] void OnDrawGizmos() => DrawDebug(false); /// Draw the editor debug view of the control when selected [UnityEditor.DrawGizmo(UnityEditor.GizmoType.NotInSelectionHierarchy | UnityEditor.GizmoType.Pickable | UnityEditor.GizmoType.Selected)] void OnDrawGizmosSelected() => DrawDebug(true); [UnityEditor.DrawGizmo(UnityEditor.GizmoType.NotInSelectionHierarchy | UnityEditor.GizmoType.Pickable | UnityEditor.GizmoType.Selected)] private void DrawDebug(bool selected) { if (gizmoMesh != null) { UpdateGizmoMeshColors(); var color = new Color(1.0f, 1.0f, 1.0f, selected ? 0.5f : 0.1f); // Taken from: https://docs.unity3d.com/ScriptReference/GL.html Shader shader = Shader.Find("Hidden/Internal-Colored"); var mat = new Material(shader); mat.hideFlags = HideFlags.HideAndDontSave; // Turn on alpha blending mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); // Turn backface culling off mat.SetInt("_Cull", (int)UnityEngine.Rendering.CullMode.Off); // Turn off depth writes mat.SetInt("_ZWrite", 0); // X-Ray vision //mat.SetInt("_ZTest", 0); // Avoid Z-fighting mat.SetFloat("_ZBias", -1); mat.SetColor("_Color", color); mat.SetPass(0); if (Event.current.type != EventType.Repaint) { // skip drawing gizmo on mouse events to avoid messing up selection return; } GL.wireframe = true; Graphics.DrawMeshNow(gizmoMesh, transform.localToWorldMatrix); GL.wireframe = false; color.a *= 0.5f; mat.color = color; mat.SetPass(0); Graphics.DrawMeshNow(gizmoMesh, transform.localToWorldMatrix); } } class AgeChecker : ITransformVisitor { internal AgeChecker(DateTime timeStamp, bool useColliders) { this.timeStamp = timeStamp; this.useColliders = useColliders; } public System.Object visit(Transform transform, System.Object parentData) { var activeMaterials = transform.GetComponents().Where(x => x.enabled); IMaterialDataProvider[] materials = Array.ConvertAll(activeMaterials.ToArray(), x => x); if ( useColliders ) { MeshCollider[] meshColliders = transform.GetComponents(); BoxCollider[] boxColliders = transform.GetComponents(); foreach (MeshCollider mc in meshColliders) { if( CheckMeshTime( mc.sharedMesh, transform.gameObject ) ) { return null; } } // Include transform in hash if it has any relevant components. if ( (meshColliders != null && meshColliders.Length > 0) || (boxColliders != null && boxColliders.Length > 0) || (materials != null && materials.Length > 0) ) { AppendTransformHash( ref hash, transform ); } } else { MeshFilter[] meshes = transform.GetComponents(); Terrain[] terrains = transform.GetComponents(); foreach (MeshFilter meshFilter in meshes) { if( CheckMeshTime( meshFilter.sharedMesh, transform.gameObject ) ) { return null; } } // Include transform in hash if it has any relevant components. if ( (meshes != null && meshes.Length > 0) || (terrains != null && terrains.Length > 0) || (materials != null && materials.Length > 0) ) { AppendTransformHash( ref hash, transform ); } } return null; } private bool CheckMeshTime( Mesh mesh, GameObject obj ) { if ( mesh != null ) { string meshPath = UnityEditor.AssetDatabase.GetAssetPath(mesh); DateTime lastModified = System.IO.File.Exists(meshPath) ? System.IO.File.GetLastWriteTime(meshPath) : DateTime.MinValue; #if META_XR_ACOUSTIC_INFO Debug.Log($"modified {lastModified}, last build {timeStamp}"); #endif if (lastModified > timeStamp) { Debug.LogWarning($"Newer mesh file {meshPath}", obj); isOlder = true; // mesh file changed return true; } } return false; } readonly DateTime timeStamp; internal bool isOlder = false; readonly bool useColliders; internal Hash128 hash = new Hash128(); } internal bool IsOlder(DateTime timeStamp) { AgeChecker ageChecker = new AgeChecker(timeStamp, useColliders); traverseMeshHierarchy(gameObject, IncludeChildMeshes, ExcludeTags, false, lodSelection, null, ageChecker); return ageChecker.isOlder || ageChecker.hash != HierarchyHash; } private static void AppendTransformHash( ref Hash128 hash, Transform transform ) { hash.Append(transform.position.x); hash.Append(transform.position.y); hash.Append(transform.position.z); hash.Append(transform.rotation.w); hash.Append(transform.rotation.x); hash.Append(transform.rotation.y); hash.Append(transform.rotation.z); hash.Append(transform.localScale.x); hash.Append(transform.localScale.y); hash.Append(transform.localScale.z); } class HashAppender : ITransformVisitor { internal HashAppender(Hash128 hash, bool useColliders) { this._hash = hash; this.useColliders = useColliders; } public System.Object visit(Transform transform, System.Object parentData) { var activeMaterials = transform.GetComponents().Where(x => x.enabled); IMaterialDataProvider[] materials = Array.ConvertAll(activeMaterials.ToArray(), x => x); if ( useColliders ) { MeshCollider[] meshColliders = transform.GetComponents(); BoxCollider[] boxColliders = transform.GetComponents(); // Include transform in hash if it has any relevant components. if ( (meshColliders != null && meshColliders.Length > 0) || (boxColliders != null && boxColliders.Length > 0) || (materials != null && materials.Length > 0) ) { AppendTransformHash( ref _hash, transform ); } } else { MeshFilter[] meshes = transform.GetComponents(); Terrain[] terrains = transform.GetComponents(); // Include transform in hash if it has any relevant components. if ( (meshes != null && meshes.Length > 0) || (terrains != null && terrains.Length > 0) || (materials != null && materials.Length > 0) ) { AppendTransformHash( ref _hash, transform ); } } return null; } private Hash128 _hash; internal Hash128 Hash { get => _hash; } readonly bool useColliders; } internal void AppendHash(ref Hash128 hash) { HashAppender hashAppender = new HashAppender(hash,useColliders); traverseMeshHierarchy(gameObject, IncludeChildMeshes, ExcludeTags, false, lodSelection, null, hashAppender); hash = hashAppender.Hash; } internal Hash128 ComputeHash() { Hash128 hash = new Hash128(); AppendHash( ref hash ); return hash; } private static string GenerateFileName(Transform current) { if (current.parent == null) return current.gameObject.scene.name + "/" + current.name; return GenerateFileName(current.parent) + "-" + current.name; } string GenerateSuggestedPath() { string basePath = $"{MetaXRAcousticSettings.AcousticFileRootDir}/{GenerateFileName(transform)}"; string modifier = ""; int counter = 0; string suggestion = ""; // avoid name collisions do { suggestion = $"{basePath}{modifier}.{FILE_EXTENSION}"; modifier = "-" + counter; ++counter; // sanity check to prevent hang if (counter > 10000) { Debug.LogError("Unable to find suitable file name", gameObject); return ""; } } while (System.IO.File.Exists(suggestion)); return suggestion; } //*********************************************************************** // WriteFile - Write the serialized mesh file. internal bool WriteFile() { // Create a temporary geometry. if (Native.Interface.CreateAudioGeometry(out IntPtr tempGeometryHandle) != Success) { Debug.LogError("Failed to create temp geometry handle", gameObject); return false; } // Upload the mesh geometry. if (!GatherGeometryEditor(tempGeometryHandle, gameObject, gameObject.transform.worldToLocalMatrix)) { if (Native.Interface.DestroyAudioGeometry(tempGeometryHandle) != Success) Debug.LogError("Failed to destroy temp geometry handle", gameObject); return false; } if (!WriteFileInternal(tempGeometryHandle)) { if (Native.Interface.DestroyAudioGeometry(tempGeometryHandle) != Success) Debug.LogError("Failed to destroy temp geometry handle", gameObject); return false; } // Destroy the geometry. if (Native.Interface.DestroyAudioGeometry(tempGeometryHandle) != Success) { Debug.LogError("Failed to destroy temp geometry handle", gameObject); return false; } return true; } internal bool WriteFileInternal(IntPtr handle) { if (string.IsNullOrEmpty(relativeFilePath)) { if (string.IsNullOrEmpty(gameObject.scene.name)) { Debug.LogError("Cannot autogenerate name scene hasn't been saved", gameObject); return false; } relativeFilePath = GenerateSuggestedPath(); Debug.Log($"No file path specified, autogenerated: {relativeFilePath}", gameObject); } MetaXRAudioUtils.CreateDirectoryForFilePath(AbsoluteFilePath); bool shouldAdd = !File.Exists(AbsoluteFilePath); if (!shouldAdd && UnityEditor.VersionControl.Provider.isActive) { var checkout = UnityEditor.VersionControl.Provider.Checkout(AbsoluteFilePath, UnityEditor.VersionControl.CheckoutMode.Asset); checkout.Wait(); Debug.Log($"Checkout {RelativeFilePath}: success = {checkout.success}"); } // Write the mesh to a file. Debug.Log($"Writing mesh geometry: {AbsoluteFilePath}", gameObject); if (Native.Interface.AudioGeometryWriteMeshFile(handle, AbsoluteFilePath) != Success) { Debug.LogError($"Error writing mesh file {AbsoluteFilePath}", gameObject); return false; } if (shouldAdd && UnityEditor.VersionControl.Provider.isActive) { var checkout = UnityEditor.VersionControl.Provider.Checkout(AbsoluteFilePath, UnityEditor.VersionControl.CheckoutMode.Asset); checkout.Wait(); Debug.Log($"Add {RelativeFilePath}: success = {checkout.success}"); } UpdateGizmoMesh(handle); if (!FileEnabled) { Debug.LogWarning("File Successfully written but File Enabled is off, turn it on to use file"); } return true; } #endif //*********************************************************************** // ReadFile - Read the serialized mesh file. internal bool ReadFile() { if (string.IsNullOrEmpty(AbsoluteFilePath)) { Debug.LogError("Invalid mesh file path", gameObject); return false; } int index = AbsoluteFilePath.IndexOf("StreamingAssets"); if (Application.isPlaying && index > 0) { string subPath = AbsoluteFilePath.Substring(index + 16); StartCoroutine(LoadGeometryAsync(subPath)); } else { if (Native.Interface.AudioGeometryReadMeshFile(geometryHandle, AbsoluteFilePath) != Success) { Debug.LogError($"Error reading mesh file {AbsoluteFilePath}", gameObject); return false; } Native.Interface.AudioGeometrySetObjectFlag(geometryHandle, ObjectFlags.ENABLED, isActiveAndEnabled); ApplyTransform(); Native.Interface.AudioGeometrySetObjectFlag(geometryHandle, ObjectFlags.STATIC, gameObject.isStatic); #if UNITY_EDITOR UpdateGizmoMesh(); #endif } return true; } private IEnumerator LoadGeometryAsync(string relativePath) { string path = Application.streamingAssetsPath + "/" + relativePath; #if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX path = "file://" + path; #endif Debug.Log($"Loading Geometry {name} from StreamingAssets {path}", gameObject); float startTime = Time.realtimeSinceStartup; Profiler.BeginSample("MetaXRAcousticGeometry web request get"); var unityWebRequest = UnityEngine.Networking.UnityWebRequest.Get(path); Profiler.EndSample(); yield return unityWebRequest.SendWebRequest(); if (!string.IsNullOrEmpty(unityWebRequest.error)) { Debug.LogError($"web request: done={unityWebRequest.isDone}: {unityWebRequest.error}", gameObject); } float readTime = Time.realtimeSinceStartup; float readDuration = readTime - startTime; Debug.Log($"Geometry {name}, read time = {readDuration}", gameObject); LoadGeometryFromMemory(unityWebRequest.downloadHandler.nativeData); } async void LoadGeometryFromMemory(Unity.Collections.NativeArray.ReadOnly data) { if (data.Length == 0) return; float startTime = Time.realtimeSinceStartup; int result = -1; await Task.Run(() => { unsafe { IntPtr ptr = (IntPtr)Unity.Collections.LowLevel.Unsafe.NativeArrayUnsafeUtility.GetUnsafeReadOnlyPtr(data); lock (this) { if (geometryHandle != IntPtr.Zero) { result = Native.Interface.AudioGeometryReadMeshMemory(geometryHandle, ptr, (UInt64)data.Length); GC.KeepAlive(data); } } } }); if (result == Success) { float loadDuration = Time.realtimeSinceStartup - startTime; Debug.Log($"Sucessfully loaded Geometry {name}, load time = {loadDuration}", gameObject); Native.Interface.AudioGeometrySetObjectFlag(geometryHandle, ObjectFlags.ENABLED, isActiveAndEnabled); ApplyTransform(); Native.Interface.AudioGeometrySetObjectFlag(geometryHandle, ObjectFlags.STATIC, gameObject.isStatic); #if UNITY_EDITOR UpdateGizmoMesh(); #endif isLoaded = true; if (isActiveAndEnabled) { IncrementEnabledGeometryCount(); } } else { Debug.Log($"Unable to read the geometry {name}", gameObject); } } #if UNITY_EDITOR internal bool WriteToObj() { // Create a temporary geometry. if (Native.Interface.CreateAudioGeometry(out IntPtr tempGeometryHandle) != Success) { Debug.LogError("Failed to create temp geometry handle", gameObject); return false; } // Upload the mesh geometry. if (!GatherGeometryEditor(tempGeometryHandle, gameObject, gameObject.transform.worldToLocalMatrix)) { return false; } // Write the mesh to a .obj file. if (Native.Interface.AudioGeometryWriteMeshFileObj(tempGeometryHandle, AbsoluteFilePath + ".obj") != Success) { Debug.LogError($"Error writing .obj file {AbsoluteFilePath}.obj", gameObject); return false; } // Destroy the geometry. if (Native.Interface.DestroyAudioGeometry(tempGeometryHandle) != Success) { Debug.LogError("Failed to destroy temp geometry handle", gameObject); return false; } return true; } #endif }