@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.
- package/README.md +225 -0
- package/dist/examples/export-embedded-viewer-standalone.html +1 -1
- package/dist/examples/three-render-smoke-standalone.html +295 -231
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +1 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +6 -0
- package/dist/model.js.map +1 -1
- package/dist/raw3d-animation.d.ts +63 -0
- package/dist/raw3d-animation.d.ts.map +1 -0
- package/dist/raw3d-animation.js +200 -0
- package/dist/raw3d-animation.js.map +1 -0
- package/dist/raw3d-effects.d.ts +66 -0
- package/dist/raw3d-effects.d.ts.map +1 -0
- package/dist/raw3d-effects.js +172 -0
- package/dist/raw3d-effects.js.map +1 -0
- package/dist/raw3d-skinning.d.ts +8 -0
- package/dist/raw3d-skinning.d.ts.map +1 -0
- package/dist/raw3d-skinning.js +37 -0
- package/dist/raw3d-skinning.js.map +1 -0
- package/dist/raw3d.d.ts +77 -2
- package/dist/raw3d.d.ts.map +1 -1
- package/dist/raw3d.js +103 -7
- package/dist/raw3d.js.map +1 -1
- package/dist/rigid-interpolation.d.ts +16 -0
- package/dist/rigid-interpolation.d.ts.map +1 -0
- package/dist/rigid-interpolation.js +52 -0
- package/dist/rigid-interpolation.js.map +1 -0
- package/dist/three/backend.d.ts +13 -0
- package/dist/three/backend.d.ts.map +1 -0
- package/dist/three/backend.js +9 -0
- package/dist/three/backend.js.map +1 -0
- package/dist/three/effects.d.ts +7 -0
- package/dist/three/effects.d.ts.map +1 -0
- package/dist/three/effects.js +127 -0
- package/dist/three/effects.js.map +1 -0
- package/dist/three/index.d.ts +2 -1
- package/dist/three/index.d.ts.map +1 -1
- package/dist/three/index.js +72 -14
- package/dist/three/index.js.map +1 -1
- package/dist/three/native-lighting.d.ts +5 -0
- package/dist/three/native-lighting.d.ts.map +1 -0
- package/dist/three/native-lighting.js +86 -0
- package/dist/three/native-lighting.js.map +1 -0
- package/dist/three/parallax.d.ts +5 -0
- package/dist/three/parallax.d.ts.map +1 -0
- package/dist/three/parallax.js +63 -0
- package/dist/three/parallax.js.map +1 -0
- package/dist/three/raw3d.d.ts +25 -0
- package/dist/three/raw3d.d.ts.map +1 -1
- package/dist/three/raw3d.js +404 -16
- package/dist/three/raw3d.js.map +1 -1
- package/dist/three/renderer.d.ts.map +1 -1
- package/dist/three/renderer.js +3 -0
- package/dist/three/renderer.js.map +1 -1
- package/dist/three/skinning.d.ts +11 -0
- package/dist/three/skinning.d.ts.map +1 -0
- package/dist/three/skinning.js +92 -0
- package/dist/three/skinning.js.map +1 -0
- package/dist/three/tsl.d.ts +60 -0
- package/dist/three/tsl.d.ts.map +1 -0
- package/dist/three/tsl.js +3 -0
- package/dist/three/tsl.js.map +1 -0
- package/dist/three/webgpu-native.d.ts +3 -0
- package/dist/three/webgpu-native.d.ts.map +1 -0
- package/dist/three/webgpu-native.js +58 -0
- package/dist/three/webgpu-native.js.map +1 -0
- package/dist/three/webgpu-relief.d.ts +3 -0
- package/dist/three/webgpu-relief.d.ts.map +1 -0
- package/dist/three/webgpu-relief.js +61 -0
- package/dist/three/webgpu-relief.js.map +1 -0
- package/package.json +1 -1
package/dist/three/raw3d.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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));
|