playcanvas
Version:
PlayCanvas WebGL game engine
335 lines (332 loc) • 12.6 kB
JavaScript
import { Mat4 } from '../../core/math/mat4.js';
import { Vec2 } from '../../core/math/vec2.js';
import { Vec3 } from '../../core/math/vec3.js';
import { BoundingBox } from '../../core/shape/bounding-box.js';
import { Color } from '../../core/math/color.js';
import { GSplatPlacement } from './gsplat-placement.js';
const _invWorldMat = new Mat4();
const _localCameraPos = new Vec3();
const _localCameraFwd = new Vec3();
const _dirToNode = new Vec3();
const _tempCompletedUrls = [];
new BoundingBox();
[
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)
];
class GSplatOctreeInstance {
constructor(octree, placement, assetLoader){
this.activePlacements = new Set();
this.dirtyModifiedPlacements = false;
this.pending = new Set();
this.pendingDecrements = new Map();
this.removedCandidates = new Set();
this.previousPosition = new Vec3();
this.needsLodUpdate = false;
this.prefetchPending = new Set();
this.pendingVisibleAdds = new Map();
this.octree = octree;
this.placement = placement;
this.assetLoader = assetLoader;
this.nodeLods = new Int32Array(octree.nodes.length);
this.nodeLods.fill(-1);
const numFiles = octree.files.length;
this.filePlacements = new Array(numFiles).fill(null);
}
destroy() {
this.pending.clear();
this.pendingDecrements.clear();
this.filePlacements.length = 0;
}
getFileDecrements() {
const toRelease = [];
for(let i = 0; i < this.filePlacements.length; i++){
if (this.filePlacements[i]) {
toRelease.push(i);
}
}
return toRelease;
}
calculateNodeLod(localCameraPosition, localCameraForward, nodeIndex, maxLod, lodDistances, lodBehindPenalty) {
const node = this.octree.nodes[nodeIndex];
_dirToNode.copy(node.bounds.center).sub(localCameraPosition);
let distance = _dirToNode.length();
if (lodBehindPenalty > 1 && distance > 0.01) {
const dotOverDistance = localCameraForward.dot(_dirToNode) / distance;
if (dotOverDistance < 0) {
const t = -dotOverDistance;
const factor = 1 + t * (lodBehindPenalty - 1);
distance *= factor;
}
}
for(let lod = 0; lod < maxLod; lod++){
if (distance < lodDistances[lod]) {
return lod;
}
}
return maxLod;
}
selectDesiredLodIndex(node, optimalLodIndex, maxLod, lodUnderfillLimit) {
if (lodUnderfillLimit > 0) {
const allowedMaxCoarseLod = Math.min(maxLod, optimalLodIndex + lodUnderfillLimit);
for(let lod = optimalLodIndex; lod <= allowedMaxCoarseLod; lod++){
const fi = node.lods[lod].fileIndex;
if (fi !== -1 && this.octree.getFileResource(fi)) {
return lod;
}
}
for(let lod = allowedMaxCoarseLod; lod >= optimalLodIndex; lod--){
const fi = node.lods[lod].fileIndex;
if (fi !== -1) {
return lod;
}
}
}
return optimalLodIndex;
}
prefetchNextLod(node, desiredLodIndex, optimalLodIndex) {
if (desiredLodIndex === -1 || optimalLodIndex === -1) return;
if (desiredLodIndex === optimalLodIndex) {
const fi = node.lods[optimalLodIndex].fileIndex;
if (fi !== -1) {
this.octree.ensureFileResource(fi, this.assetLoader);
if (!this.octree.getFileResource(fi)) {
this.prefetchPending.add(fi);
}
}
return;
}
const targetLod = Math.max(optimalLodIndex, desiredLodIndex - 1);
for(let lod = targetLod; lod >= optimalLodIndex; lod--){
const fi = node.lods[lod].fileIndex;
if (fi !== -1) {
this.octree.ensureFileResource(fi, this.assetLoader);
if (!this.octree.getFileResource(fi)) {
this.prefetchPending.add(fi);
}
break;
}
}
}
updateLod(cameraNode, params) {
const worldCameraPosition = cameraNode.getPosition();
const octreeWorldTransform = this.placement.node.getWorldTransform();
_invWorldMat.copy(octreeWorldTransform).invert();
const localCameraPosition = _invWorldMat.transformPoint(worldCameraPosition, _localCameraPos);
const worldCameraForward = cameraNode.forward;
const localCameraForward = _invWorldMat.transformVector(worldCameraForward, _localCameraFwd).normalize();
const maxLod = this.octree.lodLevels - 1;
const lodDistances = this.placement.lodDistances || [
5,
10,
15,
20,
25
];
const { lodBehindPenalty, lodRangeMin, lodRangeMax, lodUnderfillLimit = 0 } = params;
const rangeMin = Math.max(0, Math.min(lodRangeMin ?? 0, maxLod));
const rangeMax = Math.max(rangeMin, Math.min(lodRangeMax ?? maxLod, maxLod));
const nodes = this.octree.nodes;
for(let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++){
const node = nodes[nodeIndex];
let optimalLodIndex = this.calculateNodeLod(localCameraPosition, localCameraForward, nodeIndex, maxLod, lodDistances, lodBehindPenalty);
if (optimalLodIndex < rangeMin) optimalLodIndex = rangeMin;
if (optimalLodIndex > rangeMax) optimalLodIndex = rangeMax;
const currentLodIndex = this.nodeLods[nodeIndex];
const desiredLodIndex = this.selectDesiredLodIndex(node, optimalLodIndex, maxLod, lodUnderfillLimit);
if (desiredLodIndex !== currentLodIndex) {
const currentFileIndex = currentLodIndex >= 0 ? node.lods[currentLodIndex].fileIndex : -1;
const desiredFileIndex = desiredLodIndex >= 0 ? node.lods[desiredLodIndex].fileIndex : -1;
const wasVisible = currentFileIndex !== -1;
const willBeVisible = desiredFileIndex !== -1;
const pendingEntry = this.pendingDecrements.get(nodeIndex);
if (pendingEntry) {
if (pendingEntry.newFileIndex !== desiredFileIndex) {
const prevPendingPlacement = this.filePlacements[pendingEntry.newFileIndex];
if (prevPendingPlacement) {
this.decrementFileRef(pendingEntry.newFileIndex, nodeIndex);
}
if (wasVisible && willBeVisible) {
this.pendingDecrements.set(nodeIndex, {
oldFileIndex: pendingEntry.oldFileIndex,
newFileIndex: desiredFileIndex
});
} else {
this.pendingDecrements.delete(nodeIndex);
}
}
}
if (!wasVisible && willBeVisible) {
const prevPendingFi = this.pendingVisibleAdds.get(nodeIndex);
if (prevPendingFi !== undefined && prevPendingFi !== desiredFileIndex) {
this.decrementFileRef(prevPendingFi, nodeIndex);
this.pendingVisibleAdds.delete(nodeIndex);
}
this.incrementFileRef(desiredFileIndex, nodeIndex, desiredLodIndex);
const newPlacement = this.filePlacements[desiredFileIndex];
if (newPlacement?.resource) {
this.nodeLods[nodeIndex] = desiredLodIndex;
this.pendingVisibleAdds.delete(nodeIndex);
} else {
this.pendingVisibleAdds.set(nodeIndex, desiredFileIndex);
}
} else if (wasVisible && !willBeVisible) {
const pendingEntry2 = this.pendingDecrements.get(nodeIndex);
if (pendingEntry2) {
this.decrementFileRef(pendingEntry2.newFileIndex, nodeIndex);
this.pendingDecrements.delete(nodeIndex);
}
this.decrementFileRef(currentFileIndex, nodeIndex);
this.nodeLods[nodeIndex] = -1;
this.pendingVisibleAdds.delete(nodeIndex);
} else if (wasVisible && willBeVisible) {
this.incrementFileRef(desiredFileIndex, nodeIndex, desiredLodIndex);
const newPlacement = this.filePlacements[desiredFileIndex];
if (newPlacement?.resource) {
this.decrementFileRef(currentFileIndex, nodeIndex);
this.pendingDecrements.delete(nodeIndex);
this.nodeLods[nodeIndex] = desiredLodIndex;
this.pendingVisibleAdds.delete(nodeIndex);
} else {
this.pendingDecrements.set(nodeIndex, {
oldFileIndex: currentFileIndex,
newFileIndex: desiredFileIndex
});
this.pendingVisibleAdds.delete(nodeIndex);
}
}
}
this.prefetchNextLod(node, desiredLodIndex, optimalLodIndex);
}
}
incrementFileRef(fileIndex, nodeIndex, lodIndex) {
if (fileIndex === -1) return;
let placement = this.filePlacements[fileIndex];
if (!placement) {
placement = new GSplatPlacement(null, this.placement.node, lodIndex);
this.filePlacements[fileIndex] = placement;
const removeScheduled = this.removedCandidates.delete(fileIndex);
if (!removeScheduled) {
this.octree.incRefCount(fileIndex);
}
if (!this.addFilePlacement(fileIndex)) {
this.octree.ensureFileResource(fileIndex, this.assetLoader);
this.pending.add(fileIndex);
}
}
const nodes = this.octree.nodes;
const node = nodes[nodeIndex];
const lod = node.lods[lodIndex];
const interval = new Vec2(lod.offset, lod.offset + lod.count - 1);
placement.intervals.set(nodeIndex, interval);
this.dirtyModifiedPlacements = true;
}
decrementFileRef(fileIndex, nodeIndex) {
if (fileIndex === -1) return;
const placement = this.filePlacements[fileIndex];
if (!placement) {
return;
}
if (placement) {
placement.intervals.delete(nodeIndex);
this.dirtyModifiedPlacements = true;
if (placement.intervals.size === 0) {
if (placement.resource) {
this.activePlacements.delete(placement);
}
this.removedCandidates.add(fileIndex);
this.filePlacements[fileIndex] = null;
this.pending.delete(fileIndex);
}
}
}
addFilePlacement(fileIndex) {
const res = this.octree.getFileResource(fileIndex);
if (res) {
const placement = this.filePlacements[fileIndex];
if (placement) {
placement.resource = res;
placement.aabb.copy(res.aabb);
this.activePlacements.add(placement);
this.dirtyModifiedPlacements = true;
this.removedCandidates.delete(fileIndex);
return true;
}
}
return false;
}
testMoved(threshold) {
const position = this.placement.node.getPosition();
const length = position.distance(this.previousPosition);
if (length > threshold) {
return true;
}
return false;
}
updateMoved() {
this.previousPosition.copy(this.placement.node.getPosition());
}
update(scene) {
if (this.pending.size) {
for (const fileIndex of this.pending){
this.octree.ensureFileResource(fileIndex, this.assetLoader);
if (this.addFilePlacement(fileIndex)) {
_tempCompletedUrls.push(fileIndex);
for (const [nodeIndex, { oldFileIndex, newFileIndex }] of this.pendingDecrements){
if (newFileIndex === fileIndex) {
this.decrementFileRef(oldFileIndex, nodeIndex);
this.pendingDecrements.delete(nodeIndex);
let newLodIndex = 0;
const nodeLods = this.octree.nodes[nodeIndex].lods;
for(let li = 0; li < nodeLods.length; li++){
if (nodeLods[li].fileIndex === newFileIndex) {
newLodIndex = li;
break;
}
}
this.nodeLods[nodeIndex] = newLodIndex;
}
}
}
}
if (_tempCompletedUrls.length > 0) {
this.needsLodUpdate = true;
}
for (const fileIndex of _tempCompletedUrls){
this.pending.delete(fileIndex);
}
_tempCompletedUrls.length = 0;
}
this.pollPrefetchCompletions();
const dirty = this.dirtyModifiedPlacements;
this.dirtyModifiedPlacements = false;
return dirty;
}
debugRender(scene) {}
consumeNeedsLodUpdate() {
const v = this.needsLodUpdate;
this.needsLodUpdate = false;
return v;
}
pollPrefetchCompletions() {
if (this.prefetchPending.size) {
for (const fileIndex of this.prefetchPending){
this.octree.ensureFileResource(fileIndex, this.assetLoader);
if (this.octree.getFileResource(fileIndex)) {
_tempCompletedUrls.push(fileIndex);
}
}
if (_tempCompletedUrls.length > 0) {
this.needsLodUpdate = true;
}
for (const fileIndex of _tempCompletedUrls){
this.prefetchPending.delete(fileIndex);
}
_tempCompletedUrls.length = 0;
}
}
}
export { GSplatOctreeInstance };