Three.js?Interpolant實(shí)現(xiàn)動(dòng)畫(huà)插值
Interpolant
這個(gè)類主要是用來(lái)實(shí)現(xiàn)插值,常用于動(dòng)畫(huà)。
可以把這個(gè)類理解為是一個(gè)數(shù)學(xué)函數(shù),給定一個(gè)自變量,要返回對(duì)應(yīng)的函數(shù)值。只是,在我們定義函數(shù)的時(shí)候,是通過(guò)一些離散的點(diǎn)進(jìn)行定義的。
舉個(gè)例子,加入我們要定義y = x^2這條曲線,我們需要定義兩個(gè)數(shù)組(即采樣點(diǎn)和采樣的值):x = [-2, -1, 0, 1, 2]
,y = [4, 1 ,0, 1, 4]
。通過(guò)這樣的定義方式,我們?cè)趺辞蟛皇遣蓸狱c(diǎn)中的函數(shù)值?例如上面的吱吱,我們?cè)趺辞?code>x = 0.5時(shí)的值?這就時(shí)我們要說(shuō)的“插值”。
最常見(jiàn)也最簡(jiǎn)單的插值方式就是線性插值,還拿上面的例子講,就是在“連點(diǎn)”畫(huà)圖象的時(shí)候,用直線把各點(diǎn)連起來(lái)。
我們現(xiàn)在要取x=0.5
,通過(guò)(0,0)和(1,1)線性插值,即求出過(guò)這兩點(diǎn)的直線y=x,可以得到,y=0.5
;同理,x=1.5
時(shí),通過(guò)(1,1)和(2,4)的直線為y=3x−2,可以得到,y=2.5
。
我們使用three.js提供的線性插值驗(yàn)證一下:
import * as THREE from 'three' const x = [-2, -1, 0, 1, 2] const y = [4, 1, 0, 1, 4] const resultBuffer = new Float32Array(1) const interpolant = new THREE.LinearInterpolant(x, y, 1, resultBuffer) interpolant.evaluate(0.5) // 0.5 console.log(resultBuffer[0]) interpolant.evaluate(1.5) // 2.5 console.log(resultBuffer[0])
看不懂這段代碼沒(méi)有關(guān)系,接下來(lái)會(huì)慢慢解釋。
通過(guò)離散的采樣點(diǎn)定義曲線
在Interpolant
的構(gòu)造器,需要以下這些參數(shù):
parameterPositions
:采樣的位置,類比成函數(shù)就是自變量的取值
sampleValues
:采樣取的值,類比成函數(shù)就是自變量對(duì)應(yīng)的函數(shù)值
sampleSize
:每個(gè)采樣點(diǎn)的值,分量的個(gè)數(shù)。例:sampleValues
可以表示一個(gè)三維空間的坐標(biāo),有x, y, z
三個(gè)分量,所以sampleSize
就是三。
resultBuffer
:用來(lái)獲取插值的結(jié)果,長(zhǎng)度為sampleSize
時(shí),剛好夠用。
這幾個(gè)參數(shù)一般有著如下的數(shù)量關(guān)系:
通過(guò)上面這些參數(shù),我們就可以大概表示一個(gè)函數(shù)的曲線,相當(dāng)于在使用“描點(diǎn)法”畫(huà)圖象時(shí),把一些離散地采樣點(diǎn)標(biāo)注在坐標(biāo)系中。
有了這些離散的點(diǎn),我們就可以通過(guò)插值,求出任意點(diǎn)的函數(shù)值。
插值的步驟
1. 尋找要插值的位置
還拿上面的例子來(lái)說(shuō),parameterPositions = [-2, -1, 0, 1, 2]
,現(xiàn)在想要知道position = 1.5
處的函數(shù)值,我們就需要在parameterPositions
這個(gè)數(shù)組中找到position
應(yīng)該介于那兩個(gè)元素之間。很顯然,在這個(gè)例子中,值在元素1,2之間,下標(biāo)在3,4之間。
2. 根據(jù)找到的左右兩個(gè)點(diǎn),進(jìn)行插值
上面的例子中,我們找到的兩個(gè)點(diǎn)分別是(1,1)和(2,,4)??梢杂卸喾N插值的方式,這取決于你的需求,我們?nèi)匀荒镁€性插值舉例,通過(guò)(1,1)和(2,4)可以確定一條直線,然后把1.5帶入即可。
Interpolant源碼
Interpolant
采用了一種設(shè)計(jì)模式:模板方法模式。
在插值的整個(gè)流程中,對(duì)于不同的插值方法來(lái)說(shuō),尋找插值位置這一操作是一樣的,所以把這一個(gè)操作可以放在基類中實(shí)現(xiàn)。
對(duì)于不同的插值類型,都派生自Interpolant
,然后實(shí)現(xiàn)具體的插值方法,這個(gè)方法的參數(shù)就是上面尋找到的位置。
1. 構(gòu)造器
constructor(parameterPositions, sampleValues, sampleSize, resultBuffer) { this.parameterPositions = parameterPositions; this._cachedIndex = 0; this.resultBuffer = resultBuffer !== undefined ? resultBuffer : new sampleValues.constructor(sampleSize); this.sampleValues = sampleValues; this.valueSize = sampleSize; this.settings = null; this.DefaultSettings_ = {}; }
基本上就是把參數(shù)中的變量進(jìn)行賦值,對(duì)于resultBuffer
來(lái)說(shuō),如果不在參數(shù)中傳遞,那么就會(huì)在構(gòu)造器中進(jìn)行創(chuàng)建。
_cachedIndex
放到后面解釋。
2. copySampleValue_()
如果,我們要插值的點(diǎn),剛好是采樣點(diǎn),就沒(méi)必要進(jìn)行計(jì)算了,直接把采樣點(diǎn)的結(jié)果放到resultBuffer
中即可,這個(gè)方法就是在做這件事,參數(shù)就是采樣點(diǎn)的下標(biāo)。
copySampleValue_(index) { // copies a sample value to the result buffer const result = this.resultBuffer, values = this.sampleValues, stride = this.valueSize, offset = index * stride; for (let i = 0; i !== stride; ++i) { result[i] = values[offset + i]; } return result; }
3. interpolate_( /* i1, t0, t, t1 */ )
interpolate_( /* i1, t0, t, t1 */ ) { throw new Error( 'call to abstract method' ); // implementations shall return this.resultBuffer }
這個(gè)就是具體的插值方法,但是在基類中并沒(méi)有給出實(shí)現(xiàn)。
4. evaluate()
接下來(lái)就是多外暴露的接口,通過(guò)這個(gè)方法計(jì)算插值的結(jié)果。
這段代碼用了一個(gè)不常用的語(yǔ)法,類似C
語(yǔ)言中的goto
語(yǔ)句,可以給代碼塊命名,然后通過(guò)break 代碼塊名
跳出代碼塊。
這段代碼就是實(shí)現(xiàn)了上面說(shuō)的插值的過(guò)程:
尋找位置
插值(調(diào)用interpolate_()
方法)
整個(gè)validate_interval
代碼塊,其實(shí)就是在找插值的位置。它的流程是:
- 線性查找
- 根據(jù)上一次插值的位置,向數(shù)組尾部的方向查找兩個(gè)位置。(這里就是構(gòu)造器中
_cachedIndex
的作用,記錄上一次插值的位置)。如果到了數(shù)組最后仍然沒(méi)找到,則到數(shù)組頭部去找;如果沒(méi)有到數(shù)組尾部,則直接跳出線性查找,使用二分查找。
- 二分查找
為什么要先在上一次插值的左右位置進(jìn)行線性查找呢?插值最常見(jiàn)的使用場(chǎng)景就是動(dòng)畫(huà),每次會(huì)把一個(gè)時(shí)間傳進(jìn)來(lái)進(jìn)行插值,而兩次插值的間隔通常很短,分布在上一次插值的附近,可能是想通過(guò)線性查找優(yōu)化性能。
evaluate(t) { const pp = this.parameterPositions; let i1 = this._cachedIndex, t1 = pp[i1], t0 = pp[i1 - 1]; validate_interval: { seek: { let right; // 先進(jìn)性線性查找 linear_scan: { //- See http://jsperf.com/comparison-to-undefined/3 //- slower code: //- //- if ( t >= t1 || t1 === undefined ) { forward_scan: if (!(t < t1)) { // 只向后查找兩次 for (let giveUpAt = i1 + 2; ;) { // t1 === undefined,說(shuō)明已經(jīng)到了數(shù)組的末尾 if (t1 === undefined) { // t0是最后一個(gè)位置 // 如果t < t0 // 則說(shuō)明向數(shù)組末尾找,沒(méi)有找到 // 因此跳出這次尋找 接著用其他方法找 if (t < t0) break forward_scan; // after end // t >= t0 // 查找的結(jié)果就是最后一個(gè)點(diǎn) 不需要進(jìn)行插值 i1 = pp.length; this._cachedIndex = i1; return this.copySampleValue_(i1 - 1); } // 控制向尾部查找的次數(shù) 僅查找兩次 if (i1 === giveUpAt) break; // this loop // 迭代自增 t0 = t1; t1 = pp[++i1]; // t >= t0 && t < t1 // 找到了,t介于t0和t1之間 // 跳出尋找的代碼塊 if (t < t1) { // we have arrived at the sought interval break seek; } } // prepare binary search on the right side of the index right = pp.length; break linear_scan; } //- slower code: //- if ( t < t0 || t0 === undefined ) { if (!(t >= t0)) { // looping? // 上一次查找到數(shù)組末尾了 // 查找數(shù)組前兩個(gè)元素 const t1global = pp[1]; if (t < t1global) { i1 = 2; // + 1, using the scan for the details t0 = t1global; } // linear reverse scan // 如果上一次查找到數(shù)組末尾 // i1就被設(shè)置成了2,查找數(shù)組前2個(gè)元素 for (let giveUpAt = i1 - 2; ;) { // 找到頭了 // 插值的結(jié)果就是第一個(gè)采樣點(diǎn)的結(jié)果 if (t0 === undefined) { // before start this._cachedIndex = 0; return this.copySampleValue_(0); } if (i1 === giveUpAt) break; // this loop t1 = t0; t0 = pp[--i1 - 1]; if (t >= t0) { // we have arrived at the sought interval break seek; } } // prepare binary search on the left side of the index right = i1; i1 = 0; break linear_scan; } // the interval is valid break validate_interval; } // linear scan // binary search while (i1 < right) { const mid = (i1 + right) >>> 1; if (t < pp[mid]) { right = mid; } else { i1 = mid + 1; } } t1 = pp[i1]; t0 = pp[i1 - 1]; // check boundary cases, again if (t0 === undefined) { this._cachedIndex = 0; return this.copySampleValue_(0); } if (t1 === undefined) { i1 = pp.length; this._cachedIndex = i1; return this.copySampleValue_(i1 - 1); } } // seek this._cachedIndex = i1; this.intervalChanged_(i1, t0, t1); } // validate_interval // 調(diào)用插值方法 return this.interpolate_(i1, t0, t, t1); }
上面的代碼看著非常多,其實(shí)大量的代碼都是在找位置。找到位置之后,調(diào)用子類實(shí)現(xiàn)的抽象方法。
5. LinearInterpolant實(shí)現(xiàn)interpolate_( /* i1, t0, t, t1 */ )方法
class LinearInterpolant extends Interpolant { constructor(parameterPositions, sampleValues, sampleSize, resultBuffer) { super(parameterPositions, sampleValues, sampleSize, resultBuffer); } interpolate_(i1, t0, t, t1) { const result = this.resultBuffer, values = this.sampleValues, stride = this.valueSize, offset1 = i1 * stride, offset0 = offset1 - stride, weight1 = (t - t0) / (t1 - t0), weight0 = 1 - weight1; for (let i = 0; i !== stride; ++i) { result[i] = values[offset0 + i] * weight0 + values[offset1 + i] * weight1; } return result; } }
總結(jié)
Three.js
提供了內(nèi)置的插值類Interpolant
,采用了模板方法的設(shè)計(jì)模式。對(duì)于不同的插值方式,繼承基類Interpolant
,然后實(shí)現(xiàn)抽象方法interpolate_
。
計(jì)算插值的步驟就是先找到插值的位置,然后把插值位置兩邊的采樣點(diǎn)傳遞給interpolate_()
方法,不同的插值方式會(huì)override
該方法,以產(chǎn)生不同的結(jié)果。
推導(dǎo)了線性插值的公式。
以上就是Three.js Interpolant實(shí)現(xiàn)動(dòng)畫(huà)插值的詳細(xì)內(nèi)容,更多關(guān)于Three.js Interpolant動(dòng)畫(huà)插值的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序 <swiper-item>標(biāo)簽傳入數(shù)據(jù)
這篇文章主要介紹了微信小程序 <swiper-item>標(biāo)簽傳入數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2017-05-05Promise靜態(tài)四兄弟實(shí)現(xiàn)示例詳解
這篇文章主要為大家介紹了Promise靜態(tài)四兄弟實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07axios?攔截器管理類鏈?zhǔn)秸{(diào)用手寫實(shí)現(xiàn)及原理剖析
這篇文章主要為大家介紹了axios?攔截器管理類鏈?zhǔn)秸{(diào)用手寫實(shí)現(xiàn)及原理剖析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08微信小程序 實(shí)現(xiàn)點(diǎn)擊添加移除class
這篇文章主要介紹了 微信小程序 實(shí)現(xiàn)點(diǎn)擊添加移除class的相關(guān)資料,需要的朋友可以參考下2017-06-06微信小程序中實(shí)現(xiàn)一對(duì)多發(fā)消息詳解及實(shí)例代碼
這篇文章主要介紹了微信小程序中實(shí)現(xiàn)一對(duì)多發(fā)消息詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02JavaScript?sort方法實(shí)現(xiàn)數(shù)組升序降序
這篇文章主要為大家介紹了JavaScript?sort方法實(shí)現(xiàn)數(shù)組升序降序示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07微信小程序開(kāi)發(fā)一鍵登錄 獲取session_key和openid實(shí)例
這篇文章主要介紹了微信小程序開(kāi)發(fā)一鍵登錄 獲取session_key和openid實(shí)例的相關(guān)資料,需要的朋友可以參考下2016-11-11