UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

500 lines (499 loc) 16.4 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); 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 = /* @__PURE__ */ new Map([ ["char", Int8Array], ["uchar", Uint8Array], ["short", Int16Array], ["ushort", Uint16Array], ["int", Int32Array], ["uint", Uint32Array], ["float", Float32Array], ["double", Float64Array] ]); class StreamBuf { constructor(reader, progressFunc) { __publicField(this, "reader"); __publicField(this, "progressFunc"); __publicField(this, "data"); __publicField(this, "view"); __publicField(this, "head", 0); __publicField(this, "tail", 0); this.reader = reader; this.progressFunc = progressFunc; } // read the next chunk of data 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); } // append data to the buffer 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; } } } // 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() { 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 (streamBuf2, c2) => { p.storage[c2] = streamBuf2.getInt8(); }; case "uchar": return (streamBuf2, c2) => { p.storage[c2] = streamBuf2.getUint8(); }; case "short": return (streamBuf2, c2) => { p.storage[c2] = streamBuf2.getInt16(); }; case "ushort": return (streamBuf2, c2) => { p.storage[c2] = streamBuf2.getUint16(); }; case "int": return (streamBuf2, c2) => { p.storage[c2] = streamBuf2.getInt32(); }; case "uint": return (streamBuf2, c2) => { p.storage[c2] = streamBuf2.getUint32(); }; case "float": return (streamBuf2, c2) => { p.storage[c2] = streamBuf2.getFloat32(); }; case "double": return (streamBuf2, c2) => { p.storage[c2] = streamBuf2.getFloat64(); }; default: throw new Error(`Unsupported property data type '${p.type}' in ply header`); } } else { return (streamBuf2) => { streamBuf2.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 { /** * @param {AppBase} app - The app instance. * @param {number} maxRetries - Maximum amount of retries. */ constructor(app, maxRetries) { /** @type {AppBase} */ __publicField(this, "app"); /** @type {number} */ __publicField(this, "maxRetries"); this.app = app; this.maxRetries = maxRetries; } /** * @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. */ async load(url, callback, asset) { const gsplatCentersEnabledAtLoad = this.app.scene?.gsplatCentersEnabled !== false; 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 prepareCenters = gsplatCentersEnabledAtLoad; const resource = data.isCompressed && !asset.data.decompress ? new GSplatCompressedResource(this.app.graphicsDevice, data, { prepareCenters }) : new GSplatResource(this.app.graphicsDevice, data.isCompressed ? data.decompress() : data, { prepareCenters }); 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; } } export { PlyParser };