Как насчёт того, чтобы сделать анимацию Рика из «Рика и Морти» в 240 строк кода? Никаких библиотек, никаких изображений. Gif был написан в редакторе кода. В оригинале куски кода встроены в статью, так что их можно редактировать, меняя анимацию в режиме реального времени. К сожалению, здесь это реализовать не получилось.

Все приемы, которые использованы здесь для создания Рика, можно использовать для создания других анимаций или графических эффектов. Поехали!

Начало

Для данного проекта я использовал редактор OpenGL Shading Language (GLSL).

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

}

Функция color_for_pixel выполняется на GPU для каждого пикселя в предварительном просмотре. Удивительно, но это все, что потребуется для создания анимации — функция, которая отвечает на вопрос «Какого цвета должен быть этот пиксель в данный момент?».

Используем встроенную функцию GLSL length() , чтобы задать, расстояние каждого пикселя от центра экрана (оно же начало координат, оно же позиция (0,0)). Возвращая это расстояние как цвет пикселя, мы получаем 0 (чёрный) около центра и плавно переходим дальше к 1 (белый):

vec3 color_for_pixel(vec2 pixel, float time) {

   return vec3(length(pixel));

}

Совет GLSL: vec3(x) то же самое, что и vec3(x, x, x). Мы будем часто использовать этот приём.

Чтобы нарисовать круг, мы сравниваем расстояние с радиусом:

vec3 color_for_pixel(vec2 pixel, float time) {

   float radius = 0.6;

   return vec3(length(pixel) > radius);

}

Совет GLSL: vec3 преобразует логический результат > в 1 или 0.

Мы можем извлечь это в функцию circle() многократного использования:

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);
}

Позиция кругов определяется путём сдвига пикселя, переданного в circle(). Порядок строк этого кода важен — он определяет, какой круг появится перед другим.

Обратите внимание, чтобы указать положение внутри/снаружи circle() возвращает расстояние до периметра, а не просто bool. Это называется функция «поля расстояний со знаком» (SDF). «Со знаком» здесь означает, что расстояния для местоположений внутри фигуры отрицательные, а снаружи положительные. Мы используем расстояние для достижения некоторых интересных эффектов немного позже.

Помимо SDF существует множество функций circle(). Вот те из них, которые будем использовать:

// Click {...} to see the code

float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {...}

float star(vec2 p, float r, float points, float ratio) {...}

float round_rect(vec2 p, vec2 size, vec4 radii) {...}

​

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

}

Это были основы. Переходим к Рику.

Рисуем Рика

Хотел бы я сказать, что могу посмотреть мультфильм, а затем без усилий воспроизвести портрет Рика в коде. К сожалению, это не так. Я потратил много времени, кропотливо перебирая числа, чтобы воссоздать лицо Рика с постера первого сезона.

Я нашёл один трюк, который ускорил процесс проб и ошибок: высветил своё референсное изображение Рика, наложив его на картинку предварительного просмотра, чтобы сравнивать свой рисунок с оригиналом, в процессе изменения кода.

float round_rect(vec2 p, vec2 size, vec4 radii) {...}
 

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

}

Если это ещё не очевидно, приёмы в этой статье не заменят ваш любимый инструмент векторного рисования. Все, казалось бы, случайные числа в остальной части этой статьи были обнаружены в ходе этого процесса. Я нашёл значения цвета с помощью обычной пипетки в редакторе изображений.

Едем дальше. Вот значения, которые я придумал для головы Рика. Я также добавил второе round_rect() для его уха:

float round_rect(vec2 p, vec2 size, vec4 radii) {...}

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

}

Давайте добавим контур. Вот где пригодится рисование с помощью функций расстояния со знаком. Мы можем вернуть чёрный цвет для пикселей с расстоянием от -0,01 до 0,0.

float round_rect(vec2 p, vec2 size, vec4 radii) {...}

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

}

У нас получилась линия между головой и ухом, которой, согласно референсной картинке, быть не должно. Я не хочу обводить каждую форму по отдельности, лучше сделать объединение форм. Объединение легко выполняется с помощью SDF — используйте min() для объединения двух расстояний:

float round_rect(vec2 p, vec2 size, vec4 radii) {...}

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

}

Давайте нарисуем глаз:

float circle(vec2 pixel, float radius) {...}

float round_rect(vec2 p, vec2 size, vec4 radii) {...}

float star(vec2 p, float r, float points, float ratio) {...}

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

   {...}

   return vec3(1.);

}

Здесь есть две интересные вещи:

  1. eyeball_pos.y *= .9 немного растягивает глазное яблоко. Процесс аналогичен тому, как мы перемещаем фигуры, увеличивая значение позиции, мы масштабируем изображение.

  2. Для рисования глаза я использовал 6-конечную звезду и вычел небольшое значение из расстояния звезды, чтобы скруглить её углы. Любая форма SDF может быть скруглена таким образом. Это помогает визуализировать поле расстояния, чтобы вы видели, как оно становится более круглым по мере удаления от формы:

float star(vec2 p, float r, float points, float ratio) {...}

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;

}

Для второго глаза мы могли бы продублировать код первого глаза, но вместо этого давайте отразим его по горизонтали с помощью pixel.x = abs(pixel.x). Чтобы рационализировать такое решение, будем считать, что если точка (1, 0) находится внутри круга, то её зеркальное отображение (-1, 0) также будет внутри круга, поэтому обе точки будут окрашены. pixel.x = abs(pixel.x)

float circle(vec2 pixel, float 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);

}

Такой порядок работы до сих пор сбивает меня с толку, но это помогает поиграться с кодом и понять, что происходит.

Вот техника зеркалирования, применённая к глазам Рика:

float circle(vec2 pixel, float radius) {...}

float round_rect(vec2 p, vec2 size, vec4 radii) {...}

float star(vec2 p, float r, float points, float ratio) {...}

​

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

   {...}  
 
   return vec3(1);

}

Рот, нос и бровь созданы с помощью bezier(). Волосы — это 11-точечный объект star(), который я растянул по вертикали.

float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {...}

float round_rect(vec2 p, vec2 b, vec4 r) {...}

float circle(vec2 p, float r) {...}

float star(vec2 p, float r, float points, float ratio) {...}

​

vec3 color_for_pixel(vec2 pixel, float time) {

   float d;

  // eyes

  {...}

     // 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

   {...}

     // 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.);

}

Вот и все, что можно получить с помощью базовых функций позиционирования, масштабирования и обводки фигур.

Делаем волосы волнистыми

Оставшиеся шаги превратят наш грубый набросок Рика в рисунок, который в точности соответствует изображению персонажа. Чтобы добиться этого, изучим ещё несколько приёмов. Сначала исправим его прямые волосы. Нет функции расстояния со знаком «волнистые волосы», но мы можем сделать форму звезды более волнистой, используя технику, называемую доменным искажением.

Доменное искажение случайным образом смещает местоположения пикселей. Это случайное смещение «задаётся» местоположением пикселя, поэтому смещение постоянно во времени для любого данного местоположения. Вы можете использовать это для любых форм, которые вы хотите деформировать. Вот 11-конечная звезда с деформацией и без неё:

float star(vec2 p, float r, float points, float ratio) {...}

​

// 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)  {...}

float noise(vec2 p) {...}

​

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

}

Интересный факт: в фильмах «Властелин колец» для создания визуального эффекта, который можно увидеть, когда Фродо носит Кольцо, использовалось доменное искажение.

Рисуем бесконечные зубы

Рику нужны зубы, много зубов. Но начнём с рисования одного. Парабола — лучшая форма зуба, которую я смог найти:

float parabola(vec2 pos, float k) {...}

​

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

}

Да, это зуб. Не расходимся.

Есть ли способ нарисовать несколько зубов, не дублируя кучу кода или не используя цикл for? Да! Подобно тому, как мы использовали abs() для зеркального отображения фигур, мы можем использовать mod() для повторения фигур. mod(a,b) вычисляет остаток от a/b. Посмотрите ниже, что делает mod(pixel.x, 0.5). Каждый раз, когда pixel.x становится выше .5, mod(), он снова начинается с нуля (чёрный).

vec3 color_for_pixel(vec2 pixel, float time) {

   return vec3(mod(pixel.x, 0.5));

}

Здесь mod() применяется к одному зубу

float parabola(vec2 pos, float k) {...}

​

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

}

А теперь отразим эту часть по горизонтали, чтобы получить нижние зубы

float parabola(vec2 pos, float k) {...}

​

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

}

Затем, чтобы сделать улыбку, мы смещаем положение зуба по оси Y на основе pixel.x.

float parabola(vec2 pos, float k) {...}

​

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

}

Выглядит жутковато. Уменьшим бесконечные зубы до 12, так уже менее жутко. Это делается путём рисования зубов только тогда, когда pixel.x находятся в желаемом диапазоне

float parabola(vec2 pos, float k) {...}

​

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

}

Вот Рик с волнистыми волосами и новым набором зубов. Я также добавил язык. Обратите внимание, что язык и зубы рисуются только внутри рта благодаря размещению их кода внутри if, который проверяет расстояние до рта.

float map(float value, float inMin, float inMax, float outMin, float outMax) {...}

vec2 grad(ivec2 z)  {...}

float noise(vec2 p) {...}

vec2 warp(vec2 p, float scale, float strength) {...}

float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {...}

float parabola(vec2 pos, float k) {...}

float round_rect(vec2 p, vec2 b, vec4 r) {...}

float star(vec2 p, float r, float points, float ratio) {...}

​

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

  {...}
 

   // 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.);

}

Художественные линии

Последние штрихи — кривые под глазами и вокруг рта. Эти линии такие же, как наши обычные контуры формы, за исключением того, что они смещены от периметра формы. Это можно сделать небольшим вычитанием из расстояния при рисовании контура. Другими словами:

if (abs(distance_to_shape) < thickness) return vec3(0);

становится этим:

if (abs(distance_to_shape - outset) < thickness) return vec3(0);

Синяя линия ниже иллюстрирует эту технику.

Поскольку линии под глазами Рика должны быть видны только... под глазом, нам нужно будет ограничить локацию, где они будут нарисованы. Это можно сделать, используя любую логику, которую вы можете придумать, как показано зелёной линией:

float round_rect(vec2 p, vec2 b, vec4 r) {...}

​

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

}

Применил эти методы к Рику:

float map(float value, float inMin, float inMax, float outMin, float outMax) {...}

vec2 grad(ivec2 z)  {...}

float noise(vec2 p) {...}

vec2 warp(vec2 p, float scale, float strength) {...}

float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {...}

float parabola(vec2 pos, float k) {...}

float round_rect(vec2 p, vec2 b, vec4 r) {...}

float star(vec2 p, float r, float points, float ratio) {...}

​

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

   // 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

   {...}
      

   // 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

   {...}

   return vec3(1);

}

Анимация

Когда наш рисунок готов, можно применить несколько анимационных приёмов, чтобы оживить его:

1. Циклические значения

Самый простой способ добавить анимацию — вставить куда‑нибудь в код sin(time). sin важен, потому что он оборачивает постоянно увеличивающееся значение time в диапазон от -1 до 1, что делает хорошую циклическую анимацию. Вы часто будете изменять этот диапазон с помощью масштаба и смещения, например: sin(time)*.5 +.5. Таким образом анимируются угол наклона головы, угол наклона языка и высота бровей. Я добавил функцию rotateAt для выполнения математики вращения.

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

vec2 grad(ivec2 z)  {...}

float noise(vec2 p) {...}

vec2 warp(vec2 p, float scale, float strength) {...}

float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {...}

float parabola(vec2 pos, float k) {...}

float round_rect(vec2 p, vec2 b, vec4 r) {...}

float star(vec2 p, float r, float points, float ratio) {...}

​

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

   {...}

​

   // 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

   {...}
 
   return vec3(1.);

}

2. Переключение нарисованного

Анимация свойства с помощью sin() просто перемещает элементы, но вы также можете нарисовать что-то совершенно другое. Мы сделаем это, чтобы заставить Рика моргать.

vec2 rotateAt(vec2 p, float angle, vec2 origin) {...}

float map(float value, float inMin, float inMax, float outMin, float outMax) {...}

vec2 grad(ivec2 z)  {...}

float noise(vec2 p) {...}

vec2 warp(vec2 p, float scale, float strength) {...}

float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {...}

float parabola(vec2 pos, float k) {...}

float round_rect(vec2 p, vec2 b, vec4 r) {...}

float star(vec2 p, float r, float points, float ratio) {...}

​

vec3 color_for_pixel(vec2 pixel, float time) {    

   {...}

​

   // 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

   {...}

    // Rest of face

   {...}

   return vec3(1);

}

3. "Шумное" движение

Если sin кажется вам слишком гладким, попробуйте использовать noise! С помощью noise() я заставлял глаза беспорядочно оглядываться. Поскольку я не хочу, чтобы глаза постоянно двигались, я округлил значение времени перед передачей его в noise().

vec2 rotateAt(vec2 p, float angle, vec2 origin) {...}

float map(float value, float inMin, float inMax, float outMin, float outMax) {...}

vec2 grad(ivec2 z)  {...}

float noise(vec2 p) {...}

vec2 warp(vec2 p, float scale, float strength) {...}

float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {...}

float parabola(vec2 pos, float k) {...}

float round_rect(vec2 p, vec2 b, vec4 r) {...}

float star(vec2 p, float r, float points, float ratio) {...}

​

vec3 color_for_pixel(vec2 pixel, float time) {

   {...}

   // Blink eyes

   if (mod(time, 2.) < .09) {...}

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

       {...}

   }

   // Rest of face

   {...}

   return vec3(1);

}

Бонус: Искривление времени

Наша последняя техника анимации — «доменное искажение времени» — заставила волосы изгибаться при наклоне головы. Это похоже на доменное искажение, только вместо смещения пространства мы смещаем время. По сути, мы задерживаем время тем больше, чем ближе к кончику волоса находится пиксель. Поскольку эта задержка не постоянна по длине волоса, волосы будут плавно изгибаться, а не однотипно вращаться.

vec2 rotateAt(vec2 p, float angle, vec2 origin) {...}

float map(float value, float inMin, float inMax, float outMin, float outMax) {...}

vec2 grad(ivec2 z)  {...}

float noise(vec2 p) {...}

vec2 warp(vec2 p, float scale, float strength) {...}

float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {...}

float parabola(vec2 pos, float k) {...}

float round_rect(vec2 p, vec2 b, vec4 r) {...}

float star(vec2 p, float r, float points, float ratio) {...}

​

vec3 color_for_pixel(vec2 pixel, float time) {

   {...}
 
   // 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);

}

Подведение итогов

После добавления эффекта портала наша анимация  будет завершена.

vec2 rotateAt(vec2 p, float angle, vec2 origin) {...}

float map(float value, float inMin, float inMax, float outMin, float outMax) {...}

vec2 grad(ivec2 z)  {...}

float noise(vec2 p) {...}

vec2 warp(vec2 p, float scale, float strength) {...}

float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {...}

float parabola(vec2 pos, float k) {...}

float round_rect(vec2 p, vec2 b, vec4 r) {...}

float star(vec2 p, float r, float points, float ratio) {...}

#define H(i,j) fract(sin(dot(ceil(P+vec2(i,j)), resolution.xy )) * 4e3)

float N( vec2 P) {...}

vec3 portal(vec2 pixel, float time) {...}

​

vec3 color_for_pixel(vec2 pixel, float time) {

   {...}

   return portal(pixel, time);

}

Можете поэкспериментировать с кодом, чтобы сделать Рика чуть-чуть другим. Или вообще придумать своего персонажа.

Вот и всё, спасибо за внимание! Ваш Cloud4Y. Читайте нас здесь или в Telegram‑канале!

Комментарии (1)


  1. nulovkin
    07.02.2025 08:27

    Это невероятно!

    Ты очень хорош в этом