>

由于这个库还处在开发阶段澳门博发娱乐官网:

- 编辑:澳门博发娱乐官网 -

由于这个库还处在开发阶段澳门博发娱乐官网:

“等一下,我碰!”——常见的2D碰撞检测

2017/02/22 · HTML5 · 1 评论 · 碰撞检测

原文出处: 凹凸实验室   

澳门博发娱乐官网 1

“碰乜鬼嘢啊,碰走晒我滴靓牌”。想到“碰”就自然联想到了“麻将”这一伟大发明。当然除了“碰”,洗牌的时候也充满了各种『碰撞』。

好了,不废话。直入主题——碰撞检测。

在 2D 环境下,常见的碰撞检测方法如下:

  • 外接图形判别法
    • 轴对称包围盒(Axis-Aligned Bounding Box),即无旋转矩形。
    • 圆形碰撞
  • 光线投射法
  • 分离轴定理
  • 其他
    • 地图格子划分
    • 像素检测

下文将由易到难的顺序介绍上述各种碰撞检测方法:外接图形判别法 > 其他 > 光线投射法 > 分离轴定理。

另外,有一些场景只要我们约定好限定条件,也能实现我们想要的碰撞,如下面的碰壁反弹:

当球碰到边框就反弹(如x/y轴方向速度取反)。

JavaScript

if(ball.left < 0 || ball.right > rect.width) ball.velocityX = -ball.velocityX if(ball.top < 0 || ball.bottom > rect.height) ball.velocityY = -ball.velocityY

1
2
if(ball.left < 0 || ball.right > rect.width) ball.velocityX = -ball.velocityX
if(ball.top < 0 || ball.bottom > rect.height) ball.velocityY = -ball.velocityY

再例如当一个人走到 100px 位置时不进行跳跃,就会碰到石头等等。

因此,某些场景只需通过设定到适当的参数即可。

Three.js是一个比较伟大的webgl开源库,它简化了浏览器3D编程,使得使用JavaScript在浏览器中创建复杂的场景变得容易很多。Github上众多webgl demo令我兴奋不已,跃跃欲试。由于这个库还处在开发阶段,因此资料非常匮乏,爱好者大部分时间不得不通过阅读该库的源码进行学习,我现在也准备这样做。

简介

人工智能(Artificial Intelligence) ,英文缩写为AI。它是研究、开发用于模拟、延伸和扩展智能的理论、方法、技术及应用系统的一门新的技术科学。本篇从严格意义上说属于人工智能的范畴,但也是基础中的基础。本篇的目的是要赋予小球解散和集合两项基本指令(智商),本篇内容中相关算法适用于子弹追踪等塔防类游戏当中。

系列简介
也许,三百年前的艾萨克·牛顿爵士(Sir Issac Newton, 1643-1727)并没幻想过,物理学广泛地应用在今天许多游戏、动画中。为什么在这些应用中要使用物理学?笔者认为,自我们出生以来,一直感受着物理世界的规律,意识到物体在这世界是如何"正常移动",例如射球时球为抛物线(自旋的球可能会做成弧线球) 、石子系在一根线的末端会以固定频率摆动等等。要让游戏或动画中的物体有真实感,其移动方式就要符合我们对"正常移动"的预期。
今天的游戏动画应用了多种物理模拟技术,例如运动学模拟(kinematics simulation)、刚体动力学模拟(rigid body dynamics simulation)、绳子/布料模拟(string/cloth simulation)、柔体动力学模拟(soft body dynamics simulation)、流体动力学模拟(fluid dynamics simulation)等等。另外碰撞侦测(collision detection)是许多模拟系统里所需的。
本系列希望能介绍一些这方面最基础的知识,继续使用JavaScript做例子,以即时互动方式体验。
本文简介 作为系列第一篇,本文介绍最简单的运动学模拟,只有两条非常简单的公式。运动学模拟可以用来模拟很多物体运动(例如马里奥的跳跃、炮弹等),本文将会配合粒子系统做出一些视觉特效(粒子系统其实也可以用来做游戏的玩法,而不单是视觉特效)。
运动学模拟
运动学(kinematics)研究物体的移动,和动力学(dynamics)不同之处,在于运动学不考虑物体的质量(mass)/转动惯量(moment of inertia),以及不考虑加之于物体的力(force )和力矩(torque)。
我们先回忆牛顿第一运动定律:
当物体不受外力作用,或所受合力为零时,原先静止者恒静止,原先运动者恒沿着直线作等速度运动。该定律又称为「惯性定律」。此定律指出,每个物体除了其位置(position)外,还有一个线性速度(linear velocity)的状态。然而,只模拟不受力影响的物体并不有趣。撇开力的概念,我们可以用线性加速度(linear acceleration)去影响物体的运动。例如,要计算一个自由落体在任意时间t的y轴座标,可以使用以下的分析解(analytical solution):
澳门博发娱乐官网 2
当中,和分别是t=0时的y轴起始座标和速度,而g则是重力加速度(gravitational acceleration)。
这分析解虽然简单,但是有一些缺点,例如g是常数,在模拟过程中不能改变;另外,当物体遇到障碍物,产生碰撞时,这公式也很难处理这种不连续性(discontinuity) 。
在计算机模拟中,通常需要计算连续的物体状态。用游戏的用语,就是计算第一帧的状态、第二帧的状态等等。设物体在任意时间t的状态:位置矢量为、速度矢量为、加速度矢量为。我们希望从时间的状态,计算下一个模拟时间的状态。最简单的方法,是采用欧拉方法(Euler method)作数值积分(numerical integration):
澳门博发娱乐官网 3
欧拉方法非常简单,但有准确度和稳定性问题,本文会先忽略这些问题。本文的例子采用二维空间,我们先实现一个JavaScript二维矢量类:

Three.js是一个比较伟大的webgl开源库,它简化了浏览器3D编程,使得使用JavaScript在浏览器中创建复杂的场景变得容易很多。Github上众多webgl demo令我兴奋不已,跃跃欲试。由于这个库还处在开发阶段,因此资料非常匮乏,爱好者大部分时间不得不通过阅读该库的源码进行学习,我现在也准备这样做。

外接图形判别法

这是第一篇笔记,先从最基础的核心(Core)对象开始。
Core::Vector2
该构造函数用来创建一个表示二维向量的对象

基础类

二维向量(2D vector)可谓2D游戏或是动画里最常用型别了。这里二维向量用Vector2类实现,用(x, y)表示。 Vector2亦用来表示空间中的点(point),而不另建类。先看代码:

 1  (function(window) {

 2     var Vector2 = function(x, y) {
 3         this.x = x || 0;
 4         this.y = y || 0;
 5     };
 6     Vector2.prototype = {
 7         set: function(x, y) {
 8             this.x = x;
 9             this.y = y;
10             return this;
11         },
12         sub: function(v) {
13             return new Vector2(this.x - v.x, this.y - v.y);
14         },
15         multiplyScalar: function(s) {
16             this.x *= s;
17             this.y *= s;
18             return this;
19         },
20         divideScalar: function(s) {
21             if (s) {
22                 this.x /= s;
23                 this.y /= s;
24             } else {
25                 this.set(0, 0);
26             }
27             return this;
28         },
29         length: function() {
30             return Math.sqrt(this.lengthSq());
31         },
32         normalize: function() {
33             return this.divideScalar(this.length());
34         },
35         lengthSq: function() {
36             return this.x * this.x + this.y * this.y;
37         },
38         distanceToSquared: function(v) {
39             var dx = this.x - v.x,
40             dy = this.y - v.y;
41             return dx * dx + dy * dy;
42         },
43         distanceTo: function(v) {
44             return Math.sqrt(this.distanceToSquared(v));
45         },
46         setLength: function(l) {
47             return this.normalize().multiplyScalar(l);
48         }
49     };
50     window.Vector2 = Vector2;
51 } (window));

使用该类需要特别注意和区分的地方是:

它什么时候代表点、什么时候代表向量。

当其代表向量的时候,它的几何意义是什么?

不能把其当成一个黑盒来调用,需要知其然并知其所以然。

在下面的使用的过程当中,我会特别标注其代表点还是向量;代表向量时,其几何意义是什么?

给小球赋予智商,顾名思义需要小球类:

(function(window) {
    var Ball = function(r, v, p, cp) {
        this.radius = r;
        this.velocity = v;
        this.position = p;
        this.collectionPosition = cp
    }
    Ball.prototype = {
        collection: function(v) {
            this.velocity = this.collectionPosition.sub(this.position).setLength(v)
        },
        disband: function() {
            this.velocity = new Vector2(MathHelp.getRandomNumber( - 230, 230), MathHelp.getRandomNumber( - 230, 230))
        }
    }
    window.Ball = Ball
} (window)); 

其中

小球拥有4个属性,分别是:radius半径、velocity速度(Vector2)、position位置(Vector2)、collectionPosition集合点/小球的家(Vector2)。

小球拥有2个方法,分别是:collection集合、disband解散。

小球的集合方法所传递的参数为集合的速度,因为小球都有一个集合点的属性,所以这里不用再传入集合点/家给小球。

这里详细分析一下collection方法,这也是整个demo的关键代码。

collection: function (v) {
 this.velocity =this.collectionPosition.sub(this.position).setLength(v);
}, 

因为setLength设置向量的长度:

setLength: function (l) {
 return this.normalize().multiplyScalar(l);

 } 

所以collection可以改成:

  this.velocity = this.collectionPosition.sub(this.position).normalize().multiplyScalar(v);

normalize是获取单位向量,也可以改成:

this.collectionPosition.sub(this.position).divideScalar(this.length()).multiplyScalar(v);   

整个Vector2黑盒就全部展现出来,其整个过程都是向量的运算,代表含义如下所示:

this.collectionPosition

                          .sub(this.position)                获取小球所在位置指向小球集合位置的向量;

                          .divideScalar(this.length()) 得到该向量的单位向量;
                           .multiplyScalar(v);               改变该向量的长度。

最后把所得到的向量赋给小球的速度。
上面我们还是用到了解散方法,其过程是帮小球生成一个随机速度,用到了MathHelp类的一个静态方法:

(function (window) {
 var MathHelp = {};
 MathHelp.getRandomNumber = function (min, max) {
 return (min + Math.floor(Math.random() * (max - min + 1)));
 }
 window.MathHelp = MathHelp;

} (window)); 

复制代码 代码如下:

这是第一篇笔记,先从最基础的核心(Core)对象开始。
Core::Vector2
该构造函数用来创建一个表示二维向量的对象

轴对称包围盒(Axis-Aligned Bounding Box)

概念:判断任意两个(无旋转)矩形的任意一边是否无间距,从而判断是否碰撞。

算法:

JavaScript

rect1.x < rect2.x + rect2.width && rect1.x + rect1.width > rect2.x && rect1.y < rect2.y + rect2.height && rect1.height + rect1.y > rect2.y

1
2
3
4
rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.height + rect1.y > rect2.y

两矩形间碰撞的各种情况:
澳门博发娱乐官网 4

在线运行示例(先点击运行示例以获取焦点,下同):

缺点:

  • 相对局限:两物体必须是矩形,且均不允许旋转(即关于水平和垂直方向上对称)。
  • 对于包含着图案(非填满整个矩形)的矩形进行碰撞检测,可能存在精度不足的问题。
  • 物体运动速度过快时,可能会在相邻两动画帧之间快速穿越,导致忽略了本应碰撞的事件发生。

适用案例:

  • (类)矩形物体间的碰撞。

复制代码 代码如下:

粒子生成

写了Vector2、Ball、MathHeper三个类之后,终于可以开始实现一点东西出来!

 1 var ps = [],
 2 balls = [];
 3 function init(tex) {
 4     balls.length = 0;
 5     ps.length = 0;
 6     cxt.clearRect(0, 0, canvas.width, canvas.height);
 7     cxt.fillStyle = "rgba(0,0,0,1)";
 8     cxt.fillRect(0, 0, canvas.width, canvas.height);
 9     cxt.fillStyle = "rgba(255,255,255,1)";
10     cxt.font = "bolder 160px 宋体";
11     cxt.textBaseline = 'top';
12     cxt.fillText(tex, 20, 20);
13 
14     //收集所有像素
15     for (y = 1; y < canvas.height; y += 7) {
16         for (x = 1; x < canvas.width; x += 7) {
17             imageData = cxt.getImageData(20 + x, 20 + y, 1, 1);
18             if (imageData.data[0] > 170) {
19                 ps.push({
20                     px: 20 + x,
21                     py: 20 + y
22                 })
23             }
24         }
25     };
26     cxt.fillStyle = "rgba(0,0,0,1)";
27     cxt.fillRect(20, 20, canvas.width, canvas.height);
28 
29     //像素点和小球转换
30     for (var i in ps) {
31         var ball = new Ball(2, new Vector2(0, 0), new Vector2(ps[i].px, ps[i].py), new Vector2(ps[i].px, ps[i].py));
32         balls.push(ball);
33     };
34 
35     cxt.fillStyle = "#fff";
36     for (i in balls) {
37         cxt.beginPath();
38         cxt.arc(balls[i].position.x, balls[i].position.y, balls[i].radius, 0, Math.PI * 2, true);
39         cxt.closePath();
40         cxt.fill();
41     }
42 
43     //解散:生成随机速度
44     for (var i in balls) {
45         balls[i].disband();
46     }

47 } 

其中分三个步骤:收集所有像素、 像素点和小球转换、生成随机速度。整个demo我们需要一个loop:

 1 var time = 0;
 2 var cyc = 15;
 3 var a = 80;
 4 var collectionCMD = false;
 5 setInterval(function() {
 6     cxt.fillStyle = "rgba(0, 0, 0, .3)";
 7     cxt.fillRect(0, 0, canvas.width, canvas.height);
 8     cxt.fillStyle = "#fff";
 9     time += cyc;
10     for (var i in balls) {
11         if (collectionCMD === true && balls[i].position.distanceTo(balls[i].collectionPosition) < 2) {
12             balls[i].velocity.y = 0;
13             balls[i].velocity.x = 0;
14         }
15     }
16 
17     if (time === 3000) {
18         collectionCMD = true;
19         for (var i in balls) {
20             balls[i].collection(230);
21         }
22     }
23     if (time === 7500) {
24         time = 0;
25         collectionCMD = false;
26         for (var i in balls) {
27             balls[i].disband();
28         }
29     }
30 
31     for (var i in balls) {
32         cxt.beginPath();
33         cxt.arc(balls[i].position.x, balls[i].position.y, balls[i].radius, 0, Math.PI * 2, true);
34         cxt.closePath();
35         cxt.fill();
36         balls[i].position.y += balls[i].velocity.y * cyc / 1000;
37         balls[i].position.x += balls[i].velocity.x * cyc / 1000;
38     }
39 },

40 cyc);  

这里使用time整体控制,使其无限loop。ps:这里还有一点不够OO的地方就是应当为ball提供一个draw方法。

其中的balls[i].position.distanceTo(balls[i].collectionPosition) 代表了点与点之间的距离,这里判断小球是否到了集合点或家。这里其几何意义就不再向量了。

// Vector2.js
Vector2 = function(x, y) { this.x = x; this.y = y; };

复制代码 代码如下:

圆形碰撞(Circle Collision)

概念:通过判断任意两个圆形的圆心距离是否小于两圆半径之和,若小于则为碰撞。

两点之间的距离由以下公式可得:
澳门博发娱乐官网 5

判断两圆心距离是否小于两半径之和:

JavaScript

Math.sqrt(Math.pow(circleA.x - circleB.x, 2) + Math.pow(circleA.y - circleB.y, 2)) < circleA.radius + circleB.radius

1
2
3
Math.sqrt(Math.pow(circleA.x - circleB.x, 2) +
Math.pow(circleA.y - circleB.y, 2))
< circleA.radius + circleB.radius

图例:
澳门博发娱乐官网 6

在线运行示例:

缺点:

  • 与『轴对称包围盒』类似

适用案例:

  • (类)圆形的物体,如各种球类碰撞。

THREE.Vector2 = function ( x, y ) {
this.x = x || 0;
this.y = y || 0;
};

在线演示

这你也敢叫人工智能?ok,未完待续......


Vector2.prototype = {
copy : function() { return new Vector2(this.x, this.y); },
length : function() { return Math.sqrt(this.x * this.x + this.y * this.y); },
sqrLength : function() { return this.x * this.x + this.y * this.y; },
normalize : function() { var inv = 1/this.length(); return new Vector2(this.x * inv, this.y * inv); },
negate : function() { return new Vector2(-this.x, -this.y); },
add : function(v) { return new Vector2(this.x + v.x, this.y + v.y); },
subtract : function(v) { return new Vector2(this.x - v.x, this.y - v.y); },
multiply : function(f) { return new Vector2(this.x * f, this.y * f); },
divide : function(f) { var invf = 1/f; return new Vector2(this.x * invf, this.y * invf); },
dot : function(v) { return this.x * v.x + this.y * v.y; }
};

THREE.Vector2 = function ( x, y ) {
this.x = x || 0;
this.y = y || 0;
};

其他

Vector2对象的功能函数采用定义构造函数的原型对象来实现,形如:

Vector2.zero = new Vector2(0, 0);

Vector2对象的功能函数采用定义构造函数的原型对象来实现,形如:

地图格子划分

概念:将地图(场景)划分为一个个格子。地图中参与检测的对象都存储着自身所在格子的坐标,那么你即可以认为两个物体在相邻格子时为碰撞,又或者两个物体在同一格才为碰撞。另外,采用此方式的前提是:地图中所有可能参与碰撞的物体都要是格子单元的大小或者是其整数倍。

蓝色X 为障碍物:
澳门博发娱乐官网 7

实现方法:

JavaScript

// 通过特定标识指定(非)可行区域 map = [ [0, 0, 1, 1, 1, 0, 0, 0, 0], [0, 1, 1, 0, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 0, 1, 0, 0], [0, 1, 0, 0, 0, 0, 1, 0, 0], [0, 1, 1, 1, 1, 1, 1, 0, 0] ], // 设定角色的初始位置 player = {left: 2, top: 2}   // 移动前(后)判断角色的下一步的动作(如不能前行) ...

1
2
3
4
5
6
7
8
9
10
11
12
13
// 通过特定标识指定(非)可行区域
map = [
[0, 0, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 0, 0, 1, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 0, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 0, 0]
],
// 设定角色的初始位置
player = {left: 2, top: 2}
 
// 移动前(后)判断角色的下一步的动作(如不能前行)
...

在线运行示例:

缺点:

  • 适用场景局限。

适用案例:

  • 推箱子、踩地雷等

复制代码 代码如下:

然后,就可以用HTML5 Canvas去描绘模拟的过程:

复制代码 代码如下:

像素检测

概念:以像素级别检测物体之间是否存在重叠,从而判断是否碰撞。

实现方法有多种,下面列举在 Canvas 中的两种实现方式:

  1. 如下述的案例中,通过将两个物体在 offscreen canvas 中判断同一位置(坐标)下是否同时存在非透明的像素。
  2. 利用 canvas 的 globalCompositeOperation = 'destination-in' 属性。该属性会让两者的重叠部分会被保留,其余区域都变成透明。因此,若存在非透明像素,则为碰撞。

注意,当待检测碰撞物体为两个时,第一种方法需要两个 offscreen canvas,而第二种只需一个。

offscreen canvas:与之相关的是 offscreen rendering。正如其名,它会在某个地方进行渲染,但不是屏幕。“某个地方”其实是内存。渲染到内存比渲染到屏幕更快。—— Offscreen Rendering

当然,我们这里并不是利用 offscreen render 的性能优势,而是利用 offscreen canvas 保存独立物体的像素。换句话说:onscreen canvas 只是起展示作用,碰撞检测是在 offscreen canvas 中进行

另外,由于需要逐像素检测,若对整个 Canvas 内所有像素都进行此操作,无疑会浪费很多资源。因此,我们可以先通过运算得到两者相交区域,然后只对该区域内的像素进行检测即可。

图例:
澳门博发娱乐官网 8

下面示例展示了第一种实现方式:

缺点:

  • 因为需要检查每一像素来判定是否碰撞,性能要求比较高。

适用案例:

  • 需要以像素级别检测物体是否碰撞。

THREE.Vector2.prototype = {
constructor: THREE.Vector2,
set: function ( x, y ) {
this.x = x;
this.y = y;
return this;
},
copy: function ( v ) {
this.x = v.x;
this.y = v.y;
return this;
},
...... // 更多的函数
};

复制代码 代码如下:

THREE.Vector2.prototype = {
constructor: THREE.Vector2,
set: function ( x, y ) {
this.x = x;
this.y = y;
return this;
},
copy: function ( v ) {
this.x = v.x;
this.y = v.y;
return this;
},
...... // 更多的函数
};

光线投射法(Ray Casting)

概念:通过检测两个物体的速度矢量是否存在交点,且该交点满足一定条件。

对于下述抛小球入桶的案例:画一条与物体的速度向量相重合的线(#1),然后再从另一个待检测物体出发,连线到前一个物体,绘制第二条线(#2),根据两条线的交点位置来判定是否发生碰撞。

抛球进桶图例:
澳门博发娱乐官网 9

在小球飞行的过程中,需要不断计算两直线的交点。

当满足以下两个条件时,那么应用程序就可以判定小球已落入桶中:

  • 两直线交点在桶口的左右边沿间
  • 小球位于第二条线(#2)下方

在线运行示例:

优点:

  • 适合运动速度快的物体

缺点:

  • 适用范围相对局限。

适用案例:

  • 抛球运动进桶。

函数set(x,y)用以指定向量的值,调用者本身的x,y值被影响了,而该方法本身又返回调用者本身,这种情况很常见,以下不再说明。通过文字能够表述清楚功能的函数不再引用源代码,这一点以下也不再说明。
函数copy(v)用来将向量v复制进调用者。
函数add(a,b)和函数sub(a,b)分别表示对向量a,b相加和相减。
函数addSelf(v)和subSelf(v)分别表示对调用者本身加上或减去向量v。
函数multiplyScale(s)和divideScale(s)分别表示对调用者本身乘以或除以s。
函数lerpSelf(v,alpha)将调用者向v所指的方向旋转alpha,当alpha为1时,调用者最终等于v,而当alpha=0时,调用者还等于原来。

var position = new Vector2(10, 200);
var velocity = new Vector2(50, -50);
var acceleration = new Vector2(0, 10);
var dt = 0.1;
function step() {
position = position.add(velocity.multiply(dt));
velocity = velocity.add(acceleration.multiply(dt));
ctx.strokeStyle = "#000000";
ctx.fillStyle = "#FFFFFF";
ctx.beginPath();
ctx.arc(position.x, position.y, 5, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
start("kinematicsCancas", step);

函数set(x,y)用以指定向量的值,调用者本身的x,y值被影响了,而该方法本身又返回调用者本身,这种情况很常见,以下不再说明。通过文字能够表述清楚功能的函数不再引用源代码,这一点以下也不再说明。
函数copy(v)用来将向量v复制进调用者。
函数add(a,b)和函数sub(a,b)分别表示对向量a,b相加和相减。
函数addSelf(v)和subSelf(v)分别表示对调用者本身加上或减去向量v。
函数multiplyScale(s)和divideScale(s)分别表示对调用者本身乘以或除以s。
函数lerpSelf(v,alpha)将调用者向v所指的方向旋转alpha,当alpha为1时,调用者最终等于v,而当alpha=0时,调用者还等于原来。

分离轴定理(Separating Axis Theorem)

概念:通过判断任意两个 凸多边形 在任意角度下的投影是否均存在重叠,来判断是否发生碰撞。若在某一角度光源下,两物体的投影存在间隙,则为不碰撞,否则为发生碰撞。

图例:
澳门博发娱乐官网 10

在程序中,遍历所有角度是不现实的。那如何确定 投影轴 呢?其实投影轴的数量与多边形的边数相等即可。

澳门博发娱乐官网 11

以较高抽象层次判断两个凸多边形是否碰撞:

JavaScript

function polygonsCollide(polygon1, polygon2) { var axes, projection1, projection2   // 根据多边形获取所有投影轴 axes = polygon1.getAxes() axes.push(polygon2.getAxes())   // 遍历所有投影轴,获取多边形在每条投影轴上的投影 for(each axis in axes) { projection1 = polygon1.project(axis) projection2 = polygon2.project(axis)   // 判断投影轴上的投影是否存在重叠,若检测到存在间隙则立刻退出判断,消除不必要的运算。 if(!projection1.overlaps(projection2)) return false } return true }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function polygonsCollide(polygon1, polygon2) {
var axes, projection1, projection2
 
// 根据多边形获取所有投影轴
axes = polygon1.getAxes()
axes.push(polygon2.getAxes())
 
// 遍历所有投影轴,获取多边形在每条投影轴上的投影
for(each axis in axes) {
projection1 = polygon1.project(axis)
projection2 = polygon2.project(axis)
 
// 判断投影轴上的投影是否存在重叠,若检测到存在间隙则立刻退出判断,消除不必要的运算。
if(!projection1.overlaps(projection2))
return false
}
return true
}

上述代码有几个需要解决的地方:

  • 如何确定多边形的各个投影轴
  • 如何将多边形投射到某条投影轴上
  • 如何检测两段投影是否发生重叠

复制代码 代码如下:

<button onclick="eval(document.getElementById('kinematicsCode').value)" type="button">Run</button>
<button onclick="stop();" type="button">Stop</button>
<button onclick="clearCanvas();" type="button">Clear</button>
<table border="0" style="width: 100%;">
<tbody>
<tr>
<td><canvas id="kinematicsCancas" width="400" height="400"></canvas></td>
<td width="10"> </td>
<td width="100%" valign="top">
<h4>修改代码试试看</h4>
<li>改变起始位置</li>
<li>改变起始速度(包括方向) </li>
<li>改变加速度</li>

复制代码 代码如下:

投影轴

如下图所示,我们使用一条从 p1 指向 p2 的向量来表示多边形的某条边,我们称之为边缘向量。在分离轴定理中,还需要确定一条垂直于边缘向量的法向量,我们称之为“边缘法向量”。

投影轴平行于边缘法向量。投影轴的位置不限,因为其长度是无限的,故而多边形在该轴上的投影是一样的。该轴的方向才是关键的。

澳门博发娱乐官网 12

JavaScript

// 以原点(0,0)为始,顶点为末。最后通过向量减法得到 边缘向量。 var v1 = new Vector(p1.x, p1.y) v2 = new Vector(p2.x, p2.y)   // 首先得到边缘向量,然后再通过边缘向量获得相应边缘法向量(单位向量)。 // 两向量相减得到边缘向量 p2p1(注:上面应该有个右箭头,以表示向量)。 // 设向量 p2p1 为(A,B),那么其法向量通过 x1x2+y1y2 = 0 可得:(-B,A) 或 (B,-A)。 axis = v1.edge(v2).normal()

1
2
3
4
5
6
7
8
// 以原点(0,0)为始,顶点为末。最后通过向量减法得到 边缘向量。
var v1 = new Vector(p1.x, p1.y)
v2 = new Vector(p2.x, p2.y)
 
// 首先得到边缘向量,然后再通过边缘向量获得相应边缘法向量(单位向量)。
// 两向量相减得到边缘向量 p2p1(注:上面应该有个右箭头,以表示向量)。
// 设向量 p2p1 为(A,B),那么其法向量通过 x1x2+y1y2 = 0 可得:(-B,A) 或 (B,-A)。
axis = v1.edge(v2).normal()

以下是向量对象的部分实现,具体可看源码。

JavaScript

var Vector = function(x, y) { this.x = x this.y = y }   Vector.prototype = { // 获取向量大小(即向量的模),即两点间距离 getMagnitude: function() { return Math.sqrt(Math.pow(this.x, 2), Math.pow(this.y, 2)) }, // 点积的几何意义之一是:一个向量在平行于另一个向量方向上的投影的数值乘积。 // 后续将会用其计算出投影的长度 dotProduct: function(vector) { return this.x * vector.x + this.y + vector.y }, // 向量相减 得到边 subtarct: function(vector) { var v = new Vector() v.x = this.x - vector.x v.y = this.y - vector.y return v }, edge: function(vector) { return this.substract(vector) }, // 获取当前向量的法向量(垂直) perpendicular: function() { var v = new Vector() v.x = this.y v.y = 0 - this.x return v }, // 获取单位向量(即向量大小为1,用于表示向量方向),一个非零向量除以它的模即可得到单位向量 normalize: function() { var v = new Vector(0, 0) m = this.getMagnitude() if(m !== 0) { v.x = this.x / m v.y = this.y /m } return v }, // 获取边缘法向量的单位向量,即投影轴 normal: function() { var p = this.perpendicular() return p .normalize() } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var Vector = function(x, y) {
this.x = x
this.y = y
}
 
Vector.prototype = {
// 获取向量大小(即向量的模),即两点间距离
getMagnitude: function() {
return Math.sqrt(Math.pow(this.x, 2),
Math.pow(this.y, 2))
},
// 点积的几何意义之一是:一个向量在平行于另一个向量方向上的投影的数值乘积。
// 后续将会用其计算出投影的长度
dotProduct: function(vector) {
return this.x * vector.x + this.y + vector.y
},
// 向量相减 得到边
subtarct: function(vector) {
var v = new Vector()
v.x = this.x - vector.x
v.y = this.y - vector.y
return v
},
edge: function(vector) {
return this.substract(vector)
},
// 获取当前向量的法向量(垂直)
perpendicular: function() {
var v = new Vector()
v.x = this.y
v.y = 0 - this.x
return v
},
// 获取单位向量(即向量大小为1,用于表示向量方向),一个非零向量除以它的模即可得到单位向量
normalize: function() {
var v = new Vector(0, 0)
m = this.getMagnitude()
if(m !== 0) {
v.x = this.x / m
v.y = this.y /m
}
return v
},
// 获取边缘法向量的单位向量,即投影轴
normal: function() {
var p = this.perpendicular()
return p .normalize()
}
}

澳门博发娱乐官网 13
向量相减

更多关于向量的知识可通过其它渠道学习。

lerpSelf: function ( v, alpha ) {
this.x += ( v.x - this.x ) * alpha;
this.y += ( v.y - this.y ) * alpha;
return this;
},

</td>
</tr>
</tbody>
</table>

lerpSelf: function ( v, alpha ) {
this.x += ( v.x - this.x ) * alpha;
this.y += ( v.y - this.y ) * alpha;
return this;
},

投影

投影的大小:通过将一个多边形上的每个顶点与原点(0,0)组成的向量,投影在某一投影轴上,然后保留该多边形在该投影轴上所有投影中的最大值和最小值,这样即可表示一个多边形在某投影轴上的投影了。

判断两多边形的投影是否重合:projection1.max > projection2.min && project2.max > projection.min

澳门博发娱乐官网 14
为了易于理解,示例图将坐标轴原点(0,0)放置于三角形边1投影轴的适当位置。

由上述可得投影对象:

JavaScript

// 用最大和最小值表示某一凸多边形在某一投影轴上的投影位置 var Projection = function (min, max) { this.min this.max } projection.prototype = { // 判断两投影是否重叠 overlaps: function(projection) { return this.max > projection.min && projection.max > this.min } }

1
2
3
4
5
6
7
8
9
10
11
// 用最大和最小值表示某一凸多边形在某一投影轴上的投影位置
var Projection = function (min, max) {
    this.min
    this.max
}
projection.prototype = {
    // 判断两投影是否重叠
    overlaps: function(projection) {
        return this.max > projection.min && projection.max > this.min
    }
}

如何得到向量在投影轴上的长度?
向量的点积的其中一个几何含义是:一个向量在平行于另一个向量方向上的投影的数值乘积。
由于投影轴是单位向量(长度为1),投影的长度为 x1 * x2 + y1 * y2

澳门博发娱乐官网 15

JavaScript

// 根据多边形的每个定点,得到投影的最大和最小值,以表示投影。 function project = function (axis) { var scalars = [], v = new Vector()   this.points.forEach(function (point) { v.x = point.x v.y = point.y scalars.push(v.dotProduct(axis)) }) return new Projection(Math.min.apply(Math, scalars), Math.max,apply(Math, scalars)) }

1
2
3
4
5
6
7
8
9
10
11
12
// 根据多边形的每个定点,得到投影的最大和最小值,以表示投影。
function project = function (axis) {
var scalars = [], v = new Vector()
 
this.points.forEach(function (point) {
v.x = point.x
v.y = point.y
scalars.push(v.dotProduct(axis))
})
return new Projection(Math.min.apply(Math, scalars),
Math.max,apply(Math, scalars))
}

函数negate()对调用者取反。
函数dot(v)返回float类型的调用者和向量v的点乘。
函数lengthSq()和函数length()返回float类型的调用者长度平方或长度。
函数normalize()将调用者本身归一化。
函数distanceToSquared(v)和distanceTo(v)将返回调用者和向量v的距离。这里的距离其实是两向量起点都在原点时,终点之间的距离,也就是向量this-v的长度。
函数setLength(s)将向量的长度缩放至为s,方向不变。
函数equals(v)判断调用者与向量v的值是否相同。
函数isZero()判断调用者是否是零向量。
函数clone()返回一个与调用者值一样的新向量,相当于将其复制出去,注意与copy(v)的区别。
Core::Vector3
该构造函数创建一个表示三维向量的对象

这程序的核心就是step()函数头两行代码。很简单吧?
粒子系统
粒子系统(particle system)是图形里常用的特效。粒子系统可应用运动学模拟来做到很多不同的效果。粒子系统在游戏和动画中,常常会用来做雨点、火花、烟、爆炸等等不同的视觉效果。有时候,也会做出一些游戏性相关的功能,例如敌人被打败后会发出一些闪光,主角可以把它们吸收。
粒子的定义
粒子系统模拟大量的粒子,并通常用某些方法把粒子渲染。粒子通常有以下特性:
<li>粒子是独立的,粒子之间互不影响(不碰撞、没有力) </li>
<li>粒子有生命周期,生命结束后会消失</li>
<li>粒子可以理解为空间的一个点,有时候也可以设定半径作为球体和环境碰撞</li>
<li>粒子带有运动状态,也有其他外观状态(例如颜色、影像等) </li>
<li>粒子可以只有线性运动,而不考虑旋转运动(也有例外) </li>

函数negate()对调用者取反。
函数dot(v)返回float类型的调用者和向量v的点乘。
函数lengthSq()和函数length()返回float类型的调用者长度平方或长度。
函数normalize()将调用者本身归一化。
函数distanceToSquared(v)和distanceTo(v)将返回调用者和向量v的距离。这里的距离其实是两向量起点都在原点时,终点之间的距离,也就是向量this-v的长度。
函数setLength(s)将向量的长度缩放至为s,方向不变。
函数equals(v)判断调用者与向量v的值是否相同。
函数isZero()判断调用者是否是零向量。
函数clone()返回一个与调用者值一样的新向量,相当于将其复制出去,注意与copy(v)的区别。
Core::Vector3
该构造函数创建一个表示三维向量的对象

圆形与多边形之间的碰撞检测

由于圆形可近似地看成一个有无数条边的正多边形,而我们不可能按照这些边一一进行投影与测试。我们只需将圆形投射到一条投影轴上即可,这条轴就是圆心与多边形顶点中最近的一点的连线,如图所示:

澳门博发娱乐官网 16

因此,该投影轴和多边形自身的投影轴就组成了一组待检测的投影轴了。

而对于圆形与圆形之间的碰撞检测依然是最初的两圆心距离是否小于两半径之和。

分离轴定理的整体代码实现,可查看以下案例:

优点:

  • 精确

缺点:

  • 不适用于凹多边形

适用案例:

  • 任意凸多边形和圆形。

更多关于分离轴定理的资料:

  • Separating Axis Theorem (SAT) explanation
  • Collision detection and response
  • Collision detection Using the Separating Axis Theorem
  • SAT (Separating Axis Theorem)
  • Separation of Axis Theorem (SAT) for Collision Detection

复制代码 代码如下:

以下是本文例子里实现的粒子类:

复制代码 代码如下:

本文由胜博发-前端发布,转载请注明来源:由于这个库还处在开发阶段澳门博发娱乐官网: