Using Eval* nodesΒΆ

Eval* nodes are useful to evaluate various expressions during the graph evaluation. The complete list of operators, constants and functions can be consulted in the Eval reference documentation.

Each node can be nested to create complex interwined relationships, and they are not limited to float and vectors: Noise* or Color* nodes are also accepted as input resources.

For example, the following example demonstrates how to animate the position of the 2 gradient positions:

Animate a gradient and objects using CPU evaluation
import pynopegl as ngl

def gradient(cfg: ngl.SceneCfg, mode="ramp"):
    cfg.duration = 15

    # Create live-controls for the color of each point
    c0_node = ngl.UniformColor(value=(1, 0.5, 0.5), live_id="c0")
    c1_node = ngl.UniformColor(value=(0.5, 1, 0.5), live_id="c1")

    # Make their positions change according to the time
    pos_res = dict(t=ngl.Time())
    pos0 = ngl.EvalVec3("sin( 0.307*t - 0.190)", "sin( 0.703*t - 0.957)", "0", resources=pos_res)
    pos1 = ngl.EvalVec3("sin(-0.236*t + 0.218)", "sin(-0.851*t - 0.904)", "0", resources=pos_res)

    # Represent them with a circle of the "opposite" color (roughly)
    # The scales are here to honor the aspect ratio to prevent circle stretching
    pt0_color = ngl.EvalVec3("1-c.r", "1-c.g", "1-c.b", resources=dict(c=c0_node))
    pt1_color = ngl.EvalVec3("1-c.r", "1-c.g", "1-c.b", resources=dict(c=c1_node))
    geom = ngl.Circle(radius=0.05, npoints=16)
    p0 = ngl.RenderColor(color=pt0_color, geometry=geom)
    p1 = ngl.RenderColor(color=pt1_color, geometry=geom)
    p0 = ngl.Scale(p0, factors=(1 / cfg.aspect_ratio_float, 1, 1))
    p1 = ngl.Scale(p1, factors=(1 / cfg.aspect_ratio_float, 1, 1))
    p0 = ngl.Translate(p0, vector=pos0)
    p1 = ngl.Translate(p1, vector=pos1)

    # Convert the position to 2D points to make them usable in RenderGradient
    pos0_2d = ngl.EvalVec2("p.x/2+.5", ".5-p.y/2", resources=dict(p=pos0))
    pos1_2d = ngl.EvalVec2("p.x/2+.5", ".5-p.y/2", resources=dict(p=pos1))
    grad = ngl.RenderGradient(pos0=pos0_2d, pos1=pos1_2d, mode=mode, color0=c0_node, color1=c1_node)

    return ngl.Group(children=(grad, p0, p1))

This previous example shows how we can craft positions, but also colors. The eval API proposes a few helper to work with colors, so we could use it for example to build an entire palette and its grayscale variant at runtime (so that it can reacts to live-controls):

A palette strip and its grayscale version, interpolated from 2 colors
import pynopegl as ngl

def palette_strip(cfg: ngl.SceneCfg):
    cols, rows = (7, 5)
    cfg.aspect_ratio = (cols, rows)

    # The 2 colors to interpolate from
    c0_node = ngl.UniformColor(value=(1, 0.5, 0.5), live_id="c0")
    c1_node = ngl.UniformColor(value=(1, 1, 0.5), live_id="c1")

    # The 2 grayscale variants to interpolate from
    l0_node = ngl.EvalVec3("luma(c.r, c.g, c.b)", resources=dict(c=c0_node))
    l1_node = ngl.EvalVec3("luma(c.r, c.g, c.b)", resources=dict(c=c1_node))

    # Create each intermediate color
    c_nodes = []
    l_nodes = []
    for i in range(cols - 4):
        c_node = ngl.EvalVec3(
            "srgbmix(c0.r, c1.r, t)",
            "srgbmix(c0.g, c1.g, t)",
            "srgbmix(c0.b, c1.b, t)",
            resources=dict(c0=c0_node, c1=c1_node, t=ngl.UniformFloat((i + 1) / (cols - 1))),
        l_node = ngl.EvalVec3("luma(c.r, c.g, c.b)", resources=dict(c=c_node))

    # Grid positioning of RenderColor nodes using GridLayout
    c_nodes = [c0_node] + c_nodes + [c1_node]
    l_nodes = [l0_node] + l_nodes + [l1_node]
    c_nodes = [ngl.RenderColor(node) for node in c_nodes]
    l_nodes = [ngl.RenderColor(node) for node in l_nodes]
    empty = ngl.Identity()
    cells = [empty] * (cols + 1) + c_nodes + [empty] * (cols + 2) + l_nodes
    strips = ngl.GridLayout(cells, size=(cols, rows))

    bg = ngl.RenderColor(color=(0.2, 0.2, 0.2))  # A gray background
    return ngl.Group(children=[bg, strips])

Here we’re using luma() to get the greyscale values, and srgbmix() to interpolate between 2 sRGB colors (interpolation is done in linear space).

Of course, if the interpolation is not required at runtime (ie: the result does not depend on the time or live controls values), it is highly recommended to precompute them from the Python instead to have it computed only once.