IO 模块架构
IO 模块充当 Visionary 的 统一数据摄入层。它驱动与加载泼溅数据和 3D 网格数据相关的一切操作。它嗅探文件格式,解析原始字节,并将 GPU 就绪的缓冲区移交给技术栈的其余部分。该模块将各种文件格式(标准 PLY、压缩高斯格式和 Three.js 网格)的复杂性抽象为一致的内存布局(DataSource),应用程序无需了解底层文件结构即可使用。
分层设计
┌─────────────────────────────────────────────────────────┐
│ 应用层 (Application layer) │ UI, 进度回调
├─────────────────────────────────────────────────────────┤
│ 通用加载器外观 (Universal loader) │ 格式检测, 委托
├─────────────────────────────────────────────────────────┤
│ 特定格式加载器 (Format-specific) │ PLY, SPZ, KSplat, SPLAT, SOG 等
├─────────────────────────────────────────────────────────┤
│ 数据处理与验证 (Processing) │ 解析, 变换, 检查
├─────────────────────────────────────────────────────────┤
│ 缓冲区管理 (Buffer management) │ 打包, 内存调优
└─────────────────────────────────────────────────────────┘
接口层 (index.ts)
定义所有加载器必须遵守的 TypeScript 契约。每个加载器都返回一个 DataSource(GaussianDataSource 或 ThreeJSDataSource),因此渲染器永远不必关心原始文件格式。
interface GaussianDataSource {
gaussianBuffer(): ArrayBuffer; // 打包的泼溅参数(半精度浮点数)
shCoefsBuffer(): ArrayBuffer; // 打包的 SH 系数
numPoints(): number; // 泼溅总数
shDegree(): number; // SH 阶数 (通常为 0..3)
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 对象
modelType(): string; // 格式标识符
bbox(): { min: [number, number, number]; max: [number, number, number] };
// ... 可选元数据
}
设计原则:
- 无论源格式如何,都使用 统一接口。
- 惰性缓冲区 (Lazy buffers),在访问时生成,而不是急切存储。
- 可选元数据 用于特定格式的提示(相机默认值、内核大小)。
- 为下游调用者提供完全类型化的 TypeScript 工效学。
通用加载器 (universal_loader.ts)
一个外观模式 (Facade),知道如何选择正确的加载器,流式传输进度,并保持 API 表面一致。它管理 两个独立的注册表:一个用于高斯格式,一个用于 Three.js 网格格式。
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 枚举:
亮点:
- 双注册表系统: 高斯和 Three.js 加载器的独立注册表,允许同一扩展名(例如
.ply)由不同的加载器处理 - 智能路由: 通过扩展名、MIME 类型和魔数嗅探自动检测格式
- PLY 歧义消除: 通过检查头部属性(
rot_0,scale_0,opacity等)自动检测 PLY 文件是 3DGS 还是网格格式 - 运行时注册: 支持新加载器的运行时注册(插件、企业格式等)
- 进度报告: 发出基于阶段的进度更新,使 UX 感觉生动
- 统一输出: 将所有结果归一化为
DataSource(GaussianDataSource或ThreeJSDataSource)
路由策略:
- 显式配置: 如果
options.isGaussian === true,强制在高斯加载器注册表中搜索 - 特殊处理:
.compressed.ply立即路由到CompressedPLYLoader - 扩展名匹配:
.spz,.sog,.splat,.ksplat等文件路由到各自的加载器 - PLY 检测: 对于
.ply文件,读取头部以检测 3DGS 属性(rot_0,scale_0,opacity) - 内容嗅探: 检测二进制魔数(GZIP: 0x1f8b 用于 SPZ, KSPL: "KSPL", ZIP: 0x504b0304 用于 SOG)以消除格式歧义
- 回退: 如果扩展名不匹配,则使用
canHandle()方法遍历加载器
特定格式加载器
IO 模块包含针对多种高斯格式的专用实现:
PLY 加载器 (ply_loader.ts):
- 处理带有 Visionary 扩展的 ASCII 和二进制 PLY
- 验证必需的 3DGS 属性(rot_0, scale_0, opacity 等)
- 支持小端和大端二进制格式
- 执行归一化、四元数转换和 SH 系数打包
SPZ 加载器 (spz_loader.ts):
- 基于 GZIP 的容器格式
- 处理量化后的位置/旋转
- 通过 .spz 扩展名或 GZIP 魔数检测
KSplat 加载器 (ksplat_loader.ts):
- Luma AI 优化的块格式
- 检测 "KSPL" 魔数(前 4 个字节)
- 针对快速加载进行了优化
Splat 加载器 (splat_loader.ts):
- 原始二进制转储格式(每个点 32 字节步长)
- 加载极快(无解析开销)
- 格式: [x, y, z, r, g, b, a, rot_0, rot_1, rot_2, rot_3, scale_0, scale_1, scale_2] (均为 float32)
SOG 加载器 (sog_loader.ts):
- SuperOrdered Gaussians 格式
- 支持原始 (Raw) 和 ZIP 压缩变体
- 检测 ZIP 魔数 (0x504b0304)
- 处理分层高斯组织
压缩 PLY 加载器 (compressed_ply_loader.ts):
- 使用基于分块的数据量化的 PLY 格式
- 使用分块元数据存储量化边界(最小/最大值)
- 使用位级量化打包位置、旋转、缩放和颜色
- 每个分块包含 256 个点,共享量化参数
- 通过 .compressed.ply 扩展名检测
Three.js 适配器 (threejs_adapters.ts):
- 包装标准 Three.js 加载器以符合 ILoader 接口
- 支持 FBX, GLTF, OBJ, STL, 和 Mesh PLY
- 自动应用阴影设置和回退材质
- 可用时保留原始材质
所有加载器都实现 ILoader<T> 接口:
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[];
}
关键处理步骤(PLY 示例):
- 头部解析 – 检测 ASCII 与二进制,收集属性模式,验证必填字段
- 顶点提取 – 将行(文本或 DataView)解码为类型化数组
- 高斯处理 – 映射位置、四元数、缩放、不透明度、SH 系数
- 缓冲区打包 – 压缩为准备好上传 GPU 的半精度浮点数和交错的 SH 字
数据处理管道
- 位置维护 min/max 以构建边界框。
- 四元数被归一化 (
(x, y, z, w)→(w, x, y, z)). - 对数空间缩放通过
Math.exp变为线性。 - 不透明度通过 sigmoid 函数处理。
- 协方差矩阵被推导并以右上三角形式存储。
- SH 系数将两个 float16 值打包到每个
uint32中。
进度架构
LoadingOptions 支持取消和细粒度的进度报告:
interface LoadingOptions {
onProgress?: (progress: LoadingProgress) => void;
signal?: AbortSignal;
debug?: boolean;
isGaussian?: boolean; // 显式格式提示
}
interface LoadingProgress {
stage: 'fetch' | 'parse' | 'pack' | string;
progress: number; // 0..1
message?: string;
}
加载器在阶段边界和每处理几千个顶点时更新进度,因此长时间运行的导入操作可以保持 UI 响应。
内存策略
- 预先分配输出缓冲区(用于高斯的
Uint16Array,用于 SH 的Uint32Array)。 - 逐点流式处理行以避免内存使用峰值。
- 迭代时直接转换为半精度浮点数——无需临时数组。
- 使用后立即清理中间视图。
错误处理
IO 模块在多个层级进行验证并抛出描述性错误:
| 层级 | 示例验证 | 典型错误 |
|---|---|---|
| 格式 | 魔数头, ASCII 标记 | UnsupportedFormatError |
| 模式 | 必需属性存在 | MissingPropertyError |
| 数据 | 有限数值 | MalformedRowError |
| 处理 | 协方差数学计算成功 | ProcessingError |
所有错误都继承自共享的 IOError,其中包括 stage?: 'fetch' | 'parse' | 'pack' | ...,以便 UI 标注失败位置。
格式检测工具
IO 模块提供了几个用于格式检测的辅助函数:
GaussianFormat 枚举:
enum GaussianFormat {
PLY = 'ply',
SPZ = 'spz',
KSPLAT = 'ksplat',
SPLAT = 'splat',
SOG = 'sog',
COMPRESSED_PLY = 'compressed.ply'
}
检测函数:
detectGaussianFormat(filename: string): GaussianFormat | null– 从文件名返回格式枚举isGaussianFormat(filename: string): boolean– 用于 UI 过滤的布尔检查getSupportedGaussianFormats(): string[]– 返回所有支持的高斯扩展名
类型守卫:
isGaussianDataSource(data: DataSource): data is GaussianDataSource– 运行时类型检查isThreeJSDataSource(data: DataSource): data is ThreeJSDataSource– 运行时类型检查
PLY 歧义消除逻辑
该模块实现了复杂的 PLY 文件歧义消除,以区分 3D 高斯泼溅文件和传统网格文件:
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)
);
}
过程:
- 对于
.ply文件,读取前 4KB 的头部 - 检查所有必需的 3DGS 属性
- 如果全部存在 → 路由到
PLYLoader(高斯) - 如果缺失 → 路由到
ThreeJSPLYLoaderAdapter(网格)
场景保存架构
unified-scene-saver.ts 模块提供场景导出功能:
interface SaveUnifiedSceneParams {
scenes: SaveUnifiedSceneSceneEntry[];
folderHandle: FileSystemDirectoryHandle;
meta?: any;
cameraParams?: any;
totalFrames?: number;
}
特性:
- 将场景结构保存为 JSON (
scene.json) - 将模型文件复制到目标目录
- 处理 FileSystem API 权限
- 验证文件夹句柄有效性
- 保存前清空目标文件夹
- 支持关键帧和模型元数据
过程:
- 验证文件夹句柄权限
- 清空现有文件夹内容
- 复制所有模型文件(按名称去重)
- 生成包含场景结构的
scene.json - 将 JSON 写入目标文件夹
扩展性手册
要添加新格式:
- 实现
ILoader针对该格式(ILoader<GaussianDataSource>或ILoader<ThreeJSDataSource>) - 包装结果 到满足相应
DataSource接口的类中 -
注册加载器 到通用外观:
-
可选共享 辅助工具(半精度打包、边界框)以保持一致性
该架构也预见到了压缩或流式格式——只需为通用加载器提供一个理解该协议的加载器即可。