<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
這個類主要是用來實現插值,常用於動畫。
可以把這個類理解為是一個數學函數,給定一個自變數,要返回對應的函數值。只是,在我們定義函數的時候,是通過一些離散的點進行定義的。
舉個例子,加入我們要定義y = x^2這條曲線,我們需要定義兩個陣列(即取樣點和取樣的值):x = [-2, -1, 0, 1, 2]
,y = [4, 1 ,0, 1, 4]
。通過這樣的定義方式,我們怎麼求不是取樣點中的函數值?例如上面的吱吱,我們怎麼求x = 0.5
時的值?這就時我們要說的“插值”。
最常見也最簡單的插值方式就是線性插值,還拿上面的例子講,就是在“連點”畫圖象的時候,用直線把各點連起來。
我們現在要取x=0.5
,通過(0,0)和(1,1)線性插值,即求出過這兩點的直線y=x,可以得到,y=0.5
;同理,x=1.5
時,通過(1,1)和(2,4)的直線為y=3x−2,可以得到,y=2.5
。
我們使用three.js提供的線性插值驗證一下:
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])
看不懂這段程式碼沒有關係,接下來會慢慢解釋。
在Interpolant
的構造器,需要以下這些引數:
parameterPositions
:取樣的位置,類比成函數就是自變數的取值
sampleValues
:取樣取的值,類比成函數就是自變數對應的函數值
sampleSize
:每個取樣點的值,分量的個數。例:sampleValues
可以表示一個三維空間的座標,有x, y, z
三個分量,所以sampleSize
就是三。
resultBuffer
:用來獲取插值的結果,長度為sampleSize
時,剛好夠用。
這幾個引數一般有著如下的數量關係:
通過上面這些引數,我們就可以大概表示一個函數的曲線,相當於在使用“描點法”畫圖象時,把一些離散地取樣點標註在座標系中。
有了這些離散的點,我們就可以通過插值,求出任意點的函數值。
還拿上面的例子來說,parameterPositions = [-2, -1, 0, 1, 2]
,現在想要知道position = 1.5
處的函數值,我們就需要在parameterPositions
這個陣列中找到position
應該介於那兩個元素之間。很顯然,在這個例子中,值在元素1,2之間,下標在3,4之間。
上面的例子中,我們找到的兩個點分別是(1,1)和(2,,4)。可以有多種插值的方式,這取決於你的需求,我們仍然拿線性插值舉例,通過(1,1)和(2,4)可以確定一條直線,然後把1.5帶入即可。
Interpolant
採用了一種設計模式:模板方法模式。
在插值的整個流程中,對於不同的插值方法來說,尋找插值位置這一操作是一樣的,所以把這一個操作可以放在基礎類別中實現。
對於不同的插值型別,都派生自Interpolant
,然後實現具體的插值方法,這個方法的引數就是上面尋找到的位置。
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_ = {}; }
基本上就是把引數中的變數進行賦值,對於resultBuffer
來說,如果不在引數中傳遞,那麼就會在構造器中進行建立。
_cachedIndex
放到後面解釋。
如果,我們要插值的點,剛好是取樣點,就沒必要進行計算了,直接把取樣點的結果放到resultBuffer
中即可,這個方法就是在做這件事,引數就是取樣點的下標。
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; }
interpolate_( /* i1, t0, t, t1 */ ) { throw new Error( 'call to abstract method' ); // implementations shall return this.resultBuffer }
這個就是具體的插值方法,但是在基礎類別中並沒有給出實現。
接下來就是多外暴露的介面,通過這個方法計算插值的結果。
這段程式碼用了一個不常用的語法,類似C
語言中的goto
語句,可以給程式碼塊命名,然後通過break 程式碼塊名
跳出程式碼塊。
這段程式碼就是實現了上面說的插值的過程:
尋找位置
插值(呼叫interpolate_()
方法)
整個validate_interval
程式碼塊,其實就是在找插值的位置。它的流程是:
_cachedIndex
的作用,記錄上一次插值的位置)。如果到了陣列最後仍然沒找到,則到陣列頭部去找;如果沒有到陣列尾部,則直接跳出線性查詢,使用二分查詢。為什麼要先在上一次插值的左右位置進行線性查詢呢?插值最常見的使用場景就是動畫,每次會把一個時間傳進來進行插值,而兩次插值的間隔通常很短,分佈在上一次插值的附近,可能是想通過線性查詢優化效能。
evaluate(t) { const pp = this.parameterPositions; let i1 = this._cachedIndex, t1 = pp[i1], t0 = pp[i1 - 1]; validate_interval: { seek: { let right; // 先進性線性查詢 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,說明已經到了陣列的末尾 if (t1 === undefined) { // t0是最後一個位置 // 如果t < t0 // 則說明向陣列末尾找,沒有找到 // 因此跳出這次尋找 接著用其他方法找 if (t < t0) break forward_scan; // after end // t >= t0 // 查詢的結果就是最後一個點 不需要進行插值 i1 = pp.length; this._cachedIndex = i1; return this.copySampleValue_(i1 - 1); } // 控制向尾部查詢的次數 僅查詢兩次 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? // 上一次查詢到陣列末尾了 // 查詢陣列前兩個元素 const t1global = pp[1]; if (t < t1global) { i1 = 2; // + 1, using the scan for the details t0 = t1global; } // linear reverse scan // 如果上一次查詢到陣列末尾 // i1就被設定成了2,查詢陣列前2個元素 for (let giveUpAt = i1 - 2; ;) { // 找到頭了 // 插值的結果就是第一個取樣點的結果 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 // 呼叫插值方法 return this.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; } }
Three.js
提供了內建的插值類Interpolant
,採用了模板方法的設計模式。對於不同的插值方式,繼承基礎類別Interpolant
,然後實現抽象方法interpolate_
。
計算插值的步驟就是先找到插值的位置,然後把插值位置兩邊的取樣點傳遞給interpolate_()
方法,不同的插值方式會override
該方法,以產生不同的結果。
推導了線性插值的公式。
以上就是Three.js Interpolant實現動畫插值的詳細內容,更多關於Three.js Interpolant動畫插值的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45