Skip to content

shader

<Canvas> の中でレイトレースしたブラックホール。スライダーが uniform につながる。

3 つの部品がここで出会い、それぞれ自分の側にとどまっています。パイプラインは起動時に Options::setup が 1 回だけ組み立てます。eframe はそこに、wgpu のレンダーステートを含む CreationContext を渡します。リソースは専用の型のもとでcallback_resources に入るので、型で見つけ直せますし、ほかのアプリのものとぶつかりません。<Canvas> 要素はそれを何も知りません。レイアウトに矩形を求め、それを持って paint を呼ぶだけです。そして中身は、egui_wgpu::Callback をペインタに積む 1 行です。shader.wgsl の中身は、その上のRust からは見えません。

要点は状態です。speed、mass、bloom、tilt、paused は、ほかと同じuse_state です。値はコールバックの構造体にコピーされ、uniform バッファに書かれ、WGSL が読みます。スライダーを動かすとフックの中の数値が変わり、絵が変わります。その間にあるものは、何も知らされる必要がありません。まず mass を触ってみてください。シェーダが光子をその周りでトレースしているシュワルツシルト半径です。

ブラウザでは、対応していれば WebGPU で動き、そうでなければ WebGL に落ちます。どちらも eframe の既定のままで、設定するものはありません。setup という穴を文脈つきで説明したものが Web とネイティブ です。

自分で動かす ​

sh
cargo run -p shader
trunk serve --config examples/shader/Trunk.toml

ソースは examples/shader/src/lib.rs、パイプラインは src/gpu.rs、シェーダは src/shader.wgsl にあります。

rust
use egui_reactor::prelude::*;
use egui_reactor_elements::prelude::*;

pub mod gpu;

/// The shader canvas, four sliders and a pause box.
#[component]
pub fn App(cx: &mut Cx) {
    let mut speed = use_state(cx, || 1.0f32);
    let mut mass = use_state(cx, || 1.0f32);
    let mut bloom = use_state(cx, || 1.5f32);
    // Degrees, because that is what a slider labelled "tilt" should show; the
    // shader gets radians.
    let mut tilt = use_state(cx, || 22.0f32);
    let mut paused = use_state(cx, || false);
    let mut mouse = use_state(cx, || egui::Vec2::ZERO);

    // egui's own clock, in seconds since the app started. It stops moving when
    // no repaint is asked for, which is what makes `pause` work.
    let time = cx.ui().input(|i| i.time) as f32;
    // A shader animates, so it needs a frame a frame; asking here rather than
    // in `paint` keeps the request out of the drawing code.
    if !*paused {
        cx.ctx().request_repaint();
    }

    // Read the states once. The handlers below take them `&mut`, and `paint`
    // is a separate closure that would otherwise hold a borrow across them.
    let speed_value = *speed;
    let mass_value = *mass;
    let bloom_value = *bloom;
    let tilt_value = tilt.to_radians();
    let mouse_value = *mouse;
    let points_to_pixels = cx.ctx().pixels_per_point();

    rsx! {
        <View direction="column" gap={8} p={12} grow={1.0}>
            <Text size={22.0} strong>"shader"</Text>
            <Text>"Drag the canvas to orbit the camera."</Text>
            // `h={0}` with `grow={1}` is the flexbox idiom for "take what is
            // left and nothing more". A `<Canvas>` reports the whole window as
            // the size it could fill, and this column's height is decided by
            // its content, so `grow` on its own would size the column to the
            // canvas *plus* the controls and push the row below off the
            // bottom. Zero height and a share of the free space instead.
            <Canvas
                grow={1.0}
                h={0.0}
                sense={egui::Sense::drag()}
                on_drag={|delta: egui::Vec2| *mouse += delta}
                paint={move |ui: &mut egui::Ui, rect: egui::Rect| {
                    // The shader works in pixels, and a rect is in points.
                    let resolution = rect.size() * points_to_pixels;
                    ui.painter().add(egui_wgpu::Callback::new_paint_callback(
                        rect,
                        gpu::ShaderCallback {
                            time: time * speed_value,
                            mass: mass_value,
                            bloom: bloom_value,
                            tilt: tilt_value,
                            resolution,
                            mouse: mouse_value * points_to_pixels,
                        },
                    ));
                }}
            />
            <View direction="row" gap={12} align="center" wrap>
                <Slider bind={mass.bind()} range={0.1..=2.0} label="mass"/>
                <Slider bind={bloom.bind()} range={0.0..=4.0} label="bloom"/>
                <Slider bind={tilt.bind()} range={-60.0..=60.0} label="tilt"/>
                <Slider bind={speed.bind()} range={0.0..=4.0} label="speed"/>
                <Checkbox bind={paused.bind()} label="pause"/>
            </View>
        </View>
    }
}

MIT または Apache-2.0 ライセンスで公開しています。