8章 透明效果

Unity中两种方法实现透明效果: 1.透明度测试(Alpha Test),无法得到真正半透明效果,另外一种是透明度混合(Alpha Blending )
前面的渲染都不考虑渲染顺序,对于不透明的物品这样也可以渲染正确。这是因为强大的深度缓存(depth buffer 也叫z-buffer),在实时渲染中,深度缓存用于解决可见(visibility)的问题。基本思路就是:根据深度缓存中的值来判断该片元距离摄像机的距离。 需要开启深度测试和深度写入
使用深度缓存,可以让我们不关心渲染顺序,但是要实现透明效果,就不那么简单了,因为在实现透明度混合时,我们关闭了深度写入(ZWrite)。

透明度测试

只要一个片元的透明度不满足条件(通常小于某个阀值),就会舍弃片元,被舍弃的片元不会再进行任何处理,也不会对颜色缓存产生影响。否则,就会按照普通不透明的物体来处理它,即进行深度测试,深度写入等。也就是说透明度测试是不需要关闭深度写入的。它和其他不透明物体最大的不同是根据透明度舍弃一些片元。。简单但是效果很极端,完全透明看不到。要不完全不透明。

透明度混合

  使用片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色进行混合,得到新颜色。但是透明度混合要关闭深度写入,使得要关心物体渲染顺序。注意的是**只关闭了深度写入,没有关闭深度测试**。当使用透明度混合渲染一个片元时,会比较它的深度值与当前深度缓冲中的深度值。如果不透明在透明的前面就不会再进行混合,舍弃这个混合。**深度缓冲是只读的。判断需要渲染么**

渲染顺序的重要性

透明度融合中 最重要的就是渲染顺序,不光是透明和不透明需要从后到前,两个透明的物体在渲染的时候也需要注意从后到前的顺序,不然就会看起来反了。
透明和不透明一起 会因为不透明有深度写入 会盖住透明物体。 两个透明的物体会进行颜色的混合,如果顺序不会也会得到错误的表现
总结方法就是
(1)先渲染所有不透明物体,并开启它们的深度测试和深度写入
(2)把半透明物体按摄像机远近排序,按照从后往前的顺序渲染这些半透明物体,开启他们的深度测试,但关闭深度写入。
光有顺序的保障也是不够全面的。有时候会有相互重复穿插、覆盖的情况。

image

遇到上面的这种情况,一般是选择分割网格,我们尽可能让模型是凸面体,考虑将复杂的模型拆分成可以独立排序的多个子模型。如果不想分割网络,可以试着让透明通道更加柔和,使得穿插不是那么明显。也可以开始深度写入的半透明效果来模拟物体半透明。

Unity Shader的渲染顺序

Unity对顺序的问题提供了 渲染队列( render queue )的解决方案,使用SubShader的Queue来决定渲染队列。Unity 在内部使用一系列整数索引来表示每个渲染队列,且索引号越小表示越早被渲染。

image

//类似这样的声明,注意不是LightMode
SubShader {
        Tags { ”Queue”=”Alpha Test”}
        Pass {
        ……
        }
}

如果我们想要通过透明度混合来实现透明效果,代码中应该包含类似下面的代码:

SubShader {
        Tags { ”Queue”=”Transparent”}
        Pass {
            ZWrite Off   //深度写入关闭
            ……
        }
    }

其中, ZWrite Off 用于关闭深度写入,在这里我们选择把它写在Pass 中。我们也可以把它写在SubShader 中,这意味着该SubShader 下的所有Pass 都会关闭深度写入。

透明度测试

原理: 一个片元透明度不满足条件(通常小于某个阀值),那么它对应的片元就会被舍弃。被舍弃的片元不会再进行任何处理,也不会对颜色缓冲产生任何影响(就等于啥都没了,不存在);否则的话就会按照不透明的物体来进行处理
通常的判断是用片元着色器中的** clip函数来进行透明度测试**。这是一个CG函数
函数: void clip(float4 x); void clip(float3 x); void clip(float2 x); void clip(float1 x); void clip(float x);
参数:裁剪时使用的标量或矢量条件。
描述:如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色。
示例用这样一张图,有4个不同的透明度 便于观察表现


image

得到的效果会如下


image

直接上代码

Shdaer "Alpha Test"{
     Properties{
          _Color("Color Tint", Color) = (1,1,1,1)
          _MainTex("Main Tex",2D) ="white"{}
           _Cutoff("Alpha Cutoff", Range(0,1)) =0.5    // 这个是判断是否裁剪掉的 标准值,范围[0,1]透明度的范围也是这个
     }
      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;   //精度在[0,1] 所以用fixed

                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= UnityObejctToWorldNormal(v.normal);
                    o.worldPos= mul(_Object2World, 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);
                    //Alpha test
                    clip(texColor.a -_Cutoff);
                    //等同代码 if(texColor.a -_Cutoff<0.0){discard;} 舍弃
                    
                    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);

                }
                Fallback "Transparent/Cutout/VertexLit"
            }    
      }
      
}

这里的Tags是新的知识点
使用透明度测试 就需要Queue 设置为** AlphaTest 。**
RenderType 标签让 Unity把Shader 归入到提前定义的组(这里就TransparentCount)中,以指明该Shader 是一个使用了透明度测试的Shader。
RenderType 标签通常被用于着色器替换功能。我们还
把IgnoreProjector 设置为True,这意味着这个Shader 不会受到投影器(Projectors )的影响

通常,使用了透明度测试的Shader 都应该在SubShader 中设置这三个标签。
LightMode 标签是Pass标签中的一种,它用于定义该Pass 在Unity 的光照流水线中的角色。只有定义了正确的LightMode,我们才能正确得到一些Unity 的内置光照变量,例如 _LightColor0 .

image

透明度混合

原理:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。
为了进行混合,我们需要使用Unity 提供的混合命令——Blend。 Blend 是Unity 提供的设置混合模式的命令。想要实现半透明的效果就需要把当前自身的颜色和已经存在于颜色缓冲中的颜色值进行混合,混合时使用的函数就是由该指令决定的。


image

这里 使用第二种语义 Blend SrcFactor DstFactor 来进行混合。 这个命令在设置混合因子的同时打开了混合模型。只有开启混合,设置片元的透明通道才有意义。
我们把源颜色的混合因子SrcFactor 设置为SRCAlpha, 而目标颜色的混合因子 DstFactor 设为OneMinusSrcAlpha,这就是说混合后的颜色是


image

这是常用的混合命令。
这次用上面的素材 所以改动部分代码

Properties{
      _Color("Main Tint", Color) = (1,1,1,1)
      _MainTex("Main Tex", 2D) = "white"{}
      _AlphaScale("Alpha Scale",Range(0,1)) =1  //在纹理透明度的基础上控制整体透明度
}
        fixed _AlphaScale;  //属性名称对应修改
      SubShader{
          //透明度混合queue 是 Transparent
          Tags{"Queue" = "Transparent" "IgnoreProjector"="True" "RenderType"  ="Transparent"}
          Pass{
                Tags{ "LightMode"="ForwardBase"}
                ZWrite off
                Blend SrcAlpha OneMinusSrcAlpha
                //片元着色器修改
                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);
                }
                FallBack "Transparent/VertexLit"    
           }
    }
image

但是当模型本身有复杂的遮挡关系或者包含了复杂的非凸网格的时候,就会有各种排序错误产生的错误的透明效果。

image

这都是由于我们关闭了深度写入造成的,因为这样我们就无法对模型进行像素级别的深度排序。在8.1 节中我们提到了一种解决方法是分割网格,从而可以得到一个“质量优等”的网格。但是很多情况下这往往是不切实际的。这时, 我们可以想办法重新利用深度写入,让模型可以像半透明物体一样进行淡入淡出。这就是我们下面要讲的内容。

开启深度写入的半透明效果

上面的错误表现是由于深度写入关闭造成的。一种解决办法:使用两个pass来渲染模型,第一Pass 开启深度写入,但不输出颜色,它的目的仅仅是为了把模型的深度值写入深度缓冲中;第二个Pass进行正常的透明度混合,由于上个Pass得到逐像素的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。
缺点在于,多用一个Pass 对性能影响比较大,而且使用这种方式可以实现与背后场景的混合。但是模型内部的前后没有了透明混合就是没了内部的半透明。如图

image

上代码 实现过程

//我们需要在前面的代码里 加一个pass用了深度写入,测试
Shader "Unity Shaders Book/Chapter 8/Alpha Blending With ZWrite" {
    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"}
        
        // Extra pass that renders to depth buffer only
        Pass {
            ZWrite On
            ColorMask 0
        }
        
        Pass {
            // 和8.4 节同样的代码
        }
    } 
    FallBack "Transparent/VertexLit"
}

这个pass虽然没有什么具体计算,但是依然有深度消耗
pass的目的仅仅是为了把模型的深度信息写入深度缓冲中,从而剔除模型被自身遮挡的片元。 所以第一行ZWrite On。 第二行一个新的渲染命令----ColorMask。这里是用于设置颜色通道的写掩码(write mask)

ColorMask RGB |A | 0 | 其他任何R、G、B、A 的组合

当ColorMask 设为0时,表示Pass 不会写入任何颜色通道,不会输出任何颜色。个人认为 这个就是强行设置颜色值,RGBA设置某个值 这个pass就对应只会输出这个值。

ShaderLab的混合命令

我们首先来看一下混合是如何实现的。当片元着色器产生一个颜色的时候,可以选择与颜色缓存中的颜色进行混合。这样一来,混合就和两个操作数有关:源颜色(source color)和目标颜色( destination color )。源颜色,我们用S 表示,指的是由片元着色器产生的颜色值; 目标颜色,我们用D 表示,指的是从颜色缓冲中读取到的颜色值。对它们进行混合后得到的输出颜色,我们用O表示,它会重新写入到颜色缓冲中。需要注意的是,当我们谈及混合中的源颜色、目标颜色和输出颜色时,它们都包含了RGBA 四个通道的值,而并非仅仅是RGB 通道。
想要使用混合,我们必须首先开启它。在Unity 中,当我们使用Blend (Blend Off 命令除外〉命令时,除了设置混合状态外也开启了混合。但是, 在其他图形API 中我们是需要手动开启的。例如在OpenGL 中,我们需要使用glEnable(GL_BLEND)来开启混合。
这个我专门写过一篇帖子来讲这个Blend https://www.jianshu.com/p/9f5066eca31f 主要是说的Alpha的混合
这里主要讲一下命令和使用

混合等式的参数

等式一般都是加的操作(我们也可以使用其他操作)这个混合过程虽然是不可以编程的 但是是一个高度可以配置的过程。混合中 主要影响 RGB通道和A通道(分开计算)。可以传入想要的混合因子,每个等式有两个因子(一个用于和源颜色相乘, 一个用于和目标颜色相乘〉,因此一共需要4 个因子。


image

其实这些混合因子呢其实也已经定义好,直接调用机可以。我原来的帖子详细写过而且各种表现也有展示


image

调用时候的命令
Blend SrcAlpha OneMinusSrcAlpha, One Zero

混合操作

所谓真正的混合不是说和因子相乘的那一刻,是结果相加的操作混合。这样可以是加法也可以是减法。减法就是 命令** BlendOp BlendOperation**

image

image

混合操作命令通常是与混合因子命令一起工作的。但需要注意的是, 当使用 Min 或Max 混合操作时, 混合因子实际上是不起任何作用的,它们仅会判断原始的源颜色和目的颜色之间的比较结果。

Blend SrcColor OneMinusSrcColor
BlendOp Sub //等等操作 ,写法就是这样

常见的混合类型

通过混合操作和混合因子命令的组合, 我们可以得到一些类似Photoshop 混合模式中的混合效果:


image

image

需要注意的是,虽然上面使用Min 和Max 混合操作时仍然设置了混合因子,但实际上它们并不会对结果有任何影响,因为Min 和Max 混合操作会忽略混合因子。另一点是,虽然上面有些混合模式并没有设置混合操作的种类,但是它们默认就是使用加法操作,相当于设置了BlendOp Add。

双面渲染的 透明效果

前面的透明渲染呢 只有第一层,内部结构看不到这是因为开启了物体背景剔除。只渲染正面,背面不管。 如果要争取的效果,可以使用Cull 指令来控制需要剔除哪个渲染图元

Cull Back  |Front  | Off

如果设置为Back,那么那些背对着摄像机的渲染图元就不会被渲染,这也是默认情况下的剔除状态;如果设置为Front , 那么那些朝向摄像机的渲染图元就不会被撞染; 如果设置为Off,就会关闭剔除功能, 那么所有的渲染图元都会被渲染,但由于这时需要渲染的图元数目会成倍增加,因此除非是用于特殊效果, 例如这里的双面渲染的透明效果,通常情况是不会关闭剔除功能的。

透明度测试的双面渲染

代码很简单就是在 原来透明彻底是代码里面加一句 Cull off

Pass {
        Tags { "LightMode"="ForwardBase" }
            
        // Turn off culling
        Cull Off
image

透明度混合的双面渲染

这个比较麻烦,因为关闭了深度写入,所以先后顺序其实不知道。必须控制渲染顺序。
这个的解决办法就是写两个Pass(好傻)SubShader会顺序执行Pass。第一个Pass只渲染背面,第二个渲染正面,这样保证了背面的肯定在前面闲渲染,这样虽然两次Pass 但其实计算没重复,不算性能浪费。

Shader "Unity Shaders Book/Chapter 8/Alpha Blend With Both Side" {
    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" }
            
            // First pass renders only back faces 
            Cull Front
            
            // 和之前一样的代码
        }
        
        Pass {
            Tags { "LightMode"="ForwardBase" }
            
            // Second pass renders only front faces 
            Cull Back
            
            // 和之前一样的代码
        }
    } 
    FallBack "Transparent/VertexLit"
}

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容