Babylon.jsのメッシュを分解して遊ぶよ

メッシュを構成している三角形を使って色々遊ぶぞ!という作業ログ
だいたいChatGPTに聞きながらやる。

メッシュの頂点とかはこれで取得できる
// 頂点データを取得
const positions = mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
const indices = mesh.getIndices();

特定のメッシュの色だけ変えようと思ったんだけど、頂点カラーだとグラデーションみたいになっちゃう。頂点共有してるからそうなるよね。
// 頂点カラー配列を作成
const colors = new Array(positions.length / 3 * 4).fill(1); // 初期は全て白 (RGBA)
// 色を変える三角形のインデックスを指定 (例: 1番目の三角形)
const targetTriangleIndex = 1;
// インデックス配列から三角形を構成する頂点を取得
const i1 = indices[targetTriangleIndex * 3];
const i2 = indices[targetTriangleIndex * 3 + 1];
const i3 = indices[targetTriangleIndex * 3 + 2];
// 頂点カラーを設定 (赤色に変更)
colors[i1 * 4] = 1; colors[i1 * 4 + 1] = 0; colors[i1 * 4 + 2] = 0; // 頂点1の色
colors[i2 * 4] = 1; colors[i2 * 4 + 1] = 0; colors[i2 * 4 + 2] = 0; // 頂点2の色
colors[i3 * 4] = 1; colors[i3 * 4 + 1] = 0; colors[i3 * 4 + 2] = 0; // 頂点3の色
// 頂点カラーをメッシュに適用
mesh.setVerticesData(BABYLON.VertexBuffer.ColorKind, colors);
// 頂点カラーを有効にするためのマテリアルを設定
const material = new BABYLON.StandardMaterial("material", scene);
material.vertexColorEnabled = true; // 頂点カラーを使用
mesh.material = material;

頂点情報から色を変える三角形を別のメッシュとして作成すると色が変わって見える。
純粋に色だけ変えられないかな?
// 三角形のインデックスを指定 (例: 1番目の三角形)
const targetTriangleIndex = 1;
const i1 = indices[targetTriangleIndex * 3];
const i2 = indices[targetTriangleIndex * 3 + 1];
const i3 = indices[targetTriangleIndex * 3 + 2];
// 三角形の頂点を取得
const trianglePositions = [
positions[i1 * 3], positions[i1 * 3 + 1], positions[i1 * 3 + 2], // 頂点1
positions[i2 * 3], positions[i2 * 3 + 1], positions[i2 * 3 + 2], // 頂点2
positions[i3 * 3], positions[i3 * 3 + 1], positions[i3 * 3 + 2], // 頂点3
];
// 新しいインデックスデータを作成 (単一の三角形なので [0, 1, 2])
const triangleIndices = [0, 1, 2];
// 三角形用の独立したメッシュを作成
const triangleMesh = new BABYLON.Mesh("triangle", scene);
const vertexData = new BABYLON.VertexData();
vertexData.positions = trianglePositions;
vertexData.indices = triangleIndices;
vertexData.applyToMesh(triangleMesh);
// 三角形用のマテリアル (例: 赤色)
const triangleMaterial = new BABYLON.StandardMaterial("triangleMaterial", scene);
triangleMaterial.diffuseColor = new BABYLON.Color3(1, 0, 0);
triangleMesh.material = triangleMaterial;

マルチマテリアルというものがあるらしい。これで指定した三角形に色を設定できる。
// 特定の三角形を別マテリアルにするため、サブメッシュを分ける準備
const targetTriangleIndex = 1; // 色を変えたい三角形のインデックス
const triangleStartIndex = targetTriangleIndex * 3; // 対象三角形の開始位置
// サブメッシュを作成
const subMesh1 = new BABYLON.SubMesh(0, 0, indices.length, 0, triangleStartIndex, mesh);
const subMesh2 = new BABYLON.SubMesh(1, 0, indices.length, triangleStartIndex, 3, mesh);
// マルチマテリアルを設定
const multiMaterial = new BABYLON.MultiMaterial("multiMaterial", scene);
// 白いマテリアル(デフォルト用)
const whiteMaterial = new BABYLON.StandardMaterial("whiteMaterial", scene);
whiteMaterial.diffuseColor = new BABYLON.Color3(1, 1, 1);
// 赤いマテリアル(特定の三角形用)
const redMaterial = new BABYLON.StandardMaterial("redMaterial", scene);
redMaterial.diffuseColor = new BABYLON.Color3(1, 0, 0);
// マルチマテリアルに追加
multiMaterial.subMaterials.push(whiteMaterial, redMaterial);
mesh.material = multiMaterial;

サブメッシュの指定はこういう感じだそうで。
new BABYLON.SubMesh(materialIndex, verticesStart, verticesCount, indexStart, indexCount, mesh);
-
materialIndex
: マルチマテリアル内の何番目のマテリアルを使用するか。 -
verticesStart
: 頂点データの開始インデックス(通常 0 で OK)。 -
verticesCount
: 頂点の総数(通常indices.length
)。 -
indexStart
: 操作対象のインデックスバッファの開始位置。 -
indexCount
: 操作対象のインデックス数(1つの三角形なら 3)。
materialIndex
は ↓multiMaterial.subMaterials
の リストの index らしい
multiMaterial.subMaterials.push(whiteMaterial, redMaterial, redMaterial);
mesh.material = multiMaterial;

各方法をChatGPTにまとめてもらった。
「頂点カラー」「サブメッシュ」「独立メッシュ」とついでに「カスタムシェーダー」。
-
リアルタイム性
- 頂点カラーやカスタムシェーダーはリアルタイム性が高い。
- サブメッシュや独立メッシュは、頻繁に色を変更する用途には向かない。
-
パフォーマンス
- メッシュ全体を1つのデータとして扱う「頂点カラー」や「カスタムシェーダー」は、高パフォーマンスを維持できる。
- 独立メッシュは、三角形数が増えると描画コールが増加し、パフォーマンスが低下する。
-
実装の手軽さ
- 独立メッシュは直感的で実装が簡単。
- カスタムシェーダーは柔軟性が高いが、シェーダーコードを書く必要がある。
-
管理の容易さ
- 頂点カラーは、非共有化が必要な場合は管理がやや煩雑。
- サブメッシュは、三角形単位の指定とマルチマテリアルの管理が必要。
用途に応じたおすすめの選択
用途 | おすすめ方法 | 理由 |
---|---|---|
小規模シーンで色を簡単に変更したい | 独立メッシュ | メッシュ数が少なく、直感的に操作可能。 |
中規模シーンで頻繁に色を変えたい | 頂点カラー | 描画コール数を最小化しつつ、動的な色変更が簡単。 |
色を変えた結果を可視化したい(動的変更なし) | サブメッシュ | 三角形単位で色を変更でき、変更頻度が少ない場合に管理が簡単。 |
大規模シーンかつリアルタイム変更 | カスタムシェーダー | 高パフォーマンスでリアルタイム性が高く、GPU上で効率よく色変更や複雑な制御が可能。 |
今回やりたいのは、色の動的変更なのでサブメッシュはなしかな。
ちょっとしたサンプルくらいであれば、三角形を独立して作るのが簡単そう。パフォーマンス気ににするなら 頂点カラー → カスタムシェーダー とするのが良さそう。

こう見るとシェーダー勉強したくなるよね?だってデメリットがコードが複雑になることだけだよ?

試しに頂点カラーでやるので頂点の非共有化してグラデーションにならないようにする。
頂点の非共有化はこれでできるよう。
/**
* 頂点を非共有化して新しいメッシュを返す関数
* @param {BABYLON.Mesh} inputMesh - 元のメッシュ
* @param {BABYLON.Scene} scene - Babylon.js のシーン
* @returns {BABYLON.Mesh} - 非共有化されたメッシュ
*/
function unshareVertices(inputMesh, scene) {
// 元の頂点データとインデックスを取得
const originalPositions = inputMesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
const originalIndices = inputMesh.getIndices();
// 新しいデータを格納する配列
const positions = [];
const indices = [];
const colors = [];
// 三角形ごとに独立した頂点を作成
for (let i = 0; i < originalIndices.length; i += 3) {
for (let j = 0; j < 3; j++) {
const index = originalIndices[i + j];
// 頂点位置を追加
positions.push(
originalPositions[index * 3],
originalPositions[index * 3 + 1],
originalPositions[index * 3 + 2]
);
// 頂点カラー(デフォルトで白に設定)
colors.push(1, 1, 1, 1); // RGBA
}
// 新しいインデックスを設定
indices.push(i, i + 1, i + 2);
}
// 新しいメッシュを作成
const newMesh = new BABYLON.Mesh("unsharedMesh", scene);
newMesh.setVerticesData(BABYLON.VertexBuffer.ColorKind, colors, true);
const vertexData = new BABYLON.VertexData();
vertexData.positions = positions;
vertexData.indices = indices;
vertexData.applyToMesh(newMesh);
return newMesh;
}

sphereでやるとちょっとおかしなことになったので修正。
// 小さな誤差を許容するための比較関数
const arePositionsEqual = (p1, p2, epsilon = 1e-6) => {
return (
Math.abs(p1[0] - p2[0]) < epsilon &&
Math.abs(p1[1] - p2[1]) < epsilon &&
Math.abs(p1[2] - p2[2]) < epsilon
);
};
/**
* 頂点を非共有化して新しいメッシュを返す関数
* @param {BABYLON.Mesh} inputMesh - 元のメッシュ
* @param {BABYLON.Scene} scene - Babylon.js のシーン
* @returns {BABYLON.Mesh} - 非共有化されたメッシュ
*/
function unshareVertices(inputMesh, scene) {
// 元の頂点データとインデックスを取得
const originalPositions = inputMesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
const originalIndices = inputMesh.getIndices();
// 新しいデータを格納する配列
const positions = [];
const indices = [];
const colors = [];
let triangleCount = 0; // 新しいインデックス用のカウンタ
// 三角形ごとに独立した頂点を作成
for (let i = 0; i < originalIndices.length; i += 3) {
// 各頂点の座標を取得
const index0 = originalIndices[i];
const index1 = originalIndices[i + 1];
const index2 = originalIndices[i + 2];
const vertex0 = [
originalPositions[index0 * 3],
originalPositions[index0 * 3 + 1],
originalPositions[index0 * 3 + 2],
];
const vertex1 = [
originalPositions[index1 * 3],
originalPositions[index1 * 3 + 1],
originalPositions[index1 * 3 + 2],
];
const vertex2 = [
originalPositions[index2 * 3],
originalPositions[index2 * 3 + 1],
originalPositions[index2 * 3 + 2],
];
// 同一頂点を共有している場合はスキップ
if (
arePositionsEqual(vertex0, vertex1) ||
arePositionsEqual(vertex1, vertex2) ||
arePositionsEqual(vertex2, vertex0)
) {
continue;
}
// 頂点ごとに独立したデータを作成
for (const vertex of [vertex0, vertex1, vertex2]) {
// 頂点位置を追加
positions.push(vertex[0], vertex[1], vertex[2]);
// 頂点カラー(デフォルトで白に設定)
colors.push(1, 1, 1, 1); // RGBA
}
// 新しいインデックスを設定
indices.push(triangleCount, triangleCount + 1, triangleCount + 2);
triangleCount += 3;
}
// 新しいメッシュを作成
const newMesh = new BABYLON.Mesh("unsharedMesh", scene);
newMesh.setVerticesData(BABYLON.VertexBuffer.ColorKind, colors, true);
const vertexData = new BABYLON.VertexData();
vertexData.positions = positions;
vertexData.indices = indices;
vertexData.applyToMesh(newMesh);
return newMesh;
}
なんでか取得された三角形が三角形になってなくて、以下みたいに同じ頂点指してるデータがあったんだよね
[
[ 0, 1, 0 ],
[ 0, 1, 0 ],
[ 0.8660253882408142, 0.5, 0 ]
]

指定の三角形の色変更行けた。
// ボックスメッシュを作成
const box = BABYLON.MeshBuilder.CreateBox("mesh", { size: 2 }, scene);
// 頂点を非共有化して新しいメッシュを生成
const unsharedBox = unshareVertices(box, scene);
box.dispose();
// 三角形単位で色を動的に変更する例
const colors = unsharedBox.getVerticesData(BABYLON.VertexBuffer.ColorKind);
function setTriangleColor(triangleIndex, color) {
const baseIndex = triangleIndex * 12; // RGBA * 3 頂点 = 12
for (let i = 0; i < 3; i++) {
colors[baseIndex + i * 4] = color.r; // R
colors[baseIndex + i * 4 + 1] = color.g; // G
colors[baseIndex + i * 4 + 2] = color.b; // B
colors[baseIndex + i * 4 + 3] = 1.0; // A
}
unsharedBox.updateVerticesData(BABYLON.VertexBuffer.ColorKind, colors);
}
// 特定の三角形を色変更
setTriangleColor(0, new BABYLON.Color3(1, 0, 0)); // 三角形0を赤色に
setTriangleColor(1, new BABYLON.Color3(0, 1, 0)); // 三角形1を緑色に
setTriangleColor(2, new BABYLON.Color3(0, 0, 1)); // 三角形2を青色に

ひとまずここまでのコード

クリックしたメッシュの三角形だけ色を変える
// クリックした三角形の色を変更するイベント
scene.onPointerObservable.add((pointerInfo) => {
if (pointerInfo.type === BABYLON.PointerEventTypes.POINTERPICK) {
const pickInfo = scene.pick(scene.pointerX, scene.pointerY);
// クリックされた三角形のインデックスを取得
const faceId = pickInfo.faceId; // 三角形インデックス
if (faceId !== undefined) {
setTriangleColor(faceId, new BABYLON.Color3(Math.random(), Math.random(), Math.random())); // ランダムな色に変更
}
}
});

クリックした三角形に隣接した三角形を塗りつぶすとかやりたい。
そんなわけでメッシュから隣接情報データを作る。
/**
* 隣接三角形情報を構築する関数
* @param {BABYLON.Mesh} mesh - メッシュ
* @returns {Object} - 隣接情報({三角形インデックス: [隣接三角形インデックス]} の形式)
*/
function buildAdjacencyList(mesh) {
const indices = mesh.getIndices();
const positions = mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
const adjacencyList = {};
// ヘルパー関数: 頂点座標を取得
function getVertexPosition(index) {
return [
positions[index * 3],
positions[index * 3 + 1],
positions[index * 3 + 2]
];
}
// 三角形ごとに隣接情報を計算
for (let i = 0; i < indices.length; i += 3) {
const triangle1 = [
getVertexPosition(indices[i]),
getVertexPosition(indices[i + 1]),
getVertexPosition(indices[i + 2])
];
// 隣接リストを初期化
const triangleIndex1 = i / 3;
if (!adjacencyList[triangleIndex1]) {
adjacencyList[triangleIndex1] = [];
}
// 他の三角形と比較して隣接を確認
for (let j = 0; j < indices.length; j += 3) {
if (i === j) continue; // 同じ三角形はスキップ
const triangle2 = [
getVertexPosition(indices[j]),
getVertexPosition(indices[j + 1]),
getVertexPosition(indices[j + 2])
];
// 共有する頂点数をカウント
let sharedVertexCount = 0;
for (const vertex1 of triangle1) {
for (const vertex2 of triangle2) {
if (
vertex1[0] === vertex2[0] &&
vertex1[1] === vertex2[1] &&
vertex1[2] === vertex2[2]
) {
sharedVertexCount++;
}
}
}
// 隣接条件: 2つの頂点を共有している場合
if (sharedVertexCount >= 2) {
const triangleIndex2 = j / 3;
if (!adjacencyList[triangleIndex1].includes(triangleIndex2)) {
adjacencyList[triangleIndex1].push(triangleIndex2);
}
}
}
}
return adjacencyList;
}
立方体を入れるとこういうデータになる。
const mesh = BABYLON.MeshBuilder.CreateBox("mesh", { size: 2 }, scene);
const unsharedMesh = unshareVertices(mesh, scene);
const adjacencyList = buildAdjacencyList(unsharedMesh);
console.log(adjacencyList)
// {
// "0": [1, 6, 11],
// "1": [0, 5, 9],
// "2": [3, 7, 8],
// "3": [2, 4, 10],
// "4": [3, 5, 10],
// "5": [1, 4, 9],
// "6": [0, 7, 11],
// "7": [2, 6, 8],
// "8": [2, 7, 9],
// "9": [1, 5, 8],
// "10": [3, 4, 11],
// "11": [0, 6, 10],
// }

幅優先探索で塗りつぶし。
// アニメーションで隣接三角形を塗りつぶす
function animateTriangleFilling(startFaceId) {
const visited = new Set();
const queue = [startFaceId];
function nextDepth() {
if (queue.length === 0) {
return;
}
for(let i = 0, size = queue.length; i < size; i++){
const currentFaceId = queue.shift();
if (visited.has(currentFaceId)) {
continue;
}
visited.add(currentFaceId);
// 現在の三角形を色変更
const color = new BABYLON.Color3(1, 0, 0);
setTriangleColor(currentFaceId, color);
// 隣接三角形をキューに追加
const neighbors = adjacencyList[currentFaceId];
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
queue.push(neighbor);
}
}
}
setTimeout(nextDepth, 100);
}
nextDepth();
}

わーい

gltfとかのモデル読み込んで、最初は色がないけど、クリックすることで色が戻ってくる、みたいなことしようと思ったメモ。

三角形が見えるようにワイヤーフレームを表示する。
ワイヤーフレーム設定すると面の色がなくなってしまうようなので、コピーして別メッシュを作成する必要がある。
// ワイヤーフレーム用のマテリアルを作成
const wireframeMesh = mesh.clone("wireframeMesh");
const wireframeMaterial = new BABYLON.StandardMaterial("wireframeMaterial", scene);
wireframeMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0); // 黒色のエッジ
wireframeMaterial.wireframe = true; // ワイヤーフレーム表示を有効化
wireframeMesh.material = wireframeMaterial;
wireframeMesh.isPickable = false; // クリック判定を無効化

幅優先探索したんだし、深さ優先探索したいよね、と思ったんだけど、こうやればいいらしい。
DFS自体はいいんだけど、同期非同期処理がやっぱりちょっと難しいね。
深さ優先の場合、非同期だと探索終了を待たずに次のノードに行ってしまうから、Promise で同期処理にして待機してないとアニメーションがうまくいかない。
function animateTriangleFillingDFS(startFaceId) {
const visited = new Set(); // 訪問済みの三角形を記録
function dfs(faceId) {
// 既に訪問済みならスキップ
if (visited.has(faceId)) {
return Promise.resolve();
}
// 現在の三角形を訪問済みに追加
visited.add(faceId);
// 現在の三角形を色変更(アニメーション的に塗りつぶし)
const a = 1;
const color = new BABYLON.Color3(...[0.06, 0.02, 0.04].map(v => v * a));
setTriangleColor(faceId, color);
// 50ms待機してから次の探索を開始
return new Promise((resolve) => {
setTimeout(() => {
// 隣接三角形を訪問
const neighbors = adjacencyList[faceId];
(async () => {
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
await dfs(neighbor); // 再帰呼び出しを待機
}
}
resolve(); // 全ての隣接を訪問後に解決
})();
}, 50);
});
}
// DFSを開始
dfs(startFaceId);
}

単純に待機するだけならこれでもいいのか。やっぱりPromiseあんまわかってないな。
await new Promise((resolve) => setTimeout(resolve, 50));

こうするとDFSでの戻る処理も見えて楽しい。
async function dfs(faceId) {
if (visited.has(faceId)) {
return;
}
// 現在の三角形を訪問済みとしてマーク
visited.add(faceId);
// 現在の三角形を色変更
setTriangleColor(faceId, visitColor);
// 50ms待機
await new Promise((resolve) => setTimeout(resolve, 50));
// 処理完了時に完了の色を適用
setTriangleColor(faceId, completeColor);
// 隣接三角形を探索
const neighbors = adjacencyList[faceId];
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
await dfs(neighbor); // 再帰的に訪問
}
}
// 戻り時に別の色を適用
setTriangleColor(faceId, backtrackColor);
// 50ms待機
await new Promise((resolve) => setTimeout(resolve, 50));
// 処理完了時に完了の色を適用
setTriangleColor(faceId, completeColor);
}

とりあえずここまで。しれっと初期表示の透過色も追加してる。
なんか隣接が若干おかしいけどいいでしょう。

面積の計算処理をしてみるなど

平面の面の面積を計算するぜ!ということで法線の角度計算
// 三角形の法線を計算
function calculateNormal(v0, v1, v2) {
const vector1 = v1.subtract(v0);
const vector2 = v2.subtract(v0);
return BABYLON.Vector3.Cross(vector1, vector2).normalize();
}
// ベクトル間の角度を計算
function calculateAngleBetweenVectors(normal1, normal2) {
let dotProduct = BABYLON.Vector3.Dot(normal1, normal2);
dotProduct = Math.max(-1, Math.min(1, dotProduct));
const radian = Math.acos(dotProduct);
return radian * (180 / Math.PI);
}

記事かきおわ。いつもはZennで作業してからQiitaで記事書いてるけど、今回は記事書きながらZennでメモってるせいで、スクラップがなんか変な感じになってるね。うーん。