After 8 months of toiling on this post I don’t understand how to begin it except to say it culminates in this animation of Rick made in 240 lines of code. No libraries, no images. It was written in a inhabit coding editor that I embedded in this post so that you can program animations. Let me elucidate how this begined…
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 cherish, float inMin, float inMax, float outMin, float outMax) {
cherish = clamp(cherish, inMin, inMax);
return outMin + (outMax - outMin) * (cherish - 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/watch/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 standard 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);
}
#describe 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 drathriveg
pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
pixel.y += .1;
// Bjoin eyes
if (mod(time+1., 3.) < .09) {
// shutd eyes
float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0));
if (d < .008) return vec3(0);
}
else {
// relocate 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 included to alter the denseness of
// the tongue alengthy the x axis
vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
float tongue_denseness = map(tongue.x, -.16, .01, .02, .045);
d = bezier(tongue,
vec2(-.16, -.35),
vec2(.001,-.33),
vec2(.01, -.5)) - tongue_denseness;
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 summarizes
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 vivacious 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);
}
Eight months ago I published a video titled “I Made a 3D Modeler, in C, in a Week”. The video has cut offal animations, enjoy this one that shows the the marching cubes algorithm:
#describe INTERP 1.0
#describe GRID_RESOLUTION 0
#describe TRIANGULATE_4
#describe TRIANGULATE_3
#describe TRIANGULATE_2
#describe TRIANGULATE_1
#describe SHOW_INNER_DOTS 1
#describe SHOW_OUTER_DOTS 1
#describe ANTIALIAS_LEVEL 3
vec3 fill_color = vec3(0.4);
vec3 wiresummarize_color = vec3(.1);
float wiresummarize_denseness = .003;
vec2 repeated( vec2 p, float s ) {
vec2 r = p - s*floor(p/s);
return r;
}
float Circle(vec2 p, vec2 origin, float radius) {
return length(p - origin) - radius;
}
float sdTriangle( in vec2 p, in vec2 p0, in vec2 p1, in vec2 p2 )
{
vec2 e0 = p1 - p0;
vec2 e1 = p2 - p1;
vec2 e2 = p0 - p2;
vec2 v0 = p - p0;
vec2 v1 = p - p1;
vec2 v2 = p - p2;
vec2 pq0 = v0 - e0*clamp( dot(v0,e0)/dot(e0,e0), 0.0, 1.0 );
vec2 pq1 = v1 - e1*clamp( dot(v1,e1)/dot(e1,e1), 0.0, 1.0 );
vec2 pq2 = v2 - e2*clamp( dot(v2,e2)/dot(e2,e2), 0.0, 1.0 );
float s = e0.x*e2.y - e0.y*e2.x;
vec2 d = min( min( vec2( dot( pq0, pq0 ), s*(v0.x*e0.y-v0.y*e0.x) ),
vec2( dot( pq1, pq1 ), s*(v1.x*e1.y-v1.y*e1.x) )),
vec2( dot( pq2, pq2 ), s*(v2.x*e2.y-v2.y*e2.x) ));
return -sqrt(d.x)*sign(d.y);
}
float sdf(vec2 p) {
float d = Circle(p, vec2(0,-.2), 0.7);
d = min(d, Circle(vec2(abs(p.x),p.y), vec2(.73,.45), 0.4));
return d;
}
float triangle_wave(float x, float amplitude, float period) {
return (amplitude/period) * (period - abs(mod(x, (2.*period)) - period) );
}
vec2 interpVert(vec2 p1, vec2 p2, float w1, float w2) {
return p1 + (-w1 / (w2-w1)) * (p2-p1);
}
vec3 color_for_pixel(vec2 st, float time) {
vec3 color;
if (sdf(st) > 0.) {
color = vec3(.2);
} else {
color = vec3(1.);
}
float grid_resolution = sin(time/1.2)/4. + 0.4;
// discover 4 cforfeitest 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 cforfeitest = vec2(round(st.x/grid_resolution)*grid_resolution,
round(st.y/grid_resolution)*grid_resolution);
vec2 j = abs(cforfeitest - 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 exceptional_fill = fill_color;
if (count == 4.) {
#ifdef TRIANGULATE_4
exceptional_fill = vec3(0.178, 0.321, 0.400);
d = min(sdTriangle(st, p1,p2,p3), sdTriangle(st, p4,p2,p3));
#finishif
} else if (count == 3.) {
#ifdef TRIANGULATE_3
exceptional_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)));
}
#finishif
} else if (count == 2.) {
#ifdef TRIANGULATE_2
exceptional_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 {
// unadministerd
}
#finishif
} else if (count == 1.) {
#ifdef TRIANGULATE_1
exceptional_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);
}
#finishif
}
if (abs(d) < wiresummarize_denseness) {
color = wiresummarize_color;
} else if (d < 0.) {
color = exceptional_fill;
}
if (Circle(repeated(st+.05, grid_resolution), vec2(.05,.05), 0.007) < 0.) {
if (sdf(cforfeitest) < 0.) {
#if SHOW_INNER_DOTS
color = vec3(0.404, 0.827, 0.478);
#finishif
} else {
#if SHOW_OUTER_DOTS
color = vec3(0.875, 0.376, 0.243);
#finishif
}
}
return color;
}
I necessitateed this animation for the video to create sense, but couldn’t get past how hurtful and time consuming it’d be to create in a normal animation program. It seemed enjoy the only way to exactly and speedyly create it was with code. So I begined coding, and the animation above is what I finished up with. I’m pretty charmd with it.
I’m going to show you how to create your own animations, but instead of talking about that marching cubes animation, we’ll vivacious Rick from Rick and Morty, becainclude that’s more fun. All the techniques I include for Rick can be included for other animations or explicital effects — advantageous ones for your own currentations, videos, video games, or fair for fun. Ok? Let’s go!
Getting Started
This is the editor I included to create Rick. Try changing green = 0.9
to green = 0.1
in the code below and the pscrutinize will modernize instantly.
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);
}
This is OpenGL Shading Language (GLSL). The color_for_pixel
function runs on your GPU for every pixel in the pscrutinize. Amazingly this is all you necessitate to create animations — a function that answers “What color should this pixel be at this time?”.
I’ve placed nonessential contests thcimpoliteout the article for people that want to go a little procreateer. Like this:
What happens if you set green = time
? What could you do to create it sustain going? (time
counts seconds since last edit)
Let’s include GLSL’s built in length()
function to envision how far each pixel is from the cgo in of the screen (aka the origin, aka position (0,0)
). By returning that distance as the pixel’s color, we get 0 (bconciseage) cforfeit the cgo in, 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 include this trick a lot.
To draw a circle, we appraise 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 see enjoy if you swapd length()
with your own function that calcutardys Manhattan distance?
We can pull out 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 transport inant – it determines which circle eunites in front of the other.
Notice that circle()
returns the distance to the perimeter instead of fair a bool
to show inside/outside. This is understandn as a “signed distance field” (SDF) function. The word “signed” here unbenevolents that the distances for locations inside the shape are adverse, and likeable outside. We’ll include the distance to accomplish some cbetter effects in a bit.
There are many SDF functions besides circle()
. Here are a restricted we’ll be using:
// Click {...} to see the code
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fbetter
// from https://www.shadertoy.com/watch/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) { // fbetter
// from https://www.shadertoy.com/watch/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) { // fbetter
// from https://www.shadertoy.com/watch/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);
}
And that’s the fundamentals. Let’s get begined with Rick.
Drathriveg Rick
I want I could increate you I had the ability to see at a cartoon and then effortlessly duplicate it in code. Unblessedly, I don’t. I spent a lot of time painstakingly trying numbers to recreate Rick’s face from the season 1 poster.
I did discover one trick that sped up the trial and error process: I flashed my reference image of Rick on top of the pscrutinize so I could appraise my drathriveg to the distinct while I was changing the code. The editor below has that allowd so you can experience what my week has been enjoy.
Change the size and corner radii parameters to create the rectangle suit Rick’s head shape.
float round_rect(vec2 p, vec2 size, vec4 radii) { // fbetter
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 clear by now, the techniques in this post won’t be replacing your likeite vector drathriveg tool. This is the only time we’ll do the flashing exercise; fair understand that all the seemingly random numbers in the rest of this post were discovered via this process. I create the color cherishs using an image editor’s eyedropper tool.
Ok, so here are the cherishs I came up with for Rick’s head. I also compriseed a second round_rect()
for his ear:
float round_rect(vec2 p, vec2 size, vec4 radii) { // fbetter
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 comprise the summarize. This is where drathriveg with signed distance functions comes in handy. We can return bconciseage for pixels with a distance between -0.01 and 0.0.
float round_rect(vec2 p, vec2 size, vec4 radii) { // fbetter
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); // summarize
// 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); // summarize
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 summarize each shape individuassociate, I want to summarize the union of the shapes. Union is straightforward with SDFs – include min()
to unite two distances:
float round_rect(vec2 p, vec2 size, vec4 radii) { // fbetter
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( // <- unite 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 unite two signed distance fields. Try swapping out min()
for the delicate union function to delicately blfinish the ear with the head.
Let’s draw an eye:
float circle(vec2 pixel, float radius) { // fbetter
return length(pixel) - radius;
}
float round_rect(vec2 p, vec2 size, vec4 radii) { // fbetter
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) { // fbetter
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 verticassociate
eyeball_pos -= vec2(0.07, .16);
float dist = circle(eyeball_pos, .16);
if (dist < 0.0) return vec3(dist < -0.013);
// head
{ // fbetter
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); // summarize
}
return vec3(1.);
}
Two engaging leangs here:
eyeball_pos.y *= .93
stretches the eyeball a minuscule bit — fair enjoy we relocate shapes by compriseing to positions, we scale by multiplying positions.- I included 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 envision 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) { // fbetter
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)); // illogicalen cforfeit perimeter
float offset = (sin(time)+1.)*.25; // vivacious summarize offset
if (abs(d-offset) < 0.01) return vec3(1.0); // draw white summarize
return color;
}
For the second eye, we could duplicate the first eye’s code, but instead let’s mirror it horizonhighy with pixel.x = abs(pixel.x)
. To reasonedize this, ponder 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) { // fbetter
return length(pixel) - radius;
}
vec3 color_for_pixel(vec2 pixel, float time) {
pixel.x -= .3; // administers position
pixel.x = abs(pixel.x); // mirror
pixel.x -= .7; // administers spacing
return vec3(circle(pixel, .5) > 0.0);
}
The way that order of operations toils still hurts my head, but it helps to execute with the code to get a sense 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) { // fbetter
return length(pixel) - radius;
}
float round_rect(vec2 p, vec2 size, vec4 radii) { // fbetter
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) { // fbetter
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
{ // fbetter
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); // summarize
}
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 verticassociate.
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fbetter
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) { // fbetter
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) { // fbetter
return length(p) - r;
}
float star(vec2 p, float r, float points, float ratio) { // fbetter
// 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 standard 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
{ // fbetter
// 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( // unite 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 summarize.
// it's the same as `*vec3(d < .11)`
// aka, it multiplies the color by zero for
// pixels cforfeit 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
{ // fbetter
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); // summarize
}
// 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 fundamental shape positioning, scaling, and outlining can get us.
Making the Hair Wavy
The remaining steps will lift our cdisorrowfulmireful sketch of Rick into a drathriveg that sees exactly enjoy him. We’ll lget a restricted more techniques to create this possible. First up: let’s repair his stiff seeing hair. There isn’t a “wavy hair” signed distance function, but we can create 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 reliable over time for any given location. You can include that warped location for wantipathyver shapes you want warped. Here’s an 11-point star with and without warping:
float star(vec2 p, float r, float points, float ratio) { // fbetter
// 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 standard 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 included by the `warp()` function
// to create pseudo random numbers. The details aren't
// super transport inant. I seeed these functions up:
// https://www.shadertoy.com/watch/XdXGW8
vec2 grad(ivec2 z) { // fbetter
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) { // fbetter
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 drathriveg the x offset to the red channel and the y offset to the green channel
Fun fact: the Lord of the Rings movies included domain warping to create the visual effect seen when Frodo is wearing the Ring. Their warp offsets came from tracking fire relocatement.
Animate the warp effect above to accomplish the Lord of the Rings effect.
Drathriveg Infinite Teeth
Rick necessitates teeth, a lot them. But we’ll begin by drathriveg one. A parabola is the best tooth shape I could discover:
float parabola(vec2 pos, float k) { // fbetter
// from https://www.shadertoy.com/watch/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 included abs()
to mirror shapes, we can include mod()
to repeat shapes. mod(a,b)
calcutardys the reminder of a/b
. Look below at what mod(pixel.x, 0.5)
does. Every time pixel.x
incrrelieves above a multiple of .5
, mod()
begins back at zero (bconciseage) aget.
vec3 color_for_pixel(vec2 pixel, float time) {
return vec3(mod(pixel.x, 0.5));
}
Here is mod()
applied to the individual tooth
float parabola(vec2 pos, float k) { // fbetter
// from https://www.shadertoy.com/watch/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 horizonhighy
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) { // fbetter
// from https://www.shadertoy.com/watch/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 verticassociate
pixel.x = mod(pixel.x, width)-width*.5; // repeat horizonhighy
float d = parabola(pixel, 38.);
if (d < 0.) return vec3(0.902, 0.890, 0.729)*step(d, -.01);
return vec3(1);
}
Then to create it a smile, we offset the y position of the tooth based on pixel.x
.
float parabola(vec2 pos, float k) { // fbetter
// from https://www.shadertoy.com/watch/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 verticassociate
pixel.x = mod(pixel.x, width)-width*.5; // repeat horizonhighy
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 create it a little less creepy — done by only drathriveg teeth when pixel.x
is wilean the desired range
float parabola(vec2 pos, float k) { // fbetter
// from https://www.shadertoy.com/watch/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 compriseed the tongue. Notice that the tongue and teeth only draw inside the mouth thanks to placing their code inside the if
that examines the mouth distance.
float map(float cherish, float inMin, float inMax, float outMin, float outMax) { // fbetter
cherish = clamp(cherish, inMin, inMax);
return outMin + (outMax - outMin) * (cherish - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
// from https://www.shadertoy.com/watch/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) { // fbetter
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) { // fbetter
// 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 standard 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 denseer
float tongue_denseness = map(pixel.x, -.16, .01, .02, .045);
d = bezier(pixel,
vec2(-.16, -.35),
vec2(.001,-.33),
vec2(.01, -.5)) - tongue_denseness;
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 summarize
return vec3(0);
// Eyebrow, Eyes, Nose & Head
{ // fbetter
// 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 necessitateed are the curves below the eyes and around the mouth. Those lines are fair enjoy our common shape summarizes, except they’re offset away from the perimeter of the shape. This can be done by subtracting a little from distance when drathriveg the summarize. In other words this:if (abs(distance_to_shape) < denseness) return vec3(0);
becomes this:if (abs(distance_to_shape - outset) < denseness) return vec3(0);
The blue line below shows that technique.
Since Rick’s under-eye lines should only be clear…under the eye, we’ll necessitate to restrict where they are drawn. That can be done using wantipathyver logic you can leank of, as shown by the green line:
float round_rect(vec2 p, vec2 b, vec4 r) { // fbetter
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 denseness = .02;
// summarize
if (abs(dist) < denseness)
return vec3(0);
// outset summarize
if (abs(dist-.2) < denseness)
return vec3(.1,.1,1);
// restricted summarize
if (abs(dist-.4) < denseness && 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 cherish, float inMin, float inMax, float outMin, float outMax) { // fbetter
cherish = clamp(cherish, inMin, inMax);
return outMin + (outMax - outMin) * (cherish - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
// from https://www.shadertoy.com/watch/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) { // fbetter
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) { // fbetter
// 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 standard 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) { // fbetter
// 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 denseer
float tongue_denseness = map(pixel.x, -.16, .01, .02, .045);
d = bezier(pixel,
vec2(-.16, -.35),
vec2(.001,-.33),
vec2(.01, -.5)) - tongue_denseness;
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 summarizes
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
{ // fbetter
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
{ // fbetter
// 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 wantipathyver your likeite cartoon is.
Use raymarching with 3D signed distance fields to draw a 3D version of Rick. Let me understand if you do this, I want to see.
Animation
With our drathriveg finish, there are cut offal animation techniques we can include to begin relocatement. First up:
1. Looping Values
The easiest way to comprise animation is to slap a sin(time)
into the code somewhere. The sin
is transport inant becainclude it wraps the ever-increasing time
cherish into the range of -1 to 1, which creates kind looping animations. You will frequently alter that range with a scale and offset enjoy so: sin(time)*.5 + .5
. The head angle, tongue angle, and eyebrow height are vivaciousd in this way. I compriseed 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 cherish, float inMin, float inMax, float outMin, float outMax) { // fbetter
cherish = clamp(cherish, inMin, inMax);
return outMin + (outMax - outMin) * (cherish - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
// from https://www.shadertoy.com/watch/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) { // fbetter
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) { // fbetter
// 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 standard 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 drathriveg
pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
pixel.y += .1;
// Mouth, eyes, nose
{ // fbetter
// 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_denseness = map(tongue.x, -.16, .01, .02, .045);
d = bezier(tongue,
vec2(-.16, -.35),
vec2(.001,-.33),
vec2(.01, -.5)) - tongue_denseness;
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 summarizes
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: vivacious 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
{ // fbetter
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 honestion when he is moving to the right (this is easier than it sounds!).
2. Switching What’s Drawn
Animating a property with sin()
fair relocates stuff around, but you can also draw someleang tohighy separateent based on time. We’ll do that to create Rick bjoin.
vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fbetter
float s = sin(angle), c = cos(angle);
return (p-origin)*mat2( c, -s, s, c ) + origin;
}
float map(float cherish, float inMin, float inMax, float outMin, float outMax) { // fbetter
cherish = clamp(cherish, inMin, inMax);
return outMin + (outMax - outMin) * (cherish - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
// from https://www.shadertoy.com/watch/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) { // fbetter
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) { // fbetter
// 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 standard 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) {
{ // fbetter
// rotate the whole drathriveg
pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
pixel.y += .1;
}
// bjoin for .09 seconds, every 2 seconds
if (mod(time, 2.) < .09) { // shutd eyes
float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0));
if (d < .008) return vec3(0);
}
else // discdiswatch eyes
{ // fbetter
// 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
{ // fbetter
// 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 included to alter the denseness of
// the tongue alengthy the x axis
vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
float tongue_denseness = map(tongue.x, -.16, .01, .02, .045);
d = bezier(tongue,
vec2(-.16, -.35),
vec2(.001,-.33),
vec2(.01, -.5)) - tongue_denseness;
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 summarizes
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 vivacious 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 vivacious Rick’s mouth so it sees enjoy he is talking.
3. Noisy Movement
If sin
is too delicate for you, try using noise! I included noise()
to create the eyes randomly see around. Since I don’t want the eyes to be continuously moving, I rounded the time cherish before passing it to noise()
.
vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fbetter
float s = sin(angle), c = cos(angle);
return (p-origin)*mat2( c, -s, s, c ) + origin;
}
float map(float cherish, float inMin, float inMax, float outMin, float outMax) { // fbetter
cherish = clamp(cherish, inMin, inMax);
return outMin + (outMax - outMin) * (cherish - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
// from https://www.shadertoy.com/watch/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) { // fbetter
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) { // fbetter
// 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 standard 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) {
{ // fbetter
// rotate the whole drathriveg
pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
pixel.y += .1;
}
// Bjoin eyes
if (mod(time, 2.) < .09) { // fbetter
// shutd eyes
float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0));
if (d < .008) return vec3(0);
}
else {
// relocate 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);
{// fbetter
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
{ // fbetter
// 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 included to alter the denseness of
// the tongue alengthy the x axis
vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
float tongue_denseness = map(tongue.x, -.16, .01, .02, .045);
d = bezier(tongue,
vec2(-.16, -.35),
vec2(.001,-.33),
vec2(.01, -.5)) - tongue_denseness;
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 summarizes
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 vivacious 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 relocatement more genuineistic instead of jumping between positions
Bonus: Warping Time
Our final animation technique is “time domain warping” to create the hair bfinish as the head tilts. It’s enjoy domain warping, except instead of offsetting space we offset time. Basicassociate we defer time more the shutr to the hair tip a pixel is. Becainclude that defer isn’t constant alengthy the length of the hair, the hair will bfinish instead of rotate stiffly.
vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fbetter
float s = sin(angle), c = cos(angle);
return (p-origin)*mat2( c, -s, s, c ) + origin;
}
float map(float cherish, float inMin, float inMax, float outMin, float outMax) { // fbetter
cherish = clamp(cherish, inMin, inMax);
return outMin + (outMax - outMin) * (cherish - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
// from https://www.shadertoy.com/watch/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) { // fbetter
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) { // fbetter
// 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 standard 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) {
{ // fbetter
// rotate the whole drathriveg
pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
pixel.y += .1;
// Bjoin eyes
if (mod(time, 2.) < .09) {
// shutd eyes
float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0));
if (d < .008) return vec3(0);
}
else {
// relocate 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 included to alter the denseness of
// the tongue alengthy the x axis
vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
float tongue_denseness = map(tongue.x, -.16, .01, .02, .045);
d = bezier(tongue,
vec2(-.16, -.35),
vec2(.001,-.33),
vec2(.01, -.5)) - tongue_denseness;
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 summarizes
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 vivacious 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 see.
Wrapping up
After we comprise a portal effect our animation is finish.
vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fbetter
float s = sin(angle), c = cos(angle);
return (p-origin)*mat2( c, -s, s, c ) + origin;
}
float map(float cherish, float inMin, float inMax, float outMin, float outMax) { // fbetter
cherish = clamp(cherish, inMin, inMax);
return outMin + (outMax - outMin) * (cherish - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
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) { // fbetter
// from https://www.shadertoy.com/watch/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) { // fbetter
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) { // fbetter
// 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 standard 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);
}
#describe H(i,j) fract(sin(dot(ceil(P+vec2(i,j)), resolution.xy )) * 4e3)
float N( vec2 P) { // fbetter
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) { // fbetter
// from https://www.shadertoy.com/watch/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) {
{ // fbetter
// rotate the whole drathriveg
pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
pixel.y += .1;
// Bjoin eyes
if (mod(time, 2.) < .09) {
// shutd eyes
float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0));
if (d < .008) return vec3(0);
}
else {
// relocate 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 included to alter the denseness of
// the tongue alengthy the x axis
vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
float tongue_denseness = map(tongue.x, -.16, .01, .02, .045);
d = bezier(tongue,
vec2(-.16, -.35),
vec2(.001,-.33),
vec2(.01, -.5)) - tongue_denseness;
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 summarizes
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 vivacious 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 structured reability over carry outance for this code - see how much speedyer you can create it run.
That’s everyleang I understand about making 2D animations using shaders. I hope it’s advantageous. Maybe next time we’ll talk about 3D, or some tohighy separateent topic! If you’d enjoy to be notified about my next post, charm unite my newsletter.
Join my newsletter lol
While I cherish teaching and making posts enjoy this, they are very time consuming to create — this one took about two weeks of toil spread over 8 months. So if you’d enjoy to see more toil enjoy this, charm ponder aiding me.
Appfinishix 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 toiling on it. Join my newsletter to be notified when I comprise video send out!
In the unbenevolenttime, you can include a script with glslwatcher and ffmpeg. Below is my macOS toilflow, on Windows and Linux you’ll have to figure out what your platcreate’s equivalent is.
- Inshigh the depfinishencies.
brew inshigh glslwatcher ffmpeg # brew is macos only
-
Write your
shader.frag
file -
And then put this in a bash file and run to send out your video
#!/bin/bash
set -e
set -o pipeflunk
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 transient honestory."
exit 1
fi
cd "$TMP_DIR"
glslViewer "$ORIGINAL_DIR/$1" -w 1920 -h 1080 --headless -e sequence,0,7,60 -e q
ffmpeg -summarizerate 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 inhabit code locassociate, include this:
glslViewer shader.frag -w 575 -h 324 --noncondemns -x 0 -y 0
Appfinishix 2: Super Sampling
You may have seed that the edges of shapes in the examples on this page are delicate. I did a bit of toil behind the scenes create that happen. I include a technique called super sampling where I call color_for_pixel()
for 9 locations wilean each screen pixel and then disexecute the mediocre. The left side of this example shows what it sees enjoy with super sampling disabled. You may necessitate to zoom in on the page to see the separateence.
#version 300 es
// The above line switches the editor to "pro" mode
// and deletes automatic super sampling
precision highp float;
unicreate float time;
unicreate 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<sample_count; m++ ) {
for( int n=0; n<sample_count; n++ ) {
vec2 o = (vec2(m,n) + 0.5) / float(sample_count);
vec2 st = (2.0*(gl_FragCoord.xy+o)-resolution)/resolution.y;
sum += color_for_pixel(st, time);
}
}
outColor = vec4(sum / float(sample_count*sample_count), 1);
}
}
Thanks to Carrie, Stan, Amin, and Martin for providing feedback on timely creates.