@jfk-solutions/demo3d-file-format 1.8.0 → 1.10.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 (75) hide show
  1. package/README.md +225 -0
  2. package/dist/examples/export-embedded-viewer-standalone.html +1 -1
  3. package/dist/examples/three-render-smoke-standalone.html +295 -231
  4. package/dist/index.d.ts +8 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +7 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/model.d.ts +1 -0
  9. package/dist/model.d.ts.map +1 -1
  10. package/dist/model.js +6 -0
  11. package/dist/model.js.map +1 -1
  12. package/dist/raw3d-animation.d.ts +63 -0
  13. package/dist/raw3d-animation.d.ts.map +1 -0
  14. package/dist/raw3d-animation.js +200 -0
  15. package/dist/raw3d-animation.js.map +1 -0
  16. package/dist/raw3d-effects.d.ts +66 -0
  17. package/dist/raw3d-effects.d.ts.map +1 -0
  18. package/dist/raw3d-effects.js +172 -0
  19. package/dist/raw3d-effects.js.map +1 -0
  20. package/dist/raw3d-skinning.d.ts +8 -0
  21. package/dist/raw3d-skinning.d.ts.map +1 -0
  22. package/dist/raw3d-skinning.js +37 -0
  23. package/dist/raw3d-skinning.js.map +1 -0
  24. package/dist/raw3d.d.ts +77 -2
  25. package/dist/raw3d.d.ts.map +1 -1
  26. package/dist/raw3d.js +103 -7
  27. package/dist/raw3d.js.map +1 -1
  28. package/dist/rigid-interpolation.d.ts +16 -0
  29. package/dist/rigid-interpolation.d.ts.map +1 -0
  30. package/dist/rigid-interpolation.js +52 -0
  31. package/dist/rigid-interpolation.js.map +1 -0
  32. package/dist/three/backend.d.ts +13 -0
  33. package/dist/three/backend.d.ts.map +1 -0
  34. package/dist/three/backend.js +9 -0
  35. package/dist/three/backend.js.map +1 -0
  36. package/dist/three/effects.d.ts +7 -0
  37. package/dist/three/effects.d.ts.map +1 -0
  38. package/dist/three/effects.js +127 -0
  39. package/dist/three/effects.js.map +1 -0
  40. package/dist/three/index.d.ts +2 -1
  41. package/dist/three/index.d.ts.map +1 -1
  42. package/dist/three/index.js +72 -14
  43. package/dist/three/index.js.map +1 -1
  44. package/dist/three/native-lighting.d.ts +5 -0
  45. package/dist/three/native-lighting.d.ts.map +1 -0
  46. package/dist/three/native-lighting.js +86 -0
  47. package/dist/three/native-lighting.js.map +1 -0
  48. package/dist/three/parallax.d.ts +5 -0
  49. package/dist/three/parallax.d.ts.map +1 -0
  50. package/dist/three/parallax.js +63 -0
  51. package/dist/three/parallax.js.map +1 -0
  52. package/dist/three/raw3d.d.ts +25 -0
  53. package/dist/three/raw3d.d.ts.map +1 -1
  54. package/dist/three/raw3d.js +404 -16
  55. package/dist/three/raw3d.js.map +1 -1
  56. package/dist/three/renderer.d.ts.map +1 -1
  57. package/dist/three/renderer.js +3 -0
  58. package/dist/three/renderer.js.map +1 -1
  59. package/dist/three/skinning.d.ts +11 -0
  60. package/dist/three/skinning.d.ts.map +1 -0
  61. package/dist/three/skinning.js +92 -0
  62. package/dist/three/skinning.js.map +1 -0
  63. package/dist/three/tsl.d.ts +60 -0
  64. package/dist/three/tsl.d.ts.map +1 -0
  65. package/dist/three/tsl.js +3 -0
  66. package/dist/three/tsl.js.map +1 -0
  67. package/dist/three/webgpu-native.d.ts +3 -0
  68. package/dist/three/webgpu-native.d.ts.map +1 -0
  69. package/dist/three/webgpu-native.js +58 -0
  70. package/dist/three/webgpu-native.js.map +1 -0
  71. package/dist/three/webgpu-relief.d.ts +3 -0
  72. package/dist/three/webgpu-relief.d.ts.map +1 -0
  73. package/dist/three/webgpu-relief.js +61 -0
  74. package/dist/three/webgpu-relief.js.map +1 -0
  75. package/package.json +1 -1
@@ -1,3 +1,6 @@
1
+ import { bindRaw3DThreeSkin } from "./skinning.js";
2
+ import { applyRaw3DThreeEffects, configureRaw3DDrawable } from "./effects.js";
3
+ import { raw3DNodeBackend } from "./backend.js";
1
4
  import { Raw3DMaterial, Raw3DMesh, Raw3DPackage, Raw3DProject } from "../raw3d.js";
2
5
  export async function createRaw3DThreeScene(parsed, options = {}) {
3
6
  const three = await resolveThree(options);
@@ -14,6 +17,9 @@ export async function createRaw3DThreeScene(parsed, options = {}) {
14
17
  };
15
18
  }
16
19
  export async function createRaw3DThreeGroup(parsed, options = {}) {
20
+ return (await buildRaw3DThreeGroup(parsed, options)).group;
21
+ }
22
+ async function buildRaw3DThreeGroup(parsed, options) {
17
23
  const three = await resolveThree(options);
18
24
  const project = parsed instanceof Raw3DPackage ? parsed.model : parsed;
19
25
  const warnings = [];
@@ -36,6 +42,8 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
36
42
  };
37
43
  const textures = await loadTextures(project, three, stats, warn);
38
44
  const materials = project.materials.map((material) => createMaterial(material, textures, three));
45
+ for (const material of materials)
46
+ material.userData.raw3DHeightChannel = options.parallaxHeightChannel ?? "a";
39
47
  const defaultMaterial = new three.MeshStandardMaterial({ color: 0x9aa0a6, roughness: 0.8 });
40
48
  const geometryCache = new Map();
41
49
  const objects = new Map();
@@ -99,12 +107,14 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
99
107
  const drawable = createDrawable(sourceMesh, geometry, nodeMaterials, three);
100
108
  drawable.name = `${node.name} ${sourceMesh.meshType}`;
101
109
  drawable.castShadow = node.castsShadow && drawable instanceof three.Mesh;
110
+ drawable.visible = node.visible;
102
111
  drawable.receiveShadow = drawable instanceof three.Mesh;
103
112
  drawable.userData.raw3d = {
104
113
  kind: primitiveKind(sourceMesh.meshType),
105
114
  nodeIndex: node.index,
106
115
  meshIndex: node.meshIndex,
107
- meshType: sourceMesh.meshType
116
+ meshType: sourceMesh.meshType,
117
+ sourceVisible: node.visible
108
118
  };
109
119
  object.add(drawable);
110
120
  stats.meshes += 1;
@@ -117,15 +127,13 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
117
127
  const sprite = createTextSprite(text, sourceMaterial, three);
118
128
  if (sprite) {
119
129
  sprite.name = `${node.name} Text`;
120
- sprite.userData.raw3d = { kind: "text", nodeIndex: node.index, textIndex: node.textIndex };
130
+ sprite.visible = node.visible;
131
+ sprite.userData.raw3d = { kind: "text", nodeIndex: node.index, textIndex: node.textIndex, sourceVisible: node.visible };
121
132
  object.add(sprite);
122
133
  }
123
134
  }
124
135
  }
125
- for (const source of project.lights) {
126
- if (!source.enabled) {
127
- continue;
128
- }
136
+ for (const [sourceIndex, source] of project.lights.entries()) {
129
137
  const parent = source.nodeIndex === undefined ? undefined : objects.get(source.nodeIndex);
130
138
  if (!parent) {
131
139
  warn({
@@ -145,10 +153,12 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
145
153
  light = new three.AmbientLight(color, 1);
146
154
  break;
147
155
  case "point":
148
- light = new three.PointLight(color, 1, 0, 2);
156
+ light = raw3DNodeBackend(three)?.point() ?? new three.PointLight(color, 1, 0, 2);
157
+ light.color.copy(color);
149
158
  break;
150
159
  case "spot":
151
- light = new three.SpotLight(color, 1);
160
+ light = raw3DNodeBackend(three)?.spot() ?? new three.SpotLight(color, 1);
161
+ light.color.copy(color);
152
162
  break;
153
163
  default: {
154
164
  const directional = new three.DirectionalLight(color, 1);
@@ -159,8 +169,10 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
159
169
  }
160
170
  }
161
171
  light.name = `${parent.name} ${source.type} light`;
172
+ light.visible = source.enabled;
162
173
  light.userData.raw3d = {
163
174
  kind: "light",
175
+ index: source.index ?? sourceIndex,
164
176
  nodeIndex: source.nodeIndex,
165
177
  materialIndex: source.materialIndex,
166
178
  type: source.type,
@@ -195,7 +207,327 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
195
207
  // Match the metadata shape used by the Demo3D adapter so generic viewers can
196
208
  // display stats without special-casing the property name.
197
209
  root.userData.demo3d = { kind: "raw3d-package", stats, warnings };
198
- return root;
210
+ const skinBindings = new Map();
211
+ for (const node of project.nodes) {
212
+ const owner = objects.get(node.index);
213
+ const drawable = owner.children.find(child => child instanceof three.Mesh);
214
+ if (drawable && node.meshIndex !== undefined && project.meshes[node.meshIndex]?.bones.length) {
215
+ const binding = bindRaw3DThreeSkin(drawable, node.meshIndex, project, objects, three);
216
+ skinBindings.set(binding.drawable, binding);
217
+ }
218
+ for (const child of owner.children)
219
+ if (["mesh", "lines", "points"].includes(child.userData.raw3d?.kind))
220
+ configureRaw3DDrawable(child, node.isBillboard, node.isDecal, three);
221
+ }
222
+ root.updateMatrixWorld(true);
223
+ for (const binding of skinBindings.values())
224
+ binding.update();
225
+ applyRaw3DThreeEffects(root, project.animation.effects.sample(0), project, three);
226
+ return { group: root, three, project, objects, materials, textures, geometryCache, defaultMaterial, skinBindings };
227
+ }
228
+ /** Optional Three.js binding. Animation data and interpolation live in the core. */
229
+ export async function createRaw3DThreePlayback(parsed, options = {}) {
230
+ const project = parsed instanceof Raw3DPackage ? parsed.model : parsed;
231
+ const animation = project.animation;
232
+ const resources = await buildRaw3DThreeGroup(parsed, options);
233
+ const { group, three, objects, materials, geometryCache, defaultMaterial, skinBindings } = resources;
234
+ let frame = animation.sampleFrame(0);
235
+ for (const warning of animation.warnings) {
236
+ const item = { code: warning.code, message: warning.message };
237
+ group.userData.raw3d.warnings.push(item);
238
+ options.onWarning?.(item);
239
+ }
240
+ const variants = new Map();
241
+ const archivedStreams = new Map([...resources.textures].filter(([key]) => key.startsWith("stream:")));
242
+ const streamRevisions = new Map();
243
+ const streamWarnings = new Set();
244
+ const ownedMaterials = new Set([...materials, defaultMaterial]);
245
+ const ownedTextures = new Set(resources.textures.values());
246
+ const capture = () => group.traverse(object => {
247
+ const drawable = object;
248
+ if (drawable.material)
249
+ for (const material of Array.isArray(drawable.material) ? drawable.material : [drawable.material]) {
250
+ ownedMaterials.add(material);
251
+ const map = material.map;
252
+ if (map)
253
+ ownedTextures.add(map);
254
+ }
255
+ });
256
+ capture();
257
+ let disposed = false;
258
+ const palette = (node, state, mesh) => {
259
+ const layerMaterial = state.layerIndex === undefined ? undefined : project.layers[state.layerIndex]?.materialIndex;
260
+ return Array.from({ length: Math.max(1, mesh.indexBufferIndices.length) }, (_, slot) => {
261
+ const index = node.materialIndices[slot] ?? node.materialIndices[0] ?? layerMaterial;
262
+ const base = (index === undefined ? defaultMaterial : materials[index] ?? defaultMaterial);
263
+ const values = state.materialProperties[slot] ?? {};
264
+ const streamIndex = values.StreamableTexture ?? (index === undefined ? -1 : project.materials[index]?.streamableTextureIndex ?? -1);
265
+ const ordinaryMap = values.Texture === undefined ? base.map : values.Texture < 0 ? null : resources.textures.get(`${values.Texture}:color`);
266
+ const streamMap = streamIndex < 0 ? undefined : resources.textures.get(`stream:${streamIndex}:color`);
267
+ const mapSource = streamMap ?? ordinaryMap;
268
+ if (streamIndex >= 0 && !streamMap && !streamWarnings.has(streamIndex)) {
269
+ streamWarnings.add(streamIndex);
270
+ const warning = { code: "RAW3D_STREAM_TEXTURE_UNAVAILABLE", message: `Streamable texture ${streamIndex} has no archived image or resolved external frame.` };
271
+ group.userData.raw3d.warnings.push(warning);
272
+ options.onWarning?.(warning);
273
+ }
274
+ const key = `${node.index}:${slot}:${index ?? -1}:${values.Texture ?? "base"}:${values.NormalMap ?? values.NormalTexture ?? "base"}:stream:${streamIndex}`;
275
+ let entry = variants.get(key);
276
+ if (!entry) {
277
+ const material = base.clone();
278
+ const normalIndex = values.NormalMap ?? values.NormalTexture;
279
+ const normalSource = normalIndex === undefined ? base.normalMap : normalIndex < 0 ? null : resources.textures.get(`${normalIndex}:normal`);
280
+ if ((values.Texture !== undefined && values.Texture >= 0 && !mapSource) || (normalIndex !== undefined && normalIndex >= 0 && !normalSource))
281
+ throw new Error("An animated RAW3D texture resource could not be loaded.");
282
+ entry = { base, material, map: mapSource?.clone(), normalMap: normalSource?.clone(), mapSource: mapSource ?? undefined };
283
+ variants.set(key, entry);
284
+ ownedMaterials.add(material);
285
+ if (entry.map)
286
+ ownedTextures.add(entry.map);
287
+ if (entry.normalMap)
288
+ ownedTextures.add(entry.normalMap);
289
+ }
290
+ if (entry.mapSource !== (mapSource ?? undefined)) {
291
+ if (entry.map) {
292
+ entry.map.dispose();
293
+ ownedTextures.delete(entry.map);
294
+ }
295
+ entry.map = mapSource?.clone();
296
+ entry.mapSource = mapSource ?? undefined;
297
+ if (entry.map)
298
+ ownedTextures.add(entry.map);
299
+ entry.material.needsUpdate = true;
300
+ }
301
+ if (streamIndex >= 0 && entry.streamRevision !== streamRevisions.get(streamIndex)) {
302
+ if (entry.map)
303
+ entry.map.needsUpdate = true;
304
+ entry.streamRevision = streamRevisions.get(streamIndex);
305
+ }
306
+ const material = entry.material;
307
+ const wasTransparent = material.transparent;
308
+ material.copy(base);
309
+ material.map = entry.map ?? null;
310
+ material.normalMap = entry.normalMap ?? null;
311
+ if (values.Diffuse !== undefined) {
312
+ material.color.setHex(values.Diffuse & 0xffffff, three.SRGBColorSpace);
313
+ material.opacity = (values.Diffuse >>> 24) / 255;
314
+ }
315
+ if (values.Transparency !== undefined)
316
+ material.opacity = 1 - values.Transparency;
317
+ if (values.Reflectivity !== undefined)
318
+ material.roughness = .9 - clamp01(values.Reflectivity) * .75;
319
+ if (values.Luminosity !== undefined && material.emissive)
320
+ material.emissive.copy(material.color).multiplyScalar(values.Luminosity);
321
+ if (values.NormalScale !== undefined && material.normalScale)
322
+ material.normalScale.setScalar(values.NormalScale);
323
+ if (values.NormalDepth !== undefined)
324
+ material.userData.raw3dNormalDepth = values.NormalDepth;
325
+ for (const texture of [material.map, material.normalMap]) {
326
+ if (!texture)
327
+ continue;
328
+ texture.offset.set(values.U ?? base.map?.offset.x ?? 0, values.V ?? base.map?.offset.y ?? 0);
329
+ texture.repeat.set(values.ScaleU ?? base.map?.repeat.x ?? 1, values.ScaleV ?? base.map?.repeat.y ?? 1);
330
+ if (values.U !== undefined)
331
+ texture.offset.x = values.U;
332
+ if (values.V !== undefined)
333
+ texture.offset.y = values.V;
334
+ if (values.ScaleU !== undefined)
335
+ texture.repeat.x = values.ScaleU;
336
+ if (values.ScaleV !== undefined)
337
+ texture.repeat.y = values.ScaleV;
338
+ if (values.U !== undefined || values.V !== undefined)
339
+ texture.wrapS = texture.wrapT = three.RepeatWrapping;
340
+ }
341
+ material.transparent = material.opacity < 1 || base.transparent;
342
+ material.depthWrite = base.depthWrite && !material.transparent;
343
+ if (wasTransparent !== material.transparent)
344
+ material.needsUpdate = true;
345
+ return material;
346
+ });
347
+ };
348
+ let seekRevision = 0, pendingFrames;
349
+ const apply = (time) => {
350
+ if (disposed)
351
+ throw new Error("RAW3D playback has been disposed.");
352
+ frame = animation.sampleFrame(time);
353
+ const states = frame.nodes;
354
+ // Detach first so valid hierarchy changes do not depend on source node order.
355
+ for (const object of objects.values())
356
+ group.add(object);
357
+ for (const node of project.nodes) {
358
+ const object = objects.get(node.index), state = states.get(node.index);
359
+ object.position.set(state.location[0], state.location[1], -state.location[2]);
360
+ object.quaternion.set(-state.quaternion[0], -state.quaternion[1], state.quaternion[2], state.quaternion[3]);
361
+ object.scale.set(...state.scale);
362
+ object.visible = !state.deleted;
363
+ object.userData.raw3d.layerIndex = state.layerIndex;
364
+ object.userData.raw3d.interactionMode = state.interactionMode;
365
+ object.userData.raw3d.highlight = state.highlight;
366
+ object.userData.raw3d.userData = state.userData;
367
+ object.userData.raw3d.clientOwner = state.clientOwner;
368
+ object.userData.raw3d.hasBeenOptimized = state.hasBeenOptimized;
369
+ const parent = state.parentIndex === undefined ? undefined : objects.get(state.parentIndex);
370
+ if (parent && parent !== object && !isDescendantOf(parent, object))
371
+ parent.add(object);
372
+ else if (state.parentIndex !== undefined)
373
+ throw new Error(`Invalid animated parent ${state.parentIndex} for RAW3D node ${node.index}.`);
374
+ let drawable = object.children.find(child => ["mesh", "lines", "points"].includes(child.userData.raw3d?.kind));
375
+ const mesh = state.meshIndex === undefined ? undefined : project.meshes[state.meshIndex];
376
+ if (state.meshIndex !== undefined && !mesh)
377
+ throw new Error(`Missing animated RAW3D mesh ${state.meshIndex}.`);
378
+ if (mesh && !object.userData.raw3d.annotationHidden) {
379
+ if (!geometryCache.has(state.meshIndex))
380
+ geometryCache.set(state.meshIndex, decodeRaw3DThreeGeometry(mesh, project, three));
381
+ if (!drawable || drawable.userData.raw3d.meshType !== mesh.meshType || (drawable.userData.raw3d.meshIndex !== state.meshIndex && (skinBindings.has(drawable) || mesh.bones.length > 0))) {
382
+ if (drawable) {
383
+ object.remove(drawable);
384
+ skinBindings.get(drawable)?.dispose();
385
+ skinBindings.delete(drawable);
386
+ }
387
+ drawable = createDrawable(mesh, geometryCache.get(state.meshIndex), [defaultMaterial], three);
388
+ drawable.userData.raw3d = { kind: primitiveKind(mesh.meshType), nodeIndex: node.index, meshType: mesh.meshType };
389
+ drawable.castShadow = node.castsShadow && drawable instanceof three.Mesh;
390
+ object.add(drawable);
391
+ if (mesh.bones.length && drawable instanceof three.Mesh) {
392
+ const binding = bindRaw3DThreeSkin(drawable, state.meshIndex, project, objects, three);
393
+ drawable = binding.drawable;
394
+ skinBindings.set(drawable, binding);
395
+ }
396
+ // The line/point factory creates topology-specific materials.
397
+ for (const material of Array.isArray(drawable.material) ? drawable.material : [drawable.material])
398
+ ownedMaterials.add(material);
399
+ }
400
+ if (!skinBindings.has(drawable))
401
+ drawable.geometry = geometryCache.get(state.meshIndex);
402
+ drawable.userData.raw3d.meshIndex = state.meshIndex;
403
+ const selected = palette(node, state, mesh);
404
+ if (drawable instanceof three.Mesh)
405
+ drawable.material = selected.length === 1 ? selected[0] : selected;
406
+ else {
407
+ const current = Array.isArray(drawable.material) ? drawable.material : [drawable.material];
408
+ // Keep topology-specific materials instead of assigning mesh shaders to lines/points.
409
+ const adjusted = selected.map((source, slot) => {
410
+ const material = current[slot] ?? (drawable instanceof three.Points ? toPointMaterial(source, three) : toLineMaterial(source, three));
411
+ ownedMaterials.add(material);
412
+ const target = material, original = source;
413
+ target.color.copy(original.color);
414
+ target.opacity = original.opacity;
415
+ target.transparent = original.transparent;
416
+ target.depthWrite = original.depthWrite;
417
+ target.needsUpdate = true;
418
+ if (drawable instanceof three.Points)
419
+ target.size = state.materialProperties[slot]?.PointSize ?? project.materials[node.materialIndices[slot] ?? node.materialIndices[0] ?? (state.layerIndex === undefined ? -1 : project.layers[state.layerIndex]?.materialIndex ?? -1)]?.pointSize ?? 1;
420
+ return material;
421
+ });
422
+ drawable.material = adjusted.length === 1 ? adjusted[0] : adjusted;
423
+ }
424
+ configureRaw3DDrawable(drawable, state.isBillboard, state.isDecal, three);
425
+ }
426
+ if (drawable) {
427
+ drawable.visible = state.visible && !!mesh && !object.userData.raw3d.annotationHidden;
428
+ drawable.userData.raw3d.sourceVisible = drawable.visible;
429
+ }
430
+ for (const child of object.children)
431
+ if (child.userData.raw3d?.kind === "text") {
432
+ child.visible = state.visible && !object.userData.raw3d.annotationHidden;
433
+ child.userData.raw3d.sourceVisible = child.visible;
434
+ }
435
+ }
436
+ // Retain only geometry currently used by nodes; decoded mesh samples are recreatable.
437
+ const used = new Set([...states.values()].map(state => state.meshIndex));
438
+ for (const [index, geometry] of geometryCache)
439
+ if (!used.has(index)) {
440
+ geometry.dispose();
441
+ geometryCache.delete(index);
442
+ }
443
+ group.updateMatrixWorld(true);
444
+ for (const binding of skinBindings.values())
445
+ binding.update();
446
+ applyRaw3DThreeEffects(group, frame, project, three);
447
+ };
448
+ const seek = (time) => { seekRevision++; pendingFrames?.abort(); apply(time); };
449
+ const seekAsync = async (time) => {
450
+ if (disposed)
451
+ throw new Error("RAW3D playback has been disposed.");
452
+ const next = animation.sampleFrame(time), revision = ++seekRevision;
453
+ pendingFrames?.abort();
454
+ const controller = new AbortController();
455
+ pendingFrames = controller;
456
+ const requested = new Set();
457
+ for (const node of project.nodes) {
458
+ const state = next.nodes.get(node.index);
459
+ const mesh = state.meshIndex === undefined ? undefined : project.meshes[state.meshIndex];
460
+ for (let slot = 0; slot < Math.max(1, mesh?.indexBufferIndices.length ?? 1); slot++) {
461
+ const materialIndex = node.materialIndices[slot] ?? node.materialIndices[0] ?? (state.layerIndex === undefined ? undefined : project.layers[state.layerIndex]?.materialIndex);
462
+ const stream = state.materialProperties[slot]?.StreamableTexture ?? (materialIndex === undefined ? -1 : project.materials[materialIndex]?.streamableTextureIndex ?? -1);
463
+ if (stream >= 0)
464
+ requested.add(stream);
465
+ }
466
+ }
467
+ try {
468
+ const resolved = options.resolveStreamTexture ? await Promise.all([...requested].map(async (index) => [index, await options.resolveStreamTexture({ index, time: Math.max(0, time), source: project.streamableTextures.find(source => source.index === index), signal: controller.signal })])) : [];
469
+ if (disposed || revision !== seekRevision)
470
+ return false;
471
+ for (const [index, texture] of resolved) {
472
+ if (texture !== undefined && !texture.isTexture)
473
+ throw new Error("A stream texture resolver must return a Three.js texture or undefined.");
474
+ const key = `stream:${index}:color`, selected = texture ?? archivedStreams.get(key);
475
+ if (selected)
476
+ resources.textures.set(key, selected);
477
+ else
478
+ resources.textures.delete(key);
479
+ streamRevisions.set(index, (streamRevisions.get(index) ?? 0) + 1);
480
+ }
481
+ apply(time);
482
+ return true;
483
+ }
484
+ catch (error) {
485
+ if (disposed || revision !== seekRevision)
486
+ return false;
487
+ throw error;
488
+ }
489
+ finally {
490
+ if (revision === seekRevision)
491
+ pendingFrames = undefined;
492
+ }
493
+ };
494
+ const dispose = () => {
495
+ if (disposed)
496
+ return;
497
+ disposed = true;
498
+ seekRevision++;
499
+ pendingFrames?.abort();
500
+ for (const binding of skinBindings.values())
501
+ binding.dispose();
502
+ skinBindings.clear();
503
+ for (const geometry of geometryCache.values())
504
+ geometry.dispose();
505
+ group.traverse(object => { if (object.userData.raw3d?.kind === "scene-floor")
506
+ object.geometry.dispose(); });
507
+ capture();
508
+ for (const material of ownedMaterials) {
509
+ const source = material;
510
+ if (source.map)
511
+ ownedTextures.add(source.map);
512
+ if (source.normalMap)
513
+ ownedTextures.add(source.normalMap);
514
+ }
515
+ for (const material of ownedMaterials)
516
+ material.dispose();
517
+ for (const texture of ownedTextures)
518
+ texture.dispose();
519
+ group.clear();
520
+ variants.clear();
521
+ geometryCache.clear();
522
+ };
523
+ try {
524
+ await seekAsync(0);
525
+ }
526
+ catch (error) {
527
+ dispose();
528
+ throw error;
529
+ }
530
+ return { group, animation, duration: animation.duration, get state() { return frame; }, seek, seekAsync, dispose };
199
531
  }
200
532
  export function decodeRaw3DThreeGeometry(mesh, project, three) {
201
533
  const vertexBuffer = project.vertexBuffers[mesh.vertexBufferIndex];
@@ -217,10 +549,25 @@ export function decodeRaw3DThreeGeometry(mesh, project, three) {
217
549
  for (let vertex = 0; vertex < vertexCount; vertex += 1) {
218
550
  const base = vertex * vertexBuffer.stride + sourceAttribute.offset;
219
551
  for (let component = 0; component < sourceAttribute.componentCount; component += 1) {
220
- let value = view.getFloat32(base + component * 4, true);
552
+ let value;
553
+ if (/^Color$/i.test(sourceAttribute.type)) {
554
+ const packed = view.getUint32(base, true);
555
+ value = ((packed >>> [16, 8, 0, 24][component]) & 255) / 255;
556
+ }
557
+ else if (/^Byte4$/i.test(sourceAttribute.type))
558
+ value = view.getUint8(base + component);
559
+ else if (/^(?:Normalized)?Short[24]$/i.test(sourceAttribute.type)) {
560
+ value = view.getInt16(base + component * 2, true);
561
+ if (/^Normalized/i.test(sourceAttribute.type))
562
+ value = Math.max(-1, value / 32767);
563
+ }
564
+ else
565
+ value = view.getFloat32(base + component * 4, true);
221
566
  if ((name === "position" || name === "normal" || name === "tangent") && component === 2) {
222
567
  value = -value;
223
568
  }
569
+ if (name === "tangent" && component === 3)
570
+ value = -value;
224
571
  values[vertex * sourceAttribute.componentCount + component] = value;
225
572
  }
226
573
  }
@@ -307,7 +654,8 @@ function toPointMaterial(material, three) {
307
654
  opacity: source.opacity,
308
655
  transparent: source.transparent,
309
656
  depthWrite: source.depthWrite,
310
- size: 1,
657
+ size: source.userData.raw3dPointSize ?? 1,
658
+ vertexColors: /^(pointcloud|1)$/i.test(source.userData.raw3dShader ?? ""),
311
659
  sizeAttenuation: true
312
660
  });
313
661
  }
@@ -393,6 +741,9 @@ function resolveNodeMaterials(node, mesh, project, materials, fallback) {
393
741
  }
394
742
  async function loadTextures(project, three, stats, warn) {
395
743
  const wanted = new Map();
744
+ for (const stream of project.streamableTextures)
745
+ if (stream.data)
746
+ wanted.set(`stream:${stream.index}:color`, { texture: stream, color: true });
396
747
  for (const material of project.materials) {
397
748
  if (material.textureIndex !== undefined && project.textures[material.textureIndex]) {
398
749
  wanted.set(`${material.textureIndex}:color`, { texture: project.textures[material.textureIndex], color: true });
@@ -401,6 +752,21 @@ async function loadTextures(project, three, stats, warn) {
401
752
  wanted.set(`${material.normalTextureIndex}:normal`, { texture: project.textures[material.normalTextureIndex], color: false });
402
753
  }
403
754
  }
755
+ for (const update of project.root.child("Updates")?.children ?? []) {
756
+ if (update.localName !== "Material")
757
+ continue;
758
+ const name = update.attributes.find(attribute => attribute.localName === "Name")?.value;
759
+ if (!["Texture", "NormalMap", "NormalTexture"].includes(name ?? ""))
760
+ continue;
761
+ const value = update.attributes.find(attribute => attribute.localName === "Value")?.value ?? "";
762
+ for (const token of value.trim().split(/\s+/)) {
763
+ const index = Number(token), texture = project.textures[index];
764
+ if (texture) {
765
+ const color = name === "Texture";
766
+ wanted.set(`${index}:${color ? "color" : "normal"}`, { texture, color });
767
+ }
768
+ }
769
+ }
404
770
  const loaded = new Map();
405
771
  await Promise.all([...wanted].map(async ([key, item]) => {
406
772
  if (!item.texture.data) {
@@ -444,10 +810,13 @@ function createMaterial(source, textures, three) {
444
810
  : textures.get(`${source.normalTextureIndex}:normal`);
445
811
  const map = sharedMap?.clone();
446
812
  const normalMap = sharedNormalMap?.clone();
447
- if (map && (source.scaleU !== 1 || source.scaleV !== 1)) {
448
- map.wrapS = three.RepeatWrapping;
449
- map.wrapT = three.RepeatWrapping;
450
- map.repeat.set(source.scaleU, source.scaleV);
813
+ for (const texture of [map, normalMap]) {
814
+ if (!texture)
815
+ continue;
816
+ if (source.scaleU !== 1 || source.scaleV !== 1 || source.offsetU !== 0 || source.offsetV !== 0)
817
+ texture.wrapS = texture.wrapT = three.RepeatWrapping;
818
+ texture.repeat.set(source.scaleU, source.scaleV);
819
+ texture.offset.set(source.offsetU, source.offsetV);
451
820
  }
452
821
  const parameters = {
453
822
  color: new three.Color(clamp01(source.red), clamp01(source.green), clamp01(source.blue)),
@@ -464,7 +833,26 @@ function createMaterial(source, textures, three) {
464
833
  parameters.normalMap = normalMap;
465
834
  parameters.normalScale = new three.Vector2(source.normalScale, source.normalScale);
466
835
  }
467
- return new three.MeshStandardMaterial(parameters);
836
+ let result;
837
+ switch (source.shader.toLowerCase()) {
838
+ case "flattext":
839
+ case "2":
840
+ result = new three.MeshBasicMaterial({ color: parameters.color, map: map ?? null, opacity, transparent: opacity < 1 || !!map, depthWrite: opacity >= 1, side: three.DoubleSide });
841
+ break;
842
+ case "default":
843
+ case "0":
844
+ case "pointcloud":
845
+ case "1":
846
+ result = raw3DNodeBackend(three)?.standard(parameters) ?? new three.MeshStandardMaterial(parameters);
847
+ break;
848
+ default: throw new Error(`Unsupported RAW3D material shader ${source.shader}.`);
849
+ }
850
+ result.userData.raw3dShader = source.shader;
851
+ result.userData.raw3dPointSize = source.pointSize;
852
+ result.userData.raw3dNormalDepth = source.normalDepth;
853
+ if ("emissive" in result)
854
+ result.emissive.copy(result.color).multiplyScalar(source.luminosity);
855
+ return result;
468
856
  }
469
857
  function applyTransform(object, node) {
470
858
  object.position.set(node.location[0] ?? 0, node.location[1] ?? 0, -(node.location[2] ?? 0));