KLabGames Tech Blog

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

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

頂点が頻繁に更新されるのはEditモードだよね

前回の記事の続きです。

前回はObjectモード時のレンダリングはGPUで行われている事を突き止めましたが、今回は頂点データが頻繁に更新されてメインメモリ<ー>GPUメモリ間のデータ転送コストが毎回発生するであろうEditモードのレンダリングについて調べてみます。Editモードのレンダリングに使われているのはCPUでしょうか?GPUでしょうか?

OB_MODE_EDITで探るもレンダリング命令まで行き着かず

Objectモードのときはdrawobject.cの中でObjectモードを示すenum値:OB_MODE_OBJECTの付近を探っていたら運良くレンダリング命令の部分を引き当てる事に成功しましたが、今回はEditモードを示すOB_MODE_EDITの付近を探ってみるも、レンダリング命令に行き着きませんでした。

しかしdrawobject.cの中に目的の部分がある可能性は捨てず、別の方法で探す事にしました。いろいろ検索条件を変えて絞り込んでいきましょう。

Objectモードのときと同じようにdrawFacesSolidが答えか?

ObjectモードのときはdrawFacesSolidという関数ポインタがドローコールにつながっていたので、Editモードのときも同じようにViewport ShadingがSolidの場合は関数ポインタdrawFacesSolidがドローコールにつながっているかもしれません。

drawobject.cの中で関数ポインタdrawFacesSolidがコールされている場所は8カ所。試しに8カ所全てコメントアウトしてみるも、Editモードの時のモデルの面が消える事はありませんでした。

drawFaces〜〜〜かな??

ついでに「drawFaces」でdrawobject.c内を検索してみましょう。

ヒットは11件で8件が先ほどのdrawFacesSolid。1件はenum値なのでこれは関係なし。残る2件はdrawFacesGLSL(GLSLはOpenGLのシェーダ言語)という関数ポインタで、EditモードはデフォルトでSolidシェーディングなのでGLSLが使われる事もなさそうだから違うだろうなと思いつつ2件ともコメントアウトしてみるもやはり該当しませんでした。

試行錯誤

「draw」ならどうか?ヒットは1311件。これは調べきれません。

Objectモードのときと同じように構造体内の関数ポインタになっていると予想できるので、「->draw」で検索してみると135件。まだ手探りで該当箇所を探るのはキツい件数ですね。

Xcodeはパターン検索が出来るので「->draw〜〜〜(」という形で検索して絞り込む。すると55件。もう少し絞りたい!この55件の〜〜〜の中には「Verts」(頂点)や「Edges」(辺)という文字列が含まれています。でも狙いは「Faces」(面)なので「->draw〜〜〜Faces」で検索。すると12件まで絞れました。これくらいの数であれば1つ1つ手で調べていけますね!この12件の中に答えがあるか!?

容疑者:drawMappedFaces

12件のうち1件は関数でも関数ポインタでもないので除外。そして10件は関数ポインタdrawMappedFaces、1件は関数ポインタdrawMappedFacesGLSLです。Objectモードのときもdraw〜〜〜という関数ポインタがレンダリング命令につながっていたので、この11件の中のどれかがEditモードの面のレンダリングにつながっているかもしれません。

どの関数がdrawMappedFacesまたはdrawMappedFacesGLSLを呼び出しているかを列挙してみると、

  • draw_dm_faces_sel() 1件
  • draw_em_fancy() 3件
  • draw_mesh_fancy() 1件
  • bbs_mesh_solid_EM() 2件
  • bbs_mesh_solid_verts() 1件
  • bbs_mesh_solid_faces() 2件
  • draw_object_mesh_instance() 1件

となっていました。

Objectモードのときはdraw_mesh_fancy()がdrawFacesSolid()を呼び出していてレンダリング命令につながっていましたね。試しにdraw_mesh_fancy()の中のdrawMappedFaces()をコメントアウトして実行してみましたが、Editモードの面のレンダリングには関係ないようです。

次はdraw_mesh_fancy()と同じように「fancy」の付くdraw_em_fancy()辺りが怪しいでしょうか?3件ともコメントアウトしてみます。
before
↓↓↓
after
Editモードの面が消えましたね!!結果、3つの関数ポインタdrawMappedFacesのうち最後が今回探している起動時のデフォルト状態からEditモードに切り替えた直後の面のレンダリング部分につながっているようです。

関数ポインタが指す関数本体は?

さて、狙いの関数ポインタdrawMappedFacesも見つかった事ですし、ステップ実行してこのdrawMappedFacesが指し示している関数本体を探してみましょう。するとdrawMappedFacesはeditderivedmesh.c内のemDM_drawMappedFaces()を指し示している事がわかります。

emDM_drawMappedFaces()の中ではやはりOpenGLのレンダリング命令が使われており、GPUレンダリングが行われているようです。CPUか?GPUか?答えが出ましたね!

おまけ

ObjectモードのときはglDrawArraysで頂点データの格納された配列の中身を一気にレンダリングしていました。

Editモードのときはこれと違い、for文の中で三角形を1つずつレンダリングしているようです。きっと頻繁な頂点の追加&削除の負荷に耐えるためでしょう。

試しにfor文のカウントの上限値から1を引いてみたら三角形が1つ欠けたりするんでしょうか?
before
三角形が1つ消えましたね^^


@fmystB

読者の皆さまが普段使っているバージョン管理システムは何でしょうか?多くの会社さんと同様、KLabでは大多数のプロジェクトでGitを利用しています。

Gitでは全てのcommitについて名前とメールアドレスが記録されます。ところで、Git管理しているリポジトリ上で会社のメールアドレスと個人のメールアドレスが混ざることがありませんか?

KLab社内では大半のプロジェクトでGitHub Enterpriseを利用している一方、一部プロジェクトや公開用のリポジトリについてはgithub.comも併用しており、それぞれで登録メールアドレスが異なっていたりするため、間違いが起こりやすい状況になっています。

本稿では、そんなときでもリポジトリごとに適切なメールアドレスでcommitできるような~/.gitconfigの書き方を紹介します。

具体的な手順

今回紹介する手順は、リポジトリをgit cloneするタイミングでuser.nameおよびuser.emailを自動的にgit config --localで設定するようにした、というものです。また、その際の設定値をスクリプト側に書かず、~/.gitconfigに書けるようにしました。

手順としては、まず~/.gitconfigを次のように変更または追記します。

[init]
        templatedir = ~/.git_templates/
[user]
        name = Git Hanako
        email = git@example.com
[user "ssh://git@ghe.example.jp/"]
        email = hanako@ghe.example.jp
[user "https://ghe.example.jp/"]
        email = hanako@ghe.example.jp

ただし、[user]ブロックに書くメールアドレスはデフォルトのメールアドレス、[user <URL>]ブロックに書くメールアドレスはリポジトリのURLに対応するメールアドレスです。

次に、この設定通りにGitのローカル設定を変更するためのフックスクリプトを設置します。次のように~/.git_templates/hooks/post-checkoutを作成します。

#!/bin/sh

PREVIOUS_HEAD=$1
NEW_HEAD=$2
BRANCH_SWITCH=$3
Z40="0000000000000000000000000000000000000000"

# Continue only when "git clone" has been executed.
if [ "$PREVIOUS_HEAD" != "$Z40" -o "$BRANCH_SWITCH" != "1" ]; then
  exit
fi

origin_name="$(git remote | head -1)"
current_remote_url="$(git config --get --local remote.$origin_name.url)"
default_name="$(git config --get user.name)"
default_email="$(git config --get user.email)"
local_name="$(git config --local --get user.name)"
local_email="$(git config --local --get user.email)"

if [ "$current_remote_url" ]; then
    case $current_remote_url in
        *://*)
            # Normalize URL: remove leading "git+"
            #   e.g. "git+ssh://user@host/path/" ==> "ssh://user@host/path/"
            current_remote_url=$(echo $current_remote_url | sed 's/^git\+//')
            ;;
        *:*)
            # Convert scp-style URL to normal-form
            #   e.g. "user@host:path/" ==> "ssh://user@host/path/"
            current_remote_url=$(echo $current_remote_url | sed 's/\(.*\):/ssh:\/\/\1\//')
            ;;
    esac
    if [ -z "$local_name" ]; then
        name="$(git config --get-urlmatch user.name $current_remote_url)"
        if [ "$name" != "$default_name" ]; then
            git config --local user.name "$name"
        fi
    fi
    if [ -z "$local_email" ]; then
        email="$(git config --get-urlmatch user.email $current_remote_url)"
        if [ "$email" != "$default_email" ]; then
            git config --local user.email "$email"
        fi
    fi
else
    echo "No remote URL"
fi

こうすることで、git cloneのタイミングでuser.nameuser.emailが必要に応じて設定されるというわけです。

ただし、git initしてgit remote addしたような場合には対応できないので、注意が必要です。

~/.gitconfig にはURLごとの設定が書ける

スクリプトだけ紹介しても寂しいので、仕組みも紹介します。

Gitの設定は~/.gitconfigに記述できますが、 Git 1.8.5からはリポジトリURLごとの設定が書けるようになりました。git-configのman pageには次のような例が掲載されています。

; HTTP
[http]
    sslVerify
[http "https://weak.example.com"]
    sslVerify = false
    cookieFile = /tmp/cookie.txt

これはリポジトリのホスト名によってhttp.sslVerifyを勝手に切り替えるような例です。設定値によっては、このように書くだけで勝手に設定を使い分けることができます。

一方で、user.emailなどの設定はURLに紐付いているわけではないため、~/.gitconfigを設定しただけでは反映されません。とはいえ、同じ枠組みを利用すればシンプルに設定が記述できますし、git config--get-urlmatchオプションを使えば簡単に値が取り出せますから、今回はこれをgit cloneのフックスクリプトから利用したというわけです。

この仕組みを使えば、特定オーガナイゼーションのみ、もしくは特定リポジトリのみといった設定も可能ですので、他にも応用ができそうですよね。便利な使い方を思いついた方はぜひ教えてください!


@hnw

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

CPUなのか?GPUなのか?

前々から疑問に思っていた事がありました。Blenderの画面に表示されている3Dモデル、GPUでレンダリングされているのでしょうか?それともCPUでソフトウェアレンダリングされているのでしょうか?

DCCツールはゲームと違い、モデルの頂点が頻繁に編集されるのでメインメモリからGPUメモリへのデータ転送コストが毎回発生するはずなのでこのような疑問を持ちました。当然マシン環境やDCCツールの機能によってもどちらが適切なのかは変わってるくるはずですが、Blenderはどのような実装になっているのでしょう?

レンダリング部分のコードを突き止める

この疑問の答えを出すためにレンダリング部分のコードを探し出してみましょう。

OpenGLのドローコールが呼ばれていればGPUでレンダリングされていると見ていいでしょうし、CPUでピクセルの配列の色を決定しているようなコードが見つかればソフトウェアレンダリングが使われているということになります。

最終的に該当部分をコメントアウトしてみて、3Dモデルが消えればそこが答えだと証明できるはずです。

part1ではBlenderを立ち上げて何も触らない状態、要するにObjectモードでViewport ShadingはSolidの状態でのレンダリングについて調べてみます。

試しにそれっぽいキーワードでプロジェクト全体を検索してみる

レンダリング部分のコードを見つけ出したいので、手始めに「render」や「draw」でソースコード全体を検索してみます。
  draw -> 21478 results in 773 files
  render -> 15695 results in 802 files
・・・。まあ、わかってはいましたけど大量に引っかかりますよね・・・。これを1つ1つ見ていくのは人間には無理でしょう。

Viewport ShadingがSolidの状態なので「solid」ではどうか?
  solid -> 1484 results in 206 files
これもやっぱり数が多い。

Objectモードなので、「objectmode」ならどうか?
  objectmode -> 42 results in 12 files
おお!これくらいなら1つ1つ見ていく事も可能ですね!

手がかりになりそうなenum値が見つかる

するとこんなenum値が見つかります。

typedef enum ObjectMode {
    OB_MODE_OBJECT        = 0,
    OB_MODE_EDIT          = 1 << 0,
    OB_MODE_SCULPT        = 1 << 1,
    OB_MODE_VERTEX_PAINT  = 1 << 2,
    OB_MODE_WEIGHT_PAINT  = 1 << 3,
    OB_MODE_TEXTURE_PAINT = 1 << 4,
    OB_MODE_PARTICLE_EDIT = 1 << 5,
    OB_MODE_POSE          = 1 << 6,
} ObjectMode;  

Blenderを使っている方ならお気付きかと思いますが、
Mode
モード選択といかにも関係ありそうなenum値ですね。

このenum値の中でObjectモードを指し示しているであろう「OB_MODE_OBJECT」で検索してみましょう。
 OB_MODE_OBJECT -> 32 results in 14 files

いかにも怪しいファイルが

この14個のファイルの中にdrawobject.cといういかにも怪しいファイルがあるのでこれを見てみる事にします。

drawobject.cの中にはdraw_〜〜〜と名前のついた関数がたくさん存在します。この関数の中に該当するものがありそうです。

レンダリング命令の周りのコードにOB_MODE_OBJECTの記述があるかは不明ですが、draw_object()とdraw_mesh_fancy()内にのみOB_MODE_OBJECTが書かれているのでとりあえず追ってみます。

この2つの関数の先頭にブレイクポイントを張って実行してみると、2つとも引っかかるようです。

ここでわかるのは、  draw_object() -> draw_mesh_object() -> draw_mesh_fancy() という構造で各関数が呼ばれている事です。

draw_object()でdraw_mesh_object()を呼んでいる場所のswitch文は

        switch (ob->type) {
            case OB_MESH:
                empty_object = draw_mesh_object(scene, ar, v3d, rv3d, base, dt, ob_wire_col, dflag);

となっており、OB_MESHの定義に飛んでみるとオブジェクトのタイプ(??)がいくつか定義されているのでこれは狙っているオブジェクトのレンダリング部分につながっているかもしれません。

試しにこのempty_object = draw_mesh_object(...の行をコメントアウトして実行してみましょう。するとデフォルトで表示されているはずのキューブが丸ごと消えます!
default
↓↓↓
draw_mesh_object
このコメントアウトを取り消して行を復活させ、今度はdraw_mesh_object()でdraw_mesh_fancy()を呼んでいる場所をコメントアウトしましょう。すると同じくキューブが丸ごと消えます。

オブジェクト選択時のオレンジ色のアウトラインも一緒に消えてしまうので、アウトラインのレンダリングもdraw_mesh_fancy()に含まれていると言えます。

ゴールは近い!!

確認の意味も含めて、アウトラインを残してオブジェクトの面のみを消してみたいですね。

面の色の塗り方はViewport Shadingの設定によって変わります。ですので全体検索では大量に引っかかってしまった「solid」で今度はdraw_mesh_fancy()の中を検索してみます。するとヒットするのは9カ所。これくらいであれば1つ1つ追っていくのは簡単ですね。

9カ所全て見ていくと、drawFacesSolid()といういかにもそれっぽい関数が4カ所で呼ばれているのがわかります。drawFacesSolid()の呼ばれている箇所を上から順に見ていくと、
 1番目:enum値OB_MODE_TEXTURE_PAINTを使ってオブジェクトのモードを判断しているif文内にあるので該当しなさそう
  (2番目〜4番目はOB_SOLIDが条件に使われているif文内にあるので怪しい)
 2番目:if (draw_flags & DRAW_MODIFIERS_PREVIEW)という条件なので違いそう
 3番目:if文の条件を見るとスカルプトに関連していそうなので除外
 4番目:if文的に2番目ではないかつ3番目ではないのでこれが正解のはず。
よって4番目をコメントアウト。
goal
オレンジのアウトラインを残し、面のみが消えました!!
さて、コメントアウトをもとにもどします。

drawFacesSolidは関数ポインタになっているので、実際に呼ばれる関数を追ってみます。

4番目のところにブレイクポイントを張り、Step into。

するとcdderivedmesh.cの中の

static void cdDM_drawFacesSolid(DerivedMesh *dm,
                                float (*partial_redraw_planes)[4],
                                bool UNUSED(fast), DMSetMaterial setMaterial)

にたどり着きます。

この中でOpenGLの命令であるglDrawArrays()が呼ばれているので、Objectモードに関してはソフトウェアレンダリングではなく、GPUを使ってレンダリングを行っている事がわかって当初の疑問が解消され・・・

いや、待てよ

ObjectモードについてはGPUレンダリングが行われているのがわかりました。

でも最初に書いた「モデルの頂点が頻繁に編集されるのでメインメモリからGPUメモリへのデータ転送コストが毎回発生する」のは主にEditモードでした。

というわけで、part2につづきます。


@fmystB

↑このページのトップヘ