libsurvive
Libsurvive 是一套工具和库,可以在基于lighthouse 和 vive的系统上实现 6 自由度追踪,它完全开源,可以在任何设备上运行。目前支持 SteamVR 1.0 和 SteamVR 2.0 两代设备,应该能支持任何商业可用的追踪对象。
由于重点在于追踪,它不会独立运行 HMD。如果需要开源的 HMD 运行栈,请参见 monado。
大部分开发讨论都在 Discord 上进行。加入我们的 Discord 聊天和讨论!。也可以通过矩阵桥加入讨论。
这里有一个示例应用,展示了 libsurvive 在 Godot 中运行控制器和 HMD:
目录
快速开始
Debian
git clone https://github.com/cntools/libsurvive.git --recursive
cd libsurvive
sudo cp ./useful_files/81-vive.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
sudo apt update && sudo apt install build-essential zlib1g-dev libx11-dev libusb-1.0-0-dev freeglut3-dev liblapacke-dev libopenblas-dev libatlas-base-dev cmake
make
插入耳机/追踪器/控制器等,然后运行:
./bin/survive-cli
这应该会校准并显示您的设置。
对于可视化,您可以下载 websocketd 的二进制文件,或启用实验性 apt 源并使用 sudo apt install websocketd
。之后您可以运行:
./bin/survive-websocketd & xdg-open ./tools/viz/index.html
Windows
如果您的系统路径中已安装 cmake
,只需右键单击 make.ps1
脚本并选择"使用 PowerShell 运行"即可。
更手动的方法是在 CMake GUI 等工具中打开 CMakeLists 文件,使用 Visual Studio 生成器从源代码构建。这还可以让您设置各种构建选项。构建过程使用 NuGet 获取必要的开发依赖项。生成项目后,在 Visual Studio 中打开解决方案并运行全部构建。
假设您将 Websocketd 放在系统路径中的某个位置,它应该可以与可视化工具一起使用。在构建二进制文件夹中(如果从 make.ps1
构建,则为 ./build-win/Release
)应该有一个 survive-websocketd.ps1
,可以作为 PowerShell 文件运行。
在 Windows 上开始使用 libsurvive 的最简单方法可能是查看发布的二进制文件。
当前状态
目前追踪和设备枚举功能运行得相当不错;但测试基础并不是非常广泛,工具也不如 SteamVR 中捆绑的相应工具那么完善。我们正在努力量化追踪的准确性并改善用户体验。
路线图
以下是短期计划中的一些非常松散的内容:
- 校准的动态修正。这将检测并静默重新校准可能已移动的灯塔。最终目标是让用户永远不会意识到任何校准要求。
- 安卓二进制文件/移植版本
- libsurvive 作为追踪系统的准确性和精确度的硬数据。如果有人愿意贡献时间在 CNC 上进行测试,请加入我们的 discord
- 更好地处理数据匮乏 -- 如果 USB 或无线连接停顿时间过长,追踪偶尔会在短时间内出现故障。
- 在 Windows 上使用类似 usbmon 的工具
入门指南
如果您按照快速入门指南操作,您会注意到第一件要做的事(对于 Linux)是安装 udev 规则:
sudo cp ./useful_files/81-vive.rules to /etc/udev/rules.d/
sudo udevadm control --reload-rules && udevadm trigger
这允许在不需要 root 权限的情况下使用这些设备。Windows 上不需要这些步骤。
之后,当您运行 survive-cli
或 survive-websocketd
时,它应该能识别任何插入的 Vive 设备并开始校准和追踪这些设备。
重要提示:为获得最佳效果,请关闭 SteamVR。根据系统情况,libsurvive 要么会导致 SteamVR 失去与设备的连接,要么会与之争夺带宽
校准
校准是确定灯塔相对于被跟踪物体的设置位置的过程。首次运行 libsurvive 时,可能需要长达 10 秒的时间与灯塔通信并确定它们的位置。
只要物体暂时静止,校准就会持续整合物体数据。因此,你可能会注意到灯塔在获得良好锁定时会稍微移动。后续运行时移动应该会减少。
一旦完成这个过程,它就会保存在 XDG_CONFIG_HOME/libsurvive
的 config.json
文件中。如果删除这个文件,系统会重新校准;但使用 --force-calibrate
标志会更快。某些驱动程序会改变这个文件的名称 -- 特别是录制时会使用 <event_file>.json
。
如果你有一个大空间,无法将单个设备集中放置以"看到"所有灯塔,你可以先校准几个灯塔,然后将被跟踪的物体移动到未校准灯塔的视野内,同时保持它在至少一个已校准灯塔的视野内。将其放置不动;剩余的灯塔就应该能校准了。
重要提示:当灯塔被移动时自动重置校准正在计划中,但目前尚未实现。如果其中一个已校准的灯塔被移动,你要么通过删除 config.json 文件重新校准,要么在使用任何 libsurvive 工具时传入 --force-calibrate
参数。
可视化
主要的可视化工具是一个通过 (websocketd)[http://websocketd.com/] 提供数据的 THREE.js 页面。要使用这个工具,运行 survive-websocketd [options]
并在浏览器中打开克隆仓库根目录下的 ./tools/viz/index.html
。
libsurvive 工具
survive-cli
- 这是库的主要命令行界面;实际上只是库的一个非常薄的包装。survive-websocketd
- 一个通过websocketd
运行survive-cli
的脚本,设置了所有适当的标志。sensors-readout
- 在 ncurses 显示中显示原始传感器信息。
在你自己的应用程序中使用 libsurvive
低级 API
本节主要关注从库中消费数据;有关如何提供数据的信息,请参阅驱动程序部分。
扩展和使用 libsurvive 的主要方式是使用库暴露的各种回调来从系统获取信息。如果你需要访问所有输入数据 -- IMU 数据、单独的光数据和/或最终姿态数据,建议以这种方式使用 libsurvive。但需要注意不要使系统负担过重。通常这些回调是从收集数据的线程中调用的;所以如果你在处理数据时有不必要的延迟,数据将被丢弃,这会导致跟踪性能下降。
你可以通过以下方式安装自定义钩子:
<hook-name>_process_func survive_install_<hook-name>_fn(SurviveContext *ctx, <hook-name>_process_func fbp);
这会返回之前为该特定钩子设置的函数;你可以选择在自己的回调中调用它。这比只让回调返回 true
或 false
稍微麻烦一些;但允许灵活地在你的代码之前或之后调用之前定义的函数,或者完全不调用。
这些钩子在 libsurvive 内部使用;如果你提供了一个钩子但既不调用之前定义的函数也不调用默认函数,某些数据将无法到达姿态估计器。
SurviveContext
和 SurviveObject
都有一个 user_ptr
变量,它被初始化为零并且在内部未使用,旨在供库使用者出于自己的目的进行设置。这允许你安装钩子,而不需要使用全局变量。
这些接口相对稳定,但不保证不会改变。
看看仓库顶层的其他 libsurvive 工具,了解低级 API 的使用示例:
- survive-cli.c
- sensors-readout.c
- simple_pose_test.c
高级 API
对于只需要尽可能快地处理位置和速度数据的应用程序,推荐使用高级 Simple
API。它有几个主要优势:
- 用户代码在自己的线程中运行;所以你不会使 libsurvive 数据饥饿
- 对低级 API 变化的隔离度更高;所以你可以更容易地升级 libsurvive 版本
在 C
中,主循环逻辑通常如下所示:
while (survive_simple_wait_for_update(actx) && keepRunning) {
for (const SurviveSimpleObject *it = survive_simple_get_next_updated(actx); it != 0;
it = survive_simple_get_next_updated(actx)) {
SurvivePose pose;
uint32_t timecode = survive_simple_object_get_latest_pose(it, &pose);
printf("%s %s (%u): %f %f %f %f %f %f %f\n", survive_simple_object_name(it),
survive_simple_serial_number(it), timecode, pose.Pos[0], pose.Pos[1], pose.Pos[2], pose.Rot[0],
pose.Rot[1], pose.Rot[2], pose.Rot[3]);
}
}
应用程序接口的示例代码可以在 api_example.c 中找到。
更简单的 API 的完整头文件可在这里找到。
Python 绑定
Python 绑定可通过 https://pypi.org/project/pysurvive/ 获得,适用于 Windows 和 Linux 上的 python3。你可以通过以下命令安装:
pip install pysurvive
要构建 Python 绑定,在仓库根目录运行 python setup.py install
。这应该会安装 pysurvive
包。
一个流式输出姿态的示例:
import pysurvive
import sys
actx = pysurvive.SimpleContext(sys.argv)
for obj in actx.Objects():
print(obj.Name())
while actx.Running():
updated = actx.NextUpdated()
if updated:
print(updated.Name(), updated.Pose())
在 ./bindings/python
中有更多示例。
C# 绑定
C# 绑定包装了低级访问 API 和更高级的易用 API。建议使用更高级的 API,因为低级 API 严重依赖回调,而封送使其容易出现不易解决的错误。
标准二进制文件可在 https://www.nuget.org/packages/libsurvive.net/ 获得。你可以通过 Visual Studio 的 NuGet 包管理器工具为指定的 C# 项目安装它们。
使用 Visual Studio 或运行以下命令构建这里的解决方案:
dotnet build -c Release
从终端生成二进制文件 libsurvive.net.dll
。这在 Linux 和 Windows 上都适用;但文件名仍以 dll
结尾。当你运行这个二进制文件时,libsurvive.so
需要位于同一目录下(包含所需插件),或者在系统路径中。
高级 API 通过 libsurvive.SurviveAPI
对象暴露。它的使用非常简单;它可以轮询对象位置或按钮事件的更新,如 Demo 项目 中所示:
using libsurvive;
using System;
namespace Demo
{
class Program
{
static void Main() {
string[] args = System.Environment.GetCommandLineArgs();
var api = new SurviveAPI(args);
while (api.WaitForUpdate()) {
SurviveAPIOObject obj;
while ((obj = api.GetNextUpdated()) != null) {
Console.WriteLine(obj.Name + ": " + obj.LatestPose);
}
}
api.Close();
}
}
}
它也旨在易于集成到基于帧更新的代码库中;例如在 Unity 示例 中:
// 每帧调用一次 Update
void Update() {
var updated = survive?.GetNextUpdated();
if (updated == null)
return;
var updatedObject = getObject(updated.Name);
Vector3 newPosition = Vector3.zero;
Quaternion newRotation = Quaternion.identity;
SurvivePose pose = updated.LatestPose;
newPosition.x = (float) pose.Pos[0];
newPosition.y = (float) pose.Pos[1];
newPosition.z = (float) pose.Pos[2];
newRotation.w = (float) pose.Rot[0];
newRotation.x = (float) pose.Rot[1];
newRotation.y = (float) pose.Rot[2];
newRotation.z = (float) pose.Rot[3];
updatedObject.transform.localPosition = newPosition;
updatedObject.transform.localRotation = newRotation;
}
数据录制
有很多因素可能导致不良的追踪或校准结果,考虑到设备、配置和使用场景的多样性,在尝试解决 bug 时,如果有 bug 的数据记录会非常有帮助。这些记录还可以添加到我们的 CI 系统中,以便对该使用场景进行自动测试。
有两种机制可用于记录数据: --record
和 --usbmon-record
。
--record
在每个安装中都可用,设置使用更简单,所以对于纯追踪问题,通常采用这种方法。然而,如果问题出在解析和理解来自追踪设备的低级数据包上,--usbmon-record
选项可能更有帮助。
普通录制
运行任何 libsurvive 工具时,传入 --record <filename>.rec.gz
。这将在该文件中创建系统运行时看到的所有内容的数据日志。这会记录大量数据,所以如果允许长时间运行,该文件可能会变得非常大。
要回放该文件,运行:
./survive-cli --playback <filename>.rec.gz
原始 USB 录制
有时,在处理新硬件或某些类型的 bug 导致 USB 层出现问题时,需要对看到/发送的 USB 数据进行原始捕获。USBMON 驱动程序允许你这样做。
目前这个驱动程序只在 Linux 上可用,你必须安装 libpcap -- sudo apt install libpcap-dev
。你还需要安装 usbmon
内核模块;但许多 Linux 发行版都内置了这个模块。
要启动 usbmon 并为所有用户准备使用,运行:
sudo modprobe usbmon
sudo setfacl -m u:$USER:r /dev/usbmon* # 在敏感环境中,你可以用 sudo 运行 survive-cli。
要捕获 USB 数据,运行:
./survive-cli --usbmon-record <filename>.pcap.gz --htcvive <additional options>`
你可以用以下命令回放:
./survive-cli --usbmon-playback <filename>.pcap.gz [--playback-factor x] <additional options>`
如果你要发送此文件进行分析,请注意你需要随附的 *.usbdevs
文件才能使其有用。如果你遵循 *.pcap.gz
约定,运行类似以下命令:
zip logs.zip *.pcap* config.json
并将 logs.zip
发布到问题或 Discord 上。
这个驱动程序特别只捕获白名单上的 VR 设备;但如果你不想将原始 USB 数据发布到互联网上,可以在 Discord 上询问该私信发送给谁。
常用命令行标志
Libsurvive 高度可配置,根据构建时给定的驱动程序和选项,包含许多命令行选项。
如果你使用命令行选项,建议你为 libsurvive 安装 bash 自动补全:
sudo cp survive_autocomplete.sh /etc/bash_completion.d/
。
最有用的调试命令行选项是 --v
-- 这设置报告级别。这个详细程度大致遵循以下准则:
--v 10
- 在应用程序开始或结束时显示的统计信息和信息。--v 100
- 在追踪过程中许多常见点显示的信息--v 150
- 几乎每个姿态输出都显示信息--v 250
- 系统中几乎所有光数据事件都显示信息--v 1000
- 所有内容。
在更高的详细程度(>100)下运行会使可视化工具变得迟缓。
--force-calibrate
: 这会重新运行校准但重用 OOTX;这使得运行速度更快。
--playback-factor
: 回放录制内容时,这将加快回放速度(0 表示尽可能快地运行所有内容)或减慢速度(2 表示花费两倍的时间)
--lighthouse-gen
: 强制系统使用特定一代灯塔。目前,有时系统会将灯塔 1(纯方形基站)误识别为灯塔 2(圆形正面基站)或相反。当我们发现这些情况时,我们正在修复它们,但这让一个行为异常的系统暂时变得可用。
驱动程序
这些是为 libsurvive 提供信息的不同驱动程序。它们都封装在 src
目录中,前缀为 driver_
。每个驱动程序都可以用 --<driver-name>
标志指定。你可以用 --no-<driver-name>
禁用默认驱动程序(例如,htcvive
)。
htcvive
- 这是通过USB连接向Vive硬件提供数据的主驱动程序。simulator
- 这模拟一个浮动设备,同时向libsurvive提供真实的光线/IMU数据。对测试不同功能很有用。playback
- 回放驱动程序实现了记录/回放功能。它将文件重放到各个数据点。usbmon
- USBmon可以与SteamVR同时运行,允许两个系统使用跟踪对象数据。openvr
- 这个驱动程序公开外部姿态和速度,可以与usbmon
一起运行以比较两个系统。
自定义驱动程序
集成驱动程序旨在相对简单直接,上述驱动程序是很好的参考示例。
一般方法是将其编译为插件文件夹中名为driver_<name>.so
的共享对象/DLL。libsurvive内部会枚举这些插件并运行libsurvive注册的函数,格式如下:
int DriverRegExample(SurviveContext *ctx) {
if(...error...) {
return SURVIVE_DRIVER_ERROR;
}
return SURVIVE_DRIVER_NORMAL;
}
REGISTER_LINKTIME(DriverRegExample)
驱动程序不必注册其他任何内容;但要集成到libsurvive中,驱动程序必须公开轮询/关闭函数或线程/关闭函数。
最简单的方法是轮询驱动程序:
void survive_add_driver(SurviveContext *ctx, void *user_ptr, DeviceDriverCb poll, DeviceDriverCb close)
系统运行时会持续调用轮询函数;关闭函数在关机时调用。
更灵活的是线程驱动程序:
bool *survive_add_threaded_driver(SurviveContext *ctx, void *driver_data, const char *name, void *(routine)(void *), DeviceDriverCb close);
这会以给定的函数和名称启动一个线程。
在线程驱动程序的情况下,访问SurviveContext
或SurviveObject
的任何成员时,必须使用以下函数进行加锁和解锁:
void survive_get_ctx_lock(SurviveContext *ctx);
void survive_release_ctx_lock(SurviveContext *ctx);
不正确的加锁或解锁可能导致竞态条件或死锁。
在线程函数或轮询函数中,驱动程序负责使用它们公开的数据调用适当的钩子函数。通常,驱动程序还会调用survive_create_device
,并只关注该设备。可以同时运行多个驱动程序,但它们都假定使用相同的灯塔配置。
一个很好的实例是driver_simulator.c
,它使用自定义的SurviveObject
类型调用各种光线数据和IMU回调。driver_openvr.cc
演示了如何将外部位置数据整合到库中。
常见问题
使用libsurvive的其他项目
- Monado OpenXR运行时使用libsurvive作为其HMD和控制器驱动程序之一。
- 在Monado上运行的OpenXR应用程序包括Godot 3.x的OpenXR插件
- 有一个非官方的(且无法上游的)OpenHMD/libsurvive分支,它添加了libsurvive驱动程序。
- 这个OpenHMD/libsurvive分支可以插入SteamVR-OpenHMD或与Godot 3.x的OpenHMD插件原生使用。
补充说明
感谢Faul先生设计我们的标志! 特别感谢@nairol在他的https://github.com/nairol/LighthouseRedox 项目中对现有HTC Vive系统进行了极其详细的逆向工程。