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]というボタンが有りますので、これをクリックしてコンパイル済みのシェーダを開きます。
f:id:IARIKE:20130907153232j:plain

 すると、次のようなコードが現れます(ほとんどコメントアウトされていますので、下のコードではコメントを解除しています。)
 これを見ると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に標準で搭載されているシェーダの中身を見て勉強していきたいと思います。