SplatProjection.ts

src/render/SplatProjection.ts · raw · all examples · site home

Exports: ProjectedSchemaProjectionOutputsKeyOutputsSplatProjection

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
// One projection method as an object: the words it writes per splat (head
// words, then each eye's, in uint32x4 channels w0, w1, ... with a uint32x2
// last one for 1 or 2 words), the TSL that projects one head-space splat
// node into them, the writer body that does it for every splat and writes
// its sort key, and the draw-side decoding of the words. Also the key-only
// writer the CPU sort reads (the same for every method).
import { defineBundle, type BundleDef } from "../core/bundles";
import type { ArrayFormat } from "../core/formats";
import { SortKeys } from "../core/sort";
import { GpuArrayReader } from "../gpu/GpuArrayReader";
import { GpuSortKeys } from "../gpu/sortKey.tsl";
import { TSL, type TslNode } from "../gpu/tsl";
import {
  cullInvisible, headFrame, inSortGuard, prepareSplat, splatInView, type PreparedSplat, type SplatForm, type SplatInView, type SplatNodeOf, type SplatVaryings,
} from "./projection.tsl";
import { PROJECTIONS, type ProjectionDef, type ProjectionInputs, type ProjectionMethod, type SplatLook } from "./projections.tsl";
import type { EyeNodes } from "./splatCamera";
import { SplatParams, type SplatParamNodes } from "./splatParams";

const { length, select, uint, uniform, uvec2, uvec4 } = TSL;

// The key width (16, 24 or 32), and whether splats culled in every eye get
// the invalid key (else only invisible ones and those outside
// params.sortGuard do): uniforms of every projection, set per dispatch
const sortBits = uniform(32, "uint");
const cullKeys = uniform(1, "uint");

// The bundles a projection writes: `projected` (its words) and `sortKeys`
export interface ProjectedSchema {
  [name: string]: BundleDef;
  projected: BundleDef;
  sortKeys: typeof SortKeys.KEYS;
}

// What a projection writer's write(index, out) fills
export interface ProjectionOutputs {
  projected: Record<string, TslNode>;
  sortKeys: { key: TslNode };
}

// The key-only writer's outputs
export interface KeyOutputs {
  sortKeys: { key: TslNode };
}

// Components of each word channel: uint32x4, and a uint32x2 last one when
// only 1 or 2 words remain
function wordChannels(words: number): number[] {
  const channels: number[] = [];
  for (let left = words; left > 0; left -= 4) {
    channels.push(left > 2 ? 4 : 2);
  }
  return channels;
}

// Inside params.sortGuard (always, when that is 0)
const guarded = (prepared: PreparedSplat, head: SplatInView, params: SplatParamNodes) =>
  params.sortGuard.lessThanEqual(0.0).or(inSortGuard(prepared, head, params.sortGuard, params));

const instances = new Map<ProjectionMethod, SplatProjection>();

export class SplatProjection<F extends SplatForm = SplatForm> {
  static readonly METHODS: readonly ProjectionMethod[] = ["ewa", "3dgut", "htgs", "raygs"];

  readonly method: ProjectionMethod;
  // The splat form project() takes: "cov" (ewa) or "splat"
  readonly input: F;
  readonly headWords: number;
  readonly eyeWords: number;
  // The varyings the draw's vertex stage fills and density() reads
  readonly varyings: readonly (keyof SplatVaryings)[];
  private def: ProjectionDef<F>;

  private constructor(method: ProjectionMethod, def: ProjectionDef<F>) {
    this.method = method;
    this.def = def;
    this.input = def.input;
    this.headWords = def.headWords;
    this.eyeWords = def.eyeWords;
    this.varyings = def.varyings;
  }

  // The projection of a method (one object per method)
  static get<M extends ProjectionMethod>(method: M): SplatProjection<ProjectionInputs[M]> {
    let projection = instances.get(method);
    if (!projection) {
      projection = new SplatProjection(method, PROJECTIONS[method] as ProjectionDef);
      instances.set(method, projection);
    }
    return projection as unknown as SplatProjection<ProjectionInputs[M]>;
  }

  // Words per splat for `eyes` eyes
  words(eyes: number) {
    return this.headWords + this.eyeWords * eyes;
  }

  // The `projected` bundle for `eyes`: channels w0, w1, ...
  bundle(eyes: number): BundleDef {
    const channels: Record<string, ArrayFormat> = {};
    wordChannels(this.words(eyes)).forEach((components, c) => {
      channels[`w${c}`] = components === 4 ? "uint32x4" : "uint32x2";
    });
    return defineBundle(channels, { serialize: false });
  }

  // What a projection writes: bundles `projected` and `sortKeys`
  outputs(eyes: number): ProjectedSchema {
    return { projected: this.bundle(eyes), sortKeys: SortKeys.KEYS };
  }

  // A reader of the projected bundle (bind it to arrays, bundle "projected")
  reader(eyes: number): GpuArrayReader {
    return new GpuArrayReader(this.bundle(eyes));
  }

  // All words of element `index`
  loadWords(reader: GpuArrayReader, eyes: number, index: TslNode): TslNode[] {
    const words: TslNode[] = [];
    wordChannels(this.words(eyes)).forEach((components, c) => {
      const texel = reader.load(`w${c}`, index).toVar();
      words.push(...["x", "y", "z", "w"].slice(0, components).map((k) => texel[k]));
    });
    return words.slice(0, this.words(eyes));
  }

  // The words of one head-space splat for the camera's eyes, whether any
  // eye shows it, whether it is invisible anyway (zero opacity or size),
  // whether it is inside params.sortGuard (always, when that is 0), and
  // its distance from the head (the sort distance)
  project(splat: SplatNodeOf<F>, eyes: number) {
    const params = SplatParams.nodes;
    const prepared = prepareSplat(splat, this.input, params);
    const head = splatInView(prepared, headFrame(), params);
    const { words, visible } = this.def.project({ splat: prepared, head, eyes, params });
    return { words, visible, invisible: cullInvisible(prepared, params), guarded: guarded(prepared, head, params), distance: length(head.center) };
  }

  // write(index, out) for a GpuArrayWriter / WgpuArrayWriter with outputs
  // outputs(eyes): projects head-space splat `splat(index)` and writes its key
  write(splat: (index: TslNode) => SplatNodeOf<F>, eyes: number) {
    return (index: TslNode, out: ProjectionOutputs) => {
      const { words, visible, invisible, guarded, distance } = this.project(splat(index), eyes);

      wordChannels(words.length).forEach((components, c) => {
        const w = (i: number) => words[c * 4 + i] ?? uint(0);
        out.projected[`w${c}`].assign(components === 4 ? uvec4(w(0), w(1), w(2), w(3)) : uvec2(w(0), w(1)));
      });

      const keyed = select(cullKeys.equal(uint(1)), visible, invisible.not().and(guarded));
      out.sortKeys.key.assign(GpuSortKeys.pack(select(keyed, GpuSortKeys.compute(distance, sortBits), GpuSortKeys.invalid(sortBits))));
    };
  }

  // The uniforms every projection reads: the key width, and whether keys
  // of splats culled in every eye are invalid
  static setKeys(bits: number, cull: boolean) {
    sortBits.value = bits;
    cullKeys.value = cull ? 1 : 0;
  }

  // write(index, out) for a writer with outputs { sortKeys: SortKeys.KEYS }:
  // only the key of head-space splat `splat(index)` of `form` (any method;
  // invisible splats and those outside params.sortGuard get the invalid one)
  static writeKeys<F extends SplatForm>(splat: (index: TslNode) => SplatNodeOf<F>, form: F) {
    return (index: TslNode, out: KeyOutputs) => {
      const params = SplatParams.nodes;
      const prepared = prepareSplat(splat(index), form, params);
      const head = splatInView(prepared, headFrame(), params);
      const keyed = cullInvisible(prepared, params).not().and(guarded(prepared, head, params));
      out.sortKeys.key.assign(GpuSortKeys.pack(select(keyed, GpuSortKeys.compute(length(head.center), sortBits), GpuSortKeys.invalid(sortBits))));
    };
  }

  // --- The draw's side ---

  // What one eye's draw decodes from the head words and that eye's
  look(head: TslNode[], eye: TslNode[]): SplatLook {
    return this.def.look(head, eye);
  }

  // Fills the draw's vertex struct `out` for a quad corner in [-1, 1]^2
  vertex(head: TslNode[], eye: TslNode[], view: EyeNodes, corner: TslNode, boundStdDev: TslNode, cutoffStdDev: TslNode, out: TslNode) {
    this.def.vertex(head, eye, view, corner, boundStdDev, cutoffStdDev, out);
  }

  // Squared Mahalanobis distance at a fragment
  density(varyings: SplatVaryings): TslNode {
    return this.def.density(varyings);
  }
}