3D物体拾取及XNA实现
拾取原理
拾取主要用来表示能过鼠标在屏幕上单击来选中某个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实现
效果图如下:
主要方法如下:
2 {
3 GraphicsDeviceManager graphi;
4 model[] models;
5 Texture2D texture;
6
7 Matrix view;
8 Matrix projection;
9
10 int selectIndex = -1;
11
12 public Game1()
13 {
14 graphi = new GraphicsDeviceManager(this);
15 Content.RootDirectory = "Content";
16 IsMouseVisible = true;
17 }
18
19 protected override void Initialize()
20 {
21 models = new model[4];
22 models[0] = new model();
23 models[0].position = Vector3.Zero;
24
25
26 models[1] = new model();
27 models[1].position =new Vector3(80,0,0);
28
29 models[2] = new model();
30 models[2].position = new Vector3(-80, 0, 0);
31
32 models[3] = new model();
33 models[3].position = new Vector3(80, 80, 0);
34
35 //观察矩阵
36 view = Matrix.CreateLookAt(new Vector3(0, 0, 300), Vector3.Forward, Vector3.Up);
37 //投影矩阵
38 projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 1, 10000);
39 base.Initialize();
40 }
41
42
43 protected override void LoadContent()
44 {
45 //载入模型文件
46 models[0].mod = Content.Load<Model>("bsphere");
47 models[1].mod = Content.Load<Model>("cub");
48 models[2].mod = Content.Load<Model>("pyramid");
49 models[3].mod = Content.Load<Model>("teaport");
50
51 //载入选中物体的贴图纹理
52 texture = Content.Load<Texture2D>("sp");
53 base.LoadContent();
54 }
55
56 /// <summary>
57 /// 更新
58 /// </summary>
59 /// <param name="gameTime"></param>
60 protected override void Update(GameTime gameTime)
61 {
62 CheckMousClick();
63 base.Update(gameTime);
64 }
65
66 /// <summary>
67 /// 绘制
68 /// </summary>
69 /// <param name="gameTime"></param>
70 protected override void Draw(GameTime gameTime)
71 {
72 GraphicsDevice.Clear(Color.CornflowerBlue);
73
74 for (int i = 0; i < models.Length;i++ )
75 {
76 foreach (ModelMesh mesh in models[i].mod.Meshes)
77 {
78 foreach(BasicEffect effect in mesh.Effects)
79 {
80 effect.TextureEnabled = true;
81 //根据是否选中设置物体的贴图
82 if (i != selectIndex)
83 {
84 //如果没有选中,不贴图
85 effect.Texture = null;
86 }
87 else
88 effect.Texture = texture;
89
90 effect.World = Matrix.CreateTranslation(models[i].position);
91 effect.View = view;
92 effect.Projection = projection;
93
94 effect.EnableDefaultLighting();
95 effect.LightingEnabled = true;
96 }
97
98 mesh.Draw();
99 }
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方法,它们的内部结构分别如下:
2{
3 Vector3 position = new Vector3();
4 Matrix matrix = Matrix.Invert(Matrix.Multiply(Matrix.Multiply(world, view), projection));
5 position.X = (((source.X - this.X) / ((float) this.Width)) * 2f) - 1f;
6 position.Y = -((((source.Y - this.Y) / ((float) this.Height)) * 2f) - 1f);
7 position.Z = (source.Z - this.MinDepth) / (this.MaxDepth - this.MinDepth);
8 position = Vector3.Transform(position, matrix);
9 float a = (((source.X * matrix.M14) + (source.Y * matrix.M24)) + (source.Z * matrix.M34)) + matrix.M44;
10 if (!WithinEpsilon(a, 1f))
11 {
12 position = (Vector3) (position / a);
13 }
14 return position;
15}
16
17
18
19
20
2{
3 result = 0;
4 float num = 0f;
5 float maxValue = float.MaxValue;
6 if (Math.Abs(ray.Direction.X) < 1E-06f)
7 {
8 if ((ray.Position.X < this.Min.X) || (ray.Position.X > this.Max.X))
9 {
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}
83
84
85