| name | makepad-2.0-shaders |
| description | CRITICAL: Use for Makepad 2.0 shader system. Triggers on:
makepad shader, Sdf2d, pixel shader, draw_bg, draw_text, draw_quad,
makepad gpu, shader function, pixel fn, vertex fn, instance, uniform,
shader variable, sdf, premultiply, Pal.premul, GaussShadow,
makepad graphics, custom draw, DrawQuad, DrawVector,
着色器, 像素, 渲染, 自定义绘制, 距离场
|
Makepad 2.0 Shader Skill
Version: makepad-widgets (dev branch) | Last Updated: 2026-03-03
Overview
Makepad uses a custom GPU shader system integrated into the widget property tree. Shaders are defined inline using pixel: fn() { ... } and vertex: fn() { ... } blocks within draw_bg, draw_text, or custom draw objects.
Documentation
Refer to the local files for detailed documentation:
./references/shader-reference.md - Shader syntax, variables, built-ins, custom functions
./references/sdf2d-reference.md - SDF2D primitives, combinators, drawing operations
Shader Basics
Pixel Shader Structure
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
}
}
Variable Types
| Type | Declaration | Animatable | Scope |
|---|
instance | instance hover: 0.0 | Yes (via Animator) | Per-widget instance |
uniform | uniform color: #fff | No | Shared across instances |
texture_2d | texture_2d tex: none | No | Texture sampler |
varying | varying uv: vec2 | No | Vertex → fragment |
Built-in Variables
| Variable | Type | Description |
|---|
self.pos | vec2 | Normalized position (0.0 to 1.0) |
self.rect_size | vec2 | Widget size in pixels |
self.dpi_factor | float | Screen DPI factor |
self.draw_pass.time | float | Time in seconds |
CRITICAL: Premultiply Alpha
Every pixel shader MUST return premultiplied alpha color!
// 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 Quick Reference
Setup
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
Primitives
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()
Drawing
sdf.fill(color) // Filled shape
sdf.stroke(color, width) // Outlined shape
sdf.glow(color, amount) // Glow effect
sdf.clear(color) // Clear with color
Combinators
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
Transforms
sdf.translate(x, y)
sdf.rotate(angle, cx, cy)
sdf.scale(factor, cx, cy)
Color Operations
// 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))
Common Shader Patterns
Gradient Background
draw_bg +: {
pixel: fn() {
let grad = mix(#1a1a2e, #16213e, self.pos.y)
return Pal.premul(vec4(grad.xyz, 1.0))
}
}
Hover Color Change
draw_bg +: {
instance hover: 0.0
color: #333
pixel: fn() {
return Pal.premul(mix(self.color, self.color * 1.3, self.hover))
}
}
Box Shadow
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
}
}
Rounded Button with States
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
}
}
Custom Shader Functions
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 Shader Capability Boundary
Splash CAN:
- Override
pixel: fn(), vertex: fn(), get_color: fn() on existing draw types via +:
- Define helper shader functions within
+: blocks
- Set instance/uniform variables on existing draw types
- Use all SDF2D, color, math built-ins in shader functions
Splash CANNOT:
- Create new DrawQuad/DrawText/DrawSvg types (must define in Rust)
- Add new instance fields to existing shaders (GPU layout is compile-time)
Rule: Rust defines the draw type struct + registers it; Splash overrides how it draws.
See ./references/shader-reference.md "Splash Shader Capabilities & Boundaries" for the full pattern.
Custom Fullscreen Shader Pattern (learned 2026-03-26)
For standalone shader-driven widgets (e.g. particle fields, visualizers), follow the examples/shader pattern:
1. Custom Draw Type (Rust)
#[derive(Script, ScriptHook)]
#[repr(C)]
pub struct DrawMyShader {
#[deref] draw_super: DrawQuad,
#[live] my_param: f32,
}
2. Register + Define Shader (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. Widget with Turtle Layout
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);
cx.end_turtle_with_area(&mut self.area);
DrawStep::done()
}
4. Updating Shader Variables from Rust
self.draw_bg.my_param = 0.75;
self.area.redraw(cx);
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();
}
}
Instanced Particle Rendering (learned 2026-03-26)
For drawing thousands of independent particles (dots, stars, etc.):
Draw Shader
#[derive(Script, ScriptHook)]
#[repr(C)]
pub struct DrawDot {
#[deref] draw_super: DrawQuad,
#[live] dot_color: Vec3,
}
// 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))
}
Rendering Loop
self.draw_dot.begin_many_instances(cx);
for i in 0..particles.len() {
let (x, y) = particles[i];
self.draw_dot.dot_color = vec3(r, g, b);
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);
Physics Pattern (spring-back displacement)
displacements: Vec<(f64, f64)>,
for i in 0..dots.len() {
let (mut dx, mut dy) = displacements[i];
dx *= 0.94;
dy *= 0.94;
let dist = distance(dot_pos, mouse_pos);
let t = (1.0 - dist / radius).max(0.0);
let push = t * t * t * strength;
dx += direction.x * push;
dy += direction.y * push;
displacements[i] = (dx, dy);
}
Performance Notes
- 10,000 particles at 60fps: OK on macOS Metal (one draw call via instancing)
- CPU physics loop: 10K × 17 distance checks = ~170K ops/frame, negligible
- Key:
begin_many_instances / end_many_instances batches into single GPU draw call