学习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, null, 1);方法的基础。
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坐标系下的,N、W(这里就是上面求行列数的逆运算啦)
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);
}
(未完待续……)