playcanvas
Version:
PlayCanvas WebGL game engine
707 lines (704 loc) • 31.7 kB
JavaScript
import { math } from '../../core/math/math.js';
import { Mat4 } from '../../core/math/mat4.js';
import { Vec3 } from '../../core/math/vec3.js';
import { GraphNode } from '../graph-node.js';
import { GSplatInfo } from './gsplat-info.js';
import { GSplatUnifiedSorter } from './gsplat-unified-sorter.js';
import { GSplatWorkBuffer } from './gsplat-work-buffer.js';
import { GSplatRenderer } from './gsplat-renderer.js';
import { GSplatOctreeInstance } from './gsplat-octree-instance.js';
import { GSplatOctreeResource } from './gsplat-octree.resource.js';
import { GSplatWorldState } from './gsplat-world-state.js';
import { Debug } from '../../core/debug.js';
import { BoundingBox } from '../../core/shape/bounding-box.js';
import { Color } from '../../core/math/color.js';
/**
* @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js'
* @import { GSplatPlacement } from './gsplat-placement.js'
* @import { Scene } from '../scene.js'
*/ const cameraPosition = new Vec3();
const cameraDirection = new Vec3();
const translation = new Vec3();
const invModelMat = new Mat4();
const tempNonOctreePlacements = new Set();
const tempOctreePlacements = new Set();
const _updatedSplats = [];
const _splatsNeedingColorUpdate = [];
const _cameraDeltas = {
rotationDelta: 0,
translationDelta: 0
};
const tempOctreesTicked = new Set();
const _lodColorsRaw = [
[
1,
0,
0
],
[
0,
1,
0
],
[
0,
0,
1
],
[
1,
1,
0
],
[
1,
0,
1
],
[
0,
1,
1
],
[
1,
0.5,
0
],
[
0.5,
0,
1
] // purple
];
// Color instances used by debug wireframe rendering
const _lodColors = [
new Color(1, 0, 0),
new Color(0, 1, 0),
new Color(0, 0, 1),
new Color(1, 1, 0),
new Color(1, 0, 1),
new Color(0, 1, 1),
new Color(1, 0.5, 0),
new Color(0.5, 0, 1)
];
let _randomColorRaw = null;
/**
* GSplatManager manages the rendering of splats using a work buffer, where all active splats are
* stored and rendered from.
*
* @ignore
*/ class GSplatManager {
constructor(device, director, layer, cameraNode){
/** @type {GraphNode} */ this.node = new GraphNode('GSplatManager');
/**
* A map of versioned world states, keyed by version.
*
* @type {Map<number, GSplatWorldState>}
*/ this.worldStates = new Map();
/**
* The version of the last world state.
*
* @type {number}
*/ this.lastWorldStateVersion = 0;
/** @type {number} */ this.sortedVersion = 0;
/** @type {number} */ this.framesTillFullUpdate = 0;
/** @type {Vec3} */ this.lastLodCameraPos = new Vec3(Infinity, Infinity, Infinity);
/** @type {Vec3} */ this.lastLodCameraFwd = new Vec3(Infinity, Infinity, Infinity);
/** @type {Vec3} */ this.lastSortCameraPos = new Vec3(Infinity, Infinity, Infinity);
/** @type {Vec3} */ this.lastSortCameraFwd = new Vec3(Infinity, Infinity, Infinity);
/** @type {boolean} */ this.sortNeeded = true;
/** @type {Vec3} */ this.lastColorUpdateCameraPos = new Vec3(Infinity, Infinity, Infinity);
/** @type {Vec3} */ this.lastColorUpdateCameraFwd = new Vec3(Infinity, Infinity, Infinity);
/**
* Layer placements, only non-octree placements are included.
*
* @type {GSplatPlacement[]}
*/ this.layerPlacements = [];
/** @type {boolean} */ this.layerPlacementsDirty = false;
/** @type {Map<GSplatPlacement, GSplatOctreeInstance>} */ this.octreeInstances = new Map();
/**
* Octree instances scheduled for destruction. We collect their releases and destroy them
* when creating the next world state
*
* @type {GSplatOctreeInstance[]}
*/ this.octreeInstancesToDestroy = [];
/**
* Flag set when new octree instances are added, to trigger immediate LOD evaluation.
*
* @type {boolean}
*/ this.hasNewOctreeInstances = false;
this.device = device;
this.scene = director.scene;
this.director = director;
this.cameraNode = cameraNode;
this.workBuffer = new GSplatWorkBuffer(device);
this.renderer = new GSplatRenderer(device, this.node, this.cameraNode, layer, this.workBuffer);
this.sorter = this.createSorter();
}
destroy() {
this._destroyed = true;
// Clean up all world states and decrement refs
for (const [, worldState] of this.worldStates){
for (const splat of worldState.splats){
splat.resource.decRefCount();
}
worldState.destroy();
}
this.worldStates.clear();
// Destroy all octree instances (they handle their own ref count cleanup)
for (const [, instance] of this.octreeInstances){
instance.destroy();
}
this.octreeInstances.clear();
// Also destroy any queued instances
for (const instance of this.octreeInstancesToDestroy){
instance.destroy();
}
this.octreeInstancesToDestroy.length = 0;
this.workBuffer.destroy();
this.renderer.destroy();
this.sorter.destroy();
}
get material() {
return this.renderer.material;
}
createSorter() {
// create sorter
const sorter = new GSplatUnifiedSorter();
sorter.on('sorted', (count, version, orderData)=>{
this.onSorted(count, version, orderData);
});
return sorter;
}
/**
* Supply the manager with the placements to use. This is used to update the manager when the
* layer's placements have changed, called infrequently.
*
* @param {GSplatPlacement[]} placements - The placements to reconcile with.
*/ reconcile(placements) {
tempNonOctreePlacements.clear();
for (const p of placements){
if (p.resource instanceof GSplatOctreeResource) {
// make sure octree instance exists for placement
if (!this.octreeInstances.has(p)) {
// @ts-ignore - p.resource is GSplatOctreeResource so octree cannot be null
this.octreeInstances.set(p, new GSplatOctreeInstance(this.device, p.resource.octree, p));
// mark that we have new instances that need initial LOD evaluation
this.hasNewOctreeInstances = true;
}
tempOctreePlacements.add(p);
} else {
// collect non-octree placement
tempNonOctreePlacements.add(p);
}
}
// remove octree instances that are no longer present and schedule them for destruction
for (const [placement, inst] of this.octreeInstances){
if (!tempOctreePlacements.has(placement)) {
this.octreeInstances.delete(placement);
// mark world as dirty since octree set changed
this.layerPlacementsDirty = true;
// queue the instance to be processed during next world state creation
this.octreeInstancesToDestroy.push(inst);
}
}
// compute dirtiness of non-octree placements compared to existing layerPlacements
this.layerPlacementsDirty = this.layerPlacements.length !== tempNonOctreePlacements.size;
if (!this.layerPlacementsDirty) {
for(let i = 0; i < this.layerPlacements.length; i++){
const existing = this.layerPlacements[i];
if (!tempNonOctreePlacements.has(existing)) {
this.layerPlacementsDirty = true;
break;
}
}
}
// update layerPlacements to new non-octree list
this.layerPlacements.length = 0;
for (const p of tempNonOctreePlacements){
this.layerPlacements.push(p);
}
// clear temporaries
tempNonOctreePlacements.clear();
tempOctreePlacements.clear();
}
updateWorldState() {
// Recreate world state if there are changes
const worldChanged = this.layerPlacementsDirty || this.worldStates.size === 0;
if (worldChanged) {
this.lastWorldStateVersion++;
const splats = [];
// color update thresholds
const { colorUpdateAngle, colorUpdateDistance } = this.scene.gsplat;
// add standalone splats
for (const p of this.layerPlacements){
const splatInfo = new GSplatInfo(this.device, p.resource, p);
splatInfo.resetColorAccumulators(colorUpdateAngle, colorUpdateDistance);
splats.push(splatInfo);
}
// add octree splats
for (const [, inst] of this.octreeInstances){
inst.activePlacements.forEach((p)=>{
if (p.resource) {
const splatInfo = new GSplatInfo(this.device, p.resource, p);
splatInfo.resetColorAccumulators(colorUpdateAngle, colorUpdateDistance);
splats.push(splatInfo);
}
});
}
// update sorter with current splats (adds new centers, removes unused ones)
this.sorter.updateCentersForSplats(splats);
const newState = new GSplatWorldState(this.device, this.lastWorldStateVersion, splats);
// increment ref count for all resources in new state
for (const splat of newState.splats){
splat.resource.incRefCount();
}
// collect file-release requests from octree instances.
for (const [, inst] of this.octreeInstances){
if (inst.removedCandidates && inst.removedCandidates.size) {
for (const fileIndex of inst.removedCandidates){
// each entry represents a single decRef
// pending releases will be applied on onSorted for this state
newState.pendingReleases.push([
inst.octree,
fileIndex
]);
}
inst.removedCandidates.clear();
}
}
// handle destruction of octree instances
if (this.octreeInstancesToDestroy.length) {
for (const inst of this.octreeInstancesToDestroy){
// collect file-release requests from octree instances
const toRelease = inst.getFileDecrements();
for (const fileIndex of toRelease){
newState.pendingReleases.push([
inst.octree,
fileIndex
]);
}
inst.destroy();
}
this.octreeInstancesToDestroy.length = 0;
}
this.worldStates.set(this.lastWorldStateVersion, newState);
this.layerPlacementsDirty = false;
// New world state requires sorting
this.sortNeeded = true;
}
}
onSorted(count, version, orderData) {
// remove all old states between last sorted version and current version
for(let v = this.sortedVersion; v < version; v++){
const oldState = this.worldStates.get(v);
if (oldState) {
// decrement ref count for all resources in old state
for (const splat of oldState.splats){
splat.resource.decRefCount();
}
this.worldStates.delete(v);
oldState.destroy();
}
}
this.sortedVersion = version;
// find the world state that has been sorted
const worldState = this.worldStates.get(version);
Debug.assert(worldState, `World state with version ${version} not found`);
if (worldState) {
// when a new version was sorted for the first time, we need to fully update work buffer
// to match centers buffer / sorted data
if (!worldState.sortedBefore) {
worldState.sortedBefore = true;
// resize work buffer if needed
const textureSize = worldState.textureSize;
if (textureSize !== this.workBuffer.textureSize) {
this.workBuffer.resize(textureSize);
this.renderer.setMaxNumSplats(textureSize * textureSize);
}
// render all splats to work buffer
this.workBuffer.render(worldState.splats, this.cameraNode, this.getDebugColors());
// update all splats to sync their transforms and reset color accumulators
// (prevents redundant re-render later)
const { colorUpdateAngle, colorUpdateDistance } = this.scene.gsplat;
worldState.splats.forEach((splat)=>{
splat.update();
splat.resetColorAccumulators(colorUpdateAngle, colorUpdateDistance);
});
// update camera tracking for color updates
this.updateColorCameraTracking();
// apply pending file-release requests
if (worldState.pendingReleases && worldState.pendingReleases.length) {
const cooldownTicks = this.scene.gsplat.cooldownTicks;
for (const [octree, fileIndex] of worldState.pendingReleases){
// decrement once for each staged release; refcount system guards against premature unload
octree.decRefCount(fileIndex, cooldownTicks);
}
worldState.pendingReleases.length = 0;
}
// number of splats to render
this.renderer.update(count, textureSize);
}
// update order texture
this.workBuffer.setOrderData(orderData);
// update renderer with new order data
this.renderer.frameUpdate(this.scene.gsplat);
}
}
/**
* Tests if the camera has moved or rotated enough to require LOD update.
*
* @returns {boolean} True if camera moved/rotated over thresholds, otherwise false.
*/ testCameraMovedForLod() {
// distance-based movement check
const distanceThreshold = this.scene.gsplat.lodUpdateDistance;
const currentCameraPos = this.cameraNode.getPosition();
const cameraMoved = this.lastLodCameraPos.distance(currentCameraPos) > distanceThreshold;
if (cameraMoved) {
return true;
}
// rotation-based movement check (optional)
let cameraRotated = false;
const lodUpdateAngleDeg = this.scene.gsplat.lodUpdateAngle;
if (lodUpdateAngleDeg > 0) {
if (Number.isFinite(this.lastLodCameraFwd.x)) {
const currentCameraFwd = this.cameraNode.forward;
const dot = Math.min(1, Math.max(-1, this.lastLodCameraFwd.dot(currentCameraFwd)));
const angle = Math.acos(dot);
const rotThreshold = lodUpdateAngleDeg * math.DEG_TO_RAD;
cameraRotated = angle > rotThreshold;
} else {
// first run, force update to initialize last orientation
cameraRotated = true;
}
}
return cameraMoved || cameraRotated;
}
/**
* Tests if the camera has moved enough to require re-sorting.
* - For radial sorting: only position matters (rotation doesn't affect sort order)
* - For directional sorting: only forward direction matters (position doesn't affect sort order)
*
* @returns {boolean} True if camera moved enough to require re-sorting, otherwise false.
*/ testCameraMovedForSort() {
const epsilon = 0.001;
if (this.scene.gsplat.radialSorting) {
// For radial sorting, only position changes matter
const currentCameraPos = this.cameraNode.getPosition();
const distance = this.lastSortCameraPos.distance(currentCameraPos);
return distance > epsilon;
}
// For directional sorting, only forward direction changes matter
if (Number.isFinite(this.lastSortCameraFwd.x)) {
const currentCameraFwd = this.cameraNode.forward;
const dot = Math.min(1, Math.max(-1, this.lastSortCameraFwd.dot(currentCameraFwd)));
const angle = Math.acos(dot);
return angle > epsilon;
}
// first run, force update to initialize last orientation
return true;
}
/**
* Updates the camera tracking state for color accumulation calculations.
* Called after any render that updates colors (full or color-only).
*/ updateColorCameraTracking() {
this.lastColorUpdateCameraPos.copy(this.cameraNode.getPosition());
this.lastColorUpdateCameraFwd.copy(this.cameraNode.forward);
}
/**
* Determines the colorization mode for rendering based on debug flags.
*
* @returns {Array<number[]>|undefined} Color array for debug visualization, or undefined for normal rendering
*/ getDebugColors() {
if (this.scene.gsplat.colorizeColorUpdate) {
_randomColorRaw ??= [];
// Random color for this update pass - use same color for all LOD levels
const r = Math.random();
const g = Math.random();
const b = Math.random();
for(let i = 0; i < _lodColorsRaw.length; i++){
_randomColorRaw[i] ??= [
0,
0,
0
];
_randomColorRaw[i][0] = r;
_randomColorRaw[i][1] = g;
_randomColorRaw[i][2] = b;
}
return _randomColorRaw;
} else if (this.scene.gsplat.colorizeLod) {
// LOD colors
return _lodColorsRaw;
}
return undefined;
}
/**
* Calculates camera movement deltas since last color update.
* Updates and returns the shared _cameraDeltas object.
*
* @returns {{ rotationDelta: number, translationDelta: number }} Shared camera movement deltas object
*/ calculateColorCameraDeltas() {
_cameraDeltas.rotationDelta = 0;
_cameraDeltas.translationDelta = 0;
// Skip delta calculation on first frame (camera position not yet initialized)
if (isFinite(this.lastColorUpdateCameraPos.x)) {
// Calculate rotation delta in degrees using dot product
const currentCameraFwd = this.cameraNode.forward;
const dot = Math.min(1, Math.max(-1, this.lastColorUpdateCameraFwd.dot(currentCameraFwd)));
_cameraDeltas.rotationDelta = Math.acos(dot) * math.RAD_TO_DEG;
// Calculate translation delta in world units
const currentCameraPos = this.cameraNode.getPosition();
_cameraDeltas.translationDelta = this.lastColorUpdateCameraPos.distance(currentCameraPos);
}
return _cameraDeltas;
}
/**
* Fires the frame:ready event with current sorting and loading state.
*/ fireFrameReadyEvent() {
const ready = this.sortedVersion === this.lastWorldStateVersion;
// Count total pending loads from octree instances (including environment)
let loadingCount = 0;
for (const [, inst] of this.octreeInstances){
loadingCount += inst.pendingLoadCount;
}
this.director.eventHandler.fire('frame:ready', this.cameraNode.camera, this.renderer.layer, ready, loadingCount);
}
update() {
// apply any pending sorted results
this.sorter.applyPendingSorted();
// update viewport for renderer
this.renderer.updateViewport(this.cameraNode);
let fullUpdate = false;
this.framesTillFullUpdate--;
if (this.framesTillFullUpdate <= 0) {
this.framesTillFullUpdate = 10;
// if sorter can keep up
if (this.sorter.jobsInFlight < 3) {
fullUpdate = true;
}
}
// when new octree instances are added, we need to evaluate their LOD immediately
const hasNewInstances = this.hasNewOctreeInstances && this.sorter.jobsInFlight < 3;
if (hasNewInstances) this.hasNewOctreeInstances = false;
let anyInstanceNeedsLodUpdate = false;
let anyOctreeMoved = false;
let cameraMovedOrRotatedForLod = false;
if (fullUpdate) {
// process any pending / prefetch resource completions and collect LOD updates
for (const [, inst] of this.octreeInstances){
const isDirty = inst.update(this.scene);
this.layerPlacementsDirty ||= isDirty;
const instNeeds = inst.consumeNeedsLodUpdate();
anyInstanceNeedsLodUpdate ||= instNeeds;
}
// Validate that resources in use haven't been unexpectedly destroyed
Debug.call(()=>{
const sortedState = this.worldStates.get(this.sortedVersion);
if (sortedState) {
for (const splat of sortedState.splats){
// Check if resource reference is null or undefined
if (!splat.resource) {
Debug.warn(`GSplatManager: Resource reference is null but still referenced in world state ${sortedState.version}`);
}
}
}
});
// check if any octree instances have moved enough to require LOD update
const threshold = this.scene.gsplat.lodUpdateDistance;
for (const [, inst] of this.octreeInstances){
const moved = inst.testMoved(threshold);
anyOctreeMoved ||= moved;
}
// check if camera has moved/rotated enough to require LOD update
cameraMovedOrRotatedForLod = this.testCameraMovedForLod();
}
// check if camera has moved enough to require re-sorting
if (this.testCameraMovedForSort()) {
this.sortNeeded = true;
}
Debug.call(()=>{
for (const [, inst] of this.octreeInstances){
inst.debugRender(this.scene);
}
});
// if parameters are dirty, rebuild world state
if (this.scene.gsplat.dirty) {
this.layerPlacementsDirty = true;
this.renderer.updateOverdrawMode(this.scene.gsplat);
}
// when camera or octree need LOD evaluated, or params are dirty, or resources completed, or new instances added
if (cameraMovedOrRotatedForLod || anyOctreeMoved || this.scene.gsplat.dirty || anyInstanceNeedsLodUpdate || hasNewInstances) {
// update the previous position where LOD was evaluated for octree instances
for (const [, inst] of this.octreeInstances){
inst.updateMoved();
}
// update last camera data when LOD was evaluated
this.lastLodCameraPos.copy(this.cameraNode.getPosition());
this.lastLodCameraFwd.copy(this.cameraNode.forward);
// update LOD for all octree instances
for (const [, inst] of this.octreeInstances){
inst.updateLod(this.cameraNode, this.scene.gsplat);
}
}
// create new world state if needed
this.updateWorldState();
// update sorter with new world state
const lastState = this.worldStates.get(this.lastWorldStateVersion);
if (lastState) {
if (!lastState.sortParametersSet) {
lastState.sortParametersSet = true;
const payload = this.prepareSortParameters(lastState);
this.sorter.setSortParameters(payload);
}
// debug render world space bounds for all splats
Debug.call(()=>{
if (this.scene.gsplat.debugAabbs) {
const tempAabb = new BoundingBox();
const scene = this.scene;
lastState.splats.forEach((splat)=>{
tempAabb.setFromTransformedAabb(splat.aabb, splat.node.getWorldTransform());
scene.immediate.drawWireAlignedBox(tempAabb.getMin(), tempAabb.getMax(), _lodColors[splat.lodIndex], true, scene.defaultDrawLayer);
});
}
});
// kick off sorting only if needed
if (this.sortNeeded) {
this.sort(lastState);
this.sortNeeded = false;
// Update camera tracking for next sort check
this.lastSortCameraPos.copy(this.cameraNode.getPosition());
this.lastSortCameraFwd.copy(this.cameraNode.forward);
}
}
// re-render splats that have changed their transform this frame, using last sorted state
const sortedState = this.worldStates.get(this.sortedVersion);
if (sortedState) {
// color update thresholds
const { colorUpdateAngle, colorUpdateDistance, colorUpdateDistanceLodScale, colorUpdateAngleLodScale } = this.scene.gsplat;
// Calculate camera movement deltas for color updates
const { rotationDelta, translationDelta } = this.calculateColorCameraDeltas();
// check each splat for full or color update
sortedState.splats.forEach((splat)=>{
// Check if splat's transform changed (needs full update)
if (splat.update()) {
_updatedSplats.push(splat);
// Reset accumulators for fully updated splats
splat.resetColorAccumulators(colorUpdateAngle, colorUpdateDistance);
// Splat moved, need to re-sort
this.sortNeeded = true;
} else if (splat.hasSphericalHarmonics) {
// Otherwise, check if color needs updating (accumulator-based)
// Add this frame's camera movement to accumulators
splat.colorAccumulatedRotation += rotationDelta;
splat.colorAccumulatedTranslation += translationDelta;
// Apply LOD-based scaling to thresholds
const lodIndex = splat.lodIndex ?? 0;
const distThreshold = colorUpdateDistance * Math.pow(colorUpdateDistanceLodScale, lodIndex);
const angleThreshold = colorUpdateAngle * Math.pow(colorUpdateAngleLodScale, lodIndex);
// Trigger update if either threshold exceeded
if (splat.colorAccumulatedRotation >= angleThreshold || splat.colorAccumulatedTranslation >= distThreshold) {
_splatsNeedingColorUpdate.push(splat);
splat.resetColorAccumulators(angleThreshold, distThreshold);
}
}
});
// Batch render all updated splats in a single render pass
if (_updatedSplats.length > 0) {
this.workBuffer.render(_updatedSplats, this.cameraNode, this.getDebugColors());
_updatedSplats.length = 0;
}
// Batch render color updates for all splats that exceeded thresholds
if (_splatsNeedingColorUpdate.length > 0) {
this.workBuffer.renderColor(_splatsNeedingColorUpdate, this.cameraNode, this.getDebugColors());
_splatsNeedingColorUpdate.length = 0;
}
// Update camera tracking once at the end of the frame
this.updateColorCameraTracking();
}
// tick cooldowns once per frame per unique octree
if (this.octreeInstances.size) {
const cooldownTicks = this.scene.gsplat.cooldownTicks;
for (const [, inst] of this.octreeInstances){
const octree = inst.octree;
if (!tempOctreesTicked.has(octree)) {
tempOctreesTicked.add(octree);
octree.updateCooldownTick(cooldownTicks);
}
}
tempOctreesTicked.clear();
}
// fire frame:ready event
this.fireFrameReadyEvent();
// return the number of visible splats for stats
const { textureSize } = this.workBuffer;
return textureSize * textureSize;
}
/**
* Sorts the splats of the given world state.
*
* @param {GSplatWorldState} lastState - The last world state.
*/ sort(lastState) {
// Get camera's world-space properties
const cameraNode = this.cameraNode;
const cameraMat = cameraNode.getWorldTransform();
cameraMat.getTranslation(cameraPosition);
cameraMat.getZ(cameraDirection).normalize();
const sorterRequest = [];
lastState.splats.forEach((splat)=>{
const modelMat = splat.node.getWorldTransform();
invModelMat.copy(modelMat).invert();
// uniform scale
const uniformScale = modelMat.getScale().x;
// camera direction in splat's rotated space
// transform by the full inverse matrix and then normalize, which cancels the (1/s) scaling factor
const transformedDirection = invModelMat.transformVector(cameraDirection).normalize();
// camera position in splat's local space (for circular sorting)
const transformedPosition = invModelMat.transformPoint(cameraPosition);
// world-space offset
modelMat.getTranslation(translation);
const offset = translation.sub(cameraPosition).dot(cameraDirection);
// sorter parameters
const aabbMin = splat.aabb.getMin();
const aabbMax = splat.aabb.getMax();
sorterRequest.push({
transformedDirection,
transformedPosition,
offset,
scale: uniformScale,
modelMat: modelMat.data.slice(),
aabbMin: [
aabbMin.x,
aabbMin.y,
aabbMin.z
],
aabbMax: [
aabbMax.x,
aabbMax.y,
aabbMax.z
]
});
});
this.sorter.setSortParams(sorterRequest, this.scene.gsplat.radialSorting);
}
/**
* Prepares sort parameters data for the sorter worker.
*
* @param {GSplatWorldState} worldState - The world state containing all needed data.
* @returns {object} - Data for sorter worker.
*/ prepareSortParameters(worldState) {
return {
command: 'intervals',
textureSize: worldState.textureSize,
totalUsedPixels: worldState.totalUsedPixels,
version: worldState.version,
ids: worldState.splats.map((splat)=>splat.resource.id),
lineStarts: worldState.splats.map((splat)=>splat.lineStart),
padding: worldState.splats.map((splat)=>splat.padding),
// TODO: consider storing this in typed array and transfer it to sorter worker
intervals: worldState.splats.map((splat)=>splat.intervals)
};
}
}
export { GSplatManager };