06 / 12 · Build the splat构建 splat
Give the splat a camera and a 3D shape为 splat 加上相机与三维形状
Connect world/view/clip coordinates, the projection derivative and a screen-space ellipse.打通世界、观察、裁剪坐标,连接投影导数与屏幕椭圆。
A screen-space blob always faces us in the same shape. A 3D Gaussian behaves differently: rotate the camera and its projected footprint changes. We will derive that change from three short axes.屏幕上的光斑总以相同形状面对我们,3D Gaussian 则不同:旋转相机,投影形状会变化。我们用三根短轴推导这个变化。
1. Move the world into the camera’s coordinate system1. 把世界搬进相机坐标系
The view matrix answers “where is this point relative to the camera?” The projection matrix makes distant objects smaller. The code applies projection × view × point, using column-major matrices. A point carries w = 1 so translation affects it; an axis direction carries w = 0 so translation does not affect it.View 矩阵回答“这个点相对相机在哪”,Projection 矩阵让远处物体变小。代码按 projection × view × point 计算,矩阵为列主序。点的 w = 1,会受平移影响;轴方向的 w = 0,不受平移影响。
The toy camera looks along its local −Z with +Y up. The research capture has a different world orientation, so the stage 7 camera uses −Y up. This is a dataset convention, not a different projection equation. Keep the conversion at the data boundary.教学相机朝自己的 −Z 方向看,+Y 向上。研究数据的世界方向不同,因此阶段 7 使用 −Y 向上。这是数据集坐标约定,不是换了投影公式。坐标转换应在数据边界明确处理。
2. Divide only after reaching clip space2. 到裁剪空间后再做除法
The GPU divides clip xyz by clip w to obtain NDC. For a small world axis a, compute delta = VP × (a, 0). The quotient rule tells us how clip.xy / clip.w changes along that axis. Multiply by half the viewport dimensions to convert that small NDC movement to pixels.GPU 将 clip xyz 除以 clip w 得到 NDC。对一根短世界轴 a,先计算 delta = VP × (a, 0)。商的求导法则告诉我们 clip.xy / clip.w 沿这根轴如何变化,再乘视口尺寸的一半,把微小 NDC 变化换成像素。
You can read the derivative as “how many pixels would this tiny movement move the point?” This is a first-order approximation around the Gaussian center. It is accurate for small footprints away from the camera plane, not an exact projection of an arbitrarily large volume.把导数读成一句话:“这个微小移动会让点在屏幕上走多少像素?”它是 Gaussian 中心附近的一阶近似,适合远离相机平面的小 footprint,不是任意大体积的精确投影。
3. Add three projected axes into a covariance3. 将三根投影轴合成协方差
Project each scaled world axis into 2D, giving d₀, d₁, d₂. For each axis take its outer product, then add the three matrices. This yields [[a,b],[b,c]]. The diagonal describes spread in x and y; b describes their correlation. Adding 0.3 to the diagonal is the baseline’s pixel-space footprint regularization.将三根带尺度的世界轴投影成二维向量 d₀、d₁、d₂。每根轴与自身做外积,再将三个矩阵相加,得到 [[a,b],[b,c]]。对角元素描述 x、y 的分散程度,b 描述两者关联。对角线上加 0.3,是基线在像素空间的 footprint 正则化。
4. Turn covariance into two visible axes4. 把协方差变成两根可见轴
Eigenvectors give the ellipse directions; square roots of eigenvalues give one-standard-deviation lengths. Extend three standard deviations along each direction to place the quad’s corners. Convert the pixel offset back to NDC and multiply by center.w before adding to clip.xy. Omitting that last multiplication makes size change incorrectly with depth.特征向量给出椭圆方向,特征值的平方根给出一个标准差的长度。沿两个方向各延伸三个标准差以放置 quad 顶点。像素偏移换回 NDC 后,还要乘 center.w 才能加到 clip.xy。漏掉这一步,尺寸就会随深度错误变化。
The toy uses axis-aligned 3D scales so the camera effect is isolated. The real shader first builds rotated axes from a quaternion. The toy camera range keeps both centers safely in front of the camera; a general viewer additionally needs robust near-plane handling and conservative bounds.教学场景使用沿世界轴的三维尺度,方便单独观察相机作用。真实 shader 会先用四元数构建旋转后的轴。教学相机范围保证两个中心都在相机前方;通用 viewer 还需要稳健的近裁剪面处理与保守包围范围。
RUN → OBSERVE运行 → 观察
Make this step concrete把这一步运行起来
Two anisotropic splats. Orbiting changes their shape, overlap and ordering; changing distance changes their apparent size.两个各向异性的 splat。绕转相机会改变形状、重叠与排序;调整距离会改变屏幕大小。
Open GPU milestone 5 →打开 GPU 里程碑 5 →cargo run --locked --example workshop -- 5
# Arrow keys orbit; Home resets. The browser also supports drag and sliders.Commands use the reference repository. Rust/WGSL changes need a native rebuild or npm run build:wasm, followed by a browser reload.命令以参考仓库为基础。修改 Rust/WGSL 后需重新编译桌面程序,或运行 npm run build:wasm 后刷新浏览器。
IDEA → IMPLEMENTATION想法 → 实现
Follow the running code沿着真实代码看
Excerpts are extracted from the repository at site build time. Expand only the part you are studying; full files include context and line numbers.片段在网站构建时直接从仓库提取。只展开正在研究的部分,完整文件包含上下文与行号。
src/workshop.rs · L63–88
This maps input angles to view and projection matrices.将输入角度映射成观察与投影矩阵。
pub fn camera(frame: Frame, width: u32, height: u32) -> (Mat4, Mat4) {
// The research capture uses a different world orientation from the toy scene.
let (target, up, sign, distance) = if frame.stage == 7 {
(
Vec3::new(0.0, 3.5, 14.0),
Vec3::NEG_Y,
-1.0,
frame.distance * 3.0,
)
} else {
(Vec3::ZERO, Vec3::Y, 1.0, frame.distance)
};
let orbit = Vec3::new(
frame.yaw.sin() * frame.pitch.cos(),
frame.pitch.sin(),
sign * frame.yaw.cos() * frame.pitch.cos(),
);
let view = Mat4::look_at_rh(target + orbit * distance, target, up);
let projection = Mat4::perspective_rh(
50_f32.to_radians(),
width as f32 / height as f32,
0.05,
200.0,
);
(view, projection)
}Open full source with line numbers ↗打开带行号的完整源码 ↗src/workshop.wgsl · L19–23
This is the quotient rule written directly in WGSL.这是直接写成 WGSL 的商求导法则。
fn project_axis(center: vec4<f32>, axis: vec3<f32>) -> vec2<f32> {
let delta = globals.view_projection * vec4(axis, 0.0);
// Quotient rule for (clip.xy / clip.w), then NDC -> pixels.
return (delta.xy * center.w - center.xy * delta.w) / (center.w * center.w) * globals.viewport_stage.xy * 0.5;
}Open full source with line numbers ↗打开带行号的完整源码 ↗src/workshop.wgsl · L48–66
Projected axes become eigenvectors, then quad corners.投影轴成为特征向量,再成为 quad 顶点。
let center = globals.view_projection * vec4(splat.center_alpha.xyz, 1.0);
let scale = splat.scale_pad.xyz;
// Axis-aligned world ellipsoids here; the real pass also rotates these axes.
let a0 = project_axis(center, vec3(scale.x, 0., 0.));
let a1 = project_axis(center, vec3(0., scale.y, 0.));
let a2 = project_axis(center, vec3(0., 0., scale.z));
let a = a0.x*a0.x + a1.x*a1.x + a2.x*a2.x + 0.3;
let b = a0.x*a0.y + a1.x*a1.y + a2.x*a2.y;
let c = a0.y*a0.y + a1.y*a1.y + a2.y*a2.y + 0.3;
let root = sqrt(max(0.0, (a-c)*(a-c) + 4.0*b*b));
let large = max(0.1, 0.5*(a+c+root));
let small = max(0.1, 0.5*(a+c-root));
var axis = vec2(1.0, 0.0);
if abs(b) > 0.00001 { axis = normalize(vec2(b, large-a)); }
else if c > a { axis = vec2(0.0, 1.0); }
let perpendicular = vec2(-axis.y, axis.x);
let pixels = 3.0 * (q.x * sqrt(large) * axis + q.y * sqrt(small) * perpendicular);
let ndc = pixels * 2.0 / globals.viewport_stage.xy;
out.clip = center + vec4(ndc * center.w, 0.0, 0.0);Open full source with line numbers ↗打开带行号的完整源码 ↗Real-model render pass ↗真实模型渲染 pass ↗ · Real-model shader ↗真实模型 shader ↗
Predict it, change it, explain it先预测,再修改,再解释
Double all three world scales while keeping the camera fixed. Will the eigenvalues double or quadruple? What about ellipse radii before regularization matters?固定相机,将三个世界尺度全部翻倍。特征值会翻倍还是变成四倍?忽略正则项影响时,椭圆半径怎样变化?
Check your reasoning核对推理
Projected axes double, their outer products quadruple, and square-root radii double. The additive 0.3 term makes this only approximate for very small footprints.投影轴翻倍,外积变为四倍,开平方后的半径翻倍。对很小的 footprint,由于额外的 0.3 项,这个关系只是近似。
Keep the theory beside the code把原理放在代码旁边
Open the related derivation or lab ↗打开对应推导或实验 ↗