Want more cowbell? Yes please!

![]() |
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. |
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
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
|
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:
Before we get started, you will need to install the Virtual Earth 3D control. If you haven't done this already, browse to https://maps.live.com/ and click on the 3D link to install the control and supporting software.
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.
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:
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.
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 |
https://local.live.com/Manifests/HD.xml | ElevationMap | Terrain data |
https://local.live.com/Manifests/MO.xml | Model | 3D buildings |
https://local.live.com/Manifests/AT.xml | TextureMap | Unlabeled aerial |
https://local.live.com/Manifests/HT.xml | TextureMap | Labeled aerial |
https://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", @"https://maps.live.com//Manifests/HD.xml", DataSourceUsage.ElevationMap));
_globeControl.Host.DataSources.Add(new DataSourceLayerData("Texture", "Texture", @"https://maps.live.com//Manifests/AT.xml", DataSourceUsage.TextureMap));
_globeControl.Host.DataSources.Add(new DataSourceLayerData("Models", "Models", @"https://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", "https://maps.live.com//Manifests/HD.xml", DataSourceUsage.ElevationMap))
_globeControl.Host.DataSources.Add(New DataSourceLayerData("Texture", "Texture", "https://maps.live.com//Manifests/AT.xml", DataSourceUsage.TextureMap))
_globeControl.Host.DataSources.Add(New DataSourceLayerData("Models", "Models", "https://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.
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:
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>
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 ButtonEventData. AxisEventData 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:
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)
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.
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.
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)
To run the demo, do the following:
Controls
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.
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/ .
WiiEarthVR Article and Source Code Posted
I previously talked about my WiiEarthVR project shown at PDC project, and now I have posted the article
Thanks for an excellent and exiting article!
In my computer (XP Pro SP3) the correct folder for the Bindings.xml is "C:\Documents and Settings\<username>\Application Data\Microsoft\Virtual Earth 3D" (not "...<username>\Local Settings\...")
I spotted that Wes Thierry has made an activex plugin for the VR 920 that can be controlled with javascript http://www.westhierry.net/vr920/vr920test.html
Here is a list of my current Coding4Fun articles: TwitterDrive and Author Interview WiiEarthVR Animated