
一文讀懂 PhysX SDK源碼解析追一顆彈球穿越物理引擎的全過程【免費下載鏈接】PhysXNVIDIA PhysX SDK項目地址: https://gitcode.com/gh_mirrors/ph/PhysX想象一個畫面場景里掉下一顆半徑為 0.5 米的球砸在墻角的另一個球上兩個球輕輕彈開。就這么一個動作背后藏著 NVIDIA 幾十年物理引擎技術的沉淀。而你只要調用一句gScene-simulate(dt)剩下的交給引擎。可你有沒有想過——這一行代碼之后到底發(fā)生了什么這就是我們今天的任務PhysX SDK源碼解析。我們不按架構總覽→模塊分析的老套路走而是換一種更有意思的方式——把一顆彈球當成主角跟著它從被創(chuàng)建、被模擬、被檢測碰撞到最終落地的完整執(zhí)行流一步步打開引擎內部的黑盒。我們會讀到physx/source/下的真實代碼沿著真實的調用鏈看看 NVIDIA 是怎么把物理做成毫秒級的。本項目源碼可自行克隆git clone https://gitcode.com/gh_mirrors/ph/PhysXPhysX SDK源碼解析PxActor 類體系依賴圖一顆彈球從誕生起就處在這張關系網(wǎng)中一、出發(fā)前先認識我們的主角一顆彈球在內存里長什么樣在追代碼之前我們先回答一個基礎問題引擎眼里的球到底是什么在 PhysX 里一顆球由兩層組成幾何形狀Geometry描述它長什么樣——PxSphereGeometry只需要一個半徑字段。剛體Rigid Body描述它怎么動——位置、速度、角速度、質量對應PxRigidDynamic。這兩者被PxShape組合在一起再掛到剛體上。我們讀源碼時會發(fā)現(xiàn)一個很值得品味的細節(jié)PhysX 把形狀和剛體拆得極開幾何層甚至不關心物理這個概念它只做純數(shù)學的查詢和求交。這樣的分層讓同一個幾何算法既能服務碰撞檢測又能服務場景查詢raycast/sweep/overlap。我們創(chuàng)建彈球的標準姿勢對應physx/samples/snippets/snippethelloworld/SnippetHelloWorld.cpp的套路// 1. 創(chuàng)建幾何半徑 0.5 的球 PxShape* shape gPhysics-createShape(PxSphereGeometry(0.5f), *gMaterial); // 2. 創(chuàng)建剛體全局變換位置 朝向 PxRigidDynamic* ball gPhysics-createRigidDynamic(PxTransform(PxVec3(0, 10, 0))); // 3. 把形狀掛到剛體上 ball-attachShape(*shape); // 4. 塞進場景引擎開始接管它 gScene-addActor(*ball);看到這里你可能會問addActor之后引擎內部到底把它放到了哪些數(shù)據(jù)結構里這正是我們接下來要追的線索。二、第一站simulate() 的異步真相和你以為的不一樣很多人以為simulate()是同步執(zhí)行完整個物理步進的。大錯特錯。我們打開physx/source/physx/src/NpScene.cpp找到真正的入口void NpScene::simulate(PxReal elapsedTime, physx::PxBaseTask* completionTask, void* scratchBlock, PxU32 scratchBlockSize, bool controlSimulation) { simulateOrCollide(elapsedTime, completionTask, scratchBlock, scratchBlockSize, controlSimulation, PxScene::simulate: Simulation is still processing last simulate call, you should call fetchResults()!, Sc::SimulationStage::eADVANCE); }注意看simulate()只是simulateOrCollide()的一個包裝而且它干的第一件事竟然是檢查上一次模擬有沒有結束。那句報錯信息Simulation is still processing last simulate call你遲早會遇到——如果你連續(xù)調兩次simulate()不夾fetchResults()就會收到它。我們繼續(xù)看simulateOrCollide的后半段NpScene.cpp第 1908 行附近這里藏著異步的關鍵if (controlSimulation) { mTaskManager-resetDependencies(); // 重置任務依賴圖 mTaskManager-startSimulation(); // 喚醒任務系統(tǒng) } // ... 組裝任務鏈 mSceneCompletion.setContinuation(*mTaskManager, completionTask); // 終點你傳的完成回調 mSceneExecution.setContinuation(*mTaskManager, mSceneCompletion); // 主線任務 → 完成節(jié)點 mSceneCompletion.removeReference(); mSceneExecution.removeReference();這就是 PhysX 的任務驅動模型Task Graphsimulate()只是把任務節(jié)點掛到任務圖上、removeReference()觸發(fā)任務開始調度然后立即返回。真正的物理計算在后臺線程池里跑你稍后調用fetchResults()時它要么已完成要么還在跑checkResultsInternal里有個wait選項。為什么這么設計因為游戲渲染循環(huán)里物理計算完全可以和下一幀的渲染/邏輯并行。這是 PhysX 多線程優(yōu)化的地基也是我們整趟旅程的第一塊拼圖。那么任務圖里主線任務到底跑的是什么往下追進入第二站。三、第二站寬階段——如何在 1 萬個球里只挑出 30 對可能碰撞假設場景里有 10000 個物體。如果每幀都兩兩做精確碰撞檢測那就是約 5000 萬次求交——任何 CPU 都扛不住。所以物理引擎必須分兩步走也就是業(yè)界常說的階段目標復雜度對應 PhysX 源碼寬階段 Broad Phase快速篩出包圍盒可能相交的物體對O(n log n) 左右physx/source/lowlevelaabb/窄階段 Narrow Phase對候選對做精確幾何求交每對 O(1) 幾何運算physx/source/geomutils/寬階段的經(jīng)典算法是SAPSweep and Prune掃描與剪枝。思路樸素到令人發(fā)指每個物體有一個 AABB軸對齊包圍盒把每個盒子在 X/Y/Z 三個軸上的最小值和最大值分別當成端點三個軸各維護一個有序端點數(shù)組。任何一對物體只有當它們在三個軸上的區(qū)間都重疊時才可能碰撞。PhysX 的 SAP 實現(xiàn)在physx/source/lowlevelaabb/src/BpBroadPhaseSap.cpp。先看它的構造函數(shù)里一段押金式的預分配mBoxesCapacity (((maxNbStaticShapes maxNbDynamicShapes) 31) ~31); // 三個軸每個軸一組端點 mBoxEndPts[0] (SapBox1D*)PX_ALLOC(sizeof(SapBox1D) * mBoxesCapacity, SapBox1D); mBoxEndPts[1] (SapBox1D*)PX_ALLOC(sizeof(SapBox1D) * mBoxesCapacity, SapBox1D); mBoxEndPts[2] (SapBox1D*)PX_ALLOC(sizeof(SapBox1D) * mBoxesCapacity, SapBox1D); // 每個軸有兩個哨兵端點正負無窮省掉數(shù)組邊界的判斷 setMinSentinel(mEndPointValues[0][0], mEndPointDatas[0][0]); setMaxSentinel(mEndPointValues[0][1], mEndPointDatas[0][1]);這段代碼里有幾個值得停下來品味的點容量按 32 對齊31 ~31為了 SIMD 和緩存行對齊物理引擎的內存布局處處透著這種強迫癥。一上來就把內存全分了SAP 的高性能秘訣之一就是避免運行期動態(tài)分配所有數(shù)組一次到位。哨兵節(jié)點Sentinel數(shù)組首尾各放一個值為 ±∞ 的假端點這樣排序和掃描時永遠不需要判斷是不是到邊界了。這種用數(shù)據(jù)代替分支的技巧在源碼里隨處可見。但光有有序數(shù)組還不夠——每幀都有物體在動端點數(shù)組必須更新。PhysX 的高明之處在于它不做全量重排序而是增量更新。BroadPhaseSap::update()同文件第 456 行的骨架void BroadPhaseSap::update(const PxU32 numCpuTasks, PxcScratchAllocator* scratchAllocator, const BroadPhaseUpdateData updateData, PxBaseTask* continuation, PxBaseTask* narrowPhaseUnblockTask) { if (setUpdateData(updateData)) // 只拿新增/更新/刪除的物體不全量拷貝 { resizeBuffers(); mSapPostUpdateWorkTask.set(numCpuTasks); // 拆成多個并行任務 mSapUpdateWorkTask.set(numCpuTasks); mSapPostUpdateWorkTask.setContinuation(continuation); // 完成后交給窄階段 mSapUpdateWorkTask.setContinuation(mSapPostUpdateWorkTask); mSapPostUpdateWorkTask.removeReference(); mSapUpdateWorkTask.removeReference(); } }注意updateData里裝的是created / updated / removed三類句柄——只有真正動了、新增了、刪除了的物體才需要處理。絕大多數(shù)物體上一幀到這一幀幾乎沒動連 AABB 都不用重算。再配合任務系統(tǒng)把三軸的掃描拆成并行子任務SAP 的每一幀代價被壓到極低。寬階段的輸出是一堆**候選物體對pair**——注意是候選它們只是 AABB 相交不代表真的撞上了。真正拍板的是下一站。四、第三站窄階段——兩顆球到底撞沒撞用最笨也最快的方法寬階段交出了幾十上百個候選對窄階段要逐一驗證。驗證的代碼就在physx/source/geomutils/src/contact/目錄下。我們打開GuContactSphereSphere.cpp看兩顆球相撞的完整判定——這段代碼短得令人驚訝bool contactSphereSphere(GU_CONTACT_METHOD_ARGS) { const PxSphereGeometry sphereGeom0 shape0.getconst PxSphereGeometry(); const PxSphereGeometry sphereGeom1 shape1.getconst PxSphereGeometry(); PxVec3 delta transform0.p - transform1.p; // 兩球心連線向量 const PxReal distanceSq delta.magnitudeSquared(); // 球心距離的平方 const PxReal radiusSum sphereGeom0.radius sphereGeom1.radius; const PxReal inflatedSum radiusSum params.mContactDistance; // 加上接觸距離余量 if (distanceSq inflatedSum * inflatedSum) return false; // 夠不著直接放棄 // 手動歸一化順便檢查奇異情況 const PxReal magn PxSqrt(distanceSq); if (magn 0.00001f) delta PxVec3(1.0f, 0.0f, 0.0f); // 兩球心完全重合法線沒法算隨便挑一個方向 else delta * 1.0f / magn; // ... 繼續(xù)生成接觸點并寫入 contactBuffer }逐段拆解這段PhysX 碰撞檢測源碼用距離平方比較避免開根號。magnitudeSquared()省掉一次sqrt而sqrt是 CPU 上昂貴的指令之一。這種能不開方就不開方的思維貫穿整個引擎。params.mContactDistance是什么這是物理引擎里一個非常巧妙的設計兩個物體還沒真正接觸、但距離小于這個值時就提前生成接觸點。它讓求解器有機會在視覺接觸之前就開始施加輕微的反作用力模擬出物體表面的柔韌性避免抖動。你完全可以在PxSceneDesc里改它來調手感。那行隨便挑個方向的注釋——兩球心完美重合是個數(shù)學奇點球心距離為 0法線方向無定義。這里直接選 X 軸正方向兜底。看起來笨但這是工程上最穩(wěn)的處理。讀者可以記住這個位置我們動手環(huán)節(jié)會回來改它。球-球是最簡單的情形。如果候選對是凸多邊形 vs 凸多邊形呢PhysX 走的是PCMPersistent Contact Manifold持久接觸流形實現(xiàn)位于physx/source/geomutils/src/pcm/目錄如GuPCMContactConvexConvex.cpp、GuPersistentContactManifold.cpp。它的核心思想是上一幀算出的接觸點不要丟掉這一幀拿它們做種子去精化而不是從零開始重算。配合前面提到的接觸距離余量PCM 能在低采樣率下依然穩(wěn)定——這就是為什么 PhysX 用默認 60Hz 步長也能堆出穩(wěn)定的積木塔。PhysX SDK源碼解析幾何類型依賴圖窄階段的所有碰撞方法都圍繞這張類型表展開PhysX 碰撞檢測源碼中的接觸點數(shù)據(jù)結構法線、位置、分離距離每個字段都服務于后面的約束求解窄階段產(chǎn)出的接觸點會進入GuContactBuffer然后被送往求解器physx/source/lowleveldynamics/求解器算出每個接觸點的沖量最后積分器更新剛體的速度和位置——這趟旅程才算走完。五、動手實踐改一行源碼看完全重疊的球會怎樣光讀不練假把式。現(xiàn)在我們回到前面埋的伏筆——GuContactSphereSphere.cpp里的那個奇異分支來做一個真實的源碼實驗。實驗目標讓兩顆球完美重合觀察引擎的行為并理解兜底方向在物理上的意義。步驟 1在contactSphereSphere()的奇異分支里加一行調試輸出if (magn 0.00001f) { delta PxVec3(1.0f, 0.0f, 0.0f); // 兩球心完全重合 法線取隨機方向 // 新增打印警告方便觀察 printf([PhysX] Sphere-sphere singularity: two centers coincide!\n); }步驟 2在任意示例場景里比如SnippetHelloWorld的createStack之后把兩顆球的初始位置都設成PxVec3(0, 0, 0)讓它們從出生就重疊。步驟 3編譯運行觀察控制臺輸出。你會看到引擎不會崩潰也不會產(chǎn)生 NaN 或無窮大打印會密集出現(xiàn)說明求解器一直在用 X 軸方向強行推開兩個重合的球一旦兩球被推開打印頻率驟降恢復正常接觸。這個實驗讓你直觀感受到健壯性robustness是物理引擎的生命線真實游戲里物體被玩家腳本瞬間塞進同一位置、或者快速穿透是家常便飯。引擎必須在這種極端輸入下優(yōu)雅地給出一個合法結果而不是輸出 NaN 把整個場景搞崩。我們隨手一改就看到了引擎為不可能的情況預留的逃生通道。想再進一步把delta PxVec3(1.0f, 0.0f, 0.0f)改成PxVec3(0.0f, 1.0f, 0.0f)觀察兩球被推開的方向變化——這行隨便的代碼實際上決定了引擎對無解問題的傾向性。六、踩坑清單讀 PhysX 源碼和用它的時候這些坑我們替你踩過了這趟旅程里我們遇到的每一個坑都對應著一條源碼設計原則simulate()后不調fetchResults()就再調simulate()——你會收到 Simulation is still processing last simulate call 報錯。根源在NpScene.cpp的getSimulationStage()狀態(tài)機檢查。記住simulate 是異步的用 fetchResults 做同步。把contactOffset和restOffset搞混——前者決定多早開始生成接觸我們前文講過后者決定靜止后停在距離表面多遠的地方。調錯會得到物體懸空或瘋狂抖動的詭異現(xiàn)象。在寬階段代碼里找精確碰撞——找不到的。lowlevelaabb/里只有 AABB 和句柄沒有幾何數(shù)據(jù)。寬窄分離是 PhysX 源碼的第一性架構先有這個意識再讀代碼就順了。忽視內存預分配——SAP 構造函數(shù)里那些PX_ALLOC不是隨便寫的。物理引擎的性能大頭是緩存命中率運行期頻繁new/delete會毀掉一切。所以PxSceneDesc里有l(wèi)imits.maxNbBroadPhaseOverlaps這類先聲明規(guī)模的參數(shù)能填就填。搜索源碼時別用錯了關鍵詞——Np*前綴是物理 API 層如 NpScene、NpPhysicsSc*是模擬核心層ScSceneGu*是幾何工具層。知道這套命名search_in_files一搜一個準。七、閱讀路線源碼該從哪看起我們的順序建議如果這篇文章勾起了你通讀源碼的欲望我們推薦**按從外到內、沿數(shù)據(jù)流**的順序讀和本文的追球路徑一致API 層physx/include/PxPhysicsAPI.h總入口→PxScene.h場景接口→PxRigidDynamic.h。先建立接口長什么樣的地圖。實現(xiàn)入口physx/source/physx/src/NpScene.cpp重點看simulate()和fetchResults()理解任務狀態(tài)機。寬階段physx/source/lowlevelaabb/src/從BpBroadPhaseSap.cpp的構造函數(shù)讀起再讀BpBroadPhase.cpp看它怎么調度 SAP 和 MBP。窄階段physx/source/geomutils/src/contact/按簡單到復雜讀球-球 → 球-盒 → 凸-凸PCM。求解器physx/source/lowleveldynamics/src/這里你會看到接觸點如何變成沖量。動手改代碼 打印日志比讀十遍都管用。建議配一個 PVDPhysX Visual Debugger隨時可視化驗證。如果你想先看成品效果再讀源碼physx/samples/samplevehicle/和kaplademo/這個倉庫自帶的 Kapla 積木演示對約束求解器的穩(wěn)定度是個極好的壓力測試都是不錯的誘餌。八、終點即起點這顆彈球的下一程我們跟著一顆球走完了創(chuàng)建 →simulate()入任務圖 → SAP 寬階段篩候選對 → 窄階段生成接觸點 → 求解器算沖量?;仡^看PhysX 源碼設計的核心密碼就三個詞分層、增量、健壯。分層幾何、寬階段、窄階段、求解器各司其職任何一個都可以被單獨替換比如把 CPU 寬階段換成 GPU 版本。增量只更新變化的數(shù)據(jù)created/updated/removed句柄只重排序需要動的端點把每幀成本壓到常數(shù)級。健壯哨兵節(jié)點、奇異分支兜底、接觸距離余量每一個看似隨意的處理背后都是為了讓引擎在任何刁鉆輸入下都不崩。而這顆彈球的下一程也恰好是 PhysX 的方向從 4.1 到 5.0NVIDIA 把重心轉向 GPU 加速Flex、Blast 的融合與實時破壞、布料等軟體模擬。但無論上層怎么演進你今天讀懂的這套物理流水線骨架仍然是理解它們的地基。下一次你調用gScene-simulate(dt)時希望你能想起這篇PhysX SDK源碼解析里那趟穿越代碼的旅程——知道那句咒語背后是有真東西在運轉的。本文關鍵詞核心關鍵詞PhysX SDK源碼解析長尾關鍵詞PhysX碰撞檢測源碼、PhysX simulate源碼解析、PhysX寬階段SAP算法、PhysX窄階段接觸生成、NVIDIA物理引擎源碼、PhysX剛體動力學原理、PhysX 4.1源碼閱讀路線、PhysX任務系統(tǒng)源碼【免費下載鏈接】PhysXNVIDIA PhysX SDK項目地址: https://gitcode.com/gh_mirrors/ph/PhysX創(chuàng)作聲明:本文部分內容由AI輔助生成(AIGC),僅供參考