Skip to content

A shader effect from the Shader view

A shader is a tiny program the graphics card runs for every pixel, every frame: that is how games draw glowing rings, clouds and fire without slowing down. In Pyzed you do not have to write that program. You wire nodes in the Shader view (like Godot’s visual shaders or Unreal’s material editor), and the designer turns them into GPU code for you.

Goal Time Level You need
A pulsing neon ring behind a welcome card 25 minutes Intermediate · little code Your first window
  • Create a shader from a template and find your way around the Graph, Preview and WGSL tabs.
  • Change a node’s input, wire a new node, and read the live preview on every node.
  • Use variables: values your Python code changes while the shader runs.
  • Draw the shader on a form with the Shader control, and change its variables from a button.
A welcome card over a glowing cyan ring
The finished form: the glow is a shader made of nodes, the card and buttons are normal controls.
  1. Create the shader. Ctrl+N → Shader, name it glow, pick the Glow ring template and press Create shader.

    The New shader dialog with its templates
    Templates: Blank, Plasma, Clouds, Glow ring, Cells and Retro screen.
  2. Look around. The Graph tab shows the nodes, left to right: Time and the variables feed some math, a Ring shape gives the distance to a ring, Glow turns it into light and Output is the final color. Each node draws a small preview of what it makes, so you can follow the picture as it is built.

    The Shader view: sidebar of nodes, the graph with previews and the output
    The Shader view: nodes on the left sidebar, the graph in the middle, the result in the Output window.
  3. Change a value. Inputs that are not wired show their value right on the node. On the Glow node change radius from 0.12 to 0.3: the glow grows, and the preview of every node after it updates at once. (The ring’s own radius is wired: it comes from the pulse math, which is what makes it breathe.)

  4. Wire a new node. Near the end, a Multiply node mixes the color variable with the glow and feeds the RGBA node. From the Shading sidebar drag a Hue shift node (category Color) next to them. Wire the Multiply’s result into its rgb input and its rgb output into RGBA’s rgb, then wire Time into shift: the ring now cycles through every color. Press Ctrl+Z to go back.

  5. Meet the variables. The Outline lists the shader’s variables: color (the neon color) and pulse (pulses per second). They are mutable: your app can change them every frame for free. The Preview tab has a slider for each.

    The Preview tab with a slider per variable
    Preview: the finished shader, large, with a slider per variable.
  6. Save (Ctrl+S). The shader is the file glow.pzshad. Press F5 to see it fill a window of its own.

  7. Put it behind a form. Create a form welcome (560 × 380). From Display in the toolbox drag a Shader control, make it cover the whole form and anchor it on all four sides; name it backdrop and set its Shader file to glow.pzshad. Then add a Card in the middle (no anchors, so it stays centered) with a heading, a label named hint and two buttons, faster and color. Give the card a translucent background in its Style section (#0f172acc) so the glow shows through.

  8. Drive it from Python. The control’s variables property is a dict of the shader’s variables. Double-click faster and color:

    def faster_click(form, event):
    variables = dict(form.backdrop.variables)
    variables["pulse"] = variables.get("pulse", 2.0) + 1.5
    form.backdrop.variables = variables
    def color_click(form, event):
    variables = dict(form.backdrop.variables)
    variables["color"] = "#a78bfa"
    form.backdrop.variables = variables
  9. Run the form (F5) and press the buttons.

examples/tutorial/designer/welcome.py
"""Event handlers for the welcome form (tutorial: A shader behind a form).
pyzed run examples/tutorial/designer/welcome.pzform
``backdrop`` is a Shader control that draws ``glow.pzshad``, a shader made of nodes in the
Shader view. Its ``variables`` property sets the shader's variables by name.
"""
from pathlib import Path
COLORS = ["#22d3ee", "#a78bfa", "#f472b6", "#fbbf24"]
def form_load(form, event):
"""Runs once when welcome opens: point the Shader control at the file next to this one.
(A relative path in the Shader file property is read from the folder you run the app
from; building it from ``__file__`` works from anywhere.)
"""
form.backdrop.file = str(Path(__file__).with_name("glow.pzshad"))
def faster_click(form, event):
"""Runs when faster is clicked: more pulses per second, back to 1 after 8."""
variables = dict(form.backdrop.variables)
pulse = variables.get("pulse", 2.0)
variables["pulse"] = 1.0 if pulse >= 8 else pulse + 1.5
form.backdrop.variables = variables
form.hint.text = f"{variables['pulse']:.1f} pulses per second"
def color_click(form, event):
"""Runs when color is clicked: the next neon color."""
variables = dict(form.backdrop.variables)
current = variables.get("color", COLORS[0])
variables["color"] = COLORS[(COLORS.index(current) + 1) % len(COLORS)] if current in COLORS else COLORS[1]
form.backdrop.variables = variables

The same shader in pure Python, without the designer, is one widget:

pz.stack([
pz.shader("glow.pzshad", uniforms={"pulse": 4, "color": "#a78bfa"}, width="fill", height="fill"),
card,
])
Windows, macOS and Linux
uv run --with pyzed-ui pyzed run examples/tutorial/designer/welcome.pzform
Every node shows its preview, so you see each change the moment you wire it.
Control Event Handler
The form load form_load: points the Shader control at glow.pzshad.
faster (Button) click faster_click: changes the pulse variable.
color (Button) click color_click: the next neon color.
def color_right_click(form, event): # every control has right_click: back to the first color
variables = dict(form.backdrop.variables)
variables["color"] = COLORS[0]
form.backdrop.variables = variables
def backdrop_mouse_enter(form, event): # the Shader control has the mouse events too
form.hint.text = "Move the pointer away to calm it down"
def backdrop_mouse_leave(form, event):
form.hint.text = ""