DXFを扱っていると、LINEやCIRCLEのような2次元図形だけでなく、3次元形状を表すエンティティも登場します。
その中でも比較的構造が単純で、3Dデータの入口として理解しやすいのが3DFACEです。
3DFACEは名前のとおり、3次元空間上の「面」を表します。
基本構造は非常に明快です。
3DFACE
+
頂点1
+
頂点2
+
頂点3
+
頂点4
ただし、DXFパーサーや独自ビューアを作る場合には、単に4点を読めば終わりではありません。
重要なのが、
- 各頂点に対応するグループコード
- 座標系がWCSであること
- 三角形をどう表現するか
- コード70による不可視エッジ
- 頂点順序とエッジの対応
です。
この記事では、AutodeskのDXF仕様を基準に、3DFACEを再利用可能な幾何データとして読み取る方法を整理します。
3DFACEとは
3DFACEは、3次元空間上に3点または4点で定義される面を表すDXFエンティティです。
たとえば、次の4点があるとします。
これらを順番につなぐと、3次元空間上に四辺形の面を定義できます。
概念的には、
P4 -------- P3
| |
| |
P1 -------- P2
という関係です。
実際には各点がX、Y、Z座標を持つため、面はXY平面上にある必要はありません。
3DFACEは「厚みを持つ立体」そのものではなく、基本的には面を表すエンティティとして考えると理解しやすくなります。
主要なグループコード
AutodeskのDXF Referenceでは、3DFACEの主要な頂点情報を次のように定義しています。
| 頂点 | X | Y | Z |
|---|---|---|---|
| 第1頂点 | 10 | 20 | 30 |
| 第2頂点 | 11 | 21 | 31 |
| 第3頂点 | 12 | 22 | 32 |
| 第4頂点 | 13 | 23 | 33 |
したがって、
10
0.0
20
0.0
30
0.0
なら第1頂点は、
です。
続いて、
11
100.0
21
0.0
31
0.0
なら、
となります。
DXFパーサーではコードを個別の値として保持するより、
p1 = (x1, y1, z1)
p2 = (x2, y2, z2)
p3 = (x3, y3, z3)
p4 = (x4, y4, z4)
のように、座標へ正規化してから後段へ渡す方が扱いやすくなります。
3DFACEの座標はWCS
3DFACEを読むときに重要なのが座標系です。
Autodesk仕様では、4頂点はいずれも**WCS(World Coordinate System)**の座標として定義されています。
これはDXF全体を扱ううえで重要な違いです。
たとえばLINEの始点と終点もWCSですが、TRACEの各頂点はOCSとして定義されています。
つまり、
グループコード10だから同じ座標系
とは限りません。
DXFでは同じグループコードでも、どのエンティティに属しているかによって意味が変わるためです。
パーサーでは、
entity type
+
group code
+
coordinate system
をセットで解釈する必要があります。
3DFACEについては、読み取った10/20/30〜13/23/33をWCS上の点として扱えます。
三角形は第3頂点と第4頂点を同じにする
3DFACEという名前から「必ず四角形」と考えてしまうかもしれません。
しかし3点でも面を定義できます。
Autodesk仕様では、3頂点だけで3DFACEを定義する場合、第4頂点を第3頂点と同じ座標にします。
つまり三角形、
を表したい場合は、
とします。
たとえば、
10
0
20
0
30
0
11
100
21
0
31
0
12
50
22
50
32
20
13
50
23
50
33
20
なら、
です。
内部データへ変換するときは、この条件を検出して、
vertices = [p1, p2, p3]
と三角形へ正規化する設計も可能です。
ただし、DXF原文を忠実に保持する層では4頂点のまま保存し、幾何処理用モデルへ変換するときに三角形として解釈する方が、元データを失いにくくなります。
コード70は不可視エッジを表す
3DFACEには形状座標とは別に、重要なグループコードがあります。
70です。
これはInvisible edge flags、つまり「どの辺を表示しないか」を表すビットフラグです。Autodesk仕様では次のように定義されています。
| 値 | 意味 |
|---|---|
| 1 | 第1辺を非表示 |
| 2 | 第2辺を非表示 |
| 4 | 第3辺を非表示 |
| 8 | 第4辺を非表示 |
指定がなければデフォルト値は0です。
0なら、不可視指定された辺はありません。
ここで重要なのは、コード70が単純な列挙値ではなくビットコードだということです。
複数の辺を非表示にする
たとえば、
70
5
だったとします。
5は、
なので、
- 第1辺
- 第3辺
が不可視です。
同様に、
70
10
なら、
なので第2辺と第4辺が不可視です。
Pythonならビット演算で判定できます。
def invisible_edges(flags: int) -> list[bool]:
return [
bool(flags & 1),
bool(flags & 2),
bool(flags & 4),
bool(flags & 8),
]
たとえば、
print(invisible_edges(5))
なら、
[True, False, True, False]
となります。
このようにビットフラグを分解しておけば、レンダリング時に辺ごとの表示・非表示を判断できます。
第1辺から第4辺はどこか
不可視エッジを正しく扱うには、「第1辺」がどの頂点間なのかを決める必要があります。
3DFACEを頂点順に扱えば、エッジは次のように構造化できます。
edge 1 : P1 → P2
edge 2 : P2 → P3
edge 3 : P3 → P4
edge 4 : P4 → P1
したがって内部モデルでは、
edges = [
(0, 1),
(1, 2),
(2, 3),
(3, 0),
]
としておくと扱いやすくなります。
コード70のビット位置と、このエッジ配列を対応させれば、
DXF
↓
頂点解析
↓
エッジ生成
↓
不可視フラグ適用
↓
描画
という処理へ分離できます。
不可視エッジは「辺が存在しない」という意味ではない
ここはデータ構造を設計するときに重要です。
不可視エッジだからといって、
edges.remove(...)
のように辺そのものを削除してしまうと、元の面構造が失われます。
より安全なのは、
{
"vertices": [...],
"edges": [
{"start": 0, "end": 1, "visible": False},
{"start": 1, "end": 2, "visible": True},
{"start": 2, "end": 3, "visible": False},
{"start": 3, "end": 0, "visible": True},
]
}
のように、
幾何構造と表示属性を分離する
ことです。
これはDXFのレイヤー、色、線種などを扱うときと同じ考え方です。
形状そのものと、その形状をどう表示するかは別の情報です。
3DFACEとSOLIDは同じではない
DXFにはSOLIDというエンティティもあります。
名前だけを見ると、
3DFACE = 3Dの面
SOLID = 3D立体
と思いやすいのですが、DXFのSOLIDはそのような単純な意味ではありません。
AutodeskのDXF Referenceでは、SOLIDも4つのコーナーを持つエンティティとして定義されています。コード10〜13で各頂点を保持し、3点だけの場合は第4点を第3点と同じにします。
一方、境界表現を持つ3次元ソリッドには3DSOLIDという別エンティティがあり、こちらはモデルカーネル由来のデータを含む、より複雑な構造です。
したがってDXFパーサーでは、
SOLID
3DFACE
3DSOLID
を名前の印象だけで同じ処理へまとめない方が安全です。
3DFACEとTRACEの違い
TRACEも4頂点を持つため、構造だけを見ると3DFACEに似ています。
しかし大きな違いの一つが座標系です。
Autodesk仕様では、
3DFACE:頂点はWCSTRACE:頂点はOCS
です。
またTRACEには厚みや押し出し方向を表すコード39、210/220/230があります。
この違いからも、DXFパーサーで、
if code == 10:
...
だけで処理するのが危険だと分かります。
より適切なのは、
parser["3DFACE"][10]
parser["TRACE"][10]
のように、エンティティ型ごとのスキーマを持つ考え方です。
面の法線ベクトルを計算する
3DFACEから3頂点が得られれば、面の向きを表す法線ベクトルも計算できます。
とすると、法線ベクトルは外積、
で求められます。
Pythonなら、
def cross(a, b):
return (
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
)
def subtract(a, b):
return (
a[0] - b[0],
a[1] - b[1],
a[2] - b[2],
)
def face_normal(p1, p2, p3):
a = subtract(p2, p1)
b = subtract(p3, p1)
return cross(a, b)
のように実装できます。
ただし、3点が一直線上に並んでいる場合、
となり、有効な面を構成できません。
したがって幾何検証では、法線ベクトルの大きさを調べて退化面を検出できます。
四辺形を三角形へ分割する
WebGLや多くのメッシュ処理系では、面を三角形として扱う方が便利です。
4頂点の3DFACEなら、たとえば、
と、
の2三角形へ分割できます。
def triangulate_face(vertices):
if len(vertices) == 3:
return [tuple(vertices)]
if len(vertices) == 4:
return [
(vertices[0], vertices[1], vertices[2]),
(vertices[0], vertices[2], vertices[3]),
]
raise ValueError("3DFACE requires 3 or 4 vertices")
ただし、一般の四辺形を三角形分割する場合、非平面性や凹形状まで考えると単純な対角線分割だけでは十分でないケースがあります。
DXFを読み取った後に高度なメッシュ処理へ進むなら、
- 4点が同一平面上にあるか
- 頂点順序が妥当か
- 面が自己交差していないか
- どちらの対角線で分割するか
といった検証を別レイヤーで行う方が安全です。
DXFパーサーではRawとGeometryを分ける
3DFACEを将来再利用するなら、DXF原文と幾何モデルを分けて保持すると扱いやすくなります。
たとえばRaw層では、
{
"type": "3DFACE",
"p1": [0.0, 0.0, 0.0],
"p2": [100.0, 0.0, 0.0],
"p3": [100.0, 50.0, 20.0],
"p4": [0.0, 50.0, 20.0],
"invisible_edge_flags": 0,
}
とします。
Geometry層では、
{
"type": "face",
"vertices": [...],
"edges": [...],
"triangles": [...],
"normal": [...],
}
へ変換します。
こうすると、
DXF解析
↓
正規化
↓
幾何検証
↓
メッシュ変換
↓
表示・CAD生成・データ交換
を分離できます。
特定CADの内部表現へ直接変換するより、この中間モデルを挟んだ方が再利用性は高くなります。
実装時に起こりやすい失敗
3DFACE自体は単純ですが、実装ではいくつか注意点があります。
コード10〜13だけを読んでY・Zを落とす。
2D DXFの処理を流用すると起こりやすい問題です。3DFACEでは20〜23、30〜33まで含めて3次元点として保持します。
コード70を通常の数値列挙として扱う。
70はビットフラグです。値5なら「状態5」ではなく、1と4の組み合わせとして解釈します。
3頂点面で第4頂点を別の頂点として数える。
第3点と第4点が同じなら三角形表現の可能性があります。
3DFACEとTRACEを同じ座標処理へ通す。
3DFACEはWCS、TRACEはOCSなので、同一の前提では処理できません。
不可視エッジを幾何構造から削除する。
不可視は表示属性として保持した方が、後工程で元の面構造を再利用できます。
まとめ
DXFの3DFACEは、3次元空間上の面を比較的単純な構造で表現します。
主要なグループコードは、
10 / 20 / 30 → 第1頂点
11 / 21 / 31 → 第2頂点
12 / 22 / 32 → 第3頂点
13 / 23 / 33 → 第4頂点
70 → 不可視エッジ
です。各頂点はWCSで定義されます。
3点の面を表す場合は、
とします。
またコード70はビットフラグなので、
を組み合わせて4本の辺の不可視状態を表現します。
DXFパーサーを設計するなら、単に座標を読むだけでなく、
Raw DXF
↓
頂点
↓
面
↓
エッジ属性
↓
法線・三角形化
まで分離しておくと、独自ビューア、メッシュ変換、3D処理、CAD自動生成へ発展させやすくなります。
3DFACEは小さなエンティティですが、「DXFの記録形式を汎用的な幾何データへ変換する」設計を学ぶ題材として非常に扱いやすいエンティティです。

