Leap Motion実用サンプル(Visual Basic編)
3D回転で表示される画像をLeap Motionで切り替える
画像が立体的に回転して表示され、Leap Motionで任意の画像を空中タッチすると、アニメーションを伴ってその画像が大きく表示されるサンプルWPFアプリを作ってみよう。
今回のサンプルは、画像が立体的に回転して表示され、任意の画像を空中タッチすると、アニメーションを伴って大きな画像が表示されるサンプルだ。さっそく、その開発内容を説明していこう。
まずWPFプロジェクトを作成しよう
Leap MotionのアプリはWPFで作成する。これまでの連載の手順と同様に、Visual Studio 2012(以下、VS 2012)のIDEを起動してVisual Basicの「WPFアプリケーション」テンプレートで新規プロジェクトを作成する。[名前]欄には、ここでは「ImageRotation3D_LeapMotion」と指定する。
ソリューションエクスプローラーでプロジェクト内にImagesフォルダーを作成して、回転対象となるPNG画像を数枚(本稿の例では「林_01.png」~「林_10.png」のような名前にした)、そのフォルダーの中に追加する。画像数は500枚まで動作確認済みだ。画像が多ければ多いほど面白い動作になるので、ぜひ試していただきたい。画像を追加したら、[プロパティ]ウィンドウで、全画像の[ビルド アクション]に「コンテンツ」を、[出力ディレクトリにコピー]に「常にコピーする」を指定する。
今回のWPFアプリの基本的な作成手順は、第1回と同じ手順となるので、説明を割愛する。具体的な手順は、第1回の「参照の追加」「プロジェクトのルートに「LeapCSharp.dll」と「Leapd.dll」を追加する」「プロパティを設定する」を参考にしてほしい。
XMLファイルの追加
ここでは、3つのXMLファイル(forest.xml、winter.xml、winter_forest.xml)を作成する(第6回と同じなので割愛)。まずは、forest.xmlファイルを作成しよう。XMLエディターで、次のリスト内容を記述する。なお、画像ファイル名は読者の使用する画像の名前に置き換えてほしい。
<?xml version="1.0" encoding="utf-8" ?>
<画像>
<画像名>林_01.png</画像名>
<画像名>林_02.png</画像名>
<画像名>林_03.png</画像名>
<画像名>林_04.png</画像名>
<画像名>林_05.png</画像名>
<画像名>林_06.png</画像名>
<画像名>林_07.png</画像名>
<画像名>林_08.png</画像名>
<画像名>林_09.png</画像名>
<画像名>林_10.png</画像名>
……以下、上記の繰り返しのため省略……
<画像>
|
上記と全く同じ構造で、読み込む画像ファイル名だけが異なる、winter.xmlファイルとwinter_forest.xmlファイルを作成する。XMLファイルは、本稿の最後で紹介しているダウンロード用のサンプル・ファイルに追加されているので、そちらを見てほしい。
forest.xml/winter.xml/winter_forest.xmlファイルについても、.EXEファイルと同じフォルダーに常に配置したいので、画像の場合と同様に[ビルド アクション]の値を「コンテンツ」に、また[出力ディレクトリにコピー]の値を「常にコピーする」に変更しておこう。
今回のLeap Motionアプリについて
今回のアプリは、3つのXMLファイルに記述した画像ファイルを読み込み、3D回転しているように表示させるアプリだ(次の画面を参照)。
[冬と私]ボタンをタッチすると、該当する画像が3D回転しながら表示される。回転表示されている任意の画像をタップすると、回転しながら大きく表示され、最後に小さくなって消滅する。
画面のレイアウト(MainWindow.xaml)
デフォルトで配置されているGridコントロールをCanvasコントロールに直しておく(座標値が取得しやすいため)。さらにCanvasコントロールのBackgroundプロパティに「Aqua」を指定しておく。
まずタイトルとなるTextBlockコントロールを1個配置する。
次に、回転する画像を表示させる「LayoutRoot」という名前のCanvasコントロールを配置する。
3つのボタンを配置し、名前に「Button1」~「Button3」と付けておく。それぞれのContentプロパティには「林」「冬と私」「林と冬」と付けておく。
3D回転している画像をタップした際にアニメ―ションを伴って表示される、「Image1」という名前のImageコントロールを1個配置しておく。このImage1コントロールは最初の状態では非表示にしておく。
一番前面に(つまり最後の行に)、「paintCanvas」という名前のInkPresenterを配置する。これはタッチ・ポイントを表示するためのものだ。
書き出される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="1080" Width="1920" WindowState="Maximized">
Title="MainWindow" Height="1080" Width="1920" WindowState="Maximized">
<Canvas Background="Aqua">
<TextBlock x:Name="TitleTextBlock" HorizontalAlignment="Left" Height="87" TextWrapping="Wrap" Text="画像の3D回転" VerticalAlignment="Top" Width="482" FontFamily="Meiryo UI" FontSize="72" FontWeight="Bold" Margin="27,7,0,0"/>
<Canvas x:Name="LayoutRoot" HorizontalAlignment="Left" Height="649" VerticalAlignment="Top" Width="1342" Canvas.Left="251" Canvas.Top="260"/>
<Button x:Name="Button1" Content="林" Height="81" Canvas.Left="577" Canvas.Top="10" Width="173" FontSize="36" FontWeight="Bold"/>
<Button x:Name="Button2" Content="冬と私" Height="81" Canvas.Left="785" Canvas.Top="7" Width="173" FontSize="36" FontWeight="Bold"/>
<Button x:Name="Button3" Content="林と冬" Height="81" Canvas.Left="993" Canvas.Top="7" Width="182" FontSize="36" FontWeight="Bold"/>
<Image x:Name="Image1" Height="15" Canvas.Left="629" Canvas.Top="260" Width="20" Visibility="Collapsed" RenderTransformOrigin="0.5,0.5"/>
<InkPresenter x:Name="paintCanvas"/>
</Canvas>
</Window>
|
Blendを起動してストーリーボードを作成
まずはBlednを起動する(起動手順は、第3回を参考にされたい)。なお、実際にBlendを起動する前には、必ずVS 2012でプロジェクトをビルドしておこう。
Blendが起動したら、[プロジェクトを開く]から、現在作成している「ImageRotation3D_LeapMotion.sln」ファイルを指定して開く。
Blendにおけるストーリーボードの作成はLeap Motionとは直接関係がないので、以降の解説は手短にさせていただく。
「Storybord1」というストーリーボードを作成する(作成手順は「MSDN: ストーリーボードの作成、変更、または削除」を参照されたい)。アートボード全体が赤い枠線で囲まれてストーリーボードの記録が可能になるので、[オブジェクトとタイムライン]で「Image1」を選択して、黄色の再生ヘッドが「0」の位置で、Image1のVisibilityプロパティの値を「Visible」に指定する。次に、再生ヘッドを「2」の位置に移動し、Widthプロパティに「640」、Heightプロパティに「480」と指定する。Image1コントロールの[プロパティ]ウィンドウの[変換]タブの[RenderTransform]内にある[回転]タブの[角度](Angle)に「1080」と指定する(次の画面を参照)。
次に再生ヘッドを3の位置に移動し、Widthプロパティに「20」、Heightプロパティに「15」と指定して、Visibilityプロパティの値に「Collapsed」を指定する。
これで、2秒かけて画像が回転しながら大きく表示され、1秒かけて画像が小さくなって消滅するアニメーションが作成される。
Blendを終わらせて、VS 2012に戻る。保存と適用のメッセージが出るが、保存して適用させる。
またストーリーボードのコードの下に、次のリストのコードが追加されているので、このコードは削除する。このコードを残したままにしておくと、アプリを実行した際に、即!ストーリーボードが実行されてしまう(次のリストを参照)。
<Window.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource Storyboard1}"/>
</EventTrigger>
</Window.Triggers>
|
レイアウト図は次の画面のようになる。
- 1Button1~Button3コントロールを配置。
- 2Image1コントロール(最初は非表示)。
- 3「LayoutRoot」という名前のCanvasコントロール。
- 4一番前面に「paintCanvas」というInkPresenterコントロールを配置する。
プログラムコード(MainWindow.xaml.vb)
では、次にプログラムコード(MainWindows.xaml.vbファイル)を見ていこう。
プログラムコードも、タッチ処理以外は第1回と基本的に同じ内容となるので、説明を割愛する。まずは第1回の「名前空間の読み込み」「メンバー変数の宣言」「MainWindow_Loadedメソッドの処理」「Updateメソッドの処理」の開発手順を参考にタッチ処理の前までを実装してほしい。相違点として、下記の10点を修正してほしい。
(1)タイマーを使用するため、System.Windows.Threading名前空間を読み込む。
(2)ストーリーボードを使用するため、System.Windows.Media.Animation名前空間も読み込んでおく
(3)メンバー変数の宣言にWin32 APIの以下のリストを追加する
' Win32 APIの宣言
Private Declare Function SetCursorPos Lib "user32" (x As Integer, y As Integer) As Boolean
Private Declare Function apimouse_event Lib "user32" Alias "mouse_event" (ByVal dwFlags As Int32, ByVal dx As Int32, ByVal dy As Int32, ByVal cButtons As Int32, ByVal dwExtraInfo As Int32) As Boolean
Private Const MOUSEEVENTF_LEFTDOWN = &H2
Const MOUSEEVENTF_LEFTUP = &H4
|
(4) 指定した時間の間隔で、指定した優先順位で処理されるタイマーを表す、新しい DispatcherTimerクラスのインスタンス「Private myTimer As New DispatcherTimer」メンバー変数を宣言する。
(5)Image型の新しいリストである「Private _myImage As New List(Of Image)」メンバー変数を宣言する。
(6)Imageクラスのメンバー変数「Private myImage As Image」を宣言する。
(7)文字列型の新しいリストである、「Private fileNameList As New List(Of String)」メンバー変数を宣言する。
(8)XML要素を表すXElementクラスのメンバー変数「Private xmldoc As XElement」を宣言する。
(9)3D回転スピードを表すメンバー変数「Private mySpeed As Double = 0」を宣言する。
(10)第1回のリスト3にある「Private Message As String」と「Private Index As Integer」いう行は削除する
MainWindow_Loadedメソッドの処理
AddHandlerステートメントで、構成ツリーのオブジェクトがレンダリングされる直前に発生する、CompositionTarget.RenderingイベントにUpdateイベントハンドラーを追加する
Ink.Strokeの外観を表す、DrawingAttributesのインスタンスtouchIndicatorの[Width]と[Height]プロパティに「15」を指定する。スタイラスの形状を指定するStylusTipプロパティにEllipseを指定して円形とする。Leap Motionのうえで指をかざすと、かざした指の本数に応じて15pxの円が表示される。
Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
' Updateイベントハンドラーを実行する
AddHandler CompositionTarget.Rendering, AddressOf Update
touchIndicator.Width = 15
touchIndicator.Height = 15
' Leap Motionのうえで指をかざすと、かざした指の本数に応じて15pxの
' 円が表示される。
touchIndicator.StylusTip = StylusTip.Ellipse
End Sub
|
Updateメソッドの処理(タッチ処理部分)
これについても第5回の「Updateメソッドの処理(タッチ処理部分)」と同じ説明になるので割愛する。ここでは第5回の「ホバー時の処理(MainWindow.xaml.vb)」と「タッチ時の処理(MainWindow.xaml.vb)」のリストのコードを実装すればよい。
続いて、それぞれのボタンがタッチされたときの処理を実装する。
[林]ボタンがタッチされたときの処理
画像のファイル名を格納するコレクション「fileNameList」の内容をクリアしておく。「forest.xml」を引数にReadXmlメソッド(後述)を実行する(次のコード)。
Private Sub Button1_Click(sender As Object, e As RoutedEventArgs) Handles Button1.Click
LayoutRoot.Children.Clear()
_myImage.Clear()
fileNameList.Clear()
no = 0
RemoveHandler myTimer.Tick, AddressOf Me.myTimer_Tick
' 「forest.xml」を引数にReadXmlメソッドを実行する
ReadXml("forest.xml")
End Sub
|
[冬と私]ボタンがタッチされたときの処理
「winter.xml」を引数にReadXmlメソッドを実行する。
Private Sub Button2_Click(sender As Object, e As RoutedEventArgs) Handles Button2.Click
……上記のButton1_Clickメソッドと同じ処理のため割愛……
' winter.xmlを引数にReadXmlメソッドを実行する
ReadXml("winter.xml")
End Sub
|
[林と冬]ボタンがタッチされたときの処理
「winter_forest.xml」を引数にReadXmlメソッドを実行する。
Private Sub Button2_Click(sender As Object, e As RoutedEventArgs) Handles Button2.Click
……上記のButton1_Clickメソッドと同じ処理のため割愛……
' 「winter_forest.xml」を引数にReadXmlメソッドを実行する
ReadXml("winter_forest.xml")
End Sub
|
ReadXmlメソッドの処理
各ボタンクリックで渡されたXMLファイルをXElement.Loadメソッドで読み込む。子孫要素<画像名>を選択するクエリ―を定義する。
<画像名>要素の個数をCountプロパティで取得して、メンバー変数「imageCount」に格納しておく。
繰り返し変数「result」で子孫要素<画像名>の値を取得しながら、以下の処理を繰り返す。
新しいImageのインスタンスmyImageオブジェクトを作成する。
そのWidthプロパティに「320」、Heightプロパティに「240」と指定する。
Sourceプロパティには、ソリューションエクスプローラーでプロジェクト内のImagesフォルダーにある画像を指定する。
Tagプロパティには「1」ずつ増加するメンバー変数「no」の値を指定する。どの画像がタッチされたかの判断に、このTagプロパティの値を使用する。
「LayoutRoot」というCanvasコントロールにmyImageオブジェクトを追加し、リストコレクションである「fileNameList」に<画像名>要素の内容テキストを追加していく。
AddHandlerステートメントで、myImageオブジェクトがタップされたときのイベントハンドラーを追加する。イベントハンドラー内では以下の処理を行う。
Tagプロパティの値を取得して、変数「myTag」に格納する。
Image1コントロールのSourceプロパティにリストコレクションが保持している画像名で、変数「myTag」に該当する画像を指定する。
Storyboardクラスの新しいインスタンス「myStrbオブジェクト」を作成する。
FindResourceメソッドで「Storyboard1」という名前のストーリーボードを取得する。
Beginメソッドでストーリーボードを実行する。
変数「myImage」と「no」を引数に、ImagePositionメソッドを実行する。
タイマーの間隔を1ミリ秒に設定する。
タイマー間隔が経過すると発生するTickイベントにイベントハンドラー(myTimer_Tickメソッド)を指定し、Startメソッドでタイマーを開始する。
具体的なコードは次のとおり。
Private Sub ReadXml(xmlFilename As String)
xmldoc = XElement.Load(xmlFilename)
Dim query = From c In xmldoc.Descendants("画像名") Select c
imageCount = query.Count
' 読み込んだXMLファイルに記載されている数のImageオブジェクトを作成する
For Each result In query
myImage = New Image
With myImage
.Width = 320
.Height = 240
.Stretch = Stretch.Uniform
.Source = New BitmapImage(New Uri("Images/" & result.Value, UriKind.Relative))
.Tag = no.ToString
End With
LayoutRoot.Children.Add(myImage)
fileNameList.Add(result.Value)
' 3D回転をしている任意の画像をタッチしたときの処理
AddHandler myImage.MouseLeftButtonDown, Sub(mySender As Object, myArgs As MouseButtonEventArgs)
Dim myTag = DirectCast(mySender, Image).Tag
Image1.Source = New BitmapImage(New Uri("Images/" & fileNameList(CInt(myTag)), UriKind.Relative))
' ストーリーボードを実行する
Dim myStrb As New Storyboard
myStrb = TryCast(Me.FindResource("Storyboard1"), Storyboard)
myStrb.Begin()
End Sub
_myImage.Add(myImage)
no += 1
' ImagePositionメソッドを実行する
ImagePosition(myImage, no)
Next
mySpeed = 0
' タイマーを開始する
myTimer.Interval = New TimeSpan(10000)
AddHandler myTimer.Tick, AddressOf Me.myTimer_Tick
myTimer.Start()
End Sub
|
myTimer_Tickイベント(=タイマー間隔が経過すると発生するイベント)の処理
ここからは、処理が少し複雑になるので、直接コードの中に詳細な解説を追加している。
Private Sub myTimer_Tick(ByVal sender As Object, ByVal e As Object)
' 変数「_myImage」は、List(Of Image)型であるため、その値の範囲は「0」~「各XMLファイルに記載されている画像ファイルの個数」になる。
' 変数「no」に準じた、1から画像名の個数では、インデックスの範囲外エラーになるので注意。
For i As Integer = 0 To imageCount - 1
' 画像がタイマーの1刻みで移動したとき、移動先の画像の位置が、次の移動元になる。
' 変数「mySpeed」の値を増加させながら処理を繰り返し、画像を移動し続けているように見せる。
Dim myImage2 As Image = _myImage(i)
ImagePosition(myImage2, i)
Next
mySpeed = mySpeed + 1
End Sub
|
ImagePositionメソッドの処理
このプログラムの肝となる処理だ(次のコード)。
Private Sub ImagePosition(ByVal image As Image, ByVal myIndex As Integer)
' ■描画位置と軌道の指定
' 円弧の長径(本サンプルでは水平)を指定する。
Dim RadiusWidth As Integer = 650
' 円弧の短径(本サンプルでは垂直)を指定する。
Dim RadiusHeight As Integer = 150
' 円弧の左マージン、上マージンを指定する。
' この段階で調整するのではなく、MainPage_Loadedメソッド内でThicknessプロパティの値を指定して調整してもよい。
Dim RadiusLeftMargin As Integer = 100
Dim RadiusTopMargin As Integer = 100
' ■移動量の計算
' タイマーの1刻みで動く量を、速度として指定する。除算する値を小さくすると速く動く。
Dim spinAngle As Double = mySpeed / 120
' 何ラジアン分を表示させるか指定する。ここでは「2」ラジアンを指定して360度回転させる(例:180度なら「1」を、270度なら「1.5」を指定する)。
Dim displayRad As Double = 2
' タイマーの1刻みで動く角度を求める。
' 回転角度の360度(2π)を画像枚数で分割した各インデックスの位置から、タイマーに合わせて設定した値を順次追加して画像を移動させる。
' 整数値の「myIndex」はDouble型に変換しておく。Math.PIは円周率を表す。
Dim myAngle As Double = spinAngle + CDbl(myIndex) / imageCount * displayRad * Math.PI
' ■画像の重なり方の指定
'「Math.Sin(myAngle)」の値が大きいほど、画像は画面上の下方に位置する。
' これを利用して、ZIndexと「Math.Sin(myAngle)」の値を比例させ、手前の画像が最前面に表示されるようにする。
' ZIndexプロパティには整数値しか指定できないので型変換する必要がある。
image.SetValue(Canvas.ZIndexProperty, CInt(Math.Sin(myAngle)))
' ■拡大・縮小率の計算と指定
Dim myScaleTransform As New ScaleTransform
' 角度に応じた画像の高さの拡大縮小率を加算し、画像の移動に合わせてサイズを変化させる。
' ただし、画像サイズが0になると、クリックが困難であるため、最低限の画像サイズを確保しておく。
' ここでは角度θ=π/4radの場合を最低限の縮小率としている。
' 画像を常に同じサイズで表示したい場合は、この値を固定するとよい。
Dim myScale As Double
If Math.Sin(myAngle) >= -0.707 Then
myScale = 1 + Math.Sin(myAngle)
Else
myScale = 0.293
End If
' 画像の横幅の拡大縮小率は、高さの拡大縮小率と同じにして、画像の縦横比を固定する。
myScaleTransform.ScaleY = myScale
myScaleTransform.ScaleX = myScale
' 拡大・縮小率に応じて、画像を変形させる。
image.RenderTransform = myScaleTransform
' ■移動先の座標値の計算と指定
' 拡大縮小率を乗算して画像の幅を求め、その中心点から、だ円の中心点までの距離を求める。
' 左マージンを加算した値を、画像の中心点のX座標値としてセットする。
' CDbl(image.GetValue(FrameworkElement.ActualWidthProperty))については、画像のサイズ(このサンプルでは150px)を直接数値で指定しても構わない。
image.SetValue(Canvas.LeftProperty, RadiusLeftMargin + RadiusWidth + Math.Cos(myAngle) * RadiusWidth - CDbl(image.GetValue(FrameworkElement.ActualWidthProperty)) / 2 * myScale)
' 拡大・縮小率を乗算した画像の高さを求め、その中心から、だ円の中心点までの距離を求める。
' 上マージンを加算した値を、画像の中心点のY座標値としてセットする。
image.SetValue(Canvas.TopProperty, RadiusTopMargin + RadiusHeight + Math.Sin(myAngle) * RadiusHeight - CDbl(image.GetValue(FrameworkElement.ActualHeightProperty)) / 2 * myScale)
End Sub
|
このサンプルのコードは下記よりダウンロードできる*1。
- *1 サンプルをダウンロードして動かす場合は、「LeapCSharp.NET4.0.dll」や「LeapCSharp.dll」、「Leap.dll」を読者自身のフォルダー内にあるDLLファイルに指定し直さなければ動かない可能性があるので、動かない場合は再指定していただきたい。
■
今回はこれで終わりだ。今回もちょっと長めの解説になり、また3D回転という、少々面倒な処理を解説した。実は、このサンプルは実際には「3D」ではなく、単に遠近法を使ったアプリだ。3Dの知識がなくても、遠近法を用いると、いかにも3Dらしく見えるのが、楽しい。
実際にサンプルをダウンロードして動かしてみていただければ、このアプリが遠近法を用いたアプリであることが分かるだろう。
では、また次回の記事でお会いしよう。
※以下では、本稿の前後を合わせて5回分(第5回~第9回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
5. Leap MotionでBing Mapsを扱う
リスト内に表示された住所項目をLeap Motionによりタッチすることで、Web上のサービス「Bing Maps」での地図検索を行うサンプル・アプリを作ってみよう。