Skip to content

THREEjs BufferGeometry網格擴散

Billy

demo範例

https://stackblitz.com/edit/vitejs-vite-xafdoe?file=src%2Futils%2Ftool%2FBufferGeometryTool.js

頂點擴散原理

本質是通過模型上的一個頂點,找到周圍相鄰的點;其他點又能找到周圍相鄰的點,就像水波一樣擴散出去。

通常會框定一個封閉邊界來找出邊界內所有點;或者只擴散一點點來加大選定的範圍。 許多3D的算法,如裁切、干涉深度偵測等都會用到。

實現網格擴散

建構頂點與鄰居點的資料

由於BufferGeometry無法直接獲得頂點周圍的點,所以需要先建構頂點與鄰居點的資料。

const floatView = new Float32Array(3);
const intView = new Uint32Array(floatView.buffer);
/**
 * 為每個不同頂點,計算唯一的hash key
 * 該函式尚有碰撞概率 
 */
const hashPoint = (x, y, z) => {
    let hash = 2166136261;
    floatView[0] = x === 0 ? 0 : x;
    floatView[1] = y === 0 ? 0 : y;
    floatView[2] = z === 0 ? 0 : z;
    hash = Math.imul(hash ^ intView[0], 16777619);
    hash = Math.imul(hash ^ intView[1], 16777619);
    hash = Math.imul(hash ^ intView[2], 16777619);
    return hash >>> 0;
}

/**
 * @typedef PointData
 * @property {number[]} PointData.triIndices
 * @property {number} PointData.firstPosIndex
 * @property {Set<number>} [PointData.posIndices]
 * @property {Set<number>} [PointData.neighborhashPos]
 * 
 * @typedef {Map<number, PointData>} HashPointMap
 */

/**
 * @param {BufferGeometry} geometry 
 * @param {Object} options 
 * @param {boolean} options.needPosIndices
 * @param {boolean} options.needNeighborPosIndices
 * @returns {HashPointMap}
 */
const buildHashPointMap = (geometry, options = {}) => {
    const {
        needPosIndices,
        needNeighborPosIndices,
    } = Object.assign({
        needPosIndices: false,
        needNeighborPosIndices: false,
    }, options);

    let index = geometry.getIndex();
    if (!index) {
        geometry.computeBoundsTree();
        index = geometry.getIndex();
    }
    const indexArray = index.array;
    const posArray = geometry.getAttribute('position').array;
    const triHashPointArray = new Uint32Array(3);
    /**兩個一組,分別代表3個索引的鄰居索引,0的鄰居是1和2,1的是0和2 */
    const neighborOffset = [1, 2, 0, 2, 0, 1];

    /**@type {HashPointMap} */
    const pointMap = new Map();
    const triCount = indexArray.length / 3;
    for (let triIndex = 0; triIndex < triCount; triIndex++) {
        const triFlatIndex = triIndex * 3;

        const aPosFlatIndex = indexArray[triFlatIndex] * 3;
        const bPosFlatIndex = indexArray[triFlatIndex + 1] * 3;
        const cPosFlatIndex = indexArray[triFlatIndex + 2] * 3;
        triHashPointArray[0] = hashPoint(posArray[aPosFlatIndex], posArray[aPosFlatIndex + 1], posArray[aPosFlatIndex + 2]);
        triHashPointArray[1] = hashPoint(posArray[bPosFlatIndex], posArray[bPosFlatIndex + 1], posArray[bPosFlatIndex + 2]);
        triHashPointArray[2] = hashPoint(posArray[cPosFlatIndex], posArray[cPosFlatIndex + 1], posArray[cPosFlatIndex + 2]);

        for (let offset = 0; offset < 3; offset++) {
            const posIndex = indexArray[triFlatIndex + offset];
            const key = triHashPointArray[offset];
            const flatOffset = offset * 2;

            const pointData = pointMap.get(key);
            if (!pointData) {
                const pointData = {};
                pointData.firstPosIndex = posIndex;
                pointData.triIndices = [triIndex];
                if (needPosIndices) pointData.posIndices = new Set().add(posIndex);
                if (needNeighborPosIndices) {
                    const neighborhashPos = new Set(triHashPointArray);
                    neighborhashPos.add(triHashPointArray[neighborOffset[flatOffset]]);
                    neighborhashPos.add(triHashPointArray[neighborOffset[flatOffset + 1]]);
                    pointData.neighborhashPos = neighborhashPos
                }

                pointMap.set(key, pointData);
                continue;
            }

            pointData.triIndices.push(triIndex);
            if (needPosIndices) pointData.posIndices.add(posIndex);
            if (needNeighborPosIndices) {
                const flatOffset = offset * 2;
                pointData.neighborhashPos.add(triHashPointArray[neighborOffset[flatOffset]]);
                pointData.neighborhashPos.add(triHashPointArray[neighborOffset[flatOffset + 1]]);
            }
        }
    }

    return pointMap;
}

擴散指定層數的頂點

/**
 * @param {HashPointMap} hashPointMap 拓撲資訊
 * @param {Set<number>} hashPointKeySet 所有種子點
 * @param {number} nRing 擴散次數
 */
const hashPosKey2Neihbors = (hashPointMap, hashPointKeySet, nRing) => {
    /**發現的所有點 */
    const checkedKeySet = new Set();
    /**當前層的種子點 */
    let currentLayer = new Set(hashPointKeySet);
    /**下一層的邊緣點 */
    let nextLayer = new Set();

    for (let i = 0; i < nRing; i++) {
        for (const pointKey of currentLayer) {
            checkedKeySet.add(pointKey);

            const pointData = hashPointMap.get(pointKey);
            if (!pointData) continue;

            const { neighborhashPos } = pointData;
            for (const neighborKey of neighborhashPos) {
                if (checkedKeySet.has(neighborKey)) continue;
                nextLayer.add(neighborKey);
            }
        }

        if (nextLayer.size === 0) break;

        const temp = currentLayer;
        currentLayer = nextLayer;
        nextLayer = temp;
        nextLayer.clear();
    }

    return checkedKeySet;
}

條件擴散頂點

/**
 * @param {HashPointMap} hashPointMap 
 * @param {Set<string>} seedPointKeySet 所有種子點
 * @param {(seedPointKey: number, neighborPointKey: number)=>{}} ifSpreadJudgeFunc 擴散判斷函式,為true則繼續擴散
 * @return {Set<number>} 所有擴散到的點
 */
const hashPointSpread = (hashPointMap, seedPointKeySet, ifSpreadJudgeFunc = () => true) => {
    const checkedKeySet = new Set();
    const seedData = {
        /**當前層的種子點 */
        currentLayer: new Set(seedPointKeySet),
        /**下一層的邊緣點 */
        nextLayer: new Set(),
    };

    while (seedData.currentLayer.size > 0) {
        seedData.nextLayer.clear();

        for (const pointKey of seedData.currentLayer) {
            checkedKeySet.add(pointKey);
            const neighborKeySet = hashPointMap.get(pointKey).neighborhashPos;

            for (const neighborKey of neighborKeySet) {
                if (checkedKeySet.has(neighborKey)) continue;
                if (!ifSpreadJudgeFunc(pointKey, neighborKey)) continue;

                seedData.nextLayer.add(neighborKey);
            }
        }

        const temp = seedData.currentLayer;
        seedData.currentLayer = seedData.nextLayer;
        seedData.nextLayer = temp;
    }

    return checkedKeySet;
}

參考資料

https://stackoverflow.com/questions/59561323/threejs-find-neighbor-faces-in-planebuffergeometry

Anterior
JS宣告物件、陣列技巧
下列的
javascript中,一段被try...catch包覆,卻還會崩潰的程式