shimada-kの日記

ソフトウェア・エンジニアのブログです

UnityでS.S.デッドリー・ボンバーを作ってみる

人造人間13号*1が放つS.S.デッドリーボンバーをUnityで再現してみました。人造人間13号はドクター・ゲロが使っていたマシンが悟空抹殺のために自動で作りだした人造人間です。

人造人間13号

構造は、まず真ん中のコアのような球体と、その外側に赤い帯が表示されている球体、さらに一番外側には透明な球体の3つのsphereオブジェクトを作成しました。赤い帯のような模様はテクスチャで表現しました。

透過テクスチャなのでサーフェスシェーダーでマルチパスレンダリングを行います。シェーダーのコードはこちらを参考にしました。

1番外側のsphereには6個のparticle systemを子オブジェクトに設定してあります。原作のようにサイズ差のあるパーティクルにするためにstartSizeをCurveで変更してあります。

6個のparticle systemのうち4個をスクリプト上で180フレームごとにランダムに出し分けています。

using UnityEngine;
using System.Collections;

public class SSDBOuterController : MonoBehaviour {

    public GameObject[] particle_lines;

    private System.Random r;
    private int[] particle_status = new int[6];
    private int particle_interval = 0;

    // Use this for initialization
    void Start () {

        r = new System.Random(1000);

        for(int i = 0; i < particle_lines.Length; i++){
            particle_lines[i].particleSystem.Pause();
        }

    }
    
    // Update is called once per frame
    void Update () {

        if(particle_interval < 180){
            gameObject.transform.Rotate (new Vector3((float)r.Next (2, 8), (float)r.Next (2, 8), (float)r.Next(2,8)));
            particle_interval++;
        }
        else{
            // 一旦すべて表示状態にする
            for(int i = 0; i < particle_status.Length; i++){
                particle_lines[i].particleSystem.renderer.enabled = true;
                particle_status[i] = 0;
            }

            int counter = 0;

            // ランダムで1/3を選んで消す
            while(counter < (particle_lines.Length / 3)){
                int index = r.Next(0, particle_lines.Length - 1);
                if(particle_status[index] == 0){
                    particle_status[index] = 1;
                    particle_lines[index].renderer.enabled = false;
                    counter++;
                    //Debug.Log (index);
                }
            }
            particle_interval = 0;
        }
    }
}

赤い帯の回転と周りの光の筋の回転が同期していないのが原作において仕様であることと、particle systemを固定座標で回転させると見た目がワンパターンになってしまうのでこのような実装にしました。

さらに原作では光の筋の長さが一定ではないため、particle systemの設定を6本のうち2本だけ変えて2倍の長さにしてあります。

f:id:shimada-k:20140831011614p:plain

DEMO

赤い帯のsphereと真ん中のコアの周りにただよう拡散光はHaloで表現しました。

ついでにskyboxを設定しました。skyboxのテクスチャはImport Packageで取り込まれるデフォルトのテクスチャです。

私はドラゴンボールに出てくるエネルギー波の中で、人造人間13号が放つS.S.デッドリーボンバーとピッコロの魔貫光殺砲の2つが単体でのビジュアルに最も富んでいるものだと思っています。

コードと素材一式はgithubに置いておきました。

この惑星(ほし)の半分を吹き飛ばすほどのパワーがあるのだぞ!!!

Synthを試してみる

先日東京Node学園14時限目に参加してきました。そこで@pchwさんから紹介があったSynthを試してみました。試してみた分かったことを書いておきます。

SynthはSPA作成を念頭において設計されたWebフレームワークです。

Angular.jsでサイトを作成した場合、HTMLの基本構造だけロードして中のデータは後からレンダリングされます。それだと検索エンジンフレンドリーではないというのと、APIロード完了までに空のdivやtableが表示されてしまってしまうという課題があったので、その課題に対するアプローチの1つということです。

Synthを使用しない(Angular.jsだけで実装した)場合と使用した場合のソースをさらしておきます。ちょくちょく出てくるRGBというプリフィックスやgridといったワードはプライベートプロジェクト由来のものなので特に他意はありません。

Synthを使用しない場合

テンプレート

<html ng-app>
    <head>
        <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
        <script src="angular.min.js"></script>
        <script src="05.js"></script>
    </head>
    <body ng-controller="RgbController">
        <table class="table">
            <tr>
                <th>Id</th>
                <th>Name</th>
                <th>Platform</th>
                <th>Comment</th>
                <th>IsCold</th>
            </tr>
            <tr ng-repeat="rgb in rgbjson">
                <td>{{rgb.id}}</td>
                <td>{{rgb.name}}</td>
                <td>{{rgb.platform}}</td>
                <td>{{rgb.comment}}</td>
                <td>{{rgb.is_cold}}</td>
            </tr>
        </table>
    </body>
</html>

コントローラー側です。

function RgbController($scope, $http, $timeout) {
    var uri = 'http://192.168.3.2/rgb/rgb.json';
    var param = {num: 200};

    $http.get(uri, param)
         .success(function(data, status, headers, config) {
             log('success', data, status, headers, config);
         })
         .error(function(data, status, headers, config) {
             log('error', data, status, headers, config);
         });

    // jsonを表示する
    function log(type, data, status, headers, config) {
        // delay
        $timeout(function(){
                    $scope.rgbjson = data;
                    }, 5000);
        //$scope.rgbjson = data;
    }
}

Synthを使用した場合

サーバ側。チュートリアルのコードをベースにしています。Promiseオブジェクトを返す必要があるのでmysql-promiseをnpm installしました。

back/resources/rgb/getRgb.js

exports.getIndex = function (req, res) {

    req.db.configure({
        "host": "localhost",
        "user": "xxxx",
        "password": "xxxxxxxx",
        "database": "rgb"
    }); 

    return req.db.query('SELECT * FROM grid_image').then(function (rows) {
        console.log(rows[0]);
        return { data:rows };
    }); 
};

クライアント側。コントローラーから

front/controller/rgb.js

angular.module('my_app')
.controller('rgbController', function ($scope, $http, data) {
    //console.log(data.data[0]);
    $scope.rgb_entries = data.data[0];
});

続いてビュー

front/html/rgb/getIndex.jade

ul.rgb-timeline
    li.grid(ng-repeat="entry in rgb_entries").content {{entry.id}} {{entry.name}} {{entry.comment}}

総括

私はVirtualBox仮想マシン上に環境を作りましたが、ファイルサイズがデフォルトの8GBだとチュートリアルで使用するmongodbにデータを作るgenerateTeweet.jsがディスク容量不足で失敗しました。VMで試す場合はディスクサイズを余裕を持った値にしておくと詰まずに済みます。ちなみにVMのファイルサイズの変更はこちらを参考に行いました。

preloadを試すときはAPIの返り値をPromiseオブジェクトにしないといけませんが、現時点(2014年8月24日)で公式サイトで提供しているNodeはv0.10なのでPromiseをサポートしていません。

ならばと思いgithubのmasterブランチにのっているものをビルドすると-preが付いているバージョンはnode-sassモジュールをインストールできないと怒られ、Synthサーバを起動できませんでした。Promiseが公式にサポートされる(であろう)Node v0.12のリリースを待ちましょう。

Synthサーバを立ち上げている時にサーバ側のコードを変更すると変更を検知して自動でサーバを再起動してくれるのはうれしいです。

普段Expressを使っていて、ライアントサイドとサーバサイドのコードがごっちゃになっているのはいかがなものかと思っていたのでfront/とback/でディレクトリが明確に別れている構成は好感が持てます。

Firefoxの新機能Loopを試す

WebRTC Meetup Tokyo #3に参加してきました。@chikoskiさんから紹介があったFirefoxの新機能のLoopを試してみました。

LoopはWebRTCを使用して実装されたビデオチャットサービスで、Firefoxのアルファ版のAuroraで試すことができます。

f:id:shimada-k:20140803072409p:plain

基本的な使い方はGoogle+ハングアウトと似ていて、生成されたURLを相手と共有してチャット開始といった感じです。

f:id:shimada-k:20140803072534p:plain

Firefox Auroraはメニューに電話アイコンがあるのでそのアイコンをクリックするとURLが生成されます。

f:id:shimada-k:20140803072916p:plain

生成されたURLを別のブラウザで開くとチャットの画面が表示されるのでボタンをクリックするとWebRTC特有のデバイスアクセスの許可を求めるポップアップが表示されるので許可するとビデオチャットが開始されます。

LoopはGoogle+ハングアウトとは違い、ブラウザの組み込み機能として実装されています。

FirefoxOSで動くアプリと連携できるようにそういう設計にしたというのと、Web-APIの拡充を啓蒙したいというMozillaさんのもくろみがあるのかもしれません。

とはいえ、デスクトップアプリとしても動作していて、ブラウザからウィンドウを切り離してSkypeのように使用することもできます(この辺はGoogle+ハングアウトも同様ですが、、)。

f:id:shimada-k:20140803074429p:plain

アルファ版ということもあってまだ機能はシンプルですがWebRTCを使ったサービスが増えていくのはいいと思います。あとはSafariIEでもWebRTCがちゃんと動くようになればもっといいですね!

UnityでLeap Motionが認識した手を動かす

UnityでLeap Motionのビジュアライザーのように認識した手を動かしてみました。手は以下のオブジェクトで表現しました。

  • 手首:Cube
  • 手のひら:Sphere
  • 間接:Sphere
  • 骨:Line Renderer

Leap MotionSDKのバージョンはもちろんv2 Betaです。Unity側スクリプト言語C#を使いました。ソース一式はgithubにあげてあります。

f:id:shimada-k:20140726225215p:plain

以下ハマった点

1つめ

正規化した座標をUnityのオブジェクトに適応させるときにlocalPositionに代入していましたが、それだと間接の座標だけ他と位置が離れてしまって、パーツの配置に違和感が生じていました。positionに代入しないといけませんでした。

unityWrist.transform.position = utils.getScaledUnityPosition(hand.WristPosition,
                                                                interactionBox);

utils.getScaledUnityPositionは自作dllの関数です。

2つめ

手首のCubuオブジェクトの回転は直近フレームを保存して、RotationAngleでxyzの軸別に差分角度を取得してtransform.Rotateで動かす方法をとっています。

Unityでオブジェクトを回転させる時はtransform.rotationで絶対値を代入するとか、Quaternion.Slerpを使うとかいくつか方法があるようですが、Leap MotionSDKのAPIと相性がいい方法をチョイスしました。

Hand.RotationAngleで直近フレームとの差分の角度を取得するんですが、こいつがラジアン角で、Unityのtransform.Rotateで回転させるためにはオイラー角である必要があります。

なのでラジアンオイラーに変換する必要があったんですが、そこに気づくのに半日かかりました。

float angleX = hand.RotationAngle(basisFrame, basis.xBasis);
float angleY = hand.RotationAngle(basisFrame, basis.yBasis);
float angleZ = hand.RotationAngle(basisFrame, basis.zBasis);

// 回転する
unityWrist.transform.Rotate(new Vector3(
                                LMUtils.rot2Dir(angleX),
                                LMUtils.rot2Dir(angleY),
                                -1 * (LMUtils.rot2Dir(angleZ))));

LMUtils.rot2Dirは自作dllの関数です。z方向に-1をかけているのは認識した手の向きを変えるためです(positionも反転させています)。

動作中のスクリーンをキャプチャしました。Line RendererがPalmSphereを貫通してますがそこは気にせず。(Leap Motionのビジュアライザーだと第3間接のSphereを横につなげて見た目を整えてますが、、)。

両手対応してます。ソースは1つにしたかったので、Unityのオブジェクトにタグをふっておいて、スクリプト側ではHandListでforeachしてタグとisLeftやisRightの結果を比較して描画し分けています。

Line Rendererで骨を表現するとlengthを気にしなくていいので楽です。あと親指は他の指より間接が一つ少ない(というかlengthが0の骨がある)ことを考慮する必要あります。

ベースはできたのでこれからUnityとLeap Motionを遊び尽くす。

httperfでnginxにおける非同期処理の限界を知る

nginxに負荷をかけて遊んでみました。webによくあがっているnginxのベンチマークは適当な静的ファイルを1つ用意してそこに対してアクセスを行うといったものです。

ただそれだとメインメモリのファイルキャッシュにのっているものを返すだけで、オンメモリで完結する処理のはずなので非同期処理が売りのnginxのベンチマークとしては微妙じゃないかなと思うわけです。

そこでメインメモリのキャッシュではまかなえない量のデータを用意してかつ複数ファイルに対してアクセスを行うようにして負荷をかけてみました。負荷をかけるツールはhttperfを使用しました。

ネットワークの帯域がボトルネックになっていないことを確認するために、事前にiperfで帯域を調査しておきます。自宅のLANと使用するシステムはすべて1000BASE-Tで接続されているので理論値では1Gbits/sの速度がでるはずです。

------------------------------------------------------------
Client connecting to 192.168.3.3, TCP port 5001
TCP window size: 23.5 KByte (default)
------------------------------------------------------------
[  3] local 192.168.3.8 port 42975 connected with 192.168.3.3 port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-10.0 sec  1.09 GBytes   936 Mbits/sec

まあ実効値はこんなもんです。ネットワーク帯域はフルに使用可能なことが分かったのでOSの設定をしてopen(2)できるファイルディスクリプタの上限値を引き上げます。

まずはユーザプログラムが開くことができるファイルの最大数を引き上げるためlimit.confを変更します。

#ftp             hard    nproc           0
#ftp             -       chroot          /ftp
#@student        -       maxlogins       4

*               soft    nofile          1048560
*               hard    nofile          1048560

再起動してulimit -nで確認します。

shimada-k@backend:~$ ulimit -n
1048560

これでOS側の設定値は変更しました。今度はhttperf側で開くことができるファイル数を変更します。httperfがコードから参照しているFD_SETSIZEの値を変えてビルドしなおさないといけません。

これは/usr/include以下のヘッダファイルを書き換えることで対応します。OS側に設定した値と同一のものを設定しました。

/usr/include/linux/posix_types.h

#define __FD_SETSIZE    1048560

/usr/include/x86_64-linux-gnu/bits/typesizes.h

#define  __FD_SETSIZE       1048560

これと同様の手順をnginxを動かすサーバ上でも行います。httperfに対してはポートの再利用の設定をソケットに対して行うコードを追加してビルドします。

core.cの941行目付近です。

int on = 1; 
setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, (const char *)&on, sizeof(on));
SYSCALL (BIND,
    result = bind (sd, (struct sockaddr *)&myaddr,
        sizeof (myaddr)));
    if (result == 0)
        break;

これでhttperfのソースをDLしてビルドします。nginxもビルドします。ここまでで限界突破されたhttperfとnginxができますので、open(2)できるファイルディスクリプタの上限値がボトルネックになる可能性はなくなりました。

さらにhttperf側でTCPポートの再利用の設定とTCPのTIME_WAIT対策としてWAIT時間の設定をします。LAN内なので短めに設定してます。これをしないとhttperf側で使用できるポート番号の数がボトルネックとなり、bind(2)がエラーになります。 /proc/sys/net/ipv4/以下のファイルに数値を書き込むことで対応します。

echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
echo 10 > /proc/sys/net/ipv4/tcp_fin_timeout

加えてnginxの設定でworker_connectionsを引き上げておきます。デフォルトでは1024ですが4096にあげておきます。

events {
    worker_connections  4096;
}

実験環境を書いておきます。

クライアント側

  • OS:Debian Wheezy (3.2.0-4-amd64)
  • Core(TM) i7-2600 CPU @ 3.40GHz
  • Memory 8GB
  • 自家ビルドhttperf(0.9.0ベース)

サーバ側

  • OS:Debian Wheezy (3.2.0-4-amd64)
  • Core(TM) i5-3470T CPU @ 2.90GHz
  • Memory 8GB
  • 自家ビルドnginx(1.7.2ベース)

なお今回nginxのworker_processesは1を設定してあります。nginxを動かすマシンは4Coreマシンですが1プロセスで処理を受け付けます。

アクセス先のデータとしては10KByte分のテキストが書き込まれているファイルを3355440個用意しました。合計で32GBになります。ファイルパスは後述するhttpertの引数に列挙してあります。nginx側のサーバには8GBのメインメモリをつんでありますが、用意した32GByte分のデータをすべてキャッシングすることは不可能です。

httperfを実行する際に引数でリクエスト先のパスが書かれたファイルを指定します。ただ毎回同じ内容のファイルを指定するとメインメモリのキャッシュにのってしまうので、ランダムに並べ替えてhttperfで読み込むようにします。毎回ランダムアクセスが発生させることで、ディスクアクセスを一定数発生させることを目的としています。

#!/bin/bash

#-----------------------------------------------
# パスが書かれたファイルをランダムに並べ替えて
# httperfで負荷をかけるプログラム
#-----------------------------------------------

HTTPERF='/home/shimada-k/httperf-0.9.0/src/httperf'
SERVER='192.168.3.3'

if [ ! ${#} -eq 1 ]; then
    echo 'usage: httperf_text [num request rate]'
    exit 0;
fi

RATE=${1}
NUM_CONNS=`expr ${1} \* 50`

# echo ${RATE}
# echo ${NUM_CONNS}

# ランダムに並べ替える
cat list_text | sort -R > list_text_random
echo 'sort file is ready'

# httperfが読み込めるようにnull区切りのファイルにする
cat list_text_random | tr "\n" "\0" > list_text_uri
echo 'httperf uri file is ready'

echo "${HTTPERF} --hog --server=${SERVER} --port=80 --uri=/ --wlog=Y,/home/shimada-k/list_text_uri --rate=${RATE} --num-conns=${NUM_CONNS}"
${HTTPERF} --hog --server=${SERVER} --port=80 --uri=/ --wlog=Y,/home/shimada-k/list_text_uri --rate=${RATE} --num-conns=${NUM_CONNS}

上記シェルスクリプトは第1引数をrateに設定して、rateに50を掛けたものをnum-connsに指定しています。50を掛けている理由は、httperfのReply rateは5秒間隔でスコアを蓄積している*1ため、ある程度長い時間負荷をかけないとReply rateの数値を均一な精度で取得できないからです。今回は上記スクリプトに500から2500まで100刻みで引数を与えてhttperfのスコアを記録しました。

rate second(s) req/s Repy rate(min) Reply rate(avg) Reply rate(max) Net I/O(*106)
500 50.002 500 499.8 500 500 42.3
600 50.003 600 599.8 600 600 50.7
700 50.001 700 699.8 700 700.1 59.2
800 50.001 800 799.9 800 800.1 67.6
900 49.997 900.1 899.9 900.1 900.1 76.1
1000 49.997 1000.1 999.9 1000.1 1000.1 84.5
1100 50.001 1100 1099.9 1100 1100.1 93
1200 49.997 1200.1 1198.9 1199.9 1200.3 101.4
1300 49.995 1300.1 1300.1 1300.1 1300.3 109.9
1400 50.002 1400 1399.7 1399.9 1400.1 118.3
1500 49.997 1500.1 1499.9 1500.1 1500.1 126.8
1600 49.992 1600.3 1599.7 1600.2 1600.7 135.3
1700 49.996 1700.1 1625.9 1700.1 1774.3 143.7
1800 49.997 1800.1 1790.3 1800.1 1809.9 152.2
1900 50.36 1886.4 1899.1 1899.6 1899.7 159.5
2000 49.997 2000.1 1999.8 2000.1 2000.2 169.1
2100 49.994 2100.3 2099.9 2100.2 2100.8 177.5
2200 50.18 2192.1 1904.3 2197.7 2477.6 185.1
2300 50.012 2299.5 1965.9 2292.9 2568.4 193.8
2400 50.882 2358.4 1924.1 2392.9 2791.8 199.3
2500 50.582 2471.3 2061.5 2499.4 2917.1 208.9

上記表が取得した生データです。request/sとReply (min)が一致していればnginx側においてキャパ内の処理ができていると考えていいと思います。すべてのrateにおいてNet I/Oがiperfで取得した帯域以下なので通信がボトルネックにはなっていません。さらにnginxのerrorログにも特にエラーは記載されていないことを確認しているのでファイルopen数の上限もボトルネックにはなっていません。

このままだと見にくいので、Reply rate(min)をrequest数で割った数値をグラフにしてみました。

f:id:shimada-k:20140708020007p:plain

1700付近から投げたリクエスト数に対してReply rateが安定しなくなっていて、rateが2200以降は一気にグラフが下がっています。実験した環境においては2200並列程度がnginxにおける非同期処理の性能限界なのかなと思います。

目的の場所に的確に負荷をかけるためにはボトルネックを1つずつ取り除く必要があり、ハマりポイントが多かったので苦労しました。

テックヒルズ×iBeacon ハッカソンに参加してきました

テックヒルズ×iBeacon ハッカソン

iOS7からCoreLocationに追加された機能でメジャー番号とマイナー番号とUUIDを使って通信します。beacon製品は2013年11月から国内で販売されました。

作成したのはWebsocketとiBeaconを連携させたアプリで主にサバイバルゲームでの使用を提案させて頂きました(サバイバルゲームをやったことはありません)。

f:id:shimada-k:20140629224731j:plain

味方が所持しているbeaconの領域に敵が入ると検知して、「敵が近くにいる!」と教えてくれるアプリになります。beacon側の情報として相対距離が取得できるのでその情報によってより近くに敵がいる場合はUIViewのbackgroundColorを変化させるようにしました。

f:id:shimada-k:20140630231224p:plain

画面に表示されているのはハードコーディングされた仮想user_idです。SNSと連携することでもっと凝った演出ができると考えています。ソースコードgithubに上げておきました。

一番ハマった点は敵がbeaconの領域に入った情報を検知する側のアプリでWebsocketを使用するためにAZSocketIOを使用してるんですが、サーバ側でsocket.ioのバージョンが1.0だとうまく通信できないことでした。

Leap Motion SDK V2 Betaで遊んでみる

Leap Motionの新しいSDKのpublic Beta版が5月28日に公開されました。試してみて分かったことを書いておきます。

まずはビジュアライザーを起動してみます。

f:id:shimada-k:20140619005909p:plain

グーも認識してくれます。

f:id:shimada-k:20140619005922p:plain

X方向に手を重ねるのはこれくらいが限度。これ以上近づけると片方が認識から外れるか指の認識がうまくいかなくなりました。

f:id:shimada-k:20140619005936p:plain

Y方向に手を重ねるのはこれくらいが限度。試した感じだと手の平の部分が重なるとダメみたいです。これ以上重ねると上にある方の手が認識から外れます。

SDK V2の新しい機能としてboneが取り上げられることが多いですが、右手・左手の区別って結構難しいのではないかと思っているので、センサーが検出可能な範囲に手が存在する場合、特定の状況下で右手・左手がどの程度認識できているのか試してみました。

leap motionが認識した手の信頼度を出力するHTMLを作って調査しました。コードはGistにあげておきました。

Gist

左手のデータは左側の青いdivに、右手のデータは右側の赤いdivに出力されます。片方の手しか認識されて無い場合は認識されて無い方のdivのフォントの色をグレーアウトします。

今回は以下の4つのパターンで、左手・右手の判定、信頼度を取得してみました。

  1. 手の甲を上にした片手(左手)
  2. 手の甲を下にした片手(左手)
  3. 手の甲を上にした両手
  4. 手の甲を下にした左手と手の甲を上にした右手

左手・右手はhand.typeで、信頼度はhand.confidenceで取得できます。両方ともSDK V2から使用可能なパラメータです。

手の甲を上にした片手(左手)

信頼度は1なので左手と認識されてます。そのまま手のひらをひっくり返しても左手と認識されたままでした。手のひらをひっくりかえす間は手のセンサーに対して手のひらの角度が変化するので信頼度も下がるようです。

f:id:shimada-k:20140621004356p:plain

手の甲を下にした片手(左手)

右手と認識されました。信頼度も1に近い数値なので右手だと認識されているのでしょう。

f:id:shimada-k:20140621004408p:plain

手の甲を上にした両手

両手とも正しく認識されています。信頼度は両手とも1です。

f:id:shimada-k:20140621011401p:plain

手の甲を下にした左手と手の甲を上にした右手

データは0.9付近と1が交互に出力されています。左手のdivには何も出力されていないので左手は存在しないと認識されているのでしょう。

右手のdivには「same hand detected」が出力されているので同じ手が2本あると認識されています(hand.typeが同じであるかどうかで「same hand detected」を出力しています)。

f:id:shimada-k:20140621012506p:plain

leap motion楽しいです。boneやgestureも試してみたいですね。