04 / 12 · Build the splat构建 splat
Move numbers from Rust into GPU memory把 Rust 中的数字送进 GPU 内存
Build the Rust/WGSL data contract and draw one quad per instance.建立 Rust/WGSL 数据约定,让每个实例绘制一个四边形。
A shader cannot follow a Rust pointer to a Vec. We must explicitly copy bytes into GPU memory and tell both sides what those bytes mean. This is one of the most useful debugging skills in graphics.Shader 不能顺着 Rust 指针去读取 Vec。我们需要主动把字节复制到 GPU 内存,并约定每段字节的含义。这是图形开发最有用的排错能力之一。
1. Start with a byte ruler1. 先拿出“字节尺”
One f32 occupies four bytes. A vec4<f32> occupies sixteen. Our Instance has three vec4 blocks: center plus opacity, scale plus padding, RGB plus padding. Its stride is 48 bytes. The uniform contains a mat4 at bytes 0–63 and one vec4 at bytes 64–79, making 80 bytes. Rust repr(C), Pod and Zeroable make this explicit and checkable.一个 f32 占 4 字节,一个 vec4<f32> 占 16 字节。Instance 有三个 vec4 块:中心与不透明度、尺度与填充、RGB 与填充,步长共 48 字节。Uniform 的矩阵占 0–63 字节,vec4 占 64–79 字节,总共 80 字节。Rust 的 repr(C)、Pod、Zeroable 让这个结构明确且可检查。
Do not assume a Rust [f32; 3] and a WGSL vec3 will automatically produce the same struct offsets. WGSL alignment rules matter. Padding deliberately with vec4 blocks makes this first contract easy to inspect. The tests check actual Rust sizes, while GPU validation and pixel tests check the complete path.不要假定 Rust [f32; 3] 与 WGSL vec3 在结构体中的偏移天然一致,必须考虑 WGSL 对齐规则。先用 vec4 显式填充,让数据约定容易检查。测试检查真实 Rust 大小,GPU 验证和像素测试再检查整条链路。
2. Describe access before sending data2. 先描述访问方式,再发送数据
UNIFORM is for per-frame camera and stage values. STORAGE holds an array of instances. COPY_DST permits queue.write_buffer uploads. A bind group layout describes binding numbers and access types; a bind group connects those slots to actual buffers. Binding 0 and 1 must match the WGSL declarations.UNIFORM 存每帧的相机与阶段信息,STORAGE 存实例数组,COPY_DST 允许 queue.write_buffer 上传。Bind group layout 描述绑定编号与访问类型,bind group 再把插槽连接到真实 buffer。绑定 0、1 必须与 WGSL 声明一致。
3. Build a rectangle with two triangles3. 用两个三角形拼矩形
A quad is six vertex invocations: two triangles sharing two corner positions. vertex_index chooses a corner, instance_index chooses a Gaussian record. Stage 2 writes the selected record’s RGB directly and forces alpha to one, so the rectangle has a hard boundary. We have not applied the Gaussian function yet.四边形需要六次顶点执行:两个三角形共享其中两个角的位置。vertex_index 选择角,instance_index 选择 Gaussian 记录。阶段 2 直接写记录中的 RGB 并把 alpha 设为 1,所以矩形边缘是硬的。此时还没有使用高斯函数。
4. Upload what actually changes4. 上传真正变化的数据
For two toy records, copying 96 bytes per frame is easy to inspect. For a real model, repeatedly uploading every record wastes bandwidth. The production-facing StgPass keeps source records resident and uploads only a uniform and sorted source indices. Learn the small direct version first, then make that one change in ownership.对于两个教学记录,每帧复制 96 字节便于理解;对于真实模型,反复上传所有记录会浪费带宽。面向真实模型的 StgPass 让原始记录常驻 GPU,只更新 uniform 和排序后的源索引。先学直接的小版本,再改变数据所有权。
RUN → OBSERVE运行 → 观察
Make this step concrete把这一步运行起来
One solid orange rectangle. No falloff yet. Its color comes from the Rust record through a storage buffer.一个实心橙色矩形,还没有渐变衰减;颜色从 Rust 记录经 storage buffer 传入。
Open GPU milestone 2 →打开 GPU 里程碑 2 →cargo run --locked --example workshop -- 2
cargo test workshop::tests::host_shader_contract_and_invalid_inputCommands 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 · L7–20
Count the bytes on the CPU side.在 CPU 端数清字节。
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct Uniforms {
view_projection: [[f32; 4]; 4], // bytes 0..64, column-major
viewport_stage: [f32; 4], // bytes 64..80: width, height, stage, padding
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct Instance {
center_alpha: [f32; 4], // bytes 0..16
scale_pad: [f32; 4], // bytes 16..32, activated standard deviations
color_pad: [f32; 4], // bytes 32..48, direct RGB (not SH coefficients)
}Open full source with line numbers ↗打开带行号的完整源码 ↗src/workshop.wgsl · L2–10
Match the shader offsets and binding slots.对照 shader 偏移与绑定插槽。
struct Uniforms { view_projection: mat4x4<f32>, viewport_stage: vec4<f32> };
struct Instance { center_alpha: vec4<f32>, scale_pad: vec4<f32>, color_pad: vec4<f32> };
@group(0) @binding(0) var<uniform> globals: Uniforms;
@group(0) @binding(1) var<storage, read> instances: array<Instance>;
struct Out {
@builtin(position) clip: vec4<f32>,
@location(0) local: vec2<f32>,
@location(1) rgba: vec4<f32>,
};Open full source with line numbers ↗打开带行号的完整源码 ↗src/workshop.rs · L144–193
Create memory, describe its access, then bind it.创建内存、描述访问、再进行绑定。
let uniforms = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("workshop-uniforms-80-bytes"),
contents: bytemuck::bytes_of(&Uniforms::zeroed()),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let instances = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("workshop-instances-2-times-48-bytes"),
size: 96,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("workshop-bindings"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let bindings = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("workshop-bind-group"),
layout: &layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniforms.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: instances.as_entire_binding(),
},
],
});Open full source with line numbers ↗打开带行号的完整源码 ↗Real-model render pass ↗真实模型渲染 pass ↗ · Real-model shader ↗真实模型 shader ↗
Predict it, change it, explain it先预测,再修改,再解释
Set color_pad to [0, 1, 0, 0]. Explain why changing the fourth number alone has no visible effect in this milestone.把 color_pad 改成 [0, 1, 0, 0]。解释为什么只改第四个数字,这一阶段画面不变。
Check your reasoning核对推理
The quad becomes green. The shader reads only color_pad.xyz; the fourth number is padding. Opacity comes from center_alpha.w and stage 2 overrides it with one. A field name alone does not make a value affect rendering.矩形会变绿。Shader 只读取 color_pad.xyz,第四个数是填充。不透明度来自 center_alpha.w,且阶段 2 会把它替换成 1。字段有名字,不代表它一定参与渲染。