USDComposer.js 88 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843
  1. import {
  2. AnimationClip,
  3. BufferAttribute,
  4. BufferGeometry,
  5. ClampToEdgeWrapping,
  6. Euler,
  7. Group,
  8. Matrix4,
  9. Mesh,
  10. MeshPhysicalMaterial,
  11. MirroredRepeatWrapping,
  12. NoColorSpace,
  13. Object3D,
  14. Quaternion,
  15. QuaternionKeyframeTrack,
  16. RepeatWrapping,
  17. ShapeUtils,
  18. SkinnedMesh,
  19. Skeleton,
  20. Bone,
  21. SRGBColorSpace,
  22. Texture,
  23. Vector2,
  24. Vector3,
  25. VectorKeyframeTrack
  26. } from 'three';
  27. // Pre-compiled regex patterns for performance
  28. const VARIANT_PATH_REGEX = /^(.+?)\/\{(\w+)=(\w+)\}\/(.+)$/;
  29. // Spec types (must match USDCParser)
  30. const SpecType = {
  31. Unknown: 0,
  32. Attribute: 1,
  33. Connection: 2,
  34. Expression: 3,
  35. Mapper: 4,
  36. MapperArg: 5,
  37. Prim: 6,
  38. PseudoRoot: 7,
  39. Relationship: 8,
  40. RelationshipTarget: 9,
  41. Variant: 10,
  42. VariantSet: 11
  43. };
  44. /**
  45. * USDComposer handles scene composition from parsed USD data.
  46. * This includes reference resolution, variant selection, transform handling,
  47. * and building the Three.js scene graph.
  48. *
  49. * Works with specsByPath format from USDCParser.
  50. */
  51. class USDComposer {
  52. constructor( manager = null ) {
  53. this.textureCache = {};
  54. this.skinnedMeshes = [];
  55. this.manager = manager;
  56. }
  57. /**
  58. * Compose a Three.js scene from parsed USD data.
  59. * @param {Object} parsedData - Data from USDCParser or USDAParser
  60. * @param {Object} assets - Dictionary of referenced assets (specsByPath or blob URLs)
  61. * @param {Object} variantSelections - External variant selections
  62. * @param {string} basePath - Base path for resolving relative references
  63. * @returns {Group} Three.js scene graph
  64. */
  65. compose( parsedData, assets = {}, variantSelections = {}, basePath = '' ) {
  66. this.specsByPath = parsedData.specsByPath;
  67. this.assets = assets;
  68. this.externalVariantSelections = variantSelections;
  69. this.basePath = basePath;
  70. this.skinnedMeshes = [];
  71. this.skeletons = {};
  72. // Build indexes for O(1) lookups
  73. this._buildIndexes();
  74. // Get FPS from root spec
  75. const rootSpec = this.specsByPath[ '/' ];
  76. const rootFields = rootSpec ? rootSpec.fields : {};
  77. this.fps = rootFields.framesPerSecond || rootFields.timeCodesPerSecond || 30;
  78. const group = new Group();
  79. this._buildHierarchy( group, '/' );
  80. // Bind skeletons to skinned meshes
  81. this._bindSkeletons();
  82. // Build animations
  83. group.animations = this._buildAnimations();
  84. // Handle Z-up to Y-up conversion
  85. if ( rootSpec && rootSpec.fields && rootSpec.fields.upAxis === 'Z' ) {
  86. group.rotation.x = - Math.PI / 2;
  87. }
  88. return group;
  89. }
  90. /**
  91. * Apply USD transforms to a Three.js object.
  92. * Handles xformOpOrder with proper matrix composition.
  93. * USD uses row-vector convention, Three.js uses column-vector.
  94. */
  95. applyTransform( obj, fields, attrs = {} ) {
  96. const data = { ...fields, ...attrs };
  97. const xformOpOrder = data[ 'xformOpOrder' ];
  98. // If we have xformOpOrder, apply transforms using matrices
  99. if ( xformOpOrder && xformOpOrder.length > 0 ) {
  100. const matrix = new Matrix4();
  101. const tempMatrix = new Matrix4();
  102. // Iterate FORWARD for Three.js column-vector convention
  103. for ( let i = 0; i < xformOpOrder.length; i ++ ) {
  104. const op = xformOpOrder[ i ];
  105. const isInverse = op.startsWith( '!invert!' );
  106. const opName = isInverse ? op.slice( 8 ) : op;
  107. if ( opName === 'xformOp:transform' ) {
  108. const m = data[ 'xformOp:transform' ];
  109. if ( m && m.length === 16 ) {
  110. tempMatrix.fromArray( m );
  111. if ( isInverse ) tempMatrix.invert();
  112. matrix.multiply( tempMatrix );
  113. }
  114. } else if ( opName === 'xformOp:translate' ) {
  115. const t = data[ 'xformOp:translate' ];
  116. if ( t ) {
  117. tempMatrix.makeTranslation( t[ 0 ], t[ 1 ], t[ 2 ] );
  118. if ( isInverse ) tempMatrix.invert();
  119. matrix.multiply( tempMatrix );
  120. }
  121. } else if ( opName === 'xformOp:translate:pivot' ) {
  122. const t = data[ 'xformOp:translate:pivot' ];
  123. if ( t ) {
  124. tempMatrix.makeTranslation( t[ 0 ], t[ 1 ], t[ 2 ] );
  125. if ( isInverse ) tempMatrix.invert();
  126. matrix.multiply( tempMatrix );
  127. }
  128. } else if ( opName === 'xformOp:scale' ) {
  129. const s = data[ 'xformOp:scale' ];
  130. if ( s ) {
  131. if ( Array.isArray( s ) ) {
  132. tempMatrix.makeScale( s[ 0 ], s[ 1 ], s[ 2 ] );
  133. } else {
  134. tempMatrix.makeScale( s, s, s );
  135. }
  136. if ( isInverse ) tempMatrix.invert();
  137. matrix.multiply( tempMatrix );
  138. }
  139. } else if ( opName === 'xformOp:rotateXYZ' ) {
  140. const r = data[ 'xformOp:rotateXYZ' ];
  141. if ( r ) {
  142. // USD rotateXYZ: matrix = Rx * Ry * Rz
  143. // Three.js Euler 'ZYX' order produces same result
  144. const euler = new Euler(
  145. r[ 0 ] * Math.PI / 180,
  146. r[ 1 ] * Math.PI / 180,
  147. r[ 2 ] * Math.PI / 180,
  148. 'ZYX'
  149. );
  150. tempMatrix.makeRotationFromEuler( euler );
  151. if ( isInverse ) tempMatrix.invert();
  152. matrix.multiply( tempMatrix );
  153. }
  154. } else if ( opName === 'xformOp:rotateX' ) {
  155. const r = data[ 'xformOp:rotateX' ];
  156. if ( r !== undefined ) {
  157. tempMatrix.makeRotationX( r * Math.PI / 180 );
  158. if ( isInverse ) tempMatrix.invert();
  159. matrix.multiply( tempMatrix );
  160. }
  161. } else if ( opName === 'xformOp:rotateY' ) {
  162. const r = data[ 'xformOp:rotateY' ];
  163. if ( r !== undefined ) {
  164. tempMatrix.makeRotationY( r * Math.PI / 180 );
  165. if ( isInverse ) tempMatrix.invert();
  166. matrix.multiply( tempMatrix );
  167. }
  168. } else if ( opName === 'xformOp:rotateZ' ) {
  169. const r = data[ 'xformOp:rotateZ' ];
  170. if ( r !== undefined ) {
  171. tempMatrix.makeRotationZ( r * Math.PI / 180 );
  172. if ( isInverse ) tempMatrix.invert();
  173. matrix.multiply( tempMatrix );
  174. }
  175. } else if ( opName === 'xformOp:orient' ) {
  176. const q = data[ 'xformOp:orient' ];
  177. if ( q && q.length === 4 ) {
  178. const quat = new Quaternion( q[ 0 ], q[ 1 ], q[ 2 ], q[ 3 ] );
  179. tempMatrix.makeRotationFromQuaternion( quat );
  180. if ( isInverse ) tempMatrix.invert();
  181. matrix.multiply( tempMatrix );
  182. }
  183. }
  184. }
  185. obj.matrix.copy( matrix );
  186. obj.matrix.decompose( obj.position, obj.quaternion, obj.scale );
  187. return;
  188. }
  189. // Fallback: handle individual transform ops without order
  190. if ( data[ 'xformOp:translate' ] ) {
  191. const t = data[ 'xformOp:translate' ];
  192. obj.position.set( t[ 0 ], t[ 1 ], t[ 2 ] );
  193. }
  194. if ( data[ 'xformOp:translate:pivot' ] ) {
  195. const p = data[ 'xformOp:translate:pivot' ];
  196. obj.pivot = new Vector3( p[ 0 ], p[ 1 ], p[ 2 ] );
  197. }
  198. if ( data[ 'xformOp:scale' ] ) {
  199. const s = data[ 'xformOp:scale' ];
  200. if ( Array.isArray( s ) ) {
  201. obj.scale.set( s[ 0 ], s[ 1 ], s[ 2 ] );
  202. } else {
  203. obj.scale.set( s, s, s );
  204. }
  205. }
  206. if ( data[ 'xformOp:rotateXYZ' ] ) {
  207. const r = data[ 'xformOp:rotateXYZ' ];
  208. obj.rotation.set(
  209. r[ 0 ] * Math.PI / 180,
  210. r[ 1 ] * Math.PI / 180,
  211. r[ 2 ] * Math.PI / 180
  212. );
  213. }
  214. if ( data[ 'xformOp:orient' ] ) {
  215. const q = data[ 'xformOp:orient' ];
  216. if ( q.length === 4 ) {
  217. obj.quaternion.set( q[ 0 ], q[ 1 ], q[ 2 ], q[ 3 ] );
  218. }
  219. }
  220. }
  221. /**
  222. * Build indexes for efficient lookups.
  223. * Called once during compose() to avoid O(n) scans per lookup.
  224. */
  225. _buildIndexes() {
  226. // childrenByPath: parentPath -> [childName1, childName2, ...]
  227. this.childrenByPath = new Map();
  228. // attributesByPrimPath: primPath -> Map(attrName -> attrSpec)
  229. this.attributesByPrimPath = new Map();
  230. // materialsByRoot: rootPath -> [materialPath1, materialPath2, ...]
  231. this.materialsByRoot = new Map();
  232. // shadersByMaterialPath: materialPath -> [shaderPath1, shaderPath2, ...]
  233. this.shadersByMaterialPath = new Map();
  234. // geomSubsetsByMeshPath: meshPath -> [subsetPath1, subsetPath2, ...]
  235. this.geomSubsetsByMeshPath = new Map();
  236. for ( const path in this.specsByPath ) {
  237. const spec = this.specsByPath[ path ];
  238. if ( spec.specType === SpecType.Prim ) {
  239. // Build parent-child index
  240. const lastSlash = path.lastIndexOf( '/' );
  241. if ( lastSlash > 0 ) {
  242. const parentPath = path.slice( 0, lastSlash );
  243. const childName = path.slice( lastSlash + 1 );
  244. if ( ! this.childrenByPath.has( parentPath ) ) {
  245. this.childrenByPath.set( parentPath, [] );
  246. }
  247. this.childrenByPath.get( parentPath ).push( { name: childName, path: path } );
  248. } else if ( lastSlash === 0 && path.length > 1 ) {
  249. // Direct child of root
  250. const childName = path.slice( 1 );
  251. if ( ! this.childrenByPath.has( '/' ) ) {
  252. this.childrenByPath.set( '/', [] );
  253. }
  254. this.childrenByPath.get( '/' ).push( { name: childName, path: path } );
  255. }
  256. const typeName = spec.fields.typeName;
  257. // Build material index
  258. if ( typeName === 'Material' ) {
  259. const parts = path.split( '/' );
  260. const rootPath = parts.length > 1 ? '/' + parts[ 1 ] : '/';
  261. if ( ! this.materialsByRoot.has( rootPath ) ) {
  262. this.materialsByRoot.set( rootPath, [] );
  263. }
  264. this.materialsByRoot.get( rootPath ).push( path );
  265. }
  266. // Build shader index (shaders are children of materials)
  267. if ( typeName === 'Shader' && lastSlash > 0 ) {
  268. const materialPath = path.slice( 0, lastSlash );
  269. if ( ! this.shadersByMaterialPath.has( materialPath ) ) {
  270. this.shadersByMaterialPath.set( materialPath, [] );
  271. }
  272. this.shadersByMaterialPath.get( materialPath ).push( path );
  273. }
  274. // Build GeomSubset index (subsets are children of meshes)
  275. if ( typeName === 'GeomSubset' && lastSlash > 0 ) {
  276. const meshPath = path.slice( 0, lastSlash );
  277. if ( ! this.geomSubsetsByMeshPath.has( meshPath ) ) {
  278. this.geomSubsetsByMeshPath.set( meshPath, [] );
  279. }
  280. this.geomSubsetsByMeshPath.get( meshPath ).push( path );
  281. }
  282. } else if ( spec.specType === SpecType.Attribute || spec.specType === SpecType.Relationship ) {
  283. // Build attribute index
  284. const dotIndex = path.lastIndexOf( '.' );
  285. if ( dotIndex > 0 ) {
  286. const primPath = path.slice( 0, dotIndex );
  287. const attrName = path.slice( dotIndex + 1 );
  288. if ( ! this.attributesByPrimPath.has( primPath ) ) {
  289. this.attributesByPrimPath.set( primPath, new Map() );
  290. }
  291. this.attributesByPrimPath.get( primPath ).set( attrName, spec );
  292. }
  293. }
  294. }
  295. }
  296. /**
  297. * Check if a path is a direct child of parentPath.
  298. */
  299. _isDirectChild( parentPath, path, prefix ) {
  300. if ( ! path.startsWith( prefix ) ) return false;
  301. const remainder = path.slice( prefix.length );
  302. if ( remainder.length === 0 ) return false;
  303. // Check for variant paths or simple names
  304. if ( remainder.startsWith( '{' ) ) {
  305. return false; // Variant paths are not direct children
  306. }
  307. return ! remainder.includes( '/' );
  308. }
  309. /**
  310. * Build the scene hierarchy recursively.
  311. * Uses childrenByPath index for O(1) child lookup instead of O(n) iteration.
  312. */
  313. _buildHierarchy( parent, parentPath ) {
  314. // Collect children from parentPath and any active variant paths
  315. const childEntries = [];
  316. const seenPaths = new Set();
  317. // Get direct children using the index
  318. const directChildren = this.childrenByPath.get( parentPath );
  319. if ( directChildren ) {
  320. for ( const child of directChildren ) {
  321. if ( ! seenPaths.has( child.path ) ) {
  322. seenPaths.add( child.path );
  323. childEntries.push( child );
  324. }
  325. }
  326. }
  327. // Also get children from active variant paths
  328. const variantPaths = this._getVariantPaths( parentPath );
  329. for ( const vp of variantPaths ) {
  330. const variantChildren = this.childrenByPath.get( vp );
  331. if ( variantChildren ) {
  332. for ( const child of variantChildren ) {
  333. if ( ! seenPaths.has( child.path ) ) {
  334. seenPaths.add( child.path );
  335. childEntries.push( child );
  336. }
  337. }
  338. }
  339. }
  340. // Process each child
  341. for ( const { name, path } of childEntries ) {
  342. const spec = this.specsByPath[ path ];
  343. if ( ! spec || spec.specType !== SpecType.Prim ) continue;
  344. const typeName = spec.fields.typeName;
  345. // Check for references/payloads
  346. const refValue = this._getReference( spec );
  347. if ( refValue ) {
  348. // Get local variant selections from this prim
  349. const localVariants = this._getLocalVariantSelections( spec.fields );
  350. // Resolve the reference
  351. const referencedGroup = this._resolveReference( refValue, localVariants );
  352. if ( referencedGroup ) {
  353. const attrs = this._getAttributes( path );
  354. // Check if the referenced content is a single mesh (or container with single mesh)
  355. // This handles the USDZExporter pattern: Xform references geometry file
  356. const singleMesh = this._findSingleMesh( referencedGroup );
  357. if ( singleMesh && ( typeName === 'Xform' || ! typeName ) ) {
  358. // Merge the mesh into this prim
  359. singleMesh.name = name;
  360. this.applyTransform( singleMesh, spec.fields, attrs );
  361. // Apply material binding from the referencing prim if present
  362. this._applyMaterialBinding( singleMesh, path );
  363. parent.add( singleMesh );
  364. // Still build local children (overrides)
  365. this._buildHierarchy( singleMesh, path );
  366. } else {
  367. // Create a container for the referenced content
  368. const obj = new Object3D();
  369. obj.name = name;
  370. this.applyTransform( obj, spec.fields, attrs );
  371. // Add all children from the referenced group
  372. while ( referencedGroup.children.length > 0 ) {
  373. obj.add( referencedGroup.children[ 0 ] );
  374. }
  375. parent.add( obj );
  376. // Still build local children (overrides)
  377. this._buildHierarchy( obj, path );
  378. }
  379. continue;
  380. }
  381. }
  382. // Build appropriate object based on type
  383. if ( typeName === 'SkelRoot' ) {
  384. // Skeletal root - treat as transform but track for skeleton binding
  385. const obj = new Object3D();
  386. obj.name = name;
  387. obj.userData.isSkelRoot = true;
  388. const attrs = this._getAttributes( path );
  389. this.applyTransform( obj, spec.fields, attrs );
  390. parent.add( obj );
  391. this._buildHierarchy( obj, path );
  392. } else if ( typeName === 'Skeleton' ) {
  393. // Build skeleton and store it
  394. const skeleton = this._buildSkeleton( path );
  395. if ( skeleton ) {
  396. this.skeletons[ path ] = skeleton;
  397. }
  398. // Recursively build children (may contain SkelAnimation)
  399. this._buildHierarchy( parent, path );
  400. } else if ( typeName === 'SkelAnimation' ) {
  401. // Skip - animations are processed separately in _buildAnimations
  402. } else if ( typeName === 'Mesh' ) {
  403. const obj = this._buildMesh( path, spec );
  404. if ( obj ) {
  405. parent.add( obj );
  406. }
  407. } else if ( typeName === 'Material' || typeName === 'Shader' ) {
  408. // Skip materials/shaders, they're referenced by meshes
  409. } else {
  410. // Transform node, group, or unknown type
  411. const obj = new Object3D();
  412. obj.name = name;
  413. const attrs = this._getAttributes( path );
  414. this.applyTransform( obj, spec.fields, attrs );
  415. parent.add( obj );
  416. this._buildHierarchy( obj, path );
  417. }
  418. }
  419. }
  420. /**
  421. * Get variant paths for a parent path based on variant selections.
  422. */
  423. _getVariantPaths( parentPath ) {
  424. const parentSpec = this.specsByPath[ parentPath ];
  425. const variantSetChildren = parentSpec?.fields?.variantSetChildren;
  426. const variantPaths = [];
  427. if ( ! variantSetChildren || variantSetChildren.length === 0 ) {
  428. return variantPaths;
  429. }
  430. for ( const variantSetName of variantSetChildren ) {
  431. // External selections take priority
  432. let selectedVariant = this.externalVariantSelections[ variantSetName ] || null;
  433. // Fall back to file's internal selection
  434. if ( ! selectedVariant ) {
  435. const variantSelection = parentSpec.fields.variantSelection;
  436. selectedVariant = variantSelection ? variantSelection[ variantSetName ] : null;
  437. }
  438. // Fall back to first variant child
  439. if ( ! selectedVariant ) {
  440. const variantSetPath = parentPath + '/{' + variantSetName + '=}';
  441. const variantSetSpec = this.specsByPath[ variantSetPath ];
  442. if ( variantSetSpec?.fields?.variantChildren ) {
  443. selectedVariant = variantSetSpec.fields.variantChildren[ 0 ];
  444. }
  445. }
  446. if ( selectedVariant ) {
  447. const variantPath = parentPath + '/{' + variantSetName + '=' + selectedVariant + '}';
  448. variantPaths.push( variantPath );
  449. }
  450. }
  451. return variantPaths;
  452. }
  453. /**
  454. * Resolve a file path relative to basePath.
  455. */
  456. _resolveFilePath( refPath ) {
  457. let cleanPath = refPath;
  458. // Remove ./ prefix
  459. if ( cleanPath.startsWith( './' ) ) {
  460. cleanPath = cleanPath.slice( 2 );
  461. }
  462. // Combine with base path
  463. if ( this.basePath ) {
  464. return this.basePath + '/' + cleanPath;
  465. }
  466. return cleanPath;
  467. }
  468. /**
  469. * Resolve a USD reference and return the composed content.
  470. * @param {string} refValue - Reference value like "@./path/to/file.usdc@"
  471. * @param {Object} localVariants - Variant selections to apply
  472. * @returns {Group|null} Composed content or null
  473. */
  474. _resolveReference( refValue, localVariants = {} ) {
  475. if ( ! refValue ) return null;
  476. const match = refValue.match( /@([^@]+)@(?:<([^>]+)>)?/ );
  477. if ( ! match ) return null;
  478. const filePath = match[ 1 ];
  479. const primPath = match[ 2 ]; // e.g., "/Geometry"
  480. const resolvedPath = this._resolveFilePath( filePath );
  481. // Merge variant selections - external takes priority, then local
  482. const mergedVariants = { ...localVariants, ...this.externalVariantSelections };
  483. // Look up pre-parsed data in assets
  484. const referencedData = this.assets[ resolvedPath ];
  485. if ( ! referencedData ) return null;
  486. // If it's specsByPath data, compose it
  487. if ( referencedData.specsByPath ) {
  488. const composer = new USDComposer( this.manager );
  489. const newBasePath = this._getBasePath( resolvedPath );
  490. const composedGroup = composer.compose( referencedData, this.assets, mergedVariants, newBasePath );
  491. // If a primPath is specified, find and return just that subtree
  492. if ( primPath ) {
  493. const primName = primPath.split( '/' ).pop();
  494. // Find the direct child with this name (not a deep search)
  495. // This is important because there may be multiple objects with the same name
  496. let targetObject = null;
  497. for ( const child of composedGroup.children ) {
  498. if ( child.name === primName ) {
  499. targetObject = child;
  500. break;
  501. }
  502. }
  503. if ( targetObject ) {
  504. // Detach from parent for re-parenting
  505. composedGroup.remove( targetObject );
  506. // Wrap in a group to maintain consistent return type
  507. const wrapper = new Group();
  508. wrapper.add( targetObject );
  509. return wrapper;
  510. }
  511. }
  512. return composedGroup;
  513. }
  514. // If it's already a Three.js Group (legacy support), clone it
  515. if ( referencedData.isGroup || referencedData.isObject3D ) {
  516. return referencedData.clone();
  517. }
  518. return null;
  519. }
  520. /**
  521. * Find a single mesh in the group's shallow hierarchy.
  522. * Only returns a mesh if it's at depth 0 or 1, not deeply nested.
  523. * This preserves transforms in complex hierarchies like Kitchen Set
  524. * while supporting USDZExporter round-trip (Xform > Xform > Mesh pattern).
  525. */
  526. _findSingleMesh( group ) {
  527. // Check direct children first
  528. for ( const child of group.children ) {
  529. if ( child.isMesh ) {
  530. group.remove( child );
  531. return child;
  532. }
  533. }
  534. // Check grandchildren (USDZExporter pattern: Xform > Geometry > Mesh)
  535. // Only if there's exactly one child with exactly one grandchild
  536. if ( group.children.length === 1 ) {
  537. const child = group.children[ 0 ];
  538. if ( child.children && child.children.length === 1 ) {
  539. const grandchild = child.children[ 0 ];
  540. if ( grandchild.isMesh && ! this._hasNonIdentityTransform( child ) ) {
  541. // Safe to merge - intermediate has identity transform
  542. child.remove( grandchild );
  543. return grandchild;
  544. }
  545. }
  546. }
  547. return null;
  548. }
  549. /**
  550. * Check if an object has a non-identity local transform.
  551. */
  552. _hasNonIdentityTransform( obj ) {
  553. const pos = obj.position;
  554. const rot = obj.rotation;
  555. const scale = obj.scale;
  556. const hasPosition = pos.x !== 0 || pos.y !== 0 || pos.z !== 0;
  557. const hasRotation = rot.x !== 0 || rot.y !== 0 || rot.z !== 0;
  558. const hasScale = scale.x !== 1 || scale.y !== 1 || scale.z !== 1;
  559. return hasPosition || hasRotation || hasScale;
  560. }
  561. /**
  562. * Get the base path (directory) from a file path.
  563. */
  564. _getBasePath( filePath ) {
  565. const lastSlash = filePath.lastIndexOf( '/' );
  566. return lastSlash >= 0 ? filePath.slice( 0, lastSlash ) : '';
  567. }
  568. /**
  569. * Extract variant selections from a spec's fields.
  570. */
  571. _getLocalVariantSelections( fields ) {
  572. const variants = {};
  573. if ( fields.variantSelection ) {
  574. for ( const key in fields.variantSelection ) {
  575. variants[ key ] = fields.variantSelection[ key ];
  576. }
  577. }
  578. return variants;
  579. }
  580. /**
  581. * Get reference value from a prim spec.
  582. */
  583. _getReference( spec ) {
  584. if ( spec.fields.references && spec.fields.references.length > 0 ) {
  585. const ref = spec.fields.references[ 0 ];
  586. if ( typeof ref === 'string' ) return ref;
  587. if ( ref.assetPath ) return '@' + ref.assetPath + '@';
  588. }
  589. if ( spec.fields.payload ) {
  590. const payload = spec.fields.payload;
  591. if ( typeof payload === 'string' ) return payload;
  592. if ( payload.assetPath ) return '@' + payload.assetPath + '@';
  593. }
  594. return null;
  595. }
  596. /**
  597. * Get attributes for a path from attribute specs.
  598. */
  599. _getAttributes( path ) {
  600. const attrs = {};
  601. this._collectAttributesFromPath( path, attrs );
  602. // Collect overrides from sibling variants (when path is inside a variant)
  603. const variantMatch = path.match( VARIANT_PATH_REGEX );
  604. if ( variantMatch ) {
  605. const basePath = variantMatch[ 1 ];
  606. const relativePath = variantMatch[ 4 ];
  607. const variantPaths = this._getVariantPaths( basePath );
  608. for ( const vp of variantPaths ) {
  609. if ( path.startsWith( vp ) ) continue;
  610. const overridePath = vp + '/' + relativePath;
  611. this._collectAttributesFromPath( overridePath, attrs );
  612. }
  613. } else {
  614. // Check for variant overrides at ancestor levels
  615. const parts = path.split( '/' );
  616. for ( let i = 1; i < parts.length - 1; i ++ ) {
  617. const ancestorPath = parts.slice( 0, i + 1 ).join( '/' );
  618. const relativePath = parts.slice( i + 1 ).join( '/' );
  619. const variantPaths = this._getVariantPaths( ancestorPath );
  620. for ( const vp of variantPaths ) {
  621. const overridePath = vp + '/' + relativePath;
  622. this._collectAttributesFromPath( overridePath, attrs );
  623. }
  624. }
  625. }
  626. return attrs;
  627. }
  628. _collectAttributesFromPath( path, attrs ) {
  629. // Use the attribute index for O(1) lookup instead of O(n) iteration
  630. const attrMap = this.attributesByPrimPath.get( path );
  631. if ( ! attrMap ) return;
  632. for ( const [ attrName, attrSpec ] of attrMap ) {
  633. if ( attrSpec.fields?.default !== undefined ) {
  634. attrs[ attrName ] = attrSpec.fields.default;
  635. }
  636. if ( attrSpec.fields?.elementSize !== undefined ) {
  637. attrs[ attrName + ':elementSize' ] = attrSpec.fields.elementSize;
  638. }
  639. if ( attrName.startsWith( 'primvars:' ) && attrSpec.fields?.typeName !== undefined ) {
  640. attrs[ attrName + ':typeName' ] = attrSpec.fields.typeName;
  641. }
  642. }
  643. }
  644. /**
  645. * Build a mesh from a Mesh spec.
  646. */
  647. _buildMesh( path, spec ) {
  648. const attrs = this._getAttributes( path );
  649. // Check for skinning data
  650. const jointIndices = attrs[ 'primvars:skel:jointIndices' ];
  651. const jointWeights = attrs[ 'primvars:skel:jointWeights' ];
  652. const hasSkinning = jointIndices && jointWeights &&
  653. jointIndices.length > 0 && jointWeights.length > 0;
  654. // Collect GeomSubsets for multi-material support
  655. const geomSubsets = this._getGeomSubsets( path );
  656. let geometry, material;
  657. if ( geomSubsets.length > 0 ) {
  658. geometry = this._buildGeometryWithSubsets( attrs, geomSubsets, hasSkinning );
  659. const meshMaterialPath = this._getMaterialPath( path, spec.fields );
  660. material = geomSubsets.map( subset => {
  661. const matPath = subset.materialPath || meshMaterialPath;
  662. return this._buildMaterialForPath( matPath );
  663. } );
  664. } else {
  665. geometry = this._buildGeometry( path, attrs, hasSkinning );
  666. material = this._buildMaterial( path, spec.fields );
  667. }
  668. const displayColor = attrs[ 'primvars:displayColor' ];
  669. if ( displayColor && displayColor.length >= 3 ) {
  670. const applyDisplayColor = ( mat ) => {
  671. if ( mat.color && mat.color.r === 1 && mat.color.g === 1 && mat.color.b === 1 && ! mat.map ) {
  672. mat.color.setRGB( displayColor[ 0 ], displayColor[ 1 ], displayColor[ 2 ], SRGBColorSpace );
  673. }
  674. };
  675. if ( Array.isArray( material ) ) {
  676. material.forEach( applyDisplayColor );
  677. } else {
  678. applyDisplayColor( material );
  679. }
  680. }
  681. const displayOpacity = attrs[ 'primvars:displayOpacity' ];
  682. if ( displayOpacity && displayOpacity.length >= 1 ) {
  683. const opacity = displayOpacity[ 0 ];
  684. const applyDisplayOpacity = ( mat ) => {
  685. if ( opacity < 1 ) {
  686. mat.opacity = opacity;
  687. mat.transparent = true;
  688. }
  689. };
  690. if ( Array.isArray( material ) ) {
  691. material.forEach( applyDisplayOpacity );
  692. } else {
  693. applyDisplayOpacity( material );
  694. }
  695. }
  696. let mesh;
  697. if ( hasSkinning ) {
  698. mesh = new SkinnedMesh( geometry, material );
  699. // Find skeleton path from skel:skeleton relationship
  700. let skelBindingSpec = this.specsByPath[ path + '.skel:skeleton' ];
  701. if ( ! skelBindingSpec ) {
  702. skelBindingSpec = this.specsByPath[ path + '.rel skel:skeleton' ];
  703. }
  704. let skeletonPath = null;
  705. if ( skelBindingSpec ) {
  706. if ( skelBindingSpec.fields.targetPaths && skelBindingSpec.fields.targetPaths.length > 0 ) {
  707. skeletonPath = skelBindingSpec.fields.targetPaths[ 0 ];
  708. } else if ( skelBindingSpec.fields.default ) {
  709. skeletonPath = skelBindingSpec.fields.default.replace( /<|>/g, '' );
  710. }
  711. }
  712. // Get per-mesh joint mapping
  713. const localJoints = attrs[ 'skel:joints' ];
  714. this.skinnedMeshes.push( { mesh, skeletonPath, path, localJoints } );
  715. } else {
  716. mesh = new Mesh( geometry, material );
  717. }
  718. mesh.name = path.split( '/' ).pop();
  719. this.applyTransform( mesh, spec.fields, attrs );
  720. return mesh;
  721. }
  722. _getGeomSubsets( meshPath ) {
  723. const subsets = [];
  724. const subsetPaths = this.geomSubsetsByMeshPath.get( meshPath );
  725. if ( ! subsetPaths ) return subsets;
  726. for ( const p of subsetPaths ) {
  727. const attrs = this._getAttributes( p );
  728. const indices = attrs[ 'indices' ];
  729. if ( ! indices || indices.length === 0 ) continue;
  730. // Get material binding
  731. const bindingPath = p + '.material:binding';
  732. const bindingSpec = this.specsByPath[ bindingPath ];
  733. let materialPath = null;
  734. if ( bindingSpec && bindingSpec.fields.targetPaths && bindingSpec.fields.targetPaths.length > 0 ) {
  735. materialPath = bindingSpec.fields.targetPaths[ 0 ];
  736. }
  737. subsets.push( {
  738. name: p.split( '/' ).pop(),
  739. indices: indices,
  740. materialPath: materialPath
  741. } );
  742. }
  743. return subsets;
  744. }
  745. _buildGeometry( path, fields, hasSkinning = false ) {
  746. const geometry = new BufferGeometry();
  747. const points = fields[ 'points' ];
  748. if ( ! points || points.length === 0 ) return geometry;
  749. const faceVertexIndices = fields[ 'faceVertexIndices' ];
  750. const faceVertexCounts = fields[ 'faceVertexCounts' ];
  751. // Parse polygon holes (Arnold format: [holeFaceIdx, parentFaceIdx, ...])
  752. const polygonHoles = fields[ 'primvars:arnold:polygon_holes' ];
  753. const holeMap = this._buildHoleMap( polygonHoles );
  754. // Compute triangulation pattern once using actual vertex positions
  755. // This pattern will be reused for normals, UVs, etc.
  756. let indices = faceVertexIndices;
  757. let triPattern = null;
  758. if ( faceVertexCounts && faceVertexCounts.length > 0 ) {
  759. const result = this._triangulateIndicesWithPattern( faceVertexIndices, faceVertexCounts, points, holeMap );
  760. indices = result.indices;
  761. triPattern = result.pattern;
  762. }
  763. let positions = points;
  764. if ( indices && indices.length > 0 ) {
  765. positions = this._expandAttribute( points, indices, 3 );
  766. }
  767. geometry.setAttribute( 'position', new BufferAttribute( new Float32Array( positions ), 3 ) );
  768. const normals = fields[ 'normals' ] || fields[ 'primvars:normals' ];
  769. const normalIndicesRaw = fields[ 'normals:indices' ] || fields[ 'primvars:normals:indices' ];
  770. if ( normals && normals.length > 0 ) {
  771. let normalData = normals;
  772. if ( normalIndicesRaw && normalIndicesRaw.length > 0 && triPattern ) {
  773. // Indexed normals - apply triangulation pattern to indices
  774. const triangulatedNormalIndices = this._applyTriangulationPattern( normalIndicesRaw, triPattern );
  775. normalData = this._expandAttribute( normals, triangulatedNormalIndices, 3 );
  776. } else if ( normals.length === points.length ) {
  777. // Per-vertex normals
  778. if ( indices && indices.length > 0 ) {
  779. normalData = this._expandAttribute( normals, indices, 3 );
  780. }
  781. } else if ( triPattern ) {
  782. // Per-face-vertex normals (no separate indices) - use same triangulation pattern
  783. const normalIndices = this._applyTriangulationPattern(
  784. Array.from( { length: normals.length / 3 }, ( _, i ) => i ),
  785. triPattern
  786. );
  787. normalData = this._expandAttribute( normals, normalIndices, 3 );
  788. }
  789. geometry.setAttribute( 'normal', new BufferAttribute( new Float32Array( normalData ), 3 ) );
  790. } else {
  791. geometry.computeVertexNormals();
  792. }
  793. const { uvs, uvIndices } = this._findUVPrimvar( fields );
  794. const numFaceVertices = faceVertexIndices ? faceVertexIndices.length : 0;
  795. if ( uvs && uvs.length > 0 ) {
  796. let uvData = uvs;
  797. if ( uvIndices && uvIndices.length > 0 && triPattern ) {
  798. const triangulatedUvIndices = this._applyTriangulationPattern( uvIndices, triPattern );
  799. uvData = this._expandAttribute( uvs, triangulatedUvIndices, 2 );
  800. } else if ( indices && uvs.length / 2 === points.length / 3 ) {
  801. uvData = this._expandAttribute( uvs, indices, 2 );
  802. } else if ( triPattern && uvs.length / 2 === numFaceVertices ) {
  803. // Per-face-vertex UVs (faceVarying, no separate indices)
  804. const uvIndicesFromPattern = this._applyTriangulationPattern(
  805. Array.from( { length: numFaceVertices }, ( _, i ) => i ),
  806. triPattern
  807. );
  808. uvData = this._expandAttribute( uvs, uvIndicesFromPattern, 2 );
  809. }
  810. geometry.setAttribute( 'uv', new BufferAttribute( new Float32Array( uvData ), 2 ) );
  811. }
  812. // Second UV set (st1) for lightmaps/AO
  813. const { uvs2, uv2Indices } = this._findUV2Primvar( fields );
  814. if ( uvs2 && uvs2.length > 0 ) {
  815. let uv2Data = uvs2;
  816. if ( uv2Indices && uv2Indices.length > 0 && triPattern ) {
  817. const triangulatedUv2Indices = this._applyTriangulationPattern( uv2Indices, triPattern );
  818. uv2Data = this._expandAttribute( uvs2, triangulatedUv2Indices, 2 );
  819. } else if ( indices && uvs2.length / 2 === points.length / 3 ) {
  820. uv2Data = this._expandAttribute( uvs2, indices, 2 );
  821. } else if ( triPattern && uvs2.length / 2 === numFaceVertices ) {
  822. // Per-face-vertex UV2 (faceVarying, no separate indices)
  823. const uv2IndicesFromPattern = this._applyTriangulationPattern(
  824. Array.from( { length: numFaceVertices }, ( _, i ) => i ),
  825. triPattern
  826. );
  827. uv2Data = this._expandAttribute( uvs2, uv2IndicesFromPattern, 2 );
  828. }
  829. geometry.setAttribute( 'uv1', new BufferAttribute( new Float32Array( uv2Data ), 2 ) );
  830. }
  831. // Add skinning attributes
  832. if ( hasSkinning ) {
  833. const jointIndices = fields[ 'primvars:skel:jointIndices' ];
  834. const jointWeights = fields[ 'primvars:skel:jointWeights' ];
  835. const elementSize = fields[ 'primvars:skel:jointIndices:elementSize' ] || 4;
  836. if ( jointIndices && jointWeights ) {
  837. const numVertices = positions.length / 3;
  838. let skinIndexData, skinWeightData;
  839. if ( indices && indices.length > 0 ) {
  840. skinIndexData = this._expandAttribute( jointIndices, indices, elementSize );
  841. skinWeightData = this._expandAttribute( jointWeights, indices, elementSize );
  842. } else {
  843. skinIndexData = jointIndices;
  844. skinWeightData = jointWeights;
  845. }
  846. const skinIndices = new Uint16Array( numVertices * 4 );
  847. const skinWeights = new Float32Array( numVertices * 4 );
  848. for ( let i = 0; i < numVertices; i ++ ) {
  849. for ( let j = 0; j < 4; j ++ ) {
  850. if ( j < elementSize ) {
  851. skinIndices[ i * 4 + j ] = skinIndexData[ i * elementSize + j ] || 0;
  852. skinWeights[ i * 4 + j ] = skinWeightData[ i * elementSize + j ] || 0;
  853. } else {
  854. skinIndices[ i * 4 + j ] = 0;
  855. skinWeights[ i * 4 + j ] = 0;
  856. }
  857. }
  858. }
  859. geometry.setAttribute( 'skinIndex', new BufferAttribute( skinIndices, 4 ) );
  860. geometry.setAttribute( 'skinWeight', new BufferAttribute( skinWeights, 4 ) );
  861. }
  862. }
  863. return geometry;
  864. }
  865. _buildGeometryWithSubsets( fields, geomSubsets, hasSkinning = false ) {
  866. const geometry = new BufferGeometry();
  867. const points = fields[ 'points' ];
  868. if ( ! points || points.length === 0 ) return geometry;
  869. const faceVertexIndices = fields[ 'faceVertexIndices' ];
  870. const faceVertexCounts = fields[ 'faceVertexCounts' ];
  871. if ( ! faceVertexCounts || faceVertexCounts.length === 0 ) return geometry;
  872. const polygonHoles = fields[ 'primvars:arnold:polygon_holes' ];
  873. const holeMap = this._buildHoleMap( polygonHoles );
  874. const holeFaces = holeMap.holeFaces;
  875. const parentToHoles = holeMap.parentToHoles;
  876. const { uvs, uvIndices } = this._findUVPrimvar( fields );
  877. const { uvs2, uv2Indices } = this._findUV2Primvar( fields );
  878. const normals = fields[ 'normals' ] || fields[ 'primvars:normals' ];
  879. const normalIndicesRaw = fields[ 'normals:indices' ] || fields[ 'primvars:normals:indices' ];
  880. const jointIndices = hasSkinning ? fields[ 'primvars:skel:jointIndices' ] : null;
  881. const jointWeights = hasSkinning ? fields[ 'primvars:skel:jointWeights' ] : null;
  882. const elementSize = fields[ 'primvars:skel:jointIndices:elementSize' ] || 4;
  883. // Build face-to-triangle mapping (accounting for holes)
  884. const faceTriangleOffset = [];
  885. let triangleCount = 0;
  886. for ( let i = 0; i < faceVertexCounts.length; i ++ ) {
  887. faceTriangleOffset.push( triangleCount );
  888. // Skip hole faces - they're triangulated with their parent
  889. if ( holeFaces.has( i ) ) continue;
  890. const count = faceVertexCounts[ i ];
  891. const holes = parentToHoles.get( i );
  892. if ( holes && holes.length > 0 ) {
  893. // For faces with holes, count triangles based on total vertices
  894. // Earcut produces (total_vertices - 2) triangles for any polygon including holes
  895. let totalVerts = count;
  896. for ( const holeIdx of holes ) {
  897. totalVerts += faceVertexCounts[ holeIdx ];
  898. }
  899. triangleCount += totalVerts - 2;
  900. } else if ( count >= 3 ) {
  901. triangleCount += count - 2;
  902. }
  903. }
  904. const triangleToSubset = new Int32Array( triangleCount ).fill( - 1 );
  905. for ( let si = 0; si < geomSubsets.length; si ++ ) {
  906. const subset = geomSubsets[ si ];
  907. for ( let i = 0; i < subset.indices.length; i ++ ) {
  908. const faceIdx = subset.indices[ i ];
  909. if ( faceIdx >= faceVertexCounts.length ) continue;
  910. const triStart = faceTriangleOffset[ faceIdx ];
  911. const triCount = faceVertexCounts[ faceIdx ] - 2;
  912. for ( let t = 0; t < triCount; t ++ ) {
  913. triangleToSubset[ triStart + t ] = si;
  914. }
  915. }
  916. }
  917. // Sort triangles by subset
  918. const sortedTriangles = [];
  919. for ( let tri = 0; tri < triangleCount; tri ++ ) {
  920. sortedTriangles.push( { original: tri, subset: triangleToSubset[ tri ] } );
  921. }
  922. sortedTriangles.sort( ( a, b ) => a.subset - b.subset );
  923. const groups = [];
  924. let currentSubset = sortedTriangles.length > 0 ? sortedTriangles[ 0 ].subset : - 1;
  925. let groupStart = 0;
  926. for ( let i = 0; i < sortedTriangles.length; i ++ ) {
  927. if ( sortedTriangles[ i ].subset !== currentSubset ) {
  928. if ( currentSubset >= 0 ) {
  929. groups.push( {
  930. start: groupStart * 3,
  931. count: ( i - groupStart ) * 3,
  932. materialIndex: currentSubset
  933. } );
  934. }
  935. currentSubset = sortedTriangles[ i ].subset;
  936. groupStart = i;
  937. }
  938. }
  939. if ( currentSubset >= 0 && sortedTriangles.length > groupStart ) {
  940. groups.push( {
  941. start: groupStart * 3,
  942. count: ( sortedTriangles.length - groupStart ) * 3,
  943. materialIndex: currentSubset
  944. } );
  945. }
  946. for ( const group of groups ) {
  947. geometry.addGroup( group.start, group.count, group.materialIndex );
  948. }
  949. // Triangulate original data using consistent pattern
  950. const { indices: origIndices, pattern: triPattern } = this._triangulateIndicesWithPattern( faceVertexIndices, faceVertexCounts, points, holeMap );
  951. const origUvIndices = uvIndices ? this._applyTriangulationPattern( uvIndices, triPattern ) : null;
  952. const origUv2Indices = uv2Indices ? this._applyTriangulationPattern( uv2Indices, triPattern ) : null;
  953. const numFaceVertices = faceVertexCounts.reduce( ( a, b ) => a + b, 0 );
  954. const hasIndexedNormals = normals && normalIndicesRaw && normalIndicesRaw.length > 0;
  955. const hasFaceVaryingNormals = normals && normals.length / 3 === numFaceVertices;
  956. const origNormalIndices = hasIndexedNormals
  957. ? this._applyTriangulationPattern( normalIndicesRaw, triPattern )
  958. : ( hasFaceVaryingNormals
  959. ? this._applyTriangulationPattern( Array.from( { length: numFaceVertices }, ( _, i ) => i ), triPattern )
  960. : null );
  961. // Build reordered vertex data
  962. const vertexCount = triangleCount * 3;
  963. const positions = new Float32Array( vertexCount * 3 );
  964. const uvData = uvs ? new Float32Array( vertexCount * 2 ) : null;
  965. const uv1Data = uvs2 ? new Float32Array( vertexCount * 2 ) : null;
  966. const normalData = normals ? new Float32Array( vertexCount * 3 ) : null;
  967. const skinIndexData = jointIndices ? new Uint16Array( vertexCount * 4 ) : null;
  968. const skinWeightData = jointWeights ? new Float32Array( vertexCount * 4 ) : null;
  969. for ( let i = 0; i < sortedTriangles.length; i ++ ) {
  970. const origTri = sortedTriangles[ i ].original;
  971. for ( let v = 0; v < 3; v ++ ) {
  972. const origIdx = origTri * 3 + v;
  973. const newIdx = i * 3 + v;
  974. const pointIdx = origIndices[ origIdx ];
  975. positions[ newIdx * 3 ] = points[ pointIdx * 3 ];
  976. positions[ newIdx * 3 + 1 ] = points[ pointIdx * 3 + 1 ];
  977. positions[ newIdx * 3 + 2 ] = points[ pointIdx * 3 + 2 ];
  978. if ( uvData && uvs ) {
  979. if ( origUvIndices ) {
  980. const uvIdx = origUvIndices[ origIdx ];
  981. uvData[ newIdx * 2 ] = uvs[ uvIdx * 2 ];
  982. uvData[ newIdx * 2 + 1 ] = uvs[ uvIdx * 2 + 1 ];
  983. } else if ( uvs.length / 2 === points.length / 3 ) {
  984. uvData[ newIdx * 2 ] = uvs[ pointIdx * 2 ];
  985. uvData[ newIdx * 2 + 1 ] = uvs[ pointIdx * 2 + 1 ];
  986. }
  987. }
  988. if ( uv1Data && uvs2 ) {
  989. if ( origUv2Indices ) {
  990. const uv2Idx = origUv2Indices[ origIdx ];
  991. uv1Data[ newIdx * 2 ] = uvs2[ uv2Idx * 2 ];
  992. uv1Data[ newIdx * 2 + 1 ] = uvs2[ uv2Idx * 2 + 1 ];
  993. } else if ( uvs2.length / 2 === points.length / 3 ) {
  994. uv1Data[ newIdx * 2 ] = uvs2[ pointIdx * 2 ];
  995. uv1Data[ newIdx * 2 + 1 ] = uvs2[ pointIdx * 2 + 1 ];
  996. }
  997. }
  998. if ( normalData && normals ) {
  999. if ( origNormalIndices ) {
  1000. const normalIdx = origNormalIndices[ origIdx ];
  1001. normalData[ newIdx * 3 ] = normals[ normalIdx * 3 ];
  1002. normalData[ newIdx * 3 + 1 ] = normals[ normalIdx * 3 + 1 ];
  1003. normalData[ newIdx * 3 + 2 ] = normals[ normalIdx * 3 + 2 ];
  1004. } else if ( normals.length === points.length ) {
  1005. normalData[ newIdx * 3 ] = normals[ pointIdx * 3 ];
  1006. normalData[ newIdx * 3 + 1 ] = normals[ pointIdx * 3 + 1 ];
  1007. normalData[ newIdx * 3 + 2 ] = normals[ pointIdx * 3 + 2 ];
  1008. }
  1009. }
  1010. if ( skinIndexData && skinWeightData && jointIndices && jointWeights ) {
  1011. for ( let j = 0; j < 4; j ++ ) {
  1012. if ( j < elementSize ) {
  1013. skinIndexData[ newIdx * 4 + j ] = jointIndices[ pointIdx * elementSize + j ] || 0;
  1014. skinWeightData[ newIdx * 4 + j ] = jointWeights[ pointIdx * elementSize + j ] || 0;
  1015. } else {
  1016. skinIndexData[ newIdx * 4 + j ] = 0;
  1017. skinWeightData[ newIdx * 4 + j ] = 0;
  1018. }
  1019. }
  1020. }
  1021. }
  1022. }
  1023. geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
  1024. if ( uvData ) {
  1025. geometry.setAttribute( 'uv', new BufferAttribute( uvData, 2 ) );
  1026. }
  1027. if ( uv1Data ) {
  1028. geometry.setAttribute( 'uv1', new BufferAttribute( uv1Data, 2 ) );
  1029. }
  1030. if ( normalData ) {
  1031. geometry.setAttribute( 'normal', new BufferAttribute( normalData, 3 ) );
  1032. } else {
  1033. geometry.computeVertexNormals();
  1034. }
  1035. if ( skinIndexData ) {
  1036. geometry.setAttribute( 'skinIndex', new BufferAttribute( skinIndexData, 4 ) );
  1037. }
  1038. if ( skinWeightData ) {
  1039. geometry.setAttribute( 'skinWeight', new BufferAttribute( skinWeightData, 4 ) );
  1040. }
  1041. return geometry;
  1042. }
  1043. _findUVPrimvar( fields ) {
  1044. for ( const key in fields ) {
  1045. if ( ! key.startsWith( 'primvars:' ) ) continue;
  1046. if ( key.endsWith( ':typeName' ) || key.endsWith( ':elementSize' ) || key.endsWith( ':indices' ) ) continue;
  1047. if ( key.includes( 'skel:' ) ) continue;
  1048. const typeName = fields[ key + ':typeName' ];
  1049. if ( typeName && typeName.includes( 'texCoord' ) ) {
  1050. return {
  1051. uvs: fields[ key ],
  1052. uvIndices: fields[ key + ':indices' ]
  1053. };
  1054. }
  1055. }
  1056. const uvs = fields[ 'primvars:st' ] || fields[ 'primvars:UVMap' ];
  1057. const uvIndices = fields[ 'primvars:st:indices' ];
  1058. return { uvs, uvIndices };
  1059. }
  1060. _findUV2Primvar( fields ) {
  1061. const uvs2 = fields[ 'primvars:st1' ];
  1062. const uv2Indices = fields[ 'primvars:st1:indices' ];
  1063. return { uvs2, uv2Indices };
  1064. }
  1065. _buildHoleMap( polygonHoles ) {
  1066. // polygonHoles is in Arnold format: [holeFaceIdx, parentFaceIdx, holeFaceIdx, parentFaceIdx, ...]
  1067. // Returns a map: parentFaceIdx -> [holeFaceIdx1, holeFaceIdx2, ...]
  1068. // Also returns a set of hole face indices to skip during triangulation
  1069. if ( ! polygonHoles || polygonHoles.length === 0 ) {
  1070. return { parentToHoles: new Map(), holeFaces: new Set() };
  1071. }
  1072. const parentToHoles = new Map();
  1073. const holeFaces = new Set();
  1074. for ( let i = 0; i < polygonHoles.length; i += 2 ) {
  1075. const holeFaceIdx = polygonHoles[ i ];
  1076. const parentFaceIdx = polygonHoles[ i + 1 ];
  1077. holeFaces.add( holeFaceIdx );
  1078. if ( ! parentToHoles.has( parentFaceIdx ) ) {
  1079. parentToHoles.set( parentFaceIdx, [] );
  1080. }
  1081. parentToHoles.get( parentFaceIdx ).push( holeFaceIdx );
  1082. }
  1083. return { parentToHoles, holeFaces };
  1084. }
  1085. _triangulateIndicesWithPattern( indices, counts, points = null, holeMap = null ) {
  1086. const triangulated = [];
  1087. const pattern = []; // Stores face-local indices for each triangle vertex
  1088. // Build face offset lookup for accessing hole face data
  1089. const faceOffsets = [];
  1090. let offsetAccum = 0;
  1091. for ( let i = 0; i < counts.length; i ++ ) {
  1092. faceOffsets.push( offsetAccum );
  1093. offsetAccum += counts[ i ];
  1094. }
  1095. const parentToHoles = holeMap?.parentToHoles || new Map();
  1096. const holeFaces = holeMap?.holeFaces || new Set();
  1097. let offset = 0;
  1098. for ( let i = 0; i < counts.length; i ++ ) {
  1099. const count = counts[ i ];
  1100. // Skip faces that are holes - they will be triangulated with their parent
  1101. if ( holeFaces.has( i ) ) {
  1102. offset += count;
  1103. continue;
  1104. }
  1105. // Check if this face has holes
  1106. const holes = parentToHoles.get( i );
  1107. if ( holes && holes.length > 0 && points && points.length > 0 ) {
  1108. // Triangulate face with holes using vertex -> face-vertex mapping
  1109. const vertexToFaceVertex = new Map();
  1110. const faceIndices = [];
  1111. for ( let j = 0; j < count; j ++ ) {
  1112. const vertIdx = indices[ offset + j ];
  1113. faceIndices.push( vertIdx );
  1114. vertexToFaceVertex.set( vertIdx, offset + j );
  1115. }
  1116. const holeContours = [];
  1117. for ( const holeFaceIdx of holes ) {
  1118. const holeOffset = faceOffsets[ holeFaceIdx ];
  1119. const holeCount = counts[ holeFaceIdx ];
  1120. const holeIndices = [];
  1121. for ( let j = 0; j < holeCount; j ++ ) {
  1122. const vertIdx = indices[ holeOffset + j ];
  1123. holeIndices.push( vertIdx );
  1124. vertexToFaceVertex.set( vertIdx, holeOffset + j );
  1125. }
  1126. holeContours.push( holeIndices );
  1127. }
  1128. const triangles = this._triangulateNGonWithHoles( faceIndices, holeContours, points );
  1129. for ( const tri of triangles ) {
  1130. triangulated.push( tri[ 0 ], tri[ 1 ], tri[ 2 ] );
  1131. pattern.push(
  1132. vertexToFaceVertex.get( tri[ 0 ] ),
  1133. vertexToFaceVertex.get( tri[ 1 ] ),
  1134. vertexToFaceVertex.get( tri[ 2 ] )
  1135. );
  1136. }
  1137. } else if ( count === 3 ) {
  1138. triangulated.push(
  1139. indices[ offset ],
  1140. indices[ offset + 1 ],
  1141. indices[ offset + 2 ]
  1142. );
  1143. pattern.push( offset, offset + 1, offset + 2 );
  1144. } else if ( count === 4 ) {
  1145. triangulated.push(
  1146. indices[ offset ],
  1147. indices[ offset + 1 ],
  1148. indices[ offset + 2 ],
  1149. indices[ offset ],
  1150. indices[ offset + 2 ],
  1151. indices[ offset + 3 ]
  1152. );
  1153. pattern.push(
  1154. offset, offset + 1, offset + 2,
  1155. offset, offset + 2, offset + 3
  1156. );
  1157. } else if ( count > 4 ) {
  1158. // Use ear-clipping for complex n-gons if we have vertex positions
  1159. if ( points && points.length > 0 ) {
  1160. const faceIndices = [];
  1161. for ( let j = 0; j < count; j ++ ) {
  1162. faceIndices.push( indices[ offset + j ] );
  1163. }
  1164. const triangles = this._triangulateNGon( faceIndices, points );
  1165. for ( const tri of triangles ) {
  1166. triangulated.push( tri[ 0 ], tri[ 1 ], tri[ 2 ] );
  1167. // Find local indices within the face
  1168. pattern.push(
  1169. offset + faceIndices.indexOf( tri[ 0 ] ),
  1170. offset + faceIndices.indexOf( tri[ 1 ] ),
  1171. offset + faceIndices.indexOf( tri[ 2 ] )
  1172. );
  1173. }
  1174. } else {
  1175. // Fallback to fan triangulation
  1176. for ( let j = 1; j < count - 1; j ++ ) {
  1177. triangulated.push(
  1178. indices[ offset ],
  1179. indices[ offset + j ],
  1180. indices[ offset + j + 1 ]
  1181. );
  1182. pattern.push( offset, offset + j, offset + j + 1 );
  1183. }
  1184. }
  1185. }
  1186. offset += count;
  1187. }
  1188. return { indices: triangulated, pattern };
  1189. }
  1190. _applyTriangulationPattern( indices, pattern ) {
  1191. const result = [];
  1192. for ( let i = 0; i < pattern.length; i ++ ) {
  1193. result.push( indices[ pattern[ i ] ] );
  1194. }
  1195. return result;
  1196. }
  1197. _triangulateNGon( faceIndices, points ) {
  1198. // Project 3D polygon to 2D for triangulation using Newell's method for normal
  1199. const contour2D = [];
  1200. const contour3D = [];
  1201. for ( const idx of faceIndices ) {
  1202. contour3D.push( new Vector3(
  1203. points[ idx * 3 ],
  1204. points[ idx * 3 + 1 ],
  1205. points[ idx * 3 + 2 ]
  1206. ) );
  1207. }
  1208. // Calculate polygon normal using Newell's method
  1209. const normal = new Vector3();
  1210. for ( let i = 0; i < contour3D.length; i ++ ) {
  1211. const curr = contour3D[ i ];
  1212. const next = contour3D[ ( i + 1 ) % contour3D.length ];
  1213. normal.x += ( curr.y - next.y ) * ( curr.z + next.z );
  1214. normal.y += ( curr.z - next.z ) * ( curr.x + next.x );
  1215. normal.z += ( curr.x - next.x ) * ( curr.y + next.y );
  1216. }
  1217. normal.normalize();
  1218. // Create tangent basis for projection
  1219. const tangent = new Vector3();
  1220. const bitangent = new Vector3();
  1221. if ( Math.abs( normal.y ) > 0.9 ) {
  1222. tangent.set( 1, 0, 0 );
  1223. } else {
  1224. tangent.set( 0, 1, 0 );
  1225. }
  1226. bitangent.crossVectors( normal, tangent ).normalize();
  1227. tangent.crossVectors( bitangent, normal ).normalize();
  1228. // Project to 2D
  1229. for ( const p of contour3D ) {
  1230. contour2D.push( new Vector2( p.dot( tangent ), p.dot( bitangent ) ) );
  1231. }
  1232. // Triangulate using ShapeUtils
  1233. const triangles = ShapeUtils.triangulateShape( contour2D, [] );
  1234. // Map back to original indices
  1235. const result = [];
  1236. for ( const tri of triangles ) {
  1237. result.push( [
  1238. faceIndices[ tri[ 0 ] ],
  1239. faceIndices[ tri[ 1 ] ],
  1240. faceIndices[ tri[ 2 ] ]
  1241. ] );
  1242. }
  1243. return result;
  1244. }
  1245. _triangulateNGonWithHoles( outerIndices, holeContours, points ) {
  1246. // Project 3D polygon with holes to 2D for triangulation
  1247. const outer3D = [];
  1248. for ( const idx of outerIndices ) {
  1249. outer3D.push( new Vector3(
  1250. points[ idx * 3 ],
  1251. points[ idx * 3 + 1 ],
  1252. points[ idx * 3 + 2 ]
  1253. ) );
  1254. }
  1255. // Calculate polygon normal using Newell's method
  1256. const normal = new Vector3();
  1257. for ( let i = 0; i < outer3D.length; i ++ ) {
  1258. const curr = outer3D[ i ];
  1259. const next = outer3D[ ( i + 1 ) % outer3D.length ];
  1260. normal.x += ( curr.y - next.y ) * ( curr.z + next.z );
  1261. normal.y += ( curr.z - next.z ) * ( curr.x + next.x );
  1262. normal.z += ( curr.x - next.x ) * ( curr.y + next.y );
  1263. }
  1264. normal.normalize();
  1265. // Create tangent basis for projection
  1266. const tangent = new Vector3();
  1267. const bitangent = new Vector3();
  1268. if ( Math.abs( normal.y ) > 0.9 ) {
  1269. tangent.set( 1, 0, 0 );
  1270. } else {
  1271. tangent.set( 0, 1, 0 );
  1272. }
  1273. bitangent.crossVectors( normal, tangent ).normalize();
  1274. tangent.crossVectors( bitangent, normal ).normalize();
  1275. // Project outer contour to 2D
  1276. const outer2D = [];
  1277. for ( const p of outer3D ) {
  1278. outer2D.push( new Vector2( p.dot( tangent ), p.dot( bitangent ) ) );
  1279. }
  1280. // Project hole contours to 2D
  1281. const holes2D = [];
  1282. for ( const holeIndices of holeContours ) {
  1283. const hole2D = [];
  1284. for ( const idx of holeIndices ) {
  1285. const p = new Vector3(
  1286. points[ idx * 3 ],
  1287. points[ idx * 3 + 1 ],
  1288. points[ idx * 3 + 2 ]
  1289. );
  1290. hole2D.push( new Vector2( p.dot( tangent ), p.dot( bitangent ) ) );
  1291. }
  1292. holes2D.push( hole2D );
  1293. }
  1294. // Build combined index array: outer contour followed by all holes
  1295. const allIndices = [ ...outerIndices ];
  1296. for ( const holeIndices of holeContours ) {
  1297. allIndices.push( ...holeIndices );
  1298. }
  1299. // Triangulate using ShapeUtils with holes
  1300. const triangles = ShapeUtils.triangulateShape( outer2D, holes2D );
  1301. // Map back to original vertex indices
  1302. const result = [];
  1303. for ( const tri of triangles ) {
  1304. result.push( [
  1305. allIndices[ tri[ 0 ] ],
  1306. allIndices[ tri[ 1 ] ],
  1307. allIndices[ tri[ 2 ] ]
  1308. ] );
  1309. }
  1310. return result;
  1311. }
  1312. _triangulateIndices( indices, counts ) {
  1313. const triangulated = [];
  1314. let offset = 0;
  1315. for ( let i = 0; i < counts.length; i ++ ) {
  1316. const count = counts[ i ];
  1317. if ( count === 3 ) {
  1318. triangulated.push(
  1319. indices[ offset ],
  1320. indices[ offset + 1 ],
  1321. indices[ offset + 2 ]
  1322. );
  1323. } else if ( count === 4 ) {
  1324. triangulated.push(
  1325. indices[ offset ],
  1326. indices[ offset + 1 ],
  1327. indices[ offset + 2 ],
  1328. indices[ offset ],
  1329. indices[ offset + 2 ],
  1330. indices[ offset + 3 ]
  1331. );
  1332. } else if ( count > 4 ) {
  1333. // Fan triangulation for n-gons
  1334. for ( let j = 1; j < count - 1; j ++ ) {
  1335. triangulated.push(
  1336. indices[ offset ],
  1337. indices[ offset + j ],
  1338. indices[ offset + j + 1 ]
  1339. );
  1340. }
  1341. }
  1342. offset += count;
  1343. }
  1344. return triangulated;
  1345. }
  1346. _expandAttribute( data, indices, itemSize ) {
  1347. const expanded = new Array( indices.length * itemSize );
  1348. for ( let i = 0; i < indices.length; i ++ ) {
  1349. const srcIdx = indices[ i ];
  1350. for ( let j = 0; j < itemSize; j ++ ) {
  1351. expanded[ i * itemSize + j ] = data[ srcIdx * itemSize + j ];
  1352. }
  1353. }
  1354. return expanded;
  1355. }
  1356. /**
  1357. * Get the material path for a mesh, checking various binding sources.
  1358. */
  1359. _getMaterialPath( meshPath, fields ) {
  1360. let materialPath = null;
  1361. let materialBinding = fields[ 'material:binding' ];
  1362. if ( ! materialBinding ) {
  1363. const bindingPath = meshPath + '.material:binding';
  1364. const bindingSpec = this.specsByPath[ bindingPath ];
  1365. if ( bindingSpec && bindingSpec.specType === SpecType.Relationship ) {
  1366. materialBinding = bindingSpec.fields.targetPaths || bindingSpec.fields.default;
  1367. }
  1368. }
  1369. if ( materialBinding ) {
  1370. materialPath = Array.isArray( materialBinding ) ? materialBinding[ 0 ] : materialBinding;
  1371. }
  1372. return materialPath;
  1373. }
  1374. _buildMaterial( meshPath, fields ) {
  1375. const material = new MeshPhysicalMaterial();
  1376. let materialPath = null;
  1377. let materialBinding = fields[ 'material:binding' ];
  1378. if ( ! materialBinding ) {
  1379. const bindingPath = meshPath + '.material:binding';
  1380. const bindingSpec = this.specsByPath[ bindingPath ];
  1381. if ( bindingSpec && bindingSpec.specType === SpecType.Relationship ) {
  1382. materialBinding = bindingSpec.fields.targetPaths || bindingSpec.fields.default;
  1383. }
  1384. }
  1385. if ( materialBinding ) {
  1386. materialPath = Array.isArray( materialBinding ) ? materialBinding[ 0 ] : materialBinding;
  1387. }
  1388. if ( ! materialPath ) {
  1389. const materialPaths = [];
  1390. const prefix = meshPath + '/';
  1391. for ( const path in this.specsByPath ) {
  1392. if ( ! path.startsWith( prefix ) ) continue;
  1393. if ( ! path.endsWith( '.material:binding' ) ) continue;
  1394. const bindingSpec = this.specsByPath[ path ];
  1395. if ( ! bindingSpec ) continue;
  1396. const targetPaths = bindingSpec.fields.targetPaths;
  1397. if ( targetPaths && targetPaths.length > 0 ) {
  1398. materialPaths.push( targetPaths[ 0 ] );
  1399. }
  1400. }
  1401. if ( materialPaths.length > 0 ) {
  1402. materialPath = this._pickBestMaterial( materialPaths );
  1403. }
  1404. }
  1405. if ( ! materialPath ) {
  1406. // Use material index for O(1) lookup instead of O(n) iteration
  1407. const meshParts = meshPath.split( '/' );
  1408. const rootPath = '/' + meshParts[ 1 ];
  1409. const materialsInRoot = this.materialsByRoot.get( rootPath );
  1410. if ( materialsInRoot ) {
  1411. for ( const path of materialsInRoot ) {
  1412. if ( path.startsWith( rootPath + '/Looks/' ) ||
  1413. path.startsWith( rootPath + '/Materials/' ) ) {
  1414. materialPath = path;
  1415. break;
  1416. }
  1417. }
  1418. }
  1419. }
  1420. if ( materialPath ) {
  1421. this._applyMaterial( material, materialPath );
  1422. }
  1423. return material;
  1424. }
  1425. _buildMaterialForPath( materialPath ) {
  1426. const material = new MeshPhysicalMaterial();
  1427. if ( materialPath ) {
  1428. this._applyMaterial( material, materialPath );
  1429. }
  1430. return material;
  1431. }
  1432. /**
  1433. * Apply material binding from a prim path to a mesh.
  1434. * Used when merging referenced geometry into a prim that has material binding.
  1435. */
  1436. _applyMaterialBinding( mesh, primPath ) {
  1437. // Look for material:binding on this prim
  1438. const bindingPath = primPath + '.material:binding';
  1439. const bindingSpec = this.specsByPath[ bindingPath ];
  1440. if ( ! bindingSpec ) return;
  1441. let materialPath = null;
  1442. const targetPaths = bindingSpec.fields?.targetPaths || bindingSpec.fields?.default;
  1443. if ( targetPaths ) {
  1444. materialPath = Array.isArray( targetPaths ) ? targetPaths[ 0 ] : targetPaths;
  1445. }
  1446. if ( ! materialPath ) return;
  1447. // Clean the material path
  1448. materialPath = String( materialPath ).replace( /^<|>$/g, '' );
  1449. // Build and apply the material
  1450. const material = new MeshPhysicalMaterial();
  1451. this._applyMaterial( material, materialPath );
  1452. mesh.material = material;
  1453. }
  1454. _pickBestMaterial( materialPaths ) {
  1455. for ( const materialPath of materialPaths ) {
  1456. const shaderPaths = this.shadersByMaterialPath.get( materialPath );
  1457. if ( ! shaderPaths ) continue;
  1458. for ( const path of shaderPaths ) {
  1459. const attrs = this._getAttributes( path );
  1460. if ( attrs[ 'info:id' ] === 'UsdUVTexture' && attrs[ 'inputs:file' ] ) {
  1461. return materialPath;
  1462. }
  1463. }
  1464. }
  1465. return materialPaths[ 0 ];
  1466. }
  1467. _applyMaterial( material, materialPath ) {
  1468. const materialSpec = this.specsByPath[ materialPath ];
  1469. if ( ! materialSpec ) return;
  1470. const shaderPaths = this.shadersByMaterialPath.get( materialPath );
  1471. if ( ! shaderPaths ) return;
  1472. for ( const path of shaderPaths ) {
  1473. const spec = this.specsByPath[ path ];
  1474. if ( ! spec ) continue;
  1475. const shaderAttrs = this._getAttributes( path );
  1476. const infoId = shaderAttrs[ 'info:id' ] || spec.fields[ 'info:id' ];
  1477. if ( infoId === 'UsdPreviewSurface' ) {
  1478. this._applyPreviewSurface( material, path );
  1479. } else if ( infoId === 'arnold:openpbr_surface' ) {
  1480. this._applyOpenPBRSurface( material, path );
  1481. }
  1482. }
  1483. }
  1484. /**
  1485. * Shared helper for applying texture or value from shader attribute.
  1486. * Reduces duplication between _applyPreviewSurface and _applyOpenPBRSurface.
  1487. */
  1488. _applyTextureOrValue( material, shaderPath, fields, attrName, textureProperty, colorSpace, valueCallback, textureGetter ) {
  1489. const attrPath = shaderPath + '.' + attrName;
  1490. const spec = this.specsByPath[ attrPath ];
  1491. if ( spec && spec.fields.connectionPaths && spec.fields.connectionPaths.length > 0 ) {
  1492. // For OpenPBR, try all connection paths; for PreviewSurface, just the first
  1493. const paths = textureGetter === this._getTextureFromOpenPBRConnection
  1494. ? spec.fields.connectionPaths
  1495. : [ spec.fields.connectionPaths[ 0 ] ];
  1496. for ( const connPath of paths ) {
  1497. const texture = textureGetter.call( this, connPath );
  1498. if ( texture ) {
  1499. texture.colorSpace = colorSpace;
  1500. material[ textureProperty ] = texture;
  1501. return true;
  1502. }
  1503. }
  1504. }
  1505. if ( fields[ attrName ] !== undefined && valueCallback ) {
  1506. valueCallback( fields[ attrName ] );
  1507. }
  1508. return false;
  1509. }
  1510. _applyPreviewSurface( material, shaderPath ) {
  1511. const fields = this._getAttributes( shaderPath );
  1512. const applyTexture = ( attrName, textureProperty, colorSpace, valueCallback ) => {
  1513. return this._applyTextureOrValue(
  1514. material, shaderPath, fields, attrName, textureProperty, colorSpace, valueCallback,
  1515. this._getTextureFromConnection
  1516. );
  1517. };
  1518. const getAttrSpec = ( attrName ) => {
  1519. const attrPath = shaderPath + '.' + attrName;
  1520. return this.specsByPath[ attrPath ];
  1521. };
  1522. // Diffuse color / base color map
  1523. applyTexture(
  1524. 'inputs:diffuseColor',
  1525. 'map',
  1526. SRGBColorSpace,
  1527. ( color ) => {
  1528. if ( Array.isArray( color ) && color.length >= 3 ) {
  1529. material.color.setRGB( color[ 0 ], color[ 1 ], color[ 2 ], SRGBColorSpace );
  1530. }
  1531. }
  1532. );
  1533. // Emissive
  1534. applyTexture(
  1535. 'inputs:emissiveColor',
  1536. 'emissiveMap',
  1537. SRGBColorSpace,
  1538. ( color ) => {
  1539. if ( Array.isArray( color ) && color.length >= 3 ) {
  1540. material.emissive.setRGB( color[ 0 ], color[ 1 ], color[ 2 ], SRGBColorSpace );
  1541. }
  1542. }
  1543. );
  1544. if ( material.emissiveMap ) {
  1545. material.emissive.set( 0xffffff );
  1546. }
  1547. // Normal map
  1548. applyTexture( 'inputs:normal', 'normalMap', NoColorSpace, null );
  1549. // Apply normal map scale from UsdUVTexture scale input
  1550. if ( material.normalMap && material.normalMap.userData.scale ) {
  1551. const scale = material.normalMap.userData.scale;
  1552. // UsdUVTexture scale is float4 (r,g,b,a), use first two components for normalScale
  1553. material.normalScale = new Vector2( scale[ 0 ], scale[ 1 ] );
  1554. }
  1555. // Roughness
  1556. const hasRoughnessMap = applyTexture(
  1557. 'inputs:roughness',
  1558. 'roughnessMap',
  1559. NoColorSpace,
  1560. ( value ) => {
  1561. material.roughness = value;
  1562. }
  1563. );
  1564. if ( hasRoughnessMap ) {
  1565. material.roughness = 1.0;
  1566. }
  1567. // Metallic
  1568. const hasMetalnessMap = applyTexture(
  1569. 'inputs:metallic',
  1570. 'metalnessMap',
  1571. NoColorSpace,
  1572. ( value ) => {
  1573. material.metalness = value;
  1574. }
  1575. );
  1576. if ( hasMetalnessMap ) {
  1577. material.metalness = 1.0;
  1578. }
  1579. // Occlusion
  1580. applyTexture( 'inputs:occlusion', 'aoMap', NoColorSpace, null );
  1581. // IOR
  1582. if ( fields[ 'inputs:ior' ] !== undefined ) {
  1583. material.ior = fields[ 'inputs:ior' ];
  1584. }
  1585. // Specular color
  1586. applyTexture(
  1587. 'inputs:specularColor',
  1588. 'specularColorMap',
  1589. SRGBColorSpace,
  1590. ( color ) => {
  1591. if ( Array.isArray( color ) && color.length >= 3 ) {
  1592. material.specularColor.setRGB( color[ 0 ], color[ 1 ], color[ 2 ], SRGBColorSpace );
  1593. }
  1594. }
  1595. );
  1596. // Clearcoat
  1597. if ( fields[ 'inputs:clearcoat' ] !== undefined ) {
  1598. material.clearcoat = fields[ 'inputs:clearcoat' ];
  1599. }
  1600. // Clearcoat roughness
  1601. if ( fields[ 'inputs:clearcoatRoughness' ] !== undefined ) {
  1602. material.clearcoatRoughness = fields[ 'inputs:clearcoatRoughness' ];
  1603. }
  1604. // Opacity and opacity modes
  1605. const opacityThreshold = fields[ 'inputs:opacityThreshold' ] !== undefined ? fields[ 'inputs:opacityThreshold' ] : 0.0;
  1606. // Check if opacity is connected to a texture (e.g., diffuse texture's alpha)
  1607. const opacitySpec = getAttrSpec( 'inputs:opacity' );
  1608. const hasOpacityConnection = opacitySpec?.fields?.connectionPaths?.length > 0;
  1609. if ( hasOpacityConnection ) {
  1610. // Opacity from texture alpha - use the diffuse map's alpha channel
  1611. if ( opacityThreshold > 0 ) {
  1612. // Alpha cutoff mode
  1613. material.alphaTest = opacityThreshold;
  1614. material.transparent = false;
  1615. } else {
  1616. // Alpha blend mode
  1617. material.transparent = true;
  1618. }
  1619. } else {
  1620. // Direct opacity value
  1621. const opacity = fields[ 'inputs:opacity' ] !== undefined ? fields[ 'inputs:opacity' ] : 1.0;
  1622. if ( opacity < 1.0 ) {
  1623. material.transparent = true;
  1624. material.opacity = opacity;
  1625. }
  1626. }
  1627. }
  1628. _applyOpenPBRSurface( material, shaderPath ) {
  1629. const fields = this._getAttributes( shaderPath );
  1630. const applyTexture = ( attrName, textureProperty, colorSpace, valueCallback ) => {
  1631. return this._applyTextureOrValue(
  1632. material, shaderPath, fields, attrName, textureProperty, colorSpace, valueCallback,
  1633. this._getTextureFromOpenPBRConnection
  1634. );
  1635. };
  1636. // Base color (diffuse)
  1637. applyTexture(
  1638. 'inputs:base_color',
  1639. 'map',
  1640. SRGBColorSpace,
  1641. ( color ) => {
  1642. if ( Array.isArray( color ) && color.length >= 3 ) {
  1643. material.color.setRGB( color[ 0 ], color[ 1 ], color[ 2 ], SRGBColorSpace );
  1644. }
  1645. }
  1646. );
  1647. // Base metalness
  1648. applyTexture(
  1649. 'inputs:base_metalness',
  1650. 'metalnessMap',
  1651. NoColorSpace,
  1652. ( value ) => {
  1653. if ( typeof value === 'number' ) {
  1654. material.metalness = value;
  1655. }
  1656. }
  1657. );
  1658. // Specular roughness
  1659. applyTexture(
  1660. 'inputs:specular_roughness',
  1661. 'roughnessMap',
  1662. NoColorSpace,
  1663. ( value ) => {
  1664. if ( typeof value === 'number' ) {
  1665. material.roughness = value;
  1666. }
  1667. }
  1668. );
  1669. // Emission color
  1670. const hasEmissionMap = applyTexture(
  1671. 'inputs:emission_color',
  1672. 'emissiveMap',
  1673. SRGBColorSpace,
  1674. ( color ) => {
  1675. if ( Array.isArray( color ) && color.length >= 3 ) {
  1676. material.emissive.setRGB( color[ 0 ], color[ 1 ], color[ 2 ], SRGBColorSpace );
  1677. }
  1678. }
  1679. );
  1680. // Emission luminance/weight - multiply emissive by this factor
  1681. const emissionLuminance = fields[ 'inputs:emission_luminance' ];
  1682. if ( emissionLuminance !== undefined && emissionLuminance > 0 ) {
  1683. if ( hasEmissionMap ) {
  1684. material.emissiveIntensity = emissionLuminance;
  1685. } else {
  1686. // Scale the emissive color by luminance
  1687. material.emissive.multiplyScalar( emissionLuminance );
  1688. }
  1689. }
  1690. // Transmission (transparency)
  1691. const transmissionWeight = fields[ 'inputs:transmission_weight' ];
  1692. if ( transmissionWeight !== undefined && transmissionWeight > 0 ) {
  1693. material.transmission = transmissionWeight;
  1694. const transmissionDepth = fields[ 'inputs:transmission_depth' ];
  1695. if ( transmissionDepth !== undefined ) {
  1696. material.thickness = transmissionDepth;
  1697. }
  1698. const transmissionColor = fields[ 'inputs:transmission_color' ];
  1699. if ( transmissionColor !== undefined && Array.isArray( transmissionColor ) ) {
  1700. material.attenuationColor.setRGB( transmissionColor[ 0 ], transmissionColor[ 1 ], transmissionColor[ 2 ] );
  1701. material.attenuationDistance = transmissionDepth || 1.0;
  1702. }
  1703. }
  1704. // Geometry opacity (overall surface opacity)
  1705. const geometryOpacity = fields[ 'inputs:geometry_opacity' ];
  1706. if ( geometryOpacity !== undefined && geometryOpacity < 1.0 ) {
  1707. material.opacity = geometryOpacity;
  1708. material.transparent = true;
  1709. }
  1710. // Specular IOR
  1711. const specularIOR = fields[ 'inputs:specular_ior' ];
  1712. if ( specularIOR !== undefined ) {
  1713. material.ior = specularIOR;
  1714. }
  1715. // Coat (clearcoat)
  1716. const coatWeight = fields[ 'inputs:coat_weight' ];
  1717. if ( coatWeight !== undefined && coatWeight > 0 ) {
  1718. material.clearcoat = coatWeight;
  1719. const coatRoughness = fields[ 'inputs:coat_roughness' ];
  1720. if ( coatRoughness !== undefined ) {
  1721. material.clearcoatRoughness = coatRoughness;
  1722. }
  1723. }
  1724. // Thin film (iridescence)
  1725. const thinFilmWeight = fields[ 'inputs:thin_film_weight' ];
  1726. if ( thinFilmWeight !== undefined && thinFilmWeight > 0 ) {
  1727. material.iridescence = thinFilmWeight;
  1728. const thinFilmIOR = fields[ 'inputs:thin_film_ior' ];
  1729. if ( thinFilmIOR !== undefined ) {
  1730. material.iridescenceIOR = thinFilmIOR;
  1731. }
  1732. const thinFilmThickness = fields[ 'inputs:thin_film_thickness' ];
  1733. if ( thinFilmThickness !== undefined ) {
  1734. // OpenPBR uses micrometers, Three.js uses nanometers
  1735. const thicknessNm = thinFilmThickness * 1000;
  1736. material.iridescenceThicknessRange = [ thicknessNm, thicknessNm ];
  1737. }
  1738. }
  1739. // Specular
  1740. const specularWeight = fields[ 'inputs:specular_weight' ];
  1741. if ( specularWeight !== undefined ) {
  1742. material.specularIntensity = specularWeight;
  1743. }
  1744. const specularColor = fields[ 'inputs:specular_color' ];
  1745. if ( specularColor !== undefined && Array.isArray( specularColor ) ) {
  1746. material.specularColor.setRGB( specularColor[ 0 ], specularColor[ 1 ], specularColor[ 2 ] );
  1747. }
  1748. // Anisotropy
  1749. const anisotropy = fields[ 'inputs:specular_roughness_anisotropy' ];
  1750. if ( anisotropy !== undefined && anisotropy > 0 ) {
  1751. material.anisotropy = anisotropy;
  1752. }
  1753. // Geometry normal (normal map)
  1754. applyTexture(
  1755. 'inputs:geometry_normal',
  1756. 'normalMap',
  1757. NoColorSpace,
  1758. null
  1759. );
  1760. }
  1761. _getTextureFromOpenPBRConnection( connPath ) {
  1762. // connPath is like /Material/NodeGraph.outputs:baseColor or /Material/Shader.outputs:out
  1763. const cleanPath = connPath.replace( /<|>/g, '' );
  1764. const shaderPath = cleanPath.split( '.' )[ 0 ];
  1765. const shaderSpec = this.specsByPath[ shaderPath ];
  1766. if ( ! shaderSpec ) return null;
  1767. const attrs = this._getAttributes( shaderPath );
  1768. const infoId = attrs[ 'info:id' ] || shaderSpec.fields[ 'info:id' ];
  1769. const typeName = shaderSpec.fields.typeName;
  1770. // Handle NodeGraph - follow output connection to internal shader
  1771. if ( typeName === 'NodeGraph' ) {
  1772. // Get the output attribute that's connected
  1773. const outputName = cleanPath.split( '.' )[ 1 ]; // e.g., "outputs:baseColor"
  1774. const outputAttrPath = shaderPath + '.' + outputName;
  1775. const outputSpec = this.specsByPath[ outputAttrPath ];
  1776. if ( outputSpec?.fields?.connectionPaths?.length > 0 ) {
  1777. // Follow the internal connection
  1778. return this._getTextureFromOpenPBRConnection( outputSpec.fields.connectionPaths[ 0 ] );
  1779. }
  1780. return null;
  1781. }
  1782. // Handle arnold:image - Arnold's texture node
  1783. if ( infoId === 'arnold:image' ) {
  1784. const filePath = attrs[ 'inputs:filename' ];
  1785. if ( ! filePath ) return null;
  1786. return this._loadTextureFromPath( filePath );
  1787. }
  1788. // Handle MaterialX image nodes (ND_image_color4, ND_image_color3, etc.)
  1789. if ( infoId && infoId.startsWith( 'ND_image_' ) ) {
  1790. const filePath = attrs[ 'inputs:file' ];
  1791. if ( ! filePath ) return null;
  1792. return this._loadTextureFromPath( filePath );
  1793. }
  1794. // Handle Maya file texture - follow the inColor connection to the actual image
  1795. if ( infoId === 'MayaND_fileTexture_color4' ) {
  1796. const inColorPath = shaderPath + '.inputs:inColor';
  1797. const inColorSpec = this.specsByPath[ inColorPath ];
  1798. if ( inColorSpec?.fields?.connectionPaths?.length > 0 ) {
  1799. return this._getTextureFromOpenPBRConnection( inColorSpec.fields.connectionPaths[ 0 ] );
  1800. }
  1801. return null;
  1802. }
  1803. // Handle color conversion nodes - follow the input connection
  1804. if ( infoId && infoId.startsWith( 'ND_convert_' ) ) {
  1805. const inPath = shaderPath + '.inputs:in';
  1806. const inSpec = this.specsByPath[ inPath ];
  1807. if ( inSpec?.fields?.connectionPaths?.length > 0 ) {
  1808. return this._getTextureFromOpenPBRConnection( inSpec.fields.connectionPaths[ 0 ] );
  1809. }
  1810. return null;
  1811. }
  1812. // Handle Arnold bump2d - follow the bump_map input
  1813. if ( infoId === 'arnold:bump2d' ) {
  1814. const bumpMapPath = shaderPath + '.inputs:bump_map';
  1815. const bumpMapSpec = this.specsByPath[ bumpMapPath ];
  1816. if ( bumpMapSpec?.fields?.connectionPaths?.length > 0 ) {
  1817. return this._getTextureFromOpenPBRConnection( bumpMapSpec.fields.connectionPaths[ 0 ] );
  1818. }
  1819. return null;
  1820. }
  1821. // Handle Arnold color_correct - follow the input connection
  1822. if ( infoId === 'arnold:color_correct' ) {
  1823. const inputPath = shaderPath + '.inputs:input';
  1824. const inputSpec = this.specsByPath[ inputPath ];
  1825. if ( inputSpec?.fields?.connectionPaths?.length > 0 ) {
  1826. return this._getTextureFromOpenPBRConnection( inputSpec.fields.connectionPaths[ 0 ] );
  1827. }
  1828. return null;
  1829. }
  1830. // Handle nested shader paths (e.g., /Material/file2/cc.outputs:a)
  1831. // Check if parent path is an image node
  1832. const parentPath = shaderPath.substring( 0, shaderPath.lastIndexOf( '/' ) );
  1833. if ( parentPath ) {
  1834. const parentSpec = this.specsByPath[ parentPath ];
  1835. if ( parentSpec ) {
  1836. const parentAttrs = this._getAttributes( parentPath );
  1837. const parentInfoId = parentAttrs[ 'info:id' ] || parentSpec.fields[ 'info:id' ];
  1838. if ( parentInfoId === 'arnold:image' ) {
  1839. const filePath = parentAttrs[ 'inputs:filename' ];
  1840. if ( filePath ) return this._loadTextureFromPath( filePath );
  1841. }
  1842. }
  1843. }
  1844. return null;
  1845. }
  1846. _loadTextureFromPath( filePath ) {
  1847. if ( ! filePath ) return null;
  1848. // Check cache first
  1849. if ( this.textureCache[ filePath ] ) {
  1850. return this.textureCache[ filePath ];
  1851. }
  1852. const texture = this._loadTexture( filePath, null, null );
  1853. if ( texture ) {
  1854. this.textureCache[ filePath ] = texture;
  1855. }
  1856. return texture;
  1857. }
  1858. _getTextureFromConnection( connPath ) {
  1859. // connPath is like /Material/Shader.outputs:rgb
  1860. const shaderPath = connPath.split( '.' )[ 0 ];
  1861. const shaderSpec = this.specsByPath[ shaderPath ];
  1862. if ( ! shaderSpec ) return null;
  1863. const attrs = this._getAttributes( shaderPath );
  1864. const infoId = attrs[ 'info:id' ] || shaderSpec.fields[ 'info:id' ];
  1865. if ( infoId !== 'UsdUVTexture' ) return null;
  1866. const filePath = attrs[ 'inputs:file' ];
  1867. if ( ! filePath ) return null;
  1868. // Check for UsdTransform2d connection via inputs:st and trace to PrimvarReader
  1869. let transformAttrs = null;
  1870. let uvChannel = 0; // Default to first UV set
  1871. const stAttrPath = shaderPath + '.inputs:st';
  1872. const stAttrSpec = this.specsByPath[ stAttrPath ];
  1873. if ( stAttrSpec?.fields?.connectionPaths?.length > 0 ) {
  1874. const stConnPath = stAttrSpec.fields.connectionPaths[ 0 ];
  1875. const stPath = stConnPath.replace( /<|>/g, '' ).split( '.' )[ 0 ];
  1876. const stSpec = this.specsByPath[ stPath ];
  1877. if ( stSpec ) {
  1878. const stAttrs = this._getAttributes( stPath );
  1879. const stInfoId = stAttrs[ 'info:id' ] || stSpec.fields[ 'info:id' ];
  1880. if ( stInfoId === 'UsdTransform2d' ) {
  1881. transformAttrs = stAttrs;
  1882. // Trace to PrimvarReader to find UV set
  1883. const inAttrPath = stPath + '.inputs:in';
  1884. const inAttrSpec = this.specsByPath[ inAttrPath ];
  1885. if ( inAttrSpec?.fields?.connectionPaths?.length > 0 ) {
  1886. const inConnPath = inAttrSpec.fields.connectionPaths[ 0 ];
  1887. const primvarPath = inConnPath.replace( /<|>/g, '' ).split( '.' )[ 0 ];
  1888. const primvarAttrs = this._getAttributes( primvarPath );
  1889. // Check varname to determine UV channel
  1890. const varname = primvarAttrs[ 'inputs:varname' ];
  1891. if ( varname === 'st1' ) uvChannel = 1;
  1892. else if ( varname === 'st2' ) uvChannel = 2;
  1893. }
  1894. } else if ( stInfoId === 'UsdPrimvarReader_float2' ) {
  1895. // Direct connection to PrimvarReader
  1896. const varname = stAttrs[ 'inputs:varname' ];
  1897. if ( varname === 'st1' ) uvChannel = 1;
  1898. else if ( varname === 'st2' ) uvChannel = 2;
  1899. }
  1900. }
  1901. }
  1902. // Extract scale and bias for texture value modification
  1903. const scale = attrs[ 'inputs:scale' ];
  1904. const bias = attrs[ 'inputs:bias' ];
  1905. // Create cache key that includes scale/bias if present
  1906. let cacheKey = filePath;
  1907. if ( scale ) cacheKey += ':s' + scale.join( ',' );
  1908. if ( bias ) cacheKey += ':b' + bias.join( ',' );
  1909. if ( this.textureCache[ cacheKey ] ) {
  1910. return this.textureCache[ cacheKey ];
  1911. }
  1912. const texture = this._loadTexture( filePath, attrs, transformAttrs );
  1913. if ( texture ) {
  1914. // Store scale/bias and UV channel in userData
  1915. if ( scale ) texture.userData.scale = scale;
  1916. if ( bias ) texture.userData.bias = bias;
  1917. if ( uvChannel !== 0 ) texture.channel = uvChannel;
  1918. this.textureCache[ cacheKey ] = texture;
  1919. }
  1920. return texture;
  1921. }
  1922. _applyTextureTransforms( texture, attrs ) {
  1923. if ( ! attrs ) return;
  1924. const scale = attrs[ 'inputs:scale' ];
  1925. if ( scale && Array.isArray( scale ) && scale.length >= 2 ) {
  1926. texture.repeat.set( scale[ 0 ], scale[ 1 ] );
  1927. }
  1928. const translation = attrs[ 'inputs:translation' ];
  1929. if ( translation && Array.isArray( translation ) && translation.length >= 2 ) {
  1930. texture.offset.set( translation[ 0 ], translation[ 1 ] );
  1931. }
  1932. const rotation = attrs[ 'inputs:rotation' ];
  1933. if ( typeof rotation === 'number' ) {
  1934. texture.rotation = rotation * Math.PI / 180;
  1935. }
  1936. }
  1937. _loadTexture( filePath, textureAttrs, transformAttrs ) {
  1938. let cleanPath = filePath;
  1939. if ( cleanPath.startsWith( '@' ) ) cleanPath = cleanPath.slice( 1 );
  1940. if ( cleanPath.endsWith( '@' ) ) cleanPath = cleanPath.slice( 0, - 1 );
  1941. // Resolve relative to basePath first
  1942. const resolvedPath = this._resolveFilePath( cleanPath );
  1943. let assetData = this.assets[ resolvedPath ];
  1944. // Fallback to unresolved path
  1945. if ( ! assetData ) {
  1946. assetData = this.assets[ cleanPath ];
  1947. }
  1948. // Last resort: search by basename
  1949. if ( ! assetData ) {
  1950. const baseName = cleanPath.split( '/' ).pop();
  1951. for ( const key in this.assets ) {
  1952. if ( key.endsWith( baseName ) || key.endsWith( '/' + baseName ) ) {
  1953. return this._createTextureFromData( this.assets[ key ], textureAttrs, transformAttrs );
  1954. }
  1955. }
  1956. // Try loading via LoadingManager if available
  1957. if ( this.manager ) {
  1958. const url = this.manager.resolveURL( baseName );
  1959. if ( url !== baseName ) {
  1960. // URL modifier found a match - load it
  1961. return this._createTextureFromData( url, textureAttrs, transformAttrs );
  1962. }
  1963. }
  1964. console.warn( 'USDLoader: Texture not found:', cleanPath );
  1965. return null;
  1966. }
  1967. return this._createTextureFromData( assetData, textureAttrs, transformAttrs );
  1968. }
  1969. _createTextureFromData( data, textureAttrs, transformAttrs ) {
  1970. if ( ! data ) return null;
  1971. const scope = this;
  1972. const texture = new Texture();
  1973. let url;
  1974. if ( typeof data === 'string' ) {
  1975. url = data;
  1976. } else if ( data instanceof Uint8Array || data instanceof ArrayBuffer ) {
  1977. const blob = new Blob( [ data ] );
  1978. url = URL.createObjectURL( blob );
  1979. } else {
  1980. return null;
  1981. }
  1982. const image = new Image();
  1983. image.onload = function () {
  1984. texture.image = image;
  1985. if ( textureAttrs ) {
  1986. texture.wrapS = scope._getWrapMode( textureAttrs[ 'inputs:wrapS' ] );
  1987. texture.wrapT = scope._getWrapMode( textureAttrs[ 'inputs:wrapT' ] );
  1988. }
  1989. scope._applyTextureTransforms( texture, transformAttrs );
  1990. texture.needsUpdate = true;
  1991. if ( typeof data !== 'string' ) {
  1992. URL.revokeObjectURL( url );
  1993. }
  1994. };
  1995. image.src = url;
  1996. return texture;
  1997. }
  1998. _getWrapMode( wrapValue ) {
  1999. if ( wrapValue === 'repeat' ) return RepeatWrapping;
  2000. if ( wrapValue === 'mirror' ) return MirroredRepeatWrapping;
  2001. if ( wrapValue === 'clamp' ) return ClampToEdgeWrapping;
  2002. return RepeatWrapping;
  2003. }
  2004. // ========================================================================
  2005. // Skeletal Animation
  2006. // ========================================================================
  2007. _buildSkeleton( path ) {
  2008. const attrs = this._getAttributes( path );
  2009. // Get joint names (paths like "root", "root/body_joint", etc.)
  2010. const joints = attrs[ 'joints' ];
  2011. if ( ! joints || joints.length === 0 ) return null;
  2012. // Get bind transforms (world-space bind pose matrices)
  2013. // These can be nested arrays (USDA) or flat arrays (USDC)
  2014. const rawBindTransforms = attrs[ 'bindTransforms' ];
  2015. const rawRestTransforms = attrs[ 'restTransforms' ];
  2016. const bindTransforms = this._flattenMatrixArray( rawBindTransforms, joints.length );
  2017. const restTransforms = this._flattenMatrixArray( rawRestTransforms, joints.length );
  2018. // Build bones
  2019. const bones = [];
  2020. const bonesByPath = {};
  2021. const boneInverses = [];
  2022. for ( let i = 0; i < joints.length; i ++ ) {
  2023. const jointPath = joints[ i ];
  2024. const jointName = jointPath.split( '/' ).pop();
  2025. const bone = new Bone();
  2026. bone.name = jointName;
  2027. bones.push( bone );
  2028. bonesByPath[ jointPath ] = { bone, index: i };
  2029. // Compute inverse bind matrix
  2030. if ( bindTransforms && bindTransforms.length >= ( i + 1 ) * 16 ) {
  2031. const bindMatrix = new Matrix4();
  2032. bindMatrix.fromArray( bindTransforms, i * 16 );
  2033. const inverseBindMatrix = bindMatrix.clone().invert();
  2034. boneInverses.push( inverseBindMatrix );
  2035. } else {
  2036. boneInverses.push( new Matrix4() );
  2037. }
  2038. }
  2039. // Build parent-child relationships based on joint paths
  2040. for ( let i = 0; i < joints.length; i ++ ) {
  2041. const jointPath = joints[ i ];
  2042. const parts = jointPath.split( '/' );
  2043. if ( parts.length > 1 ) {
  2044. const parentPath = parts.slice( 0, - 1 ).join( '/' );
  2045. const parentData = bonesByPath[ parentPath ];
  2046. if ( parentData ) {
  2047. parentData.bone.add( bones[ i ] );
  2048. }
  2049. }
  2050. }
  2051. // Apply rest transforms to bones (local transforms)
  2052. if ( restTransforms && restTransforms.length >= joints.length * 16 ) {
  2053. for ( let i = 0; i < joints.length; i ++ ) {
  2054. const matrix = new Matrix4();
  2055. matrix.fromArray( restTransforms, i * 16 );
  2056. matrix.decompose( bones[ i ].position, bones[ i ].quaternion, bones[ i ].scale );
  2057. }
  2058. }
  2059. // Find root bone(s) - bones without a parent bone
  2060. const rootBones = bones.filter( bone => ! bone.parent || ! bone.parent.isBone );
  2061. // Get animation source path
  2062. const animSourceSpec = this.specsByPath[ path + '.skel:animationSource' ];
  2063. let animationPath = null;
  2064. if ( animSourceSpec && animSourceSpec.fields.targetPaths && animSourceSpec.fields.targetPaths.length > 0 ) {
  2065. animationPath = animSourceSpec.fields.targetPaths[ 0 ];
  2066. }
  2067. return {
  2068. skeleton: new Skeleton( bones, boneInverses ),
  2069. joints: joints,
  2070. rootBones: rootBones,
  2071. animationPath: animationPath,
  2072. path: path
  2073. };
  2074. }
  2075. _bindSkeletons() {
  2076. for ( const meshData of this.skinnedMeshes ) {
  2077. const { mesh, skeletonPath, localJoints } = meshData;
  2078. let skeletonData = null;
  2079. // Try exact match first
  2080. if ( skeletonPath && this.skeletons[ skeletonPath ] ) {
  2081. skeletonData = this.skeletons[ skeletonPath ];
  2082. }
  2083. // Try includes match as fallback
  2084. if ( ! skeletonData ) {
  2085. for ( const skelPath in this.skeletons ) {
  2086. if ( skeletonPath && ( skeletonPath.includes( skelPath ) || skelPath.includes( skeletonPath ) ) ) {
  2087. skeletonData = this.skeletons[ skelPath ];
  2088. break;
  2089. }
  2090. }
  2091. }
  2092. // Fallback to first skeleton for single-skeleton files
  2093. if ( ! skeletonData ) {
  2094. const skeletonPaths = Object.keys( this.skeletons );
  2095. if ( skeletonPaths.length > 0 ) {
  2096. skeletonData = this.skeletons[ skeletonPaths[ 0 ] ];
  2097. }
  2098. }
  2099. if ( ! skeletonData ) {
  2100. console.warn( 'USDComposer: No skeleton found for skinned mesh', mesh.name );
  2101. continue;
  2102. }
  2103. const { skeleton, rootBones, joints } = skeletonData;
  2104. if ( localJoints && localJoints.length > 0 ) {
  2105. const skinIndex = mesh.geometry.attributes.skinIndex;
  2106. if ( skinIndex ) {
  2107. const localToGlobal = [];
  2108. for ( let i = 0; i < localJoints.length; i ++ ) {
  2109. const jointName = localJoints[ i ];
  2110. const globalIdx = joints.indexOf( jointName );
  2111. localToGlobal[ i ] = globalIdx >= 0 ? globalIdx : 0;
  2112. }
  2113. const arr = skinIndex.array;
  2114. for ( let i = 0; i < arr.length; i ++ ) {
  2115. const localIdx = arr[ i ];
  2116. if ( localIdx < localToGlobal.length ) {
  2117. arr[ i ] = localToGlobal[ localIdx ];
  2118. }
  2119. }
  2120. }
  2121. }
  2122. for ( const rootBone of rootBones ) {
  2123. mesh.add( rootBone );
  2124. }
  2125. mesh.bind( skeleton, new Matrix4() );
  2126. }
  2127. }
  2128. _buildAnimations() {
  2129. const animations = [];
  2130. // Find all SkelAnimation prims
  2131. for ( const path in this.specsByPath ) {
  2132. const spec = this.specsByPath[ path ];
  2133. if ( spec.specType !== SpecType.Prim ) continue;
  2134. if ( spec.fields.typeName !== 'SkelAnimation' ) continue;
  2135. const clip = this._buildAnimationClip( path );
  2136. if ( clip ) {
  2137. animations.push( clip );
  2138. }
  2139. }
  2140. // Build transform animations from time-sampled xformOps
  2141. const transformTracks = this._buildTransformAnimations();
  2142. if ( transformTracks.length > 0 ) {
  2143. animations.push( new AnimationClip( 'TransformAnimation', - 1, transformTracks ) );
  2144. }
  2145. return animations;
  2146. }
  2147. _buildTransformAnimations() {
  2148. const tracks = [];
  2149. for ( const path in this.specsByPath ) {
  2150. const spec = this.specsByPath[ path ];
  2151. if ( spec.specType !== SpecType.Prim ) continue;
  2152. const typeName = spec.fields?.typeName;
  2153. if ( typeName !== 'Xform' && typeName !== 'Scope' && typeName !== 'Mesh' ) continue;
  2154. const objectName = path.split( '/' ).pop();
  2155. // Check for animated xformOp:orient
  2156. const orientPath = path + '.xformOp:orient';
  2157. const orientSpec = this.specsByPath[ orientPath ];
  2158. if ( orientSpec?.fields?.timeSamples ) {
  2159. const { times, values } = orientSpec.fields.timeSamples;
  2160. const keyframeTimes = [];
  2161. const keyframeValues = [];
  2162. for ( let i = 0; i < times.length; i ++ ) {
  2163. keyframeTimes.push( times[ i ] / this.fps );
  2164. const q = values[ i ];
  2165. keyframeValues.push( q[ 0 ], q[ 1 ], q[ 2 ], q[ 3 ] );
  2166. }
  2167. if ( keyframeTimes.length > 0 ) {
  2168. tracks.push( new QuaternionKeyframeTrack(
  2169. objectName + '.quaternion',
  2170. new Float32Array( keyframeTimes ),
  2171. new Float32Array( keyframeValues )
  2172. ) );
  2173. }
  2174. }
  2175. // Check for animated xformOp:translate
  2176. const translatePath = path + '.xformOp:translate';
  2177. const translateSpec = this.specsByPath[ translatePath ];
  2178. if ( translateSpec?.fields?.timeSamples ) {
  2179. const { times, values } = translateSpec.fields.timeSamples;
  2180. const keyframeTimes = [];
  2181. const keyframeValues = [];
  2182. for ( let i = 0; i < times.length; i ++ ) {
  2183. keyframeTimes.push( times[ i ] / this.fps );
  2184. const t = values[ i ];
  2185. keyframeValues.push( t[ 0 ], t[ 1 ], t[ 2 ] );
  2186. }
  2187. if ( keyframeTimes.length > 0 ) {
  2188. tracks.push( new VectorKeyframeTrack(
  2189. objectName + '.position',
  2190. new Float32Array( keyframeTimes ),
  2191. new Float32Array( keyframeValues )
  2192. ) );
  2193. }
  2194. }
  2195. // Check for animated xformOp:scale
  2196. const scalePath = path + '.xformOp:scale';
  2197. const scaleSpec = this.specsByPath[ scalePath ];
  2198. if ( scaleSpec?.fields?.timeSamples ) {
  2199. const { times, values } = scaleSpec.fields.timeSamples;
  2200. const keyframeTimes = [];
  2201. const keyframeValues = [];
  2202. for ( let i = 0; i < times.length; i ++ ) {
  2203. keyframeTimes.push( times[ i ] / this.fps );
  2204. const s = values[ i ];
  2205. keyframeValues.push( s[ 0 ], s[ 1 ], s[ 2 ] );
  2206. }
  2207. if ( keyframeTimes.length > 0 ) {
  2208. tracks.push( new VectorKeyframeTrack(
  2209. objectName + '.scale',
  2210. new Float32Array( keyframeTimes ),
  2211. new Float32Array( keyframeValues )
  2212. ) );
  2213. }
  2214. }
  2215. // Check for animated xformOp:transform (matrix animations)
  2216. // These can have suffixes like xformOp:transform:transform
  2217. const properties = spec.fields?.properties || [];
  2218. for ( const prop of properties ) {
  2219. if ( ! prop.startsWith( 'xformOp:transform' ) ) continue;
  2220. const transformPath = path + '.' + prop;
  2221. const transformSpec = this.specsByPath[ transformPath ];
  2222. if ( ! transformSpec?.fields?.timeSamples ) continue;
  2223. const { times, values } = transformSpec.fields.timeSamples;
  2224. const positionTimes = [];
  2225. const positionValues = [];
  2226. const quaternionTimes = [];
  2227. const quaternionValues = [];
  2228. const scaleTimes = [];
  2229. const scaleValues = [];
  2230. const matrix = new Matrix4();
  2231. const position = new Vector3();
  2232. const quaternion = new Quaternion();
  2233. const scale = new Vector3();
  2234. for ( let i = 0; i < times.length; i ++ ) {
  2235. const m = values[ i ];
  2236. if ( ! m || m.length < 16 ) continue;
  2237. const t = times[ i ] / this.fps;
  2238. // USD matrices are row-major, Three.js is column-major
  2239. matrix.set(
  2240. m[ 0 ], m[ 4 ], m[ 8 ], m[ 12 ],
  2241. m[ 1 ], m[ 5 ], m[ 9 ], m[ 13 ],
  2242. m[ 2 ], m[ 6 ], m[ 10 ], m[ 14 ],
  2243. m[ 3 ], m[ 7 ], m[ 11 ], m[ 15 ]
  2244. );
  2245. matrix.decompose( position, quaternion, scale );
  2246. positionTimes.push( t );
  2247. positionValues.push( position.x, position.y, position.z );
  2248. quaternionTimes.push( t );
  2249. quaternionValues.push( quaternion.x, quaternion.y, quaternion.z, quaternion.w );
  2250. scaleTimes.push( t );
  2251. scaleValues.push( scale.x, scale.y, scale.z );
  2252. }
  2253. if ( positionTimes.length > 0 ) {
  2254. tracks.push( new VectorKeyframeTrack(
  2255. objectName + '.position',
  2256. new Float32Array( positionTimes ),
  2257. new Float32Array( positionValues )
  2258. ) );
  2259. tracks.push( new QuaternionKeyframeTrack(
  2260. objectName + '.quaternion',
  2261. new Float32Array( quaternionTimes ),
  2262. new Float32Array( quaternionValues )
  2263. ) );
  2264. tracks.push( new VectorKeyframeTrack(
  2265. objectName + '.scale',
  2266. new Float32Array( scaleTimes ),
  2267. new Float32Array( scaleValues )
  2268. ) );
  2269. }
  2270. break; // Only process first transform op
  2271. }
  2272. }
  2273. return tracks;
  2274. }
  2275. _buildAnimationClip( path ) {
  2276. const attrs = this._getAttributes( path );
  2277. const joints = attrs[ 'joints' ];
  2278. if ( ! joints || joints.length === 0 ) return null;
  2279. const tracks = [];
  2280. // Get rotation time samples
  2281. const rotationsAttr = this._getTimeSampledAttribute( path, 'rotations' );
  2282. if ( rotationsAttr && rotationsAttr.times && rotationsAttr.values ) {
  2283. const { times, values } = rotationsAttr;
  2284. for ( let jointIdx = 0; jointIdx < joints.length; jointIdx ++ ) {
  2285. const jointName = joints[ jointIdx ].split( '/' ).pop();
  2286. const keyframeTimes = [];
  2287. const keyframeValues = [];
  2288. for ( let t = 0; t < times.length; t ++ ) {
  2289. const quatData = values[ t ];
  2290. if ( ! quatData || quatData.length < ( jointIdx + 1 ) * 4 ) continue;
  2291. keyframeTimes.push( times[ t ] / this.fps );
  2292. // USD GfQuatf stores imaginary (x,y,z) first, then real (w)
  2293. // This matches Three.js quaternion order (x,y,z,w)
  2294. const x = quatData[ jointIdx * 4 + 0 ];
  2295. const y = quatData[ jointIdx * 4 + 1 ];
  2296. const z = quatData[ jointIdx * 4 + 2 ];
  2297. const w = quatData[ jointIdx * 4 + 3 ];
  2298. keyframeValues.push( x, y, z, w );
  2299. }
  2300. if ( keyframeTimes.length > 0 ) {
  2301. tracks.push( new QuaternionKeyframeTrack(
  2302. jointName + '.quaternion',
  2303. new Float32Array( keyframeTimes ),
  2304. new Float32Array( keyframeValues )
  2305. ) );
  2306. }
  2307. }
  2308. }
  2309. // Get translation time samples
  2310. const translationsAttr = this._getTimeSampledAttribute( path, 'translations' );
  2311. if ( translationsAttr && translationsAttr.times && translationsAttr.values ) {
  2312. const { times, values } = translationsAttr;
  2313. for ( let jointIdx = 0; jointIdx < joints.length; jointIdx ++ ) {
  2314. const jointName = joints[ jointIdx ].split( '/' ).pop();
  2315. const keyframeTimes = [];
  2316. const keyframeValues = [];
  2317. for ( let t = 0; t < times.length; t ++ ) {
  2318. const transData = values[ t ];
  2319. if ( ! transData || transData.length < ( jointIdx + 1 ) * 3 ) continue;
  2320. keyframeTimes.push( times[ t ] / this.fps );
  2321. keyframeValues.push(
  2322. transData[ jointIdx * 3 + 0 ],
  2323. transData[ jointIdx * 3 + 1 ],
  2324. transData[ jointIdx * 3 + 2 ]
  2325. );
  2326. }
  2327. if ( keyframeTimes.length > 0 ) {
  2328. tracks.push( new VectorKeyframeTrack(
  2329. jointName + '.position',
  2330. new Float32Array( keyframeTimes ),
  2331. new Float32Array( keyframeValues )
  2332. ) );
  2333. }
  2334. }
  2335. }
  2336. // Get scale time samples
  2337. const scalesAttr = this._getTimeSampledAttribute( path, 'scales' );
  2338. if ( scalesAttr && scalesAttr.times && scalesAttr.values ) {
  2339. const { times, values } = scalesAttr;
  2340. for ( let jointIdx = 0; jointIdx < joints.length; jointIdx ++ ) {
  2341. const jointName = joints[ jointIdx ].split( '/' ).pop();
  2342. const keyframeTimes = [];
  2343. const keyframeValues = [];
  2344. for ( let t = 0; t < times.length; t ++ ) {
  2345. const scaleData = values[ t ];
  2346. if ( ! scaleData || scaleData.length < ( jointIdx + 1 ) * 3 ) continue;
  2347. keyframeTimes.push( times[ t ] / this.fps );
  2348. keyframeValues.push(
  2349. scaleData[ jointIdx * 3 + 0 ],
  2350. scaleData[ jointIdx * 3 + 1 ],
  2351. scaleData[ jointIdx * 3 + 2 ]
  2352. );
  2353. }
  2354. if ( keyframeTimes.length > 0 ) {
  2355. tracks.push( new VectorKeyframeTrack(
  2356. jointName + '.scale',
  2357. new Float32Array( keyframeTimes ),
  2358. new Float32Array( keyframeValues )
  2359. ) );
  2360. }
  2361. }
  2362. }
  2363. if ( tracks.length === 0 ) return null;
  2364. const clipName = path.split( '/' ).pop();
  2365. return new AnimationClip( clipName, - 1, tracks );
  2366. }
  2367. _getTimeSampledAttribute( primPath, attrName ) {
  2368. // Look for the attribute spec with time samples
  2369. const attrPath = primPath + '.' + attrName;
  2370. const attrSpec = this.specsByPath[ attrPath ];
  2371. if ( attrSpec && attrSpec.fields.timeSamples ) {
  2372. const timeSamples = attrSpec.fields.timeSamples;
  2373. if ( timeSamples.times && timeSamples.values ) {
  2374. return timeSamples;
  2375. }
  2376. }
  2377. return null;
  2378. }
  2379. _flattenMatrixArray( matrices, numMatrices ) {
  2380. if ( ! matrices || matrices.length === 0 ) return null;
  2381. if ( typeof matrices[ 0 ] === 'number' ) return matrices;
  2382. const flatArray = [];
  2383. for ( let m = 0; m < numMatrices; m ++ ) {
  2384. for ( let row = 0; row < 4; row ++ ) {
  2385. const rowData = matrices[ m * 4 + row ];
  2386. if ( rowData && rowData.length === 4 ) {
  2387. flatArray.push( rowData[ 0 ], rowData[ 1 ], rowData[ 2 ], rowData[ 3 ] );
  2388. } else {
  2389. flatArray.push( row === 0 ? 1 : 0, row === 1 ? 1 : 0, row === 2 ? 1 : 0, row === 3 ? 1 : 0 );
  2390. }
  2391. }
  2392. }
  2393. return flatArray;
  2394. }
  2395. }
  2396. export { USDComposer, SpecType };
粤ICP备19079148号