目的#
最終的にはBevyにてシェーダーを駆使したカッコイイ画を作れるようになりたい。
そこでBevy のシェーダー系のexamples を触ったものの、
Advancedな内容になってくるとWGPUへの習熟が前提となってくる。
webgpufundamentals の Fundamentals のみ読み、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 は単なる引数として宣言としても問題ない。 - 出力されている変数を使わないのも問題ない。
- 型が合っていなくてもどうやら 動いてしまう ようだ。
- Vertex shader は返り値の型の
- この
@builtinは別物であるとのこと。あくまで WebGPU のビルドイン機能から提供される変数値。- ここにまとめられている。
- 同じ
@builtin(position)でも Vertex shader と Fragment shader では別ものである。上記リンクの表参照。
- Vertex shader は頂点毎に処理を行うのに対し、Fragment shader はピクセル毎に処理を行うため、Inter-stage 変数は頂点単位の値から補完(interpolation)してピクセル単位の値として生成されることになる。
- 補完方法もアノテーションにて指定可能だが、あまりユースケースはないらしい。
WebGPU Uniforms#
- CPUが書き込む値であり、Vertex/Fragment shader がその処理する頂点やピクセルにかかわらず読み取り可能な値である。
- シェーダー側の宣言は
@var<uniform>。 Immutable な変数となる。 - BindGroupを介してアクセスするため、Shader では
@group(<bind-group-idx>) @binding(<binding-idx>)とアノテーションする。
- シェーダー側の宣言は
- CPU側のやること周り: ※BindGroup 周りはStorageバッファやテクスチャ等も同様の手順を踏む
- 描画前の準備:
- BindGropuのレイアウトを定義、Pipelineのレイアウトへ設定
Device::create_bind_group_layout()にてBindGroupレイアウトを作成- 生成時の Descriptor に
entitiesとしてデータの配置を記述する。
- 生成時の Descriptor に
Device::create_pipeline_layout()にてPipelineレイアウトを作成- Pipelineレイアウトは
Device::create_render_pipeline()にてPipelineを作成するとき必要になる
- Pipelineレイアウトは
- なおPipelineレイアウトをシェーダーから推定させることが可能。Rust だと
RenderPipelineDescriptor::labelをNone指定。推定されたBindGroup定義はget_bind_group_layout()にて取得可能だが、これをほかのシェーダーと共有することができない。そのため推奨は明示的に定義すること。
- バッファの作成
Device::create_buffer()などcreate_buffer()では Uniforms 以外用のバッファも作成可能。用途毎に制約があるので、それを満たす必要あり。
- BindGroupのインスタンス作成
Device::create_bind_group()にレイアウトとデータを渡して作成
- BindGropuのレイアウトを定義、Pipelineのレイアウトへ設定
- 描画毎:
- バッファへ書き込むデータを作成
- データのバッファへの書き込み(GPUへの転送命令)
Queue::write_buffer()にて転送をキューイング
- BindGroupのRenderPassへの設定
RenderPass::set_bind_group()にてどのBindGroup番号にどのBindGroupインスタンスを設定するか指定して行う。- これにより同じレイアウトの別のインスタンスを接続して扱える。PBRマテリアルを切り替えては描画する、といった形で利用されるとか。
- 描画前の準備:
- 別言語にて記述されたCPUプログラム、GPUプログラム(シェーダー)間の値の受け渡しであるため、FFIの実装と同様の問題:値のメモリ配置を意識する必要がある。
WebGPU Storage Buffers#
Uniformバッファに似ている。 UniformバッファをStorageバッファに置き換えるのは簡単。
違い:
- StorageバッファはUniformバッファより最大サイズが大きい(可能性がある)
- Storageバッファは読み書き可能だが、Uniformバッファは読み取り専用である
- (おそらく上記の制約の存在により)一般的なユースケースではUniformバッファの方が高速になりえる
Storageバッファは1回の描画呼び出しにより多数のモノを描画するような状況に適合する、とのこと。パーティクルシステムの類に使える?またCompute shaderにて書き込み可能なバッファとして使われる、など…
instance_index によるInstancing#
Vertex shader の引数に使える: @builtin(instance_index) fooInstanceIdx: u32。
draw() 命令にインスタンスのレンジ(と頂点のレンジ)を指定すると、
各インスタンス×各頂点毎にVertex shaderが呼び出される。
ストレージバッファを使い全インスタンスの情報をarray<T>に詰め込むようにすることで、
1回の描画命令にて複数のインスタンスを描画可能になる。
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];
// ...
}
//
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バッファより制約が強いため、高速になりえると思われる。
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;
// ...
}
#[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を通して渡すことができる。
@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);
// ...
}
- サンプラー生成は
Device::create_sampler() - テクスチャ生成は
Device::create_texture() - テクスチャの書き込みは
Queue::write_texture() Texture::create_view()によりテクスチャからテクスチャビュー生成できる。- Pipelineに設定可能なリソースは
BindingResourceだが、テクスチャそのものはenumに含まれない。ビューを設定する必要がある。
- Pipelineに設定可能なリソースは
// サンプラー
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#
パイプライン毎にオーバライド可能な定数を使える。
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生成時に値をオーバライドできる。エントリポイントの評価時にオーバライドを適用するため、同じモジュールの別のエントリポイントにそれぞれ別の値をオーバライドすることも可能である。
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
// 略...
},
}}
// 略...
});