Хабрахабр

[Перевод] 3D-фотографии Facebook изнутри: шейдеры параллакса

image

В последние несколько месяцев Facebook заполонили 3D-фотографии. Если вам не довелось их увидеть, то объясню: 3D-фотографии — это изображения внутри поста, которые плавно меняют перспективу при скроллинге страницы или когда перемещаешь по ним мышь.

Хотя можно легко понять, как Facebook может рендерить 3D-модели и поворачивать их в соответствии с позицией мыши, с 3D-фотографиями ситуация может быть не столь интуитивно понятной. За несколько месяцев до появления этой функции Facebook тестировал похожую функцию с 3D-моделями.

В нём применяется оптическое явление под названием «параллакс».
Техника, которую использует Facebook для создания трёхмерности двухмерных изображений, иногда называется смещение карты высот.

Пример 3D-фотографии Facebook (GIF)

Что такое параллакс

Если вы играли в Super Mario, то точно знаете, что такое параллакс. Хотя Марио бежит с одной скоростью, кажется, что далёкие объекты на фоне движутся медленнее (см. ниже).

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

Как мозг оценивает расстояние?

Предполагается, что мозг человека для оценки расстояния использует несколько механизмов. На короткой и средней дальности расстояния вычисляются сравнением различия позиции объекта, видимого правым и левым глазом. Это называется стереоскопическим видением и широко распространено в природе.

Горы, облака и звёзды слишком мало отличаются для разных глаз, чтобы заметить значимую разницу. Однако для достаточно далёких объектов одного стереоскопического зрения недостаточно. Объекты на заднем плане движутся меньше по сравнению с объектами на переднем плане. Поэтому в дело вступает относительный параллакс. Именно их относительное движение позволяет установить относительное расстояние.

Самым известным из них является атмосферная дымка, придающая далёким объектам голубой оттенок. В восприятии расстояния используется и много других механизмов. Пользователь YouTube Алекс Макколган объясняет это на своём канале Astrum, показывая, как трудно определить размер лунных объектов, показанных на видео. В других мирах большинства этих атмосферных подсказок нет, поэтому так сложно оценить масштаб объектов на других планетах и Луне.

Параллакс как сдвиг

Если вам знакома линейная алгебра, то вы вероятно знаете, насколько сложной и нетривиальной может быть математика 3D-поворотов. Поэтому есть гораздо более простой способ понимания параллакса, для которого не требуется ничего, кроме сдвигов.

ниже). Давайте представим, что мы смотрим на куб (см. Это и есть перспектива. Если мы точно выровнены относительно его центра, то передняя и задняя грани будут выглядеть для наших глаз как два квадрата разного размера.

Однако что произойдёт, если мы сдвинем камеру вниз, или поднимем куб вверх? Применив те же принципы, мы сможем увидеть, что передняя и задняя грани сместились относительно их предыдущей позиции. Ещё интереснее то, что они сдвинулись относительно друг друга. Задняя грань, находящаяся дальше от нас, как будто сдвинулась меньше.

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

Если мы сдвинемся на X метров вправо, то должно казаться, что объект в Y метрах от нас сдвинулся на Z метров. Единственное, что нам нужно определить — это масштаб. По сути, это означает, что мы можем симулировать небольшие 3D-повороты сдвигом пикселей в зависимости от их расстояния до камеры. Если X остаётся небольшим, параллакс становится задачей линейной интерполяции, а не тригонометрии.

Генерируем карты глубин

По своему принципу то, что делает Facebook, не слишком отличается от происходящего в Super Mario. Для заданной картинки определённые пиксели смещаются в направлении движения на основании расстояния до камеры. Для создания 3D-фотографии Facebook требуется только само фото и карта, сообщающая, как далеко находится каждый пиксель от камеры. Такая карта имеет вполне ожидаемое название — «карта глубин». В зависимости от контекста её также называют картой высот.

В современных устройствах используются различные техники. Сделать фотографию довольно просто, но генерация правильной карты глубин — гораздо более сложная задача. Тот же принцип используется в стереоскопическом зрении, которое люди применяют для оценки глубины на коротких и средних дистанциях. Чаще всего используют две камеры; каждая делает снимок одного и того же объекта, но с немного отличающейся перспективой. На изображении ниже показано, как iPhone 7 может создавать карты глубин из двух очень близких картинок.

Подробности выполнения такой реконструкции описаны в статье Instant 3D Photography, представленной Питером Хедманом и Йоханнесом Копфом на SIGGRAPH2018.

Реальное ограничение этой техники заключается в том, что даже если можно воссоздать грубую 3D-модель, в ней отсутствует информация о том, как рендерить части, невидимые на исходном фото. После создания качественной карты глубин симуляция трёхмерности становится почти тривиальной задачей. На данный момент эту проблему невозможно решить, и поэтому все видимые на 3D-фотографиях перемещения довольно незначительны.

Во второй части мы узнаем, как те же самые техники можно использовать для реализации 3D-фотографий в Unity при помощи шейдеров. Мы познакомились с концепцией 3D-фотографий и вкратце рассказали о том, как их могут создавать современные смартфоны.

Часть 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
_Scale ("Scale", Vector) = (0,0,0,0)
}

Эти два новых свойства также должны соответствовать двум переменным с тем же названием, которые нужно добавить в раздел CGPROGRAM/ENDCG:

sampler2D _HeightTex;
fixed2 _Scale;

Теперь всё готово, и мы можем приступать к написанию кода, который будет выполнять смещение.

Так как _HeightTex — это чёрно-белая текстура, мы можем просто взять её канал красного и отбросить остальные. Первый шаг — это сэмплирование значения из карты глубин, которое можно выполнить с помощью функции tex2D. Полученное значение измеряет расстояние в неких произвольных единицах от текущего пикселя до камеры.

Это позволяет обеспечить и положительный (белый цвет), и отрицательный (чёрный цвет) параллакс. Значение глубины находится в интервале от $0$ до $1$, но мы растянем его до интервала от $-1$ до $+1$.

Теория

Для симуляции эффекта параллакса на этом этапе нам нужно использовать информацию о глубинах для сдвига пикселей изображения. Чем ближе пиксель, тем сильнее его нужно сдвигать. Этот процесс объяснён на показанной ниже схеме. Красный пиксель из исходного изображения в соответствии с информацией из карты глубин должен сместиться на два пикселя влево. Аналогично, синий пиксель должен сместиться на два пикселя вправо.

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

Если в одном и том же месте должны оказаться несколько пикселей, то мы можем усреднить их влияние. Если мы хотим быть точными, то нам нужно сэмплировать карту глубин всех соседних пикселей, чтобы выяснить, какой из них должен (если должен) сдвинуться в текущую позицию. Хотя такая система работает и обеспечивает наилучший возможный результат, она является чрезвычайно неэффективной и потенциально в сотни раз более медленной, чем исходный 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 изображение и его карта глубин.

Готовые результаты показаны здесь:

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть