Shaders

A Shader represents the program sent to the GPU to render each pixel on the surface of our geometry. It can be instantiated with a base geometry and must be provided with a shader written with our custom shading language.

A simple shader might look something like this:

static SHADER: Shader = Shader {
    build_geom: Some(build_geom),
    code_to_concatenate: &[
        Cx::STD_SHADER,
        code_fragment!(
            r#"
            geometry geom: vec2;
            instance color: vec4;

            fn vertex() -> vec4 {
                return vec4(geom.x, geom.y, 0., 1.);
            }

            fn pixel() -> vec4 {
                return color;
            }
            "#
        ),
    ],
    ..Shader::DEFAULT
};

Shaders are statically defined, and consist of two fields:

  • build_geom: a function that produces a Geometry. Can be omitted if you want to dynamically assign a geometry at draw time.
  • code_to_concatenate: an array of CodeFragments, that get concatenated in order. Define each fragment using the code_fragment!() macro (this keeps track of filenames and line numbers, for better error messages).

Passing in data

A shader typically starts with a bunch of variable declarations. These declarations define the data that you pass into the shader, and has to exactly match the data types in Rust.

For example, to pass in instance data, you can define some instance variables in the shader:

r#"
instance pos: vec2;
instance color: vec4;
"#

Which has to exactly match the corresponding "instance struct":

#[repr(C)]
struct MyShaderIns {
    pos: Vec2,
    color: Vec4,
}

Note the use of #[repr(C)] to ensure that the data is properly laid out in memory.

When calling cx.add_instances(&SHADER, &[MyShaderIns { pos, color }]), we verify that the memory size of MyShaderIns matches that of the instance variables in the shader code.

This is how Rust types match with shader types:

RustShader
f32float
Vec2vec2
Vec3vec3
Vec4vec4
Mat4mat4

Note that within a function there are more types you can use.

These are the types of variables you can declare:

  • geometry: these have to match exactly the vertex_attributes fields in Geometry::new.
  • instance: these have to match exactly the data fields in Cx::add_instances.
  • uniform: these have to match exactly the uniforms fields in Area::write_user_uniforms.
  • texture: can only be of type texture2D and gets set using Cx::write_user_uniforms.
  • varying: doesn't get passed in from Rust, but can be used to pass data from fn vertex() to fn pixel().

Shader language

The shader language itself is modeled after Rust itself. You can use things like fn, struct, and so on. Two functions need to be defined for a shader to work:

  • fn vertex() defines the vertex shader. This gets called for each vertex returned from build_geom. It returns vec4(x, y, z, w) where the values mean the following:
    • x, y — coordinates on the screen (from -1 to 1).
    • z — draw order (from 0 to 1). Draws with higher z will be on top.
    • w — normalization parameter. In 2d rendering this is simply set to 1.0.
  • fn pixel() defines the pixel shader. It returns a color as vec4(r, g, b, a).

See Tutorial: Rendering 2D Shapes for more about the basics of drawing in a shader.

KeywordDescriptionExample
constConstant valuesconst PI: float = 3.141592653589793;
letVariable (mutable)let circle_size = 7. - stroke_width / 2.;
returnReturn valuereturn 0.0;
ifCondition
if errored > 0. {
    df.fill(error_color);
} else if loaded > 0. {
    df.fill(active_color);
} else {
    df.fill(inactive_color);
}
#hexColor
return #ff0000;
return #f00;
return #f;
fnFunction definition
fn pixel() -> vec4 {
    return #f00;
}
structStructure definition
struct Df {
    pos: vec2,
    result: vec4,
}
implStructure implementation
impl Df {
    fn clear(inout self, color: vec4) {
        self.write_color(color, 1.0);
    }
}
forRange loop
for i from 0 to 20 step 3 {
    if float(i) >= depth {
        break;
    }
}
?Ternary operatorlet pos = is_left ? start : end;

The following built-in functions are available: abs, acos, acos, all, any, asin, atan, ceil, clamp, cos, cross, degrees, dFdx, dFdy, distance, dot, equal, exp, exp2, faceforward, floor, fract, greaterThan, greaterThanEqual, inversesqrt, inverse, length, lessThan, lessThanEqual, log, log2, matrixCompMult, max, min, mix, mod, normalize, not, notEqual, pow, radians, reflect, refract, sample2d, sign, sin, smoothstep, sqrt, step, tan, transpose.

Swizzling is also supported, for both xyzw and rgba. So you can do things like let plane: vec2 = point.xy or let opaque: vec3 = color.rgba.

STD_SHADER

Zaplib provides STD_SHADER, a collection of common functions that are useful when writing shaders. For a complete run down on the available functions, it's best to directly look at the source, but we'll discuss some highlights.

3D space transforms

These values are useful when working in 3D space, translating an object's scene coordinates into pixel locations on the screen.

NameTypeDescription
camera_projectionmat4Camera projection matrix - see 3D projection.
camera_viewmat4View matrix - see Camera matrix.
inv_camera_rotmat4The inverse rotation matrix for a camera. Useful for working with billboards.

As a quick example, a basic vertex shader to convert from object to screen coordinates is:

fn vertex() -> vec4 {
    return camera_projection * camera_view * vec4(geom_position, 1.);
}

These values get set by a combination of Pass::set_matrix_mode and the actual computed dimensions of a Pass. See e.g. the Viewport3D component.

Rendering helpers

NameTypeDescription
dpi_factorfloatMore commonly known as the "device pixel ratio"; represents the ratio of the resolution in physical pixels to the resolution in GPU pixels for the current display device.
dpi_dilatefloatSome amount by which to thicken lines, depending on the dpi_factor
draw_clipvec4Clip region for rendering, represented as (x1,y1,x2,y2).
draw_scrollvec2The total 2D scroll offset, including all its parents. This is usually only relevant for 2D UI rendering.
draw_local_scrollvec2The 2D scroll offset excluding parents. This is usually only relevant for 2D UI rendering.
draw_zbiasfloatA small increment that you can add to the z-axis of your vertices, which is based on the position of the draw call in the draw tree.

Math

NameTypeDescription
Math::rotate_2d(v: vec2, a: float) -> vec2Rotate vector v by radians a

Colors

NameTypeDescription
hsv2rgb(c: vec4) -> vec4Convert color c from HSV representation to RGB representation
rgb2hsv(c: vec4) -> vec4Convert color c from RGB representation to HSV representation

Useful constants

NameTypeDescription
PIfloatPi (π)
Efloate
LN2floatln(2) - The natural log of 2
LN10floatln(10) - The natural log of 10
LOG2Efloatlog2(e) - Base-2 log of e
LOG10Efloatlog2(e) - Base-10 log of e
SQRT1_2floatsqrt(1/2) - Square root of 1/2
TORADfloatConversion factor of degrees to radians. Equivalent to PI/180.
GOLDENfloatGolden ratio

Distance fields

Zaplib contains many functions for Signed Distance Fields (SDFs) under the Df namespace. SDFs are a comprehensive way to define flexible shapes on the GPU. While applicable in 2D and 3D contexts, Zaplib uses this only for 2D rendering.

To create a distance field, use either:

NameTypeDescription
Df::viewport(pos: vec2) -> DfCreates a distance field with the current position
Df::viewport_px(pos: vec2) -> DfCreates a distance field with the current position, factoring in dpi_factor

The following methods are available on the instantiated Df struct.

NameTypeDescription
df.add_field(field: float) -> voidAdds a new field value to the current distance field
df.add_clip(d: float) -> voidAdds a clip mask to the current distance field
df.antialias(p: vec2) -> floatDistance-based antialiasing
df.translate(offset: vec2) -> vec2Translate a specified offset
df.rotate(a: float, pivot: vec2) -> voidRotate by a radians around pivot
df.scale(f: float, pivot: vec2) -> voidUniformly scale by factor f around pivot
df.clear(src: vec4) -> voidSets clear color. Useful for specifying background colors before rendering a path.
df.new_path() -> voidClears path in current distance field.
df.fill(color: vec4) -> vec4Fills the current path with color.
df.stroke(color: vec4, width: float) -> vec4Strokes the current path with color with a pixel width of width.
df.glow(color: vec4, width: float) -> vec4Updates the current path by summing colors in width with the provided one.
df.union() -> voidSet field to the union of the current and previous field.
df.intersect() -> voidSet field to the intersection of the current and previous field.
df.subtract() -> voidSubtract current field from previous.
df.blend(k: float) -> voidInterpolate current field and previous with factor k.
df.circle(p: vec2, r: float) -> voidRender a circle at p with radius r.
df.arc(p: vec2, r: float, angle_start: float, angle_end: float) -> voidRender an arc at p with radius r between angles angle_start and angle_end.
df.rect(p: vec2, d: vec2) -> voidRender a rectangle at p with dimensions d.
df.box(p: vec2, d: vec2, r: float) -> voidRender a box with rounded corners at p with dimensions d. Use r to indicate the corner radius - if r is less than 1, render a basic rectangle. If r is bigger than min(w, h), the result will be a circle.
df.triangle(p0: vec2, p1: vec2, p2: vec2) -> voidRender a triangle between points p0, p1, p2.
df.hexagon(p: vec2, r: float) -> voidRender a hexagon at p with side length r.
df.move_to(p: vec2) -> voidMove to p in current path, not drawing from current position.
df.line_to(p: vec2) -> voidRender a line to p from current position.
df.close_path() -> voidEnd the current field by rendering a line back to the start point.