08 / 12 · Connect a learned model连接训练模型
Make time an explicit rendering input让时间成为输入
Evaluate motion and temporal opacity, then keep camera, sort and playback synchronized.计算运动与时间透明度,让相机、排序及播放保持同步。
Pressing play should change Gaussian parameters before projection. That is different from putting prerecorded frames onto a rectangle. You can freeze time and still move the camera.播放应在投影之前改变 Gaussian 参数,这与把录好的视频贴到矩形上不同。你可以冻结时间,同时继续移动相机。
1. Separate seconds from model time1. 区分秒与模型时间
The UI owns elapsed seconds. The renderer receives normalized time t in [0,1]. A ten-second loop maps seconds/10 into that interval. The file does not magically define the playback duration. Capture time range, loop behavior and synchronization belong in explicit metadata or host policy.界面管理经过的秒数,渲染器接收 [0,1] 范围内的归一化时间 t。十秒循环把秒数除以 10 映射进去。文件不会自动定义播放时长,采集时间范围、循环行为、同步方式应来自明确元数据或宿主策略。
2. Move the center before projecting it2. 先移动中心,再做投影
In stage 6, the red Gaussian uses x += 2.5Δt and y += 1.5Δt², while blue moves in the opposite x direction. At t = 0.5, Δt = 0 and both return to their base centers. At t = 0.9, the red displacement is (1.0, 0.24, 0). The real STG model extends this to a cubic vector polynomial and time-dependent quaternion.阶段 6 中,红色 Gaussian 使用 x += 2.5Δt、y += 1.5Δt²,蓝色沿相反 x 方向移动。t = 0.5 时 Δt = 0,都回到基础中心。t = 0.9 时,红色位移为 (1.0, 0.24, 0)。真实 STG 模型把它扩展为三次向量多项式及随时间变化的四元数。
3. Fade a Gaussian through its lifetime3. 让 Gaussian 随生命周期淡入淡出
Multiply base opacity by exp(−(Δt/ρ)²). At Δt = 0, the factor is 1; at Δt = ρ, it is e⁻¹ ≈ 0.3679. This STG convention does not put 1/2 in the exponent. A spatial Gaussian and this temporal envelope have similar shapes but different parameter conventions.基础不透明度乘 exp(−(Δt/ρ)²)。Δt = 0 时因子为 1,Δt = ρ 时为 e⁻¹ ≈ 0.3679。这个 STG 约定在指数里没有 1/2。空间 Gaussian 与时间包络形状相似,但参数约定不同。
4. A frame must use one time consistently4. 一帧里必须使用同一个时间
Evaluate the current centers, compute camera-space depths, sort, upload, draw and submit using the same t. Sorting base centers and drawing moved centers produces incorrect overlap. Pausing freezes t, not the camera controls. Returning from a background browser tab resets the wall-clock delta so the animation does not leap unexpectedly.用同一个 t 求当前中心、计算相机深度、排序、上传、绘制并提交。按基础中心排序、却绘制移动后的中心,会导致重叠错误。暂停冻结的是 t,不是相机操作。从浏览器后台返回时重置时钟差,避免意外跳跃。
A renderer can evaluate intermediate times without proving those times are accurate reconstructions. A viewer’s smooth playback and a trained model’s temporal quality are different claims. Check held-out times and views when evaluating the model.能计算中间时间,并不证明这些时间的重建准确。Viewer 播放流畅,与模型时间质量好,是不同的结论。评价模型时需检查未用于训练的时间及视角。
RUN → OBSERVE运行 → 观察
Make this step concrete把这一步运行起来
Two splats move and one fades. Pause at t = 0.9 and orbit: time stays fixed while the viewpoint changes.两个 splat 移动,其中一个还会淡出。暂停在 t = 0.9 再绕转,时间不变而视点改变。
Open GPU milestone 6 →打开 GPU 里程碑 6 →cargo run --locked --example workshop -- 6
# Space pauses; arrow keys still orbit. Online, scrub t before pressing Play.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 · L111–125
Motion is evaluated before depth sorting.运动求值发生在深度排序之前。
if frame.stage == 6 {
let dt = frame.time - 0.5;
values[0].center_alpha[0] += 2.5 * dt;
values[0].center_alpha[1] += 1.5 * dt * dt;
values[0].center_alpha[3] *= (-(dt / 0.45).powi(2)).exp();
values[1].center_alpha[0] -= 1.5 * dt;
}
// Sorting happens AFTER evaluating the current time and camera.
values.sort_by(|a, b| {
let depth = |s: &Instance| -(view * Vec3::from_slice(&s.center_alpha[..3]).extend(1.0)).z;
depth(b).total_cmp(&depth(a))
});
if frame.reverse_order {
values.reverse();
}Open full source with line numbers ↗打开带行号的完整源码 ↗public/build/lab.mjs · L91–120
The host maps real time to the renderer’s normalized input.宿主把真实时间映射成渲染器的归一化输入。
function animate(now) {
if (failed) return;
const dt = last ? Math.min((now - last) / 1000, 0.1) : 0; last = now;
if (!document.hidden && renderer) {
// Physical pixels, capped for this lesson. CSS dimensions stay in CSS pixels.
const bounds = canvas.getBoundingClientRect(), dpr = Math.min(devicePixelRatio || 1, 1.5);
const width = Math.max(1, Math.min(2048, Math.round(bounds.width * dpr)));
const height = Math.max(1, Math.min(2048, Math.round(bounds.height * dpr)));
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width; canvas.height = height; renderer.resize(width, height);
}
if (playing) clock = (clock + dt / 10) % 1;
slider.value = clock;
const n = Number(stage.value);
if (n !== 7 || loaded) {
try {
const count = renderer.render(n, clock, yaw, pitch, distance, byId('reverse').checked);
if (count >= 0) { frames++; byId('draw-count').textContent = count.toLocaleString(); }
} catch (error) { fatal(error); return; }
} else {
renderer.render(0, clock, yaw, pitch, distance, false);
byId('draw-count').textContent = '—';
}
byId('time-value').textContent = clock.toFixed(3);
byId('resolution').textContent = `${width} × ${height}`;
byId('presented').textContent = frames.toLocaleString();
byId('camera-value').textContent = `${yaw.toFixed(2)} / ${pitch.toFixed(2)} / ${distance.toFixed(1)}`;
}
requestAnimationFrame(animate);
}Open full source with line numbers ↗打开带行号的完整源码 ↗Real-model render pass ↗真实模型渲染 pass ↗ · Real-model shader ↗真实模型 shader ↗
Predict it, change it, explain it先预测,再修改,再解释
Freeze at t = 0.9. Predict the red opacity multiplier when ρ = 0.45, then compute it.固定 t = 0.9,ρ = 0.45 时,先预测红色不透明度乘数,再计算。
Check your reasoning核对推理
Δt = 0.4. exp(−(0.4/0.45)²) ≈ 0.4538, so the base 0.5 opacity becomes about 0.2269. The motion and fade use the same temporal offset.Δt = 0.4,exp(−(0.4/0.45)²) ≈ 0.4538,所以基础 0.5 不透明度变为约 0.2269。运动和淡出使用同一个时间偏移。