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

 

Tricks of Realtime VFX with Houdini詳細解説 vol.1

こんにちは、セガゲームス龍が如くスタジオの伊地知です。

 去年もHoudiniのお話をこのSEGA TECH Blogで掲載しており、これで二回目となります。

さる2018年12月2日にCEDEC+KYUSHU2018が開催されたのですが

その時私の方で講演させていただいたTricks of Realtime VFX with Houdiniというセッションがありまして

(セッションのスライドはこちらからダウンロード出来ます。)

そのスライドの後半に

「実際にすぐにゲームに出して使えるエフェクトのテクニック」=「トリック」

を3つご紹介させて頂きました。

その3つのトリックを今回のSEGA TECH Blogで詳細に解説させて頂きます。

 

  1. 加工して戻す(RestSOP) ー processAndRest01.hip
  2. 独自シミュレーション(SolverSOP)
  3. 最短経路探索(FindShortestPath)

3ついっぺんにやると果てしなく長いので全部で3回に分け、

今回は1つ目である「加工して戻す」というトリックだけを扱うことにします。

シーンファイルもご利用頂けますしHoudiniの体験版(Apprentice版)でも開けますので

実際にご自身のPC(Windows)、Mac、Linuxで確認しながらご覧になるとご理解いただきやすいでしょう。

(HoudiniはいろんなOSに対応しています!) 

 

今回は初心者の方でも安心の詳細解説となっております。

またwrangleの行数も最低限に抑えてあります。 

操作方法や基礎知識に関しては1年前の前回のHoudiniの回でも

techblog.sega.jp

御紹介させおりますのでその辺が分からない方はそちらも合わせてご覧下さい。

 


1.加工して戻す(RestSOP)

では1つ目のトリックの内容から説明させて頂きます。

Houdiniの新規シーンを開いた想定でお話をさせて頂きます。

ダウンロードして頂いた方はシーンを上から順にデータを見ていって下さい。

まず画面一番左下のボタン

f:id:sgtech:20190224233230p:plain

を押してGlobal Animation Optionsを開きます。

FPS30End256にしてSaveAsDefaultボタンを押します。

f:id:sgtech:20190224233226p:plain

ネットワークエディタのオブジェクトレベルでTabキーを押しTabメニューでgeと押すと候補が出ますのでGeometryを選び作ります。

f:id:sgtech:20190224233347p:plain

f:id:sgtech:20190224233344p:plain

f:id:sgtech:20190224233340p:plain

できたgeo1にダブルクリックやiキーなどでジオメトリレベルに潜ります。

たいていの作業はこのジオメトリレベルで行う事になります。

 

TabメニューからSphereを選択してPrimitiveTypeをPolygon

Frequencyを50にします。

f:id:sgtech:20190224233155p:plain

 

次にTabメニューからUVTextureを出してつなげます。

TextureTypeをPolarFix Boundary Seamsにチェックします。

f:id:sgtech:20190224233149p:plain

この状態でビュー上でSpace+5を押すとUVの状態が確認出来ます。

f:id:sgtech:20190224233143p:plain

UVが01の範囲内からはみ出していると都合が悪いので

TabメニューからUV Transformを出してUV Textureにつなげ

ScaleXの値に 1/$XMAX と入力します。

これはUV値のUの最大値で全体を割るという意味で

こうする事によって01の範囲内に収めます。

f:id:sgtech:20190224233137p:plain

ビュー上でSpace+1を押してPerspectiveに戻します。

TabメニューからPointWrangleを出してUV Transformの出力につなげます。

f:id:sgtech:20190224233131p:plain

ポイントアトリビュートの@rest,@Cd,@Alphaを設定します。 

VEXpressionの欄に下記のコードを記述します。

v@rest = @P;
f@Alpha = 0.0;
@Cd = {1,1,1};

@はアトリビュートという意味でジオメトリのクラス(Point,Vertex,Primitive,Detail)自体に持たせる変数です。@の前のvやfは型を指定しておりvならベクター、fならフロートを定義します。ここではRunOverがPointになっているのでPointのクラスのアトリビュートを制御するという事になります。

@restはポジション(@P)を保持しておく為のアトリビュート。

頂点α(@Alpha)は0で初期化、頂点カラー(@Cd)は白で初期化という意味になります。

TabメニューからPointWrangleを出して先程のPointWrangleにつなげます。

f:id:sgtech:20190224233244p:plain

f@Alpha = 1.0;

VEXpressionの欄にはこの様に記述します。

再度Alphaを1にするのには訳がありますがそれはあとで解説します。

TabメニューからTwist( or Bend)を出して先程のPointWrangleにつなげます。

f:id:sgtech:20190224233240p:plain

Limit Deformation to Capture Regionのチェックを外し

Twistの値を360Capture OriginのZを-1Capture Length2にします。

こうする事で球がZ軸に沿って360度ねじれます。

TabメニューからMountaionを出して先程のBendにつなげます。

f:id:sgtech:20190224233233p:plain

MountainノードのHeight1.73Element Size2.18Scale0.06,0.16,0.06、

OffsetX値を-14に設定しY値は20に設定し1フレーム目Altキーを押しながらクリック256フレーム目18と設定しAltキーを押しながらクリック

これでパラメータの欄が緑色になったのが分かります。

これはアニメーションが設定されていますという事を現しています。

ちなみにShiftを押しながらクリックでアニメーションエディタが開きます。

Max Octaves9Lacunarity2.19Roughness0.407とします。

この状態で再生ボタン(画面左下にあります)を押すか、カーソルキーの↑を

押してみて下さい。

f:id:sgtech:20190224233321g:plain

球のトゲトゲが蠢きながら下に流れていくアニメーションが確認出来ます。

確認出来たら停止ボタン(画面左下にあります)を押すか、Ctrl+↑を押して止めて下さい。

TabメニューからTransformを出して先程のMountainにつなげます。

f:id:sgtech:20190224233317p:plain

RotateX値を31Y値を5Z値を5とし少し回転させます

TabメニューからClipを出して先程のTransformにつなげます。

f:id:sgtech:20190224234332p:plain

先程と同じ要領でDistance1フレーム目-1.25256フレーム目1.36とキーを打ちます。

この状態で再生させると下半分が削れた状態が段々と上に上がっていって

トゲトゲの球が消えていくのが分かります。

TabメニューからPointWrangleを出して先程のClipにつなげます。

f:id:sgtech:20190224234328p:plain

@P = @rest;

VEXPressionの欄にこの1行を入力すると歪められていたポイントのポジションが元に戻ります。

この挙動を不思議に思う方もいらっしゃるかもしれませんがこれは

単純に@restに保持していた座標で元の位置に戻っただけなのです。

しかし消されたポイントは戻りません。

歪んだ状態で水平に切るという事は座標を戻すと水平では無く歪んだ状態で削れていくのです。

↑キーで再生させるとこの様に動作しているのが確認出来るでしょう。

f:id:sgtech:20190224234240g:plain

映像の表現ならここまでで良いですがゲームの場合

ゲーム中で表現する為に工夫が必要です。

 CEDEC+KYUSHU2018では私はテクスチャパターンアニメーションでやりましょうと言ってしまいましたが実はもっと良いαカットオフを用いた手法があります。

シェーダ側でαしきい値を用いて透明にするのですが

そのαをどう出すのかがこのトリックの鍵になります。

2つ目のトリックで紹介するSolverというノードをもうここで使います。

毎フレーム値を加工して蓄積させていく事が出来るノードで評価した結果を

キャッシュしておくことが出来ます。

TabメニューからSolverを出して上から4つ目のpointwrangle1から

左から1番目のところにつなげて左から二番目の入力にpointwrangle3をつなげます。

 

f:id:sgtech:20190224234238p:plain

ダブルクリックしてsolver1の中に潜ります。

 TabメニューからAttributeTransferを出してPrev_Frameを第一入力に

Input_2を第二入力につなげPointsにチェックを付けAlphaを指定します。

f:id:sgtech:20190224234234p:plain

AttributeTransferは第一入力に対し第二入力のアトリビュートを近いものから転写していくノードです。MayaであればTransferAttribute、Softimageをお使いだった方ならGATORを御存知だと思いますがその万能版だと思って頂ければ理解が早いかと思います。(Softimageユーザーの方ならMayaのTransferAttributeなんかと比べられるのは屈辱だと思いますがHoudiniのAttributeTransferからすればどちらも五十歩百歩です。)

SolverSOPの中でこの様なつなぎ方をすると前のフレームの結果に対し現在のフレームの第二入力のアトリビュートを転写するという意味を持ちます。

TabメニューからPointWrangleを出して第一入力にPrev_Frame、第二入力にAttributeTransferをつなげます

f:id:sgtech:20190224233439p:plain

VEXpressionの欄には 

@Alpha += @opinput1_Alpha/256;

 と記述します。

このコードと接続の意味は前のフレームの結果のアルファに対し

第二入力のアルファを256で割った数値を加算するという意味になります。

つまりこの2つのノードの意味は毎フレーム現在のアルファ値の1/256を

加算し続けるということになります。

 

uキーで1つ上の階層に戻って256フレーム目まで進めます。

すると球がこの様に見えているはずです。

f:id:sgtech:20190224233435p:plain

毎フレームアルファ値を累積して256フレーム分貯めた結果がこれです。

これをUV座標に基づいてテクスチャに書き出してやります。

GameDevelopmentToolsetのSimpleBakerを使うやり方が最も簡単でしょう。

GameDevelopmentToolsetのインストールの最も簡単な方法は左上の

GameDevelopmentToolsetタブのUpdateToolsetボタンを押すことです。

f:id:sgtech:20190224233432p:plain

ゲーム会社や映像スタジオ、CGスクールなどでプロキシ環境下であれば

この機能がうまくいかない場合があります。

その際は公式のgithubからダウンロードして手動でインストールして下さい。

GameDevelopmentToolsetがインストールされた状態になったら

TabメニューからGameDev Simple Bakerを出して下さい。

f:id:sgtech:20190224233430p:plain

ここで普通に考えればアルファにチェックを入れれば出力出来るはずなんですが

何故か真っ白になってしまうので一旦@Cdに@Alphaを移して

basecolorとして出力する事になります。

 simple bakerの1つ前にPointWrangleを足して

@Cd = @Alpha;

 記述して頂点カラーにアルファを移してからsimple baker でテクスチャを焼きます。

すると

f:id:sgtech:20190224233426p:plain

この様なテクスチャが焼き上がりますのでPhotoshop上でレベル補正かけたり

トーンカーブで補正かけたりして1~254くらいの値の範囲にしておくと

シェーダに食わせた時の見た目が良い様です。

 

モデルの方も同時に出力しておきます。

 UVを設定したuvtransform1から出すと良いのですがそのままだと

25,000頂点もありかなりメモリを食ってしまいます。

uvtransform1の下にPolyReduceを作ってつなげます。

 Percent To Keep1Vertex Attribute Seams0.4にして頂点数を252にまで

落とします。ゲーム内で綺麗に見える最低限の頂点数であれば Percent To Keepを

いろいろ試してみても良いでしょう。

f:id:sgtech:20190224233533p:plain

TabメニューからROP FBX Outputを出してつなげます。

 Output Fileを指定してSave to Diskボタンを押します。

これでモデルファイルも出力出来ました。

 ではUnity上で確認してみましょう。

普通のスタンダードシェーダでは両面に対応していないので

Create -> Shader -> Standard Surface Shader

で作ったものにちょい足ししたシェーダで表示してみましょう。

Shader "Custom/doubleSidedCutOff" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _Cutoff ( "Cutoff", Range(0, 1) ) = 0.5
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _BumpMap ( "Normal Map" , 2D ) = "bump" {}
        _BumpScale ( "Normal Scale" , Range(0,1) ) = 1.0
    }
    SubShader{
        Tags { 
            "Queue" =    "AlphaTest" 
            "RenderType"="TransparentCutout"
        }
        LOD 200
        Cull Off
        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows alphatest:_Cutoff

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;
        sampler2D _BumpMap;

        struct Input {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        half _BumpScale;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;

            fixed4 n = tex2D( _BumpMap, IN.uv_MainTex);
            o.Normal = UnpackScaleNormal(n, _BumpScale);
        }
        ENDCG
    }
    FallBack "Transparent/Cutout/VertexLit"
    
}

このシェーダをアサインしたマテリアルを用意して適当なテクスチャを貼ってあげます。

その際テクスチャのアルファに先程生成したテクスチャを入れ込んでおきます。

するとこの様に表示されます。

youtu.be

 

@restに座標を保持して戻す時に削れたpointが戻らないのは当然としても

生成されたpointがちゃんと良い感じの場所に戻ってくれるのは凄いと思います。

中で一体どういう処理が走ってるんでしょうか?気になります。 

累積アルファのカットオフはテクスチャパターンアニメーションでやるより

遥かに質も向上しメモリも削減出来る賢いやり方なので

覚えておいて損は無いでしょう。

 

さて今回はここまでです、いかがでしたでしょうか?
ゲームに出力する部分は様々な手法を考慮し工夫する必要があります。

現状ビルボードが主流のゲームエフェクトですがこんなトリックを用いる事で思いがけない表現をプレイヤーの方々にお見せし感動体験を演出する事も可能になるのです。

ゲームエンジン側の知識もフル動員すればもっと凄いものが表現出来るでしょう。

勉強する事は山程ありますし考えなければいけない事もいっぱいあります。

ですがやりたい表現が達成出来た時の喜びもまたひとしおです。

次回のHoudiniの記事もお楽しみに。 

 

この記事に興味を持って頂けた方は弊社で私達と一緒に働いてみませんか?

弊社ではHoudiniに興味を持って取り組めるような人を募集しています!
我こそはという方、興味のある方は以下のリンクを是非クリックしてみてください。

 
sega-games.co.jp

 

©SEGA

 

Powered by はてなブログ