makepad-2.0-shaders について
Makepad 2.0でグラフィックスを開発する人向けのシェーダースキルです。Splashの構文、SDF2D描画、GPU描画パターンを扱い、RustとSplashの役割分担も説明します。
主な機能
- 01ピクセル・頂点シェーダー構文
- 02SDF2Dの図形と変換
- 03インスタンス変数とユニフォーム変数
- 04カスタム描画型の登録パターン
- 05シェーダーの注意点と実践方法
活用シーン
- 01ウィジェットの背景効果を実装
- 02SDF図形や影の描画
- 03フルスクリーンシェーダーやパーティクル効果の作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zhanghandong/ makepad-skills --skill makepad-2.0-shadersインストール後、Claude Code で/makepad-2.0-shadersを入力して読み込まれたか確認します。
同梱ファイルreferences/sdf2d-reference.mdreferences/shader-reference.md
詳細
Makepad 2.0 シェーダースキル
バージョン: makepad-widgets(dev ブランチ) | 最終更新: 2026-03-03
概要
Makepad では、ウィジェットのプロパティツリーに統合されたカスタム GPU シェーダーシステムを使用します。シェーダーは、draw_bg、draw_text、またはカスタム描画オブジェクト内の pixel: fn() { ... } および vertex: fn() { ... } ブロックとしてインラインで定義します。
ドキュメント
詳細については、ローカルファイルを参照してください。
./references/shader-reference.md- シェーダー構文、変数、組み込み機能、カスタム関数./references/sdf2d-reference.md- SDF2D プリミティブ、コンビネーター、描画操作
シェーダーの基本
ピクセルシェーダーの構造
draw_bg +: {
// Declare variables
instance hover: 0.0 // Animatable per-instance
uniform accent: #4488ff // Shared across all instances
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
// ... SDF operations ...
return sdf.result
}
}
変数の型
| 型 | 宣言 | アニメーション可能 | スコープ |
|---|---|---|---|
instance | instance hover: 0.0 | はい(Animator 経由) | ウィジェットインスタンスごと |
uniform | uniform color: #fff | いいえ | インスタンス間で共有 |
texture_2d | texture_2d tex: none | いいえ | テクスチャサンプラー |
varying | varying uv: vec2 | いいえ | 頂点 → フラグメント |
組み込み変数
| 変数 | 型 | 説明 |
|---|---|---|
self.pos | vec2 | 正規化された位置(0.0~1.0) |
self.rect_size | vec2 | ウィジェットのピクセル単位のサイズ |
self.dpi_factor | float | 画面の DPI 係数 |
self.draw_pass.time | float | 秒単位の時間 |
重要:アルファの事前乗算
すべてのピクセルシェーダーは、アルファを事前乗算した色を返す必要があります!
// WRONG - non-premultiplied
pixel: fn() {
return vec4(1.0, 0.0, 0.0, 0.5)
}
// CORRECT - use Pal.premul()
pixel: fn() {
return Pal.premul(vec4(1.0, 0.0, 0.0, 0.5))
}
// ALSO CORRECT - sdf.result is already premultiplied
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
sdf.circle(cx, cy, r)
sdf.fill(#f00)
return sdf.result
}
SDF2D クイックリファレンス
セットアップ
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
プリミティブ
sdf.circle(cx, cy, radius)
sdf.rect(x, y, w, h)
sdf.box(x, y, w, h, border_radius)
sdf.hexagon(cx, cy, radius)
sdf.arc(cx, cy, radius, start_angle, end_angle, thickness)
sdf.move_to(x, y)
sdf.line_to(x, y)
sdf.close_path()
描画
sdf.fill(color) // Filled shape
sdf.stroke(color, width) // Outlined shape
sdf.glow(color, amount) // Glow effect
sdf.clear(color) // Clear with color
コンビネーター
sdf.union() // Add shapes together
sdf.intersect() // Keep overlap only
sdf.subtract() // Remove second from first
sdf.gloop(radius) // Smooth union
sdf.blend(amount) // Linear blend
変換
sdf.translate(x, y)
sdf.rotate(angle, cx, cy)
sdf.scale(factor, cx, cy)
色の操作
// Mix two colors
mix(#f00, #00f, 0.5) // 50% blend
// Premultiply alpha
Pal.premul(vec4(r, g, b, a))
// HSV conversions
Pal.hsv2rgb(vec4(h, s, v, 1.0))
Pal.rgb2hsv(color)
// Random
Math.random_2d(vec2(x, y))
よく使われるシェーダーパターン
グラデーション背景
draw_bg +: {
pixel: fn() {
let grad = mix(#1a1a2e, #16213e, self.pos.y)
return Pal.premul(vec4(grad.xyz, 1.0))
}
}
ホバー時の色変更
draw_bg +: {
instance hover: 0.0
color: #333
pixel: fn() {
return Pal.premul(mix(self.color, self.color * 1.3, self.hover))
}
}
ボックスシャドウ
draw_bg +: {
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
// Shadow
sdf.box(2.0, 2.0, self.rect_size.x - 4.0, self.rect_size.y - 4.0, 8.0)
sdf.fill(GaussShadow.box_shadow(sdf, 4.0, #0005))
// Card
sdf.box(0.0, 0.0, self.rect_size.x - 2.0, self.rect_size.y - 2.0, 8.0)
sdf.fill(#2a2a3d)
return sdf.result
}
}
状態に応じた角丸ボタン
draw_bg +: {
instance hover: 0.0
instance down: 0.0
uniform color_bg: #4488ff
uniform color_hover: #5599ff
uniform color_down: #3377ee
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
sdf.box(0.0, 0.0, self.rect_size.x, self.rect_size.y, 6.0)
let color = mix(self.color_bg, self.color_hover, self.hover)
let color = mix(color, self.color_down, self.down)
sdf.fill(color)
return sdf.result
}
}
カスタムシェーダー関数
draw_bg +: {
fn wave(pos: vec2, time: float) -> float {
return sin(pos.x * 10.0 + time * 3.0) * 0.1
}
pixel: fn() {
let w = self.wave(self.pos, self.draw_pass.time)
let color = mix(#1a1a2e, #4488ff, self.pos.y + w)
return Pal.premul(vec4(color.xyz, 1.0))
}
}
Splash シェーダーの機能境界
Splash でできること:
+:を使って、既存の描画型のpixel: fn()、vertex: fn()、get_color: fn()をオーバーライドする+:ブロック内にヘルパーシェーダー関数を定義する- 既存の描画型のインスタンス変数/ユニフォーム変数を設定する
- シェーダー関数内ですべての SDF2D、色、数学の組み込み機能を使用する
Splash でできないこと:
- 新しい DrawQuad/DrawText/DrawSvg 型を作成する(Rust で定義する必要があります)
- 既存のシェーダーに新しいインスタンスフィールドを追加する(GPU レイアウトはコンパイル時に決まります)
ルール: Rust で描画型の構造体を定義して登録し、Splash で描画方法をオーバーライドします。
パターン全体については、./references/shader-reference.md の「Splash Shader Capabilities & Boundaries」を参照してください。
カスタム全画面シェーダーパターン(2026-03-26 に学習)
独立したシェーダー駆動型ウィジェット(パーティクルフィールド、ビジュアライザーなど)には、examples/shader のパターンに従ってください。
1. カスタム描画型(Rust)
#[derive(Script, ScriptHook)]
#[repr(C)] // CRITICAL: must be repr(C) for GPU layout
pub struct DrawMyShader {
#[deref] draw_super: DrawQuad, // inherits from DrawQuad
#[live] my_param: f32, // maps to shader variable
}
2. シェーダーの登録と定義(script_mod!)
set_type_default() do #(DrawMyShader::script_shader(vm)){
..mod.draw.DrawQuad // inherit DrawQuad defaults
my_param: 0.5 // default value
// Custom functions: property-style syntax, NOT fn name(self, ...)
my_helper: fn(a: float, b: float) -> vec2 {
return vec2(a * 2.0, b * 0.5)
}
pixel: fn() {
let result = self.my_helper(self.pos.x, self.pos.y)
return Pal.premul(vec4(result.x, result.y, 0.0, 1.0))
}
}
3. Turtle レイアウトを使ったウィジェット
fn draw_walk(&mut self, cx: &mut Cx2d, _: &mut Scope, walk: Walk) -> DrawStep {
cx.begin_turtle(walk, self.layout);
let rect = cx.turtle().rect();
self.draw_bg.draw_abs(cx, rect); // single fullscreen quad
cx.end_turtle_with_area(&mut self.area);
DrawStep::done()
}
4. Rust からのシェーダー変数の更新
// Direct field access (when draw type has #[live] fields):
self.draw_bg.my_param = 0.75;
self.area.redraw(cx);
// Via NextFrame for animation:
if let Event::NextFrame(ne) = event {
if ne.set.contains(&self.next_frame) {
self.draw_bg.my_param += 0.01;
self.area.redraw(cx);
self.next_frame = cx.new_next_frame();
}
}
インスタンス化パーティクルの描画(2026-03-26 に学習)
多数の独立したパーティクル(ドット、星など)を描画する場合:
描画シェーダー
#[derive(Script, ScriptHook)]
#[repr(C)]
pub struct DrawDot {
#[deref] draw_super: DrawQuad,
#[live] dot_color: Vec3, // per-instance color
}
// Shader: each instance is a small circle
pixel: fn() {
let d = length(self.pos - vec2(0.5, 0.5))
let alpha = 1.0 - smoothstep(0.35, 0.5, d)
return Pal.premul(vec4(self.dot_color * alpha, alpha))
}
描画ループ
self.draw_dot.begin_many_instances(cx); // start batch
for i in 0..particles.len() {
let (x, y) = particles[i];
self.draw_dot.dot_color = vec3(r, g, b); // set per-instance data
self.draw_dot.draw_abs(cx, Rect {
pos: dvec2(x - radius, y - radius),
size: dvec2(radius * 2.0, radius * 2.0),
});
}
self.draw_dot.end_many_instances(cx); // submit batch as one draw call
物理パターン(ばねのように戻る変位)
// Per particle: store persistent displacement
displacements: Vec<(f64, f64)>,
// Each frame:
for i in 0..dots.len() {
let (mut dx, mut dy) = displacements[i];
// 1. Decay (spring back, 0.94 = ~2-3 sec return)
dx *= 0.94;
dy *= 0.94;
// 2. Apply forces (cursor push, ripples, etc.)
let dist = distance(dot_pos, mouse_pos);
let t = (1.0 - dist / radius).max(0.0);
let push = t * t * t * strength; // cubic falloff
dx += direction.x * push;
dy += direction.y * push;
displacements[i] = (dx, dy);
// Draw at original_pos + displacement
}
パフォーマンスに関する注意
- 60fps で 10,000 個のパーティクル:macOS Metal では問題ありません(インスタンス化によるドローコール 1 回)
- CPU 物理ループ:10K × 17 回の距離チェック = 約 170K 演算/フレームで、無視できる程度です
- 重要な点:
begin_many_instances/end_many_instancesにより、単一の GPU ドローコールにまとめられます
シェーダー構文の落とし穴(2026-03-26 に学習)
| 落とし穴 | エラー | 修正方法 |
|---|---|---|
let x = 1.0; x = 2.0 | cannot assign to let binding | 別の名前を使用します:let x2 = ... |
fn push(self, ...) -> vec2 | method not found on self | プロパティ構文を使用します:push: fn(...) -> vec2 { } |
return vec4(r, g, b, a) を premul なしで使用 | アルファブレンドが正しくありません | return Pal.premul(vec4(r, g, b, a)) |
| Splash eval でのカスタムシェーダー | 何も表示されないままレンダリングされます | コンパイル済みの script_mod! パスを使用する必要があります |
描画構造体に #[repr(C)] がない | GPU レイアウトが一致しません | 常に #[repr(C)] を追加します |
fn calc(self, x: float) 構文 | cannot push to frozen vec | calc: fn(x: float) -> float { } を使用します |
ベストプラクティス
- 必ず premultiply する -
Pal.premul()を使用するか、sdf.resultを返します - アニメーションには
instanceを使用する - Animator で動作するのはインスタンス変数のみです - 共有値には
uniformを使用する - インスタンス間で共有する色やサイズに使用します +:マージ演算子を使用する - デフォルトのシェーダーを拡張します:draw_bg +: { ... }- シェーダーはシンプルに保つ - 複雑なシェーダーはレンダリングのパフォーマンスに影響します
- 図形には SDF を使用する - 手動で計算するよりもずっとすっきりします
new_batch: trueでテストする - シェーダーとテキストを混在させる場合に必要です- プロパティ形式の関数 -
name: fn(args) -> type { }とし、self.name(args)で呼び出します - 不変の let - シェーダーの
letは再代入できません。各ステップで一意の名前を使用します - 描画構造体に
#[repr(C)]を指定する - GPU メモリレイアウトのアラインメントに必要です
SDF カプセル(ピル)形状のパターン(2026-03-31 に学習)
border_radius が大きい sdf.box() は、半径が寸法の半分に近づくと正しく動作しません。式 size.xy - vec2(2*r, 2*r) が負になり、ひし形やトゲ状の形状が生成されます。代わりに、標準的な次のカプセル SDF を使用します:
draw_bg +: {
pixel: fn() {
let w = self.rect_size.x
let h = self.rect_size.y
let r = h * 0.5
let px = self.pos.x * w
let py = self.pos.y * h
// Standard capsule: clamp x to center segment, then circle distance
let cx = clamp(px, r, max(r, w - r))
let cy = h * 0.5
let d = length(vec2(px - cx, py - cy)) - r
let alpha = 1.0 - smoothstep(-1.0, 1.0, d)
return Pal.premul(vec4(0.1, 0.1, 0.18, alpha * 0.82))
}
}
主なポイント:
clamp(px, r, w-r)は、x を両端の円の間にある中心線分に制限しますmax(r, w-r)は、ウィジェットの幅が非常に狭い場合に clamp 範囲が逆転するのを防ぎます- Smoothstep
(-1.0, 1.0, d)により、2px のアンチエイリアスが得られます - あらゆる幅で正しく動作します —
width: Fitのコンテンツが変化すると動的に適応します - 3 部分の和集合(円 + 矩形)は不要です — 単一の式で、継ぎ目のアーティファクトもありません
背景シェーダー内の脈動するドット(2026-03-31 に学習)
子ウィジェットによる z オーダーの問題を避けるため、アニメーションを背景シェーダーに直接組み込みます(LoadingSpinner などのウィジェットによって、カプセルの端に色がにじむことがあります):
draw_bg +: {
pixel: fn() {
let w = self.rect_size.x
let h = self.rect_size.y
let r = h * 0.5
let px = self.pos.x * w
let py = self.pos.y * h
// Capsule background
let cx_bg = clamp(px, r, max(r, w - r))
let cy = h * 0.5
let d_bg = length(vec2(px - cx_bg, py - cy)) - r
let bg_alpha = 1.0 - smoothstep(-1.0, 1.0, d_bg)
let bg = vec4(0.1, 0.1, 0.18, bg_alpha * 0.82)
// Pulsing dot (driven by draw_pass.time)
let t = self.draw_pass.time
let pulse = 0.5 + 0.5 * sin(t * 4.0)
let dot_r = 4.0 + pulse * 3.0
let dot_cx = r + 2.0
let d_dot = length(vec2(px - dot_cx, py - cy)) - dot_r
let dot_alpha = (1.0 - smoothstep(-1.0, 1.0, d_dot)) * bg_alpha
let dot_color = mix(vec3(0.3, 0.6, 1.0), vec3(0.2, 0.9, 0.5), pulse)
// Composite
let final_rgb = mix(bg.xyz, dot_color, dot_alpha * 0.8)
let final_a = bg.w + dot_alpha * 0.6 * (1.0 - bg.w)
return Pal.premul(vec4(final_rgb, final_a))
}
}
重要: draw_pass.time を進め続けるには、handle_next_frame から self.ui.widget(cx, ids!(my_window)).redraw(cx) を呼び出す必要があります。継続的に再描画しないと、時間ベースのアニメーションは停止します。