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. 02绘制 SDF 图形和阴影
  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 着色器系统。着色器通过 pixel: fn() { ... } 和 vertex: fn() { ... } 代码块内联定义在 draw_bg、draw_text 或自定义绘制对象中。

文档

详细文档请参阅本地文件:

  • ./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时间(秒)

关键:预乘 Alpha

每个像素着色器都必须返回预乘 Alpha 颜色!

// 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()
  • 在 +: 代码块中定义辅助着色器函数
  • 在现有绘制类型上设置 instance/uniform 变量
  • 在着色器函数中使用所有 SDF2D、颜色和数学内置函数

Splash 不可以:

  • 创建新的 DrawQuad/DrawText/DrawSvg 类型(必须在 Rust 中定义)
  • 为现有着色器添加新的 instance 字段(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 上表现良好(通过实例化只需一次绘制调用)
  • 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) 未预乘Alpha 混合不正确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 { }

最佳实践

  1. 始终进行预乘 - 使用 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 学到)

当半径接近尺寸的一半时,sdf.box() 配合较大的 border_radius 会出问题——公式 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 内容变化动态适配
  • 无需使用三部分并集(圆 + 矩形)——单个公式,不会产生接缝伪影
背景着色器中的脉动圆点(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))
    }
}

重要提示: 必须在 handle_next_frame 中调用 self.ui.widget(cx, ids!(my_window)).redraw(cx),以保持 draw_pass.time 持续推进。如果没有持续重绘,基于时间的动画就会冻结。

评论

支持 Markdown