Pull to refresh

Изучаем программирование шейдеров с Риком и Морти

Level of difficultyMedium
Reading time31 min
Views2.9K
Original author: Daniel Hooper

Эта анимация Рика состоит из 240 строк кода. Никаких библиотек и изображений. В статье я покажу, как использовать шейдеры GPU и поля расстояний со знаком для создания подобных анимаций для видео, видеоигр и просто для развлечения! В оригинале статьи даже есть интерактивный редактор кода, благодаря которому вы сможете поэкспериментировать с примерами.

Видео в h.264

vec3 color_for_pixel(vec2 pixel, float time) {
    // fract возвращает дробную часть. fract(1.3) == 0.3
    float red   = fract(pixel.y); 
    float green = 0.9;
    float blue  = fract(pixel.x);
    return vec3(red, green, blue);
}

Код написан на OpenGL Shading Language (GLSL), функция color_for_pixel выполняется в GPU для каждого пикселя показанного выше изображения. Как ни удивительно, но это всё, что необходимо для создания анимаций, а именно функция, дающая ответ на вопрос «Какого цвета должен быть этот пиксель в данный момент времени?»

Дополнительное задание: что случится, если задать green = time? Что можно сделать, чтобы изменения продолжались? (time — это секунды после последнего редактирования кода).

Чтобы рисовать Рика, мы начнём с круга, а потом дополним его другими формами. Воспользуемся встроенной функцией GLSL length() , чтобы визуализировать расстояние каждого пикселя от центра экрана (он же origin или позиция (0,0)). [На последней странице документации есть список встроенных функций GLSL.] Возвращая это расстояние как цвет пикселя, мы получим 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.

Дополнительное задание: Как будет выглядеть этот круг, если заменить length() своей функцией, которая вычисляет расстояние городских кварталов?

Мы можем извлечь это в многократно используемую функцию 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 , обозначающий, что мы находимся внутри/снаружи. Это называется функцией полей расстояний со знаком (signed distance field, SDF). «Со знаком» здесь означает, что расстояния для точек внутри формы отрицательные, а для точек снаружи — положительные. Скоро мы воспользуемся расстоянием для создания замечательных эффектов.

Наряду с circle() существует ещё много функций SDF. Мы будем использовать следующие:

float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { 
    // из https://www.shadertoy.com/view/MlKcDD
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float star(vec2 p, float r, float points, float ratio) { 
    // из https://www.shadertoy.com/view/3tSGDy
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en));

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}
float round_rect(vec2 p, vec2 size, vec4 radii) { 
    // из https://www.shadertoy.com/view/4llXD7
    radii.xy = (p.x>0.0)?radii.xy : radii.zw;
    radii.x  = (p.y>0.0)?radii.x  : radii.y;
    vec2 q = abs(p)-size+radii.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x;
}

vec3 color_for_pixel(vec2 pixel, float time) {
    if (bezier(pixel, 
        vec2(-.7,-.35), 
        vec2(-1.5,-.4), 
        vec2(-1.2,.35)) < 0.1) 
        return vec3(.9,.3,.3); 
    
    if (round_rect(pixel, vec2(.3, .4), vec4(.1)) < 0.0) 
        return vec3(.3, .9, .3); 
    
    if (star(pixel - vec2(1.,0.), .45, 5., .3) < 0.0) 
        return vec3(.2, .4, .9);
    
    return vec3(1.0);
}

Получив все эти формы, давайте приступим к рисованию Рика.

Рисуем Рика

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

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

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

float round_rect(vec2 p, vec2 size, vec4 radii) {
    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, 
        // Измените эти значения:
        vec2(.3, .5),  // размер
        vec4(.1, .01, .05, .1) // радиусы углов
    );
    
    if (dist < 0.) 
        return vec3(.838, 0.8, 0.76);
    
    return vec3(1);
}

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

Итак, вот значения, которые я подобрал для головы Рика. Также я добавил второй round_rect() для его уха:

float round_rect(vec2 p, vec2 size, vec4 radii) {
    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);
    
    // голова
    float dist = round_rect(
        pixel, 
        vec2(.36, 0.6385), 
        vec4(.34, .415, .363, .315)
    );
    if (dist < 0.) return skin_color;
    
    // ухо
    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) {
    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);
    
    // голова
    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); // контур
    
    // ухо
    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); // контур
    
    
    return vec3(1); // фон
}

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

float round_rect(vec2 p, vec2 size, vec4 radii) {
    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( // <- комбинируем фигуры
        // голова
        round_rect(
        pixel, 
        vec2(.36, 0.6385), 
        vec4(.34, .415, .363, .315)),
        
        // ухо
        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);
}

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

Нарисуем глаз:

float circle(vec2 pixel, float radius) { 
    return length(pixel) - radius;
}
float round_rect(vec2 p, vec2 size, vec4 radii) {
    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) {
    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) {
     // зрачок
    vec2 pupil_pos = pixel - vec2(.16-.13,.24); 

    // вычитаем 0.007, чтобы сместить наружу и скруглить углы звёздочки
    if (star(pupil_pos, 0.019, 6., .9) - 0.007 < 0.0) {
        return vec3(.1);
    }
    
    // глазное яблоко
    vec2 eyeball_pos = pixel;
    eyeball_pos.y *= .93; // растягиваем по вертикали
    eyeball_pos -= vec2(0.07, .16);
    float dist = circle(eyeball_pos, .16);
    if (dist < 0.0) return vec3(dist < -0.013);
    
    // голова
    { 
    dist = min(
        // голова
        round_rect(
        pixel, 
        vec2(.36, 0.6385), 
        vec4(.34, .415, .363, .315)),
        
        // ухо
        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.);
}

Здесь стоит отметить два интересных момента:

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

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

float star(vec2 p, float r, float points, float ratio) {
    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);
    
    // рисуем синим внутри фигуры, оранжевым снаружи
    vec3 color = (d < 0.0) ? vec3(0.5, .8, 1.) : vec3(0.98,.6,.13);
    color *= sin(d*150.)*.1+.8; // показываем линии поля расстояний
    color *= 1.0 - exp(-20.0*abs(d)); // затемняем рядом с периметром
    
    float offset = (sin(time)+1.)*.25; // анимируем смещение контура
    if (abs(d-offset) < 0.01) return vec3(1.0); // рисуем белый контур
  
    return color;
}

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

float circle(vec2 pixel, float radius) {
    return length(pixel) - radius;
}
   
vec3 color_for_pixel(vec2 pixel, float time) {
    pixel.x -= .3; // управляет позицией
    pixel.x = abs(pixel.x); // отзеркаливание
    pixel.x -= .7; // управляет расстоянием между глазами
    return vec3(circle(pixel, .5) > 0.0);
}

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

Дополнительное задание: отзеркальте круги по осям x и y

Вот результат отзеркаливания глаз Рика:

float circle(vec2 pixel, float radius) {
    return length(pixel) - radius;
}
float round_rect(vec2 p, vec2 size, vec4 radii) {
    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) {
    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) {
     // зрачки
    vec2 pupil_pos = pixel;
    pupil_pos += vec2(.13, -.24); // позиционируем зрачки на глазных яблоках
    pupil_pos.x = abs(pupil_pos.x); // отзеркаливаем зрачки
    pupil_pos.x -= .16; // расстояние между зрачками
    if (star(pupil_pos, 0.019, 6., .9) < 0.007) {
        return vec3(.1);
    }
    
    // глазные яблоки
    // однострочник для задания позиции/отзеркаливания/масштабирования
    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);
    
    // голова
    {
        dist = min(
            // head
            round_rect(
            pixel, 
            vec2(.36, 0.6385), 
            vec4(.34, .415, .363, .315)),
            
            // ухо
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, .12), 
            vec4(.13, .1, .13, .13))
        );
            
        if (dist < -0.01) return vec3(0.838, 0.799, 0.760);
        if (dist < 0.0) return vec3(0); // outline
    }   
    
    return vec3(1);
}

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

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 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 circle(vec2 p, float r) {
    return length(p) - r;
}
float star(vec2 p, float r, float points, float ratio) {
    // следующие четыре строки можно рассчитать заранее для нужной фигуры
    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) для обычного многоугольника

    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;
  
   // глаза
   {
        // зрачки
        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);
        }
        
        // глазные яблоки
        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));
        }
    }
    
    // нос  
    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);
  
        
    // рот
    d = bezier(pixel,  
                 vec2(-.26, -.28), 
                 vec2(-.05,-.42), 
                 vec2(.115, -.25));
    if (d < .12) {
        // `*step(d, .11)` создаёт контур.
        // это аналогично `*vec3(d < .11)` 
        // то есть цвет умножается на ноль для пикселей
        // рядом с периметром  
        return vec3(.42, .147, .152)*step(d, .11);
    }
  
    // бровь
    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);

    // голова
    {
        float dist = min(
            // голова
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // ухо
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, 0.12), 
            vec4(.13,.1,.13,.13))
        );
            
        if (dist < -.01) return vec3(.838, .799, .76);
        if (dist < 0.) return vec3(0); // outline
    }
    
    // волосы
    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.);
}

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

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

На последующих этапах мы превратим грубый набросок Рика в рисунок, в точности похожий на него. Для этого нам нужно научиться ещё нескольким техникам. Для начала исправим эти прямые волосы. Для придания волосам волнистости нет SDF, но мы можем сделать форму звезды более волнистой при помощи domain warping.

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

float star(vec2 p, float r, float points, float ratio) {
    // следующие четыре строки можно рассчитать заранее для нужной фигуры
    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) для правильного многоугольника

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

// эти функции используются функцией `warp()` 
// для генерации псевдослучайных чисел. Подробности реализации
// не так важны. Эти функции я нашёл здесь:
// https://www.shadertoy.com/view/XdXGW8
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;
}

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

Дополнительное задание: визуализируйте смещения искажений, отрисовывая смещение по x в красный канал, а смещение по y в зелёный канал

Любопытный факт: в кинотрилогии «Властелине колец» domain warping используется для создания визуального эффекта, который мы видим, когда Фродо надевает Кольцо. Смещения искажений взяты из отслеживания движения огня.

Дополнительное задание: анимируйте представленный выше эффект искажений, чтобы получить эффект в стиле «Властелина колец».

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

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

float parabola(vec2 pos, float k) {
    // из https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float d = parabola(pixel, 38.);
    if (d < 0.) return vec3(0.902, 0.890, 0.729)*step(d, -.01);

    return vec3(1);
}

Да, это зуб, сейчас всё объясню.

Можно ли как-то нарисовать много зубов, не повторяя большой объём кода или с помощью цикла 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) {
    // из https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float width = .065;    
    pixel.x = mod(pixel.x, width)-width*.5; // НОВОЕ: повторяем по горизонтали
    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) {
    // из https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float width = .065;
    pixel.y = abs(pixel.y)-.06; // НОВОЕ: отзеркаливаем по вертикали
    pixel.x = mod(pixel.x, width)-width*.5; // повторяем по горизонтали
    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) {
    // из https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float width = .065;
    pixel.y -= pow(pixel.x, 2.); // НОВОЕ: превращаем кривую в улыбку
    pixel.y = abs(pixel.y)-.06; // отзеркаливаем по вертикали
    pixel.x = mod(pixel.x, width)-width*.5; // повторяем по горизонтали
    float d = parabola(pixel, 38.);
    if (d < 0.) return vec3(0.902, 0.890, 0.729)*step(d, -.01);

    return vec3(1);
}

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

float parabola(vec2 pos, float k) {
    // из https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float width = .065;
    vec2 teeth = pixel;
    teeth.y -= pow(pixel.x, 2.);
    teeth.y = abs(teeth.y)-.06; 
    teeth.x = mod(teeth.x, width)-width*.5;
    float d = parabola(teeth, 38.);
    if (d < 0. 
        // Ограничиваем диапазон отрисовки зубов
        && 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) {
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  {
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) {
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) {
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) {
    // из https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) {
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) {
    // следующие четыре строки можно вычислить заранее для заданной формы
    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) для правильного многоугольника

    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;
  
    // Рот
    d = bezier(pixel,  
                 vec2(-.26, -.28), 
                 vec2(-.05,-.42), 
                 vec2(.115, -.25));
    if (d < .11) {
        // отрисовываем зубы и язык только внутри формы рта
        
        // Зубы
        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);
      
        // Язык
        // Делаем правую часть языка толще
        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);
          
        // цвет заливки рта
        return vec3(.42, .147, .152); 
    } 
    if (d < .12) // контур рта
        return vec3(0); 
  
    // Бровь, глаза, нос и голова
   {
        // Зрачки
        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);
        }
        
        // Глазные яблоки
        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));
        }
    
        // Нос
        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);

        // Бровь
        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(
            // Голова
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // Ухо
            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);
    }
    
    // Волосы
    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) {
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float dist = round_rect(pixel, vec2(.5), vec4(.1));
    float thickness = .02;
    
    // контур
    if (abs(dist) < thickness) 
        return vec3(0); 
    
    // смещённый наружу контур
    if (abs(dist-.2) < thickness) 
        return vec3(.1,.1,1); 
    
    // ограниченный контур
    if (abs(dist-.4) < thickness && pixel.y < -.4) 
        return vec3(.1,.9,.1); 
    
    // заливка
    if (dist < 0.) return vec3(1); 
    
    return vec3(.92);    
}

Применяем эти техники к лицу Рика:

float map(float value, float inMin, float inMax, float outMin, float outMax) {
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  {
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) {
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) {
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) {
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) {
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) {
    // следующие четыре строки можно вычислить заранее для заданной формы
    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) для правильного многоугольника

    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) {
        // Зубы
        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);
      
        // Язык
        // Делаем правую часть языка толще
        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);
          
        // цвет заливки рта
        return vec3(.42, .147, .152); 
    } 
    
    // контуры губ
    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); 
    
    // губы
    if (d < .16) return vec3(.838, .799, 0.76);
   
    // Зрачки
    {
        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);
        }
    }
        
    // Глазные яблоки
    vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
    d = length(eye) - .16;
    if (d < 0.) return vec3(step(.013, -d));
    
    // подглазные линии
    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);
    
    // Нос, бровь, голова, волосы
    {
        // Нос  
        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);

        // Бровь
        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(
            // Голова
            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);
    
        // Волосы
        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);
}

Дополнительное задание: нарисуйте другого персонажа «Рика и Морти» или вашего любимого мультфильма.

Дополнительное задание: используйте raymarching с трёхмерными SDF для отрисовки 3D-версии Рика. Напишите, если сделаете это, мне бы хотелось посмотреть.

Анимация

Закончив с рисованием, мы можем использовать различные техники анимации, чтобы добавить движение. Во-первых:

1. Зацикленные значения

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

Видео в h.264

vec2 rotateAt(vec2 p, float angle, vec2 origin) {
    float s = sin(angle), c = cos(angle);
    return (p-origin)*mat2( c, -s, s, c ) + origin;
}

float map(float value, float inMin, float inMax, float outMin, float outMax) {
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  {
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) {
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) {
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) {
    // из https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) {
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) {
    // следующие четыре строки можно рассчитать заранее для нужной фигуры
    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) для правильного многоугольника

    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) { 
    
    // НОВОЕ: поворот всего рисунка
    pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
    pixel.y += .1;
    
    // Рот, глаза, нос
    {
        // Рот
        float d = bezier(pixel,  
                     vec2(-.26, -.28), 
                     vec2(-.05,-.42), 
                     vec2(.115, -.25));
        if (d < .11) {
            // Зубы
            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);
          
            // Язык
            vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
            float tongue_thickness = map(tongue.x, -.16, .01, .02, .045);
            d = bezier(tongue,  
                vec2(-.16, -.35), 
                vec2(.001,-.33), 
                vec2(.01, -.5)) - tongue_thickness;
            if (d < 0.0) 
                return vec3(0.816, 0.302, 0.275)*step(d, -0.01);
              
            // цвет заливки рта
            return vec3(.42, .147, .152); 
        } 
        
        // контуры губ
        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); 
        
        // губы
        if (d < .16) return vec3(.838, .799, 0.76);
       
        // Зрачки
        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);
        }
            
        // Глазные яблоки
        vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
        d = length(eye) - .16;
        if (d < 0.) return vec3(step(.013, -d));
        
        // подглазные линии
        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);
    
        // Нос
        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);
    }

    // Бровь
    float d = bezier(pixel,  
            vec2(-.34, .38), 
            // НОВОЕ: анимируем опускание и поднятие средней части
            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(
            // Голова
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // Ухо
            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);
    
        // Волосы
        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.);
}

Дополнительное задание: анимируйте голову Рика так, как будто он идёт влево и вправо. Отзеркаливайте направление лица, когда он движется вправо (это проще, чем кажется!).

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

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

Видео в h.264

vec2 rotateAt(vec2 p, float angle, vec2 origin) {
    float s = sin(angle), c = cos(angle);
    return (p-origin)*mat2( c, -s, s, c ) + origin;
}
float map(float value, float inMin, float inMax, float outMin, float outMax) {
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  {
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) {
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) {
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) {
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) {
    // из https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) {
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) {
    // следующие четыре строки можно рассчитать заранее для нужной фигуры
    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) для правильного многоугольника

    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) {     
    {
        // поворачиваем весь рисунок
        pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
        pixel.y += .1;
    }

    // раз в 2 секунды моргаем на .09 секунды
    if (mod(time, 2.) < .09) { // закрытые глаза
        float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0));
        if (d < .008) return vec3(0);      
    } 
    else // открытые глаза
    {
        // Зрачки
        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);
        }
            
        // Глазные яблоки
        vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
        d = length(eye) - .16;
        if (d < 0.) return vec3(step(.013, -d));
        
        // подглазные линии
        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);
    }
  
    // Остальная часть лица
    {
        // Рот
        float d = bezier(pixel,  
                     vec2(-.26, -.28), 
                     vec2(-.05,-.42), 
                     vec2(.115, -.25));
        if (d < .11) {
            // Зубы
            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);
          
            // Язык
            // `map()` используется для изменения толщины языка
            // вдоль оси x
            vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
            float tongue_thickness = map(tongue.x, -.16, .01, .02, .045);
            d = bezier(tongue,  
                vec2(-.16, -.35), 
                vec2(.001,-.33), 
                vec2(.01, -.5)) - tongue_thickness;
            if (d < 0.0) 
                return vec3(0.816, 0.302, 0.275)*step(d, -0.01);
              
            // цвет заливки рта
            return vec3(.42, .147, .152); 
        } 
        
        // контуры губ
        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); 
        
        // губы
        if (d < .16) return vec3(.838, .799, 0.76);
       
        
    
        // Нос  
        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);
    

    // Бровь
    d = bezier(pixel,  
            vec2(-.34, .38), 
            // НОВОЕ: анимируем опускание поднятие средней части
            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(
            // Голова
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // Ухо
            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);
    
        // Волосы
        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);
}

Дополнительное задание: используйте эту технику для анимирования рта Рика так, как будто он говорит

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

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

Видео в h.264

vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fold
    float s = sin(angle), c = cos(angle);
    return (p-origin)*mat2( c, -s, s, c ) + origin;
}
float map(float value, float inMin, float inMax, float outMin, float outMax) {...}
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) { 
    {...}
    
    // Моргание глаз
    if (mod(time, 2.) < .09) {...} 
    else { 
        // случайным образом перемещаем зрачки
        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);
        
        {...}
    }
  
    // Остальная часть лица
    {...}
    
    return vec3(1);
}

Дополнительное задание: сделайте движение зрачков более реалистичным, чтобы они не перемещались рывками.

Бонус: искажение времени

Последней анимационной техникой будет time domain warping, позволяющая изгибать волосы при наклоне головы. Это похоже на domain warping, только вместо смещения пространства мы смещаем время. По сути, мы откладываем время тем больше, чем ближе к кончику волос находится пиксель. Так как эта задержка не постоянна на протяжении длины волос, они изгибаются, а не поворачиваются жёстко.

Видео в h.264

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) { 
    {...}
    
    // Волосы     
    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);
}

Дополнительное задание: примените этот трюк к другим частям лица Рика, чтобы оно стало более «резиновым» и расслабленным.

Завершаем

После добавления эффекта портала можно считать нашу анимацию завершённой. Этот эффект был создан пользователем ShaderToy valena, а более коротким его сделал FabriceNeyret2.

Видео в h.264

Дополнительное задание: при написании этого кода я отдавал приоритет его читаемости, а не производительности. Попробуйте ускорить его работу.

Приложение 1: создаём видео

Закончив с анимацией, вы, вероятно, захотите превратить её в видео. Для этого можно использовать скрипт с glslviewer и ffmpeg. Ниже показан процесс для macOS, в Windows и Linux вам придётся самим подобрать эквиваленты для этих платформ.

  1. Устанавливаем зависимости.

brew install glslviewer ffmpeg # brew есть только в macos
  1. Пишем свой файл shader.frag

  2. Вставляем это в файл bash и запускаем его для экспорта видео

#!/bin/bash

set -e
set -o pipefail

if [ -z "$1" ]; then
  echo "Usage: $0 <shader_file>"
  exit 1
fi

ORIGINAL_DIR=$(pwd)

TMP_DIR=$(mktemp -d)
if [ ! -d "$TMP_DIR" ]; then
  echo "Failed to create temporary directory."
  exit 1
fi
cd "$TMP_DIR"

glslViewer "$ORIGINAL_DIR/$1" -w 1920 -h 1080 --headless -e sequence,0,7,60 -e q
ffmpeg -framerate 60 -y -i %05d.png -c:v libx264 -pix_fmt yuv420p animation.mp4
mv animation.mp4 "$ORIGINAL_DIR/"

cd "$ORIGINAL_DIR"
rm -rf "$TMP_DIR"

А если вам нужно будет интерактивно изменять код, то сделайте следующее:

glslViewer shader.frag -w 575 -h 324 --noncurses -x 0 -y 0

Приложение 2: суперсэмплирование

Возможно, вы заметили, что края фигур в примерах гладкие. Чтобы добиться этого, я проделал кое-какую работу. Я воспользовался техникой суперсэмплирования, при которой для девяти точек внутри каждого экранного пикселя выполняется color_for_pixel(), а на экран выводится усреднённое значение. В левой части приведённого ниже примера видно, как выглядит картинка с отключенным суперсэмплированием.

Видео в h.264

#version 300 es 
// Эта строка переключает редактор в режим "pro" 
// и отключает автоматическое суперсэмплирование

precision highp float;
uniform float time;
uniform vec2 resolution;
out vec4 outColor;

vec3 color_for_pixel(vec2 p, float time) {   
    return vec3(length(mod(p+time*.05, .5) - .25) > 0.2);
}

void main() {
    float zone = gl_FragCoord.x - resolution.x*.5;
    if (abs(zone) < 1.5) {
      // вертикальная линия
      outColor = vec4(1, 0, 0, 1);
    } else if (zone < 0.) {
      // левая сторона: суперсэмплирования нет
      vec2 st = (2.0*(gl_FragCoord.xy)-resolution)/resolution.y;
      outColor = vec4(color_for_pixel(st, time), 1);
    } else {
      // правая сторона: суперсэмплирование
      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); 
    }
}

Приложение 3: как появилась эта статья

Восемь месяцев назад я опубликовал видео «Я создал за неделю 3D-редактор на C». В видео было множество анимаций, например, вот эта, демонстрирующая алгоритм marching cubes:

Видео в h.264

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

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 19: ↑18 and ↓1+24
Comments4

Articles