Unity_Shader
写在前面:
- 虽说已经入门(至少我觉得入门了?)shader有些时间了,不过由于慢慢的对入门级的东西有了新的感悟,再者组织要喊我给学弟学妹们培训一手,遂准备记下这篇内容作为教材。
- 在我入门shader那会,啃过学长给的书,摸过siki老师的课,看过知乎大佬的专栏,无一例外,大家都是底层开篇,上手开始GPU渲染流程,渲染流水线。很专业的开篇,但对于没有底层基础和图形学基础的人来说,可能有点难以接受,更难融会贯通真正的用上。
- 所以今天我写的这篇入门呢,主要是给没什么底层基础,图形学基础的小白入门参考。
- 本文主要讲述图形渲染流水线大概是个什么样子(参考知乎大佬王子饼干的shader魔法书专栏),shader在这个流水线中怎样作用,CPU和GPU在后台工作时的区别,用shader处理图像和用c#处理图像有什么区别,一个最简单的shader。
- 注意:本篇涉及到的绝大部分思维上的例子,举例所用的数字,纯属个人理解上的虚构,并非真正的底层知识,所以只能做一个领进门的作用,底层,还是要找专业的资料好好学一学的。
- 哦对了,这篇文章实践性内容不多,所以可能会很枯燥,但如果你学的下去,小白入门进度会快很多。
渲染流水线
- 注:本部分极大参照了知乎王子饼干的专栏,UnityShader魔法书第一节,贴个蓝链UnityShader入门精要笔记1:渲染流水线,有一说一,这专栏讲的很通俗易懂了,大爱。
什么是渲染流水线
- 大概就是说,你做游戏的时候这个游戏物体,本身也是一堆数据,他要经过“渲染”,这才会出现在屏幕上。而渲染流水线,就是将一团数据层层包装迭代筛选,最终呈现在屏幕上的过程。
渲染流水线的各大阶段
渲染流水线主要有三个阶段,应用阶段,几何阶段,光栅化阶段。
- 应用阶段:在这个阶段,程序会拿到之后所用到的很多很多信息,诸如:场景中的物体模型,场景中的光源信息,以及我们所为其输入的一些数据。在这个阶段最后,会输出渲染所需要的几何信息到下一个工位。
- 几何阶段:该阶段通常处理的是几何空间下的很多事情,诸如对模型的顶点信息进行空间变换,去取到一些信息来决定要绘制的图像,最后呢,这个阶段会将顶点信息输出到二维屏幕坐标下,然后传递给下一个工位。
- 光栅化阶段:该阶段是讲之前的内容拿过来,并合并计算一个像素点的信息,最终决定屏幕上显示内容的部分。
- 接下来看一张图,其中我只写入了本节需要我们关注的三部分,这三部分我会详细说一说。其他部分全部省略了,日后需要自行找找资料详细学习,其底层流程。
应用阶段_程序与数据的读入
- 首先关于这部分呢,是系统要做的事,而不是我们要做的,但需要简单知道系统做了什么,知道在什么情况下做什么东西用c#做什么东西用shader。
- Shader代码是执行在GPU上的,和我们通常写的C#代码不同,其仅仅支持一些简单的逻辑操作。下面看一张图来了解下这俩个执行在CPU与GPU上的程序各自的优缺点。
- 如上图,蓝色部分是存储单元,其为CPU和GPU提供了直接访问权限,故而将信息读入这些存储单元再进行使用将会提升速度,这就是执行前读入数据的过程。
- 而红色部分是今天的重点,其是运算单元,CPU的运算单元更加强大,但量少,强大在其可以执行很复杂的逻辑运算,但量少导致其面对大量简单问题时显得乏力。而GPU的运算单元数量很庞大,但本身能力较弱,不支持很复杂的逻辑,但其数量决定了其可以快速处理海量的简单问题。曾见过一个网友举例:CPU就像几个大学生,你让他算什么都可以,但你让他算一万个二位数加法,他得算几个小时。而GPU像一万个小学生,你让他们一起算一个微积分,他们算不得,但你给他们分配下去一万个俩位数加分,在一人一题的情况下,不出一分钟就解决了。
- 也正是这种特点,CPU更适合我们后台的逻辑脚本,GPU更适合做渲染显示方面的工作。拿一张图的后处理来说,这张图有1万个像素点,你要把所有点都变深一点,用CPU可以用循环做很多很多次遍历,最终遍历完整张图,而GPU可以直接给每一个运算单元分配一块像素,各自开工,瞬间解决。
- 也正是因为GPU和CPU的这种区别,使得我们写的shader代码中诸如顶点着色器,片元着色器效率能很高,顶点着色器会分给很多个运算单元各自计算各自负责的顶点,片元着色器分给很多个运算单元各自计算各自负责的片元。
几何阶段_顶点着色器
- 这个阶段就是我们需要编写进shader中的第一个阶段了,主要负责从应用阶段拿到我们需要的一些参数,然后根据这些东西计算出剪裁空间下的顶点坐标,然后传给后面的工位(当然你也可以做一些其他的事情)。
-
取参数:到这里就不得不提一下shader中参数从何而来,在系统渲染时会自动调用我们写好的顶点着色器,并按照我们规定自动传入参数,所以我们需要告诉系统,我们需要哪些参数,这个告知方式固然不是自己想怎么说就怎么说,要使用和系统约好的“暗号”,专业点的术语叫做语义绑定。
下面举个小例子:
struct VertexInput
{
float4 Pos:POSITION; //语义绑定
float2 uv:TEXCOORD0; //语义绑定
};
- 如上定义了一个结构体其中包含的参数都绑定了语义,这些语义做什么用的参照Unity_Shader中一些常用变量和函数的集合,这样一来将这个结构体作为定点函数的参数传递过去即可。
-
函数绑定和取参数一样,都是需要指明哪个函数是顶点函数,这样系统才不会调用错。
举例如下
#pragma vertex _Vert
- 如上绑定了顶点函数,名字为_Vert。 #pragma为绑定函数的关键词,vertex为指明绑定的函数为顶点函数。
光栅化阶段_片元着色器
- 这个阶段与顶点着色器基础思想山很相似,不过作用主体是光栅化后的片元,什么是片元,你可以直接理解成一块块的像素点,详细的可以自己搜索下。而其最终需要返回一个颜色值,就是当前片元的颜色了。
-
取参数:与之前顶点着色器很相似,都是定义一个结构体,然后让这个结构体内的参数绑定语义,在当做参数传进去。不过需要注意的是,其中涉及到需要顶点着色器传给片元着色器的参数,因此顶点着色器需要返回一个这样的结构体,以便后面片元着色器直接取到这些参数。
举例如下:
struct VertexOutput
{
float4 Pos:SV_POSITION;
float2 uv:TEXCOORD0;
};
- 函数绑定这部分与顶点部分差不多,只是vertex换成了fragment
#pragma fragment Pixel
- 如上便绑定了片元着色器函数,函数名为Pixel
简单例子
- 举个最简单的渲染例子,我们写一个shader封装成材质给胶囊体上色。
- 右键资源管理器,create->shader->Standard Surface Shader 创建一个新的表面着色器。
- 删除其中内容开始写,首先是整体结构搭好如下:
Shader "Custom/test" //指明其为shader代码,后面的字符串是其在材质选择渲染器时的路径
{
Properties //公开型变量,在材质的属性面板可以修改
{
}
SubShader //语法之一写就完事了,之后在理解。
{
}
FallBack "Diffuse" //默认声明,表示如果我们写的SubShader在当前设备上不支持,就用默认渲染
}
- 然后向变量区写入一个color类型的,表示最终渲染成什么颜色。
Properties //公开型变量,在材质的属性面板可以修改
{
_Color ("Color", Color) = (1,1,1,1)
}
- 接着在SubShader中写入一个新的pass块(相互独立,按序执行渲染的具体渲染块),在其中写入CG语法应用范围(使用CGPROGRAM关键词表示开始应用cg语法,使用ENDCG关键词表示结束),并在范围内写入头文件声明,函数绑定。
SubShader
{
pass
{
CGPROGRAM
#pragma vertex _Vert
#pragma fragment Pixel
#include "UnityCG.cginc"
ENDCG
}
}
- 接着定义输入输入结构体,声明要用到的变量(当声明变量名字和Properties块中一样的时候,就表示你这个变量是那个外界可以调的)。
struct VertexInput
{
float4 Pos:POSITION;
};
struct VertexOutput
{
float4 Pos:SV_POSITION;
};
fixed4 _Color;
- 然后是顶点着色器和偏远着色器的编写
VertexOutput _Vert(VertexInput v)
{
VertexOutput r;
//将顶点转换到剪裁空间
r.Pos = UnityObjectToClipPos(v.Pos);
return r;
}
fixed4 Pixel(VertexOutput v):SV_Target
{
//直接将定义好的颜色返回为该片元的颜色
return _Color;
}
- 到这里就编写完了,完整代码如下:
Shader "Custom/test"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
}
SubShader
{
pass
{
CGPROGRAM
#pragma vertex _Vert
#pragma fragment Pixel
#include "UnityCG.cginc"
struct VertexInput
{
float4 Pos:POSITION;
};
struct VertexOutput
{
float4 Pos:SV_POSITION;
};
fixed4 _Color;
VertexOutput _Vert(VertexInput v)
{
VertexOutput r;
//将顶点转换到剪裁空间
r.Pos = UnityObjectToClipPos(v.Pos);
return r;
}
fixed4 Pixel(VertexOutput v):SV_Target
{
//直接将定义好的颜色返回为该片元的颜色
return _Color;
}
ENDCG
}
}
FallBack "Diffuse"
}
- 然后我们创建一个材质,如图红色箭头按照代码中编写的路径选好我们写的shader,然后蓝色箭头就是我们设定的颜色。
- 拖到胶囊体上,效果如下
写在后面:到这里这篇要说的内容就结束了,不过并没有详细讲解shader代码中的一个常用标准格式,大家可以参照以下我以前的笔记Unity_shader(属性详解,顶点片元函数的使用)和Unity_shader(结构体定义及其使用,两系统函数之间参数的传递,部分语义传递关系,光照模型)
结束,感谢阅读