跳到主要内容

SOG 格式

**SOG(Spatially Ordered Gaussians,空间有序高斯)**是紧凑的 3D 高斯泼溅容器。通过有损量化获得高压缩率,通常比等价 PLY 小约 15–20 倍。

可用 SplatTransform 创建,并在 PlayCanvas 查看器 预览。

本页为版本 3 规范。旧版见 SOG v2,第 8 节总结变更。

1. 文件集合​

数据集由一组图像和元数据文件组成:

文件用途通道(8 位)
meta.json场景元数据与文件名—
means_l.webp位置低 8 位R,G,B
means_u.webp位置高 8 位R,G,B
scales.webp通过码本编码的各轴尺寸R,G,B
quats.webp朝向,24 位指数映射R,G,B
sh0.webp基础颜色(DC)与不透明度R,G,B,A
shN_centroids.webp球谐调色板系数,可选R,G,B
shN_labels.webp球谐调色板索引,可选R,G,B
blind_<i>.webp逐单元盲数据,每张盲数据纹理一个文件,可选R,G,B,A 中 1–4 个
图像格式
  • 默认应使用无损 WebP,精确保留量化值。
  • meta.json 各属性指定文件名,因此可以使用其他支持 8 位 RGBA 的格式。
  • 不要使用有损编码,它会破坏数值,造成可见或结构性伪影。
  • 表中仅列 RGB 的图像应将 A 写为 255,读取器必须忽略 A。
  • 许多无损 WebP 编码器默认丢弃 alpha 为 0 的像素 RGB,除非明确要求保留(libwebp 的 exact 选项)。使用 A 字节的盲数据纹理(§3.7)可能含零 alpha,因此必须保留这些像素的 RGB。
  • 某些浏览器即使收到禁止预乘的请求,仍按 alpha 预乘解码图像,WebGPU 下 WebKit 就如此。这会在 A < 255 时损失 RGB 精度,A = 0 时完全丢失 RGB。因此只有 sh0(§3.4)和盲数据纹理(§3.7)使用 A 保存数据。这些浏览器中,半透明单元基础颜色,以及与非全不透明 A 共享纹素的盲字段,会损失精度。

1.1 图像尺寸与索引​

除 shN_centroids 外,所有属性图像的同一像素 x、y 都属于同一高斯单元。

  • 像素按行优先排列,原点在左上。
  • 宽 W、高 H 的图像可寻址 W*H 个单元。
  • meta.count 必须满足 <= W*H,末尾多余像素忽略。

从零开始的索引公式:

  • 索引转像素: x = i % W, y = floor(i / W)
  • 像素转索引: i = x + y * W

1.2 坐标系​

SOG 与 3DGS PLY 使用相同坐标系,位置、旋转和球谐系数原样保留训练结果。

放入 +Y 向上的右手世界(如 -Z 向前的 PlayCanvas)时,整体绕 Z 旋转 180 度,即 (x, y, z) → (−x, −y, z)。应将旋转应用到整个场景,例如承载节点的变换,因为旋转和球谐系数定义在文件坐标系中(§3.5.1)。

1.3 打包变体​

打包 SOG 是上述文件的 ZIP。读取器应支持两种布局:

  • 多文件目录,推荐制作期间使用。
  • 单归档,例如 scene.sog,相同文件位于归档根目录。

读取器必须解压后,像多文件版本一样通过 meta.json 解析文件。


2. meta.json​

interface Meta {
version: 3; // File format version (integer)
asset?: { // Optional tool/version metadata
generator?: string; // e.g. "splat-transform v1.2.3"
};
count: number; // Number of gaussians (<= W*H of the images)
model?: "antialiased" | "2dgs"; // Optional. How the scene was trained (§3.8); omitted for ordinary gaussians

means: {
// Ranges for decoding *log-transformed* positions (see §3.1).
mins: [number, number, number]; // min of nx,ny,nz (log-domain)
maxs: [number, number, number]; // max of nx,ny,nz (log-domain)
files: ["means_l.webp", "means_u.webp"];
};

scales: {
codebook: number[]; // 256 floats (log-domain); see §3.3
files: ["scales.webp"];
};

quats: {
files: ["quats.webp"]; // 24-bit exponential map (§3.2)
};

sh0: {
codebook: number[]; // 256 floats; DC coefficients for gamma-space color (§3.4)
files: ["sh0.webp"]; // alpha channel: opacity byte (§3.4)
};

// Present only if some gaussian's opacity exceeds 1 (§3.6):
opacity?: {
max: number; // The largest stored opacity, in (1, 1.5625]
};

// Present only if higher-order SH exist:
shN?: {
count: number; // Palette size (up to 16777216, §3.5)
bands: number; // Number of SH bands (1..4). DC (=band 0) lives in sh0.
codebook: number[]; // 256 floats; shared for all AC coefficients (§3.5)
files: [
"shN_centroids.webp",// Palette of AC coefficients as pixels (§3.5)
"shN_labels.webp" // Per-gaussian palette index (0..count-1)
];
};

// Present only if the scene carries blind data: one entry per blind texture (§3.7)
blind?: {
files: [string]; // e.g. "blind_0.webp"
fields: { // Packed in order into the texel's bytes (R, G, B, A)
name: string; // Application-defined name, e.g. the source PLY column
type: "u8" | "u16" | "u24" | "u32" | "f16" | "f32" | "unorm8" | "unorm16";
range?: [number, number]; // unorm types only: the values code 0 and the top code map to (default [0, 1])
}[];
}[];

extras?: any; // Application-defined JSON (§4)
comments?: string[]; // Free-form producer comments (§5)
}
备注
  • scales 码本保存对数值,需 exp() 恢复线性尺寸(§3.3)。sh0 码本为伽马空间 DC 系数,shN 为伽马空间 AC 系数。
  • 图像数据必须按原始 8 位整数处理,不做伽马转换。
  • 除非另有说明,未提到的通道忽略。
  • files 数组中文件名任意,但顺序有意义。

3. 属性编码​

3.1 位置​

means_l.webp、means_u.webp,RGB,每轴 16 位。

每轴通过两张图像量化为 16 位:

// 16-bit normalized value per axis (0..65535)
const qx = (means_u.r << 8) | means_l.r;
const qy = (means_u.g << 8) | means_l.g;
const qz = (means_u.b << 8) | means_l.b;

// Dequantize into *log-domain* nx,ny,nz using per-axis ranges from meta:
const nx = lerp(meta.means.mins[0], meta.means.maxs[0], qx / 65535);
const ny = lerp(meta.means.mins[1], meta.means.maxs[1], qy / 65535);
const nz = lerp(meta.means.mins[2], meta.means.maxs[2], qz / 65535);

// Undo the symmetric log transform used at encode time:
const unlog = (n: number) => Math.sign(n) * (Math.exp(Math.abs(n)) - 1);

const p = {
x: unlog(nx),
y: unlog(ny),
z: unlog(nz),
};

3.2 朝向​

quats.webp,RGB,24 位指数映射。

四元数分量始终按 w、x、y、z 排列。

旋转存储为单位球内的点 p:四元数的指数映射按比例缩放,让半转(180 度)位于单位球面,每分量一字节:

  • quats.r、g、b 分别保存 p.x、p.y、p.z。
  • 字节 b 映射为 (b − 128) / 127,格点恰好包含 0,因此字节 128、128、128 精确表示单位旋转。
const px = (quats.r - 128) / 127;
const py = (quats.g - 128) / 127;
const pz = (quats.b - 128) / 127;

const r = Math.hypot(px, py, pz); // 0..1
const theta = (Math.PI / 2) * r; // half the rotation angle
const k = r > 0 ? Math.sin(theta) / r : 0;

const q = [Math.cos(theta), px * k, py * k, pz * k]; // [w, x, y, z], unit length

编码过程(说明性):归一化 q,若 w < 0 则取反(q 与 -q 表示同一旋转),然后:

const v = Math.hypot(x, y, z);
const k = v > 0 ? Math.atan2(v, w) / v / (Math.PI / 2) : 2 / Math.PI;
const toByte = (c: number) => Math.max(0, Math.min(255, Math.round(c * k * 127 + 128)));
// quats.r = toByte(x), quats.g = toByte(y), quats.b = toByte(z), quats.a = 255

舍入前 |p| <= 1。逐分量舍入到字节后,长度可能略大于 1,超出量小于 0.007。例如 (0, 1/√2, 1/√2, 0) 编为 (218, 218, 128),解码长度约 1.0022。读取器必须接受任意字节组合。解码始终生成单位四元数,因此长度大于 1 仍为合法旋转。

对均匀随机旋转,该编码最大误差约 1.2 度,均方根误差约 0.6 度。

3.3 缩放​

scales.webp,RGB,使用码本。

各轴尺寸存为码本索引,码本是对数域数值,需指数运算解码:

const sx = Math.exp(meta.scales.codebook[scales.r]); // r,g,b are 0..255
const sy = Math.exp(meta.scales.codebook[scales.g]);
const sz = Math.exp(meta.scales.codebook[scales.b]);

尺寸含义(例如主轴标准差或完整范围)遵循源训练设置;exp 后的解码值单位为场景单位。

3.4 基础颜色与不透明度(DC)​

sh0.webp (RGBA)

sh0 保存各颜色通道的 DC(l=0)球谐系数和不透明度:

  • RGB 是 sh0.codebook 中 0–255 的索引。
  • A 是不透明度字节,允许大于 1(§3.6)。

在预乘解码图像的浏览器中(§1),半透明单元的 RGB 字节损失精度。

将 DC 系数转换为伽马空间 RGB:

// SH_C0 = Y_0^0 = 1 / (2 * sqrt(pi))
const SH_C0 = 0.28209479177387814;

const r = 0.5 + meta.sh0.codebook[sh0.r] * SH_C0;
const g = 0.5 + meta.sh0.codebook[sh0.g] * SH_C0;
const b = 0.5 + meta.sh0.codebook[sh0.b] * SH_C0;
const a = (sh0.a / 204) ** 2; // linear opacity, 0..1.5625

字节存储不透明度的平方根,A = (b / 204)²,让重叠半透明层所需的低不透明度具有更细步长。字节 204 恰为 A = 1,训练场景的饱和单元保持在此。205–255 表示大于 1,最高 (255 / 204)² = 1.5625。

编码过程(说明性):b = min(255, round(204 * sqrt(A))),A ≤ 0 存为 0,大于 1.5625 截断。

3.4.1 颜色空间​

DC 加高阶球谐解码后为 sRGB:Rec.709 原色与 sRGB 传递函数,这是 3DGS 训练的输出,对应 glTF KHR_gaussian_splatting 的 srgb_rec709_display。

颜色以显示为参照,不应用场景光照、曝光和色调映射。混合在 sRGB 中进行,再转换为输出颜色空间。

3.5 高阶球谐(可选)​

shN_centroids.webp, shN_labels.webp

存在高阶 AC 系数时,通过调色板存储:

  • shN.count 为条目数,范围 [1,2^24]。
  • shN.bands 为每条阶数,范围 [1,4]。

调色板图像有 ceil(count / 64) 行,受读取器最大纹理尺寸限制。写入器应保持 count ≤ 262144,即 4096 行,这是所有 WebGL2 设备支持的尺寸。

质心(调色板)​

  • shN_centroids.webp 是保存球谐系数调色板的 RGB 图像。
  • 每行固定 64 个条目,从左上开始按行优先打包。

纹理宽度取决于阶数:

阶数系数数纹理宽度(像素)
1364 * 3 = 192
2864 * 8 = 512
31564 * 15 = 960
42464 * 24 = 1536

计算球谐条目 n 的系数 c 对应像素位置:

const coeffs = [3, 8, 15, 24];
const u = (n % 64) * coeffs[bands - 1] + c;
const v = Math.floor(n / 64);

标签​

  • shN_labels.webp 为每单元保存 24 位索引,范围 0 到 count-1。
const index = shN_labels.r + (shN_labels.g << 8) + (shN_labels.b << 16);

解码​

质心像素的 RGB 是共享 shN.codebook 的 0–255 码本索引,分别对应球谐系数三个颜色通道。

解码单个单元的完整 AC 球谐系数:

const coeffs = [3, 8, 15, 24];
const shCoeffs = coeffs[meta.shN.bands - 1]; // coefficients per color channel
const codebook = meta.shN.codebook; // 256 shared float entries

// 1. Look up this gaussian's palette entry
const label = shN_labels.r + (shN_labels.g << 8) + (shN_labels.b << 16);

// 2. For each coefficient, read RGB from the centroids pixel and map through the codebook
for (let c = 0; c < shCoeffs; c++) {
const u = (label % 64) * shCoeffs + c;
const v = Math.floor(label / 64);

// pixel (u, v) in shN_centroids.webp
const r = centroidsPixel(u, v).r;
const g = centroidsPixel(u, v).g;
const b = centroidsPixel(u, v).b;

sh_channel0[c] = codebook[r]; // SH AC coefficient for color channel 0
sh_channel1[c] = codebook[g]; // SH AC coefficient for color channel 1
sh_channel2[c] = codebook[b]; // SH AC coefficient for color channel 2
}

3.5.1 基函数与求值​

各颜色通道系数按阶排列:一阶 3 个,二阶 5 个,三阶 7 个,四阶 9 个。

采用带 Condon–Shortley 相位的实球谐基,与 3DGS 参考实现和 gsplat 一致。一至三阶匹配 KHR_gaussian_splatting 附录 A,四阶沿用 gsplat。用文件坐标系中从相机指向单元中心的单位向量 d 求颜色(§1.2):

const [x, y, z] = d; // normalize(center - cameraPosition)
const xx = x * x, yy = y * y, zz = z * z;
const xy = x * y, yz = y * z, xz = x * z;

const basis = [
// band 1
-0.4886025119029199 * y,
0.4886025119029199 * z,
-0.4886025119029199 * x,
// band 2
1.0925484305920792 * xy,
-1.0925484305920792 * yz,
0.31539156525252005 * (2 * zz - xx - yy),
-1.0925484305920792 * xz,
0.5462742152960396 * (xx - yy),
// band 3
-0.5900435899266435 * y * (3 * xx - yy),
2.890611442640554 * xy * z,
-0.4570457994644658 * y * (4 * zz - xx - yy),
0.3731763325901154 * z * (2 * zz - 3 * xx - 3 * yy),
-0.4570457994644658 * x * (4 * zz - xx - yy),
1.445305721320277 * z * (xx - yy),
-0.5900435899266435 * x * (xx - 3 * yy),
// band 4
2.5033429417967046 * xy * (xx - yy),
-1.7701307697799304 * yz * (3 * xx - yy),
0.9461746957575601 * xy * (7 * zz - 1),
-0.6690465435572892 * yz * (7 * zz - 3),
0.10578554691520431 * (zz * (35 * zz - 30) + 3),
-0.6690465435572892 * xz * (7 * zz - 3),
0.47308734787878004 * (xx - yy) * (7 * zz - 1),
-1.7701307697799304 * xz * (xx - 3 * yy),
0.6258357354491761 * (xx * (xx - 3 * yy) - yy * (3 * xx - yy)),
];

// per color channel c, with dc from §3.4 and sh_channel from above
let color = 0.5 + 0.28209479177387814 * dc[c];
for (let k = 0; k < shCoeffs; k++) color += basis[k] * sh_channel[c][k];
color = Math.max(0, color);

渲染器可计算更少阶数,以精度换速度。场景旋转时,可先将 d 转入文件坐标系,或逐阶旋转系数。

3.6 大于 1 的不透明度​

简化器合并多个单元时,合并后通常需要比不透明度 1 的标准高斯更多的总不透明度(“质量”)。SOG 以 A > 1 保存,最高 1.5625(§3.4)。当且仅当存在大于 1 的单元时,meta.opacity.max 存在且大于 1,读取器无需扫描图像即可判断。

格式只定义 A 是相对不透明度 1 标准高斯的总质量。如何绘制属于渲染选择,文件不存轮廓函数,因此可调整渲染而不影响既有文件。

A ≤ 1 时按标准高斯绘制。A > 1 时,PlayCanvas 和 SplatTransform 推荐保持覆盖范围及峰值 alpha 1,加入不透明核心:覆盖范围内侧比例 u0 完全不透明,正常高斯衰减拉伸到剩余区域。求 u0 使总质量恰为标准高斯的 A 倍。若渲染器在 cσ 截断,并减去 f = exp(−c² / 2) 使边缘 alpha 为零:

// Per renderer: constants for its cutoff c (in σ)
const f = Math.exp(-c * c / 2);
const g0 = Math.sqrt(Math.PI / 2) / c * erf(c / Math.SQRT2) - f; // erf: the error function
const g1 = (1 - f) / (c * c) - f / 2;
const qa = (1 - f) / 2 - g0 + g1;
const r0 = (g0 - 2 * g1) / (2 * qa);
const q = g1 / qa;

// Per gaussian: the core radius, as a fraction of the cutoff (0 for A <= 1)
const t = Math.min(Math.max(A - 1, 0), 0.5625);
const u0 = Math.sqrt(r0 * r0 + q * t) - r0;
const peak = Math.min(A, 1);

// Per fragment, r = the Mahalanobis distance from the center, in σ
const s = Math.max(0, r - u0 * c) / (1 - u0);
const alpha = peak * Math.max(0, Math.exp(-0.5 * s * s) - f);
  • 覆盖、剔除和截断与标准高斯一致,无需改变边界。A = 1 时 u0 = 0,恢复标准轮廓。
  • 若再用 1 / (1 − f) 缩放 alpha,常数仍相同。PlayCanvas 在 c = 2√2 截断,r0 = 0.53839315,q = 0.62504992,u0 ≤ 0.2625。SplatTransform 在 c = 3 截断,u0 ≤ 0.24。

不支持大于 1 的渲染器必须将 A 截断到 1,结果等同于保存超单位不透明度之前的场景。

转为 PLY 时,若存在 A > 1,工具应使用 float 属性 opacity_linear 原样写 A,替代 opacity;否则写标准 opacity logit。两者只能有一个。读取器发现 opacity_linear 时必须优先使用,即使 opacity 也存在。不理解新属性的工具无法读取;转换器可提供标准 logit 输出,将 A 截断到 1。

3.7 盲数据(可选)​

blind_<i>.webp,每张盲数据纹理一个文件,每单元 1–4 字节。

盲数据是应用定义的逐单元数据,例如对象 ID、分割标签或权重。渲染器不解释,但可向着色器暴露。

meta.blind 每条描述一张 RGBA8。fields 依次打包到每单元四字节 RGB A:首字段从字节 0(R)开始,后续紧接前一个。字段大小由 type 决定,按小端存储,低字节在前。

type字节数值
u8, u16, u24, u321、2、3、4无符号整数
f162IEEE 754 半精度浮点位模式
f324IEEE 754 单精度浮点位模式
unorm81min + (code / 255) * (max - min),[min, max] = range
unorm162min + (code / 65535) * (max - min),[min, max] = range
  • 一张纹理的字段必须放得进四字节。
  • range 只适用于 unorm,默认 [0, 1]。写入器量化为 round((value - min) / (max - min) * 255),或乘 65535,超范围值无法存储。
  • 字段名在全部盲纹理中必须唯一,转为着色器标识符后也必须非空且唯一:非 A–Z、a–z、0–9、下划线的字符替换为下划线,连续下划线合并,去掉首尾下划线。PlayCanvas 以 getBlind_<identifier>() 暴露,并定义 SOG_BLIND_<identifier>。
  • 转为 PLY 时,工具应以字段名为属性名,整数用 uint,其他用 float。
  • 最后字段之后字节未使用。写入器应将未用 RGB 设为 0、A 设为 255,保持不透明;读取器必须忽略。
  • 每张盲纹理增加一次纹理绑定。WebGL2 保证每着色器 16 张;带球谐的 SOG 已用七张(means_l、means_u、scales、quats、sh0、shN_centroids、shN_labels),渲染器还可能绑定其他纹理,PlayCanvas 会增加码本查找纹理。写入器应保持不超过八张,将窄字段共享打包。
  • 预乘解码浏览器中(§1),A 存数据的纹理会损失 RGB 精度。字段能放入三字节时,写入器应不使用 A。
// field j of texture t, for the gaussian at texel (u, v)
const bytesOf = { u8: 1, u16: 2, u24: 3, u32: 4, f16: 2, f32: 4, unorm8: 1, unorm16: 2 };
const fields = meta.blind[t].fields;
let offset = 0;
for (let k = 0; k < j; k++) offset += bytesOf[fields[k].type];

const px = blind[t].pixel(u, v); // [r, g, b, a]
let code = 0;
for (let k = 0; k < bytesOf[fields[j].type]; k++) code |= px[offset + k] << (8 * k);
code >>>= 0; // unsigned 32-bit

如 §1 所述,使用 A 字节的盲纹理可能有零 alpha,编码必须保留此时的 RGB。

PlayCanvas 在复制到工作缓冲区时读取盲字段,参见读取 SOG 盲数据。

3.8 泼溅模型​

model 记录训练方式,决定渲染方式。普通 3D 高斯省略。

  • "antialiased":使用屏幕空间抗锯齿滤波训练,即 Mip-Splatting 二维滤波。渲染器应将相应滤波加入投影二维协方差 Σ 的对角项,替代通常 0.3 px² 扩张,并以 sqrt(det Σ / det(Σ + filter)) 缩放不透明度,保持未滤波质量。先缩放,再应用 §3.6 超单位轮廓。文件不存滤波大小;PlayCanvas 和 SplatTransform 默认 0.1 px²,即 Brush、LichtFeld Studio 和 Spirula 使用的 Mip-Splatting 值,均可覆盖。检测与渲染见抗锯齿。
  • "2dgs":二维高斯表面元,第三缩放轴为零厚度轴。

4. 额外数据​

extras 是 meta.json 的可选顶层应用自定义 JSON,类似 glTF extras。允许任意 JSON 值,推荐对象。

  • 读取器不得要求 extras 存在,且应原样暴露给应用。
  • 转换和处理工具应保留。合并场景时,仅在所有输入 extras 相同的情况下保留。

5. 注释​

comments 是可选顶层数组,保存自由格式的生产工具注释,例如源 PLY 的 comment 行:Postshot v1.1.69 或 postshot.color.eotf=sRGB。

  • 注释仅供参考,读取器不得依赖其存在或据此推导渲染行为。影响渲染的信息具有独立属性,例如 model。
  • 转换时工具应原样保留,转 PLY 时写回 comment 行,注释内换行改为空格。无需重复已由 model 等属性记录的信息,或工具自身的生成器行。
  • 注释不被解释,因此变换场景时,不更新描述原点偏移、向上轴等坐标信息的注释。
  • 合并场景时,仅当全部输入注释相同,工具才应保留。

6. meta.json 示例​

{
"version": 3,
"asset": { "generator": "splat-transform v1.2.3" },
"count": 187543,
"model": "antialiased",
"means": {
"mins": [-2.10, -1.75, -2.40],
"maxs": [ 2.05, 2.25, 1.90],
"files": ["means_l.webp", "means_u.webp"]
},
"scales": {
"codebook": [/* 256 floats */],
"files": ["scales.webp"]
},
"quats": {
"files": ["quats.webp"]
},
"sh0": {
"codebook": [/* 256 floats */],
"files": ["sh0.webp"]
},
"opacity": {
"max": 1.5625
},
"shN": {
"count": 65536,
"bands": 3,
"codebook": [/* 256 floats */],
"files": ["shN_centroids.webp", "shN_labels.webp"]
},
"blind": [
{
"files": ["blind_0.webp"],
"fields": [
{ "name": "segment", "type": "u16" },
{ "name": "weight", "type": "unorm8" },
{ "name": "depth", "type": "unorm8", "range": [0.5, 40] }
]
}
],
"extras": { "capturedBy": "rig-04" },
"comments": ["Postshot v1.1.69", "postshot.color.eotf=sRGB"]
}

7. 版本与兼容​

  • 读取器必须检查 version。本页是 v3,v2 独立说明。应拒绝不支持的版本。
  • 未来可能增加可选属性,读取器应忽略未知字段。
  • v3 无法无损保存为 v2。工具应将不透明度截断到 1、丢弃盲数据、球谐降到三阶、调色板降到 65536 条,并逐项警告。

8. 相对 v2 的变化​

项目v2v3
旋转quats.webp,26 位最小三分量quats.webp,RGB 24 位指数映射(§3.2)
不透明度sh0.a / 255,最大 1(sh0.a / 204)²,最大 1.5625;大于 1 时有 meta.opacity.max(§3.4、§3.6)
球谐阶数1–31–4(§3.5)
球谐调色板最高 65536 条,16 位标签最高 2^24 条,24 位标签(§3.5)
盲数据—meta.blind 描述的可选 blind_<i>.webp(§3.7)
额外数据—可选 extras,应用 JSON(§4)
注释—可选 comments,自由格式生产工具注释(§5)

其他内容,包括位置、缩放、DC 颜色、球谐调色板布局、索引和打包,均不变。