GPU三维图元拾取

GPU三维图元拾取

张嘉华 梁成 李桂清

(华南理工大学计算机科学与工程学院 广州 510640)

(newzjh@126.com)

摘要:本文探讨了两种新颖的在GPU上实现的三维图元拾取方法,第一种方法是场景几何无关的,通过将坐标信息和对象面片指针绘制到一张Render Target型浮点纹理实现三维对象拾取。第二种方法是场景几何依赖的,对逆变换到世界空间的拾取射线与各个几何图元在Geometry Shader下逐一求交.上述方法经过实验能够在约半帧时间内拾取几何图元的指针信息和坐标信息,达到与屏幕象素大小同等的精确度。

关键词:图元拾取、GPU、几何Shader、渲染到纹理、浮点纹理、三维交互

3D Primitive Picking using GPU

Zhang Jiahua Liang Cheng Li Guiqing

(College of Computer Science and Engineering, South China University Of Technology , Guangzhou , 510640)

Abstract This paper presents two novel 3D picking approaches implemented based on GPU.The first approach is scene geometry independent. It picks a 3D Primitive by rendering the position information and point information of geometry primitives to a Render Target floating Texture.The second approach is scene geometry dependent. It calculates the picking ray by inverting the transforms from the world space to the projection space,and then Intersects each primitive with the ray using a Geometry Shader.The approaches can pick the position and point of primitive within half frame time on GPU. Especially the first one can achieve pixel precision.

Key words Primitive Picking,GPU,Geometry Shader,RTT,Floating Texture,3D Interaction

clip_image002

图1:拾取RRT后的浮点纹理,图中RGB颜色值显示出来的是对象的世界坐标值,如左面的对象1顶部比较绿,反映图元的y值坐标比较大,对象的rgb值分别对应xyz坐标,alpha值对应图元指针

三维图元拾取技术,在计算机动画编辑,三维游戏交互过程中经常用到,如鼠标拾取地形或网格上某个图元(点,线,面片)等操作。随着现代GPU的迅速发展,GPU可编程能力带来了很高的灵活性。Shader Model 4.0的GPU中已经可以进行Vertex Shader,Geometry Shader,Pixel Shader三个阶段的GPU编程.Shader是我们自己定义的程序,替代固定渲染管线中的部分流程,实现图形特效和通用计算目的(GPGPU)。因此,过往无法利用GPU加速计算的拾取过程现在可以成为现实.

本文探讨了两种基于GPU的图形拾取方法。第一种方法与场景几何无关,在shader中进行坐标和指针到颜色的转换编码,把世界坐标和对象指针绘制到一张128位R32G32B32A32浮点RenderTarget(Opengl中的PBuffer)型纹理,然后把浮点纹理中的指定象素值拷贝回CPU,判断该象素的值实现几何图元的拾取,第二种方法是场景几何相关的,依赖于场景n元树分割,通过迭代在Geometry Shader中找到相交图元再把结果利用Stream Out特性送回IA缓冲.GPU拾取方法实质上是把GPU用于通用计算目的而非渲染目的.

1. 相关方法

拾取一般都被看作渲染的一个逆运算。主要分为场景几何依赖和场景几何无关两类方法.

场景几何依赖方法是将鼠标选中的点从屏幕坐标系逆变换到世界坐标系,然后把拾取的点看作射线在世界坐标系与各个几何图元求交,找出被选中的物体。其中较有代表性的是姚继权等[1]和王剑等[2]的工作.一般的顶点渲染管线将一个物体从本身的局部空间,转换到统一的世界空间,然后根据视锥转换到观察空间,经过背面剔除、光照、裁剪等处理后,投影(Projection)到二维的平面上,最后根据显示的环境进行视口变换和光栅化,最后在Render Target显示出来.而拾取则是在Render Target上鼠标点击了某点或者圈定了某个范围判断选取了世界空间下那些图元对象,姚继权等[1]和王剑等[2]的工作将拾取的二维坐标加上第三维深度从投影空间逆变换回世界空间下.将逆变换回世界空间的点设为一条射线,跟世界空间内的物体按深度顺序逐个求交.场景几何依赖方法需要考察场景几何的复杂度,如果场景对象网格有较多几何图元,逐一求交的运算量会非常大。姚继权等[1]和王剑等[2]的做法是计算物体的包围球,然后将射线与包围球求交。一般地,场景几何依赖拾取可以分解为下面四个步骤:

(1) 获得屏幕上的点s,找到他对应的投影窗口上的点p;

(2) 计算拾取射线,它是一条从原点出发穿过点p的射线;

(3) 将射线乘以观察矩阵和投影矩阵连乘后的联合矩阵的转置逆矩阵,变换到和模型相同的坐标系中;

(4) 判定物体和射线求交,被穿过的物体就是屏幕上拾取的物体

场景几何无关方法是正向将场景图元的几何信息直接渲染到Render Target,当用户交互点击或圈定拾取图元时直接判断Render Target对应象素位置的图元信息.但是这种方法在浮点纹理和Shader Model 2.0以前由于GPU硬件能力限制一直没法实现.

2. 渲染到浮点纹理的场景几何无关拾取

场景几何无关方法的应用一直受到限制:它需要GPU的可编程能力,需要Shader Model 2.0和浮点型纹理支持;其次是因为每次出发拾取程序的时候都要在后台绘制一张表面,等同于重复绘制了两次场景,不适合频繁连续的拾取动作。

我们方法的核心是把图元的几何信息、指针等相关信息作为它的颜色渲染到后台的一张Render Target型表面上对应帧缓冲做一次特殊的“渲染”。在渲染后我们只要读取表面上相应象素坐标的颜色值就可以得知它相对应图元的信息。整个算法分为在CPU上和在GPU上的两部分。在CPU上C++程序的任务是建立一个后台的表面,然后调用GPU上的程序对物体进行特殊的RTT(Rander to Texture,渲染到纹理),再根据渲染结果读取表面某坐标的颜色并还原信息。具体实现时考虑到纹理有pow of two 和non-pow of two之别,我们的屏幕分辨率一般不为pow of tow,因此可以考虑采用渲染到表面(RRS),用Direct3D9中的CreateRenderTarget()创建一个Render Target型表面,CPU上的主要流程如下:

鼠标点击拾取事件触发下面流程:

(1) 建立一张新的Render Target型临时纹理。

(2) 将当前设备屏幕的内容存入缓冲中,将设备的渲染对象设为该临时纹理。

(3) 做好渲染前的准备,包括从fx文件中读入effect。

(4) 对于每个几何对象在GPU通过Shader进行特殊渲染。

(5) 还原设备信息,设备的渲染对象指向帧缓冲。

(6) 获取临时纹理上拾取区域的象素。

(7) 将获得的象素按定义的格式解码。

我们首先如代码1,定义Vertex Shader的输出内容,定义了一个包括坐标和颜色的结构(struct)。其中pos用于顶点转换,color用于顶点信息存储.

在Vertex Shader中,输入某点的局部坐标,把坐标的值编码成一个颜色的RGB值后作为输出颜色的RGB值,同时将输入坐标向量左乘以世界矩阵、观察矩阵、投影矩阵后得到最终输出的坐标值。在Pixel Shader中,我们将物体指针信息作为颜色的Alpha值,加上Vertex Shader输出的颜色,输出为最终的渲染颜色。

在单个几何图元中,不同顶点有不同的坐标,在Vertex Shader中,我们根据坐标信息进行顶点转换,同时将坐标信息作为颜色值存入VSResult中。主要计算参阅代码1:

struct VSResult

{

float4 pos:POSITION;

float4 color:TEXCOORD0;

};

VSResult VS_main (float3 pos:POSITION)

{

VSResult ret;

ret.color=float4 (pos,1.0f);

float4 worldpos=mul(float4(pos,1),worldmatrix);

ret.pos=mul(worldpos,ViewProjection);

return ret;

}

代码1:场景几何无关拾取方法的Vertex Shader

对于ret.color,前三个RGB值表示输入的位置坐标;而第4个Alpha值用作存储其他信息,暂时填入1.0f,后文将填充图元的指针。利用以上方法,我们可以得到如本文开头的图1的一张标有坐标信息的彩图。

除了坐标外,还可以存储其他信息. 如物体对应的实体或者网格之类的指针。其特点是同一物体的信息是一致的,也就是说相对于Shader中同一次DP(IDirect3DDevice9::DrawPrimitive)或者DIP(IDirect3DDevice9::DrawIndexedPrimitive)调用是一个常量,在Shader中标识uniform,作为一个全局变量,在CPU中通过ID3DXEFFECT::SetFloat()进行设置,再在Pixel Shader中加入到顶点的颜色信息中。Shader中的主要代码如下:

uniform float pobj;

float4 PS_main (float4 color:COLOR):COLOR0

{

return float4(color.rgb,pobj);

}

代码2:场景几何无关拾取方法的Pixel Shader

在C++的程序中,可以把每个网格的指针转换为long值,再转换为float值,每个网格绘制时通过ID3DXEffect::SetFloat()传递到GPU中,如果拾取需要判断到图元,则需要对每个顶点将图元的指针放到顶点的纹理坐标上,作为输入顶点的纹理坐标传输到GPU.

信息解码主要在CPU上完成,因为只需要解析选中的点。基本流程是:用GetRenderTargetData()拷回纹理的一个系统内存副本;然后调用LockRect()锁定该副本纹理;将拾取的点的各个通道值转换成float*读出,其中数组的0123位依次是颜色的RGBA值;最后将象素的Alpha转换为Long值再转换为对象或图元的指针。

在RTT产生的纹理中,我们不仅加入了坐标信息,而且加入了指针信息。通过指针传递我们可以记录关于图元几何的各种信息。但是指针对纹理存贮的精度要求很高,不能存在丝毫误差。在32位的系统中,一个指针占4字节,在C++程序中,我们采用long类型进行传递,而传入Shader时我们采用了float类型。而对于绘制时所用的纹理,我们必须保证A通道有8×4,也就是32位,所以我们采用了D3DFMT_A32B32G32R32F格式,即32Bit IEEE Float格式的纹理。

对于拾取所得的纹理,可以简单地直接调用LockRect()锁定纹理最高精度表面检索纹素(texel)值。然而,直接调用Lock()函数锁定可能会让CPU等待GPU完成当前的绘制操作再进行Lock()操作,为了提高并发性,可以采取readonly标识和GetRenderTargetData()函数把Render Target纹理拷回到系统内存的一个纹理上再检索。GetRenderTargetData()能够把GPU上Render Target的一个纹理直接完整拷贝到内存,再用带D3DLOCK_READONLY和D3DLOCK_DONOTWAIT等标识锁定要拾取的象素。

3. Geometry Shader下场景几何依赖拾取

场景几何依赖的图元拾取方法将屏幕投影坐标下的拾取象素坐标逆投影回世界坐标,与视点一起构成三维射线向量。关键的一步是将该向量与场景几何里面的图元逐一求交判断是否相交,对相交结果根据投影深度取最前的图元作为结果.如果场景对象网格有较多几何图元,逐一求交的运算量会非常大,如果对场景世界空间进行了n元分割,如八叉树分割,那么可以通过迭代逐步缩小与向量相交的空间,达到一定阈值后再对该子空间内的图元逐一求交.

图2描述了本文探讨的GPU场景几何依赖拾取的流程,用户点击鼠标或圈取屏幕一个区域触发拾取流程,首先把拾取的屏幕坐标加上深度转换为投影空间坐标,左乘以投影矩阵的逆矩阵得到观察空间的坐标:

clip_image004 (1)

再左乘观察矩阵的逆矩阵得到世界空间的坐标:

clip_image006 (2)

通过(2)我们得到了拾取点在世界空间的坐标,接着根据观察矩阵的逆矩阵计算拾取射线方向:

clip_image008 (3)

clip_image010

由此,我们得到了世界空间下的拾取射线,需要注意的是Direct3D9用的是左手坐标系,而OpenGL和Direct3D10默认右手坐标系.得到拾取射线后判断场景的图元几何是否复杂是否可以进行空间八叉树分割,如果可以则在Geometry Shader中找出一个合适的八叉树子空间,接着对子空间内所有图元在Geometry Shader根据深度逐一与拾取射线求交,如果不可以八叉树分割则直接把整个场景看作八叉树的一个节点,对节点内所有图元在Geometry Shader根据深度逐一与拾取射线求交.

clip_image011

图2:GPU场景几何依赖拾取流程

Shader Model 4.0以及Geometry Shader的出现使得我们能够在GPU实现图元的几何拾取,Geometry Shader添加了对各种几何图元输入的支持,输入的图元可以是点,线,三角形以及带有邻接图元的这三种图元对象流,我们可以把这个迭代过程和图元求交过程在Geometry Shader实现,然后把结果直接利用Stream Out输出回IA或通过Pixel Shader绘制到某个一个象素的Render Target,再检索该象素的值.

在Geometry Shader中,输入是整个世界空间的两个极限位置坐标vMin,vMax,迭代调用IntersecAABB()判断AABB的两个对角三角形是否与拾取射线相交,直到求得一个满意大小的八叉子空间,IntersecAABB()首先调用IntersectTriangle()判断该空间的两个对角三角形是否与拾取射线在世界空间相交,如果相交则迭代调用八个子空间的IntersecAABB(),当达到指定大小后输出该子空间的vMin和vMax回IA .在Shader Model 4.0中,Shader的长度不再受到限制,因此可以满足在函数内迭代调用八个子空间IntersecAABB()函数.得到某个子空间后,把该子空间内所有图元根据深度与拾取射线在世界空间内进行比较.代码3描述了这个过程.现代GPU具有很高的计算性能,对于场景几何复杂或不容易进行八叉树空间分割的场景可以直接对拾取射线和各个图元进行求交.整个过程通过ID3D10Device::DrawAuto()实现.

[maxvertexcount(20)]

PS_INPUT GS(triangle GSIn input[2], inout TriangleStream<GSOut> TriangleOutputStream)

{

IntersectAABB(input[0].pos, input[1].pos);

}

bool IntersectAABB(float3 vmin,float3 vmax)

{

if (tilesize<1)

{

TriangleOutputStream .Append(vmin);

TriangleOutputStream .Append(vmax);

TriangleOutputStream.RestartStrip();

}

If (调用IntersectTriangle()判断两个对角三角形是否与拾取射线相交)

{

对8个子空间调用IntersectAABB函数

};

}

bool IntersectTriangle(float3 v0, float3 v1, float3 v2)

{

float3 edge1 = v1 - v0;

float3 edge2 = v2 - v0;

float3 pvec=cross(dir,edge2);

float det=dot(edge1,pvec);

float3 tvec;

if( det > 0 )

{

tvec = orig - v0;

}

else

{

tvec = v0 - orig;

det = -det;

}

return det<0.0001f?false:true;

}

代码3:Geometry Shader中迭代求子空间与判断拾取射线与三角面片是否相交的代码

clip_image014

图2 :DrawAuto()函数描述的概念图[5]。

如图2,当数据利用Stream Out输出到显存SO段缓冲后,能够改变视图把这些数据再次作为Input Assembler输入,DrawAuto()函数会绘制它们,并且无需应用程序知道数据的数量和缓冲的大小.CPU不需要获得数据数量以重新绑定该缓冲从SO段到IA段.尽管无需知道数据数量,但是应用程序需要负责把这些数据从SO段再绑定到IA段的格式输出描述.

4、结果与结论

我们在VC++8.0实现了上述两种方法。在程序中我们添加1个网格,取名为obj1,程序运行后实时渲染该网格,并且在鼠标单击进行拾取操作后弹出对话框说明选中物体名称以及所在坐标。对于图3的场景,可以准确拾取到图元对象的的指针以及图元的世界空间坐标(方法一可以得到对象空间坐标).对于本文方法一,由于等同于再渲染了一次场景几何图元,因此其精度等同于帧缓冲的象素精度,鼠标点击屏幕那个象素就是那个象素对应的图元世界空间坐标,而且不需考虑遮挡投影等几何关系,这些在顺向过程中由渲染流程处理了.而对于本文方法二,其精度为浮点精度.对于同一深度相互部分遮挡的图元如果不求出交点,为了快速计算根据本文代码只判断图元与射线是否相交可能得到多个拾取结果.

clip_image016

图3 三维拾取示例,鼠标拾取了obj1上某个三角形图元,应用程序报告拾取的指针和坐标

我们还和姚继权[1]等工作中描述的射线求交方法进行对比. 它们的方法能够让重叠的物体与射线相交得到交点,根据交点与视点的远近确定拾取的图元,在CPU上实现速度较快.我们在NV FX8800 GPU环境下实现了姚继权[1]的方法,并且在没有构建八叉树的场景中进行了对比,表1是本文描述方法与之的对比.

表1:各种图元拾取方法的对比

时间

场景1:如图2两个模型共含2223个图元

场景2:含120484个图元

每帧时间

3.20毫秒

12.19毫秒

姚继权[1]等的CPU射线求交

0.99毫秒

50.74毫秒

本文方法一:渲染位置和坐标到浮点纹理实现拾取

1.43毫秒

5.14毫秒

本文方法二:在Geometry Shader下实现迭代和求交

0.87毫秒

3.74毫秒

我们通过Win32下的微秒级精度时间检索函数QueryPerformanceFrequency()和QueryPerformanceCounter()检索时间,将每个操作前后两个时间相减得到时间差.对于GPU操作时间目前我们也只是在CPU上通过上述函数统计.在NV FX8800 GPU下对于普通复杂程度场景,如图2每秒绘制312帧场景左右,本文描述的方法一需要再绘制一次场景中的需要拾取的大部分几何图元,但其Vertex Shader与Pixel Shader复杂度略低于渲染时所需要进行光照等计算的复杂度,在GPU进行特殊渲染时间约为0.2845帧(拾取时间/一帧时间,这里用帧比例计算主要为了在不同GPU下能有接近的实验结果),接着通过AGP/PCIE带宽返回结果到CPU,时间约为0.3748帧,由于CPU可能存在停滞和同步等待不同[4],总的拾取时间不能简单把特殊渲染时间加上CPU等待结果时间,需要实际测量,这里共需约0.4452帧=1.43毫秒时间,尽管GPU计算很快.但是存在接近固定的CPU等待GPU传回结果的时间,对于图元较少场景要比姚继权[1]等的CPU方法慢.本文描述的方法二在GPU上利用Geometry Shader加速了迭代和求交,把CPU上图元求交计算移到GPU上进行,其计算时间主要依赖于GPU对不同场景几何复杂度的计算能力,速度要比CPU方法略快.对于较为复杂的场景2,每秒绘制82帧场景,大量图元的绘制对于GPU来说是一件能够快速批量处理(Large Batch and Few DIP Call)的任务,GPU绘制每个图元的速度远快于CPU上对每个图元进行求交的几何计算,因此文本描述的方法一主要时间消耗在从GPU传递结果到CPU,总体拾取时间要比CPU方法短,对于支持Shader Model 4.0的GPU,三角形图元能够利用Geometry Shader大大加快了求交速度,本文描述的方法二在速度上优势更加明显.由此可见,对于现代高性能GPU来说,大量通用目的的计算不会是问题,利用GPU进行拾取对于大量图元场景具有较大速度优势.

参考文献(References)

1. 姚继权,李晓豁. 计算机图形学人机交互中三维拾取方法的研究. 工程设计学报,2006, 13(2): 116-120.

2. 王剑,陆国栋,谭建荣. 三维场景中图形对象的拾取方法. 机械,2004, 31(7): 29-32.

3. 何健鹰,徐强华,游佳. 基于OpenGL的一种三维拾取方法. 计算机工程与科学, 2006, 28(1): 45-46.

4. Michael Wimmer, Jiri Bittner. Hardware occlusion queries made useful. Vienna University of Technology,GPU Gems 2 Chapter 6.

5. Microsoft DirectX SDK帮助文档,June 2007.

posted @ 2011-09-08 17:53  ~哇哇~  阅读(2088)  评论(0编辑  收藏  举报