mcpField
M
makepad-2.0-shaders

makepad-2.0-shaders

github.com/zhanghandong開発75253 回インストール29 週間前に更新
リポジトリ

makepad-2.0-shaders について

Makepad 2.0でグラフィックスを開発する人向けのシェーダースキルです。Splashの構文、SDF2D描画、GPU描画パターンを扱い、RustとSplashの役割分担も説明します。

主な機能

  1. 01ピクセル・頂点シェーダー構文
  2. 02SDF2Dの図形と変換
  3. 03インスタンス変数とユニフォーム変数
  4. 04カスタム描画型の登録パターン
  5. 05シェーダーの注意点と実践方法

活用シーン

  1. 01ウィジェットの背景効果を実装
  2. 02SDF図形や影の描画
  3. 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
    }
}
変数の型
型宣言アニメーション可能スコープ
instanceinstance hover: 0.0はい(Animator 経由)ウィジェットインスタンスごと
uniformuniform color: #fffいいえインスタンス間で共有
texture_2dtexture_2d tex: noneいいえテクスチャサンプラー
varyingvarying uv: vec2いいえ頂点 → フラグメント
組み込み変数
変数型説明
self.posvec2正規化された位置(0.0~1.0)
self.rect_sizevec2ウィジェットのピクセル単位のサイズ
self.dpi_factorfloat画面の DPI 係数
self.draw_pass.timefloat秒単位の時間

重要:アルファの事前乗算

すべてのピクセルシェーダーは、アルファを事前乗算した色を返す必要があります!

// 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.0cannot assign to let binding別の名前を使用します:let x2 = ...
fn push(self, ...) -> vec2method 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 veccalc: fn(x: float) -> float { } を使用します

ベストプラクティス

  1. 必ず premultiply する - Pal.premul() を使用するか、sdf.result を返します
  2. アニメーションには instance を使用する - Animator で動作するのはインスタンス変数のみです
  3. 共有値には uniform を使用する - インスタンス間で共有する色やサイズに使用します
  4. +: マージ演算子を使用する - デフォルトのシェーダーを拡張します:draw_bg +: { ... }
  5. シェーダーはシンプルに保つ - 複雑なシェーダーはレンダリングのパフォーマンスに影響します
  6. 図形には SDF を使用する - 手動で計算するよりもずっとすっきりします
  7. new_batch: true でテストする - シェーダーとテキストを混在させる場合に必要です
  8. プロパティ形式の関数 - name: fn(args) -> type { } とし、self.name(args) で呼び出します
  9. 不変の let - シェーダーの let は再代入できません。各ステップで一意の名前を使用します
  10. 描画構造体に #[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) を呼び出す必要があります。継続的に再描画しないと、時間ベースのアニメーションは停止します。

コメント