UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

517 lines (514 loc) 14.8 kB
import { GSplatData } from '../../scene/gsplat/gsplat-data.js'; import { GSplatCompressedData } from '../../scene/gsplat/gsplat-compressed-data.js'; import { GSplatCompressedResource } from '../../scene/gsplat/gsplat-compressed-resource.js'; import { GSplatResource } from '../../scene/gsplat/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, progressFunc){ this.head = 0; this.tail = 0; this.reader = reader; this.progressFunc = progressFunc; } async read() { const { value, done } = await this.reader.read(); if (done) { throw new Error('Stream finished before end of header'); } this.push(value); this.progressFunc?.(value.byteLength); } 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 = []; const comments = []; let format; for(let i = 1; i < lines.length; ++i){ const 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`); } 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, comments }; }; 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, comments)=>{ const result = new GSplatCompressedData(); result.comments = comments; 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; }; const storageSize = evalStorageSize(numVertices); result.numSplats = numVertices; result.chunkData = new Float32Array(numChunks * numChunkProperties); result.vertexData = new Uint32Array(storageSize * 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) { const texStorageSize = storageSize * 16; const shData0 = new Uint8Array(texStorageSize); const shData1 = new Uint8Array(texStorageSize); const shData2 = new Uint8Array(texStorageSize); const chunkSize = 1024; const srcCoeffs = elements[2].properties.length / 3; const tmpBuf = new Uint8Array(chunkSize * srcCoeffs * 3); for(let i = 0; i < result.numSplats; i += chunkSize){ const toRead = Math.min(chunkSize, result.numSplats - i); await read(tmpBuf.buffer, toRead * srcCoeffs * 3); for(let j = 0; j < toRead; ++j){ for(let k = 0; k < 15; ++k){ const 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; }; const readFloatPly = async (streamBuf, elements, comments)=>{ 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 = ()=>{ const buffer = streamBuf.data.buffer; if (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, comments); }; const readGeneralPly = async (streamBuf, elements, comments)=>{ 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, comments); }; const readPly = async (reader, propertyFilter = null, progressFunc = 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, progressFunc); 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'); const { elements, format, comments } = parseHeader(lines); if (format !== 'binary_little_endian') { throw new Error('Unsupported ply format'); } streamBuf.head = headerLength + endHeaderBytes.length; streamBuf.compact(); const readData = async ()=>{ if (isCompressedPly(elements)) { return await readCompressedPly(streamBuf, elements, comments); } 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, comments); } return await readGeneralPly(streamBuf, elements, comments); }; return await readData(); }; const defaultElementFilter = (val)=>true; class PlyParser { constructor(app, maxRetries){ this.app = app; this.maxRetries = maxRetries; } async load(url, callback, asset) { try { const response = await (asset.file?.contents ?? fetch(url.load)); if (!response || !response.body) { callback('Error loading resource', null); } else { const totalLength = parseInt(response.headers.get('content-length') ?? '0', 10); let totalReceived = 0; const data = await readPly(response.body.getReader(), asset.data.elementFilter ?? defaultElementFilter, (bytes)=>{ totalReceived += bytes; if (asset) { asset.fire('progress', totalReceived, totalLength); } }); asset.fire('load:data', data); if (!data.isCompressed) { if (asset.data.reorder ?? true) { data.reorderData(); } } const resource = data.isCompressed && !asset.data.decompress ? new GSplatCompressedResource(this.app.graphicsDevice, data) : new GSplatResource(this.app.graphicsDevice, data.isCompressed ? data.decompress() : data); callback(null, resource); } } catch (err) { callback(err, null); } } open(url, data) { return data; } } export { PlyParser };