playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
719 lines (718 loc) • 25.1 kB
JavaScript
import { math } from "../../core/math/math.js";
import { Mat4 } from "../../core/math/mat4.js";
import { Vec3 } from "../../core/math/vec3.js";
import { BoundingBox } from "../../core/shape/bounding-box.js";
import { BlockAllocator } from "../../core/block-allocator.js";
import { GSplatInfo } from "./gsplat-info.js";
import { GSplatWorkBuffer } from "./gsplat-work-buffer.js";
import { GSplatOctreeInstance } from "./gsplat-octree-instance.js";
import { GSplatOctreeResource } from "./gsplat-octree.resource.js";
import { GSplatWorldState } from "./gsplat-world-state.js";
import { GSplatPlacementStateTracker } from "./gsplat-placement-state-tracker.js";
import { GSplatBudgetBalancer } from "./gsplat-budget-balancer.js";
import { GSPLAT_DEBUG_LOD, GSPLAT_DEBUG_SH_UPDATE } from "../constants.js";
const cameraPosition = new Vec3();
const _tempVec3 = new Vec3();
const invModelMat = new Mat4();
const _localCamPos = new Vec3();
const _closestPt = new Vec3();
const _meshInstanceAabb = new BoundingBox();
const _tempPlacementAabb = new BoundingBox();
const _cameraDeltas = { translationDelta: 0 };
const tempOctreesTicked = /* @__PURE__ */ new Set();
const _queuedSplats = /* @__PURE__ */ new Set();
const _updatedSplats = [];
const _splatsWithSH = [];
const _changedColorAllocIds = /* @__PURE__ */ new Set();
const tempNonOctreePlacements = /* @__PURE__ */ new Set();
const tempOctreePlacements = /* @__PURE__ */ new Set();
const _lodColorsRaw = [
[1, 0, 0],
// red
[0, 1, 0],
// green
[0, 0, 1],
// blue
[1, 1, 0],
// yellow
[1, 0, 1],
// magenta
[0, 1, 1],
// cyan
[1, 0.5, 0],
// orange
[0.5, 0, 1]
// purple
];
let _randomColorRaw = null;
const ALLOCATOR_GROW_MULTIPLIER = 1.15;
class GSplatWorld {
_device;
_scene;
_workBuffer;
_worldStates = /* @__PURE__ */ new Map();
_lastWorldStateVersion = 0;
_currentVersion = 0;
_worldStateDirty = false;
_workBufferFormatVersion = -1;
_workBufferRebuildRequired = false;
_bufferCopyUploaded = 0;
_bufferCopyTotal = 0;
_stateTracker = new GSplatPlacementStateTracker();
_framesTillFullUpdate = 0;
_lodUpdateRequested = false;
_lastLodCameraPos = new Vec3(Infinity, Infinity, Infinity);
_lastLodCameraFwd = new Vec3(Infinity, Infinity, Infinity);
_lastLodCameraFov = -1;
_budgetBalancer = new GSplatBudgetBalancer();
_budgetScale = 1;
_allocator;
_allocationMap = /* @__PURE__ */ new Map();
_lastColorUpdateCameraPos = new Vec3(Infinity, Infinity, Infinity);
_layerPlacements = [];
_layerPlacementsDirty = false;
_placementSetChanged = false;
_octreeInstances = /* @__PURE__ */ new Map();
_octreeInstancesToDestroy = [];
_hasNewOctreeInstances = false;
_awaitingLodUpdate = false;
constructor(device, scene) {
this._device = device;
this._scene = scene;
const budget = scene.gsplat.splatBudget;
this._allocator = new BlockAllocator(budget > 0 ? Math.ceil(budget * ALLOCATOR_GROW_MULTIPLIER) : 0, ALLOCATOR_GROW_MULTIPLIER);
this._workBuffer = new GSplatWorkBuffer(device, scene.gsplat.format);
this._workBufferFormatVersion = this._workBuffer.format.extraStreamsVersion;
}
destroy() {
for (const [, worldState] of this._worldStates) {
for (const splat of worldState.splats) {
splat.resource.decRefCount();
}
worldState.destroy();
}
this._worldStates.clear();
for (const [, instance] of this._octreeInstances) {
instance.destroy();
}
this._octreeInstances.clear();
for (const instance of this._octreeInstancesToDestroy) {
instance.destroy();
}
this._octreeInstancesToDestroy.length = 0;
this._workBuffer.destroy();
}
// --- read-only accessors (the one-way contract: manager reads, never reassigns) ---
get workBuffer() {
return this._workBuffer;
}
get currentVersion() {
return this._currentVersion;
}
get lastWorldStateVersion() {
return this._lastWorldStateVersion;
}
get bufferCopyUploaded() {
return this._bufferCopyUploaded;
}
get bufferCopyTotal() {
return this._bufferCopyTotal;
}
get awaitingLodUpdate() {
return this._awaitingLodUpdate;
}
get hasOctreeInstances() {
return this._octreeInstances.size > 0;
}
get pendingLoadCount() {
let loadingCount = 0;
for (const [, inst] of this._octreeInstances) {
loadingCount += inst.pendingLoadCount;
}
return loadingCount;
}
get currentState() {
return this._worldStates.get(this._currentVersion);
}
getState(version) {
return this._worldStates.get(version);
}
get hasBounds() {
return this._workBuffer.frustumCuller.totalBoundsEntries > 0;
}
// --- mutation entry points (manager-driven) ---
resetFrameStats() {
this._bufferCopyUploaded = 0;
this._bufferCopyTotal = 0;
}
invalidate({ worldState = false, workBuffer = false } = {}) {
if (worldState) this._worldStateDirty = true;
if (workBuffer) this._workBufferRebuildRequired = true;
}
invalidateSortState() {
const currentState = this._worldStates.get(this._currentVersion);
if (!currentState) return null;
currentState.sortParametersSet = false;
currentState.sortedBefore = false;
return currentState.splats;
}
syncFormat(result) {
result.bufferRecreated = false;
result.sortNeeded = false;
const currentFormat = this._scene.gsplat.format;
if (this._workBuffer.format !== currentFormat) {
this._workBuffer.destroy();
this._workBuffer = new GSplatWorkBuffer(this._device, currentFormat);
this._workBufferFormatVersion = this._workBuffer.format.extraStreamsVersion;
this._workBufferRebuildRequired = true;
result.bufferRecreated = true;
result.sortNeeded = true;
}
const wbFormatVersion = this._workBuffer.format.extraStreamsVersion;
if (this._workBufferFormatVersion !== wbFormatVersion) {
this._workBufferFormatVersion = wbFormatVersion;
this._workBuffer.syncWithFormat();
this._workBufferRebuildRequired = true;
result.sortNeeded = true;
}
return result;
}
reconcile(placements) {
tempNonOctreePlacements.clear();
for (const p of placements) {
if (p.resource instanceof GSplatOctreeResource) {
if (!this._octreeInstances.has(p)) {
this._octreeInstances.set(p, new GSplatOctreeInstance(this._device, p.resource.octree, p));
this._hasNewOctreeInstances = true;
}
tempOctreePlacements.add(p);
} else {
tempNonOctreePlacements.add(p);
}
}
for (const [placement, inst] of this._octreeInstances) {
if (!tempOctreePlacements.has(placement)) {
this._octreeInstances.delete(placement);
this._layerPlacementsDirty = true;
this._placementSetChanged = true;
this._octreeInstancesToDestroy.push(inst);
}
}
this._layerPlacementsDirty || (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;
}
}
}
this._placementSetChanged || (this._placementSetChanged = this._layerPlacementsDirty);
this._layerPlacements.length = 0;
for (const p of tempNonOctreePlacements) {
this._layerPlacements.push(p);
}
tempNonOctreePlacements.clear();
tempOctreePlacements.clear();
}
update(camera, allowLodUpdate, requireCenters, result) {
result.newVersion = false;
result.overdrawDirty = false;
result.sortNeeded = false;
for (const [placement, inst] of this._octreeInstances) {
if (inst.octree.destroyed || !placement.resource) {
this._octreeInstances.delete(placement);
this._layerPlacementsDirty = true;
this._placementSetChanged = true;
this._octreeInstancesToDestroy.push(inst);
}
}
if (--this._framesTillFullUpdate <= 0) {
this._framesTillFullUpdate = 10;
this._lodUpdateRequested = true;
}
let fullUpdate = false;
if (this._lodUpdateRequested && allowLodUpdate) {
fullUpdate = true;
this._lodUpdateRequested = false;
}
const hasNewInstances = this._hasNewOctreeInstances && allowLodUpdate;
if (hasNewInstances) this._hasNewOctreeInstances = false;
let anyInstanceNeedsLodUpdate = false;
let anyOctreeMoved = false;
let cameraMovedOrRotatedForLod = false;
if (fullUpdate) {
for (const [, inst] of this._octreeInstances) {
const isDirty = inst.update();
this._layerPlacementsDirty || (this._layerPlacementsDirty = isDirty);
this._placementSetChanged || (this._placementSetChanged = inst.consumePlacementSetChanged());
const instNeeds = inst.consumeNeedsLodUpdate();
anyInstanceNeedsLodUpdate || (anyInstanceNeedsLodUpdate = instNeeds);
}
const threshold = this._scene.gsplat.lodUpdateDistance;
for (const [, inst] of this._octreeInstances) {
const moved = inst.testMoved(threshold);
anyOctreeMoved || (anyOctreeMoved = moved);
}
cameraMovedOrRotatedForLod = this.testCameraMovedForLod(camera);
this._awaitingLodUpdate = false;
}
if (this._scene.gsplat.dirty) {
this._layerPlacementsDirty = true;
result.overdrawDirty = true;
this._workBufferRebuildRequired = true;
result.sortNeeded = true;
if (this._octreeInstances.size > 0) {
this._awaitingLodUpdate = true;
}
}
if (cameraMovedOrRotatedForLod || anyOctreeMoved || this._scene.gsplat.dirty || anyInstanceNeedsLodUpdate || hasNewInstances) {
for (const [, inst] of this._octreeInstances) {
inst.updateMoved();
}
this._lastLodCameraPos.copy(camera.getPosition());
this._lastLodCameraFwd.copy(camera.forward);
this._lastLodCameraFov = camera.camera.fov;
const budget = this._scene.gsplat.splatBudget;
if (budget > 0) {
this._enforceBudget(budget, camera);
} else {
this._budgetScale = 1;
for (const [, inst] of this._octreeInstances) {
inst.updateLod(camera, this._scene.gsplat);
}
}
}
if (this._updateWorldState(requireCenters)) {
result.newVersion = true;
result.sortNeeded = true;
}
return result;
}
_updateWorldState(requireCenters) {
let stateChanged = this._stateTracker.hasChanges(this._layerPlacements);
for (const [, inst] of this._octreeInstances) {
if (this._stateTracker.hasChanges(inst.activePlacements)) {
stateChanged = true;
}
}
const placementsChanged = this._layerPlacementsDirty;
const worldChanged = placementsChanged || stateChanged || this._worldStates.size === 0 || this._worldStateDirty;
if (!worldChanged) {
return false;
}
this._lastWorldStateVersion++;
const splats = [];
for (const p of this._layerPlacements) {
if (requireCenters && !p.resource.hasCenters) {
continue;
}
p.ensureInstanceStreams(this._device);
const splatInfo = new GSplatInfo(this._device, p.resource, p);
splats.push(splatInfo);
}
for (const [, inst] of this._octreeInstances) {
inst.activePlacements.forEach((p) => {
if (p.resource) {
const leafResource = p.resource;
if (requireCenters && !leafResource.hasCenters) {
return;
}
p.ensureInstanceStreams(this._device);
const octreeNodes = p.intervals.size > 0 ? inst.octree.nodes : null;
const nodeInfos = octreeNodes ? inst.nodeInfos : null;
const splatInfo = new GSplatInfo(this._device, p.resource, p, octreeNodes, nodeInfos);
splats.push(splatInfo);
}
});
}
const newState = new GSplatWorldState(
this._device,
this._lastWorldStateVersion,
splats,
this._allocator,
this._allocationMap
);
for (const splat of newState.splats) {
splat.resource.incRefCount();
}
for (const [, inst] of this._octreeInstances) {
if (inst.removedCandidates && inst.removedCandidates.size) {
for (const fileIndex of inst.removedCandidates) {
newState.pendingReleases.push([inst.octree, fileIndex]);
}
inst.removedCandidates.clear();
}
}
if (this._octreeInstancesToDestroy.length) {
for (const inst of this._octreeInstancesToDestroy) {
if (inst.removedCandidates && inst.removedCandidates.size) {
for (const fileIndex of inst.removedCandidates) {
newState.pendingReleases.push([inst.octree, fileIndex]);
}
inst.removedCandidates.clear();
}
const toRelease = inst.getFileDecrements();
for (const fileIndex of toRelease) {
newState.pendingReleases.push([inst.octree, fileIndex]);
}
inst.destroy(true);
}
this._octreeInstancesToDestroy.length = 0;
}
if (this._placementSetChanged) {
newState.fullRebuild = true;
}
this._worldStates.set(this._lastWorldStateVersion, newState);
this._layerPlacementsDirty = false;
this._placementSetChanged = false;
this._worldStateDirty = false;
return true;
}
markSorted(version, count, camera, updateBounds, result) {
result.rebuilt = false;
result.count = 0;
result.textureSize = 0;
this.cleanupOldWorldStates(version);
this._currentVersion = version;
const worldState = this._worldStates.get(version);
if (worldState && !worldState.sortedBefore) {
worldState.sortedBefore = true;
this.rebuildWorkBuffer(worldState, count, false, camera, updateBounds);
result.rebuilt = true;
result.count = count;
result.textureSize = worldState.textureSize;
}
return result;
}
onSorted(version, count, orderData, camera, updateBounds, result) {
this.markSorted(version, count, camera, updateBounds, result);
if (this._worldStates.get(version)) {
this._workBuffer.setOrderData(orderData);
}
return result;
}
bake(version, camera, updateBounds, result) {
result.rebuilt = false;
result.count = 0;
result.textureSize = 0;
result.sortNeeded = false;
const sortedState = this._worldStates.get(version);
if (sortedState?.sortedBefore) {
if (this._workBufferRebuildRequired) {
const count = sortedState.totalActiveSplats;
this.rebuildWorkBuffer(sortedState, count, true, camera, updateBounds);
this._workBufferRebuildRequired = false;
result.rebuilt = true;
result.count = count;
result.textureSize = sortedState.textureSize;
} else {
result.sortNeeded = this.applyWorkBufferUpdates(sortedState, camera);
}
this.updateColorCameraTracking(camera);
}
return result;
}
rebuildWorkBuffer(worldState, count, forceFullRebuild, camera, updateBounds) {
const textureSize = worldState.textureSize;
if (textureSize !== this._workBuffer.textureSize) {
this._workBuffer.resize(textureSize);
}
if (updateBounds) {
this._workBuffer.frustumCuller.updateBoundsData(worldState.boundsGroups);
this._workBuffer.frustumCuller.updateTransformsData(worldState.boundsGroups);
}
const renderAll = forceFullRebuild || worldState.fullRebuild;
const splatsToRender = renderAll ? worldState.splats : worldState.needsUpload;
const changedAllocIds = renderAll ? null : worldState.needsUploadIds;
if (splatsToRender.length > 0) {
const totalBlocks = this._allocationMap.size;
const uploadBlocks = renderAll ? totalBlocks : worldState.needsUploadIds.size;
this._bufferCopyUploaded += uploadBlocks;
this._bufferCopyTotal = totalBlocks;
this._workBuffer.render(splatsToRender, camera, this.getDebugColors(), changedAllocIds);
}
for (let i = 0; i < worldState.splats.length; i++) {
worldState.splats[i].update();
}
this.updateColorCameraTracking(camera);
if (worldState.pendingReleases && worldState.pendingReleases.length) {
const cooldownTicks = this._scene.gsplat.cooldownTicks;
for (const [octree, fileIndex] of worldState.pendingReleases) {
octree.decRefCount(fileIndex, cooldownTicks);
}
worldState.pendingReleases.length = 0;
}
}
cleanupOldWorldStates(newVersion) {
const activeState = this._worldStates.get(newVersion);
if (!activeState.fullRebuild) {
for (let v = this._currentVersion + 1; v < newVersion; v++) {
if (this._worldStates.get(v)?.fullRebuild) {
activeState.fullRebuild = true;
break;
}
}
}
if (!activeState.fullRebuild) {
const activeIds = activeState.needsUploadIds;
const lookup = activeState.allocIdToSplat;
for (let v = this._currentVersion + 1; v < newVersion; v++) {
const oldState = this._worldStates.get(v);
if (oldState) {
for (const allocId of oldState.needsUploadIds) {
if (!activeIds.has(allocId)) {
activeIds.add(allocId);
const splat = lookup.get(allocId);
if (splat && !_queuedSplats.has(splat)) {
activeState.needsUpload.push(splat);
_queuedSplats.add(splat);
}
}
}
}
}
_queuedSplats.clear();
}
for (let v = this._currentVersion; v < newVersion; v++) {
const oldState = this._worldStates.get(v);
if (oldState) {
for (const splat of oldState.splats) {
splat.resource.decRefCount();
}
this._worldStates.delete(v);
oldState.destroy();
}
}
}
applyWorkBufferUpdates(state, camera) {
const { colorUpdateAngle } = this._scene.gsplat;
const ratio = Math.tan(colorUpdateAngle * math.DEG_TO_RAD);
const cameraPos = camera.getPosition();
const { translationDelta } = this.calculateColorCameraDeltas(camera);
const hasCameraMovement = translationDelta > 0;
let movedAny = false;
let uploadedBlocks = 0;
state.splats.forEach((splat) => {
if (splat.update()) {
_updatedSplats.push(splat);
uploadedBlocks += splat.intervalAllocIds.length;
if (splat.nodeInfos) {
for (const ni of splat.intervalNodeIndices) {
splat.nodeInfos[ni].colorAccumulatedTranslation = 0;
}
} else {
splat.colorAccumulatedTranslation = 0;
}
movedAny = true;
} else if (hasCameraMovement && splat.hasSphericalHarmonics) {
_splatsWithSH.push(splat);
if (splat.nodeInfos) {
const nodeIndices = splat.intervalNodeIndices;
for (let j = 0; j < nodeIndices.length; j++) {
const nodeInfo = splat.nodeInfos[nodeIndices[j]];
nodeInfo.colorAccumulatedTranslation += translationDelta;
const threshold = ratio * Math.max(1, nodeInfo.worldDistance);
if (nodeInfo.colorAccumulatedTranslation >= threshold) {
_changedColorAllocIds.add(splat.intervalAllocIds[j]);
nodeInfo.colorAccumulatedTranslation = 0;
uploadedBlocks++;
}
}
} else {
splat.colorAccumulatedTranslation += translationDelta;
invModelMat.copy(splat.node.getWorldTransform()).invert();
invModelMat.transformPoint(cameraPos, _localCamPos);
splat.aabb.closestPoint(_localCamPos, _closestPt);
const dist = _localCamPos.distance(_closestPt) * splat.node.getWorldTransform().getScale().x;
const threshold = ratio * Math.max(1, dist);
if (splat.colorAccumulatedTranslation >= threshold) {
_changedColorAllocIds.add(splat.allocId);
uploadedBlocks += splat.intervalAllocIds.length;
splat.colorAccumulatedTranslation = 0;
}
}
}
});
this._bufferCopyUploaded += uploadedBlocks;
this._bufferCopyTotal = this._allocationMap.size;
if (_updatedSplats.length > 0) {
this._workBuffer.render(_updatedSplats, camera, this.getDebugColors());
_updatedSplats.length = 0;
}
if (_changedColorAllocIds.size > 0) {
this._workBuffer.renderColor(
_splatsWithSH,
camera,
this.getDebugColors(),
_changedColorAllocIds
);
_changedColorAllocIds.clear();
}
_splatsWithSH.length = 0;
return movedAny;
}
testCameraMovedForLod(camera) {
const distanceThreshold = this._scene.gsplat.lodUpdateDistance;
const currentCameraPos = camera.getPosition();
const cameraMoved = this._lastLodCameraPos.distance(currentCameraPos) > distanceThreshold;
if (cameraMoved) {
return true;
}
let cameraRotated = false;
const lodUpdateAngleDeg = this._scene.gsplat.lodUpdateAngle;
if (lodUpdateAngleDeg > 0) {
if (Number.isFinite(this._lastLodCameraFwd.x)) {
const currentCameraFwd = camera.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 {
cameraRotated = true;
}
}
const currentFov = camera.camera.fov;
const fovChanged = this._lastLodCameraFov < 0 || Math.abs(currentFov - this._lastLodCameraFov) > this._lastLodCameraFov * 0.02;
return cameraMoved || cameraRotated || fovChanged;
}
updateColorCameraTracking(camera) {
this._lastColorUpdateCameraPos.copy(camera.getPosition());
}
getDebugColors() {
const debug = this._scene.gsplat.debug;
if (debug === GSPLAT_DEBUG_SH_UPDATE) {
_randomColorRaw ?? (_randomColorRaw = []);
const r = Math.random();
const g = Math.random();
const b = Math.random();
for (let i = 0; i < _lodColorsRaw.length; i++) {
_randomColorRaw[i] ?? (_randomColorRaw[i] = [0, 0, 0]);
_randomColorRaw[i][0] = r;
_randomColorRaw[i][1] = g;
_randomColorRaw[i][2] = b;
}
return _randomColorRaw;
} else if (debug === GSPLAT_DEBUG_LOD) {
return _lodColorsRaw;
}
return void 0;
}
calculateColorCameraDeltas(camera) {
_cameraDeltas.translationDelta = 0;
if (isFinite(this._lastColorUpdateCameraPos.x)) {
const currentCameraPos = camera.getPosition();
_cameraDeltas.translationDelta = this._lastColorUpdateCameraPos.distance(currentCameraPos);
}
return _cameraDeltas;
}
computeGlobalMaxDistance(camera) {
let maxDist = 0;
cameraPosition.copy(camera.getPosition());
for (const [, inst] of this._octreeInstances) {
const worldTransform = inst.placement.node.getWorldTransform();
const aabb = inst.placement.aabb;
worldTransform.transformPoint(aabb.center, _tempVec3);
const scale = worldTransform.getScale().x;
const dist = _tempVec3.distance(cameraPosition) + aabb.halfExtents.length() * scale;
if (dist > maxDist) maxDist = dist;
}
return Math.max(maxDist, 1);
}
_enforceBudget(budget, camera) {
const textureWidth = this._workBuffer.textureSize;
let fixedSplats = 0;
let paddingEstimate = 0;
for (const p of this._layerPlacements) {
const resource = p.resource;
if (resource) {
const numSplats = resource.numSplats ?? 0;
fixedSplats += numSplats;
paddingEstimate += (textureWidth - numSplats % textureWidth) % textureWidth;
}
}
const octreeBudget = Math.max(1, budget - fixedSplats);
const globalMaxDistance = this.computeGlobalMaxDistance(camera);
let totalOptimalSplats = 0;
for (const [, inst] of this._octreeInstances) {
totalOptimalSplats += inst.evaluateOptimalLods(camera, this._scene.gsplat, this._budgetScale, globalMaxDistance);
for (const placement of inst.activePlacements) {
const resource = placement.resource;
const numSplats = resource?.numSplats ?? 0;
paddingEstimate += (textureWidth - numSplats % textureWidth) % textureWidth;
}
}
const adjustedBudget = Math.max(1, octreeBudget - paddingEstimate);
if (totalOptimalSplats > 0) {
const ratio = totalOptimalSplats / adjustedBudget;
const budgetScaleDeadZone = 0.4;
const budgetScaleBlendRate = 0.3;
if (ratio > 1 + budgetScaleDeadZone || ratio < 1 - budgetScaleDeadZone) {
const invCorrection = 1 / Math.sqrt(ratio);
this._budgetScale *= 1 + (invCorrection - 1) * budgetScaleBlendRate;
this._budgetScale = Math.max(0.01, Math.min(this._budgetScale, 100));
}
}
this._budgetBalancer.balance(this._octreeInstances, adjustedBudget);
for (const [, inst] of this._octreeInstances) {
const maxLod = inst.octree.lodLevels - 1;
inst.applyLodChanges(maxLod, this._scene.gsplat);
}
}
computeAggregateAabb() {
let initialized = false;
const layerPlacements = this._layerPlacements;
for (let i = 0; i < layerPlacements.length; i++) {
initialized = this._accumulatePlacementAabb(layerPlacements[i], initialized);
}
for (const [, inst] of this._octreeInstances) {
initialized = this._accumulatePlacementAabb(inst.placement, initialized);
}
return initialized ? _meshInstanceAabb : null;
}
_accumulatePlacementAabb(placement, initialized) {
const a = placement.aabb;
if (!a) return initialized;
_tempPlacementAabb.setFromTransformedAabb(a, placement.node.getWorldTransform());
if (initialized) {
_meshInstanceAabb.add(_tempPlacementAabb);
} else {
_meshInstanceAabb.copy(_tempPlacementAabb);
}
return true;
}
tickCooldowns() {
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();
}
}
markInstancesNeedLodUpdate() {
for (const [, inst] of this._octreeInstances) {
inst.needsLodUpdate = true;
}
}
prepareSortParameters(worldState) {
return {
command: "intervals",
textureSize: worldState.textureSize,
totalActiveSplats: worldState.totalActiveSplats,
version: worldState.version,
ids: worldState.splats.map((splat) => splat.resource.id),
pixelOffsets: worldState.splats.map((splat) => splat.intervalOffsets),
// TODO: consider storing this in typed array and transfer it to sorter worker
intervals: worldState.splats.map((splat) => splat.intervals)
};
}
}
export {
GSplatWorld
};