@plastic-software/three 0.181.3 → 0.183.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (437) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +3 -4
  3. package/build/three.cjs +11330 -10017
  4. package/build/three.core.js +10011 -9493
  5. package/build/three.core.min.js +2 -2
  6. package/build/three.module.js +1414 -631
  7. package/build/three.module.min.js +2 -2
  8. package/build/three.tsl.js +21 -13
  9. package/build/three.tsl.min.js +2 -2
  10. package/build/three.webgpu.js +8007 -5427
  11. package/build/three.webgpu.min.js +2 -2
  12. package/build/three.webgpu.nodes.js +8005 -5426
  13. package/build/three.webgpu.nodes.min.js +2 -2
  14. package/examples/jsm/Addons.js +0 -3
  15. package/examples/jsm/animation/CCDIKSolver.js +2 -2
  16. package/examples/jsm/controls/ArcballControls.js +3 -3
  17. package/examples/jsm/controls/MapControls.js +55 -1
  18. package/examples/jsm/controls/OrbitControls.js +109 -6
  19. package/examples/jsm/controls/TrackballControls.js +6 -6
  20. package/examples/jsm/csm/CSM.js +2 -1
  21. package/examples/jsm/effects/AnaglyphEffect.js +102 -7
  22. package/examples/jsm/environments/ColorEnvironment.js +59 -0
  23. package/examples/jsm/environments/RoomEnvironment.js +3 -0
  24. package/examples/jsm/exporters/EXRExporter.js +1 -1
  25. package/examples/jsm/exporters/GLTFExporter.js +131 -4
  26. package/examples/jsm/exporters/USDZExporter.js +22 -3
  27. package/examples/jsm/geometries/DecalGeometry.js +1 -1
  28. package/examples/jsm/helpers/AnimationPathHelper.js +302 -0
  29. package/examples/jsm/helpers/LightProbeHelperGPU.js +1 -1
  30. package/examples/jsm/helpers/TextureHelperGPU.js +1 -1
  31. package/examples/jsm/helpers/ViewHelper.js +67 -8
  32. package/examples/jsm/inspector/Inspector.js +74 -14
  33. package/examples/jsm/inspector/RendererInspector.js +12 -2
  34. package/examples/jsm/inspector/tabs/Console.js +41 -7
  35. package/examples/jsm/inspector/tabs/Parameters.js +18 -2
  36. package/examples/jsm/inspector/tabs/Performance.js +2 -2
  37. package/examples/jsm/inspector/tabs/Viewer.js +4 -4
  38. package/examples/jsm/inspector/ui/Profiler.js +1836 -31
  39. package/examples/jsm/inspector/ui/Style.js +973 -14
  40. package/examples/jsm/inspector/ui/Tab.js +188 -1
  41. package/examples/jsm/inspector/ui/Values.js +17 -1
  42. package/examples/jsm/libs/meshopt_decoder.module.js +6 -5
  43. package/examples/jsm/lines/LineMaterial.js +6 -0
  44. package/examples/jsm/loaders/3DMLoader.js +5 -4
  45. package/examples/jsm/loaders/3MFLoader.js +2 -2
  46. package/examples/jsm/loaders/AMFLoader.js +2 -2
  47. package/examples/jsm/loaders/ColladaLoader.js +24 -4026
  48. package/examples/jsm/loaders/DRACOLoader.js +5 -5
  49. package/examples/jsm/loaders/EXRLoader.js +5 -5
  50. package/examples/jsm/loaders/FBXLoader.js +2 -4
  51. package/examples/jsm/loaders/GCodeLoader.js +34 -8
  52. package/examples/jsm/loaders/GLTFLoader.js +122 -171
  53. package/examples/jsm/loaders/HDRLoader.js +0 -1
  54. package/examples/jsm/loaders/KMZLoader.js +5 -5
  55. package/examples/jsm/loaders/KTX2Loader.js +19 -3
  56. package/examples/jsm/loaders/LDrawLoader.js +2 -3
  57. package/examples/jsm/loaders/LWOLoader.js +7 -39
  58. package/examples/jsm/loaders/NRRDLoader.js +2 -2
  59. package/examples/jsm/loaders/PCDLoader.js +4 -2
  60. package/examples/jsm/loaders/SVGLoader.js +1 -1
  61. package/examples/jsm/loaders/TDSLoader.js +0 -2
  62. package/examples/jsm/loaders/TGALoader.js +0 -2
  63. package/examples/jsm/loaders/USDLoader.js +100 -40
  64. package/examples/jsm/loaders/UltraHDRLoader.js +285 -160
  65. package/examples/jsm/loaders/VOXLoader.js +660 -117
  66. package/examples/jsm/loaders/VRMLLoader.js +79 -2
  67. package/examples/jsm/loaders/VTKLoader.js +37 -24
  68. package/examples/jsm/loaders/collada/ColladaComposer.js +2950 -0
  69. package/examples/jsm/loaders/collada/ColladaParser.js +1962 -0
  70. package/examples/jsm/loaders/usd/USDAParser.js +447 -366
  71. package/examples/jsm/loaders/usd/USDCParser.js +1841 -6
  72. package/examples/jsm/loaders/usd/USDComposer.js +4041 -0
  73. package/examples/jsm/materials/LDrawConditionalLineNodeMaterial.js +2 -2
  74. package/examples/jsm/materials/WoodNodeMaterial.js +11 -11
  75. package/examples/jsm/math/Octree.js +131 -1
  76. package/examples/jsm/misc/Volume.js +0 -1
  77. package/examples/jsm/misc/VolumeSlice.js +0 -1
  78. package/examples/jsm/objects/LensflareMesh.js +1 -1
  79. package/examples/jsm/objects/Sky.js +76 -4
  80. package/examples/jsm/objects/SkyMesh.js +127 -10
  81. package/examples/jsm/objects/Water.js +4 -3
  82. package/examples/jsm/objects/Water2.js +5 -3
  83. package/examples/jsm/objects/WaterMesh.js +5 -7
  84. package/examples/jsm/physics/AmmoPhysics.js +12 -7
  85. package/examples/jsm/physics/JoltPhysics.js +10 -6
  86. package/examples/jsm/physics/RapierPhysics.js +9 -5
  87. package/examples/jsm/postprocessing/EffectComposer.js +7 -5
  88. package/examples/jsm/postprocessing/OutputPass.js +9 -0
  89. package/examples/jsm/postprocessing/RenderPass.js +10 -0
  90. package/examples/jsm/postprocessing/RenderTransitionPass.js +1 -1
  91. package/examples/jsm/postprocessing/UnrealBloomPass.js +48 -18
  92. package/examples/jsm/renderers/CSS3DRenderer.js +1 -1
  93. package/examples/jsm/renderers/Projector.js +268 -30
  94. package/examples/jsm/renderers/SVGRenderer.js +193 -60
  95. package/examples/jsm/shaders/GTAOShader.js +19 -6
  96. package/examples/jsm/shaders/HalftoneShader.js +12 -1
  97. package/examples/jsm/shaders/PoissonDenoiseShader.js +6 -2
  98. package/examples/jsm/shaders/SAOShader.js +17 -4
  99. package/examples/jsm/shaders/SSAOShader.js +11 -1
  100. package/examples/jsm/shaders/SSRShader.js +6 -5
  101. package/examples/jsm/shaders/UnpackDepthRGBAShader.js +2 -4
  102. package/examples/jsm/shaders/VignetteShader.js +1 -1
  103. package/examples/jsm/transpiler/AST.js +44 -0
  104. package/examples/jsm/transpiler/GLSLDecoder.js +61 -4
  105. package/examples/jsm/transpiler/ShaderToyDecoder.js +2 -0
  106. package/examples/jsm/transpiler/TSLEncoder.js +46 -3
  107. package/examples/jsm/transpiler/TranspilerUtils.js +3 -3
  108. package/examples/jsm/transpiler/WGSLEncoder.js +27 -0
  109. package/examples/jsm/tsl/display/AfterImageNode.js +1 -1
  110. package/examples/jsm/tsl/display/AnaglyphPassNode.js +458 -16
  111. package/examples/jsm/tsl/display/AnamorphicNode.js +1 -1
  112. package/examples/jsm/tsl/display/BilateralBlurNode.js +364 -0
  113. package/examples/jsm/tsl/display/BloomNode.js +16 -6
  114. package/examples/jsm/tsl/display/CRT.js +150 -0
  115. package/examples/jsm/tsl/display/DenoiseNode.js +1 -1
  116. package/examples/jsm/tsl/display/DepthOfFieldNode.js +1 -1
  117. package/examples/jsm/tsl/display/DotScreenNode.js +1 -1
  118. package/examples/jsm/tsl/display/FXAANode.js +2 -2
  119. package/examples/jsm/tsl/display/GTAONode.js +5 -4
  120. package/examples/jsm/tsl/display/GaussianBlurNode.js +11 -2
  121. package/examples/jsm/tsl/display/GodraysNode.js +624 -0
  122. package/examples/jsm/tsl/display/LensflareNode.js +1 -1
  123. package/examples/jsm/tsl/display/Lut3DNode.js +1 -1
  124. package/examples/jsm/tsl/display/OutlineNode.js +3 -3
  125. package/examples/jsm/tsl/display/ParallaxBarrierPassNode.js +2 -2
  126. package/examples/jsm/tsl/display/PixelationPassNode.js +7 -6
  127. package/examples/jsm/tsl/display/RGBShiftNode.js +2 -2
  128. package/examples/jsm/tsl/display/RetroPassNode.js +263 -0
  129. package/examples/jsm/tsl/display/SMAANode.js +2 -2
  130. package/examples/jsm/tsl/display/SSAAPassNode.js +2 -2
  131. package/examples/jsm/tsl/display/SSGINode.js +8 -20
  132. package/examples/jsm/tsl/display/SSRNode.js +8 -8
  133. package/examples/jsm/tsl/display/SSSNode.js +6 -4
  134. package/examples/jsm/tsl/display/Shape.js +29 -0
  135. package/examples/jsm/tsl/display/SobelOperatorNode.js +2 -2
  136. package/examples/jsm/tsl/display/StereoCompositePassNode.js +8 -1
  137. package/examples/jsm/tsl/display/StereoPassNode.js +1 -2
  138. package/examples/jsm/tsl/display/TRAANode.js +273 -125
  139. package/examples/jsm/tsl/display/TransitionNode.js +1 -1
  140. package/examples/jsm/tsl/display/depthAwareBlend.js +80 -0
  141. package/examples/jsm/tsl/display/radialBlur.js +68 -0
  142. package/examples/jsm/tsl/math/Bayer.js +40 -1
  143. package/examples/jsm/utils/LDrawUtils.js +1 -1
  144. package/examples/jsm/utils/ShadowMapViewer.js +24 -10
  145. package/examples/jsm/utils/ShadowMapViewerGPU.js +1 -1
  146. package/examples/jsm/utils/WebGPUTextureUtils.js +1 -1
  147. package/package.json +20 -26
  148. package/src/Three.Core.js +2 -1
  149. package/src/Three.TSL.js +19 -11
  150. package/src/Three.WebGPU.Nodes.js +2 -0
  151. package/src/Three.WebGPU.js +3 -0
  152. package/src/Three.js +1 -0
  153. package/src/animation/AnimationAction.js +1 -1
  154. package/src/animation/AnimationClip.js +1 -1
  155. package/src/animation/AnimationMixer.js +6 -0
  156. package/src/animation/AnimationUtils.js +1 -12
  157. package/src/animation/KeyframeTrack.js +47 -8
  158. package/src/animation/PropertyMixer.js +4 -4
  159. package/src/animation/tracks/BooleanKeyframeTrack.js +1 -1
  160. package/src/animation/tracks/ColorKeyframeTrack.js +1 -1
  161. package/src/animation/tracks/NumberKeyframeTrack.js +1 -1
  162. package/src/animation/tracks/QuaternionKeyframeTrack.js +1 -1
  163. package/src/animation/tracks/StringKeyframeTrack.js +1 -1
  164. package/src/animation/tracks/VectorKeyframeTrack.js +1 -1
  165. package/src/audio/Audio.js +1 -1
  166. package/src/audio/AudioListener.js +5 -3
  167. package/src/cameras/Camera.js +32 -2
  168. package/src/cameras/CubeCamera.js +20 -0
  169. package/src/constants.js +90 -5
  170. package/src/core/BufferGeometry.js +14 -2
  171. package/src/core/Clock.js +7 -0
  172. package/src/core/Object3D.js +56 -4
  173. package/src/core/Raycaster.js +2 -2
  174. package/src/core/RenderTarget.js +3 -4
  175. package/src/extras/PMREMGenerator.js +7 -18
  176. package/src/extras/TextureUtils.js +5 -1
  177. package/src/geometries/ExtrudeGeometry.js +2 -2
  178. package/src/geometries/PolyhedronGeometry.js +1 -1
  179. package/src/geometries/TorusGeometry.js +8 -3
  180. package/src/helpers/CameraHelper.js +3 -0
  181. package/src/helpers/DirectionalLightHelper.js +4 -1
  182. package/src/helpers/HemisphereLightHelper.js +3 -0
  183. package/src/helpers/PointLightHelper.js +1 -25
  184. package/src/helpers/SpotLightHelper.js +3 -0
  185. package/src/lights/DirectionalLight.js +13 -0
  186. package/src/lights/HemisphereLight.js +10 -0
  187. package/src/lights/Light.js +1 -11
  188. package/src/lights/LightProbe.js +0 -15
  189. package/src/lights/LightShadow.js +15 -6
  190. package/src/lights/PointLight.js +15 -0
  191. package/src/lights/PointLightShadow.js +0 -86
  192. package/src/lights/SpotLight.js +22 -1
  193. package/src/lights/webgpu/IESSpotLight.js +2 -1
  194. package/src/loaders/Cache.js +28 -0
  195. package/src/loaders/FileLoader.js +1 -1
  196. package/src/loaders/ImageBitmapLoader.js +8 -3
  197. package/src/loaders/Loader.js +6 -0
  198. package/src/loaders/MaterialLoader.js +2 -1
  199. package/src/loaders/ObjectLoader.js +21 -2
  200. package/src/loaders/nodes/NodeLoader.js +2 -2
  201. package/src/materials/Material.js +2 -0
  202. package/src/materials/MeshLambertMaterial.js +9 -0
  203. package/src/materials/MeshPhongMaterial.js +9 -0
  204. package/src/materials/ShaderMaterial.js +20 -1
  205. package/src/materials/nodes/Line2NodeMaterial.js +7 -7
  206. package/src/materials/nodes/MeshPhysicalNodeMaterial.js +5 -2
  207. package/src/materials/nodes/MeshStandardNodeMaterial.js +5 -4
  208. package/src/materials/nodes/NodeMaterial.js +72 -25
  209. package/src/materials/nodes/manager/NodeMaterialObserver.js +10 -4
  210. package/src/math/Line3.js +3 -5
  211. package/src/math/MathUtils.js +10 -10
  212. package/src/math/Matrix4.js +74 -65
  213. package/src/math/Quaternion.js +3 -29
  214. package/src/math/Sphere.js +1 -1
  215. package/src/math/Vector3.js +3 -5
  216. package/src/math/interpolants/BezierInterpolant.js +108 -0
  217. package/src/nodes/Nodes.js +87 -68
  218. package/src/nodes/TSL.js +6 -6
  219. package/src/nodes/accessors/Arrays.js +1 -1
  220. package/src/nodes/accessors/BatchNode.js +10 -10
  221. package/src/nodes/accessors/Bitangent.js +5 -5
  222. package/src/nodes/accessors/BufferAttributeNode.js +98 -12
  223. package/src/nodes/accessors/BufferNode.js +29 -2
  224. package/src/nodes/accessors/Camera.js +149 -28
  225. package/src/nodes/accessors/ClippingNode.js +4 -4
  226. package/src/nodes/accessors/CubeTextureNode.js +20 -1
  227. package/src/nodes/accessors/InstanceNode.js +148 -43
  228. package/src/nodes/accessors/MaterialNode.js +9 -1
  229. package/src/nodes/accessors/MaterialReferenceNode.js +1 -2
  230. package/src/nodes/accessors/ModelNode.js +1 -1
  231. package/src/nodes/accessors/Normal.js +11 -11
  232. package/src/nodes/accessors/Position.js +34 -2
  233. package/src/nodes/accessors/ReferenceBaseNode.js +4 -4
  234. package/src/nodes/accessors/ReferenceNode.js +4 -4
  235. package/src/nodes/accessors/RendererReferenceNode.js +1 -2
  236. package/src/nodes/accessors/SceneProperties.js +53 -0
  237. package/src/nodes/accessors/SkinningNode.js +27 -26
  238. package/src/nodes/accessors/StorageBufferNode.js +4 -21
  239. package/src/nodes/accessors/StorageTextureNode.js +37 -1
  240. package/src/nodes/accessors/Tangent.js +4 -14
  241. package/src/nodes/accessors/Texture3DNode.js +32 -35
  242. package/src/nodes/accessors/TextureNode.js +58 -22
  243. package/src/nodes/accessors/UniformArrayNode.js +4 -2
  244. package/src/nodes/accessors/UserDataNode.js +1 -2
  245. package/src/nodes/accessors/VertexColorNode.js +1 -2
  246. package/src/nodes/code/FunctionNode.js +1 -2
  247. package/src/nodes/core/ArrayNode.js +20 -1
  248. package/src/nodes/core/AssignNode.js +2 -2
  249. package/src/nodes/core/AttributeNode.js +2 -2
  250. package/src/nodes/core/ContextNode.js +103 -4
  251. package/src/nodes/core/MRTNode.js +48 -2
  252. package/src/nodes/core/Node.js +29 -3
  253. package/src/nodes/core/NodeBuilder.js +170 -53
  254. package/src/nodes/core/NodeError.js +28 -0
  255. package/src/nodes/core/NodeFrame.js +12 -4
  256. package/src/nodes/core/NodeUtils.js +10 -8
  257. package/src/nodes/core/OutputStructNode.js +12 -10
  258. package/src/nodes/core/ParameterNode.js +3 -3
  259. package/src/nodes/core/PropertyNode.js +19 -3
  260. package/src/nodes/core/StackNode.js +65 -16
  261. package/src/nodes/core/StackTrace.js +139 -0
  262. package/src/nodes/core/StructNode.js +16 -2
  263. package/src/nodes/core/StructTypeNode.js +11 -17
  264. package/src/nodes/core/SubBuildNode.js +1 -1
  265. package/src/nodes/core/UniformNode.js +21 -5
  266. package/src/nodes/core/VarNode.js +47 -22
  267. package/src/nodes/core/VaryingNode.js +1 -18
  268. package/src/nodes/display/BlendModes.js +0 -64
  269. package/src/nodes/display/ColorAdjustment.js +17 -0
  270. package/src/nodes/display/ColorSpaceNode.js +3 -3
  271. package/src/nodes/display/NormalMapNode.js +39 -4
  272. package/src/nodes/display/PassNode.js +98 -9
  273. package/src/nodes/display/RenderOutputNode.js +3 -3
  274. package/src/nodes/display/ScreenNode.js +3 -1
  275. package/src/nodes/display/ToneMappingNode.js +1 -1
  276. package/src/nodes/display/ToonOutlinePassNode.js +2 -2
  277. package/src/nodes/display/ViewportDepthNode.js +52 -4
  278. package/src/nodes/display/ViewportTextureNode.js +21 -4
  279. package/src/nodes/fog/Fog.js +18 -35
  280. package/src/nodes/functions/BSDF/BRDF_GGX_Multiscatter.js +3 -3
  281. package/src/nodes/functions/BSDF/DFGLUT.js +56 -0
  282. package/src/nodes/functions/BSDF/EnvironmentBRDF.js +2 -2
  283. package/src/nodes/functions/BSDF/V_GGX_SmithCorrelated_Anisotropic.js +1 -1
  284. package/src/nodes/functions/PhysicalLightingModel.js +126 -45
  285. package/src/nodes/geometry/RangeNode.js +4 -2
  286. package/src/nodes/gpgpu/ComputeBuiltinNode.js +1 -2
  287. package/src/nodes/gpgpu/ComputeNode.js +5 -4
  288. package/src/nodes/gpgpu/SubgroupFunctionNode.js +1 -1
  289. package/src/nodes/gpgpu/WorkgroupInfoNode.js +4 -4
  290. package/src/nodes/lighting/AnalyticLightNode.js +53 -0
  291. package/src/nodes/lighting/EnvironmentNode.js +28 -3
  292. package/src/nodes/lighting/LightsNode.js +2 -2
  293. package/src/nodes/lighting/PointShadowNode.js +162 -149
  294. package/src/nodes/lighting/ShadowFilterNode.js +53 -65
  295. package/src/nodes/lighting/ShadowNode.js +97 -41
  296. package/src/nodes/math/BitcountNode.js +433 -0
  297. package/src/nodes/math/ConditionalNode.js +2 -2
  298. package/src/nodes/math/MathNode.js +3 -40
  299. package/src/nodes/math/OperatorNode.js +2 -1
  300. package/src/nodes/math/PackFloatNode.js +98 -0
  301. package/src/nodes/math/UnpackFloatNode.js +96 -0
  302. package/src/nodes/pmrem/PMREMNode.js +1 -1
  303. package/src/nodes/pmrem/PMREMUtils.js +9 -15
  304. package/src/nodes/tsl/TSLCore.js +17 -14
  305. package/src/nodes/utils/ArrayElementNode.js +13 -0
  306. package/src/nodes/utils/DebugNode.js +11 -11
  307. package/src/nodes/utils/EventNode.js +1 -2
  308. package/src/nodes/utils/JoinNode.js +2 -2
  309. package/src/nodes/utils/LoopNode.js +1 -1
  310. package/src/nodes/utils/MemberNode.js +1 -1
  311. package/src/nodes/utils/Packing.js +13 -1
  312. package/src/nodes/utils/PostProcessingUtils.js +33 -1
  313. package/src/nodes/utils/RTTNode.js +1 -1
  314. package/src/nodes/utils/ReflectorNode.js +3 -4
  315. package/src/nodes/utils/SampleNode.js +1 -1
  316. package/src/nodes/utils/SpriteSheetUV.js +35 -0
  317. package/src/nodes/utils/UVUtils.js +28 -0
  318. package/src/objects/BatchedMesh.js +27 -14
  319. package/src/objects/InstancedMesh.js +11 -0
  320. package/src/objects/Line.js +1 -1
  321. package/src/objects/Mesh.js +1 -1
  322. package/src/objects/Points.js +1 -1
  323. package/src/objects/Skeleton.js +9 -0
  324. package/src/renderers/WebGLRenderer.js +178 -92
  325. package/src/renderers/common/Backend.js +29 -0
  326. package/src/renderers/common/Background.js +24 -11
  327. package/src/renderers/common/BindGroup.js +1 -9
  328. package/src/renderers/common/Binding.js +11 -0
  329. package/src/renderers/common/Bindings.js +27 -12
  330. package/src/renderers/common/BlendMode.js +143 -0
  331. package/src/renderers/common/Buffer.js +40 -0
  332. package/src/renderers/common/BundleGroup.js +1 -1
  333. package/src/renderers/common/ChainMap.js +30 -6
  334. package/src/renderers/common/CubeRenderTarget.js +50 -6
  335. package/src/renderers/common/Geometries.js +29 -3
  336. package/src/renderers/common/Lighting.js +5 -21
  337. package/src/renderers/common/Pipelines.js +4 -4
  338. package/src/renderers/common/PostProcessing.js +8 -206
  339. package/src/renderers/common/RenderBundles.js +2 -1
  340. package/src/renderers/common/RenderContext.js +16 -0
  341. package/src/renderers/common/RenderContexts.js +33 -49
  342. package/src/renderers/common/RenderLists.js +2 -1
  343. package/src/renderers/common/RenderObject.js +15 -3
  344. package/src/renderers/common/RenderObjectPipeline.js +40 -0
  345. package/src/renderers/common/RenderObjects.js +18 -2
  346. package/src/renderers/common/RenderPipeline.js +203 -17
  347. package/src/renderers/common/Renderer.js +257 -72
  348. package/src/renderers/common/Sampler.js +4 -4
  349. package/src/renderers/common/StorageBuffer.js +13 -1
  350. package/src/renderers/common/Textures.js +17 -1
  351. package/src/renderers/common/TimestampQueryPool.js +5 -3
  352. package/src/renderers/common/Uniform.js +8 -0
  353. package/src/renderers/common/UniformsGroup.js +61 -0
  354. package/src/renderers/common/XRManager.js +3 -2
  355. package/src/renderers/common/extras/PMREMGenerator.js +2 -8
  356. package/src/renderers/common/nodes/NodeBuilderState.js +1 -1
  357. package/src/renderers/common/nodes/{Nodes.js → NodeManager.js} +18 -6
  358. package/src/renderers/common/nodes/NodeStorageBuffer.js +13 -2
  359. package/src/renderers/common/nodes/NodeUniformBuffer.js +52 -0
  360. package/src/renderers/shaders/DFGLUTData.js +19 -34
  361. package/src/renderers/shaders/ShaderChunk/batching_pars_vertex.glsl.js +2 -2
  362. package/src/renderers/shaders/ShaderChunk/color_fragment.glsl.js +1 -5
  363. package/src/renderers/shaders/ShaderChunk/color_pars_fragment.glsl.js +1 -5
  364. package/src/renderers/shaders/ShaderChunk/color_pars_vertex.glsl.js +1 -5
  365. package/src/renderers/shaders/ShaderChunk/color_vertex.glsl.js +8 -10
  366. package/src/renderers/shaders/ShaderChunk/envmap_fragment.glsl.js +7 -11
  367. package/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js +5 -2
  368. package/src/renderers/shaders/ShaderChunk/lights_fragment_end.glsl.js +6 -0
  369. package/src/renderers/shaders/ShaderChunk/lights_fragment_maps.glsl.js +6 -2
  370. package/src/renderers/shaders/ShaderChunk/lights_physical_fragment.glsl.js +8 -4
  371. package/src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js +112 -51
  372. package/src/renderers/shaders/ShaderChunk/packing.glsl.js +20 -4
  373. package/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js +225 -186
  374. package/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js +1 -1
  375. package/src/renderers/shaders/ShaderChunk/transmission_fragment.glsl.js +1 -1
  376. package/src/renderers/shaders/ShaderChunk.js +3 -3
  377. package/src/renderers/shaders/ShaderLib/depth.glsl.js +3 -0
  378. package/src/renderers/shaders/ShaderLib/{distanceRGBA.glsl.js → distance.glsl.js} +1 -2
  379. package/src/renderers/shaders/ShaderLib/meshlambert.glsl.js +2 -1
  380. package/src/renderers/shaders/ShaderLib/meshnormal.glsl.js +1 -2
  381. package/src/renderers/shaders/ShaderLib/meshphong.glsl.js +2 -1
  382. package/src/renderers/shaders/ShaderLib/meshphysical.glsl.js +4 -9
  383. package/src/renderers/shaders/ShaderLib/meshtoon.glsl.js +0 -1
  384. package/src/renderers/shaders/ShaderLib/shadow.glsl.js +1 -1
  385. package/src/renderers/shaders/ShaderLib/vsm.glsl.js +4 -6
  386. package/src/renderers/shaders/ShaderLib.js +7 -5
  387. package/src/renderers/shaders/UniformsLib.js +0 -3
  388. package/src/renderers/webgl/WebGLBackground.js +2 -2
  389. package/src/renderers/webgl/WebGLBindingStates.js +99 -27
  390. package/src/renderers/webgl/WebGLCapabilities.js +3 -4
  391. package/src/renderers/webgl/WebGLEnvironments.js +228 -0
  392. package/src/renderers/webgl/WebGLGeometries.js +10 -7
  393. package/src/renderers/webgl/WebGLLights.js +18 -1
  394. package/src/renderers/webgl/WebGLMaterials.js +12 -0
  395. package/src/renderers/webgl/WebGLObjects.js +3 -1
  396. package/src/renderers/webgl/WebGLOutput.js +267 -0
  397. package/src/renderers/webgl/WebGLProgram.js +45 -109
  398. package/src/renderers/webgl/WebGLPrograms.js +45 -49
  399. package/src/renderers/webgl/WebGLRenderLists.js +15 -0
  400. package/src/renderers/webgl/WebGLShadowMap.js +188 -24
  401. package/src/renderers/webgl/WebGLState.js +32 -37
  402. package/src/renderers/webgl/WebGLTextures.js +89 -28
  403. package/src/renderers/webgl/WebGLUniforms.js +40 -3
  404. package/src/renderers/webgl/WebGLUtils.js +6 -2
  405. package/src/renderers/webgl-fallback/WebGLBackend.js +148 -18
  406. package/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js +156 -35
  407. package/src/renderers/webgl-fallback/utils/WebGLState.js +181 -5
  408. package/src/renderers/webgl-fallback/utils/WebGLTextureUtils.js +5 -3
  409. package/src/renderers/webgl-fallback/utils/WebGLTimestampQueryPool.js +9 -9
  410. package/src/renderers/webgl-fallback/utils/WebGLUtils.js +6 -2
  411. package/src/renderers/webgpu/WebGPUBackend.js +119 -13
  412. package/src/renderers/webgpu/WebGPURenderer.js +2 -1
  413. package/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +322 -68
  414. package/src/renderers/webgpu/utils/WebGPUAttributeUtils.js +4 -17
  415. package/src/renderers/webgpu/utils/WebGPUBindingUtils.js +357 -200
  416. package/src/renderers/webgpu/utils/WebGPUConstants.js +2 -0
  417. package/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +61 -23
  418. package/src/renderers/webgpu/utils/WebGPUTexturePassUtils.js +152 -200
  419. package/src/renderers/webgpu/utils/WebGPUTextureUtils.js +65 -42
  420. package/src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js +7 -7
  421. package/src/renderers/webgpu/utils/WebGPUUtils.js +17 -11
  422. package/src/renderers/webxr/WebXRManager.js +2 -2
  423. package/src/textures/CubeDepthTexture.js +76 -0
  424. package/src/textures/Source.js +1 -1
  425. package/src/textures/Texture.js +3 -3
  426. package/src/utils.js +258 -3
  427. package/examples/jsm/materials/MeshGouraudMaterial.js +0 -434
  428. package/examples/jsm/materials/MeshPostProcessingMaterial.js +0 -167
  429. package/examples/jsm/shaders/GodRaysShader.js +0 -333
  430. package/src/nodes/accessors/SceneNode.js +0 -145
  431. package/src/nodes/code/ScriptableNode.js +0 -726
  432. package/src/nodes/code/ScriptableValueNode.js +0 -253
  433. package/src/nodes/display/PosterizeNode.js +0 -65
  434. package/src/nodes/functions/BSDF/DFGApprox.js +0 -71
  435. package/src/nodes/utils/SpriteSheetUVNode.js +0 -90
  436. package/src/renderers/webgl/WebGLCubeMaps.js +0 -99
  437. package/src/renderers/webgl/WebGLCubeUVMaps.js +0 -134
@@ -0,0 +1,4041 @@
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
+
28
+ // Pre-compiled regex patterns for performance
29
+ const VARIANT_PATH_REGEX = /^(.+?)\/\{(\w+)=(\w+)\}\/(.+)$/;
30
+
31
+ // Spec types (must match USDCParser)
32
+ const SpecType = {
33
+ Unknown: 0,
34
+ Attribute: 1,
35
+ Connection: 2,
36
+ Expression: 3,
37
+ Mapper: 4,
38
+ MapperArg: 5,
39
+ Prim: 6,
40
+ PseudoRoot: 7,
41
+ Relationship: 8,
42
+ RelationshipTarget: 9,
43
+ Variant: 10,
44
+ VariantSet: 11
45
+ };
46
+
47
+ /**
48
+ * USDComposer handles scene composition from parsed USD data.
49
+ * This includes reference resolution, variant selection, transform handling,
50
+ * and building the Three.js scene graph.
51
+ *
52
+ * Works with specsByPath format from USDCParser.
53
+ */
54
+ class USDComposer {
55
+
56
+ constructor( manager = null ) {
57
+
58
+ this.textureCache = {};
59
+ this.skinnedMeshes = [];
60
+ this.manager = manager;
61
+
62
+ }
63
+
64
+ /**
65
+ * Compose a Three.js scene from parsed USD data.
66
+ * @param {Object} parsedData - Data from USDCParser or USDAParser
67
+ * @param {Object} assets - Dictionary of referenced assets (specsByPath or blob URLs)
68
+ * @param {Object} variantSelections - External variant selections
69
+ * @param {string} basePath - Base path for resolving relative references
70
+ * @returns {Group} Three.js scene graph
71
+ */
72
+ compose( parsedData, assets = {}, variantSelections = {}, basePath = '' ) {
73
+
74
+ this.specsByPath = parsedData.specsByPath;
75
+ this.assets = assets;
76
+ this.externalVariantSelections = variantSelections;
77
+ this.basePath = basePath;
78
+ this.skinnedMeshes = [];
79
+ this.skeletons = {};
80
+
81
+ // Build indexes for O(1) lookups
82
+ this._buildIndexes();
83
+
84
+ // Get FPS from root spec
85
+ const rootSpec = this.specsByPath[ '/' ];
86
+ const rootFields = rootSpec ? rootSpec.fields : {};
87
+ this.fps = rootFields.framesPerSecond || rootFields.timeCodesPerSecond || 30;
88
+
89
+ const group = new Group();
90
+ this._buildHierarchy( group, '/' );
91
+
92
+ // Bind skeletons to skinned meshes
93
+ this._bindSkeletons();
94
+
95
+ // Build animations
96
+ group.animations = this._buildAnimations();
97
+
98
+ // Handle Z-up to Y-up conversion
99
+ if ( rootSpec && rootSpec.fields && rootSpec.fields.upAxis === 'Z' ) {
100
+
101
+ group.rotation.x = - Math.PI / 2;
102
+
103
+ }
104
+
105
+ return group;
106
+
107
+ }
108
+
109
+ /**
110
+ * Apply USD transforms to a Three.js object.
111
+ * Handles xformOpOrder with proper matrix composition.
112
+ * USD uses row-vector convention, Three.js uses column-vector.
113
+ */
114
+ applyTransform( obj, fields, attrs = {} ) {
115
+
116
+ const data = { ...fields, ...attrs };
117
+ const xformOpOrder = data[ 'xformOpOrder' ];
118
+
119
+ // If we have xformOpOrder, apply transforms using matrices
120
+ if ( xformOpOrder && xformOpOrder.length > 0 ) {
121
+
122
+ const matrix = new Matrix4();
123
+ const tempMatrix = new Matrix4();
124
+
125
+ // Track scale for handling negative scale with rotation
126
+ let scaleValues = null;
127
+
128
+ // Iterate FORWARD for Three.js column-vector convention
129
+ for ( let i = 0; i < xformOpOrder.length; i ++ ) {
130
+
131
+ const op = xformOpOrder[ i ];
132
+ const isInverse = op.startsWith( '!invert!' );
133
+ const opName = isInverse ? op.slice( 8 ) : op;
134
+
135
+ if ( opName === 'xformOp:transform' ) {
136
+
137
+ const m = data[ 'xformOp:transform' ];
138
+ if ( m && m.length === 16 ) {
139
+
140
+ tempMatrix.set(
141
+ m[ 0 ], m[ 4 ], m[ 8 ], m[ 12 ],
142
+ m[ 1 ], m[ 5 ], m[ 9 ], m[ 13 ],
143
+ m[ 2 ], m[ 6 ], m[ 10 ], m[ 14 ],
144
+ m[ 3 ], m[ 7 ], m[ 11 ], m[ 15 ]
145
+ );
146
+ if ( isInverse ) tempMatrix.invert();
147
+ matrix.multiply( tempMatrix );
148
+
149
+ }
150
+
151
+ } else if ( opName === 'xformOp:translate' ) {
152
+
153
+ const t = data[ 'xformOp:translate' ];
154
+ if ( t ) {
155
+
156
+ tempMatrix.makeTranslation( t[ 0 ], t[ 1 ], t[ 2 ] );
157
+ if ( isInverse ) tempMatrix.invert();
158
+ matrix.multiply( tempMatrix );
159
+
160
+ }
161
+
162
+ } else if ( opName === 'xformOp:translate:pivot' ) {
163
+
164
+ const t = data[ 'xformOp:translate:pivot' ];
165
+ if ( t ) {
166
+
167
+ tempMatrix.makeTranslation( t[ 0 ], t[ 1 ], t[ 2 ] );
168
+ if ( isInverse ) tempMatrix.invert();
169
+ matrix.multiply( tempMatrix );
170
+
171
+ }
172
+
173
+ } else if ( opName === 'xformOp:scale' ) {
174
+
175
+ const s = data[ 'xformOp:scale' ];
176
+ if ( s ) {
177
+
178
+ if ( Array.isArray( s ) ) {
179
+
180
+ tempMatrix.makeScale( s[ 0 ], s[ 1 ], s[ 2 ] );
181
+ scaleValues = [ s[ 0 ], s[ 1 ], s[ 2 ] ];
182
+
183
+ } else {
184
+
185
+ tempMatrix.makeScale( s, s, s );
186
+ scaleValues = [ s, s, s ];
187
+
188
+ }
189
+
190
+ if ( isInverse ) tempMatrix.invert();
191
+ matrix.multiply( tempMatrix );
192
+
193
+ }
194
+
195
+ } else if ( opName === 'xformOp:rotateXYZ' ) {
196
+
197
+ const r = data[ 'xformOp:rotateXYZ' ];
198
+ if ( r ) {
199
+
200
+ // USD rotateXYZ: matrix = Rx * Ry * Rz
201
+ // Three.js Euler 'ZYX' order produces same result
202
+ const euler = new Euler(
203
+ r[ 0 ] * Math.PI / 180,
204
+ r[ 1 ] * Math.PI / 180,
205
+ r[ 2 ] * Math.PI / 180,
206
+ 'ZYX'
207
+ );
208
+ tempMatrix.makeRotationFromEuler( euler );
209
+ if ( isInverse ) tempMatrix.invert();
210
+ matrix.multiply( tempMatrix );
211
+
212
+ }
213
+
214
+ } else if ( opName === 'xformOp:rotateX' ) {
215
+
216
+ const r = data[ 'xformOp:rotateX' ];
217
+ if ( r !== undefined ) {
218
+
219
+ tempMatrix.makeRotationX( r * Math.PI / 180 );
220
+ if ( isInverse ) tempMatrix.invert();
221
+ matrix.multiply( tempMatrix );
222
+
223
+ }
224
+
225
+ } else if ( opName === 'xformOp:rotateY' ) {
226
+
227
+ const r = data[ 'xformOp:rotateY' ];
228
+ if ( r !== undefined ) {
229
+
230
+ tempMatrix.makeRotationY( r * Math.PI / 180 );
231
+ if ( isInverse ) tempMatrix.invert();
232
+ matrix.multiply( tempMatrix );
233
+
234
+ }
235
+
236
+ } else if ( opName === 'xformOp:rotateZ' ) {
237
+
238
+ const r = data[ 'xformOp:rotateZ' ];
239
+ if ( r !== undefined ) {
240
+
241
+ tempMatrix.makeRotationZ( r * Math.PI / 180 );
242
+ if ( isInverse ) tempMatrix.invert();
243
+ matrix.multiply( tempMatrix );
244
+
245
+ }
246
+
247
+ } else if ( opName === 'xformOp:orient' ) {
248
+
249
+ const q = data[ 'xformOp:orient' ];
250
+ if ( q && q.length === 4 ) {
251
+
252
+ const quat = new Quaternion( q[ 0 ], q[ 1 ], q[ 2 ], q[ 3 ] );
253
+ tempMatrix.makeRotationFromQuaternion( quat );
254
+ if ( isInverse ) tempMatrix.invert();
255
+ matrix.multiply( tempMatrix );
256
+
257
+ }
258
+
259
+ }
260
+
261
+ }
262
+
263
+ obj.matrix.copy( matrix );
264
+ obj.matrix.decompose( obj.position, obj.quaternion, obj.scale );
265
+
266
+ // Fix for negative scale: decompose() may absorb negative scale into quaternion
267
+ // Restore original scale signs to keep animation consistent
268
+ if ( scaleValues ) {
269
+
270
+ const negX = scaleValues[ 0 ] < 0;
271
+ const negY = scaleValues[ 1 ] < 0;
272
+ const negZ = scaleValues[ 2 ] < 0;
273
+ const negCount = ( negX ? 1 : 0 ) + ( negY ? 1 : 0 ) + ( negZ ? 1 : 0 );
274
+
275
+ // decompose() absorbs pairs of negative scales into rotation
276
+ // For [-1,-1,-1] → [-1,1,1], Y and Z were absorbed, flip quat.y and quat.w
277
+ if ( negCount === 3 ) {
278
+
279
+ obj.scale.set( scaleValues[ 0 ], scaleValues[ 1 ], scaleValues[ 2 ] );
280
+ obj.quaternion.set(
281
+ obj.quaternion.x,
282
+ - obj.quaternion.y,
283
+ obj.quaternion.z,
284
+ - obj.quaternion.w
285
+ );
286
+
287
+ }
288
+
289
+ }
290
+
291
+ return;
292
+
293
+ }
294
+
295
+ // Fallback: handle individual transform ops without order
296
+ if ( data[ 'xformOp:translate' ] ) {
297
+
298
+ const t = data[ 'xformOp:translate' ];
299
+ obj.position.set( t[ 0 ], t[ 1 ], t[ 2 ] );
300
+
301
+ }
302
+
303
+ if ( data[ 'xformOp:translate:pivot' ] ) {
304
+
305
+ const p = data[ 'xformOp:translate:pivot' ];
306
+ obj.pivot = new Vector3( p[ 0 ], p[ 1 ], p[ 2 ] );
307
+
308
+ }
309
+
310
+ if ( data[ 'xformOp:scale' ] ) {
311
+
312
+ const s = data[ 'xformOp:scale' ];
313
+
314
+ if ( Array.isArray( s ) ) {
315
+
316
+ obj.scale.set( s[ 0 ], s[ 1 ], s[ 2 ] );
317
+
318
+ } else {
319
+
320
+ obj.scale.set( s, s, s );
321
+
322
+ }
323
+
324
+ }
325
+
326
+ if ( data[ 'xformOp:rotateXYZ' ] ) {
327
+
328
+ const r = data[ 'xformOp:rotateXYZ' ];
329
+ obj.rotation.set(
330
+ r[ 0 ] * Math.PI / 180,
331
+ r[ 1 ] * Math.PI / 180,
332
+ r[ 2 ] * Math.PI / 180
333
+ );
334
+
335
+ }
336
+
337
+ if ( data[ 'xformOp:orient' ] ) {
338
+
339
+ const q = data[ 'xformOp:orient' ];
340
+ if ( q.length === 4 ) {
341
+
342
+ obj.quaternion.set( q[ 0 ], q[ 1 ], q[ 2 ], q[ 3 ] );
343
+
344
+ }
345
+
346
+ }
347
+
348
+ }
349
+
350
+ /**
351
+ * Build indexes for efficient lookups.
352
+ * Called once during compose() to avoid O(n) scans per lookup.
353
+ */
354
+ _buildIndexes() {
355
+
356
+ // childrenByPath: parentPath -> [childName1, childName2, ...]
357
+ this.childrenByPath = new Map();
358
+
359
+ // attributesByPrimPath: primPath -> Map(attrName -> attrSpec)
360
+ this.attributesByPrimPath = new Map();
361
+
362
+ // materialsByRoot: rootPath -> [materialPath1, materialPath2, ...]
363
+ this.materialsByRoot = new Map();
364
+
365
+ // shadersByMaterialPath: materialPath -> [shaderPath1, shaderPath2, ...]
366
+ this.shadersByMaterialPath = new Map();
367
+
368
+ // geomSubsetsByMeshPath: meshPath -> [subsetPath1, subsetPath2, ...]
369
+ this.geomSubsetsByMeshPath = new Map();
370
+
371
+ for ( const path in this.specsByPath ) {
372
+
373
+ const spec = this.specsByPath[ path ];
374
+
375
+ if ( spec.specType === SpecType.Prim ) {
376
+
377
+ // Build parent-child index
378
+ const lastSlash = path.lastIndexOf( '/' );
379
+
380
+ if ( lastSlash > 0 ) {
381
+
382
+ const parentPath = path.slice( 0, lastSlash );
383
+ const childName = path.slice( lastSlash + 1 );
384
+
385
+ if ( ! this.childrenByPath.has( parentPath ) ) {
386
+
387
+ this.childrenByPath.set( parentPath, [] );
388
+
389
+ }
390
+
391
+ this.childrenByPath.get( parentPath ).push( { name: childName, path: path } );
392
+
393
+ } else if ( lastSlash === 0 && path.length > 1 ) {
394
+
395
+ // Direct child of root
396
+ const childName = path.slice( 1 );
397
+
398
+ if ( ! this.childrenByPath.has( '/' ) ) {
399
+
400
+ this.childrenByPath.set( '/', [] );
401
+
402
+ }
403
+
404
+ this.childrenByPath.get( '/' ).push( { name: childName, path: path } );
405
+
406
+ }
407
+
408
+ const typeName = spec.fields.typeName;
409
+
410
+ // Build material index
411
+ if ( typeName === 'Material' ) {
412
+
413
+ const parts = path.split( '/' );
414
+ const rootPath = parts.length > 1 ? '/' + parts[ 1 ] : '/';
415
+
416
+ if ( ! this.materialsByRoot.has( rootPath ) ) {
417
+
418
+ this.materialsByRoot.set( rootPath, [] );
419
+
420
+ }
421
+
422
+ this.materialsByRoot.get( rootPath ).push( path );
423
+
424
+ }
425
+
426
+ // Build shader index (shaders are children of materials)
427
+ if ( typeName === 'Shader' && lastSlash > 0 ) {
428
+
429
+ const materialPath = path.slice( 0, lastSlash );
430
+
431
+ if ( ! this.shadersByMaterialPath.has( materialPath ) ) {
432
+
433
+ this.shadersByMaterialPath.set( materialPath, [] );
434
+
435
+ }
436
+
437
+ this.shadersByMaterialPath.get( materialPath ).push( path );
438
+
439
+ }
440
+
441
+ // Build GeomSubset index (subsets are children of meshes)
442
+ if ( typeName === 'GeomSubset' && lastSlash > 0 ) {
443
+
444
+ const meshPath = path.slice( 0, lastSlash );
445
+
446
+ if ( ! this.geomSubsetsByMeshPath.has( meshPath ) ) {
447
+
448
+ this.geomSubsetsByMeshPath.set( meshPath, [] );
449
+
450
+ }
451
+
452
+ this.geomSubsetsByMeshPath.get( meshPath ).push( path );
453
+
454
+ }
455
+
456
+ } else if ( spec.specType === SpecType.Attribute || spec.specType === SpecType.Relationship ) {
457
+
458
+ // Build attribute index
459
+ const dotIndex = path.lastIndexOf( '.' );
460
+
461
+ if ( dotIndex > 0 ) {
462
+
463
+ const primPath = path.slice( 0, dotIndex );
464
+ const attrName = path.slice( dotIndex + 1 );
465
+
466
+ if ( ! this.attributesByPrimPath.has( primPath ) ) {
467
+
468
+ this.attributesByPrimPath.set( primPath, new Map() );
469
+
470
+ }
471
+
472
+ this.attributesByPrimPath.get( primPath ).set( attrName, spec );
473
+
474
+ }
475
+
476
+ }
477
+
478
+ }
479
+
480
+ }
481
+
482
+ /**
483
+ * Check if a path is a direct child of parentPath.
484
+ */
485
+ _isDirectChild( parentPath, path, prefix ) {
486
+
487
+ if ( ! path.startsWith( prefix ) ) return false;
488
+
489
+ const remainder = path.slice( prefix.length );
490
+ if ( remainder.length === 0 ) return false;
491
+
492
+ // Check for variant paths or simple names
493
+ if ( remainder.startsWith( '{' ) ) {
494
+
495
+ return false; // Variant paths are not direct children
496
+
497
+ }
498
+
499
+ return ! remainder.includes( '/' );
500
+
501
+ }
502
+
503
+ /**
504
+ * Build the scene hierarchy recursively.
505
+ * Uses childrenByPath index for O(1) child lookup instead of O(n) iteration.
506
+ */
507
+ _buildHierarchy( parent, parentPath ) {
508
+
509
+ // Collect children from parentPath and any active variant paths
510
+ const childEntries = [];
511
+ const seenPaths = new Set();
512
+
513
+ // Get direct children using the index
514
+ const directChildren = this.childrenByPath.get( parentPath );
515
+
516
+ if ( directChildren ) {
517
+
518
+ for ( const child of directChildren ) {
519
+
520
+ if ( ! seenPaths.has( child.path ) ) {
521
+
522
+ seenPaths.add( child.path );
523
+ childEntries.push( child );
524
+
525
+ }
526
+
527
+ }
528
+
529
+ }
530
+
531
+ // Also get children from active variant paths
532
+ const variantPaths = this._getVariantPaths( parentPath );
533
+
534
+ for ( const vp of variantPaths ) {
535
+
536
+ const variantChildren = this.childrenByPath.get( vp );
537
+
538
+ if ( variantChildren ) {
539
+
540
+ for ( const child of variantChildren ) {
541
+
542
+ if ( ! seenPaths.has( child.path ) ) {
543
+
544
+ seenPaths.add( child.path );
545
+ childEntries.push( child );
546
+
547
+ }
548
+
549
+ }
550
+
551
+ }
552
+
553
+ }
554
+
555
+ // Process each child
556
+ for ( const { name, path } of childEntries ) {
557
+
558
+ const spec = this.specsByPath[ path ];
559
+ if ( ! spec || spec.specType !== SpecType.Prim ) continue;
560
+
561
+ const typeName = spec.fields.typeName;
562
+
563
+ // Check for references/payloads
564
+ const refValue = this._getReference( spec );
565
+ if ( refValue ) {
566
+
567
+ // Get local variant selections from this prim
568
+ const localVariants = this._getLocalVariantSelections( spec.fields );
569
+
570
+ // Resolve the reference
571
+ const referencedGroup = this._resolveReference( refValue, localVariants );
572
+ if ( referencedGroup ) {
573
+
574
+ const attrs = this._getAttributes( path );
575
+
576
+ // Check if the referenced content is a single mesh (or container with single mesh)
577
+ // This handles the USDZExporter pattern: Xform references geometry file
578
+ const singleMesh = this._findSingleMesh( referencedGroup );
579
+
580
+ if ( singleMesh && ( typeName === 'Xform' || ! typeName ) ) {
581
+
582
+ // Merge the mesh into this prim
583
+ singleMesh.name = name;
584
+ this.applyTransform( singleMesh, spec.fields, attrs );
585
+
586
+ // Apply material binding from the referencing prim if present
587
+ this._applyMaterialBinding( singleMesh, path );
588
+
589
+ parent.add( singleMesh );
590
+
591
+ // Still build local children (overrides)
592
+ this._buildHierarchy( singleMesh, path );
593
+
594
+ } else {
595
+
596
+ // Create a container for the referenced content
597
+ const obj = new Object3D();
598
+ obj.name = name;
599
+ this.applyTransform( obj, spec.fields, attrs );
600
+
601
+ // Add all children from the referenced group
602
+ while ( referencedGroup.children.length > 0 ) {
603
+
604
+ obj.add( referencedGroup.children[ 0 ] );
605
+
606
+ }
607
+
608
+ parent.add( obj );
609
+
610
+ // Still build local children (overrides)
611
+ this._buildHierarchy( obj, path );
612
+
613
+ }
614
+
615
+ continue;
616
+
617
+ }
618
+
619
+ }
620
+
621
+ // Build appropriate object based on type
622
+ if ( typeName === 'SkelRoot' ) {
623
+
624
+ // Skeletal root - treat as transform but track for skeleton binding
625
+ const obj = new Object3D();
626
+ obj.name = name;
627
+ obj.userData.isSkelRoot = true;
628
+ const attrs = this._getAttributes( path );
629
+ this.applyTransform( obj, spec.fields, attrs );
630
+ parent.add( obj );
631
+ this._buildHierarchy( obj, path );
632
+
633
+ } else if ( typeName === 'Skeleton' ) {
634
+
635
+ // Build skeleton and store it
636
+ const skeleton = this._buildSkeleton( path );
637
+ if ( skeleton ) {
638
+
639
+ this.skeletons[ path ] = skeleton;
640
+
641
+ }
642
+
643
+ // Recursively build children (may contain SkelAnimation)
644
+ this._buildHierarchy( parent, path );
645
+
646
+ } else if ( typeName === 'SkelAnimation' ) {
647
+
648
+ // Skip - animations are processed separately in _buildAnimations
649
+
650
+ } else if ( typeName === 'Mesh' ) {
651
+
652
+ const obj = this._buildMesh( path, spec );
653
+ if ( obj ) {
654
+
655
+ parent.add( obj );
656
+
657
+ }
658
+
659
+ } else if ( typeName === 'Material' || typeName === 'Shader' ) {
660
+
661
+ // Skip materials/shaders, they're referenced by meshes
662
+
663
+ } else {
664
+
665
+ // Transform node, group, or unknown type
666
+ const obj = new Object3D();
667
+ obj.name = name;
668
+ const attrs = this._getAttributes( path );
669
+ this.applyTransform( obj, spec.fields, attrs );
670
+ parent.add( obj );
671
+ this._buildHierarchy( obj, path );
672
+
673
+ }
674
+
675
+ }
676
+
677
+ }
678
+
679
+ /**
680
+ * Get variant paths for a parent path based on variant selections.
681
+ */
682
+ _getVariantPaths( parentPath ) {
683
+
684
+ const parentSpec = this.specsByPath[ parentPath ];
685
+ const variantSetChildren = parentSpec?.fields?.variantSetChildren;
686
+ const variantPaths = [];
687
+
688
+ if ( ! variantSetChildren || variantSetChildren.length === 0 ) {
689
+
690
+ return variantPaths;
691
+
692
+ }
693
+
694
+ for ( const variantSetName of variantSetChildren ) {
695
+
696
+ // External selections take priority
697
+ let selectedVariant = this.externalVariantSelections[ variantSetName ] || null;
698
+
699
+ // Fall back to file's internal selection
700
+ if ( ! selectedVariant ) {
701
+
702
+ const variantSelection = parentSpec.fields.variantSelection;
703
+ selectedVariant = variantSelection ? variantSelection[ variantSetName ] : null;
704
+
705
+ }
706
+
707
+ // Fall back to first variant child
708
+ if ( ! selectedVariant ) {
709
+
710
+ const variantSetPath = parentPath + '/{' + variantSetName + '=}';
711
+ const variantSetSpec = this.specsByPath[ variantSetPath ];
712
+ if ( variantSetSpec?.fields?.variantChildren ) {
713
+
714
+ selectedVariant = variantSetSpec.fields.variantChildren[ 0 ];
715
+
716
+ }
717
+
718
+ }
719
+
720
+ if ( selectedVariant ) {
721
+
722
+ const variantPath = parentPath + '/{' + variantSetName + '=' + selectedVariant + '}';
723
+ variantPaths.push( variantPath );
724
+
725
+ }
726
+
727
+ }
728
+
729
+ return variantPaths;
730
+
731
+ }
732
+
733
+ /**
734
+ * Resolve a file path relative to basePath.
735
+ */
736
+ _resolveFilePath( refPath ) {
737
+
738
+ let cleanPath = refPath;
739
+
740
+ // Remove ./ prefix
741
+ if ( cleanPath.startsWith( './' ) ) {
742
+
743
+ cleanPath = cleanPath.slice( 2 );
744
+
745
+ }
746
+
747
+ // Combine with base path
748
+ if ( this.basePath ) {
749
+
750
+ return this.basePath + '/' + cleanPath;
751
+
752
+ }
753
+
754
+ return cleanPath;
755
+
756
+ }
757
+
758
+ /**
759
+ * Resolve a USD reference and return the composed content.
760
+ * @param {string} refValue - Reference value like "@./path/to/file.usdc@"
761
+ * @param {Object} localVariants - Variant selections to apply
762
+ * @returns {Group|null} Composed content or null
763
+ */
764
+ _resolveReference( refValue, localVariants = {} ) {
765
+
766
+ if ( ! refValue ) return null;
767
+
768
+ const match = refValue.match( /@([^@]+)@(?:<([^>]+)>)?/ );
769
+ if ( ! match ) return null;
770
+
771
+ const filePath = match[ 1 ];
772
+ const primPath = match[ 2 ]; // e.g., "/Geometry"
773
+
774
+ const resolvedPath = this._resolveFilePath( filePath );
775
+
776
+ // Merge variant selections - external takes priority, then local
777
+ const mergedVariants = { ...localVariants, ...this.externalVariantSelections };
778
+
779
+ // Look up pre-parsed data in assets
780
+ const referencedData = this.assets[ resolvedPath ];
781
+ if ( ! referencedData ) return null;
782
+
783
+ // If it's specsByPath data, compose it
784
+ if ( referencedData.specsByPath ) {
785
+
786
+ const composer = new USDComposer( this.manager );
787
+ const newBasePath = this._getBasePath( resolvedPath );
788
+ const composedGroup = composer.compose( referencedData, this.assets, mergedVariants, newBasePath );
789
+
790
+ // If a primPath is specified, find and return just that subtree
791
+ if ( primPath ) {
792
+
793
+ const primName = primPath.split( '/' ).pop();
794
+
795
+ // Find the direct child with this name (not a deep search)
796
+ // This is important because there may be multiple objects with the same name
797
+ let targetObject = null;
798
+ for ( const child of composedGroup.children ) {
799
+
800
+ if ( child.name === primName ) {
801
+
802
+ targetObject = child;
803
+ break;
804
+
805
+ }
806
+
807
+ }
808
+
809
+ if ( targetObject ) {
810
+
811
+ // Detach from parent for re-parenting
812
+ composedGroup.remove( targetObject );
813
+
814
+ // Wrap in a group to maintain consistent return type
815
+ const wrapper = new Group();
816
+ wrapper.add( targetObject );
817
+ return wrapper;
818
+
819
+ }
820
+
821
+ }
822
+
823
+ return composedGroup;
824
+
825
+ }
826
+
827
+ // If it's already a Three.js Group (legacy support), clone it
828
+ if ( referencedData.isGroup || referencedData.isObject3D ) {
829
+
830
+ return referencedData.clone();
831
+
832
+ }
833
+
834
+ return null;
835
+
836
+ }
837
+
838
+ /**
839
+ * Find a single mesh in the group's shallow hierarchy.
840
+ * Only returns a mesh if it's at depth 0 or 1, not deeply nested.
841
+ * This preserves transforms in complex hierarchies like Kitchen Set
842
+ * while supporting USDZExporter round-trip (Xform > Xform > Mesh pattern).
843
+ */
844
+ _findSingleMesh( group ) {
845
+
846
+ // Check direct children first
847
+ for ( const child of group.children ) {
848
+
849
+ if ( child.isMesh ) {
850
+
851
+ group.remove( child );
852
+ return child;
853
+
854
+ }
855
+
856
+ }
857
+
858
+ // Check grandchildren (USDZExporter pattern: Xform > Geometry > Mesh)
859
+ // Only if there's exactly one child with exactly one grandchild
860
+ if ( group.children.length === 1 ) {
861
+
862
+ const child = group.children[ 0 ];
863
+
864
+ if ( child.children && child.children.length === 1 ) {
865
+
866
+ const grandchild = child.children[ 0 ];
867
+
868
+ if ( grandchild.isMesh && ! this._hasNonIdentityTransform( child ) ) {
869
+
870
+ // Safe to merge - intermediate has identity transform
871
+ child.remove( grandchild );
872
+ return grandchild;
873
+
874
+ }
875
+
876
+ }
877
+
878
+ }
879
+
880
+ return null;
881
+
882
+ }
883
+
884
+ /**
885
+ * Check if an object has a non-identity local transform.
886
+ */
887
+ _hasNonIdentityTransform( obj ) {
888
+
889
+ const pos = obj.position;
890
+ const rot = obj.rotation;
891
+ const scale = obj.scale;
892
+
893
+ const hasPosition = pos.x !== 0 || pos.y !== 0 || pos.z !== 0;
894
+ const hasRotation = rot.x !== 0 || rot.y !== 0 || rot.z !== 0;
895
+ const hasScale = scale.x !== 1 || scale.y !== 1 || scale.z !== 1;
896
+
897
+ return hasPosition || hasRotation || hasScale;
898
+
899
+ }
900
+
901
+ /**
902
+ * Get the base path (directory) from a file path.
903
+ */
904
+ _getBasePath( filePath ) {
905
+
906
+ const lastSlash = filePath.lastIndexOf( '/' );
907
+ return lastSlash >= 0 ? filePath.slice( 0, lastSlash ) : '';
908
+
909
+ }
910
+
911
+ /**
912
+ * Extract variant selections from a spec's fields.
913
+ */
914
+ _getLocalVariantSelections( fields ) {
915
+
916
+ const variants = {};
917
+
918
+ if ( fields.variantSelection ) {
919
+
920
+ for ( const key in fields.variantSelection ) {
921
+
922
+ variants[ key ] = fields.variantSelection[ key ];
923
+
924
+ }
925
+
926
+ }
927
+
928
+ return variants;
929
+
930
+ }
931
+
932
+ /**
933
+ * Get reference value from a prim spec.
934
+ */
935
+ _getReference( spec ) {
936
+
937
+ if ( spec.fields.references && spec.fields.references.length > 0 ) {
938
+
939
+ const ref = spec.fields.references[ 0 ];
940
+ if ( typeof ref === 'string' ) return ref;
941
+ if ( ref.assetPath ) return '@' + ref.assetPath + '@';
942
+
943
+ }
944
+
945
+ if ( spec.fields.payload ) {
946
+
947
+ const payload = spec.fields.payload;
948
+ if ( typeof payload === 'string' ) return payload;
949
+ if ( payload.assetPath ) return '@' + payload.assetPath + '@';
950
+
951
+ }
952
+
953
+ return null;
954
+
955
+ }
956
+
957
+ /**
958
+ * Get attributes for a path from attribute specs.
959
+ */
960
+ _getAttributes( path ) {
961
+
962
+ const attrs = {};
963
+
964
+ this._collectAttributesFromPath( path, attrs );
965
+
966
+ // Collect overrides from sibling variants (when path is inside a variant)
967
+ const variantMatch = path.match( VARIANT_PATH_REGEX );
968
+ if ( variantMatch ) {
969
+
970
+ const basePath = variantMatch[ 1 ];
971
+ const relativePath = variantMatch[ 4 ];
972
+ const variantPaths = this._getVariantPaths( basePath );
973
+
974
+ for ( const vp of variantPaths ) {
975
+
976
+ if ( path.startsWith( vp ) ) continue;
977
+
978
+ const overridePath = vp + '/' + relativePath;
979
+ this._collectAttributesFromPath( overridePath, attrs );
980
+
981
+ }
982
+
983
+ } else {
984
+
985
+ // Check for variant overrides at ancestor levels
986
+ const parts = path.split( '/' );
987
+ for ( let i = 1; i < parts.length - 1; i ++ ) {
988
+
989
+ const ancestorPath = parts.slice( 0, i + 1 ).join( '/' );
990
+ const relativePath = parts.slice( i + 1 ).join( '/' );
991
+ const variantPaths = this._getVariantPaths( ancestorPath );
992
+
993
+ for ( const vp of variantPaths ) {
994
+
995
+ const overridePath = vp + '/' + relativePath;
996
+ this._collectAttributesFromPath( overridePath, attrs );
997
+
998
+ }
999
+
1000
+ }
1001
+
1002
+ }
1003
+
1004
+ return attrs;
1005
+
1006
+ }
1007
+
1008
+ _collectAttributesFromPath( path, attrs ) {
1009
+
1010
+ // Use the attribute index for O(1) lookup instead of O(n) iteration
1011
+ const attrMap = this.attributesByPrimPath.get( path );
1012
+
1013
+ if ( ! attrMap ) return;
1014
+
1015
+ for ( const [ attrName, attrSpec ] of attrMap ) {
1016
+
1017
+ if ( attrSpec.fields?.default !== undefined ) {
1018
+
1019
+ attrs[ attrName ] = attrSpec.fields.default;
1020
+
1021
+ } else if ( attrSpec.fields?.timeSamples ) {
1022
+
1023
+ // For animated attributes without default, use the first time sample (rest pose)
1024
+ const { times, values } = attrSpec.fields.timeSamples;
1025
+ if ( times && values && times.length > 0 ) {
1026
+
1027
+ // Find time 0, or use the first available time
1028
+ const idx = times.indexOf( 0 );
1029
+ attrs[ attrName ] = idx >= 0 ? values[ idx ] : values[ 0 ];
1030
+
1031
+ }
1032
+
1033
+ }
1034
+
1035
+ if ( attrSpec.fields?.elementSize !== undefined ) {
1036
+
1037
+ attrs[ attrName + ':elementSize' ] = attrSpec.fields.elementSize;
1038
+
1039
+ }
1040
+
1041
+ if ( attrName.startsWith( 'primvars:' ) && attrSpec.fields?.typeName !== undefined ) {
1042
+
1043
+ attrs[ attrName + ':typeName' ] = attrSpec.fields.typeName;
1044
+
1045
+ }
1046
+
1047
+ }
1048
+
1049
+ }
1050
+
1051
+ /**
1052
+ * Build a mesh from a Mesh spec.
1053
+ */
1054
+ _buildMesh( path, spec ) {
1055
+
1056
+ const attrs = this._getAttributes( path );
1057
+
1058
+ // Check for skinning data
1059
+ const jointIndices = attrs[ 'primvars:skel:jointIndices' ];
1060
+ const jointWeights = attrs[ 'primvars:skel:jointWeights' ];
1061
+ const hasSkinning = jointIndices && jointWeights &&
1062
+ jointIndices.length > 0 && jointWeights.length > 0;
1063
+
1064
+ // Collect GeomSubsets for multi-material support
1065
+ const geomSubsets = this._getGeomSubsets( path );
1066
+
1067
+ let geometry, material;
1068
+
1069
+ if ( geomSubsets.length > 0 ) {
1070
+
1071
+ geometry = this._buildGeometryWithSubsets( attrs, geomSubsets, hasSkinning );
1072
+
1073
+ const meshMaterialPath = this._getMaterialPath( path, spec.fields );
1074
+
1075
+ material = geomSubsets.map( subset => {
1076
+
1077
+ const matPath = subset.materialPath || meshMaterialPath;
1078
+ return this._buildMaterialForPath( matPath );
1079
+
1080
+ } );
1081
+
1082
+ } else {
1083
+
1084
+ geometry = this._buildGeometry( path, attrs, hasSkinning );
1085
+ material = this._buildMaterial( path, spec.fields );
1086
+
1087
+ }
1088
+
1089
+ const displayColor = attrs[ 'primvars:displayColor' ];
1090
+ if ( displayColor && displayColor.length >= 3 ) {
1091
+
1092
+ const applyDisplayColor = ( mat ) => {
1093
+
1094
+ if ( mat.color && mat.color.r === 1 && mat.color.g === 1 && mat.color.b === 1 && ! mat.map ) {
1095
+
1096
+ mat.color.setRGB( displayColor[ 0 ], displayColor[ 1 ], displayColor[ 2 ], SRGBColorSpace );
1097
+
1098
+ }
1099
+
1100
+ };
1101
+
1102
+ if ( Array.isArray( material ) ) {
1103
+
1104
+ material.forEach( applyDisplayColor );
1105
+
1106
+ } else {
1107
+
1108
+ applyDisplayColor( material );
1109
+
1110
+ }
1111
+
1112
+ }
1113
+
1114
+ const displayOpacity = attrs[ 'primvars:displayOpacity' ];
1115
+ if ( displayOpacity && displayOpacity.length >= 1 ) {
1116
+
1117
+ const opacity = displayOpacity[ 0 ];
1118
+
1119
+ const applyDisplayOpacity = ( mat ) => {
1120
+
1121
+ if ( opacity < 1 ) {
1122
+
1123
+ mat.opacity = opacity;
1124
+ mat.transparent = true;
1125
+
1126
+ }
1127
+
1128
+ };
1129
+
1130
+ if ( Array.isArray( material ) ) {
1131
+
1132
+ material.forEach( applyDisplayOpacity );
1133
+
1134
+ } else {
1135
+
1136
+ applyDisplayOpacity( material );
1137
+
1138
+ }
1139
+
1140
+ }
1141
+
1142
+ let mesh;
1143
+
1144
+ if ( hasSkinning ) {
1145
+
1146
+ mesh = new SkinnedMesh( geometry, material );
1147
+
1148
+ // Find skeleton path from skel:skeleton relationship
1149
+ let skelBindingSpec = this.specsByPath[ path + '.skel:skeleton' ];
1150
+ if ( ! skelBindingSpec ) {
1151
+
1152
+ skelBindingSpec = this.specsByPath[ path + '.rel skel:skeleton' ];
1153
+
1154
+ }
1155
+
1156
+ let skeletonPath = null;
1157
+
1158
+ if ( skelBindingSpec ) {
1159
+
1160
+ if ( skelBindingSpec.fields.targetPaths && skelBindingSpec.fields.targetPaths.length > 0 ) {
1161
+
1162
+ skeletonPath = skelBindingSpec.fields.targetPaths[ 0 ];
1163
+
1164
+ } else if ( skelBindingSpec.fields.default ) {
1165
+
1166
+ skeletonPath = skelBindingSpec.fields.default.replace( /<|>/g, '' );
1167
+
1168
+ }
1169
+
1170
+ }
1171
+
1172
+ // Get per-mesh joint mapping
1173
+ const localJoints = attrs[ 'skel:joints' ];
1174
+
1175
+ // Get geomBindTransform if present
1176
+ const geomBindTransform = attrs[ 'primvars:skel:geomBindTransform' ];
1177
+
1178
+ this.skinnedMeshes.push( { mesh, skeletonPath, path, localJoints, geomBindTransform } );
1179
+
1180
+ } else {
1181
+
1182
+ mesh = new Mesh( geometry, material );
1183
+
1184
+ }
1185
+
1186
+ mesh.name = path.split( '/' ).pop();
1187
+ this.applyTransform( mesh, spec.fields, attrs );
1188
+
1189
+ return mesh;
1190
+
1191
+ }
1192
+
1193
+ _getGeomSubsets( meshPath ) {
1194
+
1195
+ const subsets = [];
1196
+ const subsetPaths = this.geomSubsetsByMeshPath.get( meshPath );
1197
+ if ( ! subsetPaths ) return subsets;
1198
+
1199
+ for ( const p of subsetPaths ) {
1200
+
1201
+ const attrs = this._getAttributes( p );
1202
+ const indices = attrs[ 'indices' ];
1203
+ if ( ! indices || indices.length === 0 ) continue;
1204
+
1205
+ // Get material binding - check direct path and variant paths
1206
+ let materialPath = this._getMaterialBindingTarget( p );
1207
+
1208
+ subsets.push( {
1209
+ name: p.split( '/' ).pop(),
1210
+ indices: indices,
1211
+ materialPath: materialPath
1212
+ } );
1213
+
1214
+ }
1215
+
1216
+ return subsets;
1217
+
1218
+ }
1219
+
1220
+ /**
1221
+ * Get material binding target path, checking variant paths if needed.
1222
+ */
1223
+ _getMaterialBindingTarget( primPath ) {
1224
+
1225
+ const attrName = 'material:binding';
1226
+
1227
+ // First check direct path
1228
+ const directPath = primPath + '.' + attrName;
1229
+ const directSpec = this.specsByPath[ directPath ];
1230
+ if ( directSpec?.fields?.targetPaths?.length > 0 ) {
1231
+
1232
+ return directSpec.fields.targetPaths[ 0 ];
1233
+
1234
+ }
1235
+
1236
+ // Check variant paths at ancestor levels
1237
+ const parts = primPath.split( '/' );
1238
+ for ( let i = 1; i < parts.length; i ++ ) {
1239
+
1240
+ const ancestorPath = parts.slice( 0, i + 1 ).join( '/' );
1241
+ const relativePath = parts.slice( i + 1 ).join( '/' );
1242
+ const variantPaths = this._getVariantPaths( ancestorPath );
1243
+
1244
+ for ( const vp of variantPaths ) {
1245
+
1246
+ const overridePath = relativePath ? vp + '/' + relativePath + '.' + attrName : vp + '.' + attrName;
1247
+ const overrideSpec = this.specsByPath[ overridePath ];
1248
+
1249
+ if ( overrideSpec?.fields?.targetPaths?.length > 0 ) {
1250
+
1251
+ return overrideSpec.fields.targetPaths[ 0 ];
1252
+
1253
+ }
1254
+
1255
+ }
1256
+
1257
+ }
1258
+
1259
+ return null;
1260
+
1261
+ }
1262
+
1263
+ _buildGeometry( path, fields, hasSkinning = false ) {
1264
+
1265
+ const geometry = new BufferGeometry();
1266
+
1267
+ const points = fields[ 'points' ];
1268
+ if ( ! points || points.length === 0 ) return geometry;
1269
+
1270
+ const faceVertexIndices = fields[ 'faceVertexIndices' ];
1271
+ const faceVertexCounts = fields[ 'faceVertexCounts' ];
1272
+
1273
+ // Parse polygon holes (Arnold format: [holeFaceIdx, parentFaceIdx, ...])
1274
+ const polygonHoles = fields[ 'primvars:arnold:polygon_holes' ];
1275
+ const holeMap = this._buildHoleMap( polygonHoles );
1276
+
1277
+ // Compute triangulation pattern once using actual vertex positions
1278
+ // This pattern will be reused for normals, UVs, etc.
1279
+ let indices = faceVertexIndices;
1280
+ let triPattern = null;
1281
+
1282
+ if ( faceVertexCounts && faceVertexCounts.length > 0 ) {
1283
+
1284
+ const result = this._triangulateIndicesWithPattern( faceVertexIndices, faceVertexCounts, points, holeMap );
1285
+ indices = result.indices;
1286
+ triPattern = result.pattern;
1287
+
1288
+ }
1289
+
1290
+ let positions = points;
1291
+ if ( indices && indices.length > 0 ) {
1292
+
1293
+ positions = this._expandAttribute( points, indices, 3 );
1294
+
1295
+ }
1296
+
1297
+ geometry.setAttribute( 'position', new BufferAttribute( new Float32Array( positions ), 3 ) );
1298
+
1299
+ const normals = fields[ 'normals' ] || fields[ 'primvars:normals' ];
1300
+ const normalIndicesRaw = fields[ 'normals:indices' ] || fields[ 'primvars:normals:indices' ];
1301
+
1302
+ if ( normals && normals.length > 0 ) {
1303
+
1304
+ let normalData = normals;
1305
+
1306
+ if ( normalIndicesRaw && normalIndicesRaw.length > 0 && triPattern ) {
1307
+
1308
+ // Indexed normals - apply triangulation pattern to indices
1309
+ const triangulatedNormalIndices = this._applyTriangulationPattern( normalIndicesRaw, triPattern );
1310
+ normalData = this._expandAttribute( normals, triangulatedNormalIndices, 3 );
1311
+
1312
+ } else if ( normals.length === points.length ) {
1313
+
1314
+ // Per-vertex normals
1315
+ if ( indices && indices.length > 0 ) {
1316
+
1317
+ normalData = this._expandAttribute( normals, indices, 3 );
1318
+
1319
+ }
1320
+
1321
+ } else if ( triPattern ) {
1322
+
1323
+ // Per-face-vertex normals (no separate indices) - use same triangulation pattern
1324
+ const normalIndices = this._applyTriangulationPattern(
1325
+ Array.from( { length: normals.length / 3 }, ( _, i ) => i ),
1326
+ triPattern
1327
+ );
1328
+ normalData = this._expandAttribute( normals, normalIndices, 3 );
1329
+
1330
+ }
1331
+
1332
+ geometry.setAttribute( 'normal', new BufferAttribute( new Float32Array( normalData ), 3 ) );
1333
+
1334
+ } else {
1335
+
1336
+ geometry.computeVertexNormals();
1337
+
1338
+ }
1339
+
1340
+ const { uvs, uvIndices } = this._findUVPrimvar( fields );
1341
+ const numFaceVertices = faceVertexIndices ? faceVertexIndices.length : 0;
1342
+
1343
+ if ( uvs && uvs.length > 0 ) {
1344
+
1345
+ let uvData = uvs;
1346
+
1347
+ if ( uvIndices && uvIndices.length > 0 && triPattern ) {
1348
+
1349
+ const triangulatedUvIndices = this._applyTriangulationPattern( uvIndices, triPattern );
1350
+ uvData = this._expandAttribute( uvs, triangulatedUvIndices, 2 );
1351
+
1352
+ } else if ( indices && uvs.length / 2 === points.length / 3 ) {
1353
+
1354
+ uvData = this._expandAttribute( uvs, indices, 2 );
1355
+
1356
+ } else if ( triPattern && uvs.length / 2 === numFaceVertices ) {
1357
+
1358
+ // Per-face-vertex UVs (faceVarying, no separate indices)
1359
+ const uvIndicesFromPattern = this._applyTriangulationPattern(
1360
+ Array.from( { length: numFaceVertices }, ( _, i ) => i ),
1361
+ triPattern
1362
+ );
1363
+ uvData = this._expandAttribute( uvs, uvIndicesFromPattern, 2 );
1364
+
1365
+ }
1366
+
1367
+ geometry.setAttribute( 'uv', new BufferAttribute( new Float32Array( uvData ), 2 ) );
1368
+
1369
+ }
1370
+
1371
+ // Second UV set (st1) for lightmaps/AO
1372
+ const { uvs2, uv2Indices } = this._findUV2Primvar( fields );
1373
+
1374
+ if ( uvs2 && uvs2.length > 0 ) {
1375
+
1376
+ let uv2Data = uvs2;
1377
+
1378
+ if ( uv2Indices && uv2Indices.length > 0 && triPattern ) {
1379
+
1380
+ const triangulatedUv2Indices = this._applyTriangulationPattern( uv2Indices, triPattern );
1381
+ uv2Data = this._expandAttribute( uvs2, triangulatedUv2Indices, 2 );
1382
+
1383
+ } else if ( indices && uvs2.length / 2 === points.length / 3 ) {
1384
+
1385
+ uv2Data = this._expandAttribute( uvs2, indices, 2 );
1386
+
1387
+ } else if ( triPattern && uvs2.length / 2 === numFaceVertices ) {
1388
+
1389
+ // Per-face-vertex UV2 (faceVarying, no separate indices)
1390
+ const uv2IndicesFromPattern = this._applyTriangulationPattern(
1391
+ Array.from( { length: numFaceVertices }, ( _, i ) => i ),
1392
+ triPattern
1393
+ );
1394
+ uv2Data = this._expandAttribute( uvs2, uv2IndicesFromPattern, 2 );
1395
+
1396
+ }
1397
+
1398
+ geometry.setAttribute( 'uv1', new BufferAttribute( new Float32Array( uv2Data ), 2 ) );
1399
+
1400
+ }
1401
+
1402
+ // Add skinning attributes
1403
+ if ( hasSkinning ) {
1404
+
1405
+ const jointIndices = fields[ 'primvars:skel:jointIndices' ];
1406
+ const jointWeights = fields[ 'primvars:skel:jointWeights' ];
1407
+ const elementSize = fields[ 'primvars:skel:jointIndices:elementSize' ] || 4;
1408
+
1409
+ if ( jointIndices && jointWeights ) {
1410
+
1411
+ const numVertices = positions.length / 3;
1412
+
1413
+ let skinIndexData, skinWeightData;
1414
+
1415
+ if ( indices && indices.length > 0 ) {
1416
+
1417
+ skinIndexData = this._expandAttribute( jointIndices, indices, elementSize );
1418
+ skinWeightData = this._expandAttribute( jointWeights, indices, elementSize );
1419
+
1420
+ } else {
1421
+
1422
+ skinIndexData = jointIndices;
1423
+ skinWeightData = jointWeights;
1424
+
1425
+ }
1426
+
1427
+ const skinIndices = new Uint16Array( numVertices * 4 );
1428
+ const skinWeights = new Float32Array( numVertices * 4 );
1429
+
1430
+ for ( let i = 0; i < numVertices; i ++ ) {
1431
+
1432
+ for ( let j = 0; j < 4; j ++ ) {
1433
+
1434
+ if ( j < elementSize ) {
1435
+
1436
+ skinIndices[ i * 4 + j ] = skinIndexData[ i * elementSize + j ] || 0;
1437
+ skinWeights[ i * 4 + j ] = skinWeightData[ i * elementSize + j ] || 0;
1438
+
1439
+ } else {
1440
+
1441
+ skinIndices[ i * 4 + j ] = 0;
1442
+ skinWeights[ i * 4 + j ] = 0;
1443
+
1444
+ }
1445
+
1446
+ }
1447
+
1448
+ }
1449
+
1450
+ geometry.setAttribute( 'skinIndex', new BufferAttribute( skinIndices, 4 ) );
1451
+ geometry.setAttribute( 'skinWeight', new BufferAttribute( skinWeights, 4 ) );
1452
+
1453
+ }
1454
+
1455
+ }
1456
+
1457
+ return geometry;
1458
+
1459
+ }
1460
+
1461
+ _buildGeometryWithSubsets( fields, geomSubsets, hasSkinning = false ) {
1462
+
1463
+ const geometry = new BufferGeometry();
1464
+
1465
+ const points = fields[ 'points' ];
1466
+ if ( ! points || points.length === 0 ) return geometry;
1467
+
1468
+ const faceVertexIndices = fields[ 'faceVertexIndices' ];
1469
+ const faceVertexCounts = fields[ 'faceVertexCounts' ];
1470
+
1471
+ if ( ! faceVertexCounts || faceVertexCounts.length === 0 ) return geometry;
1472
+
1473
+ const polygonHoles = fields[ 'primvars:arnold:polygon_holes' ];
1474
+ const holeMap = this._buildHoleMap( polygonHoles );
1475
+ const holeFaces = holeMap.holeFaces;
1476
+ const parentToHoles = holeMap.parentToHoles;
1477
+
1478
+ const { uvs, uvIndices } = this._findUVPrimvar( fields );
1479
+ const { uvs2, uv2Indices } = this._findUV2Primvar( fields );
1480
+ const normals = fields[ 'normals' ] || fields[ 'primvars:normals' ];
1481
+ const normalIndicesRaw = fields[ 'normals:indices' ] || fields[ 'primvars:normals:indices' ];
1482
+
1483
+ const jointIndices = hasSkinning ? fields[ 'primvars:skel:jointIndices' ] : null;
1484
+ const jointWeights = hasSkinning ? fields[ 'primvars:skel:jointWeights' ] : null;
1485
+ const elementSize = fields[ 'primvars:skel:jointIndices:elementSize' ] || 4;
1486
+
1487
+ // Build face-to-triangle mapping (accounting for holes)
1488
+ const faceTriangleOffset = [];
1489
+ let triangleCount = 0;
1490
+
1491
+ for ( let i = 0; i < faceVertexCounts.length; i ++ ) {
1492
+
1493
+ faceTriangleOffset.push( triangleCount );
1494
+
1495
+ // Skip hole faces - they're triangulated with their parent
1496
+ if ( holeFaces.has( i ) ) continue;
1497
+
1498
+ const count = faceVertexCounts[ i ];
1499
+ const holes = parentToHoles.get( i );
1500
+
1501
+ if ( holes && holes.length > 0 ) {
1502
+
1503
+ // For faces with holes, count triangles based on total vertices
1504
+ // Earcut produces (total_vertices - 2) triangles for any polygon including holes
1505
+ let totalVerts = count;
1506
+ for ( const holeIdx of holes ) {
1507
+
1508
+ totalVerts += faceVertexCounts[ holeIdx ];
1509
+
1510
+ }
1511
+
1512
+ triangleCount += totalVerts - 2;
1513
+
1514
+ } else if ( count >= 3 ) {
1515
+
1516
+ triangleCount += count - 2;
1517
+
1518
+ }
1519
+
1520
+ }
1521
+
1522
+ const triangleToSubset = new Int32Array( triangleCount ).fill( - 1 );
1523
+
1524
+ for ( let si = 0; si < geomSubsets.length; si ++ ) {
1525
+
1526
+ const subset = geomSubsets[ si ];
1527
+
1528
+ for ( let i = 0; i < subset.indices.length; i ++ ) {
1529
+
1530
+ const faceIdx = subset.indices[ i ];
1531
+ if ( faceIdx >= faceVertexCounts.length ) continue;
1532
+
1533
+ const triStart = faceTriangleOffset[ faceIdx ];
1534
+ const triCount = faceVertexCounts[ faceIdx ] - 2;
1535
+
1536
+ for ( let t = 0; t < triCount; t ++ ) {
1537
+
1538
+ triangleToSubset[ triStart + t ] = si;
1539
+
1540
+ }
1541
+
1542
+ }
1543
+
1544
+ }
1545
+
1546
+ // Sort triangles by subset
1547
+ const sortedTriangles = [];
1548
+
1549
+ for ( let tri = 0; tri < triangleCount; tri ++ ) {
1550
+
1551
+ sortedTriangles.push( { original: tri, subset: triangleToSubset[ tri ] } );
1552
+
1553
+ }
1554
+
1555
+ sortedTriangles.sort( ( a, b ) => a.subset - b.subset );
1556
+
1557
+ const groups = [];
1558
+ let currentSubset = sortedTriangles.length > 0 ? sortedTriangles[ 0 ].subset : - 1;
1559
+ let groupStart = 0;
1560
+
1561
+ for ( let i = 0; i < sortedTriangles.length; i ++ ) {
1562
+
1563
+ if ( sortedTriangles[ i ].subset !== currentSubset ) {
1564
+
1565
+ if ( currentSubset >= 0 ) {
1566
+
1567
+ groups.push( {
1568
+ start: groupStart * 3,
1569
+ count: ( i - groupStart ) * 3,
1570
+ materialIndex: currentSubset
1571
+ } );
1572
+
1573
+ }
1574
+
1575
+ currentSubset = sortedTriangles[ i ].subset;
1576
+ groupStart = i;
1577
+
1578
+ }
1579
+
1580
+ }
1581
+
1582
+ if ( currentSubset >= 0 && sortedTriangles.length > groupStart ) {
1583
+
1584
+ groups.push( {
1585
+ start: groupStart * 3,
1586
+ count: ( sortedTriangles.length - groupStart ) * 3,
1587
+ materialIndex: currentSubset
1588
+ } );
1589
+
1590
+ }
1591
+
1592
+ for ( const group of groups ) {
1593
+
1594
+ geometry.addGroup( group.start, group.count, group.materialIndex );
1595
+
1596
+ }
1597
+
1598
+ // Triangulate original data using consistent pattern
1599
+ const { indices: origIndices, pattern: triPattern } = this._triangulateIndicesWithPattern( faceVertexIndices, faceVertexCounts, points, holeMap );
1600
+ const origUvIndices = uvIndices ? this._applyTriangulationPattern( uvIndices, triPattern ) : null;
1601
+ const origUv2Indices = uv2Indices ? this._applyTriangulationPattern( uv2Indices, triPattern ) : null;
1602
+
1603
+ const numFaceVertices = faceVertexCounts.reduce( ( a, b ) => a + b, 0 );
1604
+ const hasIndexedNormals = normals && normalIndicesRaw && normalIndicesRaw.length > 0;
1605
+ const hasFaceVaryingNormals = normals && normals.length / 3 === numFaceVertices;
1606
+ const origNormalIndices = hasIndexedNormals
1607
+ ? this._applyTriangulationPattern( normalIndicesRaw, triPattern )
1608
+ : ( hasFaceVaryingNormals
1609
+ ? this._applyTriangulationPattern( Array.from( { length: numFaceVertices }, ( _, i ) => i ), triPattern )
1610
+ : null );
1611
+
1612
+ // Build reordered vertex data
1613
+ const vertexCount = triangleCount * 3;
1614
+ const positions = new Float32Array( vertexCount * 3 );
1615
+ const uvData = uvs ? new Float32Array( vertexCount * 2 ) : null;
1616
+ const uv1Data = uvs2 ? new Float32Array( vertexCount * 2 ) : null;
1617
+ const normalData = normals ? new Float32Array( vertexCount * 3 ) : null;
1618
+ const skinIndexData = jointIndices ? new Uint16Array( vertexCount * 4 ) : null;
1619
+ const skinWeightData = jointWeights ? new Float32Array( vertexCount * 4 ) : null;
1620
+
1621
+ for ( let i = 0; i < sortedTriangles.length; i ++ ) {
1622
+
1623
+ const origTri = sortedTriangles[ i ].original;
1624
+
1625
+ for ( let v = 0; v < 3; v ++ ) {
1626
+
1627
+ const origIdx = origTri * 3 + v;
1628
+ const newIdx = i * 3 + v;
1629
+
1630
+ const pointIdx = origIndices[ origIdx ];
1631
+ positions[ newIdx * 3 ] = points[ pointIdx * 3 ];
1632
+ positions[ newIdx * 3 + 1 ] = points[ pointIdx * 3 + 1 ];
1633
+ positions[ newIdx * 3 + 2 ] = points[ pointIdx * 3 + 2 ];
1634
+
1635
+ if ( uvData && uvs ) {
1636
+
1637
+ if ( origUvIndices ) {
1638
+
1639
+ const uvIdx = origUvIndices[ origIdx ];
1640
+ uvData[ newIdx * 2 ] = uvs[ uvIdx * 2 ];
1641
+ uvData[ newIdx * 2 + 1 ] = uvs[ uvIdx * 2 + 1 ];
1642
+
1643
+ } else if ( uvs.length / 2 === points.length / 3 ) {
1644
+
1645
+ uvData[ newIdx * 2 ] = uvs[ pointIdx * 2 ];
1646
+ uvData[ newIdx * 2 + 1 ] = uvs[ pointIdx * 2 + 1 ];
1647
+
1648
+ }
1649
+
1650
+ }
1651
+
1652
+ if ( uv1Data && uvs2 ) {
1653
+
1654
+ if ( origUv2Indices ) {
1655
+
1656
+ const uv2Idx = origUv2Indices[ origIdx ];
1657
+ uv1Data[ newIdx * 2 ] = uvs2[ uv2Idx * 2 ];
1658
+ uv1Data[ newIdx * 2 + 1 ] = uvs2[ uv2Idx * 2 + 1 ];
1659
+
1660
+ } else if ( uvs2.length / 2 === points.length / 3 ) {
1661
+
1662
+ uv1Data[ newIdx * 2 ] = uvs2[ pointIdx * 2 ];
1663
+ uv1Data[ newIdx * 2 + 1 ] = uvs2[ pointIdx * 2 + 1 ];
1664
+
1665
+ }
1666
+
1667
+ }
1668
+
1669
+ if ( normalData && normals ) {
1670
+
1671
+ if ( origNormalIndices ) {
1672
+
1673
+ const normalIdx = origNormalIndices[ origIdx ];
1674
+ normalData[ newIdx * 3 ] = normals[ normalIdx * 3 ];
1675
+ normalData[ newIdx * 3 + 1 ] = normals[ normalIdx * 3 + 1 ];
1676
+ normalData[ newIdx * 3 + 2 ] = normals[ normalIdx * 3 + 2 ];
1677
+
1678
+ } else if ( normals.length === points.length ) {
1679
+
1680
+ normalData[ newIdx * 3 ] = normals[ pointIdx * 3 ];
1681
+ normalData[ newIdx * 3 + 1 ] = normals[ pointIdx * 3 + 1 ];
1682
+ normalData[ newIdx * 3 + 2 ] = normals[ pointIdx * 3 + 2 ];
1683
+
1684
+ }
1685
+
1686
+ }
1687
+
1688
+ if ( skinIndexData && skinWeightData && jointIndices && jointWeights ) {
1689
+
1690
+ for ( let j = 0; j < 4; j ++ ) {
1691
+
1692
+ if ( j < elementSize ) {
1693
+
1694
+ skinIndexData[ newIdx * 4 + j ] = jointIndices[ pointIdx * elementSize + j ] || 0;
1695
+ skinWeightData[ newIdx * 4 + j ] = jointWeights[ pointIdx * elementSize + j ] || 0;
1696
+
1697
+ } else {
1698
+
1699
+ skinIndexData[ newIdx * 4 + j ] = 0;
1700
+ skinWeightData[ newIdx * 4 + j ] = 0;
1701
+
1702
+ }
1703
+
1704
+ }
1705
+
1706
+ }
1707
+
1708
+ }
1709
+
1710
+ }
1711
+
1712
+ geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
1713
+
1714
+ if ( uvData ) {
1715
+
1716
+ geometry.setAttribute( 'uv', new BufferAttribute( uvData, 2 ) );
1717
+
1718
+ }
1719
+
1720
+ if ( uv1Data ) {
1721
+
1722
+ geometry.setAttribute( 'uv1', new BufferAttribute( uv1Data, 2 ) );
1723
+
1724
+ }
1725
+
1726
+ if ( normalData ) {
1727
+
1728
+ geometry.setAttribute( 'normal', new BufferAttribute( normalData, 3 ) );
1729
+
1730
+ } else {
1731
+
1732
+ geometry.computeVertexNormals();
1733
+
1734
+ }
1735
+
1736
+ if ( skinIndexData ) {
1737
+
1738
+ geometry.setAttribute( 'skinIndex', new BufferAttribute( skinIndexData, 4 ) );
1739
+
1740
+ }
1741
+
1742
+ if ( skinWeightData ) {
1743
+
1744
+ geometry.setAttribute( 'skinWeight', new BufferAttribute( skinWeightData, 4 ) );
1745
+
1746
+ }
1747
+
1748
+ return geometry;
1749
+
1750
+ }
1751
+
1752
+ _findUVPrimvar( fields ) {
1753
+
1754
+ for ( const key in fields ) {
1755
+
1756
+ if ( ! key.startsWith( 'primvars:' ) ) continue;
1757
+ if ( key.endsWith( ':typeName' ) || key.endsWith( ':elementSize' ) || key.endsWith( ':indices' ) ) continue;
1758
+ if ( key.includes( 'skel:' ) ) continue;
1759
+
1760
+ const typeName = fields[ key + ':typeName' ];
1761
+ if ( typeName && typeName.includes( 'texCoord' ) ) {
1762
+
1763
+ return {
1764
+ uvs: fields[ key ],
1765
+ uvIndices: fields[ key + ':indices' ]
1766
+ };
1767
+
1768
+ }
1769
+
1770
+ }
1771
+
1772
+ const uvs = fields[ 'primvars:st' ] || fields[ 'primvars:UVMap' ];
1773
+ const uvIndices = fields[ 'primvars:st:indices' ];
1774
+ return { uvs, uvIndices };
1775
+
1776
+ }
1777
+
1778
+ _findUV2Primvar( fields ) {
1779
+
1780
+ const uvs2 = fields[ 'primvars:st1' ];
1781
+ const uv2Indices = fields[ 'primvars:st1:indices' ];
1782
+ return { uvs2, uv2Indices };
1783
+
1784
+ }
1785
+
1786
+ _buildHoleMap( polygonHoles ) {
1787
+
1788
+ // polygonHoles is in Arnold format: [holeFaceIdx, parentFaceIdx, holeFaceIdx, parentFaceIdx, ...]
1789
+ // Returns a map: parentFaceIdx -> [holeFaceIdx1, holeFaceIdx2, ...]
1790
+ // Also returns a set of hole face indices to skip during triangulation
1791
+ if ( ! polygonHoles || polygonHoles.length === 0 ) {
1792
+
1793
+ return { parentToHoles: new Map(), holeFaces: new Set() };
1794
+
1795
+ }
1796
+
1797
+ const parentToHoles = new Map();
1798
+ const holeFaces = new Set();
1799
+
1800
+ for ( let i = 0; i < polygonHoles.length; i += 2 ) {
1801
+
1802
+ const holeFaceIdx = polygonHoles[ i ];
1803
+ const parentFaceIdx = polygonHoles[ i + 1 ];
1804
+
1805
+ holeFaces.add( holeFaceIdx );
1806
+
1807
+ if ( ! parentToHoles.has( parentFaceIdx ) ) {
1808
+
1809
+ parentToHoles.set( parentFaceIdx, [] );
1810
+
1811
+ }
1812
+
1813
+ parentToHoles.get( parentFaceIdx ).push( holeFaceIdx );
1814
+
1815
+ }
1816
+
1817
+ return { parentToHoles, holeFaces };
1818
+
1819
+ }
1820
+
1821
+ _triangulateIndicesWithPattern( indices, counts, points = null, holeMap = null ) {
1822
+
1823
+ const triangulated = [];
1824
+ const pattern = []; // Stores face-local indices for each triangle vertex
1825
+
1826
+ // Build face offset lookup for accessing hole face data
1827
+ const faceOffsets = [];
1828
+ let offsetAccum = 0;
1829
+ for ( let i = 0; i < counts.length; i ++ ) {
1830
+
1831
+ faceOffsets.push( offsetAccum );
1832
+ offsetAccum += counts[ i ];
1833
+
1834
+ }
1835
+
1836
+ const parentToHoles = holeMap?.parentToHoles || new Map();
1837
+ const holeFaces = holeMap?.holeFaces || new Set();
1838
+
1839
+ let offset = 0;
1840
+
1841
+ for ( let i = 0; i < counts.length; i ++ ) {
1842
+
1843
+ const count = counts[ i ];
1844
+
1845
+ // Skip faces that are holes - they will be triangulated with their parent
1846
+ if ( holeFaces.has( i ) ) {
1847
+
1848
+ offset += count;
1849
+ continue;
1850
+
1851
+ }
1852
+
1853
+ // Check if this face has holes
1854
+ const holes = parentToHoles.get( i );
1855
+
1856
+ if ( holes && holes.length > 0 && points && points.length > 0 ) {
1857
+
1858
+ // Triangulate face with holes using vertex -> face-vertex mapping
1859
+ const vertexToFaceVertex = new Map();
1860
+
1861
+ const faceIndices = [];
1862
+ for ( let j = 0; j < count; j ++ ) {
1863
+
1864
+ const vertIdx = indices[ offset + j ];
1865
+ faceIndices.push( vertIdx );
1866
+ vertexToFaceVertex.set( vertIdx, offset + j );
1867
+
1868
+ }
1869
+
1870
+ const holeContours = [];
1871
+ for ( const holeFaceIdx of holes ) {
1872
+
1873
+ const holeOffset = faceOffsets[ holeFaceIdx ];
1874
+ const holeCount = counts[ holeFaceIdx ];
1875
+ const holeIndices = [];
1876
+ for ( let j = 0; j < holeCount; j ++ ) {
1877
+
1878
+ const vertIdx = indices[ holeOffset + j ];
1879
+ holeIndices.push( vertIdx );
1880
+ vertexToFaceVertex.set( vertIdx, holeOffset + j );
1881
+
1882
+ }
1883
+
1884
+ holeContours.push( holeIndices );
1885
+
1886
+ }
1887
+
1888
+ const triangles = this._triangulateNGonWithHoles( faceIndices, holeContours, points );
1889
+
1890
+ for ( const tri of triangles ) {
1891
+
1892
+ triangulated.push( tri[ 0 ], tri[ 1 ], tri[ 2 ] );
1893
+ pattern.push(
1894
+ vertexToFaceVertex.get( tri[ 0 ] ),
1895
+ vertexToFaceVertex.get( tri[ 1 ] ),
1896
+ vertexToFaceVertex.get( tri[ 2 ] )
1897
+ );
1898
+
1899
+ }
1900
+
1901
+ } else if ( count === 3 ) {
1902
+
1903
+ triangulated.push(
1904
+ indices[ offset ],
1905
+ indices[ offset + 1 ],
1906
+ indices[ offset + 2 ]
1907
+ );
1908
+ pattern.push( offset, offset + 1, offset + 2 );
1909
+
1910
+ } else if ( count === 4 ) {
1911
+
1912
+ triangulated.push(
1913
+ indices[ offset ],
1914
+ indices[ offset + 1 ],
1915
+ indices[ offset + 2 ],
1916
+ indices[ offset ],
1917
+ indices[ offset + 2 ],
1918
+ indices[ offset + 3 ]
1919
+ );
1920
+ pattern.push(
1921
+ offset, offset + 1, offset + 2,
1922
+ offset, offset + 2, offset + 3
1923
+ );
1924
+
1925
+ } else if ( count > 4 ) {
1926
+
1927
+ // Use ear-clipping for complex n-gons if we have vertex positions
1928
+ if ( points && points.length > 0 ) {
1929
+
1930
+ const faceIndices = [];
1931
+ for ( let j = 0; j < count; j ++ ) {
1932
+
1933
+ faceIndices.push( indices[ offset + j ] );
1934
+
1935
+ }
1936
+
1937
+ const triangles = this._triangulateNGon( faceIndices, points );
1938
+
1939
+ for ( const tri of triangles ) {
1940
+
1941
+ triangulated.push( tri[ 0 ], tri[ 1 ], tri[ 2 ] );
1942
+ // Find local indices within the face
1943
+ pattern.push(
1944
+ offset + faceIndices.indexOf( tri[ 0 ] ),
1945
+ offset + faceIndices.indexOf( tri[ 1 ] ),
1946
+ offset + faceIndices.indexOf( tri[ 2 ] )
1947
+ );
1948
+
1949
+ }
1950
+
1951
+ } else {
1952
+
1953
+ // Fallback to fan triangulation
1954
+ for ( let j = 1; j < count - 1; j ++ ) {
1955
+
1956
+ triangulated.push(
1957
+ indices[ offset ],
1958
+ indices[ offset + j ],
1959
+ indices[ offset + j + 1 ]
1960
+ );
1961
+ pattern.push( offset, offset + j, offset + j + 1 );
1962
+
1963
+ }
1964
+
1965
+ }
1966
+
1967
+ }
1968
+
1969
+ offset += count;
1970
+
1971
+ }
1972
+
1973
+ return { indices: triangulated, pattern };
1974
+
1975
+ }
1976
+
1977
+ _applyTriangulationPattern( indices, pattern ) {
1978
+
1979
+ const result = [];
1980
+ for ( let i = 0; i < pattern.length; i ++ ) {
1981
+
1982
+ result.push( indices[ pattern[ i ] ] );
1983
+
1984
+ }
1985
+
1986
+ return result;
1987
+
1988
+ }
1989
+
1990
+ _triangulateNGon( faceIndices, points ) {
1991
+
1992
+ // Project 3D polygon to 2D for triangulation using Newell's method for normal
1993
+ const contour2D = [];
1994
+ const contour3D = [];
1995
+
1996
+ for ( const idx of faceIndices ) {
1997
+
1998
+ contour3D.push( new Vector3(
1999
+ points[ idx * 3 ],
2000
+ points[ idx * 3 + 1 ],
2001
+ points[ idx * 3 + 2 ]
2002
+ ) );
2003
+
2004
+ }
2005
+
2006
+ // Calculate polygon normal using Newell's method
2007
+ const normal = new Vector3();
2008
+ for ( let i = 0; i < contour3D.length; i ++ ) {
2009
+
2010
+ const curr = contour3D[ i ];
2011
+ const next = contour3D[ ( i + 1 ) % contour3D.length ];
2012
+ normal.x += ( curr.y - next.y ) * ( curr.z + next.z );
2013
+ normal.y += ( curr.z - next.z ) * ( curr.x + next.x );
2014
+ normal.z += ( curr.x - next.x ) * ( curr.y + next.y );
2015
+
2016
+ }
2017
+
2018
+ normal.normalize();
2019
+
2020
+ // Create tangent basis for projection
2021
+ const tangent = new Vector3();
2022
+ const bitangent = new Vector3();
2023
+
2024
+ if ( Math.abs( normal.y ) > 0.9 ) {
2025
+
2026
+ tangent.set( 1, 0, 0 );
2027
+
2028
+ } else {
2029
+
2030
+ tangent.set( 0, 1, 0 );
2031
+
2032
+ }
2033
+
2034
+ bitangent.crossVectors( normal, tangent ).normalize();
2035
+ tangent.crossVectors( bitangent, normal ).normalize();
2036
+
2037
+ // Project to 2D
2038
+ for ( const p of contour3D ) {
2039
+
2040
+ contour2D.push( new Vector2( p.dot( tangent ), p.dot( bitangent ) ) );
2041
+
2042
+ }
2043
+
2044
+ // Triangulate using ShapeUtils
2045
+ const triangles = ShapeUtils.triangulateShape( contour2D, [] );
2046
+
2047
+ // Map back to original indices
2048
+ const result = [];
2049
+ for ( const tri of triangles ) {
2050
+
2051
+ result.push( [
2052
+ faceIndices[ tri[ 0 ] ],
2053
+ faceIndices[ tri[ 1 ] ],
2054
+ faceIndices[ tri[ 2 ] ]
2055
+ ] );
2056
+
2057
+ }
2058
+
2059
+ return result;
2060
+
2061
+ }
2062
+
2063
+ _triangulateNGonWithHoles( outerIndices, holeContours, points ) {
2064
+
2065
+ // Project 3D polygon with holes to 2D for triangulation
2066
+ const outer3D = [];
2067
+
2068
+ for ( const idx of outerIndices ) {
2069
+
2070
+ outer3D.push( new Vector3(
2071
+ points[ idx * 3 ],
2072
+ points[ idx * 3 + 1 ],
2073
+ points[ idx * 3 + 2 ]
2074
+ ) );
2075
+
2076
+ }
2077
+
2078
+ // Calculate polygon normal using Newell's method
2079
+ const normal = new Vector3();
2080
+ for ( let i = 0; i < outer3D.length; i ++ ) {
2081
+
2082
+ const curr = outer3D[ i ];
2083
+ const next = outer3D[ ( i + 1 ) % outer3D.length ];
2084
+ normal.x += ( curr.y - next.y ) * ( curr.z + next.z );
2085
+ normal.y += ( curr.z - next.z ) * ( curr.x + next.x );
2086
+ normal.z += ( curr.x - next.x ) * ( curr.y + next.y );
2087
+
2088
+ }
2089
+
2090
+ normal.normalize();
2091
+
2092
+ // Create tangent basis for projection
2093
+ const tangent = new Vector3();
2094
+ const bitangent = new Vector3();
2095
+
2096
+ if ( Math.abs( normal.y ) > 0.9 ) {
2097
+
2098
+ tangent.set( 1, 0, 0 );
2099
+
2100
+ } else {
2101
+
2102
+ tangent.set( 0, 1, 0 );
2103
+
2104
+ }
2105
+
2106
+ bitangent.crossVectors( normal, tangent ).normalize();
2107
+ tangent.crossVectors( bitangent, normal ).normalize();
2108
+
2109
+ // Project outer contour to 2D
2110
+ const outer2D = [];
2111
+ for ( const p of outer3D ) {
2112
+
2113
+ outer2D.push( new Vector2( p.dot( tangent ), p.dot( bitangent ) ) );
2114
+
2115
+ }
2116
+
2117
+ // Project hole contours to 2D
2118
+ const holes2D = [];
2119
+
2120
+ for ( const holeIndices of holeContours ) {
2121
+
2122
+ const hole2D = [];
2123
+
2124
+ for ( const idx of holeIndices ) {
2125
+
2126
+ const p = new Vector3(
2127
+ points[ idx * 3 ],
2128
+ points[ idx * 3 + 1 ],
2129
+ points[ idx * 3 + 2 ]
2130
+ );
2131
+ hole2D.push( new Vector2( p.dot( tangent ), p.dot( bitangent ) ) );
2132
+
2133
+ }
2134
+
2135
+ holes2D.push( hole2D );
2136
+
2137
+ }
2138
+
2139
+ // Build combined index array: outer contour followed by all holes
2140
+ const allIndices = [ ...outerIndices ];
2141
+ for ( const holeIndices of holeContours ) {
2142
+
2143
+ allIndices.push( ...holeIndices );
2144
+
2145
+ }
2146
+
2147
+ // Triangulate using ShapeUtils with holes
2148
+ const triangles = ShapeUtils.triangulateShape( outer2D, holes2D );
2149
+
2150
+ // Map back to original vertex indices
2151
+ const result = [];
2152
+ for ( const tri of triangles ) {
2153
+
2154
+ result.push( [
2155
+ allIndices[ tri[ 0 ] ],
2156
+ allIndices[ tri[ 1 ] ],
2157
+ allIndices[ tri[ 2 ] ]
2158
+ ] );
2159
+
2160
+ }
2161
+
2162
+ return result;
2163
+
2164
+ }
2165
+
2166
+ _triangulateIndices( indices, counts ) {
2167
+
2168
+ const triangulated = [];
2169
+ let offset = 0;
2170
+
2171
+ for ( let i = 0; i < counts.length; i ++ ) {
2172
+
2173
+ const count = counts[ i ];
2174
+
2175
+ if ( count === 3 ) {
2176
+
2177
+ triangulated.push(
2178
+ indices[ offset ],
2179
+ indices[ offset + 1 ],
2180
+ indices[ offset + 2 ]
2181
+ );
2182
+
2183
+ } else if ( count === 4 ) {
2184
+
2185
+ triangulated.push(
2186
+ indices[ offset ],
2187
+ indices[ offset + 1 ],
2188
+ indices[ offset + 2 ],
2189
+ indices[ offset ],
2190
+ indices[ offset + 2 ],
2191
+ indices[ offset + 3 ]
2192
+ );
2193
+
2194
+ } else if ( count > 4 ) {
2195
+
2196
+ // Fan triangulation for n-gons
2197
+ for ( let j = 1; j < count - 1; j ++ ) {
2198
+
2199
+ triangulated.push(
2200
+ indices[ offset ],
2201
+ indices[ offset + j ],
2202
+ indices[ offset + j + 1 ]
2203
+ );
2204
+
2205
+ }
2206
+
2207
+ }
2208
+
2209
+ offset += count;
2210
+
2211
+ }
2212
+
2213
+ return triangulated;
2214
+
2215
+ }
2216
+
2217
+ _expandAttribute( data, indices, itemSize ) {
2218
+
2219
+ const expanded = new Array( indices.length * itemSize );
2220
+
2221
+ for ( let i = 0; i < indices.length; i ++ ) {
2222
+
2223
+ const srcIdx = indices[ i ];
2224
+
2225
+ for ( let j = 0; j < itemSize; j ++ ) {
2226
+
2227
+ expanded[ i * itemSize + j ] = data[ srcIdx * itemSize + j ];
2228
+
2229
+ }
2230
+
2231
+ }
2232
+
2233
+ return expanded;
2234
+
2235
+ }
2236
+
2237
+ /**
2238
+ * Get the material path for a mesh, checking various binding sources.
2239
+ */
2240
+ _getMaterialPath( meshPath, fields ) {
2241
+
2242
+ let materialPath = null;
2243
+ let materialBinding = fields[ 'material:binding' ];
2244
+
2245
+ if ( materialBinding ) {
2246
+
2247
+ materialPath = Array.isArray( materialBinding ) ? materialBinding[ 0 ] : materialBinding;
2248
+
2249
+ }
2250
+
2251
+ // Use variant-aware lookup if no direct binding in fields
2252
+ if ( ! materialPath ) {
2253
+
2254
+ materialPath = this._getMaterialBindingTarget( meshPath );
2255
+
2256
+ }
2257
+
2258
+ return materialPath;
2259
+
2260
+ }
2261
+
2262
+ _buildMaterial( meshPath, fields ) {
2263
+
2264
+ const material = new MeshPhysicalMaterial();
2265
+
2266
+ let materialPath = null;
2267
+ let materialBinding = fields[ 'material:binding' ];
2268
+
2269
+ if ( materialBinding ) {
2270
+
2271
+ materialPath = Array.isArray( materialBinding ) ? materialBinding[ 0 ] : materialBinding;
2272
+
2273
+ }
2274
+
2275
+ // Use variant-aware lookup if no direct binding in fields
2276
+ if ( ! materialPath ) {
2277
+
2278
+ materialPath = this._getMaterialBindingTarget( meshPath );
2279
+
2280
+ }
2281
+
2282
+ if ( ! materialPath ) {
2283
+
2284
+ const materialPaths = [];
2285
+ const prefix = meshPath + '/';
2286
+
2287
+ for ( const path in this.specsByPath ) {
2288
+
2289
+ if ( ! path.startsWith( prefix ) ) continue;
2290
+ if ( ! path.endsWith( '.material:binding' ) ) continue;
2291
+
2292
+ const bindingSpec = this.specsByPath[ path ];
2293
+ if ( ! bindingSpec ) continue;
2294
+
2295
+ const targetPaths = bindingSpec.fields.targetPaths;
2296
+ if ( targetPaths && targetPaths.length > 0 ) {
2297
+
2298
+ materialPaths.push( targetPaths[ 0 ] );
2299
+
2300
+ }
2301
+
2302
+ }
2303
+
2304
+ if ( materialPaths.length > 0 ) {
2305
+
2306
+ materialPath = this._pickBestMaterial( materialPaths );
2307
+
2308
+ }
2309
+
2310
+ }
2311
+
2312
+ if ( ! materialPath ) {
2313
+
2314
+ // Use material index for O(1) lookup instead of O(n) iteration
2315
+ const meshParts = meshPath.split( '/' );
2316
+ const rootPath = '/' + meshParts[ 1 ];
2317
+
2318
+ const materialsInRoot = this.materialsByRoot.get( rootPath );
2319
+
2320
+ if ( materialsInRoot ) {
2321
+
2322
+ for ( const path of materialsInRoot ) {
2323
+
2324
+ if ( path.startsWith( rootPath + '/Looks/' ) ||
2325
+ path.startsWith( rootPath + '/Materials/' ) ) {
2326
+
2327
+ materialPath = path;
2328
+ break;
2329
+
2330
+ }
2331
+
2332
+ }
2333
+
2334
+ }
2335
+
2336
+ }
2337
+
2338
+ if ( materialPath ) {
2339
+
2340
+ this._applyMaterial( material, materialPath );
2341
+
2342
+ }
2343
+
2344
+ return material;
2345
+
2346
+ }
2347
+
2348
+ _buildMaterialForPath( materialPath ) {
2349
+
2350
+ const material = new MeshPhysicalMaterial();
2351
+
2352
+ if ( materialPath ) {
2353
+
2354
+ this._applyMaterial( material, materialPath );
2355
+
2356
+ }
2357
+
2358
+ return material;
2359
+
2360
+ }
2361
+
2362
+ /**
2363
+ * Apply material binding from a prim path to a mesh.
2364
+ * Used when merging referenced geometry into a prim that has material binding.
2365
+ */
2366
+ _applyMaterialBinding( mesh, primPath ) {
2367
+
2368
+ // Look for material:binding on this prim
2369
+ const bindingPath = primPath + '.material:binding';
2370
+ const bindingSpec = this.specsByPath[ bindingPath ];
2371
+
2372
+ if ( ! bindingSpec ) return;
2373
+
2374
+ let materialPath = null;
2375
+ const targetPaths = bindingSpec.fields?.targetPaths || bindingSpec.fields?.default;
2376
+
2377
+ if ( targetPaths ) {
2378
+
2379
+ materialPath = Array.isArray( targetPaths ) ? targetPaths[ 0 ] : targetPaths;
2380
+
2381
+ }
2382
+
2383
+ if ( ! materialPath ) return;
2384
+
2385
+ // Clean the material path
2386
+ materialPath = String( materialPath ).replace( /^<|>$/g, '' );
2387
+
2388
+ // Build and apply the material
2389
+ const material = new MeshPhysicalMaterial();
2390
+ this._applyMaterial( material, materialPath );
2391
+ mesh.material = material;
2392
+
2393
+ }
2394
+
2395
+ _pickBestMaterial( materialPaths ) {
2396
+
2397
+ for ( const materialPath of materialPaths ) {
2398
+
2399
+ const shaderPaths = this.shadersByMaterialPath.get( materialPath );
2400
+ if ( ! shaderPaths ) continue;
2401
+
2402
+ for ( const path of shaderPaths ) {
2403
+
2404
+ const attrs = this._getAttributes( path );
2405
+ if ( attrs[ 'info:id' ] === 'UsdUVTexture' && attrs[ 'inputs:file' ] ) {
2406
+
2407
+ return materialPath;
2408
+
2409
+ }
2410
+
2411
+ }
2412
+
2413
+ }
2414
+
2415
+ return materialPaths[ 0 ];
2416
+
2417
+ }
2418
+
2419
+ _applyMaterial( material, materialPath ) {
2420
+
2421
+ const materialSpec = this.specsByPath[ materialPath ];
2422
+ if ( ! materialSpec ) return;
2423
+
2424
+ const shaderPaths = this.shadersByMaterialPath.get( materialPath );
2425
+ if ( ! shaderPaths ) return;
2426
+
2427
+ for ( const path of shaderPaths ) {
2428
+
2429
+ const spec = this.specsByPath[ path ];
2430
+ if ( ! spec ) continue;
2431
+
2432
+ const shaderAttrs = this._getAttributes( path );
2433
+ const infoId = shaderAttrs[ 'info:id' ] || spec.fields[ 'info:id' ];
2434
+
2435
+ if ( infoId === 'UsdPreviewSurface' ) {
2436
+
2437
+ this._applyPreviewSurface( material, path );
2438
+
2439
+ } else if ( infoId === 'arnold:openpbr_surface' ) {
2440
+
2441
+ this._applyOpenPBRSurface( material, path );
2442
+
2443
+ }
2444
+
2445
+ }
2446
+
2447
+ }
2448
+
2449
+ /**
2450
+ * Shared helper for applying texture or value from shader attribute.
2451
+ * Reduces duplication between _applyPreviewSurface and _applyOpenPBRSurface.
2452
+ */
2453
+ _applyTextureOrValue( material, shaderPath, fields, attrName, textureProperty, colorSpace, valueCallback, textureGetter ) {
2454
+
2455
+ const attrPath = shaderPath + '.' + attrName;
2456
+ const spec = this.specsByPath[ attrPath ];
2457
+
2458
+ if ( spec && spec.fields.connectionPaths && spec.fields.connectionPaths.length > 0 ) {
2459
+
2460
+ // For OpenPBR, try all connection paths; for PreviewSurface, just the first
2461
+ const paths = textureGetter === this._getTextureFromOpenPBRConnection
2462
+ ? spec.fields.connectionPaths
2463
+ : [ spec.fields.connectionPaths[ 0 ] ];
2464
+
2465
+ for ( const connPath of paths ) {
2466
+
2467
+ const texture = textureGetter.call( this, connPath );
2468
+
2469
+ if ( texture ) {
2470
+
2471
+ texture.colorSpace = colorSpace;
2472
+ material[ textureProperty ] = texture;
2473
+ return true;
2474
+
2475
+ }
2476
+
2477
+ }
2478
+
2479
+ }
2480
+
2481
+ if ( fields[ attrName ] !== undefined && valueCallback ) {
2482
+
2483
+ valueCallback( fields[ attrName ] );
2484
+
2485
+ }
2486
+
2487
+ return false;
2488
+
2489
+ }
2490
+
2491
+ _applyPreviewSurface( material, shaderPath ) {
2492
+
2493
+ const fields = this._getAttributes( shaderPath );
2494
+
2495
+ const applyTexture = ( attrName, textureProperty, colorSpace, valueCallback ) => {
2496
+
2497
+ return this._applyTextureOrValue(
2498
+ material, shaderPath, fields, attrName, textureProperty, colorSpace, valueCallback,
2499
+ this._getTextureFromConnection
2500
+ );
2501
+
2502
+ };
2503
+
2504
+ const getAttrSpec = ( attrName ) => {
2505
+
2506
+ const attrPath = shaderPath + '.' + attrName;
2507
+ return this.specsByPath[ attrPath ];
2508
+
2509
+ };
2510
+
2511
+ // Diffuse color / base color map
2512
+ applyTexture(
2513
+ 'inputs:diffuseColor',
2514
+ 'map',
2515
+ SRGBColorSpace,
2516
+ ( color ) => {
2517
+
2518
+ if ( Array.isArray( color ) && color.length >= 3 ) {
2519
+
2520
+ material.color.setRGB( color[ 0 ], color[ 1 ], color[ 2 ], SRGBColorSpace );
2521
+
2522
+ }
2523
+
2524
+ }
2525
+ );
2526
+
2527
+ // Apply UsdUVTexture scale to diffuse color (output = texture * scale + bias)
2528
+ if ( material.map && material.map.userData.scale ) {
2529
+
2530
+ const scale = material.map.userData.scale;
2531
+ if ( Array.isArray( scale ) && scale.length >= 3 ) {
2532
+
2533
+ material.color.setRGB( scale[ 0 ], scale[ 1 ], scale[ 2 ], SRGBColorSpace );
2534
+
2535
+ }
2536
+
2537
+ }
2538
+
2539
+ // Emissive
2540
+ applyTexture(
2541
+ 'inputs:emissiveColor',
2542
+ 'emissiveMap',
2543
+ SRGBColorSpace,
2544
+ ( color ) => {
2545
+
2546
+ if ( Array.isArray( color ) && color.length >= 3 ) {
2547
+
2548
+ material.emissive.setRGB( color[ 0 ], color[ 1 ], color[ 2 ], SRGBColorSpace );
2549
+
2550
+ }
2551
+
2552
+ }
2553
+ );
2554
+
2555
+ if ( material.emissiveMap ) {
2556
+
2557
+ if ( material.emissiveMap.userData.scale ) {
2558
+
2559
+ const scale = material.emissiveMap.userData.scale;
2560
+ if ( Array.isArray( scale ) && scale.length >= 3 ) {
2561
+
2562
+ material.emissive.setRGB( scale[ 0 ], scale[ 1 ], scale[ 2 ], SRGBColorSpace );
2563
+
2564
+ }
2565
+
2566
+ } else {
2567
+
2568
+ material.emissive.set( 0xffffff );
2569
+
2570
+ }
2571
+
2572
+ }
2573
+
2574
+ // Normal map
2575
+ applyTexture( 'inputs:normal', 'normalMap', NoColorSpace, null );
2576
+
2577
+ // Apply normal map scale from UsdUVTexture scale input
2578
+ if ( material.normalMap && material.normalMap.userData.scale ) {
2579
+
2580
+ const scale = material.normalMap.userData.scale;
2581
+ // UsdUVTexture scale is float4 (r,g,b,a), use first two components for normalScale
2582
+ material.normalScale = new Vector2( scale[ 0 ], scale[ 1 ] );
2583
+
2584
+ }
2585
+
2586
+ // Roughness
2587
+ const hasRoughnessMap = applyTexture(
2588
+ 'inputs:roughness',
2589
+ 'roughnessMap',
2590
+ NoColorSpace,
2591
+ ( value ) => {
2592
+
2593
+ material.roughness = value;
2594
+
2595
+ }
2596
+ );
2597
+
2598
+ if ( hasRoughnessMap ) {
2599
+
2600
+ material.roughness = 1.0;
2601
+
2602
+ }
2603
+
2604
+ // Metallic
2605
+ const hasMetalnessMap = applyTexture(
2606
+ 'inputs:metallic',
2607
+ 'metalnessMap',
2608
+ NoColorSpace,
2609
+ ( value ) => {
2610
+
2611
+ material.metalness = value;
2612
+
2613
+ }
2614
+ );
2615
+
2616
+ if ( hasMetalnessMap ) {
2617
+
2618
+ material.metalness = 1.0;
2619
+
2620
+ }
2621
+
2622
+ // Occlusion
2623
+ applyTexture( 'inputs:occlusion', 'aoMap', NoColorSpace, null );
2624
+
2625
+ // IOR
2626
+ if ( fields[ 'inputs:ior' ] !== undefined ) {
2627
+
2628
+ material.ior = fields[ 'inputs:ior' ];
2629
+
2630
+ }
2631
+
2632
+ // Specular color
2633
+ applyTexture(
2634
+ 'inputs:specularColor',
2635
+ 'specularColorMap',
2636
+ SRGBColorSpace,
2637
+ ( color ) => {
2638
+
2639
+ if ( Array.isArray( color ) && color.length >= 3 ) {
2640
+
2641
+ material.specularColor.setRGB( color[ 0 ], color[ 1 ], color[ 2 ], SRGBColorSpace );
2642
+
2643
+ }
2644
+
2645
+ }
2646
+ );
2647
+
2648
+ // Apply UsdUVTexture scale to specular color
2649
+ if ( material.specularColorMap && material.specularColorMap.userData.scale ) {
2650
+
2651
+ const scale = material.specularColorMap.userData.scale;
2652
+ if ( Array.isArray( scale ) && scale.length >= 3 ) {
2653
+
2654
+ material.specularColor.setRGB( scale[ 0 ], scale[ 1 ], scale[ 2 ], SRGBColorSpace );
2655
+
2656
+ }
2657
+
2658
+ }
2659
+
2660
+ // Clearcoat
2661
+ if ( fields[ 'inputs:clearcoat' ] !== undefined ) {
2662
+
2663
+ material.clearcoat = fields[ 'inputs:clearcoat' ];
2664
+
2665
+ }
2666
+
2667
+ // Clearcoat roughness
2668
+ if ( fields[ 'inputs:clearcoatRoughness' ] !== undefined ) {
2669
+
2670
+ material.clearcoatRoughness = fields[ 'inputs:clearcoatRoughness' ];
2671
+
2672
+ }
2673
+
2674
+ // Opacity and opacity modes
2675
+ const opacityThreshold = fields[ 'inputs:opacityThreshold' ] !== undefined ? fields[ 'inputs:opacityThreshold' ] : 0.0;
2676
+
2677
+ // Check if opacity is connected to a texture (e.g., diffuse texture's alpha)
2678
+ const opacitySpec = getAttrSpec( 'inputs:opacity' );
2679
+ const hasOpacityConnection = opacitySpec?.fields?.connectionPaths?.length > 0;
2680
+
2681
+ if ( hasOpacityConnection ) {
2682
+
2683
+ // Opacity from texture alpha - use the diffuse map's alpha channel
2684
+ if ( opacityThreshold > 0 ) {
2685
+
2686
+ // Alpha cutoff mode
2687
+ material.alphaTest = opacityThreshold;
2688
+ material.transparent = false;
2689
+
2690
+ } else {
2691
+
2692
+ // Alpha blend mode
2693
+ material.transparent = true;
2694
+
2695
+ }
2696
+
2697
+ } else {
2698
+
2699
+ // Direct opacity value
2700
+ const opacity = fields[ 'inputs:opacity' ] !== undefined ? fields[ 'inputs:opacity' ] : 1.0;
2701
+
2702
+ if ( opacity < 1.0 ) {
2703
+
2704
+ material.transparent = true;
2705
+ material.opacity = opacity;
2706
+
2707
+ }
2708
+
2709
+ }
2710
+
2711
+ }
2712
+
2713
+ _applyOpenPBRSurface( material, shaderPath ) {
2714
+
2715
+ const fields = this._getAttributes( shaderPath );
2716
+
2717
+ const applyTexture = ( attrName, textureProperty, colorSpace, valueCallback ) => {
2718
+
2719
+ return this._applyTextureOrValue(
2720
+ material, shaderPath, fields, attrName, textureProperty, colorSpace, valueCallback,
2721
+ this._getTextureFromOpenPBRConnection
2722
+ );
2723
+
2724
+ };
2725
+
2726
+ // Base color (diffuse)
2727
+ applyTexture(
2728
+ 'inputs:base_color',
2729
+ 'map',
2730
+ SRGBColorSpace,
2731
+ ( color ) => {
2732
+
2733
+ if ( Array.isArray( color ) && color.length >= 3 ) {
2734
+
2735
+ material.color.setRGB( color[ 0 ], color[ 1 ], color[ 2 ], SRGBColorSpace );
2736
+
2737
+ }
2738
+
2739
+ }
2740
+ );
2741
+
2742
+ // Apply UsdUVTexture scale to base color
2743
+ if ( material.map && material.map.userData.scale ) {
2744
+
2745
+ const scale = material.map.userData.scale;
2746
+ if ( Array.isArray( scale ) && scale.length >= 3 ) {
2747
+
2748
+ material.color.setRGB( scale[ 0 ], scale[ 1 ], scale[ 2 ], SRGBColorSpace );
2749
+
2750
+ }
2751
+
2752
+ }
2753
+
2754
+ // Base metalness
2755
+ applyTexture(
2756
+ 'inputs:base_metalness',
2757
+ 'metalnessMap',
2758
+ NoColorSpace,
2759
+ ( value ) => {
2760
+
2761
+ if ( typeof value === 'number' ) {
2762
+
2763
+ material.metalness = value;
2764
+
2765
+ }
2766
+
2767
+ }
2768
+ );
2769
+
2770
+ // Specular roughness
2771
+ applyTexture(
2772
+ 'inputs:specular_roughness',
2773
+ 'roughnessMap',
2774
+ NoColorSpace,
2775
+ ( value ) => {
2776
+
2777
+ if ( typeof value === 'number' ) {
2778
+
2779
+ material.roughness = value;
2780
+
2781
+ }
2782
+
2783
+ }
2784
+ );
2785
+
2786
+ // Emission color
2787
+ const hasEmissionMap = applyTexture(
2788
+ 'inputs:emission_color',
2789
+ 'emissiveMap',
2790
+ SRGBColorSpace,
2791
+ ( color ) => {
2792
+
2793
+ if ( Array.isArray( color ) && color.length >= 3 ) {
2794
+
2795
+ material.emissive.setRGB( color[ 0 ], color[ 1 ], color[ 2 ], SRGBColorSpace );
2796
+
2797
+ }
2798
+
2799
+ }
2800
+ );
2801
+
2802
+ // Emission luminance/weight - multiply emissive by this factor
2803
+ const emissionLuminance = fields[ 'inputs:emission_luminance' ];
2804
+
2805
+ if ( emissionLuminance !== undefined && emissionLuminance > 0 ) {
2806
+
2807
+ if ( hasEmissionMap ) {
2808
+
2809
+ material.emissiveIntensity = emissionLuminance;
2810
+
2811
+ } else {
2812
+
2813
+ // Scale the emissive color by luminance
2814
+ material.emissive.multiplyScalar( emissionLuminance );
2815
+
2816
+ }
2817
+
2818
+ }
2819
+
2820
+ // Transmission (transparency)
2821
+ const transmissionWeight = fields[ 'inputs:transmission_weight' ];
2822
+
2823
+ if ( transmissionWeight !== undefined && transmissionWeight > 0 ) {
2824
+
2825
+ material.transmission = transmissionWeight;
2826
+
2827
+ const transmissionDepth = fields[ 'inputs:transmission_depth' ];
2828
+
2829
+ if ( transmissionDepth !== undefined ) {
2830
+
2831
+ material.thickness = transmissionDepth;
2832
+
2833
+ }
2834
+
2835
+ const transmissionColor = fields[ 'inputs:transmission_color' ];
2836
+
2837
+ if ( transmissionColor !== undefined && Array.isArray( transmissionColor ) ) {
2838
+
2839
+ material.attenuationColor.setRGB( transmissionColor[ 0 ], transmissionColor[ 1 ], transmissionColor[ 2 ] );
2840
+ material.attenuationDistance = transmissionDepth || 1.0;
2841
+
2842
+ }
2843
+
2844
+ }
2845
+
2846
+ // Geometry opacity (overall surface opacity)
2847
+ const geometryOpacity = fields[ 'inputs:geometry_opacity' ];
2848
+
2849
+ if ( geometryOpacity !== undefined && geometryOpacity < 1.0 ) {
2850
+
2851
+ material.opacity = geometryOpacity;
2852
+ material.transparent = true;
2853
+
2854
+ }
2855
+
2856
+ // Specular IOR
2857
+ const specularIOR = fields[ 'inputs:specular_ior' ];
2858
+
2859
+ if ( specularIOR !== undefined ) {
2860
+
2861
+ material.ior = specularIOR;
2862
+
2863
+ }
2864
+
2865
+ // Coat (clearcoat)
2866
+ const coatWeight = fields[ 'inputs:coat_weight' ];
2867
+
2868
+ if ( coatWeight !== undefined && coatWeight > 0 ) {
2869
+
2870
+ material.clearcoat = coatWeight;
2871
+
2872
+ const coatRoughness = fields[ 'inputs:coat_roughness' ];
2873
+
2874
+ if ( coatRoughness !== undefined ) {
2875
+
2876
+ material.clearcoatRoughness = coatRoughness;
2877
+
2878
+ }
2879
+
2880
+ }
2881
+
2882
+ // Thin film (iridescence)
2883
+ const thinFilmWeight = fields[ 'inputs:thin_film_weight' ];
2884
+
2885
+ if ( thinFilmWeight !== undefined && thinFilmWeight > 0 ) {
2886
+
2887
+ material.iridescence = thinFilmWeight;
2888
+
2889
+ const thinFilmIOR = fields[ 'inputs:thin_film_ior' ];
2890
+
2891
+ if ( thinFilmIOR !== undefined ) {
2892
+
2893
+ material.iridescenceIOR = thinFilmIOR;
2894
+
2895
+ }
2896
+
2897
+ const thinFilmThickness = fields[ 'inputs:thin_film_thickness' ];
2898
+
2899
+ if ( thinFilmThickness !== undefined ) {
2900
+
2901
+ // OpenPBR uses micrometers, Three.js uses nanometers
2902
+ const thicknessNm = thinFilmThickness * 1000;
2903
+ material.iridescenceThicknessRange = [ thicknessNm, thicknessNm ];
2904
+
2905
+ }
2906
+
2907
+ }
2908
+
2909
+ // Specular
2910
+ const specularWeight = fields[ 'inputs:specular_weight' ];
2911
+
2912
+ if ( specularWeight !== undefined ) {
2913
+
2914
+ material.specularIntensity = specularWeight;
2915
+
2916
+ }
2917
+
2918
+ const specularColor = fields[ 'inputs:specular_color' ];
2919
+
2920
+ if ( specularColor !== undefined && Array.isArray( specularColor ) ) {
2921
+
2922
+ material.specularColor.setRGB( specularColor[ 0 ], specularColor[ 1 ], specularColor[ 2 ] );
2923
+
2924
+ }
2925
+
2926
+ // Anisotropy
2927
+ const anisotropy = fields[ 'inputs:specular_roughness_anisotropy' ];
2928
+
2929
+ if ( anisotropy !== undefined && anisotropy > 0 ) {
2930
+
2931
+ material.anisotropy = anisotropy;
2932
+
2933
+ }
2934
+
2935
+ // Geometry normal (normal map)
2936
+ applyTexture(
2937
+ 'inputs:geometry_normal',
2938
+ 'normalMap',
2939
+ NoColorSpace,
2940
+ null
2941
+ );
2942
+
2943
+ }
2944
+
2945
+ _getTextureFromOpenPBRConnection( connPath ) {
2946
+
2947
+ // connPath is like /Material/NodeGraph.outputs:baseColor or /Material/Shader.outputs:out
2948
+ const cleanPath = connPath.replace( /<|>/g, '' );
2949
+ const shaderPath = cleanPath.split( '.' )[ 0 ];
2950
+ const shaderSpec = this.specsByPath[ shaderPath ];
2951
+
2952
+ if ( ! shaderSpec ) return null;
2953
+
2954
+ const attrs = this._getAttributes( shaderPath );
2955
+ const infoId = attrs[ 'info:id' ] || shaderSpec.fields[ 'info:id' ];
2956
+ const typeName = shaderSpec.fields.typeName;
2957
+
2958
+ // Handle NodeGraph - follow output connection to internal shader
2959
+ if ( typeName === 'NodeGraph' ) {
2960
+
2961
+ // Get the output attribute that's connected
2962
+ const outputName = cleanPath.split( '.' )[ 1 ]; // e.g., "outputs:baseColor"
2963
+ const outputAttrPath = shaderPath + '.' + outputName;
2964
+ const outputSpec = this.specsByPath[ outputAttrPath ];
2965
+
2966
+ if ( outputSpec?.fields?.connectionPaths?.length > 0 ) {
2967
+
2968
+ // Follow the internal connection
2969
+ return this._getTextureFromOpenPBRConnection( outputSpec.fields.connectionPaths[ 0 ] );
2970
+
2971
+ }
2972
+
2973
+ return null;
2974
+
2975
+ }
2976
+
2977
+ // Handle arnold:image - Arnold's texture node
2978
+ if ( infoId === 'arnold:image' ) {
2979
+
2980
+ const filePath = attrs[ 'inputs:filename' ];
2981
+ if ( ! filePath ) return null;
2982
+
2983
+ return this._loadTextureFromPath( filePath );
2984
+
2985
+ }
2986
+
2987
+ // Handle MaterialX image nodes (ND_image_color4, ND_image_color3, etc.)
2988
+ if ( infoId && infoId.startsWith( 'ND_image_' ) ) {
2989
+
2990
+ const filePath = attrs[ 'inputs:file' ];
2991
+ if ( ! filePath ) return null;
2992
+
2993
+ return this._loadTextureFromPath( filePath );
2994
+
2995
+ }
2996
+
2997
+ // Handle Maya file texture - follow the inColor connection to the actual image
2998
+ if ( infoId === 'MayaND_fileTexture_color4' ) {
2999
+
3000
+ const inColorPath = shaderPath + '.inputs:inColor';
3001
+ const inColorSpec = this.specsByPath[ inColorPath ];
3002
+
3003
+ if ( inColorSpec?.fields?.connectionPaths?.length > 0 ) {
3004
+
3005
+ return this._getTextureFromOpenPBRConnection( inColorSpec.fields.connectionPaths[ 0 ] );
3006
+
3007
+ }
3008
+
3009
+ return null;
3010
+
3011
+ }
3012
+
3013
+ // Handle color conversion nodes - follow the input connection
3014
+ if ( infoId && infoId.startsWith( 'ND_convert_' ) ) {
3015
+
3016
+ const inPath = shaderPath + '.inputs:in';
3017
+ const inSpec = this.specsByPath[ inPath ];
3018
+
3019
+ if ( inSpec?.fields?.connectionPaths?.length > 0 ) {
3020
+
3021
+ return this._getTextureFromOpenPBRConnection( inSpec.fields.connectionPaths[ 0 ] );
3022
+
3023
+ }
3024
+
3025
+ return null;
3026
+
3027
+ }
3028
+
3029
+ // Handle Arnold bump2d - follow the bump_map input
3030
+ if ( infoId === 'arnold:bump2d' ) {
3031
+
3032
+ const bumpMapPath = shaderPath + '.inputs:bump_map';
3033
+ const bumpMapSpec = this.specsByPath[ bumpMapPath ];
3034
+
3035
+ if ( bumpMapSpec?.fields?.connectionPaths?.length > 0 ) {
3036
+
3037
+ return this._getTextureFromOpenPBRConnection( bumpMapSpec.fields.connectionPaths[ 0 ] );
3038
+
3039
+ }
3040
+
3041
+ return null;
3042
+
3043
+ }
3044
+
3045
+ // Handle Arnold color_correct - follow the input connection
3046
+ if ( infoId === 'arnold:color_correct' ) {
3047
+
3048
+ const inputPath = shaderPath + '.inputs:input';
3049
+ const inputSpec = this.specsByPath[ inputPath ];
3050
+
3051
+ if ( inputSpec?.fields?.connectionPaths?.length > 0 ) {
3052
+
3053
+ return this._getTextureFromOpenPBRConnection( inputSpec.fields.connectionPaths[ 0 ] );
3054
+
3055
+ }
3056
+
3057
+ return null;
3058
+
3059
+ }
3060
+
3061
+ // Handle nested shader paths (e.g., /Material/file2/cc.outputs:a)
3062
+ // Check if parent path is an image node
3063
+ const parentPath = shaderPath.substring( 0, shaderPath.lastIndexOf( '/' ) );
3064
+
3065
+ if ( parentPath ) {
3066
+
3067
+ const parentSpec = this.specsByPath[ parentPath ];
3068
+
3069
+ if ( parentSpec ) {
3070
+
3071
+ const parentAttrs = this._getAttributes( parentPath );
3072
+ const parentInfoId = parentAttrs[ 'info:id' ] || parentSpec.fields[ 'info:id' ];
3073
+
3074
+ if ( parentInfoId === 'arnold:image' ) {
3075
+
3076
+ const filePath = parentAttrs[ 'inputs:filename' ];
3077
+ if ( filePath ) return this._loadTextureFromPath( filePath );
3078
+
3079
+ }
3080
+
3081
+ }
3082
+
3083
+ }
3084
+
3085
+ return null;
3086
+
3087
+ }
3088
+
3089
+ _loadTextureFromPath( filePath ) {
3090
+
3091
+ if ( ! filePath ) return null;
3092
+
3093
+ // Check cache first
3094
+ if ( this.textureCache[ filePath ] ) {
3095
+
3096
+ return this.textureCache[ filePath ];
3097
+
3098
+ }
3099
+
3100
+ const texture = this._loadTexture( filePath, null, null );
3101
+
3102
+ if ( texture ) {
3103
+
3104
+ this.textureCache[ filePath ] = texture;
3105
+
3106
+ }
3107
+
3108
+ return texture;
3109
+
3110
+ }
3111
+
3112
+ _getTextureFromConnection( connPath ) {
3113
+
3114
+ // connPath is like /Material/Shader.outputs:rgb
3115
+ const shaderPath = connPath.split( '.' )[ 0 ];
3116
+ const shaderSpec = this.specsByPath[ shaderPath ];
3117
+
3118
+ if ( ! shaderSpec ) return null;
3119
+
3120
+ const attrs = this._getAttributes( shaderPath );
3121
+ const infoId = attrs[ 'info:id' ] || shaderSpec.fields[ 'info:id' ];
3122
+
3123
+ if ( infoId !== 'UsdUVTexture' ) return null;
3124
+
3125
+ const filePath = attrs[ 'inputs:file' ];
3126
+ if ( ! filePath ) return null;
3127
+
3128
+ // Check for UsdTransform2d connection via inputs:st and trace to PrimvarReader
3129
+ let transformAttrs = null;
3130
+ let uvChannel = 0; // Default to first UV set
3131
+ const stAttrPath = shaderPath + '.inputs:st';
3132
+ const stAttrSpec = this.specsByPath[ stAttrPath ];
3133
+
3134
+ if ( stAttrSpec?.fields?.connectionPaths?.length > 0 ) {
3135
+
3136
+ const stConnPath = stAttrSpec.fields.connectionPaths[ 0 ];
3137
+ const stPath = stConnPath.replace( /<|>/g, '' ).split( '.' )[ 0 ];
3138
+ const stSpec = this.specsByPath[ stPath ];
3139
+
3140
+ if ( stSpec ) {
3141
+
3142
+ const stAttrs = this._getAttributes( stPath );
3143
+ const stInfoId = stAttrs[ 'info:id' ] || stSpec.fields[ 'info:id' ];
3144
+
3145
+ if ( stInfoId === 'UsdTransform2d' ) {
3146
+
3147
+ transformAttrs = stAttrs;
3148
+
3149
+ // Trace to PrimvarReader to find UV set
3150
+ const inAttrPath = stPath + '.inputs:in';
3151
+ const inAttrSpec = this.specsByPath[ inAttrPath ];
3152
+
3153
+ if ( inAttrSpec?.fields?.connectionPaths?.length > 0 ) {
3154
+
3155
+ const inConnPath = inAttrSpec.fields.connectionPaths[ 0 ];
3156
+ const primvarPath = inConnPath.replace( /<|>/g, '' ).split( '.' )[ 0 ];
3157
+ const primvarAttrs = this._getAttributes( primvarPath );
3158
+
3159
+ // Check varname to determine UV channel
3160
+ const varname = primvarAttrs[ 'inputs:varname' ];
3161
+ if ( varname === 'st1' ) uvChannel = 1;
3162
+ else if ( varname === 'st2' ) uvChannel = 2;
3163
+
3164
+ }
3165
+
3166
+ } else if ( stInfoId === 'UsdPrimvarReader_float2' ) {
3167
+
3168
+ // Direct connection to PrimvarReader
3169
+ const varname = stAttrs[ 'inputs:varname' ];
3170
+ if ( varname === 'st1' ) uvChannel = 1;
3171
+ else if ( varname === 'st2' ) uvChannel = 2;
3172
+
3173
+ }
3174
+
3175
+ }
3176
+
3177
+ }
3178
+
3179
+ // Extract scale and bias for texture value modification
3180
+ const scale = attrs[ 'inputs:scale' ];
3181
+ const bias = attrs[ 'inputs:bias' ];
3182
+
3183
+ // Create cache key that includes scale/bias if present
3184
+ let cacheKey = filePath;
3185
+ if ( scale ) cacheKey += ':s' + scale.join( ',' );
3186
+ if ( bias ) cacheKey += ':b' + bias.join( ',' );
3187
+
3188
+ if ( this.textureCache[ cacheKey ] ) {
3189
+
3190
+ return this.textureCache[ cacheKey ];
3191
+
3192
+ }
3193
+
3194
+ const texture = this._loadTexture( filePath, attrs, transformAttrs );
3195
+
3196
+ if ( texture ) {
3197
+
3198
+ // Store scale/bias and UV channel in userData
3199
+ if ( scale ) texture.userData.scale = scale;
3200
+ if ( bias ) texture.userData.bias = bias;
3201
+ if ( uvChannel !== 0 ) texture.channel = uvChannel;
3202
+
3203
+ this.textureCache[ cacheKey ] = texture;
3204
+
3205
+ }
3206
+
3207
+ return texture;
3208
+
3209
+ }
3210
+
3211
+ _applyTextureTransforms( texture, attrs ) {
3212
+
3213
+ if ( ! attrs ) return;
3214
+
3215
+ const scale = attrs[ 'inputs:scale' ];
3216
+ if ( scale && Array.isArray( scale ) && scale.length >= 2 ) {
3217
+
3218
+ texture.repeat.set( scale[ 0 ], scale[ 1 ] );
3219
+
3220
+ }
3221
+
3222
+ const translation = attrs[ 'inputs:translation' ];
3223
+ if ( translation && Array.isArray( translation ) && translation.length >= 2 ) {
3224
+
3225
+ texture.offset.set( translation[ 0 ], translation[ 1 ] );
3226
+
3227
+ }
3228
+
3229
+ const rotation = attrs[ 'inputs:rotation' ];
3230
+ if ( typeof rotation === 'number' ) {
3231
+
3232
+ texture.rotation = rotation * Math.PI / 180;
3233
+
3234
+ }
3235
+
3236
+ }
3237
+
3238
+ _loadTexture( filePath, textureAttrs, transformAttrs ) {
3239
+
3240
+ let cleanPath = filePath;
3241
+ if ( cleanPath.startsWith( '@' ) ) cleanPath = cleanPath.slice( 1 );
3242
+ if ( cleanPath.endsWith( '@' ) ) cleanPath = cleanPath.slice( 0, - 1 );
3243
+
3244
+ // Resolve relative to basePath first
3245
+ const resolvedPath = this._resolveFilePath( cleanPath );
3246
+ let assetData = this.assets[ resolvedPath ];
3247
+
3248
+ // Fallback to unresolved path
3249
+ if ( ! assetData ) {
3250
+
3251
+ assetData = this.assets[ cleanPath ];
3252
+
3253
+ }
3254
+
3255
+ // Last resort: search by basename
3256
+ if ( ! assetData ) {
3257
+
3258
+ const baseName = cleanPath.split( '/' ).pop();
3259
+
3260
+ for ( const key in this.assets ) {
3261
+
3262
+ if ( key.endsWith( baseName ) || key.endsWith( '/' + baseName ) ) {
3263
+
3264
+ return this._createTextureFromData( this.assets[ key ], textureAttrs, transformAttrs );
3265
+
3266
+ }
3267
+
3268
+ }
3269
+
3270
+ // Try loading via LoadingManager if available
3271
+ if ( this.manager ) {
3272
+
3273
+ const url = this.manager.resolveURL( baseName );
3274
+ if ( url !== baseName ) {
3275
+
3276
+ // URL modifier found a match - load it
3277
+ return this._createTextureFromData( url, textureAttrs, transformAttrs );
3278
+
3279
+ }
3280
+
3281
+ }
3282
+
3283
+ console.warn( 'USDLoader: Texture not found:', cleanPath );
3284
+ return null;
3285
+
3286
+ }
3287
+
3288
+ return this._createTextureFromData( assetData, textureAttrs, transformAttrs );
3289
+
3290
+ }
3291
+
3292
+ _createTextureFromData( data, textureAttrs, transformAttrs ) {
3293
+
3294
+ if ( ! data ) return null;
3295
+
3296
+ const scope = this;
3297
+ const texture = new Texture();
3298
+
3299
+ let url;
3300
+
3301
+ if ( typeof data === 'string' ) {
3302
+
3303
+ url = data;
3304
+
3305
+ } else if ( data instanceof Uint8Array || data instanceof ArrayBuffer ) {
3306
+
3307
+ const blob = new Blob( [ data ] );
3308
+ url = URL.createObjectURL( blob );
3309
+
3310
+ } else {
3311
+
3312
+ return null;
3313
+
3314
+ }
3315
+
3316
+ const image = new Image();
3317
+ image.onload = function () {
3318
+
3319
+ texture.image = image;
3320
+
3321
+ if ( textureAttrs ) {
3322
+
3323
+ texture.wrapS = scope._getWrapMode( textureAttrs[ 'inputs:wrapS' ] );
3324
+ texture.wrapT = scope._getWrapMode( textureAttrs[ 'inputs:wrapT' ] );
3325
+
3326
+ }
3327
+
3328
+ scope._applyTextureTransforms( texture, transformAttrs );
3329
+ texture.needsUpdate = true;
3330
+
3331
+ if ( typeof data !== 'string' ) {
3332
+
3333
+ URL.revokeObjectURL( url );
3334
+
3335
+ }
3336
+
3337
+ };
3338
+ image.src = url;
3339
+
3340
+ return texture;
3341
+
3342
+ }
3343
+
3344
+ _getWrapMode( wrapValue ) {
3345
+
3346
+ if ( wrapValue === 'repeat' ) return RepeatWrapping;
3347
+ if ( wrapValue === 'mirror' ) return MirroredRepeatWrapping;
3348
+ if ( wrapValue === 'clamp' ) return ClampToEdgeWrapping;
3349
+ return RepeatWrapping;
3350
+
3351
+ }
3352
+
3353
+ // ========================================================================
3354
+ // Skeletal Animation
3355
+ // ========================================================================
3356
+
3357
+ _buildSkeleton( path ) {
3358
+
3359
+ const attrs = this._getAttributes( path );
3360
+
3361
+ // Get joint names (paths like "root", "root/body_joint", etc.)
3362
+ const joints = attrs[ 'joints' ];
3363
+ if ( ! joints || joints.length === 0 ) return null;
3364
+
3365
+ // Get bind transforms (world-space bind pose matrices)
3366
+ // These can be nested arrays (USDA) or flat arrays (USDC)
3367
+ const rawBindTransforms = attrs[ 'bindTransforms' ];
3368
+ const rawRestTransforms = attrs[ 'restTransforms' ];
3369
+
3370
+ const bindTransforms = this._flattenMatrixArray( rawBindTransforms, joints.length );
3371
+ const restTransforms = this._flattenMatrixArray( rawRestTransforms, joints.length );
3372
+
3373
+ // Build bones
3374
+ const bones = [];
3375
+ const bonesByPath = {};
3376
+ const boneInverses = [];
3377
+
3378
+ for ( let i = 0; i < joints.length; i ++ ) {
3379
+
3380
+ const jointPath = joints[ i ];
3381
+ const jointName = jointPath.split( '/' ).pop();
3382
+
3383
+ const bone = new Bone();
3384
+ bone.name = jointName;
3385
+ bones.push( bone );
3386
+ bonesByPath[ jointPath ] = { bone, index: i };
3387
+
3388
+ // Compute inverse bind matrix
3389
+ if ( bindTransforms && bindTransforms.length >= ( i + 1 ) * 16 ) {
3390
+
3391
+ const bindMatrix = new Matrix4();
3392
+ // USD matrices are row-major, Three.js is column-major - need to transpose
3393
+ const m = bindTransforms.slice( i * 16, ( i + 1 ) * 16 );
3394
+ bindMatrix.set(
3395
+ m[ 0 ], m[ 4 ], m[ 8 ], m[ 12 ],
3396
+ m[ 1 ], m[ 5 ], m[ 9 ], m[ 13 ],
3397
+ m[ 2 ], m[ 6 ], m[ 10 ], m[ 14 ],
3398
+ m[ 3 ], m[ 7 ], m[ 11 ], m[ 15 ]
3399
+ );
3400
+ const inverseBindMatrix = bindMatrix.clone().invert();
3401
+ boneInverses.push( inverseBindMatrix );
3402
+
3403
+ } else {
3404
+
3405
+ boneInverses.push( new Matrix4() );
3406
+
3407
+ }
3408
+
3409
+ }
3410
+
3411
+ // Build parent-child relationships based on joint paths
3412
+ for ( let i = 0; i < joints.length; i ++ ) {
3413
+
3414
+ const jointPath = joints[ i ];
3415
+ const parts = jointPath.split( '/' );
3416
+
3417
+ if ( parts.length > 1 ) {
3418
+
3419
+ const parentPath = parts.slice( 0, - 1 ).join( '/' );
3420
+ const parentData = bonesByPath[ parentPath ];
3421
+
3422
+ if ( parentData ) {
3423
+
3424
+ parentData.bone.add( bones[ i ] );
3425
+
3426
+ }
3427
+
3428
+ }
3429
+
3430
+ }
3431
+
3432
+ // Apply rest transforms to bones (local transforms)
3433
+ if ( restTransforms && restTransforms.length >= joints.length * 16 ) {
3434
+
3435
+ for ( let i = 0; i < joints.length; i ++ ) {
3436
+
3437
+ const matrix = new Matrix4();
3438
+ // USD matrices are row-major, Three.js is column-major - need to transpose
3439
+ const m = restTransforms.slice( i * 16, ( i + 1 ) * 16 );
3440
+ matrix.set(
3441
+ m[ 0 ], m[ 4 ], m[ 8 ], m[ 12 ],
3442
+ m[ 1 ], m[ 5 ], m[ 9 ], m[ 13 ],
3443
+ m[ 2 ], m[ 6 ], m[ 10 ], m[ 14 ],
3444
+ m[ 3 ], m[ 7 ], m[ 11 ], m[ 15 ]
3445
+ );
3446
+ matrix.decompose( bones[ i ].position, bones[ i ].quaternion, bones[ i ].scale );
3447
+
3448
+ }
3449
+
3450
+ }
3451
+
3452
+ // Find root bone(s) - bones without a parent bone
3453
+ const rootBones = bones.filter( bone => ! bone.parent || ! bone.parent.isBone );
3454
+
3455
+ // Get animation source path
3456
+ const animSourceSpec = this.specsByPath[ path + '.skel:animationSource' ];
3457
+ let animationPath = null;
3458
+ if ( animSourceSpec && animSourceSpec.fields.targetPaths && animSourceSpec.fields.targetPaths.length > 0 ) {
3459
+
3460
+ animationPath = animSourceSpec.fields.targetPaths[ 0 ];
3461
+
3462
+ }
3463
+
3464
+ return {
3465
+ skeleton: new Skeleton( bones, boneInverses ),
3466
+ joints: joints,
3467
+ rootBones: rootBones,
3468
+ animationPath: animationPath,
3469
+ path: path
3470
+ };
3471
+
3472
+ }
3473
+
3474
+ _bindSkeletons() {
3475
+
3476
+ for ( const meshData of this.skinnedMeshes ) {
3477
+
3478
+ const { mesh, skeletonPath, localJoints, geomBindTransform } = meshData;
3479
+
3480
+ let skeletonData = null;
3481
+
3482
+ // Try exact match first
3483
+ if ( skeletonPath && this.skeletons[ skeletonPath ] ) {
3484
+
3485
+ skeletonData = this.skeletons[ skeletonPath ];
3486
+
3487
+ }
3488
+
3489
+ // Try includes match as fallback
3490
+ if ( ! skeletonData ) {
3491
+
3492
+ for ( const skelPath in this.skeletons ) {
3493
+
3494
+ if ( skeletonPath && ( skeletonPath.includes( skelPath ) || skelPath.includes( skeletonPath ) ) ) {
3495
+
3496
+ skeletonData = this.skeletons[ skelPath ];
3497
+ break;
3498
+
3499
+ }
3500
+
3501
+ }
3502
+
3503
+ }
3504
+
3505
+ // Fallback to first skeleton for single-skeleton files
3506
+ if ( ! skeletonData ) {
3507
+
3508
+ const skeletonPaths = Object.keys( this.skeletons );
3509
+ if ( skeletonPaths.length > 0 ) {
3510
+
3511
+ skeletonData = this.skeletons[ skeletonPaths[ 0 ] ];
3512
+
3513
+ }
3514
+
3515
+ }
3516
+
3517
+ if ( ! skeletonData ) {
3518
+
3519
+ console.warn( 'USDComposer: No skeleton found for skinned mesh', mesh.name );
3520
+ continue;
3521
+
3522
+ }
3523
+
3524
+ const { skeleton, rootBones, joints } = skeletonData;
3525
+
3526
+ if ( localJoints && localJoints.length > 0 ) {
3527
+
3528
+ const skinIndex = mesh.geometry.attributes.skinIndex;
3529
+ if ( skinIndex ) {
3530
+
3531
+ const localToGlobal = [];
3532
+ for ( let i = 0; i < localJoints.length; i ++ ) {
3533
+
3534
+ const jointName = localJoints[ i ];
3535
+ const globalIdx = joints.indexOf( jointName );
3536
+ localToGlobal[ i ] = globalIdx >= 0 ? globalIdx : 0;
3537
+
3538
+ }
3539
+
3540
+ const arr = skinIndex.array;
3541
+ for ( let i = 0; i < arr.length; i ++ ) {
3542
+
3543
+ const localIdx = arr[ i ];
3544
+ if ( localIdx < localToGlobal.length ) {
3545
+
3546
+ arr[ i ] = localToGlobal[ localIdx ];
3547
+
3548
+ }
3549
+
3550
+ }
3551
+
3552
+ }
3553
+
3554
+ }
3555
+
3556
+ for ( const rootBone of rootBones ) {
3557
+
3558
+ mesh.add( rootBone );
3559
+
3560
+ }
3561
+
3562
+ // Use geomBindTransform if available, otherwise fall back to identity.
3563
+ // Estimating bind transforms from vertex/joint samples is not robust and can
3564
+ // produce severe skinning distortion for valid assets.
3565
+ let bindMatrix = new Matrix4();
3566
+
3567
+ if ( geomBindTransform && geomBindTransform.length === 16 ) {
3568
+
3569
+ // USD matrices are row-major, Three.js is column-major - need to transpose
3570
+ const m = geomBindTransform;
3571
+ bindMatrix.set(
3572
+ m[ 0 ], m[ 4 ], m[ 8 ], m[ 12 ],
3573
+ m[ 1 ], m[ 5 ], m[ 9 ], m[ 13 ],
3574
+ m[ 2 ], m[ 6 ], m[ 10 ], m[ 14 ],
3575
+ m[ 3 ], m[ 7 ], m[ 11 ], m[ 15 ]
3576
+ );
3577
+
3578
+ }
3579
+
3580
+ mesh.bind( skeleton, bindMatrix );
3581
+
3582
+ }
3583
+
3584
+ }
3585
+
3586
+ _buildAnimations() {
3587
+
3588
+ const animations = [];
3589
+
3590
+ // Find all SkelAnimation prims
3591
+ for ( const path in this.specsByPath ) {
3592
+
3593
+ const spec = this.specsByPath[ path ];
3594
+ if ( spec.specType !== SpecType.Prim ) continue;
3595
+ if ( spec.fields.typeName !== 'SkelAnimation' ) continue;
3596
+
3597
+ const clip = this._buildAnimationClip( path );
3598
+ if ( clip ) {
3599
+
3600
+ animations.push( clip );
3601
+
3602
+ }
3603
+
3604
+ }
3605
+
3606
+ // Build transform animations from time-sampled xformOps
3607
+ const transformTracks = this._buildTransformAnimations();
3608
+ if ( transformTracks.length > 0 ) {
3609
+
3610
+ animations.push( new AnimationClip( 'TransformAnimation', - 1, transformTracks ) );
3611
+
3612
+ }
3613
+
3614
+ return animations;
3615
+
3616
+ }
3617
+
3618
+ _buildTransformAnimations() {
3619
+
3620
+ const tracks = [];
3621
+
3622
+ for ( const path in this.specsByPath ) {
3623
+
3624
+ const spec = this.specsByPath[ path ];
3625
+ if ( spec.specType !== SpecType.Prim ) continue;
3626
+
3627
+ const typeName = spec.fields?.typeName;
3628
+ if ( typeName !== 'Xform' && typeName !== 'Scope' && typeName !== 'Mesh' ) continue;
3629
+
3630
+ const objectName = path.split( '/' ).pop();
3631
+
3632
+ // Check for animated xformOp:orient
3633
+ const orientPath = path + '.xformOp:orient';
3634
+ const orientSpec = this.specsByPath[ orientPath ];
3635
+ if ( orientSpec?.fields?.timeSamples ) {
3636
+
3637
+ const { times, values } = orientSpec.fields.timeSamples;
3638
+ const keyframeTimes = [];
3639
+ const keyframeValues = [];
3640
+
3641
+ for ( let i = 0; i < times.length; i ++ ) {
3642
+
3643
+ keyframeTimes.push( times[ i ] / this.fps );
3644
+
3645
+ const q = values[ i ];
3646
+ keyframeValues.push( q[ 0 ], q[ 1 ], q[ 2 ], q[ 3 ] );
3647
+
3648
+ }
3649
+
3650
+ if ( keyframeTimes.length > 0 ) {
3651
+
3652
+ tracks.push( new QuaternionKeyframeTrack(
3653
+ objectName + '.quaternion',
3654
+ new Float32Array( keyframeTimes ),
3655
+ new Float32Array( keyframeValues )
3656
+ ) );
3657
+
3658
+ }
3659
+
3660
+ }
3661
+
3662
+ // Check for animated xformOp:rotateXYZ
3663
+ const rotateXYZPath = path + '.xformOp:rotateXYZ';
3664
+ const rotateXYZSpec = this.specsByPath[ rotateXYZPath ];
3665
+ if ( rotateXYZSpec?.fields?.timeSamples ) {
3666
+
3667
+ const { times, values } = rotateXYZSpec.fields.timeSamples;
3668
+ const keyframeTimes = [];
3669
+ const keyframeValues = [];
3670
+ const tempEuler = new Euler();
3671
+ const tempQuat = new Quaternion();
3672
+
3673
+ for ( let i = 0; i < times.length; i ++ ) {
3674
+
3675
+ keyframeTimes.push( times[ i ] / this.fps );
3676
+
3677
+ const r = values[ i ];
3678
+ // USD rotateXYZ: matrix = Rx * Ry * Rz, use 'ZYX' order in Three.js
3679
+ tempEuler.set(
3680
+ r[ 0 ] * Math.PI / 180,
3681
+ r[ 1 ] * Math.PI / 180,
3682
+ r[ 2 ] * Math.PI / 180,
3683
+ 'ZYX'
3684
+ );
3685
+ tempQuat.setFromEuler( tempEuler );
3686
+ keyframeValues.push( tempQuat.x, tempQuat.y, tempQuat.z, tempQuat.w );
3687
+
3688
+ }
3689
+
3690
+ if ( keyframeTimes.length > 0 ) {
3691
+
3692
+ tracks.push( new QuaternionKeyframeTrack(
3693
+ objectName + '.quaternion',
3694
+ new Float32Array( keyframeTimes ),
3695
+ new Float32Array( keyframeValues )
3696
+ ) );
3697
+
3698
+ }
3699
+
3700
+ }
3701
+
3702
+ // Check for animated xformOp:translate
3703
+ const translatePath = path + '.xformOp:translate';
3704
+ const translateSpec = this.specsByPath[ translatePath ];
3705
+ if ( translateSpec?.fields?.timeSamples ) {
3706
+
3707
+ const { times, values } = translateSpec.fields.timeSamples;
3708
+ const keyframeTimes = [];
3709
+ const keyframeValues = [];
3710
+
3711
+ for ( let i = 0; i < times.length; i ++ ) {
3712
+
3713
+ keyframeTimes.push( times[ i ] / this.fps );
3714
+
3715
+ const t = values[ i ];
3716
+ keyframeValues.push( t[ 0 ], t[ 1 ], t[ 2 ] );
3717
+
3718
+ }
3719
+
3720
+ if ( keyframeTimes.length > 0 ) {
3721
+
3722
+ tracks.push( new VectorKeyframeTrack(
3723
+ objectName + '.position',
3724
+ new Float32Array( keyframeTimes ),
3725
+ new Float32Array( keyframeValues )
3726
+ ) );
3727
+
3728
+ }
3729
+
3730
+ }
3731
+
3732
+ // Check for animated xformOp:scale
3733
+ const scalePath = path + '.xformOp:scale';
3734
+ const scaleSpec = this.specsByPath[ scalePath ];
3735
+ if ( scaleSpec?.fields?.timeSamples ) {
3736
+
3737
+ const { times, values } = scaleSpec.fields.timeSamples;
3738
+ const keyframeTimes = [];
3739
+ const keyframeValues = [];
3740
+
3741
+ for ( let i = 0; i < times.length; i ++ ) {
3742
+
3743
+ keyframeTimes.push( times[ i ] / this.fps );
3744
+
3745
+ const s = values[ i ];
3746
+ keyframeValues.push( s[ 0 ], s[ 1 ], s[ 2 ] );
3747
+
3748
+ }
3749
+
3750
+ if ( keyframeTimes.length > 0 ) {
3751
+
3752
+ tracks.push( new VectorKeyframeTrack(
3753
+ objectName + '.scale',
3754
+ new Float32Array( keyframeTimes ),
3755
+ new Float32Array( keyframeValues )
3756
+ ) );
3757
+
3758
+ }
3759
+
3760
+ }
3761
+
3762
+ // Check for animated xformOp:transform (matrix animations)
3763
+ // These can have suffixes like xformOp:transform:transform
3764
+ const properties = spec.fields?.properties || [];
3765
+ for ( const prop of properties ) {
3766
+
3767
+ if ( ! prop.startsWith( 'xformOp:transform' ) ) continue;
3768
+
3769
+ const transformPath = path + '.' + prop;
3770
+ const transformSpec = this.specsByPath[ transformPath ];
3771
+
3772
+ if ( ! transformSpec?.fields?.timeSamples ) continue;
3773
+
3774
+ const { times, values } = transformSpec.fields.timeSamples;
3775
+ const positionTimes = [];
3776
+ const positionValues = [];
3777
+ const quaternionTimes = [];
3778
+ const quaternionValues = [];
3779
+ const scaleTimes = [];
3780
+ const scaleValues = [];
3781
+
3782
+ const matrix = new Matrix4();
3783
+ const position = new Vector3();
3784
+ const quaternion = new Quaternion();
3785
+ const scale = new Vector3();
3786
+
3787
+ for ( let i = 0; i < times.length; i ++ ) {
3788
+
3789
+ const m = values[ i ];
3790
+ if ( ! m || m.length < 16 ) continue;
3791
+
3792
+ const t = times[ i ] / this.fps;
3793
+
3794
+ // USD matrices are row-major, Three.js is column-major
3795
+ matrix.set(
3796
+ m[ 0 ], m[ 4 ], m[ 8 ], m[ 12 ],
3797
+ m[ 1 ], m[ 5 ], m[ 9 ], m[ 13 ],
3798
+ m[ 2 ], m[ 6 ], m[ 10 ], m[ 14 ],
3799
+ m[ 3 ], m[ 7 ], m[ 11 ], m[ 15 ]
3800
+ );
3801
+
3802
+ matrix.decompose( position, quaternion, scale );
3803
+
3804
+ positionTimes.push( t );
3805
+ positionValues.push( position.x, position.y, position.z );
3806
+
3807
+ quaternionTimes.push( t );
3808
+ quaternionValues.push( quaternion.x, quaternion.y, quaternion.z, quaternion.w );
3809
+
3810
+ scaleTimes.push( t );
3811
+ scaleValues.push( scale.x, scale.y, scale.z );
3812
+
3813
+ }
3814
+
3815
+ if ( positionTimes.length > 0 ) {
3816
+
3817
+ tracks.push( new VectorKeyframeTrack(
3818
+ objectName + '.position',
3819
+ new Float32Array( positionTimes ),
3820
+ new Float32Array( positionValues )
3821
+ ) );
3822
+
3823
+ tracks.push( new QuaternionKeyframeTrack(
3824
+ objectName + '.quaternion',
3825
+ new Float32Array( quaternionTimes ),
3826
+ new Float32Array( quaternionValues )
3827
+ ) );
3828
+
3829
+ tracks.push( new VectorKeyframeTrack(
3830
+ objectName + '.scale',
3831
+ new Float32Array( scaleTimes ),
3832
+ new Float32Array( scaleValues )
3833
+ ) );
3834
+
3835
+ }
3836
+
3837
+ break; // Only process first transform op
3838
+
3839
+ }
3840
+
3841
+ }
3842
+
3843
+ return tracks;
3844
+
3845
+ }
3846
+
3847
+ _buildAnimationClip( path ) {
3848
+
3849
+ const attrs = this._getAttributes( path );
3850
+ const joints = attrs[ 'joints' ];
3851
+
3852
+ if ( ! joints || joints.length === 0 ) return null;
3853
+
3854
+ const tracks = [];
3855
+
3856
+ // Get rotation time samples
3857
+ const rotationsAttr = this._getTimeSampledAttribute( path, 'rotations' );
3858
+ if ( rotationsAttr && rotationsAttr.times && rotationsAttr.values ) {
3859
+
3860
+ const { times, values } = rotationsAttr;
3861
+
3862
+ for ( let jointIdx = 0; jointIdx < joints.length; jointIdx ++ ) {
3863
+
3864
+ const jointName = joints[ jointIdx ].split( '/' ).pop();
3865
+ const keyframeTimes = [];
3866
+ const keyframeValues = [];
3867
+
3868
+ for ( let t = 0; t < times.length; t ++ ) {
3869
+
3870
+ const quatData = values[ t ];
3871
+ if ( ! quatData || quatData.length < ( jointIdx + 1 ) * 4 ) continue;
3872
+
3873
+ keyframeTimes.push( times[ t ] / this.fps );
3874
+
3875
+ // USD GfQuatf stores imaginary (x,y,z) first, then real (w)
3876
+ // This matches Three.js quaternion order (x,y,z,w)
3877
+ const x = quatData[ jointIdx * 4 + 0 ];
3878
+ const y = quatData[ jointIdx * 4 + 1 ];
3879
+ const z = quatData[ jointIdx * 4 + 2 ];
3880
+ const w = quatData[ jointIdx * 4 + 3 ];
3881
+ keyframeValues.push( x, y, z, w );
3882
+
3883
+ }
3884
+
3885
+ if ( keyframeTimes.length > 0 ) {
3886
+
3887
+ tracks.push( new QuaternionKeyframeTrack(
3888
+ jointName + '.quaternion',
3889
+ new Float32Array( keyframeTimes ),
3890
+ new Float32Array( keyframeValues )
3891
+ ) );
3892
+
3893
+ }
3894
+
3895
+ }
3896
+
3897
+ }
3898
+
3899
+ // Get translation time samples
3900
+ const translationsAttr = this._getTimeSampledAttribute( path, 'translations' );
3901
+ if ( translationsAttr && translationsAttr.times && translationsAttr.values ) {
3902
+
3903
+ const { times, values } = translationsAttr;
3904
+
3905
+ for ( let jointIdx = 0; jointIdx < joints.length; jointIdx ++ ) {
3906
+
3907
+ const jointName = joints[ jointIdx ].split( '/' ).pop();
3908
+ const keyframeTimes = [];
3909
+ const keyframeValues = [];
3910
+
3911
+ for ( let t = 0; t < times.length; t ++ ) {
3912
+
3913
+ const transData = values[ t ];
3914
+ if ( ! transData || transData.length < ( jointIdx + 1 ) * 3 ) continue;
3915
+
3916
+ keyframeTimes.push( times[ t ] / this.fps );
3917
+ keyframeValues.push(
3918
+ transData[ jointIdx * 3 + 0 ],
3919
+ transData[ jointIdx * 3 + 1 ],
3920
+ transData[ jointIdx * 3 + 2 ]
3921
+ );
3922
+
3923
+ }
3924
+
3925
+ if ( keyframeTimes.length > 0 ) {
3926
+
3927
+ tracks.push( new VectorKeyframeTrack(
3928
+ jointName + '.position',
3929
+ new Float32Array( keyframeTimes ),
3930
+ new Float32Array( keyframeValues )
3931
+ ) );
3932
+
3933
+ }
3934
+
3935
+ }
3936
+
3937
+ }
3938
+
3939
+ // Get scale time samples
3940
+ const scalesAttr = this._getTimeSampledAttribute( path, 'scales' );
3941
+ if ( scalesAttr && scalesAttr.times && scalesAttr.values ) {
3942
+
3943
+ const { times, values } = scalesAttr;
3944
+
3945
+ for ( let jointIdx = 0; jointIdx < joints.length; jointIdx ++ ) {
3946
+
3947
+ const jointName = joints[ jointIdx ].split( '/' ).pop();
3948
+ const keyframeTimes = [];
3949
+ const keyframeValues = [];
3950
+
3951
+ for ( let t = 0; t < times.length; t ++ ) {
3952
+
3953
+ const scaleData = values[ t ];
3954
+ if ( ! scaleData || scaleData.length < ( jointIdx + 1 ) * 3 ) continue;
3955
+
3956
+ keyframeTimes.push( times[ t ] / this.fps );
3957
+ keyframeValues.push(
3958
+ scaleData[ jointIdx * 3 + 0 ],
3959
+ scaleData[ jointIdx * 3 + 1 ],
3960
+ scaleData[ jointIdx * 3 + 2 ]
3961
+ );
3962
+
3963
+ }
3964
+
3965
+ if ( keyframeTimes.length > 0 ) {
3966
+
3967
+ tracks.push( new VectorKeyframeTrack(
3968
+ jointName + '.scale',
3969
+ new Float32Array( keyframeTimes ),
3970
+ new Float32Array( keyframeValues )
3971
+ ) );
3972
+
3973
+ }
3974
+
3975
+ }
3976
+
3977
+ }
3978
+
3979
+ if ( tracks.length === 0 ) return null;
3980
+
3981
+ const clipName = path.split( '/' ).pop();
3982
+ return new AnimationClip( clipName, - 1, tracks );
3983
+
3984
+ }
3985
+
3986
+ _getTimeSampledAttribute( primPath, attrName ) {
3987
+
3988
+ // Look for the attribute spec with time samples
3989
+ const attrPath = primPath + '.' + attrName;
3990
+ const attrSpec = this.specsByPath[ attrPath ];
3991
+
3992
+ if ( attrSpec && attrSpec.fields.timeSamples ) {
3993
+
3994
+ const timeSamples = attrSpec.fields.timeSamples;
3995
+ if ( timeSamples.times && timeSamples.values ) {
3996
+
3997
+ return timeSamples;
3998
+
3999
+ }
4000
+
4001
+ }
4002
+
4003
+ return null;
4004
+
4005
+ }
4006
+
4007
+ _flattenMatrixArray( matrices, numMatrices ) {
4008
+
4009
+ if ( ! matrices || matrices.length === 0 ) return null;
4010
+
4011
+ if ( typeof matrices[ 0 ] === 'number' ) return matrices;
4012
+
4013
+ const flatArray = [];
4014
+
4015
+ for ( let m = 0; m < numMatrices; m ++ ) {
4016
+
4017
+ for ( let row = 0; row < 4; row ++ ) {
4018
+
4019
+ const rowData = matrices[ m * 4 + row ];
4020
+
4021
+ if ( rowData && rowData.length === 4 ) {
4022
+
4023
+ flatArray.push( rowData[ 0 ], rowData[ 1 ], rowData[ 2 ], rowData[ 3 ] );
4024
+
4025
+ } else {
4026
+
4027
+ flatArray.push( row === 0 ? 1 : 0, row === 1 ? 1 : 0, row === 2 ? 1 : 0, row === 3 ? 1 : 0 );
4028
+
4029
+ }
4030
+
4031
+ }
4032
+
4033
+ }
4034
+
4035
+ return flatArray;
4036
+
4037
+ }
4038
+
4039
+ }
4040
+
4041
+ export { USDComposer, SpecType };