DX9中的3D拾取

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/DY_1024/article/details/84999231

先来简单介绍一下有关3D拾取的相关概念。我们通常使用鼠标点击屏幕中的物体,然后当我们点中这个物体的时候,物体做出一些反应。比如在游戏中:我们点击地上的宝箱,然后当我们点击到宝箱上面的时候,宝箱打开,这就是一次拾取的过程。

之前的博客有讲解过Direct3D中的绘制流水线,而现在我们现在接触的3D拾取和之前的绘制流水线基本是一次相反的过程,具体的博客链接在这里:DX9的绘制流水线

在三维场景中,我们通过绘制流水线可以知道,假设三维场景中的茶壶经过投射,然后投射到包含有点s的区域中,当我们点击这个点s的时候,就相当于点击到茶壶上面。更准确的说法应该是:三维场景中的茶壶,经过投影,到达了投影窗口中一个包含点p的区域,这个点p和屏幕的点s相对应。

一条经过p点的射线将于那些投影包含p点物体相交,
投影窗口中的p点与屏幕上鼠标单击的点s相对应

由上图我们可以发现,我们从坐标的原点发射一条射线,这条射线经过p点,该射线将会与那些投影包含p点的物体相交,所以我们一旦计算出这条射线,那么我们就可以一次对场景中的物体进行遍历,测试其是否与射线相交,与射线相交的物体就是用户所点击的物体。

因为射线会和投影平面有一个相交的点,然后每一个物体投影到投影平面之后会有一个区域,然后当射线和投影平面相交的点在物体的区域的时候,就表明我们点击了这个物体。

从屏幕到投影窗口

在绘制流水线的时候,我们经过视口变换,将投影窗口的点变换到屏幕上,视口矩阵如下:

视口变换矩阵

对应投影窗口的点p = (Px,Py,Pz)实施视口变换之后,就得到了屏幕上的点s = (Sx,Sy)。

也就是将:点p × 视口矩阵 = 点s。

Tip:拾取变换之后,z坐标并不作为2D图像的一部分进行存储,而是被保存在深度缓存中。

现在我们可以得到下面:

在我们点击屏幕之后,我们是已知点s的坐标的,所以我们就可以推导出Px和Py如下:

一般情况下:视口矩阵的X,Y成员是0(至于为什么是0,目前不是很清除,清楚的大佬可以交流一下),这样我们就进一步得到:

按照定义,投影窗口与平面z = 1重合,所以:Pz = 1。

现在我们得到的这个点就是经过投影矩阵变换之后,但是没有经过视口矩阵变换的这么一个状态。由于投影矩阵会对投影窗口的点进行比例变化以模拟近大远小的情况。所以我们需要做一次比例的逆运算。

设P为投影矩阵,当经过投影矩阵变换之前的点和投影矩阵相乘的时候,矩阵里面P(0,0)和P(1,1)元素就是和坐标相乘的系数,所以有:

现在这个点就是我们没有经过投影变换的点了。

拾取射线的计算

现在我们得到的点就在处于观察坐标系中,在观察坐标系中,我们的摄影机的位置就在坐标的原点。之前我们提到过,可以使用射线来求交,射线的方程我们可以使用p(t) = p0 + tu,其中p0是射线的起点,他描述了射线的位置,u是一个描述了射线方向的向量,现在在观察坐标系中,所以射线的起点p0 = (0,0,0),如果射线经过了投影窗口的点p(Px,Py,Pz)的话,那么射线的方向就是 u = p - p0 = (Px,Py,Pz)-(0,0,0) 。所以方向向量u = p。

下面的这个函数是给定屏幕坐标系中选定的x,y之后,计算拾取射线:

//射线的结构体
struct Ray 
{
	D3DXVECTOR3 _origin;//原点
	D3DXVECTOR3 _direction;//方向
};


d3d::Ray CalcPickingRay(int x, int y)
{
	float px = 0.0f;
	float py = 0.0f;

	D3DVIEWPORT9 vp;//视口的相关信息的矩阵
	Device->GetViewport(&vp);


	D3DMATRIX proj;
	//获取投影矩阵
	Device->GetTransform(D3DTS_PROJECTION, &proj);

	px = (((2.0f * x) / vp.Width - 1.0f) / proj(0, 0));
	py = (((-2.0f * y) / vp.Width + 1.0f) / proj(0, 0));

	d3d::Ray ray;
	ray._origin = D3DXVECTOR3(0.0f,0.0f,0.0f);
	ray._direction = D3DXVECTOR3(px,py,1.0f);

	return ray;
}

再将射线进行变换

刚才我们计算的射线是在观察坐标系中,我们现在判断射线相交,必须将物体和射线搞到同一个坐标系中。但是现在我们不把物体变换到观察坐标系中,因为这会比较难,我们将射线变化到其他坐标系,比如变化到世界坐标系或者局部坐标系。

射线的方程是:p(t) = p0 + tu,只要我们将p0和u分别进行变换,就将射线变换了。Tip:起点是按照点的规则来变换的,而方向u是按照向量来变换的。

变换的具体方法如下:

Tip:里面的T代表矩阵,往世界坐标变换,就传世界矩阵,往什么坐标系传,T就是什么矩阵

void Transformray(d3d::Ray* ray, D3DXMATRIX* T)
{
	//通过给定矩阵变换3D向量,将结果投影回w = 1
	D3DXVec3TransformCoord(&ray->_origin, &ray->_origin, T);

	//通过给定的矩阵变换点
	D3DXVec3TransformNormal(&ray->_direction, &ray->_direction, T);

	//3D矢量的规范化
	D3DXVec3Normalize(&ray->_direction, &ray->_direction);
}

判断射线和物体相交

现在变换之后,我们就将物体和射线计算到了同一个坐标系中,然后开始判断相交,因为我们绘制物体是使用三角形,当然可以计算射线是不是三角形相交,但是三角形太多了,计算量太大,我们采用另外一种计算方式:外接球计算。使用外接球来近似的表示某一个物体,射线和外接球相交,则射线就和物体相交,这种方法虽然不是太精确,但是效率是真鸡儿高。

射线可能会与多个物体相交,我们只识别和摄影机最近的物体,因为之后的物体都被最近的物体挡住了。

给定一个球体的圆,圆心点c和半径r,我们可以用下面的方程来判断点p是不是在圆上面:|| p-c || - r = 0;

如果p满足上面的方程,说明p在圆上面。

为了判断射线p(t) = p0 + tu是不是在圆上,我们可以将点的坐标换成射线的方程,听起来贼二,但是函数中是可以这么搞的。

就得到:|| p0 + tu -c || -r = 0;

这样我们可以导出一个二次方程:At^2 + Bt + C = 0;

A = u* u       B = 2(u*(p0 -c))      c = (p0 - c)*(p0 -c)-r^2

Tip:上面的星号都是点,打不出来点,用星号代替一下

假设u是单位向量,则可以解出t0和t1

to 和 t1可能的几种结果:

a)t0和t1都是复数,射线没有经过球体

b)0和t1都是负数,射线在球体的前方

c)t0和t1是异号的实数,射线的起始点位于球体内

d)t0和t1都是正实数,射线与球体相交

e)t0和t1都是正实数且相等,则射线与球体相切

具体方法如下:

bool RaySphereIntTest(d3d::Ray* ray, d3d::BoundingSphere* sphere)
{
	D3DXVECTOR3 v = ray->_origin - sphere->_center;

	float b = 2.0f - D3DXVec3Dot(&ray->_direction, &v);
	float c = D3DXVec3Dot(&v, &v) - (sphere->_radius * sphere->_radius);
	float discriminant = (b*b) - (4.0f * c);

	if (discriminant < 0.0f)
		return false;

	discriminant = sqrtf(discriminant);

	float s0 = (-b + discriminant) / 2.0f;
	float s1 = (-b + discriminant) / 2.0f;

	if (s0 >= 0.0f || s1 >= 0.0f)
		return true;

	return false;
}

//其中球的结构体定义
struct BoundingSphere
{
	BoundingSphere();

	D3DXVECTOR3 _centor;
	float _radius;
};

猜你喜欢

转载自blog.csdn.net/DY_1024/article/details/84999231
今日推荐