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