files.ts

examples/spark/js/files.ts · raw · open the example · all examples · site home

Exports: LoadedFileLoadOptionsloadSplatFilefileOfreleaseFilefileObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// Splat files on the GPU for the ports, with their spherical harmonics
// (Radix loads them into an extSh bundle and draws them seen from each
// view, as Spark's SplatMesh does):
//   loadSplatFile  a splat file on the GPU: { data, arrays, indices?,
//                  shPalette? } (indices: a LoD file's leaves, the original
//                  splats, as an index list; shPalette: RAD's clustered SH)
//   fileOf         a table on the GPU, as a LoadedFile
//   fileObject     a SplatObject drawing a loaded file, with optional
//                  object-space modifiers
import type * as THREE from "three";
import type { WebGPURenderer } from "three/webgpu";
import {
  GpuArrays, GpuShPalette, SplatCollection, SplatGenerator, Splats, loadSplatFile as readSplatFile,
  type AnySplatObject, type ArrayData, type ShPalette, type ShTarget, type SplatLayout, type SplatModifier, type SplatTarget,
} from "@sparkjsdev/radix";

// A splat file on the GPU
export interface LoadedFile {
  data: ArrayData;
  arrays: GpuArrays;
  // A LoD file's leaves: Radix loads LoD trees but does not choose nodes,
  // and drawing every node would stack merged parents over their children
  indices?: GpuArrays;
  // The palette of the splats' shLabel bundle
  shPalette?: GpuArrays;
}

export interface LoadOptions {
  layout?: SplatLayout;
  encoding?: SplatTarget["encoding"];
  onProgress?: (fraction: number, stage: string) => void;
  // Spherical harmonics: a codec (default "ext"), or false to skip them
  sh?: ShTarget;
}

export async function loadSplatFile(renderer: WebGPURenderer, url: string, { layout, encoding, onProgress, sh }: LoadOptions = {}): Promise<LoadedFile> {
  const { splats, shPalette } = await readSplatFile(url, { layout, encoding, onProgress, sh });
  return fileOf(renderer, splats, shPalette);
}

// A table (and the palette of its shLabel) on the GPU, as a LoadedFile
export function fileOf(renderer: WebGPURenderer, data: ArrayData, shPalette?: ShPalette): LoadedFile {
  const leaves = lodLeaves(data);
  return {
    data, arrays: GpuArrays.fromData(renderer, data), indices: leaves && SplatGenerator.indices(renderer, leaves),
    shPalette: shPalette && GpuShPalette.upload(renderer, shPalette),
  };
}

// Frees a LoadedFile's GPU arrays
export function releaseFile({ arrays, indices, shPalette }: LoadedFile) {
  [arrays, indices, shPalette].forEach((each) => each?.dispose());
}

// The leaves of a file's LoD tree (nodes without children), or undefined
// without a tree
function lodLeaves(data: ArrayData) {
  const tree = Splats.findLodTree(data);
  if (!tree) {
    return undefined;
  }
  const nodes = tree.channels.node.data;
  const leaves = new Uint32Array(data.count);
  let count = 0;
  for (let i = 0; i < data.count; i += 1) {
    if (nodes[i * 2] === 0) {
      leaves[count++] = i;
    }
  }
  return leaves.subarray(0, count);
}

// A SplatObject drawing a loaded file placed by `matrixWorld` (an Object3D's
// matrixWorld, say), with its SH and optional object-space modifiers; a
// file without LoD leaves or modifiers is plain SplatCollection.arrays
export function fileObject(file: LoadedFile, { matrixWorld, modifiers }: {
  matrixWorld?: THREE.Matrix4;
  modifiers?: SplatModifier[];
} = {}): AnySplatObject {
  const { arrays, indices, shPalette } = file;
  if (!indices && !modifiers) {
    return SplatCollection.arrays(arrays, { matrixWorld, shPalette });
  }
  const generator = SplatGenerator.forArrays(arrays, { remap: indices ? "indices" : undefined, modifiers, shPalette });
  return { generator, data: { splats: arrays, indices, shPalette }, count: indices ? indices.count : arrays.count, matrixWorld };
}