《Unity ShaderLab新手宝典》

Posted by LudoArt on May 10, 2023

《Unity ShaderLab新手宝典》

ShaderLab语法基础

Shader的组织结构

Shader "Name"
{
		Properties
		{
				//开放到材质面板的属性
		}
		SubShader
		{
				//顶点-片段着色器
				//或者表面着色器
				//或者固定函数着色器
		}
		SubShader
		{
				//更加精简的版本
				//为了在旧的图形设备上运行
		}
		...
		Fallback "Name"
}

Shader中可以编写多个子着色器(SubShader),但至少需要一个。在应用程序运行过程中,GPU会先检测第一个子着色器能否正常运行,如果不能正常运行就会再检测第二个,以此类推,最后才会执行Fallback命令,运行指定的一个基础着色器。

如果编写的是顶点-片段着色器,每个子着色器中还会包含一个甚至多个Pass。运行子着色器时,所有Pass会依次执行,每个Pass的输出的结果会以指定的方式与上一步的结果进行混合,最终输出。

Properties

开放出来的属性是通过Properties代码块定义的。

Unity Shader的属性主要分为三大类:数值,颜色,向量和纹理贴图,定义方式如下:

_Name (Display Name, type) = defaultValue [{options}]
  • 数值类属性

    name("dispaly name", Float) = number
    name("dispaly name", Range(min, max)) = number
    
  • 颜色和向量类属性

    name("dispaly name", Color) = (number, number, number, number)//有范围限制[0,1]
    name("dispaly name", Vector) = (number1, number2, number3, number4)//没有范围限制
    
  • 纹理贴图类属性

    name("dispaly name", 2D) = "defaulttexture" {}//最常使用,如漫反射贴图/法线贴图等
    name("dispaly name", Cube) = "defaulttexture" {}//立方体纹理,如Skybox, Reflection Prob
    name("dispaly name", 3D) = "defaulttexture" {}//只能由脚本创建,很少使用
    

SubShader

SubShader的大致结构如下所示:

SubShader
{
		//标签
		Tags {"TagName1" = "Value1" "TagName2" = "Value2" ...}

		//渲染状态
		Cull Back
		...

		Pass
		{
				//第一个Pass
		}
		Pass
		{
				//第二个Pass
		}
		...
}

在SubShader中设置的渲染状态会影响到该SubShader中所有的Pass,如果想要某些状态不影响其他Pass,可以针对某个Pass单独设置渲染状态。

考虑到性能方面的影响,应该尽可能地减少Pass的数量。

SubShader 的标签

  • 渲染队列

    在SubShader中可以使用Queue标签确定物体的渲染顺序,Unity预先定义了五种渲染队列,如下表所示:

    队列名称 描述 队列号
    Background 最先执行渲染,一般用来渲染天空盒(Skybox)或者背景 1000
    Geometry 非透明的几何体通常使用这个队列,当没有声明渲染队列的时候,Unity会默认使用这个队列 2000
    AlphaTest Alpha测试的几何体会使用这个队列,之所以从Geometry队列单独拆分出来,是因为当所有实体都绘制完后再绘制Alpha测试会更高效 2450
    Transparent 在这个队列的几何体按由远到近的顺序进行绘制,所有进行Alpha混合的几何体都应该使用这个队列,例如玻璃材质,粒子特效等 3000
    Overlay 用来叠加渲染的效果,例如镜头光晕等,放在最后渲染 4000

    除了使用Unity预定义的渲染队列,也可以指定一个队列,如:

    Tags {"Queue" = "Geometry+1" }
    
  • 渲染类型

    RenderType 标签可以将SubShader划分为不同的类别,用于后期进行Shader替换或者产生摄像机的深度纹理。

    类型名称 描述
    Opaque 用于普通Shader,例如:不透明,自发光,反射,地形Shader
    Transparent 用于半透明Shader,例如:透明,粒子
    TransparentCutout 用于透明测试Shader,例如:植物叶子
    Background 用于Skybox Shader
    Overlay 用于GUI纹理,Halo,Flare Shader
    TreeOpaque 用于地形系统中的树干
    TreeTransparentCutout 用于地形系统中的树叶
    TreeBillboard 用于地形系统中的Billboard树
    Grass 用于地形系统中的草
    GrassBillboard 用于地形系统中的Billboard草
  • 禁用批处理

    当使用批处理(Batching)的时候,几何体会被变换到世界空间,模型空间会被丢弃。这会导致某些使用模型空间顶点数据的Shader最终无法实现所希望的效果。开启Disable Batching(禁用批处理)可以解决这个问题。

    • “DisableBatching” = “True”:总是禁用批处理;
    • “DisableBatching” = “False”:不禁用批处理(默认值);
    • “DisableBatching” = “LODFading”:当LOD效果激活的时候才会禁用批处理,主要用于地形系统上的树;
  • 禁止阴影投射

    ”ForceNoShadowCasting” = “True”标签可以禁止阴影投射。

  • 忽略 Projector

    如果不希望物体受到Projector(投影机)的投射,可以在Shader中添加Ignore Projector标签。

  • LightMode

    • In the Built-in Render Pipeline

      Value Function
      Always Always rendered; does not apply any lighting. This is the default value.
      ForwardBase Used in Forward rendering; applies ambient, main directional light, vertex/SH lights and lightmaps.
      ForwardAdd Used in Forward rendering; applies additive per-pixel lights, one Pass per light.
      Deferred Used in Deferred Shading; renders G-buffer.
      ShadowCaster Renders object depth into the shadowmap or a depth texture.
      MotionVectors Used to calculate per-object motion vectors.
      Vertex Used in legacy Vertex Lit rendering when the object is not lightmapped; applies all vertex lights.
      VertexLMRGBM Used in legacy Vertex Lit rendering when the object is lightmapped, and on platforms where the lightmap is RGBM encoded (PC & console).
      VertexLM Used in legacy Vertex Lit rendering when the object is lightmapped, and on platforms where lightmap is double-LDR encoded (mobile platforms).
      Meta This Pass is not used during regular rendering, only for lightmap baking or Realtime Global Illumination. For more information, see Lightmapping and shaders.
    • In the Universal Render Pipeline (URP)

      Property Description
      UniversalForward The Pass renders object geometry and evaluates all light contributions. URP uses this tag value in the Forward Rendering Path.
      UniversalGBuffer The Pass renders object geometry without evaluating any light contribution. URP uses this tag value in the Deferred Rendering Path.
      UniversalForwardOnly The Pass renders object geometry and evaluates all light contributions, similarly to when LightMode has the UniversalForward value. The difference from UniversalForward is that URP can use the Pass for both the Forward and the Deferred Rendering Paths.Use this value if a certain Pass must render objects with the Forward Rendering Path when URP is using the Deferred Rendering Path. For example, use this tag if URP renders a Scene using the Deferred Rendering Path and the Scene contains objects with shader data that does not fit the GBuffer, such as Clear Coat normals.If a shader must render in both the Forward and the Deferred Rendering Paths, declare two Passes with the UniversalForward and UniversalGBuffer tag values. If a shader must render using the Forward Rendering Path regardless of the Rendering Path that the URP Renderer uses, declare only a Pass with the LightMode tag set to UniversalForwardOnly.
      Universal2D The Pass renders objects and evaluates 2D light contributions. URP uses this tag value in the 2D Renderer.
      ShadowCaster The Pass renders object depth from the perspective of lights into the Shadow map or a depth texture.
      DepthOnly The Pass renders only depth information from the perspective of a Camera into a depth texture.
      Meta Unity executes this Pass only when baking lightmaps in the Unity Editor. Unity strips this Pass from shaders when building a Player.
      SRPDefaultUnlit Use this LightMode tag value to draw an extra Pass when rendering objects. Application example: draw an object outline. This tag value is valid for both the Forward and the Deferred Rendering Paths.URP uses this tag value as the default value when a Pass does not have a LightMode tag.
  • 其他标签

    // RequireOptions tag:
    // SoftVegetation: Render this pass only if Soft Vegetation is on in Quality Settings.
      
    // CanUseSpriteAtlas tag
      
    // PreviewType tag
    // Sphere, Plane, Skybox
      
    // PassFlags tag
    // OnlyDirectional: Valid only in the Built-in Render Pipeline, 
    // when the rendering path is set to Forward, 
    // in a Pass with a LightMode tag value of ForwardBase.
    // Unity provides only the main directional light and ambient/light probe data to this Pass. 
    // This means that data of non-important lights is not passed into vertex-light or spherical harmonics shader variables. 
    // See Forward rendering path for details.
    

Pass 的渲染状态

渲染状态 数值 作用
Cull Cull Back Front
ZTest ZTest (Less Greater
ZWrite ZWrite On Off
Blend Blend sourceBlendMode destBlendMode 设置渲染图像的混合方式
ColorMask ColorMask RGB A

ShaderLab: commands

Fallback

当所有的SubShader都不能在当前显卡上运行的时候,就会运行Fallback定义的Shader。

最常用于Fallback的Shader为Unity内置的Diffuse。

顶点-片段着色器基础

CG语法基础

注:现在URP中使用HLSL更多,CG与HLSL相似,作为入门可了解。

编译指令

Pass
{
	// ..设置渲染状态

	CGPROGRAM
	// 编译指令
	# pragma vertex vert
	# pragma fragment frag

	// CG代码
	
	ENDCG
}

CG中常用的编译指令:

编译指令 作用
# pragma vertex name 定义顶点着色器的名称,通常会使用vert
# pragma fragment name 定义片段着色器的名称,通常会使用frag
# pragma traget name 定义Shader要编译的目标级别,默认2.5
  • 编译目标等级

    当编写完Shader程序之后,其中的CG代码可以被编译到不同的Shader Model中,为了能够使用更高级的GPU功能,需要对应使用更高等级的编译目标。但高等级的编译目标可能会导致Shader无法在旧的GPU上运行。

    声明编译目标的级别可以使用 # pragma target name 指令,或者也可以使用 # pragma require feature 指令直接声明某个特定的功能,如:

    # pragma target 3.0 // 目标等级3.0
    # pragma require geometry tessellation // 需要几何体细分功能
    
  • 渲染平台

    Unity具有跨平台的特性,它支持很多的渲染API,默认情况下, Unity会为所有的平台编译一份Shader层序。但可以通过编译指令 # pargma only_renderers PlatformName 或者 # pragma exclude_renderers PlatformName 指定编译某些平台或者不编译某些平台。

着色器函数

  • 无返回值的函数

    无返回值即函数不会返回任何变量,而是通过out关键词将变量输出,语法结构如下所示:

    void name(in 参数, out 参数)
    {
    		// 函数体
    }
    

    其中:

    • in:输入参数,语法为:in + 数据类型 + 名称,一个函数可以有多个输入,关键词in可以省略;
    • out:输出参数,语法为:out + 数据类型 + 名称,一个函数可以有多个输出;
  • 有返回值的函数

    有返回值的函数不再使用out关键词输出参数,而是会在最后通过return关键词返回一个变量,语法结构如下所示:

    type name(in 参数)
    {
    	// 函数体
    	return 返回值;
    }
    

    顶点函数和片段函数中支持的数据类型:

    数据类型 描述
    fixed, fixed2, fixed3, fixed4 低精度浮点值,使用11位精度进行存储,数据区间为[-2.0, 2.0],用于存储颜色、标准化后的向量等
    half, half2, half3, half4 中精度浮点值,使用16位精度进行存储,数值区间为[-6000, 6000]
    float, float2, float3, float4 高精度浮点值,使用32为精度进行存储,用于存储顶点坐标、未标准化的向量、纹理坐标等
    struct 结构体,可以将多个变量整体进行打包

语义

当使用CG语音编写着色器函数的时候,函数的输入参数和输出参数都需要填充一个语义来表示它们要传递的数据信息。参数后被冒号隔开并且全部大写的关键词就是语义。

  1. 顶点着色器输入语义
语义 描述
POSITION 顶点的坐标信息,通常为float3或者float4类型
NORMAL 顶点的法线信息,通常为float3类型
TEXCOORD0 模型的第一套UV坐标,通常为float2、float3或者float4类型,TEXCOORD0到TEXCOOED3分别对应第一到第四套UV坐标
TANGENT 顶点的切向量,通常为float4类型
COLOR 顶点的颜色信息,通常为float4类型
  1. 片段着色器输入语义

顶点着色器的输出即为片段着色器的输入。

语义 描述
SV_POSITION 顶点在裁切空间下的坐标,float4类型
TEXCOORD0、TEXCOORD1等 用于声明任意高精度的数据,例如纹理坐标、向量等
COLOR0、COLOR1等 用于声明任意低精度的数据,例如顶点颜色、数值区间[0, 1]的变量
  1. 片段着色器输出语义

片段着色器通常只会输出一个fixed4类型的颜色信息,输出的值会存储到渲染目标(Render Target)中,输出参数使用SV_TARGET语义进行填充。

Unity关于语义的文档:https://docs.unity3d.com/Manual/SL-ShaderSemantics.html

HLSL关于语义的文档:https://docs.microsoft.com/zh-cn/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics?redirectedfrom=MSDN

在CG中调用属性变量

  1. CG中声明属性变量

    Shader通过Properties代码块声明开放出来的属性,若要在Shader程序中访问这些属性,则需要在CG代码块中再次进行声明,它的语法为: type name; type为变量的类型,name为属性变量的名称。

    开放属性与CG属性变量的对应关系:

    开放属性的类型 CG中属性变量的类型
    Float,Range 浮点和范围类型的属性,根据精度可以使用float,halt或fixed声明
    Color,Vector 颜色和向量类的属性,可以使用float4,half4或fixed4声明,其中颜色使用低精度的fixed4声明可以减少性能消耗
    2D 2D纹理贴图属性,使用sample2D声明
    Cube 立方体贴图属性,使用sampleCube声明
    3D 3D纹理贴图属性,使用sample3D声明
  2. 在Shader中使用贴图

    纹理贴图除了需要在CG代码块中再次声明外,还需要额外声明一个变量用于存储贴图的其他信息。如在使用贴图的时候经常会用到平铺(Tiling)和偏移(Offset)属性,额外声明的变量就是为了存储这些信息。

    在CG中,声明一个纹理变量的Tiling和Offset的语法结构如下所示:

    float4 {TextureName} _ST;

    • TextureName:纹理属性的名称;
    • ST:Scale和Transform的首字母,表示UV的缩放和平移;

    在CG所声明的变量为float4类型,其中x和y分量分别为Tiling的X值和Y值,z和w分量分别为Offset的X值和Y值。

    纹理坐标的计算公式为:

    \[texcoord = uv·TextureName.xy + [TextureName.zw](http://TextureName.zw)\]

    2D纹理采样函数: tex2D(_MainTex, texcoord)

  3. 在Shader中使用立方体贴图

    立方体贴图(Cubemap)由前、后、左、右、上、下六个方向组成的立方体盒子,对于立方体贴图的采样所使用的函数为: texCUBE(Cube, r); ,其中Cube表示立方体贴图,r表示视线方向在物体表面上的反射方向。

    Shader "Custom/Cubemap Property"
    {
        Properties
        {
            _MainTex ("MainTex", 2D) = "white" {}
            _MainColor ("MainColor", Color) = (1, 1, 1, 1)
       
            // 添加Cubemap属性和反射强度
            _Cubemap ("Cubemap", Cube) = "" {}
            _Reflection ("Reflection", Range(0, 1)) = 0
        }
        SubShader
        {
            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
       
                sampler _MainTex;
                float4 _MainTex_ST;
                fixed4 _MainColor;
       
                struct appdata
                {
                    float4 vertex : POSITION;
                    float3 normal : NORMAL;
                    float4 uv : TEXCOORD0;
                };
       
                struct v2f
                {
                    float4 position : SV_POSITION;
                    float4 worldPos : TEXCOORD0;
                    float3 worldNormal : TEXCOORD1;
                    float2 texcoord : TEXCOORD2;
                };
       
                // 声明Cubemap和反射属性变量
                samplerCUBE _Cubemap;
                fixed _Reflection;
       
                v2f vert (appdata v)          
                {
                    v2f o;
                    o.position = UnityObjectToClipPos(v.vertex);
       
                    // 将顶点坐标变换到世界空间
                    o.worldPos = mul(unity_ObjectToWorld, v.vertex);
       
                    // 将法线向量变换到世界空间
                    o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
                    o.worldNormal = normalize(o.worldNormal);
       
                    o.texcoord = v.uv * _MainTex_ST.xy + _MainTex_ST.zy;
       
                    return o;
                }
       
                fixed4 frag (v2f i) : SV_Target
                {
                    fixed4 main = tex2D(_MainTex, i.texcoord) * _MainColor;
       
                    // 计算世界空间中从摄像机指向顶点的方向向量
                    float3 viewDir = i.worldPos.xyz - _WorldSpaceCameraPos;
                    viewDir = normalize(viewDir);
       
                    // 套用公式计算反射向量
                    float3 refDir = 2 * dot(-viewDir, i.worldNormal)
                                    * i.worldNormal + viewDir;
                    refDir = normalize(refDir);
       
                    // 对Cubemap采样
                    fixed4 reflection = texCUBE(_Cubemap, refDir);
       
                    // 使用_Reflection对颜色和反射进行线性插值计算
                    fixed4 color = lerp(main, reflection, _Reflection);
       
                    return color;
                }
                ENDCG
            }
        }
    }
    

结构体

由于函数有多个输入和输出,为了使代码编写更加方便美观,故有一个新的数据类型——结构体。

  1. 结构体语法

    结构体允许存储多个不同类型的变量,并将多个变量包装成为一个整体进行输入或者输出。语法如下:

    struct Type
    {
    	//变量1;
    	//变量2;
    	//变量n;
    };
    
  2. 返回结构体的函数

Unity的包含文件

包含文件的使用语法

{
	CGPROGRAM
	// ...
	# include "UnityCG.cginc"
	// ...
	ENDCG
}

UnityCG.cginc

顶点着色器输入结构体

//appdata基础结构体
struct appdata_base {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

//appdata切向量结构体
struct appdata_tan {
    float4 vertex : POSITION;
    float4 tangent : TANGENT;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

//appdata完整结构体
struct appdata_full {
    float4 vertex : POSITION;
    float4 tangent : TANGENT;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    float4 texcoord1 : TEXCOORD1;
    float4 texcoord2 : TEXCOORD2;
    float4 texcoord3 : TEXCOORD3;
    fixed4 color : COLOR;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

//appdata图像特效结构体
struct appdata_img
{
    float4 vertex : POSITION;
    half2 texcoord : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

//v2f图像特效结构体
struct v2f_img
{
    float4 pos : SV_POSITION;
    half2 uv : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};

顶点变换函数

函数 说明
float4 UnityObjectToClipPos(float3 pos) 将顶点从模型空间变换到齐次裁切空间,等同于 mul(UNITY_MATRIX_MVP, float4(pos, 1.0))
float4 UnityObjectToViewPos(float3 pos) 将顶点从模型空间变换到摄像机空间,等同于 mul(UNITY_MATRIX_MV, float4(pos, 1.0)).xzy 。当输入为float4类型,Unity会自动重载为float3类型。
float4 UnityWorldToClipPos(float3 pos) 将顶点从世界空间变换到齐次裁切空间,等同于 mul(UNITY_MATRIX_VP, float4(pos, 1.0))
float4 UnityWorldToViewPos(float3 pos) 将顶点从世界空间变换到摄像机空间,等同于 mul(UNITY_MATRIX_V, float4(pos, 1.0)).xyz
float4 UnityViewToClipPos(float3 pos) 将顶点从摄像机空间变换到齐次裁切空间,等同于 mul(UNITY_MATRIX_P, float4(pos, 1.0))

向量变换函数

函数 说明
float3 UnityObjectToWorldDir(float3 dir) 将向量从模型空间转换到世界空间,已经标准化处理
float3 UnityWorldToObjectDir(float3 dir) 将向量从世界空间转换到模型空间,已经标准化处理
float3 UnityObjectToWorldNormal(float3 normal) 将发现从模型空间转换到世界空间,已经标准化处理

灯光辅助函数

注:以下函数仅适用于前向渲染路径(ForwardBase 或 ForwardAdd Pass 类型)

函数 说明
float3 WorldSpaceLightDir(float4 localPos) 输入模型空间顶点坐标,返回世界空间中从顶点指向灯光的向量,没有被标准化(不建议使用)
float3 UnityWorldSpaceLightDir(float4 worldPos) 输入世界空间顶点坐标,返回世界空间中从顶点指向灯光的向量,没有被标准化
float3 ObjSpaceLightDir(float4 v) 输入模型空间顶点坐标,返回模型空间中从顶点指向灯光的向量,没有被标准化
float3 Shade4PointLights(float4 localPos) 输入一系列所需变量,返回4个点光源的光照信息,在前向渲染中使用这个函数计算逐顶点的光照

视角向量函数

函数 说明
float3 WorldSpaceViewDir(float4 v) 输入模型空间顶点,返回世界空间中从顶点指向摄像机的向量,没有被标准化(不建议使用)
float3 UnityWorldSpaceViewDir(float3 v) 输入世界空间顶点,返回世界空间中从顶点指向摄像机的向量,没有被标准化
float3 ObjSpaceViewDir(float4 v) 输入模型空间顶点,返回模型空间中从顶点指向摄像机的向量,没有被标准化

其他辅助函数和宏

宏:在使用之前需要先定义,通过一个标识符代替一个字符串。

宏定义的语法结构为:

  • #define:表示宏定义的指令;
  • name:宏名称,后续可以直接输入名称进行使用;
  • string:编译的时候要把宏名称替换成的内容,可以是数字、表达式、函数等;

例如:

// Transforms 2D UV by scale/bias property
#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)

UnityCG.cginc中的其他常用辅助函数和宏

函数 说明
TRANSFORM_TEX(tex,name) 宏定义,输入UV坐标和纹理名称,得到贴图的纹理坐标
fixed3 UnpackNormal(fixed4 packednormal) 将法线向量从[0,1]映射到[-1,1]
half Luminance(half3 rgb) 将颜色数据转变为灰度数据
float4 ComputeScreenPos(float4 pos) 输入裁切空间顶点坐标,得到屏幕空间纹理坐标,用于屏幕空间纹理映射
float4 ComputeGrabScreenPos (float4 pos) 输入裁切空间顶点坐标,得到采样GrabPass的纹理坐标

UnityShaderVariables.cginc

在UnityShaderVariables.cginc文件中,Unity提供了一些内置的全局变量,例如,变换矩阵、灯光参数、时间变量等。

空间变换矩阵

矩阵 说明
UNITY_MATRIX_MVP 模型-观察-投影矩阵,用于将顶点/向量从模型空间变换到裁切空间
UNITY_MATRIX_MV 模型-观察矩阵,用于将顶点/向量从模型空间变换到摄像机空间
UNITY_MATRIX_V 观察矩阵,用于将顶点/向量从世界空间变换到摄像机空间
UNITY_MATRIX_P 投影矩阵,用于将顶点/向量从世界空间变换到裁切空间
UNITY_MATRIX_T_MV UNITY_MATRIX_MV的转置矩阵
UNITY_MATRIX_IT_MV UNITY_MATRIX_MV的逆转置矩阵
unity_ObjectToWorld 模型矩阵,用于将顶点/向量从模型空间变换到世界空间
unity_WorldToObject unity_ObjectToWorld的逆矩阵,用于将顶点/向量从世界空间变换到模型空间

时间变量

变量 说明
_Time 关卡从开始到现在所运行的时间,4个分量分别为t/20, t, t2, t3
_SinTime 将运行时间(t/8, t/4, t/2, t)输入到正弦函数
_CosTime 将运行时间(t/8, t/4, t/2, t)输入到余弦函数
unity_DeltaTime 每一帧的递增时间,4个分量分别为dt, 1/dt, smoothDt, 1/smoothDt

Shader中的光照模型

Lambert光照模型

当光线照射到表面粗糙的物体,光线会向各个方向等强度的反射,这种现象称为光的漫反射现象(Diffuse)。漫反射满足Lambert定律:反射光线的强度与表面法线和光源方向之间的夹角成正比。

Lambert光照模型的计算公式为:

\[C_{diffuse} = (C_{light}\cdot M_{diffuse}) saturate(\bold{n} \cdot \bold{l})\]

其中,$C_{diffuse}$为物体的漫反射颜色,$C_{light}$为入射光线的颜色,$M_{diffuse}$为物体材质的漫反射颜色,$\bold{n}$为物体的表面法线,$\bold{l}$为从物体指向灯光的方向。

前向渲染中可以使用的灯光属性变量

变量 类型 说明
_LightColor0 fixed4 灯光的颜色乘上亮度,在UnityLightingCommon.cginc中被声明
_WorldSpaceLightPos0 float4 平行光属性:float4(世界空间灯光方向,0),其他灯光属性:float4(世界空间灯光方向,1)
_LightMatrix0 float4x4 世界到灯光的变换矩阵,用于采样灯光cookie和衰减贴图,在AutoLight.cginc中被声明
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0 float4 前四个非重要点光在世界空间的位置,只能用于ForwardBase pass
unity_4LightAtten0 float4 前四个非重要点光的衰减系数,只能用于ForwardBase pass
unity_LightColor half4[4] 前四个非重要点光的颜色,只能用于ForwardBase pass
unity_WorldToShadow float4x4[4] 世界到阴影的变换矩阵,一个用于聚光灯矩阵,最多4个用于串联平行光的矩阵
Shader "Chapter6/LambertByVertex"
{
    Properties
    {
        _MainColor ("Main Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            // 声明包含灯光变量的文件
            #include "UnityLightingCommon.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                fixed4 dif : COLOR0;
            };

            fixed4 _MainColor;
    
            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                // 法线向量
                float3 n = UnityObjectToWorldNormal(v.normal);
                n = normalize(n);

                // 灯光方向向量
                fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                
                // 按照公式计算漫反射
                o.dif = _LightColor0 * _MainColor * saturate(dot(n, l));

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return i.dif;
            }
            ENDCG
        }
    }
}

Half-Lamber光照模型

使用Lamber光照模型有一个明显的缺点,就是物体背光面完全是黑的,如图所示:

1

于是(Valve公司在开发《半条命》时)提出一种基于Lamber进行算法优化的Half-Lamber光照模型。Half-Lamber光照模型的计算公式为:

\[C_{diffuse} = (C_{light}\cdot M_{diffuse}) [\alpha(\bold{n} \cdot \bold{l})+\beta]\]

Half-Lamber对$\bold{n} \cdot \bold{l}$点积的结果进行了一个$\alpha$倍的缩放再加上一个$\beta$大小的偏移,绝大多数情况下,$\alpha$小的值均为0.5,通过这样的方式,可以把$\bold{n} \cdot \bold{l}$点积的结果范围从[-1,1]映射到[0,1]的范围内,从而使得背光面也有明暗变化。最终效果如下图所示:

2

Shader "Chapter6/Half-LambertByVertex"
{
    Properties
    {
        _MainColor ("Main Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            // 声明包含灯光变量的文件
            #include "UnityLightingCommon.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                fixed4 dif : COLOR0;
            };

            fixed4 _MainColor;
    
            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                // 法线向量
                float3 n = UnityObjectToWorldNormal(v.normal);
                n = normalize(n);

                // 灯光方向向量
                fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                
                // 按照公式计算漫反射
                o.dif = _LightColor0 * _MainColor * saturate(0.5 * dot(n, l) + 0.5);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return i.dif;
            }
            ENDCG
        }
    }
}

Phong光照模型

Lambert光照模型无法对表面光滑的物体进行很好地表现,因此Bui Tuong Phong提出了一种局部光照经验模型,他认为物体表面反射光线由三部分组成:

\[SurfaceColor = C_{Ambient} + C_{Diffuse} + C_{Specular}\]

其中,$C_{Ambient}$为环境光,$C_{Diffuse}$为漫反射,$C_{Specular}$为镜面反射。

镜面反射的计算公式为:

\[C_{Specular}=(C_{light}\cdot M_{Specular})saturate(\bold{v}\cdot\bold{r})^{M_{shininess}}\]

其中,$C_{light}$为灯光亮度,$M_{Specular}$为物体材质的镜面反射颜色,$\bold{v}$为视角方向(由顶点指向摄像机的方向),$\bold{r}$为光线的反射方向,$M_{shininess}$为物体材质的光泽度。

Shader中可以使用的环境光变量

变量 类型 说明
unity_AmbientSky fixed4 Gradient类型环境中的Sky Color
unity_AmbientEquator fixed4 Gradient类型环境中的Equator Color
unity_AmbientGround fixed4 Gradient类型环境中的Ground Color
UNITY_LIGHTMODEL_AMBIENT fixed4 Gradient类型环境中的Sky Color,将被unity_AmbientSky取代

当前环境光的设置可以从 Window > Rendering > Lighting Setting 中查看。

Shader "Chapter6/PhongByVertex"
{
    Properties
    {
        _MainColor ("Main Color", Color) = (1,1,1,1)
        _SpecularColor ("Specular Color", Color) = (1,1,1,1)
        _Shininess ("Shininess", Range(1, 100)) = 1
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                fixed4 color : COLOR0;
            };

            fixed4 _MainColor;
            fixed4 _SpecularColor;
            half _Shininess;		

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                // 计算公式中的所有变量
                float3 n = UnityObjectToWorldNormal(v.normal);
                n = normalize(n);
                fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 view = normalize(WorldSpaceViewDir(v.vertex));

                // 漫反射部分
                fixed ndotl = saturate(dot(n, l));
                fixed4 dif = _LightColor0 * _MainColor * ndotl;

                // 镜面反射部分
                float3 ref = reflect(-l, n);
                ref = normalize(ref);
                fixed rdotv = saturate(dot(ref, view));
                fixed4 spec = _LightColor0 * _SpecularColor * pow(rdotv, _Shininess);

                // 环境光+漫反射+镜面反射
                o.color = unity_AmbientSky + dif + spec;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return i.color;
            }
            ENDCG
        }
    }
}

逐像素光照

逐顶点光照,也被称为高洛德着色(Gouraud shading)。在逐顶点光照中,在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最终输出成像素颜色。

由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往较小,但由于逐顶点光照依赖于线性插值来得到像素光照,因此,当光照模型中有非线性的计算(如高光反射)时,逐顶点光照就会出现问题,如下图所示:

3

逐像素光照,也被称为Phong着色(Phong shading)。在逐像素光照中,以每个像素为基础,得到它的法线(可以是对顶点法线插值得到的,也可以是从法线纹理中采样得到的),然后进行光照模型的计算,计算量较大,但不会产生棱角现象,如下图所示:

4

示例代码如下:

Shader "Chapter6/PhongPerPixel"
{
    Properties
    {
        _MainColor ("Main Color", Color) = (1,1,1,1)
        _SpecularColor ("Specular Color", Color) = (1,1,1,1)
        _Shininess ("Shininess", Range(1, 100)) = 1
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 normal : TEXCOORD0;
                float4 vertex : TEXCOORD1;
            };

            fixed4 _MainColor;
            fixed4 _SpecularColor;
            half _Shininess;		

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.normal = v.normal;
                o.vertex = v.vertex;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 计算公式中的所有变量
                float3 n = UnityObjectToWorldNormal(i.normal);
                n = normalize(n);
                fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 view = normalize(WorldSpaceViewDir(i.vertex));

                // 漫反射部分
                fixed ndotl = saturate(dot(n, l));
                fixed4 dif = _LightColor0 * _MainColor * ndotl;

                // 镜面反射部分
                float3 ref = reflect(-l, n);
                ref = normalize(ref);
                fixed rdotv = saturate(dot(ref, view));
                fixed4 spec = _LightColor0 * _SpecularColor * pow(rdotv, _Shininess);

                // 环境光+漫反射+镜面反射
                fixed4 color = unity_AmbientSky + dif + spec;
                return color;
            }
            ENDCG
        }
    }
}

Blinn-Phong光照模型

Jim Blinn对Phong光照模型的算法进行了改进,提出了Blinn-Phong光照模型,不再使用反射向量 $\bold{r}$ 计算镜面反射,而是使用半角向量 $\bold{h}$ 代替 $\bold{r}$ , $\bold{h}$ 为表视角方向 $\bold{v}$ 和灯光方向 $\bold{l}$ 的角平分线方向。

5

反射向量的计算公式为:

\[\bold{r}=\bold{l}-2(\bold{l}\cdot\bold{n})\cdot\bold{n}\]

半角向量的计算公式为:

\[\bold{h} = normalize(\bold{v}+\bold{l})\]

可以看出,半角向量的计算相较于反射向量的计算要简单不少,可以大大提升计算效率。

Blinn-Phong镜面反射的计算公式为:

\[C_{Specular}=(C_{light}\cdot M_{Specular})saturate(\bold{n}\cdot\bold{h})^{M_{shininess}}\]

灯光阴影

渲染路径

在Unity里,渲染路径(Rendering Path)决定了光照是如何应用到Unity Shader中的。Unity支持多种类型的渲染路径,主要有两种:前向渲染路径(Forward Rendering Path)*和*延迟渲染路径(Deferred Rendering Path),还有另外两种:旧的延迟渲染路径(Legacy Deferred Rendering Path)顶点照明渲染路径(Vertex Lit **Rendering Path),但这两种渲染路径以提供兼容为主要目的。

  • 前向渲染路径的原理
  • 延迟渲染路径的原理

前向渲染和延迟渲染的特性对比如下表所示:

  延迟渲染 前向渲染
特性    
逐像素光照(法线贴图、灯光 cookie) 支持 支持
实时阴影 支持 需要满足某些条件
反射探针 支持 支持
深度和法线缓冲区 支持 需要添加额外的Pass
软粒子 支持 不支持
半透明物体 不支持 支持
抗锯齿 不支持 支持
灯光剔除蒙版 部分功能被限制 支持
光照的细节程度 所有灯光逐像素渲染 部分灯光逐像素渲染
性能    
单个逐像素照片所耗性能 取决于照亮的像素数量 取决于像素数量乘以灯光照亮的物体数量
物体被正常渲染需要的次数 1次 取决于逐像素光照的灯光数量
对于简单场景的开销
平台支持    
PC (Windows/Mac) Shader Model 3.0+ & MRT(Multiple Render Targets) 全部
移动设备(iOS/Android) OpenGL ES 3.0 和 MRT、Metal(在配备 A8 或更高 SoC 的设备上) 全部
主机 XB1、PS4 全部

参考资料:https://docs.unity3d.com/2019.4/Documentation/Manual/RenderingPaths.html

Pass标签

  • LightMode标签

    标签值 作用
    Always 除了主要平行光,其他灯光不会产生任何光照
    ForwardBase 用于计算主要平行光、逐顶点或者SH灯光、环境光和光照贴图,只能在前向渲染中使用
    ForwardAdd 为每一个逐像素灯光生成一个Pass进行光照计算,只能在前向渲染中使用
    Deferred 用于渲染 G-Buffer,只能在延迟着色中使用
    ShadowCaster 将物体的深度渲染到阴影贴图或者深度贴图中
  • PassFlags标签

    • PassFlags标签用于更改渲染流水线传递数据给Pass的方式。目前仅可以使用的值为OnlyDirectional
    • 当使用前向渲染的时候,这个标签使得只有主要平行光、环境光或灯光探针、光照贴图的数据才能传递给Shader,SH和逐顶点灯光不能传递数据。

内置的multi_compile

在默认状态下,前向渲染只支持一个投射阴影的平行光,如果想要修改默认状态,就需要添加多重编译指令。

  • multi_compile_fwdbase :编译ForwardBass Pass中的所有变体,用于处理不同类型的光照贴图,并为主要平行光开启或者关闭阴影(adds this set of keywords: DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_COMBINED DYNAMICLIGHTMAP_ON SHADOWS_SCREEN SHADOWS_SHADOWMASK LIGHTMAP_SHADOW_MIXING LIGHTPROBE_SH. These variants are needed by PassType.ForwardBase);
  • multi_compile_fwdadd :编译ForwardBass Pass中的所有变体,用于处理平行光、聚光灯和点光源,以及它们的cookie纹理(adds this set of keywords: POINT DIRECTIONAL SPOT POINT_COOKIE DIRECTIONAL_COOKIE. These variants are needed by PassType.ForwardAdd);
  • multi_compile_fwdadd_fullshadows :与 multi_compile_fwdadd 类似,但是增加了灯光投射实时阴影的效果(adds this set of keywords: POINT DIRECTIONAL SPOT POINT_COOKIE DIRECTIONAL_COOKIE SHADOWS_DEPTH SHADOWS_SCREEN SHADOWS_CUBE SHADOWS_SOFT SHADOWS_SHADOWMASK LIGHTMAP_SHADOW_MIXING);

参考资料:https://docs.unity3d.com/2019.4/Documentation/Manual/SL-MultipleProgramVariants.html

实现阴影效果

整个渲染过程分两部分完成:

  • 第一个部分为基础Pass,用于渲染主要平行光和逐顶点或SH的灯光,并为主要平行光产生阴影投射;
  • 第二个部分为额外Pass,用于渲染其他逐像素的灯光,并且在这个Pass中也为其他逐像素的灯光产生了阴影投射;

具体代码如下:

Shader "Chapter6/Shadow Shader"
{
    Properties
    {
        _MainColor ("Main Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        // ---------- 基础 Pass 为主要平行光产生投影 ----------
        Pass
        {
            // 将Pass的光照模式设置为前向渲染基础Pass
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase   // 为在当前Pass中渲染的每个灯光编译出不同的Shader变体
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 normal : TEXCOORD0;
                float4 vertex : TEXCOORD1;
                SHADOW_COORDS(2)            // 使用预定义宏保存阴影坐标,括号内的数字表示TEXCOORD语义后的序号,此处0和1已经被法线向量和顶点坐标占用
            };

            fixed4 _MainColor;
    
            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.normal = v.normal;
                o.vertex = v.vertex;
                TRANSFER_SHADOW(o);     // 使用预定义宏变换阴影坐标(变换阴影贴图的纹理坐标并存入结构体中),宏括号内表示要存入的结构体名称
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 准备变量
                float3 n = UnityObjectToWorldNormal(i.normal);
                n = normalize(n);

                float3 l = WorldSpaceLightDir(i.vertex);
                l = normalize(l);

                float4 worldPos = mul(unity_ObjectToWorld, i.vertex);

                // Lambert 光照
                fixed ndotl = saturate(dot(n, l));
                fixed4 color = _LightColor0 * _MainColor * ndotl;

                // 加上4个点光源的光照
                color.rgb += 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.rgb, n
                ) * _MainColor;

                // 加上环境光照
                color += unity_AmbientSky;

                // 使用预定义宏计算阴影遮罩
                UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb);
                // 阴影合成
                color.rgb *= shadowmask;

                return color;
            }
            ENDCG
        }

        // ---------- 额外 Pass 为其他逐像素的灯光产生投影 ----------
        Pass
        {
            Tags { "LightMode" = "ForwardAdd" }

            // 使用相加混合,使绘制的图像与上一个Pass完全混合
            Blend One One

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdadd_fullshadows    // 为当前Pass中渲染的灯光编译出不同的Shader变体,并产生阴影投射
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 normal : TEXCOORD0;
                float4 vertex : TEXCOORD1;
                SHADOW_COORDS(2)            // 使用预定义宏保存阴影坐标
            };

            fixed4 _MainColor;
    
            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.normal = v.normal;
                o.vertex = v.vertex;
                TRANSFER_SHADOW(o);     // 使用预定义宏变换阴影坐标
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 准备变量
                float3 n = UnityObjectToWorldNormal(i.normal);
                n = normalize(n);

                float3 l = WorldSpaceLightDir(i.vertex);
                l = normalize(l);

                float4 worldPos = mul(unity_ObjectToWorld, i.vertex);

                // Lambert 光照
                fixed ndotl = saturate(dot(n, l));
                fixed4 color = _LightColor0 * _MainColor * ndotl;

                // 加上4个点光源的光照
                color.rgb += 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.rgb, n
                ) * _MainColor;

                // 使用预定义宏计算阴影系数
                UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb);
                // 阴影合成
                color.rgb *= shadowmask;

                return color;
            }
            ENDCG
        }
       
    }
    FallBack "Diffuse"
}

透明效果

不透明物体的渲染顺序

当场景中有很多重叠物体的时候,Overdraw会导致性能急剧降低。为了避免不必要的性能浪费,Unity将不透明模型的渲染顺序设定为:近处的物体优先绘制,然后再绘制远处的物体

透明物体的渲染顺序

Unity为半透明物体单独设定了一个渲染队列(”Queue” = “Transparent”):半透明物体在所有不透明物体绘制完成之后再进行绘制。

Unity将半透明模型的渲染顺序设定为:远处的物体优先绘制,然后再绘制近处的物体

混合透明效果

混合指令以Blend关键词开始,后面接混合模式。它可以在Subshader中使用,也可以在Pass中使用。

可以使用的混合模式如下:

  • Blend Off :关闭混合处理,当Shader中没有添加任何混合指令的时候,默认就是关闭状态。

  • Blend SrcFactor DstFactor :开启混合处理,允许自定义混合模式。新渲染出来的图像被成为Source(源图像),简称Src;已经绘制完的图像被成为Destination(目标图像),简称Dst。SrcFactor为源图像的混合系数,DstFactor为目标图像的混合系数,最终源图像和目标图像按照如下公式进行图像混合:

    \[Color_{rgba}=Source_{rgba} \cdot SrcFactor + Destination_{rgba} \cdot DstFactor\]

    可以使用的混合系数有:

    名称 说明
    Zero 数值为0,用来让Source或Destination完全不能通过
    One 数值为1,用来让Source或Destination全部通过
    SrcColor 把Source的像素颜色用作混合系数
    DstColor 把Destination的像素颜色用作混合系数
    SrcAlpha 把Source的alpha数值用作混合系数
    DstAlpah 把Destination的alpha数值用作混合系数
    OneMinusSrcColor 将Source的像素颜色反相之后,用作混合系数
    OneMinusDstColor 将Destination的像素颜色反相之后,用作混合系数
    OneMinusSrcAlpha 将Source的alpha数值反相之后,用作混合系数
    OneMinusDstAlpah 将Destination的alpha数值反相之后,用作混合系数

    除此之外,Unity还提供了一些常用的混合指令:

    Blend SrcAlpha OneMinusSrcAlpha // Traditional transparency 传统的透明叠加
    Blend One OneMinusSrcAlpha // Premultiplied transparency 预乘透明?
    Blend One One // Additive 相加
    Blend OneMinusDstColor One // Soft additive 柔和相加
    Blend DstColor Zero // Multiplicative 相乘
    Blend DstColor SrcColor // 2x multiplicative 2倍相乘
    
  • Blend SrcFactor DstFactor, SrcFactorA DstFactorA :跟上述指令类似,但对于源图像和目标图像的alpha通道分别使用SrcFactorA和DstFactorA进行混合,计算公式如下:

    \[Color_{rgb}=Source_{rgb} \cdot SrcFactor + Destination_{rgb} \cdot DstFactor\] \[Color_{a}=Source_{a} \cdot SrcFactorA + Destination_{a} \cdot DstFactorA\]
  • BlendOp Op :使用其他操作进行图像混合,而不再只是进行颜色相加。常用混合操作如下:

    名称 说明
    Add 将Source和Destination进行相加
    Sub 用Source减去Destination
    RevSub 用Destination减去Source
    Min 类似于min(Source, Destination)
    Max 类似于max(Source, Destination)

    更多操作详见官方文档:https://docs.unity3d.com/Manual/SL-BlendOp.html

  • BlendOp OpColor, OpAlpha :跟上述指令类似,但是对于颜色和alpha通道分别使用OpColor和OpAlpha不同的操作。

混合透明效果(Alpha Blend)

Shader "Chapter7/Blending Transparency"
{
    Properties
    {
        _MainTex ("Main Tex", 2D) = "white" {}
        _MainColor ("Main Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        // 设置渲染状态
        Tags 
        { 
            "Queue" = "Transparent"
            "RenderType"="Transparent"
            "IgnoreProjector"="True"
        }

        Pass
        {
            Tags { "LightMode" = "ForwardBase" }

            // 设置渲染状态
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase
            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 worldPos : TEXCOORD0;
                float2 texcoord : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;
    
            v2f vert (appdata_base v)
            {
                v2f o;

                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldNormal = worldNormal;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
                worldLight = normalize(worldLight);

                fixed NdotL = saturate(dot(i.worldNormal, worldLight));
                fixed4 color = tex2D(_MainTex, i.texcoord);

                color.rgb *= _MainColor.rgb * NdotL * _LightColor0;
                color.rgb += unity_AmbientSky;

                // 通过_MainColor属性的a分量控制透明度
                color.a *= _MainColor.a;

                return color;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

半透明物体的双面渲染

将不透明物体的正面和背面分为两个Pass,渲染完背面后再渲染正面。两个Pass混合之后,最终实现双面透明的效果。

注意:由于Shader使用了两个Pass,同一个物体会执行两次渲染,因此会比不透明物体更耗费性能。

Shader "Chapter7/TwoSideTransparency"
{
    Properties
    {
        _MainTex ("Main Tex", 2D) = "white" {}
        _MainColor ("Main Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        // 设置渲染状态
        Tags 
        { 
            "Queue" = "Transparent"
            "RenderType"="Transparent"
            "IgnoreProjector"="True"
        }

        // 渲染背面
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }

            // 设置渲染状态
            Cull Front // 开启正面剔除
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase
            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 worldPos : TEXCOORD0;
                float2 texcoord : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;
    
            v2f vert (appdata_base v)
            {
                v2f o;

                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldNormal = worldNormal;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
                worldLight = normalize(worldLight);

                fixed NdotL = saturate(dot(i.worldNormal, worldLight));
                fixed4 color = tex2D(_MainTex, i.texcoord);

                color.rgb *= _MainColor.rgb * NdotL * _LightColor0;
                color.rgb += unity_AmbientSky;

                // 通过_MainColor属性的a分量控制透明度
                color.a *= _MainColor.a;

                return color;
            }
            ENDCG
        }

        // 渲染正面
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }

            // 设置渲染状态
            Cull Back // 开启背面剔除
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase
            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 worldPos : TEXCOORD0;
                float2 texcoord : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;
    
            v2f vert (appdata_base v)
            {
                v2f o;

                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldNormal = worldNormal;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
                worldLight = normalize(worldLight);

                fixed NdotL = saturate(dot(i.worldNormal, worldLight));
                fixed4 color = tex2D(_MainTex, i.texcoord);

                color.rgb *= _MainColor.rgb * NdotL * _LightColor0;
                color.rgb += unity_AmbientSky;

                // 通过_MainColor属性的a分量控制透明度
                color.a *= _MainColor.a;

                return color;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

透明测试效果

游戏场景中会经常遇到:某些部位完全透明而其他部位完全不透明(如树叶),如果继续使用Alpha Blend方法,在延迟着色渲染路径中物体将无法接受投影,并且凸起的或者重叠的部分也会出现渲染顺序错误的问题(?)。

HLSL提供了 clip() 指令,用于在像素着色器中丢弃某些数值小于0的像素。

透明测试效果(Alpha Test)

Shader "Chapter7/Alpha Test Transparent"
{
    Properties
    {
        _MainTex ("Main Tex", 2D) = "white" {}
        _AlphaTest ("Alpha Test", Range(0, 1)) = 0.5
    }
    SubShader
    {
        // 设置渲染状态
        Tags 
        { 
            "Queue" = "AlphaTest"
            "RenderType"="TransparentCutout"
            "IgnoreProjector"="True"
        }

        Pass
        {
            Tags { "LightMode" = "ForwardBase" }

            // 关闭几何体剔除
            // Cull Off
            // AlphaToMask On

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 worldPos : TEXCOORD0;
                float2 texcoord : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _AlphaTest;
    
            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldNormal = normalize(worldNormal);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
                worldLight = normalize(worldLight);

                fixed NdotL = saturate(dot(i.worldNormal, worldLight));
                fixed4 color = tex2D(_MainTex, i.texcoord);

                clip(color.a - _AlphaTest);
                // Equal to 
                //  if ((color.a - _AlphaTest) < 0.0) {
                //		discard;
                //	}

                color.rgb *= NdotL * _LightColor0;
                color.rgb += unity_AmbientSky;

                return color;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

Alpha Test抗锯齿

当使用 Alpha Test 的时候,如果靠近了观察物体,会发现透明和不透明的边界位置有很明显的锯齿。因为显卡在计算的过程中产生了透明和不透明这两种极端的结果,中间并没有任何渐变的过程。

当使用多重采样抗锯齿(MultiSampling Anti-Aliasing, MSAA)的时候,可以通过在Pass中添加 AlphaToMask On 指令开启显卡的 alpha-to-coverage 功能。增加 MSAA 采样等级会相应提高多重采样的边界覆盖范围,从而消除透明测试着色器上的锯齿现象。

模板测试

模板测试的计算流程

在模板缓存中,每个像素都有一个8位( $2^8$ 也就是范围 0~255)的整数值,这被成为模板值。

模板值可以被改写,也可以递增或者递减。执行绘制调用(Draw Call)的时候,像素的参照值会与缓存中的模板值进行比较,如果结果不符合要求,该像素就会被丢弃。

模板测试的使用语法

模板测试的指令可以写在SubShader中,也可以写在Pass中,它们包含在 Stencil { } 代码块中。

模板测试的完整语法结构如下所示:

Stencil
{
		// 用来与缓存中已经存在的模板值进行比较的数值,被称为参照值
		// 当比较之后符合某些设定条件,这个数值可以被写进缓存
		// 数值的范围为0~255的整数,默认为0。
    Ref <ref>
		// 当读取参照值与模板值可以使用的时候,模板会指定哪些位的数值可以读取
		// 数值的范围为0~255的整数(即8位二进制11111111),默认为255
		// 如:当指定ReadMask为255,表示的是所有位都可以被读取
		ReadMask <readMask>
		// 当往缓存中写入的时候可以使用,模板会指定哪些位的数值允许写入缓存
		// 数值的范围为0~255的整数(即8位二进制11111111),默认为255
		// 如:当指定WriteMask为0,表示的是没有数值会被写入缓存
    WriteMask <writeMask>
		// 将参照值与缓存中的模板数值进行比较的方法,默认为always
    Comp <comparisonOperation>
		// 如果模板测试和深度测试都通过,缓存中的模板值如何处理,默认为keep
    Pass <passOperation>
		// 如果模板测试没有通过,缓存中的模板值如何处理,默认为keep
    Fail <failOperation>
		// 如果模板测试通过,但是深度测试没有通过,缓存中的模板值如何处理,默认为keep
    ZFail <zFailOperation>

		// 分别考虑正面和背面的像素
    CompBack <comparisonOperationBack>
    PassBack <passOperationBack>
    FailBack <failOperationBack>
    ZFailBack <zFailOperationBack>
    CompFront <comparisonOperationFront>
    PassFront <passOperationFront>
    FailFront <failOperationFront>
    ZFailFront <zFailOperationFront>
}

具体参考Unity官方文档:https://docs.unity3d.com/Manual/SL-Stencil.html

比较方法

比较方法 说明
Never 使当前渲染像素总是不能通过测试。Never render pixels.
Less 当前渲染像素的参照值小于缓存的时候才会通过测试。Render pixels when their reference value is less than the current value in the stencil buffer.
Equal 当前渲染像素的参照值等于缓存的时候才会通过测试。Render pixels when their reference value is equal to the current value in the stencil buffer.
LEqual 当前渲染像素的参照值小于或等于缓存的时候才会通过测试。Render pixels when their reference value is less than or equal to the current value in the stencil buffer.
Greater 当前渲染像素的参照值大于缓存的时候才会通过测试。Render pixels when their reference value is greater than the current value in the stencil buffer.
NotEqual 当前渲染像素的参照值不等于缓存的时候才会通过测试。Render pixels when their reference value differs from the current value in the stencil buffer.
GEqual 当前渲染像素的参照值大于或等于缓存的时候才会通过测试。Render pixels when their reference value is greater than or equal to the current value in the stencil buffer.
Always 使当前渲染像素总是通过测试。Always render pixels.

模板操作

操作 说明
Keep 继续保持缓存中的模板值。Keep the current contents of the stencil buffer.
Zero 把0写进缓存。Write 0 into the stencil buffer.
Replace 把当前像素的参照值写进缓存。Write the reference value into the buffer.
IncrSat 递增缓存中的模板值,如果数值已经为255,则停止递增。Increment the current value in the buffer. If the value is 255 already, it stays at 255.
DecrSat 递减缓存中的模板值,如果数值已经为0,则停止递减。Decrement the current value in the buffer. If the value is 0 already, it stays at 0.
Invert 将模板值按位取反。Negate all the bits of the current value in the buffer.
IncrWrap 递增缓存中的模板值,如果数值已经为255,则变为0。Increment the current value in the buffer. If the value is 255 already, it becomes 0.
DecrWrap 递减缓存中的模板值,如果数值已经为0,则变为255。Decrement the current value in the buffer. If the value is 0 already, it becomes 255.

延迟渲染路径中的模板测试

Unity 在 G-buffer Pass 和照明 Pass 中会将模板测试用于其他目的,并且 Unity 定义的模板状态会被忽略,因此模板测试会在延迟渲染路径中受到限制。因此,在延迟渲染路径中使用模板测试无法将物体屏蔽。

即便如此,依然可以在物体被渲染之后修改缓存中的模板值,而那些使用前向渲染路径的物体,会在延迟渲染路径之后再设置它们的模板状态,然后执行模板测试。

(看不懂!)

模板测试实现透明效果

本案例要实现的效果为:通过A物体在B物体上挖一个洞,从而可以透过这个洞看到后面的其他物体,因此需要针对A、B两个物体分别编写两个Shader。

Shader "Chapter7/StencilTestA"
{
    SubShader
    {
        Tags { "Queue"="Geometry-1" }

        Pass
        {
            // 设置模板测试的状态
            Stencil
            {
                Ref 1
                Comp Always
                Pass Replace
            }

            // 禁止绘制任何色彩
            ColorMask 0
            ZWrite Off

            CGPROGRAM
            #pragma vertex vert
			#pragma fragment frag

            float4 vert(in float4 vertex : POSITION) : SV_POSITION
            {
				float4 pos = UnityObjectToClipPos(vertex);
				
				return pos;
			}
			
			void frag(out fixed4 color: SV_Target)
            {
				color = fixed4(0, 0, 0, 0);
			}
            ENDCG
        } 
    }
    FallBack "Diffuse"
}
Shader "Chapter7/StencilTestB"
{
    Properties
    {
		_MainColor ("Main Color", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white" {}
	}
    SubShader
    {
        Tags { "Queue"="Geometry" }

        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            // 设置模板测试的状态
            Stencil
            {
                Ref 1
                Comp NotEqual
                Pass Keep
            }

            CGPROGRAM
            #pragma vertex vert
			#pragma fragment frag
            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 worldPos : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float2 texcoord : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;

            v2f vert(appdata_base v)
            {
				v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);

                o.worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
			}
			
			fixed4 frag(v2f i) : SV_Target
            {
                float3 worldLight = normalize(UnityWorldSpaceLightDir(i.worldPos.xyz));
                fixed4 color = tex2D(_MainTex, i.texcoord);

                color.rgb *= _MainColor * saturate(dot(i.worldNormal, worldLight)) * _LightColor0.rgb;
                color.rgb += unity_AmbientSky.rgb;

				return color;
			}
            ENDCG
        } 
    }
    FallBack "Diffuse"
}

最终效果如下图所示:

6

球在墙后,通过一个胶囊体在墙上挖了个洞,使得可以透过墙看到后面的球。

Image Effect

GrabPass

GrabPass 是一个比较特殊的Pass,它在运行的时候会抓取物体所在屏幕位置的渲染图像,然后传递给接下来的Pass进行图像处理,最终再将处理完成的图像输出。

因此,GrabPass Shader 其实是作用与渲染图像的,图像的范围是物体所在屏幕的位置。

语法结构

GrabPass有两种定义形式:

  • GrabPass { } :花括号中为空,也就是没有定义抓取图像的名称。用户可以使用Unity默认的名称 _GrabTexture 访问抓取到的图像。但需要注意,当场景中有大量物体使用这种形式的 GrabPass ,每个物体都会各自执行一遍屏幕抓取操作,因此比较耗费性能。
  • GrabPass {"TextureName"} :花括号中定义了抓取图像的名称。用户可以在接下来的 Pass 中使用 TextureName 访问到抓取的图像。即使场景中有再多物体使用这种形式的 GrabPass,所有物体也只会执行一遍屏幕抓取操作,每个物体都使用同一张抓取图像,因此比上一种形式的 GrabPass 更节省性能。

GrabPass Shader

Shader "Chapter10/GrabPass"
{
    Properties
    {
        _GrayScale ("Gray Scale", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "Queue"="Transparent" }
        GrabPass { "_ScreenTex" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
						#pragma fragment frag
            #include "UnityCG.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 grabPos : TEXCOORD0;
            };

            v2f vert(float4 vertex : POSITION)
            {
								v2f o;
                o.pos = UnityObjectToClipPos(vertex);

                // 计算抓取图像在屏幕上的位置
                o.grabPos = ComputeGrabScreenPos(o.pos);

                return o;
						}

            fixed _GrayScale;
            sampler2D _ScreenTex;
			
						half4 frag(v2f i) : SV_Target
            {
                // 采样抓取图像
                half4 src = tex2Dproj(_ScreenTex, i.grabPos);
                // Equal to
                // half4 src = tex2D(_ScreenTex, i.grabPos.xy / i.grabPos.w);

                half grayscale = Luminance(src.rgb); // Luminance()函数将src去色,得到单通道图像grayscale
                half4 dst = half4(grayscale, grayscale, grayscale, 1);

                return lerp(src, dst, _GrayScale);
						}
            ENDCG
        } 
    }
    FallBack "Diffuse"
}

GrabPass最终效果效果:

7

Post-Processing

后期处理的工作流程

后期处理本质上讲其实就是Unity在屏幕最上方叠了一个与屏幕同尺寸的四边形面片(加滤镜)。

后期处理起到图像处理作用的依然是Shader,只是它不能直接用于渲染模型,而是通过一个C#脚本添加到摄像机上才能运行。

在Unity中通过调用 OnRenderImage() 函数进行图像传输。渲染图像从摄像机渲染完成到最终呈现到屏幕上还需要经过source和destination两个存储区域:

  • source:摄像机渲染完的图像以渲染纹理的形式存储在这个区域,等待接下来的进一步处理;
  • destination:处理完的图像存储在这个区域,等待输出到屏幕上显示;

当有多个后期处理的时候,上一个后期处理执行完毕之后会将结果保存到RenderTexture中,传递给下一个后期处理中的source。下一个后期处理会继续按照上述流程进行重复。因此,增加后期处理的数量也会相应增加RenderTexture的数量,这会增加性能消耗。

后期处理Shader

Shader "Hidden/BrightnessSaturationContrast"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
        _Brightness ("Brightness", float) = 1
        _Saturation ("Saturation", float) = 1
        _Contrast ("Contrast", float) = 1
    }
    SubShader
    {
        Pass
        {
            Cull Off
            ZTest Always
            ZWrite Off

            CGPROGRAM
            #pragma vertex vert_img     // 使用包含文件内置的顶点着色器
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;     // 声明RenderTexture
            half _Brightness;
            half _Saturation;
            half _Contrast;

            // 将vert_img的输出结构体v2f_img输入到片段着色器
            half4 frag(v2f_img i) : SV_Target
            {
                // 使用v2f_img结构体内的纹理坐标对RenderTexture采样
                half4 renderTex = tex2D(_MainTex, i.uv);

                // 亮度
                half3 finalColor = renderTex.rgb * _Brightness;

                // 饱和度
                half luminance = Luminance(finalColor);
                finalColor = lerp(luminance, finalColor, _Saturation);

                // 对比度
                half3 grayColor = half3(0.5, 0.5, 0.5);
                finalColor = lerp(grayColor, finalColor, _Contrast);

                return half4(finalColor, 1);
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Chapter7
{
    [RequireComponent(typeof(Camera))]
    [ExecuteInEditMode]
    public class BrightnessSaturationContrast : MonoBehaviour
    {
        public Shader EffectShader;
        [Range(0, 2)]
        public float Brightness = 1f;
        [Range(0, 2)]
        public float Saturation = 1f;
        [Range(0, 2)]
        public float Contrast = 1f;

        private Material EffectMaterial;

        private void Start()
        {
            if (EffectShader)
                EffectMaterial = new Material(EffectShader);
        }

        private void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
            if (EffectMaterial == null && EffectShader != null)
            {
                EffectMaterial = new Material(EffectShader);
            }

            if (EffectMaterial != null)
            {
                EffectMaterial.SetFloat("_Brightness", Brightness);
                EffectMaterial.SetFloat("_Saturation", Saturation);
                EffectMaterial.SetFloat("_Contrast", Contrast);

                Graphics.Blit(source, destination, EffectMaterial);
            }
            else
            {
                Graphics.Blit(source, destination);
            }
        }
    }
}

Post Processing包

以后研究。

初级案例

流光效果

核心思想:通过 _Time 变量使纹理沿着一个固定的方向持续递增,然后使用该纹理坐标对纹理贴图进行采样,采样之后的纹理效果就会随着时间持续偏移,产生了流动的效果。

Shader "Samples/MatCap"
{
    Properties
    {
        [NoScaleOffset]_MatCap ("MatCap", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue" = "Geometry"}

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct v2f
            {
                float2 texcoord : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MatCap;

            v2f vert (appdata_base v)
            {
                v2f o;

                // 使用UNITY_MATRIX_MV的逆转置矩阵
                // 变换非统一缩放物体的法线向量
                float4 normal = mul(UNITY_MATRIX_IT_MV, float4(v.normal, 0));
                o.texcoord = normalize(normal.xyz).xy;  // 存放顶点在摄像机空间中的法线向量

                o.vertex = UnityObjectToClipPos(v.vertex);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 范围 [-1, 1] => [0, 1]
                float2 texcoord = i.texcoord * 0.5 + 0.5;

                return tex2D(_MatCap, texcoord);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

描边效果

有多种实现方式:

  • 后期处理?

  • MatCap?

  • 两个Pass

两个Pass的方法:

  1. 将模型的顶点位置沿着法线方向膨胀一段距离,然后再为膨胀之后的模型指定一个纯色进行着色(即为描边的颜色);
  2. 正常渲染模型;

存在的问题:深度问题。

解决方案:关闭第一个pass的深度写入,同时在所有不透明物体绘制完成后再绘制需要描边的物体。即:

Tags { "RenderType" = "Opaque" "Queue" = "Transparent" }
Shader "Samples/Outline"
{
    Properties
    {
        [Header(Texture Group)]
        [Space(10)]
        _Albedo ("Albedo", 2D) = "white" {}
        [NoScaleOffset]_Specular ("Specular (RGB-A)", 2D) = "black" {} // 添加[NoScaleOffset]指令以隐藏Tiling和Offset
        [NoScaleOffset]_Normal ("Normal", 2D) = "bump" {}
        [NoScaleOffset]_AO ("Ambient Occlusion", 2D) = "white" {}

        [Header(Outline Properties)]
        [Space(10)]
        _OutlineColor ("Outline Color", Color) = (1, 0, 1, 1)
        _OutlineWidth ("Outline Width", Range(0, 0.1)) = 0.01

    }

    SubShader
    {
        Tags {"RenderType" = "Opaque" "Queue" = "Transparent"}

        // --------------------- Outline Layer ---------------------
        Pass
        {
            ZWrite Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "unityCG.cginc"         

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            fixed4 _OutlineColor;
            fixed _OutlineWidth;

            v2f vert (appdata_base v)
            {
                v2f o;
                v.vertex.xyz += v.normal * _OutlineWidth;
                o.vertex = UnityObjectToClipPos(v.vertex);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return _OutlineColor;
            }
            ENDCG
        }

        // --------------------- Regular Layer ---------------------
        CGPROGRAM
        #pragma surface surf StandardSpecular fullforwardshadows

        struct Input
        {
            float2 uv_Albedo;
        };

        sampler2D _Albedo;
        sampler2D _Specular;
        sampler2D _Normal;
        sampler2D _AO;

        void surf(Input IN, inout SurfaceOutputStandardSpecular o)
        {
            fixed4 c = tex2D(_Albedo, IN.uv_Albedo);
            o.Albedo = c.rgb;

            fixed4 specular = tex2D(_Specular, IN.uv_Albedo);
            o.Specular = specular.rgb;
            o.Smoothness = specular.a;

            o.Normal = UnpackNormal(tex2D(_Normal, IN.uv_Albedo));
            o.Occlusion = tex2D(_AO, IN.uv_Albedo);
        }

        ENDCG
    }
}

遮挡半透效果

被遮挡的部分可以用类似菲涅尔效果实现。

菲涅尔效果是一种在半透明表面上模拟反射和折射的方法。简单来说,就是在表面法线和视线之间的角度越小,折射率就越高,反射就越弱;角度越大,反射就越强,折射就越弱。

菲涅尔效应的简单模拟算法,具体的实现方法如下:

\[Fresnel=(1-saturate(n\cdot v))^{width}\cdot{Brightness}\]
  1. n:物体在世界空间中的法线向量;
  2. v:摄像机在世界空间中的视角方向;
  3. width:菲涅尔效果边缘的宽度;
  4. Brightness:菲涅尔效果边缘的亮度;
Shader "Samples/X-Ray"
{
    Properties
    {
        // 半透效果设置
        [Header(The Blocked Part)]
        [Space(10)]
        _Color ("X-Ray Color", Color) = (0,1,1,1)
        _Width ("X-Ray Width", Range(1, 2)) = 1
        _Brightness ("X-Ray Brightness",Range(0, 2)) = 1

        // 正常渲染部分
        [Header(The Normal Part)]
        [Space(10)]
        _Albedo("Albedo", 2D) = "white"{}
        [NoScaleOffset]_Specular ("Specular (RGB-A)", 2D) = "black"{}
        [NoScaleOffset]_Normal ("Nromal", 2D) = "bump"{}
        [NoScaleOffset]_AO ("AO", 2D) = "white"{}
    }

    SubShader
    {
        Tags {"RenderType" = "Opaque" "Queue" = "Geometry"}

        // --------------------- The Blocked Layer ---------------------
        Pass
        {
            ZTest Greater   // 即使被遮挡也会显示
            ZWrite Off

            Blend SrcAlpha OneMinusSrcAlpha     // 半透效果需要通过Blending实现

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "unityCG.cginc"         

            struct v2f
            {
                float4 vertexPos : SV_POSITION;
                float3 viewDir : TEXCOORD0;
                float3 worldNor : TEXCOORD1;
            };

            fixed4 _Color;
            fixed _Width;
            half _Brightness;

            v2f vert(appdata_base v)
            {
                v2f o;
                o.vertexPos = UnityObjectToClipPos(v.vertex);
                o.viewDir = normalize(WorldSpaceViewDir(v.vertex));
                o.worldNor = UnityObjectToWorldNormal(v.normal);

                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                // Fresnel算法
                half NDotV = saturate( dot(i.worldNor, i.viewDir));
                NDotV = pow(1 - NDotV, _Width) * _Brightness;

                fixed4 color;
                color.rgb = _Color.rgb;
                color.a = NDotV;
                return color;
            }
            ENDCG
        }

        // --------------------- The Normal Layer ---------------------
        CGPROGRAM
        #pragma surface surf StandardSpecular
        #pragma target 3.0

        struct Input
        {
            float2 uv_Albedo;
        };

        sampler2D _Albedo;
        sampler2D _Specular;
        sampler2D _Normal;
        sampler2D _AO;

        void surf(Input IN, inout SurfaceOutputStandardSpecular o)
        {
            fixed4 c = tex2D(_Albedo, IN.uv_Albedo);
            o.Albedo = c.rgb;

            fixed4 specular = tex2D(_Specular, IN.uv_Albedo);
            o.Specular = specular.rgb;
            o.Smoothness = specular.a;

            o.Normal = UnpackNormal(tex2D(_Normal, IN.uv_Albedo));
            o.Occlusion = tex2D(_AO, IN.uv_Albedo);
        }

        ENDCG
    }
}

Tri-Planar Mapping效果

Tri-Planar Mapping效果是通过世界空间顶点坐标对纹理贴图进行采样

Shader "Samples/Tri-Planar Mapping"
{
    Properties
    {
        _Tiling ("Tiling", float) = 1
        [NoScaleOffset]_Albedo ("Albedo", 2D) = "white" {}
        [NoScaleOffset]_Normal ("Normal", 2D) = "bump" {}
        _Bumpiness ("Bumpiness", Range(0.01, 10)) = 1
    }
    SubShader
    {
        CGPROGRAM
        #pragma surface surf Lambert fullforwardshadows
        
        struct Input
        {
            float3 worldPos;
            float3 worldNormal;
            INTERNAL_DATA
        };

        float _Tiling;
        sampler2D _Albedo;
        sampler2D _Normal;
        half _Bumpiness;

        void surf (Input IN, inout SurfaceOutput o)
        {
            float3 texCoord = IN.worldPos * _Tiling;

            // -------------------- Mask --------------------
            float3 normal = abs(WorldNormalVector(IN, o.Normal));
            fixed maskX = saturate(dot(normal, fixed3(1, 0, 0)));
            fixed maskY = saturate(dot(normal, fixed3(0, 1, 0)));

            // -------------------- Albedo --------------------
            fixed4 colorXY = tex2D (_Albedo, texCoord.xy);
            fixed4 colorYZ = tex2D (_Albedo, texCoord.yz);
            fixed4 colorXZ = tex2D (_Albedo, texCoord.xz);

            fixed4 c;
            c = lerp(colorXY, colorYZ, maskX);
            c = lerp(c, colorXZ, maskY);

            o.Albedo = c.rgb;

            // -------------------- Normal --------------------
            fixed3 normalXY = UnpackNormal(tex2D(_Normal, texCoord.xy));
            fixed3 normalYZ = UnpackNormal(tex2D(_Normal, texCoord.yz));
            fixed3 normalXZ = UnpackNormal(tex2D(_Normal, texCoord.xz));

            fixed3 n;
            n = lerp(normalXY, normalYZ, maskX);
            n = lerp(n, normalXZ, maskY);

            o.Normal = n * half3(_Bumpiness, _Bumpiness, 1);
        }
        ENDCG
    }
    FallBack "Diffuse"
}

MatCap效果

MatCap全称Material Capture,是使用法线向量对纹理进行采样,它将贴图中间圆形的区域直接映射到屏幕中的物体上,使物体的渲染效果跟贴图的质感一模一样。

MatCap的优点:计算量少

实现逻辑:使用顶点在摄像机空间的法线向量对纹理进行采样,然后将采样结果直接返回给片段着色器进行输出即可。

Shader "Samples/MatCap"
{
    Properties
    {
        [NoScaleOffset]_MatCap ("MatCap", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue" = "Geometry"}

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct v2f
            {
                float2 texcoord : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MatCap;

            v2f vert (appdata_base v)
            {
                v2f o;

                // 使用UNITY_MATRIX_MV的逆转置矩阵
                // 变换非统一缩放物体的法线向量
                float4 normal = mul(UNITY_MATRIX_IT_MV, float4(v.normal, 0));
                o.texcoord = normalize(normal.xyz).xy;  // 存放顶点在摄像机空间中的法线向量

                o.vertex = UnityObjectToClipPos(v.vertex);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 范围 [-1, 1] => [0, 1]
                float2 texcoord = i.texcoord * 0.5 + 0.5;

                return tex2D(_MatCap, texcoord);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

物体切割效果

物体切割效果的主要思想:将需要被切掉的部分标记一个小于零的数值,然后调用clip()函数就可以将其剔除,从而达到切割的效果。

Shader "Samples/Object Cutting"
{
    Properties
    {
        [Header(Textures)] [Space(10)]
        [NoScaleOffset] _Albedo ("Albedo", 2D) = "white" {}
        [NoScaleOffset] _Reflection ("Specular_Smoothness", 2D) = "black" {}
        [NoScaleOffset] _Normal ("Normal", 2D) = "bump" {}
        [NoScaleOffset] _Occlusion ("Ambient Occlusion", 2D) = "white" {}

        [Header(Cutting)] [Space(10)]
        [KeywordEnum(X, Y, Z)] _Direction ("Cutting Direction", Float) = 1  // KeywordEnum枚举,切割方向
        [Toggle] _Invert ("Invert Direction", Float) = 0    // Toggle开关,反转切割方向
    }
    SubShader
    {
        Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" }
        Cull Off    // 关闭多边形剔除功能

        CGPROGRAM
        #pragma surface surf StandardSpecular addshadow fullforwardshadows
        #pragma target 3.0

        #pragma multi_compile _DIRECTION_X _DIRECTION_Y _DIRECTION_Z
        
        sampler2D _Albedo;
        sampler2D _Reflection;
        sampler2D _Normal;
        sampler2D _Occlusion;

        float3 _Position;      // 存放C#脚本传递来的切割平面坐标
        fixed _Invert;

        struct Input
        {
            float2 uv_Albedo;
            float3 worldPos;
            fixed face : VFACE;     // 保存物体表面朝向的信息,当正面朝向摄像机的时候,为正值,背面为负值
        };

        void surf (Input i, inout SurfaceOutputStandardSpecular o)
        {
            fixed4 col = tex2D(_Albedo, i.uv_Albedo);
            o.Albedo =  i.face > 0 ? col.rgb : fixed3(0,0,0);

            // 判断切割方向
            // step(a, b),当a大于b时输出0,反之输出1
            #if _DIRECTION_X
                col.a = step(_Position.x, i.worldPos.x);
            #elif _DIRECTION_Y
                col.a = step(_Position.y, i.worldPos.y);
            #else 
                col.a = step(_Position.z, i.worldPos.z);
            #endif

            // 判断是否反转切割方向
            col.a = _Invert? 1 - col.a : col.a;

            clip(col.a - 0.001);
            
            fixed4 reflection = tex2D(_Reflection, i.uv_Albedo);
            o.Specular = i.face > 0 ? reflection.rgb : fixed3(0,0,0);
            o.Smoothness = i.face > 0 ? reflection.a : 0;

            o.Normal = UnpackNormal(tex2D(_Normal, i.uv_Albedo));

            o.Occlusion = tex2D(_Occlusion, i.uv_Albedo);
        }
        ENDCG
    }
}

PS:什么时候用multi_compile,什么时候用shader_feature呢? 遵守以下几点就可以了:

  • 用shader_feature:

    • 编辑器中使用KeywordEnum来编辑的
    • 或是发布后确定不变的
    • 反正就是 shader 变体的开关由 material 材质中,使用到的变体的开关来决定的
  • 用multi_compile:

    • 发布后,运行时,可能需要变化的,如:游戏的画面设置

    • 通常不会在Properties定义 [KeywordEnum] 的方式来编辑的,因为会你项目编辑器下开发过程中,脚本中控制的关键字开关会有冲突的

      如: 你在Unity Editor Player下运行项目,打开了游戏画面设置,会对该材质的一个关键字控制启用或禁用,然后又选中了Project视图下该材质,该材质的Material Inspector显示的编辑器界面实际上有时实时的对该材质使用了[KeywordEnum]的关键字的控制启用或禁用,那么就会与你上面的脚本有冲突

另外注意 multi_compile 不要滥用,容易导致变体数量爆炸,变体多了就会导致 shader 变体时长增加,所以会影响打包时的 shader 变体时长,导致打包增加

除了增加编译时间,shader 变体还会增加包体打包,和运行时内存大小,显存大小

进阶案例

消融效果

动态液体效果

Billboard效果

序列帧动画

卡通风格效果

夜视仪后期处理