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 )
{