@tinypoly/gpu-renderer 0.0.0 → 0.0.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/ARCHITECTURE.md +7 -2
- package/README.md +28 -2
- package/dist/bvh.d.ts.map +1 -1
- package/dist/bvh.js +178 -89
- package/dist/bvh.js.map +1 -1
- package/dist/bvhBuilder.d.ts +72 -0
- package/dist/bvhBuilder.d.ts.map +1 -0
- package/dist/bvhBuilder.js +126 -0
- package/dist/bvhBuilder.js.map +1 -0
- package/dist/bvhWorker.d.ts +2 -0
- package/dist/bvhWorker.d.ts.map +1 -0
- package/dist/bvhWorker.js +8 -0
- package/dist/bvhWorker.js.map +1 -0
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +128 -0
- package/dist/geometry.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/renderer.d.ts +13 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +18 -10
- package/dist/renderer.js.map +1 -1
- package/package.json +2 -1
- package/src/bvh.ts +167 -103
- package/src/bvhBuilder.ts +178 -0
- package/src/bvhWorker.ts +13 -0
- package/src/geometry.ts +168 -0
- package/src/index.ts +3 -0
- package/src/renderer.ts +40 -12
package/src/geometry.ts
CHANGED
|
@@ -351,6 +351,76 @@ export function packGeometry(
|
|
|
351
351
|
triangleMaterials.push(entry.index);
|
|
352
352
|
};
|
|
353
353
|
|
|
354
|
+
/** Every vertex of a plain mesh, transformed once and shared by its groups. */
|
|
355
|
+
let plain: PlainVertices | null = null;
|
|
356
|
+
|
|
357
|
+
const writePlainTriangles = (mesh: SerializedMesh, start: number, end: number, mirrored: boolean, entry: Entry) => {
|
|
358
|
+
if (plain?.mesh !== mesh) plain = plainVertices(mesh);
|
|
359
|
+
|
|
360
|
+
const { world, normal, hasNormal, uv, color, valid } = plain;
|
|
361
|
+
const index = mesh.index;
|
|
362
|
+
const stride = entry.placement.varyingStride;
|
|
363
|
+
const corners = [0, 0, 0];
|
|
364
|
+
|
|
365
|
+
for (let v = start; v + 2 < end; v += 3) {
|
|
366
|
+
const a = index ? index[v] : v, b = index ? index[v + 1] : v + 1, c = index ? index[v + 2] : v + 2;
|
|
367
|
+
|
|
368
|
+
if (!valid(a) || !valid(b) || !valid(c)) continue;
|
|
369
|
+
|
|
370
|
+
corners[0] = a;
|
|
371
|
+
corners[1] = mirrored ? c : b;
|
|
372
|
+
corners[2] = mirrored ? b : c;
|
|
373
|
+
|
|
374
|
+
// Geometry without normals is shaded flat: the three corners share the face normal.
|
|
375
|
+
const flat = !hasNormal[a] || !hasNormal[b] || !hasNormal[c]
|
|
376
|
+
? faceNormal(
|
|
377
|
+
Array.from(world.subarray(corners[0] * 3, corners[0] * 3 + 3)),
|
|
378
|
+
Array.from(world.subarray(corners[1] * 3, corners[1] * 3 + 3)),
|
|
379
|
+
Array.from(world.subarray(corners[2] * 3, corners[2] * 3 + 3)))
|
|
380
|
+
: null;
|
|
381
|
+
|
|
382
|
+
positions.reserve(9);
|
|
383
|
+
normals.reserve(9);
|
|
384
|
+
uvs.reserve(12);
|
|
385
|
+
|
|
386
|
+
if (anyVertexColors) colors.reserve(12);
|
|
387
|
+
|
|
388
|
+
for (const corner of corners) {
|
|
389
|
+
const p = corner * 3, q = corner * 4;
|
|
390
|
+
|
|
391
|
+
positions.data[positions.length++] = world[p];
|
|
392
|
+
positions.data[positions.length++] = world[p + 1];
|
|
393
|
+
positions.data[positions.length++] = world[p + 2];
|
|
394
|
+
|
|
395
|
+
if (flat) {
|
|
396
|
+
normals.data[normals.length++] = flat[0];
|
|
397
|
+
normals.data[normals.length++] = flat[1];
|
|
398
|
+
normals.data[normals.length++] = flat[2];
|
|
399
|
+
} else {
|
|
400
|
+
normals.data[normals.length++] = normal[p];
|
|
401
|
+
normals.data[normals.length++] = normal[p + 1];
|
|
402
|
+
normals.data[normals.length++] = normal[p + 2];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
uvs.data[uvs.length++] = uv[q];
|
|
406
|
+
uvs.data[uvs.length++] = uv[q + 1];
|
|
407
|
+
uvs.data[uvs.length++] = uv[q + 2];
|
|
408
|
+
uvs.data[uvs.length++] = uv[q + 3];
|
|
409
|
+
|
|
410
|
+
if (anyVertexColors) {
|
|
411
|
+
colors.data[colors.length++] = color[q];
|
|
412
|
+
colors.data[colors.length++] = color[q + 1];
|
|
413
|
+
colors.data[colors.length++] = color[q + 2];
|
|
414
|
+
colors.data[colors.length++] = color[q + 3];
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (stride > 0) varyings.pad(3 * stride);
|
|
419
|
+
|
|
420
|
+
triangleMaterials.push(entry.index);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
354
424
|
for (const mesh of meshes) {
|
|
355
425
|
const groups = mesh.groups.length
|
|
356
426
|
? mesh.groups
|
|
@@ -389,6 +459,13 @@ export function packGeometry(
|
|
|
389
459
|
// instance, reading the per-instance attributes at that instance.
|
|
390
460
|
const instancing = vertexShader ? instancedAttributes(mesh) : null;
|
|
391
461
|
|
|
462
|
+
// The common case, a plain mesh, skips the general reader: each vertex is transformed once, straight into
|
|
463
|
+
// typed arrays, and each triangle is copied from them. What it writes is exactly what the reader would.
|
|
464
|
+
if (!vertexShader && !material?.maps?.displacementMap) {
|
|
465
|
+
writePlainTriangles(mesh, start, end, mirrored, entry);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
392
469
|
for (let instance = 0; instance < (instancing?.count ?? 1); instance++) {
|
|
393
470
|
const reader = instancing ? new VertexReader(mesh, material, context, instancing.names, instance) : vertices;
|
|
394
471
|
|
|
@@ -459,6 +536,97 @@ export function packGeometry(
|
|
|
459
536
|
};
|
|
460
537
|
}
|
|
461
538
|
|
|
539
|
+
/** A plain mesh's vertices in world space, read lazily: only the ones some triangle uses are transformed. */
|
|
540
|
+
interface PlainVertices {
|
|
541
|
+
mesh: SerializedMesh;
|
|
542
|
+
world: Float32Array;
|
|
543
|
+
normal: Float32Array;
|
|
544
|
+
hasNormal: Uint8Array;
|
|
545
|
+
/** Four per vertex: the first UV set, then the second. */
|
|
546
|
+
uv: Float32Array;
|
|
547
|
+
/** Four per vertex, RGBA. */
|
|
548
|
+
color: Float32Array;
|
|
549
|
+
/** Whether the vertex exists and has a finite position, transforming it on first use. */
|
|
550
|
+
valid(index: number): boolean;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function plainVertices(mesh: SerializedMesh): PlainVertices {
|
|
554
|
+
const position = mesh.attributes.position;
|
|
555
|
+
const count = Math.floor(position.data.length / Math.max(1, position.itemSize));
|
|
556
|
+
const world = new Float32Array(count * 3);
|
|
557
|
+
const normal = new Float32Array(count * 3);
|
|
558
|
+
const hasNormal = new Uint8Array(count);
|
|
559
|
+
const uv = new Float32Array(count * 4);
|
|
560
|
+
const color = new Float32Array(count * 4);
|
|
561
|
+
// 0 not read yet, 1 usable, 2 refused.
|
|
562
|
+
const state = new Uint8Array(count);
|
|
563
|
+
const rotation = normalMatrix(mesh.matrixWorld);
|
|
564
|
+
const matrix = mesh.matrixWorld as number[];
|
|
565
|
+
const { normal: normals, uv: uv0, uv1, color: colors } = mesh.attributes;
|
|
566
|
+
|
|
567
|
+
/** Component `c` of a vertex, as the reader's `Array.from(subarray(...))[c]` would see it. */
|
|
568
|
+
const component = (attribute: SerializedMesh["attributes"][string], vertex: number, c: number): number | undefined => {
|
|
569
|
+
const at = vertex * attribute.itemSize + c;
|
|
570
|
+
|
|
571
|
+
return c < attribute.itemSize && at < attribute.data.length ? attribute.data[at] : undefined;
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const read = (vertex: number): boolean => {
|
|
575
|
+
const x = component(position, vertex, 0), y = component(position, vertex, 1), z = component(position, vertex, 2);
|
|
576
|
+
|
|
577
|
+
if (x === undefined || y === undefined || z === undefined) return false;
|
|
578
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return false;
|
|
579
|
+
|
|
580
|
+
transformPoint(matrix, x, y, z, world, vertex * 3);
|
|
581
|
+
|
|
582
|
+
if (normals) {
|
|
583
|
+
const nx = component(normals, vertex, 0), ny = component(normals, vertex, 1), nz = component(normals, vertex, 2);
|
|
584
|
+
|
|
585
|
+
if (nx !== undefined && ny !== undefined && nz !== undefined) {
|
|
586
|
+
transformNormal(rotation, nx, ny, nz, normal, vertex * 3);
|
|
587
|
+
hasNormal[vertex] = 1;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// A missing second set falls back to the first, and a missing component to zero, as the pushed stream would.
|
|
592
|
+
const second = uv1 ?? uv0;
|
|
593
|
+
|
|
594
|
+
uv[vertex * 4] = uv0 ? component(uv0, vertex, 0) ?? 0 : 0;
|
|
595
|
+
uv[vertex * 4 + 1] = uv0 ? component(uv0, vertex, 1) ?? 0 : 0;
|
|
596
|
+
uv[vertex * 4 + 2] = second ? component(second, vertex, 0) ?? 0 : 0;
|
|
597
|
+
uv[vertex * 4 + 3] = second ? component(second, vertex, 1) ?? 0 : 0;
|
|
598
|
+
|
|
599
|
+
if (colors) {
|
|
600
|
+
const available = Math.max(0, Math.min(colors.itemSize, colors.data.length - vertex * colors.itemSize));
|
|
601
|
+
|
|
602
|
+
color[vertex * 4] = component(colors, vertex, 0) ?? 1;
|
|
603
|
+
color[vertex * 4 + 1] = component(colors, vertex, 1) ?? 1;
|
|
604
|
+
color[vertex * 4 + 2] = component(colors, vertex, 2) ?? 1;
|
|
605
|
+
color[vertex * 4 + 3] = available >= 4 ? component(colors, vertex, 3) ?? 0 : 1;
|
|
606
|
+
} else {
|
|
607
|
+
color.fill(1, vertex * 4, vertex * 4 + 4);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return true;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
mesh,
|
|
615
|
+
world,
|
|
616
|
+
normal,
|
|
617
|
+
hasNormal,
|
|
618
|
+
uv,
|
|
619
|
+
color,
|
|
620
|
+
valid(vertex: number) {
|
|
621
|
+
if (!(vertex >= 0 && vertex < count)) return false;
|
|
622
|
+
|
|
623
|
+
if (state[vertex] === 0) state[vertex] = read(vertex) ? 1 : 2;
|
|
624
|
+
|
|
625
|
+
return state[vertex] === 1;
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
462
630
|
interface VertexContext {
|
|
463
631
|
camera?: CameraMatrices;
|
|
464
632
|
width: number;
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,9 @@ export { packGeometry } from "./geometry.js";
|
|
|
28
28
|
export type { PackedGeometry } from "./geometry.js";
|
|
29
29
|
export { buildBvh, MAX_BVH_DEPTH } from "./bvh.js";
|
|
30
30
|
export type { PackedBvh } from "./bvh.js";
|
|
31
|
+
// The BVH worker: what `createBvhWorker` has to return, and the worker this package starts by default.
|
|
32
|
+
export { BVH_WORKER_MIN_TRIANGLES, createDefaultBvhWorker, respondToBvhRequest } from "./bvhBuilder.js";
|
|
33
|
+
export type { BvhRequest, BvhResponse, BvhWorker, BvhWorkerFactory } from "./bvhBuilder.js";
|
|
31
34
|
export { sceneByteLength } from "./sceneUpload.js";
|
|
32
35
|
export type { SceneByteLength } from "./sceneUpload.js";
|
|
33
36
|
export { LIGHT_KIND, packLights } from "./lights.js";
|
package/src/renderer.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PerspectiveCamera, type LightProbe, type Material, type Object3D, type Scene } from "three";
|
|
2
2
|
import { Accumulator } from "./accumulator.js";
|
|
3
|
-
import {
|
|
3
|
+
import { type PackedBvh } from "./bvh.js";
|
|
4
|
+
import { BvhBuilder, createDefaultBvhWorker, type BvhWorkerFactory } from "./bvhBuilder.js";
|
|
4
5
|
import { cameraMatrices, type CameraMatrices } from "./camera.js";
|
|
5
6
|
import { requestRenderDevice, type RenderDevice } from "./device.js";
|
|
6
7
|
import { loadEnvironment, type PackedEnvironment } from "./environment.js";
|
|
@@ -85,6 +86,17 @@ export interface GpuRendererOptions {
|
|
|
85
86
|
pixelRatio?: number;
|
|
86
87
|
/** How the denoiser filters, when `settings.globalIllumination.denoise` turns it on. Merged over `DEFAULT_DENOISER`. */
|
|
87
88
|
denoiser?: Partial<DenoiserSettings>;
|
|
89
|
+
/**
|
|
90
|
+
* Builds the BVH of a large scene in a Web Worker, so the page keeps responding while it is split. The tree is the
|
|
91
|
+
* same either way. Defaults to true where Web Workers exist, or when `createBvhWorker` is given; false builds on the
|
|
92
|
+
* main thread. If the worker cannot start, the renderer warns once and builds on the main thread.
|
|
93
|
+
*/
|
|
94
|
+
bvhWorker?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Starts the BVH worker. Replace it when the bundler cannot follow `new URL("./bvhWorker.js", import.meta.url)`, or
|
|
97
|
+
* outside the browser, with anything shaped like a Web Worker that runs `dist/bvhWorker.js`.
|
|
98
|
+
*/
|
|
99
|
+
createBvhWorker?: BvhWorkerFactory;
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
interface PendingFrame {
|
|
@@ -213,9 +225,13 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
213
225
|
private revision = 0;
|
|
214
226
|
private frameIndex = 0;
|
|
215
227
|
private disposed = false;
|
|
228
|
+
private bvh: BvhBuilder;
|
|
216
229
|
|
|
217
230
|
constructor(options: GpuRendererOptions) {
|
|
218
231
|
super();
|
|
232
|
+
this.bvh = new BvhBuilder(
|
|
233
|
+
options.bvhWorker ?? (options.createBvhWorker !== undefined || typeof Worker !== "undefined"),
|
|
234
|
+
options.createBvhWorker ?? createDefaultBvhWorker);
|
|
219
235
|
this.width = Math.max(1, Math.floor(options.width));
|
|
220
236
|
this.height = Math.max(1, Math.floor(options.height));
|
|
221
237
|
this.pixelRatio = checkedPixelRatio(options.pixelRatio ?? 1);
|
|
@@ -443,6 +459,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
443
459
|
this.revision++;
|
|
444
460
|
this.resume();
|
|
445
461
|
this.abort("The renderer was disposed.");
|
|
462
|
+
this.bvh.dispose();
|
|
446
463
|
this.buffers?.destroy();
|
|
447
464
|
this.accumulator?.destroy();
|
|
448
465
|
this.denoisePass?.destroy();
|
|
@@ -504,7 +521,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
504
521
|
const assets = await this.assets;
|
|
505
522
|
|
|
506
523
|
this.check(revision);
|
|
507
|
-
this.prepare(gpu, assets, camera, width, height, environment);
|
|
524
|
+
await this.prepare(gpu, revision, assets, camera, width, height, environment);
|
|
508
525
|
this.output = { width, height, data: new Uint8ClampedArray(width * height * 4) };
|
|
509
526
|
this.emit("start", { width, height });
|
|
510
527
|
|
|
@@ -655,8 +672,9 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
655
672
|
}
|
|
656
673
|
|
|
657
674
|
/** Uploads what changed and makes sure the passes point at the current buffers. */
|
|
658
|
-
private prepare(
|
|
675
|
+
private async prepare(
|
|
659
676
|
gpu: RenderDevice,
|
|
677
|
+
revision: number,
|
|
660
678
|
assets: SceneAssets,
|
|
661
679
|
camera: CameraState,
|
|
662
680
|
width: number,
|
|
@@ -674,17 +692,21 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
674
692
|
if (this.sceneDirty || !this.buffers || moved) {
|
|
675
693
|
// Planar surfaces need their captures, from this camera, before the scene that samples them is packed.
|
|
676
694
|
const planar = planarCaptures(assets.scene, camera, width, height);
|
|
677
|
-
|
|
695
|
+
|
|
696
|
+
const packed = await this.packScene(
|
|
697
|
+
gpu, revision, planar?.scene ?? assets.scene, assets, matrices, width, height, environment);
|
|
678
698
|
|
|
679
699
|
this.packed = packed.packed;
|
|
680
700
|
this.buffers?.destroy();
|
|
681
701
|
this.buffers = packed.buffers;
|
|
682
702
|
this.target = packed;
|
|
683
703
|
this.sceneDirty = false;
|
|
684
|
-
this.packedView = view;
|
|
685
704
|
this.planar = !!planar;
|
|
686
705
|
|
|
687
|
-
if (planar) this.renderCaptures(gpu, planar, assets, environment);
|
|
706
|
+
if (planar) await this.renderCaptures(gpu, revision, planar, assets, environment);
|
|
707
|
+
|
|
708
|
+
// Only once the captures are in: a frame replaced while they were taken has to take them again.
|
|
709
|
+
this.packedView = view;
|
|
688
710
|
}
|
|
689
711
|
|
|
690
712
|
const resized = !this.accumulator || this.accumulator.resize(width, height);
|
|
@@ -708,14 +730,15 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
708
730
|
}
|
|
709
731
|
|
|
710
732
|
/** Packs and uploads one scene: its textures, geometry, hierarchy, lights and every extra word they point at. */
|
|
711
|
-
private packScene(
|
|
733
|
+
private async packScene(
|
|
712
734
|
gpu: RenderDevice,
|
|
735
|
+
revision: number,
|
|
713
736
|
scene: SerializedScene,
|
|
714
737
|
assets: SceneAssets,
|
|
715
738
|
matrices: CameraMatrices,
|
|
716
739
|
width: number,
|
|
717
740
|
height: number,
|
|
718
|
-
environment: PackedEnvironment | null): PackedTarget {
|
|
741
|
+
environment: PackedEnvironment | null): Promise<PackedTarget> {
|
|
719
742
|
// The textures are planned first: a material refers to one by the index it lands on in that table, and to its
|
|
720
743
|
// extra words by where they land after the texels. The environment rides along in the same buffer.
|
|
721
744
|
const plan = planTextures(scene, environment ? [environment.texture] : []);
|
|
@@ -763,7 +786,12 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
763
786
|
const volumetric = packVolumetricLights(scene.lights);
|
|
764
787
|
const volumetricData = append(new Uint32Array(volumetric.buffer));
|
|
765
788
|
const textures = writeTextures(plan, new Uint32Array(extra));
|
|
766
|
-
const
|
|
789
|
+
const bvh = await this.bvh.build(geometry.positions, geometry.triangleCount);
|
|
790
|
+
|
|
791
|
+
// Nothing is on the device yet, so a frame replaced while the hierarchy was built has nothing to give back.
|
|
792
|
+
this.check(revision);
|
|
793
|
+
|
|
794
|
+
const packed = { geometry, bvh, textures };
|
|
767
795
|
const buffers = uploadScene(gpu.device, geometry, packed.bvh, textures, lights, environment, cdf);
|
|
768
796
|
|
|
769
797
|
if (buffers.bytes.largest > gpu.maxStorageBufferBindingSize) {
|
|
@@ -785,8 +813,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
785
813
|
* the texels of its placeholder in the main scene's texture buffer. Everything is queued ahead of the frame itself,
|
|
786
814
|
* which the queue then runs in order.
|
|
787
815
|
*/
|
|
788
|
-
private renderCaptures(
|
|
789
|
-
|
|
816
|
+
private async renderCaptures(gpu: RenderDevice, revision: number, planar: PlanarCaptures, assets: SceneAssets,
|
|
817
|
+
environment: PackedEnvironment | null) {
|
|
790
818
|
const main = this.target as PackedTarget;
|
|
791
819
|
const pathTrace = this.pathTrace as PathTracePass;
|
|
792
820
|
const { width, height } = planar;
|
|
@@ -801,7 +829,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
801
829
|
let target = scenes.get(capture.scene);
|
|
802
830
|
|
|
803
831
|
if (!target) {
|
|
804
|
-
target = this.packScene(gpu, capture.scene === "scene" ? planar.captureScene : empty, assets,
|
|
832
|
+
target = await this.packScene(gpu, revision, capture.scene === "scene" ? planar.captureScene : empty, assets,
|
|
805
833
|
cameraMatrices(capture.camera, width, height), width, height, environment);
|
|
806
834
|
scenes.set(capture.scene, target);
|
|
807
835
|
}
|