🌐

Rust/wgpuをWebブラウザで動かす

2022/07/11に公開

screenshot

はじめに

以前の記事でRustのwgpuのネイティブでの使い方をメモしました。
今回の記事では、Webブラウザ上でwgpuを動かす方法をメモします。

  • Rust: 1.62.0
  • wgpu: 0.13.1

wgpuをwebブラウザで使う

canvasとの連携

wgpuをwebブラウザで使うには、まずどのようにしてcanvasと連携するかを選ぶ必要があります。

以前に紹介したLearn Wgpuではwinitのweb対応を利用してネイティブと同じコードを動かすようにしていました。

この記事ではwinitに依存せず、instance.create_surface_from_canvas(&canvas)を利用します。

winitのweb対応はあくまでwinitのweb版であって、ウィンドウという概念を無理やりWebのcanvasで再現しようとしているため、個人的には若干いびつなところがあると感じています。
また、winitを数ヶ月前に使った段階では、Webの非同期ランタイムとの相性も良くなく、wasm_bindgen_futuresのと併用するとwasm_bindgen_futuresが動かないという状態でした。
素直にcanvasに描画をしたい場合は、この記事で紹介するinstance.create_surface_from_canvas(&canvas)を利用する方法が素直で良いと思います。

instance.create_surface_from_canvas(&canvas)はwasm32-unknown-unknownのtargetにしか存在しないAPIですが、wgpuのdocs.rsにはwasm32-unknown-unknownのplatformが用意されていないため気が付きにくいAPIです……)

wgpuのバックエンド

wgpuはWebGLバックエンドと、WebGPUバックエンドの2つがあります。

WebGLバックエンドはWebGL 2のAPIに変換してwgpuを描画します。
このバックエンドを利用すると、WebGPUに対応していない環境でもwgpuが動くようになります。
ただし、APIはWebGL 2でエミュレートできる範囲内に制限はされてしまいます。

WebGPUバックエンドは、ブラウザのWebGPUの呼び出しに変換するので直接ブラウザのWebGPUを使えます。
現時点ではChromeでWebGPUを使うにはOrigin Trialが必要で、FirefoxやSafariでWebGPUを使うにはフラグを使う必要があるので、WebGPUバックエンドでは全ての環境では動きません。

この記事では、ChromeのOrigin TrialとWebGPUバックエンドを最初に試し、次にWebGLバックエンドを試そうと思います。

WebGPUバックエンドでwgpuを使う

WebGPUバックエンドでwgpuを使う場合にはそのままwgpuのすべての機能が使えます。

まずはCargo.tomlに次のようなdependenciesを追加します。

Cargo.toml
[package]
name = "zenn-wgpu-example-browser"
version = "0.1.0"
edition = "2021"

[dependencies]
log = "0.4.17"
gloo = "0.8.0"
vek = "0.15.8"
wasm-logger = "0.2.0"
wasm-bindgen = "0.2.81"
wasm-bindgen-futures = "0.4.31"
wee_alloc = "0.4.5"
wgpu = "0.13.1"

[dependencies.bytemuck]
version="1.7.3"
features = ["derive"]

[dependencies.web-sys]
version = "0.3.58"
features = ["HtmlCanvasElement"]

[profile.release]
panic = 'abort'
codegen-units = 1
opt-level = 'z'
lto = true

今回はwasmのtarget上にしか存在しないAPIを利用するため、VSCode上での補完のためにvscode上でのrust-analayzerのtargetをwasmに変更します。
次のような.vscode/settings.jsonを作成します。

settings.json
{
  "rust-analyzer.cargo.target": "wasm32-unknown-unknown",
}

main関数内でwasm_loggerを有効化し、非同期関数をspawn_localします。
また、wasmサイズ削減のため、wee_allocも有効化しておきます。

main.rs
use wasm_bindgen_futures::spawn_local;

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

async fn run() {
  // メイン処理
}

fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    spawn_local(run());
}

メイン処理内部では、canvas要素を作成しsurfaceを取得します。

main.rs
use gloo::utils::{document, document_element};
use wasm_bindgen::JsCast;
use web_sys::HtmlCanvasElement;

async fn run() {
    let canvas = document().create_element("canvas").unwrap();
    document_element().append_child(&canvas).unwrap();
    let canvas: HtmlCanvasElement = canvas.dyn_into().unwrap();

    let width = canvas.width();
    let height = canvas.height();

    let instance = wgpu::Instance::new(wgpu::Backends::all());
    let surface = instance.create_surface_from_canvas(&canvas);

    // ...
}

wgpuのレンダリング処理の残りはほとんど前回と同じなので省略します。
ソースコードの全文はあとに貼ってあります。

レンダリングループ部分だけ自分で書く必要があります。
レンダリングループにrequest_animation_frameを利用するために、glooのrequest_animation_frameをFutureでラップします。

main.rs
use gloo::render::{request_animation_frame, AnimationFrame};
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::Poll;

struct RequestAnimationFrameFuture {
    raf: Option<AnimationFrame>,
    delta: Rc<RefCell<Option<f64>>>,
}
impl RequestAnimationFrameFuture {
    fn new() -> Self {
        Self {
            raf: None,
            delta: Rc::new(RefCell::new(None)),
        }
    }
}
impl Future for RequestAnimationFrameFuture {
    type Output = f64;

    fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
        let this = self.get_mut();
        match this.delta.take() {
            None => {
                this.raf = Some(request_animation_frame({
                    let waker = cx.waker().clone();
                    let delta = this.delta.clone();
                    move |d| {
                        *delta.borrow_mut() = Some(d);
                        waker.wake();
                    }
                }));
                Poll::Pending
            }
            Some(delta) => Poll::Ready(delta),
        }
    }
}
async fn wait_request_animation_frame() -> f64 {
    RequestAnimationFrameFuture::new().await
}

これを利用してレンダリングループを書きます。

main.rs
async fn run() {
    // ...

    loop {
        log::info!("render!");

        let output = match surface.get_current_texture() {
            Ok(output) => output,
            Err(e) => {
                log::error!("{e:?}");
                return;
            }
        };
        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());
        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("Render Encoder"),
        });

        {
            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Render Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color {
                            r: 0.1,
                            g: 0.2,
                            b: 0.3,
                            a: 1.0,
                        }),
                        store: true,
                    },
                })],
                depth_stencil_attachment: None,
            });

            render_pass.set_pipeline(&render_pipeline);
            render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
            render_pass.draw(0..3, 0..1);
        }

        queue.submit(std::iter::once(encoder.finish()));
        output.present();

        wait_request_animation_frame().await;
    }
}

今回は使っていませんが、wait_request_animation_frame().awaitの値を使うとdelta_timeが手に入る用になっています。

Chromeバージョン103で対応しているWebGPUはWGSLの最新仕様に追いついておらず、一つ古いバージョンのWGSLで記述する必要があるようです。
具体的にはシェーダーパイプラインの指定が@vertexではなく@stage(vertex)のようになります。

shader/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
@stage(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
@stage(fragment)
fn fs_main(fin: VertexOutput) -> @location(0) vec4<f32> {
    return vec4<f32>(fin.color, 1.0);
}

index.htmlを書きます。

ChromeでWebGPUを使うには、現時点ではOrigin Trialが必須です。
localhostであっても、Origin Trialは登録しないと使えないようなので登録します。

登録すると手に入るトークンをindex.htmlに記述します。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8"/>
    <title>wgpu example</title>
    <link rel="rust" data-trunk data-wasm-opt="z"/>
    <meta http-equiv="origin-trial" content="Origin Trialのトークン"/>
  </head>
</html>

現時点ではまだ標準化されていないWebGPUのAPIを利用するため、ビルド時にweb_sys_unstable_apisのフラグを渡す必要があります。
.cargo/config.tomlを作成します。

.cargo/config.toml
[build]
rustflags = ["--cfg=web_sys_unstable_apis"]

これでtrunk serveすると、三角形が描画されます。

screenshot

trunkについては以前に記事を書いています。

ソースコード全文

main.rs
use gloo::render::{request_animation_frame, AnimationFrame};
use gloo::utils::{document, document_element};
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::Poll;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlCanvasElement;
use wgpu::util::DeviceExt;

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

struct RequestAnimationFrameFuture {
    raf: Option<AnimationFrame>,
    delta: Rc<RefCell<Option<f64>>>,
}
impl RequestAnimationFrameFuture {
    fn new() -> Self {
        Self {
            raf: None,
            delta: Rc::new(RefCell::new(None)),
        }
    }
}
impl Future for RequestAnimationFrameFuture {
    type Output = f64;

    fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
        let this = self.get_mut();
        match this.delta.take() {
            None => {
                this.raf = Some(request_animation_frame({
                    let waker = cx.waker().clone();
                    let delta = this.delta.clone();
                    move |d| {
                        *delta.borrow_mut() = Some(d);
                        waker.wake();
                    }
                }));
                Poll::Pending
            }
            Some(delta) => Poll::Ready(delta),
        }
    }
}
async fn wait_request_animation_frame() -> f64 {
    RequestAnimationFrameFuture::new().await
}

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
    position: [f32; 3],
    color: [f32; 3],
}
impl Vertex {
    fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
        wgpu::VertexBufferLayout {
            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
            step_mode: wgpu::VertexStepMode::Vertex,
            attributes: &[
                wgpu::VertexAttribute {
                    offset: 0,
                    shader_location: 0,
                    format: wgpu::VertexFormat::Float32x3,
                },
                wgpu::VertexAttribute {
                    offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
                    shader_location: 1,
                    format: wgpu::VertexFormat::Float32x3,
                },
            ],
        }
    }
}

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],
    },
];

async fn run() {
    let canvas = document().create_element("canvas").unwrap();
    document_element().append_child(&canvas).unwrap();
    let canvas: HtmlCanvasElement = canvas.dyn_into().unwrap();

    let width = canvas.width();
    let height = canvas.height();

    let instance = wgpu::Instance::new(wgpu::Backends::all());
    let surface = instance.create_surface_from_canvas(&canvas);
    let adapter = instance
        .request_adapter(&wgpu::RequestAdapterOptions {
            power_preference: wgpu::PowerPreference::default(),
            compatible_surface: Some(&surface),
            force_fallback_adapter: false,
        })
        .await
        .unwrap();
    let (device, queue) = adapter
        .request_device(
            &wgpu::DeviceDescriptor {
                features: wgpu::Features::empty(),
                limits: wgpu::Limits::default(),
                label: None,
            },
            None,
        )
        .await
        .unwrap();

    let config = wgpu::SurfaceConfiguration {
        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
        format: surface.get_supported_formats(&adapter)[0],
        width,
        height,
        present_mode: wgpu::PresentMode::Fifo,
    };
    surface.configure(&device, &config);

    let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some("Vertex Buffer"),
        contents: bytemuck::cast_slice(VERTICES),
        usage: wgpu::BufferUsages::VERTEX,
    });

    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
        label: Some("Shader"),
        source: wgpu::ShaderSource::Wgsl(include_str!("shader/shader.wgsl").into()),
    });

    let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
        label: Some("Render Pipeline Layout"),
        bind_group_layouts: &[],
        push_constant_ranges: &[],
    });

    let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
        label: Some("Render Pipeline"),
        layout: Some(&render_pipeline_layout),
        vertex: wgpu::VertexState {
            module: &shader,
            entry_point: "vs_main",
            buffers: &[Vertex::desc()],
        },
        fragment: Some(wgpu::FragmentState {
            module: &shader,
            entry_point: "fs_main",
            targets: &[Some(wgpu::ColorTargetState {
                format: config.format,
                blend: Some(wgpu::BlendState::REPLACE),
                write_mask: wgpu::ColorWrites::ALL,
            })],
        }),
        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,
    });

    loop {
        log::info!("render!");

        let output = match surface.get_current_texture() {
            Ok(output) => output,
            Err(e) => {
                log::error!("{e:?}");
                return;
            }
        };
        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());
        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("Render Encoder"),
        });

        {
            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Render Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color {
                            r: 0.1,
                            g: 0.2,
                            b: 0.3,
                            a: 1.0,
                        }),
                        store: true,
                    },
                })],
                depth_stencil_attachment: None,
            });

            render_pass.set_pipeline(&render_pipeline);
            render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
            render_pass.draw(0..3, 0..1);
        }

        queue.submit(std::iter::once(encoder.finish()));
        output.present();

        wait_request_animation_frame().await;
    }
}

fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    spawn_local(run());
}

shader/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
@stage(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
@stage(fragment)
fn fs_main(fin: VertexOutput) -> @location(0) vec4<f32> {
    return vec4<f32>(fin.color, 1.0);
}

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8"/>
    <title>wgpu example</title>
    <link rel="rust" data-trunk data-wasm-opt="z"/>
    <meta http-equiv="origin-trial" content="AvyDIV+RJoYs8fn3W6kIrBhWw0te0klraoz04mw/nPb8VTus3w5HCdy+vXqsSzomIH745CT6B5j1naHgWqt/tw8AAABJeyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJmZWF0dXJlIjoiV2ViR1BVIiwiZXhwaXJ5IjoxNjYzNzE4Mzk5fQ=="/>
  </head>
</html>

GitHubのリポジトリはこちら

WebGLバックエンドでwgpuを使う

次にWebGLバックエンドでwgpuを利用してみます。

Cargo.tomlにwgpuのwebglのfeatureを利用するように記述します。

[package]
name = "zenn-wgpu-example-browser"
version = "0.1.0"
edition = "2021"

[dependencies]
log = "0.4.17"
gloo = "0.8.0"
vek = "0.15.8"
wasm-logger = "0.2.0"
wasm-bindgen = "0.2.81"
wasm-bindgen-futures = "0.4.31"
wee_alloc = "0.4.5"
-wgpu = "0.13.1"

[dependencies.bytemuck]
version="1.7.3"
features = ["derive"]

[dependencies.web-sys]
version = "0.3.58"
features = ["HtmlCanvasElement"]
+
+[dependencies.wgpu]
+version = "0.13.1"
+features = ["webgl"]

[profile.release]
panic = 'abort'
codegen-units = 1
opt-level = 'z'
lto = true

deviceの作成時にLimitsとしてdownlevel_webgl2_defaults()を指定します。

main.rs
async fn run() {
    // ...

    let (device, queue) = adapter
        .request_device(
            &wgpu::DeviceDescriptor {
                features: wgpu::Features::empty(),
                limits: wgpu::Limits::downlevel_webgl2_defaults(),
                label: None,
            },
            None,
        )
        .await
        .unwrap();

    // ...
}

シェーダーを@stage(vertex)から@vertexに修正しておきます。

shader/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);
}

WebGLバックエンドの場合、Origin Trialは必要ないので、Origin Trialの項目をindex.htmlから削除します。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8"/>
    <title>wgpu example</title>
    <link rel="rust" data-trunk data-wasm-opt="z"/>
  </head>
</html>

これで問題なくWebGL 2のバックエンドで描画されます。

screenshot

ソースコード全文

main.rs
use gloo::render::{request_animation_frame, AnimationFrame};
use gloo::utils::{document, document_element};
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::Poll;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlCanvasElement;
use wgpu::util::DeviceExt;

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

struct RequestAnimationFrameFuture {
    raf: Option<AnimationFrame>,
    delta: Rc<RefCell<Option<f64>>>,
}
impl RequestAnimationFrameFuture {
    fn new() -> Self {
        Self {
            raf: None,
            delta: Rc::new(RefCell::new(None)),
        }
    }
}
impl Future for RequestAnimationFrameFuture {
    type Output = f64;

    fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
        let this = self.get_mut();
        match this.delta.take() {
            None => {
                this.raf = Some(request_animation_frame({
                    let waker = cx.waker().clone();
                    let delta = this.delta.clone();
                    move |d| {
                        *delta.borrow_mut() = Some(d);
                        waker.wake();
                    }
                }));
                Poll::Pending
            }
            Some(delta) => Poll::Ready(delta),
        }
    }
}
async fn wait_request_animation_frame() -> f64 {
    RequestAnimationFrameFuture::new().await
}

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
    position: [f32; 3],
    color: [f32; 3],
}
impl Vertex {
    fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
        wgpu::VertexBufferLayout {
            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
            step_mode: wgpu::VertexStepMode::Vertex,
            attributes: &[
                wgpu::VertexAttribute {
                    offset: 0,
                    shader_location: 0,
                    format: wgpu::VertexFormat::Float32x3,
                },
                wgpu::VertexAttribute {
                    offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
                    shader_location: 1,
                    format: wgpu::VertexFormat::Float32x3,
                },
            ],
        }
    }
}

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],
    },
];

async fn run() {
    let canvas = document().create_element("canvas").unwrap();
    document_element().append_child(&canvas).unwrap();
    let canvas: HtmlCanvasElement = canvas.dyn_into().unwrap();

    let width = canvas.width();
    let height = canvas.height();

    let instance = wgpu::Instance::new(wgpu::Backends::all());
    let surface = instance.create_surface_from_canvas(&canvas);
    let adapter = instance
        .request_adapter(&wgpu::RequestAdapterOptions {
            power_preference: wgpu::PowerPreference::default(),
            compatible_surface: Some(&surface),
            force_fallback_adapter: false,
        })
        .await
        .unwrap();
    let (device, queue) = adapter
        .request_device(
            &wgpu::DeviceDescriptor {
                features: wgpu::Features::empty(),
                limits: wgpu::Limits::downlevel_webgl2_defaults(),
                label: None,
            },
            None,
        )
        .await
        .unwrap();

    let config = wgpu::SurfaceConfiguration {
        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
        format: surface.get_supported_formats(&adapter)[0],
        width,
        height,
        present_mode: wgpu::PresentMode::Fifo,
    };
    surface.configure(&device, &config);

    let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some("Vertex Buffer"),
        contents: bytemuck::cast_slice(VERTICES),
        usage: wgpu::BufferUsages::VERTEX,
    });

    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
        label: Some("Shader"),
        source: wgpu::ShaderSource::Wgsl(include_str!("shader/shader.wgsl").into()),
    });

    let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
        label: Some("Render Pipeline Layout"),
        bind_group_layouts: &[],
        push_constant_ranges: &[],
    });

    let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
        label: Some("Render Pipeline"),
        layout: Some(&render_pipeline_layout),
        vertex: wgpu::VertexState {
            module: &shader,
            entry_point: "vs_main",
            buffers: &[Vertex::desc()],
        },
        fragment: Some(wgpu::FragmentState {
            module: &shader,
            entry_point: "fs_main",
            targets: &[Some(wgpu::ColorTargetState {
                format: config.format,
                blend: Some(wgpu::BlendState::REPLACE),
                write_mask: wgpu::ColorWrites::ALL,
            })],
        }),
        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,
    });

    loop {
        log::info!("render!");

        let output = match surface.get_current_texture() {
            Ok(output) => output,
            Err(e) => {
                log::error!("{e:?}");
                return;
            }
        };
        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());
        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("Render Encoder"),
        });

        {
            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Render Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color {
                            r: 0.1,
                            g: 0.2,
                            b: 0.3,
                            a: 1.0,
                        }),
                        store: true,
                    },
                })],
                depth_stencil_attachment: None,
            });

            render_pass.set_pipeline(&render_pipeline);
            render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
            render_pass.draw(0..3, 0..1);
        }

        queue.submit(std::iter::once(encoder.finish()));
        output.present();

        wait_request_animation_frame().await;
    }
}

fn main() {
    wasm_logger::init(wasm_logger::Config::default().module_prefix("zenn_wgpu_example_browser"));
    spawn_local(run());
}

shader/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);
}

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8"/>
    <title>wgpu example</title>
    <link rel="rust" data-trunk data-wasm-opt="z"/>
  </head>
</html>

GitHubのリポジトリはこちら

おわりに

今回の記事ではブラウザでwgpuを使ってみました。

Origin Trialが必要ですが、ChromeのWebGPUを直接動かすことができました。
また、WebGLバックエンドを使うことで、WebGPUに対応していない環境でも、WebGL 2に対応していれば動作させることができました。

WebGLバックエンドのお陰でWebGPUに対応していなくてもwgpuが動かせるので、ブラウザ上でwgpuは十分使っていける状況にあると思います。
WebGLより現代的なAPIで記述できるwgpuは書きやすいので、積極的に使っていきたいです。

Discussion