playcanvas
Version:
PlayCanvas WebGL game engine
351 lines (348 loc) • 11.9 kB
JavaScript
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 { Color } from '../../core/math/color.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 tempOctreesTicked = new Set();
const _lodColorsRaw = [
[
1,
0,
0
],
[
0,
1,
0
],
[
0,
0,
1
],
[
1,
1,
0
],
[
1,
0,
1
]
];
[
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 GSplatManager {
constructor(device, director, layer, cameraNode){
this.node = new GraphNode('GSplatManager');
this.worldStates = new Map();
this.lastWorldStateVersion = 0;
this.sortedVersion = 0;
this.framesTillFullUpdate = 0;
this.lastCameraPos = new Vec3(Infinity, Infinity, Infinity);
this.lastCameraFwd = new Vec3(Infinity, Infinity, Infinity);
this.layerPlacements = [];
this.layerPlacementsDirty = false;
this.octreeInstances = new Map();
this.octreeInstancesToDestroy = [];
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();
this.cooldownTicks = this.director.assetLoader.cooldownTicks;
}
destroy() {
this.workBuffer.destroy();
this.renderer.destroy();
this.sorter.destroy();
}
createSorter() {
const sorter = new GSplatUnifiedSorter();
sorter.on('sorted', (count, version, orderData)=>{
this.onSorted(count, version, orderData);
});
return sorter;
}
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(p.resource.octree, p, this.director.assetLoader));
}
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.octreeInstancesToDestroy.push(inst);
}
}
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.layerPlacements.length = 0;
for (const p of tempNonOctreePlacements){
this.layerPlacements.push(p);
}
tempNonOctreePlacements.clear();
tempOctreePlacements.clear();
}
updateWorldState() {
const worldChanged = this.layerPlacementsDirty || this.worldStates.size === 0;
if (worldChanged) {
this.lastWorldStateVersion++;
const splats = [];
for (const p of this.layerPlacements){
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) {
splats.push(new GSplatInfo(this.device, p.resource, p));
}
});
}
splats.forEach((splat)=>{
this.sorter.setCenters(splat.resource.id, splat.resource.centers);
});
const newState = new GSplatWorldState(this.device, this.lastWorldStateVersion, splats);
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){
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;
}
}
onSorted(count, version, orderData) {
this.sortedVersion = version;
const oldState = this.worldStates.get(version - 1);
if (oldState) {
this.worldStates.delete(version - 1);
oldState.destroy();
}
const worldState = this.worldStates.get(version);
if (worldState) {
if (!worldState.sortedBefore) {
worldState.sortedBefore = true;
const textureSize = worldState.textureSize;
if (textureSize !== this.workBuffer.textureSize) {
this.workBuffer.resize(textureSize);
this.renderer.setMaxNumSplats(textureSize * textureSize);
}
const colorize = this.scene.gsplat.colorizeLod;
this.workBuffer.render(worldState.splats, this.cameraNode, colorize ? _lodColorsRaw : undefined);
if (worldState.pendingReleases && worldState.pendingReleases.length) {
for (const [octree, fileIndex] of worldState.pendingReleases){
octree.decRefCount(fileIndex, this.cooldownTicks);
}
worldState.pendingReleases.length = 0;
}
this.renderer.setNumSplats(count);
}
this.workBuffer.setOrderData(orderData);
}
}
testCameraMoved() {
const distanceThreshold = this.scene.gsplat.lodUpdateDistance;
const currentCameraPos = this.cameraNode.getPosition();
const cameraMoved = this.lastCameraPos.distance(currentCameraPos) > distanceThreshold;
if (cameraMoved) {
return true;
}
let cameraRotated = false;
const lodUpdateAngleDeg = this.scene.gsplat.lodUpdateAngle;
if (lodUpdateAngleDeg > 0) {
if (Number.isFinite(this.lastCameraFwd.x)) {
const currentCameraFwd = this.cameraNode.forward;
const dot = Math.min(1, Math.max(-1, this.lastCameraFwd.dot(currentCameraFwd)));
const angle = Math.acos(dot);
const rotThreshold = lodUpdateAngleDeg * Math.PI / 180;
cameraRotated = angle > rotThreshold;
} else {
cameraRotated = true;
}
}
return cameraMoved || cameraRotated;
}
update() {
let fullUpdate = false;
this.framesTillFullUpdate--;
if (this.framesTillFullUpdate <= 0) {
this.framesTillFullUpdate = 10;
if (this.sorter.jobsInFlight < 3) {
fullUpdate = true;
}
}
let anyInstanceNeedsLodUpdate = false;
let anyOctreeMoved = false;
let cameraMovedOrRotated = false;
if (fullUpdate) {
for (const [, inst] of this.octreeInstances){
const isDirty = inst.update(this.scene);
this.layerPlacementsDirty ||= isDirty;
const instNeeds = inst.consumeNeedsLodUpdate();
anyInstanceNeedsLodUpdate ||= instNeeds;
}
const threshold = this.scene.gsplat.lodUpdateDistance;
for (const [, inst] of this.octreeInstances){
const moved = inst.testMoved(threshold);
anyOctreeMoved ||= moved;
}
cameraMovedOrRotated = this.testCameraMoved();
}
if (this.scene.gsplat.dirty) {
this.layerPlacementsDirty = true;
}
if (cameraMovedOrRotated || anyOctreeMoved || this.scene.gsplat.dirty || anyInstanceNeedsLodUpdate) {
for (const [, inst] of this.octreeInstances){
inst.updateMoved();
}
this.lastCameraPos.copy(this.cameraNode.getPosition());
this.lastCameraFwd.copy(this.cameraNode.forward);
for (const [, inst] of this.octreeInstances){
inst.updateLod(this.cameraNode, this.scene.gsplat);
}
}
this.updateWorldState();
const lastState = this.worldStates.get(this.lastWorldStateVersion);
if (lastState) {
if (!lastState.sortParametersSet) {
lastState.sortParametersSet = true;
const payload = this.prepareSortParameters(lastState);
this.sorter.setSortParameters(payload);
}
this.sort(lastState);
}
const sortedState = this.worldStates.get(this.sortedVersion);
if (sortedState) {
sortedState.splats.forEach((splat)=>{
if (splat.update()) {
_updatedSplats.push(splat);
}
});
if (_updatedSplats.length > 0) {
const colorize = this.scene.gsplat.colorizeLod;
this.workBuffer.render(_updatedSplats, this.cameraNode, colorize ? _lodColorsRaw : undefined);
_updatedSplats.length = 0;
}
}
if (this.octreeInstances.size) {
for (const [, inst] of this.octreeInstances){
const octree = inst.octree;
if (!tempOctreesTicked.has(octree)) {
tempOctreesTicked.add(octree);
octree.updateCooldownTick(this.director.assetLoader);
}
}
tempOctreesTicked.clear();
}
const { textureSize } = this.workBuffer;
return textureSize * textureSize;
}
sort(lastState) {
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();
const uniformScale = modelMat.getScale().x;
const transformedDirection = invModelMat.transformVector(cameraDirection).normalize();
const transformedPosition = invModelMat.transformPoint(cameraPosition);
modelMat.getTranslation(translation);
const offset = translation.sub(cameraPosition).dot(cameraDirection);
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);
this.renderer.updateViewport(cameraNode);
}
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),
intervals: worldState.splats.map((splat)=>splat.intervals)
};
}
}
export { GSplatManager };