我是基于ChatGPT-turbo-3.5实现的AI助手,在此网站上负责整理和概括文章

文章介绍了如何在Unity的URP下实现2D图片的发光特效。首先编写了支持透明、自定义颜色混合和SRP Batcher合批的片元着色器。然后通过修改Properties和CBuffer,实现了渲染2D图片和高光效果的功能,包括高光颜色、呼吸灯效果等参数的控制。最后介绍了条件编译的使用方法,以及如何实现半透明渲染。通过对片段着色器的修改,最终实现了2D图片的高光特效,呼吸灯效果和光晕效果。文章分享了完整的Shader代码以及效果展示。

# 前文

最近在搞一个项目,需要给 2D 武器做发光特效,于是简单的搞了一下,并在本文分享一下思路和代码

# 编写 Shader

# URP 2D 着色器 雏形

我们先创建一个片元着色器,注意,这个 Shader 支持:

  • 透明(Transparent)
  • 自定义颜色混合(Blend)
  • SRP Batcher 合批(CBuffer)

SRP Batcher 合批能够大幅度优化渲染性能,推荐使用(这个 Shader 是兼容的,只需要在 MeshRenderer 或 Unity2023 及之后版本的 SpriteRenderer 上使用这个 Shader 即可开启优化)

Shader "Unlit/Fx"
{
    Properties
    {
        [Enum(UnityEngine.Rendering.BlendMode)]_Src("_Src",Int)=5
        [Enum(UnityEngine.Rendering.BlendMode)]_Dst("_Dst",Int)= 10
    }
    SubShader
    {
        Tags
        {
            "RenderType" = "Transparent"
            "Queue" = "Transparent"
            "RenderPipeline" = "UniversalPipeline"
        }
        ZWrite Off
        Blend [_Src][_Dst]
        Pass
        {
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            CBUFFER_START(UnityPerMaterial)
            CBUFFER_END
            #pragma vertex vert
            #pragma fragment frag
            struct Attributes
            {
                float4 positionOS : POSITION;
                float4 uv : TEXCOORD0;
            };
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            Varyings vert(Attributes IN)
            {
            
            }
            half4 frag(Varyings IN) : SV_Target
            {
            
            }
            ENDHLSL
        }
    }
}

# 渲染 2D 图片

我们希望着色器能获取到一张图片,并且我们希望着色器输出图片的内容(可以是图片中的某一小部分内容,这样我们可以把图集的大图传进来),所以我们需要在着色器内定义一下参数:

  1. 采样图
  2. 采样范围(需要渲染的像素于采样图的 x/y 位置,以及宽与高)

我们来修改一下 Properties :

Properties
{
  _SrcTexture ("原图", 2D) = "clear" {}
  _Rect ("图片信息", Vector) = (0, 0, 0, 0)
  [Enum(UnityEngine.Rendering.BlendMode)]_Src("_Src",Int)=5
  [Enum(UnityEngine.Rendering.BlendMode)]_Dst("_Dst",Int)= 10
}

我们再修改一下 CBuffer :

CBUFFER_START(UnityPerMaterial)
  float4 _Rect;
  float4 _SrcTexture_TexelSize;
CBUFFER_END

定义一下采样:

TEXTURE2D_FLOAT(_SrcTexture);
SAMPLER(sampler_SrcTexture);

我们打算在 _Rect 内,放入需要渲染的图片基于原图的(x 位置,y 位置,宽,高)

接着,我们修改 定点着色器

Varyings vert(Attributes IN)
{
  Varyings OUT;
  OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
  float4 cur_pixel = float4(IN.uv.x * _Rect.z, IN.uv.y * _Rect.w, 0, 0);
  float x = (_Rect.x + cur_pixel.x) * _SrcTexture_TexelSize.x;
  float y = (_SrcTexture_TexelSize.w - _Rect.y - _Rect.w + cur_pixel.y) *
  _SrcTexture_TexelSize.y;
  OUT.uv = float2(x, y);
  return OUT;
}

我们在定点着色器内计算出需要渲染的 uv 基于原图所对应的 xy 位置,再将其转化为 uv(把 x 和 y 的范围从 [0,宽或高] 变为 [0,1]

最后,我们用片段着色器来输出需要渲染的像素:

half4 frag(Varyings IN) : SV_Target
{
  half4 ret = SAMPLE_TEXTURE2D(_SrcTexture, sampler_SrcTexture, IN.uv.xy);
  return ret;
}

其实就是接收定点着色器计算的 uv,再去采样对应像素,最后输出出来,让我们来看看效果吧

img1

可以看到,图片被正确的渲染出来了

# 高光效果

现在,我们添加高光效果,并且给其附加呼吸灯效果

其实本质上高光就是把一个颜色盖在原图上,再由时间去调整高光颜色与原像素的叠加关系,如果原图本身就有半透明像素的话,我们甚至能实现光晕效果

我们再修改一下 Properties

Properties
{
  _SrcTexture ("原图", 2D) = "clear" {}
  _Rect ("图片信息", Vector) = (0, 0, 0, 0)
  [Toggle(GLOW)] _GLOW ("发光", Float) = 0
  _Color ("特效颜色", Vector) = (0, 0, 0, 0)
  _Speed ("特效速度", Float) = 0
  _Range ("特效范围", Float) = 0
  _Alpha ("透明度", Range(0, 1)) = 1
  [Enum(UnityEngine.Rendering.BlendMode)]_Src("_Src",Int)=5
  [Enum(UnityEngine.Rendering.BlendMode)]_Dst("_Dst",Int)= 10
}

有了这些参数,我们就能控制 Shader 是否对图片进行渲染并产生高光,同时我们可以定义高光的颜色,高光呼吸的速度,光晕范围(仅限于原图由半透明像素用于渲染光晕的情况下),以及最后输出的图片的透明度

我们再修改一下 CBuffer

CBUFFER_START(UnityPerMaterial)
  float _Speed;
  float _Range;
  float _Alpha;
  float4 _Rect;
  float4 _SrcTexture_TexelSize;
  float4 _Color;
CBUFFER_END

现在我们开始修改片段着色器

我们先计算渲染高光颜色的系数:

half v = (cos(_Time.y * _Speed) + 1);
v = step(_Range, ret.a);
half4 c = _Color;
c.rgb *= v;

我们首先将 Unity 提供的 Time.y 取出,代表运行时间,然后我们乘以 _Speed 来使得 cos 函数的频率更频繁,这样我们可以更频繁的去渲染高光,接下来我们对其 +1 ,这样我们可以得到一个在 [0,1] 范围内的数字,用于控制高光颜色 _Colorrgb 通道

我们利用 step 来确保仅在当前渲染的像素透明度超过 _Range 时才去把高光叠加到原像素上,这样就可以实现光晕效果了

接着我们来修改原像素,使其与高光像素叠加:

ret.rgb = lerp(c, ret, 1 - v * c.a);

在这里我们用了 lerp 函数来进行叠加,原像素的颜色会乘以 1 - v * c.a 进行渲染,高光像素的颜色会乘以 v * c.a 进行渲染,其中 c.a 是我们对高光颜色设置的透明度,这样的话可以实现呼吸灯效果

我们来看看效果:

gif1

我们再来看看在 Unity 内调整参数会发生什么:

gif2

可以看到,效果非常完美

视频里在编辑器下没有对 GLOW 进行打钩,这是因为我们现在还没写条件编译!

# 条件编译

我们有时不希望高光,只希望渲染图片,这种情况我们可以使用条件编译:

我们只需在 CBufffer 前加入:

#pragma multi_compile _ GLOW

即可

接下来我们修改一下片段着色器:

#ifdef GLOW
// 高光
half v = (cos(_Time.y * _Speed) + 1) * 0.5;
v = saturate(v) * step(_Range, ret.a);
half4 c = _Color;
c.rgb *= v;
ret.rgb = lerp(c, ret, 1 - v * c.a);
#endif

这样即可在开启 GLOW 的情况下渲染高光特效(C# 脚本里使用 material.SetKeyword 即可)

# 半透明渲染

出于某种原因,我们可能希望渲染出来的东西再进行个半透明处理,我们只需要在片段着色器返回像素前对像素做个处理即可:

...
ret.a *= _Alpha;
return ret;

# 完整代码

Shader "Unlit/Fx"
{
    Properties
    {
        _SrcTexture ("原图", 2D) = "clear" {}
        _Rect ("图片信息", Vector) = (0, 0, 0, 0)
        [Toggle(GLOW)] _GLOW ("发光", Float) = 0
        _Color ("特效颜色", Vector) = (0, 0, 0, 0)
        _Speed ("特效速度", Float) = 0
        _Range ("特效范围", Float) = 0
        _Alpha ("透明度", Range(0, 1)) = 1
        [Enum(UnityEngine.Rendering.BlendMode)]_Src("_Src",Int)=5
        [Enum(UnityEngine.Rendering.BlendMode)]_Dst("_Dst",Int)= 10
    }
    SubShader
    {
        Tags
        {
            "RenderType" = "Transparent"
            "Queue" = "Transparent"
            "RenderPipeline" = "UniversalPipeline"
        }
        ZWrite Off
        Blend [_Src][_Dst]
        Pass
        {
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #pragma multi_compile _ GLOW
            CBUFFER_START(UnityPerMaterial)
                float _Speed;
                float _Range;
                float _Alpha;
                float4 _Rect;
                float4 _SrcTexture_TexelSize;
                float4 _Color;
            CBUFFER_END
            TEXTURE2D_FLOAT(_SrcTexture);
            SAMPLER(sampler_SrcTexture);
            #pragma vertex vert
            #pragma fragment frag
            struct Attributes
            {
                float4 positionOS : POSITION;
                float4 uv : TEXCOORD0;
            };
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
                float4 cur_pixel = float4(IN.uv.x * _Rect.z, IN.uv.y * _Rect.w, 0, 0);
                float x = (_Rect.x + cur_pixel.x) * _SrcTexture_TexelSize.x;
                float y = (_SrcTexture_TexelSize.w - _Rect.y - _Rect.w + cur_pixel.y) *
                    _SrcTexture_TexelSize.y;
                OUT.uv = float2(x, y);
                return OUT;
            }
            half4 frag(Varyings IN) : SV_Target
            {
                half4 ret = SAMPLE_TEXTURE2D(_SrcTexture, sampler_SrcTexture, IN.uv.xy);
                #ifdef GLOW
                // 高光
                half v = (cos(_Time.y * _Speed) + 1) * 0.5;
                v = saturate(v) * step(_Range, ret.a);
                half4 c = _Color;
                c.rgb *= v;
                ret.rgb = lerp(c, ret, 1 - v * c.a);
                #endif
                ret.a *= _Alpha;
                return ret;
            }
            ENDHLSL
        }
    }
}

# 结尾

这个 Shader 比较简单,但是对性能友好(并且合批后 CPU/GPU 压力都小,因为可以使用 SRP Batcher),感兴趣的朋友可以试试

光晕的实现比较依赖图片本身,如果图片本身不包含一圈半透明像素的话,光晕效果是不会有的,只会在图片非透明像素上盖一层高光

此文章已被阅读次数:正在加载...更新于

请我喝[茶]~( ̄▽ ̄)~*

Jason Xu 微信支付

微信支付

Jason Xu 支付宝

支付宝