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 とネイティブ です。
自分で動かす
cargo run -p shader
trunk serve --config examples/shader/Trunk.tomlソースは examples/shader/src/lib.rs、パイプラインは src/gpu.rs、シェーダは src/shader.wgsl にあります。
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>
}
}