拾取是图形学里一个很常用的应用,在3D世界里选中我们想要的东西,枪战游戏中判断子弹是否射中敌人。
这里我们讲下如何实现一个精确的拾取碰撞,不仅检测是否射中了这个物体,更进一步的返回射中了这个物体的上哪个三角形。在《COD》等比较优秀的枪战游戏中,我们都可以看到子弹是射中敌人身体的不同部位,敌人的身体会做出不同的反应。
然后我们看看如何实现一个基于GPU的拾取,把拾取的所有计算都映射到GPU上去。这里我们分别用GeometryShader和ComputeShader来做一个实现。这一篇先用GeometryShader来实现。
程序主要是如果点击鼠标,选中了哪个三角形,就绘制它,其它三角形就以线框的方式绘制。程序截图如下:
文章下面先讲下拾取的算法,然后再来讲如何在GeometryShader上实现这个算法。
精确拾取算法说白了就判断一个射线是否与一个三角形相交。我们设T(u,v,w)=uA+vB+wC定义了三角形ABC平面上的点,其中(u,v,w)为该点的质心坐标且有u+v+w=1,当且仅当该点的质心坐标满足0<=u,v,w<=1时,点T位于三角形ACB里面。设u=1-v-w,则T(v,w)=A+v(B-A)+u(C-A)。
令两端点P,Q构成的有向线段定位为R(t)=P+t(Q-P),0<=t<=1.
如果拾取到了,或者说我们射出去的射线PQ选中了三角形,那么有
T(v,w)=R(t)
则
(P-Q)t+(B-A)v+(C-A)w=P-A变成矩阵形式:
[(P-Q) (B-A) (C-A)] [t v w]'=[P-A] (住 ' 表示转置)
用克莱姆法则对t,v,w进行求解:
t=det[(P-A) (B-A ) (C-A)]/det[(P-Q) (B-A) (C-A)] (注det表示行列式)
v=det[(P-Q) (P-A ) (C-A)]/det[(P-Q) (B-A) (C-A)] (注det表示行列式
w=det[(P-Q) (B-A ) (P-A)]/det[(P-Q) (B-A) (C-A)] (注det表示行列式)
具体的负责判断拾取的Shader代码:
bool IntersectTriangle(float3 origin,float3 dir,float3 v0,float3 v1,float3 v2,out float t,out float u,out float v)
{
float3 edge1=v1-v0;
float3 edge2=v2-v0;
float3 qvec=cross(dir,edge2);
float det=dot(edge1,qvec);
if(det<0.001f && det>-0.001f)return false;
float invdet=1.0f/det;
//求u
float3 tvec=origin-v0;
u=dot(tvec,qvec);
u=u*invdet;
if(u<0.0f || u>1.0f)return false;
float3 pvec=cross(tvec,edge1);
//求v
v=dot(dir,pvec);
v=v*invdet;
if(v<0.0f || ( u+v>1.0f ))return false;
float3 nvec=cross(edge1,edge2);
//求t
t=dot(tvec,nvec);
t=t*invdet;
return true;
}
第二个问题是如何得到那条拾取射线。拾取射线的计算说白了从屏幕坐标->NDC坐标->视觉坐标->世界坐标->物体局部坐标。具体可以参考《Introduction to 3D Game Programming with DirectX 10》Chapter 15 - Picking。因为推导比较琐碎,下面只是大概的带过。
1从NDC坐标转换到屏幕坐标的矩阵M
反过来可以求Xndc,Yndc
2再转换到视角坐标
3使其zv=1,
4然后再乘以视觉坐标V的逆矩阵V(-1)把转换到世界坐标。
5乘以相应物体的世界坐标W的逆矩阵W(-1)转换到局部坐标
讲完了基本算法,我们来看下如何在GeometryShade中实现它。主要是通过GeometryShader的StreamOutput,来把选中的三角形输出到StreamOutput Buffer中,下面来看具体的负责拾取算法的technique。
technique11 IntersectTech
{
pass p0
{
SetVertexShader(CompileShader(vs_4_0,VS_STREAMOUT()));
SetGeometryShader(ConstructGSWithSO(CompileShader(gs_4_0,GS_STREAMOUT()),"POSITION.xyz;NORMAL.xyz;TEXCOORD.xy") );
SetPixelShader(NULL);
SetDepthStencilState(DisableDepth,0);
}
}
TriPoint VS_STREAMOUT(TriPoint vIn)
{
return vIn;
}
//下面是本程序的重点之一,计算拾取射线,判断是否选中了该三角形,如果选中把它放到一个StreamOutput Buffer中。
[maxvertexcount(3)]
void GS_STREAMOUT(triangle TriPoint gIn[3],inout TriangleStream<TriPoint> triStream)
{
float3 pickOrigin=float3(0.0f,0.0f,0.0f);
float3 pickDirection;
pickDirection.x=(2.0f*winPos.x/backBufferDesc.x-1.0f)/projMtx[0][0];
pickDirection.y=(-2.0f*winPos.y/backBufferDesc.y+1.0f)/projMtx[1][1];
pickDirection.z=1.0f;
//乘以视角逆矩阵
pickOrigin=mul(float4(pickOrigin,1.0f),invViewMtx).xyz;
pickDirection=mul(float4(pickDirection,0.0f),invViewMtx).xyz;
//乘以世界逆矩阵
pickOrigin=mul(float4(pickOrigin,1.0f),invWorldMtx).xyz;
pickDirection=mul(float4(pickDirection,0.0f),invWorldMtx).xyz;
float u=0;
float v=0;
float t=0;
if(IntersectTriangle(pickOrigin,pickDirection,gIn[0].posW,gIn[1].posW,gIn[2].posW,t,u,v))
{
for(int i=0;i<3;i++)
{
triStream.Append(gIn[i]);
}
}
return ;
}
下面来看下在Direct中的拾取绘制代码,主要就是3个technique,第一个是调用拾取的technique,来把选中的三角形放到StreamOutput Buffer中,第二个是绘制上面刚选中的三角形,第三个是以网格形式来绘制本来的模型,方便观察结果:
void Mesh::DrawPickMesh_GSBUF(CModelViewerCamera *pCamera,POINT cursor,POINT backDesc)
{
//先设置相关变量
//calculate the matrix
D3DXMATRIX invWorld;
D3DXMatrixInverse(&invWorld,0,&m_World);
D3DXMATRIX viewMtx=*pCamera->GetViewMatrix();
D3DXMATRIX invViewMtx;
D3DXMatrixInverse(&invViewMtx,0,&viewMtx);
const D3DXMATRIX projMtx=*pCamera->GetProjMatrix();
//calculate the cursor and backbuffer
MFloat2 mcur;
MFloat2 mback;
mcur.x=(float)cursor.x;
mcur.y=(float)cursor.y;
mback.x=(float)backDesc.x;
mback.y=(float)backDesc.y;
//set the effect variable
m_pfxInterViewMtx->SetMatrix((float*)&viewMtx);
m_pfxInterInvViewMtx->SetMatrix((float*)&invViewMtx);
m_pfxInterProjMtx->SetMatrix((float*)&projMtx);
m_pfxInterWorldMtx->SetMatrix((float*)(&m_World));
m_pfxInterInvWorldMtx->SetMatrix((float*)&invWorld);
m_pfxInterWinPos->SetRawValue((void*)&mcur,0,sizeof(MFloat2));
m_pfxInterBackbufferDesc->SetRawValue((void*)&mback,0,sizeof(MFloat2));
//set the inputlayout,vertexbuffer,sobuffer,index buffer and so on.
m_pContext->IASetInputLayout(m_pInputLayout);
UINT offset=0;
m_pContext->SOSetTargets(1,&m_pSOBuffer,&offset);
UINT stride=m_pMesh11->GetVertexStride(0,0);
ID3D11Buffer *pVertexBuffer[1];
pVertexBuffer[0]=m_pMesh11->GetVB11(0,0);
m_pContext->IASetVertexBuffers(0,1,pVertexBuffer,&stride,&offset);
m_pContext->IASetIndexBuffer(m_pMesh11->GetIB11(0),m_pMesh11->GetIBFormat11(0),0);
D3DX11_TECHNIQUE_DESC techDesc;
D3D11_PRIMITIVE_TOPOLOGY primType;
SDKMESH_SUBSET *pSubSet;
//1调用一开始说的拾取算法的shader,把拾取到的三角形都放入m_pSOBuffer中。
m_pfxInterTech->GetDesc(&techDesc);
for(int i=0;i<techDesc.Passes;i++)
{
for(int subset=0;subset<m_pMesh11->GetNumSubsets(0);subset++)
{
pSubSet=m_pMesh11->GetSubset(0,subset);
m_pfxInterTech->GetPassByIndex(0)->Apply(0,m_pContext);
//get primitiveType
primType=CDXUTSDKMesh::GetPrimitiveType11((SDKMESH_PRIMITIVE_TYPE)pSubSet->PrimitiveType );
m_pContext->IASetPrimitiveTopology(primType);
m_pContext->DrawIndexed((UINT)pSubSet->IndexCount,0,(UINT)pSubSet->VertexStart);
}
}
//clear streamout buffer
ID3D11Buffer *ClearBuffer[1]={0};
m_pContext->SOSetTargets(1,ClearBuffer,&offset);
//2绘制上面选中的三角形
m_pfxWorldMtx->SetMatrix((float*)&m_World);
m_pfxViewMtx->SetMatrix((float*)pCamera->GetViewMatrix());
m_pfxProjMtx->SetMatrix((float*)pCamera->GetProjMatrix());
m_pfxMeshTech->GetDesc(&techDesc);
m_pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
stride=sizeof(MeshVertex);
m_pContext->IASetVertexBuffers(0,1,&m_pSOBuffer,&stride,&offset);
for(int i=0;i<techDesc.Passes;i++)
{
m_pfxMeshTech->GetPassByIndex(0)->Apply(0,m_pContext);
m_pContext->DrawAuto();
}
//3 以网格的方式绘制原来的模型,只是作为显示用。
DrawFrameMesh(pCamera);
return;
}
//以网格的方式绘制原来的模型,只是作为显示用。
void Mesh::DrawFrameMesh(CModelViewerCamera* pCamera)
{
m_pfxWorldMtx->SetMatrix((float*)&m_World);
m_pfxViewMtx->SetMatrix((float*)pCamera->GetViewMatrix());
m_pfxProjMtx->SetMatrix((float*)pCamera->GetProjMatrix());
D3DX11_TECHNIQUE_DESC techDesc;
ZeroMemory(&techDesc,sizeof(D3DX11_TECHNIQUE_DESC));
UINT stride=m_pMesh11->GetVertexStride(0,0);
UINT offset=0;
ID3D11Buffer *pVertexBuffer[1];
pVertexBuffer[0]=m_pMesh11->GetVB11(0,0);
m_pContext->IASetVertexBuffers(0,1,pVertexBuffer,&stride,&offset);
m_pContext->IASetIndexBuffer(m_pMesh11->GetIB11(0),m_pMesh11->GetIBFormat11(0),0);
m_pContext->IASetInputLayout(m_pInputLayout);
m_pfxWireFrameTech->GetDesc(&techDesc);
SDKMESH_SUBSET *pSubSet=NULL;
for(int i=0;i<techDesc.Passes;i++)
{
for(int subset=0;subset<m_pMesh11->GetNumSubsets(0);subset++)
{
pSubSet=m_pMesh11->GetSubset(0,subset);
m_pfxWireFrameTech->GetPassByIndex(0)->Apply(0,m_pContext);
//get primitiveType
D3D11_PRIMITIVE_TOPOLOGY primType;
primType=CDXUTSDKMesh::GetPrimitiveType11((SDKMESH_PRIMITIVE_TYPE)pSubSet->PrimitiveType );
m_pContext->IASetPrimitiveTopology(primType);
//get material
ID3D11ShaderResourceView *pSV=NULL;
pSV=m_pMesh11->GetMaterial(pSubSet->MaterialID)->pDiffuseRV11;
m_pfxDiffTex->SetResource(pSV);
//draw the wireframe mesh
m_pContext->DrawIndexed((UINT)pSubSet->IndexCount,0,(UINT)pSubSet->VertexStart);
}
}
}