【转】D3D中详细拾取操作

Direct3D中实现图元的鼠标拾取

 

BY 重剑,2004.5.28 重剑空间

原文链接:http://dev.gameres.com/Program/Visual/3D/pick_2004_529.htm

索引:

1、什么是拾取,拾取能做什么?

2、拾取操作的步骤和实现

2.1.  变换并获得通过视点和屏幕上点击点的射线矢量(Dir)

 

2.1.1 确定鼠标选取点的屏幕坐标

 

2.1.2 得到Dir在观察坐标空间内的表示

 

2.1.3 转换Dir到世界坐标空间,并得到观察点在世界坐标系中的坐标

 

2.2   使用射线矢量对场景中的所有三角形图元求交,获得三角形索引值和重心坐标。

 

2.2.1 D3D扩展函数实现求交

2.2.2射线三角面相交的数学算法

2.2.3  拾取完成根据获得的中心坐标计算我们关心的常见量

3、结束及声明

4、参考文献

 

补充:重心坐标的概念

 

 

 

3D交互图形应用程序中,常常要用鼠标去选择图形,其实现的机制基于鼠标拾取算法。本文主要讲述如何在D3D中实现图元的鼠标拾取。为了讨论简单,本文假定读者理解D3D 坐标变换流程和基本的图形学知识,如果阅读有困难请参考相关资料。

1、什么是拾取,拾取能做什么?

首先,拾取操作指当我们在屏幕上用鼠标点击某个图元应用程序能返回该图元的一个标志和某些相关信息。有图形程序设计经验的人都知道,有这些信息就表示我们有了对该图元的控制权,我们可以删除,可以编辑,可以任意对待该图元,至于你到底想干什么,就是阁下自己的事了^_^。

2、拾取操作的步骤和实现

拾取算法的思想很简单:得到鼠标点击处的屏幕坐标,通过投影矩阵和观察矩阵把该坐标转换为通过视点和鼠标点击点的一条射入场景的光线,该光线如果与场景模型的三角形相交(本文只处理三角形图元),则获取该相交三角形的信息。本文讲述的方法除可以得到三角形的一个索引号以外还可以得到相交点的重心坐标。

从数学角度来看,我们只要得到射线的方向矢量和射线的出射点,我们就具备了判断射线与空间一个三角面是否相交的条件,本文主要讨论如何获得这些条件,并描述了射线三角面相交判断算法和D3D的通常实现方法。

根据拾取操作的处理顺序,大概可以依次分为以下几个步骤

2.1  变换并获得通过视点和屏幕上点击点的射线矢量(Dir)

详细介绍之前,为了大家方便理解,我们要先简单说一下d3d坐标转换的大概流程,如下图:

所以我们要通过一系列的反变换,得到我们关心的值在世界坐标中的表示。

2.1.1 确定鼠标选取点的屏幕坐标

这一步是非常简单的Windows给我们提供了API来完成屏幕坐标的获取,使用GetCursorPos获得鼠标指针位置,然后再利用ScreenToClient转换坐标到客户区坐标系(以窗口视区左上角为坐标原点,单位为像素),设该坐标为(POINT screenPt)。

2.1.2 得到Dir在观察坐标空间内的表示

在观察坐标系中,Dir是一条从观察坐标原点出发的射线,所以我们只需要再确定一个该射线经过的点,就可以得到它在观察坐标系中的表示。假设我们要求的射线上的另外一点为该射线与透视投影平截头体近剪切面的交点,针对最普遍的透视投影而言,透视投影平截头体经投影变换后,变成一个1/2立方体(请允许我这么叫^_^,因为它的大小为一个正方体的一半,x,y方向边长为2,z方向为1)如图:

投影坐标系以近剪切面中心为坐标原点,该立方体从z轴负向看过去与图形程序视区相对应,最终近剪切面(前剪切面)上一点与屏幕坐标之间的对应关系如下图所示:

根据比例关系,screenPt与投影空间上的点projPt之间的关系为

假设图形程序窗口的宽为screenWidth,高为screenHeight,

projPt.x = (screenPt.x-screenWidth/2)/screenWidth*2; (公式1)

projPt.y = (screenPt.y-screenHeight/2)/screenHeight*2; (公式2)

projPt.z =0;(实际该值可任意取,不影响最终结果。为了处理简单,我们取改值为0,表示该点取在近剪切面上)

得到projPt后,我们需要做的是把该点坐标从投影空间转换到观察空间(view space),

根据透视投影的定义,可假设点(projPt.x,projPt.y,projPt.z)

对应的其次坐标为

(projPt.x*projPt.w,projPt.y*projPt.w,projPt.z*projPt.w,projPt.w)

我们可以通过 GetTransform(      D3DTS_PROJECTION,    &ProjMatrix)函数获得投影矩阵ProjMatrix,则根据观察空间到投影空间的变换关系则

(projPt.x*projPt.w,projPt.y*projPt.w,projPt.z*projPt.w,projPt.w)

= (viewPt.x,viewPt.y,viewPt.z, 1)*pProjMatrx;

根据定义和图形学原理

ProjMatrix = =

所以,

(projPt.x*projPt.w,projPt.y*projPt.w,projPt.z*projPt.w,projPt.w)

= ( viewPt.x*ProjMatrix._m11,

viewPt.y*ProjMatrix._m22,

viewPt.z*Q-QZn,

viewPt.z)

所以

projPt.x*projPt.w = viewPt.x*ProjMatrix._m11

projPt.y*projPt.w = viewPt.y*ProjMatrix._m22

projPt.z*projPt.w = viewPt.z*Q-QZn (注意projPt.z = 0)

projPt.w = viewPt.z;

解得

viewPt.x = projPt.x*Zn/ ProjMatrix._m11;

viewPt.y = projPt.y*Zn/ ProjMatrix._m22;

viewPt.z = Zn;

好了,到这里为止我们终于求出了射线与近剪切面交点在观察坐标系中的坐标,现在我们拥有了射线的出发点(0,0,0)和射线方向上另外一点(viewPt.x,viewPt.y,viewPt.z),则该射线的方向矢量在观察空间中的表示可确定为(viewPt.x-0,viewPt.y-0,viewPt.z-0),化简一下三个分量同除近剪切面z坐标Zn,该方向矢量可写作

DIRview = (projPt.x/projMatrix._m11,projPt.y/projMatrix._m22,1)

代入公式1,公式2

DIRview.x = (2*screenPt.x/screenWidth-1)/projMatrix._m11;

DIRview.y = (2*screenPt.y/screenHeight-1)/projMatrix._m22;

DIRview.z = 1;

其中screenWidth和screenHeight可以通过图像显示的backBuffer的目标表面(D3DSURFACE_DESC)来获得,该表面在程序初始化时由用户创建。

2.1.3 转换Dir到世界坐标空间,并得到观察点在世界坐标系中的坐标

由于最终的运算要在世界坐标空间中进行,所以我们还需要把矢量DIRview从观察空间转换为世界坐标空间中的矢量DIRworld。

因为

DIRview = DIRworld*ViewMatrix;

其中ViewMatrix为观察矩阵,在D3D中可以用函数GetTransform( D3DTS_VIEW, &ViewMatrix )得到。

所以DIRworld = DIRview * inverse_ViewMatrix,其中inverse_ViewMatrix为

ViewMatrix的逆矩阵。

观察点在观察坐标系中坐标为OriginView(0,0,0,1),所以其在世界坐标系中的坐标同样可以利用ViewMatrix矩阵,反变换至世界坐标系中,事实上我们可以很简单的判断出,其在世界坐标系中的表示为:

OriginWorld = (inverse_ViewMatrix._41,

inverse_ViewMatrix._42,

inverse_ViewMatrix._43,

1);

到这里为止,判断射线与三角面是否相交的条件就完全具备了。

2.2   使用射线矢量对场景中的所有三角形图元求交,获得三角形索引值和重心坐标。

这一步骤地实现由两种途径:

第一种方法非常简单,利用D3D提供的扩展函数D3DXIntersect可以轻松搞定一切。见2.1

第二种方法就是我们根据空间解析几何的知识,自己来完成射线三角形的求交算法。一般来讲,应用上用第一种方法就足够了,但是我们如果要深入的话,必须理解相交检测的数学算法,这样才能自由的扩展,面对不同的需求,内容见2.2

下面分别讲解两种实现途径:

2.2.1 D3D扩展函数实现求交

这种方法很简单也很好用,对于应用来说应尽力是用这种方式来实现,毕竟效率比自己写得要高得多。

实际上其实没什么好讲的,大概讲一下函数D3DXIntersect吧

D3D SDK该函数声明如下

HRESULT D3DXIntersect(      

    LPD3DXBASEMESH pMesh,
    CONST D3DXVECTOR3 *pRayPos,
    CONST D3DXVECTOR3 *pRayDir,
    BOOL *pHit,
    DWORD *pFaceIndex,
    FLOAT *pU,
    FLOAT *pV,
    FLOAT *pDist,
    LPD3DXBUFFER *ppAllHits,
    DWORD *pCountOfHits
);
l          pMesh指向一个ID3DXBaseMesh的对象,最简单的方式是从.x文件获得,描述了要进行相交检测的三角面元集合的信息,具体规范参阅direct9 SDK
l          pRayPos 指向射线发出点
l          pRayDir 指向前面我们辛辛苦苦求出的射线方向的向量
l          pHit 当检测到相交图元时,指向一个true,不与任何图元相交则为假
l          pU 用于返回重心坐标U分量
l          pV返回重心坐标V分量
l          pDist 返回射线发出点到相交点的长度
注意:以上红色字体部分均指最近的一个返回结果(即*pDist最小)
l          ppAllHits用于如果存在多个相交三角面返回相交的所有结果
l          pCountOfHits 返回共有多少个三角形与该射线相交

 

补充:重心坐标的概念

其中pU和pV用到了重心坐标的概念,下面稍作描述

一个三角形有三个顶点,在迪卡尔坐标系中假设表示为V1(x1,y1,z1),V2(x2,y2,z2),V3(x3,y3,z3),则三角形内任意一点的坐标可以表示为 pV = V1 + U(V2-V1) + V(V3-V1),所以已知三个顶点坐标的情况下,任意一点可用坐标(U,V)来表示,其中 参数U控制V2在结果中占多大的权值,参数V控制V3占多大权值,最终1-U-V控制V1占多大权值,这种坐标定义方式就叫重心坐标。

2..2.2射线三角面相交的数学算法

使用d3d扩展函数,毕竟有时不能满足具体需求,掌握了该方法,我们才能够获得最大的控制自由度,任意修改算法。

已知条件: 射线源点orginPoint,三角形三个顶点 v1,v2,v3,射线方向 Dir

(均以三维坐标向量形式表示)

算法目的: 判断射线与三角形是否相交,如果相交求出交点的重心坐标(U,V)和射线原点到交点的距离T。

我们可先假设射线与三角形相交则交点(注以下均为向量运算,*数乘,dot(X,Y) X,Y 点乘,cross(X,Y)X,Y叉乘;U,V,T为标量)

则:

IntersectPoint = V1 + U*(V2-V1) + V*(V3-V1) ;

IntersectPoint = originPoint + T*Dir;

所以

orginPoint + T*Dir = V1 + U*(V2-V1) + V*(V3-V1);

整理得:

这是一个简单的线性方程组,若有解则行列式不为0。

根据T,U,V的含义当T>0, 0<U<1,0<V<1,0<U+V<1时该交点在三角形内部,

解此方程组即可获得我们关心的值,具体解法不再赘述,克莱姆法则就够了(详细见线性代数):射线原点到相交点的距离T,和交点的中心坐标(U,V)。

下面给出Direct 9 SDK示例程序中的实现代码

IntersectTriangle( const D3DXVECTOR3& orig,

const D3DXVECTOR3& dir, D3DXVECTOR3& v0,

D3DXVECTOR3& v1, D3DXVECTOR3& v2,

FLOAT* t, FLOAT* u, FLOAT* v )

{

// 算出两个边的向量

D3DXVECTOR3 edge1 = v1 - v0;

D3DXVECTOR3 edge2 = v2 - v0;

D3DXVECTOR3 pvec;

D3DXVec3Cross( &pvec, &dir, &edge2 );

// 如果det为0,或接近于零则射线与三角面共面或平行,不相交

//此处det就相当于上面的,

FLOAT det = D3DXVec3Dot( &edge1, &pvec );

D3DXVECTOR3 tvec;

if( det > 0 )

{

tvec = orig - v0;

}

else

{

tvec = v0 - orig;

det = -det;

}

if( det < 0.0001f )

return FALSE;

// 计算u并测试是否合法(在三角形内)

*u = D3DXVec3Dot( &tvec, &pvec );

if( *u < 0.0f || *u > det )

return FALSE;

// Prepare to test V parameter

D3DXVECTOR3 qvec;

D3DXVec3Cross( &qvec, &tvec, &edge1 );

//计算u并测试是否合法(在三角形内)

*v = D3DXVec3Dot( &dir, &qvec );

if( *v < 0.0f || *u + *v > det )

return FALSE;

/*计算t,并把t,u,v放缩为合法值(注意前面的t,v,u不同于算法描述中的相应量,乘了一个系数det),注意:由于该步运算需要使用除法,所以放到最后来进行,避免不必要的运算,提高算法效率*/

*t = D3DXVec3Dot( &edge2, &qvec );

FLOAT fInvDet = 1.0f / det;

*t *= fInvDet;

*u *= fInvDet;

*v *= fInvDet;

return TRUE;

}

2.2.3  拾取完成根据获得的中心坐标计算我们关心的常见量,。

根据重心坐标(U,V),我们可以很容易的算出各种相关量比如纹理坐标和交点的差值颜色,假设以纹理坐标为例设V1,V2,V3的纹理坐标分别为T1(tu1,tv1),T2(tu2,tv2),T3(tu3,tv3)则交点的坐标为

IntersectPointTexture = T1 + U(T2-T1) + V(T3-T1)

3、结束及声明

Ok, 到这里为止关于拾取的相关知识就介绍完了,小弟第一次写这种文章,不知道有没有把问题说清楚,希望对大家有所帮助,有任何问题可以给我发email: [email protected]

或者到我的网站留言: www.heavysword.com

声明:

本文写作的目的是为了广大D3D学习者方便学习服务,文中算法为作者参考相关文献总结,作者无意把这些据为自己的成果,所有权原算法提出者所有(参阅参考文献),文中代码为D3d SDK的示例内容,由笔者进行了必要的解释,代码版权归microsoft所有。

4、参考文献

【1】Microsoft DirectX 9.0 SDK,microsoft

【2】fast,Minimun Storage Ray/Triangle Intersection,Tomas Moler,Ben Trumbore

时间: 2024-09-30 08:03:23

【转】D3D中详细拾取操作的相关文章

opengl中拾取操作的实现

opengl采用一种比较复杂的方式来实现拾取操作,即选择模式.选择模式是一种绘制模式,它的基本思想是在一次拾取操作时,系统会根据拾取操作的参数(如鼠标位置)生成一个特定视景体,然后又系统重新绘制场景中的所有图元,但这些图元并不会绘制到颜色缓存中,系统跟踪有哪些图元绘制到了这个特定的视景体中,并将这些对象的标识符保存到拾取缓冲区数组中. 步骤: 1.设置拾取缓冲区:void glSelectBuffer(GLsizei n,GLunint *buff); 2.进入选择模式:指定选择模式采用函数:G

OpenGL中的拾取模式( Picking)

1. Opengl中的渲染模式有三种:(1)渲染模式,默认的模式:(2)选择模式, (3)反馈模式.如下 GLint glRenderMode(GLenum mode) mode可以选取以下三种模式之一:绘制模式(GL_RENDER),选择模式(GL_SELECT),反馈模式(GL_FEEDBACK). 函数的返回值可以确定选择模式下的命中次数或反馈模式下的图元数量. 2. OpenGL进行图形编程的时候,通常要用鼠标进行交互操作,比如用鼠标点选择画面中的物体,我们称之为拾取(Picking).

折腾了两天的拾取操作。。。

啊哈,折腾了两天opengl上的拾取操作,总算是找到问题了. 一开始是能拾取的但是selectbuffer中的记录总是不对,开始还以为只是拾取函数出问题了,然后仔细看了遍红宝书,按照红宝书中的步骤走了一遍,还是出错...然后就开始考虑是不是之前用了glulookat视图变换的问题,索性不用这个函数了,也就是视图变换采用默认,还是出错...然后不断的改代码,不断地debug...终于找到问题了,丫的,在 gluPickMatrix(point.x,viewport[3]-point.y,5.0,5

java中的集合操作类(未完待续)

申明: 实习生的肤浅理解,如发现有错误之处,还望大牛们多多指点 废话 其实我写java的后台操作,我每次都会遇到一条语句:List<XXXXX> list = new ArrayList<XXXXX>(); 但是我仅仅只是了解,list这个类是一个可变长用来存储的对象实例的类,我甚至觉得这个List对象可以理解成数组,但是却又与java中咱们正常理解的数组很多的不同,比如说,他的长度可以随着需要自动增长,比如说,实例化一个List类就和咱们声明数组的时候是不一样的! 今天的实习生活

android中的数据库操作(转)

android中的数据库操作 android中的应用开发很难避免不去使用数据库,这次就和大家聊聊android中的数据库操作. 一.android内的数据库的基础知识介绍 1.用了什么数据库   android中采用的数据库是SQLite这个轻量级的嵌入式开源数据库,它是用c语言构建的.相关简介可以从链接查看. 2.数据库基本知识观花   对于一些和我一样还没有真正系统学习数据库技术的同学来说,把SQL92标准中的一些基本概念.基本语句快速的了解一下,是很有必要的,这样待会用Android的da

js中的DOM操作(一)——查看及设置节点

一.前言 DOM 是 W3C(World Wide Web Consortium)标准.同时也 定义了访问诸如 XML 和 HTML 文档的标准: DOM是一个使程序和脚本有能力动态地访问和更新文档的内容.结构以及样式的平台和语言中立的接口. 在HTML和JavaScript的学习中,DOM操作可谓时重中之重.今天,小编就领着大家来看看DOM操作是个什么样子!! 二.DOM节点  DOM节点分为三大类:元素节点.属性节点.文本节点: 而我们心心念念想知道的DOM树就长酱紫!          

android中的数据库操作【转】

http://blog.csdn.net/nieweilin/article/details/5919013 android中的数据库操作 android中的应用开发很难避免不去使用数据库,这次就和大家聊聊android中的数据库操作. 一.android内的数据库的基础知识介绍 1.用了什么数据库   android中采用的数据库是SQLite这个轻量级的嵌入式开源数据库,它是用c语言构建的.相关简介可以从链接查看. 2.数据库基本知识观花   对于一些和我一样还没有真正系统学习数据库技术的同

EntityFramework中几种操作小结

目前项目中使用到的EntityFramework中几种操作小结,先标记下.没有详细介绍,后续有空的话再补充一些并完善一下. ? 列中加入RowVersion时间戳 ????public class Product????{????????public int Id { get; set; }????????public string Name { get; set; } ????????[Timestamp]????????public Byte[] RowVersion { get; set;

LINQ To SQL在N层应用程序中的CUD操作、批量删除、批量更新

原文:LINQ To SQL在N层应用程序中的CUD操作.批量删除.批量更新 0. 说明 Linq to Sql,以下简称L2S.    以下文中所指的两层和三层结构,分别如下图所示: 准确的说,这里的分层并不是特别明确:(1) 生成的DataContext(Linq t0 SQL Runtime)和Entity是放在一个文件中的,物理上不能切割开来:上图只是展示逻辑上的结构.(2) 拿上图右边的三层结构来说,鉴于第(1)点,UI层就可以跨越BusinessLogic层,直接访问L2S层,这可能