glTF-parser.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. // Copyright (c) 2013 Fabrice Robinet
  2. // All rights reserved.
  3. //
  4. // Redistribution and use in source and binary forms, with or without
  5. // modification, are permitted provided that the following conditions are met:
  6. //
  7. // * Redistributions of source code must retain the above copyright
  8. // notice, this list of conditions and the following disclaimer.
  9. // * Redistributions in binary form must reproduce the above copyright
  10. // notice, this list of conditions and the following disclaimer in the
  11. // documentation and/or other materials provided with the distribution.
  12. //
  13. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  14. // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  15. // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  16. // ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
  17. // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  18. // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  19. // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  20. // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  21. // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  22. // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  23. /*
  24. The Abstract Loader has two modes:
  25. #1: [static] load all the JSON at once [as of now]
  26. #2: [stream] stream and parse JSON progressively [not yet supported]
  27. Whatever is the mechanism used to parse the JSON (#1 or #2),
  28. The loader starts by resolving the paths to binaries and referenced json files (by replace the value of the path property with an absolute path if it was relative).
  29. In case #1: it is guaranteed to call the concrete loader implementation methods in a order that solves the dependencies between the entries.
  30. only the nodes requires an extra pass to set up the hirerarchy.
  31. In case #2: the concrete implementation will have to solve the dependencies. no order is guaranteed.
  32. When case #1 is used the followed dependency order is:
  33. scenes -> nodes -> meshes -> materials -> techniques -> shaders
  34. -> buffers
  35. -> cameras
  36. -> lights
  37. The readers starts with the leafs, i.e:
  38. shaders, techniques, materials, meshes, buffers, cameras, lights, nodes, scenes
  39. For each called handle method called the client should return true if the next handle can be call right after returning,
  40. or false if a callback on client side will notify the loader that the next handle method can be called.
  41. */
  42. var global = window;
  43. (function (root, factory) {
  44. if (typeof exports === 'object') {
  45. // Node. Does not work with strict CommonJS, but
  46. // only CommonJS-like enviroments that support module.exports,
  47. // like Node.
  48. factory(module.exports);
  49. } else if (typeof define === 'function' && define.amd) {
  50. // AMD. Register as an anonymous module.
  51. define([], function () {
  52. return factory(root);
  53. });
  54. } else {
  55. // Browser globals
  56. factory(root);
  57. }
  58. }(this, function (root) {
  59. "use strict";
  60. var categoriesDepsOrder = ["extensions", "buffers", "bufferViews", "images", "videos", "samplers", "textures", "shaders", "programs", "techniques", "materials", "accessors", "meshes", "cameras", "lights", "skins", "nodes", "animations", "scenes"];
  61. var glTFParser = Object.create(Object.prototype, {
  62. _rootDescription: { value: null, writable: true },
  63. rootDescription: {
  64. set: function(value) {
  65. this._rootDescription = value;
  66. },
  67. get: function() {
  68. return this._rootDescription;
  69. }
  70. },
  71. baseURL: { value: null, writable: true },
  72. //detect absolute path following the same protocol than window.location
  73. _isAbsolutePath: {
  74. value: function(path) {
  75. var isAbsolutePathRegExp = new RegExp("^"+window.location.protocol, "i");
  76. return path.match(isAbsolutePathRegExp) ? true : false;
  77. }
  78. },
  79. resolvePathIfNeeded: {
  80. value: function(path) {
  81. if (this._isAbsolutePath(path)) {
  82. return path;
  83. }
  84. var isDataUriRegex = /^data:/;
  85. if (isDataUriRegex.test(path)) {
  86. return path;
  87. }
  88. return this.baseURL + path;
  89. }
  90. },
  91. _resolvePathsForCategories: {
  92. value: function(categories) {
  93. categories.forEach( function(category) {
  94. var descriptions = this.json[category];
  95. if (descriptions) {
  96. var descriptionKeys = Object.keys(descriptions);
  97. descriptionKeys.forEach( function(descriptionKey) {
  98. var description = descriptions[descriptionKey];
  99. description.uri = this.resolvePathIfNeeded(description.uri);
  100. }, this);
  101. }
  102. }, this);
  103. }
  104. },
  105. _json: {
  106. value: null,
  107. writable: true
  108. },
  109. json: {
  110. enumerable: true,
  111. get: function() {
  112. return this._json;
  113. },
  114. set: function(value) {
  115. if (this._json !== value) {
  116. this._json = value;
  117. this._resolvePathsForCategories(["buffers", "shaders", "images", "videos"]);
  118. }
  119. }
  120. },
  121. _path: {
  122. value: null,
  123. writable: true
  124. },
  125. getEntryDescription: {
  126. value: function (entryID, entryType) {
  127. var entries = null;
  128. var category = entryType;
  129. entries = this.rootDescription[category];
  130. if (!entries) {
  131. console.log("ERROR:CANNOT find expected category named:"+category);
  132. return null;
  133. }
  134. return entries ? entries[entryID] : null;
  135. }
  136. },
  137. _stepToNextCategory: {
  138. value: function() {
  139. this._state.categoryIndex = this.getNextCategoryIndex(this._state.categoryIndex + 1);
  140. if (this._state.categoryIndex !== -1) {
  141. this._state.categoryState.index = 0;
  142. return true;
  143. }
  144. return false;
  145. }
  146. },
  147. _stepToNextDescription: {
  148. enumerable: false,
  149. value: function() {
  150. var categoryState = this._state.categoryState;
  151. var keys = categoryState.keys;
  152. if (!keys) {
  153. console.log("INCONSISTENCY ERROR");
  154. return false;
  155. }
  156. categoryState.index++;
  157. categoryState.keys = null;
  158. if (categoryState.index >= keys.length) {
  159. return this._stepToNextCategory();
  160. }
  161. return false;
  162. }
  163. },
  164. hasCategory: {
  165. value: function(category) {
  166. return this.rootDescription[category] ? true : false;
  167. }
  168. },
  169. _handleState: {
  170. value: function() {
  171. var methodForType = {
  172. "buffers" : this.handleBuffer,
  173. "bufferViews" : this.handleBufferView,
  174. "shaders" : this.handleShader,
  175. "programs" : this.handleProgram,
  176. "techniques" : this.handleTechnique,
  177. "materials" : this.handleMaterial,
  178. "meshes" : this.handleMesh,
  179. "cameras" : this.handleCamera,
  180. "lights" : this.handleLight,
  181. "nodes" : this.handleNode,
  182. "scenes" : this.handleScene,
  183. "images" : this.handleImage,
  184. "animations" : this.handleAnimation,
  185. "accessors" : this.handleAccessor,
  186. "skins" : this.handleSkin,
  187. "samplers" : this.handleSampler,
  188. "textures" : this.handleTexture,
  189. "videos" : this.handleVideo,
  190. "extensions" : this.handleExtension,
  191. };
  192. var success = true;
  193. while (this._state.categoryIndex !== -1) {
  194. var category = categoriesDepsOrder[this._state.categoryIndex];
  195. var categoryState = this._state.categoryState;
  196. var keys = categoryState.keys;
  197. if (!keys) {
  198. categoryState.keys = keys = Object.keys(this.rootDescription[category]);
  199. if (keys) {
  200. if (keys.length == 0) {
  201. this._stepToNextDescription();
  202. continue;
  203. }
  204. }
  205. }
  206. var type = category;
  207. var entryID = keys[categoryState.index];
  208. var description = this.getEntryDescription(entryID, type);
  209. if (!description) {
  210. if (this.handleError) {
  211. this.handleError("INCONSISTENCY ERROR: no description found for entry "+entryID);
  212. success = false;
  213. break;
  214. }
  215. } else {
  216. if (methodForType[type]) {
  217. if (methodForType[type].call(this, entryID, description, this._state.userInfo) === false) {
  218. success = false;
  219. break;
  220. }
  221. }
  222. this._stepToNextDescription();
  223. }
  224. }
  225. if (this.handleLoadCompleted) {
  226. this.handleLoadCompleted(success);
  227. }
  228. }
  229. },
  230. _loadJSONIfNeeded: {
  231. enumerable: true,
  232. value: function(callback) {
  233. var self = this;
  234. //FIXME: handle error
  235. if (!this._json) {
  236. var jsonPath = this._path;
  237. var i = jsonPath.lastIndexOf("/");
  238. this.baseURL = (i !== 0) ? jsonPath.substring(0, i + 1) : '';
  239. var jsonfile = new XMLHttpRequest();
  240. jsonfile.open("GET", jsonPath, true);
  241. jsonfile.onreadystatechange = function() {
  242. if (jsonfile.readyState == 4) {
  243. if (jsonfile.status == 200) {
  244. self.json = JSON.parse(jsonfile.responseText);
  245. if (callback) {
  246. callback(self.json);
  247. }
  248. }
  249. }
  250. };
  251. jsonfile.send(null);
  252. } else {
  253. if (callback) {
  254. callback(this.json);
  255. }
  256. }
  257. }
  258. },
  259. /* load JSON and assign it as description to the reader */
  260. _buildLoader: {
  261. value: function(callback) {
  262. var self = this;
  263. function JSONReady(json) {
  264. self.rootDescription = json;
  265. if (callback)
  266. callback(this);
  267. }
  268. this._loadJSONIfNeeded(JSONReady);
  269. }
  270. },
  271. _state: { value: null, writable: true },
  272. _getEntryType: {
  273. value: function(entryID) {
  274. var rootKeys = categoriesDepsOrder;
  275. for (var i = 0 ; i < rootKeys.length ; i++) {
  276. var rootValues = this.rootDescription[rootKeys[i]];
  277. if (rootValues) {
  278. return rootKeys[i];
  279. }
  280. }
  281. return null;
  282. }
  283. },
  284. getNextCategoryIndex: {
  285. value: function(currentIndex) {
  286. for (var i = currentIndex ; i < categoriesDepsOrder.length ; i++) {
  287. if (this.hasCategory(categoriesDepsOrder[i])) {
  288. return i;
  289. }
  290. }
  291. return -1;
  292. }
  293. },
  294. load: {
  295. enumerable: true,
  296. value: function(userInfo, options) {
  297. var self = this;
  298. this._buildLoader(function loaderReady(reader) {
  299. var startCategory = self.getNextCategoryIndex.call(self,0);
  300. if (startCategory !== -1) {
  301. self._state = { "userInfo" : userInfo,
  302. "options" : options,
  303. "categoryIndex" : startCategory,
  304. "categoryState" : { "index" : "0" } };
  305. self._handleState();
  306. }
  307. });
  308. }
  309. },
  310. initWithPath: {
  311. value: function(path) {
  312. this._path = path;
  313. this._json = null;
  314. return this;
  315. }
  316. },
  317. //this is meant to be global and common for all instances
  318. _knownURLs: { writable: true, value: {} },
  319. //to be invoked by subclass, so that ids can be ensured to not overlap
  320. loaderContext: {
  321. value: function() {
  322. if (typeof this._knownURLs[this._path] === "undefined") {
  323. this._knownURLs[this._path] = Object.keys(this._knownURLs).length;
  324. }
  325. return "__" + this._knownURLs[this._path];
  326. }
  327. },
  328. initWithJSON: {
  329. value: function(json, baseURL) {
  330. this.json = json;
  331. this.baseURL = baseURL;
  332. if (!baseURL) {
  333. console.log("WARNING: no base URL passed to Reader:initWithJSON");
  334. }
  335. return this;
  336. }
  337. }
  338. });
  339. if(root) {
  340. root.glTFParser = glTFParser;
  341. }
  342. return glTFParser;
  343. }));