Snowman Shader in Shadertoy
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.
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?
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.
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.
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);
}
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.
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
.
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;
}
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.
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;
}
The right eye will use the same offset value as the left eye except the x-axis will be mirrored.
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;
}
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:
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
.
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.
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!
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;
}
You can use your mouse to move the camera around the snowman to make sure the cone looks fine.
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.
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.
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.
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;
}
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.
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;
}
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
.
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.
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.
vec3 opFlipX(vec3 p) {
p.x *= -1.;
return p;
}
Then, we can use this function inside the scene
function to draw the right arm.
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;
}
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.
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.
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.
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.
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.
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;
}
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
.
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
.
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.
Surface co = rayMarch(ro, rd);
Additionally, we need to check if co.sd
is greater than MAX_DIST
instead of d
:
if (co.sd > MAX_DIST)
Likewise, we need to use co
instead of d
when defining p
:
vec3 p = ro + rd * co.sd;
In the mainImage
function, we were setting the color equal to the diffuse color plus the ambient color.
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.
col = dif * co.col + COLOR_AMBIENT;
Your finished code should look like the following:
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 )
{