| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 游戏开发 -> Unity Shader入门 -> 正文阅读 |
|
[游戏开发]Unity Shader入门 |
文章目录前言本文对应《Unity Shader入门精要》一书的初级篇,为自己的学习笔记与思考补充。 环境Unity版本:2020.1.6f1c1 编写shader:VS2019+ShaderlabVS 其中ShaderlabVS为VS的一个插件,安装地址: Unity的帧调试器位置:Window->Analysis->Frame Debugger 在Frame Debug所显示的所有事件的树状图中,每个叶子节点就是一个事件,右边不带数字;而每个父节点右侧带数字,代表该节点下的事件数目。 比如图中的Camera.ImageEffects右侧显示数字为2,表示其中包含两个事件,分别是Resolve Color与Draw Dynamic 以Draw开头的事件通常是一个Draw Call;当单机某个事件的时候(如上图点击了Draw Mesh)右侧的窗口中就会显示出该事件的细节(比如图中告诉了使用了哪个shader、剔除方式是背面剔除之类的),同时在Game窗口里也可以看到对应的效果。 值得一提的是,帧调试器实际上并没有实现一个真正的帧拾取(frame capture)的功能,而是仅仅使用停止渲染的方法来查看渲染事件的结果,所以得到的信息也就相对有限。所以有时还需要一些外部工具来辅助使用,比如RenderDoc Unity Shader 概述Unity中的材质需要结合一个GameObject的Mesh或者Particle Systems组件来工作,而shader则必须要和材质结合起来才能工作。 一个常见的流程:
这里我们讲的Unity Shader指的是硬盘上的 .shader 文件。实际上Unity Shader != 真正的shader,而是用Unity自己定义的shaderlab语言去写的,其实际上就是对整个渲染过程的一层抽象,开发者只需要和Unity Shader也就是ShaderLab语言去打交道,Unity会在背后根据所使用的平台来把你所编写的 .shader 文件编译成真正的代码和 Shader 文件。
比如官方提供的一个Unlit Shader模板:
详解基于Unity编写的Blinn-Phong Shader
为了得到更加原始的效果,我们选择去掉这个天空盒。我们在Window->Rendering->Lighting中把Skybox Material选为None即可。 然后我们编写shader代码如下:
接下来进行逐行拆解: 每个Unity Shader第一行都需要通过一个字符串去定义这个shader的名字,反斜杠是为了控制在材质面板中的位置,比如这里: 之后的Properties则是声明一系列属性以在材质面板中显示调整,比如这里我的_Diffuse 、_Specular 、_Gloss,就可以在材质面板进行调整: 为了使用这些属性(在Cg代码中访问它),在后面我们仍需在 SubShader 的pass块中定义出来:
这里变量的名称和类型必须与Properties语义块中的属性定义相匹配。 比如这里颜色我们常用fixed4,控制高光区域大小的gloss我们用float。 接着往下,我们到了SubShader语义块。每一个Unity Shader文件可以包含多个SubShader语义块,但至少要有一个。 当Unity需要加载这个Unity Shader时,就会去扫描所有的SubShader语义块,然后选择第一个能够在目标平台运行的SubShader。如果都不支持的话就会去使用Fallback语义指定的Unity Shader。 因此可以知道,这里的FallBack就是为了“留一条后路”。 事实上FallBack还会影响阴影的投射。为每个Unity Shader正确设置Fallback是非常重要的。
再往下到了pass块。SubShader也可以定义标签Tags、LOD、渲染状态RenderSetup,当然也可以不定义。而一个pass则对应一次完整的渲染流程。 因此SubShader定义的标签是描述其内所有pass的,比如渲染顺序Queue,而pass中的标签则是对应这一趟渲染流程的,比如LightMode。 再往下,我们写了 CGPROGRAM 与结尾的 ENDCG,顶点/片元着色器代码需要定义在这之间,表明这之间的代码使用CG/HLSL去编写的。 这两行是为了用#pragma指令来告诉Unity我们定义的顶点着色器和片元着色器叫什么名字:
#include “Lighting.cginc” 是Unity的内置文件,这些内置文件的后缀都是 .cginc ,常见的还有 UnityCG.cginc, 是为了使用一些非常有用的变量和帮助函数。 之后的:
定义了两个结构体,分别表示从应用层输入到顶点着色器 和 从顶点着色器输入到片元着色器的结构体,v2f表示vertex to fragment,这里的命名我都是去模仿unity官方提供的模板的,同样的模仿还有大括号风格。 这里的POSITION、NORMAL、SV_POSITION等都是Cg/HLSL中的语义(semantics),它们是不可省略的,这些语义将告诉系统用户需要哪些输入值,以及用户的输出是什么。这样渲染器就知道用户的输入输出是什么,以便后续的插值等操作。 比如这里的POSITION表示要把模型的顶点坐标填充到参数vertex中,法线向量填充到normal 中,SV_POSITION则告诉Unity顶点着色器输出的是裁剪空间中的顶点坐标,之后渲染引擎就会把SV_POSITION修饰的变量经过光栅化后显示到屏幕上,因此这些语义描述的变量不可随便赋值。这里SV表示system-value,即系统语义。后面片元着色器的SV_Target也是HLSL中的一个系统语义,是为了告诉渲染器要把用户的输出颜色存储到一个渲染目标(render target)中,这里将输出到默认的帧缓存中。 接着来看顶点着色器:
首先定义要输出的结构体 v2f o,接着把输入的模型坐标用unity内置函数UnityObjectToClipPos作用转换到裁剪空间坐标(以前是mul(UNITY_MATRIX_MVP,*)),并赋值给o.pos;接着是法线的变换,使用内置函数UnityObjectToWorldNormal;世界空间下的顶点坐标则是unity_ObjectToWorld与v.vertex相乘,即从模型空间转换到世界空间。 接着看片元着色器:
环境光ambient使用了UNITY的内置变量UNITY_LIGHTMODEL_AMBIENT,worldNormal世界坐标的法向量则是把传进来的参数标准化一下,worldLightDir世界坐标下的光线方向则是用UnityWorldSpaceLightDir实现的,UnityWorldSpaceLightDir仅可用于前向渲染中,这个函数的输入是一个世界空间中的顶点位置(比如这里为i.worldPos),输出为世界空间中该点到光源的光照方向。没有被归一化,所以这里还用了normalize归一化了一下。 接着计算diffuse,为了防止法线和光源方向点乘结果为负(防止物体被后面来的光源照亮),这里我们与0取了max,接着乘以光源颜色和漫反射颜色。 接着计算高光反射,为了使用Blinn-Phong模型,我们用UnityWorldSpaceViewDir得到该顶点到观察方向的向量viewDir(世界坐标下),用worldLightDir和viewDir相加得到半程向量,然后用公式
最后返回像素的颜色值:
注: 纹理纹理面板与属性解析
这里还展示了平铺(Tiling)属性,如上图为(3,3),而为(1,1)即原纹理则如下: 既然说到Mipmap,一般我们纹理都是用2的幂大小,Format决定Unity内部使用哪种格式来存储纹理。Advanced内可以选择: 代码使用纹理Properties中声明: 之后的pass中则需要声明:
与之前的Properties内的属性不同,这里我们还声明了_MainTex_ST,_MainTex_ST不是随便起的,Unity中使用纹理名_ST的方式来声明某个纹理的属性。ST表示scale和translation,即缩放和平移。_MainTex_ST.xy存储缩放值,_MainTex_ST.zw存储偏移值。 比如之后我们可以在顶点着色器中写代码:
这一行等效于:
这样才能正确使用面板中的Tilling和Offset 凹凸映射纹理的另一常见应用是凹凸映射(bump mapping)。有两种主要方法:
由于法线分量范围[-1, 1]而像素分量范围[0, 1],因此会做一个映射。 法线纹理还分模型空间下和切线空间下,如图: 切线空间的z轴是顶点的法线方向,x轴为切线方向,y轴为二者作叉积得到(y轴也被称为副切线bitangent或副法线),因此每个顶点都有自身的切线空间,而实际上切线空间下的法线纹理所存的是一个法线的扰动方向。要是一个点的法线方向不变,那么对应在切线空间就是(0, 0, 1),映射后所存的就是(0.5,0.5,1),就是浅蓝色。因此看上去会有大片蓝色,其实就是说明顶点的大部分法线和模型本身法线是一样的,不需要改变(偏移)。 由于法线是单位向量,且切线空间下法线的z分量始终为正,即法线纹理的第三个通道的值可以由前两个通道推导出来,因此法线可以进行如DXT5nm格式去压缩。使用时再针对不同的压缩格式去对法线纹理进行正确的采样(Unity内置UnpackNormal函数)。 使用切线空间下的法线纹理有如下优点:
实际上,法线本身可以存储在任意坐标系,得到法线是为了后续的光照计算。在这里我们采用切线空间,在光照计算中则有两种手法:
注意:这里涉及坐标系变换,从效率而言第一种手法优于第二种,但是从通用性而言第二种更好(因为我们有时需要在世界空间下进行一些计算)。 渐变纹理使用一张纹理(渐变纹理)去控制漫反射光照的结果。 需要注意的是,这里我们需要把渐变纹理的Wrap Mode设置为Clamp模式,以防止对纹理进行采样时由于浮点数精度而造成的问题。 遮罩纹理mask texture 什么是遮罩(mask)呢?简言之,遮罩允许我们可以保护某些区域,使他们免于某些修改。 使用遮罩纹理的一般流程是:通过采样得到遮罩纹理的纹素值,然后使用其中某个或某几个通道的值(比如texel.r)来与某种表面属性进行相乘,这样在该通道值为0的时候就可以保护表面不受该属性的影响。 注:
透明效果alpha test与alpha blending在unity中有两种方法来实现透明效果:
而实际上,对于Cg中的函数clip等同于如下伪代码:
透明度测试不需要关闭深度写入(即仍然可以把深度值更新到深度缓冲中)。和其他不透明物体最大的不同就是会根据透明度来舍弃一些片元。因此产生的效果要么完全透明(即看不到),要么完全不透明。 通常,使用alpha test的shader都应该在subshader中设置这三个标签: 在片元着色器中我们开启透明度测试:
这里_Cutoff是我们定义在Properties的参数:
最后的回调:
这保证了使用透明度测试的物体可以正确地向其他物体投射阴影。 测试结果:
混合是一个逐片元的操作,它不是可编程的,但却是高度可配置的。 为了进行混合,我们需要使用unity提供的混合命令——Blend。
详细描述可见书的P169. 通常,使用alpha blending的shader都应该在subshader中设置这三个标签: 这里我们使用Blend SrcFactor DstFactor来进行混合,这个命令在设置混合因子的同时也开启了混合模式。而只有使用Blend命令打开混合后,我们在这里设置透明通道才有意义,否则这些透明度并不会对片元的透明效果有任何影响。 Blend SrcFactor DstFactor:我们会把源颜色(该片元产生的颜色)乘以SrcFactor,而目标颜色(已经存在于颜色缓存的颜色)会乘以DstFactor。 而对于Blend SrcFactor DstFactor, SrcFactorA DstFactorA,其实就是区分了RGB通道的混合因子和Alpha通道的混合因子,这里SrcFactor、DstFactor为RGB通道的混合因子,而SrcFactorA 、DstFactorA为Alpha通道的混合因子。 当设置混合状态时,我们实际上设置的是混合等式(即从源颜色和目标颜色得到输出颜色的等式)的操作和因子,而一般操作都是默认加操作,否则可以用混合操作命令 BlendOp BlendOperation 改为其他操作。因此很多时候我们只需要设置混合因子即可。 一个例子:
关于混合操作和混合因子的更多设定可以看书本P174,这里仅放一个结果: 渲染顺序的重要性由于半透明物体关闭了深度写入,也就破坏了深度缓冲的机制,这是一个非常糟糕的事情。关闭深度写入也使得渲染顺序变得无比重要。 因此渲染引擎一般都会先对物体排序再渲染,常用的方法是:
然而即使是这样,仍然会有情况发生错误,比如下图: 这种问题的解决方法通常是分割网格。为了减少错误排序的方法,我们可以尽可能让模型是凸面体,并且尽量考虑将复杂的模型拆分成可以独立排序的多个子模型等。其实就算排序错误结果有时也不会非常糟糕,如果不想分割网格,还可以试着让透明通道更加柔和,使穿插看起来并不是那么明显。我们也可以使用开启了深度写入的半透明效果来近似模拟物体的半透明。 渲染队列unity为了解决渲染顺序问题提供了渲染队列(render queue)这一解决方案。可以用ySubShader的Queue标签来决定我们的模型将归于哪个渲染队列。这一部分在书的165页。 官方文档: 开启深度写入的半透明效果如之前所述,半透明效果需要Alpha Blending,但是由于不开启深度写入,在半透明对象之间遮挡时可能会出现错误: 这里我们的解决方法是两趟pass:
这里第一行ZWrite On开启了深度写入,第二行ColorMask 0意味着该Pass不写入任何颜色通道,即不会输出任何颜色。因此该Pass仅写入深度缓存。 ColorMask在ShaderLab中用于设置颜色通道的写掩码(write mask),它的语义如下:
第二个Pass则进行正常的透明度混合即可。 由于第一个Pass已经得到了逐像素的正确的深度信息,第二个Pass就可以按照像素级别的深度排序结果进行透明度渲染。 当然这样做缺点是多了一个Pass,影响性能,但是效果还是不错的: 双面渲染的透明效果由于默认情况下渲染引擎剔除了物体背面(相对于摄像机的方向)的渲染图元,而只渲染了物体的正面。因此如果我们想要得到双面渲染的效果,可以使用Cull指令来控制需要剔除哪个面的渲染图元。 Unity中Cull指令的语法:
透明度测试的双面渲染只需要在Pass中使用 Cull Off 去掉剔除即可: 透明度混合的双面渲染由于此时同一个物体正面和背面还有一个渲染顺序,所以不能简单地Cull Off,我们这里采用两个Pass: |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年3日历 | -2025/3/26 4:54:42- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |