You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WGPU中向片段着色器传递逐实例数据的最优方案(2D SDF绘圆)

使用2D SDF和片段着色器绘制100个圆:逐实例数据传递的最优方式

问题

我希望用2D有向距离场(SDF)和片段着色器绘制100个圆,请问向片段着色器传递逐实例数据的最优方式是什么?

现有方案的问题

你提到的用wgpu::VertexStepMode::Instance顶点缓冲区的方法确实可行,但存在明显资源浪费——每个圆用4个顶点表示边界矩形,这4个顶点会接收完全相同的实例数据,之后GPU还会对这些重复数据做插值计算,属于没必要的开销。

最优解决方案

针对这个问题,有两种高效的优化方向:

1. 给实例数据加上@flat修饰符(最小改动方案)

如果想保留原来的矩形顶点绘制方式,只需要在传递实例数据时,用@flat修饰变量,告诉GPU不要对这些数据做插值计算。同一个实例的所有顶点会直接把相同的实例数据传递给片段着色器,彻底避免插值浪费。

优化后的片段着色器代码:

struct FragmentInput {
    @builtin(position) position: vec4<f32>,
    @flat @location(5) circle_center: vec2<f32>,  // @flat 禁用插值
    @flat @location(6) circle_radius: f32,
};

@fragment
fn fs_main(input: FragmentInput) -> @location(0) vec4<f32> {
    // 计算像素到圆心的距离平方
    let dx = input.position.x - input.circle_center.x;
    let dy = input.position.y - input.circle_center.y;
    let dist_sq = dx * dx + dy * dy;
    let radius_sq = input.circle_radius * input.circle_radius;

    // SDF绘制圆,可选添加抗锯齿让边缘更平滑
    if dist_sq > radius_sq {
        return vec4<f32>(1.0, 1.0, 0.0, 1.0);  // 背景色
    } else {
        let dist = sqrt(dist_sq);
        let alpha = smoothstep(input.circle_radius, input.circle_radius - 1.0, dist);
        return vec4<f32>(0.0, 0.0, 0.0, alpha);
    }
}

2. 全屏顶点+存储缓冲区(高性能方案)

如果要追求极致性能,尤其是后续可能扩展到更多实例的情况,可以只绘制一个覆盖全屏的三角形(或两个三角形组成的矩形),完全不需要为每个圆创建顶点。通过实例ID直接从存储缓冲区读取对应圆的数据,彻底消除顶点数据的重复和插值开销。

顶点着色器代码:

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @builtin(instance_index) instance_id: u32,
};

@vertex
fn vs_main(@builtin(vertex_index) vertex_id: u32) -> VertexOutput {
    // 全屏三角形的顶点坐标,覆盖整个裁剪空间
    let vertices = array(
        vec2<f32>(-1.0, -1.0),
        vec2<f32>(3.0, -1.0),
        vec2<f32>(-1.0, 3.0)
    );
    var output: VertexOutput;
    output.position = vec4<f32>(vertices[vertex_id], 0.0, 1.0);
    output.instance_id = @builtin(instance_index);
    return output;
}

片段着色器代码:

struct CircleInstance {
    center: vec2<f32>,
    radius: f32,
};

// 存储缓冲区,存放所有圆的实例数据
@group(0) @binding(0) var<storage, read> circles: array<CircleInstance>;

struct FragmentInput {
    @builtin(position) position: vec4<f32>,
    @builtin(instance_index) instance_id: u32,
};

@fragment
fn fs_main(input: FragmentInput) -> @location(0) vec4<f32> {
    let circle = circles[input.instance_id];
    // 把裁剪空间坐标转换为屏幕空间(根据你的投影矩阵调整)
    let screen_pos = input.position.xy / input.position.w;
    
    // 计算像素到圆心的距离
    let dx = screen_pos.x - circle.center.x;
    let dy = screen_pos.y - circle.center.y;
    let dist = sqrt(dx*dx + dy*dy);

    // SDF抗锯齿绘制,让圆边缘更平滑
    let alpha = smoothstep(circle.radius, circle.radius - 1.0, dist);
    return vec4<f32>(0.0, 0.0, 0.0, alpha);
}

方案选择

  • 如果你只是绘制100个圆,第一种@flat修饰的方案改动最小,足够高效
  • 如果后续要绘制成千上万个实例,第二种全屏顶点+存储缓冲区的方案性能更优,资源开销极低

内容的提问来源于stack exchange,提问作者mq7

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 00:40:20