点
数学中的坐标系统(笛卡尔坐标系统)
这里A(0,0), B(1,2), C(4,3), D(–1,2), E(–2,–1), F(3,–2).
计算机中的坐标系统(屏幕坐标系统)
注意这里的y轴在两个坐标系统是相反的。在后面将会研究将笛卡尔坐标系统转换为屏幕坐标系统。
在3D的坐标系中,没有一个工业标准,有的采用y轴向上(y-up)的坐标系,有的采用z轴向上(z-up)的坐标系,有的采用右手坐标系统(right-handed coordinate system),有的采用左手系统(left-handed coordinate system)。
这里我们采用y轴向上的右手坐标系统。
在传统过的数学方法表示中通常是括号加注的方式,如P(1,2,3),但是对于程序员来说,他们更加喜欢这样的表示方式,如p<1,2,3>或者p[1,2,3],因为这样表示可以进行矢量运算,矢量运算后面将会介绍。
点定义:
1
2
3
4
5
6
7
8
9
10
|
function
Point2D(x,y){
this
.x=x;
this
.y = y;
}
function
Point3D(x,y,z){
this
.x=x;
this
.y = y;
this
.z = z;
}
|
两点间的距离
在人工智能程序中,智能体可以通过判断敌人与自己的距离来决定发动攻击的时机。
公式:a2+b2=c2(勾股定理)
2D场景中的距离公式:
设点P1(x1, y1)和P2(x2, y2)分别为线上的点,他们的距离d的计算方法如下:
计算机实现:
1
2
3
4
5
6
7
|
function
distance2D(p1,p2)
{
var
dx=p1.x-p2.x;
var
dy=p1.y-p2.y;
var
distance=Math.sqrt(Math.pow(dx,2)+Math.pow(dy,2));
return
distance;
}
|
计算机实现:
1
2
3
4
5
6
7
8
|
function
distance3D(p1,p2)
{
var
dx=p1.x-p2.x;
var
dy=p1.y-p2.y;
var
dz=p1.z-p2.z;
var
distance=Math.sqrt(Math.pow(dx,2)+Math.pow(dy,2)+Math.pow(dz,2));
return
distance;
}
|
线
直线公式:
Ax + By = C (A和B不能同时为0)
讨论:
A和B不为0:为斜线(不垂直于x轴和y轴);
A为0,B不为0:直线为垂直于y轴的直线,且于y轴交与点(0,C/B);
A不为0,B为0:直线为垂直于x轴的直线,且于x轴交与点(C/A,0);
例:
定义斜率:
1
2
3
4
5
6
7
8
9
|
function
getSlope(p1, p2)
{
var
slope;
if
(p1.x-p2.x!=0)
{
slope=(p1.y-p2.y)/(p1.x-p2.x);
return
slope;
}
}
|
已知:直线A:y1=k1*x1+b1 直线B:y2=k2*x2+b2
两直线相互垂直的条件是:斜率乘积为-1,即k1*k2=-1。
两直线平行的条件是:斜率相等,即k1=k2;当k1=k2且b1=b2直线重合。
两直线相交的条件是:斜率不相等,即k1!=k2;两直线垂直是直线相交的一种特殊情况。
抛物线
如果你投掷一个铅球或者踢足球,它们将做一个抛物线运动。因为它们总是受一个垂直于地面的重力加速度g的作用。下图为四个方向上的抛物线。
公式:
垂直轴向上的抛物线:y=a(x–h)2+k, 顶点为(h,k),关于x=h对称.
水平轴向上的抛物线:x=a(y–k)2+h, 顶点为(h,k),关于y=k对称.
圆
圆的两个关键因素:圆心和半径。
公式:
(x–h)2 + (y–k)2 = r2 圆心为(h,k) 半径为r.
举例:圆心为(1,2),半径为2的圆。
计算机中的实现:
1
2
3
4
5
|
function
Circle(point,r)
{
this
.center = point ;
this
.radius = r;
}
|
圆的碰撞条件:
当两个圆相切或者相交时,我们认为两个圆应该开始发生碰撞。
计算机中的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
fuction collisionBetweenCircles(circle1,circle2)
{
var
hit;
var
powerOfDistanceOfCenter=Math.pow(circle1.center.x-circle2.center.x)+ Math.pow((circle1.center.y-circle2.center.y),2));
var
powerOfDistanceOfRadius=Math.pow(circle1.radius+circle2.radius,2);
if
(powerOfDistanceOfCenter<=powerOfDistanceOfRadius)
{
hit=
true
;
}
else
{
hit=
false
;
}
return
hit;
}
|
弧度
数学表示:
角度:degree=radian*180/π
弧度:radian=degree*π/180
π≈3.141592654
计算机实现
1
2
|
var
RadToDeg=57.29577951,
DegToRad=0.017453293f;
|
矢量
矢量既有大小又有方向。
常用举例:
物理上常用的矢量与标量的对应概念有:速度和速率;距离和位移等。如图:
距离:
假设A到B的距离为a,B到C的距离为b,C到D的距离为c;
那么A到D的距离就为a+b+c。
位移:
假设A到B的位移为a,B到C的位移为b,C到D的位移为c;
那么A到D的位移大小为|a+b+c|(数学上也叫a+b+c的模),方向是从A指向D。
计算机中矢量的表示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//2D Vector
function
Vector2D(x,y)
{
this
.x = x;
this
.y = y;
}
//3D Vector
function
Vector3D(x,y,z)
{
this
.x = x;
this
.y = y;
this
.z = z;
}
|
1矢量加法
设A=a1i+a2j , B=b1i+b2j那么A+B=(a1+b1)i+(a2+b2)j
2.矢量的减法,我们可以把A-B看成A+(-B)。
设A=a1i+a2j , B=b1i+b2j那么A-B=(a1-b1)i+(a2-b2)j
3.点积
A·B=|A| |B| cosq
2D:定义矢量A[a1,a2],矢量B[b1,b2] ;那么点积A·B=a1b1+a2b2。
3D:定义矢量A[a1,a2,a3],矢量B[b1,b2,b3] ;那么点积A·B=a1b1+a2b2+a3b3。
性质:
如果A·B = 0, 那么A┴B。
A·B = B· A。
如果 A·B < 0 (负), 那么q > 90°
如果 A·B > 0 (正), 那么q < 90°
C · D=5(6) + 3(–2) = 30 – 6 = 24
计算机实现:
1
2
3
4
5
6
7
8
9
10
11
12
|
//向量加法
function
addVector(v1,v2){
return
new
Vector(v1.x+v2.x,v1.y+v2.y);
}
//向量减法
function
subVector(v1,v2){
return
new
Vector(v1.x-v2.x,v1.y-v2.y);
}
//点积
function
dotProduct(v1,v2){
return
v1.x*v2.x+v1.y*v2.y;
}
|
绝大多数程序员认为编程是远比数学有意思得多,但是掌握一些数学基本知识可以帮助你解决很多的问题。其中,向量知识是一个“超值”的数学主题。
向量通常被描述为一个既有长度,又有方向的量。如很多物理量一般都是用向量来表示的:动量、速度、加速度、位移······
在以下内容中,我们将沿用CSS/位图的屏幕坐标系统,即左上角为原点、x轴沿右侧增加、y轴沿下方增加(亦称做笛卡尔坐标系)。
向量运算
我们可以把一些有用的运算应用于向量,下面列出了一些简单的向量运算以及可能的应用程序。
加法和减法
我们可以通过加减向量的x,y坐标来实现向量的相加或相减。这就和常规的计算是一样的,所以,一个向量和自己相加可以使它的长度扩大两倍,一个向量减掉它自己会得到零向量。一些简单的例子:
- 给飞行的皮球加上一个重力向量,让它真实地下落
- 把两个碰撞物体的向量加在一起,计算逼真的碰撞反馈
- 给宇宙飞船增加一个火箭推力的向量,让飞船移动
缩放
按照一定比例缩放向量的x、y坐标,你就可以缩小或放大向量的长度。一些实例如下:
- 反复以略小于1的向量缩放移动向量,让对应物体缓慢地停下来
- 将大炮的方向向量扩大,作为一个发射炮弹的初始向量
标准化
有时需要将一个向量变为单位长度,或者说将向量长度变为一个单位。这个过程叫做标准化,单位长度的向量叫做单位向量。当我们感兴趣的是向量的方向而不是长度时,我们经常会这么做。单位向量可以表示:
- 定向喷射的方向
- 斜坡的倾斜方向
- 大炮的发射方向
一旦有了单位向量,我们可以将其缩放,使其代表喷射推力或者炮弹的初始化速度。
旋转
以任意角度来旋转一个向量是非常有用的,因为这可以让你指向任意一个你想要的方向。实例包括:
- 使一个对象始终指向另一个
- 更改一个虚拟的喷射引擎的推力方向
- 根据发射器的方向改变投射体的初始发射方向
在JavaScript中,角是以弧度)(radian)为单位,而不是我们熟悉的角度(degree)指向的。因为弧度看起来不是很直观,我们可以利用JavaScript中的函数很容易将弧度和角度进行转化。
1
2
3
4
5
6
7
8
|
//角度转为弧度
degToRad =
function
(deg){
return
deg * (Math.PI/180);
};
//弧度转为角度
radToDeg =
function
(rad){
return
rad * (180/Math.PI);
};
|
向量的点乘
单位向量的点乘给出两个向量之间角度的余弦,或者说单位向量点乘告诉我们两个向量的方向相近程度。点乘的结果在-1到1之间变化。这是一些特殊点乘含义的例子:
- 向量的方向相同:点积 = 1
- 向量的夹角为45o:点积 = 0.5
- 向量的夹角为90o:点积 = 0
- 向量的夹角为180o:点积 = -1
点乘可以让我们知道两个物体面向对方的程度。比如:在游戏中,我们可以根据点乘判断两个角色是否可以”看到“对方,或者某形状的某一边是否指向某个方向。
创建一个JavaScript向量对角
为了在JavaScript中充分利用向量,我们可以把前面描述的函数总结成一个可以利用的向量对角,这样在程序中使用向量就会非常方便。需要时我们可以加入更多与向量相关的功能。
向量的x
和y分量在对象中被称做为vx
和vy
。这使得我们以后在处理一些含有其他x、y变量的代码时,能够清楚地区分开:
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
50
51
52
53
54
55
56
57
58
59
|
var
vector2d =
function
(x ,y){
var
vec = {
//把x、y保存在对象的vx、vy中
vx: x,
vy: y,
//scale方法可以让我们来放大或缩小向量
scale:
function
(scale){
vec.vx *= scale;
vec.vy *= scale;
},
//向量的加法运算
add:
function
(vec2){
vec.vx += vec2.vx;
vec.vy += vec2.vy;
},
//向量的减法运算
sub:
function
(vec2){
vec.vx -= vec2.vx;
vec.vy -= vec2.vy;
},
//方向取反
negate:
function
(){
vec.vx = -vec.vx;
vec.vy = -vec.vy;
},
//获取向量长度
length:
function
(){
return
Math.sqrt(vec.vx * vec.vx + vec.vy * vec.vy);
},
//获取向量长度的平方
lengthSquared:
function
(){
return
vec.vx * vec.vx + vec.vy * vec.vy;
},
//将向量转化为一个单位向量
normalize:
function
(){
var
len = Math.sqrt(vec.vx * vec.vx + vec.vy * vec.vy);
if
(len){
vec.vx /=len;
vec.vy /=len;
}
//把向量的长度返回
return
len;
},
//向量的旋转
rotate:
function
(angle){
var
vx = vec.vx,
vy = vec.vy,
cosVal = Math.cos(angle),
sinVal = Math.sin(angle);
vec.vx = vx * cosVal - vy * sinVal;
vec.vy = vx * sinVal + vy * cosVal;
},
//toString方法可以把向量以文本形式输出,方便程序调试
toString:
function
(){
return
'('
+ vec.vx.toFixed(3) +
','
+ vec.vy.toFixed(3) +
')'
;
}
};
return
vec;
};
|
使用向量的大炮模拟
我们已经定义了向量对角,可以利用它创建一个简单的大炮模拟。首先,我们应该限定一下”模拟“一词的概念:我们的目标不是尝试绝对真实地复制大炮,而是要创建一个足够现实的模拟,用于程序如游戏。即使游戏中最高级的物理模拟也得在一定程度上去掉一些现实。例如:游戏中的人物角色不模拟直立行走的物理过程,游戏中的飞机也不模拟保持悬浮的物理过程。
严格地说,为了达到精确的模拟效果,你应该在计算中考虑每一帧画面的经过时间。不过在这个演示中,我们假设了一个30毫秒的帧速率。实际上,某些浏览器上的计时器反正不是特别准确,所以缺乏时间的计算是没有多大损失的(相关解决方法你可以参见:游戏开发中的定时器、速度和帧速率相关问题探讨)。
这个模拟使用HTML5 的画布来绘制图形,你也可以加入浏览器中其它的渲染(SVG、CSS3等)。为了使代码集中于对向量的使用和相关计算,这里仅使用了基本的图形。
大炮模拟中使用向量来表示:
- 表示大炮的瞄准方向
- 表示炮弹的运动(最初来源于大炮的瞄准方向)
模拟范围的变量
在主模拟函数的顶部我们定义少量的模拟范围的变量。尽管这些变量在模拟的所有函数中都可用,但是它们被封装在主模拟函数中而并不是全局变量。
1
2
3
|
var
gameObjects = [],
//用来保存游戏对象的数组
canvas = document.getElementById(
'canvas'
),
//canvas的引用
ctx = canvas.getContext(
'2d'
);
|
我们将模拟中的每一个对象(除了背景外)都加入gameObjects[]
数组。然后模拟的主循环遍历整个数组来实现移动和绘制所有对象。
炮弹
我们初始化炮弹时需要传入初始x和y位置以及移动向量。在每一个周期中,我们将向量添加到当前位置,并且给移动向量的y分量加gravity,使炮弹在向前运动的同时向下坠落。在每一个周期内,我们给gravity增加一个固定的量,以模拟重力加速效果。炮弹用一个实心圆来表示。
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
|
var
cannonBall =
function
(x ,y , vectory){
var
gravity = 0,
that = {
x: x,
//初始时X坐标
y: y,
//初始时Y坐标
removeMe:
false
,
//移除标识
//move方法用来更新炮弹位置和检测炮弹是否落地
move:
function
(){
vectory.vy += gravity;
gravity += 0.1;
that.x += vectory.vx;
that.y += vectory.vy;
if
(that.y > canvas.height -150){
that.removeMe =
true
;
}
},
draw:
function
(){
ctx.beginPath();
ctx.arc(that.x, that.y, 5, 0, Math.PI * 2,
true
);
ctx.fill();
ctx.closePath();
}
};
return
that;
};
|
大炮
大炮用安装在车轮上的一个简单的矩形炮管来表示,它的轴始终对准鼠标指针。要计算对鼠标指针的角度,我们使用Math.atan2(y,x)
函数。Math.atan2(y,x)
函数返回水平轴与点(x , y)之间的弧度。假设水平轴通过大炮的支点,输入应该是鼠标指针位置相对于大炮的支点位置:
1
|
angle = Math.atan2(mouseY - cannonY, mouseX - cannonX);
|
当你点击鼠标的时候,大炮会发射一颗炮弹。炮弹会有一个初始位置(大炮的支点)和一个移动向量。我们通过鼠标指针的位置相对于大炮的位置来计算移动的向量。
1
|
vector = vector2d(mouseX - cannonX, mouseY - cannonY);
|
尽管这个向量的方向是正确的,但是它的长度是炮弹到鼠标指针的距离。这是没有用处的,因为这个距离是变化的;不能被简单地放大或者缩小一个固定的量。解决办法是将其标准化为一个单位向量,然后缩放到想要的长度。
1
2
|
vec.normalize();
//标准化为单位向量
vec.scale(25);
//放大到25个单位长度
|
下面是一个完整的大炮对象:
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
50
51
|
var
cannon =
function
(x , y){
var
mx = 0,
my = 0,
angle = 0,
that = {
x: x,
y: y,
angle: 0,
removeMe:
false
,
//move方法计算大炮到鼠标的角度
move:
function
(){
//计算到鼠标的角度
angle = Math.atan2(my - that.y , mx - that.x);
},
draw:
function
(){
ctx.save();
ctx.lineWidth = 2;
//原点移动到炮管的中下方
ctx.translate(that.x , that.y);
//按照move方法中计算的角度进行旋转
ctx.rotate(angle);
//画一个矩形的炮管
ctx.strokeRect(0, -5, 50 ,10);
//在大炮的底部画一个轮子
ctx.moveTo(0, 0);
ctx.beginPath();
ctx.arc(0, 0, 15, 0, Math.PI * 2,
true
);
ctx.fill();
ctx.closePath();
ctx.restore();
}
};
//当鼠标点击时,发射炮弹
canvas.onmousedown =
function
(event){
//创建一个方向从大炮指向鼠标的向量
var
vec = vector2d(mx - that.x, my - that.y);
vec.normalize();
//转化为单位向量
vec.scale(25);
//把向量放大25倍
//创建一个子弹,并添加到gameObjects数组中
gameObjects.push(cannonBall(that.x, that.y, vec));
};
//实时获取鼠标在画布上的位置
canvas.onmousemove =
function
(event){
var
bb = canvas.getBoundingClientRect();
mx = (event.clientX - bb.left);
my = (event.clientY - bb.top);
};
return
that;
}
|
背景
眼光锐利的人很可能已经注意到,炮弹在空中飞的时候有一个尾巴。我们能获得这个效果是因为在蓝天绿地背景上有趣地使用了画布的globalAlpha属性。通常情况下,当我们用画布处理动画时,我们每一帧都需要重绘整个画面来擦除前一帧的图像。如果我们不这样做,画布上会留下之前的痕迹,所有移动图像都会有拖尾。通过指定背景的alpha值,我们只是在一定程度上清除了前一帧。由于这些半透明背景是一层层叠加的,它们最终完全清除了前一帧图像。背景的alpha值越小,尾巴消失所用的时间就越长。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
//在画布是绘制蓝天和草地
var
drawSkyAndGrass =
function
(){
ctx.save();
//设定透明度
ctx.globalAlpha = 0.4;
//创建一个画布渐变的对象
var
linGrad = ctx.createLinearGradient(0, 0, 0, canvas.height);
//顶部从天空蓝开始
linGrad.addColorStop(0,
'#00BFFF'
);
//中间为白色
linGrad.addColorStop(0.5,
'white'
);
//草地绿色
linGrad.addColorStop(0.5,
'#55dd00'
);
//底部过度到白色
linGrad.addColorStop(1,
'white'
);
ctx.fillStyle = linGrad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
};
|
主循环
我们将主循环封闭在setInterval()
的一个函数中。主循环每30毫秒被处理一次,它调用了模拟对象中的move()
函数和draw()
函数,同时还创建一个没有设置removeMe标志的对象列表。所有设置removeMe标志的物体将不再被包括在新列表中,因此就从模拟对象中消失了。炮弹到地平线以下后将用此方法被移除。
页面的布局
这里是大炮模拟的最终页面布局,注意,为了避免重复,这里精简了一些函数代码。只需要用把前面的代码填进来代替就可以了。
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
50
51
52
|
<!doctype html>
<
html
lang
=
"zh-cn"
>
<
head
>
<
meta
charset
=
"UTF-8"
>
<
title
>向量的应用实例</
title
>
<
script
type
=
"text/javascript"
>
window.onload = function(){
var gameObjects = [],
canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d');
var vector2d = function(x, y){
/***向量部分代码***/
};
var cannonBall = function(x, y, vector){
/***代码被精简,请见文章前面部分***/
};
var cannon = function(x, y){
/***代码被精简,请见文章前面部分***/
};
var drawSkyAndGrass = function(){
/***代码被精简,请见文章前面部分***/
};
//添加一个初始的大炮到游戏对象列表中
gameObjects.push(cannon(50, canvas.height-150));
//下面是移动和绘制的主循环
setInterval(function(){
drawSkyAndGrass();
gameObjectsFresh = [];
for(var i=0; i<
gameObjects.length
; i++){
gameObjects[i].move();
gameObjects[i].draw();
if(gameObjects[i].removeMe === false){
gameObjectsFresh.push(gameObjects[i]);
}
}
gameObjects
=
gameObjectsFresh
;
},30);
};
</script>
</
head
>
<
body
>
<
canvas
id
=
"canvas"
width
=
"640"
height
=
"480"
style
=
"border: 1px solid #ccc"
>
您的浏览器不支持canvas绘图,请更新浏览器。
</
canvas
>
</
body
>
</
html
>
|
本文内容来自《JavaScript高效图形编程》。