用shader挖一个洞
使用不同的render queue
支持半透明材质
结合反射和透明
本节是渲染得第11节,前一小节介绍的是更复杂的材质,但是这些材质都是不透明的,现在我们加入支持透明材质的需求。
本节使用的unity版本为:unity5.5.0f3
1 剔除渲染
为了创建一个透明的材质,我们需要知道每个像素点的透明度。这些透明信息是存储在颜色的alpha通道的。在我们的课程中这个透明度信息存储在的是主漫反射贴图和_Tint Color的alpha通道中。
下面是一个包含透明信息的贴图:
1.1 获取alpha值
定义GetAlpha函数,专门计算alpha值:
float GetAlpha(Interpolators i)
{
return _Tint.a * tex2D(_MainTex, i.uv.xy).a;
}
1.2 挖洞
clip函数使用,如提出alpha小于0.5的像素
float4 MyFragmentProgram(Interpolators i):SV_TARGET
{
float alpha = GetAlpha(i);
clip(alpha - 0.5);
……
}
1.3 剔除变量
上面的alpha小于0.5的是会被剔除,这里声明一个变量,可以在Inspector面板进行控制:
Properties
{
……
_AlphaCutOff("Alpha Cutoff", Range(0,1)) = 0.5;
}
float _AlphaCutOff;
float4 MyFragmentProgram(Interpolators i):SV_TARGET
{
float alpha = GetAlpha(i);
clip(alpha - _AlphaCutOff);
……
}
然后就是在Inspector面板画出来:
void DoMain()
{
……
DoAlphaCutoff();
……
}
void DoAlphaCutoff()
{
MaterialProperty slider = FindProperty("_AlphaCutoff");
EditorGUI.indentLevel += 2;
editor.ShaderProperty(slider, MakeLabel(slider));
EditorGUI.indentLevel -= 2;
}
1.4 渲染模式
透明剔除的消耗还是有的,在pc上不会体现的太明显,而在移动端则会比较耗,所以我们需要提供一个关键字来控制是否需要剔除。如下:
float alpha = GetAlpha(i);
#if defined(_RENDERING_CUTOUT)
clip(alpha - _AlphaCutoff);
#endif
添加变体声明:
#pragma shader_future _RENDERING_CUTOUT
我们可以自定义一个开关来控制物体的透明与否。比如定义一个枚举作为下拉菜单。
enum RenderingMode
{
Opaque, Cutout
}
我们试图在用户选择其中一个模式的时候,要重新设置关键字。所以要写如下的函数:
void DoRenderingMode()
{
RenderingMode mode = RenderingMode.Opaque;
if(IsKeywordEnabled("_RENDERING_CUTOUT"))
{
mode = RenderingMode.Cutout;
}
//这里是监听用书输入的
EditorGUI.BeginChangeCheck();
mode = (RenderingMode)EditorGUILayout.EnumPop(MakeLabel("Rendering Mode"), mode);
if(EditorGUI.EndChangeCheck())
{
RecordAtion("Rendering Mode"); //选择一个模式之后,可以按ctrl+z键恢复
SetKeyword("_RENDERING_CUTOUT", mode == RenderingMode.Cutout);
}
}
1.5 渲染队列
尽管现在渲染模式是可以了,但是unity需要把剔除的材质放在与不透明物体不同的渲染队列中。不透明的物体是最先被渲染的,然后是剔除。这样做的原因是剔除的代价比较高。优先渲染不透明物体,那意味着如果一个像素出现在不透明物体后边并且他是要被剔除的材质,那么则不会被绘制。
默认的queue的值为2000,剔除的queue值为2450,队列值越小则越先被渲染。
你可以使用如下的形式定义队列的大小:
“Queue” = “Geometry+1”
可以加一个偏移用来控制队列的大小。
我们在unity中查看队列值得方法是在debug模式下的inspector面板上看:
下面就要把我们的自定义的渲染模式和unity本身的渲染队列关联起来。
if(EditorGUI.EndChangeCheck())
{
RecordActoin("Rendering Mode");
SetKeyword("_RENDERING_CUTOUT", mode == RenderingMode.Cutout);
RenderQueue queue = mode == RenderingMode.Opaque? RenderQueue.Geometry:RenderQueue.AlphaTest;
foreach(Material m in editor.targets)
{
m.renderQueue = (int) queue;
}
}
1.6 渲染类型标签
另外一个细节需要注意的是unity的Rendertype标签,它本身没有什么作用,它只是一个提示,告诉unity这是什么样的一个shader。它会被替换shader的时候来控制物体是否被渲染。这个以后再说。
首先看这行代码:
我们先把它注释掉,看看debug模式下的inspector面板:
切换到cutout模式下,然后打开它之后看看:
发现多了一个size=1,并且key=RenderType value=TransparentCout
这个也许就是SetOverrideTag的作用了,以后再详细介绍。
2 半透明渲染
剔除对于挖洞来说已经足够了,但是对于半透明来说还是不够的,剔除是针对单个像素的要与否。这样会导致边缘走样,它没有从不透明到透明之间的过度,所以unity又提供了一个渲染队列就是Transparent。
我们先自己在渲染模式的枚举中增加一个渲染模式:
enum RenderingMode
{
Opaque,
Cutout,
Fade
}
调整我们的GUI代码:
void DoRenderingMode()
{
RenderingMode mode = RenderingMode.Opaque;
shouldShowAlphaCutoff = false;
if (IsKeywordEnabled("_RENDERING_CUTOUT")) {
mode = RenderingMode.Cutout;
shouldShowAlphaCutoff = true;
}
else if (IsKeywordEnabled("_RENDERING_FADE")) {
mode = RenderingMode.Fade;
}
…
if (EditorGUI.EndChangeCheck()) {
RecordAction("Rendering Mode");
SetKeyword("_RENDERING_CUTOUT", mode == RenderingMode.Cutout);
SetKeyword("_RENDERING_FADE", mode == RenderingMode.Fade);
…
}
}
2.1 渲染设置
上面说过unity为渐变提供了另外一个队列Transparent,其值为3000。
为了代码的见面,我们封装了一个结构体叫做RenderingSettins:
struct RenderingSettins
{
public RenderQueue queue;
public string renderType;
//创建三个静态的设置
public static RenderingSettings[] modes =
{
new RenderingSettings(){queue = RenderQueue.Geometry, renderType = ""},
new RenderingSettings(){queue = RenderQueue.AlphaTest, renderType = "TransparentCutout"},
new RenderingSettings(){queue = RenderQueue.Transparent, renderType = "Transparent"},
}
}
然后监听输入的地方了:
RenderingSettings settings = RenderingSettings.modes[(int)mode];
foreach (Material m in editor.targets)
{
m.renderQueue = (int)settings.queue;
m.SetOverrideTag("RenderType", settings.renderType);
}
2.2 渲染透明几何体
现在你可以切换到Fade模式了。由于我们的shader还没有支持渐变模式,所以它还是被当做不透明物体渲染。
但是其渲染方式是有区别于不透明物体的。
而如果不透明物体也存在的时候,那么Render.OpaqueGeometry会被调用;而如果是透明物体的时候调用的是Render.TransparentGeometry会被调用。不透明物体后被渲染。
2.3 混合像素
在基础通道和附加通道都增加关键字:RENDERING_FADE
#pragma shader_feature _ _RENDERING_CUTOUT _RENDERING_FADE
如果是渐变模式,那么我们要做的是将当期像素与之前帧缓存的像素进行混合。这个混合操作是由CPU处理,而且是在像素着色器之后。所以像素着色器需要输出一个alpha值。
为了实现半透明效果,我们不能仅仅是将这个颜色加上之前的颜色,而要用alpha值进行混合。
当alpha=1的时候,表面当前是不透明物体。当是不透明物体时,base的混合模式为blend one zero。而额外的通道为blend one one。
当alpha=0的时候,表面当前是透明物体。两个通道的混合方式都为:blend zero one。
当alpha=0.25的时候,则base pass的混合方式为:blend 0.25 0.75 add pass 为:blend 0.25 one ??? 为啥?
所以我们可以使用SrcAlpha以及OneMinusSrcAlpha
pass
{
Tags{"LightMode"="ForwardBase"}
Blend SrcAlpha OneMinusSrcAlpha
……
}
pass
{
Tags{"LightMode"="FowardAdd"}
Blend SrcAlpha one
ZWrite off
}
为了能够控制Blend的方式,我们需要把混合因子变成变量,然后外面可以控制。
首先要在Properties中增加两个变量,并且不需要在面板上显示:
Properties
{
……
[HideInInspector] _SrcBlend("_SrcBlend", Float) = 1
[HideInInspector] _DstBlend("_DstBlend", Float) = 0
}
然后shader中使用的时候,要用[]的形式,这是老版的shader语法。
pass
{
Tags{"LightMode"="ForwardBase"}
Blend [_SrcBlend] [_DstBlend]
……
}
pass
{
Tags{"LightMode"="FowardAdd"}
Blend [_SrcBlend] [_DstBlend]
ZWrite off
}
在编辑器下加入对变量的控制:
struct RenderingSettings
{
public RenderQueue queue;
public string renderType;
public BlendMode srcBlend, dstBlend;
public static RenderingSettings[] modes =
{
new RenderingSettings(){
queue = RenderQueue.Geometry,
renderType = string.Empty ,
srcBlend = BlendMode.One,
dstBlend = BlendMode.Zero},
new RenderingSettings(){
queue = RenderQueue.AlphaTest,
renderType = "TransparentCutout",
srcBlend = BlendMode.One,
dstBlend = BlendMode.Zero},
new RenderingSettings(){
queue = RenderQueue.Transparent,
renderType = "Transparent",
srcBlend = BlendMode.SrcAlpha,
dstBlend = BlendMode.OneMinusSrcAlpha}
};
}
foreach (Material m in editor.targets) {
m.renderQueue = (int)settings.queue;
m.SetOverrideTag("RenderType", settings.renderType);
m.SetInt("_SrcBlend", (int)settings.srcBlend);
m.SetInt("_DstBlend", (int)settings.dstBlend);
}
2.4 深度问题
当只有一个物体的时候,渲染是没有问题的。当多个物体的时候,特别是当两个物体离的比较近的时候,会出现下面的情况:
原因:
不透明的物体是由近到远画,而透明的物体是由远到近画。为什么呢?因为透明的物体要和他后面的物体进行混合。这也就是透明物体绘制比较耗的原因,因为要进行深度排序。
ok,首先确定了透明物体是由远到近的画。
接下来解释上面问题出现的原因,我们的两个面片为:
其中:Transparent Quad的坐标为:(0,0,0)
而Transparent Quad(1)的坐标为:(0,0.001,0)
相机的位置为:(0,2,0)
按照透明物体渲染得顺序来说,Transparent Quad会先画,而Transparent Quad(1)后画,因为前者里摄像机较远。
但是事实上由于两个物体离的特别近,所以渲染得顺序出现了异常,这一点可以通过Frame Debug查看:
Transparent Quad(1)先画,而Transparent Quad后画了。
Transparent Quad(1)先画,由于深度写入没有关闭,那么Transparent Quad(1)的各个像素点的深度写入了深度缓存。
而当画Transparent Quad的时候,其深度比较函数默认为:LessEqual
而事实上Transparent Quad的深度要大于Transparent Quad(1),所以重叠的部分就不会再被画了。所以出现上面的遮盖问题。
那么解决的方法时,把base pass中的深度写入关闭,而且是针对透明物体的时候,深度写入关闭。
2.5 控制深度写入
和混合的方式一样,我们在shader中加入属性,然后再C#代码中进行控制。
[HideInInspector] _ZWrite ("_ZWrite", Float) = 1
ZWrite [_ZWrite]
注意这里只有一盏灯,所以base pass中也要加入ZWrite[_ZWrite]
设置结构体也要相应的改下了:
struct RenderingSettings
{
public RenderQueue queue;
public string renderType;
public BlendMode srcBlend, dstBlend;
public bool zWrite;
public static RenderingSettings[] modes =
{
new RenderingSettings(){
queue = RenderQueue.Geometry,
renderType = string.Empty ,
srcBlend = BlendMode.One,
dstBlend = BlendMode.Zero,
zWrite = true},
new RenderingSettings(){
queue = RenderQueue.AlphaTest,
renderType = "TransparentCutout",
srcBlend = BlendMode.One,
dstBlend = BlendMode.Zero,
zWrite = true},
new RenderingSettings(){
queue = RenderQueue.Transparent,
renderType = "Transparent",
srcBlend = BlendMode.SrcAlpha,
dstBlend = BlendMode.OneMinusSrcAlpha,
zWrite = false}
};
}
然是控制的地方:
foreach (Material m in editor.targets) {
m.renderQueue = (int)settings.queue;
m.SetOverrideTag("RenderType", settings.renderType);
m.SetInt("_SrcBlend", (int)settings.srcBlend);
m.SetInt("_DstBlend", (int)settings.dstBlend);
m.SetInt("_ZWrite", settings.zWrite ? 1 : 0);
}
这样在base pass中加入了ZWrite的控制,当为Fade模式的时候,其ZWrite是关闭,那么即使离近处的先画了,深度没有写入,深度缓存中依然是无穷大的数,所以当滑到远处的物体的时候,还是会被画,并且会进行混合。
3 渐变vs透明
半透明的物体根据其alpha值进行渐变。这里的渐变包含了漫反射+镜面方式,都被渐变了。
这个对与固体的半透明表面则不适应,比如玻璃是完全透明的,但是它依然有高亮和反射。为了支持这个模式,unity提供了Transparent的渲染模式。
所以我们也加入这个模式到枚举中:
enum RenderingMode
{
Opaque, Cutout ,Fade, Transparent
}
关于RenderingMode的Transparent设置和Fade类似,但是对于反射我们不应该把alpha值考虑进去,所以源颜色的混合因子应该为one,而不是SrcAlpha,具体如下:
new RenderingSettings() {
queue = RenderQueue.Transparent,
renderType = "Transparent",
srcBlend = BlendMode.One,
dstBlend = BlendMode.OneMinusSrcAlpha,
zWrite = false
}
同样我们的加入关键字以及控制代码:
pragma shader_feature _ _RENDERING_CUTOUT _RENDERING_FADE _RENDERING_TRANSPARENT
#if defined(_RENDERING_FADE) || defined(_RENDERING_TRANSPARENT)
color.a = alpha;
#endif
return color;
控制代码:
void DoRenderingMode () {
…
else if (IsKeywordEnabled("_RENDERING_TRANSPARENT")) {
mode = RenderingMode.Transparent;
}
EditorGUI.BeginChangeCheck();
mode = (RenderingMode)EditorGUILayout.EnumPopup(
MakeLabel("Rendering Mode"), mode
);
if (EditorGUI.EndChangeCheck()) {
RecordAction("Rendering Mode");
SetKeyword("_RENDERING_CUTOUT", mode == RenderingMode.Cutout);
SetKeyword("_RENDERING_FADE", mode == RenderingMode.Fade);
SetKeyword(
"_RENDERING_TRANSPARENT", mode == RenderingMode.Transparent
);
…
}
}
当把物体的渲染模式选择为Transparent之后,其渲染得结果为:
整个plane都被看到了,原因是我们并没有乘以我们的alpha因子。
3.1 乘以alpha因子
我们要在片段着色器中,仅仅对漫反射进行处理,镜面反射不乘以alpha因子。
效果如下:
由于我们是在GPU混合之前进行乘以alpha的处理。这个技术叫先乘alpha混合。很多图像处理软件会储存这个颜色值,可以认为的存储RGB值,而不包含alpha通道。也就可以使其变得更明亮或者更暗,但是储存颜色会损失精度。
3.2 调整alpha