avatar

Text03's Blog

如果生活把你推到了板边, 记得升龙 →↓↘+👊

  • 首页
  • 链接
  • 关于
主页 OpenVR Driver API 中文文档
文章

OpenVR Driver API 中文文档

发表于 昨天 更新于 昨天
作者 text03
854~1098 分钟 阅读

OpenVR 驱动程序文档

  • OpenVR 驱动程序文档
    • 关于
    • 概述与要点
      • 使用由运行时实现的接口
      • 导入接口和函数
    • 驱动程序文件夹结构
    • 驱动程序文件
      • driver.vrdrivermanifest
      • resources/driver.vrresources
        • 示例
      • localization/localization.json
        • 在单个文件中定义本地化
        • 在多个文件中定义本地化
      • resources/settings/default.vrsettings
        • 示例
    • 图标
      • 设置图标
      • 更新图标
    • 设备类别
      • 控制器角色
      • 追踪器(全身追踪)
  • 驱动程序结构
    • HmdDriverFactory
    • IServerTrackedDeviceProvider
      • 示例
    • ITrackedDeviceServerDriver
      • 设备组件
        • IVRDisplayComponent
        • IVRDriverDirectModeComponent
        • IVRVirtualDisplay
    • IVRServerDriverHost
    • IVRDriverLog
    • IVRWatchdogProvider
      • 看门狗接口已弃用,新项目不应再使用。
      • IVRWatchdogHost
    • 设备输入
      • 输入配置文件
        • 输入源
      • 输入配置文件 JSON
        • 保留的输入源
    • 绑定配置文件
      • 默认绑定
    • 驱动程序输入
      • 创建组件
      • 更新组件
    • 事件
      • 触觉事件
    • 设备属性
      • 实现属性
      • IVRProperties
      • CVRPropertyHelpers
        • 获取属性
        • 设置属性
        • 属性实用工具
    • IVRSettings
    • IVRResources
    • IVRDriverSpatialAnchors
    • 位姿
    • 骨骼输入
      • 关于手部追踪兼容性的说明
      • 骨骼
      • 单位与坐标系
      • 骨骼结构
        • 关于骨骼的说明
      • 使用骨骼输入
        • 创建手部动画
    • 应用程序兼容性
      • 自动重新绑定
        • 文件结构
        • 一对多重映射
        • 示例
      • 在绑定中模拟设备
        • SteamVR 输入与旧版输入
        • OpenXR
      • 旧版绑定模拟
      • 绑定复制
      • 可模拟的设备
    • 渲染模型
    • 安全区
    • 构建与开发环境
      • 使用 Visual Studio 调试 SteamVR
  • 更多示例

关于

OpenVR 提供了一个驱动程序接口,用于硬件与 SteamVR 和 OpenVR 支持的应用程序交互。
该接口是设备无关的:应用程序无需为您的硬件提供明确支持,只要您遵循 OpenVR 驱动程序 API,您的设备就能在 SteamVR 游戏中得到支持。

需要注意的是,OpenVR 不提供其声明的函数的定义。这些函数是在 SteamVR 或任何其他符合 OpenVR 标准的运行时中实现的。

SteamVR 向后兼容先前版本的 OpenVR。如果 OpenVR 接口被更新,驱动程序或应用程序开发人员无需更新其软件,因为它保证会继续受支持。

本文档概述了设置驱动程序时常见的接口和用例。

概述与要点

OpenVR Driver API 中的可用接口可以在 openvr/headers/openvr_driver.h 中找到。

示例驱动程序可以在 samples/drivers 中找到。

使用由运行时实现的接口

OpenVR Driver API 中的一些接口已经由运行时实现。您绝不能自己实现这些接口,但您可以调用它们包含的方法。这些接口包括:

  • IVRSettings
  • IVRProperties
  • CVRPropertyHelpers
  • CVRHiddenAreaHelpers
  • IVRDriverLog
  • IVRServerDriverHost
  • IVRWatchdogHost
  • IVRCompositorDriverHost
  • DriverHandle_t
  • IVRDriverManager
  • IVRResources
  • IVRDriverInput
  • IVRIOBuffer
  • IVRDriverSpatialAnchors

OpenVR Driver API 提供了实用函数来检索指向这些对象的指针。这些函数与接口同名,但没有 I 前缀。例如:vr::VRServerDriverHost() 返回一个指向运行时实现的 IVRServerDriverHost 的有效指针。

IVRDriverLog* log = vr::VRDriverLog();
IVRSettings* settings = vr::VRSettings();

// 其中一些方法返回指向包装原始类的帮助器类的指针,以简化使用
CVRPropertyHelpers* properties = vr::VRProperties();

导入接口和函数

本节简要介绍您将为驱动程序编写的代码大致是什么样的。
本节不提供接口的文档,而是简要概述您将编写的代码的样子。

流程通常如下所示。每个类和方法的文档在其他地方概述。

graph TD;
    A[HmdDriverFactory]-->B[IServerTrackedDeviceProvider];
    B-->C["IServerTrackedDeviceProvider::Init"];
    B-->D["IServerTrackedDeviceProvider::RunFrame"];

    C--"VRServerDriverHost()->TrackedDeviceAdded(...)"-->E["ITrackedDeviceServerDriver::Activate"];
    D-->G["VRServerDriverHost()->PollNextEvent(...)"];

    E-->H["VRProperties()->SetXProperty(...)"];
    E-->I["VRDriverInput()->CreateXComponent(...)"];

    C-->M["MyDeviceDriver::MyInputThread"];
    M-->N["VRDriverInput()->UpdateXComponent(...)"];

    A-->K[IVRWatchdogProvider];
    K-->L["VRWatchdogHost()->WatchdogWakeUp(...)"];

驱动程序文件夹结构

<driver_name>
├── bin                                          # 包含所有二进制文件
│   ├── win64
│   │   └── driver_<driver_name>.dll             # 驱动程序二进制文件。名称必须遵循 driver_<driver_name>.dll 模式才能被加载
│   └── ...
├── resources                                    # 包含所有资源文件
│   ├── icons
│   │   ├── <my_device_status>.png               # 设备状态和绑定图标。
│   │   └── ...
│   ├── input
│   │   ├── <my_controller>_profile.json         # 设备的输入配置文件
│   │   └── legacy_binding_<my_controller>.json  # 设备的旧版绑定文件
│   ├── localization
│   │   ├── localization.json                    # 本地化字符串,将出现在绑定配置界面等地方。
│   │   └── locatization_<lang>.json             # 可选:在不同文件中指定本地化语言
│   ├── settings
│   │   └── default.vrsettings                   # 默认驱动程序设置
│   └── driver.vrresources                       # 定义驱动程序设备所需的资源,例如图标
└── driver.vrdrivermanifest                      # 清单文件,用于标识驱动程序二进制文件的位置(相对于清单文件)以及驱动程序的其他几个属性。

<driver_name> 应该是一个不带空格的、小写的字符串,给出驱动程序的名称。

<driver_name> 必须在驱动程序二进制文件的名称和根文件夹名称之间保持一致。

  • 命名不一致将导致 SteamVR 在尝试加载驱动程序时返回 VRInitError_Init_FileNotFound。

驱动程序文件

SteamVR 在许多配置文件中使用 JSON 格式。包含无效 JSON(例如尾随逗号)的文件将导致运行时无法加载文件。

driver.vrdrivermanifest

driver.vrdrivermanifest 是一个必需文件,位于驱动程序文件夹的根目录。

它包含与驱动程序属性相关的信息。

  • name - 驱动程序的全局唯一名称。名称必须与根文件夹名称(包含 driver.vrdrivermanifest 的文件夹)匹配。驱动程序 DLL(s) 必须也命名为 driver_<name>.dll(或其他平台的等效扩展名)。
  • directory - 包含其余驱动程序文件的目录的名称。如果是相对路径,则相对于包含 driver.vrdrivermanifest 的目录。默认为包含 driver.vrdrivermanifest 的完整路径。
  • alwaysActivate - 如果为 true,即使活动 HMD 来自另一个驱动程序,此驱动程序也会被激活。
    默认为 false。
    • 仅暴露控制器的驱动程序,如果已知 HMD 可能来自不同的追踪系统,应该将此设置为 true。
  • resourceOnly - 驱动程序包含 resources 目录,但不包含任何二进制文件。这用于灯塔追踪设备,它们通过 JSON 文件定义驱动程序行为和输入。有关信息,请参见 JSON 文件。
  • hmd_presence - 这是一个字符串数组,用于标识指示此驱动程序 HMD 可能存在的 USB VID 和 PID 组合。每个条目应该是这种形式的十六进制值:
    • 28DE.* - 任何 VID 为 28DE 的 USB 设备都将导致 OpenVR 从 VR_IsHmdPresent 返回 true。
    • 28DE.2000 - 任何 VID 为 28DE 且 PID 为 2000 的 USB 设备都将导致 OpenVR 从 VR_IsHmdPresent 返回 true。
    • *.* - 如果此驱动程序已安装,运行时将始终从 VR_IsHmdPresent 返回 true。
  • other_presence - 与 hmd_presence 相同格式的字符串数组,指示存在非 HMD 设备已插入。
  • redirectsDisplay - 设备是否实现了 vr::TrackedDeviceClass_DisplayRedirect 设备。
    更多细节请参见 IVRVirtualDisplay。
  • prefersUpperDeviceIndices - 设备是否应以较高的索引激活。这将使其在设置活动角色时的优先级较低。
    默认为 false。
  • spatialAnchorsSupport - 设备是否支持空间锚点。默认为 false。
    没有此设置,SteamVR 将短路来自应用程序的锚点请求,并提供一个不具备真正空间锚点优势的通用描述符。

示例 driver.vrdrivermanifest 如下所示:

{
  "alwaysActivate": false,
  "name": "barebones",
  "directory": "",
  "resourceOnly": false,
  "hmd_presence": [
    "*.*"
  ]
}

完整示例请参见 samples/drivers/barebones。

resources/driver.vrresources

driver.vrresources 文件是一个可选文件,位于 resources/ 文件夹中。

它包含与驱动程序外部资源相关的信息,例如图标。
这些图标在 SteamVR 运行时显示在 SteamVR 监视器窗口中,并传达设备当前状态的图标表示。

有关这些图标应如何格式化的信息,请参见 图标。

driver.vrresources 包含以下键:

  • jsonid - 必需。此值必须为 vrresources。
  • statusicons - 可选。用于为特定设备类型定义图标
    • <model_number> - 您要为其定义图标的设备的 Prop_ModelNumber_String。
      请参见设备属性来设置此属性。
      • <icon_name> - 要提供路径的图标名称。图标名称的可能键列表在设置图标中列出。
        • <icon_name> 的值表示图像文件的路径。文件路径可以使用 {<driver_name>} 通配符格式来匹配驱动程序的根路径。例如:{my_driver}/resources/icons/my_image.png。
        • 有关图标应如何格式化的更多信息,请参见图标。
        • 如果未指定键,将使用 {system}/icons/invalid_status.png 图标。
      • Alias - 一个保留键,用于链接到不同型号编号的图标部分(参见示例)。
        • 在原始部分中未找到的图标名称键将追踪到 Alias 键值指定的部分。
        • 这对于某些图像可能相同、但其他图像不同的不同型号编号很有用,这允许驱动程序通过提供一个"通用"图标部分来避免重复。

如果未匹配到 Prop_ModelNumber_String,则回退考虑 statusicons 的以下子键:

  • HMD - 考虑用于类别为 TrackedDeviceClass_HMD 的驱动程序设备。更多信息请参见设备类别。
  • <Left/Right>Controller - 考虑用于类别为 TrackedDeviceClass_Controller 的驱动程序设备。
    更多信息请参见设备类别。
    • 如果省略 <Left/Right>,则考虑回退到 Controller。然而,这意味着用户将无法识别哪个图标代表左/右手。
  • GenericTracker - 考虑用于类别为 TrackedDeviceClass_GenericTracker 的驱动程序设备。
    更多信息请参见设备类别。
  • TrackingReference - 考虑用于类别为 TrackedDeviceClass_TrackingReference 的驱动程序设备。
    更多信息请参见设备类别。

如果这些也未匹配,运行时将回退到 system.vrresources 中这些相同键下指定的通用图标。

示例

{
  "jsonid": "vrresources",
  "statusicons": {
    "HMD": {
      "Prop_NamedIconPathDeviceOff_String": "{sample}/icons/headset_sample_status_off.png",
      "Prop_NamedIconPathDeviceSearching_String": "{sample}/icons/headset_sample_status_searching.gif",
      "Prop_NamedIconPathDeviceSearchingAlert_String": "{sample}/icons/headset_sample_status_searching_alert.gif",
      "Prop_NamedIconPathDeviceReady_String": "{sample}/icons/headset_sample_status_ready.png",
      "Prop_NamedIconPathDeviceReadyAlert_String": "{sample}/icons/headset_sample_status_ready_alert.png",
      "Prop_NamedIconPathDeviceNotReady_String": "{sample}/icons/headset_sample_status_error.png",
      "Prop_NamedIconPathDeviceStandby_String": "{sample}/icons/headset_sample_status_standby.png",
      "Prop_NamedIconPathDeviceAlertLow_String": "{sample}/icons/headset_sample_status_ready_low.png"
    },
    "Model-v Defaults": {
      "Prop_NamedIconPathDeviceOff_String": "{sample}/icons/headset_sample_status_off.png",
      "Prop_NamedIconPathDeviceSearching_String": "Prop_NamedIconPathDeviceOff_String",
      "Prop_NamedIconPathDeviceSearchingAlert_String": "Prop_NamedIconPathDeviceOff_String",
      "Prop_NamedIconPathDeviceReady_String": "Prop_NamedIconPathDeviceOff_String",
      "Prop_NamedIconPathDeviceReadyAlert_String": "Prop_NamedIconPathDeviceOff_String",
      "Prop_NamedIconPathDeviceNotReady_String": "Prop_NamedIconPathDeviceOff_String",
      "Prop_NamedIconPathDeviceStandby_String": "Prop_NamedIconPathDeviceOff_String",
      "Prop_NamedIconPathDeviceAlertLow_String": "Prop_NamedIconPathDeviceOff_String"
    },
    "Model-v1.0": {
      "Alias": "Model-v Defaults",
      "Prop_NamedIconPathDeviceAlertLow_String": "{sample}/icons/headset_model1_alertlow.png"
    },
    "Model-v2.0": {
      "Alias": "Model-v1.0",
      "Prop_NamedIconPathDeviceAlertLow_String": "{sample}/icons/headset_model2_alertlow.png"
    },
    "LeftController": {
      "Prop_NamedIconPathDeviceOff_String": "{sample}/icons/controller_status_off.png",
      "Prop_NamedIconPathDeviceSearching_String": "{sample}/icons/controller_status_searching.gif",
      "Prop_NamedIconPathDeviceSearchingAlert_String": "{sample}/icons/controller_status_searching_alert.gif",
      "Prop_NamedIconPathDeviceReady_String": "{sample}/icons/controller_status_ready.png",
      "Prop_NamedIconPathDeviceReadyAlert_String": "{sample}/icons/controller_status_ready_alert.png",
      "Prop_NamedIconPathDeviceNotReady_String": "{sample}/icons/controller_status_error.png",
      "Prop_NamedIconPathDeviceStandby_String": "{sample}/icons/controller_status_standby.png",
      "Prop_NamedIconPathDeviceAlertLow_String": "{sample}/icons/controller_status_ready_low.png"
    },
    "RightController": {
      "Prop_NamedIconPathDeviceOff_String": "{sample}/icons/controller_status_off.png",
      "Prop_NamedIconPathDeviceSearching_String": "{sample}/icons/controller_status_searching.gif",
      "Prop_NamedIconPathDeviceSearchingAlert_String": "{sample}/icons/controller_status_searching_alert.gif",
      "Prop_NamedIconPathDeviceReady_String": "{sample}/icons/controller_status_ready.png",
      "Prop_NamedIconPathDeviceReadyAlert_String": "{sample}/icons/controller_status_ready_alert.png",
      "Prop_NamedIconPathDeviceNotReady_String": "{sample}/icons/controller_status_error.png",
      "Prop_NamedIconPathDeviceStandby_String": "{sample}/icons/controller_status_standby.png",
      "Prop_NamedIconPathDeviceAlertLow_String": "{sample}/icons/controller_status_ready_low.png"
    }
  }
}

localization/localization.json

本地化 JSON 文件指定设备的本地化信息。

驱动程序可以选择为每个本地化版本创建新文件,或者将它们全部定义在 localization/localization.json 中。

此文件还用于提供设备的用户友好名称。例如,Index 控制器的 Prop_RegisteredDeviceType_String 是 knuckles。然后在英语中本地化为 Index Controller。

键可以为驱动程序通过其可见属性导出的任何内容定义本地化版本(例如 Prop_RegisteredDeviceType_String)。

在单个文件中定义本地化

localization/localization.json 必须包含一个 JSON 格式的数组,其中包含每个本地化版本的对象。此对象必须包含:

  • language_tag: 一个可识别的语言标签,例如 en_US。

以及驱动程序希望本地化的驱动程序导出的任何字符串。

[
  {
    "language_tag": "en_US",
    "knuckles": "Index Controller",
    "knuckles_ev1": "Knuckles (EV1) Controller",
    "/input/a": "A Button",
    "/input/b": "B Button",
    "/input/pinch": "Pinch Gesture",
    "/input/finger/index": "Index Finger",
    "/input/finger/middle": "Middle Finger",
    "/input/finger/ring": "Ring Finger",
    "/input/finger/pinky": "Pinky Finger",
    "/input/thumbstick": "Thumb Stick"
  }
]

在多个文件中定义本地化

localization/localization.json 必须包含一个 JSON 格式的对象,其中包含每个本地化版本的语言标签键。

{
  "en_US": "{<driver_name>}/localization/localization_en_us.json"
}

其中 <driver_name> 是驱动程序的名称。然后这会扩展为运行时可以加载的绝对路径。

链接到的每个单独文件必须是 JSON 格式的对象。
此对象必须包含一个键:

  • language_tag: 一个可识别的语言标签,例如 en_us
{
  "language_tag": "en_US",
  "knuckles": "Index Controller",
  "knuckles_ev1": "Knuckles (EV1) Controller",
  "/input/a": "A Button",
  "/input/b": "B Button",
  "/input/pinch": "Pinch Gesture",
  "/input/finger/index": "Index Finger",
  "/input/finger/middle": "Middle Finger",
  "/input/finger/ring": "Ring Finger",
  "/input/finger/pinky": "Pinky Finger",
  "/input/thumbstick": "Thumb Stick"
}

resources/settings/default.vrsettings

default.vrsettings 文件提供驱动程序的默认设置值。文件不应被用户修改,只应提供默认的驱动程序设置值,然后可以使用 IVRSettings 接口进行配置。IVRSettings 将更新的设置值写入 steamvr.vrsettings。

驱动程序应该提供一个 default.vrsettings 文件随驱动程序一起发布,该文件必须位于 <driver_name>/resources/settings/default.vrsettings。此文件提供一组默认设置,如果在 steamvr.vrsettings 中找不到设置,将使用这些默认设置。

设置分组在驱动程序定义的节中。这些节包含键和关联的设置值。值可以是字符串(最多 4096 个字符)、浮点数、整数或布尔值。

驱动程序应该在选择节名时小心,因为它们是全局的,并且不限于特定驱动程序。驱动程序应该在其节名前加上其名称以避免冲突。

驱动程序应该至少指定一个节(文件根部的 JSON 对象),键名为 driver_<my_driver_name>。

虽然驱动程序可以在此节中添加任何自定义键,但有一些键是保留给运行时使用的:

  • enable - 一个布尔值,决定驱动程序是否启用。
    • 这是一个特殊值,用户可以通过 SteamVR 设置 UI(SteamVR 设置 > 启动/关闭 > 管理附加组件)修改。
    • 如果未指定,则默认为 true。
  • loadPriority - 一个整数值,决定加载驱动程序的顺序。
    • 优先级顺序是降序(具有较高整数 loadPriority 值的驱动程序将首先加载)。
    • 如果未指定,则默认为 0。
    • 所有随 SteamVR 一起发布的驱动程序的 loadPriority 都为 0。
  • blocked_by_safe_mode - 一个布尔值,决定当 SteamVR 处于安全模式时是否阻止加载驱动程序。
    • 如果未指定,则默认为 false。对于第三方驱动程序,这应该设置为 false(这样驱动程序就会被安全模式阻止)。将此值设置为 true 意味着如果驱动程序在启动时崩溃,用户将无法使用 SteamVR,除非卸载驱动程序。

示例

{
  "driver_mydriver": {
    "enable": true,
    "loadPriority": 100,
    "blocked_by_safe_mode": false,
    "my_custom_setting": "my_custom_value"
  },
  "driver_mydriver_customsettings": {
    "my_custom_setting2": "my_custom_value2"
  }
}

图标

如果要在 SteamVR 中显示图标,它必须是以下尺寸之一,具体取决于设备类型:

  • HMD:50x32 或 100x64(见下文)png/gif
  • 其他:32x32 或 64x64(见下文)png/gif

在文件名末尾附加 @2x 以使用 100x64 HMD 图标或 64x64 设备图标。
否则,将使用 50x32 或 32x32 图标尺寸。

例如:

  • my_hmd_icon.png - 必须使用 50x32 图标。
  • my_hmd_icon@2x.png - 必须使用 100x64 图标。

不支持尺寸的图标将从左上角裁剪。

图标可以选择使用 gif 格式进行动画。例如,追踪控制器在开启但未建立追踪时通常有一个呼吸动画。

图标必须在整个图像上具有蓝绿色渐变。图标将被适当地格式化以包含此渐变。这些渐变由运行时生成,并放置在图标所在的同一文件夹中,文件名后附加 .b4bfb144。

设置图标

要设置图标,它们必须设置为设备的属性。这可以在 driver.vrdrivermanifest 文件中完成,也可以通过编程方式设置属性来完成。
请参见设备属性。

可以设置的图标列表如下:

  • Prop_NamedIconPathDeviceOff_String - 设备已关闭。
  • Prop_NamedIconPathDeviceSearching_String - 设备已开启,但尚未建立追踪。
  • Prop_NamedIconPathDeviceSearchingAlert_String - 设备已开启,尚未建立追踪,并收到警告。
  • Prop_NamedIconPathDeviceReady_String - 设备已开启,正在追踪且正常工作。
  • Prop_NamedIconPathDeviceReadyAlert_String - 设备已开启并准备使用,但需要通知用户某事(例如固件更新)。
  • Prop_NamedIconPathDeviceNotReady_String - 设备已开启,但尚未准备好启动和建立追踪。
  • Prop_NamedIconPathDeviceStandby_String - 由于系统不活动,设备已进入睡眠状态。
  • Prop_NamedIconPathDeviceStandbyAlert_String - 设备处于待机状态,但需要通知用户某事(例如固件更新)。
  • Prop_NamedIconPathDeviceAlertLow_String - 设备正在工作,但电池电量低。当 Prop_DeviceBatteryPercentage_Float 小于 15% 时触发。请参见设备属性。
  • Prop_NamedIconPathTrackingReferenceDeviceOff_String - 追踪参考设备(即基站)尚未检测到或已关闭。
  • Prop_NamedIconPathControllerLeftDeviceOff_String - 左手控制器已关闭。
  • Prop_NamedIconPathControllerRightDeviceOff_String - 右手控制器已关闭。

如果键不匹配,将使用 {system}/icons/invalid_status.png 图标。

更新图标

当前用于表示设备的图标可以通过两种方式更新:运行时在设备(提交位姿)时更新,或驱动程序发送事件来更新图标。

在位姿中,通过设置以下成员的组合:

ETrackingResult result
bool poseIsValid
bool deviceIsConnected

将更新图标以表示设备的当前状态:

  • Prop_NamedIconPathDeviceOff_String
    • deviceIsConnected 为 false。
  • Prop_NamedIconPathDeviceSearching_String
    • deviceIsConnected 为 true。
    • poseIsValid 为 false。
    • 设备先前已连接,但随后断开连接。
  • Prop_NamedIconPathDeviceReady_String
    • deviceIsConnected 为 true。
    • poseIsValid 为 true。
  • Prop_NamedIconPathDeviceStandby_String
    • 设备位姿的方向保持接近恒定。
    • /proximity 布尔输入路径为 false。要使用此功能,必须将 Prop_ContainsProximitySensor_Bool 设置为 true。
  • Prop_NamedIconPathDeviceAlertLow_String
    • deviceIsConnected 为 true。
    • poseIsValid 为 true。
    • result 为 TrackingResult_Running_OK。
    • Prop_DeviceBatteryPercentage_Float 小于 0.15。

设备类别

OpenVR 中的设备必须具有与之关联的类别。设备的类别定义了暴露给运行时的设备类型。可用设备类别在 ETrackedDeviceClass 枚举中定义,设备的类别通过 IVRServerDriverHost::TrackedDeviceAdded 提供给运行时。

  • TrackedDeviceClass_HMD - 头戴式显示器。例如 Index HMD。
  • TrackedDeviceClass_Controller - 追踪控制器。例如 Index 控制器。
  • TrackedDeviceClass_GenericTracker - 通用追踪器,类似于控制器。例如 Vive 追踪器。SteamVR 中会显示一个额外的管理追踪器界面,供用户配置此追踪器的使用方式和位置。
    注意:追踪器角色不能由驱动程序配置。
  • TrackedDeviceClass_TrackingReference - 作为追踪参考点的相机和基站。例如基站 2.0。
  • TrackedDeviceClass_DisplayRedirect - 本身不一定被追踪,但可能重定向来自其他追踪设备视频输出的附件。

控制器角色

TrackedDeviceClass_Controller 类别的设备必须具有与之关联的角色。角色标识控制器如何被用户使用以及设备在运行时的行为方式。角色在 ETrackedControllerRole 枚举中定义。

  • TrackedControllerRole_LeftHand - 与左手关联的追踪设备。
  • TrackedControllerRole_RightHand - 与右手关联的追踪设备。
  • TrackedControllerRole_OptOut - 追踪设备选择退出左/右手选择。
  • TrackedControllerRole_Treadmill - 追踪设备是跑步机或其他与手持控制器结合使用的运动设备。
  • TrackedControllerRole_Stylus - 追踪设备是触控笔

在任何给定时间,只会有一个活动控制器与 TrackedControllerRole_LeftHand、TrackedControllerRole_RightHand 和 TrackedControllerRole_Treadmill 角色关联。

应用程序不会同时从分配给相同 TrackedControllerRole_LeftHand、TrackedControllerRole_RightHand 和 TrackedControllerRole_Treadmill 角色的多个控制器接收输入。

每个角色的活动设备由运行时决定,但可以通过设备设置 Prop_ControllerHandSelectionPriority_Int32 来影响。

TrackedControllerRole_Treadmill 可以与 TrackedControllerRole_LeftHand 和 TrackedControllerRole_RightHand 同时使用。会向用户显示额外的 UI 来配置 TrackedControllerRole_Treadmill 绑定。

如果 TrackedControllerRole_Treadmill 的输入和手持控制器的输入被分配给应用程序中的相同动作,SteamVR 将使用绝对值最大的输入。

追踪器(全身追踪)

驱动程序可以暴露类别为 TrackedDeviceClass_GenericTracker 的设备来向 SteamVR 暴露追踪器。这些追踪器随后可以被用户用于诸如全身追踪、物体追踪等功能。

用户可以在 SteamVR 的"管理追踪器"部分设置追踪器角色。

追踪器角色存储在 steamvr.vrsettings 中,位于 vr::k_pch_Trackers_Section (“trackers”) 节下。节的每个键的格式为:/devices/<driver_name>/<device_serial_number>,其字符串值为以下追踪器角色之一。

在大多数情况下,SteamVR 期望用户为各个追踪器分配角色,因此 SteamVR 在设置中提供了"管理追踪器"UI。
但是,驱动程序可以使用 IVRSettings 接口写入 steamvr.vrsettings 文件中的 trackers 节,作为修改每个追踪器角色的方式。

用户可以设置的可用追踪器角色列表:

  • TrackerRole_Handed
  • TrackerRole_LeftFoot
  • TrackerRole_RightFoot
  • TrackerRole_LeftShoulder
  • TrackerRole_RightShoulder
  • TrackerRole_LeftElbow
  • TrackerRole_RightElbow
  • TrackerRole_LeftKnee
  • TrackerRole_RightKnee
  • TrackerRole_Waist
  • TrackerRole_Chest
  • TrackerRole_Camera
  • TrackerRole_Keyboard

追踪器可以具有输入配置文件,根据追踪器的当前角色设置。
有关 tracker_types 节的更多信息,请参见输入配置文件。

驱动程序结构

运行时期望驱动程序代码被编译成共享库(.dll 或 .so)。

将驱动程序编译为共享库意味着它可以在运行时加载到 SteamVR 中,而不是在编译时,这意味着 SteamVR 无需显式支持每个与之交互的驱动程序,并且可以动态加载驱动程序。

生成的驱动程序共享库必须将 openvr_api 静态库编译到其中。

  • 静态库可以在 openvr/lib 中找到
    • 使用适用于您目标平台和架构的正确版本。

加载驱动程序涉及将运行时指向存储驱动程序二进制文件的特定文件夹。

您的驱动程序二进制文件必须位于 <my_driver_name>/bin/<platform><arch>/driver_<my_driver_name>.dll/so。

驱动程序绝不能在单个二进制文件中同时使用 openvr.h 和 openvr_driver.h。这样做将导致运行时崩溃。

OpenVR 接口在 API 中定义为具有纯虚方法的抽象类。这意味着,如果驱动程序希望实现特定接口,它们必须公开继承该接口,然后为该接口中声明的每个方法提供定义。

下一节记录 OpenVR API 的接口和函数。

HmdDriverFactory

驱动程序作为共享库(.dll 或 .so)加载到运行时中,并且必须从共享库导出 HmdDriverFactory 函数。此函数成为每个驱动程序的入口点。

extern "C" __declspec( dllexport )
void *HmdDriverFactory( const char *pInterfaceName, int *pReturnCode );

如果找不到 HmdDriverFactory,运行时将返回 VRInitError_Init_FactoryNotFound。

运行时会多次调用此函数,以查找驱动程序支持哪些接口和接口版本。
驱动程序必须仅在 pInterfaceName 与实现的接口及其版本匹配时返回指向其接口实现的指针。

Ixxxxx_Version 是在 OpenVR API 中定义的常量,设置为当前使用的 API 版本中实现的接口的当前版本。它们最终会匹配其中一个 pInterfaceName 调用。

驱动程序可能返回指针的两个常见接口是 IServerTrackedDeviceProvider 和 IVRWatchdogProvider 的实现。

#include <openvr_driver.h>

class MyServerTrackedDeviceProvider : public vr::IServerTrackedDeviceProvider {
    ...
}

class MyWatchdogProvider : public vr::IVRWatchdogProvider {
    ...
}

MyServerTrackedDeviceProvider device_provider;
MyWatchdogProvider watchdog_provider;

extern "C" __declspec( dllexport )
void *HmdDriverFactory( const char *pInterfaceName, int *pReturnCode )
{
	if( 0 == strcmp( IServerTrackedDeviceProvider_Version, pInterfaceName ) )
	{
		return &device_provider; //返回指向您的 vr::IServerTrackedDeviceProvider 对象的指针
	}
	if( 0 == strcmp( IVRWatchdogProvider_Version, pInterfaceName ) )
	{
		return &watchdog_provider; //返回指向您的 vr::IVRWatchdogProvider 对象的指针
	}


	if( pReturnCode )
		*pReturnCode = VRInitError_Init_InterfaceNotFound;

	return NULL;
}

IServerTrackedDeviceProvider

IServerTrackedDeviceProvider 是一个由驱动程序实现的接口,其方法在运行时的状态发生变化时被调用。它必须是一个全局的、单一的实例,并在整个运行时会话期间存在。

IServerTrackedDeviceProvider 可以与 IVRServerDriverHost 交互,这是一个由运行时实现的接口,提供了从运行时检索和更新状态的有用方法。

IServerTrackedDeviceProvider 不实现任何设备本身,但应该初始化一个 ITrackedDeviceServerDriver 用于每个要添加到运行时的设备。

有关如何将设备添加到运行时的信息,请参见 IVRServerDriverHost。


virtual EVRInitError Init( IVRDriverContext *pDriverContext ) = 0;

Init 在调用类的任何其他方法之前被调用。Init 可以选择初始化设备驱动程序,并且如果调用成功,必须返回 VRInitError_None。

返回 VRInitError_None 以外的任何内容将导致驱动程序和共享库从 vrserver.exe 中卸载。

IServerTrackedDeviceProvider 的实现应该在 Init 被调用之前将资源使用保持在最低限度。

  • IVRDriverContext *pDriverContext - 包含指向运行时实现的接口的指针。OpenVR API 中提供的宏 VR_INIT_SERVER_DRIVER_CONTEXT 可以用于初始化此上下文。

IServerTrackedDeviceProvider 的实现绝不能在 Init 被调用且服务器驱动程序上下文已初始化之前调用运行时的任何方法(例如在 IVRServerDriverHost 中)。请参见本节末尾的示例。


virtual void Cleanup() = 0;

Cleanup 在驱动程序卸载之前被调用。

Cleanup 应该释放 IServerTrackedDeviceProvider 或子 ITrackedDeviceServerDriver 在整个会话期间获取的任何资源。


virtual const char * const *GetInterfaceVersions() = 0;

GetInterfaceVersions 必须返回一个字符串数组指针,这些字符串表示此驱动程序中使用的接口版本。

OpenVR API 中的辅助变量 k_InterfaceVersions 应该直接从该方法返回。
此变量包含您针对其编译驱动程序的接口版本。

IVRSettings_Version
ITrackedDeviceServerDriver_Version
IVRDisplayComponent_Version
IVRDriverDirectModeComponent_Version
IVRCameraComponent_Version
IServerTrackedDeviceProvider_Version
IVRWatchdogProvider_Version
IVRVirtualDisplay_Version
IVRDriverManager_Version
IVRResources_Version
IVRCompositorPluginProvider_Version

virtual void RunFrame() = 0;

RunFrame 在 vrserver.exe 的主循环中直接调用。因此,它不应该执行可能阻塞的调用。它在每一帧都会被调用。

强烈建议驱动程序使用自己的线程来执行可能需要定期完成的工作。

驱动程序应该(或许只)使用 RunFrame 轮询事件。请参见事件。


virtual bool ShouldBlockStandbyMode() = 0;

ShouldBlockStandbyMode 已弃用,在较新版本的 SteamVR 中永远不会被调用。驱动程序必须实现此函数(因为它是纯虚函数),但返回值对行为没有影响。


virtual void EnterStandby() = 0;

EnterStandby 在整个系统进入待机模式时被调用。

这是在系统变为不活动状态(HMD 未佩戴、控制器关闭或未使用等)后经过用户配置的时间之后。

驱动程序应该切换到其拥有的任何低功耗状态。


virtual void LeaveStandby() = 0;

LeaveStandby 在整个系统离开待机模式时被调用。驱动程序应该切换回完全操作状态。

示例

class MyDeviceProvider : public IServerTrackedDeviceProvider
{
public:
    virtual EVRInitError Init( vr::IVRDriverContext *pDriverContext )
    {
        VR_INIT_SERVER_DRIVER_CONTEXT( pDriverContext );

        m_pMyHmdDevice = new MyHmdDeviceDriver();
        m_pMyControllerDevice = new MyControllerDriver();

        //参见 IVRServerDriverHost - 通知运行时我们的驱动程序希望向会话添加两个设备
        vr::VRServerDriverHost()->TrackedDeviceAdded( "MyHMD1", vr::TrackedDeviceClass_HMD, m_pMyHmdDevice );
        vr::VRServerDriverHost()->TrackedDeviceAdded( "MyController1", vr::TrackedDeviceClass_Controller, m_pMyControllerDevice );

        return VRInitError_None;
    }

    virtual void Cleanup()
    {
        delete m_pMyHmdDevice;
        m_pMyHmdDevice = NULL;
        delete m_pMyControllerDevice;
        m_pMyControllerDevice = NULL;
    }

    virtual const char * const *GetInterfaceVersions() { return vr::k_InterfaceVersions; }
    virtual void MyRunFrame() {}
    virtual bool ShouldBlockStandbyMode()  { return false; } //永远不会被调用。
    virtual void EnterStandby()  {}
    virtual void LeaveStandby()  {}

private:
    MyHmdDeviceDriver *m_pMyHmdDevice = nullptr;
    MyControllerDriver *m_pMyControllerDevice = nullptr;
};

ITrackedDeviceServerDriver

ITrackedDeviceServerDriver 代表驱动程序中的一个单一设备。


virtual EVRInitError Activate( uint32_t unObjectId ) = 0;

Activate 在驱动程序调用 IVRServerDriverHost::TrackedDeviceAdded 之后被调用。

在运行时调用此方法之前,应保持资源使用最小。

设备不应该在运行时调用 Activate() 之前对运行时进行任何调用。

  • uint32_t unObjectId - 设备的 id。这在 SteamVR 中是唯一的,用于获取和更新与设备相关的各种状态和属性,例如位姿和属性。

virtual void Deactivate() = 0;

Deactivate 在设备需要停用时被调用,可能是因为 SteamVR 关闭,或者系统在 HMD 之间切换,如果此设备类别代表当前活动的 HMD。

设备应该在此方法被运行时调用时释放它已获取的任何资源。

在 Deactivate 被调用之前,设备对各个运行时实现的接口的任何有效调用都保证是有效的。在设备从此方法返回后,设备绝不能调用这些接口中的任何一个,因为它们不能保证在此之后仍然有效。

IServerTrackedDeviceProvider::Cleanup 将在每个已添加到运行时的设备上调用了此方法之后被调用。


virtual void EnterStandby() = 0;

EnterStandby 在设备应进入待机模式时被调用。当用户请求关闭设备、系统进入待机模式或系统关闭时会发生这种情况。

设备应切换到低功耗状态或关闭。


virtual void *GetComponent( const char *pchComponentNameAndVersion ) = 0;

GetComponent 在激活时被调用,设备应该返回其具有的设备特定功能的任何组件。如果请求的组件在设备上不存在,设备必须从此方法返回 NULL。

  • const char *pchComponentNameAndVersion - 要匹配的组件名称和版本。可能的选项作为字符串保存在 OpenVR 头文件中。您可以从此方法返回的、当前目标(您正在实现)的接口名称和版本如下:
    • IVRDisplayComponent - 单个追踪设备上的通用显示组件。如果驱动程序不需要直接访问显示器或不是虚拟显示器,请使用此组件。
    • IVRDriverDirectModeComponent - 此组件用于完全自行实现直接模式而不允许 VR 合成器拥有窗口/设备的驱动程序。
    • IVRCameraComponent - 表示设备上的一个或多个相机。

有关设备组件的更多信息,请参见设备组件。

virtual void *GetComponent(const char *pchComponentNameAndVersion) override {
  if (!_stricmp(pchComponentNameAndVersion, vr::IVRVirtualDisplay_Version)) {
    return &m_myVirtualDisplay;
  }

  if (!_stricmp(pchComponentNameAndVersion, vr::IVRCameraComponent_Version)) {
    return &m_myCameraComponent;
  }

  return nullptr;
}

virtual void DebugRequest( const char *pchRequest, char *pchResponseBuffer, uint32_t unResponseBufferSize ) = 0;

DebugRequest 在应用程序请求从设备执行调试操作时被调用。设备应该在提供的缓冲区中响应该请求。

从应用程序发送到设备的内容以及响应由应用程序和设备处理。

  • const char *pchRequest - 请求字符串。设备应该在提供的缓冲区中响应该请求。
  • char *pchResponseBuffer - 写入响应的缓冲区。
  • uint32_t unResponseBufferSize - 提供的缓冲区大小。

virtual DriverPose_t GetPose() = 0;

此方法已弃用,不会被运行时调用。设备必须实现此方法,但返回值对行为没有影响。

设备组件

HMD 有多种与运行时交互的方式。它们可以选择拥有自己的合成器,让运行时代表它们直接向设备提交帧,或者需要访问最终合成的后台缓冲区。

OpenVR 提供了一组显示"组件"接口,可用于实现这些不同的行为。

选项包括:

  • IVRDisplayComponent - 推荐。驱动程序将在初始化时向运行时提供有关显示的信息,例如畸变,然后运行时将拥有显示并直接向其提交帧。
    • 这利用了"直接模式"(不要与 IVRDriverDirectModeComponent 混淆),它允许运行时跳过窗口系统级别,直接在显示级别工作。
  • IVRDriverDirectModeComponent - 仅当驱动程序进行自己的合成,或者无法提供畸变数据时才推荐使用。驱动程序直接模式中的图像纹理移交发生在合成之前,并允许驱动程序拥有显示。
  • IVRVirtualDisplay - 仅当您可以提供畸变数据但由于某种原因无法提供输出(例如无线传输帧)时才推荐使用。为驱动程序提供用于头戴显示器显示的最后合成后台缓冲区。
  • IVRCameraComponent - 设备实现了相机。

有关更多信息以及如何实现 GetComponent 方法,请参见 ITrackedDeviceServerDriver。

IVRDisplayComponent

IVRDisplayComponent 是一个表示设备上显示的组件,运行时调用它以获取有关显示的信息。

然后,运行时将通过直接与显示交互来提交合成的帧。


virtual void GetWindowBounds( int32_t *pnX, int32_t *pnY, uint32_t *pnWidth, uint32_t *pnHeight ) = 0;

当 VR 显示器处于扩展模式(即桌面的一部分)时,GetWindowBounds 被运行时调用,SteamVR 通过调用 IsDisplayOnDesktop 来确定。该函数应该提供窗口在桌面上需要的大小和位置,以使其填满扩展的显示器。

  • int32_t *pnX - 从窗口左侧到您想要渲染的位置的像素数。
  • int32_t *pnY - 从窗口顶部到您想要渲染的位置的像素数。
  • uint32_t *pnWidth - 窗口的宽度(以像素为单位)。
  • uint32_t *pnHeight - 窗口的高度(以像素为单位)。

virtual bool IsDisplayOnDesktop() = 0;

IsDisplayOnDesktop 被运行时调用以确定窗口是否扩展了桌面。

HMD不应该使用此扩展模式(从此函数返回 true),而是应该制作一个直接模式的 HMD。
有关直接模式 HMD 的更多信息,请参见 IVRDriverDirectModeComponent。


virtual bool IsDisplayRealDisplay() = 0;

IsDisplayRealDisplay 被运行时调用以确定显示是否为真实显示器。

虚拟设备应该在此处返回 false,以防止运行时尝试与不存在的显示器直接交互。


virtual void GetRecommendedRenderTargetSize( uint32_t *pnWidth, uint32_t *pnHeight ) = 0;

GetRecommendedRenderTargetSize 被运行时调用以获取建议的中间渲染目标的大小,畸变将从该目标中提取。

驱动程序在此方法中指定的值设置了 SteamVR 设置中的分辨率值(至 100%)。如果用户在 SteamVR 设置中设置了不同的分辨率,或者应用程序选择了不同的分辨率,则不保证会使用驱动程序在此方法中指定的值。

您指定的分辨率应该在畸变函数应用于显示器中心投影后,与显示器实现 1:1 像素对应,以最大化用户最关注的显示器中心的细节。

  • uint32_t *pnWidth - 渲染目标的宽度(以像素为单位)。
  • uint32_t *pnHeight - 渲染目标的高度(以像素为单位)。
virtual void GetEyeOutputViewport( EVREye eEye, uint32_t *pnX, uint32_t *pnY, uint32_t *pnWidth, uint32_t *pnHeight ) = 0;

GetEyeOutputViewport 被运行时调用以获取帧缓冲区中的视口,以便将畸变的输出绘制到其中。

  • EVREye eEye - 要获取视口的眼睛。可能的选项是:
    • Eye_Left - 左眼。
    • Eye_Right - 右眼。
  • uint32_t *pnX - 从窗口左侧到视口左边缘的像素数。通常,每只眼睛的视口宽度为窗口宽度的一半。
  • uint32_t *pnY - 从窗口顶部到视口顶部边缘的像素数。
  • uint32_t *pnWidth - 视口的宽度(以像素为单位)。
  • uint32_t *pnHeight - 视口的高度(以像素为单位)。
virtual void GetEyeOutputViewport( EVREye eEye, uint32_t *pnX, uint32_t *pnY, uint32_t *pnWidth, uint32_t *pnHeight )
{
    *pnY = 0;
    *pnWidth = m_nWindowWidth / 2;
    *pnHeight = m_nWindowHeight;

    if ( eEye == Eye_Left )
    {
        *pnX = 0;
    }
    else
    {
        *pnX = m_nWindowWidth / 2;
    }
}

virtual void GetProjectionRaw( EVREye eEye, float *pfLeft, float *pfRight, float *pfTop, float *pfBottom ) = 0;

GetProjectionRaw 被运行时调用以从畸变中获取原始值,这些值用于构建您自己的投影矩阵(例如,如果您的应用程序正在做像无限 Z 之类的奇特操作)。

这些值表示从中心视图轴线到视口边缘的半角的正切值。

请注意,"底部"和"顶部"是相反的。"底部"是从投影中心到显示器顶部(+Y)边缘的正切角,"顶部"是从投影中心到显示器底部(-Y)边缘的正切角。

  • EVREye eEye - 要获取投影的眼睛。可能的选项是:
    • Eye_Left - 左眼。
    • Eye_Right - 右眼。
  • float *pfLeft - 中心视图轴线与左侧裁剪平面(-X)之间的角度的正切值。
  • float *pfRight - 中心视图轴线与右侧裁剪平面(+X)之间的角度的正切值。
  • float *pfTop - 中心视图轴线与底部裁剪平面(-Y)之间的角度的正切值。
  • float *pfBottom - 中心视图轴线与顶部裁剪平面(+Y)之间的角度的正切值。

例如,具有 90 度视场的 HMD 将返回以下值:

void GetProjectionRaw( EVREye eEye, float *pfLeft, float *pfRight, float *pfTop, float *pfBottom )
{
    // 45度是前向向量与每个侧边之间的角度
    // 所以这些值将是45度的正切值(即1或tan(45))
    *pfLeft = -1.0f;
    *pfRight = 1.0f;
    *pfTop = -1.0f;
    *pfBottom = 1.0f;
}

virtual DistortionCoordinates_t ComputeDistortion( EVREye eEye, float fU, float fV ) = 0;

ComputeDistortion 被运行时调用,用于为每个颜色通道生成畸变后的 UV 坐标。UV 范围从 0 到 1,从源渲染目标的左上角 (0,0) 到右下角 (1,1),并覆盖单只眼睛。

  • EVREye eEye - 要获取畸变的眼睛。可能的选项是:
    • Eye_Left - 左眼。
    • Eye_Right - 右眼。
  • float fU - 当前的 U 坐标。
  • float fV - 当前的 V 坐标。
virtual bool ComputeInverseDistortion( HmdVector2_t *pResult, EVREye eEye, uint32_t unChannel, float fU, float fV ) = 0;

ComputeInverseDistortion 被运行时调用,以获取指定眼睛、通道和 uv 的反畸变函数的结果。

驱动程序可以从此方法返回 false,以指示运行时应从 IVRDisplayComponent::ComputeDistortion 返回的结果推断估计值。

从方法返回 true 表示告诉运行时不应尝试估计反函数,而是使用驱动程序提供的值。

  • HmdVector2_t *pResult - 驱动程序应将指定 UV 的结果写入此处。
  • EVREye eEye - 要获取畸变的眼睛。可能的选项是:
    • Eye_Left - 左眼。
    • Eye_Right - 右眼。
  • uint32_t unChannel - 请求的通道。0 表示红色,1 表示蓝色,2 表示绿色。
  • float fU - 当前的 U 坐标。
  • float fV - 当前的 V 坐标。

IVRDriverDirectModeComponent

IVRDriverDirectModeComponent 用于完全自行实现直接模式而不允许 VR 合成器拥有窗口的驱动程序,也用于驱动程序自己的合成器与运行时交互。

这对于可能想要实现自己的合成器的驱动程序很有用。

除非需要拥有对显示的这种控制,否则驱动程序应该实现 IVRDisplayComponent 并让合成器拥有设备。


virtual void CreateSwapTextureSet( uint32_t unPid, const SwapTextureSetDesc_t *pSwapTextureSetDesc, SwapTextureSet_t *pOutSwapTextureSet ) {}

CreateSwapTextureSet 被调用来分配供应用程序渲染的纹理。

每只眼睛将有一个这样的纹理集传递回 SubmitLayer 的每一帧。

  • uint32_t unPid - 创建纹理的客户端进程的 pid。
  • const SwapTextureSetDesc_t *pSwapTextureSetDesc - 纹理的描述。
    • uint32_t nWidth - 纹理的宽度。
    • uint32_t nHeight - 纹理的高度。
    • uint32_t nFormat - 纹理的格式。采用 DXGI_FORMAT。
    • uint32_t nSampleCount - 每个像素的样本数。
  • SwapTextureSet_t *pOutSwapTextureSet - 您分配的纹理
    • vr::SharedTextureHandle_t rSharedTextureHandles[ 3 ] - 指向纹理的_共享_句柄
    • uint32_t VRSwapTextureFlag - 纹理的标志。
      • VRSwapTextureFlag_Shared_NTHandle - 指定共享纹理资源是使用 SHARED_NTHANDLE 选项(Windows)创建的
void CreateSwapTextureSet( uint32_t unPid, const SwapTextureSetDesc_t *pSwapTextureSetDesc, SwapTextureSet_t *pOutSwapTextureSet ) {
    D3D11_TEXTURE2D_DESC desc = {};
    desc.ArraySize = 1;
    desc.Width = pSwapTextureSetDesc->unWidth;
    desc.Height = pSwapTextureSetDesc->unHeight;
    desc.MipLevels = 1;
    desc.SampleDesc.Count = 1;
    desc.SampleDesc.Quality = 0;
    desc.Format = (DXGI_FORMAT)pSwapTextureSetDesc->unFormat;
    desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
    desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;

    for(int i = 0; i < 3; i++) {
        HRESULT hresult = myD3DDevice->CreateTexture2D(&desc, nullptr, &pOutSwapTextureSet->rSharedTextureHandles[i]);

        IDXGIResource* pResource = nullptr;
        hresult = pOutSwapTextureSet->rSharedTextureHandles[i]->QueryInterface(__uuidof(IDXGIResource), (void**)&pResource);

        HANDLE hSharedHandle = nullptr;
        hresult = pResource->GetSharedHandle(&hSharedHandle);
        pResource->Release();

        pOutSwapTextureSet->rSharedTextureHandles[i] = (vr::SharedTextureHandle_t)hSharedHandle;
    }
}

virtual void DestroySwapTextureSet( vr::SharedTextureHandle_t sharedTextureHandle ) {};

DestroySwapTextureSet 应销毁使用 CreateSwapTextureSet 创建的纹理。

只需要使用集合中一个句柄来销毁整个集合。

  • vr::SharedTextureHandle_t sharedTextureHandle - 要销毁的句柄。

virtual void DestroyAllSwapTextureSets( uint32_t unPid ) {}

DestroyAllSwapTextureSets 应销毁给定进程的所有纹理。

  • uint32_t unPid - 要从中销毁纹理的进程的 pid。

virtual void GetNextSwapTextureSetIndex( vr::SharedTextureHandle_t sharedTextureHandles[ 2 ], uint32_t( *pIndices )[ 2 ] ) {}

GetNextSwapTextureSetIndex 在 Present 返回后调用,用于获取下一个用于渲染的索引。

  • vr::SharedTextureHandle_t sharedTextureHandles[ 2 ] - 每只眼睛纹理的句柄。纹理必须已使用 CreateSwapTextureSet 创建。
  • uint32_t( *pIndices )[ 2 ] - 您为每只眼睛使用的索引结果。

virtual void SubmitLayer( const SubmitLayerPerEye_t( &perEye )[ 2 ] ) {}

SubmitLayer 每层调用一次以绘制此帧。

  • const SubmitLayerPerEye_t( &perEye )[ 2 ] - 每只眼睛要绘制的层。
    • vr::SharedTextureHandle_t sharedTextureHandle - 要绘制的纹理的句柄。纹理必须已使用 CreateSwapTextureSet 创建。
    • vr::SharedTextureHandle_t hDepthTexture - 深度纹理。并非总是提供。
    • vr::VRTextureBounds_t bounds - 所提供纹理的有效区域和深度。
    • vr::HmdMatrix44_t mProjection - 用于渲染深度缓冲区的投影矩阵。
    • vr::HmdMatrix34_t mHmdPose - 用于渲染此层的 Hmd 位姿。

virtual void Present( vr::SharedTextureHandle_t syncTexture ) {}

Present 被调用来提交排队等待显示的层。

void Present(vr::SharedTextureHandle_t syncTexture) {
  // 仅当同步纹理更改时才打开。
  if (m_syncTexture != syncTexture) {
      m_syncTexture = syncTexture;
      SAFE_RELEASE( m_pSyncTexture );
      if (m_syncTexture) {
          m_pD3D11Device->OpenSharedResource( ( HANDLE )m_syncTexture, __uuidof( ID3D11Texture2D ), ( void ** )&m_pSyncTexture );
      }
  }

  IDXGIKeyedMutex *pSyncMutex = NULL;
  if (m_pSyncTexture != NULL && SUCCEEDED(m_pSyncTexture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void **)&pSyncMutex))) {
      pSyncMutex->AcquireSync(0, 10);
  }

  //...

  if (pSyncMutex) {
      pSyncMutex->ReleaseSync( 0 );
      pSyncMutex->Release();
  }
}

virtual void PostPresent( const Throttling_t *pThrottling ) {}

PostPresent 是驱动程序可选择实现的方法,在 Present 返回且合成器知道驱动程序已成功在 Present 中获取同步纹理之后调用,以允许驱动程序在成功获取同步纹理之后,在垂直同步之前占用更多时间。

  • const Throttling_t *pThrottling - 由用户设置的节流信息
    • uint32_t nFramesToThrottle - 从应用键下的设置键 k_pch_SteamVR_FramesToThrottle_Int32 设置。
    • uint32_t nAdditionalFramesToPredict - 从应用键下的设置键 k_pch_SteamVR_AdditionalFramesToPredict_Int32 设置。

virtual void GetFrameTiming( DriverDirectMode_FrameTiming *pFrameTiming ) {}

GetFrameTiming 是驱动程序可选择实现的方法,用于从驱动程序获取额外的帧计时统计信息。

  • DriverDirectMode_FrameTiming *pFrameTiming - 驱动程序提供的帧计时数据
    • uint32_t nSize - 结构体大小
    • uint32_t nNumFramePresents - 帧被呈现的次数
    • uint32_t nNumMisPresented - 帧在预期之外的垂直同步上呈现的次数
    • uint32_t nNumDroppedFrames - 前一帧被扫描输出的次数(即合成器错过了垂直同步)。
    • uint32_t m_nReprojectionFlags - 重投影信息的标志。
      • VRCompositor_ReprojectionMotion_Enabled - 当前运行应用程序的 UI 中启用了运动平滑
      • VRCompositor_ReprojectionMotion_ForcedOn - 当前运行应用程序的 UI 中强制开启了运动平滑
      • VRCompositor_ReprojectionMotion_AppThrottled - 应用程序通过 ForceInterleavedReprojectionOn 请求节流。

IVRVirtualDisplay

用于实现虚拟显示器并需要访问最终合成的后台缓冲区图像而不传输到真实显示器的驱动程序,例如用于无线传输。

从运行时的角度来看,合成器是与虚拟的,而非实际的显示器交互。这使其成为渲染管道的关键部分,驱动程序必须正确掌握时序,以继续为用户提供适当的体验。

由于 IVRVirtualDisplay 预期作为设备组件实现,它必须实现 ITrackedDeviceServerDriver。

设备必须设置以下属性才能拥有虚拟显示器组件:

  • 它作为 vr::TrackedDeviceClass_DisplayRedirect 设备类别添加到运行时(见下文)。
  • 在 driver.vrdrivermanifest 中将 redirectsDisplay 设置为 true。
  vr::VRServerDriverHost()->TrackedDeviceAdded("my_serial_ABC123", vr::TrackedDeviceClass_DisplayRedirect, &myVirtualDisplayDevice);

virtual void Present( const PresentInfo_t *pPresentInfo, uint32_t unPresentInfoSize ) = 0;

Present 在将所有渲染提交到 GPU 后每帧调用一次,并提供最终的后台缓冲区用于显示。

Present 应该避免对读取纹理等任何操作进行阻塞调用,而应将这些命令排队以尽快返回。WaitForPresent 用于调用阻塞操作。

  • const PresentInfo_t *pPresentInfo - 包含有关后台缓冲区的信息。
    • SharedTextureHandle_t backbufferTextureHandle - 指向后台缓冲区纹理的句柄。
    • EVSync vsync - 垂直同步信息
      • VSync_None - 无垂直同步
      • VSync_WaitRender - 阻塞后续渲染工作直到垂直同步发生。
      • VSync_NoWaitRender - 不阻塞后续渲染工作,允许工作尽早开始。
    • uint64_t nFrameId - 当前帧的帧 ID。
    • double flVSyncTimeInSeconds - 垂直同步发生的时间(以秒为单位)。
  • uint32_t unPresentInfoSize - 当前 PresentInfo_t 结构体的大小。

virtual void WaitForPresent() = 0;

WaitForPresent 被调用来允许驱动程序对可能在 Present 中排队的操作执行阻塞调用。


virtual bool GetTimeSinceLastVsync( float *pfSecondsSinceLastVsync, uint64_t *pulFrameCounter ) = 0;

GetTimeSinceLastVsync 被调用来提供用于与显示器同步的计时数据。

GetTimeSinceLastVsync 在 WaitForPresent 返回后调用。它应该返回自上次虚拟垂直同步事件以来的时间(以秒为单位)。

  • float *pfSecondsSinceLastVsync - 自上次虚拟垂直同步事件以来的时间(以秒为单位)。这是与物理设备时序的偏移,由驱动程序在 vr::Prop_SecondsFromVsyncToPhotons_Float 属性中指定的虚拟驱动程序引入的额外延迟。
  • uint64_t *pulFrameCounter - 当前帧计数。这是一个单调递增的值,并且应该反映虚拟垂直同步计数,而不是呈现的帧数,以便运行时检测掉帧。

有关 IVRVirtualDisplay 实现的示例,请参见此仓库:https://github.com/ValveSoftware/virtual_display。

IVRServerDriverHost

IVRServerDriverHost 允许驱动程序通知运行时有关驱动程序或设备的状态更改。

调用 vr::VRServerDriverHost() 会返回指向运行时 IVRServerDriverHost 实现的指针。


virtual bool TrackedDeviceAdded( const char *pchDeviceSerialNumber, ETrackedDeviceClass eDeviceClass, ITrackedDeviceServerDriver *pDriver ) = 0;

每当驱动程序希望向运行时添加设备时,应该调用 TrackedDeviceAdded。

  • const char *pchDeviceSerialNumber - 一个字符串,是您即将添加到服务器的设备的唯一且不可变的序列号。设备的序列号在设备添加到运行时后不能更改。
  • ETrackedDeviceClass eDeviceClass - 即将激活的设备类型。
    • TrackedDeviceClass_HMD - 头戴式显示器。
    • TrackedDeviceClass_Controller - 追踪控制器。
    • TrackedDeviceClass_GenericTracker - 通用追踪器,类似于控制器但没有定义的手持角色。
    • TrackedDeviceClass_TrackingReference - 追踪参考,例如作为追踪参考点的相机和基站。
    • TrackedDeviceClass_DisplayRedirect - 本身不一定被追踪,但可能重定向其他追踪设备视频输出的附件。请参见 IVRVirtualDisplay。
  • ITrackedDeviceServerDriver *pDriver - 指向此类别的 ITrackedDeviceServerDriver 实现的指针。

设备的序列号在整个会话期间绝不能更改。序列号代表每个设备的唯一标识符。

为 ITrackedDeviceServerDriver 提供的指针必须保持有效,直到 IServerTrackedDeviceProvider::Cleanup 被调用。

在以下情况下返回 true:

  • 设备具有有效的序列号。
  • 系统中不存在具有相同序列号的设备。
  • 设备与系统中活动的 HMD 位于同一驱动程序中,或者 activateMultipleDrivers 为 true。
  • 系统中不存在 HMD,或者该设备不是 HMD。

如果 TrackedDeviceAdded 返回 true,则设备已排队等待激活,但无法保证设备能够从此方法的返回值成功激活。


virtual void TrackedDevicePoseUpdated( uint32_t unWhichDevice, const DriverPose_t & newPose, uint32_t unPoseStructSize ) = 0;

每当设备希望更新其位姿时,应该调用 TrackedDevicePoseUpdated。

  • uint32_t unWhichDevice - 应更新的设备索引
  • const DriverPose_t & newPose - 设备的位姿
  • uint32_t unPoseStructSize - DriverPose_t 的大小

virtual void VsyncEvent( double vsyncTimeOffsetSeconds ) = 0;

VsyncEvent 通知服务器在连接到设备的显示器上发生了垂直同步。这仅允许在 HMD 类别的设备上。

  • double vsyncTimeOffsetSeconds - 垂直同步事件的时间偏移(以秒为单位)。

virtual void VendorSpecificEvent( uint32_t unWhichDevice, vr::EVREventType eventType, const VREvent_Data_t & eventData, double eventTimeOffset ) = 0;

VendorSpecificEvent 向全局事件队列发送事件。调用此函数的设备可以选择发送 vr::EVREventType 中定义的*事件,或使用自定义的、保留的事件类型(在 VREvent_VendorSpecific_Reserved_Start 和 VREvent_VendorSpecific_Reserved_End 之间)。

  • uint32_t unWhichDevice - 触发此事件的设备索引。这必须是一个有效的设备索引。
  • vr::EVREventType eventType - 事件类型。eventType 的值必须大于 0 且小于 VREvent_VendorSpecific_Reserved_End。
  • const VREvent_Data_t & eventData - 事件的数据。
  • double eventTimeOffset - 事件从现在起的时间偏移(以秒为单位)。

virtual bool IsExiting() = 0;

如果运行时正在退出,则返回 true,否则返回 false。


virtual bool PollNextEvent( VREvent_t *pEvent, uint32_t uncbVREvent ) = 0;

轮询全局事件队列中的下一个事件。如果事件队列上有事件,函数必须返回 true;如果没有事件,则返回 false。

一旦事件被从队列中读取,它就会被移除。

  • VREvent_t *pEvent - 指向方法应填充事件的位置的指针。
  • uint32_t uncbVREvent - VREvent_t 结构体的大小(以字节为单位)。

驱动程序应该每一帧轮询事件。这使其非常适合在 IServerTrackedDeviceProvider::MyRunFrame 中使用。

事件是时间敏感的,可能在添加到队列后不久被移除。它们必须在事件添加到队列后至少存在一帧。

可以连续调用 PollNextEvent,直到它返回 false,表示队列中没有更多事件。

vr::VREvent_t vrEvent;
while ( vr::VRServerDriverHost()->PollNextEvent( &vrEvent, sizeof( vrEvent )))
{
    switch ( vrEvent.eventType )
    {
        case vr::VREvent_Input_HapticVibration:
        {
            if ( vrEvent.data.hapticVibration.componentHandle == m_compMyHaptic )
            {
            ... 发送数据到硬件
            }
        }
    break;
    }
}

virtual void GetRawTrackedDevicePoses( float fPredictedSecondsFromNow, TrackedDevicePose_t *pTrackedDevicePoseArray, uint32_t unTrackedDevicePoseArrayCount ) = 0;

GetRawTrackedDevicePoses 提供对所有活动设备位姿的访问。

设备位姿将位于其原始的追踪空间中。这个追踪空间由每个为其设备提供位姿的驱动程序唯一定义。

  • float fPredictedSecondsFromNow - 预测设备位姿的时间。负数表示过去,正数表示预测未来。
  • TrackedDevicePose_t *pTrackedDevicePoseArray - 一个用户指定大小的数组,将用设备位姿填充
  • uint32_t unTrackedDevicePoseArrayCount - 提供的位姿数组的长度。

pTrackedDevicePoseArray 将用设备的位姿填充,数组的索引与存储在运行时中的设备索引相关联。

HMD 位姿将始终位于索引 0。

GetRawTrackedDevicePoses 将填充 pTrackedDevicePoseArray 到指定的 unTrackedDevicePoseArrayCount,或者直到没有更多设备为止。

vr::TrackedDevicePose_t GetDevicePose( vr::TrackedDeviceIndex_t unDeviceIndex )
{
    vr::TrackedDevicePose_t poses[unDeviceIndex];
    vr::VRServerDriverHost()->GetRawTrackedDevicePoses( 0.0, &pose, unDeviceIndex );
    return poses[unDeviceIndex];
}

vr::TrackedDevicePose_t 包含一个 HmdMatrix34_t mDeviceToAbsoluteTracking,一个 3x4 矩阵,其中包含一个 3x3 旋转矩阵和一个位置向量(在最后一列)。

可以使用以下函数从 3x4 矩阵中提取位置和旋转(作为四元数):

vr::HmdVector3_t GetPosition(const vr::HmdMatrix34_t& matrix) {
  return {
      matrix.m[0][3],
      matrix.m[1][3],
      matrix.m[2][3]
  };
}

vr::HmdQuaternion_t GetRotation(const vr::HmdMatrix34_t& matrix) {
  vr::HmdQuaternion_t q{};

  q.w = sqrt(fmax(0, 1 + matrix.m[0][0] + matrix.m[1][1] + matrix.m[2][2])) / 2;
  q.x = sqrt(fmax(0, 1 + matrix.m[0][0] - matrix.m[1][1] - matrix.m[2][2])) / 2;
  q.y = sqrt(fmax(0, 1 - matrix.m[0][0] + matrix.m[1][1] - matrix.m[2][2])) / 2;
  q.z = sqrt(fmax(0, 1 - matrix.m[0][0] - matrix.m[1][1] + matrix.m[2][2])) / 2;

  q.x = copysign(q.x, matrix.m[2][1] - matrix.m[1][2]);
  q.y = copysign(q.y, matrix.m[0][2] - matrix.m[2][0]);
  q.z = copysign(q.z, matrix.m[1][0] - matrix.m[0][1]);
  return q;
}

virtual void RequestRestart( const char *pchLocalizedReason, const char *pchExecutableToStart, const char *pchArguments, const char *pchWorkingDirectory ) = 0;

RequestRestart 请求重启 SteamVR。

可以调用此方法来在运行时重启时打开一个可执行文件。

  • const char *pchLocalizedReason - 必需。运行时需要重启的原因。应该使用用户的当前区域设置。运行时内的本地化字符串以 ‘#’ 开头。
  • const char *pchExecutableToStart - 可选。在运行时重启时要启动的可执行文件的文件名。
    传递 nullptr 以仅重启运行时。
  • const char *pchArguments - 可选(如果设置了 pchExecutableToStart 则为必需)。在运行时重启时要启动的可执行文件的参数。传递 nullptr 以仅重启运行时。
  • const char *pchWorkingDirectory - 可选(如果设置了 pchExecutableToStart 则为必需)。在运行时重启时要启动的可执行文件的工作目录。传递 nullptr 以仅重启运行时。

virtual uint32_t GetFrameTimings( Compositor_FrameTiming *pTiming, uint32_t nFrames ) = 0;

GetFrameTimings 提供复制一段合成器计时数据的访问权限。

旨在用于驱动程序直接模式组件进行节流决策。

帧按升序(从最旧到最新)返回,最后一帧是最新的帧。

  • Compositor_FrameTiming *pTiming - 填充计时数据的位置。仅第一个条目的 m_nSize 将被设置,其他条目可以从中推断。
  • uint32_t nFrames - 请求的帧计时数量。

返回填充的条目总数。


virtual void SetDisplayEyeToHead( uint32_t unWhichDevice, const HmdMatrix34_t & eyeToHeadLeft, const HmdMatrix34_t & eyeToHeadRight ) = 0;

SetDisplayEyeToHead 通知服务器某个追踪设备的显示组件变换已更新。

  • uint32_t unWhichDevice - 要更新的设备。此设备必须是 TrackedDeviceClass_HMD,并且应该是设备索引 0。
  • const HmdMatrix34_t & eyeToHeadLeft - 左眼的变换。
  • const HmdMatrix34_t & eyeToHeadRight - 右眼的变换。

virtual void SetDisplayProjectionRaw( uint32_t unWhichDevice, const HmdRect2_t & eyeLeft, const HmdRect2_t & eyeRight ) = 0;

SetDisplayProjectionRaw 通知服务器某个追踪设备的显示投影已更改。

  • uint32_t unWhichDevice - 要更新的设备。此设备必须是 TrackedDeviceClass_HMD,并且应该是设备索引 0。
  • const HmdRect2_t & eyeLeft - 左眼的显示投影。
  • const HmdRect2_t & eyeRight - 右眼的显示投影。

virtual void SetRecommendedRenderTargetSize( uint32_t unWhichDevice, uint32_t nWidth, uint32_t nHeight ) = 0;

SetRecommendedRenderTargetSize 通知服务器某个追踪设备的推荐渲染目标分辨率已更改。

  • uint32_t unWhichDevice - 要更新的设备。此设备必须是 TrackedDeviceClass_HMD,并且应该是设备索引 0。
  • uint32_t nWidth - 分辨率宽度(以像素为单位)。
  • uint32_t nHeight - 分辨率高度(以像素为单位)。

IVRDriverLog

IVRDriverLog 接口可以用于驱动程序记录到共享的运行时日志文件。

SteamVR 提供了一个用于实时查看日志的 Web 界面(SteamVR 菜单 > 开发者 > Web 控制台),也可以通过此 URL 访问:http://localhost:27062/console/index.html。

包含上次会话日志的文本文件位于:

  • <steam_install_dir>\logs\vrserver.txt。
    • 通常,此文件的完整路径是:C:\Program Files (x86)\Steam\logs\vrserver.txt。

包含上上次会话日志的文本文件位于:

  • <steam_install_dir>\logs\vrserver.previous.txt。
    • 通常,此文件的完整路径是:C:\Program Files (x86)\Steam\logs\vrserver.previous.txt。

来自驱动程序的消息将格式化为:

<日期> - <驱动程序名称>: <日志消息>。


virtual void Log( const char *pchLogMessage ) = 0;

Log 将消息记录到共享的运行时日志文件。

  • const char *pchLogMessage - 要记录到文件的消息。必须以空字符结尾。

此接口的包装器在 samples/utils/driverlog 中提供,以使日志记录接口的行为类似于 printf 函数。

IVRWatchdogProvider

IVRWatchdogProvider 提供了一个接口,该接口被加载到 steam.exe 中以接收驱动程序定义的事件,这些事件应该唤醒运行时。例如,这可能是打开控制器或戴上 HMD。

steam.exe 必须正在运行才能使看门狗处于活动状态。

IVRWatchdogProvider 必须构建为 32 位 DLL,因为 steam.exe 是 32 位的。

virtual EVRInitError Init( IVRDriverContext *pDriverContext ) = 0;

Init 在 SteamVR 退出时,当 DLL 被加载到 steam.exe 中时调用。驱动程序应该在此处启动一个线程来侦听硬件事件。

Init 将初始化看门狗驱动程序上下文。您可以使用 OpenVR API 提供的宏 VR_INIT_WATCHDOG_DRIVER_CONTEXT 来执行此操作。


virtual void Cleanup() = 0;

Cleanup 在看门狗即将卸载之前被调用,以清理其在活动期间所需的任何资源。

IVRWatchdogHost

此接口由运行时提供,允许驱动程序唤醒运行时。


virtual void WatchdogWakeUp( vr::ETrackedDeviceClass eDeviceClass ) = 0;

当 IVRWatchdogProvider 希望唤醒运行时,看门狗应该调用此函数,指定导致运行时启动的设备类别(HMD、控制器等)。

  • vr::ETrackedDeviceClass eDeviceClass - 导致运行时启动的设备类别。

设备输入

预测未来设备可能具有的输入类型是不可能的。也期望所有应用程序开发人员都明确支持所有设备,并继续支持其应用程序在未来发布的设备,这是不可行的。

为了解决这个问题,OpenVR API 在物理设备输入和应用程序内动作之间创建了分隔。

设备暴露其拥有的输入集合。例如,设备可能有一个摇杆、一个扳机按钮和一个触控板。

应用程序暴露其支持的动作集合。例如,动作可能是开火或传送。

绑定配置文件然后将这些输入链接到动作。例如,Index 控制器上的扳机按钮可以绑定到枪支扳机拉动动作。

定义什么是输入或动作由开发人员决定。但是,关于可以作为输入提交和作为动作接收的数据存在某些限制。

每个控制器设备类型应该暴露一个输入配置文件,该文件表示设备可用于绑定的输入组件集合。

输入配置文件

输入配置文件应为有效的 JSON 文件,并且应该位于:<driver_name>/resources/input/<device_name>_profile.json。

必须通过为每个设备设置 vr::ETrackedDeviceProperty::Prop_InputProfilePath_String 属性来指定此文件位置。有关属性的信息,请参见设备属性。

输入源

输入源是设备上用于输入的单个硬件组件。例如,摇杆或按钮。

输入组件是硬件可以测量的输入的单个比特。例如,按钮点击或摇杆 Y 值。

输入组件可以分组到一个输入源中,以允许用户将它们作为一个逻辑组进行操作。例如,摇杆(输入源)可以测量 X、Y 和点击值(输入组件)。

输入源由路径 /input/<input_source> 标识。

输入组件由路径 /input/<input_source>/<input_component> 标识。

例如,输入源 /input/joystick 包含输入组件 /input/joystick/x、/input/joystick/y 以及可选的 /input/joystick/click 和 /input/joystick/touch。

设备不应用一种输入模拟另一种输入(例如,当硬件上不存在物理开关时应用阈值来提供 /input/trigger/click 值)。这将允许 SteamVR 提供用户为每个应用程序设置标量到模拟转换行为的能力,而不是使用全局硬编码值。

输入配置文件 JSON

输入配置文件 JSON 文件包含每个设备的输入和绑定信息。

可以在 JSON 文件中指定的键列表:

  • jsonid - 必需。这必须设置为 input_profile。
  • controller_type - 如果设备尚未指定 Prop_ControllerType_String,则为必需。这是此配置文件适用的控制器类型。用于将配置文件与设备匹配。
  • compatibility_mode_controller_type - (v1.26 中已弃用) 指定当绑定不可用时设备应模拟指定的设备类型。
    在绑定中,它将设置以下值:
    • simulated_controller_type 为指定的控制器类型。
    • simulate_rendermodel 为 true。
    • simulate_hmd 为 true。
  • remapping - (v1.26+) 可选。一个 JSON 文件,详细说明如何将绑定从一种控制器类型转换为另一种。用于应用程序未为此设备类型指定绑定的情况。请参见:自动重新绑定。
  • device_class - 可选。从设备的 Prop_DeviceClass_Int32 属性推断。
  • hand_priority - 可选。如果之前未设置 Prop_ControllerHandSelectionPriority_Int32,
    此处指定的值将用于该属性,否则如果已设置该属性,则忽略。
  • legacy_binding - 推荐。如果未设置此项,将回退到 config/legacy_bindings_generic.json。
    旧版绑定文件与其他输入绑定文件格式相同,可以使用 SteamVR 绑定 UI 构建然后导出。
  • tracker_types - 可选。如果输入配置文件旨在与追踪器一起使用,可以指定指向另一个输入配置文件的路径。键是 ETrackerRole 的枚举,值是指定追踪器角色的输入配置文件的路径。
  • input_bindingui_mode - 推荐。如何在绑定 UI 中渲染设备。如果设备的类别是 TrackedDeviceClass_HMD,则回退到 hmd,否则设置为 controller_handed。
    • controller_handed - 设备类型通常作为一对使用,每只手一个。设备可以选择性地专用于左手或右手。当使用此样式的设备进行绑定 UI 时,将显示 2 个控制器。
    • hmd - 设备是 HMD,并希望在为其配置输入时使绑定 UI 处于 HMD 模式。
    • single_device - 设备应单独显示在一页上,因为用户一次只使用一个设备。例如 Xbox 风格的游戏控制器、枪式控制器或跑步机。
  • input_bindingui_left - 强烈推荐。在绑定 UI 中用于左手渲染的图像的路径。
    如果未设置,将回退到空图像。
    • image - 图像的路径。例如,{indexcontroller}/icons/indexcontroller_left.svg。必须是 SVG 或 PNG。绑定 UI 没有固定大小,将缩放到输出设备,因此使用 SVG 格式可确保最佳呈现您的控制器。图像应该显示控制器的倾斜视图,以展示设备上所有输入的最佳视图。
    • transform 是一个字符串,传递给绑定 UI 中的 CSS 变换。驱动程序可以使用此字符串使用 scale( -1, 1) 镜像设备图像,因此单个图像可以用于左手和右手。
  • input_bindingui_right - 强烈推荐。在绑定 UI 中用于右手渲染的图像的路径。
    如果未设置,将回退到空图像。
    • image - 图像的路径。例如,{indexcontroller}/icons/indexcontroller_right.svg。必须是 SVG 或 PNG。绑定 UI 没有固定大小,将缩放到输出设备,因此使用 SVG 格式可确保最佳呈现您的控制器。图像应该显示控制器的倾斜视图,以展示设备上所有输入的最佳视图。
    • transform 是一个字符串,传递给绑定 UI 中的 CSS 变换。驱动程序可以使用此字符串使用 scale( -1, 1) 镜像设备图像,因此单个图像可以用于左手和右手。
  • input_source
    • <input_source_path> - 输入源的路径。路径必须采用 /<input/pose/finger>/<my_name> 格式。finger 必须被视为触发器。
      • type - 必需。可用类型有:
        • joystick - 输入是摇杆或拇指摇杆。此类型的输入组件会自动创建,如下所列:
          • x
          • y
        • button - 输入是具有布尔值的按钮。
        • trigger - 输入是具有标量值的扳机。
        • trackpad - 输入是触控板。它与摇杆的不同之处在于它具有力状态。
        • skeleton - 输入是骨骼动画源。
        • haptic - 输入是触觉组件。每个设备只能绑定一个这样的组件。
        • pose - 输入是位姿。这些从设备的渲染模型中的组件自动创建,也在此处指定以将它们绑定到应用程序。此处指定的位姿名称必须与渲染模型文件中的位姿匹配。
          • /pose/raw - 必需。设备的原始位姿。这必须在输入配置文件中指定,不需要设置渲染模型。
      • binding_image_point - 推荐。在 UI 中从侧面的输入到 image 中指定的图像绘制一条线。将回退到 [50, 50]。
      • order - 可选。输入在 UI 中的顺序。默认为 0。常用源(例如主扳机)应该在较少使用的源之前列出。优先级按升序排列。
      • click - 可选。表示输入可以感知被点击。适用于类型为 joystick、button、trigger 的输入。
      • value - 可选。表示输入可以感知范围从 0-1 的标量值。适用于类型为 trigger 的输入。
      • touch - 可选。表示输入可以感知被触摸,但未被点击。适用于类型为 joystick、button、trigger、trackpad 的输入。
      • force - 可选。表示输入可以感知力。适用于类型为 joystick、trigger、trackpad 的输入。
      • input_activity_path - 可选。导致输入计为"活动"的输入组件的路径。
        如果未设置,将导致输入源计为活动的组件是(按顺序):
        • touch
        • click
        • force
        • value
        • x
      • input_activity_threshold - 浮点数。此输入计为"活动"的阈值。默认为 0。如果值严格大于阈值,则计为活动。
      • value_source - 可选。仅对 /input/pinch 有效。用作当前组件值的输入组件的路径。指向的输入组件必须具有 value 组件。
      • force_source - 可选。仅对 /input/pinch 有效。用作当前组件的力值的输入组件的路径。指向的输入组件必须具有 force 组件。
      • capsense_source - 可选。仅对 /input/pinch 有效。用作当前组件的电容感应值的输入组件的路径。指向的输入组件必须是 /input/finger 源。
  • default_bindings - 可选。一个数组,列出应随驱动程序一起提供的任何绑定配置文件。这些将游戏中的动作链接到设备上的输入。更多信息请参见默认绑定。
    • app_key - 必需。命名绑定将应用到的应用程序。如果这是 Steam 应用程序,它将以 steam.app. 为前缀,后跟 appId。
    • binding_url - 必需。绑定配置文件的路径,相对于当前目录。

要为给定的输入源设置输入组件,请将输入组件名称作为键添加到 <input source path> 对象,并将值设置为 true。例如,一个名为 thumbstick 的摇杆,可以感知 click 和 touch,可能如下所示:

{
  "/input/thumbstick": {
    "type": "joystick",
    "click": true,
    "touch": true,
    "binding_image_point": [
      31,
      26
    ],
    "order": 4
  }
}

这会创建输入组件路径:

  • /input/thumbstick/x
  • /input/thumbstick/y
  • /input/thumbstick/click
  • /input/thumbstick/touch

保留的输入源

有一些输入源是保留给 SteamVR 内部特定用途的。下面定义的源的值不会供应用程序绑定,但设备可以创建指向它们的句柄(不在输入配置文件中定义它们)以允许用户绑定到它们。

  • /input/system/click - 一个布尔值,用于调出或关闭 SteamVR 仪表板。
  • /proximity - 一个布尔值,当用户戴上头戴设备时为 true。用于使设备进入/退出待机状态。

组件路径 /input/system/click 是一个特例,用于召唤或关闭 SteamVR 仪表板。此组件的值不会对应用程序可用。此组件不需要在输入配置文件中设置。

绑定配置文件

绑定配置文件(将输入配置文件中的输入组件链接到游戏内动作)在单独的文件中指定,与输入或动作配置文件分开。

这些配置文件可以由游戏开发人员实现,并随每个游戏一起发布,或者随驱动程序一起发布,使用输入配置文件中的 default_bindings 节。

根据发布者的不同,选择绑定配置文件时有特定的优先级。按此顺序,将选择绑定配置文件:

  1. 用户设置的绑定。
  2. 应用程序开发人员随应用程序一起发布的绑定。
  3. 在 partner.steamworks 网站上设置的绑定。
  4. 驱动程序设置的默认绑定。
  5. 目标兼容模式设备的绑定(如果启用了兼容模式)。请参见设备模拟。
  6. 使用重新映射布局转换的绑定(如果启用了自动重新绑定。)

可以使用 SteamVR 绑定 Web 界面生成绑定配置文件。
在 SteamVR 开发者设置中启用在输入绑定用户界面中启用调试选项将允许您将包含所选应用程序绑定的 JSON 文件导出到 Document/steamvr/input/exports,文件名以 Steam 上的应用程序 appid 为前缀。

您可以将此文件复制到驱动程序的资源文件夹(通常是 input\bindings\),然后在输入配置文件的 default_bindings 节中注册该文件。

默认绑定

设备可以为应用程序提供一组默认绑定。

应提供绑定集合作为数组。每个绑定是 Steam 应用程序键和关联绑定文件名的配对。

可能有多个来源提供可能的绑定。系统将按以下顺序为应用程序选择绑定(编号较低的项目优先):

  1. 用户设置的绑定。
  2. 应用程序开发人员随应用程序一起发布的绑定。
  3. 在 partner.steamworks 网站上设置的绑定。
  4. 驱动程序编写者设置的默认绑定。这就是本文描述的内容。
  5. 目标兼容模式设备的绑定(如果启用了兼容模式)。请参见设备模拟。
  6. 使用重新映射布局转换的绑定(如果启用了自动重新绑定。)

默认绑定在输入配置文件的根对象中的 default_bindings 键下设置。

  • default_bindings - 对象数组。每个对象必须包含键:
    • app_key - 必需。命名绑定将应用到的应用程序。如果这是 Steam 应用程序,它将以 steam.app. 为前缀,后跟 appId。
    • binding_url - 必需。绑定配置文件的路径,相对于当前目录。
{
  "default_bindings": [
    {
      "app_key": "openvr.tool.steamvr_environments",
      "binding_url": "bindings/openvr.tool.steamvr_environments_my_controller.json"
    },
    {
      "app_key": "openvr.component.vrcompositor",
      "binding_url": "bindings/openvr.component.vrcompositor_my_controller.json"
    },
    {
      "app_key": "steam.app.546560",
      "binding_url": "bindings/steam.app.546560_my_controller.json"
    }
  ]
}

驱动程序输入

驱动程序可以更新其在输入配置文件中创建的输入组件。

IVRDriverInput 接口用于创建和更新与输入相关的组件。

创建组件

驱动程序应该为其输入配置文件中的所有输入组件创建句柄。驱动程序必须为其希望更新的输入组件创建句柄。然后可以使用这些句柄更新输入组件的状态。


virtual EVRInputError CreateBooleanComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle ) = 0;

CreateBooleanComponent 创建一个输入组件来表示控制器或其他追踪设备上的单个布尔值。

成功时,将 pHandle 指向的值设置为有效的组件句柄。

创建组件后,驱动程序可以通过重复调用 UpdateBooleanComponent 来更新它。

  • PropertyContainerHandle_t ulContainer - 作为此组件父级的设备的属性容器句柄。
  • const char *pchName - 组件的名称。所有名称都应采用 /input/<name>/<component> 的形式。
  • VRInputComponentHandle_t *pHandle - 指向要设置新组件句柄的句柄值的指针。

成功时返回 VRInputError_None。


virtual EVRInputError CreateScalarComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle, EVRScalarType eType, EVRScalarUnits eUnits ) = 0;

CreateScalarComponent 创建一个输入组件来表示控制器或其他追踪设备上的单个标量值。

成功时,将 pHandle 指向的值设置为有效的组件句柄。

创建组件后,驱动程序可以通过重复调用 UpdateScalarComponent 来更新它。

  • PropertyContainerHandle_t ulContainer - 作为此组件父级的设备的属性容器句柄。
  • const char *pchName - 组件的名称。所有名称都应采用 /input/<name>/<component> 的形式。
  • VRInputComponentHandle_t *pHandle - 指向要设置新组件句柄的句柄值的指针。
  • EVRScalarType eType - 此值使用的缩放类型。必须是以下之一:
    • VRScalarType_Absolute - 标量值使用绝对比例的值进行更新。摇杆、触控板和扳机都是绝对标量值的例子。
    • VRScalarType_Relative - 标量值使用自上次更新以来的增量值进行更新。鼠标和轨迹球是相对标量值的例子。
  • EVRScalarUnits eUnits - 指定标量值的测量单位。必须是以下之一:
    • VRScalarUnits_NormalizedOneSided - 标量值范围从 0 到 1(含)。扳机和油门通常使用此值。
    • VRScalarUnits_NormalizedTwoSided - 标量值范围从 -1 到 1(含)。摇杆和触控板通常使用此值。

成功时返回 VRInputError_None。


virtual EVRInputError CreateHapticComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle ) = 0

CreateHapticComponent 创建一个输出组件来表示控制器或其他追踪设备上的单个触觉反馈。成功时,将 pHandle 指向的值设置为有效的组件句柄。

当应用程序请求触觉事件时,驱动程序将收到类型为 VREvent_Input_HapticVibration 的事件,触觉事件的详细信息将在事件数据联合的 hapticVibration 成员中。

使用当前触觉 API 的应用程序必须仅以给定追踪设备上创建的第一个触觉组件为目标。

  • PropertyContainerHandle_t ulContainer - 作为此组件父级的设备的属性容器句柄。
  • const char *pchName - 组件的名称。所有名称都应采用 /input/<name>/<component> 的形式。
  • VRInputComponentHandle_t *pHandle - 指向要设置新组件句柄的句柄值的指针。

virtual EVRInputError CreateSkeletonComponent( PropertyContainerHandle_t ulContainer, const char *pchName, const char *pchSkeletonPath, const char *pchBasePosePath, EVRSkeletalTrackingLevel eSkeletalTrackingLevel, const VRBoneTransform_t *pGripLimitTransforms, uint32_t unGripLimitTransformCount, VRInputComponentHandle_t *pHandle ) = 0;

CreateSkeletonComponent 创建一个输入组件来表示控制器或追踪设备的手部骨骼数据。成功时返回并将 pHandle 指向的值设置为有效的组件句柄。

创建组件后,驱动程序可以通过重复调用 UpdateSkeletalComponent 来更新它。

有关使用此组件进行手部追踪的更多信息,请参见骨骼输入部分。

  • ulContainer - 作为此组件父级的设备的属性容器句柄。

  • pchName - 组件的名称。这必须设置为以下之一:

    • /input/skeleton/right - 右手的骨骼。
    • /input/skeleton/left - 左手的骨骼。
  • pchSkeletonPath - 要使用的骨骼的路径。这必须设置为以下之一:

    • /skeleton/hand/right - 右手的骨骼。
    • /skeleton/hand/left - 左手的骨骼。
  • pchBasePosePath - 控制器模型上骨骼应用作其原点的位置的路径。位置在渲染模型文件中设置。请参见 JSON 文件。例如 /pose/raw。

  • eSkeletalTrackingLevel - 此值让应用程序了解控制器跟踪用户身体位姿的能力。

    • VRSkeletalTracking_Estimated - 无法通过设备直接确定身体部位位置。设备提供的任何骨骼位姿都是通过假设激活按钮、扳机、摇杆或其他输入传感器所需的位置来估计的。例如 Vive 魔杖、游戏手柄。
    • VRSkeletalTracking_Partial - 可以测量身体部位位置,但自由度少于实际身体部位。某些身体部位位置可能未被设备测量,而是从其他输入数据估计的。例如 Index 控制器、仅测量手指弯曲的手套。
    • VRSkeletalTracking_Full - 可以在身体部位的整个运动范围内直接测量身体部位位置。例如动作捕捉套装、测量每个手指段旋转和(可选)张合的手套。
  • pGripLimitTransforms - vr::VRBoneTransform_t 数组,包含抓握限制位姿的父空间变换。大小应与 pchSkeletonPath 中指定的骨骼数量匹配。如果为 null,则系统将默认拳头位姿作为抓握限制。这应该是 31 个骨骼的数组。

  • unGripLimitTransformCount - pGripLimitTransforms 中的元素数量。这应该是 31。

  • pHandle - 指向新创建组件句柄应写入位置的指针。

更新组件

驱动程序应该在组件的值每次更改时更新它。驱动程序可以在值未更改时更新组件,这样做没有害处。


virtual EVRInputError UpdateBooleanComponent( VRInputComponentHandle_t ulComponent, bool bNewValue, double fTimeOffset ) = 0;

UpdateBooleanComponent 更新布尔组件的值。每当布尔输入组件的当前状态更改时,都应调用此方法。

  • VRInputComponentHandle_t ulComponent - 要更新的组件的组件句柄。
  • bool bNewValue - 组件的新布尔值。
  • double fTimeOffset - 组件中状态更改相对于现在的时间。负时间是过去,正时间是未来。此时间偏移应包括来自物理硬件的传输延迟。

virtual EVRInputError UpdateScalarComponent( VRInputComponentHandle_t ulComponent, float fNewValue, double fTimeOffset ) = 0;

UpdateScalarComponent 更新标量组件的值。每当输入组件的当前状态更改时,都应调用此方法。

  • VRInputComponentHandle_t ulComponent - 要更新的组件的组件句柄。
  • float fNewValue - 组件的新标量值。
  • double fTimeOffset - 组件中状态更改相对于现在的时间。负时间是过去,正时间是未来。此时间偏移应包括来自物理硬件的传输延迟。

virtual EVRInputError UpdateSkeletonComponent( VRInputComponentHandle_t ulComponent, EVRSkeletalMotionRange eMotionRange, const VRBoneTransform_t *pTransforms, uint32_t unTransformCount ) = 0;

UpdateSkeletonComponent 将骨骼组件的位姿更新为给定变换列表中的值。

有关使用此组件进行手部追踪的更多信息,请参见骨骼输入部分。

  • ulComponent - 要更新的骨骼组件的句柄。
  • eMotionRange - 您正在为哪个骨骼数据流提供数据。更多信息见下文。选项有:
    • VRSkeletalMotionRange_WithController - 骨骼的运动范围考虑控制器本身施加的任何物理限制。这往往是与用户实际手部位姿相比最准确的位姿,但可能不允许握紧拳头。
    • VRSkeletalMotionRange_WithoutController - 将输入设备提供的运动范围重新定位,使手看起来像是在没有握住控制器的情况下移动。例如:将"手握住控制器"映射到"握紧拳头"。
  • pTransforms - 用户当前检测到的手部姿势的父空间骨骼变换数组。这应该是 31 个骨骼的数组。
  • unTransformCount - pTransforms 中的变换数量。必须与此骨骼组件使用的骨骼数量匹配,否则将返回错误。这应该是 31。
//确保这些在需要更新组件时也可访问
vr::VRInputComponentHandle_t trackpadX;
vr::VRInputComponentHandle_t trackpadY;
vr::VRInputComponentHandle_t trackpadTouch;
vr::VRInputComponentHandle_t trackpadClick;

vr::VRDriverInput()->CreateScalarComponent(props, "/input/trackpad/x", &trackpadX, vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedTwoSided);
vr::VRDriverInput()->CreateScalarComponent(props, "/input/trackpad/y", &trackpadY, vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedTwoSided);
vr::VRDriverInput()->CreateBooleanComponent(props, "/input/trackpad/touch", &trackpadTouch);
vr::VRDriverInput()->CreateBooleanComponent(props, "/input/trackpad/click", &trackpadClick);

//... 稍后当硬件发送数据时

vr::VRDriverInput()->UpdateScalarComponent(trackpadX, myData.trackpadX, 0);
vr::VRDriverInput()->UpdateScalarComponent(trackpadY, myData.trackpadY, 0);

vr::VRDriverInput()->UpdateBooleanComponent(trackpadTouch, myData.trackpadTouch, 0);
vr::VRDriverInput()->UpdateBooleanComponent(trackpadClick, myData.trackpadClick, 0);

事件

由驱动程序决定何时从队列中拉取最新事件。通常,您会希望一次处理一帧内发生的所有事件,因此您的方法可能如下所示:

    vr::VREvent_t vrEvent;
while ( vr::VRServerDriverHost()->PollNextEvent( &vrEvent, sizeof( vrEvent )))
{
//以您希望的方式决定如何处理当前事件
}

驱动程序可能监听的常见事件:

  • VREvent_Input_HapticVibration - 当应用程序触发触觉事件时发送。更多信息请参见触觉事件部分。
  • VREvent_OtherSectionSettingChanged - 当非运行时指定的设置节中的节通过 IVRSettings 更改时发送。这可能会提示驱动程序重新加载其设置。

触觉事件

应用程序发送的触觉事件可以通过 IVRServerDriverHost::PollNextEvent 被驱动程序接收。

在事件类型 vr::EVREventType::VREvent_Input_HapticVibration 下接收触觉事件。

驱动程序必须然后检查 componentHandle 属性以获取事件所针对的组件。此句柄是从 IVRDriverInput::CreateHapticComponent 创建的。

触觉事件包含一些额外的属性:

  • fDurationSeconds - 触觉事件应持续的时长(以秒为单位)。
  • fFrequency - 触觉事件的频率。单位 Hz。
    • 从应用程序来看,这表示触觉振动的感觉。频率越低,设备"隆隆"感越强。
  • fAmplitude - 触觉事件的强度。振幅越高,触觉振动的强度越大。

驱动程序应该按以下方式处理属性:

  • 如果 fFrequency 或 fAmplitude 等于或小于 0,则不应触发触觉。
  • 将 fDurationSeconds 限制在最小 0 秒,最大 10 秒之间
    • 如果 fDurationSeconds 为 0,驱动程序应该脉冲其触觉组件一次。
    • 此值可能更改
  • 将 fAmplitude 限制在 0 到 1 之间。
  • 将 fFrequency 限制在最小 1000000.f / 65535.f 和最大 1000000.f / 300.f 之间
    • 此值可能更改。

触觉事件可以通过以下方式转换为表示脉冲:

  • 脉冲的周期(以秒为单位)可以通过 1.f / fFrequency 计算。
  • 脉冲持续时间可以通过在设定的最小脉冲持续时间和总脉冲持续时间的一半(或设定的最大值)之间,以 fAmplitude 为插值因子计算,取较小值。
  • 要触发的脉冲数量可以通过 fDurationSeconds * fFrequency 计算。如果 fDurationSeconds 为 0,则要触发的脉冲数量为 1。
    • 此值可能更改。
switch (vrEvent.eventType) {
  case vr::VREvent_Input_HapticVibration: {
    if (vrEvent.data.hapticVibration.componentHandle == m_compMyHaptic) {
      // 这是您将向硬件发送信号以触发实际触觉反馈的地方

      const float pulse_period = 1.f / vrEvent.data.hapticVibration.fFrequency
      const float frequency = std::clamp(1000000.f / 65535.f, 1000000.f / 300.f, pulse_period);
      const float amplitude = std::clamp(0.f, 1.f, vrEvent.data.hapticVibration.fAmplitude);
      const float duration = std::clamp(0.f, 10.f, vrEvent.data.hapticVibration.fDurationSeconds);

      if(duration == 0.f) {
        // 触发触觉组件的单次脉冲
      } else {
        const float pulse_count = fDurationSeconds * fFrequency;
        const float pulse_duration = Lerp(my_minimum_duration, my_maximum_duration, amplitude);
        const float pulse_interval = pulse_period - pulse_duration;
      }
    }
  }
  break;
}

设备属性

OpenVR 公开了一组属性,包含在 ETrackedDeviceProperty 中,这些属性向运行时提供有关设备的信息。

可以通过 IVRProperties 接口或更常见的 CVRPropertyHelpers(一个包装 IVRProperties 接口的帮助类)来检索和设置属性。

其中许多属性是从驱动程序先前对服务器的调用推断出来的,而其他属性必须或应该由设备手动设置。

由服务器定义的,因此不必由驱动程序设置的属性列表:

  • Prop_SerialNumber_String - 从对 IVRServerDriverHost::TrackedDeviceAdded 的调用推断。
  • Prop_TrackingSystemName_String - 从驱动程序名称推断。
  • Prop_DeviceClass_Int32 - 从对 IVRServerDriverHost::TrackedDeviceAdded 的调用推断。
  • Prop_HasDisplayComponent_Bool - HMD。从设备从其 IServerTrackedDeviceDriver::GetComponent 方法返回的内容推断。
  • Prop_HasCameraComponent_Bool - HMD。从设备从其 IServerTrackedDeviceDriver::GetComponent 方法返回的内容推断。
  • Prop_HasDriverDirectModeComponent_Bool - HMD。从设备从其 IServerTrackedDeviceDriver::GetComponent 方法返回的内容推断。
  • Prop_HasVirtualDisplayComponent_Bool - HMD。从设备从其 IServerTrackedDeviceDriver::GetComponent 方法返回的内容推断。
  • Prop_DisplayFrequency_Float - HMD。如果在调用 IServerTrackedDeviceDriver::Activate 后为 0,运行时将将此设置为显示器报告的刷新率。
  • Prop_SecondsFromVsyncToPhotons_Float - HMD。如果在调用 IServerTrackedDeviceDriver::Activate 后为 0,运行时将将此设置为显示器刷新率的倒数,这是大多数低余晖显示器的值。

控制器设备必须由设备设置的属性:

  • Prop_ControllerType_String - 表示控制器类型的字符串,例如 knuckles 或 vive_controller。
    • 控制器类型应该是 ASCII 且简短。这不是用户可读的名称,而是将存储在数据和日志文件中的内部名称。
  • Prop_InputProfilePath_String - 指向输入配置文件的字符串。

控制器的许多属性应该被设置。这些包括:

  • Prop_ControllerRoleHint_Int32 - 控制器的角色(左/右等)。这应该是 ETrackedControllerRole 中的一个值。
  • Prop_InputProfilePath_String - 控制器的输入配置文件的路径。这应该是控制器输入配置文件 JSON 文件的路径。
  • Prop_ManufacturerName_String - 此设备制造商的名称。
  • Prop_ModelNumber_String - 此设备的型号。这是一个可用于为设备设置图标的字符串,且不唯一。
  • Prop_DeviceProvidesBatteryStatus_Bool - 设备是否支持检索当前电池百分比。此属性决定了用户是否会在 VR 监视器和仪表板中看到电池图标。如果此属性设置为 true,则还应设置以下属性:
    • Prop_DeviceBatteryPercentage_Float - 当前电池百分比。这必须是 0-1(含)之间的值。每当设备的电池百分比更改时,应该更新此值。
    • Prop_DeviceIsCharging_Bool - 设备当前是否正在充电。当设备开始或停止充电时,应该更新此值。

实现属性

属性应该在设备的 IServerTrackedDeviceDriver::Activate 被调用时设置。

设备属性应该使用 OpenVR Driver 头文件提供的 CVRPropertyHelpers 类设置。此类通过调用 vr::VRProperties() 返回。

CVRPropertyHelpers 提供了围绕 IVRProperties 的包装器,具有简化向运行时提交不同属性类型的方法。

有关操作设备属性的推荐接口,请参见 CVRPropertyHelpers。

IVRProperties


virtual ETrackedPropertyError ReadPropertyBatch( PropertyContainerHandle_t ulContainerHandle, PropertyRead_t *pBatch, uint32_t unBatchEntryCount ) = 0;

ReadPropertyBatch 原子地读取一组属性。

  • ulContainerHandle - 要从中读取属性的容器的句柄。
  • pBatch - PropertyRead_t 结构体数组,每个结构体包含一个属性名称和要写入属性值的缓冲区。
    • ETrackedDeviceProperty prop - 要读取的属性。
    • void *pvBuffer - 要读取属性值的缓冲区。
    • uint32_t unBufferSize - 缓冲区的大小。
    • PropertyTypeTag_t unTag - 此属性中的"类型"。常用的标签如下所示,但头文件中定义了更多 PropertyTypeTag_t 常量。
      • k_unFloatPropertyTag - float 数据类型
      • k_unInt32PropertyTag - int32_t 数据类型
      • k_unUint64PropertyTag - uint64_t 数据类型
      • k_unBoolPropertyTag - bool 数据类型
      • k_unStringPropertyTag - char * 数据类型
    • uint32_t unRequiredBufferSize - 存储此数据所需的缓冲区大小。
    • ETrackedPropertyError eError - 运行时为此属性返回的错误代码。
  • unBatchEntryCount - pBatch 数组中的条目数。

virtual ETrackedPropertyError WritePropertyBatch( PropertyContainerHandle_t ulContainerHandle, PropertyWrite_t *pBatch, uint32_t unBatchEntryCount ) = 0;

WritePropertyBatch 原子地写入一组属性。

  • ulContainerHandle - 要写入属性的容器的句柄。
  • pBatch - PropertyWrite_t 结构体数组,每个结构体包含一个属性名称和要读取属性值的缓冲区。
    • ETrackedDeviceProperty prop - 要写入的属性。
    • EPropertyWriteType writeType - 正在执行的写入类型。可以是:
      • PropertWrite_Set - 将属性设置为 pvBuffer 中的值。
      • PropertyWrite_Erase - 删除属性中的值。
      • PropertyWrite_SetError - 将属性的错误返回值设置为 ETrackedPropertyError,并清除其数据(如果有)。
    • ETrackedPropertyError eSetError - 如果 writeType 是 PropertyWrite_SetError,则设置属性返回的错误代码。
    • void *pvBuffer - 要从中读取属性值的缓冲区。
    • uint32_t unBufferSize - 缓冲区的大小。
    • PropertyTypeTag_t unTag - 此属性中的"类型"。常用的标签如下所示,但头文件中定义了更多 PropertyTypeTag_t 常量。
      • k_unFloatPropertyTag - float 数据类型
      • k_unInt32PropertyTag - int32_t 数据类型
      • k_unUint64PropertyTag - uint64_t 数据类型
      • k_unBoolPropertyTag - bool 数据类型
      • k_unStringPropertyTag - char * 数据类型
    • ETrackedPropertyError eError - 运行时为此属性返回的错误代码。

virtual const char *GetPropErrorNameFromEnum( ETrackedPropertyError error ) = 0;

GetPropErrorNameFromEnum 返回与指定属性错误对应的字符串。

对于所有有效的错误代码,字符串将是错误枚举值的名称。

  • ETrackedPropertyError error - 要获取字符串的错误代码。

virtual PropertyContainerHandle_t TrackedDeviceToPropertyContainer( TrackedDeviceIndex_t nDevice ) = 0;

TrackedDeviceToPropertyContainer 返回指定追踪设备索引的属性容器句柄。

  • TrackedDeviceIndex_t nDevice - 要获取属性容器句柄的追踪设备索引。

CVRPropertyHelpers

此接口中的函数是围绕 IVRProperties 接口的包装器。它们提供了用于设置和获取属性的更简单接口,具有类型化方法。

获取属性

GetXXXProperty 方法可用于获取属性,其中 XXX 指定类型。

GetStringProperty 是多态的。驱动程序可以从此方法返回 std::string,或者传入 char * 和 uint32_t(缓冲区大小)以将属性值写入缓冲区。


T GetTProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError *pError = 0L );

GetTProperty 获取属性。

  • PropertyContainerHandle_t ulContainerHandle - 要从中读取属性的容器的句柄。
  • ETrackedDeviceProperty prop - 要获取的属性。
  • ETrackedPropertyError *pError - 运行时为此属性返回的错误代码。

以 T 类型返回属性的值。

//unObjectId 是您的设备的设备 id,从 ITrackedDeviceServerDriver::Activate 传入。
vr::PropertyContainerHandle_t ulPropertyContainer = vr::VRProperties()->TrackedDeviceToPropertyContainer( unObjectId );

vr::ETrackedPropertyError err;

std::string my_device_serial_number = vr::VRProperties()->GetStringProperty(my_device_container_handle, vr::Prop_SerialNumber_String, &err);

float my_device_battery_percentage = vr::VRProperties()->GetFloatProperty(my_device_container_handle, vr::Prop_DeviceBatteryPercentage_Float, &err);

bool my_device_is_charging = vr::VRProperties()->GetBoolProperty(my_device_container_handle, vr::Prop_DeviceIsCharging_Bool, &err);

ETrackedDeviceClass my_device_class = vr::VRProperties()->GetInt32Property(my_device_container_handle, vr::Prop_DeviceClass_Int32, &err);

设置属性

ETrackedPropertyError SetTProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, T tNewValue );

SetTProperty 将属性设置为指定值。

  • PropertyContainerHandle_t ulContainerHandle - 要从中读取属性的容器的句柄。
  • ETrackedDeviceProperty prop - 要设置的属性。
  • T tNewValue - 要将属性设置的值。
//unObjectId 是您的设备的设备 id,从 ITrackedDeviceServerDriver::Activate 传入。
vr::PropertyContainerHandle_t ulPropertyContainer = vr::VRProperties()->TrackedDeviceToPropertyContainer( unObjectId );


vr::VRProperties()->SetStringProperty( ulPropertyContainer, vr::Prop_ModelNumber_String, m_sModelNumber.c_str());
vr::VRProperties()->SetStringProperty( ulPropertyContainer, vr::Prop_RenderModelName_String, m_sModelNumber.c_str());

// 返回一个不是 0(无效)或 1(为 Oculus 保留)的常量
vr::VRProperties()->SetUint64Property( ulPropertyContainer, vr::Prop_CurrentUniverseId_Uint64, 2 );

// 避免来自 vrmonitor 的"非全屏"警告
vr::VRProperties()->SetBoolProperty( ulPropertyContainer, vr::Prop_IsOnDesktop_Bool, false );

// 我们的示例设备实际上并未被追踪,因此设置此属性以避免状态窗口中的图标闪烁
vr::VRProperties()->SetBoolProperty( ulPropertyContainer, vr::Prop_NeverTracked_Bool, true );

// 将设备设置为右手控制器。这也将允许使用绑定
vr::VRProperties()->SetInt32Property( ulPropertyContainer, vr::Prop_ControllerRoleHint_Int32, vr::TrackedControllerRole_RightHand );

// 此文件告诉 UI 向用户显示什么来绑定此控制器,以及对于旧版或其他应用程序的默认绑定应该是什么
vr::VRProperties()->SetStringProperty( m_ulPropertyContainer, vr::Prop_InputProfilePath_String, "{sample}/input/mycontroller_profile.json" );

属性实用工具

提供了一些额外的实用工具来帮助操作属性。


ETrackedPropertyError SetPropertyError( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError eError );

SetPropertyError 设置属性的错误返回值。此值将在所有后续获取属性的请求中返回,并将清除属性的当前值。

  • PropertyContainerHandle_t ulContainerHandle - 要从中删除属性的容器的句柄。
  • ETrackedDeviceProperty prop - 要删除的属性。
  • ETrackedPropertyError eError - 为此属性设置的错误代码。

ETrackedPropertyError EraseProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop );

EraseProperty 清除为属性设置的任何值或错误。

  • PropertyContainerHandle_t ulContainerHandle - 要从中删除属性的容器的句柄。
  • ETrackedDeviceProperty prop - 要删除的属性。

bool IsPropertySet( PropertyContainerHandle_t ulContainer, ETrackedDeviceProperty prop, ETrackedPropertyError *peError = nullptr );

IsPropertySet 如果指定容器上设置了指定属性,则返回 true。

  • PropertyContainerHandle_t ulContainer - 要检查是否设置属性的容器的句柄。
  • ETrackedDeviceProperty prop - 要检查是否设置的属性。

IVRSettings

IVRSettings 接口用于获取和设置设置。所有设置都是全局的,可以通过此接口访问。

SteamVR 维护一个列出用户指定设置的文件,位于 C:\Program Files (x86)\Steam\config 内的 steamvr.vrsettings 文件中。

IVRSettings 首先在 steamvr.vrsettings 中查找设置。如果找到请求的设置,则返回它。如果未找到设置,则在驱动程序的 default.vrsettings 中查找设置。

存储在 steamvr.vrsettings 中的设置在 Steam 云中在计算机之间持久保存。

然后,驱动程序可以通过 IVRSettings::GetXXX() 方法访问这些设置。

char my_setting_string[4096];
vr::VRSettings()->GetString( "<my_driver_name>_section", "my_driver_settings_string_key", buf, sizeof( buf ) );

float my_setting_float = vr::VRSettings()->GetFloat( "<my_driver_name>_section", "my_driver_settings_float_key" );

int32_t my_setting_int = vr::VRSettings()->GetInt32( "<my_driver_name>_section", "my_driver_settings_int_key" );

bool my_setting_bool = vr::VRSettings()->GetBool( "<my_driver_name>_section", "my_driver_settings_bool_key" );

可以通过 IVRSettings::SetXXX() 方法设置设置。

通过这些方法设置的键的值将写入 steamvr.vrsettings,而 default.vrsettings 保持不变。

vr::VRSettings()->SetString( "<my_driver_name>_section", "my_driver_settings_string_key", "Hi World!" );
vr::VRSettings()->SetFloat( "<my_driver_name>_section", "my_driver_settings_float_key", 2.0 );
vr::VRSettings()->SetInt32( "<my_driver_name>_section", "my_driver_settings_int_key", 2 );
vr::VRSettings()->SetBool( "<my_driver_name>_section", "my_driver_settings_bool_key", true );

更新节将触发可以通过 IVRServerDriverHost 接口接收的事件。

当设置更新时,它将触发一个事件。可能的事件是:

VREvent_BackgroundSettingHasChanged
VREvent_CameraSettingsHaveChanged
VREvent_ReprojectionSettingHasChanged
VREvent_ModelSkinSettingsHaveChanged
VREvent_EnvironmentSettingsHaveChanged
VREvent_PowerSettingsHaveChanged
VREvent_EnableHomeAppSettingsHaveChanged
VREvent_SteamVRSectionSettingChanged
VREvent_LighthouseSectionSettingChanged
VREvent_NullSectionSettingChanged
VREvent_UserInterfaceSectionSettingChanged
VREvent_NotificationsSectionSettingChanged
VREvent_KeyboardSectionSettingChanged
VREvent_PerfSectionSettingChanged
VREvent_DashboardSectionSettingChanged
VREvent_WebInterfaceSectionSettingChanged
VREvent_TrackersSectionSettingChanged
VREvent_LastKnownSectionSettingChanged
VREvent_DismissedWarningsSectionSettingChanged
VREvent_GpuSpeedSectionSettingChanged
VREvent_WindowsMRSectionSettingChanged
VREvent_OtherSectionSettingChanged

如果您修改了自己的驱动程序属性,它将触发 VREvent_OtherSectionSettingChanged 事件。列出的其他节是 SteamVR 内部用于其自身设置的保留节。

IVRResources

IVRResources 提供来自运行时的功能,用于定位和加载驱动程序的文件。


virtual uint32_t LoadSharedResource( const char *pchResourceName, char *pchBuffer, uint32_t unBufferLen ) = 0;

LoadSharedResource 将指定的共享资源加载到提供的缓冲区中(如果足够大)。

共享资源存储在 SteamVR 内。

返回容纳指定资源所需的缓冲区大小(以字节为单位)。

  • const char *pchResourceName - 要加载的资源名称。
  • char *pchBuffer - 要加载资源到的缓冲区。
  • uint32_t unBufferLen - 缓冲区的大小。

virtual uint32_t GetResourceFullPath( const char *pchResourceName, const char *pchResourceTypeDirectory, VR_OUT_STRING() char *pchPathBuffer, uint32_t unBufferLen ) = 0;

GetResourceFullPath 从特定资源名称和目录解析绝对路径到缓冲区。

在共享库中获取当前目录可能很困难,因此此函数可以帮助定位驱动程序相对资源。

返回容纳指定资源所需的缓冲区大小(以字节为单位)。

驱动程序特定的文件可以从此方法加载。

  • const char *pchResourceName - 要加载的资源名称。
  • const char *pchResourceTypeDirectory - 要从中加载资源的目录。这可以是驱动程序的命名目录,例如 {sample}/resources。
  • VR_OUT_STRING() char *pchPathBuffer - 资源的绝对路径。
  • uint32_t unBufferLen - 提供的缓冲区的长度(以字节为单位)。

IVRDriverSpatialAnchors

IVRDriverSpatialAnchors 提供了一个接口,供驱动程序与空间锚点交互:驱动程序指定的物理位置描述符。

注意:目前没有驱动程序实现此接口,应该仅用于原型设计。

您必须在 driver.vrdrivermanifest 中声明对空间锚点的支持。请参见 driver.vrdrivermanifest。

驱动程序应该监视 VREvent_SpatialAnchors_RequestPoseUpdate 事件(针对来自需要 UpdateSpatialAnchorPose 的应用程序的新描述符)和 VREvent_SpatialAnchors_RequestDescriptorUpdate 事件(针对来自需要 UpdateSpatialAnchorDescriptor 的新位姿)。

对于随时间推移的自动位姿更新,驱动程序应该跟踪它见过的句柄,并在条件发生变化时提供更新。


virtual EVRSpatialAnchorError UpdateSpatialAnchorPose( SpatialAnchorHandle_t unHandle, const SpatialAnchorDriverPose_t *pPose ) = 0;

UpdateSpatialAnchorPose 更新空间锚点的位姿。

当事件通知驱动程序应用程序已注册新描述符时,应该调用 UpdateSpatialAnchorPose。

每当驱动程序状态发生变化,改变物理世界位置如何映射到虚拟坐标(例如,任何会导致宇宙 ID 更改的情况)时,应该对所有活动句柄调用 UpdateSpatialAnchorPose。

当驱动程序对表示锚点的最佳虚拟坐标有更好的信息时,可以为任何锚点调用 UpdateSpatialAnchorPose。

UpdateSpatialAnchorPose 在第一次调用时会触发一个事件(以提醒提交描述符的人)。

返回 EVRSpatialAnchorError,希望是 VRSpatialAnchorError_Success。

  • SpatialAnchorHandle_t unHandle - 要更新的锚点的句柄。
  • const SpatialAnchorDriverPose_t *pPose - 锚点的新位姿。
    • vr::HmdQuaternion_t qWorldRotation - 锚点在世界空间中的方向,由 DriverPose_t 提供。右手坐标系。
    • vr::HmdVector3d_t vWorldTranslation - 锚点在世界空间中的平移,由 DriverPose_t 提供。右手坐标系。
    • uint64_t ulRequiredUniverseId - 空间锚点所在的宇宙。这应该与设备属性 Prop_CurrentUniverseId_Uint64 中提供的宇宙相同。注意:0 无效,1 为 Oculus 保留作为宇宙 ID。
    • double fValidDuration - 空间锚点位姿有效的持续时间(以秒为单位)。这可以用于让驱动程序无需跟踪空间锚点位姿有效的持续时间。当时间到期时,运行时在应用程序读取位姿时将开始生成 VREvent_SpatialAnchors_RequestPoseUpdate,让驱动程序知道仍然值得更新。
      • 值 -1 导致永远不会收到此位姿的更新请求。驱动程序仍然可以随时更新此锚点的位姿。
      • 值 0 导致在每次读取位姿后收到更新请求。
        • 如果应用程序以帧率获取位姿,这可能非常高。
      • 如果驱动程序知道在一段时间内没有理由更新位姿,它可以在此处设置该时间,并在稍后收到更新请求提醒。
      • 如果驱动程序计划在一段时间内自动更新此位姿(随着它获得有关此锚点虚拟位置的更好信息),它可以在此处设置该持续时间以指示不需要"请求更新"提醒。当该自动更新周期到期时,任何对该位姿的未来兴趣将通过位姿更新请求指示。
      • 驱动程序可以随时更新位姿,包括在有效持续时间内。

virtual EVRSpatialAnchorError SetSpatialAnchorPoseError( SpatialAnchorHandle_t unHandle, EVRSpatialAnchorError eError, double fValidDuration ) = 0;

SetSpatialAnchorPoseError 使与句柄关联的任何位姿无效,并导致未来对 GetSpatialAnchorPose 的调用(在客户端和驱动程序端)返回指定的错误。

  • SpatialAnchorHandle_t unHandle - 要更新的锚点的句柄。
  • EVRSpatialAnchorError eError - 为锚点返回的错误。必须是以下之一:
    • VRSpatialAnchorError_NotYetAvailable
    • VRSpatialAnchorError_NotAvailableInThisUniverse
    • VRSpatialAnchorError_PermanentlyUnavailable
  • double fValidDuration - 此错误有效的时长(以秒为单位)。有关更多详细信息,请参见上面 fValidDuration 的定义。

virtual EVRSpatialAnchorError UpdateSpatialAnchorDescriptor( SpatialAnchorHandle_t unHandle, const char *pchDescriptor ) = 0;

UpdateSpatialAnchorDescriptor 更新空间锚点的描述符。

当事件通知驱动程序应用程序已注册新位姿时,应该调用 UpdateSpatialAnchorDescriptor。

当驱动程序有更好的或额外的信息要包含在锚点描述符中时,可以为任何锚点调用 UpdateSpatialAnchorDescriptor。

但请注意,应用程序可能永远不会获取更新的锚点描述符,并且可能在忽略更新后的未来会话中请求原始描述符。

提供的描述符应该是仅驱动程序的不透明的内部数据,而不是客户端使用的修饰形式(由运行时元数据包装)。

每次调用 UpdateSpatialAnchorDescriptor 时将触发一个事件。

返回 EVRSpatialAnchorError,希望是 VRSpatialAnchorError_Success。

  • SpatialAnchorHandle_t unHandle - 要更新的锚点的句柄。
  • const char *pchDescriptor - 锚点的新描述符。描述符不能包含非 ASCII 字符或两个特殊字符 ~ 或 "。

virtual EVRSpatialAnchorError GetSpatialAnchorPose( SpatialAnchorHandle_t unHandle, SpatialAnchorDriverPose_t *pDriverPoseOut ) = 0;

GetSpatialAnchorPose 获取空间锚点的位姿。

  • SpatialAnchorHandle_t unHandle - 要获取的锚点的句柄。
  • SpatialAnchorDriverPose_t *pDriverPoseOut - 用锚点的位姿填充。有关位姿数据的详细信息,请参见 UpdateSpatialAnchorPose。

virtual EVRSpatialAnchorError GetSpatialAnchorDescriptor( SpatialAnchorHandle_t unHandle, VR_OUT_STRING() char *pchDescriptorOut, uint32_t *punDescriptorBufferLenInOut, bool bDecorated ) = 0;

GetSpatialAnchorDescriptor 获取给定空间锚点句柄的空间锚点描述符。

对于驱动程序尚未构建描述符的句柄,GetSpatialAnchorDescriptor 返回 VRSpatialAnchorError_NotYetAvailable。

返回的描述符将是先前保存的锚点(应用程序正在请求位姿)的应用程序提供的描述符。

如果驱动程序已在此会话中调用了 UpdateSpatialAnchorDescriptor,则将是驱动程序提供的描述符。

  • SpatialAnchorHandle_t unHandle - 要获取的锚点的句柄。
  • VR_OUT_STRING() char *pchDescriptorOut - 用锚点的描述符填充。
  • uint32_t *punDescriptorBufferLenInOut - 写入 pchDescriptorOut 的字节数。
  • bool bDecorated - 如果为 true,返回的描述符将是客户端使用的修饰形式(由运行时元数据包装)。否则,如果为 false,返回的描述符将是驱动程序的不透明的内部数据。

位姿

位姿包含设备在给定时刻在空间中的状态。它包含位置、旋转、速度和角速度等属性,以及一些额外的信息,描述设备如何进行追踪,以及它是否认为其提供的位姿实际上是有效的。驱动程序提供的位姿由 vr::DriverPose_t 结构体给出。

运行时使用速度来对位姿应用预测。

OpenVR 使用右手坐标系,其中 X+ 指向右,Y+ 指向上,Z+ 轴指向后方。vecPosition、vecVelocity、vecAcceleration、vecAngularVelocity 和 vecAngularAcceleration 必须在此空间中,要么直接,要么通过设置 qWorldFromDriverRotation 和 qDriverFromHeadRotation 来解释变换。

struct DriverPose_t
{
    double poseTimeOffset;

    vr::HmdQuaternion_t qWorldFromDriverRotation;
    double vecWorldFromDriverTranslation[ 3 ];

    vr::HmdQuaternion_t qDriverFromHeadRotation;
    double vecDriverFromHeadTranslation[ 3 ];

    double vecPosition[ 3 ];
    double vecVelocity[ 3 ];
    double vecAcceleration[ 3 ];

    vr::HmdQuaternion_t qRotation;

    double vecAngularVelocity[ 3 ];
    double vecAngularAcceleration[ 3 ];

    ETrackingResult result;

    bool poseIsValid;
    bool willDriftInYaw;
    bool shouldApplyHeadModel;
    bool deviceIsConnected;
};
  • double poseTimeOffset - 此位姿的时间偏移(以秒为单位),相对于向运行时提交位姿的实际时间。此属性在 SteamVR 内部用于估计设备当前在空间中的位置。
  • vr::HmdQuaternion_t qWorldFromDriverRotation - 见下文。即使驱动程序不需要使用此变换,这也必须是一个有效的四元数(w=1.0)。
  • double vecWorldFromDriverTranslation[ 3 ] - 从驱动程序的坐标空间到世界坐标空间的变换。此变换在位姿被运行时使用之前应用于位姿。这对于希望在世界坐标空间之外的不同坐标空间中提供位姿的驱动程序很有用。
  • vr::HmdQuaternion_t qDriverFromHeadRotation - 见下文。即使驱动程序不需要使用此变换,这也必须是一个有效的四元数(w=1.0)。
  • double vecDriverFromHeadTranslation[ 3 ]; - 从驱动程序的坐标空间到头坐标空间的变换。驱动程序通常不追踪"头部"位置,而是追踪 HMD 内的内部 IMU 或另一个参考点。
    以下两个变换将位置和方向从驱动程序世界空间转换到应用程序世界空间,以及从驱动程序本地身体空间转换到 HMD 头部空间。
  • double vecPosition[ 3 ]; - 设备的追踪参考点在驱动程序世界空间中的位置(以米为单位)。
  • double vecVelocity[ 3 ]; - 设备的追踪参考点在驱动程序世界空间中的速度(以米每秒为单位)。
    • 用于预测应用程序请求设备位姿时的位姿。
  • double vecAcceleration[ 3 ]; - 设备的追踪参考点在驱动程序世界空间中的加速度(以 m/s^2 为单位)。
    • 当前未被 SteamVR 使用。此成员的值对位姿没有影响。
  • vr::HmdQuaternion_t qRotation; - 设备的追踪参考点在驱动程序世界空间中的方向,表示为四元数。
  • double vecAngularVelocity[ 3 ]; - 设备的追踪参考点在驱动程序世界空间中的角速度(以弧度每秒为单位)。
    • 用于预测应用程序请求设备位姿时的位姿。
  • double vecAngularAcceleration[ 3 ]; - 设备的追踪参考点在驱动程序世界空间中的角加速度(以 rad/s^2 为单位)。
    • 当前未被 SteamVR 使用。此成员的值对位姿没有影响。
  • ETrackingResult result - 位姿的追踪结果。运行时使用此来确定位姿是否有效可以显示,以及向用户显示什么图标。
    • TrackingResult_Uninitialized - 位姿无效,不应使用。
    • TrackingResult_Calibrating_InProgress - 设备仍在校准,尚未完全准备好进行追踪。
    • TrackingResult_Calibrating_OutOfRange - 位姿有效,但设备仍在校准(或尚未完全连接),不应使用。如果这是 HMD,将显示灰色屏幕。
    • TrackingResult_Running_OK - 位姿有效,且设备已校准。
    • TrackingResult_Running_OutOfRange - 设备已连接,但位姿有问题。如果这是 HMD,将显示灰色屏幕。
    • TrackingResult_Fallback_RotationOnly - 位姿有效,但设备超出范围,不应使用。
  • bool poseIsValid; - 位姿是否有效。运行时使用此来确定位姿是否有效可以显示,以及向用户显示什么图标。
  • bool willDriftInYaw - 此设置不被 vrserver 使用,可以忽略。
  • bool shouldApplyHeadModel - 运行时是否应在屏幕和头部旋转中心(颈部)之间应用平移。对于重投影很有用。
  • bool deviceIsConnected - 设备是否已连接。运行时使用此来确定位姿是否有效可以显示,以及向用户显示什么图标。

由驱动程序来更新其设备的位姿。运行时将外推提交时间后 100 毫秒,但驱动程序应该尽可能频繁地更新其设备的位姿,但至少每帧一次。运行时不会外推超过 100 毫秒,如果设备在该时间内未更新其位姿,运行时将使位姿无效。

当设备希望更新其位姿时,应调用 vr::VRServerDriverHost()->TrackedDevicePoseUpdated(参见 IVRServerDriverHost)并填充其位姿的结构体:

vr::DriverPose_t myPose;

// ... 填充 myPose ...

vr::VRServerDriverHost()->TrackedDevicePoseUpdated( myDeviceId, myPose, sizeof( vr::DriverPose_t ));

也可以获取其他设备的位姿。vr::VRServerDriverHost()->GetRawTrackedDevicePoses 为驱动程序提供对设备位姿的访问。位姿位于其"原始"追踪空间中,该空间由每个为其设备提供位姿的驱动程序唯一定义。此函数的客户端负责关联不同驱动程序之间的位姿。位姿按其设备 id 索引,可以通过 IVRProperties 查找其关联的驱动程序和其他属性。GetRawTrackedDevicePoses 返回从 id 0(始终是 HMD)到特定计数的位姿数组。有关此方法的更多信息,请参见 IVRServerDriverHost。

骨骼输入

如果您正在为能够检测用户手部动作(无论是隐式还是显式)的设备开发 OpenVR 驱动程序,您可以通过 IVRDriverInput API 将该信息作为骨骼动画流提供给应用程序。

关于手部追踪兼容性的说明

在此接口发布之前,有一些游戏和应用程序创建,因此提供了它们自己的手部追踪实现。这些实现通常倾向于将手指弯曲绑定到输入配置文件中指定的输入组件,而不是使用骨骼输入系统。此外,这些游戏也往往在 SteamVR 中硬编码检查特定的控制器类型,以将手指弯曲绑定到其手部姿势系统。

虽然解决方案是使用 knuckles 设置为控制器类型来暴露您自己的设备,但这不推荐。这样做意味着您的设备仅限于 Index 控制器的输入,并且用户将在 SteamVR 绑定中看到 Index 控制器的绑定配置文件。

驱动程序应该实现的解决方案是通过 SteamVR 模拟来模拟控制器。这会更改各种属性,例如控制器类型和渲染模型名称,应用程序可能使用这些属性来为控制器启用特定功能,同时不影响运行时内的任何内容,例如绑定配置。请参见设备模拟。

骨骼

手部骨骼由31块骨骼组成,结构化为 vr::VRBoneTransform_t 数组。每块骨骼包含一个位置(HmdVector4_t)和方向(HmdQuaternionf_t),位于骨骼层次结构中其父骨骼的空间中。

骨骼的根骨骼放置在驱动程序提供的原始位姿处。驱动程序应该将根骨骼保持为单位变换,然后通过骨骼的腕骨(索引 1)提供将骨骼定位在正确位置所需的偏移量。

单位和坐标系

来自骨骼输入 API 的骨骼数据与 SteamVR 的其他部分一样,采用右手坐标系。

手的绑定姿势是手指向前(-Z),手掌向内(右手为 -X,左手为 +X)。


骨骼结构

手部骨骼由31块骨骼组成,结构化为 vr::VRBoneTransform_t 数组。每块骨骼包含一个位置(HmdVector4_t)和方向(HmdQuaternionf_t),位于骨骼层次结构中其父骨骼的空间中。

为了方便,这里有一个定义骨骼索引的枚举:

typedef int32_t BoneIndex_t;
const BoneIndex_t INVALID_BONEINDEX = -1;
enum HandSkeletonBone : BoneIndex_t
{
    eBone_Root = 0,
    eBone_Wrist,
    eBone_Thumb0,
    eBone_Thumb1,
    eBone_Thumb2,
    eBone_Thumb3,
    eBone_IndexFinger0,
    eBone_IndexFinger1,
    eBone_IndexFinger2,
    eBone_IndexFinger3,
    eBone_IndexFinger4,
    eBone_MiddleFinger0,
    eBone_MiddleFinger1,
    eBone_MiddleFinger2,
    eBone_MiddleFinger3,
    eBone_MiddleFinger4,
    eBone_RingFinger0,
    eBone_RingFinger1,
    eBone_RingFinger2,
    eBone_RingFinger3,
    eBone_RingFinger4,
    eBone_PinkyFinger0,
    eBone_PinkyFinger1,
    eBone_PinkyFinger2,
    eBone_PinkyFinger3,
    eBone_PinkyFinger4,
    eBone_Aux_Thumb,
    eBone_Aux_IndexFinger,
    eBone_Aux_MiddleFinger,
    eBone_Aux_RingFinger,
    eBone_Aux_PinkyFinger,
    eBone_Count
};

原点是根骨骼(索引 0)的位置。Finger0 骨骼相对于腕骨,而腕骨又相对于根骨。Finger1 骨骼相对于 Finger0 骨骼,依此类推。

关于骨骼的说明

骨骼输入 API 设计用于与常见的行业工具(如 Maya)一起使用,以便更轻松地将内容从 3D 编辑器移动到 VR 中。

FBX 处理转换到不同坐标系的方式是变换根骨骼(腕骨),然后反向变换根的子骨骼以解释根的变化,但保持其余骨骼的本地坐标系不变。

这意味着,如果尝试以编程方式构建骨骼,掌骨将相对于腕骨旋转 90 度。

双手之间的"上"轴也是翻转的。这旨在帮助为这些骨骼创建动画的艺术家,因为这意味着对双手应用相同的旋转将导致它们以镜像方式旋转,而不是在一只手上正确旋转"向内",在另一只手上旋转"向外"。

使用骨骼输入

下面提供了一个创建骨骼输入组件的示例。有关 IVRDriverInput::CreateSkeletonComponent 的文档,请参见创建组件。

// 您创建的用于包含设备属性的对象
vr::PropertyContainerHandle_t ulPropertyContainer = ...;

// 从您计划使用的骨骼规格中获取骨骼数量
const uint32_t nBoneCount = 31;

// 为您的设备适当地创建抓握限制位姿
vr::VRBoneTransform_t gripLimitTransforms[nBoneCount];
YourCreateGripLimitFunction(gripLimitTransforms, nBoneCount);

// 选择组件名称和骨骼路径
const char* pComponentName;
const char* pSkeletonPath;
if ( IsLeftHand())
{
pComponentName = "/input/skeleton/left";
pSkeletonPath = "/skeleton/hand/left";
}
else
{
pComponentName = "/input/skeleton/right";
pSkeletonPath = "/skeleton/hand/right";
}

// 选择控制器上作为骨骼原点的定位器。这些在渲染模型文件中定义。
// 如果您的实现涉及艺术家创建的位姿,请确保它们是相对于此位置制作的
const char* pBasePosePath = "/pose/raw";

// 创建骨骼组件并保存句柄供以后使用
vr::VRInputComponentHandle_t ulSkeletalComponentHandle;
vr::EVRInputError err = vr::VRDriverInput()->CreateSkeletonComponent( ulPropertyContainer, pComponentName, pSkeletonPath, pBasePosePath, gripLimitTransforms, nBoneCount, &ulSkeletalComponentHandle);
if ( err != vr::VRInputError_None )
{
// 处理失败情况
DriverLog( "CreateSkeletonComponent failed.  Error: %i\n", err );
}

一旦驱动程序创建了骨骼输入组件,驱动程序必须在合理短的时间范围内(最好在创建组件后立即)更新组件,否则 SteamVR 将假定设备上的骨骼输入不活动。

驱动程序应该定期更新骨骼位姿,并且应该以目标显示刷新率更新位姿,以确保动画流畅。这应该是 ~90Hz。如果设备更新手部追踪的速度低于此值,驱动程序应该在更新之间进行插值以确保动画流畅。

驱动程序应该同时更新两个骨骼:VRSkeletalMotionRange_WithController 和 VRSkeletalMotionRange_WithoutController。应用程序可以选择两个骨骼中的哪一个来显示手部。VRSkeletalMotionRange_WithController 显示带有控制器渲染模型的骨骼,VRSkeletalMotionRange_WithoutController 显示表示用户未使用控制器时的骨骼。

vr::VRBoneTransform_t boneTransforms[nBoneCount];

// 执行任何必要的逻辑,将设备输入转换为骨骼位姿,
// 首先创建"带控制器"的位姿,尽可能接近用户真实手部的位姿
GetMyAmazingDeviceAnimation(boneTransforms, nBoneCount);

// 然后使用这些变换更新组件上的 WithController 位姿
vr::EVRInputError err = pDriverInput->UpdateSkeletonComponent( m_ulSkeletalComponentHandle, vr::VRSkeletalMotionRange_WithController, boneTransforms, nBoneCount );
if ( err != vr::VRInputError_None )
{
    // 处理失败情况
    DriverLog( "UpdateSkeletonComponentfailed.  Error: %i\n", err );
}

// 然后创建"无控制器"的位姿,将控制器检测到的运动范围映射到用户手部空着时的完整运动范围。注意:对于某些设备,这与上一个位姿相同
GetMyAmazingEmptyHandAnimation(boneTransforms, nBoneCount);

// 然后更新组件上的 WithoutController 位姿
err = pDriverInput->UpdateSkeletonComponent( m_ulSkeletalComponentHandle, vr::VRSkeletalMotionRange_WithoutController, boneTransforms, nBoneCount );
err = pDriverInput->UpdateSkeletonComponent( m_ulSkeletalComponentHandle, vr::VRSkeletalMotionRange_WithController, boneTransforms, nBoneCount );
if ( err != vr::VRInputError_None )
{
    // 处理失败情况
    DriverLog( "UpdateSkeletonComponentfailed.  Error: %i\n", err );
}

创建手部动画

Valve 为设备制作的驱动程序引用了多个 glb 文件,用于不同的动画,例如手指弯曲或触控板移动。这些允许对控制器输入做出反应的精细动画。
然而,即使是在开放位姿的一组骨骼和闭合位姿(也可以是抓握位姿)的一组骨骼之间进行线性插值,也足以创建简单的弯曲动画。

一种以编程方式计算关节位置和方向来实现骨骼输入的方法可以在 samples/drivers/handskeletonsimulation 中找到。

应用程序兼容性

一些应用程序是针对特定控制器集构建的。它们可能根据正在使用的控制器或 HMD 的类型启用或禁用功能,或者直接崩溃。我们提供了一系列有效的解决方案,以在保持控制器独特性的同时,也与 SteamVR 游戏兼容。

举例来说:一个 SteamVR 输入应用程序可能有一个模拟握持动作和一个数字握持动作,仅对已知具有模拟握持组件的控制器使用模拟动作。如果应用程序编程时未考虑您的控制器类型,它可能不会检查您绑定的动作,甚至可能完全崩溃。这在骨骼输入中也很常见,一些应用程序仅在认为连接了 Index 控制器时才检查骨骼数据。

SteamVR 支持三种不同类型的输入,应用程序可能检查大约十几个不同的属性来确定正在使用的控制器类型。仅仅使用通用控制器的 controller_type 不仅会破坏许多应用程序,还会导致 SteamVR 本身的各种问题。

一些需要模拟才能利用某些功能的知名应用程序示例:

  • VRChat - 需要 knuckles 控制器类型来启用骨骼输入。
  • Boneworks - 需要 knuckles 控制器类型来启用骨骼输入。
  • Bonelab - 需要 valve/index_controller 交互配置文件来启用骨骼输入。
  • Skyrim VR - 检查其支持的设备渲染模型。
  • Vacation Simulator - 检查 HMD 型号名称。
  • Google Earth VR - 检查其支持的设备型号名称。

为了确保与您的控制器的最大兼容性,您应该采取三个单独的步骤。

  • 为 SteamVR Input 和 OpenXR 应用程序设置自动重新绑定文件。
  • 为重要游戏创建绑定文件(必要时包括模拟)。默认绑定
  • [可选] 在默认旧版绑定文件中设置模拟选项。旧版绑定模拟

有关如何模拟设备的信息,请参见在绑定中模拟设备。

自动重新绑定

此功能仅适用于 SteamVR v1.26 及更高版本。在早期版本中,重新映射文件将被忽略。

如果应用程序未为您的设备提供绑定,SteamVR 将尝试使用重新映射文件将现有绑定转换为与您的设备配合使用。这适用于 SteamVR Input 和 OpenXR 应用程序。

转换绑定时,SteamVR 将查找应用程序已有绑定的最高优先级 layout。然后它读取绑定配置文件并评估每个绑定。对于每个绑定,它将检查是否有匹配的 autoremapping 或 remapping 并应用转换。如果该绑定没有匹配项,它将直接逐字复制。

重新映射 json 有一个 layouts 数组,然后有一个 autoremappings 数组和/或一个 remappings 数组。这些数组列出了如何从一个绑定映射到另一个绑定的指令。当 autoremappings 和 remappings 一起使用时,它们必须共同指定两个控制器之间的所有差异。例如,如果您从 knuckles 控制器类型重新映射,它具有带有触摸传感器的 A/B 按钮。如果您映射到具有这些按钮但没有触摸传感器的控制器,那么您需要提供 autoremapping 或一组 remappings 来将触摸绑定转换为点击绑定。您可以使用 remappings 显式执行此操作,但我们建议尽可能使用 autoremappings,因为 SteamVR 会为您确定所有绑定场景。

autoremappings 对象只指定要映射自的组件和要映射到的组件。SteamVR 将通过查找各自输入配置文件中的组件及其功能,然后根据预定义模板添加 remapping 对象数组来确定所有绑定场景。因此,自动重新映射仅可用于从内置控制器类型映射:vive_controller、oculus_touch、knuckles 和 khr_simple_controller。这种重新映射方法最容易定义,因此最不容易出错,因为我们从预定义的绑定类型集合中提取。如果您有相对标准的布局,我们建议尽可能使用 autoremappings 而不是显式定义 remappings。

remappings 对象让您更明确地控制如何将一个组件重新映射到另一个组件。如果您需要进行不常见的重新映射(例如 A/B 到触控板),您可以指定所有适用的单个绑定场景。您需要为每种可以进行的绑定类型指定一个 remapping 对象。Remapping 对象有三种类型:input、mode 和 component。Input 重新映射覆盖特定模式的输入,mode 重新映射覆盖模式中的所有输入,component 重新映射覆盖所有模式和输入。

重新映射绑定时,SteamVR 首先检查最具体的 remappings - input 重新映射(例如:click)。然后是 mode 重新映射(例如:button)。最后是 component 重新映射(例如:/user/hand/left/trigger)。这让我们可以做简单的事情,比如将 A 按钮重新映射到 X 按钮。还可以做更复杂的事情,比如将触发器式握持的各种模式/输入映射到力传感器式握持。除非它列有 remapping_mode multiple(参见一对多重映射),否则我们每个绑定只使用一个重新映射。

文件结构

  • to_controller_type - 必需。这必须设置为设备的 controller_type。
  • layouts - 必需。这是一个布局对象数组。
    • priority - 必需。如果可以使用多个布局,则使用最高优先级。
    • from_controller_type - 必需。此布局将转换自的控制器类型。
    • simulate_controller_type (默认: true) - 可选。此布局是否还应将控制器模拟选项添加到绑定中。
    • simulate_render_model (默认: true) - 可选。此布局是否还应将控制器渲染模型模拟选项添加到绑定中。
    • simulate_HMD (默认: true) - 可选。此布局是否还应将 HMD 模拟选项添加到绑定中。
    • autoremappings - 可选。自动重新映射对象数组。如果您所有的组件都完全相同,则不需要此。
      • from - 必需。一个字符串组件路径。例如:"/user/hand/right/input/trigger"
      • to - 必需。一个字符串组件路径或字符串组件路径列表。例如:"/user/hand/right/input/trigger" 或:["/user/hand/right/input/trigger", “/user/hand/right/input/grip”]
      • parameters - 可选。列出用于结果映射的参数的对象。
      • mirror (默认: true) - 可选。默认情况下,我们从左到右和从右到左复制和镜像组件路径。
    • remappings - 可选。重新映射对象数组。如果您所有的组件都完全相同,则不需要此。
      • from - 必需。一个指定要识别的绑定类型详细信息的对象。
        • path - 必需。要重新映射绑定的组件的完整路径。
        • mode - 可选。要重新映射绑定的绑定模式类型。
        • input - 可选。要重新映射绑定的模式上的特定输入。
      • to - 可选。指定结果绑定详细信息的对象。
        • path - 必需。要将绑定重新映射到的组件的完整路径。
        • mode - 可选。要将绑定重新映射到的绑定模式类型。
        • input - 可选。要将绑定重新映射到的模式上的特定输入。
        • parameters - 可选。列出用于结果映射的参数的对象。
        • parameters_mode (默认: replace) - 可选。使用 to 节中指定的参数的方式。可能的值:
          • replace 或空 - 将清除原始绑定中的参数并复制 to 节中的参数(如果存在)。
          • copy - 将原始绑定中的参数复制到新绑定。
          • append - 将复制原始绑定中的参数并添加 to 节中的参数。如果有重复项,将使用 to 参数的值。
        • parameters_modification - 可选。这将修改点击和触摸阈值参数。结果激活阈值将被限制在 0.02 和 0.99 之间。结果停用阈值将被限制在 0.01 和 0.98 之间。可能的值:
          • quarter_thresholds - 将阈值值乘以 0.25。
          • half_thresholds - 将阈值值乘以 0.5。
          • double_thresholds - 将阈值值乘以 2.0。
      • remapping_mode (默认: replace) - 可选。有时您想用绑定做其他事情,而不仅仅是将其替换为另一个。可能的值:
        • replace 或空 - 将删除旧绑定并添加新绑定。
        • delete - 删除旧绑定且不创建替代品。
        • multiple - 指定从此重新映射应用的绑定创建多个新绑定。
      • mirror (默认: true) - 可选。默认情况下,我们从左到右和从右到左复制和镜像组件路径。在大多数情况下,这意味着您只需编写一组重新映射,而不是手动为 /user/hand/left 编写一组重新映射,为 /user/hand/right 编写另一组重新映射。(这也适用于脚和肘)

自动重新映射参数

如果您的映射是从数字输入(按钮)到模拟输入(扳机),您可以可选地添加点击和触摸阈值以指定这些输入何时触发。具体来说,这些参数是:click_activate_threshold、click_deactivate_threshold、touch_activate_threshold 和 touch_deactivate_threshold。否则我们将使用默认值。

一对多重映射

您可以使一个组件映射到多个组件或输入,使用 “remapping_mode” : “multiple”。这必须添加到您想要使用的每个重新映射对象中。如果您还想保留原始绑定,请添加一个具有相同 from/to 的组件映射。您还必须添加一个 multiple_group 成员,该成员对每组重新映射具有唯一的标识符。例如,以下重新映射集将复制所有摇杆绑定并为名为 thumbstick 的组件创建新绑定,为名为 trackpad 的组件创建另一组新绑定,然后保留原始摇杆绑定。

        {
          "from": {
            "path" : "/user/hand/right/input/joystick"
          },
          "to": {
            "path" : "/user/hand/right/input/thumbstick",
            "parameters_mode": "copy"
          },
          "remapping_mode" : "multiple",
          "multiple_group" : "joystick_to_thumbstick"
        },
        {
          "from": {
            "path" : "/user/hand/right/input/joystick"
          },
          "to": {
            "path" : "/user/hand/right/input/trackpad",
            "parameters_mode": "copy"
          },
          "remapping_mode" : "multiple",
          "multiple_group" : "joystick_to_trackpad"
        },
        {
          "from": {
            "path" : "/user/hand/right/input/joystick"
          },
          "to": {
            "path" : "/user/hand/right/input/joystick",
            "parameters_mode": "copy"
          },
          "remapping_mode" : "multiple",
          "multiple_group" : "joystick_to_joystick"
        },

示例

您可以在包含的控制器类型的驱动程序中找到许多重新绑定文件的示例。从您的 SteamVR 文件夹 SteamVR\drivers\indexcontroller\resources\input\index_controller_remapping.json 或 SteamVR\resources\input\。

在绑定中模拟设备

绑定 UI 在右上角的选项菜单中有用于设置模拟的设置。您也可以在绑定 json 中手动执行。这看起来不同,具体取决于应用程序使用的 VR API。

SteamVR 输入与旧版输入

在绑定中设置 simulated_controller_type 将配置 SteamVR 告诉应用程序您的设备具有与您在驱动程序中配置的属性不同的属性。具体来说,是可模拟设备中列出的属性。这仅对应用此绑定的应用程序为真。您的设备在仪表板和叠加层中仍将正常显示。

模拟设置存储在绑定配置文件 json 对象的根目录下的 options 键中。

{
  "options": {
    "simulated_controller_type": "knuckles",
    "simulate_rendermodel": true
  }
}
  • simulated_controller_type - 您要模拟的控制器的 Prop_ControllerType_String。
  • simulate_rendermodel (默认: false) - 是否模拟渲染模型。一些游戏查找渲染模型而不是控制器类型。
  • simulate_hmd (默认: true) - 模拟与 simulated_controller_type 对应的 HMD 类型。一些游戏检查 hmd 以确定控制器类型。

OpenXR

由于 OpenXR 不允许应用程序直接获取有关设备的信息,模拟要简单得多。在文档的根目录中,必须使用应用程序接受的交互配置文件之一指定 interaction_profile。绑定 UI 将在应用程序运行时显示来自应用程序的可接受配置文件。

旧版绑定模拟

在 OpenXR 和 SteamVR Input 之前,我们使用现在称为旧版输入的输入系统。该系统仍然受 SteamVR 支持,许多流行的应用程序使用它。然而,这是一个非常通用的系统,不允许应用程序按控制器提交绑定。这意味着无法以编程方式判断应用程序是否支持您的设备。

模拟设置可以使您的设备与许多旧版应用程序兼容。但是,由于很难知道所有需要模拟的旧版应用程序,我们建议在您的默认旧版绑定文件上设置模拟选项。这将为所有旧版应用程序模拟您选择的控制器。按照此处的说明将这些设置添加到您的默认旧版绑定文件中。

这种方法的不利之处是,所有旧版输入应用程序都将显示您选择模拟的控制器的渲染模型(以及控制器说明,如果有的话)。我们认为通过这种方法获得的应用程序兼容性水平是值得的。

绑定复制

自 SteamVR v1.26 起,此方法已弃用,转而使用自动重新绑定。如果指定了重新绑定文件,compatibility_mode_controller_type 将被忽略。

在设备输入配置文件中,可以提供 compatibility_mode_controller_type,这仅在应用程序没有绑定配置文件时生效。

当找不到绑定配置文件时,将使用模拟设备的绑定配置文件,并设置模拟属性。

仅在设备的输入配置文件和模拟设备的配置文件中都指定的输入组件将被成功绑定并处于活动状态。

{
  "compatibility_mode_controller_type": "knuckles"
}

在 <device_name>_profile.json 输入配置文件的根目录中,设备可以将 compatibility_mode_controller_type 设置为可模拟设备中列出的可模拟设备。

这样做等同于按应用程序绑定配置文件设置 simulated_controller_type 和 simulate_rendermodel。

可模拟的设备

希望被模拟的设备必须在其输入配置文件中明确声明。

默认由 Valve 提供的可以被模拟的设备如下所示,以及它们的输入配置文件路径。

在内部,SteamVR 将保留您的驱动程序提供的属性容器,但将引入一个新的包含模拟属性的属性容器,然后将其转发给应用程序,同时使用驱动程序提供的属性供自己使用(例如绑定)。

希望被模拟的设备在其输入配置文件中的 simulation_settings 对象内必须提供以下键。此列表中未提供的键在覆盖属性容器中将没有任何值设置:

  • hmd_profile - 如果希望模拟此设备的设备中 simulate_hmd 设置为 true,运行时将为此 HMD 使用此配置文件。
  • left_modelnumber - 如果 Prop_ControllerRoleHint_Int32 设置为 TrackedControllerRole_LeftHand,则模拟设备的 Prop_ModelNumber_String。
  • right_modelnumber - 如果 Prop_ControllerRoleHint_Int32 设置为 TrackedControllerRole_RightHand,则模拟设备的 Prop_ModelNumber_String。
  • left_serialnumber - 如果 Prop_ControllerRoleHint_Int32 设置为 TrackedControllerRole_LeftHand,则模拟设备的 Prop_SerialNumber_String。
  • right_serialnumber - 如果 Prop_ControllerRoleHint_Int32 设置为 TrackedControllerRole_RightHand,则模拟设备的 Prop_SerialNumber_String。
  • left_rendermodel - 如果 Prop_ControllerRoleHint_Int32 设置为 TrackedControllerRole_LeftHand,则模拟设备的 Prop_RenderModelName_String。
  • right_rendermodel - 如果 Prop_ControllerRoleHint_Int32 设置为 TrackedControllerRole_RightHand,则模拟设备的 Prop_RenderModelName_String。
  • tracking_system_name - 模拟设备的 Prop_TrackingSystemName_String。
  • manufacturer_name - 模拟设备的 Prop_ManufacturerName_String。
  • legacy_axis - 模拟 Prop_Axis0Type_Int32、Prop_Axis1Type_Int32…Prop_Axis4Type_Int32 轴组件类型。
  • legacy_buttons - 旧版输入系统中的按钮掩码 Prop_SupportedButtons_Uint64。

下面的列表显示了可以模拟的设备控制器类型,以及在设置时 SteamVR 将在覆盖属性容器中设置的属性和值。根据 Prop_ControllerRoleHint_Int32,如果指定了左或右手,以下属性将在覆盖属性容器中设置。

  • knuckles

    • hmd_profile - indexhmd
    • Prop_ModelNumber_String
      • 左手:Knuckles Left
      • 右手:Knuckles Right
    • Prop_SerialNumber_String
      • 左手:LHR-FFFFFFF1
      • 右手:LHR-FFFFFFF2
    • Prop_RenderModelName_String
      • 左手:{indexcontroller}valve_controller_knu_1_0_left
      • 右手:{indexcontroller}valve_controller_knu_1_0_right
    • Prop_RegisteredDeviceType_String
      • 左手:valve/index_controllerLHR-FFFFFFF1
      • 右手:valve/index_controllerLHR-FFFFFFF2
    • Prop_TrackingSystemName_String - lighthouse
    • Prop_ManufacturerName_String - Valve
    • legacy_buttons - [ 0, 1, 2, 7, 32, 33, 34 ]
    • legacy_axis - [ 2, 3, 3, 0, 0 ]
  • indexhmd

    • Prop_ModelNumber_String - Index
    • Prop_SerialNumber_String - LHR-FFFFFFF0
    • Prop_RenderModelName_String - generic_hmd
    • Prop_RegisteredDeviceType_String - valve/index
    • Prop_TrackingSystemName_String - lighthouse
    • Prop_ManufacturerName_String - Valve
  • oculus_touch

    • hmd_profile - rift
    • Prop_ModelNumber_String
      • 左手:Oculus Quest2 (Left Controller)
      • 右手:Oculus Quest2 (Right Controller)
    • Prop_SerialNumber_String
      • 左手:WMHD315M3010GV_Controller_Left
      • 右手:WMHD315M3010GV_Controller_Right
    • Prop_RenderModelName_String
      • 左手:oculus_quest2_controller_left
      • 右手:oculus_quest2_controller_right
    • Prop_RegisteredDeviceType_String
      • 左手:oculus/WMHD315M3010GV_Controller_Left
      • 右手:oculus/WMHD315M3010GV_Controller_Right
    • Prop_TrackingSystemName_String - oculus
    • Prop_ManufacturerName_String - Oculus
    • legacy_buttons - [ 0, 1, 2, 7, 32, 33, 34 ]
    • legacy_axis - [ 2, 3, 3, 0, 0 ]
  • rift

    • Prop_ModelNumber_String - Oculus Quest2
    • Prop_SerialNumber_String - WMHD315M3010GV
    • Prop_RenderModelName_String - generic_hmd
    • Prop_RegisteredDeviceType_String - oculus/WMHD315M3010GV
    • Prop_TrackingSystemName_String - oculus
    • Prop_ManufacturerName_String - Oculus
  • vive_controller

    • hmd_profile - vive
    • Prop_ModelNumber_String
      • 左手:VIVE Controller Pro MV
      • 右手:VIVE Controller Pro MV
    • Prop_SerialNumber_String
      • 左手:LHR-00000001
      • 右手:LHR-00000002
    • Prop_RenderModelName_String
      • 左手:vr_controller_vive_1_5
      • 右手:vr_controller_vive_1_5
    • Prop_RegisteredDeviceType_String
      • 左手:htc/vive_controllerLHR-00000001
      • 右手:htc/vive_controllerLHR-00000002
    • Prop_TrackingSystemName_String - lighthouse
    • Prop_ManufacturerName_String - HTC
    • legacy_buttons - [ 0, 1, 2, 32, 33 ]
    • legacy_axis - [ 1, 3, 0, 0, 0 ]
  • vive

    • Prop_ModelNumber_String - Vive
    • Prop_SerialNumber_String - LHR-00000000
    • Prop_RenderModelName_String - generic_hmd
    • Prop_RegisteredDeviceType_String - htc/vive
    • Prop_TrackingSystemName_String - lighthouse
    • Prop_ManufacturerName_String - HTC
  • vive_tracker

    • hmd_profile - vive
    • Prop_ModelNumber_String
      • 左手:VIVE Tracker MV
      • 右手:VIVE Tracker MV
    • Prop_SerialNumber_String
      • 左手:LHR-00000001
      • 右手:LHR-00000002
    • Prop_RenderModelName_String
      • 左手:{htc}vr_tracker_vive_1_0
      • 右手:{htc}vr_tracker_vive_1_0
    • Prop_RegisteredDeviceType_String
      • 左手:htc/vive_trackerLHR-00000001
      • 右手:htc/vive_trackerLHR-00000002
    • Prop_TrackingSystemName_String - lighthouse
    • Prop_ManufacturerName_String - HTC
    • legacy_buttons - [ 0, 1, 2, 32, 33 ]
    • legacy_axis - [ 1, 3, 0, 0, 0 ]

安全区

SteamVR 安全区系统为用户在 VR 中提供可见的边界,应在游戏空间的边缘显示,以避免与其他物体碰撞。

安全区系统还负责跟踪驱动程序的原始追踪空间(驱动程序通过 IVRServerDriverHost::TrackedDevicePoseUpdate 向运行时提供位姿的追踪空间)与应用程序查询位姿时参考的坐姿和站姿宇宙原点之间的关系。

每个宇宙的描述如下,以及它们对应的 json 属性:

* `TrackingUniverseSeated (seated)` - 对于需要相对于用户休息头部位置渲染内容的应用程序非常有用,例如在模拟器中呈现驾驶舱视图。
* `TrackingUniverseStanding (standing)` - 这是追踪空间地板上的某个点,其中 y = 0 **必须**始终是此追踪空间中的地板。对于想要渲染应缩放到用户真实世界设置的应用程序非常有用,例如将地板放置在同一位置。
* "Setup standing (setup_standing2)" - 来自原始追踪空间的原点。这是游戏空间中心地板上的某个点。此宇宙对应用程序不可见,但驱动程序**可以**选择使用它来打破站姿原点应位于何处与 SteamVR 应将碰撞边界放置在哪里的依赖关系。驱动程序可以选择提供此信息,如果省略,则默认为与站姿宇宙相同。

任何提供自己追踪解决方案的驱动程序应该提供自己的安全区设置。

驱动程序将其安全区设置作为 json 文件提供。驱动程序可以要么提供指向希望呈现给 SteamVR 的安全区 json 文件的绝对路径,要么通过设置 HMD 容器的 Prop_DriverProvidedChaperoneJson_String 属性来提供 json 字符串。

驱动程序可以提供多个"宇宙",其中(在此上下文中),一个宇宙代表现实世界中需要单独安全区设置的不同位置,例如切换到另一个房间。

SteamVR 只允许一个安全区宇宙在任一时刻处于活动状态。驱动程序必须通过将 Prop_CurrentUniverseId_Uint64 属性设置为其希望使用的宇宙 id 来指定它希望使用的宇宙(更多细节见下文)。

提供的 json 必须有效,没有尾随逗号,但可以包含以 // 为前缀的注释。

* `json_id` - **必需**。设置为 `chaperone_info`。
* `version` - **必需**。当前安全区 json 版本是 `5`。
* `time` - **必需**。安全区文件上次修改的 ISO 时间戳。
* `universes` - 一个 json 数组,包含 json 对象,这些对象包含:
    * `collision_bounds` - 一个数组,包含多组多边形(一个数组,**应该**包含包含 4 个元素(多边形)的数组,其中每个元素是一个包含每个顶点的 x,y,z 位置的数组)。碰撞边界相对于**设置站姿**游戏空间。驱动程序**应该**为每个绘制的面提供 4 个顶点。驱动程序**应该**提供位于同一垂直平面上的顶点。
    * `play_area` - 一个包含两个值的数组:`[width, height]` 游戏空间。宽度和高度由驱动程序定义,但**应该**表示可游戏区域的最大矩形。
    * `<seated/standing/setup_standing2>` - 一个 json 对象,表示驱动程序的原始追踪空间与指定宇宙原点之间的关系。驱动程序**必须**提供坐姿和站姿关系,但**可以**省略设置站姿宇宙。在这种情况下,设置站姿宇宙将设置为站姿属性设置的内容(见下一段)。每个**必须**包含以下属性:
        * `translation` - 原始原点到宇宙原点的位置偏移。
        * `yaw` - 原始空间与宇宙空间之间在 x,z 平面上的旋转。
    * `universeID` - 宇宙的 id。这**必须**是一个 uint32 数字,并且**必须**对每个不同的宇宙都是唯一的。

驱动程序必须要么:

  1. 同时设置设置站姿和站姿原点。
    • 在这种情况下,设置站姿原点被视为游戏区域的中心。站姿原点可以自由放置在其他地方。
  2. 仅设置站姿原点。
    • 在这种情况下,站姿原点被视为游戏区域的中心。

重新居中

SteamVR 中的重新居中功能允许用户在 SteamVR 内更新站姿和坐姿宇宙位置。这对于在驾驶舱中重新定位身高,或者将房间规模内容相对于不同的现实世界位置重新定位非常有用。

最初,站姿和坐姿宇宙的变换由驱动程序提供的安全区文件设置。
当用户请求重新居中时,SteamVR 更新其在内存中保存的站姿和坐姿变换,如果驱动程序提供了一个且该文件可写,将尝试更新安全区文件中的坐姿宇宙(且仅坐姿宇宙)。SteamVR 不会尝试修改站姿变换。

如果驱动程序想要配置处理重新居中的方式,它可以使用以下值之一配置 Prop_Driver_RecenterSupport_Int32 属性:

  • k_EVRDriverRecenterSupport_SteamVRHandlesRecenter - 默认。SteamVR 显示重新居中按钮,并在用户请求重新居中时执行上述操作。
  • k_EVRDriverRecenterSupport_NoRecenter - 不支持重新居中,UI 中不会显示重新居中按钮。
  • k_EVRDriverRecenterSupport_DriverHandlesRecenter - 显示重新居中按钮,并为驱动程序处理重新居中触发事件,但 SteamVR 不会进行任何额外的处理。

渲染模型

渲染模型是代表 VR 中设备的 3D 模型。渲染模型应该提供设备在现实中外观的图形表示。拥有看起来像对象的渲染模型对最终用户非常有用,因为它使在 VR 中拾取和处理对象变得更加容易。

一旦应用程序加载,应用程序可以用更合适的模型覆盖渲染模型。

至少,渲染模型必须由三个文件组成:一个 Wavefront (OBJ) 对象,带有材质 (MTL) 和纹理 (TGA 或 PNG)。

设计渲染模型后,它必须与现有的控制器渲染模型对齐。

对象形状很可能由机械工程师使用 3D 实体建模软件包定义。将此形状导出为 .stl 是创建渲染模型的第一步。但是,.stl 的导出方式极大地影响过程的其余部分。

渲染模型 .obj 不能包含超过 65,000 个顶点。

  • 当导出 .stl 文件以转换为渲染模型时,文件应该仅描述对象的外表面,以将顶点保持在最低限度。但是,此表面应该包括按钮、触控板和其他外部特征。这些可以通过编程方式进行动画处理,以虚拟方式响应物理设备交互。

渲染模型 .obj 必须仅包含一个对象。

构建与开发环境

SteamVR 附带一个实用程序 vrpathreg.exe,用于向 SteamVR 添加/移除驱动程序。OpenVR 注册表文件(不要与 Windows 注册表混淆)由 vrpathreg.exe 维护。

OpenVR 注册表文件位于 %appdata%/local/openvr/openvr.vrpaths,vrpathreg.exe 通常位于 <steamvr_install_dir>\bin\win64。

要定位 <steamvr_install_dir> 的位置,请使用 Windows 注册表中的标准卸载键:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 250820。

在驱动程序的测试和开发期间,应该使用 vrpathreg.exe。在最终用户计算机上安装期间,vrpathreg.exe 应从 SteamVR 安装目录调用,而不应包含在安装程序中。

可用命令:

  • show - 显示当前到运行时、配置、日志和外部驱动程序路径的路径。返回:
    • Runtime Path - 运行时目录的路径。
    • Config Path - 存储配置文件的目录的路径。
    • Log Path - 日志文件所在目录的路径。
    • External Drivers - 当前安装的驱动程序的名称和路径列表。
      • <driver_name> - 显示驱动程序名称及其路径作为其值。驱动程序的名称取自驱动程序的 driver.vrdrivermanifest 文件中的 name 属性,该文件位于列出的目录路径中。
  • setruntime <path> - 将根运行时路径设置为指定的 <path>。返回:
    • 0 - 成功。运行时路径已更新为指定路径。
    • -1 - 配置或权限问题。openvr.vrpaths 可能不存在,或者进程可能没有写入 openvr.vrpaths 文件所需的权限。
    • -2 - 参数问题。参数超过 2 个,或指定的路径不是绝对路径。
  • setthis - 将运行时路径设置为 vrpathreg.exe 所在位置的上两级目录。返回:
    • 0 - 成功。运行时路径已更新为 vrpathreg.exe 所在位置上两级目录。
    • -1 - 配置或权限问题。openvr.vrpaths 可能不存在,或者进程可能没有写入 openvr.vrpaths 文件所需的权限。
    • -2 - 参数问题。参数超过 2 个,或指定的路径不是绝对路径。
  • setconfig <path> - 设置运行时读取和写入配置文件的路径。返回:
    • 0 - 成功。配置路径已更新为指定路径。
    • -1 - 配置或权限问题。openvr.vrpaths 可能不存在,或者进程可能没有写入 openvr.vrpaths 文件所需的权限。
    • -2 - 参数问题。参数超过 2 个,或指定的路径不是绝对路径。
  • setlog <path> - 设置运行时读取和写入日志文件的日志路径。返回:
    • 0 - 成功。日志文件路径已更新为指定路径。
    • -1 - 配置或权限问题。openvr.vrpaths 可能不存在,或者进程可能没有写入 openvr.vrpaths 文件所需的权限。
    • -2 - 参数问题。参数超过 2 个,或指定的路径不是绝对路径。
  • adddriver <path> - 从指定路径添加驱动程序到要加载到运行时的外部驱动程序列表中。指定的路径必须是驱动程序 driver.vrdrivermanifest 文件所在位置的路径。返回:
    • 0 - 成功。驱动程序已添加到要加载到运行时的外部驱动程序列表中。如果 SteamVR 正在运行,驱动程序应该已被热插拔到运行时中。
    • -1 - 配置或权限问题。openvr.vrpaths 可能不存在,或者进程可能没有写入 openvr.vrpaths 文件所需的权限。
    • -2 - 参数问题。参数超过 2 个,或指定的路径不是绝对路径。
  • removedriver <path> - 移除外部驱动程序。指定的路径必须是驱动程序 driver.vrdrivermanifest 文件所在位置的路径。返回:
    • 0 - 成功。驱动程序已从外部驱动程序列表中移除。驱动程序将在下一次 SteamVR 启动时停止被加载到运行时中。
    • -1 - 配置或权限问题。openvr.vrpaths 可能不存在,或者进程可能没有写入 openvr.vrpaths 文件所需的权限。
    • -2 - 参数问题。参数超过 2 个,或指定的路径不是绝对路径。
  • removedriverswithname <driver_name> - 移除所有具有给定名称的外部驱动程序。返回:
    • 0 - 成功。所有具有给定名称的驱动程序已从外部驱动程序列表中移除。驱动程序将在下一次 SteamVR 启动时停止被加载到运行时中。
    • -1 - 配置或权限问题。openvr.vrpaths 可能不存在,或者进程可能没有写入 openvr.vrpaths 文件所需的权限。
    • -2 - 参数问题。参数超过 2 个,或指定的路径不是绝对路径。
  • finddriver <name> - 尝试按名称查找驱动程序。不区分大小写。返回:
    • <driver_path> - 指定驱动程序的 driver.vrdrivermanifest 文件的路径。
      • 返回 0。
    • 1 - 驱动程序不存在。
    • 2 - 驱动程序安装了多次。
    • -1 - 配置或权限问题。openvr.vrpaths 可能不存在,或者进程可能没有写入 openvr.vrpaths 文件所需的权限。
    • -2 - 参数问题。参数超过 2 个,或指定的路径不是绝对路径。

驱动程序不应多次注册相同的驱动程序(或其不同版本)。SteamVR 将选择 openvr.vrpaths External Drivers 数组中索引较低的驱动程序作为要加载的驱动程序。

使用 Visual Studio 调试 SteamVR

要使用 SteamVR 调试驱动程序,需要进行一些额外的设置。

为了使用 Visual Studio 进行调试,必须连接头戴设备。

SteamVR 通过运行 vrstartup.exe 启动,该进程会启动子进程:vrserver.exe 是驱动程序加载到的进程,也是您需要附加调试器的进程。

Visual Studio 有一个工具:“Microsoft child process debugging power tool”,它提供了在运行构建时启动 vrstartup.exe 并调试 vrserver.exe 的功能。

  • 在此处下载扩展:

    • VS2015、VS2017、
      VS2019 - https://marketplace.visualstudio.com/items?itemName=vsdbgplat.MicrosoftChildProcessDebuggingPowerTool
    • VS2022 - https://marketplace.visualstudio.com/items?itemName=vsdbgplat.MicrosoftChildProcessDebuggingPowerTool2022
  • 在顶部工具栏中,选择 Debug > " Debug Properties"(列表中的最后一个选项)

    • 选择 Configuration Properties > Debugging
    • 在 Command 中,复制并粘贴 vrstartup.exe 可执行文件的路径
      • 通常位于 C:\Program Files (x86)\Steam\steamapps\common\SteamVR\bin\win64\vrstartup.exe,或在您的 Steam 安装路径下。
    • 点击 Ok
  • 在顶部工具栏中选择 Debug > Other Debug Targets > Child Process Debugging Settings…

    • 点击启用 Enable child process debugging
    • 在 <All other processes 上,选择 Do not debug
    • 向表中添加一行,在文本框中输入 vrserver.exe
      • 确保此行设置为 Attach debugger
    • 点击 Save

通过 Visual Studio 运行项目时,SteamVR 应该打开,并且 Visual Studio 应该一直保持在调试状态。

如果 SteamVR 仍在运行时 Visual Studio 退出调试状态,请确保子进程调试设置已正确填写,并且驱动程序正在加载到 SteamVR 中。

在 SteamVR 运行时单击 Visual Studio 中的停止将导致 SteamVR 以崩溃状态退出,但不会导致 SteamVR 进入安全模式(停用驱动程序)。开发人员应该通过 SteamVR 监视器中的"x"或在仪表板中关闭 SteamVR 来退出 SteamVR。
要在重新启动之间保持设备开启,请在 SteamVR 控制器设置中关闭"退出 SteamVR 时关闭控制器"。

更多示例

此存储库之外的更多驱动程序示例列表:

  • https://github.com/ValveSoftware/virtual_display
许可协议:  CC BY 4.0
分享

相关文章

下一篇

上一篇

每周考研阅读 2021 Text 1

最近更新

  • OpenVR Driver API 中文文档
  • 每周考研阅读 2021 Text 1

热门标签

Halo

目录

©2026 Text03's Blog. 保留部分权利。

鲁ICP备2025195077号-1 | 鲁公网安备37092302000179号

使用 Halo 主题 Chirpy