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 framedraw(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.
Interactivity
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 toin.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 forint4(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!