KLabGames Tech Blog

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

カテゴリ: JavaScript

このエントリーは、KLab Advent Calendar 2016 の12/19の記事です。 やまだです。 昨年はWebGLをつかったソートを実装してみましたが今年はWebGLを使った画像圧縮を実装してみます。

はじめに

WebGLのみならずアプリ開発において画像のフォーマットの選択は重要です。 とくにゲームの大部分を占める画像データはゲームのダウンロード時間に直結するため可能な限り圧縮しておきたいものです。

WebGLではGPUにテクスチャをアップロードする際にはgl.texImage2Dを使用します。 gl.texImage2DTexImageSourceをアップロードできます。 このTexImageSourceというのが面白くてテクスチャデータとしてHTMLImageElementが使えます。 すなわちGIF/JPEG/PNGといったフォーマットだけでなくGoogleが開発した高圧縮率フォーマットであるWebPも使用可能です。 さらにはHTMLVideoElementもテクスチャフォーマットとして使用できるため、動画さえもテクスチャにできます。 ダウンロードサイズの削減が目的であればGIF/JPEG/PNG/WEBPから適切なフォーマットを選ぶのがよいでしょう。

一方で今日ではモバイルデバイスもターゲットとなってくるため、 ビデオメモリの容量も気にしたいところで、画素あたりのビット数も削減したいところです。 WebGLにおいてはPNGなどを用いた場合、1画素あたり32bit使用することが多いでしょう。 その場合、1024x1024のテクスチャ1枚当たり4MByteのサイズとなります。 そうなってくると選ばれるフォーマットはETCやPVRTCでしょう。 例えばETC2の8bit/pixelを用いれば1024x1024のテクスチャも1/4のサイズである1MByteまで削減できます。 ですが、これらはWebGLでも使用可能ですがWebGL 1.0ではエクステンションのため使用できる保証がありません。 ETC2はWebGL 2.0で使用可能とのことなので今後はETC2を積極的に使用していきたいところです。 ETCやPNG等を使用するのも手ではありますが前述のとおりいくつかの無視できない課題が存在するため、 今回はこれらとは異なるアプローチで画像圧縮を試みてみました。

YCgCo色空間による圧縮

Unity向けではありますがこちらYCgCo色空間を使用して画像の圧縮を試みている方がいらっしゃいました。 記事で解説されている通りではありますが画像をLuma(明度)とChroma(彩度)に分割し、 人間の目はLumaに比べてChromaの変化に対して鈍感である性質を利用してChromaの情報量を落として圧縮しています。

元画像

lena

Lumaのみの画像

luma_only

Chromaのみの画像

chroma_only

人間の目はLumaのみの画像に対してChromaの変化に鈍感であることが実感できると思います。 このアイディアそのままにWebGLによる再実装を行いました。

色空間を変換するシェーダ

RGBAをYCgCoAに変換する

YCgCoとRGBの色空間は行列による変換が可能です。なのでシェーダとも相性がよくシェーダでも行列を使用して変換を行います。 1つだけ注意しないといけないところはYCgCoのYの値域は[0, 1]に対してCgCoの値域は[-0.5, +0.5]のためテクスチャに保存する際に0.5を加算します。 この加算含めて4x4行列にまとめるとRGBAをYCgCoAに変換するシェーダは以下のように書けます。

元となる実装と同じようにYをAlpha8に割り当て、CgCoAをRGB565のフォーマットに書き込むことを考えて swizzleを使用して(Y,Cg,Co,A) => (A,R,B,G)のように割り当てます。 さらに変換後の画像をさらにRGBとAに分割し、RGB画像の解像度を1/4に縮小します。 結果1画素あたりのビット深度はAlpha8(8bit) + RGB565(16bit) / 4 = 12bitとなります。

precision mediump float;

uniform sampler2D u_map;

varying vec2 v_uv;

void main(void) {
    mat4 m = mat4(
        0.25, -0.25, 0.5, 0.0,
        0.5, 0.5, 0.0, 0.0,
        0.25, -0.25, -0.5, 0.0,
        0.0, 0.5, 0.5, 1.0
    );

    vec4 c = texture2D(u_map, v_uv);
    vec3 ycc = (m * vec4(c.rgb, 1.0)).xyz;
    float alpha = c.a;
    vec4 ycca = vec4(ycc, alpha);

    gl_FragColor = ycca.ywzx;
}

Yのみのアルファ画像

lena_y

縮小したCgCoA画像

lena_cca

YCgCoAをRGBAに変換する

YCgCoAに変換した2枚のテクスチャをシェーダで合成し、RGBAに変換します。 その際にVRAMの節約のためにCgCoA画像はRGB565のテクスチャで、Y画像はAlpha8のテクスチャとしてアップロードします。

// Y画像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, gl.ALPHA, gl.UNSIGNED_BYTE, image);
// CgCoA画像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_SHORT_5_6_5, image);

シェーダで合成する際はCgCoに0.5を加算しているため行列をかけるまえに0.5ひいてやります。

precision mediump float;

uniform sampler2D u_tex0; // CgCoA画像
uniform sampler2D u_tex1; // Y画像

varying vec2 v_uv;

void main(void) {
    mat3 m = mat3(
        1, 1, 1,
        -1, 1, -1,
        1, 0, -1
    );
    vec3 cca = texture2D(u_tex0, v_uv).xyz;
    float y = texture2D(u_tex1, v_uv).w;
    vec4 c = vec4(y, cca.x, cca.z, cca.y) - vec4(0.0, 0.5, 0.5, 0.0);
    gl_FragColor = vec4(m * c.xyz, c.a);
}

合成結果

result

比較

左:元画像、右:合成画像

compare

ほぼ劣化がないように見えます。 念のためちゃんと画像が劣化しているのかを確認するために こちらを参考に差分をとってみます。

$ composite -compose difference lena.png result.png diff.png
$ identify -format "%[mean]" diff.png
428.525

確かに差分が発生しているので画像は劣化してはいるようです。 差分画像を正規化した物がこちらです。

diff_normalized

全体的に差分が発生していることが確認できます。 Chromaのビット数を削減しているので納得できる結果となりました。

最後に

YCgCoによる圧縮は見た目上は効果的であるということはわかりましたが、いくつかの問題点があります。

1つはテクスチャフェッチが2回必要となること。 テクスチャフェッチは重い処理なのでできれば少なくしたいところです。

もう1つはアルファのみを保存する適切なフォーマットが見つからないこと。 Y画像は8bit深度のアルファ画像なのですがPNG画像でアルファ付き画像として保存すると、 32bit深度で保存されてしまい少しもったいないです。

また、原因の特定はできなかったのですがGPUによってはうまく描画できない例も見受けられました。

得られる画像は非常に良いものではあるのですが、現状のWebGLにおいてでは画像の圧縮はダウンロードサイズの削減が目的であればPNG/JPEG/WebPを使用し、 ビデオメモリの節約であればETC2に望みを託す、というのが今はよいかもしれません。

以上です。

KLab Advent Calendar 8日目の記事です。 こんにちは knsh14 です。

KLabではUnityを使って iOS、Android の端末で遊べるようなゲームを開発し、提供しています。 その一方で、それら以外の環境でも遊べるゲームが作れないかということで常に技術検証を行っています。 最近では 「ユニティちゃんのホームランスタジアム」 を Apple TV で提供したりしています。(紹介記事)

その活動の一環として、今回はブラウザで快適に動作する 3D のゲームを作るべく、Unity ではないゲームエンジンを使って開発したのでその紹介をしようと思います。

なぜUnityを使わなかったのか?

ブラウザ動作するためのゲームを開発するにあたって、幾つかの制約がありました。

  1. モバイル、PC のブラウザで動作すること
  2. 容量が小さい(理想は 3MB 以下におさまる)こと

僕らが普段使い慣れている Unity でも WebGL ビルドができ、PC やモバイルのブラウザで遊ぶことができます。ですが、試しに「ユニティちゃんのホームランスタジアム」をビルドしてみたところ、20MB 以上の容量になってしまいました。 これではとてもリリースできないので、Unity を使う選択肢を諦め、別のエンジンを使う道を探し始めました。

色々調べた結果、PlayCanvas というゲームエンジンが海外での利用実績などもあり良さそうだという結論になったので実際に試してみました。

PlayCanvasってなに?

  • 公式サイト
  • WebGL で動作することを主な目的としたゲームエンジンです
    • iOS で動作するように吐き出されるオプションもあります
  • その為吐き出されたゲームのサイズが最小で 10KB 程度と非常に軽いのが特徴です
  • 使用する言語は Javascript です
  • ゲームエンジンが Github で公開されているので、Unity と異なり動作をかなり細かいところまで追いかけることができます
  • エディタ上でチャットすることができ、お互いがその場にいなくてもペアプロのようにして開発することができます。

実際の開発画面

オブジェクトなどをいじるエディタ画面はこのような感じです。

シーンエディタ

だいぶUnityに近い感じで操作できます。

コードエディタ

Ace Editor を利用した感じのエディタになっているのでシンタックスハイライトもありますし、あまり使いにくいことはなさそうです。 このスクリーンショットだとわかりにくいですが、多少のコード補完もしてくれます。

実際にゲームを作ってみる

公式チュートリアル からタップでリフティングして落とさないようにするゲームを作ってみると一通りの使い方などがわかると思います。

PlayCanvas のメリット

開発環境のセットアップが要らない

ブラウザさえあればどこでも開発できるので便利です。

エディタ上で複数バージョンのプロダクトを管理できる

プロダクトを公開するときは、エディタ上から簡単に世界に公開することができます。 また複数バージョンをスタンバイさせておくことができるので、バグが有った場合にも簡単に前のものに差し戻すことができます。

Chrome でデバッグができる

Google Chromeは開発者ツールを使うことでかなり高機能なデバッグができます。 ブレークポイントをはって処理を止めながらデバッグすることができるのはもちろんですし、プロファイラで時間がかかっている処理を調べながら開発することができるので、初期からパフォーマンスを意識しながら開発することができます。

苦労話

PlayCanvas 上でバージョン管理できなかった

PlayCanvas からパブリッシュした成果物は PlayCanvas 上で一覧でき、昔のものを復活させることができます。 ですが、プロジェクトの状態は復活してくれないので、リファクタする時にかなり覚悟が必要でした。 Githubなどとコードを連携する方法 もありますが、Legacy なシステムなのでいつなくなるかわからないのが現状です。 Git でコミットするように成果物を Download しておいて Github で管理するか、Google Drive などに置いておくのが鉄則になりそうです。 どっちの方法でもシーンの状態を戻すことができないのでいい方法を模索中です。

なぜかありえない場所で衝突イベントが走る

物体を動かすために毎フレーム pc.Entity.setLocalPosition() で物体を動かしていました。 ですが、ある日物体が何もない場所でいきなり衝突イベントが走るバグに悩まされていました。 物理演算に任せて動いている場合は何事もないのですが、setLocalPosition() で物を移動させた後おかしくなることがわかりました。

いろいろ試した結果、pc.Entity.setLocalPosition() では Entitypc.Model は移動しますが、 rigidbody の位置が移動しないことが原因でした。

rigidbody がついた Entity を動かすときは

Sample.prototype.move = function(x, y, z) {
  this.entity.setLocalPosition(x, y, z);
  this.entity.rigidbody.teleport(x, y, z);
};

というメソッドを生やしてこれを使うほうが良さそうです。

まとめ

今回は Unity ではないゲームエンジンで WebGL ゲームを開発してみました。 PlayCanvas はどちらかと言うと個人でささっとプロトタイプを作ってすぐに見せるような場合に非常に便利なのではないかと思いました。 大人数で高品質なゲームを作るには Unity 少人数でとにかくスピード感を重視するような開発では PlayCanvas というように状況に応じて使い分けていけたらと思います。

やまだです。 SIMD.jsについてゆるく話をします。

SIMDとは何か?

まず、SIMD(Single Instruction Multiple Data)とは何かから簡単にお話しします。 SIMDは命令1つで複数のデータの演算を一括して行う計算方式です。 複数データに対する演算を一括して行うため、 同じような演算を大量に実行しなければならない場合に威力を発揮します。 しかし、SIMDを使うためにはCPUによって異なる命令を実行する必要があります。 たとえばIntel系のCPUであればSIMD拡張の命令セットであるSSEを使い、 ARM系CPUならNEONというように使い分けないといけません。

ブラウザとSIMD

今まではこれらはJavaScriptからは使うことができませんでした。 しかし、ブラウザのパフォーマンスを求め続けた結果ある実装が生まれました。

そう、Dartです。 DartはJavaScriptとは異なる高速なVM上で動作しながらも、ECMAScript 5に変換もできるという優れた処理系でした。 この野心的なプロジェクトは1年以上前にSIMDに対応していました。 記事によると3D分野でしばしば用いられる4x4行列の乗算で300%のパフォーマンスを発揮したようです。

そして、この実装はJavaScriptにも取り入れられようとしています。 JavaScriptの標準仕様であるECMAScript 2015が承認されたことは記憶に新しいと思います。 ECMAScript 2015ではアロー演算子やデストラクチャリングなど様々な拡張が行われました。 今、ECMAScriptは次なる仕様ECMAScript 7に向けて仕様の策定が進められています。 その中の一つに今回紹介するSIMD.jsがあります。 ECMAScriptの策定プロセスのうち、Stage 2、つまりDraft段階にあり主要な機能の定義が進められています。 まだまだ仕様として安定はしていないのですが、様々なプラットフォームでSIMD.jsを試すことができます。

SIMD.jsを実際に動かしてみる

今回はChromiumでSIMD.jsのパワーを体感してみましょう。 SIMD.jsの試験的な実装がなされたChromiumは こちらから手に入ります。 Windows、Mac、Linuxそれぞれのバイナリが用意されていますが、 実行時に--js-flags=--simd-objectをコマンドライン引数として与える必要があります。

このChromiumを使用してSIMDのパフォーマンスを体感できるコードを書いてみましょう。 合計値を計算するsum関数を実装してみます。 sum1は配列を1つずつ加算して行くのに対し、sum2は4つ同時にまとめて加算しています。

//NO SIMD
var sum1 = function(list) {
  var total = 0;
  var length = list.length;
  for(var i = 0; i < length; ++i) {
    total += list[i];
  }

  return total;
}

//SIMD
var sum2 = function(list) {
  var i32x4list = new Int32x4Array(list.buffer);
  var total = SIMD.int32x4.splat(0);
  var length = list.length / 4;
  for(var i = 0; i < length; ++i) {
    total = SIMD.int32x4.add(total, i32x4list.getAt(i));
  }

  return total.x + total.y + total.z + total.w;
}
SIMD ops/s
Yes 1509ops/s
No 620ops/s

結果はご覧のとおり、2倍以上のスピードが出ています。 ただの加算ですがここまで違いが出てしまいます。 こういった大量のデータ処理はSIMDの得意分野です。

とくにWebGLではベクトル演算の需要が大いにあります。 衝突演算、物理演算に威力を発揮することでしょう。

SIMD.jsと未来

SIMDを使ったチューニングはC言語などを使い、低レイヤーで行われることが多いですが、 JavaScriptによるSIMDチューニングができる時代がすぐそこまできています。 もちろん、OSやデバイスを意識せずに手軽に使うことができます。 Mac、Windows、Linux、そしてスマートフォンでも!

工夫次第で大きくパフォーマンスを向上できるSIMD.jsの今後にワクワクしてきませんか?

@やまだ

↑このページのトップヘ