算法动画 - 理解函数曲线
這篇梳理一些有關(guān)算法動畫的生成思路。
用算法生成動畫,大致可分成兩類。一類是基于時(shí)間( time-based ),一類是基于幀( frame-based )。其中有何區(qū)別,我們先通過兩段 Processing 代碼去理解。
代碼 01( 基于幀 )
float x; void setup(){ ?size(600,200); ?x = 100; } void draw(){ ?background(239,234,228); ?if(x < 500){ ? ? ?x += 5; ?} ?fill(50,120,133); ?noStroke(); ?ellipse(x,height/2,50,50); }
代碼淺析:
代碼中創(chuàng)建了一個(gè)變量 x 表示圓的橫坐標(biāo)。數(shù)值初始化為 100。而draw 函數(shù)中 x 每次累加 5,直到 500 停止累加。因此 x 的數(shù)值變化范圍則是從 100 到 500,實(shí)現(xiàn)了小球從左到右的運(yùn)動
代碼 02 ( 基于時(shí)間 )
float time; void setup(){ ?size(600,200); ?time = 3; } void draw(){ ?background(239,234,228); ?float x = min(500,map(millis()/1000.0,0,time,100,500)); ?fill(50,120,133); ?noStroke(); ?ellipse(x,height/2,50,50); }
代碼淺析:
代碼中創(chuàng)建了一個(gè)變量 time ,表示圓從左側(cè)運(yùn)動到右側(cè)的時(shí)間
millis() 表示毫秒,因而 millis()/1000.0 表示秒。通過 map 函數(shù),將時(shí)間從 0 到 time 的變化,映射為從 100 到 500 的變化。隨著時(shí)間的遞增,實(shí)現(xiàn)了小球從左到右的運(yùn)動
min 函數(shù)用于限定 x 的大小,讓數(shù)值不超過 500
簡單比較
通過對比兩段代碼可以發(fā)現(xiàn),雖然最終結(jié)果是近似的(小球從 100 勻速運(yùn)動到 500),但決定運(yùn)動的條件是不同的。前者限定了每次小球每次遞增的距離,后者限定了整個(gè)運(yùn)動的時(shí)間。
條件的不同,決定了在某些場景下,某種方法會比另一種方法使用起來更便利。例如要繪制一個(gè)運(yùn)行速度恒定的小車,使用基于幀的算法寫起來會更簡便。若希望小車從 A 點(diǎn)運(yùn)行到 B 點(diǎn)的時(shí)間是固定值,又或者實(shí)現(xiàn)時(shí)間間隔固定的淡入淡出效果(將數(shù)值變化映射到顏色變化),基于時(shí)間的算法則更合適。
除此之外,它們兩者間還有一個(gè)更重要的區(qū)別。使得自己在制作動畫時(shí),更傾向使用基于時(shí)間的思路。
前面的例子中,由于繪制的都是一些非常簡單的圖形,所以程序運(yùn)行必然非常流暢平穩(wěn)的,維持在 60 fps。但如果程序有復(fù)雜的場景切換。某些場景繪制的元素多,占用更多計(jì)算資源。就會導(dǎo)致某個(gè)時(shí)間段運(yùn)行幀率變慢。
我們可以設(shè)想下這個(gè)情況。假如小車每幀往前移動 1 個(gè)單位,第一秒內(nèi)如果程序的幀率正常(60fps),這一秒小車就會移動 60 個(gè)單位。到第二秒開始,若場景里出現(xiàn)很多元素,導(dǎo)致程序幀率變成 20 fps了,由于小車每幀累加的值是固定的,所以這一秒,小車就只移動了 20 個(gè)單位。合起來,在兩秒的時(shí)間中,小車只移動了 80 個(gè)單位。相比幀率恒定的情況下移動 120 個(gè)單位,小車移動的距離明顯變小了。而且整個(gè)動畫連起來看,小車做的就不再是勻速運(yùn)動,出現(xiàn)先快后慢的結(jié)果。
為了避免這種情況,如果用基于時(shí)間的寫法,效果就大有不同。因?yàn)檫@時(shí)小車的位置是根據(jù)時(shí)間的流逝多少決定的。它能保證在相應(yīng)時(shí)間內(nèi),小車的位置都在“正確”的地方。只是幀率低的時(shí)候,畫面運(yùn)動的流暢度降低而已,整體小車的運(yùn)行速度并沒有變化。
基于這種特性。游戲中的運(yùn)動基本是采用基于時(shí)間的算法去實(shí)現(xiàn)的。畢竟不同玩家的電腦配置可能有很大的區(qū)別,如果開發(fā)一個(gè)賽場游戲,汽車運(yùn)動算法是基于幀的。那電腦配置高的玩家,車的速度就變快,這顯然是不合理的。
運(yùn)動的自然之道 - 使用函數(shù)曲線
我們再來看前面寫的小球動畫。雖然它是動起來了,但顯得很呆板。為何會產(chǎn)生這種感覺?這是因?yàn)檫`背了人的視覺經(jīng)驗(yàn)。在日常生活中,我們很難看見一個(gè)物體從完全靜止的狀態(tài)突然變成勻速運(yùn)動的狀態(tài),也很難看到一個(gè)運(yùn)動中的物體瞬間靜止。
要改善這種狀況,一個(gè)簡單的方式是引入“力”。比如下面的例子,實(shí)現(xiàn)了小球從靜止到加速。
代碼 03 ( 加速運(yùn)動 )
float posX; float acc; float vel; void setup(){ ?size(600,200); ?posX = 100; } void draw(){ ?background(239,234,228); ?acc = 0.5; ?vel += acc; ?posX += vel; ?if(posX > 500){ ? ?posX = 500; ?} ?fill(50,120,133); ?noStroke(); ?ellipse(posX,height/2,50,50); }
如果希望上面的小球在快接近目標(biāo)的時(shí)候有減速的效果,就需要在上面增加一些屬性或是添加判定條件。這樣的做法顯然有些繁瑣,而且仍舊是“基于幀”的。如果我們希望準(zhǔn)確地控制小球的運(yùn)動時(shí)間,僅用上面代碼是無法做到的。
有更簡便的方式嗎?函數(shù)曲線此時(shí)就可以派上用場。
函數(shù)曲線
我們先選一個(gè)典型的數(shù)學(xué)函數(shù) sin
再結(jié)合圖像理解下面代碼
代碼 04 ( 加速到減速 )
float time; void setup(){ ?size(600,200); ?time = 3; } void draw(){ ?background(239,234,228); ?float sinInput = map(min(time,millis()/1000.0),0,time,-PI/2,PI/2); ?float x = map(sin(sinInput),-1,1,100,500); ?fill(50,120,133); ?noStroke(); ?ellipse(x,height/2,50,50); }
代碼淺析:
相比代碼 03,例子 04 并沒有用到速度,加速度等變量。但仍然可以看到小球有加速,減速的運(yùn)動變化,而且可以通過 time 變量去控制小球的運(yùn)動時(shí)間
雖然運(yùn)動并不嚴(yán)格遵循牛頓力學(xué),但整體效果還是比較自然的。它很巧妙地利用了 sin 函數(shù)曲線的變化來映射小球的位置變化。具體的操作,是在 x 方向上截取一段合適的區(qū)間,然后將對應(yīng)函數(shù)值 y 的變化,映射到我們需要的變化區(qū)間之內(nèi)。若有模糊的地方,可以對照下圖去理解
藍(lán)線可以看成是“時(shí)間”(時(shí)間流逝速率恒定)。請腦補(bǔ)一個(gè)動畫,藍(lán)線以恒定的速度從 -0.5 π 的位置從左往右移動到 0.5 π 的位置。它與函數(shù)曲線的交點(diǎn)為 A。此時(shí) A 點(diǎn)的 y 坐標(biāo)就表示函數(shù)的輸出值。可以看出在這個(gè)區(qū)間內(nèi)移動,sin 函數(shù)的輸出值就會從 -1 變化到 1。但這個(gè)輸出的變化值我們不能直接使用,需要通過 map 函數(shù),將它映射到在我們想要的范圍內(nèi)變化。
sin 函數(shù)在這里其實(shí)就是一個(gè)中轉(zhuǎn)站。只是使用它前,需要將輸入值和輸出值做兩次處理 (調(diào)用兩次 map)。第一次調(diào)用 map 函數(shù),就是將時(shí)間從 0 到 time 的變化,映射為 -PI/2 到 PI/2 之間的變化,再傳入函數(shù)中。第二次調(diào)用 map,則是函數(shù)的輸出值映射為我們需要的位置數(shù)值。
同一個(gè)函數(shù),選擇的輸入?yún)^(qū)間不同。得出的結(jié)果也不同。假如選擇從 A 點(diǎn)到 B 點(diǎn)作為變化區(qū)間,整體的運(yùn)動速率就是先慢后快的加速過程。如果選擇從 B 點(diǎn)到 C 點(diǎn),則整體的運(yùn)動速率就是先快后快的減速過程。要判斷是加速還是減速,可以對照函數(shù)曲線。越平的地方,就代表運(yùn)動越慢,越陡峭,就表明運(yùn)動變化越快。
指數(shù)函數(shù)
當(dāng)理解了上面的思路。現(xiàn)在數(shù)學(xué)函數(shù)就可以成為你的創(chuàng)作素材。常用的數(shù)學(xué)函數(shù)除了三角函數(shù) sin,cos。還有指數(shù)函數(shù)。
一般地,y = a^x函數(shù)(a為常數(shù)且以a>0,a≠1)叫做指數(shù)函數(shù)。下圖是 y=2^x 的圖像。
指數(shù)函數(shù)在 Processing 中寫作
pow(a,b)
其中 a 表示底數(shù),b 表示指數(shù)。pow(2,2) 表示 2 的 3 次方,結(jié)果為 8。
有關(guān)指數(shù)函數(shù)的用法就不再展開,與上面例子是類似,找準(zhǔn)輸入輸出區(qū)間再作映射即可。函數(shù)曲線的使用是非常靈活的。不僅可以單獨(dú)使用,還可以組合使用。例如兩個(gè)基本函數(shù)進(jìn)行相加和相成,都會得到意想不到的效果。
延展
現(xiàn)在僅僅靠指數(shù)函數(shù)與三角函數(shù),就可以產(chǎn)生各種不同的函數(shù)曲線。下面代碼就是指數(shù)函數(shù)與三角函數(shù)的疊加,它使得小球加速靠近的同時(shí),能有一個(gè)來回的擺動。最終產(chǎn)生了帶彈性的動畫效果。
float inputVal = min(map(millis()/1000.0,0,time,0,1),1); ?float x = map(cos(inputVal * 20) * pow(2,-10.0 * inputVal),1,0,100,500);
( 替換例 04 的運(yùn)動算法 )
總結(jié)
要盡可能理解函數(shù)曲線的特性。就需要多加實(shí)驗(yàn)。函數(shù)曲線可不僅僅只能用在運(yùn)動動畫上。下面用了 5 種常用函數(shù)輸出了幾組 gif。分別控制圖形的位置,顏色,旋轉(zhuǎn)角度,大小。可以去從中感受不同函數(shù)曲線的個(gè)性。
【 1 】線性遞增(勻速變化)
【 2 】sin 函數(shù)(區(qū)間 -PI/2 到 PI/2,從加速到減速)
【 3 】指數(shù)函數(shù)(減速)
【 4 】指數(shù)函數(shù)疊加 cos 函數(shù)(整體減速)
【 5 】sin 函數(shù)(往復(fù))
( 控制位置 )
( 控制透明度 )
( 控制旋轉(zhuǎn)角度 )
( 控制大小 )
最后附上一張由 Kynd 整理的一張圖,里面的函數(shù)曲線都很實(shí)用,有興趣可以到此地址下載高清大圖,了解更多函數(shù)曲線? (?http://thebookofshaders.com/05/kynd.png?)
補(bǔ)充
函數(shù)曲線非常實(shí)用,但如果在程序中每次使用都要考慮各種映射關(guān)系,顯然有點(diǎn)繁瑣。更好的做法是把一些常用的函數(shù)曲線用一個(gè)類把它封裝起來。
下面分享一段自己創(chuàng)作時(shí)常用到的類(代碼基于C++,框架 openframeworks)
class WenzyAni{ public: float ratio; // 內(nèi)部表示完成進(jìn)度 (范圍一般為 0 到 1) float startVal,endVal; // 開始的數(shù)值,結(jié)束的數(shù)值 float val; // 當(dāng)前的數(shù)值 float time; // 完成整個(gè)動畫所需的時(shí)間 int aniMode; // 決定數(shù)值的變化曲線類型 bool startMoving; // 是否開始運(yùn)動 float startTick; // 開始的時(shí)刻記錄 WenzyAni(){ } WenzyAni(float time_,float startVal_,float endVal_,int mode_ = 0){ ? ?time = time_; ? ?startVal = startVal_; ? ?endVal = endVal_; ? ?aniMode = mode_; ? ?startMoving = false; ? ?val = startVal_; } void update(){ ? ?if(startMoving){ ? ? ? ?ratio = MIN(time,ofGetElapsedTimef() - startTick)/time; ? ? ? ?if(aniMode == 0){ ? ? ? ? ? ?// 勻速平滑過渡 ? ? ? ? ? ?val = ofMap(ratio,0,1,startVal,endVal); ? ? ? ?}else if(aniMode == 1){ ? ? ? ? ? ?// 先加速后減速(經(jīng)過 sin 函數(shù)處理) ? ? ? ? ? ?float ratio2 = ofMap(sin(ofMap(ratio,0,1,-PI/2,PI/2)),-1,1,0,1); ? ? ? ? ? ?val = ofMap(ratio2,0,1,startVal,endVal); ? ? ? ?}else if(aniMode == 2){ ? ? ? ? ? ?// 持續(xù)減速(指數(shù)衰減) ? ? ? ? ? ?val = ofMap(pow(2,-10 * ratio),1,0,startVal,endVal); ? ? ? ?}else if(aniMode == 3){ ? ? ? ? ? ?// 彈簧效果 ? ? ? ? ? ?val = ofMap(cos(ratio * 20) * pow(2,-10 * ratio),1,0,startVal,endVal); ? ? ? ?}else if(aniMode == 4){ ? ? ? ? ? ?// cos 式往復(fù) ? ? ? ? ? ?float n = 2; // n 表示往復(fù)次數(shù) ? ? ? ? ? ?val = ofMap(cos(ratio * n * 2 * PI + PI),1,-1,startVal,endVal); ? ? ? ?} ? ?} } void start(){ ? ?startMoving = true; ? ?startTick = ofGetElapsedTimef(); } };
應(yīng)用范例 01
ofApp.h 內(nèi) —-
#include “WenzyAni.h” ... WenzyAni ani; ofEasyCam cam;
ofApp.cpp 內(nèi) —-
void ofApp::setup(){ ? ?ofSetWindowShape(1000,500); ? ?ofBackground(3,27,93); ? ?ani = WenzyAni(1, -300, 300,3); } void ofApp::update(){ ? ?ani.update(); } void ofApp::draw(){ ? ?cam.begin(); ? ?ofSetColor(233,60,37); ? ?ofDrawBox(ani.val,0,0,100); ? ?cam.end(); } void ofApp::keyPressed(int key){ ? ?if(key == '1'){ ? ? ? ?ani.start(); ? ?} ? ?if(key == '2'){ ? ? ? ?ani = WenzyAni(1, -300, 300,3); ? ? ? ?ani.start(); ? ?} ? ?if(key == '3'){ ? ? ? ?ani = WenzyAni(1, 300, -300,3); ? ? ? ?ani.start(); ? ?} ? ?if(key == '4'){ ? ? ? ? ani = WenzyAni(1, -300, 300,0); ? ? ? ?ani.start(); ? ? } ? ?if(key == '5'){ ? ? ? ?ani = WenzyAni(1, -300, 300,1); ? ? ? ?ani.start(); ? ?} ? ?if(key == '6'){ ? ? ? ?ani = WenzyAni(1, -300, 300,2); ? ? ? ?ani.start(); ? ?} }
代碼淺析:
start 函數(shù)為觸發(fā)動畫的函數(shù)。運(yùn)行程序后按數(shù)字鍵 1 即開始執(zhí)行,可以看到正方體將從左運(yùn)動到右,并且?guī)б稽c(diǎn)彈性動畫。這是因?yàn)?setup 中有一句
?ani = WenzyAni(1, -300, 300,3)
它將 ani 對象初始化時(shí)。第一個(gè)參數(shù)表示整個(gè)動畫的運(yùn)行時(shí)間,第二個(gè)參數(shù)表示初始的數(shù)值,第三個(gè)參數(shù)表示結(jié)束時(shí)的數(shù)值。第四個(gè)參數(shù)表示選擇應(yīng)用的曲線類型
draw 函數(shù)通過 ani.val 來表示正方體的橫坐標(biāo)
每按下一次數(shù)字鍵 1 執(zhí)行 start 函數(shù)時(shí),正方體的運(yùn)動都會從左變化到右。這是因?yàn)?startVal 與 endVal 的值在初始化時(shí)已經(jīng)確定。如果希望正方形實(shí)現(xiàn)從右到左的運(yùn)動,則需要重新初始化。按下數(shù)字鍵 3 就能實(shí)現(xiàn)這一效果。而來回按數(shù)字鍵 2,3 則能實(shí)現(xiàn)往復(fù)運(yùn)動。
按數(shù)字鍵 4,5,6 可以切換不同的曲線
(數(shù)字鍵 4)
(數(shù)字鍵 5)
(數(shù)字鍵 6)
應(yīng)用范例 02
下面再附上上篇文章中展示的幾個(gè) gif 源碼,還是使用同樣的類
ofApp.h 內(nèi) —-
#include “WenzyAni.h” ... vector<WenzyAni> ani; int showMode;
ofApp.cpp 內(nèi) —-
void ofApp::setup(){ ? ?ofSetWindowShape(1920,1080); ? ?ofBackground(239,234,228); ? ?for(int i = 0;i < 5;i++){ ? ? ? ?ani.push_back(WenzyAni(2,0,1,i)); ? ?} ? ?showMode = 0; } void ofApp::update(){ ? ?for(int i = 0;i < ani.size();i++){ ? ? ? ?ani[i].update(); ? ?} } void ofApp::draw(){ ? ?ofColor myColor(50,120,133); ? ?ofSetColor(myColor); ? ?ofSetCircleResolution(50); ? ?if(showMode == 0){ ? ? ? ?int num = ani.size(); ? ? ? ?float spaceRatio = 0.8; // 計(jì)算間隙占方塊的大小比 ? ? ? ?float rectW = ofGetHeight() / (num + (num + 1) * spaceRatio); ? ? ? ?float space = rectW * spaceRatio; ? ? ? ?int interval = (ofGetHeight() - space) / num; ? ? ? ?ofSetLineWidth(5); ? ? ? ?float startPos = ofGetWidth() * 0.1; ? ? ? ?float endPos = ofGetWidth() - startPos; ? ? ? ?for(int i = 0;i < num;i++){ ? ? ? ? ? ?ofSetColor(myColor); ? ? ? ? ? ?float x = ofMap(ani[i].val,0,1,startPos,endPos); ? ? ? ? ? ?float y = space/2 + (i + 0.5)* interval; ? ? ? ? ? ?ofDrawCircle(x,y,25); ? ? ? ? ? ?ofDrawLine(startPos,y,endPos,y); ? ? ? ?} ? ?}else if(showMode == 1){ ? ? ? ?int num = ani.size(); ? ? ? ?float spaceRatio = 0.4; // 計(jì)算間隙占方塊的大小比 ? ? ? ?float rectW = ofGetWidth() / (num + (num + 1) * spaceRatio); ? ? ? ?float space = rectW * spaceRatio; ? ? ? ?float rectY = ofGetHeight() * 0.5; ? ? ? ?int interval = (ofGetWidth() - space) / num; ? ? ? ?for(int i = 0;i < num;i++){ ? ? ? ? ? ?ofPushMatrix(); ? ? ? ? ? ?float x = space/2 + (i + 0.5) * interval; ? ? ? ? ? ?ofTranslate(x, ofGetHeight()/2); ? ? ? ? ? ?ofSetColor(myColor,ofMap(ani[i].val,0,1,255,0)); ? ? ? ? ? ?ofDrawCircle(0,0,rectW/2); ? ? ? ? ? ?ofPopMatrix(); ? ? ? ?} ? ?}else if(showMode == 2){ ? ? ? ?int num = ani.size(); ? ? ? ?float spaceRatio = 0.4; // 計(jì)算間隙占方塊的大小比 ? ? ? ?float rectW = ofGetWidth() / (num + (num + 1) * spaceRatio); ? ? ? ?float space = rectW * spaceRatio; ? ? ? ?float rectY = ofGetHeight() * 0.5; ? ? ? ?int interval = (ofGetWidth() - space) / num; ? ? ? ?ofSetLineWidth(4); ? ? ? ?for(int i = 0;i < num;i++){ ? ? ? ? ? ?ofSetColor(myColor); ? ? ? ? ? ?ofPushMatrix(); ? ? ? ? ? ?float x = space/2 + (i + 0.5) * interval; ? ? ? ? ? ?ofTranslate(x, ofGetHeight()/2); ? ? ? ? ? ?ofRotate(ofMap(ani[i].val,0,1,0,180)); ? ? ? ? ? ?ofDrawLine(0,rectW/2,0,-rectW/2); ? ? ? ? ? ?ofDrawCircle(0,rectW/2,30); ? ? ? ? ? ?ofDrawCircle(0,-rectW/2,30); ? ? ? ? ? ?ofPopMatrix(); ? ? ? ?} ? ?}else if(showMode == 3){ ? ? ? ?int num = ani.size(); ? ? ? ?float spaceRatio = 0.4; // 計(jì)算間隙占方塊的大小比 ? ? ? ?float rectW = ofGetWidth() / (num + (num + 1) * spaceRatio); ? ? ? ?float space = rectW * spaceRatio; ? ? ? ?float rectY = ofGetHeight() * 0.5; ? ? ? ?int interval = (ofGetWidth() - space) / num; ? ? ? ?for(int i = 0;i < num;i++){ ? ? ? ? ? ?ofPushMatrix(); ? ? ? ? ? ?float x = space/2 + (i + 0.5) * interval; ? ? ? ? ? ?ofTranslate(x, ofGetHeight()/2); ? ? ? ? ? ?ofSetColor(myColor); ? ? ? ? ? ?float w = ofMap(ani[i].val,0,1,0,rectW); ? ? ? ? ? ?ofDrawCircle(0,0,w/2); ? ? ? ? ? ?ofPopMatrix(); ? ? ? ?} ? ?} ? ?ofSetColor(0); ? ?ofDrawBitmapString("ShowMode:" + ofToString(showMode),50,50); } void ofApp::keyPressed(int key){ ? ?if(key == 'r'){ ? ? ? ?for(int i = 0;i < ani.size();i++){ ? ? ? ? ? ?ani[i].start(); ? ? ? ?} ? ?} ? ?if(key == OF_KEY_DOWN){ ? ? ? ?showMode--; ? ? ? ?showMode = MAX(0,showMode); ? ?} ? ?if(key == OF_KEY_UP){ ? ? ? ?showMode++; ? ? ? ?showMode = MIN(3,showMode); ? ?} }
運(yùn)行效果:
代碼淺析:
按 r 鍵開始動畫,按方向鍵上下切換不用的模式
模塊中只列舉了少數(shù)函數(shù)曲線,根據(jù)個(gè)人需要可以拓展補(bǔ)充
End
個(gè)人日常中還是傾向于通過自定義函數(shù)來使用曲線。如果你不想過于深究各類函數(shù)曲線的性質(zhì),只希望實(shí)現(xiàn)具體的效果。也有辦法可以直接采用別人定制好的各類運(yùn)動曲線。最后再推薦兩個(gè)插件
OF 插件 - ofxAnimatable
下載地址:https://github.com/armadillu/ofxAnimatable
附帶的范例:
Processing 插件 - Ani
在 IDE 的 Libraries 菜單中輸入 “animation”
又或是通過以下鏈接手動下載:
http://www.looksgood.de/libraries/Ani/
:)
∑編輯?|?Gemini
來源 |?InsLab
算法數(shù)學(xué)之美微信公眾號歡迎賜稿
稿件涉及數(shù)學(xué)、物理、算法、計(jì)算機(jī)、編程等相關(guān)領(lǐng)域,經(jīng)采用我們將奉上稿酬。
投稿郵箱:math_alg@163.com
總結(jié)
以上是生活随笔為你收集整理的算法动画 - 理解函数曲线的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 加加减减的奥秘——从数学到魔术的思考(二
- 下一篇: 袁亚湘委员:加强对数学等基础科学领域支持