Unity从零开始实现一个全息效果Shader

Unity从零开始实现一个全息效果Shader

  • 前言
  • 开始捣鼓
    • 一、准备阶段
    • 二、先从透明效果开始
    • 三、顶点故障效果
    • 四、扫描线效果
    • 五、菲尼尔反射效果
    • 六、颗粒效果
    • 七、颜色故障效果
  • CustomEditor编辑器
  • 写在最后

本文欢迎转载,转载请标明出处!


前言

本次笔者想分享的是最近实现出来的全息模型效果。

全息的意思我就不详细讲了,因为讲了也是我百度的~

可以通过全息效果实现很赛博很科幻的模型投影特效,可以用于一些比较科幻的怪物或者npc做游戏表现。本次笔者分享的效果图如下:

如果读者觉得不够赛博不够炫,那肯定…肯定是模型的问题!(x


开始捣鼓

一、准备阶段

笔者使用的Unity版本还是2018.4.16f,按一般情况,版本不要太低就行了。

找一个目标模型和相关纹理,笔者就不放出示例中所使用的模型了,读者可以自行找一个模型尝试。

(Ps:模型的面片节点需要保证没有错误的旋转。什么是错误的旋转,就是模型有渲染面片的节点不会出现XYZ轴莫名其妙的90度旋转,这样会导致在shader处理中的模型定点方向问题。笔者的模型拿到手的时候,mesh对象节点就有莫名其妙的旋转)

Unity从零开始实现一个全息效果Shader_第1张图片
像以前一样,做好文件夹分类,让对应的资源都能被放到它应该在的目录中去。

新建一个Shader HologramEffect,把里面的Properties和Subshader内容全部删掉,我们要从头开始写。

Shader "SaberShad/HologramEffect"
{
    Properties
    {
        
    }
    SubShader
    {
        
    }
}

创建一个材质球 HologramMat,不着急替换shader,因为前面的HologramEffect肯定会报错…

因为是详细的效果实现,篇幅较长,还希望读者们能耐心看完。笔者也还在龟速学习中,如果有错误或者建议欢迎指出~

二、先从透明效果开始

首先,我个人的理解是,全息效果是要透明的,因为全息效果是一种光线投影,不应该会说挡到模型背后的对象。

同时,它不显示原来模型的颜色,而是使用单一的光色。为什么呢,因为显示原本颜色的效果我没做…

其实也不是说不能实现,但咱们先把透明效果实现了先~

修改HologramEffect,我们增加几个颜色字段,这里笔者把全局透明跟颜色透明区分开,因为颜色的透明度有其他作用,而区分开的透明度,决定了整个模型的透明度。然后我们简单实现一个标准的顶点和片元着色器处理函数,别忘了我们前面把Pass删除了,所以要在SubShader中新开出一个Pass来包含这块逻辑。最后,我们的渲染类型是透明对象,要加上对应的渲染类型RenderType,以及关闭深度写入。

相关的逻辑如下:

Shader "SaberShad/HologramEffect"
{
    Properties
    {
        [HDR]_HologramColor("Hologram Color", Color) = (1, 1, 1, 0)
		_HologramAlpha("Hologram Alpha", Range(0.0, 1.0)) = 1.0
    }
    SubShader
    {
        Tags{"Queue" = "Transparent" "RenderType" = "Transparent"}
        CGINCLUDE
			struct a2v_hg
            {
                float4 vertex : POSITION;
            };
            struct v2f_hg
            {
                float4 pos : SV_POSITION;
            };

            float4 _HologramColor;
            fixed _HologramAlpha;
		ENDCG
        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
			ZWrite Off
            CGPROGRAM
				#pragma target 3.0
				#pragma vertex HologramVertex
				#pragma fragment HologramFragment
            
                v2f_hg HologramVertex(a2v_hg v)
                {
                    v2f_hg o;
                    o.pos = UnityObjectToClipPos(v.vertex);
                    return o;
                }

                float4 HologramFragment(v2f_hg i) : SV_Target
                {
                    return float4(_HologramColor.rgb, _HologramAlpha);
                }
			ENDCG
        }
    }
}

把HologramMat的shader改成HologramEffect,赋值给我们的目标模型。
为了看到透明的效果,我在模型的后面放了一个球,如下图所示。我们可以看到模型确实是透明了,而且能看到身后的小球。

Unity从零开始实现一个全息效果Shader_第2张图片
可是,我们这种透明效果没有用到目标模型的纹理,所以模型的细节都消失了。所以我们还是需要使用原先模型的UV纹理,但我们这种纯色的效果不需要完全使用UV纹理的颜色,而是使用其中的某个通道调整最终的透明度就可以了。

我们修改HologramEffect,把UV采样相关的东西添加进去。

Shader "SaberShad/HologramEffect"
{
    Properties
    {
        ...
        // 主纹理充当颜色蒙版
		_HologramMaskMap("Hologram Mask", 2D) = "white"{}
		_HologramMaskAffect("Hologram Mask Affect", Range(0.0, 1.0)) = 0.5
    }
    SubShader
    {
        ...
			struct a2v_hg
            {
                ...
                float2 uv : TEXCOORD0;
            };
            struct v2f_hg
            {
                ...
                float2 uv : TEXCOORD0;
            };
			...
            // 全息蒙版
            sampler2D _HologramMaskMap;
            float4 _HologramMaskMap_ST;
            half _HologramMaskAffect;
		ENDCG
        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
			ZWrite Off
            CGPROGRAM
				...
                v2f_hg HologramVertex(a2v_hg v)
                {
                    ...
                    o.uv = v.uv;
                    return o;
                }

                float4 HologramFragment(v2f_hg i) : SV_Target
                {
                    float4 main_color = _HologramColor;
                    // 用主纹理的r通道做颜色蒙版
                    float2 mask_uv = i.uv.xy * _HologramMaskMap_ST.xy + _HologramMaskMap_ST.zw;
                    float4 mask = tex2D(_HologramMaskMap, mask_uv);
                    // 追加一个参数用来控制遮罩效果
                    float mask_alpha = lerp(1, mask.r, _HologramMaskAffect);
                    
                    float4 resultColor = float4(main_color.rgb, _HologramAlpha * mask_alpha);
                    return resultColor;
                }
			ENDCG
        }
    }
}

Unity从零开始实现一个全息效果Shader_第3张图片
Unity从零开始实现一个全息效果Shader_第4张图片
可以看到,增加了新的逻辑之后,UV上覆盖到了的位置都会出现相应的细节,但是我们也可以发现,原本在模型背面的顶点也被渲染进去了。由于我们是一整个模型作为主体,同时没有剔除背面和写入深度,导致了同一模型下在透明效果中出现的自己穿透自己显示的这么一个问题。

为了解决这个问题,我们可以额外使用一个Pass提前进行深度写入但是不渲染任何颜色到深度缓存,然后后续的Pass以深度写入的顶点进行渲染。修改HologramEffect:

	...
	SubShader
    {
        Tags{"Queue" = "Transparent" "RenderType" = "Transparent"}
        CGINCLUDE
			...
			// 因为顶点深入写入的Pass也是使用跟全息Pass一样的顶点函数,所以挪到这边来
            v2f_hg HologramVertex(a2v_hg v)
            {
                v2f_hg o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }
		ENDCG
        Pass
		{
			Name "Depth Mask"
			// 打开深度写入,并且设置颜色遮罩,0代表什么颜色都不输出
			ZWrite On 
			ColorMask 0

			CGPROGRAM
				#pragma target 3.0
				#pragma vertex HologramVertex
				#pragma fragment HologramMaskFragment

                float4 HologramMaskFragment(v2f_hg i) : SV_TARGET
                {
                    return 0;
                }
			ENDCG
		}
        Pass
        {
            Name "Hologram Effect"
            Blend SrcAlpha OneMinusSrcAlpha
			ZWrite Off
            CGPROGRAM
				#pragma target 3.0
				#pragma vertex HologramVertex
				#pragma fragment HologramFragment
            
                float4 HologramFragment(v2f_hg i) : SV_Target
                {
                    ...
                }
			ENDCG
        }
    }

Unity从零开始实现一个全息效果Shader_第5张图片
怎么样,效果是不是好多了,那可不,性能换的…

由于我们多了一个Pass,我们在一次完整的渲染中就要多计算一次模型的顶点和三角面,下图就是单Pass和双Pass下的顶点和三角面差异,多出来的部分正好是笔者项目中的模型顶点数和三角面数。

Unity从零开始实现一个全息效果Shader_第6张图片
因此,我们在设计模型Shader的时候,尽可能的把不透明透明的效果分别整合到单个Pass中去实现。同时,也尽量避免在一个模型面片对象上挂多个材质球,避免渲染多次顶点,这也是项目场景顶点和三角面的优化策略之一。

解决了最基本的透明问题,接下来开始实现全息特效的效果啦。

三、顶点故障效果

从最上面的GIF图中可以看到,模型会随机出现左右抽动的效果。这个就是模拟全息广播传输的时候出现的信号不稳定导致的影响波动的效果(说的好像真的一样…)。而这个效果的实现,就需要依赖修改模型的顶点坐标制作随机动画。笔者这边就直接搬出动画函数,修改HologramEffect:

Properties
    {
        ...
        // 全息抖动参数设置,x代表速度,y代表抖动范围,z代表抖动偏移量,w代表频率(0~0.99)
        _HologramGliterData1("Hologram Gliter Data1", Vector) = (0, 1, 0, 0) 
		_HologramGliterData2("Hologram Gliter Data2", Vector) = (0, 1, 0, 0)
    }
    SubShader
    {
        Tags{"Queue" = "Transparent" "RenderType" = "Transparent"}
        CGINCLUDE
			...

            half4 _HologramGliterData1, _HologramGliterData2;
            half3 VertexHologramOffset(float3 vertex, half4 offsetData)
            {
                half speed = offsetData.x;
                half range = offsetData.y;
                half offset = offsetData.z;
                half frequency = offsetData.w;

                half offset_time = sin(_Time.y * speed);
                // step(y, x) 如果 x >= y 则返回1,否则返回0,用来决定在正弦时间的某个地方才开始进行顶点抖动
                half timeToGliter = step(frequency, offset_time);
                half gliterPosY = sin(vertex.y + _Time.z);
                half gliterPosYRange = step(0, gliterPosY) * step(gliterPosY, range);
                // 获取偏移量
                half res = gliterPosYRange * offset * timeToGliter * gliterPosY;

                // 将这个偏移量定义为视角坐标的偏移量,再转到模型坐标
                float3 view_offset = float3(res, 0, 0);
                return mul((float3x3)UNITY_MATRIX_T_MV, view_offset);
            }

            v2f_hg HologramVertex(a2v_hg v)
            {
                v2f_hg o;
                // 产生模型顶点方向上的扭曲系数
                v.vertex.xyz += VertexHologramOffset(v.vertex.xyz, _HologramGliterData1);
                v.vertex.xyz += VertexHologramOffset(v.vertex.xyz, _HologramGliterData2);
                ...
                return o;
            }
		ENDCG
        ...
    }

(参考:https://zhuanlan.zhihu.com/p/141940278)

这边参考了一个大佬的顶点故障的动画思路,笔者这边进行了一些改动,还将四个参数整合到一个Vector4属性中,最终输出的偏移方向我们认定为视角方向上的偏移,在从视角空间转到模型空间下应用到模型顶点,就可以在模型空间按照我们的视角方向进行左右的抖动了

实际上,这个动画的过程读者完全可以天马行空,反正最重要的一步就是输出偏移的顶点坐标,过程能多随机就多随机,越随机就越炫,效果越不错。


(此时与全息效果格格不入的中东赛博摇滚boy在新的效果下更加摇摆了)

四、扫描线效果

单单只是有顶点抖动还是不够朋克,我们可以再往上堆其他效果。例如全息中比较常见的扫描线效果。

扫描线效果实际上就是用一张简单的黑白线条纹理做UV采样动画,用UV采样的结果影响全息的颜色。我们又要修改HologramEffect了,这次把扫描线的逻辑补全:

Properties
    {
       	...
        // 扫描线
		_HologramLine1("HologramLine1", 2D) = "white" {}
		_HologramLine1Speed("Hologram Line1 Speed", Range(-10.0, 10.0)) = 1.0
		_HologramLine1Frequency("Hologram Line1 Frequency", Range(0.0, 100.0)) = 20.0
		_HologramLine1Alpha("Hologram Line 1 Alpha", Range(0.0, 1.0)) = 0.15

		[Toggle(_USE_SCANLINE2)]_HologramLine2Tog("Hologram Line2 Toggle", float) = 0.0
		_HologramLine2("HologramLine2", 2D) = "white" {}
		_HologramLine2Speed("Hologram Line2 Speed", Range(-10.0, 10.0)) = 1.0
		_HologramLine2Frequency("Hologram Line2 Frequency", Range(0.0, 100.0)) = 20.0
		_HologramLine2Alpha("Hologram Line 2 Alpha", Range(0.0, 1.0)) = 0.15
    }
    SubShader
    {
        Tags{"Queue" = "Transparent" "RenderType" = "Transparent"}
        CGINCLUDE
			...
            struct v2f_hg
            {
                ...
                float4 posWorld : TEXCOORD1;
            };
            ...
            // 全息扫描线
            sampler2D _HologramLine1;
            half _HologramLine1Speed, _HologramLine1Frequency, _HologramLine1Alpha;
            sampler2D _HologramLine2;
            half _HologramLine2Speed, _HologramLine2Frequency, _HologramLine2Alpha;
            
            v2f_hg HologramVertex(a2v_hg v)
            {
                v2f_hg o;
                // 产生模型顶点方向上的扭曲系数
                v.vertex.xyz += VertexHologramOffset(v.vertex.xyz, _HologramGliterData1);
                v.vertex.xyz += VertexHologramOffset(v.vertex.xyz, _HologramGliterData2);
                // o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.posWorld = mul(unity_ObjectToWorld, v.vertex);
                o.pos = mul(UNITY_MATRIX_VP, o.posWorld);

                return o;
            }
		ENDCG
        ...
        Pass
        {
            Name "Hologram Effect"
            Blend SrcAlpha OneMinusSrcAlpha
			ZWrite Off
            CGPROGRAM
				#pragma target 3.0
                #pragma shader_feature _USE_SCANLINE2
				...
            	
                float4 HologramFragment(v2f_hg i) : SV_Target
                {
                    ...
                    // 全息效果 扫描线
                    float2 line1_uv = (i.posWorld.y * _HologramLine1Frequency + _Time.y * _HologramLine1Speed).xx;
                    float line1 = clamp(tex2D(_HologramLine1, line1_uv).r, 0.0, 1.0);
                    float4 line1_color = float4((main_color * line1).rgb, line1) * _HologramLine1Alpha;
                    float line1_alpha = clamp(((main_color).a + (line1_color).w), 0.0 , 1.0);

                    #if defined (_USE_SCANLINE2)
                        float2 line2_uv = (i.posWorld.y * _HologramLine2Frequency + _Time.y * _HologramLine2Speed).xx;
                        float line2 = clamp(tex2D(_HologramLine2, line2_uv).r, 0.0, 1.0);
                        float4 line2_color = float4((main_color * line2).rgb, line2) * _HologramLine2Alpha;
                        float line2_alpha = clamp(((main_color).a + (line2_color).w), 0.0 , 1.0);
                    #else
                        float4 line2_color = 0.0;
                        float line2_alpha = 1.0;
                    #endif

                    float4 resultColor = float4(
                        // rgb
                        main_color.rgb + line1_color.rgb * line1_alpha + line2_color.rgb * line2_alpha, 
                        // alpha
                        _HologramAlpha * mask_alpha
                    );
                    return resultColor;
                }
			ENDCG
        }
    }

通过代码可以看出,我们配置了两种扫描线,并且,第二种扫描线增加了变体控制是否使用。

既然有两种不同的扫描线,我们可以配置不同的扫描线纹理以及纹理速度,制造出更加有随机感的扫描效果。emmm…其实也没有变得多随机,只是叠加起来的效果看上去更好了对吧~

Unity从零开始实现一个全息效果Shader_第7张图片

五、菲尼尔反射效果

目前来看,我们虽然已经有了全息的顶点故障和扫描线效果,但是整个模型的表现看起来过于平面,没有光的感觉,不够炫!!

所以,为了让我们的摇滚boy变成光,我们需要增加菲尼尔反射的边缘高亮效果。

调整HologramEffect,增加菲尼尔反射的部分:

Properties
    {
        ...
        // 全息菲涅尔
		_FresnelScale("Fresnel Scale", Float) = 1
		_FresnelPower("Fresnel Power", Float) = 2
    }
    SubShader
    {
        Tags{"Queue" = "Transparent" "RenderType" = "Transparent"}
        CGINCLUDE
			struct a2v_hg
            {
                ..
                float3 normal:NORMAL;
            };
            struct v2f_hg
            {
                ...
                float3 normalDir : TEXCOORD2;
            };

            ...
            // 全息菲涅尔
            half _FresnelScale, _FresnelPower;
            ...

            v2f_hg HologramVertex(a2v_hg v)
            {
                ...
                o.normalDir = mul((float3x3)unity_ObjectToWorld, v.normal);

                return o;
            }
		ENDCG
        ...
        Pass
        {
            Name "Hologram Effect"
            Blend SrcAlpha OneMinusSrcAlpha
			ZWrite Off
            CGPROGRAM
				...
                float4 HologramFragment(v2f_hg i) : SV_Target
                {
                    ...
                    // 菲涅尔反射
                    float3 w_viewDir = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
                    float3 w_normal = normalize(i.normalDir);
                    float nDotV = dot(w_normal, w_viewDir);
                    float rim_nDotV = 1.0 - nDotV;
                    float4 fresnel = _FresnelScale * pow(rim_nDotV, _FresnelPower);
                    fresnel.a = clamp(fresnel.a, 0.0, 1.0);
                    float4 fresnel_color = (float4(fresnel.rgb, 1.0) * float4(main_color.rgb, 1.0)) * fresnel.a;

                    float4 resultColor = float4(
                        // rgb
                        main_color.rgb + line1_color.rgb * line1_alpha + line2_color.rgb * line2_alpha + fresnel_color.rgb, 
                        // alpha
                        _HologramAlpha * mask_alpha
                    );
                    return resultColor;
                }
			ENDCG
        }
    }

菲涅尔的算法方面,我这里讲也不如百度来的详细…

总之就是利用了视角方向和模型的法线方向的点乘,判断这个顶点是否位于视角方向能看到的边缘,通过上面代码的算法加强这部分边缘的颜色并叠加在最终颜色上。需要注意的是要保证透明度不要超过0~1的范围。当然如果为了更加特别的效果的话,我们大可只让透明度不低于0而不设上限。

增加了菲尼尔反射效果的摇滚boy,看上去。。。稍微好了那么一点点吧~

六、颗粒效果

整个效果已经能看了,但是我们还可以再细化整体的效果。之前我们的改动都是比较整体的,现在我们加一些近距离的细节提升——颜色颗粒效果。

颜色颗粒效果就是让模型的表面颜色再进行一次扰乱,说道扰乱效果我们可以用噪声效果来实现。噪声可以用噪声算法,也可以通过采样噪声纹理。笔者的这个效果在设计的时候是考虑在移动端使用的,因此采用了的是噪声纹理采样的方式来实现。

噪声纹理的采样跟上面的主UV模型细节的一样,即我们只需要采样其中的一个通道就可以了。通过采样噪声纹理的结果直接作用于最终的主颜色即可。调整HologramEffect,改动如下:

Properties
    {
        ...
        _HologramNoiseMap("Hologram Noise Map", 2D) = "white"{}
		...
        // 颗粒效果
        // xy:噪声采样tilling(需要噪声图),zw:噪声颜色区间(0~1)
		_HologramGrainData("Hologram Grain Data", Vector) = (20, 20, 0, 1)
		_HologramGrainSpeed("Hologram Grain Speed", Float) = 1.0
		_HologramGrainAffect("Hologram Grain Affect", Range(0 , 1)) = 1
    }
    SubShader
    {
        Tags{"Queue" = "Transparent" "RenderType" = "Transparent"}
        CGINCLUDE
			...
            sampler2D _HologramNoiseMap;
            ...
            // 全息颜色颗粒
            half4 _HologramGrainData;
            half _HologramGrainSpeed, _HologramGrainAffect;

            ...
            // 采样噪声图
            float SampleNoiseMap(float2 uv)
            {
                return tex2D(_HologramNoiseMap, uv).r;
            }

            ...
		ENDCG
        ...
        Pass
        {
            Name "Hologram Effect"
            Blend SrcAlpha OneMinusSrcAlpha
			ZWrite Off
            CGPROGRAM
				...
            
                float4 HologramFragment(v2f_hg i) : SV_Target
                {
                    ...
                    // 颗粒效果
                    float grain_noise = SampleNoiseMap((i.posWorld.xy * _HologramGrainData.xy + _Time.y * _HologramGrainSpeed));
                    float grain_amount = lerp(_HologramGrainData.z, _HologramGrainData.w, grain_noise) * _HologramGrainAffect;

                    float4 resultColor = float4(
                        // rgb
                        main_color.rgb + line1_color.rgb * line1_alpha + line2_color.rgb * line2_alpha + fresnel_color.rgb + grain_amount, 
                        // alpha
                        _HologramAlpha * mask_alpha
                    );
                    return resultColor;
                }
			ENDCG
        }
    }


现在再看这个特效,是不是感觉马上就上来了~没错,好的shader效果就是,使劲堆效果…

七、颜色故障效果

前面我们做了顶点故障效果,用来模拟信号不好的画面扭曲,既然画面都会扭曲了,那为什么颜色还会保持常亮呢?

这不合理!我不满意!

因此笔者还要再加一个效果,用来模拟颜色传输不稳定时的模型闪烁效果。

上面我们做颜色颗粒效果的时候用到了噪声纹理,这里我们还可以接着用,采样的结果直接作用与最终输出的颜色。修改HologramEffect:

Properties
    {
        ...
        // 全息颜色故障效果
		[Toggle] _HologramColorGlitchTog("Enable Hologram Color Glitch", Float) = 0
        // 噪声速度(使用XY分量)
		_HologramColorGlitch("Hologram Color Glitch", Range(0.0, 1.0)) = 0.5
		_HologramColorGlitchData("Hologram Color Glitch Data", Vector) = (1, 1, 0, 0)
		_HologramColorGlitchMin("Hologram Color Glitch Min", Range(0.0, 1.0)) = 0.5
    }
    SubShader
    {
        Tags{"Queue" = "Transparent" "RenderType" = "Transparent"}
        CGINCLUDE
			...
            // 颜色故障效果
            half _HologramColorGlitchTog, _HologramColorGlitch, _HologramColorGlitchMin;
            half4 _HologramColorGlitchData;

            ...
		ENDCG
        ...
        Pass
        {
            Name "Hologram Effect"
            Blend SrcAlpha OneMinusSrcAlpha
			ZWrite Off
            CGPROGRAM
				...
            
                float4 HologramFragment(v2f_hg i) : SV_Target
                {
                    ...
                    // 颜色故障效果
                    float color_glicth_noise = SampleNoiseMap(float2(_Time.x * _HologramColorGlitchData.x, _Time.x * _HologramColorGlitchData.y));
                    color_glicth_noise = color_glicth_noise * (1.0 - _HologramColorGlitchMin) + _HologramColorGlitchMin;
                    color_glicth_noise = clamp(color_glicth_noise, 0.0, 1.0);
                    float color_glitch = lerp(1.0, color_glicth_noise, _HologramColorGlitch * _HologramColorGlitchTog);
                    ...
                    // 应用全局颜色故障效果
                    resultColor *= color_glitch;

                    return resultColor;
                }
			ENDCG
        }
    }


_HologramColorGlitchTog跟上面的第二个扫描线的效果一样都是开关,不同的是这边的开关没有必要做成Shader变体,因为没打开这个效果的时候这个值就是0,通过代码可以在后续的计算中巧妙的避开这个颜色故障效果。

_HologramColorGlitchMin的作用是用来确定闪烁的颜色的最小值,这样子最暗的时候也不会比这个还暗了。

添加完这个颜色故障之后,我们就得到了最上面的最终效果啦~

我们的摇滚boy也变成了完全体~

CustomEditor编辑器

我们的shader参数还是比较多的,放到面板上会很乱。同样的,如果让美术童鞋看到我们的shader面板,说不定会提着40米长刀对着程序来一发登龙剑吧。
Unity从零开始实现一个全息效果Shader_第8张图片
因此,为美术童鞋制作一套Shader编辑器还是挺有必要的,因为这样的面板,要是过两个星期估计连笔者都忘了哪个是哪个了…

因此!我们又要回到定制ShaderEditor的环节了。但是具体的逻辑解释笔者不再赘述,因为我都放在了代码里面了~读者根据笔者对应位置的注释阅读即可。

创建一个Editor目录,并创建一个C#文件,起名为HologramEditor,完整代码如下:

using UnityEngine;
using UnityEditor;

public class HologramEditor : ShaderGUI
{
    Material target;
    MaterialEditor editor;
    MaterialProperty[] properties;
    static GUIContent staticLabel = new GUIContent();
    // 折叠参数
    bool expand_mask;
    bool expand_ver_gliter1;
    bool expand_ver_gliter2;
    bool expand_line1;
    bool expand_line2;
    bool expand_fresnel;
    bool expand_grain;
    bool expand_color_glitch;

    public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
    {
        // base.OnGUI(materialEditor, properties);
        this.target = materialEditor.target as Material;
        this.editor = materialEditor;
        this.properties = properties;
        DoBase();
        DoHgMask();
        DoVertexGliter();
        DoScanLine();
        DoFresnel();
        DoGrain();
        DoColorGlitch();
    }

    // 基础参数
    void DoBase()
    {
        // 全息颜色
        MaterialProperty hg_color = FindProperty("_HologramColor");
        editor.ShaderProperty(hg_color, MakeLabel("全息整体颜色"));
        MaterialProperty hg_alpha = FindProperty("_HologramAlpha");
        editor.ShaderProperty(hg_alpha, MakeLabel("全息整体透明度"));
        MaterialProperty glitch_map = FindProperty("_HologramNoiseMap");
        editor.TextureProperty(glitch_map, "噪声纹理", false);
        GUILayout.Space(10);
    }

    // 蒙版
    void DoHgMask()
    {
        // 这个是Editor的折叠UI,当这个值为true时便会渲染下面的属性词条UI出来
        expand_mask = EditorGUILayout.Foldout(expand_mask, "整体蒙版");
        if(expand_mask) 
        {
            EditorGUI.indentLevel += 1;
            // 获取材质属性
            MaterialProperty mask = FindProperty("_HologramMaskMap");
            MaterialProperty mask_affect = FindProperty("_HologramMaskAffect");
            // 绘制EditorGUI属性词条
            editor.TextureProperty(mask, "蒙版");
            editor.ShaderProperty(mask_affect, "蒙版强度");
            EditorGUI.indentLevel -= 1;
        }
    }

    // 顶点故障效果
    void DoVertexGliter()
    {
        expand_ver_gliter1 = EditorGUILayout.Foldout(expand_ver_gliter1, "顶点故障1");
        if(expand_ver_gliter1)
        {
            EditorGUI.indentLevel += 1;
            MaterialProperty gliter1 = FindProperty("_HologramGliterData1");
            GUILayout.Label("x:速度,y:抖动范围,z:抖动偏移量(正负区分左右抖动),w代表频率(0~0.99)", EditorStyles.centeredGreyMiniLabel);
            editor.VectorProperty(gliter1, "故障参数1");
            EditorGUI.indentLevel -= 1;
        }
        expand_ver_gliter2 = EditorGUILayout.Foldout(expand_ver_gliter2, "顶点故障2");
        if(expand_ver_gliter2)
        {
            EditorGUI.indentLevel += 1;
            MaterialProperty gliter2 = FindProperty("_HologramGliterData2");
            GUILayout.Label("x:速度,y:抖动范围,z:抖动偏移量(正负区分左右抖动),w代表频率(0~0.99)", EditorStyles.centeredGreyMiniLabel);
            editor.VectorProperty(gliter2, "故障参数2");
            EditorGUI.indentLevel -= 1;
        }
    }

    // 扫描线属性
    void DoScanLine()
    {
        expand_line1 = EditorGUILayout.Foldout(expand_line1, "扫描线效果1");
        if(expand_line1)
        {
            EditorGUI.indentLevel += 1;
            MaterialProperty line1_map = FindProperty("_HologramLine1");
            MaterialProperty line1_speed = FindProperty("_HologramLine1Speed");
            MaterialProperty line1_tilling = FindProperty("_HologramLine1Frequency");
            MaterialProperty line1_alpha = FindProperty("_HologramLine1Alpha");
            editor.TextureProperty(line1_map, "扫描线1纹理", false);
            editor.ShaderProperty(line1_speed, "扫描线1速度");
            editor.ShaderProperty(line1_tilling, "扫描线1tilling");
            editor.ShaderProperty(line1_alpha, "扫描线1透明度");
            EditorGUI.indentLevel -= 1;
        }

        expand_line2 = EditorGUILayout.Foldout(expand_line2, "扫描线效果2");
        if(expand_line2)
        {
            EditorGUI.indentLevel += 1;
            EditorGUI.BeginChangeCheck();
            MaterialProperty line2_tog = FindProperty("_HologramLine2Tog");
            editor.ShaderProperty(line2_tog, "使用扫描线2");
            // 当扫描线2的选项勾选之后才会绘制下方的属性GUI
            bool line2_enabled = GetToggleEnabled(line2_tog);
            if(line2_enabled)
            {
                MaterialProperty line2_map = FindProperty("_HologramLine2");
                MaterialProperty line2_speed = FindProperty("_HologramLine2Speed");
                MaterialProperty line2_tilling = FindProperty("_HologramLine2Frequency");
                MaterialProperty line2_alpha = FindProperty("_HologramLine2Alpha");
                editor.TextureProperty(line2_map, "扫描线2纹理", false);
                editor.ShaderProperty(line2_speed, "扫描线2速度");
                editor.ShaderProperty(line2_tilling, "扫描线2tilling");
                editor.ShaderProperty(line2_alpha, "扫描线2透明度");
            }
            EditorGUI.indentLevel -= 1;
            if(EditorGUI.EndChangeCheck())
            {
                SetKeyword("_USE_SCANLINE2", line2_enabled);
            }
        }
    }

    // 颜色故障效果
    void DoColorGlitch()
    {
        expand_color_glitch = EditorGUILayout.Foldout(expand_color_glitch, "全息颜色故障效果");
        if(expand_color_glitch)
        {
            EditorGUI.indentLevel += 1;
            MaterialProperty c_g_tog = FindProperty("_HologramColorGlitchTog");
            editor.ShaderProperty(c_g_tog, "使用颜色故障");
            if(GetToggleEnabled(c_g_tog))
            {
                MaterialProperty glitch_val = FindProperty("_HologramColorGlitch");
                MaterialProperty glitch_data = FindProperty("_HologramColorGlitchData");
                MaterialProperty glitch_min = FindProperty("_HologramColorGlitchMin");
                editor.ShaderProperty(glitch_val, "颜色故障强度");
                editor.VectorProperty(glitch_data, "噪声速度(使用XY分量)");
                editor.ShaderProperty(glitch_min, "颜色故障最小值");
            }
            EditorGUI.indentLevel -= 1;
        }
    }

    // 菲涅尔反射
    void DoFresnel()
    {
        expand_fresnel = EditorGUILayout.Foldout(expand_fresnel, "全息Fresnel效果");
        if(expand_fresnel)
        {
            EditorGUI.indentLevel += 1;
            MaterialProperty fresnel_scale = FindProperty("_FresnelScale");
            MaterialProperty fresnel_power = FindProperty("_FresnelPower");
            editor.ShaderProperty(fresnel_scale, "Fresnel大小");
            editor.ShaderProperty(fresnel_power, "Fresnel指数");
            EditorGUI.indentLevel -= 1;
        }
    }

    // 颗粒效果
    void DoGrain()
    {
        expand_grain = EditorGUILayout.Foldout(expand_grain, "颗粒效果");
        if(expand_grain)
        {
            EditorGUI.indentLevel += 1;
            MaterialProperty grain_data = FindProperty("_HologramGrainData");
            MaterialProperty grain_speed = FindProperty("_HologramGrainSpeed");
            MaterialProperty grain_affect = FindProperty("_HologramGrainAffect");
            GUILayout.Label("xy:噪声采样tilling(需要噪声图),zw:噪声颜色区间(0~1)", EditorStyles.centeredGreyMiniLabel);
            editor.VectorProperty(grain_data, "颗粒参数");
            editor.ShaderProperty(grain_speed, "颗粒效果速度");
            editor.ShaderProperty(grain_affect, "颗粒效果强度");
            EditorGUI.indentLevel -= 1;
        }
    }


    // 利用方法优化GUI代码结构,使得代码更容易看
    MaterialProperty FindProperty(string name)
    {
        return FindProperty(name, properties);
    }

    static GUIContent MakeLabel(string text, string tooltip = null)
    {
        staticLabel.text = text;
        staticLabel.tooltip = tooltip;
        return staticLabel;
    }

    static GUIContent MakeLabel(MaterialProperty property, string tooltip = null)
    {
        staticLabel.text = property.displayName;
        staticLabel.tooltip = tooltip;
        return staticLabel;
    }
    // 设置shader关键字
    void SetKeyword(string keyword, bool state)
    {
        if(state)
        {
            foreach(Material m in editor.targets)
            {
                m.EnableKeyword(keyword);
            }
        }
        else{
            foreach(Material m in editor.targets)
            {
                m.DisableKeyword(keyword);
            }
        }
    }

    bool GetToggleEnabled(MaterialProperty property)
    {
        return (int)property.floatValue == 1;
    }
}

编译通过后,我们的HologramEffect在编辑器中就变成了这样:
Unity从零开始实现一个全息效果Shader_第9张图片
点击箭头,还能展开属性GUI:

Unity从零开始实现一个全息效果Shader_第10张图片
最重要的是什么,最重要的是我们可以让它是全中文的。这样一来美术童鞋就可以沐浴在没有语言理解障碍的快感中不能自拔,说不定还会给程序来个么么哒(??

写在最后

如果这篇分享对你有所帮助,那就是笔者最大的荣幸了。笔者起初在做这个效果的时候查不到太多资料,最后花了不少时间才慢慢总结出这些效果。

从算法上来看其实并没有太多可圈可点的地方,但是我想这里面的一些技术思路已经提供到位了,有什么其他的更炫的效果,如果能在评论区分享出来那就更好了~

参考:https://zhuanlan.zhihu.com/p/141940278 (Unity全息效果当中的抖动效果)

你可能感兴趣的:(Unity,unity,shader)