Графическая привязка

Чтобы иметь возможность использовать Удаленную отрисовку Azure в пользовательском приложении, ее необходимо интегрировать в конвейер отрисовки приложения. За эту интеграцию отвечает графическая привязка.

После настройки графическая привязка обеспечивает доступ к различным функциям, влияющим на отрисованное изображение. Эти функции можно разделить на две категории: общие функции, которые всегда доступны, и специальные функции, которые соответствуют выбранному типу Microsoft.Azure.RemoteRendering.GraphicsApiType.

Графическая привязка в Unity

В Unity вся привязка обрабатывается структурой RemoteUnityClientInit, передаваемой в RemoteManagerUnity.InitializeManager. Чтобы задать графический режим, в поле GraphicsApiType необходимо задать выбранную привязку. Это поле заполняется автоматически, в зависимости от наличия XRDevice. Это поведение можно переопределить вручную, используя следующие варианты:

Еще одной важной частью для Unity является только доступ к базовой привязке, все остальные разделы, приведенные ниже, можно пропустить.

Настройка графической привязки в пользовательских приложениях

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

RemoteRenderingInitialization managerInit = new RemoteRenderingInitialization();
managerInit.GraphicsApi = GraphicsApiType.OpenXrD3D11;
managerInit.ConnectionType = ConnectionType.General;
managerInit.Right = ///...
RemoteManagerStatic.StartupRemoteRendering(managerInit);
RemoteRenderingInitialization managerInit;
managerInit.GraphicsApi = GraphicsApiType::OpenXrD3D11;
managerInit.ConnectionType = ConnectionType::General;
managerInit.Right = ///...
StartupRemoteRendering(managerInit); // static function in namespace Microsoft::Azure::RemoteRendering

Вышеупомянутый вызов должен быть выполнен до обращения к другим интерфейсам API удаленной отрисовки. Аналогичным образом следует вызывать соответствующую функцию RemoteManagerStatic.ShutdownRemoteRendering(); de-init после того, как все остальные объекты Удаленная отрисовка уже уничтожены. Для WMR StartupRemoteRendering также необходимо вызвать до вызова любого голографического API. Для OpenXR то же самое применяется для любых API, связанных с OpenXR.

Обращение к графической привязке

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

RenderingSession currentSession = ...;
if (currentSession.GraphicsBinding != null)
{
    FrameStatistics frameStatistics;
    if (currentSession.GraphicsBinding.GetLastFrameStatistics(out frameStatistics) == Result.Success)
    {
        ...
    }
}
ApiHandle<RenderingSession> currentSession = ...;
if (ApiHandle<GraphicsBinding> binding = currentSession->GetGraphicsBinding())
{
    FrameStatistics frameStatistics;
    if (binding->GetLastFrameStatistics(&frameStatistics) == Result::Success)
    {
        ...
    }
}

Графические API

В настоящее время можно выбрать три графических API: OpenXrD3D11, WmrD3D11 и SimD3D11. Четвертый API Headless существует, но еще не поддерживается на стороне клиента.

OpenXR

GraphicsApiType.OpenXrD3D11 — привязка по умолчанию для запуска в HoloLens 2. Создается привязка GraphicsBindingOpenXrD3d11. В этом режиме Удаленная отрисовка Azure создает уровень API OpenXR, чтобы интегрировать себя в среду выполнения OpenXR.

Для доступа к производным графическим привязкам требуется привести базовый GraphicsBinding. Для использования привязки OpenXR необходимо выполнить три задачи:

Пользовательский JSON-слой OpenXR пакета

Чтобы использовать Удаленную отрисовку с OpenXR, необходимо активировать пользовательский уровень API OpenXR. Это выполняется путем вызова StartupRemoteRendering, упомянутого в предыдущем разделе. Однако необходимо упаковать XrApiLayer_msft_holographic_remoting.json с помощью приложения, чтобы его можно было загрузить. Это выполняется автоматически, если пакет NuGet "Microsoft.Azure.RemoteRendering.Cpp" добавляется в проект.

Указание Удаленной отрисовки используемого пространства XR

Это необходимо для согласования удаленно и локально отрисованного содержимого.

RenderingSession currentSession = ...;
ulong space = ...; // XrSpace cast to ulong
GraphicsBindingOpenXrD3d11 openXrBinding = (currentSession.GraphicsBinding as GraphicsBindingOpenXrD3d11);
if (openXrBinding.UpdateAppSpace(space) == Result.Success)
{
    ...
}
ApiHandle<RenderingSession> currentSession = ...;
XrSpace space = ...;
ApiHandle<GraphicsBindingOpenXrD3d11> openXrBinding = currentSession->GetGraphicsBinding().as<GraphicsBindingOpenXrD3d11>();
#ifdef _M_ARM64
    if (openXrBinding->UpdateAppSpace(reinterpret_cast<uint64_t>(space)) == Result::Success)
#else
    if (openXrBinding->UpdateAppSpace(space) == Result::Success)
#endif
{
    ...
}

В примере выше XrSpace используется приложением, определяющим систему координат в абсолютном пространстве, в которой выражаются координаты в API.

Преобразование для просмотра удаленного изображения (OpenXR)

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

Предупреждение

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

RenderingSession currentSession = ...;
GraphicsBindingOpenXrD3d11 openXrBinding = (currentSession.GraphicsBinding as GraphicsBindingOpenXrD3d11);
openXrBinding.BlitRemoteFrame();
ApiHandle<RenderingSession> currentSession = ...;
ApiHandle<GraphicsBindingOpenXrD3d11> openXrBinding = currentSession->GetGraphicsBinding().as<GraphicsBindingOpenXrD3d11>();
openXrBinding->BlitRemoteFrame();

Windows Mixed Reality

GraphicsApiType.WmrD3D11 ранее используемая графическая привязка для запуска на HoloLens 2. Создается привязка GraphicsBindingWmrD3d11. В этом режиме Удаленная отрисовка Azure подключается непосредственно к голографическим API.

Для доступа к производным графическим привязкам требуется привести базовый GraphicsBinding. Для использования привязки WMR необходимо выполнить две задачи.

Указать систему координат для Удаленной отрисовки

Это необходимо для согласования удаленно и локально отрисованного содержимого.

RenderingSession currentSession = ...;
IntPtr ptr = ...; // native pointer to ISpatialCoordinateSystem
GraphicsBindingWmrD3d11 wmrBinding = (currentSession.GraphicsBinding as GraphicsBindingWmrD3d11);
if (wmrBinding.UpdateUserCoordinateSystem(ptr) == Result.Success)
{
    ...
}
ApiHandle<RenderingSession> currentSession = ...;
void* ptr = ...; // native pointer to ISpatialCoordinateSystem
ApiHandle<GraphicsBindingWmrD3d11> wmrBinding = currentSession->GetGraphicsBinding().as<GraphicsBindingWmrD3d11>();
if (wmrBinding->UpdateUserCoordinateSystem(ptr) == Result::Success)
{
    ...
}

В примере выше ptr должен быть указателем на собственный объект ABI::Windows::Perception::Spatial::ISpatialCoordinateSystem, определяющий систему координат в абсолютном пространстве, в которой выражаются координаты в API.

Преобразование для просмотра удаленного изображения (WMR)

Здесь применимы те же рекомендации, что и в вышеупомянутом случае с OpenXR. Вызовы API выглядят следующим образом:

RenderingSession currentSession = ...;
GraphicsBindingWmrD3d11 wmrBinding = (currentSession.GraphicsBinding as GraphicsBindingWmrD3d11);
wmrBinding.BlitRemoteFrame();
ApiHandle<RenderingSession> currentSession = ...;
ApiHandle<GraphicsBindingWmrD3d11> wmrBinding = currentSession->GetGraphicsBinding().as<GraphicsBindingWmrD3d11>();
wmrBinding->BlitRemoteFrame();

Simulation

GraphicsApiType.SimD3D11 — привязка моделирования, и если она выбрана, создается графическая привязка GraphicsBindingSimD3d11. Этот интерфейс используется для моделирования движения головы, например в классическом приложении. Он отрисовывает монокулярное изображение.

Чтобы реализовать привязку моделирования, важно понимать разницу между локальной камерой и удаленным кадром, как описано на странице Камера.

Требуется две камеры.

  • Локальная камера: эта камера представляет текущую точку камеры, управляемую логикой приложения.
  • Прокси-камера: эта камера соответствует текущему удаленному кадру, отправленному сервером. Так как между запросом кадра клиентом и его поступлением есть задержка по времени, удаленный кадр всегда немного отстает от перемещения локальной камеры.

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

GraphicsApiType.SimD3D11 также поддерживает стереоскопическую отрисовку, которую необходимо включить при вызове настройки InitSimulation ниже. Его настройка является более сложной и выполняется следующим образом.

Создание промежуточного целевого объекта отрисовки

Удаленное и локальное содержимое должно быть отрисовано на целевом объекте отрисовки со смещенными значениями цвета и глубины, называемом промежуточным, с использованием данных промежуточной камеры, предоставленных функцией GraphicsBindingSimD3d11.Update.

Прокси должен соответствовать разрешению заднего буфера и иметь формат DXGI_FORMAT_R8G8B8A8_UNORM или DXGI_FORMAT_B8G8R8A8_UNORM. В случае стереоскопической отрисовки цветовая прокси-текстура и, если используется глубина, прокси-текстура глубины должны иметь два слоя массива, а не один. Когда сеанс готов, перед подключением к нему необходимо вызвать GraphicsBindingSimD3d11.InitSimulation.

RenderingSession currentSession = ...;
IntPtr d3dDevice = ...; // native pointer to ID3D11Device
IntPtr color = ...; // native pointer to ID3D11Texture2D
IntPtr depth = ...; // native pointer to ID3D11Texture2D
float refreshRate = 60.0f; // Monitor refresh rate up to 60hz.
bool flipBlitRemoteFrameTextureVertically = false;
bool flipReprojectTextureVertically = false;
bool stereoscopicRendering = false;
GraphicsBindingSimD3d11 simBinding = (currentSession.GraphicsBinding as GraphicsBindingSimD3d11);
simBinding.InitSimulation(d3dDevice, depth, color, refreshRate, flipBlitRemoteFrameTextureVertically, flipReprojectTextureVertically, stereoscopicRendering);
ApiHandle<RenderingSession> currentSession = ...;
void* d3dDevice = ...; // native pointer to ID3D11Device
void* color = ...; // native pointer to ID3D11Texture2D
void* depth = ...; // native pointer to ID3D11Texture2D
float refreshRate = 60.0f; // Monitor refresh rate up to 60hz.
bool flipBlitRemoteFrameTextureVertically = false;
bool flipReprojectTextureVertically = false;
bool stereoscopicRendering = false;
ApiHandle<GraphicsBindingSimD3d11> simBinding = currentSession->GetGraphicsBinding().as<GraphicsBindingSimD3d11>();
simBinding->InitSimulation(d3dDevice, depth, color, refreshRate, flipBlitRemoteFrameTextureVertically, flipReprojectTextureVertically, stereoscopicRendering);

Функции инициализации нужно предоставить указатели на собственное устройство D3D, а также на цвет и глубину текстуры промежуточного целевого объекта отрисовки. После инициализации RenderingSession.ConnectAsync и Disconnect можно вызывать несколько раз, но при переключении на другой сеанс необходимо сначала вызвать GraphicsBindingSimD3d11.DeinitSimulation в старом сеансе, прежде чем можно будет вызвать GraphicsBindingSimD3d11.InitSimulation в другом сеансе.

Обновление цикла отрисовки

Обновление цикла отрисовки состоит из нескольких этапов.

  1. Перед отрисовкой каждого кадра вызывается GraphicsBindingSimD3d11.Update с текущим преобразованием камеры, которое передается на сервер для отрисовки. В то же время возвращенное промежуточное преобразование необходимо применить к промежуточной камере, чтобы выполнить отрисовку на промежуточном целевом объекте отрисовки. Если возвращенное обновление промежуточного преобразования SimulationUpdate.frameId имеет значение NULL, удаленные данные пока отсутствуют. В этом случае вместо отрисовки на промежуточном целевом объекте отрисовки любое локальное содержимое должно быть преобразовано для просмотра в заднем буфере с использованием текущих данных камеры, а следующие два шага должны быть пропущены.
  2. Теперь приложение должно привязать промежуточный целевой объект отрисовки и вызвать GraphicsBindingSimD3d11.BlitRemoteFrameToProxy. Это позволит применить данные удаленного цвета и глубины к промежуточному целевому объекту. Теперь любое локальное содержимое можно будет отобразить на промежуточном целевом объекте, используя преобразование промежуточной камеры.
  3. Затем задний буфер нужно будет привязать в качестве целевого объекта отрисовки и вызвать GraphicsBindingSimD3d11.ReprojectProxy для представления заднего буфера.
RenderingSession currentSession = ...;
GraphicsBindingSimD3d11 simBinding = (currentSession.GraphicsBinding as GraphicsBindingSimD3d11);
SimulationUpdateParameters updateParameters = new SimulationUpdateParameters();
// Fill out camera data with current camera data
// (see "Simulation Update structures" section below)
...
SimulationUpdateResult updateResult = new SimulationUpdateResult();
simBinding.Update(updateParameters, out updateResult);
// Is the frame data valid?
if (updateResult.FrameId != 0)
{
    // Bind proxy render target
    simBinding.BlitRemoteFrameToProxy();
    // Use proxy camera data to render local content
    ...
    // Bind back buffer
    simBinding.ReprojectProxy();
}
else
{
    // Bind back buffer
    // Use current camera data to render local content
    ...
}
ApiHandle<RenderingSession> currentSession;
ApiHandle<GraphicsBindingSimD3d11> simBinding = currentSession->GetGraphicsBinding().as<GraphicsBindingSimD3d11>();

SimulationUpdateParameters updateParameters;
// Fill out camera data with current camera data
// (see "Simulation Update structures" section below)
...
SimulationUpdateResult updateResult;
simBinding->Update(updateParameters, &updateResult);
// Is the frame data valid?
if (updateResult.FrameId != 0)
{
    // Bind proxy render target
    simBinding->BlitRemoteFrameToProxy();
    // Use proxy camera data to render local content
    ...
    // Bind back buffer
    simBinding->ReprojectProxy();
}
else
{
    // Bind back buffer
    // Use current camera data to render local content
    ...
}

Структуры обновления моделирования

Для каждого кадра Обновление цикла отрисовки из предыдущего раздела требует ввода ряда параметров камеры, соответствующих локальной камере, и возвращает набор параметров камеры, соответствующих камере следующего доступного кадра. Эти два набора фиксируются в структурах SimulationUpdateParameters и SimulationUpdateResult соответственно.

public struct SimulationUpdateParameters
{
    public int FrameId;
    public StereoMatrix4x4 ViewTransform;
    public StereoCameraFov FieldOfView;
};

public struct SimulationUpdateResult
{
    public int FrameId;
    public float NearPlaneDistance;
    public float FarPlaneDistance;
    public StereoMatrix4x4 ViewTransform;
    public StereoCameraFov FieldOfView;
};

Смысл элементов структуры следующий.

Элемент Description
FrameId Непрерывный идентификатор кадра. Необходим для входных данных SimulationUpdateParameters и должен постоянно увеличиваться для каждого нового кадра. Будет равен 0 в SimulationUpdateResult, если данные кадра еще не доступны.
ViewTransform Левая пара — правая пара — стереопара матриц преобразования представления камеры кадра. Для монокулярной отрисовки допустим только элемент Left.
FieldOfView Левая пара — правая пара — стереопара полей зрения камеры кадра в поле соглашения о представлении OpenXR. Для монокулярной отрисовки допустим только элемент Left.
NearPlaneDistance Расстояние до близкой плоскости, используемое для матрицы проекции текущего удаленного кадра.
FarPlaneDistance Расстояние до дальней плоскости, используемое для матрицы проекции текущего удаленного кадра.

Стереопары ViewTransform и FieldOfView позволяют задать оба значения камеры глаза в случае, если включена стереоскопическая отрисовка. В противном случае элементы Right будут игнорироваться. Как видите, только преобразование камеры передается как обычные матрицы преобразования 4 x 4, а матрицы проекции не указаны. Фактические матрицы вычисляются внутренне в Удаленной отрисовке Azure с использованием указанных полей представления, а также текущего набора ближних и дальних плоскостей в API CameraSettings.

Так как вы можете изменять ближнюю и дальнюю плоскость в CameraSettings по желанию во время выполнения и служба применяет эти параметры асинхронно, каждый SimulationUpdateResult также включает конкретную ближнюю и дальнюю плоскость, используемые во время отрисовки соответствующего кадра. Эти значения плоскости можно использовать для адаптации матриц проекции для визуализации локальных объектов для соответствия отрисовке удаленных кадров.

Наконец, хотя для вызова обновления моделирования требуется поле зрения в соглашении OpenXR, для стандартизации и по соображениям безопасности алгоритма можно использовать функции преобразования, показанные в следующих примерах заполнения структуры.

public SimulationUpdateParameters CreateSimulationUpdateParameters(int frameId, Matrix4x4 viewTransform, Matrix4x4 projectionMatrix)
{
    SimulationUpdateParameters parameters = default;
    parameters.FrameId = frameId;
    parameters.ViewTransform.Left = viewTransform;
    if (parameters.FieldOfView.Left.FromProjectionMatrix(projectionMatrix) != Result.Success)
    {
        // Invalid projection matrix
        throw new ArgumentException("Invalid projection settings");
    }
    return parameters;
}

public void GetCameraSettingsFromSimulationUpdateResult(SimulationUpdateResult result, out Matrix4x4 projectionMatrix, out Matrix4x4 viewTransform, out int frameId)
{
    projectionMatrix = default;
    viewTransform = default;
    frameId = 0;

    if (result.FrameId == 0)
    {
        // Invalid frame data
        return;
    }

    // Use the screenspace depth convention you expect for your projection matrix locally
    if (result.FieldOfView.Left.ToProjectionMatrix(result.NearPlaneDistance, result.FarPlaneDistance, DepthConvention.ZeroToOne, out projectionMatrix) != Result.Success)
    {
        // Invalid field-of-view
        return;
    }
    viewTransform = result.ViewTransform.Left;
    frameId = result.FrameId;
}
SimulationUpdateParameters CreateSimulationUpdateParameters(uint32_t frameId, Matrix4x4 viewTransform, Matrix4x4 projectionMatrix)
{
    SimulationUpdateParameters parameters;
    parameters.FrameId = frameId;
    parameters.ViewTransform.Left = viewTransform;
    if (FovFromProjectionMatrix(projectionMatrix, parameters.FieldOfView.Left) != Result::Success)
    {
        // Invalid projection matrix
        return {};
    }
    return parameters;
}

void GetCameraSettingsFromSimulationUpdateResult(const SimulationUpdateResult& result, Matrix4x4& projectionMatrix, Matrix4x4& viewTransform, uint32_t& frameId)
{
    if (result.FrameId == 0)
    {
        // Invalid frame data
        return;
    }

    // Use the screenspace depth convention you expect for your projection matrix locally
    if (FovToProjectionMatrix(result.FieldOfView.Left, result.NearPlaneDistance, result.FarPlaneDistance, DepthConvention::ZeroToOne, projectionMatrix) != Result::Success)
    {
        // Invalid field-of-view
        return;
    }
    viewTransform = result.ViewTransform.Left;
    frameId = result.FrameId;
}

Эти функции преобразования позволяют быстро переключаться между спецификацией "поле зрения" и простой матрицей проекции перспективы 4 x 4 в зависимости от потребностей локальной отрисовки. Эти функции преобразования содержат логику проверки и возвращают ошибки, не устанавливая допустимый результат, если входные матрицы проекции или поля зрения являются недопустимыми.

Документация по API

Следующие шаги