CEDEC 2019 セガグループによるセッション紹介!

皆さんこんにちは!
セガゲームス、第3事業部の麓です。

今年でSEGA TECHBogもこの8月に4年目を迎え、とても多くの方々がこのブログを知り、読んでくださるようになってきまして、続けることの大切さを噛み締めているところです。

来月にパシフィコ横浜で開催されるゲーム業界最大のカンファレンス

CEDEC2019

会期:2019年9月4日(水)~9月6日(金)
にセガグループ(および関連会社)からも何名か今年も登壇します!
それでは今回で4回目となる、セガグループ関係者によるセッションと登壇者紹介に加え、ここでしか見れない講演者からのメッセージや、当日の資料からの抜粋等、紹介します。

 

セッション

スクラムチームでモブプロ!-立ちはだかる導入・運用の壁とその成果-

セッション内容と講演者より

私たちは約3年、スクラムチームとして開発業務に携わっています。
事業に貢献したい、属人化を解消したい、新しい事にチャレンジしたいという課題を解決する手段を探してる中で、モブプロの導入を決めました。
導入を決めたものの、モブプロの導入を思い描いた方ならすぐに直面する壁からエンジニアならではの壁など、いくつかの壁に直面しました。
その壁をチームがどう乗り越え、どう成長し、どのような成果が生まれたのか、実践から感じたメリットやデメリットを含め、お話させていただきます。
チームが抱える課題を分析しその解決手段を模索している方へ、参考にしていただければ幸いです。

講演者

株式会社セガゲームス DMS事業部 システム開発部 研究開発2課 課長

横島 太志

09月04日(水) 15:20 〜 15:45(ショートセッション)

cedec.cesa.or.jp

スナップショット

 

f:id:sgtech:20190816164322j:plain

f:id:sgtech:20190816164332j:plain

 

セッションについて

役割を分担開発を行うスピード感とは違った側面にあるモブプロにおけるスピード感の実感など、予想外の効果についても触れさせていただきます。

 

Mesh Effect Shape:2Dカートゥーン調デザインから、その魅力のままにアニメーションする3Dエフェクトメッシュを軽量かつ効率良く作る!

セッション内容と講演者より

Marzaが久しぶりにCEDECへ戻ってきました。
水、火、煙などのエフェクトを2Dカートゥーン調デザインの魅力そのままの形で3Dオブジェクトとして制作できる手法をご紹介します。
デザインを重視したアプローチに始まり、カスタマイズされたスカルプトツールを使い工程によってはツールで自動化し、メッシュのレゾリューションも自由にコントロールできるなど、バリエーションに富んだ機能を用意し、直感的にボリュームを持ったシェープを作ることが可能です。
本セッションのエフェクトはUnityを使用した弊社の新作ショートフィルムで使われていますので、その実例と共にご紹介します。

使用ツール:Python、2Dペイント、 Maya、Unity

講演者

マーザ・アニメーションプラネット株式会社

 

映像事業1部 リアルタイムチーム チームマネージャー 高橋 聡

技術課 Assistant Technical Director Brent Forrest

技術課 Technical Director 松成 隆正

09月05日(木) 11:20 〜 12:20

cedec.cesa.or.jp

スナップショット 

f:id:sgtech:20190816170139j:plain

f:id:sgtech:20190816170202j:plain

セッションについて

ゲームエンジンを利用した新たな進化を遂げたMarzaの新作と共に、手描きの魅力を真にCGの世界に落とし込んだ、ユニークなエフェクトをお届けします。
また、表現の進化だけではなくパイプライン面での進化もお話しします。

 

ラウンドテーブル

ワーママ・ワーパパたちの働き方と悩み/御社の悩みは何ですか?他社はどう解決しているの?

セッション内容

<<2017年から始まったラウンドテーブルです/CEDECでの恒例化を目指しています>>

2018ラウンドテーブルでは、

受講者の皆さんに、

「私たち1人1人の動きで、働きやすいゲーム会社環境を作っていきましょう」

「来年のCEDECまでに、何か1つ新しいアクションを起こしましょう」

「きっと、何かが変わっていきます」

と呼び掛け、終了しました。

この1年、皆さんの周りで、何が変わったでしょうか?

引き続き、抱えている問題点は何でしょうか?

自社だけでは解決できない問題があれば、皆に相談しましょう。話し合いましょう。

皆さんが抱えている問題の、糸口が見つかる場にしたいです。解決が難しい問題でも、相談できる相手がいるだけで、勇気づけられるものです。

冒頭で、セガでのその後1年の動きを簡単に説明します。

5グループ程度に分かれて、皆さんの会社では、この1年でどのような動きがあったか、働きかけを行ったか、等、情報の共有をしていただきます。

現在抱えている悩みに関しても、共有して下さい。

その後、グループごとに発表。

発表された各社の内容に、意見や、質問がある場合は、質疑応答を行います。

他社事例の情報は、自社へ持ち帰り、社内で情報の共有を行っていただきたいです。それぞれの会社で、さらなる1アクションを興こすキッカケにしたいと願っています。業界の、働き方改革に繋げていきましょう。

講演者

株式会社セガゲームス エンタテインメント事業本部 第4事業部

茂呂 真由美

株式会社セガインタラクティブ コンテンツ研究開発本部 デザイナー

鈴木 こずえ

09月04日(水) 11:20 〜 12:20

cedec.cesa.or.jp

スナップショット

f:id:sgtech:20190816172117p:plain

 

f:id:sgtech:20190816172440j:plain f:id:sgtech:20190816172412j:plain

講演者からのコメント

他社様と情報共有することにより、直面しうる問題と、具体的な対応例を知ることができます。
ここで刺激を受け、自社に持ち帰った後、自ら、何か行動を興していただきたいのです。
業界全体のワークライフバランス、意識改革に繋げていきたい、という目的で主催しました。

私たち1人1人の動きで、働きやすいゲーム会社環境を作っていきましょう。変えていきましょう。
Keep on Moving!

 

「ラウンドテーブル」ですので、悩みを抱えている方、共有したい自社事例のある方は、積極的にテーブル席についていただければと思います。
見学希望の方は、ラウンドテーブルが始まったら、各テーブルの話しが聞こえる場所に移動して、是非、現場の生の声を聞いて下さいね。
(立ちたくない!という方は、椅子ごと移動していただければ!)

セッションについて

ラウンドテーブル開催前、セガでのその後1年の動きを5分程度で説明、共有します。
社内共通コミュニケーションツール「Office365 Teams」を利用して、グループ会社間でのシナジーをどう高めていったか、活用法もご紹介します。

 

日々の業務から少しずつ始める!TA育成について話すラウンドテーブル

セッション内容

本ラウンドテーブルは、昨年度の「若手テクニカルアーティスト(以下、TA)の育成とその役割について話すラウンドテーブル」、および、一昨年度の「若手TAの業務効率改善への貢献、育成について話すラウンドテーブル」の議論に続くものです。

近年、その需要から各社でTA業務を専門で行う若手を育成しようという動きが広がっています。昨年、一昨年のラウンドテーブルを通じて、TA育成について若手にTA業務をやらせてみること、その機会を与えることが大事ではないかと議論が交わされました。しかし、そのようなTAの育成を行うためには育成する側の環境や人材にある程度の余裕が必要になり、現実的には余裕がないためTA育成に向けた活動を行うことは困難の場合が多いという課題も判明しました。 

そこで今年は、実業務へのアサインを通じたTA育成に焦点を当て、日々の業務へのアサインから少しずつTAを育成していくことはできないか、どのような業務へアサインすることがTAとしての知見を得ることにつながるのかをラウンドテーブルという形で突き詰め共有し、業界への貢献へとつなげていきたいと考えています。

講演者

株式会社セガゲームス 第3事業部 第3開発2部 テクニカルサポートセクション プログラマー

清水 宣寿 他

09月04日(水) 14:50 〜 15:50

cedec.cesa.or.jp

講演者からのコメント
CEDEC2018で「若手テクニカルアーティストの育成とその役割について話すラウンドテーブル」で登壇した清水です。 昨年に引き続き、テクニカルアーティスト(以下、TA)の育成についてのラウンドテーブルを開催いたします。
今年は、「日々の業務から少しずつ始める!TA育成について話すラウンドテーブル」です。 昨年のラウンドテーブルではTAの育成について、機会と環境を与えることができれば十分に育てることができる。というまとめになりました。 今回はその機会を増やすために、実業務を通じて少しづつTAを育成していくことはできないか?という考えのもと、TAスキルを伸ばせる実業務について議論します。 当日は話のタネになるように、登壇者7名がTAスキルを伸ばせた実業務をまとめた表を持っていきます。
一緒にTA育成について議論を交わし合いませんか? ご参加、お待ちしています。

パネルディスカッション

ゲーム開発におけるOSSライセンス管理の実際 (パネルディスカッション)

セッション内容

本セッションでは、複数の企業のOSS管理の責任者にお集まり頂き、各社での取り組みをシェアいただきます。

具体的な管理法、注意すべきOSS、困ったライセンスなどをご紹介頂きます。

参加者のOSS管理への啓蒙、実際の問題の解決に役立つ情報をご提供します。

講演者

株式会社セガゲームス 開発技術部 課長

山中 勇毅 他

cedec.cesa.or.jp

 

今回紹介したセッションで聴講したい!と思ったセッションはありましたか?
多くの開発者の集まるCEDECは、パシフィコ横浜 会議センター(神奈川県横浜市西区みなとみらい)で9月4日(水)~9月6日(金)の間、開催されます。

CEDECに参加して情報収集や交流をし、ゲーム業界の今を知り、業界の未来について語り合いませんか?
それでは皆さんCEDECでお会いましょう!

 

私達は将来CEDECに登壇してみたいと思っている、技術に興味のある方を求めています。
そんな貴方、以下にアクセスしてみませんか?

 

www.sega.co.jp


 

最後に、今月はこのSEGA TECHBlogの運営についてCGWORLD.jpに取材いただきまして、記事として公開もされました。

これまでの3年間、このブログを通じで何が起きて、生まれてきたか、これからのアイデアなどお話しさせていただいています。

よかったらこちらも合わせて読んで頂ければ幸いです!!

cgworld.jp

 

※複数社登壇の場合でもセガの社員のみ表記しています 

『Readyyy!』でのLive2D活用事例

2019年6月末でサービスを終了した『Readyyy!』。その『Readyyy!』にテクニカルアーティストとして参加していましたセガゲームスの宮下です。『Readyyy!』をプレイしてくださった皆様、本当にありがとうございました。


また、社内はもちろん、大勢の社外の方々のお力添えでリリースした『Readyyy!』でしたが、アプリのサービスを早々に終了させる結果になってしまったこと、誠に申し訳ありませんでした。スタッフ一同一丸となって取り組みましたが、力及びませんでした。

そんな中『Readyyy!』をプレイしていただき、サービス終了を惜しんでくださる皆様からは、厳しいご意見をはじめ、心温まるお手紙、想いの詰まった色紙などをいただきました。皆様のお声は開発チームまでちゃんと届いており、スタッフ一同ありがたく読ませていただいております。そして、そのような𠮟咤激励のお声にどう応えていくのか、日々考えながら業務に励んでおります。


テクニカルアーティストの自分と致しましては、『Readyyy!』の開発を通じて得た知見や技術を、この「セガテックブログ」の場で伝え、そして楽しんでいただくことで、皆様からのお声に少しでも応えられたらと思い、このブログを書かせていただきました。


今回『Readyyy!』でのLive2D活用事例として2つご紹介します。

1つはライティング表現で、主にその仕組みやテクスチャの使い方に関する内容です。すでにプレイできなくなっているアプリの画面などを用いての説明となってしまい、重ね重ね申し訳ありません。

もう1つは、今後も『Readyyy!』のアイドルたちの姿を届けられることになったコンテンツ、多人数の同時生配信についてです。こちらは、多人数同時生配信の仕組みと、それを作り上げていくスタッフとのやりとりの様子をお伝えします。もしかするとその当時のやりとりで気になるところもあるかもしれませんが、お付き合いいただけますと幸いです。


それでは、始めさせていただきます。


Live2D®とは?

Live2Dとは、元のイメージを保ったままイラストを動かしたり、疑似的に3Dのように見せられる特徴を持つ、株式会社Live2D*1が開発した技術やソフト、データのことです。ソフトの正式名称は「Live2D Cubism」と言います。「Cubism SDK 3.0*2」でUnity*3からLive2Dへのアクセス方法が一新されたのですが、柔軟性が高くとても良い出来だったので『Readyyy!』で採用することに決めました。ただし「Live2D Cubisim」と「Cubism SDK 3.0」だけではライティングの実現には足りていません。新たなシェーダーとコンポーネント*4の開発、それを活用するための特別なデータが必要でした。

Live2Dライティング表現のあれこれ

f:id:sgtech:20190726221016g:plain:w500

『Readyyy!』のLive2Dでのライティング表現が一部で話題*5になっていましたが、開発初期にプロトタイプを部内で紹介したときも、驚きの声とともに「力技?!」という声があがりました。そのときは「力技とは心外だな」と内心ふくれていたのですが、いまにして思えば…まあ力技ですね。やっていることは単純なんです。

それでは、『Readyyy!』のアイドルの1人、上條雅楽(かみじょううた)君に手伝ってもらって、ライティングの仕組みを説明していきます。

f:id:sgtech:20190725002118p:plain
『Readyyy!』の上條雅楽君

使っているテクスチャについて

ライティングを実現するために、1ポリゴンにつき3つのテクスチャを使ってます。具体的には、アルベドマップ(色)、シャドウマップ(影)、スペキュラマップ(光沢)です。
「Live2D Cubism」はマルチテクスチャに対応していないので、この3つのテクスチャを生成するために、3回の出力作業が必要ですが、この面倒な作業はSikuliX*6を使ってほぼ自動化しています。UV座標はLive2Dのものをそのまま利用して、ライティングの主な処理はUnity上のシェーダーで行っています。

アルベドマップ

まず、体と顔のアルベドマップが1枚ずつ、計2枚あります。画像サイズは1024x1024ピクセルと600x600ピクセル*7で、フォーマットはRGBA Crunched ETC2を使っています。これはUnity2017.3から導入されたフォーマットで、ストレージの消費量削減に抜群の効果があります。それまでは、JPEGファイルをダイレクトに使う計画で、その場合だと、RAMへの展開後にTrueColorになってしまい、メモリ消費量も膨大だったので、このフォーマットには助けられました。

なおRGBA Crunched ETC2はノイズが目立つので、軽減するようにアルベドマップのサイズは比較的大きめにしてあります。ノイズの軽減については、またあとで説明しますね。

f:id:sgtech:20190725002113j:plain
アルベドマップ

シャドウマップとスペキュラマップ

続いて、シャドウマップとスペキュラマップです。

f:id:sgtech:20190725002216j:plain
シャドウマップ(上)とスペキュラマップ(下)

シャドウマップとスペキュラマップに関しては、グレースケールで、かつ、ちょうど合計4枚なので、劣化しないTrueColorフォーマットを使ってRGBAチャンネルにパッキングしています。先ほども言いましたが、TrueColorだとメモリ消費が大きいので、画像サイズをぎりぎりの380x380ピクセルまで下げています。ETC2フォーマットで圧縮したケースも試してみましたが、見栄えが悪かったので不採用にしました。

f:id:sgtech:20190725002108p:plain
パッキングされたシャドウマップとスペキュラマップ

陰影のバリエーションについて

ちょっとややこしいのですが、アイドル1ポーズにつき、シャドウマップとスペキュラマップをそれぞれ3種類用意しています(下の画像の左から、逆光左順光右順光)。これは太陽や照明の位置の違いを表現するためで、このバリエーションはライティングの効果を発揮するための大切な部分です。なぜなら、これが1種類しかないと、この一連の仕組みはあまり意味がなくなってしまうからです。

f:id:sgtech:20190725002155j:plain
シャドウマップのバリエーション例

テクスチャの種類のまとめ

マップの種類 画像サイズ 作業上の枚数 実際の枚数 画像
アルベド 1024x1024 2 2 f:id:sgtech:20190725002113j:plain:w300
シャドウ、スペキュラ 380x380 2x3、2x3 3 f:id:sgtech:20190725002213j:plain:w300

ここまでの説明を表にまとめてみました。つまり1ポーズあたり5枚のテクスチャが必要ということです。なお『Readyyy!』では、アイドル1人に対してポーズを7種類用意する予定でした。また衣装差分は5種類あるため、5x7x5で合計175枚のテクスチャが1人あたりに必要となります。なかなかのボリュームですね。

シャドウマップとスペキュラマップの縮小

少し話を戻します。シャドウマップとスペキュラマップの画像サイズを下げた(380x380ピクセル、下の画像の左)ということは、アルベドマップ(1024x1024ピクセル)と比較して、相当ぼけているということなんですが、思いの外うまくなじんでいます。それどころか、64x64ピクセルまで縮小しても破綻しないんですね。

解像度を下げても破綻しないというのは、いわゆる3Dのシャドウマップと似ていますね。

ノイズ軽減のためアルベドが大きめという理由もあるのですが、シャドウマップとスペキュラマップはアルベドマップと同じサイズであるべきという固定観念にとらわれていたので、個人的にこの発見は大きいものでした。

f:id:sgtech:20190725002204j:plain:w1000
シャドウマップのサイズによる見た目の変化

アートディレクターからのチェックで、縮小は380x380ピクセルで止めたのですが、曇り空のときは、右の64x64ピクセルバージョンを使うように提案してみてもおもしろかったですね。

シェーダーでレイヤー合成

シェーダー上で、先ほどのアルベドマップとシャドウマップを乗算し、スペキュラマップを加算してライティングを実現しています。

fixed4 main_tex = tex2D(_MainTex, IN.texcoord);
fixed4 packed_tex = tex2D(_PackedTex, IN.texcoord);
fixed3 albedo = main_tex.rgb;
fixed3 shadow = packed_tex.rrr;
fixed3 specular = packed_tex.ggg;
// 計算を省略しています!
fixed3 clr = albedo * lerp( IN.ambient, fixed3(1,1,1), shadow ) + specular; 
return fixed4( clr.rgb, main_tex.a );

Photoshopのレイヤー合成みたいなことをシェーダーでやっているわけですね。

f:id:sgtech:20190725002125p:plain
アルベドのみ
f:id:sgtech:20190725002130p:plain
シャドウのみ

また、 影の色味の違いを表現するために、頂点カラーを利用しています。頂点カラーは「Live2D Cubisim」上で直接設定可能なのですが、ヒューマンエラーが発生しそうなので、ノード名に特定のキーワードを入れてもらって、Unityでのインポート時に、自動的にキーワードから頂点カラーを設定するようにしました。ただし、間接的な方法を採用したことで「Live2D Cubism」上で色を視認できないという弊害が発生してしまい、肌なのに服のキーワードになっているなどの設定ミスが多発することになりました。チェックにも修正にも結構時間がかかってしまったので、この方法は、もっと検討改良すべきでした。

髪の毛(赤色)、肌(緑色)、服とヘッドセット(青色)として設定していますが、この色をダイレクトに乗算しているわけではありません。

f:id:sgtech:20190725002136p:plain
頂点カラーのみ

あくまで質感の種類を識別するために使っていて、このようなシェーダーコードにしています。

ambient = _HairAmbient*v.color.r + _SkinAmbient*v.color.g + _ClothAmbient*v.color.b;

本来質感が異なる場合は、マテリアルを分けて表現しますが、今回はドローコール数を増やしたくないため、このような仕組みにしています。

f:id:sgtech:20190725002118p:plain
合成結果

桜や建物などの落ち影

Live2Dへの落ち影は、かなり反響があったので、うれしかったですね。桜や建物の落ち影用のテクスチャは、必要な背景にだけ用意しており、スクリーン座標系を使ってシェーダーで計算しています。

f:id:sgtech:20190725002142p:plain
桜の落ち影


f:id:sgtech:20190725002148p:plain
落ち影テクスチャ

スクリーン座標系はUV座標系よりも単純で、シェーダーでよく使うテクニックです。Live2Dに対して、落ち影テクスチャをプロジェクションマッピングしているといったほうがわかりやすいかもしれません。

『Readyyy!』では、Live2Dを半透明化させて消すときに、一時的にレンダーテクスチャを使っています。そのときにスクリーン座標系が画面全体ではなくレンダーテクスチャの大きさのものに変化にしてしまうため、同じ計算方法のままだと、落ち影がずれてしまうのです。その問題に対応するために、少し面倒な計算になっています。

half4 screenPos = ComputeScreenPos(OUT.position);
half2 uv2 = screenPos.xy / screenPos.w;
// _ViewportScaleX等は、CPU側で計算している
uv2.x = uv2.x * _ViewportScaleX + _ViewportOffsetX;
uv2.y = uv2.y * _ViewportScaleY + _ViewportOffsetY;

言い忘れていましたが、背景ごとのライティング用パラメーターの設定は、手動で行っています。

f:id:sgtech:20190725002249g:plain

逆光陰影のクオリティアップ

もともと逆光陰影は1段階でした。下は2段階と1段階の比較画像です。

f:id:sgtech:20190725002231j:plain

1段階のものでは物足りなさを感じていたので、順光時の陰影とブレンドして陰影を2段階とする手法を提案し承認されました。手作業でテクスチャをブレンドするのは面倒なので、スクリプト言語のRuby*8とRMagick*9という画像処理ライブラリで自動化しました。

そういえば、最近、RMagickのメモリ使用量を減らしたという素晴らしい記事を見つけまして、一人で大興奮していました。RMagickのメモリリークには、苦労させられていたので!ありがとうございます!うれしさのあまり、テクニカルアーティストの同僚たちにもチャットで共有したのですが、特に反応はありませんでした。テクニカルアーティストの使うスクリプト言語は主にPython*10ですから、まあそりゃそうですよね。

watson.hatenablog.com

RGBA Crunched ETC2のノイズ軽減について

RGBA Crunched ETC2のストレージ上での圧縮性能はとても高いのですが、そのかわりノイズが発生します。このノイズに対抗するには、テクスチャを大きくすればいいのですが、比例してRAMとストレージの消費量も増えるので、それとクオリティとを天秤にかけることになります。『Readyyy!』ではLive2Dのテクスチャの他に、フォトにもこのテクニック(大きくするだけなんですが!)を使っています。

フォトとはガチャで引くことのできる、アイドルの写真のことです。

f:id:sgtech:20190725002059j:plain
『Readyyy!』のフォト

『Readyyy!』は画面サイズ1334x750ピクセルの想定で作っているので、画面サイズに近い1336(4の倍数)、画面サイズを大きく超えた2048、4096ピクセルの3種類のフォト用の画像で、ノイズがどのように変化するか比較してみました。

横幅(ピクセル) RAM(MB) ストレージ(MB) フィット後に切り抜いた一部画像
1336 4 4 (参考データ)TrueColorだった場合
1336 1.2 0.2 f:id:sgtech:20190725002220p:plain:w300
2048 2.8 0.45 f:id:sgtech:20190725002224p:plain:w300
4096 12 1.3 f:id:sgtech:20190725002228p:plain:w300


上は、1334x750ピクセルの画面サイズにフィットさせて、一部分を切り抜いた画像と容量の表です。4096ピクセルのものが一番きれいですが『Readyyy!』ではバランスを取って2048ピクセルのものを採用しました。

ここで「フォトにはアルファチャンネルがなさそうなのに、なぜRGBA Crunched ETC2フォーマットを使うのか?」という疑問をもつ方がいるでしょう。その理由はiOSとAndroid両方で有効なCrunchedなフォーマットがRGBA Crunched ETC2しかないからです。ですから実はアルファチャンネル分、無駄にRAMを使っているのですが、それを踏まえてもストレージ上での圧縮性能が魅力的ということなのです。

最後のは少し余談でしたが、Live2Dのライティング表現については、以上です!

つづいて「多人数による同時生配信」に話を移しましょう。

多人数による同時生配信

f:id:sgtech:20190725002236j:plain
SHOWROOMさんでの生配信の様子

IPプロデューサーのOさんから、生配信の相談は突然やってきました。

「2月14日のバレンタインデーに、アイドル2人で生配信をやりたい!」

2人同時に生配信するには、リアルタイムでアイドル2人の映像を生成して、配信サービスを提供しているSHOWROOMさんのサーバーに転送する必要があります。私がLive2Dのデータ組み込みやライティングを含めた実装まわりを担当していたので、引き受けることになりました。

ところで、この生配信の見た目って何かに似ています。そう『Readyyy!』のアドベンチャーパートです。

f:id:sgtech:20190725002200j:plain
『Readyyy!』のアドベンチャーパート画面

事前に収録した音声を使っているのと、スクリプターさんが、あらかじめ設定したポーズを表示している点が異なりますが、要するに、この生配信というのは『Readyyy!』のアドベンチャーパートのようなものをリアルタイムで生成しようという試みなんです。

アイドル1人での生配信は、SHOWROOMさんのスマホアプリ「SHOWROOM V」で確立されていましたが、2人の仕組みは備わっていなかったので、セガ側で用意する必要がありました。

そのときは配信日までの時間と生配信に関する知識が不足していて、とても間に合わせる自信がなかったので、Oさんとの相談の結果、1か月後の「3月14日のホワイトデー」までに2人同時生配信の仕組みを用意する、ということになりました。リハーサルのことを考慮するとその1週間ぐらい前が締め切りですね。

ところで、Live2Dをバーチャルキャラクターとして操作するツールというと、「FaceRig」が有名ですね。

store.steampowered.com

『Readyyy!』のLive2Dは、アイドル1人に対して7種類のポーズがあるのですが、これは「FaceRig」でいうところのアバターが7種類あるという意味になります。ネットの情報を調べた限りでは「FaceRig」は多機能なんですが、生配信中に7つのアバターを切り替えていくやり方には向いていないと判断し(もし問題なくできるとしたらごめんなさい!)、Unityで簡易的なアプリを作ることにしました。

2人同時生配信の誕生

こうして会社のヘッドセットを借りての検証と開発の日々が始まりました。自席でマイクに向かって声を出す恥ずかしさと言ったら…。なので人が少なくなった夜中に声を出したり、会議室を取ってそこで検証したり、いろんな人に「これって、もはやテクニカルアーティストの仕事じゃないよね…。」とも言われたり。褒め言葉と解釈しましたが。

通常バーチャルキャラクターといえば、カメラによる顔認識などで実際の動きをトレースして、キャラに反映するものですが、今回は割り切ってウェブカメラは使わずに、インタラクティブなものはマイクから入力された声の音量による口パクの動きのみ。体の動きはキーボードの操作でポーズやアニメーションを切り替えるだけ、例えば「1」を押すと喜んでいるポーズ、「2」を押すと悲しんでいるポーズになる、というような何ともレトロな仕組みにしました。

FaceRigの代わりになるUniyアプリはできたので、次は2人同時の部分をどう実現するかです。

参考にしたのがこのサイトです。

www.cg-method.com

なるほど。2人目は「Skype」や「Discard」でビデオ通話して、そこからキャプチャしているんですね。しかし今回の場合、すぐそばに2台のノートPCがあるのに、わざわざネットワークを介してビデオ通話というのは、ちょっと大げさすぎますし、無意味に遅延が発生してしまいますよね。

数日、他の仕事しながらぼんやり考えた結果「ネットワークの代わりにHDMIを使うアイデア」にたどり着きました。

  1. 市販のキャプチャボックスを使って2台のノートPCを、HDMIとUSBでつなぐ。
  2. ヘッドセットをノートPCにつなぐ。普通のマイクでも可。
  3. それぞれでUnityアプリを立ち上げて、アイドルが映った状態にする。
  4. 下記図の左側のノートPCから、右側のノートPCに画面を転送する。右側のノートPCにはUnityアプリの画面が2つ映った状態になる。
  5. 右側のノートPCのOBS*11上で、2人のアイドルをクロマキー合成する。
  6. OBSからSHOWROOMさんのサーバーに転送する。

f:id:sgtech:20190725002244j:plain

この方法で、2人同時生配信を実現できました!
ただし、HDMIを使う方法には2つ問題がありました。

問題の1つは、HDMIでの動画と音声の転送の遅延です。これにより相互のマイクで音声を拾った場合に、遅延した音声も合わせて配信されてエコーがかかったような現象が発生しました。

もう1つは、HDMIでPC同士を接続することの不安定さです。なかなか認識しなくて、何度もHDMIを抜き差しして、やっと成功する…といった具合で、本番の配信中に接続が切れないかと心配でした。

結局未解決のまま生配信したのですが、そのときダイジェストがこちらです。

www.youtube.com

なんとかそれっぽくなりましたね。

なお、機材のセットアップや生配信中のOBSの操作、トラブル対応のため本番は毎回立ち会っています。

5人同時生配信への道

この段階では、Oさんには「同時に生配信できる人数は2人までですからっ」とクギを刺していました。

ところが、その後この仕組みを使ってユニットごとに「新曲発表」の生配信をするという企画が立ち上がりました。それも、そのユニットのメンバー全員(2人〜5人)で、です。

新曲発表1組目は「La-Veritta(ラヴェリッタ)」という2人組のユニットだったので、トラブル*12もありましたが、そのままの仕組みで乗り切れました。

2組目の「Just 4U(ジャストフォーユー)」という4人組ユニットのときは、Oさんの提案で、2人ずつ入れ替わりで生配信しました。結果、トラブルもなく大成功でしたが、でも私は負けた気がしたのです。そのときの視聴者の書き込みからもメンバー4人全員がそろっている姿を見たいという気持ちを感じましたし、なにより、できない理由が技術的な事情というところに、敗北感を覚えました。

このままではつまらない。『Readyyy!』で一番人数の多いユニット「SP!CA(スピカ)」は5人組なので、5人同時生配信の仕組みを作ってやる、と心に決めました。

f:id:sgtech:20190725002104j:plain
「SP!CA」のメンバー

単純に5台のPCをHDMIでつなげば、今の仕組みのまま対応できるかもしれません。ただPCが2台のときでさえ多少不安定なのに、5台だなんてとんでもない…。トラブルが発生することは容易に想像できます。

そんなことをぼやいていたら、いつもお世話になっているインフラ屋のAさんの助言がヒントになりました。

Aさん「1台で完結させればいいんじゃない?」

Aさんも深い意味で言ったわけではなかったと思うのですが、理にかなっています。1台…そうか…できるかも…。

PC1台で5人同時生配信を行う仕組みはこうなりました。まず以下の機材を用意します。

  • デスクトップPC1台
  • USB接続のマイク5本
  • USB接続のコントローラー5台(実は4台でよかった)
  • 液晶モニター2台
  • USBハブ2個
  • Live2Dのアイドルを表示する自作Unityアプリ
    • 使用するマイクやコントローラーを切り替える機能
    • マイクからの音量に合わせてアイドルが口パクする機能
    • アイドルを切り替える機能
    • コントローラーでアイドルのポーズを切り替える機能
  • OBS

セットアップの手順はこうです。

  1. PCにマイクとコントローラーを5台ずつ、つなぎます。
  2. そのPCで自作Unityアプリを5つ*13立ち上げます。
  3. Unityアプリ上で、それぞれ、アイドル、マイク、コントローラーの設定をします。
  4. OBSでUnityアプリの画面をすべて取り込んで、クロマキー合成します。
  5. OBSからSHOWROOMさんのサーバーへ転送。

f:id:sgtech:20190725002240j:plain

これだけの機材の数になると、セットアップの手間もかなりのものになったのですが、その苦労の甲斐あってか、5人同時生配信の野望を達成できました!また、PCが1台になったことで遅延の問題も解決して一石二鳥です。

『Readyyy!』のアプリでは、パフォーマンスの都合上、アイドルの同時表示数を3人に制限していたので、ある意味で「生配信はアプリを超えた!」とも言えます。

f:id:sgtech:20190725002209p:plain
生配信中の「SP!CA」のメンバー

ツイッター上でも5人全員そろっていることへの驚きの反応があったので、うれしかったですね。ちなみに明るさが人によって異なっているのは、話している人が誰かをわかりやすくするためです。

では次に、5人同時生配信をする上で遭遇したトラブルを3つ紹介します。

マイクデバイスを選択できない問題

Unity2017.4では、複数マイクデバイスから任意の1つを選択できないバグがあり、どう解決するか悩まされましたが、そのバグが修正されたUnity2018.2を使うことで、あっさり解決しました。直っててよかったです…。

Unity Issue Tracker - Microphone.start is not recording the audio from selected recording device

バックグラウンドのUnityアプリでは、コントローラーが反応しない

Unityアプリを5つ立ち上げるので、すべてのアプリをアクティブにしておくことはできません。そのためUnityアプリがバックグラウンドの状態でも、操作できるようにしておく必要があります。

qiita.com

このサイトを参考にXInputを使って解決したまではよかったのですが、リハーサルで、5台目のコンローラーがまったく反応しないトラブルに遭遇しました。ネットで調べてみると、XInputはコントローラーを4台までしかサポートしていないそうで。そのためUnityアプリを修正して、5人目はキーボードでポーズを切り替えるようにしました。キーボードでの操作は2人同時生配信のときに実装していたので、対応自体はスムーズでしたが、まさかそんな落とし穴があるとは…。でも、プログラマのK君に聞いたら常識のようでした。

しゃべっていないアイドルが口パクをしてしまう

指向性のマイクを用意したのですが、それでもマイク同士が近いため、他のメンバーの声を拾ってしまい、しゃべってないアイドルが口パクをしてしまう問題が発生しました。超指向性のマイクを導入する方法もありましたが、今回はコントローラーの右トリガーを引いたときにのみ口パクをするようにプログラムを修正しました。ちょっとかっこ悪い解決方法ですね。

先ほど紹介した2人同時配信のダイジェスト動画もよく見ると、右側のアイドル(全)の口が時々、左側のアイドル(淳之介)につられて動いているのが分かります。

多人数生配信のまとめ

生配信の仕組み作りについては、世間のVTuberさんにとっては目新しいことはなかったと思いますが(結果的に)遅延のない5人同時生配信の試みはちょっとおもしろかったんじゃないでしょうか?条件は異なりますが「FaceRig」のマルチアバター機能も4人までみたいですし。
生配信自体、まったく新しいチャレンジだったので、プレッシャーがありながらも「SP!CA」5人全員を登場させることができたときは、達成感ありました(疲労感も…)。また、生配信中にトラブルがよく発生するので、臨機応変で、かつ、一か八かの対応も、普段のゲーム開発では味わえないスリリングさがありました。あと、OBSというツールがほんとよく出来ています。

最後に…

『Readyyy!』でのLive2D活用事例、いかがだったでしょうか?今後も「応援し続けてよかった」と思えるくらい楽しんでいただけるよう、アイドルの生配信をはじめとするさまざまな可能性に、『Readyyy!』に関係するスタッフ一丸となって取り組んでいきます。

この記事が、皆様のモチベーションに少しでもプラスの影響を与えることができたのなら、幸いです。



©SEGA

*1:https://www.live2d.com/ja/

*2:UnityでLive2Dを扱うためのプログラム https://live2d.github.io/#unity

*3:ゲームを作るための統合開発環境 https://unity.com/ja

*4:特定の機能を持たせたプログラムのかたまり

*5:https://togetter.com/li/1316881

*6:http://sikulix.com/ GUI上での操作をRubyやPythonで自動化するツール。

*7:ETC2は2のべき乗ではなく4の倍数でOK

*8:まつもとゆきひろ氏が開発した、個人的に大好きなスクリプト言語。テクニカルアーティストにはどちらかというと人気はない。

*9:画像処理ライブラリImageMagickのRubyインターフェス。Rubyで簡単に画像を処理できるようになる。

*10:Mayaをはじめ多くのツールに組み込まれているスクリプト言語。DeepLearningでも良く使われている。テクニカルアーティストに大人気。

*11:正式には「OBS Studio」という。生配信で使われるオープンフリーなソフトウェアのこと。

*12:1人の声が突然ロボットみたいな声になるというトラブルが発生し、めちゃくちゃ焦りましたが、Unityアプリを再起動したら直った。

*13:1アプリで5人表示させる方法もアリかもしれませんが、生配信中にアイドルの数を増減させる演出をすることがあり、別々のアプリにしておいてOBSで人数を管理する方法がやりやすかった。

GDC報告会に見るセガの学習機会と共有の取り組み

 

はじめまして

SEGA Tech Blogをご覧のみなさまこんにちは。セガゲームス開発技術部の小林です。
開発技術部ではスタジオサポート以外にも勉強会の開催やグループ内の情報共有のお手伝いを行っています。
もちろんこのBlogのお手伝いもしています。

SEGA Tech Blogでは毎回スタジオの開発者が素晴らしい技術ネタを書いていますが、今回は5月15日に開催したGDC2019*1社内報告会の事例を踏まえまして、弊社における学習機会の外郭のお話をしたいと思います。

 

GDCとは

GDC(Game Developers Conference)とは毎年2月下旬から3月上旬に行われる世界各国のゲーム開発者を中心としたカンファレンスです。2015年まではGDC China、2016年まではGDC Europeなども別の日程でありましたが、2017年からはこの時期に行われるGDCのみとなっています。

2019年は3月18日から22日にかけてサンフランシスコのモスコーニセンターにおいて20のTopics、約800近いセッションが行われました*2。セッション数上位のTopicsはProgramming、Design、Business & Marketing、Visual Artsあたりとなります。Topicsの種類は業界のトレンドによって追加されます*3。この時期なると日本の業界関係者のSNSにおいて、渡航組と残留組の食べ物写真の応酬が行われるのも風物詩となっています。

また、GDC2019では"Game Developers Choice Awards"において弊社小玉理恵子さんが業界の発展に貢献したゲーム開発者に贈られる“PIONEER AWARD”を受賞しました。

 

毎年派遣しています

弊社では毎年GDCに社員を派遣しています。ここ数年の実績ではセガゲームス、セガ・インタラクティブあわせて約20人ほどの社員が参加しています。セッション数上位にあるようにプログラマー、デザイナーの参加者が多い傾向にありますが、プランナーやサウンドの参加者が皆無な訳ではありません。

 

GDC参加実績
年次 総数 管理職 プログラマー デザイナー プランナー サウンド その他
GDC2016 18 0 10 5 0 1 2
GDC2017 14 0 6 4 2 1 1
GDC2018 20 1 9 8 2 0 0
GDC2019 23 1 12 9 1 0 0

 

人選は各部門に委ねられています。なるべく多くの情報を持ち帰り展開してくれる人を選ぶ部署もありますし、トレンド技術を消化してもらう為に、専門的な知識を持った人を選ぶ部署もあります。また、刺激を受けてきてもらう為に若手を選ぶ部署もあるようです。参加経験の浅い人が不安を解消してモチベーションが高い状態で参加できるように、当部の粉川が相談用チャットや勉強会を開催して、事前準備するべきことや、心構え、開催されるセミナー情報などのフォローもしています。

 

 GDC社内報告会

GDC参加者から有志によりレポートとは別に報告会を開催して情報を共有をしています。それが今回開催したGDC2019社内報告会です。普段の勉強会は社内事例を取り扱う事が多いので11階にあるLIGTH HOUSEというセミナールームを利用する事が多いのですが、今回は外部のセミナー報告会ということで9階のTunnel Tokyoのオープンラウンジで開催しました。9階には総合受付や多くの会議室があり、大型LEDサイネージにプレゼン資料を表示するのでミーティングで近くを通る方や来訪者の方もこちらを興味深くみていました。 

f:id:sgtech:20190623220920j:plain

Tunnel Tokyoのオープンラウンジ

報告会は以下の内容で行われました。

・せっかく海外行けるのに英語?新人?そんなの気にしない!!
・AAAタイトルのデータとツール(主にスパイダーマン事例紹介
・自動テスト関連
・サウンドの実装事例とミドルウェアの紹介
・Deep Learning 活用事例にみるゲーム開発・運用への活用のありかた
・ツール関連、テクニカルアーティスト関連、プロシージャル関連
・God Of Warとスパイダーマンのアニメーションセッション話題。 

 

今回の報告会では新人から、参加16回目の常連まで幅位広いキャリアの人が登壇しました。異なるキャリア、異なる職種の人が、会社の実情を踏まえた上で情報共有してくれる会は、社外で行われている報告会とはまた別の意義があると感じています。

 

ある若手に着目して

f:id:sgtech:20190623220925p:plain

f:id:sgtech:20190623220911p:plain

リハーサルと本番の様子

写真は「ツール関連、テクニカルアーティスト関連、プロシージャル関連」で登壇した4年目のTA・清水です。GDCへの参加は今回初めてだったそうです。彼は若手ですが、社内の勉強会やCEDECなどで積極的に発表活動を行なっていて、その成果が認められてGDCへ参加する機会を得ました。清水曰く、GDCでは、発表の内容はもちろんのこと、発表の仕方にも感銘を受けたそうです。聴いて欲しい内容を浸透させるために如何に聴衆の心を掴むかという方法を生で体感できたのは、GDCに行って最も価値がある体験の一つだったと語ってくれました。また、上司がセミナー受講や登壇することに対する理解があることも自分が積極的に活動できる理由であるとも語ってくれました。

 

良いゲームをつくるために

ゲーム開発を行っている以上、最も重要なアウトプットはゲームそのものです。より良いゲームを作るためには思考を繰り返す必要があり、その思考を回す為には情報のインプットが必要です。一度自分がゲームというアウトプットを行なった後は更なる情報のインプットはもちろんの事、自分が行った事をまとめてアウトプットする事も重要だと考えています。それは周りに情報をインプットする機会を与えているだけにとどまらず、伝えた人々からフィードバックを受けることが、自身の新たなインプットにつながるからです。グループ全体を見回すと面白い知見を持っている者がたくさんおります。こうした情報を知る機会、また、広める機会をこれからもお手伝いできればと思っています。

 

最後に

弊社では多くのインプット、アウトプットの機会と設備を備えています。また、海外カンファレンスに参加するチャンスもある弊社に興味を持った方は是非下のリンクをクリックしてください。ゲームを世に出す事はもちろん、自分の学んだ事をみんなに伝えて自他共栄を行える人を求めています。

www.sega.co.jp


 

弊社の開発関連人員の知見を広げる勉強会を開催してくださる方や講師を引き受けてくださる方もあわせて募集しています。

 

©SEGA

 

*1:先月の記事のGCC2019と似てますね

*2:オフィシャルページに記載されているTopicsは以下。Advocacy、Audio、Business & Marketing、Career Development、Design、Game Career Seminar、Production & Team Management、Programming、Special Event、Vision、Visual Arts、AI Summit、Community Management Summit、Educators Summit、Entertainment VR/AR、Mobile Summit、Game Narrative Summit、Game VR/AR、Independent Games Summit、UX Summit

*3:数年前はVRのセッションが爆発的に増加し、VRDCという単独のイベントも行われました

徹底解説!セガゲームスのテクニカルアーティスト

こんにちは。

皆様いかがお過ごしでしょうか?

セガゲームス 第3事業部のテクニカルアーティスト。麓です。

 

普段はこのブログの裏方をしているのですが、今回は今年の3/30に大阪で開催された、ゲームクリエイターズカンファレンス2019(以後GCC2019)*1にて、株式会社カプコンの塩尻様とパネルディスカッション形式で講演致しまして、時間内で全て語りきれなかったということもあり、セッションのフォロー記事という形で筆を取らせていただきました。

セッションのおさらいも兼ねていますので参加できなかった方にもお伝えできるように、講演時に話した内容からも引用しつつまとめていきます。

尚、このブログではセガゲームスの事例のみを解説させていただきますので、ご了承ください。

タイトル「怖く無い、テクニカルアーティストという仕事」

f:id:sgtech:20190421145618p:plain

「テクニカルアーティスト(以下TAと略します)って大手企業のみしか関係の無い職種だろうから、ウチはあんまり関係ないなあ。その様な人材は、技術力が高くてデキる人なんだから、ウチにはいないなあ。中途で採用や教育もしたいけど、そんなヒマも余裕もウチにはないなあ。」
関西圏*2でCGやゲームを生業としている企業は、データの状態や仕様がはっきりとしない状態で制作に入らなければいけない事が多く、各アーティストは効率が悪くとも、根性で頑張って納期に間に合わせているといった実情をよく耳にします。
根性が必要になるときは必ずありますが、出来るだけラクできる所はラクした方が、空いた時間を工数削減やクオリティUPあてられるので、その方がいいですよね?

f:id:sgtech:20190421145615p:plain
今回の講演は、上のような状況に悩まされるゲーム会社の方へ向けて構成しました。

TAについて簡単に・・・

CG World Entryの記事から引用すると、

f:id:sgtech:20190421151853p:plain

という事で一般的な認識としては上記元記事「CGWorld Entry.jp」を参照していただくとして、様々な会社の事情を元にTAに必要な知識をリストアップすると・・・

  • C#やPythonなど業務に必要な各言語でのプログラム技術、及びShader関連知識
  • 絵が描ける、パースが取れる、色彩の説明が出来るなどアートスキル 
  • ゲームエンジンのフロー知識と、DCCからの出力フロー知識(描画フロー把握、用語知識など)
  • ゲームハードに関しての基本知識(ハードスペック、描画性能など)
  • VMやNASなどのストレージやライセンスサーバ関連、SubversionやPerforceなどサーバ知識技能。
  • Windows、MacOS、Linux、iOS、Androidなど色々なOSへの造詣
  • Mayaや3DS MAX、Houdiniと言ったメジャーな3DDCCツールの知識
  • PhotoShopやSubstance、ZBrushや3D-coatといった様々なツール知識
  • 過去のツールやゲームハードに関しての様々な知識
  • Havok,Simplygon,SpeedTreeやumbraなど様々なミドルウェア製品知識
  • MotionCapture関連での作業フローの理解と、VICONや各社ツールの知識
  • 画角やEV値やルーメン、カンデラといった単語が理解出来るくらいの照明技術やカメラ映像知識
  • SiggraphやGDC、CEDECなどでの様々な新規技能を能動的に取得できる知識欲
  • 交渉力、説得力、判断力などのコミュニケーションスキル
  • 技術的な仕組みを判りやすくまとめ、説明できるプレゼン力

等々・・・

f:id:sgtech:20190421145613p:plain

だと思います!
なのでどれか1つのスキルを持っているだけでもいいんです。
極論これから始めてもいいんです。効率化を考える事からぜひ始めましょう!
現状の作業の中で、“これ面倒だな”とか“単調な作業をなんとかしたいな”と思える人は向いていますので、そこからDCCツールを深いところまで覚えてみたり、Scriptを覚えたり、Shaderを書いてみたりして、第1歩を踏み出しましょう!

1日にどんな仕事をしているの?

普段何をやっているのか、どんな風にタスクをこなしているのか見えずらいとよく言われるTAですが一つ、とある1日を抜き出して見ました。

f:id:sgtech:20190421152743p:plain

f:id:sgtech:20190421152759p:plain

この部分に関しては以前各ゲーム会社のTAで対談させていただいた以下の記事内でも深く触れています。

cgworld.jp

ざっと斜め読みしただけでも1日にいろんな事が起き、いろんなタスクをこなしています。
専門知識を持って各所のミーティングに参加し、情報共有ができる身軽さ。これが無いとTAは回っていかないという事がなんとなく読み取れますでしょうか?

また、本当に細かいことでも1時間単位でもどんな問い合わせ、作業があり、どういう事を行ったかというログを記録しておくことはとても重要です。多くのタスクを同時にこなしていると、普段何をやっているか分からないと言われることがありますし、自分自身もよく分からなくなることも多々あります。

具体的にどんな効率化が出来たの?

普段の仕事は個人単位、チーム単位様々ですが、ここでは個人単位の割と簡単で良くある実例をあげて解説します。
対応時期が古いものもあるので現在ではまた違った手法もあるかもしれませんが・・・。

Photoshop効率化

<相談内容>Photoshopで多くのアイコンなどのパターン画像を作成して、一つ一つのアイコンをレイヤーセットで管理しています。そのデザイナーはレイヤーセットを必要なものだけ表示させ、別名でPNGに保存を手作業でレイヤーセット分だけやろうとしていました。その数60枚ほど。修正の度に毎回その作業をするのは地獄です。

<対応>メニューから選ぶだけで所定のフォルダにレイヤーセット毎の画像を保存する、スクリプトを作成して提供しました。30分〜1時間を予定していた作業が1分弱で終わるようになりました。

Photoshopだけに関わらず、3D用のツール以外の効率化作業は細かい単位で割とよく発生します。こういう処理をその場その場でサクッと作ってしまうといったような対応をTAには求められます。

f:id:sgtech:20190429160108p:plain

座標系を超えろ!?3Dツール間コンバート

<相談内容>過去に3dsMaxでキャラクターデータ、XSIでモーションデータを作っていてその二つのデータをMayaで融合させ、ゲームエンジンへ持って行きたいが、アニメーションが合わない。

<対応>キャラとアニメーションが合わない原因は3dsMaxとXSIの座標系と大きさの単位の違いの問題が絡み合っていました。キャラのコンバートは3体のみだったので一つ一つ対応。アニメーション数がそれなりにあるということで、"SEGA BatchFramework"*3の仕組みでワークフローを自動化。手作業でコンバートするつもりだったアニメーションデータをボタン一つで対応可能に。1体1分程度の手作業を10秒位に短縮。また、検証作業自体もある程度短縮。

f:id:sgtech:20190429162908p:plain

上記事例の座標の違い

横断サポートの強み

<相談内容>プロジェクトAからもらったデータを流用しようとしているプロジェクトB、この2プロジェクト間では使っているゲームエンジンが違いデータの整合性が取れずお互いがお互いのエンジンの事情も知らない。

<対応>プロジェクトAのサポートをしたことがあるTAがどうやってデータを変換すれば欲しいフォーマットになるのかを検証し、自動化フローまで製作。検証時間の削減と変換時間の短縮。そもそも変換が出来なかったら目コピで一から作っていたかもしれない・・・。

TAはセガゲームスの場合複数のプロジェクトやパートに対してワークフロー、パイプラインの構築を手伝ったりします。
その性質上、各プロジェクトやパートのデータ事情に詳しくなり、自然にプロジェクト間の架け橋を担うようになります。

f:id:sgtech:20190502204737p:plain

どんなツールを作成しているの?

ワークフロー構築ツール

効率化をするための個々のツールはたくさん作成していますが、処理内容を詳細に分析していくと、複数のプロジェクトでも共通のオペレーションが存在しています。

例えば命名規則だったり、何かをエクスポートする処理だったりといった、フローのプリセット化、オペレーションの共有化を目指して用意しているものの一つが上記に出てきた「SEGA BatchFramework」です。

●講演資料からスナップショット

f:id:sgtech:20190505172145p:plain

f:id:sgtech:20190505172248p:plain

f:id:sgtech:20190505172312p:plain

CEDEC2015当時の講演資料を見ていただけますとさらに詳しく知ることができると思いますが、このツールの大きな効果は以下の2点です。

  • 処理内容を再利用することにより、ワークフローの自動化を構成する時間を短縮できた。

  • 自動化を自分で考えられるようになり、さらに素養がある人の場合は自分でアクションの一つを作るように登録用のスクリプトをVBSやpython、mel、maxScriptなどで作成できるようになった。

データデバッグビューア

過去のBlog記事からの引用となりますが、パイプラインを走っているデータがどういう状態になっているのかを視覚化するためのツールは必要不可欠です。

ここまでにいくつか出てきた要件の「データの検証作業」に大きく貢献しています。

こういったツールがないと、実際にゲームエンジンで描画されたデータを見て、「もしかしたらこれが間違っているのかもしれない・・・」と推測だけでデータのデバッグをする羽目になります。

techblog.sega.jp

 

これ以外にもこのSEGA Techblogの過去の記事では、セガグループのTAがそれぞれ記事を書いていて、そのツールも紹介しています。

是非、過去記事も参考にしてみてください。

記事一覧 - SEGA TECH Blog

TAのプロジェクトへの関わり方は?

この図に関しては時代や会社事情により様々ですが、セガゲームスの場合はまず各プロジェクトにTAがそれなりにアサインされていて、その人とのコミュニケーションを密にとる横断型TAチームがあります。

基本的にTAはノウハウの共有も担うものですので、プロジェクト担当のTAはプロジェクト内のセクション間に、そして全体横断型のTAチームはプロジェクト間のノウハウの共有に一役買っています。

f:id:sgtech:20190421171115p:plain

f:id:sgtech:20190421171133p:plain

また、TA同士の密な情報共有により、問題解決への対応速度はどんどん上がっていきます。

たとえ相談された人が知らなくても、知っていそうなTAに聞けばいいのですから。

若手、新人TAについて

採用は右のバナーのリクルートページを参照いただければお分かりだと思いますが、(新卒採用情報→職種紹介)数年前から1年目からTA職を担える人を採用しています。

過去にCEDEC等ではTAはある程度現場の経験を積まないとなれないとされてきました。以前はTAの数は少なく、一人で多くの問題に手早く対応しなければいけませんでした。また、迅速な対応を求められるため、ある程度データ制作をしたことがないと本当に困っていることや、もっと全体を見通しての効率化にたどり着くことができなかったのです。

ですが今では会社全体を見てもTAは足りないと言われつつもそれなりに増え、役割分担も明確になりつつあります。

そういった状況ですので、先人のTAから多くのことを学ぶことができますので、経験不足の壁を埋める速度は懸念事項からは除外されてきつつあります。

ゲーム制作未経験なTAはまず、出来るだけデザインワークフローに関わる仕事をすることが多いです。

例えば上記データデバッグビューアの項のように、中間ファイルフォーマットのビューアを担当することで、デザイナーはどういうデータを出力しているのかを知ることができます。

そして実際にデータを作成しているデザイナーとも出来るだけ接点を作り、要件定義→仕様検討→実装までを一人で対応する機会を設けます。

もちろん周囲は全力でこれを助けます。

こうして現在も暗中模索の中、TAを育てる取り組みを繰り返しています。

この過程ではシニアTAの存在も大きく影響します。もし新人からTAをやらせてみたい場合は、まずはシニアTAの地位確立、立ち回り確保も重要になります。 

そうしてTAはアートアセットのパスを主軸にしながらも、必要に応じて各所のパイプをつなぐ役割を担うものです。

ゲーム全体や開発全体のパイプ役として日々活躍しています。

f:id:sgtech:20190505175520p:plain

まとめ

TAがいる現場といない現場ではデザインワークフローはかなりの差が出ます。

アーティストの作業面のフラストレーションの改善、工数の削減、クリエイティブな作業への注力、そしてノウハウの共有。

セクションを超えた円滑なパイプライン構築など。

最初の文章では(GCC2019ということもあり)関西圏へ向けてのメッセージとして発信していますが、これは日本のゲーム業界全てに当てはまることだと思います。

是非とも"御社"でもTAの採用、育成を推進してみませんか?

また、セガゲームスにはこのようにTAが活躍しやすい土壌があります。

ゲームを作ることも好きだけど、より作るための効率改善に興味があり、自分の強みを活かせると感じているみなさん。

セガゲームスで一緒に働きませんか?

 

採用情報 | セガ企業情報サイト

www.sega.co.jp

 

©️SEGA

*1:ゲームクリエイターズカンファレンスhttp://www.gc-conf.com/

*2:GCC2019は関西で開催したカンファレンスということで関西圏向けということになっています。

*3:Maya,SoftimaeXSI,3dsMax用に作られた自動化フロー作成ツール※詳細は次を参照

cedil.cesa.or.jp

Unityで、PBRなライティング環境をセットアップしてみよう


こんにちは。セガ・インタラクティブ技術統括室の大森です。
TAとして、アーケードタイトルにUnityを採用する際の描画設計を お手伝いしています。


Unity5以降、Unityの標準シェーダーにはPBRワークフローが採用されています。アート素材の量産性が高いことから すっかり普及したPBRワークフローですが、その恩恵を受ける為にはフォトリアル系の知識が欠かせません。

ところで、Unityプロジェクトの初期設定は、旧来シェーダーでの絵作りとも互換性を保つように設定されていて、そのままPBRワークフローを始めると混乱を招く部分があります。社内では この混乱を避ける為、プロジェクト立ち上げ時に、カラースペースやライティング単位を設定しておく事を勧めています。


この記事では、ライティング環境のセットアップに使えるシェーダーやスクリプトを共有しつつ、具体的なライティング設定の一例を紹介します。ぜひハンズオンで、自作のモデルやステージデータを使って、試しながら読んで頂けたらと思います。


f:id:katsuhiko_omori:20190402225501j:plain
このスクリーンショットで、ターゲットとした屋外ライティング環境は以下のようになります。

太陽に直交した面に 入ってくる光の量(照度) 太陽から 85,000 青空(半球)から 20,000 合計    105,000 [lux]
太陽の傾斜角が66.6度。水平地面に 入ってくる光の量(照度) 太陽から 78,000 青空(半球)から 20,000 合計    98,000 [lux]
この環境に照らされたグレー18.42%地面の明るさ(輝度) 青空から=日陰   1,173 合計=日向    5,747 [nt]
青空の明るさ(輝度) 平均     6,366 [nt]
白い雲    10,000 [nt]
水平線    8,000 [nt]
青い空    4,000 [nt]
天頂部    1,500 [nt]
カメラの露出補正(EV100準拠の絶対補正値) 15 [EV]

…これが常に正しい値!という訳ではなく、現実にありうる値の一例として。
この環境をセットアップしていきます。



Unityプロジェクトを作成する

今回のハンズオンは、Unity2018.3以降に対応しています。*1
標準の フォワードレンダリング+HDRカメラ設定を、そのまま使用して進めます。
ポスプロにはPost Processing V2を、シェーダー内部のライブラリとしてCore RP Libraryも利用します。

まずは、新しいプロジェクトを作り、必要なパッケージとスクリプトをインストールしましょう。

必要なパッケージをインストールする

  1. Unityを起動し、プロジェクトを新規作成。Templateは3Dにする
    f:id:sgtech:20190422122620g:plain
  2. PostProcessingとRender-Pipelines.Coreのパッケージをインストールする
    • パッケージマネージャ ウィンドウを開く Window > Package Manager
      f:id:sgtech:20190422122817g:plain
    • Advanced > Show Preview Packages をオンにして プレビューパッケージを表示する
      f:id:sgtech:20190422122815g:plain
    • PostProcessingをインストールする
      f:id:sgtech:20190422122813g:plain
    • Render-Pipelines.Coreをインストールする
      f:id:sgtech:20190422122811g:plain


SEGA TECH Blogからスクリプトをインポートする

…すみません、このBlog、スクリプトファイルを直接添付することが 出来ません。
お手数おかけしますが、以下3つのスクリプトを テキストファイルに コピペ, 保存し、
Unityプロジェクトにインポートしてご利用ください。

  1. それぞれの、▼ファイル名 をクリックして、コード内容を表示
  2. 表示されたコードをダブルクリックすると全文選択されるので、右クリック>コピーする
  3. メモ帳に コードをペーストし、それぞれ指定のファイル名で、Unityプロジェクトの Assetsフォルダ内へ保存する

litColorSpace.cs

using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Rendering;

namespace SegaTechBlog
{
    public class litColorSpace : MonoBehaviour
    {
#if UNITY_EDITOR
        [MenuItem("SegaTechBlog/LightsIntensity/Linear")]
        private static void luliTrue()
        {
            GraphicsSettings.lightsUseLinearIntensity = true;
            EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
            var scn = EditorSceneManager.GetSceneManagerSetup();
            EditorSceneManager.OpenScene(scn[0].path);
        }

        [MenuItem("SegaTechBlog/LightsIntensity/Gamma")]
        private static void luliFalse()
        {
            GraphicsSettings.lightsUseLinearIntensity = false;
            EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
            var scn = EditorSceneManager.GetSceneManagerSetup();
            EditorSceneManager.OpenScene(scn[0].path);
        }
#endif
    }
}

litChkLib.hlsl

#ifndef SEGATB_CHS_INCLUDED
#define SEGATB_CHS_INCLUDED
// ------------------------------------------------------------------------------------
// SEGATB _ COMMON FOR ALL PASS
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
CBUFFER_START(UnityPerCamera)
	float4 _Time;	float3 _WorldSpaceCameraPos;	float4 _ProjectionParams;	float4 _ScreenParams;	float4 _ZBufferParams;	float4 unity_OrthoParams;
CBUFFER_END
CBUFFER_START(UnityPerCameraRare)
	float4x4 unity_CameraToWorld;
CBUFFER_END
CBUFFER_START(UnityLighting)
	float4 _WorldSpaceLightPos0;
	float4 unity_4LightPosX0;	float4 unity_4LightPosY0;	float4 unity_4LightPosZ0;	half4 unity_4LightAtten0;	half4 unity_LightColor[8];
	half4 unity_DynamicLightmap_HDR;
CBUFFER_END
CBUFFER_START(UnityShadows)
	float4 unity_LightShadowBias;
CBUFFER_END
CBUFFER_START(UnityPerDraw)
	float4x4 unity_ObjectToWorld;	float4x4 unity_WorldToObject;	float4 unity_LODFade;	float4 unity_WorldTransformParams;
	real4 unity_SpecCube0_HDR;
	float4 unity_LightmapST;	float4 unity_DynamicLightmapST;
	real4 unity_SHAr;	real4 unity_SHAg;	real4 unity_SHAb;	real4 unity_SHBr;	real4 unity_SHBg;	real4 unity_SHBb;	real4 unity_SHC;
CBUFFER_END
CBUFFER_START(UnityPerFrame)
	float4x4 glstate_matrix_projection;	float4x4 unity_MatrixV;	float4x4 unity_MatrixInvV;	float4x4 unity_MatrixVP;
CBUFFER_END
CBUFFER_START(UnityReflectionProbes)
	float4 unity_SpecCube0_BoxMax;	float4 unity_SpecCube0_BoxMin;	float4 unity_SpecCube0_ProbePosition;
CBUFFER_END
#define UNITY_MATRIX_M     unity_ObjectToWorld
#define UNITY_MATRIX_I_M   unity_WorldToObject
#define UNITY_MATRIX_V     unity_MatrixV
#define UNITY_MATRIX_I_V   unity_MatrixInvV
#define UNITY_MATRIX_P     OptimizeProjectionMatrix(glstate_matrix_projection)
#define UNITY_MATRIX_VP    unity_MatrixVP
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
 
float4x4 OptimizeProjectionMatrix(float4x4 M)
{
    M._21_41 = 0;
    M._12_42 = 0;
    return M;
}

float3 CheckColorValue(float3 color, float targetValue, float targetScale, float range)
{
    targetValue *= targetScale;
    float lum = dot(color, float3(0.2126729, 0.7151522, 0.072175));
    float3 outColor;
    outColor.g = saturate(max(range - abs(lum - targetValue), 0.0) * 10000) * 1.2; // just in range
    outColor.r = saturate(max(lum - targetValue + range, 0.0) * 10000) - outColor.g * 0.5; // over    range
    outColor.b = saturate(max(targetValue - lum + range, 0.0) * 10000) - outColor.g * 0.5; // under   range

    float rhythm = sin(lum / targetScale * 10.0 + _Time.w) * 0.35;
    outColor.g += 0.123;
    return outColor * (0.65 + rhythm);
}

// ------------------------------------------------------------------------------------
//
#ifdef SEGATB_FORWARD

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonLighting.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/BSDF.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/EntityLighting.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/ImageBasedLighting.hlsl"

float _ChkTargetValue, _ChkTargetScale, _ChkRange;
half4 _LightColor0;

UNITY_INSTANCING_BUFFER_START(PerInstance)
	UNITY_DEFINE_INSTANCED_PROP(float4, _AlbedoColor)
	UNITY_DEFINE_INSTANCED_PROP(float, _Metallic)
	UNITY_DEFINE_INSTANCED_PROP(float, _Anisotropy)
	UNITY_DEFINE_INSTANCED_PROP(float, _Smoothness)
	UNITY_DEFINE_INSTANCED_PROP(float, _EmitIntensity)
UNITY_INSTANCING_BUFFER_END(PerInstance)

TEXTURE2D_SHADOW(_ShadowMapTexture);	SAMPLER(sampler_ShadowMapTexture);
TEXTURECUBE(unity_SpecCube0);			SAMPLER(samplerunity_SpecCube0);
TEXTURE2D(unity_Lightmap);				SAMPLER(samplerunity_Lightmap);
TEXTURE2D(unity_LightmapInd);
TEXTURE2D(unity_DynamicLightmap);		SAMPLER(samplerunity_DynamicLightmap);
TEXTURE2D(unity_DynamicDirectionality);
TEXTURE2D(_MainTex);					SAMPLER(sampler_MainTex);
TEXTURE2D(_MetallicGlossMap);			SAMPLER(sampler_MetallicGlossMap);
TEXTURE2D(_NormalMap);					SAMPLER(sampler_NormalMap);

// ------------------------------------------------------------------
struct VertexInput
{
    float4 posOS	 : POSITION;
    float3 normalOS  : NORMAL;
    float4 tangentOS : TANGENT;
    float4 uv0		 : TEXCOORD0;
    float2 uvLM		 : TEXCOORD1;
    float2 uvDLM	 : TEXCOORD2;
    float4 vColor	 : COLOR;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct VertexOutput
{
    float4 posCS					 : SV_POSITION;
    float4 uv						 : TEXCOORD0;
    float4 tangentToWorldAndPosWS[3] : TEXCOORD1;
    float3 viewDirWS				 : TEXCOORD4;
    float4 posNDC					 : TEXCOORD5;
    float4 ambientOrLightmapUV		 : TEXCOORD6;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct GeometrySTB
{
    float3 posWS;
    float3 verNormalWS;
    float3 normalWS;
    float3 tangentWS;
    float3 binormalWS;
};
struct CameraSTB
{
    float3 posWS;
    float3 dirWS;
    float  distanceWS;
    float2 pixelPosSCS;
};
struct LightSTB
{
    float3 dirWS;
    float3 color;
    float  atten;
};
struct SubLightsGeometrySTB
{
    float3 lightVectorWS[4];
    float  distanceSqr[4];
    float  lightAtten[4];
};
struct MaterialSTB
{
    float3 albedoColor;
    float3 reflectColor;
    float  grazingTerm;
    float  alpha;
    float  perceptualRoughness;
    float2 anisoRoughness;
    float  surfaceReduction;
    float  microOcclusion;
    float3 emitColor;
    float3 testValue;
    float  reflectOneForTest;
};
struct LitParamPerViewSTB
{
    float  specOcclusion;
    float  NdotV;
    float  envRefl_fv;
    float3 reflViewWS;
    float  partLambdaV;
};
struct LitParamPerLightSTB
{
    float3 specularColor;
    float3 diffuseColor;
    float3 testValue;
};
struct LitParamPerEnvironmentSTB
{
    float3 reflectColor;
    float3 diffuseColor;
    float3 testValue;
};

float4 GetPosNDC(float4 posCS)
{
	float4 posNDC;
	float4 ndc = posCS * 0.5f;
	posNDC.xy  = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w;
	posNDC.zw  = posCS.zw;
	return posNDC;
}

float F_Pow5(float u)
{
	float x = 1.0 - u;
	float x2 = x * x;
	float x5 = x * x2 * x2;
	return x5;
}

float3 BoxProjectedCubemapDirection(float3 reflViewWS, float3 posWS, float4 cubemapCenter, float4 boxMin, float4 boxMax)
{
    UNITY_BRANCH if (cubemapCenter.w > 0.0)
    {
        float3 nrdir = normalize(reflViewWS);
		float3 rbmax    = (boxMax.xyz - posWS) / nrdir;
		float3 rbmin    = (boxMin.xyz - posWS) / nrdir;
		float3 rbminmax = (nrdir > 0.0f) ? rbmax : rbmin;
        float  fa       = min(min(rbminmax.x, rbminmax.y), rbminmax.z);
        posWS     -= cubemapCenter.xyz;
        reflViewWS = posWS + nrdir * fa;
    }
    return reflViewWS;
}

// ------------------------------------------------------------------
GeometrySTB GetGeometry(VertexOutput input, float2 uv)
{
    GeometrySTB output;
    output.posWS = float3(input.tangentToWorldAndPosWS[0].w, input.tangentToWorldAndPosWS[1].w, input.tangentToWorldAndPosWS[2].w);	
    float3 verTangentWS  = input.tangentToWorldAndPosWS[0].xyz;
    float3 verBinormalWS = input.tangentToWorldAndPosWS[1].xyz;
    output.verNormalWS   = normalize(input.tangentToWorldAndPosWS[2].xyz);

#ifdef _NORMALMAP
    half4  normalMap  = SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, uv);
    float3 normalMapTS;
    normalMapTS.xy    = normalMap.wy *2.0 - 1.0;
    normalMapTS.z     = sqrt(1.0 - saturate(dot(normalMapTS.xy, normalMapTS.xy)));
    output.normalWS   = normalize(verTangentWS * normalMapTS.x + verBinormalWS * normalMapTS.y + output.verNormalWS * normalMapTS.z);
    output.tangentWS  = normalize(verTangentWS - dot(verTangentWS, output.normalWS) * output.normalWS);
    float3 newBB      = cross(output.normalWS, output.tangentWS);
    output.binormalWS = newBB * FastSign(dot(newBB, verBinormalWS));
#else
    output.normalWS   = output.verNormalWS;
    output.tangentWS  = normalize(verTangentWS);
    output.binormalWS = normalize(verBinormalWS);
#endif
    return output;
}

CameraSTB GetCamera(VertexOutput input, GeometrySTB geo)
{
    CameraSTB output;
    output.posWS       = _WorldSpaceCameraPos;
    output.dirWS       = normalize(input.viewDirWS);
    output.distanceWS  = LinearEyeDepth(geo.posWS, UNITY_MATRIX_V);
    output.pixelPosSCS = input.posNDC.xy / input.posNDC.w;
    return output;
}

LightSTB GetMainLight(CameraSTB cam)
{
    LightSTB output;
#if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE)
 #if defined(_NOPIDIV) && !defined(_VSUN_LIGHT_COLOR) && !defined(_VPOINT_LIGHT_COLOR)
    output.color = _LightColor0.rgb *PI;
 #else
    output.color = _LightColor0.rgb;
 #endif
    half atten = 1.0;
 #if defined(SHADOWS_SCREEN)
	atten = SAMPLE_TEXTURE2D(_ShadowMapTexture, sampler_ShadowMapTexture, cam.pixelPosSCS).x;
 #endif
	output.atten = atten;
    output.dirWS = _WorldSpaceLightPos0.xyz;
#else
	output.color = 0;
	output.atten = 0;
	output.dirWS = float3(0,0,1);
#endif
	return output;
}

SubLightsGeometrySTB GetSubLightsGeometry(GeometrySTB geo)
{
    SubLightsGeometrySTB output;
    float4 toLightX = unity_4LightPosX0 - geo.posWS.x;
    float4 toLightY = unity_4LightPosY0 - geo.posWS.y;
    float4 toLightZ = unity_4LightPosZ0 - geo.posWS.z;
    float4 distanceSqr = 0.0;
    distanceSqr += toLightX * toLightX;
    distanceSqr += toLightY * toLightY;
    distanceSqr += toLightZ * toLightZ;
    output.lightVectorWS[0] = float3(toLightX.x, toLightY.x, toLightZ.x);
    output.lightVectorWS[1] = float3(toLightX.y, toLightY.y, toLightZ.y);
    output.lightVectorWS[2] = float3(toLightX.z, toLightY.z, toLightZ.z);
    output.lightVectorWS[3] = float3(toLightX.w, toLightY.w, toLightZ.w);
    output.distanceSqr[0] = distanceSqr.x;
    output.distanceSqr[1] = distanceSqr.y;
    output.distanceSqr[2] = distanceSqr.z;
    output.distanceSqr[3] = distanceSqr.w;
    output.lightAtten[0] = unity_4LightAtten0.x;
    output.lightAtten[1] = unity_4LightAtten0.y;
    output.lightAtten[2] = unity_4LightAtten0.z;
    output.lightAtten[3] = unity_4LightAtten0.w;
    return output;
}

LightSTB GetSubLight(uint index, SubLightsGeometrySTB subLightsGeo)
{
    LightSTB output;
#if defined(_NOPIDIV) && !defined(_VSUN_LIGHT_COLOR) && !defined(_VPOINT_LIGHT_COLOR)
    output.color = unity_LightColor[index].xyz * PI;
#else
    output.color = unity_LightColor[index].xyz;
#endif

    UNITY_BRANCH if ((output.color.r + output.color.g + output.color.b) != 0.0)
    {
        float distanceSqr = max(subLightsGeo.distanceSqr[index], (PUNCTUAL_LIGHT_THRESHOLD * PUNCTUAL_LIGHT_THRESHOLD));
#if defined(_NOPIDIV)
		output.atten = 1.0 / (1.0 + distanceSqr * subLightsGeo.lightAtten[index]);
#else
        float invDistanceSqr   = 1.0 / distanceSqr;
        float lightAttenFactor = distanceSqr * subLightsGeo.lightAtten[index] * 0.04;
		lightAttenFactor      *= lightAttenFactor;
		lightAttenFactor       = saturate(1.0 - lightAttenFactor);
		lightAttenFactor      *= lightAttenFactor;
        output.atten = max(invDistanceSqr * lightAttenFactor, 0.0);
#endif
        output.dirWS = SafeNormalize(subLightsGeo.lightVectorWS[index]);
    }
    else
    {
        output.atten = 0.0;
        output.dirWS = float3(0,0,1);
    }
    return output;
}

MaterialSTB GetMaterial(float2 uv)
{
    MaterialSTB output;
    half4 colParams = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
    half4 matParams = SAMPLE_TEXTURE2D(_MetallicGlossMap, sampler_MetallicGlossMap, uv);
    float4 matColor   = UNITY_ACCESS_INSTANCED_PROP(PerInstance, _AlbedoColor);
    float  metallic   = UNITY_ACCESS_INSTANCED_PROP(PerInstance, _Metallic);
    float  anisotropy = UNITY_ACCESS_INSTANCED_PROP(PerInstance, _Anisotropy);
    float  smoothness = UNITY_ACCESS_INSTANCED_PROP(PerInstance, _Smoothness);
    float  emmision   = UNITY_ACCESS_INSTANCED_PROP(PerInstance, _EmitIntensity);
    float  occlusion  = 1.0;
#ifdef _COLMAP
    matColor   *= colParams;
#endif
#ifdef _METMAP
    metallic   *= matParams.x;
#endif
#ifdef _OCCMAP
    occlusion  *= matParams.y;
#endif
#ifdef _SMTMAP
    smoothness *= matParams.w;
#endif

    float oneMinusReflectivity = (1.0 - metallic) * 0.96;
    output.albedoColor  = matColor.rgb * oneMinusReflectivity;
    output.reflectColor = lerp(half3(0.04, 0.04, 0.04), matColor.rgb, metallic);
    output.grazingTerm  = saturate(smoothness + (1.0 - oneMinusReflectivity));
    output.alpha        = matColor.a;
    output.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(smoothness);
    ConvertAnisotropyToRoughness(output.perceptualRoughness, anisotropy, output.anisoRoughness.x, output.anisoRoughness.y);
    output.anisoRoughness.x    = max(output.anisoRoughness.x, 0.0005);
    output.anisoRoughness.y    = max(output.anisoRoughness.y, 0.0005);
    output.surfaceReduction    = 1.0 / (output.perceptualRoughness * output.perceptualRoughness + 1.0);
    output.microOcclusion = occlusion;
    output.emitColor      = matColor.rgb * emmision;

#if defined(_VMAT_COLOR)
    output.testValue = matColor.rgb;
#elif defined(_VMAT_DIFFUSE_COLOR)
	output.testValue = output.albedoColor;
#elif defined(_VMAT_METALLIC)
	output.testValue = metallic;
#elif defined(_VMAT_SMOOTHNESS)
	output.testValue = smoothness;
#elif defined(_VMAT_OCCLUSION)
	output.testValue = occlusion;
#else
    output.testValue = 0;
#endif
    output.reflectOneForTest = lerp(0.04, 1.0, metallic);
    return output;
}

LitParamPerViewSTB GetLitParamPerView(GeometrySTB geo, CameraSTB cam, MaterialSTB mat)
{
    LitParamPerViewSTB output;
    output.specOcclusion = GetHorizonOcclusion(cam.dirWS, geo.normalWS, geo.verNormalWS, 0.8);
    output.NdotV = ClampNdotV(dot(geo.normalWS, cam.dirWS));
    output.envRefl_fv = F_Pow5(saturate(output.NdotV));
    output.reflViewWS = reflect(-cam.dirWS, geo.normalWS);
    float TdotV        = dot(geo.tangentWS,  cam.dirWS);
    float BdotV        = dot(geo.binormalWS, cam.dirWS);
    output.partLambdaV = GetSmithJointGGXAnisoPartLambdaV(TdotV, BdotV, output.NdotV, mat.anisoRoughness.x, mat.anisoRoughness.y);
    return output;
}

LitParamPerLightSTB GetLitByTheLight(GeometrySTB geo, CameraSTB cam, MaterialSTB mat, LitParamPerViewSTB lip, LightSTB theLight)
{
    LitParamPerLightSTB output;
    float NdotL = dot(geo.normalWS, theLight.dirWS);
#if defined(_VSUN__) && defined(_VPOINT__)
    UNITY_BRANCH if (NdotL > 0.0)
    {
#endif
        float3 halfDir = SafeNormalize(theLight.dirWS + cam.dirWS);
        float LdotV = dot(theLight.dirWS, cam.dirWS);
        float NdotH = dot(geo.normalWS,   halfDir);
        float LdotH = dot(theLight.dirWS, halfDir);
        float TdotL = dot(geo.tangentWS,  theLight.dirWS);
        float BdotL = dot(geo.binormalWS, theLight.dirWS);
        float TdotH = dot(geo.tangentWS,  halfDir);
        float BdotH = dot(geo.binormalWS, halfDir);
        float spec_fv = F_Pow5(saturate(LdotH));
		float  occlusion    = ComputeMicroShadowing(mat.microOcclusion * 1.6 +0.2, NdotL, 1.0);
		float3 occlusionCol = GTAOMultiBounce(occlusion, mat.albedoColor);

        float  specTermD     = D_GGXAniso(TdotH, BdotH, NdotH, mat.anisoRoughness.x, mat.anisoRoughness.y);
        float  specTermG     = V_SmithJointGGXAniso(0, 0, lip.NdotV, TdotL, BdotL, NdotL, mat.anisoRoughness.x, mat.anisoRoughness.y, lip.partLambdaV);
        float3 specTermF     = mat.reflectColor + (1 - mat.reflectColor) * spec_fv;
		output.specularColor = (specTermD * specTermG * saturate(NdotL) * theLight.atten * occlusion * lip.specOcclusion) * specTermF * theLight.color;

        float  diffuseTerm   = DisneyDiffuse(lip.NdotV, NdotL, LdotV, mat.perceptualRoughness);
        output.diffuseColor  = (diffuseTerm * saturate(NdotL) * theLight.atten * occlusionCol) * theLight.color;

#if defined(_VSUN_LIGHT_COLOR) || defined(_VPOINT_LIGHT_COLOR)
		output.testValue = theLight.color;
#elif defined(_VSUN_LIGHT_ILLUMINANCE) || defined(_VPOINT_LIGHT_ILLUMINANCE) || defined(_VGET_TOTAL_ILLUMINANCE)
		output.testValue = theLight.color *saturate(NdotL) * theLight.atten * occlusion;
#elif defined(_VSUN_SHADE_LAMBERT) || defined(_VPOINT_SHADE_LAMBERT)
		output.testValue = theLight.color *saturate(NdotL) * theLight.atten * occlusion *INV_PI;
#elif defined(_VSUN_SHADE_SPECULAR) || defined(_VPOINT_SHADE_SPECULAR) || defined(_VGET_TOTAL_REFLECTION)
		output.testValue = (specTermD * specTermG * saturate(NdotL) * theLight.atten * occlusion * lip.specOcclusion) * (mat.reflectOneForTest + (1 - mat.reflectOneForTest) * spec_fv) * theLight.color;
#elif defined(_VSUN_SHADE_SPEC_DGF) || defined(_VPOINT_SHADE_SPEC_DGF)
		output.testValue.r = specTermD;
		output.testValue.g = specTermG;
		output.testValue.b = specTermF;
#elif defined(_VSUN_SHADE_SPEC_D) || defined(_VPOINT_SHADE_SPEC_D)
		output.testValue = specTermD;
#elif defined(_VSUN_SHADE_SPEC_G) || defined(_VPOINT_SHADE_SPEC_G)
		output.testValue = specTermG;
#elif defined(_VSUN_SHADE_SPEC_F) || defined(_VPOINT_SHADE_SPEC_F)
		output.testValue = mat.reflectOneForTest + (1 - mat.reflectOneForTest) * spec_fv;
#else
        output.testValue = 0;
#endif
#if defined(_VSUN__) && defined(_VPOINT__)
    }
    else
    {
        output.specularColor = 0.0;
		output.diffuseColor  = 0.0;
        output.testValue = 0;
    }
#endif
	return output;
}

LitParamPerEnvironmentSTB GetLitByEnvironment(VertexOutput input, GeometrySTB geo, MaterialSTB mat, LitParamPerViewSTB lip)
{
    LitParamPerEnvironmentSTB output;
	float  occlusion    = ComputeMicroShadowing(mat.microOcclusion * 0.8 +0.3, lip.NdotV, 1.0);
	float3 occlusionCol = GTAOMultiBounce( saturate(mat.microOcclusion *1.2), mat.albedoColor);

#if defined(LIGHTPROBE_SH)
    output.diffuseColor      = max( SHEvalLinearL0L1(geo.normalWS, unity_SHAr, unity_SHAg, unity_SHAb)+ input.ambientOrLightmapUV.rgb, 0.0);
#elif defined(DIRLIGHTMAP_COMBINED)
	half4 decodeInstructions = half4(LIGHTMAP_HDR_MULTIPLIER, LIGHTMAP_HDR_EXPONENT, 0.0h, 0.0h);
	{
		float4 direction          = SAMPLE_TEXTURE2D(unity_LightmapInd, samplerunity_Lightmap, input.ambientOrLightmapUV.xy);
		float4 encodedIlluminance = SAMPLE_TEXTURE2D(unity_Lightmap,    samplerunity_Lightmap, input.ambientOrLightmapUV.xy);
		float3 illuminance        = DecodeLightmap(encodedIlluminance, decodeInstructions);
		float  halfLambert        = dot(geo.normalWS, direction.xyz - 0.5) + 0.5;
		output.diffuseColor       = illuminance * halfLambert / max(1e-4, direction.w);
	}
 #if defined(DYNAMICLIGHTMAP_ON)
	{
		float4 direction          = SAMPLE_TEXTURE2D(unity_DynamicDirectionality, samplerunity_DynamicLightmap, input.ambientOrLightmapUV.zw);
		float4 encodedIlluminance = SAMPLE_TEXTURE2D(unity_DynamicLightmap,		  samplerunity_DynamicLightmap, input.ambientOrLightmapUV.zw);
		float3 illuminance        = DecodeLightmap(encodedIlluminance, decodeInstructions);
		float  halfLambert        = dot(geo.normalWS, direction.xyz - 0.5) + 0.5;
		output.diffuseColor      += illuminance * halfLambert / max(1e-4, direction.w);
	}
 #endif
#else
    output.diffuseColor      = 0.0;
#endif
    output.diffuseColor *= occlusionCol;

#if defined(UNITY_SPECCUBE_BOX_PROJECTION)
    float3 reflViewWS = BoxProjectedCubemapDirection(lip.reflViewWS, geo.posWS, unity_SpecCube0_ProbePosition, unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax);
#else
    float3 reflViewWS = lip.reflViewWS;
#endif
    half  reflMipLevel      = PerceptualRoughnessToMipmapLevel(mat.perceptualRoughness);
    half4 encodedIrradiance = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, reflViewWS, reflMipLevel);
#if !defined(UNITY_USE_NATIVE_HDR)
    half3 irradiance = DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR);
#else
    half3 irradiance = encodedIrradiance.rbg;
#endif
    output.reflectColor = mat.microOcclusion * mat.surfaceReduction * irradiance * lerp(mat.reflectColor, mat.grazingTerm, lip.envRefl_fv);

#if defined(_VENV_LIGHT_ILLUMINANCE)
	output.testValue = output.diffuseColor *PI;
#elif defined(_VENV_SHADE_LAMBERT)
	output.testValue = output.diffuseColor;
#elif defined(_VENV_SHADE_REFLECTION)
	output.testValue = mat.microOcclusion * mat.surfaceReduction * irradiance * lerp(1.0, mat.grazingTerm, lip.envRefl_fv);
#elif defined(_VMAT_SPECULAR_COLOR)
	output.testValue = lerp(mat.reflectColor, mat.grazingTerm, lip.envRefl_fv);
#elif defined(_VGET_TOTAL_ILLUMINANCE)
	output.testValue = output.diffuseColor *PI;
#elif defined(_VGET_TOTAL_REFLECTION)
	output.testValue = occlusion * mat.surfaceReduction * irradiance;
#else
    output.testValue = 0;
#endif
    return output;
}

// ------------------------------------------------------------------
VertexOutput ChsForwardVertex( VertexInput input)
{
    VertexOutput output;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, output);

    float4 posWS = mul(UNITY_MATRIX_M, float4(input.posOS.xyz, 1.0));
    output.posCS = mul(UNITY_MATRIX_VP, posWS);

    float3   camPosWS = _WorldSpaceCameraPos;
    output.viewDirWS  = camPosWS - posWS.xyz;

    float3   normalWS   = normalize( mul( (float3x3) UNITY_MATRIX_M, input.normalOS));
    float4   tangentWS  = float4( normalize( mul( (float3x3) UNITY_MATRIX_M, input.tangentOS.xyz)), input.tangentOS.w);
    float    sign       = tangentWS.w * unity_WorldTransformParams.w;
	float3   binormalWS = cross( normalWS, tangentWS.xyz) * sign;

	float4 ndc       = output.posCS * 0.5f;
	output.posNDC.xy = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w;
	output.posNDC.zw = output.posCS.zw;

#ifdef DIRLIGHTMAP_COMBINED
	output.ambientOrLightmapUV.xy  = input.uvLM.xy  * unity_LightmapST.xy        + unity_LightmapST.zw;
 #ifdef DYNAMICLIGHTMAP_ON
	output.ambientOrLightmapUV.zw  = input.uvDLM.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
 #else
	output.ambientOrLightmapUV.zw  = 0;
 #endif
#elif LIGHTPROBE_SH
	output.ambientOrLightmapUV.rgb = SHEvalLinearL2(normalWS, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC);
	output.ambientOrLightmapUV.w   = 0;
#else
    output.ambientOrLightmapUV     = 0;
#endif

    output.uv.xy = input.uv0.xy;
    output.uv.zw = 0;
    output.tangentToWorldAndPosWS[0].xyz = tangentWS.xyz;
    output.tangentToWorldAndPosWS[1].xyz = binormalWS;
    output.tangentToWorldAndPosWS[2].xyz = normalWS;
    output.tangentToWorldAndPosWS[0].w = posWS.x;
    output.tangentToWorldAndPosWS[1].w = posWS.y;
    output.tangentToWorldAndPosWS[2].w = posWS.z;
    return output;
}

float4 ChsForwardFragment( VertexOutput input ) : SV_Target
{
    UNITY_SETUP_INSTANCE_ID(input);
    float2             uv  = input.uv.xy;
    GeometrySTB        geo = GetGeometry(input, uv);
    CameraSTB          cam = GetCamera(input, geo);	
    MaterialSTB        mat = GetMaterial(uv);
    LitParamPerViewSTB lip = GetLitParamPerView(geo, cam, mat);

    LightSTB            sun    = GetMainLight(cam);
    LitParamPerLightSTB litSun = GetLitByTheLight(geo, cam, mat, lip, sun);

    LitParamPerEnvironmentSTB litEnv = GetLitByEnvironment(input, geo, mat, lip);

	LitParamPerLightSTB litSubLights;
	litSubLights.diffuseColor  = 0.0;
	litSubLights.specularColor = 0.0;
	litSubLights.testValue     = 0.0;
#ifdef LIGHTPROBE_SH
 #ifdef VERTEXLIGHT_ON
	SubLightsGeometrySTB subLightsGeo = GetSubLightsGeometry(geo);
	for (int i = 0; i < 3; i++) {
		LightSTB subLight = GetSubLight(i, subLightsGeo);
        UNITY_BRANCH if (subLight.atten != 0.0)
		{
            LitParamPerLightSTB litSubLight = GetLitByTheLight(geo, cam, mat, lip, subLight);
			litSubLights.diffuseColor  += litSubLight.diffuseColor;
			litSubLights.specularColor += litSubLight.specularColor;
			litSubLights.testValue     += litSubLight.testValue;
		}
	}
 #endif
#endif

    float3 color = ( litSun.diffuseColor + litEnv.diffuseColor + litSubLights.diffuseColor ) * mat.albedoColor + litSun.specularColor + litEnv.reflectColor + litSubLights.specularColor + mat.emitColor;
	float  alpha = mat.alpha;

#if defined(_VMAT_COLOR) || defined(_VMAT_DIFFUSE_COLOR) || defined(_VMAT_METALLIC) || defined(_VMAT_SMOOTHNESS) || defined(_VMAT_OCCLUSION)
	color = mat.testValue;
#elif defined(_VGET_TOTAL_ILLUMINANCE) || defined(_VGET_TOTAL_REFLECTION)
	color = litSun.testValue + litEnv.testValue + litSubLights.testValue;
#elif defined(_VGET_SUN_ONLY)
	color = litSun.diffuseColor * mat.albedoColor + litSun.specularColor;
#elif defined(_VGET_ENV_ONLY)
	color = litEnv.diffuseColor * mat.albedoColor + litEnv.reflectColor;
#elif defined(_VGET_POINTLIGHT_ONLY)
	color = litSubLights.diffuseColor * mat.albedoColor + litSubLights.specularColor;
#elif defined(_VSUN_LIGHT_COLOR) || defined(_VSUN_LIGHT_ILLUMINANCE) || defined(_VSUN_SHADE_LAMBERT) || defined(_VSUN_SHADE_SPECULAR) || defined(_VSUN_SHADE_SPEC_DGF) || defined(_VSUN_SHADE_SPEC_D) || defined(_VSUN_SHADE_SPEC_G) || defined(_VSUN_SHADE_SPEC_F)
	color = litSun.testValue;
#elif defined(_VENV_LIGHT_ILLUMINANCE) || defined(_VENV_SHADE_LAMBERT) || defined(_VENV_SHADE_REFLECTION) || defined(_VMAT_SPECULAR_COLOR)
	color = litEnv.testValue;
#elif defined(_VPOINT_LIGHT_COLOR) || defined(_VPOINT_LIGHT_ILLUMINANCE) || defined(_VPOINT_SHADE_LAMBERT) || defined(_VPOINT_SHADE_SPECULAR) || defined(_VPOINT_SHADE_SPEC_DGF) || defined(_VPOINT_SHADE_SPEC_D) || defined(_VPOINT_SHADE_SPEC_G) || defined(_VPOINT_SHADE_SPEC_F)
	color = litSubLights.testValue;
#endif

#ifdef _CHECKVALUE
	color = CheckColorValue(color, _ChkTargetValue, _ChkTargetScale, _ChkRange);
#endif
	return float4(color, alpha);
}

#endif //SEGATB_FORWARD
// ---------------------------------------------------------------------------
//
#ifdef SEGATB_SHADOWCASTER

struct VertexInput
{
    float4 posOS    : POSITION;
    float3 normalOS : NORMAL;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct VertexOutput
{
    float4 posCS : SV_POSITION;
};

// ------------------------------------------------------------------
VertexOutput DepthOnlyVertex(VertexInput input)
{
    VertexOutput output;
    UNITY_SETUP_INSTANCE_ID(input);

    float4 posWS = mul(UNITY_MATRIX_M, float4(input.posOS.xyz, 1.0));

    if (unity_LightShadowBias.z != 0.0)
    {
        float3 normalWS   = normalize(mul((float3x3) UNITY_MATRIX_M, input.normalOS));
        float3 lightDirWS = normalize(_WorldSpaceLightPos0.xyz - posWS.xyz * _WorldSpaceLightPos0.w);
        float  shadowCos  = dot(normalWS, lightDirWS);
        float  shadowSine = sqrt(1 - shadowCos * shadowCos);
        float  normalBias = unity_LightShadowBias.z * shadowSine;
        posWS.xyz        -= normalWS * normalBias;
    }

    output.posCS = mul(UNITY_MATRIX_VP, posWS);

    if (unity_LightShadowBias.y != 0.0)
    {
#ifdef UNITY_REVERSED_Z
		output.posCS.z += max(-1, min(unity_LightShadowBias.x / output.posCS.w, 0));
		output.posCS.z  = min(output.posCS.z, output.posCS.w * UNITY_NEAR_CLIP_VALUE);
#else
        output.posCS.z += saturate(unity_LightShadowBias.x / output.posCS.w);
        output.posCS.z  = max(output.posCS.z, output.posCS.w * UNITY_NEAR_CLIP_VALUE);
#endif
    }
    return output;
}

half4 DepthOnlyFragment(VertexOutput input) : SV_TARGET
{
    return 0;
}

#endif //SEGATB_SHADOWCASTER
// ---------------------------------------------------------------------------
//
#ifdef SEGATB_META

float4 _AlbedoColor;
float  _Metallic, _EmitIntensity;
float  unity_OneOverOutputBoost;
float  unity_MaxOutputValue;
float  unity_UseLinearSpace;

CBUFFER_START(UnityMetaPass)
	bool4 unity_MetaVertexControl;	 // x = use uv1 as raster position	// y = use uv2 as raster position
	bool4 unity_MetaFragmentControl; // x = return albedo				// y = return normal
CBUFFER_END

TEXTURE2D(_MainTex);            SAMPLER(sampler_MainTex);
TEXTURE2D(_MetallicGlossMap);	SAMPLER(sampler_MetallicGlossMap);

// ------------------------------------------------------------------
struct VertexInput
{
    float4 posOS : POSITION;
    float2 uv0   : TEXCOORD0;
    float2 uvLM  : TEXCOORD1;
    float2 uvDLM : TEXCOORD2;
};
struct VertexOutput
{
    float4 posCS : SV_POSITION;
    float4 uv    : TEXCOORD0;
};
struct MaterialSTB
{
    float3 albedoColor;
    float3 emitColor;
};

// ------------------------------------------------------------------
MaterialSTB GetMaterial(float2 uv)
{
    MaterialSTB output;
    half4 colParams = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
    half4 matParams = SAMPLE_TEXTURE2D(_MetallicGlossMap, sampler_MetallicGlossMap, uv);
    float4 matColor = _AlbedoColor;
    float metallic = _Metallic;
    float emmision = _EmitIntensity;
#ifdef _COLMAP
    matColor   *= colParams;
#endif
#ifdef _METMAP
    metallic   *= matParams.x;
#endif

#if !defined(EDITOR_VISUALIZATION)
	output.albedoColor = matColor.rgb *( 1.0 - metallic *0.5)  *( 0.5 + matColor.a *0.5) ;
#else
	output.albedoColor = matColor;
#endif

    output.emitColor = matColor.rgb * emmision;
    return output;
}

// ------------------------------------------------------------------
VertexOutput MetaVertex(VertexInput input)
{
    VertexOutput output;

    float3 posTXS = input.posOS.xyz;
    if (unity_MetaVertexControl.x)
    {
        posTXS.xy = input.uvLM * unity_LightmapST.xy + unity_LightmapST.zw;
        posTXS.z  = posTXS.z > 0 ? REAL_MIN : 0.0f;
    }
    if (unity_MetaVertexControl.y)
    {
        posTXS.xy = input.uvDLM * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
        posTXS.z = posTXS.z > 0 ? REAL_MIN : 0.0f;
    }
    output.posCS = mul(UNITY_MATRIX_VP, float4(posTXS, 1.0));

    output.uv.xy = input.uv0.xy;
    output.uv.zw = 0;
    return output;
}

half4 MetaFragment(VertexOutput input) : SV_TARGET
{
    half4 color = 0;
    float2 uv = input.uv.xy;

    MaterialSTB mat = GetMaterial(uv);

    if (unity_MetaFragmentControl.x)
    {
        color = half4(mat.albedoColor, 1.0);    
        unity_OneOverOutputBoost = saturate(unity_OneOverOutputBoost);	// d3d9 shader compiler doesn't like NaNs and infinity.   
        color.rgb = clamp(PositivePow(color.rgb, unity_OneOverOutputBoost), 0, unity_MaxOutputValue);	// Apply Albedo Boost from LightmapSettings.
    }
    if (unity_MetaFragmentControl.y)
    {
        color = half4(mat.emitColor, 1.0);
    }
    return color;
}

#endif //SEGATB_META
// ---------------------------------------------------------------------------
// ------------------------------------------------------------------------------------
#endif //SEGATB_CHS_INCLUDED

litChk.shader

Shader "SegaTechBlog/lightingChecker" {
	Properties {
		[Header(__ Material Params __________)][Space(5)]
		_AlbedoColor ("Color",      Color)            = (0.4663, 0.4663, 0.4663, 1)
		_Metallic    ("Metallic",   Range(0.0, 1.0))  = 0.0
		_Anisotropy	 ("Anisotropy", Range(-1.0, 1.0)) = 0.0
		_Smoothness  ("Smoothness", Range(0.0, 1.0))  = 0.5	[Space(15)]
		[Toggle(_COLMAP)]   _UseColorMap      ("@ Color Map",                    Float) = 1
		[NoScaleOffset]     _MainTex          ("Color(RGB), Alpha(A)",            2D)    = "white" {}
		[Toggle(_METMAP)]   _UseMetMap        ("@ Mat Map:Metallic",             Float) = 1
		[Toggle(_OCCMAP)]   _UseOccMap        ("@ Mat Map:Occlusion",            Float) = 1
		[Toggle(_SMTMAP)]   _UseSmtMap        ("@ Mat Map:Smoothness",           Float) = 1
		[NoScaleOffset]     _MetallicGlossMap ("Metal(R), Occlude(G), Smooth(A)", 2D)    = "white" {}
		[Toggle(_NORMALMAP)]_UseNormalMap     ("@ Normal Map",                   Float) = 0
		[NoScaleOffset]     _NormalMap        ("Tangent Normal(RGB)",             2D)    = "bump" {}
		[Header(__ View One Element ___________)][Space(5)]
		          [KeywordEnum(_,COLOR,DIFFUSE_COLOR,SPECULAR_COLOR,METALLIC,SMOOTHNESS,OCCLUSION)]_VMAT("> View Material Element", Float) = 0
		[Space(5)][KeywordEnum(_,LIGHT_COLOR,LIGHT_ILLUMINANCE,SHADE_LAMBERT,SHADE_SPECULAR,SHADE_SPEC_DGF,SHADE_SPEC_D,SHADE_SPEC_G,SHADE_SPEC_F)]_VSUN("> View Sun Light Element", Float) = 0
		[Space(5)][KeywordEnum(_,LIGHT_ILLUMINANCE,SHADE_LAMBERT,SHADE_REFLECTION)]_VENV("> View Environment Light Element", Float) = 0
		[Space(5)][KeywordEnum(_,LIGHT_COLOR,LIGHT_ILLUMINANCE,SHADE_LAMBERT,SHADE_SPECULAR,SHADE_SPEC_DGF,SHADE_SPEC_D,SHADE_SPEC_G,SHADE_SPEC_F)]_VPOINT("> View Sub Light Element", Float) = 0
		[Space(5)][KeywordEnum(_,TOTAL_ILLUMINANCE,TOTAL_REFLECTION)]_VGET("> View Total Light Amount", Float) = 0
		[Space(15)]
		[Header(__ Measure The Value __________)][Space(5)]
		[Toggle(_CHECKVALUE)]_CheckValue("> Measure The Output Value", Float) = 0
		[Space(5)]_ChkTargetValue(" ORANGE-GREEN-BLUE", Range(-0.1, 5.0)) = 0.1842
		[Enum(x0.01,0.01, x0.1,0.1, x1,1.0, x10,10.0, x100,100.0, x1000,1000.0, x10000,10000.0)]_ChkTargetScale("    (Higher - Hit - Lower)", Range( 0.001, 1000.0)) = 1.0
		[Space(8)][PowerSlider(2.0)]_ChkRange(" Tolerance", Range(0.0032, 10.0)) = 0.045
		[Space(30)]
		[Header(__ Other Options ____________)][Space(5)]
		[Toggle(_NOPIDIV)]_NoPiDiv("No INV_PI as UnityStandard", Float) = 0
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 100
		Pass {
			Name "FORWARD"
			Tags{ "LightMode" = "ForwardBase"}
			ZWrite On
			Blend One Zero
			BlendOp Add
			HLSLPROGRAM
			#pragma target 3.5
			#pragma multi_compile_instancing
			#pragma instancing_options assumeuniformscaling
			#pragma multi_compile _ VERTEXLIGHT_ON
			#pragma shader_feature DIRECTIONAL
			#pragma shader_feature SHADOWS_SCREEN
			#pragma multi_compile _ LIGHTPROBE_SH DIRLIGHTMAP_COMBINED
			#pragma multi_compile _ UNITY_USE_NATIVE_HDR UNITY_LIGHTMAP_RGBM_ENCODING UNITY_LIGHTMAP_DLDR_ENCODING
			#pragma shader_feature DYNAMICLIGHTMAP_ON
			#pragma shader_feature _NOPIDIV
			#pragma shader_feature _COLMAP
			#pragma shader_feature _METMAP
			#pragma shader_feature _OCCMAP
			#pragma shader_feature _SMTMAP
			#pragma shader_feature _NORMALMAP
			#pragma multi_compile _ _VMAT_COLOR _VMAT_DIFFUSE_COLOR _VMAT_SPECULAR_COLOR _VMAT_METALLIC _VMAT_SMOOTHNESS _VMAT_OCCLUSION _VSUN_LIGHT_COLOR _VSUN_LIGHT_ILLUMINANCE _VSUN_SHADE_LAMBERT _VSUN_SHADE_SPECULAR _VSUN_SHADE_SPEC_DGF _VSUN_SHADE_SPEC_D _VSUN_SHADE_SPEC_G _VSUN_SHADE_SPEC_F _VENV_LIGHT_ILLUMINANCE _VENV_SHADE_LAMBERT _VENV_SHADE_REFLECTION _VPOINT_LIGHT_COLOR _VPOINT_LIGHT_ILLUMINANCE _VPOINT_SHADE_LAMBERT _VPOINT_SHADE_SPECULAR _VPOINT_SHADE_SPEC_D _VPOINT_SHADE_SPEC_G _VPOINT_SHADE_SPEC_F _VGET_TOTAL_ILLUMINANCE _VGET_TOTAL_REFLECTION
			#pragma multi_compile _ _VSUN__
			#pragma multi_compile _ _VPOINT__
			#pragma shader_feature _CHECKVALUE
			#pragma vertex   ChsForwardVertex
			#pragma fragment ChsForwardFragment
			#define SEGATB_FORWARD
			#include "litChkLib.hlsl"
			ENDHLSL
		}
		Pass {
			Name "ShadowCaster"
			Tags{"LightMode" = "ShadowCaster"}
			ZWrite On
			ColorMask 0
			HLSLPROGRAM
			#pragma target 3.5
			#pragma multi_compile_instancing
			#pragma instancing_options assumeuniformscaling
			#pragma vertex   DepthOnlyVertex
			#pragma fragment DepthOnlyFragment
			#define SEGATB_SHADOWCASTER
			#include "litChkLib.hlsl"
			ENDHLSL
		}
		Pass {
			Name "META"
			Tags{"LightMode" = "Meta"}
			Cull Off
			HLSLPROGRAM
			#pragma shader_feature _COLMAP
			#pragma shader_feature _METMAP
			#pragma shader_feature EDITOR_VISUALIZATION
			#pragma vertex   MetaVertex
			#pragma fragment MetaFragment
			#define SEGATB_META
			#include "litChkLib.hlsl"
			ENDHLSL
		}
	}
}


 f:id:sgtech:20190422122809g:plain



マテリアルカラーの測定

さっそく、スフィアを置いて、ライティング検証用マテリアルを適用してみます。

マテリアルを作る

  1. シーンに、プリミティブのスフィアを置く GameObject > 3D Object > Sphere
    f:id:sgtech:20190422122847g:plain
  2. Assetsフォルダに、マテリアルを新規作成する  Assets > Create > Material
    f:id:sgtech:20190422122845g:plain
  3. マテリアルのシェーダーを、SegaTechBlog / lightingChecker に切り替える
    f:id:sgtech:20190422122843g:plain
  4. スフィアに、作成したマテリアルを割り当てる
    f:id:sgtech:20190422122841g:plainf:id:sgtech:20190422122839g:plain


マテリアルの デフォルトカラーを見てみると、グレー 119[sRGB] となっています。 この色は、印刷物や塗装に
おける 一般的な指標である、CIE L*a*b* ミドルグレー:反射率 18.42[%] に相当します。
*2
f:id:sgtech:20190422122917j:plain
このカラーが 正しい反射率になっていることを、マテリアルの 出力値測定機能を使って確認してみましょう。

マテリアルカラーを測定する

  1. マテリアルの View Material Elementプルダウンメニューから COLOR を選択
    > マテリアルカラーが単色で出力表示されます。
    f:id:sgtech:20190422122915g:plainf:id:sgtech:20190422122913g:plain
  2. Measure The Output Valueチェックボックスを オン
    f:id:sgtech:20190422122910g:plain
  3. スライダーを動かして、緑色に光るところを探す
    > 緑色になったとき、そのスライダーの値が、現在のマテリアル出力値となります。
    f:id:sgtech:20190422122908g:plainf:id:sgtech:20190422123005g:plain
    測定した結果は おおよそ0.46、反射率46[%]となってしまいました。
    これは、Unityプロジェクトのカラースペースが、初期設定では Gammaになっているからです。

    プロジェクトがGamma設定のとき、Unityは 色空間をコントロールしません。カラーの入力は sRGB(ガンマ2.2)ですが、これを そのままライティング計算に使い、ガンマ2.2用モニターに そのままの値を出力します。 結果として、入力した色は そのまま表示されるのですが、ライティング計算が 誤ったGamma色空間で行われることになるので、…なんだか…こう…濃くてギラッとしたライティング結果になりがちです。



プロジェクトのカラースペースを、Linearに切り替えます。

カラースペースを変更する

  1. プロジェクト設定ウィンドウを開く Edit> Project Settings
  2. Playerタブを開き、カラースペースのプルダウンメニューからLinearを選択
    f:id:sgtech:20190422123003g:plain
  3. 再び、マテリアルの 計測スライダーを動かして、緑色に光るところを探すf:id:sgtech:20190422123001g:plain
    今度の測定結果は 期待どおり、 おおよそ0.18、反射率18[%]になりました。
    プロジェクトがLinear設定のとき、Unityは 色空間をコントロールして、ライティング計算をLinear色空間で行います。sRGB(ガンマ2.2)カラーの入力を リニアカラーに変換し、これをライティング計算に使います。最後に、ライティング結果を リニアカラーからsRGBカラーに戻した値を、ガンマ2.2用モニターに出力します。結果として、入力した色は そのまま表示され、かつ、ライティング計算も正しく行われる形になります。 つまり、カラースペース設定変更の前後で、モニターに表示されるマテリアルカラーの見た目は変化しません。シェーダー内部での値と、ライティング結果が 変化します。
     f:id:sgtech:20190422122958g:plain f:id:sgtech:20190422122956g:plain



ライトカラーの測定

このマテリアルで、モデルを照らしているライトのカラーも 表示, 測定することができます。

ライトカラーを計測する

  1. マテリアルの View SunLight Elementプルダウンメニューから LIGHT_COLOR を選択
    > 太陽(Directional Light)のカラーが単色で出力表示されます。
    f:id:sgtech:20190422123052g:plainf:id:sgtech:20190422123050g:plain
  2. ふたたび Measure The Output Valueをオンにし、スライダーを動かして、緑色に光るところを探す
    > 測定結果は おおよそ0.9になりました。
    f:id:sgtech:20190422123048g:plain

    Directional Lightオブジェクトを選択してみると、Intensityが1、カラーに少し黄色が入っていて 、
    …だいたい0.9、合ってる!という感じがします。
    f:id:sgtech:20190422123046g:plain
    もっと 大きな値も入れてみましょう。

  3. Directional LightのIntensityを 2 に上げるf:id:sgtech:20190422123044g:plain
  4. Sphereを選択し、マテリアルの測定スライダーを動かして、緑色に光るところを探す
    > 測定結果は おおよそ 4.14 となりました。1.8になるはずが…。
    f:id:sgtech:20190422123134g:plain

    これは、ライトのIntensityがsRGB(ガンマ2.2)値として扱われているからです。ライトのColorは色なのでsRGB扱いで良いですが、Intensity(ライトの強さ)や Indirect Multiplier(ライトマップを焼くときの 強さ補正値) にガンマが掛かってしまうのは、PBRライティング環境を設定するうえで 邪魔になります。
    この仕様、Unityとしては、Gamma設定のプロジェクトとの整合性を狙った仕様なのかもしれません。 ほかにも、HDRIイメージを天球に貼るときに使う Skybox/Cubemapマテリアルの Exposure値が、同様の仕様になっています。



Project SettingsのGraphicsタブに、ライト強度をリニア値として扱うオプション設定が存在します。
が、GUI上には表示されていないので、スクリプトを使って切り替えます。*3

ライト強度のカラースペースを変更する

  1. メニューから SegaTechBlog > LightsIntensity > Linear を選択f:id:sgtech:20190422123131g:plain
  2. マテリアルの測定スライダーを動かして、緑色に光るところを探す
    > 今度の測定結果は 期待どおり、 おおよそ1.8になりました。
    f:id:sgtech:20190422123129g:plain



現実世界では、ライトに照らされたとき モデル全体にライトカラーの光が届く訳ではなく、ライトに正面を向いた所は明るく照らされますが、ライトから横に90度向いた面には光が届きません。

ライトから投げつけた光の粒が、正面ほど 多く当たり、横向きの面には あまり当たらない というイメージです。f:id:sgtech:20190422122647g:plain
この、ライトからの光が 面に当たった量のことを、照度(illuminance)といいます。
照度を出力表示してみましょう。

ライトの照度と表面の輝度を比較する

  1. マテリアルの View SunLight Elementプルダウンメニューから LIGHT_ILLUMINANCE を選択
    > 太陽(Directional Light)の照度が出力表示されます。
    f:id:sgtech:20190422123126g:plainf:id:sgtech:20190422123124g:plain
  2. Measure The Output Valueをオン
    > 太陽正面方向の照度が ちょうど1.8(=ライトカラーと同じ値)になっています。
    f:id:sgtech:20190422123210g:plainf:id:sgtech:20190422123208g:plain

    面に当たった光の量が照度。ですが、これが そのまま、面の 見た目の明るさになるわけではありません。

    カメラから見て、その ライトアップされた面が見えている、ということは、面で跳ね返った光がカメラに向かって飛び込んできた、ということになります。
    f:id:sgtech:20190422122637g:plain

    いわゆる完全拡散反射面、まったくスペキュラの無い、どこから見ても おなじ明るさに見える材質があるとすると、面に当たった光を 全方向 (面の真横までくれば見えないので、半球の範囲) に分配して飛ばさなければいけません。
    f:id:sgtech:20190422122642g:plain

    面に入ってきた光が1なのに、 全方向に1ずつ跳ね返す、というのは物理的にありえません。
    面から 全方向に1ずつ跳ね返すためには、約3倍…ちょうど円周率π倍の光が、面に当たる必要があります。
    逆に、面に当たった光が1だと、そこから跳ね返って 特定の方向に進み ちょうどカメラに当たる光の量は 1 / π となります。

    面で跳ね返って飛んできて カメラに当たった(見えた)光の量のことを、その面の輝度(luminance)といいます。 完全拡散面の輝度を出力表示してみましょう。


  3. マテリアルの View SunLight Elementプルダウンメニューから SHADE_LAMBERT を選択
    > 反射率100%, 完全拡散反射面 の輝度が出力表示されます。 …照度表示から 1 / π 暗くなってるだけですが。
    f:id:sgtech:20190422123206g:plainf:id:sgtech:20190422123204g:plain
    UnityのStandardシェーダーでは、この シェーディング時に照度をπで割る処理を省略しています。 旧来シェーダーとの互換性をとった判断と思われます。 明るさ 1 のライトで照らしたのに、白い面が 0.3のグレー にしかならない!というのが 馴染みにくかったのかもしれません。
    こういった仕様の場合、現実的な光の値を使いたい時には 手計算でπで割った数字を入れて対処することで 限定的には対応できます。 この対処方法については、記事のまとめで 改めて触れます。



太陽の明るさを設定する

準備ができたので、ターゲットの屋外ライティング環境に合わせていきましょう。

光学単位を そのまま使うには、日中屋外の数値は ケタが大きすぎるので、1/5000にしてみます。
そうすると、冒頭で提示したターゲット環境テーブルは、以下のような値になります。

太陽に直交した面に 入ってくる光の量(照度) 太陽から 17.0 青空(半球)から 4.0 合計     21.0 [lux/5k]
太陽の傾斜角が66.6度。水平地面に 入ってくる光の量(照度) 太陽から 15.6 青空(半球)から 4.0 合計     19.6 [lux/5k]
この環境に照らされたグレー18.42%地面の明るさ(輝度) 青空 =日陰   0.23 合計 =日向    1.15 [nt/5k]
青空の明るさ(輝度) 平均     1.27 [nt/5k]
白い雲     2.0 [nt/5k]
水平線     1.6 [nt/5k]
青い空     0.8 [nt/5k]
天頂部     0.3 [nt/5k]
カメラの露出補正(相対補正値) -2.975 [EV]




屋外環境のライティングを整えるのに、スフィア1個では さすがに無理があるので、最低限のシーンデータを用意します。 その他、自作のモデルなどあれば インポートして配置してみてください。lightingCheckerマテリアルは Unity Standard仕様のカラーマップ,ノーマルマップ,マテリアルマップを そのまま適用できます。

屋外環境のシーンデータを用意する

  1. プリミティブの箱を置く GameObject > 3D Object > Cube
    f:id:sgtech:20190422123202g:plain
  2. 箱にも スフィアと同じlightingCheckerマテリアルを割り当てるf:id:sgtech:20190422123241g:plain
  3. プリミティブの板を 地面として置く GameObject > 3D Object > Plane
    f:id:sgtech:20190422123239g:plain
  4. lightingCheckerマテリアルを複製して、板に割り当てるf:id:sgtech:20190422123237g:plain
  5. 板をStaticに指定する
    f:id:sgtech:20190422123235g:plain
  6. ライトプローブを置き、地面に埋まらないよう1.2m程度 持ち上げる GameObject > Light > Light Probe Group
    f:id:sgtech:20190422123233g:plain
  7. リフレクションプローブを置き、地面に埋まらないよう1.2m程度 持ち上げる GameObject > Light > Reflection Probe
    f:id:sgtech:20190422123316g:plain


ごく一般的なシーンセットアップになっていると思います。

 Unity初めての方に このシーンデータの説明

  • Staticに指定した板は、動かさない 背景モデル扱いになります。事前にGlobal Illuminationでライトマップが焼かれ、ゲーム中では このライトマップを利用して 環境光からの拡散反射(diffuse)シェーディングが表現されます。
  • それ以外のモデルは、ライトプローブを利用して環境光からの拡散反射(diffuse)シェーディングを表現します。ライトプローブには、事前にGlobal Illuminationによるライティング情報が焼かれて入っています。
  • リフレクションプローブには、事前に環境マップ(HDRのCubeMapテクスチャ)が焼かれて入っています。全てのモデルは、この環境マップを利用して 環境光からの鏡面反射(reflection)シェーディングを表現します。
  • リフレクションプローブを置かなくても天球が鏡面反射に使われますが、それだと地面板が 実モデルの輝度で映り込まないので、リフレクションプローブを置きました。
  • Global Illuminationの事前計算は、デフォルトだと エディタ上では 必要に応じて自動更新されます。自動更新が邪魔になったら、Lightingウィンドウを開いてAuto Generateチェックボックスをオフにすれば 手動更新になります。だいたい皆、邪魔になってきて切りますが、今回のようなライティング環境セットアップ中は オンのままが便利です。
  • メインライトである太陽から直接のライティングは、全てのモデルにおいて、リアルタイムシェーディングで表現されます。




太陽のライトカラーには、 太陽に直交した面に 太陽から 入ってくる光の量(照度)17.0 [lux/5k] を設定します。

太陽のライトカラーを設定する

  1. 箱のマテリアルの、View SunLight Elementプルダウンメニューから LIGHT_COLOR を選択
  2. Measure The Output Valueをオン、スライダーを 1.7 に設定、すぐ下の掛け数を x10 に設定
    > これで、出力値が17( = 1.7x10 )のときに 緑に光る設定になりました。
    f:id:sgtech:20190422123314g:plain
  3. Directional Lightオブジェクトを選択し、モデルが緑に光るまで ライトのIntensityを上げていく
    > 18.67 で緑に光りました。(いちおう検算するとライトカラーの黄色を掛けて ちょうど17になります。*4
    f:id:sgtech:20190422123311g:plainf:id:sgtech:20190422123309g:plain




太陽の角度を66.6度にセットし、
このとき 水平面に 入ってくる光の量(照度)15.6 [lux/5k] になっていることを確認してみましょう。

太陽の向きを設定する

  1. マテリアルの View SunLight Elementプルダウンメニューから LIGHT_ILLUMINANCE を選択f:id:sgtech:20190422123307g:plain
  2. Directional LightのRotationを (90.0, 0, 0) に設定
    > 試しに、太陽の角度を90度にセットしてみました。
     箱の上面が緑になっているので、このライトで真上から照らすと照度17.0であることが再確認できました。

    f:id:sgtech:20190422123348g:plainf:id:sgtech:20190422123346g:plain
  3. Directional LightのRotationを (66.6, 0, 0) に設定
    > 太陽の角度を66.6度に 傾けてセットしました。
    f:id:sgtech:20190422123343g:plain
  4. マテリアルのスライダーを 1.56 に設定
    > 箱の上面が緑になりました。66.6度から照らした時、水平面の照度15.6にできていることが確認できました。
    f:id:sgtech:20190422123341g:plainf:id:sgtech:20190422123339g:plain



空の明るさを設定する

次に、空を調整します。 デフォルトの天球マテリアルは パラメータを 変更出来ないので、Skybox/Proceduralマテリアルを新規作成し、シーンにセットして使います。 Skybox/Proceduralマテリアルに 出力値の測定機能はありませんが、モデルに映り込んだ天球を、 lightingCheckerマテリアルで測定することができます。
ただし Skybox/Proceduralマテリアルには はっきりした白い雲を表示する機能が無いので省略し、 空の平均 1.27 [nt/5k] , 水平線 1.6 [nt/5k] , 青い空 0.8 [nt/5k] , 天頂部 0.3 [nt/5k] の4つの輝度をターゲットに設定します。

空の輝度を設定する

  1. Assetsフォルダに、マテリアルを新規作成する  Assets > Create > Material
  2. 作成したマテリアルのシェーダーを、Skybox / Proceduralに切り替える
    f:id:sgtech:20190422123428g:plain
  3. ライティング設定ウィンドウを開く Window > Rendering > Lighting Settings
  4. Skybox Materialに、作成したSkybox/Proceduralマテリアルを割り当てる
    f:id:sgtech:20190422123425g:plain
  5. lightingCheckerマテリアルを選択し、 View Environment Light Elementプルダウンメニューから SHADE_REFLECTION を選択
    > ボケた天球が映りました。
    f:id:sgtech:20190422123421g:plainf:id:sgtech:20190422123418g:plain
  6. Measure The Output Valueをオン、スライダーを 0.8 に設定、掛け数を x1 に設定
    > これで、出力値0.8の部分が 緑に光る設定になりました。f:id:sgtech:20190422123414g:plain
  7. Smoothnessを動かして、天球の どのあたりが0.8になっているか観察する
    > デフォルト天球は、水平線でも0.7くらいで かなり暗いようです。
    f:id:sgtech:20190422123629g:plain
  8. Skybox/ProceduralマテリアルのExposureの値を上げて、青い空が主に0.8になるよう寄せていく
    > デフォの1.3から6.0まで上げると、青い空0.8周辺、天頂0.4で平均1.2くらいの、ほどよい値になりました。
    f:id:sgtech:20190422123626g:plainf:id:sgtech:20190422123621g:plain

天球全体の輝度をスケールするだけの、すこし雑な調整です。 水平線が 明るめになってしまいました。
Skybox/Proceduralマテリアルの Exposure以外のパラメータを変更すると、大気スキャッタ計算のバランスが変わって、空の色味がズレていくので、天球の輝度調整に使うことは お勧めしません。
白い雲の表現や、水平線付近の減衰(地表からの埃によるフォグ)などについて 細かくバランスを取るには、skyboxシェーダーのカスタマイズをしたり、シーンデータとして遠景モデルを配置していく必要があります。



空の輝度が決まったところで、今度は 水平地面に 青空(半球)から 入ってくる光の量(照度)4.0 [lux/5k] を確認してみましょう。

空の照度を設定する

  1. lightingCheckerマテリアルを選択し、 View Environment Light Elementプルダウンメニューから LIGHT_ILLUMINANCE を選択
    > 環境光からの照度が表示されました。真っ白です。
    f:id:sgtech:20190422123619g:plainf:id:sgtech:20190422123617g:plain
  2. Directional Lightの Indirect Multiplierを 0 に設定
    > 一旦、地面に反射した太陽光の照り返しを消しました。これで 空からの照度だけを計測できます。
    f:id:sgtech:20190422123701g:plainf:id:sgtech:20190422123659g:plain
  3. Measure The Output Valueをオン、スライダーを 4.0 に設定
    > これで、出力値が4のときに 緑に光る設定になりました。
    f:id:sgtech:20190422123657g:plain
  4. ライティング設定ウィンドウを開く Window > Rendering > Lighting Settings
  5. Environment Lighting > Intensity Multiplierの値を変えて、箱の上面が緑になる値を探す
    > 1.151 で、ちょうど 箱の上面が緑になりました。これで 空(半球)からの照度を4.0に設定できました。
    f:id:sgtech:20190422123738g:plainf:id:sgtech:20190422123655g:plain

Unityにおける 空からの環境照明は、鏡面反射(環境マップ)と拡散反射(ライトマップやライトプローブ)が 別々の仕組みで提供されています。この為、空の輝度をピッタリ決めたから 拡散反射の照度も自動で正しい値になる、という風には なかなかいきません。手動で微調整が必要です。




地面照り返しの明るさを設定する

最後に、さきほど一旦0にした、地面で反射した太陽光の照り返し の照度を調整しましょう。 ターゲット環境に この照度の値は無いので、ざっくり算出します。 このライティング環境に照らされたグレー18.42%地面の、日向での明るさ(輝度)1.15 [nt/5k] なので、これが完全拡散反射面として 下一面に広がっていた場合、下半球からの照度は 1.15 x π = 3.613 [lux/5k] となります。 この値に合わせてみましょう。

地面照り返しの照度を設定する

  1. 箱ではなく、地面板の方の lightingCheckerマテリアルを選択し、 View Total Light Amountプルダウンメニューから TOTAL_ILLUMINANCE を選択
    > 地面への、太陽や空からの照度の合計が出力表示されました。
    f:id:sgtech:20190422123735g:plain
  2. Measure The Output Valueをオン、スライダーを動かして、緑色に光るところを探す
    > 地面への照度は19.6( = 1.96x10 )。ターゲット環境の、 太陽の傾斜角が66.6度。水平地面に 入ってくる光の量(照度)合計 19.6 [lux/5k] に 一致していることが確認できました。
    f:id:sgtech:20190422123731g:plainf:id:sgtech:20190422123733g:plain
  3. こんどは 箱のほうの lightingCheckerマテリアルを選択し、 View Environment Light Elementプルダウンメニューから LIGHT_ILLUMINANCE を選択
  4. Measure The Output Valueをオン、スライダーを 3.613 に設定f:id:sgtech:20190422123728g:plain
  5. 箱を下から見上げながら、Directional Lightの Indirect Multiplierを上げていき、箱の下面が緑になる値を探す
    > 0.32 で、ちょうど 箱の下面が緑になりました。これで 地面照り返しの照度を 3.613に設定できました。
    f:id:sgtech:20190422123821g:plainf:id:sgtech:20190422123819g:plain


カメラの露出を設定する

ライティング設定は完了しましたが、画面が ほぼ真っ白です。1/5000していても、まだ、日中の屋外は眩しすぎます。 Unityの標準カメラには HDRの露出補正機能が無いので、ポストプロセスエフェクトを使って、カメラを適正露出に補正しましょう。

Post-Processingのセットアップ

  1. Assetsフォルダに、Post-processing Profileを新規作成する  Assets > Create > Post-processing Profile
    f:id:sgtech:20190422123817g:plain
  2. シーンに、Emptyを置く GameObject > Create Empty
    f:id:sgtech:20190422123815g:plain
  3. Emptyに、ポスプロ設定保持用コンポーネントを追加  Add Component > Rendering > Post-process Volume
    f:id:sgtech:20190422123813g:plain
  4. Post Process VolumeコンポーネントのProfile欄に、さっき作ったPost-processing Profileをセット
    > これで、カメラが このEmptyに近づくと セットしたポスプロ設定が使われるようになりました。*5
    f:id:sgtech:20190422123901g:plain
  5. Post Process Volumeコンポーネントの Is Globalをオン
    > これでカメラとEmptyの位置に関わらず、シーン内では常に このポスプロ設定が使われるようになりました。
    f:id:sgtech:20190422123859g:plain
  6. Emptyを、PostProcessingレイヤーに所属させるf:id:sgtech:20190422123856g:plain
  7. Main Cameraに、ポスプロ設定取得用コンポーネントを追加  Add Component > Rendering > Post-process Layer
    f:id:sgtech:20190422123854g:plain
  8. Post Process LayerコンポーネントのLayer欄に、PostProcessingレイヤーを指定
    > これで、PostProcessingレイヤーに置かれたPost Process Volumeを カメラが取得するようになりました。
    f:id:sgtech:20190422123852g:plain



手動で露出補正する

  1. Post-processing Profileに、Color Gradingエフェクトを追加  Add Effect > Unity > Color Grading
    f:id:sgtech:20190422123948g:plain
  2. Color Gradingの Post-exposureを -2.975 に設定
    > ターゲット環境の カメラの露出設定(相対露出補正値)-2.975 [EV] を設定し、無事に グレーな地面をグレーに表示することができました!
    f:id:sgtech:20190422123945g:plainf:id:sgtech:20190422123943g:plain



…これで、フォトリアルな質感や 屋外のライティングを 表現できるようになったのか、少し試してみましょう。

マテリアルにバリエーションを出してみる

  1. スフィアを複製し、新しいマテリアルを割り当てて、ゴールド のパラメータを設定f:id:sgtech:20190422123939g:plain
  2. 白いモルタル のパラメータを設定
    f:id:sgtech:20190422124101g:plain
  3. 半渇きの土 のパラメータを設定
    f:id:sgtech:20190422124059g:plain
  4. 草 のパラメータを設定
    f:id:sgtech:20190422124057g:plainf:id:sgtech:20190422124055j:plain



悪くありませんが、ゴールドのハイライトの 色の飛び方が下品です。カラーグレーディングにACES色空間を使うことで、高輝度成分を上品に表現した色調調整が やり易くなります。

カラーグレーディングにACESを利用する

  1. Color Gradingの Modeを ACES に設定
    > 高輝度部分のコントラストが柔らかくなり、上品な質感になりました。
    f:id:sgtech:20190422124053g:plainf:id:sgtech:20190422124139j:plain
  2. Post-processing Profileに、Bloomエフェクトを追加。Intensityを2に設定
    > ついでにブルームを載せたところ…、画面全体が ぼんやり光ってしまいました!
    f:id:sgtech:20190422124137g:plainf:id:sgtech:20190422124135j:plain



実は、Color Gradingの Post-exposureはフィルム現像段階での露出補正を模したもので、撮影時のカメラによる露出補正ではありません。ブルームやDoFエフェクトはレンズで起きる現象を模したものなので、露出補正前の輝度(ほぼ白く飛んでいる)に対して エフェクトが掛けられており、その結果 画面全体がブルームしています。
撮影段階で 適切に露出補正するには、Auto Exposureエフェクトを使います。

オートで露出補正する

  1. Color Gradingの Post-exposureをオフにするf:id:sgtech:20190422124132g:plain
  2. Post-processing Profileに、Auto Exposureエフェクトを追加。Minimumを -6、Maximumを 6、Exposure Compensationを 0.4 に設定
    > Min,Maxには オート露出の 露出補正可動範囲を設定します。Exposure Compensationには、画面内の輝度の平均値を、どれくらいの明るさに変換して表示したいかを 設定します。
    f:id:sgtech:20190422124127g:plainf:id:sgtech:20190422124221j:plain



撮影段階で適切に露出補正が行われ、ブルームは 高輝度部分にだけ発生するようになりました。
ターゲット環境で設定していた露出補正値(EV100準拠の絶対露出補正値 EV15 = 1/5000単位での輝度値に対する相対露出補正値 -2.975EV)は、事前に求めた 撮影対象の平均的輝度値を おおよそ0.1として表示するように設定(その結果 グレイ18%が 約0.18で表示される事が期待)されていますが、このAuto Exposureフィルタでは、動的に 現在の画面の平均輝度が求められ、その値が0.4として表示されるようなスケール値が 画面全体にかけられます。
結果として、画面に明るいものが多く映るほど、露出は絞られ 暗い所が より暗く表示されるようになります。

f:id:sgtech:20190422124215g:plainf:id:sgtech:20190422124210g:plain




まとめ

つくったライティング環境の活用

お疲れ様でした。ライティング環境のセットアップは 以上です。つくったライティング環境の活用方法ですが、まずは 主要なアセットを一ステージ分くらい用意して、仮組みしてみるのが良いと思います。
f:id:sgtech:20190422124231j:plain

アセットが増えて 質感のバリエーションがでてくると、画面のコントラストを作るには カメラやライティングに どんなバリエーションが必要なのかが、はっきりしてきます。そして、プロジェクト内で 質感の違いを どう表現してゆくか、またカラーコレクションの方向性なども、順を追って 絵の仕様を決めていける段階になります。


最終的なゲーム画面、コンセプトアートを実現していく中では、独自表現のマテリアルを追加することがあります。
f:id:sgtech:20190422122623g:plain
ゲーム的に必要性が高ければ、天球に雲を加えたり、大気フォグ表現についても 独自で追加する必要があります。
f:id:sgtech:20190422124207j:plainf:id:sgtech:20190422124204j:plain
こういった拡張を行った際は、ぜひ拡張後に もう一度ライティング環境のテストを行ってください。
基準となる 輝度や照度の比率を、コントロールし続けることが大切です。



今回のセットアップは PBRとして素直な値を設定しましたが、実際のUnityの描画フローには 多くの種類があり、それぞれに特性があります。とても全容は書ききれませんが、いくつか 代表的なものについて、対応をリストしておきます。

Standard

  • ライト直接光によるシェーディング輝度はπ倍明るい。
    • この環境でフィジカルな光単位を扱う場合、手動でライトintensityに1/πした値を入れる必要がある。
    • lightingCheckerマテリアルのNo INV_PI as UnityStandardをオンにすれば、この仕様に沿った値を確認できる。
    • 厳密には正しくない対応である(ライト強度を手動で暗くしても、2次反射以降のシェーディング輝度が1/πされる訳ではない)ことに注意しつつ、仕様として飲み込む方向で。
  • (初期設定では)ライトの強度にガンマがかかっている。
  • ポイントライトの強度減衰がPBR準拠(距離自乗減衰)ではない。
    • これも No INV_PI as UnityStandardをオン,オフして仕様の違いを確認できます。
  • シェーディングモデルは、ローエンド機種でなければPBR準拠。


LWRP

  • まだPreview版を抜けたばかりで、仕様が一般化されていない。
  • ライト直接光によるシェーディング輝度はπ倍明るい。
  • ライトの強度はリニア値。
  • ポイントライトの強度減衰はPBR準拠(距離自乗減衰)。
  • シェーディングモデルは、ローエンド機種でなければPBR準拠。


HDRP

  • まだPreview版で、仕様が確定していない。
  • ライト直接光によるシェーディング輝度は正しい。
  • ライトの強度はリニア値で、光学単位の 大きな数値を そのまま入力できる(内部的には EV100単位のpre-exposure値を算出して事前にかけることで 値の爆発を避けている)。
  • ポイントライトの強度減衰はPBR準拠(距離自乗減衰)。
  • ポイントライトの 光源サイズを規定したり、ライン光源, 面光源を利用できる(距離自乗減衰ではなく エリアライトの減衰カーブが扱える)。
  • ポスプロは独自仕様となる(PostProcessing V3に相当)。
  • シェーディングモデルはPBR準拠、かつ SSS,Aniso,ClearCoat表現が追加されてDisney BSDF相当に近づいた。*6


Gammaワークフロー全般

  • おおむね2Dのゲームだから Photoshopと レイヤー合成の見た目を合わせてくれ、というケース。わかります!
  • とはいえ このワークフロー、実質 カラースペース管理の放棄なので、PBRライティングとの共存は 無理です。
  • もしコストが割けるのであれば、Linearワークフロー内に Gammaワークフロー的な部分を作ることは可能です。




ターゲット ライティング環境テーブルを 自分で用意してみたい時は、以下のメモを参考にしてください。

ライティング環境の求め方

屋外の場合

・まず、太陽と空からの照度を計測します。シンプルな手法が Moving Frostbite to Physically based renderingに載っています。

Measurements were taken at ground level with the light meter’s sensor angled horizontally or perpendicular to the sun (⊥ index). Measurements were performed at various hours of a sunny day with few clouds, in Stockholm in July. The sky values were obtained by occluding the sun with a small object (i.e. casting a shadow on the sensor).

https://seblagarde.wordpress.com/2015/07/14/siggraph-2014-moving-frostbite-to-physically-based-rendering/
  1. 照度計を使い、太陽正面向きの照度を計測 = A [lux]
  2. 太陽が直射する方向だけ 小さなもので隠して(=照度センサーを影にいれて)、空の照度を計測 = B [lux]
  3. 太陽からの照度 C = (A-B) [lux]

個人的に おすすめの照度計は セコニックのL-478Dです。オプションのビューファインダーを付ければ輝度計にもなって便利です。2台欲しい!

・太陽の傾斜角は、場所と時間に基づいて 算出してくれるサイトが いくつもあります。 = Θ

・グレー18%完全拡散反射面である水平地面の輝度、日向は、0.1842 * ( C * cos(Θ) + B ) / π [nt]
・ 同 日陰の輝度は、0.1842 * B / π [nt]
・空の輝度の平均は、B  / π [nt]

・空の細かいところの輝度は、輝度計で計測します。雲や空の輝度は、環境によって かなり大きい振れ幅で変化します。なるべく細かく計測して、ならした値を使いましょう。 こちらの PV Lighthouse - ALBEDO 太陽光発電に関するレクチャーでは、地面の色が いかに空の輝度に影響を与えるかが説明されています。地面に雪が積もると 照り返しで空が2倍明るくなって、太陽光発電にも貢献するそうです!面白いですね。

・その他、標準反射板を持っていれば、輝度計で計測しておくことで、グレー地面輝度の検算が可能になりそうです。

屋内の場合

・屋内のライティングは、もっと複雑な照明設計になってくるので、難しいところです。
・とりあえず、1/5000 lumen単位だと 数字が小さすぎて扱いにくいです。一般的な室内で300[lux] = 0.06[lux/5k]など。間をとるか、場所で切り分けるか、HDRPのような動的光学単位が必要になります。
・実在の部屋や照明器具を 計測, HDR撮影して 寄せていくのが、比較的 現実的な正攻法になるかと思います。
・屋内ライティングは、グレー18%よりも高い反射率を 部屋の標準マテリアルにして 整える必要があります。現代の建物は、手元の照度や 空間の照度、床と壁のコントラストを 省電力で実現する為に、比較的 高反射率の素材を使っている傾向があります。 Panasonic - P.L.A.M. - 各種材料反射率の表で屋内向けの建材や塗装を見てみると、自然物や屋外資材と比べて 高めの反射率が並んでいます。 この場合も、露出補正用のターゲットにはグレー18%を使います。

露出補正値

・EV100の値は、照度計で計測することができます。また、Google FilamentのPBRガイドには、ターゲットの輝度や フラット面への照度、半球への照度から EV100単位の露出補正値を算出する方法が載っています。

自前のシェーダーに測定機能を付けたい

・今回のシェーダー、litChkLib.hlslに入っているCheckColorValue関数を ご覧ください。シンプルな仕組みです。
・Core RP Library使いやすそう!と思った方には、Catlike Coding - Scriptable Render Pipelineチュートリアルが お勧めです。


みなさんも一緒に働きませんか?

長文、読んでくださってありがとうございました。
この記事で 興味を持たれた方、セガ・インタラクティブで私たちと一緒に働いてみませんか?

アーケードゲーム開発は、ハイエンドPC相当の特定GPU構成、個性的な筐体をターゲットに、中小規模ゲーム開発が楽しめる、グラフィックDev好きには なかなか たまらない環境です!
採用情報については、以下のリンクを、是非ご確認ください。
sega-games.co.jp


©SEGA

*1:Unity2019.1では Global Illminationの初期設定が変更された為、環境ライティング系の調整結果が Blogと異なる値に落ち着く。
また、Unity2018.2以前のバージョンでは、シェーダー内の Core RP Libraryへのパスを書き換える必要がある。

*2:Gamma Color119 / 255 \fallingdotseq 0.46 , Gamma Color^{2.2} \fallingdotseq Linear Color なので 0.46^{2.2} \fallingdotseq 0.18
これは完全白色が反射率100%であるという仮定下でのL*a*b*ミドルグレーとなる。

*3:https://docs.unity3d.com/2018.3/Documentation/ScriptReference/Rendering.GraphicsSettings-lightsUseLinearIntensity.html

*4:ライトのデフォルト黄色はGamma Color(255, 244, 214)/255 これは Linear Color(1.0, 0.9074, 0.6800)
 , これに輝度変換係数の(0.2126729, 0.7151522, 0.072175)を掛けて足しあわせ 黄色の輝度は0.911
 ,  18.67*0.911 \fallingdotseq 17.0

*5:カメラが近づいたと見なす距離は、EmptyにBox Colliderを追加して設定する。

*6:Disney BSDFについてはSiggraph2012 - Practical physically-based shading in film and game productionのPhysically Based Shading at Disney とSiggraph2015 - Physically based shading in theory and practiceのExtending the Disney BRDF to a BSDF with Integrated Subsurface Scattering。また 同名やPrincipled BSDFで検索すると 様々な粒度の情報が得られる。

Powered by はてなブログ