UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

360 lines (357 loc) 11.6 kB
import { EventHandler } from '../../core/event-handler.js'; import { TEXTURELOCK_READ } from '../../platform/graphics/constants.js'; import { platform } from '../../core/platform.js'; function SortWorker() { const myself = typeof self !== 'undefined' && self || require('node:worker_threads').parentPort; let order; let centers; let chunks; let mapping; let cameraPosition; let cameraDirection; let forceUpdate = false; const lastCameraPosition = { x: 0, y: 0, z: 0 }; const lastCameraDirection = { x: 0, y: 0, z: 0 }; const boundMin = { x: 0, y: 0, z: 0 }; const boundMax = { x: 0, y: 0, z: 0 }; let distances; let countBuffer; const numBins = 32; const binCount = new Array(numBins).fill(0); const binBase = new Array(numBins).fill(0); const binDivider = new Array(numBins).fill(0); const binarySearch = (m, n, compare_fn)=>{ while(m <= n){ const k = n + m >> 1; const cmp = compare_fn(k); if (cmp > 0) { m = k + 1; } else if (cmp < 0) { n = k - 1; } else { return k; } } return ~m; }; const update = ()=>{ if (!order || !centers || centers.length === 0 || !cameraPosition || !cameraDirection) return; const px = cameraPosition.x; const py = cameraPosition.y; const pz = cameraPosition.z; const dx = cameraDirection.x; const dy = cameraDirection.y; const dz = cameraDirection.z; const epsilon = 0.001; if (!forceUpdate && Math.abs(px - lastCameraPosition.x) < epsilon && Math.abs(py - lastCameraPosition.y) < epsilon && Math.abs(pz - lastCameraPosition.z) < epsilon && Math.abs(dx - lastCameraDirection.x) < epsilon && Math.abs(dy - lastCameraDirection.y) < epsilon && Math.abs(dz - lastCameraDirection.z) < epsilon) { return; } forceUpdate = false; lastCameraPosition.x = px; lastCameraPosition.y = py; lastCameraPosition.z = pz; lastCameraDirection.x = dx; lastCameraDirection.y = dy; lastCameraDirection.z = dz; let minDist; let maxDist; for(let i = 0; i < 8; ++i){ const x = i & 1 ? boundMin.x : boundMax.x; const y = i & 2 ? boundMin.y : boundMax.y; const z = i & 4 ? boundMin.z : boundMax.z; const d = x * dx + y * dy + z * dz; if (i === 0) { minDist = maxDist = d; } else { minDist = Math.min(minDist, d); maxDist = Math.max(maxDist, d); } } const numVertices = centers.length / 3; const compareBits = Math.max(10, Math.min(20, Math.round(Math.log2(numVertices / 4)))); const bucketCount = 2 ** compareBits + 1; if (distances?.length !== numVertices) { distances = new Uint32Array(numVertices); } if (!countBuffer || countBuffer.length !== bucketCount) { countBuffer = new Uint32Array(bucketCount); } else { countBuffer.fill(0); } const range = maxDist - minDist; if (range < 1e-6) { for(let i = 0; i < numVertices; ++i){ distances[i] = 0; countBuffer[0]++; } } else { const numChunks = chunks.length / 4; binCount.fill(0); for(let i = 0; i < numChunks; ++i){ const x = chunks[i * 4 + 0]; const y = chunks[i * 4 + 1]; const z = chunks[i * 4 + 2]; const r = chunks[i * 4 + 3]; const d = x * dx + y * dy + z * dz - minDist; const binMin = Math.max(0, Math.floor((d - r) * numBins / range)); const binMax = Math.min(numBins, Math.ceil((d + r) * numBins / range)); for(let j = binMin; j < binMax; ++j){ binCount[j]++; } } const binTotal = binCount.reduce((a, b)=>a + b, 0); for(let i = 0; i < numBins; ++i){ binDivider[i] = binCount[i] / binTotal * bucketCount >>> 0; } for(let i = 0; i < numBins; ++i){ binBase[i] = i === 0 ? 0 : binBase[i - 1] + binDivider[i - 1]; } const binRange = range / numBins; let ii = 0; for(let i = 0; i < numVertices; ++i){ const x = centers[ii++]; const y = centers[ii++]; const z = centers[ii++]; const d = (x * dx + y * dy + z * dz - minDist) / binRange; const bin = d >>> 0; const sortKey = binBase[bin] + binDivider[bin] * (d - bin) >>> 0; distances[i] = sortKey; countBuffer[sortKey]++; } } for(let i = 1; i < bucketCount; i++){ countBuffer[i] += countBuffer[i - 1]; } for(let i = 0; i < numVertices; i++){ const distance = distances[i]; const destIndex = --countBuffer[distance]; order[destIndex] = i; } const cameraDist = px * dx + py * dy + pz * dz; const dist = (i)=>{ let o = order[i] * 3; return centers[o++] * dx + centers[o++] * dy + centers[o] * dz - cameraDist; }; const findZero = ()=>{ const result = binarySearch(0, numVertices - 1, (i)=>-dist(i)); return Math.min(numVertices, Math.abs(result)); }; const count = dist(numVertices - 1) >= 0 ? findZero() : numVertices; if (mapping) { for(let i = 0; i < numVertices; ++i){ order[i] = mapping[order[i]]; } } myself.postMessage({ order: order.buffer, count }, [ order.buffer ]); order = null; }; myself.addEventListener('message', (message)=>{ const msgData = message.data ?? message; if (msgData.order) { order = new Uint32Array(msgData.order); } if (msgData.centers) { centers = new Float32Array(msgData.centers); forceUpdate = true; if (msgData.chunks) { const chunksSrc = new Float32Array(msgData.chunks); chunks = new Float32Array(msgData.chunks, 0, chunksSrc.length * 4 / 6); boundMin.x = chunksSrc[0]; boundMin.y = chunksSrc[1]; boundMin.z = chunksSrc[2]; boundMax.x = chunksSrc[3]; boundMax.y = chunksSrc[4]; boundMax.z = chunksSrc[5]; for(let i = 0; i < chunksSrc.length / 6; ++i){ const mx = chunksSrc[i * 6 + 0]; const my = chunksSrc[i * 6 + 1]; const mz = chunksSrc[i * 6 + 2]; const Mx = chunksSrc[i * 6 + 3]; const My = chunksSrc[i * 6 + 4]; const Mz = chunksSrc[i * 6 + 5]; chunks[i * 4 + 0] = (mx + Mx) * 0.5; chunks[i * 4 + 1] = (my + My) * 0.5; chunks[i * 4 + 2] = (mz + Mz) * 0.5; chunks[i * 4 + 3] = Math.sqrt((Mx - mx) ** 2 + (My - my) ** 2 + (Mz - mz) ** 2) * 0.5; if (mx < boundMin.x) boundMin.x = mx; if (my < boundMin.y) boundMin.y = my; if (mz < boundMin.z) boundMin.z = mz; if (Mx > boundMax.x) boundMax.x = Mx; if (My > boundMax.y) boundMax.y = My; if (Mz > boundMax.z) boundMax.z = Mz; } } else { const numVertices = centers.length / 3; const numChunks = Math.ceil(numVertices / 256); chunks = new Float32Array(numChunks * 4); boundMin.x = boundMin.y = boundMin.z = Infinity; boundMax.x = boundMax.y = boundMax.z = -Infinity; let mx, my, mz, Mx, My, Mz; for(let c = 0; c < numChunks; ++c){ mx = my = mz = Infinity; Mx = My = Mz = -Infinity; const start = c * 256; const end = Math.min(numVertices, (c + 1) * 256); for(let i = start; i < end; ++i){ const x = centers[i * 3 + 0]; const y = centers[i * 3 + 1]; const z = centers[i * 3 + 2]; const validX = Number.isFinite(x); const validY = Number.isFinite(y); const validZ = Number.isFinite(z); if (!validX) centers[i * 3 + 0] = 0; if (!validY) centers[i * 3 + 1] = 0; if (!validZ) centers[i * 3 + 2] = 0; if (!validX || !validY || !validZ) { continue; } if (x < mx) mx = x; else if (x > Mx) Mx = x; if (y < my) my = y; else if (y > My) My = y; if (z < mz) mz = z; else if (z > Mz) Mz = z; if (x < boundMin.x) boundMin.x = x; else if (x > boundMax.x) boundMax.x = x; if (y < boundMin.y) boundMin.y = y; else if (y > boundMax.y) boundMax.y = y; if (z < boundMin.z) boundMin.z = z; else if (z > boundMax.z) boundMax.z = z; } chunks[c * 4 + 0] = (mx + Mx) * 0.5; chunks[c * 4 + 1] = (my + My) * 0.5; chunks[c * 4 + 2] = (mz + Mz) * 0.5; chunks[c * 4 + 3] = Math.sqrt((Mx - mx) ** 2 + (My - my) ** 2 + (Mz - mz) ** 2) * 0.5; } } } if (msgData.hasOwnProperty('mapping')) { mapping = msgData.mapping ? new Uint32Array(msgData.mapping) : null; forceUpdate = true; } if (msgData.cameraPosition) cameraPosition = msgData.cameraPosition; if (msgData.cameraDirection) cameraDirection = msgData.cameraDirection; update(); }); } class GSplatSorter extends EventHandler { constructor(){ super(); const workerSource = `(${SortWorker.toString()})()`; const nodeEnv = platform.environment === 'node'; if (nodeEnv) { this.worker = new Worker(workerSource, { eval: true }); } else { this.worker = new Worker(URL.createObjectURL(new Blob([ workerSource ], { type: "application/javascript" }))); } this.worker[nodeEnv ? 'on' : 'addEventListener']('message', (message)=>{ const msgData = message.data ?? message; const newOrder = msgData.order; const oldOrder = this.orderTexture._levels[0].buffer; this.worker.postMessage({ order: oldOrder }, [ oldOrder ]); this.orderTexture._levels[0] = new Uint32Array(newOrder); this.orderTexture.upload(); this.fire('updated', msgData.count); }); } destroy() { this.worker.terminate(); this.worker = null; } init(orderTexture, centers, chunks) { this.orderTexture = orderTexture; this.centers = centers.slice(); const orderBuffer = this.orderTexture.lock({ mode: TEXTURELOCK_READ }).slice(); this.orderTexture.unlock(); for(let i = 0; i < orderBuffer.length; ++i){ orderBuffer[i] = i; } const obj = { order: orderBuffer.buffer, centers: centers.buffer, chunks: chunks?.buffer }; const transfer = [ orderBuffer.buffer, centers.buffer ].concat(chunks ? [ chunks.buffer ] : []); this.worker.postMessage(obj, transfer); } setMapping(mapping) { if (mapping) { const centers = new Float32Array(mapping.length * 3); for(let i = 0; i < mapping.length; ++i){ const src = mapping[i] * 3; const dst = i * 3; centers[dst + 0] = this.centers[src + 0]; centers[dst + 1] = this.centers[src + 1]; centers[dst + 2] = this.centers[src + 2]; } this.worker.postMessage({ centers: centers.buffer, mapping: mapping.buffer }, [ centers.buffer, mapping.buffer ]); } else { const centers = this.centers.slice(); this.worker.postMessage({ centers: centers.buffer, mapping: null }, [ centers.buffer ]); } } setCamera(pos, dir) { this.worker.postMessage({ cameraPosition: { x: pos.x, y: pos.y, z: pos.z }, cameraDirection: { x: dir.x, y: dir.y, z: dir.z } }); } } export { GSplatSorter };