WorldWind学习总结三
转载自:http://blog.csdn.net/paul_xj/article/details/1792486
可能学习过程中给自己制造压力太大,有时候一天没有收获就会很难受,但是像WW这样的项目到一定程度会有个难过的坎而几天没有进步是很正常的,对自己要求太多而实现不了常有很坏的影响,待调整好再理清思路又过了不少时间,用一个好的心态来稳步进步,终有一天能完全理解,这也是个量变到质变的绝对过程,如果遇到了难以进步的地方,不如看看理论知识,再加强下3d知识,看看窗外的云,其实真正的收获也就是那“妙手偶得”的灵感瞬间:)
抛个砖,希望能引来朋友们更多更精采的技术性文档,共同学习进步
一、 LOD模型的补充说明
LOD模型mesh grid(网格)的生成(包括优化,视域剔除,背面剔除,遮挡剔除),对应纹理的U、V坐标,对条带化后三角形格网及u,v坐标的缓存,World Wind1.4.1只使用了“视域剔除”—使用BoundBox(包围盒)算法,下面对这些简要介绍。
有两种类型的mesh grid,可以quadTile.cs的定义里找到,对应两种mesh grid拆分的顶点数(实际拆分时还有些运算)
/// Number of points in child flat mesh grid (times 2)
protected static int vertexCount = 40;
/// Number of points in child terrain mesh grid (times 2)
protectedstatic int vertexCountElevated = 40;
两种mesh grid调用时机为:
if (QuadTileSet.TerrainMapped &&Math.Abs(verticalExaggeration) > 1e-3)
CreateElevatedMesh();
else
CreateFlatMesh();
有地形数据,并且地形的高度以夸大值大于1e-3时创建带有高度的地形,否则平坦地形网格。
实际渲染的纹理为512*512像素的,创建mesh grid时平均分为四个子树,在同一纹理里的分辩 率是相同的
protected QuadTile northWestChild;
protected QuadTile southWestChild;
protected QuadTile northEastChild;
protected QuadTile southEastChild;
四个子树不再进行拆分,顶点和u,v座标信息保存在:
protectedCustomVertex.PositionNormalTextured[] northWestVertices;
protected CustomVertex.PositionNormalTextured[] southWestVertices;
protected CustomVertex.PositionNormalTextured[] northEastVertices;
protected CustomVertex.PositionNormalTextured[] southEastVertices;
对于FlatMesh每个子树拆分为21*21个结点,对于ElevatedMesh每个子树拆分为23*23个结点, 分别计划顶点坐标和u,v坐标,由于每个顶点差不多被重用6次,在实际渲染中使用顶点索引,对mesh grid数据进行三角形条带化后再直接渲染和缓存。
ElevateMesh的高程数据从函数
TerrainTile tile =QuadTileSet.World.TerrainAccessor.GetElevationArray()取得,返回的tile 的成员ElevationData保存一个43*43的二维数组。
缓存机制:每个QuadTile对象都对应一个string类型的key变量,生成算法为:
key = string.Format("{0,4}",this.Level)
+ "_"
+ string.Format("{0,4}",this.Col)
+ string.Format("{0,4}",this.Row)
+ this.QuadTileSet.Name
+ this.QuadTileSet.ParentList.Name;可保证key的唯一性,每次计算完一区域的 mesh grid数据及u,v座标后,都保存在一静态变量中:internalstatic Dictionary<string,CacheEntry> VerticeCache= new Dictionary<string,CacheEntry>();,缓存大小为internalstatic int CacheSize = 256;,类CashEntry的定义为:
internal class CacheEntry
{
public CustomVertex.PositionNormalTextured[] northWestVertices;
public CustomVertex.PositionNormalTextured[] southWestVertices;
public CustomVertex.PositionNormalTextured[] northEastVertices;
public CustomVertex.PositionNormalTextured[] southEastVertices;
public short[] vertexIndexes;
public DateTime EntryTime;
}
保存mesh grid及u,v值,对应的顶点索引缓冲vertexIndexes,网格信息的添加时间EntryTime,每增加一个网格信息都调用VerticeCache.Add(key, c);进行缓存,若超过CacheSize上限,则寻找过期的key进行剔除,可能这个剔除算法并不保证一定删除成员,因为在实际跟踪过程中发现VerticeCache的count值达到400以上,并且剔除算法没有删除任务成员。
可以看到WW的LOD数据每个level是平均划分的,并没有做地形起伏LOD,只是根据视点的远近划分了不同的level
二、WW的渲染结构
WW中除了标题和菜单的工作区中的button,layer,line,point,line,model,当然还有planet默认为earth的地形纹理,及其上所有信息,都做为WW的渲染对象,保存在World.cs中的RenderableObjectList _renderableObjects; RenderableObjectList的继承关系如下:
IRenderable |
IComparable |
RenderableObjectList |
GPSTrackerOverlay |
Icons |
PathList |
RenderableObject |
实际的点线,平面纹理等渲染对象都是从RenderableObject继承,最终的渲染实现也是在从它继续下来的类中,RenderableObjectList的成员m_children(protectedArrayList m_children = newArrayList();)包含WW中所有的渲染对象,绘制过程中按如下的优先级进行:
public enumRenderPriority
{
SurfaceImages = 0,
TerrainMappedImages = 100,
AtmosphericImages = 200,
LinePaths = 300,
Icons = 400,
Placenames = 500,
Custom = 600
}
这里对WW调试过程中的m_children的成员做个截图,需要注意的是m_children的成员大部分还是RenderableObjectList对象,向下包含的层次很多,但只有最底层的从RenderableObject继续的对象才是渲染的最终实现:
从下图可以看到里面的成员和Layer界面下的组织结构很相似,其实最终的结时会是一样的!
从RenderableObject继承的对象列表如下(红色部分是需要着重看的,WW的主要算法部分);
RenderableObject |
StereoLayer |
AtmosphereLayer |
CompassLayer |
FloodLevel |
GPSTrackerOverlay |
GPSTrackerFixInfo |
GPSTrackLine |
GPSIcon |
GPSGeoFence |
WaitMessage |
Stars3DLayer |
BoundaryLayer |
DownloadableImageFromIconSet |
DownloadableIcon |
Icon |
ImageLayer |
LatLongGrid |
MeshLayer |
PathLine |
PolygonLayer |
QuadTileSet |
Bar3D |
WavingFlagLayer |
ShapeLayer |
TerrainPath |
TiledPlacenameSet |
TiledWFSPlacenameSet |
World |
WaitMessage |
ClassificationBanner |
RenderableObject |
QuadTile |
n… |
1 |
BoundingBox |
1 |
1 |
RenderableObjectList |
1 |
1 |
MeshLayer |
三、WW启动过程
程序的入口点当然是在WorldWind.cs里的main函数,在这里将创建一个WW程序的实例MainApplication,调用LoadSettings进行一些必要的初始化,加载除了WW工作区以外的所有菜单,及点击菜单项弹出来的对话框项,声明一个WorldWindow实例来处理WW工作区内所有图层,按扭的渲染,WorldWindow里的World对象专门用来处理planet的所有渲染信息,当然现在默认的是处理earth的所有渲染信息。
WW的启动过程需要访问系统的默认设置,启动默认行星的设置(一般默认为earth),WW并不直接调用本地config文件夹里earth的设置,而是先生成一个URL值访问服务器的xml文件并下载到本地,分别保存为tmp,uri,xml为扩展名的三个同名文件,根据配置信息再加载插件,和需要做为渲染对象的图层信息,并保存在World对象的_renderableObjects成员中,比如启动WW呈现在我们面前的地球图形就是根据这些信息进行渲染的,每一种卫星(Landsat, USGS, etc…)的最大level值,最大经纬度值…..
因为启动时需要访问WW服务器的xml信息,所以网速慢的朋友启动也会慢不少,我测试了下自己的电脑访问WW的服务器网速只有6k/s左右,启动时间有三分之一都耗费在这上面,其余大部分时间都花在加载配置文件上xml上,因为需要将配置文件里的所有图层都加载到WW中
配置文件的加载实现在ConfigurationLoader.cs中,其中主要是图层信息的加载,函数getRenderableFromLayerFile中分别执行不同类型图层,初始地形信息的加载,如下:
addImageLayersFromXPathNodeIterator(iter.Current.Select("ImageLayer"), parentWorld, parentRenderable);
addQuadTileLayersFromXPathNodeIterator(iter.Current.Select("QuadTileSet"), parentWorld, parentRenderable, cache);
addPathList(iter.Current.Select("PathList"), parentWorld, parentRenderable);
addPolygonFeature(iter.Current.Select("PolygonFeature"), parentWorld, parentRenderable);
addLineFeature(iter.Current.Select("LineFeature"), parentWorld, parentRenderable);
addModelFeature(iter.Current.Select("ModelFeature"), parentWorld, parentRenderable);
//addWater(iter.Current.Select("Water"), parentWorld, parentRenderable);
addTiledPlacenameSet(iter.Current.Select("TiledPlacenameSet"), parentWorld, parentRenderable);
addTiledWFSPlacenameSet(iter.Current.Select("TiledWFSPlacenameSet"), parentWorld, parentRenderable, cache);
addIcon(iter.Current.Select("Icon"), parentWorld, parentRenderable, cache);
addScreenOverlays(iter.Current.Select("ScreenOverlay"), parentWorld, parentRenderable, cache);
addChildLayerSet(iter.Current.Select("ChildLayerSet"), parentWorld, parentRenderable, cache);
addExtendedInformation(iter.Current.Select("ExtendedInformation"), parentRenderable);
最终结构会和WW的Layer层的结构类似,如下所示(WW出调出了BUG看不到layer暂时)
四、视域剔除
WW的视域剔除并没有精确到每个三角形,而是每个纹理对应的四个子树形成的包围盒BoundBox,看代码中也有包围球BoundSphere的实现,但却没有调用这部分代码,可能是做为下一个版本的扩充,具体实现可以在ViewFrustum.cs中找到,代码量很少,publicbool Intersects(BoundingBox bb) 根据返回的bool值确定是否绘制。
在代码中除了m_Device3d.RenderState.CullMode = Cull.Clockwise或=Cull.CounterClockwise形成的背面剔除(根本就没有背面的图吧)和z缓冲形成的遮挡外,没有其他的剪裁算法。
五、旋转变换
这要关系到基础的数学知识,也是WW很多算法的基础了,重要看的代码在Camera.cs,WorldCamera.cs,MathEngine.cs, Quaterniond.cs中,不但包含WW中planet的旋转变换,还有漫游,放大放小,切换视角,球面坐标和笛卡尔坐标的转换。
旋转一般都离不开四元数,WW的算法也是使用算定义的四元数类Quaternion4d,(另外Slerp实现了球面的线性插值)
下图是旋转的三个基本方向,所有的旋转都能分解成这三个方向的分量的和
Right(pitch) |
LookAt(Roll) |
Camera,WorldCamera,MomentumCamera三个类都重载了旋转函数RotationYawPitchRoll(Angle yaw,Angle pitch, Angle roll)
传入三个方面的分量,从而实现planet的旋转效果
ZoomStepped( float ticks )和Zoom(float percent)两个函数实现当前视点地形的放大缩小效果,类似于摄像机的镜头推近、远离地球的效果,会引起变量TargetDistance的变化进而影响当前图形的level的变化
六、快速查找及定位的实现
(本来想写有level的确定,突然发现还有点理解不足,时间也不多先把以前的粘贴下来)
WW的Place Finder功能中,或者网友给我们一个地理信息的URI值,如下所示:
worldwind://goto/world=Earth&lat=31.23372&lon=121.45974&alt=13473只需要对着WW的工作区窗口粘贴就能直接到目标位置,包含目标位置的经,纬度,海拔信息,将直接调用WorldWindow.cs的Goto(WorldWind.Net.WorldWindUri uri)函数,进而调用
this.drawArgs.WorldCamera.SetPosition(latitude, longitude,
this.drawArgs.WorldCamera.Heading.Degrees,
altitude,
this.drawArgs.WorldCamera.Tilt.Degrees);转向目标位置
当然,所有的定位信息都最终转向摄像机类来进行处理,所以三个摄像机类camera,worldcamera,momentumcamera都有SetPosition的各种重载。
七、摄像机类的一些其他需要注意的
三种坐标转换:当然是世界坐标,view坐标,投影坐标了:)首先看camera.cs里的声明,
protected Matrix m_ProjectionMatrix; // Projection matrix used in last render.
protected Matrix m_ViewMatrix; // View matrix used in last render.
protected Matrix m_WorldMatrix =Matrix.Identity;
View坐标和世界坐标分别在函数ComputeViewMatrix和ComputeProjectionMatrix里实现,基础而且重要的东西,需要深刻理解,ComputeAbsoluteMatrices则包含对三种坐标的处理,明显可以看到坐标系统是使用右手笛卡尔坐标系,
x |
y |
z |
投影,view变换时有个很重要的要素就是视口的宽高比,也是确定需要显示给用户的WW工作区的宽高比,全屏时为屏幕的宽高比,窗口时要重新计算。float aspectRatio = (float)viewPort.Width / viewPort.Height;
坐标转换是个很复杂的东西,当然不是一两句话就能讲清楚的,现在只是列出来一些,等感觉理解已经足够的时候再补足,_tilt是planet的倾角,是切换视角时的关键变量,正视图时其值为0,角度范围为0-85度,摘抄部分计算tilt的代码:
// Compute camera tilt from vertical at the camera position
double tilt = camera.Tilt.Radians * 2;
if (camera.Altitude > 10000)
{
double a = camera.WorldRadius; // World radius
double b = camera.WorldRadius + camera.Altitude; // Camera to center of planet
double c = camera.Distance; // Distance to target
tilt = Math.Abs(Math.Acos((a * a - c * c - b * b) / (-2 * c * b))) * 2;
if (double.IsNaN(tilt)) tilt = 0;
}