Unityが提供する「Universal Render Pipeline」「High Definition Render Pipeline」

大下岳志氏:みなさんこんにちは、Unityの大下です。この講演では、「表現で使いわける! UnityのURPとHDRP」と題して、「Unity」のレンダーパイプラインを選ぶ時の考え方、特にハイスペックな環境においてURPを選ぶメリットについてお話をしたいと思います。

具体的にはURPとHDRPの特徴の違い。Shader Graphを使って、URPのグラフィック表現を拡張する方法。それを使った新しい表現に挑戦するためのアプローチなどについて触れたいと思っています。途中2回ほど、プログラムのコードが登場しますが、これは同じものがダウンロードできるようにしているので、みなさんはコードを打たずに、この講演の内容を簡単に試してもらえると思います。

現在のUnityには、従来からみなさんにご愛用いただいている「Built-in Render Pipeline」と、2018バージョンから追加された「Scriptable Render Pipeline」という2つのレンダーパイプラインが搭載されています。

さらに、このScriptable Render Pipelineを使ったUniversal Render Pipeline(URP)と、High Definition Render Pipeline(HDRP)という2種類の構成済みパイプラインを提供しており、開発者は、Built-in、URP、HDRPの3種類の中から、プロジェクトの要件に合ったレンダーパイプラインを選択するというかたちになっています。

今回お話しする、URPとHDRPですが、HDRPは高品質、URPはモバイル向けと思われている方が多いかもしれません。確かに発表当初のアナウンスでは、HDRPは据え置きハード以上のスペックを前提としたレンダーパイプラインと紹介されていました。

また、URPも最初は、「Light Weight Render Pipeline」と名付けられており、HDRPと対をなす、軽量な動作に特化したものと位置付けられていました。しかしリリース後は、どちらも大枠での特徴は残しつつも、このスタンスは若干変わっています。

HDRPの特徴

現在のHDRPの特徴は、従来から大きく進歩した高精細な写実性です。HDRPでは、今日のゲーム開発で広く普及している物理ベースレンダリング、PBRを採用しています。しかし、一般的なPBRで用いられる物理ベースレンダリングによる質感の表現だけではなく、ライティングやカメラなどにも、現実世界をベースにしたルールや仕組みを取り入れ、環境全体でリアリティーを大幅に向上させています。

特にPLU(Physical Light units)、現実の光の単位をそのまま使えるライティングは、ほかに例のないHDRPの大きな特徴です。実在の光の単位を用いることで、従来手作業での調整が必須だったライティングが、より手軽で正確になりました。

このPLUは、実物のように動作する物理カメラと組み合わせることで、優れた効果を発揮します。HDRPでは、カメラの設定にも現実と同じ単位を使えるため、これらを組み合わせることで、CG独自のルールに悩まされず、現実での撮影に近いかたちで、リアルな映像を作り出すことができます。

また、オブジェクトの質感設定においても、標準のマテリアルであるLit Shaderは、Surface Typeを変更するだけで、金属やガラス、あるいはSub Surface Scatteringや、Iridescenceなど、多彩な質感の表現が可能です。

さらに、こうした物体の表面だけではなく、ボリュームレンダリングによる光の拡散や透過などによって空間を満たす大気の表現も簡単に追加できます。

これらの充実した機能を扱ううえで必要な手順が増えたり、物理法則への深い理解が必要になったりするなど、扱いが難しくなったところもあります。また、物理的な挙動の再現のために、個々の処理は複雑で負荷が高くなる傾向があり、動作に要求するスペックもURPより高くなっています。

HDRPの特徴や考え方については、昨年の「CEDEC2020」でもお話ししました。現在も「Unity Learning Materials」やCEDECのYouTubeチャンネルにアーカイブがあるので、ぜひご覧ください。

URPの特徴

一方のURPも、HDRPと同じく物理ベースレンダリングを採用しています。このように、ポストプロセスやShader Graphなどを使いこなせば、HDRPほどではないにせよ、据え置きハードでも遜色のないクオリティの絵作りが可能です。

しかし、各要素を細かく見ていくと、URPはHDRPと比べていろいろな箇所が簡素になっていることがわかります。Lit Shaderでの質感表現は、Metallic、Smoothnessで表現できる範囲に留まっており、Sub Surfaceや特殊な反射などには対応していません。

また、ライティングもHigh Dynamic Rangeには対応をしていますが、HDRPのような現実の光の単位ではなく、従来のCG的なパラメーターで設定を行うようになっています。

カメラも、物理カメラの項目はありますが、ほぼ画角についてのパラメーターとなっており、露出や被写界深度の調整には対応していません。

こうしたシンプルな作りは、処理負荷の軽さにもつながり、URPはモバイルを含めた多くのハードで動作しますが、HDRPと同じように写実的なグラフィックを目指す時に、やや見劣りしてしまうことは否めません。

しかし、これははじめに用意されたレンダーパイプラインの範疇でなにかを作ろうとした時の比較です。実は、URPの大きな特徴は、URPそのものの拡張性と、それによって得られる表現の幅にあります。

URPには、レンダーパイプラインを改造しなくても、開発者が独自の表現を入れ込める余地や拡張のための機能がいろいろと用意されており、それらを活用することで、アーティストであっても、このように写実性にとらわれないオリジナリティの高い表現を作ることができます。次は、その具体的な方法を紹介したいと思います。

Shader Graphを使ったURPのグラフィック表現

今回は、URPで独自の表現を作る手段として、Shader Graphを使います。Shader Graphは、ノードを使って視覚的にシェーダーを作成できるUnityのグラフエディタです。

Shader Graphに詳しい方は、Shader Graphで表現できる範囲をある程度ご存じかと思います。しかし今回は、Shader Graphの新しい機能を使って、従来難しかったシェーディングのスタイライズや、画面全体に効果を与えるポストエフェクトを作ってみたいと思います。

これを実現するものとして、「Custom Function」と「Renderer Feature」という機能を紹介します。また、これらの作業を効率的に進める手助けとなる「Sub Graph」にも触れたいと思います。

Shader Graphの機能その1「Custom Function」

それではさっそく、1つ目、シェーディングのカスタマイズから始めましょう。これには、Custom Functionという機能を使います。Custom Functionは、Shader Graphのノードの1つですが、ほかのノードのようにはじめから固定された機能を持っていません。

このノードは、直接Graph Inspectorにコードを書くか、外部のHLSLファイルをセットすることで、開発者自身が独自のノードを作ることができる機能です。望んだ処理をコードで書くので、シェーダーでできる大半のことは、ノードを使ってShader Graphに実装できるということになります。

特にShader Graphが苦手とする繰り返しの処理には有効で、Unity Japanの公式YouTubeチャンネルで公開している「Custom Functionで独自のノードを実装!」という動画でも、Custom Functionを使ったBoxFilterの作例が紹介されています。

Custom Functionは、非常に幅広い可能性を持つ機能ですが、一方で、機能をプログラムで書いている時点でアーティストにとってはやや敷居の高い機能です。しかし、ぜひお勧めしたい使い方があるので、今回はあえて紹介をしています。

実は、このCustom Functionを使って、URPのライトの情報を取得する方法が、Unityの公式ブログをはじめ、いろいろなところで公開されています。標準のShader Graphでは、カメラやサーフェイスの情報などは扱えるものの、ライトの情報を取得する手段がなかったので、オブジェクトの陰影、シェーディングをイチから設計することはできませんでした。

そのため、シェーディングには、URPの標準であるLitを使うか、Unlitを使って陰影をつけないかの2択しかありませんでしたが、ライトの情報が使えることによって、この選択が無限に広がります。

今回は、このようなHLSLを用意しました。数十行のコードですが、これでライティングに必要な情報が取得できるようになります。ファイルはこちらのリンクからダウンロードできるので、この先の工程は、ぜひこれを使って試してみてください。

「Custom Function」の設定

それでは、Custom Functionを実際に使ってみたいと思います。先ほどダウンロードしたファイルをプロジェクトのアセット以下に入れると、Custom FunctionのGraph Inspector、Sourceから、ファイルが選択できるようになります。

次に、1度テキストエディタなどでこのHLSLファイルを開き、最初のほうに書かれている関数名、精度を見つけ、それぞれGraph InspectorのNameとPrecisionに設定します。今回はNameがMainLight、PrecisionがHalfです。

続いて、ノードの入力と出力を設定します。これも同じくコードを見て、引数の中で指定がないものを入力、outがついているものを出力として、それぞれInputsとOutputsに同じ型を設定します。

入出力の名前は任意ですが、引数と同じものにそろえておいたほうが、混乱が少ないかもしれません。Inputsは、Vector 3のWorld Position(WorldPos)。Outputsは、Vector 3のDirectionとColor、FloatのDistanceAttenとShadowAttnです。AttnはAttenuationの省略で、減衰という意味です。

これで、Custom Functionの設定は完了です。エラーがなくなり、プレビューが表示されました。

シェーディングを作成する

このCustom Functionは、World Spaceのポジションを受け取り、その場所でのMainLightの向き、カラー、光の減衰、影の情報を出力する機能となっています。

ノードができたので、さっそくこれを使って、いくつかオリジナルの陰影表現を作ってみましょう。まずは、基本となるシェーディングを作るところから始めてみます。

Shader GraphはUnlitを使い、この見慣れたキャラクターにマテリアルをセットしています。Unlitの初期状態は、このように淡色の灰色です。

まず、先ほど作ったCustom Functionに、World Positionを接続します。次に、テクスチャを使うため、プロパティにTexture2D。「BaseColor」と「NormalMap」を追加します。今回はこの2種類のみを使います。

BaseColorのテクスチャをSampler Texture 2Dノードに接続して、それをMaster StackのBaseColorにつなぐと、テクスチャのカラーが表示できるようになります。まだこの時点では、陰影の計算を行っていないのでテクスチャの色だけがそのまま表示されています。

陰影を作成する

次は、いよいよ陰影を作ってみましょう。先ほどと同じように、NormalMapをSampler Texture 2Dにつなぎ、続いて、ジオメトリの法線を取得するNormal Vectorノードを作ります。この2つをNormal Blendノードで掛け合わせることで、ジオメトリにNormalMapの効果が反映されるようになります。

次に、ブレンドしたNormalとライトの向きをDot Productに接続します。Dot Productは、2つのベクトルの角度を受け取り、-1から1の値を返すノードです。2つのベクトルの向きが同じであれば1、ずれるほど少なくなり、直角で0、真逆で-1となります。

この値を明るさとして扱うと、ライトのほうを向いている面が明るく、90度を超えると真っ暗という、ごくシンプルなシェーディングの仕組みが作れます。ただ、0以下のマイナスの値は明るさとしては不自然なので、Saturateノードを使い、0から1以外の値を除外します。これを出力すると、このようにシンプルなシェーディングが作られます。NormalMapも反映されています。

次は、先ほどのBaseColorとシェーディングを合成しましょう。これには、Multiply、乗算を使います。「Photoshop」のレイヤーモードなどでもお馴染みですね。これで、ライトの向きによる陰影とBaseColorがブレンドされました。

カラー情報と落ち影の反映する

続いて、ライトのカラー情報も反映されるようにしましょう。これは、Multiplyでライトのカラーを先ほどの結果に乗算するだけです。これでライトカラーの影響も受けるようになりました。ライトカラーには輝度も含まれるので、少し明るくなっています。

さらに、Shadow Map、落ち影の影響も受けるようにします。これも、先ほどまでのものにShadow Attenuationを乗算するだけです。これで影が落ちるようになりました。

シェーディングと影の色を調整する

ここまでで、一通りの要素が入りました。しかし、陰影も影も真っ暗で、あまりかわいく見えません。次は、このシェーディングと影の色を調整できるようにしたいと思います。

プロパティにColorを2つ追加し、それぞれ名前を「AmbientColor」「ShadowColor」とします。まず、AmbientColorから取り出し、シェーディングのSaturateのあとにAddノードを使って加算します。シェーダーを保存すると、マテリアルのインスペクターにカラー情報が追加されます。

このAmbientColorによって、陰影の暗い箇所の色が変化するようになりました。同じ要領で、ShadowにShadowColorを加えることにより、影の色が変えられるようになります。

もっとリアルなシェーディングであれば、Light Probeなどを使って自然な明るさを求めますが、このようにデフォルメされたキャラクターにおいては、自由に色が調整できるというのもありだと思います。

シェーディングの表現そのものを変えてみる

次は、シェーディングの表現そのものを変えてみましょう。ネットワークは少しキレイに並べ直しました。1つ目は、明暗を少ない色数で塗り分ける、いわゆるトゥーン表現です。今回はとてもシンプルな方法を試してみます。

これには、Smoothstepノードを使います。Smoothstepノードは、入力された値を0と1に分けつつ、Edg1とEdge2の間をグラデーションにするノードです。この例では、Edg1とEdge2をそれぞれ0.2、0.21として、シェーディングとAddノードの間に追加します。

ざっくりしたものなので、かなり粗いですが、陰影が2階調になったのがわかるかと思います。

AmbientColorは明るめにしたほうがキレイになりそうです。もちろんライトの向きによって陰影も変化します。

陰影をソフトに表現する

続いてもう1つ、今度は陰影をソフトにする表現を作ってみたいと思います。これにもいくつか方法がありますが、手間の少ない方法を試してみます。効果をわかりやすくするため、Ambientの値を下げておきます。

こちらも使うノードは1つで、Remapノードというものです。これは、入力と出力にそれぞれの下限、上限を設定することで、入力された値を変化させます。この状態は-1から1の値を受け取り、出力時に0から1の範囲に置き換える設定になっています。

これを、Dot ProductのあとのSaturateノードと入れ替えます。Saturateは、マイナスの値をすべて0にしていましたが、Remapでは全体が0から1となります。その結果、全体の明るさが底上げされると同時に、暗くなる側にも明るさの階調ができるので、とても柔らかい印象になります。

これもトゥーン表現と同様、物理的な正しさとはまったく異なる表現です。ここまでで、いくつかシェーディングを作ってきましたが、個人的に一番最初のまったく陰影を加えていない、テクスチャを貼っただけの姿も見栄えがいいなと感じていました。

ライトの色と影だけをキャラクターに反映させる

3DCGでは、多くの作品が陰影のシェーディングを行っていますが、イラストやアニメなどでは、顔などに陰影をつけていない作品も多くあります。なので、陰影をつけないグラフィックもありだと思うのですが、その場合、周りのライティングが変化した場合に周囲から浮いてしまうことが問題になります。

そこで、これを両立する方法がないかと考えてみました。とは言っても、考えた方法はとても単純で、先ほどまでのシェーダーから表面の陰影を作る部分を削除してみただけです。

こうすると、オブジェクトの形状による陰影はなくなりますが、ライトのカラーと影の情報は活きているので、ライトの色と影だけがキャラクターに反映されるようになりました。

影の落ち方とテクスチャの白飛びを修正する

なかなかいい感じになりそうですが、Shadow Mapの影の落ち方があまりキレイでないのと、ライトの輝度が高い時にテクスチャが白飛びしてしまうのが気になります。

そこで、さらにもう一工夫加えます。まずShadowに、トゥーン表現で使ったSmoothstepノードを加えます。Edgは、0と0.1にしました。こうすると、影のエッジが鮮明になるのと同時に、うっすらと乗っていた曖昧な影が消えます。

次は、高輝度ライティングへの対応です。こちらは、ライトのColorにSaturateを加え、受け取る側が1以上の明るさにならないようにします。

これによって、高輝度のライトが当たっても白飛びせず、ライトの色と暗さには反応するという表現ができるようになりました。

いかがでしょうか。このようなイラスト的な表現の場合、落ち影とライトカラーの影響があれば、形状の陰影がなくても十分周囲と馴染ませることができそうです。むしろ、もともと想定した色に近いイメージで、キャラクターを見せられる可能性もあると思います。

描画するものすべてを作者がコントロールできるようになる

今回試した、シェーディングの中の特定の要素だけを使ったり、改変したりするというのは、物理ベースの考え方からは、程遠いものです。しかし、目指す表現が写真ではなく、絵画、イラスト寄りのものと考えると、むしろ描画するものすべてを作者がコントロールするほうが自然ではないかと思います。そして、これを実現できることこそが、カスタムシェーディングの醍醐味です。

もちろんShader Graphでは、今回のようにシンプルなものだけではなく、複雑な物理ベースシェーダーなども作れるので、表現の幅は無限大と言えるでしょう。

URP標準のライティングの影響によりシェーディングの結果が変わることがある

最後に1つ補足です。シェーダーグラフでこのような独自のシェーディングを作る場合、Unlit Graphで直接カラーとして表示するか、Lit Graphで各パラメーターの機能を止めたうえで、Emissionとして表示するという2種類の出力方法が考えられます。

多くの場合は、どちらでもほぼ同じような結果が得られますが、実際には、URP標準のライティングの影響を受けているかそうでないかという違いは大きく、思わぬところで結果が変わることもあります。

この例では、Litを使った場合、オブジェクトに正しくピントが合いますが、Unlitを使うと背景と同じようにぼけてしまいます。これは、Unlitが深度情報の計算から除外されてしまうことで起こる現象です。この場合、Litのほうがほかの描画機能と合わせやすいとも言えますが、逆に、URP標準の描画に束縛されていると捉えることもできるので、どちらがよいかを一様に決められません。

このあたりは、URP標準の機能をどのぐらい活用するかによって選択が変わってくると思うので、いろいろ試すのがよいと思います。

(次回へつづく)