USDComposer.js 93 KB

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