[Godot] CPU Particles 3D 制作面片刀光特效 3

373 阅读6分钟

第一遍看视频以为我能不照搬他的步骤就得到好的效果,现在看来是我想多了

Unity VFX Graph - Sword Slash Tutorial

1.Voronoi

1.1 教程

要照搬教程,首先要搞出一个 Voronoi

感觉还行的教程:

【浅入浅出】教你实现最简单的Voronoi图

老大哥教程:

网格噪声(Cellular Noise)

1.2 复现

按照教程来复现:

Voronoi 是一个由若干个无缝邻接的多边形构成的图案,某一条公共边是共享这条边的两个多边形的中心的垂直平分线

首先做一个显示随机点的 shader

shader_type canvas_item;

vec2 hash2( vec2 p )
{
    // procedural white noise	
	return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
}

void fragment() {
	float sum = 0.0;
	for (float j = 0.0;j < 50.0;j++)
	{
		vec2 n = hash2(vec2(j,j));
		vec2 p = sin(n*TIME);
		p = p*0.5+0.5;
		float d = length(UV-p);
		sum += smoothstep(0.02, 0.01, d);
	}
	COLOR = vec4(1.0) * sum;
}

output.gif

对于同一个 hash 函数,种子不变,键不变,值就不变

因此对于 shader 即使是对每一个像素点运行一次,得到的若干个随机点的位置不会变

shader 对每一个像素点运行,UV 到各个随机点的距离取 smoothstep,接近 smoothstep 第一个参数时结果为 0,也就是黑色,接近第二个参数时结果为 1,也就是白色。因此像素点位于任意一个随机点附近时为白色,不在任何像素点附近时为黑色,这样就画出了各个白色的随机点

这个 shader 每一个像素点的颜色是到各个随机点的 smoothstep 的距离的和,而期望的 Voronoi 图的各个多边形的“半径”,是多边形的中心到两个多边形的中心的垂直平分线

如果要找确切的“垂直平分线”,也不是不行,但是感觉有点麻烦。所以这里每一个像素点的颜色可以取到各个随机点的距离的最小值

shader_type canvas_item;

vec2 hash2( vec2 p )
{
    // procedural white noise	
	return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
}

// return distance, and cell id
vec2 voronoi( in vec2 x )
{
    vec2 n = floor( x );
    vec2 f = fract( x );

	vec3 m = vec3( 8.0 );
    for( int j=-1; j<=1; j++ )
    for( int i=-1; i<=1; i++ )
    {
        vec2  g = vec2( float(i), float(j) );
        vec2  o = hash2( n + g );
      //vec2  r = g - f + o;
	    vec2  r = g - f + (0.5+0.5*sin(TIME+6.2831*o));
		float d = dot( r, r );
        if( d<m.x )
            m = vec3( d, o );
    }

    return vec2( sqrt(m.x), m.y+m.z );
}

void fragment() {
	float sum = 0.0;
	float minDist = 100.0;
	
	for (float j = 0.0;j < 50.0;j++)
	{
		vec2 n = hash2(vec2(j,j));
		vec2 p = sin(n*TIME);
		p = p*0.5+0.5;
		float d = length(UV-p);
		if ( d < minDist )
		{
			minDist = d;
		}
	}
	COLOR = vec4(vec3(1.0) * minDist, 1.0);
}

output.gif

如果要增多多边形的数目的话,比较次数会成三次方增长,这是不合理的

解决方法为:

GLSL 对 for 循环和 数组 似乎不太友好。如前所说,循环不接受动态的迭代次数。还有,遍历很多实例会显著地降低着色器的性能。这意味着我们不能把这个方法用在很大的特征点集上。我们需要寻找另一个策略,一个能利用 GPU 并行架构优势的策略。解决这个问题的一个方法是把空间分割成网格。并不需要计算每一个像素点到每一个特征点的距离,对吧?已经知道每个像素点是在自己的线程中运行,我们可以把空间分割成网格(cells),每个网格对应一个特征点。另外,为避免网格交界区域的偏差,我们需要计算像素点到相邻网格中的特征点的距离。这就是 Steven Worley 的论文中的主要思想。最后,每个像素点只需要计算到九个特征点的距离:他所在的网格的特征点和相邻的八个网格的特征点。

代码复现如下:

简单的平铺,然后计算每个 tile 内的特征点

shader_type canvas_item;

vec2 hash2( vec2 p )
{
	// procedural white noise	
	return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
}

void fragment() {
	
	// tile
	vec2 UV_scaled = UV  * 3.0;
	vec2 UV_scaled_i = floor(UV_scaled);
	vec2 UV_scaled_f = fract(UV_scaled);
	
	// every tile has a random point
	vec2 point = hash2(UV_scaled_i);
	// every px in tile calc the dis to self random point
	vec2 diff = point - UV_scaled_f;
	float dist = length(diff);
	
	COLOR = vec4(vec3(1.0) * dist, 1.0);
}

截图 2022-09-25 00-04-37.png

平铺,然后计算每个 tile 内各个像素点周围与自身九个 tile 内的随机点之间的最小距离

shader_type canvas_item;

vec2 hash2( vec2 p )
{
	// procedural white noise	
	return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
}

void fragment() {
	
	// tile
	vec2 UV_scaled = UV  * 3.0;
	vec2 UV_scaled_i = floor(UV_scaled);
	vec2 UV_scaled_f = fract(UV_scaled);
	
	// minimum distance
	float m_dist = 1.;
	
	for (int y= -1; y <= 1; y++) {
		for (int x= -1; x <= 1; x++) {
			// Neighbor place in the grid
			vec2 neighbor = vec2(float(x),float(y));
			// Random position from current + neighbor place in the grid
			vec2 point = hash2(UV_scaled_i + neighbor);
			// every px in tile calc the dis to neighbor random point
			vec2 diff = neighbor + point - UV_scaled_f;
			float dist = length(diff);
			// Keep the closer distance
			m_dist = min(m_dist, dist);
		}
	}
	COLOR = vec4(vec3(1.0) * m_dist, 1.0);
}

截图 2022-09-25 00-15-01.png

其中,在计算每个 tile 内各个像素点周围与邻居 tile 内的随机点之间的距离时,省略了整数部分的相减,直接用小数部分相减了

// every px in tile calc the dis to neighbor random point
vec2 diff = neighbor + point - UV_scaled_f;
float dist = length(diff);

也就是说,因为自身和邻居的位置在整数部分上的差异只有一个 x 的 -1 0 1 y 的 -1 0 1,所以就不用再算 UV_scaled_i - UV_scaled_i

再加一行 sin 配 TIME 就可以动起来

// Animate the point
point = 0.5 + 0.5*sin(TIME + 6.2831*point);

2.Polar Coord

我不知道为什么 unity 的极坐标系会这么奇怪

void fragment() {
	
	vec2 st = UV;
	
	float r = length(st - vec2(0.5, 0.5));
	r = pow(r, 0.5);
	r = clamp(1.0 - r, 0.0, 1.0);
	COLOR = vec4(vec3(r), 1.0);
}

这是直接的做法

截图 2022-09-27 00-16-59.png

这是 Unity 的 Polar Coord

截图 2022-09-27 00-17-40.png

看上去就好像我 pow 的指数不够一样

但是实际上我再怎么调 pow 的指数,我这个圆的羽化半径还是比 Unity 中的大

我猜他是不是做了一个 smoothstep,于是

shader_type canvas_item;

uniform float AngleOffset;
uniform float CellDensity = 5.0;

vec2 hash2( vec2 p )
{
	// procedural white noise	
	return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
}

float Voronoi(vec2 coord)
{
	// tile
	vec2 st = coord * CellDensity;
	vec2 st_i = floor(st);
	vec2 st_f = fract(st);
	
	// minimum distance
	float m_dist = 1.;
	
	for (int y= -1; y <= 1; y++) {
		for (int x= -1; x <= 1; x++) {
			// Neighbor place in the grid
			vec2 neighbor = vec2(float(x),float(y));
			// Random position from current + neighbor place in the grid
			vec2 point = hash2(st_i + neighbor);
			// Animate the point
			point = 0.5 + 0.5*sin(AngleOffset + 6.2831*point);
			// every px in tile calc the dis to neighbor random point
			vec2 diff = neighbor + point - st_f;
			float dist = length(diff);
			// Keep the closer distance
			m_dist = min(m_dist, dist);
		}
	}
	
	return m_dist;
}

void fragment() {
	
	vec2 st = UV;
	
	float r = length(st - vec2(0.5, 0.5));
	r = smoothstep(0.0, 1.0, 2.0 * r);
	r = pow(r, 2.0);
	r = clamp(1.0 - r, 0.0, 1.0);
	
	float Voronoi_r = Voronoi(st) * r;
	COLOR = vec4(vec3(Voronoi_r), 1.0);
}

截图 2022-09-27 00-26-37.png

2.Effect

2.1 调试方法

2.1.1 打开预览阳光

有的时候粒子系统是在开着,没有受到 AnimationPlayer 的控制,就因为挂上了一个 shader 所以就看不见了

这很可能是因为 shader 做出来的颜色太浅了,透明部分又多,又没有光打下来,所以预览中就很难看到

图片.png

看不到,就打光

打开预览视窗顶部的预览灯光选项,就不需要在场景中加一个临时的,还需要记得删掉的灯光来打光了

图片.png

2.1.2 换回 canvas_item

直接写 shader 的话,没有 visual shader 那样可以展开每一个节点的预览效果

那就只好把写好的用于 spatial 的 shader 再改回用于 canvas_item 的,然后新建一个 Sprite2d 挂 shader 看效果

2.1 Shader

对于 3D 物体,把 shader 的类型改一下,输出的参数改一下,就差不多了

shader_type spatial;

uniform float Seed = 0.0;
uniform float CellDensity = 10.0;
uniform float VoronoiSpeed = 1.0;
uniform float VoronoiDissolve = 0.5;
uniform float RDissolve = 5.0;

vec2 hash2( vec2 p )
{
	// procedural white noise	
	return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*(43758.5453+Seed));
}

float Voronoi(vec2 coord)
{
	// tile
	vec2 st = coord * CellDensity;
	vec2 st_i = floor(st);
	vec2 st_f = fract(st);
	
	// minimum distance
	float m_dist = 1.;
	
	for (int y= -1; y <= 1; y++) {
		for (int x= -1; x <= 1; x++) {
			// Neighbor place in the grid
			vec2 neighbor = vec2(float(x),float(y));
			// Random position from current + neighbor place in the grid
			vec2 point = hash2(st_i + neighbor);
			// Animate the point
			point = 0.5 + 0.5*sin(VoronoiSpeed * TIME + 6.2831*point);
			// every px in tile calc the dis to neighbor random point
			vec2 diff = neighbor + point - st_f;
			float dist = length(diff);
			// Keep the closer distance
			m_dist = min(m_dist, dist);
		}
	}
	
	return m_dist;
}

void fragment() {
	
	vec2 st = UV;
	
	float r = length(st - vec2(0.5, 0.5));
	r = smoothstep(0.0, 1.0, 2.0 * r);
	r = pow(r, RDissolve);
	r = clamp(1.0 - r, 0.0, 1.0);
	
	float vor = pow(Voronoi(st), VoronoiDissolve);
	vor *= r;
	
	ALBEDO = COLOR.rgb * clamp(vor, 0.5, 1.0);
	ALPHA = COLOR.a * vor;
}

其中 COLOR.rgb * vor 是为了让颜色分布不均匀,更进一步,COLOR.rgb * clamp(vor, 0.5, 1.0) 是为了让颜色不均匀的同时还能保持颜色不要太暗

COLOR.a * vor 单纯只是允许通过控制 COLOR 来控制整体透明度

2.2 Particle3D

教程里面把一个面片特效复制了一份,一个红色在外面,一个黑色在里面

恩……我不知道为什么要把里面加一层黑色的同样的特效,其实我觉得不怎么好看

但总之可以增加丰富度还是好的

因此我也照做了

图片.png

图片.png

图片.png

开 OneShot 的话就不用管 Emitting 了

开 RotateY 之后才能通过 Angle 项的设置控制粒子旋转

Direction 和 Gravity 设置为 0 让粒子位置不变

2.3 AnimationPlayer

图片.png

00.20.3
Pslashvisiblex
Functionrestart()
LightSlashVoronoiDissolve0.510
RDissolve50
Functionrestart()
DarkSlashVoronoiDissolve0.510
RDissolve50

动画的前期通过 VoronoiDissolve 体现溶解的效果 后期通过 RDissolve 完全渐隐特效(即使 VoronoiDissolve 调的很大,在 pow 函数的 x 为 1 的附近也还是会有一些函数值为 1)

2.4 效果

output.gif