地图绘制并不是一个神秘的存在。一个地图组件,无论它被宣称有多强大,性能有多可怕,我们都不应该被它绚丽的文案迷惑得眼花缭乱,而不去尝试了解它的本质组成。
计算机地图,本质是属于计算机图形学和显示技术的结合。而这两方面都有很大的想象空间。
其一,数据可以用更多形式去组织
之前的方式是设计Geometry和Feature等等概念来描述位置和地物。现在还诞生了更多的模型概念,比如:
用不同层级规格的四边形网格,并用不同的网格编码作为唯一标志描述地理范围;
用规则六边形剖分地理范围,利用其六个边的方向性,进行战场兵棋推演或者灾害态势预测等等更多的数据组织方式。
其二,显示可在各种介质以各种技术进行
在显示介质上,我们现在是以矩形电脑显示器做显示。现在还有一些圆形显示器,或者类似于像土豆这样的不规则表面显示(想象一种逼近地球真实形状的电子地球仪产品)。
在显示技术上,例如我们上文使用的GDI+传统方式,还有如今流行的硬件加速等显示技术。
作为GIS系统的开发,不仅需需要紧跟技术潮流的发展,也更需要一种天马行空的想象,一种化人长为我有的灵气。
所以一次偶然的机会,有了一个这样的想法,便开始琢磨它的可能性--把游戏引擎搬到GIS技术的台面上来。
来看下边这个为写本章,起早贪黑做的介绍。。
第一次做MG动画,感谢不吝支持
DirectX如何绘图
DirectX是包括Unity3D等很多Windows游戏引擎的底层渲染库。在调用层面,DirectX提供了很多基本的,如点线面的绘图函数;渐变、粒子效果等特效函数。
部分特效函数
图像特效(素材图来自msdn网站)
我们这里主要关注点线面的绘图,使用DirecX绘图,主要经过以下流程:
-
初始化设备(Device),实现最终到物理显卡的连接。
-
初始化设备上下文(DeviceContext),获得重要的绘图接口。
-
初始化交换链(SwapChain),将图像"交换"到帧缓冲区。
使用DirectX的地图控件
需要知道的是,DirectX的原生语言是C,不是C#。所以我们需要开发的新地图控件DirectxMapControl,采用了一种对DirectX的C#包装组件SharpDX。
直接引用它的dll库,不需要配置就能获得系统DirectX组件的引用。
DirectxMapControl这个用户控件和上篇的MapControl写法母胎一致,但这里对DirectX的管理:初始化、释放是必要的。
1//对重名的对象的重新using,避免混淆
2using D2D1Device = SharpDX.Direct2D1.Device;
3using D3D11Device = SharpDX.Direct3D11.Device;
4using DeviceContext = SharpDX.Direct2D1.DeviceContext;
5using DXGIDevice = SharpDX.DXGI.Device;
6using DXGIFactory = SharpDX.DXGI.Factory;
7
8...
9
10public partial class DirectxMapControl : UserControl
11{
12 private Map _map;
13
14 public DirectxMapControl()
15 : base()
16 {
17 InitializeComponent();
18 InitDeviceContext();
19 _map = new Map();
20 }
21
22 Object lockObj = new Object();
23
24 // 设备上下文
25 private DeviceContext deviceContext;
26
27 // DXGI SwapChain。
28 private SwapChain swapChain;
29
30 // SwapChain 缓冲区。
31 private Surface backBuffer;
32
33 // 渲染的目标位图。
34 private Bitmap1 targetBitmap;
35
36 private void InitDeviceContext()
37 {
38 // 创建 Dierect3D 设备。
39 D3D11Device d3DDevice = new D3D11Device(
40 DriverType.Hardware, DeviceCreationFlags.BgraSupport);
41
42 DXGIDevice dxgiDevice = d3DDevice.QueryInterface<D3D11Device>()
43 .QueryInterface<DXGIDevice>();
44
45 // 创建 Direct2D 设备和工厂。
46 D2D1Device d2DDevice = new D2D1Device(dxgiDevice);
47 this.deviceContext = new DeviceContext(d2DDevice,
48 DeviceContextOptions.None);
49 // 创建 DXGI SwapChain。
50 SwapChainDescription swapChainDesc = new SwapChainDescription()
51 {
52 BufferCount = 1,
53 Usage = Usage.RenderTargetOutput,
54 //图像输出的窗口句柄->本控件
55 OutputHandle = this.Handle,
56 IsWindowed = true,
57 // 这里宽度和高度都是 0,表示自动获取。
58 ModeDescription = new ModeDescription(0, 0, new Rational(60, 1), Format.B8G8R8A8_UNorm),
59 SampleDescription = new SampleDescription(1, 0),
60 SwapEffect = SwapEffect.Discard
61 };
62
63 this.swapChain = new SwapChain(
64 dxgiDevice.GetParent<Adapter>().GetParent<DXGIFactory>(),
65 d3DDevice, swapChainDesc);
66 // 创建 BackBuffer。
67 this.backBuffer = Surface.FromSwapChain(this.swapChain, 0);
68 // 从 BackBuffer 创建 DeviceContext 可用的目标。
69 this.targetBitmap = new Bitmap1(this.deviceContext, backBuffer);
70 this.deviceContext.Target = targetBitmap;
71 }
72}
73
74//释放对象,重绘前执行
75private void DisposeContext()
76{
77 lock (lockObj)
78 {
79 this.deviceContext.Target = null;
80 this.backBuffer.Dispose();
81 this.targetBitmap.Dispose();
82 this.swapChain.ResizeBuffers(1, Width, Height, Format.B8G8R8A8_UNorm, SwapChainFlags.None);
83 this.backBuffer = Surface.FromSwapChain(this.swapChain, 0);
84 this.targetBitmap = new Bitmap1(this.deviceContext, backBuffer);
85 this.deviceContext.Target = targetBitmap;
86 }
87}
适配Graphics
你可能还记得此前基于GDI+绘图的Layer设计,绘制点线面的函数是由System.Drawing.Graphics提供的。这无法适配我们今天的DirectxMapControl,或者以后可能出现的种种MapControl。
所以需要对绘图函数做一个抽象。下面是抽象出的绘图接口:
1public interface IShapeGraphics
2{
3 void DrawPoint(System.Drawing.Point point);
4 void DrawLine(System.Drawing.Point[] points);
5 void DrawPolygon(System.Drawing.Point[] points);
6}
这样,原来的Layer的绘图函数需要变化:
1public void DrawPoint(Graphics g, Map map);
Graphics参数由对象形式变为接口。
1public void DrawPoint(IShapeGraphics g, Map map);
有了这个接口,Layer的绘图方式就可以脱离具体绘图平台了。现在对标这个接口,用DirectX的方式,编写实现类DirectxGraphics,实现IShapeGraphics。
1public class DirectxGraphics : IShapeGraphics
2{
3 DeviceContext deviceContext;
4 SwapChain swapChain;
5
6 public DirectxGraphics(DeviceContext deviceContext, SwapChain swapChain)
7 {
8 this.deviceContext = deviceContext;
9 this.swapChain = swapChain;
10 }
11
12 //色值转换
13 Color4 ColorToRaw4(SharpDX.Color color)
14 {
15 const float n = 255f;
16 return new Color4(color.R / n, color.G / n,
17 color.B / n, color.A / n);
18 }
19
20 //渐变色顶点描述
21 GradientStopCollection gradientStopCollection = null;
22
23 GradientStopCollection GradientStopCollection
24 {
25 get
26 {
27 if (gradientStopCollection != null)
28 return gradientStopCollection;
29
30 var gradientStop0 = new GradientStop()
31 {
32 Color = this.ColorToRaw4(SharpDX.Color.Yellow),
33 Position = 0f
34 };
35 var gradientStop1 = new GradientStop()
36 {
37 Color = this.ColorToRaw4(SharpDX.Color.ForestGreen),
38 Position = 0.5f
39 };
40 var gradientStop2 = new GradientStop()
41 {
42 Color = this.ColorToRaw4(SharpDX.Color.White),
43 Position = 1f
44 };
45 var gradientStops = new GradientStop[]
46 {
47 gradientStop0,
48 gradientStop1,
49 gradientStop2,
50 };
51
52 gradientStopCollection = new GradientStopCollection(deviceContext, gradientStops);
53 return gradientStopCollection;
54 }
55 }
56
57 //实现画点
58 public void DrawPoint(System.Drawing.Point point)
59 {
60 deviceContext.BeginDraw();
61
62 Brush brush = new SolidColorBrush(this.deviceContext,
63 SharpDX.Color.Orange);
64
65 Vector2 vector = new Vector2(point.X, point.Y);
66 Ellipse p = new Ellipse(vector, 2, 2);
67 deviceContext.FillEllipse(p, brush);
68
69 deviceContext.EndDraw();
70 }
71
72 //实现画线
73 public void DrawLine(System.Drawing.Point[] points)
74 {
75 Brush brush = new SolidColorBrush(this.deviceContext,
76 SharpDX.Color.Blue);
77
78 deviceContext.BeginDraw();
79
80 for (int i = 0; i < points.Length - 1; i++)
81 {
82 Vector2 p1 = new Vector2(points[i].X, points[i].Y);
83 Vector2 p2 = new Vector2(points[i + 1].X, points[i + 1].Y);
84 deviceContext.DrawLine(p1, p2, brush, 2);
85 }
86
87 deviceContext.EndDraw();
88 }
89
90 //实现画面
91 public void DrawPolygon(System.Drawing.Point[] points)
92 {
93 //开始绘图
94 deviceContext.BeginDraw();
95
96 SharpDX.Direct2D1.Factory factory = deviceContext.Factory;
97
98 PathGeometry pg = new PathGeometry(factory);
99 GeometrySink gs = pg.Open();
100 {
101 //开始记录起点
102 gs.BeginFigure(
103 new Vector2(points[0].X, points[0].Y), FigureBegin.Filled);
104
105 for (int i = 1; i < points.Length; i++)
106 {
107 //添加节点
108 gs.AddLine(
109 new Vector2((float)points[i].X, (float)points[i].Y));
110 }
111 //形成闭环
112 gs.EndFigure(FigureEnd.Closed);
113 }
114
115 gs.Close();
116 pg.Outline(gs);
117
118 //图形外框
119 //用于描述渐变效果范围
120 RectangleF bounds = pg.GetBounds();
121
122 gs.Dispose();
123
124 var gbp = new LinearGradientBrushProperties()
125 {
126 StartPoint = new Vector2(bounds.Left, bounds.Top),
127 EndPoint = new Vector2(bounds.Right, bounds.Bottom)
128 };
129
130 //渐变效果笔刷
131 var lnBrush = new LinearGradientBrush(deviceContext,
132 gbp,
133 GradientStopCollection);
134
135 var style = new StrokeStyleProperties();
136 style.LineJoin = LineJoin.Round;
137 style.StartCap = CapStyle.Round;
138 style.EndCap = CapStyle.Round;
139 var stroke = new StrokeStyle(factory, style);
140
141 //图形边缘笔刷
142 Brush edgeBrush = new SolidColorBrush(this.deviceContext, SharpDX.Color.Black);
143
144 deviceContext.DrawGeometry(pg, edgeBrush, 2, stroke);
145 deviceContext.FillGeometry(pg, lnBrush);
146
147 //结束绘图
148 deviceContext.EndDraw();
149 pg.Dispose();
150 }
151}
有了这个新的Graphics,也就代表有了绘图表面,这样地图控件就可以将此对象传递给Layer进行绘图。
传递路径是DirectxMapControl-->Map-->Layer,所以需要对下层的对象做一定的改动。
首先,控件自身传递Graphics的方式预先定义出来:
1public partial class DirectxMapControl : UserControl
2{
3 public IShapeGraphics CreateShapeGraphics()
4 {
5 return new DirectxGraphics(this.deviceContext, swapChain);
6 }
7 ...
8}
Map对象的渲染方法改为接收IShapeGraphics参数:
1 public void Render(IShapeGraphics g);
Layer对象的绘制方法,变化不大:
1//例:Layer类点绘制
2public void DrawPoint(IShapeGraphics g, Map map)
3{
4 foreach (var f in features)
5 {
6 if (!map.IsRenderingEnabled)
7 throw new RenderingAbortedException("Point Drawing Aborted");
8
9 Vertex p = f.GetGeometry().Centroid;
10 System.Drawing.Point point = map.ToScreenPoint(p);
11 g.DrawPoint(point);
12 }
13}
DirectxMapControl渲染图层
为了节省篇幅,上篇MapControl的其余部分就不列出了,依然采用每次重绘启动一个线程的方式。这里只对渲染方法作出改动:
1public partial class DirectxMapControl : UserControl
2{
3 Object lockObj = new Object();
4
5 ...
6
7 private void RenderMapInternal(Extent extent)
8 {
9 try
10 {
11 //停止已有线程的绘图任务
12 if (_map.IsRendering)
13 _map.AbortRendering();
14
15 lock (lockObj)
16 {
17 _map.UpdateExtent(extent,
18 new Rectangle(0, 0, Width, Height));
19
20 //释放对象
21 DisposeContext();
22
23 //绘制白色背景
24 deviceContext.BeginDraw();
25 deviceContext.Clear(SharpDX.Color.White);
26 deviceContext.EndDraw();
27
28 IShapeGraphics g = this.CreateShapeGraphics();
29 //将地图绘制到空白图片上
30 _map.Render(g);
31
32 //帧缓冲区图像交换
33 swapChain.Present(0, PresentFlags.None);
34 }
35 }
36
37 catch (Exception e)
38 {
39 DisposeContext();
40 }
41 }
42 ...
43}
这里说一下显卡绘图的最后流程:
显卡每一帧图像的绘制,都是在两种缓存的切换中实现的。其中一种叫后置缓存(back buffer),另一种叫前置缓存(front buffer)。
显示器正在显示的图像处于前置缓冲之中,同时CPU读取到的图像会被置于后置缓存中。
当前置缓存中的图像显示完后,两个缓存的关系翻转,前置缓存变成后置缓存,后置缓存变成前置缓存。
这个交换的过程称为呈现(presenting)。图像的刷新就是由此产生。
所以,这里的swapChain.Present一定是必要的。否则前面所有的绘制过程都不会生效在屏幕上。
调用呈现
Directx地图控件已经准备好了,现在把它拖入到Form里,替换之前的MapControl。同时shp打开按钮的调用,相应的变成了这样:
1private void btn_OpenShp_Click(object sender, EventArgs e)
2{
3 OpenFileDialog ofd = new OpenFileDialog();
4 ofd.Filter = "(*.shp)|*.shp";
5 if (ofd.ShowDialog() == DialogResult.OK)
6 {
7 ShapeFile shp = new ShapeFile();
8 Layer layer = shp.ReadShapeFile(ofd.FileName);
9 directxMapControl.Map.Layers.Add(layer);
10 directxMapControl.RenderMap();
11 }
12}
底层的变化波涛汹涌,上层的调用波澜不惊~
最后来看看加载起来的渐变特效吧:
把线也加进去:
这一切还是跑得通的,并且画风明显炫酷!
Ok本章到站,支持的朋友请点赞下车