В последние несколько месяцев Facebook заполонили 3D-фотографии. Если вам не довелось их увидеть, то объясню: 3D-фотографии — это изображения внутри поста, которые плавно меняют перспективу при скроллинге страницы или когда перемещаешь по ним мышь.
За несколько месяцев до появления этой функции Facebook тестировал похожую функцию с 3D-моделями. Хотя можно легко понять, как Facebook может рендерить 3D-модели и поворачивать их в соответствии с позицией мыши, с 3D-фотографиями ситуация может быть не столь интуитивно понятной.
Техника, которую использует Facebook для создания трёхмерности двухмерных изображений, иногда называется смещение карты высот. В нём применяется оптическое явление под названием «параллакс».
Что такое параллакс
Если вы играли в Super Mario, то точно знаете, что такое параллакс. Хотя Марио бежит с одной скоростью, кажется, что далёкие объекты на фоне движутся медленнее (см. ниже).
Этот эффект создаёт иллюзию того, что некоторые элементы, например горы и облака, расположены дальше. Он эффективен потому, что наш
Однако для достаточно далёких объектов одного стереоскопического зрения недостаточно. Горы, облака и звёзды слишком мало отличаются для разных глаз, чтобы заметить значимую разницу. Поэтому в дело вступает относительный параллакс. Объекты на заднем плане движутся меньше по сравнению с объектами на переднем плане. Именно их относительное движение позволяет установить относительное расстояние.
В восприятии расстояния используется и много других механизмов. Самым известным из них является атмосферная дымка, придающая далёким объектам голубой оттенок. В других мирах большинства этих атмосферных подсказок нет, поэтому так сложно оценить масштаб объектов на других планетах и Луне. Пользователь YouTube Алекс Макколган объясняет это на своём канале Astrum, показывая, как трудно определить размер лунных объектов, показанных на видео.
Параллакс как сдвиг
Если вам знакома линейная алгебра, то вы вероятно знаете, насколько сложной и нетривиальной может быть математика 3D-поворотов. Поэтому есть гораздо более простой способ понимания параллакса, для которого не требуется ничего, кроме сдвигов.
Давайте представим, что мы смотрим на куб (см. ниже). Если мы точно выровнены относительно его центра, то передняя и задняя грани будут выглядеть для наших глаз как два квадрата разного размера. Это и есть перспектива.
Однако что произойдёт, если мы сдвинем камеру вниз, или поднимем куб вверх? Применив те же принципы, мы сможем увидеть, что передняя и задняя грани сместились относительно их предыдущей позиции. Ещё интереснее то, что они сдвинулись относительно друг друга. Задняя грань, находящаяся дальше от нас, как будто сдвинулась меньше.
Если мы хотим вычислить истинные позиции этих вершин куба в нашей спроецированной области видимости, то нам придётся серьёзно взяться за тригонометрию. Однако на самом деле это необязательно. Если перемещение камеры достаточно мало, то мы можем аппроксимировать смещение вершин, сдвинув их пропорционально их расстоянию.
Единственное, что нам нужно определить — это масштаб. Если мы сдвинемся на X метров вправо, то должно казаться, что объект в Y метрах от нас сдвинулся на Z метров. Если X остаётся небольшим, параллакс становится задачей линейной интерполяции, а не тригонометрии. По сути, это означает, что мы можем симулировать небольшие 3D-повороты сдвигом пикселей в зависимости от их расстояния до камеры.
Генерируем карты глубин
По своему принципу то, что делает Facebook, не слишком отличается от происходящего в Super Mario. Для заданной картинки определённые пиксели смещаются в направлении движения на основании расстояния до камеры. Для создания 3D-фотографии Facebook требуется только само фото и карта, сообщающая, как далеко находится каждый пиксель от камеры. Такая карта имеет вполне ожидаемое название — «карта глубин». В зависимости от контекста её также называют картой высот.
Сделать фотографию довольно просто, но генерация правильной карты глубин — гораздо более сложная задача. В современных устройствах используются различные техники. Чаще всего используют две камеры; каждая делает снимок одного и того же объекта, но с немного отличающейся перспективой. Тот же принцип используется в стереоскопическом зрении, которое люди применяют для оценки глубины на коротких и средних дистанциях. На изображении ниже показано, как iPhone 7 может создавать карты глубин из двух очень близких картинок.
Подробности выполнения такой реконструкции описаны в статье Instant 3D Photography, представленной Питером Хедманом и Йоханнесом Копфом на SIGGRAPH2018.
После создания качественной карты глубин симуляция трёхмерности становится почти тривиальной задачей. Реальное ограничение этой техники заключается в том, что даже если можно воссоздать грубую 3D-модель, в ней отсутствует информация о том, как рендерить части, невидимые на исходном фото. На данный момент эту проблему невозможно решить, и поэтому все видимые на 3D-фотографиях перемещения довольно незначительны.
Мы познакомились с концепцией 3D-фотографий и вкратце рассказали о том, как их могут создавать современные смартфоны. Во второй части мы узнаем, как те же самые техники можно использовать для реализации 3D-фотографий в Unity при помощи шейдеров.
Часть 2. Шейдеры параллакса и карты глубин
Шаблон шейдера
Если мы хотим воссоздать 3D-фотографии Facebook с помощью шейдера, то сначала должны определиться с тем, что конкретно будем делать. Так как этот эффект лучше всего работает с 2D-изображениями, то логично будет реализовать решение, совместимое со спрайтами (Sprite) Unity. Мы создадим шейдер, который можно использовать со Sprite Renderer.
Хотя такой шейдер можно создать с нуля, часто предпочтительнее начинать с готового шаблона. Лучше всего начать двигаться вперёд, скопировав уже имеющийся diffuse shader спрайтов, который Unity по умолчанию использует для всех спрайтов. К сожалению, движок не поставляйтся с файлом shader, который можно редактировать самому.
Чтобы получить его, нужно перейти в Unity download archive и скачать пакет Built in shaders (см. ниже) для используемой вами версии движка.
После извлечения пакета можно просмотреть исходный код всех шейдеров, поставляемых с Unity. Интересующий нас находится в файле Sprites-Diffuse.shader, который по умолчанию используется для всех создаваемых спрайтов.
Изображения
Второй аспект, который нужно формализовать — это имеющиеся у нас данные. Представим, что у нас есть и изображение, которое мы хотим анимировать, и его карта глубин. Последняя будет чёрно-белым изображением, в которой чёрные и белые пиксели обозначают, насколько далеко или близко они находятся от камеры.
Использованные в этом туториале изображения взяты из проекта Pickle cat Денниса Хотсона, и это, без сомнения, лучшее, что вы сегодня увидите.
Связанная с этим изображением карта высот отражает расстояние кошачьей морды от камеры.
Легко заметить, насколько хороших результатов можно добиться с такой простой картой глубин. Это значит, что несложно создавать собственные карты глубин для уже существующих изображений.
Свойства
Теперь, когда у нас есть все ресурсы, можно приступать к написанию кода шейдера параллакса. Если мы импортируем основное изображение как спрайт, то Unity автоматически передаст его шейдеру через свойство _MainTex
. Однако нам нужно сделать так, чтобы шейдеру была доступна карта глубин. Это можно реализовать с помощью нового свойства шейдера под названием _HeightTex
. Я намеренно решил не называть его _DepthTex
, чтобы не перепутать с текстурой глубин (это похожая концепция Unity, используемая для рендеринга карты глубин сцены).
Для изменения силы эффекта мы также добавим свойство _Scale
.
Properties
{
...
_HeightTex ("Heightmap (R)", 2D) = "gray" {}
_Scale ("Scale", Vector) = (0,0,0,0)
}
Эти два новых свойства также должны соответствовать двум переменным с тем же названием, которые нужно добавить в раздел CGPROGRAM
/ENDCG
:
sampler2D _HeightTex;
fixed2 _Scale;
Теперь всё готово, и мы можем приступать к написанию кода, который будет выполнять смещение.
Первый шаг — это сэмплирование значения из карты глубин, которое можно выполнить с помощью функции tex2D
. Так как _HeightTex
— это чёрно-белая текстура, мы можем просто взять её канал красного и отбросить остальные. Полученное значение измеряет расстояние в неких произвольных единицах от текущего пикселя до камеры.
Значение глубины находится в интервале от до , но мы растянем его до интервала от до . Это позволяет обеспечить и положительный (белый цвет), и отрицательный (чёрный цвет) параллакс.
Теория
Для симуляции эффекта параллакса на этом этапе нам нужно использовать информацию о глубинах для сдвига пикселей изображения. Чем ближе пиксель, тем сильнее его нужно сдвигать. Этот процесс объяснён на показанной ниже схеме. Красный пиксель из исходного изображения в соответствии с информацией из карты глубин должен сместиться на два пикселя влево. Аналогично, синий пиксель должен сместиться на два пикселя вправо.
Хоть теоретически это должно сработать, нет простых способов для реализации этого в шейдере. Всё дело в том, что шейдер по своему принципу может менять только цвет текущего пикселя. При выполнении кода шейдера он должен отрисовывать на экране определённый пиксель; мы не можем просто сдвинуть этот пиксель в другое место или изменить цвет соседнего. Это ограничение локальности обеспечивает очень эффективную параллельную работу шейдеров, но не позволяет нам реализовывать всевозможные эффекты, которые были бы тривиальными при условии наличия произвольного доступа для записи к каждому пикселю в изображении.
Если мы хотим быть точными, то нам нужно сэмплировать карту глубин всех соседних пикселей, чтобы выяснить, какой из них должен (если должен) сдвинуться в текущую позицию. Если в одном и том же месте должны оказаться несколько пикселей, то мы можем усреднить их влияние. Хотя такая система работает и обеспечивает наилучший возможный результат, она является чрезвычайно неэффективной и потенциально в сотни раз более медленной, чем исходный diffuse shader, с которого мы начинали.
Наилучшей альтернативой будет следующее решение: мы получаем глубину текущего пикселя из карты глубин; затем, если мы должны сдвинуть его вправо, то заменяем текущий цвет на пиксель слева (см. изображение ниже). Здесь мы допускаем, что если нужно двигать пиксель вправо, то соседние пиксели слева тоже предположительно должны сдвинуться аналогично.
Легко увидеть, что это всего лишь малозатратная аппроксимация того, чего мы хотели достичь на самом деле. Тем не менее, она оказывается очень эффективной, потому что карты глубин обычно оказываются плавными.
Код
Следуя алгоритму, описанному в предыдущем разделе, мы можем реализовать шейдер параллакса с помощью простого смещения UV-координат.
Это приводит к следующему коду:
void surf (Input IN, inout SurfaceOutput o)
{
// Displacement
fixed height = tex2D(_HeightTex, IN.uv_MainTex).r;
fixed2 displacement = _Scale * ((height - 0.5) * 2);
fixed4 c = SampleSpriteTexture (IN.uv_MainTex - displacement) * IN.color;
...
}
Такая техника хорошо работает с почти плоскими объектами, как это видно на показанной ниже анимации.
Но по-настоящему отлично она проявляет себя с 3D-моделями, потому что для 3D-сцены очень легко отрендерить текстуру глубин. Ниже показано отрендеренное в 3D изображение и его карта глубин.
Готовые результаты показаны здесь:
Автор: PatientZero