DirectX11 With Windows SDK--21 鼠标拾取
前言
拾取是一项非常重要的技术,不论是电脑上用鼠标操作,还是手机的触屏操作,只要涉及到UI控件的选取则必然要用到该项技术。除此之外,一些类似魔兽争霸3、星际争霸2这样的3D即时战略游戏也需要通过拾取技术来选中角色。
给定在2D屏幕坐标系中由鼠标选中的一点,并且该点对应的正是3D场景中某一个对象表面的一点。 现在我们要做的,就是怎么判断我们选中了这个3D对象。
在阅读本章之前,先要了解下面的内容:
| 章节 |
|---|
| 05 键盘和鼠标输入 |
| 06 DirectXMath数学库 |
| 10 摄像机类 |
| 18 使用DirectXCollision库进行碰撞检测 |
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。
核心思想
龙书11上关于鼠标拾取的数学原理讲的过于详细,这里尽可能以简单的方式来描述。
因为我们所能观察到的3D对象都处于视锥体的区域,而且又已经知道摄像机所在的位置。因此在屏幕上选取一点可以理解为从摄像机发出一条射线,然后判断该射线是否与场景中视锥体内的物体相交。若相交,则说明选中了该对象。
当然,有时候射线会经过多个对象,这个时候我们就应该选取距离最近的物体。
一个3D对象的顶点原本是位于局部坐标系的,然后经历了世界变换、观察变换、投影变换后,会来到NDC空间中,可视物体的深度值(z值)通常会处于0.0到1.0之间。而在NDC空间的坐标点还需要经过视口变换,才会来到最终的屏幕坐标系。在该坐标系中,坐标原点位于屏幕左上角,x轴向右,y轴向下,其中x和y的值指定了绘制在屏幕的位置,z的值则用作深度测试。而且从NDC空间到屏幕坐标系的变换只影响x和y的值,对z值不会影响。

而现在我们要做的,就是将选中的2D屏幕点按顺序进行视口逆变换、投影逆变换和观察逆变换,让其变换到世界坐标系并以摄像机位置为射线原点,构造出一条3D射线,最终才来进行射线与物体的相交。在构造屏幕一点的时候,将z值设为0.0即可。z值的变动,不会影响构造出来的射线,相当于在射线中前后移动而已。
现在回顾一下视口类D3D11_VIEWPORT的定义:
typedef struct D3D11_VIEWPORT {
FLOAT TopLeftX;
FLOAT TopLeftY;
FLOAT Width;
FLOAT Height;
FLOAT MinDepth;
FLOAT MaxDepth;
} D3D11_VIEWPORT;
从NDC坐标系到屏幕坐标系的变换矩阵如下:
\frac{Width}{2} & 0 & 0 & 0 \\
0 & -\frac{Height}{2} & 0 & 0 \\
0 & 0 & MaxDepth - MinDepth & 0 \\
TopLeftX + \frac{Width}{2} & TopLeftY + \frac{Height}{2} & MinDepth & 1
\end{bmatrix}\]
现在,给定一个已知的屏幕坐标点(x, y, 0),要实现鼠标拾取的第一步就是将其变换回NDC坐标系。对上面的变换矩阵进行求逆,可以得到:
\frac{2}{Width} & 0 & 0 & 0 \\
0 & -\frac{2}{Height} & 0 & 0 \\
0 & 0 & \frac{1}{MaxDepth - MinDepth} & 0 \\
-\frac{2TopLeftX}{Width} - 1 & \frac{2TopLeftY}{Height} + 1 & -\frac{MinDepth}{MaxDepth - MinDepth} & 1
\end{bmatrix}\]
尽管DirectXMath没有构造视口矩阵的函数,我们也没必要去直接构造一个这样的矩阵,因为上面的矩阵实际上可以看作是进行了一次缩放和平移,即对向量进行了一次乘法和加法:
\]
\]
\]
由于可以从之前的Camera类获取当前的投影变换矩阵和观察变换矩阵,这里可以直接获取它们并进行求逆,得到在世界坐标系的位置:
\]
射线类Ray
Ray类的定义如下:
struct Ray
{
Ray();
Ray(const DirectX::XMFLOAT3& origin, const DirectX::XMFLOAT3& direction);
static Ray ScreenToRay(const Camera& camera, float screenX, float screenY);
bool Hit(const DirectX::BoundingBox& box, float* pOutDist = nullptr, float maxDist = FLT_MAX);
bool Hit(const DirectX::BoundingOrientedBox& box, float* pOutDist = nullptr, float maxDist = FLT_MAX);
bool Hit(const DirectX::BoundingSphere& sphere, float* pOutDist = nullptr, float maxDist = FLT_MAX);
bool XM_CALLCONV Hit(DirectX::FXMVECTOR V0, DirectX::FXMVECTOR V1, DirectX::FXMVECTOR V2, float* pOutDist = nullptr, float maxDist = FLT_MAX);
DirectX::XMFLOAT3 origin; // 射线原点
DirectX::XMFLOAT3 direction; // 单位方向向量
};
其中静态方法Ray::ScreenToRay执行的正是鼠标拾取中射线构建的部分,其实现灵感来自于DirectX::XMVector3Unproject函数,它通过给定在屏幕坐标系上的一点、视口属性、投影矩阵、观察矩阵和世界矩阵,来进行逆变换,得到在物体坐标系的位置:
inline XMVECTOR XM_CALLCONV XMVector3Unproject
(
FXMVECTOR V,
float ViewportX,
float ViewportY,
float ViewportWidth,
float ViewportHeight,
float ViewportMinZ,
float ViewportMaxZ,
FXMMATRIX Projection,
CXMMATRIX View,
CXMMATRIX World
)
{
static const XMVECTORF32 D = { { { -1.0f, 1.0f, 0.0f, 0.0f } } };
XMVECTOR Scale = XMVectorSet(ViewportWidth * 0.5f, -ViewportHeight * 0.5f, ViewportMaxZ - ViewportMinZ, 1.0f);
Scale = XMVectorReciprocal(Scale);
XMVECTOR Offset = XMVectorSet(-ViewportX, -ViewportY, -ViewportMinZ, 0.0f);
Offset = XMVectorMultiplyAdd(Scale, Offset, D.v);
XMMATRIX Transform = XMMatrixMultiply(World, View);
Transform = XMMatrixMultiply(Transform, Projection);
Transform = XMMatrixInverse(nullptr, Transform);
XMVECTOR Result = XMVectorMultiplyAdd(V, Scale, Offset);
return XMVector3TransformCoord(Result, Transform);
}
将其进行提取修改,用于我们的Ray对象的构造:
Ray Ray::ScreenToRay(const Camera & camera, float screenX, float screenY)
{
//
// 节选自DirectX::XMVector3Unproject函数,并省略了从世界坐标系到局部坐标系的变换
//
// 将屏幕坐标点从视口变换回NDC坐标系
static const XMVECTORF32 D = { { { -1.0f, 1.0f, 0.0f, 0.0f } } };
XMVECTOR V = XMVectorSet(screenX, screenY, 0.0f, 1.0f);
D3D11_VIEWPORT viewPort = camera.GetViewPort();
XMVECTOR Scale = XMVectorSet(viewPort.Width * 0.5f, -viewPort.Height * 0.5f, viewPort.MaxDepth - viewPort.MinDepth, 1.0f);
Scale = XMVectorReciprocal(Scale);
XMVECTOR Offset = XMVectorSet(-viewPort.TopLeftX, -viewPort.TopLeftY, -viewPort.MinDepth, 0.0f);
Offset = XMVectorMultiplyAdd(Scale, Offset, D.v);
// 从NDC坐标系变换回世界坐标系
XMMATRIX Transform = XMMatrixMultiply(camera.GetViewXM(), camera.GetProjXM());
Transform = XMMatrixInverse(nullptr, Transform);
XMVECTOR Target = XMVectorMultiplyAdd(V, Scale, Offset);
Target = XMVector3TransformCoord(Target, Transform);
// 求出射线
XMFLOAT3 direction;
XMStoreFloat3(&direction, XMVector3Normalize(Target - camera.GetPositionXM()));
return Ray(camera.GetPosition(), direction);
}
此外,在构造Ray对象的时候,还需要预先检测direction是否为单位向量:
Ray::Ray(const DirectX::XMFLOAT3 & origin, const DirectX::XMFLOAT3 & direction)
: origin(origin)
{
// 射线的direction长度必须为1.0f,误差在1e-5f内
XMVECTOR dirLength = XMVector3Length(XMLoadFloat3(&direction));
XMVECTOR error = XMVectorAbs(dirLength - XMVectorSplatOne());
assert(XMVector3Less(error, XMVectorReplicate(1e-5f)));
XMStoreFloat3(&this->direction, XMVector3Normalize(XMLoadFloat3(&direction)));
}
构造好射线后,就可以跟各种碰撞盒(或三角形)进行相交检测了:
bool Ray::Hit(const DirectX::BoundingBox & box, float * pOutDist, float maxDist)
{
float dist;
bool res = box.Intersects(XMLoadFloat3(&origin), XMLoadFloat3(&direction), dist);
if (pOutDist)
*pOutDist = dist;
return dist > maxDist ? false : res;
}
bool Ray::Hit(const DirectX::BoundingOrientedBox & box, float * pOutDist, float maxDist)
{
float dist;
bool res = box.Intersects(XMLoadFloat3(&origin), XMLoadFloat3(&direction), dist);
if (pOutDist)
*pOutDist = dist;
return dist > maxDist ? false : res;
}
bool Ray::Hit(const DirectX::BoundingSphere & sphere, float * pOutDist, float maxDist)
{
float dist;
bool res = sphere.Intersects(XMLoadFloat3(&origin), XMLoadFloat3(&direction), dist);
if (pOutDist)
*pOutDist = dist;
return dist > maxDist ? false : res;
}
bool XM_CALLCONV Ray::Hit(FXMVECTOR V0, FXMVECTOR V1, FXMVECTOR V2, float * pOutDist, float maxDist)
{
float dist;
bool res = TriangleTests::Intersects(XMLoadFloat3(&origin), XMLoadFloat3(&direction), V0, V1, V2, dist);
if (pOutDist)
*pOutDist = dist;
return dist > maxDist ? false : res;
}
至于射线与网格模型的拾取,有三种实现方式,对精度要求越高的话效率越低:
- 将网格模型单个OBB盒(或AABB盒)与射线进行相交检测,精度最低,但效率最高;
- 将网格模型划分成多个OBB盒,分别于射线进行相交检测,精度较高,效率也比较高;
- 将网格模型的所有三角形与射线进行相交检测,精度最高,但效率最低。而且模型面数越大,效率越低。这里可以先用模型的OBB(或AABB)盒与射线进行大致的相交检测,若在包围盒内再跟所有的三角形进行相交检测,以提升效率。
在该演示教程中只考虑第1种方法,剩余的方法根据需求可以自行实现。
最后是一个项目演示动图,该项目没有做点击物体后的反应。鼠标放到这些物体上会当即显示出当前所拾取的物体,点击物体就会弹出窗口。其中立方体和房屋使用的是OBB盒。


DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。
DirectX11 With Windows SDK--21 鼠标拾取的更多相关文章
- 粒子系统与雨的效果 (DirectX11 with Windows SDK)
前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用W ...
- DirectX11 With Windows SDK--05 键盘和鼠标输入
前言 提供键鼠输入可以说是一个游戏的必备要素.在这里,我们不使用DirectInput,而是使用Windows的消息处理机制,不过要从头开始实现会让事情变得很复杂.DXTK提供了鼠标输入的Mouse. ...
- DirectX11 With Windows SDK--00 目录
前言 (更新于 2019/4/10) 从第一次接触DirectX 11到现在已经有将近两年的时间了.还记得前年暑假被要求学习DirectX 11,在用龙书的源码配置项目运行环境的时候都花了好几天的时间 ...
- DirectX11 With Windows SDK--07 添加光照与常用几何模型
前言 对于3D游戏来说,合理的光照可以让游戏显得更加真实.接下来会介绍光照的各种分量,以及常见的光照模型.除此之外,该项目还用到了多个常量缓冲区,因此还会提及HLSL的常量缓冲区打包规则以及如何设置多 ...
- DirectX11 With Windows SDK--07 添加光照与常用几何模型、光栅化状态
原文:DirectX11 With Windows SDK--07 添加光照与常用几何模型.光栅化状态 前言 对于3D游戏来说,合理的光照可以让游戏显得更加真实.接下来会介绍光照的各种分量,以及常见的 ...
- DirectX11 With Windows SDK--10 摄像机类
前言 DirectX11 With Windows SDK完整目录:http://www.cnblogs.com/X-Jun/p/9028764.html 由于考虑后续的项目需要有一个比较好的演示环境 ...
- DirectX11 With Windows SDK--09 纹理映射与采样器状态
前言 在之前的DirectX SDK中,纹理的读取使用的是D3DX11CreateShaderResourceViewFromFile函数,现在在Windows SDK中已经没有这些函数,我们需要找到 ...
- DirectX11 With Windows SDK--11 混合状态与光栅化状态
前言 虽然这一部分的内容主要偏向于混合(Blending),但这里还需提及一下,关于渲染管线可以绑定的状态主要有如下四种: 光栅化状态(光栅化阶段) 采样器状态(像素着色阶段) 混合状态(输出合并阶段 ...
- DirectX11 With Windows SDK--04 使用DirectX Tool Kit帮助开发
前言(2018/11/4) DXTK库现在已经不随Github项目提供,因为只用到了其中的键鼠类,已经过提取加入到后续的项目中 但是如果你需要配置DirectXTK到自己的项目当中,可以参考这篇博客进 ...
随机推荐
- Java多线程基础(二)
1.多线程数据安全 线程同步:多个线程需要访问同一资源时,需要以某种顺序来确定该资源某一时刻只能被一个线程使用.从而,解决并发操作可能带来的异常. 2.同步代码块实现同步(部分代码的访问,我们希望它同 ...
- 多线程中的event,用于多线程的协调
''' 简单的需求:红绿灯,红灯停,绿灯行 一个线程扮演红绿灯,每过一段时间灯变化,3-5个线程扮演车,红灯停,绿灯行 红绿灯线程和车的线程会相互依赖 这种场景怎么实现?---事件 切换一次灯就是一次 ...
- 012_call和apply区别
一. function fn(a,b) { console.log(this); } fn.call(null,1,2); //call为参数方式 fn.apply(null,[1,2]); //ap ...
- Spring Cloud:统一异常处理
在启动应用时会发现在控制台打印的日志中出现了两个路径为 {[/error]} 的访问地址,当系统中发送异常错误时,Spring Boot 会根据请求方式分别跳转到以 JSON 格式或以界面显示的 /e ...
- [Oracle维护工程师手记]一次升级后运行变慢的分析
客户报告,当他从 Oracle 11.1.0.7 ,迁移到云环境,并且升级到12.1.0.2.运行客户的应用程序测试,发现比以前更慢了. 从AWR report 的"Top 10 Foreg ...
- 使用Roslyn脚本化C#代码,C#动态脚本实现方案
[前言] Roslyn 是微软公司开源的 .NET 编译器. 编译器支持 C# 和 Visual Basic 代码编译,并提供丰富的代码分析 API. Roslyn不仅仅可以直接编译输出,难能可贵的就 ...
- 跳出语句break 和continue
关键字break 常见的两种用法 在switch语句当中,一旦执行,整个switch语句立刻结束 在循环语句当中,一旦执行,整个循环语句立刻结束.跳出循环 代码举例: public class Dem ...
- CSL 的魔法
链接 [https://ac.nowcoder.com/acm/contest/551/E] 分析 很显然就是a的第k大得和b的倒数第k大相乘. 那么我们只要让a的第k大和b的倒数第k大位置是相同的即 ...
- qrcode & vue
qrcode & vue $ yarn add qrcode.vue # OR $ npm i -S qrcode.vue https://www.npmjs.com/package/qrco ...
- 记录一下各个用过 IDE 以及 其他工具 的实用快捷键(持续更新)
通用: win10锁屏:win + L win10查看服务:win+R,输入services.msc即可 Shift + Tab:多行缩进 Shift + Space:切换输入法 全/半角 Shift ...