そうだ、勉強会を開こう

セガゲームス、開発技術部の竹原です。
普段はビルド & 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】作成するデータの形式を決める

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

参加して得られたもの

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

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

まとめ

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

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

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

採用情報 | セガグループ



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

DDSファイルを自力で読んでみよう

  • みなさん初めまして。セガゲームス開発技術部の内田です。

普段は、ゲーム開発に利用するツールやライブラリなどを作成する仕事をしています。
今回は、そのツール開発で必要になった機能を1つ取り上げ、その実装についてお話してみたいと思います。

今回のお題:DDSファイルを自力で読んでみよう

DDSファイルと言えば、ゲームなどのグラフィックスプログラムを作成するときによく利用される画像ファイルフォーマットです。
これは、JPEGやPNGといった画像フォーマットの1つなのですが、DirectXなどで簡単かつ高速に扱えるという利点があります。
DirectXに用意されている機能を利用すれば、簡単にテクスチャとして読み込み、ポリゴンに貼り付けたりすることができます。

そのため、読み込みよりも、どのように高品質のDDSファイルを作成するかが重要な点であり、そのために多くの優秀なソフトウェアや圧縮のノウハウが開発、考案されてきたと言えます。

ですが、今回はDDSへの変換ではなく、読み込みを、ゼロから書いたコードで、自力でやってみましょう。
これができるようになると、

  • ピクセルデータを展開してRGBAの情報を直接利用したい。
  • DirectXやOpenGL、その他ライブラリなどを利用したくない、できない。
  • マネージコードだけで読み込みたい。
  • 何らかのツールのインポートプラグインを書きたい。
  • 他の画像形式に変換したい。
  • 画像の情報(例えば、横幅や高さ、圧縮形式)を取得したい。

などどいった場合に役立つかもしれません。


今回は以下の条件のもと、進めていきたいと思います。

  • 使用言語はC#(なるべく新しい Visual Studio)
  • DDSファイルを読み込み、System.Drawing.Bitmap クラスに変換(WinFormsアプリケーションを想定)
  • DXT1形式のみ対応(最後にBC1にも対応)
  • ミップマップには対応しない

また、処理を簡潔にわかりやすくするため、エラー処理や例外処理は最低限にしたいと思います。

今回はDDSの中でもDXT1形式というレガシーな形式のみを取り上げますが、自分の書いたコードだけで読めるようになるというのは面白いのではないでしょうか。

それでは、さっそく始めましょう。

目次

テストプログラムの作成

いきなりDDSファイルの読み込み部分を作成、、、といきたいところですが、実際には、ファイルを読み込ませながら、少しずつデバッグし、完成させていくことなります。
そこで、まずはドラッグ&ドロップで簡単にDDSファイルを読み込み表示できるようなテストプログラム(System.Windows.Forms 利用)を作成してしまいます。

f:id:sgtech:20161222105257p:plain

フォームデザイナでPictureBoxを追加した後、以下のようにドラッグ&ドロップのコードを書きました。

public Form1()
{
    InitializeComponent();

    AllowDrop = true;
    DragEnter += (sdr, ea) => ea.Effect = DragDropEffects.All;
    DragDrop += (sdr, ea) => {
        var fname = ((string[])(ea.Data.GetData(DataFormats.FileDrop))).FirstOrDefault();
        pictureBox1.Image = DdsLoader.LoadFromFile(fname);
    };
}


ですが実際のところ、開発中は何度も何度も同じ画像を読み込むことになるので、次のようにしてしまった方が楽かもしれません。

protected override void OnShown(EventArgs e)
{
    base.OnShown(e);
    pictureBox1.Image = DdsLoader.LoadFromFile(デバッグに使用するDDSファイルのフルパス);
}

そして、ここから

public static class DdsLoader
{
    public static System.Drawing.Bitmap LoadFromFile(string filename)
    {
    }
}

の中身をを少しづつ実装していきます。

DDSファイルヘッダのすごく簡単な構造

DDSファイルのヘッダは128バイトもあり、複雑な構造となっているのですが、今回は、ごく簡単なパラメータのみの取得で十分です。
必要となるのは、赤枠で示された4ヶ所だけです。

f:id:sgtech:20161222105213p:plain

ヘッダ情報を格納するクラスを作る

まずはDDSファイルのヘッダから必要な情報を抜き出してまとめるためのクラスを作りましょう。例えばこのようになります。

public class DdsSimpleFileInfo
{
    /// <summary>
    /// この情報が有効であることを示す値を取得または設定します。
    /// </summary>
    public bool IsValid { get; set; }

    /// <summary>
    /// 画像の横幅を取得または設定します。
    /// </summary>
    public int Width { get; set; }

    /// <summary>
    /// 画像の高さを取得または設定します。
    /// </summary>
    public int Height { get; set; }

    /// <summary>
    /// ピクセルデータの開始位置を取得または設定します。
    /// </summary>
    public int PixelOffset { get; set; }

    /// <summary>
    /// ファイル形式を示す文字列を取得または設定します。
    /// </summary>
    public string Format { get; set; };
}

ヘッダを解析する

解析を始める前に、まずは必要となるメソッドをいくつか実装していきましょう。ここでは、BinaryUtility というクラスにまとめました。
これは、標準で用意されている BitConverter.ToInt32() や BitConverter.ToInt16()で、リトルエンディアン環境では十分なのですが、自作してしまいましょう。

public static class BinaryUtility
{
    /// <summary>
    /// 指定したバイト列のオフセット位置に格納されている、32ビット無符号整数を取得します。
    /// </summary>
    /// <param name="bytes"></param>
    /// <param name="offset"></param>
    /// <returns></returns>
    public static uint MakeUInt32(byte[] bytes, int offset)
    {
        return MakeUInt32(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
    }

    public static uint MakeUInt32(byte a, byte b, byte c, byte d)
    {
        return (uint)((a) | ((b) << 8) | ((c) << 16) | ((d) << 24));
    }

    /// <summary>
    /// 指定したバイト列のオフセット位置に格納されている、16ビット無符号整数を取得します。
    /// </summary>
    /// <param name="bytes"></param>
    /// <param name="offset"></param>
    /// <returns></returns>
    public static ushort MakeUInt16(byte[] bytes, int offset)
    {
        return (ushort)((bytes[offset]) | ((bytes[offset + 1]) << 8));
    }
}

次のようなメソッドを作成し、読み込んだバイト列から 先ほど定義した DdsSimpleFileInfo クラスに内容を取得します。
先にも書いたように、本来であれば複雑なフラグ類をたくさん解析しないといけないのですが、今回はすごく簡略化していますので、やることは次の4つだけとなります。

  • ヘッダの先頭に "DDS{スペース}" という文字列があるかどうかを調べる
  • 横幅と高さを取得する
  • 圧縮形式が "DXT1" かどうかを調べる
  • ピクセルデータ開始位置を取得する


失敗しても null を返さずに、IsValidがfalseとなっている DdsSimpleFileInfo インスタンスを返すようにしておいたほうが使い勝手が良いでしょう。

以上をまとめてメソッドを作成すると、次のようになるでしょうか。

public static class DdsLoader
{
    public static DdsSimpleFileInfo GetDdsInfo(byte[] data)
    {
        const int HeaderSize = 0x80;

        var info = new DdsSimpleFileInfo();    // 情報クラスを作成

        if (data.Length < HeaderSize) {
            return info;        // DDSとしてのヘッダサイズが足りない
        }

        var magic = Encoding.UTF8.GetString(data, 0, 4);        // data 配列の先頭から連続する4バイトを文字列に変換
        if (magic != "DDS ") {
            return info;        // DDSファイルではない
        }

        // 画像の横幅と高さ
        info.Height = (int)BinaryUtility.MakeUInt32(data, 0x0c);        // data 配列の 12バイト目から連続する4バイトをInt32に変換
        info.Width = (int)BinaryUtility.MakeUInt32(data, 0x10);         // data 配列の 16バイト目から連続する4バイトをInt32に変換

        info.PixelOffset = HeaderSize;        // ピクセルデータの開始位置

        // DXT1形式かどうかをチェック
        info.Format = Encoding.UTF8.GetString(data, 0x54, 4);
        switch (info.Format) {
            case "DXT1":
                break;
            default:
                return info;
        }

        info.IsValid = true;            // ヘッダ取得成功を示すフラグを設定

        return info;
    }
}

さて、ヘッダ情報が解析できるようになったので、それを使ったファイル読み込みのメソッドは次のようになります。

public static class DdsLoader
{
    public static System.Drawing.Bitmap LoadFromFile(string filename)
    {
        // ファイルをバイト列に読み込む
        byte[] data = System.IO.File.ReadAllBytes(filename);

        // ヘッダを解析
        var info = GetDdsInfo(data);
        if (info.IsValid) {
            switch (info.Format) {
                case "DXT1":
                    // DXT1形式なら解凍してBitmapに変換
                    return DXT1Decoder.CreateBitmap(data, info.Width, info.Height, info.PixelOffset);
            }
        }

        return null;
    }
}

ここからはDXT1形式で格納されているピクセルデータを展開していきましょう。

DXT1形式をデコードする

ブロックの構成

DXT1形式では横4ピクセルx縦4ピクセルの情報を圧縮、格納した「ブロック」から構成されています。
1ブロックは必ず8バイトで、次のようになっています。

f:id:sgtech:20161222105215p:plain

いくつのブロックが入っているの?

1ブロックで4x4ピクセルが構成されますが、たとえば1x3や2x2の画像の場合においても、1ブロックが使われることになっています。
つまり、縦、横それぞれのピクセル数を4で割って、余りがあればさらに1ブロック余計に必要になるということになります。

通常、テクスチャとして使用される画像は、512x512 や 2048x1024 などどいった、2のn乗サイズが基本ですので、4の倍数でないということはほとんどないのですが、将来的にミップマップに対応することを考えると、2x2 や 2x1といったサイズも最初から考慮してあげた方が良いでしょう。


まずは、ピクセル数から必要なブロック数を計算する処理をメソッドにしておきましょう。

public static class DXTUtility
{
    /// <summary>
    /// 指定されたピクセルサイズを格納するために必要なブロックの個数を取得します。
    /// </summary>
    /// <param name="pixelSize">ピクセル数</param>
    /// <returns></returns>
    public static int GetBlockCount(int pixelSize)
    {
        return (pixelSize >> 2) + (((pixelSize & 3) != 0) ? 1 : 0);
    }
}

さらに、画像全体でいくつのブロックが格納されているかを調べて、すべて列挙するようなメソッドを作成します。

    public struct BlockInfo
    {
        public int X;        // ピクセル位置X
        public int Y;        // ピクセル位置Y
        public int Offset;    // ブロックデータオフセット

        public BlockInfo(int x, int y, int offset)
        {
            X = x;
            Y = y;
            Offset = offset;
        }
    }

public static class DXTUtility
{
    /// <summary>
    /// 指定された横幅、高さの画像のブロック情報をすべて列挙します。
    /// </summary>
    /// <param name="width">横幅</param>
    /// <param name="height">高さ</param>
    /// <param name="dataOffset">ファイル中のブロック先頭オフセット</param>
    /// <param name="blockSize">1ブロックのバイト数</param>
    /// <returns></returns>
    public static IEnumerable<BlockInfo> EnumerateBlocks(int width, int height, int dataOffset, int blockSize)
    {
        int horizontalBlockCount = GetBlockCount(width);
        int verticalBlockCount = GetBlockCount(height);

        int offset = dataOffset;
        for (int i = 0; i < verticalBlockCount; i++) {
            for (int j = 0; j < horizontalBlockCount; j++) {
                yield return new BlockInfo(j * 4, i * 4, offset);
                offset += blockSize;
            }
        }
    }
}
デコード開始

さてブロックが列挙できるようになったので、デコード機能を徐々に作成していきましょう。
このような仕様のメソッドにしておきましょうか。
DdsSimpleFileInfo クラスを引数に取らず、width, height, pixelOffset に分けて渡す仕様にしているのは、将来的にミップマップに対応する布石とでも考えておいてください。

public static class DXT1Decoder
{
    public static System.Drawing.Bitmap CreateBitmap(byte[] data, int width, int height, int pixelOffset)
    {
    }
}

まずは縦横それぞれのブロック数を取得し、それに4を掛けて、物理的なピクセル数を計算し、展開先のバッファを用意します。
展開先のバッファサイズは、
横物理ピクセル数 x 縦物理ピクセル数 x 4バイト (RGBA)
となります。

その後、すべてのブロックを処理するようループを記述します。

    int BlockSize = 8;

    int physicalWidth = DXTUtility.GetBlockCount(width) * 4;
    int physicalHeight = DXTUtility.GetBlockCount(height) * 4;

    var pixels = new byte[physicalWidth * physicalHeight * 4];

    var blocks = DXTUtility.EnumerateBlocks(width, height, pixelOffset, BlockSize);

    foreach (var block in blocks) {
        // ここで block をデコードし pixel 配列の該当位置にRGBAデータを展開する
    }
カラーの取り扱い


DXT1ブロックの色情報は、 RGB成分がそれぞれ5ビット、6ビット、5ビットとして16ビットにパックされています。
この状態から色の計算を簡潔に記述するため、次のような構造体を作成しておきましょう。

public struct ColorRgba
{
    public float R; // 赤成分(0.0~1.0)
    public float G; // 緑成分(0.0~1.0)
    public float B; // 青成分(0.0~1.0)
    public float A; // アルファ成分(0.0~1.0)

    /// <summary>
    /// ColorRgba構造体の新しいインスタンスを初期化します。
    /// </summary>
    public ColorRgba(float r, float g, float b, float a)
    {
        R = r;
        G = g;
        B = b;
        A = a;
    }

    /// <summary>
    /// RGB565形式の16ビットカラーデータから ColorRgbaインスタンスを生成します。
    /// </summary>
    public static ColorRgba FromRgb565(ushort rgb565)
    {
        int r = (rgb565 >> 11) & 0x1f;
        int g = (rgb565 >> 5) & 0x3f;
        int b = (rgb565 >> 0) & 0x1f;
        return new ColorRgba(r * 1.0f / 31.0f, g * 1.0f / 63.0f, b * 1.0f / 31.0f, 1.0f);
    }

    /// <summary>
    /// 指定された2つのカラーをリニア補間して中間色を求めます。
    /// </summary>
    public static ColorRgba Lerp(ColorRgba x, ColorRgba y, float s)
    {
        float r = x.R + s * (y.R - x.R);
        float g = x.G + s * (y.G - x.G);
        float b = x.B + s * (y.B - x.B);
        float a = x.A + s * (y.A - x.A);
        return new ColorRgba(r, g, b, a);
    }
}
1ブロックのデコード

まずはブロックの先頭2バイトからカラー0,次の2バイトからカラー1を読み込みます。

    ushort color0 = BinaryUtility.MakeUInt16(data, block.Offset);
    ushort color1 = BinaryUtility.MakeUInt16(data, block.Offset + 2);

この color0 と color1 から、4x4のブロックの各ピクセルの色を計算で求めていきます。

4x4の各ブロックの色は2ビットのインデックスとなっています。インデックスが2ビットということは0,1,2,3のいずれか、つまり、4色の中からどれか1色を選んで、そのピクセルの色を決めるということになります。
そこで、あとでインデックスで引きやすいよう、4色を格納するための配列を作成します。

    var colors = new ColorRgba[4];
アルファの判定と4色の求め方

DXT1のブロックには、いわゆる1ビットアルファ(パンチスルー)の情報を格納することもできます。
color0 と color1 を16ビット無符号整数として単純に大小を比較し、どちらが大きいかによって、

  • 4色すべてを色情報として扱う
  • 3色の色情報+透明

のどちらかを選んで格納することができます。
4色の場合は、カラー0, カラー1, そしてそれらの中間色を2色、
3色+透明の場合は、カラー0,カラー1, 中間色1色、透明、を求めます。

次のようなコードでそれを判定、計算し、カラーテーブルの内容を求めています。

if (color0 > color1) {
    // アルファなし。使える色は4色
    colors[0] = ColorRgba.FromRgb565(color0);                            // カラー0そのまま
    colors[1] = ColorRgba.FromRgb565(color1);                            // カラー1そのまま
    colors[2] = ColorRgba.Lerp(colors[0], colors[1], 1.0f / 3.0f);       // color_2 = 2/3*color_0 + 1/3*color_1
    colors[3] = ColorRgba.Lerp(colors[0], colors[1], 2.0f / 3.0f);       // color_3 = 1/3*color_0 + 2/3*color_1
} else {
    // アルファあり。使える色は3色
    colors[0] = ColorRgba.FromRgb565(color0);                            // カラー0そのまま
    colors[1] = ColorRgba.FromRgb565(color1);                            // カラー1そのまま
    colors[2] = ColorRgba.Lerp(colors[0], colors[1], 1.0f / 2.0f);       // color_2 = 1/2*color_0 + 1/2*color_1
    colors[3] = new ColorRgba(0, 0, 0, 0);                               // 透明
}
インデックスの展開

4色のテーブルの内容が決まりましたので、ここからは 4x4の各ピクセルにその内容を書き込んできます。
インデックスの情報は2ビットx16ピクセル分ということで、32ビットに収まりますので、uintの変数に読み込んでしまいましょう。
そして、そこから下位2ビットずつ取り出し、4色のテーブルの色を参照していきます。
ピクセルデータ書き込み先には、0~255の形でB,G,R,Aの順でカラーを格納していきます。

uint indexBits = BinaryUtility.MakeUInt32(data, block.Offset + 4);

for (int y = 0; y < 4; y++) {
    for (int x = 0; x < 4; x++) {
        var idx = indexBits & 0x03;                   // インデックスの取り出し
        var col = colors[idx];                        // カラーテーブルからインデックスでカラーを取り出す
        int xx = block.X + x;
        int yy = block.Y + y;
        int p = (xx + yy * physicalWidth) * 4;        // ピクセルの書き込み位置
        pixels[p + 0] = (byte)(col.B * 255.0f);       // 青(0~255)
        pixels[p + 1] = (byte)(col.G * 255.0f);       // 緑(0~255)
        pixels[p + 2] = (byte)(col.R * 255.0f);       // 赤(0~255)
        pixels[p + 3] = (byte)(col.A * 255.0f);       // アルファ(0~255)
        indexBits >>= 2;                              // インデックスを2ビット右シフトして次に備える
    }
}

ここまでの処理で、byte[] pixels という配列にピクセルデータが展開できました。
あとはこれをSystem.Drawing.Bitmapにしてあげれば、WinFormのアプリで取り扱いやすくなりますね。

System.Drawing.Bitmap のコンストラクタには、何と都合の良いことに「ストライド値」を指定できるバージョンがあります。
ストライド値とは一般的に、「次のデータは何バイト先から始まるか」ということを示すために使われる値のことを示します。
今回の場合、「次のピクセル行は何バイト先に格納されているか」あるいは「1行あたりのバイト数はいくつか」という意味になります。
画像の横幅が4の倍数でない場合、例えば5ピクセルだったとすると、作成したいビットマップは横幅5ピクセルですが、展開済みの byte[] 配列には
横8ピクセル分のデータが詰まっていることになります。
そこで、このストライド値を使用して、「次の行は 8 x 4 = 32バイト先から始まるよ」」ということを指定してあげます。

今回の場合、 physicalWidth * 4 を指定すれば良いでしょう。

unsafe {
    fixed (byte* ptr = &pixels[0]) {
        var bmp = new System.Drawing.Bitmap(width, height, physicalWidth * 4, PixelFormat.Format32bppArgb, new IntPtr(ptr));
        return bmp;
    }
}

ストライド値を使用したコンストラクタを使用するには、 byte[] 配列を IntPtr というポインタの形にして渡さなければなりませんので、unsafeコードが使えるようにコンパイラオプションを変更します。

f:id:sgtech:20161222105256p:plain

こうして得られた System.Drawing.Bitmap インタンスは、そのままPictureBoxなどに表示することができます。

テストしてみましょう

DDSファイルを何度かドロップしていると、何やら例外が発生してしまいました。

f:id:sgtech:20161222105214p:plain
f:id:sgtech:20161222105216p:plain

どうやら、上記の方法で作成したBitmapは、うまく取り扱えない場合があるようです。
ネットで検索したところ、fixed で固定したポインタを使用して作成したBitmapは、そのメモリがガベージコレクタによって移動されないことが前提となっているようです。
ストライド値が指定でき便利だったのですが、このままでは普通に使うことができず不便なので、ひと工夫しましょう。

簡単に解決する方法の1つは、fixed スコープから出ないうちに、別のビットマップにコピーしてしまうことです。
これで例外は起きなくなりました。

がしかし、そもそも作成したいビットマップの幅とストライドが同じ場合は、ストライドを指定するバージョンのコンストラクタを使用する必要はないわけですので、それを判定し、ごく一般的な方法でビットマップを作成します。
処理が少し複雑になりますので、メソッドにしてしまいましょう。

public static Bitmap CreateBitmap(byte[] pixels, int width, int height, int physicalWidth)
{
    if (width == physicalWidth) {
        // 作成したいビットマップの幅とストライドが同じ場合
        var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
        var bd = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, bmp.PixelFormat);
        System.Runtime.InteropServices.Marshal.Copy(pixels, 0, bd.Scan0, physicalWidth * 4 * height);
        bmp.UnlockBits(bd);
        return bmp;
    } else {
        // 作成したいビットマップの幅とストライドが異なる場合
        unsafe
        {
            fixed (byte* ptr = &pixels[0]) {
                // アンマネージドポインタを利用して作成したBitmapは fixed スコープ外に出るとAccess Violationを引き起こす
                // そこで、別のBitmapにコピーしてそれを返す。元のBitmapは不要なので usingを使用してDisposeする。
                using (var tmpbmp = new Bitmap(width, height, physicalWidth * 4, PixelFormat.Format32bppArgb, new IntPtr(ptr))) {
                    var bmp = new Bitmap(tmpbmp);
                    return bmp;
                }
            }
        }
    }

さて、これでいちおう完成となりました!
テストプログラムも改良し、情報を表示するようにもしています。

f:id:sgtech:20161222105258p:plain

BC1形式にも対応する

ヘッダ解析部分の簡単な拡張で、 DirectX10以降の新しい形式である「BC1形式」に対応することも可能ですので、一緒にやってしまいましょう。
このBC1形式のピクセル格納形式はDXT1と同じであるため、ピクセルオフセットを変更するだけで先程のDXT1Decoder.CreateBitmapメソッドで展開できてしまいます。

DirectX10形式のDDSファイルは、ヘッダのサイズが20バイト増え、148バイトになっています。従ってピクセルデータの開始オフセットもそれだけ後ろにずれることになります。

DecodeDXT()メソッドにピクセルオフセットを指定できるようにしておいたことが役に立った形となりました。

f:id:sgtech:20161222105300p:plain

この内容に従って、BC1の判定部分を追加しましょう。

switch (info.Format) {
    case "DX10":
        info.PixelOffset = HeaderSize + 20;    // DX10形式ではヘッダサイズが異なり、ピクセル開始位置もずれます。
        {
            // DX10形式であることが確認できたら、さらにBC1形式かどうかを調べる。
            uint format = BinaryUtility.MakeUInt32(data, 0x80));
            switch (format) {
                case 0x46:    // BC1_TYPELESS
                case 0x47:    // BC1_UNORM
                case 0x48:    // BC1_UNORM_SRGB
                    info.Format = "BC1";
                    break;
                default:
                    return info;
            }
        }
        break;
    default:
        return info;
}

:
:

    case "DXT1":
    case "BC1":
        return DXT1Decoder.CreateBitmap(data, info.Width, info.Height, info.PixelOffset);

最後に

さて、いかがでしたでしょうか?
今回の内容で、 DXT1形式、BC1形式のDDSファイルを System.Drawing.Bitmap に変換することができました。

この応用で、DXT3,DXT5,BC2,BC3,BC4,BC5といった圧縮形式への対応も、比較的簡単にできるのではと思います。
今後また機会がありましたら、次はぐっと複雑な、BC7形式を展開するコードの説明ができればと思っております。

なお、今回の記事を執筆するにあたり、多くのサイトを参考にさせていただきました。この場を借りてお礼を申し上げます。

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

Powered by はてなブログ