昨天我們學會了頂點數據的動態更新,通過動態創建和局部更新頂點屬性,來優化了渲染效能。接下來就讓我們結合排序演算法,實作三維場景中的動畫吧!
建議先看過這兩篇文章:
我們需要結合過去所學,將動態頂點更新、響應式數據以及排序演算法動畫相結合,並提供豐富的圖形變化和參數控制。具體來說,我們需要實現以下幾點:
讓我們一起來看一下具體如何實作這個三維長條圖動畫,並逐步解析其中的技術細節。以下是一個展示影片,能夠幫助你快速了解最終的效果:
首先,我們需要建立一個長條圖物件 column,裡面包含了 geometryData(響應式數據)、geometry 和 mesh,我們會將 geometryData 提供給排序演算法操作,然後動態修改 geometry 中的頂點數據:
// 餅就是要畫大一點!興許未來哪天就做三維的粒子系統
class ParticleSystem3D{
constructor(){
this.sort = new SortAlgorithm();
this.mesh = new THREE.Group();
this.column = this.createColumn(length, maxHeight, radius, depth);
this.mesh.add(this.column.mesh);
}
getMesh(){
return this.mesh;
}
update(){
this.#transitionRadian+= this.#trasitionOmega;
this.sort.update(this.column.geometryData);
}
}
接著,再將 mesh 加入到前幾天我們所使用的 Three.js 場景,這樣每次渲染更新時,排序演算法的結果就會在畫布上:
this.system = new ParticleSystem3D();
this.scene.add(this.system.getMesh());
這裡稍微複習一下 BufferGeometry,和前幾篇相同,由於我們需要頻繁修改頂數據,因此我們初始化先提供空值,後續再針對每個柱體將頂點數據覆蓋到相應的緩衝區:
// 餅就是要畫大一點!興許未來哪天就做三維的粒子系統
class ParticleSystem3D{
constructor(){
this.sort = new SortAlgorithm();
}
createColumn(length, maxHeight, radius, depth){
const column = {length, maxHeight, radius, depth};
const attribute = new THREE.BufferAttribute(new Float32Array(length * 36 * 3), 3);
const colorAttribute = new THREE.BufferAttribute(new Float32Array(length * 36 * 3), 3);
column.geometry = new THREE.BufferGeometry();
column.geometry.setAttribute('position', attribute);
column.geometry.setAttribute('color', colorAttribute);
const material = new THREE.MeshBasicMaterial({ 'vertexColors': true });
column.mesh = new THREE.Mesh( column.geometry, material );
// ......下略
}
}
我們用四個基本資訊來建立我們的長條圖,包含:
雖然我們提供給 Three 的頂點數據是緊密排列的,針對當中的每一個柱體,我們仍需要儲存關於它們的信息,除了整個圖形的長度、半徑、深度,還需要考慮單個柱體的高度、相對座標、更新邏輯,方便我們控制它的動畫:
createColumn(length, maxHeight, radius, depth){
const column = {length, maxHeight, radius, depth};
// ......上略
column.updateVertices = (index) => {}
column.geometryData = new Array(length).fill().map((v, index) => {
return this.createGeometryData(column, index);
});
column.geometryData.forEach((data, index) => {
column.updateVertices(index);
})
}
如果你還記得上個主題的排序演算法,應該對這個地方不陌生,我們會提供四筆資料,分別是三個變數和一個動畫物件 path。當初在做二維的排序演算法時,我們並沒有引入響應式數據的概念,而是直接用高度來進行排序,因此沿用。
createGeometryData(column, index){
const {length, radius, unitHeight} = column;
const angle = (index / length) * Math.PI * 2;
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
const height = index * unitHeight;
const path = new Path(x, y);
const geometryData = {x, y, height, path};
}
我們先從一個簡單的問題切入,unitHeight 顧名思義就是每單位高度(m/unit),所以我們可以這樣計算它:
const unitHeight = maxHeight / length;
但是問題來了,如果使用者想要修改長條圖陣列的長度或高度,那這個值並不會變化,而是被定死了!為了解決這個問題,我們可以透過 getter 來設定這個數值讀取方法:
Object.defineProperty(column, 'unitHeight', {
get() {
return column.maxHeight / column.length;
}
});
這樣我們就有一個很基本的響應式數據了,不過要注意到的是,我們沒有設定 setter,相當於是一個空函式。也就是說當你嘗試賦值給 unitHeight,什麼都不會發生。這也是我所希望的,讓這個值動態更新的同時,只讀不寫。
那麼回到主軸,排序演算法的核心是用交換兩個柱體,並為 path 動畫設置目標點:
class SortAlgorithm{
static swapColumn(a, b, frame){
[a.path.pointX, b.path.pointX] = [b.path.pointX, a.path.pointX];
[a.path.pointY, b.path.pointY] = [b.path.pointY, a.path.pointY];
[a.height, b.height] = [b.height, a.height];
a.path.NewTarget(a.x, a.y, frame);
b.path.NewTarget(b.x, b.y, frame);
}
}
因此我們可以監控 pointX 和 pointY,但這麼做會有一點問題,我們需要避免重複更新頂點數據(浪費資源),所以需要進行狀態管理和延遲:
createGeometryData(column, index){
// ......
const path = new Path(x, y);
const geometryData = {x, y, height, path};
let _pointX = path.pointX;
let _pointY = path.pointY;
let isUpdating = false;
Object.defineProperty(path, 'pointX', {
get() {
return _pointX;
},
set(newX) {
if (_pointX !== newX) {
_pointX = newX;
if (isUpdating) return; // 如果正在更新,則直接返回
isUpdating = true; // 設置為正在更新
requestAnimationFrame(() => {
column.update(index, path);
isUpdating = false; // 更新結束
});
}
}
});
Object.defineProperty(path, 'pointY', {
get() {
return _pointY;
},
set(newY) {
if (_pointY !== newY) {
_pointY = newY;
if (isUpdating) return; // 如果正在更新,則直接返回
isUpdating = true; // 設置為正在更新
requestAnimationFrame(() => {
column.updateVertices(index);
isUpdating = false; // 更新結束
});
}
}
});
}
功能確實是實現了,但是...好像有點複雜哎,讓我們重新思考這個問題然後簡化它。
說到底,動畫的切入點是什麼?的確,座標被更新,就需要移動,但是還記得嗎?在系列文 A2 我們有設計一個倒數計時器呀!
所以接下來,我們把邏輯改成,每次倒數計時更新頂點數據(動畫執行時):
createGeometryData(column, index){
const path = new Path(x, y);
const geometryData = {x, y, height, path};
// 初始化內部變數
let _timer = path.timer;
Object.defineProperty(path, 'timer', {
get() {
return _timer;
},
set(newT) {
_timer = newT;
column.updateVertices(index);
}
});
}
是不是簡潔了許多呢!
在這篇文章中,我們探索了如何使用響應式數據來實現動畫效果,特別是針對排序演算法的動態柱體顯示。
最初,我們通過 Object.defineProperty 設置動態的 unitHeight,讓長條圖的高度與陣列長度保持同步變動。這為我們引入了基本的響應式數據機制。接下來,我們討論了在動畫過程中,如何讓柱體的 pointX 和 pointY 坐標進行動態更新。這涉及到如何避免重複更新頂點數據,並透過狀態管理來確保只有必要時才執行視覺變化,以優化性能。
當我們重新審視動畫的核心需求,發現可以利用倒數計時器的更新來進行簡化。通過每次動畫執行時更新數據,我們得以將邏輯簡化為較為乾淨的設計。那麼,明天我們將繼續完成後續步驟。