478 lines
17 KiB
C#
478 lines
17 KiB
C#
// 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<PXR_Audio_Spatializer_SceneMaterial>();
|
|
}
|
|
|
|
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<MeshFilter> 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<PXR_Audio_Spatializer_SceneGeometry>() == 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<MeshFilter>();
|
|
// 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<MeshFilter> 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<Vector3>();
|
|
var combinedIndices = Array.Empty<int>();
|
|
// 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Submit non-static mesh of this geometry and its material into spatializer engine context
|
|
/// </summary>
|
|
/// <returns>Result of static mesh submission</returns>
|
|
public PXR_Audio.Spatializer.Result SubmitMeshToContext(bool showLog = true)
|
|
{
|
|
// find all meshes
|
|
var meshFilterList = new List<MeshFilter>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Submit static mesh of this geometry and its material into spatializer engine context
|
|
/// </summary>
|
|
/// <returns>Result of static mesh submission</returns>
|
|
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<MeshFilter>();
|
|
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<MeshFilter> meshList = new List<MeshFilter>();
|
|
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<MeshFilter>();
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
} |