UnityShader入门精要C17-Unity的表面着色器揭秘

本文记录《UnityShader入门精要》第17章的读书笔记。

这是本书的最后一部分内容了,主要介绍了 Unity 的表面着色器、基于物理的渲染、Unity 5 更新的内容。

*本章讲表面着色器,这是在 顶点/片元着色器上加了一层封装,目的是让方便使用者实现各种效果,而不必每次都编写光照函数等。*

表面着色器虽然给我们带来了很大便利,同时也要注意缺点,例如性能影响、无法完成一些自定义效果等。

第5篇 扩展篇

  • 第17章 Unity 的表面着色器揭秘
  • 第18章 基于物理的渲染
  • 第19章 Unity5 更新了什么
  • 第20章 还有更多内容吗

17. Unity的表面着色器揭秘

2009年,Unity 的渲染工程师 Aras 认为把渲染流程分为顶点和像素的抽象层面是错误的,这种在顶点/几何/片元着色器上的操作是对硬件友好,但不符合人类的思考方式。最终在2010年的 Unity3 中,表面着色器(Surface Shader)加入了。

表面着色器实际上是在顶点/片元着色器上添加了一层抽象,例如:

  • 用这些纹理填充颜色
  • 用这个法线纹理填充表面法线
  • 使用兰伯特光照模型
  • 不用考虑是使用前向渲染路径还是延迟渲染路径,场景中有多少光源,它们的类型是什么,怎么处理这些光源等

17.1 表面着色器的一个例子

一个简单的表面着色器代码如下:

Shader "ShaderLearning/Shader17.1_SimpleSurface"{
    Properties{
        _Color("Main Color", Color) = (1,1,1,1)
        _MainTex("Base (RGB)", 2D) = "white"{}
        _BumpMap("Normalmap", 2D) = "bump"{}
    }
    SubShader{
        Tags{"RenderType"="Opaque"}
        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 块中。最重要的部分是 两个结构体 以及它的 编译指令

17.2 编译指令

作用是指明该表面着色器使用的 表面函数光照函数,并设置一些可选参数。编译指令的一般格式如下:

#pragma surface surfaceFunction lightModel [optionalparams]

17.2.1 表面函数

surfaceFunction 用于定义反射率、光滑度、透明度等表面属性,通常是名为 surf 的函数(函数名可以是任意的),他的函数格式的固定的:

void surf(Input IN, inout SurfaceOutput o)
void surf(Input IN, inout SurfaceOutputStandard o)
void surf(Input IN, inout SurfaceOutputStandardSpecular o)

17.2.2 光照函数

光照函数会使用表面函数中设置的各种表面属性,来应用某些光照模型,进而模拟物体表面的光照效果。

  • 基于物理的光照模型函数(UnityPBSLighting.cginc):
    • Standard
    • StandardSpecular
  • 简单的非基于物理的光照模型函数(Lighting.cginc):
    • Lambert
    • BlinnPhong

还可以自定义光照函数,例如用下面的函数来定义用于前向渲染中的光照函数:

// 用于不依赖视角的光照模型,例如漫反射
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half atten);
// 用于依赖视角的光照模型,例如高光反射
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten);s

17.2.3 其他可选参数

可选参数包含了很多非常有用的指令类型,例如:

  • 自定义的修改函数
    • 表面函数
    • 光照模型
    • 顶点修改函数(vertex:VertexFunction):例如把顶点颜色传递给表面函数,修改顶点位置,实现某些顶点动画等
    • 最后的颜色修改函数(finalcolor:ColorFunction):可以在颜色绘制到屏幕前,最后一次修改颜色值,例如实现自定义的雾效等
  • 阴影
    • addshadow:为表面着色器生成一个阴影投射的Pass,可以对阴影的投射进行特殊处理
    • fullforwardshadows:可以在前向渲染路径中支持所有光源类型的阴影
    • noshadow:禁用阴影
  • 透明度混合和透明度测试
    • alpha:透明度混合
    • alphatest:VariableName:使用名为 VariableName 的变量来剔除不满足条件的片元
  • 光照
    • noambient:不要应用任何环境光照或光照探针(light probe)
    • nnovertexlights:不要应用任何逐顶点光照
    • noforwardadd:去掉所有前向渲染中的额外Pass
    • nolightmap/nofog
  • 控制代码的生成
    • 默认情况下会为一个表面着色器生成响应的前向渲染路径、延迟渲染路径使用的 Pass,这会导致生成的 Shader 文件比较大
    • exclude_path:deferred/forward/prepass

17.3 两个结构体

表面着色器支持最多4种关键的函数:表面函数,光照函数,顶点修改函数,最后的颜色修改函数。需要通过两个结构体,在他们之间传递信息:

  • 表面函数的输入结构体 Input
  • 存储表面属性的结构体 SurfaceOutput(以及 SurfaceOutputStandard 和 SurfaceOutputStandardSpecular)

17.3.1 数据来源:Input 结构体

作为表面函数的输入结构体。

  • float3 viewDir:视角方向
  • float4 COLOR语义:逐顶点颜色
  • float4 screenPos:屏幕空间的坐标
  • float3 worldPos:世界空间下的位置
  • float3 worldRefl:世界空间下的反射方向
  • float3 worldRefl; INTERNAL_DATA:修改了表面法线,需要重新计算的反射方向
  • float3 worldNormal:世界空间的法线方向
  • float3 worldNormal; INTERNAL_DATA:修改了表面法线,重新计算法线的法线方向

我们不需要自己计算上述变量,只需要在 Input 结构体中严格声明这些变量即可。

17.3.2 表面属性:SurfaceOutput 结构体

SurfaceOutput SurfaceOutputStandard SurfaceOutputStandardSpecular 作为表面函数的输出,随后作为光照函数的输入来进行光照计算。

相比 Input 结构体的自由性,这个结构体里面的变量是提前就声明好的,不能增加或减少。

SurfaceOutput 的声明可以在 Lighting.cginc 中找到:

struct SurfaceOutput {
    fixed3 Albedo;
    fixed3 Normal;
    fixed3 Emission;
    half Specular;
    fixed Gloss;
    fixed Alpha;
};

SurfaceOutputStandard 和 SurfaceOutputStandardSpecular 的声明可以在 UnityPBSLighting.cginc 中找到:

struct SurfaceOutputStandard
{
    fixed3 Albedo;      // base (diffuse or specular) color
    float3 Normal;      // tangent space normal, if written
    half3 Emission;
    half Metallic;      // 0=non-metal, 1=metal
    // Smoothness is the user facing name, it should be perceptual smoothness but user should not have to deal with it.
    // Everywhere in the code you meet smoothness it is perceptual smoothness
    half Smoothness;    // 0=rough, 1=smooth
    half Occlusion;     // occlusion (default 1)
    fixed Alpha;        // alpha for transparencies
};

struct SurfaceOutputStandardSpecular
{
    fixed3 Albedo;      // diffuse color
    fixed3 Specular;    // specular color
    float3 Normal;      // tangent space normal, if written
    half3 Emission;
    half Smoothness;    // 0=rough, 1=smooth
    half Occlusion;     // occlusion (default 1)
    fixed Alpha;        // alpha for transparencies
};

使用哪个输出结构体,取决于我们选择使用的光照模型:

  • 使用 SurfaceOutput:Unity5 之前的、简单的、非基于物理的光照模型。包括 Lambert 和 BlinnPhong
  • 使用 SurfaceOutputStandard:Unity5 添加的、基于物理的光照模型,Standard。用于默认的金属工作流程(Metallic Workflow)
  • 使用 SurfaceOutputStandardSpecular:Unity5 添加的、基于物理的光照模型,StandardSpecular。用于高光工作流程(Specular Workflow)

以下是 SurfaceOutput 结构体中的变量和含义:

  • fixed3 Albedo:对光源的反射率。
  • fixed3 Normal:表面法线方向。
  • fixed3 Emission:自发光。c.rgb += o.Emission;
  • half Specular:高光反射中指数部分的系数。
  • fixed Gloss:高光反射中的强度系数。
  • fixed Alpha:透明通道。

时刻记着,表面着色器本质上就是包含了很多 Pass 的顶点/片元着色器。

17.4 Unity 背后做了什么

表面着色器中的各个函数、编译指令和结构体与顶点/片元着色器之间有什么关系呢?

  • 有些 Pass 是为了针对不同的渲染路径
    • 会前向渲染路径生成 LightMode 为 ForwardBase 和 ForwardAdd 的 Pass
    • 为 Unity5 之前的延迟渲染路径生成 LightMode 为 PrePassBase 和 PrePassFinal 的 Pass
    • 为 Unity5 之后的延迟渲染路径生成 LightMode 为 Deferred 的 Pass
  • 一些 Pass 是用于产生额外的信息。例如为了给光照映射和动态全局光照提取表面信息,会生成一个 LightMode 为 Meta 的 Pass
  • 有些表面着色器由于修改了顶点位置,因此可以使用 addshadow 编译指令为它生成相应的 LightMode 为 ShadowCaster 的阴影投射 Pass

在每个编译完成的表面着色器面板上,都有 Show generated code 按钮,单机可以看到 Unity 为他生成的所有顶点/片元着色器。(很大一串代码!

以 Unity 生成 LightMode 为 ForwardBase 的 Pass 为例,他的渲染计算流水线如图所示:

自动生成的过程大致如下:

  1. 直接将表面着色器中的 CGPROGRAM 和 ENDCG 之间的代码复制过来。这些函数和变量会在之后的处理过程中被当成正常的结构体和函数进行调用
  2. Unity 分析上述代码,生成顶点着色器的输入 v2f_surf 结构体,用于在顶点着色器和片元着色器之间进行数据传递
  3. 生成顶点着色器
    1. 如果我们自定义了顶点修改函数,Unity 会首先调用顶点修改函数来修改顶点数据,或填充自定义的 Input 结构体中的变量
    2. 计算 v2f_surf 中卡生成的变量值
    3. 将 v2f_surf 传递给接下来的片元着色器
  4. 生成片元着色器
    1. 使用 v2f_surf 中对应变量填充 Input 结构体。例如纹理坐标、视角方向等
    2. 调用我们自定义的表面函数填充 SurfaceOutput 结构体
    3. 调用光照函数得到初始的颜色值
    4. 进行其他的颜色叠加
    5. 最后,如果自定义了最后的颜色修改函数,Unity 就会调用他进行最后的颜色修改

17.5 表面着色器实例分析

17.5.1 实例代码

本节实现的效果是对模型进行膨胀。原理是在顶点修改函数中,沿着顶点法线方向扩张顶点位置。本例中对表面着色器中4个可自定义的函数,全部采用了自定义的实现(顶点修改函数、表面函数、光照函数、最后的颜色修改函数)。

实现沿着顶点法线方向扩张顶点位置的 表面着色器 代码如下:

Shader "ShaderLearning/Shader17.5_NormalExtrusion"{
    Properties{
        _ColorTint("Color Tint", Color) = (1,1,1,1)
        _MainTex("Base (RGB)", 2D) = "white"{}
        _BumpMap("Normalmap", 2D) = "bump"{}
        _Amount("Extrusion Amount", Range(-0.5, 0.5)) = 0.1
    }

    SubShader{
        Tags{"RenderType"="Opaque"}
        LOD 300

        CGPROGRAM

        // surf - which surface function.
        // CustomLambert - which lighting model to use.
        // vertex:myvert - use custom final color modification function.
        // addshadow - generate a shadow caster pass. Because we modify the vertex position,
        // the shader 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;
        }

        ENDCG
    }
    
    FallBack "Legacy Shaders/Diffuse"
}

实现的效果:

  • 在顶点修改函数 myvert():使用顶点法线对顶点位置进行了膨胀
  • 在表面函数 surf():使用主纹理设置了表面属性中的反射率,使用法线纹理设置了表面方向
  • 在光照函数 LightingCustomLambert():实现了简单的兰伯特漫反射光照模型
  • 在最后的颜色修改函数 mycolor():简单的使用了颜色参数对输出颜色进行调整
  • #pragma surface 编译指令:
    • addshadow:生成一个该表面着色器对应的阴影投射 Pass,而不要依赖 FallBack 中找到的阴影投射 Pass
    • exclude_path:deferred exclude_path:prepass:不要为延迟渲染路径生成相应的 Pass,为了缩小自动生成的代码量
    • nometa:取消对提取元数据的 Pass 的生成

17.5.2 代码分析

点击 Show generated code 查看生成的顶点/片元着色器。生成了3个 Pass,分别是 LightMode 为 ForwardBase(前向渲染路径中处理逐像素的平行光)、ForwardAdd(处理其他逐像素光)、ShadowCaster(处理阴影投射)。

下面分析 ForwardBase Pass(以下生成的代码基于 Unity2019.4.18):

a. 首先指明了一些编译指令:

    // ---- 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
#include "UnityShaderVariables.cginc"
#include "UnityShaderUtilities.cginc"

b. 之后是一些自动生成的注释:

// -------- 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
// 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"

c. 随后定义了一些宏来辅助计算:

#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))

d. 接着 Unity 把我们在表面着色器中编写的 CG 代码复制过来,作为 Pass 的一部分

// Original surface shader snippet:
#line 11 ""
#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 final color modification function.
        // addshadow - generate a shadow caster pass. Because we modify the vertex position,
        // the shader 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;
        }

e. 然后 Unity 定义了顶点着色器到片元着色器的插值结构体(即顶点着色器的输出结构体) v2f_surf。有 #ifdef 来判断不同的情况:

// 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;

f. 随后 Unity 定义了真正的顶点着色器:

// 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;
}

g. 在 Pass 的最后,Unity 定义了真正的片元着色器:

// fragment shader
fixed4 frag_surf (v2f_surf IN) : SV_Target {
  UNITY_SETUP_INSTANCE_ID(IN);
  // prepare and unpack data
  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
  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
  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;
}

17.6 Surface Shader 的缺点

表面着色器给我们带来了很大的便利,但缺点也不少:

  • 任何在表面着色器中完成的事情,都可以在顶点/片元着色器中重现,但反过来不成立。
  • 表面着色器往往会对性能造成影响。例如移动平台的版本 Mobile/Diffuse 等只是去掉了额外的逐像素 Pass、不计算全局光照等,但没有深层的优化。
  • 表面着色器无法完成一些自定义的渲染效果。例如 10.2.2 节中的透明玻璃效果。

因此,给出一些建议供读者参考:

  • 如果你需要和各种光源打交道,尤其是想用 Unity 中的全局光照。可以使用表面着色器,但要时刻小心它的性能;
  • 如果你需要处理的光源数目非常少,例如只有一个平行光,那么使用顶点/片元着色器更好;
  • 最重要的是,如果有很多自定义的渲染效果,那么请选择顶点/片元着色器。

999. Ref

  1. 官方手册-表面着色器的例子:https://docs.unity3d.com/Manual/SL-SurfaceShaderExamples.html
  2. 官方手册-表面着色器的自定义光照模型:https://docs.unity3d.com/Manual/SL-SurfaceShaderLighting.html
  3. 官方手册-编写表面着色器:https://docs.unity3d.com/Manual/SL-SurfaceShaderLighting.html

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 cdd@ahucd.cn

×

喜欢就点赞,疼爱就打赏

B站 cdd的庇护之地 github itch