🔺

Rustのグラフィクス周りメモ/wgpuとその使い方

2022/07/05に公開

screenshot

はじめに

Rustでグラフィクスプログラムをクロスプラットフォームに書く環境が、wgpuというクレート中心に揃っていて大変素晴らしいので、それについてメモします。
記事の後半では実際にwgpuを使って三角形の描画までを試してみます。

バージョン

  • Rust: 1.62.0
  • wgpu: 0.13.1

グラフィクスAPI

この記事ではグラフィクスプログラミングを、グラフィクスAPIを利用したプログラミングを指すものとします。
まずはグラフィクスAPIについて最初に説明をします。

グラフィクスAPI

グラフィクスのAPIとは、具体例をあげるとOpenGLやDirctX的なやつです。
現代においてリアルタイムグラフィクスを利用するプログラムを書く場合、CPUだけで実行される普通のプログラムとは違い、GPUを使うプログラムを書くことになります。
GPUを使うプログラムを書くとはどういうことかというと、GPUにコマンドを発行していくようなプログラムを書くということです。
そのためには各OSが提供するグラフィクスAPI経由で、GPUドライバというGPUメーカーが提供するプログラムを叩くことになります。

グラフィクスAPIはリアルタイムなグラフィクスを必要とする場所で使われます。
主な使用用途はゲームやDCCツール、そして普段使うGUIプログラムも裏ではGPUのハードを使うためにグラフィクスAPIを使う場合があります。
GPUは特にゲーム分野と密接に絡んで発展してきました。
そのためゲームは主要なグラフィクスAPIの使用用途の一つです。
ゲームのレンダリングの部分にはグラフィクスプログラミングがあります。
ゲームエンジンを使う人にとっては、直接グラフィクスAPIを叩く必要はあまりないかもしれませんが、そのゲームエンジンが行うレンダリングはグラフィクスAPIを利用して作られています。

グラフィクスAPIはそれなりに歴史のある分野で、そのためAPI自体にも世代の差というものがあります。
現代において多く利用されるグラフィクスAPIには大きく2つの世代に分けられます。
一つはOpenGLやDirect3D 11までの、昔ながらのグラフィクスAPIです。
そしてもう一つは、Metal、DirectX 12、Vulkanなどに代表される新世代のグラフィクスAPIです。

それぞれのAPIについて簡単に見ていきます。

OpenGL

OpenGLはクロスプラットフォームで動くグラフィクスAPIです。
最近Appleが非推奨にしたことで驚かれましたが、それもそのはずで本当に広く使われているAPIです。

歴史は古く1980年代中盤にSillicon Graphics社が開発したIRISシリーズの端末とワークステーションに乗っていた、UNIXとIRIS GLのシステムの歴史を受け継いでいます。
OpenGLはIRIS GLを1992年に業界標準として策定したものとなります。
このように当時のグラフィクス周りは高価なUNIXシステム用に作られたものでした。

OpenGLはリソースの抽象度が高く、テクスチャやバッファなどはプログラマからはすべて整数の識別子として見えており、アドレスを直接扱う必要はありません。
この抽象化は今に至るまでワークする優れた抽象化であるといえるでしょう。

一方で、OpenGLは現代から見るとあまり良くないAPI設計である部分も含まれます。
その筆頭としてよく槍玉に挙げられるのは、ステートフルなAPIです。
APIの呼び出し順序が意味を持っており、APIを呼び出すことで、いわばグローバルの状態を更新しながら描画を行っていきます。
これは近年のマルチスレッドなCPU環境から扱うことを難しくしました。
この問題の解消は新世代APIの開発のモチベーションの一つともなっています。

OpenGLにはいくつかのバージョンが有り、またモバイルなどに向けたサブセットであるOpenGL ESというバージョンも存在します。

Direct3D

Direct3DはWindowsのOSが提供するグラフィクスAPIです。
DirectXのサブシステムとしてDirect3Dが位置づけられています。

WindowsではOpenGLも使えるため、ゲームの開発者はOpenGLで書くかDirectXで書くかを選択することになります。
OpenGLはDirectXに比べて、特に初期の頃はWindowsによる対応がよくありませんでした。
そのため性能の良いゲームをWindows向けに書くためにはDirectXを使うほうがよく、OpenGLは遅いと言った印象を作り出すまででした。
ただし、これはただの印象操作だけでなく、実際にOpenGLはAPIコールが多く不利な面がなかったわけではありません。

Direct3Dにはいくつかのバージョンが有り、MMDなどの古いソフトで採用されているDirect3D9を始めとして、Direct3D11、そして新世代APIのDirect3D12などがあります。
バージョンを重ねるごとにAPIは野心的な更新が行われており、新しいGPUの機能のアクセスをしたい場合などにはDirect3Dが向いている印象を受けます。
最近GPUの売り文句として聞くハードウェアレイトレもDirect3Dがまっさきに対応していました。

XInputなどのDirectXのゲームに使えるAPIなどもあり、今でもWindows向けにゲームを作る場合に第一候補としてあがるAPIと言えるでしょう。

Metal

MetalはAppleが2014年に発表した新しいグラフィクスAPIです。
macOSにおけるOpenGLの対応もあまり良いとはいえず、とくにMetal発表当時はコンピュートシェーダが使えるようになったOpenGL 4.3に対応していませんでした。
このAPIの発表の後、2018年にAppleではOpenGLが非推奨とされたことは記憶に新しいです。

OpenGLの高度な抽象化に比べ、よりローレベルなAPIとなり、VulkanやDirect3D12のようなオーバーヘッドの少ないAPIとなっています。

Direct3D12

2015年にリリースされたDirect3D12もMetalと同じような低水準のグラフィクスAPIです。
CommandListとCommandAllocatorのマルチスレッド化が大きな進歩です。
APIの名称としてはDirect3Dを引き継ぎますが、OpenGLからVulkanあるいはMetalへの転向と同じくらいの大きなアップデートがあるバージョンとなっています。

Vulkan

2016年にVulkan 1.0という低水準グラフィクスライブラリがリリースされました。
OpenGLの仕様を管理しているKhronosというところが提供しています。
Direct3D12等と同様に、コマンドバッファとコマンドキューいう仕組みを使って、GPUのコマンドをマルチスレッドで構築することも可能だそうです。
Intel、nVIDIA、AMDなどの各社のGPUのアーキテクチャが揃ってきた現代において不要な抽象化が含まれていて、モダンではないAPIであるOpenGLとは別にAPIを作ることで、現代的なグラフィクスAPIを実現しています。

特徴としては非常に低レベルであることが挙げられます。
同じ三角形を描画するにしても、OpenGLでなら数十から数百行で済むところを、Vulkanを使うと1000行以上の記述が必要になることで有名です。
これは、APIが提供する抽象度が低く自分で実装する部分が多くなったこと、そしてGPUの状態を陽に指定する必要が有ることなどが大きな原因です。
しかしプログラムの行数が増えるのが必ずしも悪いことではありません。
自分で書かねばならない範囲が増えたというのは、ドライバに隠されず自分でかける部分が増えたということで、より高度な最適化が行なえます。
また、大量な構造体によるGPUステート用の構造体の利用は、OpenGLで問題となっていたグローバルなステート管理というものから開放されることでもあります。
とはいえ、流石に記述が煩雑であることは否めないでしょう。

VulkanはOpenGLと同じくクロスプラットフォームで動く標準APIとして提唱されましたが、AppleはMetalの利用を推奨するため直接Vulkanをサポートしません。
そのため、macOS上でVulkanを動かすためには、MoltenVKというMetalの上に実装されたVulkanを利用する必要があります。

wgpuについて

さて、グラフィクスAPIについて一通り軽く説明したところで、これらグラフィクスAPIをRust言語から使うためのクレートであり、またWebGPUというAPIの実装でもあるwgpuというクレートについて見ていきます。

WebのグラフィックスAPI

wgpuはWebGPUというWebブラウザ向けのグラフィクスAPIの実装です。
まずはWebブラウザ向けのグラフィクスAPIについて見ていきましょう。

WebGL

WebブラウザでGPUによる3D表現を行うとなるとまっさきに思い浮かぶのはWebGLでしょう。
WebGLは2011年にHTML5の波に乗って登場したcanvas要素で使えるAPIです。
その実態はOpenGL ESのAPIをほとんどそのままJavaScriptに露出したようなAPIとなっています。

WebGLにはOpenGL ES 2.0相当の機能を有するWebGL 1.0と、OpenGL 3.2相当の機能を有するWebGL 2.0があります。
最近SafariがWebGL 2.0に対応したことが話題になっていましたね。

WebGLではまだコンピュートシェーダに当たる機能がつかえる状況ではありません。
GPGPU用途や高度なグラフィクス用途においてコンピュートシェーダが使えないというのはなかなか厳しい状況ではあります。

WebGPU

WebGPUは最近になって使用策定が行われている新しいWeb向けのAPIです。
Metal、Direct3D12、Vulkanの流れを受けた新世代のAPIとなっており、これらのAPIの新機能をWebで使えるようにするためのAPIです。
WebGLがOpenGLの薄いラッパーで、OpenGLの中身をほとんどそのままAPIとして公開していたのに対して、WebGPUはDirect3D12やMetal、VulkanなどのOSネイティブのAPIに対する独自の抽象化層が入っています。
これによって、OSが何であれ、同じAPIでグラフィクスを叩けるWebらしい仕様となります。
この抽象化はとても良くできていて、ネイティブのAPIの差異を吸収しつつも、ネイティブAPIから離れすぎず作られています。

標準化はW3Cで行われ、Safariを作るAppleも参加しています。
VulkanをAppleが採用しなかったことで途絶えたクロスプラットフォームグラフィクスAPIの夢が、WebGPUで実現しようとしています。
低レベルの新世代グラフィクスAPIを共通化するAPIを作ることは並大抵のことではなく、なかなか仕様策定は難航しているようですが、2022年のQ3にも1.0到達を目指しているようです。
仕様自体は最近までなかなか安定しておらず、ここ半年でもWebGPU向けシェーダー言語であるWGSLの仕様が大きく変わっています。

現状ではWebブラウザでの実行にはChromeではOrigin Trial、FirefoxやSafariではフラグが必要となっています。

wgpu

wgpuは上述のWebGPUのRust実装です。
FirefoxのレンダリングエンジンであるGeckoにも組み込まれています。

WebGPUの実装というのはどういうことかというと、wgpuを利用してWebGPUの形式のAPIでプログラムを記述すると、裏でDirect3DやVulkan、Metalなどの呼び出しとして処理されるライブラリということです。
WebGPUの実装ではありますが、Webブラウザを使わないと実行できないというわけではありません。
ネイティブでもVulkanなどのOSネイティブのAPIのバックエンドで動かすことが可能で、スタンドアロンのウィンドウをwgpuで書くことが可能です。
また、Wasm経由でWebブラウザのAPIを直接呼び出す形にすることもできて、Webブラウザでもネイティブでもwgpuというクレートを使えばプログラムを書くことができます。

wgpu周りのライブラリクレートについては多少ややこしいので、wgpuの公式のリポジトリで配布されているBig Pictureというのを見るのが良いでしょう。

周辺クレートとBig Picture

wgpuの公式リポジトリで、big-picture.pngという画像が用意されています。

big-picture

左下にはOSドライバと、それを叩くOSネイティブのAPIのバインディングクレートが並んでいます。
ashはVulkanのバインディングクレートで、全てunsafeの非常に薄いラッパーです。
そのほかmetal-rsやd3d12-rsなどがあります。
興味深いことにバックエンドにOpenGL ES 3.0であるglowやWebGL 2.0を指定することもできます。

これらネイティブのAPIの抽象化としてハードウェアアブストラクションレイヤー、HALというwgpu-halというクレートが用意されています。
wgpu-halはnagaというクレートを利用してシェーダを生成します。
各種APIは利用するシェーダー言語もことなり、Direct3DのHLSL、MetalのMSL、VulkanのSPIR-VやGLSLなどとWebGPUのWGSLを相互変換するためにnagaというクレートが使われています。

wgpu-coreという部分がブラウザに組み込まれる部分です。
FirefoxのGeckoや、Denoなどはこのwgpu-coreを使うでしょう。

そして、このwgpu-coreやWasm経由のWebブラウザのAPI呼び出しを行うライブラリがwgpuです。
Rustのグラフィクスを利用するクレートはwgpuの上に成り立っていることが多いです。
Rustのクリエイティブコーディングを行うNannouというライブラリや、Rust製のゲームエンジンBevy、あるいはRustで作られているGUIライブラリのicedなどはwgpuの上に成り立っています。
VulkanのashなどOSの提供するグラフィクスAPIに直接依存せず、wgpuを挟むことでクロスプラットフォームに開発できるため、多くのライブラリがwgpuの上に作られていっています。
wgpuがクロスプラットフォームなグラフィクスAPIの基底部分として使われるようになっている様子がわかります。

wgpuは数日前に0.13.0が出たばかりです。
wgpuはWebGPUのAPIを非常にRustyにくるんでおり、Rust的に書くことができてとても書き味が良いです。
Vulkanのラッパーであるashを利用したときは、そのC++を薄くバインドしただけのRustらしくないAPIの記述が大変でしたが、wgpuはオプショナルな値はnullではなくOption<T>型で定義されていたりなど非常にRust的にAPIが作られていて、使いやすいです。

余談ですが、big pictureは定期的に更新されています。
例えば2020年の時点では次のようでした。

big-picture-old.png

wgpu-halがgfx-rsというものでした。
この当時、VulkanやMetal、Direct3D12などを統一するAPIをgfx-rsで作ると聞いたときはバベルの塔のように完成しないのではないかとすら思いましたが、WebGPUの策定と合わせて進むことで、バベルの塔は建ちました。

この当時wgpuと呼ばれていた部分は現在はwgpu-coreと呼ばれています。
この当時wgpu-rsと呼ばれていた部分が現在はwgpuと呼ばれるようになっています。
wgpu-rsという記述のある記事は古いものとなっていることが予想されます。

壮大な夢であるクロスプラットフォームなグラフィクスAPIというのが実現されつつあって素晴らしいですね。

wgpuを使ってみる

それでは実際にwgpuを使ってみましょう。

wgpuのバージョン

wgpuはバージョン0.13.1が数日前に出たばかりです。
0.13.1が出るより前、0.12時代だった半年ほどの間にWGSLの仕様が大きく変わり最新仕様を利用するためには0.12ではなくgecko用ブランチを利用する必要がありました。
今後も、最新仕様を利用したい場合はgeckoブランチを利用する必要があるかもしれません。

今回は0.13.1を利用します。
Cargo.tomlに次のように記述します。

Cargo.toml
[dependencies]
wgpu = "0.13.1"

geckoブランチを利用する場合は次のように記述してください。

Calgo.toml
[dependencies.wgpu]
git = "https://github.com/gfx-rs/wgpu.git"
branch = "gecko"

依存ライブラリ

wgpuの他に、ネイティブのウィンドウを出すためにwinit、エラーログを出すためにenv_loggerとlogクレート、そしてasync関数をブロックして同期実行するためにpollsterというクレートを追加します。
bytemuckを頂点データの構造体をバイト列にするために使います。

Cargo.toml
[dependencies]
bytemuck = { version = "1.10.0", features = ["derive"] }
env_logger = "0.9.0"
log = "0.4.17"
pollster = "0.2.5"
wgpu = "0.13.1"
winit = "0.26.1"

env_loggerの初期化

プログラムの最初にenv_logger::init()を用意しておきます。
wgpuはエラーで落ちるときにlogを利用してエラーを伝えます。
env_loggerでなくても良いですが、なにかしらlogに対応したクレートを用意して置かなければエラーを見ることすらできずサイレントに落ちます。

main.rs
fn main() {
    env_logger::init();
}

winitの準備

winitでウィンドウを作ります。

main.rs
use winit::{
    event::*,
    event_loop::{ControlFlow, EventLoop},
    window::WindowBuilder,
};

fn main() {
    env_logger::init();
    let event_loop = EventLoop::new();
    let window = WindowBuilder::new().build(&event_loop).unwrap();

    event_loop.run(move |event, _, control_flow| match event {
        Event::WindowEvent {
            ref event,
            window_id,
        } if window_id == window.id() => match event {
            WindowEvent::CloseRequested
            | WindowEvent::KeyboardInput {
                input:
                    KeyboardInput {
                        state: ElementState::Pressed,
                        virtual_keycode: Some(VirtualKeyCode::Escape),
                        ..
                    },
                ..
            } => *control_flow = ControlFlow::Exit,
            _ => {}
        },
        _ => {}
    });
}

event_loop.run()でウィンドウの処理を書きます。
閉じるボタン及びESCキーが押されたときにウィンドウを閉じてアプリケーションを終了します。

instanceを作る

wgpuのアプリケーションはinstanceという構造体と紐付けられることになります。
バックエンドとして個別に指定できる他、allとすることですべてのバックエンドから必要なものを選択してくれます。

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

    let window = WindowBuilder::new().build(&event_loop).unwrap();

    let size = window.inner_size();

    let instance = wgpu::Instance::new(wgpu::Backends::all());

    // ...
}

Surfaceを作る

描画先であるSurfaceを次のようにして作ります。
unsafeが必要になりますが、基本的に個々だけだと思います。

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

    let surface = unsafe { instance.create_surface(&window) };

    // ...
}

adapterを作る

アダプターはOSのネイティブグラフィックスAPIからWebGPUへの変換レイヤーです。
async関数となるので、ここでは同期的にblock_onするためpollsterを使います。
pollsterはtokioやasync-stdなどの非同期ランタイムに非依存でFutureをblock_onするためのクレートです。

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

    let adapter = pollster::block_on(async {
        instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::default(),
                compatible_surface: Some(&surface),
                force_fallback_adapter: false,
            })
            .await
            .unwrap()
    });

    // ...
}

デバイスとキューを作る

論理デバイスとキューを作ります。

GPUはシステムに1つや2つなど限られた数しかありませんが、GPUを使うアプリケーションは無数にあります。
他のアプリケーションが使うテクスチャの内容などが読めないように、GPUを多重化します。
これは各アプリケーションが自分だけのGPUを持っているように振る舞えるもので、この自分だけの仮想的なGPUを論理デバイスと言ったりします。

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

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

    // ...
}

SurfaceConfigurationを作る

SurfaceConfigurationは、サーフェスの表示方法やフォーマットなどを定めるものです。
画面のリサイズしたときなどもこのConfigurationを変更することになります。

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

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

    // ...
}

widthとheightは0出ないことを確認します。
0だとアプリがクラッシュします。
PresentModeはFifoの他にMailboxなどがあります。
画面のティアリングを防ぐためにダブルバッファリングを行ったりしますが、そのバッファの入れ替えなどの方法になります。

リサイズのクロージャを作る

リサイズした際の処理をクロージャにまとめます。

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

    let resize = |device: &wgpu::Device,
                  surface: &wgpu::Surface,
                  config: &mut wgpu::SurfaceConfiguration,
                  new_size: winit::dpi::PhysicalSize<u32>| {
        if new_size.width > 0 && new_size.height > 0 {
            config.width = new_size.width;
            config.height = new_size.height;
            surface.configure(&device, &config);
        }
    };

    // ...
}

クロージャをウィンドウをリサイズした際に呼び出します。

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

    event_loop.run(move |event, _, control_flow| match event {
        Event::WindowEvent {
            ref event,
            window_id,
        } if window_id == window.id() => match event {
            WindowEvent::CloseRequested
            | WindowEvent::KeyboardInput {
                input:
                    KeyboardInput {
                        state: ElementState::Pressed,
                        virtual_keycode: Some(VirtualKeyCode::Escape),
                        ..
                    },
                ..
            } => *control_flow = ControlFlow::Exit,
            WindowEvent::Resized(physical_size) => {
                resize(&device, &surface, &mut config, *physical_size);
            }
            WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
                resize(&device, &surface, &mut config, **new_inner_size);
            }
            _ => {}
        },
        _ => {}
    });

    // ...
}

頂点用の構造体をつくる

頂点データ用の構造体を作ります。

main.rs
#[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],
    },
];

fn main() {
    // ...
}

頂点バッファを作る

頂点バッファを用意します。

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

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

    // ...
}

シェーダを作る

シェーダーを書きます。
shader.wgslというファイル名でmain.rsの隣に保存しておきます。

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

main.rsにshaderモジュールを作成します。

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

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

    // ...
}

レンダーパイプラインを作る

レンダーパイプラインレイアウトとレンダーパイプラインを作ります。

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

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

    // ...
}

レンダリングを記述する

レンダリングをクロージャにまとめます。

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

    let render = |device: &wgpu::Device,
                  queue: &wgpu::Queue,
                  surface: &wgpu::Surface,
                  render_pipeline: &wgpu::RenderPipeline,
                  vertex_buffer: &wgpu::Buffer| {
        let output = surface.get_current_texture()?;
        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();

        Ok(())
    };

    // ...
}

レンダリングをwinitのevent_loop.run()の中で呼び出します。

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

    event_loop.run(move |event, _, control_flow| match event {
        Event::WindowEvent {
            ref event,
            window_id,
        } if window_id == window.id() => match event {
            WindowEvent::CloseRequested
            | WindowEvent::KeyboardInput {
                input:
                    KeyboardInput {
                        state: ElementState::Pressed,
                        virtual_keycode: Some(VirtualKeyCode::Escape),
                        ..
                    },
                ..
            } => *control_flow = ControlFlow::Exit,
            WindowEvent::Resized(physical_size) => {
                resize(&device, &surface, &mut config, *physical_size);
            }
            WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
                resize(&device, &surface, &mut config, **new_inner_size);
            }
            _ => {}
        },
        Event::RedrawRequested(window_id) if window_id == window.id() => {
            match render(&device, &queue, &surface, &render_pipeline, &vertex_buffer) {
                Ok(_) => {}
                Err(wgpu::SurfaceError::Lost) => {
                    resize(&device, &surface, &mut config, size);
                }
                Err(e) => eprintln!("{:?}", e),
            }
        }
        Event::MainEventsCleared => {
            window.request_redraw();
        }
        _ => {}
    });
}

結果

screenshot

三角形が描画されました。

ソースコード全文

main.rs
use wgpu::util::DeviceExt;
use winit::{
    event::*,
    event_loop::{ControlFlow, EventLoop},
    window::WindowBuilder,
};

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

fn main() {
    env_logger::init();
    let event_loop = EventLoop::new();
    let window = WindowBuilder::new().build(&event_loop).unwrap();

    let size = window.inner_size();

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

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

    let resize = |device: &wgpu::Device,
                  surface: &wgpu::Surface,
                  config: &mut wgpu::SurfaceConfiguration,
                  new_size: winit::dpi::PhysicalSize<u32>| {
        if new_size.width > 0 && new_size.height > 0 {
            config.width = new_size.width;
            config.height = new_size.height;
            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.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,
    });

    let render = |device: &wgpu::Device,
                  queue: &wgpu::Queue,
                  surface: &wgpu::Surface,
                  render_pipeline: &wgpu::RenderPipeline,
                  vertex_buffer: &wgpu::Buffer| {
        let output = surface.get_current_texture()?;
        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();

        Ok(())
    };

    event_loop.run(move |event, _, control_flow| match event {
        Event::WindowEvent {
            ref event,
            window_id,
        } if window_id == window.id() => match event {
            WindowEvent::CloseRequested
            | WindowEvent::KeyboardInput {
                input:
                    KeyboardInput {
                        state: ElementState::Pressed,
                        virtual_keycode: Some(VirtualKeyCode::Escape),
                        ..
                    },
                ..
            } => *control_flow = ControlFlow::Exit,
            WindowEvent::Resized(physical_size) => {
                resize(&device, &surface, &mut config, *physical_size);
            }
            WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
                resize(&device, &surface, &mut config, **new_inner_size);
            }
            _ => {}
        },
        Event::RedrawRequested(window_id) if window_id == window.id() => {
            match render(&device, &queue, &surface, &render_pipeline, &vertex_buffer) {
                Ok(_) => {}
                Err(wgpu::SurfaceError::Lost) => {
                    resize(&device, &surface, &mut config, size);
                }
                Err(e) => eprintln!("{:?}", e),
            }
        }
        Event::MainEventsCleared => {
            window.request_redraw();
        }
        _ => {}
    });
}
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);
}

wgpuの学習教材

wgpuの学習用にとても良いリソースがあるので最後に紹介しておきます。

著者の方はpatreonを募集しているようなので良ければ。
地味に私もpatronになっています。

patron

参考リンク

おわりに

この記事では各種グラフィクスAPIの紹介から始め、wgpuの紹介をしました。
その後、wgpuを使ってネイティブで三角形を描画するのをやりました。

次回以降の記事では、wgpuをWebブラウザで利用するのをやってみたいと思います。

Discussion