GLTFTab.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import { Mesh, Nullable, NullEngine, PBRMaterial, Scene, SceneLoader, StandardMaterial, Texture, TransformNode } from "babylonjs";
  2. import { GLTF2, GLTFFileLoader } from "babylonjs-loaders";
  3. import { GLTF2Export } from "babylonjs-serializers";
  4. import { DetailPanel } from "../details/DetailPanel";
  5. import { Property } from "../details/Property";
  6. import { PropertyLine } from "../details/PropertyLine";
  7. import { Helpers } from "../helpers/Helpers";
  8. import { Inspector } from "../Inspector";
  9. import { Tab } from "./Tab";
  10. import { TabBar } from "./TabBar";
  11. import "babylonjs-gltf2interface";
  12. import * as Split from "Split";
  13. export class GLTFTab extends Tab {
  14. private static _LoaderDefaults: any = null;
  15. private static _ValidationResults: Nullable<IGLTFValidationResults> = null;
  16. private static _OnValidationResultsUpdated: Nullable<(results: IGLTFValidationResults) => void> = null;
  17. private _inspector: Inspector;
  18. private _actions: HTMLDivElement;
  19. private _detailsPanel: Nullable<DetailPanel> = null;
  20. private _split: any;
  21. public static get IsSupported(): boolean {
  22. return !!(SceneLoader && GLTFFileLoader && GLTF2.GLTFLoader) || !!GLTF2Export;
  23. }
  24. /** @hidden */
  25. public static _Initialize(): void {
  26. // Must register with OnPluginActivatedObservable as early as possible to override the loader defaults.
  27. SceneLoader.OnPluginActivatedObservable.add((loader: GLTFFileLoader) => {
  28. if (loader.name === "gltf") {
  29. GLTFTab._ApplyLoaderDefaults(loader);
  30. loader.onValidatedObservable.add(results => {
  31. GLTFTab._ValidationResults = results;
  32. if (GLTFTab._OnValidationResultsUpdated) {
  33. GLTFTab._OnValidationResultsUpdated(results);
  34. }
  35. });
  36. }
  37. });
  38. }
  39. constructor(tabbar: TabBar, inspector: Inspector) {
  40. super(tabbar, "GLTF");
  41. this._inspector = inspector;
  42. this._panel = Helpers.CreateDiv("tab-panel") as HTMLDivElement;
  43. this._actions = Helpers.CreateDiv("gltf-actions", this._panel) as HTMLDivElement;
  44. this._actions.addEventListener("click", event => {
  45. this._closeDetailsPanel();
  46. });
  47. if (SceneLoader && GLTFFileLoader && GLTF2.GLTFLoader) {
  48. this._addImport();
  49. }
  50. if (GLTF2Export) {
  51. this._addExport();
  52. }
  53. }
  54. public dispose() {
  55. if (this._detailsPanel) {
  56. this._detailsPanel.dispose();
  57. }
  58. }
  59. private _addImport() {
  60. const importTitle = Helpers.CreateDiv("gltf-title", this._actions);
  61. importTitle.textContent = "Import";
  62. const importActions = Helpers.CreateDiv("gltf-actions", this._actions) as HTMLDivElement;
  63. GLTFTab._GetLoaderDefaultsAsync().then(defaults => {
  64. const loaderAction = Helpers.CreateDiv("gltf-action", importActions) as HTMLDivElement;
  65. loaderAction.innerText = "Loader";
  66. loaderAction.addEventListener("click", event => {
  67. this._showLoaderDefaults(defaults);
  68. event.stopPropagation();
  69. });
  70. const extensionsTitle = Helpers.CreateDiv("gltf-title", importActions) as HTMLDivElement;
  71. extensionsTitle.textContent = "Extensions";
  72. for (const extensionName in defaults.extensions) {
  73. const extensionDefaults = defaults.extensions[extensionName];
  74. const extensionAction = Helpers.CreateDiv("gltf-action", importActions);
  75. extensionAction.addEventListener("click", event => {
  76. if (this._showLoaderExtensionDefaults(extensionDefaults)) {
  77. event.stopPropagation();
  78. }
  79. });
  80. const checkbox = Helpers.CreateElement("span", "gltf-checkbox", extensionAction);
  81. if (extensionDefaults.enabled) {
  82. checkbox.classList.add("action", "active");
  83. }
  84. checkbox.addEventListener("click", () => {
  85. checkbox.classList.toggle("active");
  86. extensionDefaults.enabled = checkbox.classList.contains("active");
  87. });
  88. const label = Helpers.CreateElement("span", null, extensionAction);
  89. label.textContent = extensionName;
  90. }
  91. let validationTitle: Nullable<HTMLDivElement> = null;
  92. let validationAction: Nullable<HTMLDivElement> = null;
  93. GLTFTab._OnValidationResultsUpdated = results => {
  94. if (!validationTitle) {
  95. validationTitle = Helpers.CreateDiv("gltf-title", importActions) as HTMLDivElement;
  96. }
  97. if (!validationAction) {
  98. validationAction = Helpers.CreateDiv("gltf-action", importActions) as HTMLDivElement;
  99. validationAction.addEventListener("click", event => {
  100. GLTFTab._ShowValidationResults();
  101. event.stopPropagation();
  102. });
  103. }
  104. validationTitle.textContent = results.uri === "null" ? "Validation" : `Validation - ${BABYLON.Tools.GetFilename(results.uri)}`;
  105. GLTFTab._FormatValidationResultsShort(validationAction, results);
  106. };
  107. if (GLTFTab._ValidationResults) {
  108. GLTFTab._OnValidationResultsUpdated(GLTFTab._ValidationResults);
  109. }
  110. });
  111. }
  112. private static _FormatValidationResultsShort(validationAction: HTMLDivElement, results: IGLTFValidationResults): void {
  113. validationAction.innerHTML = "";
  114. let message = "";
  115. const add = (count: number, issueType: string): void => {
  116. if (count) {
  117. if (message) {
  118. message += ", ";
  119. }
  120. message += count === 1 ? `${count} ${issueType}` : `${count} ${issueType}s`;
  121. }
  122. };
  123. const issues = results.issues;
  124. add(issues.numErrors, "error");
  125. add(issues.numWarnings, "warning");
  126. add(issues.numInfos, "info");
  127. add(issues.numHints, "hint");
  128. const actionDiv = Helpers.CreateDiv("gltf-action", validationAction) as HTMLDivElement;
  129. const iconSpan = Helpers.CreateElement("span", "gltf-icon", actionDiv, issues.numErrors ? "The asset contains errors." : "The asset is valid.");
  130. iconSpan.textContent = issues.numErrors ? "\uf057" : "\uf058";
  131. iconSpan.style.color = issues.numErrors ? "red" : "green";
  132. const messageSpan = Helpers.CreateElement("span", "gltf-icon", actionDiv);
  133. messageSpan.textContent = message || "No issues";
  134. }
  135. private static _ShowValidationResults(): void {
  136. if (GLTFTab._ValidationResults) {
  137. const win = window.open("", "_blank");
  138. if (win) {
  139. // TODO: format this better and use generator registry (https://github.com/KhronosGroup/glTF-Generator-Registry)
  140. win.document.title = "glTF Validation Results";
  141. win.document.body.innerText = JSON.stringify(GLTFTab._ValidationResults, null, 2);
  142. win.document.body.style.whiteSpace = "pre";
  143. win.document.body.style.fontFamily = `monospace`;
  144. win.document.body.style.fontSize = `14px`;
  145. win.focus();
  146. }
  147. }
  148. }
  149. private static _ApplyLoaderDefaults(loader: GLTFFileLoader): void {
  150. const defaults = GLTFTab._LoaderDefaults;
  151. if (defaults) {
  152. for (const key in defaults) {
  153. if (key !== "extensions") {
  154. (loader as any)[key] = defaults[key];
  155. }
  156. }
  157. loader.onExtensionLoadedObservable.add(extension => {
  158. const extensionDefaults = defaults.extensions[extension.name];
  159. for (const key in extensionDefaults) {
  160. (extension as any)[key] = extensionDefaults[key];
  161. }
  162. });
  163. }
  164. }
  165. private static _GetPublic(obj: any): any {
  166. const result: any = {};
  167. for (const key in obj) {
  168. if (key !== "name" && key[0] !== "_") {
  169. const value = obj[key];
  170. const type = typeof value;
  171. if (type !== "object" && type !== "function" && type !== "undefined") {
  172. result[key] = value;
  173. }
  174. }
  175. }
  176. return result;
  177. }
  178. /** @hidden */
  179. public static _GetLoaderDefaultsAsync(): Promise<any> {
  180. if (GLTFTab._LoaderDefaults) {
  181. return Promise.resolve(GLTFTab._LoaderDefaults);
  182. }
  183. const engine = new NullEngine();
  184. const scene = new Scene(engine);
  185. const loader = new GLTFFileLoader();
  186. GLTFTab._LoaderDefaults = GLTFTab._GetPublic(loader);
  187. GLTFTab._LoaderDefaults.extensions = {};
  188. loader.onExtensionLoadedObservable.add(extension => {
  189. GLTFTab._LoaderDefaults.extensions[extension.name] = GLTFTab._GetPublic(extension);
  190. });
  191. const data = '{ "asset": { "version": "2.0" } }';
  192. return loader.importMeshAsync([], scene, data, "").then(() => {
  193. engine.dispose();
  194. return GLTFTab._LoaderDefaults;
  195. });
  196. }
  197. private _openDetailsPanel(): DetailPanel {
  198. if (!this._detailsPanel) {
  199. this._detailsPanel = new DetailPanel();
  200. this._panel.appendChild(this._detailsPanel.toHtml());
  201. this._split = Split([this._actions, this._detailsPanel.toHtml()], {
  202. blockDrag: this._inspector.popupMode,
  203. sizes: [50, 50],
  204. direction: "vertical"
  205. });
  206. }
  207. this._detailsPanel.clean();
  208. return this._detailsPanel;
  209. }
  210. private _closeDetailsPanel(): void {
  211. if (this._detailsPanel) {
  212. this._detailsPanel.toHtml().remove();
  213. this._detailsPanel.dispose();
  214. this._detailsPanel = null;
  215. }
  216. if (this._split) {
  217. this._split.destroy();
  218. delete this._split;
  219. }
  220. }
  221. private _showLoaderDefaults(defaults: { [key: string]: any }): void {
  222. var detailsPanel = this._openDetailsPanel();
  223. const details = new Array<PropertyLine>();
  224. for (const key in defaults) {
  225. if (key !== "extensions") {
  226. details.push(new PropertyLine(new Property(key, defaults, this._inspector.scene)));
  227. }
  228. }
  229. detailsPanel.details = details;
  230. }
  231. private _showLoaderExtensionDefaults(defaults: { [key: string]: any }): boolean {
  232. if (Object.keys(defaults).length === 1) {
  233. return false;
  234. }
  235. var detailsPanel = this._openDetailsPanel();
  236. const details = new Array<PropertyLine>();
  237. for (const key in defaults) {
  238. if (key !== "enabled") {
  239. details.push(new PropertyLine(new Property(key, defaults, this._inspector.scene)));
  240. }
  241. }
  242. detailsPanel.details = details;
  243. return true;
  244. }
  245. private _addExport() {
  246. const exportTitle = Helpers.CreateDiv("gltf-title", this._actions);
  247. exportTitle.textContent = "Export";
  248. const exportActions = Helpers.CreateDiv("gltf-actions", this._actions) as HTMLDivElement;
  249. const name = Helpers.CreateInput("gltf-input", exportActions);
  250. name.placeholder = "File name...";
  251. const button = Helpers.CreateElement("button", "gltf-button", exportActions) as HTMLButtonElement;
  252. button.innerText = "Export GLB";
  253. button.addEventListener("click", () => {
  254. GLTF2Export.GLBAsync(this._inspector.scene, name.value || "scene", {
  255. shouldExportTransformNode: transformNode => !GLTFTab._IsSkyBox(transformNode)
  256. }).then((glb) => {
  257. glb.downloadFiles();
  258. });
  259. });
  260. }
  261. private static _IsSkyBox(transformNode: TransformNode): boolean {
  262. if (transformNode instanceof Mesh) {
  263. if (transformNode.material) {
  264. const material = transformNode.material as PBRMaterial | StandardMaterial;
  265. const reflectionTexture = material.reflectionTexture;
  266. if (reflectionTexture && reflectionTexture.coordinatesMode === Texture.SKYBOX_MODE) {
  267. return true;
  268. }
  269. }
  270. }
  271. return false;
  272. }
  273. }
  274. GLTFTab._Initialize();