URPのサンプルで「Missing URP Asset」と出るとき

エラーのスクショ画面

こんなのが出るときの対応策です

The URP Package samples require a specific pipeline asset to be assigned to the Quality Level in use.

Please assign "PackageSamplesURPAsset" to the Pipeline Asset property in Edit/Project Settings under the Quality tab

というエラーメッセージが出てきます。

指示通りProject SettingsのQuality Tabを開いて

Add Quality Levelを追加。

Render Pipeline Assetの項目があるので、URP Package Samples/SharedAssets/SettingsにあるPackage Samples URP Assetをドラッグアンドドロップ

これで動作します

「Writing Shader Code in Universal RP (v2)」の試訳

www.cyanilux.com

というチュートリアルがある。これはUnityのURP関連のeBookでおススメされていた記事で、URPでのシェーダーの書き方がよくわかっていなかったのでこれを読もうと思った。せっかくなので訳しながら勉強したほうが為になるかなと思い訳した。せっかく文章もあるので、インターネットに放流しようと思う。

ちなみに私はシェーダーをちゃんと書いたことはないので間違っていても悪しからず。

URPでシェーダーを書く(v2)

Intro

 オンラインにあるシェーダーのチュートリアルはUnityのビルトインレンダーパイプライン用で、URPで動作しないかもしれません。そのせいでピンクになるかもしれないし、少なくともSRP Batcherに対応していないためにパフォーマンスの問題を生じます。SRP Batcherは各シェーダー/マテリアルドローコール中のセットアップをバッチ化する機能で、SRP Batcherに対応したシェーダーはたくさんのオブジェクトを描画するときに効率的に描画できます。

 SRP Batcherにシェーダーが対応しているかを確認する場合は、ProjectWindowsからシェーダーを選択してInspectorをみると確認できます。そしてオブジェクトが正しくバッチ化されているかはFrameDebugger Windowを利用して確認できます。

 もし対応していないのなら書き直すべきです。サーフェスシェーダー添付レーターを使ったすべてのシェーダーはURPと非互換であり、頂点/フラグメントをつかったものに書き換える必要があります。ShaderGraphで簡単に書き換えることができそうですが、すべての機能に対応しているわけではないので、代わりにコードを書いて書き換える状況もあります

 もしシェーダーについてよくわかっていないなら、「Intro to Shaders」記事を先に読んでください。そして初学者なら簡単なので、ShaderGraphを使いましょう。「Intro to Shader Graph」という記事も用意しています。

 Built-in Render Pipelineに慣れているなら最後のセクションまで読み飛ばしてもらって構いません


Sections:

  • ShaderLab
    • Properties
    • SubShader
      • Render Pipeline
      • Queue
    • Pass
      • LightMode Tag
      • Cull
      • Depth Test/Write (ZTest, ZWrite & Offset)
      • Blend & Transparency (Blend, BlendOp)
    • Multi-Pass
  • HLSL
    • HLSLPROGRAM & HLSLINCLUDE
    • Variables
      • Scalar
      • Vector
      • Texture Objects
      • Matrix
      • Array
      • Buffer
    • Functions
    • UnityPerMaterial CBUFFER
    • Structs
      • Attributes (VertexInput)
      • Varyings (FragmentInput)
      • FragmentOutput
    • Vertex Shader
    • Fragment Shader
    • Keywords & Shader Variants
      • Multi Compile
      • Shader Feature
      • Shader Variants
      • Keyword Limits
    • Lighting Introduction
      • Surface Data & Input Data
      • InitializeInputData
      • Simple Lighting
      • PBR Lighting
    • Other Passes
      • ShadowCaster
      • DepthOnly
      • DepthNormals
      • Meta
  • Summary of Built-in vs URP differences
    • ShaderLab
    • HLSL
    • Keywords
    • Common Functions/Macros
  • Templates

ShaderLab

 Unityのシェーダーファイルは二種類の言語で書かれています。Unity固有のShaderLabはシェーダーのプロパティ、サブシェーダーとパスについて記述します、一方実際のシェーダーコードはHLSL(High Level Shading Language)で書きます。

 ShaderLabの文法はBuilt-in pipelineの時とそれほど変わってません。Unityはいくつかの資料を提供しています。もしShaderLabに明るくないのであれば「Render Pipeline」「LightMode Tag」「Multi Pass」のセクションを読みましょう。

 すべてのシェーダーはShaderブロックから始まっていて、パスと名前が含まれています。この名前はインスペクタウィンドウでマテリアルのシェーダーを変更するときにドロップダウンでどのように表示されるかを決定します。

Shader "Custom/UnlitShaderExample" {
    ...
}

 Propertiesブロックや様々なSubshaderブロックもこの中に記述します


Propeties

 propertiesブロックはマテリアルのインスペクタに公開する必要のある値を公開するために使います。この値によって同じシェーダーでも異なるテクスチャや色を利用することができます。

Properties {
//  [name] ("[name in inspector]", [type]) = [default value]
    _BaseMap ("Base Texture", 2D) = "white" {}
    _BaseColor ("Base Colour", Color) = (0, 0.66, 0.73, 1)
//  _ExampleDir ("Example Vector", Vector) = (0, 1, 0, 0)
//  _ExampleFloat ("Example Float (Vector1)", Float) = 0.5
}

 このプロパティはC#スクリプトからでも変更できます(例 material.SetColor / SetFloat/ SetVector)。マテリアルごとにプロパティが違う場合は、それらをプロパティブロックだけでなくUnityPerMaterial CBufferにも含める必要があります。なぜならそうしないとSRP Batcherが正しく有効にならないからです。このことについては後述します

 すべてのシェーダーが同じ値を共有するときはそれらを公開する必要がありません。代わりにHLSLコードの中で定義するだけです。そしてそれをC#スクリプトから shader,SetGlobalColor / SetGlobalFloat / SetGlobalVector などの関数から設定できます

 C#スクリプトでのプロパティの設定に関する詳細は「Intro to Shader Graph」を参照してください


SubShader

 Shaderブロックは複数のSubShadersを含んでいます。UnityはGPUでサポートされている最初のSubShaderブロックを利用します。次のセクションで説明しますが、RenderPipeline Tagを利用することでそのパイプラインで使用されるべきでないSubShaderが選ばれないようにすることができ、一つのシェーダーにそれぞれのパイプラインに対応した複数のバージョンを持たせることができます。

 SubShaderが対応していないときにFallBackを定義できます。もしfallbackがない場合はピンクエラーシェーダーになります

Shader "Custom/UnlitShaderExample" {
    Properties { ... }
    SubShader { ... }
    FallBack "Path/Name"
}

 後でHLSLを含む各SubShaderにパスを定義します。この中で特定のShader Compile Targetを指定できます。より高いターゲットはより多くのGPU機能をサポートしますが、それはすべてのプラットフォームでサポートされるとは限りません。

 v10以前ではURPでは次のようなものをすべてのパスに使用していました

// Required to compile gles 2.0 with standard SRP library
// All shaders must be compiled with HLSLcc and currently only gles is not using HLSLcc by default
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma target 2.0

 例としてはURP/Lit shader (v8.3.1).があります。

 v10以降は遅延サポートが追加されてきています。この例では二つのSubShaderを代わりに利用するシェーダーが出てきました。一つ目のシェーダーは各パスにこれが適用されます

#pragma exclude_renderers gles gles3 glcore
#pragma target 4.5

 基本的には「OpenGL以外のすべてのプラットフォームで使用する」という意味です。2番目のSubShaderは以下のように適用します

#pragma only_renderers gles gles3 glcore d3d11
#pragma target 2.0

 私の知る限り、両方のSubShaderは基本的に同じです。違うのはターゲットが異なること、二つ目のSubShaderではUniversalGBufferパスを除外して、deferred renderingに用いられることです。おそらくこれは現時点ではこれらのプラットフォームでサポートされてないからです。この投稿/チュートリアルではこのターゲットのものを含めていませんが、deferredとOpenGLプラットフォームに対応する場合はこのように二つに分割するのは重要かもしれません。

 deferredパスはURPで正式リリースしていないので私はまだ使っていません。後で投稿を更新します(約束はできません!)

Render Pipeline

 RenderPipelineタグは現在使用されているレンダリングパイプライン用でない限りSubShaderが使用されないようにします。このタグはScriptable Render Pipelineを使うときにShader.globalRenderPipelineの値に対応するときに利用されます。

 この値は "UniversalPipeline"(もしくは昔の"LightweightPipeline")と"HDRenderPipeline"に設定できます。試していませんが異なる値を用いるということはCustomRenderPipelineを定義しShader.globalRenderPipelineで文字列を指定しない限り無視されるということになります。

 タグを完全に除去すると、すべてのパイプラインで使用できるようになります。タグがから文字列の時の動作は不明ですが同じかもしれません。Built-In RPにはRenderPipelineタグの値がないので、もしBuilt-In RPに対応する場合は最後のSubShaderにRenderPipelineタグを空にしておくとよいです。考え方としてはFallbackと同じです。例えば

Shader "Custom/UnlitShaderExample" {
    Properties { ... }
    SubShader {
        Tags { "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
        ...
    }
    SubShader {
        Tags { "RenderPipeline"="HDRenderPipeline" "Queue"="Geometry" }
        ...
    }
    SubShader {
        Tags { }
        ...
    }
    FallBack "Path/Name"
}

注意: Unity2019.3, URP 7.xでRenderPipelineタグを試したとき、シェーダーが単一のSubShaderしか含まない場合にタグが何に設定されているか関係なくつてにそれを使用するような挙動になっていました。今変わっているかわかりません。

 "UniversalRenderPipeline"というタグを見かけても正しくないので使わないでください。以前は機能して公式ドキュメントでも使われていたのですが私が指摘するとすぐ修正されました。

 また、Unity2018のバージョンではタグに関係なく常に最初のパスからSceneSelectionPass & Pickingパスを使用していたようです。これはUnity2019以降に修正されましたが、バックポートされたかは不明です。custom scene selection renderingを使用するときは注意が必要です

Queue

Queueタグはオブジェクトのレンダリング順を決めるうえで重要です。ただし、これはマテリアルのインスペクターのRenderQueueで上書きされます。

このタグは定義済みの以下の名前の中から設定する必要があり、それぞれがレンダリングキューの値に対応しています。

  • “Background” (1000)
  • “Geometry” (2000)
  • “AlphaTest” (2450)
  • “Transparent” (3000)
  • “Overlay” (4000)

 +N -Nを追加することで、キュー値を変更できます。例えば"Geometry+1"とすると2001となり2000の後に描画されるようになり、"Transparent-1"とすると2999となり3000の前に描画されるようになります

 2500までの値はOpaque(不透明)とみなされ、同じキュー値ならカメラに近い手前から奥に描画されます。これはレンダリング最適化のためにあり、深度テストに失敗したときに後のフラグメントが破棄されます(後で詳しく説明します)

 2501を超えるとTransparent(透明)で奥から手前に描画されます。なぜなら透明シェーダは深度テスト/書き込みを使わないため、キューを変更することでほかの透明オブジェクトに対するシェーダーの順番を変えることができます

 他のタグも Unity SubShaderTags documentationを見ることで見つけることができます。


Pass

 Passブロックは各SubShaderで定義されます。これらは複数のパスを指定でき、それぞれにLightModeと呼ばれる特別なタグを指定できます。このタグはPassをいつどのように使われるか指定することができます。

SubShader {
    Tags { "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
    Pass {
        Name "Forward"
        Tags { "LightMode"="UniversalForward" }
        ...
    }
    Pass {
        Name "ShadowCaster"
        Tags { "LightMode"="ShadowCaster" }
        ...
    }
    Pass {
        Name "DepthOnly"
        Tags { "LightMode"="DepthOnly" }
        ...
    }
    //UsePass "Universal Render Pipeline/Lit/ShadowCaster"
    ...
}

 オプションでNameを追加することでUsePassで違うシェーダーで使えるようになります。上記ではURP Lit shaderからShadowCasterを用いた例を示しています。しかしコメントアウトしています。これは実際にはUsePassを使用することを推奨しないからです。SRP Batcherとの互換性を維持するために、すべてのパスは同じUnityPerMaterial CBUFFERを持つ必要があり、UsePassは元のシェーダーで定義されたCBUFFERを利用するため、互換性が壊れる可能性があります。代わりに各パスを自分で書くかコピーする必要があります。あとのセクションでこれらのパスのいくつかを説明します。

 用途によっては追加のパスを必要としない場合があります。例えばフルスクリーンイメージエフェクトを適用するBlit render featureの場合はLightModeを省いて一つのパスで十分です。

LightMode Tag

 前述したとおり、それぞれのPassはLightModeと呼ばれるタグが含まれている。これはUnityがどのようにPassを使うとかを記述する。Universal Render Pipelineは以下のモードを使う

  • “UniversalForward” - フォワードレンダリングパスのオブジェクトの描画に使われる。ライティングを含むジオメトリを描画する
  • “ShadowCaster” - キャストシャドウに用いる
  • “DepthOnly” - MSAA が有効かdepth bufferのコピーが無効な時、Depth PrepassDepth Texture(_CameraDepthTexture) を作るのにつかわれる
  • “DepthNormals” - ConfigureInput(ScriptableRenderPassInput.Normal)を通じてRenderer featureが要求したとき、Depth Normals PrepassDepth Texture(CameraDepthTexture) とNormal Texture(CameraNormalsTexture) を作成するために使います。ScriptableRenderPassではSSAO featureを例としてみてください
  • “Meta” - ライトマップのベイクに用いられます
  • “Universal2D” - 2D Renderer が有効な時に使われます
  • “SRPDefaultUnlit” - LightModeタグがPassに含まれていないときのデフォルトです。forwardとdeferredの両方で追加のパスを描画するときにも使用できます。ただし、SRP Batcherの互換性を損ないます。Multi-Passセクションを見てください

v12以降次の機能変更も予定されています

  • “UniversalGBuffer” - Deferredレンダリングパスのオブジェクトの描画に用いられます。ジオメトリを複数のバッファにライティングなしで描画します、ライティングは後で処理されます。
  • “UniversalForwardOnly” - “UniversalForward”と似ていますがDeferredパスのレンダラもforwardパスで描画される。クリアコートのnormalのようにGBufferに収まらないデータを扱う場合に有用

 UniversalGBufferパスはまた正式リリースされていないので、今のところセクションは設けていません。将来的に記事を更新するかもしれません(約束はできない!)

 「Always」「ForwardAdd」「PrepassBase」「PrepassFinal」「Vertex」「VertexLMRGBM」「VertexLM」のようなタグはBuilt-in RP向けでURPでは対応していません

 カスタムのLightModeタグも使用でき、これはCustom Rederer FeatureまたはURPが提供するRenderObjects featureでトリガーできます

Cull

 それぞれのpassには三角形のどの辺をレンダリングするか制御するためのcullを含めることができます。

Pass {
    //Cull Back     // デフォルトで背面がカリングされます
    //Cull Front    // 全面がカリングされます
    Cull Off        // カリングされず両面がカリングされます
    ...
}

どの面が前面か背面かは三角形の頂点の並び順で決まります。blenderではnormalで決定されます

Depth Test/Write

それぞれのパスにはdepth Test(ZTest)とdepth write(ZWrite)の操作を含めることができます

Pass {
    ZTest LEqual    // Default
    // ZTest Less | Greater | GEqual | Equal | NotEqual | Always
    
    ZWrite On       // Default
    // ZWrite Off
    ...
}

 深度テストは深度値を深度バッファの値と比較してどうであるかをもとにフラグメントをどう描画するか決定します。例えばLEqual(記述がなければこれがデフォルトです)は深度バッファ値より少ないか同じ深度のフラグメントを描画します。

 Depth writeはフラグメントの深度テストが成功したときに深度バッファの深度値を置き換えるか決めます。Zrite Offのとき、値は変更されません。これは主に透明オブジェクトに有効で、正しいブレンドのために必要です。一方でこれらのソートは複雑で時々誤った順番で描画されます。

 またOffset操作は二つのパラメータ(factor, units)で深度値をオフセットできます。私は詳しくないのでドキュメントの説明からコピーします(すみません):

 FactorはポリゴンのXまたはYのポリゴンに対するZslopeをスケールし、unitsは分解可能な最小の深度値をスケールします。これにより本当は同じ位置にあるポリゴンを別のポリゴンの上に描画させることができます。 例えばOffset 0, -1はポリゴンをカメラに近づけますが、ポリゴンの傾きを無視します、Offset -1, -1はかすめてみたときにポリゴンをより近づけます。

Pass {
    Offset 0, -1
}

Blend & Transparency

 シェーダーが透過をサポートするために、Blend modeを定義します。これはフラグメント結果がどのようにカメラのカラーターゲット/バッファの既存の値と組み合わせるかを決定します。構文は

Blend SrcFactor DstFactor
// or
Blend SrcFactor DstFactor, SrcFactorA DstFactorA
// アルファチャンネルの異なるファクターをサポート

 シェーダーの出力色はSrcFactorによって乗算され既存のカラー target/bufferピクセルはDstFactorによって乗算されます。それぞれの値はそれぞれのBlendOp操作(デフォルトはAdd)に基づき組み合わされ、バッファ内の値を置き換える最終的な色が生成されます。  ファクターは以下の通りです

  • One
  • Zero
  • SrcColor
  • SrcAlpha
  • DstColor
  • OneMinusSrcColor
  • OneMinusSrcAlpha
  • OneMinusDstColor
  • OneMinusDstAlpha

 Addと異なる操作を選ぶときは「Blend Docs Page」を参照してください。サポートしているBlendOpのリストがあります。

 最もよく使われるblendは次の通りです: - Blend SrtcAlpha OneMinusSrcAlpha - 既存の透過 - Blend One OneMinusSrcAlpha 前乗算透過 - Blend One One - 加算 - Blend OneMinusDstColor One - ソフト加算 - Blend DstColor Zero - 乗算 - Blend DstColor SrcColor - 2乗算

いくつかの例を示します:

Pass {
    Blend SrcAlpha OneMinusSrcAlpha // (Traditional transparency)
    BlendOp Add // (is default anyway)
    
    /*
    次と同じです
    newBufferColor = (fragColor * fragColor.a) + (bufferColor * (1 - fragColor.a))
    
        lerpが行うこのケースとも同じです
    newBufferColor = lerp(bufferColor, fragColor, fragColor.a)
    
    Of note :
    - fragColor.aが0ならbufferColorは変わりません.
    - fragColor.aが1,ならfragColor完全に使用されます.
    */
}
Pass {
    Blend One One // (Additive)
    BlendOp Add // (is default anyway)
    
    /*
    This means,
    newBufferColor = (fragColor * 1) + (bufferColor * 1)
    
    Of note :
    - アルファはこのブレンドでは影響しません。ただし最終的なアルファ値は変更される可能性があり、DstAlphaにようなものが将来的に使われるときに影響があります。そのためアルファチャンネルに対して異なるfactorを使用したい場合があります
    - bufferColorを変更しないためにはfragColorは必ず黒(0,0,0,0)でなくてはいけません
    */
}

MultiPass

 LightModeタグを使用しない、あるいはSRPDefaultUnlitを使用してUniversalForwardに追加してパスを設定できます。これは一般的に"Multi-Pass"と呼ばれています。しかし、これはURPでは機能しますが、SRP Batcherの互換性を壊すため推奨されません。つまり描画が高負荷になりえます。

 かわりにMulti-Passを実現するために必要な方法は以下のいずれかです。 - シェーダーを分離し、二つ目のマテリアルをメッシュレンダラーに適用する。サブメッシュを使えばより多くのマテリアルを追加し、繰り返すことができます - ForwardRendererのRenderObjects機能を利用すると、特定のレイヤーにあるすべての不透明もしくは透明なオブジェクトをOverride Materialで再レンダリングできます。これは二回目のレンダリングでたくさんのオブジェクトをレンダリングしたい場合にのみ有効です。一つのオブジェクトで全部のレイヤーを無駄にしないでください。Overrideマテリアルを使うと前のシェーダーのプロパティやテクスチャを保持しないからです。 - RenderObjects機能を再度使用する。Overrideマテリアルの代わりにLightModeタグを持つPassを使用し、その機能のシェーダータグIDを設定して描画できます。この方法は同じシェーダなのでプロパティやテクスチャは保持されますが、Shader Graphはカスタムパスを追加する方法がなくコードで書かれたシェーダーにのみ有効です


HLSL

 ShaderのコードはUnityではHigh Level Shading Languade(HLSL)で記述されます。

HLSLPROGRAM & HLSLINCLUDE

 それぞれのShaderLabのパスの中でHLSLコードののブロックをHLSLPROGRAMタグとENDHLSLタグで定義できる。それぞれのブロックでVertexシェーダーとFragmentシェーダーを含む必要があり、#pragma vertex/fragmentと書くことでどの関数が対象か指定できる

 Built-in パイプラインでは"vert"と"frag"が共通の名前としてよく用いられたが、URPでは"UnlitPassVertex"や"UnlitPassFragment"など、もう少し具体的に何をするかを示す関数名で指定する傾向にある

 SubShader内でHLSLINCLUDEを使うことで、SubShaderのすべてのパスにコードを含めることができる。これはURPでは有用で、SRP Batcherと互換性を保つためにすべてのパスで同じUnityPerMaterial CBUFFERを定義することができ、個別に定義する必要がなくなります。代わりに別のincludeファイルを用いることもできます。

SubShader {
    Tags { "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
    
    HLSLINCLUDE
    ...
    ENDHLSL

    Pass {
        Name "Forward"
        // LightMode tag. Using default here as the shader is Unlit
        // Cull, ZWrite, ZTest, Blend, etc

        HLSLPROGRAM
        #pragma vertex UnlitPassVertex
        #pragma fragment UnlitPassFragment
        ...
        ENDHLSL
    }
}

 このコードの内容についてはこの後見ていきます。今はこの後のセクションを理解するために重要なHLSLの基礎を理解します。


Variables

 HLSLではいくつかの変数型があります。もっとも基本的なのはscalar型、Vector型、Matrix型です。また、テクスチャ/サンプラー用の特別なオブジェクトもあり、シェーダーに多くのデータを渡すための配列とバッファもあります

Scaler

 スカラータイプは以下があります:

  • bool - trueかfalse
  • float - 32bit浮動小数点数。一般的にワールド空間の位置やテクスチャ座標、三角法やる以上などの複雑な関数を含むスカラー計算に利用します
  • half - 16bit浮動小数点数。一般的に短いベクトル、方向、オブジェクト空間の位置、色に利用します
  • double - 64bit浮動小数点数。入力としては使用できません。
  • real - URP/HDRPで関数がhalfかfloatをサポートするときに利用し、(プラットフォームが対応していて)シェーダーで#define PREFER_HALF 0と指定されていない限りデフォルトはhalfで半精度です。SHaderLibraryの数学関数の多くがこれを利用します
  • int - 32bit符号付整数
  • uint - 32bit符号なし整数(ただしGLES2ではサポート外なのでintで定義されます)

あわせて: - fixed - 範囲が-2から2の11bit固定小数点。基本的にLDRカラーに使用し、古いCG構文からきています。CGPROGRAMで使ってもすべてのプラットフォームでhalfに変換されるようです。HLSLはサポートしていませんが、Built-in RP用のシェーダーで見かけることがあるので言及しました。halfを使ってください

Vector

 vectorはscalarに1から4のコンポーネント数を付け加えることで作ることができます。例として

  • float4 - 4つのfloatを含むベクトル
  • half3 3コンポーネントのhalfベクトル
  • int2, など
  • float1も1次元ベクトルではあるが、floatと同じです

 ベクトルの要素の一つを取得するときは .x .y. z .w (かわりに .r .g. b. aでも)を使うことができます。また、.xyを使ってvector2を.xyzをつかてvector3をより高次元のベクトルから取得することができます。

 swizzlingと呼ばれるテクニックを使って成分を並び替えたベクトルを返すこともできます。いかが例です

float3 vector = float3(1, 2, 3); // 3次元floatベクトル

float3 a = vector.xyz;  // or .rgb,     a = (1, 2, 3)
float3 b = vector3.zyx; // or .bgr,     b = (3, 2, 1)
float3 c = vector.xxx;  // or .rrr,     c = (1, 1, 1)
float2 d = vector.zy;   // or .bg,      d = (3, 2)
float4 e = vector.xxzz; // or .rrbb,    e = (1, 1, 3, 3)
float  f  = vector.y;   // or .g,       f = 2

// Node xyzwとrgbaを混ぜることはできません

Matrix

 Matrix(行列)はscalarにxで分割表記した1~4の二つの整数を加えることで作ることができます。最初の数はrow(行)で二つ目はcolumn(列)です。例えば

  • float4x4 - 4行4列
  • int4x3 - 4行3列
  • half2x1 - 2行1列
  • float1x4 - 1行4列

 行列は異なる空間の変換に使われます。詳しくない方はこのチュートリアルを見てください。

 Unityは組み込みの変換行列があり、一般的な空間の変換に用いられます。例えば

  • UNITY_MATRIX_M(もしくは Unity_ObjectToWorld) - オブジェクト空間からワールド空間に変換するモデル行列
  • UNITY_MATRIX_V - ワールド空間からView空間に変換するビュー行列
  • UNITY_MATRIX_P - View空間からクリップ空間に変換する射影行列
  • UNITY_MATRIX_VP - ワールド空間からクリップ空間に変換するビュープロジェクション行列

反転させる行列もあり、

  • UNITY_MATRIX_I_M(もしくは unity_WorldToObject) - ワールド空間からオブジェクト空間に変換する逆モデル行列
  • UNITY_MATRIX_I_V - ビュー空間からワールド空間に変換する逆ビュー行列
  • UNITY_MATRIX_I_P - クリップ空間からビュー空間に変換する逆射影行列
  • UNITY_MATRIX_I_VP - クリップ空間からワールド空間に変換する逆ビュープロジェクション行列

 これらの行列を乗算することでスペースの変換をすることができますが、 SRP Core Shader Library SpaceTransforms.hlslに同様のヘルパー関数があります。

 行列の乗算として気を付けることとして掛け算の順序が重要なことがあります。一般的に行列が先に入力としてきて、ベクトルが二番手にきます。二番手のベクトルの入力は4行に拡張した1列の行列とみなすことができます。最初の入力のベクトルは代わりに4列1行の行列とみなすことができます。

 行列のそれぞれの要素は次のようにアクセスすることができます。0始まりの行と列の位置は

  • .m00, .m01, .m02, .m03
  • .m10, .m11, .m12, .m13
  • .m20, .m21, .m22, .m23
  • .m30, .m31, .m32, .m33

1スタートの行と列の位置は

  • .11, .12, .13, .14
  • .21, .22, .23, .24
  • .31, .32, .33, .34
  • .41, .42, .43, .44

0スタートで配列アクセスすると

  • [0][0], [0][1], [0][2], [0][3]
  • [1][0], [1][1], [1][2], [1][3]
  • [2][0], [2][1], [2][2], [2][3]
  • [3][0], [3][1], [3][2], [3][3]

 二つのオプションを用いて入れ替えることができます、m00_m11や.11_22が一例です。

 注意点として.m03_m13_m23はそれぞれの行列の変換部分になっています。UNITY_MATRIX_M.m03_m13_m23はGameObjectの原点のワールド空間の位置になります(ただし、静的/動的バッチ処理がない前提です。理由は Intro to Shaders post)で説明しています。

Texture Objects

 テクスチャは各テクセル(基本的にピクセルと同じ)ごとに色を保存します。テクスチャの時はテクセル(Texture elementの略)と呼ばれ二次元だけとは限りません。

 フラグメントシェーダーは各フラグメント/ピクセル単位で実行され、指定された座標でテクセルの色を取得できます。テクスチャは異なるサイズ(width/height/depth)を持っていますが、テクスチャをサンプリングするための座標は0~1の間に正規化されます。これらはTexture CoordinateまたはUVという名称で知られています(Uはテクスチャの横軸、Vは縦軸に相当し、UVWのときはテクスチャの三つ目の次元あるいは深度がWになります)

もっとも一般的なテクスチャは2Dで、URPでは次のマクロでグローバルスコープに定義されます。

TEXTURE2D(textureName);
SAMPLER(sampler_textureName);

 それぞれのテクスチャオブジェクトに対してサンプラー状態を定義し、これはテクスチャのインポート設定から指定するラップとフィルターのモードです。SAMPLER(sampler_linear_repeat)のようにサンプラー内で定義することもできます。

Filter Mode
  • Point(もしくは Nearest-Point): 色は最も近いテクセルから選ばれます。結果はブロック状のピクセル的なものになります。ピクセルアートをサンプリングする場合はこれを使います
  • Linear/Biliniar: 近いテクセルの距離で加重平均された色が使われます
  • Trilinear: Linear/Bilinearと同じですが、mipmapレベルでブレンドされます
Wrap Mode
  • Repeat: UVの値が0-1の外に出たときテクスチャはタイリングして繰り返されます
  • Clamp: UVの値が0-1の外に出たときはクランプされます、つまりテクスチャの端が引き延ばされます
  • Mirror: テクスチャはタイル化して繰り返され、UVの値の整数値を境に反転します
  • Mirror Once: テクスチャは一度だけ反転し、UVの値が-1以下か2以上の時は引き延ばされます

 フラグメントシェーダーのあとで別のマクロを使い、頂点シェーダーも通過したUV座標を用いてtexture2Dをサンプリングします

float4 color = SAMPLE_TEXTURE2D(textureName, sampler_textureName, uv);
// 使用するミップマップレベルを計算するため、これはフラグメントでのみ使用できます。頂点シェーダでテクスチャをサンプリングする必要がある場合は、LODバージョンを使用してミップマップを指定します(たとえば、フル解像度の場合は0)::
float4 color = SAMPLE_TEXTURE2D_LOD(textureName, sampler_textureName, uv, 0);

 他のテクスチャタイプには以下があります:Texture2DArray, Texture3D, TextureCube(シェーダ街ではCubemapとも), TextureCubeArray。そしてそれぞれにマクロがあり、

// Texture2DArray
TEXTURE2D_ARRAY(textureName);
SAMPLER(sampler_textureName);
// ...
float4 color = SAMPLE_TEXTURE2D_ARRAY(textureName, sampler_textureName, uv, index);
float4 color = SAMPLE_TEXTURE2D_ARRAY_LOD(textureName, sampler_textureName, uv, lod);

// Texture3D
TEXTURE3D(textureName);
SAMPLER(sampler_textureName);
// ...
float4 color = SAMPLE_TEXTURE3D(textureName, sampler_textureName, uvw);
float4 color = SAMPLE_TEXTURE3D_LOD(textureName, sampler_textureName, uvw, lod);
// uses 3D uv coord (commonly referred to as uvw)

// TextureCube
TEXTURECUBE(textureName);
SAMPLER(sampler_textureName);
// ...
float4 color = SAMPLE_TEXTURECUBE(textureName, sampler_textureName, dir);
float4 color = SAMPLE_TEXTURECUBE_LOD(textureName, sampler_textureName, dir, lod);
// uses 3D uv coord (named dir here, as it is typically a direction)

// TextureCubeArray
TEXTURECUBE_ARRAY(textureName);
SAMPLER(sampler_textureName);
// ...
float4 color = SAMPLE_TEXTURECUBE_ARRAY(textureName, sampler_textureName, dir, index);
float4 color = SAMPLE_TEXTURECUBE_ARRAY_LOD(textureName, sampler_textureName, dir, lod);

Array

 Arrayも同様に定義でき、forループを使って繰り返し処理できます。例えば

float4 _VectorArray[10]; // Vector array
float _FloatArray[10]; // Float array

void ArrayExample_float(out float Out){
    float add = 0;
    [unroll]
    for (int i = 0; i < 10; i++){
        add += _FloatArray[i];
    }
    Out = add;
}

 ループのサイズが決まっていて、(つまり変数に基づかない)、ループがすぐに終わらない場合、ループを"unroll"するほうがパフォーマンスが高くなる場合があります。これはインデックスを変更して同じコードを何度もコピーペーストするようなものです。

 技術的にはほかのタイプも配列に持たせられますが、UnityからC#スクリプトで設定できるのはVector(float4)とFloat配列だけです。

 また常にグローバルに設定する方法、つまりshader.SetGlobalArrayもしくはShader.SetGlobalFloatArrayを用いることで、material.SetVector/Floatを用いる方法よりもおススメです。なぜならarrayはUnityPerMaterial CBUFFERに適切に含めることができないからです(また、ShaderLab Propertiesでも定義が必要でarrayはそちらもサポートされていません)。オブジェクトがSRP Batcherでバッチ化されるとき、複数のマテリアルが異なる配列を使おうとすると画面上で何かレンダリングされているかに依存してすべてのオブジェクトの値が変わりグリッチ現象を引き起こします、グローバルに設定した場合一つのarrayが使われるので、そのようなことを回避することができます。

 注意点としてSetXArrayメソッドで指定できる配列の最大サイズは1023であることがあります。より多くのサイズが必要な場合はターゲットプラットフォームでサポートされていることを前提に別の解決策、例えばCompute Buffer(StructuredBuffer)を試す必要があるかもしれません。

Buffer

 arrayの代わりにCompute Bufferを使うことができます、これはHLSLで StructuredBuffer(読み取り専用で読み書きようにRWStructuredBufferがありますが、pixel,fragment,computeシェーダーでのみサポートされています)と呼ばれています

 これらの機能を使うときは少なくとも#pragma target 4.5を付ける必要があります。すべてのプラットフォームがcompute bufferをサポートしているわけではありません。SystemInfo.supportsComputeShaderをC#ランタイム内で使うことでプラットフォームがサポートするか確認することができます

struct Example {
    float3 A;
    float B;
};

StructuredBuffer<Example> _BufferExample;

void GetBufferValue(float Index, out float3 Out) {
    Out = _BufferExample[Index].A;
}

C#で設定するときはテストとして

using UnityEngine;

[ExecuteAlways]
public class BufferTest : MonoBehaviour {

    private ComputeBuffer buffer;

    private struct Test {
        public Vector3 A;
        public float B;
    }
    
    private void OnEnable() {
        Test test = new Test {
            A = new Vector3(0, 0.5f, 0.5f),
            B = 0.1f,
        };
        Test test2 = new Test {
            A = new Vector3(0.5f, 0.5f, 0),
            B = 0.1f,
        };
        
        Test[] data = new Test[] { test, test2 };
        
        buffer = new ComputeBuffer(data.Length, sizeof(float) * 4);
        buffer.SetData(data);
        
        GetComponent<MeshRenderer>().sharedMaterial.SetBuffer("_BufferExample", buffer);
    }

    private void OnDisable() {
        buffer.Dispose();
    }
}

 StructuredBufferについてはあまり詳しくないのでこのセクションでは少し不足していたかもしれません。ほかのオンラインの情報のほうがより充実しているでしょう


Functions

 HLSLでの関数の宣言はC#とよく似ていますが、大事な注意点として関数を呼び出せるのは宣言後になります。関数を宣言する前に呼び出せないので、関数と#includeの順番は重要です。

float3 example(float3 a, float3 b){
    return a * b;
}

 ここのfloat3は返り値です。"example"は関数名で、カッコ内が関数に渡されるパラメータです。返り値がないときはvoidが使われます。パラメーターの前にoutを付けることで出力パラメータを指定できます。inもありますが書く必要がありません

// voidを使用し、float3を出力パラメータとする
void example(float3 a, float3 b, out float3 Out){
    Out = a * b;
}
/* 複数の出力をまとめるときはStructにまとめるかこの方法が良いです */

 inlineも関数の返り値で見ることができます。これはデフォルトで関数が実際に持てる唯一の修飾子で重要ではありません。これはコンパイラーが関数の呼び出すたびに関数のコピーを生成することを意味します。これは関数を呼び出すときのオーバーヘッドを減らすために行われます。

次のような関数も見ることがあります

#define EXAMPLE(x, y) ((x) * (y))

 これはマクロと呼ばれるものです。マクロはシェーダーコンパイル前に定義通りにパラメータを置換します。たとえば

float f = EXAMPLE(3, 5);
float3 a = float3(1,1,1);
float3 f2 = EXAMPLE(a, float3(0,1,0));
 
// コンパイル前にこのようになります
float f = ((3) * (5));
float a = float(1,1,1);
float3 f2 = ((a) * (float3(0,1,0)));
 
// 重要な点としてxとyを()でかこんでいることで
// というのもこれは
float b = EXAMPLE(1+2, 3+4);
// このように
float b = ((1+2) * (3+4)); // 3 * 7, so 21
// ()が含まれていなければ代わりに
float b = (1+2*3+4)
// が優先されるため、11になる

ほかのマクロの例として

#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)
 
// Usage :
OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex)
 
// which becomes :
OUT.uv = (IN.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw);

 演算子の##はマクロが役に立つ特別なケースです。これにより名前とSTを連結してMainTex_ST を入力として得られる。もし##を省略すると、単にname_STが生成されて、定義されていないためにエラーになります(もちろんMainTex_STも定義されている必要があります、これは意図されています。テクスチャ名にSTを付加していますが、これはUnityにテクスチャのタイリングとオフセット値の扱い方を指示します)

UnityPerMaterial CBUFFER

 実際にシェーダーコードを作るところを見ていきましょう、まずUnityPerMaterial CBUDDERをSubShaderの中のHLSLINCLUDEブロック内に定義しましょう。これは同じCBUFFERがすべてのパスで使われることを保証します。これはSRP Batcherに互換性を保つために重要です。

 CBUFFERは必ず公開されているプロパティ(ShaderLabプロパティフロックにあるものと同じ)をテクスチャ除いてすべて含めないといけません。ただしテクスチャはタイリングとオフセットの値(例:ExampleTexture_ST, STはScaleとTranslate)とTexelSize(例:ExampleTexture_TexelSize)が使用されている場合は含める必要があります。

 これは公開されていない変数を含めることはできません

HLSLINCLUDE
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    
    CBUFFER_START(UnityPerMaterial)
    float4 _ExampleTexture_ST; // Tiling & Offset, x = TilingX, y = TilingY, z = OffsetX, w = OffsetY
    float4 _ExampleTexture_TexelSize; // x = 1/width, y = 1/height, z = width, w = height.
    float4 _ExampleColor;
    float _ExampleRange;
    float _ExampleFloat;
    float4 _ExampleVector;
    // etc.
    CBUFFER_END
ENDHLSL

注意:C#のmaterial.SetColor/SetFloat/SetVectorなどで変数を設定する場合、変数を公開する必要はありませんが、もし複数のマテリアルインスタンスが異なる値を持っていた時、グリッチ的な振る舞いになります、なぜならSRPBatcherは画面上でBatch処理を走らせるためです。公開されていない変数がある場合はShader.SetGlobalX関数を使って常に設定して起き、すべてのマテリアルインスタンスで一定になるようにします。マテリアルごとに異なる場合は、代わりにShaderPropertiesブロックを使い公開し、CBUFFERに追加してください。

 上記コードでは#includeを使用してCore.hlslをURP ShaderLibraryからインクルードしてます。これは基本的にBuilt-in pipelineのUnityCG.cgincのURP版です。Core.hlsl(と自動的にインクルードされるほかのShaderLibraryファイル)はCBUFFER_STARTとCBUFFER_ENDマクロを含む便利な関数やマクロがたくさんあります。CBUFFER_STARTマクロとCBUFFER_ENDマクロはそれ自体「cbuffer name { }」と サポートしているプラットフォームで置き換えられます。(GLES2以外のすべてのプラットフォームが対象だと思いますが、SRP BatcherがGLES2でサポートされていないので理にかなっています)


Structs(構造体)

 頂点シェーダーかフラグメントシェーダーの関数を書く前にpass dataの入力と出力に受け渡すstructを定義する必要があります。built-inでは一般的に"appdata"と"v2f"("vertex to fragment"の略です)と名付けられたものを作成します。一方URPでは"Attributes"と"Varying"を代わりに使います。これらは単純に名前でありそこまで重要ではありません。必要であれば"VertexInput"と"FragmentInput"と名付けることができます。

 URP ShaderLibraryは特定の関数に必要なデータを整理するためにいくつかのstructを使用しています。そのstructはInputやSurfaceDataなどライティングやシェーディングの計算で使われます。これらはライティングのセクションで説明します。

 これはかなり単純なUnlitシェーダーなので、AttributeとVaryingは複雑になりません

Attributes(VertexInput)

struct Attributes {
    float4 positionOS   : POSITION;
    float2 uv           : TEXCOORD0;
    float4 color        : COLOR;
};
// 構造体の最後のセミコロンを忘れないようにしてください
// でないと"Unexpected Token"エラーがでます

 Attribute構造体は頂点シェーダーの入力になります。これはメッシュからセマンティクスとして知られる(ほとんどが大文字の)文字列を用いて頂点ごとのデータを取得します

 セマンティクスの完全なリストはリンクを見ればわかりますが、ここでは頂点入力でよく使われるセマンティクスを紹介します

  • POSITION: 頂点位置
  • COLOR: 頂点色
  • TEXCOORD0-7:UV。メッシュは0-7までの8つの異なるUVチャンネルを持っています。C#ではMesh.uvがTEXCOORD0に対応し、Mesh.uv1は存在せず、次のチャンネルではuv2になり、Mesh.uv8とTEXCOORD7まで続きます。
  • NORMAL:頂点の法線(ライティング計算に使用し、unlitでは不要)
  • TANGENT:頂点の接線("接空間"を定義するために必要で、法線マップや視差効果に重要です)

 これらはさらに特別なセマンティクスがあります。例えばSV_VertexID(#pragma target 3.5が必須です)がありますこれは頂点ごとにIDを取得できます。これはComputeBufferで使うときに便利です。

Varying(FragmentInput)

struct Varyings {
    float4 positionCS   : SV_POSITION;
    float2 uv           : TEXCOORD0;
    float4 color        : COLOR;
};
// 構造体の最後のセミコロンを忘れないようにしてください
// でないと"Unexpected Token"エラーがでます

 Varying構造体はフラグメントシェーダーの入力になります。そして頂点シェーダーの出力になります(この間にジオメトリシェーダがないと仮定します。別の構造体が必要になる可能性がありますが、この記事では説明しません。)

 前の構造体と異なり、POSITONではなくSV_POSITIONを使います。これは頂点シェーダーの出力からクリップ空間の座標を格納します。ジオメトリを画面上の正しい位置のフラグメント/ピクセルに変換することが重要です。

   COLORとTEXCORRDn(nは数値)セマンティクスも使用します。しかし以前と異なりメッシュの頂点色/UVに対応する必要は全くありません。そのかわり、三角形全体のデータを補完するために使われます。NORMAL/TANGENTは通常Varying構造体で使用されませんが、機能しているのをみたことがあります(完全にカスタムされたセマンティクス、例えばShader GraphがINTERPnを使うときなどです)、これはすべてのプラットフォームをサポートしておらず、私なら無難なTEXCOORD0を選びます。

 プラットフォームやコンパイルターゲットにより、可能な補間数は異なります

  • OpenGL ES 2.0 (Android), Direct3D 11 9.x level (Windows Phone), とDirect3D 9 Shader Model 2.0 (#pragma target 2.0) は8個 (例えば. TEXCOORD0-7)
  • Direct3D 9 Shader Model 3.0 (#pragma target 3.0) は10個まで (e.g. TEXCOORD0-9)
  • OpenGL ES 3.0 (Android) とMetal (iOS) は16個 (e.g. TEXCOORD0-15)
  • Direct3D 10 Shader Model 4.0 (#pragma target 4.0) は32個まで (e.g. TEXCOORD0-31)

 もう一つの便利なセマンティクスはCull Offと合わせて用いるVFACE(浮動小数点型でDirect3D 9 シェーダーモデル3で使用可能)です。負の値は裏面で正の値は全面であることを示します。つまり(face >0) ? _ColorFront : _ColorBackのような3項演算子を用いるとそれぞれの面に異なる色を当てることができます。Direct3D 10では似たSV_IsFrontFaceというものがありますが、これは小数点ではなくboolです。

 詳細は「Shader Semantics docs page」と「Shader Compile Targets docs page」を見てください

FragmentOutput

 フラグメントシェーダーでは出力構造体も提供します。しかし、通常はSV_Targetという単一の出力セマンティクスしか使用しないため必要ありません。SV_Targetはフラグメント/ピクセル色を現在のレンダーターゲットに書き込むために使用されます。この場合以下の関数のようになります

half4 UnlitPassFragment(Varyings input) : SV_Target {
    // ... // 色の計算など
    return color;
}

 シェーダーが複数のレンダーターゲットに向けて出力することは可能で、MultiRenderTarget(MRT)として知られています。これは例えばUnityGBuffer.hlsl(URPではまだ完全サポートではない)を参照するDeffered Renderingパスで使用されます。

 Deffererdパスを使わないときに、MRTはC#でRenderBufferとともにGrahics.SetRenderTargetを使ったり、RenderTargetIdentiferとともにCommandBuffer.SetRenderTargetを使って設定します。MRTはすべてのプラットふぃーむでサポートされているわけではありません(たとえば GLES2)

 シェーダー内で次のようにMRT出力を設定できます

struct FragOut {
    half4 color : SV_Target0;   // SV_Targetとも
    half4 color2 : SV_Target1;  // 別のrender target
};

FragOut UnlitPassFragment(Varyings input) {
    // ... // color と color2の計算
    FragOut output;
    output.color = color;
    output.color2 = color2;
    return output;
}

深度の値を変えることもできます。SV_Depthセマンティクス(SV_DepthGreaterEqual/ SV_DepthLessEqual)を使います。私のDepth articleを見てください。


Vertex Shader

 頂点シェーダーに必要な中心的なことはメッシュからオブジェクト空間の座標をクリップ空間の位置に変換することです。これは正しくフラグメント/ピクセルスクリーン上の意図した位置に描画するのに必要です。

 ビルトインシェーダではUnityObjectToClipPos関数でこれを行いますが、これはTransformObjectToHClip(SRP-core SpaceTransform.hlslに見つかります)に名前が変更されました。とはいえURPで座標を扱うにはほかの方法があります。以下示すほかの空間に変換する方法はとても簡単です。

Varyings UnlitPassVertex(Attributes IN) {
    Varyings OUT;
    // Varyings OUT = (Varyings)0;としても
    // すべての構造体を0に初期化できます
    // そうでなければすべての変数を設定する必要があります
 
    // OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
    // か
    VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
    OUT.positionCS = positionInputs.positionCS;
    // どちらも .positionWS, .positionVS と.positionNDC (またはscreen position)を含みます
    
    // テクスチャのタイリングとオフセット(_BaseMap_ST)が適用されたUV/TEXCORRD0を通過
    OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
    
    // 頂点カラーを通過
    OUT.color = IN.color;
    return OUT;
}

 GetVetexPositionInputはよく使われる各空間の座標を計算します。これはCore.hlslの一部ですが、URP v9からは独立した ShaderVariablesFunctions.hlslに分離されました。

 この関数はAttributeからのオブジェクト空間座標を入力と使用し、以下の内容を含むVertexPositionInputs構造体を返します

  • positionWS : ワールド空間での座標
  • positionVS : ビュー空間での座標
  • positionCS : クリップ空間での座標
  • positionNDC : スクリーン上の正規化された座標, ご存じの通り スクリーン座標の左下が. (0,0), 右上が(w,wになります)。注意点としてフラグメント段階に座標を渡すときは位置をw成分で除算するので(1,1)が右上になります。

 今お見せしているunlitシェーダーではこれらの座標空間での値を使いませんが、必要になったときはこの関数はとても便利です。未使用の座標空間の値はコンパイルされたシェーダーには含まれないので、余計な計算になりません。

 頂点シェーダーはフラグメントにデータを渡す責務があります。データはテクスチャ座標(UV)や頂点カラーなどです。Intro To Shadersで話している通り、これらの値は三角形を通して補間されます。UVに関しては単に OUT.uv = IN.uv としてもよいです。どちらも構造体の中でfloat2に設定しているとしたらテクスチャにタイリングとオフセットの値を含めるのが一般的で、ST(Sはslace、Tはtranslate)をテクスチャ名につけてUnityがfloat4にします。この時、BaseMap_STも事前にUnityPerMaterial CBUFFERに含まれています。これをUVに適用するには以下の通りにします。

OUT.uv = IN.uv * _BaseMap_ST.xy + _BaseMap_ST.zw;

 TRANSFORM_TEX マクロがあり、これを代わりに使うこともできます。これはビルトインRPにもURPにもあります。

 Unlitシェーダーには法線/接線データは不要ですが、法線、接線、生成されたBigTangent(従接線)ベクトルのワールド空間の位置を取得するGetVertexNormalInputs があります。

VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
OUT.normalWS = normalInputs.normalWS;
OUT.tangentWS = normalInputs.tangentWS;
OUT.bitangentWS = normalInputs.bitangentWS;

 これは後でライティングで必要になったときに役立ちます。この関数にはnormalOSだけを受け取るバージョンもあり、その場合TangentWSは(1,0,0)でbigtangentWSは(0,1,0)のままです。もしくはPositionWS = TransformObjectToWorldNormal(IN.normalOS)を代わりに使うことで、tangent/bitangentが必要ない場合(例えば法線/バンプまたは視差マッピング効果がないとき)に有効です。


Fragment Shader

 フラグメントシェーダーはアルファを含むピクセル出力の色を決める責務があります。unlitシェーダーではこれは非常に単純な単色か入力テクスチャをサンプリングして得られる色になります。ライトシェーダーではもう少し複雑ですが、URPではいくつかの便利な関数を提供します。詳しくはLightingセクションで見ていきます。

 これから見ていくのはunlitです必要な記述は以下です

half4 UnlitPassFragment(Varyings IN) : SV_Target {
    //  BaseMap Texture をサンプリング
    half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
    
    // カラープロパティーと頂点色でテクスチャを着色する
    return baseMap * _BaseColor * IN.color;
}

 これはhalf4の色を出力するシェーダーで、BaseMapテクスチャからサンプルされ、BaseColorプロパティと頂点色の補間値によっても着色されます。SAMPLE_TEXTURE2DマクロはShaderLibraryによて提供されており、フラグメント/ピクセル毎に実行され指定されたuv座標の色を返します。

 FragmentOutputのセクションで述べたように、SV_Targetはフラグメント/ピクセルの色を現在のレンダーターゲットに書き込むために使います。

 望めばアルファが指定値以下のピクセルを破棄できます。そうしてメッシュ全体を見えないようにできます。これは透過シェーダーだけでなく不透明シェーダーでも利用でき、一般的にアルファクリップ/カットアウト/カットオフと呼ばれます。シェーダーグラフに慣れていればこれはアルファクリップ閾値で処理されます。シェーダーコードでは一般に_Cutoffという名前のfloatプロパティを含みます(SRP Batcher互換のためにUniterPerMaterial CBUFFERと同様にShaderlab propertiesに追加されています)。そしてこれはフラグメントシェーダーに使用できます。

if (_BaseMap.a < _Cutoff){
    discard;
}
// OR
clip(_BaseMap.a - _Cutoff);
// フラグメントシェーダー内でreturn前に利用できます

これで基本的にはunlitシェーダーは完成です。


Keywords & Shader Variants

 ライティングに入る前にkeywordとshader variantについてまず話していきます。シェーダーでは#pragma multi_compileディレクティブと#pragma shader_featureディレクティブを指定でき、シェーダコードの特定部分を「オン」または「オフ」に切り替えるためのキーワードを指定するために使用します。このシェーダーは実際に複数のバージョンのシェーダーにコンパイルされます。Unityではマテリアルごとにキーワードを有効/無効にすることでどのバリアントを使用するか選択できます。

Multi Compile

#pragma multi_compile _A _B _C (...etc)

 この例では三種類のシェーダーを作っています。それらはa B _Cがキーワードになっています。そして#if defined(KEYWORD)/ #ifdef KEYWORDを定義することで、どのコードが使われるかキーワードでトグルさせることができます。例えば

#ifdef _A
// Aが有効な時コンパイルされます
#endif
 
#ifndef _B
// Bが有効じゃないときにコンパイルされます。AまたはCのときです
// 注意:#ifndef の n "if not defined"の略です
#else
// Bが有効な時コンパイルされます
#endif
 
#if defined(_A) || defined(_C)
// AかCの時コンパイルされます。(ほかのキーワードがなければ、上記と同じです。)
// 長い形式の "#if defined()"を使わなければいけない場合があり、複数の条件が必要な時です。|| がor && が and そして ! がnotでC#と似ています
// 注意:ただし一つのmulti_compileでキーワードが定義されるので両方を有効にすることは実際は不可能です
#endif
 
/* #elif もあり "else if" を意味するステートメントです */

 URPは多くのmulti_compilesを使用しますが、ここでは一般的なものをいくつか示します。すべてのシェーダーがこれらのキーワードを含める必要はありませんが、ShaderLibraryのいくつかの関数はこれらのキーワードに含まれていることに依存し そうしなければ計算を省略してしまう可能性があります。

// 追加のライト(例えば Point, Spotlights)
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS

// 影
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
// 注意: v11ではこのように変わります
// #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile _ _SHADOWS_SOFT

// Baked Lightmap
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile _ DIRLIGHTMAP_COMBINED
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
#pragma multi_compile _ SHADOWS_SHADOWMASK

// Other
#pragma multi_compile_fog
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
#pragma multi_compile _ _SCREEN_SPACE_OCCLUSION

Shader Feature

 Shader FeatureはMulti-Compileと似ていますが追加のvariantはすべてのキーワードを無効にして生成し、未使用のvariantは最終ビルドに含まれなくなります。これはビルド時間を短縮するのに便利ですが、実行時にこれらのキーワードを有効/無効にするのは良くありません。なぜならビルドに必要なシェーダーが含まれていないかもしれないからです。もし実行時にこれらのキーワードを処理するなら代わりにmulti_compileを使うべきです。

#pragma shader_feature _A _B (...etc)

 上記のコードではAとBがキーワードで三つのvariantが生成されます。キーワードは二つだけですが、両方が無効なものも生成されます。マルチコンパイル使用時、最初のキーワードを一つかそれ以上のアンダースコア(_)を使って空白にする事で同様なことができます。

#pragma multi_compile _ _A _B

Shader Variants

 multi_compileとshader_featureを追加するたびに、より非常に多くのshader variantsをキーワードの有効/無効に応じて可能な組み合わせごとに生成します。例えば次のようになります。

#pragma multi_compile_vertex _ _A
#pragma multi_compile_fragment _ _B
#pragma shader_feature_vertex _C
#pragma shader_feature_fragment _D

 例ではAとCは頂点プログラムにだけ使用され、BとDはフラグメントにのみ使用されます。Unityはこれで二つのshader variantsを生成すると教えてくれますが、両方が無効になっているものと、「半分」のものが二つあるように実際にコンパイルされたコードを見るとそう見えます。

 ドキュメントにShader Variantsに関する詳細な情報があります。

Keyword limits

 注意点としてプロジェクトあたり256個までのグローバルキーワードの個数制限があります。なので、命名規則にこだわって、ほかのシェーダーで同じキーワードを再利用して、新しいキーワードを使わないようにしましょう。

 またMulti-Compileについてさらに気を付けることとして最初のキーワードは通常単なる「_」になっています。このキーワードを空白にすることで、256の最大個数の中でより多くのスペースが可能になります。Shader Featuresではこれは自動的に行われます。

#pragma multi_compile _ _KEYWORD
#pragma shader_feature _KEYWORD
 
// キーワードが無効になっているかを知りたい場合は以下のようにします

#ifndef _KEYWORD
// ご存じの通り "#if !defined(_KEYWORD)"
// か "#ifdef _KEYWORD #else" も機能します

// ... code ...

#endif

 最大キーワード数を使い切らないように、ローカルバージョンのmulti_compileとshader_featureを使うことができます、これらはローカルなキーワードをシェーダーに生成します。ただしこれはシェーダーごとに64個までです。

#pragma multi_compile_local _ _KEYWORD
#pragma shader_feature_local _KEYWORD
 
// There's also local_fragment/vertex ones too!
#pragma multi_compile_local_vertex _ _KEYWORD
#pragma multi_compile_local_fragment _ _KEYWORD
#pragma shader_feature_local_vertex _KEYWORD
#pragma shader_feature_local_fragment _KEYWORD

Lighting Introduction

 ビルトインパイプラインではライティング/シェーディングを必須とするカスタムシェーダーは通常サーフェスシェーダーで扱って慰安した。これらのシェーダーはどのライティングモデルを使用するかのオプションを選択でき、物理ベースのStandard/StandardSpecularやLambert(拡散)と BlinnPhonng(鏡面)を使うモデルがあります。また、カスタムライティングモデルを書くこともできて、例えばトゥーンシェーディングな見た目が欲しいときに使います。

 Universal RPはサーフェスシェーダーをサポートしませんが、ShaderLibraryにはライティング計算の多くを取り扱うヘルパー関数を提供しています。これらはLighting.hlslに含まれています(Core.hlslとともに自動的includeされないので別途includeする必要があります)

 これらのlightingファイルの中にはほかにも関数があり、lightingを完全に取り扱いうことができます、例えばUniversalFragmentPBRやUniversalFragmentBlinnPhongなどです。これらの関数は本当に便利ですが、セットアップが複雑で、InputDataやSurfaceData構造体を関数に渡す必要があります。

 多くの公開されたプロパティ(これもCBUFFERに追加するべきです)がシェーダーに送ったり、マテリアルごとに変更するのに必要です。正確なプロパティに関して例えばPBRLitTemplateのようなテンプレートをチェックすることができます。

 また、Lighting.hlslをインクルードするまえに定義しなくてはいけないキーワードがあります。なぜなら関数がシャドウやベイクドライティングなど必要なすべての計算をしておく必要があるからです。シェーダーには共通のshader feature keyword(以下には含まれていませんが、テンプレートを見て下さい)があり、機能を切り替えることで、テクスチャサンプリングなど不要な処理を菖着して軽量化できます。

#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
// Note, v11 changes this to :
// #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN

#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _SHADOWS_SOFT
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile _ DIRLIGHTMAP_COMBINED
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
#pragma multi_compile _ SHADOWS_SHADOWMASK
#pragma multi_compile _ _SCREEN_SPACE_OCCLUSION

#pragma multi_compile_fog
#pragma multi_compile_instancing

// Include Lighting.hlsl
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

Surface Data & Input Data

 UniversalFragmentPBR/UniverrsalFragmentBlinnPhong関数のどちらもデータを渡すための二つの構造体を使います。それはSerfaceDataとInputDataです

 SurfaceData構造体はテクスチャをサンプリングする責務を持ち、URP/LitShaderと同じ入力を持ちます。定義は以下の通りです

struct SurfaceData {
    half3 albedo;
    half3 specular;
    half  metallic;
    half  smoothness;
    half3 normalTS;
    half3 emission;
    half  occlusion;
    half  alpha;
    
    //  v10で追加
    half  clearCoatMask;
    half  clearCoatSmoothness;
};

 注意点として、この構造体はShaderLibraryの一部であり、このコードw-個別にインクルードする必要がありません。v10以前ではSurfaceInput.hlslにこの構造体があり、Lighting.hlslの関数は実際にはこの構造体を使用していませんでした。

 構造体を使うことができますが代わりに以下のようにする必要があります。

half4 color = UniversalFragmentPBR(inputData, surfaceData.albedo, surfaceData.metallic, surfaceData.specular,
    surfaceData.smoothness, surfaceData.occlusion, surfaceData.emission, surfaceData.alpha);

 v10+ではこの構造体は独自のファイルSurfaceData.hlslに移され、UniversalFragmentPBR関数は更新されてどちらの構造体も単純に渡すことができます(UniversalFragmentBlinnPhon関数ではSurfaceDataのバージョンがv12で追加されますが、現在のバージョンでは分割する必要があります。例を後で示します)

half4 color = UniversalFragmentPBR(inputData, surfaceData);

 SurfaceInput.hlslもいあわりにインクルードできSufaceData.hlslは自動的にインクルードされます。それらにはBaseMap、BumpMap、_EmissionMapテクスチャ定義とそれらをサンプリングする補助の関数が含まれています。もちろんこれらの関数にアクセスするためにLighting.hlslをインクルードする必要があります。

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"

 InputData構造体はライティング計算に必要な追加の情報を渡すために用いられます。v10では以下を含んでいます

struct InputData {
    float3  positionWS;
    half3   normalWS;
    half3   viewDirectionWS;
    float4  shadowCoord;
    half    fogCoord;
    half3   vertexLighting;
    half3   bakedGI;
    float2  normalizedScreenSpaceUV;
    half4   shadowMask;
};

 繰り返しますが、このコードはInput.hlslに既に含まれていて、Core.hlslをインクルードするときに自動的にインクルードされるのでこのコードを含める必要がありません。

 ライティング関数がこの構造体を使用するので、構造体を作成してそれぞれの変数を含める必要があります。より整理するために、これを別関数で行いフラグメントシェーダーで呼び出すようにします。関数の正確な内容はライティングモデルが実際に必要とするものによって異なります。

 まずは関数を空白のままにして、ファイルの構造がどうなっているかを見ます。次のセクションでは、InitializeSurfaceData関数とInitializeInputData関数の中身を説明します。

// Includes
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"

// Attributes, Varyings, Texture definitions etc.
// ...

// Functions
// ...

// SurfaceData & InputData
void InitializeSurfaceData(Varyings IN, out SurfaceData surfaceData){
    surfaceData = (SurfaceData)0; // avoids "not completely initalized" errors
    // ...
}

void InitializeInputData(Varyings IN, half3 normalTS, out InputData inputData) {
    inputData = (InputData)0; // avoids "not completely initalized" errors
    // ...
}

// Vertex Shader
// ...

// Fragment Shader
half4 LitPassFragment(Varyings IN) : SV_Target {
    // Setup SurfaceData
    SurfaceData surfaceData;
    InitializeSurfaceData(IN, surfaceData);

    // Setup InputData
    InputData inputData;
    InitializeInputData(IN, surfaceData.normalTS, inputData);

    // Lighting Model, e.g.
    half4 color = UniversalFragmentPBR(inputData, surfaceData);
    // or
    // half4 color = UniversalFragmentBlinnPhong(inputData, surfaceData); // v12 only
    // half4 color = UniversalFragmentBlinnPhong(inputData, surfaceData.albedo, half4(surfaceData.specular, 1), 
    //      surfaceData.smoothness, surfaceData.emission, surfaceData.alpha);
    // or something custom

    // Handle Fog
    color.rgb = MixFog(color.rgb, inputData.fogCoord);
    return color;
}

 私の知る限り、関数がvoidであることはそこまで重要でありません。代わりに構造体そのものを返すこともでき、私もこのほうが好きですが、URP/Lit Shaderコードとより一貫性を持たせようとしました。

 これらをより整理したいなら、すべての関数を別々のhlslファイルに分割して、 #includeを使うことができます。こうすると複数のシェーダーやMeta Passをサポートする場合に裁量することができます。少なくとも、initializeSurfaceDataとそれに必要な関数/テクスチャ定義を含むhlslファイルは分けておくのをお勧めします。

InitializeInputData

 前述したように、InitializeInputData関数はInputData構造体の各変数を設定する必要があります。ですが、これは主に頂点ステージから渡されてデータを主に取得し、いくつかのマクロや関数を使用するものです(例えばスペース間のtransformationを処理するためです)

 この構造体はすべてのライティングモデルで同じにできますが、いくつかの部分を省略することもできます、例えばbaked lightingやshadowMaskをサポートしない場合などです。注意点として、InputData構造体のすべてを初期化する必要があり、関数の最初の行ではエラーを避けるためにすべての値を初期値0にしています。注意深く重要なところを見逃さないようにする必要があります。また、将来のShaderLibraryのアップデートで構造体に変数が追加されたときに、シェーダーが壊れないようにするのにも有用です。

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

#if SHADER_LIBRARY_VERSION_MAJOR < 9
// URP v9.xで追加された関数で、これ以前のバージョンをサポートする場合は含める必要があります
// v10を使っていて前のバージョンを気にしなくてよいなら省略して構いません
// (Note, GetWorldSpaceViewDirをVertex Shaderでも使っています)

// ワールド空間のビュー方向 (viewerに向かう方向)を計算
float3 GetWorldSpaceViewDir(float3 positionWS) {
    if (unity_OrthoParams.w == 0) {
        // Perspective
        return _WorldSpaceCameraPos - positionWS;
    } else {
        // Orthographic
        float4x4 viewMat = GetWorldToViewMatrix();
        return viewMat[2].xyz;
    }
}

half3 GetWorldSpaceNormalizeViewDir(float3 positionWS) {
    float3 viewDir = GetWorldSpaceViewDir(positionWS);
    if (unity_OrthoParams.w == 0) {
        // Perspective
        return half3(normalize(viewDir));
    } else {
        // Orthographic
        return half3(viewDir);
    }
}
#endif

void InitializeInputData(Varyings input, half3 normalTS, out InputData inputData) {
    inputData = (InputData)0; // avoids "not completely initalized" errors

    inputData.positionWS = input.positionWS;
    
    #ifdef _NORMALMAP
        half3 viewDirWS = half3(input.normalWS.w, input.tangentWS.w, input.bitangentWS.w);
        inputData.normalWS = TransformTangentToWorld(normalTS,half3x3(input.tangentWS.xyz, input.bitangentWS.xyz, input.normalWS.xyz));
    #else
        half3 viewDirWS = GetWorldSpaceNormalizeViewDir(inputData.positionWS);
        inputData.normalWS = input.normalWS;
    #endif

    inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);
    viewDirWS = SafeNormalize(viewDirWS);

    inputData.viewDirectionWS = viewDirWS;

    #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
        inputData.shadowCoord = input.shadowCoord;
    #elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)
        inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS);
    #else
        inputData.shadowCoord = float4(0, 0, 0, 0);
    #endif

    // Fog
    #ifdef _ADDITIONAL_LIGHTS_VERTEX
        inputData.fogCoord = input.fogFactorAndVertexLight.x;
        inputData.vertexLighting = input.fogFactorAndVertexLight.yzw;
    #else
        inputData.fogCoord = input.fogFactor;
        inputData.vertexLighting = half3(0, 0, 0);
    #endif

    /* in v11/v12?,ではこれが使えるかもしれません
    #ifdef _ADDITIONAL_LIGHTS_VERTEX
        inputData.fogCoord = InitializeInputDataFog(float4(inputData.positionWS, 1.0), input.fogFactorAndVertexLight.x);
        inputData.vertexLighting = input.fogFactorAndVertexLight.yzw;
    #else
        inputData.fogCoord = InitializeInputDataFog(float4(inputData.positionWS, 1.0), input.fogFactor);
        inputData.vertexLighting = half3(0, 0, 0);
    #endif
    // 今のところフラグメントごとにフォグを再評価するように強制しているようです
    */

    inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS);
    inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS);
    inputData.shadowMask = SAMPLE_SHADOWMASK(input.lightmapUV);
}

 ここにあるすべての関数を説明するのは少し大変なので、大体は見てわかるとうれしいです。ただ一つ不明瞭なのはnormalizedScreenSpaceUVでこれはScreen Space Ambient Occlusionテクスチャを後でサンプリングするためだけに使われ得ちます。サポートしないのであれば省略できますが、含めてもよいです。未使用であればコンパイラが削除してくれるはずです。

 また、わかりにくいかもしれませんが、bakedGIはBaked Global Illumination(ベイクドライティング)の略でshadowMaskはShadowmask modeを設定したときに追加のシャドーマスクテクスチャが使用されることを特にさしています。SAMPLE_GIとSAMPLE_SHADOWMASK幕rは特定のキーワードによってコンパイル時に変更されます。これらの関数はURP ShaderLibraryのLighting.hlsl(v12ではGlobalillmination.hlslに分割/移動)とShadows.hlslにあります。

Simple Lighting

 URP/SimpleLitシェーダーはLighting.hlslのUniversalFragmentBlinnPhong関数を使用していて、LambertBlinn-Phongライティングモデルを使用しています。もしこれらになじみがないならオンラインに良い教材があります。一応手短に説明しておきます。

 Lamberモデルは表面の完全な拡散反射部分ですべての方向に反射し、光源方向と法線の内積で表現されます(どちらも正規化されます)

 Phongモデルは表面の鏡面部分で、view方向(視線)と法線によって反射した光ベクトルが一致したときにより強く反射されます。Blinn-Phongは少し違い、反射されたベクトルの代わりに光ベクトルとview方向の間の半分のベクトルを使用し、より効率よく計算できるようにしています。

 これらのライティングモデルの計算方法を知っておくと便利ですが、URP ShadeerLibraryの関数で取り扱えます。UniversalFragmentBlinnPhong関数はLightingLmbertとLightingSpecular関数の両方を使いLighting.hlslに含まれています

half3 LightingLambert(half3 lightColor, half3 lightDir, half3 normal) {
    half NdotL = saturate(dot(normal, lightDir));
    return lightColor * NdotL;
}

half3 LightingSpecular(half3 lightColor, half3 lightDir, half3 normal, half3 viewDir, half4 specular, half smoothness) {
    float3 halfVec = SafeNormalize(float3(lightDir) + float3(viewDir));
    half NdotH = half(saturate(dot(normal, halfVec)));
    half modifier = pow(NdotH, smoothness);
    half3 specularReflection = specular.rgb * modifier;
    return lightColor * specularReflection;
}

これらの関数を呼びだすにはLighting.hlslをインクルードするか、コードをコピーするだけでいいですが、UniversalFragmentBlinnPhongが代わりにやってくれるので、それを使えばいいです。この関数を使うときは二つの構造体を渡します。InitializeInputData関数については上記セクションで説明しましたが、InitializeSurfaceData関数についてはサポートする必要があるものにより若干異なることがあります(Blinn-Phongは例えばPBRのメタリックを使いません)。私は以下のようなものを使っています

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"

// Textures, Samplers
// (note, BaseMap, BumpMap and EmissionMap is being defined by the SurfaceInput.hlsl include)
TEXTURE2D(_SpecGlossMap);   SAMPLER(sampler_SpecGlossMap);

// Functions
half4 SampleSpecularSmoothness(float2 uv, half alpha, half4 specColor, TEXTURE2D_PARAM(specMap, sampler_specMap)) {
    half4 specularSmoothness = half4(0.0h, 0.0h, 0.0h, 1.0h);
    #ifdef _SPECGLOSSMAP
        specularSmoothness = SAMPLE_TEXTURE2D(specMap, sampler_specMap, uv) * specColor;
    #elif defined(_SPECULAR_COLOR)
        specularSmoothness = specColor;
    #endif

    #ifdef _GLOSSINESS_FROM_BASE_ALPHA
        specularSmoothness.a = exp2(10 * alpha + 1);
    #else
        specularSmoothness.a = exp2(10 * specularSmoothness.a + 1);
    #endif
    return specularSmoothness;
}

void InitializeSurfaceData(Varyings IN, out SurfaceData surfaceData){
    surfaceData = (SurfaceData)0; // avoids "not completely initalized" errors

    half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);

    #ifdef _ALPHATEST_ON
        // Alpha Clipping
        clip(baseMap.a - _Cutoff);
    #endif

    half4 diffuse = baseMap * _BaseColor * IN.color;
    surfaceData.albedo = diffuse.rgb;
    surfaceData.normalTS = SampleNormal(IN.uv, TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap));
    surfaceData.emission = SampleEmission(IN.uv, _EmissionColor.rgb, TEXTURE2D_ARGS(_EmissionMap, sampler_EmissionMap));

    half4 specular = SampleSpecularSmoothness(IN.uv, diffuse.a, _SpecColor, TEXTURE2D_ARGS(_SpecGlossMap, sampler_SpecGlossMap));
    surfaceData.specular = specular.rgb;
    surfaceData.smoothness = specular.a * _Smoothness;
}

 前述したとおり、フラグメントシェーダーではこれらのすべての関数を呼び出すこともできます

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
// ...
half4 LitPassFragment(Varyings IN) : SV_Target {
    // Setup SurfaceData
    SurfaceData surfaceData;
    InitializeSurfaceData(IN, surfaceData);

    // Setup InputData
    InputData inputData;
    InitializeInputData(IN, surfaceData.normalTS, inputData);

    // Simple Lighting (Lambert & BlinnPhong)
    // half4 color = UniversalFragmentBlinnPhong(inputData, surfaceData); // v12 only
    half4 color = UniversalFragmentBlinnPhong(inputData, surfaceData.albedo, half4(surfaceData.specular, 1), 
        surfaceData.smoothness, surfaceData.emission, surfaceData.alpha);
    
    color.rgb = MixFog(color.rgb, inputData.fogCoord);
    return color;
}

 全体の例はURP_SimpleLitTemplateを見てください

PBR Lighting

 URP/Litシェーダー はより正確なPhysically Based Rendering(PBR)モデルを使用し、これはLambertとMinimalist CookTorrancemモデルに基づいています。正確な実装はShaderLibraryにより若干異なります。興味を持ったらLighting.hlslのLightingPhysicallyBased関数と、BRDF.hlslのDirectBRDFSpecular関数を見てください。

 これを使うためにどのような実装なのかを理解する櫃王はなく、ただ単にUniversalFragmentPBR関数を呼ぶだけで良いです。前述したとおり、v10+では二つの構造体が必要でInputDataとSurfaceDataです。InitializeInputData関数の作り方は前のいくつかのセクションで説明しました。InitializeSurfaceDataでは以下のように利用します。

// ...
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"

// Textures, Samplers 
// (note, BaseMap, BumpMap, EmissionMap は the SurfaceInput.hlsl をインクルードすることで定義されます)
TEXTURE2D(_MetallicSpecGlossMap);   SAMPLER(sampler_MetallicSpecGlossMap);
TEXTURE2D(_OcclusionMap);           SAMPLER(sampler_OcclusionMap);

// Functions
half4 SampleMetallicSpecGloss(float2 uv, half albedoAlpha) {
    half4 specGloss;
    #ifdef _METALLICSPECGLOSSMAP
        specGloss = SAMPLE_TEXTURE2D(_MetallicSpecGlossMap, sampler_MetallicSpecGlossMap, uv)
        #ifdef _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
            specGloss.a = albedoAlpha * _Smoothness;
        #else
            specGloss.a *= _Smoothness;
        #endif
    #else // _METALLICSPECGLOSSMAP
        #if _SPECULAR_SETUP
            specGloss.rgb = _SpecColor.rgb;
        #else
            specGloss.rgb = _Metallic.rrr;
        #endif

        #ifdef _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
            specGloss.a = albedoAlpha * _Smoothness;
        #else
            specGloss.a = _Smoothness;
        #endif
    #endif
    return specGloss;
}

half SampleOcclusion(float2 uv) {
    #ifdef _OCCLUSIONMAP
    #if defined(SHADER_API_GLES)
        return SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv).g;
    #else
        half occ = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv).g;
        return LerpWhiteTo(occ, _OcclusionStrength);
    #endif
    #else
        return 1.0;
    #endif
}

void InitializeSurfaceData(Varyings IN, out SurfaceData surfaceData){
    surfaceData = (SurfaceData)0; // avoids "not completely initalized" errors

    half4 albedoAlpha = SampleAlbedoAlpha(IN.uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap));
    surfaceData.alpha = Alpha(albedoAlpha.a, _BaseColor, _Cutoff);
    surfaceData.albedo = albedoAlpha.rgb * _BaseColor.rgb * IN.color.rgb;

    surfaceData.normalTS = SampleNormal(IN.uv, TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap));
    surfaceData.emission = SampleEmission(IN.uv, _EmissionColor.rgb, TEXTURE2D_ARGS(_EmissionMap, sampler_EmissionMap));
    surfaceData.occlusion = SampleOcclusion(IN.uv);
        
    half4 specGloss = SampleMetallicSpecGloss(IN.uv, albedoAlpha.a);
    #if _SPECULAR_SETUP
        surfaceData.metallic = 1.0h;
        surfaceData.specular = specGloss.rgb;
    #else
        surfaceData.metallic = specGloss.r;
        surfaceData.specular = half3(0.0h, 0.0h, 0.0h);
    #endif
    surfaceData.smoothness = specGloss.a;
}

そしてフラグメントシェーダーではこのようにします

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
// ...
half4 LitPassFragment(Varyings IN) : SV_Target {
    // Setup SurfaceData
    SurfaceData surfaceData;
    InitializeSurfaceData(IN, surfaceData);

    // Setup InputData
    InputData inputData;
    InitializeInputData(IN, surfaceData.normalTS, inputData);

    // PBR Lighting
    half4 color = UniversalFragmentPBR(inputData, surfaceData);
    
    // Fog
    color.rgb = MixFog(color.rgb, inputData.fogCoord);
    return color;
}

Other Passes

 Universal RPが使用しているパスはほかにもあり、ShadowCaster, DepthOnly, DepthNormals(v10+), Metaパスなどがあります。またカスタムLightModeタグでパスを作成することもでき、Multi-Passセクションで議論しています。

ShadowCaster

 "LightMode"="ShadowCaster"とタグ付けされたパスはオブジェクトにリアルタイムな影を反映させる役割があります。

 前のセクションでUsePassはシェーダーをトリガーするのにつかわれ、別のシェーダーのパスを使うことができると述べましたが、これはSRP Batchのご関係を壊すので、代わりにシェーダー自体にパスを定義する必要があります。

 もっとも簡単な方法がわかりました。(URP/Litのようなシェーダーで使われる)SadowCasterPass.hlslに任せることです。これはAttributesとVaryings構造体、かなり単純な頂点シェーダとフラグメントシェーダを含んでいて、shadow bias offsetとalpha clippint/cutoutを処理します。

//UsePass "Universal Render Pipeline/Lit/ShadowCaster"
// Breaks SRP Batcher compatibility, instead we define the pass ourself :

Pass {
    Name "ShadowCaster"
    Tags { "LightMode"="ShadowCaster" }

    ZWrite On
    ZTest LEqual

    HLSLPROGRAM
    #pragma vertex ShadowPassVertex
    #pragma fragment ShadowPassFragment

    // Material Keywords
    #pragma shader_feature _ALPHATEST_ON
    #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

    // GPU Instancing
    #pragma multi_compile_instancing
    // (Note, properties instancingには対応していませんURP/Litと同じです。)
    // #pragma multi_compile _ DOTS_INSTANCING_ON
    // (LitInput.hlslによって処理されます。私はDOTSを使っていないのでサポートしていません )

    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
    ENDHLSL
}

 繰り返しますが、頂点displacementをサポートしたいならカスタム頂点シェーダーが必要です。

HLSLPROGRAM
#pragma vertex DisplacedDepthOnlyVertex // (instead of DepthOnlyVertex)

// ...

Varyings DisplacedDepthOnlyVertex(Attributes input) {
    Varyings output = (Varyings)0;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

    // Example Displacement
    input.positionOS += float4(0, _SinTime.y, 0, 0);

    output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
    output.positionCS = TransformObjectToHClip(input.position.xyz);
    return output;
}
ENDHLSL

DepthOnly

 "LightMode"="DepthOnly"とタグ付けされたパスは、オブジェクトの深度をCamera Depth Textureに各コム役割を持っており、具体的にはdepth bufferがコピーされないときや、MSAAが有効な時に使われます。もしシェーダーが不透明でZwrite Onを使用している場合はLit/UnlitにかかわらずDepthOnlyPassを含めるべきです。半透明シェーダーも含めることができますが、深度テクスチャは透明オブジェクトの描画前に生成されるために、透明オブジェクトのばあいDepthOnlyPassに現れません。

 DepthOnlyパスは上記のShadowCasterとほぼ同じで、頂点シェーダーでshadow bias offsetを使わないこと(通常のTransformObjectToHClop(IN.positionOS.xyz)をGetShadowPositionHClip(input)の代わりに使う)が異なります。

 上記と同様にURP/Litなどのシェーダで使用されるDepthOnlyPass.hlslを使用してAttributesとVarying構造体と頂点、フラグメントシェーダーで定義することができます。

Pass {
    Name "DepthOnly"
    Tags { "LightMode"="DepthOnly" }

    ColorMask 0
    ZWrite On
    ZTest LEqual

    HLSLPROGRAM
    #pragma vertex DepthOnlyVertex
    #pragma fragment DepthOnlyFragment

    // Material Keywords
    #pragma shader_feature _ALPHATEST_ON
    #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

    // GPU Instancing
    #pragma multi_compile_instancing
    // #pragma multi_compile _ DOTS_INSTANCING_ON

    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
    ENDHLSL
}

また、もし頂点ディスプレイスメントをサポートしたいなら、カスタム頂点シェーダーが必要です。

HLSLPROGRAM
#pragma vertex DisplacedDepthOnlyVertex // (instead of DepthOnlyVertex)

// ...

Varyings DisplacedDepthOnlyVertex(Attributes input) {
    Varyings output = (Varyings)0;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

    // Example Displacement
    input.positionOS += float4(0, _SinTime.y, 0, 0);

    output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
    output.positionCS = TransformObjectToHClip(input.position.xyz);
    return output;
}
ENDHLSL

DepthNormals

 "LightMode"="DepthNormals"でタグ付けされたPassはオブジェクトの深度をCamera Normals TextureにnormalをCamera Normals Textureに書き込む役割があり。cameraのForward/Universal RenderのRender Featureから要求されたときに有効になります。

 例えば、Screen Space Ambient Occlusion FeatureはDepth法線をソースとして使用するかDepthからnormalを再構築できることができます(そのため、代わりにDepthOnlyパスを使います)。これは_CameraNormalsTextureを保存するために追加のbuffer/render textureを避けるためです。

 SSAOやそれを使う可能性のあるほかの機能が確実に不要であれば、パスを除外できます。ただ、それでもサポートするのをお勧めします。オブジェクトが深度とnormalテクスチャに表示されなくなって混乱するのを避けるためです。

 前のパスと同様にDepthNormalsPass.hlslを使用することもできます。

Pass {
    Name "DepthNormals"
    Tags { "LightMode"="DepthNormals" }

    ZWrite On
    ZTest LEqual

    HLSLPROGRAM
    #pragma vertex DepthNormalsVertex
    #pragma fragment DepthNormalsFragment

    // Material Keywords
    #pragma shader_feature_local _NORMALMAP
    //#pragma shader_feature_local _PARALLAXMAP
        //#pragma shader_feature_local _ _DETAIL_MULX2 _DETAIL_SCALED
    #pragma shader_feature_local_fragment _ALPHATEST_ON
    #pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

    // GPU Instancing
    #pragma multi_compile_instancing
    //#pragma multi_compile _ DOTS_INSTANCING_ON

    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthNormalsPass.hlsl"

    // Note なんらかのvertex displacementをするなら頂点関数を変える必要があります。 例えば :
    /*
    #pragma vertex DisplacedDepthOnlyVertex (instead of DepthOnlyVertex above)

    Varyings DisplacedDepthOnlyVertex(Attributes input) {
        Varyings output = (Varyings)0;
        UNITY_SETUP_INSTANCE_ID(input);
        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

        // Example Displacement
        input.positionOS += float4(0, _SinTime.y, 0, 0);

        output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
        output.positionCS = TransformObjectToHClip(input.position.xyz);
        VertexNormalInputs normalInput = GetVertexNormalInputs(input.normal, input.tangentOS);
        output.normalWS = NormalizeNormalPerVertex(normalInput.normalWS);
        return output;
    }
    */
    
    ENDHLSL
}

 述べておきたいこととして、新しいバージョンのURP(v12)ではLitDepthNormalsPass.hlslが代わりに使用されていて、normalマップとdetail normalマップの使用,,parallax/heightマッピング(上記コードのコメントの追加キーワードも必要)がサポートされています。

Meta

 "LightMode"="Meta"でタグ付けされたパスはグローバルイルミネーションを焼きこむときに使われます。もしbaked GIを使わないのならば省略して構いません。

 Unlit ShaderではLitMetaPass.hlslが使えるかもしれませんが、InitializeStandardLitSurfaceData関数が必要で、これは我々が使っているものとも少し異なり、私のPBRの例では頂点色も含まれているので、Varyingsも変更する必要があります。代わりにこのようなものを使うことにしました。

Pass {
    Name "Meta"
    Tags{"LightMode" = "Meta"}

    Cull Off

    HLSLPROGRAM
    #pragma vertex UniversalVertexMeta
    #pragma fragment UniversalFragmentMeta

    #pragma shader_feature_local_fragment _SPECULAR_SETUP
    #pragma shader_feature_local_fragment _EMISSION
    #pragma shader_feature_local_fragment _METALLICSPECGLOSSMAP
    #pragma shader_feature_local_fragment _ALPHATEST_ON
    #pragma shader_feature_local_fragment _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
    //#pragma shader_feature_local _ _DETAIL_MULX2 _DETAIL_SCALED

    #pragma shader_feature_local_fragment _SPECGLOSSMAP

    struct Attributes {
        float4 positionOS   : POSITION;
        float3 normalOS     : NORMAL;
        float2 uv0          : TEXCOORD0;
        float2 uv1          : TEXCOORD1;
        float2 uv2          : TEXCOORD2;
        #ifdef _TANGENT_TO_WORLD
        float4 tangentOS     : TANGENT;
        #endif
        float4 color        : COLOR;
    };

    struct Varyings {
        float4 positionCS   : SV_POSITION;
        float2 uv           : TEXCOORD0;
        float4 color        : COLOR;
    };

    #include "PBRSurface.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/MetaInput.hlsl"

    Varyings UniversalVertexMeta(Attributes input) {
        Varyings output;
        output.positionCS = MetaVertexPosition(input.positionOS, input.uv1, input.uv2, unity_LightmapST, unity_DynamicLightmapST);
        output.uv = TRANSFORM_TEX(input.uv0, _BaseMap);
        return output;
    }

    half4 UniversalFragmentMeta(Varyings input) : SV_Target {
        SurfaceData surfaceData;
        InitializeSurfaceData(input, surfaceData);

        BRDFData brdfData;
        InitializeBRDFData(surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.alpha, brdfData);

        MetaInput metaInput;
        metaInput.Albedo = brdfData.diffuse + brdfData.specular * brdfData.roughness * 0.5;
        metaInput.SpecularColor = surfaceData.specular;
        metaInput.Emission = surfaceData.emission;

        return MetaFragment(metaInput);
    }
    ENDHLSL
}

 PBRSurface.hlslはシェーダーファイルと同じフォルダにあるカスタムHLSLファイルでそのフォルダ内にあります。このファイルにはPBRライティングセクションで使うInitializeSurfaceData関数(あわせてSurfaceInput.hlslのインクルード、テクスチャ/サンプラー定義とInitializeSurfaceDataで必要なSampleMetalicSpecGross、SampleOcclusionなど)が含まれています 。UniversalForward パス右派シェーダー内にそのコードを持たせる代わりにインクルードも行います。


 ここまで読んでくださりありがとうございます!最後のセクションはURPとビルトインRPのすべての違いをまとめたもので、主にシェーダーのコーディングに既に慣れている人向けですが、それでも今まで説明したことのまとめとしても有用です。

 また、この投稿で使用したシェーダー・コードから作成したサンプル/テンプレートを含むセクションも以下にあります。


Summary of Built-in vs URP differences

ShaderLab :

  • URPのShaderLabは“RenderPipeline”=”UniversalPipeline”タグを使います
  • URPのPassはいくつかの異なる"LightMode"をビルトインより使用し、最も標準的なのは"UniversalForward"か完全に省略されたものです(デフォルトは"SRPDefaultUnlit")。リストについてはLightMode Tagセクションを参照してください
  • 最初のUniversalForwardパスのみがレンダリングされます。UPRのマルチパスシェーダーはSRPDefaultUnlitを使用して追加のパスをサポートしていますが、SRP Batcherの互換性が壊れるため非推奨です。。代替手段(Second Material RenderObjectsなど)についてはMulti-Passセクションを参照してください。
  • URPはGrabPassをサポートしません。その代わりにカメラ不透明テクスチャは不透明オブジェクトと透明オブジェクトをレンダリングする間にキャプチャされ、歪み/屈折効果を透明キューのシェーダーに適用することができます。DeclareOpaqueTexture.hlslをインクルードしてSampleSceneColor関数をScreenPos(PositionNDC)を入力として使用します。ほかの透明オブジェクトはテクスチャに表示されません。もし必要であれば、Custom Renderer Featureを使用して、加算歪みオブジェクトをoffscreenバッファに描画して、最終的な出力結果をCommandBuffer.Blitを使用してゆがませるという方法もあります。このアイデアMakin’ Stuff Look Good in Unity video に似ていますが、そこに使われているコードはまだビルトイン向けです。

HLSL:

  • HLSLPROGRAMとENDHLSLは常にCGPROGRAM/ENDCGの代わりに使うべきです。これは公社がいくつかの追加ファイルを含んでいて、URP ShaderLibraryとコンフリクトして再定義エラーを起こすからです
  • half型/精度はHLSLPRAGRAMには存在しないので"half"を代わりに使ってください
  • URPはSurfaceシェーダー(#pragma surface)をサポートせず、Vertex/Fragmentスタイルのシェーダーだけです。(ジオメトリとHull/Domainはまだサポートされています)
  • 構造体は頂点シェーダとフラグメントシェーダの間のデータの受け渡しに使い、それぞれAttributesとVaryingsとURPでは良く呼ばれておりappdataとv2fの代わりに用いられます。これは命名規則であり重要ではありません。
  • UnityCG.cgincをインクルードする代わりにURP ShaderLibraryを使います。おもなインクルード方法は
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
  • SRP Batcherはドローコール間のセットアップうをバッチ処理するので、複数のオブジェクトを同じシェーダーでレンダリングするコストが低くなります。異なるマテリアルのオブジェクトをバッチ処理することもできますが、異なりシェーダ/シェーダバリアントをバッチ処理することはできません。シェーダーがこれと互換性を持つためには、URP ShaderLibraryをインクルードし、UnityPerMaterial CBUFFERを公開されているShaderLab Properties(テクスチャを除く)に対してインクルードする必要があります。グローバルシェーダー変数や互換性の破壊を含めることはできません。しぇーーだーが互換性を持っているかはInspectorから確認できます。CBUFFERはすべてのシェーダーパスで一定でなければならない他m、サブじぇーだーのHLSLINCLUDE内に書いておくのをお勧めします。たとえば
HLSLINCLUDE
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    
    CBUFFER_START(UnityPerMaterial)
    float4 _ExampleTexture_ST; // Tiling & Offset, x = TilingX, y = TilingY, z = OffsetX, w = OffsetY
    float4 _ExampleTexture_TexelSize; // x = 1/width, y = 1/height, z = width, w = height.
    float4 _ExampleColor;
    float _ExampleRange;
    float _ExampleFloat;
    float4 _ExampleVector;
    // etc.
    CBUFFER_END
ENDHLSL
  • MainTexを使う代わりに、URPはBaseMapを使う傾向にあります。これは単に命名規則の違いでAlbedo、Bump、Emissionテクスチャを定義しているSurfaceInput.hlslを含めない限りは重要ではありません。_MainTexはCommandBuffer.Blit(つまりBlit Render Feature)を用いた画像エフェクトや、SpriteRenderコンポーネントからスプライトを取得する場合はまだ利用されます
  • URPはテクスチャを定義するためのマクロを提供しており、テクスチャとサンプラーを別々に定義するDX10+スタイルの構文を使っています:
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
  • また、テクスチャのサンプリングは:
half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
// フラグメントシェーダでのみ使用可で、ビルトインのtex2D()と似ています
// 頂点シェーダでサンプリングするなら使用するmipmap leveを選択するためにLOD versionを選択します:
half4 baseMap = SAMPLE_TEXTURE2D_LOD(_BaseMap, sampler_BaseMap, IN.uv, 0);

// また tex2Dbiasとtex2DgradはこれらのURPマクロと同等です
float bias = -1;
half4 baseMap = SAMPLE_TEXTURE2D_BIAS(_BaseMap, sampler_BaseMap, IN.uv, bias);

float dpdx = ddx(IN.uv.x);
float dpdy = ddy(IN.uv.y);
half4 baseMap = SAMPLE_TEXTURE2D_GRAD(_BaseMap, sampler_BaseMap, IN.uv, dpdx, dpdy);
  • その他のテクスチャタイプ(Texture2DArray、Texture3D、TextureCube、TextureCubeArrayなど)についてはTexture Objectsセクションで追加のマクロを見てください
  • URPにはGetVertexPositionInputsという関数があり、頂点シェーダーで使用でき簡単に別空間への変換を取得できます。未使用のものは計算されないのでこれを使うとかなり便利です。例えば:
struct Attributes {
    float4 positionOS   : POSITION;
};
 
struct Varyings {
    float3 positionCS   : SV_POSITION;
    float3 positionWS   : TEXCOORD2;
};
 
Varyings vert(Attributes IN) {
    Varyings OUT;
    VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
    OUT.positionCS = positionInputs.positionCS; // Clip Space
    OUT.positionWS = positionInputs.positionWS; // World Space
    // OUT.positionVS = positionInputs.positionVS; // View Space
    // OUT.positionNDC = positionInputs.positionNDC; // Normalised Device Coords, aka ScreenPos
    return OUT;
}
  • 同様にGetVertexNormalInputsもあり、ワールド空間の法線(normalWS)、タンジェント空間の接線(tangentWS)、従接線(bitangentWS)が取得できます。法線だけが必要ならTransformObjectToWorldNormal を代わりに使用できます

Keyword

URPでLitシェーダーによく使われるキーワードです:

// Additional Lights (e.g. Point, Spotlights)
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS

// Shadows
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
// Note, v11 changes this to :
// #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN

#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _SHADOWS_SOFT

// Baked GI
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile _ DIRLIGHTMAP_COMBINED
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
#pragma multi_compile _ SHADOWS_SHADOWMASK

// Other
#pragma multi_compile_fog
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION

Unlitならフォグとインスタンス化だけが必要かもしれません。

いくつかの含めることができるshader_featureもあり、下記のテンプレートでもよく使われるもの(_NORMALMAPなど)を見ることができますが、これらはシェーダーに依存し、キーワードがサポートするものでないなら含めるべきではありません


Common Functions/Macros :

Built-In URP で同等
TRANSFORM_TEX(uv, textureName) TRANSFORM_TEX(uv, textureName)
tex2D, tex2Dlod, etc SAMPLE_TEXTURE2D, SAMPLE_TEXTURE2D_LOD, etc. 上記参照
UnityObjectToClipPos(positionOS) TransformObjectToHClip(positionOS),かGetVertexPositionInputs().positionCSを使ってください
UnityObjectToWorldNormal(normalOS) TransformObjectToWorldNormal(normalOS)か GetVertexNormalInputs().normalWSを使ってください
ComputeScreenPos(positionCS) ComputeScreenPos(positionCS)はUnity 2021 / URP v11+で非推奨。 GetVertexPositionInputs().positionNDCを代わりに使ってください
ComputeGrabScreenPos(positionCS) GrabPassはURPで非対応です
WorldSpaceViewDir(positionOS) positionWSを計算し以下の関数を代わりに使ってください
UnityWorldSpaceViewDir(positionWS) GetWorldSpaceViewDir(positionWS) (v9+からShaderVariablesFunctions.hlslに追加されました). それ以前のバージョンではこれをコピーしてください。正規化されたものが必要ならGetWorldSpaceNormalizeViewDir(positionWS)を代わりに使ってください
WorldSpaceLightDir(positionOS) 下を見てください
UnityWorldSpaceLightDir(positionWS) / _WorldSpaceLightPos0 Main Directional Light用です。GetMainLight().directionを使ってください。詳しくはLighting.hlsl
Shade4PointLights(...) 正確に等価なものはありませんが、ビルトインではForwardでの頂点ライティングで使っていました。下を見てください
ShadeVertexLights(vertex, normal) Lighting.hlslのVertexLighting(positionWS, normalWS)
ShadeSH9(half4(worldNormal,1)) SampleSH(normalWS)ですがLighting.hlslのSAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS) マクロ/ SampleSHVertex/SampleSHPixel 関数を使ってください。例としてLitForwardPass.hlslをみてください
UNITY_FOG_COORDS(n) float fogFactor : TEXCOORDn
UNITY_TRANSFER_FOG(o, positionCS) OUT.fogFactor = ComputeFogFactor(positionCS.z)
UNITY_APPLY_FOG(fogCoord, color, fogColor) color.rgb = MixFog(color.rgb, fogCoord)
UNITY_APPLY_FOG_COLOR(fogCoord, color) color.rgb = MixFogColor(color.rgb, fogColor.rgb, fogCoord)
Linear01Depth(z) Linear01Depth(z, _ZBufferParams)
LinearEyeDepth(z) LinearEyeDepth(z, _ZBufferParams)
ParallaxOffset(h, height, viewDirTS) ParallaxOffset1Step(h, amplitude, viewDirTS) v10.1+で使用可 (前のバージョンでは関数をコピーしてください)ParallaxMapping.hlslを見てください
Luminance(rgb) Luminance(rgb), Color.hlslを見てください
V2F_SHADOW_CASTER 大体float4 positionCS : SV_POSITION;と同じですが、ShadowCasterセクションを見てください
TRANSFER_SHADOW_CASTER_NORMALOFFSET ShadowCasterPass.hlslのGetShadowPositionHClip(input)を例としてみてください 。また上を見てください
SHADOW_CASTER_FRAGMENT return 0;
SHADOW_COORDS(1) float4 shadowCoord : TEXCOORD1;
TRANSFER_SHADOW(o) TransformWorldToShadowCoord(inputData.positionWS)
SHADOW_ATTENUATION(i) MainLightRealtimeShadow(shadowCoord), ですがGetMainLight(shadowCoord)でも取り扱えます。Lighting.hlslShadows.hlslを見てください

(ここに載っていないビルトインでよく使われる関数があれば教えてください。追加を考えます)


いくつかのテンプレート/例を私のGitHubで共有します。これらに含まれるものとして

  • Opaque Unlit Shader Template
  • Transparent Unlit Shader Template
  • Opaque Unlit+ Shader Template
    • (optional Alpha Clipping とShadowCaster, DepthOnly & DepthNormals passesを含む)
  • Diffuse Lit Shader Template
    • (Main Directional LightからのみのAmbient / Baked GI & Lambert Diffuse shading)
  • Simple Lit Shader Template
    • (Lambert拡散& Blinn-Phong鏡面.Lighting.hlslのUniversalFragmentBlinnPhongメソッドを使用し,URP/SimpleLitシェーダーと似ている)
  • PBR Lit Shader Template
    • (物理ベースレンダリングのライティングモデル. Lighting.hlsl,のUniversalFragmentPBRメソッドを使用し、URP/Litシェーダーと似ている., 注意点としてheight/parallaxマッピング, detail mapsとclear coatは含みません. URP_PBRLitTemplate.shader, PBRSurface.hlsl, PBRInput.hlslに分割され、organisation & Metaパスをサポートします)

「なぜ依存を注入するのか DIの原理・原則とパターン」が良かった話

book.mynavi.jp

この本を読みました。よかったのでブログにします

読もうと思ったきっかけ

 設計に関する本やデザインパターンに関する本はいくらか読んできたのですが、DIに関しては理解があやふやだなと思っていました。<とりあえずDBへの処理など外部への処理をDIしとけばいい>というような浅い理解しかしておらず、ちゃんと使うには知識が足りていないと感じていました。  そこでこのタイトルの本が発売されたのです。私にドンピシャだったので即決で購入しました。

最初の印象

分厚い  

が最初の印象でした。扱うことはDIのことだけなのに600ページ以上のボリュームがあります。レイアウト的にも文字がビッシリと書かれており、翻訳書特有の読みにくさに合わせて、知らないワードが矢継ぎ早に流れてくるので、一章ではかなり面喰いました。この分量を読み切るのは無理なのではないかと思ってしまいました

読み進めてわかったこの本の良さ

 一章で面喰ったのは当然でした。一章はこれから説明する概念の紹介パートであり、一章だけで理解できるならこの本を読む必要はありません、二章以降は丁寧な説明が続いていて、丁寧さが分厚さにつながっているのだと理解しました。この本では最後まで一貫して


一般的なプログラマーが陥りがちなアンチパターンコードを例示する

そのアンチパターンになぜ陥ってしまうのか、プログラマーの心理と合わせて解説する

そのアンチパターンがなぜダメなのかを解説する

アンチパターンを改善するためにどうすればいいのか、改善後のコードと合わせて解説する

という流れで解説されており、アンチパターンに対しての分析と解説を非常に丁寧にしている印象でした。いきなり正解を提示せずに、アンチパターンコードの考察から展開されていくので、その流れを追っていくだけで自然に理解できるような構成になっています。私も業務の中でやってしまいがちなアンチパターンが多く、どういう考え方でリファクタリングすればいいのかこの本を読むことでかなり理解できたので、今後の業務で実践できそうだと思いました。疎結合で保守性の高いコードを書きたい方はぜひ書店で立ち読みしてみてください

一章はわからなかったら流し読みでOK!一章は一部の要素を除いて全体の紹介です

強いて問題点を挙げるなら

  • C#全くわからないと厳しいかも
  • ASP .NETなどのマイクロソフトフレームワークを使ったことがないと読むのが大変。読めるように配慮はされているが、私はUnityエンジニアなので少し大変だった

Introduction to the Universal Render Pipeline for advanced Unity creators (Unity上級クリエイターのためのUniversal Render Pipeline入門)を読んだ感想

Unityに関する基礎知識がなんだかんだで足りていないなと思うことがあったので、UnityのEbookを読み漁っている。今回の本は和訳がないので感想をブログにまとめておこうかなと思う。

 タイトルは「Introduction to the Universal Render Pipeline for advanced Unity creators」。Built-in RP(URP以前のレンダラーパイプライン)を使っていたUnity開発者がスムーズにURPに移行できるようにURPの機能と設定についてまとめてある本だ。 unity.com

 少し眺めてみたらわかると思うが、Built-in RPとの違いに対する記述の比率がとても多い。URPへのプロジェクトの移行方法もそうだが、細かい機能面でもBuilt-in RPとURPの違いがリストアップされている。もともとBuilt-in RPで業務をしていた人を対象読者にしているだけあるなと思った。といっても、別にBuilt-in RPの内容を飛ばして読めばいいので、これからURPを学びたい人でも読んで良いと思う。

 基本的にはURPの機能を網羅的に紹介する書籍なので、雰囲気でURPを使っていて知識の抜け漏れを補いたいという人とかはかなり良いんじゃないだろうかと思った。

 URPを使ってどういう風に演出に使うか学びたい場合は、URPクックブックというものがあるので、こちらを読もう。日本語もあるので読みやすくなっている。 gamemakers.jp

読まなくていいから印刷して手元に置こう

 読んでみた感想としては、結構ややこしいところが抑えられていて手元にあるとありがたいなと思った。

 設定がプロジェクトについてたりPipelineAssetについてたりカメラについてたりするし、特定の設定はこっちのwindowからじゃないと設定できないとか、Built-in RPと混在するがゆえにごちゃごちゃしているので、この本を印刷しておいて、すぐに取り出せるところに置いておくとかなり便利になりそうだなと思った。

 Built-inとの比較や移行方法が細かく載ってるのだが、意外と役に立つかもしれない。Unity向けでシェーダーとかで調べると、Built-in RP向けのものが引っかかったりして、URPに対応するにはどうすればよいのだろうかと結構悩んだりするのだが、この本が手元にあればヘルパー関数の対応表とかもあるので結構楽に対応できるようになると思う。

C#で型によるSwitchの網羅漏れをRoslynで検出できる...かも

qiita.com

という記事をよんで、いやRoslynで良くね?と思ったのでちょっと調べてみた。

調べたところこのようなものがあり、

github.com

これをNugetForUnityで入れる。

そうすると既存のパッケージに対して大量のエラーを吐いてしまうので、StyleCopなるものを使って抑制する(参考:https://docs.unity3d.com/ja/2021.1/Manual/roslyn-analyzers.html)。以下のファイルをAssets下に書けばOK

<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Default Rule Set" Description=" " ToolsVersion="10.0">
    <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
        <Rule Id="EM0001" Action="None" />
    </Rules>
</RuleSet>

そして、asmdefを切って、そこには何も指定しないでルールセットファイルを追加

<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Default Rule Set" Description=" " ToolsVersion="10.0">
    <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
    </Rules>
</RuleSet>

警告を出してもらうにはcase文を以下のようにしなければいけない。このデザインはちょっとイマイチな気がするが理由はよくわからない

switch (character)
{
    default:
        throw ExhaustiveMatch.Failed(character);
}

そうするとこのようにエラーがでてくるので、

継承するクラスを指定してあげる。もし不足があったらちゃんとエラーになる

/// キャラクターの抽象クラス
[Closed(typeof(Hero), typeof(MagicCaster), typeof(Thief), typeof(Priest))]
public abstract class Character
{

}

そうするとエラーが出て、何の型が不足しているかも表示してくれる

使用感的にちょっとフィットしないが、やりたいことは実現できている気がする。

caseの列挙が漏れるみたいな事象は根本的な設計ミスというよりは、単なる人的ミスという印象で、デザインパターンでどうにか落とし込むよりは、Analyzerを使った方が分かりやすいんじゃないかなーと

throw ExhaustiveMatch.Failed(character);を書かなきゃいけないとか、それぞれClosed(Hoge)しないといけないとか、なんか使用感がしっくりこないので、暇があったらオレオレAnalyzerを作ってみる

Rukhankaの物理関係の挙動 unity DOTS ECS

今はECSをチマチマ触っている。ECSで開発をしているというよりは、ECSで自分の作ろうと思っているゲームが作れそうかの検証と言った感じでやっている。

実際にゴリゴリ開発しているわけではないので参考程度にしてください



ECS、なんだかんだで難しいなと思った。し、そもそも現状のECSではできないこともある。ECSでやりたいことは大量のオブジェクトの描画で負荷がかかりすぎないようにしたいというものなので、

  • 大量の静的なオブジェクトはECS
  • 細かい制御はGameObject

という風に使い分けるのが良いだろうなと思った。大抵はそれで解決するのだけれど、ちょっと困ったことは物理で、Unity Physicsを入れると既存のUnityの物理演算(PhyX)が動かなくなる。動かすオプションがあるかもしれないがどの道GameObject側の物理演算とは干渉しない。プレイヤーは道や橋のような静的なオブジェクトの上を歩くことになるので、干渉してくれないと困る。

とりあえずキャラクターの基本的な挙動はこのパッケージで動かすことにした。 www.youtube.com

そして描画回りだったりはGameObjectで動かすというのがいい塩梅の妥協案なのだと思う。このDOTSおじさんもそのやり方を案内している youtu.be

とはいえ当たり判定をどうしたものかなと思った。モーションとかをanimationさせる時に、colliderも一緒にコントロールできないといろいろと不便なんじゃないかなと。少なくとも私の作る予定のゲームでは困りそうだなと思った。

animationで当たり判定のオブジェクトを動かして、それをUnity PhysicsのPhysics Shapeに変換して同期するようなプログラムを書くと。ちょっとめんどくさそう

で、すこし調べたらこのアセットを見つけた。animationをentityにベイクできるライブラリだ Rukhanka - ECS Animation System | アニメーション ツール | Unity Asset Store

どうも物理にも対応しているらしい。と Discordに入ってやり取りをみてると、結構ドンピシャな使い方をしている人を見つけた。どうやらcolliderもanimationできるらしい

このアセットを調べて動かしてみて分かったことをまとめる

  • colliderを持つオブジェクトを動かしたとき、あるいはscaleを変更したときに、オブジェクトのcolliderもちゃんと追従する
  • animationで子オブジェクト側にcolliderがある場合でも問題なく追従する
  • colliderそもそもを大きくしても反映されない

ということが分かった。私は直方体がブンブンできれば良いのでこれで良いが、FPSのような厳密な当たり判定が必要な場合はちょっと対応方法を考え直さないといけないかもしれない

LFSをGoogleDriveで利用 & git lfsの設定をGitHub Actionsに反映する方法

ちゃんとした業務開発ではあんまり役に立たない知識を授けます

LFS

GitLFSは入れておきたい。なんだかんだアセットの容量はバカにならないし、LFSは使いたいもののGitHub LFSは高い。帯域に課金されるのが辛くて、GitHubActionsを使う時毎回帯域を消費するのがちょっと残念。

UnityVersionControlを使ってみたものの、操作ミスでデータをすべて吹き飛ばすということを二回やらかしたのであきらめた。大規模開発なら使った方が良いのだろうが、ファイルロックも特に必要ないのでGitも使いたい

※git lfs 2.0からファイルロック機能も提供されているらしい

どうしたものかと思っていたが、この記事がいい感じだった。 qiita.com

Googleで課金していて2TB使えるストレージがあるし、かなり良さそうだと思った。rclone周りでちょっと手間取ったが使えるようになった。

game-ci

今一人で作業しているので、正直必要はないのだが、サーバーサイド出身の人間は自動テスト×デプロイの環境が無いと落ち着かないのだ。

TDDっぽい開発フローを採用しているので一応入れておく。なんだかんだ入れておくとgitのpush忘れとかも気づけたりするので良いんじゃないだろうか

github actionでlfsを動かす

LFSをGoogleDriveで代用するのはいいがそれでGithub Actions側でLFS対象ファイルが取れないのは後々困りそうなので対応。

github.com

これをベースに

C:\Users\hogehoge> rclone config file
Configuration file is stored at:
C:\Users\hogehoge\AppData\Roaming\rclone\rclone.conf

で場所が分かるのでこのファイルをwslにもっていって

$ base64 -w 0 rclone.conf

で出た値をRCLONE_CONFIGにいれて、ciを

name: unity-ci
on:
  workflow_dispatch: {}
jobs:
  test:
    name: test
    runs-on: ubuntu-latest
    steps:
    - name: set rclone
      uses: AnimMouse/setup-rclone@v1
      with:
        rclone_config: ${{ secrets.RCLONE_CONFIG }}
    - name: set git-lfs-agent-rclone 
      run: |
        wget https://github.com/funatsufumiya/git-lfs-agent-rclone/releases/download/v0.0.2/git-lfs-agent-rclone_v0.0.2_linux_x64.zip
        unzip git-lfs-agent-rclone_v0.0.2_linux_x64.zip
        mv git-lfs-agent-rclone /usr/local/bin/git-lfs-agent-rclone
    - run: |
        git config --global lfs.standalonetransferagent rclone
        git config --global lfs.customtransfer.rclone.path git-lfs-agent-rclone
        git config --global lfs.customtransfer.rclone.args $場所
      env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    - name: Checkout
      uses: actions/checkout@v4
      with:
        lfs: true
    - name: test
      uses: game-ci/unity-test-runner@v4
      env:
        UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
        UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
        UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
      with:
        projectPath: ./
        githubToken: ${{ secrets.GITHUB_TOKEN }}

のように書いて完了。lfs: trueを利かせる前にもろもろのconfigを設定することと、checkoutをする前はリポジトリのフォルダが無くてlocalに設定できないので、--global をつけておくことがミソ

ビルドして配布したりとか、マシンをself-hosted runnerに差し替えて諸々のファイル(Libraryなど)をキャッシュしたりとかを今後はしていく