ou要成为一名TA大佬
烂笔头
顶点=>世界坐标
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
什么是ShaderLab?
在图形API中有:OpenGL
,DX
,Vulkan
等。Unity为了处理不同平台使用的Shader语言的差异,就用ShaderLab封装起来,最后根据不同平台编译成对应的着色器语言。
UnityShader 结构图:
Shader 变体
1.什么是变体
官网介绍
在上面的结构图最底下的Variant就是变体部分,如下所示frag中有三个判断分支,Unity就会产生多个Shader变体。如果变体数量太多就会增加内存消耗,一般ShaderLab占用内存在30-70M左右,如果超出太多就要考虑优化变体数量。
fixed4 frag (v2f i) : SV_Target
{
#if _OVERLAY_1
col += secCol;
#elif _OVERLAY_2
col *= secCol;
#endif
col *= _OtherCol;
return col;
}
2.变体的产生
主要是在Shader中使用了:#pragma shader_feature
和#pragma multi_compile
起到宏的作用。不一样的是 shader_feature
没有用到的不会被包含进去, 而 multi_compile
全部版本都会被包含,因此 shader_feature
适用于那些会在材质面板中直接设置的情况,但在脚本里通过DisableKeyword 和 EnableKeyword 来开启或关闭 keyword 的话就要使用 multi_compile
。
3.变体数量计算
Case1
#pragma multi_compile A B C
#pragma multi_compile D E
如果shader中使用了以上两个multi_compile那么不管后面是否使用到宏(A,B,C,D,E)
它都会产生变体,这样计算的方法是(A,B,C)(D,E) => 32=>6。
Case2
#pragma multi_compile A B C
#pragma shader_feature D E
这里有两个shader_feature,当他们启用才会产生变体因此有以下几种组合情况:
(A+D,B+D,C+D) ,(A+E,B+E,C+E),(A+D,B+D,C+D,A+E,B+E,C+E)
Case3
#pragma multi_compile A B C
#pragma shader_feature _ D E
这里的(’_’)表示空,当D或E启用时会产生:(A,B,C,A+D,B+D,C+D) 或 (A,B,C,A+E,B+E,C+E)
4.变体查看
Unity在编辑器里提供了变体查看的方法,我们在shader中添加#pragma shader_feature_local CS_BOOL
为例演示如何查看:
1.选中shader,在inspector面板中有个Keywords
,它里面包含了全局和本地的所有keword。
2.点击inspector上的Compiled code
右边的倒三角,再点击弹出框内的Show
按钮即可看到当前shader用到的Keyword。
5.减少变体
1.shader内剔除
我们可以在shader中使用#pragma skip_variants
来剔除不想要的Keyword变体。如下所示在shader中添加#pragma skip_variants CS_BOOL
后就可以屏蔽该Keyword。
2.Build时剔除(只能用于Global )
使用以下方法剔除时发现无法把Local类型的keyword剔除掉,如果有哪位晓得方法麻烦留言指导下哈,非常感谢。
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
namespace ZYF
{
/// <summary>
/// 在Build时剔除shader keyword
/// </summary>
public class ZYF_Test_ShaderKeywordStripping : IPreprocessShaders
{
public int callbackOrder => 0;
//要剔除的keyword
ShaderKeyword cs_bool_keyword;
public ZYF_Test_ShaderKeywordStripping()
{
cs_bool_keyword = new ShaderKeyword("CS_BOOL");
}
public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
{
List<ShaderCompilerData> strippingList = new List<ShaderCompilerData>();
//收集要剔除的data
for (int i = 0, length = data.Count; i < length; i++)
{
if (data[i].shaderKeywordSet.IsEnabled(cs_bool_keyword))
{
strippingList.Add(data[i]);
}
}
//剔除收集到的data
strippingList.ForEach(d =>
{
data.Remove(d);
});
}
}
}
6.变体在Editor中的生成过程
Shader Load
Shader预加载
使用ShaderVariantCollection预加载指定Shader
官方说明
ShaderVariantCollection里记录着每个shader里真正使用到的变体,它可以用来预加载shader。
1.首先进入运行模式并让摄像机看到整个场景,然后到ProjectSettings/Graphics
的最下方点击Save to asset
保存。
2.把保存的SVC文件放到Project Settings/Graphics 中的Preloaded Shaders中,这样就能预加载shader了,当然你可以按需求修改SVC内容。
3.还可以在代码中进行预加载(因为预加载比较耗时最好放在加载场景中执行):Shader.WarmupAllShaders
(预加载所有shader) 和ShaderVariantCollection.Warmup
(加载指定SVC中的shader)。
Unity Shader入门精要
渲染流水线
概念流水线
每个阶段也是一个流水线系统。
应用阶段:输出渲染所需的几何信息即渲染图元(点线面)
;
几何阶段:把顶点坐标变换到屏幕空间中,输出屏幕空间二维顶点坐标、每个顶点对应的深度值、着色等相关信息;
光栅化阶段:在GPU上运行,决定哪些像素被绘制,对逐顶点数据(纹理坐标、顶点颜色等)进行插值,然后逐像素处理。
CPU–>GPU
GPU流水线
概念阶段的几何、光栅化两个阶段开发者无法拥有绝对的控制权,其实现的载体是GPU.
几何阶段
顶点着色器(Vertex Shader):每个顶点都会调用一次顶点着色器,顶点与顶点之间相互独立。主要工作是坐标变换
、逐顶点光照
。坐标变换把顶点坐标从模型空间转换到齐次裁剪空间(通过MVP变换坐标到-1~1)
裁剪:把不在摄像机视野范围的物体处理掉。一般图元与摄像机的视野关系有3种:完全在视野内
、部分在视野内
、完全在事业外
。完全在视野内的直接进入下一个流水线阶段,完全在视野外不继续进行处理,对部分在视野内的那些图元的处理就是裁剪
。处理方式:在图元与视野边界的交点处用一个新的顶点替代在视野外部的顶点。可以自定义裁剪操作但不可编程!
屏幕映射(Sreenn Mapping):把-1~1坐标转换到屏幕坐标系下=>走你。注意屏幕映射时不会对z坐标做处理,屏幕坐标系和z坐标构成了窗口坐标系
(Window Coordinates)。
光栅化阶段
光栅化两个重要的目标是计算图元覆盖的像素和计算这些像素颜色。
三角形设置(Triangle Setup):计算三角网格表示数据的过程。
三角形遍历(Triangle Traversal):遍历像素并找到那些被三角网格覆盖的过程。也叫扫描变换(Scan Conversion)。
它还有用三角网格的3个顶点的顶点信息对覆盖区域像素进行插值。输出得到一个片元序列,片元不是真正意义上的像素,它还包含了很多状态(屏幕坐标、深度信息…法线、纹理坐标…)。
片元着色器(Fragment Shader):片元着手之前的阶段并不会影响屏幕颜色值,而是会产生一系列的数据信息。此阶段的输入是那些从顶点着色器输出数据插值得到的。在片元着色器中进行纹理采样需要在顶点着色器输出每个顶点对应的纹理坐标,这样就可以获得插值后的片元纹理坐标了。
逐片元操作(Per-Fragment Operations):这一阶段主要任务是决定每个片元的可见性(深度测试、模板测试...
),如果通过测试就要把颜色值与颜色缓冲区中的颜色进行混合。
模板测试
:通常用来限制渲染区域,还有渲染阴影、轮廓渲染等。
各种测试
:测试顺序并不是唯一的,为了提高性能Unity把深度测试放到了片元着色器之前,这种技术称为Early-Z技术
。但有时候提前测试会与片元着色器种的一些操作冲突。
混合(Blend)操作
:把当前渲染的颜色与颜色缓冲中的颜色进行混合。不透明物体
可以之间关闭混合操作,之间覆盖颜色缓冲区上的像素值。但是半透明物体
需要混合操作让物体看起来是透明的。
Draw Call
Draw Call 就是CPU 调用图像编程接口以命令GPU进行渲染的操作。Draw Call造成性能问题的元凶是CPU,而不是GPU。
CPU和GUP并行工作
CPU向命令缓冲区(Command Buffer)添加命令,然后GPU从中读取命令,命令缓冲区中有很多种类的命令,DrawCall只是其中一种。
Draw Call ==》帧率
Draw Call 数量过多 CPU就会把大量的时间花费在提交Draw Call上,造成CPU过载性能下降。其中一个减少Draw Call的方法是批处理(Batching),在CPU的内存中合并网格,把很多小的Draw Call合并成一个大的Draw Call,合并是需要消耗时间的因此对那些静态的物体更适合,动态的每帧都需要重新合并。合并的网格将会使用同一种渲染状态,如果网格之间需要使用不同的渲染状态就无法使用批处理技术!
Shader
Shader所在的阶段就是渲染流水线的一部分,是GPU流水线上一些可高度编程的阶段。有特定类型的着色器:顶点着色器、片元着色器等。在Unity中我们可以方便的编写着色器并且又可以设置渲染状态。
Unity Shader
Unity 使用ShaderLab语言编写Unity Shader文件。
模板介绍
1.Standard Surface Shader:包含标准光照模型的表面着色器模板。
2.Unilit Shader:不包含光照的基本顶点/片元着色器。
3.Image Effect Shader:屏幕后处理效果模板。
4.Compute Shader:利用GPU并行性进行一些与常规渲染流水线无关的计算。
结构
命名
Shader "Custom/MyShader"
:由字符串定义,可以加斜杠“/”来控制在材质面板中出现的位置。示例位置为:Shader->Custom->MyShader
。
Properties
Properties
{
/*Name("显示名称",属性类型) = 默认值*/
_MainTex ("Texture", 2D) = "white" {}
}
属性可以让我们在材质面板中方便的调整,在Shader中通过属性的名字(Name)访问。这些属性的名字通常是由下划线开始。
属性类型 | 默认值的定义语法 | 例子 |
---|---|---|
Int | number | _Int(“Int”,Int)=2 |
Float | number | _Float(“Float”,Float) =1.5 |
Range(min,max) | number | _Range(“Range”,Range(0.0,5.0))=3.0 |
Color | (number,number,number,number) | _Color(“Color”,Color)=(1,1,1,1) |
Vector | (number,number,number,number) | _Vector(“Vector”,Vector)=(2,3,4,5) |
2D | “defaulttexture”{} | _2D(“2D”,2D)=""{} |
Cube | “defaulttexture”{} | _Cube(“Cube”,Cube)=“white”{} |
3D | “defaulttexture”{} | “black”{} |
纹理类型
2D、Cube、3D这3种纹理类型的默认值是由字符串和花括号组成,字符串是纹理名称,要么是空要么是内置的纹理名称:white、black、gray、bump
。花括号是用于控制固定管线的纹理坐标生成时指定一些纹理属性,在Unity5.0后选项被移除了,如果需要类似的功能要在顶点着色器中编写计算相应的纹理坐标代码。
访问属性
为了在Shader中访问属性需要在CG代码片段中定义和这些属性类型相匹配的变量,Properties
语义块中声明的属性只是为了让我们可以在材质面板看见修改而已,我们可以直接通过代码向Shader传递属性而不定义在Properties中。
SubShader
SubShader{
//可选
[Tags]
//可选
[RenderSetup]
Pass{}
Pass{}
.
.
.
}
一个Unity Shader文件可以包含多个SubShader语义块(至少一个),因为不同的显卡
支持的操作指令数目不同,Unity会扫描所有的SubShader找到一个能在目标平台运行的SubShader,在都不支持的情况下就会调用Fallback
语义指定的UnityShader。
SubShader中定义了一系列的Pass
以及可选的状态
[RenderSetup]和标签
[Tags]。
每个Pass定义了一次完整的渲染流程,所以Pass数量不能太多。
状态和标签同样可以在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 | 开启并设置混合模式 |
SubShader标签类型 | 说明 | 例子 |
---|---|---|
Queue | 控制渲染顺序,指定该物体属于哪一个渲染队列 ,通过这种方式可以保证所有的透明物体可以在所有不透明物体后面被渲染,可以自定义使用的渲染队列来控制物体的渲染顺序 | Tags{“Queue”=“Transparent”} |
RenderType | 对着色器分类 ,例如这是一个不透明的着色器,或是一个透明的着色器等。可以被用于着色器替换功能(ShaderReplacement ) | Tags{“RenderType”=“Opapue”} |
DisableBatching | 一些SubShader在使用Unity的批处理 功能时会出现问题,比如使用了模型空间下的坐标进行顶点动画 ,这时可以通过该标签直接禁用批处理功能 | Tags{“DisableBatching”=“True”} |
ForceNoShadowCasting | 控制使用该SubShader的物体是否投射阴影 | Tags{“ForceNotShadowCasting”} |
IgnoreProjector | 如果该标签值为”True“,那么使用该SubShader的物体将不会受Projector 的影响,通常用于半透明物体 | Tags{“IgnoreProjector”=“True”} |
CanUseSpriteAtlas | 设置CanUseSpriteAtlas标签为“False ”,如果着色器是为精灵 设计的,并且当他们被打包到图集 时将不能工作 | Tags{“CanUseSpriteAtlas”=“False”} |
PreviewType | 指明材质面板将如何预览 该材质。默认为球形,可以设置为”Plane“ ”SkyBox“来改变预览类型 | Tags{“PreviewType”=“Plane”} |
Pass
Pass{
[Name]
[Tags]
[RenderSetup]
//其它...
}
可以在Pass中定义该Pass名称:Name “PassName”,通过这个名字我们可以在其它Unity Shader中用UsePass命令直接引用该Pass
: UsePass “xxxShader /PASSNAME”。因为Unity会自动把所有Pass名字转成大写字母
,所以UsePass必须用大写形式!
Pass标签类型 | 说明 | 例子 |
---|---|---|
LightMode | 定义该Pass在Unity的渲染流水线中的角色 | Tags{“LightMode”=“ForwardBase”} |
RequireOptions | 用于指定当满足条件 时才渲染该Pass,目前支持选项”SoftVegetation“ | Tags{“RequireOptions”=“SoftVegetation”} |
除了UsePass
外还有GrabPass
:抓取屏幕并将结果存储在纹理中用于后续的Pass处理。
数学基础
坐标系
模型空间与世界空间
Unity使用的是左手
坐标系。观察空间
(以摄像机为原点的坐标系,摄像机前向是z轴的负方向)使用的是右手
坐标系。
矢量
矢量 * 标量:对矢量进行缩放。标量为负数会让矢量方向取反。
矢量加减
法:对应分量相加减。
矢量的模
:矢量在空间中的长度,对每个分量的平方相加后的值开根号。
单位
矢量:模长为1的矢量。
矢量点积
:对应分量相乘后取和, 结果是一个标量。矢量 . 单位矢量 =在该单位矢量方向的投影
。
公式一: a . b =(ax,ay,az).(bx,by,bz) = ax bx + ay by + az bz.。
公式二:a.b=|a|b|cosθ。
矢量叉积
:a x b = (ax ,ay ,az) x (bx, by, bz) =(ay bz - az by, az bx - ax bz,ax by - ay bx)。
叉积不满足交换律(axb =-(bxa)),也不满足结合律。
叉积的几何意义:结果会得到一个同时垂直于这两个矢量的新矢量。
叉积模长:|axb| = |a||b|sinθ = 平行四边形面积。
叉积方向:需要根据使用的坐标系判断方向。
矩阵
矩阵 * 标量:矩阵的每个元素与这个标量相乘,结果还是个矩阵。
矩阵 * 矩阵:要满足[n x m
] * [m
x k],不满足
交换律,满足结合律。