@es3d/es3d 1.1.2 → 1.2.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 +24 -0
- package/browser/es3d-stage.min.js +243 -10232
- package/browser/es3d.min.js +534 -0
- package/docs/STAGE.md +95 -0
- package/package.json +6 -3
- package/src/browser.js +16 -0
- package/src/cache/geometry-store.js +56 -18
- package/src/geometry/geometry-cache.js +93 -8
- package/src/io/compression.js +4 -1
- package/src/io/convert.js +14 -1
- package/src/pipeline.js +2 -1
- package/src/render/compile-worker.js +143 -0
- package/src/render/frame-builder.js +114 -77
- package/src/render/renderer.js +28 -2
- package/src/render/stage.js +305 -50
- package/src/render/worker-boot.js +14 -0
- package/src/scene/fragments.js +527 -0
- package/src/scene/picking.js +31 -2
- package/src/scene/prepared.js +398 -0
- package/src/scene/scene-graph.js +1 -1
- package/src/scene/scene-set.js +251 -0
|
@@ -171,23 +171,31 @@ export class FrameBuilder {
|
|
|
171
171
|
*/
|
|
172
172
|
build(p) {
|
|
173
173
|
const { scene, camera, pool, meshOf, options } = p;
|
|
174
|
-
|
|
174
|
+
// A SceneSet draws several instance tables (the host and the fragments,
|
|
175
|
+
// scene-set.js); a RenderScene is one part whose indices need no map.
|
|
176
|
+
const parts = scene.parts ? scene.drawnParts() : [{ scene, geomMap: null, matMap: null, selectable: true }];
|
|
177
|
+
let n = 0;
|
|
178
|
+
for (const part of parts) n += part.scene.count;
|
|
175
179
|
// reserve instance rows for: batches (<= n), bounding boxes (<= n), selection boxes
|
|
176
180
|
let selCount = 0;
|
|
177
181
|
if (options.selection && options.selection.size) {
|
|
178
|
-
for (
|
|
182
|
+
for (const part of parts) {
|
|
183
|
+
if (!part.selectable) continue;
|
|
184
|
+
const P = part.scene;
|
|
185
|
+
for (let i = 0; i < P.count; i++) if (options.selection.has(P.uid[i])) selCount++;
|
|
186
|
+
}
|
|
179
187
|
}
|
|
180
188
|
this.ensure(n + (options.bbox ? n : 0) + selCount + 64);
|
|
181
189
|
const camPos = camera.position;
|
|
182
190
|
const dist = (c) => Math.hypot(c[0] - camPos[0], c[1] - camPos[1], c[2] - camPos[2]);
|
|
183
|
-
const sw = this.lodStates(scene, camPos, dist);
|
|
184
191
|
const planes = options.culling === false ? null : planesFrom(camera.viewProj);
|
|
185
|
-
const S = scene.spheres;
|
|
186
192
|
const keys = this.keys;
|
|
187
193
|
const lods = this.lods;
|
|
188
194
|
const batchMap = new Map();
|
|
189
195
|
const batches = [];
|
|
190
|
-
const
|
|
196
|
+
const geometries = scene.geometries;
|
|
197
|
+
const materials = scene.materials;
|
|
198
|
+
const nMat = Math.max(1, materials.length);
|
|
191
199
|
const lodHist = [0, 0, 0, 0];
|
|
192
200
|
let visibleCount = 0;
|
|
193
201
|
let culled = 0;
|
|
@@ -197,52 +205,64 @@ export class FrameBuilder {
|
|
|
197
205
|
// pixels per world unit: depth based for perspective, constant for orthographic
|
|
198
206
|
const lodK = lodFrameConstants(camera, vh);
|
|
199
207
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
// rows are numbered across the parts: part k's row r is base(k) + r
|
|
209
|
+
let base = 0;
|
|
210
|
+
for (const part of parts) {
|
|
211
|
+
const P = part.scene;
|
|
212
|
+
const gm = part.geomMap;
|
|
213
|
+
const mm = part.matMap;
|
|
214
|
+
const pn = P.count;
|
|
215
|
+
const sw = this.lodStates(P, camPos, dist);
|
|
216
|
+
const S = P.spheres;
|
|
217
|
+
for (let r = 0; r < pn; r++) {
|
|
218
|
+
const i = base + r;
|
|
219
|
+
keys[i] = -1;
|
|
220
|
+
const ln = P.lodNode[r];
|
|
221
|
+
if (ln >= 0 && !(sw.visible[ln] && sw.active[ln] === P.lodChild[r])) continue;
|
|
222
|
+
const cx = S[r * 4], cy = S[r * 4 + 1], cz = S[r * 4 + 2], rad = S[r * 4 + 3];
|
|
223
|
+
if (planes) {
|
|
224
|
+
let out = false;
|
|
225
|
+
for (let k = 0; k < 5; k++) {
|
|
226
|
+
const pl = planes[k];
|
|
227
|
+
if (pl[0] * cx + pl[1] * cy + pl[2] * cz + pl[3] < -rad) {
|
|
228
|
+
out = true;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (out) {
|
|
233
|
+
culled++;
|
|
234
|
+
continue;
|
|
212
235
|
}
|
|
213
236
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
237
|
+
const gi = gm ? gm[P.geom[r]] : P.geom[r];
|
|
238
|
+
const entry = geometries[gi];
|
|
239
|
+
let lod = 0;
|
|
240
|
+
if (entry.lods > 1) {
|
|
241
|
+
if (fixedLod >= 0) lod = Math.min(fixedLod, entry.lods - 1);
|
|
242
|
+
else {
|
|
243
|
+
const dx = cx - camPos[0], dy = cy - camPos[1], dz = cz - camPos[2];
|
|
244
|
+
const d = length3(dx, dy, dz);
|
|
245
|
+
const ppu = instancePixelsPerUnit(lodK, cx, cy, cz, d);
|
|
246
|
+
lod = selectLod(entry.lodDeviation, entry.lods, axisScale(P.matrices, r), ppu, bias);
|
|
247
|
+
}
|
|
217
248
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
249
|
+
lods[i] = lod;
|
|
250
|
+
lodHist[lod]++;
|
|
251
|
+
const mirrored = P.flags[r] & 1;
|
|
252
|
+
const mi = mm ? mm[P.mat[r]] : P.mat[r];
|
|
253
|
+
const key = ((gi * 4 + lod) * nMat + mi) * 2 + mirrored;
|
|
254
|
+
let b = batchMap.get(key);
|
|
255
|
+
if (b === undefined) {
|
|
256
|
+
b = batches.length;
|
|
257
|
+
batchMap.set(key, b);
|
|
258
|
+
const material = materials[mi];
|
|
259
|
+
batches.push({ gi, entry, lod, mi, material, mirrored: !!mirrored, count: 0, firstInstance: 0, depth: 0, instances: null });
|
|
229
260
|
}
|
|
261
|
+
batches[b].count++;
|
|
262
|
+
keys[i] = b;
|
|
263
|
+
visibleCount++;
|
|
230
264
|
}
|
|
231
|
-
|
|
232
|
-
lodHist[lod]++;
|
|
233
|
-
const mirrored = scene.flags[i] & 1;
|
|
234
|
-
const mi = scene.mat[i];
|
|
235
|
-
const key = ((gi * 4 + lod) * nMat + mi) * 2 + mirrored;
|
|
236
|
-
let b = batchMap.get(key);
|
|
237
|
-
if (b === undefined) {
|
|
238
|
-
b = batches.length;
|
|
239
|
-
batchMap.set(key, b);
|
|
240
|
-
const material = scene.materials[mi];
|
|
241
|
-
batches.push({ gi, entry, lod, mi, material, mirrored: !!mirrored, count: 0, firstInstance: 0, depth: 0, instances: null });
|
|
242
|
-
}
|
|
243
|
-
batches[b].count++;
|
|
244
|
-
keys[i] = b;
|
|
245
|
-
visibleCount++;
|
|
265
|
+
base += pn;
|
|
246
266
|
}
|
|
247
267
|
|
|
248
268
|
// resolve meshes / pool ranges. Once this frame has spent its tessellation
|
|
@@ -288,27 +308,32 @@ export class FrameBuilder {
|
|
|
288
308
|
b.cursor = b.firstInstance;
|
|
289
309
|
}
|
|
290
310
|
const data = this.instanceData;
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
for (
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
continue;
|
|
311
|
+
const transparent = []; // {M, r, b, depth}: drawn back to front after the opaque rows
|
|
312
|
+
base = 0;
|
|
313
|
+
for (const part of parts) {
|
|
314
|
+
const P = part.scene;
|
|
315
|
+
const M = P.matrices;
|
|
316
|
+
const S = P.spheres;
|
|
317
|
+
for (let r = 0; r < P.count; r++) {
|
|
318
|
+
const k = keys[base + r];
|
|
319
|
+
if (k < 0) continue;
|
|
320
|
+
const b = batches[k];
|
|
321
|
+
if (b.transparent) {
|
|
322
|
+
transparent.push({ M, r, b, depth: -length3(S[r * 4] - camPos[0], S[r * 4 + 1] - camPos[1], S[r * 4 + 2] - camPos[2]) });
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const o = b.cursor++ * 16;
|
|
326
|
+
for (let j = 0; j < 16; j++) data[o + j] = M[r * 16 + j];
|
|
300
327
|
}
|
|
301
|
-
|
|
302
|
-
for (let j = 0; j < 16; j++) data[o + j] = M[i * 16 + j];
|
|
328
|
+
base += P.count;
|
|
303
329
|
}
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const b = batches[keys[i]];
|
|
330
|
+
if (transparent.length) {
|
|
331
|
+
transparent.sort((a, b) => a.depth - b.depth);
|
|
332
|
+
for (const t of transparent) {
|
|
333
|
+
const b = t.b;
|
|
309
334
|
const o = b.cursor++ * 16;
|
|
310
|
-
for (let j = 0; j < 16; j++) data[o + j] = M[
|
|
311
|
-
b.depth = Math.max(b.depth, -
|
|
335
|
+
for (let j = 0; j < 16; j++) data[o + j] = t.M[t.r * 16 + j];
|
|
336
|
+
b.depth = Math.max(b.depth, -t.depth);
|
|
312
337
|
}
|
|
313
338
|
// farthest transparent batches first
|
|
314
339
|
const opaque = order.filter((b) => !b.transparent);
|
|
@@ -326,12 +351,13 @@ export class FrameBuilder {
|
|
|
326
351
|
if (options.normals) overlays.normals.push({ range: pool.normalRange(b.mesh), firstInstance: b.firstInstance, count: b.count });
|
|
327
352
|
}
|
|
328
353
|
}
|
|
329
|
-
const boxMatrix = (
|
|
330
|
-
const
|
|
354
|
+
const boxMatrix = (part, r, o) => {
|
|
355
|
+
const P = part.scene;
|
|
356
|
+
const e = geometries[part.geomMap ? part.geomMap[P.geom[r]] : P.geom[r]];
|
|
331
357
|
const bb = e.bounds;
|
|
332
358
|
const cx = (bb.min[0] + bb.max[0]) / 2, cy = (bb.min[1] + bb.max[1]) / 2, cz = (bb.min[2] + bb.max[2]) / 2;
|
|
333
359
|
const sx = Math.max(bb.max[0] - bb.min[0], 1e-4), sy = Math.max(bb.max[1] - bb.min[1], 1e-4), sz = Math.max(bb.max[2] - bb.min[2], 1e-4);
|
|
334
|
-
const m =
|
|
360
|
+
const m = P.matrices.subarray(r * 16, r * 16 + 16);
|
|
335
361
|
// world * T(c) * S(s)
|
|
336
362
|
for (let k = 0; k < 3; k++) {
|
|
337
363
|
data[o + k] = m[k] * sx;
|
|
@@ -346,22 +372,33 @@ export class FrameBuilder {
|
|
|
346
372
|
};
|
|
347
373
|
if (options.bbox) {
|
|
348
374
|
const first = offset;
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
375
|
+
base = 0;
|
|
376
|
+
for (const part of parts) {
|
|
377
|
+
for (let r = 0; r < part.scene.count; r++) {
|
|
378
|
+
if (keys[base + r] < 0) continue;
|
|
379
|
+
boxMatrix(part, r, offset * 16);
|
|
380
|
+
offset++;
|
|
381
|
+
}
|
|
382
|
+
base += part.scene.count;
|
|
353
383
|
}
|
|
354
384
|
overlays.bbox = { firstInstance: first, count: offset - first };
|
|
355
385
|
}
|
|
356
386
|
if (selCount) {
|
|
357
387
|
const first = offset;
|
|
358
388
|
const sel = options.selection;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
389
|
+
base = 0;
|
|
390
|
+
for (const part of parts) {
|
|
391
|
+
const P = part.scene;
|
|
392
|
+
if (part.selectable) {
|
|
393
|
+
for (let r = 0; r < P.count; r++) {
|
|
394
|
+
if (!sel.has(P.uid[r])) continue;
|
|
395
|
+
// hidden level-of-detail alternatives are not highlighted
|
|
396
|
+
if (keys[base + r] < 0 && P.lodNode[r] >= 0) continue;
|
|
397
|
+
boxMatrix(part, r, offset * 16);
|
|
398
|
+
offset++;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
base += P.count;
|
|
365
402
|
}
|
|
366
403
|
overlays.selection = { firstInstance: first, count: offset - first };
|
|
367
404
|
}
|
package/src/render/renderer.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// The facade is the only renderer API used by the application and the
|
|
6
6
|
// benchmark; backends never see the ES3D document, only a RenderScene.
|
|
7
7
|
|
|
8
|
-
import { GeometryPool } from './geometry-pool.js';
|
|
8
|
+
import { GeometryPool, VERTEX_BYTES } from './geometry-pool.js';
|
|
9
9
|
import { FrameBuilder } from './frame-builder.js';
|
|
10
10
|
import { TextureCache } from './texture-cache.js';
|
|
11
11
|
import { FRAME_FLOATS, packFrameUniforms } from './lighting.js';
|
|
@@ -106,14 +106,40 @@ export class Renderer {
|
|
|
106
106
|
setScene(scene, meshOf, { keepGeometry = true } = {}) {
|
|
107
107
|
this.scene = scene;
|
|
108
108
|
this.meshOf = meshOf;
|
|
109
|
-
if (!scene || !keepGeometry || this.pool.byteSize > POOL_REPACK_BYTES) {
|
|
109
|
+
if (!scene || !keepGeometry || (!scene.parts && this.pool.byteSize > POOL_REPACK_BYTES)) {
|
|
110
110
|
this.pool.reset();
|
|
111
|
+
this.pool.unusedBytes = 0;
|
|
111
112
|
this.cubeRange = this.pool.meshRange(this.cube);
|
|
112
113
|
}
|
|
113
114
|
this.backend.releaseMaterials(new Set(scene ? scene.materials : []));
|
|
114
115
|
this.stats.totalUploadBytes = 0;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The scene set being drawn changed (a fragment placed, hidden or removed):
|
|
120
|
+
* keep every packed mesh, free the materials no part uses, and repack the
|
|
121
|
+
* pool only once most of what it holds is geometry no part draws any more.
|
|
122
|
+
* Cheap: nothing here is proportional to the scene.
|
|
123
|
+
* @param {import('../scene/scene-set.js').SceneSet} set
|
|
124
|
+
*/
|
|
125
|
+
updateScene(set) {
|
|
126
|
+
for (const entry of set.takeReleased()) {
|
|
127
|
+
if (set.geomIndex.has(entry)) continue; // taken again by another part
|
|
128
|
+
for (const mesh of entry.meshes) {
|
|
129
|
+
if (!mesh) continue;
|
|
130
|
+
const r = this.pool.ranges.get(mesh);
|
|
131
|
+
if (r) this.pool.unusedBytes = (this.pool.unusedBytes || 0) + mesh.vertexCount * VERTEX_BYTES + r.indexCount * 4;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (this.pool.byteSize > POOL_REPACK_BYTES && (this.pool.unusedBytes || 0) * 2 > this.pool.byteSize) {
|
|
135
|
+
this.pool.reset();
|
|
136
|
+
this.pool.unusedBytes = 0;
|
|
137
|
+
this.cubeRange = this.pool.meshRange(this.cube);
|
|
138
|
+
}
|
|
139
|
+
if (this.materialsSeen !== set.materials.length || set.freeMaterials.length) this.backend.releaseMaterials(new Set(set.materials));
|
|
140
|
+
this.materialsSeen = set.materials.length;
|
|
141
|
+
}
|
|
142
|
+
|
|
117
143
|
/**
|
|
118
144
|
* Render one frame.
|
|
119
145
|
* @param {import('./camera.js').Camera} camera
|