目的#

最終的にはBevyにてシェーダーを駆使したカッコイイ画を作れるようになりたい。

そこでBevy のシェーダー系のexamples を触ったものの、 Advancedな内容になってくるとWGPUへの習熟が前提となってくる。 webgpufundamentalsFundamentals のみ読み、wgpu のチュートリアル(Beginner)を攫った程度では理解が足りていないと感じた。

そこでwebgpufundamentals を読み進める。 以下にメモ書きをしていく。

WebGPU Inter-stage Variables#

  • Inter-stage 変数は Vertex shader から Fragment shader に受け渡す変数である。
    • @location(<n>) としてアノテーションする。
      • この <n> を出力する側(Vertex shader)と入力として受け取る側(Fragment shader)にて同じ値を指定する必要がある。
      • location さえ合っていればよい。
        • Vertex shader は返り値の型の struct のフィールドの一つとして宣言、Fragment shader は単なる引数として宣言としても問題ない。
        • 出力されている変数を使わないのも問題ない。
        • 型が合っていなくてもどうやら 動いてしまう ようだ。
    • @builtin は別物であるとのこと。あくまで WebGPU のビルドイン機能から提供される変数値。
    • Vertex shader は頂点毎に処理を行うのに対し、Fragment shader はピクセル毎に処理を行うため、Inter-stage 変数は頂点単位の値から補完(interpolation)してピクセル単位の値として生成されることになる。
      • 補完方法もアノテーションにて指定可能だが、あまりユースケースはないらしい。

WebGPU Uniforms#

WebGPU Storage Buffers#

Uniformバッファに似ている。 UniformバッファをStorageバッファに置き換えるのは簡単。

違い:

  • StorageバッファはUniformバッファより最大サイズが大きい(可能性がある)
  • Storageバッファは読み書き可能だが、Uniformバッファは読み取り専用である
  • (おそらく上記の制約の存在により)一般的なユースケースではUniformバッファの方が高速になりえる

Storageバッファは1回の描画呼び出しにより多数のモノを描画するような状況に適合する、とのこと。パーティクルシステムの類に使える?またCompute shaderにて書き込み可能なバッファとして使われる、など…

instance_index によるInstancing#

Vertex shader の引数に使える: @builtin(instance_index) fooInstanceIdx: u32draw() 命令にインスタンスのレンジ(と頂点のレンジ)を指定すると、 各インスタンス×各頂点毎にVertex shaderが呼び出される。

ストレージバッファを使い全インスタンスの情報をarray<T>に詰め込むようにすることで、 1回の描画命令にて複数のインスタンスを描画可能になる。

[wgsl]
struct InstanceStruct {
    // ...
    vertex_color: array<vec4<f32>, 3>,
}
@group(0) @binding(0) var<storage, read> instances: array<InstanceStruct>;

@vertex fn vs(
    @builtin(vertex_index) vertex_index: u32,
    @builtin(instance_index) instance_index: u32,
) -> VSOutput {
    let instance = instances[instance_index];
    let color = instance.vertex_color[vertex_index];
    // ...
}
[rust]
//
render_pass.draw(0..3, 0..100); // `instance_index` を 0-99, `vertex_index` を 0-2 で描画

vertex_index 分のストレージバッファを使って頂点位置を渡し、それを使いすべてのインスタンスの形状を同一のものにする例が同ページに記載されている。これは頂点バッファをストレージバッファを使って再現しているような形か。

WebGPU Vertex Buffers#

頂点単位の情報を保持するバッファ。Vertex shaderに引数として渡される。 すなわちVertex shader実行前にバッファから値を取り出している。 Storageバッファの例をもとに比較すると、この例はインスタンス分のバッファの例ではあるが、 バッファ全体にVertex shaderからアクセスしようと思えば可能である点が異なる。

ユースケースによるらしいが、Unformバッファでの例と同様、Vertexバッファの方がStorageバッファより制約が強いため、高速になりえると思われる。

[wgsl]
struct Vertex {
    @location(0) position: vec3<f32>,
    @location(1) color: vec4<f32>,
    // ...
}

@vertex fn vs(
    vertex: Vertex,
    @builtin(instance_index) instance_index: u32,
) -> VertexOutput {
    let position = vertex.position;
    let color = vertex.color;
    // ...
}
[rust]
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
    position: [f32; 3],
    color: [f32; 4],
}

fn make_vertex_buffer_layout() -> VertexBufferLayout {
    VertexBufferLayout {
        array_stride: std::mem::size_of::<Vertex>() as BufferAddress,
        step_mode: VertexStepMode::Vertex,
        attributes: &[
            // `position`
            VertexAttribute {
                offset: 0,
                shader_location: 0,
                format: VertexFormat::Float32x3,
            },
            // `color`
            VertexAttribute {
                offset: std::mem::size_of::<[f32;3]>() as BufferAddress, // 0+sizeof(`position`)
                shader_location: 1,
                format: VertexFormat::Float32x4,
            },

        ],
    }
}

fn render( /* ... */ ) {
    // ...
    render_pass.set_vertex_buffer(0, vertex_buffer);
    render_pass.draw(0..3, 0..1);
    // ...
}

step_mode指定のVertexStepMode だが、VertexStepMode::Instanceという値がほかの候補にある。こちらはバッファからVertex shaderへ渡す値を取り出すとき、 instance_indexをインデックスとして使う。前述のStorageバッファの例でやっている、インスタンス単位でのデータ受け渡しと同等のことを行える。

WebGPU Textures#

1~3次元(だいたい2次元)のデータ。バッファとのおおきな違いはデータアクセスであり、サンプラーというH/Wを使ってのアクセスを行えることである。

テクスチャへのアクセスは0.0~1.0に正規化されたテクスチャ座標系を用いる。 テクスチャ自体は2次元配列である。 浮動小数点数であるテクスチャ座標上のある1点は、ほとんどの場合、配列上の1要素にピッタリ対応することはない。例えば 4x4 のテクスチャを考えたとき、u=0.5 は2つめの要素と3つめの要素の間の位置をさすことになる)。サンプラーは指定座標から複数の要素にアクセスすることが可能であり、各要素の値からブレンドした値を取得する・最近傍の要素の値を取得するなど行える。

テクスチャはバッファ同様、BindGroupを通して渡すことができる。

[wgsl]
@group(0) @binding(0) var mySampler: sampler;
@group(0) @binding(1) var myTexture: texture_2d<f32>;

@fragment fn fs(vsInput: VertexOutput) -> @location(0) vec4f {
    let uv = vsInput.uv; // テクスチャ座標は通常Vertex shaderからのinter-stage変数
    let color = textureSample(myTexture, mySampler, uv);
    // ...
}
[rust]
// サンプラー
let sampler = device.create_sampler(&SamplerDescriptor {
    mag_filter: wgpu::FilterMode::Linear,  // 拡大時
    min_filter: wgpu::FilterMode::Nearest, // 縮小時
    mipmap_filter: wgpu::MipmapFilterMode::Nearest, // Mipmapレベル間
    ..Default::default()
});
// テクスチャ生成
let texture = device.create_texure(&TextureDescriptor {
    size: texture_size,
    mip_level_count: 1,
    sample_count: 1,
    dimension: TextureDimension::D2,
    format: TextureFormat::Rgba8UnormSrgb, // Unorm = unsigned normalized
    usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
    label: None,
    view_formats: &[],
});
// テクスチャビュー生成
let texture_view = texture.create_view(&TextureViewDescriptor::default());
// BindGroup にテクスチャビューを設定
let bind_group = device.create_bind_group(&BindGroupDescriptor {
    entries: &[
        BindGroupEntry {
            binding: 0,
            resource: BindingResource::TextureView(&texture_view),
        },
        // 略...
    ],
    // 略...
});

// 書き込み
let texture_data = // ...
queue.write_texture(
    TexelCopyTextureInfo { // 書き込み先
        texture: &texture,
        mip_level: 0,
        origin: Origin3d::ZERO,
        aspect: TexureAspect::All,
    },
    &texture_data,
    TexelCopyBufferLayout {
        offset: 0,
        byte_per_row: Some(4 * texture_width), // (R[Unorm:0-255] * G * B * A) * width
        rows_per_image: Some(texture_height),
    },
    texture_size,
);

WebGPU Shader Constants#

パイプライン毎にオーバライド可能な定数を使える。

[wgsl]
override my_const = 0.0;
// 略...

@vertex fn vs(
    vertex: Vertex,
) -> VertexOutput {
    // ...
    let vertex_color = my_const * vertex.color;
    // ...
}

@fragment fn fs(vsInput: VertexOutput) -> @location(0) vec4f {
    // ...
    let color = my_const * textureSample(myTexture, mySampler, uv);
    // ...
}

Render pipeline生成時に値をオーバライドできる。エントリポイントの評価時にオーバライドを適用するため、同じモジュールの別のエントリポイントにそれぞれ別の値をオーバライドすることも可能である。

[rust]
let shader = device.create_shader_module(inclcude_wgsl!("shader.wgsl"));
let vs_constants = HashMap::from([ ("my_const", 0.5), ]);
let fs_constants = HashMap::from([ ("my_const", 2.0), ]);
let render_pipeline = device.create_rende_pipeline(&RenderPipelineDescriptor {
    vertex: VertexState {
        module: shader,
        entry_point: Some("vs"),
        compilation_opstions: PipelineCompilationOptions {
            &vs_constants, // Vertex shader 内の `my_const` は 0.5
            // 略...
        },

        // 略...
    },
    fragment: FragmentState {
        module: shader,
        entry_point: Some("fs"),
        compilation_opstions: PipelineCompilationOptions {
            &fs_constants, // Fragment shader 内の `my_const` は 2.0
            // 略...
        },
    }}
    // 略...
});