さまざまな形状モデル
カメラでの撮影とCG では、CGの被写体の形を表すデータを形状モデルと呼びました。
3次元の形状データの表し方には種類があり、どんな表し方を選ぶかによって、そのデータから計算できる内容が変わります。
形状モデルの分類
形状モデルは、立体を表現する情報をどこまでデータとして持つかによって、大きく3つに分けられます。次の3つは、下に行くほど持つ情報が多く、データから分かることが増えていきます。
- ワイヤフレームモデル:頂点と稜線だけで形を表す
- サーフェスモデル:稜線に加えて面をもち、物体の表面を表す
- ソリッドモデル:中身の詰まった立体として表す
持たせる情報が少ないほど、データ量も小さく、表示や変形の計算も軽く済みますが、そのデータでは情報不足になる場合もあります。どのモデルを選ぶかは、形状データを何に使いたいかによって決める必要があります。
ワイヤフレームモデル
ワイヤフレームモデルは、立体を、頂点と稜線(頂点どうしを結ぶ線)だけで表す形状モデルです。針金(ワイヤ)を組んだ骨組みのように見えることから、この名前がついています。
Three.jsによる実装概要
/** 頂点と稜線だけを持つジオメトリ。面のデータは持たない */
const createWireframeGeometry = (vertices: [number, number, number][], edges: number[]) => {
// 頂点は 1 つの配列にまとめ、稜線はその頂点番号の対として持つ
const position = new Float32BufferAttribute(vertices.flat(), 3)
return {
edgeGeometry: new BufferGeometry().setAttribute("position", position).setIndex(edges),
vertexGeometry: new BufferGeometry().setAttribute("position", position)
}
}
const { edgeGeometry, vertexGeometry } = createWireframeGeometry(VERTICES, EDGES)
// 稜線を線分として、頂点を点としてシーンに置く
const edgeMaterial = new LineBasicMaterial({ color: EDGE_COLOR })
scene.add(new LineSegments(edgeGeometry, edgeMaterial))
const vertexMaterial = new PointsMaterial({ color: VERTEX_COLOR, size: 0.08 })
const vertexPoints = new Points(vertexGeometry, vertexMaterial)
scene.add(vertexPoints)面のデータを持たない分、データ量は小さく、回転させたり動かしたりしたときの再描画も高速です。頂点どうしの接続関係を簡単に表現できるため、建築物など直線要素の多い立体の形の確認や、対話的に操作しながら形を検討する場面に向いています。
3次元CGのソフトウェアには、形状を稜線だけで描くワイヤフレーム表示 が用意されていることがあります。これは画面への描き方の話であり、内部で持っているデータがワイヤフレームモデルであるとは限りません。
一方で、面のデータを持たないことから、次のような情報を表すことはできません。
- どこに面があるか(面がなく開いている部分もあるかもしれない)
- どこが立体の内部か
サーフェスモデル
サーフェスモデルは、稜線に加えて面の情報をもち、物体の表面を表せる形状モデルです。
面がどこにあるかが決まることで、視点から見てどの面が手前にあるのかを判定できます。
これによって、面で隠れる部分を消せるようになり、面に光がどう当たるかも計算できるため、リアリティのあるレンダリング が可能になります。滑らかな曲面をそのまま表せることから、自動車のボディや航空機の機体のように、外形の見え方が問われる設計に使われます。
ただし、サーフェスモデルが表しているのはあくまでも表面であり、中身は空洞です。
切断するをONにして、中身が空洞であることを確認しよう
Three.jsによる実装概要
/** 面のデータから、面ごとに独立した頂点を持つジオメトリを作る */
const createSurfaceGeometry = (vertices: [number, number, number][], faces: number[][]) => {
const positions = createFacePositions(vertices, faces)
const geometry = new BufferGeometry()
geometry.setAttribute("position", new Float32BufferAttribute(positions, 3))
// 頂点を面ごとに分けているので、法線は面ごとに平らに求まる
geometry.computeVertexNormals()
return geometry
}
// クリッピング(切り口の面の向こう側を描かない)を有効にする
renderer.localClippingEnabled = true
const geometry = createSurfaceGeometry(VERTICES, FACES)
// 切り口の面。手前下の角を斜めに落とす位置に置き、
// 奥側だけを残すと本体、手前側だけを残すと切り取った欠片になる
const [p0, p1, p2] = SECTION.map((point) => new Vector3(...point))
const bodyClip = new Plane().setFromCoplanarPoints(p0, p1, p2)
const fragmentClip = bodyClip.clone().negate()
// 本体。切り口から内側の面が見えるよう、裏側も描く
const bodyMaterial = new MeshStandardMaterial({
color: SURFACE_COLOR,
roughness: 0.6,
side: DoubleSide,
clippingPlanes: [bodyClip]
})
scene.add(new Mesh(geometry, bodyMaterial))
// 欠片。同じ形状データを、逆向きの面で切り分ける
const fragmentMaterial = new MeshStandardMaterial({
color: SURFACE_COLOR,
roughness: 0.6,
side: DoubleSide,
clippingPlanes: [fragmentClip]
})
const fragment = new Mesh(geometry, fragmentMaterial)
scene.add(fragment)
// 「切断する」をオンにしたとき、欠片を離す。
// 切り口に垂直な向きだと、本体の切り口をちょうど隠してしまうので下へ逃がす
const separationDirection = fragmentClip.normal.clone().add(new Vector3(0, -1, 0)).normalize()
fragment.position.copy(separationDirection).multiplyScalar(0.7)
// クリッピング面はワールド座標で効くので、欠片の移動に合わせて動かす
fragment.updateMatrixWorld()
fragmentClip.applyMatrix4(fragment.matrixWorld)
// 面があるので、光の当たり方から陰影が決まる
const light = new DirectionalLight(LIGHT_COLOR, 2.5)
light.position.set(4, 5, 3)
scene.add(light)
scene.add(new AmbientLight(LIGHT_COLOR, 0.4))中身が空洞であることから、体積や重さなどはこのモデルからは求められません。また、面と面の間に隙間が空いていても、データとしては成立してしまいます。
ソリッドモデル
ソリッドモデルは、中身の詰まった物体を表すことができるモデルです。
サーフェスモデルとの違いは、面の内側と外側、つまり立体の内部と外部を区別している点にあります。
内部と外部が区別されているため、空間のある点が立体の中にあるのか外にあるのかを判定できます。面の繋がりに隙間や矛盾がないことも、データが満たすべき条件として保証されます。
中身が定まると、体積や質量、重心といった物理的な量を計算できるようになります。部品どうしがぶつかっていないかを調べる干渉チェックや、立体を付け加えたり削ったりする集合演算 も行えます。コンピュータ上で製品を設計するCADでは、ソリッドモデルが使われます。
切断するをONにして、断面が現れることを確認しよう
Three.jsによる実装概要
// クリッピング(切り口の面の向こう側を描かない)を有効にする
renderer.localClippingEnabled = true
const geometry = createSurfaceGeometry(VERTICES, FACES)
// 切り口の面。奥側だけを残すと本体、手前側だけを残すと切り取った欠片になる
const [p0, p1, p2] = SECTION.map((point) => new Vector3(...point))
const bodyClip = new Plane().setFromCoplanarPoints(p0, p1, p2)
const fragmentClip = bodyClip.clone().negate()
// 本体の殻と、欠片の殻。同じ形状データを、逆向きの面で切り分ける
const bodyMaterial = new MeshStandardMaterial({
color: SURFACE_COLOR,
roughness: 0.6,
side: DoubleSide,
clippingPlanes: [bodyClip]
})
scene.add(new Mesh(geometry, bodyMaterial))
const fragmentMaterial = new MeshStandardMaterial({
color: SURFACE_COLOR,
roughness: 0.6,
side: DoubleSide,
clippingPlanes: [fragmentClip]
})
const fragment = new Group()
fragment.add(new Mesh(geometry, fragmentMaterial))
scene.add(fragment)
/** 切り口の輪郭から、断面を塞ぐ 1 枚の面を作る */
const createCapGeometry = (outline: [number, number, number][]) =>
createSurfaceGeometry(outline, [outline.map((_, index) => index)])
// 断面。これがあると、中身の詰まった立体として見える。
// 本体側と欠片側で向きが逆になるので、同じ 1 枚を裏表とも描く
const capGeometry = createCapGeometry(SECTION)
const capMaterial = new MeshStandardMaterial({ color: SURFACE_COLOR, roughness: 0.6, side: DoubleSide })
scene.add(new Mesh(capGeometry, capMaterial))
fragment.add(new Mesh(capGeometry, capMaterial))
// 「切断する」をオンにしたとき、欠片を断面ごと下へ離す
const separationDirection = fragmentClip.normal.clone().add(new Vector3(0, -1, 0)).normalize()
fragment.position.copy(separationDirection).multiplyScalar(0.7)
fragment.updateMatrixWorld()
fragmentClip.applyMatrix4(fragment.matrixWorld)ソリッドモデルをデータとしてどう表すかについては、次のようなさまざまな方法が考案されています。