avatar

Text03's Blog

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

  • 首页
  • 链接
  • 关于
主页 OpenVR 驱动教程
文章

OpenVR 驱动教程

发表于 7天前 更新于 7天前
作者 text03
263~338 分钟 阅读

本文档提供了设置OpenVR驱动的教程。

我们将最终创建一个虚拟控制器驱动,它以HMD位置作为其姿态,并具有一些基本输入。虽然可以在没有任何硬件接口的情况下遵循本指南与SteamVR交互,但最终结果不会有特别实际的用处。相反,本指南旨在作为构建您自己的SteamVR驱动的参考或基础,包含常见用例及其实现方法。

本目录中的文件包含了遵循本教程后得到的驱动。如果您只想使用该驱动,必须在本目录中运行 git submodule update --init。

介绍

本教程…

假设:

  • 了解C++和面向对象编程。
  • 基本了解CMake。

需要:

  • Git - https://git-scm.com/download

推荐:

  • Visual Studio 和 MSVC - https://visualstudio.microsoft.com/
  • Visual Studio 子进程调试增强工具扩展 - 用于调试您的驱动。
    • VS2015, VS2017, VS2019 - https://marketplace.visualstudio.com/items?itemName=vsdbgplat.MicrosoftChildProcessDebuggingPowerTool
    • VS2022 - https://marketplace.visualstudio.com/items?itemName=vsdbgplat.MicrosoftChildProcessDebuggingPowerTool2022

将:

  • 提供如何实现某些接口的信息,以及何时调用哪些方法。
  • 生成一个简单的控制器驱动,可用作设备的基础。
  • 将用户定义的值用尖括号括起来。例如:<my_driver_name>

不会:

  • 提供接口文档。有关文档,请参阅Wiki。它有时会提供有用的提示,但这应与Wiki中的完整描述相辅相成。

初始设置

创建一个文件夹来存储您的驱动项目。此根文件夹的位置和名称无关紧要。

添加OpenVR SDK

我们需要将OpenVR SDK添加到项目中,它包含必要的头文件和二进制文件,供我们的驱动编译以与OpenVR运行时配合工作。

在我们的根项目文件夹内创建一个文件夹来存储依赖项,命名为 lib/。

  • mkdir lib
  • cd lib

然后将OpenVR SDK添加为子模块。在 lib/ 内运行:

  • git submodule add https://github.com/ValveSoftware/openvr

添加必需的驱动文件

有一些必需的文件,我们应首先添加到项目中。我们将把这些文件添加到与我们即将编写的代码分开的文件夹中。

创建一个名为 <my_driver_name> 的文件夹。<my_driver_name> 应为一个全小写、无空格的字符串。给它起个好名字!您将在几个地方使用它。

  • mkdir <my_driver_name>
  • cd <my_driver_name>

driver.vrdrivermanifest

在 <my_driver_name> 内创建一个名为 driver.vrdrivermanifest 的文件。

driver.vrdrivermanifest 是一个包含有关您的驱动属性的文件。用以下内容填充它:

{
  "alwaysActivate": true,
  "name": "<my_driver_name>",
  "directory": "",
  "resourceOnly": false,
  "hmd_presence": []
}

有关该文件的文档,请参阅wiki。

name 的值必须与它当前所在的文件夹名称匹配:<my_driver_name>。

现在您的项目文件夹结构应该如下所示:

<project_root>
└── <my_driver_name>
    └── driver.vrdrivermanifest

源文件

导航回根项目文件夹。

创建一个名为 src 的文件夹。这将包含我们的源代码。在 src/ 中,让我们创建将要填充的文件:

  • hmd_driver_factory.cpp
  • device_provider.h
  • device_provider.cpp
  • controller_device.h
  • controller_device.cpp

现在您的项目文件夹结构应该如下所示:

<project_root>
├── <my_driver_name>
│   └── driver.vrdrivermanifest
└── src
    ├── hmd_driver_factory.cpp
    ├── device_provider.h
    ├── device_provider.cpp
    ├── controller_device.h
    └── controller_device.cpp

创建驱动二进制文件

本教程概述了两种为驱动创建共享库的方法。第一种是使用CMake,第二种是使用Visual Studio。两者的输出将是相同的。

使用CMake创建项目

导航回根项目文件夹。

创建一个名为 CMakeLists.txt 的文件。

这将是我们指示CMake生成驱动二进制文件并编译依赖项的地方。

将以下内容添加到 CMakeLists.txt:

cmake_minimum_required(VERSION 3.7.1)

# 这是您的项目名称
set(TARGET_NAME <my_driver_name>)

# 这是二进制文件的名称(driver_<my_driver_name>)
set(DRIVER_NAME "driver_${TARGET_NAME}")

project(${TARGET_NAME})

set(OPENVR_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lib/openvr)

# 如果未设置,确定平台架构
if (NOT PLATFORM)
    if (CMAKE_SIZEOF_VOID_P MATCHES 8)
        set(PLATFORM 64)
    else ()
        set(PLATFORM 32)
    endif ()
endif ()
message(STATUS "编译设置为 ${PLATFORM}位架构。")

# OpenVR 兼容性检查
if (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
    add_definitions(-DLINUX -DPOSIX)
    set(ARCH_TARGET linux64)

    if (${PLATFORM} MATCHES 32)
        message(WARNING "OpenVR 未在 GNU/Linux 上提供 x86 二进制文件。")
    endif ()
elseif (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
    set(CMAKE_MACOSX_RPATH 0)
    add_definitions(-DOSX -DPOSIX)
    set(ARCH_TARGET osx32)

elseif (${CMAKE_SYSTEM_NAME} MATCHES "Windows")
    add_definitions(-D_WIN32)
    set(ARCH_TARGET win${PLATFORM})

    # 第三方二进制文件的路径不通用,因此我们尝试猜测它们的后缀。
    set(WINDOWS_PATH_SUFFIXES win${PLATFORM} Win${PLATFORM} x${PLATFORM})
endif ()

find_library(OPENVR_LIBRARIES
        NAMES
        openvr_api
        PATHS
        ${OPENVR_LIB_DIR}/bin
        ${OPENVR_LIB_DIR}/lib
        PATH_SUFFIXES
        osx${PLATFORM}
        linux${PLATFORM}
        win${PLATFORM}
        NO_DEFAULT_PATH
        NO_CMAKE_FIND_ROOT_PATH
        )

set(OPENVR_INCLUDE_DIR ${OPENVR_LIB_DIR}/headers)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY $<1:${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}/bin/${ARCH_TARGET}>)

add_library(${DRIVER_NAME} SHARED
        src/hmd_driver_factory.cpp
        src/device_provider.h
        src/device_provider.cpp
        src/controller_device.h
        src/controller_device.cpp
        )

target_link_libraries(${DRIVER_NAME} PRIVATE ${OPENVR_LIBRARIES})
target_include_directories(${DRIVER_NAME} PRIVATE ${OPENVR_INCLUDE_DIR})

# 将驱动资源复制到输出文件夹
add_custom_command(
        TARGET ${DRIVER_NAME}
        PRE_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory
        ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET_NAME}
        ${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}
)

将 <my_driver_name> 替换为您的驱动名称。

这个 CMakeLists.txt 文件的大部分内容都是设置OpenVR SDK作为依赖项。但有一些额外的事情:

  • set(CMAKE_RUNTIME_OUTPUT_DIRECTORY $<1:${CMAKE_CURRENT_BINARY_DIR}/${DRIVER_NAME}/bin/${PLATFORM_NAME}${PROCESSOR_ARCH}>)
    • 将我们的二进制文件输出目录设置为(如果在Windows 64位上)<build_dir>/<my_driver_name>/bin/win64 或其他操作系统上的等效平台/架构。
  • add_library(${DRIVER_NAME} SHARED src/hmd_driver_factory.cpp)
    • 创建我们的驱动将要构建为的共享库目标。
  • target_link_libraries(${DRIVER_NAME} PRIVATE ${OPENVR_LIBRARIES}) 链接到OpenVR SDK二进制文件。
  • target_include_directories(${DRIVER_NAME} PRIVATE ${OPENVR_INCLUDE_DIR}) 将OpenVR SDK头文件添加到我们目标的包含路径中。
  • add_custom_command(..)
    • 将我们必需的驱动文件的内容复制到输出目录。运行时需要额外的文件来加载驱动。

如果您使用的是Visual Studio,创建一个 build 文件夹,我们的解决方案文件将在其中生成。CLion会为您完成此操作。

运行:

  • mkdir build
  • cd build
  • cmake ..

这应该生成一个解决方案文件,您可以在Visual Studio中打开以编辑驱动源代码。

现在我们的项目文件夹结构应该如下所示:

<project_root>
├── <my_driver_name>
│   └── driver.vrdrivermanifest
├── build
│   └── <my_driver_name>.sln
├── CMakeLists.txt
└── src
    ├── hmd_driver_factory.cpp
    ├── device_provider.h
    ├── device_provider.cpp
    ├── controller_device.h
    └── controller_device.cpp

使用Visual Studio创建项目

确保您已创建源文件中列出的文件。我们将使用它们。

创建Visual Studio项目

在Visual Studio中,单击“创建新项目”:

image

选择 空项目:

image

您的“项目名称”应为 <my_driver_name>。

将 位置 指向 src/ 和 <my_driver_name>/ 所在的文件夹。

勾选 将解决方案和项目放在同一目录中。

image

单击“创建”。

Visual Studio可能会将您的 .sln 和 .vcxproj 文件放在 <my_driver_name>/ 内。如果它们在此处,请将它们上移一级,使文件位于包含 src/ 和 <my_driver_name>/ 的同一文件夹内。

右键单击名为 <my_driver_name> 的项目。选择 添加 > 现有项...

image

在文件资源管理器中,导航到您的 src/ 文件夹并高亮显示所有文件。

image

单击 添加。

您的文件现在应出现在解决方案资源管理器中的项目下。

现在我们的项目文件夹结构应该如下所示:

<project_root>
├── <my_driver_name>.sln
├── <my_driver_name>.vcxproj
├── <my_driver_name>
│   └── driver.vrdrivermanifest
└── src
    ├── hmd_driver_factory.cpp
    ├── device_provider.h
    ├── device_provider.cpp
    ├── controller_device.h
    └── controller_device.cpp

设置构建

再次右键单击项目。选择 属性。

确保“配置”设置为 所有配置,“平台”设置为 所有平台。


导航到 配置属性 > 常规。

设置“输出目录”为:$(SolutionDir)build\$(ProjectName)\bin\win$(PlatformArchitecture)。

设置“目标名称”为:driver_$(ProjectName)。

设置“配置类型”为:动态库(.dll)。

image

这将使我们的共享库二进制文件输出到运行时期望的位置(<my_driver_name>/bin/win64/driver_<my_driver_name>.dll)。

单击应用。


导航到 C/C++ > 常规。

设置“附加包含目录”为:$(SolutionDir)lib/openvr/headers。

image

这将允许我们 #include 构建所需的OpenVR头文件。确保您已下载OpenVR SDK并解压到 lib/openvr/。

单击应用。


导航到 链接器 > 常规。

设置“附加库目录”为 $(SolutionDir)lib\openvr\lib\win$(PlatformArchitecture)。

image

这将设置链接器在此位置查找OpenVR SDK库。它有助于保持下一部分的整洁。

单击应用。


导航到 链接器 > 输入。

设置“附加依赖项”为:openvr_api.lib。

image

这将将必需的OpenVR静态库链接到我们的驱动二进制文件中。

单击应用。


导航到 生成事件 > 后期生成事件。

设置“命令行”为:XCOPY "$(ProjectDir)$(ProjectName)" "$(SolutionDir)build\$(ProjectName)" /S /Y /I。

image

这将把我们的其他必需驱动文件复制到驱动中,以便运行时加载。

单击确定。


按 Ctrl + Shift + B。您的项目应该构建成功,输出将放置在 build/<my_driver_name>。

IServerTrackedDeviceProvider

我们将首先实现驱动的 IServerTrackedDeviceProvider 接口。此接口为我们提供了一种在驱动加载时启动我们自己的进程和设备的方法,以及一些运行时在运行时状态更改时可以调用的有用方法。

OpenVR中的接口是抽象类:它们提供了一组纯虚函数(没有定义且可重写的函数),因此我们必须实现接口中的所有方法。

实现 IServerTrackedDeviceProvider

首先,在 device_provider.h 中,我们将定义我们的 DeviceProvider 类:

#pragma once

#include "openvr_driver.h"

class DeviceProvider : public vr::IServerTrackedDeviceProvider {
    public:
        // 继承自 IServerTrackedDeviceProvider
        vr::EVRInitError Init(vr::IVRDriverContext *pDriverContext) override;
        void Cleanup() override;
        const char * const *GetInterfaceVersions() override;
        void RunFrame() override;
        bool ShouldBlockStandbyMode() override;
        void EnterStandby() override;
        void LeaveStandby() override;
};

我们首先包含 openvr_driver.h,它包含我们想要实现的接口声明。

注意在 class DeviceProvider : public vr::IServerTrackedDeviceProvider 和定义方法时使用 public。

这将覆盖 IServerTrackedDeviceProvider 中的所有方法,以便我们自己实现。

接下来,在 device_provider.cpp 中,我们将实现这些方法:

#include "device_provider.h"

vr::EVRInitError DeviceProvider::Init(vr::IVRDriverContext* pDriverContext) {
    VR_INIT_SERVER_DRIVER_CONTEXT(pDriverContext);

    vr::VRDriverLog()->Log("Hello world!");

    return vr::VRInitError_None;
}

void DeviceProvider::Cleanup() {
    VR_CLEANUP_SERVER_DRIVER_CONTEXT();
}

const char* const* DeviceProvider::GetInterfaceVersions() {
    return vr::k_InterfaceVersions;
}

void DeviceProvider::RunFrame() {

}

bool DeviceProvider::ShouldBlockStandbyMode() {
    return false;
}

void DeviceProvider::EnterStandby() {

}

void DeviceProvider::LeaveStandby() {

}

在这里,我们首先包含我们的 device_provider.h 头文件,其中包含我们将在 device_provider.cpp 中定义的方法的声明。

我们的 Init 方法在SteamVR启动时调用。它接收一个 IVRDriverContext 指针,我们必须使用它来初始化我们的驱动上下文。这可以通过OpenVR API提供的宏 VR_INIT_SERVER_DRIVER_CONTEXT 来完成。然后我们返回 VRInitError_None 以指示我们已成功初始化。

然后我们记录到运行时日志文件。实时日志可以通过打开SteamVR Web控制台找到(SteamVR 汉堡菜单 > 开发者选项 > Web控制台)。

我们的 Cleanup 方法在SteamVR退出时调用。它清理我们在 Init 中初始化的驱动上下文。

GetInterfaceVersions 告诉OpenVR我们实现了哪些接口版本。OpenVR是向后兼容的,并支持所有以前版本的接口,因此我们需要告诉它我们支持哪些版本。为此,OpenVR提供了一个接口版本数组,名为 k_InterfaceVersions,代表您当前定位的OpenVR版本。

ShouldBlockStandbyMode 已弃用,不起任何作用。

我们稍后将添加更多内容!

HMDDriverFactory

HMDDriverFactory是一个从您的驱动共享库导出的函数,运行时使用它作为入口点来访问您的驱动并找到您实现的接口。

在 hmd_driver_factory.cpp 中,添加以下内容:

#include "device_provider.h"

#if defined(_WIN32)
#define HMD_DLL_EXPORT extern "C" __declspec( dllexport )
#define HMD_DLL_IMPORT extern "C" __declspec( dllimport )
#elif defined(__GNUC__) || defined(COMPILER_GCC) || defined(__APPLE__)
#define HMD_DLL_EXPORT extern "C" __attribute__((visibility("default")))
#define HMD_DLL_IMPORT extern "C" 
#else
#error "不支持的平台。"
#endif

//全局的、我们设备提供者的单一接口实例。
DeviceProvider device_provider;

HMD_DLL_EXPORT void* HmdDriverFactory(const char* pInterfaceName, int* pReturnCode) {
	if (0 == strcmp(vr::IServerTrackedDeviceProvider_Version, pInterfaceName)) {
		return &device_provider;
	}

	if (pReturnCode) {
		*pReturnCode = vr::VRInitError_Init_InterfaceNotFound;
	}

	return nullptr;
}

首先,我们包含 device_provider.h 头文件,以获取我们的 IServerTrackedDeviceProvider 实现。

接下来,我们定义一个宏,该宏扩展为平台特定的从共享库导出函数的方法。

然后,我们创建一个 DeviceProvider 类的栈分配实例,我们将其添加到运行时中。

之后,我们创建一个返回 void * 的函数,名为 HmdDriverFactory。它接收两个参数:

  • const char *pInterfaceName - 当前请求的接口名称。
  • int *pReturnCode - 指向当我们尝试获取接口时可能发生的 EVRInitError 的指针。

当调用我们支持的 pInterfaceName 时,我们返回指向我们实现的指针。

目前,我们只想在请求 IServerTrackedDeviceProvider_Version 时进行检查,届时我们将返回我们的 IServerTrackedDeviceProvider 实现。

IServerTrackedDeviceProvider_Version 是您当前定位的OpenVR SDK版本中当前 IServerTrackedDeviceProvider 接口的接口版本。

如果我们不支持该接口,我们将 pReturnCode 设置为 VRInitError_Init_InterfaceNotFound 并返回 nullptr,告诉运行时我们不支持该接口。

我们现在有了一个可以被运行时加载的最小驱动!

构建

我们现在有了一个可以构建并稍后加载到运行时中的最小驱动。

在Visual Studio中按 ctrl + shift + b 构建驱动。应该没有错误。

您的驱动将被构建到 build/<my_driver_name>。现在构建文件夹的内容应如下所示:

<project_root>
└── build
    └── <my_driver_name>
        ├── bin
        │   └── win64
        │       └── <my_driver_name>.dll
        └── driver.vrdrivermanifest

链接到 SteamVR

导航到您的SteamVR安装目录(通常为 C:\Program Files (x86)\Steam\steamapps\common\SteamVR)

导航到 bin\win64

运行 vrpathreg.exe adddriver "<path_to_project_dir>/build/<my_driver_name>"

如果您转到 <AppData>/Local/openvr/openvrpaths.vrpath,您应该会在 external_drivers 下看到您的驱动列出。

运行SteamVR!SteamVR监视器中不会显示任何内容,但您可以导航到SteamVR Web控制台(SteamVR 汉堡菜单 > 开发者选项 > Web控制台),您应该会看到来自 IServerTrackedDeviceProvider::Init 的驱动日志 Hello World。

如果有很多日志,请尝试在顶部搜索 <my_driver_name> 或 hello。日志也会写入 C:\Program Files (x86)\Steam\logs\vrserver.txt。

日志行的结构是 <日期> <驱动名称>: <日志消息>。例如,Fri Jan 13 2023 17:46:20.237 - sample: Hello world!。

在此示例中,驱动名为 sample,但您应该搜索您的驱动名称。

image

调试驱动

能够调试驱动需要一些额外的设置。

您必须安装Visual Studio和Visual Studio子进程调试增强工具扩展。 有关安装链接,请参见介绍。

在安装了扩展的Visual Studio中,导航到 调试 > 其他调试目标 > 子进程调试设置...。

单击 启用子进程调试。

在表中,添加一个新条目。在进程名列中,添加 vrserver.exe。

在 vrserver.exe 行中,确保操作设置为 附加调试器。

在 <所有其他进程> 行中,确保操作设置为 不调试。

单击保存。

image

接下来,我们希望当我们单击绿色启动按钮时,SteamVR启动。

确保将项目设置为在 Debug 模式下构建,用于 x64。

在绿色播放按钮和 本地 Windows 调试器 文本旁边,有一个小向下箭头,单击它,然后选择 ALL_BUILD 调试属性。

  • 如果 ALL_BUILD 不是选项,在解决方案资源管理器中右键单击 解决方案 '<my_driver_name>' 属性 > 启动项目。选择 单个启动项目 并设置为 ALL_BUILD。
  • 现在再试一次。

确保 配置 和 平台 为 活动。

选择 配置属性 > 调试。在 命令 中,替换为:C:\Program Files (x86)\Steam\steamapps\common\SteamVR\bin\win64\vrstartup.exe。

单击 确定。

image

现在,当您按 本地 Windows 调试器 时,SteamVR应该启动并加载您的驱动。

在启动SteamVR之前,如果您在 device_provider.cpp 中的 vr::VRDriverLog() 调用处设置断点,Visual Studio应该允许您单步执行您的驱动。

创建设备驱动

单一设备类型(例如Index控制器、Vive手柄)通过 ITrackedDeviceServerDriver 接口表示。

我们将在本教程中制作一个简单的控制器,我们将通过此接口实现它。

与我们的 IServerTrackDeviceProvider 接口从运行时调用的方式不同,我们选择何时将我们的 ITrackDeviceServerDriver 实现添加到运行时。我们将在 IServerTrackDeviceProvider::Activate 中执行此操作。

但首先,让我们实现我们的 ITrackDeviceServerDriver。

在 controller_device.h 中,添加:

#pragma once

#include "openvr_driver.h"

class ControllerDevice : public vr::ITrackedDeviceServerDriver {
public:
    ControllerDevice(vr::ETrackedControllerRole role);

    // 继承自 ITrackedDeviceServerDriver
    virtual vr::EVRInitError Activate(uint32_t unObjectId) override;
    virtual void Deactivate() override;
    virtual void EnterStandby() override;
    virtual void* GetComponent(const char* pchComponentNameAndVersion) override;
    virtual void DebugRequest(const char* pchRequest, char* pchResponseBuffer, uint32_t unResponseBufferSize) override;
    virtual vr::DriverPose_t GetPose() override;

private:
    vr::ETrackedControllerRole role_;
    vr::TrackedDeviceIndex_t device_id_;
};

我们为此类实现了一个构造函数,它接收 vr::ETrackedControllerRole。

我们的 ControllerDevice 将是我们为左手和右手控制器初始化的同一类,因此我们需要某种方式知道哪个控制器是左手,哪个是右手。

我们将创建 ControllerDevice 的实例,因此我们将稍后传入该参数。

注意在 class ControllerDevice : public vr::ITrackedDeviceServerDriver 和定义方法时使用 public。

接下来,我们将在 controller_device.cpp 中定义这些方法:

#include "controller_device.h"

ControllerDevice::ControllerDevice(vr::ETrackedControllerRole role) : role_(role), device_id_(vr::k_unTrackedDeviceIndexInvalid) {};

vr::EVRInitError ControllerDevice::Activate(uint32_t unObjectId) {
    vr::VRDriverLog()->Log("ControllerDevice::Activate");
    
    device_id_ = unObjectId;
    
    return vr::VRInitError_None;
}

void ControllerDevice::Deactivate() {
}

void ControllerDevice::EnterStandby() {
}

void* ControllerDevice::GetComponent(const char* pchComponentNameAndVersion) {
    return nullptr;
}

void ControllerDevice::DebugRequest(const char* pchRequest, char* pchResponseBuffer, uint32_t unResponseBufferSize) {
    if(unResponseBufferSize >= 1)
        pchResponseBuffer[0] = 0;
}

vr::DriverPose_t ControllerDevice::GetPose() {
    return vr::DriverPose_t();
}

我们的 Activate 方法在设备添加到运行时后调用(稍后详述)。

我们可以记录一条消息到控制台,让我们知道控制器已激活。然后,我们还将唯一的设备ID存储在 device_id_ 中,稍后我们将需要它。

Deactivate 在运行时开始退出时调用。

我们在 GetComponents 中没有要返回的“组件”,因此我们应该只返回 nullptr。

应用程序可以调用 DebugRequest 来从驱动请求信息。我们目前不在这里实现任何内容,因为当前没有需要调试的内容。

GetPose 不是由运行时调用的,而是一个我们可以实现的辅助方法,用于计算控制器的姿态。我们将在下一节中填充它。

设置控制器角色

我们必须通知运行时我们控制器的角色(无论是左手还是右手)。我们将在 Activate 中执行此操作。

设备属性存储在容器中,这些容器特定于系统中的每个设备。我们首先需要为要设置属性的设备检索正确的容器,然后我们可以设置属性。

vr::EVRInitError ControllerDevice::Activate(uint32_t unObjectId) {
    vr::VRDriverLog()->Log("ControllerDevice::Activate");
    
    const vr::PropertyContainerHandle_t container = vr::VRProperties()->TrackedDeviceToPropertyContainer(unObjectId);
    vr::VRProperties()->SetInt32Property(container, vr::Prop_ControllerRoleHint_Int32, role_);
    
    device_id_ = unObjectId;
    return vr::VRInitError_None;
}

我们将属性 Prop_ControllerRoleHint_Int32 设置为我们的 role_,该值将在 DeviceProvider 中设置为 TrackedControllerRole_RightHand 或 TrackedControllerRole_LeftHand。

可以设置的属性存储在 ETrackedDeviceProperty 中。您可以在wiki中找到有关此的更多文档。

添加型号编号

我们还可以为控制器设置特定的型号编号。如果我们希望在同一驱动中具有相同角色的不同型号,这将非常有用,因为它提供了一种在设置图标等属性时区分它们的方法。

设置属性 Prop_ModelNumber_String 以设置您设备的型号编号。

vr::EVRInitError ControllerDevice::Activate(uint32_t unObjectId) {
...
    const vr::PropertyContainerHandle_t container = vr::VRProperties()->TrackedDeviceToPropertyContainer(unObjectId);
...
    vr::VRProperties()->SetStringProperty( container, vr::Prop_ModelNumber_String, "<my_controller_model_number>" );
...
    return vr::VRInitError_None;
}

创建设备的姿态

姿态表示设备在给定时刻在空间中的当前状态。它包含设备的位置和方向,以及速度和角速度等其他信息,以及当前的跟踪状态。

由于本教程我们没有使用任何实际的硬件,我们将基于HMD作为参考来设置我们的姿态。

我们将在 ControllerDevice::GetPose 中设置我们的姿态。

DriverPose_t 是OpenVR期望返回姿态的结构体。有几个字段需要正确设置才能使姿态有效:

vr::DriverPose_t pose = { 0 };

pose.poseIsValid = true;
pose.result = vr::TrackingResult_Running_OK;
pose.deviceIsConnected = true;

pose.qWorldFromDriverRotation.w = 1.f;
pose.qDriverFromHeadRotation.w = 1.f;

pose.qRotation.w = 1.f;

该结构体的文档在Wiki中提供。

四元数成员必须具有有效的 w 分量,否则我们的控制器将不会显示。目前,我们将其设置为单位四元数。

同样,我们还应该告诉运行时我们的姿态有效,设备已连接,并且我们正在按预期进行跟踪,否则运行时会将我们的控制器设置为非活动状态。

虽然这是一个有效的姿态,但如果我们尝试使用它,它位于原点(可能在地板中)(尽管,我们尚未将姿态提交给运行时,因此我们的控制器目前不会显示)。

为了使我们的控制器显示在更合理的位置,我们可以使用HMD的位置将控制器偏移到用户稍前方。

要获取HMD的姿态,我们可以使用:

vr::TrackedDevicePose_t hmd_pose{};

vr::VRServerDriverHost()->GetRawTrackedDevicePoses(0.f, &hmd_pose, 1);

GetRawTrackedDevicePoses 接收一个 TrackedDevicePose_t 结构体数组,并用系统中该索引处设备的相关姿态填充每个索引。

HMD姿态将始终位于索引0,因此我们可以只传递一个 TrackedDevicePose_t 结构体的指针,而不是数组。

vr::TrackedDevicePose_t 在 vr::HmdMatrix34_t 结构体中包含设备的位置和方向:一个3x4矩阵。

矩阵的最后一列是HMD的位置。我们将使用这个(以及稍后可选的方向):

pose.vecPosition[0] = role_ == vr::TrackedControllerRole_LeftHand
		? hmd_pose.mDeviceToAbsoluteTracking.m[0][3] - 0.2f
		: hmd_pose.mDeviceToAbsoluteTracking.m[0][3] + 0.2f; // 根据我们是左手还是右手设置x位置偏移(这样控制器不会出现在彼此顶部)。这将使两个控制器看起来并排出现,相距0.4米。

pose.vecPosition[1] = hmd_pose.mDeviceToAbsoluteTracking.m[1][3];		// 无垂直偏移
pose.vecPosition[2] = hmd_pose.mDeviceToAbsoluteTracking.m[2][3] - 0.5f; // 使控制器出现在HMD前半米处(在空间中,-z是向前方向)

我们提交的姿态是在右手坐标系中。将拇指、食指和中指相互成直角伸出。将拇指向右指,食指向上指,中指向自己指。您的拇指是正x轴,食指是正y轴,中指是正z轴(+z朝向您)。

上面的代码将控制器的位置设置为HMD左侧或右侧0.2米处(左手控制器左侧0.2米,右手控制器右侧0.2米),并在HMD前方0.5米处。

最后,我们只需要从 GetPose 返回我们的姿态。总之,我们的 ControllerDevice::GetPose 方法现在应该如下所示:

vr::DriverPose_t ControllerDevice::GetPose() {
    vr::DriverPose_t pose = { 0 };

    pose.poseIsValid = true;
    pose.result = vr::TrackingResult_Running_OK;
    pose.deviceIsConnected = true;
    
    pose.qWorldFromDriverRotation.w = 1.f;
    pose.qDriverFromHeadRotation.w = 1.f;
    
    pose.qRotation.w = 1.f;
    
    vr::TrackedDevicePose_t hmd_pose{};
    vr::VRServerDriverHost()->GetRawTrackedDevicePoses(0.f, &hmd_pose, 1);

    pose.vecPosition[0] = role_ == vr::TrackedControllerRole_LeftHand
                                ? hmd_pose.mDeviceToAbsoluteTracking.m[0][3] - 0.2f
                                : hmd_pose.mDeviceToAbsoluteTracking.m[0][3] + 0.2f;
    
    pose.vecPosition[1] = hmd_pose.mDeviceToAbsoluteTracking.m[1][3];
    pose.vecPosition[2] = hmd_pose.mDeviceToAbsoluteTracking.m[2][3] - 0.5f;

    return pose;
}

向姿态添加方向

我们还可以将HMD的方向添加到我们的设备中。执行此操作需要更多工作,并且是可选的。

如前所述,GetRawTrackedDevicePoses 在 vr::HmdMatrix34_t 结构体中提供姿态方向和位置。

矩阵的前3列代表当前HMD方向的3x3旋转矩阵。但是,我们需要将其转换为四元数以提交给OpenVR。

samples/util/vrmath.h 为此提供了实用函数:

// 3x3 或 3x4 矩阵
template < class T >
vr::HmdQuaternion_t HmdQuaternion_FromMatrix( const 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;
}

因此,这可以变为:

const vr::HmdQuaternion_t hmd_orientation = HmdQuaternion_FromMatrix(hmd_pose.mDeviceToAbsoluteTracking);

现在,我们可以将其添加到我们的姿态中:

pose.qRotation = hmd_orientation;

总之,我们的 GetPose 现在应该如下所示:

vr::DriverPose_t ControllerDevice::GetPose() {
	vr::DriverPose_t pose = { 0 };

	pose.poseIsValid = true;
	pose.result = vr::TrackingResult_Running_OK;
	pose.deviceIsConnected = true;

	pose.qWorldFromDriverRotation.w = 1.f;
	pose.qDriverFromHeadRotation.w = 1.f;

	pose.qRotation.w = 1.f;

	vr::TrackedDevicePose_t hmd_pose{};
	vr::VRServerDriverHost()->GetRawTrackedDevicePoses(0.f, &hmd_pose, 1);

	const vr::HmdQuaternion_t hmd_orientation = HmdQuaternion_FromMatrix(hmd_pose.mDeviceToAbsoluteTracking);
	pose.qRotation = hmd_orientation;

	pose.vecPosition[0] = role_ == vr::TrackedControllerRole_LeftHand
		? hmd_pose.mDeviceToAbsoluteTracking.m[0][3] - 0.2f
		: hmd_pose.mDeviceToAbsoluteTracking.m[0][3] + 0.2f;

	pose.vecPosition[1] = hmd_pose.mDeviceToAbsoluteTracking.m[1][3];
	pose.vecPosition[2] = hmd_pose.mDeviceToAbsoluteTracking.m[2][3] - 0.5f;

	return pose;
}

提交姿态

我们目前没有合适的地方来更新姿态。

对于本教程,我们将在 RunFrame 中更新姿态。实际上,您不应该在 RunFrame 中这样做,首先因为它依赖于帧率,其次因为它运行在运行时的主循环中,所以在这里阻塞会导致运行时也阻塞。

推荐的方法是创建另一个线程,该线程以设定的间隔或在从设备接收数据时更新姿态。您可以查看 samples/drivers/simplecontroller 了解其实现。

ITrackedDeviceServerDriver 没有 RunFrame,因此我们将在派生类中创建一个方法,并从我们的 IServerTrackedDeviceProvider 实现中调用该方法(这将在下一节中完成)。

class ControllerDevice : public vr::ITrackedDeviceServerDriver {
  public:
    ...
    void RunFrame();
    ...
};

现在我们有了一个姿态,我们需要将其提交给OpenVR。我们可以通过调用 vr::VRServerDriverHost()->TrackedDevicePoseUpdated 来实现。我们将在 ControllerDevice::RunFrame 中进行此调用:

void ControllerDevice::RunFrame() {
    vr::VRServerDriverHost()->TrackedDevicePoseUpdated(device_id_, GetPose(), sizeof(vr::DriverPose_t));
}

将设备添加到运行时

现在我们有了一个简单的设备全部设置完成,我们可以将其添加到运行时。

首先,我们应该将指向 ControllerDevice 实现的指针存储在 DeviceProvider 内:

将 my_left_device_ 和 my_right_device_ 作为 std::unique_ptr<ControllerDevice> 添加到 DeviceProvider 类:

#pragma once
#include <memory>

#include "controller_device.h"
#include "openvr_driver.h"

class DeviceProvider : public vr::IServerTrackedDeviceProvider {
    ...
private:
    std::unique_ptr<ControllerDevice> my_left_device_;
    std::unique_ptr<ControllerDevice> my_right_device_;
};

与我们的 IServerTrackedDeviceProvider 实现(在启动时由运行时加载)不同,我们可以选择何时将设备添加到运行时。

当我们准备好向运行时添加设备时,我们需要在 DeviceProvider 中调用 IVRServerDriverHost::TrackedDeviceAdded。这将告诉运行时我们的驱动有一个新设备要添加,并将其排队等待激活。

我们将在 DeviceProvider::Init 中激活设备,以便在启动时将设备添加到运行时,但它们可以在会话期间的任何时候添加(可能在检测到控制器连接时)。

vr::EVRInitError DeviceProvider::Init(vr::IVRDriverContext* pDriverContext) {
    VR_INIT_SERVER_DRIVER_CONTEXT(pDriverContext);
    
    vr::VRDriverLog()->Log("Hello world!");

    my_left_device_ = std::make_unique<ControllerDevice>(vr::TrackedControllerRole_LeftHand);
    vr::VRServerDriverHost()->TrackedDeviceAdded("<my_left_serial_number>",
                                                vr::TrackedDeviceClass_Controller,
                                                my_left_device_.get());
    
    my_right_device_ = std::make_unique<ControllerDevice>(vr::TrackedControllerRole_RightHand);
    vr::VRServerDriverHost()->TrackedDeviceAdded("<my_right_serial_number>",
                                                vr::TrackedDeviceClass_Controller,
                                                my_right_device_.get());
    
    return vr::VRInitError_None;
}

添加到运行时的每个设备都需要一个唯一的序列号。这必须是每个设备的唯一字符串。

然后我们需要告诉运行时这是什么类别的设备。在这种情况下,我们正在添加一个控制器,因此我们传入 vr::TrackedDeviceClass_Controller。

最后,我们传入我们刚刚实例化的设备的指针。

最后,我们还应该在 DeviceProvider::RunFrame 中添加对 ControllerDevice::RunFrame 的调用,以便更新我们的姿态。

void DeviceProvider::RunFrame() {
    if(my_left_device_ != nullptr) {
        my_left_device_->RunFrame();
    }
    
    if(my_right_device_ != nullptr) {
        my_right_device_->RunFrame();
    }
}

启动SteamVR,您应该会看到类似游戏手柄的模型漂浮在HMD前方的某个位置!

确保在测试时处于空的SteamVR环境中;我们尚未设置设备以在应用程序中使用。

image

您还应该会在SteamVR中看到两个新图标。这些是您的控制器设备。我们稍后会配置图标。它们应该是纯色渐变。如果不是,那么您的姿态有问题。

如果这些没有出现,那么您在将设备添加到SteamVR时遇到了问题。确保驱动已启用(SteamVR设置 > 启动/关闭 > 管理附加组件)。您需要启用高级选项才能看到此选项。

设备输入

现在我们有了一个设备设置,我们可以向其添加一些输入。

我们告诉运行时我们的设备具有哪些物理输入,然后可以在运行时中绑定到游戏内操作(例如开枪或跳跃)。

为此,我们向驱动添加一个输入配置文件。这是一个JSON文件,描述了我们的设备具有的物理输入。

我们将其添加到 <my_driver_name>/resources/input/<my_device_name>_profile.json:

{
  "jsonid": "input_profile",
  "controller_type": "<my_device_name>",
  "input_bindingui_mode": "controller_handed",
  "input_source": {
    "/input/a": {
      "type": "button",
      "touch": true,
      "binding_image_point": [
        80,
        60
      ],
      "order": 1
    },
    "/input/trigger": {
      "type": "trigger",
      "binding_image_point": [
        250,
        60
      ],
      "order": 2
    },
    "/input/joystick": {
      "type": "joystick",
      "click": true,
      "binding_image_point": [
        60,
        60
      ],
      "order": 3
    },
    "/output/haptic": {
      "type": "vibration",
      "binding_image_point": [
        300,
        150
      ],
      "order": 4
    }
  }
}

controller_type 是您当前设备的名称,例如 vive_controller 或 knuckles(Index控制器)。这应该是一个简短、无空格的字符串,更多地用于开发目的而不是向用户显示。您可以在本地化文件中设置人类可读的名称。

input_bindingui_mode 是输入绑定UI将使用的模式。这将是 controller_handed,因为我们的设备是可以手持的控制器。

input_source 是我们的设备具有的输入列表。每个输入以其路径作为键,并以一个对象作为值,提供有关该输入的信息。

注意 my_joystick、my_a 和 my_trigger 上的 click 或 touch 属性。这些告诉运行时输入源具有感知点击或触摸事件的能力。

输入源路径应遵循:<input/output>/<my_input_source_name>。这应该是设备上的物理“输入源”,例如单个按钮、操纵杆、触发器、触摸板等。

有关输入设备类型的文档,请参阅相关部分。

设备还有一个可用的触觉振动组件,我们使用 /output/haptic 指定。

现在您的项目文件应如下所示:

<project_root>
├── <my_driver_name>
│   ├── driver.vrdrivermanifest
│   └── resources
│       └── input
│           └── <my_device_name>_profile.json
└── src
    ├── hmd_driver_factory.cpp
    ├── device_provider.h
    ├── device_provider.cpp
    ├── controller_device.h
    └── controller_device.cpp

创建设备输入的句柄

现在我们已经创建了输入配置文件中的一些输入,我们可以创建它们的句柄,以便在设备更新时能够更新它们。

为此,我们需要为每个想要更新的输入创建一个 vr::VRInputComponentHandle_t。

我们将为所有输入句柄创建一个数组,并使用一个枚举来访问它们:

#include <array>

enum InputHandles {
	kInputHandle_A_click,
	kInputHandle_A_touch,
	kInputHandle_trigger_value,
	kInputHandle_trigger_click,
	kInputHandle_joystick_x,
	kInputHandle_joystick_y,
    kInputHandle_joystick_click,
	kInputHandle_haptic,
	kInputHandle_COUNT
};

class ControllerDevice : public vr::ITrackedDeviceServerDriver {
    ...
private:
    std::array<vr::VRInputComponentHandle_t, kInputHandle_COUNT> my_input_handles_;
};

我们需要为每个单独的输入组件(操纵杆的 x 和 y,A 按钮的 click 和 touch 等)创建句柄。

输入的句柄存储在特定于每个设备的容器中。我们首先需要获取此容器,以获取每个输入组件应更新的正确句柄。

我们还需要告诉运行时我们的输入配置文件位于何处。我们可以在 ControllerDevice::Activate 方法中执行此操作,并创建组件:

vr::EVRInitError ControllerDevice::Activate(uint32_t unObjectId) {
    ...
    
    // 属性存储在容器中,通常每个设备索引一个容器。我们需要获取此容器来设置我们想要的属性,因此我们调用此函数以获取其句柄。
	vr::PropertyContainerHandle_t container = vr::VRProperties()->TrackedDeviceToPropertyContainer( device_id_ );
    
    // 告诉运行时我们的输入配置文件位于何处
    vr::VRProperties()->SetStringProperty(container, vr::Prop_InputProfilePath_String,
                                          "{<my_driver_name>}/resources/input/<my_device_name>_profile.json");
    
	vr::VRDriverInput()->CreateBooleanComponent(container, "/input/a/click", &my_input_handles_[kInputHandle_A_click]);
	vr::VRDriverInput()->CreateBooleanComponent(container, "/input/a/touch", &my_input_handles_[kInputHandle_A_touch]);

	vr::VRDriverInput()->CreateScalarComponent(container, "/input/trigger/value", &my_input_handles_[kInputHandle_trigger_value],
		vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedOneSided);
	vr::VRDriverInput()->CreateBooleanComponent(container, "/input/trigger/click", &my_input_handles_[kInputHandle_trigger_click]);

	vr::VRDriverInput()->CreateScalarComponent(container, "/input/joystick/x", &my_input_handles_[kInputHandle_joystick_x],
		vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedTwoSided);
	vr::VRDriverInput()->CreateScalarComponent(container, "/input/joystick/y", &my_input_handles_[kInputHandle_joystick_y],
		vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedTwoSided);
	vr::VRDriverInput()->CreateBooleanComponent(container, "/input/joystick/click", &my_input_handles_[kInputHandle_joystick_click]);

	vr::VRDriverInput()->CreateHapticComponent(container, "/output/haptic", &my_input_handles_[kInputHandle_haptic]);
    
    return vr::VRInitError_None;
}

将驱动名称用花括号括起来会扩展到根驱动程序文件夹的完整路径。您可以使用这些来设置路径属性。

更新输入

现在我们有了输入的句柄,我们可以随时更新它们。

实际上,输入应该在从设备接收到新数据时更新,但由于本教程没有物理设备,我们将再次在 RunFrame 中更新。

我们现在可以实现 RunFrame 方法来更新我们的输入:

void ControllerDevice::RunFrame() {
    vr::VRServerDriverHost()->TrackedDevicePoseUpdated(device_id_, GetPose(), sizeof(vr::DriverPose_t));
    
    vr::VRDriverInput()->UpdateBooleanComponent(my_input_handles_[kInputHandle_A_click], 1, 0.0);
    vr::VRDriverInput()->UpdateBooleanComponent(my_input_handles_[kInputHandle_A_touch], 1, 0.0);
    vr::VRDriverInput()->UpdateScalarComponent(my_input_handles_[kInputHandle_trigger_value], 0.5, 0.0);
    vr::VRDriverInput()->UpdateScalarComponent(my_input_handles_[kInputHandle_joystick_x], 0.5, 0.0);
    vr::VRDriverInput()->UpdateScalarComponent(my_input_handles_[kInputHandle_joystick_y], 0.5, 0.0);
    vr::VRDriverInput()->UpdateBooleanComponent(my_input_handles_[kInputHandle_joystick_click], 1, 0.0);
}

我们提供要更新的输入的句柄,要更新到的值,以及自上次输入以来的时间。

测试输入

我们现在可以在VR中测试输入以确保它们正常工作。SteamVR提供了一个测试控制器界面,将直观显示我们的数据:

image

image

触觉输出

触觉输出在应用程序发出事件时发生。我们可以使用 IVRServerDriverHost::PollNextEvent 轮询这些事件。

我们应该只在一个地方轮询事件(即,不在每个设备类中),因为事件作用域是每个驱动,并且在轮询时从队列中弹出。这意味着在每个设备类中轮询事件将导致只有一个设备接收到每个事件。

因此,我们将在 IServerTrackedDeviceProvider::RunFrame 中轮询,然后将事件传递给每个设备类处理。

我们将向 ControllerDevice 类添加一个 HandleEvent 方法:

class ControllerDevice : public vr::ITrackedDeviceServerDriver {
public:
    ...
    void HandleEvent(const vr::VREvent_t &vrevent);
};

并在 DeviceProvider::RunFrame 中调用它:

void DeviceProvider::RunFrame() {
    vr::VREvent_t vrevent;
    while (vr::VRServerDriverHost()->PollNextEvent(&vrevent, sizeof(vrevent))) {
        if (my_left_device_ != nullptr) {
            my_left_device_->HandleEvent(vrevent);
        }
        if (my_right_device_ != nullptr) {
            my_right_device_->HandleEvent(vrevent);
        }
    }
    ...
}

我们现在可以实现 HandleEvent 方法:

void ControllerDevice::HandleEvent(const vr::VREvent_t& vrevent) {
	switch (vrevent.eventType) {
        case vr::VREvent_Input_HapticVibration: {
            if (vrevent.data.hapticVibration.componentHandle == my_input_handles_[kInputHandle_haptic]) {
                vr::VRDriverLog()->Log("Buzz!");
            }
            break;
        }
	}
}

有关触觉事件的实现,请参阅示例和文档。

绑定

现在我们已经设置了输入,我们需要告诉运行时如何将它们映射到游戏内操作。我们使用绑定配置文件来实现这一点。

绑定配置文件可以在SteamVR绑定界面中配置,该界面可从SteamVR设置菜单访问(SteamVR设置 > 控制器 > 显示旧版控制器绑定UI)。

在列表中选择要为其配置绑定的应用程序。我们将使用《半条命:爱莉克斯》作为示例:

image

接下来,确保选择您的控制器进行绑定配置。

image

单击 创建新绑定。

下一个界面应如下所示: image

顶部是选项卡,将操作分成某些组,以使绑定更容易处理。它们由应用程序定义。

将鼠标悬停在选项卡上会显示尚未绑定的操作。请务必填写所有必需的绑定。

要创建绑定,请单击输入旁边的 +,选择它是哪种输入,然后选择要绑定的操作。

image

image

image

对您想要绑定的所有操作执行此操作。您必须绑定所有必需的绑定,否则您将无法保存绑定配置文件。

一旦您已将应用程序的所有输入绑定到操作,请单击 保存个人绑定。这将保存到 Documents\steamvr\input。 导航回并确保您刚刚设置的绑定配置文件处于活动状态。

默认绑定

您可以将这些绑定与您的驱动一起提供,作为默认绑定配置文件(如果用户尚未配置)。

您创建的绑定配置文件位于 Documents\steamvr\input。复制适当的绑定文件(检查应用程序的appId)。

将绑定配置文件复制并粘贴到 <my_driver_name>/resources/input/default_bindings。

现在您的文件结构应如下所示:

<project_root>
├── <my_driver_name>
│   ├── driver.vrdrivermanifest
│   └── resources
│       └── input
│           ├── <my_device_name>_profile.json
│           └── default_bindings
│               └── steam.app.<appId>.<my_controller_name>.json
└── src
    ├── hmd_driver_factory.cpp
    ├── device_provider.h
    ├── device_provider.cpp
    ├── controller_device.h
    └── controller_device.cpp

我们在输入配置文件中指定我们的默认绑定。

向输入配置文件添加一个 default_bindings 字段:

{
  "default_bindings": [
    {
      "app_key": "steam.app.<appId>",
      "binding_url": "default_bindings/steam.app.<appId>.<my_controller_name>.json"
    }
  ]
}

例如,对于名为 sample_controller 的设备,为《半条命:爱莉克斯》的配置如下:

{
  "default_bindings": [
    {
      "app_key": "steam.app.546560",
      "binding_url": "default_bindings/steam.app.546560_sample_controller.json"
    }
  ]
}

现在我们的输入配置文件应如下所示:

{
  "jsonid": "input_profile",
  "controller_type": "<my_device_name>",
  "input_bindingui_mode": "controller_handed",
  "input_source": {
    "/input/a": {
      "type": "button",
      "touch": true,
      "binding_image_point": [
        80,
        60
      ],
      "order": 1
    },
    "/input/trigger": {
      "type": "trigger",
      "binding_image_point": [
        250,
        60
      ],
      "order": 2
    },
    "/input/joystick": {
      "type": "joystick",
      "click": true,
      "binding_image_point": [
        60,
        60
      ],
      "order": 3
    },
    "/output/haptic": {
      "type": "vibration",
      "binding_image_point": [
        300,
        150
      ],
      "order": 4
    }
  },
  "default_bindings": [
    {
      "app_key": "steam.app.546560",
      "binding_url": "default_bindings/steam.app.546560_sample_controller.json"
    }
  ]
}

向 default_bindings 数组追加以添加控制器的其他绑定。

设备图标

目前,我们在SteamVR监视器中有代表我们控制器的默认图标。我们可以自定义这些图标以帮助用户了解系统中每个设备的状态。

我们将把图标添加到 resources/icons 文件夹。我们可以随意命名它们。

推荐图标

我们的设备应该有一些图标。这些是:

  • 设备正常运行
    • 这应该是一个实心、彩色的图标,提供设备的视觉表示。
  • 设备关闭
    • 设备的阴影化视觉表示。
  • 设备警报
    • 设备处于就绪状态且顶部有警报图标的视觉表示。这表示设备有一些需要告知用户的信息,例如需要固件更新。
  • 设备搜索中
    • 这应该是一个作为.gif的“呼吸”动画,淡入淡出以显示设备尚未建立跟踪。
  • 设备搜索中警报
    • 这应该是一个作为.gif的“呼吸”动画,淡入淡出以显示设备尚未建立跟踪,但顶部有警报图标,表示设备有一些需要告知用户的信息,例如需要固件更新。
  • 低电量
    • 设备电量低。这应该是显示设备电量低的设备视觉表示。

图标必须是以下尺寸之一:

  • HMD:50x32 或 100x64(见下文)png/gif
  • 其他(控制器、追踪器等):32x32 或 64x64(见下文)png/gif

图标的文件名指示运行时使用的图标尺寸。在文件名后附加 @2x 以使用100x64的HMD图标或64x64的控制器图标。例如:

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

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

我们将把图标添加到 resources/icons 文件夹。我们可以随意命名它们,并稍后指定哪些图标链接到哪些属性。

本目录 sample/resources/icons 中指定的文件是:

  • controller_status_standby.png
  • controller_status_error.png
  • controller_status_off.png
  • controller_status_ready.png
  • controller_status_ready_alert.png
  • controller_status_ready_low.png
  • controller_status_searching.gif
  • controller_status_searching_alert.gif

我们现在将在 resources/ 中创建一个名为 driver.vrresources 的新文件,我们可以在其中指定哪些图标链接到设备的哪些属性。

在 <my_driver_name>/resources/driver.vrresources 中添加:

{
  "jsonid": "vrresources",
  "statusicons": {
    "Controller": {
      "Prop_NamedIconPathDeviceOff_String": "{<my_driver_name>}/icons/controller_status_off.png",
      "Prop_NamedIconPathDeviceSearching_String": "{<my_driver_name>}/icons/controller_status_searching.gif",
      "Prop_NamedIconPathDeviceSearchingAlert_String": "{<my_driver_name>}/icons/controller_status_searching_alert.gif",
      "Prop_NamedIconPathDeviceReady_String": "{<my_driver_name>}/icons/controller_status_ready.png",
      "Prop_NamedIconPathDeviceReadyAlert_String": "{<my_driver_name>}/icons/controller_status_ready_alert.png",
      "Prop_NamedIconPathDeviceNotReady_String": "{<my_driver_name>}/icons/controller_status_error.png",
      "Prop_NamedIconPathDeviceStandby_String": "{<my_driver_name>}/icons/controller_status_standby.png",
      "Prop_NamedIconPathDeviceAlertLow_String": "{<my_driver_name>}/icons/controller_status_ready_low.png"
    }
  }
}

用花括号包裹的 <my_driver_name>(即 {<my_driver_name>})指定到驱动文件夹绝对目录的通配符。)

由于我们左右手控制器使用相同的图标,我们可以使用通用键 Controller 来指定两个控制器的图标。

如果您想分别指定左右手控制器的图标,可以使用键 LeftController 和 RightController:

{
  "jsonid": "vrresources",
  "statusicons": {
    "LeftController": {
      "Prop_NamedIconPathDeviceOff_String": "{<my_driver_name>}/icons/controller_status_off.png",
      "Prop_NamedIconPathDeviceSearching_String": "{<my_driver_name>}/icons/controller_status_searching.gif",
      "Prop_NamedIconPathDeviceSearchingAlert_String": "{<my_driver_name>}/icons/controller_status_searching_alert.gif",
      "Prop_NamedIconPathDeviceReady_String": "{<my_driver_name>}/icons/controller_status_ready.png",
      "Prop_NamedIconPathDeviceReadyAlert_String": "{<my_driver_name>}/icons/controller_status_ready_alert.png",
      "Prop_NamedIconPathDeviceNotReady_String": "{<my_driver_name>}/icons/controller_status_error.png",
      "Prop_NamedIconPathDeviceStandby_String": "{<my_driver_name>}/icons/controller_status_standby.png",
      "Prop_NamedIconPathDeviceAlertLow_String": "{<my_driver_name>}/icons/controller_status_ready_low.png"
    },
    "RightController": {
      "Prop_NamedIconPathDeviceOff_String": "{<my_driver_name>}/icons/controller_status_off.png",
      "Prop_NamedIconPathDeviceSearching_String": "{<my_driver_name>}/icons/controller_status_searching.gif",
      "Prop_NamedIconPathDeviceSearchingAlert_String": "{<my_driver_name>}/icons/controller_status_searching_alert.gif",
      "Prop_NamedIconPathDeviceReady_String": "{<my_driver_name>}/icons/controller_status_ready.png",
      "Prop_NamedIconPathDeviceReadyAlert_String": "{<my_driver_name>}/icons/controller_status_ready_alert.png",
      "Prop_NamedIconPathDeviceNotReady_String": "{<my_driver_name>}/icons/controller_status_error.png",
      "Prop_NamedIconPathDeviceStandby_String": "{<my_driver_name>}/icons/controller_status_standby.png",
      "Prop_NamedIconPathDeviceAlertLow_String": "{<my_driver_name>}/icons/controller_status_ready_low.png"
    }
  }
}

实际上,LeftController 和 RightController 的每个图标路径都会不同。

或者您可以按型号编号指定图标:

{
  "jsonid": "vrresources",
  "statusicons": {
    "<my_model_number>": {
      "Prop_NamedIconPathDeviceOff_String": "{<my_driver_name>}/icons/controller_status_off.png",
      "Prop_NamedIconPathDeviceSearching_String": "{<my_driver_name>}/icons/controller_status_searching.gif",
      "Prop_NamedIconPathDeviceSearchingAlert_String": "{<my_driver_name>}/icons/controller_status_searching_alert.gif",
      "Prop_NamedIconPathDeviceReady_String": "{<my_driver_name>}/icons/controller_status_ready.png",
      "Prop_NamedIconPathDeviceReadyAlert_String": "{<my_driver_name>}/icons/controller_status_ready_alert.png",
      "Prop_NamedIconPathDeviceNotReady_String": "{<my_driver_name>}/icons/controller_status_error.png",
      "Prop_NamedIconPathDeviceStandby_String": "{<my_driver_name>}/icons/controller_status_standby.png",
      "Prop_NamedIconPathDeviceAlertLow_String": "{<my_driver_name>}/icons/controller_status_ready_low.png"
    }
  }
}

现在您的文件结构应如下所示:

<project_root>
├── <my_driver_name>
│   ├── driver.vrdrivermanifest
│   └── resources
│       ├── input
│       │   ├── <my_device_name>_profile.json
│       │   └── default_bindings
│       │       └── steam.app.<appId>.<my_controller_name>.json
│       ├── icons
│       │   └── icon files...
│       └── driver.vrresources
└── src
    ├── hmd_driver_factory.cpp
    ├── device_provider.h
    ├── device_provider.cpp
    ├── controller_device.h
    └── controller_device.cpp
许可协议:  CC BY 4.0
分享

相关文章

下一篇

上一篇

OpenVR Driver API 中文文档

最近更新

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

热门标签

Halo

目录

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

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

使用 Halo 主题 Chirpy