iT邦幫忙

2024 iThome 鐵人賽

DAY 27
1

引言

昨天我們透過計算向量和頂點,完成了 3D 的頻譜圖形,接下來我們可以嘗試結合主題 C 的排序演算法,把 2D 的視覺化帶到 3D 場景。不過,在那之前,讓我們先回過頭來檢視目前的實作,找找看哪裡可以進行效能優化,特別是針對頂點數據的更新部分,這對於處理大量的 3D 物件會非常重要。

建議先看過前兩篇文章:

頂點屬性

先前在處理音訊資料時,我們每一幀都需要更新大量的頂點數據,大約有三萬個頂點座標,在這樣的情況下,我的電腦每秒大約可以完成 33 幀渲染:

class BufferFactory{
    transformData(data){
        // 循環陣列
        const factory = this.#factorys.shift();
        this.#factorys.push(factory);
        // 計算頂點座標
        const vector = this.getPosition(data);
        const vertices = this.getVertices(vector);
        const attribute = new THREE.BufferAttribute(vertices, 3);
        factory.geometry.setAttribute('position', attribute);
        // 計算頂點顏色
        const colorVertices = this.getColorVertices(data, vertices);
        const colorAttribute = new THREE.BufferAttribute(colorVertices, 3)
        factory.geometry.setAttribute('color', colorAttribute);
    }
}
  • THREE.BufferAttribute:由於我們每次都呼叫了這個方法來重新創建數據,這會是一個潛在的效能問題。

雖然,在這個場景下,我們的需求是完全更新所有的頻譜資訊,這樣做是合理的。也要考慮到,在其他場景下我們只想更新部分數據,以此節省開銷,例如,在做排序演算法時,只需交換部分數據,而不必重新更新所有的長方體數據。

動態更新

針對這種需求,我們可以採取動態更新的策略,只更新幾何體中需要變動的頂點屬性,同時利用 setAttribute 來動態創建。

transformData(data){
    const factory = this.#factorys.shift();
    this.#factorys.push(factory);
    this.setAttribute(factory, data.length);

    const vector = this.getPosition(data);
    const vertices = this.getVertices(vector);
    
    factory.geometry.attributes.position.needsUpdate = true;
    factory.geometry.attributes.position.array = vertices;

    const colorVertices = this.getColorVertices(data, vertices);

    factory.geometry.attributes.color.needsUpdate = true;
    factory.geometry.attributes.color.array = colorVertices;
}
  • attributes.position.array:儲存頂點座標的數據陣列
  • attributes.position.needsUpdate:告訴 Three 這些頂點數據已被修改,需要更新。要注意的是,當幾何體更新完畢後,這個值會被修改回 false。因此,我們需要在每次修改數據時,手動設置更新提醒。

為什麼要動態創建屬性?

讓我們回顧整個架構,當聲音轉換工廠在初始化時,會準備 360 組長條圖的實體 mesh。如果我們在初始化時,為每組長條圖指派它們所需的頂點屬性,就會造成非常大的負擔。那將是近百萬筆的頂點座標,一口氣初始化並不現實。

class BufferFactory{
    constructor(){
        this.#material = new THREE.MeshBasicMaterial({
            vertexColors: true,
        });
        this.mesh = new THREE.Group();
        this.#factorys = new Array(360).filzl(0).map(()=>this.createFactory());
    }
    createFactory(){
        const factory = {
            'geometry': new THREE.BufferGeometry(),
            'mesh': null,
            'needSet': true
        };
        factory.mesh = new THREE.Mesh( factory.geometry, this.#material );
        this.mesh.add(factory.mesh);
        return factory;
    }
}

因此我們用 needSet 進行初始化的狀態管理,在轉換音訊之前,先進行初始化檢查,並動態建立空的頂點屬性:

setAttribute(factory, length){
    if(factory.needSet != true) return

    const attribute = new THREE.BufferAttribute(new Float32Array(length * 36 * 3), 3);
    const attribute2 = new THREE.BufferAttribute(new Float32Array(length * 36 * 3), 3);
    factory.geometry.setAttribute('position', attribute);
    factory.geometry.setAttribute('color', attribute2);

    factory.needSet = false;
}
  • 這裡有個小坑,雖然初始化時是賦予空值,但是修改時仍會參照該屬性,因此位置和顏色必須分成兩個獨立屬性設置,不能共用。

這樣做的好處是,我們避免了在初始化階段生成過多不必要的數據,僅在需要時才創建屬性。雖然初始化時屬性是空的,但它仍然可以在後續被更新時參照並修改。

如此一來,效能就會得到改善了,我測試發現我的電腦現在可以達到每秒 34 幀,較之前多了一幀,這證明了這樣做的效益。

覆蓋部分數據

好吧,老實說!這變化並不顯著,畢竟我開頭也提到了,這個場景下不需要動態更新數據,這只是試試水溫,先從前兩天大家熟悉的模式,一步一腳印學習。

現在讓我們只修改一個長方體的頂點數據,這將進一步展示 BufferGeometry 的效能優勢。注意到每個長方體有 36 個頂點,每個頂點有 3 個座標,更新一個長方體的頂點的時候,架構上可以這麼寫:

updateColumn = (index, height) => {
    const cubeVertexCount = 36;
    const vertexIndex = index * cubeVertexCount * 3;

    const vertices = column.geometry.attributes.position.array;
    const color = column.geometry.attributes.color.array;

    const newVertices = this.getColumnVertices(); // 108 個座標
    const colorVertices = this.getColorVertices(); // 108 個顏色

    for(let N = 0; N < cubeVertexCount * 3; N ++){
        vertices[vertexIndex + N] = newVertices[N];
    }
    for(let N = 0; N < cubeVertexCount * 3; N ++){
        color[vertexIndex + N] = colorVertices[N];
    }

    column.geometry.attributes.position.needsUpdate = true;
    column.geometry.attributes.color.needsUpdate = true;
}
  • getColumnVertices:等明天做排序演算法的時候,我們再來實作頂點的轉換

這裡展示了如何僅修改一個長方體的數據,並告訴 BufferGeometry 將該部分數據標記為需要更新,從而在處理大量數據時僅更新必要的部分。

優化頂點座標和頂點顏色

原先我們以 FP 的原則解耦設計函式,是為了保留靈活性,能夠繪製其他多邊形和形狀。

transformData(data){
    //......
    const vector = this.getPosition(data);
    const vertices = this.getVertices(vector);
    const colorVertices = this.getColorVertices(data, vertices);
}

如果暫不考慮其他多邊形和形狀,我們可以將長條圖的頂點座標、顏色集中處理,這樣不僅可以減少效能的負擔,還能夠簡化資料的生成流程。

在接下來這段程式碼中,我們將用單一函式 getColumnVertices 簡化流程,透過更直接的邏輯分配頂點和顏色,並且使用預先定義好的三角形索引值來排列頂點:

transformData(data){
    //......
    const [vertices, colorVertices] = this.getColumnVertices(data)
}
getColumnVertices(data, width = 2, depth = 2){
    const vertices = new Float32Array(data.length * 36 * 3);
    const colorVertices = new Float32Array(data.length * 36 * 3);
    let i = 0, j = 0;
    for(let N = 0; N < data.length; N++){
        const height = data[N];
        // 1. 頂點座標的準備
        // 2. 頂點顏色的準備
        // 3. 三角形片段索引值
    }
    return [vertices, colorVertices];
}

1. 頂點座標的準備

首先,將長條圖 8 個頂點位置分別定義,並使用編號 1 到 8 來對應每個頂點。這樣做的好處是,我們可以分開管理座標的生成邏輯和點的排列方法。

const push = (x, y, z) => {
    vertices[i++] = x;
    vertices[i++] = y;
    vertices[i++] = z;
}
const addPoint = {};
const addPoint[1] = () => { push(width * N    , 0     , depth) };
const addPoint[2] = () => { push(width * (N+1), 0     , depth) };
const addPoint[3] = () => { push(width * (N+1), height, depth) };
const addPoint[4] = () => { push(width * N    , height, depth) };
const addPoint[5] = () => { push(width * N    , 0     , 0) };
const addPoint[6] = () => { push(width * N    , height, 0) };
const addPoint[7] = () => { push(width * (N+1), height, 0) };
const addPoint[8] = () => { push(width * (N+1), 0     , 0) };

2. 頂點顏色的準備

顏色的生成策略延續了座標的編號邏輯,同樣利用三角函數 Math.sin 來製造顏色的過渡效果。相對於原始版本(利用座標來判斷上下左右),更加直覺和簡潔:

const pushBRG = (x, y, z) => {
    colorVertices[j++] = x;
    colorVertices[j++] = y;
    colorVertices[j++] = z;
}
const tran = Math.sin(this.#transitionRadian);
// 計算右側顏色
const r1 = 0.200 + height / 255 * (0.816 - 0.200);
const g1 = 0.329 + height / 255 * (0.590 - 0.329) * tran;
const b1 = 0.584 + height / 255 * (0.949 - 0.584);
// 計算左側顏色
const r2 = 0.816;
const g2 = 0.590 * tran;
const b2 = 0.949;

const addColor = {};
addColor[1] = () => { pushBRG(b2, r2, g2) }; // 上面左側
addColor[2] = () => { pushBRG(b1, r1, g1) }; // 上面右側
addColor[3] = () => { pushBRG(b1, r1, g1) }; // 上面右側外側
addColor[4] = () => { pushBRG(b2, r2, g2) }; // 上面左側外側
addColor[5] = () => { pushBRG(b2, r2, g2) }; // 下面左側
addColor[6] = () => { pushBRG(b2, r2, g2) }; // 下面左側外側
addColor[7] = () => { pushBRG(b1, r1, g1) }; // 下面右側外側
addColor[8] = () => { pushBRG(b1, r1, g1) }; // 下面右側

3. 三角形片段索引值

最後一步也很關鍵,必須正確排列每一面的頂點,每個長方體的 6 個面都可以分為兩個三角形,每個三角形由 3 個頂點組成。索引值陣列定義了頂點的順序,這樣就能依照順序將頂點數據填入。

const indices = new Uint8Array([
    // 前面
    1, 2, 3,
    1, 3, 4,
    // 後面
    5, 6, 7,
    5, 7, 8,
    // 左面
    1, 4, 6,
    1, 6, 5,
    // 右面
    2, 8, 7,
    2, 7, 3,
    // 上面
    3, 7, 6,
    3, 6, 4,
    // 下面
    1, 5, 8,
    1, 8, 2
]);
indices.forEach((point) => {
    addPoint[point]();
    addColor[point]();
})

結論

本文主要探討了如何在 3D 頂點數據的動態更新中進行效能優化,特別是針對音訊頻譜可視化這樣的場景。通過動態創建和局部更新頂點屬性,我們成功優化了渲染效能,雖然初步測試的幀數提升不明顯,但這種方法在未來結合排序演算法處理時,將顯示出更大的優勢。


上一篇
D3 從數據到視覺:利用 BufferGeometry 生成 3D 長條圖(下)
下一篇
D5 讓排序演算法起舞吧!響應式數據驅動 BufferGeometry
系列文
讓演算法起舞:前端特效應用的探索之旅35
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言