Snowman Shader in Shadertoy

Published: Tuesday, May 11, 2021

Shadertoy canvas with multiple snowman wiggling on top of snow in an amazing winter scene. The sky is a light purple and purple fog blends in with the background giving the 3D scene a sense of depth. Snow is falling all around the snowmen.

Do you wanna build a snowmannnnnnnn ☃️ 🎶?

Come on, let's go and code.

Trust me, it won't be a bore.

Prepare your keyboard.

It's time to ray march awayyyyyyy!!!!

Greetings, friends! You have made it so far on your Shadertoy journey! I'm so proud! Even if you haven't read any of my past articles and landed here from Google, I'm still proud you visited my website 😃. If you're new to Shadertoy or even shaders in general, please visit Part 1 of my Shadertoy tutorial series.

In this article, I will show you how to make a snowman shader using the lessons in my Shadertoy tutorial series. We'll create a simple snowman, add color using structs, and then add lots of details to our scene to create an amazing shader!!!

Initial Setup

We'll start with the ray marching template we used at the beginning of Part 14 of my Shadertoy tutorial series.

glsl
Copied! ⭐️
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;
const float PI = 3.14159265359;
const vec3 COLOR_BACKGROUND = vec3(.741, .675, .82);
const vec3 COLOR_AMBIENT = vec3(0.42, 0.20, 0.1);

mat2 rotate2d(float theta) {
  float s = sin(theta), c = cos(theta);
  return mat2(c, -s, s, c);
}

float sdSphere(vec3 p, float r, vec3 offset)
{
  return length(p - offset) - r;
}

float scene(vec3 p) {
  return sdSphere(p, 1., vec3(0, 0, 0));
}

float rayMarch(vec3 ro, vec3 rd) {
  float depth = MIN_DIST;
  float d; // distance ray has travelled

  for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
    vec3 p = ro + depth * rd;
    d = scene(p);
    depth += d;
    if (d < PRECISION || depth > MAX_DIST) break;
  }

  d = depth;

  return d;
}

vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1, -1) * EPSILON;
    return normalize(
      e.xyy * scene(p + e.xyy) +
      e.yyx * scene(p + e.yyx) +
      e.yxy * scene(p + e.yxy) +
      e.xxx * scene(p + e.xxx));
}

mat3 camera(vec3 cameraPos, vec3 lookAtPoint) {
    vec3 cd = normalize(lookAtPoint - cameraPos);
    vec3 cr = normalize(cross(vec3(0, 1, 0), cd));
    vec3 cu = normalize(cross(cd, cr));

    return mat3(-cr, cu, -cd);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec2 mouseUV = iMouse.xy/iResolution.xy;

  if (mouseUV == vec2(0.0)) mouseUV = vec2(0.5); // trick to center mouse on page load

  vec3 col = vec3(0);
  vec3 lp = vec3(0); // lookat point
  vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position

  float cameraRadius = 2.;
  ro.yz = ro.yz * cameraRadius * rotate2d(mix(-PI/2., PI/2., mouseUV.y));
  ro.xz = ro.xz * rotate2d(mix(-PI, PI, mouseUV.x)) + vec2(lp.x, lp.z);

  vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction

  float d = rayMarch(ro, rd); // signed distance value to closest object

  if (d > MAX_DIST) {
    col = COLOR_BACKGROUND; // ray didn't hit anything
  } else {
    vec3 p = ro + rd * d; // point discovered from ray marching
    vec3 normal = calcNormal(p); // surface normal

    vec3 lightPosition = vec3(0, 2, 2);
    vec3 lightDirection = normalize(lightPosition - p) * .65; // The 0.65 is used to decrease the light intensity a bit

    float dif = clamp(dot(normal, lightDirection), 0., 1.) * 0.5 + 0.5; // diffuse reflection mapped to values between 0.5 and 1.0

    col = vec3(dif) + COLOR_AMBIENT;
  }

  fragColor = vec4(col, 1.0);
}

When you run this code, you should see a sphere appear in the center of the screen. It kinda looks like a snowball, doesn't it?

Shadertoy canvas with a bright purple background. A white sphere with a tint of orange is drawn to the center of the canvas.

Building a Snowman Model

When building 3D models using ray marching, it's best to think about what SDFs we'll need to build a snowman. A snowman is typically made using two or three spheres. For our snowman, we'll keep it simple and build it using only two spheres.

Let's draw two spheres to the scene. We can use the opUnion function we learned in Part 14 to draw more than one shape to the scene.

glsl
Copied! ⭐️
float opUnion(float d1, float d2) {
  return min(d1, d2);
}

We've been using this function already in the previous tutorials. It simply takes the minimum "signed distance" between two SDFs.

glsl
Copied! ⭐️
float scene(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));

  return opUnion(bottomSnowball, topSnowball);
}

Shadertoy canvas with a bright purple background. Two white spheres with a tint of orange are drawn to the center of the canvas. They are stacked vertically, resembling a snowman.

Right away, you can our snowman starting to take shape, but it looks awkward at the intersection where the two spheres meet. As we learned in Part 14 of my Shadertoy tutorial series, we can blend two shapes smoothly together by using the opSmoothUnion function or smin function, if you want to use a shorter name.

glsl
Copied! ⭐️
float opSmoothUnion(float d1, float d2, float k) {
  float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
  return mix( d2, d1, h ) - k*h*(1.0-h);
}

Now, let's replace the opUnion function with opSmoothUnion in our scene. We'll use a value of 0.2 as the smoothing factor, k.

glsl
Copied! ⭐️
float scene(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));

  float d = opSmoothUnion(bottomSnowball, topSnowball, 0.2);
  return d;
}

Shadertoy canvas with a bright purple background. Two white spheres with a tint of orange are drawn to the center of the canvas. They are stacked vertically, resembling a snowman. The edges are smoothed out around the edges.

That looks much better! The snowman is missing some eyes though. People tend to give them eyes using buttons or some other round objects. We'll give our snowman spherical eyes. Let's start with the left eye.

glsl
Copied! ⭐️
float scene(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));
  float leftEye = sdSphere(p, .1, vec3(-0.2, 0.6, 0.7));

  float d = opSmoothUnion(bottomSnowball, topSnowball, 0.2);
  d = opUnion(d, leftEye);
  return d;
}

Shadertoy canvas with a bright purple background. A snowman with a tint of orange is drawn in the center of the canvas. A small sphere is drawn for its left eye.

The right eye will use the same offset value as the left eye except the x-axis will be mirrored.

glsl
Copied! ⭐️
float scene(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));
  float leftEye = sdSphere(p, .1, vec3(-0.2, 0.6, 0.7));
  float rightEye = sdSphere(p, .1, vec3(0.2, 0.6, 0.7));

  float d = opSmoothUnion(bottomSnowball, topSnowball, 0.2);
  d = opUnion(d, leftEye);
  d = opUnion(d, rightEye);
  return d;
}

Shadertoy canvas with a bright purple background. A snowman with a tint of orange is drawn in the center of the canvas. Two small spheres are drawn for its eyes.

Next, the snowman needs a nose. People tend to make noses for snowmen out of carrots. We can simulate a carrot nose by using a cone SDF from Inigo Quilez's list of 3D SDFs. We'll choose the SDF called "Cone - bound (not exact)" which has the following function declaration:

glsl
Copied! ⭐️
float sdCone( vec3 p, vec2 c, float h )
{
  float q = length(p.xz);
  return max(dot(c.xy,vec2(q,p.y)),-h-p.y);
}

This is for a cone pointing straight up. We want the tip of the cone to face us, toward the positive z-axis. To switch this, we'll replace p.xz with p.xy and replace p.y with p.z.

glsl
Copied! ⭐️
float sdCone( vec3 p, vec2 c, float h )
{
  p -= offset;
  float q = length(p.xy);
  return max(dot(c.xy,vec2(q,p.z)),-h-p.z);
}

We also need to add an offset parameter to this function, so we can move the cone around in 3D space. Therefore, we end up with the following function declaration for the cone SDF.

glsl
Copied! ⭐️
float sdCone( vec3 p, vec2 c, float h, vec3 offset )
{
  p -= offset;
  float q = length(p.xy);
  return max(dot(c.xy,vec2(q,p.z)),-h-p.z);
}

To use this SDF, we need to create an angle for the cone. This requires playing around with the value a bit. A value of 75 degrees seems to work fine. You can use the radians function that is built into the GLSL language to convert a number from degrees to radians. The parameters, c and h, are used to control the dimensions of the cone.

Let's add a nose to our snowman!

glsl
Copied! ⭐️
float scene(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));

  float leftEye = sdSphere(p, .1, vec3(-0.2, 0.6, 0.7));
  float rightEye = sdSphere(p, .1, vec3(0.2, 0.6, 0.7));

  float noseAngle = radians(75.);
  float nose = sdCone(p, vec2(sin(noseAngle), cos(noseAngle)), 0.5, vec3(0, 0.4, 1.2));

  float d = opSmoothUnion(bottomSnowball, topSnowball, 0.2);
  d = opUnion(d, leftEye);
  d = opUnion(d, rightEye);
  d = opUnion(d, nose);
  return d;
}

Shadertoy canvas with a bright purple background. A snowman with a tint of orange is drawn in the center of the canvas. Two small spheres are drawn for its eyes. A cone is drawn for its nose.

You can use your mouse to move the camera around the snowman to make sure the cone looks fine.

Shadertoy canvas with a bright purple background. A snowman with a tint of orange is drawn in the center of the canvas. Two small spheres are drawn for its eyes. A cone is drawn for its nose. The camera is rotated such that its facing to the right instead of toward the viewer.

Let's add arms to the snowman. Typically, the arms are made of sticks. We can simulate sticks by using a 3D line or "capsule." In Inigo Quilez's list of 3D SDFs, there's an SDF called "Capsule / Line - exact" that we can leverage for building a snowman arm.

glsl
Copied! ⭐️
float sdCapsule( vec3 p, vec3 a, vec3 b, float r )
{
  vec3 pa = p - a, ba = b - a;
  float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
  return length( pa - ba*h ) - r;
}

Add an offset parameter to this function, so we can move the capsule around in 3D space.

glsl
Copied! ⭐️
float sdCapsule( vec3 p, vec3 a, vec3 b, float r, vec3 offset )
{
  p -= offset;
  vec3 pa = p - a, ba = b - a;
  float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
  return length( pa - ba*h ) - r;
}

Then, we'll add a capsule in our 3D scene to simulate the left arm of the snowman.

glsl
Copied! ⭐️
float scene(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));

  float leftEye = sdSphere(p, .1, vec3(-0.2, 0.6, 0.7));
  float rightEye = sdSphere(p, .1, vec3(0.2, 0.6, 0.7));

  float noseAngle = radians(75.);
  float nose = sdCone(p, vec2(sin(noseAngle), cos(noseAngle)), 0.5, vec3(0, 0.4, 1.2));

  float mainBranch = sdCapsule(p, vec3(0, 0.5, 0), vec3(0.8, 0, 0.), 0.05, vec3(-1.5, -0.5, 0));

  float d = opSmoothUnion(bottomSnowball, topSnowball, 0.2);
  d = opUnion(d, leftEye);
  d = opUnion(d, rightEye);
  d = opUnion(d, nose);
  d = opUnion(d, mainBranch);
  return d;
}

Shadertoy canvas with a bright purple background. A snowman with a tint of orange is drawn in the center of the canvas. Two small spheres are drawn for its eyes. A cone is drawn for its nose. A left arm made out of a capsule shape is drawn.

The arm looks a bit too small and kinda awkward. Let's add a couple small capsules that branch off the "main branch" arm, so that it looks like the arm is built out of a tree branch.

glsl
Copied! ⭐️
float scene(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));

  float leftEye = sdSphere(p, .1, vec3(-0.2, 0.6, 0.7));
  float rightEye = sdSphere(p, .1, vec3(0.2, 0.6, 0.7));

  float noseAngle = radians(75.);
  float nose = sdCone(p, vec2(sin(noseAngle), cos(noseAngle)), 0.5, vec3(0, 0.4, 1.2));

  float mainBranch = sdCapsule(p, vec3(0, 0.5, 0), vec3(0.8, 0, 0.), 0.05, vec3(-1.5, -0.5, 0));
  float smallBranchBottom = sdCapsule(p, vec3(0, 0.1, 0), vec3(0.5, 0, 0.), 0.05, vec3(-2, 0, 0));
  float smallBranchTop = sdCapsule(p, vec3(0, 0.3, 0), vec3(0.5, 0, 0.), 0.05, vec3(-2, 0, 0));

  float d = opSmoothUnion(bottomSnowball, topSnowball, 0.2);
  d = opUnion(d, leftEye);
  d = opUnion(d, rightEye);
  d = opUnion(d, nose);
  d = opUnion(d, mainBranch);
  d = opUnion(d, smallBranchBottom);
  d = opUnion(d, smallBranchTop);
  return d;
}

Shadertoy canvas with a bright purple background. A snowman with a tint of orange is drawn in the center of the canvas. Two small spheres are drawn for its eyes. A cone is drawn for its nose. A left arm made out of three capsule shapes to resemble a tree branch is drawn.

For the right arm, we need to apply the same three capsule SDFs but flip the sign of the x-component to "mirror" the arm on the other side of the snowman. We could write another three lines for the right arm, one for each capsule SDF, or we can get clever. The snowman is currently centered in the middle of our screen. We can take advantage of symmetry to draw the right arm with the same offset as the left arm but with a positive x-component instead of negative.

Let's create a custom SDF that merges the three branches into one SDF called sdArm.

glsl
Copied! ⭐️
float sdArm(vec3 p) {
  float mainBranch = sdCapsule(p, vec3(0, 0.5, 0), vec3(0.8, 0, 0.), 0.05, vec3(-1.5, -0.5, 0));
  float smallBranchBottom = sdCapsule(p, vec3(0, 0.1, 0), vec3(0.5, 0, 0.), 0.05, vec3(-2, 0, 0));
  float smallBranchTop = sdCapsule(p, vec3(0, 0.3, 0), vec3(0.5, 0, 0.), 0.05, vec3(-2, 0, 0));

  float d = opUnion(mainBranch, smallBranchBottom);
  d = opUnion(d, smallBranchTop);
  return d;
}

Then, we can use this function inside our scene function.

glsl
Copied! ⭐️
float scene(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));

  float leftEye = sdSphere(p, .1, vec3(-0.2, 0.6, 0.7));
  float rightEye = sdSphere(p, .1, vec3(0.2, 0.6, 0.7));

  float noseAngle = radians(75.);
  float nose = sdCone(p, vec2(sin(noseAngle), cos(noseAngle)), 0.5, vec3(0, 0.4, 1.2));

  float leftArm = sdArm(p);

  float d = opSmoothUnion(bottomSnowball, topSnowball, 0.2);
  d = opUnion(d, leftEye);
  d = opUnion(d, rightEye);
  d = opUnion(d, nose);
  d = opUnion(d, leftArm);
  return d;
}

Let's make a custom operation called opFlipX that will flip the sign of the x-component of the point passed into it.

glsl
Copied! ⭐️
vec3 opFlipX(vec3 p) {
  p.x *= -1.;
  return p;
}

Then, we can use this function inside the scene function to draw the right arm.

glsl
Copied! ⭐️
float scene(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));

  float leftEye = sdSphere(p, .1, vec3(-0.2, 0.6, 0.7));
  float rightEye = sdSphere(p, .1, vec3(0.2, 0.6, 0.7));

  float noseAngle = radians(75.);
  float nose = sdCone(p, vec2(sin(noseAngle), cos(noseAngle)), 0.5, vec3(0, 0.4, 1.2));

  float leftArm = sdArm(p);
  float rightArm = sdArm(opFlipX(p));

  float d = opSmoothUnion(bottomSnowball, topSnowball, 0.2);
  d = opUnion(d, leftEye);
  d = opUnion(d, rightEye);
  d = opUnion(d, nose);
  d = opUnion(d, leftArm);
  d = opUnion(d, rightArm);
  return d;
}

Shadertoy canvas with a bright purple background. A snowman with a tint of orange is drawn in the center of the canvas. Two small spheres are drawn for its eyes. A cone is drawn for its nose. A left and right arm made out of three capsule shapes to resemble tree branches are

Voilà! We used symmetry to draw the right arm of the snowman! If we decide to move the arm a bit, it'll automatically be reflected in the offset of the right arm.

We can use the new opFlipX operation for the right eye of the snowman as well. Let's create a custom SDF for an eye of the snowman.

glsl
Copied! ⭐️
float sdEye(vec3 p) {
  return sdSphere(p, .1, vec3(-0.2, 0.6, 0.7));
}

Next, we can use it inside the scene function to draw both the left eye and right eye.

glsl
Copied! ⭐️
float scene(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));

  float leftEye = sdEye(p);
  float rightEye = sdEye(opFlipX(p));

  float noseAngle = radians(75.);
  float nose = sdCone(p, vec2(sin(noseAngle), cos(noseAngle)), 0.5, vec3(0, 0.4, 1.2));

  float leftArm = sdArm(p);
  float rightArm = sdArm(opFlipX(p));

  float d = opSmoothUnion(bottomSnowball, topSnowball, 0.2);
  d = opUnion(d, leftEye);
  d = opUnion(d, rightEye);
  d = opUnion(d, nose);
  d = opUnion(d, leftArm);
  d = opUnion(d, rightArm);
  return d;
}

The snowman looks great so far, but it's missing some pizazz. It could be great if the snowman had a top hat. We can simulate a top hat by combining two cylinders together. For that, we'll need to grab the cylinder SDF titled "Capped Cylinder - exact" from Inigo Quilez's list of 3D SDFs.

glsl
Copied! ⭐️
float sdCappedCylinder( vec3 p, float h, float r )
{
  vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(h,r);
  return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}

Make sure to add an offset, so we can move the hat around in 3D space.

glsl
Copied! ⭐️
float sdCappedCylinder( vec3 p, float h, float r, vec3 offset )
{
  p -= offset;
  vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(h,r);
  return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}

We can create a thin cylinder for the bottom part of the hat, and a tall cylinder for the top part of the hat.

glsl
Copied! ⭐️
float scene(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));

  float leftEye = sdEye(p);
  float rightEye = sdEye(opFlipX(p));

  float noseAngle = radians(75.);
  float nose = sdCone(p, vec2(sin(noseAngle), cos(noseAngle)), 0.5, vec3(0, 0.4, 1.2));

  float leftArm = sdArm(p);
  float rightArm = sdArm(opFlipX(p));

  float hatBottom = sdCappedCylinder(p, 0.5, 0.05, vec3(0, 1.2, 0));
  float hatTop = sdCappedCylinder(p, 0.3, 0.3, vec3(0, 1.5, 0));

  float d = opSmoothUnion(bottomSnowball, topSnowball, 0.2);
  d = opUnion(d, leftEye);
  d = opUnion(d, rightEye);
  d = opUnion(d, nose);
  d = opUnion(d, leftArm);
  d = opUnion(d, rightArm);
  d = opUnion(d, hatBottom);
  d = opUnion(d, hatTop);
  return d;
}

Shadertoy canvas with a bright purple background. A snowman with a tint of orange is drawn in the center of the canvas. It has has two spheres for eyes, a cone for a nose, two arms that are shaped like tree branches, and a top hat made out of a thin cylinder (bottom) and thick

Our snowman is looking dapper now! 😃

Organizing Code with Custom SDFs

When we color the snowman, we'll need to target the individual parts of the snowman that have unique colors. We can organize the code by creating custom SDFs for each part of the snowman that will have a unique color.

Let's create an SDF called sdBody for the body of the snowman.

glsl
Copied! ⭐️
float sdBody(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));

  return opSmoothUnion(bottomSnowball, topSnowball, 0.2);
}

We already created an SDF for the eyes called sdEyes, but we need to create an SDF for the nose. Create a new function called sdNose with the following contents.

glsl
Copied! ⭐️
float sdNose(vec3 p) {
  float noseAngle = radians(75.);
  return sdCone(p, vec2(sin(noseAngle), cos(noseAngle)), 0.5, vec3(0, 0.4, 1.2));
}

We already created a custom SDF for the arms, but let's create one for the hat called sdHat with the following code.

glsl
Copied! ⭐️
float sdHat(vec3 p) {
  float hatBottom = sdCappedCylinder(p, 0.5, 0.05, vec3(0, 1.2, 0));
  float hatTop = sdCappedCylinder(p, 0.3, 0.3, vec3(0, 1.5, 0));

  return opUnion(hatBottom, hatTop);
}

Now, we can adjust our scene function to use all of our custom SDFs that already take account for the offset or position of each part of the snowman inside the function declaration.

glsl
Copied! ⭐️
float scene(vec3 p) {
  float body = sdBody(p);
  float leftEye = sdEye(p);
  float rightEye = sdEye(opFlipX(p));
  float nose = sdNose(p);
  float leftArm = sdArm(p);
  float rightArm = sdArm(opFlipX(p));
  float hat = sdHat(p);

  float d = body;
  d = opUnion(d, leftEye);
  d = opUnion(d, rightEye);
  d = opUnion(d, nose);
  d = opUnion(d, leftArm);
  d = opUnion(d, rightArm);
  d = opUnion(d, hat);
  return d;
}

Looks much cleaner now! There's one more thing we can do to make this code a bit more abstract. If we plan on drawing multiple snowmen to the scene, then we should create a custom SDF that draws an entire snowman. Let's create a new function called sdSnowman that does just that.

glsl
Copied! ⭐️
float sdSnowman(vec3 p) {
  float body = sdBody(p);
  float leftEye = sdEye(p);
  float rightEye = sdEye(opFlipX(p));
  float nose = sdNose(p);
  float leftArm = sdArm(p);
  float rightArm = sdArm(opFlipX(p));
  float hat = sdHat(p);

  float d = body;
  d = opUnion(d, leftEye);
  d = opUnion(d, rightEye);
  d = opUnion(d, nose);
  d = opUnion(d, leftArm);
  d = opUnion(d, rightArm);
  d = opUnion(d, hat);
  return d;
}

Finally, our scene function will simply return the value of snowman SDF.

glsl
Copied! ⭐️
float scene(vec3 p) {
 return sdSnowman(p);
}

Our snowman is now built and ready to be colored! You can find the finished code for this entire scene below.

glsl
Copied! ⭐️
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;
const float PI = 3.14159265359;
const vec3 COLOR_BACKGROUND = vec3(.741, .675, .82);
const vec3 COLOR_AMBIENT = vec3(0.42, 0.20, 0.1);

mat2 rotate2d(float theta) {
  float s = sin(theta), c = cos(theta);
  return mat2(c, -s, s, c);
}

float opUnion(float d1, float d2) {
  return min(d1, d2);
}

float opSmoothUnion(float d1, float d2, float k) {
  float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
  return mix( d2, d1, h ) - k*h*(1.0-h);
}

vec3 opFlipX(vec3 p) {
  p.x *= -1.;
  return p;
}

float sdSphere(vec3 p, float r, vec3 offset)
{
  return length(p - offset) - r;
}

float sdCone( vec3 p, vec2 c, float h, vec3 offset )
{
  p -= offset;
  float q = length(p.xy);
  return max(dot(c.xy,vec2(q,p.z)),-h-p.z);
}

float sdCapsule( vec3 p, vec3 a, vec3 b, float r, vec3 offset )
{
  p -= offset;
  vec3 pa = p - a, ba = b - a;
  float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
  return length( pa - ba*h ) - r;
}

float sdCappedCylinder( vec3 p, float h, float r, vec3 offset )
{
  p -= offset;
  vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(h,r);
  return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}

float sdBody(vec3 p) {
  float bottomSnowball = sdSphere(p, 1., vec3(0, -1, 0));
  float topSnowball = sdSphere(p, 0.75, vec3(0, 0.5, 0));

  return opSmoothUnion(bottomSnowball, topSnowball, 0.2);
}

float sdEye(vec3 p) {
  return sdSphere(p, .1, vec3(-0.2, 0.6, 0.7));
}

float sdNose(vec3 p) {
  float noseAngle = radians(75.);
  return sdCone(p, vec2(sin(noseAngle), cos(noseAngle)), 0.5, vec3(0, 0.4, 1.2));
}

float sdArm(vec3 p) {
  float mainBranch = sdCapsule(p, vec3(0, 0.5, 0), vec3(0.8, 0, 0.), 0.05, vec3(-1.5, -0.5, 0));
  float smallBranchBottom = sdCapsule(p, vec3(0, 0.1, 0), vec3(0.5, 0, 0.), 0.05, vec3(-2, 0, 0));
  float smallBranchTop = sdCapsule(p, vec3(0, 0.3, 0), vec3(0.5, 0, 0.), 0.05, vec3(-2, 0, 0));

  float d = opUnion(mainBranch, smallBranchBottom);
  d = opUnion(d, smallBranchTop);
  return d;
}

float sdHat(vec3 p) {
  float hatBottom = sdCappedCylinder(p, 0.5, 0.05, vec3(0, 1.2, 0));
  float hatTop = sdCappedCylinder(p, 0.3, 0.3, vec3(0, 1.5, 0));

  return opUnion(hatBottom, hatTop);
}

float sdSnowman(vec3 p) {
  float body = sdBody(p);
  float leftEye = sdEye(p);
  float rightEye = sdEye(opFlipX(p));
  float nose = sdNose(p);
  float leftArm = sdArm(p);
  float rightArm = sdArm(opFlipX(p));
  float hat = sdHat(p);

  float d = body;
  d = opUnion(d, leftEye);
  d = opUnion(d, rightEye);
  d = opUnion(d, nose);
  d = opUnion(d, leftArm);
  d = opUnion(d, rightArm);
  d = opUnion(d, hat);
  return d;
}

float scene(vec3 p) {
 return sdSnowman(p);
}

float rayMarch(vec3 ro, vec3 rd) {
  float depth = MIN_DIST;
  float d; // distance ray has travelled

  for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
    vec3 p = ro + depth * rd;
    d = scene(p);
    depth += d;
    if (d < PRECISION || depth > MAX_DIST) break;
  }

  d = depth;

  return d;
}

vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1, -1) * EPSILON;
    return normalize(
      e.xyy * scene(p + e.xyy) +
      e.yyx * scene(p + e.yyx) +
      e.yxy * scene(p + e.yxy) +
      e.xxx * scene(p + e.xxx));
}

mat3 camera(vec3 cameraPos, vec3 lookAtPoint) {
    vec3 cd = normalize(lookAtPoint - cameraPos);
    vec3 cr = normalize(cross(vec3(0, 1, 0), cd));
    vec3 cu = normalize(cross(cd, cr));

    return mat3(-cr, cu, -cd);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec2 mouseUV = iMouse.xy/iResolution.xy;

  if (mouseUV == vec2(0.0)) mouseUV = vec2(0.5); // trick to center mouse on page load

  vec3 col = vec3(0);
  vec3 lp = vec3(0); // lookat point
  vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position

  float cameraRadius = 2.;
  ro.yz = ro.yz * cameraRadius * rotate2d(mix(-PI/2., PI/2., mouseUV.y));
  ro.xz = ro.xz * rotate2d(mix(-PI, PI, mouseUV.x)) + vec2(lp.x, lp.z);

  vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction

  float d = rayMarch(ro, rd); // signed distance value to closest object

  if (d > MAX_DIST) {
    col = COLOR_BACKGROUND; // ray didn't hit anything
  } else {
    vec3 p = ro + rd * d; // point discovered from ray marching
    vec3 normal = calcNormal(p); // surface normal

    vec3 lightPosition = vec3(0, 2, 2);
    vec3 lightDirection = normalize(lightPosition - p) * .65; // The 0.65 is used to decrease the light intensity a bit

    float dif = clamp(dot(normal, lightDirection), 0., 1.) * 0.5 + 0.5; // diffuse reflection mapped to values between 0.5 and 1.0

    col = vec3(dif) + COLOR_AMBIENT;
  }

  fragColor = vec4(col, 1.0);
}

Coloring the Snowman

Now that we have the model of snowman built, let's add some color! We can declare some constants at the top of our code. We already have constants declared for the background color and ambient color in our scene. Let's add colors for each part of the snowman.

glsl
Copied! ⭐️
const vec3 COLOR_BACKGROUND = vec3(.741, .675, .82);
const vec3 COLOR_AMBIENT = vec3(0.42, 0.20, 0.1);
const vec3 COLOR_BODY = vec3(1);
const vec3 COLOR_EYE = vec3(0);
const vec3 COLOR_NOSE = vec3(0.8, 0.3, 0.1);
const vec3 COLOR_ARM = vec3(0.2);
const vec3 COLOR_HAT = vec3(0);

Take note that the final color of the snowman is currently determined by Lambertian diffuse reflection plus the ambient color. Therefore, the color we defined in our constants will be blended with the ambient color. If you prefer, you can remove the ambient color to see the true color of each part of the snowman.

glsl
Copied! ⭐️
float dif = clamp(dot(normal, lightDirection), 0., 1.) * 0.5 + 0.5;
col = vec3(dif) + COLOR_AMBIENT;

As we learned in Part 7 of my Shadertoy tutorial series, we can use structs to hold multiple values. We'll create a new struct that will hold the "signed distance" from the camera to the surface of an object in our scene and the color of that surface.

glsl
Copied! ⭐️
struct Surface {
  float sd; // signed distance
  vec3 col; // diffuse color
};

We'll have to make changes to a few operations, so they return Surface structs instead of just float values.

For the opUnion operation, we will actually overload this function. We'll keep the original function intact, but create a new opUnion function that passes in Surface structs instead of floats.

glsl
Copied! ⭐️
float opUnion(float d1, float d2) {
  return min(d1, d2);
}

Surface opUnion(Surface d1, Surface d2) {
  if (d2.sd < d1.sd) return d2;
  return d1;
}

Function overloading is quite common across different programming languages. It lets us define the same function name, but we can pass in a different number of parameters or different types of parameters. Therefore, if we call opUnion with float values, then it'll call the first function definition. If we call opUnion with Surface structs, then it'll call the second definition.

For the opSmoothUnion function, we won't need to overload this function. We will change this function to accept Surface structs instead of float values. Therefore, we need to call mix on both the signed distance, sd, and the color, col. This lets us smoothly blend two shapes together and blend their colors together as well.

glsl
Copied! ⭐️
Surface opSmoothUnion( Surface d1, Surface d2, float k ) {
  Surface s;
  float h = clamp( 0.5 + 0.5*(d2.sd-d1.sd)/k, 0.0, 1.0 );
  s.sd = mix( d2.sd, d1.sd, h ) - k*h*(1.0-h);
  s.col = mix( d2.col, d1.col, h ) - k*h*(1.0-h);

  return s;
}

We'll leave the SDFs for the primitive shapes (sphere, cone, capsule, cylinder) alone. They will continue to return a float value. However, we'll need to adjust our custom SDFs that return a part of the snowman. We want to return a Surface struct that contains a color for each part of our snowman, so we can pass along the color value during our ray marching loop.

glsl
Copied! ⭐️
Surface sdBody(vec3 p) {
  Surface bottomSnowball = Surface(sdSphere(p, 1., vec3(0, -1, 0)), COLOR_BODY);
  Surface topSnowball = Surface(sdSphere(p, 0.75, vec3(0, 0.5, 0)), COLOR_BODY);

  return opSmoothUnion(bottomSnowball, topSnowball, 0.2);
}

Surface sdEye(vec3 p) {
  float d = sdSphere(p, .1, vec3(-0.2, 0.6, 0.7));
  return Surface(d, COLOR_EYE);
}

Surface sdNose(vec3 p) {
  float noseAngle = radians(75.);
  float d = sdCone(p, vec2(sin(noseAngle), cos(noseAngle)), 0.5, vec3(0, 0.4, 1.2));
  return Surface(d, COLOR_NOSE);
}

Surface sdArm(vec3 p) {
  float mainBranch = sdCapsule(p, vec3(0, 0.5, 0), vec3(0.8, 0, 0.), 0.05, vec3(-1.5, -0.5, 0));
  float smallBranchBottom = sdCapsule(p, vec3(0, 0.1, 0), vec3(0.5, 0, 0.), 0.05, vec3(-2, 0, 0));
  float smallBranchTop = sdCapsule(p, vec3(0, 0.3, 0), vec3(0.5, 0, 0.), 0.05, vec3(-2, 0, 0));

  float d = opUnion(mainBranch, smallBranchBottom);
  d = opUnion(d, smallBranchTop);
  return Surface(d, COLOR_ARM);
}

Surface sdHat(vec3 p) {
  Surface bottom = Surface(sdCappedCylinder(p, 0.5, 0.05, vec3(0, 1.2, 0)), COLOR_HAT);
  Surface top = Surface(sdCappedCylinder(p, 0.3, 0.3, vec3(0, 1.5, 0)), COLOR_HAT);

  return opUnion(bottom, top);
}

Surface sdSnowman(vec3 p) {
  Surface body = sdBody(p);
  Surface leftEye = sdEye(p);
  Surface rightEye = sdEye(opFlipX(p));
  Surface nose = sdNose(p);
  Surface leftArm = sdArm(p);
  Surface rightArm = sdArm(opFlipX(p));
  Surface hat = sdHat(p);

  Surface co = body;
  co = opUnion(co, leftEye);
  co = opUnion(co, rightEye);
  co = opUnion(co, nose);
  co = opUnion(co, hat);
  co = opUnion(co, leftArm);
  co = opUnion(co, rightArm);

  return co;
}

Surface scene(vec3 p) {
  return sdSnowman(p);
}

Our ray marching loop will need adjusted, since we are now returning a Surface struct instead of a float value.

glsl
Copied! ⭐️
Surface rayMarch(vec3 ro, vec3 rd) {
  float depth = MIN_DIST;
  Surface co; // closest object

  for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
    vec3 p = ro + depth * rd;
    co = scene(p);
    depth += co.sd;
    if (co.sd < PRECISION || depth > MAX_DIST) break;
  }

  co.sd = depth;

  return co;
}

We also need to adjust the calcNormal function to use the signed distance value, sd.

glsl
Copied! ⭐️
vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1, -1) * EPSILON;
    return normalize(
      e.xyy * scene(p + e.xyy).sd +
      e.yyx * scene(p + e.yyx).sd +
      e.yxy * scene(p + e.yxy).sd +
      e.xxx * scene(p + e.xxx).sd);
}

In the mainImage function, the ray marching loop used to return a float.

glsl
Copied! ⭐️
float d = rayMarch(ro, rd);

We need to replace the above code with the following, since the ray marching loop now returns a Surface struct.

glsl
Copied! ⭐️
Surface co = rayMarch(ro, rd);

Additionally, we need to check if co.sd is greater than MAX_DIST instead of d:

glsl
Copied! ⭐️
if (co.sd > MAX_DIST)

Likewise, we need to use co instead of d when defining p:

glsl
Copied! ⭐️
vec3 p = ro + rd * co.sd;

In the mainImage function, we were setting the color equal to the diffuse color plus the ambient color.

glsl
Copied! ⭐️
col = vec3(dif) + COLOR_AMBIENT;

Now, we need to replace the above line with the following, since the color is determined by the part of the snowman hit by the ray as well.

glsl
Copied! ⭐️
col = dif * co.col + COLOR_AMBIENT;

Your finished code should look like the following:

glsl
Copied! ⭐️
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;
const float PI = 3.14159265359;
const vec3 COLOR_BACKGROUND = vec3(.741, .675, .82);
const vec3 COLOR_AMBIENT = vec3(0.42, 0.20, 0.1);
const vec3 COLOR_BODY = vec3(1);
const vec3 COLOR_EYE = vec3(0);
const vec3 COLOR_NOSE = vec3(0.8, 0.3, 0.1);
const vec3 COLOR_ARM = vec3(0.2);
const vec3 COLOR_HAT = vec3(0);

struct Surface {
  float sd; // signed distance
  vec3 col; // diffuse color
};

mat2 rotate2d(float theta) {
  float s = sin(theta), c = cos(theta);
  return mat2(c, -s, s, c);
}

float opUnion(float d1, float d2) {
  return min(d1, d2);
}

Surface opUnion(Surface d1, Surface d2) {
  if (d2.sd < d1.sd) return d2;
  return d1;
}

Surface opSmoothUnion( Surface d1, Surface d2, float k ) {
  Surface s;
  float h = clamp( 0.5 + 0.5*(d2.sd-d1.sd)/k, 0.0, 1.0 );
  s.sd = mix( d2.sd, d1.sd, h ) - k*h*(1.0-h);
  s.col = mix( d2.col, d1.col, h ) - k*h*(1.0-h);

  return s;
}

vec3 opFlipX(vec3 p) {
  p.x *= -1.;
  return p;
}

float sdSphere(vec3 p, float r, vec3 offset)
{
  return length(p - offset) - r;
}

float sdCone( vec3 p, vec2 c, float h, vec3 offset )
{
  p -= offset;
  float q = length(p.xy);
  return max(dot(c.xy,vec2(q,p.z)),-h-p.z);
}

float sdCapsule( vec3 p, vec3 a, vec3 b, float r, vec3 offset )
{
  p -= offset;
  vec3 pa = p - a, ba = b - a;
  float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
  return length( pa - ba*h ) - r;
}

float sdCappedCylinder(vec3 p, float h, float r, vec3 offset)
{
  p -= offset;
  vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(h,r);
  return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}

Surface sdBody(vec3 p) {
  Surface bottomSnowball = Surface(sdSphere(p, 1., vec3(0, -1, 0)), COLOR_BODY);
  Surface topSnowball = Surface(sdSphere(p, 0.75, vec3(0, 0.5, 0)), COLOR_BODY);

  return opSmoothUnion(bottomSnowball, topSnowball, 0.2);
}

Surface sdEye(vec3 p) {
  float d = sdSphere(p, .1, vec3(-0.2, 0.6, 0.7));
  return Surface(d, COLOR_EYE);
}

Surface sdNose(vec3 p) {
  float noseAngle = radians(75.);
  float d = sdCone(p, vec2(sin(noseAngle), cos(noseAngle)), 0.5, vec3(0, 0.4, 1.2));
  return Surface(d, COLOR_NOSE);
}

Surface sdArm(vec3 p) {
  float mainBranch = sdCapsule(p, vec3(0, 0.5, 0), vec3(0.8, 0, 0.), 0.05, vec3(-1.5, -0.5, 0));
  float smallBranchBottom = sdCapsule(p, vec3(0, 0.1, 0), vec3(0.5, 0, 0.), 0.05, vec3(-2, 0, 0));
  float smallBranchTop = sdCapsule(p, vec3(0, 0.3, 0), vec3(0.5, 0, 0.), 0.05, vec3(-2, 0, 0));

  float d = opUnion(mainBranch, smallBranchBottom);
  d = opUnion(d, smallBranchTop);
  return Surface(d, COLOR_ARM);
}

Surface sdHat(vec3 p) {
  Surface bottom = Surface(sdCappedCylinder(p, 0.5, 0.05, vec3(0, 1.2, 0)), COLOR_HAT);
  Surface top = Surface(sdCappedCylinder(p, 0.3, 0.3, vec3(0, 1.5, 0)), COLOR_HAT);

  return opUnion(bottom, top);
}

Surface sdSnowman(vec3 p) {
  Surface body = sdBody(p);
  Surface leftEye = sdEye(p);
  Surface rightEye = sdEye(opFlipX(p));
  Surface nose = sdNose(p);
  Surface leftArm = sdArm(p);
  Surface rightArm = sdArm(opFlipX(p));
  Surface hat = sdHat(p);

  Surface co = body;
  co = opUnion(co, leftEye);
  co = opUnion(co, rightEye);
  co = opUnion(co, nose);
  co = opUnion(co, hat);
  co = opUnion(co, leftArm);
  co = opUnion(co, rightArm);

  return co;
}

Surface scene(vec3 p) {
  return sdSnowman(p);
}

Surface rayMarch(vec3 ro, vec3 rd) {
  float depth = MIN_DIST;
  Surface co; // closest object

  for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
    vec3 p = ro + depth * rd;
    co = scene(p);
    depth += co.sd;
    if (co.sd < PRECISION || depth > MAX_DIST) break;
  }

  co.sd = depth;

  return co;
}

vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1, -1) * EPSILON;
    return normalize(
      e.xyy * scene(p + e.xyy).sd +
      e.yyx * scene(p + e.yyx).sd +
      e.yxy * scene(p + e.yxy).sd +
      e.xxx * scene(p + e.xxx).sd);
}

mat3 camera(vec3 cameraPos, vec3 lookAtPoint) {
    vec3 cd = normalize(lookAtPoint - cameraPos);
    vec3 cr = normalize(cross(vec3(0, 1, 0), cd));
    vec3 cu = normalize(cross(cd, cr));

    return mat3(-cr, cu, -cd);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec2 mouseUV = iMouse.xy/iResolution.xy;

  if (mouseUV == vec2(0.0)) mouseUV = vec2(0.5); // trick to center mouse on page load

  vec3 col = vec3(0);
  vec3 lp = vec3(0); // lookat point
  vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position

  float cameraRadius = 2.;
  ro.yz = ro.yz * cameraRadius * rotate2d(mix(-PI/2., PI/2., mouseUV.y));
  ro.xz = ro.xz * rotate2d(mix(-PI, PI, mouseUV.x)) + vec2(lp.x, lp.z);

  vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction

  Surface co = rayMarch(ro, rd); // closest object

  if (co.sd > MAX_DIST) {
    col = COLOR_BACKGROUND; // ray didn't hit anything
  } else {
    vec3 p = ro + rd * co.sd; // point discovered from ray marching
    vec3 normal = calcNormal(p); // surface normal

    vec3 lightPosition = vec3(0, 2, 2);
    vec3 lightDirection = normalize(lightPosition - p) * .65; // The 0.65 is used to decrease the light intensity a bit

    float dif = clamp(dot(normal, lightDirection), 0., 1.) * 0.5 + 0.5; // diffuse reflection mapped to values between 0.5 and 1.0

    col = dif * co.col + COLOR_AMBIENT;
  }

  fragColor = vec4(col, 1.0);
}

When you run this code, you should see the snowman in color!

Shadertoy canvas with a bright purple background. A snowman with a tint of orange is drawn in the center of the canvas. It has has two black spheres for eyes, an orange cone for a nose, two brown arms that are shaped like tree branches, and a dark brown top hat made out of a

Creating Multiple Snowmen

Now that we have added color to our snowman, let's create an awesome scene using our new snowman model!

The snowman model is currently floating in air. Let's add a floor of now beneath the snowman. We'll create a new custom SDF that returns a Surface struct.

glsl
Copied! ⭐️
Surface sdFloor(vec3 p) {
  float snowFloor = p.y + 2.;
  vec3 snowFloorCol = vec3(1);
  return Surface(snowFloor, snowFloorCol);
}

Then, we'll adjust our scene function to add the floor to our 3D scene.

glsl
Copied! ⭐️
Surface scene(vec3 p) {
  return opUnion(sdSnowman(p), sdFloor(p));
}

Shadertoy canvas with a bright purple background. A snowman with a tint of orange is drawn in the center of the canvas. It has has two black spheres for eyes, an orange cone for a nose, two brown arms that are shaped like tree branches, and a dark brown top hat made out of a

The colors we have chosen makes it look like it's a sunny day outside. What if we wanted to make it look like it was nighttime instead? We can adjust the ambient light color to change the mood of the scene.

glsl
Copied! ⭐️
const vec3 COLOR_AMBIENT = vec3(0.0, 0.20, 0.8) * 0.3;

Now the scene instantly appears different.

Shadertoy canvas with a bright purple background. A snowman with a tint of blue is drawn in the center of the canvas. It has has two black spheres for eyes, a dark orange cone for a nose, two black arms that are shaped like tree branches, and a black brown top hat made out of

The surface of the snow appears a bit flat. What if we wanted to add a bit of texture to it? We can use "channels" in Shadertoy to add a texture to our shader. Underneath the code section on Shadertoy, you should see four channels: iChannel0, iChannel1, iChannel2, and iChannel3.

Screenshot of part of the Shadertoy user interface showing four black boxes underneath the text area to write code. The boxes are labelled in order: iChannel0, iChannel1, iChannel2, and iChannel3.

You can use channels to add interactivity to your shader such as a webcam, microphone input, or even sound from SoundCloud! In our case, we want to add a texture. Click on the box for iChannel0. You should see a modal pop up. Click on the "Textures" tab, and you should see a selection of textures to choose from.

A popup modal that appears when the user clicks on a channel in Shadertoy. The Textures tab is selected. Nine textures are visible in a 3 by 3 grid. The Gray Noise Small texture should be selected. It is in the third row, second column.

Select the texture called "Gray Noise Small." Once selected, it should appear in the iChannel0 box beneath your code.

Screenshot of part of the Shadertoy user interface showing four black boxes underneath the text area to write code. The boxes are labelled in order: iChannel0, iChannel1, iChannel2, and iChannel3. After selecting the Gray Noise Small texture, it will appear in the box for

Noise lets us add a bit of fake randomness or "pseudorandomness" to our code. It's not truly random because the shader will look the same upon every run. This makes the shader deterministic, which is useful for making sure everyone sees the same shader. Noise will make it seem like the floor has a "random" pattern. We don't have access to anything like Math.random in GLSL code like we do in JavaScript. Therefore, shader authors typically have to rely on procedurally generating noise through an algorithm or by utilizing textures from images like what we're going to do.

Go back to the sdFloor function we defined earlier and replace it with the following code.

glsl
Copied! ⭐️
Surface sdFloor(vec3 p) {
  float snowFloor = p.y + 2. + texture(iChannel0, p.xz).x * 0.01;
  vec3 snowFloorCol = 0.85 * mix(vec3(1.5), vec3(1), texture(iChannel0, p.xz/100.).x);
  return Surface(snowFloor, snowFloorCol);
}

The texture function lets us access the texture stored in iChannel0. Each texture has a set of UV coordinates much like the Shadertoy canvas. The first parameter of the texture function will be iChannel0. The second parameter is the point on the "Gray Noise Small" image we would like to select.

We can adjust the height of the floor by sampling values from the texture.

glsl
Copied! ⭐️
float snowFloor = p.y + 2. + texture(iChannel0, p.xz).x * 0.01;

We can also adjust the color of the floor by sampling values from the texture.

glsl
Copied! ⭐️
vec3 snowFloorCol = 0.85 * mix(vec3(1.5), vec3(1), texture(iChannel0, p.xz/100.).x);

I played around with scaling factors and values in the mix function until I found a material that looked close enough to snow.

Shadertoy canvas with a bright purple background. A snowman with a tint of blue is drawn in the center of the canvas. It has has two black spheres for eyes, a dark orange cone for a nose, two black arms that are shaped like tree branches, and a black brown top hat made out of

The snowman looks a bit lonely, so why not give him some friends! We can use the opRep operation I discussed in Part 14 of my Shadertoy tutorial series to create lots of snowmen!

glsl
Copied! ⭐️
Surface opRep(vec3 p, vec3 c)
{
  vec3 q = mod(p+0.5*c,c)-0.5*c;
  return sdSnowman(q);
}

In the scene function, we can set the spacing between the snowmen and set the directions the snowmen should repeat.

glsl
Copied! ⭐️
Surface scene(vec3 p) {
  Surface sdSnowmen = opRep(p - vec3(0, 0, -2), vec3(5, 0, 5));

  return opUnion(sdSnowmen, sdFloor(p));
}

The snowman is no longer alone! However, one snowman seems to be hogging all the attention in the scene.

Shadertoy canvas with a bright purple background. A group of snowmen appear in the scene. They are sitting on an textured floor that resembles snow. One snowman appears large as if it's blocking the camera.

Let's make a few adjustments. We'll change the default position of the mouse when the page loads, so it's slightly offset from the center of the screen.

glsl
Copied! ⭐️
if (mouseUV == vec2(0.0)) mouseUV = vec2(0.5, 0.4);

Next, we'll adjust the lookat point:

glsl
Copied! ⭐️
vec3 lp = vec3(0, 0, -2);

Finally, we'll adjust the starting angle and position of the scene when the page loads:

glsl
Copied! ⭐️
Surface scene(vec3 p) {
  p.x -= 0.75; // move entire scene slightly to the left
  p.xz *= rotate2d(0.5); // start scene at an angle

  Surface sdSnowmen = opRep(p - vec3(0, 0, -2), vec3(5, 0, 5));

  return opUnion(sdSnowmen, sdFloor(p));
}

Now, the scene is setup such that people visiting your shader for the first time will see a bunch of snowmen without one of the snowman getting in the way of the camera. You can still use your mouse to rotate the camera around the scene.

Shadertoy canvas with a bright purple background. A group of snowmen appear in the scene. They are sitting on an textured floor that resembles snow. The scene is now tilted. The snowmen are no longer facing the viewer and are pointing in different directions. The snowman in

The scene is starting to look better, but as you look down the isle of snowmen, it looks too artificial. Let's add some fog to add a sense of depth to our scene. We learned about fog in Part 13 of my Shadertoy series. Right before the final fragColor value is set, add the following line:

glsl
Copied! ⭐️
col = mix(col, COLOR_BACKGROUND, 1.0 - exp(-0.00005 * co.sd * co.sd * co.sd)); // fog

Shadertoy canvas with a bright purple background and fog that blends in with the background. A group of snowmen appear in the scene. They are sitting on an textured floor that resembles snow. The scene is now tilted. The snowmen are no longer facing the viewer and are pointing

Much better! The snowmen seem to be facing away from the light. Let's change the light direction, so they appear brighter. Inside the mainImage function, we'll adjust the value of the light position.

glsl
Copied! ⭐️
vec3 lightPosition = vec3(0, 2, 0);

We'll also make the color of each snowman's body and hat a bit brighter.

glsl
Copied! ⭐️
const vec3 COLOR_BODY = vec3(1.15);
const vec3 COLOR_HAT = vec3(0.4);

Shadertoy canvas with a bright purple background and fog that blends in with the background. A group of snowmen appear in the scene. They are sitting on an textured floor that resembles snow. The scene is now tilted. The snowmen are no longer facing the viewer and are pointing

Their hats look more noticeable now! Next, let's make the snowmen a bit more lively. We'll wiggle them a bit and have them bounce up and down.

We can cause them to wiggle a bit by applying a transformation matrix to each snowman. Create a function called wiggle and use the rotateZ function I discussed in Part 8.

glsl
Copied! ⭐️
mat3 rotateZ(float theta) {
  float c = cos(theta);
  float s = sin(theta);
  return mat3(
    vec3(c, -s, 0),
    vec3(s, c, 0),
    vec3(0, 0, 1)
  );
}

mat3 wiggle() {
  return rotateZ(mix(-0.01, 0.01, cos(iTime * SPEED)));
}

We'll define a SPEED constant at the top of our code. Let's set it to a value of four.

glsl
Copied! ⭐️
const float SPEED = 4.;

Then, we'll apply the wiggle function inside the opRep function, so it's applied to each snowman.

glsl
Copied! ⭐️
Surface opRep(vec3 p, vec3 c)
{
  vec3 q = mod(p+0.5*c,c)-0.5*c;
  return sdSnowman(q * wiggle());
}

Next, we want the snowmen to bounce up and down a bit. We can add the following line to our scene function.

glsl
Copied! ⭐️
p.y *= mix(1., 1.03, sin(iTime * SPEED));

This will deform the snowmen about the y-axis by a tiny amount. We use the mix function to remap the value of the sin function to values between 1.0 and 1.03.

Your scene function should now look like the following.

glsl
Copied! ⭐️
Surface scene(vec3 p) {
  p.x -= 0.75; // move entire scene slightly to the left
  p.xz *= rotate2d(0.5); // start scene at an angle
  p.y *= mix(1., 1.03, sin(iTime * SPEED)); // bounce snowman up and down a bit

  Surface sdSnowmen = opRep(p - vec3(0, 0, -2), vec3(5, 0, 5));

  return opUnion(sdSnowmen, sdFloor(p));
}

When you run the code, you should see the snowmen start wiggling!

Shadertoy canvas with multiple snowman wiggling on top of snow in an amazing winter scene. The sky is a light purple and purple fog blends in with the background giving the 3D scene a sense of depth.

Finally, we can "let it snow" by overlaying falling snow on top of the scene. There are already plenty of great snow shaders out there on Shadertoy. We'll use "snow snow" by the Shadertoy author, changjiu. Always make sure you give credit to authors when using their shaders. If you're using an author's shader for commercial applications such as a game, make sure to ask their permission first!

Inside Shadertoy, we can use channels to add a buffer similar to how we added a texture earlier. Buffers let you create "multi-pass" shaders that let you pass the output or final color of each pixel of one shader to another shader. Think of it as a shader pipeline. We can pass the output of Buffer A to the main program running in the "Image" tab in your Shadertoy environment.

Click on the iChannel1 box in the section underneath your code. A popup should appear. Click on the "Misc" tab and select Buffer A.

A popup modal that appears when the user clicks on a channel in Shadertoy. The Misc tab is selected. Nine items are visible in a 3 by 3 grid. Buffer A should be selected. It is in the second row, second column.

Once you add Buffer A, you should see it appear in the iChannel1 box.

Screenshot of part of Shadertoy showing four boxes for each channel. The boxes are labelled in order: iChannel0, iChannel1, iChannel2, and iChannel3. The Gray Noise Small texture is being used for iChannel0. After selecting Buffer A, it will appear in the box for iChannel1.

Next, we need to create the Buffer A shader. Then, we'll add code inside of this shader pass. At the top of your screen, you should see a tab that says "Image" above your code. To the left of that, you will find a tab with a plus sign (+). Click on the plus sign, and choose "Buffer A" in the dropdown that appears.

Part of the Shadertoy user interface. A tab with a plus sign is visible to the left of a tab labelled Image.

Inside Buffer A, add the following code:

glsl
Copied! ⭐️
/*
** Buffer A
** Credit: This buffer contains code forked from "snow snow" by changjiu: https://www.shadertoy.com/view/3ld3zX
*/

float SIZE_RATE = 0.1;
float XSPEED = 0.5;
float YSPEED = 0.75;
float LAYERS = 10.;

float Hash11(float p)
{
  vec3 p3 = fract(vec3(p) * 0.1);
  p3 += dot(p3, p3.yzx + 19.19);
  return fract((p3.x + p3.y) * p3.z);
}

vec2 Hash22(vec2 p)
{
  vec3 p3 = fract(vec3(p.xyx) * 0.3);
  p3 += dot(p3, p3.yzx+19.19);
  return fract((p3.xx+p3.yz)*p3.zy);
}

vec2 Rand22(vec2 co)
{
  float x = fract(sin(dot(co.xy ,vec2(122.9898,783.233))) * 43758.5453);
  float y = fract(sin(dot(co.xy ,vec2(457.6537,537.2793))) * 37573.5913);
  return vec2(x,y);
}

vec3 SnowSingleLayer(vec2 uv,float layer){
  vec3 acc = vec3(0.0,0.0,0.0);
  uv = uv * (2.0 + layer);
  float xOffset = uv.y * (((Hash11(layer)*2.-1.)*0.5+1.)*XSPEED);
  float yOffset = YSPEED * iTime;
  uv += vec2(xOffset,yOffset);
  vec2 rgrid = Hash22(floor(uv)+(31.1759*layer));
  uv = fract(uv) - (rgrid*2.-1.0) * 0.35 - 0.5;
  float r = length(uv);
  float circleSize = 0.04*(1.5+0.3*sin(iTime*SIZE_RATE));
  float val = smoothstep(circleSize,-circleSize,r);
  vec3 col = vec3(val,val,val)* rgrid.x ;
  return col;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;

  vec3 acc = vec3(0,0,0);
  for (float i = 0.; i < LAYERS; i++) {
    acc += SnowSingleLayer(uv,i);
  }

  fragColor = vec4(acc,1.0);
}

Then, go back to the "Image" tab where our main shader code lives. At the bottom of our code, we're going to use Buffer A to add falling snow to our scene in front of all the snowmen. Right after the fog and before the final fragColor is set, add the following line:

glsl
Copied! ⭐️
col += texture(iChannel1, fragCoord/iResolution.xy).rgb;

We use the texture function to access iChannel1 that holds the Buffer A texture. The second parameter of the texture function will be normal UV coordinates that go from zero to one. This will let us access each pixel of the shader in Buffer A as if it were an image.

Once you run the code, you should see an amazing winter scene with wiggling snowmen and falling snow! Congratulations! You did it! 🎉🎉🎉

Shadertoy canvas with multiple snowman wiggling on top of snow in an amazing winter scene. The sky is a light purple and purple fog blends in with the background giving the 3D scene a sense of depth. Snow is falling all around the snowmen.

You can see the finished code by visiting my shader on Shadertoy. Don't forget! You can use one of the channels to add music to your shader by selecting SoundCloud and pasting a URL in the input field.

Conclusion

I hope you had fun building a snowman model, learning how to color it, and then drawing multiple snowmen to a beautiful scene with falling snow. You learned how to use ray marching to build a 3D model, add a textured floor to a 3D scene, add fog to give your scene a sense of depth, and use buffers to create a multi-pass shader!

If this helped you in any way or inspired you, please consider donating. Please check out the resources for the finished code for each part of this tutorial. Until next time, happy coding! Stay inspired!!!

Resources