【ふりかえり】CyberAgent Graphics Academy に参加しました
- はじめに
- アカデミーの流れ
- アカデミー開始以前
- 講義について
- 講義の流れ
- 最終課題デモ
- 懇親会 (2回)
- 井戸端
- 制作物発表会 (最終課題)
- アカデミーで得たもの
- アカデミーを終えてみて
- CyberAgentさんについて
はじめに
CyberAgentさんが主催する、Graphic Academy に参加させていただきました。
今回は、ふりかえりの記事を書きたいと思います。
アカデミーの流れ
アカデミーの流れをざっくりまとめると、以下のような流れでした。
- 書類選考
- 面接
- 講義 (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あたりで、残りの時間は課題のレポート作成に時間を使う感じでした。
- 受講生20名がZoom上に集まり、講師(石井さん)の講義を受けます。
- 毎週課題があり、次の講義までに提出する必要があります
- 課題の形式はレポート形式で、自由なフォーマットで書くことができました。
- 課題の形式はレポート形式で、自由なフォーマットで書くことができました。
受講生の方々の熱意が高く、欠席者はいませんでした (体調不良で欠席する人が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という組織について
- Cyber Agentの方が場を取り仕切ってくれたので、気持ちよく時間をすごせました。
井戸端
- 受講生(希望者)が毎週木曜日・日曜日の22:00~23:00にあつまり、課題のわからないところを教えあうという取り組み(井戸端)がありました。
- 井戸端は受講生の一人(丸山 純さん) のアイデアから始まった取り組みになります。
制作物発表会 (最終課題)
受講生の方々が、絵作りをテーマにして作品発表を行いました。
テーマはさまざまでした。
- キャラクター表現をテーマにした作品
- SFをテーマにした作品
- ブラウン管シェーダー
- 水槽をテーマにした作品
- ハッチングを利用した手描き調の表現
- 草原をテーマにした作品
- 雪原をテーマにした作品
- 光の使い方にこだわったライブステージ
- ニーアっぽい絵作り
- 海をテーマにした作品
- アメコミをテーマにした作品
などなど
人によって目指す方向性が違うのが面白かったです。
- 汎用性を意識している方
- パフォーマンスを意識した方
- 技術的な興味関心が高い方
- ルックをリッチにすることを意識した方
作品
私は 「リアル調と手描き調の両立」をテーマにした作品を制作しました。
ポストエフェクトで手書きっぽい感じに加工しています。
アセットは、CyberAgentゲーム事業部から提供いただいた素材をお借りしています。
(BLADE XLORD のアセットになります)
制作の流れは以下のような感じです。
- 火山のアセットを並べて、URP PostProcessingで加工
- 陽炎エフェクトを作成し、高熱を表現
- 地面マテリアルを調整し、キャラクターを目立たせる
- 距離・高さに依存するフォグの作成
- 深度に応じて彩度を落とすエフェクトの作成
- 近景を赤・遠景を青に寄せる (色彩遠近法)
- 深度を利用したアウトライン描画キャラだけを専用テクスチャに描画した後、アウトライン抽出
- Sobelフィルタで水彩画調に加工
作品をOSSとして公開している方もいました。(yunodaさん)
Unityで使えるブラウン管シェーダーをオープンソースとして公開しました。
— yunoda@Blender/Unity/Roblox (@yunoda_3DCG) 2022年4月23日
CyberAgentさんが主催するGraphicsAcademyの最終制作物です。詳しい実装については、後日ブログにまとめようと思います。
※VRChatは未対応です、ごめんね。#Unity #CyberAgent #hlsl #gamedev https://t.co/FrFtgGmlvu
アカデミーで得たもの
グラフィックスの根幹にあるものへの理解が深まった
- カメラ変換周り・ノーマルマッピング・影の描画・ライティングあたりの話は理解が浅かったのでとても勉強になりました
- グラフィックスに関する疑問を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 に入っているカラーピッカーを使うと、画面の色を数値で見ることができ、シェーダーをデバッグする際に役に立ちます。
アカデミーを終えてみて
- グラフィックスを学ぶ仲間がいたので最後までモチベーションを保ったままグラフィックスを学ぶことができた
- 独学でグラフィックスを学んでいた時は、一緒に学ぶ仲間がいなかったり、発表の機会が無かったので挫折しやすかった
- 独学でグラフィックスを学んでいた時は、一緒に学ぶ仲間がいなかったり、発表の機会が無かったので挫折しやすかった
- 毎週課題を提出する必要もあったので、緊張感を持って取り組むことができた
- 作品を発表するという場もあったので、そこを目標として絵作りに取り組むこともできた
- 作品に納得がいっていない部分があるので、アカデミーが終わった後もクオリティアップを続けていきたい。
CyberAgentさんについて
- 費用を投じて、他社のエンジニアにノウハウを教えるというのはすごいと思いました。
- OSS活動も行っていたり、CA.unityを開催するなど、技術投資や業界貢献に力を入れている会社さんだと感じました
- OSS活動も行っていたり、CA.unityを開催するなど、技術投資や業界貢献に力を入れている会社さんだと感じました
- 優秀な方々が多く集まっているという印象を受けました
【技術書】シェーダーグラフの書籍をリリースしました【ShaderGraph CookBook vol.1】
はじめに
2021/03/21 に Unity ShaderGraphの本をリリースしました。
今回は、この本の魅力を紹介したいと思います。
zenn.dev
本書の特徴
- 20種以上のノードの解説
- 30種以上の作例
- サンプルプロジェクト付き
特徴① : ノードの解説
ノードについての解説や、ノードで作れる表現の作例を紹介しています。
サンプルページ
🍎 Sineノード|Unity ShaderGraph CookBook vol.1【ShaderGraph 入門】
特徴② : 豊富な作例
書籍では、30種以上の作例を紹介しています。
ShaderGraph単体で完結するような表現に焦点を絞っています。
作例を知りたい方は以下のページをご覧ください。
🍍 レシピ一覧|Unity ShaderGraph CookBook vol.1【ShaderGraph 入門】
特徴③ : サンプルプロジェクト
本書の作例の一部は、GitHubにて公開しています。 github.com
環境
Unity2020.3.0f1
Universal RP 10.3.2
【Houdini + Unity】点光源の影でグリッドを作って遊ぶ
はじめに
Unityのポイントライトの影でグリッドを出してみました。
今回はこの3Dモデルの作り方を紹介します。
作り方
グリッドを球面へ投影したようなモデルを作ります。
このモデルと点光源が地面に映し出す影はグリッドの形になります。
投影点の座標の求め方
線分PAの長さをD, 線分PBの長さをd、球の半径をRとおいたとき、以下のようになります。
dが分かれば、投影点Bの座標が分かります。
長さdの導出
三角形 HPA と 三角形BPHは相似なので、以下が成り立ちます。
投影点Bの座標は以下のようになります。
Houdiniで実装する
投影点Bの座標が求まったので、次にこれをHoudiniで実装します。
Grid
Gridノードを使ってグリッドを作成します。
PolyExtrude
PolyExtrudeノードを使ってグリッドに穴をあけます。
Attribute Wrangle
投影点の座標を計算して、グリッド点を球へ投影します。
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へ取り込み、点光源を置くと、影がグリッドになります。
点光源の位置
モデルの位置
関連書籍
【シェーダーグラフメモ その58】XOR演算を利用した市松模様
はじめに
XOR演算を利用して、市松模様を作る方法を紹介します。
今回はワールド座標のX,Z成分から市松模様を作ります。
目次
環境
Unity2020.2.0f1
Universal RP 10.2.2
市松模様の作り方
二つのシマシマに関してXORを適用すると、市松模様が作れます
XORについて
xor(a,b)は以下のような値を取ります。
a | b | xor(a,b) |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
シェーダーグラフ実装
シマシマを作る
以下のようなシェーダーグラフを組みます。
結果
これを板ポリゴンに貼り付けると、以下のようになります。
色のX成分、Z成分はそれぞれ以下のようになっています。
ShaderGraphにXOR演算は無い
ShaderGraphにはXOR演算は用意されておらず、自分で作る必要があります。 今回はXORの実装方法を3つ紹介します。
方法1 : Abs を利用したXOR
1つ目の方法はAbsを利用したXORの実装です。
abs(a-b)
X成分(横シマ)からZ成分(縦シマ)を減算すると、以下のようになります。
減算結果にAbs(絶対値)を取ると、0,1が交互に並びます。
シェーダーグラフで実装すると以下のようになります。
方法2 : X・(1 - Z) + (1 - X)・Z
以下の計算式でXORを作ることができます。
x * (1 - z) + (1 - x) * z
方法3 : Moduloを利用したXOR
3つ目の方法は、Moduloを使ったXORの実装です
fmod(a+b, 2.0)
X成分とZ成分を足し合わせると、以下のようになります。
2で割った余りを計算すると0と1が交互に並びます。
シェーダーグラフで実装すると、以下のようになります。
結果
【Unity】シャドウマッピングについて
はじめに
Unityのシャドウマッピングについて、調べてみたものをまとめてみようと思います。
環境
Unity2020.2.0f1
Universal RP 10.2.2
シーンに光源を置いてみる
シーンに緑色のDirectional Lightを置くと、以下のようになります。

光が当たった領域は緑色になりますが、影の領域には色を与えません ( = 真っ黒になります)
シャドウマップ
Unityでは、「ある領域が影になっているかどうか」の判定にシャドウマップを利用します。

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

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

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

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

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

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

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


おまけ : シャドウマップの実装場所
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空間で行う
- 計算結果はガンマ補正してから画面に表示する
sRGBテクスチャが画面に表示されるまで (Linearワークフロー)
sRGBテクスチャが画面に表示される流れは以下のようになります。
1. sRGBテクスチャはサンプリング時にLinear色空間へ変換される (逆ガンマ補正)
2. シェーダーではLinear色空間で計算を行う
3. 色の計算結果はsRGB色空間へ変換してから画面に表示する(ガンマ補正)

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

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

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

Linearワークフロー と Gammaワークフローの切り替え
UnityのPlayerSettingsには Color Space という設定項目があります。
ここがLinearの場合、UnityはLinearワークフローでレンダリングを行うようになります。

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

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

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

まとめ
- Linearワークフローでは、ガンマ補正や逆ガンマ補正が入る
- Gammaワークフローでは、ガンマ補正や逆ガンマ補正は入らない
Linearワークフロー

Gammaワークフロー

【Compute Shader メモ 2】中心差分法を利用してノーマルマップを作る
はじめに
Compute Shader で何かを作るシリーズ 第二弾!
今回は、Compute Shader で ノーマルマップ(法線マップ)を作ってみたいと思います。

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

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

ノーマルマップによって陰影がつく原理
Unityは物体をレンダリングするときに、物体表面の法線を見て凹んでいるかどうかを判断しています。
(法線は黄色の線で表示しています)

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

Unityは物体をレンダリングするときに、物体の法線を見て凹んでいるかどうかを判断しています。
そのため、法線がバラつくとあたかもその面には凹凸が付いているかのように陰影が付くわけです。
Heightマップからノーマルマップを作る
今回、以下のような高低マップ(Heightマップ)から法線マップを作りたいと思います。

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

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

求めた法線情報はテクスチャ(ノーマルマップ)に保存します。
中心差分法で勾配を求める
ここで、連続な曲線 f(x) 上の接線(勾配)を求めることを考えます。

ある点での接線の傾き(勾配)は、以下の式で近似できます。(中心差分法)
参考 : http://www.icehap.chiba-u.jp/activity/SS2016/textbook/SS2016_miyoshi_FD.pdf
勾配ベクトルを求める
Hiehgtマップの座標(x,y)に保存されている高さ情報をf(x,y)とすると、以下のような図になります。

中心差分法を利用すると二つの勾配ベクトル ,
を求めることができます。

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; } }
実行結果
