IO Module Architecture
The IO module serves as the Unified Data Ingestion Layer for Visionary. It powers everything related to loading splat data and 3D mesh data. It sniffs file formats, parses raw bytes, and hands off GPU-ready buffers to the rest of the stack. The module abstracts the complexity of various file formats (Standard PLY, Compressed Gaussian formats, and Three.js Meshes) into a consistent memory layout (DataSource) that the application can consume without knowing the underlying file structure.
Layered design
┌─────────────────────────────────────┐
│ Application layer │ UI, progress callbacks
├─────────────────────────────────────┤
│ Universal loader facade │ Format detection, delegation
├─────────────────────────────────────┤
│ Format-specific loaders │ PLY, SPZ, KSplat, SPLAT, SOG, etc.
├─────────────────────────────────────┤
│ Data processing & validation │ Parsing, transforms, checks
├─────────────────────────────────────┤
│ Buffer management │ Packing, memory tuning
└─────────────────────────────────────┘
Interface layer (index.ts)
Defines the TypeScript contracts that all loaders must honour. Every loader returns a DataSource (either GaussianDataSource or ThreeJSDataSource) so the renderer never cares about original file formats.
interface GaussianDataSource {
gaussianBuffer(): ArrayBuffer; // Packed splat parameters (half-floats)
shCoefsBuffer(): ArrayBuffer; // Packed SH coefficients
numPoints(): number; // Total splats
shDegree(): number; // SH degree (0..3 typical)
bbox(): { min: [number, number, number]; max: [number, number, number] };
center?: [number, number, number];
up?: [number, number, number] | null;
kernelSize?: number;
backgroundColor?: [number, number, number];
}
interface ThreeJSDataSource {
object3D(): THREE.Object3D; // Three.js object
modelType(): string; // Format identifier
bbox(): { min: [number, number, number]; max: [number, number, number] };
// ... optional metadata
}
Design principles:
- A uniform interface regardless of source format.
- Lazy buffers that are produced when accessed, not eagerly stored.
- Optional metadata for format-specific hints (camera defaults, kernel sizes).
- Fully typed TypeScript ergonomics for downstream callers.
Universal Loader (universal_loader.ts)
A facade that knows how to pick the right loader, stream progress, and keep the API surface consistent. It manages two separate registries: one for Gaussian formats and one for Three.js mesh formats.
class UniversalLoader implements ILoader, LoaderRegistry {
private gaussianLoaders = new Map<string, ILoader>();
private threeLoaders = new Map<string, ILoader>();
register<T extends DataSource>(loader: ILoader<T>, extensions: string[], type: LoaderType): void;
getLoader(filename: string, mimeType?: string, options?: { isGaussian?: boolean }): ILoader | null;
loadFile(file: File, options?: LoadingOptions): Promise<DataSource>;
loadUrl(url: string, options?: LoadingOptions): Promise<DataSource>;
loadBuffer(buffer: ArrayBuffer, options?: LoadingOptions): Promise<DataSource>;
canHandle(filename: string, mimeType?: string, options?: { isGaussian?: boolean }): boolean;
getAllSupportedExtensions(): string[];
}
LoaderType Enum:
Highlights:
- Dual Registry System: Separate registries for Gaussian and Three.js loaders, allowing the same extension (e.g.,
.ply) to be handled by different loaders - Intelligent Routing: Auto-detects formats by extension, MIME type, and magic-byte sniffing
- PLY Disambiguation: Automatically detects if a PLY file is 3DGS or mesh format by checking header properties (
rot_0,scale_0,opacity, etc.) - Runtime Registration: Supports runtime registration of new loaders (plugins, enterprise formats, etc.)
- Progress Reporting: Emits stage-based progress updates so UX feels alive
- Unified Output: Normalises all results to
DataSource(eitherGaussianDataSourceorThreeJSDataSource)
Routing Strategy:
- Explicit Configuration: If
options.isGaussian === true, forces search in Gaussian loader registry - Special Handling:
.compressed.plyis immediately routed toCompressedPLYLoader - Extension Matching: Files like
.spz,.sog,.splat,.ksplatare routed to their respective loaders - PLY Detection: For
.plyfiles, reads header to detect 3DGS properties (rot_0,scale_0,opacity) - Content Sniffing: Detects binary magic bytes (GZIP: 0x1f8b for SPZ, KSPL: "KSPL", ZIP: 0x504b0304 for SOG) to disambiguate formats
- Fallback: If extension doesn't match, iterates through loaders using
canHandle()method
Format-Specific Loaders
The IO module includes specialized implementations for multiple Gaussian formats:
PLY Loader (ply_loader.ts):
- Handles ASCII and binary PLY with Visionary extensions
- Validates required 3DGS properties (rot_0, scale_0, opacity, etc.)
- Supports both little-endian and big-endian binary formats
- Performs normalization, quaternion conversion, and SH coefficient packing
SPZ Loader (spz_loader.ts):
- GZIP-based container format
- Handles quantized positions/rotations
- Detected by .spz extension or GZIP magic bytes
KSplat Loader (ksplat_loader.ts):
- Luma AI optimized block format
- Detects "KSPL" magic bytes (first 4 bytes)
- Optimized for fast loading
Splat Loader (splat_loader.ts):
- Raw binary dump format (32-byte stride per point)
- Extremely fast loading (no parsing overhead)
- Format: [x, y, z, r, g, b, a, rot_0, rot_1, rot_2, rot_3, scale_0, scale_1, scale_2] (all float32)
SOG Loader (sog_loader.ts):
- SuperOrdered Gaussians format
- Supports both Raw and ZIP-compressed variants
- Detects ZIP magic bytes (0x504b0304)
- Handles hierarchical Gaussian organization
Compressed PLY Loader (compressed_ply_loader.ts):
- Quantized PLY format using chunk-based data compression
- Uses chunk metadata to store quantization bounds (min/max values)
- Packs positions, rotations, scales, and colors with bit-level quantization
- Each chunk contains 256 points with shared quantization parameters
- Detected by .compressed.ply extension
Three.js Adapters (threejs_adapters.ts):
- Wraps standard Three.js loaders to conform to ILoader interface
- Supports FBX, GLTF, OBJ, STL, and Mesh PLY
- Automatically applies shadow settings and fallback materials
- Preserves original materials when available
All loaders implement the ILoader<T> interface:
interface ILoader<T extends DataSource = DataSource> {
loadFile(file: File, options?: LoadingOptions): Promise<T>;
loadUrl(url: string, options?: LoadingOptions): Promise<T>;
loadBuffer(buffer: ArrayBuffer, options?: LoadingOptions): Promise<T>;
canHandle(filename: string, mimeType?: string): boolean;
getSupportedExtensions(): string[];
}
Key Processing Steps (PLY Example):
- Header parsing – detect ASCII vs binary, collect property schema, validate required fields
- Vertex extraction – decode rows (text or DataView) into typed arrays
- Gaussian processing – map positions, quaternions, scales, opacities, SH coefficients
- Buffer packing – compress to half-floats and interleaved SH words ready for GPU upload
Data processing pipeline
- Positions maintain min/max to build bounding boxes.
- Quaternions are normalised (
(x, y, z, w)→(w, x, y, z)). - Log-space scales become linear with
Math.exp. - Opacity runs through a sigmoid.
- Covariance matrix is derived and stored in upper-triangular form.
- SH coefficients pack two float16 values into each
uint32.
Progress architecture
LoadingOptions supports cancellation and fine-grained progress reporting:
interface LoadingOptions {
onProgress?: (progress: LoadingProgress) => void;
signal?: AbortSignal;
debug?: boolean;
isGaussian?: boolean; // Explicit format hint
}
interface LoadingProgress {
stage: 'fetch' | 'parse' | 'pack' | string;
progress: number; // 0..1
message?: string;
}
The loader updates progress at stage boundaries and every few thousand vertices so long-running imports stay responsive.
Memory strategy
- Allocate output buffers up front (
Uint16Arrayfor gaussians,Uint32Arrayfor SH). - Stream rows point-by-point to avoid spikes in memory usage.
- Convert to half-floats directly while iterating—no temporary arrays.
- Clean up intermediate views immediately after use.
Error handling
The IO module validates at multiple levels and surfaces descriptive errors:
| Layer | Example validation | Typical error |
|---|---|---|
| Format | Magic header, ASCII marker | UnsupportedFormatError |
| Schema | Required properties present | MissingPropertyError |
| Data | Finite numeric values | MalformedRowError |
| Processing | Covariance math succeeds | ProcessingError |
All errors extend a shared IOError that includes stage?: 'fetch' | 'parse' | 'pack' | ... so the UI can annotate failures.
Format Detection Utilities
The IO module provides several helper functions for format detection:
GaussianFormat Enum:
enum GaussianFormat {
PLY = 'ply',
SPZ = 'spz',
KSPLAT = 'ksplat',
SPLAT = 'splat',
SOG = 'sog',
COMPRESSED_PLY = 'compressed.ply'
}
Detection Functions:
- detectGaussianFormat(filename: string): GaussianFormat | null – Returns format enum from filename
- isGaussianFormat(filename: string): boolean – Boolean check for UI filtering
- getSupportedGaussianFormats(): string[] – Returns all supported Gaussian extensions
Type Guards:
- isGaussianDataSource(data: DataSource): data is GaussianDataSource – Runtime type checking
- isThreeJSDataSource(data: DataSource): data is ThreeJSDataSource – Runtime type checking
PLY Disambiguation Logic
The module implements sophisticated PLY file disambiguation to distinguish between 3D Gaussian Splatting files and traditional mesh files:
private is3dgsPlyFromHeader(header: string): boolean {
const requiredKeywords = [
'property float opacity',
'property float scale_0',
'property float scale_1',
'property float scale_2',
'property float rot_0',
'property float rot_1',
'property float rot_2',
'property float rot_3'
];
return requiredKeywords.every(keyword =>
header.toLowerCase().includes(keyword)
);
}
Process:
1. For .ply files, read first 4KB of header
2. Check for all required 3DGS properties
3. If all present → route to PLYLoader (Gaussian)
4. If missing → route to ThreeJSPLYLoaderAdapter (Mesh)
Scene Saving Architecture
The unified-scene-saver.ts module provides scene export functionality:
interface SaveUnifiedSceneParams {
scenes: SaveUnifiedSceneSceneEntry[];
folderHandle: FileSystemDirectoryHandle;
meta?: any;
cameraParams?: any;
totalFrames?: number;
}
Features:
- Saves scene structure as JSON (scene.json)
- Copies model files to target directory
- Handles FileSystem API permissions
- Validates folder handle validity
- Clears target folder before saving
- Supports keyframes and model metadata
Process:
1. Verify folder handle permissions
2. Clear existing folder contents
3. Copy all model files (deduplicated by name)
4. Generate scene.json with scene structure
5. Write JSON to target folder
Extensibility Playbook
To add a new format:
- Implement
ILoaderfor the format (eitherILoader<GaussianDataSource>orILoader<ThreeJSDataSource>) - Wrap results in a class that satisfies the appropriate
DataSourceinterface - Register the loader with the universal facade:
- Optionally share helper utilities (half-float packing, bounding boxes) for consistency
The architecture also anticipates compressed or streaming formats—just feed the universal loader a loader that understands the protocol.