Version: 2022.3
言語: 日本語
DX11/OpenGL コアテッセレーションを伴うサーフェスシェーダー
Understanding shader performance

様々なグラフィックス API のシェーダーの作成

様々なグラフィックス API の間で、グラフィックスレンダリングの動作が異なる場合があります。大抵、Unity エディターはその違いをカバーしてくれますが、エディターでは対応できない状況もあります。これらの状況と、その場合に必要な対処法を説明します。

レンダーテクスチャ座標

垂直のテクスチャ座標の規則は 2 つのプラットフォーム、Direct3D 類似のものと OpenGL 類似のものとでは異なります。

  • Direct3D 類: 座標の一番上が 0 で、下に行くにつれ数値が増加します。これは Direct3D、Metal、コンソールに当てはまります。
  • OpenGL 類: 座標の一番下が 0 で、上に行くにつれ数値が増加します。これは OpenGL と OpenGL ES に当てはまります。

この違いは、レンダーテクスチャ にレンダリングするとき以外は、プロジェクトにまったく影響しない傾向があります。Direct3D のようなプラットフォーム上でテクスチャにレンダリングするとき、Unity は内部的にレンダリングを逆さまに反転させます。これにより、異なる類のプラットフォーム (OpenGL 類) とのきまりを一致させることができます。

UV 空間でのイメージエフェクトとレンダリングでは、自分で処理を行って異なる座標のきまりによる問題が発生しないように気を配る必要があります。

イメージエフェクト

イメージエフェクト とアンチエイリアスを使用すると、イメージエフェクトのソーステクスチャは、反転して OpenGL のようなプラットフォームに合わせることはありません。この場合、Unity はスクリーンにレンダリングしてアンチエイリアスを行い、レンダリングをレンダーテクスチャに解決してイメージエフェクトでさらに処理します。

イメージ エフェクトが一度に 1 つのレンダーテクスチャを処理する単純なものである場合は、Graphics.Blit が不整合な座標を処理します。しかし、イメージエフェクト で複数の レンダーテクスチャ を一緒に処理する場合、Direct3D のようなプラットフォームやアンチエイリアスを使用すると、レンダーテクスチャが異なる垂直方向に出力される可能性があります。座標を標準化するためには、頂点シェーダーでスクリーンテクスチャを手動で上下反転させ、OpenGL のような座標標準に一致させる必要があります。

次のコード例は、これを行う方法を示しています。

// テクスチャを上下反転させる例 
// 主要テクスチャの
// テクセルサイズはネガティブの Y になります

# if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
        uv.y = 1-uv.y;
# endif

同様の状況が GrabPass で発生します。 結果のレンダーテクスチャは、Direct3D のような (OpenGL 類でない) プラットフォームで、実際には逆さまにされない場合もあります。シェーダーコードが GrabPass テクスチャをサンプリングする場合、UnityCG インクルードファイルから ComputeGrabScreenPos 関数を使用します。

UV 空間のレンダリング

特殊効果やツールのためにテクスチャ座標 (UV) 空間でレンダリングする場合、レンダリングが Direct3D 類と OpenGL 類の座標系で一貫するように、シェーダーの調整が必要な場合があります。また、画面にレンダリングするときとテクスチャにレンダリングするときで、レンダリングの調整が必要な場合があるかもしれません。このような場合は、Direct3D のような投影を上下反転させて、その座標が OpenGL のような投影座標と一致するように調整します。

ビルトイン変数 ProjectionParams.x には +1-1 の値が含まれています。-1 は投影が OpenGL のような投影座標に一致するように上下を逆さまに反転したことを示し、+1 は反転していないことを示します。 シェーダーでこの値をチェックして、異なる対応を取れます。以下の例では、投影が反転しているかどうかを確認し、反転している場合は、UV 座標を反転して一致させます。

float4 vert(float2 uv : TEXCOORD0) : SV_POSITION
{
    float4 pos;
    pos.xy = uv;
    // この例は上下反転した投影で描画されます
    // ですから垂直の UV 座標も反転させます
    if (_ProjectionParams.x < 0)
        pos.y = 1 - pos.y;
    pos.z = 0;
    pos.w = 1;
    return pos;
}

クリップスペース座標

テクスチャ座標と同様に、Direct3D のようなプラットフォームと OpenGL のようなプラットフォームでは、クリップ空間座標 (投影後の空間座標 post-projection space coordinates とも呼ばれます) は異なります。

  • Direct3D 類: クリップ空間の深度は、ニアクリップ面の +1.0 からファークリップ面の 0.0 までになります。これは、Direct3D、Metal、コンソールに適用されます。

  • OpenGL 類:クリップ空間の深度の範囲は、ニアクリップ面の –1.0 からファークリップ面の +1.0 までになります。これは OpenGL と OpenGL ES に適用されます。

シェーダーコードの内部では、UNITY_NEAR_CLIP_VALUE ビルトインマクロ を使用して、プラットフォームに基づいてニアクリップ面の値を取得できます。

プラットフォームに適切な場合は、スクリプトコードの中で、GL.GetGPUProjectionMatrix を使用して、Unity の座標系 (OpenGL のような規則に従っています) から Direct3D 類の座標に変換します。

シェーダー演算の精度

精度の問題を回避するには、必ずターゲットプラットフォームでシェーダをテストしてください。モバイルデバイスと PC の GPU は、浮動小数点型の処理方法が異なります。PC の GPU は、すべての浮動小数点型 (float、half、fixed) を同じものとして扱います。つまり、すべての演算を完全な 32 ビット精度で行います。一方、多くのモバイルデバイス GPU はこのようには行いません。

詳しくは シェーダーのデータタイプと精度 を参照してください。

シェーダーでの Const 宣言

Use of const differs between Microsoft HLSL (see msdn.microsoft.com) and OpenGL’s GLSL (see Wikipedia) Shader language.

  • Microsoft の HLSL の const は、宣言された変数がそのスコープ内では読み取り専用ですが、どのような方法でも初期化できる点で、C# や C++ とほとんど同じ意味を持ちます。

  • OpenGL の GLSL const は、変数が実際コンパイル時の定数であることを意味するため、コンパイル時の制約 (リテラル値、または他の const の計算) で初期化する必要があります。

OpenGL の GLSL のセマンティクスに従うことが最善であり、本当に不変であるときにだけ変数を const として宣言します。const 変数を他の変更可能な値 (例えば、関数のローカル変数など) で初期化することは避けてください。これは Microsoft の HLSL でも有効です。このように const を使用すると、一部のプラットフォームで混乱しやすいエラーを避けることができます。

Using buffers with GPU buffers

If you use buffers to declare variables in your shader, then set values using the data from a GPU compute buffer or graphics buffer, the data layouts might not match depending on the graphics API. This means you might overwrite data or set variables to the wrong values.

For example if you use cbuffer or Unity’s constant buffer macro, depending on the constant buffer’s data layout and the graphics API, a float3 might become a float4, or a float might become a float2.

You can do the following to make sure all graphics APIs compile a buffer with the same data layout:

  • Use float4 and float4x4 instead of float3 and float3x3, because float4 variables are the same size on all graphics APIs, while float3 variables can become a different size on some graphics APIs.
  • Declare variables in decreasing size order, for example float4 then float2 then float, so all graphics APIs structure the data in the same way.

例:

cbuffer myConstantBuffer { 
    float4x4 matWorld;
    float4 vObjectPosition; // Uses a float4 instead of a float3
    float arrayIndex;
}

シェーダーに使用されるセマンティクス

シェーダーをすべてのプラットフォームで使えるようにするためには、シェーダー値のいくつかはこれらのセマンティクスを使う必要があります。

  • 頂点シェーダーの出力する (クリップスペース) 位置: SV_POSITION。ときおり、シェーダーは POSITION セマンティクスを使用し、シェーダーをすべてのプラットフォームで使えるようにします。これは、Sony PS4 やテッセレーションを使う場合にはうまく挙動しません。

  • フラグメントシェーダーの出力する色: SV_Target。ときおり、シェーダーは COLORCOLOR0 を使用し、シェーダーをすべてのプラットフォームで使えるようにします。これは、Sony PS4 ではうまく挙動しません。

メッシュを点としてレンダリングするときは、頂点シェーダから PSIZE セマンティクスを出力します (たとえば、1 に設定します)。OpenGL ES や Metal などの一部のプラットフォームでは、シェーダーから書き込まれないときにポイントサイズを「未定義」として扱います。

詳細については、シェーダーセマンティクス に関するドキュメントを参照してください。

Direct3D シェーダーコンパイラーシンタックス

Direct3D プラットフォームは、Microsoft の HLSL シェーダーコンパイラー を使用します。HLSL コンパイラーは、さまざまな微妙なシェーダーエラーについて他のコンパイラーよりも厳密です。例えば、正しく初期化されていない関数出力値は受け入れません。

これを使用して実行する可能性のある最も一般的な状況は以下のとおりです。

  void vert (inout appdata_full v, out Input o) 
      {
        **UNITY_INITIALIZE_OUTPUT(Input,o);**
        // ...
      }
  • 部分的に初期化された値。例えば、関数は float4 を返しますが、コードはその .xyz 値のみを設定します。3 つの値だけが必要な場合は、すべての値を設定するか、float3 に変更します。

  • 頂点シェーダで tex2D を使用する場合。UV デリバティブが頂点シェーダーに存在しないため、これは無効です。代わりに、明示的なミップレベルをサンプリングする必要があります。例えば、 tex2Dlod (tex, float4(uv,0,0)) を使用します。また、tex2Dlod はシェーダーモデル 3.0 の機能なので #pragma target 3.0 を加える必要があります。

シェーダーの DirectX 11 (DX11) HLSL シンタックス

サーフェスシェーダー コンパイルパイプラインの一部は、DirectX 11 特有の HLSL (Microsoft’s shader language) シンタックスを理解しません。

StructuredBuffersRWTextures、その他の非 DirectX 9 シンタックスのような HLSL 関数を使用する場合は、この例のように DirectX X11 だけのプリプロセッサーマクロでそれらをラップします。

# ifdef SHADER_API_D3D11
// DirectX11 特有のコード。例えば
StructuredBuffer<float4> myColors;
RWTexture2D<float4> myRandomWriteTexture;
#endif

Shader framebuffer fetch の使用

一部の GPU (特に iOS の PowerVR ベースのもの) では、現在のフラグメントカラーをフラグメントシェーダーに入力することによって、プログラム可能なブレンディングを行うことができます (khronos.orgEXT_shader_framebuffer_fetch を参照)。

framebuffer fetch 機能を使用するシェーダーを Unity で書くことは可能です。これを行うには、フラグメントシェーダーを HLSL (Microsoft のシェーディング言語 - msdn.microsoft.com を参照) または Cg (Nvidia のシェーディング言語 - nvidia.co.uk を参照) のいずれかに書き込むときに inout の色の引数を使用します。

以下の例は Cg の場合です。

CGPROGRAM
//潜在的に可能なプラットフォーム (現在 gles、gles3、metal) の
//シェーダーのみをコンパイルします。
#pragma only_renderers framebufferfetch

void frag(v2f i、inout half4 ocol:SV_Target)
{
   //ocol は現在のフレームバッファカラーの読み込みと
   //... への書き込み (その色に変更します) が可能です。
}
ENDCG

シェーダーの深度 (Z) の向き

深度 (Z) の向きはシェーダープラットフォームによって異なります。

DirectX 11, DirectX 12, Metal: Reversed direction

  • 深度 (Z) バッファーは、ニアクリップ面で 1.0、ファークリップ面の 0.0 まで減少します。

  • クリップ空間範囲は、 [near,0] (つまり、ニアクリップ面の距離から減少し、ファークリップ面の 0.0 まで)。

他のプラットフォーム: 伝来の方向

  • 深度 (Z) バッファーは、ニアクリップ面で 0.0、ファークリップ面では 1.0 。

  • クリップ空間は特定のプラットフォームで異なります。
    • Direct3D のようなプラットフォームでは、範囲は [0,far] (つまり、ニアクリップ面の 0.0 から増加し、ファークリップ面の距離まで)。
    • OpenGL のようなプラットフォームでは、範囲は [-near,far] (つまり、マイナスの “ニアクリップ面の距離” から増加し、ファークリップ面の距離まで)。

浮動小数点深度バッファーと組み合わせた逆方向深度 (Z) は、元来の方向での深度バッファー精度を大幅に向上させます。 これの利点は、特に小さなニアクリップ面と大きなファークリップ面を使用する場合、Z 座標と影の矛盾が少なくなります。

そのため、逆方向深度 (Z) を使ったプラットフォームのシェーダーを使用する場合は

  • UNITY_REVERSED_Z が定義されます。
  • _CameraDepth Texture のテクスチャ範囲は 1 (ニア) から 0 (ファー) です。
  • クリップ空間範囲は “near” (ニア) から 0 (ファー) の範囲内です。

ただし、以下のマクロと関数は、自動的に深度 (Z) 方向の違いをすべて処理します。

  • Linear01Depth(float z)
  • LinearEyeDepth(float z)
  • UNITY_CALC_FOG_FACTOR(coord)

深度バッファーのフェッチ

深度 (Z) バッファーの値を手動でフェッチしている場合は、バッファーの方向を確認してください。以下は、その例です。

float z = tex2D(_CameraDepthTexture, uv);
#if defined(UNITY_REVERSED_Z)
    z = 1.0f - z;
#endif

クリップ空間の使用

クリップ空間の (Z) 深度を手動で使用している場合は、以下のマクロを使ってプラットフォームの違いを抽出してください。

float clipSpaceRange01 = UNITY_Z_0_FAR_FROM_CLIPSPACE(rawClipSpace);

Note: このマクロは、OpenGL または OpenGL ES プラットフォームのクリップ空間を変更しないため、これらのプラットフォームでは “-near”1 (ニア) から far (ファー) の範囲内で返します。

射影行列

GL.GetGPUProjectionMatrix() は、深度 (Z) が反転したプラットフォーム上では、Zが反転した行列を返します。ただし、投影行列から手動で作成する場合は (例えば、カスタムの影や深度レンダリングなど)、スクリプトを通して適用するために、深度 (Z) の方向を手動で反転する必要があります。

以下はその例です。

var shadowProjection = Matrix4x4.Ortho(...); //影のカメラ投影行列
var shadowViewMat = ...     //影のカメラビュー行列 matrix
var shadowSpaceMatrix = ... //クリップからシャドウマップのテクスチャ空間へ
    
//エンジンが、カメラ投影からデバイス投影の行列を計算する場合、 
//'m_shadowCamera.projectionMatrix' は暗示的に反転されます 
m_shadowCamera.projectionMatrix = shadowProjection; 

//'m_shadowMatrix' に追加される前に、'shadowProjection' は手動で反転されます 
//なぜなら、それはシェーダにとって他のマトリックスと同じに見えるからです
if(SystemInfo.usesReversedZBuffer) 
{
    shadowProjection[2, 0] = -shadowProjection[2, 0];
    shadowProjection[2, 1] = -shadowProjection[2, 1];
    shadowProjection[2, 2] = -shadowProjection[2, 2];
    shadowProjection[2, 3] = -shadowProjection[2, 3];
}
    m_shadowMatrix = shadowSpaceMatrix * shadowProjection * shadowViewMat;

深度 (Z) バイアス

Unity は自動的に深度 (Z) バイアスを処理して、Unity の深度 (Z) 方向に一致するようにします。ただし、ネイティブコードレンダリングプラグインを使用している場合は、C または C++ コードの深度 (Z) バイアスを打ち消す (反転) 必要があります。

深度 (Z) 方向を確認するツール

プラットフォームが逆の深度 (Z) 方向を使用しているかどうかを調べるには、SystemInfo.usesReversedZBuffer を使用します。

DX11/OpenGL コアテッセレーションを伴うサーフェスシェーダー
Understanding shader performance