Как насчёт того, чтобы сделать анимацию Рика из «Рика и Морти» в 240 строк кода? Никаких библиотек, никаких изображений. Gif был написан в редакторе кода. В оригинале куски кода встроены в статью, так что их можно редактировать, меняя анимацию в режиме реального времени. К сожалению, здесь это реализовать не получилось.
![](https://habrastorage.org/getpro/habr/upload_files/e4d/4c3/4b9/e4d4c34b9e7a97aa8db78297a4074dc5.gif)
Все приемы, которые использованы здесь для создания Рика, можно использовать для создания других анимаций или графических эффектов. Поехали!
Начало
Для данного проекта я использовал редактор OpenGL Shading Language (GLSL).
![](https://habrastorage.org/getpro/habr/upload_files/0dd/003/3b9/0dd0033b98de09995e90dde66ab9b6f8.png)
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 (белый):
![](https://habrastorage.org/getpro/habr/upload_files/a22/5e1/705/a225e1705ac0feb79fdec509008972b5.png)
vec3 color_for_pixel(vec2 pixel, float time) {
return vec3(length(pixel));
}
Совет GLSL: vec3(x) то же самое, что и vec3(x, x, x). Мы будем часто использовать этот приём.
Чтобы нарисовать круг, мы сравниваем расстояние с радиусом:
![](https://habrastorage.org/getpro/habr/upload_files/28e/432/e00/28e432e00eb9e287990a02d7a82d9006.png)
vec3 color_for_pixel(vec2 pixel, float time) {
float radius = 0.6;
return vec3(length(pixel) > radius);
}
Совет GLSL: vec3 преобразует логический результат >
в 1 или 0.
Мы можем извлечь это в функцию circle() многократного использования:
![](https://habrastorage.org/getpro/habr/upload_files/40d/fce/11f/40dfce11fb16189f6293f8b9281bdb0f.png)
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()
. Вот те из них, которые будем использовать:
![](https://habrastorage.org/getpro/habr/upload_files/abd/693/ec6/abd693ec6f39b1d07c575467d113d3f1.png)
// 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);
}
Это были основы. Переходим к Рику.
Рисуем Рика
Хотел бы я сказать, что могу посмотреть мультфильм, а затем без усилий воспроизвести портрет Рика в коде. К сожалению, это не так. Я потратил много времени, кропотливо перебирая числа, чтобы воссоздать лицо Рика с постера первого сезона.
Я нашёл один трюк, который ускорил процесс проб и ошибок: высветил своё референсное изображение Рика, наложив его на картинку предварительного просмотра, чтобы сравнивать свой рисунок с оригиналом, в процессе изменения кода.
![](https://habrastorage.org/getpro/habr/upload_files/690/b75/102/690b7510239a2a44b45fe9e5f64610da.png)
![](https://habrastorage.org/getpro/habr/upload_files/c5e/286/59a/c5e28659a9876383b80f7fdd8efc4a1e.png)
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()
для его уха:
![](https://habrastorage.org/getpro/habr/upload_files/c8a/b1d/2a7/c8ab1d2a719d0fead0e579e5024d7bee.png)
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.
![](https://habrastorage.org/getpro/habr/upload_files/65e/3d2/5ae/65e3d25ae597d62aa1a5d750b66fd0c5.png)
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()
для объединения двух расстояний:
![](https://habrastorage.org/getpro/habr/upload_files/19b/d29/d6a/19bd29d6ad0f038c4dd1a2df4e846af5.png)
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);
}
Давайте нарисуем глаз:
![](https://habrastorage.org/getpro/habr/upload_files/83a/6f2/ce4/83a6f2ce4d7adef9c89b673c0e778cc0.png)
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.);
}
Здесь есть две интересные вещи:
eyeball_pos.y *= .9
немного растягивает глазное яблоко. Процесс аналогичен тому, как мы перемещаем фигуры, увеличивая значение позиции, мы масштабируем изображение.Для рисования глаза я использовал 6-конечную звезду и вычел небольшое значение из расстояния звезды, чтобы скруглить её углы. Любая форма SDF может быть скруглена таким образом. Это помогает визуализировать поле расстояния, чтобы вы видели, как оно становится более круглым по мере удаления от формы:
![](https://habrastorage.org/getpro/habr/upload_files/0b5/d33/3ce/0b5d333ce0b9f8b29df7fbc1184aec45.png)
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)
![](https://habrastorage.org/getpro/habr/upload_files/d57/aeb/d4e/d57aebd4e8e04c124a95a4d44d05f823.png)
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);
}
Такой порядок работы до сих пор сбивает меня с толку, но это помогает поиграться с кодом и понять, что происходит.
Вот техника зеркалирования, применённая к глазам Рика:
![](https://habrastorage.org/getpro/habr/upload_files/ad9/273/d15/ad9273d15f8ec4dc81b4b16d779e0a61.png)
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()
, который я растянул по вертикали.
![](https://habrastorage.org/getpro/habr/upload_files/40f/29a/a74/40f29aa74e726d5a854e065f35abd02c.png)
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-конечная звезда с деформацией и без неё:
![](https://habrastorage.org/getpro/habr/upload_files/fce/477/47e/fce47747e7a1d54aab1810a72333dc0b.png)
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);
}
Интересный факт: в фильмах «Властелин колец» для создания визуального эффекта, который можно увидеть, когда Фродо носит Кольцо, использовалось доменное искажение.
Рисуем бесконечные зубы
Рику нужны зубы, много зубов. Но начнём с рисования одного. Парабола — лучшая форма зуба, которую я смог найти:
![](https://habrastorage.org/getpro/habr/upload_files/155/d4c/fd6/155d4cfd610a8821e851f2e1490330d9.png)
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()
, он снова начинается с нуля (чёрный).
![](https://habrastorage.org/getpro/habr/upload_files/8ae/8dd/578/8ae8dd578f0a8f221d2b01315708c803.png)
vec3 color_for_pixel(vec2 pixel, float time) {
return vec3(mod(pixel.x, 0.5));
}
Здесь mod()
применяется к одному зубу
![](https://habrastorage.org/getpro/habr/upload_files/c31/036/33a/c3103633a1cf21c9049bb4aeafbde02c.png)
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);
}
А теперь отразим эту часть по горизонтали, чтобы получить нижние зубы
![](https://habrastorage.org/getpro/habr/upload_files/58e/1b9/94d/58e1b994d12ffc8c29cbd786d7f36fb0.png)
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.
![](https://habrastorage.org/getpro/habr/upload_files/298/63e/1b9/29863e1b9a38a97f376e8f0326d72f40.png)
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 находятся в желаемом диапазоне
![](https://habrastorage.org/getpro/habr/upload_files/429/4d5/11b/4294d511b5ed39cb7b7f3143ee3ce028.png)
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
, который проверяет расстояние до рта.
![](https://habrastorage.org/getpro/habr/upload_files/813/4b8/70d/8134b870d633d36a148761ad54332e37.png)
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);
Синяя линия ниже иллюстрирует эту технику.
Поскольку линии под глазами Рика должны быть видны только... под глазом, нам нужно будет ограничить локацию, где они будут нарисованы. Это можно сделать, используя любую логику, которую вы можете придумать, как показано зелёной линией:
![](https://habrastorage.org/getpro/habr/upload_files/f23/cc5/fd2/f23cc5fd292820f74fb520bb17ed8ed6.png)
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);
}
Применил эти методы к Рику:
![](https://habrastorage.org/getpro/habr/upload_files/6c5/af3/9ac/6c5af39ac04502a060b050d3607f49d6.png)
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
для выполнения математики вращения.
![](https://habrastorage.org/getpro/habr/upload_files/704/20a/bb4/70420abb4a72ef9470628ac0c010b8fd.gif)
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()
просто перемещает элементы, но вы также можете нарисовать что-то совершенно другое. Мы сделаем это, чтобы заставить Рика моргать.
![](https://habrastorage.org/getpro/habr/upload_files/0f9/265/b6d/0f9265b6d2685554f598378398e96f9e.gif)
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()
.
![](https://habrastorage.org/getpro/habr/upload_files/26d/3c6/ce3/26d3c6ce3dac6a573927c567f47fa0a5.gif)
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);
}
Бонус: Искривление времени
Наша последняя техника анимации — «доменное искажение времени» — заставила волосы изгибаться при наклоне головы. Это похоже на доменное искажение, только вместо смещения пространства мы смещаем время. По сути, мы задерживаем время тем больше, чем ближе к кончику волоса находится пиксель. Поскольку эта задержка не постоянна по длине волоса, волосы будут плавно изгибаться, а не однотипно вращаться.
![](https://habrastorage.org/getpro/habr/upload_files/b00/d8a/517/b00d8a517cc4681bd3008c07b1becd9e.gif)
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);
}
Подведение итогов
После добавления эффекта портала наша анимация будет завершена.
![](https://habrastorage.org/getpro/habr/upload_files/349/675/a01/349675a01fe52ef3cc68933581d57046.gif)
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‑канале!
nulovkin
Это невероятно!
Ты очень хорош в этом