Open21

ゲームエンジンを作る

yuma14yuma14

関連リポジトリ

https://github.com/yuma140902/Reverie

ReverieEngine

https://github.com/yuma140902/ReverieEngine-WebGL

ReverieEngineをWebGLに対応させたかったもの

https://github.com/yuma140902/RustyCraft

ReverieEngineを使って作ったゲーム(?)

https://github.com/kcs1959/BlockingIO-client

ReverieEngineを使って作ったゲーム

https://github.com/yuma140902/blocking-io-webgl

上のゲームをWebGLに対応させたかったもの

https://github.com/yuma140902/tetrs

ReverieEngineのバックエンドをOpenGLからwgpuに変更するための布石として作ったリポジトリ
テトリスを作るつもりだったが、最終的にOBJファイルビューアになった
Learn WGPUを見ながら作業した

https://github.com/yuma140902/typing

これはReverieEngineとはあまり関係がない
htmlのcanvasをバックエンドとして動作する独自のエンジンを実装している

https://github.com/yuma140902/Xanadu

これはECS (Entity Component System)のライブラリである
条件によってはBevy ECSよりも速い場合がある

yuma14yuma14

ゲームエンジンの構成要素

ゲームエンジンは様々な構成要素でできている

https://arewegameyet.rs/

Are we game yet?はゲーム開発に役立つクレートをまとめたサイトである。

いろいろなカテゴリがある。クレートの他にもRust製のゲーム、本、チュートリアルなどが紹介されている。

yuma14yuma14

既存のソフトウェア

既存のソフトウェアを知ることは重要である。主に、APIの仕様や内部実装、提供する機能などを参考にしたい。

yuma14yuma14

Bevy

BevyはRust製のゲームエンジン。ECSをベースにデザインされている。ゲームエンジンのユーザーはRustで自分のゲームを作る(このようなことをわざわざ書いているのは、ゲームエンジンの開発言語とゲームの開発言語が別である場合もあるからである。例えばGodotはC++で書かれているがユーザーはGDScript等でゲームを作る)。

https://bevy-cheatbook.github.io/introduction.html

Bevyについての情報。内部の実装についても書いてある。

Bevyの構成要素としてBevy ECSがある。Xanaduを開発するときに参考にした。

yuma14yuma14

Lyon - 2D graphics rendering on the GPU in rust using path tessellation.

https://github.com/nical/lyon

Rustで書かれたtessellatorである。雑に言うと図形や曲線をポリゴンに変換するものである。Servoにも使われているらしい。

READMEのFAQがわかりやすかったので簡単に翻訳して引用する。

  • Tessellatorとは?
    • 複雑な図形を入力すると三角形でできた形状を出力します。出力されたものはOpenGL、Vulkan、D3DなどのAPIで使用できます。
  • LyonでSVGファイルを描画するには?
    • LyonはSVGレンダラーではありません。Lyonは複雑なパスによる塗りつぶしや曲線をテッセレートするためのものです。gfx-rs, glium, OpenGL, D3DなどのGPU APIと一緒に使うと便利です。テッセレート結果をどのように描画するかはlyonのユーザー次第です。
  • tessellatorの出力をどうやって描画しますか?
    • tessellatorの出力フォーマットはカスタマイズ可能ですが、vertex bufferとindex bufferが出力されます。
  • アンチエイリアシングはサポートされていますか?
    • ビルトインのサポートはありません。しかし、ビデオゲームでよく使われるテクニックを使うことで、lyonのユーザーが自分で実現することができます(msaa, taa, fxaaなど)。

所感: Rustで書かれたレンダリングエンジンはOpenGLなどのバックエンドと密結合になっていたり、独自にウィンドウやイベントループなどを用意するものが大半だった。それに対してLyonはPathを入力すると三角形のリストを出力してくれるというシンプルな仕組みであり、自分のエンジンに組み込みやすいと感じた。

yuma14yuma14

Cyllista Game Engine

Cygamesのゲームエンジンである。スマホゲームではなくコンシューマゲームの開発に使われているらしい。エンジン本体はC++とPythonで書かれている。

https://logmi.jp/tech/articles/241512

開発において以下のような工夫がなされている。

  • テスト駆動開発
    • コードが安定する
    • エンジンのユーザー視点でAPIを吟味できる
  • CUIのビルドツールcybuild
    • 自動化しやすい
  • Runtime Compiled C++
    • ホットリロードができる

以下のような機能を持っている。

  • アセットの変換
  • レベルエディタ
  • アセットの管理・配信
yuma14yuma14

piston2d-graphics

Rust製のゲームエンジンPistonの一部である。2Dのレンダリングを行う。

注目すべき点は、OpenGL、gfx、gliumの3つのバックエンドに対応している点である。さらに、他のバックエンドに対応させることもできる。

Graphicsトレイトによって複数のバックエンドに対応している。自分でGraphicsトレイトを実装することで好きなバックエンドに対応させることもできる。

https://docs.rs/piston2d-graphics/0.44.0/graphics/trait.Graphics.html

簡略化した定義は以下の通りである。

pub trait Graphics: Sized {
	type Texture: ImageSize;

バックエンドによってテクスチャが異なるので関連型になっている。これは当然。
レンダリングにおけるテクスチャの扱いがよくわかっていないので後で調べたい。
ImageSizeはサイズを返すトレイト

fn clear_color(&mut self, color: Color);

指定した色でバッファをクリアする。背景色ということ。

fn clear_stencil(&mut self, value: u8);

stencilバッファをクリアする。stencilバッファは比較的具体的な概念だと思っていたので、複数のバックエンドを抽象化したGraphicsトレイトの定義に出てくるのは意外である。

// すべて同じ色の頂点。fはf(vertices)
fn tri_list<F>(&mut self, draw_state: &DrawState, color: &[f32; 4], f: F)
	where F: FnMut(&mut dyn FnMut(&[[f32; 2]]))
// 色付きの頂点。fはf(vertices, colors)
fn tri_list_c<F>(&mut self, draw_state: &DrawState, f: F)
	where F: FnMut(&mut dyn FnMut(&[[f32; 2]], &[[f32; 4]]))
// UV付きの頂点。fはf(vertices, texture_coords)
fn tri_list_uv<F>(&mut self, draw_state: &DrawState, color: &[f32; 4], texture: &Self::Texture, f: F)
	where F: FnMut(&mut dyn FnMut(&[[f32; 2]], &[[f32; 2]]))
// UVと色付きの頂点。fはf(vertices, texture_coords, colors)
fn tri_list_uv_c<F>(&mut self, draw_state: &DrawState, texture: &Self::Texture, f: F)
	where F: FnMut(&mut dyn FnMut(&[[f32; 2]], &[[f32; 2]], &[[f32; 4]]))

三角形の列を描画する。fはクロージャを受け取る関数である。tri_listの実装の中でf(頂点を受け取って描画するクロージャ)のようにして呼び出してあげる。

頂点の型に応じてメソッドが4種類ある。頂点に記録されうる情報には座標の他に色とUVがある。

しかしこれではバックエンド側で都度シェーダーを切り替える必要があるのでは? それからfinish_drawingみたいなメソッドが無いのが気になる。実際に描画するタイミングはいつなんだろう。

OpenGLのバックエンドの実装では

  1. draw_stateが変わったとき
  2. バッファがいっぱいになったとき
  3. fに渡したクロージャの最後

などのタイミングで描画を行っていた(gl::DrawArraysを呼び出していた)。つまりtri_list_**の呼び出しごとに実際に描画が行われるということである。したがって、呼び出す側はできるだけまとめてtri_listを呼び出すようにすべき、ということになる。

もう1つの疑問点として、クロージャやdynをたくさん使っていてパフォーマンス上の問題がないのかという点である。Pistonはいろいろなゲームで使われているので実際に問題はないのだと思うが。

DrawStateは描画時にクリッピングする領域(optional)、使用するstencil(optional)、ブレンド関数(optional)の組である。

fn rectangle(&mut self, rectangle)
fn polygon(&mut self, polygon)

ここからはデフォルト実装のあるメソッドである。パフォーマンスを向上させるためにデフォルト実装以外の実装を提供することもできる。面倒なので引数の型は適当に書いている。また、一部のみ書いている。

fn image(&mut self, image: &Image, texture: &Self::Texture)

画像を描画する。これのデフォルト実装を提供できるのは少し不思議な感じがする。実装を見たところ、内部的にrectangleを作ってそれをテクスチャとともに描画していた。考えてみれば当然である。

fn ellipse(&mut self, ellipse)

曲線を描画するにはどうするかというと、tesselationを行う。このクレートではtriangulationと呼んでいるようである。
triangulation::with_ellipse_tri_list()

Graphicsトレイトの設計は非常に参考になる。つまり、いろいろなバックエンドに共通するインターフェイスが示されているわけである。上手な抽象化だと思った。

yuma14yuma14

piston_window

piston_windowはPistonの一部で、ウィンドウの作成やイベントループの処理などを行う。

バックエンドとしてglutin、winit、SDL2、GLFWを使うことができる。どのようにして対応しているのか興味があるのであとで実装を見てみたい。→Windowトレイトを実装すれば良いらしい

以下は公式サイトに載っているサンプルコード

extern crate piston_window;

use piston_window::*;

fn main() {
    let mut window: PistonWindow = 
        WindowSettings::new("Hello Piston!", [640, 480])
        .exit_on_esc(true).build().unwrap();
    while let Some(e) = window.next() {
        window.draw_2d(&e, |c, g, _device| {
            clear([1.0; 4], g);
            rectangle([1.0, 0.0, 0.0, 1.0], // red
                      [0.0, 0.0, 100.0, 100.0],
                      c.transform, g);
        });
    }
}

windowがイテレータになっていて、next()でイベントを取り出せるようである。

yuma14yuma14

Piston

PistonはRust製のゲームエンジンである。デフォルトで提供している機能が四角形の描画、楕円の描画など素朴であるため、Pistonを使ったゲームも素朴な見た目のものが多い印象がある。

Pistonは複数の(かなりの数の)クレートから構成されている。ReverieEngineも複数クレートに分割する予定なので参考になると思う。

プロジェクト内に複数のクレートがある場合ワークスペースを使うこともできるが、Pistonでは1リポジトリ1クレートという構成になっている。

  • examples/tutorials/getting started
  • demo
    • skeletal_animation_demo - 3Dのスケルトンアニメーションの例
    • piston-music - 音楽に関するライブラリ。Pistonのcurrentというライブラリを使用している
    • hematite - Minecraft風ゲーム
  • The Piston core - 翻訳: これらのライブラリは入力、ウィンドウ、イベントループをモデル化します。プラットフォーム固有のAPIには依存しません。(プラットフォーム側の?)破壊的変更の影響を減らすためモジュール化された設計になっています。複数のプロジェクト間で使える一般的なライブラリを書き、エコシステムの90%を再利用可能かつプラットフォーム/API非依存にすることが目的です。
  • Utility libraries - 翻訳: これらのライブラリは他のライブラリとのインテグレーションをサポートしています。これらは小規模でシンプルです。ユーザが高レベルのライブラリを自由に選択できるようにし、一方でPiston内部でのインテグレーションを維持することが目的です。
    • vecmath - A simple and type agnostic Rust library for vector math designed for reexporting
    • quaternion - A simple and type agnostic Rust library for quaternion math designed for reexporting
    • dual_quaternion - A simple and type agnostic Rust library for dual-quaternion math designed for reexporting
    • piston3d-cam - A Rust library for 3D camera and navigation
    • interpolation - A library for interpolation
    • shader_version - A helper library for detecting and picking compatible shaders
    • piston-viewport - A library for storing viewport information
    • piston-float - Traits for generic floats in game development ← コンセプトとしてはnum-traitsに似ている。型をfloat、トレイトをゲームに必要なもののみに絞っている点が異なる。
    • piston-rect - Helper methods for computing simple rectangle layout
    • piston-texture - A library for texture conventions
    • current - A library for setting current values for stack scope, such as application structure
    • table - Dynamical typed Lua-like table structure
    • texture_packer - Pack small images together into larger ones
    • select_color - Color selection
    • read_color - Read hex colors
    • read_token - Read tokens using look-ahead
    • range - Range addressing
    • fps_counter - FPS counter
    • find_folder - Find a folder from current directory
    • array - Convenience methods for working with arrays ← Rust 1.63.0で追加されたstd::array::from_fnのような機能を提供する。最近のRustでは不要なライブラリだと思う
    • piston-shaders - Repository for GPU shaders
    • piston-history_tree - A persistent history tree for undo/redo
  • Bindings - Cライブラリのバインディング
  • Standalone libraries
  • Backends

piston-rect

Pistonの一部で、レイアウトのためのユーティリティである。長方形を基本として、中心を求めたり分割したりできる。

このような機能はあった方が良いし、すでにReverieEngineにも実装されている。piston-rectは機能不足であるように感じるので、自分で実装したほうがいいと思う。具体的には、長方形の中心に長方形を配置する、長方形の中にアンカー付きで長方形を配置するなどの機能がほしい。

piston-texture

Pistonの一部で、テクスチャを反転させたりフィルタをかけたりできる。テクスチャの型としては単なるfloatのスライスを使っている。

テクスチャを読み込んだりGPUに送ったりといったことは行わない。

current

Pistonの一部で、可変なグローバル変数を作れる。ただしunsafeである。

プロトタイピング、より高レベルなライブラリの作成、デバッグなどの目的で使用することが想定されている。

table

Pistonの一部で、Lua風の動的型付けのテーブルが作れる。Lua風なのでテーブルのキーとして文字列以外も使用できる。

texture_packer

Pistonの一部で、テクスチャのパッキングを行う。特定のAPIに依存していないのでReverieEngineでも使えるかもしれない。

テクスチャの型がpiston-textureよりもリッチな感じになっていて、これも参考になるかもしれない。

piston-shaders / piston-shaders_graphics2d

Pistonの一部で、2DCG用のGLSL形式のシェーダーが置いてある。WebGL、Core Profileなどのバリエーションがある。

yuma14yuma14

glyphon

https://docs.rs/glyphon/0.5.0/glyphon/

テキストの描画に関するライブラリ

  • cosmic-textでグリフのレイアウト、ラスタライズを行う
  • etagereでグリフをtexture atlasにまとめる
  • texture atlasから一部を取り出してwgpuで描画する

https://github.com/grovesNL/glyphon/blob/main/examples/hello-world.rs から引用:

use glyphon::{
    Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
    TextArea, TextAtlas, TextBounds, TextRenderer, Viewport,
};
use std::sync::Arc;
use wgpu::{
    CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Instance, InstanceDescriptor,
    LoadOp, MultisampleState, Operations, PresentMode, RenderPassColorAttachment,
    RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, TextureFormat,
    TextureUsages, TextureViewDescriptor,
};
use winit::{
    dpi::LogicalSize,
    event::{Event, WindowEvent},
    event_loop::EventLoop,
    window::WindowBuilder,
};

fn main() {
    pollster::block_on(run());
}

async fn run() {
    // Set up window
    let (width, height) = (800, 600);
    let event_loop = EventLoop::new().unwrap();
    let window = Arc::new(
        WindowBuilder::new()
            .with_inner_size(LogicalSize::new(width as f64, height as f64))
            .with_title("glyphon hello world")
            .build(&event_loop)
            .unwrap(),
    );
    let size = window.inner_size();
    let scale_factor = window.scale_factor();

    // Set up surface
    let instance = Instance::new(InstanceDescriptor::default());
    let adapter = instance
        .request_adapter(&RequestAdapterOptions::default())
        .await
        .unwrap();
    let (device, queue) = adapter
        .request_device(&DeviceDescriptor::default(), None)
        .await
        .unwrap();

    let surface = instance
        .create_surface(window.clone())
        .expect("Create surface");
    let swapchain_format = TextureFormat::Bgra8UnormSrgb;
    let mut config = SurfaceConfiguration {
        usage: TextureUsages::RENDER_ATTACHMENT,
        format: swapchain_format,
        width: size.width,
        height: size.height,
        present_mode: PresentMode::Fifo,
        alpha_mode: CompositeAlphaMode::Opaque,
        view_formats: vec![],
        desired_maximum_frame_latency: 2,
    };
    surface.configure(&device, &config);

    // Set up text renderer
    let mut font_system = FontSystem::new();
    let mut swash_cache = SwashCache::new();
    let cache = Cache::new(&device);
    let mut viewport = Viewport::new(&device, &cache);
    let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format);
    let mut text_renderer =
        TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None);
    let mut buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0));

    let physical_width = (width as f64 * scale_factor) as f32;
    let physical_height = (height as f64 * scale_factor) as f32;

    buffer.set_size(&mut font_system, physical_width, physical_height);
    buffer.set_text(&mut font_system, "Hello world! 👋\nThis is rendered with 🦅 glyphon 🦁\nThe text below should be partially clipped.\na b c d e f g h i j k l m n o p q r s t u v w x y z", Attrs::new().family(Family::SansSerif), Shaping::Advanced);
    buffer.shape_until_scroll(&mut font_system, false);

    event_loop
        .run(move |event, target| {
            if let Event::WindowEvent {
                window_id: _,
                event,
            } = event
            {
                match event {
                    WindowEvent::Resized(size) => {
                        config.width = size.width;
                        config.height = size.height;
                        surface.configure(&device, &config);
                        window.request_redraw();
                    }
                    WindowEvent::RedrawRequested => {
                        viewport.update(
                            &queue,
                            Resolution {
                                width: config.width,
                                height: config.height,
                            },
                        );

                        text_renderer
                            .prepare(
                                &device,
                                &queue,
                                &mut font_system,
                                &mut atlas,
                                &viewport,
                                [TextArea {
                                    buffer: &buffer,
                                    left: 10.0,
                                    top: 10.0,
                                    scale: 1.0,
                                    bounds: TextBounds {
                                        left: 0,
                                        top: 0,
                                        right: 600,
                                        bottom: 160,
                                    },
                                    default_color: Color::rgb(255, 255, 255),
                                }],
                                &mut swash_cache,
                            )
                            .unwrap();

                        let frame = surface.get_current_texture().unwrap();
                        let view = frame.texture.create_view(&TextureViewDescriptor::default());
                        let mut encoder = device
                            .create_command_encoder(&CommandEncoderDescriptor { label: None });
                        {
                            let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
                                label: None,
                                color_attachments: &[Some(RenderPassColorAttachment {
                                    view: &view,
                                    resolve_target: None,
                                    ops: Operations {
                                        load: LoadOp::Clear(wgpu::Color::BLACK),
                                        store: wgpu::StoreOp::Store,
                                    },
                                })],
                                depth_stencil_attachment: None,
                                timestamp_writes: None,
                                occlusion_query_set: None,
                            });

                            text_renderer.render(&atlas, &viewport, &mut pass).unwrap();
                        }

                        queue.submit(Some(encoder.finish()));
                        frame.present();

                        atlas.trim();
                    }
                    WindowEvent::CloseRequested => target.exit(),
                    _ => {}
                }
            }
        })
        .unwrap();
}
yuma14yuma14

cosmic-text

https://pop-os.github.io/cosmic-text/cosmic_text/

主にテキストのレイアウトを行うライブラリ

  • fontdbでフォントの読み込みを行う
  • rustybuzzでフォントのshapingを行う
  • フォントのフォールバックをサポートしている
  • フォントのレイアウトを行う
  • swashでフォントの描画を行う

https://pop-os.github.io/cosmic-text/cosmic_text/ から引用:

use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics, Shaping};

// A FontSystem provides access to detected system fonts, create one per application
let mut font_system = FontSystem::new();

// A SwashCache stores rasterized glyphs, create one per application
let mut swash_cache = SwashCache::new();

// Text metrics indicate the font size and line height of a buffer
let metrics = Metrics::new(14.0, 20.0);

// A Buffer provides shaping and layout for a UTF-8 string, create one per text widget
let mut buffer = Buffer::new(&mut font_system, metrics);

// Borrow buffer together with the font system for more convenient method calls
let mut buffer = buffer.borrow_with(&mut font_system);

// Set a size for the text buffer, in pixels
buffer.set_size(80.0, 25.0);

// Attributes indicate what font to choose
let attrs = Attrs::new();

// Add some text!
buffer.set_text("Hello, Rust! 🦀\n", attrs, Shaping::Advanced);

// Perform shaping as desired
buffer.shape_until_scroll(true);

// Inspect the output runs
for run in buffer.layout_runs() {
    for glyph in run.glyphs.iter() {
        println!("{:#?}", glyph);
    }
}

// Create a default text color
let text_color = Color::rgb(0xFF, 0xFF, 0xFF);

// Draw the buffer (for performance, instead use SwashCache directly)
buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| {
    // Fill in your code here for drawing rectangles
});
yuma14yuma14

zeno

https://crates.io/crates/zeno

zenoはラスタライズを行うライブラリ

https://crates.io/crates/zeno から引用:

use zeno::{Cap, Join, Mask, PathData, Stroke};

// Buffer to store the mask
let mut mask = [0u8; 64 * 64];

/// Create a mask builder with some path data
Mask::new("M 8,56 32,8 56,56 Z")
    .style(
        // Stroke style with a width of 4
        Stroke::new(4.0)
            // Round line joins
            .join(Join::Round)
            // And round line caps
            .cap(Cap::Round)
            // Dash pattern followed by a dash offset
            .dash(&[10.0, 12.0, 0.0], 0.0),
    )
    // Set the target dimensions
    .size(64, 64)
    // Render into the target buffer
    .render_into(&mut mask, None);
yuma14yuma14

wgpu_text

https://crates.io/crates/wgpu_text

wgpu_textはglyph-brushのラッパー

https://crates.io/crates/wgpu_text から引用:

use wgpu_text::{glyph_brush::{Section as TextSection, Text}, BrushBuilder, TextBrush};

let brush = BrushBuilder::using_font_bytes(font).unwrap()
 /* .initial_cache_size((16_384, 16_384))) */ // use this to avoid resizing cache texture
    .build(&device, config.width, config.height, config.format);

// Directly implemented from glyph_brush.
let section = TextSection::default().add_text(Text::new("Hello World"));

// on window resize:
        brush.resize_view(config.width as f32, config.height as f32, &queue);

// window event loop:
    winit::event::Event::RedrawRequested(_) => {
        // Before are created Encoder and frame TextureView.

        // Crashes if inner cache exceeds limits.
        brush.queue(&device, &queue, vec![&section, ...]).unwrap();

        {
            let mut rpass = encoder.begin_render_pass(...);
            brush.draw(&mut rpass);
        }

        queue.submit([encoder.finish()]);
        frame.present();
    }
yuma14yuma14

glyph-brush

https://github.com/alexheretic/glyph-brush

glyph-brushはレンダリングAPI非依存でテキストのラスタライズを行うライブラリ。キャッシュ付き。

内部でab_glyphを使っている。

https://github.com/alexheretic/glyph-brush/tree/main/glyph-brush から引用:

use glyph_brush::{ab_glyph::FontArc, BrushAction, BrushError, GlyphBrushBuilder, Section, Text};

let dejavu = FontArc::try_from_slice(include_bytes!("../../fonts/DejaVuSans.ttf"))?;
let mut glyph_brush = GlyphBrushBuilder::using_font(dejavu).build();

glyph_brush.queue(Section::default().add_text(Text::new("Hello glyph_brush")));
glyph_brush.queue(some_other_section);

match glyph_brush.process_queued(
    |rect, tex_data| update_texture(rect, tex_data),
    |vertex_data| into_vertex(vertex_data),
) {
    Ok(BrushAction::Draw(vertices)) => {
        // Draw new vertices.
    }
    Ok(BrushAction::ReDraw) => {
        // Re-draw last frame's vertices unmodified.
    }
    Err(BrushError::TextureTooSmall { suggested }) => {
        // Enlarge texture + glyph_brush texture cache and retry.
    }
}
yuma14yuma14

ab_glyph

https://github.com/alexheretic/ab-glyph

OpenTypeフォントの読み込み、スケーリング、ポジショニング、ラスタライズを行う。

ラスタライズはab_glyph_rasterizerで行う。

https://github.com/alexheretic/ab-glyph/tree/main/glyph から引用:

use ab_glyph::{FontRef, Font, Glyph, point};

let font = FontRef::try_from_slice(include_bytes!("../../dev/fonts/Exo2-Light.otf"))?;

// Get a glyph for 'q' with a scale & position.
let q_glyph: Glyph = font.glyph_id('q').with_scale_and_position(24.0, point(100.0, 0.0));

// Draw it.
if let Some(q) = font.outline_glyph(q_glyph) {
    q.draw(|x, y, c| { /* draw pixel `(x, y)` with coverage: `c` */ });
}
yuma14yuma14

ab_glyph_rasterizer

https://crates.io/crates/ab_glyph_rasterizer

ラスタライズを行う。OTFフォントの描画に向いている。

外部依存がない。

https://crates.io/crates/ab_glyph_rasterizer から引用:

let mut rasterizer = ab_glyph_rasterizer::Rasterizer::new(106, 183);

// draw a 300px 'ę' character
rasterizer.draw_cubic(point(103.0, 163.5), point(86.25, 169.25), point(77.0, 165.0), point(82.25, 151.5));
rasterizer.draw_cubic(point(82.25, 151.5), point(86.75, 139.75), point(94.0, 130.75), point(102.0, 122.0));
rasterizer.draw_line(point(102.0, 122.0), point(100.25, 111.25));
rasterizer.draw_cubic(point(100.25, 111.25), point(89.0, 112.75), point(72.75, 114.25), point(58.5, 114.25));
rasterizer.draw_cubic(point(58.5, 114.25), point(30.75, 114.25), point(18.5, 105.25), point(16.75, 72.25));
rasterizer.draw_line(point(16.75, 72.25), point(77.0, 72.25));
rasterizer.draw_cubic(point(77.0, 72.25), point(97.0, 72.25), point(105.25, 60.25), point(104.75, 38.5));
rasterizer.draw_cubic(point(104.75, 38.5), point(104.5, 13.5), point(89.0, 0.75), point(54.25, 0.75));
rasterizer.draw_cubic(point(54.25, 0.75), point(16.0, 0.75), point(0.0, 16.75), point(0.0, 64.0));
rasterizer.draw_cubic(point(0.0, 64.0), point(0.0, 110.5), point(16.0, 128.0), point(56.5, 128.0));
rasterizer.draw_cubic(point(56.5, 128.0), point(66.0, 128.0), point(79.5, 127.0), point(90.0, 125.0));
rasterizer.draw_cubic(point(90.0, 125.0), point(78.75, 135.25), point(73.25, 144.5), point(70.75, 152.0));
rasterizer.draw_cubic(point(70.75, 152.0), point(64.5, 169.0), point(75.5, 183.0), point(105.0, 170.5));
rasterizer.draw_line(point(105.0, 170.5), point(103.0, 163.5));
rasterizer.draw_cubic(point(55.0, 14.5), point(78.5, 14.5), point(88.5, 21.75), point(88.75, 38.75));
rasterizer.draw_cubic(point(88.75, 38.75), point(89.0, 50.75), point(85.75, 59.75), point(73.5, 59.75));
rasterizer.draw_line(point(73.5, 59.75), point(16.5, 59.75));
rasterizer.draw_cubic(point(16.5, 59.75), point(17.25, 25.5), point(27.0, 14.5), point(55.0, 14.5));
rasterizer.draw_line(point(55.0, 14.5), point(55.0, 14.5));

// iterate over the resultant pixel alphas, e.g. save pixel to a buffer
rasterizer.for_each_pixel(|index, alpha| {
    // ...
});