3D物体拾取及XNA实现
- 摘要:拾取主要用来表示能过鼠标在屏幕上单击来选中某个3D模型,然后就可以获取这个模型信息,同时也可以对这个模型进行编辑。 拾取算法的主要思想是:得到鼠标点击处的屏幕坐标,通过投影矩阵和观察矩阵把该坐标转换为通过视点和鼠标点击点的一条射入场景的光线,该光线如果与场景模型的三角形相交,则获取该相交三角形的信息。
-
-
拾取原理
拾取主要用来表示能过鼠标在屏幕上单击来选中某个3D模型,然后就可以获取这个模型信息,同时也可以对这个模型进行编辑。
拾取算法的主要思想是:得到鼠标点击处的屏幕坐标,通过投影矩阵和观察矩阵把该坐标转换为通过视点和鼠标点击点的一条射入场景的光线,该光线如果与场景模型的三角形相交,则获取该相交三角形的信息。
拾取的具体过程如下:
1.使用获取鼠标当前状态。
2.把屏幕坐标转换为屏幕空间坐标。
屏幕中心是(0, 0),屏幕右下角是 (1*(屏幕宽度/屏幕高度), 1)。屏幕空间的x坐标这样计算: ((鼠标x坐标) / (屏幕宽度/2))-1.0f) *(屏幕宽度/屏幕高度)
屏幕空间的y坐标这样计算: (1.0f − ((鼠标y坐标) / (屏幕高度/ 2 ) )
3.计算摄像机视图中的宽度和高度的截距比。如下计算:
view ratio = tangent(camera field of view / 2 )
通常,你只需要一次,然后保存这个结果供以后使用。在摄像机视野改变的时候,你需要重新计算。
4.把屏幕空间坐标转换成摄像机空间坐标系近景裁剪平面中的一个点。
Near point = ((屏幕空间x坐标)*(近景裁剪平面的Z值)*(view ratio ),
(屏幕空间y坐标)*(近景裁剪平面的Z值)*(view ratio ),
(-近景裁剪平面的Z值) )
5.把屏幕空间坐标转换成摄像机空间坐标系远景裁剪平面中的一个点。
Far point = ((屏幕空间x坐标)* (远景裁剪平面的Z值)*(view ratio ),
(屏幕空间y坐标)*(远景裁剪平面的Z值 )*(view ratio),
(-远景裁剪平面的Z值) )
6.使用Invert 获得摄像机视图矩阵的一个Invert的拷贝。使用摄像机视图矩阵(Matrix)来把世界坐标转化成摄像机坐标,使用这个反转的矩阵可以把摄像机坐标转化成世界坐标。
Matrix invView = Matrix.Invert(view);
7.使用反转的视图矩阵(view Matrix ) 和Transform方法把远景点和近景点转换成世界空间坐标。
Vector3 worldSpaceNear = Vector3.Transform(cameraSpaceNear, invView);
Vector3 worldSpaceFar = Vector3.Transform(cameraSpaceFar, invView);
8.创建一个射线(Ray)类的对象,起点是近景点,指向远景点。
Ray pickRay = new Ray(worldSpaceNear, worldSpaceFar - worldSpaceNear);
9.对世界空间中的所有物体循环调用 Intersects 方法来检测 Ray 是否与其相交。如果相交,则检测是不是目前为止距玩家最近的物体,如果是,记录下这个物体以及距离值,替换掉之前找到的最近的物体的记录。当完成对所有物体的检测后,最后一次记录的那个物体就是玩家用鼠标点击的距离玩家最近的物体。
XNA实现
效果图如下:
主要方法如下:
001.
public
class
Game1:Game
002.
{
003.
GraphicsDeviceManager graphi;
004.
model[] models;
005.
Texture2D texture;
006.
007.
Matrix view;
008.
Matrix projection;
009.
010.
int
selectIndex = -1;
011.
012.
public
Game1()
013.
{
014.
graphi =
new
GraphicsDeviceManager(
this
);
015.
Content.RootDirectory =
"Content"
;
016.
IsMouseVisible =
true
;
017.
}
018.
019.
protected
override
void
Initialize()
020.
{
021.
models =
new
model[4];
022.
models[0] =
new
model();
023.
models[0].position = Vector3.Zero;
024.
025.
026.
models[1] =
new
model();
027.
models[1].position =
new
Vector3(80,0,0);
028.
029.
models[2] =
new
model();
030.
models[2].position =
new
Vector3(-80, 0, 0);
031.
032.
models[3] =
new
model();
033.
models[3].position =
new
Vector3(80, 80, 0);
034.
035.
//观察矩阵
036.
view = Matrix.CreateLookAt(
new
Vector3(0, 0, 300), Vector3.Forward, Vector3.Up);
037.
//投影矩阵
038.
projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 1, 10000);
039.
base
.Initialize();
040.
}
041.
042.
043.
protected
override
void
LoadContent()
044.
{
045.
//载入模型文件
046.
models[0].mod = Content.Load<Model>(
"bsphere"
);
047.
models[1].mod = Content.Load<Model>(
"cub"
);
048.
models[2].mod = Content.Load<Model>(
"pyramid"
);
049.
models[3].mod = Content.Load<Model>(
"teaport"
);
050.
051.
//载入选中物体的贴图纹理
052.
texture = Content.Load<Texture2D>(
"sp"
);
053.
base
.LoadContent();
054.
}
055.
056.
/**/
/// <summary>
057.
/// 更新
058.
/// </summary>
059.
/// <param name="gameTime"></param>
060.
protected
override
void
Update(GameTime gameTime)
061.
{
062.
CheckMousClick();
063.
base
.Update(gameTime);
064.
}
065.
066.
/**/
/// <summary>
067.
/// 绘制
068.
/// </summary>
069.
/// <param name="gameTime"></param>
070.
protected
override
void
Draw(GameTime gameTime)
071.
{
072.
GraphicsDevice.Clear(Color.CornflowerBlue);
073.
074.
for
(
int
i = 0; i < models.Length;i++ )
075.
{
076.
foreach
(ModelMesh mesh
in
models[i].mod.Meshes)
077.
{
078.
foreach
(BasicEffect effect
in
mesh.Effects)
079.
{
080.
effect.TextureEnabled =
true
;
081.
//根据是否选中设置物体的贴图
082.
if
(i != selectIndex)
083.
{
084.
//如果没有选中,不贴图
085.
effect.Texture =
null
;
086.
}
087.
else
088.
effect.Texture = texture;
089.
090.
effect.World = Matrix.CreateTranslation(models[i].position);
091.
effect.View = view;
092.
effect.Projection = projection;
093.
094.
effect.EnableDefaultLighting();
095.
effect.LightingEnabled =
true
;
096.
}
097.
098.
mesh.Draw();
099.
}
100.
101.
}
102.
103.
base
.Draw(gameTime);
104.
}
105.
106.
/**/
/// <summary>
107.
/// 取得射线
108.
/// </summary>
109.
/// <returns></returns>
110.
private
Ray GetRay()
111.
{
112.
MouseState ms = Mouse.GetState();
113.
Vector3 neerSource =
new
Vector3(ms.X, ms.Y, 0);
114.
Vector3 farSource =
new
Vector3(ms.X, ms.Y, 1);
115.
116.
Vector3 neerPosi = GraphicsDevice.Viewport.Unproject(neerSource, projection, view, Matrix.Identity);
117.
Vector3 farPosi = GraphicsDevice.Viewport.Unproject(farSource, projection, view, Matrix.Identity);
118.
Vector3 direction=farPosi-neerPosi;
119.
direction.Normalize();
120.
return
new
Ray(neerPosi, direction);
121.
}
122.
123.
/**/
/// <summary>
124.
/// 鼠标单击
125.
/// </summary>
126.
private
void
CheckMousClick()
127.
{
128.
if
(Mouse.GetState().LeftButton==ButtonState.Pressed)
129.
{
130.
Ray ray = GetRay();
131.
for
(
int
i=0;i<models.Length;i++)
132.
{
133.
BoundingSphere bs=models[i].mod.Meshes[0].BoundingSphere;
134.
bs.Center = models[i].position;
135.
Nullable<
float
> result = ray.Intersects(bs);
136.
if
(result.HasValue)
137.
{
138.
selectIndex = i;
139.
}
140.
}
141.
}
142.
}
143.
}
144.
145.
/**/
/// <summary>
146.
/// 自定义模型结构
147.
/// </summary>
148.
public
struct
model
149.
{
150.
public
Model mod;
151.
public
Texture2D text;
152.
public
Vector3 position;
153.
}
其中用到了XNA中的Viewport.Unproject方法和Ray.Intersects方法,它们的内部结构分别如下:
01.
public
void
Intersects(
ref
Ray ray,
out
float
? result)
02.
{
03.
result = 0;
04.
float
num = 0f;
05.
float
maxValue =
float
.MaxValue;
06.
if
(Math.Abs(ray.Direction.X) < 1E-06f)
07.
{
08.
if
((ray.Position.X <
this
.Min.X) || (ray.Position.X >
this
.Max.X))
09.
{
10.
return
;
11.
}
12.
}
13.
else
14.
{
15.
float
num11 = 1f / ray.Direction.X;
16.
float
num8 = (
this
.Min.X - ray.Position.X) * num11;
17.
float
num7 = (
this
.Max.X - ray.Position.X) * num11;
18.
if
(num8 > num7)
19.
{
20.
float
num14 = num8;
21.
num8 = num7;
22.
num7 = num14;
23.
}
24.
num = MathHelper.Max(num8, num);
25.
maxValue = MathHelper.Min(num7, maxValue);
26.
if
(num > maxValue)
27.
{
28.
return
;
29.
}
30.
}
31.
if
(Math.Abs(ray.Direction.Y) < 1E-06f)
32.
{
33.
if
((ray.Position.Y <
this
.Min.Y) || (ray.Position.Y >
this
.Max.Y))
34.
{
35.
return
;
36.
}
37.
}
38.
else
39.
{
40.
float
num10 = 1f / ray.Direction.Y;
41.
float
num6 = (
this
.Min.Y - ray.Position.Y) * num10;
42.
float
num5 = (
this
.Max.Y - ray.Position.Y) * num10;
43.
if
(num6 > num5)
44.
{
45.
float
num13 = num6;
46.
num6 = num5;
47.
num5 = num13;
48.
}
49.
num = MathHelper.Max(num6, num);
50.
maxValue = MathHelper.Min(num5, maxValue);
51.
if
(num > maxValue)
52.
{
53.
return
;
54.
}
55.
}
56.
if
(Math.Abs(ray.Direction.Z) < 1E-06f)
57.
{
58.
if
((ray.Position.Z <
this
.Min.Z) || (ray.Position.Z >
this
.Max.Z))
59.
{
60.
return
;
61.
}
62.
}
63.
else
64.
{
65.
float
num9 = 1f / ray.Direction.Z;
66.
float
num4 = (
this
.Min.Z - ray.Position.Z) * num9;
67.
float
num3 = (
this
.Max.Z - ray.Position.Z) * num9;
68.
if
(num4 > num3)
69.
{
70.
float
num12 = num4;
71.
num4 = num3;
72.
num3 = num12;
73.
}
74.
num = MathHelper.Max(num4, num);
75.
maxValue = MathHelper.Min(num3, maxValue);
76.
if
(num > maxValue)
77.
{
78.
return
;
79.
}
80.
}
81.
result =
new
float
?(num);
82.
}
一、二维图片的拾取与碰撞检测 1、拾取 二维图片的拾取是通过判断鼠标的当前坐标是否落在图片的现实区域中来实现的 (1)、获取顶点信息(图片的显示位置) (2)、获取纹理的宽度与高度 myTexture.Width
myTexture.Height
(3)、获取鼠标位置信息
MouseState state = Mouse.GetState();
(4)、判断鼠标坐标是否在纹理的显示范围之内
if (state.X >= pos.X && state.X <= pos.X + myTexture.Width && state.Y >= pos.Y
&& state.Y <= pos.Y + myTexture.Height)
2、碰撞检测 二维图片的碰撞检测可以通过判断图片的两个显示矩形是否相交来判断。
(1)、构造矩形
/*构造第一个矩形*/
Rectangle moveRect = new Rectangle(); moveRect.X = (int)movePosition.X; moveRect.Y = (int)movePosition.Y; moveRect.Width = myTexture.Width; moveRect.Height = myTexture.Height;
/*构造第二个矩形*/
Rectangle rectPos = new Rectangle(); rectPos.X = (int)pos.X; rectPos.Y = (int)pos.Y;
rectPos.Width = myTexture.Width; rectPos.Height = myTexture.Height; (2)、判断矩形是否相交
if(moveRect.Intersects(rectPos))
二、SD模型的拾取与碰撞检测 1、碰撞检测
XNA中的碰撞检测是通过测试两个物体的包围盒或者包围球是否相交来实现的。XNA为模型的每个Meshe建立一个包围盒和包围球。
(1)、获取包围球
BoundingSphere c1BoundingSphere = model1.Meshes[i].BoundingSphere; BoundingSphere c2BoundingSphere = model2.Meshes[j].BoundingSphere; (2)、判断两个包围球是否相交
if (c1BoundingSphere.Intersects(c2BoundingSphere))
返回值为true表示两模型相交,返回值为false表示两模型没有相交
2、拾取
拾取是指通过鼠标来选中某个模型。 (1)、获取当前鼠标状态
MouseState mouseState = Mouse.GetState(); (2)、获取鼠标的位置信息
int mouseX = mouseState.X; int mouseY = mouseState.Y;
(3)、构造摄像机坐标系下的两个点,分别代表了近点和远点
Vector3 nearsource = new Vector3((float)mouseX, (float)mouseY, 0f); Vector3 farsource = new Vector3((float)mouseX, (float)mouseY, 1f);
(4)、将两个点通过逆投影矩阵转换为世界坐标系下的两个点
Matrix world = Matrix.CreateTranslation(0, 0, 0);
Vector3 nearPoint = graphics.GraphicsDevice.Viewport.Unproject(nearsource,
proj, view, world);
Vector3 farPoint = graphics.GraphicsDevice.Viewport.Unproject(farsource,
proj, view, world);
(5)、构造拾取射线
Vector3 direction = farPoint - nearPoint; direction.Normalize();
Ray pickRay = new Ray(nearPoint, direction);
(6)、判断射线是否与模型Mesh的包围盒相交
Nullable<float> result = pickRay.Intersects(sphere);
如果result.HasValue 为 true并且result.Value < selectedDistance则认为物体被拾取。当射线与多个物体相交时可通过result.Value来判断谁在前面,值小的更靠近屏幕。在初始阶段我们将selectedDistance设置为最大值(float.MaxValue;)。
注:XNA为我们提供的拾取仅仅能够知道是否与某模型相交,无法提供与哪一点相交的信息,
为此,我们可以通过让拾取射线与模型的某个三角片相交并求其交点的方法来实现。
这个例子讲述了通过创建一个从摄像机近景裁剪面指向远景裁剪面的射线怎样检测鼠标是否放在一个3D物体上。
本示例仅适用于Windows 平台开发。 Xbox 360不支持Mouse和MouseState对象。
检查用户是否点击了一个3D对象
检查鼠标是否位于一个3D对象之上
-
使用GetState获取鼠标当前状态。
MouseState mouseState = Mouse.GetState();
-
从X和Y中获取鼠标的屏幕坐标。
int mouseX = mouseState.X;int mouseY = mouseState.Y;
-
使用Viewport.Unproject得出在近景裁剪面和远景裁剪面上的点在世界空间中的位置。对于近景裁剪面的点,传递一个Vector3向量,此向量的x和y设置为鼠标位置,而z设置为0。
-
对于远景裁剪面的点,传递一个Vector3向量,此向量的x和y设置为鼠标位置,而z设置为1。
-
对于这两个点,使用Unproject传递给当前投影矩阵,观察矩阵和点(0,0,0)的平移矩阵。
Vector3 nearsource = new Vector3((float)mouseX, (float)mouseY, 0f); Vector3 farsource = new Vector3((float)mouseX, (float)mouseY, 1f);Matrix world = Matrix.CreateTranslation(0, 0, 0); Vector3 nearPoint = graphics.GraphicsDevice.Viewport.Unproject(nearsource, proj, view, world); VectVector3 farPoint = graphics.GraphicsDevice.Viewport.Unproject(farsource, proj, view, world);
-
创建一个从nearPoint指向farPoint的Ray。
// Create a ray from the near clip plane to the far clip plane. Vector3 direction = farPoint - nearPoint;rection.Normalize(); Ray Ray pickRay = new Ray(nearPoint, direction);
-
使用Intersects循环检测场景中的所有对象是否和Ray相交。
-
如果Ray与一个对象相交,检查这个对象是否是相交最近的。如果是,储存这个对象和相交的距离,并替换前面储存的对象。
-
对你完成对象的循环,存储的最后一个对象将会是用户点击区域内最近的那个对象。