張子の虎をキー・タップすると頭が上下に動くLeap Motionプログラムを作る

2013年11月15日(金)
薬師寺 国安

今回紹介するLeap Motionアプリは、画面に表示した“張り子の虎”の頭の部分をキー・タップ(あたかも指でキーを押しているように「下方向」にタップ)すると、頭が上下に揺れるアプリです。早速開発手順を見ていきましょう。以下VS2012と記述している個所はVS2013と読み変えても問題はありません。

まずWPFプロジェクトを作成しよう

今回のLeap MotionアプリもWPFで作成します。

これには、Visual Studio 2012(以下、VS 2012)のIDEを起動して、メニューバーから[ファイル]−[新規作成]−[プロジェクト]と選択して、それにより表示される[新しいプロジェクト]ダイアログで「Visual Basic」のテンプレートから「WPF アプリケーション」を選択します。

※Leap Motionは、.NET Framework 3.5と4.0に対応しています。しかし、.NET Framework 4.5でも動作します。今回のアプリは全て.NET Framework 4.5で作成しています。しかし、あくまでも対応しているのは、.NET Framework 3.5と4.0です。心配な方は、.NET Framework 4.0で作成すると安心でしょう。

[名前]欄には、ここでは「ToraTapLeapMotion」と指定します。

ソリューション・エクスプローラー内にImagesというフォルダを作成して、「虎の頭の部分」と「虎の身体の部分」の2つの透明化されたPNG画像を配置しておきます。

ダウンロードされたサンプルファイルには、これらの画像ファイルは追加済みです。

WPFの基本的な作成手順は、「画面上の図形を5本の指で操作する基本的なLeap Motionプログラムを作る」と同じ手順となるので、説明を割愛します。具体的な手順は、第1回の「参照の追加」「プロジェクトのルートに「LeapCSharp.dll」と「Leapd.dll」を追加する」「プロパティを設定する」を参考にしてください。

今回のLeap Motionアプリについて

今回のアプリは、張り子の虎の頭部分をキー・タップすると、頭が上下に揺れるアプリです(図1参照)。

キー・タップにはコツがいりますので、最初はうまく動かない可能性があります。1本の指で虎の頭をチョンチョンと2回ほどつつくような動作をしてください。タッチ・ポイントの色が赤に変わってからつつく動作をします。

図1:キー・タップすると張り子の虎の頭部分が上下に揺れる(クリックで拡大)

実際に動かした動画は下記になります。

今回は、「ポーリング方式」と「コールバック方式」の混合を用いています。「コールバック方式」については、第1回のサンプルの「コールバック方式の採用」を参照してください。「ポーリング方式」については4回の「ポーリング方式の採用」を参照してください。

またコールバックメソッド等については、「C#開発者から見たLeap Motion開発のファースト・インプレッション」を参照してください。

画面のレイアウト(MainWindow.xaml)

Canvas内に名前が「head」というImageコントロールと、名前が「body」というImageコントロールを配置し、それぞれのSourceプロパティにImagesフォルダ内にある虎の「頭」と「身体」の部分の画像を読み込んでおきます。

一番前面に、タッチ・ポイントを表示するpaintCanvasという名前のInkPresenterコントロールを配置します(リスト1)。

リスト1 書き出されたMainWindow.xaml

<Window x:Class="MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="MainWindow" Height="350" Width="525">
  <Canvas>
    <Image x:Name="head" HorizontalAlignment="Left" Height="54" VerticalAlignment="Top" Width="65" Source="Images/虎頭.png"  Canvas.Left="184" Canvas.Top="147"/>
    <Image x:Name="body" HorizontalAlignment="Left" Height="134" VerticalAlignment="Top" Width="172" Source="Images/虎身体.png" Canvas.Left="148" Canvas.Top="102"/>
  
    <InkPresenter Name="paintCanvas"/>
  </Canvas>
</Window>

レイアウトは図2のようになります。

図2:各コントロールを配置した(クリックで拡大)

Blendを起動してストーリーボードを作成

次に、アニメーションのためのストーリーボードを作成します。

まずはBlendを起動します。実際にBlend起動する前に、必ずVS 2012でプロジェクトをビルドしておきます。

VS 2012からBlendを直接起動する方法もありますが、今回は.exeファイルを実行して起動します。64bitのWindows 8環境であれば、「C:\Program Files (x86)\Microsoft Visual Studio 11.0\Blend\Blend.exe」を実行してください。なお、VS 2012をインストールすると、Blendも自動的にインストールされています。Windows 8のスタート画面にも表示されているはずですから、それをタップして起動しても構いません。

Blendが起動したら、[プロジェクトを開く]から、現在作成している「TotaTapLeapMotion.sln」ファイルを指定して開きます。

「Storybord1」というストーリーボードを作成します(作成手順は「MSDN: ストーリーボードの作成、変更、または削除」を参照してください)。

アートボード全体が赤い枠線で囲まれてストーリーボードの記録が可能になるので、[オブジェクトとタイムライン]で「head」を選択して、黄色の再生ヘッドが「0.5」の位置で、headプロパティの[変換]パネル内にある[平行移動]の「x」に「2」、「y」に「10」を指定し、同じく、[傾斜]の「x」に「0」、「y」に「-20」と指定します。次に黄色の再生ヘッドを「1」の位置で、[変換パネル]内の[平行移動]の「x」に「5」、「y」に「3」と指定します。[傾斜]の「x」と「y」には0を指定します。最後に記録された楕円のマークを選択して、「イージング」プロパティを指定します。BounceInを選択し、Bouncesに「2」、Bouncinessに「1」を指定します。この辺りの数値は、タイムラインから再生をしてみて、適当な値に設定してください(図3参照)。

図3:イージングプロパティを設定している(クリックで拡大)

Blendを閉じて、VS 2012に戻ります。保存と適用のメッセージが出ますので、保存して適用させます。

またストーリーボードのコードの下に、次のリスト2のコードが追加されていますので、このコードは削除します。このコードを残したままにしておくと、アプリを実行した際に、即、ストーリーボードが実行されてしまいます。

リスト2(MainWindow.xaml) 削除するコード

<Window.Triggers>
  <EventTrigger RoutedEvent="FrameworkElement.Loaded">
    <BeginStoryboard Storyboard="{StaticResource Storyboard1}"/>
  </EventTrigger>
</Window.Triggers>

プログラム・コード(MainWindow.xaml.vb)

では、次にプログラム・コード(MainWindows.xaml.vbファイル)を見ていきましょう。

コードは4回のコードと似通っている部分については割愛します。4回のコード説明を参照してください。異なる部分のみ解説します。

名前空間の読み込み

「Imports Leap」と「Imports System.Windows.Ink」名前空間の他に、Storyboardを使用するため「Imports System.Windows.Media.Animation」を読み込んでおきます。

メンバー変数の宣言

次にメンバー変数を宣言します。

「leap」、「touchIndicator」、「touchPoint」、「windowWidth」、「windowHeight」、「x」、「y」、「tx」、「ty」、「FingersCount」等のメンバー変数については4回と同じのため、そちらを参照してください。但し今回は、windowWidthを525、windowHeightを350で初期化しています。

モジュールの作成

VS2012メニューから[プロジェクト]−[モジュールの追加]と選択して、[名前]が「Module1.vb」という名前のモジュールを作成します(リスト3)。

リスト3 (Modeule1.vb)

Imports System.Windows.Media.Animation

Module Module1
  Public Index As Integer
  Public strb As Storyboard
End Module

MainWindow_Loadedメソッドの処理

4回と同じにつき割愛します、該当記事を参照してください。

Updateメソッドの処理

先頭に、

strb = TryCast(Me.Resources("Storyboard1"), Storyboard) ' strbに「Storyboard1」を取得して格納する

と記述し、モジュール変数strbに「Storyboard1」を取得して格納しておきます。

次にLeap Motionのタッチ処理になります。タッチのイメージは次の図4のようなイメージです。

Updateメソッドの処理(ホバー処理)

図4:Leap Motionのタッチ検出イメージ(Leap Motion SDKのAPIドキュメントから引用)(クリックで拡大)

まずホバーの場合は、表示されている円がBlueの色になります。タッチ・ポイントの位置をメンバー変数「x」と「y」に格納します。

画面に表示されている指の数をleap.Frame.Fingers.Countプロパティで取得して、メンバー変数「FingersCount」に格納しておきます(リスト3)。

リスト3 ホバー時の処理(MainWindow.xaml.vb)

For Each Pointable As Pointable In leap.Frame.Pointables
  ……コード略(前述)……
  If Pointable.TouchDistance > 0 AndAlso Pointable.TouchZone <> Global.Leap.Pointable.Zone.ZONENONE Then
         touchIndicator.Color = Colors.Blue
 
         x = touchPoint.X
         y = touchPoint.Y
         Index=0
         FingersCount = leap.Frame.Fingers.Count' 表示されている指の本数を取得して、メンバー変数FingersCountに格納しておく
  ……コード略(続きは後述)……
  End If
Next

次にタッチした処理になります。タッチした場合は、表示されている円が赤に変わります。

Updateメソッドの処理(タッチ処理)

指が1本認識されている場合は、虎の頭(head)部分の座標値を取得し、モジュール変数Indexを1で初期化します。その他の場合はIndexを0で初期化しストーリーボードを停止します(リスト4)。

リスト4 タッチとタッチ以外の処理(MainWindow.xaml.vb)

If Pointable.TouchDistance > 0 AndAlso Pointable.TouchZone <> Global.Leap.Pointable.Zone.ZONENONE Then
  ……コード略(前述)……
  ElseIf Pointable.TouchDistance <= 0 Then
   touchIndicator.Color = Colors.Red
      If FingersCount = 1 Then
                If x > head.GetValue(Canvas.LeftProperty) And x < head.GetValue(Canvas.LeftProperty) + head.Width AndAlso y > head.GetValue(Canvas.TopProperty) And y < head.GetValue(Canvas.TopProperty) + head.Height Then ' headの座標値を取得して、Indexを1で初期化する
                  Index = 1
                Else
                  Index = 0
                  strb.Stop()
                End If
              End If
       Else
              touchIndicator.Color = Colors.Gold
              Index=0
              strb.Stop()
      End If
    Next
  End Sub

ここまでは、ポーリング処理になります。次にコールバック処理を記述します。

クラスの作成(TapDirection.vb)

では、次にクラスを作成していきます。ソリューション・エクスプローラー内のToraTapLeapMotionプロジェクトを選択して、マウスの右クリックで表示されるメニューから[追加]−[クラス]と選択します。表示される画面から[クラス]を選択し、[名前]に[TapDirection.vb] と指定して[追加]ボタンをタップします。

ScreenTapDirection.vbのプログラム・コード

Enum列挙体は値を定義するための入れ物として使用する、特殊なクラスです。列挙体が便利なのはプログラム中に入力候補が自動的に表示される点です。

TapDirection.vb列挙体の中に、Tapという値を入れておきます(リスト5参照)。

リスト5(TapDirection.vb) 列挙体を記述したコード

Public Enum ScreenTapDirection
  Tap
End Enum

クラスの作成(LeapListener.vb)

次に、第1回でSwipeDirection.vbを作成した方法で、LeapListener.vbクラスを作成しますが、第1回のサンプルと同じコードが含まれているので第1回のLeapListener.vbクラスを使用します。

ソリューション・エクスプローラー内で、ToraTapLeapMotionプロジェクトを選択し、マウスの右クリックで表示されるメニューから、[追加]−[既存の項目]と選択して、第1回で作成しておいたLeapListener.vbを取り込みます。

以下、第1回のサンプルのLeapListener.vbと異なる点のみ解説します。

(1)OnConnectコールバックメソッド内で、GestureTypeにTYPEKEYTAPを指定します。
(2)OnFrameコールバックメソッド内で、GestureType.TYPESWIPEの個所を、GestureType.TYPKEYTAPに変更します。
(3)If (swipe.Direction.y > 0) ThenからEnd Ifまでを削除し

    If gst.Type = Gesture.GestureType.TYPEKEYTAP Then
      TapAction(fingers, TapDirection.Tap)
    End If

と記述します。

(4)デリゲートのコードを「Public Delegate Sub TapEvent(ByVal sd As TapDirection)」と記述します。
(5)「Public Event LeapSwipe As SwipeEvent」の部分を「Public Event LeapTap As TapEvent」に書き換えます。
(6)「ScreenTapAction」メソッド内の「Case SwipeDirection.Up」の部分を「Case TapDirection.Tap」に書き換えます。
(7)「RaiseEvent LeapSwipe(SwipeDirection.Up)」に関する個所を「 RaiseEvent LeapTap(TapDirection.ScreenTap)」に書き換えます。

上記の変更を行った後、リスト6のコードを作成します。

LeapListener.vbのプログラム・コード

リスト6(LeapListener.vb)

・・・・コード略・・・・
  Public Overrides Sub OnFrame(ctlr As Controller)
    ・・・・コード略・・・・
  If (Not fingers.IsEmpty) Then
                Dim gestures As GestureList = currentFrame.Gestures
                For Each gst As Gesture In gestures
              ' GestureTypeがTYPEKEYTAPであった場合は、
              ' TapActionメソッドを実行する。
                  If gst.Type = Gesture.GestureType.TYPEKEYTAP Then
                    TapAction(fingers, TapDirection.Tap)
                  End If
                  Next 
          End If
      End If
  End Sub

' TapEventデリゲート経由で、LeapTapメソッドを呼び出す。
Public Delegate Sub TapEvent(ByVal sd As TapDirection)
Public Event LeapTap As TapEvent

' GestureTypeがTYPEKEYTAPであった場合のTapActionメソッドの処理。
Private Sub TapAction(ByVal fingers As FingerList, ByVal sd As TapDirection)
    fingersCount = fingers.Count
    If (fingersCount = 1) Then
      Select Case sd
        Case TapDirection.Tap
          ' RaiseEventでLeapTapイベントを発生させる。
          RaiseEvent LeapTap(TapDirection.Tap)
          Exit Select
      End Select
    End If
  End Sub

クラスの作成(ToraModel.vb)

第1回のサンプルの、SwipeDirection.vbを作成した方法で、ToraModel.vbクラスを作成します。

第1回の「UpDownModel.vb」と同じコードが多いので異なる個所のみ解説します。

(1) 「Private dispatcher = Application.Current.Dispatcher」メンバー変数を宣言しし、ワーカースレッドからUIスレッドに処理を任せます。
(2) Public Sub New内に「AddHandler listener.LeapTap, AddressOf TapAction」を追加します。

上記の変更を行った後、リスト7のコードを作成します。

ToraModel.vbのプログラム・コード

リスト7(ToraModel.vb)

・・・・コード略・・・・
Public Class ToraModel
  Implements INotifyPropertyChanged
' メンバー変数の宣言
  Private ctlr As Controller
  Private listener As LeapListener
  Private dispatcher = Application.Current.Dispatcher

' Newメソッド内で、AddHandlerステートメントに、listenerオブジェクトのLeapTap
' イベントに、TapActionベントハンドラ—を追加する。
  Public Sub New()
  ・・・コード略・・・
    AddHandler listener.LeapTap, AddressOf TapAction
  End Sub
・・・・コード略・・・・

TapActionメソッドの処理

TapDirection列挙体の値がTapであった時の処理を記述します。

モジュール変数Indexが1の場合の処理、すなわち、張り子の虎のheadの座標値が取得された場合の処理です。

dispatcher.Invokeを使って、ワーカースレッドからUIスレッドに処理を任せます。
StoryboardをBeginメソッドで開始します。

具体的なコードはリスト8のようになります。

リスト8 張り子の虎の頭をタップしてストーリーボードが実行される処理(TapActionメソッド)

Private Sub TapAction(ByVal sd As TapDirection)
    Select Case sd
      Case TapDirection.Tap
        If Index = 1 Then
          dispatcher.Invoke(Sub()' dispatcher.Invokeを使って、ワーカースレッドからUIスレッドに処理を任せる。
                            strb.Begin()' Storyboardを開始する
                          End Sub)
      End If
      Exit Select
    End Select
  End Sub

MainWindow.xaml内に[ToraTapModel]を取り込む。

まず名前空間として「xmlns:local="clr-namespace:ToraTapLeapMotion"」を定義します。

次にプロパティ要素内に「」と記述します。MainWindow.xaml内に「ToraModel」クラスが取り込まれます。リスト9のようになります。

イージング処理も記述されています(ストーリーボードコード内の太字部分)

リスト9 「ToraModel」を取り込んだMainWindo.xaml

<Window x:Class="MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:ToraTapLeapMotion"
  Title="MainWindow" Height="350" Width="525">
  <Window.DataContext>
    <local:ToraModel/>
  </Window.DataContext>
  <Window.Resources>

<!--Blendから設定したStoryboadの設定-->
      <Storyboard x:Key="Storyboard1">
        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(SkewTransform.AngleY)" Storyboard.TargetName="head">
          <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="-20"/>
          <EasingDoubleKeyFrame KeyTime="0:0:1" Value="0">
            <EasingDoubleKeyFrame.EasingFunction>
              <BounceEase EasingMode="EaseIn" Bounces="2" Bounciness="1"/>
            </EasingDoubleKeyFrame.EasingFunction>
          </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)" Storyboard.TargetName="head">
          <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="2"/>
          <EasingDoubleKeyFrame KeyTime="0:0:1" Value="5">
            <EasingDoubleKeyFrame.EasingFunction>
              <BounceEase EasingMode="EaseIn" Bounces="2" Bounciness="1"/>
            </EasingDoubleKeyFrame.EasingFunction>
          </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)" Storyboard.TargetName="head">
          <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="10"/>
          <EasingDoubleKeyFrame KeyTime="0:0:1" Value="3">
            <EasingDoubleKeyFrame.EasingFunction>
              <BounceEase EasingMode="EaseIn" Bounces="2" Bounciness="1"/>
            </EasingDoubleKeyFrame.EasingFunction>
          </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>
      </Storyboard>
    </Window.Resources>
 
    <Canvas>
      <Image x:Name="head" HorizontalAlignment="Left" Height="74" VerticalAlignment="Top" Width="90" Source="Images/虎頭.png"  Canvas.Left="165" Canvas.Top="128" Stretch="None">
        <Image.RenderTransform>
          <TransformGroup>
            <ScaleTransform/>
            <SkewTransform/>
            <RotateTransform/>
            <TranslateTransform/>
          </TransformGroup>
        </Image.RenderTransform>
      </Image>
 
      <Image x:Name="body" HorizontalAlignment="Left" Height="182" VerticalAlignment="Top" Width="126" Source="Images/虎身体.png" Canvas.Left="221" Canvas.Top="69" Stretch="None" />
  
      <InkPresenter Name="paintCanvas"/>
    </Canvas>
</Window>

※注意
今回紹介したサンプルコードを動かす際には、「LeapCSharp.NET4.0.dll」や「LeapCSharp.dll」、「Leap.dll」を読者自身のフォルダ内にあるDLLファイルに指定し直さなければ動かない可能性があるので、動かない場合は再指定してください。

  • 張子の虎をキー・タップで動かすLeap Motionプログラム

    『新世代モーションコントローラー Leap Motion -Visual Basicによる実践プログラミング-』 第6回のサンプルプログラムです。
薬師寺国安事務所

薬師寺国安事務所代表。Visual Basic プログラミングと、マイクロソフト系の技術をテーマとした、書籍や記事の執筆を行う。
1950年生まれ。事務系のサラリーマンだった40歳から趣味でプログラミングを始め、1996年より独学でActiveXに取り組む。1997年に薬師寺聖とコラボレーション・ユニット PROJECT KySS を結成。2003年よりフリーになり、PROJECT KySS の活動に本格的に参加、.NETやRIAに関する書籍や記事を多数執筆する傍ら、受託案件のプログラミングも手掛ける。Windows Phoneアプリ開発を経て、現在はWindows ストア アプリを多数公開中

Microsoft MVP for Development Platforms - Client App Dev (Oct 2003-Sep 2012)。Microsoft MVP for Development Platforms - Windows Phone Development(Oct 2012-Sep 2013)。Microsoft MVP for Development Platforms - Client Development(Oct 2013-Sep 2014)。Microsoft MVP for Development Platforms-Windows Platform Development (Oct 2014-Sep 2015)。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています