很长时间没写WorldWind方面的东西啦!一方面是自己最近工作上忙点,一方面因为自己热情衰减了,俗话说,“一鼓作气,再而衰,三而竭”,我现在学习WW就有点没太有毅力和士气了!本来想这周末总结一下前段时间的WorldWind学习,没想自己放假期间自制力很差,没一点效率,几乎都上网玩了。
WW的总结只能拖后了,可能过段时间有兴趣有时间了,可能会把总结写了,然后继续深入研究吧。我感觉自己学习或做事都缺点毅力,总是搞定虎头蛇尾的!本来研究WW好好的,可是看了.NET互操作方面的书感觉很好,于是兴致勃勃地学习.NET互操作。看了三章,遇到难点又想放弃来搞线程方面的。我有时都服了自己了:三心二意的!不扯周末的思想了。
WW学习研究虽没像以前那样专注,但也还是时不时关注一下的,前段时间看到3D Cross Section插件,感觉很惊奇,就想研究一下,没想到自已一拖再拖,现在才准备写点东西。
3D Cross Section插件主要是提取WW中当前视图的地形数据和影像数据,然后在新的窗口TerrainViewer中显示。也就是提取一部分三维在新的窗体里重点显示。功能就简单一说,3D Cross Section插件中实现提取WW数据的插件部分我们下次再说,我这次主要是关注TerrainViewer的实现。
TerrainViewer的功能可以单独使用,正如其名字就是一个简单的三维地形数据的浏览器,简直是Mini型的WW。但它里面内容很丰富,有很多知识点值得我们学习借鉴:一方面是C#知识;一方面是Direct3D方面知识;还有就是其中涉及数据算法方面的处理。
首先,说一下其中的C#拖拽文件到窗体打开功能的实现,看过很多软件特别是视频播放器软件,只要将视频文件拖到上面就能播放该视频;看过Office软件普遍支持拖放打开相应的文件;看过只要将文件拖入回收站就能自己删除等等。这些拖拽方法是如何在C#实现的?自己搞编程很久了,没遇到过这样的需求,也没见过这样代码实现案例,所以自己也就没深入研究这方面的知识。在研究TerrainViewer功能时,看到支持拖拽功能,就首先学习了一下它是如何实现的。自己以后的程序支持类似的拖放打开文件功能多酷?!分析一下该功能代码,与大家分享一下。
// Drag drop this .AllowDrop = true ; //允许窗口拖放 //注册拖放开始事件 this .DragEnter += new DragEventHandler( this .OnDragEnter); //注册拖放处理事件 this .DragDrop += new DragEventHandler( this .OnDragDrop);看看this.OnDragEnter和this.OnDragDrop事件处理中都分别做了什么。
// File drop handling private void OnDragEnter( object sender, System.Windows.Forms.DragEventArgs e) { e.Effect = DragDropEffects.Copy; // set the cursor to show a drop copy }上面的代码里主要是告诉,拖放的目的和效果是COPY。从下面的截图中,可以看到各种各种拖放效果。
从MSDN上截取的DragDropEffects说明:
真正处理拖放文件的打开实现的代码:
private void OnDragDrop( object sender, System.Windows.Forms.DragEventArgs e) { string theFile; try { // check to make sure the dropped item is of type FileDrop if (e.Data.GetDataPresent(DataFormats.FileDrop)) { //获取拖放数据 object filename = e.Data.GetData(DataFormats.FileDrop); //这里是数组,说明支持多选文件的同时拖放 theFile = ( string )((System.Array)filename).GetValue( 0 ); // Create map from file // MessageBox.Show("Dropped file : " + theFile); //获取拖放文件的后缀名 string ext = Path.GetExtension(theFile); string sky = skyFileName == null ? "" : skyFileName; string tex = textureFileName == null ? "" : textureFileName; if ( ! tex.StartsWith( " colors " )) tex = " colors/Geo_Water_1.png " ; //根据不同的文件类型,分别处理 switch (ext) { case " .jpg " : break ; case " .png " : // Load terrain from 8bit .png DisposeMap(); terrainFileName = theFile; skyFileName = sky; textureFileName = tex; mapName = terrainFileName; mapSpan = 0 ; mapWidth = 0 ; dem16 = false ; verticalFactor = 1.0f ; LoadMap(); break ; case " .bil " : // Load terrain from 16bit SRTM binary .bil DisposeMap(); terrainFileName = theFile; skyFileName = sky; textureFileName = tex; mapName = terrainFileName; mapSpan = 0 ; mapWidth = 0 ; dem16 = true ; verticalFactor = 1.0f ; LoadMap(); break ; case " .xml " : // Load map list from .xml DisposeMap(); mapListFileName = theFile; InitializeMapList(); // Create Map menu from .xml MapMenuSelectMap( 0 ); // Load first map break ; } } } catch (Exception ex) { // MessageBox.Show(ex.Message.ToString()); } }
从上面分析可知,我们如果要在自己的程序中实现拖放功能,只需分别实现相应的自己的事件处理。大家也可以在网上搜搜相关资料,相信大致步骤是一样的。
其次,说一下其中的键盘事件监听处理,其实在前面WW学习系列中已经分析了键盘监听处理。但是这次要分析的有的不同是,DirectX里面的键盘监听实现。该思路很新颖,自己之前没见过,在DirectX编程里可以借鉴一下。但不推荐使用,Form里的键盘监听处理已经很好很方便了,而且DirectX里键盘监听需要DirectX运行环境的。另外,除了新颖方面,我没看到该方法的优势。
private Microsoft.DirectX.DirectInput.Device keyb; //完成输入设备(键盘)对象的初始化 public void InitializeKeyboard() { keyb = new Microsoft.DirectX.DirectInput.Device(SystemGuid.Keyboard); keyb.SetCooperativeLevel( this , CooperativeLevelFlags.Background | CooperativeLevelFlags.NonExclusive); keyb.Acquire(); }
键盘监听处理方法实现:
DirectX键盘监听实现 private void ReadKeyboard() { //获取键盘所以按键状态 KeyboardState keys = keyb.GetCurrentKeyboardState(); //通过 keys[Key.LeftShift]方式 获取SHIFT键是否按下 bool shift = keys[Key.LeftShift] || keys[Key.RightShift]; bool ctrl = keys[Key.LeftControl] || keys[Key.RightControl]; double moveFactor = dist * 0.01f ; // Toggle Light if (keys[Key.L] && ! shift && ! ctrl) { showLight = true ; menuItemShowLight.Checked = showLight; redraw = true ; } if (keys[Key.L] && shift && ! ctrl) { showLight = false ; menuItemShowLight.Checked = showLight; redraw = true ; } // Create/delete light map if (keys[Key.L] && ! shift && ctrl) { if (textureFileName.IndexOf( " colors " ) == - 1 ) // only on textured maps { if (lightMapTexture != null ) lightMapTexture.Dispose(); lightMapTexture = null ; this .Cursor = Cursors.WaitCursor; lightMapTexture = LightMap(device, DEM, 1 ); this .Cursor = Cursors.Default; redraw = true ; } } if (keys[Key.L] && shift && ctrl) { if (lightMapTexture != null ) lightMapTexture.Dispose(); lightMapTexture = null ; redraw = true ; } // Create/delete section mesh if (keys[Key.S] && ! shift && ! ctrl) { if (sectionMesh != null ) sectionMesh.Dispose(); sectionMesh = null ; showSection = true ; sectionMesh = TerrainSection(device, DEM); showTransparentTerrain = true ; menuItemShowSection.Checked = showSection; redraw = true ; } //按下S键+shift键 if (keys[Key.S] && shift && ! ctrl) { if (sectionMesh != null ) sectionMesh.Dispose(); sectionMesh = null ; showSection = false ; showTransparentTerrain = false ; menuItemShowSection.Checked = showSection; redraw = true ; } // Toggle Fog if (keys[Key.F] && ! shift) { showFog = true ; menuItemShowFog.Checked = showFog; redraw = true ; } if (keys[Key.F] && shift) { showFog = false ; menuItemShowFog.Checked = showFog; redraw = true ; } // Toggle map spin if (keys[Key.Space]) { spin = false ; menuItemShowSpin.Checked = spin; redraw = true ; } if (keys[Key.Return]) { spin = true ; menuItemShowSpin.Checked = spin; redraw = true ; } // Rotate map if (keys[Key.RightArrow] && ! shift) { angle -= 0.02f ; redraw = true ; } if (keys[Key.LeftArrow] && ! shift) { angle += 0.02f ; redraw = true ; } if (keys[Key.UpArrow] && shift) { angle2 += 0.02f ; redraw = true ; } if (keys[Key.DownArrow] && shift) { angle2 -= 0.02f ; redraw = true ; } // Move map if (keys[Key.RightArrow] && shift) { dx -= ( float )(Math.Sin(angle) * moveFactor); dy -= ( float )(Math.Cos(angle) * moveFactor); redraw = true ; } if (keys[Key.LeftArrow] && shift) { dx += ( float )(Math.Sin(angle) * moveFactor); dy += ( float )(Math.Cos(angle) * moveFactor); redraw = true ; } if (keys[Key.UpArrow] && ! shift) { dx -= ( float )(Math.Cos(angle) * moveFactor); dy += ( float )(Math.Sin(angle) * moveFactor); redraw = true ; } if (keys[Key.DownArrow] && ! shift) { dx += ( float )(Math.Cos(angle) * moveFactor); dy -= ( float )(Math.Sin(angle) * moveFactor); redraw = true ; } // Change Distance if (keys[Key.NumPadPlus] && ! shift && ! ctrl) { dist -= dist * 0.02f ; redraw = true ; } if (keys[Key.NumPadMinus] && ! shift && ! ctrl) { dist += dist * 0.02f ; redraw = true ; } // Change FOV if (keys[Key.NumPadPlus] && shift && ! ctrl) { fov -= 0.01f ; redraw = true ; } if (keys[Key.NumPadMinus] && shift && ! ctrl) { fov += 0.01f ; redraw = true ; } // Change alt scale factor (vert exaggeration) if ((keys[Key.X] || keys[Key.NumPadPlus]) && ! shift && ctrl) { verticalFactor *= 1.3333f ; MenuClearCheck(menuItemVerticalFactor); menuItemVerticalFactor.MenuItems[ 0 ].Text = " x " + verticalFactor.ToString(CultureInfo.InvariantCulture); menuItemVerticalFactor.MenuItems[ 0 ].Checked = true ; this .Cursor = Cursors.WaitCursor; // Rebuilt terrain mesh DisposeTerrainMesh(); BuildTerrainMesh(); if (sidesMesh != null ) sidesMesh.Dispose(); sidesMesh = TerrainSides(device, DEM); this .Cursor = Cursors.Default; redraw = true ; } if ((keys[Key.Z] || keys[Key.NumPadMinus]) && ! shift && ctrl) { verticalFactor *= 0.75f ; MenuClearCheck(menuItemVerticalFactor); menuItemVerticalFactor.MenuItems[ 0 ].Text = " x " + verticalFactor.ToString(CultureInfo.InvariantCulture); menuItemVerticalFactor.MenuItems[ 0 ].Checked = true ; this .Cursor = Cursors.WaitCursor; // Rebuilt terrain mesh DisposeTerrainMesh(); BuildTerrainMesh(); if (sidesMesh != null ) sidesMesh.Dispose(); sidesMesh = TerrainSides(device, DEM); this .Cursor = Cursors.Default; redraw = true ; } }
上面按键处理,主要是通过特定的按键实现一些功能的执行,跟菜单里相应项是对应的。这里并没有事件调用,只是个键盘按键响应处理方法而已,真正的调用是放在OnPaint()事件处理中1089行:
// Read keyboard if(this.Focused) ReadKeyboard();
再者,讲一些TerrainViewer的核心实现,即Direct3D编程方面。这个TerrainViewer虽然很小,总共只要三千多行代码,但这该说“麻雀虽小,五脏俱全”,我称TerrainViewer是Mini版的WorldWind一点不过分,它完全拥有了WW的核心实现:Direct3D编程和地理坐标转换。Direct3D编程如果想写好是有点难,但是要实现Direct3D编程还是不难的,因为所有的Direct3D编程的套路都是一样的,简直是工厂流水线式的。大的基本步骤为Device基本参数设置和Device初始化——》构建Mesh集合——》设置Texture——》绘制渲染参数设置——》渲染。真正的难点是构建Mesh集合,就是三维建模吧!最后渲染过程是在OnPaint()事件里实现的,这里一般会在最后调用 this.Invalidate();实现不断刷新界面实现不断重绘。
OnPaint()里面的重绘渲染,是通过redraw标识来控制是否需要重绘的。
else // No redraw { device.BeginScene(); device.EndScene(); device.Present(); System.Threading.Thread.Sleep(50); }
上面的代码很好地解决了不必要的重绘问题,WorldWind的里面的重绘是“牵一发而动全身”,我一直在想能否减少WW里不必要的重绘,难道不能尽可能地只重渲染必须的部分嘛?!WW提高效率问题可以考虑从此点开始。这部分Direct3D编程是重点,但是没有深入一步步分析各部分MESH的如何构建,因为几乎所有插件在实现三维渲染上步骤一致,实现上也是反复说Direct3D编程,几乎跟处理上WW里的一致,所以就不再赘述啦!
最后,说一下DEM数据的使用,主要包括两方面:DEM文件使用和高程值获取。
DEM文件使用:jpg和png直接使用,bil格式的先要转换成BitMap.请看下面LoadMap()方法的734行代码:
string filePath = GetFullPathTo(terrainFileName); if (Path.GetExtension(terrainFileName) != " .bil " ) { DEM = new Bitmap(filePath); // .jpg, .png dem16 = false ; } else { DEM = BitmapFromBil(filePath); // .bil dem16 = true ; }
将bil格式转换成BitMap:
// Converts a .bil elevation data file into a Bitmap object private Bitmap BitmapFromBil( string bilFile) { int width = 150 ; // default size int height = 150 ; // Bitmap DEM; using ( Stream s = File.OpenRead(bilFile)) { // Find out dem size FileInfo demFile = new FileInfo(bilFile); long length = demFile.Length; if (length != 0 ) { width = height = ( int )Math.Sqrt(length / 2 ); if (width * height * 2 != length) throw new ApplicationException( " .bil file size not double of a square (eg. 150x150x2) " ); } DEM = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); byte [] tfBuffer = new byte [width * height * 2 ]; if (s.Read(tfBuffer, 0 ,tfBuffer.Length) < tfBuffer.Length) throw new IOException( string .Format( " End of file error while reading file '{0}'. " , bilFile) ); int offset = 0 ; for ( int y = 0 ; y < height; y ++ ) { for ( int x = 0 ; x < width; x ++ ) { // 16 bit values int low = tfBuffer[offset ++ ]; int hi = tfBuffer[offset]; int hi2 = ( short )(tfBuffer[offset ++ ] << 8 ); // Scale down to 0..255 int alt = ( int )(( float )(hi2 + low) * 255f / 9000f); if (alt < 0 ) alt = 0 ; if (alt > 255 ) alt = 255 ; // Store altitude in red, and original 16 bit value in g and b DEM.SetPixel(x, y, Color.FromArgb( 0xff , alt, low, hi)); } } } dem16 = true ; return DEM; }
高程值获取获取方法, float GetAlt(Bitmap DEM, float x, float y)和int GetAlt(Bitmap DEM, int x, int y)。这两个不同之处是,第一个方法里面调用了第二个方法,第二个是真正获取高程值的,而且是获取整数点上的高程值。
// Get elevation from DEM at exact location (integer coord) public int GetAlt(Bitmap DEM, int x, int y) { int alt = 0 ; if (x >= 0 && x <= DEM.Width - 1 && y >= 0 && y <= DEM.Height - 1 ) { Color p = DEM.GetPixel(x, y); alt = ( int )p.R; // Get altitude from red (8 bit) if (dem16) { // Get 16bit altitude from g/b alt = ( short )(p.B << 8 ) + ( int )p.G; // Check for negative values if (alt > 32767 ) alt = 65536 - alt; } } return alt; }第一个方法是通过调用第一个方法,然后通过插值计算的方法,获取任意点的插值。
获取任意点高程的代码 // Get averaged elevation from DEM at decimal location (float coord) public float GetAlt(Bitmap DEM, float x, float y) { float alt = 0f; if (x >= 0 && x <= DEM.Width - 1 && y >= 0 && y <= DEM.Height - 1 ) { int xNW = ( int )Math.Floor(x); // North-West corner int yNW = ( int )Math.Floor(y); float xF = ( float )x - xNW; // x factor 0...1 float yF = ( float )y - yNW; // y factor 0...1 //分别获取包含该点的最小矩形的四点的高程值 int altNW = GetAlt(DEM, xNW, yNW); // Alt north-west int altNE = GetAlt(DEM, xNW + 1 , yNW); // Alt north-east int altSW = GetAlt(DEM, xNW, yNW + 1 ); // Alt south-west int altSE = GetAlt(DEM, xNW + 1 , yNW + 1 ); // Alt south-east //插值获取该点的高程值 float altN = ( float )altNW * (1f - xF) + ( float )altNE * xF; // North average float altS = ( float )altSW * (1f - xF) + ( float )altSE * xF; // South average alt = altN * (1f - yF) + altS * yF; // North-South average } return alt; }
太晚了,不再详细分析最后部分代码了。下次有时间说一下3D Cross Section插件加载部分的代码实现,也算是完整分析个插件功能。希望大家能有所收获。
转载于:https://www.cnblogs.com/wuhenke/archive/2010/01/17/1650236.html