リソースの取得
Shader Canvas は私が独自に開発した軽量なオープンソースの Shader エディタで、Metal Shading Language を使用しています。MSL は Apple プラットフォーム専用の shader 言語であるため、Shader Canvas は Windows や Linux システムでは動作しません。あらかじめご了承ください。
通常、Shader 関連の知識を学ぶ場合、Unity、Unreal、あるいは Shader Toy などのプラットフォームが非常に良い選択肢になります。しかし、これらは機能が充実しすぎているため、ShaderLab の構文をどう学ぶか、リソースをどうバインドするかなど、Shader そのもの以外の部分に気を取られがちです。そのため、可能な限りシンプルな primitive から出発し、パラメータ化されたデザインそのものとは無関係な要素を極力減らせるように、このような小型 engine を作成しました。
Shader Canvas に対する修正提案は大歓迎です!いつでも GitHub からブランチを Fork して、あなたのワークフローにより適した Shader エディタを作成することができます。ただし、これは私個人のアドバイスですが、このエディタの機能は可能な限りシンプルに保つことをお勧めします。
rendering pipeline
もしあなたが Shader 開発の初心者で、rendering pipeline という概念すら聞いたことがないほどであれば、まずはコンピュータグラフィックス(特にリアルタイム rendering)関連の知識を体系的に学ぶことを強くお勧めします。そうすることで、shader が機能する仕組み全体を効果的に理解できるようになります。
すでに Shader に精通しているなら、shader を開発する際に頭の中でどのような思考を構築すべきかについて、さっそく話を進めましょう。
shader ステージ、あるいはデータフロー
どのようなグラフィックス API を使用するにしても、rendering pipeline には必ず存在するいくつかのステージがあります。私たちが直接 GPU とやり取りすることは稀で、多くの場合、アーティスト向け Shader を開発するニーズはゲーム engine やエフェクト engine から生じます。
個人的には、データフロー(Data Flow)という表現がとても気に入っています。考えてみてください。マテリアルを作成するにしても、ポストプロセスエフェクトを作成するにしても、私たちは常に何らかの primitive に対して操作を行います。これらの primitive の生データ(vertex 座標、法線、texture マッピングなど)は、いずれにせよアプリケーションから提供されます。これらのデータが最終的に画面へと流れていくことで、私たちは様々な視覚効果を目にすることができるのです。
グラフィックスの授業では、着色モデルの公式をいくつか暗記させられます。 や Blinn-Phong、あるいはさらに高度な rendering 方程式などです。しかし、いずれにせよ、最もシンプルな Lambert Diffuse を例にとっても、 のデータが空から降ってくるわけではありません。 は法線と呼ばれ、これは primitive 自身の属性です。 はライトベクトルと呼ばれ、シーン内のライトの位置とオブジェクトの位置の間のベクトルであり、これは primitive 自身の属性であるだけでなく、シーンの属性でもあります。さらに言えば、もし と が同じ座標空間に存在すらしていなければ、内積をとることは全くの無意味です。
そういえば、どの面接官も内積とは何かと尋ねてきます。この質問に答えるときは、面接官を5歳の子供か80歳のおばあちゃんだと思って説明するように注意してください。
もしあなたが Shader や rendering pipeline に本当に不慣れだとしても、これらのデータが何もないところから現れるわけではないことだけは知っておく必要があります。色を計算する際に法線が必要なら、法線の情報を提供してくれる何かが必ず必要です。その次に考えるべきなのが、「この法線の情報はどこから来たのか?」ということです。
モデル(あるいはアプリケーション、あるいは fbx/obj/usd ファイル)が提供できるのは、常にそれ自身のローカル空間の情報だけです。そのため、この空間はモデル空間とも呼ばれます。ライティングの計算にワールド空間の法線が必要な場合、変換が必要になります。この変換自体のロジックには馴染みがあるはずですが、モデルデータの座標系変換自体は通常 vertex shader で行われます。その後、私たちが着色する対象はピクセルであり、すべてのピクセル上に vertex があるわけではないため、データ量を から へと拡大する rasterization (ラスタライザ)が必要になります。ここでの は明らかに画面の幅と高さです。その後、各ピクセル上に「架空の」 vertex が配置され、そこで初めて pixel shader あるいは fragment shader を使って色を rendering します。
Shader Canvas におけるデータフロー
Shader Canvas の左側には、Data Flow という名前のパネルを用意しています。
このパネルは、Shader でどのデータを使用するかを決定するのに役立ちます。ただし、これは最も基本的なバージョンの Shader Canvas であり、実際には接線や従法線など、より多くのデータをサポートするように拡張することが完全に可能です。しかし、よりシンプルなエフェクトがもたらす可能性にできるだけ焦点を当てるため、私のデータフローでは、法線、uv、位置、時間、および視線ベクトルといったデータのみを渡すことにしました。
シンプルなエフェクト
通常、最もシンプルなエフェクトといえば、おそらくリムライト(Rim Light)ではないかと思います。
リムライト自体は vertex の変化を一切必要とせず、 を通じてエッジを「取得」することを理解するだけで十分です。
Shader Canvas では、Layer に Shader を一切追加しなくても、基本的な Lambert Shading が適用され、ピクセルも正常にスクリーン空間にマッピングされるため、心配はいりません。vertex の位置を変更する必要がないのであれば、Vertex Shader を追加する必要は全くありません。

それでは、基本的な Fragment Shader を追加してみましょう。クリックして、デフォルトで提供されている Fragment Shader をクリアします。

もし MSL に不慣れなら、ちょうどこのテンプレートを見てみてください。そしてお分かりのように、サイドパネルに fragment shader を書くだけでよいのです!その他のコードを補足する必要はありません。
MSLfragment float4 fragment_main(VertexOut in [[stage_in]]) {
return float4(1.0, 1.0, 1.0, 1.0);
}
ここで、fragment はこの関数が fragment shader のエントリーポイントであることを示すキーワードです。[[stage_in]] は in が補間された入力であることを示します。特筆すべき点として、実際のところ戻り値の型は float4 である必要はなく、Apple は色を表現するのに half4 を使用することを推奨しているようです。ただし、half と float を少し区別しておく必要があることと、MSL では様々な場所で暗黙の型変換がサポートされていない可能性が高いことに注意してください。
数学的原理
数学的原理において、Rim Light の本質はフレネル効果であり、フレネル効果の数学公式は以下の通りです。
ここで:
- :通常は (法線ベクトルと視線ベクトルの内積)を用いて計算します。
- :垂直入射(つまり視角が表面に対して垂直、)時の物質の反射率です。
- :標準的な Schlick モデルではこの累乗は通常 5 に固定されますが、アート的な効果を狙って、リムライトの範囲を制御するために変数 に変更されることもあります。
比較的読みやすい Schlick の近似公式は以下のようになります。
ここで、アーティストにとって理解しやすい表現をすると、 は輝度係数、 は透明度の閾値です。
実装
公式を書き出すだけで、コードはすでに非常に明確になります。
MSLfragment float4 fragment_main(VertexOut in [[stage_in]]) {
float nDotV = dot(normalize(in.normalWS), normalize(in.viewDirWS));
float rTheta = 1.0 * pow((1 - nDotV), 5.0) + 0.2;
return float4(rTheta);
}
確かに、なかなか良い効果が確認できます。

これで十分でしょうか?いいえ、十分ではありません。Technical Artist にとってパラメータの柔軟性は非常に重要であり、1.0 や 0.2 のような magic number をコードに書き込むことは絶対に避けたいはずです。Shader Canvas は非常に便利なパラメータ設定方法を提供しています。左側の Parameters に新しいパラメータを追加することができます。例えば、Bias という名前の slider を追加してみましょう。
これにより、fragment shader の上部に自動的に一行のコメントが追加されます。
MSL//@param _Bias float 0.5 0.0 1.0
このような構文によって、数値の型、現在の値、最小値、最大値が定義されます。自分で他のパラメータを追加して効果を確認することもできます。例えば、私は以下のようなパラメータを追加しました。

MSL// @param _RimScale float 0.0
// @param _RimPower float 0.0
// @param _RimLight color 0.0 0.748 0.993
// @param _Bias float 0.5 0.0 1.0
fragment float4 fragment_main(VertexOut in [[stage_in]]) {
float nDotV = dot(normalize(in.normalWS), normalize(in.viewDirWS));
float rTheta = _RimScale * pow((1 - nDotV), _RimPower) + _Bias;
return float4(float3(rTheta) * _RimLight, rTheta);
}
技術的な側面から見ると、数値の範囲についても考慮する必要があるかもしれません。例えば、 が負の値になる可能性はあるでしょうか?十分にあり得ることなので、クランプ(clamp)処理を行っておくべきです。
MSLfloat nDotV = saturate(dot(normalize(in.normalWS), normalize(in.viewDirWS)));
まずは簡単なポストプロセスを追加する
これは単に、Shader Canvas がいくつかの基本的なポストプロセスエフェクトを提供できることを知ってもらうためです。もちろん、ポストプロセスも本来はあなたのタスクであるべきです!あなたは視覚効果全体の前半、中盤、そして後半のすべてに責任を持つべきです。フルスクリーン関連の内容については、今後別の Blog で取り上げることにしましょう。
今のところは、PP ボタンをクリックして Fullscreen Layer を追加するだけで十分です。
このエフェクト、なかなかクールだと思いませんか?Fullscreen Layer 1 を開いてください(Bloom にリネームしても構いません)。以下のいくつかの設定に従うだけで、デフォルトの Bloom エフェクトを使用できるようになります。

あ、背景画像は Shader Canvas の右下で設定できますよ!
え?この Bloom はパフォーマンスがひどすぎるし、品質も効果も悪すぎるって?問題ありません。あなたならきっと Shader Canvas を使ってもっと良いものを書けるはずだと信じています!
余談
もし Shader、特に MSL に非常に不慣れな場合は、Shader Canvas の Tutorial 機能を試してみてください。この機能は、MSL を素早く習得するのに役立つはずです。ぜひ試してみてください。

