第一遍看视频以为我能不照搬他的步骤就得到好的效果,现在看来是我想多了
Unity VFX Graph - Sword Slash Tutorial
1.Voronoi
1.1 教程
要照搬教程,首先要搞出一个 Voronoi
感觉还行的教程:
老大哥教程:
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;
}
对于同一个 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);
}
如果要增多多边形的数目的话,比较次数会成三次方增长,这是不合理的
解决方法为:
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);
}
平铺,然后计算每个 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);
}
其中,在计算每个 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);
}
这是直接的做法
这是 Unity 的 Polar Coord
看上去就好像我 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);
}
2.Effect
2.1 调试方法
2.1.1 打开预览阳光
有的时候粒子系统是在开着,没有受到 AnimationPlayer 的控制,就因为挂上了一个 shader 所以就看不见了
这很可能是因为 shader 做出来的颜色太浅了,透明部分又多,又没有光打下来,所以预览中就很难看到
看不到,就打光
打开预览视窗顶部的预览灯光选项,就不需要在场景中加一个临时的,还需要记得删掉的灯光来打光了
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
教程里面把一个面片特效复制了一份,一个红色在外面,一个黑色在里面
恩……我不知道为什么要把里面加一层黑色的同样的特效,其实我觉得不怎么好看
但总之可以增加丰富度还是好的
因此我也照做了
开 OneShot 的话就不用管 Emitting 了
开 RotateY 之后才能通过 Angle 项的设置控制粒子旋转
Direction 和 Gravity 设置为 0 让粒子位置不变
2.3 AnimationPlayer
| 0 | 0.2 | 0.3 | ||
|---|---|---|---|---|
| Pslash | visible | ✓ | x | |
| Function | restart() | |||
| LightSlash | VoronoiDissolve | 0.5 | 10 | |
| RDissolve | 5 | 0 | ||
| Function | restart() | |||
| DarkSlash | VoronoiDissolve | 0.5 | 10 | |
| RDissolve | 5 | 0 |
动画的前期通过 VoronoiDissolve 体现溶解的效果 后期通过 RDissolve 完全渐隐特效(即使 VoronoiDissolve 调的很大,在 pow 函数的 x 为 1 的附近也还是会有一些函数值为 1)