【Unity3D Shader编程】之四 热带雨林篇: 剔除、深度测试、Alpha测试以及基本雾效合辑

 

本文介绍了Unity中Shader书写中会用到的剔除、深度测试、Alpha测试以及基本雾效相关的语法知识,然后以6个Shader的书写作为实战内容,最后创建了一个生机勃勃的热带雨林场景进行了Shader的测试。依旧是国际惯例,先上本文配套程序的截图。

绿色的海洋:

 

满眼的生机:

 

竹林:

 

 

参天大树:

 

 

飘到脸上的树叶:

 

 

 

 

OK,图先就上这么多。文章末尾有更多的运行截图,并提供了原工程的下载。可运行的exe下载在这里:

 

【可运行的exe游戏场景请点击这里下载试玩】

 

 

好的,我们正式开始。

 

 

 

 

一、剔除与深度测试(Culling & Depth Testing)相关内容

 

1.1 剔除(Culling)的概念

 

对于实时交互的3D环境而言,现实的速度和效率是非常重要的。虽然现在的硬件能力非常的快,但是要想保持30FPS的同时处理数十万的三角形,以现在的主流机器来说还是有些困难的。

为了解决这种问题,人们提出了很多方法,其中有LOD,有Culling。这两种方法并不矛盾,而且我们往往需要在culling的基础上再使用LOD进一步解决pipeline的负担。

剔除是一种通过避免渲染背对观察者的几何体面来提高性能的优化措施。所有几何体都包含正面和反面。剔除基于大多数对象都是封闭的事实;如果你有一个立方体,你不会看到背离你的那一面(总是只有一面在你的前方),因此我们不需要绘制出背面。因此也被称做背面剔除。

一言以蔽之,所谓剔除,就是被挡住或视角以外的我们看不到的物体,因为它们无关紧要,所以我们就不去绘制,以节省资源,提高场景的运行效率。

 

 

 

1.2 深度测试(Depth Testing)的概念

在复杂的场景中,通常有多个物体需要绘制,这些物体之间通常会存在遮挡关系,离观察点较远的物体会因为近处物体的者的遮挡而不可见或只有部分可见,Direct3D图形系统提供了深度测试功能来实现这种效果。深度测试可以简化复杂场景的绘制,确保只有场景内的对象的最靠近的表面参与绘制。

 

浅墨之前在写DirectX相关博文的时候写过对深度测试的形象化理解,在这边也列出来吧:

把深度测试看做在一口井的井口处向井中观望。把所有物体都赋予一个深度值,放到井中来显示。深度越深的物体,离井口就越远。深度越浅的物体,离井口就越近。井表面的深度值为0。离井口近而深度浅的物体,可能会把离井口远的物体遮挡住。最终显示在屏幕上的开启深度测试后的画面,就如在井口处向井中观望里面物体显示出的遮挡与层次的效果一样。当然,离井口的深度就是每个物体在世界坐标系中的矩阵的Z坐标值了。

 

 

 

1.3 剔除与深度测试(Culling & DepthTesting)相关句法

 

语句之一:Cull Back | Front| Off

此语句用于控制几何体的哪一面会被剔除(也就是不被绘制) 。其中:

  • Cull Back—— 不绘制背离观察者的几何面
  • Cull Front—— 不绘制面向观察者的几何面,用于由内自外的旋转对象
  • Cull Off —— 显示所有面。用于特殊效果。

 

语句之二:ZWrite On | Off

此语句用于控制是否将来之对象的像素写入深度缓冲(默认开启),如果需要绘制纯色物体,便将此项打开,也就是写上ZWrite On。如果要绘制半透明效果,关闭深度缓冲,则用ZWrite Off。

语句之三:ZTest Less |Greater | LEqual | GEqual | Equal | NotEqual | Always

此语句用于控制深度测试如何执行。

缺省值是LEqual (绘制和存在的对象一致或是在其中的对象;隐藏其背后的对象),含义列举对应如下:

 

Greater 只渲染大于AlphaValue值的像素
GEqual 只渲染大于等于AlphaValue值的像素
Less 只渲染小于AlphaValue值的像素
LEqual 只渲染小于等于AlphaValue值的像素
Equal 只渲染等于AlphaValue值的像素
NotEqual 只渲染不等于AlphaValue值的像素
Always 渲染所有像素,等于关闭透明度测试。等于用AlphaTest Off
Never 不渲染任何像素

 

 

语句之四:Offset Factor ,Units

此语句用两个参数(Facto和Units)来定义深度偏移。

  • Factor参数表示 Z缩放的最大斜率的值。
  • Units参数表示可分辨的最小深度缓冲区的值。

于是,我们就可以强制使位于同一位置上的两个集合体中的一个几何体绘制在另一个的上层。例如偏移量Offset 设为0, -1(即Factor=0, Units=-1)的值使得靠近摄像机的几何体忽略几何体的斜率,而偏移量为-1,-1(即Factor =-1, Units=-1)时,则会让几何体偏移一个微小的角度,让观察使看起来更近些。

 

 

 

二、Alpha测试(Alpha testing)相关内容讲解

2.1 Alpha测试的概念

 

Unity中,Alpha测试(Alpha Testing)是阻止像素被写到屏幕的最后机会。在Pineline中Alpha测试的位置如下:

 

在最终渲染出的颜色被计算出来之后,可选择通过将颜色的透明度值和一个固定值比较。如果比较的结果失败,像素将不会被写到显示输出中。

 

Alpha测试在渲染凹形物体的透明部分时非常有用。显卡上有着每个像素写到屏幕上的深度记录。如果一个新的像素比原来的像素的深度深,那么新的像素就不会被写到屏幕中。

 

让我们看一幅图:

仔细看上图,会发现:

  • 左边的树使用了透明度测试(AlphaTest),注意在它的图形上的像素是如何完全透明或不透明的。
  • 中间的树只使用透明度混合(Alpha Blending)来渲染,注意由于深度缓冲的缘故靠近分支的透明部分是如何覆盖更远的叶子。
  • 最右边的树是通过下文实战中的第六个Shader“6.简单的植被Shader”着色器来渲染的,实现了透明度混合和透明度测试的组合,细节没有粗糙之处,浑然天成。

 

 

 

2.2 Alpha测试相关句法

 

语句之一:AlphaTest Off                 

此语句用于渲染所有像素(缺省)

 

语句之二:AlphaTest comparison AlphaValue

此语句用于设定透明度测试只渲染在某一确定范围内的透明度值的像素。其中的comparison取值为下表之一:

Greater Only render pixels whose alpha is greater than AlphaValue. 大于
GEqual Only render pixels whose alpha is greater than or equal to AlphaValue. 大于等于
Less Only render pixels whose alpha value is less than AlphaValue. 小于
LEqual Only render pixels whose alpha value is less than or equal to from AlphaValue. 小于等于
Equal Only render pixels whose alpha value equals AlphaValue. 等于
NotEqual Only render pixels whose alpha value differs from AlphaValue. 不等于
Always Render all pixels. This is functionally equivalent to AlphaTest Off.
渲染所有像素,等于关闭透明度测试AlphaTest Off
Never Don’t render any pixels. 不渲染任何像素

而AlphaValue为一个范围在0到1之间的浮点值。也可以是一个指向浮点属性或是一个范围属性,在后一种情况下需要使用标准的方括号写法标注索引名字,如([变量名]).

 

 

 

 

 

 

 

 

三、基本雾效(Fog)设置

 

 

3.1 雾效相关背景和概念理解

 

雾效(Fog)参数用于控制雾相关的渲染。

在计算机图形学中,雾化是通过混合已生成的像素的颜色和基于到镜头的距离来确定的最终的颜色来完成的。雾化不会改变已经混合的像素的透明度值,只是改变RGB值。

 

Unity中的缺省雾设定是基于Edit->RenderSettings中的设置的,且雾模式既可以是Exp2也可以是关闭;密度和颜色完全取自设定。

注意如果使用了片段着色器的话,着色器中的雾设定仍然是有效的。如果平台没有对固定管线的雾功能支持,Unity会实时补上着色器以支持需要的雾模式。

 

 

 

 

 

3.2 雾效相关的句法

 

 

语句之一:Fog { FogCommands }

此语句用于设定雾命令的内容,具体的填在大括号之中的内容见下面的语句。

 

语句之二:Mode Off | Global | Linear | Exp | Exp2

此语句用于定义雾模式。缺省是全局的,依据雾在渲染设定中是否打开确定可从无变化到平方值

 

语句之三:Color ColorValue

此语句用于设定雾的颜色值。

 

语句之四:Density FloatValue

此语句以指数的方式设定雾的密度。

 

语句之五:Range FloatValue , FloatValue

此语句用于为linear类型的雾设定远近距离值。

 

 

 OK,本文的基本知识就将这么多,接下来看看QianMo’s Toolkit的更新,然后开始基于上面讲解的基础知识的Shader实战。

 

 

四、QianMo’s Toolkit迎来v1.2更新

如前面三篇文章中所言,QianMo’s Toolkit是浅墨在Unity中写Shader时会用到的一些脚本小工具,用C#来实现。

此次在QianMo’s Toolkit Ver1.2版中加入了新的特性——检测当前系统的cpu与显卡的型号和参数,并显示到游戏窗口中。其实现代码如下


//-----------------------------------------------【脚本说明】-------------------------------------------------------
//      脚本功能:   在游戏运行时显示系统CPU、显卡信息
//      使用语言:   C#
//      开发所用IDE版本:Unity4.5 06f 、Visual Studio 2010
//      2014年12月 Created by 浅墨
//      更多内容或交流,请访问浅墨的博客:http://blog.csdn.net/poem_qianmo
//---------------------------------------------------------------------------------------------------------------------

//-----------------------------------------------【使用方法】-------------------------------------------------------
//      方法一:在Unity中拖拽此脚本到场景任意物体之上
//      方法二:在Inspector中[Add Component]->[浅墨's Toolkit v1.2]->[ShowSystemInfo]
//---------------------------------------------------------------------------------------------------------------------
using UnityEngine;
using System.Collections;
//添加组件菜单
[AddComponentMenu("浅墨's Toolkit v1.2/ShowSystemInfo")]
public class ShowSystemInfo : MonoBehaviour
{

string systemInfoLabel;
public Rect rect = new Rect(10, 100, 300, 300);

void OnGUI()
{
//在指定位置输出参数信息
GUI.Label(rect,systemInfoLabel);
}

void Update ()
{
//获取参数信息
systemInfoLabel = " \n\n\nCPU型号:" + SystemInfo.processorType + "\n (" + SystemInfo.processorCount +
" cores核心数, " + SystemInfo.systemMemorySize + "MB RAM内存)\n " + "\n 显卡型号:" + SystemInfo.graphicsDeviceName + "\n " +
Screen.width + "x" + Screen.height + " @" + Screen.currentResolution.refreshRate +
" (" + SystemInfo.graphicsMemorySize + "MB VRAM显存)";
}
}

添加此脚本到场景中的任意物体后,填一下其中的x和y坐标的值,用于设置输出在屏幕中的哪个位置。

接着点运行,于是在游戏窗口的指定区域中出现了CPU和显卡的信息:

 

 

于是,目前的QianMo’s Toolkit v1.2拥有的功能如下所示:

 

ShowFPS:在游戏运行时显示帧率相关信息

ShowObjectInfo:在测试过程里,于场景中和游戏窗口中分别显示添加给任意物体文字标签信息。隐藏和显示可选,基于公告板技术实现。

ShowGameInfo:在游戏运行时显示GUI相关说明

ShowLogo:在游戏运行时显示Logo

ShowUI:在游戏运行时显示简单的镶边UI。

SetMaxFPS :用于突破Unity每秒渲染 60帧的设定,自由设置最大帧率。

ShowObjectInfoInGamePlay:用于发布游戏之后显示文本信息。

ShowSystemInfo:在游戏运行时显示系统CPU、显卡信息

 

 

 

 

五、写Shader实战

根据上文讲解的剔除、深度测试和Alpha测试操作的句法,我们将完成如下六个Shader的写法:

 

1.用剔除操作渲染对象背面

2.渲染对象背面v2

3.用剔除实现玻璃效果

4.基本Alpha测试

5.顶点光照+可调透明度

6.简单的植被Shader

 

 

让我们通过详细注释的代码一个一个将他们讲清楚。

 

 

 

 

 

 

1.用剔除操作渲染对象背面

 

 

先写一个非常简单的只会渲染对象的背面的Shader:


Shader "浅墨Shader编程/Volume4/12.用剔除操作渲染对象背面"
{
//--------------------------------【子着色器】--------------------------------
SubShader
{
Pass
{
//【1】设置顶点光照
Material
{
Emission(0.3,0.3,0.3,0.3)
Diffuse (1,1,1,1)
}
//【2】开启光照
Lighting On

//【3】剔除正面(不绘制面向观察者的几何面)
Cull Front
}
}
}

我们将此Shader编译后赋给材质,得到如下效果,可以发现比原本默认的材质颜色暗,因为我们绘制的是物体的背面:

 

我们将材质赋给一个立方体,可以看到,通过此Shader我们可以渲染对象的背面,从而看到一个很奇怪立方体(因为我们看到的是它的内部)。效果如下:

 

 

 

 

 

2. 用剔除操作渲染对象背面(第二版)

 

让我们给上面写的这个Shader加上之前我们已经掌握的顶点光照的一些内容,并通过定义出第二个通道,采用亮蓝色来渲染物体的背面。代码如下:


Shader "浅墨Shader编程/Volume4/13.渲染对象背面v2"
{
//-------------------------------【属性】---------------------------------------
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
{

//---------------------------【通道一】------------------------------
//  说明:绘制对象的前面部分,使用简单的白色材质,并应用主纹理
//----------------------------------------------------------------------
Pass
{
//【1】设置顶点光照
Material
{
Diffuse [_Color]
Ambient [_Color]
Shininess [_Shininess]
Specular [_SpecColor]
Emission [_Emission]
}

//【2】开启光照
Lighting On

// 【3】将顶点颜色混合上纹理
SetTexture [_MainTex]
{
Combine Primary * Texture
}

}

//--------------------------【通道二】-------------------------------
//  说明:采用亮蓝色来渲染背面
//----------------------------------------------------------------------
Pass
{
Color (0,0,1,1)
Cull Front
}
}
}

同样是将Shader先赋给一个材质,得到的效果如下:

我们将材质赋给一个立方体,直接看是一个非常正常的立方体:

但一旦镜头靠近,看到物体的内部时,就发现了我们定义的蓝色部分了:

 

 

3.用剔除实现玻璃效果

 

 

很多时候,控制剔除比背面调试(debugging backfaces)更有用。比如遇到需要渲染透明的物体,经常会想要显示一个对象的背面。如果不做任何剔除操作的话,我们会发现有时常有一部分背面会覆盖在前面的一部分之上。下面就是一个解决这个问题的用于凸物体(球,立方体,车窗)的简单玻璃效果的着色器:


Shader "浅墨Shader编程/Volume4/14.用剔除实现玻璃效果"
{
//-------------------------------【属性】---------------------------------------
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
{
//【1】定义材质
Material
{
Diffuse [_Color]
Ambient [_Color]
Shininess [_Shininess]
Specular [_SpecColor]
Emission [_Emission]
}
//【2】开启光照
Lighting On
//【3】开启独立镜面反射
SeparateSpecular On

//【4】开启透明度混合(alpha blending)
Blend SrcAlpha OneMinusSrcAlpha

//--------------------------【通道一】-------------------------------
//  说明:渲染对象的背面部分
//----------------------------------------------------------------------
Pass
{
// 如果对象是凸型, 那么总是离镜头离得比前面更远
Cull Front //不绘制面向观察者的几何面
SetTexture [_MainTex]
{
Combine Primary * Texture
}
}

//----------------------------【通道二】-----------------------------
//  说明:渲染对象背对我们的部分
//----------------------------------------------------------------------
Pass
{
// 如果对象是凸型,那么总是离镜头离得比背面更远
Cull Back //不绘制背离观察者的几何面
SetTexture [_MainTex]
{
Combine Primary * Texture
}
}
}
}

同样是将Shader先赋给一个材质,得到的效果如下,可以发现是透明的黄色玻璃的效果:

 

再将材质赋给一个物体,就让此物体实现了简单的玻璃效果:

 

 

 

 

4.基本Alpha测试

 

先看一个最简单的能用的例子,使用一张带有透明度通道的纹理。对象只会在透明度大于0.6 时显示:


Shader "浅墨Shader编程/Volume4/15.基本Alpha测试"
{
//-------------------------------【属性】-----------------------------------------
Properties
{
_MainTex ("基础纹理 (RGB)-透明度(A)", 2D) = "white" {}
}

//--------------------------------【子着色器】--------------------------------
SubShader
{
//----------------------------【通道】-------------------------------
//  说明:进行Alpha测试操作,且只渲染透明度大于60%的像素
//----------------------------------------------------------------------
Pass
{
// 只渲染透明度大于60%的像素
AlphaTest Greater 0.6
SetTexture [_MainTex] { combine texture }
}
}
}

此Shader编译后赋给材质,材质又赋给物体的效果如下,且因为选取的纹理的透明度除了局部线条以外,其他的都小于了60%,于是就是只剩下这大概的轮廓了:

 

 

 

 

 

 

5.顶点光照+可调透明度

 

 

让我们在上面这个Shader的基础上增加一些光照和并让Alpha的阈值可以调节,于是便写出了如下的Shader:


Shader "浅墨Shader编程/Volume4/16.顶点光照+可调透明度"
{
//-------------------------------【属性】-----------------------------------------
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" { }
_Cutoff ("Alpha透明度阈值", Range (0,1)) = 0.5
}

//--------------------------------【子着色器】--------------------------------
SubShader
{
Pass
{
// 【1】使用Cutoff参数定义能被渲染的透明度阈值
AlphaTest Greater [_Cutoff]

//【2】设置顶点光照参数值
Material
{
Diffuse [_Color]
Ambient [_Color]
Shininess [_Shininess]
Specular [_SpecColor]
Emission [_Emission]
}

//【3】开启光照
Lighting On

// 【4】进行纹理混合
SetTexture [_MainTex] { combine texture * primary }
}
}
}

赋给材质后的效果如下,可以自由调节Alpha透明度的阈值:

通过调节Alpha透明度的阈值,得到了显示效果差异很大的材质:

  

 

 

将此材质赋给物体后的效果如下:

 

 

 

 

 

 

 

 

6.简单的植被Shader

 

 

 

最后一个Shader,让我们实现一个简单的植物渲染Shader的写法。

当渲染树和植物时,透明度测试使许多游戏中出现尖锐的边缘。解决这个问题的方法之一是渲染对象两次。首次通道中,我们只渲染超过50%透明度的像素。在第二次通道中,我们使用透明度混合上次我们切除的部分,而不记录像素的深度。我们可能会使一些树枝覆盖近的其他树枝,以实现逼真的效果。于是,简单的植被Shader写法如下:


Shader "浅墨Shader编程/Volume4/17.简单的植被Shader"
{
//-------------------------------【属性】-----------------------------------------
Properties
{
_Color ("主颜色", Color) = (.5, .5, .5, .5)
_MainTex ("基础纹理 (RGB)-透明度(A)", 2D) = "white" {}
_Cutoff ("Alpha透明度阈值", Range (0,.9)) = .5
}

//--------------------------------【子着色器】--------------------------------
SubShader
{
//【1】定义材质
Material
{
Diffuse [_Color]
Ambient [_Color]
}

//【2】开启光照
Lighting On

//【3】关闭裁剪,渲染所有面,用于接下来渲染几何体的两面
Cull Off

//--------------------------【通道一】-------------------------------
//  说明:渲染所有超过[_Cutoff] 不透明的像素
//----------------------------------------------------------------------
Pass
{
AlphaTest Greater [_Cutoff]
SetTexture [_MainTex] {
combine texture * primary, texture
}
}

//----------------------------【通道二】-----------------------------
//  说明:渲染半透明的细节
//----------------------------------------------------------------------
Pass
{
// 不写到深度缓冲中
ZWrite off

// 不写已经写过的像素
ZTest Less

// 深度测试中,只渲染小于或等于的像素值
AlphaTest LEqual [_Cutoff]

// 设置透明度混合
Blend SrcAlpha OneMinusSrcAlpha

// 进行纹理混合
SetTexture [_MainTex]
{
combine texture * primary, texture
}
}
}
}

我们将一棵树的叶子和树皮的材质替换成刚刚写好的Shader:

 

 

然后看看效果:

 

 

嗯,还是挺不错的,整体显示效果比用Unity自带的Bumped Diffuse要出色一些(Unity自带的Bumped Diffuse还可以在树叶上看到透过的光影,这部分内容我们以后以后学了再加)。

 

OK,我们就写如上的这六个Shader作为本次实战的内容吧。最后是构建本文配套的游戏场景,以及一些最终程序运行截图的欣赏。

 

 

五、最终游戏场景效果演示——热带雨林

 

 

上一次我们处于神秘的光之城堡,这次的场景,不妨让我们来到充满生机的热带雨林,领略一番不一样的味道。以大师级美工鬼斧神工的场景作品为基础,浅墨调整了场景布局,加入了音乐,并加入了更多高级特效,于是便得到了如此这次生机勃勃的场景。

运行游戏,我们来到充满各种绿色植物的热带雨林之中:

 

满满的绿色植物:

 

飘落到脸上的树叶:

竹林:

 

被椰子树包围:

 

鹤立鸡群的铁树:

 

冲破云霄的树:

 

 

凸起树根的老树:

最后放一张这次Shader的全家福:


 

 

OK,美图就放这么多。游戏场景可运行的exe可以在文章开头中提供的链接下载。

本篇文章的示例程序请点击此处下载:

 

     【浅墨Unity3D Shader编程】之四 热带雨林篇配套Unity工程下载

 

浅墨已经完成《OpenCV3编程入门》一书的技术修订工作,以及OpenCV2、OpenCV3双版本总计两百多个示例程序的注释和整理工作,书籍预计会在12月底印刷,1月上市。
 
最近应该可以没什么琐事了,没有特殊情况的话Unity Shader系列博文可以保持每周周一更新到明年2月份过年前。美剧都冬歇了咱们才刚上线:)
 
所以本周开始,Unity Shader系列博文宣告正式回归~

下周一,我们,不见不散。

 本文转载自浅墨毛星云,点此访问原文