一、什么是Shader?
Shader又名为着色器,是渲染管线运行的一段小程序,负责通知GPU如何渲染图形。那问题又来了,什么是渲染管线呢?通过学习收集资料发现,渲染管线也称渲染流水线,是显示芯片(提供显示功能的芯片,是显卡上面的一个最重要的芯片,主要处理显卡的计算工作,如同电脑中cpu的地位)内部处理图形信号相互独立的并行处理单元,渲染管线可以分为,固定渲染管线、可编程渲染管线。。。
1.Shader的开发语言
HLSL: 主要用于Direct3D。平台:windows。
GLSL: 主要用于OpenGL。 平台:移动平台(iOS,安卓),mac(only use when you target Mac OS X or OpenGL ES 2.0)
CG:与DirectX 9.0以上以及OpenGL 完全兼容。运行时或事先编译成GPU汇编代码。 CG比HLSL、GLSL支持更多的平台,Unity Shader采用CG/HLSL作为开发语言。
2.着色器用途
(1) 作为图形管线一部分的着色器是最常见的着色器类型。它们执行一些计算来确定屏幕上像素的颜色。在 Unity 中,通常是通过 Shader 对象使用这种类型的着色器。
(2) 计算着色器在常规图形管线之外,在 GPU 上执行计算。
(3) 光线追踪着色器执行与光线追踪相关的计算。
3.着色器的编辑
(1) ShaderLab-一种用于编写着色起的Unity特定语言。
(2)Shader Graph-一种无需编写代码即可创建着色器的工具。
4.着色器性能分析工具
https://developer.imaginationtech.com/pvrshadereditor/
5.着色器编译
每次构建项目时,Unity 编辑器都会编译构建所需的所有着色器:针对每个所需的图形 API 编译每个所需的着色器变体。
当您在 Unity 编辑器中工作时,编辑器不会提前编译所有内容。这是因为为每个图形 API 编译每个变体可能需要很长时间。
相反,Unity 编辑器会这样做:
当导入一个着色器资源时,会执行一些最小的处理(例如表面着色器生成)。 当需要显示着色器变体时,它会检查 Library/ShaderCache 文件夹。 如果找到使用相同源代码的先前编译的着色器变体,则会使用该着色器变体。 如果没有找到匹配项,则编译所需的着色器变体并将结果保存到缓存中。 注意:如果您启用异步着色器编译,它在后台执行此操作并同时显示占位着色器。
6.异步着色器的编译工作原理
(1).当编辑器第一次遇到未编译的着色器变体时,它会将着色器变体添加到作业线程上的编译队列中。编辑器右下角的进度条会显示编译队列的状态。 (2).在加载着色器变体时,编辑器使用占位着色器渲染几何体,该着色器显示为纯青色。 (3).当编辑器完成对着色器变体的编译后,它会使用着色器变体来渲染几何体。
7.内置着色器中的着色器替换标签
Opaque:大部分着色器(法线、自发光、反射和地形着色器)。 Transparent:大部分半透明着色器(透明、粒子、字体和地形附加通道着色器)。 TransparentCutout:遮罩透明度着色器(透明镂空、两个通道植被着色器)。 Background:天空盒着色器。 Overlay:光环、光晕着色器。 TreeOpaque:地形引擎树皮。 TreeTransparentCutout:地形引擎树叶。 TreeBillboard:地形引擎公告牌树。 Grass:地形引擎草。 GrassBillboard:地形引擎公告牌草。
二、固定渲染管线
只提供了一些渲染功能的开关项,不能灵活控制渲染的每个片段,是OpenGL ES 1.0所使用的渲染管线,从OpenGL ES 2.0开始,Unity全面支持可编程渲染管线,现在已经不建议使用固定渲染管线,在这里做个简单了解。
Shader "Unlit/FixedShader"
{
Properties
{
_MainTex("Texture",2D) = "white" {}
}
SubShader
{
Pass
{
SetTexture [_MainTex]
{
combine texture
}
}
}
}
Shader "Unlit/FixedShader01"
{
Properties
{
_MainTex("Texture",2D) = "white" {}
_Color("Main Color",Color) = (1,1,1,0)
}
SubShader
{
Pass
{
Lighting On
Material
{
Diffuse [_Color]
Ambient [_Color]
}
SetTexture [_MainTex]
{
combine previous*texture
}
}
}
三、可编程渲染管线
程序代码可以对每一个片段进行着色,如果要对每个片段像素点做特殊着色,那么在Shader中首先需要获取集合图形对应的顶点以及UV信息,然后通过UV以及贴图拿到当前片段的像素信息,然后就可以自定义着色了。
Shader "Unlit/VertexandFragmentShader"
{
Properties
{
_MainTex("Texture", 2D)="White"{}
}
{
Pass
{
CGPROGRAM
#pragma vertex vert
#include "UnityCG.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
} ;
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
四、可编程渲染管线的表面着色器
该着色器可以省略编写#pragma vertex vert方法,并且Shader中不需要写Pass代码块,#pragma surface surf Lambert表示执行光照模型,SurfaceOutput 表示 vertex 输出的结构对象。
Shader "Custom/NewSurfaceShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
SurfaceOutPut的结构定义:
struct SurfaceOutput
{
fixed3 Albedo;
fixed3 Normal;
fixed3 Emission;
half Specular;
fixed Gloss;
fixed Alpha;
};
五、 深度排序
模型之间存在遮挡关系,因此需要设置模型间的渲染顺序,这可以在Shader中表明,如Tags{“RenderType”=“Opaque”}(Opaque: 用于大多数着色器(法线着色器、自发光着色器、反射着色器以及地形的着色器)。),共分为5个类型(数值越小,越先渲染):
1.Background: 代表1000,天空盒或背景,其它元素都盖在它前面。 2.Geometry: 代表2000,几何体、地形、地上的房子、树木等不需要带透明通道的模型。 3.AlphaTest:代表2450,透明测试(透明测试开启时,当前像素根据设定条件决定是否输出颜色)。 4.Transparent:代表3000,透明或半透明的模型。 5.Overlay:代表4000,渲染在最前面,比如UI一类。
这些数值可以直接对其进行加减,也可以对其进行二次编辑。如Tags{“RenderType”=“Geometry+1”}。 技巧分享:在游戏地形之上,还会绘制很多建筑一类的元素,如果先绘制地形再绘制建筑的话,那么重合的像素点就需要画很多遍,所以可以将地形的RenderType值设置的比地上建筑大,这样就会先绘制建筑,然后再绘制地形。
可以在Window->Analysis->Frame Debug窗口中依次查看当前的渲染顺序:
六、透明
在Shader中可以用Alpha Test和Alpha Blend这两种方式实现透明效果,Alpha Blend透明部分会和背景混合,Alpha Test不会,它只会出现透明和不透明两种结果,Alpha Test无法做混合,由于移动平台下不支持Early-Z,它的效率会比Alpha Blend慢,不过游戏中有时会需要用到它,比如实现自身溶解的效果,Alpha Blend使用的场景比较多,比如粒子特效、角色身体、翅膀等发光效果。 游戏中应当减少使用透明通道,因为透明会出现混合的现象,这样的渲染队列必须是从后向前渲染,此时就会出现大量的过度绘制(overdraw)现象。如果是不透明的话,可以将它设置到Geometry上,这样的渲染顺序就会从前向后渲染,因为后面的像素挡住了前面的像素,所以会大量降低过度绘制,总之,能不用透明的地方就不用。 Unity专门提供了几种放在Shader->Mobile下的着色器,它们是专门优化过的:
七、裁切
Shader中的Stencil(模板),它与深度测试比较像,测试能否写入像素,测试成功后写入像素。Stencil也可以做裁切,在裁切与被裁切的Properties代码块中添加一个唯一标识的ID:
Properties
{
_MainTex{"Texture",2D} = "white" {}
_ID{"Mask ID",Int} = 1
}
在需要裁切的模型上写入Stencil,其中Ref[_ID]表示唯一标识符,Comp equal用来和裁切区域比较是否显示这个像素:
Stencil
{
Ref {_ID}
Comp equal
}
此外,还需要设置一个裁切区域,其中Ref[_ID]和裁切模型匹配,Comp always和Pass replace表示在这个区域内的像素永远显示,否则将被裁切掉:
Stencil
{
Ref {_ID}
Comp always
Pass replace
}
代码如下:
Shader "Unlit/Mask"
{
Properties
{
_ID("Mask ID", Int) = 1
}
SubShader
{
Tags{ "RenderType" = "Opaque" "Queue" = "Geometry" }
ColorMask 0
ZWrite off
Stencil
{
Ref[_ID]
Comp always
Pass replace
}
Pass
{
CGINCLUDE
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
};
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
return o;
}
half4 frag(v2f i) : SV_Target
{
return half4(1,1,1,1);
}
ENDCG
}
}
}
八、着色器变体采集
通常,Unity中的Shader可以直接放在Resources目录下,运行时可以这样读取:
Shader a = Resources.Load<Shader>("Shader Name");
Shader b = Shader.Find("Shder Name")
采用这种方法加载出来的Shader第一次赋值给材质时,会进行解析,因此会带来一点卡顿,为了避免卡顿,可以将Shader放在Shader Variant Collection中提前进行预热。 Create->Shader->Shader Variant Collection创建,在Graphics Setting中,拉到最下方,点击Save to asset…即可创建Shader并将其包含进Shader Variant Collection中。 然后在初始化的地方进行预热:
Resources.Load<ShaderVariantCollection>("NewShaderVariants").WarmUp();
Unity提供了一种将Shader预制在包体中的功能,操作方法在上Graphics Settings中的Always Included Shaders处,将需要的Shader拖拽进来即可,但这样做有个隐患——变体(Variant)。 如果Shader预制在Always Included Shaders中,那么所有的变体组合都会进行打包,这会大幅度增加包体,并且在加载时会带来额外开销。因此要先确定变体有多少个,再决定将它放在哪里。选择一个Shader,点击Compile and show code,即可查看变体数量: 若数量太多,不要放进Always Included Shaders中。
参考文献: 1.Unity官方文档:https://docs.unity.cn/cn/current/Manual/UnityManual.html。 2.Unity3D游戏开发(第二版)。
|