Skip to content

export

occulus.export

3D Tiles and Potree export for web visualization.

Converts point clouds to formats suitable for browser-based 3-D viewers:

  • 3D Tiles (Cesium) — tileset.json + .pnts binary tiles
  • Potree — octree hierarchy with metadata.json

export_3dtiles(cloud, output_dir, *, max_points_per_tile=50000, geometric_error=10.0)

Export a point cloud as a 3D Tiles tileset.

Generates a tileset.json and .pnts binary files suitable for display in CesiumJS or other 3D Tiles viewers.

Parameters:

Name Type Description Default
cloud PointCloud

Input point cloud.

required
output_dir str or Path

Output directory for the tileset.

required
max_points_per_tile int

Maximum points per .pnts file.

50000
geometric_error float

Root tile geometric error in metres.

10.0

Returns:

Type Description
Path

Path to the generated tileset.json.

Raises:

Type Description
OcculusExportError

If the export fails.

Source code in src/occulus/export/__init__.py
def export_3dtiles(
    cloud: PointCloud,
    output_dir: str | Path,
    *,
    max_points_per_tile: int = 50_000,
    geometric_error: float = 10.0,
) -> Path:
    """Export a point cloud as a 3D Tiles tileset.

    Generates a ``tileset.json`` and ``.pnts`` binary files suitable
    for display in CesiumJS or other 3D Tiles viewers.

    Parameters
    ----------
    cloud : PointCloud
        Input point cloud.
    output_dir : str or Path
        Output directory for the tileset.
    max_points_per_tile : int
        Maximum points per ``.pnts`` file.
    geometric_error : float
        Root tile geometric error in metres.

    Returns
    -------
    Path
        Path to the generated ``tileset.json``.

    Raises
    ------
    OcculusExportError
        If the export fails.
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    xyz = cloud.xyz
    n = len(xyz)

    if n == 0:
        raise OcculusExportError("Cannot export empty point cloud to 3D Tiles")

    # Compute bounding volume (region or box)
    center = xyz.mean(axis=0)
    half_extent = (xyz.max(axis=0) - xyz.min(axis=0)) / 2

    # Split into chunks
    chunks = []
    for i in range(0, n, max_points_per_tile):
        chunk = xyz[i : i + max_points_per_tile]
        chunks.append(chunk)

    logger.info("Exporting %d points as %d 3D Tiles chunks", n, len(chunks))

    # Write .pnts files
    children = []
    for idx, chunk in enumerate(chunks):
        pnts_name = f"tile_{idx:04d}.pnts"
        pnts_path = output_dir / pnts_name
        _write_pnts(chunk, center, pnts_path)

        chunk_center = chunk.mean(axis=0)
        chunk_half = (chunk.max(axis=0) - chunk.min(axis=0)) / 2

        children.append(
            {
                "boundingVolume": {
                    "box": [
                        *chunk_center.tolist(),
                        chunk_half[0],
                        0,
                        0,
                        0,
                        chunk_half[1],
                        0,
                        0,
                        0,
                        chunk_half[2],
                    ]
                },
                "geometricError": 0.0,
                "content": {"uri": pnts_name},
            }
        )

    # Write tileset.json
    tileset = {
        "asset": {"version": "1.0", "generator": "occulus"},
        "geometricError": geometric_error,
        "root": {
            "boundingVolume": {
                "box": [
                    *center.tolist(),
                    half_extent[0],
                    0,
                    0,
                    0,
                    half_extent[1],
                    0,
                    0,
                    0,
                    half_extent[2],
                ]
            },
            "geometricError": geometric_error,
            "refine": "ADD",
            "children": children,
        },
    }

    tileset_path = output_dir / "tileset.json"
    tileset_path.write_text(json.dumps(tileset, indent=2))
    logger.info("3D Tiles tileset → %s (%d tiles)", tileset_path, len(chunks))
    return tileset_path

export_potree(cloud, output_dir, *, max_depth=10, max_points_per_node=50000)

Export a point cloud in Potree 2.0 format.

Generates an octree hierarchy with metadata.json and binary .bin chunks for use with the Potree web viewer.

Parameters:

Name Type Description Default
cloud PointCloud

Input point cloud.

required
output_dir str or Path

Output directory.

required
max_depth int

Maximum octree depth.

10
max_points_per_node int

Maximum points before splitting a node.

50000

Returns:

Type Description
Path

Path to the generated metadata.json.

Raises:

Type Description
OcculusExportError

If the export fails.

Source code in src/occulus/export/__init__.py
def export_potree(
    cloud: PointCloud,
    output_dir: str | Path,
    *,
    max_depth: int = 10,
    max_points_per_node: int = 50_000,
) -> Path:
    """Export a point cloud in Potree 2.0 format.

    Generates an octree hierarchy with ``metadata.json`` and binary
    ``.bin`` chunks for use with the Potree web viewer.

    Parameters
    ----------
    cloud : PointCloud
        Input point cloud.
    output_dir : str or Path
        Output directory.
    max_depth : int
        Maximum octree depth.
    max_points_per_node : int
        Maximum points before splitting a node.

    Returns
    -------
    Path
        Path to the generated ``metadata.json``.

    Raises
    ------
    OcculusExportError
        If the export fails.
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    xyz = cloud.xyz
    n = len(xyz)

    if n == 0:
        raise OcculusExportError("Cannot export empty point cloud to Potree")

    bb_min = xyz.min(axis=0)
    bb_max = xyz.max(axis=0)
    bb_size = bb_max - bb_min
    cube_size = float(bb_size.max())

    logger.info("Exporting %d points as Potree octree (max depth %d)", n, max_depth)

    # Build simple octree
    nodes = _build_octree(xyz, bb_min, cube_size, max_depth, max_points_per_node)

    # Write node binary files
    octree_dir = output_dir / "octree"
    octree_dir.mkdir(exist_ok=True)

    hierarchy = {}
    for node_key, node_xyz in nodes.items():
        bin_path = octree_dir / f"{node_key}.bin"
        node_xyz.astype(np.float32).tofile(bin_path)
        hierarchy[node_key] = len(node_xyz)

    # Write metadata
    metadata = {
        "version": "2.0",
        "name": "occulus_export",
        "points": n,
        "boundingBox": {
            "min": bb_min.tolist(),
            "max": bb_max.tolist(),
        },
        "encoding": "DEFAULT",
        "scale": [0.001, 0.001, 0.001],
        "offset": bb_min.tolist(),
        "hierarchy": hierarchy,
    }

    meta_path = output_dir / "metadata.json"
    meta_path.write_text(json.dumps(metadata, indent=2))
    logger.info("Potree export → %s (%d nodes)", meta_path, len(nodes))
    return meta_path