image

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

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

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

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

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


Если вы играли в 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 — это чёрно-белая текстура, мы можем просто взять её канал красного и отбросить остальные. Полученное значение измеряет расстояние в неких произвольных единицах от текущего пикселя до камеры.

Значение глубины находится в интервале от $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 изображение и его карта глубин.


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

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


  1. BkmzSpb
    23.03.2019 13:12

    Судя по последней анимации, карты глубин не всегда достаточно. Я вижу артефакты у задней части черепа, яблока и, что самое важное, бутылки в центре. Создается иллюзия, что за бутылкой есть еще одна, которую мы видим только со сдвигом.
    Я полагаю, что т.к. карта глубин дает вам расстояние только до ближайшей к камере поверхности объекта, то настоящая форма той же бутылки неизвестна — она может быть как циллиндрической (настоящей) формы, так и плоской картонкой или даже протяженным вдоль Z объектом (считая что ось Z направлена из камеры и перпенидкулярна плоскости изображения).

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


    1. phantom-code
      23.03.2019 14:55

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


      1. Nagg
        23.03.2019 16:33

        Вот кстати по коту непонятно — верхних клыков на карте высот нет, однако они корректно прячутся при повороте


        1. DrZlodberg
          23.03.2019 18:11
          +1

          Совсем не корректно. Как и нижние.
          Они деформируются просто так, как будто наклонены сильно назад. Это отлично видно на нижних в крайних положениях


      1. Slavik_Kenny
        23.03.2019 18:15
        +1

        А мне кажется что у кота нет клыков, а просто ряд зубов виден.


  1. pehat
    23.03.2019 17:57
    +1

    >Если вы играли в Super Mario, то точно знаете, что такое параллакс
    >прикладывает gif какого-то ремейка с параллаксными фонами


    1. ferosod
      24.03.2019 08:36

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


  1. sith
    23.03.2019 21:14
    +1

    1. nikolau
      23.03.2019 21:38

      Напомнило картинки в газетах в Гарри Поттере). Там, правда, черно-белые были.


    1. cat_crash
      24.03.2019 03:34

      Хоть незначительные артефакты «псевдо 3Д» видны под пристальным изуением — надо отдать должное что обычный обыватель вряд ли заметит — довольно качественно получается. Могу ошибаться но мне кажется что тут не обощлось без нынче модных нейроночек


  1. tuxi
    23.03.2019 22:40

    Интересная тема. Жаль что нет алгоритма вычисления карты глубины/высот на базе только одной фотографии, но с заранее известным геометрическим предметом на ней. Уже давно пытаюсь решить задачу получения фронтального изображения из фотографии в «пол оборота» (от 30 до 60 градусов)


  1. Dendroid
    24.03.2019 06:23

    Простыми шейдерами так не сделаешь. Как я вижу на FB грузятся three.js фреймворк и текстуры. Т.е. это настоящая 3д-модель поворачивается.


  1. ADenisUA
    24.03.2019 14:39
    +3

    Все возможно. Мы совсе скоро собираемся выпустить приложение которое базируется на шейдерах. Пробовали как FB через three.js + GLB (3D модель) — качество получается хуже и модель больше весит.
    Вот пример того что наша приложуха делает


    https://www.post3d.app/viewer.html?id=2a06e48474dd4611a15a2d18cdf370c9


    если кому интересно — пишите — добавлю в бету


  1. Bookvarenko
    24.03.2019 16:05

    Недавно реализовал такую фишку для движка Instead
    Использовал код из комментариев к статье habr.com/ru/post/220557
    На шейдерах конечно быстрее работает, зато без шейдеров работает везде.


  1. Tutanhomon
    24.03.2019 20:53

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


    1. panteleymonov
      25.03.2019 10:39

      Как говорят, все новое — это хорошо забытое старое.


  1. panteleymonov
    25.03.2019 10:32

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


    1. KpoKec
      25.03.2019 13:21

      А как синхронизировать "реальные ощущения" без воздействия на гипоталамус (мозжечок)?


      1. panteleymonov
        25.03.2019 14:03

        Сейчас основная проблема, это задержки изображения на положение гироскопа при низком FPS, которые могут вызывать некоторые неприятные позывы организма. Это можно компенсировать таким вот «фотофильтром», не заставляя пользователя покупать более мощную видеокарту. И сделать работу в VR более комфортной.


  1. perfect_genius
    26.03.2019 13:43

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