:從原理到環(huán)境配置與API調(diào)用)
簡介面向HTC Vive開發(fā)者的OpenVR簡化封裝與示例代碼包基于社區(qū)項目triad_openvr-master適合想要快速上手Vive頭顯、控制器及Tracker開發(fā)的Python、C#工程師使用。壓縮包共9個文件約71KB包括4個Python腳本、1個C#腳本、1個vrsettings配置文件、1個Markdown說明文檔及少量輔助文件其中Python腳本負責設(shè)備追蹤數(shù)據(jù)讀取與UDP發(fā)送C#腳本用于跨語言接收處理配置和文檔則提供運行參數(shù)與用法指引。目前已有1945人學習下載。通過其中的tracker測試、控制器測試和UDP通信示例開發(fā)者可以理解OpenVR的設(shè)備狀態(tài)獲取、空間定位追蹤以及數(shù)據(jù)網(wǎng)絡(luò)傳輸流程配合示例配置和文檔還能快速搭建自己的Vive Tracker追蹤原型或?qū)⑵浼傻浆F(xiàn)有虛擬現(xiàn)實交互系統(tǒng)中。這些示例覆蓋了控制器按鈕、Tracker位置更新和無線數(shù)據(jù)傳輸?shù)瘸R婇_發(fā)需求便于按需修改復(fù)用有效降低入門門檻適合作為OpenVR開發(fā)初期的實用參考。無論是學習OpenVR底層原理還是實際構(gòu)建VR交互應(yīng)用這份代碼都能提供直觀的范例。 我第一次把HTC Vive的包裝盒打開是在2016年春天。頭顯、兩個基站、兩把手柄、一捆線纜攤滿桌面SteamVR很快認出了它們可我心里一直有個問題揮之不去如果不借助Unity的SteamVR Camera Rig我該怎么用代碼自己驅(qū)動這套設(shè)備答案就是OpenVR。OpenVR是Valve提供的VR運行時接口它把你和HTC Vive之間所有硬件細節(jié)包起來讓程序通過一組統(tǒng)一API讀取追蹤數(shù)據(jù)、渲染畫面、發(fā)送手柄輸入。這篇文章就圍繞openvr for htc vive這條主線從原理講到實際工程配置給想從底層入手做PC VR開發(fā)的朋友一份可以直接參考的實戰(zhàn)筆記。無論你是剛拿上頭顯的初學者還是在Unity/Unreal里被插件黑盒困擾的開發(fā)者都值得花幾分鐘讀下去。1. OpenVR在HTC Vive體系里的真實位置1.1 三層結(jié)構(gòu)硬件、運行時、API很多初學者把OpenVR和SteamVR混為一談這是第一個要糾正的概念。HTC Vive是硬件SteamVR是Valve提供的運行時平臺而OpenVR是面向開發(fā)者的API庫。三個層級各司其職頭顯、基站、手柄負責采集圖像與玩家運動數(shù)據(jù)SteamVR運行時負責設(shè)備驅(qū)動、Lighthouse定位計算、房間設(shè)置、驅(qū)動管理OpenVR API讓你的程序能讀取這些數(shù)據(jù)并向Compositor提交渲染畫面。數(shù)據(jù)流的方向是Vive頭顯和基站通過串流盒把傳感器數(shù)據(jù)送給SteamVR驅(qū)動驅(qū)動完成定位解算后你的程序調(diào)用OpenVR的WaitGetPoses拿到這一幀的頭顯位姿和手柄位姿然后按這個位姿渲染左右眼畫面最后調(diào)用Submit把畫面交還給SteamVR Compositor由它統(tǒng)一輸出到頭顯屏幕并做鏡頭畸變校正。整個過程每一幀都在循環(huán)。這套分層設(shè)計最直接的好處是你的程序根本不關(guān)心HTC Vive具體如何掃描激光、如何算坐標只要在初始化時告訴OpenVR我要運行場景程序剩下的工作全是標準API調(diào)用。1.2 為什么不用HTC自己的SDKHTC和Valve合作推出Vive時設(shè)備端驅(qū)動和定位算法主要由Valve負責HTC并沒有對外發(fā)布一套獨立的Vive SDK。也就是說在PC端做Vive原生開發(fā)OpenVR/SteamVR就是事實上的官方路徑。這和當年Oculus Rift的開發(fā)方式形成了鮮明對比Oculus要求開發(fā)者必須使用Oculus SDK登錄Oculus硬件代碼幾乎無法直接平移到其他頭顯。而OpenVR恰恰相反它定義了一套廠商無關(guān)的接口HTC Vive可以用Valve Index可以用Windows MR設(shè)備也能用甚至一些開發(fā)者的DIY頭顯只要實現(xiàn)了OpenVR驅(qū)動同一個應(yīng)用照樣可以運行。這套抽象層極大地降低了多平臺VR開發(fā)的成本。我后來把一套Demo從Vive換到Index跑沒有改一行業(yè)務(wù)邏輯只是重新配置了SteamVR綁定體驗完全正常。1.3 OpenXR時代還需要OpenVR嗎這是最近幾年被反復(fù)問到的問題。OpenXR作為Khronos Group主導(dǎo)的開放標準兼容了多家廠商的硬件Valve也深度參與了標準制定。理論上新項目優(yōu)先考慮OpenXR是更穩(wěn)妥的選擇但實際情況是大量生產(chǎn)環(huán)境中的代碼、論文配套源碼、SteamVR平臺的Overlay工具、以及很多老牌Unity插件的底層仍然使用OpenVR接口。OpenVR至今沒有被移除反而因為SteamVR生態(tài)的慣性繼續(xù)維護著。如果你只打算給SteamVR生態(tài)開發(fā)直接學OpenVR完全夠用如果考慮未來跨平臺部署可以先通過OpenVR理解VR渲染的核心概念再遷移OpenXR就很順滑了兩者在追蹤、提交、輸入模型上高度相似。2. 開發(fā)環(huán)境搭建從硬件擺位到SDK接通2.1 硬件安裝中影響開發(fā)體驗的幾個細節(jié)搭建開發(fā)環(huán)境不只是在電腦上裝SDK首先是物理環(huán)境。HTC Vive的基站建議安裝在房間對角線上方離地至少2米向下傾斜30到45度兩只基站最好能互相看到對方。光看官方圖容易忽略一點基站一旦安裝好開發(fā)過程中就不要頻繁移動因為SteamVR會以當前房間設(shè)置為基準生成Chaperone邊界你每次挪基站都可能需要重新做房間設(shè)置。串流盒的連接順序也有講究HDMI線插顯卡USB線插主板電源線單獨供電頭顯端的線纜要扣緊。開發(fā)時如果你用的是筆記本記得把SteamVR的性能面板打開優(yōu)先使用獨立顯卡。還有一條安全事項必須提不要讓Vive透鏡長期暴露在陽光下透鏡聚光會燒壞屏幕損壞不可逆。2.2 SDK引入與最小初始化工程從ValveSoftware/openvr倉庫拿到頭文件和庫文件常用的有三種引入方式直接引用預(yù)編譯的openvr_api.dll使用源碼里的openvr_capi.h/openvr.h或者用官方提供的CMake工程做子目錄引用。我最常用的是預(yù)編譯庫方式在Visual Studio項目里配好include和lib路徑運行時把openvr_api.dll拷貝到exe目錄。第一步先驗證設(shè)備是否被識別代碼非常簡單#include openvr.h #include iostream int main() { if (!vr::VR_IsHmdPresent()) { std::cerr 未檢測到VR頭顯 std::endl; return -1; } vr::EVRInitError error vr::VRInitError_None; vr::IVRSystem* system vr::VR_Init(error, vr::VRApplication_Scene); if (error ! vr::VRInitError_None) { std::cerr 初始化失敗: vr::VR_GetVRInitErrorAsEnglishDescription(error) std::endl; return -1; } std::cout OpenVR initialized std::endl; return 0; }注意VRApplication_Scene和VRApplication_Overlay的區(qū)別。普通場景應(yīng)用游戲、仿真必須提交畫面紋理用Scene只打算疊加UI工具欄的程序用Overlay不會占用場景提交通道。選錯類型輕則機能浪費重則Compositor不顯示你的畫面。2.3 HelloVR先驗證追蹤鏈路初始化成功后我建議先不看渲染先讀取HMD位姿并打印到控制臺驗證追蹤鏈路是否真正工作。這一步能解決的常見問題包括SteamVR沒有運行、頭顯處于待機狀態(tài)、基站沒有喚醒等等。vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]; vr::VRCompositor()-WaitGetPoses(poses, vr::k_unMaxTrackedDeviceCount, nullptr, 0); for (uint32_t i 0; i vr::k_unMaxTrackedDeviceCount; i) { if (poses[i].bDeviceIsConnected poses[i].bPoseIsValid) { std::cout 設(shè)備 i 位置: poses[i].mDeviceToAbsoluteTracking.m[2][3] poses[i].mDeviceToAbsoluteTracking.m[1][3] poses[i].mDeviceToAbsoluteTracking.m[0][3] std::endl; } }當你在房間走動時控制臺輸出的位置數(shù)據(jù)發(fā)生變化就說明OpenVR到頭顯的追蹤數(shù)據(jù)流已經(jīng)打通了。有了這條驗證路徑后面所有工作都可以在推數(shù)據(jù)和拉數(shù)據(jù)兩個方向上分別調(diào)試。3. 核心API拆解親手完成一幀VR畫面的提交3.1 初始化與設(shè)備枚舉上一節(jié)只做到了初始化實際項目中還需要區(qū)分頭顯、控制器、基站和追蹤器。HTC Vive手邊常見的狀態(tài)是頭顯始終在線控制器可能休眠基站不作為追蹤目標上報。用vr::VRSystem()-GetTrackedDeviceClass(index)遍歷所有設(shè)備索引即可判斷設(shè)備角色。vr::TrackedDeviceClass deviceClass vr::VRSystem()-GetTrackedDeviceClass(i); if (deviceClass vr::TrackedDeviceClass_HMD) { // 處理頭顯 } else if (deviceClass vr::TrackedDeviceClass_Controller) { // 處理手柄 }這里有個容易被新手踩的坑設(shè)備索引i并不是固定的硬件斷開重連后索引可能變化。正確做法是在每一幀都重新枚舉依據(jù)設(shè)備角色緩存對應(yīng)的索引而不是假設(shè)手柄永遠是k_unTrackedDeviceIndex_Hmd 1。3.2 左右眼相機矩陣的獲取VR渲染和普通3D渲染最大的區(qū)別在于每一幀都要生成兩個略有偏移的相機。OpenVR提供兩組矩陣頭顯到眼睛的變換GetEyeToHeadTransform(vr::Eye_Left)返回一個HmdMatrix34_t表示左眼相對于頭顯中心的偏移一般沿X軸負方向偏移約0.032米眼睛的投影矩陣GetProjectionMatrix(vr::Eye_Left, nearZ, farZ)返回一個4x4投影矩陣視錐形狀考慮了Vive透鏡的畸變參數(shù)。拿到這兩組矩陣后把HMD位姿矩陣和眼睛偏移矩陣組合得到該幀左眼的視圖矩陣再乘以投影矩陣就是最終送入GPU的VP矩陣。近裁剪面建議設(shè)置在0.1到0.3米之間太大會導(dǎo)致近距離物體被裁掉太小又會浪費深度精度。還有一個細節(jié)非常容易搞錯OpenVR矩陣本身是列主序還是行主序。不同版本的文檔、不同圖形API下表現(xiàn)不同我在D3D11和OpenGL里都遇到過需要轉(zhuǎn)置的情況。如果你的模型出現(xiàn)在完全錯誤的位置或者鏡像顛倒第一反應(yīng)應(yīng)該是檢查矩陣是否轉(zhuǎn)置、坐標系是否從右手系轉(zhuǎn)成了左手系。3.3 控制器追蹤和按鈕狀態(tài)控制器追蹤同樣來自于TrackedDevicePose_t數(shù)組只是設(shè)備的賬號不同。拿到控制器位姿后你可以把它當作一個6自由度手柄模型來渲染也可以把它當作虛擬手的位置。按鈕狀態(tài)舊的讀取方式是VRSystem()-GetControllerState(deviceIndex, state)state.ulButtonPressed位掩碼里有扳機、觸控板、菜單、系統(tǒng)按鍵等映射。Vive手柄的觸控板還提供二維向量state.rAxis[0].x和state.rAxis[0].y用來做觸控板滑動操作。如果你是從Unity時代的SteamVR插件轉(zhuǎn)過來的可能會習慣直接用按鈕位掩碼這在原型驗證時很方便。但從項目可持續(xù)性角度看我建議盡快遷移到vr::IVRInput的Action/ActionSet體系應(yīng)用定義抓取扳機移動這類邏輯動作用戶自定義每種動作映射到具體哪個按鍵。這樣換硬件或者讓玩家用自己的按鍵習慣時不需要改程序邏輯SteamVR綁定界面直接處理映射。3.4 使用IVRCompositor提交畫面HTC Vive的屏幕是雙眼一體的你在程序里不能直接往系統(tǒng)窗口寫畫面必須把左右眼紋理交個Compositor它會負責鏡片畸變校正、異步時間扭曲、以及向頭顯遞交的輸出。提交函數(shù)核心參數(shù)是一個紋理描述結(jié)構(gòu)體。D3D11下典型的提交代碼vr::VRCompositor()-Submit(vr::Eye_Left, leftTexture, nullptr);其中l(wèi)eftTexture是vr::Texture_t類型它的handle字段指向一個D3D11 ShaderResourceVieweType必須是vr::TextureType_DirectX。OpenGL下需要改成對應(yīng)的紋理對象ID和類型。如果你用的是Vulkan還必須額外設(shè)置隊列族索引和圖像布局麻煩一些但原理相同。紋理不能每幀隨便重新創(chuàng)建應(yīng)該創(chuàng)建好雙緩沖或循環(huán)緩沖區(qū)反復(fù)提交。否則幀率會因驅(qū)動頻繁分配GPU資源而掉到不可接受的水平。另外提交時注意左右眼順序反了會導(dǎo)致雙眼圖像錯位玩家立刻會產(chǎn)生類似暈車的定向障礙。3.5 幀率管理Vive屏幕刷新率是90Hz這意味著每幀時間必須控制在11.1毫秒以內(nèi)。達不到這個目標時Compositor不會簡單等你而是啟用異步重投影把上一幀畫面根據(jù)最新頭顯姿態(tài)做糾正后輸出你的應(yīng)用實際就跑在更低的刷新率下。降低渲染分辨率可以換取穩(wěn)定90幀但畫面會變得模糊。我在開發(fā)中通常先關(guān)掉垂直同步保持WaitGetPoses的節(jié)奏并且用SteamVR的性能圖監(jiān)控重投影比例。目標是把重投影比例壓到10%以下否則畫面邊緣會有明顯殘影。如果GPU開銷太大優(yōu)先檢查是不是每幀重新創(chuàng)建了紋理或頻繁調(diào)用不必要的API。OpenVR本身不替你做任何渲染優(yōu)化它只是負責把最終結(jié)果送到屏上。4. Chaperone與Overlay容易被忽略的兩塊基礎(chǔ)設(shè)施4.1 安全邊界SteamVR在房間設(shè)置時會畫出一個矩形安全區(qū)域這就是Chaperone。開發(fā)時把這個區(qū)域交給OpenVR的IVRChaperone接口讀取然后在你自己的渲染場景里畫出地面邊界可以避免玩家轉(zhuǎn)頭或后退時直接撞墻。這個功能在開發(fā)者調(diào)試時尤其重要因為調(diào)試者往往注意力全在代碼上身體移動全靠邊界提醒。在HTC Vive上房間邊界可以設(shè)置成站姿模式只有地面小圓盤也可以設(shè)置成房間尺度模式畫出完整矩形。OpenVR通過GetPlayAreaSize返回房間寬和深通過GetPlayAreaRect返回矩形中心及旋轉(zhuǎn)。如果你的應(yīng)用強制要求玩家站立游玩至少要處理邊界不存在的情況因為用戶可能沒做房間設(shè)置。真正的房間尺度應(yīng)用還應(yīng)該檢測玩家是否走出邊界在靠近邊緣時給出可見警告。4.2 手柄渲染模型Vive手柄在動畫里是一個復(fù)雜的網(wǎng)格自己建模不僅費時間而且和真實手柄形制有偏差玩家看到會不信任。OpenVR提供了IVRRenderModels接口可以直接加載SteamVR內(nèi)置的控制器模型。做法是先用GetComponentRenderModelName拿到手柄某個組件比如本體、觸控板、扳機的模型名稱再用LoadRenderModel加載網(wǎng)格數(shù)據(jù)然后自行繪制。調(diào)試時有個小技巧把手柄模型放到和真實控制器相同的位置之前先用一個簡單幾何體代替確認位姿矩陣轉(zhuǎn)換沒有錯再加載正式模型。我在自己項目里就遇到過矩陣坐標軸轉(zhuǎn)置錯誤導(dǎo)致手柄模型橫著漂在頭顯邊上排查了很久才意識到是左手系和右手系的轉(zhuǎn)換問題。4.3 用于UI的OverlayOpenVR的Overlay可以疊加在場景上方的另一個圖層它由Compositor混合顯示不占用你的場景提交管線。HTC Vive的加載畫面、SteamVR的Home界面、第三方工具的控制面板都用了Overlay機制。如果你做的工具需要在VR里顯示配置菜單但又不想把菜單渲染進場景顏色緩沖用VROverlayHandle_t創(chuàng)建一個Overlay再每幀提交一個紋理給Overlay就完成了。Overlay的好處是它不受場景GPU復(fù)雜度影響定位和大小只在創(chuàng)建時規(guī)定適合做常駐UI。代價是Overlay會額外消耗Compositor的混合性能數(shù)量不宜太多。推薦最多同時存在三到四個小Overlay再多會出現(xiàn)明顯的層級閃爍。5. 我在HTC Vive上實測OpenVR踩過的坑5.1 WaitGetPoses的位置WaitGetPoses是Compositor提供的一個同步點它的作用是讓CPU等待直到Compositor準備好本幀渲染姿勢數(shù)據(jù)同時向驅(qū)動請求最新頭顯姿態(tài)。很多人第一次寫循環(huán)時習慣在一進入幀循環(huán)就調(diào)用它然后立即進行渲染。這在幀率足夠時沒有問題但一旦渲染超時WaitGetPoses會在上一幀的Compositor處理完之前就一直阻塞整個管線退化成同步模式。我的做法是先完成本幀CPU端的邏輯計算在真正需要GPU渲染的場景提交前調(diào)用WaitGetPoses并用返回的姿勢數(shù)據(jù)更新相機矩陣。這樣能把CPU和GPU的流水線錯開一點減少等待時間。但也不要放在渲染之后否則姿態(tài)延遲會明顯增加轉(zhuǎn)頭時畫面會反應(yīng)遲鈍。5.2 坐標系與單位OpenVR使用右手坐標系Y軸向上單位是米。這聽著很簡單但實際使用時總有開發(fā)者栽在方向上Vive手柄在你向前伸手時Z軸是負的還是正的不同資料說法不一因為引擎內(nèi)部往往還會做一次坐標變換。我的建議是在初始化后立刻做一次基準測試。手柄水平放在桌上打印它的旋轉(zhuǎn)矩陣旋轉(zhuǎn)90度再打印對比結(jié)果。用這個方法來確認程序里預(yù)期的前、上、右方向與OpenVR實際輸出是否一致。這個成本很低但能避免你把所有場景模型的朝向?qū)懛础?.3 SteamVR未運行時的容錯并不所有用戶都會先手動打開SteamVR再運行你的程序。如果SteamVR進程未運行VR_Init可能返回VRInitError_Init_NoServerForBackgroundApp或者返回成功但VR_IsHmdPresent為假。程序必須在這種狀態(tài)下給出清晰提示而不是直接崩潰或黑屏。更穩(wěn)妥的做法是在啟動時調(diào)用VR_IsRuntimeInstalled檢查運行時是否存在VR_IsHmdPresent檢查設(shè)備狀態(tài)最后才VR_Init。如果初始化失敗用VR_GetVRInitErrorAsEnglishDescription把錯誤信息展示給用戶并建議他先啟動SteamVR。這些代碼加在一起不過十幾行但對用戶體驗的提升是決定性的。5.4 追蹤丟失的現(xiàn)象與調(diào)試開發(fā)中經(jīng)常會遇到手柄突然懸空不動或者頭顯影像瞬間平移一下這不是OpenVR的問題而是追蹤丟失。Vive的Lighthouse系統(tǒng)靠基站激光掃描遮擋、反射面過大、基站震動都會導(dǎo)致追蹤短暫丟失。追蹤丟失時TrackedDevicePose_t里的bPoseIsValid會變成false但bDeviceIsConnected仍然是true。調(diào)試時要區(qū)分兩者bDeviceIsConnected判斷設(shè)備是否在線bPoseIsValid判斷這一幀追蹤數(shù)據(jù)是否有效。即使追蹤暫時丟失設(shè)備也可能仍然連著基站。我在自己的測試場景里用一個簡單小球跟隨手柄位姿一旦位姿無效就把它變成紅色這樣走在房間里能快速定位哪片區(qū)域遮擋嚴重也方便及時調(diào)整設(shè)備擺放。從這些坑里爬出來的經(jīng)驗是OpenVR本身并不復(fù)雜真正耗費時間的是對坐標系、時序同步和運行狀態(tài)的細心管理。這也是我建議所有初次接觸OpenVR的開發(fā)者先讀一遍官方hello VR示例源碼、確認每一幀的調(diào)用順序之后再動手寫自己項目的原因。渲染管線一旦理順HTC Vive在你眼里就不再是黑盒而是可以逐幀掌控的硬件。本文還有配套的精品資源點擊獲取