跳转至

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 契约。每个加载器都返回一个 DataSourceGaussianDataSourceThreeJSDataSource),因此渲染器永远不必关心原始文件格式。

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 枚举:

enum LoaderType {
  GAUSSIAN = 'gaussian',
  THREE = 'three'
}

亮点:

  • 双注册表系统: 高斯和 Three.js 加载器的独立注册表,允许同一扩展名(例如 .ply)由不同的加载器处理
  • 智能路由: 通过扩展名、MIME 类型和魔数嗅探自动检测格式
  • PLY 歧义消除: 通过检查头部属性(rot_0, scale_0, opacity 等)自动检测 PLY 文件是 3DGS 还是网格格式
  • 运行时注册: 支持新加载器的运行时注册(插件、企业格式等)
  • 进度报告: 发出基于阶段的进度更新,使 UX 感觉生动
  • 统一输出: 将所有结果归一化为 DataSourceGaussianDataSourceThreeJSDataSource

路由策略:

  1. 显式配置: 如果 options.isGaussian === true,强制在高斯加载器注册表中搜索
  2. 特殊处理: .compressed.ply 立即路由到 CompressedPLYLoader
  3. 扩展名匹配: .spz, .sog, .splat, .ksplat 等文件路由到各自的加载器
  4. PLY 检测: 对于 .ply 文件,读取头部以检测 3DGS 属性(rot_0, scale_0, opacity
  5. 内容嗅探: 检测二进制魔数(GZIP: 0x1f8b 用于 SPZ, KSPL: "KSPL", ZIP: 0x504b0304 用于 SOG)以消除格式歧义
  6. 回退: 如果扩展名不匹配,则使用 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 示例):

  1. 头部解析 – 检测 ASCII 与二进制,收集属性模式,验证必填字段
  2. 顶点提取 – 将行(文本或 DataView)解码为类型化数组
  3. 高斯处理 – 映射位置、四元数、缩放、不透明度、SH 系数
  4. 缓冲区打包 – 压缩为准备好上传 GPU 的半精度浮点数和交错的 SH 字

数据处理管道

PLY 文件 ──► 头解析 ──► 顶点解码 ──► 高斯变换 ──► 缓冲区打包
  • 位置维护 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)
  );
}

过程:

  1. 对于 .ply 文件,读取前 4KB 的头部
  2. 检查所有必需的 3DGS 属性
  3. 如果全部存在 → 路由到 PLYLoader (高斯)
  4. 如果缺失 → 路由到 ThreeJSPLYLoaderAdapter (网格)

场景保存架构

unified-scene-saver.ts 模块提供场景导出功能:

interface SaveUnifiedSceneParams {
  scenes: SaveUnifiedSceneSceneEntry[];
  folderHandle: FileSystemDirectoryHandle;
  meta?: any;
  cameraParams?: any;
  totalFrames?: number;
}

特性:

  • 将场景结构保存为 JSON (scene.json)
  • 将模型文件复制到目标目录
  • 处理 FileSystem API 权限
  • 验证文件夹句柄有效性
  • 保存前清空目标文件夹
  • 支持关键帧和模型元数据

过程:

  1. 验证文件夹句柄权限
  2. 清空现有文件夹内容
  3. 复制所有模型文件(按名称去重)
  4. 生成包含场景结构的 scene.json
  5. 将 JSON 写入目标文件夹

扩展性手册

要添加新格式:

  1. 实现 ILoader 针对该格式(ILoader<GaussianDataSource>ILoader<ThreeJSDataSource>
  2. 包装结果 到满足相应 DataSource 接口的类中
  3. 注册加载器 到通用外观:

    loader.register(new FooLoader(), ['.foo'], LoaderType.GAUSSIAN);
    // 或者
    loader.register(new FooMeshLoader(), ['.foo'], LoaderType.THREE);
    

  4. 可选共享 辅助工具(半精度打包、边界框)以保持一致性

该架构也预见到了压缩或流式格式——只需为通用加载器提供一个理解该协议的加载器即可。