• Unity中实现溶解(Dissolve)特效及其原理解析


    Unity中实现溶解(Dissolve)特效及其原理解析

    今天我们分享一个Unity中非常常见的特效: 溶解.

    下面先来看看效果.

    在这里插入图片描述

    大家可能和我一样, 在不了解实现方式的时候, 会觉得这个效果很神奇, 实现起来十分复杂.

    但是一旦你了解之后, 会觉得…更神奇, 简单到神奇.

    溶解效果的原理和其实现

    溶解特效的实现原理非常非常简单, 一句话概况就是: 给定一个变化量, 在值变化时, 抛弃对象的一部分像素不进行渲染.

    所谓给定一个变化量, 我们可以将其抽象为一个进度条Slider, 代表溶解程度, 从0到1, 0代表一点都不溶解, 1代表全部溶解.

    也就是说, Slider从0到1, 溶解效果从无到有.

    那么怎么做到抛弃对象一部分像素不进行渲染呢.

    依然很简单, 在片段着色器中使用clip函数, 抛弃掉不满足要求的片段.

    clip(value)函数的效果是: 当传入的value小于0时, 则抛弃本片段, 内部的实现类似:

    void clip(int value)
    {
        if (value < 0)
            discard;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那么我们唯一的问题就是这个value该怎么传了.

    我们通过Slider取值, 作为一个阈值(threshold value), 所有小于该阈值的片段颜色(任选颜色向量x, y, z, w的一个值作为比较)丢弃.

    fixed4 color = tex2D(_MainTex, data.uv);
    clip(color.r - threshold);
    
    • 1
    • 2

    这样, 随着阈值的增大, 对象身上的颜色值大的片段会慢慢被丢弃, 在视觉上就形成了溶解效果. 代码如下:

    Shader "Dissolve_Origin"
    {
    	Properties
    	{
    		_MainTex ("Texture", 2D) = "white" {}
            
            // 通过Slider取阈值
            _DissolveThreshold("DissolveThreshold", Range(0, 1)) = 0
    	}
    	SubShader
    	{
    		Tags { "RenderType"="Opaque" }
    		LOD 100
    
    		Pass
    		{
    			CGPROGRAM
    			#pragma vertex vert
    			#pragma fragment frag
    			
    			#include "UnityCG.cginc"
    
    			struct appdata
    			{
    				float4 vertex : POSITION;
    				float2 uv : TEXCOORD0;
    			};
    
    			struct v2f
    			{
    				float2 uv : TEXCOORD0;
    				float4 vertex : SV_POSITION;
    			};
    
    			sampler2D _MainTex;
    			float4 _MainTex_ST;
    			
                fixed _DissolveThreshold;
    
    			v2f vert (appdata v)
    			{
    				v2f o;
    				o.vertex = UnityObjectToClipPos(v.vertex);
    				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    				return o;
    			}
    			
    			fixed4 frag (v2f i) : SV_Target
    			{
    				fixed4 col = tex2D(_MainTex, i.uv);
                    fixed4 dissolveCol = col;
    
                    // 从噪声纹理采样颜色, 如果该值[小于阈值]则丢弃本片段
                    // 比如阈值为0.1, 则在噪声纹理上采样的所有像素r值小于0.1的片段都会被丢弃
                    // 即噪声纹理上偏黑的颜色(->0)首先开始溶解, 偏白的颜色(->1)最后溶解
                    clip(dissolveCol.r - _DissolveThreshold);
    
    				return col;
    			}
    			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

    大家可以看到, 只是在最基本的着色器代码上加了区区4行代码就达到的一个酷炫的效果, 甚至真正起效的只有一行代码.

    在这里插入图片描述

    这个效果和第一张图有所差别, 是因为仅仅使用了对象自身的纹理信息, 效果比较受限.

    业界常用的实现是还需要添加另一张噪声纹理, 用来指定溶解的区域和程度, 我们下面就开始介绍.

    使用噪声纹理指定溶解信息

    首先我们将纹理直接替换为另一张纹理, Shader不做任何变化, 可以看到下面的效果:

    在这里插入图片描述

    可以看到溶解是从黑色(0, 0, 0)的部分开始的, 渐渐溶解到白色的部分.

    如果将对象的纹理信息和溶解信息分开, 也就是说使用另一张"溶解纹理(实际上被称为噪声纹理)", 我们就可以操作溶解的区域和速度.

    噪声纹理的用途可谓是十分广, 很多特效都是使用噪声纹理, 比如水的流动, 旗帜的飘动, 裂缝等, 后面有机会我们会一一看到.

    这里简单理解噪声纹理的意义就是: 将原来对象的纹理进行"扰动", 这些扰动信息使用单独的一张纹理来表示.

    那么这是怎么实现呢?

    其实也很简单, 在片段着色器中对对象本身的纹理正常采样, 然后使用同一个uv坐标在噪声纹理上采样, 使用该值来判断是否丢弃:

    fixed4 frag (v2f i) : SV_Target
    {
        fixed4 dissolveCol = tex2D(_DissolveTex, i.uv);
    
        // 从噪声纹理采样颜色, 如果该值[小于阈值]则丢弃本片段
        // 比如阈值为0.1, 则在噪声纹理上采样的所有像素r值小于0.1的片段都会被丢弃
        // 即噪声纹理上偏黑的颜色(->0)首先开始溶解, 偏白的颜色(->1)最后溶解
        clip(dissolveCol.r - _DissolveThreshold);
    
        fixed4 col = tex2D(_MainTex, i.uv);
        return col;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    相应的Shader属性面板如下:

    在这里插入图片描述
    这样就可以获得和第一张图一样的效果了, 从指定的位置开始溶解.

    细心的同学可能看到, 最终效果会从怪物的嘴巴部分开始溶解, 但是噪声纹理最黑的部分明明是左下角:

    在这里插入图片描述

    这是因为本文使用的怪物纹理是基于PBR制作的, 和普通的纹理布局不太一样, 大家在实验的时候, 找一个正常的纹理就行了.

    或者制作噪声纹理时也使用PBR的方式制作也可以达到相同的目的, 这里我是为了说明这种情况, 故意如此.

    下面是完整的代码:

    Shader "Dissolve"
    {
    	Properties
    	{
    		_MainTex ("Texture", 2D) = "white" {}
            
            _DissolveTex("DissolveTex", 2D) = "white" {}
            _DissolveThreshold("DissolveThreshold", Range(0, 1)) = 0
    	}
    	SubShader
    	{
    		Tags { "RenderType"="Opaque" }
    		LOD 100
    
    		Pass
    		{
    			CGPROGRAM
    			#pragma vertex vert
    			#pragma fragment frag
    			
    			#include "UnityCG.cginc"
    
    			struct appdata
    			{
    				float4 vertex : POSITION;
    				float2 uv : TEXCOORD0;
    			};
    
    			struct v2f
    			{
    				float2 uv : TEXCOORD0;
    				float4 vertex : SV_POSITION;
    			};
    
    			sampler2D _MainTex;
    			float4 _MainTex_ST;
    			
                sampler2D _DissolveTex;
                fixed _DissolveThreshold;
    
    			v2f vert (appdata v)
    			{
    				v2f o;
    				o.vertex = UnityObjectToClipPos(v.vertex);
    				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    				return o;
    			}
    			
    			fixed4 frag (v2f i) : SV_Target
    			{
                    fixed4 dissolveCol = tex2D(_DissolveTex, i.uv);
    
                    // 从噪声纹理采样颜色, 如果该值[小于阈值]则丢弃本片段
                    // 比如阈值为0.1, 则在噪声纹理上采样的所有像素r值小于0.1的片段都会被丢弃
                    // 即噪声纹理上偏黑的颜色(->0)首先开始溶解, 偏白的颜色(->1)最后溶解
                    clip(dissolveCol.r - _DissolveThreshold);
    
    				fixed4 col = tex2D(_MainTex, i.uv);
    				return col;
    			}
    			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

    总结

    今天我们介绍了游戏中比较常见的溶解特效, 比如怪物在被击杀后, 可以溶解消失, 或者角色穿过传送门, 溶解消失等.

    对于初次接触的同学来说, 溶解特效比较酷炫, 但是实现却超级简单, 不需要复杂的数学知识和图形学知识, 我认为这是一个非常好的学习Shader示例, 所以在此特意共享给大家.

    后续我可能也会继续给大家分享一些简单却有意思的Shader, 让大家领略Shader的魅力, 坚定学习下去的信心.

    我认为图形学的基础固然重要, 但是一上来就各种矩阵, 各种管线, 容易让人产生劝退效果, 所以我更喜欢从一些简单的特效来学习, 在学习和实现了效果之后, 再去探究其背后的原理, 这样比较容易产生成果, 也更能加深理解.

    我觉得对于前端的同学, 做出各种绚丽的特效也是大家最初想做游戏的目的之一 , 不然做服务器不是更舒服? 虽然做游戏之后才知道, 这部分是美术的活儿, 哈哈.

    好了, 今天就是这些啦, 希望对大家有所帮助.

  • 相关阅读:
    描述符——设备描述符
    docker对网络和程序速度的影响
    2022-11-21 mysql列存储引擎-架构实现缺陷梳理-P1
    git硬重置(hard reset)重找回
    GRU 训练和解码的时候h随着输入的变化而变化
    为什么在做微服务设计的时候一定需要DDD?
    前端性能优化的常见方式
    webpack和vite的区别
    清华学姐三年的测试成长经历,到最后的喜提高薪offer
    CSS调试实例
  • 原文地址:https://blog.csdn.net/woodengm/article/details/125501899