【Unity Shaders】初探Surface Shader背后的机制
转载请注明出处:http://blog.csdn.net/candycat1992/article/details/39994049
写在前面
一直以来,Unity Surface Shader背后的机制一直是初学者为之困惑的地方。Unity Surface Shader在Unity 3.0的时候被开放给公众使用,其宣传手段也是号称让所有人都可以轻松地写shader。但由于资料缺乏,很多人知其然不知其所以然,无法理解Unity Surface Shader在背后为我们做了哪些事情。
前几天一直被问到一个问题,为什么我的场景里没有灯光,但物体不是全黑的呢?为什么我把Light的颜色调成黑色,物体还是有一些默认颜色呢?这些问题其实都是因为那些物体使用了Surface Shader的缘故。因此,了解Surface Shader背后的机制是非常重要滴~
虽然Surface Shader一直是一个神秘的存在,但其实Unity给了我们揭开她面纱的方式:查看它生成的CG代码。大家应该都知道,所谓的Surface Shader实际上是封装了CG语言,隐藏了很多光照处理的细节,它的设计初衷是为了让用户仅仅使用一些指令(#pragma)就可以完成很多事情,并且封装了很多常用的光照模型和函数,例如Lambert、Blinn-Phong等。而查看Surface Shader生成的代码也很简单:在每个编译完成的Surface Shader的面板上,都有个“Show generated code”的按钮,像下面这样:
点开后,就可以查看啦~面板上还表明了很多其他的有用信息。而这些方便的功能实际上是Unity 4.5发布出来的。详情可见这篇博文。
使用Surface Shader,很多时候,我们只需要告诉shader,“嘿,使用这些纹理去填充颜色,法线贴图去填充法线,使用Lambert光照模型,其他的不要来烦我!!!”我们不需要考虑是使用forward还是deferred rendering,有多少光源类型、怎样处理这些类型,每个pass需要处理多少个光源!!!(人们总会rant写一个shader是多么的麻烦。。。)So!Unity说,不要急,放着我来~
上面的情景当然对于小白是比较简单的方式,Surface Shader可以让初学者快速实现很多常见的shader,例如漫反射、高光反射、法线贴图等,这些常见的效果也都不错。而对应面就是,由于隐藏了很多细节,如果想要自定义一些比较复杂或特殊的效果,使用Surface Shader就无法达到了(或者非常麻烦)。在学了一段时间的Surface Shader后,我认为:
- 如果你从来没有学习过怎样编写shader,而又想写一些常见的、比较简单的shader,那仅学习Surface Shader是一个不错的选择。
- 如果你向往那些高品质的游戏画面,那么Surface Shader是远远无法满足你的,而且某种方面来说它会让你变得越来越困惑。
流水线
两个结构体
- struct SurfaceOutput {
- half3 Albedo;
- half3 Normal;
- half3 Emission;
- half Specular;
- half Gloss;
- half Alpha;
- };
- Albedo:我们通常理解的对光源的反射率。它是通过在Fragment Shader中计算颜色叠加时,和一些变量(如vertex lights)相乘后,叠加到最后的颜色上的。
- Normal:即其对应的法线方向。只要是受法线影响的计算都会受到影响。
- Emission:自发光。会在Fragment 最后输出前(调用final函数前,如果定义了的话),使用下面的语句进行简单的颜色叠加:
- c.rgb += o.Emission;
- Specular:高光反射中的指数部分的系数。影响一些高光反射的计算。按目前的理解,也就是在光照模型里会使用到(如果你没有在光照函数等函数——包括Unity内置的光照函数,中使用它,这个变量就算设置了也没用)。有时候,你只在surf函数里设置了它,但也会影响最后的结果。这是因为,你可能使用了Unity内置的光照模型,如BlinnPhong,它会使用如下语句计算高光反射的强度(在Lighting.cginc里):
- float spec = pow (nh, s.Specular*128.0) * s.Gloss;
- Gloss:高光反射中的强度系数。和上面的Specular类似,一般在光照模型里使用。
- Alpha:通常理解的透明通道。在Fragment Shader中会直接使用下列方式赋值(如果开启了透明通道的话):
- c.a = o.Alpha;
编译指令
- #pragma surface surfaceFunction lightModel [optionalparams]
Surface Shader和CG其他部分一样,代码也是要写在CGPROGRAM和ENDCG之间。但区别是,它必须写在SubShader内部,而不能写在Pass内部。Surface Shader自己会自动生成所需的各个Pass。由上面的编译格式可以看出,surfaceFunction和lightModel是必须指定的,而且是可选部分。
- void surf (Input IN, inout SurfaceOutput o)
即Input是输入,SurfaceOutput是输出。
- 直接将CGPROGRAM和ENDCG之间的代码复制过来(其实还是更改了一些编译指令),这些代码包括了我们对Input、surfaceFuntion、LightingXXX等变量和函数的定义。这些函数和变量会在之后的处理过程中当成普通的结构体和函数进行调用,就和在C++中我们会在main函数中调用某些函数一样;
- 分析上述代码,生成v2f_surf结构,用于在Vertex Shader和Fragment Shader之间进行数据传递。Unity会分析我们在四个自定义函数中所使用的变量,例如纹理坐标等。如果需要,它会在v2f_surf中生成相应的变量。而且,即便有时我们在Input中定义了某些变量(如某些纹理坐标),但Unity在分析后续代码时发现我们并没有使用这些变量,那么这些变量实际上是不会在v2f_surf中生成的。这也就是说,Unity做了一些优化动作。
- 生成Vertex Shader。
* 如果我们自定义了VertexFunction,Unity会在这里首先调用VertexFunction修改顶点数据;然后分析VertexFunction修改的数据,最后通过Input结构体将修改结果存储到v2f_surf中。
* 计算v2f_surf中其他默认的变量值。这主要包括了pos、纹理坐标、normal(如果没有使用LightMap)、vlight(如果没有使用LightMap)、lmap(如果使用LightMap)等。
* 最后,通过内置的TRANSFER_VERTEX_TO_FRAGMENT指令将v2f_surf传递给下面的Fragment Shader。 - 生成Fragment Shader。
* 使用v2f_surf中的对应变量填充Input结构,例如一些纹理坐标等。
* 调用surfFuntion填充SurfaceOutput结构。
* 调用LightingXXX函数得到初始的颜色值。
* 进行其他的颜色叠加。如果没有启用LightMap,这里会使用SurfaceOutput.Albedo和v2f_surf.vlight的乘积和原颜色值进行叠加;否则会进行一些更复杂的颜色叠加。
* 最后,如果自定了final函数,则调用它进行最后额颜色修改。
代码分析
- Shader "Custom/BasicDiffuse" {
- Properties {
- _EmissiveColor ("Emissive Color", Color) = (1,1,1,1)
- _AmbientColor ("Ambient Color", Color) = (1,1,1,1)
- _MySliderValue ("This is a Slider", Range(0,10)) = 2.5
- _RampTex ("Ramp Texture", 2D) = "white"{}
- }
- SubShader {
- Tags { "RenderType"="Opaque" "RenderType"="Opaque" }
- LOD 200
- CGPROGRAM
- #pragma surface surf BasicDiffuse vertex:vert finalcolor:final noforwardadd
- #pragma debug
- float4 _EmissiveColor;
- float4 _AmbientColor;
- float _MySliderValue;
- sampler2D _RampTex;
- struct Input
- {
- float2 uv_RampTex;
- float4 vertColor;
- };
- void vert(inout appdata_full v, out Input o)
- {
- o.vertColor = v.color;
- }
- void surf (Input IN, inout SurfaceOutput o)
- {
- float4 c;
- c = pow((_EmissiveColor + _AmbientColor), _MySliderValue);
- o.Albedo = c.rgb + tex2D(_RampTex, IN.uv_RampTex).rgb;
- o.Alpha = c.a;
- }
- inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
- {
- float difLight = max(0, dot (s.Normal, lightDir));
- float hLambert = difLight * 0.5 + 0.5;
- float3 ramp = tex2D(_RampTex, float2(hLambert)).rgb;
- float4 col;
- col.rgb = s.Albedo * _LightColor0.rgb * (ramp) * atten;
- col.a = s.Alpha;
- return col;
- }
- void final(Input IN, SurfaceOutput o, inout fixed4 color) {
- color = color * 0.5 + 0.5;
- }
- ENDCG
- }
- FallBack "Diffuse"
- }
它包含了全部四个函数,以及一些比较常见的运算。为了只关注一个Pass,我添加了noforwardadd指令。它所得到的渲染结果不重要(事实上我只是在BasicDiffuse上瞎改了一些。。。)
- Shader "Custom/BasicDiffuse_Gen" {
- Properties {
- _EmissiveColor ("Emissive Color", Color) = (1,1,1,1)
- _AmbientColor ("Ambient Color", Color) = (1,1,1,1)
- _MySliderValue ("This is a Slider", Range(0,10)) = 2.5
- _RampTex ("Ramp Texture", 2D) = "white"{}
- }
- SubShader {
- Tags { "RenderType"="Opaque" "RenderType"="Opaque" }
- LOD 200
- // ------------------------------------------------------------
- // Surface shader code generated out of a CGPROGRAM block:
- // ---- forward rendering base pass:
- Pass {
- Name "FORWARD"
- Tags { "LightMode" = "ForwardBase" }
- CGPROGRAM
- // compile directives
- #pragma vertex vert_surf
- #pragma fragment frag_surf
- #pragma multi_compile_fwdbase nodirlightmap
- #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
- // Original surface shader snippet:
- #line 11 ""
- #ifdef DUMMY_PREPROCESSOR_TO_WORK_AROUND_HLSL_COMPILER_LINE_HANDLING
- #endif
- //#pragma surface surf BasicDiffuse vertex:vert finalcolor:final noforwardadd
- #pragma debug
- float4 _EmissiveColor;
- float4 _AmbientColor;
- float _MySliderValue;
- sampler2D _RampTex;
- struct Input
- {
- float2 uv_RampTex;
- float4 vertColor;
- };
- void vert(inout appdata_full v, out Input o)
- {
- o.vertColor = v.color;
- }
- void surf (Input IN, inout SurfaceOutput o)
- {
- float4 c;
- c = pow((_EmissiveColor + _AmbientColor), _MySliderValue);
- o.Albedo = c.rgb + tex2D(_RampTex, IN.uv_RampTex).rgb;
- o.Alpha = c.a;
- }
- inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
- {
- float difLight = max(0, dot (s.Normal, lightDir));
- float hLambert = difLight * 0.5 + 0.5;
- float3 ramp = tex2D(_RampTex, float2(hLambert)).rgb;
- float4 col;
- col.rgb = s.Albedo * _LightColor0.rgb * (ramp);
- col.a = s.Alpha;
- return col;
- }
- void final(Input IN, SurfaceOutput o, inout fixed4 color) {
- color = color * 0.5 + 0.5;
- }
- // vertex-to-fragment interpolation data
- #ifdef LIGHTMAP_OFF
- struct v2f_surf {
- float4 pos : SV_POSITION;
- float2 pack0 : TEXCOORD0;
- float4 cust_vertColor : TEXCOORD1;
- fixed3 normal : TEXCOORD2;
- fixed3 vlight : TEXCOORD3;
- // LIGHTING_COORDS在AutoLight.cginc里定义
- // 本质上就是一个#define指令
- // e.g.
- // #define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)
- // #define SHADOW_COORDS(idx1) float3 _ShadowCoord : TEXCOORD##idx1;
- LIGHTING_COORDS(4,5)
- };
- #endif
- #ifndef LIGHTMAP_OFF
- struct v2f_surf {
- float4 pos : SV_POSITION;
- float2 pack0 : TEXCOORD0;
- float4 cust_vertColor : TEXCOORD1;
- float2 lmap : TEXCOORD2;
- LIGHTING_COORDS(3,4)
- };
- #endif
- #ifndef LIGHTMAP_OFF
- float4 unity_LightmapST;
- #endif
- // 定义所需的纹理坐标
- float4 _RampTex_ST;
- // vertex shader
- v2f_surf vert_surf (appdata_full v) {
- v2f_surf o;
- // 使用自定义的vert函数填充Input结构
- Input customInputData;
- vert (v, customInputData);
- // 再赋值给真正所需的v2f_surf结构
- o.cust_vertColor = customInputData.vertColor;
- o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
- // 将顶点的纹理坐标转换到纹理对应坐标
- o.pack0.xy = TRANSFORM_TEX(v.texcoord, _RampTex);
- #ifndef LIGHTMAP_OFF
- // 如果启用了LightMap,则计算对应的LightMap坐标
- o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
- #endif
- // 计算世界坐标系中法线的方向
- // SCALED_NORMAL在UnityCG.cginc里定义
- // 本质上就是一个#define指令
- // #define SCALED_NORMAL (v.normal * unity_Scale.w)
- float3 worldN = mul((float3x3)_Object2World, SCALED_NORMAL);
- // 如果没有开启LightMap,
- // 顶点法线方向就是worldN
- #ifdef LIGHTMAP_OFF
- o.normal = worldN;
- #endif
- // SH/ambient and vertex lights
- #ifdef LIGHTMAP_OFF
- // 如果没有开启LightMap,
- // vertex lights就是球面调和函数的结果
- // 球面调和函数ShadeSH9在UnityCG.cginc里定义
- float3 shlight = ShadeSH9 (float4(worldN,1.0));
- o.vlight = shlight;
- // unity_4LightPosX0等变量在UnityShaderVariables.cginc里定义
- #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
- // pass lighting information to pixel shader
- // TRANSFER_VERTEX_TO_FRAGMENT在AutoLight.cginc里定义,
- // 本质上就是一个#define指令
- // 用于转换v2f_surf中的_LightCoord和_ShadowCoord
- TRANSFER_VERTEX_TO_FRAGMENT(o);
- return o;
- }
- #ifndef LIGHTMAP_OFF
- sampler2D unity_Lightmap;
- #ifndef DIRLIGHTMAP_OFF
- sampler2D unity_LightmapInd;
- #endif
- #endif
- // fragment shader
- fixed4 frag_surf (v2f_surf IN) : SV_Target {
- // prepare and unpack data
- #ifdef UNITY_COMPILER_HLSL
- Input surfIN = (Input)0;
- #else
- Input surfIN;
- #endif
- // 使用v2f_surf中的变量给Input中的纹理坐标进行赋值
- surfIN.uv_RampTex = IN.pack0.xy;
- surfIN.vertColor = IN.cust_vertColor;
- #ifdef UNITY_COMPILER_HLSL
- SurfaceOutput o = (SurfaceOutput)0;
- #else
- SurfaceOutput o;
- #endif
- // 初始化SurfaceOutput结构
- 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
- // call surface function
- // 调用自定义的surf函数填充SurfaceOutput结构
- surf (surfIN, o);
- // compute lighting & shadowing factor
- // LIGHT_ATTENUATION在AutoLight.cginc里定义,
- // 本质上就是一个#define指令
- // 用于计算光衰减
- fixed atten = LIGHT_ATTENUATION(IN);
- fixed4 c = 0;
- // realtime lighting: call lighting function
- #ifdef LIGHTMAP_OFF
- // 如果没有开启LightMap,
- // 调用自定义的LightXXX函数,
- // 使用填充好的SurfaceOutput等变量作为参数,
- // 得到初始的像素值
- c = LightingBasicDiffuse (o, _WorldSpaceLightPos0.xyz, atten);
- #endif // LIGHTMAP_OFF || DIRLIGHTMAP_OFF
- #ifdef LIGHTMAP_OFF
- // 如果没有开启LightMap,
- // 向像素叠加vertex light的光照颜色
- c.rgb += o.Albedo * IN.vlight;
- #endif // LIGHTMAP_OFF
- // lightmaps:
- #ifndef LIGHTMAP_OFF
- // 计算LightMap,这部分不懂
- #ifndef DIRLIGHTMAP_OFF
- // directional lightmaps
- 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;
- #else // !DIRLIGHTMAP_OFF
- // single lightmap
- fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy);
- fixed3 lm = DecodeLightmap (lmtex);
- #endif // !DIRLIGHTMAP_OFF
- // combine lightmaps with realtime shadows
- #ifdef SHADOWS_SCREEN
- #if (defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)) && 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
- // 给Alpha通道赋值
- c.a = o.Alpha;
- #endif // LIGHTMAP_OFF
- // 调用自定义的final函数,
- // 对像素值进行最后的更改
- final (surfIN, o, c);
- return c;
- }
- ENDCG
- }
- // ---- end of surface shader generated code
- #LINE 57
- }
- FallBack "Diffuse"
- }
其中比较重要的部分我都写了注释。
一些问题
- #ifdef LIGHTMAP_OFF
- // 如果没有开启LightMap,
- // 向像素叠加vertex light的光照颜色
- c.rgb += o.Albedo * IN.vlight;
- #endif // LIGHTMAP_OFF
而IN.vlight是在Vertex Shader中计算的:
- // 如果没有开启LightMap,
- // vertex lights就是球面调和函数的结果
- // 球面调和函数ShadeSH9在UnityCG.cginc里定义
- float3 shlight = ShadeSH9 (float4(worldN,1.0));
- o.vlight = shlight;
我们可以去查看ShadeSH9函数的实现:
- // normal should be normalized, w=1.0
- half3 ShadeSH9 (half4 normal)
- {
- half3 x1, x2, x3;
- // Linear + constant polynomial terms
- x1.r = dot(unity_SHAr,normal);
- x1.g = dot(unity_SHAg,normal);
- x1.b = dot(unity_SHAb,normal);
- // 4 of the quadratic polynomials
- half4 vB = normal.xyzz * normal.yzzx;
- x2.r = dot(unity_SHBr,vB);
- x2.g = dot(unity_SHBg,vB);
- x2.b = dot(unity_SHBb,vB);
- // Final quadratic polynomial
- float vC = normal.x*normal.x - normal.y*normal.y;
- x3 = unity_SHC.rgb * vC;
- return x1 + x2 + x3;
- }
它是一个球面调和函数,但unity_SHAr这些变量具体是什么我还不清楚。。。如果有人知道麻烦告诉我一下,不胜感激~但是,这些变量是和Unity使用了一个全局环境光(你可以在Edit->RenderSettings->Ambient Light中调整)有关。如果把这个环境光也调成黑色,那么场景就真的全黑了。