在stage3D中拾取物体

思来想去,我决定先写这篇教程,地形阴影留着明晚再写.拾取物体涉及到射线,这块比较难懂,自认为比地形什么要难得多,于是决定先写拾取物体来梳理下自己的思路.

首先 屏幕上点称为 S(x,y); 视口上的点称为 P(x,y) 实际上在flash中,屏幕和视口的宽高一样,只是中心点屏幕在左上角,视口在屏幕中央.
但这里还是列举下它们的关系

Sx = Px(Width / 2) + X + Width / 2;
变形得出 Px = (2 * Sx) / Width – 1
这公式将屏幕的坐标从左上角移动到 屏幕中央(视口的起点) 后再偏移

Sy = -Py(Height / 2) + Y + Height / 2;
变形得出 Py =-(2 * Sy) / Height + 1
这公式将屏幕的坐标从左上角移动到 屏幕中央(视口的起点) 后再偏移

由于视口是投影的平面,所以我们的Pz 总是 1
所有的物体通过透视矩阵显示在屏幕上的时候,会由于相机缩放会影响到物体的大小.
这些缩放值存储在透视矩阵的 00 和 11 位置即
[00 01 02 03]
[10 11 12 13]
[20 21 22 23]
[30 31 32 33]
因此我们需要把我们求出的 Px, 和Py 除以缩放值,得出经过透视投影后物体坐标

然后我们要定义一个射线类,其中包含了射线的起点 和 射线的方向. 射线的方向是个单位向量.
[cc lang=”actionscript3″]
package
{
import flash.geom.Vector3D;

public class Ray
{
/**起点**/
public var origin : Vector3D;

/**方向**/
public var direction : Vector3D;

public function Ray()
{
}
}
}
[/cc]

射线的起点总是视口的中心点,即屏幕中央. 所以 origin 总是 0,0,0
用一个等式来表示方向向量 u = P – P0; P0即起点, origin.
如果P点是射线穿过投影平面上的点,那么射线的方向向量 u = 投影点P – origin = (Px,Py,1) – (0,0,0) = P; 因此我们只需要直接把投影坐标标准化转换成单位向量表示方向即可.

下面的函数来帮助我们创建一条射线,起点是屏幕中央,方向是点击屏幕后转换到投影平面的坐标标准化.

[cc lang=”actionscript3″]
/**
* 计算射线
*/
private function calcPicingRay(x : int, y : int) : Ray
{
var px : Number = 0;
var py : Number = 0;

var vp : Rectangle = new Rectangle(0,0,stage.stageWidth,stage.stageHeight);

var rawData : Vector. = m_proj.rawData;
px = (((2 * x) / vp.width) – 1.0) / rawData[0];
py = (((-2 * y) / vp.height) + 1.0) / rawData[5];

var ray : Ray = new Ray();
ray.origin = new Vector3D(0,0,0);
ray.direction = new Vector3D(px,py,1);

return ray;
}
[/cc]

在代码中,你可能发现 ray.direction 没有进行标准化 .normalize() .不必担心,在另一个函数里还要对射线进行加工.

现在我们已经有一条射线了,但是它只停留在投影平面上,为了进行跟3D物体的相交测试,我们还需要把射线转换到3D空间中.

做法很简单,只需要把我们的射线右乘相机矩阵即可. 即VM Vector * Matrix; 乘以的结果还是一个向量. 在转换原点时,要将原点向量的分量w = 1. 转换方向时,分量 w = 0;
转换完毕后,我们将方向的标准化 补上.

代码如下
[cc lang=”actionscript3″]
/**
* 转换射线
*/
private function transformRay(ray : Ray, t : Matrix3D) : void
{
//转换射线的原点, w = 1
ray.origin.w = 1;
ray.origin = Utils.subjectMat(ray.origin,t,ray.origin);

//转换射线的方向, w = 0;
ray.direction.w = 0;
ray.direction = Utils.subjectMat(ray.direction,t,ray.direction);

ray.direction.normalize();
}
[/cc]

Utils.subjectMat 是我自己写的向量乘以矩阵的实现函数, 我不知道stage3D中是否有该API,如果有,请告知我是哪个. 这里我也提供下这个函数

[cc lang=”actionscript3″]
//向量左乘矩阵
public static function subjectMat(vec : Vector3D, mat : Matrix3D, out : Vector3D) : Vector3D
{
var data : Vector. = mat.rawData;

if(null == out) out = new Vector3D();

out.x = (vec.x * data[0] + vec.y * data[4] + vec.z * data[8] + vec.w * data[12]);
out.y = (vec.x * data[1] + vec.y * data[5] + vec.z * data[9] + vec.w * data[13]);
out.z = (vec.x * data[2] + vec.y * data[6] + vec.z * data[10] + vec.w * data[14]);
out.w = (vec.x * data[3] + vec.y * data[7] + vec.z * data[11] + vec.w * data[15]);

return out;
}
[/cc]

现在我们有了3D空间中的射线了,可以做相交测试了.
我们可以遍历视口中的所有三角形,判断哪些三角形被射线击中了,虽然很精确,但是性能并不高.
因此在本例中我们用包围球来碰撞,实际上你也可以自己试试AABB碰撞.

给出包围球的圆心C 和 半径R. 使用下面的恒等式就能测试点 P 是否在包围球上.

||P-C|| – R = 0;

向量 P 到 圆心 C 的长度表示为 ||P-C||. 如果等于半径C 表示P在包围球上.
现在把我们的射线公式带入这个相交的公式中 P = P0 +tU;
|| P0 + tU – C || – R = 0;

由此我们我们可以退出一元二次方程
At^2 + Bt + C = 0; 这个公式很熟悉吧~ 貌似初中就学过了.
t0 = (-B + Sqrt(B * B – 4AC) / 2;
t1 = (-B – Sqrt(B * B – 4Ac) / 2;
因为U是表示方向的单位向量,因此A = 1; 带入即可求出 t0,t1
当t0 或者 t1 >= 1 时,表示切线,相交 等情况. <0 表示完全没有相交,或者在物体的前方. 最后我们通过下面的函数来做碰撞检测 由于仅仅是为了做碰撞检测,这里没有具体的实现包围球,但是本例的Cube顶点的间距是 从 -1 ~ 1 而圆心是在 Cube 的中央,因此 Cube的直径是 2. 那么半径 cube.radius 我们就知道是 1 了. [cc lang="actionscript3"] /** * 碰撞检测 */ private function raySphereIntTest(ray : Ray, cube : CubeMesh) : Boolean { var v : Vector3D = ray.origin.subtract(cube.position); var b : Number = 2 * ray.direction.dotProduct(v); var c : Number = v.dotProduct(v) - (cube.radius * cube.radius); var discriminant : Number = (b * b) - (4 * c); if(discriminant < 0) return false; discriminant = Math.sqrt(discriminant); var s0 : Number = (-b + discriminant) / 2; var s1 : Number = (-b - discriminant) / 2; if(s0 >= 0 || s1 >= 0)
return true;

return false;
}
[/cc]

最后的最后我们将这些代码串起来,就是下面的Demo了.
首先是 stage.addEventListener(MouseEvent.CLICK, onClick);
然后是
[cc lang=”actionscript3″]
private function onClick(e:MouseEvent) : void
{
var ray : Ray = calcPicingRay(e.stageX,e.stageY);
var viewInverse : Matrix3D = m_viewMatrix.clone();
viewInverse.invert();
transformRay(ray,viewInverse);

if(raySphereIntTest(ray,m_cubeList[0])){
tip.text = “碰撞”;
}
else{
tip.text = “W,S,A,D 控制飞船移动n方向键控制相机移动”;
}
}
[/cc]

你想问 m_cubeList[0] 是哪个? 我实际上我创建的30个Cube中的第一个,而且为了让我们知道哪个是它,我让它一直旋转.

每次刷新 Cube的位置都会变化.
Demo is Here

发表评论

电子邮件地址不会被公开。 必填项已用*标注