Importer.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8. using Newtonsoft.Json;
  9. using Newtonsoft.Json.Linq;
  10. using UnityEngine;
  11. using UnityEngine.EventSystems;
  12. namespace Siccity.GLTFUtility {
  13. /// <summary> API used for importing .gltf and .glb files </summary>
  14. public static class Importer {
  15. public static GameObject LoadFromFile(string filepath, Format format = Format.AUTO) {
  16. AnimationClip[] animations;
  17. return LoadFromFile(filepath, new ImportSettings(), out animations, format);
  18. }
  19. public static GameObject LoadFromFile(string filepath, ImportSettings importSettings, Format format = Format.AUTO) {
  20. AnimationClip[] animations;
  21. return LoadFromFile(filepath, importSettings, out animations, format);
  22. }
  23. public static GameObject LoadFromFile(string filepath, ImportSettings importSettings, out AnimationClip[] animations, Format format = Format.AUTO) {
  24. if (format == Format.GLB) {
  25. return ImportGLB(filepath, importSettings, out animations);
  26. } else if (format == Format.GLTF) {
  27. return ImportGLTF(filepath, importSettings, out animations);
  28. } else {
  29. string extension = Path.GetExtension(filepath).ToLower();
  30. if (extension == ".glb") return ImportGLB(filepath, importSettings, out animations);
  31. else if (extension == ".gltf") return ImportGLTF(filepath, importSettings, out animations);
  32. else {
  33. Debug.Log("Extension '" + extension + "' not recognized in " + filepath);
  34. animations = null;
  35. return null;
  36. }
  37. }
  38. }
  39. /// <param name="bytes">GLB file is supported</param>
  40. public static GameObject LoadFromBytes(byte[] bytes, ImportSettings importSettings = null) {
  41. AnimationClip[] animations;
  42. if (importSettings == null) importSettings = new ImportSettings();
  43. return ImportGLB(bytes, importSettings, out animations);
  44. }
  45. /// <param name="bytes">GLB file is supported</param>
  46. public static GameObject LoadFromBytes(byte[] bytes, ImportSettings importSettings, out AnimationClip[] animations) {
  47. return ImportGLB(bytes, importSettings, out animations);
  48. }
  49. public static void LoadFromFileAsync(string filepath, ImportSettings importSettings, Action<GameObject, AnimationClip[]> onFinished, Action<float> onProgress = null) {
  50. string extension = Path.GetExtension(filepath).ToLower();
  51. if (extension == ".glb") ImportGLBAsync(filepath, importSettings, onFinished, onProgress);
  52. else if (extension == ".gltf") ImportGLTFAsync(filepath, importSettings, onFinished, onProgress);
  53. else {
  54. Debug.Log("Extension '" + extension + "' not recognized in " + filepath);
  55. onFinished(null, null);
  56. }
  57. }
  58. #region GLB
  59. private static GameObject ImportGLB(string filepath, ImportSettings importSettings, out AnimationClip[] animations) {
  60. FileStream stream = File.OpenRead(filepath);
  61. long binChunkStart;
  62. string json = GetGLBJson(stream, out binChunkStart);
  63. GLTFObject gltfObject = JsonConvert.DeserializeObject<GLTFObject>(json);
  64. return gltfObject.LoadInternal(filepath, null, binChunkStart, importSettings, out animations);
  65. }
  66. private static GameObject ImportGLB(byte[] bytes, ImportSettings importSettings, out AnimationClip[] animations) {
  67. Stream stream = new MemoryStream(bytes);
  68. long binChunkStart;
  69. string json = GetGLBJson(stream, out binChunkStart);
  70. GLTFObject gltfObject = JsonConvert.DeserializeObject<GLTFObject>(json);
  71. return gltfObject.LoadInternal(null, bytes, binChunkStart, importSettings, out animations);
  72. }
  73. public static void ImportGLBAsync(string filepath, ImportSettings importSettings, Action<GameObject, AnimationClip[]> onFinished, Action<float> onProgress = null) {
  74. FileStream stream = File.OpenRead(filepath);
  75. long binChunkStart;
  76. string json = GetGLBJson(stream, out binChunkStart);
  77. LoadAsync(json, filepath, null, binChunkStart, importSettings, onFinished, onProgress).RunCoroutine();
  78. }
  79. public static void ImportGLBAsync(byte[] bytes, ImportSettings importSettings, Action<GameObject, AnimationClip[]> onFinished, Action<float> onProgress = null) {
  80. Stream stream = new MemoryStream(bytes);
  81. long binChunkStart;
  82. string json = GetGLBJson(stream, out binChunkStart);
  83. LoadAsync(json, null, bytes, binChunkStart, importSettings, onFinished, onProgress).RunCoroutine();
  84. }
  85. private static string GetGLBJson(Stream stream, out long binChunkStart) {
  86. byte[] buffer = new byte[12];
  87. stream.Read(buffer, 0, 12);
  88. // 12 byte header
  89. // 0-4 - magic = "glTF"
  90. // 4-8 - version = 2
  91. // 8-12 - length = total length of glb, including Header and all Chunks, in bytes.
  92. string magic = Encoding.Default.GetString(buffer, 0, 4);
  93. if (magic != "glTF") {
  94. Debug.LogWarning("Input does not look like a .glb file");
  95. binChunkStart = 0;
  96. return null;
  97. }
  98. uint version = System.BitConverter.ToUInt32(buffer, 4);
  99. if (version != 2) {
  100. Debug.LogWarning("Importer does not support gltf version " + version);
  101. binChunkStart = 0;
  102. return null;
  103. }
  104. // What do we even need the length for.
  105. //uint length = System.BitConverter.ToUInt32(bytes, 8);
  106. // Chunk 0 (json)
  107. // 0-4 - chunkLength = total length of the chunkData
  108. // 4-8 - chunkType = "JSON"
  109. // 8-[chunkLength+8] - chunkData = json data.
  110. stream.Read(buffer, 0, 8);
  111. uint chunkLength = System.BitConverter.ToUInt32(buffer, 0);
  112. TextReader reader = new StreamReader(stream);
  113. char[] jsonChars = new char[chunkLength];
  114. reader.Read(jsonChars, 0, (int) chunkLength);
  115. string json = new string(jsonChars);
  116. // Chunk
  117. binChunkStart = chunkLength + 20;
  118. stream.Close();
  119. // Return json
  120. return json;
  121. }
  122. #endregion
  123. private static GameObject ImportGLTF(string filepath, ImportSettings importSettings, out AnimationClip[] animations) {
  124. string json = File.ReadAllText(filepath);
  125. // Parse json
  126. GLTFObject gltfObject = JsonConvert.DeserializeObject<GLTFObject>(json);
  127. return gltfObject.LoadInternal(filepath, null, 0, importSettings, out animations);
  128. }
  129. public static void ImportGLTFAsync(string filepath, ImportSettings importSettings, Action<GameObject, AnimationClip[]> onFinished, Action<float> onProgress = null) {
  130. string json = File.ReadAllText(filepath);
  131. // Parse json
  132. LoadAsync(json, filepath, null, 0, importSettings, onFinished, onProgress).RunCoroutine();
  133. }
  134. public abstract class ImportTask<TReturn> : ImportTask {
  135. public TReturn Result;
  136. /// <summary> Constructor. Sets waitFor which ensures ImportTasks are completed before running. </summary>
  137. public ImportTask(params ImportTask[] waitFor) : base(waitFor) { }
  138. /// <summary> Runs task followed by OnCompleted </summary>
  139. public TReturn RunSynchronously() {
  140. if(task != null)
  141. {
  142. task.RunSynchronously();
  143. }
  144. IEnumerator en = OnCoroutine();
  145. while (en.MoveNext()) { };
  146. return Result;
  147. }
  148. }
  149. public abstract class ImportTask {
  150. public Task task;
  151. public readonly ImportTask[] waitFor;
  152. public bool IsReady { get { return waitFor.All(x => x.IsCompleted); } }
  153. public bool IsCompleted { get; protected set; }
  154. /// <summary> Constructor. Sets waitFor which ensures ImportTasks are completed before running. </summary>
  155. public ImportTask(params ImportTask[] waitFor) {
  156. IsCompleted = false;
  157. this.waitFor = waitFor;
  158. }
  159. public virtual IEnumerator OnCoroutine(Action<float> onProgress = null) {
  160. IsCompleted = true;
  161. yield break;
  162. }
  163. }
  164. #region Sync
  165. private static GameObject LoadInternal(this GLTFObject gltfObject, string filepath, byte[] bytefile, long binChunkStart, ImportSettings importSettings, out AnimationClip[] animations) {
  166. CheckExtensions(gltfObject);
  167. // directory root is sometimes used for loading buffers from containing file, or local images
  168. string directoryRoot = filepath != null ? Directory.GetParent(filepath).ToString() + "/" : null;
  169. importSettings.shaderOverrides.CacheDefaultShaders();
  170. // Import tasks synchronously
  171. GLTFBuffer.ImportTask bufferTask = new GLTFBuffer.ImportTask(gltfObject.buffers, filepath, bytefile, binChunkStart);
  172. bufferTask.RunSynchronously();
  173. GLTFBufferView.ImportTask bufferViewTask = new GLTFBufferView.ImportTask(gltfObject.bufferViews, bufferTask);
  174. bufferViewTask.RunSynchronously();
  175. GLTFAccessor.ImportTask accessorTask = new GLTFAccessor.ImportTask(gltfObject.accessors, bufferViewTask);
  176. accessorTask.RunSynchronously();
  177. GLTFImage.ImportTask imageTask = new GLTFImage.ImportTask(gltfObject.images, directoryRoot, bufferViewTask);
  178. imageTask.RunSynchronously();
  179. GLTFTexture.ImportTask textureTask = new GLTFTexture.ImportTask(gltfObject.textures, imageTask);
  180. textureTask.RunSynchronously();
  181. GLTFMaterial.ImportTask materialTask = new GLTFMaterial.ImportTask(gltfObject.materials, textureTask, importSettings);
  182. materialTask.RunSynchronously();
  183. GLTFMesh.ImportTask meshTask = new GLTFMesh.ImportTask(gltfObject.meshes, accessorTask, bufferViewTask, materialTask, importSettings);
  184. meshTask.RunSynchronously();
  185. GLTFSkin.ImportTask skinTask = new GLTFSkin.ImportTask(gltfObject.skins, accessorTask);
  186. skinTask.RunSynchronously();
  187. GLTFNode.ImportTask nodeTask = new GLTFNode.ImportTask(gltfObject.nodes, meshTask, skinTask, gltfObject.cameras);
  188. nodeTask.RunSynchronously();
  189. GLTFAnimation.ImportResult[] animationResult = gltfObject.animations.Import(accessorTask.Result, nodeTask.Result, importSettings);
  190. if (animationResult != null) animations = animationResult.Select(x => x.clip).ToArray();
  191. else animations = new AnimationClip[0];
  192. foreach (var item in bufferTask.Result) {
  193. item.Dispose();
  194. }
  195. GameObject gameObject = nodeTask.Result.GetRoot();
  196. if (importSettings.extrasProcessor != null)
  197. {
  198. if(gltfObject.extras == null)
  199. {
  200. gltfObject.extras = new JObject();
  201. }
  202. if(gltfObject.materials != null)
  203. {
  204. JArray materialExtras = new JArray();
  205. bool hasMaterialExtraData = false;
  206. foreach (GLTFMaterial material in gltfObject.materials)
  207. {
  208. if (material.extras != null)
  209. {
  210. materialExtras.Add(material.extras);
  211. hasMaterialExtraData = true;
  212. }
  213. else
  214. {
  215. materialExtras.Add(new JObject());
  216. }
  217. }
  218. if (hasMaterialExtraData)
  219. {
  220. gltfObject.extras.Add("material", materialExtras);
  221. }
  222. }
  223. if (gltfObject.animations != null)
  224. {
  225. JArray animationExtras = new JArray();
  226. bool hasAnimationExtraData = false;
  227. foreach (GLTFAnimation animation in gltfObject.animations)
  228. {
  229. if (animation.extras != null)
  230. {
  231. hasAnimationExtraData = true;
  232. animationExtras.Add(animation.extras);
  233. }
  234. else
  235. {
  236. animationExtras.Add(new JObject());
  237. }
  238. }
  239. if (hasAnimationExtraData)
  240. {
  241. gltfObject.extras.Add("animation", animationExtras);
  242. }
  243. }
  244. importSettings.extrasProcessor.ProcessExtras(gameObject, animations, gltfObject.extras);
  245. }
  246. return gameObject;
  247. }
  248. #endregion
  249. #region Async
  250. private static IEnumerator LoadAsync(string json, string filepath, byte[] bytefile, long binChunkStart, ImportSettings importSettings, Action<GameObject, AnimationClip[]> onFinished, Action<float> onProgress = null) {
  251. // Threaded deserialization
  252. Task<GLTFObject> deserializeTask = new Task<GLTFObject>(() => JsonConvert.DeserializeObject<GLTFObject>(json));
  253. deserializeTask.Start();
  254. while (!deserializeTask.IsCompleted) yield return null;
  255. GLTFObject gltfObject = deserializeTask.Result;
  256. CheckExtensions(gltfObject);
  257. // directory root is sometimes used for loading buffers from containing file, or local images
  258. string directoryRoot = filepath != null ? Directory.GetParent(filepath).ToString() + "/" : null;
  259. importSettings.shaderOverrides.CacheDefaultShaders();
  260. // Setup import tasks
  261. List<ImportTask> importTasks = new List<ImportTask>();
  262. GLTFBuffer.ImportTask bufferTask = new GLTFBuffer.ImportTask(gltfObject.buffers, filepath, bytefile, binChunkStart);
  263. importTasks.Add(bufferTask);
  264. GLTFBufferView.ImportTask bufferViewTask = new GLTFBufferView.ImportTask(gltfObject.bufferViews, bufferTask);
  265. importTasks.Add(bufferViewTask);
  266. GLTFAccessor.ImportTask accessorTask = new GLTFAccessor.ImportTask(gltfObject.accessors, bufferViewTask);
  267. importTasks.Add(accessorTask);
  268. GLTFImage.ImportTask imageTask = new GLTFImage.ImportTask(gltfObject.images, directoryRoot, bufferViewTask);
  269. importTasks.Add(imageTask);
  270. GLTFTexture.ImportTask textureTask = new GLTFTexture.ImportTask(gltfObject.textures, imageTask);
  271. importTasks.Add(textureTask);
  272. GLTFMaterial.ImportTask materialTask = new GLTFMaterial.ImportTask(gltfObject.materials, textureTask, importSettings);
  273. importTasks.Add(materialTask);
  274. GLTFMesh.ImportTask meshTask = new GLTFMesh.ImportTask(gltfObject.meshes, accessorTask, bufferViewTask, materialTask, importSettings);
  275. importTasks.Add(meshTask);
  276. GLTFSkin.ImportTask skinTask = new GLTFSkin.ImportTask(gltfObject.skins, accessorTask);
  277. importTasks.Add(skinTask);
  278. GLTFNode.ImportTask nodeTask = new GLTFNode.ImportTask(gltfObject.nodes, meshTask, skinTask, gltfObject.cameras);
  279. importTasks.Add(nodeTask);
  280. // Ignite
  281. for (int i = 0; i < importTasks.Count; i++) {
  282. TaskSupervisor(importTasks[i], onProgress).RunCoroutine();
  283. }
  284. // Wait for all tasks to finish
  285. while (!importTasks.All(x => x.IsCompleted)) yield return null;
  286. // Fire onFinished when all tasks have completed
  287. GameObject root = nodeTask.Result.GetRoot();
  288. GLTFAnimation.ImportResult[] animationResult = gltfObject.animations.Import(accessorTask.Result, nodeTask.Result, importSettings);
  289. AnimationClip[] animations = new AnimationClip[0];
  290. if (animationResult != null) animations = animationResult.Select(x => x.clip).ToArray();
  291. if (onFinished != null) onFinished(nodeTask.Result.GetRoot(), animations);
  292. // Close file streams
  293. foreach (var item in bufferTask.Result) {
  294. item.Dispose();
  295. }
  296. }
  297. /// <summary> Keeps track of which threads to start when </summary>
  298. private static IEnumerator TaskSupervisor(ImportTask importTask, Action<float> onProgress = null) {
  299. // Wait for required results to complete before starting
  300. while (!importTask.IsReady) yield return null;
  301. // Prevent asynchronous data disorder
  302. yield return null;
  303. if(importTask.task != null)
  304. {
  305. // Start threaded task
  306. importTask.task.Start();
  307. // Wait for task to complete
  308. while (!importTask.task.IsCompleted) yield return null;
  309. // Prevent asynchronous data disorder
  310. yield return new WaitForSeconds(0.1f);
  311. }
  312. // Run additional unity code on main thread
  313. importTask.OnCoroutine(onProgress).RunCoroutine();
  314. //Wait for additional coroutines to complete
  315. while (!importTask.IsCompleted) { yield return null; }
  316. // Prevent asynchronous data disorder
  317. yield return new WaitForSeconds(0.1f);
  318. }
  319. #endregion
  320. private static void CheckExtensions(GLTFObject gLTFObject) {
  321. if (gLTFObject.extensionsRequired != null) {
  322. for (int i = 0; i < gLTFObject.extensionsRequired.Count; i++) {
  323. switch (gLTFObject.extensionsRequired[i]) {
  324. case "KHR_materials_pbrSpecularGlossiness":
  325. break;
  326. case "KHR_draco_mesh_compression":
  327. break;
  328. default:
  329. Debug.LogWarning($"GLTFUtility: Required extension '{gLTFObject.extensionsRequired[i]}' not supported. Import process will proceed but results may vary.");
  330. break;
  331. }
  332. }
  333. }
  334. }
  335. }
  336. }