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)
文章列表