From fdd1ad26d667476e4c656ce36486f8a3f74a2198 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Fri, 4 Apr 2025 11:41:13 +0200 Subject: [PATCH 01/22] added initial KHR_audio: schema, plugins, and first draft export --- .../GLTFSerialization/Extensions/KHR_audio.cs | 372 ++++++++++++++++++ .../Extensions/KHR_audio.cs.meta | 11 + Runtime/Scripts/Plugins/AudioExport.cs | 206 ++++++++++ Runtime/Scripts/Plugins/AudioExport.cs.meta | 3 + Runtime/Scripts/Plugins/AudioImport.cs | 24 ++ Runtime/Scripts/Plugins/AudioImport.cs.meta | 3 + 6 files changed, 619 insertions(+) create mode 100644 Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs create mode 100644 Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs.meta create mode 100644 Runtime/Scripts/Plugins/AudioExport.cs create mode 100644 Runtime/Scripts/Plugins/AudioExport.cs.meta create mode 100644 Runtime/Scripts/Plugins/AudioImport.cs create mode 100644 Runtime/Scripts/Plugins/AudioImport.cs.meta diff --git a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs new file mode 100644 index 000000000..364809d56 --- /dev/null +++ b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.Generic; +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.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 static string ExtensionName => KHR_audio.ExtensionName; + public List emitters = new List(); + + 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 static string ExtensionName => KHR_audio.ExtensionName; + 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 name; + public string type; + public float gain; + public List sources = new List(); + + 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 (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 minDistance; + 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 (minDistance != 0) + { + positional.Add(new JProperty(nameof(minDistance), minDistance)); + } + + 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 string name; + 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); + } + + if (!string.IsNullOrEmpty(name)) + { + jo.Add(nameof(name), name); + } + + 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_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() + { + audio = audio, + sources = sources, + emitters = emitters, + }; + } + } +} \ No newline at end of file diff --git a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs.meta b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs.meta new file mode 100644 index 000000000..d1e7d2e85 --- /dev/null +++ b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 49076143c3c264548a7d3734f637b0f3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Plugins/AudioExport.cs b/Runtime/Scripts/Plugins/AudioExport.cs new file mode 100644 index 000000000..916517137 --- /dev/null +++ b/Runtime/Scripts/Plugins/AudioExport.cs @@ -0,0 +1,206 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using GLTF.Schema; +using UnityEditor; +using UnityEngine; + +namespace UnityGLTF.Plugins +{ + public class AudioExport: GLTFExportPlugin + { + public override string DisplayName => "KHR_audio"; + public override string Description => "Exports positional and global audio sources and .mp3 audio clips."; + public override GLTFExportPluginContext CreateInstance(ExportContext context) + { + return new AudioExportContext(context); + } + } + + public class AudioExportContext: GLTFExportPluginContext + { + private ExportContext _context; + private List _audioSourceIds = new(); + private KHR_audio _audioExtension; + private KHR_SceneAudioEmittersRef _sceneExtension = null; + + private bool _saveAudioToFile; + private Dictionary _audioSourceToEmitter = new(); + private Dictionary _audioSourceToNode = new(); + + + public class AudioDescription + { + public int Id; + public string Name; + public AudioClip Clip; + } + + public AudioExportContext(ExportContext context) + { + _context = context; + _saveAudioToFile = false; + } + + 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 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]; + + KHR_AudioEmitter emitter; + if (isGlobal) + { + emitter = new KHR_AudioEmitter + { + type = "global", + gain = firstAudioSource.volume, + name = "global emitter", + }; + } + else + { + emitter = new KHR_PositionalAudioEmitter() + { + type = "positional", + gain = firstAudioSource.volume, + minDistance = firstAudioSource.minDistance, + maxDistance = firstAudioSource.maxDistance, + distanceModel = PositionalAudioDistanceModel.linear, + name = "positional emitter" + }; + } + 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 result = exporter.ExportFile(fileName, "audio/mpeg", fileStream); + audio.mimeType = result.mimeType; + audio.bufferView = result.bufferView; + _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(); + if (gltfRoot != null) + { + gltfRoot.AddExtension(KHR_audio.ExtensionName, _audioExtension); + exporter.DeclareExtensionUsage(KHR_audio.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); + + } + } + +} \ No newline at end of file diff --git a/Runtime/Scripts/Plugins/AudioExport.cs.meta b/Runtime/Scripts/Plugins/AudioExport.cs.meta new file mode 100644 index 000000000..6a67fdf25 --- /dev/null +++ b/Runtime/Scripts/Plugins/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/AudioImport.cs b/Runtime/Scripts/Plugins/AudioImport.cs new file mode 100644 index 000000000..90018c2f1 --- /dev/null +++ b/Runtime/Scripts/Plugins/AudioImport.cs @@ -0,0 +1,24 @@ +namespace UnityGLTF.Plugins +{ + public class AudioImport : GLTFImportPlugin + { + public override string DisplayName => "KHR_audio"; + public override string Description => "Import positional and global audio sources and .mp3 audio clips."; + + public override GLTFImportPluginContext CreateInstance(GLTFImportContext context) + { + return new AudioImportContext(context); + } + + } + + public class AudioImportContext : GLTFImportPluginContext + { + private GLTFImportContext _context; + + public AudioImportContext(GLTFImportContext context) + { + _context = context; + } + } +} \ No newline at end of file diff --git a/Runtime/Scripts/Plugins/AudioImport.cs.meta b/Runtime/Scripts/Plugins/AudioImport.cs.meta new file mode 100644 index 000000000..f0eb3baee --- /dev/null +++ b/Runtime/Scripts/Plugins/AudioImport.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d106e4b0674b44e09e9c8d85ba6216df +timeCreated: 1743751243 \ No newline at end of file From 700aed703de4227b2bcae18540b3a3d3dffe7cc1 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Fri, 4 Apr 2025 11:51:51 +0200 Subject: [PATCH 02/22] fixed 2d <> 3d blend sorting --- Runtime/Scripts/Plugins/AudioExport.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Runtime/Scripts/Plugins/AudioExport.cs b/Runtime/Scripts/Plugins/AudioExport.cs index 916517137..a3eaf0a28 100644 --- a/Runtime/Scripts/Plugins/AudioExport.cs +++ b/Runtime/Scripts/Plugins/AudioExport.cs @@ -144,8 +144,8 @@ public override void AfterNodeExport(GLTFSceneExporter exporter, GLTFRoot gltfRo 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) ); + 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) { From d2b76cd56fb2881f56be13e77daf464d847876b4 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Fri, 4 Apr 2025 12:06:50 +0200 Subject: [PATCH 03/22] renamed extension KHR_audio to KHR_audio_emitter --- .../{KHR_audio.cs => KHR_audio_emitter.cs} | 24 +++++++++---------- ...udio.cs.meta => KHR_audio_emitter.cs.meta} | 0 Runtime/Scripts/Plugins/AudioExport.cs | 8 +++---- 3 files changed, 16 insertions(+), 16 deletions(-) rename Runtime/Plugins/GLTFSerialization/Extensions/{KHR_audio.cs => KHR_audio_emitter.cs} (90%) rename Runtime/Plugins/GLTFSerialization/Extensions/{KHR_audio.cs.meta => KHR_audio_emitter.cs.meta} (100%) diff --git a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs similarity index 90% rename from Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs rename to Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs index 364809d56..0afbfe115 100644 --- a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs +++ b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs @@ -26,9 +26,9 @@ public override KHR_AudioEmitter Value { get { - if (Root.Extensions.TryGetValue(KHR_audio.ExtensionName, out IExtension iextension)) + if (Root.Extensions.TryGetValue(KHR_audio_emitter.ExtensionName, out IExtension iextension)) { - KHR_audio extension = iextension as KHR_audio; + KHR_audio_emitter extension = iextension as KHR_audio_emitter; return extension.emitters[Id]; } else @@ -54,9 +54,9 @@ public override KHR_AudioSource Value { get { - if (Root.Extensions.TryGetValue(KHR_audio.ExtensionName, out IExtension iextension)) + if (Root.Extensions.TryGetValue(KHR_audio_emitter.ExtensionName, out IExtension iextension)) { - KHR_audio extension = iextension as KHR_audio; + KHR_audio_emitter extension = iextension as KHR_audio_emitter; return extension.sources[Id]; } else @@ -82,9 +82,9 @@ public override KHR_AudioData Value { get { - if (Root.Extensions.TryGetValue(KHR_audio.ExtensionName, out IExtension iextension)) + if (Root.Extensions.TryGetValue(KHR_audio_emitter.ExtensionName, out IExtension iextension)) { - KHR_audio extension = iextension as KHR_audio; + KHR_audio_emitter extension = iextension as KHR_audio_emitter; return extension.audio[Id]; } else @@ -98,13 +98,13 @@ public override KHR_AudioData Value [Serializable] public class KHR_SceneAudioEmittersRef : IExtension { - public static string ExtensionName => KHR_audio.ExtensionName; + 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.ExtensionName, jo); + JProperty jProperty = new JProperty(KHR_audio_emitter.ExtensionName, jo); JArray arr = new JArray(); @@ -127,13 +127,13 @@ public IExtension Clone(GLTFRoot root) [Serializable] public class KHR_NodeAudioEmitterRef : IExtension { - public static string ExtensionName => KHR_audio.ExtensionName; + public static string ExtensionName => KHR_audio_emitter.ExtensionName; public AudioEmitterId emitter; public JProperty Serialize() { var jo = new JObject(); - JProperty jProperty = new JProperty(KHR_audio.ExtensionName, jo); + JProperty jProperty = new JProperty(KHR_audio_emitter.ExtensionName, jo); jo.Add(new JProperty(nameof(emitter), emitter.Id)); return jProperty; } @@ -306,7 +306,7 @@ public JObject Serialize() } [Serializable] - public class KHR_audio : IExtension + public class KHR_audio_emitter : IExtension { public const string ExtensionName = "KHR_audio_emitter"; @@ -361,7 +361,7 @@ public JProperty Serialize() public IExtension Clone(GLTFRoot root) { - return new KHR_audio() + return new KHR_audio_emitter() { audio = audio, sources = sources, diff --git a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs.meta b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs.meta similarity index 100% rename from Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio.cs.meta rename to Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs.meta diff --git a/Runtime/Scripts/Plugins/AudioExport.cs b/Runtime/Scripts/Plugins/AudioExport.cs index a3eaf0a28..ea659ac36 100644 --- a/Runtime/Scripts/Plugins/AudioExport.cs +++ b/Runtime/Scripts/Plugins/AudioExport.cs @@ -21,7 +21,7 @@ public class AudioExportContext: GLTFExportPluginContext { private ExportContext _context; private List _audioSourceIds = new(); - private KHR_audio _audioExtension; + private KHR_audio_emitter _audioExtension; private KHR_SceneAudioEmittersRef _sceneExtension = null; private bool _saveAudioToFile; @@ -149,11 +149,11 @@ public override void AfterNodeExport(GLTFSceneExporter exporter, GLTFRoot gltfRo if (_audioExtension == null) { - _audioExtension = new KHR_audio(); + _audioExtension = new KHR_audio_emitter(); if (gltfRoot != null) { - gltfRoot.AddExtension(KHR_audio.ExtensionName, _audioExtension); - exporter.DeclareExtensionUsage(KHR_audio.ExtensionName); + gltfRoot.AddExtension(KHR_audio_emitter.ExtensionName, _audioExtension); + exporter.DeclareExtensionUsage(KHR_audio_emitter.ExtensionName); } } From 6d963505e8bc44052a6a70ae8381a5174e429d27 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Fri, 4 Apr 2025 14:00:37 +0200 Subject: [PATCH 04/22] fixed missing uri assignment in audio exporter --- Runtime/Scripts/Plugins/AudioExport.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Runtime/Scripts/Plugins/AudioExport.cs b/Runtime/Scripts/Plugins/AudioExport.cs index ea659ac36..d328b94f2 100644 --- a/Runtime/Scripts/Plugins/AudioExport.cs +++ b/Runtime/Scripts/Plugins/AudioExport.cs @@ -113,8 +113,16 @@ private AudioEmitterId ProcessAudioSource(bool isGlobal, AudioSource[] audioSour var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); var result = exporter.ExportFile(fileName, "audio/mpeg", fileStream); - audio.mimeType = result.mimeType; - audio.bufferView = result.bufferView; + + if (string.IsNullOrEmpty(result.uri)) + { + audio.mimeType = result.mimeType; + audio.bufferView = result.bufferView; + } + else + { + audio.uri = result.uri; + } _audioExtension.audio.Add(audio); } From beda0d27985a64fcf1fc2e768d8ff56029ebfdcf Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Fri, 4 Apr 2025 16:12:32 +0200 Subject: [PATCH 05/22] added audio deserialization --- .../Extensions/KHR_audio_emitter.cs | 194 ++++++++++++++++-- .../Extensions/KHR_audio_emitterFactory.cs | 64 ++++++ .../KHR_audio_emitterFactory.cs.meta | 3 + .../GLTFSerialization/Schema/GLTFProperty.cs | 1 + Runtime/Scripts/Plugins/AudioExport.cs | 2 +- 5 files changed, 244 insertions(+), 20 deletions(-) create mode 100644 Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitterFactory.cs create mode 100644 Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitterFactory.cs.meta diff --git a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs index 0afbfe115..dcf445b2e 100644 --- a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs +++ b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using GLTF.Extensions; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace GLTF.Schema @@ -37,6 +39,15 @@ public override KHR_AudioEmitter Value } } } + + public static AudioEmitterId Deserialize(GLTFRoot root, JsonReader reader) + { + return new AudioEmitterId + { + Id = reader.ReadAsInt32().Value, + Root = root + }; + } } [Serializable] @@ -65,6 +76,15 @@ public override KHR_AudioSource Value } } } + + public static AudioSourceId Deserialize(GLTFRoot root, JsonReader reader) + { + return new AudioSourceId + { + Id = reader.ReadAsInt32().Value, + Root = root + }; + } } [Serializable] @@ -93,6 +113,15 @@ public override KHR_AudioData Value } } } + + public static AudioDataId Deserialize(GLTFRoot root, JsonReader reader) + { + return new AudioDataId + { + Id = reader.ReadAsInt32().Value, + Root = root + }; + } } [Serializable] @@ -122,6 +151,23 @@ 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] @@ -142,6 +188,19 @@ 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; + } } [Serializable] @@ -179,6 +238,43 @@ public virtual JObject Serialize() 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; + } + } + + return emitter; + } } [Serializable] @@ -244,39 +340,67 @@ public override JObject Serialize() [Serializable] public class KHR_AudioSource : GLTFChildOfRootProperty { - public string name; - public bool autoPlay; - public float gain; - public bool loop; + 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 (autoPlay) { - jo.Add(nameof(autoPlay), autoPlay); - // } + if (gain != null) + jo.Add(nameof(gain), gain); - // if (gain != 1.0f) { - jo.Add(nameof(gain), gain); - // } + if (loop != null) + jo.Add(nameof(loop), loop); + + if (audio != null) + jo.Add(nameof(audio), audio.Id); - // if (loop) { - jo.Add(nameof(loop), loop); - // } + if (!string.IsNullOrEmpty(Name)) + jo.Add("name", Name); - if (audio != null) + return jo; + } + + public static KHR_AudioSource Deserialize(GLTFRoot root, JsonReader reader) + { + var audioSource = new KHR_AudioSource(); + + if (reader.Read() && reader.TokenType != JsonToken.StartObject) { - jo.Add(nameof(audio), audio.Id); + throw new Exception("AudioSource must be an object."); } - if (!string.IsNullOrEmpty(name)) + while (reader.Read() && reader.TokenType == JsonToken.PropertyName) { - jo.Add(nameof(name), name); - } + var curProp = reader.Value.ToString(); - return jo; + 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; } } @@ -303,6 +427,38 @@ public JObject Serialize() 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.Name): + audioData.Name = reader.ReadAsString(); + break; + case nameof(KHR_AudioData.bufferView): + audioData.bufferView = BufferViewId.Deserialize(root, reader); + break; + } + } + return audioData; + } } [Serializable] 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/Plugins/AudioExport.cs b/Runtime/Scripts/Plugins/AudioExport.cs index d328b94f2..984e30a24 100644 --- a/Runtime/Scripts/Plugins/AudioExport.cs +++ b/Runtime/Scripts/Plugins/AudioExport.cs @@ -135,7 +135,7 @@ private AudioEmitterId ProcessAudioSource(bool isGlobal, AudioSource[] audioSour loop = firstAudioSource.loop, gain = firstAudioSource.volume, // TODO: uniquename required? - name = audioSourceId.Clip.name + Name = audioSourceId.Clip.name }; _audioExtension.sources.Add(khrAudio); From 7abe7e9a52dee326d22ae2b1ef7c352116cf567a Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Fri, 4 Apr 2025 17:08:41 +0200 Subject: [PATCH 06/22] Audio: inital import (WIP) --- Runtime/Scripts/Plugins/AudioImport.cs | 147 +++++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/Runtime/Scripts/Plugins/AudioImport.cs b/Runtime/Scripts/Plugins/AudioImport.cs index 90018c2f1..e118f0a7a 100644 --- a/Runtime/Scripts/Plugins/AudioImport.cs +++ b/Runtime/Scripts/Plugins/AudioImport.cs @@ -1,3 +1,9 @@ +using System.Collections.Generic; +using GLTF.Schema; +using UnityEditor; +using UnityEngine; +using UnityEngine.Networking; + namespace UnityGLTF.Plugins { public class AudioImport : GLTFImportPlugin @@ -15,10 +21,151 @@ public override GLTFImportPluginContext CreateInstance(GLTFImportContext context 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(); public AudioImportContext(GLTFImportContext context) { _context = context; } + + 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 AudioClip GetAudioClip(KHR_AudioData audio) + { + return null; + } + + private void AssignClips() + { + foreach (var ac in _assignClips) + { + if (_audioClips.TryGetValue(ac.AudioDataId.Id, out var audioClip)) + { + ac.audioSource.clip = audioClip; + } + else + { + Debug.LogWarning($"Audio clip not found for AudioDataId {ac.AudioDataId}"); + } + } + } + + 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; + } + // TODO: save buffer as file and load it with UnityWebRequestMultimedia.GetAudioClip() + AudioClip clip = null; + _audioClips.Add(index, clip); + + } + else + { + Debug.LogWarning($"Audio buffer view not found for {audio.Name}"); + } + } + } + + public override void OnAfterImportScene(GLTFScene scene, int sceneIndex, GameObject sceneObject) + { + //TODO: when import as asset, we should create the audio clips in OnAfterImport to add the clips as subAssets + CreateAudioClips(); + AssignClips(); + } + + public override void OnAfterImport() + { +#if UNIT_EDITOR + + // + // foreach (var audio in _audioClips) + // { + // _context.AssetContext.AddObjectToAsset(audio.name, audio); + // } + +#endif + } + + 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; + foreach (var source in emitter.sources) + { + if (source == null) + continue; + + // TODO: set parameters + + var audioSource = nodeObject.AddComponent(); + _assignClips.Add(new AssignClip(audioSource, source.Value.audio)); + audioSource.spatialBlend = 1.0f; // Set to 3D sound + audioSource.rolloffMode = AudioRolloffMode.Linear; + audioSource.minDistance = 1.0f; + audioSource.maxDistance = 100.0f; + } + } + else + { + Debug.LogWarning($"Audio source not found for node {node.Name}"); + } + } + + } } } \ No newline at end of file From 9dd08888329746a7f9bbf609bae46039f511ff22 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Fri, 4 Apr 2025 17:09:03 +0200 Subject: [PATCH 07/22] added new GetBufferViewData for public access to buffers --- Runtime/Scripts/GLTFSceneImporter.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Runtime/Scripts/GLTFSceneImporter.cs b/Runtime/Scripts/GLTFSceneImporter.cs index 4b945ed2b..d0dbdf587 100644 --- a/Runtime/Scripts/GLTFSceneImporter.cs +++ b/Runtime/Scripts/GLTFSceneImporter.cs @@ -778,6 +778,13 @@ private void GetGltfContentTotals(GLTFScene scene) progress?.Report(progressStatus); } + public NativeArray GetBufferViewData(BufferView bufferView) + { + GetBufferData(bufferView.Buffer).Wait(); + GLTFHelpers.LoadBufferView(bufferView, 0, _assetCache.BufferCache[bufferView.Buffer.Id].bufferData, out var bufferViewCache); + return bufferViewCache; + } + private async Task GetBufferData(BufferId bufferId) { if (bufferId == null) return null; From 9b788a3ef9270bb6d6cec4b9aa2ef74dc4b85c35 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Mon, 7 Apr 2025 16:54:31 +0200 Subject: [PATCH 08/22] added Generic Object Cache to track AudioClips in runtime loaded files --- Runtime/Scripts/Cache/RefCountedCacheData.cs | 16 +++++++++++++++- Runtime/Scripts/GLTFSceneImporter.cs | 10 +++++++++- Runtime/Scripts/Plugins/AudioImport.cs | 2 ++ 3 files changed, 26 insertions(+), 2 deletions(-) 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 d0dbdf587..c8da9280f 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() ); } diff --git a/Runtime/Scripts/Plugins/AudioImport.cs b/Runtime/Scripts/Plugins/AudioImport.cs index e118f0a7a..bc8b10199 100644 --- a/Runtime/Scripts/Plugins/AudioImport.cs +++ b/Runtime/Scripts/Plugins/AudioImport.cs @@ -110,6 +110,8 @@ private void CreateAudioClips() Debug.LogWarning($"Audio buffer view not found for {audio.Name}"); } } + + _context.SceneImporter.GenericObjectReferences.AddRange(_audioClips.Select( kvp => kvp.Value).ToArray()); } public override void OnAfterImportScene(GLTFScene scene, int sceneIndex, GameObject sceneObject) From 4bebea2fdb83ac4a8c2560c4a07776ac985a3fa0 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Tue, 8 Apr 2025 08:59:14 +0200 Subject: [PATCH 09/22] removed AudioData.Name from deserialization --- .../Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs index dcf445b2e..d2e71fda5 100644 --- a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs +++ b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs @@ -449,9 +449,6 @@ public static KHR_AudioData Deserialize(GLTFRoot root, JsonReader reader) case nameof(KHR_AudioData.uri): audioData.uri = reader.ReadAsString(); break; - case nameof(KHR_AudioData.Name): - audioData.Name = reader.ReadAsString(); - break; case nameof(KHR_AudioData.bufferView): audioData.bufferView = BufferViewId.Deserialize(root, reader); break; From 2095c237915f157052b0fbce4f0da1b803869b5f Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Tue, 8 Apr 2025 08:59:51 +0200 Subject: [PATCH 10/22] Audio Export: added support for wav, mp3 and ogg --- Runtime/Scripts/Plugins/AudioExport.cs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Runtime/Scripts/Plugins/AudioExport.cs b/Runtime/Scripts/Plugins/AudioExport.cs index 984e30a24..4a1320a42 100644 --- a/Runtime/Scripts/Plugins/AudioExport.cs +++ b/Runtime/Scripts/Plugins/AudioExport.cs @@ -61,6 +61,18 @@ private AudioDescription AddAudioSource(AudioSource audioSource, out bool isNew) 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(); @@ -110,9 +122,14 @@ private AudioEmitterId ProcessAudioSource(bool isGlobal, AudioSource[] audioSour var fileName = Path.GetFileName(path); var audio = new KHR_AudioData(); - var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); - var result = exporter.ExportFile(fileName, "audio/mpeg", fileStream); + 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)) { From 3c16724e462c3c1dd0e0d3627563579dc8a8bee1 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Tue, 8 Apr 2025 09:02:10 +0200 Subject: [PATCH 11/22] added support for Audio Import as Assets --- Runtime/Scripts/Plugins/Audio.meta | 3 + .../Scripts/Plugins/Audio/TempAssignClip.cs | 13 + .../Plugins/Audio/TempAssignClip.cs.meta | 3 + Runtime/Scripts/Plugins/AudioImport.cs | 249 +++++++++++++++--- 4 files changed, 229 insertions(+), 39 deletions(-) create mode 100644 Runtime/Scripts/Plugins/Audio.meta create mode 100644 Runtime/Scripts/Plugins/Audio/TempAssignClip.cs create mode 100644 Runtime/Scripts/Plugins/Audio/TempAssignClip.cs.meta 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/AudioImport.cs b/Runtime/Scripts/Plugins/AudioImport.cs index bc8b10199..55fae6122 100644 --- a/Runtime/Scripts/Plugins/AudioImport.cs +++ b/Runtime/Scripts/Plugins/AudioImport.cs @@ -1,15 +1,21 @@ +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using GLTF.Schema; -using UnityEditor; using UnityEngine; using UnityEngine.Networking; +using UnityGLTF.Plugins.Audio; +#if UNITY_EDITOR +using UnityEditor; +#endif namespace UnityGLTF.Plugins { public class AudioImport : GLTFImportPlugin { public override string DisplayName => "KHR_audio"; - public override string Description => "Import positional and global audio sources and .mp3 audio clips."; + public override string Description => "Import positional and global audio sources (Wav, Mp3, Ogg)"; public override GLTFImportPluginContext CreateInstance(GLTFImportContext context) { @@ -18,6 +24,40 @@ public override GLTFImportPluginContext CreateInstance(GLTFImportContext 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; @@ -38,10 +78,22 @@ public AssignClip(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) @@ -66,12 +118,7 @@ public override void OnAfterImportRoot(GLTFRoot gltfRoot) { GetExtension(gltfRoot); } - - private AudioClip GetAudioClip(KHR_AudioData audio) - { - return null; - } - + private void AssignClips() { foreach (var ac in _assignClips) @@ -82,11 +129,72 @@ private void AssignClips() } 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; @@ -97,13 +205,54 @@ private void CreateAudioClips() { 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; } - // TODO: save buffer as file and load it with UnityWebRequestMultimedia.GetAudioClip() - AudioClip clip = null; +#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 { @@ -114,26 +263,61 @@ private void CreateAudioClips() _context.SceneImporter.GenericObjectReferences.AddRange(_audioClips.Select( kvp => kvp.Value).ToArray()); } - public override void OnAfterImportScene(GLTFScene scene, int sceneIndex, GameObject sceneObject) + private void AddGlobalEmitters(GameObject sceneObject) { - //TODO: when import as asset, we should create the audio clips in OnAfterImport to add the clips as subAssets - CreateAudioClips(); - AssignClips(); + 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); + } + } + } } - public override void OnAfterImport() + private void AddEmitter(KHR_AudioEmitter emitter, GameObject toGameObject, bool isGlobal) { -#if UNIT_EDITOR - - // - // foreach (var audio in _audioClips) - // { - // _context.AssetContext.AddObjectToAsset(audio.name, audio); - // } - -#endif + 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; + audioSource.rolloffMode = AudioRolloffMode.Linear; + audioSource.minDistance = 1.0f; + audioSource.maxDistance = 100.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) @@ -147,20 +331,7 @@ public override void OnAfterImportNode(Node node, int nodeIndex, GameObject node if (audioEmitterRef.emitter != null) { var emitter = audioEmitterRef.emitter.Value; - foreach (var source in emitter.sources) - { - if (source == null) - continue; - - // TODO: set parameters - - var audioSource = nodeObject.AddComponent(); - _assignClips.Add(new AssignClip(audioSource, source.Value.audio)); - audioSource.spatialBlend = 1.0f; // Set to 3D sound - audioSource.rolloffMode = AudioRolloffMode.Linear; - audioSource.minDistance = 1.0f; - audioSource.maxDistance = 100.0f; - } + AddEmitter(emitter, nodeObject, false); } else { From d8712eed1c66e218a03eb9395f92503e68532f49 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Tue, 8 Apr 2025 10:01:40 +0200 Subject: [PATCH 12/22] fixed wrong offset in GetBufferViewData --- Runtime/Scripts/GLTFSceneImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/Scripts/GLTFSceneImporter.cs b/Runtime/Scripts/GLTFSceneImporter.cs index c8da9280f..73465972d 100644 --- a/Runtime/Scripts/GLTFSceneImporter.cs +++ b/Runtime/Scripts/GLTFSceneImporter.cs @@ -789,7 +789,7 @@ private void GetGltfContentTotals(GLTFScene scene) public NativeArray GetBufferViewData(BufferView bufferView) { GetBufferData(bufferView.Buffer).Wait(); - GLTFHelpers.LoadBufferView(bufferView, 0, _assetCache.BufferCache[bufferView.Buffer.Id].bufferData, out var bufferViewCache); + GLTFHelpers.LoadBufferView(bufferView, _assetCache.BufferCache[bufferView.Buffer.Id].ChunkOffset, _assetCache.BufferCache[bufferView.Buffer.Id].bufferData, out var bufferViewCache); return bufferViewCache; } From e5ebc81801e89d5fce208590f22fc5f3be12be3d Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Tue, 8 Apr 2025 10:15:11 +0200 Subject: [PATCH 13/22] Audio plugins: set enabledbydefault to false --- Runtime/Scripts/Plugins/AudioExport.cs | 1 + Runtime/Scripts/Plugins/AudioImport.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Runtime/Scripts/Plugins/AudioExport.cs b/Runtime/Scripts/Plugins/AudioExport.cs index 4a1320a42..802b7a593 100644 --- a/Runtime/Scripts/Plugins/AudioExport.cs +++ b/Runtime/Scripts/Plugins/AudioExport.cs @@ -9,6 +9,7 @@ namespace UnityGLTF.Plugins { public class AudioExport: GLTFExportPlugin { + public override bool EnabledByDefault => false; public override string DisplayName => "KHR_audio"; public override string Description => "Exports positional and global audio sources and .mp3 audio clips."; public override GLTFExportPluginContext CreateInstance(ExportContext context) diff --git a/Runtime/Scripts/Plugins/AudioImport.cs b/Runtime/Scripts/Plugins/AudioImport.cs index 55fae6122..f49d0d419 100644 --- a/Runtime/Scripts/Plugins/AudioImport.cs +++ b/Runtime/Scripts/Plugins/AudioImport.cs @@ -14,6 +14,7 @@ namespace UnityGLTF.Plugins { public class AudioImport : GLTFImportPlugin { + public override bool EnabledByDefault => false; public override string DisplayName => "KHR_audio"; public override string Description => "Import positional and global audio sources (Wav, Mp3, Ogg)"; From 77eb9084f4838da12b7050b6bb3b723a96db4323 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Tue, 8 Apr 2025 10:27:51 +0200 Subject: [PATCH 14/22] deleted KHR_audio Sample --- Samples~/KHR_audio.meta | 8 - .../KHR_audio/AudioSourceScriptableObject.cs | 13 - .../AudioSourceScriptableObject.cs.meta | 3 - Samples~/KHR_audio/KHRAudioPlugin.cs | 215 ------------ Samples~/KHR_audio/KHRAudioPlugin.cs.meta | 11 - Samples~/KHR_audio/KHRAudioSchemas.cs | 322 ------------------ Samples~/KHR_audio/KHRAudioSchemas.cs.meta | 3 - .../KHRGlobalAudioEmitterBehaviour.cs | 11 - .../KHRGlobalAudioEmitterBehaviour.cs.meta | 3 - .../KHRPositionalAudioEmitterBehaviour.cs | 78 ----- ...KHRPositionalAudioEmitterBehaviour.cs.meta | 3 - .../UnityGLTF.Plugins.KHR_audio.asmdef | 19 -- .../UnityGLTF.Plugins.KHR_audio.asmdef.meta | 7 - 13 files changed, 696 deletions(-) delete mode 100644 Samples~/KHR_audio.meta delete mode 100644 Samples~/KHR_audio/AudioSourceScriptableObject.cs delete mode 100644 Samples~/KHR_audio/AudioSourceScriptableObject.cs.meta delete mode 100644 Samples~/KHR_audio/KHRAudioPlugin.cs delete mode 100644 Samples~/KHR_audio/KHRAudioPlugin.cs.meta delete mode 100644 Samples~/KHR_audio/KHRAudioSchemas.cs delete mode 100644 Samples~/KHR_audio/KHRAudioSchemas.cs.meta delete mode 100644 Samples~/KHR_audio/KHRGlobalAudioEmitterBehaviour.cs delete mode 100644 Samples~/KHR_audio/KHRGlobalAudioEmitterBehaviour.cs.meta delete mode 100644 Samples~/KHR_audio/KHRPositionalAudioEmitterBehaviour.cs delete mode 100644 Samples~/KHR_audio/KHRPositionalAudioEmitterBehaviour.cs.meta delete mode 100644 Samples~/KHR_audio/UnityGLTF.Plugins.KHR_audio.asmdef delete mode 100644 Samples~/KHR_audio/UnityGLTF.Plugins.KHR_audio.asmdef.meta 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/KHRAudioPlugin.cs.meta b/Samples~/KHR_audio/KHRAudioPlugin.cs.meta deleted file mode 100644 index 7b2759e7c..000000000 --- a/Samples~/KHR_audio/KHRAudioPlugin.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 664d6e75fec3044eb9274db51fbb70da -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: 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: From 2c824be91f65cb756a9de245cd099651a26bf3f3 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Tue, 8 Apr 2025 10:59:57 +0200 Subject: [PATCH 15/22] changed Audio Export plugin description --- Runtime/Scripts/Plugins/AudioExport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/Scripts/Plugins/AudioExport.cs b/Runtime/Scripts/Plugins/AudioExport.cs index 802b7a593..3a3a8c3b9 100644 --- a/Runtime/Scripts/Plugins/AudioExport.cs +++ b/Runtime/Scripts/Plugins/AudioExport.cs @@ -11,7 +11,7 @@ public class AudioExport: GLTFExportPlugin { public override bool EnabledByDefault => false; public override string DisplayName => "KHR_audio"; - public override string Description => "Exports positional and global audio sources and .mp3 audio clips."; + public override string Description => "Exports positional and global audio sources"; public override GLTFExportPluginContext CreateInstance(ExportContext context) { return new AudioExportContext(context); From 2bfa171b88ddf44001e06e85874d5902d5edd334 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Tue, 8 Apr 2025 11:00:41 +0200 Subject: [PATCH 16/22] changed audio emitter import creation to split global/posiotional parameters --- Runtime/Scripts/Plugins/AudioImport.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Runtime/Scripts/Plugins/AudioImport.cs b/Runtime/Scripts/Plugins/AudioImport.cs index f49d0d419..f9bb46845 100644 --- a/Runtime/Scripts/Plugins/AudioImport.cs +++ b/Runtime/Scripts/Plugins/AudioImport.cs @@ -305,10 +305,13 @@ private void AddEmitter(KHR_AudioEmitter emitter, GameObject toGameObject, bool 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; - audioSource.rolloffMode = AudioRolloffMode.Linear; - audioSource.minDistance = 1.0f; - audioSource.maxDistance = 100.0f; + audioSource.spatialBlend = isGlobal ? 0f : 1.0f; + if (!isGlobal && emitter is KHR_PositionalAudioEmitter posEmitter) + { + audioSource.rolloffMode = AudioRolloffMode.Linear; + audioSource.minDistance = posEmitter.minDistance; + audioSource.maxDistance = posEmitter.maxDistance; + } } } From 071063dda83fecc701c8427495f01e24db687d81 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Tue, 8 Apr 2025 15:30:01 +0200 Subject: [PATCH 17/22] changed how positional emitter data gets serializied and deserialized --- .../Extensions/KHR_audio_emitter.cs | 174 +++++++++++------- Runtime/Scripts/Plugins/AudioExport.cs | 26 +-- Runtime/Scripts/Plugins/AudioImport.cs | 6 +- 3 files changed, 125 insertions(+), 81 deletions(-) diff --git a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs index d2e71fda5..7196cbfbf 100644 --- a/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs +++ b/Runtime/Plugins/GLTFSerialization/Extensions/KHR_audio_emitter.cs @@ -203,6 +203,106 @@ public static KHR_NodeAudioEmitterRef Deserialize(GLTFRoot root, JProperty exten } } + 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 { @@ -211,6 +311,8 @@ public class KHR_AudioEmitter : GLTFChildOfRootProperty public float gain; public List sources = new List(); + public PositionalEmitterData positional = null; + public virtual JObject Serialize() { var jo = new JObject(); @@ -223,6 +325,11 @@ public virtual JObject Serialize() 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) { @@ -269,74 +376,17 @@ public static KHR_AudioEmitter Deserialize(GLTFRoot root, JsonReader reader) break; foreach (var source in list) emitter.sources.Add(new AudioSourceId { Id = source, Root = root }); - break; + break; + case nameof(positional): + emitter.positional = PositionalEmitterData.Deserialize(root, reader); + break; } } return emitter; } } - - [Serializable] - public class KHR_PositionalAudioEmitter : KHR_AudioEmitter - { - public float coneInnerAngle; - public float coneOuterAngle; - public float coneOuterGain; - public PositionalAudioDistanceModel distanceModel; - public float minDistance; - 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 (minDistance != 0) - { - positional.Add(new JProperty(nameof(minDistance), minDistance)); - } - - 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 { diff --git a/Runtime/Scripts/Plugins/AudioExport.cs b/Runtime/Scripts/Plugins/AudioExport.cs index 3a3a8c3b9..a0ca2f08f 100644 --- a/Runtime/Scripts/Plugins/AudioExport.cs +++ b/Runtime/Scripts/Plugins/AudioExport.cs @@ -29,7 +29,6 @@ public class AudioExportContext: GLTFExportPluginContext private Dictionary _audioSourceToEmitter = new(); private Dictionary _audioSourceToNode = new(); - public class AudioDescription { public int Id; @@ -88,26 +87,21 @@ private AudioEmitterId ProcessAudioSource(bool isGlobal, AudioSource[] audioSour var firstAudioSource = audioSources[0]; - KHR_AudioEmitter emitter; - if (isGlobal) + + var emitter = new KHR_AudioEmitter { - emitter = new KHR_AudioEmitter - { - type = "global", - gain = firstAudioSource.volume, - name = "global emitter", - }; - } - else + type = isGlobal ? "global" : "positional", + gain = firstAudioSource.volume, + name = isGlobal ? "global emitter" : "positional emitter" + }; + + if (!isGlobal) { - emitter = new KHR_PositionalAudioEmitter() + emitter.positional = new PositionalEmitterData() { - type = "positional", - gain = firstAudioSource.volume, - minDistance = firstAudioSource.minDistance, + refDistance = firstAudioSource.minDistance, maxDistance = firstAudioSource.maxDistance, distanceModel = PositionalAudioDistanceModel.linear, - name = "positional emitter" }; } emitter.sources.AddRange(audioSourceIds.Select(a => new AudioSourceId { Id = a.Id, Root = gltfRoot })); diff --git a/Runtime/Scripts/Plugins/AudioImport.cs b/Runtime/Scripts/Plugins/AudioImport.cs index f9bb46845..b26b4819f 100644 --- a/Runtime/Scripts/Plugins/AudioImport.cs +++ b/Runtime/Scripts/Plugins/AudioImport.cs @@ -306,11 +306,11 @@ private void AddEmitter(KHR_AudioEmitter emitter, GameObject toGameObject, bool audioSource.volume = emitter.gain * (source.Value.gain ?? 1f); audioSource.playOnAwake = source.Value.autoPlay ?? false; audioSource.spatialBlend = isGlobal ? 0f : 1.0f; - if (!isGlobal && emitter is KHR_PositionalAudioEmitter posEmitter) + if (!isGlobal && emitter.positional != null) { audioSource.rolloffMode = AudioRolloffMode.Linear; - audioSource.minDistance = posEmitter.minDistance; - audioSource.maxDistance = posEmitter.maxDistance; + audioSource.minDistance = emitter.positional.refDistance ?? 1f; + audioSource.maxDistance = emitter.positional.maxDistance ?? 0f; } } } From 3a060bdb507244cca0083c495197dd3a8dd2615f Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Thu, 15 May 2025 09:36:26 +0200 Subject: [PATCH 18/22] moved Audio plugins to Experimental --- Runtime/Scripts/Plugins/{ => Experimental}/AudioExport.cs | 0 Runtime/Scripts/Plugins/{ => Experimental}/AudioExport.cs.meta | 0 Runtime/Scripts/Plugins/{ => Experimental}/AudioImport.cs | 0 Runtime/Scripts/Plugins/{ => Experimental}/AudioImport.cs.meta | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename Runtime/Scripts/Plugins/{ => Experimental}/AudioExport.cs (100%) rename Runtime/Scripts/Plugins/{ => Experimental}/AudioExport.cs.meta (100%) rename Runtime/Scripts/Plugins/{ => Experimental}/AudioImport.cs (100%) rename Runtime/Scripts/Plugins/{ => Experimental}/AudioImport.cs.meta (100%) diff --git a/Runtime/Scripts/Plugins/AudioExport.cs b/Runtime/Scripts/Plugins/Experimental/AudioExport.cs similarity index 100% rename from Runtime/Scripts/Plugins/AudioExport.cs rename to Runtime/Scripts/Plugins/Experimental/AudioExport.cs diff --git a/Runtime/Scripts/Plugins/AudioExport.cs.meta b/Runtime/Scripts/Plugins/Experimental/AudioExport.cs.meta similarity index 100% rename from Runtime/Scripts/Plugins/AudioExport.cs.meta rename to Runtime/Scripts/Plugins/Experimental/AudioExport.cs.meta diff --git a/Runtime/Scripts/Plugins/AudioImport.cs b/Runtime/Scripts/Plugins/Experimental/AudioImport.cs similarity index 100% rename from Runtime/Scripts/Plugins/AudioImport.cs rename to Runtime/Scripts/Plugins/Experimental/AudioImport.cs diff --git a/Runtime/Scripts/Plugins/AudioImport.cs.meta b/Runtime/Scripts/Plugins/Experimental/AudioImport.cs.meta similarity index 100% rename from Runtime/Scripts/Plugins/AudioImport.cs.meta rename to Runtime/Scripts/Plugins/Experimental/AudioImport.cs.meta From 85327e2def3c0cd312a62b0514b5cb5d2dbb8404 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Thu, 15 May 2025 09:36:48 +0200 Subject: [PATCH 19/22] AudioExport: removed unsused variables --- Runtime/Scripts/Plugins/Experimental/AudioExport.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Runtime/Scripts/Plugins/Experimental/AudioExport.cs b/Runtime/Scripts/Plugins/Experimental/AudioExport.cs index a0ca2f08f..73c5945ac 100644 --- a/Runtime/Scripts/Plugins/Experimental/AudioExport.cs +++ b/Runtime/Scripts/Plugins/Experimental/AudioExport.cs @@ -20,12 +20,10 @@ public override GLTFExportPluginContext CreateInstance(ExportContext context) public class AudioExportContext: GLTFExportPluginContext { - private ExportContext _context; private List _audioSourceIds = new(); private KHR_audio_emitter _audioExtension; private KHR_SceneAudioEmittersRef _sceneExtension = null; - private bool _saveAudioToFile; private Dictionary _audioSourceToEmitter = new(); private Dictionary _audioSourceToNode = new(); @@ -38,8 +36,6 @@ public class AudioDescription public AudioExportContext(ExportContext context) { - _context = context; - _saveAudioToFile = false; } private AudioDescription AddAudioSource(AudioSource audioSource, out bool isNew) @@ -219,7 +215,6 @@ public override void AfterSceneExport(GLTFSceneExporter exporter, GLTFRoot gltfR return; gltfRoot.Scenes[0].AddExtension(KHR_SceneAudioEmittersRef.ExtensionName, _sceneExtension); - } } From 4c47b6a49a811edbcaad22f3f5185e75c097549c Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Thu, 15 May 2025 10:42:24 +0200 Subject: [PATCH 20/22] new non-ratified plugin attribute, added to Audio and Int.VisualScripting exporter --- Editor/Scripts/GLTFSettingsInspector.cs | 22 +++++++++++++++++++ .../VisualScriptingExportPlugin.cs | 1 + .../Core/ExperimentalPluginAttribute.cs | 18 +++++++++++++++ .../Core/ExperimentalPluginAttribute.cs.meta | 3 +++ .../Plugins/Experimental/AudioExport.cs | 3 ++- .../Plugins/Experimental/AudioImport.cs | 4 ++-- 6 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs create mode 100644 Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs.meta diff --git a/Editor/Scripts/GLTFSettingsInspector.cs b/Editor/Scripts/GLTFSettingsInspector.cs index 26b4bc3da..4abbf087b 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; @@ -210,9 +212,29 @@ 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(); + + var expAttribute = plugin.GetType().GetCustomAttribute(typeof(NonRatifiedPluginAttribute), true); + if (expAttribute != null) + { + + var exp = expAttribute as NonRatifiedPluginAttribute; + var toolTip = exp.toolTip == null ? DEFAULT_NON_RATIFIED_TOOLTIP : exp.toolTip; + var explabel = new GUIContent("non-ratified" , toolTip); + var expLabelRect = lastFoldoutRect; + expLabelRect.x += 20 + GUI.skin.label.CalcSize(label).x; + expLabelRect.width = EditorStyles.miniButton.CalcSize(explabel).x+20; + GUI.backgroundColor = new Color(1f*4,0.5f*4,0f,1f); + GUI.contentColor = Color.black; + EditorGUI.LabelField(expLabelRect, explabel, EditorStyles.miniButton); + GUI.backgroundColor = Color.white; + GUI.contentColor = Color.white; + } + // check for right click so we can show a context menu EditorGUI.EndDisabledGroup(); if (Event.current.type == EventType.MouseDown && lastFoldoutRect.Contains(Event.current.mousePosition)) 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/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs b/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs new file mode 100644 index 000000000..23c13379a --- /dev/null +++ b/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs @@ -0,0 +1,18 @@ +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; + } + } +} \ 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 index 73c5945ac..5e0c0f7c3 100644 --- a/Runtime/Scripts/Plugins/Experimental/AudioExport.cs +++ b/Runtime/Scripts/Plugins/Experimental/AudioExport.cs @@ -7,10 +7,11 @@ namespace UnityGLTF.Plugins { + [NonRatifiedPlugin] public class AudioExport: GLTFExportPlugin { public override bool EnabledByDefault => false; - public override string DisplayName => "KHR_audio"; + public override string DisplayName => "KHR_audio_emitter"; public override string Description => "Exports positional and global audio sources"; public override GLTFExportPluginContext CreateInstance(ExportContext context) { diff --git a/Runtime/Scripts/Plugins/Experimental/AudioImport.cs b/Runtime/Scripts/Plugins/Experimental/AudioImport.cs index b26b4819f..8fa7d5a6c 100644 --- a/Runtime/Scripts/Plugins/Experimental/AudioImport.cs +++ b/Runtime/Scripts/Plugins/Experimental/AudioImport.cs @@ -12,17 +12,17 @@ namespace UnityGLTF.Plugins { + [NonRatifiedPlugin] public class AudioImport : GLTFImportPlugin { public override bool EnabledByDefault => false; - public override string DisplayName => "KHR_audio"; + 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 From 105e655a4559e88900c78ec35e2a0dae7225ea90 Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Thu, 15 May 2025 10:45:24 +0200 Subject: [PATCH 21/22] changed Audio Export to editor only --- Runtime/Scripts/Plugins/Experimental/AudioExport.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Runtime/Scripts/Plugins/Experimental/AudioExport.cs b/Runtime/Scripts/Plugins/Experimental/AudioExport.cs index 5e0c0f7c3..582166843 100644 --- a/Runtime/Scripts/Plugins/Experimental/AudioExport.cs +++ b/Runtime/Scripts/Plugins/Experimental/AudioExport.cs @@ -2,16 +2,18 @@ using System.IO; using System.Linq; using GLTF.Schema; -using UnityEditor; 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"; + 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) { @@ -218,5 +220,5 @@ public override void AfterSceneExport(GLTFSceneExporter exporter, GLTFRoot gltfR gltfRoot.Scenes[0].AddExtension(KHR_SceneAudioEmittersRef.ExtensionName, _sceneExtension); } } - -} \ No newline at end of file +} +#endif \ No newline at end of file From ef29060a68e9bcb419296e8ac0d776df71b560ca Mon Sep 17 00:00:00 2001 From: Robert Dorn Date: Thu, 15 May 2025 12:53:40 +0200 Subject: [PATCH 22/22] added new Experimental Attribute for Plugins --- Editor/Scripts/GLTFSettingsInspector.cs | 63 ++++++++++++++----- .../Core/ExperimentalPluginAttribute.cs | 14 +++++ .../Experimental/BakeParticleSystem.cs | 1 + .../Plugins/Experimental/CanvasExport.cs | 1 + .../Experimental/MaterialVariantsPlugin.cs | 1 + .../Experimental/TextMeshGameObjectExport.cs | 1 + 6 files changed, 67 insertions(+), 14 deletions(-) diff --git a/Editor/Scripts/GLTFSettingsInspector.cs b/Editor/Scripts/GLTFSettingsInspector.cs index 4abbf087b..d5e647cfc 100644 --- a/Editor/Scripts/GLTFSettingsInspector.cs +++ b/Editor/Scripts/GLTFSettingsInspector.cs @@ -163,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 = ""; @@ -212,27 +214,21 @@ 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(); - var expAttribute = plugin.GetType().GetCustomAttribute(typeof(NonRatifiedPluginAttribute), true); + 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) { - - var exp = expAttribute as NonRatifiedPluginAttribute; - var toolTip = exp.toolTip == null ? DEFAULT_NON_RATIFIED_TOOLTIP : exp.toolTip; - var explabel = new GUIContent("non-ratified" , toolTip); - var expLabelRect = lastFoldoutRect; - expLabelRect.x += 20 + GUI.skin.label.CalcSize(label).x; - expLabelRect.width = EditorStyles.miniButton.CalcSize(explabel).x+20; - GUI.backgroundColor = new Color(1f*4,0.5f*4,0f,1f); - GUI.contentColor = Color.black; - EditorGUI.LabelField(expLabelRect, explabel, EditorStyles.miniButton); - GUI.backgroundColor = Color.white; - GUI.contentColor = Color.white; + batchOffsetX = DrawExperimentalBadge(expAttribute, batchOffsetX); } // check for right click so we can show a context menu @@ -301,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/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs b/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs index 23c13379a..99394055e 100644 --- a/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs +++ b/Runtime/Scripts/Plugins/Core/ExperimentalPluginAttribute.cs @@ -15,4 +15,18 @@ 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/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";