読者です 読者をやめる 読者になる 読者になる

GameJamで覚えるタスクボード(カンバン)

セガ・インタラクティブ 第三研究開発部 プログラマの石畑と申します。よろしくお願い致します。
今までアーケードゲームのタイトル開発(電脳戦機バーチャロン・シリーズ、デカスリート、パワースマッシュ3、WORLD CLUB Champion Football(WCCF)、CODE OF JOKER(COJ) )を中心に仕事をしております。
最近はマネージャとしての仕事をしつつ、社内アジャイル開発コミュニティの運営も行っております。


今回は、業務の改善に繋げるための見える化を行う手法「タスクボード」についてお話ししてみましょう。


内容は以下の6つになります。

タスクボードは、業務の見える化の代表的な手法として簡単に導入できるのですが、プロジェクトで使おうとすると、そのタスク量の多さから踏み切れないケースも多いです。
そこでGameJamでタスクボードを試しながら、実際のプロジェクト「WCCF」「CODE OF JOKER」に活用していき、その中でも改善を続けていった事例をご紹介します。

見よう見まねでプロジェクトでタスクボードを使い始めたのですが最初はうまくいかず、最初のGameJamでもうまくいかなかったものの、GameJamの回数をこなす毎に洗練され、プロジェクトへの導入、改良を進めることができるようになりました。


実際の時系列では、GameJam → プロジェクト → GameJam → プロジェクト と、交互に行っているのですが、まとまりに配慮してGame Jam、実プロジェクトという順に今回は解説していきます。


それでは1つずつみていきましょう。

タスクボード(カンバン)は、仕事を見える化するツール

皆さんは自分たちの周囲の改善をどのように進めているでしょうか?
改善をするためには、まず問題を明らかにしなければ改善する所自体が見つかりません。


では問題を明らかにする方法にはどんなものがあるでしょうか?
自分達でいろいろ考えても、なかなか何が問題なのかわからないことも多いでしょう。
問題を発見するのに役立つのは、問題だと考えている周辺を「見える化」することです。


見える化」の手法は様々ありますが、仕事の進め方で問題を感じていたならば、タスクボードを使うことで見える化ができます。


タスクボードを使うメリットには、以下のようなものがあります。

  • 残作業が一目瞭然になる
  • 優先順位通りに進められるようになる
  • 誰が何をしているのか見えるようになる
  • 作業の流れが見えるようになる


このように業務が見える化されていけば、問題がどこにあるのか発見することができ、そこから改善を始めることができます。
タスクボードの運営で気をつけることは、業務の見える化により問題は見えるようになりますが、タスクボードは問題の解決方法は示してくれません
見つかった問題を解決する方法を考えるのは、チーム自身です。問題は見える化されて共有されているので、皆で話し合いながら改善していきましょう。


それではタスクボードの使い方の紹介です。

タスクボード(カンバン)の使い方

まずタスクボードとはどんなものかを簡単に説明します。


ここで扱うタスクボードとは、実現したいものを機能そしてタスクに分解し、そのタスクがどのような状態にあるかを「見える化」するツールです。
カンバン、カンバンボード、タスクかんばん、アジャイルかんばんなど、いくつも名称があります。
トヨタ生産方式でのカンバン、システムとしてのカンバン、目的によって使い方も変わるのですが、ここでは「見える化」としてのツールとして使うことについて扱います。


使い方は単純です。
例えばあるゲームを作るとします。
使い方の流れは以下になります。


1. ゲームを作るのに必要な要素、機能をリストアップします。
2. 要素、機能を実現するために必要な作業をタスクとしてリストアップします。

3. ホワイトボードや、模造紙、イーゼルパッドなどに「ToDo」「Doing(In Progress)」「Done」の3つの列を用意します。
 この時に「Done(作業完了)」の定義をしておきます。レビューができる状態になったら、バグがあっても「Done」とする等です。

【付箋の例】

4. リストアップしたタスクを付箋紙に書いて、「ToDo」の列に貼り付けます。優先順位が高いタスクを上に配置しましょう

5. 付箋に書かれたタスクに手を付け始めたら、付箋に自分の名前を書いて「Doing」の項目に移動します。

6. 作業が完了したら(3.で決めた状態)、付箋を「Done」に移動します。

7. 全部「Done」になっていたら、ゲームは完成です。


上の4,5,7の写真は、2016年7月開催「第九回SEGA Game Jam」で使ったタスクボードです。

タスクボードの作成は作業開始前にやらないといけないものではありません。
プロジェクトの中盤でも終盤でも、また一部のパートだけでも、いつでもどこでもどんな状況でも導入してください。


ゲームを作る際に、皆さんがいつもやっている作業なので難しいことはないはずなのですが、実行しようとするとなかなか難しいものです。
例えば、プリプロ作成の場合だと、(1)の段階でリストアップしきれなかったり、大規模プロジェクトでは、(2)でタスクを全てリストアップすることがあまりに多くて難しいなどです。


私も実際にいきなりプロジェクトにタスクボードを導入できたわけではありません。
タスクの粒度がバラバラだったり、更新頻度が不定期になったりしてうまく活用できていませんでした。
ワークショップや小さいプロジェクトで練習ができないものかと考えて、活用したのがGameJamです。


GameJamとは、短期間で即席チームによりゲームを制作するイベントです。
短期間ですので、結果がすぐにわかりますし、失敗してもダイジョーブ!な安全なイベントなので、実験には最適です。


以下では、私が実際にGameJamでのタスクボードを活用した事例を紹介します。

GameJamでのタスクボード運用例

初回:Global Game Jam 2012
f:id:sgtech:20170522102324j:plain
やってみたこと

  • ToDo 機能リスト作成

最初のGameJamでは、ゲームに必要な機能を付箋にリストアップしていきました。
ただし、粒度がそろわず、タスクに分解できていない項目が多く残りました。

結果としてToDoリストとして使うことはできましたが、作業が未着手なのか、完了したのか共有されておらず、運用している状態ではありませんでした。
最初のタスクボードはここからスタートでした。
振り返り
振り返りでは、タスクに分解する粒度をそろえること、進捗管理の方法が改善点としてあがりました。


2回目:福島GameJam 2012
f:id:sgtech:20170522102524j:plain

やってみたこと

  • ToDo 機能リストからタスクへ分解
  • Doneの定義(コミットしたらDone)

2回目では、実装する機能をタスクまで分解することでタスクの粒度の問題が改善し、そして完了Doneを意識して付箋をToDo→Doing→Doneと管理して、タスクボードの運用を開始することに成功しました。
ただし、忙しさのあまり終盤で進捗確認の頻度が低くなり、リリース直前に何が入っていて、何が入っていないかわからなくなり、うまく運用したとはいえなくなりました。
振り返り
振り返りでは、どうやって最後まで定期的な進捗管理として機能していくかが課題となりました。


3回目:Global Game Jam 2013
f:id:sgtech:20170522102502j:plain

【マイルストーン】
f:id:sgtech:20170522102633j:plain
やってみたこと

  • マイルストーン設定
  • マイルストーン毎にレビュー会開催

3回目では、マイルストーンを設定し、マイルストーンのタイミングでレビュー会を行い、ここで進捗確認と、次のマイルストーンまでに何を行うかを明確にするように組み合わせてタスクボードを運用することで、定期的に進捗管理のタイミングが生まれ、時間を意識したタスク管理ができるように改善されました。


ここで始めてタスクボードによる運営が成功したといえるでしょう。


4回目以降:Global Game Jam 2017
f:id:sgtech:20170522102626j:plainf:id:sgtech:20170522102619j:plain

【レビュー会】
f:id:sgtech:20170522102610j:plain

やってみたこと

  • 職種別に付箋の色分け
  • 付箋に担当者を記入
  • 付箋にいつまでに終わらせるか期限を記入

4回目以降では、タスクの分解、職種別に色分け、担当者の記入、レビュー会を意識したタスクの割り振りにより、小さな成功を積み重ね、モチベーション維持しながら毎回最後まで運用できるようになっています。


結果として、タスクボードを運用できるようになるまでに3回かかっており、これを実際のプロジェクトで試そうとすると、チャンスを与えてもらえないと何年も改善しないまま進むことになります。
GameJamは短期間でプロジェクトが終わるので、最高に安全な場であり、今回の例のようなタスクボードだけでなく他にもチーム運営、技術などの導入、改善に最適です。
ぜひお試しください。

プロジェクト(WCCF)でのタスクボード運用例

GameJamで練習したら次は、プロジェクトでの活用です。
実際にプロジェクトでの運用をどのように進めていったかの事例を以下に紹介します。

WCCF2010-2011 Ver.2.0
f:id:sgtech:20170522102604j:plain
やってみたこと

  • ネットワークパート内で、1人でテスト
  • ToDo は、タスクではなく実装する機能

プロジェクトでの最初は、ネットワークパート内でタスクの一部を管理するスモールスタートでの導入開始にしました。
ほぼ個人での活用だったため、他の人にはほとんど意識されず、改善にはつながらないタスクボードとなりました。
それでも初回のGameJamの経験があったので、運用の形にはなりました。
振り返り
振り返りとしては、多少なりとも周りを巻き込まないと、見える化の浸透のしようもないことが明確になっています。


WCCF2011-2012

f:id:sgtech:20170522102951j:plain
やってみたこと

  • エンジニア全体でタスクボード運用の対象にする
  • ToDo は、機能から分解したタスクにまで分解する
  • タスクには、工数を入力する
  • 工数を合計して、バーンダウンチャートを作成

自分が直接関わるエンジニアには、タスクボード運用に参加してもらうように協力をとりつけ、見える化の範囲を広げることができています。
2回目のGameJam経験から、最初から欲しい機能を担当者が作業可能になるところにまでタスクを分解することで、粒度が統一され、タスクボードの運用として改善しております。
この時点で付箋には「工数」が入力されています。
これによりバーンダウンチャートが導入可能になっており、進捗管理に関しては大きく改善することができました。

バーンダウンチャートとは、縦軸に残作業工数、横軸に時間を設定したグラフです。タスクがDoneになったら、その工数の数字分残作業量から減らたところにプロットしていくと、グラフの傾きからマイルストーンまでに作業が消化できるかどうかが明確にすることができます。

ただし、実装内容などの見直すポイントはわかるようになったものの、見直す際の優先順位が明確でなく、結局作業がある程度進んだ後で優先順位を変えることになってしまいました。
振り返り
振り返りとしては、優先順位の再確認の時期をどう設定するか、という改善点が残ることになりました。


WCCF2011-2012 Ver.2.0

f:id:sgtech:20170522103100j:plain
やってみたこと

  • タスクボードの運用を、プランナー、デザイナーにも広げる
  • 優先順位の確認を2ヶ月に1回実施する

3回目では、バーンダウンチャートと合わせてタスクボードを運用すること、タスクボードをプランナー、デザイナーの分も書き出すことで、プロジェクト運営として全体をコントロールできるように改善しました。
これは3回目のGameJamでの経験から、チーム全体で活用しないと、プロジェクト全体をみていることにならないと気付いたため、全体に適用しています。
優先順位も定期的に確認することで改善しています。
ただし、タスクボードの適用範囲をチーム全体に拡大したため、管理するタスク量がかなり多くなることで目が行き届かなくなり、手戻りが発生するという問題が発生しました。また工数だし時には明確になっていない要素が、作業を始めると予想以上に多く工数が足りなくなり、そのために見送った機能が出ています。
振り返り
振り返りとしては、安定して開発ができるようになったものの、お客様の満足度が向上していない点が改善したいこととしてあげられています。


WCCF2012-2013
f:id:sgtech:20170522103055j:plain
f:id:sgtech:20170522103049j:plain

【レビュー会の様子】
f:id:sgtech:20170522103146j:plain

やってみたこと

  • 工数出しに、プランナー、デザイナーに参加してもらう
  • レビュー会を月に1回実施する

4回目では、工数出しにプランナーにも参加してもらうことして、タスクもれ減少、工数の精度の改善を行っています。
定期的なレビュー会と組み合わせることで、手戻りを減らし、質を高めるように改善することができました。


安定したタスクボードの運営には4回必要としました。
それぞれタスクボードを運用することで見えた問題点について、一歩ずつ解決し続けています。


時系列では、GameJam → プロジェクト → GameJam → プロジェクト と、交互に行っていくことで、タスクボード運用改善のスピードを早くすることにつなげています。
これはWCCFが半年単位で新しいバージョンをリリースしていたので、改善のチャンスが多かったことはタイミング良く導入できたことに繋がったと考えています。

他のプロジェクトでのタスクボードの運用について

WCCFの例が完成形ではありません。プロジェクトが変われば、運用方法もプロジェクト毎に変わります。
例えばCOJでは、今までよりもさらに短期リリースが必要とされたため以下の運用に変更しました。

  • 工数出しにプランナー、グラフィックデザイナーも参加して、よりタスク量、工数の精度を上げる
  • 優先順位の変更の指標にするために、機能毎にToDoを分けて、機能単位の合計工数を書いておく(オレンジと緑)

f:id:sgtech:20170522103143j:plain

これにより、無駄なタスクを減らし、継続的に短期リリースができるような体制を構築しております。

他の例では、以下のような運用もあります。

  • 割り込みタスクを赤い付箋で追加する
  • バグ修正もタスクにして管理する
  • Doneの前に、「レビュー」を入れる

付箋については、Redmineから直接プリンタを使って付箋を作ったりしたこともあります。


自分達のプロジェクトに合わせて、自由に変えられるのがタスクボードの強みです。
ですので、最初はアナログな手書きで始めると、何度も簡単にやり直せるのでおすすめです。

まとめ

タスクボードをGameJamで運用して、プロジェクトに適用することで以下のことがわかりました。

  • タスクボードの使い方は簡単だが、チームに合わせて何度も変更することで安定運用になります。
  • GameJamで試してからプロジェクトに適用すると、失敗のリスクを減らすことができます。
  • タスクボードによる見える化のテスト中でも、問題点は次々に明るみになるので、見える化の効果は高い。

  ただし、タスクボードによって問題点は見えても、問題そのものの解決方法はチーム自身で皆で協力して考えて改善してください。


今回はタスクボードによる業務の見える化について話してみました。
タスクボードの使い方は難しくありませんが、実際の運用には多くの問題が発生してきます。
なぜなら問題点が見えるようになることが、タスクボードを使う目的だからです。


最初からうまくいくはずはありませんし、これが最終形でもありません。
完璧を目指さず、シンプルにはじめ、必要になったら後からいくらでも変えていきましょう。


さあ明日からタスクボード、始めてみましょう!


タスクボードを最初から最後まで一通り試してみたい、という方には短期間でゲーム開発ができて、失敗してもダイジョーブ!な環境、「GameJam」でタスクボードを試してみませんか?
セガグループでは、失敗してもダイジョーブ!な環境、「SEGA Game Jam」を開催しております。
次回、記念すべき「第十回 SEGA Game Jam」は2017年7月15日~16日開催です!


私達、第三研究開発部では、このような取り組みにも積極的な方と一緒に働きたいと考えています。
もしご興味を持たれましたら、弊社グループ採用サイトをご確認ください。

採用情報|株式会社セガ・インタラクティブ
採用情報|株式会社セガゲームス -【SEGA Games Co., Ltd.】


アーケードで稼働中の「WORLD CLUB Champion Football」もよろしくお願い致します!
www.wccf.jp

そうだ、勉強会を開こう

セガゲームス、開発技術部の竹原です。
普段はビルド & QA エンジニア(自称)として開発部署の環境の効率化のお手伝いを主にさせて頂いています。
最近は VR とキャベツダイエットにハマっています。
前回、前々回と良い意味で濃ゆい技術の話が続きましたので、私からは技術寄りの話から少し離れた形で SDC というグループ内カンファレンスの紹介をさせて頂こうと思います。
技術的な話はほとんど出てきませんので、箸休めのキャベツをつまむノリで気楽に楽しんでいってくださいね。

はじめに

今回のブログの主題はグループ内カンファレンスイベントを開催するメリットに関してとなります。
また、更にそこからもう一歩踏み込んで、グループ内カンファレンスの運営を通して自分(QA エンジニア)が得られたものについてもお話しできればと思っています。
「グループ内カンファレンスなんてやる意味あるの?」
と疑問に思っている方から、
「グループ内カンファレンス最高!もっとやれ!」
と思っている方まで、グループ内カンファレンスの価値を見直して頂くきっかけになれば幸いです。

ちなみに、今回の記事内では社内の呼び方に則ってグループ内カンファレンスという表現をしていますが、この辺りは会社によって変わると思いますので、適宜「社内勉強会」等に読み替えて頂ければと思います*1

SDC とは?

本題へ入る前に、まずは SDC とは何ぞや、という説明をさせて頂きます。
SDC とは「SEGA Developers Conference」の略称で、セガグループ内のコンテンツに関わる全ての人を対象に、開発、運営等多くのグループ内技術情報をセガサミーグループ内で共有するために開催されているグループ内カンファレンスイベントです。
社内勉強会のでっかい版、もしくはプチ CEDEC みたいなものをイメージして貰えれば大体 OK です。

開催時期は毎年 3 月後半で、現在までの開催数は 2016, 2017 年の計 2 回とまだまだ若いイベントです。
基本的には CEDEC ような一般に行われるカンファレンスイベントと同様の形態なのですが、一番の特徴としてはグループ内の開催ならではの講演内容を重視している事が挙げられます。
ここで言うグループ内の開催ならではとは「赤裸々な失敗事例」や「内製ツールやサービスの紹介」、「未公開の技術デモの展示」等々、グループ外では公開できないような内容のものを指しています。
これは、

  • 折角開催するならグループ内カンファレンスでないと聞く事が出来ない内容を共有した方が有意義である
  • グループ外に出せるような情報であれば外で講演を行って業界全体に情報共有を行って欲しい

という SDC 運営の意向を反映した方針です。

さて、これだけでは SDC の実態のイメージが付かないと思いますので、ここからはもう少し具体的に SDC の内容や規模感について紹介させて頂こうと思います。

講演の種類

講演の種類は以下のように分かれています。

  • 通常セッション 50 分
  • ショートセッション 25 分
  • ラウンドテーブル 110 分
  • 展示 110 分

ラウンドテーブルは 2016 年の開催時は 50 分での開催だったのですが、「話し足りない!もっと時間が欲しい!!」という要望を多く頂いたので、 2017 年では 110 分に変更してがっつり話し合って貰いました。
ただ、今度は逆に「長過ぎて参加し辛い(聞きたいセッションと被ってしまう)」という意見も頂いたので、この辺のバランシングの難しさを痛感しています。
その他では 2017 年のセッションの中に「通常セッション枠を使ってライトニングトーク 5 連発!」というような面白い試みもありました。

セッション数

セッション総数は

  • 2016 年総数 : 28
    • 通常 : 10
    • ショート : 6
    • ラウンドテーブル : 9
    • 展示 : 1
  • 2017 年総数 : 26
    • 通常 : 11
    • ショート : 4
    • ライトニングトーク : 1
    • ラウンドテーブル : 8
    • 展示 : 2

とった感じで大体 20 台後半くらいとなります。
これを朝 10 時から夕方 18 時までみっちりと詰め込んで開催しています。
セッションの採択率は大体 50% 前後とかなり激戦で、期末の忙しい時期の開催にも関わらず積極的な応募を頂いています*2
こうしたグループ内の社員のモチベーションの高さは同じ会社で働いている身としては嬉しい限りです。

参加人数
  • 2016 年
    • のべ : 1184 人
    • ユニーク : 340 人
  • 2017 年
    • のべ : 1281 人*3
    • ユニーク : 401 人

と数多くの方に参加頂いています。
開発以外の方が試しに開発の話を聞きに来てみたり、自社ビル内での開催なので業務の合間に 1 セッションだけ聞いて仕事に戻る人が居たりと一般のカンファレンスと違いフレキシブルな参加も行えるのはグループ内カンファレンスならではの良い所だと思います
また、参加者の職業別比率は以下の通りです。

  • プログラマー : 55%
  • デザイナー : 17%
  • プランナー : 7%
  • その他(TA 等) : 21%

割合的にはプログラマの参加が多めなので、他の職種の方にももっと参加頂けるように工夫をしていきたい所です。

参加会社

セガサミーグループ内の各社から参加頂いています。

  • アトラス
  • サミー
  • サミーネットワークス
  • セガインタラクティブ
  • セガゲームス
  • セガサミークリエイション
  • セガサミーホールディングス
  • セガトイズ
  • セガホールディングス
  • セガライブクリエイション
  • ダーツライブ
  • マーザ・アニメーションプラネット

セガサミーグループは関連会社の数が非常に多く、そうした中でシナジーを高めて開発効率を高めていくにはこういったカンファレンスは必要不可欠だと感じています。

セッションの一例の紹介

続いて SDC で講演された各資料を公開します!
一般のカンファレンスでは見られないあんな話やこんな話が盛り沢山!
……と言いたい所だったのですが、前述した通り SDC の講演はグループ内の開催ならではのものがほとんどなので、ここでお見せできるものがほとんどありません。
セッションではありませんが、「Git 運用を考える」というラウンドテーブルの概要であれば公開しても大丈夫そうですので、これを紹介してみようと思います。
セッションやその他のラウンドテーブルにも興味がある方は是非セガゲームスに入社して SDC に参加してください。
お待ちしております!!

さてさて、「Git 運用を考える」は以前当ブログで「モダンな OpenGL で頂点モーフ」という記事を書いた山田と私でモデレーターを務めているラウンドテーブルです。
社内での Git の利用増加を踏まえて 2016,2017 と連続で開催しており、自分で言うのもおこがましいですが、社内の Git 事情や状況、運用方法を情報交換する場として非常に有益なものとなっています。

具体的にどんな議題が話されていたか話題をピックアップしてみますと、

  • 2016 年
    • git の利用状況
    • git への移行
    • クライアントソフトについて
    • サーバソフトについて
    • Git LFS について
    • トラブル事例
    • 企画、デザイナーに使ってもらうには
  • 2017 年
    • フローは git-flow? github-flow?
    • どのくらいルールを守れているか
    • Master ブランチへの追従にリベースを使うかマージを使うか
    • コンフリクトの扱い
    • 非プログラマ向けの布教について
    • Git からの卒業を考えたことがあるか
    • SVN からの移行
    • サブツリー、サブモジュール
    • MS GVFS

という感じで、 2016 年はまだ探り探り使い始めたくらいだったのが、 2017 年に掛けて本格的に利用が始まり、突っ込んだ議論を多くできるようになったことが分かります。
毎年同じテーマでラウンドテーブルを開催する事により、グループ全体の状況をお互いに把握しながら情報交換できるのはグループ内カンファレンスならではの強みと言えると思います。
こうした情報交換の熱が高まり、意見交換の頻度を高めた方が良い状況になってきたら、月 1 くらいの定例開催の勉強会等にスムーズに移行できると尚良いですよね。
実際 SDC がきっかけで開催されるようになった「自動化交流会」や「アジャイル開発コミュニティ」というような勉強会も存在しています。
次はこうした SDC 開催による効果についてお話ししようと思います。

グループ内カンファレンスの効果

グループ内カンファレンスを開催するメリットは盛り沢山です。
SDC を通じて実感している点をご紹介したいと思います。

学習の場になる

カンファレンスの効果と言えばこれをイメージする方が多いと思います。
受講者は講演を聞く事により新しい知見を得て、講演者は情報をまとめる事により復習の機会を得る事ができます。
グループ内での開催では一般のそれに比べ、講演内容に対する「うちはこうしてるよ」というようなフィードバックが包み隠さず積極的に得られる傾向が高く、より大きな効能があると感じています。

情報共有の場になる

こちらも「カンファレンスと言えばこれ!」という効果ですよね。
講演者の話を聞くだけでなく、講演者やその他の参加者と情報交換を行う目的としても、カンファレンスは非常に貴重な場となります。
更にグループ内カンファレンスの場合、

  • プロジェクト内容に深く関連する話を気兼ねく出来る
  • 特定のツールに特化したラウンドテーブルを開催できる

等々、より一層突っ込んだ形で情報交換を行う事ができるのが魅力です。

人と人を繋げる場となる

組織の規模の大小に関わらず、隣のチームや部署が何をやっているか、どういう技術を持っているかを知る機会は意外に少ないのではないでしょうか。
そんな中で、グループ内カンファレンスは隣の部署やチーム、会社が何をやっているか知り、担当者の顔を覚える事ができるのはとても貴重な機会となります。

  • 〇〇で困ったがそういえばグループ内カンファレンスで△△さんが関連したことを話していたな……ちょっと聞いてみよう
  • カンファレンスで紹介されていたあのツール便利そうだったのでうちにも使わせてくれないかな

等々、グループ内での業務協力の切っ掛けやシナジーの創造に一役買う事ができると思います。
前項目で挙げた「自動化交流会」や「アジャイル開発コミュニティ」もこうした繋がりから生まれたものです。

情報を発信していく土壌ができる

グループ内カンファレンスのようなものを継続して実施していく事により、情報共有を行う事の価値や効果が組織内に浸透していきます。
SDC 発の勉強会は元々こうした情報共有を得意とする人たちが作ったものですが、こうした活動の認知度が徐々に上がっていく事により「じゃあ自分もやってみようかな」と人が増えていき、最終的に組織全体として情報共有を積極的に行っていく雰囲気が生まれてきています。

組織全体のモチベーションが上がる

講演者、受講者の方々から SDC に参加した事によりモチベーションが上がったという感想を頂いています。
自分を省みてもそうなのですが、エンジニアにとってこうしたイベントはとても良い刺激になりますよね。
ましてそれが同じグループ内でやっている事例や技術の紹介であれば、尚更ではないかと思います。

グループ内カンファレンスの効果まとめ

上記のようなメリットは直接見えてこないものが多く、運営や講演の工数にどうしても目が行ってしまいますが、掛かる工数以上の効果があると感じています。
とは言え、数字に見えてないからこそ最低限取れる部分の KPI はきっちり計測して、有用性を示して行く事も大事だと思います。
例えば SDC ではアンケートの集計を行い、その効果を測定しています。
※アンケート集計の一例
f:id:sgtech:20170419104149p:plain

グループ内カンファレンスの運営を通じて得たもの

グループ内カンファレンスに上記のような多くのメリットがある事は色々な所で語られていると思います。
しかし、その運営の仕事からも得られるものが数多くある事はあまり知られていないように感じています。
これはとても勿体ない事だと思いますので、自分が SDC の運営を通じて得られたものを紹介してみます*4

社内の情報ハブとしての役割を強化できる

カンファレンスの運営を担当するには、その役割上一通り講演概要を把握する必要があります*5
また、講演を行うような情報を持った人に名前や顔を覚えて貰う事もできます。
このように情報の在り処やキーパーソンを把握する事で、何かあった際に

  • その情報はだれだれさんが持っていますよ
  • いついつのカンファレンスで発表されてましたよ

というような形で、社内の情報ハブとして強く機能する事ができるようになります。

カンファレンスと通じて組織全体の品質・開発効率を向上させる事ができる

グループ内カンファレンスは個々の開発者の能力の底上げをするにはもってこいの場で、最終的にはプロダクトの品質や開発効率の向上に繋がるイベントと言えます。
QA エンジニアの使命の一つはプロダクトの品質や開発効率の向上にあると思います。
つまり、グループ内カンファレンスは QA エンジニアにとっては普段の業務の延長線上にある、効果の高い仕事内容と言えるのではないかと思います。
これは運営の中で得たもの、というよりはカンファレンス自体の効果と被った内容ではあるのですが、グループ内カンファレンスを開催する事が QA の仕事に繋がる、という気付きを得る事ができたので、あえて運営を通じて得たものとして紹介させて頂きました。

開発者が必要としている情報を見つける事ができる

運営の仕事の特性上、人気のあるセッションの傾向やアンケートのコメント等の情報を知る事ができます*6
これにより、今グループ内で必要とされているもの、足りないものは何かを見つける良いヒントを得る事ができます。

自分が情報を発信する際の参考になる

QA エンジニアは資料を作成して費用対効果を示す等、ステークホルダーに情報をうまく伝えなくてはならない機会が数多くある職種です。

  • 参加者へのカンファレンス情報の公開
  • スタッフ用の説明資料の作成
  • 講演者の発表資料の確認

等のような仕事を通じて、情報の発信方法や伝え方について多くの気付きを得る事ができました。

一般のカンファレンスや勉強会へ参加する意義を見直すきっかけになる

運営の仕事をこなしていく中で、限られたコストの中でどうすればこのカンファレンスの効果が最大になるかという問題には常に頭を悩ませられました。
こうした経験を通じて、翻って自分がカンファレンスや勉強会への参加する場合はどうしたら最大限の効果を得られるだろうか、とイベントに参加する姿勢を見直す切っ掛けとなりました。

グループ内カンファレンスの運営を通じて得たものまとめ

グループ内カンファレンスの運営の仕事は(規模にもよりますが)エンジニアとしては精神的に辛い事務的な作業も多くあります。
反面、通常の業務をしていては中々得る事ができない上記のような貴重な経験を積む事ができます*7
カンファレンスイベントの運営、と聞くと抵抗感を持つ方も多いと思いますが、運営の仕事の大変さ以上に得られるものは多いので皆さんも是非グループ内カンファレンスを開催してみてください。

全体まとめ

SDC を通じ、グループ内カンファレンスの概要と効果、運営を担当する事により得られるものについて紹介させて頂きましたが如何でしたでしょうか。
こうした活動に興味がなかった方には、少しでもグループ内カンファレンスに興味を持って頂き、自分の所でも開催してみようかな……、と思って頂ければ嬉しく思います。
逆に既にグループ内カンファレンスの運営のような活動をしている方は、グループ内カンファレンスをやる意義を考え直す機会となれば幸いです。

最後になりますが、セガグループでは SDC 以外にも月例の勉強会や社内ゲームジャムの開催等、様々な取り組みを行っています。過去の SDC の講演資料や録画映像も見放題です!
こうした活動に興味と意欲がある方は是非セガグループで一緒に働きましょう!!


採用情報|株式会社セガゲームス -【SEGA Games Co., Ltd.】

採用情報|株式会社セガ・インタラクティブ



それでは次回の投稿をお楽しみに。

*1:グループのスケールメリットを活かした内容も若干ありますが、基本的にはどの規模の会社にも共通して言える事をお話しするつもりです

*2:セッション以外の応募については大体用意している枠と同じくらいの数となります

*3:2017 年の SDC では一部のグループ会社オフィス上へ、セッションのライブストリーミング配信を実施しました

*4:QA エンジニア目線の話が多めではありますが、一般のエンジニアの方にもあてはまる話も結構あるのではないかと思います

*5:審査や資料確認等

*6:集計作業が必要なのです!

*7:私も今では運営をやって良かったと強く感じています

Vulkanでシェーダリフレクション(Shader Reflection)を取得してみる

みなさん初めまして。工藤@セガゲームス開発技術部です。

社内ライブラリを開発する仕事を長年しています。これまでゲーム機のSDKやDirectX, OpenGLなどのグラフィックスAPIを使い、グラフィックスライブラリを作成してきました。最近のAPIとしてはDirectX12, Metal等がありますが、昨年にはさらにVulkanがリリースされました。VulkanはKhronosグループが策定しているマルチプラットフォーム向けグラフィックスAPIです。


情報が乏しく複雑怪奇なこのVulkanには苦戦させられています。Vulkanがリリースされてから一年が経ち、昨年秋には赤本(Vulkan Programming Guide)が発売されたり、GDC2017(Game Developers Conference 2017)での発表があるなど、やっと情報が増えてきました。みなさんはいかがでしょうか。


最近はゲームエンジンを使う機会が増え、ローレベル(低階層)のグラフィックスAPIを直接使う人の数も減っていると思います。ここでVulkanの情報を発信しても役立つ人がどれだけいるのかわからないような状況ですが、今回のブログはこのVulkanでのシェーダリフレクションの使い方について取り上げたいと思います。
※「シェーダリフレクション」とはシェーダの中にある変数の情報を取得することです。


Vulkanでシェーダを使うには一般的にシェーダ言語にGLSLを使用しSPIR-Vへ変換して使用します。SPIR-VはVulkanで導入されたシェーダの中間言語です。HLSLからSPIR-Vへの変換なども今後は対応していくようです。


GLSLの使い方はOpenGLで使用していた場合とほとんど同じですがVulkan用にキーワードが追加されています。Vulkan用に追加されたキーワードにはシェーダへユニフォームバッファやサンプラなどのリソースをバインドするために必要なsetとbindingがあります。この値は上位ライブラリを実装するときに必要になりますが、この値を取得する関数はVulkan SDKには用意されていません。

前置きが長くなりましたが、今回はVulkanで追加されたsetとbindingの値をシェーダからと取得したいと思います。


目次

VulkanでのGLSLの例を見てみよう

まずVulkanでリフレクションに触れる前に簡単なシェーダの例を見てみましょう。

#version 450 core
#extension GL_ARB_separate_shader_objects : enable
#extension GL_ARB_shading_language_420pack : enable
layout(set = 0, binding = 0) uniform buf {
        mat4 MVP;
        vec4 position[12*3];
        vec4 attr[12*3];
} ubuf;
void main() 
{
    // Do nothing!
}

このシェーダではuniform bufにset = 0とbinding = 0が設定されています。この値がVulkan SDKからユニフォームバッファへリソースをバインドするときに必要になります。set、bindingがない場合はset、bindingとも0として扱われます。Vulkan SDKへのdescriptorsetとbindingへの設定は話が長くなりますので今回は説明しません。uniform bufにはmat4のMVP変数、vec4のpositionの36個の配列、vec4のattrの36個の配列がメンバーにいます。

GLSLをロードしSPIR-Vへ変換する

ゲーム開発中で絵作りがなかなか決まらない場合、何度もシェーダを書き換える必要が出てきます。ゲーム中でGLSLをSPIR-Vへ変換できるとゲームを一旦終了することなしにシェーダを書き換えた時点でシェーダを切り替えることが可能になります。開発終盤になりシェーダが確定したら事前コンパイルしたSPIR-Vを直接ロードして使用します。GLSLをglslangを使用してSPIR-Vへ変換します。glslangはValkan SDKに入っています。

bool ShaderReflection::GLSLtoSPV(const vk::ShaderStageFlagBits shader_type, const char *pshader, std::vector<uint32_t> &spirv)
{
    glslang::InitializeProcess();
    glslang::TProgram &program = *new glslang::TProgram;
    const char *shaderStrings[1];
    TBuiltInResource Resources;
    init_resources(Resources);

    EShLanguage stage = FindLanguage(shader_type);
    glslang::TShader *shader = new glslang::TShader(stage);

    shaderStrings[0] = pshader;
    shader->setStrings(shaderStrings, 1);

    EShMessages messages = (EShMessages)(EShMsgSpvRules | EShMsgVulkanRules);
    if (!shader->parse(&Resources, SHADER_VERSION, false, messages)) {
        delete &program;
        delete shader;
        return false;
    }

    program.addShader(shader);

    if (!program.link(messages)) {
        delete &program;
        delete shader;
        return false;
    }

    glslang::GlslangToSpv(*program.getIntermediate(stage), spirv);
    glslang::FinalizeProcess();

    delete &program;
    delete shader;
    return true;
}

glslangにリフレクションがあるけど使えないの?

先ほどGLSLからSPIR-Vへ変換にglslangを使いました。glslangを見ていますとglslang\glslang\MachineIndependentにreflection.hとreflection.cppがあります。結論から言えば変数名や型、サイズなどは取得できます。ですがVulkanで追加された今回の目的であるsetやbindingの値を取得することができません。ソース提供されているので改造すれば取得できるようになるかもしれませんし、今後対応されるかもしれません。

SPIRV-Crossを使用してリフレクションを取得する

glslangを使用してsetとbindingが取得できないので次の一手を探しました。SPIR-Vからsetとbinding値をとることができないものかとSPIRV-Cross-masterを入手してソースコードを眺めていたところ、spirv_cross.hppにspirv_cross::Compilerクラスを発見。get_nameやget_typeなどの関数の他にメンバー変数にはsetやbindingもあったのでSPIRV-Crossを使用してみます。ユニフォームバッファのメンバーも取得したいのでメンバー数とメンバータイプを取得する関数を派生クラスDemoCompilerクラスを作成しました。

class DemoCompiler :public spirv_cross::Compiler
{
public:
    DemoCompiler(std::vector<uint32_t>& ir) :Compiler(ir) {}
    virtual ~DemoCompiler() {};

    size_t get_member_count(uint32_t id) const
    {
        const spirv_cross::Meta &m = meta.at(id);
        return m.members.size();
    }

    spirv_cross::SPIRType get_member_type(const spirv_cross::SPIRType &struct_type, uint32_t index) const
    {
        return get<spirv_cross::SPIRType>(struct_type.member_types[index]);
    }
};


取得した情報を入れる構造体を2つ定義します。UniformInfo構造体とBufferInfo構造体です。SPIR-Vから取得した情報をこれらの構造体へ入れていきます。

typedef struct
{
    spirv_cross::SPIRType::BaseType baseType;
    std::string     name;     // ユニフォーム名
    size_t          bytesize; // バイトサイズ
    int             arraysize;// 配列数
    int             offset;   // バッファ先頭からのオフセット
}  UniformInfo;

typedef struct
{
    spirv_cross::SPIRType::BaseType baseType;   // Struct, Image, SampledImage,Samplerなど
    std::string     name;         // バッファ名
    size_t          bytesize;     // バッファバイトサイズ
    int             arraysize;    // 配列数 ない場合は0
    int             offset;       // バッファ先頭からのオフセット
    int             descriptorSet;// descriptorSetID
    int             binding;      // bindingID
    std::vector<UniformInfo> uniformInfos;
} BufferInfo;

spirv_cross::Compilerクラスに入った情報はspirv_cross::Resourceのvectorで定義されている各変数へ入ります。getReflection関数を作成しDemoCompilerから情報を収集します。初めにユニフォームバッファ情報やサンプラ情報をBufferInfo構造体へ収集し、ユニフォームバッファの場合メンバー変数の情報もUniformInfo構造体を使用して収集します。今回のブログの目的であるsetとbindingの値はcomp.get_decorationの第2引数をspv::DecorationDescriptorSetとspv::DecorationBindingにすることで取得することができます。各値の取得方法についてはspirv_cross.cppにソースコードがあるので参考にしてください。

void ShaderReflection::getReflection(const std::vector<spirv_cross::Resource> &resources, const DemoCompiler &comp, std::vector<BufferInfo> &bufferinfos)
{
    using namespace spirv_cross;

    for ( const auto& resource : resources)
    {
        const SPIRType spirv_type = comp.get_type(resource.type_id);

        BufferInfo binfo;
        binfo.baseType      = spirv_type.basetype;
        binfo.name          = resource.name.c_str();
        binfo.offset        = comp.get_decoration(resource.id, spv::DecorationOffset);
        binfo.arraysize     = spirv_type.array.empty() ? 0 : spirv_type.array[0];
        binfo.bytesize      = spirv_type.basetype == SPIRType::Struct ? comp.get_declared_struct_size(spirv_type) : 0;
        binfo.descriptorSet = comp.get_decoration(resource.id, spv::DecorationDescriptorSet);
        binfo.binding       = comp.get_decoration(resource.id, spv::DecorationBinding);

        size_t num_value = comp.get_member_count(resource.base_type_id);
        for (uint32_t index = 0; index < num_value; ++index)
        {
            const SPIRType &member_type = comp.get_member_type(spirv_type, index);

            UniformInfo uinfo;
            uinfo.baseType  = member_type.basetype;
            uinfo.name      = comp.get_member_name(resource.base_type_id, index).c_str();
            uinfo.bytesize  = comp.get_declared_struct_member_size(spirv_type, index);
            uinfo.offset    = comp.get_member_decoration(resource.base_type_id, index, spv::DecorationOffset);
            uinfo.arraysize = member_type.array.empty() ? 0 : member_type.array[0];
            binfo.uniformInfos.push_back(uinfo);
        }
        bufferinfos.push_back(binfo);
    }
}

次に作成した関数を使用してSPIR-Vから情報を収集します。DemoCompilerを作成しresourcesを取得しuniform_buffers、sampled_images、separate_images、separate_samplersからリフレクションを収集します。

void ShaderReflection::getSPVtoReflection( const void *pBinSPV, size_t BinSPVBytes)
{
    using namespace spirv_cross;
    std::vector<uint32_t> spirv_binary;
    spirv_binary.resize(align4(BinSPVBytes) / sizeof(uint32_t));
    memcpy(spirv_binary.data(), pBinSPV, BinSPVBytes);
    DemoCompiler comp(spirv_binary);
    ShaderResources resources = comp.get_shader_resources();
    //uniform_buffersから情報取得
    getReflection(resources.uniform_buffers, comp, m_bufferuniform_info);
    //sampled_imagesから情報取得
    getReflection(resources.sampled_images, comp, m_sampleruniform_info);
    //separate_imagesから情報取得
    getReflection(resources.separate_images, comp, m_sampleruniform_info);
    //separate_samplersから情報取得
    getReflection(resources.separate_samplers, comp, m_sampleruniform_info);
}

収集した情報を標準出力へ表示するサンプル関数です。

void ShaderReflection::printReflection(const std::vector<BufferInfo> &bufferInfos )
{
    const char *BaseTypeNmae[] =
    {
        "Unknown",  "Void", "Boolean",  "Char", "Int",  "UInt", "Int64","UInt64",
        "AtomicCounter","Float","Double","Struct","Image","SampledImage","Sampler"  
    };

    for ( const auto& binfo : bufferInfos)
    {
        std::cout << "name         :" << binfo.name << std::endl;
        std::cout << "baseType     :" << BaseTypeNmae[binfo.baseType] << std::endl;
        std::cout << "size         :" << binfo.bytesize << " Byte" << std::endl;
        std::cout << "arraysize    :" << binfo.arraysize << std::endl;
        std::cout << "offset       :" << binfo.offset << std::endl;
        std::cout << "descriptorSet:" << binfo.descriptorSet << std::endl;
        std::cout << "binding      :" << binfo.binding << std::endl;

        if (!binfo.uniformInfos.empty())
        {
            std::cout << "uniformInfo" << std::endl;
            for ( const auto& uinfo : binfo.uniformInfos)
            {
                std::cout << "    name         :" << uinfo.name << std::endl;
                std::cout << "    baseType     :" << BaseTypeNmae[uinfo.baseType] << std::endl;
                std::cout << "    size         :" << uinfo.bytesize << " Byte" << std::endl;
                std::cout << "    arraysize    :" << uinfo.arraysize << std::endl;
                std::cout << "    offset       :" << uinfo.offset << std::endl << std::endl;
            }
        }
        std::cout << std::endl;
    }
}

実験

test.fragからリフレクションを取得し出力してみます。テスト用のシェーダですのでシェーダ内容には特に意味はありません。

#version 400
#extension GL_ARB_separate_shader_objects : enable
#extension GL_ARB_shading_language_420pack : enable

layout(std140, set = 0, binding = 0) uniform buf {
        mat4    MVP;
        vec4    position[12*3];
        vec4    attr[12*3];
        float   f1_val;
        vec2    f2_val;
        vec3    f3_val;
        bool    bool_val;
} ubuf;

layout (set = 0, binding = 1) uniform sampler2D samp2d;
layout (set = 1, binding = 0) uniform texture2D tex2d;
layout (set = 1, binding = 5) uniform sampler samp;

layout (location = 0) in vec4 texcoord;
layout (location = 0) out vec4 uFragColor;

void main() {
   uFragColor = texture(samp2d, texcoord.xy);
}

サンプルを実行結果は以下のとおりになります。

---------------------------------------------
filename     :shader/test.frag
---------------------------------------------
name         :buf
baseType     :Struct
size         :1248 Byte
arraysize    :0
offset       :0
descriptorSet:0
binding      :0
uniformInfo
    name         :MVP
    baseType     :Float
    size         :64 Byte
    arraysize    :0
    offset       :0

    name         :position
    baseType     :Float
    size         :576 Byte
    arraysize    :36
    offset       :64

    name         :attr
    baseType     :Float
    size         :576 Byte
    arraysize    :36
    offset       :640

    name         :f1_val
    baseType     :Float
    size         :4 Byte
    arraysize    :0
    offset       :1216

    name         :f2_val
    baseType     :Float
    size         :8 Byte
    arraysize    :0
    offset       :1224

    name         :f3_val
    baseType     :Float
    size         :12 Byte
    arraysize    :0
    offset       :1232

    name         :bool_val
    baseType     :UInt
    size         :4 Byte
    arraysize    :0
    offset       :1244


name         :samp2d
baseType     :SampledImage
size         :0 Byte
arraysize    :0
offset       :0
descriptorSet:0
binding      :1

name         :tex2d
baseType     :Image
size         :0 Byte
arraysize    :0
offset       :0
descriptorSet:1
binding      :0

name         :samp
baseType     :Sampler
size         :0 Byte
arraysize    :0
offset       :0
descriptorSet:1
binding      :5

push any key

まとめ

Vulkanでのシェーダリフレクションを取得をやってみましたがいかがだったでしょうか。今回はspirv_cross::Compilerクラスを使うことでシェーダの必要なデータを簡単に取得することができました。すぐに導入することができますのでVulkanを使って上位ライブラリを作る方の助けになれば幸いです。

このような取り組みにも積極的な方と一緒に働きたいと考えています。もしご興味を持たれましたら、弊社グループ採用サイトをご確認ください。
採用情報 | セガグループ


それでは次回の更新をお楽しみに。

Unityでパレット+インデックスカラーテクスチャを描画してみる

はじめまして、セガネットワークスカンパニーの関と申します。
iOS/Androidでサービス中のタイトル「戦の海賊」のクライアントサイドのプログラマです。
普段は育成パート(アジトやマップなどバトル以外全部)のUIやシステム関連を主に担当しています。レンダリングの専門家ではないので間違いがございましたらご容赦ください。


VRAMはいつの時代も足りないものでテクスチャサイズを削減する為に圧縮テクスチャを試したものの画質に我慢出来ずに結局フルカラーに戻した経験はありませんか?
私はいっぱいあります。

ここで「戦の海賊」の幼なじみキャラ、カーシャさんに登場していただきましょう。
f:id:sgtech:20170223101232p:plain
左がフルカラーのカーシャさんで右が8bitカラー(256色)のカーシャさんです。どうですか?違いがわかるでしょうか?*1

私はこういったアニメ調で輪郭のはっきりした絵は8bitカラーが最適だと思います。*2

ならばどんどん使えば良いと思うのですが、大変残念なことに今時のGPUはパレット+インデックスカラーのハードウェアデコードを捨ててしまいました。

最近はChromaPack*3が提唱されてプログラマブルシェーダが頑張れば色のリアルタイムデコードもなんとかなることがわかりました。
ChromaPackは素晴らしいアイディアですが、標準で1ピクセル12bitで複数ビットのアルファチャネルを考慮するともう一声削減したいところです。

ということでプログラマブルシェーダでパレット+インデックスカラーのデコードにチャレンジしてみましょう。

実装

以下のサンプルはUnity5.3.xで実装しました。

まずは256色に減色します。
減色にはOPTPiX imesta*4を使用しました。アルファ成分も考慮したパレットを生成出来る優れ物です。

f:id:sgtech:20170303190039p:plain

さて減色した画像をUnityにインポートするとパレット情報は何処かに飛んで行ってしまいますが心配はいりません。256色しか使われていないTrueColor(RGBA32bit)のテクスチャとしてインポートされますますので、インポートスクリプトでパレットを再構成し、パレット成分とインデックス成分を分離して二つのテクスチャを生成します。

// ImporterTextureCLUT.cs
using System.IO;
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;

class ImporterTextureCLUT : AssetPostprocessor
{
	// src_pathに入っている画像から_clut、_indexのテクスチャを生成しdst_pathへ格納する
	string src_path = "Assets/src/";
	string dst_path = "Assets/dst/";

	void OnPreprocessTexture()
	{
		var importer = (assetImporter as TextureImporter);
		if (importer.assetPath.Contains( src_path )) 
		{
			// src_pathのテクスチャから正常にピクセルを取り出せるようにフォーマットを変更
			TextureImporter textureImporter = (TextureImporter)assetImporter;
			textureImporter.filterMode = FilterMode.Point;
			textureImporter.isReadable = true;
			textureImporter.textureFormat = TextureImporterFormat.RGBA32;
			textureImporter.mipmapEnabled = false;
		}
	}

	void OnPostprocessTexture( Texture2D texture )
	{
	    var importer = (assetImporter as TextureImporter);

		if(!importer.assetPath.Contains(src_path))
			return;
		// 全てのピクセルをなめてカラーの辞書(パレット)を生成する
		Dictionary<Color32,int> clut_dic = new Dictionary<Color32,int>();
		Color32[] tex32bit = texture.GetPixels32();
		int index = 0;
		foreach( Color32 color in tex32bit )
		{
			// 初めて出現した色に新しいインデックスを割り当てる
			if( !clut_dic.ContainsKey( color ) )
			{
				clut_dic[color] = index;
				index ++ ;
				if( index>256 )
				{
					// 256色以上あったら中断
					Debug.LogError( "over 256!!!!" );
					return;
				}
			}
		}
		// パレットの辞書が出来たのでテクスチャとして出力しPNGにエンコードして保存
		Texture2D tex_clut = new Texture2D( 256, 1, TextureFormat.RGBA32, false );
		tex_clut.filterMode = FilterMode.Point;
		Color32[] clut_array = new Color32[256];
		foreach( Color32 val in clut_dic.Keys )
		{
			clut_array[clut_dic[val]] = val;
		}
		tex_clut.SetPixels32( clut_array );
		byte[] clut_png_binary = tex_clut.EncodeToPNG();

		// パレットの辞書からインデックスを取得しアルファチャネルに設定し、PNGにエンコードして保存
		Texture2D tex_index = new Texture2D( texture.width, texture.height, TextureFormat.RGBA32, false );
		Color32[] index_array = new Color32[tex32bit.Length];
		int i=0;
		foreach( Color32 color in tex32bit )
		{
			index_array[i] = new Color32( 255, 255, 255, (byte)clut_dic[color] );
			i++;
		}
		tex_index.SetPixels32( index_array );
		byte[] index_png_binary = tex_index.EncodeToPNG();


		// 出力用の名前を作成
		string name = importer.assetPath.Substring( importer.assetPath.IndexOf( src_path ) + src_path.Length );
		name = name.Substring( 0, name.LastIndexOf( '.' ) );

		// パレットテクスチャを出力
		FileStream fs = new FileStream( dst_path + name + "_clut.png", FileMode.Create, FileAccess.Write );
		fs.Write( clut_png_binary, 0, clut_png_binary.Length );
		fs.Close();
		// インデックステクスチャを出力
		fs = new FileStream( dst_path + name + "_index.png", FileMode.Create, FileAccess.Write );
		fs.Write( index_png_binary, 0, index_png_binary.Length );
		fs.Close();
	}
}


今回は手を抜いて出力されたテクスチャのインポート設定は手動で変更します。実際に使用することになったらインポートスクリプトでマテリアルまで一気に作れる様にすると便利でしょう。

横に細長いテクスチャはパレットを画像化したものです。
f:id:sgtech:20170223101110p:plain
FormatはAutomatic TrueColor、MipMapはoff、WrapModeはClamp、FillterModeはPointにします。
いきなりTrueColorにしてしまいましたが256ドット*4バイトでたったの1Kbyteです。必要経費だと思いましょう。
f:id:sgtech:20170223101121p:plain

灰色で良く分からない感じになったカーシャさんはインデックスをグレースケールで表現したものです。
f:id:sgtech:20170223102357p:plain
FormatはAlpha8bit、Alpha from grayscaleをon、MipMapはoff、WrapModeはClamp、FillterModeはPointにします。
f:id:sgtech:20170223101132p:plain

あとはこれらを組み合わせてプログラマブルシェーダでデコードするだけです。コードは極めてシンプルです。

// CLUT+Index.shader
Shader "Unlit/CLUT+Index" 
{
	Properties 
	{
		_Clut ("Clut", 2D) = "white" {}
		_Index ("Index", 2D) = "white" {}
	}
	SubShader
	{

		Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
		ZWrite Off
		Blend SrcAlpha OneMinusSrcAlpha 

		Pass
		{
			CGPROGRAM
			#include "UnityCG.cginc"
			#pragma vertex vert_img
			#pragma fragment frag

			sampler2D _Clut;
			sampler2D _Index;

			half4 frag(v2f_img i) : SV_Target 
			{
				// インデックスをカラーとして取り出す
				// (※必ずポイントサンプリングにすること。フィルタをかけるとインデックスが壊れます)
				half4 index_tex = tex2D(_Index,i.uv);
				// インデックスのアルファ成分をパレットテクスチャのu値としてカラーを取り出す
				half4 c = tex2D(_Clut, index_tex.a );
				return c;
			}

			ENDCG
		}
	}
}

マテリアルを新規作成し、シェーダを上のものに設定し、CLUTに細長いパレットテクスチャを設定、Indexにグレーのカーシャさんを設定します。

f:id:sgtech:20170223101143p:plain

適当に平面ポリゴンに貼り付けて表示してみましょう。

f:id:sgtech:20170223102418p:plain

出来た!

と思いきや、スクリーンサイズを変えると…

f:id:sgtech:20170223102429p:plain

なんとも無残にガビガビになってしまいました。

インデックステクスチャをポイントサンプリングしているので当然といえば当然です。バイリニアフィルタをかけるとインデックスに補完がかかり正確な位置のカラーが取り出せず滅茶苦茶な絵になってしまいます。

さてどうしたものでしょう。

画質向上

対処法は色々あると思いますが、万能な方法はなく状況に合わせた対処法になるでしょう。ここでは考え方の提示に留めます。
ご興味ある方は是非セガグループの仲間になって一緒に考えましょう!*5

1.諦めてそのまま使う。

いい加減で恐縮ですがスクリーンの1ピクセルとテクスチャの1ピクセルが正確に対応する様に注意して描画すれば問題ありません。*6
スクリーンサイズが変わる状況では画像の占めるサイズが変わってしまうのでレイアウトに工夫が必要です。
ターゲットのスクリーンサイズに合わせて複数サイズのテクスチャを用意するという戦略もありでしょう。

2.レンダーテクスチャ使う

フィルタのかかるレンダーテクスチャを仮想スクリーンとしてピクセルが拡大縮小されないように描画し、実スクリーンにぴったり収まる様にリサイズして描画します。
巨大な32bitカラーのレンダーテクスチャが必要なので本末転倒ですが、オーバヘッドを回収出来るくらい大量に描けばメリットはあるかもしれません。

3.シェーダでバイリニアフィルタをかける

ポイントサンプリングではテクスチャのUV値は線形に増加しますがピクセル単位に丸めた一点だけがサンプリングされます。
UV値をずらしながら4点サンプリングしパレットからカラーを取得、UV値の端数を使用して線形補間をかけたカラーがバイリニアフィルタをかけた状態になります。
f:id:sgtech:20170223102437p:plain
多少の変形では画質が破綻しないので、扱いやすさは抜群です。
問題は処理負荷です。

処理負荷

ここまで処理負荷について目をつぶって来ましたがリアルタイムに動作するゲームにおいて無視できない要素です。
フレームレートも心配ですが、バッテリ消費量、発熱の問題も現在では無視できません。
今回のプログラムは一見極めてシンプルですが、フラグメントシェーダを使用しているためピクセル数分、関数が呼び出しが行われます。描画先が1024*1024ピクセルであれば、100万回もの呼び出しが行われます。それも毎フレーム!
処理負荷はざっくり言うと命令数と外部メモリへのアクセス回数に依存します。
さらに現在ではどれだけ並列に同時実行できるかが重要な鍵になります。

フォーマット 命令数 メモリへのアクセス回数
TrueColor 2 1
パレット+8bitインデックス 5 2

命令数は多少増えますが並列実行の恩恵を受けられればさほどインパクトはないでしょう。
しかしながらメモリアクセスが2回行われ、1回目の結果(インデックス)が取得完了しないと2回目のメモリアクセス(パレット)が取得できないため読み込み待ちが発生してしまいます。
さらに言うとパレットへのアクセスはランダムな座標を取得しないといけないので、順次テクスチャ座標が増加する想定のGPUではロスが発生するでしょう。
マルチコアで並列に実行するといっても局所的なメモリアクセス(同じ座標のテクスチャ読み込み)では読み込み待ちが頻発してしまい並列実行はできなくなってしまいます。*7

色々考察してみましたが、結局の所リアルタイムで動くかどうかなので何はともあれベンチマークを取ってみましょう。

f:id:sgtech:20170223102454p:plain

フォーマット スコア
TrueColor 58Kasha
パレット+8bitインデックス 39Kasha
パレット+8bitインデックス+バイリニアフィルタ 10Kasha
ChromaPack 15Kasha

謎の単位Kashaの詳細は諸事情により伏せますが、数字が大きい方がパフォーマンスが良いと思ってください*8

単純にテクスチャを読み込む回数に大体比例する結果となりました。この位ならギリギリ実用に耐えるのではないでしょうか。
もちろんGPUのアーキテクチャによって結果は大きく変わりますので慎重な検証が必要です。

応用

パレットはメモリ使用量の削減に効果的ですが、醍醐味はパレットチェンジによる色変えです。インデックスはそのままにカラーバリエーションを持つような事が簡単に実装出来ます。
f:id:sgtech:20170223102503g:plain

パレットテクスチャ自体をプログラマブルシェーダで動的に生成すればパレットアニメーションのような事も出来るでしょう。

また、多少ロジックは複雑になりますが4ビットカラー(16色)の実現も可能です。UIでは16色で十分なケースも多いでしょう。

まとめ

  • ハードウェアが対応していなくてもプログラマブルシェーダを使用すればパレット+インデックスカラーの描画は可能。
  • 画素が変形(拡大縮小など)するケースでは汚くなるので何らかの対策が必要。
  • フラグメントシェーダでのバイリニアフィルタは高コスト(遅い)なので、使い所に注意する。
  • 工夫次第でレガシーなパレット技がいろいろ使えるので可能性は無限大
  • サンプルでさんざんこき使われたカーシャさんの出てくる「戦の海賊」は大好評サービス中なので是非プレイしよう!

www.sen-no-kaizoku.jp

それでは次回の投稿をお楽しみに。


戦の海賊 © SEGA

*1:ちなみにフルカラーのカーシャさんは26727色使用されていました。

*2:逆に輪郭のはっきりしない背景のような絵は圧縮テクスチャでも十分だと思います。

*3:https://github.com/keijiro/ChromaPack/blob/master/README_ja.md

*4:OPTPiX imésta 7 シリーズ | ウェブテクノロジ

*5:私はセガネットワークスカンパニーの所属なのでこちらをおすすめしておきます。
採用情報 |株式会社セガゲームス セガネットワークス カンパニー -【SEGA Games Co., Ltd. SEGA Networks Company】

*6:いわゆるPixelPerfectと呼ばれる描画方法。例えばスクリーンが1080pであればカメラをOrthographicにしてカメラサイズを540とした時ワールド座標の1*1の矩形は1ピクセルで描画されます。

*7:この辺りがパレットが廃止された大きな要因でしょう

*8:読み方はキロアシャじゃなくてローマ字読みして間に「ー」を入れます

セガグループ・インターンシップで学生とゲームを作った話

みなさま、はじめまして。
セガ・インタラクティブ 第二研究開発本部 プログラマーの日髙(ひだか)と申します。

普段はアーケードゲームのクライアントサイドの実装やツール作成、
アセットパイプライン構築などを行っています。
今回のブログ執筆を担当することになりました。
どうぞよろしくお願いします!

年明け一発目ということで小難しい話は置いておき、
昨年秋に開催された「セガグループ・インターンシップ」の取り組みのうち、
ゲームジャム形式で行われた「ゲーム制作体験」についてご紹介します。
また、実際に私もサポートスタッフとして学生とゲーム制作を行ったので、
当日の役割とそこから得られたものについて共有したいと思います。

セガグループ・インターンシップとは

ゲーム業界を志望している、もしくは興味のある学生を対象に、
セガグループ各社が合同で開催するインターンシップになります。
今回は「ゲーム制作体験」と「職種体験」の2種類の取り組みが行われました。

「ゲーム制作体験」では学生とサポートスタッフが一緒のチームになり、
2日間でゲームを1本作るといった内容、
「職種体験」では現場で使用されているツールの説明と、
ツールを使用した作業工程を学生に体験してもらうといった内容になっています。

これらの取り組みを通じてゲーム業界での仕事や制作の工程を知ってもらうだけでなく、
実際に現場のクリエイターと一緒にセガのものづくりを体験することで、
ゲーム業界により一層興味を持ってもらうことを目的としています。

続いて「ゲーム制作体験」の内容についてもう少し掘り下げたいと思います。

ゲーム制作体験について

今回のゲーム制作体験では「ゲームジャム形式」を採用しており、
2日間で合わせて18時間という短い時間でゲーム制作を行いました。

各チームの構成メンバーは以下の通り。

職種 学生 サポートスタッフ
プランナー 2名 1名
デザイナー 2名 1名
プログラマー 2名 1名

1チームあたり9名ずつ、合計5チームが形成されました。
当日はこの他に運営スタッフとチーム外のサポートスタッフが数名参加しました。

今回はサポートスタッフのスキルや開発の手軽さなどを考慮して、
ゲームエンジンにUnityを採用しました。

開発環境については会社側で全て手配しており、
開発用WindowsマシンにUnityを含む各種ソフトがインストールされた状態で各自1台ずつ提供されました。
また、開発ターゲットとしてAndroid端末も各チームに2台ずつ配られました。

そして、「セガっぽい」をテーマに各チームがゲーム制作に取り組みました。

ゲーム制作体験の紹介は以上になります。

開催中の雰囲気などは先日公開された動画をご覧ください。

セガのモノ創りに触れてみよう!『ゲーム制作体験』紹介

サポートスタッフの役割

サポートスタッフはチーム内での実装作業はもちろんのこと、他にも様々な役割があります。
私が担当したチームを例にプログラマー視点での役割を具体的に紹介していきます。

チーム制作におけるUnity活用方法のレクチャー

Unityのチュートリアルをやったことがある、本を読みながら少し触った事がある、
といった初歩を脱して、実際にチーム制作でUnityを活用するための方法を説明しました。
以下にいくつか例を挙げます。

Unityの設定で変更した項目とその理由
Unityを用いて複数人で制作を行う場合、後述のバージョン管理ツールの使用に伴い、
metaファイルを表示したり、アセットをテキスト形式で保持したりする必要があります。
また、Android端末への出力のために、プラットフォームの切り替え、
ゲーム画面解像度の設定などを適切に行う必要があります。

制御のしやすさを意識したGameObjectの組み方
複数のGameObjectを一度に制御する場合に、それらをまとめる親GameObjectを作成して、
親GameObjectを操作することで各GameObjectの制御を簡単に行えるようになります。

もちろんスクリプトからすべてのGameObjectを個別に制御しても良いですが、
せっかくUnityを使用するのであれば、親子構造の特性を活用することで、
短い時間の中でも複雑な制御を手軽に行えるようになります。

アプリをビルドしてAndroid端末で実行する手順
Unity上であればアプリのビルドから実行までを1ボタンで行えますが、
ビルド済みのアプリを端末にインストールして実行する場合は別途手順が必要になります。

コンソールからadbコマンドでアプリをインストールするのですが、
毎回コンソールを開いてコマンドと引数を入力するのは手間なので、
バッチファイルを活用して単純作業を簡略化する方法を説明しました。

また、apkファイルをそのバッチファイルに関連付けすることで、
ビルド後のapkをダブルクリックするだけでインストールできるようになります。

バージョン管理ツールの説明と使い方のレクチャー

開発現場では当たり前のように使用されているバージョン管理ツールですが、
ゲーム制作自体やチーム制作の経験が無い学生にとっては馴染みがありません。

バージョン管理の仕組みやなぜそれらが必要になるのか、などを簡潔に説明したあとで、
実際に使うところを見せて、試してもらうといった対応を行いました。
2日目にもなると、競合した状態を学生たちだけでも解決できる程度に使いこなしていました。

作業分担するためのコーディング方法のレクチャー

個人制作とは異なり、チーム制作では複数人のプログラマーが作業分担してゲームを作り上げます。
その際にそれぞれが好き勝手に実装すると結合作業に時間がかかったり、
必要な機能が抜け落ちたりといった問題が起こりがちです。
そこでスムーズに結合するためにはどういう風に実装すれば良いか、
具体的な方法を提示して取り組んでもらいました。

今回制作したゲームではAndroid端末のセンサー情報を利用してキャラクターを操作する、
といった実装が必要だったため、学生2人と相談して、
「キャラクター制御」と「Android端末の入力取得」で作業分担を行いました。
その際にアドバイスした作業手順は以下の通りです。

キャラクター制御担当の作業

  1. キャラクター制御に必要な入力を管理するControllerクラスを作成する。
  2. Controllerクラスにキーボードなどのデバッグ入力を使用した仮実装を行う。
  3. Controllerクラスをコミットする。
  4. デバッグ入力を使用してキャラクター制御の実装を進める。

Android端末の入力取得担当の作業

  1. 前述のControllerクラスがコミットされるまでに、Android端末の入力取得を検証する。
  2. 仮実装でコミットされたControllerクラスを取得する。
  3. Android端末の入力取得処理をControllerクラスに反映する。
  4. Controllerクラスをコミットする。

事前にどのように作業分担するかを決めてから、
実装に取り掛かることでスムーズな結合を行うことができます。

チーム制作におけるプログラマーの立ち振舞いについてのアドバイス

チーム制作においてデザイナーやプランナーとのコミュニケーションは必要不可欠となります。
制作を円滑に進めるためにプログラマーとしてどういった立ち振舞いをすれば良いかをアドバイスしました。
アドバイスの内容を2つほど紹介します。

【1】仕様待ちをしない

自分の担当箇所について「仕様が決まらないから」という理由で待っていると、
いつまで経っても実装は進まず、本当にその実装で良いのかを判断する時間が減ってしまいます。
プランナーのやりたい事を汲み取り、こちらから案を提示したり、
実装したものを見せて判断してもらったり、といった動きができると良いです。

【2】作成するデータの形式を決める

ゲームに必要なデータを組み込んだり、読み込んで使用したりと、
最終的にデータを扱うのはプログラマーになることが多いです。
そのため扱い易さや特徴を考慮してプログラマーからデータの形式を指定する必要があります。
また、早めに形式が決まっていると作業の手戻りを防ぐことにも繋がります。

参加して得られたもの

サポートスタッフとして学生と一緒にゲーム制作を行うことで私自身多くのことを学びました。
経験に差がある相手に教える難しさや、学生本来の能力を発揮してもらうために導く難しさを痛感しました。
また、学生にアドバイスした内容について「自分はその通りにできているか?」と振り返る良い機会にもなりました。

これらは学生だけで取り組むグループワークを見ているだけでは得がたい、貴重な経験となりました。
今後の業務や新人・後輩の育成にも活かせるのではないかと思います。

まとめ

学生と共にゲーム制作を行うインターンシップは他の事例をあまり聞いたことが無く、
セガでも初の試みでしたが、参加した学生の満足度も高く、
スタッフ一同得るものがあり大変有意義でした。

このエントリーが学生とものづくりをされる方や、ものづくりについて教える方の参考になれば幸いです。

次回のセガグループ・インターンシップの開催については未定ですが、
私の所属するセガ・インタラクティブ 第二研究開発本部では、
このような取り組みにも積極的な方と一緒に働きたいと考えています。
もしご興味を持たれましたら、弊社グループ採用サイトをご確認ください。

採用情報 | セガグループ



それでは次回の投稿をお楽しみに。

Powered by はてなブログ