zoukankan      html  css  js  c++  java
  • 【转】游戏程序员的数学食粮05——向量速查表

    原文:http://gad.qq.com/program/translateview/7172922

    翻译:王成林(麦克斯韦的麦斯威尔)  审校:黄秀美(厚德载物)

    这是本系列大家盼望已久的第五篇。如果你对向量了解不多,请先查看本系列的前四篇文章:介绍向量基础向量的几何表示向量的运算

    这篇速查表会列举一些游戏中常见的几何问题,以及使用数学向量解决它们的方法。

    基本向量运算的完整表单

    首先,先复习一下。

    首先我假设你有一个可用的向量类。它的功能大部分集中在2D上,但是3D的原理相同。差别只在于向量乘积,在2D中我假设向量相乘只会返回代表“z”轴的标量。我会特别指出任何只适用于2D或3D的情况。

    严格来说,一个点不是一个向量——但是一个向量可以代表从原点(0,0)到点的距离,那么把向量当做点代表位置就很合理了。

    我预期在该类中你拥有每个分量,以及下列各运算(使用C++风格的标记法,包括算子的重载——但根据你的需求应该很容易将它翻译到任何其它的语言)的使用权限。如果一个运算不可用,你仍可以通过拓展该类或者创建一个“VectorUtils”的类来实现它。以下的例子一般适用于2D向量——但对于3D通常只需简单地按照x、y的形式加入z坐标即可。

    • Vector2foperator+(Vector2f vec):返回两向量的和。(在没有重载的语言中,该函数可能叫做add()。下面几个例子类似。)

    a+b=Vector2f(a.x+b.x,a.y+b.y);

    • Vector2foperator-(Vector2f vec):返回两向量的差

    a-b=Vector2f(a.x-b.x,a.y-b.y);

    • Vector2foperator*(Vector2f vec):返回两向量的逐分量的乘积

    a*b=Vector2f(a.x*b.x,a.y*b.y);

    • Vector2foperator/(Vector2f vec):返回两向量的逐分量的商

    a/b=Vector2f(a.x/b.x,a.y/b.y);

    • Vector2foperator*(float scalar):返回向量所有分量分别乘以一标量参数的结果。

    a*s=Vector2f(a.x*s,a.y*s);

    s*a=Vector2f(a.x*s,a.y*s);

    • Vector2foperator/(float scalar):返回向量所有分量分别除以一标量参数的结果。

    a/s=Vector2f(a.x/s,a.y/s);

    • floatdot(Vector2f vec):返回两向量的点乘

    a.dot(b)=a.x*b.x+a.y*b.y;

    • floatcross(Vector2f vec):(2D情况)返回两向量叉乘(3D向量)的z分量

    a.cross(b)=a.x*b.y-a.y*b.x;

    • Vector3fcross(Vector3f vec):(3D情况)返回两个向量的叉乘。

    a.cross(b)=Vector3f(a.y*b.z-a.z*b.y, a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x);

    • floatlength():返回向量的长度。

    a.length()=sqrt(a.x*a.x+a.y*a.y);

    • floatsquaredLength():返回向量长度的平方。适用于比较两向量的长度,避免了计算平方根。

    a.squaredLength()=a.x*a.x+a.y*a.y;

    • floatunit():返回一个指向同一方向长度为1的向量。

    a.unit()=a/a.length();

    • Vector2fturnLeft():返回向量向左旋转90度的结果。适用于计算法向量。(假设y轴指向上,否则就是向右转)

    a.turnLeft=Vector2f(-a.y,a.x);

    • Vector2fturnRight():返回向量向右旋转90度的结果。适用于计算法向量。(假设y轴指向上,否则就是向左转)
    • a.turnRight=Vector2f(a.y,-a.x);
    • Vector2frotate(float angle):按照特定角度旋转向量。尽管很少能够在向量类中找到,但这是非常有用的运算。等效于乘以一个2x2旋转矩阵

    a.rotate(angle)=Vector2f(a.x*cos(angle)-a.y*sin(angle),a.x*sin(angle)+a.y*cos(angle));

    • floatangle():返回向量指向的角度。

    a.angle()=atan2(a.y,a.x);

    简单的示例——热身

    例1——两点间距离

    也许你知道可以用毕达哥拉斯定理得出,但是向量方法更简单。给定向量a和向量b:

    1

    float distance = (a-b).length();

    例2——矫直(alignment)

    一些情况下你也许想要按照一张照片的中心矫直它。有时要按照它的左上角或者中上点。更广泛来说,为了最大限度地控制矫直线,你可以使用一个两分量从0到1(甚至超出也可以,如果你希望的话)的向量进行任意方向的矫直。

    1

    2

    // imgPos, imgSize and align are all Vector2f

    Vector2f drawPosition = imgPos + imgSize * align

     

    例3——参数化直线方程

    两点定义一条直线,但是该定义有很多玄机。一个处理直线不错的方法是使用参数化方程:一个点(“P0”)和一个方向(“dir”)。

    1

    2

    Vector2f p0 = point1;

    Vector2f dir = (point2 - point1).unit();

     

    有了这个,你可以,例如,你可以得到一个距离p010单位远的点:

    1

    Vector2f p1 = p0 + dir * 10;

     

    例4:——中点和两点间的内延(interpolation)

    给定向量p0和p1。它们的中点是(p0+p1)/2。更广泛来讲,p0和p1定义的线段可以通过线性内延在0到1间改变t来得到

    1

    Vector2f p = (1-t) * p0 + t * p1;

     

     

    在t=0时,你得到p0;在t=1时,你得到p1;在t=0.5时,你得到中点,等等。

     

    例5:找到一线段的法向

    你已经知道如何找到线段的方向了(例3)。将它旋转90度得到法向量,所以对它使用turnLeft()或turnRight()即可得到结果。

     

    使用点乘的投影

    点乘对于计算向量沿一个方向投影的长度具有很大帮助。我们需要向量(“a”)和一个代表投影方向(“dir”)的单位向量(所以确保你首先使用unit())。那么投影长度就是a.dot(dir)。例如,如果a=(3,4),dir=(1,0),那么a.dot(dir)=3。你可以判断这是正确的,因为(1,0)是x轴的方向。实际上a.x恒等于a.dot(Vector2f(1,0)),a.y恒等于a.dot(Vector2f(0,1))。

    因为a和b的点乘还可以被定义为|a||b|cos(alpha)(alpha是两向量夹角),所以如果两者垂直结果为0,如果两者夹角小于90°结果为正,大于90°结果为负。我们可以使用这一点判断两向量是否指向同一大方向。

    如果将点乘的结果乘以方向向量,你会得到沿着那方向的向量的投影——我们称之为“at”(t代表切向)。如果我们做a-at,我们会得到垂直于方向向量的向量——我们称之为“an”(n代表法向)。at+an=a。

     

    例6——决定最靠近dir的方向

    假设你有许多指向不同方向的单位向量,你想要找出哪一个方向最靠近dir。只需找到列表中向量和dir点乘的结果最大的那个。同样,最小的点乘意味着距离最远的那个。

     

    例7——判断两向量的夹角是否小于alpha

    使用上述方程,我们知道如果两向量a和b的单位向量的点乘小于cos(alpha),那么它们之间的夹角小于alpha。

    1
    2
    3
    bool isLessThanAlpha(Vector2f a, Vector2f b, float alpha) {
        return a.unit().dot(b.unit()) < cos(alpha);
    }

     

    例8——判断一点所在的半平面

    假设平面中有任意一点p0,和一个方向(单位)向量,dir。假设一条穿过p0,垂直于dir的无线长的线将平面一分为二:dir指向的半平面和dir没有指向的半平面。那么如何判断一个点p是否在dir指向的那一边呢?记住当向量间夹角小于90°时它们的点乘为正,那么只需做投影然后检查:

    1
    2
    3
    bool isInsideHalfPlane(Vector2f p, Vector2f p0, Vector dir) {
        return (p - p0).dot(dir) >= 0;
    }

     

    例9——迫使一点在半平面内

    和上面范例相似,但是不仅仅是检查,如果投影小于0的话,我们使用它将目标-投影移动到dir方向,这样目标点就在半平面的边缘了。

    1
    2
    3
    4
    5
    Vector2f makeInsideHalfPlane(Vector2f p, Vector2f p0, Vector dir) {
        float proj = (p - p0).dot(dir);
        if (proj >= 0) return p;
        else return p - proj * dir;
    }

     

    例10——检查/迫使一点在一凸多面体内。

    一个凸多面体可以被定义为多个半平面相交所得结果,每个交线作为多面体的边。它们的p0是边上的顶点,它们的dir是边的内面法线向量(例如,如果你按顺时针方向走,那就是turnRight()法向)。一个点在多面体内部当且仅当它在所有的半平面内。同样地,你可以迫使它在多面体内(通过移动到最靠近的边)通过对所有半平面运用makeInsideHalfPlane 算法。[哎呀!其实只有当所有角度>=90°时才管用]

     

    例11——按照给定法线反射一个向量

    弹球类游戏,球撞向一个斜墙面。已知球的速度向量和墙的法向量(见例5)。现实情况下它会如何反弹?简单!只需反射球的法向速度,保持它的切向速度即可。

    1
    2
    3
    4
    5
    Vector2f vel = getVel();
    Vector2f dir = getWallNormal(); // Make sure this is a unit vector
    Vector2f velN = dir * vel.dot(dir); // Normal component
    Vector2f velT = vel - velN; // Tangential component
    Vector2f reflectedVel = velT - velN;

    想要更贴近现实,你可以将velT和velN分别乘以一个代表摩擦力和恢复系数的常数。

     

    例12——抵消沿轴的运动

    有时我们需要将运动限制在一个给定坐标轴内。思路和上面相同:将速度分解到法向和切向上,然后仅保留切线速度。这有助于计算沿轨道运动的人的速度。

     

    旋转

    例13——点绕定点做旋转

    假设我们有一点,rotate()会将点按原点进行旋转。这很有趣,但是也有限制。按任意一定点旋转简单而且实用——只需用点减去定点,也就是将定点平移到原点,然后旋转,然后再把定点加回去。

    1
    2
    3
    Vector2f rotateAroundPivot(Vector2f p, Vector2f pivot) {
        return (pos - pivot).rotate(angle) + pivot;
    }

     

    例14——判断向哪一方向旋转

    假设我们有一个角色想要转身面向敌人。已知他的方向和面向敌人的方向。那么他应该向左转还是向右转?叉乘给出了简单的答案:curDir.cross(targetDir)返回正数如果你应该左转,负数如果你应该右转(返回0如果已经面对他了或者180°背对他)。

     

    其他几何示例

    有一些其它实用的例子,其中没有过多使用向量,但是很有用:

    例15——等距空间到屏幕坐标

    等距游戏中,你知道世界的(0,0)在屏幕的什么位置(让我们称之为原点,使用一个向量代表它),但是你如何知道(x,y)在屏幕的什么位置呢?首先,你需要两个代表坐标基的向量,新x轴和新y轴。对于一个典型的等距游戏来说,它们可以是bx=Vector2f(2,1)和by=Vector2f(-2,1)——它们不一定是单位向量。现在,一切都简单了。

    1
    2
    Vector2f p = getWorldPoint();
    Vector2f screenPos = bx * p.x + by * p.y + origin;

    没错,的确很简单。

    例16——等距屏幕到世界坐标

    相同情况,但是这次你想要知道鼠标滑过哪块儿拼接图(tile)。这要更复杂一些。我们知道(x’,y’)=(x*bx.x+y*by.x,x*bx.y+y*by.y)+原点,所以可以先减去原点,然后对线性方程求解。使用克拉默法则,我们可以聪明地使用2D叉乘(查看文章开始的定义)进行简化:

    1
    2
    3
    4
    5
    Vector2f pos = getMousePos() - origin;
    float demDet = bx.cross(by);
    float xDet = pos.cross(by);
    float yDet = bx.cross(pos);
    Vector2f worldPos = Vector2f(xDet / demDet, yDet / demDet);

    我看多许多人都在用“找到矩形然后查看位图”的方法,现在你不需要了。

  • 相关阅读:
    第15章 在应用程序中使用虚拟内存(1)
    第14章 探索虚拟内存(2)
    第14章 探索虚拟内存(1)
    第13章 Windows内存体系结构
    第12章 纤程(Fiber)
    第11章 Windows线程池(3)_私有的线程池
    第11章 Windows线程池(2)_Win2008及以上的新线程池
    第11章 Windows线程池(1)_传统的Windows线程池
    第10章 同步设备I/O和异步设备I/O(4)_利用I/O完成端口实现Socket通信
    php+JQuery+Ajax简单实现页面异步刷新 (转)
  • 原文地址:https://www.cnblogs.com/mimime/p/6240279.html
Copyright © 2011-2022 走看看