CSharpGL(20)用unProject和Project实现鼠标拖拽图元
效果图
例如,你可以把Big Dipper这个模型拽成下面这个样子。
配合旋转,还可以继续拖拽成这样。
当然,能拖拽的不只是线段。还可以拖拽三角形(如下图)、四边形。
另外,还可以单点拖拽。
2016-04-28
现在实现了高亮显示拾取、拖拽的图元的功能。
下面演示了鼠标移动到图元上时显示图元的索引值的功能。
起初会出现stitching和z-fighting的现象。例如下面选中一个三角形时,由于stitching问题,没高亮其斜边。
于是我添加了PolygonOffsetSwtich开关,解决了这个问题。
下载
CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
unProject/Project
这个两个函数的执行结果完全相反。拖拽功能全靠他们了。
Project把模型坐标系上的点转换为窗口坐标系上的点。这可以通过其实现代码来验证。
1 /// <summary> 2 /// Map the specified object coordinates (obj.x, obj.y, obj.z) into window coordinates. 3 /// </summary> 4 /// <param name="obj">The object.</param> 5 /// <param name="model">The model.</param> 6 /// <param name="proj">The proj.</param> 7 /// <param name="viewport">The viewport.</param> 8 /// <returns></returns> 9 public static vec3 project(vec3 obj, mat4 model, mat4 proj, vec4 viewport) 10 { 11 vec4 tmp = new vec4(obj, (1f)); 12 tmp = model * tmp; 13 tmp = proj * tmp; 14 15 tmp /= tmp.w; 16 tmp = tmp * 0.5f + new vec4(0.5f, 0.5f, 0.5f, 0.5f); 17 tmp[0] = tmp[0] * viewport[2] + viewport[0]; 18 tmp[1] = tmp[1] * viewport[3] + viewport[1]; 19 20 return new vec3(tmp.x, tmp.y, tmp.z); 21 }
通过试验发现,一个vec3经过Project后再经过unProject,会变回原来的值。这就是说,unProject把窗口坐标系上的点转换为模型坐标系上的点。
OpenGL是以窗口左下角为原点(0, 0)的。而Windows窗口是以左上角为原点的。所以用的时候要注意转换一下。
弄清楚了这两个函数,才能实现鼠标拖拽的功能。
拖拽原理
既然可以把模型空间的点转换为平面坐标系上的点,并且可以逆向操作。那么只需将要拖拽的点A通过project函数投影到屏幕上(变成a);根据鼠标在屏幕上的移动,相应的移动a,变成a',最后把a'通过unProject反射回模型空间,就是拖拽后的A'了。在VBO里,把A改为A'即可。
1 /// <summary> 2 /// 根据<paramref name="differenceOnScreen"/>来修改指定索引处的顶点位置。 3 /// </summary> 4 /// <param name="differenceOnScreen"></param> 5 /// <param name="viewMatrix"></param> 6 /// <param name="projectionMatrix"></param> 7 /// <param name="viewport"></param> 8 /// <param name="positionIndexes"></param> 9 public void MovePositions(Point differenceOnScreen, 10 mat4 viewMatrix, mat4 projectionMatrix, vec4 viewport, uint[] positionIndexes) 11 { 12 if (positionIndexes == null) { return; } 13 if (positionIndexes.Length == 0) { return; } 14 15 GL.BindBuffer(BufferTarget.ArrayBuffer, this.positionBufferPtr.BufferId); 16 IntPtr pointer = GL.MapBuffer(BufferTarget.ArrayBuffer, MapBufferAccess.ReadWrite); 17 unsafe 18 { 19 var array = (vec3*)pointer.ToPointer(); 20 for (int i = 0; i < positionIndexes.Length; i++) 21 { 22 vec3 projected = glm.project(array[positionIndexes[i]], 23 viewMatrix, projectionMatrix, viewport); 24 vec3 newProjected = new vec3(projected.x + differenceOnScreen.X, 25 projected.y + differenceOnScreen.Y, projected.z); 26 array[positionIndexes[i]]=glm.unProject(newProjected, 27 viewMatrix, projectionMatrix, viewport); 28 } 29 } 30 GL.UnmapBuffer(BufferTarget.ArrayBuffer); 31 GL.BindBuffer(BufferTarget.ArrayBuffer, 0); 32 }
MouseDown
鼠标按下时,如果拾取到图元,就要为拖拽做准备。(如果想了解拾取的原理,可参考CSharpGL(18)分别处理glDrawArrays()和glDrawElements()两种方式下的拾取(ColorCodedPicking))
1 private void glCanvas1_MouseDown(object sender, MouseEventArgs e) 2 { 3 if (e.Button == System.Windows.Forms.MouseButtons.Left) 4 { 5 // move vertex 6 PickedGeometry pickedGeometry = RunPicking(e.X, e.Y); 7 if (pickedGeometry != null) 8 { 9 var dragParam = new DragParam(pickedGeometry, 10 camera.GetProjectionMat4(), 11 camera.GetViewMat4(), 12 new Point(e.X, glCanvas1.Height - e.Y - 1)); 13 this.dragParam = dragParam; 14 } 15 } 16 }
其中的RunPicking就是执行一次拾取操作。
1 private PickedGeometry RunPicking(int x, int y) 2 { 3 this.glCanvas1_OpenGLDraw(selectedModel, null); 4 IColorCodedPicking pickable = this.rendererDict[this.SelectedModel]; 5 pickable.MVP = this.camera.GetProjectionMat4() * this.camera.GetViewMat4(); 6 PickedGeometry pickedGeometry = ColorCodedPicking.Pick( 7 this.camera, x, y, this.glCanvas1.Width, this.glCanvas1.Height, pickable); 8 9 return pickedGeometry; 10 }
这里有个dragParam类型,记录了按下后的一些数据。
1 class DragParam 2 { 3 4 public PickedGeometry pickedGeometry; 5 public mat4 projectionMatrix; 6 public mat4 viewMatrix; 7 public Point lastMousePositionOnScreen; 8 public vec4 viewport; 9 10 public DragParam(PickedGeometry pickedGeometry, mat4 projectionMatrix, mat4 viewMatrix, Point lastMousePositionOnScreen) 11 { 12 this.pickedGeometry = pickedGeometry; 13 this.projectionMatrix = projectionMatrix; 14 this.viewMatrix = viewMatrix; 15 this.lastMousePositionOnScreen = lastMousePositionOnScreen; 16 var viewport = new int[4]; GL.GetInteger(GetTarget.Viewport, viewport); 17 this.viewport = new vec4(viewport[0], viewport[1], viewport[2], viewport[3]); 18 } 19 }
MouseMove
鼠标开始移动后,就要实时更新模型顶点的位置了。
1 private void glCanvas1_MouseMove(object sender, MouseEventArgs e) 2 { 3 if (e.Button == System.Windows.Forms.MouseButtons.Left) 4 { 5 // move vertex 6 DragParam dragParam = this.dragParam; 7 if (dragParam != null) 8 { 9 var current = new Point(e.X, glCanvas1.Height - e.Y - 1); 10 Point differenceOnScreen = new Point( 11 current.X - dragParam.lastMousePositionOnScreen.X, 12 current.Y - dragParam.lastMousePositionOnScreen.Y); 13 dragParam.lastMousePositionOnScreen = current; 14 this.rendererDict[this.selectedModel].MovePositions( 15 differenceOnScreen, 16 dragParam.viewMatrix, dragParam.projectionMatrix, 17 dragParam.viewport, 18 dragParam.pickedGeometry.Indexes); 19 } 20 } 21 }
MouseUp
鼠标抬起,清空数据,恢复状态。
1 private void glCanvas1_MouseUp(object sender, MouseEventArgs e) 2 { 3 if (e.Button == System.Windows.Forms.MouseButtons.Left) 4 { 5 // move vertex 6 this.dragParam = null; 7 } 8 }
总结
本文虽然简单,但是我却花了好几天才解决拖拽的问题。过程中想过试过种种奇葩的方案。最后,在弄明白了project和unProject的功能后,立即想到了现在这个方案,既简单又实用。
所以说必须戒除浮躁和急切的心理,慢慢地搞清楚每一个小问题。这才是磨刀不误砍柴工。
原CSharpGL的其他功能(UI、3ds解析器、TTF2Bmp、CSSL等),我将逐步加入新CSharpGL。
欢迎对OpenGL有兴趣的同学关注(https://github.com/bitzhuwei/CSharpGL)