Unity_Shader,作用流程入门程精讲

Unity_Shader

写在前面

  • 虽说已经入门(至少我觉得入门了?)shader有些时间了,不过由于慢慢的对入门级的东西有了新的感悟,再者组织要喊我给学弟学妹们培训一手,遂准备记下这篇内容作为教材。
  • 在我入门shader那会,啃过学长给的书,摸过siki老师的课,看过知乎大佬的专栏,无一例外,大家都是底层开篇,上手开始GPU渲染流程,渲染流水线。很专业的开篇,但对于没有底层基础和图形学基础的人来说,可能有点难以接受,更难融会贯通真正的用上。
  • 所以今天我写的这篇入门呢,主要是给没什么底层基础,图形学基础的小白入门参考。
  • 本文主要讲述图形渲染流水线大概是个什么样子(参考知乎大佬王子饼干的shader魔法书专栏),shader在这个流水线中怎样作用,CPU和GPU在后台工作时的区别,用shader处理图像和用c#处理图像有什么区别,一个最简单的shader。
  • 注意:本篇涉及到的绝大部分思维上的例子,举例所用的数字,纯属个人理解上的虚构,并非真正的底层知识,所以只能做一个领进门的作用,底层,还是要找专业的资料好好学一学的。
  • 哦对了,这篇文章实践性内容不多,所以可能会很枯燥,但如果你学的下去,小白入门进度会快很多。

渲染流水线

什么是渲染流水线

  • 大概就是说,你做游戏的时候这个游戏物体,本身也是一堆数据,他要经过“渲染”,这才会出现在屏幕上。而渲染流水线,就是将一团数据层层包装迭代筛选,最终呈现在屏幕上的过程。

渲染流水线的各大阶段

渲染流水线主要有三个阶段,应用阶段,几何阶段,光栅化阶段。

  • 应用阶段:在这个阶段,程序会拿到之后所用到的很多很多信息,诸如:场景中的物体模型,场景中的光源信息,以及我们所为其输入的一些数据。在这个阶段最后,会输出渲染所需要的几何信息到下一个工位。
  • 几何阶段:该阶段通常处理的是几何空间下的很多事情,诸如对模型的顶点信息进行空间变换,去取到一些信息来决定要绘制的图像,最后呢,这个阶段会将顶点信息输出到二维屏幕坐标下,然后传递给下一个工位。
  • 光栅化阶段:该阶段是讲之前的内容拿过来,并合并计算一个像素点的信息,最终决定屏幕上显示内容的部分。
  • 接下来看一张图,其中我只写入了本节需要我们关注的三部分,这三部分我会详细说一说。其他部分全部省略了,日后需要自行找找资料详细学习,其底层流程。
    Unity_Shader,作用流程入门程精讲

应用阶段_程序与数据的读入

  • 首先关于这部分呢,是系统要做的事,而不是我们要做的,但需要简单知道系统做了什么,知道在什么情况下做什么东西用c#做什么东西用shader。
  • Shader代码是执行在GPU上的,和我们通常写的C#代码不同,其仅仅支持一些简单的逻辑操作。下面看一张图来了解下这俩个执行在CPU与GPU上的程序各自的优缺点。
    Unity_Shader,作用流程入门程精讲
  • 如上图,蓝色部分是存储单元,其为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总结-语义1


下一篇:Android OpenGL 学习记录