三维环境搭建丨每周一点 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 先天是不具备这条坐标轴的。所以,我们需要手工的去设定这条坐标轴。
在坐标轴的设定上,我们有两种选择,如下图所示:
第一种叫做左手坐标系(左手的食指,中指,大拇指三者垂直,大拇指指向自己)
第二种叫做右手坐标系(右手的食指,中指,大拇指三者垂直,大拇指指向外面)
他们之间的区别如图所示,z轴的指向不同。以右手坐标为例,反映到物体的表现上(如果只是考虑物体的大小),当物体朝着z轴的正向运动,那么我们会看到物体变得越来越小。就如第一幅效果图中展示的那样,当物体朝着负方向运动的时候。我们可以看到物体变得越来越大,产生一种朝我们迎面飞来的感觉。
另外,在本文中我们默认使用的是右手坐标系。
2.透视(perspective)
2.1 概念
不管你是叫他景深
也好,透视
也罢。perspective
是 3D 场景中最重要的概念之一,如果你使用过three.js
,就会发现camera
中有一个参数,就是perspective
。另一个你很熟悉的场景,恐怕就是 css3 中的perspective
了吧!如果你对这其中的任何一个有了解,那么perspective
的概念就很好理解了。
perspective
的作用是确定物体是靠近我们,还是远离我们。因为在2维的空间中,我们只需要两个坐标就可以确定一个物体在平面上的位置。但是,在3维的环境中这是行不通的,两个物体可能具有相同的x坐标,y坐标。但是只要z坐标不相同,我们就不能判断他们两的位置是不是重合的。
那么,怎么在2维的平面去体现透视效果呢?
各位看官请试想一下,当篮球从远处飞向你的时候(这里如果我们只考虑一个元素——篮球大小),篮球是不是越来越大呢,当你把篮球扔出去,它是不是越来越小呢!Ok,就是这个理。在2维的平面,我们想要让物体感觉向我们走来,就放大它,同理远离的效果就是让它变小。
除了让它的大小发生变化。另一个比较重要的点是:当物体远离直至消失的过程中,要想模拟三维的消失效果,我们必须让物体的x坐标,y坐标向消失点移动。这个消失点你可以理解为汽车向远方驶去,最终它会逐渐变成黑点消失在地平线上,这个黑点就是消失点。
所以总结下来,我们在perspective
这一块要做两件事:
放大或者缩小物体
让它靠近或者远离消失点
2.2 公式
上面的概念中我们了解了要想形成透视的效果,我们需要做两件事。下图展示了一个侧面视图,人眼代表观察点
,蓝色的球体代表屏幕中的物体,人眼距离屏幕的距离为fl
,物体与屏幕之间有一段距离z
值(这个值在成像的时候并不存在,反映到物体大小的变化上)
物体的大小与这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
,然后作用于ball
的scale
上,最后计算物体的新位置。这里有个值得注意的点是,我们在外层加了一个判定条件(zpos > -fl)
,这样做的目的是当物体太大的时候,超出了canvas画布我们就不再绘制它。
这一节的内容是整个3维效果的核心,后面的所有效果都是基于此。所以请务必弄明白!