В этом посте показано, как написать шейдер для имитации заставки macOS Monterey.

Все коды этого туториала загружены в shapertoy. Вы можете попробовать шейдер онлайн: https://www.shadertoy.com/view/7tGfWm. Прежде чем мы начнем, я хочу, чтобы вы имели некоторые базовые понятия о GLSL. Если вы это сделаете, может быть легче понять код. И вы можете смело пропустить код. Объясню принцип.

Рисуем волну

Изображение состоит из нескольких волн. Нарисуем волну в шейдере. Давайте создадим новый проект в Shadertoy.

Шейдер, который мы собираемся написать, называется «фрагментный шейдер». Это программа для оценки цвета каждого пикселя изображения. Первый параметр fragCoord указывает координаты пикселя для оценки. Поскольку координаты пикселя зависят от устройства, нам нужно сначала нормализовать координату от 0 до 1.

Эта процедура выполняется одной строкой кода:

// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;

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

vec3 sin_shape(in vec2 uv, in float offset_y) {
  float y = sin((uv.x * 3.14 * 2.0));

  float y0 = step(0.0, y - uv.y * 2.0 + offset_y);
  return vec3(y0, y0, y0);
}


void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;

    vec3 col = sin_shape(uv, 1.0);

    // Output to screen
    fragColor = vec4(col,1.0);
}

Добавьте немного шума

Волны, которые мы нарисовали, тусклые. Нам нужно сделать их веселее. Поэтому я решил добавить немного «шума».

vec3 noised_sin_shape(in vec2 uv, in float offset_y) {
  // Time varying pixel color
  float y = sin(uv.x * 3.14 * 4.0 + iTime * -0.6);
  
  float x = uv.x * 8.;
  float a=1.;
 for (int i=0; i<5; i++) {
  x*=0.53562;
  x+=6.56248;
  y+=sin(x)*a;  
  a*=.5;
    }

  float y0 = step(0.0, y - uv.y * 4.0 + offset_y);
  return vec3(y0, y0, y0);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;

    vec3 col = noised_sin_shape(uv, 1.0);

    // Output to screen
    fragColor = vec4(col,1.0);
}

Состав

На финальном изображении у нас получилось три волны, окрашенные в разные цвета. Мы должны найти способ составить три волны (даже больше). Мы можем узнать, находится ли пиксель в области волны по результату функции синуса. Все усложнилось, когда мы получили три волны. Нам нужно написать много операторов if/else, чтобы определить цвета. И этот метод не масштабируется. Нам нужен математический способ решения этой задачи.

Математика — это абстрактный инструмент реальности. Представьте, если вы хотите нарисовать эту картину кистями и красками. Как ты будешь рисовать?

Когда мы рисуем изображение кистями и красками, мы рисуем слои по отдельности. Как насчет использования толщины красок для определения волны?

Значение оттенков серого похоже на представление «толщины красок». И мы можем использовать оттенки серого, чтобы определить, какой цвет показывать.

vec3 sin_shape(in vec2 uv, in float offset_y) {
  // Time varying pixel color
  float y = sin((uv.x + iTime * -0.06 + offset_y) * 5.5);

  float x = uv.x * 8.;
  float a=1.;
 for (int i=0; i<5; i++) {
  x*=0.53562;
  x+=6.56248;
  y+=sin(x)*a;  
  a*=.5;
 }

  float y0 = step(0.0, y * 0.08 - uv.y + offset_y);
  return vec3(y0, y0, y0);
}

vec2 rotate(vec2 coord, float alpha) {
  float cosA = cos(alpha);
  float sinA = sin(alpha);
  return vec2(coord.x * cosA - coord.y * sinA, coord.x * sinA + coord.y * cosA);
}

vec3 scene(in vec2 uv) {
    vec3 col = vec3(0.0, 0.0, 0.0);
    col += sin_shape(uv, 0.3) * 0.2;
    col += sin_shape(uv, 0.7) * 0.2;
    col += sin_shape(uv, 1.1) * 0.2;

    vec3 fragColor;

    if (col.x >= 0.6 ) {
      fragColor = vec3(0.27, 0.11, 0.64);
    } else if (col.x >= 0.4) {
      fragColor = vec3(0.55, 0.19, 0.69);
    } else if (col.x >= 0.2) {
      fragColor = vec3(0.68, 0.23, 0.65);
    } else {
      fragColor = vec3(0.86, 0.57, 0.68);
    }
    return fragColor;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    fragCoord = rotate(fragCoord + vec2(0.0, -300.0), 0.5);
    // Normalized pixel coordinates (from 0 to 1)
    vec3 col0 = scene((fragCoord * 2.0)/iResolution.xy);
    vec3 col1 = scene(((fragCoord * 2.0) + vec2(1.0, 0.0))/iResolution.xy);
    vec3 col2 = scene(((fragCoord * 2.0) + vec2(1.0, 1.0))/iResolution.xy);
    vec3 col3 = scene(((fragCoord * 2.0) + vec2(0.0, 1.0))/iResolution.xy);

    // Output to screen
    fragColor = vec4((col0 + col1 + col2 + col2) / 4.0,1.0);
}

Настройте вид

Теперь мы очень близки к финальному изображению. Нам нужно настроить вид и параметры синусоидальной функции, чтобы сделать ее ближе.

vec2 rotate(vec2 coord, float alpha) {
  float cosA = cos(alpha);
  float sinA = sin(alpha);
  return vec2(coord.x * cosA - coord.y * sinA, coord.x * sinA + coord.y * cosA);
}

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

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

Сглаживание

Существует множество способов реализации сглаживания. Здесь мы используем простой метод: суперсэмплинг. Существует еще множество паттернов суперсэмплинга. Используем самый простой: смешиваем четыре пикселя в один.

Представьте, что мы рисуем изображение экрана в 4 раза большего размера. И смешиваем 4 пикселя в один. Тогда мы получили плавную кривую.

Анимация

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

Заключение

Наконец, для реализации этого мы используем менее 100 строк кода. Есть некоторые математические понятия, но это довольно просто. Но смешно и красиво. Между этой игрушкой и официальной заставкой Apple все еще есть разрыв, потому что у нас нет 3D-модели. В качестве альтернативы мы используем 2D-формы для реализации. Это просто и весело. В этом-то и дело.