splatGenerator.ts

src/gpu/splatGenerator.ts · raw · all examples · site home

Exports: SplatGeneratorContextSplatBindContextSplatGeneratorAnySplatGeneratorSplatModifierCovSplatModifierStoredSplatsSplatShOptionsgeneratorContextmodifierContextSplatGenerator

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import * as THREE from "three";
import type { WebGPURenderer } from "three/webgpu";
import { ArrayData } from "../core/ArrayData";
import type { ShCodecName, ShDegree } from "../core/sh";
import { Splats, type BundleLike, type SplatLayout } from "../core/splats";
import { GpuArrayReader } from "./GpuArrayReader";
import { GpuArrays } from "./GpuArrays";
import { GpuShReader } from "./sh.tsl";
import { GpuSplatReader } from "./splatAccess";
import type { CovSplatNode, SplatNode } from "./splats.tsl";
import { TSL, type TslNode } from "./tsl";

const { uniform } = TSL;

// What Radix knows about the object being projected and the viewpoint, in
// one space: for a generator its splats' own (object) space, for a modifier
// the modifier's space. TSL nodes (uniforms set before each object's
// dispatch); more members may join later.
export interface SplatGeneratorContext {
  // The head (camera, or the eye pair's center) position
  viewOrigin: TslNode;
  // The head's viewing direction, unit
  viewForward: TslNode;
  // This space to head space and back (mat4)
  objectToHead: TslNode;
  headToObject: TslNode;
  // This space to world space (mat4)
  objectToWorld: TslNode;
}

// The same values for bind, as three.js objects (read them in bind; they
// change for the next object)
export interface SplatBindContext {
  viewOrigin: THREE.Vector3;
  viewForward: THREE.Vector3;
  objectToHead: THREE.Matrix4;
  headToObject: THREE.Matrix4;
  objectToWorld: THREE.Matrix4;
}

// Makes splat `index` in TSL: a program built once, whose inputs are slots
// (readers, uniforms) that bind(data, context) points at before each
// dispatch. It gives the forms it has; the pipeline converts to the other
// when it needs it (splat to cov is cheap, cov to splat an eigen
// decomposition). Generators set the index field. `context` tells it about
// the object and the viewpoint (SplatGeneratorContext; the projection
// always passes it, a wrapping generator may leave it out: it defaults to
// SplatGenerator.context). `Data` is what the data of its splat objects is
// (StoredSplats for fromSplats).
export type SplatGenerator<Data = unknown> =
  | {
    generateSplat(index: TslNode, context?: SplatGeneratorContext): SplatNode;
    generateCovSplat?(index: TslNode, context?: SplatGeneratorContext): CovSplatNode;
    bind?: (data: Data, context?: SplatBindContext) => void;
  }
  | {
    generateSplat?(index: TslNode, context?: SplatGeneratorContext): SplatNode;
    generateCovSplat(index: TslNode, context?: SplatGeneratorContext): CovSplatNode;
    bind?: (data: Data, context?: SplatBindContext) => void;
  };

// A generator of any data
export type AnySplatGenerator = SplatGenerator<never>;

// TSL functions of a splat in either form (fromSplats steps, a renderer's
// or view's modifier); `context` is in the modifier's space
export type SplatModifier = (splat: SplatNode, context?: SplatGeneratorContext) => SplatNode;
export type CovSplatModifier = (cov: CovSplatNode, context?: SplatGeneratorContext) => CovSplatNode;

// The data of a fromSplats generator's splat objects: the splat arrays, an
// index list (SplatGenerator.indices) with remap "indices", and the SH
// palette with sh palette mode
export interface StoredSplats {
  splats: GpuArrays;
  indices?: GpuArrays;
  shPalette?: GpuArrays;
}

// Spherical harmonics a fromSplats generator adds: a codec and degree, per
// splat (the codec's bundle in the splat arrays) or through a palette
// (bundle shLabel and StoredSplats.shPalette)
export interface SplatShOptions {
  codec: ShCodecName;
  degree: ShDegree;
  palette?: boolean;
}

// One set of context uniforms and their values
function makeContext() {
  const values: SplatBindContext = {
    viewOrigin: new THREE.Vector3(),
    viewForward: new THREE.Vector3(0, 0, -1),
    objectToHead: new THREE.Matrix4(),
    headToObject: new THREE.Matrix4(),
    objectToWorld: new THREE.Matrix4(),
  };
  const nodes: SplatGeneratorContext = {
    viewOrigin: uniform(values.viewOrigin),
    viewForward: uniform(values.viewForward),
    objectToHead: uniform(values.objectToHead),
    headToObject: uniform(values.headToObject),
    objectToWorld: uniform(values.objectToWorld),
  };
  // From this space's matrices to world and to head
  const set = (toWorld: THREE.Matrix4, toHead: THREE.Matrix4) => {
    values.objectToWorld.copy(toWorld);
    values.objectToHead.copy(toHead);
    values.headToObject.copy(toHead).invert();
    values.viewOrigin.setFromMatrixPosition(values.headToObject);
    values.viewForward.set(0, 0, -1).transformDirection(values.headToObject);
  };
  return { nodes, values, set };
}

// The generator's context (object space) and the modifier's (its space):
// render/ sets both before each object's dispatch (gpu/ cannot import it)
export const generatorContext = makeContext();
export const modifierContext = makeContext();

// Generators from templates (SplatGenerator is also the type)
export const SplatGenerator = {
  // The context nodes generators get, for code outside a generator call
  // (modifiers of fromSplats, tests)
  context: generatorContext.nodes,

  // A generator of stored splats of `layout`: splat i is the arrays' splat
  // remap(i) (entry i of an index list with "indices", or any TSL function
  // of the index; default i itself), its SH color added with `sh` (seen
  // from context.viewOrigin; GpuShReader), then each of `modifiers` in
  // turn. Its data is { splats, indices?, shPalette? }. Make it once: every
  // file of the layout and every index list then runs one program.
  fromSplats({ layout = "extSplats", remap, modifiers = [], sh }: {
    layout?: SplatLayout;
    remap?: "indices" | ((index: TslNode) => TslNode);
    modifiers?: SplatModifier[];
    sh?: SplatShOptions;
  } = {}): SplatGenerator<StoredSplats> {
    const reader = GpuSplatReader.for(layout);
    const list = remap === "indices" ? new GpuArrayReader(ArrayData.INDICES) : undefined;
    const shReader = sh ? new GpuShReader(sh) : undefined;
    const at = (i: TslNode) => (list ? list.load("index", i) : typeof remap === "function" ? remap(i) : i);
    const bind = ({ splats, indices, shPalette }: StoredSplats) => {
      reader.bind(splats);
      shReader?.bind(splats, shPalette);
      if (list) {
        if (!indices) {
          throw new Error("SplatGenerator.fromSplats: remap \"indices\" needs { splats, indices } as data");
        }
        list.bind(indices, "indices");
      }
    };
    const withSh = <N extends SplatNode | CovSplatNode>(load: (i: TslNode) => N) => (i: TslNode, context = generatorContext.nodes): N => {
      if (!shReader) {
        return load(at(i));
      }
      const stored = at(i).toVar();
      return shReader.apply(load(stored), stored, context.viewOrigin);
    };
    const loadSplat = withSh((i) => reader.loadSplat(i));
    if (modifiers.length === 0) {
      return { generateSplat: loadSplat, generateCovSplat: withSh((i) => reader.loadCovSplat(i)), bind };
    }
    return {
      generateSplat: (i, context = generatorContext.nodes) => modifiers.reduce((splat, modifier) => modifier(splat, context), loadSplat(i, context)),
      bind,
    };
  },

  // The generator for `arrays` (and `shPalette`): fromSplats of their splat
  // layout, with the SH they hold (per splat, or a palette with shPalette),
  // shared by every set of arrays of the same kind. `sh` caps the degree (0
  // or false: none); modifiers make a generator of its own.
  forArrays(arrays: GpuArrays, { remap, modifiers, sh, shPalette }: {
    remap?: "indices";
    modifiers?: SplatModifier[];
    sh?: number | false;
    shPalette?: GpuArrays;
  } = {}): SplatGenerator<StoredSplats> {
    const layout = Splats.findBundle(arrays.bundles as Record<string, BundleLike>)?.layout;
    if (!layout) {
      throw new Error("SplatGenerator.forArrays: no splat bundle in these arrays");
    }
    const found = sh === false || sh === 0 ? undefined : GpuShReader.find(arrays, shPalette);
    const shOptions = found && { codec: found.codec, degree: Math.min(sh || 4, found.degree) as ShDegree, palette: found.palette };
    if (modifiers) {
      return SplatGenerator.fromSplats({ layout, remap, modifiers, sh: shOptions });
    }
    const key = `${layout}|${remap ?? ""}|${shOptions ? `${shOptions.codec}|${shOptions.degree}|${shOptions.palette}` : ""}`;
    let generator = shared.get(key);
    if (!generator) {
      generator = SplatGenerator.fromSplats({ layout, remap, sh: shOptions });
      shared.set(key, generator);
    }
    return generator;
  },

  // An index list as GpuArrays (bundle "indices" of ArrayData.INDICES),
  // uploaded into `into` when given (its count becomes the list's)
  indices(renderer: WebGPURenderer, list: ArrayLike<number>, into?: GpuArrays): GpuArrays {
    const data = new ArrayData({ count: list.length }).addBundle("indices", ArrayData.INDICES);
    data.bundles.indices.channels.index.data.set(list);
    const arrays = into ?? new GpuArrays({ renderer, bundles: { indices: ArrayData.INDICES } });
    arrays.upload(data);
    return arrays;
  },

  // Sets the generator's context for an object placed by `objectToWorld`,
  // seen from `worldToHead`, and with `space` (a modifier's space: its
  // matrices to world and head) the modifier's (render/ calls this before
  // each object's dispatch)
  setContext(objectToWorld: THREE.Matrix4, worldToHead: THREE.Matrix4, space?: { toWorld: THREE.Matrix4; toHead: THREE.Matrix4 }) {
    generatorContext.set(objectToWorld, scratch.multiplyMatrices(worldToHead, objectToWorld));
    if (space) {
      modifierContext.set(space.toWorld, space.toHead);
    }
  },
};

const shared = new Map<string, SplatGenerator<StoredSplats>>();
const scratch = new THREE.Matrix4();