HTML5实验室
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

实验13不规则的密室

简介

在实验8中,处理了小球和四壁的碰撞。当小球与上下两壁碰撞时,沿Y轴的运动方向变成原来的反方向;当小球与左右两壁碰撞时,小球沿X轴运动的方向变成其反方向,即下面这段代码:

        if (ball.r+ball.x > canvas.width || ball.x < ball.r) ball.vx *=-1;
        if (ball.r+ball.y > canvas.height || ball.y < ball.r) ball.vy *=-1;

然而,这是镜面反射的一种特殊情况,因为碰撞面平行于 X 轴或者平行于 Y 轴。本实验要采用向量来解决的就是在所有情况下(即任意碰撞面)的镜面反射处理,理解好这个实验的相关概念对理解后面的物理引擎有很大的帮助。

向量的优势

向量到底有什么优势,为什么用它?在没有体会到向量的好处之前,很多人会按照图2-21所示,写出下面的代码来处理镜面反射。

图2-21 反射分析

如图2-21所示,a点和b点关于反射面对称,b点和d点关于c点对称。要求出反射后的向量,只要经过两次对称处理求出d点坐标,然后用d点坐标减去c点坐标即可得出反射之后的向量。

计算线的对称点:

        function reflectionByLine(line) {
        var cp=line.getVerticalCrossoverPoint(this);
        this.reflectionByPoint(cp);
        returnthis;
                  }

计算点的对称点:

        function reflectionByPoint(point) {
        this.x=2 * point.x-this.x;
        this.y=2 * point.y-this.y;
        returnthis;
                  }

计算垂线与反射面相交的点:

        function getVerticalCrossoverPoint(v) {
        if (this.p2.x===this.p1.x) { returnnew Vector2(this.p1.x, v.y); }
        if (this.p2.y===this.p1.y) { returnnew Vector2(v.x, this.p1.y); }
        var k=(this.p2.y-this.p1.y) / (this.p2.x-this.p1.x);
        var cx=(k * k * this.p1.x+v.y * k+v.x-this.p1.y * k) / (k * k+1);
        var cy=k * cx-k * this.p1.x+this.p1.y;
        returnnew Vector2(cx, cy);
                  }

这种计算完全基于点的坐标,然后求出反射向量。

向量反射则可以大大简化这个过程,并且提高程序的运行效率,如图2-22所示。

图2-22 向量反射

v=v1-2(v1·x×v2·x+v1·y×v2·yv2

其中,v1为入射向量,v2为法向量(单位向量),计算结果v为反射向量。v1·x×v2·x+v1·y×v2·yv1v2上的投影,2(v1·x×v2·x+v1·y×v2·yv2为作用于v1上的向量。

然后,新建一个Vector2类,向量和点公用vector,不再为点另外建立新类。

        Vector2=function (x, y) {
        this.x=x || 0;
        this.y=y || 0;
        };
        Vector2.prototype={
            constructor: Vector2,
            multiplyScalar: function (s) {
        this.x *=s;
        this.y *=s;
        returnthis;
            },
            divideScalar: function (s) {
        if (s) {
        this.x /=s;
        this.y /=s;
                } else {
        this.set(0, 0);
                }
        returnthis;
            },
            dot: function (v) {
        returnthis.x * v.x+this.y * v.y;
        },
            lengthSq: function () {
        returnthis.x * this.x+this.y * this.y;
            },
            length: function () {
        return Math.sqrt(this.lengthSq());
            },
            normalize: function () {
        returnthis.divideScalar(this.length());
            },
            reflectionSelf: function (v) {
        var nv=v.normalize();
        this.subSelf(nv.multiplyScalar(2 * this.dot(nv)));    \注:normalize方法把向量转为单位向量。\
            }
        };

其中,向量a与向量b的单位向量的内积等于向量a到向量b上的投影,可以用如下方式证明。

从定理:

a · b=|a|·|b|·cosθ

可以得到:

其中,b/|b|为向量b的单位向量,|a|cosθ为投影。

理解了概念,下面来进行实验模拟一个密闭空间中N个小球与四壁的完全弹性碰撞(小球之间无碰撞,之后的实验再加入),如图2-23所示。

图2-23 粒子模拟

代码如下:

        var canvas=document.getElementById("mycanvas");
        var cxt=canvas.getContext("2d");
            cxt.fillStyle="#030303";
            cxt.fillRect(0, 0, canvas.width, canvas.height);
        var balls=[];
        function getRandomNumber(min, max) {
        return (min+Math.floor(Math.random() * (max-min+1)))
        }
        for (var i=0; i < 100; i++) {    \注:产生100个速度随机、位置随机的小球。\
        var ball={
                  position: new Vector2(250, 200),
                  r: getRandomNumber(6, 20),
                  v:new Vector2( getRandomNumber(-200, 200), getRandomNumber(-200, 200))
                };
                balls.push(ball);
            }
        var cyc=10;
        var moveAsync=eval(Jscex.compile("async", function () {
        while (true) {
                  cxt.fillStyle="rgba(0, 0, 0, .3)";
                  cxt.fillRect(0, 0, canvas.width, canvas.height);
                  cxt.fillStyle="#fff";
        for (i in balls) {
                      cxt.beginPath();
                      cxt.arc(balls[i].position.x, balls[i].position.y, balls[i].r, 0, Math.PI * 2, true);
                      cxt.closePath();
                      cxt.fill();
        if (balls[i].r+balls[i].position.x > canvas.width || balls[i].position.x < balls[i].r) {
                        balls[i].v.reflectionSelf(new Vector2(1, 0));    \注:法向量为(1,0)。\
                      }
        if (balls[i].r+balls[i].position.y > canvas.height || balls[i].position.y < balls[i].r) {
                        balls[i].v.reflectionSelf(new Vector2(0, 1));    \注:法向量为(0,1)。\
                      }
                      balls[i].position.x +=balls[i].v.x * cyc / 1000;
                      balls[i].position.y +=balls[i].v.y * cyc / 1000;
                  }
                  $await(Jscex.Async.sleep(cyc));
                }
            }))

这样可能体现不出与任意面碰撞的优势,所以在密闭空间增加几条斜线。

        var p1=new Vector2(0, 400);
        var p2=new Vector2(300, 500);
        var p3=new Vector2(600, 400);
        var p4=new Vector2(0, 100);
        var p5=new Vector2(300, 0);
        var p6=new Vector2(600, 100);

在Canvas中绘制出来:

              cxt.strokeStyle="#fff";
              cxt.moveTo(p1.x, p1.y);
              cxt.lineTo(p2.x, p2.y);
              cxt.lineTo(p3.x, p3.y);
              cxt.moveTo(p4.x, p4.y);
              cxt.lineTo(p5.x, p5.y);
              cxt.lineTo(p6.x, p6.y);
              cxt.stroke();

反射处理:

        if (balls[i].position.distanceToLine(p1, p2) < balls[i].r) {
                            balls[i].v.reflectionSelf(Vector2.sub(p1, p2).vertical());
                        }
        if (balls[i].position.distanceToLine(p2, p3) < balls[i].r) {
                            balls[i].v.reflectionSelf(Vector2.sub(p2, p3).vertical());
                        }
        if (balls[i].position.distanceToLine(p4, p5) < balls[i].r) {
                            balls[i].v.reflectionSelf(Vector2.sub(p4, p5).vertical());
                        }
        if (balls[i].position.distanceToLine(p5, p6) < balls[i].r) {
                            balls[i].v.reflectionSelf(Vector2.sub(p5, p6).vertical());
                        }

值得注意的是,这里的碰撞检测变得复杂了,求的是点到直线的距离。已知Ax+By+C=0,则点(x,y)到该直线的距离公式如下:

所以,其中distanceToLine函数如下所示:

        distanceToLine: function (p1, p2) {
        if (p2.x===p1.x) {
        return Math.abs(this.y-p1.y);
                }
        elseif (p2.y===p1.y) {
        return Math.abs(this.x-p1.x);
                }
        else {
        var A=(p2.y-p1.y) / (p2.x-p1.x);
        var B=-1;
        var C=p1.y-A * p1.x;
        return Math.abs(A * this.x+B * this.y+C) / Math.sqrt(A * A+B * B);
                }
            },

其中,p1p2为直线上的两点,this.x和this.y是直线垂线经过的点。

另外一个新增的函数是Vector2.vertical(),用来求自身向量的垂线。

            vertical: function () {
        returnnew Vector2(-this.y, this.x);
            }

即:向量(3, 5)和向量(-5, 3)是垂直的,向量(1, 2)和向量(-2, 1)是垂直的。

通过这个函数也可以求得碰撞面的法线,如:

        Vector2.sub(p1, p2).vertical()

这里求到的就是垂直于线段的法线。

最后运行代码,其效果如图2-24所示。

图2-24 粒子和反射壁模拟