Shader又名为着色器,是渲染管线运行的一段小程序,负责通知GPU如何渲染图形。那问题又来了,什么是渲染管线呢?通过学习收集资料发现,渲染管线也称渲染流水线,是显示芯片(提供显示功能的芯片,是显卡上面的一个最重要的芯片,主要处理显卡的计算工作,如同电脑中cpu的地位)内部处理图形信号相互独立的并行处理单元,渲染管线可以分为,固定渲染管线、可编程渲染管线。。。
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作为开发语言。
(1) 作为图形管线一部分的着色器是最常见的着色器类型。它们执行一些计算来确定屏幕上像素的颜色。在 Unity 中,通常是通过 Shader 对象使用这种类型的着色器。
(2) 计算着色器在常规图形管线之外,在 GPU 上执行计算。
(3) 光线追踪着色器执行与光线追踪相关的计算。
(1) ShaderLab-一种用于编写着色起的Unity特定语言。
(2)Shader Graph-一种无需编写代码即可创建着色器的工具。
https://developer.imaginationtech.com/pvrshadereditor/
每次构建项目时,Unity 编辑器都会编译构建所需的所有着色器:针对每个所需的图形 API 编译每个所需的着色器变体。
当您在 Unity 编辑器中工作时,编辑器不会提前编译所有内容。这是因为为每个图形 API 编译每个变体可能需要很长时间。
相反,Unity 编辑器会这样做:
当导入一个着色器资源时,会执行一些最小的处理(例如表面着色器生成)。
当需要显示着色器变体时,它会检查 Library/ShaderCache 文件夹。
如果找到使用相同源代码的先前编译的着色器变体,则会使用该着色器变体。
如果没有找到匹配项,则编译所需的着色器变体并将结果保存到缓存中。
注意:如果您启用异步着色器编译,它在后台执行此操作并同时显示占位着色器。
(1).当编辑器第一次遇到未编译的着色器变体时,它会将着色器变体添加到作业线程上的编译队列中。编辑器右下角的进度条会显示编译队列的状态。
(2).在加载着色器变体时,编辑器使用占位着色器渲染几何体,该着色器显示为纯青色。
(3).当编辑器完成对着色器变体的编译后,它会使用着色器变体来渲染几何体。
Opaque:大部分着色器(法线、自发光、反射和地形着色器)。
Transparent:大部分半透明着色器(透明、粒子、字体和地形附加通道着色器)。
TransparentCutout:遮罩透明度着色器(透明镂空、两个通道植被着色器)。
Background:天空盒着色器。
Overlay:光环、光晕着色器。
TreeOpaque:地形引擎树皮。
TreeTransparentCutout:地形引擎树叶。
TreeBillboard:地形引擎公告牌树。
Grass:地形引擎草。
GrassBillboard:地形引擎公告牌草。
只提供了一些渲染功能的开关项,不能灵活控制渲染的每个片段,是OpenGL ES 1.0所使用的渲染管线,从OpenGL ES 2.0开始,Unity全面支持可编程渲染管线,现在已经不建议使用固定渲染管线,在这里做个简单了解。
Shader "Unlit/FixedShader" //表示Shader的显示目录与shader代码语块
{
Properties //Shader的属性部分,这里配置渲染管线需要用到的参数
{
//主纹理
_MainTex("Texture",2D) = "white" {}
}
SubShader //子着色器,Shader可以包含多个子着色器,Unity会自动找到当前设备硬件支持的SubShader执行
{
Pass //渲染通道,可以添加多个Pass,由于一个Pass就是一个DrawCall,所以尽量只使用一个Pass。
{
//设置主纹理
SetTexture [_MainTex]
{
combine texture
}
}
}
}
Shader "Unlit/FixedShader01"
{
Properties
{
//主纹理
_MainTex("Texture",2D) = "white" {}
//颜色
_Color("Main Color",Color) = (1,1,1,0)
}
SubShader //子着色器,Shader可以包含多个子着色器,Unity会自动找到当前设备硬件支持的SubShader执行
{
Pass //渲染通道,可以添加多个Pass,由于一个Pass就是一个DrawCall,所以尽量只使用一个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
{
//标记CG程序块的起始位置
//c++语法
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)
{
//输出几何图形顶点以及UV信息
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);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
//标记CG程序块的结束
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
//使用shader模型3.0目标,以得到更好的外观照明
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
// #pragma instancing_options assumeuniformscaling
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游戏开发(第二版)。