@es3d/es3d 1.1.2 → 1.2.1
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/geometry-pool.js +103 -41
- package/src/render/renderer.js +28 -2
- package/src/render/stage.js +305 -50
- package/src/render/webgl2/webgl2-backend.js +68 -16
- package/src/render/webgpu/webgpu-backend.js +42 -11
- 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,12 +171,8 @@ export class WebGL2Backend {
|
|
|
171
171
|
this.vao = gl.createVertexArray();
|
|
172
172
|
gl.bindVertexArray(this.vao);
|
|
173
173
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
174
|
-
gl.enableVertexAttribArray(
|
|
175
|
-
|
|
176
|
-
gl.enableVertexAttribArray(1);
|
|
177
|
-
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, VERTEX_BYTES, 12);
|
|
178
|
-
gl.enableVertexAttribArray(2);
|
|
179
|
-
gl.vertexAttribPointer(2, 2, gl.FLOAT, false, VERTEX_BYTES, 24);
|
|
174
|
+
for (let k = 0; k < 3; k++) gl.enableVertexAttribArray(k);
|
|
175
|
+
this.bindVertexAttributes();
|
|
180
176
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
|
|
181
177
|
for (let k = 0; k < 4; k++) {
|
|
182
178
|
gl.enableVertexAttribArray(3 + k);
|
|
@@ -215,14 +211,28 @@ export class WebGL2Backend {
|
|
|
215
211
|
this.gl.deleteTexture(t);
|
|
216
212
|
}
|
|
217
213
|
|
|
218
|
-
/**
|
|
214
|
+
/** Point the VAO's vertex attributes at the vertex buffer bound to ARRAY_BUFFER. */
|
|
215
|
+
bindVertexAttributes() {
|
|
216
|
+
const gl = this.gl;
|
|
217
|
+
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, VERTEX_BYTES, 0);
|
|
218
|
+
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, VERTEX_BYTES, 12);
|
|
219
|
+
gl.vertexAttribPointer(2, 2, gl.FLOAT, false, VERTEX_BYTES, 24);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Upload geometry appended to the pool since the last sync.
|
|
224
|
+
*
|
|
225
|
+
* A new pool generation (reset, repack, rebuilt context) reallocates both
|
|
226
|
+
* buffers and uploads everything. A pool that only grew keeps its content:
|
|
227
|
+
* the buffer that ran out of room is replaced by a larger one, the bytes
|
|
228
|
+
* already uploaded are copied on the GPU, and only new data is sent.
|
|
229
|
+
*/
|
|
219
230
|
syncPool(pool) {
|
|
220
231
|
const gl = this.gl;
|
|
221
|
-
const vBytes = pool.
|
|
222
|
-
const iBytes = pool.
|
|
223
|
-
const realloc = pool.generation !== this.poolGeneration || vBytes !== this.vertexCapacity || iBytes !== this.indexCapacity;
|
|
232
|
+
const vBytes = pool.vertexCapacityBytes;
|
|
233
|
+
const iBytes = pool.indexCapacityBytes;
|
|
224
234
|
let uploaded = 0;
|
|
225
|
-
if (
|
|
235
|
+
if (pool.generation !== this.poolGeneration) {
|
|
226
236
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
227
237
|
gl.bufferData(gl.ARRAY_BUFFER, vBytes, gl.STATIC_DRAW);
|
|
228
238
|
gl.bindVertexArray(this.vao);
|
|
@@ -234,25 +244,67 @@ export class WebGL2Backend {
|
|
|
234
244
|
this.poolGeneration = pool.generation;
|
|
235
245
|
pool.uploadedVertices = 0;
|
|
236
246
|
pool.uploadedIndices = 0;
|
|
247
|
+
} else {
|
|
248
|
+
if (vBytes > this.vertexCapacity) {
|
|
249
|
+
const old = this.vertexBuffer;
|
|
250
|
+
const next = gl.createBuffer();
|
|
251
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, next);
|
|
252
|
+
gl.bufferData(gl.ARRAY_BUFFER, vBytes, gl.STATIC_DRAW);
|
|
253
|
+
gl.bindVertexArray(this.vao);
|
|
254
|
+
this.bindVertexAttributes();
|
|
255
|
+
gl.bindVertexArray(null);
|
|
256
|
+
this.copyBuffer(old, next, pool.uploadedVertices * VERTEX_BYTES);
|
|
257
|
+
gl.deleteBuffer(old);
|
|
258
|
+
this.vertexBuffer = next;
|
|
259
|
+
this.vertexCapacity = vBytes;
|
|
260
|
+
}
|
|
261
|
+
if (iBytes > this.indexCapacity) {
|
|
262
|
+
// An index buffer must first be bound as ELEMENT_ARRAY_BUFFER (WebGL2
|
|
263
|
+
// types buffers on first bind); bind it through the VAO, which keeps it.
|
|
264
|
+
const old = this.indexBuffer;
|
|
265
|
+
const next = gl.createBuffer();
|
|
266
|
+
gl.bindVertexArray(this.vao);
|
|
267
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, next);
|
|
268
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, iBytes, gl.STATIC_DRAW);
|
|
269
|
+
gl.bindVertexArray(null);
|
|
270
|
+
this.copyBuffer(old, next, pool.uploadedIndices * 4);
|
|
271
|
+
gl.deleteBuffer(old);
|
|
272
|
+
this.indexBuffer = next;
|
|
273
|
+
this.indexCapacity = iBytes;
|
|
274
|
+
}
|
|
237
275
|
}
|
|
238
276
|
const p = pool.pending();
|
|
239
277
|
if (p.vertexEnd > p.vertexStart) {
|
|
240
278
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
279
|
+
pool.forEachVertices(p.vertexStart, p.vertexEnd, (first, data) => {
|
|
280
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, first * VERTEX_BYTES, data);
|
|
281
|
+
uploaded += data.byteLength;
|
|
282
|
+
});
|
|
244
283
|
}
|
|
245
284
|
if (p.indexEnd > p.indexStart) {
|
|
246
285
|
gl.bindVertexArray(this.vao);
|
|
247
286
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
248
|
-
|
|
287
|
+
pool.forEachIndices(p.indexStart, p.indexEnd, (first, data) => {
|
|
288
|
+
gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, first * 4, data);
|
|
289
|
+
uploaded += data.byteLength;
|
|
290
|
+
});
|
|
249
291
|
gl.bindVertexArray(null);
|
|
250
|
-
uploaded += (p.indexEnd - p.indexStart) * 4;
|
|
251
292
|
}
|
|
252
293
|
pool.markUploaded();
|
|
253
294
|
return uploaded;
|
|
254
295
|
}
|
|
255
296
|
|
|
297
|
+
/** Copy the first `bytes` of one buffer into another, on the GPU. */
|
|
298
|
+
copyBuffer(from, to, bytes) {
|
|
299
|
+
if (bytes <= 0) return;
|
|
300
|
+
const gl = this.gl;
|
|
301
|
+
gl.bindBuffer(gl.COPY_READ_BUFFER, from);
|
|
302
|
+
gl.bindBuffer(gl.COPY_WRITE_BUFFER, to);
|
|
303
|
+
gl.copyBufferSubData(gl.COPY_READ_BUFFER, gl.COPY_WRITE_BUFFER, 0, 0, bytes);
|
|
304
|
+
gl.bindBuffer(gl.COPY_READ_BUFFER, null);
|
|
305
|
+
gl.bindBuffer(gl.COPY_WRITE_BUFFER, null);
|
|
306
|
+
}
|
|
307
|
+
|
|
256
308
|
uploadInstances(data, count) {
|
|
257
309
|
const gl = this.gl;
|
|
258
310
|
const bytes = count * 64;
|
|
@@ -254,30 +254,61 @@ export class WebGPUBackend {
|
|
|
254
254
|
});
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Upload geometry appended to the pool since the last sync.
|
|
259
|
+
*
|
|
260
|
+
* A new pool generation (reset, repack) reallocates both buffers and uploads
|
|
261
|
+
* everything. A pool that only grew keeps its content: the buffer that ran
|
|
262
|
+
* out of room is replaced by a larger one, the bytes already uploaded are
|
|
263
|
+
* copied on the GPU, and only new data is sent.
|
|
264
|
+
*/
|
|
257
265
|
syncPool(pool) {
|
|
258
266
|
const d = this.device;
|
|
259
|
-
const vBytes = pool.
|
|
260
|
-
const iBytes = pool.
|
|
267
|
+
const vBytes = pool.vertexCapacityBytes;
|
|
268
|
+
const iBytes = pool.indexCapacityBytes;
|
|
269
|
+
const vUsage = GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
|
270
|
+
const iUsage = GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
|
261
271
|
let uploaded = 0;
|
|
262
|
-
if (pool.generation !== this.poolGeneration || !this.vertexBuffer
|
|
272
|
+
if (pool.generation !== this.poolGeneration || !this.vertexBuffer) {
|
|
263
273
|
if (this.vertexBuffer) this.vertexBuffer.destroy();
|
|
264
274
|
if (this.indexBuffer) this.indexBuffer.destroy();
|
|
265
|
-
this.vertexBuffer = d.createBuffer({ size: pow2(vBytes, 1 << 16), usage:
|
|
266
|
-
this.indexBuffer = d.createBuffer({ size: pow2(iBytes, 1 << 16), usage:
|
|
275
|
+
this.vertexBuffer = d.createBuffer({ size: pow2(vBytes, 1 << 16), usage: vUsage });
|
|
276
|
+
this.indexBuffer = d.createBuffer({ size: pow2(iBytes, 1 << 16), usage: iUsage });
|
|
267
277
|
this.poolGeneration = pool.generation;
|
|
268
278
|
pool.uploadedVertices = 0;
|
|
269
279
|
pool.uploadedIndices = 0;
|
|
280
|
+
} else {
|
|
281
|
+
let encoder = null;
|
|
282
|
+
const grow = (old, bytes, usage, keep) => {
|
|
283
|
+
const next = d.createBuffer({ size: pow2(bytes, 1 << 16), usage });
|
|
284
|
+
if (keep > 0) {
|
|
285
|
+
encoder ||= d.createCommandEncoder();
|
|
286
|
+
encoder.copyBufferToBuffer(old, 0, next, 0, keep);
|
|
287
|
+
}
|
|
288
|
+
return next;
|
|
289
|
+
};
|
|
290
|
+
const oldV = this.vertexBuffer.size < vBytes ? this.vertexBuffer : null;
|
|
291
|
+
const oldI = this.indexBuffer.size < iBytes ? this.indexBuffer : null;
|
|
292
|
+
if (oldV) this.vertexBuffer = grow(oldV, vBytes, vUsage, pool.uploadedVertices * VERTEX_BYTES);
|
|
293
|
+
if (oldI) this.indexBuffer = grow(oldI, iBytes, iUsage, pool.uploadedIndices * 4);
|
|
294
|
+
// the copy is queued before the writes below, so the new data lands after it;
|
|
295
|
+
// destroying a buffer waits for submitted work that reads it
|
|
296
|
+
if (encoder) d.queue.submit([encoder.finish()]);
|
|
297
|
+
if (oldV) oldV.destroy();
|
|
298
|
+
if (oldI) oldI.destroy();
|
|
270
299
|
}
|
|
271
300
|
const p = pool.pending();
|
|
272
301
|
if (p.vertexEnd > p.vertexStart) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
302
|
+
pool.forEachVertices(p.vertexStart, p.vertexEnd, (first, src) => {
|
|
303
|
+
d.queue.writeBuffer(this.vertexBuffer, first * VERTEX_BYTES, src.buffer, src.byteOffset, src.byteLength);
|
|
304
|
+
uploaded += src.byteLength;
|
|
305
|
+
});
|
|
276
306
|
}
|
|
277
307
|
if (p.indexEnd > p.indexStart) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
308
|
+
pool.forEachIndices(p.indexStart, p.indexEnd, (first, src) => {
|
|
309
|
+
d.queue.writeBuffer(this.indexBuffer, first * 4, src.buffer, src.byteOffset, src.byteLength);
|
|
310
|
+
uploaded += src.byteLength;
|
|
311
|
+
});
|
|
281
312
|
}
|
|
282
313
|
pool.markUploaded();
|
|
283
314
|
return uploaded;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The first module the stage evaluates (render/stage.js imports it before
|
|
2
|
+
// anything else). In the compiler worker it keeps every message that arrives
|
|
3
|
+
// before the compiler is ready: the WebAssembly kernels (wasm/kernels.js) are
|
|
4
|
+
// instantiated with a top-level await, and a module worker delivers messages
|
|
5
|
+
// while it waits, so a handler installed after that would miss the first
|
|
6
|
+
// requests. compile-worker.js answers the kept messages once it takes over.
|
|
7
|
+
|
|
8
|
+
/** True inside a dedicated worker. */
|
|
9
|
+
export const inWorker = typeof WorkerGlobalScope !== 'undefined' && typeof self !== 'undefined' && self instanceof WorkerGlobalScope;
|
|
10
|
+
|
|
11
|
+
/** Messages that arrived before the compiler was ready, oldest first. */
|
|
12
|
+
export const early = [];
|
|
13
|
+
|
|
14
|
+
if (inWorker) self.onmessage = (e) => early.push(e);
|