Ir al contenido

Un grafo de shader propio

En el tutorial intermedio de shaders editaste una plantilla en la vista de shader. Aquí construyes un shader desde cero, en Python, con el mismo grafo que edita la vista de shader: una aurora, cortinas de luz verde que flotan sobre un cielo nocturno, con deslizadores para su velocidad, su altura y su color.

Objetivo Tiempo Nivel Necesitas
Una aurora animada hecha de nodos, con controles en vivo 40 minutos Avanzado · Python Un efecto de shader
  • Crear un shading.Graph, añadir nodos llamándolos y elegir sus salidas.
  • Usar aritmética sobre las salidas (+, -, *) para que se añadan nodos Math por ti.
  • Combinar ruido fractal, smoothstep y un degradado en una imagen.
  • Añadir variables mutables (uniforms) y cambiarlas en cada fotograma desde deslizadores.
  • Guardar el grafo como .pzshad y seguir editándolo visualmente en Pyzed Design.
Una aurora verde sobre un cielo oscuro con un panel de cristal con deslizadores
La aurora, con sus controles en un panel de cristal.

Un shader responde a una pregunta, para todos los píxeles a la vez: ¿de qué color es este píxel ahora mismo? Sabe dónde está el píxel (UV: de 0,0 arriba a la izquierda a 1,1 abajo a la derecha) y el tiempo. Todo lo demás son operaciones sobre esas dos cosas. El grafo es esa matemática, dibujada como cajas y cables:

flowchart LR
    UV[UV] --> P["Compose: x·3 + t, y·0.6 − t·0.3"]
    T["Time × speed"] --> P
    P --> N[Ruido fractal]
    N --> S1["Smoothstep 0.35 → 0.8"]
    UV --> B["Banda cerca de arriba (height)"]
    S1 --> L["luz = cortinas × banda"]
    B --> L
    UV --> SKY["Gradient: cielo nocturno"]
    SKY --> M["Mix cielo → tint según la luz"]
    L --> M
    M --> V["× Vignette"] --> O[Output]
  1. Un grafo y sus variables. g.uniform añade una variable mutable (la GPU la lee en cada fotograma, así que cambiarla no cuesta nada) y devuelve un nodo que la lee:

    from pyzed import shading
    g = shading.Graph("aurora")
    speed = g.uniform("speed", "float", 0.3, min=0, max=2)
    height = g.uniform("height", "float", 0.55, min=0.2, max=0.9)
    tint = g.uniform("tint", "color", "#34d399")
  2. Entradas. Llamar a un tipo de nodo sobre el grafo añade ese nodo. node["nombre"] elige una salida, y la aritmética añade nodos Math:

    uv = g.uv()
    t = g.time()["time"] * speed # un nodo Multiply
  3. Las cortinas. Estira las coordenadas a lo ancho y muévelas con el tiempo, y luego toma ruido fractal (capas de ruido, cada una más fina que la anterior; el aspecto clásico de nubes y humo):

    p = g.compose(uv["x"] * 3.0 + t, uv["y"] * 0.6 - t * 0.3)["vector"]
    curtains = g.fbm(p, scale=2.5, octaves=5)
  4. Mantenlas en una banda. smoothstep(a, b, x) vale 0 por debajo de a, 1 por encima de b y sube suave entre medio: la versión de shader de “aparecer gradualmente”. Dos de ellos forman una banda cerca de arriba, controlada por height:

    band = g.smoothstep(height - 0.35, height, 1.0 - uv["y"]) * g.smoothstep(1.0, height, 1.0 - uv["y"])
    light = g.smoothstep(0.35, 0.8, curtains["value"]) * band
  5. Dale color. Un nodo Gradient hace el cielo; Mix pasa del cielo al tinte donde hay luz; Vignette oscurece las esquinas; Output es el color final:

    sky = g.gradient(uv["y"], stops=[[0.0, "#020617"], [1.0, "#0f172a"]])
    glow = g.mix(sky["rgb"], tint * 1.4, light)
    g.output(g.rgba(glow * g.vignette(strength=0.6), 1.0)["color"])
    g.arrange() # ordena los nodos en columnas, como el botón Arrange de la vista de shader
  6. Muéstralo. pz.shader recibe el grafo, lo compila a WGSL una vez y lo ejecuta en la GPU. uniforms= fija las variables mutables por nombre, así que los deslizadores cambian la imagen sin recompilar nada:

    pz.shader(AURORA, uniforms={"speed": self.speed, "height": self.height, "tint": TINTS[self.tint]},
    width="fill", height="fill", fallback=pz.Gradient(["#020617", "#064e3b"], angle=180))
  7. Ábrelo en el diseñador. Ejecuta el ejemplo con --save para escribir examples/tutorial/shaders/aurora.pzshad, y luego pyzed design examples/tutorial/shaders/aurora.pzshad: cada nodo que creaste en código está en el lienzo, con su vista previa en vivo, listo para ajustarlo con el mouse.

examples/tutorial/aurora_graph.py
"""A custom shader graph built in Python: an aurora with sliders for its variables.
python examples/tutorial/aurora_graph.py # show it
python examples/tutorial/aurora_graph.py --save # also write shaders/aurora.pzshad
- ``shading.Graph`` is the same graph the Shader view of Pyzed Design edits: calling a node type
on it (``g.fbm(...)``, ``g.smoothstep(...)``) adds that node, ``node["output"]`` picks an output,
and ``+``, ``-``, ``*`` and ``/`` on outputs add Math nodes.
- ``g.uniform(...)`` adds a *mutable* variable: the sliders change it every frame without
recompiling anything.
- ``pz.shader(graph, uniforms=...)`` compiles the graph to WGSL and runs it on the GPU. Without a
GPU it draws the ``fallback`` gradient.
- ``--save`` writes the graph as a ``.pzshad`` file you can open in Pyzed Design to keep editing.
Walkthrough: https://albakore.github.io/pyzed/tutorials/advanced/shader-graph/
"""
import sys
from pathlib import Path
import pyzed as pz
from pyzed import shading, ui
HERE = Path(__file__).resolve().parent
TINTS = {"Green": "#34d399", "Violet": "#a78bfa", "Blue": "#38bdf8"}
def build_aurora() -> shading.Graph:
g = shading.Graph("aurora")
g.description = "Curtains of light drifting over a night sky."
# Variables: the knobs your Python code (and the sliders) can turn.
speed = g.uniform("speed", "float", 0.3, min=0, max=2)
height = g.uniform("height", "float", 0.55, min=0.2, max=0.9)
tint = g.uniform("tint", "color", "#34d399")
# 1. Where is this pixel, and what time is it?
uv = g.uv()
t = g.time()["time"] * speed
# 2. Drifting fractal noise, stretched sideways: the curtains.
p = g.compose(uv["x"] * 3.0 + t, uv["y"] * 0.6 - t * 0.3)["vector"]
curtains = g.fbm(p, scale=2.5, octaves=5)
# 3. Keep them in a band near the top, fading toward the ground.
band = g.smoothstep(height - 0.35, height, 1.0 - uv["y"]) * g.smoothstep(1.0, height, 1.0 - uv["y"])
light = g.smoothstep(0.35, 0.8, curtains["value"]) * band
# 4. Colors: a night sky gradient, plus the tint where there is light.
sky = g.gradient(uv["y"], stops=[[0.0, "#020617"], [1.0, "#0f172a"]])
glow = g.mix(sky["rgb"], tint * 1.4, light)
g.output(g.rgba(glow * g.vignette(strength=0.6), 1.0)["color"])
g.arrange() # lay the nodes out in columns, as the Arrange button of the Shader view does
return g
AURORA = build_aurora()
class Aurora(pz.App):
title = "Aurora"
size = (760, 480)
theme = "One Dark"
def __init__(self):
self.speed = 0.3
self.height = 0.55
self.tint = "Green"
def knob(self, label, value, low, high, field):
return ui.field(label, pz.container(ui.slider(value, lambda v: setattr(self, field, v), min=low, max=high),
width=180))
def view(self):
sky = pz.shader(AURORA, uniforms={"speed": self.speed, "height": self.height, "tint": TINTS[self.tint]},
width="fill", height="fill", fallback=pz.Gradient(["#020617", "#064e3b"], angle=180))
panel = pz.container(pz.column([
ui.label("Aurora", weight="semibold"),
self.knob("Speed", self.speed, 0, 2, "speed"),
self.knob("Height", self.height, 0.2, 0.9, "height"),
ui.field("Tint", ui.segmented(list(TINTS), self.tint,
lambda c: setattr(self, "tint", c))),
], spacing=8), padding=14).style(glass=True, radius=12)
return pz.stack([sky, pz.container(panel, padding=16, width="fill", height="fill",
align_x="start", align_y="end")])
if __name__ == "__main__":
if "--save" in sys.argv:
print("wrote", AURORA.save(HERE / "shaders" / "aurora.pzshad"))
pz.run(Aurora)
Windows, macOS y Linux
uv run --with pyzed-ui python examples/tutorial/aurora_graph.py