BabylonExporter.GLTFExporter.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. using BabylonExport.Entities;
  2. using GLTFExport.Entities;
  3. using Newtonsoft.Json;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Drawing;
  7. using System.Globalization;
  8. using System.IO;
  9. using System.Text;
  10. using Color = System.Drawing.Color;
  11. namespace Max2Babylon
  12. {
  13. internal partial class BabylonExporter
  14. {
  15. List<BabylonMaterial> babylonMaterialsToExport;
  16. private List<BabylonNode> babylonNodes;
  17. public void ExportGltf(BabylonScene babylonScene, string outputFile, bool generateBinary)
  18. {
  19. RaiseMessage("GLTFExporter | Export outputFile=" + outputFile + " generateBinary=" + generateBinary);
  20. RaiseMessage("GLTFExporter | Exportation started", Color.Blue);
  21. float progressionStep;
  22. var progression = 0.0f;
  23. ReportProgressChanged((int)progression);
  24. // Initialization
  25. initBabylonNodes(babylonScene);
  26. babylonMaterialsToExport = new List<BabylonMaterial>();
  27. var gltf = new GLTF(outputFile);
  28. // Asset
  29. gltf.asset = new GLTFAsset
  30. {
  31. version = "2.0",
  32. generator = "Babylon2Gltf2017",
  33. copyright = "2017 (c) BabylonJS"
  34. // no minVersion
  35. };
  36. // Scene
  37. gltf.scene = 0;
  38. // Scenes
  39. GLTFScene scene = new GLTFScene();
  40. GLTFScene[] scenes = { scene };
  41. gltf.scenes = scenes;
  42. // Meshes
  43. RaiseMessage("GLTFExporter | Exporting meshes");
  44. progression = 10.0f;
  45. ReportProgressChanged((int)progression);
  46. progressionStep = 40.0f / babylonScene.meshes.Length;
  47. foreach (var babylonMesh in babylonScene.meshes)
  48. {
  49. ExportMesh(babylonMesh, gltf, babylonScene);
  50. progression += progressionStep;
  51. ReportProgressChanged((int)progression);
  52. CheckCancelled();
  53. }
  54. // Root nodes
  55. RaiseMessage("GLTFExporter | Exporting nodes");
  56. List<BabylonNode> babylonRootNodes = babylonNodes.FindAll(node => node.parentId == null);
  57. progressionStep = 40.0f / babylonRootNodes.Count;
  58. babylonRootNodes.ForEach(babylonNode =>
  59. {
  60. exportNodeRec(babylonNode, gltf, babylonScene);
  61. progression += progressionStep;
  62. ReportProgressChanged((int)progression);
  63. CheckCancelled();
  64. });
  65. // Switch from left to right handed coordinate system
  66. var tmpNodesList = new List<int>(scene.NodesList);
  67. var rootNode = new BabylonMesh
  68. {
  69. name = "root",
  70. rotation = new float[] { 0, (float)Math.PI, 0 },
  71. scaling = new float[] { 1, 1, -1 },
  72. idGroupInstance = -1
  73. };
  74. scene.NodesList.Clear(); // Only root node is listed in node list
  75. GLTFNode gltfRootNode = ExportAbstractMesh(rootNode, gltf, null, null);
  76. gltfRootNode.ChildrenList.AddRange(tmpNodesList);
  77. // Materials
  78. RaiseMessage("GLTFExporter | Exporting materials");
  79. foreach (var babylonMaterial in babylonMaterialsToExport)
  80. {
  81. ExportMaterial(babylonMaterial, gltf);
  82. CheckCancelled();
  83. };
  84. RaiseMessage(string.Format("GLTFExporter | Nb materials exported: {0}", gltf.MaterialsList.Count), Color.Gray, 1);
  85. // Prepare buffers
  86. gltf.BuffersList.ForEach(buffer =>
  87. {
  88. buffer.BufferViews.ForEach(bufferView =>
  89. {
  90. bufferView.Accessors.ForEach(accessor =>
  91. {
  92. // Chunk must be padded with trailing zeros (0x00) to satisfy alignment requirements
  93. accessor.bytesList = new List<byte>(padChunk(accessor.bytesList.ToArray(), 4, 0x00));
  94. // Update byte properties
  95. accessor.byteOffset = bufferView.byteLength;
  96. bufferView.byteLength += accessor.bytesList.Count;
  97. // Merge bytes
  98. bufferView.bytesList.AddRange(accessor.bytesList);
  99. });
  100. // Update byte properties
  101. bufferView.byteOffset = buffer.byteLength;
  102. buffer.byteLength += bufferView.bytesList.Count;
  103. // Merge bytes
  104. buffer.bytesList.AddRange(bufferView.bytesList);
  105. });
  106. });
  107. // Cast lists to arrays
  108. gltf.Prepare();
  109. // Output
  110. RaiseMessage("GLTFExporter | Saving to output file");
  111. // Write .gltf file
  112. string outputGltfFile = Path.ChangeExtension(outputFile, "gltf");
  113. File.WriteAllText(outputGltfFile, gltfToJson(gltf));
  114. // Write .bin file
  115. string outputBinaryFile = Path.ChangeExtension(outputFile, "bin");
  116. using (BinaryWriter writer = new BinaryWriter(File.Open(outputBinaryFile, FileMode.Create)))
  117. {
  118. gltf.BuffersList.ForEach(buffer =>
  119. {
  120. buffer.bytesList.ForEach(b => writer.Write(b));
  121. });
  122. }
  123. // Binary
  124. if (generateBinary)
  125. {
  126. // Export glTF data to binary format .glb
  127. RaiseMessage("GLTFExporter | Generating .glb file");
  128. // Header
  129. UInt32 magic = 0x46546C67; // ASCII code for glTF
  130. UInt32 version = 2;
  131. UInt32 length = 12; // Header length
  132. // --- JSON chunk ---
  133. UInt32 chunkTypeJson = 0x4E4F534A; // ASCII code for JSON
  134. // Remove buffers uri
  135. foreach (GLTFBuffer gltfBuffer in gltf.BuffersList)
  136. {
  137. gltfBuffer.uri = null;
  138. }
  139. // Switch images to binary
  140. var imageBufferViews = SwitchImagesFromUriToBinary(gltf);
  141. imageBufferViews.ForEach(imageBufferView =>
  142. {
  143. imageBufferView.Buffer.bytesList.AddRange(imageBufferView.bytesList);
  144. });
  145. gltf.Prepare();
  146. // Serialize gltf data to JSON string then convert it to bytes
  147. byte[] chunkDataJson = Encoding.ASCII.GetBytes(gltfToJson(gltf));
  148. // JSON chunk must be padded with trailing Space chars (0x20) to satisfy alignment requirements
  149. chunkDataJson = padChunk(chunkDataJson, 4, 0x20);
  150. UInt32 chunkLengthJson = (UInt32)chunkDataJson.Length;
  151. length += chunkLengthJson + 8; // 8 = JSON chunk header length
  152. // bin chunk
  153. UInt32 chunkTypeBin = 0x004E4942; // ASCII code for BIN
  154. UInt32 chunkLengthBin = 0;
  155. if (gltf.BuffersList.Count > 0)
  156. {
  157. foreach (GLTFBuffer gltfBuffer in gltf.BuffersList)
  158. {
  159. chunkLengthBin += (uint)gltfBuffer.byteLength;
  160. }
  161. length += chunkLengthBin + 8; // 8 = bin chunk header length
  162. }
  163. // Write binary file
  164. string outputGlbFile = Path.ChangeExtension(outputFile, "glb");
  165. using (BinaryWriter writer = new BinaryWriter(File.Open(outputGlbFile, FileMode.Create)))
  166. {
  167. // Header
  168. writer.Write(magic);
  169. writer.Write(version);
  170. writer.Write(length);
  171. // JSON chunk
  172. writer.Write(chunkLengthJson);
  173. writer.Write(chunkTypeJson);
  174. writer.Write(chunkDataJson);
  175. // bin chunk
  176. if (gltf.BuffersList.Count > 0)
  177. {
  178. writer.Write(chunkLengthBin);
  179. writer.Write(chunkTypeBin);
  180. gltf.BuffersList[0].bytesList.ForEach(b => writer.Write(b));
  181. }
  182. };
  183. }
  184. ReportProgressChanged(100);
  185. }
  186. private List<BabylonNode> initBabylonNodes(BabylonScene babylonScene)
  187. {
  188. babylonNodes = new List<BabylonNode>();
  189. if (babylonScene.meshes != null)
  190. {
  191. int idGroupInstance = 0;
  192. foreach (var babylonMesh in babylonScene.meshes)
  193. {
  194. var babylonAbstractMeshes = new List<BabylonAbstractMesh>();
  195. babylonAbstractMeshes.Add(babylonMesh);
  196. if (babylonMesh.instances != null)
  197. {
  198. babylonAbstractMeshes.AddRange(babylonMesh.instances);
  199. }
  200. // Add mesh and instances to node list
  201. babylonNodes.AddRange(babylonAbstractMeshes);
  202. // Tag mesh and instances with an identifier
  203. babylonAbstractMeshes.ForEach(babylonAbstractMesh => babylonAbstractMesh.idGroupInstance = idGroupInstance);
  204. idGroupInstance++;
  205. }
  206. }
  207. if (babylonScene.lights != null)
  208. {
  209. babylonNodes.AddRange(babylonScene.lights);
  210. }
  211. if (babylonScene.cameras != null)
  212. {
  213. babylonNodes.AddRange(babylonScene.cameras);
  214. }
  215. return babylonNodes;
  216. }
  217. private void exportNodeRec(BabylonNode babylonNode, GLTF gltf, BabylonScene babylonScene, GLTFNode gltfParentNode = null)
  218. {
  219. GLTFNode gltfNode = null;
  220. var type = babylonNode.GetType();
  221. if (type == typeof(BabylonAbstractMesh) ||
  222. type.IsSubclassOf(typeof(BabylonAbstractMesh)))
  223. {
  224. gltfNode = ExportAbstractMesh(babylonNode as BabylonAbstractMesh, gltf, gltfParentNode, babylonScene);
  225. }
  226. else if (type == typeof(BabylonCamera))
  227. {
  228. GLTFCamera gltfCamera = ExportCamera(babylonNode as BabylonCamera, gltf, gltfParentNode);
  229. gltfNode = gltfCamera.gltfNode;
  230. }
  231. else if (type == typeof(BabylonLight))
  232. {
  233. if (isNodeRelevantToExport(babylonNode))
  234. {
  235. // Export light nodes as empty nodes (no lights in glTF 2.0 core)
  236. RaiseWarning($"GLTFExporter | Light named {babylonNode.name} has children but lights are not exported with glTF 2.0 core version. An empty node is used instead.", 1);
  237. gltfNode = ExportLight(babylonNode as BabylonLight, gltf, gltfParentNode);
  238. }
  239. else
  240. {
  241. RaiseMessage($"GLTFExporter | Light named {babylonNode.name} is not relevant to export", 1);
  242. }
  243. }
  244. else
  245. {
  246. RaiseError($"Node named {babylonNode.name} as no exporter", 1);
  247. }
  248. CheckCancelled();
  249. // If node is exported successfully...
  250. if (gltfNode != null)
  251. {
  252. // ...export its children
  253. List<BabylonNode> babylonDescendants = getDescendants(babylonNode);
  254. babylonDescendants.ForEach(descendant => exportNodeRec(descendant, gltf, babylonScene, gltfNode));
  255. }
  256. }
  257. private List<BabylonNode> getDescendants(BabylonNode babylonNode)
  258. {
  259. return babylonNodes.FindAll(node => node.parentId == babylonNode.id);
  260. }
  261. /// <summary>
  262. /// Return true if node descendant hierarchy has any Mesh or Camera to export
  263. /// </summary>
  264. private bool isNodeRelevantToExport(BabylonNode babylonNode)
  265. {
  266. var type = babylonNode.GetType();
  267. if (type == typeof(BabylonAbstractMesh) ||
  268. type.IsSubclassOf(typeof(BabylonAbstractMesh)) ||
  269. type == typeof(BabylonCamera))
  270. {
  271. return true;
  272. }
  273. // Descandant recursivity
  274. List<BabylonNode> babylonDescendants = getDescendants(babylonNode);
  275. int indexDescendant = 0;
  276. while (indexDescendant < babylonDescendants.Count) // while instead of for to stop as soon as a relevant node has been found
  277. {
  278. if (isNodeRelevantToExport(babylonDescendants[indexDescendant]))
  279. {
  280. return true;
  281. }
  282. indexDescendant++;
  283. }
  284. // No relevant node found in hierarchy
  285. return false;
  286. }
  287. private string gltfToJson(GLTF gltf)
  288. {
  289. var jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings());
  290. var sb = new StringBuilder();
  291. var sw = new StringWriter(sb, CultureInfo.InvariantCulture);
  292. // Do not use the optimized writer because it's not necessary to truncate values
  293. // Use the bounded writer in case some values are infinity ()
  294. using (var jsonWriter = new JsonTextWriterBounded(sw))
  295. {
  296. jsonWriter.Formatting = Formatting.None;
  297. jsonSerializer.Serialize(jsonWriter, gltf);
  298. }
  299. return sb.ToString();
  300. }
  301. private List<GLTFBufferView> SwitchImagesFromUriToBinary(GLTF gltf)
  302. {
  303. var imageBufferViews = new List<GLTFBufferView>();
  304. foreach (GLTFImage gltfImage in gltf.ImagesList)
  305. {
  306. var path = Path.Combine(gltf.OutputFolder, gltfImage.uri);
  307. using (Image image = Image.FromFile(path))
  308. {
  309. using (MemoryStream m = new MemoryStream())
  310. {
  311. var imageFormat = gltfImage.FileExtension == "jpeg" ? System.Drawing.Imaging.ImageFormat.Jpeg : System.Drawing.Imaging.ImageFormat.Png;
  312. image.Save(m, imageFormat);
  313. byte[] imageBytes = m.ToArray();
  314. // Chunk must be padded with trailing zeros (0x00) to satisfy alignment requirements
  315. imageBytes = padChunk(imageBytes, 4, 0x00);
  316. // BufferView - Image
  317. var buffer = gltf.buffer;
  318. var bufferViewImage = new GLTFBufferView
  319. {
  320. name = "bufferViewImage",
  321. buffer = buffer.index,
  322. Buffer = buffer,
  323. byteOffset = buffer.byteLength
  324. };
  325. bufferViewImage.index = gltf.BufferViewsList.Count;
  326. gltf.BufferViewsList.Add(bufferViewImage);
  327. imageBufferViews.Add(bufferViewImage);
  328. gltfImage.uri = null;
  329. gltfImage.bufferView = bufferViewImage.index;
  330. gltfImage.mimeType = "image/" + gltfImage.FileExtension;
  331. bufferViewImage.bytesList.AddRange(imageBytes);
  332. bufferViewImage.byteLength += imageBytes.Length;
  333. bufferViewImage.Buffer.byteLength += imageBytes.Length;
  334. }
  335. }
  336. }
  337. return imageBufferViews;
  338. }
  339. private byte[] padChunk(byte[] chunk, int padding, byte trailingChar)
  340. {
  341. var chunkModuloPadding = chunk.Length % padding;
  342. var nbCharacterToAdd = chunkModuloPadding == 0 ? 0 : (padding - chunkModuloPadding);
  343. var chunkList = new List<byte>(chunk);
  344. for (int i = 0; i < nbCharacterToAdd; i++)
  345. {
  346. chunkList.Add(trailingChar);
  347. }
  348. return chunkList.ToArray();
  349. }
  350. }
  351. }