1. はじめに
WebGPU は、WebGL の後継として、より高性能なグラフィクス API を提供することを目的としています。
wgpu は、Rust で書かれた WebGPU という Webブラウザ向けのグラフィクス API の実装です。
要は、wgpu を使えば、Rust で書いたコードを Webブラウザ上で動かすことができるということのようです。
2. ログの出力
Cargo.toml に以下のように記述します。
[lib]
crate-type = ["cdylib"]
[dependencies]
log = "0.4.27"
console_error_panic_hook = "0.1.7"
console_log = { version ="1.0.0", features = ["color"] }
wasm-bindgen = "0.2.100"
wasm-bindgen-futures = "0.4.50"
web-sys = { version = "0.3.77", features = ['console', 'Document', 'Element', 'HtmlCanvasElement', 'Window'] }
cdylib
は、Rust のライブラリを WebAssembly にコンパイルするためのターゲットです。log
は、Rust のロギングライブラリです。console_error_panic_hook
は、エラーが発生したときに、コンソールにエラーメッセージを表示するためのライブラリです。console_log
は、Rust のログを JavaScript のコンソールに出力するためのライブラリです。wasm-bindgen
は、Rust と JavaScript の間でデータをやり取りするためのライブラリです。wasm-bindgen-futures
は、非同期処理を行うためのライブラリです。web-sys
は、Web API を Rust から使うためのライブラリです。
lib.rs に以下のように記述します。
use wasm_bindgen::prelude::*;
use web_sys::*;
#[wasm_bindgen]
pub fn start() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Debug).expect("error initializing log");
log::info!("start function called");
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id("Canvas").unwrap();
let canvas: web_sys::HtmlCanvasElement = canvas
.dyn_into::<web_sys::HtmlCanvasElement>()
.map_err(|_| ())
.unwrap();
log::info!("{}", &format!("width: {}", canvas.width()));
log::info!("{}", &format!("height: {}", canvas.height()));
}
#[wasm_bindgen(start)]
は、WASM が読み込まれたときに呼び出される関数です。
関数が呼び出されるタイミングを指定するために、通常のエクスポート関数として定義するため #[wasm_bindgen]
に変更します。
index.html に以下のように記述します。
<body>
<h1>WASM Example</h1>
<canvas id="Canvas" width="640" height="480"></canvas>
<script type="module">
import init, { start } from './wasm_wgpu.js';
async function run() {
await init();
console.log("WASM initialized");
start();
}
run();
</script>
</body>
上記のコードでブラウザを開くと、コンソールに以下のようなログが出力されます。
WASM initialized // JavaScript の初期化が完了したことを示すログ
[INFO] start function called // Rust の start 関数が呼び出されたことを示すログ
[INFO] width: 640 // canvas の幅を示すログ
[INFO] height: 480 // canvas の高さを示すログ
3. wgpu の設定周り
次に、wgpu を使ってみます。
Crate wgpu と more-code を参考します。
Cargo.toml に以下を追加します。
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wgpu = "25.0.0"
bytemuck = "1.22.0"
rlib
は、Rust のライブラリとしてコンパイルするためのオプションです。wgpu
は、Rust で書かれた WebGPU の実装です。bytemuck
は、Rust のバイト列を扱うためのライブラリです。
3.1. ラッパー関数の作成
後述の Adapter を使うために、ラッパー関数に変更します。
use wasm_bindgen::prelude::*;
use web_sys::*;
use wgpu::*;
#[wasm_bindgen]
pub fn start() {
// ラッパー関数を呼び出す
wasm_bindgen_futures::spawn_local(async {
start_async().await;
});
}
async fn start_async() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Debug).expect("error initializing log");
log::info!("start function called");
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id("Canvas").unwrap();
let canvas: web_sys::HtmlCanvasElement = canvas
.dyn_into::<web_sys::HtmlCanvasElement>()
.map_err(|_| ())
.unwrap();
log::info!("{}", &format!("width: {}", canvas.width()));
log::info!("{}", &format!("height: {}", canvas.height()));
...
}
Adapter で .await
を使うために、非同期関数に変更します。wasm_bindgen_futures::spawn_local
を使うことで、非同期関数を呼び出すことができます。
以降 start_async
関数に追記していきます。
3.2. GPU インスタンスの作成
let instance = wgpu::Instance::new(
&wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
},
);
wgpu::Backends::all()
は、すべてのバックエンドを使用することを示します。..Default::default()
は、他のフィールドはデフォルトの設定を使用することを示します。
3.3. Surface の作成
let surface_target = wgpu::SurfaceTarget::Canvas(canvas.clone());
let surface = instance
.create_surface(surface_target)
.expect("Failed to create surface");
surface_target
を作成して、その対象を HTML の canvas
要素に設定しています。Surface
は、GPU に描画するウィンドウやキャンバスのようなもの。バックバッファを持ち、後で swap_chain
や surface_texture
などを通して描画される対象になります。expect(...)
は、Surface の作成に失敗した場合にエラーを出してクラッシュさせるためのエラーハンドリング。
3.4. Adapter の取得
let adapter = instance
.request_adapter(
&wgpu::RequestAdapterOptions {
compatible_surface: Some(&surface),
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
}
)
.await
.expect("Failed to find an appropriate adapter");
wgpu::RequestAdapterOptions
で、アダプタを取得するためのオプションを指定し、request_adapter
を呼び出すことで、アダプタを取得しています。compatible_surface
で、surface
に適合するアダプタを取得しています。power_preference
で、デフォルトの電力設定を使用することを示します。force_fallback_adapter
で、false
を設定し、フォールバックアダプタを使用しないことを示します。
非同期関数であり、非同期処理の完了を待つ必要があるため .await
を使います。
3.5. Device と Queue を作成
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_webgl2_defaults(),
memory_hints: Default::default(),
trace: wgpu::Trace::default(),
},
)
.await
.expect("Failed to create device");
wgpu::DeviceDescriptor
で、デバイスを取得するためのオプションを指定し、request_device
を呼び出すことで、デバイスとキューを取得しています。label
は、デバイスのラベルを指定するためのオプションです。デバッグ時に識別しやすくなります。required_features
は、デバイスで有効にする必要がある機能のオプションです。デフォルトでは機能を指定しない場合、空の値を指定します。required_limits
は、デバイスが満たすべきリソース制限です。WebGL2 の下位互換性を持つ制限を指定しています。memory_hints
は、デバイスのメモリに関するヒントを指定するためのオプションです。デフォルト設定しています。trace
トレース(デバッグ用の記録)を有効にするかどうか。デフォルトを設定 false
しています。
非同期関数であり、非同期処理の完了を待つ必要があるため .await
を使います。
3.6. Surface の設定
let surface_capabilities = surface.get_capabilities(&adapter);
let surface_format = surface_capabilities.formats.iter()
.find(|f| f.is_srgb())
.copied()
.unwrap_or(surface_capabilities.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: canvas.width() as u32,
height: canvas.height() as u32,
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &config);
get_capabilities
は、Surface
オブジェクトに対して、指定された Adapter
がサポートする描画の設定やフォーマットの情報を取得します。surface_capabilities.formats
の中から SRGB フォーマットを優先的に選択し、見つからない場合はデフォルトのフォーマットを使用します。wgpu::SurfaceConfiguration
で、Surface
の設定を行います。usage
で、描画に使用するテクスチャの用途を指定します。wgpu::TextureUsages::RENDER_ATTACHMENT
で、レンダーターゲットとして使用します。format
で、選択したフォーマットを指定します。width
で、描画対象の幅(ピクセル単位)を指定します。canvas の幅を指定します。height
で、描画対象の高さ(ピクセル単位)を指定します。canvas の高さを指定します。present_mode
で、描画の更新方法を指定します。wgpu::PresentMode::Fifo
で、FIFO モード(垂直同期を有効にしたモード)を指定します。alpha_mode
は、描画の合成方法を指定します。wgpu::CompositeAlphaMode::Auto
で、ブラウザが自動的に合成方法を決定します。view_formats
で、描画対象のフォーマットを指定します。空のベクタを指定しています。desired_maximum_frame_latency
は、描画のフレームレートを指定します。2
フレームのレイテンシを指定しています。
4. wgpu の描画周り
続いて頂点データから画面表示までのGPU描画処理の一連の流れの説明になります。
4.1. Vertex
#[repr(C)]
#[derive(Copy, Clone, Debug)]
struct Vertex {
position: [f32; 3],
color: [f32; 3],
}
unsafe impl bytemuck::Pod for Vertex {}
unsafe impl bytemuck::Zeroable for Vertex {}
const VERTICES: &[Vertex] = &[
Vertex { position: [0.0, 0.5, 0.0], color: [1.0, 0.0, 0.0] },
Vertex { position: [-0.5, -0.5, 0.0], color: [0.0, 1.0, 0.0] },
Vertex { position: [0.5, -0.5, 0.0], color: [0.0, 0.0, 1.0] },
];
#[repr(C)]
は、構造体のメモリレイアウトを C言語と同じ順序・アライメントでメモリに配置されます。これにより、RustコードをCやWebGPUのような外部ライブラリと安全にやり取りできるようになります。position: [f32; 3]
は、頂点の3次元空間での位置を表します。[f32; 3] は3つの32ビット浮動小数点数の配列です。color: [f32; 3]
は、頂点の色をRGB形式で表します。[f32; 3] は赤、緑、青の3つの成分を持つ配列です。
Vertex に対して、bytemuck
クレートが提供するトレイト Pod
と Zeroable
を実装しています。ただし、これらのトレイトは「unsafe(安全ではない)」としてマークされているため、unsafe
キーワードを使用して明示的に実装する必要があります。
let vertex_buffer = device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(VERTICES),
usage: wgpu::BufferUsages::VERTEX,
}
);
頂点データを格納するためのバッファ(vertex_buffer)を作成します。label
は、デバッグ用のラベルを指定します。contents
は、バッファに格納するデータを指定します。bytemuck::cast_slice
は、Rustの型をバイト列に変換するためのユーティリティ関数で、ここではVERTICESという配列をバイト列に変換しています。usage
は、バッファの用途を指定します。ここでは wgpu::BufferUsages::VERTEX
が指定されており、このバッファが頂点データ用であることを示しています。
4.2. Shader
WebGPU Shading Language (WGSL) を使用して記述されたシェーダーを使い、GPU に描画するためのシェーダーコードを記述するためのものです。
shader.wgsl
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) color: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs_main(vin: VertexInput) -> VertexOutput {
var vout: VertexOutput;
vout.color = vin.color;
vout.clip_position = vec4<f32>(vin.position, 1.0);
return vout;
}
@fragment
fn fs_main(fin: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(fin.color, 1.0);
}
頂点シェーダー (vs_main)
vs_main は頂点シェーダーのエントリーポイントです。この関数は、GPU に送られる頂点データを処理し、クリップ空間座標に変換します。入力として VertexInput 構造体を受け取り、出力として VertexOutput 構造体を返します。
- VertexInput 構造体: 頂点データの入力形式を定義します。
@location(0)
と@location(1)
は、それぞれ頂点の位置 (position) と色 (color) を示しています。これらはvec3<f32>
型で、3次元ベクトルを表します。 - VertexOutput 構造体: 頂点シェーダーの出力形式を定義します。
@builtin(position)
はクリップ空間座標 (clip_position) を示し、@location(0)
は色データを保持します。
関数内では、入力の position をvec4<f32>
型に変換し、クリップ空間座標として設定します。また、入力の color をそのまま出力に渡します。
フラグメントシェーダー (fs_main)
fs_main はフラグメントシェーダーのエントリーポイントです。この関数は、頂点シェーダーから渡されたデータを受け取り、ピクセルの色を計算します。
- 入力は VertexOutput 構造体を受け取り、出力として
@location(0)
に対応する色データを返します。 - 出力は
vec4<f32>
型で、RGB 色成分 (fin.color) にアルファ値 1.0 を追加して最終的な色を生成します。
let shader = device.create_shader_module(
wgpu::ShaderModuleDescriptor {
label: Some("Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
}
);
device.create_shader_module
は、GPU デバイス上で使用するシェーダーモジュールを作成するためのメソッドです。このモジュールは、シェーダーコードを GPU にロードし、後でパイプラインに組み込むために使用されます。
wgpu::ShaderModuleDescriptor
は、シェーダーモジュールの設定を指定します。label
は、デバッグ用のラベルを指定します。source
は、シェーダーコードのソースを指定します。
4.3. Pipeline
GPU パイプラインで使用するレイアウトを作成します。シェーダーがどのようにリソースにアクセスするかを定義します。
let pipeline_layout = device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: Some("Pipeline Layout"),
bind_group_layouts: &[],
push_constant_ranges: &[],
}
);
label
は、デバッグ用のラベルを指定します。bind_group_layouts
は、シェーダーが使用するバインドグループレイアウトの配列を指定します。
バインドグループは、シェーダーがアクセスするリソース(例: ユニフォームバッファやテクスチャ)をグループ化したものです。ここではリソースを使用しない設定になっています。push_constant_ranges
は、プッシュコンスタントの範囲を指定します。
プッシュコンスタントは、シェーダーに小さなデータを効率的に渡すための仕組みです。ここではプッシュコンスタントを使用しない設定になっています。
4.4. Render
GPU 上でグラフィックスを描画する際の一連の設定を定義するもので、頂点シェーダーやフラグメントシェーダー、プリミティブの描画方法などを含むレンダリングパイプラインを作成する部分です。
let render_pipeline = device.create_render_pipeline(
&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3,
offset: 0,
shader_location: 0, // `position` に対応
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3,
offset: 12,
shader_location: 1, // `color` に対応
},
],
},
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(surface_format.into())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
}
);
wgpu::RenderPipelineDescriptor
で、パイプラインの詳細な設定を指定します。label
は、デバッグ用のラベルを指定します。layout
は、このパイプラインで使用するパイプラインレイアウトを指定します。
vertex
は、頂点シェーダーと頂点バッファの設定を定義します。module
は、頂点シェーダーモジュールを指定します。この例では、shader に格納されたシェーダーモジュールを使用しています。entry_point
は、頂点シェーダーのエントリーポイント(関数名)を指定します。この例では vs_main を指定しています。buffers
は、頂点バッファのレイアウトを定義します。ここでは、Vertex 構造体のサイズを array_stride
として指定し、2つの属性(position と color)を定義しています。shader_location
の 0 は、WGSL シェーダー内の @location(0)
に対応し、頂点の位置データを示します。shader_location
の 1 は、@location(1)
に対応し、頂点の色データを示します。compilation_options
は、wgpu::PipelineCompilationOptions
型のフィールドで、シェーダーのコンパイル時に使用されるオプションを指定します。これは、特別な設定を行わずにデフォルトのコンパイルオプションを使用することを意味します。
fragment
は、フラグメントシェーダーの設定を定義します。module
は、フラグメントシェーダーモジュールを指定します。頂点シェーダーと同じ shader を使用しています。entry_point
は、フラグメントシェーダーのエントリーポイント(関数名)を指定します。この例では fs_main を指定しています。targets
は、出力ターゲット(レンダーターゲット)のフォーマットを指定します。ここでは、surface_format を使用してスワップチェーンのフォーマットを指定しています。compilation_options
は、wgpu::PipelineCompilationOptions
型のフィールドで、シェーダーのコンパイル時に使用されるオプションを指定します。これは、特別な設定を行わずにデフォルトのコンパイルオプションを使用することを意味します。
primitive
は、描画するプリミティブの設定を定義します。topology
は、頂点データの接続方法を指定します。この場合、wgpu::PrimitiveTopology::TriangleList
を設定しており、頂点が三角形リストとして解釈されます。strip_index_format
は、ストリップトポロジーで使用されるインデックスフォーマットを指定します。ここでは None を指定しており、ストリップトポロジーは使用されていません。front_face
は、ポリゴンの表面がどちらを向いているかを指定します。wgpu::FrontFace::Ccw
は反時計回りの頂点順序を表面として扱います。cull_mode
は、裏面カリングの設定です。ここでは Some(wgpu::Face::Back)
が指定されており、裏面(カメラから見えない面)が描画されないようにしています。polygon_mode
は、ポリゴンの描画モードを指定します。wgpu::PolygonMode::Fill
はポリゴンを塗りつぶして描画する設定です。unclipped_depth
は、深度値がクリッピングされるかどうかを制御します。ここでは false
となっており、深度値はクリッピングされます。conservative
は、保守的なラスタライズを有効にするかどうかを指定します。ここでは false
で無効化されています。
depth_stencil
は、深度ステンシルバッファの設定を行います。ここでは None
となっており、深度ステンシルテストは使用されていません。
multisample
は、マルチサンプリングの設定を定義する wgpu::MultisampleState
構造体を指定しています。count
は、サンプリング数を指定します。ここでは 1
が設定されており、マルチサンプリングは無効です。mask
は、サンプルマスクを指定します。!0
はすべてのサンプルを有効にする設定です。alpha_to_coverage_enabled
は、アルファ値を使用したカバレッジマスクを有効にするかどうかを指定します。ここでは false
で無効化されています。
multiview
は、マルチビューの設定を行いますが、ここでは None
となっており、マルチビューは使用されていません。cache
は、パイプラインキャッシュの設定を行いますが、ここでは None
となっており、キャッシュは使用されていません。
4.5. Output
現在の描画対象テクスチャを取得し、そのテクスチャに対するビューを作成します。
このビューは、後続のシェーダーやレンダーパイプラインに渡し、実際の描画処理を行う際に使用します。
let output = surface.get_current_texture().unwrap();
let output_view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
4.6. Encoder
GPUコマンドを記録するためのコマンドエンコーダを作成します。
wgpuでは、GPUに送信するコマンドは直接実行されるのではなく、まずコマンドエンコーダに記録されます。
その後、記録されたコマンドはCommandBufferに変換され、GPUに送信されます。
この設計により、複数のコマンドを効率的にまとめて送信することが可能になります。
let mut encoder = device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("Command Encoder"),
}
);
device.create_command_encoder
は、device
から新しいコマンドエンコーダを作成します。wgpu::CommandEncoderDescriptor
は、コマンドエンコーダの作成時に必要な設定を指定します。label
は、デバッグ用のラベルを指定します。
4.7. Command
レンダリングパスを設定し、描画コマンドを発行します。
Rustの所有権と借用ルールにより、encoder
が借用されている間は、所有権を移動する操作(finish()など)を行うことはできないため、render_pass
のスコープを明示的に終了させることで、encoder
の借用を解放し、その後に encoder.finish()
を呼び出せるようにしています。
// 限定スコープを作成
{
// render pass descriptor
let mut render_pass = encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &output_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
}
);
render_pass.set_pipeline(&render_pipeline);
render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
render_pass.draw(0..VERTICES.len() as u32, 0..1);
}
// submit the command buffer
queue.submit(Some(encoder.finish()));
wgpu::RenderPassDescriptor
構造体は、レンダリングパスの設定を行います。label
は、デバッグ用のラベルを指定します。color_attachments
は、カラーバッファの設定を行います。wgpu::RenderPassColorAttachment
を使用して、以下の設定をしていますview
出力先のテクスチャビュー(output_view)を指定。resolve_target
マルチサンプリングの解決先を指定します。ここでは None
。ops
カラーバッファの操作を指定します。load
バッファをクリアし、黒(wgpu::Color::BLACK
)で初期化。store
描画結果を保存する設定。
その他のプロパティ(depth_stencil_attachment
、occlusion_query_set
、timestamp_writes
)は使用していないため、None
に設定されています。
set_pipeline
使用するレンダリングパイプライン(render_pipeline)を設定します。このパイプラインにはシェーダーや描画設定が含まれています。set_vertex_buffer
頂点バッファ(vertex_buffer)を設定します。ここでは、バッファ全体をスライスして渡しています。draw
描画コマンドを発行します。0..VERTICES.len() as u32
描画する頂点の範囲を指定しています。VERTICES.len()
は頂点数を表します。0..1
インスタンスの範囲を指定しています。ここでは1つのインスタンスを描画します。
スコープを抜けるた後は、encoder.finish()
でコマンドバッファを終了し、それを queue.submit
でGPUに送信します。この操作により、GPUが実際に描画処理を実行します。
4.8. Present
描画結果を画面に表示します。
output.present();
5. 表示
以下が表示されます。(Chromeで確認)
6. まとめ
いろいろと参考にさせてもらいましたが、バージョンの問題か写経しても動かないところがあり、Github Copilot にはずいぶんと助けてもらいました。
描画するための一連の流れを理解することができました。
今後は各機能毎に整理しつつ、いろんな機能を試していきたいと思います。
今回の記事は、Github Copilot と ChatGPT を使い9割くらい書いてくれました。
A. 参考サイト
Crate wgpu
100日後にRustをちょっと知ってる人になる: [Day 23]wasm-pack テンプレート Deep Dive
Learn Wgpu
Rustのグラフィクス周りメモ/wgpuとその使い方
Rust/wgpuをWebブラウザで動かす
Rust wgpuで3DCGに挑戦する
コメント