33 · 電梯與多樓層:轎廂、門檻,以及車站在會動的地板上
多樓層把一個新東西帶進場景:一塊會動的地板。車站在上面、跟著它走、在它停之前不該動——而這三件事各自有一個不會報錯的失敗方式。
這一篇記八個位置:轎廂為什麼升不起來、承重接觸怎麼驗、跳動是指令還是接觸、陰性結果怎麼寫才算數、門檻上那個 200 mm 的洞、擋住轎廂的其實是它自己的門、車在移動中的轎廂裡繼續開,以及運動學該由誰算。
驗證狀態:內容來自一段多車 + 多樓層的 Isaac Sim 場域前期研究(2026-07 至 08),兩台電梯、五台車的實測。數字連同量測條件列出。
1. 轎廂升不起來,而每一層檢查都是綠的
症狀是「發了升降指令而轎廂高度一直是 0」,看起來像驅動端沒送出去。
而且沒有任何錯誤訊息:USD 檢查過、joint 建得出來、DriveAPI 也在,資產建置腳本印的 joints 清單裡那個關節也在——那是 USD 的視角,不是 PhysX 的。
真正的證據要問 articulation 自己:
articulation 的自由度:['door_joint', 'door_joint_mirror']
轎廂的關節不在裡面。
1.1 成因:根附著不能帶自由度
原本的接法是 cabin_joint 接 Shaft(靜態剛體)→ Cabin。
PhysX 把「靜態物體到 link」的關節當成 articulation 的根附著,而 reduced-coordinate articulation 的根只能是固定或浮動、不帶自由度——於是轎廂變成被釘住的根,自由度清單只剩兩片門。
修法是加一個沒有幾何、1 kg 的 Base,用 FixedJoint(body0 留空 = 世界)釘住,再讓 cabin_joint 接 Base → Cabin。這樣它是 articulation 的內部關節,也就是真的自由度。
井道那些牆維持靜態碰撞體——它們不該進 articulation。
1.2 physics:targetPosition 在模擬中改沒有用
USD 屬性是初始值,PhysX 建 articulation 時讀一次。執行期要走 isaacsim.core.prims.Articulation 的 set_joint_position_targets(..., joint_indices=...),指定單一自由度而不是整條餵。
這是 17 篇那五個「無聲失效條件」在關節上的具體形狀:寫得進去,不代表 runtime 會採用。
1.3 判定要把「沒被推動」與「跟不上」分開
只看「指令 vs 實際」的落差時,兩者長得一模一樣——沒動的落差正好等於全行程。
先問走到的最高點,再談誤差。
2. 承重接觸站得住:三個數字,三個量測坑
滿載 650 kg(轎廂 450 + 兩台車各 100)跑 0 → 8 m → 0:
| 量什麼 | 理論 | 實測 |
|---|---|---|
靜態下沉 mg/k |
4.01 mm | 4.22 mm(+5%) |
等速追隨誤差 c·v/k |
28.3 mm | 27.2 mm |
| 車相對轎廂的橫向滑動 | 0 | 0.0 mm |
三個數字分別驗了彈簧勁度、阻尼、橫向約束三個獨立的假設,而且理論與實測對得上。承重接觸站得住——這是「讓車真的站在轎廂上」相對於「每步 teleport 車的位置」多出來的東西,而它成立。
2.1 三個會讓數字說謊的量法
① 靜態下沉不能在最低層量。 轎廂停在最低層時正好壓在 lowerLimit = 0.0 上,載重由限位承受而不是由驅動彈簧,量到的下沉是 0 ——一個看起來完美、實際上什麼都沒驗到的數字。要在行程中間量。
② 等速追隨誤差不能拿去跟「靜態下沉 ≤ 5 mm」的需求比。 那是兩種東西:追隨誤差是 c·v/k(阻尼力要有位置誤差才產生,與載重無關),下沉才是 mg/k。混著比會把一個正常的 31 mm 讀成「剛度不足」。
③ 載重要對上設計條件。 場景裡的車是空車 85 kg,而剛度是由「滿載 650 kg」反推的。用空車跑會得到一個偏寬鬆、而且看起來剛好擦過門檻的數字(5.07 對門檻 5.00)。
2.2 設計餘裕要留在設計目標上,不是靠調求解器
原本的剛度 k 是讓理論下沉正好等於需求上限反推出來的,餘裕是零。而 PhysX 的實際值比線性模型高 5~6%,於是必然超標。
改成需求 5 mm、設計目標 4 mm(留 1.25 倍),實測 4.22 mm,過。
從需求反推設計值 = 零餘裕 = 必然超標。 數值方法與線性模型之間永遠有個幾個百分比,那個百分比要事先讓出來,不是等超標了再去調 solver。
3. 跳動 22 mm:指令形狀,不是接觸
階躍速度指令下,兩台車在啟動 / 到頂反向 / 到底停的那一刻相對轎廂跳 22.0 mm(等速段是 0)。換成限加速度 1.0 m/s² 的梯形速度之後是 0.0 mm。
時間分布本身就是答案:只在速度不連續的那三個瞬間出現,等速段乾淨。650 kg 在階躍速度下會產生遠大於 g 的減速度,車離地是必然的。
所以修法是在驅動端加加速度上限,不是動 solverPositionIterationCount 或摩擦。
⚠ 這一項的判定一定要有對照。 「接觸太硬」與「指令太猛」在單一個跳動數字上長得一模一樣,分不開就會去調接觸參數——而那會把一個指令問題蓋成一組看起來有效的魔數。
4. 陰性結果怎麼寫才算數
要驗的是「剛體與碰撞分層有沒有造成間歇接觸失效」。五台車靜置 20 s(沉降 120 步之後看 1200 步):z 抖動、z 漂移、x/y 漂移全部 0.000 mm。
全零。而全零最危險:
全 0 的結果一定要配正對照,否則「很穩」與「我的量尺沒在量」產生的輸出一模一樣。
腳本內建的對照是把最後一台抬高 20 mm——它必須掉回地面。實測 −0.000 mm,量尺有效。
4.1 對照的基準要選對
第一版拿被抬高那台跟其他車的平均高度比,而五台車在不同樓層(z = 0 / 4 / 8),算出 −3000 mm,看起來像嚴重失效——實際上是基準錯了。
要跟它自己被抬高之前的高度比。
「假失效」和「真失效」一樣會讓你花掉半天,而且它更難察覺,因為數字很大、很嚇人、看起來很像真的。
5. 門檻上那個 200 mm 的洞,只在「開進去」時發作
樓板的井道開口切在井道外緣,而轎廂地板在內緣,中間 panel/2 + clearance + wall 是一個什麼都沒有的洞——實測 0.20 m。
車的驅動輪半徑 0.09 m。掉進 0.20 m 的洞再爬出來就是一次彈跳。
5.1 方向不對稱本身就是診斷指紋
開進轎廂那 1.6 m → 19% 的步數輪子離地、彈到 76 mm、傾角 12.3°
開出來那一段 → 完全乾淨(0.2°、1.5 mm、0%)
同一台車、同樣的控制律,差別只有那一段有沒有跨過那個洞。
若成因是控制律、摩擦或速度這類對稱的條件,進出兩個方向應該同樣受影響。只有幾何上的、有方向性的東西才會做出這種不對稱。
修法是補門檻板,與轎廂留 30 mm(真電梯是 20~35 mm)——不能補滿,轎廂會撞到它:
| 之前 | 之後 | |
|---|---|---|
| 電梯 B 進入 1.6 m | 11.6 s(4.3×)、傾角 12.3°、離地 19% | 3.0 s(1.1×)、傾角 2.1°、離地 5% |
| 電梯 A 進入 1.0 m | (沒量過) | 2.0 s(1.2×)、傾角 0.2°、離地 0% |
5.2 ⚠ 負對照證明的是時間順序,不是因果
補上門檻板之後,某台電梯變成「卡在門口、退了 55 次」,看起來像新改動的 regression。負對照(暫時停用門檻板重建再跑)顯示確實是改動之後才出現的。
而成因不是它。 逐步資料顯示:車起步時朝 0°、目標在正左方 90°,於是它在淨開口裡原地轉,卡在 31°。那個 90° 是離線工具放的(它一律把車放成 yaw=0),真實輪次裡車是從大廳開過來、本來就朝著門。
負對照證明「是這次改動之後才有的」,不證明「是這次改動造成的」——兩者之間還隔著逐步資料。
這一條很反直覺,因為負對照通常被當成因果的黃金標準。它在這裡失效的原因是:改動同時改變了觸發條件(門檻板讓車能開進去,於是離線工具擺放姿態的問題才有機會現形)。
6. 門檻板擋住的不是車,是轎廂自己的門片
補上樓層門檻板之後,即時輪次裡某台電梯的轎廂卡在 1.62 m 整整 480 秒,而它的目標一直是 8.0 m。
沒有錯誤訊息:仲裁器照樣發出授權,車端互鎖照樣正確地拒絕進入,整條鏈路每一段都在做對的事,只有轎廂沒動。
成因是幾何差了半個門厚。要讓開的是轎廂最外側的可動件,而算的時候拿的是轎廂地板的邊緣:
| 局部 y | 世界 y | |
|---|---|---|
| 轎廂地板前緣 | half_d = 1.60 |
13.60 |
| 門片外緣 | half_d + door_thickness/2 = 1.63 |
13.57 |
| 舊的門檻板前緣 | half_d + SILL_GAP = 1.63 |
13.57 ← 正好貼上 |
門片跟著轎廂上下,門檻板固定在樓層——兩者在同一個 y 上,轎廂一升起就撞。停在 1.62 m 是因為門片頂端(轎廂 +2.3 m)那時剛好抵到二樓的門檻板(4.0 m)。
6.1 只量相對量的量尺,量不到參考系本身有沒有動
驗「車搭電梯」的那支工具在壞掉的幾何上是綠的。因為它量的三件事全是「車相對轎廂」——轎廂卡死時車跟著不動,傾角 0.0°、相對位移 0 mm,漂亮的滿分。
一支只量相對量的量尺,量不到參考系本身有沒有動。
補上第四項:讀轎廂關節的實際位置,一段走完之後離目標超過 50 mm 就判失敗。負對照(把門檻板搬回舊位置重跑)印出「轎廂沒到,停在 1.62 m」——與即時輪次同一個數字。
6.2 互鎖門檻要比停穩誤差大
順帶量到:轎廂停穩後的殘餘誤差 0.0~3.6 mm,而車端的對位門檻是 20 mm,約 5 倍餘裕。
⚠ 門檻訂得比停穩誤差還小的話,車會被自己的互鎖永遠擋在門外,而那個症狀跟「轎廂真的沒來」一模一樣。
7. 車在移動中的轎廂裡繼續開
一輪五台物理車的實測:某台車在 t=97 進了三樓的轎廂,t=106 傾角 91.8°,t=120 躺在一樓的井道裡。
中間發生的事:轎廂開始下降,而車還在開。
下一個路網節點與當前節點是同一組 (x, y),只差樓層,所以車瞬間視為抵達,接著朝一樓大廳的節點走——那時轎廂還在 4 m 高。它撞上關著的轎廂門、卡住退出三次、爬上門,翻了。
7.1 兩道既有防線都沒接住,而各自都沒有錯
- 車端的電梯互鎖只問「目標在不在轎廂裡、轎廂在不在這一層」。車已經在轎廂裡了,而目標在另一層的大廳——兩個條件都不成立。
- 離線的搭乘量尺裡的車是靜止的。它驗的是「站著會不會滑、會不會翻」,而這裡壞掉的是「根本不該在動」。
兩道防線的檢查範圍中間有一條縫,而那條縫剛好是這個情境。沒有任何一道防線寫錯,是它們的聯集不完整。
7.2 ⚠ 判準不能用「車與轎廂的高度差」
補上的規則是:車自己在轎廂框裡的時候,轎廂沒停妥就不准動。
但判準不能寫成「轎廂高度與車的高度差多少」——車站在轎廂裡時,它的世界 z 跟著轎廂走,兩者永遠相等,那個條件恆真。
要問的是轎廂本身的狀態:
停在某一層(|cabin − round(cabin/樓高)×樓高| ≤ 20 mm)
且 門開到八成以上
兩個都讀關節的實際位置。門全開的量從關節上限讀,不寫死——那是資產側由轎廂內寬算出來的。
這是一個參考系陷阱的乾淨例子:在會動的參考系裡,相對量會恆真或恆零,而那看起來像「一直都正常」。
8. 運動學只算一次
電梯的高度與門位移由調度那一側算(從車隊管理層的電梯狀態推),Isaac 這側不算運動學,只把收到的關節目標設進去(兩片門是鏡像,右門吃原值、左門吃負值)。
理由很直接:
兩邊各算一次的話,兩個「電梯在哪一層」遲早會分岔,而分岔的症狀是車掉進井道或被門夾住——不是錯誤訊息。
這是 12 篇講的「兩份狀態分歧」在多樓層上的版本,而它的後果更具體:分歧的那幾公分正好是門的開口。
傳輸走一條共用的 MQTT 通道,retain=True。兩個選擇都是量出來的:
- 走 MQTT 而不是 DDS,因為模擬主機的網路只放行 22 埠,DDS 過不去。
retain=True,因為電梯的位置是狀態不是事件——沒有它,晚一點才連上的模擬端要等到下一次電梯動才知道門是開是關。
9. 檢查清單
- [ ] 轎廂關節出現在 articulation 的自由度清單裡,不是只出現在 USD 的 joints 清單裡
- [ ] articulation 的根不帶自由度;要動的關節掛在內部,不是掛在靜態物體上
- [ ] 執行期用
set_joint_position_targets,不改physics:targetPosition - [ ] 「沒被推動」與「跟不上」分開判:先問走到的最高點
- [ ] 靜態下沉在行程中間量,不在限位上量
- [ ] 追隨誤差(
c·v/k)與下沉(mg/k)分開比,不混 - [ ] 驗證載重對上設計條件(不要拿空車驗滿載反推的剛度)
- [ ] 設計目標比需求留餘裕,不靠調 solver 補
- [ ] 跳動類問題先改指令形狀做對照,再考慮接觸參數
- [ ] 全零的結果配一個「應該有反應」的正對照,而且對照的基準是它自己的歷史值
- [ ] 進出方向不對稱時,先找有方向性的幾何,不要先調對稱的參數
- [ ] 門檻/淨空算的是最外側可動件,不是地板邊緣
- [ ] 量尺至少有一項量絕對位置,不能全是相對量
- [ ] 互鎖門檻比實測停穩誤差大(留數倍)
- [ ] 「車在會動的地板上」的判準問地板自己的狀態,不問兩者的差值
- [ ] 運動學只有一側算,另一側純套用
延伸閱讀:31 OmniGraph 與 ROS 2 橋接的真相(門是有的,只是沒有人下指令)、32 把車做成真的會動的車、12 長跑維運(兩份狀態分歧)、17 6.0 的物理調參(寫得進去不代表被採用)。