@needle-tools/usd 1.0.0-next.d536d99 → 1.0.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.
Binary file
@@ -3,7 +3,7 @@
3
3
  "openusd": {
4
4
  "version": "0.26.5",
5
5
  "pxrVersion": 2605,
6
- "gitSha": "091e1c02196d7bbda8b536ec745b36824da71589",
6
+ "gitSha": "9f211a656877c26b39e8170f9ed53810094254d5",
7
7
  "gitDirty": false
8
8
  },
9
9
  "toolchain": {
@@ -349,6 +349,9 @@ export async function createThreeHydra(config) {
349
349
 
350
350
  /** Draw once, after stage metadata has been applied to the root scene. */
351
351
  const initialDrawPromise = draw();
352
+ const readyPromise = config.waitForMaterials
353
+ ? initialDrawPromise.then(() => renderInterface.waitForMaterialsReady())
354
+ : initialDrawPromise;
352
355
 
353
356
  let time = 0;
354
357
  let currentTimeCode = stageStartTimeCode;
@@ -383,7 +386,7 @@ export async function createThreeHydra(config) {
383
386
 
384
387
  return {
385
388
  driver: /** @type {import(".").HdWebSyncDriver} */ (driverOrPromise),
386
- ready: () => initialDrawPromise,
389
+ ready: () => readyPromise,
387
390
  update: (dt) => {
388
391
  // ensure we're not dead
389
392
  if (driver.isDeleted()) {
@@ -394,6 +397,9 @@ export async function createThreeHydra(config) {
394
397
  }
395
398
  return;
396
399
  }
400
+ if (drawInFlight || editInFlight) {
401
+ return;
402
+ }
397
403
  if (playing) {
398
404
  time += dt;
399
405
  const startTimeCode = stageStartTimeCode;
@@ -6,6 +6,8 @@ const {
6
6
  TextureLoader,
7
7
  BufferGeometry,
8
8
  MeshPhysicalMaterial,
9
+ FrontSide,
10
+ BackSide,
9
11
  DoubleSide,
10
12
  Color,
11
13
  Mesh,
@@ -80,6 +82,31 @@ function disposeObjectResources(object) {
80
82
  object.parent?.remove(object);
81
83
  }
82
84
 
85
+ function isFiniteArray(values, dimension = 3) {
86
+ if (!values || values.length === 0 || values.length % dimension !== 0) return false;
87
+ for (let i = 0; i < values.length; i++) {
88
+ if (!Number.isFinite(values[i])) return false;
89
+ }
90
+ return true;
91
+ }
92
+
93
+ function cullStyleToThreeSide(doubleSided, cullStyle) {
94
+ switch (cullStyle) {
95
+ case "nothing":
96
+ return DoubleSide;
97
+ case "back":
98
+ return FrontSide;
99
+ case "front":
100
+ return BackSide;
101
+ case "frontUnlessDoubleSided":
102
+ return doubleSided ? DoubleSide : BackSide;
103
+ case "backUnlessDoubleSided":
104
+ case "dontCare":
105
+ default:
106
+ return doubleSided ? DoubleSide : FrontSide;
107
+ }
108
+ }
109
+
83
110
  function primNameFromPath(id, fallback) {
84
111
  const path = String(id || "");
85
112
  const slash = path.lastIndexOf("/");
@@ -258,8 +285,6 @@ class TextureRegistry {
258
285
  } else if (extension === 'jpeg') {
259
286
  filetype = 'image/jpeg';
260
287
  } else if (extension === 'exr') {
261
- console.warn("EXR textures are not fully supported yet", resourcePath);
262
- // using EXRLoader explicitly
263
288
  filetype = 'image/x-exr';
264
289
  } else if (extension === 'tga') {
265
290
  console.warn("TGA textures are not fully supported yet", resourcePath);
@@ -527,6 +552,8 @@ class HydraMesh {
527
552
  this._uvs = undefined;
528
553
  this._indices = undefined;
529
554
  this._materials = [];
555
+ this._materialSideClones = new Map();
556
+ this._side = DoubleSide;
530
557
  this._visible = false;
531
558
  this._renderTag = 'geometry';
532
559
  this._instancedMesh = null;
@@ -540,6 +567,7 @@ class HydraMesh {
540
567
  this._ownedMaterial = material;
541
568
  this._materials.push(material);
542
569
  this._mesh = new Mesh(this._geometry, material);
570
+ this._installMeshHooks(this._mesh);
543
571
  this._mesh.visible = false;
544
572
  this._mesh.castShadow = true;
545
573
  this._mesh.receiveShadow = true;
@@ -561,6 +589,7 @@ class HydraMesh {
561
589
  if (!this._mesh) return;
562
590
  this._interface.unassignMeshFromMaterials(this._mesh);
563
591
  this._disposeInstancedMesh();
592
+ this._disposeMaterialSideClones();
564
593
  if (this._mesh.parent) {
565
594
  this._mesh.parent.remove(this._mesh);
566
595
  }
@@ -573,13 +602,25 @@ class HydraMesh {
573
602
  updateOrder(attribute, attributeName, dimension = 3) {
574
603
  if (debugMeshes) console.log("updateOrder", attribute, attributeName, dimension);
575
604
  if (attribute && this._indices) {
605
+ if (!isFiniteArray(attribute, dimension)) {
606
+ this._geometry.deleteAttribute(attributeName);
607
+ return;
608
+ }
576
609
  let values = [];
577
610
  for (let i = 0; i < this._indices.length; i++) {
578
611
  let index = this._indices[i]
612
+ if (!Number.isInteger(index) || index < 0 || (dimension * index + dimension) > attribute.length) {
613
+ this._geometry.deleteAttribute(attributeName);
614
+ return;
615
+ }
579
616
  for (let j = 0; j < dimension; ++j) {
580
617
  values.push(attribute[dimension * index + j]);
581
618
  }
582
619
  }
620
+ if (!isFiniteArray(values, dimension)) {
621
+ this._geometry.deleteAttribute(attributeName);
622
+ return;
623
+ }
583
624
  this._geometry.setAttribute(attributeName, new Float32BufferAttribute(values, dimension));
584
625
  if (attributeName === 'position') {
585
626
  this._geometry.computeBoundingBox();
@@ -628,6 +669,7 @@ class HydraMesh {
628
669
  if (!this._instancedMesh || this._instancedMesh.count !== instanceCount) {
629
670
  this._disposeInstancedMesh();
630
671
  this._instancedMesh = new InstancedMesh(this._geometry, this._mesh.material, instanceCount);
672
+ this._installMeshHooks(this._instancedMesh);
631
673
  this._instancedMesh.name = `${this._mesh.name}_instances`;
632
674
  this._instancedMesh.castShadow = this._mesh.castShadow;
633
675
  this._instancedMesh.receiveShadow = this._mesh.receiveShadow;
@@ -662,6 +704,78 @@ class HydraMesh {
662
704
  this._instancedMesh = null;
663
705
  }
664
706
 
707
+ _installMeshHooks(mesh) {
708
+ mesh.userData.usdPath = this._id;
709
+ mesh.userData.usdHydraMaterialSide = this._side;
710
+ mesh.userData.usdHydraApplyMaterialSide = (material, hydraMaterial) => this._applyMaterialSide(material, hydraMaterial);
711
+ }
712
+
713
+ _disposeMaterialSideClones() {
714
+ for (const material of this._materialSideClones.values()) {
715
+ material.dispose?.();
716
+ }
717
+ this._materialSideClones.clear();
718
+ }
719
+
720
+ _updateMeshSideState() {
721
+ if (this._mesh) {
722
+ this._mesh.userData.usdHydraMaterialSide = this._side;
723
+ }
724
+ if (this._instancedMesh) {
725
+ this._instancedMesh.userData.usdHydraMaterialSide = this._side;
726
+ }
727
+ }
728
+
729
+ _applyMaterialSide(material, hydraMaterial = null) {
730
+ if (!material) return material;
731
+ if (Array.isArray(material)) {
732
+ return material.map(entry => this._applyMaterialSide(entry, hydraMaterial));
733
+ }
734
+ if (material === this._ownedMaterial || material.userData?.usdHydraMeshOwner === this._id) {
735
+ material.side = this._side;
736
+ material.needsUpdate = true;
737
+ return material;
738
+ }
739
+
740
+ if (!hydraMaterial?.requiresMaterialSideVariants?.()) {
741
+ material.side = this._side;
742
+ material.needsUpdate = true;
743
+ return material;
744
+ }
745
+
746
+ let clone = this._materialSideClones.get(material);
747
+ if (!clone) {
748
+ clone = material.clone();
749
+ clone.userData.usdHydraSideCloneOf = material.uuid;
750
+ this._materialSideClones.set(material, clone);
751
+ } else {
752
+ clone.copy(material);
753
+ clone.userData.usdHydraSideCloneOf = material.uuid;
754
+ }
755
+ clone.side = this._side;
756
+ clone.needsUpdate = true;
757
+ return clone;
758
+ }
759
+
760
+ _applyCullSideToMeshes() {
761
+ this._updateMeshSideState();
762
+ let refreshedMaterialAssignments = false;
763
+ if (this._mesh) {
764
+ refreshedMaterialAssignments = this._interface.refreshMeshMaterialAssignments(this._mesh);
765
+ if (!refreshedMaterialAssignments) {
766
+ this._mesh.material = this._applyMaterialSide(this._mesh.material);
767
+ }
768
+ }
769
+ if (this._instancedMesh) {
770
+ this._instancedMesh.material = this._mesh?.material;
771
+ }
772
+ }
773
+
774
+ setCullStyle(doubleSided, cullStyle) {
775
+ this._side = cullStyleToThreeSide(Boolean(doubleSided), String(cullStyle || "dontCare"));
776
+ this._applyCullSideToMeshes();
777
+ }
778
+
665
779
  setVisibilityState(visible, renderTag = 'geometry') {
666
780
  this._visible = Boolean(visible);
667
781
  this._renderTag = String(renderTag || 'geometry');
@@ -687,12 +801,20 @@ class HydraMesh {
687
801
  this.updateOrder(this._normals, 'normal');
688
802
  }
689
803
 
804
+ updateOrderedNormals(normals) {
805
+ // don't apply automatically generated normals if there are already authored normals.
806
+ if (this._geometry.hasAttribute('normal')) return;
807
+
808
+ this._normals = normals.slice(0);
809
+ this._geometry.setAttribute('normal', new Float32BufferAttribute(this._normals, 3));
810
+ }
811
+
690
812
  setNormals(data, interpolation) {
691
813
  if (interpolation === 'facevarying') {
692
814
  // The UV buffer has already been prepared on the C++ side, so we just set it
693
815
  this._geometry.setAttribute('normal', new Float32BufferAttribute(data, 3));
694
- } else if (interpolation === 'vertex') {
695
- // We have per-vertex UVs, so we need to sort them accordingly
816
+ } else if (interpolation === 'vertex' || interpolation === 'varying') {
817
+ // Per-point data is sorted into the expanded triangle order.
696
818
  this._normals = data.slice(0);
697
819
  this.updateOrder(this._normals, 'normal');
698
820
  }
@@ -701,7 +823,7 @@ class HydraMesh {
701
823
  setTangents(data, dimension, interpolation) {
702
824
  if (interpolation === 'facevarying') {
703
825
  this._geometry.setAttribute('tangent', new Float32BufferAttribute(data, dimension));
704
- } else if (interpolation === 'vertex') {
826
+ } else if (interpolation === 'vertex' || interpolation === 'varying') {
705
827
  this._tangents = data.slice(0);
706
828
  this.updateOrder(this._tangents, 'tangent', dimension);
707
829
  }
@@ -735,7 +857,9 @@ class HydraMesh {
735
857
  this._mesh.parent.remove(this._mesh);
736
858
  }
737
859
  this._mesh = new Mesh(this._geometry, this._materials);
860
+ this._installMeshHooks(this._mesh);
738
861
  this.setVisibilityState(this._visible, this._renderTag);
862
+ this._applyCullSideToMeshes();
739
863
  this._interface.config.usdRoot.add(this._mesh);
740
864
 
741
865
  for (let i = 0; i < sections.length; i++) {
@@ -750,14 +874,16 @@ class HydraMesh {
750
874
  let wasDefaultMaterial = false;
751
875
  if (this._mesh.material === defaultMaterial) {
752
876
  this._mesh.material = this._mesh.material.clone();
877
+ this._mesh.material.userData.usdHydraMeshOwner = this._id;
753
878
  wasDefaultMaterial = true;
754
879
  }
880
+ this._mesh.material = this._applyMaterialSide(this._mesh.material);
755
881
 
756
882
  this._colors = null;
757
883
 
758
884
  if (interpolation === 'constant') {
759
885
  this._mesh.material.color = new Color().fromArray(data);
760
- } else if (interpolation === 'vertex') {
886
+ } else if (interpolation === 'vertex' || interpolation === 'varying') {
761
887
  // Per-vertex buffer attribute
762
888
  this._mesh.material.vertexColors = true;
763
889
  if (wasDefaultMaterial) {
@@ -784,8 +910,8 @@ class HydraMesh {
784
910
  if (interpolation === 'facevarying') {
785
911
  // The UV buffer has already been prepared on the C++ side, so we just set it
786
912
  this._geometry.setAttribute('uv', new Float32BufferAttribute(data, dimension));
787
- } else if (interpolation === 'vertex') {
788
- // We have per-vertex UVs, so we need to sort them accordingly
913
+ } else if (interpolation === 'vertex' || interpolation === 'varying') {
914
+ // Per-point data is sorted into the expanded triangle order.
789
915
  this._uvs = data.slice(0);
790
916
  this.updateOrder(this._uvs, 'uv', 2);
791
917
  }
@@ -937,11 +1063,15 @@ class HydraMaterial {
937
1063
  if (!existing) {
938
1064
  this._assignments.push({ mesh, materialIndex });
939
1065
  }
940
- this._applyMaterialToMesh(mesh, materialIndex);
1066
+ this._applyMaterialToAssignedMeshes();
941
1067
  }
942
1068
 
943
1069
  unassignMesh(mesh) {
1070
+ const previousLength = this._assignments.length;
944
1071
  this._assignments = this._assignments.filter(assignment => assignment.mesh !== mesh);
1072
+ if (this._assignments.length !== previousLength) {
1073
+ this._applyMaterialToAssignedMeshes();
1074
+ }
945
1075
  }
946
1076
 
947
1077
  dispose() {
@@ -951,17 +1081,22 @@ class HydraMaterial {
951
1081
  }
952
1082
 
953
1083
  _applyMaterialToMesh(mesh, materialIndex) {
1084
+ const applyMaterialSide = mesh.userData?.usdHydraApplyMaterialSide;
1085
+ const material = typeof applyMaterialSide === 'function'
1086
+ ? applyMaterialSide(this._material, this)
1087
+ : this._material;
1088
+
954
1089
  if (materialIndex === null || materialIndex === undefined) {
955
- mesh.material = this._material;
1090
+ mesh.material = material;
956
1091
  return;
957
1092
  }
958
1093
 
959
1094
  if (Array.isArray(mesh.material)) {
960
- mesh.material[materialIndex] = this._material;
1095
+ mesh.material[materialIndex] = material;
961
1096
  return;
962
1097
  }
963
1098
 
964
- mesh.material = this._material;
1099
+ mesh.material = material;
965
1100
  }
966
1101
 
967
1102
  _applyMaterialToAssignedMeshes() {
@@ -970,6 +1105,28 @@ class HydraMaterial {
970
1105
  }
971
1106
  }
972
1107
 
1108
+ hasMeshAssignment(mesh) {
1109
+ return this._assignments.some(assignment => assignment.mesh === mesh);
1110
+ }
1111
+
1112
+ requiresMaterialSideVariants() {
1113
+ const sides = new Set();
1114
+ for (const assignment of this._assignments) {
1115
+ const side = assignment.mesh?.userData?.usdHydraMaterialSide;
1116
+ if (typeof side === 'number') {
1117
+ sides.add(side);
1118
+ }
1119
+ if (sides.size > 1) {
1120
+ return true;
1121
+ }
1122
+ }
1123
+ return false;
1124
+ }
1125
+
1126
+ refreshAssignments() {
1127
+ this._applyMaterialToAssignedMeshes();
1128
+ }
1129
+
973
1130
  beginMaterialSync() {
974
1131
  this._nodes = {};
975
1132
  this._resolvedAssetPaths.clear();
@@ -1749,6 +1906,16 @@ export class ThreeRenderDelegateInterface {
1749
1906
  }
1750
1907
  }
1751
1908
 
1909
+ refreshMeshMaterialAssignments(mesh) {
1910
+ let refreshed = false;
1911
+ for (const material of Object.values(this.materials)) {
1912
+ if (!material.hasMeshAssignment(mesh)) continue;
1913
+ material.refreshAssignments();
1914
+ refreshed = true;
1915
+ }
1916
+ return refreshed;
1917
+ }
1918
+
1752
1919
  CommitResources() {
1753
1920
  for (const id in this.meshes) {
1754
1921
  const hydraMesh = this.meshes[id]
@@ -129,6 +129,7 @@ function onAddNeedlePlugin(NEEDLE, opts) {
129
129
  scene: this.comp.root,
130
130
  url: url,
131
131
  files: files,
132
+ waitForMaterials: opts.waitForMaterials,
132
133
  });
133
134
  this.comp.handle = handle;
134
135
  hydraHandlesByRoot.set(this.comp.root, handle);
@@ -71,6 +71,13 @@ export declare type createThreeHydraConfig = {
71
71
  * only controls the helper/demo Three light brightness.
72
72
  */
73
73
  scenePrimitiveLightIntensityScale?: number,
74
+
75
+ /**
76
+ * Include asynchronous material generation and texture assignment in ready().
77
+ * Defaults to false so stage loading and first draw are not blocked by materials.
78
+ * Call handle.materialsReady() when you need an explicit material barrier.
79
+ */
80
+ waitForMaterials?: boolean,
74
81
  }
75
82
 
76
83
  /**
@@ -109,6 +116,8 @@ export declare type NeedleThreeHydraHandle = {
109
116
  */
110
117
  editStage: <T>(callback: (stage: USDStage, driver: HdWebSyncDriver) => T | Promise<T>) => Promise<T | undefined>,
111
118
  /** Resolves after the initial Hydra draw has settled.
119
+ * If createThreeHydra was called with waitForMaterials, also waits for
120
+ * asynchronous material generation and texture assignment.
112
121
  */
113
122
  ready: () => Promise<void>,
114
123
  /** Resolves when asynchronous material generation and texture assignment have settled.
@@ -2,6 +2,11 @@
2
2
 
3
3
  export type PluginContext = {
4
4
  debug?: boolean,
5
+ /**
6
+ * Include asynchronous material generation and texture assignment in the
7
+ * Hydra handle's ready() promise. Defaults to false.
8
+ */
9
+ waitForMaterials?: boolean,
5
10
  getFiles: () => Array<import("../types").HydraFile>
6
11
  }
7
12
 
package/src/vite/index.js CHANGED
@@ -1,7 +1,19 @@
1
+ const packageRoot = decodeURIComponent(new URL('../..', import.meta.url).pathname);
1
2
 
2
3
  /** @type {import('vite').Plugin} */
3
4
  const crossOriginIsolatedPlugin = {
4
5
  name: 'needle:usd-crossoriginisolated',
6
+ config: (config) => {
7
+ const cwd = typeof process !== 'undefined' && process.cwd ? process.cwd() : '/';
8
+ const allow = config.server?.fs?.allow ?? [cwd];
9
+ return {
10
+ server: {
11
+ fs: {
12
+ allow: [...new Set([...allow, packageRoot])],
13
+ },
14
+ },
15
+ };
16
+ },
5
17
  configureServer: (server) => {
6
18
  server.middlewares.use((_req, res, next) => {
7
19
  res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
@@ -19,4 +31,4 @@ const crossOriginIsolatedPlugin = {
19
31
  */
20
32
  export function needleUSD() {
21
33
  return [crossOriginIsolatedPlugin]
22
- }
34
+ }