首页 > 代码库 > Unity3D Shader之路 写Shader前必须要知道的事情 渲染流水线的概括

Unity3D Shader之路 写Shader前必须要知道的事情 渲染流水线的概括

版本:unity 5.4.1  语言:Unity Shader

 

总起:

    最近花了一个月的时间把《Unity Shader 入门精要》看完了,没怎么写博文,因为写得太好了,看得有点废寝忘食了,再次强烈推荐。

 

 今天把写Shader前必须要知道的渲染流水线给概括一下,然后简单结合顶点\片元着色器Shader,说说各个代码在流水线的位置,以及职责功能。

 

渲染流水线:

 在写Unity脚本的时候,不管是C#也好还是js也好,都是在跟CPU打交道,做算术运算、调用类成员、控制程序流程。而写Shader不同,他是跟GPU打交道。

 

 我们首先来看看脚本的一个调用流程:(百度上有中文翻译,我这边直接上官方网站了)https://docs.unity3d.com/Manual/ExecutionOrder.html

 

 从这里看出游戏中的一帧首先是初始化,也就是Awake、OnEnable、Start这些函数的执行,接着物理、输入、游戏逻辑的执行,在Unity中写代码的一天到晚都是跟这些函数打交道吧?接下来我们看到了Scene Rendering,这里就是一帧最重要的渲染部分了:由CPU发出指令(也就是平常所说的drawcall),GPU响应指令进行渲染,执行的就是一个渲染流水线。

 

 而Shader在其中对一个渲染流水线进行控制,告诉GPU,CPU传过来的这个物体该如何渲染。粗浅的来说Shader之于GPU,就如同C#之于CPU。

 

 那么渲染流水线是怎样的一个流程呢?简单的来说就是:顶曲几裁屏,三三片逐屏”。

 

    这是一种简单的记忆方法,感觉要还蛮好用的。

 

 第一行所说的是几何阶段:顶点着色器 -> 曲面细分着色器 -> 几何着色器 -> 裁剪 -> 屏幕映射。主要的工作就是将模型的各个顶点变换到屏幕坐标的一个空间中,为真正的渲染做准备。

 

 第二行说的是光栅化阶段:三角形设置 -> 三角形遍历 -> 片元着色器 -> 逐片操作 -> 屏幕图像。这一个阶段就是真正的在屏幕显示像素。

 

一个简单的Shader:

    这边就通过书上的例子简单的说说流水线各个部分的作用。

Shader "Unity Shaders Book/Chapter 5/Simple Shader" 
{
	// 显示在Unity Inspector上的属性,给用户提供控制
	// 形式:属性名("Inspector显示的标签", 属性类型) = 初始化数据
	Properties 
	{
		// 这是一个Color,初始化为(1,1,1,1),在Inspector上显示Color Tint,而在Shader中使用_Color进行调用
		_Color("Color Tint", Color) = (1, 1, 1, 1)	
	}

	// Shader的控制代码都在Pass中,一个SubShader可能包含多个Pass
	// 根据控制代码的不同可能会调用所有的Pass,也可能只调用一个
	// 现在我们只有一个,进行渲染就会调用这个Pass,暂时不用多管其他的情况
	SubShader 
	{
		// 标签,SubShader下的标签对所有Pass都有效,而Pass中的只对本身有效
		// Queue:说明渲染所在的队列,Geometry表明大多数不透明物体是在该队列中渲染的
		// RenderType Opaque:表明这个物体是不透明的,与队列的区别是,Queue只是渲染顺序,即使设置成其他的也没问题,
		//		              但你需要这个物体是不透明的RenderType就必须设置为Opaque
		Tags { "Queue" = "Geometry" "RenderType" = "Opaque" }
        Pass 
        {
        	// LightMode:光照模式
        	Tags { "LightMode" = "ForwardBase" }

        	// 设置,控制Shader的一些默认处理,深度测试和深度写入总是开启
        	ZTest On ZWrite On

            // CGPROGRAM和ENDCG成对出现,两行内部的代码就是用cg语言(c语言的变种)写的控制代码
            CGPROGRAM

            // vertex:表明控制顶点着色器的函数是vert
            // fragment:表明控制片元着色器的函数是frag
            #pragma vertex vert
            #pragma fragment frag
            
            // 申明Properties中的属性,以便在vert和frag函数中调用
            uniform fixed4 _Color;

            // Unity给vert函数提供的输入
			struct a2v 
			{
				// vertex必须,POSITION表明他是一个模型的顶点坐标
                float4 vertex : POSITION;
                // normal,NORMAL表明需要的是法线
				float3 normal : NORMAL;
				// texcoord,TEXCOORD0表明需要的是当前的模型的uv坐标
				float4 texcoord : TEXCOORD0;
            };
            
            // vert函数的输出,frag函数的输入
            // SV_开头的两个输出是必须的,而且必须这么写,这是流水线过程中必须获取的数据
            struct v2f 
            {
            	// pos必须,SV_POSITION表明需要输出一个屏幕坐标
                float4 pos : SV_POSITION;
                // color,颜色,其他属性,在frag函数中使用,不过一般冒号后面的标识为TEXCOORD0、TEXCOORD1、TEXCOORD2...
                //        只要不重复就行了,其他属性标明什么无所谓
                fixed3 color : COLOR0;
            };
            
            // 顶点着色器控制的函数
            v2f vert(a2v v) 
            {
            	// 先初始化输出结构
            	v2f o;

            	// UNITY_MATRIX_M:从模型空间到世界空间
            	// UNITY_MATRIX_V:从世界空间到相机的观察空间
            	// UNITY_MATRIX_P:从观察空间到屏幕空间
            	// o.pos:需要输出一个屏幕空间的坐标,所以很简单,UNITY_MATRIX_MVP是模型空间到屏幕空间的矩阵,然后乘以v.vertex就能得到
            	o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

            	// 根据法线计算颜色
            	o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);

            	// 输出
                return o;
            }

            // 片元着色器使用的控制函数,必须写上SV_Target,表明该输出是一个屏幕上的颜色
            fixed4 frag(v2f i) : SV_Target 
            {
            	// 获取vert处理后的颜色,再与_Color.rgb相乘
            	fixed3 c = i.color;
            	c *= _Color.rgb;

            	// 最后输出
                return fixed4(c, 1.0);
            }

            ENDCG
        }
    }

    // 申明备用的Shader,如果以上Pass无法运行的话
    Fallback "Diffuse"
}

 最重要的是vertex和fragment所指定的两个函数vert和frag。它们所在的流水线位置分别是顶点着色器和片元着色器。

 

 顶点着色器,对应vert函数,正如上文所说SV_POSITION变量是必须的,顶点着色器最重要的职责就是根据一个模型坐标输出屏幕坐标。

 

    曲面细分着色器和几何着色器应该也是可以编写的,不过学这本书的时候没有用到,所以暂时忽略。

 

 裁剪,vert把模型坐标变换到了屏幕坐标,这样就可以确定每个顶点是否在屏幕中,裁剪就是将不在屏幕中的顶点给裁剪掉。

 

 屏幕映射,把每个图元的坐标转换到屏幕坐标中,经过MVP矩阵后,坐标是在(-1,-1)到(1,1)中的,而屏幕映射就是把它映射到屏幕分辨率中。比如分辨率为1920*1080,则需要把(-1,-1)至(1,1)映射到(0,0)至(1920,1080)。

 

    三角形设置、三角形遍历。顶点组成三角形片,三角形片才组成网格,组成整个模型。这个两个阶段就是将顶点所组成三角形所覆盖的像素计算出来。

 

 片元着色器,对应frag函数,对上两个阶段中传来像素进行处理,最终需要输出一个SV_Target所代表的最终颜色。

 

 逐片操作,做深度测试、模板测试等,通过的像素才能显示在屏幕上,可以配置。对应上面的代码ZTest On开启深度测试。

 

    屏幕图像,最终显示的结果。

 

总结:

 顶曲几裁屏,三三片逐屏。记住这个的同时,需要知道vert函数做的是顶点变换对应顶点着色器这个阶段,而frag函数是对每个像素颜色进行处理,对应的是片元着色器这个阶段。

 

    而裁剪和逐片操作这两个阶段虽然无法编写代码,但可以进行控制。

 

 今天用自己的语言简单概括了一下渲染流水线,不知道大家能否通过这篇文章对渲染流水线有个简单的认识。写Shader,不知道流水线写的时候就根本不知道为什么要这样去写,所以掌握这个流水线是非常必要的。

 

 以后会给大家带来更多的Shader理解和一些有趣的Shader使用。


Unity3D Shader之路 写Shader前必须要知道的事情 渲染流水线的概括