『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に行って最も価値がある体験の一つだったと語ってくれました。また、上司がセミナー受講や登壇することに対する理解があることも自分が積極的に活動できる理由であるとも語ってくれました。

 

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

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

 

最後に

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

 
sega-games.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が活躍しやすい土壌があります。

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

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

 

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

©️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で検索すると 様々な粒度の情報が得られる。

ゲーム・アニメーション創りは面白い!

皆さん、はじめまして。

セガ・インタラクティブ 第一研究開発部 デザイン技術セクション テクニカルアーティストの鈴木です。モーションデザイナーとして、アーケードゲームのガンゲームをメインに10年ほど携わった後、テクニカルアーティストとしてモーション作業周りのサポートをしています。最近では「HOUSE OF THE DEAD ~SCARLET DAWN~」のサポートを行いました。また、採用活動業務も担当するようになりました。

ゲーム歴は長く、電子ゲームやファミコンなどに触れて、現在もゲームを楽しんでいます。

はじめに

さて、今回のブログの前置きですが、

  • 「映像アニメーション」に対応して「ゲームアニメーション」、「ゲームアニメーター」としています。
  • このブログでは、ゲームのアニメーションについて、特にゲームならではの表現方法について書いています。
  • ゲーム制作を勉強している、勉強しようとしている、興味がある方に向けた記事となっています。そのためプログラミングや数式、ツールは登場しません。

 

ゲーム制作を勉強している、またはゲーム制作に興味がある皆さんは、ゲーム制作のどの部分に興味があるでしょうか?ゲームデザイン(ゲーム企画)?キャラクターデザイン?アニメーション?表示(メニュー画面のデザインやゲーム中の表示)?背景?エフェクト?それともプログラミングでしょうか?。

 私は昔からゲームをしている中で、ゲームのアニメーションが担っている「ゲームのさわり心地」に興味があり、それをテーマに試行錯誤しています。
 今回は「ゲームアニメーションと触りごこち」に焦点を絞ってブログを書きます。ゲームアニメーションの面白さ、奥深さが伝われば幸いです。

 

ゲームアニメーションとは

「アニメーション」という言葉について、ここでは、カートゥーンや3DCGアニメーションは「映像アニメーション」、ボタンを押すことでキャラクターが動くアニメーションを「ゲームアニメーション」と分けました。

 考え出すと複雑になってしまうので、ゲームアニメーションとは、ここではシンプルに


  • ボタン操作することでリアルタイムに変化するキャラクターアニメーション。
    (ボタン入力の必要ない演出的な動きと分けて考えます)
 
とします。補足として、これらの定義以外はゲームアニメーションではないと述べているのではありません。(ここが「考え出すと複雑になってしまう」ところです)。また、会社としての見解、取り組みを述べたものではなく、私個人の見解です。この点は誤解のないようお願い致します。

ゲームアニメーター(モーションデザイナー)の仕事として求められる役割は、

  • ゲームのアニメーション素材を制作し、実装されたゲームを遊んで確認し、アニメーションを修正。時にはディレクターやプログラマーとも相談してゲームを面白くしていく

です。
 

ゲームアニメーションを創ることの面白さ

長いことガンゲームを創りづつけて、ゲームアニメーションを創る面白さには「映像アニメーションを創る面白さ」と「ゲームアニメーションを創る面白さ」があります。それぞれ分けて考えました。

映像アニメーションを創る面白さ

  • 動きの重量感、しなやかさ、キャラクターの感情を動きだけで表現できる面白さ

    「演技について」考える面白さです。自分が演技しないとしても、役者としての表現追求の奥深さ

  • 時間や言語を超えて伝わる面白さ

    自分の創ったものが良ければいつまで立っても色あせない。違う文化の人にも言葉の壁を超えて伝えることができる。普遍的な魅力。

ゲームアニメーションを創る面白さ

  • ゲームは、ボタン操作しているだけなのに、プレイヤーが重さを感じたり、爽快感を感じたりする瞬間が興味深いです。(「操作感」「さわり心地」と呼ばれるものでしょうか)

    例えば、ガンゲームであれば、ガン型コントローラーのトリガーを引いているだけなのに、弾が飛んでキャラクターに当たった感じがする。アクションゲームであれば、方向キーを押しているだけなのに、移動に加速感が感じられたり、キャラクターの疲れを感じたりする。ボタンを押しただけなのに、モノを斬っている手応えがある。といったことです。改めて考えると、「ボタンを押しただけでゲームによって感覚が変化している」と考えると不思議ですよね?
     さわり心地の、何年経っても変化することなく、文化や言葉の壁を超えて伝わる普遍的な点も魅力的です。例えば、ボタン操作でキャラクターが「重いブロックを押している」アクションをして、それを見たプレイヤーに重さが感じられたら、その感覚は何年経っても変わらないし、どの国の人が遊んでも、その感覚は得られるはずです。
     ゲームを長い期間、何度でも遊んでしまう要素の1つである「さわり心地」が追求できる点、「『ボタンを押して意図した感触が提供できているのか』といった『操作と結果』に踏み入って考える楽しさ」がこの仕事の一番興味深いところです。

  • 作成したゲームアニメーション次第でゲームが難しくなったり、簡単になったり、ゲームそのものの難易度に影響を与える

    例えば、ガンゲームのゾンビ攻撃で、「腕を振ってプレイヤーを攻撃」があるとします。腕を振るために振りかぶります。この振りかぶりのスピードが速すぎれば、プレイヤーはゾンビの攻撃に気づかず、ダメージを受けます。遅すぎれば、ゾンビは撃たれてしまい、ゲームが簡単になります。特にゾンビ攻撃については、できるだけギリギリの難易度になるよう時間をかけて調整します。自分で作成した動きでゲーム難易度に変化を与えているところが面白い点です。
     (HOUSE OF THE DEAD ~SCARLET DAWN~ のアニメーターは3~4人+協力会社さんでの開発です。少人数での開発のため、個人の裁量に任せられているところが大きいので、難易度調整も許されています。アニメーション調整の裁量についてはプロジェクトによって違います。特に家庭用ゲームについては厳密に仕様が決められているものもありますので、学生さんは希望する会社での個人の裁量について質問したほうが良いと思います。)

  • ゲームの仕組みを知ることが面白い
    仕様を「どうやって」実現するかゲームの仕組み(プログラミング)部分にもつながるパートで、ゲームの仕組みについて広く浅く理解する大変さはありますが、仕組みを知る面白さがあります。

もし、学生さんで「自分は映像アニメーターをめざすのか、ゲームアニメーターをめざすのか」を考えるとき、これらの特徴から方向を検討しても良いかもしれませんね。

 

ゲームアニメーションの難しい点

ゲームアニメーションを考える上で難しい点があります。それは「感覚的な部分のため人に伝えづらい」という点です。「伝えづらい」ということは、人から教えてもらうことが難しく、自分で体験して考えていかなければならないということになります。
 実際に製作中のゲームレビューでも、見えにくく伝えづらい操作感や動きの気持ちよさの話になりにくく、見えやすく伝えやすいアートやゲームシステム、ゲーム性に意見が集約しがちです。もちろんこれらも重要です。しかし、先程ゲームアニメーションの魅力で挙げたとおり、時間や文化を超えて伝わる部分であるにもかかわらず、議論にならないのはもったいないと思っています。

感覚的な部分をどう伝えるか課題ですが、海外で映像アニメーション制作している方の講演を聴いたとき、「アニメーションにはアニメーション12原則があり、それを共通言語としてコミュニケーションを取るとリテイク時に伝わりやすい」と紹介されていました。この講演から、表現のエッセンスを体系化して共通言語とすれば、感覚的なことも伝えられるのでは?というヒントをえました。

ちなみに「アニメーション12原則」とはアニメーションを創るための基本的な要素です。言葉で説明するよりも、映像で見るとイメージしやすいのではないでしょうか。

(音が出ます)
vimeo.com

http://the12principles.tumblr.com/post/84175638939/appeal

the12principles.tumblr.com

いかがでしょうか?基本図形が動いているだけなのに、「動きがやわらかい」「生きている感じがする」印象ではないでしょうか。12原則の要素を元にどこを修正すればよくなるのか議論したほうが修正方針が立てやすいと感じました。

 

モノが干渉した表現(当たった感じの表現)と気持ちよさ

ゲームアニメーションの感覚的な部分といっても、「モノが干渉した表現(当たった感じの表現)」「加速感の表現」「ジャンプ表現」などなど、ゲームごとに突き詰める分野が数多くあります。
 今回のブログでは「モノが干渉した表現(当たった感じの表現)」にはどのようなものがあるか考えました。

ゲームにしても、3DCGツール上にしても、現実とは違い、3D空間上では何も設定していないと、モノとモノが交差しても引っかかりもなく、すり抜けてしまいます。
 あなたがゲームを遊んでいて、当たった感じがするのは何らかの「仕掛け」をしているからです。
 当たった感じがする「仕掛け」とは何か?私がゲーム制作の上で学んだことや、ゲームを遊びながら考えたものを、アニメーション原則のような感じで「リアクション原則」としてまとめました。

リアクション原則

カメラシェイク

カメラを細かく振動させるだけの古典的な方法ですが、当たった感じ、モノの重さを伝える効果があります。

ただし、動かしすぎると3D酔いの原因になりるので、加減が難しいところです。また、カメラエフェクトのモーションブラーが入っていると、画面が汚くなってしまうことがあるので、カメラエフェクトやカメラシェイクの調整が必要です。
 ゲームでは、攻撃時やダメージを受けた、爆発、キャラクターが地面に倒れた、キャラクターが壁を蹴って飛び上がった、など使われていることに気づくと思います。

例1.「弾がカメラに当たる → 爆発して消える」

カメラシェイクが無いときは、弾が跳ね返って当たったのはわかりますが、衝撃の物足りなさがあります。
f:id:sgtech:20190325100959g:plain


弾が当たったときと、爆発したときにカメラシェイクを追加しました。衝撃がより伝わった感じがしませんか?(この揺れでもずっと見ていると疲れてしまいますね。実際にプレイして 調整の繰り返しが必要です)
f:id:sgtech:20190325100918g:plain

 

例2.「キャラクターに武器が当たる」

武器がキャラクターに当たっていますが、すり抜けています。
これが

「ゲームにしても、3DCGツール上にしても、3D空間上では何も設定していないとモノとモノが交差しても現実とは違い、引っかかりもなく、すり抜けてしまいます。」

の状態です。
これでは物足りないので、真っ先にエフェクトを付けたくなります。しかし、動きでも解決できます。エフェクトを付けたい気持ちをグッとこらえて、カメラシェイクの効果を入れます。
f:id:sgtech:20190325101122g:plain

武器の当たった感じが増していませんか?
 キャラクターに攻撃が当たったときの揺れは「ダメージを与えた気持ちよさを伝えるために揺らすのか」「ダメージを受けた時の痛さを伝えるために揺らすのか」プレイヤーにどちらの気持ちを与えたいのか考えて、揺れを調整する必要があります。
f:id:sgtech:20190325101031g:plain

例3.「2段ジャンプの踏み切り」

2段ジャンプをするキャラクターがいるとします。しかし、2段めのジャンプが分かりにくかったり、物足りなさがあります。こちらもエフェクトを付けたい気持ちをグッとこらえて、カメラシェイクを入れます。
f:id:sgtech:20190325100846g:plain

「何かに当たったときだけに使うものではない」例でもあります。空中には何もありませんが、空中で踏み切った「重さ」を与えることもできます。
 壁を蹴って飛び上がるときにカメラシェイクをしているゲームもありました。
f:id:sgtech:20190325100816g:plain


ウエイト

モノが当たった瞬間動きを止めて引っかかりを表現します。効果は強力で、ゲームではよく見かける表現です。当たった瞬間に動きが止まるので、プレイヤーに「当たった!」とわからせる効果があります。
 パズルゲームで「絵柄が揃って消える」ときの一瞬の「止め」もこちらの分類に含まれるのではないでしょうか。

この手法もカメラシェイクに注意してゲームを遊んでいると、いろいろなところで使われていることに気づきます。

例.「キャラクターに武器が当たる」

再び武器がキャラクターに当たっているシーンです。
武器がキャラクターに触れたタイミングで武器の動きを止めます。
f:id:sgtech:20190325101735g:plain

こちらのほうが「当たった感じ」が伝わるのではないでしょうか。
ゲームでは、どのタイミングで武器が当たるかわからないので、ウエイト効果はプログラミングで制御します。
f:id:sgtech:20190325101518g:plain


オブジェクトシェイク

「カメラシェイク」ではカメラを動かしましたが、当たった瞬間にモノに対して細かな振動を入れることがあります。2Dゲームではよく見られる手法です。記号的な表現ですが、少ない手間で実現できるので、ゲームでは使われることが多いです。

例.「弾がドラム缶に当たる」

弾が当たっても動かなければ、プレイヤーに背景の一部と捉えられてしまいます。

f:id:sgtech:20190325102240g:plain

現実では重いドラム缶は跳ねる事はありませんが、ゲーム世界観によっては使える手法です。弾に当たって少しでも反応すれば、背景から独立したものである。とプレイヤーに思わせることができます。
 ここではドラム缶を上下に動かしましたが、左右に動かしたほうが良いでしょうか?それとも弾が当たってずれていくのが良いでしょうか?ドラム缶の反応だけでもクリエイティブ力が試されます。

f:id:sgtech:20190325102202g:plain


ノックバック(キャラクターが押される)

格闘ゲームやアクションゲームで見られる、攻撃やガードしたキャラクターが「少し後ろにずれる動き」です。単純そうですが、実はゲーム性とも絡む、奥が深い部分ではないか、と考えています。


アニメーション

アーティストが、キャラクターが攻撃を受けてのけぞったり、よろけたり、倒れなど、キャラクターアニメーションを作成します。ゲームアニメーター(モーションデザイナー)の担当部分です。
 キャラクターアニメーションの弱点は「決まった動きしかしない」です。これをどう崩すかが課題で、いろいろな動きを混ぜたり、物理シミュレーションを混ぜたり、各社いろいろな取り組みをしています。

 

例.「キャラクターに武器が跳ね返される」(オブジェクトシェイク、ノックバック、キャラクターアニメーションの合わせ技)

武器がキャラクターに当たって跳ね返されるアニメーション素材に、青いキャラクターの細かな振動や後ろに下がる動きの組み込みをプログラマー、どれくらい振動して後ろに下げるのかの調整をゲームデザイナーやアーティストがそれぞれ担当し、各パートの役割の合わせ技で表現します。
f:id:sgtech:20190325102457g:plain


変形(形で表現する)

「リアクション」は動きでモノが当たった感じを表現しましたが、「変形」は形で表現します。モノが当たったタイミングで、急激に形を変化させることで、当たった感じを表現します。
 ゲームでは現実と同じ壊れ方や変形を再現すると、非常に手間と時間がかかるので、記号的な変形が使われることがあります。

ゲーム中では車や壁、小物が一瞬で変形したり、シューティングゲームで敵機体が壊れていくのを確認できます。

例1.「ドラム缶が壊れる」

ゲーム特有の「記号的な表現」です。
現実では弾が当たるたびに少しづつ変形しますが、ここでは一定数弾が当たると変形する仕組みになっています。徐々に変形していくのがリアルですが、ゲームでは一気に形が変形したほうが気持ち良さが増します。
変形の効果だけでは当たった感じは弱いので、他の効果も合わせて表現します。

f:id:sgtech:20190325101405g:plain


例2.「キャラクターを踏み潰す」

青がプレイヤー、赤が敵のイメージです。
敵が踏みつけられたとき、敵がペチャンコになることで、プレイヤーに「敵をやっつけた」とわからせると同時に「気持ちよさ」を提供します。
f:id:sgtech:20190325101224g:plain


リアルタイム物理シミュレーション

ゲームエンジンでは物理シミュレーションが搭載されているので、より現実的な動きを表現できるようになりました。キャラクターアニメーションの弱点「決まった動きしかしない」は克服できますが、意図する動きにするには非常に時間と手間がかかります。

ヒットエフェクト

当たった瞬間にヒットマークを出したり、当たったモノを点滅させたり、エフェクトを表示する、ゲームでは必ず見かける手法です。

爆発や攻撃が当たったとき、カメラのストロボ発光のように画面をフラッシュさせる手法もありますが、光過敏性発作 (Wikipedia)という問題があるので、使用はあまり好ましくありません。アーケードゲームでは過剰な発光をしないよう計測しながら開発しています。


SE(効果音)

「音は物質を定義する」と何かで聞いたことがあります。例えば、白い画面に■ (ただの小さい四角形) が横切る動きに、蚊やハエの羽音のSEをつければ人は■を蚊やハエと認識できます。

ゲーム効果音にもいくつか種類があると考えています。

  • 質感を伝えるSE(想像系)
    現実ではありえない音ですが、質感が伝わる音。例えば20年前の2D格闘ゲームのガード音、キャラクターが敵に触れてやられたときのSE、30年前のシューティングゲームの爆発音など、「バシッ!」や「ドカーン!」ではない音
  • 見た目、イメージ通りのSE(リアル系)
    映画の効果音のようなリアルと感じられる音。
  • 特殊
    攻撃ヒット時に和音を鳴らしたり、音楽にヒットSEを合わせたりしたもの。

 

スナップ

アイテム欄に装備をドラッグ・アンド・ドロップした時、「カチッ」とハマる演出がこれにあたります。office製品やグラフィックソフトでも「スナップ」という機能があり、「ピタッ」とくっついた瞬間に気持ちよさと「合った!」という感覚があると思います。

例.「離れているキャラクターを引き寄せる」

あるゲームでは「キャラクターが離れていても引きつけて投げる」技があります。ゲームとして見事な手応えが感じられます。組み合った瞬間に「ウエイト」演出が含まれています。
f:id:sgtech:20190325104706g:plain

処理落ち、スロー

格闘ゲームの最後の一撃を決めて、ゆっくり吹っ飛ぶ演出がこれにあたります。ヘッドショットが成功するとスローになる演出のゲームもあります。「ここぞ!」という時に使う事で特別な、より強い手応えを表現できます。

例1.フィニッシュブロー

キャラクターの最後の一撃をくらったやられ演出です。もう少し「やっつけた!、やられた!」という余韻を提供したいところです。
f:id:sgtech:20190325102814g:plain

 

攻撃が当たった後、スローを入れました。最後の一撃の感覚を長く残すことで、やっつけた、やられたの感覚を強調することができます。
f:id:sgtech:20190325102642g:plain
 

例2.撃破

モノに当たった感じの表現とはずれますが、気持ちよさにつながる部分だと思います。
シューティングゲームのボス機体を破壊したイメージです。激闘の末にこの終わり方をしたら、プレイヤーはどんな気持ちになるでしょうか。
f:id:sgtech:20190325102554g:plain

最後の爆発でエフェクトの動きをスローにしました。「爆発した」ことを強調することで、倒した達成感を長持ちさせる効果があります。
f:id:sgtech:20190325110007g:plain

ハプティクス(皮膚感覚フィードバック)

身近なところではコントローラーの振動があります。ゲームエンジンでもコントローラーを振動させることができます。モノに当たった瞬間にコントローラーを振動させることで、当たった感じを提供します。
 その他、引っ張られる感覚が得られたり、圧力が感じられる装置の研究が進んでいます。(セガ・インタラクティブでも新しい感覚を伝えられる装置の調査、研究をしている部署があります)

 

 

以上がゲームでよく見られる「当たった感じがする『仕掛け』」だと考えています。

これらを組み合わせてゲームにしていきます。ただし、注意点があります。

  • 「意図したことがプレイヤーに伝わっているか?」「個々の原則は十分調整されているか」を考える
    例えば、迫力を出そうと爆発エフェクトを大きくしたところ、キャラクターの動きが見えない、変形が見えない、などのことがあります。それぞれの要素をバランスよく考える事が大事です。とは言うものの、私も実装してから「しまった」と思ったり、指摘されて気づくことが多々ありますので、これは永遠の課題です。
  • タイミング
    モノに当たって、エフェクトやリアクションを「いつ開始するか」が重要です。タイミングがずれていると原則を組み合わせてもよくなりません。

  

どうすれば? ~トレーニング法~

「ゲームアニメーションの難しい点」でも述べたとおり、感覚的な部分なので、作例を見て「なるほど!そういえば!」と思う方もいれば、「当たり前のことじゃん」「そうなんだ」「そうなのかなぁ」と思う方もいらっしゃるかもしれません。重要なのは、これらの原則を頭の片隅に置きつつ「ゲームを遊んで、実感して、自分なりの仮設を立てて、実践(制作)していく」ことです。

面白いと思うゲームを遊んで、なぜさわり心地がいいか考えるうちに表現力もあがる。一石二鳥ですね(笑)。

 

 最後に

面接で「ゲームが好きです!」という学生さんは多いのですが、もう一歩「ゲームのどんなところに注目して遊んでいるのか」「どこが、なぜよかったのか」「(気になる点があれば)自分だったらこうしたい!」を伝えられると、より良いアピールになるのでは?と考えてブログを書かせていただきました。

ゲームアニメーションを希望する方が増えることを願ってやみません。

 

セガ・インタラクティブは2019年2月より大崎オフィスに引っ越しました。コンビニや社員食堂、カフェ、バーコーナーなど充実した施設があるオフィスです。気持も新たに業務に励んでいます。

 

ご興味ありましたら、下記の弊社グループ採用サイトをご確認ください。

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

 

 ©SEGA

 

Powered by はてなブログ