KLabGames Tech Blog

KLabは、多くのスマートフォン向けゲームを開発・提供しています。 スマートフォン向けゲームの開発と運用は、Webソーシャルゲームと比べて格段に複雑で、またコンシューマゲーム開発とも異なったノウハウが求められる領域もあります。 このブログでは、KLabのゲーム開発・運用の中で培われた様々な技術や挑戦とそのノウハウについて、広く紹介していきます。

(本稿はKLab Advent Calendar 2017 の3日目の記事になります)

昨今、電子工作やマイコンプログラミングへのハードルが急激に下がっているという印象があります。皆さんの身の周りでもArduinoやRaspberry Piに秋葉原で買ったセンサーを繋いで遊んでいる人がいるのではないでしょうか?

私の所属しているKLab株式会社にも「Make部」という部活があり、毎年Maker Faire Tokyoで各個人が作品を展示したり社内勉強会を開いたりして、業務と無関係に電子工作を楽しんでいたりします。

ふつうのLinuxマシンでもセンサー類を扱いたい

とはいえ、ArduinoもRaspberry Piも普段使っているLinuxマシンやmacOSマシンとは随分異なる環境です。ふつうのLinuxマシンにセンサーを接続して、気軽に扱えないものでしょうか?

Raspberry Piでセンサー類を簡単に扱えるのは、GPIOインターフェースが存在するためです。GPIOピンと各種センサー類やサーボモータなどを接続することで、Linux+電子工作の組み合わせが簡単に実現できるよ、というのがRaspberry Piの強みだと言えるでしょう。

Raspberry PiのGPIOピン

一方で、多くのLinuxマシンにはGPIOインターフェースが存在しません。たとえばUSB to I2C変換モジュールを使えばLinuxマシンでも電子工作的な遊びはできますが、追加の出費が必要ならRaspberry Piを買った方がマシだよ、となってしまいそうです。

本稿では、DigiTempを使ってLinuxマシンで比較的安価に温度センサーを扱う方法を紹介します。DigiTempはLinuxのシリアル通信インターフェース経由で1-Wire接続のセンサーを扱うOSSで、多くのLinuxディストリビューションで標準パッケージとして採用されています。

ソフトウェアのインストール

まずはソフトウェアのインストールをしましょう。大抵の環境でDigiTempはコマンド一発で入るはずです。

Debian系なら apt でインストールできます。

$ sudo apt install digitemp

RedHat系は yum でインストールできます。

$ sudo yum install digitemp

macOSでも遊べます。Homebrewからインストールしましょう。

$ brew install digitemp

後述するようにシリアル通信ドライバも必要です。こちらは利用するシリアル変換ICに応じて適切なドライバをインストールしてください。

ハードウェアの準備

当然ですが、ソフトウェアだけで外気温の測定はできません。DigiTempの利用には下記のような準備が必要です。

USB to シリアル変換モジュール

USBからシリアル通信(UART)への変換モジュールです。Arduinoで遊んだことがあれば必ず1個は持っているのではないでしょうか。変換ICとしてはFTDI社のFT232RLなどが有名ですが、他にもSilicon Labs社CP2102やProlific社 PL2303を採用した変換モジュールも容易に入手できます。

ちなみに私はCP2102ベースの変換モジュールを利用しています。これはeBayで1.15ドルでした。

CP2102 USB to シリアル変換モジュール

FTDI製ICを使っているモジュールであればドライバは標準でインストールされていることが多いかもしれません。それ以外の場合は手動でドライバをインストールする必要があるはずです。

温度センサ DS18B20

DS18B20はMaxim Integrated Products社製の温度センサです。秋月で買うと1個250円ですが、eBayなどを探せばもっと安いものも見つかります。

DS18B20

このセンサーは1-WireというMaxim独自のプロトコルで動作します。このプロトコルがUART経由で扱う上でのキモです。1-Wireはデータレートが低速かつ通信線1本で動作するので、UARTからでもドライブできるのです。他のプロトコルのセンサーであれば何らかのICが必須になるでしょう。

UARTとDS18B20の接続用ボード

UARTとDS18B20を直結しても動くことは動くらしいのですが、逆流防止などの用心のため、簡単な回路を工作してみました。

UART to DS18B20 接続用ボード

これはdword1511/onewire-over-uartで紹介されている下記の回路図を元に作成したものです。

回路図

材料は下記の通りです。

  • ユニバーサル基板
  • L型ピンソケット(1×6、メス)
  • 5.1kΩ 抵抗
  • スイッチングダイオード 1N4148
  • ポリウレタン銅線(配線用)

動かしてみる

これらを組み合わせると次のような見た目になります。思ったより場所を取る感じの仕上がりになってしまいました。

完成図

これをLinuxマシンのUSBに接続して、先ほどインストールしたDigiTempを起動すると温度が取れます。

コマンドの使い方として、まずは-wオプションを指定して1-Wireデバイスのスキャンを行う必要があります。-sオプションはシリアルポートの指定です。

$ /usr/bin/digitemp_DS9097 -s/dev/ttyUSB0 -w
DigiTemp v3.7.1 Copyright 1996-2015 by Brian C. Lane
GNU General Public License v2.0 - http://www.digitemp.com
Turning off all DS2409 Couplers
.
Devices on the Main LAN
28FF933161150389 : DS18B20 Temperature Sensor

これで接続されているセンサーの情報が$HOME/.digitemprcに記録され、次回以降の起動でこの情報を参照するようになります。

$ /usr/bin/digitemp_DS9097 -a -d 2 -n 5
DigiTemp v3.7.1 Copyright 1996-2015 by Brian C. Lane
GNU General Public License v2.0 - http://www.digitemp.com
Dec 02 21:21:27 Sensor 0 C: 21.50 F: 70.70
Dec 02 21:21:29 Sensor 0 C: 21.56 F: 70.81
Dec 02 21:21:31 Sensor 0 C: 21.50 F: 70.70
Dec 02 21:21:33 Sensor 0 C: 21.50 F: 70.70
Dec 02 21:21:35 Sensor 0 C: 21.44 F: 70.59

上記は2秒間隔でセンサーの値を5回取得する指定です。摂氏と華氏で温度が取れているのがわかります。

ちなみに、筆者はこれを家のLinuxルータに刺した上で取得した値をMackerelに書き出しています。

温度変化グラフ

上記のグラフが作りたいだけならRaspberry Piで温度センサーを扱った方が楽なのでは?と思われるかもしれません。理屈で言えばそうかもしれませんが、個人的にRaspberry Piは長期間電源を入れっぱなしにする気が起きないので、ふつうのLinuxマシンで運用できることに価値があるように感じています。同じ感覚の方が他にいらっしゃるかはわかりませんが…。

まとめ

  • DigiTempというLinux/macOS上で温度センサーDS18B20を扱うOSSを紹介しました
    • シリアル通信(UART)経由で1-Wireセンサーをドライブできます
    • 多くのLinuxディストリビューションで標準パッケージ採用されています
  • Raspberry Piで同じことをするより適用範囲が広かったり長期運用しやすかったりするかもしれません

@hnw

(本稿はKLab Advent Calendar 2017 の1日目の記事になります)

2017年のKLabのアドベントカレンダーです。最初はoho-sです。よろしくお願いします。

自分が作ったVRコンテンツをどんなのか説明したい!

VRコンテンツを作っていて、自分が作ったVRコンテンツをどんなものか説明したり共有したい時ってありませんか? ハイスペックPCやVR用HMDを持っていない人にも、雰囲気だけでも伝えたい。そんな時に便利なのが、いわゆる360°動画でのキャプチャです。 今回は、Facebookが公開している、360-Capture-SDKを使ってキャプチャしてみます。日本語での使い方の情報が探してもあまりなかったので、参考になれば幸いです。

Facebookの360-Capture-SDKを使ってみる

360-Capture-SDKの動作環境は、こちらを参考にしてください。 Windows(64bitのみ)の8以降で、NVIDIAもしくは、AMDのGPUが必要になります。各ドライバのバージョン指定があるので確認してください。 対応しているUnityのバージョンは、README.mdに記載がありませんが、今回Unity 5.5.3p3 (64-bit)で動きました。一方、Unity 2017.2.0p2 (64-bit)では、動画のキャプチャはできましたが、画面の表示がされないなどの不具合が見られました。

さて、では使ってみましょう。まず、キャプチャしたいUnityプロジェクトにSDKを組み込みます。 360-Capture-SDKが公開されているリポジトリは、こちらです。 このリポジトリをcloneしてきて、 Samples/Unity/Assets 以下のEncodePackageフォルダとPluginsフォルダをそのまま組み込みたいプロジェクトのAssets以下にコピーします。

次に、EncodePackage内の、EncoderObjectプレファブをシーンに置きます。

組み込み

そして、EncoderObjectのインスペクターからいくつかの録画用パラメータなどを修正などします。 負荷とクオリティを考えて、ピクセル数などを選びましょう。 EncoderObjectの位置がキャプチャの中心位置になるので、適切にPositionを設定します。少し離れた位置にすると、観客視点とかもできると思います。

最後にキャプチャをします。 プロジェクトを実行して、キャプチャしたいポイントでF2キー(デフォルトでは。インスペクターで変更可能)を押すとキャプチャ開始、F3キーで停止です。キャプチャした動画は、プロジェクトフォルダのGallery内に保存されます。

動画のメタデータの調整

ここまでの手順で作成した動画を、そのままYouTubeやFacebookに投稿しても360°動画再生の専用プレイヤーになりません。 動画に360°動画であることのメタデータを付けてやります。

そのためのツールが、こちらになります。

展開して実行すると下図のようなダイアログが出てくるので、「Open」からキャプチャした動画ファイルを選び、「Spherical」 のチェックボックスをオンにして「Inject metadata」します。以上で新しい動画ファイルができたと思います。

ダイアログ

アップロードしてみる

通常の手段でアップロードします。FacebookやYouTubeであれば、自動的に先ほどの手順でつけたメタデータに基づいて処理してくれて、360°動画として扱われるはずです。

サンプル動画ファイルリンク

まとめ

以上のように、比較的容易に360°動画でのキャプチャができてしまいました。 VRコンテンツのプロモーションなどに使えるのではないかと思います。 是非試してみてください。

最後に余談ですが、EncoderObjectのインスペクターを見ていると、どうもキャプチャーした動画のストリーミングができるようですが、試してみたところうまく動きませんでした。 この辺りも面白そうなので、今後調査を進めてみたいと思います。

KLab Advent Calendar 2017 の2日目は、haltさんです。よろしくお願いします。

Webでとにかく高速に計算したい

やまだです。Webでとにかく高速な計算を行うために人生の何%かを使っています。
前回はJavaScriptから直接SIMD.jsを呼びましたが今回はEmscriptenを使用し、C言語からSIMD命令を呼び出してみます。

題材としては定番ですがマンデルブロ集合を使用します。

mandelbrot_glsl

マンデルブロ集合は以下の漸化式で計算が可能でしばしば並列演算の課題としてとりあげられます。

mandelbrot0

z は複素数なので実部と虚部をXY平面に表すと以下のようになります。

mandelbrot1

mandelbrot2

Emscriptenを使う

今回はWebでということでC言語のコードをJavaScriptコードにコンパイルするEmscriptenを使用します。
Emscriptenを使用するとasm.jsを利用した最適化をかけることができるため単純にJavaScriptで実装した時よりも高速になることがあります。
Emscriptenでは -O1 以降を指定するとasm.jsが有効となります。今回は -O3 を使用して計測しました。

Emscriptenでコンパイルすることを前提としてCでマンデルブロ集合を実装すると以下のようになります。

#define N (500)

void compute(int bufferLength)
{
  for (int i = 0; i < N; i++)
  {
    for (int j = 0; j < bufferLength; j++)
    {
      float zx = zxBuffer[j];
      float zy = zyBuffer[j];

      zxBuffer[j] = zx * zx - zy * zy + cxBuffer[j];
      zyBuffer[j] = 2.0 * zx * zy + cyBuffer[j];
    }
  }
}

全ピクセルに対してN回ループする必要があるため非常に重い処理であることがひと目でわかります。
実行時間はマンデルブロ集合のループ計算を開始してからテクスチャ用のRGBバッファに変換するまでの時間を計測しています。

SSE: disabled
Thread: 1
Width: 1024
Height: 1024
PixelRatio: 2.0
Step: 500
Time : 3414ms

スレッドを使う

compute関数の内側のループはスレッドを使用して分割できそうですのでこちらに挑戦してみます。

Emscriptenはマルチスレッドライブラリであるpthreadを実験的にサポートしています
コンパイル引数に-s USE_PTHREADS=1を加えることによりpthreadを有効にできます。
ただし、ECMAScriptのドラフト仕様であるSharedArrayBufferに依存しているため、2017年6月現在Firefox Nightlyでのみ動作します。

#define N (500)
#define NUM_THREADS (2)

// マンデルブロ集合を計算する
static void *compute(void *args)
{
  THREAD_ARGS *threadArgs = (THREAD_ARGS *)args;
  int min = threadArgs->min;
  int max = threadArgs->max;

  for (int i = 0; i < N; i++)
  {
    for (int j = min; j < max; j++)
    {
      float zx = zxBuffer[j];
      float zy = zyBuffer[j];

      zxBuffer[j] = zx * zx - zy * zy + cxBuffer[j];
      zyBuffer[j] = 2.0 * zx * zy + cyBuffer[j];
    }
  }

  return NULL;
}

// スレッドを作成する
static void create_threads(int bufferLength, pthread_t **threads)
{
  for(int i = 0; i < NUM_THREADS; i++) {
    // 分割する
    int min = i * bufferLength / NUM_THREADS;
    int max = (i + 1) * bufferLength / NUM_THREADS;

    // スレッドを作成
    pthread_create(threads[i], NULL, compute, (void *)get_thread_args(min, max));
  }
}

結果は以下のようになりました。
単一スレッドで実行した時に比べて1.7倍程度高速化できています。
計測環境はCPUコア数が2であったこともあり、4スレッドにしたとしてもパフォーマンス向上は体感できませんでした。
ですが、コア数がもっと多いCPUですと演算器の数の制限などはあるでしょうが、スレッド数を増やすことによりパフォーマンスが向上することが期待できます。

SSE: disabled
Thread: 2
Width: 1024
Height: 1024
PixelRatio: 2.0
Step: 500
Time : 1952ms

SIMDを使う

さて、もう一つの高速化の方法として1命令で複数の計算処理が可能なSIMD命令を使用してみます。
SIMD命令に対応しているブラウザでのみ動作します。

SIMDを使用するにはいくつか方法があります。

  • LLVMの自動ベクトル化機能を使用する
  • GCC/Clang固有のSIMD拡張を使用する
  • IntelのSIMD拡張であるSSEを使用する

今回はSSEを使用します。
IntelのSIMD拡張はMMXやAVDなどがありますがEmscriptenはSSE1, SSE2, SSE3, SSSE3のみ対応しています。
SSEを使用するにはコンパイル時に-msseオプションを指定します。

SSEを使用する場合は128ビット幅の__m128を使用します。__m128には32ビットfloatが4つパックされています。
_mm_add_ps_mm_mul_psがSSE用の関数ですので演算にはこちらを使用します。
見ての通り_mm_add_psが加算、_mm_mul_psが乗算APIとなります。
公式リファレンスにアラインメントの指定なども書かれているので、こちらの指示に従った形でメモリ確保をする必要があります。
SSEは16ビットにアラインされたバッファが必要となるため、専用のaligned_allocでメモリを確保します。
ですがaligned_allocはEmscriptenでビルドすると関数が見つからずにエラーとなってしまったため、Emscriptenのbenchmark_sseで使用されているマクロを使用することにしました。

#define aligned_alloc(align, size) (void*)(((uintptr_t)malloc((size) + ((align)-1)) + ((align)-1)) & (~((align)-1)))

// 項zと定数cのバッファを確保する
static float *zxBuffer = NULL;
static float *zyBuffer = NULL;
static float *cxBuffer = NULL;
static float *cyBuffer = NULL;

static void alloc_bufers(int bufferLength)
{
  zxBuffer = (float *)aligned_alloc(16, bufferLength);
  zyBuffer = (float *)aligned_alloc(16, bufferLength);
  cxBuffer = (float *)aligned_alloc(16, bufferLength);
  cyBuffer = (float *)aligned_alloc(16, bufferLength);
}

static void compute(int bufferLength)
{
  const __m128 two128 = _mm_set1_ps(2.0);

  // floatを4つ同時に計算するため4で割る
  // 即ちバッファの長さは4の倍数である必要がある
  int bufferLength128 = bufferLength / 4;

  // SIMD型にキャストする
  __m128* zxBuffer128 = (__m128 *)zxBuffer;
  __m128* zyBuffer128 = (__m128 *)zyBuffer;
  __m128* cxBuffer128 = (__m128 *)cxBuffer;
  __m128* cyBuffer128 = (__m128 *)cyBuffer;

  for (int i = 0; i < N; i++)
  {
    for (int j = 0; j < bufferLength128; j++)
    {
      __m128 zx = zxBuffer128[j];
      __m128 zy = zyBuffer128[j];

      __m128 cx = cxBuffer128[j];
      __m128 cy = cyBuffer128[j];

      // SSEを使用して4つまとめて計算する
      __m128 tx = _mm_mul_ps(zx, zx);
      __m128 ty = _mm_mul_ps(zy, zy);

      zxBuffer128[j] = _mm_add_ps(_mm_sub_ps(tx, ty), cx);
      zyBuffer128[j] = _mm_add_ps(_mm_mul_ps(two128, _mm_mul_ps(zx, zy)), cy);
    }
  }
}

実行結果は以下の通り3414msから1659msに約2倍程度高速化しています。
理想を言えば4倍程度になって欲しいのですが複素数をRGBに変換する処理などいくらかオーバーヘッドが発生しています。

SSE: enabled
Thread: 1
Width: 1024
Height: 1024
PixelRatio: 2.0
Step: 500
Time : 1659ms

SSEとpthreadの併用

browser

さて、ではSSEとpthreadは同時に使えないものかと思い併用してみました。
ソースコードは省略しますがあっさりうまくいってしまい、3倍程度の高速化に成功しました。

SSE: enabled
Thread: 2
Width: 1024
Height: 1024
PixelRatio: 2.0
Step: 500
Time : 1083ms

Chromeでは?

Google Chromeは残念ながらSSEもpthreadも対応していません。
ですがasm.jsには対応しているのでSSEやpthreadを無効にして実行してみたところ、
同じコンパイルオプションでFirefox Nightlyよりも良好なパフォーマンスを得ることができました。
JavaScript実行エンジンの違いでの差ということになります。

SSE: disabled
Thread: 1
Width: 1024
Height: 1024
PixelRatio: 2.0
Step: 500
Time : 2065ms

WebAssembly

本当はWebAssemblyでコンパイルしたかったのですが、以下のようなエラーがでていました。

Traceback (most recent call last):
  File "/Users/yamada/emsdk-portable/emscripten/1.37.9/emcc", line 13, in <module>
    emcc.run()
  File "/Users/yamada/emsdk-portable/emscripten/1.37.9/emcc.py", line 1278, in run
    assert not shared.Settings.USE_PTHREADS, 'WebAssembly does not support pthreads'
AssertionError: WebAssembly does not support pthreads

まだ、pthreadは対応していないようです。

まとめ

処理時間をまとめてみました。かなり効果的に高速化されています。
今後はSSEやpthreadを使った高速化に積極的に挑戦していきたいです。

Firefox Nightly SIMD及びマルチスレッド化なし 3414ms
Chrome SIMD及びマルチスレッド化なし 2065ms
Firefox Nightly マルチスレッド化 1952ms
Firefox Nightly SIMD化 1659ms
Firefox マルチスレッド化 + SIMD化 1083ms

Firefox Nightlyは他の環境で試してみたところ処理するスレッドは1スレッドだったとしても、
pthread_createを使用してメインスレッドで処理せずに一つのサブスレッドで処理した場合のほうが高速だったこともありNightly Buildということもあり結果が安定していない印象を受けました。

と、ここまでSIMD推しできましたが直近ちょっとした事件が発生しました。
ECMAScriptの新仕様としてSIMDは議論が続けられてきましたがInactive Proposalに移動しました

今後はWebAssemblyの一部としてSIMDの実装は続けられていくようです。

マルチスレッドやSIMDは従来はWebになかったチューニング手段です。
ゲームを始めとしてWebがプラットフォームとして育っていくのを見るとわくわくしますね。

参考

↑このページのトップヘ