我是基于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 图片
我们希望着色器能获取到一张图片,并且我们希望着色器输出图片的内容(可以是图片中的某一小部分内容,这样我们可以把图集的大图传进来),所以我们需要在着色器内定义一下参数:
- 采样图
- 采样范围(需要渲染的像素于采样图的 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,再去采样对应像素,最后输出出来,让我们来看看效果吧
可以看到,图片被正确的渲染出来了
# 高光效果
现在,我们添加高光效果,并且给其附加呼吸灯效果
其实本质上高光就是把一个颜色盖在原图上,再由时间去调整高光颜色与原像素的叠加关系,如果原图本身就有半透明像素的话,我们甚至能实现光晕效果
我们再修改一下 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]
范围内的数字,用于控制高光颜色 _Color
的 rgb
通道
我们利用 step
来确保仅在当前渲染的像素透明度超过 _Range
时才去把高光叠加到原像素上,这样就可以实现光晕效果了
接着我们来修改原像素,使其与高光像素叠加:
ret.rgb = lerp(c, ret, 1 - v * c.a); |
在这里我们用了 lerp
函数来进行叠加,原像素的颜色会乘以 1 - v * c.a
进行渲染,高光像素的颜色会乘以 v * c.a
进行渲染,其中 c.a
是我们对高光颜色设置的透明度,这样的话可以实现呼吸灯效果
我们来看看效果:
我们再来看看在 Unity 内调整参数会发生什么:
可以看到,效果非常完美
视频里在编辑器下没有对 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),感兴趣的朋友可以试试
光晕的实现比较依赖图片本身,如果图片本身不包含一圈半透明像素的话,光晕效果是不会有的,只会在图片非透明像素上盖一层高光