欢迎来到Shader的世界

Shader的学习之所以比学习C++、C#等编程语言难,一个原因就是因为Shader需要牵扯到对整个渲染流程的理解。

渲染流水线

应用阶段(Application Stage)、几何阶段(Geometry Stage)、光栅化阶段(Rasterizer Stage)

什么是shader

  • GPU流水线上一些课高度编程的阶段。

Unity Shader基础

  • Standard Surface Shader:包含标准关照迷行的shader;
  • Unlit Shader:不包含光效但包含雾效的基本shader;
  • Image Effect Shader:实现屏幕处理的shader;
  • Compute Shader:进行与渲染流水线无关的计算;

ShaderLab

基础结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Shader "ShaderName"
{
Properties
{
// 属性
}
SubShader
{
// 显卡1
}
SubShader
{
// 显卡2
}
Fallback "VertexLit"
}

shader名字:

1
Shader "Custom/MyShader"{} // 显示在Shader -> Custom -> MyShader

属性(Properties):

1
2
3
4
5
Properties
{
Name ("display name", PropertyType) = DefaultValue
Name ("display name", PropertyType) = DefaultValue
}

仿版在脚本与材质面板中方便的调整。

属性类型:Int, Float, Range(min, max), Color, Vector, 2D, 3D, Cube;

SubShader:

可以包含多个,但至少有一个。当加载这个shader的时候,unity会扫描所有SubShader语义块,选择第一个能够在对应平台运行的SubShader。如果都不支持,则使用Fallback指定的shader。

1
2
3
4
5
6
7
8
9
10
SubShader
{
// 可选的
[Tags] // 标签
// 可选的
[RenderSetup] // 状态

Pass {}
// 其他的Pass
}

SubShader中定义了一系列Pass,每个Pass中定义了一次完整的渲染流程。Pass数目过多会导致渲染性能的下降。状态和标签同样可以在Pass中声明,但有些标签设置SubShader和Pass不一样。在SubShader中进行这些设置将会作用于所有的Pass。

  • 状态设置:渲染状态的设置指令。

    状态名称 设置指令 解释
    Cull Cull Back | Front | Off 设置剔除模式:背面/正面/关闭
    ZTest ZTest Less Greater | LEqual |GEqual | Equal | NotEqual | Always 设置深度测试
    ZWrite ZWrite On | Off 开启关闭深度写入
    Blend Blend SrcFactor DstFactor 开启并设置混合模式

    可以在Pass语句中单独设置。

  • 标签:

    1
    Tags {"TagName1" = "Value1" "TagName2" = "Value2"}

    支持的标签块类型:

    • Queue:渲染顺序,比如透明不透明之类的。
    • RenderType:着色器分类,透明不透明,可以被利用于着色器替换。
    • DisableBatching:是否采用批处理。
    • ForceNoShadowCasting:是否会投射阴影。
    • IgnoreProjector:是否受Projector的影响,通常用于半透明物体。
    • CanUseSpriteAtlas:如果shader用于Sprites的时候设置为false。
    • PreviewType:设置预览,比如Plane或SkyBox啥的。

    这些标签只能在SubShader中声明,不能再Pass中声明。pass中也可以定义,但与这些不同。

  • Pass

    1
    2
    3
    4
    5
    6
    7
    Pass
    {
    [Name]
    [Tags]
    [RenderSetup]
    // 其他
    }

    Pass名称:

    1
    Name "MyPassName"

    通过这个可以在别的shader中使用这个Pass:

    1
    UsePass "MyShader/MYPASSNAME" // 必须大写

    Pass支持的标签类型:

    • LightMode:Pass在Unity渲染流水线中的角色。
    • RequireOptions:满足某些条件时才渲染这个Pass,SoftVegation。
  • UsePass:上面提到的一样。

  • GrabPass:抓取屏幕并将结果储存在纹理中,一遍后续的Pass处理。

Fallback:最低级的shader。

Unity Shader形式

表面着色器(Surface Shader):Unity自己创造的一种着色器代码类型,本质上还是顶点和片元着色器。渲染代价比较大,Unity为我们处理了很多光照细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Shader "Custom/Surface Shader"
{
SubShader
{
Tags {"RenderType" = "Opaque"}
CGPROGRAM
#pragma surface surf Lambert

struct Input
{
float4 color : COLOR;
};

void surf(Input IN, inout SurfaceOutput o)
{
o.Albedo = 1;
}
ENDCG
}
FallBack "Diffuse"
}

表面着色器不需要定义Pass。

顶点/片元着色器(Vertex/Fragment Shader):高灵活性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Shader "Custom/Simple VF Shader"
{
Subshader
{
Tags {}
Pass
{
Tags{}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

float4 vert(float v : POSITION) : SV_POSITION
{
return mul(UNITY_MATRIX_MVP, v);
}
fixed4 frag() : SV_TARGET
{
return fixed4(1, 0, 0, 1);
}
ENDCG
}
}
}

固定函数着色器(Fixed Function Shader):对于较旧的设备,不支持可编程渲染管线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Shader "Custom/Fixed Shader"
{
Properties
{
_Color ("Main Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Pass
{
Material
{
Diffuse [_Color]
}
Lighting On
}
}
}

学习Shader所需的数学基础

笛卡尔坐标系

  • 笛卡尔坐标系(Cartesian Coordinate System)
  • 基矢量(basis vector):坐标轴.
  • 正交基(orthogonal basis):互相垂直的基矢量.
  • 标准正交基(orthonormal basis):互相垂直且长度为1的基矢量.

Unity使用Y轴向上的左手系。但在观察空间,也就是相机坐标是右手系,摄像机的前向是-z方向。

点和矢量

矩阵

  • 正交矩阵(orthogonal matrix):方阵M和它的转置矩阵相乘是单位矩阵的话。一组标准正交基可以满足这个条件。

矩阵的几何意义:变换

坐标空间

模型变换 -> 透视投影 -> 剪裁 -> 齐次除法 -> 屏幕变换

法线变换 p86

Unity Shader的内置变量(数学篇)p87

变换矩阵

变量名 描述
UNITY_MATRIX_MVP MVP矩阵,用于将顶点和方向从模型空间转换到剪裁空间。
UNITY_MATRIX_MV 将顶点和方向从模型空间变换到观察空间
UNITY_MATRIX_V 将顶点和方向从世界空间变换到观察空间
UNITY_MATRIX_P 将顶点和方向从观察空间变换到剪裁空间
UNITY_MATRIX_VP 将顶点和方向从世界空间变换到剪裁空间
UNITY_MATRIX_T_MV MV的转置矩阵
UNITY_MATRIX_IT_MV MV的逆转置矩阵,可以转换法线,或者获得MV的逆矩阵
_Object2World 模型矩阵,模型空间到世界空间
_World2Object 从世界空间到模型空间
  • MVP -> UnityObjectToClipPos(*)
  • replaced ‘_World2Object’ with ‘unity_WorldToObject’
  • replaced ‘_Object2World’ with ‘unity_ObjectToWorld’

比如要将观察空间中的点或者方向变换到模型空间:

1
2
3
4
// 方法一:用转置矩阵
float4 modelPos = mul(transpose(UNITY_MATRIX_IT_MV), viewPos);
// 方法二:交换相乘
float4 modelPos = mul(viewPos, UNITY_MATRIX_IT_MV);

摄像机和屏幕参数:

变量名 类型 描述
_WorldSpaceCameraPos float3 该摄像机在世界空间中的位置
_ProjectionParams float4 x = 1.0, y = Near, z = Far, w = 1.0 + 1.0/Far
_ScreenParams float4 x = width, y = height, z = 1.0 + 1.0/width, w = 1.0 + 1.0/height
_ZBufferParams float4 x = 1 - Far/Near, y = Far/Near, z = x/Far, w = y/Far
unity_OrthoParams float4 x = width, y = height, z = null, w = 正交为1,透视为0
unity_CameraProjection float4x4 该摄像机的投影矩阵
unity_CameraInvProjection float4x4 该摄像机投影矩阵的逆矩阵
unity_CameraWorldClipPlanes[6] flloat4 6个剪裁平面在世界空间下的等式:左右下上近远

填充矩阵行优先。不过Unity在脚本中提供了一种Matrix4x4,这个是列优先。

屏幕坐标:语义是VPOS或WPOS。

开始Unity Shader的学习之旅

一个最简单的顶点/片元着色器

基本结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Shader "ShaderLearn/Simple VF Shader"
{
Properties
{
// 属性
}
SubShader
{
// 针对显卡A的SubShader
Pass
{
// 设置渲染状态和标签
// 开始CG代码片段
CGPROGRAM
// 该代码片段的编译指令,比如:
#pragma vertex vert
#pragma fragment frag

// CG代码写在这里
ENDCG
// 其他设置
}
// 其他需要的Pass
}
SubShader
{
// 针对显卡B的SubShader
}
// 上述SubShader都失败后回调的Shader
FallBack "VertexLit"
}

自定义结构体:

1
2
3
4
5
6
struct StructName
{
TypeName : Semantic;
TypeName : Semantic;
...
};

属性类型和变量类型对应:

  • Color, Vector -> float4, half4, fixed4
  • Range, Float -> float, half, fixed
  • 2D -> sampler2D
  • Cube -> samplerCube
  • 3D ->sampler3D

Unity提供的内置文件和变量

语义(semantics)

是不可省略的,告诉系统用户的需要哪些输入值,以及用户的输出是什么。

  • 从app传到顶点着色器

    语义 描述
    POSITION 模型空间中的顶点位置,float4
    NORMAL 顶点法线,float3
    TANGENT 顶点切线,float4
    TEXCOORDn 纹理坐标
    COLOR 顶点颜色,fixed4或float4
  • 顶点着色器传到片元着色器

    语义 描述
    SV_POSITION 裁剪空间中的顶点坐标
    COLOR0 第一组顶点颜色,非必须
    COLOR1 第二组顶点颜色,非必须
    TEXCOORD1-7 输出纹理坐标,非必须
  • 片元着色器输出,只有一个SV_TARGET语义。

渲染平台的差异

UNITY_INITIALIZE_OUTPUT(Input, o); 初始化输出。

DirectX 9/11不支持在顶点着色器中使用tex2D函数,可以使用tex2Dlod替代。

Shader整洁之道

三种精度:

  • float:最高精度浮点值,32位;
  • half:中等精度浮点值,16位,范围-60000 - 60000;
  • fixed:最低精度浮点值,11位,-2.0 - 2.0;

在现代桌面GPU中将所有都按float计算,所以无区别,只有移动平台有区别。且现代的移动平台GPU也将fixed和half当成同等精度对待。但不管怎么说,用尽可能低精度的。

尽量不在Shader中用分支语句。

Unity中的基础光照

标准光照模型

Phong shading:逐像素

Gouraud shading:逐顶点

在Unity Shader中实现漫反射光照模型

在只有一个平行光的情况下:

  • UNITY_LIGHTMODEL_AMBIENT:获取环境光照

  • _LightColor0:访问光源颜色和强度信息

  • _WorldSpaceLightPos0:光源方向

  • 逐顶点光照

  • 逐像素光照

在Unity Shader中实现高光反射光照模型

  • _WorldSpaceCameraPos:世界坐标相机位置

  • 逐顶点光照

  • 逐像素光照

使用Unity内置的函数

UnituCG.cginc中一些常用的函数:

  • float3 WorldSpaceViewDir (float4 v):输入模型空间的顶点位置,返回世界空间中的摄像机的观察方向。
  • float3 UnityWorldSpaceViewDir (float4 v):输入世界空间中的顶点位置,返回世界空间中的摄像机的观察方向。
  • float3 ObjSpaceViewDir (float4 v):输入模型空间中的顶点位置,返回模型空间中摄像机的观察方向。
  • float3 WorldSpaceLightDir (flaot4 v):仅可用于前向渲染。输入模型空间中的顶点位置,返回世界空间中的光照方向。
  • float3 UnityWorldSpaceLightDir (float4 v):仅可用于前向渲染。输入世界空间中的定点位置,返回世界空间中的光照方向,没有被归一化。
  • float3 ObjSpaceLightDir (float3 norm):仅可用于前向渲染。输入模型空间中的顶点位置,返回模型空间中的光照方向,没有被归一化。
  • float3 UnityObjectToWorldNormal (float3 norm):将法线从模型空间转换到世界空间。
  • float3 UnityObjectToWorldDir (in float3 dir):把方向矢量从模型空间变换到世界空间。
  • float3 UnityWorldToObjectDir (float3 dir ):把方向矢量从世界空间变换到模型空间。

基础纹理

凹凸映射

切线空间计算

世界空间计算

渐变纹理

半兰伯特模型、Wrap Mode: Clamp

遮罩纹理

透明效果

  • 透明度测试(Alpha Test):开启深度测试,根据透明度舍弃一些片元。要不就完全透明,要不就完全不透明。
  • 透明度混合(Alpha Blending):根据透明度进行混合。

先渲染不透明物体,再渲染半透明物体

半透明物体按照距离摄像机的远近排序,从后往前渲染

Unity Shader的渲染顺序

名称 队列索引号 描述
Background 1000 最先被渲染,绘制在背景上的物体
Geometry 2000 默认渲染队列,不透明物体。
AlphaTest 2450 透明度测试队列
Transparent 3000 透明度混合队列
Overlay 4000 最后渲染,叠加效果

透明度测试

透明度混合

Blend:

  • Blend Off:关闭混合
  • Blend SrcFactor DstFactor:开启混合并设置混合因子。源颜色(该片元产生的颜色)会乘以SrcFactor,目标颜色(已经存在在缓冲区的颜色会乘以DstFactor),将两者相加然后再存入颜色缓冲。
  • Blend SrcFactor DstFactor, SrcFactorA DstFactorA:和上面几乎一样
  • BlendOp BlendOperation:并非简单混合,使用BlendOperation进行其他操作。

开启深度写入的半透明效果

使用两个Pass:第一个开启深度写入,不输出颜色只为了把魔性的深度值写入深度缓冲;第二个进行正常的透明度混合。

这种方法可以实现模型与背后背景的混合,但模型内部之间不会有任何真正的半透明效果。

ShaderLab混合命令

源颜色(source color):片元着色器产生的颜色值;

目标颜色(destination color):颜色缓冲中的值;

设置混合因子的命令:Blend SrcFactor DstFactor、Blend SrcFactor DstFactor, SrcFactorSA DstFactorA

  • 混合因子:

    One:1

    Zero:0

    SrcColor:源颜色

    SrcAlpha:源颜色透明度值

    DstColor:目标颜色

    DstAlpha:目标颜色透明度值

    OneMinusSrcColor:1 - 源颜色

    OneMinusSrcAlpha:1 - 源颜色透明度值

    OneMinusDstColor:1 - 目标颜色

    OneMinusDstAlpha1 - 目标颜色透明度值

  • 混合操作:BlendOp BlendOperation

    Add:默认的相加

    Sub:源 - 目标

    RevSub:目标 - 混合

    Min:各个分量较小的

    Max:各个分量较大的

  • 常见混合类型

    正常:Blend SrcAlpha OneMinusSrcAlpha

    柔和相加:Blend OneMinusDstColor One

    正片叠底:Blend DstColor Zero

    两倍相乘:Blend DstColor SrcColor

    变暗:BlendOp Min Blend One One

    变亮:BlendOp Max Blend One One

    滤色:Blend OneMinusDstColor One

    线性减淡:Blend One One

双面渲染的透明效果

1
Cull Back | Front | Off
  • 透明度测试可以直接关闭Cull
  • 透明度混合需要两个Pass,一个只渲染背面,一个只渲染正面

更复杂的光照

渲染路径

  • LightMode

    Always:不管用什么渲染路径都会被渲染,不计算光照;

    ForwardBase:前向渲染。会计算环境光,最重要的平行光,逐顶点光源和Lightmaps;

    ForwardAdd:前向渲染。计算额外的逐顶点光源,每个Pass对应一个光源;

    Deferred:延迟渲染。会渲染G-buffer;

    ShadowCaster:把物体的深度信息渲染到阴影映射纹理中。

    PrepassBase:遗留的延迟渲染

    PrepassFinal:遗留的延迟渲染

    Vertex、VertexLMRGBM、VertexLM:遗留的顶点照明

前向渲染

有三种处理光照的方式:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics,SH)处理

光源属于逐顶点还是逐像素取决于类型和渲染模式,类型指的是平行光或者其他类型,渲染模式指的是细节面板的重要性(important)

  • Base Pass:一个逐像素平行光以及所有逐顶点和SH光源

    Tags {“LightMode” = “ForwardBase”}

    #pragma multi_compile_fwdbase

  • Additional Pass:其他影响该物体的逐像素光源,每个光源执行一次此Pass

    Tags {“LightMode” = “ForwardAdd”}

    Blend One One

    #pragma multi_compile_fwdadd

前向渲染内置的光照变量:

名称 类型 描述
_LightColor0 float4 该Pass处理的逐像素光源的颜色
_WorldSpaceLightPos0 float4 xyz是该Pass处理的逐像素光源的位置,如果是平行光则w = 0,其他w = 1
_LightMatrix0 float4x4 从世界空间到光源空间的变换矩阵。可用于采样cookie和光强衰减(attenuation)纹理
unity_4LightPosX0(Y0/Z0) float4 仅用于Base Pass,前4个非重要点光源在世界空间的位置
unity_4LightAtten0 float4 仅用于Base Pass,前四个非重要点光源的衰减因子
unity_LightColor half4[4] 仅用于Base Pass,前四个非重要点光源的颜色

前向渲染内置的光照函数

函数名 描述
float3 WorldSpaceLightDir (float4 v) 输入一个模型空间中的顶点位置,返回世界空间中的光照方向,没有被归一化
float3 UnityWorldSpaceLightDir (float4 v) 输入一个世界空间中的顶点位置,返回世界空间中的光照方向,没有被归一化
float3 ObjSpaceLightDir (float4 v) 输入一个模型空间中的顶点位置,返回模型空间中的光照方向,没有被归一化
float3 Shaded4PointLights (…) 计算四个点光源的光照

顶点照明渲染路径

已被抛弃 p185

延迟渲染

更古老的渲染方法,由于前向渲染在灯光数量增加的时候回大幅度降低性能的瓶颈存在,近些年又流行起来。

  • 原理:

    主要包含两个Pass,第一个Pass中不进行光照计算,仅计算哪些片元是可见的(深度缓冲技术)。如果一个片元是可见的,就将其的相关信息储存到G-buffer中。在第二个Pass中,利用G-buffer中的各个片元信息(表面法线、漫反射系数等)进行光照计算。

  • 优点:

    光源数目多的时候使用;

    每个光源都可以逐像素;

  • 缺点:

    不支持真正的抗锯齿功能;

    不能处理半透明物体

    对显卡有一定要求。显卡必须支持MRT(Multiple Render Targets)、Shader Mode 3.0及以上、深度纹理渲染以及双面的模板缓冲。

    Pass1:用于渲染G-buffer,对于每个物体,这个Pass只执行一次。

    Pass2:计算真正的光照模型。包含以下渲染纹理(Render Textrue)

    • RT0:ARGB32,RGB储存漫反射颜色,A通道没有使用。
    • RT1:ARGB32,RGB储存高光反射颜色,A通道用于储存高光反射颜色。
    • RT2:ARGB2101010,RGB储存法线,A没有使用。
    • RT3:ARGB32(非HDR)或ARGBHlaf(HDR),储存自发光 + Lightmap + 反射探针。
    • 深度缓冲和模板缓冲。

    延迟渲染路径的内置变量:

    名称 类型 描述
    _LightColor float4 光源颜色
    _LightMatrix float4x4 从世界空间到光源空间的变换矩阵。用于采样cookie和光源衰减纹理

Unity的光源类型

平行光:几何属性只有方向,没有位置和衰减;

点光源:由球体定义。

聚光灯:最复杂的一种,

前向渲染中处理多种光源

Base Pass:

  • #pragam multi_compile_fwdbase
  • 环境色 + 漫反射 + 高光

Add Pass

  • #pragam multi_compile_fwdadd
  • 判断灯光(#ifdef USING_DIRECTIONAL_LIGHT)
  • 无环境光

Unity的光照衰减

使用_LightTexture0

如果使用了cookie,那么应该使用_LightTextureB0

Unity的阴影

  • 如果想要接受阴影,就必须在shader中对阴影映射纹理进行采样,然后把采样结果与最后的光照结果相乘来产生阴影效果。
  • 如果想要投射阴影,就必须把该物体加入到光源的阴影映射纹理计算中(使用LightMode为ShadeowCaster的Pass)。

不透明物体的阴影

  • #include “AutoLight.cginc”
  • SHADOW_COORDS TRANSFER_SHADOW SHADOW_ATTENUATION
  • 命名规范:a2v结构体中顶点坐标变量名必须是vertex;顶点着色器输入变量必须被命名为v;v2f中顶点位置变量必须命名为pos。

统一管理光照衰减和阴影

  • UNITY_LIGHT_ATTENUATION:计算光照衰减和阴影的宏。

透明度物体的阴影

  • 透明度测试:Fallback “Transparent/Cutout/Vertexlit” Cast Shadow设置为TwoSide。
  • 透明度混合:半透明无阴影。

标准Unity Shader

高级纹理

立方体纹理

  • 反射

    1
    2
    3
    o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
    ...
    fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb;
  • 折射

    1
    2
    3
    o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);
    ...
    fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb;
  • 菲涅尔效果

    Schlick菲涅尔近似等式:F (v, n) = F0 + (1 - F0)(1 - v · n)^5

    Empricial菲涅尔近似等式:F (v, n) = max (0, min(1, bias + scale * (1 - v · n)^power ))

    1
    fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldNormal, worldViewDir), 5);

渲染纹理

  • 镜子效果:使用Render Texture

  • 玻璃效果:GrabPass(注意设置成透明队列)

    1
    2
    3
    4
    5
    GrabPass {"_RefractionTex"}
    ···
    float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
    i.scrPos.xy = offset + i.scrPos.xy;
    fixed3 refractColor = tex2D(_RefractionTex, i.scrPos.xy / i.scrPos.w).rgb;

    GrabPass { }和GrabPass {“TextureName”}

    第一种每一个物体抓取一次屏幕,第二种只会每一帧抓取一次。

程序纹理

  • 简单程序纹理

    1
    2
    3
    4
    5
    6
    Texture2D proceduralTexture = new Texture2D(textureWidth, textureWidth);
    ···
    proceduralTexture.SetPixel(w, h, pixel);
    ···
    proceduralTexture.Apply();
    material.SetTexture("_MainTex", proceduralTexture);
  • Unity的程序材质

    配合Substance Designer使用

让画面动起来

Unity Shader中内置变量(时间)

名称 类型 描述
_Time float4 自该场景加载所经过的时间,四个分量是(t / 20, t, 2t, 3t)
_SinTime float4 时间的正弦值,四个分量是(t / 8, t / 4, t / 2, t)
_CosTime float4 时间的余弦值,四个分量是(t / 8, t / 4, t / 2, t)
unity_DeltaTime float4 时间增量,四个分量是(dt, 1 / dt, smoothDt, 1 / smoothDt)

纹理动画

  • 序列帧动画

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fixed4 frag (v2f i) : SV_Target
    {
    float time = floor(_Time.y * _Speed);
    float row = floor(time / _HorizontalAmount);
    float column = time - row * _HorizontalAmount;

    half2 uv = i.uv + half2(column, -row);
    uv.x /= _HorizontalAmount;
    uv.y /= _VerticalAmount;

    fixed4 color = tex2D(_MainTex, uv);
    color.rgb *= _Color;
    return color;
    }
  • 滚动的背景

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    v2f vert (appdata v)
    {
    v2f o;

    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0) * _Time.y);
    o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0) * _Time.y);

    return o;
    }

顶点动画

1
"DisableBatching" = "True" // 禁用批处理

如果要正确投射阴影的话要加入Cast Shadow的Pass

屏幕后处理效果

建立一个基本的屏幕后处理脚本系统

1
MonoBehaviour.OnRenderImage (RenderTexture src, RenderTexture dest)

其中src是源纹理,dest是目标渲染纹理。

通常使用Graphics.Blit函数来完成对RenderTexture的处理。

Unity中实现屏幕后处理效果:

  1. 在摄像机上添加屏幕后处理脚本,实现OnRenderImage函数
  2. 调用Graphics.Blit函数,使用特定shader处理屏幕图像。

而且在进行屏幕后处理之前,需要检查一些列条件是否满足,例如当前平台是否支持渲染纹理和屏幕特效,是否支持当前使用的Unity Shader等。

后处理脚本的基类:

  • 所有效果要在编辑器状态下也可以查看,而且必须放在相机上

    1
    2
    3
    [ExecuteInEditMode]
    [RequireComponent (typeof(Camera))]
    public class PostEffectBase : MonoBehaviour

调整屏幕的亮度、饱和度和对比度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sampler2D _MainTex;
float _Brightness;
float _Saturation;
float _Contrast;

fixed4 frag (v2f i) : SV_Target
{
fixed4 renderTex = tex2D(_MainTex, i.uv);

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

// 饱和度
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
finalColor = lerp(luminanceColor, finalColor, _Saturation);

// 对比度
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);

return fixed4(finalColor, renderTex.a);
}

边缘检测

卷积

边缘检测常用的卷积核:Roberts Prewitt Sobel

使用Sobel实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv[9] : TEXCOORD0;
float4 pos : SV_POSITION;
};
···
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.uv;
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}

fixed Luminance(fixed4 color)
{
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}

half Sobel(v2f i)
{
// Sobel算子
const half gX[9] = {-1, -2, -1, 0, 0, 0, 1, 2, 1};
const half gY[9] = {-1, 0, 1, -2, 0, 2, -1, 0, 1};
half texColor;
half edgeX = 0;
half edgeY = 0;
for(int it = 0; it <9; it++)
{
texColor = Luminance(tex2D(_MainTex, i.uv[it]));
edgeX += texColor * gX[it];
edgeY += texColor * gY[it];
}
half edge = 1 - abs(edgeX) - abs(edgeY);
return edge;
}

fixed4 frag (v2f i) : SV_Target
{
half edge = Sobel(i);
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}

这个会导致阴影也被检测出边缘,下面还有更高级的边缘检测

高斯模糊

image

脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material)
{
int rtW = src.width / downSample;
int rtH = src.height / downSample;

RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;

Graphics.Blit(src, buffer0);

for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1 + i * blurSize);

RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 横向模糊
Graphics.Blit(buffer0, buffer1, material, 0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;

buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 纵向模糊
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}

Graphics.Blit(buffer0, dest);
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(src, dest);
}
}

Shader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;

struct v2f
{
float4 pos : SV_POSITION;
half2 uv[5] : TEXCOORD0;
};

v2f vertBlurVertical(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;

o.uv[0] = uv;
o.uv[1] = uv + float2(0, _MainTex_TexelSize.y * 1) * _BlurSize;
o.uv[2] = uv - float2(0, _MainTex_TexelSize.y * 1) * _BlurSize;
o.uv[3] = uv + float2(0, _MainTex_TexelSize.y * 2) * _BlurSize;
o.uv[4] = uv - float2(0, _MainTex_TexelSize.y * 2) * _BlurSize;

return o;
}
v2f vertBlurHorizontal(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;

o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1, 0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1, 0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2, 0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2, 0) * _BlurSize;

return o;
}
fixed4 fragBlur (v2f i) : SV_TARGET
{
float weight[3] = {0.4026, 0.2442, 0.0545};
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
for(int i1 = 1; i1 < 3; i1++)
{
sum += tex2D(_MainTex, i.uv[i1 * 2 - 1]).rgb * weight[i1];
sum += tex2D(_MainTex, i.uv[i1 * 2]).rgb * weight[i1];
}

return fixed4(sum, 1);
}
ENDCG

Cull Off ZWrite Off ZTest Always


Pass
{
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}

Pass
{
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}

泛光效果(Bloom)

原理:提取亮部,进行模糊,然后叠加在原图片

脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material)
{
material.SetFloat("_LuminanceThreshold", luminanceThreshold);

int rtW = src.width / downSample;
int rtH = src.height / downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;

// 提取亮部
Graphics.Blit(src, buffer0, material, 0);

for(int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1 + i * blurSize);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

// 纵向模糊
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

// 横向模糊
Graphics.Blit(buffer0, buffer1, material, 2);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}

material.SetTexture("_Bloom", buffer0);

// 叠加结果
Graphics.Blit(src, dest, material, 3);

RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(src, dest);
}
}

Shader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _Bloom;
float _LuminanceThreshold;
float _BlurSize;

struct v2f
{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};

v2f vertExtractBrightness(appdata_img v)
{
v2f o;
o.pos = UnityViewToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}

fixed luminance (fixed4 color)
{
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}

fixed4 fragExtractBright (v2f i) : SV_Target
{
fixed4 color = tex2D(_MainTex, i.uv);
fixed val = clamp(luminance(color) - _LuminanceThreshold, 0, 1);

return color * val;
}

struct v2fBloom
{
float4 pos : SV_POSITION;
half4 uv : TEXCOORD0;
};

v2fBloom vertBloom (appdata_img v)
{
v2fBloom o;
o.pos = UnityViewToClipPos(v.vertex);
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;

#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0) o.uv.w = 1 - o.uv.w;
#endif

return o;
}

fixed4 fragBloom (v2fBloom i) : SV_Target
{
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
}

ENDCG
Cull Off ZWrite Off ZTest Always

Pass
{
CGPROGRAM
#pragma vertex vertExtractBrightness
#pragma fragment fragExtractBright
ENDCG
}
UsePass "Unity Shader Book/Chapter12/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL"
UsePass "Unity Shader Book/Chapter12/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL"
Pass
{
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}

运动模糊

实现方法:

  1. 累计缓存(accumulation buffer):使用一块累计缓存混合多张连续图像,取平均值,性能消耗很大。
  2. 速度缓存(velocity buffer):应用更加广泛,在速度缓存中储存各个像素的运动速度,然后利用这个值来确定模糊的方向和大小。

这次实现第一种方法。

脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material)
{
if (!accumulationTexture || accumulationTexture.width != src.width ||
accumulationTexture.height != src.height)
{
DestroyImmediate(accumulationTexture);
accumulationTexture = new RenderTexture(src.width, src.height, 0);
accumulationTexture.hideFlags = HideFlags.HideAndDontSave;
Graphics.Blit(src, accumulationTexture);
}

accumulationTexture.MarkRestoreExpected();

material.SetFloat("_BlurAmount", 1f - blurAmount);

Graphics.Blit(src, accumulationTexture, material);
Graphics.Blit(accumulationTexture, dest);
}
else
{
Graphics.Blit(src, dest);
}
}

Shader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
fixed _BlurAmount;

struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
};
v2f vert(appdata_img v)
{
v2f o;
o.pos = UnityViewToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}

fixed4 fragRGB (v2f i) : SV_Target
{
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}

half4 fragA (v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDCG

Cull Off ZWrite Off ZTest Always

Pass
{
Blend SrcAlpha OneMinusSrcAlpha
ColorMask RGB
CGPROGRAM

#pragma vertex vert
#pragma fragment fragRGB

ENDCG
}
Pass
{
Blend One Zero
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}

不过这个效果一般,下面使用深度贴图还有一个[运动模糊 高级版](##运动模糊 高级版),使用速度映射图。

使用深度和法线纹理

获取深度和法线纹理

深度值是非线性的(由于透视投影导致)

深度纹理:要设置正确的RenderType标签

1
camera.depthTextureMode = DepthTextureMode.Depth;

然后在shader中声明**_CameraDepthTexture**变量即可访问。

深度法线纹理(法线存在RG通道,深度存在BA通道)

1
camera.depthTextureMode = DepthTextureMode.DepthNormals;

然后在Sader中声明**_CameraDepthNormalsTexture**变量即可访问。

组合这些模式(生成深度和深度法线纹理):

1
2
camera.depthTextureMode |= DepthTextureMode.Depth;
camera.depthTextureMode |= DepthTextureMode.DepthNormals;

采样深度纹理:SAMPLE_DEPTH_TEXTURE宏

1
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);

将深度值转换为线性:

  • LinearEyeDepth:把深度纹理的采样结果转换到视角空间下的深度值
  • Linear01Depth:返回一个在[0, 1]的线性深度值。

对深度法线采样结果进行解码:DecodeDepthNormal (float4 enc, out float depth, out float3 normal)

运动模糊 高级版

储存两帧的深度图,通过深度图乘以逆矩阵获得每个像素世界位置的坐标,计算两帧位置差获得速度。可以在一个屏幕后处理步骤中完成整个效果的模拟,但是对性能有所影响。

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material)
{
material.SetFloat("_BlurSize", blurSize);
material.SetMatrix("_PreviousViewProjectionMatrix", m_previousViewProjectionMatrix);
Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
m_previousViewProjectionMatrix = currentViewProjectionMatrix;

Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}

Shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
float4x4 _CurrentViewProjectionInverseMatrix;
float4x4 _PreviousViewProjectionMatrix;
half _BlurSize;

struct v2f
{
half2 uv : TEXCOORD0;
half2 uvDepth : TEXCOORD1;
float4 pos : SV_POSITION;
};

v2f vert (appdata_img v)
{
v2f o;
o.pos = UnityViewToClipPos(v.vertex);
o.uv = v.texcoord;
o.uvDepth = v.texcoord;

#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0) o.uvDepth.y = 1 - o.uvDepth.y;
#endif

return o;
}

fixed4 frag(v2f i) : SV_Target
{
// 采样深度贴图
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uvDepth);
// 将屏幕位置从[0, 1]映射到[-1, 1]
float4 h4 = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
// 求出当前像素世界位置
float4 d4 = mul(_CurrentViewProjectionInverseMatrix, h4);
float4 worldPos = d4 / d4.w;

// 求出当前像素上一帧的屏幕位置
float4 currentPos = h4;
float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
previousPos /= previousPos.w;

// 求出当前像素的速度
float2 velocity = (currentPos.xy - previousPos.xy) / 2;

float2 uv = i.uv;
float4 c = tex2D(_MainTex, uv);
for(int it = 1; it < 5; it++)
{
uv += velocity * _BlurSize * it;
float4 currentColor = tex2D(_MainTex, uv);
c += currentColor;
}
c /= 5;

return fixed4(c.rgb, 1);
}
ENDCG

SubShader
{
Cull Off ZWrite Off ZTest Always

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}

这个只能用于摄像机运动(因为是使用的摄像机变换矩阵),对于物品运动没有效果。

全局雾效

Unity内置的雾效是基于距离的线性或者指数雾,每个 shader 都需要添加代码,这里讨论一种屏幕后处理全局雾效。

另一种高效的世界位置计算方法:

1
float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay

雾的系数 f:

1
float3 afterColor = f * fogColor + (1 - f) * origColor;

线性(Linear)指数(Exponential)指数平方(Exponential Squared)

这里使用比较简单的线性。

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material)
{
Matrix4x4 frustumCorners = Matrix4x4.identity;

float fov = camera.fieldOfView;
float near = camera.nearClipPlane;
float far = camera.farClipPlane;
float aspect = camera.aspect;

float hlafHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
Vector3 toRight = cameraTransform.right * hlafHeight * aspect;
Vector3 toTop = cameraTransform.up * hlafHeight;

Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
float scale = topLeft.magnitude / near;
topLeft.Normalize();
topLeft *= scale;

Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
topRight.Normalize();
topRight *= scale;

Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
bottomLeft.Normalize();
bottomLeft *= scale;

Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
bottomRight.Normalize();
bottomRight *= scale;

frustumCorners.SetRow(0, bottomLeft);
frustumCorners.SetRow(1, bottomRight);
frustumCorners.SetRow(2, topRight);
frustumCorners.SetRow(3, topLeft);

material.SetMatrix("_FrustumCornersRay", frustumCorners);
material.SetMatrix("_ViewProjectionInverseMatrix", (camera.projectionMatrix * camera.worldToCameraMatrix).inverse);

material.SetFloat("_FogDensity", fogDensity);
material.SetFloat("_FogStart", fogStart);
material.SetFloat("_FogEnd", fogEnd);
material.SetColor("_FogColor", fogColor);

Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}

Shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
CGINCLUDE
#include "UnityCG.cginc"
float4x4 _FrustumCornersRay;
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
half _FogDensity;
fixed4 _FogColor;
float _FogStart;
float _FogEnd;

struct v2f
{
half2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
half2 uvDepth : TEXCOORD1;
float4 interpolatedRay : TEXCOORD2;
};

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

o.uv = v.texcoord;
o.uvDepth = v.texcoord;

#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uvDepth.y = 1 - o.uvDepth.y;
#endif

int index = 0;
if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) index = 0;
else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5) index = 1;
else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5) index = 2;
else index = 3;


#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0) index = 3 - index;
#endif

o.interpolatedRay = _FrustumCornersRay[index];
return o;
}

fixed4 frag (v2f i) : SV_Target
{
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uvDepth));
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;

float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
fogDensity = saturate(fogDensity * _FogDensity);

fixed4 finalColor = tex2D(_MainTex, i.uv);
finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);

return finalColor;
}
ENDCG

Cull Off ZWrite Off ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}

使用深度的边缘检测

使用Roberts算子

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material)
{
material.SetFloat("_EdgeOnly",edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
material.SetFloat("_SampleDistance", sampleDistance);
material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0, 0));

Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}

Shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
float _SampleDistance;
half4 _Sensitivity;
sampler2D _CameraDepthNormalsTexture;

struct v2f
{
float4 pos : SV_POSITION;
half2 uv[5] : TEXCOORD0;
};

v2f vert (appdata_img v)
{
v2f o;
o.pos = UnityViewToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;

#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0) uv.y = 1 - uv.y;
#endif

o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1, 1) * _SampleDistance;
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1, -1) * _SampleDistance;
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 1) * _SampleDistance;
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1, -1) * _SampleDistance;

return o;
}

half CheckSame (half4 center, half4 sample)
{
half2 centerNormal = center.xy;
float centerDepth = DecodeFloatRG(center.zw);
half2 sampleNormal = sample.xy;
float sampleDepth = DecodeFloatRG(sample.zw);

half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
int isSameDepth = diffDepth < 0.1 * centerDepth;
// 只会返回1或者0
return isSameNormal * isSameDepth ? 1 : 0;
}

fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target
{
half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
half edge = 1;
edge *= CheckSame(sample1, sample2);
edge *= CheckSame(sample3, sample4);

fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]),edge);
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);

return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
ENDCG

SubShader
{
Cull Off ZWrite Off ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRobertsCrossDepthAndNormal
ENDCG
}
}

非真实感渲染

卡通风格渲染

《Real Time Rendering, 3rd edition》中所介绍的渲染轮廓线的方法

  1. 基于观察角度和表面法线的轮廓线渲染。使用视角方向与法线点乘。这种方法简单快速,可以在一个pass内完成,但局限性大,效果一般。
  2. 过程式几何轮廓线渲染。使用两个pass,第一个渲染背面,并且使其轮廓可见,第二个pass再渲染正面。这种方法快速有效,但是不适合类似立方体这种较为平整的模型。
  3. 基于图像处理的轮廓线渲染。使用卷积算子进行边缘检测。优点为适用于任何模型,局限在于无法检测到法线和深度变化很小的轮廓,比如放在桌子上的纸。
  4. 基于轮廓边检测的轮廓线渲染。这种方法旨在于解决上述方法无法控制轮廓风格的问题,可以直接检测出轮廓边,并且直接渲染轮廓边。检测方法为检测与这条边相邻的两个三角形是否一个朝向正面,一个朝向背面。缺点在于由于是逐帧单独提取轮廓,所以会出现帧与帧之间发生跳变的情况。
  5. 最后一类混合了上述方法。

在这里直接使用过程式几何轮廓线渲染,将背面沿着法线扩张一段距离。

高光:为了防止高光边缘,可以使用smoothstep函数切换高光区域和非高光区域。

描边pass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
v2f vert (a2v v)
{
v2f o;
float4 pos = mul(UNITY_MATRIX_MV, v.vertex);
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
normal.z = -0.5;
pos = pos + float4(normalize(normal), 0) * _OutLine;
o.pos = mul(UNITY_MATRIX_P, pos);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return float4(_OutLineColor.rgb, 1);
}

卡通着色Pass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).rgb;
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir);

fixed4 c = tex2D(_MainTex, i.uv);
fixed3 albedo = c.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

fixed diff = dot(worldNormal, worldLightDir);
diff = (diff * 0.5 + 0.5) * atten;

fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;
fixed spec = dot(worldNormal, worldHalfDir);
fixed w = fwidth(spec) * 2;
fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(0.0001, _SpecularScale);

return fixed4(ambient + diffuse + specular, 1);
}

素描风格渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord.xy * _TileFactor;

fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex));
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed diff = max(0, dot(worldLightDir, worldNormal));

o.hatchWeights0 = fixed3(0, 0, 0);
o.hatchWeights1 = fixed3(0, 0, 0);
float hatchFactor = diff * 7;

if(hatchFactor > 6){}
else if(hatchFactor > 5)
{
o.hatchWeights0.x = hatchFactor - 5;
}
else if(hatchFactor > 4)
{
o.hatchWeights0.x = hatchFactor - 4;
o.hatchWeights0.y = 1 - o.hatchWeights0.x;
}
else if(hatchFactor > 3)
{
o.hatchWeights0.y = hatchFactor - 3;
o.hatchWeights0.z = 1 - o.hatchWeights0.y;
}
else if(hatchFactor > 2)
{
o.hatchWeights0.z = hatchFactor - 2;
o.hatchWeights1.x = 1 - o.hatchWeights0.z;
}
else if(hatchFactor > 1)
{
o.hatchWeights1.x = hatchFactor - 1;
o.hatchWeights1.y = 1 - o.hatchWeights1.x;
}
else
{
o.hatchWeights1.y = hatchFactor;
o.hatchWeights1.z = 1 - o.hatchWeights1.y;
}

o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 hatchTex0 = tex2D(_Hatch0, i.uv) * i.hatchWeights0.x;
fixed4 hatchTex1 = tex2D(_Hatch1, i.uv) * i.hatchWeights0.y;
fixed4 hatchTex2 = tex2D(_Hatch2, i.uv) * i.hatchWeights0.z;
fixed4 hatchTex3 = tex2D(_Hatch3, i.uv) * i.hatchWeights1.x;
fixed4 hatchTex4 = tex2D(_Hatch4, i.uv) * i.hatchWeights1.y;
fixed4 hatchTex5 = tex2D(_Hatch5, i.uv) * i.hatchWeights1.z;
fixed4 whiteColor = fixed4(1, 1, 1, 1) * (1 - i.hatchWeights0.x - i.hatchWeights0.y - i.hatchWeights0.z
- i.hatchWeights1.x - i.hatchWeights1.y - i.hatchWeights1.z);
fixed4 hatchColor = hatchTex0 + hatchTex1 + hatchTex2 + hatchTex3 + hatchTex4 + hatchTex5 + whiteColor;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

return fixed4(hatchColor.rgb * _Color.rgb * atten, 1);
}

使用噪声

消融效果

通过一张噪点贴图实现消融

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv_Main = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv_Bump = TRANSFORM_TEX(v.texcoord, _BumpMap);
o.uv_Burn = TRANSFORM_TEX(v.texcoord, _BumpMap);
TANGENT_SPACE_ROTATION;
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed3 burn = tex2D(_BurnMap, i.uv_Burn).rgb;
clip(burn.r - _BurnAmount);
float3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv_Bump));

fixed3 albedo = tex2D(_MainTex, i.uv_Main).rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));

fixed t = 1 - smoothstep(0, _LineWidth, burn.r - _BurnAmount);
fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, t);
burnColor = pow(burnColor, 5);

UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
fixed3 finalColor = lerp(ambient + diffuse * atten, burnColor, t * step(0.0001, _BurnAmount));

return fixed4(finalColor, 1);
}

在Shadow Caster Pass中也要记得clip,这样可以投正确的阴影。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct v2f
{
V2F_SHADOW_CASTER;
float2 uvBurnMap : TEXCOORD1;
};

fixed _BurnAmount;
sampler2D _BurnMap;
float4 _BurnMap_ST;

v2f vert (appdata_base v)
{
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed3 burn = tex2D(_BurnMap, i.uvBurnMap);
clip(burn.r - _BurnAmount);
SHADOW_CASTER_FRAGMENT(i);
}

水波效果

给全局雾效添加噪声纹理

Unity中的渲染优化技术

移动平台的特点

影响性能的因素

CPU GPU

造成游戏性能瓶颈的原因

  1. CPU
    • 过多Draw Call。
    • 复杂的脚本或者物理模拟
  2. GPU
    • 顶点处理(过多的顶点,过多的逐顶点计算)
    • 片元处理(过多的片元,过多的逐片元计算)
  3. 带宽
    • 使用了尺寸很大而且未经过压缩的纹理
    • 分辨率过高的帧缓存。

CPU优化:

  • 使用批处理减少draw call数目

GPU优化:

  • 减少顶点数目(优化几何体, 使用LOD, 使用遮挡剔除)
  • 减少片元数目(控制绘制顺序,少用透明物体,减少实时光照。
  • 降低计算复杂度(使用LOD, 代码优化)

Unity中的渲染分析工具

  • 渲染统计窗口

    信息名称 描述
    每帧的时间和FPS
    Batches 一帧中需要进行的批处理数目
    Saved by batching 合并的批处理数目,标明了批处理为我们节省了多少draw call
    Tris和Verts 需要绘制的三角形面片和顶点数目
    Screen 屏幕大小以及其占用的内存大小
    SetPass 渲染使用的Pass的数目,可能造成CPU瓶颈
    Visible Skinned Meshes 渲染的蒙皮网格的数目
    Animations 播放的动画数目
  • 性能分析器的渲染区域

    Window -> Profiler

  • 帧调试器(Frame Debugger)

减少draw call数目

动态批处理:

静态批处理:勾选static

共享材质:参数也不能调

减少需要处理的顶点数目

优化几何体:减少顶点数目,减少不必要的硬边以及纹理衔接;

模型LOD技术:使用LOD Group组件;

遮挡剔除技术:

减少需要处理的片元数目

减少overdraw

控制绘制顺序:将更可能出现在前面的物体先画,更可能在最后的物体后画;

时刻小心透明物体:

减少实时光照和阴影:

节省带宽

减小纹理大小:长宽比最好是正方形,长宽值最好是2的整数幂;

利用分辨率缩放:

减少计算复杂度

shader的LOD技术:

代码方面的优化:尽量逐对象或逐顶点;使用低精度浮点数;尽量不使用全屏幕的后处理效果;

根据硬件条件进行更改:

Unity的表面着色器探秘

表面着色器(Surface Shader)实际上就是在顶点片元着色器之上的一种抽象概念,使得shader编写更加方便。

表面着色器的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
SubShader
{
Tags { "RenderType" = "Opaque"}
LOD 300

CGPROGRAM
#pragma surface surf Lambert
#pragma target 3.0

sampler2D _MainTex;
sampler2D _NormalMap;
fixed4 _Color;

struct Input
{
float2 uv_MainTex;
float2 uv_NormalMap;
};

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(_NormalMap, IN.uv_NormalMap));
}
ENDCG
}

编译指令

1
#pragma surface surfaceFunction lightMode [Optionalparams]

表面函数:

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

光照函数:

  • 自带:Standard, StandardSpecular, Lambert, BlinnPhong

  • 自定义:

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

其他可选参数:

  • 自定义修改函数:顶点修改函数(vertex:VertexFunction)和最后颜色修改函数(finalcolor:ColorFunction)
  • 阴影:addshadow / fullforwardshadows / noshadow
  • 透明度混合和透明度测试:alpha / alphatest:VariableName
  • 光照:noambient / novertexlight / noforwardlight / nolightmap / nofog
  • 控制代码的生成:exclude_path:deferred / forward / prepass

两个结构体

  • Input结构体

    以uv或者uv2为前缀后面跟sampler名称,下划线隔开。

    变量 描述
    float3 viewDir 包含了视角方向,可用于计算边缘光照
    使用Color语义定义float4变量 包含了插值后的逐顶点颜色
    float4 screenPos 包含了屏幕空间坐标,可用于反射和屏幕特效
    float3 worldPos 包含了世界空间下的位置
    float3 worldRefl 包含了世界空间下的反射方向,前提是没有修改法线方向
    float3 worldNormal 包含了世界空间的法线方向,前提是没有修改法线方向

    如果更改了法线,则需要添加INTERNAL_DATA,然后使用WorldReflectionVector(IN, o.Normal) / WorldNormalVector(IN, o.Normal)来得到世界空间下的反射和法线方向。

  • SurfaceOutput / SurfaceOutputStandard / SurfaceOutputStandardSpecular结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    struct SurfaceOutput
    {
    fixed3 Albedo;
    fixed3 Normal;
    fixed3 Emission;
    half Specular;
    fixed Gloss;
    fixed Alpha;
    };

    struct SurfaceOutputStanard
    {
    fixed3 Albedo;
    fixed3 Normal;
    fixed3 Emission;
    half Metallic;
    half Snoothness;
    half Occlision;
    fixed Alpha;
    };

    struct SurfaceOutputStandardSpecular
    {
    fixed3 Albedo;
    fixed3 Specular;
    fixed3 Normal;
    half3 Emission;
    half Smoothness;
    half Occlusion;
    fixed Alpha;
    };

Unity背后做了什么

将表面着色器编译成顶点和片元着色器

表面着色器实例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Tags { "RenderType"="Opaque" }
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 _NormalMap;
half _Amount;

struct Input
{
float2 uv_MainTex;
float2 uv_NormalMap;
};

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(_NormalMap, IN.uv_NormalMap));
}

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
  • addshadow指令告诉unity生成一个对应顶点着色器的阴影投射pass
  • exclude_path:deferredexclude_path:prepass告诉unity不要为延迟渲染路径生成相应的pass
  • nometa告诉unity取消生成提取元数据的pass

基于物理的渲染

PBS的理论和数学基础

  • 光是什么

    吸收(absorption)散射(scattering)

  • 双向反射分布函数(BRDF)

  • 漫反射项

  • 高光反射项

    Torrance-Sparrow微面元模型、GGX模型、Beckmann模型

  • Unity中的PBS实现

Unity5的Standard Shader

  1. 金属材质
    • 几乎没有漫反射,因为所有被吸收的光都会被自由电子立刻转化为其他形式的能量
    • 有非常强烈的高光反射
    • 高光反射通常是有颜色的
  2. 废金属材质
    • 高光反射强度比较弱,但是菲涅尔现象比较强
    • 高光反射颜色单一
    • 漫反射的颜色多种多样

一个更加复杂的例子

  1. 设置光照环境

    添加HDR的Skybox

    设置间接光照反弹强度以及次数

  2. 放置反射探针

  3. 调整材质

  4. 线性颜色空间

Unity5更新了什么

场景更亮了

表面着色器更容易报错了

自己控制非统一缩放的网格

固定管线着色器逐渐退出舞台

还有更多内容吗