https://danielchasehooper.com/posts/code-animated-rick/ Daniel Hooper Home Posts Projects Twitter/X Bluesky Learn Shader Programming with Rick and Morty February 4, 2025 The interactive features and visualizations of this post won't work because Javascript isn't enabled in your browser. Try disabling browser extensions and content blockers, or view this page in another browser. This animation of Rick was made with 240 lines of code. No libraries, no images. I'm going to show you how to use GPU shaders and signed distance fields to make animations like it for videos, video games, or just for fun! I even built a live coding editor into the page so you can see the examples running and tinker with them. vec2 rotateAt(vec2 p, float angle, vec2 origin) { float s = sin(angle), c = cos(angle); return (p-origin)*mat2( c, -s, s, c ) + origin; } float map(float value, float inMin, float inMax, float outMin, float outMax) { value = clamp(value, inMin, inMax); return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin); } vec2 grad(ivec2 z) { int n = z.x+z.y*11111; n = (n<<13)^n; n = (n*(n*n*15731+789221)+1376312589)>>16; n &= 7; vec2 gr = vec2(n&1,n>>1)*2.0-1.0; return ( n>=6 ) ? vec2(0.0,gr.x) : ( n>=4 ) ? vec2(gr.x,0.0) : gr; } float noise(vec2 p) { ivec2 i = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x), mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y); } vec2 warp(vec2 p, float scale, float strength) { float offsetX = noise(p * scale + vec2(0.0, 100.0)); float offsetY = noise(p * scale + vec2(100.0, 0.0)); return p + vec2(offsetX, offsetY) * strength; } float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { vec2 i = v0 - v2; vec2 j = v2 - v1; vec2 k = v1 - v0; vec2 w = j-k; v0-= p; v1-= p; v2-= p; float x = v0.x*v2.y-v0.y*v2.x; float y = v1.x*v0.y-v1.y*v0.x; float z = v2.x*v1.y-v2.y*v1.x; vec2 s = 2.0*(y*j+z*k)-x*i; float r = (y*z-x*x*0.25)/dot(s,s); float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0); vec2 d = v0+t*(k+k+t*w); vec2 outQ = d + p; return length(d); } float parabola(vec2 pos, float k) { // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } float round_rect(vec2 p, vec2 b, vec4 r) { r.xy = (p.x>0.0)?r.xy : r.zw; r.x = (p.y>0.0)?r.x : r.y; vec2 q = abs(p)-b+r.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; } float star(vec2 p, float r, float points, float ratio) { // next 4 lines can be precomputed for a given shape float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } #define H(i,j) fract(sin(dot(ceil(P+vec2(i,j)), resolution.xy )) * 4e3) float N( vec2 P) { float s,i,w = .5; for (; i < 3. ; i++, w *= .4, P *= 1.9 ) { vec2 F = fract( P *= mat2(.866,-.5,.5,.866) ); F *= F*(3.-F-F); s += w* mix( mix(H(0,0) , H(1,0), F.x), mix(H(0,1) , H(1,1), F.x), F.y ); } return s; } vec3 portal(vec2 pixel, float time) { float l = length( pixel ), a = atan(pixel.y, pixel.x) / 6.28 + .5, k = 10.; a = fract(a + l*.3 - time*.01 ); vec2 U = vec2( l+time*.3, a ); return vec3[]( vec3(.18, .53, .09), vec3(.56, .89, .16), vec3(.35, .84, .11), vec3(.92, .98, .85) ) [ int( 4.* pow( mix( N(U*k), N(U*k-vec2(0,k)), U.y) * 1.5, 2.5))]; } vec3 color_for_pixel(vec2 pixel, float time) { { // rotate the whole drawing pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6)); pixel.y += .1; // Blink eyes if (mod(time+1., 3.) < .09) { // closed eyes float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0)); if (d < .008) return vec3(0); } else { // move pupils randomly vec2 pupil_warp = pixel + vec2(.095,-.18); pupil_warp.x -= noise(vec2(round(time)*7.+.5, 0.5))*.1; pupil_warp.y -= noise(vec2(round(time)*9.+.5, 0.5))*.1; pupil_warp.x = abs(pupil_warp.x) - .16; float d = star(pupil_warp, 0.019, 6., .9); if (d < 0.007) { return vec3(.1); } // Eyeballs vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16); d = length(eye) - .16; if (d < 0.) return vec3(step(.013, -d)); // under eye lines bool should_show = pixel.y < 0.25 && (abs(pixel.x+.29) < .05 || abs(pixel.x-.12) < .085); if (abs(d - .04) < .0055 && should_show) return vec3(0); } // Mouth float d = bezier(pixel, vec2(-.26, -.28), vec2(-.05,-.42), vec2(.115, -.25)); if (d < .11) { // Teeth float width = .065; vec2 teeth = pixel; teeth.x = mod(teeth.x, width)-width*.5; teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34; teeth.y = abs(teeth.y)-.06; d = parabola(teeth, 38.); if (d < 0. && abs(pixel.x+.06) < .194) return vec3(0.902, 0.890, 0.729)*step(d, -.01); // Tongue // `map()` is used to change the thickness of // the tongue along the x axis vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5)); float tongue_thickness = map(tongue.x, -.16, .01, .02, .045); d = bezier(tongue, vec2(-.16, -.35), vec2(.001,-.33), vec2(.01, -.5)) - tongue_thickness; if (d < 0.0) return vec3(0.816, 0.302, 0.275)*step(d, -0.01); // mouth fill color return vec3(.42, .147, .152); } // lip outlines if (d < .12 || (abs(d-.16) < .005 && (pixel.x*-6.4 > -pixel.y+1.6 || pixel.x*1.7 > -pixel.y+.1 || pixel.y < -0.49))) return vec3(0); // lips if (d < .16) return vec3(.838, .799, 0.76); // Nose d = min( bezier(pixel, vec2(-.15, -.13), vec2(-.21,-.14), vec2(-.14, .08)), bezier(pixel, vec2(-.085, -.01), vec2(-.12, -.13), vec2(-.15,-.13))); if (d < 0.0055) return vec3(0); // Eyebrow d = bezier(pixel, vec2(-.34, .38), // NEW animate the middle up and down vec2(-.05, 0.5 + cos(time)*.1), vec2(.205, .36)) - 0.035; if (d < 0.0) return vec3(.71, .839, .922)*step(d, -.013); d = min( // Head round_rect( pixel, vec2(.36, .6385), vec4(.34, .415, .363, .315)), // Ear round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)) ); if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01); // Hair float twist = sin(time*2.-length(pixel)*2.1)*.12; vec2 hair = rotateAt(pixel, twist, vec2(0.,.1)); hair -= vec2(.08,.15); hair.x *= 1.3; hair = warp(hair, 4.0, 0.07); d = star(hair, 0.95, 11., .28); if (d < 0.) { return vec3(0.682, 0.839, 0.929)*step(d, -0.012); } } return portal(pixel, time); } This is the editor I used to create Pixel Rick. It updates after every code edit -- try changing green = 0.9 to green = 0.1. vec3 color_for_pixel(vec2 pixel, float time) { // fract returns fractional part. fract(1.3) == 0.3 float red = fract(pixel.y); float green = 0.9; float blue = fract(pixel.x); return vec3(red, green, blue); } The code is written in OpenGL Shading Language (GLSL), and the color_for_pixel function runs on your GPU for every pixel in the preview. Amazingly this is all you need to make animations -- a function that answers "What color should this pixel be at this time? ". Optional Challenge: What happens if you set green = time? What could you do to make it keep going? (time counts seconds since last edit) To draw Rick, we'll start with drawing a circle and build up to other shapes. Let's use GLSL's built in^1 length() function to visualize how far each pixel is from the center of the screen (aka the origin, aka position (0,0)). By returning that distance as the pixel's color, we get 0 (black) near the center, and fade to 1 (white) further away: vec3 color_for_pixel(vec2 pixel, float time) { return vec3(length(pixel)); } GLSL Tip: vec3(x) is the same as vec3(x, x, x). We'll use this trick a lot. To draw a circle, we compare the distance to a radius: vec3 color_for_pixel(vec2 pixel, float time) { float radius = 0.6; return vec3(length(pixel) > radius); } GLSL Tip: vec3 turns the boolean result of > into 1 or 0. What would that circle look like if you replaced length() with your own function that calculates Manhattan distance? We can extract that into a reusable circle() function: float circle(vec2 pixel, float radius) { return length(pixel) - radius; } vec3 color_for_pixel(vec2 pixel, float time) { if (circle(pixel - vec2(.3, -.3), .4) < 0.0) { return vec3(0.2,.7,.5); } if (circle(pixel - vec2(-.4,0), .8) < 0.0) { return vec3(.7,.5, .3); } return vec3(.2); } The circles are positioned by shifting the pixel passed to circle(). The line order of that code is important - it determines which circle appears in front of the other. Notice that circle() returns the distance to the perimeter instead of just a bool to indicate inside/outside. This is known as a "signed distance field" (SDF) function. The word "signed" here means that the distances for locations inside the shape are negative, and positive outside. We'll use the distance to achieve some cool effects in a bit. There are many SDF functions besides circle(). Here are a few we'll be using: // Click {...} to see the code float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold // from https://www.shadertoy.com/view/MlKcDD vec2 i = v0 - v2; vec2 j = v2 - v1; vec2 k = v1 - v0; vec2 w = j-k; v0-= p; v1-= p; v2-= p; float x = v0.x*v2.y-v0.y*v2.x; float y = v1.x*v0.y-v1.y*v0.x; float z = v2.x*v1.y-v2.y*v1.x; vec2 s = 2.0*(y*j+z*k)-x*i; float r = (y*z-x*x*0.25)/dot(s,s); float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0); vec2 d = v0+t*(k+k+t*w); vec2 outQ = d + p; return length(d); } float star(vec2 p, float r, float points, float ratio) { // fold // from https://www.shadertoy.com/view/3tSGDy float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } float round_rect(vec2 p, vec2 size, vec4 radii) { // fold // from https://www.shadertoy.com/view/4llXD7 radii.xy = (p.x>0.0)?radii.xy : radii.zw; radii.x = (p.y>0.0)?radii.x : radii.y; vec2 q = abs(p)-size+radii.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x; } vec3 color_for_pixel(vec2 pixel, float time) { if (bezier(pixel, vec2(-.7,-.35), vec2(-1.5,-.4), vec2(-1.2,.35)) < 0.1) return vec3(.9,.3,.3); if (round_rect(pixel, vec2(.3, .4), vec4(.1)) < 0.0) return vec3(.3, .9, .3); if (star(pixel - vec2(1.,0.), .45, 5., .3) < 0.0) return vec3(.2, .4, .9); return vec3(1.0); } With those shapes in hand, let's get started on Rick. Drawing Rick I wish I could tell you I had the ability to look at a cartoon and then effortlessly replicate it in code. Unfortunately, I don't. I spent a lot of time painstakingly trying numbers to recreate Rick's face from the season 1 poster. I did find one trick that sped up the trial and error process: I flashed my reference image of Rick on top of the preview so I could compare my drawing to the original while I was changing the code. The editor below has that enabled so you can experience what my week has been like. Change the size and corner radii parameters to make the rectangle match Rick's head shape. float round_rect(vec2 p, vec2 size, vec4 radii) { // fold radii.xy = (p.x>0.0)?radii.xy : radii.zw; radii.x = (p.y>0.0)?radii.x : radii.y; vec2 q = abs(p)-size+radii.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x; } vec3 color_for_pixel(vec2 pixel, float time) { float dist = round_rect( pixel, // Change these: vec2(.3, .5), // size vec4(.1, .01, .05, .1) // corner radii ); if (dist < 0.) return vec3(.838, 0.8, 0.76); return vec3(1); } In case it isn't obvious by now, the techniques in this post won't be replacing your favorite vector drawing tool. This is the only time we'll do the flashing exercise; just know that all the seemingly random numbers in the rest of this post were discovered via this process. I found the color values using an image editor's eyedropper tool. Ok, so here are the values I came up with for Rick's head. I also added a second round_rect() for his ear: float round_rect(vec2 p, vec2 size, vec4 radii) { // fold radii.xy = (p.x>0.0)?radii.xy : radii.zw; radii.x = (p.y>0.0)?radii.x : radii.y; vec2 q = abs(p)-size+radii.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x; } vec3 color_for_pixel(vec2 pixel, float time) { vec3 skin_color = vec3(0.838, 0.799, 0.760); // head float dist = round_rect( pixel, vec2(.36, 0.6385), vec4(.34, .415, .363, .315) ); if (dist < 0.) return skin_color; // ear dist = round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)); if (dist < 0.) return skin_color; return vec3(1); } Let's add the outline. This is where drawing with signed distance functions comes in handy. We can return black for pixels with a distance between -0.01 and 0.0. float round_rect(vec2 p, vec2 size, vec4 radii) { // fold radii.xy = (p.x>0.0)?radii.xy : radii.zw; radii.x = (p.y>0.0)?radii.x : radii.y; vec2 q = abs(p)-size+radii.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x; } vec3 color_for_pixel(vec2 pixel, float time) { vec3 skin_color = vec3(0.838, 0.799, 0.760); // head float dist = round_rect( pixel, vec2(.36, 0.6385), vec4(.34, .415, .363, .315) ); if (dist < -0.01) return skin_color; if (dist < 0.0) return vec3(0); // outline // ear dist = round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)); if (dist < -0.01) return skin_color; if (dist < 0.0) return vec3(0); // outline return vec3(1); // background } That line between the ear and the head shouldn't be there (according to my reference image of Rick). I don't want to outline each shape individually, I want to outline the union of the shapes. Union is easy with SDFs - use min() to combine two distances: float round_rect(vec2 p, vec2 size, vec4 radii) { // fold radii.xy = (p.x>0.0)?radii.xy : radii.zw; radii.x = (p.y>0.0)?radii.x : radii.y; vec2 q = abs(p)-size+radii.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x; } vec3 color_for_pixel(vec2 pixel, float time) { float dist = min( // <- combine the shapes // head round_rect( pixel, vec2(.36, 0.6385), vec4(.34, .415, .363, .315)), // ear round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)) ); if (dist < -0.01) return vec3(0.838, 0.799, 0.760); if (dist < 0.0) return vec3(0); return vec3(1); } There are other ways to combine two signed distance fields. Try swapping out min() for the smooth union function to smoothly blend the ear with the head. Let's draw an eye: float circle(vec2 pixel, float radius) { // fold return length(pixel) - radius; } float round_rect(vec2 p, vec2 size, vec4 radii) { // fold radii.xy = (p.x>0.0)?radii.xy : radii.zw; radii.x = (p.y>0.0)?radii.x : radii.y; vec2 q = abs(p)-size+radii.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x; } float star(vec2 p, float r, float points, float ratio) { // fold float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } vec3 color_for_pixel(vec2 pixel, float time) { // pupil vec2 pupil_pos = pixel - vec2(.16-.13,.24); // subtract 0.007 to outset & round the corners of star if (star(pupil_pos, 0.019, 6., .9) - 0.007 < 0.0) { return vec3(.1); } // eyeball vec2 eyeball_pos = pixel; eyeball_pos.y *= .93; // stretch vertically eyeball_pos -= vec2(0.07, .16); float dist = circle(eyeball_pos, .16); if (dist < 0.0) return vec3(dist < -0.013); // head { // fold dist = min( // head round_rect( pixel, vec2(.36, 0.6385), vec4(.34, .415, .363, .315)), // ear round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)) ); if (dist < -0.01) return vec3(0.838, 0.799, 0.760); if (dist < 0.0) return vec3(0); // outline } return vec3(1.); } Two interesting things here: 1. eyeball_pos.y *= .93 stretches the eyeball a tiny bit -- just like we move shapes by adding to positions, we scale by multiplying positions. 2. I used a 6-point star for the eye, and I subtracted a little from the star's distance to round its corners. Any SDF shape can be rounded this way. It helps to visualize the distance field so you see how it gets more round the further from the shape you get: float star(vec2 p, float r, float points, float ratio) { // fold float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } vec3 color_for_pixel(vec2 pixel, float time) { float d = star(pixel, 0.4, 6., .5); // show blue inside shape, orange outside vec3 color = (d < 0.0) ? vec3(0.5, .8, 1.) : vec3(0.98,.6,.13); color *= sin(d*150.)*.1+.8; // show distance field lines color *= 1.0 - exp(-20.0*abs(d)); // darken near perimeter float offset = (sin(time)+1.)*.25; // animate outline offset if (abs(d-offset) < 0.01) return vec3(1.0); // draw white outline return color; } For the second eye, we could duplicate the first eye's code, but instead let's mirror it horizontally with pixel.x = abs(pixel.x). To rationalize this, consider that if the point (1, 0) is inside the circle, then it's mirror (-1, 0) will also be inside the circle after pixel.x = abs(pixel.x), so both points will get colored. float circle(vec2 pixel, float radius) { // fold return length(pixel) - radius; } vec3 color_for_pixel(vec2 pixel, float time) { pixel.x -= .3; // controls position pixel.x = abs(pixel.x); // mirror pixel.x -= .7; // controls spacing return vec3(circle(pixel, .5) > 0.0); } The way that order of operations works still hurts my head, but it helps to play with the code to get a feel for what's going on. Mirror the circles on both the x and the y axis Here is the mirroring technique applied to Rick's eyes: float circle(vec2 pixel, float radius) { // fold return length(pixel) - radius; } float round_rect(vec2 p, vec2 size, vec4 radii) { // fold radii.xy = (p.x>0.0)?radii.xy : radii.zw; radii.x = (p.y>0.0)?radii.x : radii.y; vec2 q = abs(p)-size+radii.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x; } float star(vec2 p, float r, float points, float ratio) { // fold float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } vec3 color_for_pixel(vec2 pixel, float time) { // pupils vec2 pupil_pos = pixel; pupil_pos += vec2(.13, -.24); // position pupils on eyeballs pupil_pos.x = abs(pupil_pos.x); // mirror pupils pupil_pos.x -= .16; // pupil spacing if (star(pupil_pos, 0.019, 6., .9) < 0.007) { return vec3(.1); } // eyeballs // position/mirror/scale one liner vec2 eye_pos = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16); float dist = circle(eye_pos, .16); if (dist < 0.0) return vec3(dist < -0.013); // head { // fold dist = min( // head round_rect( pixel, vec2(.36, 0.6385), vec4(.34, .415, .363, .315)), // ear round_rect( pixel + vec2(-.32, .15), vec2(.15, .12), vec4(.13, .1, .13, .13)) ); if (dist < -0.01) return vec3(0.838, 0.799, 0.760); if (dist < 0.0) return vec3(0); // outline } return vec3(1); } Let's skip ahead. The mouth, nose, and eyebrow are all created with bezier(). The hair is an 11-point star() that I stretched vertically. float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold vec2 i = v0 - v2; vec2 j = v2 - v1; vec2 k = v1 - v0; vec2 w = j-k; v0-= p; v1-= p; v2-= p; float x = v0.x*v2.y-v0.y*v2.x; float y = v1.x*v0.y-v1.y*v0.x; float z = v2.x*v1.y-v2.y*v1.x; vec2 s = 2.0*(y*j+z*k)-x*i; float r = (y*z-x*x*0.25)/dot(s,s); float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0); vec2 d = v0+t*(k+k+t*w); vec2 outQ = d + p; return length(d); } float round_rect(vec2 p, vec2 b, vec4 r) { // fold r.xy = (p.x>0.0)?r.xy : r.zw; r.x = (p.y>0.0)?r.x : r.y; vec2 q = abs(p)-b+r.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; } float circle(vec2 p, float r) { // fold return length(p) - r; } float star(vec2 p, float r, float points, float ratio) { // fold // next 4 lines can be precomputed for a given shape float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } vec3 color_for_pixel(vec2 pixel, float time) { float d; // eyes { // fold // pupils vec2 pupil_warp = pixel; pupil_warp.x = abs(pupil_warp.x +.13); pupil_warp -= vec2(.16,.24); d = star(pupil_warp, 0.019, 6., .9); if (d < 0.007) { return vec3(.1); } // eyeballs vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16); d = length(eye) - .16; if (d < 0.) { return vec3(step(0.013, -d)); } } // nose d = min( // combine the curves bezier(pixel, vec2(-.15, -.13), vec2(-.21,-.14), vec2(-.14, .08)), bezier(pixel, vec2(-.085, -.01), vec2(-.12, -.13), vec2(-.15,-.13))); if (d < 0.0055) return vec3(0); // mouth d = bezier(pixel, vec2(-.26, -.28), vec2(-.05,-.42), vec2(.115, -.25)); if (d < .12) { // The `*step(d, .11)` creates the outline. // it's the same as `*vec3(d < .11)` // aka, it multiplies the color by zero for // pixels near the perimeter return vec3(.42, .147, .152)*step(d, .11); } // eyebrow d = bezier(pixel, vec2(-.34, .38), vec2(-.05, .68), vec2(.205, .36)) - 0.035; if (d < 0.0) return vec3(.71, .839, .922)*step(d, -.013); // head { // fold float dist = min( // head round_rect( pixel, vec2(.36, .6385), vec4(.34, .415, .363, .315)), // ear round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)) ); if (dist < -.01) return vec3(.838, .799, .76); if (dist < 0.) return vec3(0); // outline } // hair d = star((pixel-vec2(.08,.15))*vec2(1.3,1.), 0.95, 11., .28); if (d < 0.) { return vec3(0.682, 0.839, 0.929)*step(0.012, -d); } return vec3(1.); } That's as far as basic shape positioning, scaling, and outlining can get us. Making the Hair Wavy The remaining steps will elevate our crude sketch of Rick into a drawing that looks exactly like him. We'll learn a few more techniques to make this possible. First up: let's fix his rigid looking hair. There isn't a "wavy hair" signed distance function, but we can make the star shape more wavy using a technique called domain warping. Domain warping randomly offsets pixel locations. That random offset is "seeded" by the pixel's location, so the offset is consistent over time for any given location. You can use that warped location for whatever shapes you want warped. Here's an 11-point star with and without warping: float star(vec2 p, float r, float points, float ratio) { // fold // next 4 lines can be precomputed for a given shape float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } // these functions are used by the `warp()` function // to generate pseudo random numbers. The details aren't // super important. I looked these functions up: // https://www.shadertoy.com/view/XdXGW8 vec2 grad(ivec2 z) { // fold int n = z.x+z.y*11111; n = (n<<13)^n; n = (n*(n*n*15731+789221)+1376312589)>>16; n &= 7; vec2 gr = vec2(n&1,n>>1)*2.0-1.0; return ( n>=6 ) ? vec2(0.0,gr.x) : ( n>=4 ) ? vec2(gr.x,0.0) : gr; } float noise(vec2 p) { // fold ivec2 i = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x), mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y); } vec2 warp(vec2 p, float scale, float strength) { float offsetX = noise(p * scale + vec2(0.0, 100.0)); float offsetY = noise(p * scale + vec2(100.0, 0.0)); return p + vec2(offsetX, offsetY) * strength; } vec3 color_for_pixel(vec2 pixel, float time) { vec2 warped_pixel = warp(pixel, 4., .07); float d = min( star(warped_pixel + vec2(.8,0), 0.7, 11., .28), star(pixel - vec2(.8,0), 0.7, 11., .28) ); if (d < 0.) { return vec3(0.682, 0.839, 0.929); } return vec3(1); } Visualize the warp offsets by drawing the x offset to the red channel and the y offset to the green channel Fun fact: the Lord of the Rings movies used domain warping to create the visual effect seen when Frodo is wearing the Ring. Their warp offsets came from tracking fire movement. Animate the warp effect above to achieve the Lord of the Rings effect. Drawing Infinite Teeth Rick needs teeth, a lot them. But we'll start by drawing one. A parabola is the best tooth shape I could find: float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } vec3 color_for_pixel(vec2 pixel, float time) { float d = parabola(pixel, 38.); if (d < 0.) return vec3(0.902, 0.890, 0.729)*step(d, -.01); return vec3(1); } Yes that is a tooth. Stick with me. Is there a way to draw multiple teeth without duplicating a bunch of code, or using a for loop? Yes! Similar to how we used abs() to mirror shapes, we can use mod() to repeat shapes. mod(a,b) calculates the reminder of a/b. Look below at what mod(pixel.x, 0.5) does. Every time pixel.x increases above a multiple of .5 , mod() starts back at zero (black) again. vec3 color_for_pixel(vec2 pixel, float time) { return vec3(mod(pixel.x, 0.5)); } Here is mod() applied to the single tooth float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } vec3 color_for_pixel(vec2 pixel, float time) { float width = .065; pixel.x = mod(pixel.x, width)-width*.5; // NEW: repeat horizontally float d = parabola(pixel, 38.); if (d < 0.) return vec3(0.902, 0.890, 0.729)*step(d, -.01); return vec3(1); } Repeat the teeth in a circle instead of in a line to create a sandworm mouth and we can mirror that to get the bottom teeth float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } vec3 color_for_pixel(vec2 pixel, float time) { float width = .065; pixel.y = abs(pixel.y)-.06; // NEW: mirror vertically pixel.x = mod(pixel.x, width)-width*.5; // repeat horizontally float d = parabola(pixel, 38.); if (d < 0.) return vec3(0.902, 0.890, 0.729)*step(d, -.01); return vec3(1); } Then to make it a smile, we offset the y position of the tooth based on pixel.x. float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } vec3 color_for_pixel(vec2 pixel, float time) { float width = .065; pixel.y -= pow(pixel.x, 2.); // NEW: curve into a smile pixel.y = abs(pixel.y)-.06; // mirror vertically pixel.x = mod(pixel.x, width)-width*.5; // repeat horizontally float d = parabola(pixel, 38.); if (d < 0.) return vec3(0.902, 0.890, 0.729)*step(d, -.01); return vec3(1); } Kind of creepy. Reducing the infinite teeth down to 12 will make it a little less creepy -- done by only drawing teeth when pixel.x is within the desired range float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } vec3 color_for_pixel(vec2 pixel, float time) { float width = .065; vec2 teeth = pixel; teeth.y -= pow(pixel.x, 2.); teeth.y = abs(teeth.y)-.06; teeth.x = mod(teeth.x, width)-width*.5; float d = parabola(teeth, 38.); if (d < 0. // Limit where the teeth are drawn && pixel.x < width*3. && pixel.x > -width*3. ) { return vec3(0.902, 0.890, 0.729)*step(d, -.01); } return vec3(1); } Here's Rick with wavy hair and new set of teeth. I also added the tongue. Notice that the tongue and teeth only draw inside the mouth thanks to placing their code inside the if that checks the mouth distance. float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold value = clamp(value, inMin, inMax); return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin); } vec2 grad(ivec2 z) { // fold int n = z.x+z.y*11111; n = (n<<13)^n; n = (n*(n*n*15731+789221)+1376312589)>>16; n &= 7; vec2 gr = vec2(n&1,n>>1)*2.0-1.0; return ( n>=6 ) ? vec2(0.0,gr.x) : ( n>=4 ) ? vec2(gr.x,0.0) : gr; } float noise(vec2 p) { // fold ivec2 i = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x), mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y); } vec2 warp(vec2 p, float scale, float strength) { // fold float offsetX = noise(p * scale + vec2(0.0, 100.0)); float offsetY = noise(p * scale + vec2(100.0, 0.0)); return p + vec2(offsetX, offsetY) * strength; } float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold vec2 i = v0 - v2; vec2 j = v2 - v1; vec2 k = v1 - v0; vec2 w = j-k; v0-= p; v1-= p; v2-= p; float x = v0.x*v2.y-v0.y*v2.x; float y = v1.x*v0.y-v1.y*v0.x; float z = v2.x*v1.y-v2.y*v1.x; vec2 s = 2.0*(y*j+z*k)-x*i; float r = (y*z-x*x*0.25)/dot(s,s); float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0); vec2 d = v0+t*(k+k+t*w); vec2 outQ = d + p; return length(d); } float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } float round_rect(vec2 p, vec2 b, vec4 r) { // fold r.xy = (p.x>0.0)?r.xy : r.zw; r.x = (p.y>0.0)?r.x : r.y; vec2 q = abs(p)-b+r.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; } float star(vec2 p, float r, float points, float ratio) { // fold // next 4 lines can be precomputed for a given shape float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } vec3 color_for_pixel(vec2 pixel, float time) { float d; // Mouth d = bezier(pixel, vec2(-.26, -.28), vec2(-.05,-.42), vec2(.115, -.25)); if (d < .11) { // only draw the teeth and tongue inside hte mouth shape // Teeth float width = .065; vec2 teeth = pixel; teeth.x = mod(teeth.x, width)-width*.5; teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34; teeth.y = abs(teeth.y)-.06; d = parabola(teeth, 38.); if (d < 0. && abs(pixel.x+.06) < .194) return vec3(0.902, 0.890, 0.729)*step(d, -.01); // Tongue // Make the right side of the tongue thicker float tongue_thickness = map(pixel.x, -.16, .01, .02, .045); d = bezier(pixel, vec2(-.16, -.35), vec2(.001,-.33), vec2(.01, -.5)) - tongue_thickness; if (d < 0.0) return vec3(0.816, 0.302, 0.275)*step(d, -0.01); // mouth fill color return vec3(.42, .147, .152); } if (d < .12) // mouth outline return vec3(0); // Eyebrow, Eyes, Nose & Head { // fold // Pupils vec2 pupil_warp = pixel; pupil_warp.x = abs(pupil_warp.x +.13); pupil_warp -= vec2(.16,.24); d = star(pupil_warp, 0.019, 6., .9); if (d < 0.007) { return vec3(.1); } // Eyeballs vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16); d = length(eye) - .16; if (d < 0.) { return vec3(step(0.013, -d)); } // Nose d = min( bezier(pixel, vec2(-.15, -.13), vec2(-.21,-.14), vec2(-.14, .08)), bezier(pixel, vec2(-.085, -.01), vec2(-.12, -.13), vec2(-.15,-.13))); if (d < 0.0055) return vec3(0); // Eyebrow d = bezier(pixel, vec2(-.34, .38), vec2(-.05, .68), vec2(.205, .36)) - 0.035; if (d < 0.0) return vec3(.71, .839, .922)*step(d, -.013); d = min( // Head round_rect( pixel, vec2(.36, .6385), vec4(.34, .415, .363, .315)), // Ear round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)) ); if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01); } // Hair vec2 hair = pixel; hair -= vec2(.08,.15); hair.x *= 1.3; hair = warp(hair, 4.0, 0.07); d = star(hair, 0.95, 11., .28); if (d < 0.) { return vec3(0.682, 0.839, 0.929)*step(0.012, -d); } return vec3(1.); } Artistic Lines The final bits needed are the curves below the eyes and around the mouth. Those lines are just like our normal shape outlines, except they're offset away from the perimeter of the shape. This can be done by subtracting a little from distance when drawing the outline. In other words this: if (abs(distance_to_shape) < thickness) return vec3(0); becomes this: if (abs(distance_to_shape - outset) < thickness) return vec3(0); The blue line below illustrates that technique. Since Rick's under-eye lines should only be visible…under the eye, we'll need to limit where they are drawn. That can be done using whatever logic you can think of, as shown by the green line: float round_rect(vec2 p, vec2 b, vec4 r) { // fold r.xy = (p.x>0.0)?r.xy : r.zw; r.x = (p.y>0.0)?r.x : r.y; vec2 q = abs(p)-b+r.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; } vec3 color_for_pixel(vec2 pixel, float time) { float dist = round_rect(pixel, vec2(.5), vec4(.1)); float thickness = .02; // outline if (abs(dist) < thickness) return vec3(0); // outset outline if (abs(dist-.2) < thickness) return vec3(.1,.1,1); // limited outline if (abs(dist-.4) < thickness && pixel.y < -.4) return vec3(.1,.9,.1); // fill if (dist < 0.) return vec3(1); return vec3(.92); } Here are those techniques applied to Rick: float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold value = clamp(value, inMin, inMax); return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin); } vec2 grad(ivec2 z) { // fold int n = z.x+z.y*11111; n = (n<<13)^n; n = (n*(n*n*15731+789221)+1376312589)>>16; n &= 7; vec2 gr = vec2(n&1,n>>1)*2.0-1.0; return ( n>=6 ) ? vec2(0.0,gr.x) : ( n>=4 ) ? vec2(gr.x,0.0) : gr; } float noise(vec2 p) { // fold ivec2 i = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x), mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y); } vec2 warp(vec2 p, float scale, float strength) { // fold float offsetX = noise(p * scale + vec2(0.0, 100.0)); float offsetY = noise(p * scale + vec2(100.0, 0.0)); return p + vec2(offsetX, offsetY) * strength; } float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold vec2 i = v0 - v2; vec2 j = v2 - v1; vec2 k = v1 - v0; vec2 w = j-k; v0-= p; v1-= p; v2-= p; float x = v0.x*v2.y-v0.y*v2.x; float y = v1.x*v0.y-v1.y*v0.x; float z = v2.x*v1.y-v2.y*v1.x; vec2 s = 2.0*(y*j+z*k)-x*i; float r = (y*z-x*x*0.25)/dot(s,s); float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0); vec2 d = v0+t*(k+k+t*w); vec2 outQ = d + p; return length(d); } float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } float round_rect(vec2 p, vec2 b, vec4 r) { // fold r.xy = (p.x>0.0)?r.xy : r.zw; r.x = (p.y>0.0)?r.x : r.y; vec2 q = abs(p)-b+r.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; } float star(vec2 p, float r, float points, float ratio) { // fold // next 4 lines can be precomputed for a given shape float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } vec3 color_for_pixel(vec2 pixel, float time) { // Mouth float d = bezier(pixel, vec2(-.26, -.28), vec2(-.05,-.42), vec2(.115, -.25)); if (d < .11) { // fold // Teeth float width = .065; vec2 teeth = pixel; teeth.x = mod(teeth.x, width)-width*.5; teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34; teeth.y = abs(teeth.y)-.06; d = parabola(teeth, 38.); if (d < 0. && abs(pixel.x+.06) < .194) return vec3(0.902, 0.890, 0.729)*step(d, -.01); // Tongue // Make the right side of the tongue thicker float tongue_thickness = map(pixel.x, -.16, .01, .02, .045); d = bezier(pixel, vec2(-.16, -.35), vec2(.001,-.33), vec2(.01, -.5)) - tongue_thickness; if (d < 0.0) return vec3(0.816, 0.302, 0.275)*step(d, -0.01); // mouth fill color return vec3(.42, .147, .152); } // lip outlines if (d < .12 || (abs(d-.16) < .005 && (pixel.x*-6.4 > -pixel.y+1.6 || pixel.x*1.7 > -pixel.y+.1 || pixel.y < -0.49))) return vec3(0); // lips if (d < .16) return vec3(.838, .799, 0.76); // Pupils { // fold vec2 pupil_warp = pixel; pupil_warp.x = abs(pupil_warp.x +.13); pupil_warp -= vec2(.16,.24); d = star(pupil_warp, 0.019, 6., .9); if (d < 0.007) { return vec3(.1); } } // Eyeballs vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16); d = length(eye) - .16; if (d < 0.) return vec3(step(.013, -d)); // under eye lines bool should_show = pixel.y < 0.25 && (abs(pixel.x+.29) < .05 || abs(pixel.x-.12) < .085); if (abs(d - .04) < .0055 && should_show) return vec3(0); // Nose, Eyebrow, Head, Hair { // fold // Nose d = min( bezier(pixel, vec2(-.15, -.13), vec2(-.21,-.14), vec2(-.14, .08)), bezier(pixel, vec2(-.085, -.01), vec2(-.12, -.13), vec2(-.15,-.13))); if (d < 0.0055) return vec3(0); // Eyebrow d = bezier(pixel, vec2(-.34, .38), vec2(-.05, .68), vec2(.205, .36)) - 0.035; if (d < 0.0) return vec3(.71, .839, .922)*step(d, -.013); d = min( // Head round_rect( pixel, vec2(.36, .6385), vec4(.34, .415, .363, .315)), // Ear round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)) ); if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01); // Hair vec2 hair = pixel; hair -= vec2(.08,.15); hair.x *= 1.3; hair = warp(hair, 4.0, 0.07); d = star(hair, 0.95, 11., .28); if (d < 0.) { return vec3(0.682, 0.839, 0.929)*step(0.012, -d); } } return vec3(1); } Draw another character from Rick and Morty, or whatever your favorite cartoon is. Use raymarching with 3D signed distance fields to draw a 3D version of Rick. Let me know if you do this, I want to see. Animation With our drawing complete, there are several animation techniques we can use to introduce movement. First up: 1. Looping Values The easiest way to add animation is to slap a sin(time) into the code somewhere. The sin is important because it wraps the ever-increasing time value into the range of -1 to 1, which makes nice looping animations. You will often change that range with a scale and offset like so: sin(time)*.5 + .5. The head angle, tongue angle, and eyebrow height are animated in this way. I added a rotateAt function to do the rotation math. vec2 rotateAt(vec2 p, float angle, vec2 origin) { float s = sin(angle), c = cos(angle); return (p-origin)*mat2( c, -s, s, c ) + origin; } float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold value = clamp(value, inMin, inMax); return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin); } vec2 grad(ivec2 z) { // fold int n = z.x+z.y*11111; n = (n<<13)^n; n = (n*(n*n*15731+789221)+1376312589)>>16; n &= 7; vec2 gr = vec2(n&1,n>>1)*2.0-1.0; return ( n>=6 ) ? vec2(0.0,gr.x) : ( n>=4 ) ? vec2(gr.x,0.0) : gr; } float noise(vec2 p) { // fold ivec2 i = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x), mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y); } vec2 warp(vec2 p, float scale, float strength) { // fold float offsetX = noise(p * scale + vec2(0.0, 100.0)); float offsetY = noise(p * scale + vec2(100.0, 0.0)); return p + vec2(offsetX, offsetY) * strength; } float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold vec2 i = v0 - v2; vec2 j = v2 - v1; vec2 k = v1 - v0; vec2 w = j-k; v0-= p; v1-= p; v2-= p; float x = v0.x*v2.y-v0.y*v2.x; float y = v1.x*v0.y-v1.y*v0.x; float z = v2.x*v1.y-v2.y*v1.x; vec2 s = 2.0*(y*j+z*k)-x*i; float r = (y*z-x*x*0.25)/dot(s,s); float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0); vec2 d = v0+t*(k+k+t*w); vec2 outQ = d + p; return length(d); } float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } float round_rect(vec2 p, vec2 b, vec4 r) { // fold r.xy = (p.x>0.0)?r.xy : r.zw; r.x = (p.y>0.0)?r.x : r.y; vec2 q = abs(p)-b+r.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; } float star(vec2 p, float r, float points, float ratio) { // fold // next 4 lines can be precomputed for a given shape float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } vec3 color_for_pixel(vec2 pixel, float time) { // NEW: rotate the whole drawing pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6)); pixel.y += .1; // Mouth, eyes, nose { // fold // Mouth float d = bezier(pixel, vec2(-.26, -.28), vec2(-.05,-.42), vec2(.115, -.25)); if (d < .11) { // Teeth float width = .065; vec2 teeth = pixel; teeth.x = mod(teeth.x, width)-width*.5; teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34; teeth.y = abs(teeth.y)-.06; d = parabola(teeth, 38.); if (d < 0. && abs(pixel.x+.06) < .194) return vec3(0.902, 0.890, 0.729)*step(d, -.01); // Tongue vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5)); float tongue_thickness = map(tongue.x, -.16, .01, .02, .045); d = bezier(tongue, vec2(-.16, -.35), vec2(.001,-.33), vec2(.01, -.5)) - tongue_thickness; if (d < 0.0) return vec3(0.816, 0.302, 0.275)*step(d, -0.01); // mouth fill color return vec3(.42, .147, .152); } // lip outlines if (d < .12 || (abs(d-.16) < .005 && (pixel.x*-6.4 > -pixel.y+1.6 || pixel.x*1.7 > -pixel.y+.1 || pixel.y < -0.49))) return vec3(0); // lips if (d < .16) return vec3(.838, .799, 0.76); // Pupils vec2 pupil_warp = pixel; pupil_warp.x = abs(pupil_warp.x +.13); pupil_warp -= vec2(.16,.24); d = star(pupil_warp, 0.019, 6., .9); if (d < 0.007) { return vec3(.1); } // Eyeballs vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16); d = length(eye) - .16; if (d < 0.) return vec3(step(.013, -d)); // under eye lines bool should_show = pixel.y < 0.25 && (abs(pixel.x+.29) < .05 || abs(pixel.x-.12) < .085); if (abs(d - .04) < .0055 && should_show) return vec3(0); // Nose d = min( bezier(pixel, vec2(-.15, -.13), vec2(-.21,-.14), vec2(-.14, .08)), bezier(pixel, vec2(-.085, -.01), vec2(-.12, -.13), vec2(-.15,-.13))); if (d < 0.0055) return vec3(0); } // Eyebrow float d = bezier(pixel, vec2(-.34, .38), // NEW: animate the middle up and down vec2(-.05, 0.5 + cos(time)*.1), vec2(.205, .36)) - 0.035; if (d < 0.0) return vec3(.71, .839, .922)*step(d, -.013); // Head and hair { // fold d = min( // Head round_rect( pixel, vec2(.36, .6385), vec4(.34, .415, .363, .315)), // Ear round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)) ); if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01); // Hair vec2 hair = pixel; hair -= vec2(.08,.15); hair.x *= 1.3; hair = warp(hair, 4.0, 0.07); d = star(hair, 0.95, 11., .28); if (d < 0.) { return vec3(0.682, 0.839, 0.929)*step(0.012, -d); } } return vec3(1.); } Animate Rick's head as if he is walking left and right. Flip the face direction when he is moving to the right (this is easier than it sounds!). 2. Switching What's Drawn Animating a property with sin() just moves stuff around, but you can also draw something totally different based on time. We'll do that to make Rick blink. vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fold float s = sin(angle), c = cos(angle); return (p-origin)*mat2( c, -s, s, c ) + origin; } float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold value = clamp(value, inMin, inMax); return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin); } vec2 grad(ivec2 z) { // fold int n = z.x+z.y*11111; n = (n<<13)^n; n = (n*(n*n*15731+789221)+1376312589)>>16; n &= 7; vec2 gr = vec2(n&1,n>>1)*2.0-1.0; return ( n>=6 ) ? vec2(0.0,gr.x) : ( n>=4 ) ? vec2(gr.x,0.0) : gr; } float noise(vec2 p) { // fold ivec2 i = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x), mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y); } vec2 warp(vec2 p, float scale, float strength) { // fold float offsetX = noise(p * scale + vec2(0.0, 100.0)); float offsetY = noise(p * scale + vec2(100.0, 0.0)); return p + vec2(offsetX, offsetY) * strength; } float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold vec2 i = v0 - v2; vec2 j = v2 - v1; vec2 k = v1 - v0; vec2 w = j-k; v0-= p; v1-= p; v2-= p; float x = v0.x*v2.y-v0.y*v2.x; float y = v1.x*v0.y-v1.y*v0.x; float z = v2.x*v1.y-v2.y*v1.x; vec2 s = 2.0*(y*j+z*k)-x*i; float r = (y*z-x*x*0.25)/dot(s,s); float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0); vec2 d = v0+t*(k+k+t*w); vec2 outQ = d + p; return length(d); } float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } float round_rect(vec2 p, vec2 b, vec4 r) { // fold r.xy = (p.x>0.0)?r.xy : r.zw; r.x = (p.y>0.0)?r.x : r.y; vec2 q = abs(p)-b+r.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; } float star(vec2 p, float r, float points, float ratio) { // fold // next 4 lines can be precomputed for a given shape float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } vec3 color_for_pixel(vec2 pixel, float time) { { // fold // rotate the whole drawing pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6)); pixel.y += .1; } // blink for .09 seconds, every 2 seconds if (mod(time, 2.) < .09) { // closed eyes float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0)); if (d < .008) return vec3(0); } else // open eyes { // fold // Pupils vec2 pupil_warp = pixel; pupil_warp.x = abs(pupil_warp.x +.13); pupil_warp -= vec2(.16,.24); float d = star(pupil_warp, 0.019, 6., .9); if (d < 0.007) { return vec3(.1); } // Eyeballs vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16); d = length(eye) - .16; if (d < 0.) return vec3(step(.013, -d)); // under eye lines bool should_show = pixel.y < 0.25 && (abs(pixel.x+.29) < .05 || abs(pixel.x-.12) < .085); if (abs(d - .04) < .0055 && should_show) return vec3(0); } // Rest of face { // fold // Mouth float d = bezier(pixel, vec2(-.26, -.28), vec2(-.05,-.42), vec2(.115, -.25)); if (d < .11) { // Teeth float width = .065; vec2 teeth = pixel; teeth.x = mod(teeth.x, width)-width*.5; teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34; teeth.y = abs(teeth.y)-.06; d = parabola(teeth, 38.); if (d < 0. && abs(pixel.x+.06) < .194) return vec3(0.902, 0.890, 0.729)*step(d, -.01); // Tongue // `map()` is used to change the thickness of // the tongue along the x axis vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5)); float tongue_thickness = map(tongue.x, -.16, .01, .02, .045); d = bezier(tongue, vec2(-.16, -.35), vec2(.001,-.33), vec2(.01, -.5)) - tongue_thickness; if (d < 0.0) return vec3(0.816, 0.302, 0.275)*step(d, -0.01); // mouth fill color return vec3(.42, .147, .152); } // lip outlines if (d < .12 || (abs(d-.16) < .005 && (pixel.x*-6.4 > -pixel.y+1.6 || pixel.x*1.7 > -pixel.y+.1 || pixel.y < -0.49))) return vec3(0); // lips if (d < .16) return vec3(.838, .799, 0.76); // Nose d = min( bezier(pixel, vec2(-.15, -.13), vec2(-.21,-.14), vec2(-.14, .08)), bezier(pixel, vec2(-.085, -.01), vec2(-.12, -.13), vec2(-.15,-.13))); if (d < 0.0055) return vec3(0); // Eyebrow d = bezier(pixel, vec2(-.34, .38), // NEW animate the middle up and down vec2(-.05, 0.5 + cos(time)*.1), vec2(.205, .36)) - 0.035; if (d < 0.0) return vec3(.71, .839, .922)*step(d, -.013); d = min( // Head round_rect( pixel, vec2(.36, .6385), vec4(.34, .415, .363, .315)), // Ear round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)) ); if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01); // Hair vec2 hair = pixel; hair -= vec2(.08,.15); hair.x *= 1.3; hair = warp(hair, 4.0, 0.07); d = star(hair, 0.95, 11., .28); if (d < 0.) { return vec3(0.682, 0.839, 0.929)*step(0.012, -d); } } return vec3(1); } Use this technique to animate Rick's mouth so it looks like he is talking. 3. Noisy Movement If sin is too smooth for you, try using noise! I used noise() to make the eyes randomly look around. Since I don't want the eyes to be continuously moving, I rounded the time value before passing it to noise(). vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fold float s = sin(angle), c = cos(angle); return (p-origin)*mat2( c, -s, s, c ) + origin; } float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold value = clamp(value, inMin, inMax); return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin); } vec2 grad(ivec2 z) { // fold int n = z.x+z.y*11111; n = (n<<13)^n; n = (n*(n*n*15731+789221)+1376312589)>>16; n &= 7; vec2 gr = vec2(n&1,n>>1)*2.0-1.0; return ( n>=6 ) ? vec2(0.0,gr.x) : ( n>=4 ) ? vec2(gr.x,0.0) : gr; } float noise(vec2 p) { // fold ivec2 i = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x), mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y); } vec2 warp(vec2 p, float scale, float strength) { // fold float offsetX = noise(p * scale + vec2(0.0, 100.0)); float offsetY = noise(p * scale + vec2(100.0, 0.0)); return p + vec2(offsetX, offsetY) * strength; } float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold vec2 i = v0 - v2; vec2 j = v2 - v1; vec2 k = v1 - v0; vec2 w = j-k; v0-= p; v1-= p; v2-= p; float x = v0.x*v2.y-v0.y*v2.x; float y = v1.x*v0.y-v1.y*v0.x; float z = v2.x*v1.y-v2.y*v1.x; vec2 s = 2.0*(y*j+z*k)-x*i; float r = (y*z-x*x*0.25)/dot(s,s); float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0); vec2 d = v0+t*(k+k+t*w); vec2 outQ = d + p; return length(d); } float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } float round_rect(vec2 p, vec2 b, vec4 r) { // fold r.xy = (p.x>0.0)?r.xy : r.zw; r.x = (p.y>0.0)?r.x : r.y; vec2 q = abs(p)-b+r.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; } float star(vec2 p, float r, float points, float ratio) { // fold // next 4 lines can be precomputed for a given shape float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } vec3 color_for_pixel(vec2 pixel, float time) { { // fold // rotate the whole drawing pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6)); pixel.y += .1; } // Blink eyes if (mod(time, 2.) < .09) { // fold // closed eyes float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0)); if (d < .008) return vec3(0); } else { // move pupils randomly vec2 pupil_warp = pixel + vec2(.095,-.18); pupil_warp.x -= noise(vec2(round(time)*7.+.5, 0.5))*.1; pupil_warp.y -= noise(vec2(round(time)*9.+.5, 0.5))*.1; pupil_warp.x = abs(pupil_warp.x) - .16; float d = star(pupil_warp, 0.019, 6., .9); {// fold if (d < 0.007) { return vec3(.1); } // Eyeballs vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16); d = length(eye) - .16; if (d < 0.) return vec3(step(.013, -d)); // under eye lines bool should_show = pixel.y < 0.25 && (abs(pixel.x+.29) < .05 || abs(pixel.x-.12) < .085); if (abs(d - .04) < .0055 && should_show) return vec3(0); } } // Rest of face { // fold // Mouth float d = bezier(pixel, vec2(-.26, -.28), vec2(-.05,-.42), vec2(.115, -.25)); if (d < .11) { // Teeth float width = .065; vec2 teeth = pixel; teeth.x = mod(teeth.x, width)-width*.5; teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34; teeth.y = abs(teeth.y)-.06; d = parabola(teeth, 38.); if (d < 0. && abs(pixel.x+.06) < .194) return vec3(0.902, 0.890, 0.729)*step(d, -.01); // Tongue // `map()` is used to change the thickness of // the tongue along the x axis vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5)); float tongue_thickness = map(tongue.x, -.16, .01, .02, .045); d = bezier(tongue, vec2(-.16, -.35), vec2(.001,-.33), vec2(.01, -.5)) - tongue_thickness; if (d < 0.0) return vec3(0.816, 0.302, 0.275)*step(d, -0.01); // mouth fill color return vec3(.42, .147, .152); } // lip outlines if (d < .12 || (abs(d-.16) < .005 && (pixel.x*-6.4 > -pixel.y+1.6 || pixel.x*1.7 > -pixel.y+.1 || pixel.y < -0.49))) return vec3(0); // lips if (d < .16) return vec3(.838, .799, 0.76); // Nose d = min( bezier(pixel, vec2(-.15, -.13), vec2(-.21,-.14), vec2(-.14, .08)), bezier(pixel, vec2(-.085, -.01), vec2(-.12, -.13), vec2(-.15,-.13))); if (d < 0.0055) return vec3(0); // Eyebrow d = bezier(pixel, vec2(-.34, .38), // NEW animate the middle up and down vec2(-.05, 0.5 + cos(time)*.1), vec2(.205, .36)) - 0.035; if (d < 0.0) return vec3(.71, .839, .922)*step(d, -.013); d = min( // Head round_rect( pixel, vec2(.36, .6385), vec4(.34, .415, .363, .315)), // Ear round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)) ); if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01); // Hair vec2 hair = pixel; hair -= vec2(.08,.15); hair.x *= 1.3; hair = warp(hair, 4.0, 0.07); d = star(hair, 0.95, 11., .28); if (d < 0.) { return vec3(0.682, 0.839, 0.929)*step(0.012, -d); } } return vec3(1); } Make the pupil movement more realistic instead of jumping between positions Bonus: Warping Time Our final animation technique is "time domain warping" to make the hair bend as the head tilts. It's like domain warping, except instead of offsetting space we offset time. Basically we delay time more the closer to the hair tip a pixel is. Because that delay isn't constant along the length of the hair, the hair will bend instead of rotate rigidly. vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fold float s = sin(angle), c = cos(angle); return (p-origin)*mat2( c, -s, s, c ) + origin; } float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold value = clamp(value, inMin, inMax); return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin); } vec2 grad(ivec2 z) { // fold int n = z.x+z.y*11111; n = (n<<13)^n; n = (n*(n*n*15731+789221)+1376312589)>>16; n &= 7; vec2 gr = vec2(n&1,n>>1)*2.0-1.0; return ( n>=6 ) ? vec2(0.0,gr.x) : ( n>=4 ) ? vec2(gr.x,0.0) : gr; } float noise(vec2 p) { // fold ivec2 i = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x), mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y); } vec2 warp(vec2 p, float scale, float strength) { // fold float offsetX = noise(p * scale + vec2(0.0, 100.0)); float offsetY = noise(p * scale + vec2(100.0, 0.0)); return p + vec2(offsetX, offsetY) * strength; } float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold vec2 i = v0 - v2; vec2 j = v2 - v1; vec2 k = v1 - v0; vec2 w = j-k; v0-= p; v1-= p; v2-= p; float x = v0.x*v2.y-v0.y*v2.x; float y = v1.x*v0.y-v1.y*v0.x; float z = v2.x*v1.y-v2.y*v1.x; vec2 s = 2.0*(y*j+z*k)-x*i; float r = (y*z-x*x*0.25)/dot(s,s); float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0); vec2 d = v0+t*(k+k+t*w); vec2 outQ = d + p; return length(d); } float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } float round_rect(vec2 p, vec2 b, vec4 r) { // fold r.xy = (p.x>0.0)?r.xy : r.zw; r.x = (p.y>0.0)?r.x : r.y; vec2 q = abs(p)-b+r.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; } float star(vec2 p, float r, float points, float ratio) { // fold // next 4 lines can be precomputed for a given shape float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } vec3 color_for_pixel(vec2 pixel, float time) { { // fold // rotate the whole drawing pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6)); pixel.y += .1; // Blink eyes if (mod(time, 2.) < .09) { // closed eyes float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0)); if (d < .008) return vec3(0); } else { // move pupils randomly vec2 pupil_warp = pixel + vec2(.095,-.18); pupil_warp.x -= noise(vec2(round(time)*7.+.5, 0.5))*.1; pupil_warp.y -= noise(vec2(round(time)*9.+.5, 0.5))*.1; pupil_warp.x = abs(pupil_warp.x) - .16; float d = star(pupil_warp, 0.019, 6., .9); if (d < 0.007) { return vec3(.1); } // Eyeballs vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16); d = length(eye) - .16; if (d < 0.) return vec3(step(.013, -d)); // under eye lines bool should_show = pixel.y < 0.25 && (abs(pixel.x+.29) < .05 || abs(pixel.x-.12) < .085); if (abs(d - .04) < .0055 && should_show) return vec3(0); } // Mouth float d = bezier(pixel, vec2(-.26, -.28), vec2(-.05,-.42), vec2(.115, -.25)); if (d < .11) { // Teeth float width = .065; vec2 teeth = pixel; teeth.x = mod(teeth.x, width)-width*.5; teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34; teeth.y = abs(teeth.y)-.06; d = parabola(teeth, 38.); if (d < 0. && abs(pixel.x+.06) < .194) return vec3(0.902, 0.890, 0.729)*step(d, -.01); // Tongue // `map()` is used to change the thickness of // the tongue along the x axis vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5)); float tongue_thickness = map(tongue.x, -.16, .01, .02, .045); d = bezier(tongue, vec2(-.16, -.35), vec2(.001,-.33), vec2(.01, -.5)) - tongue_thickness; if (d < 0.0) return vec3(0.816, 0.302, 0.275)*step(d, -0.01); // mouth fill color return vec3(.42, .147, .152); } // lip outlines if (d < .12 || (abs(d-.16) < .005 && (pixel.x*-6.4 > -pixel.y+1.6 || pixel.x*1.7 > -pixel.y+.1 || pixel.y < -0.49))) return vec3(0); // lips if (d < .16) return vec3(.838, .799, 0.76); // Nose d = min( bezier(pixel, vec2(-.15, -.13), vec2(-.21,-.14), vec2(-.14, .08)), bezier(pixel, vec2(-.085, -.01), vec2(-.12, -.13), vec2(-.15,-.13))); if (d < 0.0055) return vec3(0); // Eyebrow d = bezier(pixel, vec2(-.34, .38), // NEW animate the middle up and down vec2(-.05, 0.5 + cos(time)*.1), vec2(.205, .36)) - 0.035; if (d < 0.0) return vec3(.71, .839, .922)*step(d, -.013); d = min( // Head round_rect( pixel, vec2(.36, .6385), vec4(.34, .415, .363, .315)), // Ear round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)) ); if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01); } // Hair float twist = sin(time*2.-length(pixel)*2.1)*.12; vec2 hair = rotateAt(pixel, twist, vec2(0.,.1)); hair -= vec2(.08,.15); hair.x *= 1.3; hair = warp(hair, 4.0, 0.07); float d = star(hair, 0.95, 11., .28); if (d < 0.) { return vec3(0.682, 0.839, 0.929)*step(d, -0.012); } return vec3(1); } Apply this trick to to other parts of Rick's face for a rubbery and ricklaxed look. Wrapping up After we add a portal effect^2 our animation is complete. vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fold float s = sin(angle), c = cos(angle); return (p-origin)*mat2( c, -s, s, c ) + origin; } float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold value = clamp(value, inMin, inMax); return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin); } vec2 grad(ivec2 z) { // fold int n = z.x+z.y*11111; n = (n<<13)^n; n = (n*(n*n*15731+789221)+1376312589)>>16; n &= 7; vec2 gr = vec2(n&1,n>>1)*2.0-1.0; return ( n>=6 ) ? vec2(0.0,gr.x) : ( n>=4 ) ? vec2(gr.x,0.0) : gr; } float noise(vec2 p) { // fold ivec2 i = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x), mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y); } vec2 warp(vec2 p, float scale, float strength) { // fold float offsetX = noise(p * scale + vec2(0.0, 100.0)); float offsetY = noise(p * scale + vec2(100.0, 0.0)); return p + vec2(offsetX, offsetY) * strength; } float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold vec2 i = v0 - v2; vec2 j = v2 - v1; vec2 k = v1 - v0; vec2 w = j-k; v0-= p; v1-= p; v2-= p; float x = v0.x*v2.y-v0.y*v2.x; float y = v1.x*v0.y-v1.y*v0.x; float z = v2.x*v1.y-v2.y*v1.x; vec2 s = 2.0*(y*j+z*k)-x*i; float r = (y*z-x*x*0.25)/dot(s,s); float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0); vec2 d = v0+t*(k+k+t*w); vec2 outQ = d + p; return length(d); } float parabola(vec2 pos, float k) { // fold // from https://www.shadertoy.com/view/ws3GD7 pos.x = abs(pos.x); float ik = 1.0/k; float p = ik*(pos.y - 0.5*ik)/3.0; float q = 0.25*ik*ik*pos.x; float h = q*q - p*p*p; float r = sqrt(abs(h)); float x = (h>0.0) ? pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) : 2.0*cos(atan(r,q)/3.0)*sqrt(p); return length(pos-vec2(x,k*x*x)) * sign(pos.x-x); } float round_rect(vec2 p, vec2 b, vec4 r) { // fold r.xy = (p.x>0.0)?r.xy : r.zw; r.x = (p.y>0.0)?r.x : r.y; vec2 q = abs(p)-b+r.x; return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; } float star(vec2 p, float r, float points, float ratio) { // fold // next 4 lines can be precomputed for a given shape float an = 3.141593/points; float en = 3.141593/(ratio*(points-2.) + 2.); vec2 acs = vec2(cos(an),sin(an)); vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon float bn = mod(atan(p.x,p.y),2.0*an) - an; p = length(p)*vec2(cos(bn),abs(sin(bn))); p -= r*acs; p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); return length(p)*sign(p.x); } #define H(i,j) fract(sin(dot(ceil(P+vec2(i,j)), resolution.xy )) * 4e3) float N( vec2 P) { // fold float s,i,w = .5; for (; i < 3. ; i++, w *= .4, P *= 1.9 ) { vec2 F = fract( P *= mat2(.866,-.5,.5,.866) ); F *= F*(3.-F-F); s += w* mix( mix(H(0,0) , H(1,0), F.x), mix(H(0,1) , H(1,1), F.x), F.y ); } return s; } vec3 portal(vec2 pixel, float time) { // fold // from https://www.shadertoy.com/view/l3f3zM float l = length( pixel ), a = atan(pixel.y, pixel.x) / 6.28 + .5, k = 10.; a = fract(a + l*.3 - time*.01 ); vec2 U = vec2( l+time*.3, a ); return vec3[]( vec3(.18, .53, .09), vec3(.56, .89, .16), vec3(.35, .84, .11), vec3(.92, .98, .85) ) [ int( 4.* pow( mix( N(U*k), N(U*k-vec2(0,k)), U.y) * 1.5, 2.5))]; } vec3 color_for_pixel(vec2 pixel, float time) { { // fold // rotate the whole drawing pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6)); pixel.y += .1; // Blink eyes if (mod(time, 2.) < .09) { // closed eyes float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0)); if (d < .008) return vec3(0); } else { // move pupils randomly vec2 pupil_warp = pixel + vec2(.095,-.18); pupil_warp.x -= noise(vec2(round(time)*7.+.5, 0.5))*.1; pupil_warp.y -= noise(vec2(round(time)*9.+.5, 0.5))*.1; pupil_warp.x = abs(pupil_warp.x) - .16; float d = star(pupil_warp, 0.019, 6., .9); if (d < 0.007) { return vec3(.1); } // Eyeballs vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16); d = length(eye) - .16; if (d < 0.) return vec3(step(.013, -d)); // under eye lines bool should_show = pixel.y < 0.25 && (abs(pixel.x+.29) < .05 || abs(pixel.x-.12) < .085); if (abs(d - .04) < .0055 && should_show) return vec3(0); } // Mouth float d = bezier(pixel, vec2(-.26, -.28), vec2(-.05,-.42), vec2(.115, -.25)); if (d < .11) { // Teeth float width = .065; vec2 teeth = pixel; teeth.x = mod(teeth.x, width)-width*.5; teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34; teeth.y = abs(teeth.y)-.06; d = parabola(teeth, 38.); if (d < 0. && abs(pixel.x+.06) < .194) return vec3(0.902, 0.890, 0.729)*step(d, -.01); // Tongue // `map()` is used to change the thickness of // the tongue along the x axis vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5)); float tongue_thickness = map(tongue.x, -.16, .01, .02, .045); d = bezier(tongue, vec2(-.16, -.35), vec2(.001,-.33), vec2(.01, -.5)) - tongue_thickness; if (d < 0.0) return vec3(0.816, 0.302, 0.275)*step(d, -0.01); // mouth fill color return vec3(.42, .147, .152); } // lip outlines if (d < .12 || (abs(d-.16) < .005 && (pixel.x*-6.4 > -pixel.y+1.6 || pixel.x*1.7 > -pixel.y+.1 || pixel.y < -0.49))) return vec3(0); // lips if (d < .16) return vec3(.838, .799, 0.76); // Nose d = min( bezier(pixel, vec2(-.15, -.13), vec2(-.21,-.14), vec2(-.14, .08)), bezier(pixel, vec2(-.085, -.01), vec2(-.12, -.13), vec2(-.15,-.13))); if (d < 0.0055) return vec3(0); // Eyebrow d = bezier(pixel, vec2(-.34, .38), // NEW animate the middle up and down vec2(-.05, 0.5 + cos(time)*.1), vec2(.205, .36)) - 0.035; if (d < 0.0) return vec3(.71, .839, .922)*step(d, -.013); d = min( // Head round_rect( pixel, vec2(.36, .6385), vec4(.34, .415, .363, .315)), // Ear round_rect( pixel + vec2(-.32, .15), vec2(.15, 0.12), vec4(.13,.1,.13,.13)) ); if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01); // Hair float twist = sin(time*2.-length(pixel)*2.1)*.12; vec2 hair = rotateAt(pixel, twist, vec2(0.,.1)); hair -= vec2(.08,.15); hair.x *= 1.3; hair = warp(hair, 4.0, 0.07); d = star(hair, 0.95, 11., .28); if (d < 0.) { return vec3(0.682, 0.839, 0.929)*step(d, -0.012); } } return portal(pixel, time); } I prioritized readability over performance for this code - see how much faster you can make it run. That's everything I know about making 2D animations using shaders. I hope it's useful. Maybe next time we'll talk about 3D, or some totally different topic! If you'd like to be notified about my next post, please join my newsletter. [ ] Join Newsletter Join my newsletter lol While I love teaching and making posts like this, they are very time consuming to make -- this one took about two weeks of work spread over 8 months. So if you'd like to see more work like this, please consider supporting me. Appendix 1: Creating a Video When you're done with an animation you'll probably want to turn it into a video. The editor we've been using on this page can not yet do that, but I'm working on it. Join my newsletter to be notified when I add video export! In the meantime, you can use a script with glslviewer and ffmpeg. Below is my macOS workflow, on Windows and Linux you'll have to figure out what your platform's equivalent is. 1. Install the dependencies. brew install glslviewer ffmpeg # brew is macos only 2. Write your shader.frag file 3. And then put this in a bash file and run to export your video #!/bin/bash set -e set -o pipefail if [ -z "$1" ]; then echo "Usage: $0 " exit 1 fi ORIGINAL_DIR=$(pwd) TMP_DIR=$(mktemp -d) if [ ! -d "$TMP_DIR" ]; then echo "Failed to create temporary directory." exit 1 fi cd "$TMP_DIR" glslViewer "$ORIGINAL_DIR/$1" -w 1920 -h 1080 --headless -e sequence,0,7,60 -e q ffmpeg -framerate 60 -y -i %05d.png -c:v libx264 -pix_fmt yuv420p animation.mp4 mv animation.mp4 "$ORIGINAL_DIR/" cd "$ORIGINAL_DIR" rm -rf "$TMP_DIR" And if you want to live code locally, use this: glslViewer shader.frag -w 575 -h 324 --noncurses -x 0 -y 0 Appendix 2: Super Sampling You may have noticed that the edges of shapes in the examples on this page are smooth. I did a bit of work behind the scenes make that happen. I use a technique called super sampling where I call color_for_pixel() for 9 locations within each screen pixel and then display the average. The left side of this example shows what it looks like with super sampling disabled. You may need to zoom in on the page to see the difference. #version 300 es // The above line switches the editor to "pro" mode // and removes automatic super sampling precision highp float; uniform float time; uniform vec2 resolution; out vec4 outColor; vec3 color_for_pixel(vec2 p, float time) { return vec3(length(mod(p+time*.05, .5) - .25) > 0.2); } void main() { float zone = gl_FragCoord.x - resolution.x*.5; if (abs(zone) < 1.5) { // vertical line outColor = vec4(1, 0, 0, 1); } else if (zone < 0.) { // left side: no super sampling vec2 st = (2.0*(gl_FragCoord.xy)-resolution)/resolution.y; outColor = vec4(color_for_pixel(st, time), 1); } else { // right side: super sampling int sample_count = 3; vec3 sum = vec3(0); for( int m=0; m 0.) { color = vec3(.2); } else { color = vec3(1.); } float grid_resolution = sin(time/1.2)/4. + 0.4; // find 4 nearest points vec2 p1 = floor((st) / grid_resolution) * grid_resolution; vec2 p2 = p1 + vec2(0, grid_resolution); vec2 p3 = p1 + vec2(grid_resolution, 0); vec2 p4 = p1 + vec2(grid_resolution, grid_resolution); vec2 nearest = vec2(round(st.x/grid_resolution)*grid_resolution, round(st.y/grid_resolution)*grid_resolution); vec2 j = abs(nearest - st); // sample sdf float v1 = sdf(p1); float v2 = sdf(p2); float v3 = sdf(p3); float v4 = sdf(p4); // count number inside float count = step(0.,-v1) + step(0.,-v2) +step(0.,-v3) +step(0.,-v4); vec2 p12 = (p1 + p2) / 2.; vec2 p13 = (p1 + p3) / 2.; vec2 p24 = (p4 + p2) / 2.; vec2 p34 = (p4 + p3) / 2.; p12 = mix(p12, interpVert(p1, p2, v1, v2), min(1.,time*INTERP)); p13 = mix(p13, interpVert(p1, p3, v1, v3), min(1.,time*INTERP)); p24 = mix(p24, interpVert(p2, p4, v2, v4), min(1.,time*INTERP)); p34 = mix(p34, interpVert(p4, p3, v4, v3), min(1.,time*INTERP)); float d = 10.; vec3 special_fill = fill_color; if (count == 4.) { #ifdef TRIANGULATE_4 special_fill = vec3(0.178, 0.321, 0.400); d = min(sdTriangle(st, p1,p2,p3), sdTriangle(st, p4,p2,p3)); #endif } else if (count == 3.) { #ifdef TRIANGULATE_3 special_fill = vec3(0.431, 0.532, 0.595); if (v1 > 0.) { d = min(sdTriangle(st, p4,p2,p12), min(sdTriangle(st, p4,p3,p13), sdTriangle(st, p4,p13,p12))); } else if (v2 > 0.) { d = min(sdTriangle(st, p1,p3,p12), min(sdTriangle(st, p12,p3,p24), sdTriangle(st, p4,p3,p24))); } else if (v3 > 0.) { d = min(sdTriangle(st, p1,p2,p13), min(sdTriangle(st, p2,p13,p34), sdTriangle(st, p2,p4,p34))); } else { d = min(sdTriangle(st, p1,p2,p24), min(sdTriangle(st, p1,p24,p34), sdTriangle(st, p1,p3,p34))); } #endif } else if (count == 2.) { #ifdef TRIANGULATE_2 special_fill = vec3(0.622, 0.692, 0.738); if ((v1 < 0.) == (v2 < 0.) || (v1 < 0.) == (v3 < 0.)) { if (v1 < 0. && v2 < 0.) { d = min(sdTriangle(st, p1,p2,p24), sdTriangle(st, p1,p24,p13)); } else if (v3 < 0. && v4 < 0.) { d = min(sdTriangle(st, p3,p4,p24), sdTriangle(st, p3,p24,p13)); } else if (v3 < 0. && v1 < 0.) { d = min(sdTriangle(st, p3,p1,p12), sdTriangle(st, p3,p12,p34)); } else if (v2 < 0. && v4 < 0.){ d = min(sdTriangle(st, p4,p2,p12), sdTriangle(st, p4,p12,p34)); } } else { // unhandled } #endif } else if (count == 1.) { #ifdef TRIANGULATE_1 special_fill = vec3(0.825, 0.815, 0.794); if (v1 < 0.) { d = sdTriangle(st, p1,p12,p13); } else if (v2 < 0.) { d = sdTriangle(st, p2,p12,p24); } else if (v3 < 0.) { d = sdTriangle(st, p3,p13,p34); } else { d = sdTriangle(st, p4,p24,p34); } #endif } if (abs(d) < wireframe_thickness) { color = wireframe_color; } else if (d < 0.) { color = special_fill; } if (Circle(repeated(st+.05, grid_resolution), vec2(.05,.05), 0.007) < 0.) { if (sdf(nearest) < 0.) { #if SHOW_INNER_DOTS color = vec3(0.404, 0.827, 0.478); #endif } else { #if SHOW_OUTER_DOTS color = vec3(0.875, 0.376, 0.243); #endif } } return color; } I needed this animation for the video to make sense, but couldn't get past how painful and time consuming it'd be to make in a typical animation program. It seemed like the only way to accurately and quickly make it was with code. So I started coding, and the animation above is what I ended up with. I'm pretty happy with it. Some people asked me how I made the animations, so I wrote this post to answer that question. --------------------------------------------------------------------- Reset All Example Code --------------------------------------------------------------------- 1. This document has a list of GLSL's built in functions on the last page. -[?] 2. The portal effect was made by ShaderToy user valena and shortened by FabriceNeyret2. -[?] Get notified about my next post. [ ] Join Newsletter More posts by Daniel Support my work