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 aGeometry
. Can be omitted if you want to dynamically assign a geometry at draw time.code_to_concatenate
: an array ofCodeFragment
s, that get concatenated in order. Define each fragment using thecode_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:
Rust | Shader |
---|---|
f32 | float |
Vec2 | vec2 |
Vec3 | vec3 |
Vec4 | vec4 |
Mat4 | mat4 |
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 thevertex_attributes
fields inGeometry::new
.instance
: these have to match exactly thedata
fields inCx::add_instances
.uniform
: these have to match exactly theuniforms
fields inArea::write_user_uniforms
.texture
: can only be of typetexture2D
and gets set usingCx::write_user_uniforms
.varying
: doesn't get passed in from Rust, but can be used to pass data fromfn vertex()
tofn 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 frombuild_geom
. It returnsvec4(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 higherz
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 asvec4(r, g, b, a)
.
See Tutorial: Rendering 2D Shapes for more about the basics of drawing in a shader.
Keyword | Description | Example |
const | Constant values | const PI: float = 3.141592653589793; |
let | Variable (mutable) | let circle_size = 7. - stroke_width / 2.; |
return | Return value | return 0.0; |
if | Condition |
|
#hex | Color |
|
fn | Function definition |
|
struct | Structure definition |
|
impl | Structure implementation |
|
for | Range loop |
|
? | Ternary operator | let 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.
Name | Type | Description |
---|---|---|
camera_projection | mat4 | Camera projection matrix - see 3D projection. |
camera_view | mat4 | View matrix - see Camera matrix. |
inv_camera_rot | mat4 | The 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
Name | Type | Description |
---|---|---|
dpi_factor | float | More 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_dilate | float | Some amount by which to thicken lines, depending on the dpi_factor |
draw_clip | vec4 | Clip region for rendering, represented as (x1,y1,x2,y2). |
draw_scroll | vec2 | The total 2D scroll offset, including all its parents. This is usually only relevant for 2D UI rendering. |
draw_local_scroll | vec2 | The 2D scroll offset excluding parents. This is usually only relevant for 2D UI rendering. |
draw_zbias | float | A 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
Name | Type | Description |
---|---|---|
Math::rotate_2d | (v: vec2, a: float) -> vec2 | Rotate vector v by radians a |
Colors
Name | Type | Description |
---|---|---|
hsv2rgb | (c: vec4) -> vec4 | Convert color c from HSV representation to RGB representation |
rgb2hsv | (c: vec4) -> vec4 | Convert color c from RGB representation to HSV representation |
Useful constants
Name | Type | Description |
---|---|---|
PI | float | Pi (Ï€) |
E | float | e |
LN2 | float | ln(2) - The natural log of 2 |
LN10 | float | ln(10) - The natural log of 10 |
LOG2E | float | log2(e) - Base-2 log of e |
LOG10E | float | log2(e) - Base-10 log of e |
SQRT1_2 | float | sqrt(1/2) - Square root of 1/2 |
TORAD | float | Conversion factor of degrees to radians. Equivalent to PI/180. |
GOLDEN | float | Golden 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:
Name | Type | Description |
---|---|---|
Df::viewport | (pos: vec2) -> Df | Creates a distance field with the current position |
Df::viewport_px | (pos: vec2) -> Df | Creates a distance field with the current position, factoring in dpi_factor |
The following methods are available on the instantiated Df
struct.
Name | Type | Description |
---|---|---|
df.add_field | (field: float) -> void | Adds a new field value to the current distance field |
df.add_clip | (d: float) -> void | Adds a clip mask to the current distance field |
df.antialias | (p: vec2) -> float | Distance-based antialiasing |
df.translate | (offset: vec2) -> vec2 | Translate a specified offset |
df.rotate | (a: float, pivot: vec2) -> void | Rotate by a radians around pivot |
df.scale | (f: float, pivot: vec2) -> void | Uniformly scale by factor f around pivot |
df.clear | (src: vec4) -> void | Sets clear color. Useful for specifying background colors before rendering a path. |
df.new_path | () -> void | Clears path in current distance field. |
df.fill | (color: vec4) -> vec4 | Fills the current path with color . |
df.stroke | (color: vec4, width: float) -> vec4 | Strokes the current path with color with a pixel width of width . |
df.glow | (color: vec4, width: float) -> vec4 | Updates the current path by summing colors in width with the provided one. |
df.union | () -> void | Set field to the union of the current and previous field. |
df.intersect | () -> void | Set field to the intersection of the current and previous field. |
df.subtract | () -> void | Subtract current field from previous. |
df.blend | (k: float) -> void | Interpolate current field and previous with factor k . |
df.circle | (p: vec2, r: float) -> void | Render a circle at p with radius r . |
df.arc | (p: vec2, r: float, angle_start: float, angle_end: float) -> void | Render an arc at p with radius r between angles angle_start and angle_end . |
df.rect | (p: vec2, d: vec2) -> void | Render a rectangle at p with dimensions d . |
df.box | (p: vec2, d: vec2, r: float) -> void | Render 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) -> void | Render a triangle between points p0 , p1 , p2 . |
df.hexagon | (p: vec2, r: float) -> void | Render a hexagon at p with side length r . |
df.move_to | (p: vec2) -> void | Move to p in current path, not drawing from current position. |
df.line_to | (p: vec2) -> void | Render a line to p from current position. |
df.close_path | () -> void | End the current field by rendering a line back to the start point. |