实验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·y)×v2
其中,v1为入射向量,v2为法向量(单位向量),计算结果v为反射向量。v1·x×v2·x+v1·y×v2·y为v1在v2上的投影,2(v1·x×v2·x+v1·y×v2·y)×v2为作用于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); } },
其中,p1,p2为直线上的两点,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 粒子和反射壁模拟