天天看点

【U3D】进入shader编程的世界神马是Shader?     在Unity中创建第一个ShaderShader的基本框架

本系列文章由CSDN@萌萌的一天 出品,未经博主允许不得转载。

        接触shader编程已经有很长一段时间,最近有很多初学者问我许多关于Unity3D Shader方面的问题,我打算写一篇关于shader教学的文章,方便解决大家初入图像编程~大坑时遇到的麻烦。

       简单来说,学习Shader Programming就是学习如何利用GPU的力量。刚开始我会详细解释shader的作用、创建方式、基本语法等等,之后我会详细教给大家如何写出一个简单有效的shader案例。

神马是Shader?     

      Shader并不神秘,它只是很小的一段程序,包含着数学计算和算法模型,运行在计算机的图像管道层面,告诉计算机图像上的每一个像素应该怎样显示在屏幕上,它将一个个输入的网格绘制到屏幕之上,就能得到一个Material(材质),之后通过渲染器进行一系列的渲染输出操作后,我们就可以观察到客观的物体。Shader一般被称作着色器,它可以通过改变自身的属性来改变材质渲染到屏幕上的效果。以下是3中不同的Shader渲染到同一个材质上的例子:

【U3D】进入shader编程的世界神马是Shader?     在Unity中创建第一个ShaderShader的基本框架

      Unity3D中编写的Shader是通过.shader文件来进行实现的,它使用ShaderLab语言,极其类似Cg/HLSL。ShaderLab语言很类似于C语言,如果你有不错的C/C++编程功底的话,学习ShaderLab将会很容易。

    Shader分为三种类型:

           1、Fixed Function Shader( 固定渲染管线着色器)                        

           2、Surface Shader(表面着色器)

           3、Vertex Shader&Fragment Shader (顶点着色器&片段着色器)     

    接下来我们在Unity中创建Shader和Material,详细介绍每一种Shader的创建,用法等。

在Unity中创建第一个Shader

      首先,我们新建一个场景,在场景下的Project面板中新建一个叫做“Shader”的文件夹,方便我们管理今后创建的Shader文件。然后右键点击空白处--->Creat--->Shader,这样操作后,我们就能得到一个名为“NewShader”的.shader类型的文件了。

      之前说过,shader文件只有依附在Material,我们才能看到正确渲染出来的图像,所以我们还应创建一个新的Material,和之前类似,右键点击空白处--->Creat--->Material,创建一个新的Material,命名为“TestMaterial_1”。

【U3D】进入shader编程的世界神马是Shader?     在Unity中创建第一个ShaderShader的基本框架

     那么怎么将新创建的Shader赋予到Material上呢,我们点击“TestMaterial_1”,观察它的Inspector面板,在Shader-->Custom下就能找到我们刚刚创建的"NewShader"文件了。

【U3D】进入shader编程的世界神马是Shader?     在Unity中创建第一个ShaderShader的基本框架

Shader的基本框架

双击Shader文件,就能看到Shader的基本写法。总体来说,我们可以概括Shader的框架写法:

Shader "name" { [Properties] Subshaders [Fallback] [CustomEditor] }
           

用图形可以这样表示:

【U3D】进入shader编程的世界神马是Shader?     在Unity中创建第一个ShaderShader的基本框架

        在一个完整的Shader里,有一个Property,它定义了所有输入到Shader中属性的名称和类型,Property是一个可以自行设定的列表;Shader中有多个(至少一个)SubShader,根据GPU或者其它硬件处理性能来决定使用哪一个SubShader。通常为了保证一个游戏能在不同性能的机器上正确的运行,会有大量的备选SubShader方案,这些SubShader或许会选择放弃实现某些细节来保证效果大致的显示,每个SubShader下可能会有多个Pass通道,用来处理多种输入方式。并且在最后会有个FallBack,用来处理所有SubShader不能解决的情况。

     用编辑器打开刚才的“NewShader”文件后,代码大概是如下的样子(可能会因为Unity版本略有不同):

Shader "Custom/NewShader" {
	Properties {
		_Color ("Color", Color) = (1,1,1,1)
		_MainTex ("Albedo (RGB)", 2D) = "white" {}
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200		
		CGPROGRAM	
		
		sampler2D _MainTex;

		struct Input {
			float2 uv_MainTex;
		};
		half _Glossiness;
		half _Metallic;
		fixed4 _Color;
		void surf (Input IN, inout SurfaceOutputStandard o) {			
			fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
			o.Albedo = c.rgb;			
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG
	}
	FallBack "Diffuse"
           
  •       首先,第一行Shader "Custom/NewShader"为我们说明了这是一个.Shader文件,紧接的“Custom/NewShader”表示了此Shader文件的目录位置和名称。注意,.shader文件不会自动更新文件名,当我们更改一个Shader文件的名称之后,也要对这里的路径下的名称进行更改。
  •      接下来的Properties后的{}表示这个Shader的属性。属性类似于c/c#里的变量,我们可以定义它的名称、类型以及属性数值。关于Properties的相关语法说明可以参考:
    _Name("Display Name", type) = defaultValue[{options}]
               
          我们可以定义如下的类型:                                                                                                                                                  

             1、name ("display name", Range (min, max)) = number   

          自定义浮点型属性,在面板上通过介于最大/最小值间的滑条修改 

         2、name ("display name", Float) = number 

          自定义浮点型属性,在面板上可以手动输入浮点值

         3、name ("display name", Int) = number   

          自定义整型数值,在面板上可以手动输入整型数值

         4、name ("display name", Color) = (number,number,number,number) 

          自定义颜色属性,在面板上可以打开颜色选择器

         5、name ("display name", Vector) = (number,number,number,number) 

          自定义四维向量数值,在面板上可以手动输入四维向量(Float类型)

         6、name ("display name", 2D) = "defaulttexture" {}

          自定义2D Texture

         7、name ("display name", Cube) = "defaulttexture" {}

          自定义Cubemap

         8、name ("display name", 3D) = "defaulttexture" {}

          自定义3D Texture

【U3D】进入shader编程的世界神马是Shader?     在Unity中创建第一个ShaderShader的基本框架
  •     接下来是SubShader,它代表一个子着色器,里边所有声明的变量和输入输出都不会影响到其他的SubShader。我们看看它的第一行做了什么。
    Tags { "RenderType"="Opaque" }
               

             首先是个Tags标签,渲染管道会在进入每个SubShader之前,通过这些Tags标签来决定是否调用它们。Tag的语法如下:           

Tags{ "TagName1" = "Value1" "TagName2" = "Value2" }
           

Tags标签是用键值对的方式进行声明,上述例子中的“RenderType”="Opaque",表示告诉引擎,当渲染非透明物体时,我们会调用这个子着色器。RenderType表示渲染类型,除了Opaque(非透明物)还有有很多种,例如:

Transparent
半透明着色器,包括绝大多数粒子Shader,地形Shader等; TransparentCutout 半透明着色器补充类(Cutout Shader类型不允许绘制部分透明的区域),包括双通道(Pass)植被Shader Background 背景着色器,比如典型的天空盒; Overlay 覆盖类着色器,常用于GUITexture和Flare Shaders; TreeOpaque 不透明树木着色器;
 TreeTransparentCutout  透明树木类着色器,比如树叶的渲染;      

        Grass

        草地类渲染着色器;  

    除了RenderTyper,还有一些其它常用的标签类型,例如:

       "DisableBatching tag"="true",表示对此着色器禁用批处理绘制方式;

       "ForceNoShadowCasting tag"="true" 表示该SubShader不会对着色物体产生阴影;

       "IgnoreProjector tag"="true"表示该SubShader产生的阴影不受投影机(Projector)的影响;

       "Queue"=" "表示制定的的渲染顺序队列;

   这里需要详细解释Queue这个属性。可以想象,在一个大型游戏中,游戏物体的渲染大致顺序应该是由近到远逐步进行,从而保证游戏镜头逐步进行呈现,透明的水后边应该绘制不透明的物体等等。这样我们可以通过制定渲染顺序队列,来确保让透明的着色器类型能渲染在不透明物体前边。

   Unity为我们提供了四种预定义的渲染队列,当然,我们也可以自行添加自定义的渲染方式。官方提供的预定义渲染队列如下:

  • BackGroud:背景渲染队列,它优先于其他队列,主要用于渲染天空盒等背景元素;
  • Geometry(default):这是默认渲染队列,一般来说,场景中的大部分不透明物体都用此方式进行渲染。
  • AlphaTest:Alpha渲染通道,它是一种比较特殊的队列,用于渲染Alpha-tested的对象。
  • Transparent:透明物体渲染队列(Alpha值小于100),适合用于玻璃、粒子效果等。
  • Overlay:这个队列用来处理覆盖效果,一般来说最后呈现的渲染特效都在这里进行处理。

   上述几种渲染队列都有预定义的渲染优先级,优先级以整型数字进行表示,数字越小优先级越高。其中Background是1000,Geometry是2000,AlphaTest为2450,Transparent是3000,Overlay是4000.我们可以自定义队列类型,比如:Tag{"Queue"="AlphaTest+100"}。

  • 说完Tags标签作用和类型,下边是一句LOD声明 :      
    LOD 200 
               

          LOD即Level of Detail,它的值可以在Unity主界面--→Edit--→Project Settings--→Quality下进行设置,如下:

【U3D】进入shader编程的世界神马是Shader?     在Unity中创建第一个ShaderShader的基本框架

          此页面用来设置游戏每种画质的参数,如果SubShader的LOD数值小于该画质所设定的LOD时,此SubShader将会不可用。

  • 接下来是Shader中的主体部分
CGPROGRAM		
		#pragma surface surf Lambert		
		#pragma target 3.0
		sampler2D _MainTex;
		struct Input {
			float2 uv_MainTex;
		};
		half _Glossiness;
		half _Metallic;
		fixed4 _Color;
		void surf (Input IN, inout SurfaceOutputStandard o) {
			// Albedo comes from a texture tinted by color
			fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
			o.Albedo = c.rgb;
			// Metallic and smoothness come from slider variables
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG
           

          首先,CGPROGRAM表示开始位,与结束位ENDCG相对应,表明这之中是子着色器编译命令,所有的输入输出操作在其中进行。之后是一句#pragma surface指令,它的语法是:          #pragma surface surfaceFunction lightModel[optionalparams]        其中surfaceFunciton表示指定的Cg函数里有surface shader的函数或者相关代码,比如void surf(Input IN,inout SurfaceOutputStandard)。lightModel表示表面着色器所使用的光照模型,一般有两种:Lamert(defuse)和BlinnPhong(specular)两种,大多数情况下会用Lamert,即漫反射光照模式。         

#paragma target 3.0//<span style="font-family: Arial, Helvetica, sans-serif;">是Unity5.X版本新加入的指令,用来处理某些2.0版本的Surface shader指令集限制。</span>
           
struct Input {
			float2 uv_MainTex;
		};
           

         之后是对_MainTex的声明,这里要注意,虽然我们已经在Properties中对_MainTex进行声明,但是这个_MainTex是在CG代码块中,它和Properties分属于两个部分。因此如果我们想在CG中使用Properties中预定义的变量,需要重新进行声明。还有一点需要重新说明,在Properties中_MainTex的类型是2D类型,来CG块中它变为了2D纹理贴图类型,即sampler2D。       声明完_MainTex之后,紧接着是一个结构体,我们在结构体中声明了一个叫做uv_MainTex的变量,它的类型是Float2,即一个二维float类型。它的作用是将一个2D贴图上的点按照一定的规则映射到3D模型上,从而对3D模型进行渲染处理。通过声明uv_MainTex变量,我们就可以在之后的surf函数中通过访问uv_MainTex来获得2D贴图上的需要计算的点(float2)。

half _Glossiness;
		half _Metallic;
		fixed4 _Color;
           

        接下来,就像之前对_MainTex进行声明一样,我们还需对输入的_Glossiness变量、_Metallic变量和_Color变量进行声明。其中half类型是Cg语言中对Float/Double等浮点型数字处理的一种方式,它是半精度浮点数,运行效率很快,适合用于大规模的并行计算。

void surf (Input IN, inout SurfaceOutputStandard o) {
			// Albedo comes from a texture tinted by color
			fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
			o.Albedo = c.rgb;
			// Metallic and smoothness come from slider variables
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
           

     现在到了最后的关键函数surf,在surf里首先对_MainTex进行采样,将它和_Color叠加后进行输出。这里的tex2D函数是Cg中用于2D纹理采样,它的函数原型很多:         

float4 tex2D(sampler2D samp, float2 s)
float4 tex2D(sampler2D samp, float2 s, inttexelOff)
float4 tex2D(sampler2D samp, float3 s)
float4 tex2D(sampler2D samp, float3 s, inttexelOff)
float4 tex2D(sampler2D samp, float2 s,float2 dx, float2 dy)
float4 tex2D(sampler2D samp, float2 s,float2 dx, float2 dy, int texelOff)
float4 tex2D(sampler2D samp, float3 s,float2 dx, float2 dy)
float4 tex2D(sampler2D samp, float3 s,float2 dx, float2 dy, int texelOff)
int4 tex2D(isampler2D samp, float2 s)
int4 tex2D(isampler2D samp, float2 s, inttexelOff)
int4 tex2D(isampler2D samp, float2 s,float2 dx, float2 dy)
int4 tex2D(isampler2D samp, float2 s,float2 dx, float2 dy, int texelOff)
unsigned int4 tex2D(usampler2D samp, float2s)
unsigned int4 tex2D(usampler2D samp, float2s, int texelOff)
unsigned int4 tex2D(usampler2D samp, float2s, float2 dx, float2 dy)
unsigned int4 tex2D(usampler2D samp, float2s, float2 dx, float2 dy,int texelOff)
           

        tex2D函数的返回值是采样后的纹理,这样就能得到最终的着色效果。       最后感谢上善大神的文章~~~点这里,给了我许多帮助。   ------------------------------------------------------------------------------------------------------------------------------------------------------------        今晚就先写这么多,以后想到在写.......困死了。。我要睡觉~~~

继续阅读