Shader入门精要项目链接:
材质和Unity Shader:
材质附上Shader,并用材质Inspector面板提供Shader参数,Shader中的Proproties{}块只是显示出这Inspector面板的可视化输入参数。
Unity内置的Shader:
Standard Surface Shader:包含标准光照模型的表面着色器模板;
Unlit Shader:不包含光照(但包含雾效)的基本的顶点/片元着色器;
Image Effect Shader:提供实现各种屏幕后处理效果的基本模板;
一个Shader必须放在Unity的材质上,并将材质挂在物体上才会起作用。
Unity Shader是用ShaderLab语言进行编码的,其中还嵌套了Cg语言进行编写顶点着色器和片元着色器
一个基本Shader代码结构如下:(注意是不能用的!,只是一个格式模板!)
//ShaderName支持文件路径表达:如"UnityShader/Shader1",那么材质下就先找到UnityShader,
//再找到Shader1标签就是这个Shader了!
Shader "ShaderName"
{
//Properties{}非必要,只是写在Properties里面的属性都会显示在材质Inspector面板上!
Properties{
//属性
_Int("IntName", Int) = 2
_Float("FloatName", Float) = 1.5
_Range("RangeName", Range(0.0, 5.0)) = 3.0
_Color("ColorName", Color) = (1,1,1,1)
_Vector("VectorName", Vector) = (2,3,6,1)
_2D("2DName", 2D) = ""{}
_Cube("CubeName", Cube) = "white"{}
_3D("3DName", 3D) = "black"{}
}
//上面Properties{}不是定义shader变量,必须要在Properties之外如下进行一一匹配上方变量
int _Int;
float _Float;//half _Float; 或 fixed _Float; 其中float精度32 half精度16 fixed精度11
float _Range;//同Float一样!
float4 _Color;//也可用half4 fixed4
float4 _Vector;//同Color一样!
sampler2D _2D;
samplerCube _Cube;
sampler3D _3D;
//注意:Properties{}里面是不用写逗号,分号来进行相隔每一个属性的!而外面的需要!
//SubShader{}必须要有一个!且只生效一个!从Shader上往下进行遍历找到一个可用的就会使用它
//作为Shader渲染的真正执行代码进行渲染管线的一部分工作,如果没找到一个可用的就会进入后面
//讲到的FallBack指定的Shader进行渲染。
SubShader{
//[LOD]用于性能调整,用法:当一个SubShader的Lod数值大于设定的Lod最大值或小于设定的Lod
//最小值时,就不会进行使用。
LOD 100
//SubShader中的Tags标签
Tags{
"Queue" = "Geometry"
"RenderType" = "Opaque"
"DisableBatching" = "True"
"ForceNoShadowCasting" = "True"
"IgnoreProjector" = "True"
"CanUseSpriteAtlas" = "False"
"PreviewType" = "Plane"
}
//Cull,ZTest,ZWrite,Blend都是属于RenderSetup状态
Cull Back //设置剔除模式,剔除背面
ZTest LEqual//设置深度测试时的函数为LEqual小于等于条件
ZWrite On//开启深度写入, ZWrite Off关闭
Blend SrcFactor DstFactor //开启并设置SrcFacotr DstFactor混合模式
Pass{
//Name后面的字符串会被unity自动转成全大写的,所以使用UsePass时要注意这一点!
Name "MyPassName"
//Tags和SubShader不一样!仅仅只有2个
Tags{
"LightMode" = "ForwardBase"
"RequireOptions" = "SoftVegetation"
}
//[RenderSetup]和SubShader的一样.
//CGPROGRAM ... ENDCG 中间代码是CG语言编写格式,可理解成就是CG语言了!
//下面写的代码是一个简单的表面着色器示例
CGPROGRAM
#pragma surface surf Lambert
struct Input{
float4 color : COLOR;
};//注意这里也有一个分号!
void surf(Input IN, inout SurfaceOutput o){
o.Albedo = 1;
}
ENDCG
}
//假设我在别的Shader文件中的SubShader下写如下代码就是复用这个Shader的上方Pass块
UsePass "ShaderName/MYPASSNAME"
GrapPass{} //负责抓取屏幕并将结果存储在一张纹理中,后续讲解
Pass{
//顶点着色器vert 片元着色器frag示例代码
CGPROGRAM
//声明一个顶点着色器vert
#pragma vertex vert
//声明一个片元着色器frag
#pragma fragment frag
//顶点着色器
//POSITION语义:用于告诉Unity要用模型空间下的顶点坐标填充到v变量;
//SV_POSITION语义:用于告诉Unity,vert函数返回值float4是一个裁剪空间下的顶点坐标
float4 vert(float4 v : POSITION) : SV_POSITION
{
//将v从模型空间转裁剪空间下然后返回给Unity去处理!
//mul是矩阵乘法函数,UNITY_MATRIX_MVP是一个矩阵!(详细情况后续解释)
//能将一个点或矢量模型空间转裁剪空间,2个参数的位置不能变换!
return mul(UNITY_MATRIX_MVP, v);
}
//片元着色器
//SV_Target语义:用于告诉Unity,frag函数返回值fixed4是一个颜色并且要将它放入
//默认的帧缓存中,这个帧缓存就是下一次出现在屏幕的像素颜色数组一样的玩意。
fixed4 frag() : SV_Target
{
return fixed4(1.0, 1.0, 1.0, 1.0);
}
ENDCG
}
...
}
SubShader{
...
}
Fallback "Diffuse" //""里面是shader名
}
常见的渲染状态[RenderSetup]设置选项
状态名称 设置指令 解释 Cull Cull Back|Font|Off 设置剔除模式:剔除背面/正面/关闭剔除 ZTest ZTest Less Greater|LEqual|GEqual|Equal|NotEqual|Always 设置深度测试时使用的函数 ZWrite ZWrite On|Off 开启/关闭深度写入 Blend Blend SrcFactor DstFactor 这个我不知道干嘛用的
Blend SrcAlpha OneMinusSrcAlpha 与画面颜色进行混合
开启并设置混合模式
SubShader的标签快支持的标签类型
标签类型 说明 例子 Queue 控制渲染顺序,指定该物体属于哪一个渲染队列,通过这种方式可以保证所有的透明物体可以在所有不透明物体渲染之后被渲染,我们也可以自定义使用的渲染队列来控制物体的渲染顺序
Tags{"Queue"="Transparent"}
Background(1000), Geometry(2000) 一般是不透明物体,AlphaTest(2450) 半透明物体渲染级别,Transparent(3000) 完全透明的物体,按照深Z度由远到近进行渲染 , Overlay(4000) 最后进行渲染的如光晕效果,数值代表优先级,从低级开始渲染,可以这样写"Queue"="Background +10”即在背景渲染后10级进行渲染.
RenderType 对着色器进行分类,例如这是一个不透明的着色器,或是一个透明的着色器等。这可以被用于着色器替换功能
有2个方法Camera.SetReplacementShader(Shader , int replaceTag)即为摄像机设置一个Shader用于替换,替换条件是replaceTag, 当摄像机照射的物体身上的Shader有一个SubShader的RenderType是等于replaceTag的话,就会将其Shader替换成设置好的Shader!(大概是这样的意思..)
另一个方法是Camera.RenderWithShader 貌似是直接将照射到的物体Shader替换...
Tags{"RenderType"="Opaque"}
RenderType细分为:Opaque 不透明、TreeOpaque树木不透明、Transparent 透明、TreeTransparentCutout树木透明缕空、TransparentCutout透明缕空、TreeBillboard 树木布告牌、Background 背景、Grass草地、Overlay 叠加、GrassBillboard 草地布告牌.
DisableBatching 一些SubShader在使用Unity的批处理功能时会出现问题,例如使用了模型空间下的坐标进行顶点动画。这时可以通过该标签来直接指明是否对该SubShader使用批处理 Tags{"DisableBatching"="True"} ForceNoShadowCasting 控制使用该SubShader的物体是否会投射阴影 Tags{"ForceNoShadowCasting"="True"} IgnoreProjector 如果该标签为"True",那么使用该SubShader的物体将不会受到Projector的影响。通常用于半透明物体
Projector是一个照射组件,它会将照射到的物体Material切换成指定的Material
Tags{"IgnoreProjector"="True"} CanUseSpriteAtlas 当该SubShader是用于精灵(sprites)时,将该标签设为"False" Tags("CanUseSpriteAtlas"="False"} PreviewType 指明材质面板将如何预览该材质。默认情况下,材质将显示为一个球形,我们可以通过把该标签的值设为"Plane" / "SkyBox" 来改变预览类型 Tags{"PreviewType"="Plane"}
Pass的标签类型
标签类型 说明 例子 LightMode 定义该Pass在Unity的渲染流水线中的角色 Tags{"LightMode"="ForwardBase"} RequireOptions 用于指定当满足某些条件时才渲染该Pass,它的值是一个由空格分隔的字符串。目前,Unity支持的选项有:SoftVegetation。在后面的版本中,可能会增加更多的选项 Tags{"RequireOptions"="SoftVegetation"}
注意:当SubShader或Pass中没有RenderSetup和Tags时,会使用Unity默认的!
Unity Shader中常用语义
这些语义可以让Shader知道从哪里读取数据,并把数据输出到哪里。(在上面的Shader模板示例,我是以Unity为第一人称说明的)
SV_开头的是系统数值语义,在渲染流水线中有特殊的含义,不带SV_开头的也有一部分在某种情况下是有特殊意义的,而在非特殊情况下,可以由我们来决定这个语义的含义。
从应用阶段传递模型数据给顶点着色器时Unity支持的常用语义
语义 描述 POSITION 模型空间中的顶点位置,通常是float4类型 NORMAL 顶点法线,通常是float3类型 TANGENT 顶点切线,通过是float4类型 TEXCOORDn,如TEXCOORD0、TEXCOORD1 该顶点的纹理坐标,TEXCOORD0表示第一组纹理坐标,依次类推。通常是float2或float4类型 COLOR 顶点颜色,通常是fixed4或float4类型
其中,TEXCOORDn中 n 的数目和Shader Model有关,Shader Model 2为默认Shader Model,Shader Model 2和Shader Model 3中,n为8, Shader Model 4 和 Shader Model 5 中,n等于16,通常情况下只使用TEXCOORD0和TEXCOORD1
从顶点着色器传递数据给片元着色器时Unity使用的常用语义
语义 描述 SV_POSITION 裁剪空间中的顶点坐标,顶点着色器输出结构体中必须包含一个用该语义修饰的变量。 COLOR0 通常用于输出第一组顶点颜色,但不是必需的。 COLOR1 通常用于输出第二组顶点颜色,但不是必需的。 TEXCOORD0~TEXCOORD7 通常用于输出纹理坐标,但不是必需的。
片元着色器输出时Unity支持的常用语义
语义 描述 SV_Target 输出值将会存储到渲染目标(render target)中
内置文件和变量
在Windows上,在Unity的安装路径/Data/CGIncludes下能找到相关内置文件, 它是一种类似C++头文件的文件,在Unity中,它们文件的后缀为.cginc。用#include指令把这些文件包含进来,这样我们就可以使用Unity为我们提供的一些变量和帮助函数。
CGPROGRAM
//...
#include "UnityCG.cginc"
//...
ENDCG
PS:在Mac上,文件位置:/Applications/Unity/Unity.app/Contents/CGIncludes
Unity中一些常用的包含文件
文件名 描述 UnityCG.cginc 包含了最常使用的帮助函数,宏和结构体等 UnityShaderVariables.cginc 在编译Unity Shader时,会被自动包含进来。包含了许多内置的全局变量,如:UNITY_MATRIX_MVP等 Lighting.cginc 包含了各种内置的光照模型,如果编写的是Surface Shader的话,会自动包含进来 HLSLSupport.cginc 在编译Unity Shader时,会被自动包含进来。声明了很多用于跨平台编译的宏和定义
其中,UnityCG.cginc是最常用的一个包含文件下面详细介绍,它的帮助函数和结构体;
UnityCG.cginc常用结构体
名字 描述 包含的变量 appdata_base 可用于顶点着色器的输入 顶点位置、顶点法线、第一组纹理坐标 appdata_tan 可用于顶点着色器的输入 顶点位置、顶点法线、第一组纹理坐标 + 顶点切线 appdata_full 可用于顶点着色器的输入 顶点位置、顶点法线、第一组纹理坐标、顶点切线 + (其他组纹理坐标) appdata_img 可用于顶点着色器的输入 顶点位置、第一组纹理坐标 v2f_img 可用于顶点着色器的输出 裁剪空间中的位置、纹理坐标
UnityCG.cginc常用帮助函数
函数名 描述 float3 WorldSpaceViewDir(float4 v) 输入一个模型空间中顶点位置,返回世界空间中从该点到摄像机的观察方向 float3 ObjSpaceViewDir(float4 v) 输入一个模型空间中顶点位置,返回模型空间中从该点到摄像机的观察方向 float3 WorldSpaceLightDir(float4 v) 仅可用于前向渲染中。输入一个模型空间中顶点位置,返回世界空间中从该点到光源的光照方向。没有被归一化 float3 ObjSpaceLightDir(float4 v) 仅可用于前向渲染中。输入一个模型空间中顶点位置,返回模型空间中从该点到光源的光照方向。没有被归一化 float3 UnityObjectToWorldNormal(float3 normal) 将法线方向从模型空间转换到世界空间 float3 UnityObjectToWorldDir(float3 dir) 将方向矢量从模型空间转换到世界空间 float3 UnityWorldToObjectDir(float3 dir) 将方向矢量从世界空间转换到模型空间
渲染平台差异性
一、渲染的坐标差异
由于Unity使用的是OpenGL方式进行映射坐标,而PC上使用的是DirectX方式进行映射,会造成图像y轴旋转了180°的效果,在一般情况下,Unity会为我们DirectX平台下的用户进行处理,即翻转180°,使得渲染正常图像。
但是,当我们需要使用渲染纹理保存渲染结果,而且是开启了抗锯齿效果,且渲染两张或两张以上图像时就会出问题!(渲染一张时不会有问题),其实,当你开启抗锯齿,Unity还是会帮你处理好主纹理,而其他纹理(如法线纹理)就不会帮你处理,你亲自去处理这个问题,即反转纹理坐标。
#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y<0)
uv.y = 1-uv.y;
#endif
uv可理解为是非主纹理坐标,其中UNITY_UV_STARTS_AT_TOP是代表DirectX类型平台的宏,_MainTex_TexelSize.y是主纹理的纹理像素大小的y值,在DirectX平台下开启抗锯齿后这个y值会变成负数(为什么会变负数?我还不清楚!),因此用这个
在用UNITY_UV_STARTS_AT_TOP宏判断是在DirectX平台下,再用_MainTex_TexelSize.y小于0条件来判断是否开启抗锯齿,
若小于0,那么就进行uv.y = 1-uv.y; 进行反转纹理坐标y轴,使之能正确渲染图像。为什么是1-uv.y?因为纹理坐标范围是[0,1]。而1-uv.y为什么称之为反转y值?是因为,例如:
在DirectX屏幕映射坐标系,假如有(0, 0.2), (0, 0.8)这2个点,那么转换到OpenGL屏幕映射坐标系下,
(0, 0.2) 会变成 (0, 0.8) 即 (0, 1-0.2)
(0, 0.8) 会变成 (0, 0.2) 即 (0, 1-0.8)
从而得出(0, y) 会变成(0, 1-y)即 1-y就是反转公式。
可以从矩阵变换来思考为什么这个公式就是纹理的反转公式(有点儿复杂)
反转其实就是指将DirectX屏幕映射坐标系下的点转换到OpenGL屏幕映射坐标系下。
观察2个坐标系发现,我们可以围绕DirectX的正X轴进行旋转+180°(左手法则)来得到OpenGL屏幕映射坐标系(注意此时还有一个(0,1)位移偏移量还没解决后面会说这个,最好自己画画图,此时仅仅是单纯旋转坐标轴,此时可发现原本为正数的y值都会变成负数,注意:XY平面上的点是不会变动的,因为我们只是转换空间概念,对实际坐标点没有任何影响!
比方说:你在办公室看到一本书在桌子上,这本书的坐标为(x,y,z),当你以桌子中心点为空间坐标系时,这本书坐标为(x1,y1,z1)此时书本的物理真实位置是不会变的,只是改变的是我们的参考坐标系 !不要想象成这些XY平面上的点会跟随x轴旋转180°而移动了哦!!)
那么DirectX->OpenGL的M转换矩阵(旋转x轴+位移)为
1 0 0 0 cos180° 1 0 0 1 解释:学过3D数学或矩阵公式都知道,2D的围绕X轴的旋转矩阵是
1 0 0 0 cosθ 0 0 0 1 平移矩阵:
1 0 px 0 1 py 0 0 1
上面的M矩阵是由1个围绕X轴旋转的旋转矩阵 * 平移矩阵组成的。
转换空间方法:
DirectX屏幕映射空间 转换到 OpenGL屏幕映射空间下,若已知OpenGL空间下的DirectX空间原点位置和DirectX空间的X、Y、Z轴在OpenGL空间下的二维矢量表示,那么就可通过如下公式,可将DirectX空间下的坐标点转换到OpenGL空间下的坐标点。假设DirectX空间下一个点A(x,y);
OpenGL空间下的A点 = OpenGL空间下表示的DirectX空间原点位置 + OpenGL空间下表示的DirectX空间X轴二维矢量 * x + OpenGL空间下表示的DirectX空间Y轴二维矢量 * y
由于纹理坐标范围[0,1],所以y轴取值[0,1],x轴取值[0,1],故可得:
OpenGL空间下表示的DirectX空间原点位置 = (0, 1)
OpenGL空间下表示的DirectX空间X轴二维矢量 = (1, 0)
OpenGL空间下表示的DirectX空间Y轴二维矢量 = (0, -1)
故OpenGL空间下的A点 = (0,1) + (1,0) * x + (0,-1) * y = (0,1) + (x, -y) = (0+x, 1-y),
因此反转公式为1-y。
如果用矩阵思考,那么先是进行了x轴旋转(其实+180还是-180都一样),那么按照上方给出的旋转矩阵就能知道是
1 0 0 0 -1 0 0 0 1 事实上,经过这个旋转后,所有坐标点的y轴都会变成负数(你画画图就知道)。
PS:在上面旋转矩阵转换后,2个空间坐标系矢量已经相同了,只是还有一个位移差别!下面进行再次转换,抵消位移差别。
然后再这个基础之上,再考虑【空间坐标系矢量相同情况下的简单转换空间方法(仅仅只有位移差别)】
简单来说,A空间转B空间,已知B空间下的A空间原点坐标OA和B空间下的A空间三个坐标轴矢量,即可将A空间下的坐标点p转到B空间下进行表示的坐标点p'。即p' = OA + A.X * p.x + A.Y * p.y , 其中A.X为(1,0), A.Y为(0,1)都是单位矢量,即p' = OA + p
归纳上方公式,可知仅只需偏移OA,就可以得到最终结果p',那么换算成平移矩阵如下:(注意:正因为是x,y矢量都是单位矢量才是如下形态!不然第一列和第二列都不是这样子的)
1 0 0 0 1 1 0 0 1 最终将2个矩阵相乘,得到最终我所说的M矩阵,解说完毕。。。(恐怕以后我自己看都看不懂。)
额外知识点
一、Shader报错:
1. incorrect number of arguments to numeric-type constructor (compiling for d3dll)
2. output parameter 'o' not completely initialized (compiling for d3dll)
解释如下:
//v是float4类型,若我们初始化时不足4个float参数时会错
float4 v = float4(0.0);
//报错incorrect number of arguments to numeric-type constructor (compiling for d3dll)
//正确写法:
float4 v = float4(0.0, 0.0, 0.0, 0.0);
//在表面着色器时,如果你用了一个out修饰符的参数,且没有对它所有成员都进行初始化会报错!
//报错output parameter 'o' not completely initialized (compiling for d3dll)
//正确写法:
void vert(inout appdata_full v, out Input o){
//使用Unity内置的UNITY_INITIALIZE_OUTPUT宏对输出结构体o进行初始化
UNITY_INITIALIZE_OUTPUT(Input, o); //去掉这行会报错!
}
二、支持在顶点着色器使用tex2D函数类似的功能(纹理采样函数)
SubShader{
Pass{
CGPROGRAM
#include "UnityCG.cginc"
//必须引用target 3.0
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
float4 vert(appdata_full v) : SV_POSITION
{
//maintex是主纹理,v.texcoord0是第一组纹理坐标,tex2Dlog返回值是颜色值fixed4
fixed4 c = tex2Dlog(maintex, float4(v.texcoord0.xy, 0, 0));
}
ENDCG
}
}
优化手段
一、合理地选择float、half、fixed变量类型
记得在修改这些变量后,一定要确保在真机上运行测试你的Shader,因为PC端用的一般都是float(最大精度)来进行的,而手机上才会去选用half或fixed进行,比方说:在PC端测试时完全OK,效果很好,但是在手机上可能就会因为精度过小导致效果不好,甚至出现BUG。
例如:颜色、单位矢量可用fixed类型存储
fixed类型 通常11位存储,精度范围[-2.0, 2.0],
half 16位,精度范围[-60000, 60000]
float 32位
二、避免不必要的计算
如果进行大量计算(特别是在片元着色器),就可能会出现如下两个错误:
temporary register limit of 8 exceeded 或
Arithmetic instruction limit of 64 exceeded; 65 arithmetic instructions needed to compile program
1.可以指定更高的#pragma target x.0 来进行解决问题 ,x = 2,3,4,5 ,3支持顶点纹理的采样,4支持几何着色器
2.减少运算量,或通过预计算来提供更多的数据;
三、慎用分支和循环语句
GPU使用了不同于CPU的技术来实现分支语句,在最坏情况下,如果我们在CPU下执行一个if else,仅仅花费了if(){ ... }代码块时间,那么在GPU会消耗掉if + else代码块的时间,如果有if else if elseif else 这种多分支,或嵌套分支情况下情况会更加糟糕。
建议:
1、分支判断语句中使用的条件变量最好是常数;
2、每个分支中包含的操作指令数尽可能少;(if(){...} else{ ... } 中的 {}块代码少点!)
3、分支的嵌套层数尽可能少。
四、不要除以0
在Shader编写时,Shader编辑器可能不会为你报错这种除数为0的情况,而且还可以正常运行,效果正确;
但是!在另外一些平台下,可能就会因此而崩溃!!!
我们可以给分母加一个很小的浮点值0.000001保证分母大于0(不为0)(前提是原始数据是非负数),如果分母可能是负数,那么我们不得不用一个if来判断除数是否为0了。
本文章是基于Shader入门精要一书进行的总结【第三章、第四章、第五章部分内容】,比较多的数学运算都没有详细说明,但是有一点是很清楚的就是,主要是学会了一些空间变换矩阵、内置变量、内置函数、一些小优化。
顶点着色器的空间变换:模型空间->世界空间->观察空间->裁剪空间
由Unity进行裁剪空间->屏幕空间。