@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/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 { buildBvh, type PackedBvh } from "./bvh.js";
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
- const packed = this.packScene(gpu, planar?.scene ?? assets.scene, assets, matrices, width, height, environment);
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 packed = { geometry, bvh: buildBvh(geometry.positions, geometry.triangleCount), textures };
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
- gpu: RenderDevice, planar: PlanarCaptures, assets: SceneAssets, environment: PackedEnvironment | null) {
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
  }