Introduction
In my
previous article I described how you can go about inserting a 3D model into
a WPF application using Expression Blend; with a little help from
Blender. In this
article I will describe how you can go about using
Leap Motion to rotate a 3D model in
WPF, effectively creating trackball-like rotation using hand gestures.
Requirements
To run the article's project you require the following;
NB: Add a reference to LeapCSharp.NET4.0.dll and
ensure that you have also added Leap.dll and LeapCSharp.dll to
the project. The Copy to Output Directory property of the latter two
files should be set to Copy always.
Leap Motion
The Leap is a 3D motion sensing device that detects and tracks hands, fingers
and finger-like tools. With the use of the Leap SDK an application can make use of the
data captured by the Leap controller.
The 3D Model
I will be making use of the 3D model from my previous article. The 3D model
is that of a
XA-20 Razorback Strike Fighter. I have adjusted the orientation of the Camera
slightly,
and the orientation of the DirectionalLight
so that the 3D object is better illuminated.
The Code
LeapListener
is a subclass of the Listener
class, which defines a set of
callback methods that can be overridden to respond to events dispatched by the
Leap.
Imports Leap
Public Class LeapListener
Inherits Listener
Public Overrides Sub OnConnect(ctlr As Controller)
ctlr.Config.SetFloat("Gesture.Swipe.MinLength", 10)
ctlr.Config.SetFloat("Gesture.Swipe.MinVelocity", 100)
ctlr.Config.Save()
ctlr.EnableGesture(Gesture.GestureType.TYPESWIPE)
End Sub
Public Overrides Sub OnFrame(ctlr As Controller)
Dim currentFrame As Frame = ctlr.Frame
If (Not currentFrame.Hands.Empty) Then
Dim firstHand As Hand = currentFrame.Hands(0)
Dim fingers As FingerList = firstHand.Fingers
If (Not fingers.Empty) Then
Dim gestures As GestureList = currentFrame.Gestures
For Each gst As Gesture In gestures
If gst.Type = Gesture.GestureType.TYPESWIPE Then
Dim swipe As New SwipeGesture(gst)
If (Math.Abs(swipe.Direction.x) > Math.Abs(swipe.Direction.y)) Then If (swipe.Direction.x > 0) Then SwipeAction(fingers, SwipeDirection.Right)
Else SwipeAction(fingers, SwipeDirection.Left)
End If
Else If (swipe.Direction.y > 0) Then SwipeAction(fingers, SwipeDirection.Up)
Else SwipeAction(fingers, SwipeDirection.Down)
End If
End If
Exit For
End If
Next
End If
End If
End Sub
Public Delegate Sub SwipeEvent(ByVal sd As SwipeDirection)
Public Event LeapSwipe As SwipeEvent
Private fingersCount As Integer
Private Sub SwipeAction(ByVal fingers As FingerList, ByVal sd As SwipeDirection)
fingersCount = fingers.Count
If (fingersCount = 5) Then
Select Case sd
Case SwipeDirection.Left
RaiseEvent LeapSwipe(SwipeDirection.Right)
Exit Select
Case SwipeDirection.Right
RaiseEvent LeapSwipe(SwipeDirection.Left)
Exit Select
Case SwipeDirection.Up
RaiseEvent LeapSwipe(SwipeDirection.Up)
Exit Select
Case SwipeDirection.Down
RaiseEvent LeapSwipe(SwipeDirection.Down)
Exit Select
End Select
End If
End Sub
End Class
using System;
using System.Linq;
using Leap;
using XA_20_Razorback.Enums;
namespace XA_20_Razorback
{
class LeapListener: Listener
{
public override void OnConnect(Controller ctlr)
{
ctlr.Config.SetFloat("Gesture.Swipe.MinLength", 10);
ctlr.Config.SetFloat("Gesture.Swipe.MinVelocity", 100);
ctlr.Config.Save();
ctlr.EnableGesture(Gesture.GestureType.TYPESWIPE);
}
public override void OnFrame(Controller ctrl)
{
Frame currentFrame = ctrl.Frame();
if (!currentFrame.Hands.Empty)
{
Hand firstHand = currentFrame.Hands[0];
FingerList fingers = firstHand.Fingers;
if (!fingers.Empty)
{
GestureList gestures = currentFrame.Gestures();
foreach (Gesture gst in gestures)
{
SwipeGesture swipe = new SwipeGesture(gst);
if (Math.Abs(swipe.Direction.x) > Math.Abs(swipe.Direction.y)) {
if (swipe.Direction.x > 0) {
SwipeAction(fingers, SwipeDirection.Right);
}
else {
SwipeAction(fingers, SwipeDirection.Left);
}
}
else {
if (swipe.Direction.y > 0) {
SwipeAction(fingers, SwipeDirection.Up);
}
else {
SwipeAction(fingers, SwipeDirection.Down);
}
}
}
}
}
}
public delegate void SwipeEvent(SwipeDirection sd);
public event SwipeEvent LeapSwipe;
private int fingersCount;
public void SwipeAction(FingerList fingers, SwipeDirection sd)
{
fingersCount = fingers.Count();
if (fingersCount == 5)
{
switch (sd)
{
case SwipeDirection.Left:
if (LeapSwipe != null)
{
LeapSwipe(SwipeDirection.Left);
}
break;
case SwipeDirection.Right:
if (LeapSwipe != null)
{
LeapSwipe(SwipeDirection.Right);
}
break;
case SwipeDirection.Up:
if (LeapSwipe != null)
{
LeapSwipe(SwipeDirection.Up);
}
break;
case SwipeDirection.Down:
if (LeapSwipe != null)
{
LeapSwipe(SwipeDirection.Down);
}
break;
}
}
}
}
}
The OnConnect()
callback method is called when your application
connects to the Leap. This is the method where you enable detection of swipe-gestures and set the minimum length and
velocity required to detect this gesture (The length and velocity values are in
mm and mm/s respectively). Since I want to
detect slight hand/finger movements as swipe-gestures I have set the values of
the minimum length and velocity to very small values, 10mm and 100mm/s.
The OnFrame()
callback method is called when the Leap generates a new frame
of motion tracking data. This is where I process the data from the Leap to
determine the direction of a swipe gesture.
The SwipeDirection
enum contains values for four swipe-gesture directions I'm interested in,
Public Enum SwipeDirection
Up
Down
Left
Right
End Enum
namespace XA_20_Razorback.Enums
{
enum SwipeDirection
{
Up,
Down,
Left,
Right
}
}
Next is the RazorbackViewModel
which contains some properties that the 3D
object will bind to,
Imports System.ComponentModel
Imports Leap
Public Class RazorbackViewModel
Implements INotifyPropertyChanged
Private ctlr As Controller
Private listener As LeapListener
Private Const SHIFT As Double = 1
Public Sub New()
ctlr = New Controller
listener = New LeapListener
ctlr.AddListener(listener)
AddHandler listener.LeapSwipe, AddressOf SwipeAction
End Sub
Private _xAngle As Double
Public Property XAngle As Double
Get
Return _xAngle
End Get
Set(value As Double)
_xAngle = value
OnPropertyChanged("XAngle")
End Set
End Property
Private _yAngle As Double
Public Property YAngle As Double
Get
Return _yAngle
End Get
Set(value As Double)
_yAngle = value
OnPropertyChanged("YAngle")
End Set
End Property
Private Sub SwipeAction(ByVal sd As SwipeDirection)
Select Case sd
Case SwipeDirection.Up
XAngle += SHIFT
Exit Select
Case SwipeDirection.Down
XAngle -= SHIFT
Exit Select
Case SwipeDirection.Left
YAngle += SHIFT
Exit Select
Case SwipeDirection.Right
YAngle -= SHIFT
Exit Select
End Select
End Sub
Public Sub DisposeListener()
ctlr.RemoveListener(listener)
ctlr.Dispose()
End Sub
Public Event PropertyChanged(sender As Object,
e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
Private Sub OnPropertyChanged(ByVal propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
using System;
using System.Linq;
using System.ComponentModel;
using XA_20_Razorback.Enums;
using Leap;
namespace XA_20_Razorback
{
class RazorbackViewModel : INotifyPropertyChanged
{
private LeapListener listener;
private Controller ctrl;
private const double SHIFT = 1;
public RazorbackViewModel()
{
listener = new LeapListener();
ctrl = new Controller();
ctrl.AddListener(listener);
listener.LeapSwipe += SwipeAction;
}
private double _xAngle;
public double XAngle
{
get { return _xAngle; }
set
{
_xAngle = value;
OnPropertyChanged("XAngle");
}
}
private double _yAngle;
public double YAngle
{
get { return _yAngle; }
set
{
_yAngle = value;
OnPropertyChanged("YAngle");
}
}
private void SwipeAction(SwipeDirection sd)
{
switch (sd)
{
case SwipeDirection.Up:
XAngle += SHIFT;
break;
case SwipeDirection.Down:
XAngle -= SHIFT;
break;
case SwipeDirection.Left:
YAngle += SHIFT;
break;
case SwipeDirection.Right:
YAngle -= SHIFT;
break;
}
}
public void DisposeListener()
{
ctrl.RemoveListener(listener);
ctrl.Dispose();
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
The DataContext
of the window is set in the XAML markup,
<Window.DataContext>
<local:RazorbackViewModel/>
</Window.DataContext>
I need to rotate the 3D model on only two of its axes, specifically its X and Y axis,
<ModelVisual3D x:Name="DefaultGroup">
<ModelVisual3D.Transform>
<Transform3DGroup>
<Transform3DGroup.Children>
<RotateTransform3D x:Name="VerticalRTransform">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis="1 0 0" Angle="{Binding XAngle}"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<RotateTransform3D x:Name="HorizontalRTransform">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis="0 1 0" Angle="{Binding YAngle}"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</Transform3DGroup.Children>
</Transform3DGroup>
</ModelVisual3D.Transform>
...
Rotating the 3D object around its X and Y axes is sufficient to achieve the
trackball effect. The next step is to connect
your Leap Motion controller, run the project, and wave your hands about to
rotate the 3D model.
Conclusion
That's it, I hope you learnt something useful from this article.
History
- 23rd June 2013: Initial post