WebGL & Three.js

Three.js Geometryの種類と変更方法|BufferGeometry・頂点更新を解説

Three.jsのGeometryの種類と選び方を初心者向けに解説します。Box・Sphere・Planeなどの基本形状、BufferGeometryのposition・normal・uv、頂点の変更、サイズ変更、EdgesGeometry、セグメント数、disposeまで実装例で確認できます。

この記事の目次
  1. Geometry・Material・Meshの違い
  2. よく使うGeometryの種類と選び方
  3. Box・Sphere・Planeの基本例
  4. Shape・Extrude・Lathe・Tubeには元データが必要
  5. GeometryはBufferGeometryでできている
  6. BufferGeometryで三角形を自作する
  7. indexで頂点を共有する
  8. 生成済みGeometryの頂点を変更する
  9. 法線とbounding volumeを再計算する
  10. bufferの頂点数は途中で増減できない
  11. 幅・高さ・segment数を変更する
  12. Geometryを移動・回転・中央揃えする
  13. segment数とperformanceの考え方
  14. wireframeとEdgesGeometryの違い
  15. Geometryを切り替える安全な実装
  16. 不要なGeometryをdisposeする
  17. 古いTHREE.Geometryコードを移行する
  18. Geometryが表示されないときの確認項目
  19. 形が黒い・陰影がおかしい
  20. 一部の面が消える
  21. 頂点を変えたのに表示が変わらない
  22. Geometry選択の実践チェックリスト
  23. まとめ

Three.jsのGeometry(ジオメトリ)は、3Dオブジェクトの頂点・面・法線・UVなど「形」を表すデータです。見た目を決めるMaterialと組み合わせ、MeshにしてSceneへ追加します。

最初に要点を整理します。

  • 箱はBoxGeometry、球はSphereGeometry、床はPlaneGeometryから始める
  • 現在の組み込みGeometryは共通してBufferGeometryを基盤にする
  • 頂点を変更したらposition.needsUpdate = trueを設定する
  • 幅やsegment数を変える場合は、新しいGeometryを作って古いものをdispose()する
  • 輪郭線はEdgesGeometryLineSegmentsを組み合わせる

この記事では、種類の丸暗記ではなく「どのGeometryを選ぶか」「生成後にどう変更するか」を、現行Three.jsのAPIで順番に解説します。

Geometry・Material・Meshの違い

Three.jsで物体を表示する基本単位がMeshです。MeshはGeometryとMaterialを組み合わせたObject3Dです。

要素 役割
Geometry 頂点、三角形、法線、UVなどの形状データ BoxGeometry
Material 色、光沢、透明度、textureなどの描画方法 MeshStandardMaterial
Mesh GeometryとMaterialを組み合わせた表示object new THREE.Mesh(geometry, material)
const geometry = new THREE.BoxGeometry(2, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x38bdf8 });
const mesh = new THREE.Mesh(geometry, material);

scene.add(mesh);

MeshStandardMaterialは光源を必要とします。Geometryだけをまず表示したい場合は、光源なしで見えるMeshBasicMaterialを使うと切り分けやすくなります。Materialの選び方はThree.js Materialの種類で詳しく解説しています。

Scene、Camera、Rendererを含む最小構成から確認したい場合は、先にThree.jsの始め方を参照してください。

スポンサーリンク

よく使うGeometryの種類と選び方

Three.jsには実行時に形を生成するprimitive Geometryが用意されています。まずは用途から候補を絞ります。

作りたい形 Geometry 主な引数
箱・直方体 BoxGeometry width, height, depth, 各segment数
SphereGeometry radius, widthSegments, heightSegments
平面・床 PlaneGeometry width, height, widthSegments, heightSegments
円・円環 CircleGeometry / RingGeometry radius、内外半径、segment数
円錐・円柱・カプセル ConeGeometry / CylinderGeometry / CapsuleGeometry radius, height, segment数
ドーナツ・結び目 TorusGeometry / TorusKnotGeometry radius, tube, segment数
正多面体 TetrahedronGeometryなど radius, detail
2D輪郭 ShapeGeometry Shape, curveSegments
2D輪郭を押し出す ExtrudeGeometry Shape, depth・bevelなどのoptions
回転体・曲線に沿う管 LatheGeometry / TubeGeometry Vector2の列 / Curve

Box・Sphere・Planeの基本例

よく使う3種類は次のように生成できます。

const box = new THREE.BoxGeometry(2, 1, 1);
const sphere = new THREE.SphereGeometry(1, 32, 16);
const plane = new THREE.PlaneGeometry(10, 10, 1, 1);

PlaneGeometryは初期状態でXY平面上にあり、正面は+Z方向です。床としてXZ平面へ置くならMeshを回転します。

const floor = new THREE.Mesh(plane, floorMaterial);
floor.rotation.x = -Math.PI / 2;

位置・回転・拡大縮小はGeometryの頂点を書き換えなくても、Meshのtransformで変更できます。用途の違いはThree.jsで移動・回転・scaleを設定する方法も参照してください。

Shape・Extrude・Lathe・Tubeには元データが必要

一部のGeometryは、constructorを引数なしで呼ぶだけでは目的の形を作れません。

  • ShapeGeometryExtrudeGeometryにはTHREE.Shapeが必要
  • LatheGeometryには輪郭を表すVector2の配列が必要
  • TubeGeometryにはCurveを継承したpathが必要

たとえばTubeGeometryは、3D点の配列をCatmullRomCurve3へ渡してpathを作ります。

const path = new THREE.CatmullRomCurve3([
  new THREE.Vector3(-2, 0, 0),
  new THREE.Vector3(-1, 1, 0),
  new THREE.Vector3(1, -1, 0),
  new THREE.Vector3(2, 0, 0),
]);

const geometry = new THREE.TubeGeometry(path, 64, 0.15, 8, false);

種類を切り替えるUIを作る場合も、引数なしのconstructor名だけをswitchするのではなく、各形状に必要なinputを用意したfactory関数へ分けます。

GeometryはBufferGeometryでできている

現在のThree.jsでは、組み込みのBoxGeometryやSphereGeometryもBufferGeometryを継承しています。BufferGeometryは、GPUへ渡しやすいTypedArrayをBufferAttributeとして保持します。

代表的なattributeは次のとおりです。

attribute 内容 1頂点あたりの値
position 頂点の座標 x, y, zの3個
normal 面が向く方向。lightingに使用 x, y, zの3個
uv texture上の位置 u, vの2個
color 頂点ごとの色 r, g, bなど
index どの頂点を使って三角形を作るか 三角形ごとに3 index

実際のGeometryをconsoleで確認できます。

const geometry = new THREE.BoxGeometry(1, 1, 1);
const position = geometry.getAttribute("position");

console.log(position.count);
console.log(position.itemSize); // 3
console.log(geometry.getAttribute("normal"));
console.log(geometry.getAttribute("uv"));
console.log(geometry.getIndex());

textureを正しく貼るにはuvが関係します。画像textureの読み込みとMaterialへの設定はThree.jsでtextureを設定する方法で分離して解説しています。

BufferGeometryで三角形を自作する

Meshの面は最終的に三角形として描かれます。3頂点のpositionを用意すれば、最小の独自Geometryを作れます。

const positions = new Float32Array([
  -1, -1, 0,
   1, -1, 0,
   0,  1, 0,
]);

const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
  "position",
  new THREE.BufferAttribute(positions, 3),
);
geometry.computeVertexNormals();

const material = new THREE.MeshStandardMaterial({
  color: 0x38bdf8,
  side: THREE.DoubleSide,
});

const triangle = new THREE.Mesh(geometry, material);
scene.add(triangle);

BufferAttribute(positions, 3)の3は、1頂点をx・y・zの3値で表すという意味です。表裏を確認しやすい例としてDoubleSideを使っていますが、閉じた立体で常に両面描画すると描画costが増えます。必要な面方向を揃えてFrontSideを使うのが基本です。

indexで頂点を共有する

正方形は2つの三角形で構成できます。indexを使うと4頂点を再利用できます。

const positions = new Float32Array([
  -1, -1, 0, // 0
   1, -1, 0, // 1
   1,  1, 0, // 2
  -1,  1, 0, // 3
]);

const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setIndex([
  0, 1, 2,
  2, 3, 0,
]);
geometry.computeVertexNormals();

ただし、同じ位置でも面ごとにnormalやuvが異なる頂点は共有できません。「座標が同じなら必ず同じ頂点」ではなく、position・normal・uvなどの組み合わせが1頂点です。

生成済みGeometryの頂点を変更する

頂点座標はposition attributeから変更します。変更後はneedsUpdateをtrueにして、GPU側のbufferを更新する必要があります。

const geometry = new THREE.PlaneGeometry(4, 4, 20, 20);
const position = geometry.getAttribute("position");

position.setUsage(THREE.DynamicDrawUsage);

for (let i = 0; i < position.count; i += 1) {
  const x = position.getX(i);
  const y = position.getY(i);
  const z = Math.sin(x * 2) * Math.cos(y * 2) * 0.2;

  position.setZ(i, z);
}

position.needsUpdate = true;
geometry.computeVertexNormals();
geometry.computeBoundingBox();
geometry.computeBoundingSphere();

DynamicDrawUsageは頻繁に更新する予定であることをrendererへ伝えるhintです。最初のrenderより前に設定します。

法線とbounding volumeを再計算する

頂点の形が変わると、lightingに使うnormalも古くなる場合があります。陰影が不自然ならcomputeVertexNormals()を実行します。

bounding boxやbounding sphereを利用する処理がある場合も、頂点変更後に再計算します。これらはfrustum culling、raycast、helperなどの判定に影響します。

毎frame大きなGeometryの法線や境界を再計算すると重くなります。変形頻度・頂点数・必要な機能を見て、shaderでの変形や事前計算も検討します。

bufferの頂点数は途中で増減できない

TypedArrayは作成時に長さが決まります。既存BufferAttributeの内容は更新できますが、同じbufferを無制限に拡張することはできません。

頂点数が増える可能性があるlineなどでは、最大数を先に確保し、setDrawRange()で描画範囲を調整します。

const maxPoints = 500;
const positions = new Float32Array(maxPoints * 3);
const geometry = new THREE.BufferGeometry();

geometry.setAttribute(
  "position",
  new THREE.BufferAttribute(positions, 3),
);

geometry.setDrawRange(0, 2);

// 点を追加したあと、描画する頂点数だけ増やす
geometry.setDrawRange(0, currentPointCount);

最大数を予測できない、attribute構成自体が変わる場合は、新しいBufferGeometryを作るほうが明確です。

幅・高さ・segment数を変更する

組み込みGeometryのparametersは、constructorへ渡した値の記録です。次のように変更しても、すでに作られた頂点は更新されません。

// 形は変わらない
mesh.geometry.parameters.width = 5;

幅・半径・segment数を変える場合は、新しいGeometryへ交換し、不要になった古いGeometryを破棄します。

function resizeBox(mesh, width, height, depth) {
  const oldGeometry = mesh.geometry;

  mesh.geometry = new THREE.BoxGeometry(width, height, depth);
  oldGeometry.dispose();
}

resizeBox(boxMesh, 3, 1.5, 1);

単に全体を2倍に見せたいだけなら、Geometryを再生成せずmesh.scale.set(2, 2, 2)を使えます。Geometry自体の寸法を変えたいか、Object3Dのtransformで見た目を変えたいかを分けて考えます。

Geometryを移動・回転・中央揃えする

BufferGeometryにはtranslate()rotateX()scale()など、頂点へ変換を焼き込むmethodがあります。

geometry.translate(0, 1, 0);
geometry.rotateX(-Math.PI / 2);
geometry.scale(2, 1, 1);
geometry.center();

一方、通常のanimationや配置ではMeshのtransformを使います。

変更先 適する用途
mesh.position / rotation / scale Scene内での配置、animation、同じGeometryの共有
geometry.translate() / rotateX() / scale() pivot調整、import後の軸補正、頂点へ固定変換を適用

同じGeometryを複数Meshが共有している場合、Geometry側を変更するとすべてに影響します。1つだけ変形したいならMeshのtransformを使うか、Geometryをclone()してから変更します。

segment数とperformanceの考え方

SphereGeometryやPlaneGeometryのsegment数を増やすと、頂点と三角形が増えます。球は滑らかになりますが、GPUへ渡すdata量、頂点shader・fragment処理、memory使用量も増えます。

const low = new THREE.SphereGeometry(1, 12, 8);
const medium = new THREE.SphereGeometry(1, 32, 16);
const high = new THREE.SphereGeometry(1, 96, 64);

1個の球だけを見る場合と、1,000個表示する場合では適切な値が異なります。画面上の大きさ、数、端末性能、Material、shadowの有無を含めて測定します。

Planeは変形しない床や1枚画像なら2三角形で十分です。displacementや頂点変形を行うときだけ必要な分割数を追加します。

wireframeとEdgesGeometryの違い

三角形の分割をすべて見たい場合はMaterialのwireframe: trueが簡単です。

const material = new THREE.MeshBasicMaterial({
  color: 0x111827,
  wireframe: true,
});

物体の輪郭に近い線だけを表示したい場合はEdgesGeometryを作り、LineSegmentsで描画します。

const sourceGeometry = new THREE.BoxGeometry(2, 2, 2);
const edgeGeometry = new THREE.EdgesGeometry(sourceGeometry, 15);
const edgeMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
const edges = new THREE.LineSegments(edgeGeometry, edgeMaterial);

scene.add(edges);

第2引数thresholdAngleは、隣接面の法線角度が何度を超えたedgeを残すか指定します。WireframeGeometryもline segment用のGeometryであり、通常のMeshではなくLineSegmentsと組み合わせます。

Geometryを切り替える安全な実装

selectやbuttonで形を切り替える場合は、constructorを返すfactory関数を用意すると管理しやすくなります。

function createGeometry(type) {
  switch (type) {
    case "box":
      return new THREE.BoxGeometry(1.5, 1.5, 1.5);
    case "sphere":
      return new THREE.SphereGeometry(1, 32, 16);
    case "torus":
      return new THREE.TorusGeometry(0.8, 0.3, 16, 64);
    default:
      throw new Error(`Unknown geometry type: ${type}`);
  }
}

function replaceGeometry(mesh, type) {
  const nextGeometry = createGeometry(type);
  const previousGeometry = mesh.geometry;

  mesh.geometry = nextGeometry;
  previousGeometry.dispose();
}

先に次のGeometryを正常に作ってから交換しているため、unknown typeやconstructor errorで現在の表示を失いにくい構造です。

Geometryを共有しているMeshがある場合は、1つのMeshが勝手にdispose()しないよう所有関係を決めます。参照中のGeometryを破棄すると、次のrenderで再uploadが必要になったり表示管理が崩れたりします。

不要なGeometryをdisposeする

SceneからMeshをremoveしただけでは、Geometryが内部で確保したGPU resourceは自動解放されません。今後使わないGeometryは明示的に破棄します。

scene.remove(mesh);
mesh.geometry.dispose();
mesh.material.dispose();

Materialが配列の場合、各Materialを破棄します。textureはMaterialと別resourceなので、ほかで共有していないことを確認してtexture.dispose()します。

EdgesGeometryのように元Geometryから別Geometryを作った場合は、元と派生後の両方が不要になった時点でそれぞれdisposeします。

古いTHREE.Geometryコードを移行する

古い記事やsampleには、次のAPIが出てくることがあります。

// 現行Three.jsでは使わない古い例
geometry.vertices[0].x = 1;
geometry.verticesNeedUpdate = true;
geometry.faces[0].color.set(0xff0000);

Geometry classとFace3中心のAPIはcoreから削除されています。現在はBufferGeometryのattributeを取得し、setterとneedsUpdateを使います。

const position = geometry.getAttribute("position");
position.setX(0, 1);
position.needsUpdate = true;

古いBoxBufferGeometryなどの別名ではなく、現在はBoxGeometryを使います。古いprojectを更新するときは、一度に大きくversionを飛ばさず、Three.js公式Migration Guideでrelease間の破壊的変更を確認してください。

Geometryが表示されないときの確認項目

Geometryを作れたのに見えない場合は、次の順番で確認します。

  1. MeshまたはLineSegmentsをscene.add()したか
  2. Cameraの前にあり、near・farの範囲内か
  3. Materialが透明、黒、裏面だけになっていないか
  4. 光を必要とするMaterialなのにLightがない状態ではないか
  5. Planeなど1面Geometryの表裏がCameraと合っているか
  6. position attributeにNaNInfinityが入っていないか
  7. 頂点変更後にneedsUpdateを設定したか
  8. indexの順序と頂点数が正しいか
  9. resize後にCameraとRendererのsizeを更新しているか
  10. すでに共有Geometryをdisposeしていないか

形が黒い・陰影がおかしい

MeshStandardMaterialなどではLightが必要です。Lightを追加しても不自然ならnormalを確認します。頂点を変えた場合はcomputeVertexNormals()を実行します。

Lightの種類と配置はAmbientLight・DirectionalLight・PointLightの使い方で解説しています。

一部の面が消える

面の頂点順序が逆、またはCameraが裏側を見ている可能性があります。診断時だけside: THREE.DoubleSideへ切り替え、原因が分かったらFrontSideへ戻せるか検討します。

頂点を変えたのに表示が変わらない

変更しているGeometryが実際にMeshへ設定されているか、共有Geometryではないか、positionのneedsUpdateを設定したかを確認します。幅・segment数はparametersを書き換えず、Geometryを再生成します。

Geometry選択の実践チェックリスト

  • 基本形で足りるなら組み込みprimitiveを使う
  • 複雑な製品・人物・背景はBlenderなどからglTFで読み込む選択も検討する
  • 独自形状はBufferGeometryのposition・normal・uv・indexを設計する
  • 同じ形を多数置く場合はGeometryを共有し、必要ならInstancedMeshを検討する
  • segment数は画面上の品質と表示個数に合わせる
  • 頻繁な頂点更新ではDynamicDrawUsage、needsUpdate、再計算costを確認する
  • 形状交換時は古いGeometryの所有者を確認してdisposeする

まとめ

Three.jsのGeometryは、BoxやSphereなどの種類を選ぶだけでなく、その内部にあるBufferGeometryとattributeを理解すると扱いやすくなります。

基本形はconstructorの引数で生成し、Scene内の配置はMeshのposition・rotation・scaleで変更します。頂点を直接変える場合はposition attributeを更新してneedsUpdateを設定し、必要に応じてnormalとbounding volumeを再計算します。

幅・半径・segment数を変更するときは新しいGeometryへ交換し、不要なGPU resourceをdispose()します。この区別を押さえると、表示されない・変形が反映されない・切り替えるたびに重くなるといった問題を避けやすくなります。

スポンサーリンク