playcanvas
Version:
PlayCanvas WebGL game engine
660 lines (657 loc) • 24.3 kB
JavaScript
import { GSplatData } from '../../scene/gsplat/gsplat-data.js';
import { GSplatCompressedData } from '../../scene/gsplat/gsplat-compressed-data.js';
import { GSplatResource } from './gsplat-resource.js';
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _async_to_generator(fn) {
return function() {
var self = this, args = arguments;
return new Promise(function(resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}
/**
* @import { AssetRegistry } from '../asset/asset-registry.js'
* @import { Asset } from '../asset/asset.js'
* @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js'
* @import { ResourceHandlerCallback } from '../handlers/handler.js'
*/ /**
* @typedef {Int8Array|Uint8Array|Int16Array|Uint16Array|Int32Array|Uint32Array|Float32Array|Float64Array} DataType
*/ /**
* @typedef {object} PlyProperty
* @property {string} type - E.g. 'float'.
* @property {string} name - E.g. 'x', 'y', 'z', 'f_dc_0' etc.
* @property {DataType} storage - Data type, e.g. instance of Float32Array.
* @property {number} byteSize - BYTES_PER_ELEMENT of given data type.
*/ /**
* @typedef {object} PlyElement
* @property {string} name - E.g. 'vertex'.
* @property {number} count - Given count.
* @property {PlyProperty[]} properties - The properties.
*/ var magicBytes = new Uint8Array([
112,
108,
121,
10
]); // ply\n
var endHeaderBytes = new Uint8Array([
10,
101,
110,
100,
95,
104,
101,
97,
100,
101,
114,
10
]); // \nend_header\n
var dataTypeMap = new Map([
[
'char',
Int8Array
],
[
'uchar',
Uint8Array
],
[
'short',
Int16Array
],
[
'ushort',
Uint16Array
],
[
'int',
Int32Array
],
[
'uint',
Uint32Array
],
[
'float',
Float32Array
],
[
'double',
Float64Array
]
]);
// helper for streaming in chunks of data in a memory efficient way
class StreamBuf {
// read the next chunk of data
read() {
var _this = this;
return _async_to_generator(function*() {
var { value, done } = yield _this.reader.read();
if (done) {
throw new Error('Stream finished before end of header');
}
_this.push(value);
_this.progressFunc == null ? void 0 : _this.progressFunc.call(_this, value.byteLength);
})();
}
// append data to the buffer
push(data) {
if (!this.data) {
// first buffer
this.data = data;
this.view = new DataView(this.data.buffer);
this.tail = data.length;
} else {
var remaining = this.tail - this.head;
var newSize = remaining + data.length;
if (this.data.length >= newSize) {
// buffer is large enough to contain combined data
if (this.head > 0) {
// shuffle existing data to index 0 and append the new data
this.data.copyWithin(0, this.head, this.tail);
this.data.set(data, remaining);
this.head = 0;
this.tail = newSize;
} else {
// no shuffle needed, just append new data
this.data.set(data, this.tail);
this.tail += data.length;
}
} else {
// buffer is too small and must grow
var tmp = new Uint8Array(newSize);
if (this.head > 0 || this.tail < this.data.length) {
// shuffle existing data to index 0 and append the new data
tmp.set(this.data.subarray(this.head, this.tail), 0);
} else {
tmp.set(this.data, 0);
}
tmp.set(data, remaining);
this.data = tmp;
this.view = new DataView(this.data.buffer);
this.head = 0;
this.tail = newSize;
}
}
}
// remove the read data from the head of the buffer
compact() {
if (this.head > 0) {
this.data.copyWithin(0, this.head, this.tail);
this.tail -= this.head;
this.head = 0;
}
}
get remaining() {
return this.tail - this.head;
}
// helpers for extracting data from head
getInt8() {
var result = this.view.getInt8(this.head);
this.head++;
return result;
}
getUint8() {
var result = this.view.getUint8(this.head);
this.head++;
return result;
}
getInt16() {
var result = this.view.getInt16(this.head, true);
this.head += 2;
return result;
}
getUint16() {
var result = this.view.getUint16(this.head, true);
this.head += 2;
return result;
}
getInt32() {
var result = this.view.getInt32(this.head, true);
this.head += 4;
return result;
}
getUint32() {
var result = this.view.getUint32(this.head, true);
this.head += 4;
return result;
}
getFloat32() {
var result = this.view.getFloat32(this.head, true);
this.head += 4;
return result;
}
getFloat64() {
var result = this.view.getFloat64(this.head, true);
this.head += 8;
return result;
}
constructor(reader, progressFunc){
this.head = 0;
this.tail = 0;
this.reader = reader;
this.progressFunc = progressFunc;
}
}
// parse the ply header text and return an array of Element structures and a
// string containing the ply format
var parseHeader = (lines)=>{
var elements = [];
var comments = [];
var format;
for(var i = 1; i < lines.length; ++i){
var words = lines[i].split(' ');
switch(words[0]){
case 'comment':
comments.push(words.slice(1).join(' '));
break;
case 'format':
format = words[1];
break;
case 'element':
elements.push({
name: words[1],
count: parseInt(words[2], 10),
properties: []
});
break;
case 'property':
{
if (!dataTypeMap.has(words[1])) {
throw new Error("Unrecognized property data type '" + words[1] + "' in ply header");
}
var element = elements[elements.length - 1];
element.properties.push({
type: words[1],
name: words[2],
storage: null,
byteSize: dataTypeMap.get(words[1]).BYTES_PER_ELEMENT
});
break;
}
default:
throw new Error("Unrecognized header value '" + words[0] + "' in ply header");
}
}
return {
elements,
format,
comments
};
};
// return true if the array of elements references a compressed ply file
var isCompressedPly = (elements)=>{
var chunkProperties = [
'min_x',
'min_y',
'min_z',
'max_x',
'max_y',
'max_z',
'min_scale_x',
'min_scale_y',
'min_scale_z',
'max_scale_x',
'max_scale_y',
'max_scale_z',
'min_r',
'min_g',
'min_b',
'max_r',
'max_g',
'max_b'
];
var vertexProperties = [
'packed_position',
'packed_rotation',
'packed_scale',
'packed_color'
];
var shProperties = new Array(45).fill('').map((_, i)=>"f_rest_" + i);
var hasBaseElements = ()=>{
return elements[0].name === 'chunk' && elements[0].properties.every((p, i)=>p.name === chunkProperties[i] && p.type === 'float') && elements[1].name === 'vertex' && elements[1].properties.every((p, i)=>p.name === vertexProperties[i] && p.type === 'uint');
};
var hasSHElements = ()=>{
return elements[2].name === 'sh' && [
9,
24,
45
].indexOf(elements[2].properties.length) !== -1 && elements[2].properties.every((p, i)=>p.name === shProperties[i] && p.type === 'uchar');
};
return elements.length === 2 && hasBaseElements() || elements.length === 3 && hasBaseElements() && hasSHElements();
};
var isFloatPly = (elements)=>{
return elements.length === 1 && elements[0].name === 'vertex' && elements[0].properties.every((p)=>p.type === 'float');
};
// read the data of a compressed ply file
var readCompressedPly = /*#__PURE__*/ _async_to_generator(function*(streamBuf, elements) {
var result = new GSplatCompressedData();
var numChunks = elements[0].count;
var numChunkProperties = elements[0].properties.length;
var numVertices = elements[1].count;
// evaluate the storage size for the given count (this must match the
// texture size calculation in GSplatCompressed).
var evalStorageSize = (count)=>{
var width = Math.ceil(Math.sqrt(count));
var height = Math.ceil(count / width);
return width * height;
};
var storageSize = evalStorageSize(numVertices);
// allocate result
result.numSplats = numVertices;
result.chunkData = new Float32Array(numChunks * numChunkProperties);
result.vertexData = new Uint32Array(storageSize * 4);
// read length bytes of data into buffer
var read = /*#__PURE__*/ _async_to_generator(function*(buffer, length) {
var target = new Uint8Array(buffer);
var cursor = 0;
while(cursor < length){
while(streamBuf.remaining === 0){
/* eslint-disable no-await-in-loop */ yield streamBuf.read();
}
var toCopy = Math.min(length - cursor, streamBuf.remaining);
var src = streamBuf.data;
for(var i = 0; i < toCopy; ++i){
target[cursor++] = src[streamBuf.head++];
}
}
});
// read chunk data
yield read(result.chunkData.buffer, numChunks * numChunkProperties * 4);
// read packed vertices
yield read(result.vertexData.buffer, numVertices * 4 * 4);
// read sh data
if (elements.length === 3) {
// allocate memory for 48 coefficients per gaussian
var texStorageSize = storageSize * 16; // RGBA32U per texel
var shData0 = new Uint8Array(texStorageSize);
var shData1 = new Uint8Array(texStorageSize);
var shData2 = new Uint8Array(texStorageSize);
// the file contains 1, 2 or 3 bands of SH data (with 9, 24 or 45 coefficients respectively)
// we must load the data and pad it for GPU
var chunkSize = 1024;
var srcCoeffs = elements[2].properties.length / 3;
var tmpBuf = new Uint8Array(chunkSize * srcCoeffs * 3);
// read in chunks of 1k gaussians and write to the padded texture data
for(var i = 0; i < result.numSplats; i += chunkSize){
var toRead = Math.min(chunkSize, result.numSplats - i);
// read the next chunk of data
yield read(tmpBuf.buffer, toRead * srcCoeffs * 3);
// pad the data
for(var j = 0; j < toRead; ++j){
for(var k = 0; k < 15; ++k){
var tidx = (i + j) * 16 + k;
if (k < srcCoeffs) {
shData0[tidx] = tmpBuf[(j * 3 + 0) * srcCoeffs + k];
shData1[tidx] = tmpBuf[(j * 3 + 1) * srcCoeffs + k];
shData2[tidx] = tmpBuf[(j * 3 + 2) * srcCoeffs + k];
} else {
shData0[tidx] = 127;
shData1[tidx] = 127;
shData2[tidx] = 127;
}
}
}
}
result.shData0 = shData0;
result.shData1 = shData1;
result.shData2 = shData2;
result.shBands = ({
3: 1,
8: 2,
15: 3
})[srcCoeffs];
} else {
result.shBands = 0;
}
return result;
});
// read the data of a floating point ply file
var readFloatPly = /*#__PURE__*/ _async_to_generator(function*(streamBuf, elements) {
// calculate the size of an input element record
var element = elements[0];
var properties = element.properties;
var numProperties = properties.length;
var storage = properties.map((p)=>p.storage);
var inputSize = properties.reduce((a, p)=>a + p.byteSize, 0);
var vertexIdx = 0;
var floatData;
var checkFloatData = ()=>{
var buffer = streamBuf.data.buffer;
if ((floatData == null ? void 0 : floatData.buffer) !== buffer) {
floatData = new Float32Array(buffer, 0, buffer.byteLength / 4);
}
};
checkFloatData();
while(vertexIdx < element.count){
while(streamBuf.remaining < inputSize){
/* eslint-disable no-await-in-loop */ yield streamBuf.read();
checkFloatData();
}
var toRead = Math.min(element.count - vertexIdx, Math.floor(streamBuf.remaining / inputSize));
for(var j = 0; j < numProperties; ++j){
var s = storage[j];
for(var n = 0; n < toRead; ++n){
s[n + vertexIdx] = floatData[n * numProperties + j];
}
}
vertexIdx += toRead;
streamBuf.head += toRead * inputSize;
}
return new GSplatData(elements);
});
var readGeneralPly = /*#__PURE__*/ _async_to_generator(function*(streamBuf, elements) {
// read and deinterleave the data
for(var i = 0; i < elements.length; ++i){
var element = elements[i];
// calculate the size of an input element record
var inputSize = element.properties.reduce((a, p)=>a + p.byteSize, 0);
var propertyParsingFunctions = element.properties.map((p)=>{
/* eslint-disable brace-style */ if (p.storage) {
switch(p.type){
case 'char':
return (streamBuf, c)=>{
p.storage[c] = streamBuf.getInt8();
};
case 'uchar':
return (streamBuf, c)=>{
p.storage[c] = streamBuf.getUint8();
};
case 'short':
return (streamBuf, c)=>{
p.storage[c] = streamBuf.getInt16();
};
case 'ushort':
return (streamBuf, c)=>{
p.storage[c] = streamBuf.getUint16();
};
case 'int':
return (streamBuf, c)=>{
p.storage[c] = streamBuf.getInt32();
};
case 'uint':
return (streamBuf, c)=>{
p.storage[c] = streamBuf.getUint32();
};
case 'float':
return (streamBuf, c)=>{
p.storage[c] = streamBuf.getFloat32();
};
case 'double':
return (streamBuf, c)=>{
p.storage[c] = streamBuf.getFloat64();
};
default:
throw new Error("Unsupported property data type '" + p.type + "' in ply header");
}
} else {
return (streamBuf)=>{
streamBuf.head += p.byteSize;
};
}
/* eslint-enable brace-style */ });
var c = 0;
while(c < element.count){
while(streamBuf.remaining < inputSize){
/* eslint-disable no-await-in-loop */ yield streamBuf.read();
}
var toRead = Math.min(element.count - c, Math.floor(streamBuf.remaining / inputSize));
for(var n = 0; n < toRead; ++n){
for(var j = 0; j < element.properties.length; ++j){
propertyParsingFunctions[j](streamBuf, c);
}
c++;
}
}
}
// console.log(elements);
return new GSplatData(elements);
});
/**
* asynchronously read a ply file data
*
* @param {ReadableStreamDefaultReader<Uint8Array>} reader - The reader.
* @param {Function|null} propertyFilter - Function to filter properties with.
* @param {Function|null} progressFunc - Function to call with progress updates.
* @returns {Promise<{ data: GSplatData | GSplatCompressedData, comments: string[] }>} The ply file data.
*/ var readPly = /*#__PURE__*/ _async_to_generator(function*(reader, propertyFilter, progressFunc) {
if (propertyFilter === void 0) propertyFilter = null;
if (progressFunc === void 0) progressFunc = null;
/**
* Searches for the first occurrence of a sequence within a buffer.
* @example
* find(new Uint8Array([1, 2, 3, 4]), new Uint8Array([3, 4])); // 2
* @param {Uint8Array} buf - The buffer in which to search.
* @param {Uint8Array} search - The sequence to search for.
* @returns {number} The index of the first occurrence of the search sequence in the buffer, or -1 if not found.
*/ var find = (buf, search)=>{
var endIndex = buf.length - search.length;
var i, j;
for(i = 0; i <= endIndex; ++i){
for(j = 0; j < search.length; ++j){
if (buf[i + j] !== search[j]) {
break;
}
}
if (j === search.length) {
return i;
}
}
return -1;
};
/**
* Checks if array 'a' starts with the same elements as array 'b'.
* @example
* startsWith(new Uint8Array([1, 2, 3, 4]), new Uint8Array([1, 2])); // true
* @param {Uint8Array} a - The array to check against.
* @param {Uint8Array} b - The array of elements to look for at the start of 'a'.
* @returns {boolean} - True if 'a' starts with all elements of 'b', otherwise false.
*/ var startsWith = (a, b)=>{
if (a.length < b.length) {
return false;
}
for(var i = 0; i < b.length; ++i){
if (a[i] !== b[i]) {
return false;
}
}
return true;
};
var streamBuf = new StreamBuf(reader, progressFunc);
var headerLength;
while(true){
// get the next chunk of data
/* eslint-disable no-await-in-loop */ yield streamBuf.read();
// check magic bytes
if (streamBuf.tail >= magicBytes.length && !startsWith(streamBuf.data, magicBytes)) {
throw new Error('Invalid ply header');
}
// search for end-of-header marker
headerLength = find(streamBuf.data, endHeaderBytes);
if (headerLength !== -1) {
break;
}
}
// decode buffer header text and split into lines and remove comments
var lines = new TextDecoder('ascii').decode(streamBuf.data.subarray(0, headerLength)).split('\n');
// decode header and build element and property list
var { elements, format, comments } = parseHeader(lines);
// check format is supported
if (format !== 'binary_little_endian') {
throw new Error('Unsupported ply format');
}
// skip past header and compact the chunk data so the read operations
// fall nicely on aligned data boundaries
streamBuf.head = headerLength + endHeaderBytes.length;
streamBuf.compact();
var readData = /*#__PURE__*/ _async_to_generator(function*() {
// load compressed PLY with fast path
if (isCompressedPly(elements)) {
return yield readCompressedPly(streamBuf, elements);
}
// allocate element storage
elements.forEach((e)=>{
e.properties.forEach((p)=>{
var storageType = dataTypeMap.get(p.type);
if (storageType) {
var storage = !propertyFilter || propertyFilter(p.name) ? new storageType(e.count) : null;
p.storage = storage;
}
});
});
// load float32 PLY with fast path
if (isFloatPly(elements)) {
return yield readFloatPly(streamBuf, elements);
}
// fallback, general case
return yield readGeneralPly(streamBuf, elements);
});
return {
data: yield readData(),
comments
};
});
// by default load everything
var defaultElementFilter = (val)=>true;
class PlyParser {
/**
* @param {object} url - The URL of the resource to load.
* @param {string} url.load - The URL to use for loading the resource.
* @param {string} url.original - The original URL useful for identifying the resource type.
* @param {ResourceHandlerCallback} callback - The callback used when
* the resource is loaded or an error occurs.
* @param {Asset} asset - Container asset.
*/ load(url, callback, asset) {
var _this = this;
return _async_to_generator(function*() {
try {
var response = yield fetch(url.load);
if (!response || !response.body) {
callback('Error loading resource', null);
} else {
var _response_headers_get;
var totalLength = parseInt((_response_headers_get = response.headers.get('content-length')) != null ? _response_headers_get : '0', 10);
var totalReceived = 0;
var _asset_data_elementFilter;
var { data, comments } = yield readPly(response.body.getReader(), (_asset_data_elementFilter = asset.data.elementFilter) != null ? _asset_data_elementFilter : defaultElementFilter, (bytes)=>{
totalReceived += bytes;
if (asset) {
asset.fire('progress', totalReceived, totalLength);
}
});
// reorder data
if (!data.isCompressed) {
var _asset_data_reorder;
if ((_asset_data_reorder = asset.data.reorder) != null ? _asset_data_reorder : true) {
data.reorderData();
}
}
// construct the resource
var resource = new GSplatResource(_this.device, data.isCompressed && asset.data.decompress ? data.decompress() : data, comments);
callback(null, resource);
}
} catch (err) {
callback(err, null);
}
})();
}
/**
* @param {string} url - The URL.
* @param {GSplatResource} data - The data.
* @returns {GSplatResource} Return the data.
*/ open(url, data) {
return data;
}
/**
* @param {GraphicsDevice} device - The graphics device.
* @param {AssetRegistry} assets - The asset registry.
* @param {number} maxRetries - Maximum amount of retries.
*/ constructor(device, assets, maxRetries){
this.device = device;
this.assets = assets;
this.maxRetries = maxRetries;
}
}
export { PlyParser };