昨天我們透過計算向量和頂點,完成了 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);
}
}
雖然,在這個場景下,我們的需求是完全更新所有的頻譜資訊,這樣做是合理的。也要考慮到,在其他場景下我們只想更新部分數據,以此節省開銷,例如,在做排序演算法時,只需交換部分數據,而不必重新更新所有的長方體數據。
針對這種需求,我們可以採取動態更新的策略,只更新幾何體中需要變動的頂點屬性,同時利用 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;
}
讓我們回顧整個架構,當聲音轉換工廠在初始化時,會準備 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;
}
這裡展示了如何僅修改一個長方體的數據,並告訴 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];
}
首先,將長條圖 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) };
顏色的生成策略延續了座標的編號邏輯,同樣利用三角函數 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) }; // 下面右側
最後一步也很關鍵,必須正確排列每一面的頂點,每個長方體的 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 頂點數據的動態更新中進行效能優化,特別是針對音訊頻譜可視化這樣的場景。通過動態創建和局部更新頂點屬性,我們成功優化了渲染效能,雖然初步測試的幀數提升不明顯,但這種方法在未來結合排序演算法處理時,將顯示出更大的優勢。