Disclaimer: пост написан на основе изрядно отредактированных логов чата closedcircles.com, отсюда и стиль изложения, и наличие уточняющих вопросов.
Главный термин который надо знать относительно VR — motion-to-photon latency.
Иначе говоря, задержка между поворотом головы и последним фотоном изображения (отрисованного с ракурса нового положения головы) покинувшим экран.
Эмпирически выведено что motion-to-photon latency 20 msec и ниже позволяет достигать presence — т.е. ощущения что двигаешь головой в виртуальном мире.
Важны ли значения меньше 20 ms или нет — непонятно, но в общем цель — достигнуть 20.
GearVR правдами и неправдами достигает, и я расскажу как.
Заранее прошу извинить за свободное использование английских терминов вперемешку с транслитерированными и переведенными.
GearVR это если кто не знает headset в который втыкается телефон. В самом хедсете экрана нет — там есть линзы и IMU.
IMU это стандартное gyroscope + accelerometer. Есть ли в GearVR магнетометр я точно не знаю, вроде нет — но в любом случае на практике магнетометр не очень важен.
Есть стандартные способы получать ориентацию устройства с помощью gyro и accelerometer, я останавливаться на этом не буду.
Важный момент: В GearVR свой IMU, который дает новые данные об ориентации на частоте 1000 Hz с минимальным latency (скорее всего порядка 1 ms).
Это в разы лучше встроенных сенсоров в телефонах (в Samsung Note 5 gyro частоты 200 Hz и latency 10-15 ms).
А как оно питается? От телефона?
Насколько я понимаю, питается от телефона через USB — там собственно почти железа внутри нет, так что наверное расход минимальный.
Значит, базовая расстановка — у нас есть IMU данные, мы их прочитали в начале кадра и надо отрендерить два кадра (стерео картинку). В GearVR нет positional tracking — мы знаем только ориентацию головы.
С помощью ориентации головы получаются приблизительные данные для камер двух глаз — условно, если мы повернули голову направо, то позиция глаз тоже двигается направо т.к. голова вращается вокруг сустава в шее, и известно насколько далеко глаза от этой точки вращения смещены.
Ну и мы знаем примерное (среднее) расстояние между глаз пользователя, поэтому несложной геометрией получаем две камеры, рисуем две картинки.
Как уже сказано, в GearVR есть две линзы — по одной на глаз. Линзы нужны по двум причинам:
Увеличение FOV. Если померять FOV экрана прилепленного к голове то получается недостаточно высокое значение — хочется 90+ градусов на каждый глаз, чтобы экран мог быть достаточно маленьким.
Изменение оптического фокусного расстояния.
Если не было бы линзы, то разные пиксели были бы в разных фокальных точках т.к. экран очень близко и разница в расстояниях разных пикселей достаточно существенная.
Линза "выпрямляет" лучи идущие из глаза, получается ощущение оптического фокуса на бесконечности:
Собственно получается так что линза с такими параметрами имеет достаточно значительный distortion — если просто отрендерить картинку в оба глаза то объекты будут заметно искажаться; чем дальше от центра линзы (глаза), тем больше.
Поэтому мы возьмем картинку полученную стандартным 3D рендером и ее при выводе на экран исказим, чтобы после искажения линзы получилось примерно то же самое.
Для этого обычно рисуется distortion mesh который выглядит примерно так:
Плюс еще если у вас достаточно много лишнего времени на GPU, то можно в этом же проходе попытаться скорректировать chromatic aberration
(который тоже получается из-за свойств линзы).
На практике на GearVR по умолчанию этот фильтр отключен — чтобы его скомпенсировать надо вместо 1 текстурной выборки в шейдере делать 3, а это мобильное железо.
Итак, вводная закончена, давайте разбираться с latency :)
В предложенной схеме никаких 20 ms не будет и вообще все будет очень плохо.
Потому что...
В начале кадра мы прочитали IMU data — с скажем 1 ms latency;
Потом мы собрали матрицы кадра, отрендерили сцену — это в целом длительный процесс, особенно с учетом качества OpenGL драйверов на Android;
(это еще 16 мс например если 60 FPS render...)
Потом мы отдали кадр на GPU — он долго и упорно этот самый кадр рисует, и потом делает distortion чтобы получить финальную картинку;
(это еще 16 ms)
Потом запускается display scan-out!
Дисплей понятно совсем не CRT, но при этом понятие "луча" считывающего данные все еще осталось — контроллер дисплея считывает дисплей сканлайн за сканлайном и зажигает пиксели.
И чтобы считать весь дисплей — уходит 16 мс.
Сканлайны на телефоне обычно "короткие" — когда вы используете VR то телефон в ландшафтном режиме, и сканлайны это вертикальные полосочки, идущие слева направо (ну или справа налево если телефон перевернут...)
Соответственно самый левый сканлайн (в левом глазу) обновляется на 16 мс раньше чем самый правый (в правом глазу).
И наконец, есть еще одна радость:
Описанный механизм это стандартный double buffering…
В доблестной операционной системе на самом деле triple buffering.
Т.е. когда GPU дорисовывает картинку то она отдается не на дисплей а в композитор, который ее возможно совмещает с другими окнами итп.
И получается еще один кадр (16 ms) задержки.
В итоге короче получилось что-то типа 65 ms latency
Что невообразимо больше чем 20.
Плюс есть еще одна проблема…
Обычно дисплей работает в full persistence mode — а именно, сканлайн зажигается новыми цветами, и потом остается в таком режиме фактически весь кадр — т.е. 16 мс.
Оказывается, что за 16 мс можно достаточно далеко повернуть голову, и в результате этот сканлайн глазом регистрируется в разных точках retina с одним и тем же цветом.
И получается smearing при повороте головы.
Значит если все это не побеждать — GearVR игры выглядели бы как Google Cardboard.
Надо побеждать.
Ключевой технологией является технология которую Oculus называет Time Warp.
Еще она называется reprojection, и наверное как-нибудь еще, но мне нравится слово timewarp.
Идея в следующем — если взять картинку отрендеренную камерой С, потом взять камеру C' которая является камерой C повернутой (но не смещенной) на не очень большое расстояние (единицы-десятки градусов), и отрендерить ей картинку…
То оказывается что вторую картинку из первой можно получить простым 2D преобразованием.
Почему это так? На пальцах, поскольку в картинке отрендеренной из камеры каждый пиксель соответствует лучу из камеры в точку где этот пиксель расположен, то ясно что при повороте камеры пересечение двух фрустумов будет содержать лучи которые будут соответствовать пикселям одного и того же цвета.
Если зафиксировать матрицы и поворачивать голову влево-вправо то на небольших углах поворота тот факт что ты матрицы зафиксировал неочевиден.
Это собственно главный тест того что time warp работает корректно
(зафиксировал матрицы своего рендера и смотришь можно ли плюс-минус 5 градусов вертеть головой).
Можно и больше чем 5 градусов но там уже много информации брать неоткуда поэтому большая часть экрана становится черной.
Собственно "трясти девайс" это стандартный stress test для хедсетов в которых есть positional tracking — фиксируешь голову а дальше руками высокочастотно сдвигаешь хедсет на голове влево-вправо, и значит картинка должна быть стабильной — для этого собственно и надо sub-20 ms (ну и качественный трекинг).
Слушай, а если на Gear VR рендерить с фиксированной матрицей вьюпроджекшен и включить репроджекшен, а потом начать трясти девайс, картинка на нем трясется?
Ммм, "трясти" — плохо т.к. нет positional tracking и что будет непонятно Ну если нет positional tracking, то тряска — это будет просто изменение угла, пускай даже маленькое.
Ну да, но в реальном мире будет меняться позиция а в хедсете нет — т.е. стабильной картинки не получится.
Можно пробовать высокочастотно крутить голову влево-вправо если шея не отвалится :)
Timewarp делается просто — мы уже рисуем тот самый distortion mesh.
Поэтому можно просто в вершинном шейдере "повернуть" текстурные координаты (в 3D пространстве).
Пиксельный шейдер не меняется, timewarp получается фактически бесплатным.
На сколько поворачивать?
На разницу между поворотом головы сейчас и поворотом головы с которым была отрисована исходная картинка.
Чем больше поворот — тем больше будет пустого пространства, его можно заполнять черным цветом
(или можно делать clamp адресацию, на небольших углах поворота получается чуть лучше чем черное, но на больших получается странное).
Теперь, когда у нас есть механизм "доворота" картинки, давайте разберемся как бы его можно использовать чтобы уменьшить latency.
Сначала разберемся с full persistence — это технически не влияет на motion-to-photon, но таки надо убирать.
Для телефонов которые поддерживает GearVR написан специальный драйвер дисплея который включает low persistence mode:
Каждый сканлайн зажигается как раньше нужными цветами, а дальше почти сразу — я точно не знаю через сколько, но скажем 3-4 ms — тухнет (становится черным).
Оказывается что достаточно большое количество людей в силу особенностей восприятия не замечают "моргания".
Говорят что граница восприятия варьируется от где-то 50 Hz моргания до 85 Hz — что является одной из главных причин почему десктопные VR хедсеты (Rift/Vive) работают с частотой 90 Hz.
Плюс low persistence в хедсете воспринимается лучше чем просто моргающая с частотой 60 Hz панель в реальном мире, потому что вроде бы моргает все что ты видишь, и мозг адаптируется...
Ааа, то есть потому что мы уже "убрали" изображение (погасили экран), то это лучше воспринимается? А проблема — это когда он горит все 16 мс, а голова двигается?
Так точно.
Есть штука которая называется vestibulo ocular reflex:
Когда ты поворачиваешь голову у тебя автоматически глаз поворачивается в обратную сторону.
Поэтому при поворотах головы ты можешь фиксировать глаза на объекте и например читать при не очень сильных поворотах без проблем.
И значит если у тебя пиксели горят длительное время, то при проекции на retina одного пикселя получается полосочка из-за поворота глаза и получается smearing / ghosting.
А если пиксель сразу почти выключать то smearing не появляется, и ввиду каких-то особенностей зрения высокочастотное моргание как моргание не воспринимается — я не знаю конкретно почему.
VOR это вообще крутая штука, там натурально напрямую соединен вестибулярный аппарат с глазными мышцами.
Участия мозга практически не требуется, и ultra low latency :)
Теперь при движении головой у нас больше нет smearing, но все еще есть 65 ms latency…
Давайте теперь разберемся как на самом деле в GearVR работает рендер.
То что я написал выше про то что в начале рендер кадра мы читаем данные с IMU, потом генерируем OpenGL команды и потом отдаем их GPU который рисует кадр — остается валидным.
Это игровой тред и игровой GL контекст.
Есть еще один тред который создает GearVR SDK и у него есть свой GL context, работающий в high priority режиме
(на новых версиях Android такой контекст можно теперь создать самому с помощью EGL расширения).
High priority режим означает что команды, отданные GPU на этом контексте, начнут исполняться GPU практически сразу.
Я не знаю деталей GPU scheduling на конкретно GPU которые поддерживают GearVR — предположу, что там preemption granularity это draw call — т.е. если у вас нет огромных 10 msec draw calls то можно за одну-две миллисекунды закончить текущий и начать следующий.
На десктопном GPU у NVidia эта гранулярность обычно в draw call а на AMD если я не ошибаюсь 1 wave (для compute), ну и десктопные вендоры работают над улучшением т.к. эта фича тоже важна на десктопном VR...
Вторая особенность этого VR контекста в том…
Что он рисует сразу в front buffer — в тот самый буфер из которого происходит scan out — таким образом пропуская все композиторы и прочую ерунду.
Как мы уже разобрались, scan out занимает 16 мс и идет по сканлайнам один за другим в строгом порядке.
Сканлайны это вертикальные полоски которые идут слева направо (в ландшафтном режиме), поэтому половина полосок содержат картинку из левого глаза (сканируются за 8 мс), а вторая половина полосок содержит картинку из правого глаза (сканируется за 8 мс).
А значит можно попробовать рисовать в правый глаз пока дисплей сканирует левый и наоборот.
Отмечу на всякий случай — это не означает что мы будем рисовать прямо треугольники из кадра игры!
Тут нужен очень аккуратный тайминг и четкие гарантии, поэтому игра там как получится рисует кадр, а VR контекст будет его корректировать с помощью time warp.
Итак, этот самый VR тред просыпается дважды — когда vsync интервал начался, и через 8 мс после этого (в середине scan out).
Читает свежие IMU данные (с latency 1 ms), а дальше генерирует две матрицы корректирующего поворота.
Почему две?
Из-за scan out левый сканлайн левого глаза и правый сканлайн левого глаза разнесены во времени на 8 мс (с точки зрения photon emission time), поэтому в идеале надо их корректировать по-разному.
Поэтому мы читаем данные IMU (latency 1 ms), и генерируем две ориентации головы используя gyroscope данные для prediction вперед.
Первую мы предиктим вперед на 8 мс, вторую на 16 мс (сейчас будет понятно почему).
Потом сабмитим draw call в high priority context в котором половина того distortion mesh сверху для нужного глаза и надеемся что GPU его достаточно быстро подберет и отрисует.
Через 8 мс после того как мы сгенерировали ориентации scan out дойдет до той части экрана в которую мы только что нарисовали данные и начнет ее показывать.
В итоге полная latency до photon emission крайнего слева сканлайна каждого глаза получается примерно 9 ms, а крайнего справа — примерно 17 ms.
(те кто 30 лет назад на Amiga писали race-the-beam rasterizers — прослезитесь)
Собственно если порезать distortion mesh на вертикальные полосочки и иметь более четкие timing гарантии — как я понимаю, с текущим GPU это невозможно — то можно было бы уменьшать motion-to-photon до <10 ms.
Как видно, половина (8 ms) текущей latency это типа safety буфер чтобы уложиться и засабмитить draw call на GPU и чтобы он его отрисовал, и половина — это худший scanout latency для сканлайна.
Если резать на полосочки и иметь более четкий тайминг то можно это попробовать решать.
Или прямо интегрировать time warp в scanout контроллер, операция warp — это просто билинейный семплинг текстуры, не очень сложно.
Но по большому счету текущий latency — good enough. Если увеличить частоту дисплея до 90 Hz чтобы убрать проблемы с морганием для более чувствительных людей, то получится на той же технологии сразу примерно 10 ms.
Да, 8 ms это вообще говоря произвольный буфер Можно теоретически и меньше
Теоретически можно меньше, практически заметно меньше наверное становится опасно.
Если preemption на draw call basis то надо учитывать это тоже
Особенно короче в VR на mobile есть тенденция запихивать всю сцену в 1-2 draw calls
Потому что OpenGL медленный, Unity медленный, итп.
Итого, проснулись сразу после vsync — в это время scan out начал процессить левый глаз — и нарисовали правый глаз в front buffer.
Потом заснули до середины vsync interval, проснулись — в это время scan out начал процессить правый глаз — и нарисовали левый глаз в front buffer.
Ad infinitum.
Отмечу, time warp имеет забавную проблему…
Он умеет только довернуть картинку.
Не умеет ее смещать — при смещении начинаются проблемы disocclusion, нужен depth buffer, больше ресурсов итп.
Но на самом деле глаза два, и когда вы поворачиваете голову — глаза смещаются и поворачиваются одновременно.
Поэтому чем больше timewarp коррекция тем более неправильной получается stereo картинка и тем сложнее фокусироваться.
На практике получается что time warp на 20-30 msec к видимым проблемам не приводит, кроме как при огромных скоростях поворота где convergence перестает работать, наверное.
Еще пара моментов которые GearVR умеет делать, и которые в обычном Андроиде невозможны...
Во-первых, можно включить realtime приоритет своему рендер потоку (ну и он включается VR потоку)
Чтобы всякие там потоки garbage collection не очень мешали.
Во-вторых, можно включить (точнее, нужно) fixed clock frequency.
По-умолчанию частота CPU и GPU варьируется в зависимости от нагрузки и прочих факторов.
На практике в Андроиде эти механизмы работают очень странно, и могут привести к тому что нагрузка которая должна укладываться в 16 ms на кадр несколько секунд подряд не укладывается.
Поэтому SDK отключает динамический adjustment и позволяет выбрать приложению фиксированные frequency scales для CPU/GPU отдельно.
Вот, я значит уточню. Что весь этот цикл — он для этого специального треда про time warp А собственно рендер приложения ему параллелен
Да, я где-то там выше это писал. Рендер тред взаимодействует с этим time warp тредом через функцию "я закончил сабмитить на GPU команды для следующего кадра, держи его".
А так два независимых треда и два фактически независимых контекста (ну, наверное в первый контекст вставляются fences чтобы не иметь проблем с синхронизацией). И в приложении все ещё может быть и 16 msec на CPU в драйвере, и 16 msec на отрисовку
В принципе может. Может быть и 40 ms на CPU. :) Вот до каких чисел нужно это уменьшать, чтобы остальное вывозил time warp?
Тут сказать сложно однозначно. Очень зависит от контента. В целом Oculus рекомендует держать стабильные 60 fps (16 ms cpu / gpu) и рассчитывать на time warp для убирания этих лишних 20-30 ms latency, и для сглаживания внезапных frame spikes.
Но если у тебя контент плюс-минус статический — например, представь себе adventure game — то в SDK есть режим в котором они ожидают что ты отдаешь им кадры раз в 33 ms а не в 16.
И time warp работает почти идеально (почти — т.к. см. выше про движение глаз при поворотах) 60 FPS, но 32 msec latency, ага?
20-30, там зависит от того как конкретно работает драйвер для GPU который обычно tiled, насколько быстро ты можешь засабмитить все draw calls для первого frame buffer и т.п.
Но в целом 32.
Ты это будешь в основном видеть на позициях других объектов, если будешь.
Т.е. это не отличается от стандартных вопросов latency в обычных играх — реакция от нажатия на какую-нибудь клавишу до движения и т.п., это все не так критично как head tracking.
А, еще забыл! Про Cardboard.
Вот как видно из вышеперечисленного — есть ровно ОДИН момент в hardware у GearVR.
Хорошие сенсоры с маленьким latency и большим refresh rate.
Все остальное — software.
(есть еще линзы но в GearVR линзы насколько я понимаю — ничем не выделяются, в cardboard девайсах такие же считай)
А про это можно подробно где-то почитать? Про линзу саму. Мне интересно, почему с объектами, которые совсем близко получается фигня. Там левая-правая картинки расходятся так сильно, что я не могу сфокусироваться. А на реальном объекте — могу. Легко тестить на HTC Vive — контроллер к шлему подносишь, потом шлем снимаешь и на него же смотришь в реале.
Там есть проблема которая называется vergence-accomodation conflict, может быть из-за этого…
Vergence — это феномен при котором глаза поворачиваются так чтобы смотреть на объект:
Если объект далеко то глаза смотрят прямо;
Если объект совсем близко то они поворачиваются к носу например если объект перед носом.
Accomodation — это та самая фокусировка зрения, там изменяются оптические свойства глаза чтобы фокусироваться (https://en.wikipedia.org/wiki/Accommodation_(eye) ).
И вот значит в VR происходит следующая штука:
Vergence работает корректно т.к. каждому глазу дается своя картинка, и если объект близко то в левом и правом глазах разные изображения — объект сильно смещен и глаза поворачиваются как надо.
А accomodation не работает т.к. линза одна и она не настраивается никак поэтому все объекты на оптической бесконечности.
Этот конфликт разными людьми резолвится по-разному, ну и чем ближе объект тем больше конфликт.
Судя по моим экспериментам с сенсорами на разных телефонах с т.з. latency…
У Samsung и Apple трекинг заметно хуже, но у Nexus 6 трекинг хуже но уже в пределах разумного.
(ЕМНИП 4ms refresh interval, avg 3 ms latency)
Т.е. на Nexus 6 — можно делать все что я написал выше и иметь experience натурально сравнимый с GearVR, но в картонной коробке.
Проблема в том что практически все что я написал выше не сделать из user mode — нужно патчить ядро и драйвера.
Так что авторы приложений этого делать не могут, но вот Гугл — вполне.
игрался я вчера как раз на Gear VR — на знаю кому как, но меня мутит, хотя на осознанном уровне я лага не замечаю ну и пиксели громадные!
Главная проблема GearVR с т.з. motion sickness — отсутствие positional tracking.
Когда ты двигаешь голову, картинка это движение не отражает — это порождает конфликт между вестибулярной и оптической системой Были слухи, что направления ресеча на будущее в Окулусе — это positional tracking для mobile и AR
Ну я не удивлен, positional tracking это missing feature #1, #2 и #3 в gear.
#4 наверное full RGB display
Пока все успешные подходы к positional tracking требуют внешней камеры.
Или кучи камер на хедсете как в HoloLens… вот я пробовал оба что rift dk2 что gear vr — одинаково показались бесполезной игрушкой ну у меня еще косоглазие
У меня тоже, это лично мне не очень мешает.
Это то что я там выше писал про convergence
Лично мне плюс-минус пофиг какой там IPD в камерах :)
У меня большая часть восприятия через один из глаз, не через оба — смотря на каком "сфокусируешься" ага — или я 3д плохо вижу у меня было ощущение как будто я внутри проекционной сферы а не внутри 3д мира
Кстати возможно что для людей с косоглазием positional tracking более критичен по-умолчанию — когда у тебя есть стерео зрение то ты расстояние до объектов воспринимаешь собственно им, а когда оно не очень хорошее то главный visual cue это параллакс
А параллакс получается в основном от движения головы
Особенно в gunjack где туррель вокруг тебя
Но тут я слегка out of my depth, не очень хорошо представляю значимость разных факторов восприятия, так что точно не уверен.
Я в целом согласен что меня лично GearVR не очень вставлял, в отличие от десктопных experiences.