Understanding Basic GLSL Shader Techniques

There are some excellent resources online for learning shaders and WebGL, namely The Book of Shaders and WebGL Fundamentals. This post covers some of the basic patterns that I needed to learn in order to complete the Dream Builder's crop implementation.

Mix a Mask

float mask = step(edge, x);
vec3 color = mix(color1, color2, mask);

The code for each shape below can be abstracted to a separate function and called from main like so:

vec3 color = mix(color1, color2, getShapeMask(v));

Draw a Rectangle

varying vec2 vUv;

float rect(vec2 xy, vec2 wh, vec2 st) {
  vec2 xyMask = step(xy, st);
  vec2 whMask = 1.0 - step(xy + wh, st);
  vec2 mask = xyMask * whMask;
  return mask.x * mask.y;
}

void main() {
    vec4 red = vec4(1.,0.,0.,1.);
    vec4 blue = vec4(0.,0.,1.,1.);

    vec2 xy = vec2(.1,.2);
    vec2 wh = vec2(.3,.4);

    gl_FragColor = mix(blue, red, rect(xy, wh, vUv));
}

Step Between Macro

This is used to return 1.0 if v is between lo and hi, 0.0 otherwise:

#define between(lo, hi, v) step(lo, v) * step(v, hi)

This logic comes in very handy for the subsequent shapes.

Rectangle with Borders

Notice how max() and min() act as union/intersection on our mask in this example:

float outlineRect(vec2 xy, vec2 wh, vec2 th, vec2 st) {
  vec2 bl = between(xy, xy + th, st);
  vec2 tr = between(xy + wh, xy + wh + th, st);
  float l = min(bl.x, between(xy.y, xy.y + wh.y + th.y, st.y));
  float b = min(bl.y, between(xy.x, xy.x + wh.x + th.x, st.x));
  float r = min(tr.x, between(xy.y, xy.y + wh.y + th.y, st.y));
  float t = min(tr.y, between(xy.x, xy.x + wh.x + th.x, st.x));
  return max(max(l, b), max(r, t));
}

Full Border

Naively we can use our between macro again:

float full_border(vec2 th, vec2 st) {
  vec2 mask = between(th, 1.0 - th, st);
  return 1.0 - min(mask.x, mask.y);
}

But if you scale this up and down (see below) you'll notice the border continues infinitely beyond the edges of the clip space. It may be more desirable to use our outlineRect again to produce a finite border:

float full_border(vec2 th, vec2 st) {
  return outlineRect(vec2(0.0), 1.0 - th, th, st);
}

Circle

A simple circle:

float circle(float radius, vec2 st) {
  vec2 center = vec2(0.5, 0.5);
  float size = radius;
  return 1.0 - step(size, distance(center, st));
}

Shape Cutting

We can cut one mask with another. Here we cut a circle out of a full border which creates the effect of pointed arrow corners, ideal for scaling/resizing handles:

// these values produce the desired effect
vec2 border_thickness = vec2(0.025);
float radius = 0.6;

// shape cutting code
float border_mask = full_border(border_thickness, st);
vec4 color = mix(bg_color, border_color, border_mask);
gl_FragColor = mix(color, bg_color, min(border_mask, circle(radius, st)));

More from this series

This post is part of the Dream Builder Series.

The previous post was Setting up Shaders with React Three Fiber.

The next post is Implementing Crop.

Get in touch

If you have any questions or ideas, please email me at tom@bearjam.dev.