Unity Shader学习记录(一)

Unity Shader学习记录(一)

  Shader,一般称为“着色器”,是在显示处理中的渲染流水线里扮演重要角色的一种程序,它的目的是为了更加详细和精确地控制显卡渲染图像的方式和方法,极大地增加了图形图像编程人员对显卡工作流程的干预能力。
  现代显卡的渲染流水线一般而言分成三个阶段,应用阶段,几何阶段和光栅阶段。其中应用阶段是指在图形图像制作软件里的过程,比如使用3D模型制作工具Maya或者3DMax制作三维模型,特效等的流程;在这个过程中制作人员是具有相当高的控制能力的,模型的外形,贴图,动作,场景的设定,灯光,粒子等等都由制作人员进行创建和编辑。
  但当模型和场景都制作完毕后,数据送入显卡进行渲染时,早期的制作人员都等同于失去了控制能力,显卡按照固定的流程和方法对输入的数据进行渲染并输出结果,那时的几何阶段和光栅阶段都在显卡内部完成。所谓几何阶段就是将输入显示处理设备的顶点数据,摄像机数据,各种坐标和向量等都转化为几何概念下的数据集合;该阶段包含许多细分的小阶段,通常来说会有如下的阶段。

  • 顶点着色
  • 曲面细分着色(可选)
  • 几何着色(可选)
  • 裁剪
  • 屏幕映射

  几何阶段完成后便进入最后的光栅阶段,之所以使用“光栅”这个名词,来源于计算机的显示屏,早期的计算机使用电子管发射阴极射线激发荧光物质来显示图像,因为阴极射线管一次只能照亮一个像素点,而为了显示整个图像需要在很短时间内对屏幕进行扫描,因此整个图像显示是“离散”的。而渲染流程中的光栅阶段也具有类似的特性,该阶段中会将几何数据转化为屏幕像素颜色数据并且刷新到缓存中,随后显卡便可以将图像显示到屏幕上。
  通常来说,光栅阶段也会有如下的细分阶段。

  • 三角形设置
  • 三角形遍历
  • 片元着色(又称像素着色)
  • 逐片元操作(又称逐像素操作)

  三个阶段全部完成,则一张图片被渲染到了屏幕上进行展示。

Unity中的Shader

  作为现代游戏开发引擎,Unity中的Shader和更早的OpenGL和DirectX中的Shader略有不同,其中最重要的一点就是Unity开发了自己的一套功能封装ShaderLab,它通过一层抽象提供了对主流的图形图像处理编程语言CG(NVida公司开发),HLSL语言(微软公司开发)和GLSL语言功能的调用,换言之在Unity中只需要按照ShaderLab编写Shader,Unity引擎会自动将其编译为对应平台上的Shader来让游戏使用。
  Untiy中一个最简单的Shader代码如下,一点点慢慢解析。

Shader "Custom/FinalTestShader" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
    }
    SubShader {
        Pass {
            Tags {"LightMode"="ForwardBase"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            float4 _Color;

            float4 vert(float3 vertex : POSITION) : SV_POSITION {
                float4 pos = UnityObjectToClipPos(vertex);
                return pos;
            }

            fixed4 frag(float4 pos : SV_POSITION) : SV_TARGET {
                return _Color;
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

  这个Shader赋给材质之后将不会渲染出任何3D效果,它等同于取了模型在当前视角下的平面映射并且将模型遮挡区域变成了纯色,根据Color设置的颜色来填充。
  第一行是ShaderLab的标准开头,表明了当前Shader的名字,根据名字中体现的分级情况这个Shader会在Unity的材质面板里显示在Custom菜单项下。

Shader "Custom/FinalTestShader"

  而后的Properties标签也是ShaderLab的设置,它说明了该Shader在Unity的Inspector面板中能显示的设置项,在示例里只有一个可设置项“_Color”,该设置项的类型为“Color”,就是一个有argb四个分量的组,等号后面给出了初始值(1,1,1,1),也就是纯白。

Properties {
    _Color ("Color", Color) = (1,1,1,1)
}

  往下SubShader是ShaderLab中的主要部分,也就是Shader主体,在这个部分Unity又有了新花样。在OpenGL和DirectX中,Shader主要就是两个部分“顶点着色”和“片元着色”,无论是CG语言还是GLSL语言都可以看得到这样的设定;然而Untiy提出了一种新的概念“表面着色”,它本质上是对“顶点着色”和“片元着色”的二次封装,在更高的抽象层次为开发者提供了方便快捷的接口。
  因此在SubShader标签下,Unity独有的“表面着色”和传统的“顶点着色”与“片元着色”都可能存在。“表面着色器”是直接在SubShader标签之中编写的,语法依然为ShaderLab,Unity会在随后自动编译出所需的“顶点着色”和“片元着色”代码。
  而“顶点着色”和“片元着色”部分的代码则需要在SubShader下属Pass标签中进行书写。
  通常来说,SubShader只能有一个起效,但在代码里开发者可以编写多个SubShader,这是因为Unity会自动选择按顺序第一个可以执行的SubShader来执行,因此多个SubShader并存可以增加Shader的适配性。
  示例中就是一个简单的“顶点着色器”和“片元着色器”组合,看到SubShader内的Pass部分代码,Tags标签下是对当前着色批次(Pass)的特性描述,在这里使用了一个LightMode设置,用于说明当前Pass是在前向渲染中处理光照的,有这个说明才能使用对应的一些和光照相关的数据。

Tags {"LightMode"="ForwardBase"}

  当然在这个简单的Shader中这个光照说明是无意义的,因为没有用到光照信息,因此可以删去。
  此后的CGPROGRAM表示开始进入Shader的正式代码部分,这个标签说明了下面的Shader代码是CG/HLSL风格的,而Unity还提供了另一个标签GLSLPROGRAM用于说明代码为GLSL风格。对于Unity来说两者并没有本质的区别,在部署时Unity会根据目标平台判断应该使用何种Shader并将不符合要求的Shader进行编译和转换。
  代码段以ENDCG结尾,使用GLSL语言时自然就是ENDGLSL,在这对标签之间的代码便是Shader的主体部分。
  第一和第二行的#pragma关键字是用于定义着色器的入口函数,对于顶点着色过程,入口函数为vertex描述,而片元着色过程则是fragment描述;入口函数的名称可以自定义,但通常使用vert和frag比较直观而且方便。

#pragma vertex vert
#pragma fragment frag

  接下来的#include关键字和C语言很相似,都是将另外一个文件中的代码包含进来,Unity对这种可以被包含的代码文件有相应的后缀名规定,CG/HLSL语言编写的为cginc,而GLSL语言编写的则是glinc。
  本例中包含的Lighting.cginc是用CG/HLSL语言编写的,和光照相关的功能代码,当然对Shader实现的简单功能也是没有意义的,可以删去。
  紧接着的一个变量定义是为了将Properties标签中设置的那些外部输入数据放入Shader中真正地使用,Unity会自动为这些变量赋值来适配Inspector面板上的设定。

float4 _Color;

  然后便是代码正文,两个方法分别对应顶点着色过程和片元着色过程,开发者可随意自定义别的方法方便使用,但被#pragma修饰的两个方法是必须的。

float4 vert(float3 vertex : POSITION) : SV_POSITION {
    float4 pos = UnityObjectToClipPos(vertex);
    return pos;
}

fixed4 frag(float4 pos : SV_POSITION) : SV_TARGET {
    return _Color;
}

  首先要注意到的是冒号以及后面的关键字,这些都是CG/HLSL语言中的“语义(Semantics)”,它们的主要作用就是告诉显卡驱动,哪些东西是用来干什么的。比如用POSITION语义修饰的vertex变量,其类型为float3,这也就是告诉显卡,把程序制作好的模型的顶点空间位置信息放到这个地方来方便使用,所以在vert函数执行过程中,vertex中储存的就是模型的顶点在模型空间下的坐标。
  而写在函数声明后面的语义,其实修饰的是函数返回值,比如vert函数声明后面的SV_POSITION语义就是告诉显示驱动,vert函数返回的数据是裁剪空间下的坐标位置,于是在frag函数里就可以用同样的语义从寄存器中取得相应的数据来使用了。
  至于frag的返回值,一般而言固定为fixed4类型,指的是屏幕上的一个像素颜色,语义修饰也一般固定为SV_TARGET,告诉显示驱动这个返回值就是要显示的目标数据。
  写在最后的Fallback关键字,是在告诉Unity如果因为显卡能力或者别的什么原因导致当前的SubShader无法运行,则回滚到一个可以运行的Shader上,因此Fallback后面一般会写一个Unity自带的Shader,主要是增强健壮性。

FallBack "Diffuse"

  这里就是说在SubShader无法运行时回滚到Unity自带的漫反射Shader。
  至此,Unity Shader编写的基础就介绍完了,一个Unity Shader就是这样一些部分组合出来的,而至于那些效果炫酷,功能强大的Shader究竟是怎么写出来的,那就需要在代码中涉及到数学知识以及相当水平的想象力了。
  以后会介绍一些简单功能的Shader并且解析其代码。

猜你喜欢

转载自blog.csdn.net/soul900524/article/details/79376222