Unityシェーダ入門 #003 #pragma debug
Unityシェーダ入門3回目です。
今回はUnityが標準で作成するサーフェイスシェーダと、頂点シェーダ、フラグメントシェーダの関係を説明します。
#pragma debug
前回説明したコードの中に#pragmaというのが有りました。これはコンパイラに命令を出すためのキーワードでした。これを使ってdebugという命令をコンパイラに与えると、コンパイル済みのシェーダを覗き見ることが出来ます。早速やってみます。
Shader "Custom/FirstShader2" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert #pragma debug // <------------------------ ここに追加しました sampler2D _MainTex; struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
保存してUnity上に戻ると、コンパイルが行われます。今回作成したシェーダを選択し、インスペクタウィンドウを見ると[Open compiled shader]というボタンが有りますので、これをクリックしてコンパイル済みのシェーダを開きます。
すると、次のようなコードが現れます(ほとんどコメントアウトされていますので、下のコードではコメントを解除しています。)
これを見るとUnityのコンパイラが、サーフェイスシェーダに、複雑な頂点シェーダとフラグメントシェーダ(ピクセルシェーダ)を追加しているのがわかると思います。
サーフェイスシェーダはピクセルの色を設定しているだけで、実際のライティング計算などは予め用意された頂点シェーダとフラグメントシェーダが行っています。
Shader "Custom/FirstShader2" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 /* surface debug info: */ /* surface debug info: */ Pass { Name "FORWARD" Tags { "LightMode" = "ForwardBase" } // shader source for this pass: CGPROGRAM #pragma vertex vert_surf // vert_surf でvert_surfという関数を頂点シェーダとして使用する、とコンパイラに指定しています。 #pragma fragment frag_surf // frag_surfという関数をフラグメントシェーダとして使用する、とコンパイラに指定しています。 #pragma fragmentoption ARB_precision_hint_fastest #pragma multi_compile_fwdbase #include "HLSLSupport.cginc" #include "UnityShaderVariables.cginc" #define UNITY_PASS_FORWARDBASE #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" #define INTERNAL_DATA #define WorldReflectionVector(data,normal) data.worldRefl #define WorldNormalVector(data,normal) normal #line 1 #line 10 //#pragma surface surf Lambert #pragma debug // <------------------------ ここに追加しました sampler2D _MainTex; struct Input { float2 uv_MainTex; }; // // ここにSurfaceシェーダがあります! // void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; } #ifdef LIGHTMAP_OFF struct v2f_surf { float4 pos : SV_POSITION; float2 pack0 : TEXCOORD0; fixed3 normal : TEXCOORD1; fixed3 vlight : TEXCOORD2; LIGHTING_COORDS(3,4) }; #endif #ifndef LIGHTMAP_OFF struct v2f_surf { float4 pos : SV_POSITION; float2 pack0 : TEXCOORD0; float2 lmap : TEXCOORD1; LIGHTING_COORDS(2,3) }; #endif #ifndef LIGHTMAP_OFF float4 unity_LightmapST; #endif float4 _MainTex_ST; // // 頂点シェーダvert_surfがここにあります // v2f_surf vert_surf (appdata_full v) { v2f_surf o; // 以下座標変換や、頂点単位でのライティング計算が行われています。 o.pos = mul (UNITY_MATRIX_MVP, v.vertex); o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex); #ifndef LIGHTMAP_OFF o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw; #endif float3 worldN = mul((float3x3)_Object2World, SCALED_NORMAL); #ifdef LIGHTMAP_OFF o.normal = worldN; #endif #ifdef LIGHTMAP_OFF float3 shlight = ShadeSH9 (float4(worldN,1.0)); o.vlight = shlight; #ifdef VERTEXLIGHT_ON float3 worldPos = mul(_Object2World, v.vertex).xyz; o.vlight += Shade4PointLights ( unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, unity_4LightAtten0, worldPos, worldN ); #endif // VERTEXLIGHT_ON #endif // LIGHTMAP_OFF TRANSFER_VERTEX_TO_FRAGMENT(o); return o; } #ifndef LIGHTMAP_OFF sampler2D unity_Lightmap; #ifndef DIRLIGHTMAP_OFF sampler2D unity_LightmapInd; #endif #endif // // フラグメント(ピクセル)シェーダ frag_surfがここにあります。 // fixed4 frag_surf (v2f_surf IN) : COLOR { Input surfIN; surfIN.uv_MainTex = IN.pack0.xy; #ifdef UNITY_COMPILER_HLSL SurfaceOutput o = (SurfaceOutput)0; #else SurfaceOutput o; #endif o.Albedo = 0.0; o.Emission = 0.0; o.Specular = 0.0; o.Alpha = 0.0; o.Gloss = 0.0; #ifdef LIGHTMAP_OFF o.Normal = IN.normal; #endif surf (surfIN, o); // ここでサーフェイスシェーダを呼び出しています! // 以下複雑なライティング計算が行われています fixed atten = LIGHT_ATTENUATION(IN); fixed4 c = 0; #ifdef LIGHTMAP_OFF c = LightingLambert (o, _WorldSpaceLightPos0.xyz, atten); #endif // LIGHTMAP_OFF #ifdef LIGHTMAP_OFF c.rgb += o.Albedo * IN.vlight; #endif // LIGHTMAP_OFF #ifndef LIGHTMAP_OFF #ifdef DIRLIGHTMAP_OFF fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy); fixed3 lm = DecodeLightmap (lmtex); #else fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy); fixed4 lmIndTex = tex2D(unity_LightmapInd, IN.lmap.xy); half3 lm = LightingLambert_DirLightmap(o, lmtex, lmIndTex, 0).rgb; #endif #ifdef SHADOWS_SCREEN #if defined(SHADER_API_GLES) && defined(SHADER_API_MOBILE) c.rgb += o.Albedo * min(lm, atten*2); #else c.rgb += o.Albedo * max(min(lm,(atten*2)*lmtex.rgb), lm*atten); #endif #else // SHADOWS_SCREEN c.rgb += o.Albedo * lm; #endif // SHADOWS_SCREEN c.a = o.Alpha; #endif // LIGHTMAP_OFF // 最終的な色(陰影がついた後の)が出力されます return c; } ENDCG -以下略ー
ソースコードの先の方を見ると、何だかよくわからないアセンブリになっていると思います。実際にGPUが実行するのはこのコード(の機械語)です。
Unityは開発者が書いたサーフェイスシェーダに対し、適切な頂点シェーダとフラグメント(ピクセル)シェーダを追加し、独立したシェーダとして完成させます。それをコンパイラに渡してGPUが実行可能なアセンブリを作成しています。
#pragma debugを付けてコンパイルすると、このUnityが作成した最終的なコード(アセンブリになる前の)をコメントとして見ることが出来るということになります。
このソースコードを見て、頂点シェーダやフラグメント(ピクセル)シェーダが存在することはわかりましたが、それ以外はごちゃごちゃしていてよくわかりません。複雑なライティング計算や、条件による分岐が入っているためです。
Unity側はこの複雑なコードを開発者に書かせない(触れさせない)ために、サーフェイスシェーダという関数を呼び出す仕組みを用意してブラックボックス化しています。
次回からは、一旦頂点シェーダとフラグメントシェーダのことはキッパリ忘れて、Unityに標準で搭載されているシェーダの中身を見て勉強していきたいと思います。