mirror of
https://github.com/storytold/spark.git
synced 2026-10-17 00:12:59 +00:00
Renamed "forge" to "spark". Updated meta-references and future domain. (#40)
This commit is contained in:
committed by
GitHub
parent
ea9e4e3da9
commit
acb541cc4e
@@ -0,0 +1,758 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
import { DynoPackedSplats } from "./PackedSplats";
|
||||
import { Readback } from "./Readback";
|
||||
import type { SparkRenderer } from "./SparkRenderer";
|
||||
import type { SplatAccumulator } from "./SplatAccumulator";
|
||||
import { SplatGeometry } from "./SplatGeometry";
|
||||
import {
|
||||
type DynoBlock,
|
||||
DynoBool,
|
||||
DynoFloat,
|
||||
type DynoVal,
|
||||
DynoVec3,
|
||||
Gsplat,
|
||||
add,
|
||||
combine,
|
||||
defineGsplat,
|
||||
dyno,
|
||||
dynoBlock,
|
||||
dynoConst,
|
||||
mul,
|
||||
packHalf2x16,
|
||||
readPackedSplat,
|
||||
uintToRgba8,
|
||||
unindent,
|
||||
unindentLines,
|
||||
} from "./dyno";
|
||||
import { withWorker } from "./splatWorker";
|
||||
import { FreeList, withinCoorientDist } from "./utils";
|
||||
|
||||
export type SparkViewpointOptions = {
|
||||
// Controls whether to auto-update its sort order whenever the SparkRenderer
|
||||
// updates the Gsplats. If you expect to render/display from this viewpoint
|
||||
// most frames, set this to true. (default: false)
|
||||
autoUpdate?: boolean;
|
||||
// Set a THREE.Camera for this viewpoint to follow. (default: undefined)
|
||||
camera?: THREE.Camera;
|
||||
// Set an explicit view-to-world transformation matrix for this viewpoint (equivalent
|
||||
// to camera.matrixWorld), overrides any camera setting. (default: undefined)
|
||||
viewToWorld?: THREE.Matrix4;
|
||||
// Configure viewpoint with an off-screen render target. (default: undefined)
|
||||
target?: {
|
||||
// Width of the render target in pixels.
|
||||
width: number;
|
||||
// Height of the render target in pixels.
|
||||
height: number;
|
||||
// If you want to be able to render a scene that depends on this target's
|
||||
// output (for example, a recursive viewport), set this to true to enable
|
||||
// double buffering. (default: false)
|
||||
doubleBuffer?: boolean;
|
||||
// Super-sampling factor for the render target. Values 1-4 are supported.
|
||||
// Note that re-sampling back down to .width x .height is done on the CPU
|
||||
// with simple averaging only when calling readTarget(). (default: 1)
|
||||
superXY?: number;
|
||||
};
|
||||
// Callback function that is called when the render target texture is updated.
|
||||
// Receives the texture as a parameter. Use this to update a viewport with
|
||||
// the latest viewpoint render each frame. (default: undefined)
|
||||
onTextureUpdated?: (texture: THREE.Texture) => void;
|
||||
// Whether to sort splats radially (geometric distance) from the viewpoint (true)
|
||||
// or by Z-depth (false). Most scenes are trained with the Z-depth sort metric
|
||||
// and will render more accurately at certain viewpoints. However, radial sorting
|
||||
// is more stable under viewpoint rotations. (default: true)
|
||||
sortRadial?: boolean;
|
||||
// Distance threshold for re-sorting splats. If the viewpoint moves more than
|
||||
// this distance, splats will be re-sorted. (default: 0.01 units)
|
||||
sortDistance?: number;
|
||||
// View direction dot product threshold for re-sorting splats. For
|
||||
// sortRadial: true we use 0.99 while sortRadial: false uses 0.999 because it is
|
||||
// more sensitive to view direction. (default: 0.99 if sortRadial else 0.999)
|
||||
sortCoorient?: boolean;
|
||||
// Constant added to Z-depth to bias values into the positive range for
|
||||
// sortRadial: false, but also used for culling Gsplats "well behind"
|
||||
// the viewpoint origin (default: 1.0)
|
||||
depthBias?: number;
|
||||
// Set this to true if rendering a 360 to disable "behind the viewpoint"
|
||||
// culling during sorting. This is set automatically when rendering 360 envMaps
|
||||
// using the SparkRenderer.renderEnvMap() utility function. (default: false)
|
||||
sort360?: boolean;
|
||||
};
|
||||
|
||||
// A SparkViewpoint is created from and tied to a SparkRenderer, and represents
|
||||
// an independent viewpoint of all the scene Gsplats and their sort order. Making
|
||||
// these viewpoints explicit allows us to have multiple, simultaneous viewpoint
|
||||
// renders, for example for camera preview panes or overhead map views.
|
||||
//
|
||||
// When creating a SparkRenderer it automatically creates a default viewpoint
|
||||
// .defaultView that is used in the normal render loop when drawing to the canvas,
|
||||
// and is automatically updated whenever the camera moves. Additional viewpoints
|
||||
// can be created and configured separately.
|
||||
|
||||
export class SparkViewpoint {
|
||||
spark: SparkRenderer;
|
||||
autoUpdate: boolean;
|
||||
camera?: THREE.Camera;
|
||||
viewToWorld: THREE.Matrix4;
|
||||
lastTime: number | null = null;
|
||||
|
||||
target?: THREE.WebGLRenderTarget;
|
||||
private back?: THREE.WebGLRenderTarget;
|
||||
onTextureUpdated?: (texture: THREE.Texture) => void;
|
||||
encodeLinear = false;
|
||||
superXY = 1;
|
||||
private superPixels?: Uint8Array;
|
||||
private pixels?: Uint8Array;
|
||||
|
||||
sortRadial: boolean;
|
||||
sortDistance?: number;
|
||||
sortCoorient?: boolean;
|
||||
depthBias?: number;
|
||||
sort360?: boolean;
|
||||
|
||||
display: {
|
||||
accumulator: SplatAccumulator;
|
||||
viewToWorld: THREE.Matrix4;
|
||||
geometry: SplatGeometry;
|
||||
} | null = null;
|
||||
|
||||
private sorting: { viewToWorld: THREE.Matrix4 } | null = null;
|
||||
private pending: {
|
||||
accumulator?: SplatAccumulator;
|
||||
viewToWorld: THREE.Matrix4;
|
||||
displayed: boolean;
|
||||
} | null = null;
|
||||
private sortingCheck = false;
|
||||
|
||||
private readback: Uint16Array = new Uint16Array(0);
|
||||
private orderingFreelist: FreeList<Uint32Array, number>;
|
||||
|
||||
constructor(options: SparkViewpointOptions & { spark: SparkRenderer }) {
|
||||
this.spark = options.spark;
|
||||
this.camera = options.camera;
|
||||
this.viewToWorld = options.viewToWorld ?? new THREE.Matrix4();
|
||||
|
||||
if (options.target) {
|
||||
const { width, height, doubleBuffer } = options.target;
|
||||
const superXY = Math.max(1, Math.min(4, options.target.superXY ?? 1));
|
||||
this.superXY = superXY;
|
||||
if (width * superXY > 8192 || height * superXY > 8192) {
|
||||
throw new Error("Target size too large");
|
||||
}
|
||||
|
||||
this.target = new THREE.WebGLRenderTarget(
|
||||
width * superXY,
|
||||
height * superXY,
|
||||
{
|
||||
format: THREE.RGBAFormat,
|
||||
type: THREE.UnsignedByteType,
|
||||
colorSpace: THREE.SRGBColorSpace,
|
||||
},
|
||||
);
|
||||
if (doubleBuffer) {
|
||||
this.back = new THREE.WebGLRenderTarget(
|
||||
width * superXY,
|
||||
height * superXY,
|
||||
{
|
||||
format: THREE.RGBAFormat,
|
||||
type: THREE.UnsignedByteType,
|
||||
colorSpace: THREE.SRGBColorSpace,
|
||||
},
|
||||
);
|
||||
}
|
||||
this.encodeLinear = true;
|
||||
}
|
||||
this.onTextureUpdated = options.onTextureUpdated;
|
||||
|
||||
this.sortRadial = options.sortRadial ?? true;
|
||||
this.sortDistance = options.sortDistance;
|
||||
this.sortCoorient = options.sortCoorient;
|
||||
this.depthBias = options.depthBias;
|
||||
this.sort360 = options.sort360;
|
||||
|
||||
this.orderingFreelist = new FreeList({
|
||||
allocate: (maxSplats) => new Uint32Array(maxSplats),
|
||||
valid: (ordering, maxSplats) => ordering.length === maxSplats,
|
||||
});
|
||||
|
||||
this.autoUpdate = false;
|
||||
this.setAutoUpdate(options.autoUpdate ?? false);
|
||||
}
|
||||
|
||||
// Call this when you are done with the SparkViewpoint and want to
|
||||
// free up its resources (GPU targets, pixel buffers, etc.)
|
||||
dispose() {
|
||||
this.setAutoUpdate(false);
|
||||
if (this.target) {
|
||||
this.target.dispose();
|
||||
this.target = undefined;
|
||||
}
|
||||
if (this.back) {
|
||||
this.back.dispose();
|
||||
this.back = undefined;
|
||||
}
|
||||
if (this.display) {
|
||||
this.spark.releaseAccumulator(this.display.accumulator);
|
||||
this.display.geometry.dispose();
|
||||
this.display = null;
|
||||
}
|
||||
if (this.pending?.accumulator) {
|
||||
this.spark.releaseAccumulator(this.pending.accumulator);
|
||||
this.pending = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Use this function to change whether this viewpoint will auto-update
|
||||
// its sort order whenever the attached SparkRenderer updates the Gsplats.
|
||||
// Turn this on or off depending on whether you expect to do renders from
|
||||
// this viewpoint most frames.
|
||||
setAutoUpdate(autoUpdate: boolean) {
|
||||
if (!this.autoUpdate && autoUpdate) {
|
||||
this.spark.autoViewpoints.push(this);
|
||||
} else if (this.autoUpdate && !autoUpdate) {
|
||||
this.spark.autoViewpoints = this.spark.autoViewpoints.filter(
|
||||
(v) => v !== this,
|
||||
);
|
||||
}
|
||||
this.autoUpdate = autoUpdate;
|
||||
}
|
||||
|
||||
// See below async prepareRenderPixels() for explanation of parameters.
|
||||
// Awaiting this method updates the Gsplats in the scene and performs a sort of the
|
||||
// Gsplats from this viewpoint, preparing it for a subsequent this.renderTarget()
|
||||
// call in the same tick.
|
||||
async prepare({
|
||||
scene,
|
||||
camera,
|
||||
viewToWorld,
|
||||
update,
|
||||
forceOrigin,
|
||||
}: {
|
||||
scene: THREE.Scene;
|
||||
camera?: THREE.Camera;
|
||||
viewToWorld?: THREE.Matrix4;
|
||||
update?: boolean;
|
||||
forceOrigin?: boolean;
|
||||
}) {
|
||||
if (viewToWorld) {
|
||||
this.viewToWorld = viewToWorld;
|
||||
} else {
|
||||
this.camera = camera ?? this.camera;
|
||||
if (this.camera) {
|
||||
this.camera.updateMatrixWorld();
|
||||
this.viewToWorld = this.camera.matrixWorld.clone();
|
||||
}
|
||||
}
|
||||
while (update ?? true) {
|
||||
// Force an update, possibly with origin centered at this camera
|
||||
// to yield the best quality output.
|
||||
const originToWorld = forceOrigin ? this.viewToWorld : undefined;
|
||||
const updated = this.spark.updateInternal({ scene, originToWorld });
|
||||
if (updated) {
|
||||
break;
|
||||
}
|
||||
// A bit of a hack, but try again. We shouldn't be starved for long.
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
const accumulator = this.spark.active;
|
||||
if (accumulator !== this.display?.accumulator) {
|
||||
this.spark.active.refCount += 1;
|
||||
}
|
||||
await this.sortUpdate({ accumulator, viewToWorld: this.viewToWorld });
|
||||
}
|
||||
|
||||
// Render out the viewpoint to the view target RGBA buffer.
|
||||
// Swaps buffers if doubleBuffer: true was set.
|
||||
// Calls onTextureUpdated(texture) with the resulting texture.
|
||||
renderTarget({
|
||||
scene,
|
||||
camera,
|
||||
}: { scene: THREE.Scene; camera?: THREE.Camera }) {
|
||||
const target = this.back ?? this.target;
|
||||
if (!target) {
|
||||
throw new Error("Must initialize SparkViewpoint with target");
|
||||
}
|
||||
|
||||
camera = camera ?? this.camera;
|
||||
if (!camera) {
|
||||
throw new Error("Must provide camera");
|
||||
}
|
||||
if (camera instanceof THREE.PerspectiveCamera) {
|
||||
const newCam = new THREE.PerspectiveCamera().copy(camera, false);
|
||||
newCam.aspect = target.width / target.height;
|
||||
newCam.updateProjectionMatrix();
|
||||
camera = newCam;
|
||||
}
|
||||
this.viewToWorld = camera.matrixWorld.clone();
|
||||
|
||||
try {
|
||||
this.spark.renderer.setRenderTarget(target);
|
||||
this.spark.prepareViewpoint(this);
|
||||
|
||||
this.spark.renderer.render(scene, camera);
|
||||
} finally {
|
||||
this.spark.prepareViewpoint(this.spark.defaultView);
|
||||
this.spark.renderer.setRenderTarget(null);
|
||||
}
|
||||
|
||||
if (target !== this.target) {
|
||||
// Swap back buffer and target
|
||||
[this.target, this.back] = [this.back, this.target];
|
||||
}
|
||||
this.onTextureUpdated?.(target.texture);
|
||||
}
|
||||
|
||||
// Read back the previously rendered target image as a Uint8Array of packed
|
||||
// RGBA values (in that order). If superXY was set greater than 1 then
|
||||
// downsampling is performed in the target pixel array with simple averaging
|
||||
// to derive the returned pixel values. Subsequent calls to this.readTarget()
|
||||
// will reuse the same buffers to minimize memory allocations.
|
||||
async readTarget(): Promise<Uint8Array> {
|
||||
if (!this.target) {
|
||||
throw new Error("Must initialize SparkViewpoint with target");
|
||||
}
|
||||
const { width, height } = this.target;
|
||||
const byteSize = width * height * 4;
|
||||
if (!this.superPixels || this.superPixels.length < byteSize) {
|
||||
this.superPixels = new Uint8Array(byteSize);
|
||||
}
|
||||
await this.spark.renderer.readRenderTargetPixelsAsync(
|
||||
this.target,
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height,
|
||||
this.superPixels,
|
||||
);
|
||||
|
||||
const { superXY } = this;
|
||||
if (superXY === 1) {
|
||||
return this.superPixels;
|
||||
}
|
||||
|
||||
const subWidth = width / superXY;
|
||||
const subHeight = height / superXY;
|
||||
const subSize = subWidth * subHeight * 4;
|
||||
if (!this.pixels || this.pixels.length < subSize) {
|
||||
this.pixels = new Uint8Array(subSize);
|
||||
}
|
||||
|
||||
const { superPixels, pixels } = this;
|
||||
const super2 = superXY * superXY;
|
||||
for (let y = 0; y < subHeight; y++) {
|
||||
const row = y * subWidth;
|
||||
for (let x = 0; x < subWidth; x++) {
|
||||
const superCol = x * superXY;
|
||||
let r = 0;
|
||||
let g = 0;
|
||||
let b = 0;
|
||||
let a = 0;
|
||||
for (let sy = 0; sy < superXY; sy++) {
|
||||
const superRow = (y * superXY + sy) * this.target.width;
|
||||
for (let sx = 0; sx < superXY; sx++) {
|
||||
const superIndex = (superRow + superCol + sx) * 4;
|
||||
r += superPixels[superIndex];
|
||||
g += superPixels[superIndex + 1];
|
||||
b += superPixels[superIndex + 2];
|
||||
a += superPixels[superIndex + 3];
|
||||
}
|
||||
}
|
||||
const pixelIndex = (row + x) * 4;
|
||||
pixels[pixelIndex] = r / super2;
|
||||
pixels[pixelIndex + 1] = g / super2;
|
||||
pixels[pixelIndex + 2] = b / super2;
|
||||
pixels[pixelIndex + 3] = a / super2;
|
||||
}
|
||||
}
|
||||
return pixels;
|
||||
}
|
||||
|
||||
// Render out a viewpoint as a Uint8Array of RGBA values for the provided scene
|
||||
// and any camera/viewToWorld viewpoint overrides. By default update is true,
|
||||
// which triggers its SparkRenderer to check and potentially update the Gsplats.
|
||||
// Setting update to false disables this and sorts the Gsplats as they are.
|
||||
// Setting forceOrigin (default: false) to true forces the view update to
|
||||
// recalculate the splats with this view origin, potentially altering any
|
||||
// view-dependent effects. If you expect view-dependent effects to play a role
|
||||
// in the rendering quality, enable this.
|
||||
//
|
||||
// Underneath, prepareRenderPixels() simply calls await this.prepare(...),
|
||||
// this.renderTarget(...), and finally returns the result this.readTarget(),
|
||||
// a Promise to a Uint8Array with RGBA values for all the pixels (potentially
|
||||
// downsampled if the superXY parameter was used). These steps can also be called
|
||||
// manually, for example if you need to alter the scene before and after
|
||||
// this.renderTarget(...) to hide UI elements from being rendered.
|
||||
async prepareRenderPixels({
|
||||
scene,
|
||||
camera,
|
||||
viewToWorld,
|
||||
update,
|
||||
forceOrigin,
|
||||
}: {
|
||||
scene: THREE.Scene;
|
||||
camera?: THREE.Camera;
|
||||
viewToWorld?: THREE.Matrix4;
|
||||
update?: boolean;
|
||||
forceOrigin?: boolean;
|
||||
}) {
|
||||
await this.prepare({ scene, camera, viewToWorld, update, forceOrigin });
|
||||
this.renderTarget({ scene, camera });
|
||||
return this.readTarget();
|
||||
}
|
||||
|
||||
// This is called automatically by SparkRenderer, there is no need to call it!
|
||||
// The method cannot be private because then SparkRenderer would
|
||||
// not be able to call it.
|
||||
autoPoll({ accumulator }: { accumulator?: SplatAccumulator }) {
|
||||
if (this.camera) {
|
||||
this.camera.updateMatrixWorld();
|
||||
this.viewToWorld = this.camera.matrixWorld.clone();
|
||||
}
|
||||
|
||||
let needsSort = false;
|
||||
let displayed = false;
|
||||
|
||||
if (!this.display) {
|
||||
// Need to do first sort
|
||||
needsSort = true;
|
||||
} else if (accumulator) {
|
||||
needsSort = true;
|
||||
const { mappingVersion } = this.display.accumulator;
|
||||
if (accumulator.mappingVersion === mappingVersion) {
|
||||
// Splat mapping has not changed, so reuse the existing sorted
|
||||
// geometry to show updates faster. We will still fire off
|
||||
// a re-sort if necessary. First release old accumulator.
|
||||
this.spark.releaseAccumulator(this.display.accumulator);
|
||||
this.display.accumulator = accumulator;
|
||||
displayed = true;
|
||||
}
|
||||
}
|
||||
|
||||
const latestView = this.sorting?.viewToWorld ?? this.display?.viewToWorld;
|
||||
if (
|
||||
latestView &&
|
||||
!withinCoorientDist({
|
||||
matrix1: this.viewToWorld,
|
||||
matrix2: latestView,
|
||||
// By default update sort each 1 cm
|
||||
maxDistance: this.sortDistance ?? 0.01,
|
||||
// By default for radial sort, update for intermittent movement so that
|
||||
// we bring back splats culled by being behind the camera.
|
||||
// For depth sort, small rotations can change sort order a lot, so
|
||||
// update sort for even small rotations.
|
||||
minCoorient: (this.sortCoorient ?? this.sortRadial) ? 0.99 : 0.999,
|
||||
})
|
||||
) {
|
||||
needsSort = true;
|
||||
}
|
||||
|
||||
if (!needsSort) {
|
||||
// Stop here, no sort necessary
|
||||
return;
|
||||
}
|
||||
|
||||
if (accumulator) {
|
||||
// Hold a reference to the accumulator so it isn't released
|
||||
accumulator.refCount += 1;
|
||||
}
|
||||
|
||||
if (
|
||||
accumulator &&
|
||||
this.pending?.accumulator &&
|
||||
this.pending.accumulator !== this.display?.accumulator
|
||||
) {
|
||||
this.spark.releaseAccumulator(this.pending.accumulator);
|
||||
}
|
||||
this.pending = { accumulator, viewToWorld: this.viewToWorld, displayed };
|
||||
|
||||
// Don't await this, just trigger the sort if necessary
|
||||
this.driveSort();
|
||||
}
|
||||
|
||||
private async driveSort() {
|
||||
while (true) {
|
||||
if (this.sorting || !this.pending) {
|
||||
return; // Sort already in process or nothing to sort
|
||||
}
|
||||
|
||||
const { viewToWorld, displayed } = this.pending;
|
||||
let accumulator = this.pending.accumulator ?? this.display?.accumulator;
|
||||
if (!accumulator) {
|
||||
accumulator = this.spark.active;
|
||||
accumulator.refCount += 1;
|
||||
}
|
||||
this.pending = null;
|
||||
if (!accumulator) {
|
||||
throw new Error("No accumulator to sort");
|
||||
}
|
||||
|
||||
this.sorting = { viewToWorld };
|
||||
await this.sortUpdate({ accumulator, viewToWorld, displayed });
|
||||
this.sorting = null;
|
||||
// Continue in loop with any queued sort
|
||||
}
|
||||
}
|
||||
|
||||
private async sortUpdate({
|
||||
accumulator,
|
||||
viewToWorld,
|
||||
displayed = false,
|
||||
}: {
|
||||
accumulator?: SplatAccumulator;
|
||||
viewToWorld: THREE.Matrix4;
|
||||
displayed?: boolean;
|
||||
}) {
|
||||
if (this.sortingCheck) {
|
||||
throw new Error("Only one sort at a time");
|
||||
}
|
||||
this.sortingCheck = true;
|
||||
|
||||
accumulator = accumulator ?? this.spark.active;
|
||||
const { numSplats, maxSplats } = accumulator.splats;
|
||||
let activeSplats = 0;
|
||||
let ordering = this.orderingFreelist.alloc(maxSplats);
|
||||
|
||||
if (numSplats > 0) {
|
||||
const {
|
||||
reader,
|
||||
doubleSortReader,
|
||||
dynoSortRadial,
|
||||
dynoOrigin,
|
||||
dynoDirection,
|
||||
dynoDepthBias,
|
||||
dynoSort360,
|
||||
dynoSplats,
|
||||
} = SparkViewpoint.makeSorter();
|
||||
const halfMaxSplats = Math.ceil(maxSplats / 2);
|
||||
this.readback = reader.ensureBuffer(halfMaxSplats, this.readback);
|
||||
|
||||
const worldToOrigin = accumulator.toWorld.clone().invert();
|
||||
const viewToOrigin = viewToWorld.clone().premultiply(worldToOrigin);
|
||||
|
||||
dynoSortRadial.value = this.sort360 ? true : this.sortRadial;
|
||||
dynoOrigin.value.set(0, 0, 0).applyMatrix4(viewToOrigin);
|
||||
dynoDirection.value
|
||||
.set(0, 0, -1)
|
||||
.applyMatrix4(viewToOrigin)
|
||||
.sub(dynoOrigin.value)
|
||||
.normalize();
|
||||
dynoDepthBias.value = this.depthBias ?? 1.0;
|
||||
dynoSort360.value = this.sort360 ?? false;
|
||||
dynoSplats.packedSplats = accumulator.splats;
|
||||
|
||||
await reader.renderReadback({
|
||||
renderer: this.spark.renderer,
|
||||
reader: doubleSortReader,
|
||||
count: Math.ceil(numSplats / 2),
|
||||
readback: this.readback,
|
||||
});
|
||||
|
||||
const result = (await withWorker(async (worker) => {
|
||||
return worker.call("sortDoubleSplats", {
|
||||
numSplats,
|
||||
readback: this.readback,
|
||||
ordering,
|
||||
});
|
||||
})) as {
|
||||
readback: Uint16Array;
|
||||
ordering: Uint32Array;
|
||||
activeSplats: number;
|
||||
};
|
||||
this.readback = result.readback;
|
||||
ordering = result.ordering;
|
||||
activeSplats = result.activeSplats;
|
||||
}
|
||||
|
||||
this.updateDisplay({
|
||||
accumulator,
|
||||
viewToWorld,
|
||||
ordering,
|
||||
activeSplats,
|
||||
displayed,
|
||||
});
|
||||
this.sortingCheck = false;
|
||||
}
|
||||
|
||||
private updateDisplay({
|
||||
accumulator,
|
||||
viewToWorld,
|
||||
ordering,
|
||||
activeSplats,
|
||||
displayed = false,
|
||||
}: {
|
||||
accumulator: SplatAccumulator;
|
||||
viewToWorld: THREE.Matrix4;
|
||||
ordering: Uint32Array;
|
||||
activeSplats: number;
|
||||
displayed?: boolean;
|
||||
}) {
|
||||
if (!this.display) {
|
||||
this.display = {
|
||||
accumulator,
|
||||
viewToWorld,
|
||||
geometry: new SplatGeometry(ordering, activeSplats),
|
||||
};
|
||||
} else {
|
||||
if (!displayed && accumulator !== this.display.accumulator) {
|
||||
this.spark.releaseAccumulator(this.display.accumulator);
|
||||
this.display.accumulator = accumulator;
|
||||
}
|
||||
|
||||
this.display.viewToWorld = viewToWorld;
|
||||
|
||||
const oldOrdering = this.display.geometry.ordering;
|
||||
if (oldOrdering.length === ordering.length) {
|
||||
this.display.geometry.update(ordering, activeSplats);
|
||||
} else {
|
||||
this.display.geometry.dispose();
|
||||
// console.log("*** alloc SplatGeometry", ordering.length);
|
||||
this.display.geometry = new SplatGeometry(ordering, activeSplats);
|
||||
}
|
||||
this.orderingFreelist.free(oldOrdering);
|
||||
}
|
||||
if (this.spark.viewpoint === this) {
|
||||
this.spark.prepareViewpoint(this);
|
||||
}
|
||||
}
|
||||
|
||||
// If you need an empty THREE.Texture to use to initialize a uniform that is
|
||||
// updated via onTextureUpdated(texture), this static texture can be handy.
|
||||
static EMPTY_TEXTURE = new THREE.Texture();
|
||||
|
||||
private static dynos: {
|
||||
dynoSortRadial: DynoBool<string>;
|
||||
dynoOrigin: DynoVec3<THREE.Vector3, "value">;
|
||||
dynoDirection: DynoVec3<THREE.Vector3, "value">;
|
||||
dynoDepthBias: DynoFloat<string>;
|
||||
dynoSort360: DynoBool<string>;
|
||||
dynoSplats: DynoPackedSplats;
|
||||
reader: Readback;
|
||||
doubleSortReader: DynoBlock<{ index: "int" }, { rgba8: "vec4" }>;
|
||||
} | null = null;
|
||||
|
||||
private static makeSorter() {
|
||||
if (!SparkViewpoint.dynos) {
|
||||
const dynoSortRadial = new DynoBool({ value: true });
|
||||
const dynoOrigin = new DynoVec3({ value: new THREE.Vector3() });
|
||||
const dynoDirection = new DynoVec3({ value: new THREE.Vector3() });
|
||||
const dynoDepthBias = new DynoFloat({ value: 1.0 });
|
||||
const dynoSort360 = new DynoBool({ value: false });
|
||||
const dynoSplats = new DynoPackedSplats();
|
||||
|
||||
const reader = new Readback();
|
||||
const doubleSortReader = dynoBlock(
|
||||
{ index: "int" },
|
||||
{ rgba8: "vec4" },
|
||||
({ index }) => {
|
||||
if (!index) {
|
||||
throw new Error("No index");
|
||||
}
|
||||
const sortParams = {
|
||||
sortRadial: dynoSortRadial,
|
||||
sortOrigin: dynoOrigin,
|
||||
sortDirection: dynoDirection,
|
||||
sortDepthBias: dynoDepthBias,
|
||||
sort360: dynoSort360,
|
||||
};
|
||||
const index2 = mul(index, dynoConst("int", 2));
|
||||
|
||||
const gsplat0 = readPackedSplat(dynoSplats, index2);
|
||||
const metric0 = computeSortMetric({ gsplat: gsplat0, ...sortParams });
|
||||
|
||||
const gsplat1 = readPackedSplat(
|
||||
dynoSplats,
|
||||
add(index2, dynoConst("int", 1)),
|
||||
);
|
||||
const metric1 = computeSortMetric({ gsplat: gsplat1, ...sortParams });
|
||||
|
||||
const combined = combine({
|
||||
vectorType: "vec2",
|
||||
x: metric0,
|
||||
y: metric1,
|
||||
});
|
||||
const rgba8 = uintToRgba8(packHalf2x16(combined));
|
||||
return { rgba8 };
|
||||
},
|
||||
);
|
||||
|
||||
SparkViewpoint.dynos = {
|
||||
dynoSortRadial,
|
||||
dynoOrigin,
|
||||
dynoDirection,
|
||||
dynoDepthBias,
|
||||
dynoSort360,
|
||||
dynoSplats,
|
||||
reader,
|
||||
doubleSortReader,
|
||||
};
|
||||
}
|
||||
return SparkViewpoint.dynos;
|
||||
}
|
||||
}
|
||||
|
||||
const defineComputeSortMetric = unindent(`
|
||||
float computeSort(Gsplat gsplat, bool sortRadial, vec3 sortOrigin, vec3 sortDirection, float sortDepthBias, bool sort360) {
|
||||
if (!isGsplatActive(gsplat.flags)) {
|
||||
return INFINITY;
|
||||
}
|
||||
|
||||
vec3 center = gsplat.center - sortOrigin;
|
||||
float biasedDepth = dot(center, sortDirection) + sortDepthBias;
|
||||
if (!sort360 && (biasedDepth <= 0.0)) {
|
||||
return INFINITY;
|
||||
}
|
||||
|
||||
return sortRadial ? length(center) : biasedDepth;
|
||||
}
|
||||
`);
|
||||
|
||||
function computeSortMetric({
|
||||
gsplat,
|
||||
sortRadial,
|
||||
sortOrigin,
|
||||
sortDirection,
|
||||
sortDepthBias,
|
||||
sort360,
|
||||
}: {
|
||||
gsplat: DynoVal<typeof Gsplat>;
|
||||
sortRadial: DynoVal<"bool">;
|
||||
sortOrigin: DynoVal<"vec3">;
|
||||
sortDirection: DynoVal<"vec3">;
|
||||
sortDepthBias: DynoVal<"float">;
|
||||
sort360: DynoVal<"bool">;
|
||||
}) {
|
||||
return dyno({
|
||||
inTypes: {
|
||||
gsplat: Gsplat,
|
||||
sortRadial: "bool",
|
||||
sortOrigin: "vec3",
|
||||
sortDirection: "vec3",
|
||||
sortDepthBias: "float",
|
||||
sort360: "bool",
|
||||
},
|
||||
outTypes: { metric: "float" },
|
||||
globals: () => [defineGsplat, defineComputeSortMetric],
|
||||
inputs: {
|
||||
gsplat,
|
||||
sortRadial,
|
||||
sortOrigin,
|
||||
sortDirection,
|
||||
sortDepthBias,
|
||||
sort360,
|
||||
},
|
||||
statements: ({ inputs, outputs }) => {
|
||||
const {
|
||||
gsplat,
|
||||
sortRadial,
|
||||
sortOrigin,
|
||||
sortDirection,
|
||||
sortDepthBias,
|
||||
sort360,
|
||||
} = inputs;
|
||||
return unindentLines(`
|
||||
${outputs.metric} = computeSort(${gsplat}, ${sortRadial}, ${sortOrigin}, ${sortDirection}, ${sortDepthBias}, ${sort360});
|
||||
`);
|
||||
},
|
||||
}).outputs.metric;
|
||||
}
|
||||
Reference in New Issue
Block a user