这篇梳理一些有关算法动画的生成思路。

用算法生成动画,大致可分成两类。一类是基于时间( time-based ),一类是基于帧( frame-based )。其中有何区别,我们先通过两段 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); }

代码浅析:

  • 代码中创建了一个变量 x 表示圆的横坐标。数值初始化为 100。而draw 函数中 x 每次累加 5,直到 500 停止累加。因此 x 的数值变化范围则是从 100 到 500,实现了小球从左到右的运动

代码 02 ( 基于时间 )

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); }

代码浅析:

  • 代码中创建了一个变量 time ,表示圆从左侧运动到右侧的时间

  • millis() 表示毫秒,因而 millis()/1000.0 表示秒。通过 map 函数,将时间从 0 到 time 的变化,映射为从 100 到 500 的变化。随着时间的递增,实现了小球从左到右的运动

  • min 函数用于限定 x 的大小,让数值不超过 500

简单比较

通过对比两段代码可以发现,虽然最终结果是近似的(小球从 100 匀速运动到 500),但决定运动的条件是不同的。前者限定了每次小球每次递增的距离,后者限定了整个运动的时间。

条件的不同,决定了在某些场景下,某种方法会比另一种方法使用起来更便利。例如要绘制一个运行速度恒定的小车,使用基于帧的算法写起来会更简便。若希望小车从 A 点运行到 B 点的时间是固定值,又或者实现时间间隔固定的淡入淡出效果(将数值变化映射到颜色变化),基于时间的算法则更合适。

除此之外,它们两者间还有一个更重要的区别。使得自己在制作动画时,更倾向使用基于时间的思路。

前面的例子中,由于绘制的都是一些非常简单的图形,所以程序运行必然非常流畅平稳的,维持在 60 fps。但如果程序有复杂的场景切换。某些场景绘制的元素多,占用更多计算资源。就会导致某个时间段运行帧率变慢。

我们可以设想下这个情况。假如小车每帧往前移动 1 个单位,第一秒内如果程序的帧率正常(60fps),这一秒小车就会移动 60 个单位。到第二秒开始,若场景里出现很多元素,导致程序帧率变成 20 fps了,由于小车每帧累加的值是固定的,所以这一秒,小车就只移动了 20 个单位。合起来,在两秒的时间中,小车只移动了 80 个单位。相比帧率恒定的情况下移动 120 个单位,小车移动的距离明显变小了。而且整个动画连起来看,小车做的就不再是匀速运动,出现先快后慢的结果。

为了避免这种情况,如果用基于时间的写法,效果就大有不同。因为这时小车的位置是根据时间的流逝多少决定的。它能保证在相应时间内,小车的位置都在“正确”的地方。只是帧率低的时候,画面运动的流畅度降低而已,整体小车的运行速度并没有变化。

基于这种特性。游戏中的运动基本是采用基于时间的算法去实现的。毕竟不同玩家的电脑配置可能有很大的区别,如果开发一个赛场游戏,汽车运动算法是基于帧的。那电脑配置高的玩家,车的速度就变快,这显然是不合理的。

运动的自然之道 - 使用函数曲线

我们再来看前面写的小球动画。虽然它是动起来了,但显得很呆板。为何会产生这种感觉?这是因为违背了人的视觉经验。在日常生活中,我们很难看见一个物体从完全静止的状态突然变成匀速运动的状态,也很难看到一个运动中的物体瞬间静止。

要改善这种状况,一个简单的方式是引入“力”。比如下面的例子,实现了小球从静止到加速。

代码 03 ( 加速运动 )

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); }

如果希望上面的小球在快接近目标的时候有减速的效果,就需要在上面增加一些属性或是添加判定条件。这样的做法显然有些繁琐,而且仍旧是“基于帧”的。如果我们希望准确地控制小球的运动时间,仅用上面代码是无法做到的。

有更简便的方式吗?函数曲线此时就可以派上用场。

函数曲线

我们先选一个典型的数学函数 sin

再结合图像理解下面代码

代码 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 并没有用到速度,加速度等变量。但仍然可以看到小球有加速,减速的运动变化,而且可以通过 time 变量去控制小球的运动时间

  • 虽然运动并不严格遵循牛顿力学,但整体效果还是比较自然的。它很巧妙地利用了 sin 函数曲线的变化来映射小球的位置变化。具体的操作,是在 x 方向上截取一段合适的区间,然后将对应函数值 y 的变化,映射到我们需要的变化区间之内。若有模糊的地方,可以对照下图去理解

  • 蓝线可以看成是“时间”(时间流逝速率恒定)。请脑补一个动画,蓝线以恒定的速度从 -0.5 π 的位置从左往右移动到 0.5 π 的位置。它与函数曲线的交点为 A。此时 A 点的 y 坐标就表示函数的输出值。可以看出在这个区间内移动,sin 函数的输出值就会从 -1 变化到 1。但这个输出的变化值我们不能直接使用,需要通过 map 函数,将它映射到在我们想要的范围内变化。

  • sin 函数在这里其实就是一个中转站。只是使用它前,需要将输入值和输出值做两次处理 (调用两次 map)。第一次调用 map 函数,就是将时间从 0 到 time 的变化,映射为 -PI/2 到 PI/2 之间的变化,再传入函数中。第二次调用 map,则是函数的输出值映射为我们需要的位置数值。

  • 同一个函数,选择的输入区间不同。得出的结果也不同。假如选择从 A 点到 B 点作为变化区间,整体的运动速率就是先慢后快的加速过程。如果选择从 B 点到 C 点,则整体的运动速率就是先快后快的减速过程。要判断是加速还是减速,可以对照函数曲线。越平的地方,就代表运动越慢,越陡峭,就表明运动变化越快。

指数函数

当理解了上面的思路。现在数学函数就可以成为你的创作素材。常用的数学函数除了三角函数 sin,cos。还有指数函数。

一般地,y = a^x函数(a为常数且以a>0,a≠1)叫做指数函数。下图是 y=2^x 的图像。

指数函数在 Processing 中写作

pow(a,b)

其中 a 表示底数,b 表示指数。pow(2,2) 表示 2 的 3 次方,结果为 8。

有关指数函数的用法就不再展开,与上面例子是类似,找准输入输出区间再作映射即可。函数曲线的使用是非常灵活的。不仅可以单独使用,还可以组合使用。例如两个基本函数进行相加和相成,都会得到意想不到的效果。

延展

现在仅仅靠指数函数与三角函数,就可以产生各种不同的函数曲线。下面代码就是指数函数与三角函数的叠加,它使得小球加速靠近的同时,能有一个来回的摆动。最终产生了带弹性的动画效果。

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 的运动算法 )

总结

要尽可能理解函数曲线的特性。就需要多加实验。函数曲线可不仅仅只能用在运动动画上。下面用了 5 种常用函数输出了几组 gif。分别控制图形的位置,颜色,旋转角度,大小。可以去从中感受不同函数曲线的个性。

  • 【 1 】线性递增(匀速变化)

  • 【 2 】sin 函数(区间 -PI/2 到 PI/2,从加速到减速)

  • 【 3 】指数函数(减速)

  • 【 4 】指数函数叠加 cos 函数(整体减速)

  • 【 5 】sin 函数(往复)

( 控制位置 )

( 控制透明度 )

( 控制旋转角度 )

( 控制大小 )

最后附上一张由 Kynd 整理的一张图,里面的函数曲线都很实用,有兴趣可以到此地址下载高清大图,了解更多函数曲线  ( http://thebookofshaders.com/05/kynd.png )

补充

函数曲线非常实用,但如果在程序中每次使用都要考虑各种映射关系,显然有点繁琐。更好的做法是把一些常用的函数曲线用一个类把它封装起来。

下面分享一段自己创作时常用到的类(代码基于C++,框架 openframeworks)

class WenzyAni{ public: float ratio; // 内部表示完成进度 (范围一般为 0 到 1) float startVal,endVal; // 开始的数值,结束的数值 float val; // 当前的数值 float time; // 完成整个动画所需的时间 int aniMode; // 决定数值的变化曲线类型 bool startMoving; // 是否开始运动 float startTick; // 开始的时刻记录 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){            // 先加速后减速(经过 sin 函数处理)            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){            // 持续减速(指数衰减)            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 式往复            float n = 2; // n 表示往复次数            val = ofMap(cos(ratio * n * 2 * PI + PI),1,-1,startVal,endVal);        }    } } void start(){    startMoving = true;    startTick = ofGetElapsedTimef(); } };

应用范例 01

ofApp.h 内 —-

#include “WenzyAni.h” ... WenzyAni ani; ofEasyCam cam;

ofApp.cpp 内 —-

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 函数为触发动画的函数。运行程序后按数字键 1 即开始执行,可以看到正方体将从左运动到右,并且带一点弹性动画。这是因为 setup 中有一句

    ani = WenzyAni(1, -300, 300,3)

    它将 ani 对象初始化时。第一个参数表示整个动画的运行时间,第二个参数表示初始的数值,第三个参数表示结束时的数值。第四个参数表示选择应用的曲线类型

  • draw 函数通过 ani.val 来表示正方体的横坐标

  • 每按下一次数字键 1 执行 start 函数时,正方体的运动都会从左变化到右。这是因为 startVal 与 endVal 的值在初始化时已经确定。如果希望正方形实现从右到左的运动,则需要重新初始化。按下数字键 3 就能实现这一效果。而来回按数字键 2,3 则能实现往复运动。

  • 按数字键 4,5,6 可以切换不同的曲线

(数字键 4)

(数字键 5)

(数字键 6)

应用范例 02

下面再附上上篇文章中展示的几个 gif 源码,还是使用同样的类

ofApp.h 内 —-

#include “WenzyAni.h” ... vector<WenzyAni> ani; int showMode;

ofApp.cpp 内 —-

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; // 计算间隙占方块的大小比        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; // 计算间隙占方块的大小比        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; // 计算间隙占方块的大小比        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; // 计算间隙占方块的大小比        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);    } }

运行效果:

代码浅析:

  • 按 r 键开始动画,按方向键上下切换不用的模式

  • 模块中只列举了少数函数曲线,根据个人需要可以拓展补充

End

个人日常中还是倾向于通过自定义函数来使用曲线。如果你不想过于深究各类函数曲线的性质,只希望实现具体的效果。也有办法可以直接采用别人定制好的各类运动曲线。最后再推荐两个插件

OF 插件 - ofxAnimatable

下载地址:https://github.com/armadillu/ofxAnimatable

附带的范例:

Processing 插件 - Ani

在 IDE 的 Libraries 菜单中输入 “animation”

又或是通过以下链接手动下载:

http://www.looksgood.de/libraries/Ani/

:)

∑编辑 | Gemini

来源 | InsLab

算法数学之美微信公众号欢迎赐稿

稿件涉及数学、物理、算法、计算机、编程等相关领域,经采用我们将奉上稿酬。

投稿邮箱:math_alg@163.com

算法动画 - 理解函数曲线相关推荐

  1. 算法 | 动画+解析,轻松理解「Trie树」

    Trie这个名字取自"retrieval",检索,因为Trie可以只用一个前缀便可以在一部字典中找到想要的单词. 虽然发音与「Tree」一致,但为了将这种 字典树 与 普通二叉树 ...

  2. 电子凸轮追剪曲线生成算法 算法,理解后可转成其他品牌PLC或任何一种编程语言

    电子凸轮追剪曲线生成算法. 品牌:麦格米特(算法,理解后可转成其他品牌PLC或任何一种编程语言) YID:5850633554519425

  3. 【Unity3D】动画回调函数、动画事件、动画曲线

    1 动画回调函数 动画回调函数是指动画在开始时.执行中.结束时回调的函数,主要有:OnStateEnter.OnStateUpdate.OnStateExit.OnStateMove.OnStateI ...

  4. 透过WebGL 3D看动画Easing函数本质

    50年前的这个月诞生了BASIC这门计算机语言,回想起自己喜欢上图形界面这行,还得归功于当年在win98下用QBASIC照葫芦画瓢敲了一段绘制奥运五环的代码,当带色彩的奥运五环呈现在自己面前时我已知道 ...

  5. Android开发 之 曲线运动动画(贝塞尔曲线)

    曲线运动动画(贝塞尔曲线) 贝塞尔曲线:维基百科中这样说到:在数学的数值分析领域中,贝塞尔曲线(英语:Bézier curve)是计算机图形学中相当重要的参数曲线.更高维度的广泛化贝塞尔曲线就称作贝塞 ...

  6. Interview:算法岗位面试—11.15下午上海某航天***公司(国企)技术面之工业机器视觉认知、计算机视觉算法的理解、目标检测相关项目案例

    ML岗位面试:11.15下午上海某航天***公司(国企)技术面之工业机器视觉认知.计算机视觉算法的理解.目标检测相关项目案例 Interview:算法岗位面试-11.15下午上海某航天***公司(国企 ...

  7. matlab碎纸拼接相似函数,基于蒙特卡洛算法构建能量函数的碎纸图片拼接方法

    基于蒙特卡洛算法构建能量函数的碎纸图片拼接方法 [专利摘要]本发明提供了一种基于蒙特卡洛算法构建能量函数的碎纸图片拼接方法,主要涉及双面打印文件的拼接及复原问题,通常由于图片较多,信息量较大,故通常为 ...

  8. React中diff算法的理解

    React中diff算法的理解 diff算法用来计算出Virtual DOM中改变的部分,然后针对该部分进行DOM操作,而不用重新渲染整个页面,渲染整个DOM结构的过程中开销是很大的,需要浏览器对DO ...

  9. MATLAB学习系列--绘制函数曲线

    函数与分段函数 绘制函数曲线 直接使用M文件 使用函数 function 分段函数定义和使用 法1:分段函数的输入参数是单个的X值 法2:定义分段函数的输入参数是向量X 绘制函数曲线 直接使用M文件 ...

最新文章

  1. D3.js可视化库入门视频教程
  2. 【无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(7.2) 模块管理,模块的添加、修改、删除...
  3. 操作系统(十二)线程的实现方式、多线程模型
  4. markdowndd
  5. chrom禁用浏览器回退按钮不管用_什么?作为程序员你都工作了还不会用Git
  6. Unity3D_NGUI_安卓APK安装包瘦身实践
  7. 3013-04-13 腾讯笔试
  8. BZOJ 1270: [BeijingWc2008]雷涛的小猫( dp )
  9. 用VC创建程序启动画面
  10. html checked属性值,HTML复选框的checked属性的值是多少?
  11. 如何修复在Microsoft Azure中“虚拟机防火墙打开,关闭RDP的连接端口”问题
  12. PSIM软件学习---04 子电路的创建
  13. SpringCloud工作笔记047---FastJson解析多级JSON_FastJson解析嵌套JSON_FastJson对于JSON格式字符串、JSON对象及JavaBean之间的相互转换
  14. JS设计模式与开发实践
  15. python搭建下载/上传服务器
  16. 学习通信原理之——什么是傅立叶级数
  17. 学计算机的女生选择公务员还是考研,女生本科毕业!考研好,还是考公务员更好?...
  18. LPMS-B2 数据采集源码分析
  19. 即时通讯,1 天快速集成 支持单群聊、聊天室、系统通知等通信能力,安全可靠、 全球互通
  20. Scrapy绕过反爬虫策略汇总

热门文章

  1. git 拉取远程其他分支代码_git切换远程分支并拉取远程分支代码
  2. android layoutparams,Android LayoutParams用法解析
  3. PID控制器开发笔记之四:梯形积分PID控制器的实现
  4. yii schema.mysql.sql_YII学习,初体验 ,对YII的一些理解.
  5. 软件使用手册模板_我的印象笔记使用手册(精简说明)
  6. IOC操作Bean管理注解方式(注入属性@Autowired和Qualifier)
  7. 前端做后台管理系统有前途吗_关于后台管理系统前端项目的思考
  8. JAVA头部声明异常,Java 异常详解
  9. python条件循环叠加_Python基础:条件判断与循环的两个要点
  10. python进阶-Python 进阶用法 (持续更新)