wgpu-rs绘制2D点时消除顶点缓冲区字节重复的方法
wgpu-rs 批量绘制2D纯色点时顶点属性冗余存储的优化方案
问题背景
当前基于wgpu-rs开展2D图形开发,需求为绘制三角形面片构成的纯色2D点,现有实现存在明显的顶点数据重复存储问题,内存浪费严重。
现有实现逻辑为:每个点由1个独立三角形(共3个顶点)构成,每组顶点(x,y)坐标都会重复绑定三类同点完全一致的属性:
Vec3<f32>类型的(r,g,b)纯色值Vec2<f32>类型的点中心点坐标f32类型的点半径值
上述三类属性在同一个点对应的3个顶点上各存储1次,但逻辑上每个点仅需存储1次即可满足渲染需求。
现有代码片段
当前定义的点数据结构体如下:
pub struct Point { pub position: [f32; 2], pub color: [f32; 3], pub center: [f32; 2], pub radius: f32, }
顶点属性内存布局配置(非完整片段)如下:
&[ (mem::size_of::<[f32; 2]>(), wgpu::VertexFormat::Float32x2), (mem::size_of::<[f32; 3]>(), wgpu::VertexFormat::Float32x3), (mem::size_of::<[f32; 2]>(), wgpu::VertexFormat::Float32x2), (mem::size_of::<[f32; 1]>(), wgpu::VertexFormat::Float32), ]
核心诉求
每个三角形的3个顶点对应的半径、中心点、颜色值完全一致,需要找到合理的顶点缓冲区封装方式避免这类逐点属性重复存储。
注:无法使用uniform缓冲区实现,因为需要一次性批量绘制大量半径、颜色各不相同的点,上述属性为逐点可变参数,而非全局统一参数。
当前实现下每个点的三类非位置属性被重复存储3次,绘制大规模点集时冗余内存开销非常明显,需要可行的优化方案消除这部分浪费,同时保证着色器端可以正确为每个顶点读取到对应的逐点属性。
解决方案
使用GPU原生实例化渲染(Instanced Rendering)+ 双顶点缓冲区绑定即可完全消除这部分冗余,这也是WebGPU/wgpu针对批量重复图元渲染的标准优化方案,不需要手动做复杂的索引偏移计算。
具体实现步骤
- 拆分两类数据到独立缓冲区
将原有混合存储的顶点数据拆分为两个独立缓冲区,绑定到不同的顶点槽位:- 槽位0缓冲区:存储逐顶点变化的属性,也就是三角形3个顶点相对于点中心的位置偏移,这个缓冲区是全局固定的,所有点都可以复用,仅需存储3组
[f32;2]的偏移值,不会随绘制点的数量增加扩容。 - 槽位1缓冲区:存储逐点(逐实例)变化的属性,也就是原来重复存储的中心点、颜色、半径,每个点仅存储1份,结构体可简化为:
// 逐实例属性,每个点仅存1次,无冗余 pub struct PointInstance { pub center: [f32; 2], pub color: [f32; 3], pub radius: f32, }
- 槽位0缓冲区:存储逐顶点变化的属性,也就是三角形3个顶点相对于点中心的位置偏移,这个缓冲区是全局固定的,所有点都可以复用,仅需存储3组
- 配置差异化的顶点步长模式
创建顶点布局时,为两个槽位设置不同的step_mode,让GPU自动处理数据步进:- 槽位0(逐顶点偏移):
step_mode设为VertexStepMode::Vertex,每绘制1个顶点就移动到下一个数据条目 - 槽位1(逐点属性):
step_mode设为VertexStepMode::Instance,每绘制完1个完整点(对应3个顶点)才移动到下一个数据条目
对应的顶点布局配置示例:
// 槽位0:全局复用的三角形顶点偏移 let vertex_layout = wgpu::VertexBufferLayout { array_stride: mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, step_mode: wgpu::VertexStepMode::Vertex, attributes: &[wgpu::VertexAttribute { offset: 0, shader_location: 0, format: wgpu::VertexFormat::Float32x2, }], }; // 槽位1:逐点存储的实例属性 let instance_layout = wgpu::VertexBufferLayout { array_stride: mem::size_of::<PointInstance>() as wgpu::BufferAddress, step_mode: wgpu::VertexStepMode::Instance, attributes: &[ // 中心点 对应shader location 1 wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x2, }, // 颜色 对应shader location 2 wgpu::VertexAttribute { offset: mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, shader_location: 2, format: wgpu::VertexFormat::Float32x3, }, // 半径 对应shader location 3 wgpu::VertexAttribute { offset: (mem::size_of::<[f32; 2]>() + mem::size_of::<[f32; 3]>()) as wgpu::BufferAddress, shader_location: 3, format: wgpu::VertexFormat::Float32, } ], }; - 槽位0(逐顶点偏移):
- 调整绘制调用
渲染时使用实例化绘制接口,顶点数固定传3(每个点对应三角形3个顶点),实例数传入待绘制的点总数量即可:// 3个顶点,绘制point_count个点实例 pass.draw(0..3, 0..point_count); - 着色器端适配
WGSL着色器中直接按location读取属性即可,同一个点的3个顶点会自动获取到相同的逐点属性,不需要额外计算索引:struct VertexInput { // 逐顶点:三角形相对中心的偏移 @location(0) pos_offset: vec2<f32>, // 逐实例:同一点3个顶点读取值完全一致 @location(1) center: vec2<f32>, @location(2) color: vec3<f32>, @location(3) radius: f32, }; struct VertexOutput { @builtin(position) clip_pos: vec4<f32>, @location(0) color: vec3<f32>, }; @vertex fn vs_main(input: VertexInput) -> VertexOutput { var out: VertexOutput; // 计算顶点最终世界坐标,可替换为自定义的变换逻辑 let world_pos = input.center + input.pos_offset * input.radius; out.clip_pos = vec4<f32>(world_pos, 0.0, 1.0); out.color = input.color; return out; } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { return vec4<f32>(input.color, 1.0); }
优化收益
- 完全消除3倍冗余存储:原实现每个点需要占用
3*(2+3+2+1)*4 = 96字节,优化后每个点仅占用(2+3+1)*4 = 24字节,内存占用降至原来的1/4,点规模越大收益越明显 - 固定顶点缓冲区仅占24字节,无论绘制多少点都不需要修改
- 步长步进由GPU硬件原生支持,比手动在着色器里计算顶点索引的方案性能更高、出错概率更低
备选方案:如果不想使用实例化,也可以将逐点属性连续存储在单缓冲区中,在着色器中通过
@builtin(vertex_index) / 3u计算当前顶点对应的点索引,再手动寻址读取属性。但该方案需要手动处理缓冲区偏移、对齐逻辑,性能弱于原生实例化,不推荐优先使用。
内容的提问来源于stack exchange,提问作者Simplex_
相关产品推荐
相关产品推荐

