@animech-public/playcanvas
Version:
PlayCanvas WebGL game engine
399 lines (396 loc) • 12.1 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';
const magicBytes = new Uint8Array([112, 108, 121, 10]);
const endHeaderBytes = new Uint8Array([10, 101, 110, 100, 95, 104, 101, 97, 100, 101, 114, 10]);
const dataTypeMap = new Map([['char', Int8Array], ['uchar', Uint8Array], ['short', Int16Array], ['ushort', Uint16Array], ['int', Int32Array], ['uint', Uint32Array], ['float', Float32Array], ['double', Float64Array]]);
class StreamBuf {
constructor(reader) {
this.reader = void 0;
this.data = void 0;
this.view = void 0;
this.head = 0;
this.tail = 0;
this.reader = reader;
}
async read() {
const {
value,
done
} = await this.reader.read();
if (done) {
throw new Error('Stream finished before end of header');
}
this.push(value);
}
push(data) {
if (!this.data) {
this.data = data;
this.view = new DataView(this.data.buffer);
this.tail = data.length;
} else {
const remaining = this.tail - this.head;
const newSize = remaining + data.length;
if (this.data.length >= newSize) {
if (this.head > 0) {
this.data.copyWithin(0, this.head, this.tail);
this.data.set(data, remaining);
this.head = 0;
this.tail = newSize;
} else {
this.data.set(data, this.tail);
this.tail += data.length;
}
} else {
const tmp = new Uint8Array(newSize);
if (this.head > 0 || this.tail < this.data.length) {
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;
}
}
}
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;
}
getInt8() {
const result = this.view.getInt8(this.head);
this.head++;
return result;
}
getUint8() {
const result = this.view.getUint8(this.head);
this.head++;
return result;
}
getInt16() {
const result = this.view.getInt16(this.head, true);
this.head += 2;
return result;
}
getUint16() {
const result = this.view.getUint16(this.head, true);
this.head += 2;
return result;
}
getInt32() {
const result = this.view.getInt32(this.head, true);
this.head += 4;
return result;
}
getUint32() {
const result = this.view.getUint32(this.head, true);
this.head += 4;
return result;
}
getFloat32() {
const result = this.view.getFloat32(this.head, true);
this.head += 4;
return result;
}
getFloat64() {
const result = this.view.getFloat64(this.head, true);
this.head += 8;
return result;
}
}
const parseHeader = lines => {
const elements = [];
let format;
for (let i = 1; i < lines.length; ++i) {
const words = lines[i].split(' ');
switch (words[0]) {
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`);
}
const 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
};
};
const isCompressedPly = elements => {
const 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'];
const vertexProperties = ['packed_position', 'packed_rotation', 'packed_scale', 'packed_color'];
const shProperties = new Array(45).fill('').map((_, i) => `f_rest_${i}`);
const 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');
};
const 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();
};
const isFloatPly = elements => {
return elements.length === 1 && elements[0].name === 'vertex' && elements[0].properties.every(p => p.type === 'float');
};
const readCompressedPly = async (streamBuf, elements, littleEndian) => {
const result = new GSplatCompressedData();
const numChunks = elements[0].count;
const numChunkProperties = elements[0].properties.length;
const numVertices = elements[1].count;
const evalStorageSize = count => {
const width = Math.ceil(Math.sqrt(count));
const height = Math.ceil(count / width);
return width * height;
};
result.numSplats = numVertices;
result.chunkData = new Float32Array(numChunks * numChunkProperties);
result.vertexData = new Uint32Array(evalStorageSize(numVertices) * 4);
const read = async (buffer, length) => {
const target = new Uint8Array(buffer);
let cursor = 0;
while (cursor < length) {
while (streamBuf.remaining === 0) {
await streamBuf.read();
}
const toCopy = Math.min(length - cursor, streamBuf.remaining);
const src = streamBuf.data;
for (let i = 0; i < toCopy; ++i) {
target[cursor++] = src[streamBuf.head++];
}
}
};
await read(result.chunkData.buffer, numChunks * numChunkProperties * 4);
await read(result.vertexData.buffer, numVertices * 4 * 4);
if (elements.length === 3) {
result.shData = new Uint8Array(elements[2].count * elements[2].properties.length);
await read(result.shData.buffer, result.shData.byteLength);
}
return result;
};
const readFloatPly = async (streamBuf, elements, littleEndian) => {
const element = elements[0];
const properties = element.properties;
const numProperties = properties.length;
const storage = properties.map(p => p.storage);
const inputSize = properties.reduce((a, p) => a + p.byteSize, 0);
let vertexIdx = 0;
let floatData;
const checkFloatData = () => {
var _floatData;
const buffer = streamBuf.data.buffer;
if (((_floatData = floatData) == null ? void 0 : _floatData.buffer) !== buffer) {
floatData = new Float32Array(buffer, 0, buffer.byteLength / 4);
}
};
checkFloatData();
while (vertexIdx < element.count) {
while (streamBuf.remaining < inputSize) {
await streamBuf.read();
checkFloatData();
}
const toRead = Math.min(element.count - vertexIdx, Math.floor(streamBuf.remaining / inputSize));
for (let j = 0; j < numProperties; ++j) {
const s = storage[j];
for (let n = 0; n < toRead; ++n) {
s[n + vertexIdx] = floatData[n * numProperties + j];
}
}
vertexIdx += toRead;
streamBuf.head += toRead * inputSize;
}
return new GSplatData(elements);
};
const readGeneralPly = async (streamBuf, elements, littleEndian) => {
for (let i = 0; i < elements.length; ++i) {
const element = elements[i];
const inputSize = element.properties.reduce((a, p) => a + p.byteSize, 0);
const propertyParsingFunctions = element.properties.map(p => {
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;
};
}
});
let c = 0;
while (c < element.count) {
while (streamBuf.remaining < inputSize) {
await streamBuf.read();
}
const toRead = Math.min(element.count - c, Math.floor(streamBuf.remaining / inputSize));
for (let n = 0; n < toRead; ++n) {
for (let j = 0; j < element.properties.length; ++j) {
propertyParsingFunctions[j](streamBuf, c);
}
c++;
}
}
}
return new GSplatData(elements);
};
const readPly = async (reader, propertyFilter = null) => {
const find = (buf, search) => {
const endIndex = buf.length - search.length;
let 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;
};
const startsWith = (a, b) => {
if (a.length < b.length) {
return false;
}
for (let i = 0; i < b.length; ++i) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
};
const streamBuf = new StreamBuf(reader);
let headerLength;
while (true) {
await streamBuf.read();
if (streamBuf.tail >= magicBytes.length && !startsWith(streamBuf.data, magicBytes)) {
throw new Error('Invalid ply header');
}
headerLength = find(streamBuf.data, endHeaderBytes);
if (headerLength !== -1) {
break;
}
}
const lines = new TextDecoder('ascii').decode(streamBuf.data.subarray(0, headerLength)).split('\n').filter(line => !line.startsWith('comment '));
const {
elements,
format
} = parseHeader(lines);
if (format !== 'binary_little_endian' && format !== 'binary_big_endian') {
throw new Error('Unsupported ply format');
}
streamBuf.head = headerLength + endHeaderBytes.length;
streamBuf.compact();
if (isCompressedPly(elements)) {
return await readCompressedPly(streamBuf, elements);
}
elements.forEach(e => {
e.properties.forEach(p => {
const storageType = dataTypeMap.get(p.type);
if (storageType) {
const storage = !propertyFilter || propertyFilter(p.name) ? new storageType(e.count) : null;
p.storage = storage;
}
});
});
if (isFloatPly(elements)) {
return await readFloatPly(streamBuf, elements);
}
return await readGeneralPly(streamBuf, elements);
};
const defaultElementFilter = val => true;
class PlyParser {
constructor(device, assets, maxRetries) {
this.device = void 0;
this.assets = void 0;
this.maxRetries = void 0;
this.device = device;
this.assets = assets;
this.maxRetries = maxRetries;
}
async load(url, callback, asset) {
const response = await fetch(url.load);
if (!response || !response.body) {
callback('Error loading resource', null);
} else {
var _asset$data$elementFi;
readPly(response.body.getReader(), (_asset$data$elementFi = asset.data.elementFilter) != null ? _asset$data$elementFi : defaultElementFilter).then(gsplatData => {
if (!gsplatData.isCompressed) {
var _asset$data$reorder;
if ((_asset$data$reorder = asset.data.reorder) != null ? _asset$data$reorder : true) {
gsplatData.reorderData();
}
}
const resource = new GSplatResource(this.device, gsplatData.isCompressed && asset.data.decompress ? gsplatData.decompress() : gsplatData);
callback(null, resource);
}).catch(err => {
callback(err, null);
});
}
}
open(url, data) {
return data;
}
}
export { PlyParser };