WorldWind学习系列十一:Virtual Earth插件学习

it2024-11-05  13

    学习WorldWind有很长时间了,理论学习算是基本完成了。我体会是WW的学习主要分为两大步:WW框架体系学习和WW插件学习。学习WW插件逐步深入后,必然要首先学习Direct3D编程,这也算是我的经验之谈吧。今天Virtual Earth插件学习完成,也标志着我可以从WW理论转向WW实践啦。虽然我总结介绍的是Virtual Earth插件,但是希望网友阅读下面的内容前,最好能够先深入学习Direct3D编程、BMNG和Globe Icon插件的底层渲染,这些都是学习Virtual Earth的基础。

  Virtual Earth插件包括以下几个类:

  VirtualEarthForm 窗体类

  VirtualEarthPlugin插件类,继承自Plugin(重要)

  VeReprojectTilesLayer 渲染对象类,继承自Renderable Object(重要)

  VeTile 瓦片对象类(真正实现大部分功能的)(重要)

  Projection投影变换类(重要)

  Search类

  PushPin类

  我们先看看VirtualEarthPlugin,所有的插件类必须继承自Plugin.cs,必须重写Load()和Unload()方法。这两个方法分别实现插件的加载和卸载。

  Load()方法一般是实现添加菜单和添加工具按钮,跟前面介绍插件都很类似的,自己开发插件时模仿着这套路写就行。

   public   override   void  Load()        {             try             {            //判断当前World为Earth(注:其他星体可通过名字判断)                  if  (ParentApplication.WorldWindow.CurrentWorld.IsEarth)                {  //初始化VE插件控制窗体                     m_Form  =   new  VirtualEarthForm(ParentApplication);                    m_Form.Owner  =  ParentApplication;            //添加VE插件菜单                     m_MenuItem  =   new  MenuItem( " MicroSoft VirtualEarth " );                    m_MenuItem.Click  +=   new  EventHandler(menuItemClicked);                    ParentApplication.PluginsMenu.MenuItems.Add(m_MenuItem);                     // #if DEBUG                      string  imgPath  =  Path.GetDirectoryName(System.Windows.Forms.Application.ExecutablePath)  +   " \\Plugins\\VirtualEarth\\VirtualEarthPlugin.png " ;                     // #else                     //                     _pluginDir = this.PluginDirectory;                     //                     string imgPath = this.PluginDirectory + @"\VirtualEarthPlugin.png";                     // #endif                      if  (File.Exists(imgPath)  ==   false )                    {                        Utility.Log.Write( new  Exception( " imgPath not found  "   +  imgPath));                    }              //添加工具按钮,会出现工具栏里                     m_ToolbarItem  =   new  WorldWind.WindowsControlMenuButton(                         " MicroSoft VirtualEarth " ,                        imgPath,                        m_Form);                    ParentApplication.WorldWindow.MenuBar.AddToolsMenuButton(m_ToolbarItem);                     base .Load();                }            }             catch  (Exception ex)            {                Utility.Log.Write(ex);                 throw ;            }        }

  Unload()方法就是释放插件窗体,并移除插件菜单项和插件工具按钮。

  我们再来看看VeReprojectTilesLayer 类,像其他插件类一样,要重载Initialize()、Update()、Render()方法,但是,VE插件重点在Upadate()方法,当然他还有其他自己特色的方法。

  Initialize()方法主要是一些该类全局对象的实例化,如:投影、VE控制窗体及VeTile初始化等。

  Render()方法主要是实现插件的三维渲染功能的,但是VE的Render真正实现是调用VeTile类中的Render()方法。

VE插件渲染代码   ///   <summary>          ///  Draws the layer         ///   </summary>          public   override   void  Render(DrawArgs drawArgs)        {             try             {                 if  ( this .isOn  ==   false )                {                     return ;                }                 if  ( this .isInitialized  ==   false )                {                     return ;                }                 if  (drawArgs.device  ==   null )                     return ;                 if  (veTiles  !=   null   &&  veTiles.Count  >   0 )                {                     // render mesh and tile(s)                      bool  disableZBuffer  =   false // TODO where do i get this setting                     // foreach(VeTile veTile in veTiles)                     // {                     //     veTile.Render(drawArgs, disableZBuffer);                     // }                     //  camera jitter fix                     drawArgs.device.Transform.World  =  Matrix.Translation(                           ( float ) - drawArgs.WorldCamera.ReferenceCenter.X,                        ( float ) - drawArgs.WorldCamera.ReferenceCenter.Y,                        ( float ) - drawArgs.WorldCamera.ReferenceCenter.Z                    );                     //  Clear ZBuffer between layers (as in WW)                     drawArgs.device.Clear(ClearFlags.ZBuffer,  0 1.0f 0 );                     //  Render tiles( 这里的GetZoomLevelByTrueViewRange方法是个知识点,该方法是根据WorldCamera视角范围,求取当前球体的缩放层次)              int  zoomLevel  =   GetZoomLevelByTrueViewRange (drawArgs.WorldCamera.TrueViewRange.Degrees);                   //真正的渲染处理是由VeTile类的Render()方法实现的。            int  tileDrawn  =  VeTile.Render(drawArgs, disableZBuffer, veTiles, zoomLevel);  //  Try current level first           if (tileDrawn  ==   0 ) VeTile.Render(drawArgs, disableZBuffer, veTiles, prevLvl);  //  If nothing drawn, render previous level                     // camera jitter fix                     drawArgs.device.Transform.World  =  drawArgs.WorldCamera.WorldMatrix;                     // Render logo                     RenderDownloadProgress(drawArgs,  null 0 );                }                 // else pushpins only                 // render PushPins                  if  (pushPins  !=   null   &&  pushPins.Count  >   0 )                {                    RenderPushPins(drawArgs);                }            }             catch  (Exception ex)            {                Utility.Log.Write(ex);            }        }

 

   VE中获取缩放级别的方法是很巧妙的,根据原作者文章知:VE的缩放级别是1-19级甚至更低,是以2的阶乘递减的。方法我们自己开发时可以重用,但是它的思想还是要好好体会的。

  注:缩放级别为 180 、90、45、22.5、……

根据视角范围获取缩放级别          public   int  GetZoomLevelByTrueViewRange( double  trueViewRange)        {             int  maxLevel  =   3 ; //视角范围为45度             int  minLevel  =   19 ;             int  numLevels  =  minLevel  -  maxLevel  +   1 ;             int  retLevel  =  maxLevel;             for  ( int  i  =   0 ; i  <  numLevels; i ++ )            {                retLevel  =  i  +  maxLevel;                 double  viewAngle  =   180 ;                 for  ( int  j  =   0 ; j  <  i; j ++ )                {                    viewAngle  =  viewAngle  /   2.0 ;                }                 if  (trueViewRange  >=  viewAngle)                {                     break ;                }            }             return  retLevel;        }

 

  VE插件最最关键的方法为Update(),因为VE实质上就是不断地根据缩放级别更新影像数据,其实就是构建要渲染绘制的对象集合,最后由Render()方法完成渲染绘制。所以该方法是VE中最复杂的,当然也就是VE的处理精华所在,也就是我们研究学习的重点啦。(说这些,主要是告诉大家这里是重点,要好好研究和关注)

 

Update()代码          ///   <summary>          ///  Update layer (called from worker thread)         ///   </summary>          public   override   void  Update(DrawArgs drawArgs)        {             try             {                 if  ( this .isOn  ==   false )                {                     return ;                }                 // NOTE for some reason Initialize is not getting called from the Plugin Menu Load/Unload                 // it does get called when the plugin loads from Startup                 // not sure what is going on, so i'll just call it manually                  if  ( this .isInitialized  ==   false )                {                     this .Initialize(drawArgs);                     return ;                }                 // get lat, lon 获取经纬度、倾斜角度、高度等                  double  lat  =  drawArgs.WorldCamera.Latitude.Degrees;                 double  lon  =  drawArgs.WorldCamera.Longitude.Degrees;                 double  tilt  =  drawArgs.WorldCamera.Tilt.Degrees;                 // determine zoom level                  double  alt  =  drawArgs.WorldCamera.Altitude;                 // could go off distance, but this changes when view angle changes                 // Angle fov = drawArgs.WorldCamera.Fov;  // stays at 45 degress                 // Angle viewRange = drawArgs.WorldCamera.ViewRange;  // off of distance, same as TVR but changes when view angle changes                // 获取当前视角范围                        Angle tvr  =  drawArgs.WorldCamera.TrueViewRange;  // off of altitude                 // smallest altitude = 100m                 // tvr = .00179663198575926                 // start altitude = 12756273m                 // tvr = 180                 // WW _levelZeroTileSizeDegrees  获取缩放级别,上面已经介绍啦                  // 180 90 45 22.5 11.25 5.625 2.8125 1.40625 .703125 .3515625 .17578125 .087890625 0.0439453125 0.02197265625 0.010986328125 0.0054931640625                  int  zoomLevel  =  GetZoomLevelByTrueViewRange(tvr.Degrees);             //只要到一定缩放级别时,才启用VE(这里我们可以学习,来控制一些图层的显示)                  // dont start VE tiles until a certain zoom level                  if  (zoomLevel  <  veForm.StartZoomLevel)                {                     this .RemoveAllTiles();             this .ForceRefresh();                     return ;                }                 // WW tiles                 // double tileDegrees = GetLevelDegrees(zoomLevel);                 // int row = MathEngine.GetRowFromLatitude(lat, tileDegrees);                 // int col = MathEngine.GetColFromLongitude(lon, tileDegrees);                 // VE tiles                  double  metersY;                 double  yMeters;                 int  yMetersPerPixel;                 int  row;                 /*                 //WRONG - doesn't stay centered away from equator                //int yMeters = LatitudeToYAtZoom(lat, zoomLevel); //1024                double sinLat = Math.Sin(DegToRad(lat));                metersY = earthRadius / 2 * Math.Log((1 + sinLat) / (1 - sinLat)); //0                yMeters = earthHalfCirc - metersY; //20037508.342789244                yMetersPerPixel = (int) Math.Round(yMeters / MetersPerPixel(zoomLevel));                row = yMetersPerPixel / pixelsPerTile;                 */                  // CORRECT                 // int xMeters = LongitudeToXAtZoom(lon, zoomLevel);  // 1024          //计算弧长,(earthRadius 地球半径,DegToRad(lon)经度的度值转弧度值)         //DegToRad()为角度转弧度的方法,很简单。弧长=半径*弧度值。(这里球体的弧度值认为有负值)                  double  metersX  =  earthRadius  *  DegToRad(lon);  // 0               //因为从西到东算列数,所以要从-180度算列所在的弧度的总长度的                          double  xMeters  =  earthHalfCirc  +  metersX;  // 20037508.342789244           //获取总的像素数。MetersPerPixel(zoomLevel))是计算每像素代表的米数。(知识点)                  int  xMetersPerPixel  =  ( int )Math.Round(xMeters  /  MetersPerPixel(zoomLevel));           //获取列数,从西-》东(-180-》180)来算列数                  int  col  =  xMetersPerPixel  /  pixelsPerTile;                           // reproject - overrides row above                 // this correctly keeps me on the current tile that is being viewed         //使用UV结构体来存放经纬度,然后调用proj.Forward(uvCurrent)实现投影变换                 UV uvCurrent  =   new  UV(DegToRad(lon), DegToRad(lat));                uvCurrent  =  proj.Forward(uvCurrent);                metersY  =  uvCurrent.V;           //这里为啥是“—”??看过该插件原作者的文章才会知道:原来VE的行数是从北向南算的,而WW是从南向北计算行数的。                 yMeters  =  earthHalfCirc  -  metersY;          //获取总像素数                yMetersPerPixel  =  ( int )Math.Round(yMeters  /  MetersPerPixel(zoomLevel));           //获取行数                 row  =  yMetersPerPixel  /  pixelsPerTile; 说明:计算行列数,是为了后面获取切片后图片,并将正确的图片作为纹理渲染到正确的位置上,当然这过程还有很多处理,我会一一分析的。                  // update mesh if VertEx changes                  if  (prevVe  !=  World.Settings.VerticalExaggeration)                {                     lock  (veTiles.SyncRoot)                    {                        VeTile veTile;                         for  ( int  i  =   0 ; i  <  veTiles.Count; i ++ )                        {                            veTile  =  (VeTile)veTiles[i];                             if  (veTile.VertEx  !=  World.Settings.VerticalExaggeration)                            {                //创建网格MESH(稍后重点分析)                          veTile.CreateMesh( this .Opacity, World.Settings.VerticalExaggeration);                            }                        }                    }                }                prevVe  =  World.Settings.VerticalExaggeration;                 // if within previous bounds and same zoom level, then exit                  if  (row  ==  prevRow  &&  col  ==  prevCol  &&  zoomLevel  ==  prevLvl  &&  tilt  ==  preTilt)                {                     return ;                }                 // System.Diagnostics.Debug.WriteLine("CHANGE");                  lock  (veTiles.SyncRoot)                {                    VeTile veTile;             //使之前存放的veTile,标记为“暂时不需要”,这里没有删除                      for  ( int  i  =   0 ; i  <  veTiles.Count; i ++ )                    {                        veTile  =  (VeTile)veTiles[i];                        veTile.IsNeeded  =   false ;                    }                }                 // metadata                 ArrayList alMetadata  =   null ;                 if  (veForm.IsDebug  ==   true )                {                    alMetadata  =   new  ArrayList();                    alMetadata.Add( " yMeters  "   +  yMeters.ToString());                    alMetadata.Add( " metersY  "   +  metersY.ToString());                    alMetadata.Add( " yMeters2  "   +  yMeters.ToString());                    alMetadata.Add( " vLat  "   +  uvCurrent.V.ToString());                     // alMetadata.Add("xMeters " + xMeters.ToString());                     // alMetadata.Add("metersX " + metersX.ToString());                     // alMetadata.Add("uLon " + uvCurrent.U.ToString());                 }          //添加目前VeTile(这是重点,稍后分析)                  // add current tiles first                 AddVeTile(drawArgs, row, col, zoomLevel, alMetadata);                 // then add other tiles outwards in surrounding circles            //添加周边的VeTile                 AddNeighborTiles(drawArgs, row, col, zoomLevel,  null 1 );                AddNeighborTiles(drawArgs, row, col, zoomLevel,  null 2 );                AddNeighborTiles(drawArgs, row, col, zoomLevel,  null 3 );         //  Extend tile grid if camera tilt above some values                 //根据倾斜角度决定是否继续添加相邻Tile单位                             if (tilt  >   45 ) AddNeighborTiles(drawArgs, row, col, zoomLevel,  null 4 );                 if (tilt  >   60 ) AddNeighborTiles(drawArgs, row, col, zoomLevel,  null 5 );                 // if(prevLvl > zoomLevel)  // zooming out                 // {                 // }                              lock  (veTiles.SyncRoot)                {                    VeTile veTile;             //移除不需要的veTile图片,为啥是现在移除??(思考一下有啥好处)                     for  ( int  i  =   0 ; i  <  veTiles.Count; i ++ )                    {                        veTile  =  (VeTile)veTiles[i];                         if  (veTile.IsNeeded  ==   false )                        {                            veTile.Dispose();                            veTiles.RemoveAt(i);                i -- ;                        }                    }                }        //保存当前基本的行列、缩放级别、倾斜角度                prevRow  =  row;                prevCol  =  col;                prevLvl  =  zoomLevel;        preTilt  =  tilt;            }             catch  (Exception ex)            {                Utility.Log.Write(ex);            }        }

 

 我们来看看AddVeTile(drawArgs, row, col, zoomLevel, alMetadata);方法,因为该方法是AddNeighborTiles(drawArgs, row, col, zoomLevel, null1);方法的基础。

AddVeTile()方法代码

 private void AddVeTile(DrawArgs drawArgs, int row, int col, int zoomLevel, ArrayList alMetadata)        {            //TODO handle column wrap-around            //haven't had to explicitly handle this yet            bool tileFound = false;            lock (veTiles.SyncRoot)            {                foreach (VeTile veTile in veTiles)                {                    if (veTile.IsNeeded == true)                    {                        continue;                    }

  //判断当前的veTile对象,是否是当前缩放级别下的行列对应的瓦片,这里涉及到VeTile类的IsEqual()方法                    if (veTile.IsEqual(row, col, zoomLevel) == true)                    {                        veTile.IsNeeded = true;                        tileFound = true;                        break;                    }                }            }            if (tileFound == false)            {                //exit if zoom level has changed

               //获取当前新的缩放级别                int curZoomLevel = GetZoomLevelByTrueViewRange(drawArgs.WorldCamera.TrueViewRange.Degrees);                if (curZoomLevel != zoomLevel)                {                    return;                }

         //之前没下载过,需要创建新的VeTile对象。CreateVeTile方法很重要                VeTile newVeTile = CreateVeTile(drawArgs, row, col, zoomLevel, alMetadata);                newVeTile.IsNeeded = true;                lock (veTiles.SyncRoot)                {                    veTiles.Add(newVeTile);                }            }        }

CreateVeTile()方法实现,相当底层啦!我自己研究了很长时间,希望能给大家说明白。即使不明白,知道是干啥知道咋用就行啦,不用深究原理的。

        private VeTile CreateVeTile(DrawArgs drawArgs, int row, int col, int zoomLevel, ArrayList alMetadata)        {

           //初始化VeTile对象            VeTile newVeTile = new VeTile(row, col, zoomLevel);            //metadata            if (alMetadata != null)            {                foreach (string metadata in alMetadata)                {                    newVeTile.AddMetaData(metadata);                }            }       //获取纹理图片,即从服务器端下载图片用作纹理(重点,稍后详细分析)            //thread to download new tile(s) or just load from cache            newVeTile.GetTexture(drawArgs, pixelsPerTile);            //handle the diff projection

      //每个像素代表的距离            double metersPerPixel = MetersPerPixel(zoomLevel);

        //获取当前级别总的网格边数            double totalTilesPerEdge = Math.Pow(2, zoomLevel);

            //总长度(我认为:就是地球周长)            double totalMeters = totalTilesPerEdge * pixelsPerTile * metersPerPixel;

            //(我认为:halfMeters就是地球周长的一半)            double halfMeters = totalMeters / 2;            //do meters calculation in VE space            //the 0,0 origin for VE is in upper left

            //VE空间坐标系下,计算距离,原点在左上角

       //首先求出WW坐标系下的,NW(这里就是上面求行列数的逆运算啦)            double N = row * (pixelsPerTile * metersPerPixel);            double W = col * (pixelsPerTile * metersPerPixel);            //now convert it to +/- meter coordinates for Proj.4            //the 0,0 origin for Proj.4 is 0 lat, 0 lon            //-22 to 22 million, -11 to 11 million

            //在以经纬度为0 0为原点的坐标系下的新的N W,坐标系方向轴为 N——S; W——E                  N = halfMeters - N;            W = W - halfMeters;

            //计算出单元格的 E S            double E = W + (pixelsPerTile * metersPerPixel);            double S = N - (pixelsPerTile * metersPerPixel);       

       //给新的瓦片单元格的UL UR LL LR赋值,其实就是单元网格的四角的坐标(理解这点很重要,是为了创建Mesh用)            newVeTile.UL = new UV(W, N);            newVeTile.UR = new UV(E, N);            newVeTile.LL = new UV(W, S);            newVeTile.LR = new UV(E, S);            //create mesh            byte opacity = this.Opacity; //from RenderableObject            float verticalExaggeration = World.Settings.VerticalExaggeration;

           //重点CreateMesh()方法,里面也涉及数学,不是太容易理解的            newVeTile.CreateMesh(opacity, verticalExaggeration);            newVeTile.CreateDownloadRectangle(drawArgs, World.Settings.DownloadProgressColor.ToArgb());            return newVeTile;        }

VeTile类,是真正实现渲染方法,也主要用于球体和平面坐标的转换处理、计算,算是VE插件的核心啦,这里面涉及大量的数学知识,主要编程为Direct3D编程。我们现在来看看CreateMesh()方法。

注:如果你看懂CreateMesh(),就说明你已经理解VE大部分啦

    // NOTE this is a mix from Mashi's Reproject and WW for terrain          public   void  CreateMesh( byte  opacity,  float  verticalExaggeration)        {             this .vertEx  =  verticalExaggeration;             int  opacityColor  =  System.Drawing.Color.FromArgb(opacity,  0 0 0 ).ToArgb();            meshPointCount  =   32 // 64;  // 96  //  How many vertices for each direction in mesh (total: n^2)             // vertices = new CustomVertex.PositionColoredTextured[meshPointCount * meshPointCount];             //  Build mesh with one extra row and col around the terrain for normal computation and struts             vertices  =   new  CustomVertex.PositionNormalTextured[(meshPointCount  +   2 *  (meshPointCount  +   2 )];             int  upperBound  =  meshPointCount  -   1 ;             float  scaleFactor  =  ( float ) 1   /  upperBound;             // using(Projection proj = new Projection(m_projectionParameters))             // {              double  uStep  =  (UR.U  -  UL.U)  /  upperBound;             double  vStep  =  (UL.V  -  LL.V)  /  upperBound;            UV curUnprojected  =   new  UV(UL.U  -  uStep, UL.V  +  vStep);        // 将平面坐标投影转换为WW球面坐标             //  figure out latrange (for terrain detail)             UV geoUL  =  _proj.Inverse(m_ul);            UV geoLR  =  _proj.Inverse(m_lr);             double  latRange  =  (geoUL.U  -  geoLR.U)  *   180   /  Math.PI;        // 将弧度转为角度(这里没再使用下面四个变量,原方法用到啦,不研究!)             North  =  geoUL.V  *   180   /  Math.PI;            South  =  geoLR.V  *   180   /  Math.PI;            West  =  geoUL.U  *   180   /  Math.PI;            East  =  geoLR.U  *   180   /  Math.PI;        // 半径              float  meshBaseRadius  =  ( float )_layerRadius;            UV geo;            Vector3 pos;             double  height  =   0 ;             for  ( int  i  =   0 ; i  <  meshPointCount  +   2 ; i ++ )            {                 for  ( int  j  =   0 ; j  <  meshPointCount  +   2 ; j ++ )                {  // 将平面坐标(proj 4)投影转换为WW球面坐标                     geo  =  _proj.Inverse(curUnprojected);                    // 将弧度转为角度                     //  Radians -> Degrees                     geo.U  *=   180   /  Math.PI;                    geo.V  *=   180   /  Math.PI;                     if  (_terrainAccessor  !=   null )                    {                         if  (_veForm.IsTerrainOn  ==   true )                        {                             // height = heightData[i, j] * verticalExaggeration;                             // original : need to fetch altitude on a per vertex basis (in VE space) to have matching tile borders (note PM)                             height  =  verticalExaggeration  *  _terrainAccessor.GetElevationAt(geo.V, geo.U, Math.Abs(upperBound  /  latRange));                        }                         else                         {                            height  =   0 ;                        }                    }             // 将空间坐标转换为笛卡尔坐标                     pos  =  MathEngine.SphericalToCartesian(                        geo.V,                        geo.U,                        _layerRadius  +  height);                  // 构建Mesh顶点集合                      int  idx  =  i  *  (meshPointCount  +   2 +  j;                    vertices[idx].X  =  pos.X;                    vertices[idx].Y  =  pos.Y;                    vertices[idx].Z  =  pos.Z;                     // double sinLat = Math.Sin(geo.V);                     // vertices[idx].Z = (float) (pos.Z * sinLat);                     vertices[idx].Tu  =  (j  -   1 *  scaleFactor;                    vertices[idx].Tv  =  (i  -   1 *  scaleFactor;                     // vertices[idx].Color = opacityColor;                     curUnprojected.U  +=  uStep;                }                curUnprojected.U  =  UL.U  -  uStep;                curUnprojected.V  -=  vStep;            }             // } // 构建索引集合,分组存放Mesh点集合中的点。学Direct3D编程,看懂下面的很容易的              int  slices  =  meshPointCount  +   1 ;            indices  =   new   short [ 2   *  slices  *  slices  *   3 ];             for  ( int  i  =   0 ; i  <  slices; i ++ )            {                 for  ( int  j  =   0 ; j  <  slices; j ++ )                {                    indices[( 2   *   3   *  i  *  slices)  +   6   *  j]  =  ( short )(i  *  (meshPointCount  +   2 +  j);                    indices[( 2   *   3   *  i  *  slices)  +   6   *  j  +   1 =  ( short )((i  +   1 *  (meshPointCount  +   2 +  j);                    indices[( 2   *   3   *  i  *  slices)  +   6   *  j  +   2 =  ( short )(i  *  (meshPointCount  +   2 +  j  +   1 );                    indices[( 2   *   3   *  i  *  slices)  +   6   *  j  +   3 =  ( short )(i  *  (meshPointCount  +   2 +  j  +   1 );                    indices[( 2   *   3   *  i  *  slices)  +   6   *  j  +   4 =  ( short )((i  +   1 *  (meshPointCount  +   2 +  j);                    indices[( 2   *   3   *  i  *  slices)  +   6   *  j  +   5 =  ( short )((i  +   1 *  (meshPointCount  +   2 +  j  +   1 );                }            }             //  Compute normals and fold struts             calculate_normals();            fold_struts( false , meshBaseRadius);        }

 

 

      

  (未完待续……

 

 

  

转载于:https://www.cnblogs.com/wuhenke/archive/2009/12/24/1631739.html

相关资源:数据结构—成绩单生成器
最新回复(0)