Drawing shaders

Drawing to the screen

Let’s paint the screen (small square canvas in our case) dark red.

Exciting, I know! Let’s break it down:

  • fn main() again this is our frame loop, it’s called once per frame
  • draw(shader { ... }) this is a function that takes a shader as an argument and draws it to the screen (more on this in a moment)
  • shader { ... } this is a shader block, they’re first-class citizens. Feel free to store them in variables, pass them around, etc.
  • out.color = (0.2, 0, 0, 1) this is the output color of the shader. It’s a 4 component vector (r, g, b, a) where each component is a float between 0 and 1

There are a few hidden things happening here:

  • Just before the frame loop starts, the canvas is cleared to transparent (0, 0, 0, 0). Each frame you have a fresh canvas to paint into.
  • Passing a single argument to the draw() function is a shorthand for drawing a full-screen fragment shader (aka pixel shader).
    • If you remember from a previous section, a fragment shader is run on a per-pixel basis. Think of it like a function that has some input (the pixel’s position) and some output (the pixel’s color).
    • A fragment shader doesn’t just have to have a single input or output, it can draw to mulitple attachments (textures/canvases) and it can input multiple values (textures, uniforms, etc). We’ll cover this in a later section.

Let’s make something a little more interesting and use the pixel position to draw a gradient.

  • in.uv is a float2 that spans from (0, 0) to (1, 1) across the screen. It’s a normalized coordinate system. (the name UV comes from a paper on texture mapping, U = horizontal axis and V = vertical axis)
  • The shader is painted for every pixel, and our in.uv.x runs from 0-1 across the x axis, so we get a simple red gradient.

Tip: If you want to see a 2d gradient replace the (in.uv.x, 0, 0, 1) with (in.uv, 0.5, 1)

Passing values into shaders (uniforms)

This is cool and all but what if we want to pass in some values from the CPU? We can do this by defining uniforms.

What’s happening here is that we’re defining a variable myColor in the CPU and then referencing it in the shader. The shader will automatically close over any references to variables in the CPU and bind them at definition time. By referencing myColor inside of a shader we’re telling the compiler that we want to pass this value in as a uniform. The compiler will automatically generate the code to bind the value to the shader.


With this knowledge let’s make it interactive (go ahead and move your mouse over the canvas):

  • env.mouse.screen is a float2 that spans from (0, 0) to (width, height) of your screen in pixels. It’s automatically updated every frame by Shadeup.
  • in.screen is a float2 that spans from (0, 0) to (width, height) of your screen. Similar to in.uv but in pixels instead of normalized coordinates.
  • dist(a, b) is a function that returns the distance between two points. It’s a built-in function that’s part of the standard library.
  • 1.xyzw is a vector with the components (1, 1, 1, 1). It’s a shorthand for int4(1, 1, 1, 1).

One cool thing about uniforms is that we aren’t just limited to be numeric primitives, they can be structures too. Let’s remove our let mouse = ... and just use env.mouse directly in the shader.

Shader ergonomics

As mentioned above shaders can be defined almost anywhere in CPU code. They are first class citizens and can be passed around like any other value:

Remember that values are bound at definition time, so we can close over values and they will follow the shader around.

  • rand(seed) returns a random number between 0 and 1 for a given seed. It’s a built-in function that’s part of the standard library.
  • env.frame is a number that increments every frame.

Let’s combine these conecpts to create a shader that cycles through colors

Next up we’ll draw some geometry!