Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

WiiEarthVR – A Fully Immersive 3D Experience with Virtual Earth 3D

globe4 In this article, Brian Peek will demonstrate how to use a Nintendo Wii Remote (Wiimote), a Wii Fit Balance Board, and Vuzix VR920 glasses as input devices for Microsoft Virtual Earth 3D, providing a fully immersive, 3D experience.
ASPSOFT, Inc.

Difficulty: Intermediate
Time Required: 2-3 hours
Cost: $60 for Wiimote and Nunchuk, $90 for Wii Fit (which includes Balance Board), $400 for Vuzix VR920 glasses
Software: Managed Library for Nintendo's Wiimote, Visual Basic or Visual C# Express Editions
Hardware: Nintendo Wii Remote (Wiimote) with Nunchuk, Wii Fit Balance Board, Vuzix VR920 glasses, a compatible PC Bluetooth adapter and stack
Download: Download
Discussion Forum: Forum

 

Introduction

Virtual Earth is the 3D interface to Microsoft's Live Maps service.  Normally this control is loaded via the web browser and allows interaction with a keyboard, mouse, and Xbox 360 controller.  In this article, we will take the Virtual Earth 3D control out of the web browser, use it in a WinForms application, and control it with a Nintendo Wii Remote (Wiimote) and a pair of Vuzix VR920 glasses, while also providing a stereoscopic 3D image to the glasses, creating the illusion of a fully three dimensional environment.  Note that use of the Virtual Earth 3D control in this way is undocumented and unsupported at the moment.  Because of this, some of the descriptions in this article are educated guesses and may not be 100% accurate…

Originally, this project started as a simple Wiimote interface to Virtual Earth 3D as shown in the video below.  Since I wrote that application, I learned of the VR920 glasses and the Wii Fit Balance Board was released, so I've decided to create a more immersive experience using all of these controls which was demonstrated at PDC2008, shown here:

Get Microsoft Silverlight

Setup

Before we get started, you will need to install the Virtual Earth 3D control.  If you haven't done this already, browse to http://maps.live.com/ and click on the 3D link to install the control and supporting software.

captured_Image.png

Additionally, if you haven't already, please review my Managed Library for Nintendo's Wiimote article on this site.  We will be using the library in this article, but I will not repeat the basic information that is located in the original article.  You will also need to have the Vuzix VR920 glasses installed and setup according to its own user manual.  That will also not be covered here.

Implementation

The Virtual Earth 3D Control

The Virtual Earth 3D (VE3D) control is intended to be used through a well documented JavaScript interface from a web page, however we would not be able to access the Wiimote or VR920 glasses from JavaScript.  Therefore, we will be using the VE3D control through its native, but wholly undocumented interface.

Start by creating a new Windows Forms application named WiiEarthVR in C# or VB.  As with all controls and 3rd party libraries, a reference needs to be set to the Virtual Earth 3D libraries.  Add references to the following items: 

  • Microsoft.MapPoint.Data
  • Microsoft.MapPoint.Data.VirtualEarthTileDataSource
  • Microsoft.MapPoint.Geometry
  • Microsoft.MapPoint.Rendering3D
  • Microsoft.MapPoint.Rendering3D.Utility

If they do not show up in the .NET references, they can be found by selecting the Browse tab and navigating to C:\Program Files\Virtual Earth 3D\ or C:\Program Files (x86)\Virtual Earth 3D\ .  With the references in place, the project file can now be opened and the references will be seen in the References folder in the Solution Explorer as usual.

captured_Image.png[11]

Creating an instance of the control can be done in code just like any other control.  Used in the constructor or load event of the form, the following code will create a VE3D control and add it to the form as fully docked:

C#

private GlobeControl _globeControl;
 
public MainForm()
{
    InitializeComponent();
 
    _globeControl = new GlobeControl();
    SuspendLayout();
    _globeControl.Location = new System.Drawing.Point(0, 0);
    _globeControl.Name = "_globeControl";
    _globeControl.Size = ClientSize;
    _globeControl.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom;
    _globeControl.TabIndex = 0;
    _globeControl.SendToBack(); // we want the button to be on top
 
    pnlGlobe.Controls.Add(_globeControl);
    ResumeLayout(false);
}

VB

Private _globeControl As GlobeControl
 
Public Sub New()
    InitializeComponent()
 
    _globeControl = New GlobeControl()
    SuspendLayout()
    _globeControl.Location = New System.Drawing.Point(0, 0)
    _globeControl.Name = "_globeControl"
    _globeControl.Size = ClientSize
    _globeControl.Anchor = AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.Top Or AnchorStyles.Bottom
    _globeControl.TabIndex = 0
    _globeControl.SendToBack() ' we want the button to be on top
 
    pnlGlobe.Controls.Add(_globeControl)
    ResumeLayout(False)
End Sub

This sets up the VE3D control in its default state.  If you were to run an application with only this code, you would see nothing but the earth.  The navigation controls and other extras would be missing.

We can start adding items to the VE3D control by listening for the FirstFrameRendered event of the GlobeControl and then setting the appropriate properties.  Setting these properties prior to this point can lead to some unexpected results.

In the FirstFrameRendered event handler, if you wish to add the default navigation controls to the screen, the PlugInLoader object is used.  The PlugInLoader is created by using the CreateLoader static method, passing in an instance of the GlobeControl's Host object.  Then, the NavigationPlugIn can be loaded and activated as shown:

C#

// load all the spiffy UI navigation goodies
PlugInLoader loader = PlugInLoader.CreateLoader(this.globeControl.Host);
loader.LoadPlugIn(typeof(NavigationPlugIn));
loader.ActivatePlugIn(typeof(NavigationPlugIn).GUID, null);

VB

' load all the spiffy UI navigation goodies
Dim loader As PlugInLoader = PlugInLoader.CreateLoader(Me.globeControl.Host)
loader.LoadPlugIn(GetType(NavigationPlugIn))
loader.ActivatePlugIn(GetType(NavigationPlugIn).GUID, Nothing)

The last thing to be added for basic functionality is the data.  As it stands, the only data that will appear on the globe is the image of the continents.  Zooming in only produces a blurry representation of that base image.

Data layers are created from specially formatted data sources provided by maps.live.com known as content manifests.  These are XML files which tell the VE3D control how to load the data required for any view.  Content layers can be added by adding them to the DataSources object of the GlobeControl.  We can add any of the following layers (note that there may be other content manifests provided by maps.live.com, but these are the only 5 that I am aware of):

URL

DataSourceUsage Type

Description

http://local.live.com/Manifests/HD.xml ElevationMap Terrain data
http://local.live.com/Manifests/MO.xml Model 3D buildings
http://local.live.com/Manifests/AT.xml TextureMap Unlabeled aerial
http://local.live.com/Manifests/HT.xml TextureMap Labeled aerial
http://local.live.com/Manifests/RT.xml TextureMap Roads only

For the best display, add the ElevationMap, Model and Aerial TextureMap layers as shown:

C#

// set various data sources, here for elevation data, terrain data, and model data.
_globeControl.Host.DataSources.Add(new DataSourceLayerData("Elevation", "Elevation", @"http://maps.live.com//Manifests/HD.xml", DataSourceUsage.ElevationMap));
_globeControl.Host.DataSources.Add(new DataSourceLayerData("Texture", "Texture", @"http://maps.live.com//Manifests/AT.xml", DataSourceUsage.TextureMap));
_globeControl.Host.DataSources.Add(new DataSourceLayerData("Models", "Models", @"http://maps.live.com//Manifests/MO.xml", DataSourceUsage.Model));

VB

' set various data sources, here for elevation data, terrain data, and model data.
_globeControl.Host.DataSources.Add(New DataSourceLayerData("Elevation", "Elevation", "http://maps.live.com//Manifests/HD.xml", DataSourceUsage.ElevationMap))
_globeControl.Host.DataSources.Add(New DataSourceLayerData("Texture", "Texture", "http://maps.live.com//Manifests/AT.xml", DataSourceUsage.TextureMap))
_globeControl.Host.DataSources.Add(New DataSourceLayerData("Models", "Models", "http://maps.live.com//Manifests/MO.xml", DataSourceUsage.Model))

By passing the URL of the content manifest, a name for the layer, and what the manifest represents, a new DataSource is created, which is in turn used to create a DataSourceLayerData object which is then given to the VE3D control to consume.  This should be done in the FirstFrameRendered event handler as well.

We also need to setup VE3D to turn off any UI elements, turn on the atmosphere effects, and ensure we have a full unobstructed view. Again, in the FirstFrameRendered event handler, we can use the following code to achieve this:

C#

// turn on the nice atmosphere
_globeControl.Host.WorldEngine.Environment.AtmosphereDisplay = Microsoft.MapPoint.Rendering3D.Atmospherics.EnvironmentManager.AtmosphereStyle.Scattering;
 
// default to all off
_globeControl.Host.WorldEngine.Display3DCursor = false;
_globeControl.Host.WorldEngine.SetWindowsCursor(null);
_globeControl.Host.WorldEngine.ShowNavigationControl = false;
_globeControl.Host.WorldEngine.ShowCursorLocationInformation = false;
_globeControl.Host.WorldEngine.ShowScale = false;
_globeControl.Host.WorldEngine.ShowUI = false;
_globeControl.Host.WorldEngine.Environment.SunPosition = null;
_globeControl.Host.WorldEngine.Environment.LocalWeatherEnabled = true;
_globeControl.Host.WorldEngine.BaseCopyrightText = " "; // workaround for a display issue

VB

' turn on the nice atmosphere
_globeControl.Host.WorldEngine.Environment.AtmosphereDisplay = Microsoft.MapPoint.Rendering3D.Atmospherics.EnvironmentManager.AtmosphereStyle.Scattering
 
' default to all off
_globeControl.Host.WorldEngine.Display3DCursor = False
_globeControl.Host.WorldEngine.SetWindowsCursor(Nothing)
_globeControl.Host.WorldEngine.ShowNavigationControl = False
_globeControl.Host.WorldEngine.ShowCursorLocationInformation = False
_globeControl.Host.WorldEngine.ShowScale = False
_globeControl.Host.WorldEngine.ShowUI = False
_globeControl.Host.WorldEngine.Environment.SunPosition = Nothing
_globeControl.Host.WorldEngine.Environment.LocalWeatherEnabled = True
_globeControl.Host.WorldEngine.BaseCopyrightText = " " ' workaround for a display issue

If you were to run the application at this point, you would see a fully functioning Virtual Earth 3D control with proper data and navigation.

Control Scheme and Bindings

The user will control VE3D with the Wiimote by holding the nunchuk in the left hand, which will move the user forward/back/left/right using the joystick.  The C and Z buttons on the front of the nunchuk will be used to raise and lower the altitude of the camera.  The Wiimote, held in the right hand, will be used to toggle various things on or off and interact with menus.  The user will also stand on the Balance Board which will use their center of gravity to turn them in the environment.

VE3D bindings allow you to change or create new control schemes for VE3D by placing an XML file in a specific directory as follows:

  • Vista: C:\Users\<username>\AppData\LocalLow\Microsoft\Virtual Earth 3D
  • XP: C:\Documents and Settings\<username>\Local Settings\Microsoft\Virtual Earth 3D

In this directory you will find a Bindings.xml file.  This XML schema defines the default keyboard, mouse, Gamepad and other input device properties.  Open the file to see the schema used to define events and parameters.

By default, VE3D will load any file named Bindings*.xml from this directory.  For the Wiimote control scheme, create a new file named BindingsWiiEarthVR.xml in this directory.  Set the contents of the file to the following:

<?xml version="1.0" encoding="utf-8" ?>
<Bindings>
    <BindingSet Name="WiiEarthVRBindings" AutoUse="True" Cursor="Drag">
        <!-- Nunchuk joystick -->
        <Bind Event="Wiimote.NunchukX" Factor="0.5"><Action Name="Strafe"/></Bind>
        <Bind Event="Wiimote.NunchukY" Factor="1"><Action Name="Move"/></Bind>
 
        <!-- Nunchuk buttons -->
        <Bind Event="Wiimote.NunchukC" Factor="0.20"><Action Name="Ascend"/></Bind>
        <Bind Event="Wiimote.NunchukZ" Factor="-0.20"><Action Name="Ascend"/></Bind>
 
        <!-- Balance Board -->
        <Bind Event="Wiimote.BalanceBoardX" Factor="-0.0009"><Action Name="Turn"/></Bind>
        <Bind Event="Wiimote.BalanceBoardY" Factor="0.0009"><Action Name="Ascend"/></Bind>
 
        <!-- Wiimote buttons -->
        <Bind Event="Wiimote.Home"><Action Name="ResetOnCenter"/></Bind>
        <Bind Event="Wiimote.A" Factor="1"><Action Name="Locations, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
 
        <Bind Event="Wiimote.Left" Factor="-1"><Action Name="Locations, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
        <Bind Event="Wiimote.Up" Factor="-1"><Action Name="LocationsMove, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
        <Bind Event="Wiimote.Down" Factor="1"><Action Name="LocationsMove, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
 
        <!-- FPS-style keyboard controls in case we don't have a nunchuk -->
        <Bind Event="Key.W" Factor="22"><Action Name="Move" /></Bind>
        <Bind Event="Key.S" Factor="-22"><Action Name="Move" /></Bind>
        <Bind Event="Key.D" Factor="22"><Action Name="Strafe" /></Bind>
        <Bind Event="Key.A" Factor="-22"><Action Name="Strafe" /></Bind>
        <Bind Event="Key.Space" Factor="20"><Action Name="Ascend" /></Bind>
        <Bind Event="Key.C" Factor="-20"><Action Name="Ascend" /></Bind>
 
        <!-- Other keys -->
        <Bind Event="Key.F1"><Action Name="VR920SetZero, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
        <Bind Event="Key.F2"><Action Name="BalanceBoardSetZero, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
        <Bind Event="Key.F3"><Action Name="ToggleVR920Stereo, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
 
        <Bind Event="Wiimote.Minus" Factor="-0.1"><Action Name="VR920SetEyeDistance, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
        <Bind Event="Wiimote.Plus" Factor="0.1"><Action Name="VR920SetEyeDistance, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
 
        <Bind Event="Wiimote.One"><Action Name="ToggleBalanceBoard, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
        <Bind Event="Wiimote.Two"><Action Name="ToggleVR920, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
        <Bind Event="Key.B"><Action Name="ToggleBalanceBoard, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
        <Bind Event="Key.V"><Action Name="ToggleVR920, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
        <Bind Event="Key.F"><Action Name="FullScreen, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/></Bind>
    </BindingSet>
</Bindings>

The <BindingSet> tags wrap groups of control bindings.  It requires a Name and optionally a Cursor.  If the binding set is to be used automatically, as it would be in most cases, set the AutoUse parameter to True.  Inside of that are <Bind> tags.  The tag requires the Event parameter and optionally the Factor parameter.  The Event parameter will be used to match the binding to its handler which will be written later.  The syntax is <Handler Name>.<Event Name>.  The Factor parameter is optional and can be used to scale the data value up or down to increase or decrease sensitivity of the input method.  The Action tag inside the Bind tag is used to map the specific binding to a particular method.  Once the handler is written, these will make more sense.

The bindings above create the control scheme described above:  NunchukX/Y describe what happens when the analog joystick is moved, NunchukC/Z describe what happens with the C/Z buttons are pressed, and so on.

The bindings also allow for several variations.  Bindings are defined for both the IR position (IRX, IRY) and accelerometer values (AX, AY).  If an IR sensor bar is not available, the accelerometer values of the Wiimote can be used instead.  Additionally, keyboard bindings are created in the style of a first person shooter using WASD.  These can be used if a Nunchuk is not available.

Note that some bindings append two Events together with a + sign.  This allows for button combinations.  In this case, for the accelerometer and/or IR sensor, we only want to register the action if a button is pressed down.  So, those events which require the button to be held down contain Wiimote.B+ and the event it is combined with.

For those events which require a custom action that will be written separately and not part of the VE3D control, the Action parameter must contain the action name, followed by a comma, and then the full assembly name:

<Bind Event="Wiimote.One">
<Action Name="ToggleBalanceBoard, WiiEarthVR, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</Bind>


You can change any of these button bindings simply by changing this XML file and deploying to the directory above.

Event Source

An EventSource is needed which will grab data from the Wiimote and pass it along to VE3D as defined by the bindings file above.  Create a new class named WiimoteEventSource which derives from Microsoft.MapPoint.Binding.EventSource  as follows:

Next, add an enumeration named WiimoteEvent (the name isn't important) which contains all of the Name items from the bindings XML file above.  It should look like this:

C#

// all events handled by this event source from XML file
public enum WiimoteEvent
{
IRX, // IR X position
IRY, // IR Y position
NunchukX, // Nunchuk joystick X position
NunchukY, // Nunchuk joystick Y position
NunchukC, // Nunchuk C button
NunchukZ, // Nunchuk Z button
AX, // Wiimote accelerometer X
AY, // Wiimote accelerometer Y
Up, // Dpad up
Down, // Dpad down
Left, // Dpad left
Right, // Dpad right
A, // A button
B, // B button
Minus, // Minus button
Home, // Wiimote Home button
Plus, // Plus button
One, // Wiimote One button
Two, // Wiimote Two button
BalanceBoardX, // Balance Board COG X
BalanceBoardY // Balance Board COG Y
}

VB

' all events handled by this event source from XML file
Public Enum WiimoteEvent
IRX ' IR X position
IRY ' IR Y position
NunchukX ' Nunchuk joystick X position
NunchukY ' Nunchuk joystick Y position
NunchukC ' Nunchuk C button
NunchukZ ' Nunchuk Z button
AX ' Wiimote accelerometer X
AY ' Wiimote accelerometer Y
Up ' Dpad up
Down ' Dpad down
Left ' Dpad left
Right ' Dpad right
A ' A button
B ' B button
Minus ' Minus button
Home ' Wiimote Home button
Plus ' Plus button
One ' Wiimote One button
Two ' Wiimote Two button
BalanceBoardX ' Balance Board COG X
BalanceBoardY ' Balance Board COG Y
End Enum

Next, several methods from the EventSource object need to be overridden:  GetEventData, IsModifier, CanModify, TryGetEventId, TryGetEventName, Name.  The methods do the following:

Method/Property

Description

GetEventData Unsure at the moment...does not need to be implemented?
IsModifier Returns a boolean stating whether the passed in event ID is a modifier (such as the Wiimote.B event above)
CanModify Returns a boolean stating whether the current event is allowed as a modifier
TryGetEventID Maps a string event name from the bindings file to the integer value in the enumeration above
TryGetEventName Maps an integer event ID to the string name in the enumeration above
Name (property) Returns the name of the handler which must match the name in the XML file above (Wiimote in this case)

The code for these methods is presented below:

C#

// return out value of the passed enum
public override bool TryGetEventId(string eventName, out int eventId)
{
    eventId = (int)Enum.Parse(typeof(WiimoteEvent), eventName);
    return true;
}
 
// return out the string name of the passed in enum value
public override bool TryGetEventName(int eventId, out string eventName)
{
    eventName = ((WiimoteEvent)eventId).ToString();
    return true;
}
 
// unknown
public override EventData GetEventData(int eventId, EventActivateState state)
{
    throw new NotImplementedException();
}
 
// can the event be used as a modifier?
public override bool IsModifier(int eventId)
{
    // yes to all for now
    return true;
}
 
// can the supplied event be used as a modifier?
public override bool CanModify(int eventId, EventKey other)
{
    // only if it's from us
    return (other.Source == this);
}
 
// this must match the Source name in the bindings XML file
public override string Name
{
    get { return "Wiimote"; }
}

VB

' return out value of the passed enum
Public Overrides Function TryGetEventId(ByVal eventName As String, <System.Runtime.InteropServices.Out()> ByRef eventId As Integer) As Boolean
    eventId = CInt(Fix(System.Enum.Parse(GetType(WiimoteEvent), eventName)))
    Return True
End Function
 
' return out the string name of the passed in enum value
Public Overrides Function TryGetEventName(ByVal eventId As Integer, <System.Runtime.InteropServices.Out()> ByRef eventName As String) As Boolean
    eventName = (CType(eventId, WiimoteEvent)).ToString()
    Return True
End Function
 
' unknown
Public Overrides Function GetEventData(ByVal eventId As Integer, ByVal state As EventActivateState) As EventData
    Throw New NotImplementedException()
End Function
 
' can the event be used as a modifier?
Public Overrides Function IsModifier(ByVal eventId As Integer) As Boolean
    ' yes to all for now
    Return True
End Function
 
' can the supplied event be used as a modifier?
Public Overrides Function CanModify(ByVal eventId As Integer, ByVal other As EventKey) As Boolean
    ' only if it's from us
    Return (other.Source Is Me)
End Function
 
' this must match the Source name in the bindings XML file
Public Overrides ReadOnly Property Name() As String
    Get
        Return "Wiimote"
    End Get
End Property

With that in place, the constructor can be implemented which will call the base constructor and connect to the Wiimote.  It is assumed you read the Wiimote article above and know how the library works.

The constructor must take one argument passed from the main from:  an instance of the GlobeControl's ActionSystem.  This just gets passed directly to the parent object's constructor untouched.  The constructor code looks like the following:

C#

public WiimoteEventSource(ActionSystem actionSystem) : base(actionSystem)
{
// get all connected Wiimotes
WiimoteCollection wc = new WiimoteCollection();
wc.FindAllWiimotes();

// setup wiimotes and event handlers
foreach(Wiimote wm in wc)
{
wm.WiimoteChanged += new EventHandler<WiimoteChangedEventArgs>(OnWiimoteChanged);
wm.WiimoteExtensionChanged += new EventHandler<WiimoteExtensionChangedEventArgs>(OnWiimoteExtensionChanged);
wm.Connect();

// if we don't have an extension, set the report type to IR and accel's only
if(!wm.WiimoteState.Extension && wm.WiimoteState.ExtensionType != ExtensionType.BalanceBoard)
wm.SetReportType(InputReport.IRAccel, true);

if(wm.WiimoteState.ExtensionType == ExtensionType.BalanceBoard)
_bb = wm;
else
_wm = wm;

// turn off all LEDs
wm.SetLEDs(0x00);
}
}

VB

Public Sub New(ByVal actionSystem As ActionSystem)
MyBase.New(actionSystem)

' get all connected Wiimotes
Dim wc As New WiimoteCollection()
wc.FindAllWiimotes()

' setup wiimotes and event handlers
For Each wm As Wiimote In wc
AddHandler wm.WiimoteChanged, AddressOf OnWiimoteChanged
AddHandler wm.WiimoteExtensionChanged, AddressOf OnWiimoteExtensionChanged
wm.Connect()

' if we don't have an extension, set the report type to IR and accel's only
If (Not wm.WiimoteState.Extension) AndAlso wm.WiimoteState.ExtensionType <> ExtensionType.BalanceBoard Then
wm.SetReportType(InputReport.IRAccel, True)
End If

If wm.WiimoteState.ExtensionType = ExtensionType.BalanceBoard Then
_bb = wm
Else
_wm = wm
End If

' turn off all LEDs
wm.SetLEDs(&H00)
Next wm
End Sub

The OnWiimoteExtensionChanged method simply sets the report mode for the Wiimote based on whether or not a Nunchuk is inserted as shown:

C#

private void OnWiimoteExtensionChanged(object sender, WiimoteExtensionChangedEventArgs args)
{
if(_wm == null)
return;

// if nunchuk inserted, set the report type to return extension data
if(args.ExtensionType == ExtensionType.Nunchuk && args.Inserted)
_wm.SetReportType(InputReport.IRExtensionAccel, true);
else // in all other cases, set it to the default IR and accel's
_wm.SetReportType(InputReport.IRAccel, true);
}

VB

Private Sub OnWiimoteExtensionChanged(ByVal sender As Object, ByVal args As WiimoteExtensionChangedEventArgs)
If _wm Is Nothing Then
Return
End If

' if nunchuk inserted, set the report type to return extension data
If args.ExtensionType = ExtensionType.Nunchuk AndAlso args.Inserted Then
_wm.SetReportType(InputReport.IRExtensionAccel, True)
Else ' in all other cases, set it to the default IR and accel's
_wm.SetReportType(InputReport.IRAccel, True)
End If
End Sub

The OnWiimoteChanged event handler is where the Wiimote data is handled and sent off to the VE3D control to reflect the changes.  First, we handle the Balance Board.  If the Balance Board is the controller reporting data, we take the center of gravity values and pass them to VE3D.  This code looks at the appropriate values, determines if they are beyond the specified thresholds for the dead zones, and, if they are, activates the event for that value using the Execute method.  Execute is a method in the base EventSource class.  This method will activate the event specified from the enumeration (which, remember, is contained in the bindings XML file) with the value associated with that event.  An EventData object of some type must be created and passed to the Execute method.  There are two EventData types to know about:  AxisEventData and ButtonEventDataAxisEventData should be used when an event is activated that will modify the map position in some way.  That is, if the map is being turned, elevation is changing, etc.  ButtonEventData should be used if the event is a simple toggle like pressing a button down and releasing it.

The Balance Board code can be seen below:

C#

if(ws.ExtensionType == ExtensionType.BalanceBoard && this.BalanceBoardEnabled)
{
    float x1 = ws.BalanceBoardState.CenterOfGravity.X;
    if(x1 > Properties.Settings.Default.BBDeadX || x1 < -Properties.Settings.Default.BBDeadX)
        this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.BalanceBoardX), x1 - _zero.X));
    float y1 = ws.BalanceBoardState.CenterOfGravity.Y;
    if(y1 > Properties.Settings.Default.BBDeadY || y1 < -Properties.Settings.Default.BBDeadY)
        this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.BalanceBoardY), y1 - _zero.Y));
}

VB

If ws.ExtensionType = ExtensionType.BalanceBoard AndAlso Me.BalanceBoardEnabled Then
    Dim x1 As Single = ws.BalanceBoardState.CenterOfGravity.X
    If x1 > My.Settings.Default.BBDeadX OrElse x1 < -My.Settings.Default.BBDeadX Then
        Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.BalanceBoardX))), x1 - _zero.X))
    End If
    Dim y1 As Single = ws.BalanceBoardState.CenterOfGravity.Y
    If y1 > My.Settings.Default.BBDeadY OrElse y1 < -My.Settings.Default.BBDeadY Then
        Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.BalanceBoardY))), y1 - _zero.Y))
    End If

Next, let's handle the IR and accelerometer data.  The IR midpoint of the X and Y axes will be used from the WiimoteState object to activate the IRX and IRY events we defined above in the bindings XML file.  The accelerometer X and Y values will be used to activate the AX and AY events.

This snippet assumes that there is a boolean property named UseIR created in the project to determine whether IR or motion values are used.  Additionally, it assumes there are property settings created which contain values for the X/Y "dead zones" for the IR and accelerometers.  These dead zones are used as a way to only activate the event when the values are pushed beyond the thresholds.  This allows there to be a margin where the user's hand will not be read as movement, allowing the user to not have to worry about keeping a steady hand.

The application linked above uses the following default values for dead zones:

  • NunchukDeadX/Y -> 0.025
  • WiimoteDeadX/Y -> 0.15
  • IRDeadX/Y -> 0.1
  • BBDeadX/Y –> 3

C#

// if we're using the IR
if(Properties.Settings.Default.UseIR)
{
    // and both LEDs are found
    if(ws.IRState.Found1 && ws.IRState.Found2)
    {
        // normalize the midpoints to -0.5 to 0.5 (from 0 to 1.0)
        float x = ws.IRState.MidX - 0.5f;
        float y = ws.IRState.MidY - 0.5f;
 
        // if we're beyond the thresholds, activate the events
        if(x > Properties.Settings.Default.IRDeadX || x < -Properties.Settings.Default.IRDeadX)
            this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.IRX), x));
        if(y > Properties.Settings.Default.IRDeadY || y < -Properties.Settings.Default.IRDeadY)
            this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.IRY), y));
 
        // save the last IR settings...these get used if we go beyond the range of the IRs.
        // in that case, the last used positions will be used until the Wiimote comes back in range
        this._lastIRX = x;
        this._lastIRY = y;
    }
    else // one or both LEDs aren't seen
    {
        // activate events based on the last known positions
        if(this._lastIRX > Properties.Settings.Default.IRDeadX || this._lastIRX < -Properties.Settings.Default.IRDeadX)
            this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.IRX), this._lastIRX));
        if(this._lastIRY > Properties.Settings.Default.IRDeadY || this._lastIRY < -Properties.Settings.Default.IRDeadY)
            this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.IRY), this._lastIRY));
    }
}
else // we're using motion controls
{
    // activate the events based on the accelerometer values
    if(ws.AccelState.X > Properties.Settings.Default.WiimoteDeadX || ws.AccelState.X < -Properties.Settings.Default.WiimoteDeadX)
        this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.AX), ws.AccelState.X));
    if(ws.AccelState.Y > Properties.Settings.Default.WiimoteDeadY || ws.AccelState.Y < -Properties.Settings.Default.WiimoteDeadY)
        this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.AY), ws.AccelState.Y));
}

VB

' if we're using the IR
If My.Settings.Default.UseIR Then
    ' and both LEDs are found
    If ws.IRState.Found1 AndAlso ws.IRState.Found2 Then
        ' normalize the midpoints to -0.5 to 0.5 (from 0 to 1.0)
        Dim x As Single = ws.IRState.MidX - 0.5f
        Dim y As Single = ws.IRState.MidY - 0.5f
 
        ' if we're beyond the thresholds, activate the events
        If x > My.Settings.Default.IRDeadX OrElse x < -My.Settings.Default.IRDeadX Then
            Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.IRX))), x))
        End If
        If y > My.Settings.Default.IRDeadY OrElse y < -My.Settings.Default.IRDeadY Then
            Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.IRY))), y))
        End If
 
        ' save the last IR settings...these get used if we go beyond the range of the IRs.
        ' in that case, the last used positions will be used until the Wiimote comes back in range
        Me._lastIRX = x
        Me._lastIRY = y
    Else ' one or both LEDs aren't seen
        ' activate events based on the last known positions
        If Me._lastIRX > My.Settings.Default.IRDeadX OrElse Me._lastIRX < -My.Settings.Default.IRDeadX Then
            Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.IRX))), Me._lastIRX))
        End If
        If Me._lastIRY > My.Settings.Default.IRDeadY OrElse Me._lastIRY < -My.Settings.Default.IRDeadY Then
            Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.IRY))), Me._lastIRY))
        End If
    End If
Else ' we're using motion controls
    ' activate the events based on the accelerometer values
    If ws.AccelState.X > My.Settings.Default.WiimoteDeadX OrElse ws.AccelState.X < -My.Settings.Default.WiimoteDeadX Then
        Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.AX))), ws.AccelState.X))
    End If
    If ws.AccelState.Y > My.Settings.Default.WiimoteDeadY OrElse ws.AccelState.Y < -My.Settings.Default.WiimoteDeadY Then
        Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.AY))), ws.AccelState.Y))
    End If
End If

Next, the nunchuk values need to be read and the associated events activated.  This is done as follows:

C#

// if the nunchuk is connected
if(ws.Extension && ws.ExtensionType == ExtensionType.Nunchuk)
{
    // activate the nunchuk-based events
    if(ws.NunchukState.X > Properties.Settings.Default.NunchukDeadX || ws.NunchukState.X < -Properties.Settings.Default.NunchukDeadX)
        this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.NunchukX), ws.NunchukState.X));
    if(ws.NunchukState.Y > Properties.Settings.Default.NunchukDeadY || ws.NunchukState.Y < -Properties.Settings.Default.NunchukDeadY)
        this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.NunchukY), ws.NunchukState.Y));
    if(ws.NunchukState.C)
        this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.NunchukC), 1.0f));
    if(ws.NunchukState.Z)
        this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.NunchukZ), 1.0f));
}

VB

' if the nunchuk is connected
If ws.Extension AndAlso ws.ExtensionType = ExtensionType.Nunchuk Then
    ' activate the nunchuk-based events
    If ws.NunchukState.X > My.Settings.Default.NunchukDeadX OrElse ws.NunchukState.X < -My.Settings.Default.NunchukDeadX Then
        Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.NunchukX))), ws.NunchukState.X))
    End If
    If ws.NunchukState.Y > My.Settings.Default.NunchukDeadY OrElse ws.NunchukState.Y < -My.Settings.Default.NunchukDeadY Then
        Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.NunchukY))), ws.NunchukState.Y))
    End If
    If ws.NunchukState.C Then
        Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.NunchukC))), 1.0f))
    End If
    If ws.NunchukState.Z Then
        Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.NunchukZ))), 1.0f))
    End If
End If

Finally, the button events need to be activated.  A helper method which will check the current button state will be used for determining which button of all the Wiimote buttons is pressed.  For those that are, the appropriate event is activated with a call to Execute.

C#

private void HandleButton(WiimoteEvent we, bool buttonState, bool lastButtonState)
{
    if(buttonState == lastButtonState)
        return;
    else
    {
        if(buttonState)
            this.Execute(new ButtonEventData(new EventKey(this, (int)we), EventActivateState.Activate));
        else
            this.Execute(new ButtonEventData(new EventKey(this, (int)we), EventActivateState.Deactivate));
    }
}
 
// handle all the Wiimote buttons
HandleButton(WiimoteEvent.Up, ws.ButtonState.Up, _lastBS.Up);
HandleButton(WiimoteEvent.Down, ws.ButtonState.Down, _lastBS.Down);
HandleButton(WiimoteEvent.Left, ws.ButtonState.Left, _lastBS.Left);
HandleButton(WiimoteEvent.Right, ws.ButtonState.Right, _lastBS.Right);
HandleButton(WiimoteEvent.A, ws.ButtonState.A, _lastBS.A);
HandleButton(WiimoteEvent.B, ws.ButtonState.B, _lastBS.B);
HandleButton(WiimoteEvent.Minus, ws.ButtonState.Minus, _lastBS.Minus);
HandleButton(WiimoteEvent.Home, ws.ButtonState.Home, _lastBS.Home);
HandleButton(WiimoteEvent.Plus, ws.ButtonState.Plus, _lastBS.Plus);
HandleButton(WiimoteEvent.One, ws.ButtonState.One, _lastBS.One);
HandleButton(WiimoteEvent.Two, ws.ButtonState.Two, _lastBS.Two);
 
...
 
// save off the current button state for next time
_lastBS = ws.ButtonState;
_lastNunchuk = ws.NunchukState;

VB

Private Sub HandleButton(ByVal we As WiimoteEvent, ByVal buttonState As Boolean, ByVal lastButtonState As Boolean)
    If buttonState = lastButtonState Then
        Return
    Else
        If buttonState Then
            Me.Execute(New ButtonEventData(New EventKey(Me, CInt(Fix(we))), EventActivateState.Activate))
        Else
            Me.Execute(New ButtonEventData(New EventKey(Me, CInt(Fix(we))), EventActivateState.Deactivate))
        End If
    End If
End Sub
 
...
 
' handle all the Wiimote buttons
HandleButton(WiimoteEvent.Up, ws.ButtonState.Up, _lastBS.Up)
HandleButton(WiimoteEvent.Down, ws.ButtonState.Down, _lastBS.Down)
HandleButton(WiimoteEvent.Left, ws.ButtonState.Left, _lastBS.Left)
HandleButton(WiimoteEvent.Right, ws.ButtonState.Right, _lastBS.Right)
HandleButton(WiimoteEvent.A, ws.ButtonState.A, _lastBS.A)
HandleButton(WiimoteEvent.B, ws.ButtonState.B, _lastBS.B)
HandleButton(WiimoteEvent.Minus, ws.ButtonState.Minus, _lastBS.Minus)
HandleButton(WiimoteEvent.Home, ws.ButtonState.Home, _lastBS.Home)
HandleButton(WiimoteEvent.Plus, ws.ButtonState.Plus, _lastBS.Plus)
HandleButton(WiimoteEvent.One, ws.ButtonState.One, _lastBS.One)
HandleButton(WiimoteEvent.Two, ws.ButtonState.Two, _lastBS.Two)
 
' save off the current button state for next time
_lastBS = ws.ButtonState
_lastNunchuk = ws.NunchukState

And, the current button values are stored away to check on the next event so button events are only fired once.

Now that the event source object is written, it needs to be hooked up to the GlobeControl so it can be used.  This can be done by creating an instance of the WiimoteEventSource object, passing in the VE3D's ActionSystem from the BindingsManager object.  Then, the event source instance is passed to the ActionSystem's EventSourceManager and registered using the RegisterEventSource method.  Event sources should re registered before the control is added to the form.

C#

// wiimote events
private WiimoteEventSource _wiimoteEventSource;
 
...
 
// create a new instance of the Wiimote event handler
_wiimoteEventSource = new WiimoteEventSource(this.globeControl.Host.BindingsManager.ActionSystem, this);
 
// register it in the event source list
_globeControl.Host.BindingsManager.ActionSystem.EventSourceManager.RegisterEventSource(this._wiimoteEventSource);

VB

' wiimote events
Private _wiimoteEventSource As WiimoteEventSource
 
...
 
' create a new instance of the Wiimote event handler
_wiimoteEventSource = New WiimoteEventSource(Me.globeControl.Host.BindingsManager.ActionSystem, Me)
 
' register it in the event source list
_globeControl.Host.BindingsManager.ActionSystem.EventSourceManager.RegisterEventSource(Me._wiimoteEventSource)

Actions and the BindingManager

Our binding list contains several action types that are not defined by the default VE3D actions.  These actions and their handlers must be registered with the VE3D control.  The actions can be registered as follows in the FirstFrameRendered event handler:

C#

BindingsSource bs = new BindingsSource(base.GetType());
_globeControl.Host.BindingsManager.RegisterAction(bs, "Locations", LocationsHandler);
_globeControl.Host.BindingsManager.RegisterAction(bs, "LocationsMove", LocationsMoveHandler);
_globeControl.Host.BindingsManager.RegisterAction(bs, "ToggleVR920", ToggleVR920Handler);
_globeControl.Host.BindingsManager.RegisterAction(bs, "VR920SetZero", VR920SetZero);
_globeControl.Host.BindingsManager.RegisterAction(bs, "BalanceBoardSetZero", BalanceBoardSetZero);
_globeControl.Host.BindingsManager.RegisterAction(bs, "ToggleBalanceBoard", ToggleBalanceBoardHandler);
_globeControl.Host.BindingsManager.RegisterAction(bs, "FullScreen", FullScreenHandler);
_globeControl.Host.BindingsManager.RegisterAction(bs, "ToggleVR920Stereo", ToggleVR920Stereo);
_globeControl.Host.BindingsManager.RegisterAction(bs, "VR920SetEyeDistance", VR920SetEyeDistance);

VB

BindingsSource bs = new BindingsSource(base.GetType());
_globeControl.Host.BindingsManager.RegisterAction(bs, "Locations", LocationsHandler);
_globeControl.Host.BindingsManager.RegisterAction(bs, "LocationsMove", LocationsMoveHandler);
_globeControl.Host.BindingsManager.RegisterAction(bs, "ToggleVR920", ToggleVR920Handler);
_globeControl.Host.BindingsManager.RegisterAction(bs, "VR920SetZero", VR920SetZero);
_globeControl.Host.BindingsManager.RegisterAction(bs, "BalanceBoardSetZero", BalanceBoardSetZero);
_globeControl.Host.BindingsManager.RegisterAction(bs, "ToggleBalanceBoard", ToggleBalanceBoardHandler);
_globeControl.Host.BindingsManager.RegisterAction(bs, "FullScreen", FullScreenHandler);
_globeControl.Host.BindingsManager.RegisterAction(bs, "ToggleVR920Stereo", ToggleVR920Stereo);
_globeControl.Host.BindingsManager.RegisterAction(bs, "VR920SetEyeDistance", VR920SetEyeDistance);

With the actions registered and handlers associated with them, the actual handlers need to be implemented.  All event handler methods must be of the following signature:

C#

public bool EventHandler(EventData cause)

VB

Public Function EventHandler(ByVal cause As EventData) As Boolean

Let's take a look at the FullScreen binding which simply turns the status bar at the bottom of the window on and off:

C#

private bool FullScreenHandler(EventData eventData)
{
if(eventData.Activate)
BeginInvoke(new UIEventHandlerDelegate(FullScreen), eventData);
return true;
}

VB

Private Function FullScreenHandler(ByVal eventData As EventData) As Boolean
If eventData.Activate Then
BeginInvoke(New UIEventHandlerDelegate(AddressOf FullScreen), eventData)
End If
Return True
End Function

Private Sub FullScreen(ByVal eventData As EventData)
statusStrip1.Visible = Not statusStrip1.Visible
End Sub

Because we are in the VE3D thread when this handler is called, we need to use BeginInvoke to call the real method on the UI thread.

Be sure to check the source code for the full demo linked above for the location handler methods.  I omitted them here since it is just more of the same type of code above.

VR920 Head Tracker

Once the VR920 glasses and driver are installed on your PC, getting the data required from the glasses is quite easy.  We are going to create an object named VR920Tracker which will pull data from the device and send it to VE3D.

First we need to setup a few P/Invoke signatures to talk to the glasses as follows:

C#

[DllImport("IWEARDRV.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int IWROpenTracker();
 
[DllImport("IWEARDRV.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern void IWRZeroSet();
 
[DllImport("IWEARDRV.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int IWRGetTracking(out int yaw, out int pitch, out int roll);
 
private const int ERROR_SUCCESS  = 0;

VB

<DllImport("IWEARDRV.dll", SetLastError := True, CharSet := CharSet.Auto)> _
Private Shared Function IWROpenTracker() As Integer
End Function
 
<DllImport("IWEARDRV.dll", SetLastError := True, CharSet := CharSet.Auto)> _
Private Shared Sub IWRZeroSet()
End Sub
 
<DllImport("IWEARDRV.dll", SetLastError := True, CharSet := CharSet.Auto)> _
Private Shared Function IWRGetTracking(<System.Runtime.InteropServices.Out()> ByRef yaw As Integer, <System.Runtime.InteropServices.Out()> ByRef pitch As Integer, <System.Runtime.InteropServices.Out()> ByRef roll As Integer) As Integer
End Function
 
private const int ERROR_SUCCESS  = 0;

These 3 methods will allows to open access to the glasses, set its zero position, and get the roll, pitch and yaw information from the sensors.

In the constructor for this object, we will open a handle to the glasses and startup a timer that will poll the glasses at a regular interval to get the sensor data as shown below:

C#

private const int TimerPeriod = 1;
private MainForm _mainForm;
private Timer _timer;
 
public VR920Tracker(MainForm mainForm)
{
    _mainForm = mainForm;
 
    int openResult = IWROpenTracker();
    if(openResult != ERROR_SUCCESS)
        throw new ApplicationException("Could not connect to VR920: " + openResult);
 
    _timer = new Timer(VR920Poller, null, Timeout.Infinite, TimerPeriod);
}

VB

Private Const TimerPeriod As Integer = 1
Private _mainForm As MainForm
Private _timer As Timer
 
Public Sub New(ByVal mainForm As MainForm)
    _mainForm = mainForm
 
    Dim openResult As Integer = IWROpenTracker()
    If openResult <> ERROR_SUCCESS Then
        Throw New ApplicationException("Could not connect to VR920: " & openResult)
    End If
 
    _timer = New Timer(AddressOf VR920Poller, Nothing, Timeout.Infinite, TimerPeriod)
End Sub

The VR920Poller method referenced above will be called at a specific interval and read the values from the glasses as shown below:

C#

// collection of values for averaging
private List<double> _yawValues = new List<double>();
private List<double> _rollValues = new List<double>();
private List<double> _pitchValues = new List<double>();
 
// last calculated values
private double _lastYaw;
private double _lastPitch;
private double _lastRoll;
 
private void VR920Poller(object state)
{
    lock(this)
    {
        int yaw, pitch, roll;
 
        int result = IWRGetTracking(out yaw, out pitch, out roll);
 
        if(result != ERROR_SUCCESS)
            throw new ApplicationException("Could not get VR920 tracking information: " + result);
 
        _yawValues.Add(VR920ToRadians(yaw));
        _rollValues.Add(VR920ToRadians(roll));
        _pitchValues.Add(VR920ToRadians(pitch));
 
        if(_yawValues.Count == 5)
        {
            double y = Average(_yawValues, _lastYaw);
            if(Math.Abs(y - _lastYaw) > 0.026)
                _lastYaw = y;
 
            double p = Average(_pitchValues, _lastPitch);
            if(Math.Abs(p - _lastPitch) > 0.017)
                _lastPitch = p;
 
            _lastRoll = Average(_rollValues, _lastRoll);
 
            _yawValues.Clear();
            _pitchValues.Clear();
            _rollValues.Clear();
 
            RollPitchYaw rollPitchYaw = new RollPitchYaw(_lastRoll, _lastPitch, _lastYaw);
            _mainForm.SetRollPitchYaw(rollPitchYaw);
 
            _mainForm.lblAxes.Text = VR920ToDegrees(yaw) + ", " + VR920ToDegrees(pitch) + ", " + VR920ToDegrees(roll);
        }
    }
}
 
private double Average(List<double> values, double last)
{
    double total = 0;
 
    foreach(double value in values)
        total += value;
 
    total += last;
 
    return total / (values.Count+1);
}
 
private double VR920ToRadians(int vr920Value)
{
    return (vr920Value * .00549) * (Math.PI/180);
}
 
private double VR920ToDegrees(int vr920Value)
{
    return (vr920Value * .00549);
}

VB

' collection of values for averaging
Private _yawValues As List(Of Double) = New List(Of Double)()
Private _rollValues As List(Of Double) = New List(Of Double)()
Private _pitchValues As List(Of Double) = New List(Of Double)()
 
' last calculated values
Private _lastYaw As Double
Private _lastPitch As Double
Private _lastRoll As Double
 
Private Sub VR920Poller(ByVal state As Object)
    SyncLock Me
        Dim yaw, pitch, roll As Integer
 
        Dim result As Integer = IWRGetTracking(yaw, pitch, roll)
 
        If result <> ERROR_SUCCESS Then
            Throw New ApplicationException("Could not get VR920 tracking information: " & result)
        End If
 
        _yawValues.Add(VR920ToRadians(yaw))
        _rollValues.Add(VR920ToRadians(roll))
        _pitchValues.Add(VR920ToRadians(pitch))
 
        If _yawValues.Count = 5 Then
            Dim y As Double = Average(_yawValues, _lastYaw)
            If Math.Abs(y - _lastYaw) > 0.026 Then
                _lastYaw = y
            End If
 
            Dim p As Double = Average(_pitchValues, _lastPitch)
            If Math.Abs(p - _lastPitch) > 0.017 Then
                _lastPitch = p
            End If
 
            _lastRoll = Average(_rollValues, _lastRoll)
 
            _yawValues.Clear()
            _pitchValues.Clear()
            _rollValues.Clear()
 
            Dim rollPitchYaw As New RollPitchYaw(_lastRoll, _lastPitch, _lastYaw)
            _mainForm.SetRollPitchYaw(rollPitchYaw)
 
            _mainForm.lblAxes.Text = VR920ToDegrees(yaw) & ", " & VR920ToDegrees(pitch) & ", " & VR920ToDegrees(roll)
        End If
    End SyncLock
End Sub
 
Private Function Average(ByVal values As List(Of Double), ByVal last As Double) As Double
    Dim total As Double = 0
 
    For Each value As Double In values
        total += value
    Next value
 
    total += last
 
    Return total / (values.Count+1)
End Function
 
Private Function VR920ToRadians(ByVal vr920Value As Integer) As Double
    Return (vr920Value *.00549) * (Math.PI/180)
End Function
 
Private Function VR920ToDegrees(ByVal vr920Value As Integer) As Double
    Return (vr920Value *.00549)
End Function

This chunk calls the IWRGetTracking method and tosses the results in 3 lists for roll, pitch and yaw.  then, when the list has 5 members in it, the values are averaged and, if that resulting values is past a certain threshold, the values are passed back to the MainForm's SetRollPitchYaw method as shown below:

C#

private double _lastYaw;
 
public void SetRollPitchYaw(RollPitchYaw rollPitchYaw)
{
    if(!_initialized || _globeControl.IsDisposed || (_globeControl.Host) == null || _globeControl.Host.CameraControllers.Current == null)
        return;
 
    double y = ((_globeControl.Host.CameraControllers.Current as ActionCameraController).LastReportedViewpoint.LocalOrientation.Yaw - _lastYaw) + rollPitchYaw.Yaw;
 
    RollPitchYaw rpw = new RollPitchYaw(rollPitchYaw.Roll, rollPitchYaw.Pitch, y);
    (_globeControl.Host.CameraControllers.Current as ActionCameraController).LastReportedViewpoint.LocalOrientation.RollPitchYaw = rpw;
 
    _lastYaw = rollPitchYaw.Yaw;
}

VB

Private _lastYaw As Double
 
Public Sub SetRollPitchYaw(ByVal rollPitchYaw As RollPitchYaw)
    If (Not _initialized) OrElse _globeControl.IsDisposed OrElse (_globeControl.Host) Is Nothing OrElse _globeControl.Host.CameraControllers.Current Is Nothing Then
        Return
    End If
 
    Dim y As Double = ((TryCast(_globeControl.Host.CameraControllers.Current, ActionCameraController)).LastReportedViewpoint.LocalOrientation.Yaw - _lastYaw) + rollPitchYaw.Yaw
 
    Dim rpw As New RollPitchYaw(rollPitchYaw.Roll, rollPitchYaw.Pitch, y)
    TryCast(_globeControl.Host.CameraControllers.Current, ActionCameraController).LastReportedViewpoint.LocalOrientation.RollPitchYaw = rpw
 
    _lastYaw = rollPitchYaw.Yaw
End Sub

This method takes the roll, pitch and yaw values provided and applies them to the current CameraController used by VE3D (which, by default, is an ActionCameraController).

So, this code ultimately takes the roll, yaw and pitch of the user's head and translates that directly to the roll, yaw and pitch of the camera in VE3D providing an accurate, real-time view into the VE3D world.

Stereoscopic Images

The final piece of the puzzle is drawing frames to the VR920 glasses in such a way that the final view to the user will be three dimensional.  This works similarly to those Magic Eye puzzles you may have seen.  Essentially, for every frame, we want to take the current VE3D camera position and move it several units to the left, render that to the left eye of the glasses, and then move the camera several units to the right and render that to the right eye of the glasses.  To handle this we,  can create an object named VR920StereoStep which derives from the Step class provided by VE3D and add this to the StepManager.  When this is done, the VR920StereoStep will be called at the end of every frame drawn by VE3D.

As with the VR920Tracker class, we will need to setup a few P/Invoke method signatures to talk to the stereo driver as shown below:

C#

[DllImport("iWrstDrv.dll", EntryPoint = "IWRSTEREO_Open", SetLastError=true)]
public static extern IntPtr OpenStereo();
 
[DllImport("iWrstDrv.dll", EntryPoint = "IWRSTEREO_SetStereo")]
public static extern Boolean SetStereoEnabled(IntPtr handle, Boolean enabled);
 
[DllImport("iWrstDrv.dll", EntryPoint = "IWRSTEREO_SetLR")]
public static extern Boolean SetStereoLR(IntPtr handle, Boolean eye);
 
[DllImport("iWrstDrv.dll", EntryPoint = "IWRSTEREO_WaitForAck")]
public static extern Byte WaitForOpenFrame(IntPtr handle, Boolean eye);

VB

<DllImport("iWrstDrv.dll", EntryPoint := "IWRSTEREO_Open", SetLastError:=True)> _
Public Shared Function OpenStereo() As IntPtr
End Function
 
<DllImport("iWrstDrv.dll", EntryPoint := "IWRSTEREO_SetLR")> _
Public Shared Function SetStereoLR(ByVal handle As IntPtr, ByVal eye As Boolean) As Boolean
End Function
 
<DllImport("iWrstDrv.dll", EntryPoint := "IWRSTEREO_SetStereo")> _
Public Shared Function SetStereoEnabled(ByVal handle As IntPtr, ByVal enabled As Boolean) As Boolean
End Function
 
<DllImport("iWrstDrv.dll", EntryPoint := "IWRSTEREO_WaitForAck")> _
Public Shared Function WaitForOpenFrame(ByVal handle As IntPtr, ByVal eye As Boolean) As Byte
End Function

In the constructor for this object, we will open a handle to the stereo driver and turn on the stereo functionality of the glasses as shown below:

C#

private static readonly IntPtr INVALID_FILE_HANDLE = (IntPtr)(-1);
 
private IntPtr _hStereo = INVALID_FILE_HANDLE;
private Host _host;
private bool _stereoEnabled = true;
 
public double EyeDistance { get; set; }
 
public VR920StereoStep(StepManager manager, Host host) : base(manager)
{
    EyeDistance = 10;
    _host = host;
 
    _hStereo = OpenStereo();
    if(_hStereo != INVALID_FILE_HANDLE)
        SetStereoEnabled(_hStereo, true);
    else
        _stereoEnabled = false;
}

VB

Private Shared ReadOnly INVALID_FILE_HANDLE As IntPtr = CType(-1, IntPtr)
 
Private _hStereo As IntPtr = INVALID_FILE_HANDLE
Private _host As Host
Private _stereoEnabled As Boolean = True
 
Private privateEyeDistance As Double
Public Property EyeDistance() As Double
    Get
        Return privateEyeDistance
    End Get
    Set(ByVal value As Double)
        privateEyeDistance = value
    End Set
End Property
 
Public Sub New(ByVal manager As StepManager, ByVal host As Host)
    MyBase.New(manager)
    EyeDistance = 10
    _host = host
 
    _hStereo = OpenStereo()
    If _hStereo <> INVALID_FILE_HANDLE Then
        SetStereoEnabled(_hStereo, True)
    Else
        _stereoEnabled = False
    End If
End Sub

Next, we need to override the OnExecute method provided by the base Step class as shown below:

C#

private bool _rightEye = true;
private Vector3D _position;
 
public override void OnExecute(SceneState state)
{
    if(_hStereo != INVALID_FILE_HANDLE && _stereoEnabled && (_host.CameraControllers.Current as ActionCameraController).LastReportedViewpoint != null)
    {
        if(!_rightEye)
        {
            _position = (_host.CameraControllers.Current as ActionCameraController).LastReportedViewpoint.Position.Vector;
            (_host.CameraControllers.Current as ActionCameraController).LastReportedViewpoint.Position.Vector = _position + new Vector3D(-EyeDistance, 0, 0);
        }
        else
        {
            _position = (_host.CameraControllers.Current as ActionCameraController).LastReportedViewpoint.Position.Vector;
            (_host.CameraControllers.Current as ActionCameraController).LastReportedViewpoint.Position.Vector = _position + new Vector3D(EyeDistance, 0, 0);;
        }
 
        SetStereoLR(_hStereo, _rightEye);
        WaitForOpenFrame(_hStereo, _rightEye);
        _rightEye = !_rightEye;
    }
}

VB

Private _rightEye As Boolean = True
Private _position As Vector3D
 
Public Overrides Sub OnExecute(ByVal state As SceneState)
    If _hStereo <> INVALID_FILE_HANDLE AndAlso _stereoEnabled AndAlso (TryCast(_host.CameraControllers.Current, ActionCameraController)).LastReportedViewpoint IsNot Nothing Then
        If (Not _rightEye) Then
            _position = (TryCast(_host.CameraControllers.Current, ActionCameraController)).LastReportedViewpoint.Position.Vector
            TryCast(_host.CameraControllers.Current, ActionCameraController).LastReportedViewpoint.Position.Vector = _position + New Vector3D(-EyeDistance, 0, 0)
        Else
            _position = (TryCast(_host.CameraControllers.Current, ActionCameraController)).LastReportedViewpoint.Position.Vector
            TryCast(_host.CameraControllers.Current, ActionCameraController).LastReportedViewpoint.Position.Vector = _position + New Vector3D(EyeDistance, 0, 0)
 
        End If
 
        SetStereoLR(_hStereo, _rightEye)
        WaitForOpenFrame(_hStereo, _rightEye)
        _rightEye = Not _rightEye
    End If
End Sub

OnExecute will be called at the end of each VE3D frame rendered.  In this method, we obtain the current position of the VE3D camera and then set its position several units to the left or right (- or + EyeDistance).  SetStereoLR from the VR920 API is called, setting the appropriate eye to be active (left = false, right = true).  Then WaitForOpenFrame is called which will pause the action long enough for the glasses to draw the entire frame to eye to avoid image tearing.  Essentially, this call “sits and spins” at this location until the glasses report back that the entire frame is drawn in the proper eye and that we can continue on drawing the opposite eye for the next frame.  Finally, the _rightEye member variable is togged to the opposite value, so the next time through this method the opposite eye is drawn.

With this class in place, back in our MainForm in the FirstFrameRendered event handler, we can instantiate the object and add it to the StepManager as shown below:

C#

private VR920StereoStep _vr920StereoStep;
 
_vr920StereoStep = new VR920StereoStep(_globeControl.Host.RenderEngine.StepManager, _globeControl.Host);
_globeControl.Host.RenderEngine.StepManager.Add(_vr920StereoStep);

VB

Private _vr920StereoStep As VR920StereoStep
 
_vr920StereoStep = New VR920StereoStep(_globeControl.Host.RenderEngine.StepManager, _globeControl.Host)
_globeControl.Host.RenderEngine.StepManager.Add(_vr920StereoStep)

 

Running the Application

To run the demo, do the following:

  1. Copy BindingsWiimote.xml to the appropriate directory listed above
  2. Pair the Wiimote and Balance Board to the computer.  See the WiimoteLib article for more information on how to do that
  3. Run the executable
  4. Stand on the Balance Board
  5. Put on the glasses
  6. Zero both the Balance Board and glasses
  7. Toggle everything on or off at will

Controls

  • F1 – Set VR920 glasses to zero position
  • F2 – Set Balance Board to zero position
  • F3 – Toggle stereo mode on and off
  • Wiimote 1 or B – Toggle Balance Board on or off
  • Wiimote 2 or V – Toggle VR920 head tracking on or off
  • F – Toggle full-screen on or off
  • Nunchuk joystick X/Y – Strafe/Move
  • Nunchuk C/Z buttons – Raise/lower altitude
  • Wiimote A – Open location menu/Select location (note that if a new location is selected, the Balance Board is turned off and must be re-enabled after “landing”
  • Wiimote Dpad Up/Down – Move through List
  • Balance Board – Lean your body left or right to turn in the VE3D environment

Conclusion

With the above code, we have created a very interesting interface for Virtual Earth 3D.  The demo and source code linked above contain a few more features and bindings which enhance the application a bit more.  Be sure to give the full demo a try and check out the full source code for a few more implementation details.

Additional Information

Bio

Brian is a Microsoft C# MVP who has been actively developing in .NET since its early betas in 2000, and who has been developing solutions using Microsoft technologies and platforms for even longer. Along with .NET, Brian is particularly skilled in the languages of C, C++ and assembly language for a variety of CPUs. He is also well-versed in a wide variety of technologies including web development, document imaging, GIS, graphics, game development, and hardware interfacing. Brian has a strong background in developing applications for the health-care industry, as well as developing solutions for portable devices, such as tablet PCs and PDAs. Additionally, Brian has co-authored the book "Debugging ASP.NET" published by New Riders, and is currently co-authoring a book titled "Coding4Fun: 10 .NET Programming Projects for Wiimote, YouTube, World of Warcraft, and More" to be published by O'Reilly in December 2008. Brian is also an author for MSDN's Coding4Fun website.  You can reach Brian via vis website at http://www.brianpeek.com/ .

Tags:

Follow the Discussion

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.