3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Babylon.jsAdvent Calendar 2024

Day 16

【Babylon.js】メッシュを闇に染め上げる...!

Last updated at Posted at 2024-12-15

この記事はBabylon.js Advent Calendar 2024の16日目の記事です。

はじめに

ああ、こんな世界...黒く、暗く、闇に染め上げてやる...!
光など不要。すべては虚無の彼方へ沈むべきなのだ。我の手により、混沌は完成する――!

そんなわけでBabylon.jsでメッシュを闇に染め上げます。

やみのまー

ふははは!どうだ!徐々に侵食されていくのは...!闇による真なる恐怖を知るがよい...!!

コード全体はここに。

やったこと

処理としては以下のことをやってます。

  1. メッシュの頂点の非共有化
  2. メッシュの三角形の隣接リスト作成
  3. クリックした場所から幅優先探索を行って塗りつぶし

以下、解説していきます。

メッシュの頂点の非共有化

今回、メッシュの色を徐々に変更するということをやりたかったので、メッシュの三角形ごとに色の変更をする必要がありました。そこで頂点カラーを指定することによって色の変更を行ったのですが、単純にやると以下のようにグラデーションになってしまいます。

スクリーンショット 2024-12-15 085114.png

上の画像は、左上の三角形の頂点カラーを赤色にしたものなのですが、右下の三角形がグラデーションになっています。これは左上と右下の三角形で頂点を共有しているためです。頂点カラーを変更すると、頂点を共有している他の三角形も色が変わってしまいます。

そこで頂点を共有しないように、以下のコードで頂点の非共有化の処理を行います。
やっていることとしては、頂点インデックスごとに頂点データを作り直して、そのデータからメッシュを作成しています。

/**
 * 頂点を非共有化して新しいメッシュを返す関数
 * @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に生成させてちょこちょこ修正する感じで作ってます。アイデアがあれば簡単なコードはさくさくかけるので、いい時代になったなーと思ったり。


ちなみに冒頭のは...なんでかこうなりました。「色づく世界の明日から」っていうアニメからネタを思いついたのになんででしょうね?というか去年も冒頭のノリで記事書いてたような...?この時期になると中二病が発症する...!?

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?