KLabGames Tech Blog

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

※この記事は8/10に書かれたものです。現在では最新のGit2.5のWindows用バイナリも公式に公開されていますので、
 自前ビルドせずともGit2.5が利用できますが、次期バージョンでも同様の手順でビルドできると思われます。

@makki_dです。
普段はLinuxを使っていますが、WindowsでGitをビルドしてみたというお話です。
とても簡単にビルドすることができたので紹介したいと思います。

Git 2.5 を使いたいんです。

先日Git2.5が公開されました。
様々な修正のほか機能も多数追加されましたが、個人的には git worktree コマンドに注目しています。

git worktreeとは

あるブランチで作業をしているときそれを中断して、急ぎで別のブランチの修正をしなければならないことってよくありますよね。
そんな時皆さんはどうしていますか?

  1. 今の作業をとりあえずcommitstashしてブランチを切り替える
  2. もうひとつcloneしてきてそこで作業する
  3. git new-workdirで別ディレクトリに作業ツリーを作る
  4. git worktreeで別ディレクトリに作業ツリーを作る ← New!!

git worktreegit new-workdirと同じく、別ディレクトリに作業ツリーを作ることができるコマンドです。
作業ツリーを別に作ることで、今進めている作業をそのまま置いておきながら、同じリポジトリの別ブランチの作業を別ディレクトリで行うことができます。

具体的な使い方はドキュメントの例がわかりやすいです。

これまでもgit new-workdirがありましたが、このコマンドはシンボリックリンクを作成するため、Windowsの場合一般ユーザ権限では利用できませんでした
一方、git worktreeではシンボリックリンクではなく、ファイル中にパスを記録する形で作業ツリーと親リポジトリの関係が管理されるので、一般ユーザでも利用することができます。

公式のWindows用バイナリは1.9.5 (※8/10時点)

早速 Git 2.5 をWindowsでも使ってみたいところですが、公式のDownloadページには、Windows向けバイナリは1.9.5までしか用意されていません。
そして「新しいバージョンが使いたかったら、ソースからビルドしてね」とさらっと書かれています。

Windows上でOSSのビルド環境を整えるというと多くの場合苦行となるような印象がありますが、Gitの場合は驚くほど簡単に開発環境がセットアップできました。

WindowsでのGitビルド環境

Windows用のバイナリと開発環境は、Git for Windowsで入手できます。
紛らわしいですが、「git for windows」等で検索すると上位に出てくる msysGit は 1.9.5 までしかありません。
特に2系を使いたい場合は、間違えずに後継プロジェクトであるGit for Windowsを見てください。

Git for Windows SDKのダウンロード・インストール

インストーラを実行すると、指定したインストールディレクトリ以下に gcc を始めとした開発ツール・環境・ライブラリ一式がダウンロード・インストールされます。
64bit版SDKのデフォルトのインストールパスは C:\git-sdk-64 なのでそのまま記載しますが、変更した場合は適宜読み替えてください。

インストールパス
ネットワークインストール

インストールが終わるとそのまま Git のビルドが始まりますが、私の環境では gettext 関連のビルドでエラーとなりました。

ビルドエラー

エラーとなったコマンドプロンプトを一旦終了し、msysのシェル (C:\git-sdk-64\mingw64_shell.bat) で改めて開き直します。
gitのソースディレクトリ (/usr/src/git) でmakeを実行すると、先ほどのエラーに引っかからずにビルドできます。

ビルド成功

ビルドできたらmake instalすると C:\git-sdk-64\mingw64\bin に実行ファイル一式がインストールされます。
ここにPATHを通すことで、gitコマンドがコマンドプロンプトやcygwinから使えるようになります。

バージョン2.4.6??

執筆時点のmasterブランチでビルドすると、git のバージョンが2.4.6になっていました。

version

これではgit worktreeが使えない!?と一瞬焦りましたが、よくよくコミットグラフを確認すると、v2.5.0タグのブランチがまるごとmergeされてるため、使いたかったgit worktreeコマンドも使えました。

worktree

ちょっとパッチを当ててみる

git worktreeで作った作業ツリーのサブディレクトリの中でalias登録したgitコマンドを呼び出すと、次のようなエラーが出てしまいます。

makiuchi-d@PC-1034 MINGW64 ~/Projects/test/work1/a (work1)
$ git br
fatal: internal error: work tree has already been set
Current worktree: C:/Users/makiuchi-d/Projects/test/work1
New worktree: C:/Users/makiuchi-d/Projects/test/work1/a

これでは不便なので、エラーを出している部分に即席パッチをあててみようと思います。

diff --git a/environment.c b/environment.c
index fb4eda7..8cf1442 100644
--- a/environment.c
+++ b/environment.c
@@ -226,7 +226,7 @@ void set_git_work_tree(const char *new_work_tree)
 {
        if (git_work_tree_initialized) {
                new_work_tree = real_path(new_work_tree);
-               if (strcmp(new_work_tree, work_tree))
+               if (strncmp(new_work_tree, work_tree, strlen(work_tree)))
                        die("internal error: work tree has already been set\n"
                            "Current worktree: %s\nNew worktree: %s",
                            work_tree, new_work_tree);

※あくまで応急措置です

この修正を加えた上でmakemake installすることで、動くようになりました。

動作確認

まとめ

GitはWindowsでも簡単に開発環境をセットアップできます。
いち早く新機能を試したいときや、ちょっとした修正をしたい場合など、ぜひ試してみてください。

P.S.
8/18、Git 2.5のWndows用バイナリが公式に公開されました。

※この記事ではgitのタグ「v2.75」から生成したブランチ上でコードリーディングしています。

Blender標準のレンダラよりもリアルな絵を作る事の出来る「Cycles」というレンダラ。アーティストの方々はクオリティ・リアリティの高い画像や動画などを作る際に利用されている事でしょう。数あるBlenderの機能の中でも注目度が高いのではないでしょうか?

プログラマである私のBlenderの使い道と言えばメッシュを作ったりボーンを仕込んだりしてゲーム開発の際のテスト用モデルを作る事であり、Cyclesのお世話になる事はなかったのですが、コードリーディングをしてみようと思い立ち1から勉強してみることにしました(なんだか目的と手段が逆な気もしますが ^_^; )。

Cyclesはとても奥深い機能であるし、現在も開発が進んでいます。ですのでこのCyclesの記事は1回では終わりませんし、part何までいくか今のところ分かりません。もしかしたら途中で別の機能についての記事を挟んでまた再開するようなこともあるかもしれません。とにかく頑張ってコードリーディングしていこうと思いますのでお付き合いいただければ幸いです。

”Cyclesとは” と書こうとしたんです!

まずはじめに、「Cyclesとは〜〜だ!」と一言で説明されたものを頭に入れておけば後々理解の助けになりそうです。そこでBlender Reference ManualのCyclesのイントロダクション から引用します。

Cycles is a ray tracing renderer focused on interactivity and ease of use, while still supporting many production features.

要するに肝心なところは「Cyclesとはレイトレーシングレンダラだ!」というところですね!
しかしこの「レイトレーシング」、ちょっと調べてみると指しているものが広すぎてもう少し整理しないことには理解の助けにならない事が分かったのです。

ここから先は上記の「レイトレーシング」が何を指すのかもう少しハッキリさせるべく、歴史的なところから調べた事を書いていますが、それぞれのキーワードについて厳密な定義が存在するのかはわかりません。故に私よりも深く勉強されている方からは指摘されるような部分があるかもしれませんが、このように解釈すればCyclesの理解に役立つだろうという部分に主眼を置いて自分なりに整理してみました。

レイトレーシングの歴史

「レイトレーシング」(便宜上、古典的レイトレーシングと呼ぶ事にします。)が考案されたのは1980年代の初頭だったそうです。古典的レイトレーシングは光源から放出される光が物体に直接及ぼす影響しか考慮されていなかったようです。

上記の英文がさす"ray tracing"とは「古典的レイトレーシング」のことなのでしょうか?

実際にCyclesを使ってみるとそうではなさそうだという事が分かります。
Cycles Rendering
Torus Material
Floor Material
床のオブジェクトにトーラスの色が少し映り込んでいるのが分かります。トーラスと床のマテリアル設定はノードの示す通りで光源として設定されている訳ではなく、Diffuseとなっています。つまりCyclesは光源からの直接の影響だけではなく、他の物体が反射した間接的な光も影響するように出来ているようです。このような現象は「間接照明」と呼ばれ、間接照明まで考慮して物体表面の光の挙動をモデル化したものを「グローバルイルミネーション」と呼ぶそうです。

「古典的レイトレーシング」は「間接照明」を考慮していないので「グローバルイルミネーション」には含まれませんが、1984年頃に「古典的レイトレーシング」を発展させる形で「間接照明」まで考慮されたレイトレーシングが考案されたようです。それが「分散レイトレーシング」と呼ばれるアルゴリズムです。「分散レイトレーシング」では1本のレイが物体に入射すると複数のレイが反射されるようです。

しかしこの「分散レイトレーシング」にはレイの数が膨大になる事やその他の欠点があるようで、これらの問題を解消する形で「パストレーシング」というアルゴリズムが1986年頃に考案されました。

その後、1990年代になると視点からのレイも光源からのレイも両方追跡する「双方向パストレーシング」というアルゴリズムが考案されます。

今後書く記事の中で詳しく触れようと思いますが、Cyclesのレンダリング部分のコードを追いかけてみると、kernel_path_traceという名前の関数にたどり着きます。この関数の名前が「パストレース」「双方向パストレース」のどちらを指すのかは今のところわかりませんので後々探っていく事となりますが、ここまでで「レイトレーシングという言葉の指すものが広すぎてもう少し詳しく見ないとCyclesを理解する上で助けにならない」といった意味がおわかりいただけたかと思います。

長い説明になってしまったので、ここまでの私の見解を大まかに図に表しておきます。
RaytraceHistory

レイトレーシングとラスタライジングの違い

さて、ゲームのように秒間に何十フレームもレンダリングする必要のある場合、その技術はリアルタイムレンダリングというカテゴリに分類されて、多くはラスタライジングという手法が採用されます。ラスタライジングはプリミティブ単位でレンダリングする手法です。

一方、何秒・何分・モノによっては何時間もかけて1フレームをレンダリングする事を繰り返し、予め画像や動画などを仕上げておく技術をプリレンダリングと呼びます。レイトレーシングがリアルタイムレンダリングで採用される例は現時点では少なく、多くはプリレンダリングにおいて使われます。

レイトレーシングはラスタライジングと違い、ピクセル単位でレンダリングされます。その事をCyclesのコード上で確認してみましょう。

GPUを使う設定をしていないのであれば、Cyclesでレンダリングをする際に
Sources/cycles_device/Source Files/device_cpu.cppの

for(int y = tile.y; y < tile.y + tile.h; y++) {
    for(int x = tile.x; x < tile.x + tile.w; x++) {
        path_trace_kernel(&kg, render_buffer, rng_state,
                                     sample, x, y, tile.offset, tile.stride);
    }
}

という部分を通ります。

どうやらタイル状に区切ってピクセル単位でレンダリングしているような雰囲気がありますね。レンダリング結果はこうなります。
Default

tile.hとtile.wを2で割ってみたらどのようにレンダリングされているのかわかりやすい絵が出来そうですね。レンダリング結果はこうです。
After
ご覧の通りタイル状に区切られてピクセル単位でレンダリングされています。プリミティブ単位でレンダリングされるラスタライジングとは違うのがわかります。

参考書

レイトレーシングの歴史を調べるにあたり、こちらの本を参考にさせていただきました。

CG Magic:レンダリング
倉地 紀子
オーム社
2007-11

とても親切に書かれている本ですが、学術的な内容であるだけに私にとって容易なものではありませんでした。もし何か間違った解釈をしたままこの記事の中に書いてしまっている箇所があれば、その責任は私にあります。

@fmystB

連載目次

はじめに

@tenntennです。

前回の記事では,Go Mobileのインストールを行い,MacとAndroid上でサンプルを動かしてみました。

前回動かしたexample/basicというサンプルのソースコードを見ると,glというプリフィックスがついたOpenGLの関数を使っていたり, シェーダーを書いたりとOpenGLを直接ガリガリと使っていることが分かります。 しかし,筆者はOpenGLについてあまり詳しくありません。 そこでこの記事では,OpenGLの関数群を直接使うのは最低限に抑え, spriteパッケージを使って,画像の描画や回転,拡大/縮小などを行う方法について説明します。
また,spriteパッケージを扱う上で,Go Mobileのイベントモデルを理解する必要があります。
そのため,この記事ではGo Mobileのイベントモデルとspriteパッケージに深く関係しているペイントイベントについても説明したいと思います。

なお,この記事は2015年7月26日時点の情報を基に執筆しております。 Go 1.5がリリースされる2015年8月には,パッケージ名や使用方法などが変更されている可能性がありますので, あらかじめご了承ください。
また,spriteパッケージは執筆当時,expディレクトリ以下にあり実験的な実装であることが分かります。
そのため,正式版になるころには破壊的な変更が入るおそれがあります。

サンプルを動かしてみよう

まずはサンプルを動かしてみて何ができるのか実際に見てみましょう。 spriteパッケージのサンプルは,Go Mobileのリポジトリのexample/sprite以下で提供されています。
それでは,gomobile buildコマンドを使ってapkファイルを作成しましょう。

$ cd ~/Desktop # どこでもよい
$ gomobile build golang.org/x/mobile/example/sprite

できたapkファイルをAndroid端末にインストールして動かしてみます。

$ adb install sprite.apk

いかがでしょうか?以下のように,Gopherが何かの本を献身的に燃やしているアニメーションが描画されたと思います。

example/spriteをAndroid上で動かしてみた

Gopherは何かの本の山から何冊か本を積んで焼却炉に向かい,本を燃やしてまた本の山に戻っていきます。 Gopherの画像は,手前の焼却炉に近づくたびに大きくなり,奥の何かの本の山に近づくたびに小さくなっています。 このことから,このサンプルでは,画像の描画,移動,拡大/縮小が行われていることが分かります。
この記事では,このサンプルを例に使って画像の描画および移動,拡大/縮小を行う方法について説明を行います。

spriteパッケージ

spriteパッケージは,2Dグラッフィク向けのシーングラフを提供するパッケージです。 シーングラフを構築し,各ノードにテクスチャを貼り付けたり,座標の指定や回転,拡大/縮小を行うことができます。

詳しい説明は後述しますが,spriteパッケージを使って画像を描画するには,以下のような流れでシーングラフを構築し,描画します。

シーングラフの構築と描画は,描画が必要になった際に発生するペイントイベントをハンドルして行います。
画像ファイルの読み込みとシーングラフの構築は,初めてペイントイベントを受信した際にだけ行います。
なお,画像ファイルはテクスチャという形でロードされます。
シーングラフの構築では,ノードの作成と登録を行い,そのノードにテクスチャの一部分をサブテクスチャとして設定し,そして,ノードのサイズや座標などを決めるアフィン変換行列を設定します。
そして,描画を行う前にそのフレームでのノードの配置を決定し,それから描画を行います。

spriteパッケージの概念図

spriteパッケージのgodocを見ると,どのような機能が提供されているかが分かります。 spriteパッケージ自体は,最低限のインタフェースやシンプルな構造体を提供するだけにとどまっています。 具体的な実装を提供しないことで,OpenGL以外でも描画できるように設計されています。

たとえば,sprite.Engineインタフェースを見てみましょう。

type Engine interface {
    Register(n *Node)
    Unregister(n *Node)

    LoadTexture(a image.Image) (Texture, error)

    SetSubTex(n *Node, x SubTex)
    SetTransform(n *Node, m f32.Affine) // sets transform relative to parent.

    // Render renders the scene arranged at the given time, for the given
    // window configuration (dimensions and resolution).
    Render(scene *Node, t clock.Time, c config.Event)
}

sprite.Engineインタフェースは,ノードの登録やテクスチャのロード,サブテクスチャの設定,シーングラフの描画などの機能を提供します。
このインタフェースのOpenGLを使った実装は,sprite/glspriteパッケージで提供されます。
glsprite.Engine関数を呼び出すことで,sprite.Engineインタフェースを実装した値を取得することができます(サンプルでの該当コードはこちら)。

eng := glsprite.Engine() // engはsprite.Engine型

sprite.Engineインタフェースの実装は,sprite/glspriteパッケージ以外にも,sprite/portableパッケージがあります。
sprite/portableパッケージは,描画にimageパッケージを使います。
そのため,シーングラフを画像として書き出すことができます。
たとえば,著者が作ったサンプルでは,
画面(sprite/glsprite)と画像(sprite/portable)に描画し,
画像にはHTTPサーバ経由でアクセスできるようにしています。

portableを使ったサンプル

シーングラフの構築

シーングラフを構築し,Engine.Renderメソッドを呼び出すことでそのシーングラフを描画することができます。
シーングラフのノードはsprite.Node型として提供されています。

type Node struct {
    Parent, FirstChild, LastChild, PrevSibling, NextSibling *Node

    Arranger Arranger

    // EngineFields contains fields that should only be accessed by Engine
    // implementations. It is exported because such implementations can be
    // in other packages.
    EngineFields struct {
        // TODO: separate TexDirty and TransformDirty bits?
        Dirty  bool
        Index  int32
        SubTex SubTex
    }
}

sprite.Node型の各フィールドは,外部に公開はされていますが, それはsprite/glspriteパッケージやsprite/portableパッケージなどのエンジンの実装パッケージからアクセスするためで基本的には直接代入は行いません。
sprite.Engineインタフェースのメソッドや*sprite.Node型のメソッドを介して設定されます。
なお,例外としてArrangerというフィールド(詳細は後述)は直接設定するようになっています。

それでははじめに,ルートノードを作成して,エンジンに登録してみましょう(サンプルの該当コードはこちら)。

scene := &sprite.Node{}
eng.Register(scene)     // engはsprite.Engine型

登録を解除する際はUnregisterを呼び出せばよさそうですが,執筆当時はglsprite.Engine関数が返すエンジンは,Unregisterメソッドを呼ぶとpanicが発生しました。どうやらUnregisterメソッドはまだ未実装のようです。

つぎに,子ノードを追加してみましょう(サンプルの該当コードはこちら)。

n := &sprite.Node{}
eng.Regsiter(n)
scene.AppendChild(n) // sceneが親ノードになる

これでシーングラフを構築することができるようになりました。

画像の読み込み

シーングラフを構築することはできるようになりましたが, このままEngine.Renderメソッドを呼び出してシーングラフを描画しても何も画像を設定していないため,何も表示されません。
そこでつぎに,テクスチャをロードして,ノードに画像を設定する方法を説明します。

example/spriteを見ると,assetsというディレクトリの中にwaza-gophers.jpegという画像ファイルが1つ入っています。
画像ファイルは1つしかありませんが,example/spriteでは,この画像をテクスチャとしてロードして, Gopherや本の山などを切り出して4つのサブテクスチャとしてシーングラフのノードに設定しています。
まずは,テクスチャをロードしてサブテクスチャに切り出すところまでを説明します。
example/spriteでは,以下のloadTexturesという関数でそれを行っています。 なお,番号は説明のために筆者が追加したものです。

func loadTextures() []sprite.SubTex {
    // (1) アセットファイルを開く
    a, err := asset.Open("waza-gophers.jpeg")
    if err != nil {
        log.Fatal(err)
    }
    defer a.Close()

    // (2) 画像をデコードし,image.Image型として読み込む
    img, _, err := image.Decode(a)
    if err != nil {
        log.Fatal(err)
    }

    // (3) エンジンにテクスチャをロードする
    t, err := eng.LoadTexture(img)
    if err != nil {
        log.Fatal(err)
    }

    // (4) テクスチャをサブテクスチャに切り出す
    return []sprite.SubTex{
        texBooks:   sprite.SubTex{t, image.Rect(4, 71, 132, 182)},
        texFire:    sprite.SubTex{t, image.Rect(330, 56, 440, 155)},
        texGopherR: sprite.SubTex{t, image.Rect(152, 10, 152+140, 10+90)},
        texGopherL: sprite.SubTex{t, image.Rect(162, 120, 162+140, 120+90)},
    }
}

(1)では,assetsパッケージのOpen関数を使うことでassetsディレクトリ以下にあるwaza-gophers.jepgという画像ファイルを開いています。
なお,os.Openも使えますが,OSごとにファイルパスが異なるため,assets.Openを使うほうがよいでしょう。
また,assetsパッケージにはファイルを開く関数しか無いため,ファイルを作りたい場合はos.Createなどを使うしかないようです。
AndroidではAndroidManifest.xmlファイルにWRITE_EXTERNAL_STORAGE権限を追加すれば,os.Createを使ってdataディレクトリ以下にファイルを作成することができるのを確認できました。

(2)では,image/pngパッケージやimage/jpegパッケージを使って画像をimage.Image型にデコードしています。
透過PNG画像を描画したい場合は,以下のOpenGLの関数をloadTexturesの中で呼んでおくと良いでしょう。

gl.Enable(gl.BLEND)
gl.BlendEquation(gl.FUNC_ADD)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

(3)で,エンジンにテクスチャをロードしています。 執筆時点ではロードしかできないので,削除はできませんでした。
ゲームなどでシーンが変わって,テクスチャをメモリ上から破棄したい場合は,エンジンごと破棄するしかないようです。
また,エンジンとしてglspriteを使用する場合は,内部で画像サイズが2^n変換されるようです。
なお,テクスチャのロードは最初のペイントイベント(paint.Event)が発生する前に呼び出すとpanic発生します。 ペイントイベントについての詳細は後述します。

(4)では,sprite.SubTexという構造体の配列を返しています。 sprite.SubTex型は,サブテクスチャを表す型で,あるテクスチャの一部分を表しています。 ここでは,テクスチャを指定した座標でサブテクスチャとして4分割にしています。 執筆時点では,テクスチャ画像の作成およびサブテクスチャの分割は手作業でやる必要がありました。 将来的には,go generateを使って渡した画像からテクスチャを作成するツールなどをが出てくることでしょう。なお,一度設定した画像を消したい場合は,空のサブテクスチャ(sprite.SubTex{})を設定すると画像が描画されません。もちろん、設定するサブテクスチャを変える事もできます。

つぎに,切り出したサブテクスチャをノードに設定する部分を見てみます(サンプルの該当コードはこちら)。

eng.SetSubTex(n, texs[texBooks])

Engine.SetSubTex関数を使うと,引数で渡されたノードに対して,サブテクスチャを設定できます。
なお,サブテクスチャを設定するには,ノードがエンジンに登録されている必要があり, 登録していないノードに対して呼び出すとpanicが発生します。 これでノードにサブテクスチャを設定することが出来ました。
しかしこれだけでは,まだノードの大きさが設定されていないため,画面に表示されません。
ノードにアフィン変換行列を設定して,描画する画像サイズを指定しましょう。

画像の移動・回転・拡大/縮小

Engine.SetTransformメソッドを使うことで,ノードにアフィン変換行列を設定することができます。
あるノードの座標や大きさは,親ノードまでの変換結果に,そのノードの変換行列を掛け合わせることで求められます。
ルート(根)となるノードは,サイズは1pt x 1ptで,座標は(0, 0),回転角度は0度を基準として,変換行列を掛けます。
そして,ノードにサブテクスチャが設定されている場合は,ノードの大きさや座標,回転角度を基に描画が行われます。
なお,example/spriteでは,sceneというノードがルートノードにあたります。

また,ptという単位は,フォントで使われるptと同様で,1pt = 1/72 inchとなり,geom.Pt型で定義されています。 1ptが何ピクセルを表すかは端末ごとに違い,geom.Pt.Pxメソッドで取得できます。

それでは,Engine.SetTransformメソッドを使った例を見てみましょう(example/spriteには該当箇所はありません)。

parent := &sprite.Node{}
eng.Register(parent)
eng.SetTransform(parent, f32.Affine{
    {2, 0, 5},
    {0, 2, 5},
})

child := &sprite.Node{}
eng.Register(child)
parent.AddChild(child)
eng.SetSubTex(child, tex)
eng.SetTransform(child, f32.Affine{
    {100, 0, 10},
    {0, 100, 10},
})

この例では,parentノードに設定された変換行列で,2倍の大きさになり,さらに座標は(5, 5)に移動しています。 子のchildノードでは,さらに100倍,座標も(10, 10)だけ移動します。
親の変換行列にさらに子の変換行列を掛けるため,最終的に画像は200pt x 200ptの大きさで,(25, 25)の位置に描画されます。
つまり,親ノードで2倍になっているため,子のノードで移動距離を(10, 10)に設定すると,実際は(20, 20)だけ移動します。

アフィン変換行列を表すf32.Affine型のポインタ型には,いくつか便利なメソッドが用意されています。
変換行列を生で書くのが辛い場合は,*f32.Affine.Scaleメソッドや*f32.Affine.Rotateメソッド,*f32.Affine.Translateメソッドなどを使うと良いでしょう。
メソッドのレシーバがf32.Affine型のポインタとなっているため少々使いづらいですが,直接変換行列を書くよりかは分かりやすいはずです。

イベント

ここまで,いくつかイベントという言葉を使ってきました。 ここでは,Go Mobileのイベントモデルと各イベントについて簡単に説明します。 Go Mobileでは以下のようなイベントを扱うことができます。 なお,かっこ内は対応するイベントの型です。

  • 描画イベント(paint.Event
  • 設定イベント(config.Event
  • ライフサイクルイベント(lifecycle.Event
  • タッチイベント(touch.Event
  • マウスイベント(mouse.Event
  • キーイベント(key.Event

執筆時は,キーイベントやマウスイベントはまだ実装中のようでした。 おそらくモバイル用というよりはPC用のイベントのようです。 キーイベントはAndroidのバックボタンを特定のキーでシミュレートしたりと,デバッグ用途に使えそうです。

Go Mobileでは,app.Main関数を用いて,イベントループを構築します。
通常,app.Main関数はmain関数内で呼び出すことが多いでしょう。 app.Main関数の引数には,func(app.App)型の関数を渡します。

func main() {
    app.Main(func(a app.App) {
        // ...
    })
}

この関数で渡されるapp.Appインタフェースは,以下のように定義されています。

// App is how a GUI mobile application interacts with the OS.
type App interface {
    // Events returns the events channel. It carries events from the system to
    // the app. The type of such events include:
    //  - config.Event
    //  - lifecycle.Event
    //  - mouse.Event
    //  - paint.Event
    //  - touch.Event
    // from the golang.org/x/mobile/event/etc packages. Other packages may
    // define other event types that are carried on this channel.
    Events() <-chan interface{}

    // Send sends an event on the events channel. It does not block.
    Send(event interface{})

    // EndPaint flushes any pending OpenGL commands or buffers to the screen.
    // If EndPaint is called with an old generation number, it is ignored.
    EndPaint(paint.Event)
}

各イベントは,app.App.Eventsメソッドで取得できるチャネルを通じて送られてきます。
一般的には,以下のようにswitch-case文で各イベントをハンドリングします(サンプルでの該当コードはこちら)。
app.Filter関数を通すことにより,各イベントの前処理などをやっているようです。
なお,for文のrangeの後にチャネルを書くとチャネルから送られてくるデータを変数に入れループを回してくれます()。

func main() {
    app.Main(func(a app.App) {
        for e := range a.Events() {
            var c config.Event
            switch e := app.Filter(e).(type) {
                case config.Event:
                    c = e
                case paint.Event:
                    onPaint(c)
                    a.EndPaint(e)
            }
        }
    })
}

それでは,各イベントについて説明していきたいと思います。 この記事では,config.Eventpaint.Eventについて説明し,他のイベントについては次回以降の記事で説明したいと思います。

config.Eventは,画面サイズが変わった場合などに送られてきます。PCだとウィンドウサイズが変わるごとにイベントが発生しますが,端末だと画面が回転した場合などに呼ばれるようです。
config.Event構造体からは,画面のサイズが取得できます。

// Event holds the dimensions and physical resolution of the app's window.
type Event struct {
    // WidthPx and HeightPx are the window's dimensions in pixels.
    WidthPx, HeightPx int

    // WidthPt and HeightPt are the window's dimensions in points (1/72 of an
    // inch).
    WidthPt, HeightPt geom.Pt

    // PixelsPerPt is the window's physical resolution. It is the number of
    // pixels in a single geom.Pt, from the golang.org/x/mobile/geom package.
    //
    // There are a wide variety of pixel densities in existing phones and
    // tablets, so apps should be written to expect various non-integer
    // PixelsPerPt values. In general, work in geom.Pt.
    PixelsPerPt float32
}

paint.Eventは,描画が必要になった場合に送られてきます。
上述のとおり,テクスチャをロードはpaint.Eventが送られてきた後に行う必要があります。
パフォーマンスのことを考えると,テクスチャのロード以外は事前に済ませておくと良いでしょうが, はじめはひとまず,テクスチャの読み込みおよびシーングラフの構築は初めてpaint.Eventが送られてきたときに行うと良いでしょう(サンプルでの該当コードはこちら)。

シーングラフの描画は,sprite.Engine.Renderメソッドを呼び出すことで行なわれます。
sprite.Engine.Renderメソッドの引数は,シーングラフのルートノード(*sprite.Node),時刻(clock.Time),config.Eventです。
引数に時刻を渡していることから分かるように,sprite.Engine.Renderメソッドはある特定の時刻のシーングラフの状態を描画します。
時刻よって座標などが変化するノードのある時刻の状態を決定するには,ノードにsprite.Arrangerインタフェースを設定している必要があります。

type Arranger interface {
    Arrange(e Engine, n *Node, t clock.Time)
}

sprite.Arrangerインタフェースの定義を見ると,Arrangeというメソッドを持っており,その引数に時刻(clock.Time)を取ることがわかります。sprite.Nodeに設定されたsprite.Arrangerインタフェースは,sprite.Engine.Renderメソッドが呼び出された際に,sprite.Engine.Renderの引数として渡された事項をそのまま渡し,実行されます。

example/spriteにおいても,Gopherの座標を決めるためにsprite.Arrangerインタフェースを設定してます(該当箇所はこちら)。

n.Arranger = arrangerFunc(func(eng sprite.Engine, n *sprite.Node, t clock.Time) {
    // TODO: use a tweening library instead of manually arranging.
    t0 := uint32(t) % 120
    if t0 < 60 {
        eng.SetSubTex(n, texs[texGopherR])
    } else {
        eng.SetSubTex(n, texs[texGopherL])
    }

    u := float32(t0) / 120
    u = (1 - f32.Cos(u*2*math.Pi)) / 2

    tx := 18 + u*48
    ty := 36 + u*108
    sx := 36 + u*36
    sy := 36 + u*36
    eng.SetTransform(n, f32.Affine{
        {sx, 0, tx},
        {0, sy, ty},
    })
})

なお,arrangerFuncは以下のように定義されている,sprite.Arrangerインタフェースを実装する型です(サンプルでの該当コードはこちら)。
なお、関数によるインタフェースの実装については,筆者のQiitaの投稿で解説しています。

type arrangerFunc func(e sprite.Engine, n *sprite.Node, t clock.Time)

func (a arrangerFunc) Arrange(e sprite.Engine, n *sprite.Node, t clock.Time) { a(e, n, t) }

// TODO: use a tweening library instead of manually arranging.とコメントあるように,sprite.clockパッケージの中にtween.goというファイルがあり,補間を行う関数が提供されています。

paint.Eventが発生し,その後sprite.Engine.Renderメソッドを呼び出しても実際にはまだ描画はされません。
そのフレームでのすべての描画に関する処理が終わったらapp.App.EndPaintメソッドを呼ぶ必要があります。
EndPaintメソッドが呼び出すと画面など(sprite.portableは画像)に描画結果が出力されます。

今回は,spriteパッケージの簡単な説明とGo Mobileにおけるイベントモデルの説明を行いました。
次回は今回扱わなかったtouch.Eventlifecycle.Eventについて説明する予定です。

↑このページのトップヘ