@ioai/robot_viewer 0.0.0-stage → 1.1.2

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 (159) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +217 -2
  3. package/bin/cli.js +83 -0
  4. package/dist/assets/css/PlotPanel-3e7d7555.css +1 -0
  5. package/dist/assets/js/ColladaLoader-2a0c4638.js +1 -0
  6. package/dist/assets/js/GLTFLoader-7a7f40d7.js +1 -0
  7. package/dist/assets/js/MTLLoader-b6ce88b7.js +1 -0
  8. package/dist/assets/js/OBJLoader-7667aa01.js +1 -0
  9. package/dist/assets/js/PlotPanel-09f986c7.js +1 -0
  10. package/dist/assets/js/STLLoader-de0915d4.js +1 -0
  11. package/dist/assets/js/URDFLoader-a34b6917.js +1 -0
  12. package/dist/assets/js/USDZLoader-64197f74.js +7 -0
  13. package/dist/assets/js/__vite-browser-external-afb87ea8.js +1 -0
  14. package/dist/assets/js/codemirror-c3cec7b3.js +1 -0
  15. package/dist/assets/js/codemirror-themes-e6995a6a.js +1 -0
  16. package/dist/assets/js/d3-2e308866.js +1 -0
  17. package/dist/assets/js/index-56e94ce5.js +6 -0
  18. package/dist/assets/js/mujoco_wasm-345a2576.js +1 -0
  19. package/dist/assets/js/three-9c891de0.js +6 -0
  20. package/dist/camera-publish-worker.js +1 -0
  21. package/dist/favicon.svg +2 -0
  22. package/dist/heartbeat-worker.js +1 -0
  23. package/dist/index.html +4373 -0
  24. package/dist/models/mocap/blender_human.urdf +2319 -0
  25. package/dist/models/mocap/blender_human_skeleton_v4.urdf +1432 -0
  26. package/dist/models/mocap/blender_human_skeleton_v5.urdf +2717 -0
  27. package/dist/models/mocap/meshes/v4/L_base_link.STL +0 -0
  28. package/dist/models/mocap/meshes/v4/L_figer1_1.STL +0 -0
  29. package/dist/models/mocap/meshes/v4/L_figer1_2.STL +0 -0
  30. package/dist/models/mocap/meshes/v4/L_figer1_3.STL +0 -0
  31. package/dist/models/mocap/meshes/v4/L_figer1_4.STL +0 -0
  32. package/dist/models/mocap/meshes/v4/L_figer1_5.STL +0 -0
  33. package/dist/models/mocap/meshes/v4/L_figer1base.STL +0 -0
  34. package/dist/models/mocap/meshes/v4/L_figer2_1.STL +0 -0
  35. package/dist/models/mocap/meshes/v4/L_figer2_2.STL +0 -0
  36. package/dist/models/mocap/meshes/v4/L_figer2_3.STL +0 -0
  37. package/dist/models/mocap/meshes/v4/L_figer2_4.STL +0 -0
  38. package/dist/models/mocap/meshes/v4/L_figer2_5.STL +0 -0
  39. package/dist/models/mocap/meshes/v4/L_figer3_1.STL +0 -0
  40. package/dist/models/mocap/meshes/v4/L_figer3_2.STL +0 -0
  41. package/dist/models/mocap/meshes/v4/L_figer3_3.STL +0 -0
  42. package/dist/models/mocap/meshes/v4/L_figer3_4.STL +0 -0
  43. package/dist/models/mocap/meshes/v4/L_figer3_5.STL +0 -0
  44. package/dist/models/mocap/meshes/v4/L_figer4_1.STL +0 -0
  45. package/dist/models/mocap/meshes/v4/L_figer4_2.STL +0 -0
  46. package/dist/models/mocap/meshes/v4/L_figer4_3.STL +0 -0
  47. package/dist/models/mocap/meshes/v4/L_figer4_4.STL +0 -0
  48. package/dist/models/mocap/meshes/v4/L_figer4_5.STL +0 -0
  49. package/dist/models/mocap/meshes/v4/L_figer5_1.STL +0 -0
  50. package/dist/models/mocap/meshes/v4/L_figer5_2.STL +0 -0
  51. package/dist/models/mocap/meshes/v4/L_figer5_3.STL +0 -0
  52. package/dist/models/mocap/meshes/v4/L_figer5_4.STL +0 -0
  53. package/dist/models/mocap/meshes/v4/L_figer5_5.STL +0 -0
  54. package/dist/models/mocap/meshes/v4/L_handle.STL +0 -0
  55. package/dist/models/mocap/meshes/v4/L_handleseat.STL +0 -0
  56. package/dist/models/mocap/meshes/v4/L_mark.STL +0 -0
  57. package/dist/models/mocap/meshes/v4/L_markseat.STL +0 -0
  58. package/dist/models/mocap/meshes/v4/Lgamepadbase.STL +0 -0
  59. package/dist/models/mocap/meshes/v4/Link1.STL +0 -0
  60. package/dist/models/mocap/meshes/v4/Link2.STL +0 -0
  61. package/dist/models/mocap/meshes/v4/R_base_link.STL +0 -0
  62. package/dist/models/mocap/meshes/v4/R_figer1_1.STL +0 -0
  63. package/dist/models/mocap/meshes/v4/R_figer1_2.STL +0 -0
  64. package/dist/models/mocap/meshes/v4/R_figer1_3.STL +0 -0
  65. package/dist/models/mocap/meshes/v4/R_figer1_4.STL +0 -0
  66. package/dist/models/mocap/meshes/v4/R_figer1_5.STL +0 -0
  67. package/dist/models/mocap/meshes/v4/R_figer1base.STL +0 -0
  68. package/dist/models/mocap/meshes/v4/R_figer2_1.STL +0 -0
  69. package/dist/models/mocap/meshes/v4/R_figer2_2.STL +0 -0
  70. package/dist/models/mocap/meshes/v4/R_figer2_3.STL +0 -0
  71. package/dist/models/mocap/meshes/v4/R_figer2_4.STL +0 -0
  72. package/dist/models/mocap/meshes/v4/R_figer2_5.STL +0 -0
  73. package/dist/models/mocap/meshes/v4/R_figer3_1.STL +0 -0
  74. package/dist/models/mocap/meshes/v4/R_figer3_2.STL +0 -0
  75. package/dist/models/mocap/meshes/v4/R_figer3_3.STL +0 -0
  76. package/dist/models/mocap/meshes/v4/R_figer3_4.STL +0 -0
  77. package/dist/models/mocap/meshes/v4/R_figer3_5.STL +0 -0
  78. package/dist/models/mocap/meshes/v4/R_figer4_1.STL +0 -0
  79. package/dist/models/mocap/meshes/v4/R_figer4_2.STL +0 -0
  80. package/dist/models/mocap/meshes/v4/R_figer4_3.STL +0 -0
  81. package/dist/models/mocap/meshes/v4/R_figer4_4.STL +0 -0
  82. package/dist/models/mocap/meshes/v4/R_figer4_5.STL +0 -0
  83. package/dist/models/mocap/meshes/v4/R_figer5_1.STL +0 -0
  84. package/dist/models/mocap/meshes/v4/R_figer5_2.STL +0 -0
  85. package/dist/models/mocap/meshes/v4/R_figer5_3.STL +0 -0
  86. package/dist/models/mocap/meshes/v4/R_figer5_4.STL +0 -0
  87. package/dist/models/mocap/meshes/v4/R_figer5_5.STL +0 -0
  88. package/dist/models/mocap/meshes/v4/R_handle.STL +0 -0
  89. package/dist/models/mocap/meshes/v4/R_handleseat.STL +0 -0
  90. package/dist/models/mocap/meshes/v4/R_mark.STL +0 -0
  91. package/dist/models/mocap/meshes/v4/R_markseat.STL +0 -0
  92. package/dist/models/mocap/meshes/v4/Rgamepadbase.STL +0 -0
  93. package/dist/models/mocap/meshes/v4/base_link.STL +0 -0
  94. package/dist/models/mocap/meshes/v4/r_figer1_1.STL +0 -0
  95. package/dist/models/mocap/meshes/v5/L_handleseat.STL +0 -0
  96. package/dist/models/mocap/meshes/v5/R_handleseat.STL +0 -0
  97. package/dist/models/mocap/meshes/v5/link_LeftSkeletonBase.STL +0 -0
  98. package/dist/models/mocap/meshes/v5/link_LeftSkeletonIndex1.STL +0 -0
  99. package/dist/models/mocap/meshes/v5/link_LeftSkeletonIndex2.STL +0 -0
  100. package/dist/models/mocap/meshes/v5/link_LeftSkeletonIndex3.STL +0 -0
  101. package/dist/models/mocap/meshes/v5/link_LeftSkeletonIndex4.STL +0 -0
  102. package/dist/models/mocap/meshes/v5/link_LeftSkeletonMiddle1.STL +0 -0
  103. package/dist/models/mocap/meshes/v5/link_LeftSkeletonMiddle2.STL +0 -0
  104. package/dist/models/mocap/meshes/v5/link_LeftSkeletonMiddle3.STL +0 -0
  105. package/dist/models/mocap/meshes/v5/link_LeftSkeletonMiddle4.STL +0 -0
  106. package/dist/models/mocap/meshes/v5/link_LeftSkeletonPinky1.STL +0 -0
  107. package/dist/models/mocap/meshes/v5/link_LeftSkeletonPinky2.STL +0 -0
  108. package/dist/models/mocap/meshes/v5/link_LeftSkeletonPinky3.STL +0 -0
  109. package/dist/models/mocap/meshes/v5/link_LeftSkeletonPinky4.STL +0 -0
  110. package/dist/models/mocap/meshes/v5/link_LeftSkeletonRing1.STL +0 -0
  111. package/dist/models/mocap/meshes/v5/link_LeftSkeletonRing2.STL +0 -0
  112. package/dist/models/mocap/meshes/v5/link_LeftSkeletonRing3.STL +0 -0
  113. package/dist/models/mocap/meshes/v5/link_LeftSkeletonRing4.STL +0 -0
  114. package/dist/models/mocap/meshes/v5/link_LeftSkeletonThumb1.STL +0 -0
  115. package/dist/models/mocap/meshes/v5/link_LeftSkeletonThumb2.STL +0 -0
  116. package/dist/models/mocap/meshes/v5/link_LeftSkeletonThumb3.STL +0 -0
  117. package/dist/models/mocap/meshes/v5/link_LeftSkeletonThumb4.STL +0 -0
  118. package/dist/models/mocap/meshes/v5/link_LeftSkeletonThumbBase.STL +0 -0
  119. package/dist/models/mocap/meshes/v5/link_RightSkeletonBase.STL +0 -0
  120. package/dist/models/mocap/meshes/v5/link_RightSkeletonIndex1.STL +0 -0
  121. package/dist/models/mocap/meshes/v5/link_RightSkeletonIndex2.STL +0 -0
  122. package/dist/models/mocap/meshes/v5/link_RightSkeletonIndex3.STL +0 -0
  123. package/dist/models/mocap/meshes/v5/link_RightSkeletonIndex4.STL +0 -0
  124. package/dist/models/mocap/meshes/v5/link_RightSkeletonMiddle1.STL +0 -0
  125. package/dist/models/mocap/meshes/v5/link_RightSkeletonMiddle2.STL +0 -0
  126. package/dist/models/mocap/meshes/v5/link_RightSkeletonMiddle3.STL +0 -0
  127. package/dist/models/mocap/meshes/v5/link_RightSkeletonMiddle4.STL +0 -0
  128. package/dist/models/mocap/meshes/v5/link_RightSkeletonPinky1.STL +0 -0
  129. package/dist/models/mocap/meshes/v5/link_RightSkeletonPinky2.STL +0 -0
  130. package/dist/models/mocap/meshes/v5/link_RightSkeletonPinky3.STL +0 -0
  131. package/dist/models/mocap/meshes/v5/link_RightSkeletonPinky4.STL +0 -0
  132. package/dist/models/mocap/meshes/v5/link_RightSkeletonRing1.STL +0 -0
  133. package/dist/models/mocap/meshes/v5/link_RightSkeletonRing2.STL +0 -0
  134. package/dist/models/mocap/meshes/v5/link_RightSkeletonRing3.STL +0 -0
  135. package/dist/models/mocap/meshes/v5/link_RightSkeletonRing4.STL +0 -0
  136. package/dist/models/mocap/meshes/v5/link_RightSkeletonThumb1.STL +0 -0
  137. package/dist/models/mocap/meshes/v5/link_RightSkeletonThumb2.STL +0 -0
  138. package/dist/models/mocap/meshes/v5/link_RightSkeletonThumb3.STL +0 -0
  139. package/dist/models/mocap/meshes/v5/link_RightSkeletonThumb4.STL +0 -0
  140. package/dist/models/mocap/meshes/v5/link_RightSkeletonThumbBase.STL +0 -0
  141. package/dist/scripts/start-rosbridge.sh +77 -0
  142. package/dist/usd-iframe.html +41 -0
  143. package/dist/usd-viewer/bindings/.gitattributes +2 -0
  144. package/dist/usd-viewer/bindings/emHdBindings.data +19331 -0
  145. package/dist/usd-viewer/bindings/emHdBindings.js +12281 -0
  146. package/dist/usd-viewer/bindings/emHdBindings.wasm +0 -0
  147. package/dist/usd-viewer/bindings/emHdBindings.worker.js +124 -0
  148. package/dist/usd-viewer/environments/neutral.hdr +0 -0
  149. package/dist/usd-viewer/error-handler.js +57 -0
  150. package/dist/usd-viewer/hydra/HydraPrimitives.js +740 -0
  151. package/dist/usd-viewer/hydra/TextureRegistry.js +130 -0
  152. package/dist/usd-viewer/hydra/ThreeJsRenderDelegate.js +65 -0
  153. package/dist/usd-viewer/index.d.ts +1 -0
  154. package/dist/usd-viewer/main.js +1018 -0
  155. package/dist/usd-viewer/usd.html +36 -0
  156. package/dist/usd-viewer/usdUtils.js +67 -0
  157. package/package.json +70 -4
  158. package/server/package.json +14 -0
  159. package/server/static-server.js +106 -0
@@ -0,0 +1,740 @@
1
+ import {
2
+ BufferGeometry,
3
+ MeshPhysicalMaterial,
4
+ DoubleSide,
5
+ Color,
6
+ Mesh,
7
+ Float32BufferAttribute,
8
+ SRGBColorSpace,
9
+ RGBAFormat,
10
+ RepeatWrapping,
11
+ LinearSRGBColorSpace,
12
+ Vector2,
13
+ } from "three";
14
+
15
+ const DEBUG = {
16
+ textures: false,
17
+ materials: false,
18
+ meshes: false,
19
+ };
20
+ const DISABLE_MATERIALS = false;
21
+
22
+ let warningMessagesToCount = new Map();
23
+
24
+ /** @type {MeshPhysicalMaterial} */
25
+ let defaultMaterial;
26
+
27
+ export class HydraMesh {
28
+ /**
29
+ * @param {string} id
30
+ * @param {import('./ThreeRenderDelegateInterface.js').ThreeRenderDelegateInterface} hydraInterface
31
+ */
32
+ constructor(id, hydraInterface) {
33
+ this._geometry = new BufferGeometry();
34
+ this._id = id;
35
+ this._interface = hydraInterface;
36
+ this._points = undefined;
37
+ this._normals = undefined;
38
+ this._colors = undefined;
39
+ this._uvs = undefined;
40
+ this._indices = undefined;
41
+ this._materials = [];
42
+
43
+ let material = new MeshPhysicalMaterial({
44
+ side: DoubleSide,
45
+ color: new Color(0xb4b4b4),
46
+ // envMap: hydraInterface.config.envMap,
47
+ });
48
+ this._materials.push(material);
49
+ this._mesh = new Mesh(this._geometry, material);
50
+ this._mesh.castShadow = true;
51
+ this._mesh.receiveShadow = true;
52
+
53
+ // ID can contain paths, we strip those here
54
+ let _name = id;
55
+ let lastSlash = _name.lastIndexOf("/");
56
+ if (lastSlash >= 0) {
57
+ _name = _name.substring(lastSlash + 1);
58
+ }
59
+ this._mesh.name = _name;
60
+
61
+ // console.log("Creating HydraMesh: " + id + " -> " + _name);
62
+
63
+ hydraInterface.config.usdRoot.add(this._mesh); // FIXME
64
+
65
+ }
66
+
67
+ updateOrder(attribute, attributeName, dimension = 3) {
68
+ if (DEBUG.meshes) console.log("updateOrder", attributeName, dimension);
69
+ if (attribute && this._indices) {
70
+ const values = [];
71
+ for (let i = 0; i < this._indices.length; i++) {
72
+ const index = this._indices[i];
73
+ for (let j = 0; j < dimension; ++j) {
74
+ values.push(attribute[dimension * index + j]);
75
+ }
76
+ }
77
+ this._geometry.setAttribute(
78
+ attributeName,
79
+ new Float32BufferAttribute(values, dimension)
80
+ );
81
+ }
82
+ }
83
+
84
+ updateIndices(indices) {
85
+ if (DEBUG.meshes) console.log("updateIndices", indices.length);
86
+ this._indices = [...indices];
87
+ this.updateOrder(this._points, "position");
88
+ this.updateOrder(this._normals, "normal");
89
+ if (this._colors) this.updateOrder(this._colors, "color");
90
+ if (this._uvs) {
91
+ this.updateOrder(this._uvs, "uv", 2);
92
+ this._geometry.attributes.uv2 = this._geometry.attributes.uv;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Sets the transform of the mesh.
98
+ * @param {Iterable<number>} matrix - The 4x4 matrix to set on the mesh.
99
+ */
100
+ setTransform(matrix) {
101
+ this._mesh.matrix.set(...matrix);
102
+ this._mesh.matrix.transpose();
103
+ this._mesh.matrixAutoUpdate = false;
104
+ }
105
+
106
+ /**
107
+ * Sets automatically generated normals on the mesh. Should only be used if there are no authored normals.
108
+ * @param {} normals
109
+ */
110
+ updateNormals(normals) {
111
+ // don't apply automatically generated normals if there are already authored normals.
112
+ if (this._geometry.hasAttribute("normal")) return;
113
+
114
+ this._normals = normals.slice(0);
115
+ this.updateOrder(this._normals, "normal");
116
+ }
117
+
118
+ setNormals(data, interpolation) {
119
+ if (interpolation === "facevarying") {
120
+ // The UV buffer has already been prepared on the C++ side, so we just set it
121
+ this._geometry.setAttribute(
122
+ "normal",
123
+ new Float32BufferAttribute(data, 3)
124
+ );
125
+ } else if (interpolation === "vertex") {
126
+ // We have per-vertex UVs, so we need to sort them accordingly
127
+ this._normals = data.slice(0);
128
+ this.updateOrder(this._normals, "normal");
129
+ }
130
+ }
131
+
132
+ setMaterial(materialId) {
133
+ if (DEBUG.materials) console.log("Setting material:", materialId);
134
+ const material = this._interface.materials[materialId];
135
+ if (material) {
136
+ this._mesh.material = material._material;
137
+ } else {
138
+ console.error("Material not found:", materialId);
139
+ }
140
+ }
141
+
142
+ setGeomSubsetMaterial(sections) {
143
+ //console.log("setting subset material: ", this._id, sections)
144
+
145
+ for (let i = 0; i < sections.length; i++) {
146
+ const section = sections[i];
147
+ if (this._interface.materials[section.materialId]) {
148
+ this._materials.push(
149
+ this._interface.materials[section.materialId]._material
150
+ );
151
+ this._geometry.addGroup(section.start, section.length, i + 1);
152
+ }
153
+ }
154
+ this._mesh = new Mesh(this._geometry, this._materials);
155
+ this._interface.config.usdRoot.add(this._mesh);
156
+ }
157
+
158
+ setDisplayColor(data, interpolation) {
159
+ if (DISABLE_MATERIALS) return;
160
+
161
+ const wasDefaultMaterial = this._mesh.material === defaultMaterial;
162
+ if (wasDefaultMaterial) {
163
+ this._mesh.material = this._mesh.material.clone();
164
+ }
165
+
166
+ this._colors = null;
167
+
168
+ if (interpolation === "constant") {
169
+ this._mesh.material.color = new Color().fromArray(data);
170
+ } else if (interpolation === "vertex") {
171
+ this._mesh.material.vertexColors = true;
172
+ if (wasDefaultMaterial) this._mesh.material.color = new Color(0xffffff);
173
+ this._colors = data.slice(0);
174
+ this.updateOrder(this._colors, "color");
175
+ } else {
176
+ const count = warningMessagesToCount.get(interpolation) || 0;
177
+ warningMessagesToCount.set(interpolation, count + 1);
178
+ if (count === 0) {
179
+ console.warn(
180
+ `Unsupported displayColor interpolation type '${interpolation}'.`
181
+ );
182
+ }
183
+ }
184
+ }
185
+
186
+ setUV(data, dimension, interpolation) {
187
+ // TODO: Support multiple UVs. For now, we simply set uv = uv2, which is required when a material has an aoMap.
188
+ this._uvs = null;
189
+
190
+ if (interpolation === "facevarying") {
191
+ // The UV buffer has already been prepared on the C++ side, so we just set it
192
+ this._geometry.setAttribute(
193
+ "uv",
194
+ new Float32BufferAttribute(data, dimension)
195
+ );
196
+ } else if (interpolation === "vertex") {
197
+ // We have per-vertex UVs, so we need to sort them accordingly
198
+ this._uvs = data.slice(0);
199
+ this.updateOrder(this._uvs, "uv", 2);
200
+ }
201
+
202
+ if (this._geometry.hasAttribute("uv"))
203
+ this._geometry.attributes.uv2 = this._geometry.attributes.uv;
204
+ }
205
+
206
+ updatePrimvar(name, data, dimension, interpolation) {
207
+ if (!name) return;
208
+
209
+ if (name === "points") {
210
+ // || name === 'normals') {
211
+ // Points and normals are set separately
212
+ return;
213
+ }
214
+
215
+ // console.log('Setting PrimVar: ' + name + ", interpolation: " + interpolation);
216
+
217
+ // TODO: Support multiple UVs. For now, we simply set uv = uv2, which is required when a material has an aoMap.
218
+ if (name.startsWith("st")) {
219
+ name = "uv";
220
+ }
221
+
222
+ const uvNames = ["uv", "uvmap", "uv0", "uvw", "map1"];
223
+ const normalizedName = name.toLowerCase();
224
+
225
+ if (name === "displayColor") {
226
+ this.setDisplayColor(data, interpolation);
227
+ } else if (uvNames.includes(normalizedName)) {
228
+ this.setUV(data, dimension, interpolation);
229
+ } else if (name === "normals") {
230
+ this.setNormals(data, interpolation);
231
+ } else {
232
+ const count = warningMessagesToCount.get(name) || 0;
233
+ warningMessagesToCount.set(name, count + 1);
234
+ if (count === 0) console.warn("Unsupported primvar:", name);
235
+ }
236
+ }
237
+
238
+ updatePoints(points) {
239
+ this._points = points.slice(0);
240
+ this.updateOrder(this._points, "position")
241
+ }
242
+
243
+ commit() {
244
+ // All Three.js resources are already updated during the sync phase.
245
+ }
246
+ }
247
+
248
+ // Stub classes for camera and light prims to prevent Hydra warnings
249
+ export class HydraCamera {
250
+ constructor(id, hydraInterface) {
251
+ this._id = id;
252
+ this._interface = hydraInterface;
253
+ }
254
+ }
255
+
256
+ export class HydraLight {
257
+ constructor(id, hydraInterface) {
258
+ this._id = id;
259
+ this._interface = hydraInterface;
260
+ }
261
+ }
262
+
263
+ export class HydraMaterial {
264
+ // Maps USD preview material texture names to Three.js MeshPhysicalMaterial names
265
+ static usdPreviewToMeshPhysicalTextureMap = {
266
+ diffuseColor: "map",
267
+ clearcoat: "clearcoatMap",
268
+ clearcoatRoughness: "clearcoatRoughnessMap",
269
+ emissiveColor: "emissiveMap",
270
+ occlusion: "aoMap",
271
+ roughness: "roughnessMap",
272
+ metallic: "metalnessMap",
273
+ normal: "normalMap",
274
+ opacity: "alphaMap",
275
+ };
276
+
277
+ static usdPreviewToColorSpaceMap = {
278
+ diffuseColor: SRGBColorSpace,
279
+ emissiveColor: SRGBColorSpace,
280
+ opacity: SRGBColorSpace,
281
+ };
282
+
283
+ static channelMap = {
284
+ // Three.js expects many 8bit values such as roughness or metallness in a specific RGB texture channel.
285
+ // We could write code to combine multiple 8bit texture files into different channels of one RGB texture where it
286
+ // makes sense, but that would complicate this loader a lot. Most Three.js loaders don't seem to do it either.
287
+ // Instead, we simply provide the 8bit image as an RGBA texture, even though this might be less efficient.
288
+ r: RGBAFormat,
289
+ g: RGBAFormat,
290
+ b: RGBAFormat,
291
+ rgb: RGBAFormat,
292
+ rgba: RGBAFormat,
293
+ };
294
+
295
+ // Maps USD preview material property names to Three.js MeshPhysicalMaterial names
296
+ static usdPreviewToMeshPhysicalMap = {
297
+ clearcoat: "clearcoat",
298
+ clearcoatRoughness: "clearcoatRoughness",
299
+ diffuseColor: "color",
300
+ emissiveColor: "emissive",
301
+ ior: "ior",
302
+ metallic: "metalness",
303
+ opacity: "opacity",
304
+ roughness: "roughness",
305
+ opacityThreshold: "alphaTest",
306
+ };
307
+
308
+ constructor(id, hydraInterface) {
309
+ this._id = id;
310
+ this._nodes = {};
311
+ this._interface = hydraInterface;
312
+
313
+ if (!defaultMaterial) {
314
+ defaultMaterial = new MeshPhysicalMaterial({
315
+ side: DoubleSide,
316
+ color: new Color(DISABLE_MATERIALS ? 0x999999 : 0xff2997),
317
+ name: "DefaultMaterial",
318
+ });
319
+ }
320
+
321
+ this._material = defaultMaterial;
322
+ if (DEBUG.materials) console.log("Hydra Material", this);
323
+ }
324
+
325
+ updateNode(networkId, path, parameters) {
326
+ if (DEBUG.textures) console.log("Updating Material Node:", networkId, path);
327
+ this._nodes[path] = parameters;
328
+ }
329
+
330
+ convertWrap(usdWrapMode) {
331
+ if (usdWrapMode === undefined) return RepeatWrapping;
332
+
333
+ const WRAPPINGS = {
334
+ repeat: 1000, // RepeatWrapping
335
+ clamp: 1001, // ClampToEdgeWrapping
336
+ mirror: 1002, // MirroredRepeatWrapping
337
+ };
338
+
339
+ if (WRAPPINGS[usdWrapMode]) return WRAPPINGS[usdWrapMode];
340
+
341
+ return RepeatWrapping;
342
+ }
343
+
344
+ /**
345
+ * @return {Promise<void>}
346
+ */
347
+ assignTexture(mainMaterial, parameterName) {
348
+ return new Promise((resolve) => {
349
+ const materialParameterMapName =
350
+ HydraMaterial.usdPreviewToMeshPhysicalTextureMap[parameterName];
351
+ if (materialParameterMapName === undefined) {
352
+ console.warn(
353
+ `Unsupported material texture parameter '${parameterName}'.`
354
+ );
355
+ resolve();
356
+ return;
357
+ }
358
+ if (mainMaterial[parameterName] && mainMaterial[parameterName].nodeIn) {
359
+ const nodeIn = mainMaterial[parameterName].nodeIn;
360
+ if (!nodeIn.resolvedPath) {
361
+ console.warn("Texture node has no file!", nodeIn);
362
+ }
363
+ if (DEBUG.textures)
364
+ console.log(
365
+ "Assigning texture with resolved path",
366
+ parameterName,
367
+ nodeIn.resolvedPath
368
+ );
369
+ const textureFileName = nodeIn.resolvedPath?.replace("./", "");
370
+ const channel = mainMaterial[parameterName].inputName;
371
+
372
+ if (DEBUG.textures) {
373
+ const matName = Object.keys(this._nodes).find(
374
+ (key) => this._nodes[key] === mainMaterial
375
+ );
376
+ console.log(
377
+ `Setting texture '${materialParameterMapName}' (${textureFileName}) of material '${matName}' with channel '${channel}'`
378
+ );
379
+ }
380
+
381
+ this._interface.registry
382
+ .getTexture(textureFileName)
383
+ .then((texture) => {
384
+ if (!this._material) {
385
+ console.error(
386
+ "Material not set when trying to assign texture, this is likely a bug"
387
+ );
388
+ resolve();
389
+ }
390
+ // console.log("getTexture", texture, nodeIn);
391
+ if (materialParameterMapName === "alphaMap") {
392
+ // If this is an opacity map, check if it's using the alpha channel of the diffuse map.
393
+ // If so, simply change the format of that diffuse map to RGBA and make the material transparent.
394
+ // If not, we need to copy the alpha channel into a new texture's green channel, because that's what Three.js
395
+ // expects for alpha maps (not supported at the moment).
396
+ // NOTE that this only works if diffuse maps are always set before opacity maps, so the order of
397
+ // 'assingTexture' calls for a material matters.
398
+ if (
399
+ nodeIn.file === mainMaterial.diffuseColor?.nodeIn?.file &&
400
+ channel === "a"
401
+ ) {
402
+ this._material.map.format = RGBAFormat;
403
+ } else {
404
+ // TODO: Extract the alpha channel into a new RGB texture.
405
+ console.warn(
406
+ "Separate alpha channel is currently not supported.",
407
+ nodeIn.file,
408
+ mainMaterial.diffuseColor?.nodeIn?.file,
409
+ channel
410
+ );
411
+ }
412
+ if (!this._material.alphaClip) this._material.transparent = true;
413
+
414
+ this._material.needsUpdate = true;
415
+ resolve();
416
+ return;
417
+ } else if (materialParameterMapName === "metalnessMap") {
418
+ this._material.metalness = 1.0;
419
+ } else if (materialParameterMapName === "roughnessMap") {
420
+ this._material.roughness = 1.0;
421
+ } else if (materialParameterMapName === "emissiveMap") {
422
+ this._material.emissive = new Color(0xffffff);
423
+ } else if (!HydraMaterial.channelMap[channel]) {
424
+ console.warn(`Unsupported texture channel '${channel}'!`);
425
+ resolve();
426
+ return;
427
+ }
428
+ // TODO need to apply bias/scale to the texture in some cases.
429
+ // May be able to extract that for metalness/roughness/opacity/normalScale
430
+
431
+ // Clone texture and set the correct format.
432
+ const clonedTexture = texture.clone();
433
+ let targetSwizzle = "rgba";
434
+
435
+ if (materialParameterMapName == "roughnessMap" && channel != "g") {
436
+ targetSwizzle = "0" + channel + "11";
437
+ }
438
+ if (materialParameterMapName == "metalnessMap" && channel != "b") {
439
+ targetSwizzle = "01" + channel + "1";
440
+ }
441
+ if (materialParameterMapName == "occlusionMap" && channel != "r") {
442
+ targetSwizzle = channel + "111";
443
+ }
444
+ if (materialParameterMapName == "opacityMap" && channel != "a") {
445
+ targetSwizzle = channel + channel + channel + channel;
446
+ }
447
+
448
+ clonedTexture.colorSpace =
449
+ HydraMaterial.usdPreviewToColorSpaceMap[parameterName] ||
450
+ LinearSRGBColorSpace;
451
+
452
+ if (targetSwizzle != "rgba") {
453
+ clonedTexture.image = HydraMaterial._swizzleImageChannels(
454
+ clonedTexture.image,
455
+ targetSwizzle
456
+ );
457
+ }
458
+
459
+ clonedTexture.format = HydraMaterial.channelMap[channel];
460
+ clonedTexture.needsUpdate = true;
461
+ if (nodeIn.st && nodeIn.st.nodeIn) {
462
+ const uvData = nodeIn.st.nodeIn;
463
+
464
+ // TODO this is messed up but works for scale and translation, not really for rotation.
465
+ // Refer to https://github.com/mrdoob/three.js/blob/e5426b0514a1347d7aafca69aa34117503c1be88/examples/jsm/exporters/USDZExporter.js#L461
466
+ // (which is also not perfect but close)
467
+
468
+ const rotation = uvData.rotation
469
+ ? (uvData.rotation / 180) * Math.PI
470
+ : 0;
471
+ const offset = uvData.translation
472
+ ? new Vector2(uvData.translation[0], uvData.translation[1])
473
+ : new Vector2(0, 0);
474
+ const repeat = uvData.scale
475
+ ? new Vector2(uvData.scale[0], uvData.scale[1])
476
+ : new Vector2(1, 1);
477
+
478
+ const xRotationOffset = Math.sin(rotation);
479
+ const yRotationOffset = Math.cos(rotation);
480
+ offset.y = offset.y - (1 - yRotationOffset) * repeat.y;
481
+ offset.x = offset.x - xRotationOffset * repeat.x;
482
+
483
+ clonedTexture.repeat.set(repeat.x, repeat.y);
484
+ clonedTexture.offset.set(offset.x, offset.y);
485
+ clonedTexture.rotation = rotation;
486
+ }
487
+
488
+ // TODO use nodeIn.wrapS and wrapT and map to THREE
489
+ clonedTexture.wrapS = this.convertWrap(nodeIn.wrapS);
490
+ clonedTexture.wrapT = this.convertWrap(nodeIn.wrapT);
491
+ if (DEBUG.textures)
492
+ console.log(
493
+ "Setting texture " + materialParameterMapName + " to",
494
+ clonedTexture
495
+ );
496
+ this._material[materialParameterMapName] = clonedTexture;
497
+ this._material.needsUpdate = true;
498
+
499
+ if (DEBUG.textures) {
500
+ const matName = Object.keys(this._nodes).find(
501
+ (key) => this._nodes[key] === mainMaterial
502
+ );
503
+ console.log(
504
+ "RESOLVED TEXTURE",
505
+ clonedTexture.name,
506
+ matName,
507
+ parameterName
508
+ );
509
+ }
510
+ resolve();
511
+ return;
512
+ })
513
+ .catch((err) => {
514
+ console.warn("Error when loading texture", err);
515
+ resolve();
516
+ return;
517
+ });
518
+ } else {
519
+ this._material[materialParameterMapName] = undefined;
520
+ resolve();
521
+ return;
522
+ }
523
+ });
524
+ }
525
+
526
+ // from https://github.com/mrdoob/three.js/blob/dev/src/math/ColorManagement.js
527
+ static SRGBToLinear(c) {
528
+ return c < 0.04045
529
+ ? c * 0.0773993808
530
+ : Math.pow(c * 0.9478672986 + 0.0521327014, 2.4);
531
+ }
532
+
533
+ static LinearToSRGB(c) {
534
+ return c < 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 0.41666) - 0.055;
535
+ }
536
+
537
+ /**
538
+ * Swizzle image channels (e.g. move red channel to green channel)
539
+ * @param {*} image three.js image
540
+ * @param {string} swizzle For example, "rgga". Must have max. 4 components. Can contain 0 and 1, e.g. "rgba1" is valid.
541
+ * @returns three.js image
542
+ */
543
+ static _swizzleImageChannels(image, swizzle) {
544
+ if (
545
+ (typeof HTMLImageElement !== "undefined" &&
546
+ image instanceof HTMLImageElement) ||
547
+ (typeof HTMLCanvasElement !== "undefined" &&
548
+ image instanceof HTMLCanvasElement) ||
549
+ (typeof ImageBitmap !== "undefined" && image instanceof ImageBitmap)
550
+ ) {
551
+ const canvas = document.createElementNS(
552
+ "http://www.w3.org/1999/xhtml",
553
+ "canvas"
554
+ );
555
+
556
+ canvas.width = image.width;
557
+ canvas.height = image.height;
558
+
559
+ const context = canvas.getContext("2d");
560
+ context.drawImage(image, 0, 0, image.width, image.height);
561
+
562
+ const imageData = context.getImageData(0, 0, image.width, image.height);
563
+ const data = imageData.data;
564
+
565
+ // console.log(data);
566
+
567
+ const swizzleToIndex = {
568
+ r: 0,
569
+ g: 1,
570
+ b: 2,
571
+ a: 3,
572
+ x: 0,
573
+ y: 1,
574
+ z: 2,
575
+ w: 3,
576
+ 0: 4, // set to 0
577
+ 1: 5, // set to 1
578
+ "-": -1, // passthrough
579
+ };
580
+ const arrayAccessBySwizzle = [4, 4, 4, 4]; // empty value if nothing defined in the swizzle pattern
581
+ for (let i = 0; i < swizzle.length; i++) {
582
+ arrayAccessBySwizzle[i] = swizzleToIndex[swizzle[i]];
583
+ }
584
+
585
+ const dataEntry = data.slice(0);
586
+ for (let i = 0; i < data.length; i += 4) {
587
+ dataEntry[0] = data[i];
588
+ dataEntry[1] = data[i + 1];
589
+ dataEntry[2] = data[i + 2];
590
+ dataEntry[3] = data[i + 3];
591
+ dataEntry[4] = 0; // empty value
592
+ dataEntry[5] = 1;
593
+
594
+ const rAccess = arrayAccessBySwizzle[0];
595
+ const gAccess = arrayAccessBySwizzle[1];
596
+ const bAccess = arrayAccessBySwizzle[2];
597
+ const aAccess = arrayAccessBySwizzle[3];
598
+
599
+ if (rAccess !== -1) data[i] = dataEntry[rAccess];
600
+ if (gAccess !== -1) data[i + 1] = dataEntry[gAccess];
601
+ if (bAccess !== -1) data[i + 2] = dataEntry[bAccess];
602
+ if (aAccess !== -1) data[i + 3] = dataEntry[aAccess];
603
+ }
604
+
605
+ context.putImageData(imageData, 0, 0);
606
+ return canvas;
607
+ } else if (image.data) {
608
+ const data = image.data.slice(0);
609
+
610
+ for (let i = 0; i < data.length; i++) {
611
+ if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
612
+ data[i] = Math.floor(this.SRGBToLinear(data[i] / 255) * 255);
613
+ } else {
614
+ // assuming float
615
+ data[i] = this.SRGBToLinear(data[i]);
616
+ }
617
+ }
618
+
619
+ return {
620
+ data: data,
621
+ width: image.width,
622
+ height: image.height,
623
+ };
624
+ } else {
625
+ console.warn(
626
+ "ImageUtils.sRGBToLinear(): Unsupported image type. No color space conversion applied."
627
+ );
628
+ return image;
629
+ }
630
+ }
631
+
632
+ assignProperty(mainMaterial, parameterName) {
633
+ const materialParameterName =
634
+ HydraMaterial.usdPreviewToMeshPhysicalMap[parameterName];
635
+ if (materialParameterName === undefined) {
636
+ console.warn(`Unsupported material parameter '${parameterName}'.`);
637
+ return;
638
+ }
639
+ if (
640
+ mainMaterial[parameterName] !== undefined &&
641
+ !mainMaterial[parameterName].nodeIn
642
+ ) {
643
+ // console.log(`Assigning property ${parameterName}: ${mainMaterial[parameterName]}`);
644
+ if (Array.isArray(mainMaterial[parameterName])) {
645
+ this._material[materialParameterName] = new Color().fromArray(
646
+ mainMaterial[parameterName]
647
+ );
648
+ } else {
649
+ this._material[materialParameterName] = mainMaterial[parameterName];
650
+ if (
651
+ materialParameterName === "opacity" &&
652
+ mainMaterial[parameterName] < 1.0
653
+ ) {
654
+ this._material.transparent = true;
655
+ }
656
+ if (
657
+ parameterName == "opacityThreshold" &&
658
+ mainMaterial[parameterName] > 0.0
659
+ ) {
660
+ this._material.transparent = false;
661
+ this._material.alphaClip = true;
662
+ }
663
+ }
664
+ }
665
+ }
666
+
667
+ async updateFinished(type, relationships) {
668
+ // Wire up node relationships
669
+ for (const relationship of relationships) {
670
+ relationship.nodeIn = this._nodes[relationship.inputId];
671
+ relationship.nodeOut = this._nodes[relationship.outputId];
672
+ relationship.nodeIn[relationship.inputName] = relationship;
673
+ relationship.nodeOut[relationship.outputName] = relationship;
674
+ }
675
+
676
+ if (DEBUG.materials) console.log("Finalizing Material:", this._id);
677
+
678
+ // Find the main material node
679
+ const mainMaterialNode = Object.values(this._nodes).find(
680
+ (node) => node.diffuseColor
681
+ );
682
+
683
+ if (!mainMaterialNode || DISABLE_MATERIALS) {
684
+ this._material = defaultMaterial;
685
+ return;
686
+ }
687
+
688
+ // Create new material (TODO: avoid recreating on every update)
689
+ this._material = new MeshPhysicalMaterial({ side: DoubleSide });
690
+ this._material.name = this._id.split("/").pop() || this._id;
691
+
692
+ // Check for texture maps
693
+ const haveRoughnessMap = !!mainMaterialNode.roughness?.nodeIn;
694
+ const haveMetalnessMap = !!mainMaterialNode.metallic?.nodeIn;
695
+ const haveOcclusionMap = !!mainMaterialNode.occlusion?.nodeIn;
696
+
697
+ if (DEBUG.materials) {
698
+ console.log("Creating Material:", this._id, {
699
+ haveRoughnessMap,
700
+ haveMetalnessMap,
701
+ haveOcclusionMap,
702
+ });
703
+ }
704
+
705
+ const texturePromises = Object.keys(
706
+ HydraMaterial.usdPreviewToMeshPhysicalTextureMap
707
+ ).map((key) => this.assignTexture(mainMaterialNode, key));
708
+ await Promise.all(texturePromises);
709
+
710
+ // Sanitize metallic/roughness maps for glTF compatibility
711
+ if (haveRoughnessMap && !haveMetalnessMap) {
712
+ this._material.metalnessMap = this._material.roughnessMap;
713
+ if (this._material.metalnessMap) {
714
+ this._material.metalnessMap.needsUpdate = true;
715
+ } else {
716
+ console.error("Roughness map loading failed");
717
+ }
718
+ } else if (haveMetalnessMap && !haveRoughnessMap) {
719
+ this._material.roughnessMap = this._material.metalnessMap;
720
+ if (this._material.roughnessMap) {
721
+ this._material.roughnessMap.needsUpdate = true;
722
+ } else {
723
+ console.error("Metalness map loading failed");
724
+ }
725
+ }
726
+
727
+ // Assign material properties
728
+ Object.keys(HydraMaterial.usdPreviewToMeshPhysicalMap).forEach((key) =>
729
+ this.assignProperty(mainMaterialNode, key)
730
+ );
731
+
732
+ if (DEBUG.materials) {
733
+ console.log(
734
+ `Material "${this._material.name}"`,
735
+ mainMaterialNode,
736
+ this._material
737
+ );
738
+ }
739
+ }
740
+ }