@voluma/vlam 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/THIRD_PARTY_NOTICES.md +86 -0
- package/dist/budget-governor.d.ts +154 -0
- package/dist/camera-budget-governor.d.ts +234 -0
- package/dist/chunk-cache-budget.d.ts +156 -0
- package/dist/chunk-fetch-scheduler.d.ts +168 -0
- package/dist/chunk-loader.d.ts +85 -0
- package/dist/compute-sorter.d.ts +131 -0
- package/dist/create-splat-renderer.d.ts +99 -0
- package/dist/dataset-source.d.ts +55 -0
- package/dist/depth-of-field.d.ts +73 -0
- package/dist/effects.d.ts +337 -0
- package/dist/effects.js +180 -0
- package/dist/effects.js.map +1 -0
- package/dist/formats/ksplat/index.d.ts +8 -0
- package/dist/formats/ksplat/parse-ksplat.d.ts +9 -0
- package/dist/formats/ksplat.js +167 -0
- package/dist/formats/ksplat.js.map +1 -0
- package/dist/formats/lcc/collision-mesh.d.ts +37 -0
- package/dist/formats/lcc/collision-partition.d.ts +35 -0
- package/dist/formats/lcc/index.d.ts +16 -0
- package/dist/formats/lcc/lcc.d.ts +28 -0
- package/dist/formats/lcc/lcc2-transform.d.ts +18 -0
- package/dist/formats/lcc/lcc2.d.ts +4 -0
- package/dist/formats/lcc/parse-collision-lci.d.ts +28 -0
- package/dist/formats/lcc/parse-lcc.d.ts +177 -0
- package/dist/formats/lcc/parse-mesh-ply.d.ts +21 -0
- package/dist/formats/lcc.js +969 -0
- package/dist/formats/lcc.js.map +1 -0
- package/dist/formats/ply/index.d.ts +12 -0
- package/dist/formats/ply/parse-compressed-ply.d.ts +16 -0
- package/dist/formats/ply/parse-splat-ply.d.ts +46 -0
- package/dist/formats/ply.js +230 -0
- package/dist/formats/ply.js.map +1 -0
- package/dist/formats/rad/frontier-worker-protocol.d.ts +177 -0
- package/dist/formats/rad/index.d.ts +10 -0
- package/dist/formats/rad/parse-rad.d.ts +88 -0
- package/dist/formats/rad/rad-column-decoders.d.ts +15 -0
- package/dist/formats/rad/rad-foveated-source.d.ts +71 -0
- package/dist/formats/rad/rad-parent-sizes.d.ts +45 -0
- package/dist/formats/rad/rad.d.ts +132 -0
- package/dist/formats/rad.js +1098 -0
- package/dist/formats/rad.js.map +1 -0
- package/dist/formats/sog/index.d.ts +9 -0
- package/dist/formats/sog/parse-sog.d.ts +45 -0
- package/dist/formats/sog.js +257 -0
- package/dist/formats/sog.js.map +1 -0
- package/dist/formats/splat/index.d.ts +8 -0
- package/dist/formats/splat/parse-splat.d.ts +10 -0
- package/dist/formats/splat.js +39 -0
- package/dist/formats/splat.js.map +1 -0
- package/dist/formats/spz/index.d.ts +8 -0
- package/dist/formats/spz/parse-spz.d.ts +10 -0
- package/dist/formats/spz.js +218 -0
- package/dist/formats/spz.js.map +1 -0
- package/dist/frontier-worker-CAnK_Cmu.js +566 -0
- package/dist/frontier-worker-CAnK_Cmu.js.map +1 -0
- package/dist/half-float-Cs4FDq8Q.js +30 -0
- package/dist/half-float-Cs4FDq8Q.js.map +1 -0
- package/dist/half-float.d.ts +15 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +9054 -0
- package/dist/index.js.map +1 -0
- package/dist/load-scene.d.ts +50 -0
- package/dist/load-worker-protocol.d.ts +82 -0
- package/dist/loading-wLo8vRbA.js +102 -0
- package/dist/loading-wLo8vRbA.js.map +1 -0
- package/dist/loading.d.ts +150 -0
- package/dist/lod-manifest.d.ts +61 -0
- package/dist/lod-scheduler.d.ts +229 -0
- package/dist/lod-source.d.ts +207 -0
- package/dist/logging-BfPdd7NJ.js +19 -0
- package/dist/logging-BfPdd7NJ.js.map +1 -0
- package/dist/logging.d.ts +37 -0
- package/dist/one-shot-worker-G80btnth.js +664 -0
- package/dist/one-shot-worker-G80btnth.js.map +1 -0
- package/dist/orientation.d.ts +35 -0
- package/dist/ply-header-BwgExCn-.js +125 -0
- package/dist/ply-header-BwgExCn-.js.map +1 -0
- package/dist/ply-header.d.ts +87 -0
- package/dist/radix-sort.d.ts +28 -0
- package/dist/radix-sorter.d.ts +46 -0
- package/dist/relighting.d.ts +50 -0
- package/dist/selection-volume.d.ts +87 -0
- package/dist/sh-pack-D5wAe5gg.js +54 -0
- package/dist/sh-pack-D5wAe5gg.js.map +1 -0
- package/dist/sh-pack.d.ts +49 -0
- package/dist/sort-scheduler.d.ts +62 -0
- package/dist/sort-worker-protocol.d.ts +37 -0
- package/dist/sorter.d.ts +31 -0
- package/dist/source-transform.d.ts +57 -0
- package/dist/splat-budget-PSojLJPO.js +157 -0
- package/dist/splat-budget-PSojLJPO.js.map +1 -0
- package/dist/splat-budget.d.ts +404 -0
- package/dist/splat-data-BZM5dhhx.js +10 -0
- package/dist/splat-data-BZM5dhhx.js.map +1 -0
- package/dist/splat-data.d.ts +150 -0
- package/dist/splat-depth-pack.d.ts +39 -0
- package/dist/splat-mesh-material.d.ts +255 -0
- package/dist/splat-mesh-picking.d.ts +87 -0
- package/dist/splat-mesh-pool.d.ts +241 -0
- package/dist/splat-mesh-types-8QDjTbZO.js +390 -0
- package/dist/splat-mesh-types-8QDjTbZO.js.map +1 -0
- package/dist/splat-mesh-types.d.ts +390 -0
- package/dist/splat-mesh.d.ts +737 -0
- package/dist/splat-modifier-stack.d.ts +38 -0
- package/dist/splat-modifier.d.ts +156 -0
- package/dist/splat-partition.d.ts +37 -0
- package/dist/splat-query.d.ts +51 -0
- package/dist/splat-scene.d.ts +127 -0
- package/dist/splat-sort-bounds.d.ts +11 -0
- package/dist/static-lod-splat-mesh.d.ts +53 -0
- package/dist/static-lod-worker-D2fyqQZO.js +474 -0
- package/dist/static-lod-worker-D2fyqQZO.js.map +1 -0
- package/dist/static-lod-worker-protocol.d.ts +37 -0
- package/dist/static-lod.d.ts +24 -0
- package/dist/storage-attribute-mirror.d.ts +103 -0
- package/dist/streamed-splat-mesh-utils.d.ts +146 -0
- package/dist/streamed-splat-mesh.d.ts +985 -0
- package/dist/unified-splat-renderer.d.ts +205 -0
- package/dist/webgpu-limits.d.ts +133 -0
- package/dist/work-buffer-gather.d.ts +119 -0
- package/dist/work-buffer-material.d.ts +42 -0
- package/dist/worker-sorter.d.ts +57 -0
- package/dist/xr-view.d.ts +92 -0
- package/package.json +135 -21
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ply.js","sources":["../../src/lib/formats/ply/parse-compressed-ply.ts","../../src/lib/formats/ply/parse-splat-ply.ts"],"sourcesContent":["import { writeCovariance, type SplatData, type SplatPackedShData } from '../../splat-data';\nimport {\n assertPlyElementFits,\n assertPlyPropertyTypes,\n plyPropertyOffset,\n type PlyHeader,\n} from '../../ply-header';\n\n/**\n * Decoder for the compressed PLY flavor written by SuperSplat / PlayCanvas\n * (and by `@playcanvas/splat-transform -c`). It is the same container as the\n * uncompressed 3DGS PLY but quantized ~10× smaller:\n *\n * - `element chunk` - one record per 256 splats, holding the float min/max\n * bounds those splats' quantized values are interpolated across.\n * - `element vertex` - four `uint32` per splat: position and scale packed\n * 11-10-11, rotation as smallest-three (2-bit selector + 3×10 bits), and\n * color as RGBA8.\n * - `element sh` - optional per-splat higher-order SH, one `uchar` per\n * coefficient per channel, channel-major, quantized across [-4, 4].\n *\n * Spec: PlayCanvas's `GSplatCompressedData` is the reference implementation\n * (MIT). The decode below matches its unpacking and dequantization; the\n * implementation is original.\n */\n\n/** Splats per `chunk` record - fixed by the format. */\nconst SPLATS_PER_CHUNK = 256;\n\n/** Chunk bounds every compressed PLY carries. */\nconst CHUNK_BOUNDS = [\n 'min_x',\n 'min_y',\n 'min_z',\n 'max_x',\n 'max_y',\n 'max_z',\n 'min_scale_x',\n 'min_scale_y',\n 'min_scale_z',\n 'max_scale_x',\n 'max_scale_y',\n 'max_scale_z',\n] as const;\n\n/** Optional per-chunk color bounds (absent in the format's earliest writers). */\nconst CHUNK_COLOR_BOUNDS = ['min_r', 'min_g', 'min_b', 'max_r', 'max_g', 'max_b'] as const;\n\n/** The four packed words that make a compressed vertex. */\nconst VERTEX_PROPERTIES = [\n 'packed_position',\n 'packed_rotation',\n 'packed_scale',\n 'packed_color',\n] as const;\n\n/** SH coefficient count per channel, indexed by band. */\nconst SH_COEFFICIENTS: Record<number, number> = { 1: 3, 2: 8, 3: 15 };\n\n/** Bytes the `sh` element's uchar values dequantize across. */\nconst SH_MIN = -4;\nconst SH_MAX = 4;\n\n/**\n * Whether this PLY is the compressed flavor rather than the uncompressed\n * 3DGS one. Keyed on the packed vertex properties, which no uncompressed\n * export has.\n */\nexport function isCompressedPly(header: PlyHeader): boolean {\n const vertex = header.element('vertex');\n return vertex !== undefined && vertex.properties.has('packed_position');\n}\n\nfunction unpackUnorm(value: number, bits: number): number {\n const max = (1 << bits) - 1;\n return (value & max) / max;\n}\n\nfunction lerp(min: number, max: number, t: number): number {\n return min + (max - min) * t;\n}\n\n/**\n * Parses a compressed splat PLY into flat, GPU-friendly arrays.\n *\n * @param header - The already-parsed header (its element offsets locate the\n * chunk, vertex and sh records).\n * @throws {Error} if the chunk/vertex layout is not the compressed one.\n */\nexport function parseCompressedPly(buffer: ArrayBuffer, header: PlyHeader): SplatData {\n const chunks = header.element('chunk');\n const vertices = header.element('vertex');\n if (!chunks || !vertices) {\n throw new Error('Compressed PLY is missing its \"chunk\" or \"vertex\" element.');\n }\n assertPlyPropertyTypes(vertices, VERTEX_PROPERTIES, ['uint', 'uint32'], 'Compressed PLY vertex');\n assertPlyPropertyTypes(chunks, CHUNK_BOUNDS, ['float', 'float32'], 'Compressed PLY chunk');\n // Color bounds arrived later in the format's life; without them the packed\n // color is already the final value (the reference skips the lerp).\n const hasColorBounds = chunks.properties.has('min_r');\n if (hasColorBounds) {\n assertPlyPropertyTypes(\n chunks,\n CHUNK_COLOR_BOUNDS,\n ['float', 'float32'],\n 'Compressed PLY chunk',\n );\n }\n\n assertPlyElementFits(chunks, buffer.byteLength);\n assertPlyElementFits(vertices, buffer.byteLength);\n\n const count = vertices.count;\n const neededChunks = Math.ceil(count / SPLATS_PER_CHUNK);\n if (chunks.count < neededChunks) {\n throw new Error(\n `Compressed PLY declares ${chunks.count} chunks, too few for ${count} splats ` +\n `(needs ${neededChunks} at ${SPLATS_PER_CHUNK} splats per chunk).`,\n );\n }\n\n const chunkView = new DataView(buffer, chunks.offset);\n const vertexView = new DataView(buffer, vertices.offset);\n const chunkAt = (chunk: number, property: string): number =>\n chunkView.getFloat32(chunk * chunks.stride + plyPropertyOffset(chunks, property), true);\n\n const positionOffset = plyPropertyOffset(vertices, 'packed_position');\n const rotationOffset = plyPropertyOffset(vertices, 'packed_rotation');\n const scaleOffset = plyPropertyOffset(vertices, 'packed_scale');\n const colorOffset = plyPropertyOffset(vertices, 'packed_color');\n\n const positions = new Float32Array(count * 3);\n const colors = new Uint8Array(count * 4);\n const covariances = new Float32Array(count * 6);\n\n for (let chunk = 0; chunk < neededChunks; chunk++) {\n // Chunk bounds are read once per 256 splats rather than per splat.\n const minX = chunkAt(chunk, 'min_x'),\n minY = chunkAt(chunk, 'min_y'),\n minZ = chunkAt(chunk, 'min_z');\n const maxX = chunkAt(chunk, 'max_x'),\n maxY = chunkAt(chunk, 'max_y'),\n maxZ = chunkAt(chunk, 'max_z');\n const minSx = chunkAt(chunk, 'min_scale_x'),\n minSy = chunkAt(chunk, 'min_scale_y'),\n minSz = chunkAt(chunk, 'min_scale_z');\n const maxSx = chunkAt(chunk, 'max_scale_x'),\n maxSy = chunkAt(chunk, 'max_scale_y'),\n maxSz = chunkAt(chunk, 'max_scale_z');\n const minR = hasColorBounds ? chunkAt(chunk, 'min_r') : 0;\n const minG = hasColorBounds ? chunkAt(chunk, 'min_g') : 0;\n const minB = hasColorBounds ? chunkAt(chunk, 'min_b') : 0;\n const maxR = hasColorBounds ? chunkAt(chunk, 'max_r') : 1;\n const maxG = hasColorBounds ? chunkAt(chunk, 'max_g') : 1;\n const maxB = hasColorBounds ? chunkAt(chunk, 'max_b') : 1;\n\n const first = chunk * SPLATS_PER_CHUNK;\n const last = Math.min(first + SPLATS_PER_CHUNK, count);\n for (let i = first; i < last; i++) {\n const base = i * vertices.stride;\n\n // Position: 11-10-11 unorm, interpolated across the chunk's bounds.\n const packedPosition = vertexView.getUint32(base + positionOffset, true);\n positions[i * 3 + 0] = lerp(minX, maxX, unpackUnorm(packedPosition >>> 21, 11));\n positions[i * 3 + 1] = lerp(minY, maxY, unpackUnorm(packedPosition >>> 11, 10));\n positions[i * 3 + 2] = lerp(minZ, maxZ, unpackUnorm(packedPosition, 11));\n\n // Color: RGB is the base color in 0..1 (the 0.5 + Y₀₀·f_dc the\n // uncompressed flavor stores unevaluated), alpha the already-activated\n // opacity - so neither needs the PLY parser's SH/sigmoid conversion.\n const packedColor = vertexView.getUint32(base + colorOffset, true);\n const r = unpackUnorm(packedColor >>> 24, 8);\n const g = unpackUnorm(packedColor >>> 16, 8);\n const b = unpackUnorm(packedColor >>> 8, 8);\n const a = unpackUnorm(packedColor, 8);\n colors[i * 4 + 0] = toByte(hasColorBounds ? lerp(minR, maxR, r) : r);\n colors[i * 4 + 1] = toByte(hasColorBounds ? lerp(minG, maxG, g) : g);\n colors[i * 4 + 2] = toByte(hasColorBounds ? lerp(minB, maxB, b) : b);\n colors[i * 4 + 3] = toByte(a);\n\n // Scale: 11-10-11 unorm across the chunk's bounds, in log space.\n const packedScale = vertexView.getUint32(base + scaleOffset, true);\n const scaleX = Math.exp(lerp(minSx, maxSx, unpackUnorm(packedScale >>> 21, 11)));\n const scaleY = Math.exp(lerp(minSy, maxSy, unpackUnorm(packedScale >>> 11, 10)));\n const scaleZ = Math.exp(lerp(minSz, maxSz, unpackUnorm(packedScale, 11)));\n\n // Rotation: \"smallest three\". The 2-bit selector says which component\n // was dropped (the largest, rebuilt from unit length); the other three\n // are 10-bit unorms over [-√½, +√½]. Component order is (w, x, y, z) -\n // the same convention the SOG decoder uses.\n const packedRotation = vertexView.getUint32(base + rotationOffset, true);\n const q0 = (unpackUnorm(packedRotation >>> 20, 10) - 0.5) * Math.SQRT2;\n const q1 = (unpackUnorm(packedRotation >>> 10, 10) - 0.5) * Math.SQRT2;\n const q2 = (unpackUnorm(packedRotation, 10) - 0.5) * Math.SQRT2;\n const largest = Math.sqrt(Math.max(0, 1 - (q0 * q0 + q1 * q1 + q2 * q2)));\n const [qw, qx, qy, qz] =\n packedRotation >>> 30 === 0\n ? [largest, q0, q1, q2]\n : packedRotation >>> 30 === 1\n ? [q0, largest, q1, q2]\n : packedRotation >>> 30 === 2\n ? [q0, q1, largest, q2]\n : [q0, q1, q2, largest];\n\n writeCovariance(covariances, i, scaleX, scaleY, scaleZ, qw, qx, qy, qz);\n }\n }\n\n const shPacked = decodeSh(buffer, header, count);\n return { count, positions, colors, covariances, ...(shPacked ? { shPacked } : {}) };\n}\n\nfunction toByte(value: number): number {\n return Math.max(0, Math.min(255, Math.round(value * 255)));\n}\n\n/**\n * Decodes the optional `sh` element into the per-splat packed form.\n *\n * The file stores one uchar per (channel, coefficient), channel-major:\n * `f_rest_0…n-1` are red, then green, then blue. The packed form is\n * coefficient-major with all three channels in one word (11-10-11), so this\n * transposes as it goes. Both quantizations are unit fractions of the same\n * [-4, 4] range, so re-quantizing 8-bit into 10/11-bit fields is exact to\n * well within the source's own resolution.\n */\nfunction decodeSh(\n buffer: ArrayBuffer,\n header: PlyHeader,\n count: number,\n): SplatPackedShData | undefined {\n const sh = header.element('sh');\n if (!sh || sh.count === 0) return undefined;\n if (sh.count !== count) {\n throw new Error(`Compressed PLY \"sh\" element has ${sh.count} records for ${count} splats.`);\n }\n\n const coefficients = sh.propertyNames.length / 3;\n const bands = Number(\n Object.keys(SH_COEFFICIENTS).find((band) => SH_COEFFICIENTS[Number(band)] === coefficients),\n );\n if (!bands) {\n throw new Error(\n `Compressed PLY \"sh\" element has ${sh.propertyNames.length} properties, which is not ` +\n '3, 8 or 15 coefficients across three channels.',\n );\n }\n const names = Array.from({ length: coefficients * 3 }, (_, i) => `f_rest_${i}`);\n assertPlyPropertyTypes(sh, names, ['uchar', 'uint8'], 'Compressed PLY sh');\n assertPlyElementFits(sh, buffer.byteLength);\n\n const bytes = new Uint8Array(buffer, sh.offset);\n const offsets = names.map((name) => plyPropertyOffset(sh, name));\n const packed = new Uint32Array(count * coefficients);\n for (let i = 0; i < count; i++) {\n const base = i * sh.stride;\n for (let c = 0; c < coefficients; c++) {\n // Channel-major in the file: red coefficients, then green, then blue.\n const r = (bytes[base + (offsets[c] as number)] as number) / 255;\n const g = (bytes[base + (offsets[coefficients + c] as number)] as number) / 255;\n const b = (bytes[base + (offsets[2 * coefficients + c] as number)] as number) / 255;\n packed[i * coefficients + c] =\n (Math.round(r * 2047) | (Math.round(g * 1023) << 11) | (Math.round(b * 2047) << 21)) >>> 0;\n }\n }\n\n return {\n bands: bands as 1 | 2 | 3,\n packed,\n range: { min: [SH_MIN, SH_MIN, SH_MIN], max: [SH_MAX, SH_MAX, SH_MAX] },\n };\n}\n","import { packShCoefficients } from '../../sh-pack';\nimport { SH_C0, writeCovariance, type SplatData } from '../../splat-data';\nimport { isCompressedPly, parseCompressedPly } from './parse-compressed-ply';\nimport { readWholeFile, type SplatProgressCallback } from '../../loading';\nimport {\n assertPlyElementFits,\n assertPlyPropertyTypes,\n parsePlyHeader,\n plyPropertyOffset,\n type PlyElement,\n} from '../../ply-header';\n\n/**\n * Parser for the \"INRIA flavor\" Gaussian splat PLY format: a binary\n * little-endian PLY whose vertices carry position, spherical-harmonics\n * color (f_dc_*), opacity, scale and rotation properties.\n *\n * `.ply` is a container rather than one format, so {@link parseSplatPly} also\n * dispatches to the compressed flavor (see `parse-compressed-ply.ts`), which\n * SuperSplat and `@playcanvas/splat-transform -c` write.\n *\n * Only the math from the 3DGS paper (Kerbl et al., SIGGRAPH 2023) is used\n * here; the implementation is original.\n */\n\n/** Property names this parser requires, in no particular order. */\nconst REQUIRED_PROPERTIES = [\n 'x',\n 'y',\n 'z',\n 'f_dc_0',\n 'f_dc_1',\n 'f_dc_2',\n 'opacity',\n 'scale_0',\n 'scale_1',\n 'scale_2',\n 'rot_0',\n 'rot_1',\n 'rot_2',\n 'rot_3',\n] as const;\n\n/** Higher-order SH coefficient counts per band (channel-major `f_rest_*`). */\nconst REST_COEFFICIENTS: Record<number, 1 | 2 | 3> = { 3: 1, 8: 2, 15: 3 };\n\nfunction sigmoid(x: number): number {\n return 1 / (1 + Math.exp(-x));\n}\n\n/**\n * Parses a binary Gaussian splat PLY file into flat, GPU-friendly arrays -\n * either the uncompressed 3DGS flavor or the compressed SuperSplat one, which\n * is detected from the header.\n *\n * For the uncompressed flavor the spherical harmonics DC term is baked into an\n * 8-bit RGB color; optional `f_rest_*` higher-order coefficients are packed\n * into per-splat `shPacked`. Opacity is passed through a sigmoid, and per-splat\n * (log-space) + rotation (quaternion) are folded into a 3D covariance matrix -\n * all per the 3DGS paper's parameterization.\n *\n * @throws {Error} if the file is not a binary little-endian splat PLY.\n *\n * A direct parser call sits outside the `SplatLoadError`-or-`AbortError`\n * contract of the worker-mediated loaders ({@link loadScene},\n * {@link loadSceneFile}, `ChunkLoader`, `StreamedSplatMesh`): malformed input\n * surfaces here as a plain `Error`.\n */\nexport function parseSplatPly(buffer: ArrayBuffer): SplatData {\n const header = parsePlyHeader(buffer);\n if (isCompressedPly(header)) return parseCompressedPly(buffer, header);\n\n const vertices = requireSplatVertexElement(header);\n assertPlyElementFits(vertices, buffer.byteLength);\n\n const out = allocate(vertices.count);\n const rest = restLayout(vertices);\n const coefficients = rest ? new Float32Array(vertices.count * rest.coefficients * 3) : null;\n decodeRecords(\n new DataView(buffer, vertices.offset),\n vertexOffsets(vertices),\n vertices.stride,\n 0,\n 0,\n vertices.count,\n out,\n );\n if (rest && coefficients) {\n decodeRestRecords(\n new DataView(buffer, vertices.offset),\n vertices.stride,\n 0,\n 0,\n vertices.count,\n rest,\n coefficients,\n );\n }\n return finalizePlyDecode(vertices.count, out, rest, coefficients);\n}\n\n/**\n * Bytes read per slice while streaming. Large enough that a multi-gigabyte\n * file costs tens of reads rather than thousands, small enough to stay far\n * below the 2 GiB an ArrayBuffer can hold.\n */\nconst STREAM_WINDOW_BYTES = 64 * 1024 * 1024;\n\n/** Bytes of a PLY that the header is assumed to fit inside. */\nconst HEADER_WINDOW_BYTES = 64 * 1024;\n\n/**\n * Parses a local splat PLY without ever holding the whole file.\n *\n * A browser cannot allocate an ArrayBuffer over 2 GiB, so `file.arrayBuffer()`\n * fails outright on a raw 3DGS export of a few million splats (a full-SH\n * `point_cloud.ply` runs ~248 bytes per splat). The uncompressed flavor has\n * fixed-stride records, so it is read a window at a time. Higher-order\n * `f_rest_*` coefficients ride in the same stride and are decoded per window.\n *\n * The compressed flavor is read whole: it is ~10× smaller, and its chunked\n * layout would need a second pass to stream for no practical gain.\n *\n * @param options.windowBytes - Bytes read per slice; defaults to 64 MiB. Lower\n * trades read count for peak memory.\n * @param options.onProgress - Called after each window with the vertex bytes\n * decoded so far and the total. A multi-gigabyte decode takes tens of seconds,\n * which needs a determinate indicator rather than a spinner.\n * @throws {Error} if the file is not a binary little-endian splat PLY;\n * a `DOMException` named `AbortError` when cancelled.\n */\nexport async function parseSplatPlyFile(\n file: File,\n options: { signal?: AbortSignal; windowBytes?: number; onProgress?: SplatProgressCallback } = {},\n): Promise<SplatData> {\n const { signal, onProgress } = options;\n const windowBytes = options.windowBytes ?? STREAM_WINDOW_BYTES;\n const head = await file.slice(0, Math.min(file.size, HEADER_WINDOW_BYTES)).arrayBuffer();\n const header = parsePlyHeader(head);\n if (isCompressedPly(header)) return parseCompressedPly(await readWholeFile(file), header);\n\n const vertices = requireSplatVertexElement(header);\n assertPlyElementFits(vertices, file.size);\n signal?.throwIfAborted();\n\n const count = vertices.count;\n const stride = vertices.stride;\n const out = allocate(count);\n const offsets = vertexOffsets(vertices);\n const rest = restLayout(vertices);\n const coefficients = rest ? new Float32Array(count * rest.coefficients * 3) : null;\n // At least one whole record per read: a window never splits a record, so\n // the decoder always sees complete ones.\n const perWindow = Math.max(1, Math.floor(windowBytes / stride));\n\n const totalBytes = count * stride;\n onProgress?.(0, totalBytes);\n for (let first = 0; first < count; first += perWindow) {\n signal?.throwIfAborted();\n const last = Math.min(first + perWindow, count);\n const slice = file.slice(vertices.offset + first * stride, vertices.offset + last * stride);\n const view = new DataView(await slice.arrayBuffer());\n // The window's byte 0 is record `first`, so the decoder rebases onto it.\n decodeRecords(view, offsets, stride, first, first, last, out);\n if (rest && coefficients)\n decodeRestRecords(view, stride, first, first, last, rest, coefficients);\n onProgress?.(last * stride, totalBytes);\n }\n return finalizePlyDecode(count, out, rest, coefficients);\n}\n\n/** The vertex element of a splat PLY, with its required properties checked. */\nfunction requireSplatVertexElement(header: ReturnType<typeof parsePlyHeader>): PlyElement {\n const vertices = header.element('vertex');\n if (!vertices) throw new Error('PLY file is not a Gaussian splat file: no \"vertex\" element.');\n assertPlyPropertyTypes(\n vertices,\n REQUIRED_PROPERTIES,\n ['float', 'float32'],\n 'PLY file is not a Gaussian splat file',\n );\n return vertices;\n}\n\ninterface RestLayout {\n bands: 1 | 2 | 3;\n coefficients: number;\n offsets: readonly number[];\n}\n\n/** Resolves optional `f_rest_*` higher-order SH in a 3DGS vertex element. */\nfunction restLayout(vertices: PlyElement): RestLayout | null {\n const restNames = vertices.propertyNames.filter((name) => name.startsWith('f_rest_'));\n if (restNames.length === 0) return null;\n if (restNames.length % 3 !== 0) {\n throw new Error(\n `PLY vertex has ${restNames.length} f_rest_* properties, which is not divisible by three.`,\n );\n }\n const coefficients = restNames.length / 3;\n const bands = REST_COEFFICIENTS[coefficients];\n if (!bands) {\n throw new Error(\n `PLY vertex has ${coefficients} SH coefficients per channel; expected 3, 8 or 15.`,\n );\n }\n const names = Array.from({ length: coefficients * 3 }, (_, i) => `f_rest_${i}`);\n assertPlyPropertyTypes(vertices, names, ['float', 'float32'], 'PLY f_rest');\n const offsets = names.map((name) => plyPropertyOffset(vertices, name));\n return { bands, coefficients, offsets };\n}\n\nfunction finalizePlyDecode(\n count: number,\n out: SplatArrays,\n rest: RestLayout | null,\n coefficients: Float32Array | null,\n): SplatData {\n const shPacked =\n rest && coefficients ? packShCoefficients(coefficients, count, rest.bands) : undefined;\n return { count, ...out, ...(shPacked ? { shPacked } : {}) };\n}\n\ninterface SplatArrays {\n positions: Float32Array;\n colors: Uint8Array;\n covariances: Float32Array;\n}\n\nfunction allocate(count: number): SplatArrays {\n return {\n positions: new Float32Array(count * 3),\n colors: new Uint8Array(count * 4),\n covariances: new Float32Array(count * 6),\n };\n}\n\n/** Byte offsets of the properties the decoder reads, within one record. */\ninterface VertexOffsets {\n x: number;\n y: number;\n z: number;\n r: number;\n g: number;\n b: number;\n alpha: number;\n s0: number;\n s1: number;\n s2: number;\n q0: number;\n q1: number;\n q2: number;\n q3: number;\n}\n\nfunction vertexOffsets(vertices: PlyElement): VertexOffsets {\n const at = (name: string): number => plyPropertyOffset(vertices, name);\n return {\n x: at('x'),\n y: at('y'),\n z: at('z'),\n r: at('f_dc_0'),\n g: at('f_dc_1'),\n b: at('f_dc_2'),\n alpha: at('opacity'),\n s0: at('scale_0'),\n s1: at('scale_1'),\n s2: at('scale_2'),\n q0: at('rot_0'),\n q1: at('rot_1'),\n q2: at('rot_2'),\n q3: at('rot_3'),\n };\n}\n\n/**\n * Decodes records `[from, to)` into `out`, reading from a view whose byte 0\n * holds record `viewFirst`. Shared by the whole-buffer and streamed paths, so\n * both produce identical arrays.\n */\nfunction decodeRecords(\n view: DataView,\n o: VertexOffsets,\n stride: number,\n viewFirst: number,\n from: number,\n to: number,\n out: SplatArrays,\n): void {\n const { positions, colors, covariances } = out;\n for (let i = from; i < to; i++) {\n const base = (i - viewFirst) * stride;\n const readFloat = (offset: number): number => view.getFloat32(base + offset, true);\n\n positions[i * 3 + 0] = readFloat(o.x);\n positions[i * 3 + 1] = readFloat(o.y);\n positions[i * 3 + 2] = readFloat(o.z);\n\n // Degree-0 SH to linear color: c = 0.5 + Y₀₀ · f_dc.\n const toByte = (sh: number): number =>\n Math.max(0, Math.min(255, Math.round((0.5 + SH_C0 * sh) * 255)));\n colors[i * 4 + 0] = toByte(readFloat(o.r));\n colors[i * 4 + 1] = toByte(readFloat(o.g));\n colors[i * 4 + 2] = toByte(readFloat(o.b));\n colors[i * 4 + 3] = Math.round(sigmoid(readFloat(o.alpha)) * 255);\n\n // Scale is stored in log-space; rotation as a (w, x, y, z) quaternion.\n writeCovariance(\n covariances,\n i,\n Math.exp(readFloat(o.s0)),\n Math.exp(readFloat(o.s1)),\n Math.exp(readFloat(o.s2)),\n readFloat(o.q0),\n readFloat(o.q1),\n readFloat(o.q2),\n readFloat(o.q3),\n );\n }\n}\n\n/**\n * Decodes channel-major `f_rest_*` floats into coefficient-major RGB triples.\n */\nfunction decodeRestRecords(\n view: DataView,\n stride: number,\n viewFirst: number,\n from: number,\n to: number,\n rest: RestLayout,\n out: Float32Array,\n): void {\n const { coefficients, offsets } = rest;\n for (let i = from; i < to; i++) {\n const base = (i - viewFirst) * stride;\n const dest = i * coefficients * 3;\n for (let c = 0; c < coefficients; c++) {\n out[dest + c * 3 + 0] = view.getFloat32(base + (offsets[c] as number), true);\n out[dest + c * 3 + 1] = view.getFloat32(base + (offsets[coefficients + c] as number), true);\n out[dest + c * 3 + 2] = view.getFloat32(\n base + (offsets[2 * coefficients + c] as number),\n true,\n );\n }\n }\n}\n"],"names":["SPLATS_PER_CHUNK","CHUNK_BOUNDS","CHUNK_COLOR_BOUNDS","VERTEX_PROPERTIES","SH_COEFFICIENTS","SH_MIN","SH_MAX","isCompressedPly","header","vertex","unpackUnorm","value","bits","max","lerp","min","t","parseCompressedPly","buffer","chunks","vertices","assertPlyPropertyTypes","hasColorBounds","assertPlyElementFits","count","neededChunks","chunkView","vertexView","chunkAt","chunk","property","plyPropertyOffset","positionOffset","rotationOffset","scaleOffset","colorOffset","positions","colors","covariances","minX","minY","minZ","maxX","maxY","maxZ","minSx","minSy","minSz","maxSx","maxSy","maxSz","minR","minG","minB","maxR","maxG","maxB","first","last","i","base","packedPosition","packedColor","r","g","b","a","toByte","packedScale","scaleX","scaleY","scaleZ","packedRotation","q0","q1","q2","largest","qw","qx","qy","qz","writeCovariance","shPacked","decodeSh","sh","coefficients","bands","band","names","_","bytes","offsets","name","packed","c","REQUIRED_PROPERTIES","REST_COEFFICIENTS","sigmoid","x","parseSplatPly","parsePlyHeader","requireSplatVertexElement","out","allocate","rest","restLayout","decodeRecords","vertexOffsets","decodeRestRecords","finalizePlyDecode","restNames","packShCoefficients","at","view","o","stride","viewFirst","from","to","readFloat","offset","SH_C0","dest"],"mappings":"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"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol for the `.rad` page-table frontier worker
|
|
3
|
+
* (`frontier-worker.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Split from the worker module so `StreamedSplatMesh` can be typed against it
|
|
6
|
+
* without pulling worker source into the published declarations - the worker is
|
|
7
|
+
* reached only through `?worker&inline` and is not a public entry point.
|
|
8
|
+
*/
|
|
9
|
+
import type { SplatData } from '../../splat-data';
|
|
10
|
+
/**
|
|
11
|
+
* Foveation ramp for the frontier traversal, matching Spark's `SparkRenderer`
|
|
12
|
+
* defaults (`coneFov0` / `coneFov` / `coneFoveate` / `behindFoveate`). Detail is
|
|
13
|
+
* full inside `coneFov0`, falls to `coneFoveate` by `coneFov`, and to
|
|
14
|
+
* `behindFoveate` directly behind the camera - a *weight*, never a cull, so the
|
|
15
|
+
* scene stays covered when the camera turns. Lives here (a dependency-free
|
|
16
|
+
* module) because both the worker and `StreamedSplatMesh` need it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const FRONTIER_FOVEATION_DEFAULTS: {
|
|
19
|
+
readonly coneFov0: 90;
|
|
20
|
+
readonly coneFov: 120;
|
|
21
|
+
readonly coneFoveate: 0.4;
|
|
22
|
+
readonly behindFoveate: 0.2;
|
|
23
|
+
};
|
|
24
|
+
/** Degrees / weights describing the foveation ramp. */
|
|
25
|
+
export interface FrontierFoveation {
|
|
26
|
+
readonly coneFov0: number;
|
|
27
|
+
readonly coneFov: number;
|
|
28
|
+
readonly coneFoveate: number;
|
|
29
|
+
readonly behindFoveate: number;
|
|
30
|
+
}
|
|
31
|
+
/** A decoded chunk's arrays, as forwarded from the main thread. */
|
|
32
|
+
export interface FrontierChunkMessage {
|
|
33
|
+
readonly type: 'chunk';
|
|
34
|
+
readonly file: number;
|
|
35
|
+
readonly count: number;
|
|
36
|
+
readonly positions: Float32Array;
|
|
37
|
+
readonly colors: Uint8Array;
|
|
38
|
+
readonly covariances: Float32Array;
|
|
39
|
+
readonly childCount: Uint16Array;
|
|
40
|
+
readonly childStart: Uint32Array;
|
|
41
|
+
readonly size: Float32Array;
|
|
42
|
+
readonly shBands: 0 | 1 | 2 | 3;
|
|
43
|
+
readonly shPacked?: Uint32Array;
|
|
44
|
+
readonly shRange?: {
|
|
45
|
+
min: readonly [number, number, number];
|
|
46
|
+
max: readonly [number, number, number];
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface FrontierInitMessage {
|
|
50
|
+
readonly type: 'init';
|
|
51
|
+
readonly capacity: number;
|
|
52
|
+
readonly chunkSize: number;
|
|
53
|
+
readonly cpuCacheBytes: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Changes how many slots the pager may fill, after the host grew or shrank the
|
|
57
|
+
* storage behind them (a near mesh climbing its budget, a distant one giving
|
|
58
|
+
* pages back). The chunk cache is untouched - only the pager is resized - so no
|
|
59
|
+
* chunk is re-downloaded.
|
|
60
|
+
*/
|
|
61
|
+
export interface FrontierResizeMessage {
|
|
62
|
+
readonly type: 'resize';
|
|
63
|
+
readonly capacity: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Changes the byte cap the chunk cache evicts against, after the scene's shared
|
|
67
|
+
* `ChunkCacheBudget` re-split it - a near mesh climbing, a far one giving bytes
|
|
68
|
+
* back.
|
|
69
|
+
*
|
|
70
|
+
* Only the cap moves; nothing is dropped here. Eviction stays on the one path
|
|
71
|
+
* that knows what the frontier still needs (`evict` runs inside `reschedule`,
|
|
72
|
+
* against `neededFiles` and the pager's resident set), and the `evicted` list
|
|
73
|
+
* only reaches the main thread on a plan. Evicting off that path would drop
|
|
74
|
+
* chunks out from under resident splats with no way to tell the host - the
|
|
75
|
+
* dark-speckle failure the resident-set guard exists to prevent.
|
|
76
|
+
*/
|
|
77
|
+
export interface FrontierCacheBudgetMessage {
|
|
78
|
+
readonly type: 'cacheBudget';
|
|
79
|
+
readonly cpuCacheBytes: number;
|
|
80
|
+
}
|
|
81
|
+
export interface FrontierRescheduleMessage {
|
|
82
|
+
readonly type: 'reschedule';
|
|
83
|
+
readonly seq: number;
|
|
84
|
+
readonly cameraLocal: [number, number, number];
|
|
85
|
+
/** Unit camera forward in mesh-local space. Detail falls off away from it -
|
|
86
|
+
* the traversal foveates rather than frustum-culls, so the scene stays covered
|
|
87
|
+
* when the camera turns or zooms out. */
|
|
88
|
+
readonly cameraForward: [number, number, number];
|
|
89
|
+
/** Foveation ramp, in degrees / weights (Spark's `coneFov0`/`coneFov`/…). */
|
|
90
|
+
readonly coneFov0: number;
|
|
91
|
+
readonly coneFov: number;
|
|
92
|
+
readonly coneFoveate: number;
|
|
93
|
+
readonly behindFoveate: number;
|
|
94
|
+
/** Cut on foveated `size / distance` - `2·tan(fovY/2) / renderHeight`, scaled
|
|
95
|
+
* by `foveationTargetPx`. Fixed per frame; the budget is what bounds the cut. */
|
|
96
|
+
readonly limit: number;
|
|
97
|
+
/** Maximum drawn splats; enforced inside the traversal, never after. */
|
|
98
|
+
readonly budget: number;
|
|
99
|
+
}
|
|
100
|
+
export type FrontierRequest = FrontierInitMessage | FrontierChunkMessage | FrontierResizeMessage | FrontierCacheBudgetMessage | FrontierRescheduleMessage;
|
|
101
|
+
/** Packed splats to write, in slot order (a subset of {@link SplatData}). */
|
|
102
|
+
export interface PlanSplats {
|
|
103
|
+
readonly count: number;
|
|
104
|
+
readonly positions: Float32Array;
|
|
105
|
+
readonly colors: Uint8Array;
|
|
106
|
+
readonly covariances: Float32Array;
|
|
107
|
+
readonly shPacked?: SplatData['shPacked'];
|
|
108
|
+
}
|
|
109
|
+
export interface FrontierPlanMessage {
|
|
110
|
+
readonly type: 'plan';
|
|
111
|
+
readonly seq: number;
|
|
112
|
+
/** Survivors relocated by swap-remove: write `moves` splat j at `moveSlots[j]`. */
|
|
113
|
+
readonly moveSlots: Uint32Array;
|
|
114
|
+
readonly moves: PlanSplats;
|
|
115
|
+
/** Newcomers written contiguously at `[appendStart, appendStart + appends.count)`. */
|
|
116
|
+
readonly appendStart: number;
|
|
117
|
+
readonly appends: PlanSplats;
|
|
118
|
+
/** Freed tail slots to degenerate. */
|
|
119
|
+
readonly degenerateStart: number;
|
|
120
|
+
readonly degenerateCount: number;
|
|
121
|
+
/** Chunks the frontier wants next, biggest-on-screen first. */
|
|
122
|
+
readonly touched: Uint32Array;
|
|
123
|
+
/** Drawn (non-degenerate) frontier size - the true on-screen splat count. */
|
|
124
|
+
readonly residentCount: number;
|
|
125
|
+
/**
|
|
126
|
+
* Splats this plan could not gather because their chunk had been evicted, and
|
|
127
|
+
* so wrote as zeros into slots that are still drawn - coverage holes, seen as
|
|
128
|
+
* dark speckle in a region while it refines. Eviction protects every chunk
|
|
129
|
+
* with resident splats, so this is 0; a non-zero value is a bug.
|
|
130
|
+
*/
|
|
131
|
+
readonly gatherMissing: number;
|
|
132
|
+
/** Newcomers the slab had no room for. The traversal is budget-bounded, so
|
|
133
|
+
* this is 0 unless the slab is smaller than the draw budget - a real bug. */
|
|
134
|
+
readonly dropped: number;
|
|
135
|
+
/** Chunks evicted from the worker cache this round. The main thread must
|
|
136
|
+
* forget them (`pageTableCachedFiles`) or they could never be refetched. */
|
|
137
|
+
readonly evicted: Uint32Array;
|
|
138
|
+
/**
|
|
139
|
+
* The cut this plan was built at - at or below the requested `limit`, because
|
|
140
|
+
* the worker refines past the quality target to spend the draw budget.
|
|
141
|
+
*
|
|
142
|
+
* The host needs it because its screen-radius band was chosen for the *target*
|
|
143
|
+
* cut: a finer cut selects smaller splats, and a band left at the coarse
|
|
144
|
+
* setting would cull exactly the detail the refinement just bought.
|
|
145
|
+
*/
|
|
146
|
+
readonly solvedLimit: number;
|
|
147
|
+
/**
|
|
148
|
+
* The pager capacity this plan was built against.
|
|
149
|
+
*
|
|
150
|
+
* The host grows and shrinks the storage behind the slots, so a plan can
|
|
151
|
+
* arrive describing slots that no longer exist: a `reschedule` posted before a
|
|
152
|
+
* `resize` is answered from the old capacity, and applying that answer writes
|
|
153
|
+
* some splats nowhere while leaving their slots holding whatever was there
|
|
154
|
+
* before - a coarse node's data in a slot the frontier now wants fine, which
|
|
155
|
+
* draws as a single enormous splat. The host compares this and drops such a
|
|
156
|
+
* plan instead.
|
|
157
|
+
*/
|
|
158
|
+
readonly capacity: number;
|
|
159
|
+
/**
|
|
160
|
+
* False when the plan was held short of the traversal's frontier to bound how
|
|
161
|
+
* much the host must write in one tick, so the resident set is an intermediate
|
|
162
|
+
* one - some newcomers deferred, some replaced nodes still drawn. The host must
|
|
163
|
+
* reschedule promptly, otherwise convergence stalls wherever the cap left it.
|
|
164
|
+
*/
|
|
165
|
+
readonly converged: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Decoded bytes the worker's chunk cache is holding, and the cap it evicts
|
|
168
|
+
* against.
|
|
169
|
+
*
|
|
170
|
+
* Reported because the cache lives entirely in the worker, so a host watching
|
|
171
|
+
* a scene that will not stop streaming cannot otherwise tell "the working set
|
|
172
|
+
* does not fit" from "still converging" - and the main thread's own mirror of
|
|
173
|
+
* which files are cached is not enough to reconstruct the byte total.
|
|
174
|
+
*/
|
|
175
|
+
readonly cacheBytes: number;
|
|
176
|
+
readonly cacheLimitBytes: number;
|
|
177
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@voluma/vlam/formats/rad` - SparkJS `.rad`/`.radc` parsers and streamed scene builder.
|
|
3
|
+
*
|
|
4
|
+
* Loading through {@link StreamedSplatMesh.load} / {@link loadScene} does not
|
|
5
|
+
* require this import; the library pulls the format in on demand. Use this
|
|
6
|
+
* subpath for direct decode or inspection outside the default loaders.
|
|
7
|
+
*/
|
|
8
|
+
export { RAD_MAGIC, RAD_CHUNK_MAGIC, parseRad, parseRadHeaderMeta, parseRadChunkStreaming, type RadChunkRange, type RadMeta, type RadTree, } from './parse-rad';
|
|
9
|
+
export { buildRadScene, RadLodSource } from './rad';
|
|
10
|
+
export type { RadChunkRangeRequest } from '../../load-worker-protocol';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type RadShCodebook, type SplatData } from '../../splat-data';
|
|
2
|
+
/**
|
|
3
|
+
* Parser for World Labs' Spark `.rad`/`.radc` format (a precomputed LOD splat
|
|
4
|
+
* tree with per-column, gzip/deflate-compressed properties). Ported from the
|
|
5
|
+
* MIT-licensed Rust reference (`sparkjsdev/spark`, `rust/spark-lib/src/rad.rs`
|
|
6
|
+
* and `splat_encode.rs`) since this project's worker has no Rust/WASM
|
|
7
|
+
* toolchain - Spark's own decoder runs as WASM and writes into Spark's own
|
|
8
|
+
* GPU texture layout, not this package's `SplatData`.
|
|
9
|
+
*
|
|
10
|
+
* Two entry points:
|
|
11
|
+
* - {@link parseRad} flattens a whole file into leaf splats only (the original
|
|
12
|
+
* capture), for a small `.rad` loaded in one piece by `loadScene`.
|
|
13
|
+
* - {@link parseRadChunkStreaming} decodes one chunk keeping *every* splat -
|
|
14
|
+
* the merged coarse LOD-tree nodes included - plus the per-splat tree
|
|
15
|
+
* columns, for the streamed reader (see `rad.ts`), which chooses the
|
|
16
|
+
* rendered cut across chunks at runtime.
|
|
17
|
+
*
|
|
18
|
+
* SH columns are repacked into the renderer's per-splat packed representation.
|
|
19
|
+
*/
|
|
20
|
+
export declare const RAD_MAGIC = 809779538;
|
|
21
|
+
export declare const RAD_CHUNK_MAGIC = 1128546642;
|
|
22
|
+
/** One chunk's location within a `.rad` file (or its external `.radc` name). */
|
|
23
|
+
export interface RadChunkRange {
|
|
24
|
+
readonly offset: number;
|
|
25
|
+
readonly bytes: number;
|
|
26
|
+
readonly filename?: string;
|
|
27
|
+
}
|
|
28
|
+
/** The file-level `RAD0` metadata (a subset - only the fields we consume). */
|
|
29
|
+
export interface RadMeta {
|
|
30
|
+
readonly version: number;
|
|
31
|
+
readonly type: string;
|
|
32
|
+
/** Total splats in the LOD tree (merged nodes + leaves). */
|
|
33
|
+
readonly count: number;
|
|
34
|
+
readonly maxSh?: number;
|
|
35
|
+
readonly lodTree?: boolean;
|
|
36
|
+
readonly chunkSize?: number;
|
|
37
|
+
readonly chunks: readonly RadChunkRange[];
|
|
38
|
+
/** Spark's free-form build-info JSON; carries `input_splat_count` (the leaf
|
|
39
|
+
* count) among other fields. Parsed best-effort by the reader. */
|
|
40
|
+
readonly comment?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Per-splat LOD-tree links: an internal node's children are the contiguous
|
|
43
|
+
* global-index range `[childStart, childStart + childCount)`. Leaves have
|
|
44
|
+
* `childCount === 0`. */
|
|
45
|
+
export interface RadTree {
|
|
46
|
+
readonly childCount: Uint16Array;
|
|
47
|
+
readonly childStart: Uint32Array;
|
|
48
|
+
}
|
|
49
|
+
export interface RadChunkProperty {
|
|
50
|
+
readonly offset: number;
|
|
51
|
+
readonly bytes: number;
|
|
52
|
+
readonly property: string;
|
|
53
|
+
readonly encoding: string;
|
|
54
|
+
readonly compression?: string;
|
|
55
|
+
readonly min?: number;
|
|
56
|
+
readonly max?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Decodes a `.rad` file, or a lone `.radc` chunk, into a flat `SplatData`
|
|
60
|
+
* of leaf splats only (the original capture). Merged coarse LOD-tree nodes
|
|
61
|
+
* are dropped - they exist for streamed reading, and are meaningless once
|
|
62
|
+
* everything is already decoded into one buffer.
|
|
63
|
+
*
|
|
64
|
+
* @throws {Error} if the file is malformed, an unsupported version, carries
|
|
65
|
+
* an external `.radc` chunk reference (one-shot loading needs every chunk in
|
|
66
|
+
* the same buffer), or uses a property encoding this parser does not
|
|
67
|
+
* recognize.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseRad(buffer: ArrayBuffer): Promise<SplatData>;
|
|
70
|
+
/**
|
|
71
|
+
* Parses just the `RAD0` header: the typed metadata and the byte offset where
|
|
72
|
+
* the chunk stream begins. `buffer` need only be long enough to hold the
|
|
73
|
+
* header - a ranged prefix of a large single-file `.rad` is enough.
|
|
74
|
+
*/
|
|
75
|
+
export declare function parseRadHeaderMeta(buffer: ArrayBuffer): {
|
|
76
|
+
meta: RadMeta;
|
|
77
|
+
chunksStart: number;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Decodes one chunk (a `RADC`-magic byte range) into a `SplatData` holding
|
|
81
|
+
* **every** splat in the chunk - internal LOD-tree nodes and leaves alike -
|
|
82
|
+
* plus the per-splat tree columns when the file carries a LOD tree. The
|
|
83
|
+
* streamed reader keeps all splats resident and chooses the rendered cut
|
|
84
|
+
* itself from {@link SplatData.radTree}.
|
|
85
|
+
*
|
|
86
|
+
* @throws {Error} on a malformed chunk or an unrecognized property encoding.
|
|
87
|
+
*/
|
|
88
|
+
export declare function parseRadChunkStreaming(buffer: ArrayBuffer, shCodebook?: RadShCodebook, shExtent?: number, reorder?: boolean): Promise<SplatData>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RadChunkProperty } from './parse-rad';
|
|
2
|
+
/**
|
|
3
|
+
* Asserts a (possibly decompressed) column physically holds one value per
|
|
4
|
+
* splat per dimension before its `count`-sized output is allocated - the
|
|
5
|
+
* cross-check between the untrusted chunk `count` and the actual bytes.
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertColumnHoldsCount(data: Uint8Array, prop: RadChunkProperty, dims: number, count: number, bytesPerValue: number): void;
|
|
8
|
+
export declare function decodeFloatColumn(data: Uint8Array, prop: RadChunkProperty, dims: number, count: number): Float32Array;
|
|
9
|
+
export declare function decodeOrientation(data: Uint8Array, prop: RadChunkProperty, count: number): Float32Array;
|
|
10
|
+
export declare function decodeChildCount(data: Uint8Array, prop: RadChunkProperty, count: number): Uint16Array;
|
|
11
|
+
export declare function decodeChildStart(data: Uint8Array, prop: RadChunkProperty, count: number): Uint32Array;
|
|
12
|
+
/** Planar little-endian u16. */
|
|
13
|
+
export declare function decodeU16(data: Uint8Array, count: number): Uint16Array;
|
|
14
|
+
/** Planar little-endian u32. */
|
|
15
|
+
export declare function decodeU32(data: Uint8Array, count: number): Uint32Array;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
import type { LodRun } from '../../lod-scheduler';
|
|
3
|
+
import type { LodSource, LodSourceOptions } from '../../lod-source';
|
|
4
|
+
import type { SplatData } from '../../splat-data';
|
|
5
|
+
export declare class RadFoveatedSource implements LodSource {
|
|
6
|
+
budget: number;
|
|
7
|
+
lodBaseDistance: number;
|
|
8
|
+
lodMultiplier: number;
|
|
9
|
+
private readonly chunkSize;
|
|
10
|
+
private readonly chunkCounts;
|
|
11
|
+
private readonly numChunks;
|
|
12
|
+
private readonly decoded;
|
|
13
|
+
/** Real bounds of a decoded chunk (AABB of its splat centers). */
|
|
14
|
+
private readonly bounds;
|
|
15
|
+
/** Region estimated from decoded parents, to rank an undecoded chunk. */
|
|
16
|
+
private readonly estBounds;
|
|
17
|
+
/** Per chunk: the chunks its internal splats refine into (for prefetch). */
|
|
18
|
+
private readonly childChunks;
|
|
19
|
+
/** Inverts the tree's forward links to give each splat its parent's size,
|
|
20
|
+
* for the frontier cut. Fed the same `own_size` measure the material derives. */
|
|
21
|
+
private readonly parentSizes;
|
|
22
|
+
/** Splats chunk 0's coarse shell draws (the minimum-coverage floor). */
|
|
23
|
+
coverageSplatCount: number;
|
|
24
|
+
/**
|
|
25
|
+
* Whether per-splat `parent_size` is needed. Only the GPU cuts
|
|
26
|
+
* (`foveationMode: 'band' | 'frontier'`) read `frontierParent`; the page-table
|
|
27
|
+
* pager runs its own traversal in the worker and `gatherGlobals` never carries
|
|
28
|
+
* it. Computing it anyway costs a full covariance pass per chunk *and* leaves
|
|
29
|
+
* one pending-range object per internal node alive in {@link RadParentSizes}
|
|
30
|
+
* until its child chunk decodes - tens of millions of live objects on an
|
|
31
|
+
* 800-chunk capture, enough GC pressure to read as a frozen page.
|
|
32
|
+
*/
|
|
33
|
+
needsParentSizes: boolean;
|
|
34
|
+
private readonly scratchPoint;
|
|
35
|
+
constructor(chunkSize: number, chunkCounts: readonly number[], options: LodSourceOptions);
|
|
36
|
+
onChunkDecoded(file: number, data: SplatData): void;
|
|
37
|
+
/**
|
|
38
|
+
* Computes each splat's `parent_size` for the frontier cut and packs it, with
|
|
39
|
+
* a leaf-sign bit, onto `data.frontierParent` (uploaded into `covarianceB.w`).
|
|
40
|
+
*
|
|
41
|
+
* `own_size` is derived from the covariance the same way the material does -
|
|
42
|
+
* `2·√(trace(Σ)/3)`, which with the merged-node-expanded scales already carries
|
|
43
|
+
* the expansion - so a node's own size and its children's `parent_size` are the
|
|
44
|
+
* identical measure, giving clean level transitions.
|
|
45
|
+
*
|
|
46
|
+
* The catch: this source streams chunks in **camera** order, not tree order, so
|
|
47
|
+
* a child frequently decodes before its parent and `RadParentSizes` returns
|
|
48
|
+
* `Infinity` (unknown). Making every such splat a root would defeat the cut -
|
|
49
|
+
* a root always passes the "parent too big" half, so it can never be culled and
|
|
50
|
+
* the drawn count runs away. Instead an unknown parent falls back to Spark's
|
|
51
|
+
* ~1.75× merge ratio (`parent ≈ own · 1.75`), which brackets the splat to one
|
|
52
|
+
* level per ray (a per-splat band). Chunk 0 - the always-resident coarse shell -
|
|
53
|
+
* keeps the true-root sentinel so far rays always have coverage.
|
|
54
|
+
*/
|
|
55
|
+
private attachFrontierParent;
|
|
56
|
+
computeDesiredRuns(cameraLocal: THREE.Vector3, frustum: THREE.Frustum, _now: number): LodRun[];
|
|
57
|
+
/** The coarse shell covers far/loading regions via the band cull, so there
|
|
58
|
+
* are no separate substitute runs. */
|
|
59
|
+
coarsestRunsFor(): LodRun[];
|
|
60
|
+
/** Chunk 0's coarse shell plus the nearest decoded chunks, up to the budget. */
|
|
61
|
+
private buildResident;
|
|
62
|
+
private nearestUndecodedRefinements;
|
|
63
|
+
/** Refine priority for an undecoded chunk (higher = sooner): nearest, in-view
|
|
64
|
+
* region first, scored against its *localized* estimated bounds (not the
|
|
65
|
+
* scene-spanning whole-chunk bounds). Off-screen regions - not drawn - are
|
|
66
|
+
* strongly penalized so the surfaces the camera faces refine first. */
|
|
67
|
+
private refineScore;
|
|
68
|
+
private chunkDistance;
|
|
69
|
+
/** One run covering a whole chunk; the material cull selects splats within. */
|
|
70
|
+
private wholeChunkRun;
|
|
71
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Propagates each `.rad` splat's **parent LOD node size** as chunks stream in.
|
|
3
|
+
*
|
|
4
|
+
* The foveation cut (see `docs/formats/rad-notes.md` M14.6) draws a splat `i` when
|
|
5
|
+
* `parentPixelScale(i) > limit ≥ ownPixelScale(i)` - exactly one node per
|
|
6
|
+
* root→leaf path, since `pixel_scale = size / distance` decreases monotonically
|
|
7
|
+
* down the tree. That test needs each splat's *parent's* `size`, but the file
|
|
8
|
+
* only stores the forward links (`child_start`/`child_count`): a node lists its
|
|
9
|
+
* children, not its parent.
|
|
10
|
+
*
|
|
11
|
+
* So we invert the links incrementally. When a chunk decodes, each internal
|
|
12
|
+
* node's `size` is recorded against the global index range of its children
|
|
13
|
+
* (`[child_start, child_start + child_count)`); a later chunk holding those
|
|
14
|
+
* children reads its own parent sizes back out. Because chunks stream roughly
|
|
15
|
+
* coarsest-first (a node's parent is always at a lower global index, and the
|
|
16
|
+
* coarse base loads first), parents are normally decoded before their children.
|
|
17
|
+
* A child whose parent has not decoded yet gets `Infinity` - treated as a root,
|
|
18
|
+
* so it draws whenever it is small enough on screen; the worst case is a little
|
|
19
|
+
* over-draw, never a hole or a crash.
|
|
20
|
+
*
|
|
21
|
+
* This decode order is the **original** chunk order (no frontier reorder), so
|
|
22
|
+
* `child_start` still points at the right splats - the foveation path keeps
|
|
23
|
+
* splats in file order and lets the GPU cull choose the cut per splat.
|
|
24
|
+
*/
|
|
25
|
+
export declare class RadParentSizes {
|
|
26
|
+
private readonly chunkSize;
|
|
27
|
+
/** Parent-size ranges awaiting the child chunk that will consume them,
|
|
28
|
+
* keyed by that child chunk index. Each range is a global-index span. */
|
|
29
|
+
private readonly pending;
|
|
30
|
+
/** Chunks already resolved - a parent decoded after its child can't fix it,
|
|
31
|
+
* so we drop such stragglers rather than leak them. */
|
|
32
|
+
private readonly resolved;
|
|
33
|
+
constructor(chunkSize: number);
|
|
34
|
+
/**
|
|
35
|
+
* Records `chunkIndex`'s internal→child links and returns that chunk's
|
|
36
|
+
* per-splat parent sizes (in the chunk's own splat order). Call once per
|
|
37
|
+
* chunk, as it decodes.
|
|
38
|
+
*
|
|
39
|
+
* @param childCount - Per-splat child count (0 = leaf).
|
|
40
|
+
* @param childStart - Per-splat first-child global index.
|
|
41
|
+
* @param size - Per-splat LOD node size.
|
|
42
|
+
*/
|
|
43
|
+
resolve(chunkIndex: number, childCount: Uint16Array, childStart: Uint32Array, size: Float32Array): Float32Array;
|
|
44
|
+
private push;
|
|
45
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
import type { LodRun } from '../../lod-scheduler';
|
|
3
|
+
import type { LodSource, LodSourceOptions, StreamedScene } from '../../lod-source';
|
|
4
|
+
import type { SplatData } from '../../splat-data';
|
|
5
|
+
import type { SplatDatasetSource } from '../../dataset-source';
|
|
6
|
+
import { type SplatRequestOptions } from '../../loading';
|
|
7
|
+
/**
|
|
8
|
+
* Builds a streamed scene from a single-file `.rad`. Fetches the header and
|
|
9
|
+
* chunk table, then decodes chunk 0 (the coarsest whole-scene overview) to
|
|
10
|
+
* establish scene bounds - the header carries none - and to seed the LOD
|
|
11
|
+
* source with the root of the tree.
|
|
12
|
+
*
|
|
13
|
+
* `maxShBands` lets a caller **decline** the spherical harmonics the file
|
|
14
|
+
* carries. Unlike SOG's opt-*in* `shBands` (whose manifest never declares
|
|
15
|
+
* whether tiles have shN), a `.rad` names its own `maxSh`, so the question here
|
|
16
|
+
* is only whether to keep it. It matters: every band costs 16 B/splat of GPU
|
|
17
|
+
* pool plus as much again of CPU backing, and a texture fetch and coefficient
|
|
18
|
+
* evaluation per splat per frame - exactly what the `smooth` performance profile
|
|
19
|
+
* exists to decline on mobile.
|
|
20
|
+
*
|
|
21
|
+
* The choice is deliberately **all-or-nothing**: `0` drops SH, anything else
|
|
22
|
+
* keeps every band the file has. A *partial* cap cannot be honoured, because the
|
|
23
|
+
* `.rad` decoder emits the file's full band count and `SplatMesh.writePackedSh`
|
|
24
|
+
* only copies packed words when the counts match exactly - a mismatch falls
|
|
25
|
+
* through to neutral words, which would pay for the reduced bands' textures and
|
|
26
|
+
* still render flat, view-independent color. Truncating the packed SH per splat
|
|
27
|
+
* at decode would lift this restriction.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildRadScene(source: SplatDatasetSource, options: LodSourceOptions, request?: SplatRequestOptions, maxShBands?: 0 | 1 | 2 | 3, budgetLifts?: boolean): Promise<StreamedScene>;
|
|
30
|
+
/**
|
|
31
|
+
* Selects a budget-bounded prefix of a `.rad` chunk sequence and renders its
|
|
32
|
+
* frontier - the splats whose children are not yet resident, plus all leaves.
|
|
33
|
+
*
|
|
34
|
+
* The tree is discovered incrementally: {@link onChunkDecoded} records each
|
|
35
|
+
* chunk's `child_count`/`child_start` columns, and `discoveredDepth` tracks the
|
|
36
|
+
* longest contiguous decoded prefix. `computeDesiredRuns` emits the frontier of
|
|
37
|
+
* the largest prefix whose drawn count fits the budget (cached - it does not
|
|
38
|
+
* depend on the camera), plus fetch-intent runs that pull the next chunks in.
|
|
39
|
+
*
|
|
40
|
+
* Refinement is **uniform** over the scene, not camera-distance foveated. Chunk-
|
|
41
|
+
* granular spatial foveation is impossible for this format: the chunk graph is a
|
|
42
|
+
* deeply entangled DAG - every non-root chunk has ≥2 parent chunks - so no
|
|
43
|
+
* ancestor-closed chunk subset larger than `{0}` exists, and a coarse ancestor
|
|
44
|
+
* that is not resident would double-draw over any refined descendant. True near-
|
|
45
|
+
* camera refinement therefore needs Spark-style per-splat GPU culling (see
|
|
46
|
+
* `docs/formats/rad-notes.md`, ROADMAP M14.6). Two levers help meanwhile: a budget large
|
|
47
|
+
* enough to load a moderate scene's full leaf set (blobs vanish once every chunk
|
|
48
|
+
* is resident), and the `maxSplatScreenRadius` blob cull for what stays coarse.
|
|
49
|
+
*
|
|
50
|
+
* {@link RadLodSource} is exported for unit testing only; it is not part of the
|
|
51
|
+
* public API and is reached in practice through {@link buildRadScene}.
|
|
52
|
+
*/
|
|
53
|
+
export declare class RadLodSource implements LodSource {
|
|
54
|
+
budget: number;
|
|
55
|
+
lodBaseDistance: number;
|
|
56
|
+
lodMultiplier: number;
|
|
57
|
+
private readonly total;
|
|
58
|
+
private readonly chunkSize;
|
|
59
|
+
private readonly chunkCounts;
|
|
60
|
+
private readonly numChunks;
|
|
61
|
+
/**
|
|
62
|
+
* Decoded per-splat tree columns, by chunk index.
|
|
63
|
+
*
|
|
64
|
+
* Grows monotonically and is deliberately never evicted. That costs ~10 B per
|
|
65
|
+
* splat of every chunk ever decoded (`childCount` u16 + `childStart` u32 +
|
|
66
|
+
* `size` f32) and is invisible to the chunk cache's byte accounting, so it
|
|
67
|
+
* reads like a leak - but it is not reclaimable. {@link discoveredDepth} is
|
|
68
|
+
* the longest *contiguous* decoded prefix and every frontier walk at or below
|
|
69
|
+
* it reads these columns, so dropping an entry would either corrupt the
|
|
70
|
+
* traversal or force `discoveredDepth` back and visibly coarsen the scene.
|
|
71
|
+
*
|
|
72
|
+
* Bounded in practice by the mode that uses it: `.rad` defaults to
|
|
73
|
+
* `foveationMode: 'pagetable'`, where the frontier worker owns the traversal
|
|
74
|
+
* and this source is never constructed. Only the `'band'` and `'frontier'`
|
|
75
|
+
* A/B modes pay it. Reducing it means changing the tree encoding, not adding
|
|
76
|
+
* an eviction pass here.
|
|
77
|
+
*/
|
|
78
|
+
private readonly decoded;
|
|
79
|
+
/** Longest contiguous decoded prefix `{0 … discoveredDepth − 1}`. */
|
|
80
|
+
private discoveredDepth;
|
|
81
|
+
/** Chunk-0-only frontier: the always-available coarse substitute coverage. */
|
|
82
|
+
private coarseRuns;
|
|
83
|
+
/** Splats the coarse (chunk-0) coverage draws. */
|
|
84
|
+
coverageSplatCount: number;
|
|
85
|
+
/** Cache of the last emitted frontier, keyed by the inputs that shape it. */
|
|
86
|
+
private cachedRuns;
|
|
87
|
+
private cachedFrontierCount;
|
|
88
|
+
/** Frontier splat count by prefix depth; see {@link countAtDepth}. */
|
|
89
|
+
private readonly depthCounts;
|
|
90
|
+
/** Budget range over which {@link cachedRuns} stays the correct frontier. */
|
|
91
|
+
private cachedBudgetLo;
|
|
92
|
+
private cachedBudgetHi;
|
|
93
|
+
private cachedForDiscovered;
|
|
94
|
+
constructor(total: number, chunkSize: number, chunkCounts: readonly number[], options: LodSourceOptions);
|
|
95
|
+
onChunkDecoded(file: number, data: SplatData): void;
|
|
96
|
+
computeDesiredRuns(_cameraLocal: THREE.Vector3, _frustum: THREE.Frustum, _now: number): LodRun[];
|
|
97
|
+
coarsestRunsFor(from: number, to: number): LodRun[];
|
|
98
|
+
/**
|
|
99
|
+
* Recomputes the cached frontier when the budget or discovery has moved.
|
|
100
|
+
*
|
|
101
|
+
* The cache is keyed on the *range* of budgets that select the same prefix
|
|
102
|
+
* depth, not on the budget itself. A camera-weighted governor rewrites the
|
|
103
|
+
* budget every frame - it wanders by a few percent as the camera drifts - and
|
|
104
|
+
* an exact-equality key therefore missed on roughly a third of all calls while
|
|
105
|
+
* choosing the very same depth. Each of those misses re-walks `frontierRuns`
|
|
106
|
+
* over the whole decoded prefix (millions of splats, several times over if it
|
|
107
|
+
* steps down levels): measured at 33ms mean and 1.1s worst, it was 92% of all
|
|
108
|
+
* frame CPU on a marker-heavy scene. Frontier count rises monotonically with
|
|
109
|
+
* depth, so the selected depth is stable for any budget in
|
|
110
|
+
* `[thisDepthCount, nextDepthCount)` and the walk can be skipped outright.
|
|
111
|
+
*/
|
|
112
|
+
private ensureFrontier;
|
|
113
|
+
/**
|
|
114
|
+
* Splats the frontier draws at `depth`, memoized.
|
|
115
|
+
*
|
|
116
|
+
* `discoveredDepth` is the longest *contiguous* decoded prefix, so for any
|
|
117
|
+
* `depth <= discoveredDepth` every chunk the walk reads is already decoded and
|
|
118
|
+
* the answer can never change afterwards - chunks are only ever added to
|
|
119
|
+
* `decoded`. That makes this permanently cacheable, which is what lets the
|
|
120
|
+
* depth search above run without rebuilding runs at each level it rejects.
|
|
121
|
+
*/
|
|
122
|
+
private countAtDepth;
|
|
123
|
+
/**
|
|
124
|
+
* The rendered runs for a resident prefix of `depth` chunks: within each
|
|
125
|
+
* chunk, the contiguous ranges of splats that are leaves or whose children
|
|
126
|
+
* fall beyond `residentCount` (the prefix's total splat count). The chunk's
|
|
127
|
+
* splats are child-start-descending (leaves first), so each range coalesces
|
|
128
|
+
* into one run.
|
|
129
|
+
*/
|
|
130
|
+
private frontierRuns;
|
|
131
|
+
private fetchIntentRun;
|
|
132
|
+
}
|