USDLoader.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import {
  2. FileLoader,
  3. Loader,
  4. LoaderUtils
  5. } from 'three';
  6. import { unzipSync } from '../libs/fflate.module.js';
  7. import { USDAParser } from './usd/USDAParser.js';
  8. import { USDCParser } from './usd/USDCParser.js';
  9. import { USDComposer } from './usd/USDComposer.js';
  10. /**
  11. * A loader for the USD format (USD, USDA, USDC, USDZ).
  12. *
  13. * Supports both ASCII (USDA) and binary (USDC) USD files, as well as
  14. * USDZ archives containing either format.
  15. *
  16. * ```js
  17. * const loader = new USDLoader();
  18. * const model = await loader.loadAsync( 'model.usdz' );
  19. * scene.add( model );
  20. * ```
  21. *
  22. * @augments Loader
  23. * @three_import import { USDLoader } from 'three/addons/loaders/USDLoader.js';
  24. */
  25. class USDLoader extends Loader {
  26. /**
  27. * Constructs a new USDZ loader.
  28. *
  29. * @param {LoadingManager} [manager] - The loading manager.
  30. */
  31. constructor( manager ) {
  32. super( manager );
  33. }
  34. /**
  35. * Starts loading from the given URL and passes the loaded USDZ asset
  36. * to the `onLoad()` callback.
  37. *
  38. * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
  39. * @param {function(Group)} onLoad - Executed when the loading process has been finished.
  40. * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
  41. * @param {onErrorCallback} onError - Executed when errors occur.
  42. */
  43. load( url, onLoad, onProgress, onError ) {
  44. const scope = this;
  45. const path = ( scope.path === '' ) ? LoaderUtils.extractUrlBase( url ) : scope.path;
  46. const loader = new FileLoader( scope.manager );
  47. loader.setPath( scope.path );
  48. loader.setResponseType( 'arraybuffer' );
  49. loader.setRequestHeader( scope.requestHeader );
  50. loader.setWithCredentials( scope.withCredentials );
  51. loader.load( url, function ( text ) {
  52. try {
  53. scope.parse( text, path, onLoad, onError );
  54. } catch ( e ) {
  55. if ( onError ) {
  56. onError( e );
  57. } else {
  58. console.error( e );
  59. }
  60. scope.manager.itemError( url );
  61. }
  62. }, onProgress, onError );
  63. }
  64. /**
  65. * Parses the given USDZ data and returns the resulting group.
  66. *
  67. * The returned group is created synchronously, but any referenced textures
  68. * are loaded asynchronously. Provide `onLoad` to be notified once all
  69. * textures have finished loading.
  70. *
  71. * @param {ArrayBuffer|string} buffer - The raw USDZ data as an array buffer.
  72. * @param {string} [path=''] - The URL base path.
  73. * @param {function(Group)} [onLoad] - Executed once the group and all of its textures are ready.
  74. * @param {onErrorCallback} [onError] - Executed when errors occur.
  75. * @return {Group} The parsed asset as a group.
  76. */
  77. parse( buffer, path = '', onLoad, onError ) {
  78. const usda = new USDAParser();
  79. const usdc = new USDCParser();
  80. const textDecoder = new TextDecoder();
  81. function toArrayBuffer( data ) {
  82. if ( data instanceof ArrayBuffer ) return data;
  83. if ( data.byteOffset === 0 && data.byteLength === data.buffer.byteLength ) {
  84. return data.buffer;
  85. }
  86. return data.buffer.slice( data.byteOffset, data.byteOffset + data.byteLength );
  87. }
  88. function getLowercaseExtension( filename ) {
  89. const lastDot = filename.lastIndexOf( '.' );
  90. if ( lastDot < 0 ) return '';
  91. const lastSlash = filename.lastIndexOf( '/' );
  92. if ( lastSlash > lastDot ) return '';
  93. return filename.slice( lastDot + 1 ).toLowerCase();
  94. }
  95. function parseAssets( zip ) {
  96. const data = {};
  97. for ( const filename in zip ) {
  98. const fileBytes = zip[ filename ];
  99. const ext = getLowercaseExtension( filename );
  100. if ( ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'avif' ) {
  101. // Keep raw image bytes and create object URLs lazily in USDComposer.
  102. data[ filename ] = fileBytes;
  103. continue;
  104. }
  105. if ( ext !== 'usd' && ext !== 'usda' && ext !== 'usdc' ) continue;
  106. if ( isCrateFile( fileBytes ) ) {
  107. data[ filename ] = usdc.parseData( toArrayBuffer( fileBytes ) );
  108. } else {
  109. data[ filename ] = usda.parseData( textDecoder.decode( fileBytes ) );
  110. }
  111. }
  112. return data;
  113. }
  114. function isCrateFile( buffer ) {
  115. const crateHeader = new Uint8Array( [ 0x50, 0x58, 0x52, 0x2D, 0x55, 0x53, 0x44, 0x43 ] ); // PXR-USDC
  116. const view = buffer instanceof Uint8Array ? buffer : new Uint8Array( buffer );
  117. if ( view.byteLength < crateHeader.length ) return false;
  118. for ( let i = 0; i < crateHeader.length; i ++ ) {
  119. if ( view[ i ] !== crateHeader[ i ] ) return false;
  120. }
  121. return true;
  122. }
  123. function findUSD( zip ) {
  124. const fileNames = Object.keys( zip );
  125. if ( fileNames.length < 1 ) return { file: undefined, filename: '', basePath: '' };
  126. const firstFileName = fileNames[ 0 ];
  127. const ext = getLowercaseExtension( firstFileName );
  128. let isCrate = false;
  129. const lastSlash = firstFileName.lastIndexOf( '/' );
  130. const basePath = lastSlash >= 0 ? firstFileName.slice( 0, lastSlash ) : '';
  131. // Per AOUSD core spec v1.0.1 section 16.4.1.2, the first ZIP entry is the root layer.
  132. // ASCII files can end in either .usda or .usd.
  133. if ( ext === 'usda' ) return { file: zip[ firstFileName ], filename: firstFileName, basePath };
  134. if ( ext === 'usdc' ) {
  135. isCrate = true;
  136. } else if ( ext === 'usd' ) {
  137. // If this is not a crate file, we assume it is a plain USDA file.
  138. if ( ! isCrateFile( zip[ firstFileName ] ) ) {
  139. return { file: zip[ firstFileName ], filename: firstFileName, basePath };
  140. } else {
  141. isCrate = true;
  142. }
  143. }
  144. if ( isCrate ) {
  145. return { file: zip[ firstFileName ], filename: firstFileName, basePath };
  146. }
  147. return { file: undefined, filename: '', basePath: '' };
  148. }
  149. const scope = this;
  150. const finalize = ( composer, group ) => {
  151. if ( onLoad ) {
  152. Promise.all( composer.texturePromises )
  153. .then( () => onLoad( group ) )
  154. .catch( ( err ) => onError ? onError( err ) : console.error( err ) );
  155. }
  156. return group;
  157. };
  158. // USDA (standalone)
  159. if ( typeof buffer === 'string' ) {
  160. const composer = new USDComposer( scope.manager );
  161. const data = usda.parseData( buffer );
  162. return finalize( composer, composer.compose( data, {}, {}, path ) );
  163. }
  164. // USDC (standalone)
  165. if ( isCrateFile( buffer ) ) {
  166. const composer = new USDComposer( scope.manager );
  167. const data = usdc.parseData( toArrayBuffer( buffer ) );
  168. return finalize( composer, composer.compose( data, {}, {}, path ) );
  169. }
  170. const bytes = new Uint8Array( buffer );
  171. // USDZ
  172. if ( bytes[ 0 ] === 0x50 && bytes[ 1 ] === 0x4B ) {
  173. const zip = unzipSync( bytes );
  174. const assets = parseAssets( zip );
  175. const { file, filename, basePath } = findUSD( zip );
  176. if ( ! file ) {
  177. throw new Error( 'THREE.USDLoader: Invalid USDZ package. The first ZIP entry must be a USD layer (.usd/.usda/.usdc).' );
  178. }
  179. const composer = new USDComposer( scope.manager );
  180. const data = assets[ filename ];
  181. if ( ! data ) {
  182. throw new Error( 'THREE.USDLoader: Failed to parse root layer "' + filename + '".' );
  183. }
  184. return finalize( composer, composer.compose( data, assets, {}, basePath ) );
  185. }
  186. // USDA (standalone, as ArrayBuffer)
  187. const composer = new USDComposer( scope.manager );
  188. const text = textDecoder.decode( bytes );
  189. const data = usda.parseData( text );
  190. return finalize( composer, composer.compose( data, {}, {}, path ) );
  191. }
  192. }
  193. export { USDLoader };
粤ICP备19079148号