この記事はBabylon.js Advent Calendar 2024の16日目の記事です。
はじめに
ああ、こんな世界...黒く、暗く、闇に染め上げてやる...!
光など不要。すべては虚無の彼方へ沈むべきなのだ。我の手により、混沌は完成する――!
そんなわけでBabylon.jsでメッシュを闇に染め上げます。
やみのまー
ふははは!どうだ!徐々に侵食されていくのは...!闇による真なる恐怖を知るがよい...!!
コード全体はここに。
やったこと
処理としては以下のことをやってます。
- メッシュの頂点の非共有化
- メッシュの三角形の隣接リスト作成
- クリックした場所から幅優先探索を行って塗りつぶし
以下、解説していきます。
メッシュの頂点の非共有化
今回、メッシュの色を徐々に変更するということをやりたかったので、メッシュの三角形ごとに色の変更をする必要がありました。そこで頂点カラーを指定することによって色の変更を行ったのですが、単純にやると以下のようにグラデーションになってしまいます。
上の画像は、左上の三角形の頂点カラーを赤色にしたものなのですが、右下の三角形がグラデーションになっています。これは左上と右下の三角形で頂点を共有しているためです。頂点カラーを変更すると、頂点を共有している他の三角形も色が変わってしまいます。
そこで頂点を共有しないように、以下のコードで頂点の非共有化の処理を行います。
やっていることとしては、頂点インデックスごとに頂点データを作り直して、そのデータからメッシュを作成しています。
/**
* 頂点を非共有化して新しいメッシュを返す関数
* @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 vertices = [];
for (let j = 0; j < 3; j++) {
const index = originalIndices[i + j];
const vertex = [
originalPositions[index * 3],
originalPositions[index * 3 + 1],
originalPositions[index * 3 + 2],
];
vertices.push(vertex);
}
// 頂点ごとに独立したデータを作成
for (const vertex of vertices) {
// 頂点位置を追加
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;
}
メッシュの三角形の隣接リスト作成
次に、どの三角形が隣り合っているのか、の情報を作成します。徐々に色を変更していく処理で、隣り合っている三角形から順に色を塗っていくので、それには三角形の隣接情報が必要になります。
コードとしては以下です。やっていることとしては、
- すべての三角形の組み合わせを見て、
- 頂点を2つ共有していたら隣接している、
という判断をしています。
/**
* 隣接三角形情報を構築する関数
* @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(arePositionsEqual(vertex1, vertex2)){
sharedVertexCount++;
}
}
}
// 隣接条件: 2つの頂点を共有している場合
if (sharedVertexCount >= 2) {
const triangleIndex2 = j / 3;
if (!adjacencyList[triangleIndex1].includes(triangleIndex2)) {
adjacencyList[triangleIndex1].push(triangleIndex2);
}
}
}
}
return adjacencyList;
}
ChatGPTに生成させてほぼいじってないので、処理効率がちょっと微妙なのですが、まあ良しとします。
クリックした場所から幅優先探索実行
最後に徐々に色を塗っていく処理を幅優先探索によって行います。
コードは以下です。隣接リストを使用して幅優先探索の処理を行い、キューから取り出したノードの色を変えています。また、幅優先探索の深さごとに色を更新処理するようにして、侵食感?が出るようにしています。
// アニメーションで隣接三角形を塗りつぶす
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(0.06, 0.02, 0.04);
setTriangleColor(currentFaceId, color);
// 隣接三角形をキューに追加
const neighbors = adjacencyList[currentFaceId];
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
queue.push(neighbor);
}
}
}
setTimeout(nextDepth, 50);
}
nextDepth();
}
クリック処理はこれです。
// クリックした三角形の色を変更するイベント
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) {
animateTriangleFilling(faceId); // アニメーション開始
}
}
});
まとめ
頂点カラーの変更 + 幅優先探索 でメッシュの色塗りをしました。
今回、コードはほぼChatGPTに生成させてちょこちょこ修正する感じで作ってます。アイデアがあれば簡単なコードはさくさくかけるので、いい時代になったなーと思ったり。
ちなみに冒頭のは...なんでかこうなりました。「色づく世界の明日から」っていうアニメからネタを思いついたのになんででしょうね?というか去年も冒頭のノリで記事書いてたような...?この時期になると中二病が発症する...!?