diff --git a/Editor/Scripts/GLTFSettingsInspector.cs b/Editor/Scripts/GLTFSettingsInspector.cs index 26b4bc3da..d5e647cfc 100644 --- a/Editor/Scripts/GLTFSettingsInspector.cs +++ b/Editor/Scripts/GLTFSettingsInspector.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; @@ -17,6 +18,7 @@ namespace UnityGLTF #if SHOW_SETTINGS_EDITOR internal class GltfSettingsProvider : SettingsProvider { + private const string DEFAULT_NON_RATIFIED_TOOLTIP = "This extension specification is not yet ratified. It may change in the future."; internal static Action OnAfterGUI; private static GLTFSettings settings; private SerializedProperty showDefaultReferenceNameWarning, showNamingRecommendationHint; @@ -161,6 +163,8 @@ internal static void DrawGLTFSettingsGUI(GLTFSettings settings, SerializedObject private static Dictionary editorCache = new Dictionary(); + private static GUIStyle _badgeStyle = null; + internal static void OnPluginsGUI(IEnumerable plugins, bool allowDisabling = true) { var lastAssembly = ""; @@ -210,9 +214,23 @@ internal static void OnPluginsGUI(IEnumerable plugins, bool allowDis EditorUtility.SetDirty(plugin); var label = new GUIContent(displayName, plugin.Description); + EditorGUI.BeginDisabledGroup(!plugin.Enabled); var expanded2 = EditorGUILayout.Foldout(expanded, label); var lastFoldoutRect = GUILayoutUtility.GetLastRect(); + + float batchOffsetX = EditorStyles.label.CalcSize(label).x + 20f; + var nonRatAttribute = plugin.GetType().GetCustomAttribute(typeof(NonRatifiedPluginAttribute), true); + if (nonRatAttribute != null) + { + batchOffsetX = DrawNonRatifiedBadge(nonRatAttribute, batchOffsetX); + } + var expAttribute = plugin.GetType().GetCustomAttribute(typeof(ExperimentalPluginAttribute), true); + if (expAttribute != null) + { + batchOffsetX = DrawExperimentalBadge(expAttribute, batchOffsetX); + } + // check for right click so we can show a context menu EditorGUI.EndDisabledGroup(); if (Event.current.type == EventType.MouseDown && lastFoldoutRect.Contains(Event.current.mousePosition)) @@ -279,6 +297,45 @@ internal static void OnPluginsGUI(IEnumerable plugins, bool allowDis } } + private static float DrawBadge(string text, string toolTip, Color color, float offsetX) + { + if (_badgeStyle == null) + { + _badgeStyle = new GUIStyle(EditorStyles.objectFieldThumb); + _badgeStyle.fontSize = 11; + _badgeStyle.contentOffset = new Vector2(0, 0); + _badgeStyle.clipping = TextClipping.Overflow; + _badgeStyle.fixedHeight = 15f; + } + + var explabel = new GUIContent(text , toolTip); + var expLabelRect = GUILayoutUtility.GetLastRect(); + + expLabelRect.x += offsetX; + expLabelRect.width = _badgeStyle.CalcSize(explabel).x+15; + expLabelRect.y += 2f; + + GUI.contentColor = color; + GUI.backgroundColor = color; + EditorGUI.LabelField(expLabelRect, explabel, _badgeStyle); + GUI.backgroundColor = Color.white; + GUI.contentColor = Color.white; + return offsetX + expLabelRect.width - 10f; + } + + private static float DrawNonRatifiedBadge(Attribute expAttribute, float offsetX) + { + var exp = expAttribute as NonRatifiedPluginAttribute; + var toolTip = exp.toolTip == null ? DEFAULT_NON_RATIFIED_TOOLTIP : exp.toolTip; + return DrawBadge("non-ratified", toolTip, new Color(1f*2,0.5f*2,0f,1f), offsetX); + } + + private static float DrawExperimentalBadge(Attribute expAttribute, float offsetX) + { + var exp = expAttribute as ExperimentalPluginAttribute; + var toolTip = exp.toolTip == null ? null : exp.toolTip; + return DrawBadge("experimental", toolTip, new Color(1f*2f,0.7f,0f,1f), offsetX); + } } [CustomEditor(typeof(GLTFSettings))] diff --git a/Editor/Scripts/Interactivity/VisualScriptingExport/VisualScriptingExportPlugin.cs b/Editor/Scripts/Interactivity/VisualScriptingExport/VisualScriptingExportPlugin.cs index 5f2084a73..ab9c09d00 100644 --- a/Editor/Scripts/Interactivity/VisualScriptingExport/VisualScriptingExportPlugin.cs +++ b/Editor/Scripts/Interactivity/VisualScriptingExport/VisualScriptingExportPlugin.cs @@ -15,6 +15,7 @@ namespace UnityGLTF.Interactivity.VisualScripting /// See https://github.com/KhronosGroup/UnityGLTF?tab=readme-ov-file#extensibility /// for the external documentation on how to extend UnityGLTF. /// + [NonRatifiedPlugin] public class VisualScriptingExportPlugin: GLTFExportPlugin { public override JToken AssetExtras diff --git a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs new file mode 100644 index 000000000..7196cbfbf --- /dev/null +++ b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs @@ -0,0 +1,575 @@ +using System; +using System.Collections.Generic; +using GLTF.Extensions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace GLTF.Schema +{ + public enum PositionalAudioDistanceModel + { + linear, + inverse, + exponential, + } + + [Serializable] + public class AudioEmitterId : GLTFId + { + public AudioEmitterId() + { + } + + public AudioEmitterId(AudioEmitterId id, GLTFRoot newRoot) : base(id, newRoot) + { + } + + public override KHR_AudioEmitter Value + { + get + { + if (Root.Extensions.TryGetValue(KHR_audio_emitter.ExtensionName, out IExtension iextension)) + { + KHR_audio_emitter extension = iextension as KHR_audio_emitter; + return extension.emitters[Id]; + } + else + { + throw new Exception("KHR_audio not found on root object"); + } + } + } + + public static AudioEmitterId Deserialize(GLTFRoot root, JsonReader reader) + { + return new AudioEmitterId + { + Id = reader.ReadAsInt32().Value, + Root = root + }; + } + } + + [Serializable] + public class AudioSourceId : GLTFId + { + public AudioSourceId() + { + } + + public AudioSourceId(AudioSourceId id, GLTFRoot newRoot) : base(id, newRoot) + { + } + + public override KHR_AudioSource Value + { + get + { + if (Root.Extensions.TryGetValue(KHR_audio_emitter.ExtensionName, out IExtension iextension)) + { + KHR_audio_emitter extension = iextension as KHR_audio_emitter; + return extension.sources[Id]; + } + else + { + throw new Exception("KHR_audio not found on root object"); + } + } + } + + public static AudioSourceId Deserialize(GLTFRoot root, JsonReader reader) + { + return new AudioSourceId + { + Id = reader.ReadAsInt32().Value, + Root = root + }; + } + } + + [Serializable] + public class AudioDataId : GLTFId + { + public AudioDataId() + { + } + + public AudioDataId(AudioDataId id, GLTFRoot newRoot) : base(id, newRoot) + { + } + + public override KHR_AudioData Value + { + get + { + if (Root.Extensions.TryGetValue(KHR_audio_emitter.ExtensionName, out IExtension iextension)) + { + KHR_audio_emitter extension = iextension as KHR_audio_emitter; + return extension.audio[Id]; + } + else + { + throw new Exception("KHR_audio not found on root object"); + } + } + } + + public static AudioDataId Deserialize(GLTFRoot root, JsonReader reader) + { + return new AudioDataId + { + Id = reader.ReadAsInt32().Value, + Root = root + }; + } + } + + [Serializable] + public class KHR_SceneAudioEmittersRef : IExtension + { + public static string ExtensionName => KHR_audio_emitter.ExtensionName; + public List emitters = new List(); + + public JProperty Serialize() + { + var jo = new JObject(); + JProperty jProperty = new JProperty(KHR_audio_emitter.ExtensionName, jo); + + JArray arr = new JArray(); + + foreach (var emitter in emitters) + { + arr.Add(emitter.Id); + } + + jo.Add(new JProperty(nameof(emitters), arr)); + + return jProperty; + } + + public IExtension Clone(GLTFRoot root) + { + return new KHR_SceneAudioEmittersRef() { emitters = emitters }; + } + + public static KHR_SceneAudioEmittersRef Deserialize(GLTFRoot root, JProperty extensionToken) + { + var extension = new KHR_SceneAudioEmittersRef(); + + var idsToken = extensionToken.Value[nameof(KHR_SceneAudioEmittersRef.emitters)]; + if (idsToken != null) + { + var ids = idsToken as JArray; + + // var ids = idsToken.CreateReader().ReadInt32List(); + foreach (var id in ids) + extension.emitters.Add(new AudioEmitterId { Id = id.DeserializeAsInt(), Root = root }); + } + + return extension; + } + } + + [Serializable] + public class KHR_NodeAudioEmitterRef : IExtension + { + public static string ExtensionName => KHR_audio_emitter.ExtensionName; + public AudioEmitterId emitter; + + public JProperty Serialize() + { + var jo = new JObject(); + JProperty jProperty = new JProperty(KHR_audio_emitter.ExtensionName, jo); + jo.Add(new JProperty(nameof(emitter), emitter.Id)); + return jProperty; + } + + public IExtension Clone(GLTFRoot root) + { + return new KHR_NodeAudioEmitterRef() { emitter = emitter }; + } + + public static KHR_NodeAudioEmitterRef Deserialize(GLTFRoot root, JProperty extensionToken) + { + var extension = new KHR_NodeAudioEmitterRef(); + + var id = extensionToken.Value[nameof(KHR_NodeAudioEmitterRef.emitter)]?.ToObject(); + if (id != null) + { + extension.emitter = new AudioEmitterId { Id = id.Value, Root = root }; + } + + return extension; + } + } + + public class PositionalEmitterData + { + public string shapeType; + public float? coneInnerAngle; + public float? coneOuterAngle; + public float? coneOuterGain; + public PositionalAudioDistanceModel? distanceModel; + + public float? maxDistance + ; + public float? refDistance; + public float? rolloffFactor; + + public JObject Serialize() + { + var positional = new JObject(); + + //if (!Mathf.Approximately(coneInnerAngle, Mathf.PI * 2)) { + // positional.Add(new JProperty(nameof(coneInnerAngle), coneInnerAngle)); + //} + + //if (!Mathf.Approximately(coneInnerAngle, Mathf.PI * 2)) { + // positional.Add(new JProperty(nameof(coneOuterAngle), coneOuterAngle)); + //} + + //if (coneOuterGain != 0.0f) { + // positional.Add(new JProperty(nameof(coneOuterGain), coneOuterGain)); + //} + + + if (distanceModel != PositionalAudioDistanceModel.inverse) + { + positional.Add(new JProperty(nameof(distanceModel), distanceModel.ToString())); + } + + if (maxDistance != 10000.0f) + { + positional.Add(new JProperty(nameof(maxDistance), maxDistance)); + } + + if (refDistance != 1.0f) + { + positional.Add(new JProperty(nameof(refDistance), refDistance)); + } + + //if (rolloffFactor != 1.0f) { + // positional.Add(new JProperty(nameof(rolloffFactor), rolloffFactor)); + //} + + + return positional; + } + + public static PositionalEmitterData Deserialize(GLTFRoot root, JsonReader reader) + { + var positional = new PositionalEmitterData(); + + if (reader.Read() && reader.TokenType != JsonToken.StartObject) + { + throw new Exception("PositionalEmitterData must be an object."); + } + + while (reader.Read() && reader.TokenType == JsonToken.PropertyName) + { + var curProp = reader.Value.ToString(); + + switch (curProp) + { + case nameof(shapeType): + positional.shapeType = reader.ReadAsString(); + break; + case nameof(coneInnerAngle): + positional.coneInnerAngle = (float)reader.ReadAsDouble(); + break; + case nameof(coneOuterAngle): + positional.coneOuterAngle = (float)reader.ReadAsDouble(); + break; + case nameof(coneOuterGain): + positional.coneOuterGain = (float)reader.ReadAsDouble(); + break; + case nameof(distanceModel): + positional.distanceModel = (PositionalAudioDistanceModel)Enum.Parse(typeof(PositionalAudioDistanceModel), reader.ReadAsString()); + break; + case nameof(maxDistance): + positional.maxDistance = (float)reader.ReadAsDouble(); + break; + case nameof(refDistance): + positional.refDistance = (float)reader.ReadAsDouble(); + break; + case nameof(rolloffFactor): + positional.rolloffFactor = (float)reader.ReadAsDouble(); + break; + } + } + + return positional; + + } + } + + [Serializable] + public class KHR_AudioEmitter : GLTFChildOfRootProperty + { + public string name; + public string type; + public float gain; + public List sources = new List(); + + public PositionalEmitterData positional = null; + + public virtual JObject Serialize() + { + var jo = new JObject(); + + if (!string.IsNullOrEmpty(name)) + { + jo.Add(nameof(name), name); + } + + jo.Add(nameof(type), type); + + jo.Add(nameof(gain), gain); + + if (positional != null) + { + jo.Add(new JProperty(nameof(positional), positional.Serialize())); + } + + if (sources != null && sources.Count > 0) + { + JArray arr = new JArray(); + + foreach (var source in sources) + { + arr.Add(source.Id); + } + + jo.Add(new JProperty(nameof(sources), arr)); + } + + return jo; + } + + public static KHR_AudioEmitter Deserialize(GLTFRoot root, JsonReader reader) + { + var emitter = new KHR_AudioEmitter(); + + if (reader.Read() && reader.TokenType != JsonToken.StartObject) + { + throw new Exception("AudioSource must be an object."); + } + + while (reader.Read() && reader.TokenType == JsonToken.PropertyName) + { + var curProp = reader.Value.ToString(); + + switch (curProp) + { + case nameof(KHR_AudioEmitter.name): + emitter.Name = reader.ReadAsString(); + break; + case nameof(KHR_AudioEmitter.gain): + emitter.gain = (float)reader.ReadAsDouble(); + break; + case nameof(KHR_AudioEmitter.type): + emitter.type = reader.ReadAsString(); + break; + case nameof(KHR_AudioEmitter.sources): + var list = reader.ReadInt32List(); + if (list == null) + break; + foreach (var source in list) + emitter.sources.Add(new AudioSourceId { Id = source, Root = root }); + break; + case nameof(positional): + emitter.positional = PositionalEmitterData.Deserialize(root, reader); + break; + } + } + + return emitter; + } + } + + [Serializable] + public class KHR_AudioSource : GLTFChildOfRootProperty + { + public bool? autoPlay; + public float? gain; + public bool? loop; + public AudioDataId audio; + + public JObject Serialize() + { + var jo = new JObject(); + + if (autoPlay != null) + jo.Add(nameof(autoPlay), autoPlay); + + if (gain != null) + jo.Add(nameof(gain), gain); + + if (loop != null) + jo.Add(nameof(loop), loop); + + if (audio != null) + jo.Add(nameof(audio), audio.Id); + + if (!string.IsNullOrEmpty(Name)) + jo.Add("name", Name); + + return jo; + } + + public static KHR_AudioSource Deserialize(GLTFRoot root, JsonReader reader) + { + var audioSource = new KHR_AudioSource(); + + if (reader.Read() && reader.TokenType != JsonToken.StartObject) + { + throw new Exception("AudioSource must be an object."); + } + + while (reader.Read() && reader.TokenType == JsonToken.PropertyName) + { + var curProp = reader.Value.ToString(); + + switch (curProp) + { + case nameof(KHR_AudioSource.Name): + audioSource.Name = reader.ReadAsString(); + break; + case nameof(KHR_AudioSource.audio): + audioSource.audio = AudioDataId.Deserialize(root, reader); + break; + case nameof(KHR_AudioSource.autoPlay): + audioSource.autoPlay = reader.ReadAsBoolean(); + break; + case nameof(KHR_AudioSource.gain): + audioSource.gain = (float)reader.ReadAsDouble(); + break; + case nameof(KHR_AudioSource.loop): + audioSource.loop = reader.ReadAsBoolean(); + break; + } + } + + return audioSource; + } + } + + [Serializable] + public class KHR_AudioData : GLTFChildOfRootProperty + { + public string uri; + public string mimeType; + public BufferViewId bufferView; + + public JObject Serialize() + { + var jo = new JObject(); + + if (uri != null) + { + jo.Add(nameof(uri), uri); + } + else + { + jo.Add(nameof(mimeType), mimeType); + jo.Add(nameof(bufferView), bufferView.Id); + } + + return jo; + } + + public static KHR_AudioData Deserialize(GLTFRoot root, JsonReader reader) + { + var audioData = new KHR_AudioData(); + + if (reader.Read() && reader.TokenType != JsonToken.StartObject) + { + throw new Exception("Audio must be an object."); + } + + while (reader.Read() && reader.TokenType == JsonToken.PropertyName) + { + var curProp = reader.Value.ToString(); + + switch (curProp) + { + case nameof(KHR_AudioData.mimeType): + audioData.mimeType = reader.ReadAsString(); + break; + case nameof(KHR_AudioData.uri): + audioData.uri = reader.ReadAsString(); + break; + case nameof(KHR_AudioData.bufferView): + audioData.bufferView = BufferViewId.Deserialize(root, reader); + break; + } + } + return audioData; + } + } + + [Serializable] + public class KHR_audio_emitter : IExtension + { + public const string ExtensionName = "KHR_audio_emitter"; + + public List audio = new List(); + public List sources = new List(); + public List emitters = new List(); + + public JProperty Serialize() + { + var jo = new JObject(); + JProperty jProperty = new JProperty(ExtensionName, jo); + + if (audio != null && audio.Count > 0) + { + JArray audioArr = new JArray(); + + foreach (var audioData in audio) + { + audioArr.Add(audioData.Serialize()); + } + + jo.Add(new JProperty(nameof(audio), audioArr)); + } + + + if (sources != null && sources.Count > 0) + { + JArray sourceArr = new JArray(); + + foreach (var source in sources) + { + sourceArr.Add(source.Serialize()); + } + + jo.Add(new JProperty(nameof(sources), sourceArr)); + } + + if (emitters != null && emitters.Count > 0) + { + JArray emitterArr = new JArray(); + + foreach (var emitter in emitters) + { + emitterArr.Add(emitter.Serialize()); + } + + jo.Add(new JProperty(nameof(emitters), emitterArr)); + } + + return jProperty; + } + + public IExtension Clone(GLTFRoot root) + { + return new KHR_audio_emitter() + { + audio = audio, + sources = sources, + emitters = emitters, + }; + } + } +} \ No newline at end of file diff --git a/Samples~/KHR_audio/KHRAudioPlugin.cs.meta b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs.meta similarity index 83% rename from Samples~/KHR_audio/KHRAudioPlugin.cs.meta rename to Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs.meta index 7b2759e7c..d1e7d2e85 100644 --- a/Samples~/KHR_audio/KHRAudioPlugin.cs.meta +++ b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 664d6e75fec3044eb9274db51fbb70da +guid: 49076143c3c264548a7d3734f637b0f3 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitterFactory.cs b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitterFactory.cs new file mode 100644 index 000000000..31c7577d8 --- /dev/null +++ b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitterFactory.cs @@ -0,0 +1,64 @@ +using Newtonsoft.Json.Linq; + +namespace GLTF.Schema +{ + public class KHR_audio_emitterFactory : ExtensionFactory + { + public const string EXTENSION_NAME = KHR_audio_emitter.ExtensionName; + + public KHR_audio_emitterFactory() + { + ExtensionName = EXTENSION_NAME; + } + + public override IExtension Deserialize(GLTFRoot root, JProperty extensionToken) + { + // Positional audio emitter + JToken ermitterToken = extensionToken.Value[nameof(KHR_NodeAudioEmitterRef.emitter)]; + if (ermitterToken != null) + { + return KHR_NodeAudioEmitterRef.Deserialize(root, extensionToken); + } + + var audioToken = extensionToken.Value[nameof(KHR_audio_emitter.audio)]; + var sourcesToken = extensionToken.Value[nameof(KHR_audio_emitter.sources)]; + + if (audioToken == null && sourcesToken == null) + { + // Global audio emitter + JToken globalToken = extensionToken.Value[nameof(KHR_SceneAudioEmittersRef.emitters)]; + if (globalToken != null) + { + return KHR_SceneAudioEmittersRef.Deserialize(root, extensionToken); + } + } + + var extension = new KHR_audio_emitter(); + + if (audioToken != null) + { + JArray audioArray = audioToken as JArray; + foreach (var audio in audioArray.Children()) + extension.audio.Add(KHR_AudioData.Deserialize(root, audio.CreateReader())); + } + + if (sourcesToken != null) + { + JArray sourcesArray = sourcesToken as JArray; + foreach (var source in sourcesArray.Children()) + extension.sources.Add(KHR_AudioSource.Deserialize(root, source.CreateReader())); + } + + var emittersToken = extensionToken.Value[nameof(KHR_audio_emitter.emitters)]; + if (emittersToken != null) + { + JArray emittersArray = emittersToken as JArray; + foreach (var emitters in emittersArray.Children()) + extension.emitters.Add(KHR_AudioEmitter.Deserialize(root, emitters.CreateReader())); + } + + return extension; + } + } + +} \ No newline at end of file diff --git a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitterFactory.cs.meta b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitterFactory.cs.meta new file mode 100644 index 000000000..217906b01 --- /dev/null +++ b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitterFactory.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e5ab621cb8a24394855308f18fe5216d +timeCreated: 1743761298 \ No newline at end of file diff --git a/Runtime/Plugins/GLTFSerialization/Schema/GLTFProperty.cs b/Runtime/Plugins/GLTFSerialization/Schema/GLTFProperty.cs index 4a1651228..9f99aba1a 100644 --- a/Runtime/Plugins/GLTFSerialization/Schema/GLTFProperty.cs +++ b/Runtime/Plugins/GLTFSerialization/Schema/GLTFProperty.cs @@ -44,6 +44,7 @@ public static IReadOnlyList RegisteredExtensions { KHR_node_visibility_Factory.EXTENSION_NAME, new KHR_node_visibility_Factory()}, { KHR_node_selectability_Factory.EXTENSION_NAME, new KHR_node_selectability_Factory()}, { KHR_node_hoverability_Factory.EXTENSION_NAME, new KHR_node_hoverability_Factory()}, + { KHR_audio_emitterFactory.EXTENSION_NAME, new KHR_audio_emitterFactory()}, }; private static DefaultExtensionFactory _defaultExtensionFactory = new DefaultExtensionFactory(); diff --git a/Runtime/Scripts/Cache/RefCountedCacheData.cs b/Runtime/Scripts/Cache/RefCountedCacheData.cs index 59a9e4d87..6fabb2536 100644 --- a/Runtime/Scripts/Cache/RefCountedCacheData.cs +++ b/Runtime/Scripts/Cache/RefCountedCacheData.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using UnityEngine; +using Object = UnityEngine.Object; namespace UnityGLTF.Cache { @@ -27,6 +28,11 @@ public class RefCountedCacheData /// public MeshCacheData[] MeshCache { get; private set; } + /// + /// Generic Unity Objects used by this GLTF node. + /// + public Object[] GenericObjectCache { get; private set; } + /// /// Materials used by this GLTF node. /// @@ -47,13 +53,14 @@ public class RefCountedCacheData /// public Texture2D[] ImageCache { get; private set; } - public RefCountedCacheData(MaterialCacheData[] materialCache, MeshCacheData[] meshCache, TextureCacheData[] textureCache, Texture2D[] imageCache, AnimationCacheData[] animationCache) + public RefCountedCacheData(MaterialCacheData[] materialCache, MeshCacheData[] meshCache, TextureCacheData[] textureCache, Texture2D[] imageCache, AnimationCacheData[] animationCache, Object[] genericObjectCache) { MaterialCache = materialCache; MeshCache = meshCache; TextureCache = textureCache; ImageCache = imageCache; AnimationCache = animationCache; + GenericObjectCache = genericObjectCache; } public void IncreaseRefCount() @@ -100,6 +107,13 @@ private void DestroyCachedData() MeshCache[i]?.Dispose(); MeshCache[i] = null; } + + // Destroy the cached AudioClips + for (int i = 0; i < GenericObjectCache.Length; i++) + { + UnityEngine.Object.Destroy(GenericObjectCache[i]); + GenericObjectCache[i] = null; + } // Destroy the cached textures for (int i = 0; i < TextureCache.Length; i++) diff --git a/Runtime/Scripts/GLTFSceneImporter.cs b/Runtime/Scripts/GLTFSceneImporter.cs index 4b945ed2b..73465972d 100644 --- a/Runtime/Scripts/GLTFSceneImporter.cs +++ b/Runtime/Scripts/GLTFSceneImporter.cs @@ -14,6 +14,7 @@ using UnityGLTF.Extensions; using UnityGLTF.Loader; using UnityGLTF.Plugins; +using Object = UnityEngine.Object; using Quaternion = UnityEngine.Quaternion; using Vector3 = UnityEngine.Vector3; #if !WINDOWS_UWP && !UNITY_WEBGL @@ -219,6 +220,12 @@ public GameObject LastLoadedScene public GameObject[] NodeCache => _assetCache.NodeCache; public MeshCacheData[] MeshCache => _assetCache.MeshCache; + /// + /// Add here any objects, which are not GameObject, Materials, Textures and Animation Clips, + /// that need to be cleaned up when the scene is destroyed + /// + public List GenericObjectReferences { get; private set; } = new List(); + private Dictionary> _nativeBuffers = new Dictionary>(); #if HAVE_MESHOPT_DECOMPRESS private List> meshOptNativeBuffers = new List>(); @@ -651,7 +658,8 @@ private void InitializeGltfTopLevelObject() _assetCache.MeshCache, _assetCache.TextureCache, _assetCache.ImageCache, - _assetCache.AnimationCache + _assetCache.AnimationCache, + GenericObjectReferences.ToArray() ); } @@ -778,6 +786,13 @@ private void GetGltfContentTotals(GLTFScene scene) progress?.Report(progressStatus); } + public NativeArray GetBufferViewData(BufferView bufferView) + { + GetBufferData(bufferView.Buffer).Wait(); + GLTFHelpers.LoadBufferView(bufferView, _assetCache.BufferCache[bufferView.Buffer.Id].ChunkOffset, _assetCache.BufferCache[bufferView.Buffer.Id].bufferData, out var bufferViewCache); + return bufferViewCache; + } + private async Task GetBufferData(BufferId bufferId) { if (bufferId == null) return null; diff --git a/Runtime/Scripts/Plugins/Audio.meta b/Runtime/Scripts/Plugins/Audio.meta new file mode 100644 index 000000000..9028e7a8b --- /dev/null +++ b/Runtime/Scripts/Plugins/Audio.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 28126dfa5cbf40bfb49cc9dededfae28 +timeCreated: 1744044350 \ No newline at end of file diff --git a/Runtime/Scripts/Plugins/Audio/TempAssignClip.cs b/Runtime/Scripts/Plugins/Audio/TempAssignClip.cs new file mode 100644 index 000000000..3250aa2ae --- /dev/null +++ b/Runtime/Scripts/Plugins/Audio/TempAssignClip.cs @@ -0,0 +1,13 @@ +using UnityEngine; + +namespace UnityGLTF.Plugins.Audio +{ + /// + /// Helper class to assign audio clips to AudioSources when importing a glTF file + /// + public class TempAssignClip : MonoBehaviour + { + public int audioSourceIndex; + public string audioPath; + } +} \ No newline at end of file diff --git a/Runtime/Scripts/Plugins/Audio/TempAssignClip.cs.meta b/Runtime/Scripts/Plugins/Audio/TempAssignClip.cs.meta new file mode 100644 index 000000000..246ab4686 --- /dev/null +++ b/Runtime/Scripts/Plugins/Audio/TempAssignClip.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 34e09585d1c2485eb521f74a9a909ae6 +timeCreated: 1744044360 \ No newline at end of file diff --git a/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs b/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs new file mode 100644 index 000000000..99394055e --- /dev/null +++ b/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs @@ -0,0 +1,32 @@ +using System; + +namespace UnityGLTF.Plugins +{ + /// + /// Marks a plugin as non-ratified. This is used to indicate that the extension is not yet part of the official glTF specification. + /// + [AttributeUsage(AttributeTargets.Class)] + public class NonRatifiedPluginAttribute : Attribute + { + public string toolTip; + + public NonRatifiedPluginAttribute(string toolTip = null) + { + this.toolTip = toolTip; + } + } + + /// + /// Marks a plugin as experiental. + /// + [AttributeUsage(AttributeTargets.Class)] + public class ExperimentalPluginAttribute : Attribute + { + public string toolTip; + + public ExperimentalPluginAttribute(string toolTip = null) + { + this.toolTip = toolTip; + } + } +} \ No newline at end of file diff --git a/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs.meta b/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs.meta new file mode 100644 index 000000000..02322d594 --- /dev/null +++ b/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 28ebcf4da97a474181b902349136c593 +timeCreated: 1747294638 \ No newline at end of file diff --git a/Runtime/Scripts/Plugins/Experimental/AudioExport.cs b/Runtime/Scripts/Plugins/Experimental/AudioExport.cs new file mode 100644 index 000000000..582166843 --- /dev/null +++ b/Runtime/Scripts/Plugins/Experimental/AudioExport.cs @@ -0,0 +1,224 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using GLTF.Schema; +using UnityEngine; + +#if UNITY_EDITOR +using UnityEditor; + +namespace UnityGLTF.Plugins +{ + [NonRatifiedPlugin] + public class AudioExport: GLTFExportPlugin + { + public override bool EnabledByDefault => false; + public override string DisplayName => "KHR_audio_emitter (Editor Only)"; + public override string Description => "Exports positional and global audio sources"; + public override GLTFExportPluginContext CreateInstance(ExportContext context) + { + return new AudioExportContext(context); + } + } + + public class AudioExportContext: GLTFExportPluginContext + { + private List _audioSourceIds = new(); + private KHR_audio_emitter _audioExtension; + private KHR_SceneAudioEmittersRef _sceneExtension = null; + + private Dictionary _audioSourceToEmitter = new(); + private Dictionary _audioSourceToNode = new(); + + public class AudioDescription + { + public int Id; + public string Name; + public AudioClip Clip; + } + + public AudioExportContext(ExportContext context) + { + } + + private AudioDescription AddAudioSource(AudioSource audioSource, out bool isNew) + { + AudioClip clip = audioSource.clip; + string name = clip.name; + + foreach (var a in _audioSourceIds) + { + if (name == a.Name && clip == a.Clip) + { + isNew = false; + return a; + } + } + AudioDescription ad = new AudioDescription() { Id = _audioSourceIds.Count, Name = clip.name, Clip = clip }; + _audioSourceIds.Add(ad); + isNew = true; + return ad; + } + + private string GetMimeType(string path) + { + var extension = Path.GetExtension(path); + if (extension == ".mp3") + return "audio/mpeg"; + if (extension == ".ogg") + return "audio/ogg"; + if (extension == ".wav") + return "audio/wav"; + return null; + } + + private AudioEmitterId ProcessAudioSource(bool isGlobal, AudioSource[] audioSources, GLTFSceneExporter exporter, GLTFRoot gltfRoot) + { + var audioSourceIds = new List(); + var exportRequired = new List(); + foreach (var source in audioSources) + { + var audioDescription = AddAudioSource(source, out var isNew); + audioSourceIds.Add(audioDescription); + if (isNew) + exportRequired.Add(audioDescription); + } + + var firstAudioSource = audioSources[0]; + + + var emitter = new KHR_AudioEmitter + { + type = isGlobal ? "global" : "positional", + gain = firstAudioSource.volume, + name = isGlobal ? "global emitter" : "positional emitter" + }; + + if (!isGlobal) + { + emitter.positional = new PositionalEmitterData() + { + refDistance = firstAudioSource.minDistance, + maxDistance = firstAudioSource.maxDistance, + distanceModel = PositionalAudioDistanceModel.linear, + }; + } + emitter.sources.AddRange(audioSourceIds.Select(a => new AudioSourceId { Id = a.Id, Root = gltfRoot })); + + _audioExtension.emitters.Add(emitter); + + foreach (var audioSourceId in exportRequired) + { + var clip = audioSourceId.Clip; + + var path = AssetDatabase.GetAssetPath(clip); + + var fileName = Path.GetFileName(path); + + var audio = new KHR_AudioData(); + var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); + var mimeType = GetMimeType(fileName); + if (string.IsNullOrEmpty(mimeType)) + { + Debug.LogError("Unsupported audio file type: " + fileName); + continue; + } + var result = exporter.ExportFile(fileName, mimeType, fileStream); + + if (string.IsNullOrEmpty(result.uri)) + { + audio.mimeType = result.mimeType; + audio.bufferView = result.bufferView; + } + else + { + audio.uri = result.uri; + } + _audioExtension.audio.Add(audio); + } + + foreach (var audioSourceId in audioSourceIds) + { + var khrAudio = new KHR_AudioSource + { + audio = new AudioDataId { Id = audioSourceId.Id, Root = gltfRoot }, + autoPlay = firstAudioSource.playOnAwake, + loop = firstAudioSource.loop, + gain = firstAudioSource.volume, + // TODO: uniquename required? + Name = audioSourceId.Clip.name + }; + + _audioExtension.sources.Add(khrAudio); + } + + var ermitterId = new AudioEmitterId() { Id = _audioExtension.emitters.Count - 1, Root = gltfRoot }; + + return ermitterId; + } + + public override void AfterNodeExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot, Transform transform, Node node) + { + var audioSources = transform.GetComponents(); + if (audioSources.Length == 0) + return; + + var globalSources = new List( audioSources.Where( a => a.spatialBlend < 0.5f) ); + var positionalSources = new List( audioSources.Where( a => a.spatialBlend >= 0.5f) ); + + if (_audioExtension == null) + { + _audioExtension = new KHR_audio_emitter(); + if (gltfRoot != null) + { + gltfRoot.AddExtension(KHR_audio_emitter.ExtensionName, _audioExtension); + exporter.DeclareExtensionUsage(KHR_audio_emitter.ExtensionName); + } + } + + // TODO: check if audio source settings are the same, otherwise add the source separately and create child nodes for the ermitter + // We try to export multiple audio source in a single ermitter with multiple sources + + if (positionalSources.Count > 0) + { + var ermitterId = ProcessAudioSource(false, positionalSources.ToArray(), exporter, gltfRoot); + if (ermitterId == null) + return; + foreach (var a in positionalSources) + { + _audioSourceToEmitter.Add(a, ermitterId); + _audioSourceToNode.Add(a, node); + } + + var nodeErmitter = new KHR_NodeAudioEmitterRef(); + nodeErmitter.emitter = ermitterId; + node.AddExtension(KHR_NodeAudioEmitterRef.ExtensionName, nodeErmitter); + } + + if (globalSources.Count > 0) + { + var ermitterId = ProcessAudioSource(true, globalSources.ToArray(), exporter, gltfRoot); + if (ermitterId == null) + return; + + foreach (var a in globalSources) + _audioSourceToEmitter.Add(a, ermitterId); + + if (_sceneExtension == null) + _sceneExtension = new KHR_SceneAudioEmittersRef(); + + _sceneExtension.emitters.Add(ermitterId); + } + + } + + public override void AfterSceneExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot) + { + if (_sceneExtension == null) + return; + + gltfRoot.Scenes[0].AddExtension(KHR_SceneAudioEmittersRef.ExtensionName, _sceneExtension); + } + } +} +#endif \ No newline at end of file diff --git a/Runtime/Scripts/Plugins/Experimental/AudioExport.cs.meta b/Runtime/Scripts/Plugins/Experimental/AudioExport.cs.meta new file mode 100644 index 000000000..6a67fdf25 --- /dev/null +++ b/Runtime/Scripts/Plugins/Experimental/AudioExport.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a59cc7d090dc43ef86b454e445bdbf9a +timeCreated: 1743751235 \ No newline at end of file diff --git a/Runtime/Scripts/Plugins/Experimental/AudioImport.cs b/Runtime/Scripts/Plugins/Experimental/AudioImport.cs new file mode 100644 index 000000000..8fa7d5a6c --- /dev/null +++ b/Runtime/Scripts/Plugins/Experimental/AudioImport.cs @@ -0,0 +1,348 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using GLTF.Schema; +using UnityEngine; +using UnityEngine.Networking; +using UnityGLTF.Plugins.Audio; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace UnityGLTF.Plugins +{ + [NonRatifiedPlugin] + public class AudioImport : GLTFImportPlugin + { + public override bool EnabledByDefault => false; + public override string DisplayName => "KHR_audio_emitter"; + public override string Description => "Import positional and global audio sources (Wav, Mp3, Ogg)"; + + public override GLTFImportPluginContext CreateInstance(GLTFImportContext context) + { + return new AudioImportContext(context); + } + } + +#if UNITY_EDITOR + + // In OnPostprocessAllAssets, we have now the possibility to load the AudioClips from the asset path + // and can assign them to the AudioSources in the Gltf Prefab + internal class AudioImportPostprocessor : AssetPostprocessor + { + internal static List lastImportedGltfs = new(); + + public static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, + string[] movedFromAssetPaths, bool didDomainReload) + { + foreach (var lastImportedGltf in lastImportedGltfs) + { + var gltfPrefab = AssetDatabase.LoadAssetAtPath(lastImportedGltf); + var assignClipComponent = gltfPrefab.GetComponentsInChildren(); + var importer = AssetImporter.GetAtPath(lastImportedGltf); + foreach (var assignClip in assignClipComponent) + { + var clip = AssetDatabase.LoadAssetAtPath(assignClip.audioPath); + var audioSourceComponent = assignClip.GetComponents(); + audioSourceComponent[assignClip.audioSourceIndex].clip = clip; + } + + foreach (var ac in assignClipComponent) + GameObject.DestroyImmediate(ac, true); + + EditorUtility.SetDirty(gltfPrefab); + } + + lastImportedGltfs.Clear(); + } + } +#endif + + public class AudioImportContext : GLTFImportPluginContext + { + private GLTFImportContext _context; + private KHR_audio_emitter _audioExtension; + + private class AssignClip + { + public AudioSource audioSource; + public AudioDataId AudioDataId; + + public AssignClip(AudioSource audioSource, AudioDataId audioDataId) + { + this.audioSource = audioSource; + AudioDataId = audioDataId; + } + } + + private List _assignClips = new(); + + private Dictionary _audioClips = new(); + private Dictionary _audioPaths = new(); + + private string _audioFilesDestinationPath; + + public AudioImportContext(GLTFImportContext context) + { + _context = context; + +#if UNITY_EDITOR + if (_context.AssetContext != null) + { + AudioImportPostprocessor.lastImportedGltfs.Add(_context.AssetContext.assetPath); + var filenameWithOutExtension = Path.GetFileNameWithoutExtension(_context.AssetContext.assetPath); + _audioFilesDestinationPath = Path.Combine(Path.GetDirectoryName(_context.AssetContext.assetPath), filenameWithOutExtension + "_Audio"); + } +#endif + } + + private void GetExtension(GLTFRoot root) + { + if (_audioExtension != null) + return; + + if (root.Extensions == null) + return; + + if (root.Extensions.TryGetValue(KHR_audio_emitter.ExtensionName, out var extension)) + { + _audioExtension = extension as KHR_audio_emitter; + } + else + { + Debug.LogWarning($"Audio extension not found in GLTF root."); + } + } + + public override void OnAfterImportRoot(GLTFRoot gltfRoot) + { + GetExtension(gltfRoot); + } + + private void AssignClips() + { + foreach (var ac in _assignClips) + { + if (_audioClips.TryGetValue(ac.AudioDataId.Id, out var audioClip)) + { + ac.audioSource.clip = audioClip; + } + else + { +#if UNITY_EDITOR + if (_context.AssetContext != null) + { + // When importing the gltf file as an asset, the audio files are not imported yet by Unity. + // So we add the temporary Component TempAssignClip In OnPostprocessAllAssets, we will assign the audio clips to the AudioSources. + if (_audioPaths.TryGetValue(ac.AudioDataId.Id, out var audioPath)) + { + audioPath = audioPath.Replace(@"\", "/"); + var assignClip =ac.audioSource.gameObject.AddComponent(); + assignClip.audioPath = audioPath; + var sources = ac.audioSource.gameObject.GetComponents(); + assignClip.audioSourceIndex = Array.IndexOf(sources, ac.audioSource); + } + continue; + } +#endif + + Debug.LogWarning($"Audio clip not found for AudioDataId {ac.AudioDataId}"); + } + + // In case we load a gltf at runtime, and the Scene GameObject is already active, the playOnAwake will not + // be called, so we need to call it manually + if (ac.audioSource.playOnAwake && ac.audioSource.gameObject.activeInHierarchy +#if UNITY_EDITOR + && _context.AssetContext == null + #endif + ) + { + ac.audioSource.Play(); + } + } + + } + + private string GetFileExtensionForMimeType(string mimeType) + { + switch (mimeType) + { + case "audio/mpeg": + return ".mp3"; + case "audio/wav": + return ".wav"; + case "audio/ogg": + return ".ogg"; + default: + Debug.LogWarning($"Unsupported audio mime type: {mimeType}"); + return null; + } + } + + private AudioType GetAudioTypeForMimeType(string mimeType) + { + switch (mimeType) + { + case "audio/mpeg": + return AudioType.MPEG; + case "audio/wav": + return AudioType.WAV; + case "audio/ogg": + return AudioType.OGGVORBIS; + default: + Debug.LogWarning($"Unsupported audio mime type: {mimeType}"); + return AudioType.UNKNOWN; + } + } + + private void CreateAudioClips() + { + int index = -1; + foreach (var audio in _audioExtension.audio) + { + index++; + if (audio.bufferView != null) + { + var buffer = _context.SceneImporter.GetBufferViewData(audio.bufferView.Value); + if (buffer == null) + continue; + + var mimeTypeFileExtension = GetFileExtensionForMimeType(audio.mimeType); + if (string.IsNullOrEmpty(mimeTypeFileExtension)) + continue; + +#if UNITY_EDITOR + + if (_context.AssetContext != null) + { + // When imported as an Asset: + var assetFilepath = Path.Combine(_audioFilesDestinationPath, $"audio_{index:D3}{mimeTypeFileExtension}"); + if (!Directory.Exists(_audioFilesDestinationPath)) + Directory.CreateDirectory(_audioFilesDestinationPath); + File.WriteAllBytes(assetFilepath, buffer.ToArray()); + + AssetDatabase.ImportAsset(assetFilepath, ImportAssetOptions.ForceUpdate); + _audioPaths.Add(index, assetFilepath); + continue; + } +#endif + // Runtime loaded Gltf: + var tempFile = Path.Combine(Application.temporaryCachePath, "gltfAudioImport"+ mimeTypeFileExtension); + File.WriteAllBytes(tempFile, buffer.ToArray()); + + var audioClipRequest = UnityWebRequestMultimedia.GetAudioClip(tempFile, GetAudioTypeForMimeType(audio.mimeType)); + audioClipRequest.SendWebRequest(); + while (!audioClipRequest.isDone) + { + // Wait for the request to complete + } + + if (audioClipRequest.result != UnityWebRequest.Result.Success) + { + Debug.LogError($"Cannot load audio clip for mimeType {audio.mimeType}: {audioClipRequest.error}"); + continue; + } + + AudioClip clip = DownloadHandlerAudioClip.GetContent(audioClipRequest); + if (clip == null || clip.samples == 0) + { + Debug.LogError($"Cannot load audio clip for mimeType {audio.mimeType}"); + continue; + } + + clip.name = $"audio_{index:D3}"; + + _audioClips.Add(index, clip); + } + else + { + Debug.LogWarning($"Audio buffer view not found for {audio.Name}"); + } + } + + _context.SceneImporter.GenericObjectReferences.AddRange(_audioClips.Select( kvp => kvp.Value).ToArray()); + } + + private void AddGlobalEmitters(GameObject sceneObject) + { + GLTFScene scene = null; + if (_context.Root.Scene != null) + scene = _context.Root.Scene.Value; + else + scene = _context.Root.Scenes[0]; + + if (scene == null) + return; + + if (!scene.Extensions.TryGetValue(KHR_audio_emitter.ExtensionName, out var extension)) + return; + if (extension is KHR_SceneAudioEmittersRef audioEmitterRef) + { + if (audioEmitterRef.emitters != null) + { + foreach (var emitter in audioEmitterRef.emitters) + { + if (emitter == null) + continue; + AddEmitter(emitter.Value, sceneObject, true); + } + } + } + } + + private void AddEmitter(KHR_AudioEmitter emitter, GameObject toGameObject, bool isGlobal) + { + foreach (var source in emitter.sources) + { + if (source == null) + continue; + + // TODO: set all parameters + + var audioSource = toGameObject.AddComponent(); + _assignClips.Add(new AssignClip(audioSource, source.Value.audio)); + audioSource.loop = source.Value.loop ?? false; + audioSource.volume = emitter.gain * (source.Value.gain ?? 1f); + audioSource.playOnAwake = source.Value.autoPlay ?? false; + audioSource.spatialBlend = isGlobal ? 0f : 1.0f; + if (!isGlobal && emitter.positional != null) + { + audioSource.rolloffMode = AudioRolloffMode.Linear; + audioSource.minDistance = emitter.positional.refDistance ?? 1f; + audioSource.maxDistance = emitter.positional.maxDistance ?? 0f; + } + } + } + + public override void OnAfterImportScene(GLTFScene scene, int sceneIndex, GameObject sceneObject) + { + AddGlobalEmitters(sceneObject); + CreateAudioClips(); + AssignClips(); + } + + public override void OnAfterImportNode(Node node, int nodeIndex, GameObject nodeObject) + { + if (_audioExtension == null) + return; + + if (node.Extensions == null || !node.Extensions.TryGetValue(KHR_NodeAudioEmitterRef.ExtensionName, out var extension)) + return; + + if (extension is KHR_NodeAudioEmitterRef audioEmitterRef) + { + if (audioEmitterRef.emitter != null) + { + var emitter = audioEmitterRef.emitter.Value; + AddEmitter(emitter, nodeObject, false); + } + else + { + Debug.LogWarning($"Audio source not found for node {node.Name}"); + } + } + + } + } +} \ No newline at end of file diff --git a/Runtime/Scripts/Plugins/Experimental/AudioImport.cs.meta b/Runtime/Scripts/Plugins/Experimental/AudioImport.cs.meta new file mode 100644 index 000000000..f0eb3baee --- /dev/null +++ b/Runtime/Scripts/Plugins/Experimental/AudioImport.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d106e4b0674b44e09e9c8d85ba6216df +timeCreated: 1743751243 \ No newline at end of file diff --git a/Runtime/Scripts/Plugins/Experimental/BakeParticleSystem.cs b/Runtime/Scripts/Plugins/Experimental/BakeParticleSystem.cs index a2dd01a6c..d43a5ff44 100644 --- a/Runtime/Scripts/Plugins/Experimental/BakeParticleSystem.cs +++ b/Runtime/Scripts/Plugins/Experimental/BakeParticleSystem.cs @@ -4,6 +4,7 @@ namespace UnityGLTF.Plugins { + [ExperimentalPlugin] public class BakeParticleSystem: GLTFExportPlugin { public override string DisplayName => "Bake to Mesh: Particle Systems"; diff --git a/Runtime/Scripts/Plugins/Experimental/CanvasExport.cs b/Runtime/Scripts/Plugins/Experimental/CanvasExport.cs index ef5234bec..9b10744bd 100644 --- a/Runtime/Scripts/Plugins/Experimental/CanvasExport.cs +++ b/Runtime/Scripts/Plugins/Experimental/CanvasExport.cs @@ -7,6 +7,7 @@ namespace UnityGLTF.Plugins { + [ExperimentalPlugin] public class CanvasExport : GLTFExportPlugin { public override string DisplayName => "Bake to Mesh: Canvas"; diff --git a/Runtime/Scripts/Plugins/Experimental/MaterialVariantsPlugin.cs b/Runtime/Scripts/Plugins/Experimental/MaterialVariantsPlugin.cs index 5bf7fa084..2aa2f50dd 100644 --- a/Runtime/Scripts/Plugins/Experimental/MaterialVariantsPlugin.cs +++ b/Runtime/Scripts/Plugins/Experimental/MaterialVariantsPlugin.cs @@ -7,6 +7,7 @@ namespace UnityGLTF.Plugins { + [ExperimentalPlugin] public class MaterialVariantsPlugin: GLTFExportPlugin { public override string DisplayName => "KHR_materials_variants"; diff --git a/Runtime/Scripts/Plugins/Experimental/TextMeshGameObjectExport.cs b/Runtime/Scripts/Plugins/Experimental/TextMeshGameObjectExport.cs index 78e2406be..7669e424f 100644 --- a/Runtime/Scripts/Plugins/Experimental/TextMeshGameObjectExport.cs +++ b/Runtime/Scripts/Plugins/Experimental/TextMeshGameObjectExport.cs @@ -6,6 +6,7 @@ namespace UnityGLTF.Plugins { + [ExperimentalPlugin] public class TextMeshGameObjectExport : GLTFExportPlugin { public override string DisplayName => "Bake to Mesh: TextMeshPro GameObjects"; diff --git a/Samples~/KHR_audio.meta b/Samples~/KHR_audio.meta deleted file mode 100644 index bda4b6e5b..000000000 --- a/Samples~/KHR_audio.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: e8556c362185a487c92146598a143c7c -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Samples~/KHR_audio/AudioSourceScriptableObject.cs b/Samples~/KHR_audio/AudioSourceScriptableObject.cs deleted file mode 100644 index 4bbfcb31b..000000000 --- a/Samples~/KHR_audio/AudioSourceScriptableObject.cs +++ /dev/null @@ -1,13 +0,0 @@ -using UnityEngine; - -namespace UnityGLTF.Plugins.Experimental -{ - [CreateAssetMenu(fileName = "AudioSource", menuName = "UnityGLTF/KHR_audio/AudioSource", order = 1)] - public class AudioSourceScriptableObject : ScriptableObject - { - public AudioClip clip; - public float gain = 1.0f; - public bool autoPlay = true; - public bool loop = true; - } -} \ No newline at end of file diff --git a/Samples~/KHR_audio/AudioSourceScriptableObject.cs.meta b/Samples~/KHR_audio/AudioSourceScriptableObject.cs.meta deleted file mode 100644 index c86cb7f1e..000000000 --- a/Samples~/KHR_audio/AudioSourceScriptableObject.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: e0f78417310647de8dbcfd01ceab5eee -timeCreated: 1703889181 \ No newline at end of file diff --git a/Samples~/KHR_audio/KHRAudioPlugin.cs b/Samples~/KHR_audio/KHRAudioPlugin.cs deleted file mode 100644 index f0da459e9..000000000 --- a/Samples~/KHR_audio/KHRAudioPlugin.cs +++ /dev/null @@ -1,215 +0,0 @@ -#if UNITY_EDITOR - -using System; -using System.Collections.Generic; -using System.IO; -using GLTF.Schema; -using UnityEditor; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace UnityGLTF.Plugins.Experimental -{ - public class KHRAudioPlugin : GLTFExportPlugin - { - public override string DisplayName => "KHR_audio"; - public override string Description => "Exports positional and global audio sources and .mp3 audio clips. Currently requires adding \"KHRPositionalAudioEmitterBehavior\" and \"KHRGlobalAudioEmitterBehavior\" components to scene objects."; - public override GLTFExportPluginContext CreateInstance(ExportContext context) - { - return new AudioExtensionConfig(); - } - } - public class AudioExtensionConfig: GLTFExportPluginContext - { - static List audioDataClips = new List(); - static List audioSourceObjects = new List(); - static List audioEmitters = new List(); - - public override void AfterNodeExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot, Transform transform, Node node) - { - var audioEmitterBehavior = transform.GetComponent(); - - if (audioEmitterBehavior != null) - { - var audioSourceIds = AddAudioSources(gltfRoot, audioEmitterBehavior.sources); - - var emitterId = new AudioEmitterId - { - Id = audioEmitters.Count, - Root = gltfRoot - }; - - var emitter = new KHR_PositionalAudioEmitter - { - type = "positional", - sources = audioSourceIds, - gain = audioEmitterBehavior.gain, - coneInnerAngle = audioEmitterBehavior.coneInnerAngle * Mathf.Deg2Rad, - coneOuterAngle = audioEmitterBehavior.coneOuterAngle * Mathf.Deg2Rad, - coneOuterGain = audioEmitterBehavior.coneOuterGain, - distanceModel = audioEmitterBehavior.distanceModel, - refDistance = audioEmitterBehavior.refDistance, - maxDistance = audioEmitterBehavior.maxDistance, - rolloffFactor = audioEmitterBehavior.rolloffFactor - }; - - audioEmitters.Add(emitter); - - var extension = new KHR_NodeAudioEmitterRef - { - emitter = emitterId - }; - - node.AddExtension(KHR_audio.ExtensionName, extension); - exporter.DeclareExtensionUsage(KHR_audio.ExtensionName); - } - } - - public override void AfterSceneExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot) - { - var globalEmitterBehaviors = Object.FindObjectsOfType(); - - if (globalEmitterBehaviors.Length > 0) - { - var globalEmitterIds = new List(); - - foreach (var emitterBehavior in globalEmitterBehaviors) - { - var audioSourceIds = AddAudioSources(gltfRoot, emitterBehavior.sources); - - var emitterId = new AudioEmitterId - { - Id = audioEmitters.Count, - Root = gltfRoot - }; - - globalEmitterIds.Add(emitterId); - - var globalEmitter = new KHR_AudioEmitter - { - type = "global", - sources = audioSourceIds, - gain = emitterBehavior.gain - }; - - audioEmitters.Add(globalEmitter); - } - - var extension = new KHR_SceneAudioEmittersRef - { - emitters = globalEmitterIds - }; - - var scene = gltfRoot.Scenes[gltfRoot.Scene.Id]; - - scene.AddExtension(KHR_audio.ExtensionName, extension); - exporter.DeclareExtensionUsage(KHR_audio.ExtensionName); - } - - if (audioEmitters.Count > 0) - { - var audioData = new List(); - - for (int i = 0; i < audioDataClips.Count; i++) - { - var audioClip = audioDataClips[i]; - - var path = AssetDatabase.GetAssetPath(audioClip.GetInstanceID()); - - var fileExtension = Path.GetExtension(path); - - if (fileExtension != ".mp3") - { - audioDataClips.Clear(); - audioSourceObjects.Clear(); - audioEmitters.Clear(); - throw new Exception("Unsupported audio file type \"" + fileExtension + "\", only .mp3 is supported."); - } - - var fileName = Path.GetFileName(path); - var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); - var result = exporter.ExportFile(fileName, "audio/mpeg", fileStream); - var audio = new KHR_AudioData - { - uri = result.uri, - mimeType = result.mimeType, - bufferView = result.bufferView, - }; - - audioData.Add(audio); - } - - var audioSources = new List(); - - for (int i = 0; i < audioSourceObjects.Count; i++) - { - var audioSourceObject = audioSourceObjects[i]; - var audioDataIndex = audioDataClips.IndexOf(audioSourceObject.clip); - - var audioSource = new KHR_AudioSource - { - audio = audioDataIndex == -1 ? null : new AudioDataId { Id = audioDataIndex, Root = gltfRoot }, - autoPlay = audioSourceObject.autoPlay, - loop = audioSourceObject.loop, - gain = audioSourceObject.gain, - }; - - audioSources.Add(audioSource); - } - - var extension = new KHR_audio - { - audio = new List(audioData), - sources = new List(audioSources), - emitters = new List(audioEmitters), - }; - - gltfRoot.AddExtension(KHR_audio.ExtensionName, extension); - } - - audioDataClips.Clear(); - audioSourceObjects.Clear(); - audioEmitters.Clear(); - } - - private static List AddAudioSources(GLTFRoot gltfRoot, List sources) - { - var audioSourceIds = new List(); - - foreach (var audioSource in sources) - { - var audioSourceIndex = audioSourceObjects.IndexOf(audioSource); - - if (audioSourceIndex == -1) - { - audioSourceIndex = audioSourceObjects.Count; - audioSourceObjects.Add(audioSource); - } - - if (!audioDataClips.Contains(audioSource.clip)) - { - audioDataClips.Add(audioSource.clip); - } - - var sourceId = new AudioSourceId - { - Id = audioSourceIndex, - Root = gltfRoot - }; - - audioSourceIds.Add(sourceId); - } - - return audioSourceIds; - } - } - - public enum PositionalAudioDistanceModel - { - linear, - inverse, - exponential, - } -} - -#endif \ No newline at end of file diff --git a/Samples~/KHR_audio/KHRAudioSchemas.cs b/Samples~/KHR_audio/KHRAudioSchemas.cs deleted file mode 100644 index 0e02a9593..000000000 --- a/Samples~/KHR_audio/KHRAudioSchemas.cs +++ /dev/null @@ -1,322 +0,0 @@ - -using System; -using System.Collections.Generic; -using GLTF.Schema; -using Newtonsoft.Json.Linq; -using UnityEngine; - -namespace UnityGLTF.Plugins.Experimental -{ - [Serializable] - public class AudioEmitterId : GLTFId { - public AudioEmitterId() - { - } - - public AudioEmitterId(AudioEmitterId id, GLTFRoot newRoot) : base(id, newRoot) - { - } - - public override KHR_AudioEmitter Value - { - get - { - if (Root.Extensions.TryGetValue(KHR_audio.ExtensionName, out IExtension iextension)) - { - KHR_audio extension = iextension as KHR_audio; - return extension.emitters[Id]; - } - else - { - throw new Exception("KHR_audio not found on root object"); - } - } - } - } - - [Serializable] - public class AudioSourceId : GLTFId { - public AudioSourceId() - { - } - - public AudioSourceId(AudioSourceId id, GLTFRoot newRoot) : base(id, newRoot) - { - } - - public override KHR_AudioSource Value - { - get - { - if (Root.Extensions.TryGetValue(KHR_audio.ExtensionName, out IExtension iextension)) - { - KHR_audio extension = iextension as KHR_audio; - return extension.sources[Id]; - } - else - { - throw new Exception("KHR_audio not found on root object"); - } - } - } - } - - [Serializable] - public class AudioDataId : GLTFId { - public AudioDataId() - { - } - - public AudioDataId(AudioDataId id, GLTFRoot newRoot) : base(id, newRoot) - { - } - - public override KHR_AudioData Value - { - get - { - if (Root.Extensions.TryGetValue(KHR_audio.ExtensionName, out IExtension iextension)) - { - KHR_audio extension = iextension as KHR_audio; - return extension.audio[Id]; - } - else - { - throw new Exception("KHR_audio not found on root object"); - } - } - } - } - - [Serializable] - public class KHR_SceneAudioEmittersRef : IExtension { - public List emitters; - - public JProperty Serialize() { - var jo = new JObject(); - JProperty jProperty = new JProperty(KHR_audio.ExtensionName, jo); - - JArray arr = new JArray(); - - foreach (var emitter in emitters) { - arr.Add(emitter.Id); - } - - jo.Add(new JProperty(nameof(emitters), arr)); - - return jProperty; - } - - public IExtension Clone(GLTFRoot root) - { - return new KHR_SceneAudioEmittersRef() { emitters = emitters }; - } - } - - [Serializable] - public class KHR_NodeAudioEmitterRef : IExtension { - public AudioEmitterId emitter; - - public JProperty Serialize() { - var jo = new JObject(); - JProperty jProperty = new JProperty(KHR_audio.ExtensionName, jo); - jo.Add(new JProperty(nameof(emitter), emitter.Id)); - return jProperty; - } - - public IExtension Clone(GLTFRoot root) - { - return new KHR_NodeAudioEmitterRef() { emitter = emitter }; - } - } - - [Serializable] - public class KHR_AudioEmitter : GLTFChildOfRootProperty { - - public string type; - public float gain; - public List sources; - - public virtual JObject Serialize() { - var jo = new JObject(); - - jo.Add(nameof(type), type); - - if (gain != 1.0f) { - jo.Add(nameof(gain), gain); - } - - if (sources != null && sources.Count > 0) { - JArray arr = new JArray(); - - foreach (var source in sources) { - arr.Add(source.Id); - } - - jo.Add(new JProperty(nameof(sources), arr)); - } - - return jo; - } - } - - [Serializable] - public class KHR_PositionalAudioEmitter : KHR_AudioEmitter { - - public float coneInnerAngle; - public float coneOuterAngle; - public float coneOuterGain; - public PositionalAudioDistanceModel distanceModel; - public float maxDistance; - public float refDistance; - public float rolloffFactor; - - public override JObject Serialize() { - var jo = base.Serialize(); - - var positional = new JObject(); - - if (!Mathf.Approximately(coneInnerAngle, Mathf.PI * 2)) { - positional.Add(new JProperty(nameof(coneInnerAngle), coneInnerAngle)); - } - - if (!Mathf.Approximately(coneInnerAngle, Mathf.PI * 2)) { - positional.Add(new JProperty(nameof(coneOuterAngle), coneOuterAngle)); - } - - if (coneOuterGain != 0.0f) { - positional.Add(new JProperty(nameof(coneOuterGain), coneOuterGain)); - } - - if (distanceModel != PositionalAudioDistanceModel.inverse) { - positional.Add(new JProperty(nameof(distanceModel), distanceModel.ToString())); - } - - if (maxDistance != 10000.0f) { - positional.Add(new JProperty(nameof(maxDistance), maxDistance)); - } - - if (refDistance != 1.0f) { - positional.Add(new JProperty(nameof(refDistance), refDistance)); - } - - if (rolloffFactor != 1.0f) { - positional.Add(new JProperty(nameof(rolloffFactor), rolloffFactor)); - } - - jo.Add("positional", positional); - - return jo; - } - } - - [Serializable] - public class KHR_AudioSource : GLTFChildOfRootProperty { - - public bool autoPlay; - public float gain; - public bool loop; - public AudioDataId audio; - - public JObject Serialize() { - var jo = new JObject(); - - if (autoPlay) { - jo.Add(nameof(autoPlay), autoPlay); - } - - if (gain != 1.0f) { - jo.Add(nameof(gain), gain); - } - - if (loop) { - jo.Add(nameof(loop), loop); - } - - if (audio != null) { - jo.Add(nameof(audio), audio.Id); - } - - return jo; - } - } - - [Serializable] - public class KHR_AudioData : GLTFChildOfRootProperty { - - public string uri; - public string mimeType; - public BufferViewId bufferView; - - public JObject Serialize() { - var jo = new JObject(); - - if (uri != null) { - jo.Add(nameof(uri), uri); - } else { - jo.Add(nameof(mimeType), mimeType); - jo.Add(nameof(bufferView), bufferView.Id); - } - - return jo; - } - } - - [Serializable] - public class KHR_audio : IExtension - { - public const string ExtensionName = "KHR_audio"; - - public List audio; - public List sources; - public List emitters; - - public JProperty Serialize() - { - var jo = new JObject(); - JProperty jProperty = new JProperty(ExtensionName, jo); - - if (audio != null && audio.Count > 0) { - JArray audioArr = new JArray(); - - foreach (var audioData in audio) { - audioArr.Add(audioData.Serialize()); - } - - jo.Add(new JProperty(nameof(audio), audioArr)); - } - - - if (sources != null && sources.Count > 0) { - JArray sourceArr = new JArray(); - - foreach (var source in sources) { - sourceArr.Add(source.Serialize()); - } - - jo.Add(new JProperty(nameof(sources), sourceArr)); - } - - if (emitters != null && emitters.Count > 0) { - JArray emitterArr = new JArray(); - - foreach (var emitter in emitters) { - emitterArr.Add(emitter.Serialize()); - } - - jo.Add(new JProperty(nameof(emitters), emitterArr)); - } - - return jProperty; - } - - public IExtension Clone(GLTFRoot root) - { - return new KHR_audio() { - audio = audio, - sources = sources, - emitters = emitters, - }; - } - } -} diff --git a/Samples~/KHR_audio/KHRAudioSchemas.cs.meta b/Samples~/KHR_audio/KHRAudioSchemas.cs.meta deleted file mode 100644 index 6a4660184..000000000 --- a/Samples~/KHR_audio/KHRAudioSchemas.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 41f754afffa448e4b451767beb58178e -timeCreated: 1703888738 \ No newline at end of file diff --git a/Samples~/KHR_audio/KHRGlobalAudioEmitterBehaviour.cs b/Samples~/KHR_audio/KHRGlobalAudioEmitterBehaviour.cs deleted file mode 100644 index a613b1089..000000000 --- a/Samples~/KHR_audio/KHRGlobalAudioEmitterBehaviour.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace UnityGLTF.Plugins.Experimental -{ - public class KHRGlobalAudioEmitterBehaviour : MonoBehaviour - { - public List sources; - public float gain = 1.0f; - } -} \ No newline at end of file diff --git a/Samples~/KHR_audio/KHRGlobalAudioEmitterBehaviour.cs.meta b/Samples~/KHR_audio/KHRGlobalAudioEmitterBehaviour.cs.meta deleted file mode 100644 index 824e39d1d..000000000 --- a/Samples~/KHR_audio/KHRGlobalAudioEmitterBehaviour.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 3b79d559c5804cf9875b0d4a3d6d6b99 -timeCreated: 1703889073 \ No newline at end of file diff --git a/Samples~/KHR_audio/KHRPositionalAudioEmitterBehaviour.cs b/Samples~/KHR_audio/KHRPositionalAudioEmitterBehaviour.cs deleted file mode 100644 index 9692be16b..000000000 --- a/Samples~/KHR_audio/KHRPositionalAudioEmitterBehaviour.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace UnityGLTF.Plugins.Experimental -{ - public class KHRPositionalAudioEmitterBehaviour : MonoBehaviour - { - public List sources; - public float gain = 1.0f; - public float coneInnerAngle = 120.0f; - public float coneOuterAngle = 180.0f; - public float coneOuterGain = 0.0f; - public PositionalAudioDistanceModel distanceModel = PositionalAudioDistanceModel.inverse; - public float refDistance = 1.0f; - public float maxDistance = 10000.0f; - public float rolloffFactor = 1.0f; - - private void OnDrawGizmos() { - #if UNITY_EDITOR - UnityEditor.Handles.color = Color.green; - - UnityEditor.Handles.DrawWireArc( - transform.position, // Center point - transform.up, // Up vector - DirFromAngle(transform, -coneInnerAngle / 2), // Left starting point - coneInnerAngle, // End angle - refDistance // Radius - ); - - UnityEditor.Handles.DrawLine( - transform.position, - transform.position + (DirFromAngle(transform, -coneInnerAngle / 2) * refDistance) - ); - - UnityEditor.Handles.DrawLine( - transform.position, - transform.position + (DirFromAngle(transform, coneInnerAngle / 2) * refDistance) - ); - - UnityEditor.Handles.color = Color.yellow; - - var halfOuterAngle = (coneOuterAngle - coneInnerAngle) / 2; - - UnityEditor.Handles.DrawWireArc( - transform.position, // Center point - transform.up, // Up vector - DirFromAngle(transform, -coneOuterAngle / 2), // Left starting point - halfOuterAngle, // End angle - refDistance // Radius - ); - - UnityEditor.Handles.DrawWireArc( - transform.position, // Center point - transform.up, // Up vector - DirFromAngle(transform, coneOuterAngle / 2), // Left starting point - -halfOuterAngle, // End angle - refDistance // Radius - ); - - UnityEditor.Handles.DrawLine( - transform.position, - transform.position + (DirFromAngle(transform, -coneOuterAngle / 2) * refDistance) - ); - - UnityEditor.Handles.DrawLine( - transform.position, - transform.position + (DirFromAngle(transform, coneOuterAngle / 2) * refDistance) - ); - #endif - } - - public Vector3 DirFromAngle(Transform _transform, float angleInDegrees) - { - var angle = _transform.localEulerAngles.y + angleInDegrees; - return new Vector3(Mathf.Sin(angle * Mathf.Deg2Rad), 0, Mathf.Cos(angle * Mathf.Deg2Rad)); - } - } -} \ No newline at end of file diff --git a/Samples~/KHR_audio/KHRPositionalAudioEmitterBehaviour.cs.meta b/Samples~/KHR_audio/KHRPositionalAudioEmitterBehaviour.cs.meta deleted file mode 100644 index 5ee3e33fb..000000000 --- a/Samples~/KHR_audio/KHRPositionalAudioEmitterBehaviour.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: e1ccaea265644ccd83d78bbec6045999 -timeCreated: 1703888952 \ No newline at end of file diff --git a/Samples~/KHR_audio/UnityGLTF.Plugins.KHR_audio.asmdef b/Samples~/KHR_audio/UnityGLTF.Plugins.KHR_audio.asmdef deleted file mode 100644 index d5177d1e4..000000000 --- a/Samples~/KHR_audio/UnityGLTF.Plugins.KHR_audio.asmdef +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "UnityGLTF.Plugins.KHR_audio", - "rootNamespace": "", - "references": [ - "GUID:18d18f811ba286c49814567a3cfba688", - "GUID:40f39bff7bc9be34182ebe488fcf8228" - ], - "includePlatforms": [], - "excludePlatforms": [], - "allowUnsafeCode": false, - "overrideReferences": true, - "precompiledReferences": [ - "Newtonsoft.Json.dll" - ], - "autoReferenced": false, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false -} \ No newline at end of file diff --git a/Samples~/KHR_audio/UnityGLTF.Plugins.KHR_audio.asmdef.meta b/Samples~/KHR_audio/UnityGLTF.Plugins.KHR_audio.asmdef.meta deleted file mode 100644 index 2e7061b17..000000000 --- a/Samples~/KHR_audio/UnityGLTF.Plugins.KHR_audio.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 76673ba94a1121243b883a319da22cec -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: