SplatView.ts
src/render/SplatView.ts · raw · all examples · site home
Exports: SplatOrderAutoSortSplatViewOptionsViewSettingsSplatView
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 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
import * as THREE from "three";
import { ArrayData } from "../core/ArrayData";
import { SortKeys, type SortKeyBits } from "../core/sort";
import { isWebGPUBackend } from "../gpu/backend";
import { GpuArrays } from "../gpu/GpuArrays";
import type { CovSplatModifier, SplatModifier } from "../gpu/splatGenerator";
import { SplatCamera } from "./splatCamera";
import { SplatCollection } from "./SplatCollection";
import type { SplatObjectParams, SplatViewParams } from "./splatParams";
import { projectCollection, readSortKeys, type ProjectSettings } from "./SplatProjector";
import type { SplatRenderer } from "./SplatRenderer";
import type { ModifierSpace } from "./splatTransform.tsl";
// A draw order for a collection (SplatView.sort): `ordering` lists the
// first `visible` of its splats back to front; `version` counts the view's
// sorts, in the order they started
export interface SplatOrder {
readonly collection: SplatCollection;
readonly version: number;
readonly ordering: Uint32Array<ArrayBuffer>;
readonly visible: number;
// Wall ms of the keys (GPU pass and readback) and of the worker sort
readonly keysMs: number;
readonly sortMs: number;
}
// When autoSort re-sorts: the camera moved more than `distance` (world
// units) or, when a sortGuard is set, turned more than `angle` (radians)
// or changed its lens since the last sort started; any change of the
// collection or of an object's world transform also re-sorts, and so does
// view.requestSort()
export interface AutoSort {
distance: number;
angle: number;
}
// What a view sets over its renderer's settings (SplatRenderer.viewFor);
// a modifier of null draws without the renderer's
export interface SplatViewOptions {
collection?: SplatCollection;
params?: Partial<SplatViewParams>;
objectParams?: Partial<SplatObjectParams>;
modifier?: SplatModifier | null;
covModifier?: CovSplatModifier | null;
modifierSpace?: ModifierSpace;
sortBits?: SortKeyBits;
autoSort?: AutoSort | false;
}
// The settings a view resolves to: its own over its renderer's
export interface ViewSettings extends ProjectSettings {
collection: SplatCollection;
autoSort: AutoSort | false;
}
// One viewpoint of a SplatRenderer (renderer.viewFor(key), keyed by a
// camera or any value): settings over the renderer's, unset ones following
// it, and on the CPU-sort path the order being drawn. In sortMode "gpu"
// nothing here depends on the camera, so a renderer needs views only to
// draw some viewpoint differently. In "cpu" (the WebGL2 fallback) the
// renderer draws `order`, made by sort(camera) (keys on the GPU, read back
// and sorted on a worker) and shown with apply(order): until a newer order
// lands it keeps drawing the collection that order was sorted for, so every
// camera gets a view of its own (the renderer makes one per view key it
// renders). An order can be applied until one of a later sort lands or is
// applied; switching the renderer to "gpu" drops them all. autoSort does
// that loop: one sort in flight, re-sorted when the camera or the
// collection changed or a sort was requested, each order applied as it
// lands.
export class SplatView {
readonly splats: SplatRenderer;
readonly key: unknown;
// Over the renderer's, field by field
params: Partial<SplatViewParams> = {};
objectParams: Partial<SplatObjectParams> = {};
// Unset: the renderer's
modifier?: SplatModifier | null;
covModifier?: CovSplatModifier | null;
modifierSpace?: ModifierSpace;
sortBits?: SortKeyBits;
autoSort?: AutoSort | false;
private _collection?: SplatCollection;
private _order?: SplatOrder;
// The applied order's indices (ArrayData.INDICES)
private orderArrays?: GpuArrays;
private sorts = 0;
// In cpu mode, collections of sorts in flight and of the newest landed
// order not yet applied (`landed`), by version
private pending = new Map<number, SplatCollection>();
private landed = 0;
private keyArrays: GpuArrays[] = [];
private disposed = false;
private autoSorting = false;
private sortRequested = false;
private sortedFrom?: {
collection: SplatCollection; bits: SortKeyBits; position: THREE.Vector3; quaternion: THREE.Quaternion; projection: number[]; matrices: number[][];
};
/** @internal Made by SplatRenderer.viewFor */
constructor(splats: SplatRenderer, key: unknown) {
this.splats = splats;
this.key = key;
}
// The collection this view draws instead of its renderer's
get collection(): SplatCollection | undefined {
return this._collection;
}
set collection(collection: SplatCollection | undefined) {
this._collection = collection;
this.splats.updateLive();
}
// The applied order (cpu mode draws its collection)
get order(): SplatOrder | undefined {
return this._order;
}
// Sort again at the next render, for changes autoSort can't see (a
// generator's splats moved, a uniform of it changed), also with autoSort
// off. With a sort in flight, the next one starts when it finishes, since
// its sort keys may predate the change. Calls the renderer's onInvalidate
// unless a sort in flight will.
requestSort() {
this.wantSort();
if (!this.autoSorting) {
this.splats.onInvalidate?.(this);
}
}
// Sort this view's collection (as of this call) from `camera`: its keys
// are computed on the GPU now, then read back and sorted on a worker. In
// gpu mode only with the renderer's sortWorker, and its orders cannot be
// applied.
async sort(camera: THREE.Camera): Promise<SplatOrder> {
this.checkDisposed();
if (this.splats.sortMode === "gpu" && !this.splats.sortWorker) {
throw new Error("SplatView.sort: a CPU sort needs sortMode \"cpu\" or a sortWorker");
}
const settings = this.splats.settingsOf(this);
const { collection, sortBits: bits } = settings;
const version = (this.sorts += 1);
if (this.splats.sortMode === "cpu") {
this.pending.set(version, collection);
this.splats.updateLive();
}
try {
let ordering = new Uint32Array(0);
let visible = 0;
const timing = { keysMs: 0, sortMs: 0 };
if (collection.count > 0) {
const renderer = this.splats.renderer;
const keys = this.keyArrays.pop() ?? new GpuArrays({ renderer, bundles: { sortKeys: SortKeys.KEYS }, storage: isWebGPUBackend(renderer) });
const start = performance.now();
let read: Uint32Array<ArrayBuffer>;
try {
SplatCamera.set(renderer, camera);
projectCollection({ renderer, settings, collection, arrays: keys, pass: "keys" });
this.checkDisposed();
read = await readSortKeys(keys, collection.count);
} finally {
if (this.disposed) {
keys.dispose();
} else {
this.keyArrays.push(keys);
}
}
this.checkDisposed();
const sorting = performance.now();
({ ordering, visible } = await this.splats.sortKeys(read, bits));
this.checkDisposed();
timing.keysMs = sorting - start;
timing.sortMs = performance.now() - sorting;
}
this.land(version);
return { collection, version, ordering, visible, ...timing };
} catch (error) {
if (this.pending.delete(version)) {
this.splats.updateLive();
}
throw error;
}
}
// Draw `order` from now on (cpu mode); whether it was applied. Not once
// an order of a later sort has landed or been applied, nor in gpu mode.
apply(order: SplatOrder): boolean {
if (this.disposed || this.splats.sortMode !== "cpu" || !this.pending.has(order.version)) {
return false;
}
const data = new ArrayData({ count: order.visible }).addBundle("indices", ArrayData.INDICES, { data: { index: order.ordering } });
this.orderArrays ??= new GpuArrays({ renderer: this.splats.renderer, bundles: { indices: ArrayData.INDICES }, capacity: 1 });
this.orderArrays.upload(data);
this._order = order;
for (const version of [...this.pending.keys()]) {
if (version <= order.version) {
this.pending.delete(version);
}
}
this.landed = this.landed > order.version ? this.landed : 0;
this.splats.updateLive();
return true;
}
// Free the view's arrays and take it off its renderer (its orders no
// longer hold their objects)
dispose() {
if (this.disposed) {
return;
}
this.disposed = true;
this.orderArrays?.dispose();
this.keyArrays.forEach((keys) => keys.dispose());
this.keyArrays = [];
this._order = undefined;
this.pending.clear();
this.splats.forget(this);
}
/**
* @internal SplatRenderer's, before a render in cpu mode, after
* autoSort's step: the applied order's collection, with its indices
*/
drawn(camera: THREE.Camera): { collection: SplatCollection; arrays: GpuArrays; visible: number } | undefined {
this.autoSortStep(camera);
return this._order && this.orderArrays && { collection: this._order.collection, arrays: this.orderArrays, visible: this._order.visible };
}
/** @internal The collections this view holds live: its own, and in cpu mode its orders' */
collections(): SplatCollection[] {
const collections = this._collection ? [this._collection] : [];
if (this._order) {
collections.push(this._order.collection);
}
this.pending.forEach((collection) => collections.push(collection));
return collections;
}
/** @internal requestSort without onInvalidate (cpu mode only) */
wantSort() {
if (this.splats.sortMode === "cpu") {
this.sortRequested = true;
}
}
/** @internal The renderer switched to gpu: the orders go */
dropOrders() {
this._order = undefined;
this.pending.clear();
this.landed = 0;
this.sortRequested = false;
}
// An order of version `version` landed: older landed ones can no longer
// be applied, nor this one if a later one landed or was applied
private land(version: number) {
if (!this.pending.has(version)) {
return;
}
if (version < this.landed || (this._order && version <= this._order.version)) {
this.pending.delete(version);
} else {
this.pending.delete(this.landed);
this.landed = version;
}
this.splats.updateLive();
}
private checkDisposed() {
if (this.disposed) {
throw new Error("SplatView.sort: the view is disposed");
}
}
private autoSortStep(camera: THREE.Camera) {
if (this.autoSorting) {
return;
}
const settings = this.splats.settingsOf(this);
if (settings.collection.count === 0 || !(this.sortRequested || (settings.autoSort && this.needsSort(camera, settings, settings.autoSort)))) {
return;
}
this.sortRequested = false;
camera.updateMatrixWorld();
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
camera.matrixWorld.decompose(position, quaternion, new THREE.Vector3());
this.sortedFrom = {
collection: settings.collection, bits: settings.sortBits, position, quaternion,
projection: [...lensOf(camera).elements], matrices: matrices(settings.collection),
};
this.autoSorting = true;
this.sort(camera)
.then((order) => {
this.autoSorting = false;
if (this.apply(order)) {
this.splats.onInvalidate?.(this);
}
})
.catch((error: unknown) => {
this.autoSorting = false;
if (!this.disposed) {
console.error("SplatView autoSort:", error);
}
});
}
private needsSort(camera: THREE.Camera, { collection, sortBits, objectParams }: ViewSettings, auto: AutoSort) {
const from = this.sortedFrom;
if (!this._order || !from || from.collection !== collection || from.bits !== sortBits) {
return true;
}
camera.updateMatrixWorld();
const position = scratch.position;
const quaternion = scratch.quaternion;
camera.matrixWorld.decompose(position, quaternion, scratch.scale);
if (position.distanceTo(from.position) > auto.distance) {
return true;
}
const guarded = objectParams.sortGuard > 0 || collection.objects.some(({ object }) => (object.params?.sortGuard ?? 0) > 0);
if (guarded && (quaternion.angleTo(from.quaternion) > auto.angle || lensOf(camera).elements.some((value, k) => value !== from.projection[k]))) {
return true;
}
return collection.objects.some(({ object }, i) => SplatCollection.matrixWorldOf(object).elements.some((value, k) => value !== from.matrices[i][k]));
}
}
function matrices(collection: SplatCollection) {
return collection.objects.map(({ object }) => [...SplatCollection.matrixWorldOf(object).elements]);
}
// A camera's lens: its first eye's projection
function lensOf(camera: THREE.Camera): THREE.Matrix4 {
const array = camera as THREE.ArrayCamera;
return (array.isArrayCamera && array.cameras.length > 0 ? array.cameras[0] : camera).projectionMatrix;
}
const scratch = { position: new THREE.Vector3(), quaternion: new THREE.Quaternion(), scale: new THREE.Vector3() };