《每周一点 canvas 动画》——3D 物理效果

3,186 阅读5分钟
原文链接: segmentfault.com

每周一点canvas动画代码文件

在上一节《每周一点canvas动画》——3维环境搭建中我们详细的介绍了要想在2D的画布上实现立体效果,需要做哪些事情。也就是我们所说的,怎样给画布中的物体搭建一个可以做三维运动的环境。这之后的所有知识和应用都是基于此环境来运行的。所以,务必弄懂。

到现在为止,你可能发现我们所搭建的三维环境只是针对物体的大小变化做了一定的处理。但是,除了物体的大小,三维环境中,物体的颜色,物体的在光源环境下所展现出来的复杂形态等都没有涉及。当然,我们的初衷是把它作为简单3D效果的降级方案,那些复杂的变化可能以后会用涉及。在本节中我们开始探讨如何在我们的三维环境中实现我们2D环境中的一些物理效果。

1.速度与加速度

二维的环境中,我们使用vx,vy来表示物体的水平速度,与竖直速度。在三维的环境中,我们多了一个维度。所以,只需要给物体设置一个Z轴方向的速度vz。同理,加速度也是一样的道理。下面我们来看看具体的效果图。

在DEMO中,通过方向键控制小球x轴和y轴的速度,通过Shift和Ctrl控制z轴速度。具体代码如下:

   
   

为了加深对上节代码的理解,我在这里列出了第一个示例的详细代码。代码虽然有点长,但是结构清晰,原理简单。首先我们初始化小球的3维坐标和速度。然后,通过按键控制在各个轴上的速度增减,最后在动画循环中改变小球的位置。和2D画布中的一样,非常简单。你只需要注意三维环境设置那一块,如果不明白请看前一节的详细解释。

2.反弹

2.1 单物体反弹

在2D的环境中,我们设置canvas的边界为物体的反弹边界。同样在,三维的环境中我们也可以设置一个边界,但这个边界不再是简单的平面,你可以把它想象成立方体的六个面,那我们的边界就可以设置成下面这样:

    top = -100
    bottom = 100
    left = -100
    right = 100
    front = -100
    back = 100

注意,我们的坐标系默认z的朝向是垂直屏幕向里。我们所要做的与在2D平面上要做的一样,除了下x, y轴的碰撞检测,只是多加了一条z轴的检测。

if (xpos + ball.radius > right) {
     xpos = right - ball.radius;
     vx *= bounce;
   } else if (xpos - ball.radius < left) {
     xpos = left + ball.radius;
     vx *= bounce;
   }
   if (ypos + ball.radius > bottom) {
     ypos = bottom - ball.radius;
     vy *= bounce;
   } else if (ypos - ball.radius < top) {
     ypos = top + ball.radius;
     vy *= bounce;
   }
   if(zpos + ball.radius > back){
       zpos = back -ball.radius;
       vz *= bounce;
   }else if(zpos - ball.radius < front){
       zpos = front + ball.radius;
       vz *= bounce;
   }

具体代码请看:bouncing-3d.html,下面是效果图。为了让碰撞效果明显些,我特意给小球加上了尾迹,在源码中你可以删掉。

2.2 多物体反弹

新建球类文件ball3d.js,具体代码在源文件中查找,我们只是在ball.js文件的基础上给小球增加了第三纬度的坐标变量和速度变量。唯一的变化如下:

this.xpos = 0;
this.ypos = 0;
this.zpos = 0;
this.vz = 0;
this.vx = 0;
this.vy = 0;

接下来,都是老套路。引入新文件ball3d.js,创建球体实例,将边界代码封装成函数,最后每个球体都引用该函数。下面是核心代码块,具体代码请查看multiple-bouncing.html

 function move(ball){
         ball.xpos += ball.vx;
         ball.ypos += ball.vy;
         ball.zpos += ball.vz;
               
         /*.......边界碰撞检测代码,与上部分一样.......*/
         
         //3D环境设置
          if(ball.zpos > -fl){
             var scale = fl/(fl + ball.zpos);
             ball.scaleX = ball.scaleY = scale;
             ball.x = vpX + ball.xpos * scale;
             ball.y = vpY + ball.ypos * scale;
             ball.visible = true;
          }else{
              ball.visible = false;
          }
          //绘制小球
          if(ball.visible){
              ball.draw(context);
          }
}

(function drawFrame(){
          window.requestAnimationFrame(drawFrame, canvas);
          context.clearRect(0, 0, canvas.width, canvas.height);
          
          //看这里,套路
          balls.forEach(move);
}())

效果图如下:

3. Z-sort

仔细观察上面的动态图,你会发现当半径小的球体出现在半径大的球体的前面时,几乎感觉不到立体效果。这是因为上面的代码是按照balls数组的顺序来绘制小球。所以,就会出现我们看到的情况。那么,如何解决这个问题呢?答案是我们需要按照小球在z轴上的位置来绘制它们

这里要介绍的函数你不会陌生——Array.sort(fn),我们经常用它来做数组的排序。参数fn是一个函数,具体事例如下:

var arr = [9,7,3,2,4];
arr.sort(function(a, b){
    return a - b;
});
console.log(arr); // [2,3,4,7,9]

其中,a - b < 0  a排在b前面
     a - b = 0   a,b顺序不变
     a - b > 0   b排在a前面

那么,要在z轴上做出相应的排序,我们只需要比较物体的zpos

 function sort(a, b){
       return (b.zpos - a.zpos);
 }

根据深度顺序,索引值大的物体会出现在比它低的物体的上面。按照上面的方法排序,也就是由高到低排序。得到的结果就是最远处的物体先绘制,最近的最后绘制,近处的物体会绘制在所有的小球之上。如果你采用 a.zpos - b.zpos,那么得到的结果就是最近的物体最先绘制,最远的物体最后绘制,导致远处的物体遮盖了近处的物体。
具体效果如下:

具体代码请查看z-sort.html

ok,本节到这就结束了,下一节我们介绍更多的3D物理效果实现,敬请期待!