Untiy Shader学习基础(build in管线)
1.渲染流水线
流水线的任务是从3D模型出发,绘制出一个2D的屏幕场景。
渲染流水线一共分为三个阶段:1.应用阶段,主要作用是准备好场景数据,执行Culling操作,设置每个模型的渲染状态,输出渲染图元给下一个阶段 2.几何阶段,决定绘制的图元是什么,要怎么样绘制。并将数据变换到屏幕上,将数据与着色等信息传递给下一个阶段 3.光栅化阶段,使用上一个阶段的数据,产生像素渲染到屏幕上。
在流水线阶段,两个可完全编程也是我们最长使用到的着色器是:顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)。
顶点着色器:输入来自于CPU,处理的单位是顶点,并且无法创建销毁顶点也无法得知顶点与顶点之间的关系。由于这样的相互独立性,导致了顶点着色器可以并行处理顶点。顶点着色器完成的主要功能是:坐标变换和逐顶点光照。 坐标变换:把顶点坐标从模型空间转换到齐次空间。逐顶点光照:计算顶点颜色。
片元着色器:片元着色器的输入数据是光栅化阶段中三角形遍历后的数据,这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一是纹理采样(要实现纹理采样,需要在顶点着色器阶段输出每个顶点对应的纹理坐标)。片元着色器可以完成很多重要效果,但是它仅可以影响单个片元,也就是说不能把自己的任何结果直接发送给邻居。
虽然流水线的过程比较复杂,但是Unity为我们封装了很多功能,绝大多数情况下,我们只需要在Untiy Shader中设置输入,编写顶点、片元着色器,设置设置一些状态就可以实现绝大多数想要的功能了。
2.HLSL,GLSL,Cg
这三个是着色语言,专门用于编写Shader。常见的有DirectX的HLSL,OpenGL的GLSL,NVIDIA的Cg。在Unity中使用Build in渲染管线使用的是Cg语言,URP和HDRP 的Shader Lab 采用的是HLSL语言。
3.Unity Shader
3.1 材质与Unity Shader
Unity中我们需要配合材质(Material)和Unty Shader才能实现一个渲染效果,一般基本流程如下:
1)创建一个材质
2)创建一个Unity Shader,并把它赋给创建的材质
3)把材质赋给想要渲染的对象
4)面板中调整Untiy Shader参数,实现想要的效果
Unity中的材质需要结合Mesh或者粒子系统才能工作。Unity在创建Shader时会为我们提供多种选项。Unlit Shader会产生一个不包含光照的基本顶点/片元着色器。Image Effect Shader为我们实现各种屏幕后处理效果提供了一个模版。Compute Shader会产生一种特殊的,利用GPU并行性进行一些与常规渲染流水线无关的计算。Standard Surface Shader为我们提供了典型的表面着色器的实现方法。
3.2 Shader Lab
Unity的Shader Lab是Untiy为我们提供的高层次的渲染抽象层。它使用一些嵌套在花括号中的语义来描述一个Unity Shader的文件结构。例如Properties语句块中定义了着色器所需的各种属性。Untiy会将它结构编译成真正的代码和Shader文件,开发者只需要和Untiy Shader打交道。
一个Unity Shader的基础结构如下:
Shader "ShaderName"{ Properties { //属性 } SubShader { //显卡A使用的子着色器 } SubShader { //显卡B使用的子着色器 } Fallback "VertexLit" }
3.3 材质与Shader Lab的桥梁:Properties
Properties包含了一系列属性,它的语义块定义如下:
Properties { Name ("display name", Properties) = DefaultValue Name ("display name", Properties) = DefaultValue //更多属性 }
如果我们需要在Shader中访问它们,就需要每个属性的名字(Name),这些名字通常由下划线开始。显示名字(display name)则是出现在材质面板上的名字。除此之外我们还需要指定它们的属性(PropertyType)。每个属性还需要设置默认值
Properties支持的属性有以下:包括Int,Float,Range数字类型。Vector四维数组。2D,Cube,3D三种纹理类型,默认是字符串+花括号,字符串可以为空或者是“white”“black”“gray”“bump”内置纹理名称。
Properties { _Int ("Int",Int) = 2 _Float ("Float",Float) = 1.5 _Range ("Range",Range(0.0,5.0)) = 3.0 // _Color ("Color", Color) = (1,1,1,1) _Vector ("Vector",Vector) = (2,3,5,1) // _2D ("2D",2D) = "" {} _Cube ("Cube",Cube) = "white" {} _3D ("3D",3D) = "black"{} }
赋给材质球后效果:
3.4 重量级成员 SubShader
每一个Unity Shader文件可以有多个SubShader,但最少一个。Unity在加载时会扫描所有的SubShader语义块,选择一个能够在目标平台上运行的SubShader。如果都不支持的话就会使用FallBack语义指定的Unity Shader。这样做的目的是因为不同显卡有不同的能力,为了兼容更多的设备。
SubShader语义块定义如下:
SubShader { // 可选 [Tags] // 可选 [RenderSetup] Pass { } //Other Pass }
SubShader中定义了一系列Pass以及可选的状态[RenderSetup],标签[Tags]设置。每个Pass定义了一次完整的渲染流程,但是Pass过多可能会导致渲染性能下降。因此尽可能要减少Pass的使用。
状态设置:
|状态名称|设置指令|解释|
|–|–|
|Cull|Cull Back | Front |Off|设置裁剪模式|
|ZTest|ZTest Less Greater | LEqual | GEqual | Equal | NotEqual | Always |设置深度测试时使用的函数|
|ZWrite|ZWrite On | Off|开启/关闭深度写入|
|Blend|Blend SrcFactor DstFactor|开启并设置混合模式|
状态设置可以设置显卡的各种状态,并且设置了的渲染状态会影响到所有的Pass。如果不想这样,我们可以在Pass语义块中单独设置。
SubShader标签:
标签是一组键值对,它们用来告诉渲染引擎,我们应该如何渲染这个对象。
Pass语义块,如下:
Pass { [Name] [Tags] [RenderSetup] //other code }
我们可以定义Pass的名称: Name “MyPass” , 通过这个名称我们可以在其他的Unity Shader的Pass : UsePass “MyShader/MYPASSNAME”
我们还可以设置Pass的标签和设置,告诉引擎我们应该如何渲染该物体。
3.5 后路Fallback
跟在SubShader后面可以是一个Fallback指令。它用来告诉Untiy如果上面所有的SubShader都行不通那就使用这个最低级的Shader。
语义如下:
Fallback “name” // 或者 Fallback Off // 使用最低级Shader Fallback "VertexLit"
当然除此之外,你应当还需要具备矢量,矩阵的一定基础。
顶点的空间变换过程:
一个顶点最开始在模型空间上被定义,之后会被变换到世界空间(无限大的一个唯一空间)上,之后会从世界空间变换到观察空间上,我们可以通过平移整个观察空间,让摄像机远点位于世界坐标远点,坐标轴与前者重合。之后从观察空间变换到裁剪空间,裁剪空间的目的是方便地对渲染图元进行裁剪。一般是使用视椎体来决定的,被剔除的部分后续不会被渲染。当所有裁剪操作执行完毕就可以从裁剪空间变换到屏幕空间上了。
法线变换,法线是需要我们特殊处理的一种方向矢量。游戏中,模型往往一个顶点会携带额外信息,顶点法线就是其中的一种信息。法线在模型变换时可能会有不与面垂直,我们可以利用与切线永远垂直的特性重新确定法线。
4.步入Untiy Shader
4.1简单的顶点片元着色器
下面是一个简单的顶点片元着色器
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' Shader "Untiy Book/Chapter5/SimpleShader" { SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag float4 vert(float4 v:POSITION):SV_POSITION{ return UnityObjectToClipPos(v); } fixed4 frag():SV_Target { return fixed4(1.0,1.0,1.0,1.0); } ENDCG } } }
这里省略调了Properties语块,因为它并不是必备的,声明了一个SubShader和Pass语块。由于没有任何的标签和渲染设置所以都是使用的默认设置。重点都在CGPROGEAM和ENDCG里
#pragma vertex vert #pragma fragment frag
这里类似于函数声明,告诉了Unity哪个函数包含了顶点着色器的代码,哪个函数包含了片元着色器的代码。
float4 vert(float4 v:POSITION):SV_POSITION{ return UnityObjectToClipPos(v); }
这是顶点着色器的实现部分,它是逐顶点执行的。v包含了这个点位置,通过POSITION指定。返回值是顶点在裁剪空间的位置。POSITION和SV_POSITION都是Cg/HLSH中的语义。SV_POSITION相当于函数返回值类型,告诉Untiy返回值是一个裁剪空间的位置类型是float4。
fixed4 frag():SV_Target { return fixed4(1.0,1.0,1.0,1.0); }
模型数据从哪来? 我们现在是使用POSITION得到了模型的顶点位置,但是如果想要获得纹理坐标,法线方向等更多模型数据该怎么办呢?我们可以定义一个结构体,然后顶点着色器输入参数不是输入坐标,而是输入这个结构体。如下:
struct a2v{ float4 vertex : POSITION; //为模型法线空间的法线填充normal变量 float3 normal : NORMAL; //用模型的第一套纹理坐标填充texcoord变量 float4 texcoord : TEXCOORD0; }; float4 vert(a2v v):SV_POSITION{ return UnityObjectToClipPos(v.vertex); }
顶点,片元着色器如何通讯?
我们可以再定义一个结构体,用来充当顶点着色器的输出,片元着色器的输入。
struct v2f{ float4 pos : SV_POSTION; fixed3 color : COLOR0; }; v2f vert(a2v v){ v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.color = v.normal*0.5 +fixed3(0.5,0.5,0.5); return o; } fixed4 frag(v2f i):SV_Target { return fixed4(i.color,1.0); }
如何使用属性?
在前面的代码中我们都没有使用到属性Properties语块,属性相当于可以给我们实时自定义控制的一些参数。所以我们可以修改如下:添加一个Properties语块,添加颜色变量。所有代码如下
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' Shader "Untiy Book/Chapter5/SimpleShader" { Properties { _Color ("Color Tint", Color) = (1.0,1.0,1.0,1.0) } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag //在Cg代码中,我们需要定义一个与属性名和类型都匹配的变量 fixed4 _Color; struct a2v{ float4 vertex : POSITION; //为模型法线空间的法线填充normal变量 float3 normal : NORMAL; //用模型的第一套纹理坐标填充texcoord变量 float4 texcoord : TEXCOORD0; }; struct v2f{ float4 pos : SV_POSITION; fixed3 color : COLOR0; }; v2f vert(a2v v){ v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.color = v.normal*0.5 +fixed3(0.5,0.5,0.5); return o; } fixed4 frag(v2f i):SV_Target { fixed3 c = i.color; c*= _Color.rgb; return fixed4(c,1.0); } ENDCG } } }
效果:
4.2 使用内置文件
类似于C语言,我们也可以使用一些其他头文件的函数。
CGPROGEAM //.. #include "UntiyCG.cginc" //.. ENDCG
这里面包含了常用的结构体。还有一些帮助函数,具体可自行查询。
刚才例子中说到了Cg/HLSL的语义。具体的可以查询DirectX的文档,但值得注意的是Shader并不是支持所有的语义。
4.3 Shader中的Debug方法
1.假色彩技术
指的是采用假色彩技术生成的一种图像。实现方法就是把需要调试的变量映射到[0,1]之间,把它们作为颜色输出到屏幕上,然后通过屏幕上显示的像素值用来判断是否正确。
2.Frame Debugger
这是Unity自带的一种针对渲染的调试器,打开Window->Analysis->Frame Debugger。它可以用于查看渲染帧时进行的各种渲染事件。
5.Shader的简洁之道
5.1 float、half、fixed
从左到右精度一次降低,当使用时尽可能使用精度较低的类型,因为这可以优化Shader的性能。从大体范围来看,我们可以使用fixed存储颜色和单位矢量,更大范围的数据使用half,最差的情况再使用float。
5.2 避免不必要的计算
我们不能无限制的在Shader中,尤其是片元着色器中进行大量计算。因为这会使得需要的临时寄存器或指令数目超出限制。
5.3 慎用分支和循环语句
虽然现在的GPU已经发展出可以使用分支语句了,但是GPU使用了不同的底层来处理这些分支语句,对性能的消耗是极大的。所以尽可能的不要在Shader中写分支语句。
如果一定要使用,建议是把计算都提前交给cpu,gpu接收计算好的数据。或者是分支判断语句中的条件变量最好是常数,分支中的指令数尽可能少,分支嵌套尽可能减少。
5.4不要除以0
同大多数的编程语言一样,这会导致结果不可预期。解决办法是对于那些分母可能为0的情况,强制截取到非0范围,例如设置一个0.000001用来保证分母大于0
本文作者:CatSevenMillion
本文链接:https://www.cnblogs.com/CatSevenMillion/p/17534466.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了