Potree file formats (1.x .hrc/.bin and 2.0 binary octree)
Reference for potree-to-3dtiles (and the planned potree-to-copc). Two generations:
Reference for potree-to-3dtiles (and the planned potree-to-copc). Two generations:
Potree 1.x (cloud.js + per-node files) — authoritative spec
Sources (Potree repo docs/, pinned commits):
- 1.7: https://github.com/potree/potree/blob/c266f3391ffa7149f832426a18c94492fb17bf49/docs/potree-file-format.md
- older: https://github.com/potree/potree/blob/7edc0c536b59e6747cd068856440e7cb54bdaa33/docs/file_format.md
- (find via
repo:potree/potree file-format.mdcommit search)
cloud.js (meta): version, octreeDir, boundingBox{lx,ly,lz,ux,uy,uz} (world bbox),
tightBoundingBox, pointAttributes[], spacing (root point spacing, halved per level),
scale, hierarchyStepSize. cloud.js may also carry a projection/proj4 CRS string — recover it.
Point decode — THE load-bearing rule. The spec writes position = (POSITION_CARTESIAN * scale) + boundingBox.min, but boundingBox.min is the NODE's box min, not the global cloud min — the loader
subdivides the cloud bbox per node (createChildAABB) and adds that node's min. POSITION_CARTESIAN
is node-relative: raw uint32 span 0..(nodeWidth/scale).
- ⚠️ Using the global cloud min for every node piles all geometry at the min corner (raw values are small node-relative offsets). Using a per-node min computed with the wrong octant convention scatters nodes into wrong octants. BOTH must be right: node-relative decode with the correct octant boxes.
- Octant → sub-box convention (Potree
PointCloudOctreeGeometryNode.createChildAABB): child index bit 0 (0b001) → Z, bit 1 (0b010) → Y, bit 2 (0b100) → X. (Not bit0→X — that X/Z swap scatters; verified via child→root nearest-neighbour: 0.03 m correct vs 0.81 m swapped.) POSITION_CARTESIAN= 3× uint32 (12 B);COLOR_PACKED/RGBA= 4× uint8;INTENSITYu16;CLASSIFICATIONu8;NORMAL_SPHEREMAPPED2 B. Stride = Σ attribute sizes, little-endian, inpointAttributesorder.
Hierarchy (.hrc): a node r + octants 0–7. Each .hrc record = uint8 childMask + uint32 numPoints (BFS within a chunk of hierarchyStepSize levels; the deepest level recurses into a sub-.hrc).
childMask bit c set ⇒ child c exists. File path: octreeDir/r/<name[1..] chunked by stepSize>/<name>.<bin|laz>.
Node files are .bin (raw), .las, or .laz (pointAttributes:"LAZ").
1.4 variant: inline hierarchy:[[name,numPoints],…] in cloud.js (no .hrc, no hierarchyStepSize),
flat data/<name>.<bin|laz>.
Potree 2.0 (metadata.json + hierarchy.bin + octree.bin)
metadata.json:attributes[{name,type,size,numElements}],scale[3],offset[3],boundingBox,spacing,encoding(DEFAULT|BROTLI),hierarchy.firstChunkSize.hierarchy.bin: 22 B/node —type u8, childMask u8, numPoints u32, byteOffset u64, byteSize u64;type==2= PROXY (descend into a child hierarchy chunk).octree.bin: per-node point records. DEFAULT = interleavedint32×3·scale+offset+ attrs. BROTLI = per-node brotli; the decompressed buffer is attribute-PLANAR and position/rgb are Morton-encoded (16 B / 8 B per point; de-interleave with Potree'sdealign24b). SeeDecoderWorker_brotli.js.
Both store positions as integers + a global scale(+offset/boundingBox.min) → the octree maps
directly onto an EPT/COPC-style octree (r, r0..r7), which is why potree-to-copc is natural.
Feature request draft — expose SparkRenderer LoD options in 3d-tiles-rendererjs-3dgs-plugin
Target repo: https://github.com/WilliamLiu-1997/3D-Tiles-RendererJS-3DGS-Plugin
RAD (SparkJS / World Labs) → 3D Tiles — format notes & why it isn't "streamed"
Notes distilled from the Spark team + WilliamLiu (3DGS-PLY-3DTiles-Converter) on GitHub, captured to