I was only going to write 3 Bing Maps 3D posts (they start Here), but today I added something that I thought was rather cool.
You can download the source code for this post here 3DViewerBingMaps.zip
In OpenGL you can draw something called a Stippled Line which is basically a dashed line with some extra control over the dash pattern, however in Bing Maps 3D there is no such thing, which is why I wrote my own dashed line mesh class. My class supports the following things:
- Lines are made up of multiple segments
- Dashes and gaps between dashes are independent in size
- You can set an offset each frame allowing for a moving ant effect
- You can set different widths for the start and the end of the line
Here’s 2 pictures of the same line showing the billboard effect of the wide dashes, and the constant dash and gap size:


If you download the sample at the top of this post you can run the application shown in the screenshots.
Here’s the code snippet for setting up the above Dashed Mesh:
DashedLineMesh dashedLine = new DashedLineMesh();
dashedLine.DashLength = 8;
dashedLine.GapLength = 16;
dashedLine.LineStartWidth = 1;
dashedLine.LineEndWidth = 64;
dashedLine.Color = Color.White;
dashedLine.ZBufferEnable = true;
dashedLine.Points.Clear();
GeodeticViewpoint centrePoint = new GeodeticViewpoint();
double alt = 1000000;
double lat = 45.0;
double lon = 180.0;
for(double i = 0; i<(Math.PI*2); i+=0.103)
{
double x = 8+30 * Math.Sin(i);
double y = 8+30 * Math.Cos(i);
centrePoint.Position.Location = LatLonAlt.CreateUsingDegrees(lat+x, lon+y, alt);
dashedLine.Points.Add(new Vector3F(centrePoint.Position.Vector));
}
and as a bonus here’s the complete source code for the DashedLineMesh class
// Dashed Line Mesh by John Stewien 2010
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.MapPoint.Rendering3D.GraphicsProxy;
using Microsoft.MapPoint.Geometry.VectorMath;
using Microsoft.MapPoint.Geometry;
using Microsoft.MapPoint.Graphics3D;
using Microsoft.MapPoint.Rendering3D.State;
namespace _3DViewerBingMaps.Plugin {
/// <summary>
/// Draws a multi-segment line that can be broken up into dashes
/// </summary>
public class DashedLineMesh : MeshGraphicsObject<Vertex.PositionOnly, ushort> {
// ************************************************************************
// Private Fields
// ************************************************************************
#region Private Fields
/// <summary>
/// A list of points that make up the line to be drawn. Points are in
/// Lat, Lon, Alt format.
/// </summary>
private List<Vector3F> points;
/// <summary>
/// The starting width of the line in pixels
/// </summary>
private float lineStartWidth = 1f;
/// <summary>
/// The ending width of the line in pixels
/// </summary>
private float lineEndWidth = 1f;
/// <summary>
/// The length of the dashes in pixels
/// </summary>
private float dashLength = 0f;
/// <summary>
/// the length of the gaps between the dashes in pixels
/// </summary>
private float gapLength = 0f;
/// <summary>
/// The offset of the dahses in pixels. Incrementing this by .5 every frame
/// gives a moving and effect.
/// </summary>
private float dashOffset = 0f;
/// <summary>
/// Enable alpha blending for this mesh
/// </summary>
private bool alphaEnable = false;
/// <summary>
/// Enable using the Depth Buffer for this mesh
/// </summary>
private bool zBufferEnable = true;
/// <summary>
/// The color to draw this line in
/// </summary>
private System.Drawing.Color color = System.Drawing.Color.White;
#endregion Private Fields
// ************************************************************************
// Public Methods
// ************************************************************************
#region Public Methods
/// <summary>
/// Constructor
/// </summary>
/// <param name="radius"></param>
/// <param name="material"></param>
/// <param name="materialId"></param>
public DashedLineMesh()
: base(GraphicsBufferUsage.Dynamic, GraphicsBufferUsage.Dynamic, 4, 4, true) {
this.points = new List<Vector3F>();
if (base.State.Materials.Count == 0) {
base.State.Materials.Add(new Material());
}
base.ProjectionEnabled = false;
this.UpdateRenderState();
}
/// <summary>
/// Updates the graphic prior to rendering it in a frame. Needs to be
/// called every frame.
/// </summary>
/// <param name="camera"></param>
/// <param name="transform"></param>
/// <remarks>
/// Unfortunately the GraphicsObject.UpdateInternalRenderObject method
/// can't be overriden because of its "internal" protection, so this
/// needs to be called every frame before the graphics is rendered
/// </remarks>
public void PreRenderUpdate(CameraData camera, Matrix4x4D transform) {
int width = camera.View.Size.Width;
int height = camera.View.Size.Height;
this.UpdateLineGeometry(width, height, transform*camera.Snapshot.TransformMatrix);
}
#endregion Public Methods
// ************************************************************************
// Private Methods
// ************************************************************************
#region Private Methods
/// <summary>
/// Calculates the required line width from the user specified value and the
/// size of the display area
/// </summary>
/// <param name="deviceWidth"></param>
/// <param name="deviceHeight"></param>
/// <returns></returns>
private Vector3F GetWidth(float lineWidth, int deviceWidth, int deviceHeight) {
float x = lineWidth / ((float)deviceWidth);
float y = lineWidth / ((float)deviceHeight);
Vector3F retVal = new Vector3F(x, y, 1f);
return retVal;
}
/// <summary>
/// Apply the position/scale tranform, and the projection transform to the points
/// to convert them to screen coordinates
/// </summary>
/// <param name="projectionTransform"></param>
/// <returns></returns>
private IList<Vector3F> TransformedPoints(Matrix4x4D projectionTransform) {
List<Vector3F> list = new List<Vector3F>(this.points.Count);
Matrix4x4D matrix = projectionTransform;
foreach (Vector3F vectorf in this.points) {
Vector4D vector = new Vector4D((double)vectorf.X, (double)vectorf.Y, (double)vectorf.Z, 1.0);
vector = Vector4D.TransformCoordinate(ref vector, ref matrix);
if (vector.W > 0.0) {
vector.MultiplyBy(1.0 / vector.W);
} else {
vector.MultiplyBy(1.0 / -vector.W);
}
list.Add(new Vector3F((float)vector.X, (float)vector.Y, (float)vector.Z));
}
return list;
}
/// <summary>
/// This method takes all the solid lines and breaks them up into dashes
/// </summary>
/// <param name="lineList"></param>
/// <param name="widthList"></param>
/// <param name="deviceWidth"></param>
/// <param name="deviceHeight"></param>
private bool ConvertToDashed(ref IList<Vector3F> lineList, ref IList<float> widthList, int deviceWidth, int deviceHeight) {
this.dashOffset = dashOffset % (dashLength + gapLength);
// Scale everything to viewport space
float dashStep = 2 * this.dashLength / Math.Min(deviceWidth, deviceHeight);
float gapStep = 2 * this.gapLength / Math.Min(deviceWidth, deviceHeight);
float offsetStep = 2 * this.dashOffset / Math.Min(deviceWidth, deviceHeight);
// Sanity check dashStep and gapStep
if (dashStep <= 0 || gapStep <= 0) {
return false;
}
// Calculate the initial conditions
float remainder = offsetStep;
bool doDash = remainder==0;
if (remainder > gapStep) {
remainder -= gapStep;
doDash = true;
}
bool doingJoin = false;
List<Vector3F> dashedLineList = new List<Vector3F>();
List<float> dashedWidthList = new List<float>();
for (int i = 0; i < (lineList.Count - 1); i++) {
Vector3F fromPoint = lineList[i];
Vector3F toPoint = lineList[i + 1];
// Store the original line length. Used for calculating the line width later on.
float lineLength = Vector3F.Distance(fromPoint, toPoint);
// Clamp the line to the visible area otherwise a lot more calculation will be done
// than is required
if (ClampLineToBox(ref fromPoint, ref toPoint, new Vector2F(-1, -1), new Vector2F(1, 1))) {
// Need to break up the line in the X-Y direction regardless of Z
Vector2F segment = new Vector2F(toPoint.X - fromPoint.X, toPoint.Y - fromPoint.Y);
float segLength = segment.Length();
if (segLength > 0) {
float widthDiff = widthList[i + 1] - widthList[i];
Vector3F step = new Vector3F(segment.X, segment.Y, toPoint.Z - fromPoint.Z) / segLength;
float distTravelled = 0;
float nextDash = remainder > 0 ? remainder : dashStep;
float nextgap = remainder > 0 ? remainder : gapStep;
// Calcluate the line width for the point we are starting at and
// Calculate the line width increments for when we iterate over the dashes
float invLineLength = 1f / lineLength;
float progressiveWidth = widthList[i] + widthDiff * (remainder + Vector3F.Distance(fromPoint, lineList[i])) * invLineLength;
float dashDWidth = widthDiff * dashStep * invLineLength;
float gapDWidth = widthDiff * gapStep * invLineLength;
// Keep on adding dashes until we reach the end of the line segment
// Also calculate the line widths for each end of the dashes
while (distTravelled < segLength) {
if (doDash) {
if (doingJoin) {
dashedLineList.RemoveAt(dashedLineList.Count - 1);
dashedWidthList.RemoveAt(dashedWidthList.Count - 1);
} else {
dashedLineList.Add(distTravelled * step + fromPoint);
dashedWidthList.Add(progressiveWidth);
}
doingJoin = false;
distTravelled += nextDash;
progressiveWidth += dashDWidth;
dashedLineList.Add(Math.Min(segLength, distTravelled) * step + fromPoint);
dashedWidthList.Add(progressiveWidth);
if (distTravelled > segLength && i < (lineList.Count - 2)) {
doingJoin = true;
}
} else {
distTravelled += nextgap;
progressiveWidth += gapDWidth;
}
doDash = !doDash;
nextDash = dashStep;
nextgap = gapStep;
}
remainder = distTravelled - segLength;
if (remainder > 0) {
doDash = !doDash;
}
}
} else {
doingJoin = false;
}
}
lineList = dashedLineList;
widthList = dashedWidthList;
return true;
}
/// <summary>
/// Clamps the line between the 2 vectors passed to be within the box limits passed in.
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="min"></param>
/// <param name="max"></param>
/// <returns>
/// true if the line is visible, false otherwise
/// </returns>
private bool ClampLineToBox(ref Vector3F from, ref Vector3F to, Vector2F boxMin, Vector2F boxMax) {
// need to limit everything to within -1 to 1 on all dimensions
// Test if anything is inside the viewbox
if (Math.Min(from.X, to.X) > boxMax.X || Math.Max(from.X, to.X) < boxMin.X ||
Math.Min(from.Y, to.Y) > boxMax.Y || Math.Max(from.Y, to.Y) < boxMin.Y) {
return false;
}
// Test if the line is completely contained in the viewbox
if (Math.Min(from.X, to.X) > boxMin.X && Math.Max(from.X, to.X) < boxMax.X &&
Math.Min(from.Y, to.Y) > boxMin.Y && Math.Max(from.Y, to.Y) < boxMax.Y) {
return true;
}
ClampDestToBox(ref from, ref to, boxMin, boxMax);
ClampDestToBox(ref to, ref from, boxMin, boxMax);
return true;
}
/// <summary>
/// Clamps the destination point to the box limits passed in
/// </summary>
/// <param name="from"></param>
/// <param name="dest"></param>
/// <param name="boxMin"></param>
/// <param name="boxMax"></param>
private void ClampDestToBox(ref Vector3F from, ref Vector3F dest, Vector2F boxMin, Vector2F boxMax) {
Vector3F diff = dest - from;
if (dest.X > boxMax.X) {
dest = (boxMax.X - from.X) / diff.X * diff + from;
} else if (dest.X < boxMin.X) {
dest = (boxMin.X - from.X) / diff.X * diff + from;
}
if (dest.Y > boxMax.Y) {
dest = (boxMax.Y - from.Y) / diff.Y * diff + from;
} else if (dest.Y < boxMin.Y) {
dest = (boxMin.Y - from.Y) / diff.Y * diff + from;
}
}
/// <summary>
/// Updates the vertex and index buffer for rendering the line for the current viewpoint
/// </summary>
/// <param name="deviceWidth"></param>
/// <param name="deviceHeight"></param>
/// <param name="projectionTransform"></param>
private void UpdateLineGeometry(int deviceWidth, int deviceHeight, Matrix4x4D projectionTransform) {
this.Indices.Clear();
this.Vertices.Clear();
// Get all the points in screen coordinates
IList<Vector3F> list = this.TransformedPoints(projectionTransform);
IList<float> widths = this.GetWidths(list);
bool isDashed = this.ConvertToDashed(ref list, ref widths, deviceWidth, deviceHeight);
for (int i = 0; i < (list.Count - 1); i++) {
Vector3F width1 = this.GetWidth(widths[i], deviceWidth, deviceHeight);
Vector3F width2 = this.GetWidth(widths[i+1], deviceWidth, deviceHeight);
Vector3F difference = list[i + 1] - list[i];
Vector2F direction = Vector2F.Normalize(new Vector2F(difference.X, difference.Y));
Vector3F perpendicular1 = new Vector3F(-direction.Y * width1.X, direction.X * width1.Y, 0f);
Vector3F perpendicular2 = new Vector3F(-direction.Y * width2.X, direction.X * width2.Y, 0f);
this.Vertices.AddData(new Vertex.PositionOnly(list[i] + perpendicular1));
this.Vertices.AddData(new Vertex.PositionOnly(list[i] - perpendicular1));
this.Vertices.AddData(new Vertex.PositionOnly(list[i+1] + perpendicular2));
this.Vertices.AddData(new Vertex.PositionOnly(list[i+1] - perpendicular2));
int num2 = i*2;
ushort[] data = new ushort[] { (ushort)num2, (ushort)(num2 + 2), (ushort)(num2 + 3), (ushort)num2, (ushort)(num2 + 3), (ushort)(num2 + 1) };
this.Indices.AddDataUnsafe(data, PrimitiveType.TriangleList, 0);
if (isDashed)
i++;
}
}
/// <summary>
/// Creates a list of widths for the points passed in.
/// </summary>
/// <param name="list"></param>
/// <returns></returns>
private IList<float> GetWidths(IList<Vector3F> list) {
float[] progressiveLength = new float[list.Count];
float totalLength = 0;
for (int i = 0; i < (list.Count-1); ++i) {
progressiveLength[i] = totalLength;
totalLength += Vector3F.Distance(list[i], list[i + 1]);
}
progressiveLength[list.Count - 1] = totalLength;
if (totalLength > 0) {
float widthDiff = lineEndWidth - lineStartWidth;
float invTotalLength = 1f/totalLength;
for (int i = 0; i < progressiveLength.Length; ++i) {
progressiveLength[i] = lineStartWidth + widthDiff * progressiveLength[i] * invTotalLength;
}
}
return progressiveLength;
}
/// <summary>
/// Updates the colors, depth buffer, and alpha settings for rendering the mesh
/// </summary>
private void UpdateRenderState() {
RenderState renderState = base.State.RenderState;
if (this.alphaEnable) {
renderState.Alpha.Enabled = true;
renderState.Alpha.SourceBlend = Blend.SourceAlpha;
renderState.Alpha.DestinationBlend = Blend.InvSourceAlpha;
} else {
renderState.Alpha.Enabled = false;
}
renderState.Lighting.Enabled = false;
renderState.Cull.Enabled = false;
renderState.Stages[0].Blending.AlphaArgument1 = TextureArgument.TFactor;
renderState.Stages[0].Blending.AlphaOperation = TextureOperation.SelectArg1;
renderState.Stages[0].Blending.ColorArgument1 = TextureArgument.TFactor;
renderState.Stages[0].Blending.ColorOperation = TextureOperation.SelectArg1;
renderState.Stages[1].Enabled = false;
renderState.TextureFactor.Value = this.color;
renderState.ZBuffer.Enabled = zBufferEnable;
renderState.ZBuffer.ZBufferFunction = Compare.Less;
}
#endregion Private Methods
// ************************************************************************
// Properties
// ************************************************************************
#region Properties
/// <summary>
/// A list of points that make up the line to be drawn. Points are in
/// Lat, Lon, Alt format.
/// </summary>
public List<Vector3F> Points {
get {
return points;
}
}
/// <summary>
/// Gets and Sets the starting width of the line in pixels
/// </summary>
public float LineStartWidth {
get {
return this.lineStartWidth;
}
set {
if (value <= 0f) {
throw new ArgumentOutOfRangeException("value");
}
this.lineStartWidth = value;
}
}
/// <summary>
/// Gets and Sets the end width of the line in pixels
/// </summary>
public float LineEndWidth {
get {
return this.lineEndWidth;
}
set {
if (value <= 0f) {
throw new ArgumentOutOfRangeException("value");
}
this.lineEndWidth = value;
}
}
/// <summary>
/// Gets and Sets the length of the dashes in pixels
/// </summary>
public float DashLength {
get {
return dashLength;
}
set {
if (value < 0f) {
throw new ArgumentOutOfRangeException("value");
}
dashLength = value;
}
}
/// <summary>
/// Gets and Sets the length of the gaps between the dashes in pixels
/// </summary>
public float GapLength {
get {
return gapLength;
}
set {
if (value < 0f) {
throw new ArgumentOutOfRangeException("value");
}
gapLength = value;
}
}
/// <summary>
/// Gets and Sets the offset of the dashes in pixels.
/// </summary>
/// <remarks>
/// Incrementing this by .5 every frame gives a moving ant effect.
/// </remarks>
public float DashOffset {
get {
return dashOffset;
}
set {
if (value < 0f) {
throw new ArgumentOutOfRangeException("value");
}
dashOffset = value;
}
}
/// <summary>
/// Gets and Sets if alpha blending is enabled for this line
/// </summary>
public bool AlphaEnable {
get {
return this.alphaEnable;
}
set {
if (this.alphaEnable != value) {
this.alphaEnable = value;
this.UpdateRenderState();
}
}
}
/// <summary>
/// Gets and Sets if the Depth Buffer is enabled for this mesh
/// </summary>
public bool ZBufferEnable {
get {
return this.zBufferEnable;
}
set {
if (this.zBufferEnable != value) {
this.zBufferEnable = value;
this.UpdateRenderState();
}
}
}
/// <summary>
/// Gets and Sets the color to draw this line in
/// </summary>
public System.Drawing.Color Color {
get {
return this.color;
}
set {
if (this.color != value) {
this.color = value;
this.UpdateRenderState();
}
}
}
#endregion Properties
}
}