Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十七章:拾取
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十七章:拾取
代码工程地址:
https://github.com/jiabaodan/Direct12BookReadingNotes
学习目标
学习如何实现拾取算法,我们将它分解为下面几个步骤:
- 当点击屏幕上s点时,计算对应的透视窗口上的点p;
- 在视景坐标系下计算拾取射线;
- 将射线和要进行检测的模型变换到同一个坐标系下;
- 检测模型是否和射线相交,取深度值最小的那个。

1 屏幕透视窗口的变换
第一个需要变换的是,从点击的屏幕变换到NDC,回顾之前从视景坐标系变换到NDC的变换矩阵:

它是通过D3D12_VIEWPORT结构中的数据组成:
typedef struct D3D12_VIEWPORT
{
FLOAT TopLeftX;
FLOAT TopLeftY;
FLOAT Width;
FLOAT Height;
FLOAT MinDepth;
FLOAT MaxDepth;
} D3D12_VIEWPORT;
一般情况下视景是整个后置缓冲,深度缓冲范围是0~1,所以TopLeftX = 0, TopLeftY = 0, MinDepth = 0, MaxDepth = 1, Width = w, Height = h,那么变换矩阵可以简化为:

现在令pndc = (xndc, yndc, zndc, 1)是NDC的一个点(−1 ≤ xndc ≤ 1, −1 ≤ yndc ≤ 1, and 0 ≤ zndc ≤ 1),变换pndc到屏幕坐标系:

我们不修改Z值,因为拾取计算不关系深度值在哪个坐标系,那么2D屏幕上的点ps = (xs, ys)就对应于NDC下的pndc:

上面的方式通过NDC点找到了屏幕坐标系下的点ps,但是拾取算法中我们需要通过屏幕上的点找打NDC下的点:

现在我们拥有了NDC下面的点,但是为了发射射线,我们需要得到视景坐标系下面的点,回顾第五章6.3.3,我们映射点从视景坐标系到NDC是通过x坐标除以宽高比r:

所以直接在X坐标上乘以宽高比即可:

再回顾第五章6.3.1,透视窗口是距离原点d=(α2)d = (\frac{\alpha}{2} )d=(2α),其中a是竖直方向上的角度。那么我们就可以通过点(xv, yv, d )发射射线,只要计算出d:

通过相似三角形:


那么我们可以通过点(x′v, y′v, 1)发射射线,和(xv, yv, d )发射的射线是一样的,在视景坐标系下计算发射射线的代码如下:
void PickingApp::Pick(int sx, int sy)
{
XMFLOAT4X4 P = mCamera.GetProj4x4f();
// Compute picking ray in view space.
float vx = (+2.0f*sx / mClientWidth - 1.0f) / P(0, 0);
float vy = (-2.0f*sy / mClientHeight + 1.0f) / P(1, 1);
// Ray definition in view space.
XMVECTOR rayOrigin = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
XMVECTOR rayDir = XMVectorSet(vx, vy, 1.0f, 0.0f);
2 世界/局部坐标系拾取射线
如果rv(t) = q + tu是世界坐标系下的拾取射线,V是世界坐标系到视景坐标系的变换矩阵,那么世界坐标系下的拾取射线为:

世界坐标系拾取射线对于在世界坐标系下定义的物体比较有用,但是大部分情况下,物体是在它自己的局部坐标系下定义的。如果W是局部坐标系到世界坐标系的变换矩阵,那么局部坐标系下的射线为:

如果在世界坐标系下做检测,就需要将物体都变换到世界坐标系下;通常情况下物体拥有很多顶点,都变换过去的计算量非常大,所以只把射线变换到每个物体的局部坐标系下做检测就比较高效。
下面的代码展示了将一个射线变换到一个物体的局部坐标系:
// Assume nothing is picked to start, so the picked render-item is invisible.
mPickedRitem->Visible = false;
// Check if we picked an opaque render item. A real app might keep a separate
// "picking list" of objects that can be selected.
for(auto ri : mRitemLayer[(int)RenderLayer::Opaque])
{
auto geo = ri->Geo;
// Skip invisible render-items.
if(ri->Visible == false)
continue;
XMMATRIX V = mCamera.GetView();
XMMATRIX invView = XMMatrixInverse(&XMMatrixDeterminant(V), V);
XMMATRIX W = XMLoadFloat4x4(&ri->World);
XMMATRIX invWorld = XMMatrixInverse(&XMMatrixDeterminant(W), W);
// Tranform ray to vi space of Mesh.
XMMATRIX toLocal = XMMatrixMultiply(invView, invWorld);
rayOrigin = XMVector3TransformCoord(rayOrigin, toLocal);
rayDir = XMVector3TransformNormal(rayDir, toLocal);
// Make the ray direction unit length for the intersection tests.
rayDir = XMVector3Normalize(rayDir);
XMVector3TransformNormal和XMVector3TransformCoord函数都是传入3D向量,但是XMVector3TransformNormal里w = 0,XMVector3TransformCoord里w=1,所以XMVector3TransformNormal用来变换向量,XMVector3TransformCoord用来变换点。
3 射线/网格的相交检测
下面的代码展示了射线和三角形的相交检测,如果有多个三角形相交,取最近的那个三角形:
// If we hit the bounding box of the Mesh, then we might have
// picked a Mesh triangle, so do the ray/triangle tests.
//
// If we did not hit the bounding box, then it is impossible that we hit
// the Mesh, so do not waste effort doing ray/triangle tests.
float tmin = 0.0f;
if(ri->Bounds.Intersects(rayOrigin, rayDir, tmin))
{
// NOTE: For the demo, we know what to cast the vertex/index data to.
// If we were mixing formats, some metadata would be needed to figure
// out what to cast it to.
auto vertices = (Vertex*)geo->VertexBufferCPU->GetBufferPointer();
auto indices = (std::uint32_t*)geo->IndexBufferCPU->GetBufferPointer();
UINT triCount = ri->IndexCount / 3;
// Find the nearest ray/triangle intersection.
tmin = MathHelper::Infinity;
for(UINT i = 0; i < triCount; ++i)
{
// Indices for this triangle.
UINT i0 = indices[i * 3 + 0];
UINT i1 = indices[i * 3 + 1];
UINT i2 = indices[i * 3 + 2];
// Vertices for this triangle.
XMVECTOR v0 = XMLoadFloat3(&vertices[i0].Pos);
XMVECTOR v1 = XMLoadFloat3(&vertices[i1].Pos);
XMVECTOR v2 = XMLoadFloat3(&vertices[i2].Pos);
// We have to iterate over all the triangles in order to find
// the nearest intersection.
float t = 0.0f;
if(TriangleTests::Intersects(rayOrigin, rayDir, v0, v1, v2, t))
{
if(t < tmin)
{
// This is the new nearest picked triangle.
tmin = t;
UINT pickedTriangle = i;
// Set a render item to the picked triangle so that
// we can render it with a special "highlight" material.
mPickedRitem->Visible = true;
mPickedRitem->IndexCount = 3;
mPickedRitem->BaseVertexLocation = 0;
// Picked render item needs same world matrix as object picked.
mPickedRitem->World = ri->World;
mPickedRitem->NumFramesDirty = gNumFrameResources;
// Offset to the picked triangle in the mesh index buffer.
mPickedRitem->StartIndexLocation = 3 * pickedTriangle;
}
}
}
}
上面的算法中,我们先进行了物体的包围体的检测,这样可以对性能进行很大的优化;因为只有通过包围体检测的物体,才进行逐三角形的相交检测。
观察上面的拾取,我们使用系统内存拷贝保存网格几何数据在MeshGeometry类中。这是因为我们无法读取vertex/index缓冲。
3.1 射线/AABB的相交检测
DirectX碰撞检测库中BoundingBox::Intersects函数可以用来检测,返回true就代表已经相交:
bool XM_CALLCONV BoundingBox::Intersects(
FXMVECTOR Origin, // ray origin
FXMVECTOR Direction, // ray direction (must be unit length)
float& Dist ); const // ray intersection parameter
给出射线r(t) = q + tu,最后一个参数就是t0计算出点P:

3.2 射线/球体的相交检测
DirectX碰撞检测库中的函数:
bool XM_CALLCONV BoundingSphere::Intersects(
FXMVECTOR Origin,
FXMVECTOR Direction,
float& Dist ); const
3.3 射线/三角形的相交检测
DirectX碰撞检测库中的函数:
bool XM_CALLCONV TriangleTests::Intersects(
FXMVECTOR Origin, // ray origin
FXMVECTOR Direction, // ray direction (unit length)
FXMVECTOR V0, // triangle vertex v0
GXMVECTOR V1, // triangle vertex v1
HXMVECTOR V2, // triangle vertex v2
float& Dist ); // ray intersection parameter
4 Demo应用
本例子中渲染了一个小车,可以让用户使用右键拾取网格中的三角形。为了实现拾取的三角形的高亮显示,本例中的Render-Item和以前的有些不同,只能部分在初始化中填充。我们添加了一个Visible属性,不可见的Render-Item将不绘制。下面的代码展示了如何根据拾取设置Render-Item属性:

// Cache a pointer to the render-item of the picked
// triangle in the PickingApp class.
RenderItem* mPickedRitem;
if(TriangleTests::Intersects(rayOrigin, rayDir, v0, v1, v2, t))
{
if(t < tmin)
{
// This is the new nearest picked triangle.
tmin = t;
UINT pickedTriangle = i;
// Set a render item to the picked triangle so that
// we can render it with a special "highlight" material.
mPickedRitem->Visible = true;
mPickedRitem->IndexCount = 3;
mPickedRitem->BaseVertexLocation = 0;
// Picked render item needs same world matrix as object picked.
mPickedRitem->World = ri->World;
mPickedRitem->NumFramesDirty = gNumFrameResources;
// Offset to the picked triangle in the mesh index buffer.
mPickedRitem->StartIndexLocation = 3 * pickedTriangle;
}
}
这个render-item是在绘制完不透明render-item后绘制的,使用一个高亮度的PSO。需要注意的是深度测试使用的是D3D12_COMPARISON_FUNC_LESS_EQUAL,因为如果使用D3D12_COMPARISON_FUNC_LESS会导致深度测试失败而不绘制:
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);
mCommandList->SetPipelineState(mPSOs["highlight"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Highlight]);
5 总结
- 拾取技术是判定用户在屏幕上点击的2D投射的物体,与之对应的3D物体;
- 拾取射线是从视景坐标系的原点发射出的一条射线,经过透视窗口上与用户点击屏幕的点对应的点;
- 我们可以用过变换射线的原点和方向向量来变换射线所在的坐标系(顶点w = 1,向量w = 0);
- 为了判定射线和物体是否相交,我们对物体的每一个三角形进行射线/三角形判定,如果有多个三角形相交,我们选择最近的那一个;
- 为了优化考虑,我们先进行物体包围体检测,只有检测通过的物体再遍历每个三角形检测。
6 练习
调查八叉树,优化拾取检测;之前提到的截头锥体剔除,也可以优化拾取检测。
(物理和碰撞也同理)
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十七章:拾取的更多相关文章
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画 学习目标 熟悉蒙皮动画的术语: 学习网格层级变换 ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十一章:环境光遮蔽(AMBIENT OCCLUSION)
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十一章:环境光遮蔽(AMBIENT OCCLUSION) 学习目标 ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十三章:计算着色器(The Compute Shader)
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十三章:计算着色器(The Compute Shader) 代码工程 ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试 代码工程地址: https://github.co ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第七章:在Direct3D中绘制(二)
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第七章:在Direct3D中绘制(二) 代码工程地址: https:/ ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第六章:在Direct3D中绘制
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第六章:在Direct3D中绘制 代码工程地址: https://gi ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第五章:渲染流水线
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第五章:渲染流水线 学习目标 了解几个用以表达真实场景的标志和2D图像 ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第四章:Direct 3D初始化
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第四章:Direct 3D初始化 学习目标 对Direct 3D编程在 ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第三章:变换
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第三章:变换 学习目标 理解如何用矩阵表示线性变换和仿射变换: 学习在 ...
随机推荐
- docker 安装redis 并配置外网可以访问 - flymoringbird的博客 - CSDN博客
原文:docker 安装redis 并配置外网可以访问 - flymoringbird的博客 - CSDN博客 端口映射,data目录映射,配置文件映射(在当前目录下进行启动). docker run ...
- Rabbitmq交换机三种模式介绍
1.topic 将路由键和某模式进行匹配.此时队列需要绑定要一个模式上.符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词.因此“abc.#”能够匹配到“abc.def.ghi”,但是“abc. ...
- BigDecimal的四则运算及小数位数格式
一.加法 BigDecimal b1 = new BigDecimal("20");BigDecimal b2 = new BigDecimal("30");B ...
- php获取数据转换成json格式
<?php header("content-type:text/html;charset=utf-8"); $con=mysql_connect("localhos ...
- ES6学习笔记之Symbol
新的数据类型Symbol 1. 概述 ES5 的对象属性名都是字符串,这容易造成属性名的冲突.比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与 ...
- 安装node/npm,通过express搭建node项目
nodejs软件的下载地址:https://nodejs.org/en/ (推荐下载稳定版) 1.只要安装好了nodejs,就自动安装好了npm包. 2.在cmd中通过命令node -version查 ...
- 2019.10.21 csp-s模拟测试81 反思总结
T1: 把每一行状压,按行DP.设fi,j,k,i表示第几行,j是当前行的1覆盖状态,k是当前行选择按钮的状态.转移的时候枚举j和k,再枚举下一层的按钮选择情况l.如果l和j可以全覆盖当前层则转移合法 ...
- bzoj 1045 [HAOI2008] 糖果传递——设变量推式子
题目:https://www.lydsy.com/JudgeOnline/problem.php?id=1045 费用流TLE. #include<iostream> #include&l ...
- 微信小程序-- WXML语法
页面数据 test.js test.wxml 运行结果:
- Hdu 4923(单调栈)
题目链接 Room and Moor Time Limit: 12000/6000 MS (Java/Others) Memory Limit: 262144/262144 K (Java/Ot ...