Импортозамещение VR-а клавиатуре покоя не даёт

от автора

Давным-давно, … в общем появился у Sony PlayStation шлем VR. Штука оказалась интересная и позволяла не только играть в vr-игры, но и смотреть фильмы.

Правда, сразу выяснились некоторые «тонкости»: нормальное использование возможно было только при использовании с Sony PlayStation (что, в общем-то, очевидно) и через специализированную программу Rad (бывший LittlStar). Причём особого разнообразия программ-проигрывателей не было, использование же программы Rad требовало оплаты подписки. Сначала всё было хорошо: и подписка платилась, и кино смотрелось. Потом появились санкции и, вдруг, оказалось, что заплатить из России нельзя. И вообще вы ничего не можете, «… until those restrictions and sanctions have been lifted …».

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

Чтобы шлем работал, необходимо было отвязать функцию просмотра фильмов от специализированного / платного / не-российского программного обеспечения. И ниже описан процесс такого «отвязывания».

Эта статья по составу содержит примеры кода и решений, которые нужно добавить в проигрыватель VR, чтобы им можно было нормально пользоваться. Приведённые здесь сведения не являются какими-либо новыми или особенными знаниями. Всё, что здесь приведено вы можете самостоятельно найти в интернете или посчитать на калькуляторе.

Для мнеленьчитать результат здесь: https://github.com/evgenykislov/psvr_player.

Во-первых, определимся с тем, что нам нужно:

  • просмотр 3D фильмов: в режиме полусферы и в режиме плоского экрана;

  • использование программ с открытым исходным кодом. Желательно вообще бесплатных;

  • поддержка Linux;

  • функционал плейера (перемотки, паузы, стоп).

Что не требовалось:

  • вывод звука на наушники шлема.

Поиск по интернету показал, что есть пара жизнеспособных подходов:

  • использование Steam, протокола SteamVR и драйвера OpenPSVR;

  • отдельный проигрыватель с нужными функциями.

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

Со вторым вариантом тоже всё не сахарно: много проигрывателей в стадии заготовок: умеем выводить картинку и иногда считывать сенсоры.

В результате выбор пал на проигрыватель psvr от Florian Märkl, который и был подвергнут доработке. Проигрыватель обладал рядом «плюшек»:

  • реализован функционал проигрывания видеофайлов с использованием библиотеки vlc;

  • вывод изображения «на сферу» с использованием шейдеров;

  • отслеживание положения шлема и корректировка изображения;

  • проигрыватель заявлен как кросс-платформенный: Windows, Linux;

  • для использования шлема не требовалась сама Sony PlayStation;

  • код открыт и бесплатен (лицензия GPL3).

В общем, много чего уже было сделано. Хотя и осталось тоже достаточно:

  • запуск без прав root (актуально для Linux);

  • включение режима VR;

  • добавить клавиатурные комбинации и вывести дополнительную информацию;

  • добавить плоский вид: для просмотра «обычных» 3D фильмов;

  • реализовать компенсацию дисторсии;

  • реализовать компенсацию хроматической абберации.

Запуск без root прав

Шлем PS VR подключается к компьютеру (как на Windows, так и на Linux) посредством процессорного модуля через 2 разъёма: hdmi для передачи видеопотока и usb для получения данных с шлема и управления режимами.

При подключении шлема как usb устройства появляются несколько устройств. Устройства имеют идентификатор производителя (idVendor) 054с и идентификатор продукта (idProduct) 09af, находятся глубоко в дереве устройств и по умолчанию у них права только для root.

Для проигрывателя интересны интерфейсы с номерами:

  • 4 — используется для вычитывания данных сенсоров (используется для вычисления положения шлема: наклон / вращение);

  • 5 — используется для управления режимами шлема.

Использование прав root для запуска проигрывателя это довольно нехорошая идея. Правильнее выставить права на устройства. И для этого используется механизм udev. Механизм реализуется посредством правил, содержащихся в папке /etc/udev/rules.d. В нашем случае создаём правила для смены режима устройств при их создании:

Создаём (под sudo) файл /etc/udev/rules.d/99-psvr.rules с содержимым:

SUBSYSTEM=="usb",ATTRS{idVendor}=="054c",ATTRS{idProduct}=="09af",MODE="0666"

Здесь usb, idVendor и idProduct — тип устройств, идентификаторы производителя и продукта, для которых применяется правило: выставляется режим 0666 — позволяем всем читать и записывать в устройства.

Для обновления правил либо перезагружаем компьютер, либо используем пару команд (под sudo):

sudo udevadm control —reload-rules
sudo udevadm trigger

Включаем режим VR

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

Для включения / выключения режима VR необходимо найти устройство для управления. Находим все устройства с требуемым vendorid и productid и среди них выбираем имя устройства, заканчивающееся на «:05».

const unsigned short kPsvrVendorID = 0x054c; const unsigned short kPsvrProductID = 0x09af; auto devs = hid_enumerate(kPsvrVendorID, kPsvrProductID); 
const char kPsvrControlInterface[4] = ":05"; std::string p = dev->path; if (p.substr(p.length() - 3) == kPsvrControlInterface) { ... }

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

По найденному имени открываем устройство и прописываем в него «правильный» пакет:

device_ = hid_open_path(dev_name.c_str()); buffer_[0] = 0x23; buffer_[1] = 0x00; buffer_[2] = 0xaa; buffer_[3] = 0x04; buffer_[4] = vrmode ? 0x01 : 0x00; buffer_[5] = 0x00; buffer_[6] = 0x00; buffer_[7] = 0x00; return hid_write((hid_device*)device_, buffer_, 8) != -1; 

Буфер команды для шлема содержит 8 байт:
0x00 — 0x23 — команда управления. В данном случае управление режимом VR;
0x01 — 0x00, 0xaa — магические числа;
0x03 — 0x04 — длина данных команды;
0x04 — 0x00000001/0x00000000 — флаг включения или выключения режима VR.

Подробнее про команды можно почитать здесь: github gusmanb: там есть и код фреймворка для работы с PSVR, и раздел wiki, содержащий информацию по управлению.

Также для управления был создан класс PsvrControl здесь: github.

Добавляем клавиатурные комбинации и выводим дополнительную информацию

Так как просматривают фильмы в vr шлеме, то наличие клавиатурных комбинаций является очень-очень полезной штукой. Основное требование: возможность «нащупать» нужные клавиши вслепую.

В Qt обработка клавиатурных комбинаций делается стандартными способами:

Создаём класс наследник QObject и перегружаем функцию

virtual bool eventFilter(QObject*, QEvent* event) override;

В реализации создаём массив описаний клавишных комбинаций и по ним вызываем сигналы:

struct KeyEvent {   int key;   bool ctrl;   bool shift;   bool alt;   std::function<void(KeyFilter*)> processor; }; KeyEvent g_keys[] = { {Qt::Key_Space, false, false, false, [](KeyFilter* kf){ emit kf->Pause(); }}, {Qt::Key_Space, true,  false, false, [](KeyFilter* kf){ emit kf->Stop(); }}, ... for (auto it = std::begin(g_keys); it != std::end(g_keys); ++it) { if (it->key == key && it->ctrl == ctrl && it->shift == shift && it->alt == alt) { it->processor(this); return true; }

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

Из выводимой информации оказалась полезной длина фильма по времени и текущая точка проигрывания. Вроде очевидно, но ценить начинаешь только когда проигрыватель без них.

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

Для получения длины фильма пользуемся библиотекjq vlc: на открытую media (файл с фильмом) получаем менеджер событий и регистрируем в нём обработчик по событию парсинга:

media = libvlc_media_new_path(libvlc, path); ... auto media_evman = libvlc_media_event_manager(media); assert(media_evman); libvlc_event_attach(media_evman, libvlc_MediaParsedChanged, vlc_event, this); 

Далее запрашиваем парсинг открытой media для получения параметров:

  if (libvlc_media_parse_with_options(media, libvlc_media_parse_network, kParseTimeout) == -1) { // TODO process parsing error } 

По завершении парсинга вызовется обработчик vlc_event с типом события libvlc_MediaParsedChanged. После этого можно уже получать длительность фильма

auto dur = libvlc_media_get_duration(media);

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

Добавить плоский вид

Для 3D фильмов используются несколько форматов перспективы и вывод на плоскость является одним из часто используемых. В исходном проигрывателе поддержки такого формата не было — делаем.

Хотя вид является «плоским», использовать для вывода лучше немного закруглённый цилиндр: в кинотеатрах экран тоже закругляют, и мониторы уже делают гнутыми.

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

С нашей стороны необходимо сделать фрагментный шейдер, который выдаёт цвет на заданные координаты.

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

vec4 GetCylinderColor(vec3 position) {   ...   const float plane_arc = 1.3;   const float xarc = plane_arc * plane_distance / cylinder_radius; // in radians

Для вывода на цилиндр обязательно нужно определиться с дугой на которую будет растянута сцена. plane_arc — та самая дуга: угол с точки зрения наблюдателя в радианах. И xarc — её аппроксимация на цилиндр экрана.

Далее определяем углы, под которыми находится отображаемый пиксель. Здесь необходимо учитывать, что координаты в шейдере они не декартовы, они однородные. Поэтому всё лучше считать в относительных величинах: position.x / position.z покажет относительную координату x, а фактически это тангенс угла направления в горизонтальной плоскости:

  float relx = position.x / (position.z / plane_distance);   float rely = position.y / (position.z / plane_distance);   float anglex = atan(-relx, cylinder_radius);   float angley = atan(rely, cylinder_radius); 

Далее отсекаем лишнее (вне заданной зоны всё чёрное) и приводим координаты к диапазону (0-1;0-1):

if (anglex < -xarc/2 || anglex > xarc/2) {   return vec4(0.0); } if (angley < -yarc/2 || angley > yarc/2) { return vec4(0.0); } vec2 cyl_coor; cyl_coor.x = 0.5 * anglex / (xarc / 2.0) + 0.5; cyl_coor.y = 0.5 * angley / (yarc / 2.0) + 0.5;

Далее вытаскиваем с текстуры цвет (texture(…)) с учётом матрицы обзора, которая выбирает, какую половинку (правую/левую) показать для соответствующего глаза (вообще в проигрывателе идёт двойная обработка: сначала для одного глаза (одна половина изображения), потом для другого глаза):

  vec2 uv = min_max_uv_uni.xy + (min_max_uv_uni.zw - min_max_uv_uni.xy) * cyl_coor; return vec4(texture(tex_uni, uv).rgb, 1.0);

В результате получаем отображение на цилиндр и можем смотреть обычные 3D фильмы. В общем, уже можно начинать пользоваться.

Компенсация дисторсии

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

Для дисторсии есть модели, например Брауна-Конради. И, судя по дефекту изображения, нам нужно на «подушку» поставить «бочку» и всё будет ОК. На практике всё оказалось несколько интереснее, так как на изображении на экране компьютера все прямые ровненькие и перпендикулярненькие. А если на это изображение посмотреть через vr шлем, то явно видна дисторсия. Поэтому пришлось подбирать коэффициенты «на глаз». Для компенсации использовалась упрощённая модель:

dis_length = length + k1 * length^2 + k2 * length^4

где length — это расстояние от центра обзора до требуемой точки;

dis_length — это новое расстояние до точки в том же направлении;

k1, k2 — подбираемые коэффициенты.

Длина до точки берётся на некоторых, относительных единицах. В проигрывателе взято максимальное удаление пикселя по диагонали за единицу, и относительно этого размера считалось всё остальное.

Коэффициенты подбирались из «прямизны» картинки: сначала подбирался коэффициент k1, чтобы в целом всё было ровно. Потом подбирался коэффициент k2, чтобы на краю изображения не возникали резкие волны и переходы.

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

Задаёмся константами:

    const float scr_radius = 1.8;     const float dis_k1 = -0.32; // Manual calibration     const float dis_k2 = 0.015;     const float min_radius = 0.01;     const float max_radius = 3.0;

scr-radius — это наш единичный радиус с привязкой к однородным координатам;

dis_k1, dis_k2 — подобранные коэффициенты;

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

max_radius — тоже важное ограничение: максимальный радиус для дисторсии. Всё, что снаружи считается плоским. Здесь тоже тонкость: если не ставить ограничение по максимуму, то где-то за краями зрения (обсчитывается ведь вся сцена) треугольники (через модель дисторсии) могут сосчитаться и попасть в центр сцены. Этакий завёрнутый бублик. Шейдеру то всё равно, он сосчитает, а у вас будет вырви-глаз.

Далее всё по математике: находим относительные координаты (dispos), считаем длину dislen (относительную), и прикладываем к уравнению (newlen). В результате получим вектор distorsion с корректировками для координат x и y, которым двигаем позицию:

    vec2 dispos;     vec4 distorsion = vec4(1.0, 1.0, 1.0, 1.0);     dispos.x = scr_pos.x / scr_pos.z;     dispos.y = scr_pos.y / scr_pos.z;     float dislen = length(dispos) / scr_radius;     if (dislen > min_radius && dislen < max_radius) {       float dislen2 = dislen * dislen;       float dislen4 = dislen2 * dislen2;       float newlen = dislen + dis_k1 * dislen2 + dis_k2 * dislen4;       float dis_koef = newlen / dislen;       distorsion = vec4(dis_koef, dis_koef, 1.0, 1.0);     } gl_Position = scr_pos * distorsion;  

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

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

  for (size_t i = 0; i < kTriangleFactor; ++i) { for (size_t j = 0; j < kTriangleFactor; ++j) { QVector3D s1 = ApproximateVertice(p1, p2, p3, p4, static_cast<double>(i) / kTriangleFactor, static_cast<double>(j) / kTriangleFactor); QVector3D s2 = ApproximateVertice(p1, p2, p3, p4, static_cast<double>(i + 1) / kTriangleFactor, static_cast<double>(j) / kTriangleFactor); QVector3D s3 = ApproximateVertice(p1, p2, p3, p4, static_cast<double>(i + 1) / kTriangleFactor, static_cast<double>(j + 1) / kTriangleFactor); QVector3D s4 = ApproximateVertice(p1, p2, p3, p4, static_cast<double>(i) / kTriangleFactor, static_cast<double>(j + 1) / kTriangleFactor); AddSquareToVertices(s1, s2, s3, s4); } } 

где kTriangleFactor указывает на сколько квадратиков (по каждой оси) делить один большой квадрат. И для каждого квадрата (4 вершины) создаются 2 треугольника (всего 6 вершин):

void HMDWidget::AddSquareToVertices(QVector3D p1, QVector3D p2, QVector3D p3, QVector3D p4) { cube_vertices_.push_back(p1); cube_vertices_.push_back(p2); cube_vertices_.push_back(p3); cube_vertices_.push_back(p3); cube_vertices_.push_back(p4); cube_vertices_.push_back(p1); }

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

Компенсация хроматической абберации

Другой проблемой при просмотре фильмов через шлем была хроматическая абберация. Выражалось это в сдвижке цвета при удалении от центра обзора: предметы приобретали сине-зелёные ореолы. Выглядит, конечно, немного фантасмагорично. Но нам же не любоваться — поэтому устраняем.

Изучение интернета показало, что цвет расползается линейно: чем дальше от центра, тем больше расползание. Одна из тонкостей: расползание по горизонтали и по вертикали — различное. И, конечно, разные цвета расползаются по-разному.

Для компенсации абберации достаточно считать красный цвет за эталон и к нему подгонять синий и зелёный.

Для компенсации использовался функционал вершинного и фрагментного шейдера:

  • в вершинном обсчитывалось отклонение от центра обзора;

  • в фрагментном находились цвета по отдельности для красного, синего и зелёного пикселя.

Итак, как всегда, задаёмся константами:

  // Compensate chromatic abberation   float blue_y = -0.015; // 1.0192   float blue_x = -0.01; // 1.0224   float green_y = -0.005; // 1.0078   float green_x = -0.004; // 1.0091 

Далее находим смещение от центра в относительных координатах:

  blue_x_disp = gl_Position.x / gl_Position.z * blue_x;   blue_y_disp = gl_Position.y / gl_Position.z * blue_y;   green_x_disp = gl_Position.x / gl_Position.z * green_x;   green_y_disp = gl_Position.y / gl_Position.z * green_y; 

Смещение выдаётся во фрагментный шейдер (не забываем: оно аппроксимируется на треугольник; но треугольники у нас маленькие) и там вычисляются три точки, которые соответствуют цветам одного пикселя:

  // Calculates position of colored point   vec3 pos_red = position_var; // Red without correction   vec3 pos_blue = position_var;   pos_blue.x += pos_blue.z * blue_x_disp;   pos_blue.y += pos_blue.z * blue_y_disp;   vec3 pos_green = position_var;   pos_green.x += pos_green.z * green_x_disp;   pos_green.y += pos_green.z * green_y_disp; 

Далее для трёх точек считаем цвет по пространственной модели (цилиндр или сфера) и выдаём результат:

    color_out = GetCylinderColor(pos_red);     color_out.b = GetCylinderColor(pos_blue).b;     color_out.g = GetCylinderColor(pos_green).g; 

В результате получаем картинку, которую можно вполне можно смотреть.

Результат

На выходе получился проигрыватель 3D фильмов для шлема Sony PlayStation VR. Результат здесь: https://github.com/evgenykislov/psvr_player.


Примечание:

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

Изображение взято из открытого источника.


ссылка на оригинал статьи https://habr.com/ru/post/691116/


Комментарии

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

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