@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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/THIRD_PARTY_NOTICES.md +86 -0
  4. package/dist/budget-governor.d.ts +154 -0
  5. package/dist/camera-budget-governor.d.ts +234 -0
  6. package/dist/chunk-cache-budget.d.ts +156 -0
  7. package/dist/chunk-fetch-scheduler.d.ts +168 -0
  8. package/dist/chunk-loader.d.ts +85 -0
  9. package/dist/compute-sorter.d.ts +131 -0
  10. package/dist/create-splat-renderer.d.ts +99 -0
  11. package/dist/dataset-source.d.ts +55 -0
  12. package/dist/depth-of-field.d.ts +73 -0
  13. package/dist/effects.d.ts +337 -0
  14. package/dist/effects.js +180 -0
  15. package/dist/effects.js.map +1 -0
  16. package/dist/formats/ksplat/index.d.ts +8 -0
  17. package/dist/formats/ksplat/parse-ksplat.d.ts +9 -0
  18. package/dist/formats/ksplat.js +167 -0
  19. package/dist/formats/ksplat.js.map +1 -0
  20. package/dist/formats/lcc/collision-mesh.d.ts +37 -0
  21. package/dist/formats/lcc/collision-partition.d.ts +35 -0
  22. package/dist/formats/lcc/index.d.ts +16 -0
  23. package/dist/formats/lcc/lcc.d.ts +28 -0
  24. package/dist/formats/lcc/lcc2-transform.d.ts +18 -0
  25. package/dist/formats/lcc/lcc2.d.ts +4 -0
  26. package/dist/formats/lcc/parse-collision-lci.d.ts +28 -0
  27. package/dist/formats/lcc/parse-lcc.d.ts +177 -0
  28. package/dist/formats/lcc/parse-mesh-ply.d.ts +21 -0
  29. package/dist/formats/lcc.js +969 -0
  30. package/dist/formats/lcc.js.map +1 -0
  31. package/dist/formats/ply/index.d.ts +12 -0
  32. package/dist/formats/ply/parse-compressed-ply.d.ts +16 -0
  33. package/dist/formats/ply/parse-splat-ply.d.ts +46 -0
  34. package/dist/formats/ply.js +230 -0
  35. package/dist/formats/ply.js.map +1 -0
  36. package/dist/formats/rad/frontier-worker-protocol.d.ts +177 -0
  37. package/dist/formats/rad/index.d.ts +10 -0
  38. package/dist/formats/rad/parse-rad.d.ts +88 -0
  39. package/dist/formats/rad/rad-column-decoders.d.ts +15 -0
  40. package/dist/formats/rad/rad-foveated-source.d.ts +71 -0
  41. package/dist/formats/rad/rad-parent-sizes.d.ts +45 -0
  42. package/dist/formats/rad/rad.d.ts +132 -0
  43. package/dist/formats/rad.js +1098 -0
  44. package/dist/formats/rad.js.map +1 -0
  45. package/dist/formats/sog/index.d.ts +9 -0
  46. package/dist/formats/sog/parse-sog.d.ts +45 -0
  47. package/dist/formats/sog.js +257 -0
  48. package/dist/formats/sog.js.map +1 -0
  49. package/dist/formats/splat/index.d.ts +8 -0
  50. package/dist/formats/splat/parse-splat.d.ts +10 -0
  51. package/dist/formats/splat.js +39 -0
  52. package/dist/formats/splat.js.map +1 -0
  53. package/dist/formats/spz/index.d.ts +8 -0
  54. package/dist/formats/spz/parse-spz.d.ts +10 -0
  55. package/dist/formats/spz.js +218 -0
  56. package/dist/formats/spz.js.map +1 -0
  57. package/dist/frontier-worker-CAnK_Cmu.js +566 -0
  58. package/dist/frontier-worker-CAnK_Cmu.js.map +1 -0
  59. package/dist/half-float-Cs4FDq8Q.js +30 -0
  60. package/dist/half-float-Cs4FDq8Q.js.map +1 -0
  61. package/dist/half-float.d.ts +15 -0
  62. package/dist/index.d.ts +54 -0
  63. package/dist/index.js +9054 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/load-scene.d.ts +50 -0
  66. package/dist/load-worker-protocol.d.ts +82 -0
  67. package/dist/loading-wLo8vRbA.js +102 -0
  68. package/dist/loading-wLo8vRbA.js.map +1 -0
  69. package/dist/loading.d.ts +150 -0
  70. package/dist/lod-manifest.d.ts +61 -0
  71. package/dist/lod-scheduler.d.ts +229 -0
  72. package/dist/lod-source.d.ts +207 -0
  73. package/dist/logging-BfPdd7NJ.js +19 -0
  74. package/dist/logging-BfPdd7NJ.js.map +1 -0
  75. package/dist/logging.d.ts +37 -0
  76. package/dist/one-shot-worker-G80btnth.js +664 -0
  77. package/dist/one-shot-worker-G80btnth.js.map +1 -0
  78. package/dist/orientation.d.ts +35 -0
  79. package/dist/ply-header-BwgExCn-.js +125 -0
  80. package/dist/ply-header-BwgExCn-.js.map +1 -0
  81. package/dist/ply-header.d.ts +87 -0
  82. package/dist/radix-sort.d.ts +28 -0
  83. package/dist/radix-sorter.d.ts +46 -0
  84. package/dist/relighting.d.ts +50 -0
  85. package/dist/selection-volume.d.ts +87 -0
  86. package/dist/sh-pack-D5wAe5gg.js +54 -0
  87. package/dist/sh-pack-D5wAe5gg.js.map +1 -0
  88. package/dist/sh-pack.d.ts +49 -0
  89. package/dist/sort-scheduler.d.ts +62 -0
  90. package/dist/sort-worker-protocol.d.ts +37 -0
  91. package/dist/sorter.d.ts +31 -0
  92. package/dist/source-transform.d.ts +57 -0
  93. package/dist/splat-budget-PSojLJPO.js +157 -0
  94. package/dist/splat-budget-PSojLJPO.js.map +1 -0
  95. package/dist/splat-budget.d.ts +404 -0
  96. package/dist/splat-data-BZM5dhhx.js +10 -0
  97. package/dist/splat-data-BZM5dhhx.js.map +1 -0
  98. package/dist/splat-data.d.ts +150 -0
  99. package/dist/splat-depth-pack.d.ts +39 -0
  100. package/dist/splat-mesh-material.d.ts +255 -0
  101. package/dist/splat-mesh-picking.d.ts +87 -0
  102. package/dist/splat-mesh-pool.d.ts +241 -0
  103. package/dist/splat-mesh-types-8QDjTbZO.js +390 -0
  104. package/dist/splat-mesh-types-8QDjTbZO.js.map +1 -0
  105. package/dist/splat-mesh-types.d.ts +390 -0
  106. package/dist/splat-mesh.d.ts +737 -0
  107. package/dist/splat-modifier-stack.d.ts +38 -0
  108. package/dist/splat-modifier.d.ts +156 -0
  109. package/dist/splat-partition.d.ts +37 -0
  110. package/dist/splat-query.d.ts +51 -0
  111. package/dist/splat-scene.d.ts +127 -0
  112. package/dist/splat-sort-bounds.d.ts +11 -0
  113. package/dist/static-lod-splat-mesh.d.ts +53 -0
  114. package/dist/static-lod-worker-D2fyqQZO.js +474 -0
  115. package/dist/static-lod-worker-D2fyqQZO.js.map +1 -0
  116. package/dist/static-lod-worker-protocol.d.ts +37 -0
  117. package/dist/static-lod.d.ts +24 -0
  118. package/dist/storage-attribute-mirror.d.ts +103 -0
  119. package/dist/streamed-splat-mesh-utils.d.ts +146 -0
  120. package/dist/streamed-splat-mesh.d.ts +985 -0
  121. package/dist/unified-splat-renderer.d.ts +205 -0
  122. package/dist/webgpu-limits.d.ts +133 -0
  123. package/dist/work-buffer-gather.d.ts +119 -0
  124. package/dist/work-buffer-material.d.ts +42 -0
  125. package/dist/worker-sorter.d.ts +57 -0
  126. package/dist/xr-view.d.ts +92 -0
  127. 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
+ }