U3D_Shader编程(第二篇:基础夯实篇)

U3D_Shader编程》

##U3D_Shader编程》发布说明:

++++Shader一个高大上的领域,不管怎么样,我来了。

++++立钻哥哥从2018年开始正式对Shader进行战略布局。

++++U3D_Shader编程》将从零开始,循序渐进探索,还是先探索一段时间吧,后期根据需要再推出一个精品博文,这篇就算一个杂谈吧。

##U3D_Shader编程》目录:

#第一篇:快速入门篇

#第二篇:基础夯实篇

#第三篇:基础进阶篇

#第四篇:中级挑战篇

#第五篇:高级成魔篇



#第二篇:基础夯实篇

#第二篇:基础夯实篇

++++经过“第一篇:快速入门篇”,我们了解了Shader的基本结构,但是还不够,通过这一篇的熟悉,我们将夯实Shader基本结构和相关原理。在这里立钻哥哥已经总结梳理了5个阶段(基础、初级、中级、高级、扩展等)对Shader进行较为全面的剖析,也为后续Shader进阶、Shader成魔夯实基础。

++++通过这一篇的学习,了解Unity中的一些渲染机制以及如何使用UnityShader实现各种自定义的渲染效果。(实际上,Shader仅是整个渲染流程的一个子部分,因此,任何脱离渲染流程的对Shader的介绍都会让我们更加困惑。)(向量运算、矩阵变换等数学知识在Shader编写中无处不在。数学知识在Shader中也非常重要的。)

++++Shader可以:渲染游戏模型、模拟波动的海面、实现各种屏幕特效等。(让Shader和其他游戏开发元素(例如,模型、纹理、脚本等)相配合,实现游戏中常见的渲染效果。)

++++Shader其实比我们想象的要强大得多。(这个需要我们共同努力去发掘。)

++++学好Shader需要:1、有一定(或少量)的编程经验;2、对Unity引擎的操作界面比较熟悉;3、保持一定的耐心;4、有一定的数学基础。

++++想要彻底理解Shader,必须了解整个渲染流水线的工作方式。(要知道为什么这么写,它们是怎么执行的等一系列基础问题。)

++++有一定的数学基础,包括了解基本的代数运算(如结合律、交换律等)、三角运算(如正弦、余弦计算等),要具有大学水平的线性代数、微积分等数学知识。

##1章:Shader基础篇

##2章:Shader初级篇

##3章:Shader中级篇

##4章:Shader高级篇

##5章:Shader扩展篇



##1章:Shader基础篇

++1章:Shader基础篇

++++1.1、了解Shader

++++1.2、渲染流水线

++++1.3UnityShader基础

++++1.4Shader中的数学基础



###1.1、了解Shader

++1.1、了解Shader

++++程序员的三大浪漫:编译原理、操作系统和图形学。

++++学习Shader比学习C#这样的编程语言更加困难,因为Shader需要牵扯到整个渲染流程。(Shader更多地是面向GPU的工作方式。)(建立一个渲染流程的整体体系,是跨越Shader学习中层层障碍的重要因素。)

++++Unity在原有的渲染流程中进行了封装,并提供给开发者新的图像编程接口:UnityShader

++++了解Shader中的基础的光照模型、纹理和透明效果等初级渲染效果。(了解Shader中实现基本的光照模型,如漫反射、高光反射等。)(纹理的使用给渲染的世界带来了更多的变化,)(透明是游戏中常用的渲染效果。)



###1.2、渲染流水线

++1.2、渲染流水线

++++不了解渲染流水线的工作流程,就永远无法说自己对Shader已经入门。

++++渲染流水线的最终目的在于生成或者说是渲染一张二维纹理,即我们在屏幕上看到的所有效果。(它的输入是一个虚拟摄像机、一些光源、一些Shader以及纹理等。)

++++Shader仅仅是渲染流水线中的一个环节,想要让我们的Shader发挥出它的作用,就需要知道Shader在渲染流水线中扮演的角色。

++++渲染流程一般分为3个阶段:应用阶段(Application Stage)、几何阶段(Geometry Stage)、光栅化阶段(Rasterizer Stage)。


++1.2.1、渲染流程

++++渲染流程一般分为3个阶段:应用阶段(Application Stage)、几何阶段(Geometry Stage)、光栅化阶段(Rasterizer Stage)。

 

++++应用阶段(Application Stage):此阶段由应用主导,通常由CPU负责实现(开发者具有这个阶段的绝对控制权)。开发者有以下主要任务:

--首先,需要准备好场景数据,例如摄像机的位置、视锥体、场景中包含了哪些模型、使用了哪些光源等等;

--其次,为了提高渲染性能,需要做一个粗粒度剔除(culling)工作,以把那些不可见的物体剔除出去,这样就不需要移交给几何阶段进行处理;

--最后,需要设置好每个模型的渲染状态。(这些渲染状态包括但不限于它使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的Shader等。)

--应用阶段最重要的输出是:渲染所需的几何信息,即渲染图元(rendering primitives)。(渲染图元可以是点、线、三角面等。)(这些渲染图元将会被传递给下一阶段:几何阶段)

++++几何阶段(Geometry Stage):用于处理所有和我们要绘制的几何相关的事情。(例如,决定需要绘制的图元是什么,怎样绘制它们,在哪里绘制它们。)(这一阶段通常在GPU上进行。)(几何阶段负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作。)(几何阶段可以进一步分成更小的流水线阶段。)

--几何阶段的一个重要任务是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。

--通过对输入的渲染图元进行多不处理后,这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段(光栅化阶段)。

++++光栅化阶段(Rasterizer Stage):使用上一阶段(几何阶段)传递的数据来产生屏幕上的像素,并渲染出最终的图像。(光栅化阶段是在GPU上运行。)

--光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。

--光栅化需要对上一阶段(几何阶段)得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。

--光栅化阶段也可以分成更小的流水线阶段。

++1.2.2CPUGPU之间的通信

++++渲染流水线的起点是CPU,即应用阶段。应用阶段大致可分为以下阶段:

--1、把数据加载到显存中;

--2、设置渲染状态;

--3、调用Draw Call

++++1、把数据加载到显存中】:所有渲染所需的数据都需要从硬盘(Hard Disk Drive, HDD)中加载到系统内存(Random Access Memory, RAM)中。然后,网格和纹理等数据又被加载到显卡上的存储空间显存(Video Random Access MemoryVRAM)中。这是因为,显卡对于显存的访问速度更快,而且大多数显卡对于RAM没有直接的访问权限。(真实渲染中需要加载到显卡中的数据比较复杂,比如顶点的位置信息、法线方向、顶点颜色、纹理坐标等。)

++++2、设置渲染状态】:渲染状态定义了场景中的网格是怎样被渲染的。(使用哪个顶点着色器(Vertex Shader/片元着色器(Fragment Shader)、光源属性、材质等。)(如果我们没有更改渲染状态,那么所有的网格都将使用同一种渲染状态。)

--渲染状态:不开启混合、使用这张纹理、使用这个顶点着色器、使用这个片元着色器等。

++++3、调用Draw Call】:Draw Call就是一个命令,它的发起方是CPU,接收方是GPU。(这个命令仅仅会指向一个需要被渲染的图元(primitives)列表,而不会再包含任何材质信息。)(在给定一个Draw Call时,GPU就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的那些漂亮的像素。这个过程,就是GPU流水线。)

 

 

++1.2.3GPU流水线

++++GPUCPU那里得到渲染命令后,就会进行一系列流水线操作,最终把图元渲染到屏幕上。(GPU渲染的过程就是GPU流水线。)(几何阶段和光栅化阶段,开发者无法拥有绝对的控制权,其实现的载体是GPU。但GPU向开发者开放了很多控制权。)(GPU通过实现流水线化,大大加快了渲染速度。)

++++几何阶段和光栅化阶段可以分成若干更小的流水线阶段,这些流水线阶段由GPU来实现,每个阶段GPU提供了不同的可配置性或可编程性。

++++GPU渲染流水线实现:

 

--颜色不同表示了不同阶段的可配置性或可编程性。

--【绿色】:表示该流水线阶段是完全可编程控制的;(顶点着色器、曲面细分着色器、几何着色器、片元着色器等)

--【黄色】:表示该流水线阶段可以配置但不是可编程的;(剪裁、逐片元操作等)

--【蓝色】:表示该流水线阶段是由GPU固定实现的,开发者没有任何控制权。(顶点数据、屏幕映射、三角形设置、三角形遍历、屏幕图像等)

--【实线】:表示该Shader必须由开发者编程实现。(顶点数据、顶点着色器、剪裁、屏幕映射、三角形设置、三角形遍历、逐片元操作、屏幕图像等)

--【虚线】:表示该Shader是可选的。(曲面细分着色器、几何着色器、片元着色器等)

--几何阶段:顶点着色器、曲面细分着色器、几何着色器、裁剪、屏幕映射等。

--光栅化阶段:三角形设置、三角形遍历、片元着色器、逐片元操作、屏幕图像等。

++1.2.4、顶点着色器(Vertex Shader

++++顶点着色器(Vertex Shader是流水线的第一个阶段。(它的输入来自于CPU。)

++++顶点着色器的处理单元是顶点。(输入进来的每个顶点都会调用一次顶点着色器。)(顶点着色器本身不可以创建后者销毁任何顶点,而且无法得到顶点与顶点之间的关系。)(此阶段的处理速度会很快。)

++++顶点着色器需要完成的工作主要有:坐标变换、逐顶点光照和输出后续阶段所需的数据。

++++GPU在每个输入的网格顶点上都会调用顶点着色器。顶点着色器必须进行顶点的坐标变换,需要时还可以计算和输出顶点的颜色。(可能需要进行逐顶点的光照。)

++++坐标变换,就是对顶点的坐标(即位置)进行某种变换。(顶点着色器可以在这一步中改变顶点的位置,这在顶点动画中是非常有用的。例如,我们可以通过改变顶点位置来模拟水面、布料等。)(无论我们在顶点着色器中怎样改变顶点的位置,一个最基本的顶点着色器必须完成一个工作:把顶点坐标从模型空间转换到齐次裁剪空间。)

++++顶点着色器会将模型顶点的位置变换到齐次裁剪坐标空间下,进行输出后再由硬件做透视除法得到NDC下的坐标。(NDCNormalized Device Coordinates,归一化的设备坐标。)

++1.2.5、裁剪(Clipping

++++我们的游戏场景可能会很大,而摄像机的视野范围不会覆盖所有的场景物体,那些不在摄像机视野范围的物体需要被裁剪(Clipping

++++一个图元和摄像机视野的关系有3种:完全在视野内、部分在视野内、完全在视野外。

--完全在视野内的图元就继续传递给下一个流水线阶段。

--完全在视野外的图元不会继续向下传递,因为他们不需要被渲染。

--部分在视野内的图元需要进行一个处理,就是裁剪。(在视野外的顶点应该使用一个新的顶点来代替,这个新的顶点位于视野边界的交点处。)

++1.2.6、屏幕映射(Screen Mapping

++++屏幕映射(Screen Mapping的任务是把每个图元的xy坐标转换到屏幕坐标系(Screen Coordinates)下。(屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。)

++++屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪些像素以及距离这个像素有多远。

++++注意1OpenGL把屏幕左下角作为(0,0)原点,而DirectX定义左上角为(0,0)原点。(如果发现得到的图像是倒转的,很有可能是屏幕坐标系的差异问题。)

++1.2.7、三角形设置(Triangle Setup

++++光栅化的第一个流水线阶段是三角形设置(Triangle Setup。(这个阶段会计算光栅化一个三角网络所需的信息。)(光栅化阶段有两个重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色。)(上一阶段输出的信息是屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等。)

++1.2.8、三角形遍历(Triangle Traversal

++++三角形遍历(Triangle Traversal阶段将会检查每个像素是否被一个三角网格所覆盖。(如果被覆盖的话,就会生成一个片元(fragment)。)(这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion))。

++++三角形遍历的输出就是得到一个片元序列。(一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息,如法线、纹理坐标等。)

++1.2.9、片元着色器(Fragment Shader

++++片元着色器(Fragment Shader:一个非常重要的可编程着色器阶段。(在DirectX中,片元着色器被称为像素着色器(Pixel Shader),但片元着色器更合适些,因为此时的片元并不是一个真正意义上的像素。)

++++片元着色器的输入是上一阶段对顶点信息插值得到的结果,输出是一个或多个颜色值。

++++片元着色器阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。(为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后通过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。)


++1.2.10、逐片元操作(Per-Fragment Operations

++++渲染流水线的最后一步:逐片元操作(Per-Fragment Operations,对每一个片元进行一些操作。(在DirectX中,这一阶段被称为输出合并阶段(Output-Merger))

++++逐片元操作阶段有几个主要任务:

--1)决定每个片元的可见性。(这涉及到很多测试工作,例如深度测试、模板测试等。)

--2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合(Blend)。

++++【片元】=>【模板测试】=>【深度测试】=>【混合】=>【颜色缓冲区】。

++++逐片元操作阶段是高度可配置性的,我们可以设置每一步的操作细节。(混合操作也是可以高度配置的,开发者可以选择开启/关闭混合功能。)

++1.2.11Unity Shader

++++渲染流水线比较复杂,Unity为我们封装了很多功能。(在Unity Shader设置一些输入、编写顶点着色器和片元着色器、设置一些状态就可以达到大部分常见的屏幕效果。)

++++Unity Shader缺点:封装性会导致编程自由度下降,无法掌握其背后的原理,在出现问题时,无法找到错误原因。

++++Shader就是:

--GPU流水线上一些可高度编程的阶段,而由着色器编译出来的最终代码是会在GPU上运行的(对于固定管线的渲染来说,着色器有时等同于一些特定的渲染设置);

--有一些特定类型的着色器,如顶点着色器、片元着色器等;

--依靠着色器我们可以控制流水线中的渲染细节,例如用顶点着色器来进行顶点变换以及传递数据,用片元着色器来进行逐像素的渲染。

++++要得到出色的游戏画面是需要包括Shader在内的所有渲染流水线阶段的共同参与才可完成:设置适当的渲染状态,使用合适的混合函数,开启还是关闭深度测试/深度写入等。

++++Unity提供了一个既可以方便地编写着色器,同时又可设置渲染状态的地方:Unity Shader

++1.2.12OpenGL/DirectX

++++OpenGLDirectX是图像应用编程接口,这些接口用于渲染二维或三维图形。这些接口架起了上层应用程序和底层GPU的沟通桥梁。(一个应用程序向这些接口发送渲染命令,而这些接口会依次向显卡驱动(Graphics Driver)发送渲染指令,这些显卡驱动是真正知道如何和GPU通信的角色,正是它们把OpenGL或者DirectX的函数调用翻译成了GPU能够听懂的语言,同时它们也负责把纹理等数据转换成GPU所支持的格式。)(显卡驱动就是显卡的操作系统。)

++1.2.13HLSLGLSLCG

++++HLSLGLSLCG是着色器语言。

--HLSLHigh Level Shading Language):DirectX着色语言。(微软控制着色器的编译。)

--GLSLOpenGL Shading Language):OpenGL着色语言。(GLSL是依赖硬件,而非操作系统层级的。)

--CGC for Graphic):NVIDIA着色语言。(CG是真正意义上的跨平台,会根据不同的平台,编译成相应的中间语言。)(CG语言可无缝移植成HLSL代码,但无法完全发挥出OpenGL的最新特性。)

++1.2.14Draw Call

++++Draw Call就是CPU调用图像编程接口,以命令GPU进行渲染的操作。

++++Draw Call多了会影响帧率:在每次调用Draw Call之前,CPU需要向GPU发送很多内容,包括数据、状态和命令的等。(如果Draw Call是数量太多,CPU就会把大量时间花费在提交Draw Call上,造成CPU的过载。)

++++如何减少Draw Call:使用批处理(Batching的方法。(把很多小的Draw Call合并成一个大的Draw Call,这就是批处理的思想。)(批处理技术更加适合于静态的物体,例如不会移动的大地、石头等,对于这些静态物体我们只需要合并一次即可。)

++++利用批处理,CPURAM把多个网格合并成一个更大的网格,再发送给GPU,然后在一个Draw Call中渲染它们。(使用批处理合并的网格将会使用同一种渲染状态,如果网格之间需要使用不同的渲染状态,那么就无法使用批处理技术。)

++++在游戏开发过程中,为了减少Draw Call的开销,注意点:

--注意1:避免使用大量很小的网格。(当不可避免地需要使用很小的网格结构时,考虑是否可以合并它们。)

--注意2:避免使用过多的材质。(尽量在不同的网格之间共用同一个材质。)

++1.2.15、什么是固定管线渲染

++++固定函数的流水线(Fixed-Function Pipeline,也简称固定管线,通常是指在较旧的GPU上实现的渲染流水线。(这种流水线只给开发者提供一些配置操作,但开发者没有对流水线阶段的完全控制权。)

++++固定管线通常提供了一系列接口,这些接口包含了一个函数入口点(Function Entry Points)集合,这些函数入口点会匹配GPU上的一个特定的逻辑功能。(固定渲染管线是只可配置的管线。)

++++第一个支持可编程管线的版本:OpenGL2.0OpenGL ES2.0DirectX8.0等。

++++随着GPU的发展,固定管线已经逐渐退出历史舞台。

++1.2.16、立钻哥哥推荐的一个Shader示例

//立钻哥哥:一个简单的Shader示例

Shader YanlzShaderDemo/SurfaceShade03{

    Properties{

        _MainTex(Base(RGB), 2D) = white{ }

    }

 

    SubShader{

        Tags{ RenderType=Opaque }

        LOD 200

    

        CGPROGRAM

        #pragma surface surf Lambert addshadow

        #pragma shader_feature REDIFY_ON

 

        sampler2D _MainTex;

    

        struct Input{

            float2 uv_MainTex;

        };

 

        void surf(Input IN, inout SurfaceOutput o){

             half4 c = tex2D(_MainTex, IN.uv_MainTex);

             o.Albedo = c.rgb;

            o.Alpha = c.a;

 

            #if REDIFY_ON

            o.Albedo.gb *= 0.5;

            #endif

        }

 

        ENDCG

    }

 

    CustomEditor YanlzCustomShaderGUI

}



###1.3UnityShader基础

++1.3UnityShader基础

++++Shader就是渲染流水线中的某些特定阶段,如顶点着色器阶段、片元着色器阶段等。

++++UnityShader提供了更加轻松地管理着色器代码以及渲染设置(如开启/关闭混合、深度测试、设置渲染顺序等)。

++1.3.1、材质和Unity Shader

++++Unity中我们需要配合使用材质(Material)和Unity Shader才能达到需要的效果。常见的流程是:

--1)创建一个材质;

--2)创建一个Unity Shader,并不它赋给创建的材质;

--3)把材质赋给要渲染的对象;

--4)在材质面板中调整Unity Shader的属性,以得到满意的效果。

++++Unity Shader定义了渲染所需的各种代码(如顶点着色器和片元着色器)、属性(如使用哪些纹理等)和指令(渲染和标签设置等),而材质则允许我们调节这些属性,并将其最终赋给相应的模型。

++1.3.2Unity中的材质

++++Unity中的材质需要结合一个GameObjectMesh或者Particle Systems组件来工作。(材质决定了我们游戏对象看起来是什么样子的。(这当然也需要Unity Shader的配合。))

++++创建材质:【Create=>Material】。(当创建一个材质后,就可以把它赋给一个对象。)

++++Unity5.x后续版本,默认一个新建的材质将使用Unity内置的Standard Shader。(这是一种基于物理渲染的着色器。)

++1.3.3Unity中的Shader

++++Unity Shader和渲染管线的Shader是不同的。

++++新建Unity Shader:【Create=>Shader】。

++++Unity Shader类型:“Standard Surface Shader”、“Unlit Shader”、“Image Effect Shader”、“Compute Shader”等。

--Standard Surface Shader】:会产生一个包含了标准光照模型(使用了Unity5中新添加的基于物理的渲染方法)的表面着色器模板。(我我们提供了典型的表面着色器的实现方法。)

--Unlit Shader】:会产生一个不包含光照(但包含雾效)的基本的顶点/片元着色器。(使用Unlit Shader来生成一个基本的顶点/片元着色器模板。)

--Image Effect Shader】:为我们实现各种屏幕后处理效果提供一个基本模板。

--Compute Shader】:会产生一种特殊的Shader文件,这类Shader旨在利用GPU的并行性来进行一些与常规渲染流水线无关的计算。

++++一个单独的Unity Shader是无法发挥任何作用的,它必须和材质结合起来使用。

++++Unity Shader的导入面板可查看:渲染队列(Render queue)、是否关闭批处理(Disable batching)、属性列表(Properties)等信息。

++1.3.4ShaderLab

++++ShaderLab is a friend you can afford。”

++++Unity提供了一层抽象:Unity Shader,而我们和这层抽象打交道的途径就是使用Unity提供的一种专门为Unity Shader服务的语言:ShaderLab。(Unity ShaderUnity为开发者提供的高层次的渲染抽象层。Unity希望通过这种方式来让开发者更加轻松地控制渲染。)

++++Unity中,所有的Unity Shader都是使用ShaderLab来编写的。(ShaderLabUnity提供的编写Unity Shader的一种说明性语言。)(ShaderLab使用了一些嵌套的花括号内部的语义来描述一个Unity Shader文件的结构。它们定义了要显示一个材质所需的所有东西,而不仅仅是着色器代码。)

 

++++Unity会根据使用的平台来把Unity Shader这些结构编译成真正的代码和Shader文件,开发者只需要和Unity Shader打交道即可。

++++Unity Shader的基础结构:

//立钻哥哥:Unity Shader基础结构

Shader YanlzShaderDemo/ShaderName{

    Properties{

        //属性

    }

 

    SubShader{

        //显卡A使用的子着色器

    }

 

    SubShader{

        //显卡B使用的子着色器

    }

 

    Fallback YanlzVertexLit

}

++1.3.5立钻哥哥推荐的一个简单Unity Shader示例:

Shader Custom/YanlzShaderDemo_shader2{

    //属性

    Properties{

        _Color(主颜色, Color) = (1,1,1,0)

        _SpecColor(高光颜色, Color) = (1,1,1,1)

        _Emission(光泽颜色, Color) = (0,0,0,0)

        _Shininess(光泽度, Range(0.01, 1)) = 0.7

        _MainTex(基础纹理(RGB-透明度(A, 2D) = white{ }

    }

 

    //子着色器

    SubShader{

        //定义材质

        Material{

            Diffuse[_Color]

            Ambient[_Color]

            Shininess[_Shininess]

            Specular[_SpecColor]

            Emission[_Emission]

        }

 

        Lighting On    //开启光照

        SeparateSpecular On    //开启独立镜面反射

        Blend SrcAlpha OneMinusSrcAlpha    //开启透明度混合(alpha blending

 

        //通道一:渲染对象的背面部分

        Pass{

            //如果对象是凸型,那么总是离镜头离得比前面更远

            Cull Front    //不绘制面向观察者的几何面

            SetTexture[_MainTex]{

                 Combine Primary * Texture

            }

        }

 

        //通道二:渲染对象背对我们的部分

        Pass{

            //如果对象是凸型,那么总是离镜头离得比背面更远

            Cull Back    //不绘制背离观察者的几何面

            SetTexture[_MainTex]{

                Combine Primary * Texture

           }

        }

    }

}

++1.3.6Unity Shader的名字

++++Shader Custom/YanlzShader{  }

++++通过在字符串中添加斜杠“/”,可以控制Unity Shader在材质面板中出现的位置:【Shader=>Custom=>YanlzShader】。

++1.3.7Properties属性(材质和Unity Shader的桥梁)

++++Properties语义块中包含了一系列属性(property),这些属性将会出现在材质面板中。

++++Properties语义块的定义:

Properties{

    _Name(display name, PropertyType) = DefaultValue

    _Name(display name, PropertyType) = DefaultValue

    ....    //更多属性

}

--【名字(_Name)】:这些属性的名字通常有一个下划线开始。(如果我们需要在Shader中访问属性,就需要使用每个属性的名字。)

--【显示的名称(display name)】:出现在材质面板上的名字。

--【类型(PropertyType)】:指定属性的类型。

--【默认值(DefaultValue)】:属性指定的默认值。(在我们第一次把该Unity Shader赋给某个材质时,材质面板上显示的就是这些默认值。)

++++Properties语义块支持的属性类型:IntFloatRange(min, max)ColorVector2DCube3D等。

--IntFloatRange这些数字类型的属性,默认值就是一个单独的数字;

--ColorVector这类属性,默认值是用圆括号包围的一个思维向量;

--2DCube3D这三种纹理类型,默认值是通过一个字符串后跟一个花括号来指定的。(字符串要么是空,要么是内置的纹理名称,如white”、“black”、“gray”、“bump”等;花括号的用处原本是用于指定一些纹理属性的。)

++++立钻哥哥推荐的一个简单示例:

Shader YanlzShader/Properties/MyTestShader{

    Properties{

        //Numbers and Sliders

        _Int(Int, Int) = 2

        _Float(Float, Float) = 1.5

        _Range(Range, Range(0.0, 5.0)) = 3.0

    

        //Colors and Vectors

        _Color(Color, Color) = (1,1,1,1)

        _Vector(Vector, Vector) = (2,3,6,1)

 

        //Textures

        _2D(2D, 2D) = “”{}

        _Cube(Cube, Cube) = white{}

        _3D(3D, 3D) = black{}

    }

 

    SubShader{

        Pass{

        }

    }

 

    FallBack Diffuse

}


++1.3.8SubShader子着色器

++++每个Unity Shader文件可以包含多个SubShader语义块,但最少要有一个。

++++Unity需要加载这个Unity Shader时,Unity会扫描所有的SubShader语义块,然后选择第一个能够在目标平台上运行的SubShader。(如果都不支持的话,Unity就会使用Fallback语义指定的Unity Shader。)(由于不同的显卡具有不同的能力,让低端显卡使用计算复杂度较低的着色器,在高端显卡上使用计算复杂度较高的着色器。)

++++SubShader语义块定义:

SubShader{

    [Tags]    //可选的

    [RenderSetup]    //可选的

 

    Pass{

    }

 

    //Other Passes

}

++++SubShader中定义了一系列Pass以及可选的状态([RenderSetup])和标签([Tags])设置。(每个Pass定义了一次完整的渲染流程,但是如果Pass数目过多,往往会造成渲染性能的下降。(因此,我们应尽量使用最小数目的Pass))

++++状态设置([RenderSetup]ShaderLab提供了一系列渲染状态的设置指令,这些指令可以设置显卡的各种状态。常见的渲染状态设置选项:

--Cull Back/Front/Off】:设置剔除模式,剔除背面(Back)、正面(Front)、关闭剔除(Off);

--ZTest Less Greater/LEqual/GEqual/Equal/NotEqual/Always】:设置深度测试时使用的函数;

--ZWrite On/Off】:开启/关闭深度写入。

--Blend SrcFactor DstFactor】:开启并设置混合模式。

--说明:当在SubShader块中设置渲染状态,将会应用到所有的Pass,也可在Pass语义块中单独进行状态设置

++++标签([Tags]:是一个键值对(Key/Value Pair),它的键和值都是字符串类型。(这些键值对是SubShader和渲染引擎之间的沟通桥梁。标签用来告诉Unity的渲染引擎怎样以及何时渲染这个对象。)

++++标签的结构:Tags{ TagName1=Value1 TagName2=Value2 }

++++SubShader的标签类型:QueueRenderTypeDisableBatchingForceNoShadowCastingIgnoreProjectorCanUseSpriteAtlasPreviewType等:

--Queue】:Tags{Queue=Transparent} ,控制渲染顺序,指定该物体属于哪一个渲染队列,通过这种方式可以保证所有的透明物体可以在所有不透明物体后面被渲染,我们也可以自定义使用的渲染队列来控制物体的渲染顺序。

--RenderType】:Tags{ RenderType=Opaque } ,对着色器进行分类,例如这是一个不透明的着色器,或者一个透明的着色器等。这可以被用于着色器替换(Shader Replacement)功能。

--DisableBatching】:Tags{ DisableBatching=True } ,一些SubShader在使用Unity的批处理功能时会出现问题,例如使用了模型空间下的坐标进行顶点动画。这时可以通过该标签来直接指明是否对该SubShader使用批处理。

--ForceNoShadowCasting】:Tags{ ForceNoShadowCasting=True } ,控制使用该SubShader的物体是否会投射阴影。

--IgnoreProjector】:Tags{ IgnoreProjector=True } ,如果该标签为True”,那么使用该SubShader的物体将不会受Projector的影响。通常用于半透明物体。

--CanUseSpriteAtlas】:Tags{ CanUseSpriteAtlas = False } ,当该SubShader是用于精灵(sprites)时,将该标签设置为“False”。

--PreviewType】:Tags{ PreviewType=Plane } ,指明材质面板将如何预览该材质。默认情况下,材质将显示为一个球形,我们可以通过把该标签的值设为Plane”“SkyBox”来改变预览类型。

--注意:标签仅可以在SubShader中声明,不可以在Pass块中声明

++1.3.9Pass(渲染通道)

++++Pass语义块结构:

Pass{

    [Name]

    [Tags]

    [RenderSetup]

    //Other code

}

++++Pass名称: Name “MyPassName”】:UsePass MyShader/MYPASSNAME

--由于Unity内部会把所有Pass的名称转换成大写字母,因此,在使用UsePass命令时必须使用大写形式的名字。

++++Pass的标签类型:LightModeRequireOptions

--LightMode】:Tags{ LightMode=ForwardBase } ,定义该PassUnity的渲染流水线中的角色。

--RequireOptions】:Tags{ RequireOptions=SoftVegetation } ,用于指定当满足某些条件时才渲染该Pass,它的值是一个由空格分隔的字符串。(目前,Unity支持的选项有:SoftVegetation

++++UsePass】:可以使用该命令来复用其他Unity Shader中的Pass

++++GrabPass】:该Pass负责抓取屏幕并将结果存储在一张纹理中,用于后续的Pass处理。

++1.3.10Fallback回滚

++++紧跟在各个SubShader语义块后面的,可以是一个Fallback指令。(Fallback指令告诉Unity:如果上面所有的SubShader在这块显卡上都不能运行,那么就使用这个最低级的Shader吧。)

++++为每个Unity Shader正确设置Fallback是非常重要的。(Fallback还会影响阴影的投射。在渲染阴影纹理时,Unity会在每个Unity Shader中寻找一个阴影投射的Pass。通常情况下,我们不需要自己专门实现一个Pass,这是因为Fallback使用的内置Shader中包含了这样一个通用的Pass。)

++1.3.11Unity Shader的形式(表面着色器、顶点/片元着色器、固定函数着色器)

++++Unity Shader可以做的事情非常多(例如设置渲染状态等),但其最重要的任务还是指定各种着色器所需的代码。这些着色器代码可以写在SubShader语义块中(表面着色器的做法),也可以写在Pass语义块中(顶点/片元着色器和固定函数着色器的做法。)

++++真正意义上的Shader代码需要包含在ShaderLab语义块中:

Shader YanlzShaderDemo/MyShader{

    Properties{

        //所需的各种属性

    }

 

    SubShader{

        //立钻哥哥:真正意义上的Shader代码会出现在这里

        //表面着色器(Surface Shader)或者顶点/片元着色器(Vertex/Fragment Shader)或者固定函数着色器(Fixed Function Shader

    }

 

    SubShader{

    }

}

++++选择哪种Unity Shader形式:

--除非有非常明确的要求必须要使用固定函数着色器,例如需要在非常旧的设备上运行游戏,否则请使用可编程管线的着色器,即表明着色器或顶点/片元着色器。

--如果想和光源打交道,可能更喜欢使用表明着色器,但它在移动平台的性能表现不好。

--如果需要使用的光照数目非常少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。

--有很多自定义的渲染效果,请选择顶点/片元着色器。

++1.3.12、表面着色器(Surface Shader

++++表面着色器(Surface Shader)是Unity自己创造的一种着色器代码类型。(它需要的代码量很少,Unity在背后做了很多工作,但渲染的代价比较大。)

++++当给Unity提供一个表面着色器的时候,它在背后仍旧把它转换成对应的顶点/片元着色器。(表面着色器是Unity对顶点/片元着色器的更高一层的抽象。)

++++表面着色器存在的价值在于,Unity为我们处理了很多光照细节。

++++一个简单的表面着色器示例:

Shader YanlzShaderDemo/Surface Shader{

    SubShader{

        Tags{ RenderType=Opaque }

 

        CGPROGRAM

        #pragma surface surf Lambert

 

        struct Input{

            float4 color : COLOR;

        };

 

        void surf(Input IN, inout SurfaceOutput o){

            o.Albedo = 1;

        }

 

        ENDCG

    }

 

    Fallback Diffuse

}

++++表面着色器定义在SubShader语义块(而非Pass语义块)中的CGPROGRAMENDCG之间。(表面着色器不需要开发者关心使用多少个Pass、每个Pass如何渲染等问题,Unity会在背后为我们做好这些事情。我们只要告诉:使用哪些纹理去填充颜色,使用哪个法线纹理去填充法线,使用Lambert光照模型等信息即可。)

++++CGPROGRAMENDCG之间的代码是使用CG/HLSL编写的,也就是说,我们需要把CG/HLSL语言嵌套在ShaderLab语言中。

++1.3.13、顶点/片元着色器(Vertex/Fragment Shader

++++Unity中我们可以使用CG/HLSL语言来编写顶点/片元着色器(Vertex/Fragment Shader)。它们更加复杂,但灵活性也更高。

++++一个简单的顶点/片元着色器示例:

//立钻哥哥:一个简单的顶点/片元着色器

Shader YanlzShaderDemo/VertexFragment Shader{

    SubShader{

        Pass{

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

        

            float4 vert(float4 v : POSITION) : SV_POSITION{

                return mul(UNITY_MATRIX_MVP, v);

            }

 

            fixed4 frag() : SV_Target{

                return fixed4(1.0, 0.0, 0.0, 1.0);

            }

 

            ENDCG

        }

    }

}

++++和表面着色器类似,顶点/片元着色器的代码也需要定义在CGPROGRAMENDCG之间,但不同的是,顶点/片元着色器是写在Pass语义块内,而非SubShader内的。(我们需要自己定义每个Pass需要使用的Shader代码。)(我们需要编写更多的代码,灵活性很高,可以控制渲染的实现细节。)

++1.3.14、固定函数着色器(Fixed Function Shader

++++这是一个被弃用的着色器。对于那些来设备不支持可编程管线着色器的,需要使用固定函数着色器(Fixed Function Shader)来完成渲染。(这些着色器可以完成一些非常简单的效果。)

++++一个简单的固定函数着色器示例:

//立钻哥哥:一个简单的固定函数着色器

Shader YanlzShaderDemo/FixedFunctionShader{

    Properties{

        _Color(Main Color, Color) = (1,0.5,0.5,1)

    }

 

    SubShader{

        Pass{

            Material{

                Diffuse[_Color]

            }

 

            Lighting On

        }

    }

}

++++固定函数着色器的代码被定义在Pass语义块中,这些代码相当于Pass中的一些渲染设置。

++++对于固定函数着色器来说,我们需要完全使用ShaderLab的语法(即使用ShaderLab的渲染设置命令)来编写,而非使用CG/HLSL

++++由于现在绝大多数GPU都支持可编程的渲染管线,这种固定管线的编程方式已经逐渐被抛弃。(实际上,在Unity5.2中,所有固定函数着色器都会在背后被Unity编译成对应的顶点/片元着色器,因此真正意义上的固定函数着色器已经不存在了。)

1.3.15Unity Shader不是真正的Shader

++++Unity Shader并不等同于Shader。(在Unity里,Unity Shader实际上指的就是一个ShaderLab文件(硬盘上以.shader作为文件后缀的一种文件。))

++++ShaderLab文件中,我们可以做更多的事情:

--在传统的Shader中,仅可以编写特定类型的Shader,例如顶点着色器、片元着色器等。而在ShaderLab中,我们可以在同一个文件里同时包含需要的顶点着色器和片元着色器代码。

--在传统的Shader中,我们无法设置一些渲染设置,例如是否开启混合、深度测试等,这些是开发者在另外的代码中自行设置的。而在ShaderLab中,我们通过一行特定的指令就可以完成这些设置。

--在传统的Shader中,我们需要编写冗长的代码来设置着色器的输入和输出,要小心地处理这些输入输出的位置对应关系等。而在ShaderLab中,我们只需要在特定语句块中声明一些属性,就可以依靠材质来方便地改变这些属性。(对于模型自带的数据(如顶点位置、纹理坐标、法线等),ShaderLab也提供了直接访问的方法,不需要开发者自行编码来传给着色器。)

++++ShaderLab的缺点:由于Unity Shader的高度封装性,我们可以编写的Shader类型和语法被限制了。(对于一些类型的Shader,例如曲面细分着色器(Tessellation Shader)、几何着色器(Geometry Shader)等,Unity的支持就相对差一些。)(一些高级的Shader语法ShaderLab也不支持。)

++++ShaderLab提供了一种让开发者同时控制渲染流水线中多个阶段的一种方式,不仅仅是提供Shader代码。(作为开发者而言,我们绝大部分时候只需要和ShaderLab打交道,而不需要关心渲染引擎底层的实现细节。)

++1.3.16Unity ShaderCG/HLSL之间的关系

++++Unity Shader使用ShaderLab语言编写的,但对于表面着色器和顶点/片元着色器,我们可以在ShaderLab内部嵌套CG/HLSL语言来编写这些着色器代码。(这些CG/HLSL代码是嵌套在CGPROGRAMENDCG之间的。)

++++CG代码片段是位于Pass语义块内部的:

Pass{

    //Pass的标签和状态设置

 

    CGPROGRAM

    //编译指令,例如:

    #pragma vertex vert

    #pragma fragment frag

 

    //CG代码

 

    ENDCG

 

    //其他一些设置

}

++++在表面着色器中,CG/HLSL代码是写在SubShader语义块内,但在背后,Unity还是会把它转化成一个包含多Pass的顶点/片元着色器。

++++从本质上来讲:Unity Shader只有两种形式:顶点/片元着色器和固定函数着色器。(Unity5.2以后的版本中,固定函数着色器也会在背后被转化成顶点/片元着色器,因此从本质上来说:Unity只存在顶点/片元着色器。)

++++在提供给编程人员这些便利的背后,Unity编译器会把这些CG片段编译成低级语言,如汇编语言等。(通常,Unity会自动把这些CG片段编译到所有相关平台上。)



###1.4Shader中的数学基础

++1.4Shader中的数学基础

++++“不懂数学者不得入内。”

++++计算机图形学之所以深奥难懂,很大原因在于它是建立在虚拟世界上的数学模型。(数学渗透到图形学的方方面面,当然也包括Shader。)

++++在学习Shader的过程中,我们最常使用的就是矢量和矩阵(即数学的线性代数)。

++1.4.1、笛卡尔坐标系

++++在游戏制作中,我们使用数学绝大部分都是为了计算位置、距离和角度等变量。(而这些计算大部分都是在笛卡尔坐标系(Cartesian Coordinate System)下进行的。)

++++传说,笛卡尔坐标系来源于笛卡尔对天花板上一只苍蝇的运动轨迹的观察。(笛卡尔发现,可以使用苍蝇距不同墙面的距离来描述它的当前位置。这个坐标平面后来逐渐发展形成了坐标系系统。)

++1.4.2、二维笛卡尔坐标系

++++一个二维的笛卡尔坐标系包含的信息:

--原点:它是整个坐标系中的中心。

--x轴和y轴:两条过原点的相互垂直的矢量。(这些坐标轴也被称为该坐标系的基矢量。)

++1.4.3、三维笛卡尔坐标系

++++在三维笛卡尔坐标系中,我们需要定义3个坐标轴和一个原点。

++++XYZ三个坐标轴被称为该坐标系的基矢量(basic vector

++++通常情况下,这3个坐标轴之间是相互垂直的,且长度为1,这样的基矢量被称为标准正交基(orthonormal basis

++++三维笛卡儿坐标系中的坐标轴方向也不是固定的,这种不同导致了两种不同种类的坐标系:左手坐标系(left-handed coordinate space右手坐标系(right-handed coordinate space

++1.4.4Unity使用的坐标系

++++Unity使用的左手坐标系。

++++对于观察空间来说,Unity使用的右手坐标系。(观察空间,通俗来讲就是以摄像机为原点的坐标系。在这个坐标系中,摄像机的前向是z轴的负方向,这与在模型空间和世界空间中的定义相反。)

++++观察空间中,z轴坐标的减少意味着场景深度的增加:


++1.4.5、点和矢量

++++点(Pointn维空间(游戏中主要使用二维和三维空间)中的一个位置,它没有大小、宽度这类概念。

++++矢量(vector,也称为向量)是指n维空间中一种包含了模(magnitude方向(direction有向线段(比如速度)。(标量只要模没有方向,比如距离)(矢量的模是一个标量,可以理解为矢量在空间中的长度。)

++++点是一个没有大小之分的空间中的位置,而矢量是一个有模和方向但没有位置的量。

++++矢量通常用于描述偏移量,而一个点点可以用于指定空间中的一个位置(即相对于原点的位置。)

++++单位矢量(unit vector指的是那些模为1的矢量。(单位矢量也被称为被归一化的矢量(normalized vector。)(对任何给定的非零矢量,把它转换成单位矢量的过程就被称为归一化(normalization

++++法线方向(法矢量)、光源方向等,这些矢量不一定是归一化后的矢量。(由于我们的计算往往要求矢量是单位矢量,因此在使用前应先对这些矢量进行归一化运算。)

++++矢量之间也可以进行乘法。矢量的乘法有两种:点积(dot product,也称内积(inner product))叉积(cross product,也被外积(outer product))

++1.4.6、矢量的运算

++++从几何意义上看,把一个矢量v和一个标量k相乘,意味着对矢量v进行一个大小为|K|的缩放。

++++从图形学中矢量通常用于描述位置偏移(简称位移)。(我们可以利用矢量的加法和减法来计算一点相对于另一点的位移。)

++++从几何意义上看,对于加法,可以把矢量a的头连接到矢量b的尾,然后画一条从a的尾到b的头的矢量,来得到ab相加后的矢量。(也就是说,如果我们从一个起点开始进行了一个位置偏移a,然后又进行一个位置偏移b,那么就等于进行了一个a+b的位置偏移。这被称为矢量加法的三角形定则(triangle rule。矢量的减法是类似的。)


++1.4.7、矢量的点积

++++点积(dot product,也称内积(inner product))

++++Unity Shader中,我们可以直接使用形如 dot(a, b)的代码来对两个矢量值进行点积的运算。

++++矢量的点积满足交换律:a.b = b.a

++++点积公式1a.b=(ax,ay,az).(bx,by,bz)=axbx+ayby+azbz

++++点积公式2a.b=|a||b|cosA

++++点积性质1:点积可以结合标量乘法:(ka).b=a.(kb)=k(a.b)

++++点积性质2:点积可结合矢量加法和减法:a.(b+c)=a.b+a.c

++++点积性质3:一个矢量和本身进行点积的结果,是该矢量的模的平方:v.v=vxvx+vyvy+vzvz=|v|^2

++++点积的几何意义很重要,因为点积几乎应用到图形学的各个方面。(其中一个几何意义就是投影(projection)(投影的值可能是负数。)

++++可以直接利用点积来求矢量的模,而不需要使用模的计算公式。(在只想要比较两个矢量的长度大小时,可以直接使用点积的结果。)

++++两个矢量的点积可以表示为两个矢量的模相乘,再乘以它们之间夹角的余弦值。(a.b=|a||b|cosA)。(两个矢量的方向不同会得到不同符号的投影值:当夹角小于90度时,cosA>0;当夹角等于90度时,cosA=0;当夹角大于90度时,cosA<0。)(利用arcos反余弦操作求得两个向量之间的夹角(在0~180度):A=arcos(a^.b^)

++++点积的符号可以让我们知道两个矢量的方向关系。(投影结果的正负号与a^b的方向有关:当它们的方向相反(夹角大于90度)时,结果小于0;当它们的方向相互垂直(夹角为90度)时,结果等于0;当它们的方向相同(夹角小于90度)时,结果大于0。)


++1.4.8、矢量的叉积

++++叉积(cross product,也被外积(outer product))

++++叉积与点积不同的是,矢量叉积的结果仍是一个矢量,而非标量。

++++两个矢量的叉积:aXb=(ax,ay,az)X(bx,by,bz)=(aybz-azby, azbx-axbz, axby-aybx)

 

++++叉积满足反交换律的:axb=-(bxa)

++++对两个矢量进行叉积的结果会得到一个同时垂直于这两个矢量的新矢量

++++叉积axb的长度等于ab的模的乘积再乘以它们之间夹角的正弦值:|axb|=|a||b|sinA

++++叉积最常见的应用就是:计算垂直于一个平面、三角形的矢量。(还可以用于判断三角面片的朝向。)

++1.4.9、矩阵(Matrix

++++矩阵就是由m*n个标量组成的长方形数组。(网格结构,意味着矩阵有行(row)和列(column))

++++矢量就是一个数组,矩阵也是一个数组。

++++矩阵和矢量类似,也可以和标量相乘,它的结果仍然是一个相同维度的矩阵。(它们之间的乘法非常简单,就是矩阵的每个元素和该标量相乘。)

++++两个矩阵的乘法结果会是一个新的矩阵

++++特殊的矩阵:方块矩阵(square matrix)、单位矩阵(identity matrix)、转置矩阵(transposed matrix)、逆矩阵(inverse matrix)、正交矩阵(orthogonal matrix)等。

++++方块矩阵(square matrix】:简称方阵,是指那些行和列数目相等的矩阵。(在三维渲染里,最常使用就是3*34*4的方阵。)

++++单位矩阵(identity matrix】:一个特殊的对角矩阵是单位矩阵(identity matrix)。(任何矩阵和它相乘的结果还是原来的矩阵。)

++++转置矩阵(transposed matrix】:实际是对原矩阵的一种运算,即转置运算。

++++逆矩阵(inverse matrix】:不是所有矩阵都有逆矩阵,第一个前提就是该矩阵必须是一个方阵。(矩阵最复杂的一种操作。)

++++正交矩阵(orthogonal matrix】:正交是矩阵的一种属性。如果一个方阵M和它的转置矩阵的乘积是单位矩阵的话,我们就说这个矩阵是正交的(orthogonal)。

++1.4.10、矩阵的几何意义:变换

++++在游戏的世界中,这些变换一般包括了旋转、缩放和平移。(给定一个点或矢量,再给定一个变换,就可以通过某个数学运算来求得新的点和矢量,可以使用矩阵来完美地解决这些问题。)

++++变换(transform,是指把一些数据,如点、方向矢量甚至是颜色等,通过某种方式进行转换的过程。(在计算机图形学领域,变换非常重要。)

++++线性变换(linear transform】:是指那些可以保留矢量加和标量乘的变换。(缩放(scale就是一种线性变换。)(旋转(rotation也是一种线性变换。)(对于线性变换来说,如果我们要对一个三维的矢量进行变换,那么使用3*3的矩阵就可以表示所有的线性变换。)

++++线性变换除了缩放和旋转外,还有错切(shear镜像(mirroring,也称reflection正交投影(orthographic projection等等。

++++平移变换(f(x)=x+(1,2,3)),这个变换就不是一个线性变换,它满足标量乘法,但不满足矢量加法。(平移变换不会对方向矢量产生任何影响,矢量没有位置属性。)(平移矩阵并不是一个正交矩阵。)

++++仿射变换(affine transform就是合并线性变换和平移变换的变换类型。(仿射变换可以使用一个4*4的矩阵来表示,把矢量扩展到思维空间下,这就是齐次坐标空间(homogeneous space

++++可以使用一个4*4的矩阵来表示平移、旋转和缩放。(把表示纯平移、纯旋转和纯缩放的变换矩阵叫做基础变换矩阵。)(平移矩阵并不是一个正交矩阵。)(缩放矩阵一般不是正交矩阵。)(旋转矩阵的逆矩阵是旋转相反角度得到的变换矩阵。旋转矩阵是正交矩阵,而且多个旋转矩阵之间的串联同样是正交的。)

++++在绝大多数情况下,约定变换的顺序:先缩放,再旋转,最后平移。(复合变换可以通过矩阵的串联来实现。把平移、旋转和缩放组合起来,形成一个复杂的变换过程。)

++++分别绕x轴、y轴、z轴旋转的变换矩阵,Unity的旋转顺序是:zxy顺序旋转

++1.4.11、齐次坐标

++++由于3*3矩阵不能表示平移操作,扩展到4*4的矩阵就可以实现对平移的表示。

++++把三维矢量转换成四维矢量,也就是齐次坐标(homogeneous coordinate,齐次坐标泛指四维齐次坐标(事实上齐次坐标的维度可以超过四维)。

++++齐次坐标是一个四维矢量。

++1.4.12、坐标空间

++++顶点着色器最基本的功能就是把模型的顶点坐标从模型空间转换到齐次裁剪坐标空间

++++渲染游戏的过程可以理解为:把一个个顶点经过层层处理最终转化到屏幕上的过程。

++++在编写Shader的过程中,很多看起来很难理解和复杂的数学运算都是为了在不同坐标空间之间转换点和矢量。(单一坐标系其实会很麻烦:比如用公司大门为原点的坐标系描述办公桌上的电脑。)

++++在渲染流水线中,需要把一个点或方向矢量从坐标空间转换到另一个坐标空间。(坐标空间,指明其原点位置和3个坐标轴的方向。)

++++坐标空间会形成一个层次结构:每个坐标空间都是另一个坐标空间的子空间,每个空间都有一个父(parent)坐标空间。(对坐标空间的变换实质上就是在父空间和子空间之间对点和矢量进行变换。)

++++Shader中,常常截取变换矩阵的前3行前3列对法线方向、光照方向等进行空间变换。(矢量是没有位置的,坐标空间的原点变换是可以忽略的,仅仅平移坐标系的原点是不会对矢量造成任何影响的。)(对矢量的坐标空间变换可以使用3*3的矩阵来表示。)

++++在渲染流水线中,一个顶点要经过多个坐标空间的变换才能最终被画在屏幕上。(一个顶点最开始是在模型空间中定义的,最后它将会变换到屏幕空间中,得到真正的屏幕像素坐标。)

++1.4.13、模型空间(model space

++++模型空间(model space是和某个模型或者说是对象有关的。(模型空间也称对象空间(object space局部空间(local space

++++每个模型都有自己独立的坐标空间,当它移动或旋转的时候,模型空间也会跟着它移动和旋转。(在模型空间中,经常使用的一些方向概念:前(forward)、后(back)、左(left)、右(right)、上(up)、下(down)等。)

++++Unity在模型空间中使用的是左手坐标系:+x轴(右)、+y轴(上)、+z轴(前)等。

++++模型空间的原点和坐标轴通常是由美术人员在建模软件里确定好的。当导入到Unity中后,我们可以在顶点着色器中访问到模型的顶点信息,其中包括了每个顶点的坐标。(这些坐标都是相对于模型空间中的原点(通常位于模型的重心)定义的。)

++++模型坐标空间中的位置可以通过访问顶点属性来得到。(一个位置是(0,3,4),由于顶点变换中往往包含了平移变换,因此需要把其扩展到齐次坐标系下,得到顶点坐标是(0,3,4,1))。

++1.4.14、世界空间(world space

++++世界空间(world space是一个特殊的坐标系,它建立了我们所关心的最大的空间。

++++世界空间可以被用于描述绝对位置。(绝对位置指的就是在世界坐标系中的位置。)

++++通常,我们会把世界空间的原点放置在游戏空间的中心。

++++Unity中,世界空间使用左手坐标系,但它的x轴,y轴,z轴是固定不变的。(在Unity中,我们可以通过调整Transform组件中的Position属性来改变模型的位置,这里的位置值相对于这个Transform的父节点(parent)的模型坐标空间中的原点定义的。)(如果一个Transform没有任何父节点,那么这个位置就是在世界坐标系中的位置。)

++++顶点变换的第一步:就是将顶点坐标从模型空间变换到世界空间中。(这个变换通常叫做模型变换(model transform

++1.4.15、观察空间(view space

++++观察空间(view space也被称为摄像机空间(camera space。(观察空间可以认为是模型空间的一个特例,在所有的模型中有一个非常特殊的模型,即摄像机(通常来说摄像机本事是不可见的),它的模型空间就是观察空间。)

++++摄像机决定了我们渲染游戏所使用的视角。(在观察空间中,摄像机位于原点,其坐标轴的选择可以是任意的,在Unity中观察空间的坐标轴:+x轴(右)、+y轴(上)、+z轴(摄像机后方))(Unity在模型空间和世界空间中选用的都是左手坐标系,而在观察空间中使用的是右手坐标系。)

++++OpenGL传统中,观察空间中摄像机的正前方指向的是-z轴方向。

++++Unity为我们做了很多渲染的底层工作,包括很多坐标空间的转换。(调用类似Camera.cameraToWorldMatrixCamera.worldToCameraMatrix等接口自行计算某模型在观察空间中的位置。)

++++提示:观察空间和屏幕空间是不同的,观察空间是一个三维空间,而屏幕空间是一个二维空间。(从观察空间到屏幕空间的转换需要一个投影(projection)操作。)

++++得到顶点在观察空间中的位置,有两种方法:

--第一种方法:是计算观察空间的3个坐标轴在世界空间下的表示,构建出从观察空间变换到世界空间的变换矩阵,再对该矩阵求逆来得到从世界空间变换到观察空间的变换矩阵。

--第二种方法:想象平移整个观察空间,让摄像机原点位于世界坐标的原点,坐标轴与世界空间中的坐标轴重合即可。

++1.4.16、裁剪空间

++++顶点要从观察空间转换到裁剪空间(clip space,也称齐次裁剪空间)中,这个用于变换的矩阵叫做裁剪矩阵(clip matrix,也称投影矩阵(projection matrix

++++裁剪空间的目标是能够方便地对渲染图元进行裁剪:完全位于这块空间内部的图元将会被保留,完全位于这块空间外部的图元将会被剔除,与这块空间边界相交的图元就会被裁剪。(这块空间是由视锥体(view frustum来决定的。)

++++视锥体指的是空间中的一块区域,这块区域决定了摄像机可以看到的空间。(视锥体由六个平面包围而成,这些平面也称裁剪平面(clip planes)。(视锥体有两种类型:正交投影(orthographic projection透视投影(perspective projection)(透视投影模拟了人眼看世界的方式,近大远小。)(正交投影完全保留了物体的距离和角度,在2D游戏或渲染小地图等其他HUD元素时,使用正交投影。)

++++在视锥体的6块裁剪平面中,有两块裁剪平面比较特殊:近裁剪平面(near clip plane远裁剪平面(far clip plane。(它们决定了摄像机可以看到的深度范围。)

 

++++投影矩阵有两个目的:

--投影矩阵目的1:为投影做准备。(虽然投影矩阵包含投影二字,但是它并没有进行真正的投影工作,而是在为投影做准备。)(真正的投影发生在后面的齐次除法(homogeneous division过程中。经过投影矩阵的变换后,顶点的w分量将会具有特殊的意义。)(投影可理解成一个空间的降维,而投影矩阵实际上并不会真的进行这个步骤,它会为真正的投影做准备工作。真正的投影会在屏幕映射时发生,通过齐次除法来得到二维坐标。)

--投影矩阵目的2:对xyz分量进行缩放。(直接使用视锥体的6个裁剪平面来进行裁剪会比较麻烦。而经过投影矩阵的缩放后,我们可以直接使用w分量作为一个范围值,如果xyz分量都位于这个范围内,就说明该顶点位于裁剪空间内。)

++++透视投影:视锥体的意义在于定义了场景中的一块三维空间。(所有位于这块空间内的物体将会被渲染,否则就会被剔除或裁剪。)

++++Unity中,裁剪空间是由Camera组件中的参数和Game视图的横纵比共同决定。(可以通过Camera组件的Field of ViewFOV)属性来改变视锥体竖直方向的张开角度,而Clipping Planes中的NearFar参数可以控制视锥体的近裁剪平面和远裁剪平面距离摄像机的远近。)


++++透视投影perspective projection的视锥体是一个金字塔形。(侧面的4个裁剪平面将会在摄像机处相交。)

++++正交投影orthographic projection的视锥体是一个长方体。

++1.4.17、屏幕空间(screen space

++++经过投影矩阵的变换后,可以进行裁剪操作了。(当完成了所有的裁剪工作后,就需要进行真正的投影了,需要把视锥体投影到屏幕空间(screen space中。)(经过这一步变换,会得到真正的像素位置,而不是虚拟的三维坐标。)

++++屏幕空间是一个二维空间,必须把顶点从裁剪空间投影到屏幕空间中,来生成对应的2D坐标,这个过程可以理解成两个步骤:齐次除法和映射到屏幕空间。

++++进行标准齐次除法(homogeneous division,也称透视除法(perspective division。(就是用齐次坐标系的w分量去除以xyz分量)。(在OpenGL中,这一步得到的坐标叫做归一化的设备坐标(Normalized Device Coordinates, NDC)(经过这一步,可以把坐标从齐次裁剪坐标空间转换到NDC中。)(经过透视投影变换后的裁剪空间,经过齐次除法后就变换到一个立方体内。Unity选择了OpenGL这样的齐次裁剪空间,这个立方体的xyz分量的范围都是[-1, 1]


++++Unity中,从裁剪空间到屏幕空间的转换是由Unity帮我们完成的。(顶点着色器只需要把顶点转换到裁剪空间即可。)

++++顶点着色器的最基本的任务就是把顶点坐标从模型空间转换到裁剪空间中

++++片元着色器中可以得到该片元在屏幕空间的像素位置

++1.4.18、渲染流水线中顶点的空间变换过程


++1.4.19、法线变换

++++一种特殊的变换:法线变换。

++++法线(normal,也称法矢量(normal vector

++++在游戏中,模型的一个顶点往往会携带额外的信息,而顶点法线就是其中一种信息。(当我们变换一个模型的时候,不仅需要变换它的顶点,还需要变换顶点法线,以便在后续处理(如片元着色器)中计算光照等。)

++++切线(tangent,也称切矢量(tangent vector。(与法线类似,切线往往也是模型顶点携带的一种信息。它通常与纹理空间对齐,而且与法线方向垂直。)

++1.4.20Unity Shader的内置变量

++++使用UnityShader的一个好处在于,它提供了很多内置的参数,这使得我们不再需要自己动手计算一些值。

++++Unity内置的变换矩阵:UNITY_MATRIX_MVPUNITY_MATRIX_MVUNITY_MATRIX_VUNITY_MATRIX_PUNITY_MATRIX_VPUNITY_MATRIX_T_MVUNITY_MATRIX_IT_MV_Object2World_World2Object等。

  --UNITY_MATRIX_MVP】:当前的模型.观察.投影矩阵,用于将顶点/方向矢量从模型空间变换到裁剪空间

  --UNITY_MATRIX_MV】:当前的模型.观察矩阵,用于将顶点/方向矢量从模型空间变换到观察空间

  --UNITY_MATRIX_V】:当前的观察矩阵,用于将顶点/方向矢量从世界空间变换到观察空间

  --UNITY_MATRIX_P】:当前的投影矩阵,用于将顶点/方向矢量从观察空间变换到裁剪空间

  --UNITY_MATRIX_VP】:当前的观察.投影矩阵,用于将顶点/方向矢量从世界空间变换到裁剪空间

  --UNITY_MATRIX_T_MV】:UNITY_MATRIX_MV的转置矩阵。

  --UNITY_MATRIX_IT_MV】:UNITY_MATRIX_MV的逆转置矩阵,用于将法线从模型空间变换到观察空间,也可用于得到UNITY_MATRIX_MV的逆矩阵。

  --_Object2World】:当前的模型矩阵,用于将顶点/方向矢量从模型空间变换到世界空间。

  --_World2Object】:_Object2World的逆矩阵,用于将顶点/方向矢量从世界空间变换到模型空间。

++++Unity内置的摄像机和屏幕参数:_WorldSpaceCameraPos_ProjectionParams_ScreenParams_ZBufferParamsunity_OrthoParamsunity_CameraProjectionunity_CameraInvProjectionunity_CameraWorldClipPlanes[6]等。

--_WorldSpaceCameraPos】:float3:该摄像机在世界空间中的位置。

--_ProjectionParams】:float4x=1.0(或-1.0,如果正在使用一个翻转的投影矩阵进行渲染),y=Nearz=Farw-1.0+1.0/Far,其中NearFar分别是近裁剪平面和远裁剪平面和摄像机的距离。

--_ScreenParams】:float4x=widthy=heightz=1.0+1.0/widthw=1.0+1.0/height,其中widthheight分别是该摄像机的渲染目标(render target)的像素宽度和高度。

--_ZBufferParams】:float4x=1-Far/Near, y=Far/Near, z=x/Far, w=y/Far,该变量用于线性化Z缓存中的深度值。

--unity_OrthoParams】:float4x=width, y-height, z没有定义,w=1.0(该摄像机是正交摄像机)或w=0.0(该摄像机是透视摄像机),其中widthheight是正交投影摄像机的宽度和高度。

--unity_CameraProjection】:float4x4:该摄像机的投影矩阵。

--unity_CameraInvProjection】:float4x4:该摄像机的投影矩阵的逆矩阵。

--unity_CameraWorldClipPlanes[6]】:float4:该摄像机的6个裁剪平面在世界空间下的等式,按如下顺序:左、右、下、上、近、远裁剪平面。

++1.4.21、使用3*3还是4*4的变换矩阵?

++++对于线性变换(例如旋转和缩放)来说,仅使用3*3的矩阵就足够表示所有的变换了。(如果存在平移变换,就需要使用4*4的矩阵。)

++++在对顶点的变换中,通常使用4*4的变换矩阵。(在变换前需要把点坐标转换成齐次坐标的表示,即把顶点的w分量设为1。)

++++在对方向矢量的变换中,通常使用3*3的矩阵就足够了。(平移变换对方向矢量是没有影响的。)

++1.4.22CG中的矢量和矩阵类型

++++Unity Shader中通常使用CG作为着色器编程语言。

++++CG中,矩阵类型是由float3x3float4x4等关键词进行声明和定义的。(对于float3float4等类型的变量,可以把它当成一个矢量,也可以把它当成是一个1*n的行矩阵或者一个n*1的列矩阵。)

++++CG使用的是行优先的方法,即是一行一行地填充矩阵的。(我们在CG中访问一个矩阵中的元素时,也是按行来索引的。)(提示:Unity在脚本中提供一种矩阵类型:Matrix4x4,脚本中这个矩阵类型时采用列优先的方式。这与Unity Shader中的规定不一样,需多加注意)

++++CG优先按行来索引一个矩阵中的元素:

//立钻哥哥:行行优先的方式初始化矩阵M

float3x3 M = float3x3(1.0,2.0,3.0,   4.0,5.0,6.0,  7.0,8.0,9.0);

float3 row = M[0];    //得到M的第一行:(1.0,2.0,3.0

float e1e = M[1][0];    //得到M的第2行第1列的元素:4.0

++1.4.23Unity中的屏幕坐标:ComputeScreenPos/VPOS/WPOS

++++在写Shader的过程中,我们有时候希望能够获得片元在屏幕上的像素位置。

++++在顶点/片元着色器中,有两种方式来获得片元的屏幕坐标:

--第一种:是在片元着色器的输入中声明VPOSWPOS语义。(VPOSHLSL中对屏幕坐标的语义,而WPOSCG中对屏幕坐标的语义。两者在Unity Shader中是等价的。)(我们可以在HLSL/CG中通过语义的方式来定义顶点/片元着色器的默认输入,而不需要自己定义输入输出的数据结构。)

//立钻哥哥:在片元着色器的输入中声明VPOSWPOS语义

fixed4 frag(float4 sp : VPOS) : SV_Target{

    //用屏幕坐标除以屏幕分辨率_ScreenParams.xy,得到空间中的坐标

    return fixed4(sp.xy/_ScreenParams.xy, 0.0, 1.0);

}

--第二种:是通过Unity提供的ComputeScreenPos函数。(这个函数在UnityCG.cginc里被定义。)(通常的用法:首先在顶点着色器中将ComputeScreenPos的结果保存在输出结构体中,然后在片元着色器中进行一次齐次除法运算后得到空间下的坐标。)

//立钻哥哥:通过Unity提供的ComputeScreenPos函数

struct vertOut{

    float4 pos : SV_POSITION;

    float4 scrPos : TEXCOORD0;

};

 

vertOut vert(appdata_base v){

    vertOut o;

    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

 

    //第一步:把ComputeScreenPos的结果保存到scrPos

    o.scrPos = ComputeScreenPos(o.pos);    

 

    return o;

}

 

fixed4 frag(vertOut i) : SV_Target{

    //第二步:用scrPos.xy除以scrPos.w得到空间中的坐标

    float2 wcoord = (i.scrPos.xy/i.scrPos.w);

    return fixed4(wcoord, 0.0, 1.0);

}




##2章:Shader初级篇

++2章:Shader初级篇

++++2.1Shader入门

++++2.2Unity中的基础光照

++++2.3、基础纹理

++++2.4、透明效果

++++2.5、更复杂的光照

++++2.6、高级纹理

++++2.7、让画面动起来

++++2.8、立钻哥哥带我们学Shader



###2.1Shader入门

++2.1Shader入门

++++在第1章(Shader基础篇)中,立钻哥哥带我们学习了渲染流水线,并给出了Unity Shader的基本概况,同时还打下了一定的数学基础。

++2.1.1、顶点/片元着色器的基本结构

++++顶点/片元着色器的结构与Unity Shader基本结构大体类似,包括了ShaderPropertiesSubShaderFallback等语义块:

//立钻哥哥:顶点/片元着色器的基本结构

Shader YanlzShaderDemoName{

    Properties{

        //属性

    }

 

    SubShader{

        //针对显卡ASubShader

        Pass{

            //设置渲染状态和标签

 

            CGPROGRAM    //开始CG代码片段

            #pragma vertex vert    //该代码片段的编译指令

            #pragma fragment frag

 

            //立钻哥哥:CG代码写在这里

 

            ENGCG

 

            //其他设置

        }

 

        //其他需要的Pass

     }

 

    SubShader{

        //针对显卡BSubShader

    }

 

    //立钻哥哥:上述SubShader都失败后用于回调的Unity Shader

    Fallback VertexLit

}

++1.2.2、剖析一个最简单的顶点/片元着色器代码

++++一个简单顶点/片元着色器示例:

//立钻哥哥:一个最简单的顶点/片元着色器示例

Shader YanlzShaderDemo/VertexFragmentDemo01{

    SubShader{

        Pass{

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

 

            float4 vert(float4 v : POSITION) : SV_POSITION{

                return mul(UNITY_MATRIX_MVP, v);

            }

 

            fixed4 frag() : SV_Target{

                return fixed4(1.0, 1.0, 1.0, 1.0);

            }

 

            ENGCG

        }

    }

}

++++Properties属性语义并不是必需的。(我们可以选择不声明任何材质属性。)

++++没有进行任何渲染设置和标签设置时,SubShader将使用默认的渲染设置和标签设置。

++++#pragma vertex vert  :告诉Unity哪个函数包含了顶点着色器代码。

++++#pragma fragment frag  :告诉Unity哪个函数包含了片元着色器代码。

++++立钻哥哥带我们剖析vert函数:

float4 vert(float4 v : POSITION) : SV_POSITION{

    return mul(UNITY_MATRIX_MVP, v);    //把顶点坐标从模型空间转换到裁剪空间

}

--函数的输入v包含了这个顶点的位置,这是通过POSITION语义指定的。

--函数的返回值是一个float4类型的变量,它是该顶点在裁剪空间中的位置。

--POSITIONSV_POSITION都是CG/HLSL中的语义(semantics),它们是不可省略的,这些语义将告诉系统用户需要哪些输入值,以及用户的输出是什么。(POSITION告诉Unity,把模型的顶点坐标填充到输入参数v中。)(SV_POSITION告诉Unity,顶点着色器的输出是裁剪空间中的顶点坐标。)

--UNITY_MATRIX_MVP矩阵是Unity内置的模型.观察.投影矩阵

++++立钻哥哥带我们剖析frag函数:

fixed4 frag() : SV_Target{

    return fixed4(1.0, 1.0, 1.0, 1.0);    //返回一个表示白色的fixed4类型的变量

}

--frag函数没有任何输入。

--函数的输出是一个fixed4类型的变量,并且使用了SV_Target语义进行限定。(SV_Target也是HLSL中的一个系统语义,它等同于告诉渲染器,把用户的输出颜色存储到一个渲染目标(render target)中,这里将输出到默认的帧缓存中。)

--片元着色器输出的颜色的每个分量范围在[0,1],其中(0,0,0)表示黑色,而(1,1,1)表示白色。

++1.2.3、模型数据从哪里来

++++在顶点着色器中使用POSITION语义得到模型的顶点位置。

++++想要得到更多模型数据,比如每个顶点的纹理坐标和法线方向等(使用纹理坐标访问纹理,使用法线计算光照),那就需要为顶点着色器定义个新的输入参数,这个参数是一个结构体。

Shader YanlzShaderDemo/VertexFragmentShade02{

    SubShader{

        Pass{

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

 

            //使用一个结构体来定义顶点着色器的输入

            struct a2v{

                //POSITION语义告诉Unity,用模型空间的顶点坐标填充vertex变量

                float4 vertex : POSITION;

 

                //NORMAL语义告诉Unity,用模型空间的法线方向填充normal变量

                float3 normal : NORMAL;

 

                //TEXCOORD0语义告诉Unity,用模型的第一套纹理坐标填充texcoord变量

                float4 texcoord : TEXCOORD0;

            };

 

            float4 vert(a2v v) : SV_POSITION{

                //使用v.vertex来访问模型空间的顶点坐标

                return mul(UNITY_MATRIX_MVP, v.vertex);

            }

 

            fixed4 frag() : SV_Target{

                return fixed4(1.0, 1.0, 1.0, 1.0);

            }

 

            ENDCG

        }

    }

}

++++在上面代码中,我们声明了一个新的结构体a2v,它包含了顶点着色器需要的模型数据。(在a2v的定义中,我们用到了更多Unity支持的语义,如NORMALTEXCOORD0,当它们作为顶点着色器的输入时都是有特定含义的,Unity会根据这些语义来填充这个结构体。)

++++vert函数的输入参数类型是一个新定义的结构体a2v,通过这个自定义结构体的方式,可以在顶点着色器中访问模型数据。(a表示应用(application),v表示顶点着色器(vertex shader),a2v就是把数据从应用阶段传递到顶点着色器中。)

++++顶点着色器的输出,Unity支持的语义有:POSITIONTANGENTNORMALTEXCOORD0TEXCOORD1TEXCOORD2TEXCOORD3COLOR等。

++++Unity中,POSITIONTANGENTNORMAL等这些语义中的数据是由使用该材质的Mesh Render组件提供的。(在每帧调用Draw Call的时候,Mesh Render组件会把它负责渲染的模型数据发送给Unity Shader。一个模型通常包含了一组三角面片,每个三角面片由3个顶点构成,而每个顶点又包含了一些数据,例如顶点位置、法线、切线、纹理坐标、顶点颜色等。通过语义方法可以在顶点着色器中访问顶点的这些模型数据。)

++++创建自定义的结构体,格式如下:

struct StructName{

    Type Name : Semantic;

    Type Name : Semantic;

    ....

}

++1.2.4、顶点着色器和片元着色器之间如何通信

++++在实践中,从顶点着色器输出一些数据,例如把模型的法线、纹理坐标等传递给片元着色器,这就涉及顶点着色器和片元着色器之间的通信。

++++再定义一个新的结构体,代码:

Shader YanlzShaderDemo/VertexFragmentShader03{

    SubShader{

        Pass{

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float4 texcoord : TEXCOORD0;

            };

 

            //使用一个结构体来定义顶点着色器的输出

            struct v2f{

                //SV_POSITION语义告诉Unitypos里包含了顶点在裁剪空间中的位置信息

                float4 pos : SV_POSITION;

 

                //COLOR0语义可以用于存储颜色信息

                fixed3 color : COLOR0;

            };

 

            v2f vert(a2v v) : SV_POSITION{

                v2f o;    //声明输出结构

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

 

                //v.normal包含了顶点的法线方向,其分量范围在[-1.0, 1.0]

                //下面的代码把分量范围映射到了[0.0, 1.0],存储到o.color中传递给片元着色器

                o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                return fixed4(i.color, 1.0);    //将插值后的i.color显示到屏幕上

            }

 

            ENDCG

        }

    }

}

++++在上面的代码中,声明了一个新的结构体v2f。(v2f用于在顶点着色器和片元着色器之间传递信息。)(v2f也需要指定每个变量的语义。例如SV_POSITIONCOLOR0等语义)

++++顶点着色器的输出结构中,必须包含一个变量,它的语义是SV_POSITION。(否则,渲染器将无法得到裁剪空间中的顶点坐标,也就无法把顶点渲染到屏幕上。)

++++COLOR0语义中的数据可以由用户自行定义,但一般都是存储颜色,例如逐顶点的漫反射颜色或逐顶点的高光反射颜色。

++++完成了顶点着色器和片元着色器之间的通信,顶点着色器是逐顶点调用的,而片元着色器是逐片元调用的。(片元着色器中的输入实际上是把顶点着色器的输出进行插值后得到的结果。)

++1.2.5、如何使用属性

++++材质和Unity Shader之间有着紧密联系。(材质提供给我们一个可以方便地调节Unity Shader中参数的方式,通过这些参数,我们可以随时调整材质的效果。这些参数就写在Properties语义块中。)

++++一个小需求:在材质面板显示一个颜色拾取器,从而可以直接控制模型在屏幕上显示的颜色:

Shader YanlzShaderDemo/VertexFragmentShader04{

    Properties{

        //声明一个Color类型的属性

        _Color (Color Tint, Color) = (1.0, 1.0, 1.0, 1.0)

    }

 

    SubShader{

        Pass{

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

 

            //定义一个与属性名称和类型都匹配的变量

            fixed4 _Color;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float4 texcoord : TEXCOORD0;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                fixed3 color : COLOR0;

            };

 

            v2f vert(a2v v) : SV_POSITION{

                v2f o;

                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                fixed3 c = i.color;

                c *= _Color.rgb;    //使用_Color属性来控制输出颜色

                return fixed4(c, 1.0);

            }

 

            ENDCG

        }

    }

}

++++在上面的代码中,我们添加了Properties语义块,并声明了一个属性_Color,它的类型是Color,初始值是(1.0, 1.0, 1.0, 1.0),对应白色。

++++CG代码中需要提前定义一个新变量,这个变量的名称和类型必须与Properties语义块中的属性定义相匹配,就可以访问它了。

++1.2.6ShaderLab属性类型和CG变量类型的匹配关系

++++ShaderLab属性类型】VSCG变量类型】

++++ColorVectorVSfloat4half4fixed4

++++RangeFloatVSfloathalffixed

++++2DVSsampler2D

++++CubeVSsamplerCube

++++3DVSsampler3D

++++uniform】关键词是CG中修饰变量和参数的一种修饰词,它仅仅用于提供一些关于该变量的初始值是如何指定和存储的相关信息。(在Unity Shader中,uniform关键词是可以省略的。)(这和其他一些图像编程接口中的uniform关键词的作用不太一样。)

++++uniform fixed4 _Color;

++1.2.7Unity提供的内置文件和变量

++++顶点/片元着色的复杂之处在于,很多事情都需要自己转换法线方向,自己处理光照、阴影等。Unity提供了很多的内置文件,这些文件包含了很多提前定义的函数、变量和宏等。

++++内置的包含文件(include file),是类似于C++中头文件的一种文件。(在Unity中,它们的文件后缀是.cginc。)

++++在编写Shader时,我们可以使用#include指令把这些文件包含进来:

CGPROGRAM

//...

#include UnityCG.cginc

//...

ENDCG

++++可以从官网下载这些文件:【官网】=>【下载】=>【内置着色器】:

 

--CGIncludes】文件夹中包含了所有的内置包含文件;

--DefaultResources】文件夹中包含了一些内置组件或功能所需要的Unity Shader,例如一些GUI元素使用的Shader

--DefaultResourcesExtra】:包含了所有Unity中内置的Unity Shader

--Editor】文件夹目前只包含一个脚本文件,它用于定义Unity5引入的Standard Shader所用的材质面板。

--这些文件都是非常好的参考资料,在我们想要学习内置着色器的实现或是寻找内置函数的实现时,都可以在这里找到内部实现。

--Windows上的位置是:Unity的安装路径/Data/CGIncludes

++++Unity中一些常用的包含文件:UnityCG.cgincUnityShaderVariables.cgincLighting.cgincHLSLSupport.cginc等:

--UnityGC.cginc】:包含了最常使用的帮助函数、宏和结构体等;

--UnityShaderVariables.cginc】:在编译Unity Shader时,会被自动包含进来。包含了许多内置的全局变量,如UNITY_MATRIX_MVP等;

--Lighting.cginc】:包含了各种内置的光照模型,如果编写的是Surface Shader的话,会自动包含进来;

--HLSLSupport.cginc】:在编译Unity Shader时,会被自动包含进来。声明了很多用于跨平台编译的宏和定义。

++++Unity5引入了许多新的重要的包含文件,如UnityStandardBRDF.cgincUnityStandardCore.cginc等,这些包含文件用于实现基于物理的渲染。

++++UnityCG.cginc等文件可以帮助我们提供代码的复用率。(UnityCG.cginc还包含了很多宏。)

++++Unity还提供了用于访问时间、光照、雾效和环境光等目的的变量,这些内置变量大多位于UnityShaderVariables.cginc中。(与光照有关的内置变量还会位于Lighting.cgincAutoLight.cginc等文件中。)

++1.2.8UnityCG.cginc中一些常用的结构体

++++UnityCG.cginc是最常用的一个包含文件。(会经常使用该文件提供的结构体和函数,例如可以直接使用UnityCG.cginc中预定义的结构体作为顶点着色器的输入和输出。)

++++appdata_base】:可用于顶点着色器的输入:包含顶点位置、顶点法线、第一组纹理坐标;

++++appdata_tan】:可用于顶点着色器的输入:包含顶点位置、顶点切线、顶点法线、第一组纹理坐标;

++++appdata_full】:可用于顶点着色器的输入:包含顶点位置、顶点切线、顶点法线、四组(或更多)纹理坐标;

++++appdata_img】:可用于顶点着色器的输入:包含顶点位置、第一组纹理坐标;

++++v2f_img】:可用于顶点着色器的输出:裁剪空间中的位置、纹理坐标;

++++强烈建议找到UnityCG.cginc文件并查看相关结构体的声明,可以帮助我们更好理解Unity中一些内置变量的工作原理:

//立钻哥哥带我们剖析UnityCG.cginc

//...\Unityxxx\Editor\Data\CGIncludes\UnityCG.cginc

#ifndef UNITY_CG_INCLUDED

#define UNITY_CG_INCLUDED

 

#define UNITY_PI 3.14159265359f

....

 

#include UnityShaderVariables.cginc

#include UnityInstancing.cginc

 

....

 

struct appdata_base{

    float4 vertex : POSITION;

    float3 normal : NORMAL;

    float4 texcoord : TEXCOORD0;

    UNITY_VERTEX_INPUT_INSTANCE_ID

};

 

struct appdata_tan{

    float4 vertex : POSITION;

    float4 tangent : TANGENT;

    float3 normal : NORMAL;

    float4 texcoord : TEXCOORD0;

    UNITY_VERTEX_INPUT_INSTANCE_ID

};

 

struct appdata_full{

    float4 vertex : POSITION;

    float4 tangent : TANGENT;

    float3 normal : NORMAL;

    float4 texcoord : TEXCOORD0;

    float4 texcoord1 : TEXCOORD1;

    float4 texcoord2 : TEXCOORD2;

    float4 texcoord3 : TEXCOORD3;

    fixed4 color : COLOR;

    UNITY_VERTEX_INPUT_INSTANCE_ID

};

 

...

 

struct v2f_vertex_lit{

    float2 uv : TEXCOORD0;

    fixed4 diff : COLOR0;

    fixed4 spec : COLOR1;

};

 

...

 

struct appdata_img{

    float4 vertex : POSITION;

    half2 texcoord : TEXCOORD0;

};

 

struct v2f_img{

    float4 pos : SV_POSITION;

    half2 uv : TEXCOORD0;

}

 

....

 

#endif  //立钻哥哥:UNITY_CG_INCLUDED

++1.2.9UnityCG.cginc中一些常用的帮助函数

++++UnityCG.cginc是最常用的一个包含文件。(会经常使用该文件提供的结构体和函数,例如可以直接使用UnityCG.cginc中预定义的结构体作为顶点着色器的输入和输出。)

++++float3 WorldSpaceViewDir(float4 v)】:输入一个模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向;

++++float3 ObjSpaceViewDir(float4 v)】:输入一个模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向;

++++float3 WorldSpaceLightDir(float4 v)】:仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。没有被归一化。

++++float3 ObjSpaceLightDir(float4 v)】:仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。没有被归一化。

++++float3 UnityObjectToWorldNormal(float3 norm)】:把法线方向从模型空间转换到世界空间中;

++++float3 UnityObjectToWorldDir(in float3 dir)】:把方向矢量从模型空间变换到世界空间中;

++++float3 UnityWorldToObjectDir(float3 dir)】:把方向矢量从世界空间变换到模型空间中;

++++强烈建议找到UnityCG.cginc文件并查看相关函数的定义,可以帮助我们更好理解Unity中这些函数(一些函数我们完全是可以自己实现的,比如UnityObjectToWorldDirUnityWorldToObjectDir,这两个函数实际上就是对方向矢量进行了一次坐标空间变换):

//立钻哥哥带我们剖析UnityCG.cginc

//...\Unityxxx\Editor\Data\CGIncludes\UnityCG.cginc

#ifndef UNITY_CG_INCLUDED

#define UNITY_CG_INCLUDED

 

#define UNITY_PI 3.14159265359f

....

 

#include UnityShaderVariables.cginc

#include UnityInstancing.cginc

 

....

 

inline bool IsGammaSpace(){

    #ifdef UNITY_COLORSPACE_GAMMA

        return true;

    #else

        return false;

    #endif

}

 

....

 

//Computes world space view direction, from object space position

inline float3 WorldSpaceViewDir(in float4 localPos){

    float3 worldPos = mul(unity_ObjectToWorld, localPos).xyz;

    return UnityWorldSpaceViewDir(worldPos);

}

 

//Computes object space view direction

inline float3 ObjSpaceViewDir(in float4 v){

    float3 objSpaceCameraPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz, 1)).xyz;

    return objSpaceCameraPos - v.xyz;

}

 

....

 

//Computes world space light direction, from object space position

inline float3 WorldSpaceLightDir(in float4 localPos){

    float3 worldPos = mul(unity_ObjectToWorld, localPos).xyz;

    return UnityWorldSpaceLightDir(worldPos);

}

 

//Computes object space light direction

inline float3 ObjSpaceLightDir(in float4 v){

    float3 objSpaceLightPos = mul(unity_WorldToObject, _WorldSpaceLightPos0).xyz;

    #ifndef USING_LIGHT_MULTI_COMPILE

        return objSpaceLightPos.xyz - v.xyz * _WorldSpaceLightPos0.w;

    #else

        #ifndef USING_DIRECTIONAL_LIGHT

            return objSpaceLightPos.xyz - v.xyz;

        #else

            return objSpaceLightPos.xyz;

        #endif

    #endif

}

 

....

 

//Transforms normal from object to world space

inline float3 UnityObjectToWorldNormal(in float3 norm){

    #ifdef UNITY_ASSUME_UNIFORM_SCALING

        return UnityObjectToWorldDir(norm);

    #else

        //mul(IT_M, norm)=>mul(norm, I_M)=>{dot(norm, I_M.col0), dot(norm, I_M.col1), dot(norm, I_M.col2)}

        return normalize(mul(norm, (float3x3)unity_WorldToObject));

    #endif

}

 

....

 

//Transforms direction from object to world space

inline float3 UnityObjectToWorldDir(in float3 dir){

    return normalize(mul((float3x3)unity_ObjectToWorld, dir));

}

 

//Transforms direction from world to object space

inline float3 UnityWorldToObjectDir(in float3 dir){

    return normalize(mul((float3x3)unity_WorldToObject, dir));

}

 

....

 

#endif    //立钻哥哥:UNITY_CG_INCLUDED

++1.2.10Unity提供的CG/HLSL语义

++++Shader中经常可以看到:在顶点着色器和片元着色器的输入输出变量后还有一个冒号以及一个全部大写的名称,例如SV_POSITIONPOSITIONCOLOR0等,这些是CG/HLSL提供的语义(semantics

++++语义实际上就是一个赋给Shader输入和输出的字符串,这个字符串表达了这个参数的含义。(这些语义可以让Shader知道从哪里读取数据,并把数据输出到哪里,它们在CG/HLSLShader流水线中是不可或缺的。)(注意:Unity并没有支持所有的语义。)

++++一种新的语义类型:系统数值语义(system-value semantics,这类语义是以SV开头的。(SV代表的含义就是系统数值(system-value

++++从应用阶段传递模型数据给顶点着色器时Unity支持的常用语义:POSITIONNORMALTANGENTTEXCOORDnCOLOR等:

--POSITION】:模型空间中的顶点位置,通常是float4类型;

--NORAML】:顶点法线,通常是float3类型;

--TANGENT】:顶点切线,通常是float4类型;

--TEXCOORDn,如TEXCOORD0TEXCOORD1】:该顶点的纹理坐标,TEXCOORD0表示第一组纹理坐标,依次类推。通常是float3float4类型;(通常情况下,一个模型的纹理坐标组数一般不超过2,即TEXCOORD0TEXCOORD1。在Unity内置的数据结构体appdata_full中,它最多使用了6个坐标纹理组。)

--COLOR】:顶点颜色,通常是fixed4或者float4类型;

++++从顶点着色器传递数据给片元着色器时Unity使用的常用语义:SV_POSITIONCOLOR0COLOR1TEXCOORD0~TEXCOORD7等:

--SV_POSITION】:裁剪空间中的顶点坐标,结构体中必须包含一个用该语义修饰的变量。等同于DirectX9中的POSITION,但最好使用SV_POSITION

--COLOR0】:通常用于输出第一组顶点颜色,但不是必需的;

--COLOR1】:通常用于输出第二组顶点颜色,但不是必需的;

--TEXCOORD0~TEXCOORD7】:通常用于输出纹理坐标,但不是必需的;

--上面的语义中,除了SV_POSITION是由特别含义外,其他语义对变量的含义没有明确要求,也就是说,我们可以存储任意值到这些语义描述的变量中。(通常,如果我们需要把一些自定义的数据从顶点着色器传递给片元着色器,一般选用TEXCOORD0等)

++++片元着色器输出时Unity支持的常用语义:SV_Target

--SV_Target】:输出值将会存储到渲染目标(render target)中。(等同于DirectX9中的COLOR语义,但最好使用SV_Target

++1.2.11、定义复杂的变量类型

++++语义绝大部分用于描述标量或矢量类型的变量,例如fixed2floatfloat4fixed4等。

++++也可以使用语义来修饰不同类型变量:

struct v2f{

    float4 pos : SV_POSITION;

    fixed3 color0 : COLOR0;

    fixed4 color1 : COLOR1;

    half value0 : TEXCOORD0;

    float2 value2 : TEXCOORD1;

};

++++注意:一个语义可以使用的寄存器只能处理4个浮点值(float)。因此,如果我们想要定义矩阵类型,如float3X4float4X4等变量就需要使用更多的空间。(一种方法是:把这些变量拆分成多个变量,例如对于float4X4的矩阵类型,我们可以拆分成4float4类型的变量,每个变量存储了矩阵中的一行数据。)

++1.2.12Unity Shader调试工具

++++调试(debug)是所有程序员的噩梦,对一个Shader进行调试更是噩梦中的噩梦。

++++使用假彩色图像】:假彩色图像(false-color image指的是用假彩色技术生成的一种图像。(与假彩色图像对应的是照片这种真彩色图像(true-color image)(一张假彩色图像可以用于可视化一些数据。)(主要思想是:我们可以把需要调试的变量映射到[0,1]之间,把它们作为颜色输出到屏幕上,然后通过屏幕上显示的像素颜色来判断这个值是否正确。这种方法得到的调试信息很模糊,能够得到的信息很有限,但在很长一段时间内,这种方法的确是唯一的可选方法。)

++++利用神器:Visual Studio】:Visual Studio作为Windows系统下的开发利器,也提供了对Unity Shader的调试功能:Graphics Debugger。(通过Graphics Debugger可以查看每个像素的最终颜色、位置等信息,还可以对顶点着色器和片元着色器进行单步调试。)

++++最新利器:帧调试器】:Unity5除了带来全新的UI系统外,还给我们带来了一个新的针对渲染的调试器:帧调试器(Frame Debugger(【Window=>Frame Debugger】)。(与其他调试工具的复杂性相比,Unity原生的帧调试器非常简单快捷。我们可以使用它来看到游戏图像的某一帧是如何一步步渲染出来的。)(帧调试器可以用于查看渲染该帧时进行的各种渲染事件(event),这些事件包含了Draw Call序列,也包括了类似清空帧缓存等操作。)(Unity5提供的帧调试器实际上并没有实现一个真正的帧拾取(frame capture)的功能,而是仅仅使用停止渲染的方法来查看渲染事件的结果。)

++1.2.13、渲染平台的差异

++++Unity的优点之一是其强大的跨平台性:写一份代码可以运行在很多平台上。(绝大多数情况下,Unity为我们隐藏了这些细节,但有些时候我们需要自己处理它们。)

++++渲染纹理的坐标差异】:在OpenGL中,(0,0)点对应了屏幕的左下角;在DirectX中,(0,0)点对应了左上角。(如果不采取任何措施的话,就会出现纹理翻转的情况。)(我们不仅可以把渲染结果输出到屏幕上,还可以输出到不同的渲染目标(Render Target)中。这时,我们需要使用渲染纹理(Render Texture)来保存这些渲染结果。)

++++Shader的语法差异】:DirectX9/11Shader的语义更加严格。

--报错1incorrect number of arguments to numeric-type constructor(compiling for d3dll)

    ----float4 v = float4(0.0);    //OpenGL平台上,此代码是合法的

    ----float4 v = float4(0.0, 0.0, 0.0, 0.0);   //提供和变量类型相匹配的参数数目

--报错2output parameter o not completely initialized(compiling for d3dll)

    ----表面着色器的顶点函数有一个使用了out修饰符的参数,没有对这个参数的所有成员变量进行初始化。

    ----使用类似下面代码对这些参数进行初始化:

        void vert(inout appdata_full v, out Input o){

            //使用Unity内置的UNITY_INITIALIZE_OUTPUT宏对初始化结构体o

            UNITY_INITIALIZE_OUTPUT(Input, o);

            //....

        }

++++Shader的语义差异】:例如SV_POSITIONPOSITION在某些平台下是等价的,但在另一些平台上这些语义是不等价的。为了让Shader能够在所有平台上正常工作,我们应该尽可能使用下面的语义来描述Shader的输入输出变量:

--使用SV_POSITION来描述顶点着色器输出的顶点位置。(一些Shader使用了POSITION语义,但这些Shader无法在索尼PS4平台上或使用了细分着色器的情况下正常工作。)

--使用SV_Target来描述片元着色器的输出颜色。(一些Shader使用了COLOR或者COLOR0语义,同样的,这些Shader无法在索尼PS4上正常工作。)

++1.2.14Shader代码规范

++++写出规范的代码让代码变得漂亮易懂,养成良好习惯有助于写出高效的代码。

++++floathalf还是fixed】:在CG/HLSL中,有3种精度的数据类型:float, halffixed

--float:最高精度的浮点值。通常使用32位来存储。

--half:中等精度的浮点值。通常使用16位来存储,精确范围是:-60000~+60000

--fixed:最低精度的浮点值。通常使用11位来存储,精确范围是:-2.0~+2.0

--大多数现代的桌面GPU会把所有计算按最高的浮点精度进行计算。(也就是说,floathalffixed在这些平台上实际是等价的。)

--在移动平台GPU上,会有不同的精度范围,不同精度的浮点值的运算速度会有差异。

--fixed精度实际上只在一些较旧的移动平台上有用,大多数现代的GPU上,它们内部把fixedhalf当成同等精度来对待。

--建议:尽可能使用精度较低的类型,因为这可以优化Shader的性能,这一点在移动平台上尤其重要。(可以使用fixed类型来存储颜色和单位矢量,如果要存储更大范围的数据可以选择half类型,最差情况下再选择使用float。)

++++规范语法】:DirectX平台对Shader的语义有更加严格的要求,例如,使用和变量类型相匹配的参数数目来对变量进行初始化。

++++避免不必要的计算】:如果我们毫无节制地在Shader(尤其是片元着色器)中进行大量计算,那么很快就会收到Unity的错误提示:

--错误1temporary register limit of 8 exceeded

--错误2Arithmetic instruction limit of 64 exceeded; 65 arithmetic instructions needed to compile program

--出现这些错误信息大多是因为我们在Shader中进行了过多的运算,使得需要的临时寄存器数目或指令数目超过了当前可支持的数目。

++++慎用分支和循环语句】:最开始,GPU是不支持在顶点着色器和片元着色器中使用流程控制语句的。随着GPU的发展,可以使用if-elseforwhile这种流程控制指令了。(分支语句在GPU上的实现和CPU上有很大不同:GPU使用了不同于CPU的技术来实现分支语句,在最坏的情况下,花在一个分支语句的时间相当于运行所有分支语句的时间。因此,不鼓励在Shader中使用流程控制语句,因为它们会降低GPU的并行处理操作(尽管在现代的GPU上已经有了改进))(如果我们在Shader中使用了大量的流程控制语句,那么这个Shader的性能可能会成倍下降。我们应该尽量把计算向流水线上端移动,例如把放在片元着色器中的计算放到顶点着色器中,或者直接在CPU中进行预计算,再把结果传递给Shader。)(在不可避免使用分支语句进行计算时:第一,分支判断语句中使用的条件变量最好是常数,即在Shader运行过程中不会发生变化;第二,每个分支包含的操作指令数尽可能少;第三,分支的嵌套层数尽可能少。)

++++不要除以0】:在某些渲染平台上,除以0的代码不会造成Shader的崩溃,但得到的结果也是不确定的;在另一些平台上可能会直接崩溃。(对那些除数可能为0的情况,强制截取到非0范围。)

++1.2.15Shader Model

++++Shader Model是由微软提出的一套规范,Shader Model决定了Shader中各个特性(feature)的能力(capability)。这些特性和能力体现在Shader能使用的运算指令数目、寄存器个数等各个方面。(Shader Model等级越高,Shader的能力就越大。)

++++虽然更高等级的Shader Target可以让我们使用更多的临时寄存器和运算指令,但一个更好的方法是尽可能减少Shader中的运算,或者通过预计算的方式来提供更多的数据。

++++Unity支持的Shader Target

--#pragma target 2.0】:默认的Shader Target等级。相当于Direct3D9上的Shader Model 2.0

--#pragma target 3.0】:相当于Direct3D9上的Shader Model 3.0

--#pragma target 4.0】:相当于Direct3D10上的Shader Model4.0,目前只在DirectX11XboxOne/PS4平台上提供了支持;

--#pragma target 5.0】:相当于Direct3D11上的Shader Model5.0,目前只在DirectX11XboxOne/PS4平台上提供了支持;




###2.2Unity中的基础光照

++2.2Unity中的基础光照

++++渲染包含了两大部分:决定一个像素的可见性,决定这个像素上的光照计算。(光照模型就是用于决定在一个像素上进行怎样的光照计算。)

++++红色物体实际上是因为这个物体会反射更多的红光波长,而吸收了其他波长。(黑色物体实际上是因为它吸收了绝大部分的波长。)

++++光源:在实时渲染中,我们通常把光源当成一个没有体积的点。(在光学里,使用辐照度(irradiance来量化光。)

++++吸收和散射:光线由光源发射出来后,会与物体相交的结果:散射(scattering吸收(absorption等。(散射只改变光线的方向,但不改变光线的密度和颜色。)(吸收只改变光线的密度和颜色,不改变光线的方向。)(光线在物体表面经过散射后,有两种方向:一种将会散射到物体内部,这种现象称为折射(refraction透射(transmission;另一种将会散射到外部,这种现象称为反射(reflection。)

++++在光照模型中使用不同的部分来计算散射方向:高光反射(specular部分表示物体表面是如何反射光线的,而漫反射(diffuse部分则表示有多少光线会被折射、吸收和散射出表面。

++++根据入射光线的数量和方向,可以计算出射光线的数量和方向,通常使用出射度(exitance来描述它。(辐照度和出射度之间是满足线性关系的,而它们之间的比值就是材质的漫反射和高光反射属性。)

++2.2.1、着色(shading

++++着色(shading指的是,根据材质属性(如漫反射属性等)、光源信息(如光源方向、辐照度等),使用一个等式去计算沿某个观察方向的出射度的过程。

++++我们把这个等式称为光照模型(Lighting Model。不同的光照模型有不同的目的。(例如,一些用于描述粗糙的物体表面,一些用于描述金属表面等。)

++2.2.2BRDF光照模型

++++BRDFBidirectional Reflectance Distribution Function双向反射分布函数。(它用来定义给定入射方向上的辐射照度(irradiance)如何影响给定出射方向上的辐射率(radiance)。更笼统地说,它描述了入射光线经过某个表面反射后如何在各个出射方向上分布:这可以是从理想镜面反射到漫反射、各向同性(isotropic)或者各向异性(anisotropic)的各种反射。)

++++当给定模型表面上的一个点时,BRDF包含了对该点外观的完整的描述。

++++在图形学中,BRDF大多使用一个数学公式来表示,并且提供了一些参数来调整材质属性。(通俗来说,当给定入射光线的方向盒辐照度后,BRDF可以给出在某个出射方向上的光照能量分布。)

++2.2.3、标准光照模型

++++标准光照模型只关心直接光照(direct light),也就是那些直接从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入射线机的光线。

++++标准光照模型的基本方法,把进入到摄像机内的光线分为4个部分,每个部分使用一种方法来计算它的贡献度。这4个部分是:自发光(emissive部分、高光反射(specular部分、漫反射(diffuse部分、环境光(ambient部分。

--自发光(emissive部分】:这个部分用来描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量。(注意:如果没有使用全局光照(global illumination)技术,这些自发光的表面并不会真的照亮周围的物体,而是它本身看起来更亮了而已。)

--高光反射(specular部分】:这个部分用于描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量。

--漫反射(diffuse部分】:这个部分用于描述,当光线从光源照射到模型表面时,该表面会向每个方向散射多少辐射量。

--环境光(ambient部分】:它用于描述其他所有的间接光照。

++2.2.4、环境光

++++虽然标准光照模型的重点在于描述直接光照,但在真实的世界中,物体也可以被间接光照(indirect light)所照亮。

++++间接光照指的是,光线通常会在多个物体之间反射,最后进入摄像机,也就是说,在光线进入摄像机之前,经过了不止一次的物体反射。

++++在标准光照模型中,我们使用了一种被称为环境光的部分来近似模拟间接光照。(环境光的计算非常简单,它通常是一个全局变量,即场景中的所有物体都使用这个环境光。)

++2.2.5、自发光

++++光线也可以直接由光源发射进入摄像机,而不需要经过任何物体的反射。(标准光照模型使用自发光来计算这个部分的贡献度。)

++++通常在实时渲染中,自发光的表面往往并不会照亮周围的表面,也就是说,这个物体并不会被当成一个光源。(Unity5引入了全新的全局光照系统则可以模拟这类自发光物体对周围物体的影响。)

++2.2.6、漫反射

+++++漫反射光照是用于对那些被表面随机散射到各个方向的辐射度进行建模的。

++++在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的。(入射光线的角度很重要。)

++2.2.7、高光反射

++++这里的高光反射是一种经验模型,它并不完全符合真实世界中的高光反射现象。

++++高光反射可用于计算那些沿着完全镜面反射方向被反射的光线,这可以让物体看起来是有光泽的,例如金属材质。

++++计算高光反射需要知道的信息比较多,如表面法线、视角方向、光源方向、反射方向等。

++2.2.8、逐像素还是逐顶点

++++基本光照模型使用的数学公式:逐像素光照、逐顶点光照。

++++逐顶点光照(per-vertex lighting】:在顶点着色器中计算。(逐顶点光照,也称为高洛德(Gouraud shading)。)(在逐顶点光照中,我们在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最后输出成像素颜色。)

++++逐像素光照(per-pixel lighting】:在片元着色器中计算。(在逐像素光照中,我们会以每个像素为基础,得到它的法线(可以是顶点法线插值得到的,也可以是从法线纹理中采样得到的),然后进行光照模型的计算。)(这种在面片之间对顶点法线进行的插值的技术被称为Phong着色(Phong shading,也被称为Phong插值或法线插值着色技术。)

++2.2.9Unity中的环境光和自发光

++++在标准光照模型中,环境光和自发光的计算是最简单的。

++++Unity场景中的环境光可以在【Window=>Lighting=>Ambient Source/Ambient Color/Ambient Intensity】中控制。

++++Shader中,我们只需要通过Unity的内置变量UNITY_LIGHTMODEL_AMBIENT就可以得到环境光的颜色和强度信息。

++++大多数物体是没有自发光特性的,如果要计算自发光只需要在片元着色器输出最后的颜色之前,把材质的自发光颜色添加到输出颜色上即可。

++2.2.10、逐顶点光照的实践:逐顶点的漫反射光照效果

++++我们来看如何实现一个逐顶点的漫反射光照效果。

//立钻哥哥:逐顶点的漫反射光照效果

Shader YanlzShaderDemo/DiffuseVertexLevel_ShaderDemo01{

    Properites{

        _Diffuse(Diffuse, Color) = (1,1,1,1)

    }

 

    SubShader{

        Pass{

            Tags{  LightMode=ForwardBase  }

        

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

 

            fixed4 _Diffuse;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                fixed3 color : COLOR;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

    

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

                fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);

                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNorml, worldLight));

                o.color = ambient + diffuse;

    

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                return fixed4(i.color, 1.0);

            }

 

            ENGCG

        }

   }

 

    FallBack Diffuse

}

++++为了得到并且控制材质的漫反射颜色,在ShaderProperties语义块中声明了一个Color类型的属性,并把它的初始值设为白色:【_Diffuse (Diffuse, Color) = (1,1,1,1)

++++SubShader语义块中定义了一个Pass语义块。(顶点/片元着色器的代码需要写在Pass语义块,而非SubShader语义块中。)

++++Pass的第一行指明了该Pass的光照模式:【Tags { LightMode = ForwardBase }

--LightMode标签是用于定义该PassUnity的光照流水线中的角色,只有定义了正确的LightMode,才能得到一些Unity的内置光照变量。

++++使用CGPROGRAMENDCG来包围CG代码片段,以定义最重要的顶点着色器和片元着色器代码。(使用#pragma指令来告诉Unity,我们定义的顶点着色器和片元着色器叫什么名字。)

++++为了使用Unity内置的一些变量,需要包含内置文件Lighting.cginc:【#include Lighting.cginc

++++为了在Shader中使用Properties语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量:【fixed4 _Diffuse】:通过这种方式,我们就可以得到漫反射公式中需要的参数之一:材质的漫反射属性。(由于颜色属性的范围在01之间,因此我们可以使用fixed精度的变量来存储它。)

++++定义顶点着色器的输入和输出结构体(输出结构体同时也是片元着色器的输入结构体):【struct a2v{ };】、【struct v2f{ };

struct a2v{

    float4 vertex : POSITION;

    float3 normal : NORMAL;

};

 

struct v2f{

    float4 pos : SV_POSITION;

    fixed3 color : COLOR;

};

--为了访问顶点的法线,需要在a2v中定义一个normal变量,并通过使用NORMAL语义来告诉Unity要把模型顶点的法线信息存储到normal变量中。

--为了把在顶点着色器中计算得到的光照颜色传递给片元着色器,我们需要在v2f中定义一个color变量,且并不是必须使用COLOR语义,一些资料中会使用TEXCOORD0语义。

++++顶点着色器实现一个逐顶点的漫反射光照(漫反射部分的计算都将在顶点着色器中进行):

v2f vert(a2v v){

    v2f o;

    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

 

    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

    fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));

    fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);

    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));

    o.color = ambient + diffuse;

 

    return o;

}

--首先定义返回值o:【v2f o;

--顶点着色器最基本的任务就是把顶点位置从模型空间转换到裁剪空间中,因此我们需要使用Unity内置的模型*世界*投影矩阵UNITY_MATRIX_MVP来完成这样的坐标变换。

--通过Unity内置变量UNITY_LIGHTMODEL_AMBIENT得到了环境光部分。

--材质的漫反射颜色_Diffuse以及顶点法线v.normal

--Unity提供了一个内置变量_LightColor0来访问该Pass处理的光源和强度信息以及光源方向(想要得到正确的值需要定义合适的LightMode标签)。

--光源方向可以由_WorldSpaceLightPos0来得到。(注意:如果场景中有多个光源并且类型可能是点光源等其他类型,直接使用_WorldSpaceLightPos0就不能得到正确的结果。)

--在计算法线和光源方向之间的点积时,我们需要选择它们所在的坐标系,只有两者处于同一坐标系空间下,它们的点积才有意义。(我们选择了世界坐标空间,由a2v得到的顶点法线是位于模型空间下的,因此我们首先需要把法线转换到世界空间中。)(可以使用顶点变换矩阵的逆转置矩阵对法线进行相同的变换,首先得到模型空间到世界空间的变换矩阵的逆矩阵_World2Object,然后通过调换它在mul函数中的位置,得到和转置矩阵相同的矩阵乘法。)(由于法线是一个三维矢量,需要截取_World2Object的前三行前三列即可。)

--在得到了世界空间中的法线和光源受,我们需要对它进行归一化操作。(在得到它们点积的结果后,需要防止这个结果为负值,使用saturate函数。)(saturate函数是CG提供的一种函数,它的作用是可以把参数截取到[0,1]的范围内。)

--光源的颜色和强度以及材质的漫反射颜色相乘即可得到最终的漫反射光照部分。

--对环境光和漫反射光部分相加,得到最终的光照结果。

++++所有的计算在顶点着色器中都已经完成了,片元着色器的代码只需要直接把顶点颜色输出即可:

fixed4 frag(v2f i) : SV_Target{

    return fixed4(i.color, 1.0);

}

++++UnityShader的回调shader设置为内置的Diffuse:【Fallback Diffuse

++++逐顶点的漫反射光照,对于细分程度较高的模型,逐顶点光照已经可以得到比较好的光照效果了。但对于一些细分程序较低的模型,逐顶点光照就会出现一些视觉问题:例如背光面与向光面交界处有一些锯齿等,为了解决这些问题,就可以使用逐像素的漫反射光照。

++2.2.11、逐像素光照实践:逐像素的漫反射光照效果

++++逐像素的漫反射效果。

//立钻哥哥:逐像素的漫反射光照效果

Shader YanlzShaderDemo/DiffusePixelLevel_ShaderDemo02{

    Properties{

        _Diffuse(Diffuse, Color) = (1,1,1,1)

    }

 

    SubShader{

        Pass{

            Tags{  LightMode=ForwardBase  }

 

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

 

            fixed4 _Diffuse;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);

 

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 worldNormal = normalize(i.worldNormal);

                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

                fixed3 color = ambient + diffuse;

 

                return fixed4(color, 1.0);

            }

 

            ENDCG

        }

    }

 

    FallBack Diffuse

}

++++顶点着色器不需要计算光照模型,只需要把世界空间下的法线传递给片元着色器即可。

++++逐像素光照可以得到更加平滑的光照效果。(但是,即便使用了逐像素漫反射光照,有一个问题依然存在:当光照无法达到的区域,模型的外观通常是全黑的,没有任何明暗变化,这会使背光区域看起来就像一个平面一样,失去了模型细节表现。)

++++可以通过添加环境光来得到非全黑的效果,但即便这样仍然无法解决背光面明暗一样的缺点。

++++有一种改善技术被提出来,这就是半兰伯特(Half Lambert)光照模型

++2.2.12、半兰伯特模型实践:半兰伯特漫反射光照效果

++++漫反射光照模型也被称为兰伯特光照模型,因为它符合兰伯特定律:在平面某点漫反射光的光强与该反射点的法向量和入射光角度的余弦值成正比。

++++半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。

//立钻哥哥:半兰伯特漫反射光照

Shader YanlzShaderDemo/HalfLambert_ShaderDemo03{

    Properties{

        _Diffuse(Diffuse, Color) = (1,1,1,1)

    }

 

    SubShader{

        Pass{

            Tags{  LightMode=ForwardBase  }

 

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

        

            fixed4 _Diffuse;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

            };

 

            v2f vert(a2v v){

                float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

            }

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);

 

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 worldNormal = normalize(i.worldNormal);

                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                fixed3 halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;

                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;

                fixed3 color = ambient + diffuse;

 

                return fixed4(color, 1.0);

            }

 

            ENGCG

        }

    }

 

    FallBack Diffuse

}

++++使用半兰伯特公式修改片元着色器中计算漫反射光照的部分。

++2.2.13、漫反射光照对比

++++逐顶点漫反射光照、逐像素漫反射光照、半兰伯特光照的对比效果:


++2.2.14、逐顶点光照实践:逐顶点的高光反射光照效果

++++实现一个逐顶点的高光反射效果。

//立钻哥哥:逐顶点的高光反射效果

Shader YanlzShaderDemo/SpecularVertexLevel_ShaderDemo01{

    Properties{

        _Diffuse (Diffuse, Color) = (1,1,1,1)

        _Specular (Specular, Color) = (1,1,1,1)

        _Gloss (Gloss, Range(8.0, 256)) = 20

    }

 

    SubShader{

        Pass{

            Tags{  LightMode=ForwardBase }

 

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

 

            fixed4 _Diffuse;

            fixed4 _Specular;

            float _Gloss;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                fixed3 color : COLOR;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

    

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

                fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));

                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);

 

                o.color = ambient + diffuse + specular;

 

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                return fixed4(i.color, 1.0);

            }

 

           ENGCG

        }

    }

 

    FallBack Specular

}

++++为了在材质面板上能够方便地控制高光反射属性,在ShaderProperties语义中声明了三个属性:_Diffuse_Specular_Gloss等。(新添加的_Specular用于控制材质的高光反射颜色)(新增的_Gloss用于控制高光区域的大小)

++++SubShader语义块中定义了一个Pass语义块。定义了光照模式:【Tags { LightMode=ForwardBase }

++++对于高光反射部分,我们首先计算了入射光线方向关于表面法线的反射方向reflectDir。(由于CGreflect函数的入射方向要求是由光源指向交点处的,因此我们需要对worldLightDir取反后再传给reflect函数。)

++++我们通过_WorldSpaceCameraPos得到了世界空间中的摄像机位置,再把顶点位置从模型空间变换到世界空间下,再通过和_WorldSpaceCameraPos相减即可得到世界空间下的视角方向。

++++使用逐顶点的方法得到的高光效果有比较大的问题,高光部分明显不平滑。(是因为:高光反射部分的计算是非线性的,而在顶点着色器中计算光照再进行插值的过程是线性的,破坏了原计算的非线性关系,就会出现较大的视觉问题。)=>因此,我们就需要使用逐像素的方法来计算高光反射。

++2.2.15、逐像素光照实践:逐像素的高光反射光照效果

++++可以使用逐像素光照来得到更加平滑的高光效果。

//立钻哥哥:逐像素的高光反射光照

Shader YanlzShaderDemo/SpecularPixelLevel_ShaderDemo02{

    Properties{

        _Diffuse (Diffuse, Color) = (1,1,1,1)

        _Specular (Specular, Color) = (1,1,1,1)

        _Gloss (Gloss, Range(8.0, 256)) = 20

    }

 

    SubShader{

        Pass{

            Tags {  LightMode=ForwardBase }

 

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

 

            fixed4 _Diffuse;

            fixed4 _Specular;

            float _Gloss;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

                float3 worldPos : TEXCOORD1;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);

                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

 

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 worldNormal = normalize(i.worldNormal);

                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

                fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));

                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);

 

                 return fixed4(ambient + diffuse + specular, 1.0);

            }

 

            ENGCG

        }

    }

 

    FallBack Specular

}

++++顶点着色器只需要计算世界空间下的法线方向和顶点坐标,并把它们传递给片元着色器即可。

++++片元着色器需要计算关键的光照模型:【fixed4 frag(v2f i) : SV_Target{  }

++++按逐像素的方式处理光照可以得到更加平滑的高光效果。=>可以实现一个完整的Phong光照模型。

++2.2.16Blinn-Phong光照模型

++++另一种高光反射的实现方法:Blinn光照模型。

//立钻哥哥:Blinn-Phong高光反射光照

Shader YanlzShaderDemo/BlinnPhong_ShaderDemo03{

    Properties{

        _Diffuse (Diffuse, Color) = (1,1,1,1)

        _Specular (Specular, Color) = (1,1,1,1)

        _Gloss (Gloss, Range(8.0, 256)) = 20

    }

 

    SubShader{

        Pass{

            Tags {  LightMode=ForwardBase  }

  

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

      

            fixed4 _Diffuse;

            fixed4 _Specular;

            float _Gloss;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

            };

 

             struct v2f{

                float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

                float3 worldPos : TEXCOORD1;

             };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);

                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

 

                return o;

             }

 

            fixed4 frag(v2f i) : SV_Target{

                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                 fixed3 worldNormal = normalize(i.worldNormal);

                 fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                 fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

                 fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);

                 fixed3 halfDir = normalize(worldLightDir + viewDir);

                 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

 

                 return fixed4(ambient + diffuse + specular, 1.0);

             }

 

            ENDCG

        }

    }

 

    FallBack Specular

}

++++Blinn-Phong光照模型的高光反射部分看起来更大、更亮一些。(在实际渲染中,绝大多数情况我们都会选择Blinn-Phong光照模型。)

++++说明:这两种光照模型都是经验模型,不应该认为Blinn-Phong模型是对“正确的”Phong模型的近似。(实际上,在一些情况下,Blinn-Phong模型更符合实验结果。)

2.2.17、高光反射光照对比

++++逐顶点的高光反射光照、逐像素的高光反射光照(Phong光照模型)、Blinn-Phong高光反射光照的对比参考:


++2.2.18、使用Unity内置的函数

++++在计算光照模型的时候,需要得到光源方向、视角方向这两个基本信息。

++++使用normalize(_WorldSpaceLightPos0.xyz)来得到光源方向:这种方法实际只适用于平行光。

++++使用normalize(_WorldSpaceCameraPos.xyz - i.worldPosition.xyz)来得到视角方向。

++++手动计算这些光源信息的过程相对比较麻烦(但并不意味着需要了解它们的原理),Unity提供了一些内置函数来帮助我们计算这些信息。

++++UnityCG.cginc中一些常用的帮助函数:

--float3 WorldSpceViewDir(float4 v)】:输入一个模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向。内部实现使用了UnityWorldSpaceViewDir函数。

--float3 UnityWorldSpaceViewDir(float4 v)】:输入一个世界空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向。

--float3 ObjSpaceViewDir(float4 v)】:输入一个模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向。

--float3 WorldSpaceLightDir(float4 v)】:仅可用于前向渲染中,输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。内部实现使用了UnityWorldSpaceLightDir函数。没有被归一化。

--float3 UnityWorldSpaceLightDir(float4 v)】:仅可用于前向渲染中,输入一个世界空间中的顶点位置,返回世界空间中从该点到光源的光照方向。没有被归一化。

--float3 ObjSpaceLightDir(float4 v)】:仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。没有被归一化。

--float3 UnityObjectToWorldNormal(float3 norm)】:把法线方向从模型空间转换到世界空间中。

--float3 UnityObjectToWorldDir(in float3 dir)】:把方向矢量从模型空间变换到世界空间中。

--float3 UnityWorldToObjectDir(float3 dir)】:把方向矢量从世界空间变换到模型空间中。

++++类似UnityXXX的几个函数是Unity5中新添加的内置函数。(这些帮助函数使得我们不需要跟各种变换矩阵、内置变量打交道,也不需要考虑各种不同的情况(例如使用了哪种光源),而仅仅调用一个函数就可以得到需要的信息。)

++++说明:这些函数都没有保证得到的方向矢量是单位矢量,因此,我们需要在使用前把它们归一化。

++++计算光源方向的3个函数:WorldSpaceLightDirUnityWorldSpaceLightDirObjSpaceLightDir,稍微复杂一些,是因为:Unity帮我们处理了不同种类光源的情况。(这3个函数仅可用于前向渲染,这是因为只有在前向渲染时,这3个函数里使用的内置变量_WorldSpaceLightPos0等才会被正确赋值。)

++2.2.19、使用Unity内置函数改写Blinn-Phong高光反射光照

++++在实际编写过程中,往往会借助于Unity的内置函数来帮助我们进行各种计算,这可以减轻不少我们的“痛苦”。

//立钻哥哥:使用Unity内置函数的Blinn-Phong高光反射光照

Shader YanlzShaderDemo/BlinnPhongUseBuiltInFunctions_ShaderDemo04{

    Properties{

        _Diffuse (Diffuse, Color) = (1,1,1,1)

        _Specular (Specular, Color) = (1,1,1,1)

        _Gloss (Gloss, Range(1.0, 500)) = 20

    }

 

    SubShader{

        Pass{

            Tags {  LightMode=ForwardBase  }

        

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

 

            fixed4 _Diffuse;

            fixed4 _Specular;

            float _Gloss;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

            };

 

            struct v2f{

                 float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

                float4 worldPos : TEXCOORD1;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                o.worldPos = mul(unity_ObjectToWorld, v.vertex);

 

                return o;

            }

 

             fixed4 frag(v2f i) : SV_Target{

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 worldNormal = normalize(i.worldNormal);

                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));

                fixed3 halfDir = normalize(worldLightDir + viewDir);

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

 

                return fixed4(ambient + diffuse + specular, 1.0);

            }

 

            ENGCG

        }

    }

 

    FallBack Specular

}

++++在顶点着色器中,我们使用内置的UnityObjectToWorldNormal函数计算世界空间下的法线方向。

++++在片元着色器中,我们使用内置的UnityWorldSpaceLightDir函数和UnityWorldSpaceViewDir函数来分别计算世界空间的光照方向和视角方向。

++++说明:内置函数得到的方向是没有归一化的,因此需要使用normalize函数来对结果进行归一化,在进行光照模型的计算。





###2.3、基础纹理

++2.3、基础纹理

++++纹理最初的目的就是使用一张图片来控制模型的外观。(使用纹理映射(texture mapping)技术,我们可以把一张图“黏”在模型表面,逐纹素(texel地控制模型的颜色。)

++++在美术人员建模的时候,通常会在建模软件中利用纹理展开技术把纹理映射坐标(texture-mapping coordinates存储在每个顶点上。(纹理映射坐标定义了该顶点在纹理中对应的2D坐标。)(通常,这些坐标使用一个二维变量(u,v)来表示,其中u是横向坐标,而v是纵向坐标。因此,纹理映射坐标也被称为UV坐标)

++++纹理的大小可以是多种多样的,但顶点UV坐标的范围通常被归一化到[0,1]范围内。(纹理采样时使用的纹理坐标不一定是在[0,1]范围内。)

++2.3.1、单张纹理实践:Blinn-Phong光照模型来计算光照

++++通常会使用一张纹理来代替物体的漫反射颜色。

//立钻哥哥:使用单张纹理

Shader YanlzShaderDemo/SingleTexture_ShaderDemo01{

    Properties{

        _Color (Color Tint, Color) = (1,1,1,1)

        _MainTex (Main Tex, 2D) = white{}

        _Specular (Specular, Color) = (1,1,1,1)

        _Gloss (Gloss, Range(8.0, 256)) = 20

    }

 

    SubShader{

        Pass{

            Tags {  LightMode=ForwardBase }

 

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

 

            fixed4 _Color;

            sampler2D _MainTex;

            float4 _MainTex_ST;

            fixed4 _Specular;

            float _Gloss;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float4 texcoord : TEXCOORD0;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

                float3 worldPos : TEXCOORD1;

                float2 uv : TEXCOORD2;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

                //o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);   //built-in function

 

               return o;

            }

 

             fixed4 frag(v2f i) : SV_Target{

                fixed3 worldNormal = normalize(i.worldNormal);

                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));

                fixed3 halfDir = normalize(worldLightDir + viewDir);

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

 

                return fixed4(ambient +diffuse + specular, 1.0);

            }

 

            ENDCG

        }

    }

 

    FallBack Specular

}

++++为了使用纹理,需要在Properties语义块中添加纹理属性:_Color_MainTex_Specular_Gloss等。(【_MainTex (Main Tex, 2D) = white { }】:2D是纹理属性的声明方式。使用一个字符串后跟一个花括号作为它的初始值,“white”是内置纹理的名字,也就是一个全白的纹理。)(为了控制物体的整体色调,声明一个_Color属性:【_Color (Color Tint, Color) = (1,1,1,1)】)

++++为纹理类型的属性声明一个float4类型的变量_MainTex_ST:【float4 _MainTex_ST;】:在Unity中使用 纹理名_ST的方式来声明某个纹理的属性。(ST缩放(scale平移(translation的缩写。)

++++_MainTex_ST】:可以让我们得到该纹理的缩放和平移(偏移)值,_MainTex_ST.xy存储的是缩放值(Tilling),_MainTex_ST.zw存储的是偏移值(Offset)。

++++a2v结构体中使用TEXCOORD0语义声明一个新的变量texcoord(【float4 texcoord : TEXCOORD0;】):Unity就会将模型的第一组纹理坐标存储到该变量中。

++++v2f结构体中添加了用于存储纹理坐标的变量uv(【float2 uv : TEXCOORD2;】),以便在片元着色器中使用该坐标进行纹理采样。

++++在顶点着色器中,使用纹理的属性值_MainTex_ST来对顶点纹理坐标进行变换,得到最终的纹理坐标。(计算过程是:首先使用缩放属性_MainTex_ST.xy对顶点纹理坐标进行缩放,然后再使用偏移属性_MainTex_ST.zw对结果进行偏移。)

++++Unity提供了一个内置宏TRANSFORM_TEX来帮我们计算上述过程:【o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

++++TRANSFORM_TEXUnityCG.cginc中的定义:

//立钻哥哥:...\UnityCG.cginc

//Transforms 2D UV by scale/bias property

#define TRANSFORM_TEX(tex, name) (tex.xy * name##_ST.xy + name##_ST.zw)

++++TRANSFORM_TEX(tex, name)】:接受两个参数,第一个参数是顶点纹理坐标,第二个参数是纹理名,它的实现中,利用纹理名_ST的方式来计算变换后的纹理坐标。

++++实现片元着色器,并在计算漫反射时使用纹理中的纹素值。(首先计算世界空间下的法线方向和光照方向。然后,使用CGtex2D函数对纹理进行采样。【tex2D(_MainTex, i.uv)】:第一个参数是需要被采样的纹理,第二个参数是一个float2类型的纹理坐标,它将返回计算得到的纹素值。)(使用采样结果和颜色属性_Color的乘积来作为材质的反射率albedo:【fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;】,并把发射率albedo和环境光照相乘得到环境光部分:【fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;】)

++2.3.2、纹理的属性

++++Unity的纹理映射:声明一个纹理变量,再使用tex2D函数采样。

++++实际上,在渲染流水线中,纹理映射的实现远比我们想象的复杂。

++++纹理缩小的过程比放大更加复杂:原纹理中的多个像素将会对应一个目标像素。(需要处理抗锯齿问题:使用多级渐远纹理(mipmapping)技术。)

++2.3.3、凹凸映射

++++纹理的另一种常见的应用就是凹凸映射(bump mapping

++++凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。(这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”。)

++++有两种主要的方法可以用来进行凹凸映射:一种方法是使用一张高度纹理(height map来模拟表面位移(displacement,然后得到一个修改后的法线值,这种方法也被称为高度映射(height mapping;另一种方法则是使用一张法线纹理(normal map来直接存储表面法线,这种方法又被称为法线映射(normal mapping

++++常常把凹凸映射法线映射当成是相同的技术。

++2.3.4、高度纹理

++++使用一张高度纹理(height map来模拟表面位移(displacement,然后得到一个修改后的法线值,这种方法也被称为高度映射(height mapping

++++高度图中存储的是强度值(intensity),它用于表示模型表面局部的海拔高度。(颜色越浅表明该位置的表面越向外凸起,颜色越深表面该位置越向里凹。这种方法的好处是非常直观,可以从高度图中明确地知道一个模型表面的凹凸情况,但缺点是计算更加复杂,在实时计算时不能直接得到表面法线,而是需要由像素的灰度值计算而得,因此需要消耗更多的性能。)

++++高度图通常会和法线映射一起使用,用于该出表面凹凸的额外信息。(通常会使用法线映射来修改光照。)

++2.3.5、法线纹理

++++使用一张法线纹理(normal map来直接存储表面法线,这种方法又被称为法线映射(normal mapping

++++法线纹理中存储的就是表面的法线方向。

++++对于模型顶点自带的法线,它们是定义在模型空间中的,因此一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为是模型空间的法线纹理(object-space normal map。(在实际制作中,往往会采用另一种坐标空间,即模型顶点的切线空间(tangent space来存储法线。)

++++法线本身存储在哪个坐标系中都是可以的,甚至可以选择存储在世界空间下。

++++使用模型空间来存储法线的优点:1、实现简单,更加直观;2、在纹理坐标的缝合处和尖锐的边角部分,可以的突变(缝隙)较少,即可以提供平滑的边界。

++++使用切线空间有更多优点:1、自由度很高;2、可进行UV动画;3、可以重用法线纹理;4、可压缩。(切线空间在很多情况下都优于模型空间。)

++++计算光照模型中统一各个方向矢量所在的坐标空间。由于法线纹理中存储的法线是切线空间下的方向,有两种选择:

--第一种选择是在切线空间下进行光照计算,此时我们需要把光照方向、视角方向变换到切线空间下;

--另一种选择是世界空间下进行光照计算,此时我们需要把采样得到的法线方向变换到世界空间下,再和世界空间下的光照方向和视角方向进行计算。

--从效率上来说,第一种方法往往要优于第二种方法,因为我们可以在顶点着色器中就完成对光照方向盒视角方向的变换,而第二种方法由于要先对法线纹理进行采样,所以变换过程必须在片元着色器中实现,这意味着我们需要在片元着色器中进行一次矩阵操作。

--从通用性角度来说,第二种方法要由于第一种方法,因为有时我们需要在世界空间下进行一些计算,例如在使用Cubemap进行环境映射时,我们需要使用世界空间下的反射方向对Cubemap进行采样。(如果同时需要进行法线映射,我们就需要把法线方向变换到世界空间下。)

++2.3.6、使用法线纹理实践:在切线空间下计算光照模型

++++在切线空间下计算光照模型的基本思路是:在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向、光照方向等进行计算,得到最终的光照结果。

++++首先需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中,即我们需要知道从模型空间到切线空间的变换矩阵。(从模型空间到切线空间的变换矩阵就是从切线空间到模型空间的变换矩阵的转置矩阵,把切线(x轴)、副切线(y轴)、法线(z轴)的顺序按行排列即可得到。)

//立钻哥哥:在切线空间下计算光照模型

Shader YanlzShaderDemo/NormalMapTangentSpaceMat_ShaderDemo01{

    Properties{

        _Color (Color Tint, Color) = (1,1,1,1)

        _MainTex (Main Tex, 2D) = white{ }

        _BumpMap (Normal Map, 2D) = bump{ }

        _BumpScale (Bump Scale, Float) = 1.0

        _Specular (Specular, Color) = (1,1,1,1)

        _Gloss (Gloss, Range(8.0, 256)) = 20

    }

 

    SubShader{

        Pass{

            Tags {  LightMode=ForwardBase  }

        

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

   

            fixed4 _Color;

            sampler2D _MainTex;

            float4 _MainTex_ST;

            sampler2D _BumpMap;

            float4 _BumpMap_ST;

            float _BumpScale;

            float4 _Specular;

            float _Glosss;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float4 tangent : TANGENT;

                float4 texcoord : TEXCOORD0;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float4 uv : TEXCOORD0;

                float3 lightDir : TEXCOORD1;

                float3 viewDir : TEXCOORD2;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

                o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

 

                //float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;

                //float3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);

                TANGENT_SPACE_ROTATION;    //use the built-in macro

 

                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;

                o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;

 

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                fixed3 tangentLightDir = normalize(i.lightDir);

                fixed3 tangentViewDir = normalize(i.viewDir);

                fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);

 

                 fixed3 tangentNormal;

                 tangentNormal = UnpackNormal(packedNormal);

                 tangentNormal.xy *= _BumpScale;

                 tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

 

                 fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));

                 fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);

                 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);

 

                 return fixed4(ambient + diffuse + specular, 1.0);

            }

 

             ENDCG

        }

    }

 

    FallBack Specular

}

++++Properties语义块中添加法线纹理的属性,以及用于控制凹凸程度的属性:_Color_MainTex_BumpMap_BumpScale_Specular_Gloss等。(对于法线纹理_BumpMap:【_BumpMap (Normal Map, 2D) = bump { }】:使用bump作为它的默认值。(bumpUnity内置的法线纹理,当没有提供任何法线纹理时,bump就对应了模型自带的法线信息。))(_BumpScale:【_BumpScale (Bump Scale, Float) = 1.0】:用于控制凹凸程度的,当它为0时,意味着该法线纹理不会对光照产生任何影响。)

++++为了使用Unity内置的一些变量,如_LightColor0,还需要包含进Unity的内置文件Lighting.cginc=>#include Lighting.cginc

++++为了得到该纹理的属性(平铺和偏移系数),为_MainTex_BumpMap定义了_MainTex_ST_BumpMap_ST变量。

++++切线空间是顶点法线和切线构建出的一个坐标空间,需要得到顶点的切线信息。顶点着色器的输入结构体a2v

struct a2v{

    float4 vertex : POSITION;

    float3 normal : NORMAL;

    float4 tangent : TANGENT;

    float4 texcoord : TEXCOORD0;

};

--使用TANGENT语义来描述float4类型的tangent变量,以告诉Unity把顶点的切线方向填充到tangent变量中。(注意:和法线方向normal不同,tangent的类型是float4,而非float3,这是因为我们需要使用tangent.w分量来决定切线空间中的第三个坐标轴:副切线的方向性。)

++++在顶点着色器中计算切线空间下的光照和视角方向,在v2f结构体中添加变量来存储变换后的光照和视角方向:

struct v2f{

    float4 pos : SV_POSITION;

    float4 uv : TEXCOORD0;

    float3 lightDir : TEXCOORD1;

    float3 viewDir : TEXCOORD2;

};

++++定义顶点着色器:【v2f vert(a2v v){  }】:使用了两张纹理,因此需要存储两个纹理坐标。(我们把v2f中的uv变量的类型定义为float4类型,其中xy分量存储了_MainTex的纹理坐标,而zw分量存储了_BumpMap的纹理坐标(实际上,_MainTex_BumpMap通常会使用同一组纹理坐标,出于减少插值寄存器的使用数目的目的,我们往往只计算和存储一个纹理坐标即可。))(然后,把模型空间下切线方向、副切线方向和法线方向按行排列来得到从模型空间到切线空间的变换矩阵rotation。)

++++Unity提供了一个内置宏TANGENT_SPACE_ROTATION(在UnityCG.cginc中定义)来帮助我们直接计算得到rotation变换矩阵。

++++使用Unity的内置函数ObjSpaceLightDirObjSpaceViewDir来得到模型空间下的光照和视角方向,再利用变换矩阵rotation把它们从模型空间变换到切线空间中。

++++在顶点着色器中完成了大部分工作,因此片元着色器中只需要采样得到切线空间下的法线方向,再在切线空间下进行光照计算即可。

++++fixed4 frag(v2f i) : SV_Target{  }】:首先利用tex2D对法线纹理_BumpMap进行采样。(法线纹理中存储的是把法线经过映射后得到的像素值,因此我们需要把它们反映射回来。)

(如果没有在Unity里把该法线纹理的类型设置成Normal map,就需要在代码中手动进行这个过程。)(首先把packedNormalxy分量按公式映射回法线方向,然后乘以_BumpScale(控制凹凸程度)来得到tangentNormalxy分量。由于法线都是单位矢量,因此tangentNormal.z分量可以由tangentNormal.xy计算而得。由于我们使用的是切线空间下的法线纹理,可以保证法线方向的z分量为正。)(在Unity中,为了方便对法线纹理的存储进行优化,通常会把法线纹理的纹理类型标识成Normal mapUnity会根据平台来选择不同的压缩方法。=>可以使用Unity内置函数UnpackNormal来得到正确的法线方向。)

++2.3.7、使用法线纹理实践:在世界空间下计算光照模型

++++在世界空间下计算光照模型的基本思路:在顶点着色器中计算从切线空间到世界空间的变换矩阵,并把它传递给片元着色器。(变换矩阵的计算可以由顶点的切线、副切线和法线在世界空间下的表示来得到。)(最后,只需要在片元着色器中把法线纹理中的法线方向从切线空间变换到世界空间下即可。)(尽管这种方法需要更多的计算,但在需要使用Cubemap进行环境映射等情况下,我们就需要使用这种方法。)

//立钻哥哥:在世界空间下计算光照模型

Shader YanlzShaderDemo/NormalMapWorldSpace_ShaderDemo02{

    Properties{

        _Color (Color Tint, Color) = (1,1,1,1)

        _MainTex (Main Tex, 2D) = white { }

        _BumpMap (Normal Map, 2D) = bump { }

        _BumpScale (Bump Scale, Float) = 1.0

        _Specular (Specular, Color) = (1,1,1,1)

        _Gloss (Gloss, Range(8.0, 256)) = 20

    }

 

    SubShader{

        Pass{

            Tags {  LightMode=ForwardBase }

 

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc”    //立钻哥哥

 

            fixed4 _Color;

            sampler2D _MainTex;

            float4 _MainTex_ST;

            sampler2D _BumpMap;

            float4 _BumpMap_ST;

            float _BumpScale;

            fixed4 _Specular;

            float _Gloss;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float4 tangent : TANGENT;

                float4 texcoord : TEXCOORD0;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float4 uv : TEXCOORD0;

                float4 TtoW0 : TEXCOORD1;

                float4 TtoW1 : TEXCOORD2;

                float4 TtoW2 : TEXCOORD3;

            };

 

             v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

                o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

 

                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                fixed3 worldNormal = UnityObjcetToWorldNormal(v.normal);

                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);

                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);

                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);

                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

 

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);

                fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));

                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));

                bump.xy *= _BumpScale;

                bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));

                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));

                fixed3 halfDir = normalize(lightDir + viewDir);

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);

 

                return fixed4(ambient + diffuse + specular, 1.0);

            }

 

            ENDCG

        }

    }

 

    FallBack Specular

}

++++顶点着色器的输出结构体v2f,使它包含从切线空间到世界空间的变换矩阵:

struct v2f{

    float4 pos : SV_POSITION;

    float4 uv : TEXCOORD0;

    float4 TtoW0 : TEXCOORD1;

    float4 TtoW1 : TEXCOORD2;

    float4 TtoW2 : TEXCOORD3;

};

    --一个插值寄存器最多只能存储float4大小的变量,对于矩阵这样的变量,我们可以把它们按行拆成多个变量再进行存储。(TtoW0TtoW1TtoW2就依次存储了从切线空间到世界空间的变换矩阵的每一行。)(实际上,对方向矢量的变换只需要使用3x3大小的矩阵,也就是说,每一行只需要使用float3类型的变量即可。但为了充分利用插值寄存器的存储空间,把世界空间下的顶点位置存储在这些变量的w分量中。)

++++顶点着色器,计算从切线空间到世界空间的变换矩阵:【v2f vert(a2v v){  }

--首先从TtoW0TtoW1TtoW2w分量中构建世界空间下的坐标。

--然后,使用内置的UnityWorldSpaceLightDirUnityWorldSpaceViewDir函数得到世界空间下的光照和视角方向。

--接着,使用内置的UnpackNormal函数对法线纹理进行采样和解码(需要把法线纹理的格式标识成Normal map),并使用_BumpScale对其进行缩放。

--最后,我们使用TtoW0TtoW1TtoW2存储的变换矩阵把法线变换到世界空间下。【bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));】这是通过使用点乘操作来实现矩阵的每一行和法线相乘来得到的。

++++从视觉表现上,在切线空间下和世界空间下计算光照几乎没有任何差别。

--Unity 4.x版本中,在不需要使用Cubemap进行环境映射的情况下,内置的Unity Shader使用的是切线空间来进行法线映射和光照计算。

--Unity 5.x中,所有内置的Unity Shader都使用了世界空间来进行光照计算。(这也是为什么Unity5.x中表面着色器更容易报错,因为它们使用了更多的插值寄存器来存储变换矩阵(还有一些额外的插值寄存器是用来辅助计算雾效的。))

++2.3.8Unity中的法线纹理类型

++++当把法线纹理的纹理类型标识成Normal map时,可以使用Unity的内置函数UnpackNormal来得到正确的法线方向。

++++当我们需要使用那些包含了法线映射的内置的Unity Shader时,必须把使用的法线纹理标识成Normal map才能得到正确结果。(即便忘了,Unity也会在材质面板中提醒修正这个问题。)(这是因为这些Unity Shader都使用了内置的UnpackNormal函数采样法线方向。)

++++把纹理类型设置成Normal map,可以让Unity根据不同平台对纹理进行压缩,再通过UnpackNormal函数来针对不同的压缩格式对法线纹理进行正确的采样。

++++Normal map=>Create from Grayscale】:用于从高度图中生成法线纹理。(高度图本身记录的是相对高度,是一张灰度图,白色表示相对更高,黑色表示相对更低。)

++++Create from Grayscale=>Bumpiness】、【Filtering=>Smooth】、【Sharp

--Bumpiness】:用于控制凹凸程度;【Filtering】决定我们使用哪种方式来计算凹凸程度。

--Smooth】:这使得生成后的法线纹理会比较平滑;【Sharp】:它会使用Sobel滤波(一种边缘检测时使用的滤波器)来生成法线。

--Sobel滤波的实现非常简单:只需要在一个3x3的滤波器中计算xy方向上的导数,然后从中得到法线即可。(具体方法是:对于高度图中的每个像素,我们考虑它与水平方向和竖直方向上的像素差,把它们的差当成该点对应的法线在xy方向上的位移,然后使用之前提到的映射函数存储到法线纹理的rg分量即可。)

++2.3.9、渐变纹理

++++在渲染中使用纹理是为了定义一个物体的颜色,其实纹理可以用于存储任何表面属性。(一种常见的用法就是:使用渐变纹理来控制漫反射光照的结果。)(漫反射光照通常是使用表面法线和光照方向的点积结果与材质的反射率相乘来得到表面的漫反射光照。也可以用冷到暖色调的着色技术。)

++++冷到暖色调(cool-to-warm tones的着色技术:用来得到一种插画风格的渲染效果。(使用这种技术,可以保证物体的轮廓线相比于之前使用的传统漫反射光照更加明显,而且能够提供多种色调变化。)(很多卡通风格的渲染中都使用这种技术。)

++++使用一张渐变纹理来控制漫反射光照效果实例:

//立钻哥哥:使用一张渐变纹理来控制漫反射光照

Shader YanlzShaderDemo/RampTexture_ShaderDemo01{

    Properties{

        _Color (Color Tint, Color) = (1,1,1,1)

        _RampTex (Ramp Tex, 2D) = white {  }

        _Specular (Specular, Color) = (1,1,1,1)

        _Gloss (Gloss, Range(8.0, 256)) = 20

    }

 

    SubShader{

        Pass{

            Tags {  LightMode=ForwardBase }

        

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

 

            fixed4 _Color;

            sampler2D _RampTex;

            float4 _RampTex_ST;

            fixed4 _Specular;

            float _Gloss;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float4 texcoord : TEXCOORD0;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

                float3 worldPos : TEXCOORD1;

                float2 uv : TEXCOORD2;

             };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);

 

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                fixed3 worldNormal = normalize(i.worldNormal);

                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;

                fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb;

                fixed3 diffuse = _LightColor0.rgb * diffuseColor;

                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));

                fixed3 halfDir = normalize(worldLightDir + viewDir);

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

 

                return fixed4(ambient + diffuse + specular, 1.0);

            }

 

            ENDCG

        }

    }

 

    FallBack Specular

}

++++Properties语义块中声明一个纹理属性来存储渐变纹理。

++++半兰伯特模型:通过对法线方向和光照方向的点积做一次0.5倍的缩放以及一个0.5大小的偏移来计算半兰伯特部分halfLambert=>得到halfLambert的范围被映射到了[0,1]之间。

++++使用halfLambert来构建一个纹理坐标,并用这个纹理坐标对渐变纹理_RampTex进行采样。(由于_RampTex实际就是一个一维纹理(它在纵轴方向上颜色不变),因此纹理坐标的uv方向我们都使用了halfLambert。然后,把从渐变纹理采样得到的颜色和材质颜色_Color相乘,得到最终的漫反射颜色。)


++2.3.10、遮罩纹理(mask texture

++++遮罩允许我们可以保护某些区域,使它们免于某些修改。可以一张遮罩纹理来控制光照:希望模型表面某些区域的反光强烈一些,某些区域弱一些。(另一种常见的应用:制作地形材质时需要混合多种图片,例如表现草地的纹理、表现石子的纹理、表现裸露土地的纹理等,使用遮罩纹理可以控制如何混合这些纹理。))

++++使用遮罩纹理的流程一般是:通过采样得到遮罩纹理的纹素值,然后使用其中某个(或某几个)通道的值来与某种表面属性进行相乘,这样,当该通道的值为0时,可以保护表面不受该属性的影响。(使用遮罩纹理可以让美术人员更加精准(像素级别)地控制模型表面的各种性质。)

++++拓展:遮罩纹理已经不止限于保护某些区域使它们免于某些修改,而是可以存储任何我们希望逐像素控制的表面属性。(通常,我们会充分利用一张纹理的RGBA四个通道,用于存储不同的属性。例如:可以把高光反射的强度存储在R通道,把边缘光照的强度存储在G通道,把高光反射的指数部分存储在B通道,把自发光强度存储在A通道。)

++++最佳实践:开发人员可以为每个模型使用4张纹理:一张用于定义模型颜色,一张用于定义表面法线,另外两张则是遮罩纹理。这样,两张遮罩纹理提供了共8种额外的表面属性,这使得游戏中的人物材质自由度很强,可以支持很多高级的模型属性。

++++高光遮罩纹理实践:

//立钻哥哥:高光遮罩纹理(漫反射+高光反射+遮罩)

Shader YanlzShaderDemo/MaskTexture_ShaderDemo02{

    Properties{

        _Color (Color Tint, Color) = (1,1,1,1)

        _MainTex (Main Tex, 2D) = white{ }

        _BumpMap (Normal Map, 2D) = bump { }

        _BumpScale (Bump Scale, Float) = 1.0

        _SpecularMask (Specular Mask, 2D) = white {  }

        _SpecularScale (Specular Scale, Float) = 1.0

        _Specular (Specular, Color) = (1,1,1,1)

        _Gloss (Gloss, Range(8.0, 26)) = 20

    }

 

    SubShader{

        Pass{

            Tags {  LightMode=ForwardBase  }

        

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

 

            fixed4 _Color;

            sampler2D _MainTex;

            float4 _MainTex_ST;

            sampler2D _BumpMap;

            float _BumpScale;

            sampler2D _SpecularMask;

            float _SpecularScale;

            fixed4 _Specular;

            float _Gloss;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float4 tangent : TANGENT;

                float4 texcoord : TEXCOORD0;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float2 uv : TEXCOORD0;

                float3 lightDir : TEXCOORD1;

                float3 viewDir : TEXCOORD2;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

  

                TANGENT_SPACE_ROTATION;

                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;

                o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;

 

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                fixed3 tangentLightDir = normalize(i.lightDir);

                fixed3 tangentViewDir = normalize(i.viewDir);

 

                fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap), i.uv);

                tangentNormal.xy *= _BumpScale;

                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

 

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));

                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);

                fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);

 

                return fixed4(ambient + diffuse + specular, 1.0);

            }

 

            ENDCG

        }

    }

 

    FallBack Specular

}

++++Properties语义块中声明变量来控制高光反射。(_SpecularMask是需要使用的高光反射遮罩纹理,_SpecularScale是用于控制遮罩影响度的系数。)

++++为主纹理_MainTex、法线纹理_BumpMap、遮罩纹理_SpecularMask定义了它们共同使用的纹理属性变量_MainTex_ST。(这意味着:在材质面板中修改主纹理的平铺系数和偏移系数会同时影响3个纹理的采样。)(使用这种方式可以让我们节省需要存储的纹理坐标数目,如果我们为每一个纹理都使用一个单独的属性变量TextureName_ST,那么随着使用的纹理数目的增加,我们会迅速占满顶点着色器中可以使用的插值寄存器。而很多时候,我们不需要对纹理进行平铺和位移操作,或者很多纹理可以使用同一种平铺和位移操作,此时我们就可以对这些纹理使用同一个变换后的纹理坐标进行采样。)

++++在顶点着色器中,我们对光照方向和视角方向进行了坐标空间的变换,把它们从模型空间变换到了切线空间中,以便在片元着色器中和法线进行光照运算。

++++使用遮罩纹理的地方是片元着色器。我们使用它来控制模型表面的高光反射强度。

++++在计算高光反射时,我们首先对遮罩纹理_SpecularMask进行采样。(使用的遮罩纹理中每个纹素的rgb分量其实都是一样的,表明了该点对应的高光反射强度,在这里我们选择使用r分量来计算掩码值(【fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;】)。然后,我们用得到的掩码值和_SpecularScale相乘,一起来控制高光反射的强度。)

++++说明:我们使用的这张遮罩纹理其实有很多空间被浪费了:它的rgb分量存储的都是同一个值。(在实际的游戏制作中,我们会充分利用遮罩纹理中的每一个颜色通道来存储不同的表面属性。)





###2.4、透明效果

++2.4、透明效果

++++透明是游戏中经常要使用的一种效果。在实时渲染中要实现透明效果,通常会在渲染模型时控制它的透明通道(Alpha Channel。(当开启透明混合后,当一个物体被渲染到屏幕上时,每个片元除了颜色值和深度值之外,它还有另一个属性:透明度。)(当透明度为1时,表示该像素是完全不透明的,而当其为0时,则表示该像素完全不会显示。)

++++Unity中,实现透明效果方法有:透明度测试(Alpha Test透明度混合(Alpha Blending等。

++++对于不透明(opaque物体,不考虑它们的渲染顺序也能得到正确的排序效果,这是由于强大的深度缓冲(depth buffer,也称z-buffer的存在。(在实时渲染中,深度缓冲是用于解决可见性(visibility)问题的,它可以决定哪个物体的哪些部分会被渲染在前面,而哪些部分会被其他物体遮挡。基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把它的深度值和已经存在于深度缓冲中的值进行比较(有物体挡住了它);否则,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中(如果开启了深度写入。))

++++使用深度缓冲,可以让我们不用关心不透明物体的渲染顺序。(例如A挡住B,即便我们先渲染A再渲染B也不用担心B会遮盖掉A,因为在进行深度测试时会判断出B距离摄像机更远,也就不会写入到颜色缓冲中。)(但如果想要实现透明效果,事情就不那么简单了,这是因为,当使用透明度混合时,我们关闭了深度写入(ZWrite))

++2.4.1、透明度测试的基本原理

++++透明度测试(Alpha Test:它采用一种“霸道极端”的机制,只要一个片元的透明度不满足条件(通常是小于某个阀值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试、深度写入等。

++++透明度测试是不需要关闭深度写入的,它和其他不透明物体最大的不同就是它会根据透明度来舍弃一些片元。

++++虽然简单,但它产生的效果也很极端,要么完全透明,即看不到,要么完全不透明,就像不透明物体那样。

++2.4.2、透明度混合的基本原理

++++透明度混合(Alpha Blending:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。

++++透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。

++++说明:透明度混合只关闭了深度写入,但没有关闭深度测试。这意味着,当使用透明度混合渲染一个片元时,还是会比较它的深度值与当前深度缓冲中的深度值,如果它的深度值距离摄像机更远,那么就不会再进行混合操作。(这一点决定了:当一个不透明物体出现在一个透明物体的前面,而我们先渲染了不透明物体,它仍然可以正常得遮挡住透明物体。也就是说,对于透明度混合来说,深度缓冲是只读的。)

++2.4.3、渲染顺序很重要

++++对于透明度混合技术,需要关闭深度写入,此时我们就需要小心处理透明物体的渲染顺序。(如果不关闭深度写入,一个半透明表面背后的表面本来是可以透过它被看到的,但由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面将会被剔除,我们就无法透过半透明表面看到后面的物体了。)=>这就破坏了深度缓冲的工作机制,而这是一个非常非常非常糟糕的事情,尽管我们不得不这样做。

++++关闭深度写入导致渲染顺序将变得非常重要

++++渲染引擎一般都会先对物体进行排序,再渲染。常用的方法是:

--1)先渲染所有不透明物体,并开启它们的深度测试和深度写入。

--2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。

++2.4.4Unity Shader的渲染顺序

++++Unity为了解决渲染顺序的问题提供了渲染队列(render queue这一解决方案。使用SubShaderQueue标签来决定我们的模型将归于哪个渲染队列。(Unity在内部使用一系列整数索引来表示每个渲染队列,且索引号越小表示越早被渲染。)

++++Unity定义了5个渲染队列:BackgroundGeometryAlphaTestTransparentOverlay(当然在每个队列中间我们可以使用其他队列)。

++++Background】:1000:这个渲染队列会在任何其他队列之前被渲染,我们通常使用该队列来渲染那些需要绘制在背景上的物体。

++++Geometry】:2000:默认的渲染队列,大多数物体都使用这个队列。不透明物体使用这个队列。

++++AlphaTest】:2450:需要透明度测试的物体使用这个队列。(在Unity5中它从Geometry队列中被单独分出来,这是因为在所有不透明物体渲染之后再渲染它们会更加高效。)(Queue=AlphaTest

++++Transparent】:3000:这个队列中的物体会在所有GeometryAlphaTest物体渲染后,再按从后往前的顺序进行渲染。(任何使用了透明度混合(例如关闭了深度写入的Shader)的物体都应该使用该队列。)

++++Overlay】:4000:该队列用于实现一些叠加效果。(任何需要在最后渲染的物体都应该使用该队列。)

++++通过透明度混合来实现透明效果,代码结构如下:

SubShader{

    Tags {  Queue=Transparent  }

    Pass{

        ZWrite off    //立钻哥哥:关闭深度写入

        ....

    }

}

++2.4.5、透明度测试(Alpha Test

++++透明度测试(Alpha Test:只要一个片元的透明度不满足条件(通常是小于某个阀值),那么它对应的片元就会被舍弃。(被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它。)

++++通常,我们会在片元着色器中使用clip函数来进行透明度测试。(clipCG中的一个函数)

++++void clip(float4 x);】:参数:剪裁时使用的标量或矢量条件。

++++void clip(float3 x);

++++void clip(float2 x);

++++void clip(float1 x);

++++void clip(float x);

++++说明:如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色。

//立钻哥哥:透明度测试

Shader YanlzShaderDemo/AlphaTest_ShaderDemo01{

    Properties{

        _Color (Color Tint, Color) = (1,1,1,1)

        _MainTex (Main Tex, 2D) = white {  }

        _Cutoff (Alpha Cutoff, Range(0, 1)) = 0.5

    }

 

    SubShader{

        Tags {  Queue=AlphaTest IgnoreProjector=True RenderType=TransparentCutout  }

 

        Pass{

            Tags {  LightMode=ForwardBase  }    //立钻哥哥

 

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

 

            fixed4 _Color;

            sampler2D _MainTex;

            float4 _MainTex_ST;

            fixed _Cutoff;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float4 texcoord : TEXCOORD0;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

                float3 worldPos : TEXCOORD1;

                float2 uv : TEXCOORD2;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

 

                return o;

             }

 

            fixed4 frag(v2f i) : SV_Target{

                fixed3 wordNormal = normalize(i.worldNormal);

                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                fixed4 texColor = tex2D(_MainTex, i.uv);

    

                clip(texColor.a - _Cutoff);    //Alpha test

 

                fixed3 albedo = texColor.rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

 

                return fixed4(ambient + diffuse, 1.0);

            }

 

            ENGCG

        }

    }

 

    FallBack Transparent/Cutout/VertexLit;

}

++++为了在材质面板中控制透明度测试时使用的阀值,在Properties语义块中声明一个范围在[0,1]之间的属性:_Cutoff:【_Cutoff (Alpha Cutoff, Range(0, 1)) = 0.5 】:_Cutoff参数用于决定我们调用clip进行透明度测试时使用的判断条件。(它的范围是[0, 1],这是因为纹理像素的透明度就是在此范围内。)

++++Tags {  Queue=AlphaTest IgnoreProjector=True RenderType=TransparentCutout} 】:

--Queue=AlphaTest】:在Unity中透明度测试使用的渲染队列是名为AlphaTest的队列,因此需要把Queue标签设置为AlphaTest

--RenderType=TransparentCutout】:RenderType标签可以让Unity把这个Shader归入到提前定义的组(这里就是TransparentCutout组)中,以指明该Shader是一个使用了透明度测试的Shader。(RenderType标签通常被用于着色器替换功能。)

--IgnoreProjector=True】:这意味着这个Shader不会受到投射器(Projectors)的影响。

--通常,使用了透明度测试的Shader都应该在SubShader中设置这三个标签。

++++Tags {  LightMode=ForwardBase  }】:LightMode标签是Pass标签中的一种,它用于定义该PassUnity的光照流水线中的角色。(只有定义了正确的LightMode,才能正确得到一些Unity的内置光照变量,例如_LightColor0

++++材质面板中的Alpha cutoff参数用于调整透明度测试时使用的阀值,当纹理像素的透明度小于该值时,对应的片元就会被舍弃。(当我们逐渐调大该值时,立方体上的网格会逐渐消失。)

++++透明度测试得到的透明效果很“极端”:要么完全透明,要么完全不透明,它的效果往往像在一个不透明物体上挖了一个空洞。(得到的透明效果在边缘处往往参差不齐,有锯齿,这是因为在边界处纹理的透明度的变化精度问题。)

++++为了得到更加柔滑的透明效果,就可以使用透明度混合。


++2.4.6、透明度混合(Alpha Blending

++++透明度混合(Alpha Blending:透明度混合的实现要比透明度测试复杂一些,这是因为我们在处理透明度测试时,实际上跟对待普通的不透明物体几乎是一样的,只是在片元着色器中增加了对透明度判断并裁剪片元的代码。

++++透明度混合:这种方法可以得到真正的半透明效果。(它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。)

++++为了进行混合,我们使用Unity提供的混合命令:Blend。(BlendUnity提供的设置混合模式的命令。)(想要实现半透明的效果就需要把当前自身的颜色和已经存在于颜色缓冲中的颜色值进行混合。)

++++ShaderLabBlend命名:Blend OffBlend SrcFactor DstFactorBlend SrcFactor DstFactor, ScrFactorA DstFactorABlendOp BlendOperation等:

--Blend Off】:关闭混合。

--Blend SrcFactor DstFactor】:开启混合,并设置混合因子。(源颜色(该片元产生的颜色)会乘以SrcFactor,而目标颜色(已经存在于颜色缓存的颜色)会乘以DstFactor,然后把两者相加后再存入颜色缓冲中。)

--Blend SrcFactor DstFactor, SrcFactorA DstFactorA】:同上,只是使用不同的因子来混合透明通道。

--BlendOp BlendOperation】:并非是把源颜色和目标颜色简单相加后混合,而是使用BlendOperation对它们进行其他操作。

++++我们使用第二种语义:【Blend SrcFactor DstFactor】:这个命令在设置混合因子的同时也开启了混合模式。(这是因为,只有开启了混合之后,设置片元的透明通道才有意义,而Unity在我们使用Blend命令的时候就自动帮我们打开了。)

++++模型没有任何透明效果:因为没有在Pass中使用Blend命令,一方面是没有设置混合因子,更重要的是,根本没有打开混合模式。(我们会把源颜色的混合因子SrcFactor设为SrcAlpha,而目标颜色的混合因子DstFactor设为OneMinusSrcAlpha。)(经过混合后的新颜色是:DstColornew = SrcAlpha * SrcColor + (1-SrcAlpha) * DstColorold

//立钻哥哥:透明度混合实现模型透明效果

Shader YanlzShaderDemo/AlphaBlend_ShaderDemo02{

    Properties{

        _Color (Color Tint, Color) = (1,1,1,1)

        _MainTex (Main Tex, 2D) = white { }

        _AlphaScale (Alpha Scale, Range(0, 1)) = 1

    }

 

    SubShader{

        Tags {  Queue=Transparent IgnoreProjector=True RenderType=Transparent  }

 

        Pass{

            Tags {  LightMode=ForwardBase  }

 

            ZWirte off

            Blend SrcAlpha OneMinusSrcAlpha

 

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

        

            fixed4 _Color;

            sampler2D _MainTex;

            float4 _MainTex_ST;

            fixed _AlphaScale;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float4 texcoord : TEXCOORD0;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

                float3 worldPos : TEXCOORD1;

                float2 uv : TEXCOORD2;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

 

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                fixed3 worldNormal = normalize(i.worldNormal);

                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                fixed4 texColor = tex2D(_MainTex, i.uv);

                fixed3 albedo = texColor.rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

 

                return fixed4(ambient + diffuse, texColor.a * _AlphaScale);

            }

 

            ENDCG

        }

    }

 

    FallBack Transparent/VertexLit

}

++++Properties语义块中使用一个新的属性_AlphaScale。(_AlphaScale用于在透明纹理的基础上控制整体的透明度。)

++++与透明度测试不同,需要在Pass中为透明度混合进行合适的混合状态设置:

Pass{

    Tags { LightMode=ForwardBase  }

 

    ZWrite Off

    Blend SrcAlpha OneMinusSrcAlpha

    ....

}

--Pass的标签,即把LightMode设为ForwardBase,这是为了让Unity能够按前向渲染路径的方式为我们正确提供各个光照变量。

--把该Pass的深度写入(ZWrite)设置为关闭状态(Off)。

--开启并设置该Pass的混合模式:将源颜色(该片元着色器产生的颜色)的混合因子设为SrcAlpha,把目标颜色(已经存在于颜色缓冲中的颜色)的混合因子设为OneMinusSrcAlpha,以得到合适的半透明效果。


++2.4.7、开启深度写入的半透明效果

++++由于关闭深度写入而造成的错误排序的情况,一种解决方法是使用两个Pass来渲染模型:第一个Pass开启深度写入,但不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲中;第二个Pass进行正常的透明度混合,由于上一个Pass已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。(这种方法的缺点是:多使用一个Pass会对性能造成一定的影响。)

++++使用这种方法,可以实现模型与它后面的背景混合的效果,但模型内部之间不会有任何真正的半透明效果。

//立钻哥哥:开启深度写入的半透明效果

Shader YanlzShaderDemo/AlphaBlendZWrite_ShaderDemo03{

    ....

 

    SubShader{

        ....

    

        Pass{

            Zwrite On

            ColorMask 0

        }

 

        Pass{

            ....

        }

    }

 

    ....

}

++++这个新添加的Pass的目的仅仅是为了把模型的深度信息写入深度缓冲中,从而剔除模型中被自身遮挡的片元。

++++Pass第一行开启了深度写入。

++++Pass第二行使用了一个新的渲染命令:ColorMask

++++ShaderLab中,ColorMask用于设置颜色通道的写掩码(write mask):

--ColorMask RGB | A | 0| 其他任何RGBA的组合

--ColorMask设为0时,意味着该Pass不写入任何颜色通道,即不会输出任何颜色。(这正是我们需要的:该Pass只需写入深度缓存即可。)


++2.4.8ShaderLab的混合命令

++++混合还有很多其他用处,不仅仅是用于透明度混合。

++++混合的实现原理:当片元着色器产生一个颜色的时候,可以选择与颜色缓存中的颜色进行混合。(混合就和两个操作数有关:源颜色(source color)和目标颜色(destination color))(源颜色,用S表示,指的是由片元着色器产生的颜色值;目标颜色,用D表示,指的是从颜色缓冲中读取到的颜色值。对它们进行混合后得到的输出颜色,用O表示,它会重新写入到颜色缓冲中。)(混合中的源颜色、目标颜色、输出颜色,它们都包含了RGBA四个通道的值。)

++++Blend SrcFactor DstFactor】:开启混合,并设置混合因子。(源颜色(该片元产生的颜色)会乘以SrcFactor,而目标颜色(已经存在于颜色缓存的颜色)会乘以DstFactor,然后把两者相加后再存入颜色缓冲中。)

++++Blend SrcFactor DstFactor, SrcFactorA DstFactorA】:同上,只是使用不同的因子来混合透明通道。

++++ShaderLab中的混合因子:OneZeroSrcColorSrcAlphaDstColorDstAlphaOneMinusSrcColorOneMinusSrcAlphaOneMinusDstColorOneMinusDstAlpha

++++ShaderLab中的混合操作:AddSubRevSubMinMax、其他逻辑操作等。

++2.4.9、常见的混合类型

++++通过混合操作和混合因子命令的组合,可以得到一些混合模式中的混合效果。

++++Blend SrcAlpha OneMinusSrcAlpha】:正常(Normal),即透明度混合。

++++Blend OneMinusDstColor One】:柔和相加(Soft Additive)。

++++Blend DstColor Zero】:正片叠底(Multiply),即相乘。

++++Blend DstColor SrcColor】:两倍相乘(2x Multiply)。

++++BlendOp Min    Blend One One】:变暗(Darken)。

++++BlendOp Max    Blend One One】:变亮(Lighten)。

++++Blend OneMinusDstColor One】:滤色(Screen)。(等同于:【Blend One OneMinusSrcColor】)

++++Blend One One】:线性减淡(Linear Dodge)。

++++说明1:虽然上面使用MinMax混合操作时仍然设置了混合因子,但实际上它们并不会对结果有任何影响,因为MinMax混合操作会忽略混合因子。

++++说明2:虽然上面有些混合模式没有设置混合操作的种类,但是它们默认就是使用加法操作,相当于设置了BlendOp Add

++2.4.10、双面渲染的透明效果

++++在现实生活中,如果一个物体时透明的,意味着我们不仅可以透过它看到其他物体的样子,也可以看到它内部的结构。(透明度测试和透明度混合:无法观察到正方体内部及其背面的形状,导致物体看起来就好像只有半个一样。这是因为,默认情况下渲染引擎剔除了物体背面(相对于摄像机的方向)的渲染图元,而只渲染了物体的正面。)

++++如果我们想要得到双面渲染的效果,可以使用Cull指令来控制需要剔除哪个面的渲染图元。

++++Unity中,Cull指令的语法:【Cull Back/Front/Off

--如果设置为Back,那么那些背对着摄像机的渲染图元就不会被渲染,这也是默认情况下的剔除状态;

--如果设置为Front,那么那些朝向摄像机的渲染图元就不会被渲染;

--如果设置为Off,就会关闭剔除功能,那么所有的渲染图元都会被渲染,但由于这时需要渲染的图元数目会成倍增加,因此除非是用于特殊效果,通常情况是不会关闭剔除功能的。

++++透明度测试的双面渲染:只添加一行代码即可:

Cull Off    //Turn off culling

--这行代码的作用是关闭剔除功能,使得该物体的所有的渲染图元都会被渲染。(可以透过正方体的镂空区域看到内部的渲染结果)


++2.4.11、透明度混合的双面渲染

++++和透明度测试相比,想要让透明度混合实现双面渲染会更复杂一些,这是因为透明度混合需要关闭深度写入,而这是“一切混乱的开端”。(想要得到正确的透明效果,渲染顺序时非常重要的:我们想要保证图元是从后往前渲染的。)(对于透明度测试来说,由于我们没有关闭深度写入,因此可以利用深度缓冲按逐像素的粒度进行深度排序,从而保证渲染的正确性。)

++++关闭了深度写入,就需要小心地控制渲染顺序来得到正确的深度关系。(如果直接关闭剔除功能,就无法保证同一个物体的正面和背面图元的渲染顺序,就有可能得到错误的半透明效果。)

++++透明度混合的双面渲染的策略:把双面渲染分成两个Pass:第一个Pass只渲染背面,第二个Pass只渲染正面。(由于Unity会顺序执行SubShader中的各个Pass,因此我们可以保证背面总是在正面被渲染之后渲染,从而可以保证正确的深度渲染关系。)

//立钻哥哥:透明度混合的双面渲染

Shader YanlzShaderDemo/AlphaBlendWithBothSide_ShaderDemo04{

    Properties{

        _Color (Color Tint, Color) = (1,1,1,1)

        _MainTex (Main Tex, 2D) = white {  }

        _AlphaScale (Alpha Scale, Range(0, 1)) = 1

    }

 

    SubShader{

        Tags {  Queue=Transparent IgnoreProjector=True RenderType=Transparent  }

 

        Pass{

            Tags{  LightMode=ForwardBase  }

        

            Cull Front    //立钻哥哥:First pass renders only back faces

 

            ZWrite Off

            Blend ScrAlpha OneMinusSrcAlpha

 

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

 

            fixed4 _Color;

            sampler2D _MainTex;

            float4 _MainTex_ST;

            fixed _AlphaScale;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float4 texcoord : TEXCOORD0;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

                float3 worldPos : TEXCOORD1;

                float2 uv : TEXCOORD2;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP v.vertex);

                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

 

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target{

                 fixed3 worldNormal = normalize(i.worldNormal);

                 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                 fixed4 texColor = tex2D(_MainTex, i.uv);

                 fixed3 albedo = texColor.rgb * _Color.rgb;

                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

 

                 return fixed4(ambient + diffuse, texColor.a * _AlphaScale);

            }

 

            ENDCG

        }

 

        Pass{

            Tags{  LightMode=ForwardBase  }

 

            Cull Back    //立钻哥哥:Second pass renders only front faces

 

            ZWrite Off

            Blend SrcAlpha OneMinusSrcAlpha

 

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #include Lighting.cginc

    

            fixed4 _Color;

            sampler2D _MainTex;

            float4 _MainTex_ST;

            fixed _AlphaScale;

 

            struct a2v{

                float4 vertex : POSITION;

                float3 normal : NORMAL;

                float4 texcoord : TEXCOORD0;

            };

 

            struct v2f{

                float4 pos : SV_POSITION;

                float3 worldNormal : TEXCOORD0;

                float3 worldPos : TEXCOORD1;

                float2 uv : TEXCOORD2;

            };

 

            v2f vert(a2v v){

                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

 

                return o;

            }

 

            fixed4 frag(v2f i ) : SV_Target{

                fixed3 worldNormal = normalize(i.worldNormal);

                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                fixed4 texColor = tex2D(_MainTex, i.uv);

                fixed3 albedo = texColor.rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

    

                return fixed4(ambient + diffuse, texColor.a * _AlphaScale);

            }

 

            ENDCG

        }

    }

 

    FallBack  Transparen/VertexLit

}

++++在两个Pass中分别使用Cull指令剔除不同朝向的渲染图元。






##3章:Shader中级篇

++3章:Shader中级篇

++++3.1、更复杂的光照

++++3.2、高级纹理

++++3.3、让画面动起来







。。。未完待续。。。立钻哥哥。。。


#立钻哥哥Unity 学习空间: http://blog.csdn.net/VRunSoftYanlz/

++立钻哥哥推荐的拓展学习链接(Link_Url

++++立钻哥哥Unity 学习空间: http://blog.csdn.net/VRunSoftYanlz/

++++U3D_Shader编程(第一篇:快速入门篇)https://blog.csdn.net/vrunsoftyanlz/article/details/80372071

++++U3D_Shader编程(第二篇:基础夯实篇)https://blog.csdn.net/vrunsoftyanlz/article/details/80372628

++++设计模式简单整理https://blog.csdn.net/vrunsoftyanlz/article/details/79839641

++++U3D小项目参考https://blog.csdn.net/vrunsoftyanlz/article/details/80141811

++++UML类图https://blog.csdn.net/vrunsoftyanlz/article/details/80289461

++++Unity知识点0001https://blog.csdn.net/vrunsoftyanlz/article/details/80302012

++++Unity引擎基础https://blog.csdn.net/vrunsoftyanlz/article/details/78881685

++++Unity面向组件开发https://blog.csdn.net/vrunsoftyanlz/article/details/78881752

++++Unity物理系统https://blog.csdn.net/vrunsoftyanlz/article/details/78881879

++++Unity2D平台开发https://blog.csdn.net/vrunsoftyanlz/article/details/78882034

++++UGUI基础https://blog.csdn.net/vrunsoftyanlz/article/details/78884693

++++UGUI进阶https://blog.csdn.net/vrunsoftyanlz/article/details/78884882

++++UGUI综合https://blog.csdn.net/vrunsoftyanlz/article/details/78885013

++++Unity动画系统基础https://blog.csdn.net/vrunsoftyanlz/article/details/78886068

++++Unity动画系统进阶https://blog.csdn.net/vrunsoftyanlz/article/details/78886198

++++Navigation导航系统https://blog.csdn.net/vrunsoftyanlz/article/details/78886281

++++Unity特效渲染https://blog.csdn.net/vrunsoftyanlz/article/details/78886403

++++Unity数据存储https://blog.csdn.net/vrunsoftyanlz/article/details/79251273

++++Unity中Sqlite数据库https://blog.csdn.net/vrunsoftyanlz/article/details/79254162

++++WWW类和协程https://blog.csdn.net/vrunsoftyanlz/article/details/79254559

++++Unity网络https://blog.csdn.net/vrunsoftyanlz/article/details/79254902

++++C#事件https://blog.csdn.net/vrunsoftyanlz/article/details/78631267

++++C#委托https://blog.csdn.net/vrunsoftyanlz/article/details/78631183

++++C#集合https://blog.csdn.net/vrunsoftyanlz/article/details/78631175

++++C#泛型https://blog.csdn.net/vrunsoftyanlz/article/details/78631141

++++C#接口https://blog.csdn.net/vrunsoftyanlz/article/details/78631122

++++C#静态类https://blog.csdn.net/vrunsoftyanlz/article/details/78630979

++++C#中System.String类https://blog.csdn.net/vrunsoftyanlz/article/details/78630945

++++C#数据类型https://blog.csdn.net/vrunsoftyanlz/article/details/78630913

++++Unity3D默认的快捷键https://blog.csdn.net/vrunsoftyanlz/article/details/78630838

++++游戏相关缩写https://blog.csdn.net/vrunsoftyanlz/article/details/78630687

++++立钻哥哥Unity 学习空间: http://blog.csdn.net/VRunSoftYanlz/


--_--VRunSoft : Lovezuanzuan--_--

你可能感兴趣的:(shader编程)