前言
上个月读了《Unity Shader 入门精要》这本书,原本准备将学习笔记整理一下,作为博文发布出来,复习的时候检查了一下。发现很大程度上与书籍内容相同,有点抄书的嫌疑,想了想还是决定整理归纳一下,顺便查缺补漏,看看有没有被遗落的知识点,内容极其主观,特此声明。
基础部分:渲染流水线与Unity Sahder基础
这本书的基础部分主要是由渲染流水线的介绍和Unity Shader的基础写法构成的。这部分内容主要分布在全书的第2章与第3章。第一章主要介绍了一下全书内容结构,并没有涉及到太多的知识点。第4章主要介绍一些涉及图形学相关的数学知识,关于这部分内容,想要深入学习的同学可以去阅读一下相关书籍《3D游戏数学》。这本书详细介绍了3d游戏中,涉及图形学的数学概念,各个空间变换所需要的矩阵计算方法的底层原理等等。
渲染流水线
由于在大学期间学习过龙书(DirectX12 3D游戏开发实战),还跟着老师写过论文(混),所以对渲染流水线还是有着基本上的认识的,虽然DirectX12的渲染流水线与Unity的渲染存在一定程度上的差别,但是基础原理与专有名词还是相同的,对理解起来不会存在太大的难度。
在unity的渲染流水线中主要分为以下3个阶段:
分别是应用阶段,几何阶段以及光栅化阶段。其中应用阶段主要由CPU进行控制的,而几何阶段以及光栅化阶段则主要由GPU进行控制。
应用阶段
是CPU的工作阶段,由上图可以看出主要是负责将渲染图元(几何信息,顶点数据)提交给GPU并请求渲染。具体的工作这是:
- 准备好场景数据(摄像机位置、模型、光源等等),进行粗粒度剔除的工作(根据视锥体剔除不可见物体),将不可见物体CPU首先需要将所需要的数据加载进显存当中(大多数显卡缺少对内存的访问权限);
- 设置好渲染状态(告诉GPU这个渲染物体使用的材质,纹理,shader等等),会得到一个待渲染的几何信息,也就是
渲染图元
,在待渲染的图元列表中;
- 向GPU发送请求渲染命令,指向一个待渲染的图元列表,通知GPU进行渲染。
这一部分的大部分工作能够在Unity的图形化界面上进行操作,包括但不限于:调整场景中物体(顶点,法线)位置,设置纹理(颜色、透明度调整)等等⇒部分纹理属性需要再在shader中定义对应属性。除此之外还有一部分需要再unity的Shader文件中进行设置,例如渲染状态等等。而CPU每次提交渲染图元,向GPU发出的一个请求渲染的命令就是通常开发者们熟悉的DrawCall
。而通常对DrawCall的优化就是需要减少CPU发出的渲染命令,也就是减少图元列表,这通常也就需要使用到我们常说的合批
与图集
了,这里先不进行表述。
几何阶段
与光栅化阶段
也就是我们需要重点关注的部分。内部可以细分成多个阶段,如下表所示:
几何阶段
主要包括以下几个步骤:
顶点着色器:完全可编程。以单个顶点为单位进行处理,主要完成顶点坐标变换,逐顶点光照,计算顶点颜色等功能。
曲面细分着色器:完全可编程,可选。主要用来设置与生成高精度网格(细分图元)。
几何着色器:完全可编程,可选。进行逐图元的着色操作,或者产生更多的图元。
剪裁:可配置。(注意与CPU的粗粒度剔除的区别)将摄像机视锥体范围外的物体部分
剔除(产生新顶点替代边界)。
屏幕映射:不可配置,不可编程。将图元坐标由齐次剪裁空间转化到屏幕坐标系中。
光栅化阶段
主要包括以下几个步骤:
三角形设置:不可配置,不可编程。根据几何阶段输出的顶点信息,获取三角形每条边的像素坐标,得到三角形边界的表达方式。
三角形遍历:不可配置,不可编程。检查像素点是否被三角网格覆盖,并生成片元(包括屏幕坐标,深度,发现,纹理等信息)。
片元着色器:完全可编程。进行纹理采样,实现逐片元的着色操作。
逐片元操作:高度可配置。进行颜色修改,可见性测试(深度检测,模板检测),混合等操作。
- 模板检测:将片元模板缓冲区的模板值与设置的参考值比较,比较函数由开发者指定,判断是否通过模板测试。
- 深度检测:将片元深度缓冲区的深度值与片元的深度值比较,比较函数由开发者指定,判断是否通过深度测试。如果通过深度测试,才有覆盖深度缓冲区值的权利,开发者可以选择是否覆盖(开启/关闭深度写入)
- 等等等等
由上面介绍,可以看见有关于于完全可编程
,可配置
等等的字样。这些说明了开发者能够控制内部实现又或是能够配置对应阶段的相关属性,并且这些都是需要再Unity Shader文件中通过代码进行实现。在书中,后面章节主要通过使用了顶点着色器与片元着色器以及部分配置功能来实现效果,至于曲面细分着色器与几何着色器的使用,有兴趣的可以参考官网内容进行学习。
UnityShader文件主要通过类CG语言进行编写,由以上介绍可知,我们能够在顶点着色器,曲面细分着色器,几何着色器以及片元着色器中进行具体的效果实现,同时可以配置剪裁或逐片元操作的混合、深度写入等功能。
书中该部分内容还介绍了两大图形接口(OpenGL与DirectX),三大着色语言(HLSL、GLSL、CG)DrawCall,以及固定渲染管线的相关知识,与具体实现Shader相关效果联系不大,这里不做过多的介绍。
Unity Shader基础
该部分内容主要是书中第3章部分内容,主要介绍了UnityShader的整体结构。需要注意的是Shader是和材质相互绑定的,Unity中创建材质会自动绑定默认Shader,可以通过Unity界面上进行调整。
常见的UnityShader使用流程:
- 创建一个材质
- 创建一个unity shader,并赋给材质
- 将材质赋给要渲染的对象
- 在材质面板调整Unity Shader中的属性
需要注意的是,还存在其他情况的Shader使用情况,例如涉及到屏幕截取以及后处理等功能时(第12章),会通过代码创建材质并将相应Shader进行材质设置,不遵照上述使用流程。
除此之外,创建UnityShader文件还需要注意以下几种Unity的内置模板:
- Standard Surface Shader:一个包含标准光照模型的表面着色器模板。
- Unlit Shader:一个不包含光效,但是包含雾效的基本的顶点/片元着色器
- Image Effect Shader:一个用来处理各种屏幕后处理效果的模板
- Compute Shader(例外):利用GPU并行性进行一些与流水线无关计算的模板(白嫖计算量)
- Ray Tracing Shader:一个光线追踪shader
以下是2个常用Shader着色器的特点:
- 表面着色器:代码量少,渲染代价大(unity实现的时候需要转换成顶点/片元着色器,相当于上层),unity内部自动处理光照细节
- 不需要写Pass,最表层接口不关心(只实现什么纹理填充什么颜色,法线,使用什么光照模型等,进行表面渲染)
- 代码定义在CGPROGRAM与ENDCG之间
- 使用CG/HLSL语法
- 与光源打交道可以优先使用
- 顶点/片元着色器:更加复杂,灵活性高
- 代码定义在CGPROGRAM与ENDCG之间
- 需要写在Pass语义块中,需要自定义每个Pass使用的shader代码
- 使用CG/HLSL语法
- 需要许多自定义的渲染效果
- 和较少的光源打交道
在之后的学习的过程中主要采用创建使用了Unlit Shader模版(通过编写顶点/片元着色器实现具体特效效果,包括下列代码实例也是)。还有可能使用的Shader面板的相关设置:
- Default Maps:shader使用的默认纹理
- Show generated code:unity为shader模板(我们创建的)生成的顶点/片元着色器代码。
- Compile and show code(下拉列表):直接点击查看底层汇编指令。下拉列表查看不同图形接口生成编译的shader代码。
- 其余信息:渲染队列(Render queue)情况,批处理(Disable batching)情况、属性列表等
接下来就是Unity Shader代码文件的大概结构了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| Shader "ShaderName" { Properties { }
SubShader {
Pass{ [Name] [Tags] [RenderSetup] } Pass{ Name "MyPassName"
UsePass "MyShader/MYPASSNAME"
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
struct a2v { .... }; struct v2f { .... };
v2f vert(a2v v) { v2f o; ... return o; } fixed4 frag(v2f i) : SV_Target { ... } ENDCG } }
SubShader { .... } FallBack "VertexLit" }
|
以下是Properties语义块支持的属性类型:
以下是SubShader标签中的标签类型:
以下是SubShader状态的可设置类型:
以下是Pass中的标签可设置类型:
Pass的状态可设置类型与SubShader相同+固定渲染管线命令
相关资料
初级部分:基础光照与基础纹理
了解了有关于Unity的渲染流程以及UnityShader的基本结构,那么接下来就是实际操作了,接下来的初级篇分别是第6张介绍了光照的基本原理,第7章介绍了基础的纹理效果实现,以及第8张讲述了透明效果的实现。
基础光照
对于了解图形学光照原理的同学,这一部分很好理解的。基础光照本身是由四个部分构成,分别是以下四个部分:
- 自发光:
missive
,一个表面本身该向四周发射多少辐射量。如果没有使用全局光照技术,自发光的表面不会照亮周围物体,只是本身会显得更亮。直接使用材质的自发光颜色数据。
- 高光反射:
specular
,描述光线从光源照射到模型表面,该表面会以完全镜面反射散射多少辐射量。
- 漫反射:
diffuse
,描述光线从光源照射到模型表面,该表面会向每个方向散射多少辐射量。根据兰伯特定律
:反射光线的强度与表面法线和光源方向之间的夹角的余弦值成正比。
- 环境光:
ambient
,用来描述其他所有的间接光照。使用一个全局变量来进行模拟。
需要注意的,环境光与自发光都能够分别由场景中的平行光数据以及材质的数据直接获取。所以要完成基本光照在于实现漫反射与高光反射。
漫反射
首先是漫反射的原理主要根据兰伯特定律进行实现,是根据材质本身的颜色以及光源颜色向四周反射光线,以下是光照的计算公式,也是兰伯特模型
:
其中n是表面法线;I是指向光源的单位矢量,Mdiffuse是材质的漫反射颜色,Clight是光源颜色。0和点乘结果取最大值,避免物体被从后方来的光源照亮。
除了上述的兰伯特模型之外,还有一个半兰伯特模型也用来实现漫反射效果,这是因为兰伯特模型在处理模型背光的情况下会发现模型全黑的情况(模型细节完全丢失,看过去全是黑的,没有轮廓),这是因为在上述计算中法线与光源的余弦值如果是负数,就会统一变成0来限制背光被照亮的情况。但是也导致了背光没有漫反射效果。
为了解决这个问题,就采用了半兰伯特模型
来进行改进。对于法线与入射光线的余弦值的max处理修正为进行一个a倍的缩放以及一个b大小的偏移。通常情况下是0.5。这样映射范围就在[0,1]之间。
接下来就主要是根据UnityShader的实际结构实现漫反射光照了,这里加上一个完整的Shader文件进行展示,之后如果有同样的代码内容只展示原理实现部分:
- 漫反射效果=入射光线漫反射系数法线与入射光线的余弦值(具体公式上查)
- Max函数的写法⇒函数saturate(x),参数x是用于操作的标量或矢量,是float、float2、float3类型。能够将x截取在[0,1]范围内,如果x是矢量,就对每一个分量都进行一次该操作。
以下是基于顶点着色器实现的兰伯特漫反射光照:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-Level" { 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; fixed3 color : COLOR; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(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(worldNormal, worldLight));
o.color = ambient + diffuse;
return o; }
fixed4 frag(v2f i) :SV_Target{ return fixed4(i.color,1.0); }
ENDCG } } FallBack "Diffuse" }
|
由上述代码可以看出,兰伯特光照的计算填写在顶点着色器中,也就是说这里实现的是逐顶点光照。但如果使用片元着色器的话能够得到更好的效果。原因在于模型的细分程度,如果是顶点数量高的高细分模型,逐渐顶点关照能够表现出良好的效果,如果是低细分模型可能会存在一些视觉问题(锯齿等)。
以下是逐像素兰伯特漫反射的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
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); }
|
除了兰伯特模型之外,这里也能够进行半兰伯特效果的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| fixed4 frag(v2f i) : SV_Target { fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5; fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0); }
|
高光反射
高光反射主要根据两个高光反射模型进行实现的,分别是Phone高光反射模型与Bline高光反射模型。Phone模型主要根据光源的反射光线与视角方向之间的夹角
来确定颜色显示。而Bline高光反射模型主要是引入新的矢量h作为入射光线与视角方向的一个划分平均值,并通过他与法线方向之间的夹角
确定颜色。如下:
- Phone高光反射模型:
- 反射方向计算方法
- 高光反射计算方法
- Mglass是表面的光泽度,Mspecular是表面的高光反射颜色,控制高光反射的强度与颜色,Clight是光源的颜色与强度
- Bline高光反射模型 引入新的矢量h,作为入射光线l与出射光线v的一个平均值(使用时需要进行归一化变成单位矢量)
- 计算中间平均矢量的单位矢量
- 高光反射计算方法
- Mglass是表面的光泽度,Mspecular是表面的高光反射颜色,控制高光反射的强度与颜色,Clight是光源的颜色与强度
总而言之,高光反射效果=入射光线高光反射系数(视角方向与反射方向的余弦值)或(视角方向与平分矢量的余弦值)的物体表面光泽度的指数方(具体公式上查)
以下是Phone高光反射的逐顶点光照实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(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; }
|
以下是Phone高光反射的逐像素光照实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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); }
|
以下是blinn-Phone模型高光反射的逐像素光照实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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); }
|
以下是光照效果图:
Unity常用内置函数
基础纹理
Unity Shader的使用离不开纹理,所以第7章内容介绍了在unityShader中的纹理使用,分别介绍了单张纹理的使用,凹凸映射(法线)纹理的使用,渐变纹理的使用以及遮罩纹理的使用。
单张纹理的使用
首先是单张基础纹理的使用,首先需要定义Shader中纹理的属性,之后在Pass中声明属性存储之前定义的属性信息,设置好进出着色器的存储信息的结构体,之后再在顶点着色器中进行纹理采样即可。
重点在于顶点着色器中对纹理的偏移以及在片元着色器中对纹理的采样,可以使用unity内置的偏移函数TRANSFORM_TEX(纹理,偏移量)以及采样函数**tex2D(纹理,偏移UV)**。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| Shader "Unity Shaders Book/Chapter 7/Single Texture"{ 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 frafment frag #include "Lighting.cginc"
fixed4 _Color; sampler2D _MainTex; fixed4 _Specular; float _Gloss;
float4 _MainTex_ST;
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 = UnityObjectToClipPos(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;
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 "Diffuse" }
|
除了上述在UnityShader脚本中编写的代码之外,材质需要绑定Shader,根据Shader中的属性设置能够绑定纹理,但同时还需要注意一下纹理的界面设置:
- Texture Type:纹理类型
- Alpha Source:透明度选项来源(from Grayscale 来自像素灰度值生成,等等)
- Wrap Mode:贴图模式
- Repeat:纹理坐标超过1,整数部分舍弃,直接使用小数部分进行采样(纹理将会不断重复)
- Clamp:纹理坐标大于1,将会截取到1,如果小于0,将会截取到0(超出范围部分取纹理边界颜色)
- Filter Mode:滤波模式,一共有三种:Point,Bilinear,Trilinear。得到的图片显示效果依次提升。以下是三种效果:
- Point:采样像素点数目只有一个
- Bilinear:采用了线性滤波,每个像素目标会找周围4个邻近像素,通过线性插值混合之后最终像素。
- Trilinear:大致与Bilinear相同,只是多了多级渐远纹理之间的相互混合。
多级渐远纹理(mipmapping)技术(有点像lod技术,⇒Levels of Detail)
将原纹理提前使用滤波处理得到更小的图像,形成图像金字塔,每一层级都是上一层级的降采样效果。(需要多余空降进行降采样纹理存储,大致多耗费33%左右的空间)是一种空间换时间的方法。
Unity中使用方法:Texture Type选择Advanced,勾选Generate Mip Maps 即可。以下是渲染效果:
- 材质面版的选项是由shader中的属性条目进行设置的
- MainTex:纹理的缩放与偏移量
- Tiling(对应代码中的MainTex.xy):纹理缩放值
- Offset(对应代码中的MainTex.zw):纹理偏移值
- Render Queue:渲染队列,可以看一下渲染队列顺序那一篇博文(关于Unity渲染顺序)
- Enable GPU Instancing:高效渲染大量相似物体或粒子的技术。通过合并相同材质和属性的物体或粒子,并以单个绘制调用的方式发送给GPU,从而减少了CPU与GPU之间的数据传输和渲染开销。(降低draw call的手段之一)
- Gouble Sided Global Illumination:双面全局照明,指定光照贴图是否在计算全局光照时考虑几何体的两面。设置为 true 时,如果使用渐进光照贴图,则背面将使用与正面相同的发射和反照率来反射光。
凹凸映射(高度图,法线纹理)
实现凹凸映射的两种方法,但两者一般一起使用(丰富表面凹凸额外信息——光照):
- 高度映射:使用一张高度纹理来模拟表面位移,然后得到一个修改后的法线值
- 法线映射:使用一张法线纹理来存储表面法线
法线映射:
- 法线方向的分量范围是[-1,1],像素的分量范围是[0,1](以像素形式存储)。所以需要进行映射。公式如下: $pixel=(normal+1)/2$
- 在shader中对法线纹理采样之后还需要进行一次反映射,公式如下: $normal=pixel*2-1$
Unity内置函数能够提供这部分计算。
采用的法线纹理:
法线是矢量,所以法线的保存通常是需要找到是哪一个空间的矢量,才有意义:
- 模型空间的法线纹理:(以模型为中心)模型顶点自带的法线,是定义在模型空间中的,所以将修改后模型空间的表面法线存储在一张纹理中,称为模型空间的法线纹理。
- 切线空间的法线纹理(使用):(以顶点为中心)z轴为法线方向,x轴为顶点切线方向,y轴是x轴与z轴叉乘得到的,称为副切线或副法线。如图。
模型空降存储法线优点:
- 实现简单
- 纹理坐标的缝合处和尖锐的边角部分,突变(缝隙)减少,可以提供跟平滑的边界。
切线空间存储法线优点:
- 自由度高,模型空间下的法线信息是绝对法线信息,只能够用于创建他的模型。切线空间上的是相对法线信息,用在不用网格上能得到较为合理的结果。
- 可以进行UV动画,可以移动纹理的UV坐标实现凹凸移动的效果(水面),模型空间的法线纹理会完全错误,原因同上。
- 可以重用法线纹理,一个砖块,使用一张法线纹理就能够用到所有的6个面
- 可压缩,切线空间的法线纹理的法线的z方向总是正方向,因此能够仅存储xy,推导得到z。
以下是切线空间的凹凸渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| 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 }
Pass { ...... CGPROGRAM ......
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; float3 lightDir: TEXCOORD1; float3 viewDir : TEXCOORD2; };
v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(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; float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal); o.lightDir = mul(rotation, normalize(ObjSpaceLightDir(v.vertex))).xyz; o.viewDir = mul(rotation, normalize(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 }
|
以下是世界空间的凹凸渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| Shader "Unity Shaders Book/Chapter 7/Normal Map In World Space" { 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 = UnityObjectToClipPos(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 = UnityObjectToWorldNormal(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" }
|
- 注意,如果使用UnpackNormal函数来进行法线贴图的法线获取。需要将法线贴图设置成Normal map类型。这是因为设置成Normal map类型后,unity会根据不同平台对纹理进行压缩(如DZT5nm格式会省略z轴,通过xy轴求)。之后在通过UnpackNormal解压缩并获取正确的法线。
渐变纹理
以下是三种渐变纹理的区别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| Shader "Unity Shaders Book/Chapter 7/Ramp Texture" { 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 = UnityObjectToClipPos(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" }
|
- 在片元着色器中使用半兰伯特模型对法线方向与光照方向的点积进行一次半兰伯特计算,映射了显示范围。
- 个人推测,避免兰伯特光照造成的背光模型无变化(不符合渐变纹理显示效果),所以使用半兰伯特模型
- 由_RampTex纹理图片可以看出,这是一个横轴颜色逐渐变化,重轴颜色相同的纹理。所以使用半兰伯特部分构建一维纹理坐标,对_RampTex纹理进行采样。(fixed2(halfLambert, halfLambert))
- 需要注意:进行纹理采样的时候,需要将渐变纹理的Wrap Mode模式设置成Clamp模式,防止纹理采样由于浮点数精度造成的问题。
遮罩纹理
- 用来控制部分区域不同于其他区域的显示效果
- 遮罩纹理流程:通过采样获取遮罩纹理的值,使用其中某个(或某几个)通道的值与某种表面属性相乘(改通道值为0时,表面不受该属性影响)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
|
Shader "Unity Shaders Book/Chapter 7/Mask Texture" { 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, 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; 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 = UnityObjectToClipPos(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) * specularMask; return fixed4(ambient + diffuse + specular, 1.0); } ENDCG } } FallBack "Specular" }
|
以下是效果对比:
透明测试、混合与面剔除
透明效果是游戏中最常见的效果之一。在unity中,实现透明效果有2种方式,分别是完全透明的透明度测试以及会混合前后颜色,实现半透明效果的透明度混合。透明度测试相对简单,这也是因为透明度测试实现的是完全透明效果,不用关闭深度写入,效果极端;而透明度混合则相对复杂,主要原因还是因为关闭了深度写入,导致的一些由于渲染顺序或物体相互之间的层级引起的一些问题。所以首先我们要先来了解一下深度写入与渲染顺序的概念。
深度写入与渲染顺序
深度写入:用于判断物体遮挡情况下需要渲染的颜色。将深度数值写入深度缓冲区,在深度测试的时候,在颜色缓冲区将深度小的像素的颜色替换深度大的物体的深度颜色。这样就能够渲染出离摄像机更近的物体颜色。
- 关闭深度写入:需要注意的是,透明度混合是由
透明物体的颜色
以及透明物体之后的物体颜色
组合成的。其中透明物体之后的物体颜色存储在存储在颜色缓冲区。如果不关闭深度写入,在进行深度测试的时候,之后的物体由于深度更大,会因为深度写入将原本存储在颜色缓冲区的物体表面颜色数值剔除掉。就无法进行透明度混合了。(之后的物体颜色丢失)
- 简单来说,两个物体如果都有深度值,就会进行深度检测,导致颜色缓冲区的颜色被替代。
- 关闭深度写入,颜色缓冲区就有之后物体的颜色,与当前半透明物体的颜色混合,就有了混合效果的颜色。
- 渲染顺序:由于关闭了深度写入,所以无法判断物体里摄像机远近,所以渲染时需要注意物体的渲染顺序,先渲染不透明物体,在渲染半透明物体。以下是个渲染效果说明:
- 存在半透明物体A,在前;不透明物体B,在后。(都为半透明也同理)
- 正确渲染效果:先渲染物体B,再渲染物体A。不透明物体B开启了深度测试与深度写入,此时深度缓冲区没有数值,B将自己的深度写入深度缓冲区,颜色写入颜色缓冲区。再渲染半透明物体A,由于物体A关闭深度写入,只有深度检测。深度检测物体A里摄像机更近,读取颜色缓冲区B的颜色,与物体A颜色混合,达成半透明效果。
- 错误渲染效果:先渲染物体A,再渲染物体B。半透明物体A只开启了深度测试,没有深度写入,此时深度缓冲区没有数值也不写入,A将自己的颜色写入颜色缓冲区。再渲染不透明物体B,由于B开启了深度检测与深度写入。同时发现深度缓冲区内没有深度值,B会直接修改颜色缓冲区中的颜色(为B的颜色),A的颜色被覆盖丢失,最后屏幕上渲染出B的颜色(看起来B在A之前,也就是没有混合,只有B的颜色)。
至于Unity的渲染顺序,有需要的可以参考Unity渲染顺序探究 | 墨墨辰的旋转小屋 (the-black-sun.github.io)的简要说明。需要注意的是:
- 先渲染所有不透明物体,并开启深度测试与深度写入
- 将半透明物体安距离摄像机远近排序,然后从后往前渲染,并开启深度测试,关闭深度写入
存在的问题
虽然有深度测试与渲染顺序的调整存在,但是仍然存在一个问题:单个重叠物体或多个相互重叠物体会导致混合效果错误。存在物体相互堆叠的情况,存在物体的部分区域的渲染前后排序顺序不一致的情况,但是调整渲染顺序无法解决这一个问题。因为根据深度进行渲染顺序调整是以物体为单位进行的,而深度测试与深度写入是以像素点为单位进行的。面对这种情况,UnityShader通常使用两个Pass进行解决(一个进行混合操作,一个进行深度写入,注意关闭深度写入的相关操作也在Pass中进行)。
UnityShader的渲染队列
上述由于物体堆叠导致的渲染顺序问题,Unity通过渲染队列来进行解决。SubShader的Queue
标签可以决定模型改归于哪一个渲染队列,索引号越小表示越早被渲染。渲染队列有以下几种:
名称 |
队列索引号(RenderQueue) |
描述 |
Background |
1000 |
该队列在其他所有队列之前被渲染,通常用来渲染需要被绘制在背景上的物体 |
Geometry |
2000 |
默认渲染队列,大多数物体使用的队列,不透明物体使用的队列 |
AlphaTest |
2450(小于2500为完全不透明) |
需要进行透明度测试的物体使用的队列。 |
Transparent |
3000 |
该队列中的物体会在前两个队列(Geometry与AlphaTest)渲染之后,再根据从后往前的顺序进行渲染,所有使用了透明度混合的物体使用该队列。 |
Overlay |
4000 |
该队列用来实现叠加效果,需要在最后渲染的物体使用这个队列 |
以下是UnityShader中的渲染队列设置:
- 透明度测试
1 2 3 4 5 6
| SubShader{ Tags{"Queue"="AlphaTest"} Pass{ ...... } }
|
- 透明度混合
1 2 3 4 5 6 7
| SubShader{ Tags{"Queue"="Transparent"} Pass{ ZWrite Off ...... } }
|
接下来来看一下透明度测试与透明度混合的具体效果实现。
透明度测试
实现的是完全透明或完全不透明效果。需要用到一个由CG提供的透明度测试函数clip,如下所示:
- 函数:void clip(floatX x)⇒
X代表数字无~4
- 参数:剪裁时使用的标量或矢量条件
- 描述:如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色
1 2 3 4
| void clip(float4 x){ if(any(x<0)) discard; }
|
以下是透明度测试的部分代码:没有通过透明度测试的像素直接舍弃掉(discard)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| Shader "Unity Shaders Book/Chapter 8/Alpha Test" { Properties { .... _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5 } SubShader { Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"} Pass { .... fixed _Cutoff; .... fixed4 frag(v2f i) : SV_Target { ....
fixed4 texColor = tex2D(_MainTex, i.uv); clip (texColor.a - _Cutoff); .... }
.... } } FallBack "Transparent/Cutout/VertexLit" }
|
透明度混合
透明度混合能够实现真正的半透明效果。透明度混合需要通过Blend命令进行逐片元透明混合设置,主要的Blend命令如下:
由上图可以看出第2种和第3种命令存在着混合因子这个概念,这个混合因子是为了控制两种颜色的混合比例而产生的。其中第2种混合命令,提供两个混合因子用来控制两个混合颜色的混合比例,RGB与A通道的混合因子相同;第3种提供了四个混合因子,多出来的两个分别用来里控制两种颜色的透明度通道,RGB与A通道的混合因子不同。
同时以下是SubShader中支持的混合因子:
除此之外还有SubShader支持的混合操作(前后两种颜色混合方式)以及常见的混合类型,通过BlendOp命令
进行修改:
操作 |
描述 |
Add |
将混合后的源颜色与混合后的目的颜色相加。是默认的混合操作,等式如下:O=SrcFactorS+DstFactorD |
Sub |
将混合后的源颜色减去混合后的目的颜色。O=SrcFactorS-DstFactorD |
RevSub |
将混合后的目的颜色减去混合后的源颜色。O=DstFactorD-SrcFactorS |
Min |
使用源颜色和目的颜色中较小的值,每个分量都进行比较。 |
Max |
使用源颜色和目的颜色中较大的值,每个分量都进行比较。 |
其他逻辑操作 |
仅仅DX11支持 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| Blend SrcFactor DstFactor Blend SrcFactor DstFactor,SrcFactorA DstFactorA
Blend SrcAlpha OneMinusSrcAlpha
Blend OneMinusDstColor One
Blend DstColor Zero
Blend DstColor SrcColor
BlendOp Min Blend One One
BlendOp Max Blend One One
Blend OneMinusDstColor One
Blend One OneMinusSrcColor
Blend One One
|
下面是最常见的混合计算公式(一般而言是按透明度进行混合的,但是可以根据混合命令进行设置):
- DstColor:颜色缓冲区的颜色
- SrcAlpha:原颜色的混合因子:SrcFactor。可能是原颜色的透明度。
- SrcColor:原颜色
- DstColor:目标颜色;原本颜色缓冲区颜色
- (1-SrcAlpha):DstFactor,目标颜色的混合因子
- 注意:修改公式内容可以得到其他类型的混合公式与混合效果
以下是透明度混合的部分部分UnityShader代码设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| Shader "Unity Shaders Book/Chapter 8/Alpha Blend" { Properties { .... _AlphaScale ("Alpha Scale", Range(0, 1)) = 1 } SubShader { Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} Pass { .... ZWrite Off Blend SrcAlpha OneMinusSrcAlpha .... fixed _AlphaScale; .... fixed4 frag(v2f i) : SV_Target { .... return fixed4(ambient + diffuse, texColor.a * _AlphaScale); } ENDCG } } FallBack "Transparent/VertexLit" }
|
开启深度写入的半透明效果
还记得之前在存在的问题
小节中提到的问题吗?不同物体部分区域前后位置不一致导致的混合效果错误现象,是由于深度写入关闭的原因,解决方法就是开启深度写入了(- -“)。之前说了,之所以关闭深度写入是因为避免深度写入之后,将颜色缓冲区的旧颜色(目标颜色,后面物体的颜色)替换成深度较浅的前面物体的颜色。所以这里说的开启深度写入指的是将深度写入深度缓冲区,但是不进行包括颜色替换的其他任何操作。在实际实现中表现为单独使用一个Pass进行深度写入操作。部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| Shader "Unity Shaders Book/Chapter 8/Alpha Blend" { Properties { ....
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1 } SubShader { Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} Pass { ZWrite On
ColorMask 0 }
Pass { Tags { "LightMode"="ForwardBase" } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha .... } } FallBack "Transparent/VertexLit" }
|
PS:这种深度写入能够完成不同顺序的物体之间的混合效果,但是在同一物体的遮挡混合中不能够实现混合,具体效果可以查看下方效果展示中的透明度混合,深度写入
,除此之外,在透明效果中还需要注意透明应该需要能够看见当前透明物体的内部结构,这就需要使用到剔除的功能了,同样经过测试,开启了深度写入的同物体也不能展示双面不被剔除的显示效果(也就是关闭剔除之后,开启深度写入,没办法展示出双面
的效果,反而与开启剔除效果相同),具体原因待探究。
双面渲染的透明效果
如果物体有透明效果。那么不但应该能够透过它看见其他物体的样子,还应该能够看见物体的内部结构。但是以上的渲染效果并不会实现这些功能,导致渲染的透明或半透明物体看上去只是半个物体(背面的半个消失了,如下图)
这是因为物体渲染的时候开启了剔除(cull)功能,并且默认设置成了Back。让背对摄像机,摄像机看不见的部分不进行渲染。所以造成了透明效果的穿帮。
- Cull Back:开启背面剔除
- Cull Front:开启正面剔除
- Cull Off:关闭剔除
以下是透明度测试的双面剔除的部分代码,效果展示可以再本节最下方查看:
1 2 3 4 5 6 7 8
| Pass { Tags { "LightMode"="ForwardBase" } Cull Off ......
}
|
透明度测试的剔除很简单,但是透明度混合不能够向透明度测试一样操作。这是因为透明度混合关闭了深度写入,为了保证同一个物体的渲染顺序,需要分成两个Pass分别进行正面剔除的渲染与背面剔除的渲染。部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| Shader "Unity Shaders Book/Chapter 8/Alpha Blend With Both Side" { Properties { .... _AlphaScale ("Alpha Scale", Range(0, 1)) = 1 } SubShader { Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} Pass { Tags { "LightMode"="ForwardBase" } Cull Front ZWrite Off Blend SrcAlpha OneMinusSrcAlpha ...... } Pass { Tags { "LightMode"="ForwardBase" } Cull Back ZWrite Off Blend SrcAlpha OneMinusSrcAlpha ...... } } FallBack "Transparent/VertexLit" }
|
本章实现的透明效果如下:
问题探究
等unityShader总结全部整理好之后,会归纳一下可能不太熟悉的知识点或问题,看看能不能出个问题总结。这里先记录一下这部分内容学习遇上的问题,各位大佬有了解的也可以尝试在评论区进行评论。
- 透明混合效果的阴影实现
- 开启深入写入的本物体无法进行透明混合,原因与是否有解决方法
- uv坐标与tex2d()的采样原理
- UnityShader中定义的属性存储语义(这里指的是结构体中的POSITION等等)以及属性(这里指的是属性)相关的声明类型。