目录第17章 Unity的表面着色器揭秘17.1 表面着色器的一个例子17.2 编译指令17.3 两个结构体17.4 Unity背后做了什么17.5 表面着色器实例分析17.6 Surface Shader的缺点第17章 Unity的表面着色器揭秘·Aras认为应划分为表面着色器、光照模型和光照着色器这样的层面表面着色器定义了模型表面反射率、法线和高光光照模型选择兰伯特还是Blinn-Phong等模型光照着色器负责计算光照衰减、阴影等。·表面着色器加入Unity在顶点/片元着色器上又添加了一层抽象。17.1 表面着色器的一个例子·实践一系列新建和复制操作Shader代码如下由于Properties中文无效先前为了熟悉各属性使用了中文现改为英文Shader Unity Shaders Book/Self Try/Chapter 17/Bumped Diffuse { Properties{ _Color(Main Color, Color) (1.0, 1.0, 1.0, 1.0) _MainTex(Base (RGB), 2D) white{} _BumpMap(Normalmap, 2D) bump{} } SubShader{ Tags{ RenderTypeOpaque } LOD 300 CGPROGRAM #pragma surface surf Lambert #pragma target 3.0 sampler2D _MainTex; sampler2D _BumpMap; fixed4 _Color; struct Input{ float2 uv_MainTex; float2 uv_BumpMap; }; void surf(Input IN, inout SurfaceOutput o){ fixed4 tex tex2D(_MainTex, IN.uv_MainTex); o.Albedo tex.rgb * _Color.rgb; o.Alpha tex.a * _Color.a; o.Normal UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)); } ENDCG } FallBack Legacy Shaders/Diffuse }·表面着色器的CG是直接且必须写在SubShader块中Unity会在背后生成多个Pass·表面着色器最重要的部分是两个结构体和它的编译指令。17.2 编译指令·最重要的作用指明该表面着色器使用的表面函数和光照函数并设置一些可选参数·一般格式例子如下参考上一节#pragma surface surfaceFunction lightModel [optionalparams]表面函数·用于定义反射率、光滑度、透明度等值在内的表面属性·通常是名为surf的函数函数名可以是任意的以下inout结构体需要配合不同的光照模型使用void surf(Input IN, inout SurfaceOutput o) void surf(Input IN, inout SurfaceOutputStandard o) void surf(Input IN, inout SurfaceOutputStandardSpecular o)·Input IN来设置表面属性并存储在inout结构体中再传递给光照函数计算光照结果。光照函数·使用表面函数中设置的各种表面属性来应用某些光照模型进而模拟物体表面的光照效果·基于物理的Standard、StandardSpecularUnityPBSLighting.cginc中·基于非物理的Lambert、Blinn PhongLighting.cginc中·定义新的光照函数// 不依赖视角的光照模型 half4 LightingName (SurfaceOutput s, half3 lightDir, half atten); // 依赖视角的光照模型 half4 LightingName (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten);其他可选参数·自定义的修改函数顶点修改函数自定义顶点属性、最后的颜色修改函数最后一次修改颜色值·阴影addshadow为表面着色器生成一个阴影投射的Pass、fullforwardshadows在前向渲染路径中支持所有光源类型的阴影·透明度混合和透明度测试alpha、alphatest剔除不满足条件的片元·光照noambient不应用任何光照或光照探针、novertexlights不应用任何逐顶点光照、noforwardadd去掉所有前向渲染中额外的Pass只支持一个逐像素的平行光其他光源会按照逐顶点或SH来计算光照影响、nolightmap光照烘焙、nofog雾效·控制代码的生成生成相应的前向渲染路径、延迟渲染路径使用的Pass用exclude_path来表示不需要为某些渲染路径生成代码。17.3 两个结构体数据来源Input结构体·包含了表面属性的数据来源因此作为表面函数的输入结构体如果自定义了顶点修改函数还会是顶点修改函数的输出结构体。·内置变量略需按名称严格声明。表面属性SurfaceOutput结构体·存储表面属性的结构体随后作为光照函数的输入进行各种光照计算·变量是提前声明好的不会增加和减少如Lighting.cginc中SurfaceOutputAlbedo对光源的反射率、Normal表面法线方向、Emission自发光、Specular高光反色中指数部分系数、Gloss高光反射强度系数、Alpha透明通道UnityPBSLighting.cginc中SurfaceOutputStandard和SurfaceOutputStandardSpecular略·非基于物理的光照模型使用SurfaceOutput基于物理的分别使用SurfaceOutputStandard默认金属工作流程和SurfaceOutputStandardSpecular高光工作流程17.4 Unity背后做了什么·Show generated code查看为此表面着色器生成的所有顶点/片元着色器·对Pass的自动生成过程直接复制CGPROGRAM和ENDCG之间的代码包括的函数和变量会在之后的处理中被当成正常的结构体和函数进行调用Unity分析上述代码据此生成顶点着色器的输出v2f_surf结构体用于在顶点着色器和片元着色器中进行数据传递生成顶点着色器首先调用顶点修改函数来修改顶点数据或填写自定义结构体中变量如有需要则储存计算v2f_surf中其他变量值传递给片元着色器生成片元着色器使用v2f_surf对应变量填充Input结构体调用表面函数填充surfaceOutput结构体调用光照函数得到初始颜色值进行颜色叠加调用最后的颜色修改函数。17.5 表面着色器实例分析表面着色器代码复现如下Shader Unity Shaders Book/Self Try/Chapter 17/Normal Extrusion { Properties{ _ColorTint(Color Tint, Color) (1.0, 1.0, 1.0, 1.0) _MainTex(Base (RGB), 2D) white{} _BumpMap(Normalmap, 2D) bump{} _Amount(Extrusion Amount, Range(-0.5, 0.5)) 0.1 } SubShader{ Tags{ RenderTypeOpaque } LOD 300 CGPROGRAM #pragma surface surf CustomLambert vertex:myvert finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa #pragma target 3.0 fixed4 _ColorTint; sampler2D _MainTex; sampler2D _BumpMap; half _Amount; struct Input{ float2 uv_MainTex; float2 uv_BumpMap; }; void myvert(inout appdata_full v){ v.vertex.xyz v.normal * _Amount; } void surf(Input IN, inout SurfaceOutput o ){ fixed4 tex tex2D(_MainTex, IN.uv_MainTex); o.Albedo tex.rgb; o.Alpha tex.a; o.Normal UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)); } half4 LightingCustomLambert(SurfaceOutput s, half3 lightDir, half atten){ half NdotL dot(s.Normal, lightDir); half4 c; c.rgb s.Albedo * _LightColor0.rgb * (NdotL * atten); c.a s.Alpha; return c; } void mycolor(Input IN, SurfaceOutput o, inout fixed4 color){ color * _ColorTint; } ENDCG } FallBack Legacy Shaders/Diffuse }查看Unity生成的前向渲染路径中的处理逐像素平行光的ForwardBase Pass// ---- forward rendering base pass: Pass { Name FORWARD Tags { LightMode ForwardBase } CGPROGRAM // compile directives #pragma vertex vert_surf // 自动生成的 #pragma fragment frag_surf // 自动生成的 #pragma target 3.0 #pragma multi_compile_instancing #pragma multi_compile_fwdbase #include HLSLSupport.cginc #define UNITY_INSTANCED_LOD_FADE #define UNITY_INSTANCED_SH #define UNITY_INSTANCED_LIGHTMAPSTS #define UNITY_INSTANCED_RENDERER_BOUNDS #include UnityShaderVariables.cginc #include UnityShaderUtilities.cginc // -------- variant for: when no other keywords are defined #if !defined(INSTANCING_ON) //以下注释用于理解分析过程和结果 // Surface shader code generated based on: // vertex modifier: myvert // writes to per-pixel normal: YES // writes to emission: no // writes to occlusion: no // needs world space reflection vector: no // needs world space normal vector: no // needs screen space position: no // needs world space position: no // needs view direction: no // needs world space view direction: no // needs world space position for lighting: no // needs world space view direction for lighting: no // needs world space view direction for lightmaps: no // needs vertex color: no // needs VFACE: no // needs SV_IsFrontFace: no // passes tangent-to-world matrix to pixel shader: YES // reads from normal: no // 2 texcoords actually used // float2 _MainTex // float2 _BumpMap #include UnityCG.cginc #include Lighting.cginc #include AutoLight.cginc // 定义宏辅助计算实际上没有用到是为了在修改了表面法线的情况下辅助计算得到世界空间下反射和法线方向 #define INTERNAL_DATA half3 internalSurfaceTtoW0; half3 internalSurfaceTtoW1; half3 internalSurfaceTtoW2; #define WorldReflectionVector(data,normal) reflect (data.worldRefl, half3(dot(data.internalSurfaceTtoW0,normal), dot(data.internalSurfaceTtoW1,normal), dot(data.internalSurfaceTtoW2,normal))) #define WorldNormalVector(data,normal) fixed3(dot(data.internalSurfaceTtoW0,normal), dot(data.internalSurfaceTtoW1,normal), dot(data.internalSurfaceTtoW2,normal)) // Original surface shader snippet: #line 10 #ifdef DUMMY_PREPROCESSOR_TO_WORK_AROUND_HLSL_COMPILER_LINE_HANDLING #endif // 以下是把源代码复制过来便于调用 /* UNITY: Original start of shader */ // surf - which surface function. // CustomLambert - which lighting model to use. // vertex:myvert - use custom vertex modification function. // finalcolor:mycolor - use custom final color modification function. // addshadow - generate a shadow caster pass. Because we modify the vertex position, the shder needs special shadows handling. // exclude_path:deferred/exclude_path:prepas - do not generate passes for deferred/legacy deferred rendering path. // nometa - do not generate a “meta” pass (that’s used by lightmapping dynamic global illumination to extract surface information). //#pragma surface surf CustomLambert vertex:myvert finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa //#pragma target 3.0 fixed4 _ColorTint; sampler2D _MainTex; sampler2D _BumpMap; half _Amount; struct Input { float2 uv_MainTex; float2 uv_BumpMap; }; void myvert (inout appdata_full v) { v.vertex.xyz v.normal * _Amount; } void surf (Input IN, inout SurfaceOutput o) { fixed4 tex tex2D(_MainTex, IN.uv_MainTex); o.Albedo tex.rgb; o.Alpha tex.a; o.Normal UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)); } half4 LightingCustomLambert (SurfaceOutput s, half3 lightDir, half atten) { half NdotL dot(s.Normal, lightDir); half4 c; c.rgb s.Albedo * _LightColor0.rgb * (NdotL * atten); c.a s.Alpha; return c; } void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) { color * _ColorTint; } // 判断是否使用了光照纹理并生成不同的结构体 // vertex-to-fragment interpolation data // no lightmaps: #ifndef LIGHTMAP_ON // half-precision fragment shader registers: #ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS struct v2f_surf { UNITY_POSITION(pos); float4 pack0 : TEXCOORD0; // _MainTex _BumpMap float4 tSpace0 : TEXCOORD1; float4 tSpace1 : TEXCOORD2; float4 tSpace2 : TEXCOORD3; // 切线空间到世界空间的变换矩阵 fixed3 vlight : TEXCOORD4; // ambient/SH/vertexlights UNITY_LIGHTING_COORDS(5,6) #if SHADER_TARGET 30 float4 lmap : TEXCOORD7; #endif UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; #endif // high-precision fragment shader registers: #ifndef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS struct v2f_surf { UNITY_POSITION(pos); float4 pack0 : TEXCOORD0; // _MainTex _BumpMap float4 tSpace0 : TEXCOORD1; float4 tSpace1 : TEXCOORD2; float4 tSpace2 : TEXCOORD3; fixed3 vlight : TEXCOORD4; // ambient/SH/vertexlights UNITY_SHADOW_COORDS(5) #if SHADER_TARGET 30 float4 lmap : TEXCOORD6; #endif UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; #endif #endif // with lightmaps: #ifdef LIGHTMAP_ON // half-precision fragment shader registers: #ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS struct v2f_surf { UNITY_POSITION(pos); float4 pack0 : TEXCOORD0; // _MainTex _BumpMap float4 tSpace0 : TEXCOORD1; float4 tSpace1 : TEXCOORD2; float4 tSpace2 : TEXCOORD3; float4 lmap : TEXCOORD4; UNITY_LIGHTING_COORDS(5,6) UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; #endif // high-precision fragment shader registers: #ifndef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS struct v2f_surf { UNITY_POSITION(pos); float4 pack0 : TEXCOORD0; // _MainTex _BumpMap float4 tSpace0 : TEXCOORD1; float4 tSpace1 : TEXCOORD2; float4 tSpace2 : TEXCOORD3; float4 lmap : TEXCOORD4; UNITY_SHADOW_COORDS(5) UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; #endif #endif float4 _MainTex_ST; float4 _BumpMap_ST; // vertex shader // 真正的顶点着色器调用自定义顶点修改函数来修改一些顶点属性 v2f_surf vert_surf (appdata_full v) { UNITY_SETUP_INSTANCE_ID(v); v2f_surf o; UNITY_INITIALIZE_OUTPUT(v2f_surf,o); UNITY_TRANSFER_INSTANCE_ID(v,o); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); myvert (v); o.pos UnityObjectToClipPos(v.vertex); o.pack0.xy TRANSFORM_TEX(v.texcoord, _MainTex); o.pack0.zw TRANSFORM_TEX(v.texcoord, _BumpMap); float3 worldPos mul(unity_ObjectToWorld, v.vertex).xyz; float3 worldNormal UnityObjectToWorldNormal(v.normal); fixed3 worldTangent UnityObjectToWorldDir(v.tangent.xyz); fixed tangentSign v.tangent.w * unity_WorldTransformParams.w; fixed3 worldBinormal cross(worldNormal, worldTangent) * tangentSign; o.tSpace0 float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); o.tSpace1 float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); o.tSpace2 float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z); #ifdef DYNAMICLIGHTMAP_ON o.lmap.zw v.texcoord2.xy * unity_DynamicLightmapST.xy unity_DynamicLightmapST.zw; #endif #ifdef LIGHTMAP_ON o.lmap.xy v.texcoord1.xy * unity_LightmapST.xy unity_LightmapST.zw; #endif // SH/ambient and vertex lights #ifndef LIGHTMAP_ON #if UNITY_SHOULD_SAMPLE_SH !UNITY_SAMPLE_FULL_SH_PER_PIXEL float3 shlight ShadeSH9 (float4(worldNormal,1.0)); o.vlight shlight; #else o.vlight 0.0; #endif #ifdef VERTEXLIGHT_ON 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, worldNormal ); #endif // VERTEXLIGHT_ON #endif // !LIGHTMAP_ON UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy); // pass shadow and, possibly, light cookie coordinates to pixel shader return o; } // fragment shader // 真正的片元着色器 fixed4 frag_surf (v2f_surf IN) : SV_Target { UNITY_SETUP_INSTANCE_ID(IN); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(IN); // prepare and unpack data // 利用插值后结构体v2f_surf来初始化Input结构体中变量 Input surfIN; #ifdef FOG_COMBINED_WITH_TSPACE UNITY_RECONSTRUCT_TBN(IN); #else UNITY_EXTRACT_TBN(IN); #endif UNITY_INITIALIZE_OUTPUT(Input,surfIN); surfIN.uv_MainTex.x 1.0; surfIN.uv_BumpMap.x 1.0; surfIN.uv_MainTex IN.pack0.xy; surfIN.uv_BumpMap IN.pack0.zw; float3 worldPos float3(IN.tSpace0.w, IN.tSpace1.w, IN.tSpace2.w); #ifndef USING_DIRECTIONAL_LIGHT fixed3 lightDir normalize(UnityWorldSpaceLightDir(worldPos)); #else fixed3 lightDir _WorldSpaceLightPos0.xyz; #endif // 声明结构体变量并对其中表面属性进行初始化 #ifdef UNITY_COMPILER_HLSL // 编译语言类型是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; fixed3 normalWorldVertex fixed3(0,0,1); o.Normal fixed3(0,0,1); // call surface function surf (surfIN, o); // compute lighting shadowing factor // 真正的光照计算 UNITY_LIGHT_ATTENUATION(atten, IN, worldPos) fixed4 c 0; float3 worldN; worldN.x dot(_unity_tbn_0, o.Normal); worldN.y dot(_unity_tbn_1, o.Normal); worldN.z dot(_unity_tbn_2, o.Normal); worldN normalize(worldN); o.Normal worldN; // 如果关闭了光照映射就把逐顶点的光照结果叠加到输出颜色中 #ifndef LIGHTMAP_ON // 书本上内容为LIGHTMAP_OFF,暂未搞明白原因 c.rgb o.Albedo * IN.vlight; #endif // !LIGHTMAP_ON // lightmaps #ifdef LIGHTMAP_ON #if DIRLIGHTMAP_COMBINED // directional lightmaps fixed4 lmtex UNITY_SAMPLE_TEX2D(unity_Lightmap, IN.lmap.xy); fixed4 lmIndTex UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, IN.lmap.xy); half3 lm DecodeDirectionalLightmap (DecodeLightmap(lmtex), lmIndTex, o.Normal); #else // single lightmap fixed4 lmtex UNITY_SAMPLE_TEX2D(unity_Lightmap, IN.lmap.xy); fixed3 lm DecodeLightmap (lmtex); #endif #endif // LIGHTMAP_ON // realtime lighting: call lighting function #ifndef LIGHTMAP_ON c LightingCustomLambert (o, lightDir, atten); #else c.a o.Alpha; #endif #ifdef LIGHTMAP_ON // combine lightmaps with realtime shadows #ifdef SHADOWS_SCREEN #if defined(UNITY_LIGHTMAP_DLDR_ENCODING) 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 #endif // LIGHTMAP_ON #ifdef DYNAMICLIGHTMAP_ON fixed4 dynlmtex UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, IN.lmap.zw); c.rgb o.Albedo * DecodeRealtimeLightmap (dynlmtex); #endif mycolor (surfIN, o, c); UNITY_OPAQUE_ALPHA(c.a); return c; } #endif17.6 Surface Shader的缺点·虽可以可以快速实现各种光照效果但失去了对各种优化和特效实现的控制·会对性能造成一定的影响·无法完成一些自定义的渲染效果和各种光源打交道更适合表面着色器光源数目少使用顶点/片元着色器自定义渲染效果多使用顶点/片元着色器。