さまざまな形状モデル

CG

カメラでの撮影とCG では、CGの被写体の形を表すデータを形状モデルと呼びました。
3次元の形状データの表し方には種類があり、どんな表し方を選ぶかによって、そのデータから計算できる内容が変わります。

形状モデルの分類
CG

形状モデルは、立体を表現する情報をどこまでデータとして持つかによって、大きく3つに分けられます。次の3つは、下に行くほど持つ情報が多く、データから分かることが増えていきます。

  1. ワイヤフレームモデル:頂点と稜線だけで形を表す
  2. サーフェスモデル:稜線に加えて面をもち、物体の表面を表す
  3. ソリッドモデル:中身の詰まった立体として表す

持たせる情報が少ないほど、データ量も小さく、表示や変形の計算も軽く済みますが、そのデータでは情報不足になる場合もあります。どのモデルを選ぶかは、形状データを何に使いたいかによって決める必要があります。

ワイヤフレームモデル
CG

ワイヤフレームモデルは、立体を、頂点と稜線(頂点どうしを結ぶ線)だけで表す形状モデルです。針金(ワイヤ)を組んだ骨組みのように見えることから、この名前がついています。

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のソフトウェアには、形状を稜線だけで描くワイヤフレーム表示 が用意されていることがあります。これは画面への描き方の話であり、内部で持っているデータがワイヤフレームモデルであるとは限りません。

一方で、面のデータを持たないことから、次のような情報を表すことはできません。

  • どこに面があるか(面がなく開いている部分もあるかもしれない)
  • どこが立体の内部か

サーフェスモデル
CG

サーフェスモデルは、稜線に加えて面の情報をもち、物体の表面を表せる形状モデルです。

面がどこにあるかが決まることで、視点から見てどの面が手前にあるのかを判定できます。
これによって、面で隠れる部分を消せるようになり、面に光がどう当たるかも計算できるため、リアリティのあるレンダリング が可能になります。滑らかな曲面をそのまま表せることから、自動車のボディや航空機の機体のように、外形の見え方が問われる設計に使われます。

ただし、サーフェスモデルが表しているのはあくまでも表面であり、中身は空洞です。

Action

切断するを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))

中身が空洞であることから、体積や重さなどはこのモデルからは求められません。また、面と面の間に隙間が空いていても、データとしては成立してしまいます。

ソリッドモデル
CG

ソリッドモデルは、中身の詰まった物体を表すことができるモデルです。

サーフェスモデルとの違いは、面の内側と外側、つまり立体の内部と外部を区別している点にあります。

内部と外部が区別されているため、空間のある点が立体の中にあるのか外にあるのかを判定できます。面の繋がりに隙間や矛盾がないことも、データが満たすべき条件として保証されます。

中身が定まると、体積や質量、重心といった物理的な量を計算できるようになります。部品どうしがぶつかっていないかを調べる干渉チェックや、立体を付け加えたり削ったりする集合演算 も行えます。コンピュータ上で製品を設計するCADでは、ソリッドモデルが使われます。

Action

切断するを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)

ソリッドモデルをデータとしてどう表すかについては、次のようなさまざまな方法が考案されています。