mirror of
https://github.com/storytold/spark.git
synced 2026-10-17 00:12:59 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,656 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
import type { GsplatGenerator } from "./SplatGenerator";
|
||||
import { type SplatFileType, unpackSplats } from "./SplatLoader";
|
||||
import { SPLAT_TEX_HEIGHT, SPLAT_TEX_WIDTH } from "./defines";
|
||||
import {
|
||||
DynoProgram,
|
||||
DynoProgramTemplate,
|
||||
DynoUniform,
|
||||
dynoBlock,
|
||||
outputPackedSplat,
|
||||
} from "./dyno";
|
||||
import { TPackedSplats, definePackedSplats } from "./dyno/splats";
|
||||
import computeUvec4Template from "./shaders/computeUvec4.glsl";
|
||||
import { getTextureSize, setPackedSplat, unpackSplat } from "./utils";
|
||||
|
||||
// Initialize a PackedSplats collection from source data via
|
||||
// url, fileBytes, or packedArray. Creates an empty array if none are set,
|
||||
// and splat data can be constructed using pushSplat()/setSplat(). The maximum
|
||||
// splat size allocation will grow automatically, starting from maxSplats.
|
||||
export type PackedSplatsOptions = {
|
||||
// URL to fetch a Gaussian splat file from (supports .ply, .splat, .ksplat,
|
||||
// .spz formats). (default: undefined)
|
||||
url?: string;
|
||||
// Raw bytes of a Gaussian splat file to decode directly instead of fetching
|
||||
// from URL. (default: undefined)
|
||||
fileBytes?: Uint8Array | ArrayBuffer;
|
||||
// Override the file type detection for formats that can't be reliably
|
||||
// auto-detected (.splat, .ksplat). (default: undefined auto-detects other
|
||||
// formats from file contents)
|
||||
fileType?: SplatFileType;
|
||||
// Reserve space for at least this many splats when constructing the collection
|
||||
// initially. The array will automatically resize past maxSplats so setting it is
|
||||
// an optional optimization. (default: 0)
|
||||
maxSplats?: number;
|
||||
// Use provided packed data array, where each 4 consecutive uint32 values
|
||||
// encode one "packed" Gsplat. (default: undefined)
|
||||
packedArray?: Uint32Array;
|
||||
// Override number of splats in packed array to use only a subset.
|
||||
// (default: length of packed array / 4)
|
||||
numSplats?: number;
|
||||
// Callback function to programmatically create splats at initialization.
|
||||
// (default: undefined)
|
||||
construct?: (splats: PackedSplats) => Promise<void> | void;
|
||||
// Additional splat data, such as spherical harmonics components (sh1, sh2, sh3). (default: {})
|
||||
extra?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
// A PackedSplats is a collection of Gaussian splats, packed into a format that
|
||||
// takes exactly 16 bytes per Gsplat to maximize memory and cache efficiency.
|
||||
// The center xyz coordinates are encoded as float16 (3 x 2 bytes), scale xyz
|
||||
// as 3 x uint8 that encode a log scale from e^-9 to e^9, rgba as 4 x uint8,
|
||||
// and quaternion encoded via axis+angle using 2 x uint8 for octahedral encoding
|
||||
// of the axis direction and a uint8 to encode rotation amount from 0..Pi.
|
||||
|
||||
export class PackedSplats {
|
||||
maxSplats = 0;
|
||||
numSplats = 0;
|
||||
packedArray: Uint32Array | null = null;
|
||||
extra: Record<string, unknown>;
|
||||
|
||||
initialized: Promise<PackedSplats>;
|
||||
isInitialized = false;
|
||||
|
||||
// Either target or source will be non-null, depending on whether the PackedSplats
|
||||
// is being used as a data source or generated to.
|
||||
target: THREE.WebGLArrayRenderTarget | null = null;
|
||||
source: THREE.DataArrayTexture | null = null;
|
||||
// Set to true if source packedArray is updated to have it upload to GPU
|
||||
needsUpdate = true;
|
||||
|
||||
// A PackedSplats can be used in a dyno graph using the below property dyno:
|
||||
// const gsplat = dyno.readPackedSplats(this.dyno, dynoIndex);
|
||||
dyno: DynoUniform<typeof TPackedSplats, "packedSplats">;
|
||||
|
||||
constructor(options: PackedSplatsOptions = {}) {
|
||||
this.extra = {};
|
||||
this.dyno = new DynoPackedSplats({ packedSplats: this });
|
||||
|
||||
// The following line will be overridden by reinitialize()
|
||||
this.initialized = Promise.resolve(this);
|
||||
this.reinitialize(options);
|
||||
}
|
||||
|
||||
reinitialize(options: PackedSplatsOptions) {
|
||||
this.isInitialized = false;
|
||||
if (options.url || options.fileBytes || options.construct) {
|
||||
// We need to initialize asynchronously given the options
|
||||
this.initialized = this.asyncInitialize(options).then(() => {
|
||||
this.isInitialized = true;
|
||||
return this;
|
||||
});
|
||||
} else {
|
||||
this.initialize(options);
|
||||
this.isInitialized = true;
|
||||
this.initialized = Promise.resolve(this);
|
||||
}
|
||||
}
|
||||
|
||||
initialize(options: PackedSplatsOptions) {
|
||||
if (options.packedArray) {
|
||||
this.packedArray = options.packedArray;
|
||||
// Calculate number of horizontal texture rows that could fit in array.
|
||||
// A properly initialized packedArray should already take into account the
|
||||
// width and height of the texture and be rounded up with padding.
|
||||
this.maxSplats = Math.floor(this.packedArray.length / 4);
|
||||
this.maxSplats =
|
||||
Math.floor(this.maxSplats / SPLAT_TEX_WIDTH) * SPLAT_TEX_WIDTH;
|
||||
this.numSplats = Math.min(
|
||||
this.maxSplats,
|
||||
options.numSplats ?? Number.POSITIVE_INFINITY,
|
||||
);
|
||||
} else {
|
||||
this.maxSplats = options.maxSplats ?? 0;
|
||||
this.numSplats = 0;
|
||||
}
|
||||
this.extra = options.extra ?? {};
|
||||
}
|
||||
|
||||
async asyncInitialize(options: PackedSplatsOptions) {
|
||||
let { url, fileBytes, construct } = options;
|
||||
if (url) {
|
||||
fileBytes = await fetch(url).then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`${response.status} "${response.statusText}" fetching URL: ${url}`,
|
||||
);
|
||||
}
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return arrayBuffer;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileBytes) {
|
||||
const unpacked = await unpackSplats({
|
||||
input: fileBytes,
|
||||
fileType: options.fileType,
|
||||
pathOrUrl: url,
|
||||
});
|
||||
this.initialize(unpacked);
|
||||
}
|
||||
if (construct) {
|
||||
const maybePromise = construct(this);
|
||||
// If construct returns a promise, wait for it to complete
|
||||
if (maybePromise instanceof Promise) {
|
||||
await maybePromise;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call this when you are finished with the PackedSplats and want to free
|
||||
// any buffers it holds.
|
||||
dispose() {
|
||||
if (this.target) {
|
||||
this.target.dispose();
|
||||
this.target = null;
|
||||
}
|
||||
if (this.source) {
|
||||
this.source.dispose();
|
||||
this.source = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensures that this.packedArray can fit numSplats Gsplats. If it's too small,
|
||||
// resize exponentially and copy over the original data.
|
||||
//
|
||||
// Typically you don't need to call this, because calling this.setSplat(index, ...)
|
||||
// and this.pushSplat(...) will automatically call ensureSplats() so we have
|
||||
// enough splats.
|
||||
ensureSplats(numSplats: number): Uint32Array {
|
||||
const targetSize =
|
||||
numSplats <= this.maxSplats
|
||||
? this.maxSplats
|
||||
: // Grow exponentially to avoid frequent reallocations
|
||||
Math.max(numSplats, 2 * this.maxSplats);
|
||||
const currentSize = !this.packedArray ? 0 : this.packedArray.length / 4;
|
||||
|
||||
if (!this.packedArray || targetSize > currentSize) {
|
||||
this.maxSplats = getTextureSize(targetSize).maxSplats;
|
||||
const newArray = new Uint32Array(this.maxSplats * 4);
|
||||
if (this.packedArray) {
|
||||
// Copy over existing data
|
||||
newArray.set(this.packedArray);
|
||||
}
|
||||
this.packedArray = newArray;
|
||||
}
|
||||
return this.packedArray;
|
||||
}
|
||||
|
||||
// Ensure the extra array for the given level is large enough to hold numSplats
|
||||
ensureSplatsSh(level: number, numSplats: number): Uint32Array {
|
||||
let wordsPerSplat: number;
|
||||
let key: string;
|
||||
if (level === 0) {
|
||||
return this.ensureSplats(numSplats);
|
||||
}
|
||||
if (level === 1) {
|
||||
// 3 x 3 uint7 = 63 bits = 2 uint32
|
||||
wordsPerSplat = 2;
|
||||
key = "sh1";
|
||||
} else if (level === 2) {
|
||||
// 5 x 3 uint8 = 120 bits = 4 uint32
|
||||
wordsPerSplat = 4;
|
||||
key = "sh2";
|
||||
} else if (level === 3) {
|
||||
// 7 x 3 uint6 = 126 bits = 4 uint32
|
||||
wordsPerSplat = 4;
|
||||
key = "sh3";
|
||||
} else {
|
||||
throw new Error(`Invalid level: ${level}`);
|
||||
}
|
||||
|
||||
// Figure out our current and desired maxSplats
|
||||
let maxSplats: number = !this.extra[key]
|
||||
? 0
|
||||
: (this.extra[key] as Uint32Array).length / wordsPerSplat;
|
||||
const targetSize =
|
||||
numSplats <= maxSplats ? maxSplats : Math.max(numSplats, 2 * maxSplats);
|
||||
|
||||
if (!this.extra[key] || targetSize > maxSplats) {
|
||||
// Reallocate the array
|
||||
maxSplats = getTextureSize(targetSize).maxSplats;
|
||||
const newArray = new Uint32Array(maxSplats * wordsPerSplat);
|
||||
if (this.extra[key]) {
|
||||
// Copy over existing data
|
||||
newArray.set(this.extra[key] as Uint32Array);
|
||||
}
|
||||
this.extra[key] = newArray;
|
||||
}
|
||||
return this.extra[key] as Uint32Array;
|
||||
}
|
||||
|
||||
// Unpack the 16-byte Gsplat data at index into the Three.js components
|
||||
// center: THREE.Vector3, scales: THREE.Vector3, quaternion: THREE.Quaternion,
|
||||
// opacity: number 0..1, color: THREE.Color 0..1.
|
||||
getSplat(index: number): {
|
||||
center: THREE.Vector3;
|
||||
scales: THREE.Vector3;
|
||||
quaternion: THREE.Quaternion;
|
||||
opacity: number;
|
||||
color: THREE.Color;
|
||||
} {
|
||||
if (!this.packedArray || index >= this.numSplats) {
|
||||
throw new Error("Invalid index");
|
||||
}
|
||||
return unpackSplat(this.packedArray, index);
|
||||
}
|
||||
|
||||
// Set all PackedSplat components at index with the provided Gsplat attributes
|
||||
// (can be the same objects returned by getSplat). Ensures there is capacity
|
||||
// for at least index+1 Gsplats.
|
||||
setSplat(
|
||||
index: number,
|
||||
center: THREE.Vector3,
|
||||
scales: THREE.Vector3,
|
||||
quaternion: THREE.Quaternion,
|
||||
opacity: number,
|
||||
color: THREE.Color,
|
||||
) {
|
||||
const packedSplats = this.ensureSplats(index + 1);
|
||||
setPackedSplat(
|
||||
packedSplats,
|
||||
index,
|
||||
center.x,
|
||||
center.y,
|
||||
center.z,
|
||||
scales.x,
|
||||
scales.y,
|
||||
scales.z,
|
||||
quaternion.x,
|
||||
quaternion.y,
|
||||
quaternion.z,
|
||||
quaternion.w,
|
||||
opacity,
|
||||
color.r,
|
||||
color.g,
|
||||
color.b,
|
||||
);
|
||||
this.numSplats = Math.max(this.numSplats, index + 1);
|
||||
}
|
||||
|
||||
// Effectively calls this.setSplat(this.numSplats++, center, ...), useful on
|
||||
// construction where you just want to iterate and create a collection of Gsplats.
|
||||
pushSplat(
|
||||
center: THREE.Vector3,
|
||||
scales: THREE.Vector3,
|
||||
quaternion: THREE.Quaternion,
|
||||
opacity: number,
|
||||
color: THREE.Color,
|
||||
) {
|
||||
const packedSplats = this.ensureSplats(this.numSplats + 1);
|
||||
setPackedSplat(
|
||||
packedSplats,
|
||||
this.numSplats,
|
||||
center.x,
|
||||
center.y,
|
||||
center.z,
|
||||
scales.x,
|
||||
scales.y,
|
||||
scales.z,
|
||||
quaternion.x,
|
||||
quaternion.y,
|
||||
quaternion.z,
|
||||
quaternion.w,
|
||||
opacity,
|
||||
color.r,
|
||||
color.g,
|
||||
color.b,
|
||||
);
|
||||
++this.numSplats;
|
||||
}
|
||||
|
||||
// Iterate over Gsplats index 0..=(this.numSplats-1), unpack each Gsplat
|
||||
// and invoke the callback function with the Gsplat attributes.
|
||||
forEachSplat(
|
||||
callback: (
|
||||
index: number,
|
||||
center: THREE.Vector3,
|
||||
scales: THREE.Vector3,
|
||||
quaternion: THREE.Quaternion,
|
||||
opacity: number,
|
||||
color: THREE.Color,
|
||||
) => void,
|
||||
) {
|
||||
if (!this.packedArray || !this.numSplats) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.numSplats; ++i) {
|
||||
const unpacked = unpackSplat(this.packedArray, i);
|
||||
callback(
|
||||
i,
|
||||
unpacked.center,
|
||||
unpacked.scales,
|
||||
unpacked.quaternion,
|
||||
unpacked.opacity,
|
||||
unpacked.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensures our PackedSplats.target render target has enough space to generate
|
||||
// maxSplats total Gsplats, and reallocate if not large enough.
|
||||
ensureGenerate(maxSplats: number): boolean {
|
||||
if (this.target && (maxSplats ?? 1) <= this.maxSplats) {
|
||||
return false;
|
||||
}
|
||||
this.dispose();
|
||||
|
||||
const textureSize = getTextureSize(maxSplats ?? 1);
|
||||
const { width, height, depth } = textureSize;
|
||||
this.maxSplats = textureSize.maxSplats;
|
||||
|
||||
// The packed Gsplats are stored in a 2D array texture of max size
|
||||
// 2048 x 2048 x 2048, one RGBA32UI pixel = 4 uint32 = one Gsplat
|
||||
this.target = new THREE.WebGLArrayRenderTarget(width, height, depth, {
|
||||
depthBuffer: false,
|
||||
stencilBuffer: false,
|
||||
generateMipmaps: false,
|
||||
magFilter: THREE.NearestFilter,
|
||||
minFilter: THREE.NearestFilter,
|
||||
});
|
||||
this.target.texture.format = THREE.RGBAIntegerFormat;
|
||||
this.target.texture.type = THREE.UnsignedIntType;
|
||||
this.target.texture.internalFormat = "RGBA32UI";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Given an array of splatCounts (.numSplats for each
|
||||
// SplatGenerator/SplatMesh in the scene), compute a
|
||||
// "mapping layout" in the composite array of generated outputs.
|
||||
generateMapping(splatCounts: number[]): {
|
||||
maxSplats: number;
|
||||
mapping: { base: number; count: number }[];
|
||||
} {
|
||||
let maxSplats = 0;
|
||||
const mapping = splatCounts.map((numSplats) => {
|
||||
const base = maxSplats;
|
||||
// Generation happens in horizonal row chunks, so round up to full width
|
||||
const rounded = Math.ceil(numSplats / SPLAT_TEX_WIDTH) * SPLAT_TEX_WIDTH;
|
||||
maxSplats += rounded;
|
||||
return { base, count: numSplats };
|
||||
});
|
||||
return { maxSplats, mapping };
|
||||
}
|
||||
|
||||
// Returns a THREE.DataArrayTexture representing the PackedSplats content as
|
||||
// a Uint32x4 data array texture (2048 x 2048 x depth in size)
|
||||
getTexture(): THREE.DataArrayTexture {
|
||||
if (this.target) {
|
||||
// Return the render target's texture
|
||||
return this.target.texture;
|
||||
}
|
||||
if (this.source || this.packedArray) {
|
||||
// Update source texture if needed and return
|
||||
const source = this.maybeUpdateSource();
|
||||
return source;
|
||||
}
|
||||
|
||||
return PackedSplats.getEmpty();
|
||||
}
|
||||
|
||||
// Check if source texture needs to be created/updated
|
||||
private maybeUpdateSource(): THREE.DataArrayTexture {
|
||||
if (!this.packedArray) {
|
||||
throw new Error("No packed splats");
|
||||
}
|
||||
|
||||
if (this.needsUpdate || !this.source) {
|
||||
this.needsUpdate = false;
|
||||
|
||||
if (this.source) {
|
||||
const { width, height, depth } = this.source.image;
|
||||
if (this.maxSplats !== width * height * depth) {
|
||||
// The existing source texture isn't the right size, so dispose it
|
||||
this.source.dispose();
|
||||
this.source = null;
|
||||
}
|
||||
}
|
||||
if (!this.source) {
|
||||
// Allocate a new source texture of the right size
|
||||
const { width, height, depth } = getTextureSize(this.maxSplats);
|
||||
this.source = new THREE.DataArrayTexture(
|
||||
this.packedArray,
|
||||
width,
|
||||
height,
|
||||
depth,
|
||||
);
|
||||
this.source.format = THREE.RGBAIntegerFormat;
|
||||
this.source.type = THREE.UnsignedIntType;
|
||||
this.source.internalFormat = "RGBA32UI";
|
||||
this.source.needsUpdate = true;
|
||||
} else if (this.packedArray.buffer !== this.source.image.data.buffer) {
|
||||
// The source texture is the right size, update the data
|
||||
this.source.image.data = new Uint8Array(this.packedArray.buffer);
|
||||
}
|
||||
// Indicate to Three.js that the source texture needs to be uploaded to the GPU
|
||||
this.source.needsUpdate = true;
|
||||
}
|
||||
return this.source;
|
||||
}
|
||||
|
||||
private static emptySource: THREE.DataArrayTexture | null = null;
|
||||
|
||||
// Can be used where you need an uninitialized THREE.DataArrayTexture like
|
||||
// a uniform you will update with the result of this.getTexture() later.
|
||||
static getEmpty(): THREE.DataArrayTexture {
|
||||
if (!PackedSplats.emptySource) {
|
||||
const { width, height, depth, maxSplats } = getTextureSize(1);
|
||||
const emptyArray = new Uint32Array(maxSplats * 4);
|
||||
PackedSplats.emptySource = new THREE.DataArrayTexture(
|
||||
emptyArray,
|
||||
width,
|
||||
height,
|
||||
depth,
|
||||
);
|
||||
PackedSplats.emptySource.format = THREE.RGBAIntegerFormat;
|
||||
PackedSplats.emptySource.type = THREE.UnsignedIntType;
|
||||
PackedSplats.emptySource.internalFormat = "RGBA32UI";
|
||||
PackedSplats.emptySource.needsUpdate = true;
|
||||
}
|
||||
return PackedSplats.emptySource;
|
||||
}
|
||||
|
||||
// Get a program and THREE.RawShaderMaterial for a given GsplatGenerator,
|
||||
// generating it if necessary and caching the result.
|
||||
prepareProgramMaterial(generator: GsplatGenerator): {
|
||||
program: DynoProgram;
|
||||
material: THREE.RawShaderMaterial;
|
||||
} {
|
||||
let program = PackedSplats.generatorProgram.get(generator);
|
||||
if (!program) {
|
||||
// A Gsplat needs to be turned into a packed uvec4 for the dyno graph
|
||||
const graph = dynoBlock(
|
||||
{ index: "int" },
|
||||
{ output: "uvec4" },
|
||||
({ index }) => {
|
||||
generator.inputs.index = index;
|
||||
const gsplat = generator.outputs.gsplat;
|
||||
const output = outputPackedSplat(gsplat);
|
||||
return { output };
|
||||
},
|
||||
);
|
||||
if (!PackedSplats.programTemplate) {
|
||||
PackedSplats.programTemplate = new DynoProgramTemplate(
|
||||
computeUvec4Template,
|
||||
);
|
||||
}
|
||||
// Create a program from the template and graph
|
||||
program = new DynoProgram({
|
||||
graph,
|
||||
inputs: { index: "index" },
|
||||
outputs: { output: "target" },
|
||||
template: PackedSplats.programTemplate,
|
||||
});
|
||||
Object.assign(program.uniforms, {
|
||||
targetLayer: { value: 0 },
|
||||
targetBase: { value: 0 },
|
||||
targetCount: { value: 0 },
|
||||
});
|
||||
PackedSplats.generatorProgram.set(generator, program);
|
||||
}
|
||||
|
||||
// Prepare and update our material we'll use to render the Gsplats
|
||||
const material = program.prepareMaterial();
|
||||
PackedSplats.mesh.material = material;
|
||||
return { program, material };
|
||||
}
|
||||
|
||||
private saveRenderState(renderer: THREE.WebGLRenderer) {
|
||||
return {
|
||||
xrPresenting: renderer.xr.isPresenting,
|
||||
autoClear: renderer.autoClear,
|
||||
scissorTest: renderer.getScissorTest(),
|
||||
pixelRatio: renderer.getPixelRatio(),
|
||||
};
|
||||
}
|
||||
|
||||
private resetRenderState(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
state: {
|
||||
xrPresenting: boolean;
|
||||
autoClear: boolean;
|
||||
scissorTest: boolean;
|
||||
pixelRatio: number;
|
||||
},
|
||||
) {
|
||||
renderer.setRenderTarget(null);
|
||||
renderer.setPixelRatio(state.pixelRatio);
|
||||
renderer.xr.isPresenting = state.xrPresenting;
|
||||
renderer.autoClear = state.autoClear;
|
||||
renderer.setScissorTest(state.scissorTest);
|
||||
}
|
||||
|
||||
// Executes a dyno program specified by generator which is any DynoBlock that
|
||||
// maps { index: "int" } to { gsplat: Gsplat }. This is called in
|
||||
// ForgeRenderer.updateInternal() to re-generate Gsplats in the scene for
|
||||
// SplatGenerator instances whose version is newer than what was generated
|
||||
// for it last time.
|
||||
generate({
|
||||
generator,
|
||||
base,
|
||||
count,
|
||||
renderer,
|
||||
}: {
|
||||
generator: GsplatGenerator;
|
||||
base: number;
|
||||
count: number;
|
||||
renderer: THREE.WebGLRenderer;
|
||||
}): { nextBase: number } {
|
||||
if (!this.target) {
|
||||
throw new Error("Target must be initialized with ensureSplats");
|
||||
}
|
||||
if (base + count > this.maxSplats) {
|
||||
throw new Error("Base + count exceeds maxSplats");
|
||||
}
|
||||
|
||||
const { program, material } = this.prepareProgramMaterial(generator);
|
||||
program.update();
|
||||
|
||||
const renderState = this.saveRenderState(renderer);
|
||||
|
||||
// Generate the Gsplats in "layer" chunks, in horizontal row ranges,
|
||||
// that cover the total count of Gsplats.
|
||||
const nextBase =
|
||||
Math.ceil((base + count) / SPLAT_TEX_WIDTH) * SPLAT_TEX_WIDTH;
|
||||
const layerSize = SPLAT_TEX_WIDTH * SPLAT_TEX_HEIGHT;
|
||||
material.uniforms.targetBase.value = base;
|
||||
material.uniforms.targetCount.value = count;
|
||||
|
||||
// Keep generating layers until we've reached the next generation's base
|
||||
while (base < nextBase) {
|
||||
const layer = Math.floor(base / layerSize);
|
||||
material.uniforms.targetLayer.value = layer;
|
||||
|
||||
const layerBase = layer * layerSize;
|
||||
const layerYStart = Math.floor((base - layerBase) / SPLAT_TEX_WIDTH);
|
||||
const layerYEnd = Math.min(
|
||||
SPLAT_TEX_HEIGHT,
|
||||
Math.ceil((nextBase - layerBase) / SPLAT_TEX_WIDTH),
|
||||
);
|
||||
|
||||
// Render the desired portion of the layer
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setRenderTarget(this.target, layer);
|
||||
renderer.xr.isPresenting = false;
|
||||
renderer.autoClear = false;
|
||||
renderer.setScissorTest(true);
|
||||
renderer.setScissor(
|
||||
0,
|
||||
layerYStart,
|
||||
SPLAT_TEX_WIDTH,
|
||||
layerYEnd - layerYStart,
|
||||
);
|
||||
renderer.render(PackedSplats.scene, PackedSplats.camera);
|
||||
|
||||
base += SPLAT_TEX_WIDTH * (layerYEnd - layerYStart);
|
||||
}
|
||||
|
||||
this.resetRenderState(renderer, renderState);
|
||||
return { nextBase };
|
||||
}
|
||||
|
||||
static programTemplate: DynoProgramTemplate | null = null;
|
||||
|
||||
// Cache for GsplatGenerator programs
|
||||
static generatorProgram = new Map<GsplatGenerator, DynoProgram>();
|
||||
|
||||
// Static Three.js objects for pseudo-compute shader rendering
|
||||
static geometry = new THREE.PlaneGeometry(2, 2);
|
||||
static mesh = new THREE.Mesh(
|
||||
PackedSplats.geometry,
|
||||
new THREE.RawShaderMaterial({ visible: false }),
|
||||
);
|
||||
static scene = new THREE.Scene().add(PackedSplats.mesh);
|
||||
static camera = new THREE.Camera();
|
||||
}
|
||||
|
||||
// You can use a PackedSplats as a dyno block using the function
|
||||
// dyno.readPackedSplats(packedSplats.dyno, dynoIndex) where
|
||||
// dynoIndex is of type DynoVal<"int">. If you need to be able to change
|
||||
// the input PackedSplats dynamically, however, you should create a
|
||||
// DynoPackedSplats, whose property packedSplats you can change to any
|
||||
// PackedSplats and that will be used in the dyno shader program.
|
||||
|
||||
export const dynoPackedSplats = (packedSplats?: PackedSplats) =>
|
||||
new DynoPackedSplats({ packedSplats });
|
||||
|
||||
export class DynoPackedSplats extends DynoUniform<
|
||||
typeof TPackedSplats,
|
||||
"packedSplats",
|
||||
{
|
||||
texture: THREE.DataArrayTexture;
|
||||
numSplats: number;
|
||||
}
|
||||
> {
|
||||
packedSplats?: PackedSplats;
|
||||
|
||||
constructor({ packedSplats }: { packedSplats?: PackedSplats } = {}) {
|
||||
super({
|
||||
key: "packedSplats",
|
||||
type: TPackedSplats,
|
||||
globals: () => [definePackedSplats],
|
||||
value: {
|
||||
texture: PackedSplats.getEmpty(),
|
||||
numSplats: 0,
|
||||
},
|
||||
update: (value) => {
|
||||
value.texture =
|
||||
this.packedSplats?.getTexture() ?? PackedSplats.getEmpty();
|
||||
value.numSplats = this.packedSplats?.numSplats ?? 0;
|
||||
return value;
|
||||
},
|
||||
});
|
||||
this.packedSplats = packedSplats;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user