【Rust】winit と tiny-skia で低レベルなグラフィックス描画〜ことはじめ〜

はじめに

Are we GUI Yet? にある通り、Rust で GUI を扱う場合のライブラリは、未だ決定打が無い状況にあります。

ディスクトップ・アプリケーション の構築には、WebView ベースの Tauri が、現時点での有力候補になるでしょうか。最近では、Atom の後継エディタの Zed の内部で開発されている GPUI にも注目が集まっているように思います。

その他、egui や Slint や Iced、GTK のラッパライブラリなど多々あり、有象無象の様相を呈しています。

GPUレンダリングで3D グラフィックスを扱う場合はwgpuがベースになるのは間違いないでしょう。

CPUレンダリングだけの単純な2Dグラフィックスを扱う場合は、Skia の亜種のライブラリを選ぶことになりそうです。 tiny-skiaは、Skia を直接利用している訳ではなく実装を参考にしているだけで、パフォーマンスも Skia には劣りますが、利用しやすさから、多く使われている印象です。しかしフォントの描画がサポートされていないため、raqote が選択されているケースも多そうです。

Rust の GUI 周りのライブラリは色々あり、現時点では何を選ぶのがベストなのかが分からない状況が続いています。


ということで、ここでは、低レベルの2Dグラフィックス操作を winit と tiny-skia で様子を見てみます。なにが「ということ」なのかは分かりませんが。


winit

winit は、マルチプラットフォームの window 制御ライブラリです。

window を操作するだけのシンプルなライブラリで、グラフィックスの描画自体は他のライブラリを選んで使うことになります。

最初に winit で window を生成していきましょう。

mkdir winit-tiny-skia
cd winit-tiny-skia
cargo init
cargo add winit

main.rs を以下のように編集します。

use winit::event::{ Event, WindowEvent };
use winit::event_loop::{ ControlFlow, EventLoop };
use winit::window::WindowBuilder;

fn main() {

    let event_loop = EventLoop::new().unwrap();

    let window = WindowBuilder::new()
        .with_inner_size(winit::dpi::LogicalSize::new(500, 300))
        .with_title("winit")
        .build(&event_loop).unwrap();

    event_loop.set_control_flow(ControlFlow::Wait);

    let _ = event_loop.run(move |event, elwt| {
        match event {
            Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => elwt.exit(),
            Event::AboutToWait => window.request_redraw(),
            Event::WindowEvent {
                window_id, event: WindowEvent::RedrawRequested
            } if window_id == window.id() => {
                // Redraw the application.
            },
            _ => ()
        }
    });
}

EventLoop::new() でイベントループを生成し、WindowBuilder::new() で生成したウインドウと紐づけ、イベントループで受信したイベントに応じて処理を記載していきます。

この状態で実行すれば以下のようになります。

cargo run


softbuffer

作成したウインドウへの描画は、raw-window-handleに対応したライブラリを使うことができます。

ここでは、描画バッファとしてsoftbufferを使って描画を行います。

softbuffer を追加します。

cargo add softbuffer

前述のコード let window = WindowBuilder::new() ... の後に続けて、softbuffer を準備します。

let window = std::rc::Rc::new(window);
let context = softbuffer::Context::new(window.clone()).unwrap();
let mut surface = softbuffer::Surface::new(&context, window.clone()).unwrap();

作成した surface から描画バッファを作成し、描画バッファに対して書き込みを行います。

前述のコードのコメント部分 // Redraw the application に以下の処理を追加します。

let (width, height) = {
    let size = window.inner_size();
    (size.width, size.height)
};
surface.resize(
    NonZeroU32::new(width).unwrap(),
    NonZeroU32::new(height).unwrap(),
).unwrap();

let mut buffer = surface.buffer_mut().unwrap();
for index in 0..(width * height) {
    let y = index / width;
    let x = index % width;

    let r = 0;
    let g = 0;
    let b = 255 * x / width;

    buffer[index as usize] = b | (g << 8) | (r << 16);
}
buffer.present().unwrap();

buffer に対してピクセルを設定すれば、その内容で描画できます。

実行すれば以下のようになります。


tiny-skia

softbuffer で準備した描画バッファにピクセルを設定することでグラフィックス操作ができるようになりましたが、あまりにも低レベルすぎるので、グラフィックスライブラリである tiny-skia を使います。

以下で追加します。

cargo add tiny_skia

tiny_skia での描画を以下のように行います。

let mut pixmap = Pixmap::new(width, height).unwrap();
let path = PathBuilder::from_circle(
    (width / 2) as f32,
    (height / 2) as f32,
    100 as f32,
).unwrap();

let mut paint = Paint::default();
paint.set_color_rgba8(128, 0, 0, 255);
pixmap.fill_path(
    &path,
    &paint,
    FillRule::EvenOdd,
    Transform::identity(),
    None,
);

Pixmap を用意し、PathBuilder::from_circle() で円のパスを作成し、pixmap.fill_path() でパスを塗りつぶしで描画します。

Pixmap の内容は、以下のように描画バッファに設定することで描画することができます。

let mut buffer = surface.buffer_mut().unwrap();
for index in 0..(width * height) as usize {
    buffer[index] =
         pixmap.data()[index * 4 + 2] as u32
      | (pixmap.data()[index * 4 + 1] as u32) << 8
      | (pixmap.data()[index * 4 + 0] as u32) << 16;
}

まとめると以下のコードになります。

use winit::event::{ Event, WindowEvent };
use winit::event_loop::{ ControlFlow, EventLoop };
use winit::window::WindowBuilder;
use tiny_skia::{ FillRule, Paint, PathBuilder, Pixmap, Transform };

fn main() {

    let event_loop = EventLoop::new().unwrap();

    let window = WindowBuilder::new()
        .with_inner_size(winit::dpi::LogicalSize::new(500, 300))
        .with_title("winit")
        .build(&event_loop).unwrap();

    let window = std::rc::Rc::new(window);
    let context = softbuffer::Context::new(window.clone()).unwrap();
    let mut surface = softbuffer::Surface::new(&context, window.clone()).unwrap();

    event_loop.set_control_flow(ControlFlow::Wait);

    let _ = event_loop.run(move |event, elwt| {
        match event {
            Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => elwt.exit(),
            Event::AboutToWait => window.request_redraw(),
            Event::WindowEvent {
                window_id, event: WindowEvent::RedrawRequested
            } if window_id == window.id() => {
                let (width, height) = {
                    let size = window.inner_size();
                    (size.width, size.height)
                };
                surface.resize(
                    core::num::NonZeroU32::new(width).unwrap(),
                    core::num::NonZeroU32::new(height).unwrap(),
                ).unwrap();

                let mut pixmap = Pixmap::new(width, height).unwrap();
                let path = PathBuilder::from_circle(
                    (width / 2) as f32,
                    (height / 2) as f32,
                    100 as f32,
                ).unwrap();

                let mut paint = Paint::default();
                paint.set_color_rgba8(255, 112, 67, 255);
                pixmap.fill_path(
                    &path,
                    &paint,
                    FillRule::EvenOdd,
                    Transform::identity(),
                    None,
                );

                let mut buffer = surface.buffer_mut().unwrap();
                for index in 0..(width * height) as usize {
                    buffer[index] =
                        pixmap.data()[index * 4 + 2] as u32
                            | (pixmap.data()[index * 4 + 1] as u32) << 8
                            | (pixmap.data()[index * 4 + 0] as u32) << 16;
                }
                buffer.present().unwrap();
            },
            _ => ()
        }
    });
}

実行結果は以下のようになります。


まとめ

winit と tiny-skia による単純なグラフィックス描画を見てみました。

ひとまず長くなりそうなのでここで区切り、次回は続けて テトリス の実装まで進めようと思います。

blog1.mammb.com