3 года назад художник спросил меня:
— Слушай, а можно в нашу мобильную игру добавить красивые облачка?
— Нет, это абсолютно невозможно, у нас постоянно вращается камера, так что билборды будут смотреться очень фальшиво даже если на них добавить карты нормалей, а другие способы…
*художник погружается в летаргический сон*

Для меня нет большего удовольствия, чем выяснять, что я был неправ.



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

Собираем данные


Нам понадобятся:

  1. Глубина мира:
  2. Глубина облаков:
  3. Нормали облаков:

Немного о формате
Левая половина изображения — Aльфа канал. Чем темнее — тем прозрачнее.
Правая половина изображения — RGB каналы.

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

Глубина зашифровывается в RG каналы текстуры, при этом $z = r + g / 255$.
Нормаль представляется как 3d вектор в пространстве камеры, причем $rgb = (normal + 1) / 2$, т.к. RGB не поддерживает отрицательные значения.

Размываем


Размываем нормали в 2 прохода:

  1. По горизонтали:
  2. По вертикали:

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


Аналогично размываем карту глубины облаков:



Проецируем шум


Неплохо было бы добавить шума в наши данные.
Есть 3 варианта:

  1. 3D текстура — требует много памяти, медленно работает на мобильных.
  2. Генератор шума в шейдере — для шума перлина нужно много раз вызывать ГСЧ => медленно работает; Нет художественного контроля: нельзя включить другой тип шума без переписывания кода.
  3. Трипланарная проекция 2d текстуры — генерируем текстуру с шумом, проецируем её по осям X, Y и Z. Эффективно; можно подставить любую текстуру; занимает мало памяти.

Временно отключим размытие чтобы лучше понять как работает проекция шума.
Трипланарная проекция
Если $p$ — координаты точки в 3d пространстве, а $n$ — нормаль к поверхности, то проекция рассчитывается так:
$color = noise(p.yz) * n.x^2 +\\ \qquad \quad\, noise(p.zx) * n.y^2 +\\ \qquad \quad\, noise(p.xy) * n.z^2$

Так как длина вектора $n$ равна 1, сумма квадратов его координат дадут 1, сохранив яркость шума.

Проецируем шум по оси X:


Проецируем шум по осям X и Y:



Проецируем шум по осям X,Y,Z:



Теперь используем этот шум, чтобы изменить карту глубины облака по формуле:
$depth \mathrel{+}= noise.r*sin(t * \pi \qquad \;\;\,) + \\ \qquad \qquad \; noise.g*sin(t * \pi + \dfrac{\pi}3\ ) + \\ \qquad \qquad \; noise.b*sin(t * \pi+\dfrac{2\pi}3)$


И спроецируем шум заново:


Вернем размытие:



Освещение


Освещение складывается из 2 составляющих:

  1. Псевдо диффузное освещение
    $t = saturate((cos(a) + lightEnd) / (1 + lightEnd))$
    $color = lerp(lightColor, shadowColor, t) = lightColor * t + shadowColor * (1 - t)$
    Иллюстрации

    Зависимость освещения от угла $a$ при различных значениях $lightEnd$:

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

    Результат:


  2. Просвечивание
    Если в этом пикселе нет ни одного объекта из твердого мира, добавляем просвечивание:
    $t = saturate(radius - distance) ^ 2 * (1 - color.a) ^ 2 $
    $color = lerp(color, lightColor, t)$
    Где
    $radius$ — радиус просвечивания, а
    $distance$ — расстояние от солнца до текущего пикселя




Применяем шум


Пока мы применили шум только к карте глубины.
Давайте в процессе применения освещения тоже используем шум.
Прибавим вектор шума к нормалям:



Сдвинем позицию из которой мы читаем на $noise.xy$:



Наложение облаков на остальной мир


Накладываем на остальной мир с помощью альфа-блендинга, добавляя прозрачность там, где объекты мира близки к поверхности облаков, или и вовсе заслоняют их.
$color.a \mathrel{*}= 1 - saturate((cloudDepth + fallback - worldDepth) / fallback)$
Где $fallback$ — глубина, на котором объект пропадает из видимости внутри облака.



Производительность


Если запустить все эти преобразования в разрешении Full HD, ваш топовый смартфон заплачет, свернется в клубочек и начнет показывать слайдшоу с частотой 15-20 fps: телефоны вытягивают 3d графику только с помощью технологии early-z, которая уменьшает количество операций до выполнения трёх-четырёх простых шейдеров на пиксель. А мы выполняем много вычислений в каждом пикселе.

Что делать? Пора избавляться от операций. Формы у облаков расплывчатые, а движущийся шум прикроет наши грязные делишки: будем резать разрешение!

  1. Карта глубины мира рисуется в полном или половинном разрешении (всё равно у смартфонов очень мелкие пиксели).
  2. Все остальные операции производятся в разрешении $\dfrac{1}{4}$, уменьшая площадь в 16 раз => увеличивая производительность в 16 раз.
  3. Операцию наложения мы производим уже в полноразмерный экранный буфер, при этом используя исходную карту глубины мира, что позволяет показывать четкие контуры объектов, поддерживая иллюзию того, что облака рендерились в полном разрешении.

Итоговая производительность:
7 ms на nexus 5x при средних настройках графики (бюджетный телефон 2015 года).
0.5 ms на ноутбуке с GTX 940M, что означает что такие облака по производительности отлично подойдут для VR, где важна высокая частота кадров.

Конечный результат:



Не знаю, можно ли выкладывать ссылку на Asset Store, но кто ищет — тот всегда найдёт:)

UPD: Добавил раздел про производительность

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


  1. mmortall
    24.01.2018 01:16
    +1

    Очень круто! Респект и уважение.
    А есть ли проблемы с перформансом, особенно когда туман весь экрн занимает.?
    Тригонометрические функции заменяли на ряды для лучшего перформанса?
    И не возникает ли ограничений по операциям в шейдере на мобайле?


    1. marsermd Автор
      24.01.2018 01:30

      Производительность не зависит от покрытия экрана туманом/облаками (Кстати, я этого не упомянул, но все операции до момента наложения облаков на остальной мир происходят в пониженном разрешении).


      Тригонометрических функций на самом деле нет:


      sin(t) нет необходимости вычислять в каждом пикселе, т.к. он одинаковый => вычисляем на CPU.
      cos(a) где a — угол между n и light вычисляется как dot(n, light) если n и light единичной длины.


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


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


  1. AxisPod
    24.01.2018 06:28

    Похоже на дым, а не облака и туман. Уж слишком плотно, темно и динамично.


    1. Flakky
      24.01.2018 09:58

      В таких вещах это уже вопрос настройки. А так важен сам принцип.


      1. marsermd Автор
        24.01.2018 10:22

        Верно, плотность, динамичность и цвета настраиваются.
        Я просто так вижу облака:)


  1. EndUser
    24.01.2018 10:35

    Можно иллюстрацию изнутри облака? С точки зрения, тэкскэть, самолётика.
    Спасибо!


    1. marsermd Автор
      24.01.2018 10:58

      Как корректно рендерить изнутри я пока не придумал:)


  1. DstivjTS
    24.01.2018 10:58

    /offtop/ А как вы проверяете производительность игр/приложений на реальных устройствах? Как снимаете значение fps и т.п. вещей? /offtop/


    1. marsermd Автор
      24.01.2018 11:10

      Как корректно замерять производительность видеовычислений — это вообще ужасная тема, достойная отдельной статьи. Я даже не уверен что готов такую сейчас написать:)


      FPS считается просто: берём время между началом кадров n и n+1. Для удобства можно строить график с указанием медианы и 95 перцентиля за последние k кадров.
      Но есть одно маленькое "но": на Андроиде включен оркестратор, который, по сути, навязывает частоту обновления не более 60 кадров в секунду.
      Так что если хочется посчитать производительность конкретного эффекта — у вас большие проблемы.


      Я решаю эту проблему так: даю нагрузку в размере 16 МС(1 кадр при частоте 60 ФПС) и замеряют разницу между частотой кадров со включенным плагином и с выключенным.


      Но на IOS такое не прокатывает — у них насильно включена вертикальная синхронизация, так что если ты не уложился в 16.6 мм, fps сразу падает до 30:)


  1. Torvald3d
    24.01.2018 14:59

    Преимущество этих облаков в том, что они одинаково хорошо выглядят с любого ракурса, а вы в демо показываете только один ракурс. С одним ракурсом и билборды справятся — вы же сами об этом писали. Можно сделать облако более размытым (можно в еще более низком разрешении), более прозрачным (чтобы центр тоже был на ~80% прозрачнее) и показать его с разных ракурсов? Было бы нагляднее и, как по мне, реалистичнее.


    1. marsermd Автор
      24.01.2018 15:27

      Да, показать с разных ракурсов стоит, вы абсолютно правы:)
      Сделать облако более размытым безусловно можно.


      Центр не может быть прозрачным, если это происходит не засчёт fallofff, т.к. наложение двух облаков друг на друга не сделать корректным. Считайте, что тут мы делаем только кучевые облака или стелящийся туман.


      Кучевые облака.jpg


      1. Torvald3d
        25.01.2018 15:31

        Да, действительно, реальные облака вообще не особо пушистые, скорее с подповерхностным рассеиванием. Прям захотелось сделать так же, только не для мобилки, а десктопа. Довольно таки высокополигональное + какой-нибудь SSS или даже SSSSS)

        P.S. обожаю ваши статьи)

        Давно не писал. Спасибо, это мотивирует.


        1. marsermd Автор
          26.01.2018 03:21

          Если захочется сколлаборироваться/побрейнштормить/.., я в вашем распоряжении:)


  1. schernyae
    25.01.2018 17:07

    До чего же круто выглядит.
    Спасибо за подробное описание, надо попробовать самому такую штуку сделать (мне просто не хочется 25$ платить).


    1. marsermd Автор
      25.01.2018 17:28

      Да, для русского пользователя дороговато:)
      Но это дешевле чем большая часть подобных ассетов, а разработка подобной штуки без соответствующего опыта — не меньше 80 часов займет даже зная что надо делать (очень сложно отлаживать; много платформо-зависимых штук).
      Так что купить дешевле. Но если это для саморазвития — флаг вам в руки:)
      P.S. Подпишитесь на меня, скоро расскажу как крутой шум генерировать.