Help us understand the problem. What is going on with this article?

Leap MotionアプリをSiv3Dで開発

Siv3D Advent Calendar 2017 14日目の記事です。
Leap Motionで遊んでみます。

はじめに

そもそもLeap Motionって?

5147lYwry1L.jpg

「素手で仮想世界に手を伸ばしましょう」「大きなアイデアが詰まった、小さなデバイス」をキャッチコピーとし、画像のように小さなデバイスで簡単に手首から指の先まで認識してくれる優れものです。
少々値段はしますが、自宅で簡単にパソコン一つで仮想世界体験+開発ができると考えれば十二分に買う価値ありでしょう。
詳しくは下記リンクを参考にしてください

公式ホームページ:https://www.leapmotion.com
Amazon:https://www.amazon.co.jp/dp/B00GWCATS8

そして、このLeap Motionですが、Siv3Dで開発できます。というわけで、今回はこのLeap Motionを使って遊んでみたいと思います。

環境

Siv3D(August2016v2)
Visual Studio 2015 Community
Windows 10

Leap Motion SDKを導入する

下記URLからLeap Motion SDKダウンロードしてください。
https://www.leapmotion.com/setup/desktop/windows

ダウンロードしたものを起動して、道なりに進んでいけば問題なくインストールできると思います

Siv3D側の設定

Siv3Dのプロジェクトを立ち上げます。

[ プロジェクト ] > [ プロジェクト名 のプロパティ ]からプロパティページを開き、
[構成]を[すべての構成]に変更

[構成プロパティ] > [C/C++] > [全般] > [追加のインクルード ディレクトリ] に
LeapSDKの中にあるincludeまでのパスを追加

C:\Leap\LeapSDK\include

同じように [構成プロパティ] > [リンカー] > [全般] > [追加のライブラリ ディレクトリ] に
LeapSDKの中にある lib/x86 もしくは lib/x64 をプロジェクトのターゲットプラットフォームに応じて追加

C:\Leap\LeapSDK\lib\x86

最後に、実行ファイルのカレントディレクトリにLeapMotionSDKの中にある Leap.dll を配置したら終了です。

 
制作者のSuzuki Ryoさんがリファレンスページにてサンプルプログラムを公開しているので、試しに動かしてみましょう!
https://github.com/Siv3D/Reference-JP/wiki/Leap-Motion

Leap Motionに関するクラスについて

見栄えの良い書き方が思いつかなかったので、コメントいっぱいのソースコードを置いてみます。説明の便宜上、空の処理や明らかにバグる処理もありますが、悪しからず。

Sample.cpp
# include <Siv3D.hpp>
# include <Siv3DAddon/LeapMotion.hpp>

void Main()
{
    // Leap Motionデバイスの追加
    // これがないと始まらない
    LeapMotion::RegisterAddon();

    while (System::Update())
    {
        // 読み取った手
        // 0が左手、1が右手というわけではなく、差し出した順に添字が振られる
        LeapMotion::Hands()[0];
        LeapMotion::Hands()[1];

        // 手の数だけ拡張for文を回す
        for (const auto& hand : LeapMotion::Hands())
        {
            // 親指:0 ~ 小指:4
            // もしくはLeapMotion::FingerTypeに親指から小指までenumで定義されています
            hand.fingers[LeapMotion::FingerType::Thumb];

            // 指の数だけ拡張for文を回す
            for (const auto& finger : hand.fingers)
            {
                // 各関節(5点)
                // 数字が小さいほど手前側
                finger.joints[0];

                for (const auto& joint : finger.joints)
                {
                    // jointはVec3で定義されているのであとは好きなように
                }
            }
        }
    }
}

 
また、Leap Motionには円を描くジェスチャーやピアノの鍵盤をたたくようなジェスチャーを読み取る機能もありますが、それについては 先程紹介したリファレンスページのサンプル を参照されるのが早いと思います。

さらに、LeapMotion.hppを見てもらえばわかると思いますが、ここで紹介したもの以外にも、なにやら使えそうなクラス・メソッドがありますので、参考にしてみてください。Leap Motion関連については比較的コメントも多いので分かりやすいと思います。

キラキラ光るポインタ + 打ち上げ花火を作ってみよう!

jaoigs.gif

こんな感じのプログラムを作ってみようと思います。
手と連動して表示されるキラキラの色は、人差し指先端のz座標によって定めています。

なお、Leap Motionに加えてタスクシステムも利用して作ります。
また、Siv3D.hpp , Siv3DAddon/LeapMotion.hpp , rnfs.hstdafx.h にまとめて 必ず使用されるインクルード ファイル に指定しています。その設定方法は 『 Siv3D プリコンパイル済みヘッダー 』などと検索すれば出てくるので割愛します。

キラキラ光るカーソルを作る

とりあえずMain.cppを示します

Main.cpp
# include "Particle2D.h"

void Main()
{
    Window::Resize(1024, 640);

    LeapMotion::RegisterAddon();

    while (System::Update())
    {
        for (const auto& hand : LeapMotion::Hands())
        {
            Creates<Particle2D>(4, hand.fingers[LeapMotion::FingerType::Index].joints[4]);
        }

        TaskCall::All::Update();
        Task::All::Update();
    }
    Task::All::Clear();
}

リープモーション使うのにデフォルトのウィンドウサイズだと少し小さいので適度にリサイズします。

拡張for文で手の数だけループ回し、手の人差し指の先端からParticle2Dを4つ作ります。
それでは、Particle2Dの実装をしていきます。

Particle2D.h
# pragma once

class Particle2D :
    public Task
{
public:
    Particle2D() = default;
    Particle2D(const Vec3&);

private:
    Vec2 m_pos;
    const double m_radius;
    Color m_color;
    int32 m_alpha;
    Vec2 m_vec;
    const double m_speed;

    TaskCall m_update;

    void update();
};

Particle.cpp
#include "Particle2D.h"

Particle2D::Particle2D(const Vec3& joint)
    : Task()
    , m_radius(1.3)
    , m_color(HSV(joint.z, 100, 1.0))
    , m_alpha(255)
    , m_vec(RandomVec2())
    , m_speed(Random(0.3, 1.0))
    , m_update(this, &Particle2D::update)
{
    // x, y座標を調整
    double x = Window::Center().x + joint.x * 2.0;
    double y = Window::Height() - joint.y * 2.0 + 200;
    m_pos = Vec2(x, y);
}

void Particle2D::update()
{
    const Circle(m_pos, m_radius).draw(Color(m_color, m_alpha));

    m_pos += m_vec * m_speed;
    m_alpha -= 4;
    if (m_alpha <= 0) this->Destroy();
}

「x, y座標を調整」 については、そのままjoint.xy()の値を使用してしまうと、(ユーザー的に)中心だと思われる場所が ( 0, 0 ) 座標になり、また、手を上に上げるとyの値が加算されて、カーソルは下にいくという事態になりますので、こういった処理を施しています。
2倍しているのは単に移動速度を上げるためです。

※Vec3 ToScreenPos(const Vec3& worldPos)というその時のカメラの設定を使って、ワールド座標をスクリーン座標に変換する便利な関数も用意されています。こちらを利用してもいいと思います。

花火を打ち上げる

花火を実装します。
ピアノの鍵盤をたたくようなジェスチャーをするとランダムな位置から花火が打ち上がるようにします。

Main.cpp(変更)
# include "FireWorks.h"
# include "Particle2D.h"

void Main()
{
    Window::Resize(1024, 640);

    LeapMotion::RegisterAddon();
    LeapMotion::EnableGesture(LeapMotion::GestureType::KeyTap);

    while (System::Update())
    {
        for (const auto& gesture : LeapMotion::Gestures())
        {
            if (gesture.type == LeapMotion::GestureType::KeyTap)
            {
                Create<Fireworks>();
            }
        }

        for (const auto& hand : LeapMotion::Hands())
        {
            Creates<Particle2D>(4, hand.fingers[LeapMotion::FingerType::Index].joints[4]);
        }

        TaskCall::All::Update();
        Task::All::Update();
    }
    Task::All::Clear();
}

ジェスチャーに関する処理を追加しました。
それではFireworks及びその中で使うSparkの実装をしていきます。

FireWorks.h
# pragma once

class Fireworks :
    public Task
{
public:
    Fireworks();
    ~Fireworks();

private:
    const double m_speed;
    Vec2 m_pos;
    const double m_radius;
    Color m_color;

    TaskCall m_update;

    void update();
};

Fireworks.cpp
#include "Fireworks.h"
#include "Spark.h"

Fireworks::Fireworks()
    : Task(Random(30, 90))
    , m_speed(5)
    , m_pos(Random(0, Window::Width()), Window::Height())
    , m_radius(5.0)
    , m_color(RandomColor())
    , m_update(this, &Fireworks::update)
{
}


Fireworks::~Fireworks()
{
    Creates<Spark>(100, m_pos, m_color);
}

void Fireworks::update()
{
    m_pos.y -= m_speed;
    const Circle(m_pos, m_radius).draw(m_color);
}

Spark.h
# pragma once

class Spark 
    : public Task
{
public:
    Spark() = default;
    Spark(const Vec2&, const Color&);

private:
    Vec2 m_pos;
    const double m_radius;
    Vec2 m_vec;
    double m_speed;
    Color m_color;

    TaskCall m_update;

    void update();
};

Spark.cpp
# include "Spark.h"

Spark::Spark(const Vec2 & pos, const Color & color)
    : Task()
    , m_pos(pos)
    , m_radius(2.0)
    , m_vec(RandomVec2())
    , m_speed(Random(5.0, 20.0)), m_color(color)
    , m_update(this, &Spark::update)
{
}

void Spark::update()
{
    if (--m_speed <= 0.0) this->Destroy();
    m_pos += m_vec * m_speed;

    Circle(m_pos, m_radius).draw(m_color);
}

さて、これでプログラムの完成です。
手を動かすときれいなパーティクルが描画され、鍵盤をタップするようなジェスチャーをすると花火が打ち上がると思います。

当たり判定

とってつけたような感じにはなりますが、リファレンスページのサンプルプログラムをすこし改造して、図形と指の各関節との当たり判定を追加したものを示したいと思います。

Sample改.cpp
# include <Siv3D.hpp>
# include <Siv3DAddon/LeapMotion.hpp>

void Main()
{
    Window::Resize(1024, 640);

    LeapMotion::RegisterAddon();

    const Font font(36), font2(18);

    const Box box(4);
    bool temp = false;

    while (System::Update())
    {
        Graphics3D::FreeCamera();

        for (const auto& hand : LeapMotion::Hands())
        {
            const Vec3 scaledPos = hand.pos*0.05;

            Sphere(scaledPos, 0.5).draw(HSV(hand.id * 30).toColor());

            const Vec2 screenBase = Graphics3D::ToScreenPos(scaledPos).xy();

            font(hand.isLeft ? L"Left" : L"Right").drawCenter(screenBase.movedBy(20, -160));

            const Circle pinchCircle(screenBase.movedBy(-60, -260), 60);

            const Circle grabCircle(screenBase.movedBy(100, -260), 60);

            pinchCircle.draw(HSV(hand.id * 30).toColor(60));

            pinchCircle.drawPie(0.0, hand.pinchStrength * TwoPi, HSV(hand.id * 30));

            grabCircle.draw(HSV(hand.id * 30).toColor(60));

            grabCircle.drawPie(0.0, hand.grabStrength * TwoPi, HSV(hand.id * 30));

            font2(L"Pinch").drawCenter(pinchCircle.center);

            font2(L"Grab").drawCenter(grabCircle.center);

            for (const auto& finger : hand.fingers)
            {
                for (const auto& joint : finger.joints)
                {
                    Sphere(joint*0.05, 0.2).draw(HSV(finger.id * 10).toColor());

                    if (box.intersects(joint * 0.05))
                    {
                        temp = true;
                    }
                }

                const int32 begin = (finger.type == LeapMotion::FingerType::Thumb)
                    || (finger.type == LeapMotion::FingerType::Pinky) ? 0 : 1;

                for (int32 i = begin; i < 4; ++i)
                {
                    Cylinder(finger.joints[i] * 0.05, finger.joints[i + 1] * 0.05, 0.1).draw();
                }
            }

            for (auto i : step(Min(4, static_cast<int32>(hand.fingers.size()))))
            {
                Cylinder(hand.fingers[i].joints[1] * 0.05, hand.fingers[i + 1].joints[1] * 0.05, 0.1).draw();
            }

            Cylinder(hand.fingers[0].joints[0] * 0.05, hand.fingers[hand.fingers.size() - 1].joints[0] * 0.05, 0.1).draw();
        }

        box.draw(temp ? Palette::Yellow : Palette::White);
        temp = false;
    }
}

配布

Leap Motionを使ったアプリケーションの配布には

  • アプリケーションの実行ファイル (.exe)
  • dll 以外を削除した Engine フォルダ
  • アプリケーションが使う画像やサウンドなどのファイル(.exe に埋め込んでいなければ)
  • (必要であれば)ユーザーに読んでもらうマニュアルや README などのドキュメント
  • Leap.dll

が必要です。

トラブルシューティング

リープモーションが反応しない!

管理者権限コマンドプロンプトから以下コマンドを実行
net start LeapService

あるいはドライバの再インストールも良いかもしれません

C:\Program Files (x86)\Leap Motion\Core Services\Drivers\dpinst64.exe

終わりに

本記事の内容を試すには「Leap Motionを購入する」という一つハードルがありますが、この記事をきっかけに購入を検討をしていただければと思います。楽しいゲーム・アプリも配信されているので是非。

そして、手全体を読み取ることができるわけなので、その特性を発揮しまくったアプリがSiv3Dで開発されればと思います。

明日は @peterpanppp さんの記事です。よろしくお願いします。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away