USDZLoader.js 11 KB

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