UNPKG

@animech-public/playcanvas

Version:
399 lines (396 loc) 12.1 kB
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 };