playcanvas
Version:
PlayCanvas WebGL game engine
371 lines (368 loc) • 13.4 kB
JavaScript
import { Color } from '../../core/math/color.js';
import { Mat4 } from '../../core/math/mat4.js';
import { Quat } from '../../core/math/quat.js';
import { Vec3 } from '../../core/math/vec3.js';
import { BoundingBox } from '../../core/shape/bounding-box.js';
/**
* @import { PlyElement } from '../../framework/parsers/ply.js'
* @import { Scene } from '../scene.js'
* @import { Vec4 } from '../../core/math/vec4.js'
*/ var mat4 = new Mat4();
var quat = new Quat();
var aabb = new BoundingBox();
var aabb2 = new BoundingBox();
var debugColor = new Color(1, 1, 0, 0.4);
var SH_C0 = 0.28209479177387814;
// iterator for accessing uncompressed splat data
class SplatIterator {
constructor(gsplatData, p, r, s, c){
var x = gsplatData.getProp('x');
var y = gsplatData.getProp('y');
var z = gsplatData.getProp('z');
var rx = gsplatData.getProp('rot_1');
var ry = gsplatData.getProp('rot_2');
var rz = gsplatData.getProp('rot_3');
var rw = gsplatData.getProp('rot_0');
var sx = gsplatData.getProp('scale_0');
var sy = gsplatData.getProp('scale_1');
var sz = gsplatData.getProp('scale_2');
var cr = gsplatData.getProp('f_dc_0');
var cg = gsplatData.getProp('f_dc_1');
var cb = gsplatData.getProp('f_dc_2');
var ca = gsplatData.getProp('opacity');
/**
* Calculates the sigmoid of a given value.
*
* @param {number} v - The value for which to compute the sigmoid function.
* @returns {number} The result of the sigmoid function.
*/ var sigmoid = (v)=>{
if (v > 0) {
return 1 / (1 + Math.exp(-v));
}
var t = Math.exp(v);
return t / (1 + t);
};
this.read = (i)=>{
if (p) {
p.x = x[i];
p.y = y[i];
p.z = z[i];
}
if (r) {
r.set(rx[i], ry[i], rz[i], rw[i]);
}
if (s) {
s.set(Math.exp(sx[i]), Math.exp(sy[i]), Math.exp(sz[i]));
}
if (c) {
c.set(0.5 + cr[i] * SH_C0, 0.5 + cg[i] * SH_C0, 0.5 + cb[i] * SH_C0, sigmoid(ca[i]));
}
};
}
}
/**
* Calculate a splat orientation matrix from its position and rotation.
* @param {Mat4} result - Mat4 instance holding calculated rotation matrix.
* @param {Vec3} p - The splat position
* @param {Quat} r - The splat rotation
*/ var calcSplatMat = (result, p, r)=>{
quat.set(r.x, r.y, r.z, r.w).normalize();
result.setTRS(p, quat, Vec3.ONE);
};
class GSplatData {
/**
* @param {BoundingBox} result - Bounding box instance holding calculated result.
* @param {Vec3} p - The splat position
* @param {Quat} r - The splat rotation
* @param {Vec3} s - The splat scale
*/ static calcSplatAabb(result, p, r, s) {
calcSplatMat(mat4, p, r);
aabb.center.set(0, 0, 0);
aabb.halfExtents.set(s.x * 2, s.y * 2, s.z * 2);
result.setFromTransformedAabb(aabb, mat4);
}
// access a named property
getProp(name, elementName) {
if (elementName === undefined) elementName = 'vertex';
var _this_getElement_properties_find, _this_getElement;
return (_this_getElement = this.getElement(elementName)) == null ? undefined : (_this_getElement_properties_find = _this_getElement.properties.find((p)=>p.name === name)) == null ? undefined : _this_getElement_properties_find.storage;
}
// access the named element
getElement(name) {
return this.elements.find((e)=>e.name === name);
}
// add a new property
addProp(name, storage) {
this.getElement('vertex').properties.push({
type: 'float',
name,
storage,
byteSize: 4
});
}
/**
* Create an iterator for accessing splat data
*
* @param {Vec3|null} [p] - the vector to receive splat position
* @param {Quat|null} [r] - the quaternion to receive splat rotation
* @param {Vec3|null} [s] - the vector to receive splat scale
* @param {Vec4|null} [c] - the vector to receive splat color
* @returns {SplatIterator} - The iterator
*/ createIter(p, r, s, c) {
return new SplatIterator(this, p, r, s, c);
}
/**
* Calculate pessimistic scene aabb taking into account splat size. This is faster than
* calculating an exact aabb.
*
* @param {BoundingBox} result - Where to store the resulting bounding box.
* @param {(i: number) => boolean} [pred] - Optional predicate function to filter splats.
* @returns {boolean} - Whether the calculation was successful.
*/ calcAabb(result, pred) {
var mx, my, mz, Mx, My, Mz;
var first = true;
var x = this.getProp('x');
var y = this.getProp('y');
var z = this.getProp('z');
var sx = this.getProp('scale_0');
var sy = this.getProp('scale_1');
var sz = this.getProp('scale_2');
for(var i = 0; i < this.numSplats; ++i){
if (pred && !pred(i)) {
continue;
}
var scaleVal = 2.0 * Math.exp(Math.max(sx[i], sy[i], sz[i]));
var px = x[i];
var py = y[i];
var pz = z[i];
if (first) {
first = false;
mx = px - scaleVal;
my = py - scaleVal;
mz = pz - scaleVal;
Mx = px + scaleVal;
My = py + scaleVal;
Mz = pz + scaleVal;
} else {
mx = Math.min(mx, px - scaleVal);
my = Math.min(my, py - scaleVal);
mz = Math.min(mz, pz - scaleVal);
Mx = Math.max(Mx, px + scaleVal);
My = Math.max(My, py + scaleVal);
Mz = Math.max(Mz, pz + scaleVal);
}
}
if (!first) {
result.center.set((mx + Mx) * 0.5, (my + My) * 0.5, (mz + Mz) * 0.5);
result.halfExtents.set((Mx - mx) * 0.5, (My - my) * 0.5, (Mz - mz) * 0.5);
}
return !first;
}
/**
* Calculate exact scene aabb taking into account splat size
*
* @param {BoundingBox} result - Where to store the resulting bounding box.
* @param {(i: number) => boolean} [pred] - Optional predicate function to filter splats.
* @returns {boolean} - Whether the calculation was successful.
*/ calcAabbExact(result, pred) {
var p = new Vec3();
var r = new Quat();
var s = new Vec3();
var iter = this.createIter(p, r, s);
var first = true;
for(var i = 0; i < this.numSplats; ++i){
if (pred && !pred(i)) {
continue;
}
iter.read(i);
if (first) {
first = false;
GSplatData.calcSplatAabb(result, p, r, s);
} else {
GSplatData.calcSplatAabb(aabb2, p, r, s);
result.add(aabb2);
}
}
return !first;
}
/**
* @param {Float32Array} result - Array containing the centers.
*/ getCenters(result) {
var x = this.getProp('x');
var y = this.getProp('y');
var z = this.getProp('z');
for(var i = 0; i < this.numSplats; ++i){
result[i * 3 + 0] = x[i];
result[i * 3 + 1] = y[i];
result[i * 3 + 2] = z[i];
}
}
/**
* @param {Vec3} result - The result.
* @param {Function} pred - Predicate given index for skipping.
*/ calcFocalPoint(result, pred) {
var x = this.getProp('x');
var y = this.getProp('y');
var z = this.getProp('z');
var sx = this.getProp('scale_0');
var sy = this.getProp('scale_1');
var sz = this.getProp('scale_2');
result.x = 0;
result.y = 0;
result.z = 0;
var sum = 0;
for(var i = 0; i < this.numSplats; ++i){
if (pred && !pred(i)) {
continue;
}
var weight = 1.0 / (1.0 + Math.exp(Math.max(sx[i], sy[i], sz[i])));
result.x += x[i] * weight;
result.y += y[i] * weight;
result.z += z[i] * weight;
sum += weight;
}
result.mulScalar(1 / sum);
}
/**
* @param {Scene} scene - The application's scene.
* @param {Mat4} worldMat - The world matrix.
*/ renderWireframeBounds(scene, worldMat) {
var p = new Vec3();
var r = new Quat();
var s = new Vec3();
var min = new Vec3();
var max = new Vec3();
var iter = this.createIter(p, r, s);
for(var i = 0; i < this.numSplats; ++i){
iter.read(i);
calcSplatMat(mat4, p, r);
mat4.mul2(worldMat, mat4);
min.set(s.x * -2, s.y * -2, s.z * -2);
max.set(s.x * 2.0, s.y * 2.0, s.z * 2.0);
// @ts-ignore
scene.immediate.drawWireAlignedBox(min, max, debugColor, true, scene.defaultDrawLayer, mat4);
}
}
get isCompressed() {
return false;
}
// return the number of spherical harmonic bands present. value will be between 0 and 3 inclusive.
get shBands() {
var numProps = ()=>{
for(var i = 0; i < 45; ++i){
if (!this.getProp("f_rest_" + i)) {
return i;
}
}
return 45;
};
var sizes = {
9: 1,
24: 2,
45: 3
};
var _sizes_numProps;
return (_sizes_numProps = sizes[numProps()]) != null ? _sizes_numProps : 0;
}
calcMortonOrder() {
var calcMinMax = (arr)=>{
var min = arr[0];
var max = arr[0];
for(var i = 1; i < arr.length; i++){
if (arr[i] < min) min = arr[i];
if (arr[i] > max) max = arr[i];
}
return {
min,
max
};
};
// https://fgiesen.wordpress.com/2009/12/13/decoding-morton-codes/
var encodeMorton3 = (x, y, z)=>{
var Part1By2 = (x)=>{
x &= 0x000003ff;
x = (x ^ x << 16) & 0xff0000ff;
x = (x ^ x << 8) & 0x0300f00f;
x = (x ^ x << 4) & 0x030c30c3;
x = (x ^ x << 2) & 0x09249249;
return x;
};
return (Part1By2(z) << 2) + (Part1By2(y) << 1) + Part1By2(x);
};
var x = this.getProp('x');
var y = this.getProp('y');
var z = this.getProp('z');
var { min: minX, max: maxX } = calcMinMax(x);
var { min: minY, max: maxY } = calcMinMax(y);
var { min: minZ, max: maxZ } = calcMinMax(z);
var sizeX = minX === maxX ? 0 : 1024 / (maxX - minX);
var sizeY = minY === maxY ? 0 : 1024 / (maxY - minY);
var sizeZ = minZ === maxZ ? 0 : 1024 / (maxZ - minZ);
var codes = new Map();
for(var i = 0; i < this.numSplats; i++){
var ix = Math.floor((x[i] - minX) * sizeX);
var iy = Math.floor((y[i] - minY) * sizeY);
var iz = Math.floor((z[i] - minZ) * sizeZ);
var code = encodeMorton3(ix, iy, iz);
var val = codes.get(code);
if (val) {
val.push(i);
} else {
codes.set(code, [
i
]);
}
}
var keys = Array.from(codes.keys()).sort((a, b)=>a - b);
var indices = new Uint32Array(this.numSplats);
var idx = 0;
for(var i1 = 0; i1 < keys.length; ++i1){
var val1 = codes.get(keys[i1]);
for(var j = 0; j < val1.length; ++j){
indices[idx++] = val1[j];
}
}
return indices;
}
// reorder the splat data to aid in better gpu memory access at render time
reorder(order) {
var cache = new Map();
var getStorage = (size)=>{
if (cache.has(size)) {
var buffer = cache.get(size);
cache.delete(size);
return buffer;
}
return new ArrayBuffer(size);
};
var returnStorage = (buffer)=>{
cache.set(buffer.byteLength, buffer);
};
var reorder = (data)=>{
var result = new data.constructor(getStorage(data.byteLength));
for(var i = 0; i < order.length; i++){
result[i] = data[order[i]];
}
returnStorage(data.buffer);
return result;
};
this.elements.forEach((element)=>{
element.properties.forEach((property)=>{
if (property.storage) {
property.storage = reorder(property.storage);
}
});
});
}
// reorder the splat data to aid in better gpu memory access at render time
reorderData() {
this.reorder(this.calcMortonOrder());
}
/**
* @param {PlyElement[]} elements - The elements.
*/ constructor(elements){
this.elements = elements;
this.numSplats = this.getElement('vertex').count;
}
}
export { GSplatData };