USDZLoader.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. import {
  2. BufferAttribute,
  3. BufferGeometry,
  4. FileLoader,
  5. Group,
  6. Loader,
  7. Mesh,
  8. MeshStandardMaterial,
  9. TextureLoader
  10. } from 'three';
  11. import * as fflate from '../libs/fflate.module.js';
  12. class USDAParser {
  13. parse( text ) {
  14. const data = {};
  15. const lines = text.split( '\n' );
  16. const length = lines.length;
  17. let current = 0;
  18. let string = null;
  19. let target = data;
  20. const stack = [ data ];
  21. // debugger;
  22. function parseNextLine() {
  23. const line = lines[ current ];
  24. // console.log( line );
  25. if ( line.includes( '=' ) ) {
  26. const assignment = line.split( '=' );
  27. const lhs = assignment[ 0 ].trim();
  28. const rhs = assignment[ 1 ].trim();
  29. if ( rhs.endsWith( '{' ) ) {
  30. const group = {};
  31. stack.push( group );
  32. target[ lhs ] = group;
  33. target = group;
  34. } else {
  35. target[ lhs ] = rhs;
  36. }
  37. } else if ( line.endsWith( '{' ) ) {
  38. const group = target[ string ] || {};
  39. stack.push( group );
  40. target[ string ] = group;
  41. target = group;
  42. } else if ( line.endsWith( '}' ) ) {
  43. stack.pop();
  44. if ( stack.length === 0 ) return;
  45. target = stack[ stack.length - 1 ];
  46. } else if ( line.endsWith( '(' ) ) {
  47. const meta = {};
  48. stack.push( meta );
  49. string = line.split( '(' )[ 0 ].trim() || string;
  50. target[ string ] = meta;
  51. target = meta;
  52. } else if ( line.endsWith( ')' ) ) {
  53. stack.pop();
  54. target = stack[ stack.length - 1 ];
  55. } else {
  56. string = line.trim();
  57. }
  58. current ++;
  59. if ( current < length ) {
  60. parseNextLine();
  61. }
  62. }
  63. parseNextLine();
  64. return data;
  65. }
  66. }
  67. class USDZLoader extends Loader {
  68. constructor( manager ) {
  69. super( manager );
  70. }
  71. load( url, onLoad, onProgress, onError ) {
  72. const scope = this;
  73. const loader = new FileLoader( scope.manager );
  74. loader.setPath( scope.path );
  75. loader.setResponseType( 'arraybuffer' );
  76. loader.setRequestHeader( scope.requestHeader );
  77. loader.setWithCredentials( scope.withCredentials );
  78. loader.load( url, function ( text ) {
  79. try {
  80. onLoad( scope.parse( text ) );
  81. } catch ( e ) {
  82. if ( onError ) {
  83. onError( e );
  84. } else {
  85. console.error( e );
  86. }
  87. scope.manager.itemError( url );
  88. }
  89. }, onProgress, onError );
  90. }
  91. parse( buffer ) {
  92. const parser = new USDAParser();
  93. function parseAssets( zip ) {
  94. const data = {};
  95. const loader = new FileLoader();
  96. loader.setResponseType( 'arraybuffer' );
  97. for ( const filename in zip ) {
  98. if ( filename.endsWith( 'png' ) ) {
  99. const blob = new Blob( [ zip[ filename ] ], { type: { type: 'image/png' } } );
  100. data[ filename ] = URL.createObjectURL( blob );
  101. }
  102. if ( filename.endsWith( 'usd' ) ) {
  103. const text = fflate.strFromU8( zip[ filename ] );
  104. data[ filename ] = parser.parse( text );
  105. }
  106. }
  107. return data;
  108. }
  109. function findUSD( zip ) {
  110. for ( const filename in zip ) {
  111. if ( filename.endsWith( 'usda' ) ) {
  112. return zip[ filename ];
  113. }
  114. }
  115. }
  116. const zip = fflate.unzipSync( new Uint8Array( buffer ) ); // eslint-disable-line no-undef
  117. // console.log( zip );
  118. const assets = parseAssets( zip );
  119. // console.log( assets )
  120. const file = findUSD( zip );
  121. if ( file === undefined ) {
  122. console.warn( 'THREE.USDZLoader: No usda file found.', zip );
  123. return new Group();
  124. }
  125. // Parse file
  126. const text = fflate.strFromU8( file );
  127. const root = parser.parse( text );
  128. // Build scene
  129. function findMeshGeometry( data ) {
  130. if ( 'prepend references' in data ) {
  131. const reference = data[ 'prepend references' ];
  132. const parts = reference.split( '@' );
  133. const path = parts[ 1 ].replace( /^.\//, '' );
  134. const id = parts[ 2 ].replace( /^<\//, '' ).replace( />$/, '' );
  135. return findGeometry( assets[ path ], id );
  136. }
  137. return findGeometry( data );
  138. }
  139. function findGeometry( data, id ) {
  140. if ( id !== undefined ) {
  141. const def = `def "%{id}"`;
  142. if ( def in data ) {
  143. return data[ def ];
  144. }
  145. }
  146. for ( const name in data ) {
  147. const object = data[ name ];
  148. if ( name.startsWith( 'def Mesh' ) ) {
  149. // Move points to Mesh
  150. if ( data[ 'point3f[] points' ] ) {
  151. object[ 'point3f[] points' ] = data[ 'point3f[] points' ];
  152. }
  153. // Move st indices to Mesh
  154. if ( data[ 'int[] primvars:st:indices' ] ) {
  155. object[ 'int[] primvars:st:indices' ] = data[ 'int[] primvars:st:indices' ];
  156. }
  157. return object;
  158. }
  159. if ( typeof object === 'object' ) {
  160. const geometry = findGeometry( object );
  161. if ( geometry ) return geometry;
  162. }
  163. }
  164. }
  165. function buildGeometry( data ) {
  166. let geometry = new BufferGeometry();
  167. if ( 'int[] faceVertexIndices' in data ) {
  168. const indices = JSON.parse( data[ 'int[] faceVertexIndices' ] );
  169. geometry.setIndex( new BufferAttribute( new Uint16Array( indices ), 1 ) );
  170. }
  171. if ( 'point3f[] points' in data ) {
  172. const positions = JSON.parse( data[ 'point3f[] points' ].replace( /[()]*/g, '' ) );
  173. const attribute = new BufferAttribute( new Float32Array( positions ), 3 );
  174. geometry.setAttribute( 'position', attribute );
  175. }
  176. if ( 'normal3f[] normals' in data ) {
  177. const normals = JSON.parse( data[ 'normal3f[] normals' ].replace( /[()]*/g, '' ) );
  178. const attribute = new BufferAttribute( new Float32Array( normals ), 3 );
  179. geometry.setAttribute( 'normal', attribute );
  180. } else {
  181. geometry.computeVertexNormals();
  182. }
  183. if ( 'texCoord2f[] primvars:st' in data ) {
  184. const uvs = JSON.parse( data[ 'texCoord2f[] primvars:st' ].replace( /[()]*/g, '' ) );
  185. const attribute = new BufferAttribute( new Float32Array( uvs ), 2 );
  186. if ( 'int[] primvars:st:indices' in data ) {
  187. geometry = geometry.toNonIndexed();
  188. const indices = JSON.parse( data[ 'int[] primvars:st:indices' ] );
  189. geometry.setAttribute( 'uv', toFlatBufferAttribute( attribute, indices ) );
  190. } else {
  191. geometry.setAttribute( 'uv', attribute );
  192. }
  193. }
  194. return geometry;
  195. }
  196. function toFlatBufferAttribute( attribute, indices ) {
  197. const array = attribute.array;
  198. const itemSize = attribute.itemSize;
  199. const array2 = new array.constructor( indices.length * itemSize );
  200. let index = 0, index2 = 0;
  201. for ( let i = 0, l = indices.length; i < l; i ++ ) {
  202. index = indices[ i ] * itemSize;
  203. for ( let j = 0; j < itemSize; j ++ ) {
  204. array2[ index2 ++ ] = array[ index ++ ];
  205. }
  206. }
  207. return new BufferAttribute( array2, itemSize );
  208. }
  209. function findMeshMaterial( data ) {
  210. if ( 'rel material:binding' in data ) {
  211. const reference = data[ 'rel material:binding' ];
  212. const id = reference.replace( /^<\//, '' ).replace( />$/, '' );
  213. const parts = id.split( '/' );
  214. return findMaterial( root, ` "${ parts[ 1 ] }"` );
  215. }
  216. return findMaterial( data );
  217. }
  218. function findMaterial( data, id = '' ) {
  219. for ( const name in data ) {
  220. const object = data[ name ];
  221. if ( name.startsWith( 'def Material' + id ) ) {
  222. return object;
  223. }
  224. if ( typeof object === 'object' ) {
  225. const material = findMaterial( object, id );
  226. if ( material ) return material;
  227. }
  228. }
  229. }
  230. function buildMaterial( data ) {
  231. const material = new MeshStandardMaterial();
  232. if ( data !== undefined ) {
  233. if ( 'def Shader "PreviewSurface"' in data ) {
  234. const surface = data[ 'def Shader "PreviewSurface"' ];
  235. if ( 'color3f inputs:diffuseColor' in surface ) {
  236. const color = surface[ 'color3f inputs:diffuseColor' ].replace( /[()]*/g, '' );
  237. material.color.fromArray( JSON.parse( '[' + color + ']' ) );
  238. }
  239. if ( 'float inputs:roughness' in surface ) {
  240. material.roughness = surface[ 'float inputs:roughness' ];
  241. }
  242. if ( 'float inputs:metallic' in surface ) {
  243. material.metalness = surface[ 'float inputs:metallic' ];
  244. }
  245. }
  246. if ( 'def Shader "diffuseColor_texture"' in data ) {
  247. const texture = data[ 'def Shader "diffuseColor_texture"' ];
  248. const file = texture[ 'asset inputs:file' ].replace( /@*/g, '' );
  249. material.map = new TextureLoader().load( assets[ file ] );
  250. }
  251. if ( 'def Shader "normal_texture"' in data ) {
  252. const texture = data[ 'def Shader "normal_texture"' ];
  253. const file = texture[ 'asset inputs:file' ].replace( /@*/g, '' );
  254. material.normalMap = new TextureLoader().load( assets[ file ] );
  255. }
  256. }
  257. return material;
  258. }
  259. function buildMesh( data ) {
  260. const geometry = buildGeometry( findMeshGeometry( data ) );
  261. const material = buildMaterial( findMeshMaterial( data ) );
  262. const mesh = new Mesh( geometry, material );
  263. if ( 'matrix4d xformOp:transform' in data ) {
  264. const array = JSON.parse( '[' + data[ 'matrix4d xformOp:transform' ].replace( /[()]*/g, '' ) + ']' );
  265. mesh.matrix.fromArray( array );
  266. mesh.matrix.decompose( mesh.position, mesh.quaternion, mesh.scale );
  267. }
  268. return mesh;
  269. }
  270. // console.log( data );
  271. const group = new Group();
  272. for ( const name in root ) {
  273. if ( name.startsWith( 'def Xform' ) ) {
  274. const mesh = buildMesh( root[ name ] );
  275. group.add( mesh );
  276. }
  277. }
  278. // console.log( group );
  279. return group;
  280. }
  281. }
  282. export { USDZLoader };