diff --git a/Assets/UniGLTF/Runtime/UniGLTF/IO/ExportingGltfData.cs b/Assets/UniGLTF/Runtime/UniGLTF/IO/ExportingGltfData.cs index c08ee784b..a5b29d16c 100644 --- a/Assets/UniGLTF/Runtime/UniGLTF/IO/ExportingGltfData.cs +++ b/Assets/UniGLTF/Runtime/UniGLTF/IO/ExportingGltfData.cs @@ -138,66 +138,6 @@ namespace UniGLTF #endregion #region ToGltf & ToGlb - static Utf8String s_extensions = Utf8String.From("extensions"); - - static bool UsedExtension(glTF self, string key) - { - if (self.extensionsUsed.Contains(key)) - { - return true; - } - - return false; - } - - static void Traverse(glTF self, JsonNode node, JsonFormatter f, Utf8String parentKey) - { - if (node.IsMap()) - { - f.BeginMap(); - foreach (var kv in node.ObjectItems()) - { - if (parentKey == s_extensions) - { - if (!UsedExtension(self, kv.Key.GetString())) - { - // skip extension not in used - continue; - } - } - f.Key(kv.Key.GetUtf8String()); - Traverse(self, kv.Value, f, kv.Key.GetUtf8String()); - } - f.EndMap(); - } - else if (node.IsArray()) - { - f.BeginList(); - foreach (var x in node.ArrayItems()) - { - Traverse(self, x, f, default(Utf8String)); - } - f.EndList(); - } - else - { - f.Value(node); - } - } - - /// - /// 出力前に不要な extension を削除する - /// - /// - /// - /// - static string RemoveUnusedExtensions(glTF self, string json) - { - var f = new JsonFormatter(); - Traverse(self, JsonParser.Parse(json), f, default(Utf8String)); - return f.ToString(); - } - /// /// GLBバイト列 /// @@ -207,9 +147,10 @@ namespace UniGLTF var f = new JsonFormatter(); GltfSerializer.Serialize(f, GLTF); - // remove unused extenions var json = f.ToString().ParseAsJson().ToString(" "); - RemoveUnusedExtensions(GLTF, json); + + json = GltfJsonUtil.FindUsedExtensionsAndUpdateJson(json); + return Glb.Create(json, BinBytes).ToBytes(); } @@ -234,7 +175,9 @@ namespace UniGLTF var f = new JsonFormatter(); GltfSerializer.Serialize(f, GLTF); var json = f.ToString().ParseAsJson().ToString(" "); - RemoveUnusedExtensions(GLTF, json); + + json = GltfJsonUtil.FindUsedExtensionsAndUpdateJson(json); + return (json, GLTF.buffers[0]); } #endregion diff --git a/Assets/UniGLTF/Runtime/UniGLTF/IO/GltfJsonUtil.cs b/Assets/UniGLTF/Runtime/UniGLTF/IO/GltfJsonUtil.cs new file mode 100644 index 000000000..c96e33e84 --- /dev/null +++ b/Assets/UniGLTF/Runtime/UniGLTF/IO/GltfJsonUtil.cs @@ -0,0 +1,212 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UniJSON; +using UnityEngine; + +namespace UniGLTF +{ + public static class GltfJsonUtil + { + const string EXTENSION_USED_KEY = "extensionUsed"; + + /// + /// JsonPath を 再帰的に列挙する + /// object[] の中身は int(array index) or string(object key) + /// + /// + /// + /// + public static IEnumerable TraverseJsonPath(JsonNode node, List path) + { + if (path == null) + { + path = new List(); + } + yield return path.ToArray(); + + if (node.IsArray()) + { + int i = 0; + foreach (var child in node.ArrayItems()) + { + path.Add(i); + foreach (var x in TraverseJsonPath(child, path)) + { + yield return x; + } + path.RemoveAt(path.Count - 1); + ++i; + } + } + else if (node.IsMap()) + { + foreach (var kv in node.ObjectItems()) + { + path.Add(kv.Key.GetString()); + foreach (var x in TraverseJsonPath(kv.Value, path)) + { + yield return x; + } + path.RemoveAt(path.Count - 1); + } + } + } + + static string DoubleQuote(string src) + { + return $"\"{src}\""; + } + + /// + /// jsonPath が + /// + /// [..., "extensions", "EXTENSION_NAME"] + /// + /// で有る場合に EXTENSION_NAME を返す。 + /// + /// + /// + /// + static bool TryGetExtensionName(object[] path, out string extensionName) + { + if (path.Length >= 2) + { + if (path[path.Length - 2] is string x) + { + if (x == "extensions") + { + if (path[path.Length - 1] is string y) + { + extensionName = y; + return true; + } + else + { + // ありえない。はず + var join = string.Join(", ", path); + Debug.LogWarning($"invalid json path: {join}"); + } + } + } + } + + extensionName = default; + return false; + } + + static void CopyJson(IReadOnlyList extensionUsed, JsonFormatter dst, JsonNode src, int level) + { + if (src.IsArray()) + { + dst.BeginList(); + foreach (var v in src.ArrayItems()) + { + CopyJson(extensionUsed, dst, v, level + 1); + } + dst.EndList(); + } + else if (src.IsMap()) + { + if (level == 0) + { + // 最上層だけ extensionsUsed の処理をする + var done = false; + dst.BeginMap(); + foreach (var kv in src.ObjectItems()) + { + var key = kv.Key.GetString(); + if (key == EXTENSION_USED_KEY) + { + if (extensionUsed.Count == 0) + { + // skip + } + else + { + dst.Key(key); + // replace + dst.BeginList(); + foreach (var ex in extensionUsed) + { + dst.Value(ex); + } + dst.EndList(); + // 処理済 + } + done = true; + } + else + { + dst.Key(key); + CopyJson(extensionUsed, dst, kv.Value, level + 1); + } + } + if (!done && level == 0 && extensionUsed.Count > 0) + { + // add + dst.Key(EXTENSION_USED_KEY); + dst.BeginList(); + foreach (var ex in extensionUsed) + { + dst.Value(ex); + } + dst.EndList(); + } + dst.EndMap(); + } + else + { + dst.BeginMap(); + foreach (var kv in src.ObjectItems()) + { + dst.Key(kv.Key.GetUtf8String()); + CopyJson(extensionUsed, dst, kv.Value, level + 1); + } + dst.EndMap(); + } + } + else + { + // leaf + dst.Value(src); + } + } + + /// + /// https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/schema/glTF.schema.json + /// + /// extensionUsed の更新を各拡張自身にやらせるのは無駄だし、手動でコントロールするのも間違いの元である。 + /// 完成品の JSON から後付けで作ることにした。 + /// + /// * Exporter しか使わない処理なので、GC, 処理速度は気にしてない + /// + /// + /// + /// + public static string FindUsedExtensionsAndUpdateJson(string src) + { + var parsed = src.ParseAsJson(); + + // unique な extension 名を収集 + var used = new HashSet(); + foreach (var path in TraverseJsonPath(parsed, null)) + { + if (TryGetExtensionName(path, out string extensionName)) + { + used.Add(extensionName); + } + } + + // json 加工 + var f = new JsonFormatter(); + CopyJson(used.ToArray(), f, parsed, 0); + + // bom無しutf8 + var bytes = f.GetStoreBytes(); + var utf8 = new UTF8Encoding(false); + return utf8.GetString(bytes.Array, bytes.Offset, bytes.Count); + } + } +} diff --git a/Assets/UniGLTF/Runtime/UniGLTF/IO/GltfJsonUtil.cs.meta b/Assets/UniGLTF/Runtime/UniGLTF/IO/GltfJsonUtil.cs.meta new file mode 100644 index 000000000..bc9cd03f0 --- /dev/null +++ b/Assets/UniGLTF/Runtime/UniGLTF/IO/GltfJsonUtil.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5a820504e80b57241b7716b5606235f7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UniGLTF/Tests/UniGLTF/GltfJsonUtilTests.cs b/Assets/UniGLTF/Tests/UniGLTF/GltfJsonUtilTests.cs new file mode 100644 index 000000000..cd806db21 --- /dev/null +++ b/Assets/UniGLTF/Tests/UniGLTF/GltfJsonUtilTests.cs @@ -0,0 +1,167 @@ +using NUnit.Framework; +using UniJSON; +using System.Linq; + +namespace UniGLTF +{ + public class GltfJsonUtilTests + { + [Test] + public void Update_extensionUsed() + { + var dst = GltfJsonUtil.FindUsedExtensionsAndUpdateJson(@"{ + ""asset"": { + ""generator"": ""COLLADA2GLTF"", + ""version"": ""2.0"" + }, + ""scene"": 0, + ""scenes"": [ + { + ""nodes"": [ + 0 + ] + } + ], + ""materials"": [ + { + ""pbrMetallicRoughness"": { + ""baseColorFactor"": [ + 0.800000011920929, + 0.0, + 0.0, + 1.0 + ], + ""metallicFactor"": 0.0 + }, + ""name"": ""Red"", + ""extensions"": { + ""KHR_materials_unlit"": {} + } + } + ] +}"); + + var parsed = dst.ParseAsJson(); + + Assert.AreEqual(new string[] { "KHR_materials_unlit" }, + parsed["extensionUsed"].ArrayItems().Select(x => x.GetString()).ToArray()); + } + + [Test] + public void Replace_extensionUsed() + { + var dst = GltfJsonUtil.FindUsedExtensionsAndUpdateJson(@"{ + ""asset"": { + ""generator"": ""COLLADA2GLTF"", + ""version"": ""2.0"" + }, + ""scene"": 0, + ""scenes"": [ + { + ""nodes"": [ + 0 + ] + } + ], + ""extensionUsed"": [""dummy""], + ""materials"": [ + { + ""pbrMetallicRoughness"": { + ""baseColorFactor"": [ + 0.800000011920929, + 0.0, + 0.0, + 1.0 + ], + ""metallicFactor"": 0.0 + }, + ""name"": ""Red"", + ""extensions"": { + ""KHR_materials_unlit"": {} + } + } + ] +}"); + + var parsed = dst.ParseAsJson(); + + Assert.AreEqual(new string[] { "KHR_materials_unlit" }, + parsed["extensionUsed"].ArrayItems().Select(x => x.GetString()).ToArray()); + } + + [Test] + public void Empty_extensionUsed() + { + var dst = GltfJsonUtil.FindUsedExtensionsAndUpdateJson(@"{ + ""asset"": { + ""generator"": ""COLLADA2GLTF"", + ""version"": ""2.0"" + }, + ""scene"": 0, + ""scenes"": [ + { + ""nodes"": [ + 0 + ] + } + ], + ""extensionUsed"": [""dummy""] , + ""materials"": [ + { + ""pbrMetallicRoughness"": { + ""baseColorFactor"": [ + 0.800000011920929, + 0.0, + 0.0, + 1.0 + ], + ""metallicFactor"": 0.0 + }, + ""name"": ""Red"" + } + ] +}"); + + var parsed = dst.ParseAsJson(); + Assert.False(parsed.ContainsKey("extensionUsed")); + } + + [Test] + public void Empty2_extensionUsed() + { + var dst = GltfJsonUtil.FindUsedExtensionsAndUpdateJson(@"{ + ""asset"": { + ""generator"": ""COLLADA2GLTF"", + ""version"": ""2.0"" + }, + ""scene"": 0, + ""scenes"": [ + { + ""nodes"": [ + 0 + ] + } + ], + ""materials"": [ + { + ""pbrMetallicRoughness"": { + ""baseColorFactor"": [ + 0.800000011920929, + 0.0, + 0.0, + 1.0 + ], + ""metallicFactor"": 0.0 + }, + ""name"": ""Red"" + } + ], + ""extensionUsed"": [""dummy""] +}"); + + var parsed = dst.ParseAsJson(); + Assert.False(parsed.ContainsKey("extensionUsed")); + } + + } +} diff --git a/Assets/UniGLTF/Tests/UniGLTF/GltfJsonUtilTests.cs.meta b/Assets/UniGLTF/Tests/UniGLTF/GltfJsonUtilTests.cs.meta new file mode 100644 index 000000000..d45dd48e9 --- /dev/null +++ b/Assets/UniGLTF/Tests/UniGLTF/GltfJsonUtilTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bf03004ca38f9994c8feedbf3afc9352 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: