CAD 資產的判讀與轉換:從 IGES 檔到可用的碰撞幾何
一個累積幾年的資產庫裡,同一個物件常常有好幾份:CAD 原始檔、從它轉出的 USD、 還有一份長得很像但來源完全不同的美術資產。哪一份現在被場景用著、哪一份的幾何品質更好, 不會寫在檔名上,而判斷錯的代價是實質的——把一份現成可用的資產判定成「轉換失敗」, 或者更糟,把美術資產當成工程模型去調物理,然後花好幾天追一個根本不存在的建模瑕疵。
這篇處理三件事:怎麼在不開 CAD 軟體的前提下判讀 IGES 原始檔、怎麼正確數出一份 USD 裡 到底有沒有幾何(這一項有一個會讓人完全看錯的陷阱)、以及 Isaac Sim 6.0 內建轉換鏈的 實際呼叫方式。
前置定位見 03 模型格式與匯入;本篇是它 §2「CAD → USD」 那一節的操作展開。
1. 三種版本,三種幾何品質
同一個物件在資產庫裡常見的三種形態:
| 形態 | 幾何來源 | 典型特徵 |
|---|---|---|
CAD 原始檔(.IGS / .STEP / .SLDPRT) |
機構設計 | 曲面(NURBS / trimmed surface),不是三角網格 |
| CAD 轉出的 USD | tessellate 上面那份 | 零件階層完整、面數低、外觀樸素 |
| 美術資產 | 資產庫 / 掃描 / 手工建模 | 面數高、有 PBR 貼圖、幾何刻意做出「真實感」 |
一個實測對照(同一種標準棧板):
| 美術資產 | CAD 轉出 | |
|---|---|---|
| 三角形 | 11,870 | 5,152(43%) |
| 側面平整度 | 1.6~41.5 mm 凹凸,缺角達 4 cm 級 | 設計值 |
| 零件結構 | 單一 mesh | 4 個 prototype × 多實例 |
那 4 cm 的凹凸不是建模瑕疵,是設計意圖 —— 那份資產叫「回收木棧板」, 它就該看起來破舊。問題在於它同時被拿去當物理接觸面用, 而接觸面的平整度直接決定摩擦、翻滾力矩與求解器能不能收斂 (見 13 接觸與抓握的第一性原理)。
1.1 怎麼辨識美術資產
看檔案裡有沒有這些東西,任一命中就幾乎確定:
| 訊號 | 為什麼 |
|---|---|
DomeLight / DistantLight / Camera / RenderProduct / RenderSettings |
那是一個資產展示場景,不是零件模型 |
材質是 PBR 三件組(*_Albedo / *_Normal / *_ORM 貼圖 + .mdl) |
為了渲染真實感而做 |
mesh 名稱形如 SM_<名稱>_<兩位數變體> |
SM = Static Mesh,商業資產庫的命名慣例 |
| 單一高密度 mesh、沒有零件階層 | 建模時就合併了 |
1.2 怎麼辨識 CAD 轉出的 USD
| 訊號 | 為什麼 |
|---|---|
prim 名稱有 tn__ 前綴 |
USD 對非 ASCII 名稱的編碼(translated name)。CAD 零件常是中文或帶特殊字元命名 |
名稱含 DE<數字> |
IGES 的 Directory Entry 編號,轉換器直接拿來命名 |
名稱含 STEP1 之類的來源標記 |
從 STEP 檔轉來 |
stage.GetPrototypes() 非空 |
轉換器預設用 instancing(見 §3) |
材質是 UsdPreviewSurface,沒有貼圖 |
CAD 沒有材質資訊,轉換器只給預設 |
2. 判讀 IGES 原始檔(不需要 CAD 軟體)
IGES 是固定 80 欄的純文字格式,用一支 Python 就能讀出關鍵資訊。
2.1 檔案結構
每一行的第 73 欄是 section code:
| Code | Section | 內容 |
|---|---|---|
S |
Start | 人可讀的說明 |
G |
Global | 單位、精度、產生系統、作者 |
D |
Directory Entry | 每個實體 2 行:型別、狀態、指向 P section 的指標 |
P |
Parameter Data | 實體的實際數值(控制點、節點向量……) |
T |
Terminate | 各 section 的行數統計 |
2.2 實體型別統計:判斷需不需要 tessellation
Directory Entry 每個實體佔 2 行,第 1 行的欄位 1–8 是型別編號:
with open(path, errors="replace") as f:
de = [ln for ln in f if len(ln) >= 73 and ln[72] == "D"]
for i in range(0, len(de) - 1, 2):
entity_type = int(de[i][0:8])
常見型別與判讀:
| 型別 | 意義 | 對轉換的含意 |
|---|---|---|
| 110 / 100 / 126 | 直線 / 圓弧 / NURBS 曲線 | 曲線,多半是曲面的邊界 |
| 128 | NURBS 曲面 | 要 tessellation |
| 144 | Trimmed surface(修剪曲面) | 要 tessellation,最常見的實體 |
| 120 / 122 | 旋轉面 / 拉伸面 | 解析曲面,要 tessellation |
| 186 / 510 / 512 / 514 | B-rep 實體 / Face / Shell | 要 B-rep tessellation,比曲面更重 |
| 106 | Copious data | 已經是離散點,不需 tessellation |
| 308 / 408 | Subfigure 定義 / 實例 | 零件階層——轉出的 USD 會用它做 instancing |
一份典型的機構件 IGES(標準棧板)實測:
1552 × 126 NURBS 曲線 384 × 144 Trimmed surface
1352 × 110 直線 292 × 128 NURBS 曲面
768 × 102 複合曲線 92 × 120 旋轉面
384 × 142 曲面上的曲線 28 × 408 Subfigure 實例
→ 全部是曲面,沒有任何離散幾何。所以轉換結果的面數與品質完全由 tessellation 參數決定,不是檔案內容決定。要調品質就去調轉換器的 LOD(§4.3)。
2.3 單位:Global section 的 Hollerith 陷阱
Global section 用逗號分隔,單位資訊在第 14/15 個參數。但不能直接用逗號 split ——
IGES 的字串是 Hollerith 格式(2HMM 表示「長度 2 的字串 MM」),
字串內容裡的逗號會破壞欄位切分,讓後面所有欄位偏移。
實務上最省事的做法是直接找 <數字>H 開頭的 token:
raw = "".join(ln[:72] for ln in g_lines)
# 找 Hollerith:如 "2HMM" → 單位是 mm
實測值得留意的是,同一份檔案裡 Global section 也常帶著原始檔名與零件編號, 那往往是尺寸的直接線索(例如編號裡就寫著標稱長寬高)。
2.4 blank status:一個看起來很有解釋力、但通常不是原因的欄位
Directory Entry 第 1 行的欄位 65–72 是 status number,格式 BBSSAAHH:
| 欄 | 意義 |
|---|---|
BB |
Blank status:00 可見 / 01 隱藏 |
SS |
Subordinate entity switch:00 獨立 / 01 被其他實體參照 |
AA |
Entity use flag:00 幾何 / 05 結構 / 06 其他 |
HH |
Hierarchy |
看到「轉出來沒有幾何」時,很容易懷疑是所有實體都被標成 blanked 而被轉換器略過
(轉換器確實有 omitHiddenOnLoad 這個預設開啟的選項)。
⚠ 但實測三份不同的 CAD 檔,blank 分布是同型的:
| 檔案 | blanked / 總實體 | 轉換結果 |
|---|---|---|
| 棧板 | 4,408 / 5,478(80%) | 有幾何 |
| 貨架總成 | 28,856 / 36,378(79%) | 有幾何 |
| 滑軌底座 | 660 / 765(86%) | 有幾何 |
「大多數實體 blanked」是 CAD 匯出的常態(被 Subfigure 參照的子實體通常都標 blanked), 不是異常訊號。把它當成失敗原因會走進死路——這一條是實際踩過才寫下來的。
3. 判讀轉出的 USD:instancing 會讓你完全看錯
這是本篇最重要的一節。
數一份 USD 有多少 mesh,最直覺的寫法是:
meshes = [p for p in stage.Traverse() if p.IsA(UsdGeom.Mesh)]
這對 CAD 轉出的資產會回傳 0,而檔案裡明明有幾百個 mesh。
原因:Usd.Stage.Traverse() 的預設 predicate 不走訪 instance proxy。
而 Omniverse 的 CAD 轉換器預設就用 instancing
(instancingStyle = eInstanceableReference)——CAD 組件裡大量重複的螺栓、
橫樑、墊塊,用 instance 存一份原型再引用是正確的做法。
三種讀法的實測對照(同一份棧板 USD,148 KB):
| 讀法 | 結果 |
|---|---|
stage.Traverse() |
0 |
Usd.PrimRange.Stage(stage, Usd.TraverseInstanceProxies(Usd.PrimAllPrimsPredicate)) |
3,684 |
stage.GetPrototypes() 內的 mesh |
384(唯一幾何:5,920 頂點 / 5,152 三角形) |
3.1 正確的數法
from pxr import Usd, UsdGeom
def count_geometry(stage):
"""回傳 (唯一幾何的 mesh 數, 展開後的實例數)。"""
protos = stage.GetPrototypes()
if protos: # ← 先問這一句
uniq = [q for pr in protos
for q in Usd.PrimRange(pr, Usd.PrimAllPrimsPredicate)
if q.IsA(UsdGeom.Mesh)]
rng = Usd.PrimRange.Stage(stage, Usd.TraverseInstanceProxies(
Usd.PrimAllPrimsPredicate))
return len(uniq), len([p for p in rng if p.IsA(UsdGeom.Mesh)])
plain = [p for p in stage.Traverse() if p.IsA(UsdGeom.Mesh)]
return len(plain), len(plain)
判準:數 mesh 之前先問 stage.GetPrototypes() 有幾個。非 0 就必須換讀法。
兩個數字的用途不同:唯一幾何決定記憶體與 cook 成本、也是你要檢查的幾何本體; 展開後的實例數才是場景裡實際存在幾個物件。
3.1a GetPrototypes() 的順序不保證穩定
GetPrototypes() 回傳的是一個 list,但同一份檔案在不同次 Stage.Open 之間,
順序可能不同(prototype 路徑形如 /__Prototype_1,編號由 instancing 的處理順序決定)。
實測踩到:第一次分析時 proto[3] 是一個參考包絡框、proto[1] 是零件;
第二次執行同一份檔案,兩者對調。腳本裡寫死 EXCLUDE = 3 去排除包絡框,
第二次就把 9 個零件一起排除了,而輸出看起來完全正常(只是少了一類零件)。
不要用 prototype 的索引當識別。用內容特徵。
def classify(stage, cache):
"""用內容特徵標記 prototype,而不是索引。"""
tags = {}
for pr in stage.GetPrototypes():
ms = [q for q in Usd.PrimRange(pr, Usd.PrimAllPrimsPredicate)
if q.IsA(UsdGeom.Mesh)]
tri = sum(sum(max(0, c - 2) for c in
(UsdGeom.Mesh(q).GetFaceVertexCountsAttr().Get() or []))
for q in ms)
r = cache.ComputeUntransformedBound(pr).ComputeAlignedRange()
ext = sorted([r.GetMax()[k] - r.GetMin()[k] for k in range(3)], reverse=True)
tags[pr.GetPath()] = dict(mesh=len(ms), tri=tri, ext=ext,
box_like=(len(ms) <= 6 and tri <= 12))
return tags
box_like(6 個 mesh / 12 三角形 = 一個純六面盒)是個實用的判別式:
CAD 檔裡常混著參考包絡框、量測基準面、包裝空間標示這類非零件幾何,
它們的共同特徵就是「一個沒有任何細節的盒子」。
3.2 為什麼正對照沒有擋住這個錯
這個錯誤實際發生過,而且當時是做過對照的:用同一段程式讀一份美術資產, 得到的三角形數與另一份文件記錄的場上值完全吻合——對照通過了。
但美術資產的 GetPrototypes() 是空的(沒有 instancing),CAD 資產全部有。
對照組剛好落在方法有效的那一側,於是驗證了一個並不成立的推廣。
正對照要挑「與待測對象同類」的樣本。 拿一份沒有 instancing 的檔去驗證「我數得到 mesh」, 不能保證對有 instancing 的檔也成立。
這是 19 調參實驗方法論 的 「極端值正對照」在查詢方法上的對應形式:對照組要能區分「方法有效」與「方法無效」, 落在同一側的對照不提供資訊。
4. 轉換管線:Isaac Sim 6.0.1 的實際呼叫方式
Isaac Sim 6.0.1 內建完整的 CAD 轉換鏈,不需要另外裝 Omniverse 桌面版:
| Extension | 角色 |
|---|---|
omni.kit.converter.cad |
聚合層(依賴下面三個) |
omni.kit.converter.hoops / hoops_core |
HOOPS Exchange,負責 IGES / STEP / SLDPRT / CATPart / X_T / PRT |
omni.kit.converter.dgn / jt |
MicroStation DGN / Siemens JT |
4.1 正確的呼叫方式
官方自己的做法是啟動一個只載入轉換器的獨立 kit 進程
(見 omni.kit.converter.hoops/delegate.py 的 launch_kit_app),照抄即可:
LAUNCH=$(ls -d $EXTSCACHE/omni.kit.converter.hoops-*/)omni/kit/converter/hoops/process/launch_hoops_app.py
echo '{}' > /tmp/cfg.json # 空 config = 用轉換器預設
$ISAAC_SIM/kit/kit \
--ext-folder $ISAAC_SIM/exts \
--ext-folder $ISAAC_SIM/extscache \
--ext-folder $ISAAC_SIM/apps \
--enable omni.kit.converter.hoops_core \
--exec "$LAUNCH --input-path <in.IGS> --output-path <out.usd> --config-path /tmp/cfg.json" \
--no-window --info
實測耗時:1.9 MB / 5,478 實體的棧板 15 秒;12 MB / 36,378 實體的貨架總成 19 秒 (含 kit 啟動)。
4.2 三條走不通的路
| 做法 | 結果 |
|---|---|
用 omni.kit.asset_converter |
UNSUPPORTED_IMPORT_FORMAT。它只吃 .gltf / .fbx / .obj,CAD 是完全另一條管線 |
在 SimulationApp 進程裡 import omni.kit.converter.hoops_core |
libTD_DbCore.so: undefined symbol: _ZN13OdConstStringC1EPKw —— ODA library 的載入順序在完整 app 環境下不對 |
| 掛載其他 kit 進程正在使用的 cache volume | Failed to acquire exclusive lock to data store。轉換用的臨時容器要用自己的 cache |
第一條特別值得記:兩個 extension 名字都叫 "converter",支援格式卻不重疊,
而錯誤訊息 UNSUPPORTED_IMPORT_FORMAT 很容易被讀成「這個檔案有問題」。
4.3 可調參數
HoopsOptions 的欄位(透過 config JSON 傳入),影響幾何品質的是前幾項:
| 參數 | 預設 | 作用 |
|---|---|---|
tessLOD |
2 | tessellation 細緻度(0~4)。§2.2 說過面數完全由這裡決定 |
accurateTessellation |
false | 更貼近原始曲面,面數更高 |
accurateSurfaceCurvatures |
true | 曲率處理 |
instancingStyle |
eInstanceableReference |
產生 instance——就是 §3 那個陷阱的來源 |
dedup |
true | 相同幾何去重 |
convertPhysicsData |
false | CAD 若帶物理資訊是否轉入 |
omitHiddenOnLoad |
true | 略過 blanked 實體(§2.4) |
upAxis |
file default | 座標系 |
5. 驗證轉換結果:三層,缺一層就會誤判
| 層 | 看什麼 | 陷阱 |
|---|---|---|
| 轉換器自述 | log 裡的 Total Meshes in USD = N / Total Triangles in USD = N |
訊息在冗長 log 的中段,不要用管線截斷 |
| USD 結構 | prototypes 數 + 唯一幾何 mesh 數(§3.1) | Traverse() 對 instanced 資產回 0 |
| 幾何本體 | bbox、頂點數分布、沿高度軸的佔用剖面 | 見 §6 的座標與單位陷阱 |
第一層最容易被跳過,而它是最直接的答案。實際發生過的情況:轉換器在 log 中段
明寫 Total Meshes in USD = 384,而那段正好落在被 head -40 / tail -70 截掉的區間,
於是往「檔案有問題」「license 有問題」猜了兩輪。
診斷用的輸出寫進檔案,不要走管線截斷。 「跑完了但沒產出」的第一步永遠是看完整輸出,不是猜成因。
第三層的「佔用剖面」值得一提:把 bbox 沿高度軸切 N 段,數每段有幾個 mesh 相交, 可以在不開 GUI 的情況下看出結構。一份 CAD 檔的實測剖面:
−59.6 ~ 133.0 mm 79→477→1688→798→688→2285 個 mesh ← 密集,零件本體
133.0 ~ 1225.0 mm 固定 12 個 ← 稀疏,貫穿全高
那 12 個固定值一路貫穿到頂,追下去是 2 個實例 × 6 面的純盒子(24 頂點 / 12 三角形) —— CAD 檔裡混著參考包絡框,不是零件。沒有這個剖面,整體 bbox 會把包絡框算進去, 得到一個比實際零件大得多的尺寸。
離群零件是同一類問題的另一種形狀。 同一份檔案排除包絡框之後,整體尺寸仍比 標稱值大 86.8 mm。列出每個零件的座標範圍才看出來:26 個零件裡有 25 個 的某軸範圍落在同一組值上,只有 1 個偏移了整整一個零件寬度 —— 那是 CAD 裡 一個沒對齊的零件。
讀整體尺寸時,順手做一次「各零件座標範圍的次數統計」。 出現次數為 1 的那組值就是離群值,而平均值或 bbox 都不會告訴你它存在。
6. 讀幾何尺寸時的座標與單位陷阱
三個都會產生「看起來合理但錯誤」的數字。
6.1 world AABB 對有傾角的物件會被撐大
軸對齊包圍盒(AABB)不會跟著物件旋轉。一個 1060 mm 寬、真實高度 121 mm 的板件, 安裝時有 2.62° 傾角,它的 world AABB 高度會量到 168.8 mm:
1060 mm × sin(2.62°) = 48.5 mm 121 + 48.5 ≈ 169 ✓
照那個數字往下算(例如「上下板之間的淨空」),會得到完全虛構的結果。
物件有傾角時,world AABB 只能用來比對同一物件的相對變化, 不能拿來讀尺寸,也不能拿來比較不同位置的高度差。 要讀尺寸就讀局部座標(
xformOp:translate/xformOp:scale), 或用ComputeUntransformedBound。
6.2 軸向不能假設
「高度軸是 Z」在 CAD 轉出的資產上經常不成立——CAD 軟體的座標慣例、
轉換器的 upAxis 設定、以及各層 xform 疊加後,實際的高度軸可能是任一軸。
穩健的做法是從資料本身判別:
hz = min(range(3), key=lambda k: ext[k]) # 最薄的那軸 = 板件的高度軸
但這只對「明顯扁平」的物件有效。近正方形的物件(長寬比 < 1.1)用這招會判錯, 要用別的線索(例如零件的排列方向),而且判錯不會報錯。
6.3 單位不能假設,而且同一份 stage 裡可能不一致
UsdGeom.GetStageMetersPerUnit() 只給 stage 層級的宣告值。
各子樹的 xform scale 會讓局部座標的實際單位不同——實測同一份場景裡,
一個子樹的局部座標是公分、另一個是公釐。
用一個已知真值反推單位。 量到
1120000這種數量級不合理的值,就是單位錯的訊號——但前提是你把它印出來看。
7. 檢查清單
拿到一份不熟悉的資產時:
- [ ]
stage.GetPrototypes()有幾個?非 0 就換讀法(§3.1) - [ ] 檔案裡有
Camera/DomeLight/RenderSettings嗎?有 → 是美術展示場景(§1.1) - [ ] prim 名稱有
tn__/DE<數字>嗎?有 → 是 CAD 轉出的(§1.2) - [ ] 材質是 PBR 貼圖組還是
PreviewSurface? - [ ] 整體 bbox 與「一個零件應該多大」差很多嗎?差很多 → 檔案裡混著別的東西,做佔用剖面(§5)
- [ ] 排除非零件幾何時,用的是內容特徵還是 prototype 索引?索引跨 session 會變(§3.1a)
- [ ] 整體 bbox 有沒有被單一離群零件撐大?列出各零件的座標範圍找離群值
- [ ] 要讀尺寸:讀的是局部座標還是 world AABB?物件有傾角嗎?(§6.1)
- [ ] 高度軸是從資料判出來的,還是假設的?(§6.2)
- [ ] 單位用已知真值反推過了嗎?(§6.3)
要跑轉換時:
- [ ] 用的是
hoops_core那條管線,不是asset_converter(§4.2) - [ ] 獨立 kit 進程,不在
SimulationApp裡 import(§4.2) - [ ] 臨時容器用自己的 cache volume(§4.2)
- [ ] 完整 log 寫進檔案(§5)
- [ ] 驗證三層都做了,不是只看檔案大小(§5)
8. 延伸閱讀
- 03 模型格式與匯入 —— 匯入路徑的整體定位、依賴解析、資產授權
- 10 場景資產的物理結構 —— 轉出幾何之後怎麼加碰撞與物理
- 13 接觸與抓握的第一性原理 —— 為什麼接觸面的平整度是物理結果的上游
- 19 調參實驗方法論 —— 正對照的設計原則(§3.2 是它在查詢方法上的對應)
- 官方文件:Omniverse CAD Converter、HOOPS Exchange 支援格式清單