rn.log

備忘録など

【ふりかえり】CyberAgent Graphics Academy に参加しました

はじめに

CyberAgentさんが主催する、Graphic Academy に参加させていただきました。

www.cyberagent.co.jp

今回は、ふりかえりの記事を書きたいと思います。

アカデミーの流れ

アカデミーの流れをざっくりまとめると、以下のような流れでした。

  • 書類選考
  • 面接
  • 講義 (2022/02/05 ~ 2022/04/09)
  • 制作物発表会 (2022/4/23)

アカデミー開始以前

書類選考 (2021年12月)

Googleフォーム上で質問に回答し、応募を行います。

質問の内容は以下のようなものでした

  • アカデミーを受講したいと思った理由
  • CyberAgentへの興味
  • 印象に残っている開発経験
  • 使用したことのあるフレームワークや開発言語
  • アピールポイント
  • 学歴

グラフィックスを学ぶ下地があるかどうかを判断されたのではないかと思います。

アカデミーのDay1では、カメラ変換周りの話や、行列演算まわりの講義があり、
多少の数学の知識が必要になりました。

面接 (1月)

  • 書類選考を通過すると、次は面接があります。
  • 面接の日程は希望を出すことができる形になっていました (Googleカレンダー上で選ぶ形でした)
  • 面接では、アカデミーの趣旨をCAの方が解説してくれました
  • 自分からは、グラフィックスを体系的に学びたいという意思や、グラフィックスエンジニアとしてのキャリアに興味がある、などといった話をしました。(プロセカを遊んでいるという話もしました)
  • ミスマッチを防ぐ場として、面接を行っているとの話でした

余談 : 面接の場にはCAの矢野 春樹さん(@harumak_11)がおられたので緊張しました。 LIGHT11にはいつもお世話になってます。

選考通過 (1月末)

  • 1月末あたりに、選考結果はメールで届き、Graphics AcademyのSlackに招待されました。
  • これ以降は、Slack上でCyberAgentの運営の方々や、ほかの受講生の方々とやり取りをしていくことになります。
  • 受講生は、私を含めて20名いました。(技術的につよつよな方々が集まっている印象でした)

グラフィックスに興味がある人が20人も集まるなんて、すごい!

教材が届く (2月)

  • グラフィックスアカデミーの講義資料(紙の冊子)
  • コンピューターグラフィックス (紙の書籍)
  • HLSLの魔導書

講義について

  • Zoom上で講義があり、グラフィックスに関わるあらゆることを叩き込まれます
  • 講師の方は、Unityの石井 勇一さん(@z_zabaglione)でした
  • 内容の一例
    • グラフィックスの理論寄りの話
      • 画像フォーマットの話
      • カメラ座標変換や、行列演算まわりの話
      • NPRやPBRの話
      • 影の描画の仕組み
      • ライティング など
    • NPRシェーダーやPBRシェーダーの作成
    • 複数Passによるシェーダー(.shader)の実装
    • ポストプロセスの話
      など

NRPシェーダーに関しては、記事にまとめてみました。
【Unity ShaderLab】NPRなライティングを実装してみる

講義の流れ

  • 毎週土曜日、朝の10:00から夜の19:00までZoom上で講義があります
    • 受講生20名がZoom上に集まり、講師(石井さん)の講義を受けます。
       - 1時間ごとに10分くらいの休憩がありました。
    • 13:00から14:00までお昼休み
    • 石井さんの講義があるのは 10:00 ~ 16:00あたりで、残りの時間は課題のレポート作成に時間を使う感じでした。
  • 毎週課題があり、次の講義までに提出する必要があります
    • 課題の形式はレポート形式で、自由なフォーマットで書くことができました。

受講生の方々の熱意が高く、欠席者はいませんでした (体調不良で欠席する人が1, 2名いた程度でした)

最終課題デモ

アカデミーの最終日には、制作物を発表するという最終課題があります。
Day5 のタイミングで、CAの方々が最終課題の一例(デモ)を見せてくれました。
- 株式会社Colorful Palette の山口 智也さん ( @togucchi )
- 株式会社グレンジ の山上 翔さん

山口さんの作品はニーアっぽい雰囲気の作品で、山上さんの作品はトゥーン系の表現でした。


使われていたテクニックの一例
- Deferredレンダリング
- G-Buffer(深度や法線)を利用した、アウトライン抽出
- グラデーションフォグ
- 距離・高さに依存するカスタムフォグ
- Cook-Torranceのスペキュラモデルを利用した金属表現
- Jump Floodを利用したアウトライン
- 色と場所を指定できるBloom (スキンブルーム)
などなど

山口さんはプロセカに関わっているということもあり、アーティストでも使いやすいようなカスタムエフェクトを作っているのが印象的でした。
参考 : 『プロセカ』の世界観はUnityシェーダで表現 空気感、柔らかさ、奥行きを作る4つのポストエフェクト - ログミーTech

懇親会 (2回)

  • CAの方々とZoom上で情報交換する機会がありました
  • 懇親会では nonpi の食事をふるまってくれました (費用はCyberAgentさんが全部持ってくれるという事で、太っ腹です)
  • こちらの質問にはどんなことでも答えてくれる感じでした。
    • Cyber Agentという組織について
    • グラフィックスエンジニアの業務について
    • グラフィックスエンジニアのキャリアについて
  • Cyber Agentの方が場を取り仕切ってくれたので、気持ちよく時間をすごせました。

井戸端

  • 受講生(希望者)が毎週木曜日・日曜日の22:00~23:00にあつまり、課題のわからないところを教えあうという取り組み(井戸端)がありました。
  • 井戸端は受講生の一人(丸山 純さん) のアイデアから始まった取り組みになります。

制作物発表会 (最終課題)

受講生の方々が、絵作りをテーマにして作品発表を行いました。
テーマはさまざまでした。

  • キャラクター表現をテーマにした作品
  • SFをテーマにした作品
  • ブラウン管シェーダー
  • 水槽をテーマにした作品
  • ハッチングを利用した手描き調の表現
  • 草原をテーマにした作品
  • 雪原をテーマにした作品
  • 光の使い方にこだわったライブステージ
  • ニーアっぽい絵作り
  • 海をテーマにした作品
  • アメコミをテーマにした作品
    などなど

人によって目指す方向性が違うのが面白かったです。
- 汎用性を意識している方
- パフォーマンスを意識した方
- 技術的な興味関心が高い方
- ルックをリッチにすることを意識した方

作品

私は 「リアル調と手描き調の両立」をテーマにした作品を制作しました。


ポストエフェクトで手書きっぽい感じに加工しています。

アセットは、CyberAgentゲーム事業部から提供いただいた素材をお借りしています。
(BLADE XLORD のアセットになります)

制作の流れは以下のような感じです。
- 火山のアセットを並べて、URP PostProcessingで加工
- 陽炎エフェクトを作成し、高熱を表現
- 地面マテリアルを調整し、キャラクターを目立たせる
- 距離・高さに依存するフォグの作成
- 深度に応じて彩度を落とすエフェクトの作成
- 近景を赤・遠景を青に寄せる (色彩遠近法)
- 深度を利用したアウトライン描画キャラだけを専用テクスチャに描画した後、アウトライン抽出
- Sobelフィルタで水彩画調に加工


作品をOSSとして公開している方もいました。(yunodaさん)

アカデミーで得たもの

グラフィックスの根幹にあるものへの理解が深まった

  • カメラ変換周り・ノーマルマッピング・影の描画・ライティングあたりの話は理解が浅かったのでとても勉強になりました
  • グラフィックスに関する疑問をSlackで投げると、講師やCAの運営の方々がすぐに回答してくれたのも良かったです。
  • 課題の中には、カメラのビューボリュームの投影を可視化するという課題があり、カメラ変換のイメージがつかめるようになりました。

絵作りの楽しさを知ることができた

  • 絵が美しいゲームを観察し、どうすればそれに近づけるのかをひたすら思考し、実装するという経験ができました。
  • 絵がそこそこのクオリティになるまでに2、3週間かかりました。
  • 数週間かけて、自分の絵を目標とする絵に近づけていくというもので、ある意味デッサンに近い行為だと感じました。
  • グラフィックスが美しいゲームは「自分には作れないかも」と今まで感じていたが、「自分でも作れるかもしれない」という認識に変わった

最終課題で得られるものが多かった

  • ゲームを観察し、どういう技術が使われているかを観察するトレーニングになった
  • 絵の見た目をリッチにしていく過程で、実装やアイデアに行き詰まることがあった
    • その際に、技術カンファレンスの資料や、論文にヒントが書かれてないか読んでヒントを探すということを行った
    • カンファレンス資料や論文などは、自分の道具として考えられるようにもなりました
    • アカデミーを受ける以前より、CEDECの資料を読むことへの抵抗が薄くなったと感じます
  • ほかの方の発表を見ることで勉強になった
    • 技術的に工夫したポイント
    • 苦労したポイント
    • 他の方の発表スライドと最終課題のUnityプロジェクト一式を見ることができる

グラフィックス技術資料を読むことへの心理的な抵抗が減った

  • 実装が上手くいかないとき、CEDECの資料を読んでヒントが無いか探した
  • 論文を読むこともあった

グラフィックスの引き出しが増えた

  • Slack上で、グラフィックス関連の情報交換が盛んにおこなわれていました
  • グラフィックスに関する情報が集約される感じで、グラフィックスの引き出しが増えたと感じました。

受講生とのつながりができた

  • グラフィックスアカデミーが終わった後も、Slackは残したままにしていただけるとのことでした
  • アカデミーが終わった後も、交流は続けていきたいと思っています。
  • 最終課題をブラッシュアップしたものに上げる、みたいな使い方をするのも良さそうな気がします。
  • 作品の途中経過を上げると、他の受講生の方がリアクションしてくれたり、為になる情報を共有してくれたりなど、 いろいろと勉強になりました。
  • 逆に他の受講生が詰まっているところがあったとき、自分からアイデアを提案するといったことも行いました。
  • 互いに教えあうという良好な関係を築くことができたと感じました。

FrameDebuggerと仲良くなった

  • 思った通りの結果にならない、と思ったときはFrameDebuggerとにらめっこしていました。
  • シェーダーへの入力・シェーダーの出力・レンダーターゲットといった情報が見れるので、描画のデバッグには欠かせない機能です

Slack上で話題に上がったシェーダーやツール

NiloCatさんが出しているToonシェーダー
- https://github.com/ColinLeung-NiloCat/UnityURPToonLitShaderExample
Microsoft PowerToys : https://docs.microsoft.com/ja-jp/windows/powertoys/
- PowerToys に入っているカラーピッカーを使うと、画面の色を数値で見ることができ、シェーダーをデバッグする際に役に立ちます。

Color Picker で色を表示

アカデミーを終えてみて

  • グラフィックスを学ぶ仲間がいたので最後までモチベーションを保ったままグラフィックスを学ぶことができた
    • 独学でグラフィックスを学んでいた時は、一緒に学ぶ仲間がいなかったり、発表の機会が無かったので挫折しやすかった
  • 毎週課題を提出する必要もあったので、緊張感を持って取り組むことができた
  • 作品を発表するという場もあったので、そこを目標として絵作りに取り組むこともできた
  • 作品に納得がいっていない部分があるので、アカデミーが終わった後もクオリティアップを続けていきたい。

CyberAgentさんについて

  • 費用を投じて、他社のエンジニアにノウハウを教えるというのはすごいと思いました。
    • OSS活動も行っていたり、CA.unityを開催するなど、技術投資や業界貢献に力を入れている会社さんだと感じました
  • 優秀な方々が多く集まっているという印象を受けました

【技術書】シェーダーグラフの書籍をリリースしました【ShaderGraph CookBook vol.1】

はじめに

2021/03/21 に Unity ShaderGraphの本をリリースしました。
今回は、この本の魅力を紹介したいと思います。
zenn.dev

本書の特徴

  • 20種以上のノードの解説
  • 30種以上の作例
  • サンプルプロジェクト付き


特徴① : ノードの解説

ノードについての解説や、ノードで作れる表現の作例を紹介しています。

f:id:r-ngtm:20210408043105p:plain

サンプルページ

🍎 Sineノード|Unity ShaderGraph CookBook vol.1【ShaderGraph 入門】

特徴② : 豊富な作例

書籍では、30種以上の作例を紹介しています。
ShaderGraph単体で完結するような表現に焦点を絞っています。
f:id:r-ngtm:20210408044005g:plain

作例を知りたい方は以下のページをご覧ください。
🍍 レシピ一覧|Unity ShaderGraph CookBook vol.1【ShaderGraph 入門】



特徴③ : サンプルプロジェクト

本書の作例の一部は、GitHubにて公開しています。 github.com

環境

Unity2020.3.0f1
Universal RP 10.3.2

【Houdini + Unity】点光源の影でグリッドを作って遊ぶ

はじめに

Unityのポイントライトの影でグリッドを出してみました。
今回はこの3Dモデルの作り方を紹介します。

f:id:r-ngtm:20210208013405p:plain
ポイントライトの影でグリッドを作ってみました
www.youtube.com

作り方

グリッドを球面へ投影したようなモデルを作ります。 このモデルと点光源が地面に映し出す影はグリッドの形になります。

f:id:r-ngtm:20210208010331p:plain
平面上の点を球面へ投影

投影点の座標の求め方

線分PAの長さをD, 線分PBの長さをd、球の半径をRとおいたとき、以下のようになります。
dが分かれば、投影点Bの座標が分かります。

f:id:r-ngtm:20210208005259p:plain
平面上の点を球面に投影

長さdの導出

三角形 HPA と 三角形BPHは相似なので、以下が成り立ちます。

 
\begin{align}
PB:PH &= PH : PA \\\
\\\
d:2R &= 2R : D \\\
\\\
d&= \frac{4R^2}{D}  
\end{align}

投影点Bの座標は以下のようになります。


\vec{B} = \vec{P} + d \cdot  \dfrac {\vec{PA}}{D}

Houdiniで実装する

投影点Bの座標が求まったので、次にこれをHoudiniで実装します。

Grid

Gridノードを使ってグリッドを作成します。

f:id:r-ngtm:20210208012423p:plain
グリッド作成

PolyExtrude

PolyExtrudeノードを使ってグリッドに穴をあけます。

f:id:r-ngtm:20210208012500p:plain
穴をあける

Attribute Wrangle

投影点の座標を計算して、グリッド点を球へ投影します。

f:id:r-ngtm:20210208012809p:plain
投影点の計算

vector origin = { 0, 1, 0 }; // 点Pの座標
vector p = (@P - origin); // 点Pから平面へ向かうベクトル
vector dir = normalize(p); // 正規化
float R = 0.5; // 球の半径
float D = length(p); // 点Pから平面上の点までの距離
float d = 4 * R * R / D; // 点Pから投影点までの距離

@P = origin + d * dir; // 投影点の座標で置き換える

投影したモデルは、ROP FBX Outputノードなどで3Dモデル化します。

完成

モデルをUnityへ取り込み、点光源を置くと、影がグリッドになります。

f:id:r-ngtm:20210208012942p:plain
モデルと点光源を配置

点光源の位置

f:id:r-ngtm:20210208013119p:plain
点光源の位置 (0, 1, 0)

モデルの位置

f:id:r-ngtm:20210208013143p:plain
モデル位置 (0, 0, 0)

関連書籍

www.amazon.co.jp

【シェーダーグラフメモ その58】XOR演算を利用した市松模様

はじめに

XOR演算を利用して、市松模様を作る方法を紹介します。

f:id:r-ngtm:20210130051822p:plain:w320
市松模様

今回はワールド座標のX,Z成分から市松模様を作ります。

目次

 

環境

Unity2020.2.0f1
Universal RP 10.2.2


市松模様の作り方

タテ方向、ヨコ方向のシマシマを作ります。

f:id:r-ngtm:20210130045131p:plain
シマシマ

二つのシマシマに関してXORを適用すると、市松模様が作れます

f:id:r-ngtm:20210130045207p:plain
XORを取ると市松模様になる

XORについて

xor(a,b)は以下のような値を取ります。

a b xor(a,b)
0 0 0
0 1 1
1 0 1
1 1 0

シェーダーグラフ実装

シマシマを作る

以下のようなシェーダーグラフを組みます。

f:id:r-ngtm:20210130050053p:plain
ワールド座標からシマシマを作る

結果

これを板ポリゴンに貼り付けると、以下のようになります。 f:id:r-ngtm:20210130050809p:plain:w480

色のX成分、Z成分はそれぞれ以下のようになっています。

f:id:r-ngtm:20210130051643p:plain
X,Z 成分はそれぞれ0, 1 の繰り返しになっている

次にXORを利用して市松模様を作ります。

f:id:r-ngtm:20210130051822p:plain:w320
市松模様

ShaderGraphにXOR演算は無い

ShaderGraphにはXOR演算は用意されておらず、自分で作る必要があります。 今回はXORの実装方法を3つ紹介します。

方法1 : Abs を利用したXOR

1つ目の方法はAbsを利用したXORの実装です。

abs(a-b)

X成分(横シマ)からZ成分(縦シマ)を減算すると、以下のようになります。

f:id:r-ngtm:20210130054431p:plain
二つのシマシマを引き算する

減算結果にAbs(絶対値)を取ると、0,1が交互に並びます。

f:id:r-ngtm:20210130054944p:plain
abs(絶対値)を適用

シェーダーグラフで実装すると以下のようになります。

f:id:r-ngtm:20210130055257p:plain
absを利用したXORの実装

方法2 : X・(1 - Z) + (1 - X)・Z

以下の計算式でXORを作ることができます。

x * (1 - z) + (1 - x) * z

f:id:r-ngtm:20210130060901p:plain
X(1-Z) + (1-X)Z

f:id:r-ngtm:20210130061329p:plain
X(1-Z) + (1-X)ZによるXOR実装

方法3 : Moduloを利用したXOR

3つ目の方法は、Moduloを使ったXORの実装です

fmod(a+b, 2.0)

X成分とZ成分を足し合わせると、以下のようになります。

f:id:r-ngtm:20210130053513p:plain
横のシマシマと縦のシマシマの加算

2で割った余りを計算すると0と1が交互に並びます。

f:id:r-ngtm:20210130053955p:plain
Addした結果にMod 2 を取る

シェーダーグラフで実装すると、以下のようになります。

f:id:r-ngtm:20210130052243p:plain
Moduloを使ったXOR実装

結果

f:id:r-ngtm:20210130051822p:plain:w320
市松模様

【Unity】シャドウマッピングについて

環境

Unity2020.2.0f1
Universal RP 10.2.2

シーンに光源を置いてみる

シーンに緑色のDirectional Lightを置くと、以下のようになります。

f:id:r-ngtm:20210126050256p:plain:w480
緑のDirectional Lightを配置

光が当たった領域は緑色になりますが、影の領域には色を与えません ( = 真っ黒になります)

シャドウマップ

Unityでは、「ある領域が影になっているかどうか」の判定にシャドウマップを利用します。

f:id:r-ngtm:20210126072742p:plain:w640
シャドウマップの深度と描画点の深度を比較

シャドウマップのベイク

点光源を例に見てみます。
点光源から、シーンのオブジェクトに最も近い点までの距離を求め、それをテクスチャ(シャドウマップ)に保存します。
テクスチャは0 ~ 1 までの値しか保存できないので、範囲[0, maxDistance] を [0, 1] へ変換したものをテクスチャに保存します。

f:id:r-ngtm:20210126072044p:plain:w640
光源から見て最も近いオブジェクトまでの距離をシャドウマップへ保存

影の最大距離(maxDistance) はURP の Pipelineアセットの Shadows の部分から設定できます。

f:id:r-ngtm:20210126072552p:plain:w320
影のmaxDistance

シャドウマップを利用した影の判定

カメラで描画しようとしている点とライト間の距離D、シャドウマップに保存されている距離dを比較して、影かどうかを判定します。

f:id:r-ngtm:20210126072742p:plain:w640
シャドウマップの深度と描画点の深度を比較


ちなみに、MainLightのシャドウマッピングは Universal RP パッケージ内部の MainLightShadowCasterPass.cs にて実装されており、実装の内容を見ることができます。

FrameDebugger で シャドウマップを見てみる

以下のようなシーンを作成して、シャドウマップを見てみます。

f:id:r-ngtm:20210126080022p:plain
仮のシーン

DrawOpaqueObjects パスからシャドウマップを Ctrl + 左クリックすることで、シャドウマップを確認することができます。

f:id:r-ngtm:20210126075939p:plain
DrawOpaqueObjects

シャドウマップ

今回は以下のようなシャドウマップが確認できました。

f:id:r-ngtm:20210126080150p:plain
シャドウマップ

このシャドウマップは、カスケードシャドウマップになっています。

f:id:r-ngtm:20210126081532p:plain
シャドウマップの詳細
f:id:r-ngtm:20210126081731p:plain
カスケードの最大距離

おまけ : シャドウマップの実装場所

Universal RP の 中を見ると、シャドウマップを利用している処理を見ることができます。

Shadows.hlsl

Shadows.hlsl の中身を見ると、シャドウマップをサンプリングしている関数があります。

half MainLightRealtimeShadow(float4 shadowCoord)
{
#if !defined(MAIN_LIGHT_CALCULATE_SHADOWS)
    return 1.0h;
#endif

    ShadowSamplingData shadowSamplingData = GetMainLightShadowSamplingData();
    half4 shadowParams = GetMainLightShadowParams();
    return SampleShadowmap(TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), shadowCoord, shadowSamplingData, shadowParams, false);
}

Lighting.hlsl

上記のMainLightRealtimeShadow関数は、 Lighting.hlsl にて利用されています。

Light GetMainLight(float4 shadowCoord)
{
    Light light = GetMainLight();
    light.shadowAttenuation = MainLightRealtimeShadow(shadowCoord);
    return light;
}

上記のGetMainLight関数は URP の PBRシェーディングを行う関数 UniversalFragmentPBRの中で利用されています。(場所はLighting.hlslです)

    Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, shadowMask);

    #if defined(_SCREEN_SPACE_OCCLUSION)
        AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(inputData.normalizedScreenSpaceUV);
        mainLight.color *= aoFactor.directAmbientOcclusion;
        surfaceData.occlusion = min(surfaceData.occlusion, aoFactor.indirectAmbientOcclusion);
    #endif

    MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI);
    half3 color = GlobalIllumination(brdfData, brdfDataClearCoat, surfaceData.clearCoatMask,
                                     inputData.bakedGI, surfaceData.occlusion,
                                     inputData.normalWS, inputData.viewDirectionWS);
    color += LightingPhysicallyBased(brdfData, brdfDataClearCoat,
                                     mainLight,
                                     inputData.normalWS, inputData.viewDirectionWS,
                                     surfaceData.clearCoatMask, specularHighlightsOff);

【Unity】Linearワークフロー と Gammaワークフローについてまとめてみる

はじめに

UnityのLinearワークフローGammaワークフローについて、軽くまとめてみようと思います。

環境

Unity2020.2.0f1
Universal RP 10.2.2

問題

突然ですが、ここで問題です。
フラグメントシェーダーで 0.5 ( RGB = (128, 128, 128 ) ) という色を返した場合、
画面にはどんな色が表示されるでしょうか? (Linearワークフロー)

fixed4 frag (v2f i) : SV_Target
{
    return 0.5;
}

答え

RGB = (188, 188, 188) が表示されます。(明るく表示されます)


表示色が異なる理由はLinearワークフローにあります。

Linearワークフロー

通常のUnityでは、Linearワークフローが採用されており、以下のような計算を行います。

  • シェーダー内では色の計算をLinear空間で行う
  • 計算結果はガンマ補正してから画面に表示する

f:id:r-ngtm:20210120235736p:plain
色はガンマ補正してから画面に表示する

sRGBテクスチャが画面に表示されるまで (Linearワークフロー)

sRGBテクスチャが画面に表示される流れは以下のようになります。
1. sRGBテクスチャはサンプリング時にLinear色空間へ変換される (逆ガンマ補正)
2. シェーダーではLinear色空間で計算を行う
3. 色の計算結果はsRGB色空間へ変換してから画面に表示する(ガンマ補正)

f:id:r-ngtm:20210121000255p:plain
sRGBテクスチャがスクリーンに表示されるまで

sRGBテクスチャとLinearテクスチャの切り替え

テクスチャは、デフォルトではsRGBにチェックが入った状態になっています。
sRGBのチェックが入っていると、そのテクスチャはsRGB色空間にあるものとして扱われます。

f:id:r-ngtm:20210121003421p:plain
sRGBのチェックが入っていると、sRGB色空間として扱われる

テクスチャのsRGBのチェックを外すと、Linear色空間のテクスチャとして扱われるようになります。

f:id:r-ngtm:20210121002928p:plain
sRGBのチェックを外すとLinear色空間として扱われる

Linearテクスチャが画面に表示されるまで (Linearワークフロー)

Linear色空間のテクスチャは、テクスチャサンプリング時に逆ガンマ補正がかからなくなります。
画面に表示される際にはガンマ補正がかかります。
結果として、テクスチャは明るく表示されるようになります。

f:id:r-ngtm:20210121002814p:plain
非sRGBテクスチャが画面に表示されるまで(Linearワークフロー)

Linearワークフロー と Gammaワークフローの切り替え

UnityのPlayerSettingsには Color Space という設定項目があります。

ここがLinearの場合、UnityはLinearワークフローレンダリングを行うようになります。

f:id:r-ngtm:20210121001007p:plain
Linearワークフロー

Gammaだと、UnityはGammaワークフローレンダリングを行うようになります。。

f:id:r-ngtm:20210121000837p:plain
Gammaワークフロー

Gammaワークフロー

Gammaワークフローでは、色が画面にそのまま表示されるようになります。(Linearワークフローだとガンマ補正がかかっていました)

f:id:r-ngtm:20210121004008p:plain
Gammaワークフローでは色がそのまま表示される

テクスチャが画面に表示されるまで (Gammaワークフロー)

Gamma ワークフローでは、テクスチャの色はそのままシェーダーに取り込まれ、シェーダーの計算結果もそのまま画面に表示されます。

f:id:r-ngtm:20210121001336p:plain
Gammaワークフロー

まとめ

  1. Linearワークフローでは、ガンマ補正や逆ガンマ補正が入る
  2. Gammaワークフローでは、ガンマ補正や逆ガンマ補正は入らない

Linearワークフロー

f:id:r-ngtm:20210121000255p:plain:w500
Linearワークフロー

Gammaワークフロー

f:id:r-ngtm:20210121001336p:plain:w500
Gammaワークフロー

【Compute Shader メモ 2】中心差分法を利用してノーマルマップを作る

はじめに

Compute Shader で何かを作るシリーズ 第二弾!

今回は、Compute Shader で ノーマルマップ(法線マップ)を作ってみたいと思います。

f:id:r-ngtm:20210120000231p:plain:h240
実行結果 (1024x1024のハイトマップからノーマルマップを計算)

ノーマルマップ(法線マップ)とは

ノーマルマップとは、3Dモデルの法線情報を画像に保存したものです。

f:id:r-ngtm:20210119214419p:plain
ノーマルマップ

平面メッシュにノーマルマップを適用することで、あたかも凹凸が付いているように陰影をつけることができます。

f:id:r-ngtm:20210119214336p:plain
ノーマルマップを適用することで、凹凸が付いて見える

ノーマルマップによって陰影がつく原理

Unityは物体をレンダリングするときに、物体表面の法線を見て凹んでいるかどうかを判断しています。
(法線は黄色の線で表示しています)

f:id:r-ngtm:20210119221045p:plain:w480
凹凸のある面の法線はバラバラになる

ノーマルマップを使うと、物体の法線を再現することができ、陰影をつけることができます。

f:id:r-ngtm:20210119221358p:plain:w480
法線によって陰影がつく

Unityは物体をレンダリングするときに、物体の法線を見て凹んでいるかどうかを判断しています。
そのため、法線がバラつくとあたかもその面には凹凸が付いているかのように陰影が付くわけです。

Heightマップからノーマルマップを作る

今回、以下のような高低マップ(Heightマップ)から法線マップを作りたいと思います。

f:id:r-ngtm:20210119232127p:plain:w240
パーリンノイズ

以下のような法線マップを作ります。 (下記テクスチャはSubstance Designer で作ったノーマルマップになります)

f:id:r-ngtm:20210119232316p:plain:w240
ノーマルマップ

Heightマップからノーマルマップを求める方法

今回は中心差分法を利用して、Heightマップから勾配を求め、そこから法線を計算します。

f:id:r-ngtm:20210119211927p:plain:w320
法線ベクトルnは勾配の外積で計算

求めた法線情報はテクスチャ(ノーマルマップ)に保存します。



中心差分法で勾配を求める

ここで、連続な曲線 f(x) 上の接線(勾配)を求めることを考えます。

f:id:r-ngtm:20210119215214p:plain:w400
ある点での接線の傾きは、両隣の点の座標の差分で近似できる

ある点での接線の傾き(勾配)は、以下の式で近似できます。(中心差分法)
 \dfrac{ \partial f }{ \partial x } = \dfrac{ y_{i + 1} - y_{i - 1} }{ 2 \Delta x } = \dfrac{ f(x_{i + 1}) - f(x_{i - 1}) }{ 2 \Delta x }

参考 : http://www.icehap.chiba-u.jp/activity/SS2016/textbook/SS2016_miyoshi_FD.pdf



勾配ベクトルを求める

Hiehgtマップの座標(x,y)に保存されている高さ情報をf(x,y)とすると、以下のような図になります。

f:id:r-ngtm:20210119210754p:plain:w640
あるピクセルとその隣接ピクセルを可視化

中心差分法を利用すると二つの勾配ベクトル(\Delta x, 0, \Delta f_x ) , (0, \Delta y, \Delta f_y ) を求めることができます。

f:id:r-ngtm:20210119231430p:plain:w640
差分から勾配を求めることができる

外積を利用して法線を求める

法線ベクトル \vec{n} はこれら2つの勾配の外積を利用すると計算することができます。
 \vec{n} (- \Delta y \Delta f_x,  - \Delta x \Delta f_y,  \Delta x \Delta y ) に平行になります。

f:id:r-ngtm:20210119211927p:plain:w640
法線ベクトルnは外積で計算

 \Delta x = 2, \Delta y = 2と置いたとき、
 \vec{n} (- \Delta f_x,  - \Delta f_y,  2 ) に平行になります。

Compute Shader で実装

#pragma kernel CSMain

RWTexture2D<float4> Result; // ノーマルマップの書き込み先
Texture2D<float4> HeightMap; // Heightマップ

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{    
    // Heightマップの差分
    float dfx = (HeightMap[id.xy + uint2(1, 0)].r - HeightMap[id.xy + uint2(-1, 0)].r);
    float dfy = (HeightMap[id.xy + uint2(0, 1)].r - HeightMap[id.xy + uint2(0, -1)].r);
    
    // 法線
    float3 n = float3(-dfx, -dfy, 2.0);
    n = normalize(n);
    
    // 範囲[-a,a]を[0,1]に変換してからテクスチャに書き込む    
    float a = 0.02;
    n = (n / a + 1.0)  * 0.5;

    Result[id.xy] = float4(n, 1.0);
}

Compute Shader を実行する C#スクリプト

using UnityEngine;
using UnityEngine.UI;

public class ComputeNormal : MonoBehaviour
{
    [SerializeField] private ComputeShader shader; // 実行するComputeShader
    [SerializeField] private Texture _texture; // 入力画像
    [SerializeField] private RawImage targetImage; // RenderTextureを割り当てる対象のRawImage
    private RenderTexture _renderTexture;
    private int kernel;
    
    private void Start()
    {
        // RenderTexture作成
        _renderTexture = new RenderTexture(_texture.width, _texture.height, 0);
        _renderTexture.enableRandomWrite = true;
        _renderTexture.Create();
        
        // 実行したいカーネル(関数のようなもの)
        kernel = shader.FindKernel("CSMain"); 
        
        // ComputeShaderへデータを渡す
        shader.SetTexture(kernel, "HeightMap", _texture); // 元となるテクスチャ
        shader.SetTexture(kernel, "Result", _renderTexture); // 書き込み先のテクスチャ
        
        // スレッドグループサイズの取得
        uint sizeX, sizeY, sizeZ;
        shader.GetKernelThreadGroupSizes(kernel, out sizeX, out sizeY, out sizeZ);
        
        shader.Dispatch(kernel, _texture.width / (int)sizeX, _texture.height / (int)sizeY, 1 / (int)sizeZ);

        targetImage.texture = _renderTexture;
    }
}

実行結果

f:id:r-ngtm:20210120000231p:plain
実行結果 (1024x1024のハイトマップからノーマルマップを計算)