こんにちは、エンジニアのオオバです。

お悩みさん
お悩みさん
  • Unityのシェーダーはなぜ1つで複数プラットフォーム対応できるの?
  • CGPROGRAMとHLSLPROGRAMの違いは?
  • 変換後のシェーダーはどうやって確認するの?
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    Unityのクロスプラットフォーム対応の仕組みが分かる

    UnityはiOS、Androidといった複数のプラットフォームに対して同時開発が可能です。シェーダーも例外ではありません。

    Unityのようなゲームエンジンがない状態では、iOS用、Android用と別々のシェーダーを書く必要があります。なぜならiOSとAndroidでは使用しているGPUが違い、シェーダー言語も異なるからです。

    本記事では、なぜUnityは1つのシェーダーだけで良いのか、その仕組みを解説していきます。

    👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!

    Unityは1つのシェーダーを各プラットフォーム向けに変換している

    結論から話すとUnityは1つのシェーダーファイルを各プラットフォームに合わせて変換しています。

    【Unity】1つのシェーダーで複数プラットフォーム対応できる理由_0

    次の表は代表的なプラットフォームと使用されているGPU APIです。

    プラットフォームとGPU APIの対応
    プラットフォームGPU API
    iOSMetal
    AndroidVulkan、OpenGL ES 3.0
    MacMetal
    WindowsDirectX 11 / 12

    GPUとは画面に表示するための演算ユニットです。GPUがシェーダーを解釈して画面にものを表示させます。つまりGPUの種類によってシェーダー言語を変える必要があるのです。

    Unityを使わず素の状態でシェーダーを書く場合は、iOS、Android向けとそれぞれシェーダーを書かなければなりません。非常に大変な作業です。Unityを使うことで、ひとつのシェーダーを書くだけで複数のプラットフォームに対応できます。

    Unityのシェーダー言語「ShaderLab」とHLSL

    UnityのシェーダーはShaderLabと呼ばれるUnity独自の記法で記述します。Unityでシェーダーを書く場合はShaderLabの構文に従う必要があります。

    ShaderLabの中にHLSLPROGRAMENDHLSLブロックを配置し、その中にHLSLコードを記述するという構成です。以前はCGPROGRAMENDCGというCg言語ベースの記法が使われていましたが、NVIDIAがCgのサポートを終了したため、現在はHLSLベースの記述が推奨されています。

    ※CGPROGRAM/ENDCGは互換性のために残っていますが、新規シェーダーはHLSLPROGRAM/ENDHLSLで書きましょう。

    HLSLで書いたシェーダーはビルド時に各プラットフォーム向けの言語に変換されます。たとえばOpenGL環境向けにはGLSLに変換されるイメージです。ここで言う「変換」とは、ShaderLabからターゲットとなる環境のシェーダー言語に自動変換されるという意味になります。

    シェーダーはランタイム中にもコンパイルされますが、この記事ではShaderLabから各プラットフォームへの変換を指して「変換」と表現しています。

    変換後のシェーダーを確認する方法

    各ターゲットに変換された状態はUnityエディタ上で確認できます。ピクセルを赤く描画するシンプルなシェーダーを用意しました。

    Shader "Simple"  
    {
        SubShader  
        {
            Pass  
            {
                HLSLPROGRAM  
                #pragma vertex vert  
                #pragma fragment frag  
                #include "UnityCG.cginc"  
    
                struct appdata  
                {
                    float4 vertex : POSITION;  
                };  
    
                struct v2f  
                {
                    float4 pos : SV_POSITION;  
                };  
    
                v2f vert(appdata v)  
                {
                    v2f o;  
                    o.pos = UnityObjectToClipPos(v.vertex);  
                    return o;  
                }
    
                half4 frag(v2f i) : SV_Target  
                {
                    return half4(1, 0, 0, 1);  
                }
                ENDHLSL  
            }
        }
    }
    

    HLSLPROGRAMENDHLSLで囲んだ中にHLSLコードを書きます。appdata構造体で頂点データを受け取り、v2f構造体で頂点シェーダーからフラグメントシェーダーへデータを渡す流れです。UnityObjectToClipPosでオブジェクト空間の座標をクリップ空間に変換し、フラグメントシェーダーでは赤色(1, 0, 0, 1)を返しています。

    シェーダーインスペクタから変換結果を確認する

    シェーダーファイルを選択してInspectorの「Compile and show code」ボタンを押すと、変換後のシェーダーを確認できます。

    【Unity】1つのシェーダーで複数プラットフォーム対応できる理由_1

    先のシェーダーをOpenGL ES環境に変換すると次のようになります。

    ※読みやすくするために整形しています。
    Shader "Simple"  
    {
      SubShader  
      {
        Pass  
        {
          GpuProgramID 16286  
          Program "vp"  
          {
            SubProgram "gles "  
            {
    "#version 100  
    
    #ifdef VERTEX
    attribute vec4 _glesVertex;  
    uniform highp mat4 glstate_matrix_mvp;  
    void main ()  
    {
      gl_Position = (glstate_matrix_mvp * _glesVertex);  
    }
    #endif
    
    #ifdef FRAGMENT
    void main ()  
    {
      gl_FragData[0] = vec4(1.0, 0.0, 0.0, 1.0);  
    }
    #endif
            "  
            }
          }
        }
      }
    }
    

    変換前後のコードを比較すると面白い

    変換されたコードを読んでみると、元のHLSLと同じような処理ですがところどころ違いがあります。フラグメントシェーダーを比較してみましょう。

    HLSLのフラグメントシェーダー
    half4 frag(v2f i) : SV_Target  
    {
        return half4(1, 0, 0, 1);  
    }
    
    GLSL ESのフラグメントシェーダー
    void main ()  
    {
      gl_FragData[0] = vec4(1.0, 0.0, 0.0, 1.0);  
    }
    

    HLSLではhalf4(1, 0, 0, 1)と整数表記ですが、GLSL ESではvec4(1.0, 0.0, 0.0, 1.0)と小数点表記に変換されています。関数名もfragからmainに変わり、戻り値の書き方もgl_FragData[0]への代入に変わっています。Unityの変換処理が各ターゲットの仕様に合わせて最適な形に変換していることが分かります。

    👉 【Unity】シェーダーが2.5倍早くなる方法

    変換済みシェーダーのデバッグ活用

    変換済みのシェーダーはゲーム開発中のデバッグに役立ちます。特に実機テストで描画不具合に遭遇したときに力を発揮する手法です。

    たとえばAndroid端末だけ描画がおかしい場合、次のようなフローで調査を進めます。

    Android端末で描画不具合が発生した場合の調査フロー
    • ①ShaderLab(HLSL)のコードにミスはないか確認
    • ②iOSでは正常に描画されているか確認
    • ③Androidだけ異常な場合はAndroid向け変換シェーダーを確認
    • ④GLSL変換後のコードに問題がないかチェック

    このフローで変換後のシェーダーを読むことで、プラットフォーム固有の描画問題を特定できます。描画周りの開発に携わる開発者は覚えておくと役立つテクニックです。

    Shader Graphという選択肢もある

    ShaderLabをコードで書く以外にShader Graphという選択肢もあります。Shader Graphはプログラムを書かずにノードベースでシェーダーを作成できるUnityの機能です。

    シェーダーの処理フローを視覚的に理解できるため、シェーダー入門としてShader Graphから始めるのもオススメの方法。Shader Graphで作成したシェーダーも同様に、Unityが各プラットフォーム向けに自動変換してくれます。コードを書くのに抵抗がある方はまずShader Graphから触ってみてください。

    まとめ

    Unityのシェーダーが各プラットフォームへ変換される仕組みについて解説してきました。

    この記事のまとめ
    • UnityのシェーダーはShaderLabという独自記法で書く
    • ShaderLabの中でHLSLコードを記述する(HLSLPROGRAM/ENDHLSL)
    • 旧来のCGPROGRAM/ENDCGは非推奨で新規シェーダーはHLSLで書く
    • ビルド時に各プラットフォーム向けのシェーダー言語に自動変換される
    • 変換後のシェーダーはInspectorから確認でき描画デバッグに活用可能

    Unityを使えば複数のプラットフォームに対し1つのシェーダーで対応できます。ものづくりに集中できる時間が増えるという点が最大のメリットです。

    Unityがなければ、iOS・Android向けにひとつずつ「移植作業」が必要になります。移植元に修正が入るたびに移植作業が発生するうえ、移植作業そのものは製品の質を上げる作業ではありません。Unityを使えば1つのソースを変更するだけで全プラットフォームを更新できます。

    開発効率を上げるという意味でもShaderLabを覚えるメリットは大きいです。ぜひシェーダーを書けるようになって面白い表現を作っていきましょう。

    Unityのシェーダー・3Dプログラミングを基礎から学びたい方はこちらの記事がおすすめです。

    Unityオブジェクトの描画順の制御って難しいですよね。
    この度、Unityの描画順を体系的に学べる「Unity描画順の教科書」を執筆しました。

    Unityの描画順を基礎から学びたい方はぜひ確認してみてください!
    Unity描画順の教科書

    最後まで読んでいただきありがとうございました!
    すばらしいシェーダーライフをお過ごしください。

    オススメ記事