白纸一张

三十而立,四十而不惑

0%

练习项目(十四):速度线效果的实现

概述

最近接到一个需求,需要实现速度线的效果。这里,会一步步地展示实现的过程。希望对大家能有所启发。下面是项目的Github地址,欢迎Star。

Road Of Shader

实现

首先,合理地使用搜索引擎,找一下速度线效果的参考图:

对于上图,下面进行一些分析。

对于黑色线条的形状,猛一看没什么思路。但如果换一种角度,考虑与黑色线条互补的白色部分,是类似中间一个光团,往周围发射的形状。这种形状,就比较好实现了。下面,先实现这种形状。

下面是后面要用到的基础代码,主要关注片元着色器部分:

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
66
67
68
69
Shader "RoadOfShader/2.2-SpeedLine/Speed Line"
{
Properties
{
[NoScaleOffset]_NoiseTex ("NoiseTex", 2D) = "white" { }
_Center ("Center", Vector) = (0.5, 0.5, 0, 0)
}
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True" }

Pass
{
Tags { "LightMode" = "UniversalForward" }

Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off

HLSLPROGRAM

#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma target 2.0

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

#pragma vertex vert
#pragma fragment frag

struct Attributes
{
float4 positionOS: POSITION;
float2 uv: TEXCOORD0;
};

struct Varyings
{
float2 uv: TEXCOORD0;
float4 vertex: SV_POSITION;
};

CBUFFER_START(UnityPerMaterial)
half4 _Center;
CBUFFER_END

TEXTURE2D(_NoiseTex); SAMPLER(sampler_NoiseTex);

Varyings vert(Attributes input)
{
Varyings output = (Varyings)0;

output.vertex = TransformObjectToHClip(input.positionOS.xyz);
output.uv = input.uv;

return output;
}

half4 frag(Varyings input): SV_Target
{
half2 uv = input.uv - _Center.xy;
half2 normalizedUV = normalize(uv);

half textureMask = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, normalizedUV).r;
return half4(textureMask, textureMask, textureMask, 1);
}
ENDHLSL
}
}
}

实现的效果如下所示:

主要的功能都在片元着色器中。首先,根据中心点偏移UV,目前的Center设置成\((0.5,0.5)\),目的是把UV的中心移动到图像的中心位置。然后,对偏移后的UV归一化,这一步达到的效果是,对于方向相同的UV,取值都相同,也就是说,对于同一方向的UV,对纹理采样的结果都是相同的。这样,就会形成从中心点往周围发散的效果。对于上图的效果图,使用的NoiseTex纹理比较特殊,是亮暗方格间隔的,选择这张图,主要是为了更好地展示上述采样的逻辑。(仔细看一下上图,黑色部分是不是就像上面的速度线了。)

速度线不是静止的,是有快速变化的,接下来,在上面的基础上做一些旋转的效果。

主要的改变都在片元着色器中:

1
2
3
4
5
6
7
8
9
10
11
12
13
half2 uv = input.uv - _Center.xy;

half angle = radians(_RotateSpeed * _Time.y);

half sinAngle, cosAngle;
sincos(angle, sinAngle, cosAngle);

half2x2 rotateMatrix = half2x2(cosAngle, -sinAngle, sinAngle, cosAngle);

half2 normalizedUV = normalize(mul(rotateMatrix, uv));

half textureMask = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, normalizedUV).r;
return half4(textureMask, textureMask, textureMask, 1);

添加了一个速度参数_RotateSpeed,根据时间变化,计算出旋转的角度angle。注意,这里的计算得到的angle是弧度,再使用sincos函数计算得到angle的正弦和余弦值。然后,构造2维的旋转矩阵rotateMatrix,对UV进行旋转后再计算得到归一化的normalizedUV。

实现的效果如下所示:

观察发现,上面的效果很明显地往一个方向旋转,而速度线并不是往一个方向旋转的。此时,可以加一个反方向的旋转,这样,就可以抵消这种单方向的旋转。

主要的改变都在片元着色器中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
half2 uv = input.uv - _Center.xy;

half angle = radians(_RotateSpeed * _Time.y);

half sinAngle, cosAngle;
sincos(angle, sinAngle, cosAngle);

half2x2 rotateMatrix0 = half2x2(cosAngle, -sinAngle, sinAngle, cosAngle);
half2 normalizedUV0 = normalize(mul(rotateMatrix0, uv));

half2x2 rotateMatrix1 = half2x2(cosAngle, sinAngle, -sinAngle, cosAngle);
half2 normalizedUV1 = normalize(mul(rotateMatrix1, uv));

half textureMask = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, normalizedUV0).r * SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, normalizedUV1).r;
return half4(textureMask, textureMask, textureMask, 1);

主要的区别是,添加了rotateMatrix1的计算,这里的有一点需要注意一些,对于反方向的旋转: \[ \begin{aligned} cos(-angle) = cos(angle)\\ sin(-angle) = -sin(angle) \end{aligned} \] 然后,计算得到normalizedUV1。使用normalizedUV0和normalizedUV1分别对纹理采样,将采样的结果相乘,就可以解决上面的往一个方向旋转的问题了。

实现的效果如下所示:

再观察一下最前面的效果图,发现黑色线条分布在边缘。很容易可以想到,可以根据UV距离中心点的距离对上面的结果做一个叠加,也就是根据UV距离做一个遮罩。

主要的改变都在片元着色器中:

1
2
3
4
5
6
7
half textureMask = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, normalizedUV0).r * SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, normalizedUV1).r;

half uvMask = pow(_RayMultiply * length(uv), _RayPower);

half mask = textureMask * uvMask;

return half4(mask, mask, mask, 1);

textureMask是之前计算得到的纹理遮罩,而uvMask是新计算的UV遮罩。这里计算uvMask,使用的是幂函数,使用幂函数,相对于直接使用uv的长度,幂函数曲线比直线更加平缓。然后对textureMask和uvMask进行叠加,得到最终的mask。

实现的效果如下所示:

目前,使用的NoiseTex是亮暗间隔的格子图。真正使用的,是噪声图。下面替换成噪声图。

实现的效果如下所示:

可以发现,边缘的条纹太密集了。这里,可以添加一个阈值,对上面计算得到的mask进行分层。

主要改变的片元着色器代码如下:

1
half mask = smoothstep(_Threshold - 0.1, _Threshold + 0.1, textureMask * uvMask);

主要就是使用_Threshold对纹理遮罩和UV遮罩做一个分层,这里,使用smoothstep函数,使分层的边缘不会那么的“硬”。

实现的效果如下所示:

到这里,与开头的速度线效果相比较,已经神似了。下面,给速度线条加上颜色控制。

主要改变的片元着色器代码如下:

1
2
3
half mask = smoothstep(_Threshold - 0.1, _Threshold + 0.1, textureMask * uvMask);

return half4(_TintColor.rgb, mask * _TintColor.a);

主要的区别在返回值部分。返回了_TintColor.rgb作为颜色,使用遮罩mask和_TintColor.a相乘的结果作为透明度。

这里需要注意一下,目前使用的混合模式如下:

1
Blend SrcAlpha OneMinusSrcAlpha

速度线目前使用面片实现的,是按照半透明物体渲染的。上面的混合模式,可以得到速度线与后面的场景混合的效果。当然,也可以使用如下的混合模式:

1
Blend One OneMinusSrcAlpha

此时,最终返回的颜色,要自己做颜色与透明度的叠加了:

1
return half4(_TintColor.rgb * mask * _TintColor.a, mask * _TintColor.a);

实现的效果如下所示:

最终的代码如下

扩展

上面实现了基本的速度线效果。但速度线一般是全屏的。可以使用粒子、全屏的面片或者后处理实现全屏的效果,这里不再赘述。另外,还可以进行扩展。目前使用的是单层的速度线,也可以经过改变获得双层甚至多层的速度线。主要的算法都是上面的。考虑到对NoiseTex采样次数多了会对性能造成影响,这里,可以充分利用NoiseTex的其它通道,一次采样,可以获得四个通道的数据,根据不同通道的数据,可以实现多层速度线的效果。

参考

-------------本文结束感谢您的阅读-------------