vlambda博客
学习文章列表

三维环境搭建丨每周一点 canvas 动画

在《从 2D 到 3D丨每周一点 canvas 动画》中,我们讨论了要在 2D 的平面实现 3D 的效果,是一件多么复杂的事情。但是对于一些简单的 3D 效果,使用 webGL 不仅有杀鸡用牛刀的感觉,而且浏览器的兼容性也是一个很大的问题。所以,我们考虑在 2D 的 canvas 中去模拟 3D 的效果,将其作为我们项目中的降级方案。

也许你对在 2D 的 canvas 中去模仿 3D 的效果保有怀疑,这里我先给一个小小的 demo,让你直观的感受下,canvas 模拟的 3D 效果到底如何?
是不是很逼真,立体效果不错吧!我记得前段时间淘宝首页有一个简单的 3D 效果(可是我没找到,不过有那么个印象),以为用了 webGL。其实,canvas完全就可以模拟。下面我们介绍 3D 环境的搭建。

1.坐标系统

前面部分的动画内容之所以是2维的,是因为我们所有的动画都基于一个2维坐标系统。要实现3维的动画效果,除了 x轴,y轴,我们还需要另一条坐标轴——z轴。但是,canvas 先天是不具备这条坐标轴的。所以,我们需要手工的去设定这条坐标轴。

在坐标轴的设定上,我们有两种选择,如下图所示:

三维环境搭建丨每周一点 canvas 动画第一种叫做左手坐标系(左手的食指,中指,大拇指三者垂直,大拇指指向自己)

第二种叫做右手坐标系(右手的食指,中指,大拇指三者垂直,大拇指指向外面)

他们之间的区别如图所示,z轴的指向不同。以右手坐标为例,反映到物体的表现上(如果只是考虑物体的大小),当物体朝着z轴的正向运动,那么我们会看到物体变得越来越小。就如第一幅效果图中展示的那样,当物体朝着负方向运动的时候。我们可以看到物体变得越来越大,产生一种朝我们迎面飞来的感觉。

另外,在本文中我们默认使用的是右手坐标系。

2.透视(perspective)

2.1 概念

不管你是叫他景深也好,透视也罢。perspective是 3D 场景中最重要的概念之一,如果你使用过three.js,就会发现camera中有一个参数,就是perspective。另一个你很熟悉的场景,恐怕就是 css3 中的perspective了吧!如果你对这其中的任何一个有了解,那么perspective的概念就很好理解了。

perspective作用是确定物体是靠近我们,还是远离我们。因为在2维的空间中,我们只需要两个坐标就可以确定一个物体在平面上的位置。但是,在3维的环境中这是行不通的,两个物体可能具有相同的x坐标,y坐标。但是只要z坐标不相同,我们就不能判断他们两的位置是不是重合的。

那么,怎么在2维的平面去体现透视效果呢?

各位看官请试想一下,当篮球从远处飞向你的时候(这里如果我们只考虑一个元素——篮球大小),篮球是不是越来越大呢,当你把篮球扔出去,它是不是越来越小呢!Ok,就是这个理。在2维的平面,我们想要让物体感觉向我们走来,就放大它,同理远离的效果就是让它变小。

三维环境搭建丨每周一点 canvas 动画
除了让它的大小发生变化。另一个比较重要的点是:当物体远离直至消失的过程中,要想模拟三维的消失效果,我们必须让物体的x坐标,y坐标向消失点移动这个消失点你可以理解为汽车向远方驶去,最终它会逐渐变成黑点消失在地平线上,这个黑点就是消失点。

三维环境搭建丨每周一点 canvas 动画

所以总结下来,我们在perspective这一块要做两件事:

  1. 放大或者缩小物体

  2. 让它靠近或者远离消失点

2.2 公式

上面的概念中我们了解了要想形成透视的效果,我们需要做两件事。下图展示了一个侧面视图,人眼代表观察点,蓝色的球体代表屏幕中的物体,人眼距离屏幕的距离为fl,物体与屏幕之间有一段距离z值(这个值在成像的时候并不存在,反映到物体大小的变化上)

三维环境搭建丨每周一点 canvas 动画

物体的大小与这fl,z两者之间满足下面的关系:

scale = fl / (fl + z) = 1 / (1 + z/fl)

fl的值就如 css3 中我们设定的perspective值。 同理,这里也是我们自己设定的一个值,200,300,500 都无所谓啦。从公式中我们可以得出:scale 的值一般情况下范围为(0,1.0),这个值之后会用在缩放物体的比例与靠近消失点的比例。

试想两种比较极端的情况:当z轴无限大的时候(也就是朝着z轴的正向持续运动),scale的值就会趋近于0。当z的距离趋近于-fl的时候,scale的值就会变得很大,就像是戳进我们的眼里。

以我们前面使用的小球为例,在draw方法上我们定义

context.scale(this.scaleX, this.scaleY)

当缩放比例确定后,一方面我们确定了球体大小的变化,另一方面我们用物体的坐标乘以这个比例就可以得到物体的新坐标。

可能这样说比较抽象,我们假定fl=200,这时物体的z坐标等于0,由公式可得scale=1.0,那么物体的大小不变,物体的位置不变。如果z=200,那么scale=0.5。物体的大小变为原来的1/2,同时我们要让它现在的坐标乘以缩放比例scale,得到新的位置。如果原来的为(200,300),那么新坐标就为(100,150)。具体效果如下图:


3.代码实现

先上效果图

这里我们让小球的位置跟随鼠标移动,通过键盘的上下键控制小球在z轴上的距离。

<canvas id="canvas" width="500" height="400" style="background:#000;"></canvas>
   <script src="../js/utils.js"></script>
   <script src="../js/ball.js"></script>
   <script>
       window.onload = function(){
           var canvas = document.getElementById('canvas'),
               context = canvas.getContext('2d'),
               ball = new Ball(40, "red"),
               mouse = utils.captureMouse(canvas);

           var xpos = 0,  //物体的3D坐标
               ypos = 0,
               zpos = 0,
               fl = 250,  //距离屏幕的距离(焦距)
               vpX = canvas.width/2,    //消失点
               vpY = canvas.height/2;

           window.addEventListener('keydown', function(e){               
              if(e.keyCode === 38){ //up                   zpos += 5;               }else if(e.keyCode === 40){                   zpos -= 5;               }           }, false);           (function drawFrame(){               window.requestAnimationFrame(drawFrame, canvas);               context.clearRect(0, 0, canvas.width, canvas.height);              
             
              if
(zpos > -fl){                    var scale = fl/(fl + zpos);       //缩放比列                           xpos = mouse.x - vpX;                           ypos = mouse.y - vpY;                   ball.scaleX = ball.scaleY = scale; //物体大小变化                   ball.x = vpX + xpos*scale;          //新坐标                   ball.y = vpY + ypos*scale;                   ball.visible = true;                 //物体可见               }else{                   ball.visible = false               }              
             if(ball.visible){                   ball.draw(context);             }           }())           }

代码相对来说比较简单。首先,我们设定物体的 3D 坐标 xpos,ypos,zpos,初始默认为 0。然后设定焦距fl = 250,最后设置消失点(vpX, vpY)。这里需要注意的地方是,我们设置的消失点为画布的中心。如果不这样做,物体就会向画布的左上角(0,0)处汇集,这并不是我们想要的效果。

接下来,在动画循环中我们根据公式计算缩放比例scale,然后作用于ballscale上,最后计算物体的新位置。这里有个值得注意的点是,我们在外层加了一个判定条件(zpos > -fl),这样做的目的是当物体太大的时候,超出了canvas画布我们就不再绘制它。

这一节的内容是整个3维效果的核心,后面的所有效果都是基于此。所以请务必弄明白!