Unity Shaders初探Surface Shader背后的机制
Posted jzdwajue
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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生成的代码也非常easy:在每一个编译完毕的Surface Shader的面板上,都有个“Show generated code”的button。像以下这样:
点开后。就能够查看啦~面板上还表明了非常多其它的实用信息。
而这些方便的功能实际上是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是远远无法满足你的,并且某种方面来说它会让你变得越来越困惑。
流水线
两个结构体
这些变量仅仅有在真正使用的时候才会被计算生成。比方。在某些Pass里生成而某些就生成。
我们无法自己定义这个结构体内的变量。关于它最难理解的也就是每一个变量的详细含义以及工作机制(对像素颜色的影响)。
我们来看一下它的定义(在Lighting.cginc里面):
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是输出。
因为Unity内置了一些光照函数——Lambert(diffuse)和Blinn-Phong(specular),因此这里在默认情况下会使用内置的Lambert模型。
当然我们也能够自己定义。
这里。我们仅仅关注可指定的函数。其它可去官网自行查看。除了上述的surfaceFuntion和lightModel,我们还能够自己定义两种函数:vertex:VertexFunction和finalcolor:ColorFunction。
也就是说,Surface Shader同意我们自己定义四种函数。
- 直接将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" }
当中比較重要的部分我都写了凝视。
一些问题
前面说过,它使用LightingXXX对颜色值进行初始化。但后面还进行了一系列颜色叠加计算。
当中,在没有使用LightMap的情况下。Unity还计算了vertex lights对颜色的影响,也就是以下这句话:
#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中调整)有关。
假设把这个环境光也调成黑色。那么场景就真的全黑了。
假设你真的去看那些在UnityCG.cginc、AutoLight.cginc等文件中的关于指令的定义,能够发现Unity是依据定义的光照类型来处理不同的光照的。
这部分还没有搞明确,后面会继续探究一下的!
以上是关于Unity Shaders初探Surface Shader背后的机制的主要内容,如果未能解决你的问题,请参考以下文章
Unity Shaders and Effects Cookbook (7-1) 在Surface Shader 中 访问 顶点颜色