Всем привет! В этой статье хочу разобрать процесс подключения шлема виртуальной реальности к десктопному приложению под Windows. Речь пойдет об Oculus Rift.
Архитектурная визуализация — очень благодатная тема для различного рода экспериментов. Мы решили не отставать от тренда. В одной из следующих версий наших BIM-систем (напомню, что я работаю в компании Renga Software, совместном предприятии АСКОН и фирмы «1С»): Renga Architecture — для архитектурно-строительного проектирования и Renga Structure — для проектирования конструктивной части зданий и сооружений, появится возможность хождения по проектируемому зданию в шлеме виртуальной реальности. Это очень удобно для демонстрации проекта заказчику и оценки тех или иных проектных решений с точки зрения эргономики.
На сайте разработчика шлема доступно для скачивания SDK. На момент написания статьи последняя доступная версия 1.16. Существует еще OpenVR от Valve. Сам я эту штуку не пробовал, но есть подозрения, что она работает хуже, чем нативный для Oculus SDK.
Разберем основные этапы подключения шлема к приложению. Вначале необходимо инициализировать устройство:
Инициализация завершена. Теперь у нас есть session — это указатель на внутреннюю структуру ovrHmdStruct. Его мы будем использовать для всех запросов к oculus runtime. luid — это идентификатор графического адаптера, к которому подсоединился шлем. Он необходим для конфигураций с несколькими видеокартами или ноутбуков. Приложение должно использовать для отрисовки этот же адаптер.
Процесс создания кадра в обычном режиме и для шлема Oculus Rift отличается не очень сильно.
Для каждого глаза нам нужно создать текстуру вместе с SwapChain и RenderTarget.
Для создания текстур Oculus SDK предоставляет набор функций.
Пример обертки для создания и хранения SwapChain и RenderTarget для каждого глаза:
С помощью этой обертки создаем для каждого глаза текстуру, куда мы будем отрисовывать 3D-сцену. Размер текстуры необходимо узнать у Oculus Runtime. Для этого нужно получить description устройства и с помощью функции ovr_GetFovTextureSize получить необходимый размер текстур для каждого глаза:
Еще удобно создать так называемую Mirror Texture. Эту текстуру можно показывать в окне приложения. В эту текстуру Oculus Runtime будет копировать объединенное для двух глаз изображение после постобработки.
Если не делать пост обработку, то человек увидит в шлеме изображение, полученное с помощью оптической системы с положительной дисторсией (изображение слева). Для компенсации oculus накладывает эффект отрицательной дисторсии (изображение справа).
Код создания mirror texture:
Важный момент при создании текстур. При создании SwapChain с помощью функции ovr_CreateTextureSwapChainDX мы передаем желаемый формат текстуры. Этот формат используется в дальнейшем для постобработки рантаймом Oculus.
Чтобы все правильно работало, приложение должно создавать swap chain в sRGB цветовом пространстве. Например, OVR_FORMAT_R8G8B8A8_UNORM_SRGB. Если вы в своем приложении не делаете гамма-коррекцию, то необходимо создавать swap chain в формате sRGB. Задать флаг ovrTextureMisc_DX_Typeless в ovrTextureSwapChainDesc. Создать Render Target в формате DXGI_FORMAT_R8G8B8A8_UNORM. Если этого не сделать, то изображение на экране будет слишком светлым.
После того, как мы подключили шлем и создали текстуры для каждого глаза, нужно отрисовать сцену в соответствующую текстуру. Это делается обычным способом, который предоставляет direct3d. Тут ничего интересного. Нужно только получить от шлема его положение, делается это так:
Теперь при создании кадра нужно не забыть для каждого глаза задать правильную матрицу вида с учетом положения шлема.
После того, как мы отрисовали сцену для каждого глаза, нужно передать получившиеся текстуры на постобработку в рантайм Oculus.
Делается это так:
После этого в шлеме появится готовое изображение. Для того чтобы показать в приложении то, что видит человек в шлеме, можно скопировать в back buffer окна приложения содержимое mirror texture, которую мы создали ранее:
На этом все. Много хороших примеров можно найти в Oculus SDK.
Архитектурная визуализация — очень благодатная тема для различного рода экспериментов. Мы решили не отставать от тренда. В одной из следующих версий наших BIM-систем (напомню, что я работаю в компании Renga Software, совместном предприятии АСКОН и фирмы «1С»): Renga Architecture — для архитектурно-строительного проектирования и Renga Structure — для проектирования конструктивной части зданий и сооружений, появится возможность хождения по проектируемому зданию в шлеме виртуальной реальности. Это очень удобно для демонстрации проекта заказчику и оценки тех или иных проектных решений с точки зрения эргономики.
На сайте разработчика шлема доступно для скачивания SDK. На момент написания статьи последняя доступная версия 1.16. Существует еще OpenVR от Valve. Сам я эту штуку не пробовал, но есть подозрения, что она работает хуже, чем нативный для Oculus SDK.
Разберем основные этапы подключения шлема к приложению. Вначале необходимо инициализировать устройство:
#define OVR_D3D_VERSION 11 // in case direct3d 11
#include "OVR_CAPI_D3D.h"
bool InitOculus()
{
ovrSession session = 0;
ovrGraphicsLuid luid = 0;
// Initializes LibOVR, and the Rift
ovrInitParams initParams = { ovrInit_RequestVersion, OVR_MINOR_VERSION, NULL, 0, 0 };
if (!OVR_SUCCESS(ovr_Initialize(&initParams)))
return false;
if (!OVR_SUCCESS(ovr_Create(&session, &luid)))
return false;
// FloorLevel will give tracking poses where the floor height is 0
if(!OVR_SUCCESS(ovr_SetTrackingOriginType(session, ovrTrackingOrigin_EyeLevel)))
return false;
return true;
}
Инициализация завершена. Теперь у нас есть session — это указатель на внутреннюю структуру ovrHmdStruct. Его мы будем использовать для всех запросов к oculus runtime. luid — это идентификатор графического адаптера, к которому подсоединился шлем. Он необходим для конфигураций с несколькими видеокартами или ноутбуков. Приложение должно использовать для отрисовки этот же адаптер.
Процесс создания кадра в обычном режиме и для шлема Oculus Rift отличается не очень сильно.
Для каждого глаза нам нужно создать текстуру вместе с SwapChain и RenderTarget.
Для создания текстур Oculus SDK предоставляет набор функций.
Пример обертки для создания и хранения SwapChain и RenderTarget для каждого глаза:
struct EyeTexture
{
ovrSession Session;
ovrTextureSwapChain TextureChain;
std::vector<ID3D11RenderTargetView*> TexRtv;
EyeTexture() :
Session(nullptr),
TextureChain(nullptr)
{
}
bool Create(ovrSession session, int sizeW, int sizeH)
{
Session = session;
ovrTextureSwapChainDesc desc = {};
desc.Type = ovrTexture_2D;
desc.ArraySize = 1;
desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB;
desc.Width = sizeW;
desc.Height = sizeH;
desc.MipLevels = 1;
desc.SampleCount = 1;
desc.MiscFlags = ovrTextureMisc_DX_Typeless;
desc.BindFlags = ovrTextureBind_DX_RenderTarget;
desc.StaticImage = ovrFalse;
ovrResult result = ovr_CreateTextureSwapChainDX(Session, pDevice, &desc, &TextureChain);
if (!OVR_SUCCESS(result))
return false;
int textureCount = 0;
ovr_GetTextureSwapChainLength(Session, TextureChain, &textureCount);
for (int i = 0; i < textureCount; ++i)
{
ID3D11Texture2D* tex = nullptr;
ovr_GetTextureSwapChainBufferDX(Session, TextureChain, i, IID_PPV_ARGS(&tex));
D3D11_RENDER_TARGET_VIEW_DESC rtvd = {};
rtvd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
ID3D11RenderTargetView* rtv;
DIRECTX.Device->CreateRenderTargetView(tex, &rtvd, &rtv);
TexRtv.push_back(rtv);
tex->Release();
}
return true;
}
~EyeTexture()
{
for (int i = 0; i < (int)TexRtv.size(); ++i)
{
Release(TexRtv[i]);
}
if (TextureChain)
{
ovr_DestroyTextureSwapChain(Session, TextureChain);
}
}
ID3D11RenderTargetView* GetRTV()
{
int index = 0;
ovr_GetTextureSwapChainCurrentIndex(Session, TextureChain, &index);
return TexRtv[index];
}
void Commit()
{
ovr_CommitTextureSwapChain(Session, TextureChain);
}
};
С помощью этой обертки создаем для каждого глаза текстуру, куда мы будем отрисовывать 3D-сцену. Размер текстуры необходимо узнать у Oculus Runtime. Для этого нужно получить description устройства и с помощью функции ovr_GetFovTextureSize получить необходимый размер текстур для каждого глаза:
ovrHmdDesc hmdDesc = ovr_GetHmdDesc(session);
ovrSizei idealSize = ovr_GetFovTextureSize(session, (ovrEyeType)eye, hmdDesc.DefaultEyeFov[eye], 1.0f);
Еще удобно создать так называемую Mirror Texture. Эту текстуру можно показывать в окне приложения. В эту текстуру Oculus Runtime будет копировать объединенное для двух глаз изображение после постобработки.
Если не делать пост обработку, то человек увидит в шлеме изображение, полученное с помощью оптической системы с положительной дисторсией (изображение слева). Для компенсации oculus накладывает эффект отрицательной дисторсии (изображение справа).
Код создания mirror texture:
// Create a mirror to see on the monitor.
ovrMirrorTexture mirrorTexture = nullptr;
mirrorDesc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB;
mirrorDesc.Width = width;
mirrorDesc.Height =height;
ovr_CreateMirrorTextureDX(session, pDXDevice, &mirrorDesc, &mirrorTexture);
Важный момент при создании текстур. При создании SwapChain с помощью функции ovr_CreateTextureSwapChainDX мы передаем желаемый формат текстуры. Этот формат используется в дальнейшем для постобработки рантаймом Oculus.
Чтобы все правильно работало, приложение должно создавать swap chain в sRGB цветовом пространстве. Например, OVR_FORMAT_R8G8B8A8_UNORM_SRGB. Если вы в своем приложении не делаете гамма-коррекцию, то необходимо создавать swap chain в формате sRGB. Задать флаг ovrTextureMisc_DX_Typeless в ovrTextureSwapChainDesc. Создать Render Target в формате DXGI_FORMAT_R8G8B8A8_UNORM. Если этого не сделать, то изображение на экране будет слишком светлым.
После того, как мы подключили шлем и создали текстуры для каждого глаза, нужно отрисовать сцену в соответствующую текстуру. Это делается обычным способом, который предоставляет direct3d. Тут ничего интересного. Нужно только получить от шлема его положение, делается это так:
ovrHmdDesc hmdDesc = ovr_GetHmdDesc(session);
ovrEyeRenderDesc eyeRenderDesc[2];
eyeRenderDesc[0] = ovr_GetRenderDesc(session, ovrEye_Left, hmdDesc.DefaultEyeFov[0]);
eyeRenderDesc[1] = ovr_GetRenderDesc(session, ovrEye_Right, hmdDesc.DefaultEyeFov[1]);
// Get both eye poses simultaneously, with IPD offset already included.
ovrPosef EyeRenderPose[2];
ovrVector3f HmdToEyeOffset[2] = { eyeRenderDesc[0].HmdToEyeOffset,
eyeRenderDesc[1].HmdToEyeOffset };
double sensorSampleTime; // sensorSampleTime is fed into the layer later
ovr_GetEyePoses(session, frameIndex, ovrTrue, HmdToEyeOffset, EyeRenderPose, &sensorSampleTime);
Теперь при создании кадра нужно не забыть для каждого глаза задать правильную матрицу вида с учетом положения шлема.
После того, как мы отрисовали сцену для каждого глаза, нужно передать получившиеся текстуры на постобработку в рантайм Oculus.
Делается это так:
OculusTexture * pEyeTexture[2] = { nullptr, nullptr };
// ...
// Draw into eye textures
// ...
// Initialize our single full screen Fov layer.
ovrLayerEyeFov ld = {};
ld.Header.Type = ovrLayerType_EyeFov;
ld.Header.Flags = 0;
for (int eye = 0; eye < 2; ++eye)
{
ld.ColorTexture[eye] = pEyeTexture[eye]->TextureChain;
ld.Viewport[eye] = eyeRenderViewport[eye];
ld.Fov[eye] = hmdDesc.DefaultEyeFov[eye];
ld.RenderPose[eye] = EyeRenderPose[eye];
ld.SensorSampleTime = sensorSampleTime;
}
ovrLayerHeader* layers = &ld.Header;
ovr_SubmitFrame(session, frameIndex, nullptr, &layers, 1);
После этого в шлеме появится готовое изображение. Для того чтобы показать в приложении то, что видит человек в шлеме, можно скопировать в back buffer окна приложения содержимое mirror texture, которую мы создали ранее:
ID3D11Texture2D* tex = nullptr;
ovr_GetMirrorTextureBufferDX(session, mirrorTexture, IID_PPV_ARGS(&tex));
pDXContext->CopyResource(backBufferTexture, tex);
На этом все. Много хороших примеров можно найти в Oculus SDK.