注:本篇为学习记录,请支持原作者原文链接:(31条消息) 对酒当歌﹏✍_CSDN博客-游戏开发,Unity,Shader领域博主https://blog.csdn.net/lxt610?type=blog
一.图形硬件简史与可编程管线
1、图形处理器(GPU)简史
1.1、GPU发展简史
GPU英文全称Graphic Processing Unit,中文翻译为“图形处理器”,在现代计算机系统中的作用变得越来越重要。
20世纪六、七十年代,受硬件条件的限制,图形显示器只是计算机输出的一种工具。限于硬件发展水平,人们只是纯粹从软件实现的角度来考虑图形用户界面的规范问题。此时还没有GPU的概念。
GPU概念在20世纪70年代末和80年代初被提出,使用单片集成电路(monolithic)作为图形芯片,此时的GPU被用于视频游戏和动画方面,它能够很快地进行几张图片的合成(仅限于此)。在20世纪80年代末到90年代初的这段时间内,基于数字信号处理芯片(digital signal processor chip)的GPU被研发出来。与前代相比速度更快、功能更强,当然价格是非常的昂贵。在1991年,S3 Graphics公司研制出第一个单芯片2D加速器,到了1995年,主流的PC图形芯片厂商都在自己的芯片上增加了对2D加速器的支持。
1998年NVIDA公司宣布modern GPU的研发成功,标志着GPU研发的历史性突破成为现实。通常将20世纪70年代末到1998年的这一段时间称之为pre-GPU时期,而自1998年往后的GPU成为modern GPU。在pre-GPU时期,一些图形厂商,如SGI、Evans&Sutherland,都研发了各自的GPU,这些GPU在现在并没有被淘汰,依然在持续改进和被广泛使用,当然价格也是非常高昂。modern GPU使用晶体管(transistors)进行计算,在微芯片(microchip)中,GPU所使用的晶体管已经远远超过CPU。例如,Intel在2.4GHz的Pentium IV上使用5千5百万(55 million)个晶体管,而NVIDIA在GeForce FX GPU上使用超过1亿2千5百万(125 million)个晶体管,在NVIDIA 7800 GTX上的晶体管达到3亿2百万(302)个。
回顾GPU的发展历史,自1998年后可以分为4个阶段。NVDIA于1998年宣布Modern GPU研发成功,这标志着第一代Modern GPU的诞生,第一代GPU包括NVDIA TNT2,ATI的Rage和3Dfx的Voodoo。这些GPU可以独立于CPU进行像素缓存区的更新,并可以光栅化三角面片以及进行纹理操作,但是缺乏三维顶点的空间坐标变换能力,这意味着必须依赖于CPU执行顶点坐标变换的计算。这一时期的GPU功能非常有限,只能用于纹理组合的数学计算或者像素颜色值的计算。
1999年,NVDIA推出一款可以用“惊变”来形容的显示核心代号为NV10的GeFore 256。NVDIA率先将硬体T&L整合到显示核中。T&L原先有CPU负责,或者由另一个独立处理机处理。T&L是一大进步,原因是显示核心从CPU接管了大量工作。硬件T&L引擎带来的效果是,3D模型可以用更多的多边形来描绘,这样就拥有了更加细腻的效果。而对于Lighting来说,CPU不必再计算大量的光照数据,直接通过显卡就能获得更好的效能。同时,这一阶段的GPU对于纹理的操作也扩展到了立方体纹理(cube map)。NVDIA的GeForce MAX,ATI的Radeon 7500等都是在这一阶段研发的。
T&L技术是最近在图形加速卡上都可以看得到的规格名称之一,这个名词的原来意义是:Transforming以及Lighting,光影转换。在图形加速卡中,T&L的最大功能是处理图形的整体角度旋转以及光源阴影等三维效果。
2001年是第三代modern GPU的发展时期,这一时期研发的GPU提供vertex programmability(顶点编程能力),如GeForce 4Ti,ATI的8500等。这些GPU允许应用程序指定一个序列的指令进行顶点操作控制(GPU编程的本质),这同样是一个具有开创意义的时期,这一时期确立的GPU编程思想一直延续到今天,不但深入到工程领域帮助改善人类日常生活(医疗、地质勘探、游戏、电影等),而且开创或延伸了计算机科学的诸多研究领域(体绘制、光照模拟、人体动画、通用计算等)。同时,Direct8和OpenGL都本着与时俱进的精神,提供了支持vertex programmability的扩展。不过,这一时期的GPU还不支持像素级的编程能力,即fragment programmability(片段编程能力)。
所谓Vertex,就是我们熟悉的组成3D图形的顶点,由于3D模型是基于坐标空间内部设计的,所以Vertex信息包含了3D模型在空间内的坐标等信息。Vertex Shader则是对于Vertex信息的运算编程器,可以通过赋予特定的算法而在工作中改变3D模型的外形,Vertex Shader顶点运算单元可以直接检索显存中的材质数据。现在的游戏场景越来越复杂了。所涉及到的材质和多边形数量都是非常惊人的。并且游戏开发人员还可以利用Vertex Shader的这一新的特性,充分发挥想象,实现很多非常漂亮的特效。例如在星际争霸2 demo片中展示的神族母舰黑洞的技能效果。
第四代GPU的发展时期从2002年末到2003年。NVDIA的GeForceFX和ATI Radeon 9700同时在市场的舞台上闪亮登场,这两种GPU都支持vertex programmability和fragment programmability。同时DirectX和OpenGL也扩展了自身的API,用以支持vertex programmability和fragment programmability。自2003年起,可编程图形处理器正式诞生,并且由于DirectX和OpenGL锲而不舍地追赶潮流,导致基于图形硬件的编程技术,简称GPU编程,也宣告诞生。
1.2、GPU的优越性
由于GPU具有高并行结构,所以GPU在处理图形数据和复杂算法方面拥有比CPU更高的效率。CPU大部分面积为控制器和寄存器,与之相比,GPU拥有更多的ALU(Arithmetic Logic Unit,逻辑运算单元)用于数据处理,这样的结构适合对密集型数据进行并行处理。
GPU采用流式并行计算模式,可对每个数据进行独立的并行计算,所谓“对数据进行独立计算”就是流内任意元素的计算不依赖于其他同类型数据。例如,计算一个顶点的世界位置坐标,不依赖于其他顶点的位置,所谓“并行计算”是指多个数据可以同时被使用,多个数据并行运算的时间和一个数据单独实行的时间是一样的。所以,在顶点处理程序中,可以同时处理N个顶点数据。
1.3、GPU的缺陷
由于任意一个元素的计算不依赖于其他同类型数据,导致需要知道数据之间相关性的算法在GPU上难以得到实现。一个典型的例子是射线与物体的求交运算。GPU中的控制器少于CPU,致使控制能力有限。另外,进行GPU编程必须掌握计算机图形学相关知识,以及图形处理API,入门门槛高,学习周期长,尤其国内关于GPU编程的资料较为匮乏,这些都导致了学习的难度。在早期,GPU编程只能使用汇编语言,开发难度高,开发效率低。不过,随着高级Shader Language的兴起,在GPU上编程已经容易多了。
2、GPU的更多应用
- 科学可视化计算:由于人体CT、地质勘探、气象数据、流体力学等科学可视化计算处理的数据量极大,仅仅基于CPU进行计算完全不能满足实时性要求,而在GPU上进行计算则可以在效率上达到质的突破。许多在CPU上非常耗时的算法,如体绘制中的光线投射算法,都可以成功移植到GPU上。所以基于GPU的科学可视化研究目前已经成为主流。
- 通用算法:基于GPU进行通用计算的研究逐渐成为热点,被称为GPGPU(General-Purpose Computing on Graphics Procedding Units,也被成为GPGP,或GP2),很多数值计算等通用算法都已经在GPU上得到了实现,并有不俗的性能表现。目前,线性代数、物理仿真和光线跟踪算法都已经成功地移植到GPU上。在国内,中科院计算所进行了基于GPU的串匹配算法的实现。
3、总结
这里比较重要的就是从2003年开始GPU正式进入可编程GPU阶段。再者就是GPU的并行处理能力强于CPU,正是因为GPU的并行处理能力强我们才可以在同一时间内让GPU处理很多的顶点数据。最后就是尽管GPU的并行能力很强,但是目前GPU也无法取代CPU,这主要就是因为GPU还无法实现CPU强大的逻辑运算能力。
二.Shader和渲染管线
1、什么是Shader
Shader,中文翻译即着色器,是一种较为短小的程序片段,用于告诉图形硬件如何计算和输出图像,过去由汇编语言来编写,现在也可以使用高级语言来编写。一句话概括:Shader是可编程图形管线的算法片段。
它主要分为两类:Vertex Shader和Fragment Shader。
2、什么是渲染管线
渲染管线也称为渲染流水线,是显示芯片内部处理图形信号相互独立的并行处理单元。一个流水线是一序列可以并行和按照固定顺序进行的阶段。就像一个在同一时间内,不同阶段不同的汽车一起制造的装配线,传统的图形硬件流水线以流水的方式处理大量的顶点、几何图元和片段。
注意理解的是这里有一个前后关系,前一个阶段的输入会到后一个阶段去输出。比如在顶点程序当中,顶点程序计算的数据就会作为片段程序再进一步加工的材料。
为了更形象地理解渲染管线,来看一下下面的插图。
最上面的是【3D应用或者游戏】,【3D应用或者游戏】会直接调用【3D应用接口】,也就是OpenGL或者DirectX等。OpenGL和DirectX是方便应用程序去进行硬件访问和调用的中间层。如果没有它们,我们需要为硬件去编写一个非常复杂的专门针对硬件的驱动程序。
再接下来就是【CPU和GPU的分界线】,在此之上都是CPU的操作,以下进行GPU运算。
GPU的运算是从左到右依次进行,最开始【GPU前端模块】到【图元装配】这中间如果在过去T&L流水线的过程当中是由硬件设计好的运算过程,也就是集成的,它不能够进行编程控制。当图形硬件具有了可编程能力之后,我们就可以在这个阶段使用【顶点着色器】进行编写运算逻辑,用于取代过去直接集成在硬件当中的运算。在这个阶段完成之后,会到【光栅化以及插值】阶段。
光栅化就是把计算机显卡当中运算的数据进行一个细分,用于去适配屏幕上具体的每一个像素的显示,但是光栅化并不等同于像素显示,像素显示最终反映的是颜色,而光栅化过后得到的结果是【帧缓存】,在这个过程当中,我们可以插入【片段着色器】,这个部分就可以使用可编程化运算。所以【片段着色器】的目标是为屏幕上最终要显示的每一个像素去计算它最后需要什么样的颜色。
对于Unity来说,以上过程可以通过下图来解释。
最上面【Geometry】是几何模型,几何模型进入【Unity】,可以理解为把几何模型Mesh、网格等数据交给Unity,Unity导入后就通过Unity引擎去调用【Grphics APU】图形API,调用图形API的过程就是在驱动GPU进行处理运算。
进入GPU运算首先进行的是【Vertex Processor】顶点处理器,这个部分就需要我们使用【Vertex Shader】顶点着色器,顶点着色器运算的结果会交给【Pixel Processor】像素处理器,也就是片段处理器,在这个部分我需要为像素处理编写【Pixel Shader】像素着色器程序,这部分计算完后就输出了最终我们可以用于在屏幕上的颜色信息,我们把它叫做【Frame Buffer】帧缓冲。帧缓冲存储的是计算机依次显示所要的数据,但也不仅仅是这些数据,它还有其他的附加信息,比如深度值等。
下面是Unity官方手册中的一张渲染管线图示。
- 渲染管道线中最左边的这个部分中Transform指的是模型的空间变换,主要针对的是顶点的空间几何变换;TexGen即Texture Generator,表示的是纹理坐标的生成,主要用于在顶点当中去取得纹理坐标,再转换为UV取值的范围;Lighting指的是光照。因此这个部分就是过去就是T&L几何变换光照流水线,当图形硬件具有了可编程能力后,这个固定的模块就被【Vertex Shader】顶点着色器代替了。
- 在顶点着色器处理过后,Unity就进入【Culling & Depth Test】裁剪和深度测试过程。裁剪和深度测试描述的是如果一个物体在摄像机前展示,它向着摄像机的面会被观察到,它背对着摄像机的面不会被观察到,在这样的情况下,为了减少GPU处理数据量就进行了一个裁剪(Culling),把看不见的面直接剔除,不需要去处理的这些面所涉及的顶点数据,从而加速图形处理。第二个方面深度测试(Depth Test),指的是摄像机有一个特性,在计算机当中没有无限这个概念,计算机处理的数据都是离散化的,它有一个范围,当超过最近和最远这个范围的这部分会被剔除。
- 接下来就进入到纹理采样(Texturing)和雾化处理(Fog)阶段。在这阶段实际上就是在进行光栅化处理,描述的就是如何在屏幕上显示每一个像素的颜色。这里需要去纹理采样,一张贴图有很多数据,我们去采集纹理上某一点的颜色值,这个就叫做纹理采样。雾化就是根据最后计算的数据后需不需要进行一个雾化处理,近处的很清晰,远处的有种朦胧感,这个部分就是片段着色器可编程的能力范围。
- 之后还需要【Alpha Test】,指的是去绘制那些半透明的或全透明的物体。经过【Alpha Test】之后还需要进行【Blending】处理,这阶段会混合最终的图像。
以上介绍的是渲染管线的流程,要把握的主要的过程就是我们可编程能力是两个部分,一个就是几何变换和光照使用的顶点着色器部分,另一个就是关于如何去采样,去计算颜色以及雾化处理等使用的片段着色器部分。
这里需要注意的是Unity优化当中有一个重要部分就是减少Draw Call的调用,Draw Call就指的是应用程序去调用图形硬件GPU去进行渲染的调度过程。应用程序需要准备很多的数据,包括顶点数据、矩阵、向量等数据,都需要通过应用程序传递给GPU,这样个调度过程CPU必须要去收集数据以后才产生一个API的调用,这个过程的消耗是高昂的,如果反复地启动这个调用,那么就仅仅收集和传递参数这样的过程相当耗时耗力,就会造成了应用程序或游戏运行的瓶颈。因此要尽量减少Draw Call,尽量减少CPU对GPU这样的调用。
3、Shader和材质、贴图的关系
Shader(着色器)实际上就是一小段程序,它负责将输入的顶点数据以指定的方式和输入的贴图或者颜色等组合起来,然后输出。绘图单元可以依据这个输出来将图像绘制到屏幕上。输入的贴图或者颜色等,加上对应的Shader,以及对Shader的特定的参数设置,将这些内容(Shader及输入参数)打包存储在一起,得到的就是一个Material(材质),之后,我们便可以将材质赋予三维物体来进行渲染(输出)了。
材质好比引擎最终使用的商品,Shader好比是生产这种商品的加工方法,而贴图就是原材料。
4、总结
- Shader是图形可编程方案的程序片段。主要分为顶点着色器和片段着色器。
- 渲染管线是一种计算机从数据到最终图形成像的形象描述。
- 材质可以理解为商品,Shader是加工这种商品的方法,而贴图是加工过程中需要的材料。
三. 三大主流编程语言 HLSL/GLSL/Cg
1、Shader Language
Shader Language的发展方向是设计出在便携性方面可以和C++、Java等相比的高级语言,“赋予程序员灵活而方便的编程方式”,并“尽可能的控制渲染过程”同时“利用图形硬件的并行性,提高算法效率”。
Shader Language目前主要有3种语言:基于OpenGL的OpenGL Shading Language,简称GLSL;基于DirectX的High Level Shading Language,简称HLSL;还有NVIDIA公司的C for Graphic,简称Cg语言。
2、OpenGL简介
OpenGL(全写Open Graphics Library)是一个定义了跨编程语言、跨平台的编程接口规格的专业图形程序接口。它用于三维图像(二维亦可),是一个功能强大,调用方便的底层图形库。OpenGL是行业领域中最为广泛接纳的2D/3D图形API,其自诞生至今已催生了各种计算机平台及设备上的数千优秀应用程序。它独立于视窗操作系统或其他操作系统的,亦是网络透明的。在包含CAD、内容创作、能源、娱乐、游戏开发、制造业及虚拟现实等行业领域中。OpenGL是一个与硬件无关的软件接口,可以在不同的平台如Windows 95、Windows NT、Unix、Linux、MacOS、OS/2之间进行移植。因此,支持OpenGL的软件具有很好的移植性,可以获得非常广泛的应用。
OpenGL的发展一直处于一种较为迟缓的态势,每次版本的提高新增的技术很少,大多只是对其中部分作出修改和完善。1992年7月,SGI公司发布了OpenGL的1.0版本,随后又与微软公司共同开发了Windows NT版本的OpenGL,从而使一些原来必须在高档图形工作站上运行的大型3D图形处理软件也可以在微机上运用。1995年OpenGL的1.1版本面世,该版本比1.0的性能有许多提高,并加入了一些新的功能,其中包括改进打印机支持,在增强元文件中包含OpenGL的调用,顶点数组的新特性,提高顶点位置、法线、颜色、色彩指数、纹理坐标、多边形边缘标识的传输速度,引入了新的纹理特性等等。OpenGL 1.5又新增了“OpenGL Shading Language”,该语言是“OpenGL 2.0”的底核,用于着色对象、顶点着色以及片段着色技术的扩展功能。
3、DirectX简介
DirectX(Direct eXtension,简称DX)是由微软公司创建的多媒体编程接口。由C++编程语言实现,遵循COM。被广泛适用于Microsoft Windows、Microsoft XBOX、Microsoft XBOX 360和Microsoft XBOX ONE电子游戏开发,并且只能支持这些平台。最新版本为DirextX 12,创建在最新的Windows 10。DirectX是这样一组技术:它们旨在使基于Windows的计算机成为运行和显示具有丰富多媒体元素(例如全色图形、视频、3D动画和丰富音频)的应用程序的理想平台。DirectX包括安全和性能更新程序,以及许多涵盖所有技术的新功能。应用程序可以通过使用DirectX API来访问这些新功能。
DirectX加强3D图形声音效果,并提供设计人员一个共同的硬件驱动标准,让游戏开发者不必为每一品牌的硬件来写不同的驱动程序,也降低了用户安装及设置硬件的复杂度。从字面意义上说,Direct就是直接的意思,而后边的X则代表了很多意思,从这一点上可以看出DirectX的出现就是为了众多软件提供直接服务的。
举例来说,以前在DOS下玩家玩游戏时,并不是安装上就可以玩了,他们往往首先要设置声卡的品牌和型号,然后还要设置IRQ(中断)、I/O(输入和输出)、DMA(存取模式),如果哪项设置不对,那么游戏声音就发不出来。这部分的设置不仅让玩家伤透脑筋,对游戏开发者来说就更为头痛,为了让游戏能够正确运行,开发者必须在游戏制作之初,把市面上所有声卡硬件数据都收集过来,然后根据不同的API(应用编程接口)来写不同的驱动程序。这对于游戏制作公司来说,是很难完成的,所以在当时多媒体游戏很少。微软正是看到了这个问题,为众厂家推出了一个共同的应用程序接口——DirectX。只要游戏是依照DirectX来开发的,不管显卡、声卡型号如何,统统都能玩,而且还能发挥最佳的效果。当然,前提是使用的显卡、声卡的驱动程序必须支持DirectX才行。
4、Cg
GLSL与HLSL分别基于OpenGL和Direct3D的接口,两者不能混用,事实上OpenGL和Direct3D一直都是冤家对头,争斗良久。OpenGL在其长期发展中积累下的用户群庞大,这些用户会选择GLSL学习。GLSL继承了OpenGL的良好移植性,一度在Unix等操作系统上独领风骚。但GLSL的语法体系自成一家。微软的HLSL移植性较差,在Windows平台上可谓一家独大,这一点在很大程度上限制了HLSL的推广和发展。但是HLSL用于DX游戏领域却是深入人心。
Cg语言(C for Graphic)是为GPU编程设计的高级着色语言,Cg极力保留C语言的大部分语义,并让开发者从硬件细节中解脱出来,Cg同时也有一个高级语言的其它好处,如代码的易重用性,可读性得到提高,编译器代码优化。Cg是一个可以被OpenGL和Direct3D广泛支持的图形处理器编程语言。Cg语言和OpenGL、Direct3D并不是同一层次的语言,而是OpenGL和DirectX的上层,即Cg程序是运行在OpenGL和DirectX标准顶点和像素着色的基础上的。Cg由NVIDIA公司和微软公司相互协作在标准硬件光照语言的语法和语义上达成了一致开发。所以,HLSL和Cg其实是同一种语言。
5、总结
Unity官方手册上讲Shader程序嵌入的小片段是用Cg/HLSL编写的,从“CGPROGRAM”开始,到“ENDCG”结束。所以,Unity官方主要是用Cg/HLSL编写Shader程序片段。Unity官方手册也说明对于Cg/HLSL程序进行扩展也可以使用GLSL,不过Unity官方建议使用原生的GLSL进行编写和测试。如果不使用原生GLSL,你就需要知道你的平台必须是Mac OS X、OpenGL ES 2.0以上的移动设备或者是Linux。在一般情况下Unity会把Cg/HLSL交叉编译成优化过的GLSL。因此我们有多种选择,我们既可以考虑使用Cg/HLSL,也可以使用GLSL。不过由于Cg/HLSL更好的跨平台性,更倾向于使用Cg/HLSL编写Shader程序。
由于DirectX基于微软的操作系统,多用于PC,Xbox等主机设备上。而OpenGL是独立于操作系统的多用于移动端的开发!
四.Unity Shader的组织形式(ShaderLab)
1、Unity Shader的形态
Unity官方手册上讲Unity Shader有三种不同的编写方案,这三种编写方案分别是surface shaders、vertex and fragment shaders和fixed function shaders。 从前面几篇笔记中可以了解到,可编程图形管线中能够编写shader的主要是两个部分:vertex shader和fragment shader,但Unity还有surface shaders和fixed function shaders。
对于fixed function shaders,从表面意思来理解就是固定管线着色器,在可编程管线硬件出现之前,很多的光照流水计算都会放在硬件里进行处理,我们把这样的固定管线功能也看作是对应于固定管线硬件的操作,这种shader的功能是很保守的,比如说启用简单的光照,进行简单的纹理采样等等,对于现在绝大多数硬件都能得到很好的支持。
surface shaders是Unity中被推荐和鼓励使用的着色器,当你在Unity(Unity5之前)中创建一个shader时,默认的代码就是使用surface shaders。那么为什么会有surface shaders这样的着色器呢?它和vertex and fragment shaders又有什么关系呢?我们可以这样理解,可编程图形管线能够识别就是两种shader程序:vertex shader和fragment shader,至于surface shader是在vertex shader和fragment shader上面的一种包装,Unity引擎最终会把surface shaders代码编译成能够被硬件识别和调用的vertex and fragment shaders。
Unity官方推荐在学习shader时先去阅读ShaderLab syntax的基本概念,然后再去阅读surface shaders或者vertex and fragment shaders的相关内容。fixed function shaders只能够使用ShaderLab语法进行编写,而surface shaders和vertex and fragment shaders不限于ShderLab语法,是可以使用Cg/HLSL,甚至可用GLSL去编写,它是镶嵌在ShaderLab中的一种代码片段。
2、ShaderLab基本结构
ShaderLab是Unity定制的专门编写Shader的一种方案。使用ShaderLab可以把上文提到的三种shader用同一种格式来编写,这样就不会导致编写不同的shader需要不同的语法。前文中对shader、材质和贴图等它们之间的关系做了一个形象的比喻:材质是最终要使用的对象,shader是关于如何加工和处理的一种方案,是一种程序片段,至于贴图、颜色等它们是属于这个加工方案中的材料。那么当要建立一个Shader,要把它用于在材质上去使用的时候,既需要有算法,也需要添加一些原材料,首先来了解一下ShaderLab的主要结构,以下是ShaderLab的基本结构:
shader "name"{
[Properties]
SubShaders
[FallBack]
}
- Properties的作用是能够允许在Unity材质(material)检视面板中为材质去定义一些需要的参数,如颜色、贴图、参数等,这些都属于原材料,被shader程序使用。
- SubShader的作用就是专门为GPU渲染所编写的shader程序片段,在一个shader当中有并且至少需要一个SubShader,也可以有多个SubShader,但是在执行时只能选择其中的一个SubShader。那为什么允许多个SubShader同时存在呢?原因是shader中的某一种算法或者某一种指令可能不支持当前的硬件,比如说一个shader程序有多个SubShader,当这个shader程序被执行的时候,首先会去检测当前硬件能不能完好地支持第一个SubShader,如果第一个SubShader可以被当前硬件良好支持,那么就会使用当前SubShader,如果不能,那么就去检测下一个SubShader。在编写shader时,假如不能确定当前的SubShader在某些老旧的硬件上得到良好的支持,可以编写第二个SubShader,用来适配这些老旧的、稍差的图形硬件,当然也可以继续编写第三个、第四个、第五个等等,编写的SubShader功能要依次简化、运算指令依次简单,用来满足更多的硬件支持。如果编写的SubShader都不能支持当前的硬件时,就需要用到ShaderLab的“FallBack”功能了。
- FallBack的意思就是回滚。如果当前硬件无法支持所有的SubShader时,Unity会将当前的着色回滚,回滚到FallBack所指定的shader,因此FallBack指令后跟随的一般都是系统自带比较简单的,能够被绝大多数硬件执行的shader。以下列举了几个Unity官方的内建的shader,更加具体的信息可以查阅Unity官方手册。
Unity Build-In Shader
- Unlit. This is just a texture,not affected by any lighting.(不发光。这只是一个纹理,不被任何光照影响)
这是Unity中最为简单的shader,执行效率非常快,该shader经常被用于UI系统。- VertexLit.(顶点光照)
该shader拥有在顶点上渲染光照的能力。- Diffuse.(漫反射)
漫反射也是一种光照形态,不过它不仅仅在顶点上进行光照计算,在片段程序中也要进行光照计算。- Normal Mapped.This is a bit more expensive than Diffuse:it adds more texture(normal map),and a couple of shader instructions.(法线贴图,比漫反射更昂贵:增加了一个或更多纹理(法线贴图)和几个着色器结构)
法线贴图技术是一种比较传统的图形渲染技术,通过一张贴图进行采样计算,但这张贴图不是一张普通的贴图,而是一张存储法向量的贴图,把这张图采样出来的数据当做法向量,然后再进行光照计算,主要目的就是当几个模型面片顶点数量不多,构成的面片细节不多的时候,为了表达丰富的细节,可以使用法线贴图去弥补这样的细节,以假乱真,因此法线贴图shader的使用率很高。- Specular.This adds specular highlight calculation.(高光。增加了特殊的高光计算)
Specular指的是镜面高光反射,主要用于模拟光滑物体,比如金属、玻璃等。- Normal Mapped Specular.Again,this is a bit more expensive than Specular.(高光法线贴图。比高光更昂贵一点)
该shader结合了以上两点,既有法线贴图,又有高光显示。- Parallax Normal mapped.This adds parallax normal-mapping calculation.(视差法线贴图。增加了视差法线贴图计算)
- Parallax Normal Mapped Specular.This adds both poarallax normal-mapping and specluar highlight calculation.(视差高光法线贴图。增加了视差法线贴图和镜面高光计算)
视差即视觉的差异,比较形象的解释就是当人的左眼和右眼分别去看同一个物体时,两只眼睛看到的结果是不一样的,正是由于人的左眼和右眼有三厘米左右的距离,人看到的这个世界是三维的、立体的。
那么视差法线贴图的作用是什么呢?原因就是当法线贴图进行贴图映射的时候,是为了更好地表现物体的细节,法线数据存储在纹理当中,但是该纹理是不可变的,当物体渲染出来后,会得到一个比较好的结果,表现了物体凹凸的细节,所以法线贴图在有些教科书当中也叫作凹凸贴图,但是当把有法线贴图的物体进行旋转,照理说有些地方由于视角的偏差是看不到它的凹凸的形态或者说看到凹凸感觉会更强烈,由于法线贴图是一个固定的贴图,就会达不到这样的效果,而视差法线贴图的作用就弥补这样的缺陷。
3、总结
Unity Shader有三种不同的编写方案,这三种编写方案分别是surface shaders、vertex and fragment shaders和fixed function shaders。由于surface shaders 是 vertex shader和fragment shader的一种包装,最终编译时会重新解析成vertex shader和fragment shader,所以surface shaders 能实现的效果,vertex shader和fragment shader也能实现同样的效果,反之则不一定行!fixed function shaders由于实现的功能过于单一现在几乎不怎么使用了,我们可以做一个了解就可以了。
Shader中SubShader的数量至少有一个。 GPU渲染时会自上而下依次去执行SubShader,如果第一个SubShader能够支持运行,就执行第一个,如果第一个不能运行就尝试执行第二个,依次类推。。。如果所有的SubShader都不能执行,就执行回滚FallBack中的shader。书写SubShader的时候效果越好的放在最前,差一些的放后面这样执行的时候就会根据设备的情况运行最好的效果。而FallBack指令后跟随的一般都是系统自带比较简单的,能够被绝大多数硬件执行的shader。
五. Fixed Function Shader(第一篇)
1、Fixed Function Shader涉及的知识点
在学习固定管线着色器中要涉及到的知识点是:
- Properties(属性)
- Material(材质)
- Lighting(光照)
- SetTexture(纹理)
- Pass(通道)
2、ShaderLab的基本结构
shader "name"{
[Properties]
SubShaders
[FallBack]
}
在shader主要的三个部分中除了SubShaders有且至少需要一个之外,Properties和FallBack都是可以没有的。但是,如果没有Properties,我们就不能为Shader定制属性,没有FallBack,当任何一个SubShader都无法执行的时候,那么这个着色就会失败,就没有任何的方案去用于显示,所以一般条件下我们不了解我们的Shader是否适应所有的平台硬件时,我们都会使用一个FallBack,保证能够得到基本的正确的显示。
需要注意:
- SubShader中至少有一个Pass通道
- ShaderLab本身是没有大小写之分
- Properties中的代码语句之间没有分号隔开,否则报异常
- Pass语句块中的CG程序块中一个语句结束必须要用分号隔开,否则报异常
- Pass语句块中的CG程序块是区分大小写的,否则报异常(这里我们还是推荐大小写区分)
- CG程序块通常以CGPROGRAM开始,以ENDCG为结束
3、示例介绍
下面我们以一个示例来介绍Fixed Function Shader涉及到的知识点。
3.1、搭建场景
下面我们在Unity中创建一个新的工程,这里我使用的unity2017.4.27f1。保存当前场景,添加一个小球,一个材质球崇明名为M1和一个名为FixedFunctionShader1的shader,如图所示:
3.2、颜色属性(color)
此时我们打开FixedFunctionShader1.shder文件,我们删除全部内容书写如下代码:
//这里的shader名字不要求和文件名相同,也可以是一个路径
Shader "lxt610/FixedFunctionShader1" {
//subshader至少有一个
SubShader{
//Pass块至少有一个
Pass
{
color(1,1,1,1) // 分别代表了 r,g,b,a
}
}
}
此时我们把M1材质球拖拽给小球,在小球中的shader组件中选择 lxt610/FixedFunctionShader1,在Unity工程中就可以看到球体变成了红色,如下图所示:
我们虽然实现了改变颜色的功能,但是不方便调色!我们这里希望可以通过检视面版来操作颜色属性,办法也是有的。这里我们可以添加属性,属性的位置放在shaderd的开头,以便于后面的SubShader使用,属性的形式如下所示:
Properties{
//这里的变量名子前一半需要添加下划线防止和系统重名(不分大小写容易重名如:color和Color)
变量名("检视面版显示的该变量的名字",变量类型)=变量的初始值 //这里不能添加分号,否则报异常
}
这里我们修改颜色只需要改代码为以下形式:
Shader "lxt610/FixedFunctionShader1" {
Properties{
_Color("Main Color",color)=(1,1,1,1) //分别代表了r、g、b、a,当前默认值为白色。
}
SubShader{
Pass
{
//这里的_Color是区分大小写的这里把_Color改为_color时,默认就是_color = (0,0,0,0)由于这里和属性中的_Color是不同的,取不到属性中的值
//此时小球就会变为黑色
color[_Color] // 分别代表了 r,g,b,a
}
}
}
这里我们把Pass块中的括号改为中括号,并赋予Properties里的“_Color”参数,原因是小括号中的值是固定值,中括号里的值是可变参数值。然后在球体的检视面板中就可以通过“Main Color”来修改颜色值:
3.3、漫反射(diffuse)
这种以color形式着色的着色器,我们无论如何转换视角球体看起来都只是一个平面的圆,并没有体现球体三维立体的感觉,要达到三维立体的感觉需要运用光照。我们知道一个有立体感的物体表面是明暗变化的,这里的相对暗一点的颜色就是我们平常说的漫反射,高亮部分就是高光镜面反射。这里我们就是漫反射来达到这样一个效果。
这里就需要引入一些固定管线功能——漫反射:
Shader "lxt610/FixedFunctionShader1" {
properties{
_Color("Main Color",color)=(1,1,1,1)
}
SubShader {
pass{
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
}
}
}
}
代码中material材质是一个命令块,在这个命令块中可以添加属性,其中的diffuse属性描述的是材质的漫反射颜色,也可以把它理解为物体本身固有的颜色。然后回到Unity工程,编译成功后可以发现,在当前shader检视面板中无论怎么改变“Main Color”的颜色,球体的颜色都没有什么变化,这是什么原因呢?打个比方在真实的世界当中,所有颜色的呈现都必须依靠光,没有光的话我们将看不到任何东西,在有光的情况下,物体会接受光照,然后并反射出一部分光,这样我们才能够看到物体。
ShaderLab默认情况下光照不启用的,因此在这里漫反射没有反应,需要在pass通道中添加光照命令lighting,开启为on,关闭为off
Shader "lxt610/FixedFunctionShader1" {
properties{
_Color("Main Color",color)=(1,1,1,1)
}
SubShader {
pass{
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color]
material{
diffuse[_Color] // 漫反射
}
lighting on // 光照开关
}
}
}
这时我们在项目中shader编译成功后就可以看到球体有一定的立体效果了,球体被光照射的面相对变亮了,不背光照射的面相对变黑了,如下图。
3.4、环境光(ambient)
此时我们调整视角来观察球体,可以发现球体的状态还是不够形象生动,我们只是添加了漫反射,球体处在某一个环境当中,应该还会受到环境光的影响,所以还需要ambient环境光属性。
Shader "lxt610/FixedFunctionShader1" {
properties{
_Color("Main Color",color)=(1,1,1,1)
_Ambient("Ambient",COLOR) = (0.3,0.3,0.3,1)
}
SubShader {
pass{
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
ambient[_Ambient] //环境光
}
lighting on
}
}
}
在环境光ambient命令也配上一个参数“_Amnient”,声明在“Properties”中,类型也是“color”颜色,默认值范围是0~1,如果值为1,表示环境光全部为白色,这样的话即使有光照也反应不了光对物体的影响,环境光把物体全部照到最亮了,因此环境光的默认值rgb可以设置为0.3或者其他值,至于alpha值暂时可以随便设置,可以是1,也可以是0,对于目前的环境光没有影响。
回到Unity工程中,在检视面板中修改Ambient的颜色值,可以发现环境光属性对球体的影响。
3.5、高光反射(specular)
我们在日常中有不少表面光滑的物体具有很强的反光效果,我们如何让这个球体来表现这种现象呢?为了表现这种光滑的物体表面看到高光的效果,固定管线中有一个名为“specular”的命令,来表现高光部分。
我们在material中添加specular,并配上参数“_Specular”,“_Specular”类型“color”,默认颜色为白色。
如果在material使用了“specular”命令,还必须在pass通道中添加“separatespecular on”开启命令,否则specular镜面高光不启用。
Shader "lxt610/FixedFunctionShader1" {
properties{
_Color("Main Color",color)=(1,1,1,1)
_Ambient("Ambient",COLOR) = (1,0,0,1)
_Specular("Specular",COLOR) = (1,1,1,1)
}
SubShader {
pass{
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
ambient[_Ambient] //环境光
specular[_Specular] // 高光
}
lighting on // 光照开关
separatespecular on //镜面高光开关
}
}
}
编译后,可以发现specular已经起作用,改变specular的颜色也能得到即时的反馈。
3.6、高光强度(shininess)
这个高光效果看起来很奇怪,大面积地照亮了这个物体,看起来不是很舒服,并没有达到预期物体高光反射的感觉,在这里还需要另一个属性“shininess”,其参数类型为浮点值,用来描述“specular”的强度,它的参数“_Shininess”的类型可以是float,也可以用range给定一个范围,取值范围是[0.0, 128.0],值越高,表示物体越光滑,高光点越小且越亮(聚焦越好)。这里范围给定0~8,默认值为4。
Shader "lxt610/FixedFunctionShader1" {
properties{
_Color("Main Color",color) = (1,1,1,1)
_Ambient("Ambient",color)=(0.3,0.3,0.3,0)
_Specular("Specular",COLOR) = (1,1,1,1)
_Shininess("Shininess",Range(0,8)) = 4
}
SubShader {
pass{
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
ambient[_Ambient] // 环境光
specular[_Specular] // 高光
shininess[_Shininess] // 述specular强度
}
lighting on // 光照开关
separatespecular on //镜面高光开关
}
}
}
在shader编译通过后,可以看到球体的高光部分变得很集中了,通过拖动修改“Shininess”的值,可以看到物体高光反射部分的区域起了相应的变化。
3.7、自发光(emission)
固定管线着色器中还有一个功能是自发光,叫做“emission”,它可以使物体自身发光,不依赖于外部光源,参数类型也是颜色“color”。 如果将emission设置为白色,那么就会发现整个球体都变成白色了,因为在计算机当中颜色值从0到1,0是全黑,1是全白,因此即使没有光照,物体自发光都已经达到了全白,为了达到真实的感觉我们可以降低自发光的强度。
Shader "lxt610/FixedFunctionShader1" {
properties{
_Color("Main Color",color) = (1,1,1,1)
_Ambient("Ambient",color)=(0.3,0.3,0.3,0)
_Specular("Specular",COLOR) = (1,1,1,1)
_Shininess("Shininess",Range(0,8)) = 4
_Emission("Emission",COLOR) = (1,1,1,1)
}
SubShader {
pass{
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
ambient[_Ambient] // 环境光
specular[_Specular] // 高光
shininess[_Shininess] // 述specular强度
emission[_Emission] //自发光
}
lighting on // 光照开关
separatespecular on //镜面高光开关
}
}
}
4、Demo下载
如果有不明白的童鞋可以在这里下载上面的演示Demo,包含了全部效果!
5、总结
以上解释了ShaderLab的固定管线着色器的部分功能,通过设置diffuse(漫反射)、ambient(环境光)、specular(高光)、shininess(高光强度)、emission(自发光)等来修改固定管线着色器的功能,来改变material(材质)对物体产生的着色效果。
ShaderLab默认情况下光照不启用的,因此在这里漫反射没有反应,需要在pass通道中添加光照命令lighting,开启为on,关闭为off
如果在material使用了“specular”命令,还必须在pass通道中添加“separatespecular on”开启命令,否则specular镜面高光不启用。
六. Fixed Function Shader(第二篇)
1、Fixed Function Shader的知识点
上一篇中我们提到固定管线着色器中要涉及到的知识点,其中我们已经了解了Properties(属性)、Material(材质)、Lighting(光照)、settexture(设置纹理),由于篇幅原因还没说完。这里我们继续完善Fixed Function Shader的一个重要命令settexture(设置纹理)。
2、示例演示
这里我们还是做一个场景来演示具体功能!
2.1、场景搭建
场景跟上篇一样,一个小球,一个材质球M2、创建一个新的shader,命名为FixedFunctionShader2。方便起见我们直接在上一节的场景中演示(unity2017.4.27f1)。
2.2、settexture命令
这里我们使用上一篇中的shader内容,并添加settexture命令,详细代码如下:
Shader "lxt610/FixedFunctionShader2" {
properties{
_Color("Main Color",color) = (1,1,1,1)
_Ambient("Ambient",color)=(0.3,0.3,0.3,0)
_Specular("Specular",COLOR) = (1,1,1,1)
_Shininess("Shininess",Range(0,8)) = 4
_Emission("Emission",COLOR) = (1,1,1,1)
_MainTex("Main Texture",2D) = ""
}
SubShader {
pass{
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
ambient[_Ambient] // 环境光
specular[_Specular] // 高光
shininess[_Shininess] // 述specular强度
emission[_Emission] //自发光
}
lighting on // 光照开关
separatespecular on //镜面高光开关
SetTexture[_MainTex]
{
Combine texture
}
}
}
}
要使用贴图采样,我们需要命令settexture,在固定管线着色器中settexture是最重要的也是最复杂的一个命令。settexture需要一个纹理属性参数“_MainTex(“MainTex”,2d)="",其中的值就是纹理的名称,默认可以是空字符串,接着在settexture结构体中添加命令“combine texture”。回到Unity工程中,创建一个新的球体,将新建的材质拖放在球体上,在该球体材质的检视面板中拖放一张贴图,效果如下:
2.3、primary关键字
这时就可以看到在球体上使用这个贴图以后,球体上原有的灯光照明和高光效果已经没有了。这时因为:
在着色器中的combine命令指的是合并,它的参数目前只是用了texture纹理,这个texture指的就是属性中的_MainTex设置的贴图,而这里只有贴图而没有应用先前已经计算好的光照数据。这里如果需要先前的光照数据,我们需要为当前贴图texture乘上primary,primary是Fixed Function Shader的关键字,代表了前面所有计算材质和光照后的颜色值,将贴图和这个值相乘,就会得到一个混合的新的颜色值。
Shader "lxt610/FixedFunctionShader2" {
properties{
_Color("Main Color",color) = (1,1,1,1)
_Ambient("Ambient",color)=(0.3,0.3,0.3,0)
_Specular("Specular",COLOR) = (1,1,1,1)
_Shininess("Shininess",Range(0,8)) = 4
_Emission("Emission",COLOR) = (1,1,1,1)
_MainTex("Main Texture",2D) = ""
}
SubShader {
pass{
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
ambient[_Ambient] // 环境光
specular[_Specular] // 高光
shininess[_Shininess] // 述specular强度
emission[_Emission] //自发光
}
lighting on // 光照开关
separatespecular on //镜面高光开关
SetTexture[_MainTex]
{
Combine texture * primary
}
}
}
}
值得注意的是texture纹理的颜色值rgba每一个分量都是0到1,primary值的每一个分量也是0到1,当一个小于1的浮点数与另一个小于1的浮点数相乘,就会得到一个更小的浮点数,因此得到的颜色会变深,效果如下:
2.4、double关键字
考虑到这个问题,Fixed Function Shader当中有一个关键字“double”,表示对某个结果乘以2的运算,用法为“combine texture * primary double”。修改完shader,回到工程中,可以观察到球体变亮了。
Shader "lxt610/FixedFunctionShader2" {
properties{
_Color("Main Color",color) = (1,1,1,1)
_Ambient("Ambient",color)=(0.3,0.3,0.3,0)
_Specular("Specular",COLOR) = (1,1,1,1)
_Shininess("Shininess",Range(0,8)) = 4
_Emission("Emission",COLOR) = (1,1,1,1)
_MainTex("Main Texture",2D) = ""
}
SubShader {
pass{
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
ambient[_Ambient] // 环境光
specular[_Specular] // 高光
shininess[_Shininess] // 述specular强度
emission[_Emission] //自发光
}
lighting on // 光照开关
separatespecular on //镜面高光开关
SetTexture[_MainTex]
{
Combine texture * primary double
}
}
}
}
效果如下:
2.5、previous关键字
以上就是一个最基本的settexture的运用,当然在某些情况下需要为这个物体不只混合一张图,如果要混合两张或者两张以上的纹理该怎么处理呢?
这里我需要注意的是,一个settexture只能带上一个参数,不能再添加第二个参数,因此需要再编写一个settexture和一个纹理属性参数_SecTex。
这里我们修添加一个settexture命令,代码修改为下所示:
Shader "lxt610/FixedFunctionShader2" {
properties{
_Color("Main Color",color) = (1,1,1,1)
_Ambient("Ambient",color)=(0.3,0.3,0.3,0)
_Specular("Specular",COLOR) = (1,1,1,1)
_Shininess("Shininess",Range(0,8)) = 4
_Emission("Emission",COLOR) = (1,1,1,1)
_MainTex("Main Texture",2D) = ""
_SecTex("Main Texture",2D) = ""
}
SubShader {
pass{
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
ambient[_Ambient] // 环境光
specular[_Specular] // 高光
shininess[_Shininess] // 述specular强度
emission[_Emission] //自发光
}
lighting on // 光照开关
separatespecular on //镜面高光开关
SetTexture[_MainTex]
{
Combine texture * primary double
}
SetTexture[_SecTex]
{
Combine texture * primary double
}
}
}
}
这时等待编译通过,然后给给_SecTex拖上一张纹理,效果如下:
我们可以看到仅仅显示了第二张贴图,第一张贴图没有和第二张贴图进行混合。这时什么原因呢?这时因为:
这里虽然进行了两次settexture计算,但是如果想让第一次settexture和第二次settexture的结果进行混合就不能使用“combine texture * primary”语句,因为primary总是代表前面顶点光照计算的颜色值,这里还需要加上第一次settexture的结果。
在Fixed Function Shader当中除了primary外,还有一个previous关键字,previous指的是先前的数据。
将代码改成“combine texture * previous”,这里的意思就是用当前纹理的值去乘上当前settexture操作之前所有计算和采样过后的结果。
可以看到效果如下:
可以观察到已经有了需要的结果,球体已经被混合了两张纹理贴图。
按照这个思路可以继续去编写多个settexture,但是纹理混合的次数不是无限的,Fixed Function Shader是基本对照于显卡硬件的固定渲染部分,所以显卡有一个混合纹理的最大个数,一般来讲越好的硬件可以混合的纹理越多,越差的硬件可以混合的纹理越少,基本上两张纹理的混合是目前所有显卡都能够支持的。
2.6、透明处理
有两个物体一前一后,透明的物体放在前面我们是可以看到后面的物体的。就医上述场景中的球体做演示。我们要实现物体的透明化,需要借助alpha值,alpha值介于0到1之间,0为完全透明,1位不透明。
这里先分析下前面的计算,所有的计算都和颜色有关系。在当前的shader程序中,最后一个settexture使用了第二张纹理与先前所有计算的颜色值相乘,如果我们去改变先前计算过程中的alpha值,那么应该可以去影响这个球体的半透明度。在当前material材质计算当中,使用了四种颜色值,分别是diffuse、ambient、specular和emission,这四种颜色的alpha值基本都是1,我们可以在检视面板中把它们的alpha值都相对降低,按理来说这些颜色的alpha值都很低了,这四个颜色去影响贴图的时候,即使贴图的alpha值为1,相乘以那些颜色的alpha后,也会得到一个比较低的alpha值,然后第二个settexture又去相乘比较低的alpha值,最后应该会得到一个很低的alpha值,但是为什么不能观察到球体的半透明化呢?这个时候就需要去了解一下Unity Shader当中一个比较重要的命令,这个命令是ShaderLab当中的Blending。如图:
在着色器渲染过后,会进入到alpha测试,alpha测试以后就会进入到Blending阶段,Blending指的是混合,它可以用SrcFactor(源元素)和DstFactor(目标元素)去进行混合运算。其中,当我们正在渲染当前这个球体时,渲染这个球体得到的颜色值就是SrcFactor,而球体以外其他的物体包括天空盒等已经在这个球体渲染之前被渲染完成的值就是DstFactor。
查看Unity Manual中的ShaderLab: Blending,在Blend factors下可以看到很多关键词,其中我们可以先关注"SrcAlpha"和“OneMinusSrcAlpha”,"SrcAlpha"指的就是当前已经渲染得到的alpha值,而“OneMinusSrcAlpha”是指用1减去"SrcAlpha"的值,我们可以将命令“Blend SrcAlpha OneMinusSrcAlpha”放在shader程序当前的渲染通道当中。
Shader "lxt610/FixedFunctionShader2" {
properties{
_Color("Main Color",color) = (1,1,1,1)
_Ambient("Ambient",color)=(0.3,0.3,0.3,0)
_Specular("Specular",COLOR) = (1,1,1,1)
_Shininess("Shininess",Range(0,8)) = 4
_Emission("Emission",COLOR) = (1,1,1,1)
_MainTex("Main Texture",2D) = ""
_SecTex("Main Texture",2D) = ""
}
SubShader {
pass{
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
ambient[_Ambient] // 环境光
specular[_Specular] // 高光
shininess[_Shininess] // 述specular强度
emission[_Emission] //自发光
}
lighting on // 光照开关
separatespecular on //镜面高光开关
SetTexture[_MainTex]
{
Combine texture * primary double
}
SetTexture[_SecTex]
{
Combine texture * primary double
}
}
}
}
这句指令的意义是用当前渲染的alpha值与1减去当前渲染的alpha值的比例去混合之前已经被渲染好的场景颜色值。回到工程中,可以查看到球体发生了变化。如图:
虽然看起来不明显,但是我们可以发现球体整体变亮了一些,不过它没有真正达到透明,原因是在Unity引擎当中,存在一种渲染顺序的问题。图形显卡渲染某一帧图像的时候,当场景中有很多物体,到先渲染哪一个,后渲染哪一个,就取决于渲染顺序。在当前场景中,如果先渲染后面的球体,再渲染前面的球体,那么在渲染后面球体的时候是看不到前面这个球体的,因此在第一次渲染后面的球体时是可以看到整个球体的,如下图:
当第二次渲染前面这个球体时,该球体由于深度(z次序)被放在了靠近摄像机的地方,它会被稍后渲染,因此它就被叠加到先前生成的图像上,并且遮住了后面的物体,在这种情况下,我们就需要稍微去改变一下前面这个球体的渲染顺序,要改变渲染顺序,我们就需要了解使用ShaderLab: SubShader Tags。
在SubShader当中允许添加Tags标签,并以类似键值对的形式存在,语法格式为:
Tags { “TagName1” = “Value1” “TagName2” = “Value2” }
查看“Rendering Order - Queue tag”,这是渲染队列的标签,它包括以下这些值,分别是Background(背景)、
Geometry(几何图形)、AlphaTest、Transparent、和Overlay。一个实实在在的物体,它不透明的时候是默认使用Geometry进行渲染的,如果要去渲染半透明的物体,要使用在默认值后的渲染队列,其中我们可以使用Transparent,将“Tags { “Queue” = “Transparent” }”加到SubShader中。
Shader "lxt610/FixedFunctionShader2" {
properties{
_Color("Main Color",color) = (1,1,1,1)
_Ambient("Ambient",color)=(0.3,0.3,0.3,0)
_Specular("Specular",COLOR) = (1,1,1,1)
_Shininess("Shininess",Range(0,8)) = 4
_Emission("Emission",COLOR) = (1,1,1,1)
_MainTex("Main Texture",2D) = ""
_SecTex("Main Texture",2D) = ""
}
SubShader {
Tags { "Queue" = "Transparent" }
pass{
Blend SrcAlpha OneMinusSrcAlpha
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
ambient[_Ambient] // 环境光
specular[_Specular] // 高光
shininess[_Shininess] // 述specular强度
emission[_Emission] //自发光
}
lighting on // 光照开关
separatespecular on //镜面高光开关
SetTexture[_MainTex]
{
Combine texture * primary double
}
SetTexture[_SecTex]
{
Combine texture * previous
}
}
}
}
这句指令的意义是用当前渲染的alpha值与1减去当前渲染的alpha值的比例去混合之前已经被渲染好的场景颜色值。回到工程中,可以查看到球体发生了变化。如图:
假设一张贴图里已经含有alpha通道,我们希望使用这张图本身的alpha通道去进行透明化处理,如果贴图的一部分alpha为0,一部分alpha为1,那么就会形成一种镂空的效果,怎么去设置贴图的alpha通道处理呢?我们可以在最后的settexture的combine命令中加入第二个参数texture,代码如下:
combine texture * previous double,texture
其中,逗号后面的参数只是取了alpha通道值,我们期望用当前纹理的alpha通道去做当前alpha通道的设置。
回到场景中,发现球体已经不透明了,这是因为如果在settexture的combine命令的第二个位置填写了alpha的参数,那么它只能针对这个部分去取alpha值运算,而之前所有的颜色alpha值都失效了,要想看到透明效果,我们可以把这张贴图的灰度值编程alpha值,如图:
回到场景中,可以发现球体已经半透明了,如图:
还有一个constantColor命令,它可带一个参数,在properties(属性中)中声明一个参数:
“_ConstantColor(“ConstantColor”,color)=(1,1,1,0.3)”
将其放在第二个settexture中,然后在combine命令中参数texture乘上constant,用constant的alpha值0.3与纹理颜色中的alpha值再相乘,那么运算结果的alpha值会变得更小。回到工程场景中后会发现一个编译错误,但是没发现代码中有什么问题,这里需要注意的是先前在编写“_MainTex”和“_SecTex”纹理属性时直接用了一个空字符串,而在官方Shader教程中,纹理的值往往是有值的,比如“_MainTex(“MainTex”,2d)=“white”()”,如果不愿意去添加这些值,可以将语句“_ConstantColor(“ConstantColor”,color)=(1,1,1,0.3)”添加在纹理属性之前,如:
Shader "lxt610/FixedFunctionShader2" {
properties{
_Color("Main Color",color) = (1,1,1,1)
_Ambient("Ambient",color)=(0.3,0.3,0.3,0)
_Specular("Specular",COLOR) = (1,1,1,1)
_Shininess("Shininess",Range(0,8)) = 4
_Emission("Emission",COLOR) = (1,1,1,1)
_ConstantColor("ConstantColor",color)=(1,1,1,1)
_MainTex("Main Texture",2D) = ""
_SecTex("Main Texture",2D) = ""
}
SubShader {
Tags { "Queue" = "Transparent" }
pass{
Blend SrcAlpha OneMinusSrcAlpha
// color(1,0,0,1) // 分别代表了 r,g,b,a
// color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值
material{
diffuse[_Color] // 漫反射
ambient[_Ambient] // 环境光
specular[_Specular] // 高光
shininess[_Shininess] // 述specular强度
emission[_Emission] //自发光
}
lighting on // 光照开关
separatespecular on //镜面高光开关
SetTexture[_MainTex]
{
Combine texture * primary double
}
SetTexture[_SecTex]
{
constantColor[_ConstantColor]
Combine texture * previous,texture * constant
}
}
}
}
切回到场景中,着色器已经可以正常工作了,这个时候可以托送检视面板中的“ContantColor”值来改变球体的透明程度,但是可以发现当“ContantColor”的值为1时,球体也是半透明的,原因就是这里使用纹理的灰度值来当透明度。
2.7、总结
以上就是ShaderLab中的Fixed Function Shader的简单应用,比较重要的命令就是“settexture”,并且有一点复杂,而“settexture”中比较重要的命令是“combine”,“combine”命令可以配合“constantColor”使用来控制透明度,使用“blend”指令来控制最后的混合,并且使用tags标签来控制渲染顺序。
3、结束语
The End
好了,今天的分享就到这里,如有不足之处,还望大家及时指正,随时欢迎探讨交流!!!