TinyUSDZLoader.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. import { Loader } from 'three'; // or https://cdn.jsdelivr.net/npm/three/build/three.module.js';
  2. // WASM module of TinyUSDZ.
  3. import initTinyUSDZNative from './tinyusdz.js';
  4. class FetchAssetResolver {
  5. constructor() {
  6. this.assetCache = new Map();
  7. }
  8. async resolveAsync(uri) {
  9. try {
  10. const response = await fetch(uri);
  11. if (!response.ok) {
  12. throw new Error(`Failed to fetch asset: ${uri}`);
  13. }
  14. const data = await response.arrayBuffer();
  15. //console.log(`Fetched asset ${uri} successfully, size: ${data.byteLength} bytes`);
  16. this.assetCache.set(uri, data);
  17. return Promise.resolve([uri, data]);
  18. } catch (error) {
  19. console.error(`Error resolving asset ${uri}:`, error);
  20. throw error;
  21. }
  22. }
  23. getAsset(uri) {
  24. if (this.assetCache.has(uri)) {
  25. return this.assetCache.get(uri);
  26. } else {
  27. console.warn(`Asset not found in cache: ${uri}`);
  28. return null;
  29. }
  30. }
  31. hasAsset(uri) {
  32. return this.assetCache.has(uri);
  33. }
  34. setAsset(uri, data) {
  35. this.assetCache.set(uri, data);
  36. }
  37. clearCache() {
  38. this.assetCache.clear();
  39. }
  40. }
  41. // TODO
  42. //
  43. // Polish API
  44. //
  45. class TinyUSDZLoader extends Loader {
  46. constructor(manager) {
  47. super(manager);
  48. this.native_ = null;
  49. this.assetResolver_ = null;
  50. // texture loader callback
  51. // null = Use TinyUSDZ's builtin image loader(C++ native module)
  52. //this.texLoader = null;
  53. this.imageCache = {};
  54. this.textureCache = {};
  55. // Default: do NOT use zstd compressed WASM.
  56. this.useZstdCompressedWasm_ = false;
  57. this.compressedWasmPath_ = 'tinyusdz.wasm.zst';
  58. }
  59. // Decompress zstd compressed WASM
  60. async decompressZstdWasm(compressedPath) {
  61. try {
  62. const fzstd = await import('fzstd');
  63. const wasmURL = new URL(compressedPath, import.meta.url).href;
  64. //console.log(`Loading compressed WASM from: ${wasmURL}`);
  65. const response = await fetch(wasmURL);
  66. //console.log(response);
  67. if (!response.ok) {
  68. throw new Error(`Failed to fetch compressed WASM: ${response.statusText}`);
  69. }
  70. const compressedData = await response.arrayBuffer();
  71. //console.log(`Compressed WASM size: ${compressedData.byteLength} bytes`);
  72. if (compressedData.byteLength < 1024*64) {
  73. throw new Error('Compressed WASM size is unusually small, may not be valid zstd compressed data.');
  74. }
  75. // Check zstd magic number (0x28B52FFD in little-endian)
  76. const magicBytes = new Uint8Array(compressedData, 0, 4);
  77. const expectedMagic = [0x28, 0xB5, 0x2F, 0xFD]; // Little-endian representation
  78. //console.log(magicBytes);
  79. //console.log(expectedMagic);
  80. if (compressedData.byteLength < 4 ||
  81. magicBytes[0] !== expectedMagic[0] ||
  82. magicBytes[1] !== expectedMagic[1] ||
  83. magicBytes[2] !== expectedMagic[2] ||
  84. magicBytes[3] !== expectedMagic[3]) {
  85. throw new Error('Invalid zstd file: magic number mismatch');
  86. }
  87. // Decompress using zstd
  88. const decompressedData = fzstd.decompress(new Uint8Array(compressedData));
  89. //console.log(`Decompressed WASM size: ${decompressedData.byteLength} bytes`);
  90. return decompressedData;
  91. } catch (error) {
  92. console.error('Error decompressing zstd WASM:', error);
  93. throw error;
  94. }
  95. }
  96. // Initialize the native WASM module
  97. // This is async but the load() method handles it internally with promises
  98. async init( options = {}) {
  99. if (Object.prototype.hasOwnProperty.call(options, 'useZstdCompressedWasm')) {
  100. this.useZstdCompressedWasm_ = options.useZstdCompressedWasm;
  101. }
  102. if (!this.native_) {
  103. //console.log('Initializing native module...');
  104. let wasmBinary = null;
  105. if (this.useZstdCompressedWasm_) {
  106. // Load and decompress zstd compressed WASM
  107. wasmBinary = await this.decompressZstdWasm(this.compressedWasmPath_);
  108. }
  109. // Initialize with custom WASM binary if decompressed
  110. const initOptions = wasmBinary ? { wasmBinary } : {};
  111. this.native_ = await initTinyUSDZNative(initOptions);
  112. if (!this.native_) {
  113. throw new Error('TinyUSDZLoader: Failed to initialize native module.');
  114. }
  115. //console.log('Native module initialized');
  116. }
  117. return this;
  118. }
  119. // TODO: remove
  120. // Set AssetResolver callback.
  121. // This is used to resolve asset paths(e.g. textures, usd files) in the USD.
  122. // For web app, usually we'll convert asset path to URI
  123. //setAssetResolver(callback) {
  124. // this.assetResolver_ = callback;
  125. //}
  126. //
  127. // Load a USDZ/USDA/USDC file from a URL as USD Stage(Freezed scene graph)
  128. // NOTE: for loadAsync(), Use base Loader class's loadAsync() method
  129. //
  130. load(url, onLoad, onProgress, onError) {
  131. //console.log('url', url);
  132. const scope = this;
  133. // Create a promise chain to handle initialization and loading
  134. const initPromise = this.native_ ? Promise.resolve() : this.init();
  135. initPromise
  136. .then(() => {
  137. return fetch(url);
  138. })
  139. .then((response) => {
  140. return response.arrayBuffer();
  141. })
  142. .then((usd_data) => {
  143. const usd_binary = new Uint8Array(usd_data);
  144. //console.log('Loaded USD binary data:', usd_binary.length, 'bytes');
  145. scope.parse(usd_binary, url, function (usd) {
  146. onLoad(usd);
  147. }, onError);
  148. })
  149. .catch((error) => {
  150. console.error('TinyUSDZLoader: Error initializing native module:', error);
  151. if (onError) {
  152. onError(error);
  153. }
  154. });
  155. }
  156. //
  157. // Parse a USDZ/USDA/USDC binary data
  158. //
  159. parse(binary /* ArrayBuffer */, filePath /* optional */, onLoad, onError) {
  160. const _onError = function (e) {
  161. if (onError) {
  162. onError(e);
  163. } else {
  164. console.error(e);
  165. }
  166. //scope.manager.itemError( url );
  167. //scope.manager.itemEnd( url );
  168. };
  169. if (!this.native_) {
  170. console.error('TinyUSDZLoader: Native module is not initialized.');
  171. _onError(new Error('TinyUSDZLoader: Native module is not initialized.'));
  172. }
  173. const usd = new this.native_.TinyUSDZLoaderNative();
  174. const ok = usd.loadFromBinary(binary, filePath);
  175. if (!ok) {
  176. _onError(new Error('TinyUSDZLoader: Failed to load USD from binary data.', {cause: usd.error()}));
  177. } else {
  178. onLoad(usd);
  179. }
  180. }
  181. //
  182. // Load a USDZ/USDA/USDC file from a URL as USD Layer(for composition)
  183. //
  184. loadAsLayer(url, onLoad, onProgress, onError) {
  185. //console.log('url', url);
  186. const scope = this;
  187. const _onError = function (e) {
  188. if (onError) {
  189. onError(e);
  190. } else {
  191. console.error(e);
  192. }
  193. //scope.manager.itemError( url );
  194. //scope.manager.itemEnd( url );
  195. };
  196. // Create a promise chain to handle initialization and loading
  197. const initPromise = this.native_ ? Promise.resolve() : this.init();
  198. initPromise
  199. .then(() => {
  200. //usd_ = new this.native_.TinyUSDZLoaderNative();
  201. return fetch(url);
  202. })
  203. .then((response) => {
  204. //console.log('fetch USDZ file done:', url);
  205. return response.arrayBuffer();
  206. })
  207. .then((usd_data) => {
  208. const usd_binary = new Uint8Array(usd_data);
  209. //console.log('Loaded USD binary data:', usd_binary.length, 'bytes');
  210. //return this.parse(usd_binary);
  211. const usd = new this.native_.TinyUSDZLoaderNative();
  212. const ok = usd.loadAsLayerFromBinary(usd_binary, url);
  213. if (!ok) {
  214. _onError(new Error('TinyUSDZLoader: Failed to load USD as Layer from binary data. url: ' + url, {cause: usd.error()}));
  215. } else {
  216. onLoad(usd);
  217. }
  218. })
  219. .catch((error) => {
  220. console.error('TinyUSDZLoader: Error initializing native module:', error);
  221. if (onError) {
  222. onError(error);
  223. }
  224. });
  225. }
  226. async loadAsLayerAsync(url, onProgress) {
  227. const scope = this;
  228. return new Promise( function ( resolve, reject ) {
  229. scope.loadAsLayer( url, resolve, onProgress, reject );
  230. } );
  231. }
  232. ///**
  233. // * Set texture callback
  234. // */
  235. //setTextureLoader(texLoader) {
  236. // this.texLoader = texLoader;
  237. //}
  238. }
  239. export { TinyUSDZLoader, FetchAssetResolver };