[Unity Shaders] explores the mechanism behind Surface Shader, shadersshader
Reprinted please indicate the source: http://blog.csdn.net/candycat1992/article/details/39994049
Preface
The mechanism behind Unity Surface Shader has always been a bit confusing for beginners. Unity Surface Shader is open to the public when it is in Unity 3.0. Its propaganda means also claim to allow everyone to easily write shader. However, due to lack of information, many people do not know why, and they cannot understand what Unity Surface Shader has done for us.
I was asked a question a few days ago. Why is there no light in my scenario, but the object is not all black? Why do I change the Light color to black and the objects still have some default colors? These problems are actually because those objects use Surface Shader. Therefore, it is very important to understand the mechanisms behind the Surface Shader ~
Although Surface Shader has always been a mysterious existence, Unity actually gives us the way to unveil it: view the CG code it generates. We should all know that the so-called Surface Shader actually encapsulates the CG language and hides a lot of detailed lighting processing details. It was designed to allow users to use only some commands (# pragma) it can accomplish many things and encapsulate many common illumination models and functions, such as Lambert and Blinn-Phong. The code generated by viewing the Surface Shader is also very simple: on the panel of each compiled Surface Shader, there is a "Show generated code" button, as shown below:
Click it to view it ~ The Panel also shows a lot of other useful information. These convenient functions are actually released by Unity 4.5. For more information, see this blog post.
When using Surface Shader, we often only need to tell shader, "Hey, use these textures to fill the color, use the normal map to fill the normal, and use the Lambert illumination model, don't bother me with others !!!" We don't need to consider whether to use forward or deferred rendering. How many light sources are there and how to handle these types? How many light sources need to be processed for each pass !!! (People will always rant How troublesome it is to write a shader ...) So! Unity said, don't worry. Let me go ~
The above scenario is of course relatively simple for Tom. Surface Shader allows beginners to quickly implement many common shader, such as diffuse reflection, high-gloss reflection, and normal textures, these common effects are also good. The corresponding Surface is that because many details are hidden, if you want to customize complex or special effects, you cannot use the Surface Shader (or it is very troublesome ). After learning the Surface Shader for a while, I think:
- If you have never learned how to write a shader and want to writeCommon and simple shaderLearning Surface Shader is a good choice.
- If you look forward to those high-quality game images, the Surface Shader is far from satisfying you, and in some ways it will make you more and more confused.
What should I do if I am confused? Learn mainstream rendering languages honestly ~ For example, CG, GLSL, and HLSL. After learning some of the above content, let's look back at the Surface Shader.
With so many lectures, the main purpose of this article is to analyze the things behind the Surface Shader! That is, to analyze how the Surface Shader parses the functions such as surf and LightingXXX, and how to obtain the pixel color. So, let's get started!
Assembly Line
First, we need to understand what features Surface Shader supports. For more information, see the official website.
The most important part of Surface Shader is
Two struct and its compilation commands.
Two struct
The two struct types are struct Input and SurfaceOutput. The Input struct can be customized. It can contain texture coordinates and other pre-defined variables, such as view ction (float3 viewDir), world space position (worldPos), and world space reflection vector (float3 worldRefl. These variables are only available in
In real useIs calculated and generated. For example, some are generated in some Pass.
Another struct is SurfaceOutput. We cannot customize the variables in this structure. What is hard to understand about it is the specific meaning of each variable and its working mechanism (the effect on pixel color ). Let's take a look at its definition (in Lighting. cginc ):
struct SurfaceOutput { half3 Albedo; half3 Normal; half3 Emission; half Specular; half Gloss; half Alpha;};
The above conclusions are obtained by analyzing the generated code. If any, you are welcome to point it out. If you don't understand it, you can analyze the generated code like this. Generally, you can understand the problem ~
Compile command
The general format of the Compilation instruction is as follows:
#pragma surface surfaceFunction lightModel [optionalparams]
Like other parts of the CG, the Surface Shader code must be written between CGPROGRAM and ENDCG. But the difference is that it must be written inside the SubShader, rather than inside the Pass. Surface Shader automatically generates the required Pass. The preceding compilation format shows that surfaceFunction and lightModel must be specified and are optional.
SurfaceFunction is usually a function named "surf" (the function name can be arbitrary). Its function format is fixed:
void surf (Input IN, inout SurfaceOutput o)
That is, Input is Input, and SurfaceOutput is output.
LightModel is also required. Since Unity has built-in lighting functions-Lambert (diffuse) and Blinn-Phong (specular), the built-in Lambert model is used by default. Of course, we can also customize it.
Optionalparams contains many available command types, including enabling and disabling some statuses, setting the generated Pass type, and specifying optional functions. Here, we only focus on the functions that can be specified, and others can be viewed on the official website. In addition to surfaceFuntion and lightModel, we can also customize two types of functions: vertex: VertexFunction and finalcolor: ColorFunction. That is to say, Surface Shader allows us to customize four types of functions.
Two struct + four functions-- Their process in the entire render pipeline is as follows:
We can see that the "women" behind the Surface Shader are vertex shader and fragment shader. In addition to VertexFunction, the other two struct and three functions both play some roles in fragment shader. Surface Shader first generates a lot of Pass based on our code for forwardbase and forwardadd, which is not covered in this article. The code for each Pass is generated based on the above four functions.
Take a Pass code as an example. The Surface Shader generation process is described as follows:
Code Analysis
Let's take a Surface Shader as an example to analyze the code it generates.
The Surface Shader is as follows:
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"}
It contains all four functions and some common operations. To focus on only one Pass, I added the noforwardadd command. The rendering result it gets is not important (in fact, I just made some changes on BasicDiffuse ...)
Click here to view the generated code:
Shader "Custom/BasicDiffuse_Gen" {Properties {_ EmissiveColor ("Emissive Color", Color) = (,) _ AmbientColor ("Ambient Color", Color) =, 1, 1) _ MySliderValue ("This is a Slider", Range (2.5) = _ RampTex ("Ramp Texture", 2D) = "white" {}} SubShader {Tags {"RenderType" = "Opaque" "RenderType" = "Opaque"} lays 200 // Surface shader code Generated out of a CGPROGRAM block: // ---- forward rendering base pass: pass {Name "FORWARD" Tags {"LightMode" = "ForwardBase"} CGPROGRAM // compile ctictives # pragma vertex vert_surf # pragma fragment frag_surf # pragma limit nodirlightmap # include "HLSLSupport. cginc "# include" UnityShaderVariables. cginc "# define UNITY_PASS_FORWARDBASE # include" UnityCG. cginc "# include" Lighting. cginc "# incl Ude "AutoLight. cginc "# define INTERNAL_DATA # define WorldReflectionVector (data, normal) data. worldRefl # define WorldNormalVector (data, normal) normal // Original surface shader snippet: # line 11 "" # ifdef mask # 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 (Surfac EOutput 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 implements v2f_surf {float4 pos: SV_POSITION; float2 pack0: TEXCOORD0; float4 labels: TEXCOORD1; fixed3 normal: TEXCOORD2; fixed3 vlight: TEXCOORD3; // uploads the data in AutoLight. the definition in cginc // is essentially a # define command // e.g. // # define LIGHTING_COORDS (idx1, idx2) float3 _ LightCoord: TEXCOORD # idx1; SHADOW_COORDS (idx2) // # define SHADOW_COORDS (idx1) float3 _ ShadowCoord: TEXC OORD # idx1; round () };# endif # ifndef 1_v2f_surf {float4 pos: SV_POSITION; float2 pack0: TEXCOORD0; float4 cursor: TEXCOORD1; float2 lmap: TEXCOORD2; opacity (3, 4) };# endif # ifndef incluunity_lightmapst; # endif // define the required texture coordinate float4 _ RampTex_ST; // vertex shaderv2f_surf vert_surf (appdata_full v) {v2f_surf o; // use the custom vert function to fill in the Input Structure Input cust OmInputData; vert (v, customInputData); // assign a value to the required v2f_surf structure o. cust_vertColor = customInputData. vertColor; o. pos = mul (UNITY_MATRIX_MVP, v. vertex); // coordinates the texture of the vertex to the coordinate o of the texture. pack0.xy = TRANSFORM_TEX (v. texcoord, _ RampTex); # ifndef LIGHTMAP_OFF // If LightMap is enabled, the corresponding LightMap coordinate o is calculated. lmap. xy = v. texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw; # endif // calculate the direction of the China-France line in the world coordinate system // SCALED_NORMAL in UnityCG. cg The definition in inc // is essentially a # define command // # define SCALED_NORMAL (v. normal * unity_Scale.w) float3 worldN = mul (float3x3) _ Object2World, SCALED_NORMAL); // If LightMap is not enabled, // The vertex normal direction is worldN # ifdef LIGHTMAP_OFF o. normal = worldN; # endif // SH/ambient and vertex lights # ifdef LIGHTMAP_OFF // If LightMap is not enabled, // vertex lights is the result of the spherical harmonic function. // The Spherical Harmonic Function ShadeSH9 is in UnityCG. in cginc, float3 shlight = ShadeSH9 (float4 (worldN, 1. 0); o. vlight = shlight; // unity_4LightPosX0 and other variables are stored in UnityShaderVariables. definition in 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 // VERTEXLI GHT_ON # endif // LIGHTMAP_OFF // pass lighting information to pixel shader // TRANSFER_VERTEX_TO_FRAGMENT in AutoLight. defined in cginc, // is essentially a # define command // used to convert _ LightCoord and _ ShadowCoord TRANSFER_VERTEX_TO_FRAGMENT (o); return o ;}# ifndef LIGHTMAP_OFFsampler2D unity_Lightmap; # ifndef DIRLIGHTMAP_OFFsampler2D unity_LightmapInd; # endif // fragment shaderfixed4 frag_surf (v2f_surf IN): SV_Target {// Prepare and unpack data # ifdef UNITY_COMPILER_HLSL Input surfIN = (Input) 0; # else Input surfIN; # endif // assign surfIN values to texture coordinates in Input using variables in v2f_surf. uv_RampTex = IN. pack0.xy; surfIN. vertColor = IN. cust_vertColor; # ifdef UNITY_COMPILER_HLSL SurfaceOutput o = (SurfaceOutput) 0; # else SurfaceOutput o; # endif // initialize the SurfaceOutput structure o. albedo = 0.0; o. emission = 0.0; o. specular = 0.0; o. alpha = 0.0; o. glos S = 0.0; # ifdef LIGHTMAP_OFF o. normal = IN. normal; # endif // call surface function // call the custom surf function to fill the SurfaceOutput structure with the surf (surfIN, o); // compute lighting & shadowing factor // LIGHT_ATTENUATION in AutoLight. as defined IN cginc, // is essentially a # define command // used to calculate the light attenuation fixed atten = LIGHT_ATTENUATION (IN); fixed4 c = 0; // realtime lighting: call lighting function # ifdef LIGHTMAP_OFF // If LightMap is not enabled, // call the custom LightXXX function, // Use the filled SurfaceOutput and other variables as parameters. // obtain the initial pixel value c = LightingBasicDiffuse (o, _ WorldSpaceLightPos0.xyz, atten ); # endif // LIGHTMAP_OFF | DIRLIGHTMAP_OFF # ifdef LIGHTMAP_OFF // If LightMap is not enabled, // overlay vertex light color to the pixel c. rgb + = o. albedo * IN. vlight; # endif // LIGHTMAP_OFF // lightmaps: # ifndef LIGHTMAP_OFF // calculate LightMap. This part does not understand # ifndef DIRLIGHTMAP_OFF // directional lightmaps fixed4 lmtex = tex2D (unity_L Ightmap, 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 // assign a value to the Alpha channel c. a = o. alpha; # endif // LIGHTMAP_OFF // call the custom final function, // final (surfIN, o, c); return c ;} ENDCG} // ---- end of surface shader generated code # LINE 57} FallBack "Diffuse "}
I have made comments on all the important parts.
Some Problems
Back to our first question: why is there no light in my scenario, but the object is not all black? All of this is the result of some color overlay calculations in the Fragment Shader.
We carefully observe the color calculated in the Fragment Shader. As mentioned above, it uses LightingXXX to initialize the color value, but a series of color superposition calculations are also carried out later. When LightMap is not used, Unity also calculates the effect of vertex lights on the color, that is, the following sentence:
# Ifdef LIGHTMAP_OFF // If LightMap is not enabled, // overlay the vertex light illumination color to the pixel c. rgb + = o. Albedo * IN. vlight; # endif // LIGHTMAP_OFF
IN. vlight is calculated IN Vertex Shader:
// If LightMap is not enabled, // vertex lights is the result of the spherical harmonic function. // The Spherical Harmonic Function ShadeSH9 is in UnityCG. in cginc, float3 shlight = ShadeSH9 (float4 (worldN, 1.0); o. vlight = shlight;
We can view the implementation of the ShadeSH9 function:
// normal should be normalized, w=1.0half3 ShadeSH9 (half4 normal){half3 x1, x2, x3;// Linear + constant polynomial termsx1.r = dot(unity_SHAr,normal);x1.g = dot(unity_SHAg,normal);x1.b = dot(unity_SHAb,normal);// 4 of the quadratic polynomialshalf4 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 polynomialfloat vC = normal.x*normal.x - normal.y*normal.y;x3 = unity_SHC.rgb * vC; return x1 + x2 + x3;}
It is a Spherical Harmonic Function, but I still don't know what the unity_SHAr variables are... I am very grateful if anyone knows this ~ However, these variables are related to Unity's use of a global environment Light (you can adjust them in Edit-> RenderSettings-> Ambient Light. If you turn the environment light into black, the scene will be black.
Calling, the computing of these light sources is one of the reasons that made writing shader very complicated! If you really look at the definition of commands in UnityCG. cginc, AutoLight. cginc, and other files, you can find that Unity processes different lighting based on the defined lighting type. This part has not been figured out, and we will continue to explore it later!
The above content is purely obtained. If you have any mistakes, please correct them ~