前言
参考效果:
参考文章:
实现效果:
- 其他:
- 初学ComputeShader,只是想到了以前用HDRP中的VFX粒子复刻过的效果,可以作为练习
- 效果中的旋转并非原效果中的旋转规则,可自定义
- 性能上还有许多优化空间还望读者见谅
- 本文仅作为个人学习记录和思路分享
实现思路
0.场景准备
- 准备一个空场景
- 准备好用于触发效果的玩家、一个书本预制体、一个C#脚本、一个ComputeShader。
1.从生成一排/一面书本物体开始,初始化位置和旋转
- 利用ComputeShader将每个书本物体的位置设置为其SV_DispatchThreadID号
- 线程组设为(8, 8, 1),为了性能考虑,线程大小设为(4, 4, 1),后续可自行调整
- 那么一个线程的SV_DispatchThreadID如图所示:
- 整个线程组的排列如图所示:
- 那么,数组大小为(4×8)×(4×8)=32×32,每个单位在数组中的索引可以用id.y * 4 * 8 + id.x来表示,即第几行的第几列,如下图黄色数字所示:
- 而这每个单位都指代一本书所处的位置,为Vector3或float3类型,因此需要一个Vector3[]容器装这些数据,并使用Buffer与GPU进行数据传输
展开代码
- ComputeShader:
1
2
3
4
5
6
7
8
9#pragma kernel InitPosition
RWStructuredBuffer<float3> InitPositions;
[numthreads(4,4,1)]
void InitPosition (uint3 id : SV_DispatchThreadID)
{
InitPositions[id.y * 32 + id.x] = float3(id.x, id.y, 0);
} - C#:
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
54using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MagicLibrary : MonoBehaviour
{
// 1.
[private ComputeShader shader; ]
private int size;
private ComputeBuffer resultInitPosBuffer;
private int kernelInitPosHandle;
private Vector3[] initPositions;
[private GameObject prefab; ]
private List<GameObject> books;
private void Start()
{
size = 32 * 32;
InitShader();
InitBookAndPosition();
}
private void InitShader()
{
// Get KernelID and Init Buffer
kernelInitPosHandle = shader.FindKernel("InitPosition");
resultInitPosBuffer = new ComputeBuffer(size, sizeof(float) * 3);
shader.SetBuffer(kernelInitPosHandle, "InitPositions", resultInitPosBuffer);
// Dispatch initPosition
shader.Dispatch(kernelInitPosHandle, 8, 8, 1);
}
private void InitBookAndPosition()
{
books = new List<GameObject>(size);
// Get Buffer Data and Apply to GameObject
initPositions = new Vector3[size];
resultInitPosBuffer.GetData(initPositions);
for (int i = 0; i < initPositions.Length; i++)
{
GameObject book = Instantiate(prefab, initPositions[i], Quaternion.identity, transform);
books.Add(book);
}
}
private void OnDestroy()
{
resultInitPosBuffer.Release();
}
}
- 第一步效果图:
2.加入随机数、三角函数使物体凌乱并做漂浮运动
- 说到运动,自然是要增加一个核函数,并与CPU端Update联动
- 物体的随机旋转值使用C#里的Quaternion.EulerRotation()实现,参数为物体自身位置。读者也可以使用其他方法自行实现。
- 书本从书架(一个平面)飘出,实际上就是Z轴的偏移,因此只需要计算Z轴的偏移量,而不用把整个三维坐标加入计算,节约性能
展开代码
- ComputeShader:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20#pragma kernel UpdatePosition
// 2
RWStructuredBuffer<float3> UpdatePositions;
float Time;
float ZTimeScale;
float ZStrength;
float Random(float2 p)
{
return frac(sin(p.x * 23.23123 + p.y * 12.335) * 2343523.2345234);
}
[numthreads(4,4,1)]
void UpdatePosition (uint3 id : SV_DispatchThreadID)
{
float z_offset = Random(id.xy);
float flowZ = z_offset * ZStrength + cos(Time * ZTimeScale * z_offset);
UpdatePositions[id.y * 32 + id.x] = float3(id.x, id.y, flowZ);
} - C#:
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// 2.
private int kernelUpdateHandle;
private ComputeBuffer resultUpdatePosBuffer;
private Vector3[] nowPositions;
[private float ZStrength; // Z轴离原平面的距离强度 ]
[private float ZTimeScale; // 漂浮速度 ]
private void InitShader()
{
kernelUpdateHandle = shader.FindKernel("UpdatePosition");
resultUpdatePosBuffer = new ComputeBuffer(size, sizeof(float) * 3);
}
private void InitBookAndPosition()
{
nowPositions = new Vector3[size];
}
private void Update()
{
UpdateBookPosition();
}
private void UpdateBookPosition()
{
// Set Shader param
shader.SetFloat("ZStrength", ZStrength);
shader.SetFloat("ZTimeScale", ZTimeScale);
shader.SetFloat("Time", Time.time);
shader.SetBuffer(kernelUpdateHandle, "UpdatePositions", resultUpdatePosBuffer);
shader.Dispatch(kernelUpdateHandle, 8, 8, 1);
// Get Buffer and Apply to GameObject
resultUpdatePosBuffer.GetData(nowPositions);
for (int i = 0; i < nowPositions.Length; i++)
{
books[i].transform.position = nowPositions[i];
books[i].transform.rotation = Quaternion.EulerRotation(nowPositions[i]);
}
}
private void OnDestroy()
{
// ...
resultUpdatePosBuffer.Release();
}
- 第二步效果图:
3.增加书本物体的位置、旋转对玩家靠近时的响应
- 通过传入玩家的世界坐标,和一些函数运算,来对每个书本单位进行位置和旋转的平滑复位
- 此时会发现一些问题,后续会解决:
- 旋转是在CPU端计算的,性能不好
- CPU端的SmoothStep函数与GPU端效果不同,需要自己实现
- 书本位置、旋转固定,不会跟随其父物体位移和旋转
展开代码
- ComputeShader:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 3
float3 PlayerWorldPos;
float PlayerResponseStartDistance;
float PlayerResponseRange;
[numthreads(4,4,1)]
void UpdatePosition (uint3 id : SV_DispatchThreadID)
{
float3 p = float3(id.x, id.y, 0);
float z_offset = Random(id.xy);
// Reply PlayerPos
float dis = distance(p, PlayerWorldPos);
float flowZ = z_offset * ZStrength + cos(Time * ZTimeScale * z_offset);
float newZ = flowZ * (1 - smoothstep(PlayerResponseStartDistance + PlayerResponseRange, PlayerResponseStartDistance, dis));
UpdatePositions[id.y * 32 + id.x] = float3(id.x, id.y, newZ);
} - C#:
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// 3.
[private float PlayerResponseStartDistance; // 玩家距离能够造成影响的起始距离 ]
[private float PlayerResponseRange; // 玩家距离能够造成影响的范围 ]
[private GameObject player; ]
private void UpdateBookPosition()
{
var playerPos = player.transform.position;
shader.SetVector("PlayerWorldPos", playerPos);
shader.SetFloat("PlayerResponseStartDistance", PlayerResponseStartDistance);
shader.SetFloat("PlayerResponseRange", PlayerResponseRange);
// ...
books[i].transform.rotation = Quaternion.EulerRotation(nowPositions[i] * SmoothStep(PlayerResponseStartDistance, PlayerResponseStartDistance + PlayerResponseRange, Vector3.Distance(playerPos, nowPositions[i])));
}
private float SmoothStep(float a, float b, float t)
{
if (t <= a)
{
return 0;
}
else if (t >= b)
{
return 1;
}
else
{
return (t - a) / (b - a);
}
}
- 第三步效果图:
4.将CPU端的旋转计算转到GPU端进行
- CPU端的旋转是通过Quaternion.EulerRotation()函数中传入位置得到的
- 因此我的思路是再开一个线程组,记录对应物体的旋转,直接在GPU中计算旋转后,在CPU端使用
- 将原先的shader.Dispatch(kernelUpdateHandle, 8, 8, 1)改为(8, 8, 2)
- 相当于增加一张表,表中的索引从32*32-1开始,到32*32*2-1结束,原先记录Vector3信息的下标加上一半的表容量即可得到其对应的旋转值
展开代码
- ComputeShader:
1
2
3
4
5
6
7
8
9
10
11
12
13
14[numthreads(4,4,1)]
void UpdatePosition (uint3 id : SV_DispatchThreadID)
{
float3 p = float3(id.x, id.y, 0);
float z_offset = Random(id.xy);
// Reply PlayerPos
float dis = distance(p, PlayerWorldPos);
float flowZ = z_offset * ZStrength + cos(Time * ZTimeScale * z_offset);
float newZ = flowZ * (1 - smoothstep(PlayerResponseStartDistance + PlayerResponseRange, PlayerResponseStartDistance, dis));
p.z += newZ;
UpdatePositions[32*32*id.z + id.y * 32 + id.x] = p * (id.z == 0 ? 1 : smoothstep(PlayerResponseStartDistance, PlayerResponseStartDistance + PlayerResponseRange, dis));
} - C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24private void InitShader()
{
// ...
resultUpdatePosBuffer = new ComputeBuffer(size * 2, sizeof(float) * 3);
// ...
}
private void InitBookAndPosition()
{
// ...
nowPositions = new Vector3[size * 2];
}
private void UpdateBookPosition()
{
// ...
shader.Dispatch(kernelUpdateHandle, 8, 8, 2);
// ...
for (int i = 0; i < nowPositions.Length / 2; i++)
{
books[i].transform.position = nowPositions[i];
books[i].transform.rotation = Quaternion.EulerRotation(nowPositions[i + nowPositions.Length / 2]);
}
}
5.解决书本不随父物体位移旋转的问题
- 原因是坐标系不同,ComputeShader中计算得出的位移和旋转值,在CPU中赋值给了books[i].transform.position和rotation
- 而不论是改为localPosition还是在Instantiate时不以一个物体为父物体,都存在不好控制、旋转后响应效果错误的问题
- 最终的解决方案是,以一个空物体作为父物体,控制所有的子物体书本进行位移和旋转,并统一为世界坐标系
- CPU端直接将父物体的坐标和旋转传入GPU,使GPU端计算得到的值能够加入父物体的影响,成为世界坐标系
- 子物体的世界坐标=父物体transform的right、up、forward三个分量分别乘上子物体x、y、z轴向上的分量
- 其中又可以通过两个轴的叉乘得到第三个轴的朝向向量
- 此时书本飘出的方向就不是Z轴了,而是父物体的transform.forawrd
- 需要重构部分ComputeShader代码
展开代码
- ComputeShader:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 5
float3 ParentRight;
float3 ParentForward;
float3 ParentPos;
[numthreads(4,4,1)]
void UpdatePosition (uint3 id : SV_DispatchThreadID)
{
float3 p = float3(id.x, id.y, 0);
p = p.x * ParentRight + p.y * cross(ParentForward, ParentRight) + p.z * ParentForward;
p += ParentPos;
float z_offset = Random(id.xy);
// Reply PlayerPos
float dis = distance(p, PlayerWorldPos);
// Rdm flow Z
float flowZ = z_offset * ZStrength + cos(Time * ZTimeScale * z_offset);
float newZ = flowZ * (1 - smoothstep(PlayerResponseStartDistance + PlayerResponseRange, PlayerResponseStartDistance, dis));
p += newZ * ParentForward;
UpdatePositions[32*32*id.z + id.y * 32 + id.x] = p * (id.z == 0 ? 1 : smoothstep(PlayerResponseStartDistance, PlayerResponseStartDistance + PlayerResponseRange, dis));
} - C#:
1
2
3
4
5
6
7
8private void UpdateBookPosition()
{
// ...
shader.SetVector("ParentRight", transform.right);
shader.SetVector("ParentForward", transform.forward);
shader.SetVector("ParentPos", transform.position);
// ...
}
- 第五步效果图:
6.更好的效果控制
- 此时基本的功能都实现了,而在具体使用场景中,还需要控制物体间隔、物体的初始旋转值等参数
- 在这就演示这两项参数的增加
- 还可以加入玩家在书架背面时,不响应效果的设置,通过向量叉乘结果的正负来判断
展开代码
- ComputeShader:
1
2
3
4
5
6
7
8
9// 6
float2 BooksScale;
[numthreads(4,4,1)]
void UpdatePosition (uint3 id : SV_DispatchThreadID)
{
float3 p = float3(id.x * BooksScale.x, id.y * BooksScale.y, 0);
// ...
} - C#:
1
2
3
4
5
6
7
8
9
10
11
12// 6.
[private Vector3 InitRotationEular = new Vector3(0, 0, 0); ]
[private Vector2 InitScale = new Vector2(1, 1); ]
private void UpdateBookPosition()
{
// ...
shader.SetVector("BooksScale", InitScale);
// ...
books[i].transform.rotation = Quaternion.EulerRotation(nowPositions[i + nowPositions.Length / 2]) * Quaternion.Euler(InitRotationEular);
// ...
}
完整效果、参数、代码展示
展开代码
- ComputeShader:
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#pragma kernel InitPosition
#pragma kernel UpdatePosition
// 1
RWStructuredBuffer<float3> InitPositions;
// 2
RWStructuredBuffer<float3> UpdatePositions;
float Time;
float ZTimeScale;
float ZStrength;
// 3
float3 PlayerWorldPos;
float PlayerResponseStartDistance;
float PlayerResponseRange;
// 5
float3 ParentRight;
float3 ParentForward;
float3 ParentPos;
// 6
float2 BooksScale;
float Random(float2 p)
{
return frac(sin(p.x * 23.23123 + p.y * 12.335) * 2343523.2345234);
}
[numthreads(4,4,1)]
void InitPosition (uint3 id : SV_DispatchThreadID)
{
InitPositions[id.y * 32 + id.x] = float3(id.x, id.y, 0);
}
[numthreads(4,4,1)]
void UpdatePosition (uint3 id : SV_DispatchThreadID)
{
float3 p = float3(id.x * BooksScale.x, id.y * BooksScale.y, 0);
p = p.x * ParentRight + p.y * cross(ParentForward, ParentRight) + p.z * ParentForward;
p += ParentPos;
float z_offset = Random(id.xy);
// Reply PlayerPos
float dis = distance(p, PlayerWorldPos);
// Rdm flow Z
float flowZ = z_offset * ZStrength + cos(Time * ZTimeScale * z_offset);
float newZ = flowZ * (1 - smoothstep(PlayerResponseStartDistance + PlayerResponseRange, PlayerResponseStartDistance, dis));
p += newZ * ParentForward;
UpdatePositions[32*32*id.z + id.y * 32 + id.x] = p * (id.z == 0 ? 1 : smoothstep(PlayerResponseStartDistance, PlayerResponseStartDistance + PlayerResponseRange, dis));
} - C#:
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MagicLibrary : MonoBehaviour
{
// 1.
[private ComputeShader shader; ]
private int size;
private ComputeBuffer resultInitPosBuffer;
private int kernelInitPosHandle;
private Vector3[] initPositions;
[private GameObject prefab; ]
private List<GameObject> books;
// 2.
private int kernelUpdateHandle;
private ComputeBuffer resultUpdatePosBuffer;
private Vector3[] nowPositions;
[private float ZStrength; // Z轴离原平面的距离强度 ]
[private float ZTimeScale; // 漂浮速度 ]
// 3.
[private float PlayerResponseStartDistance; // 玩家距离能够造成影响的起始距离 ]
[private float PlayerResponseRange; // 玩家距离能够造成影响的范围 ]
[private GameObject player; ]
// 6.
[private Vector3 InitRotationEular = new Vector3(0, 0, 0); ]
[private Vector2 InitScale = new Vector2(1, 1); ]
private void Start()
{
size = 32 * 32;
InitShader();
InitBookAndPosition();
}
private void Update()
{
UpdateBookPosition();
}
private void InitShader()
{
// Get KernelID and Init Buffer
kernelInitPosHandle = shader.FindKernel("InitPosition");
resultInitPosBuffer = new ComputeBuffer(size, sizeof(float) * 3);
shader.SetBuffer(kernelInitPosHandle, "InitPositions", resultInitPosBuffer);
// Dispatch initPosition
shader.Dispatch(kernelInitPosHandle, 8, 8, 1);
kernelUpdateHandle = shader.FindKernel("UpdatePosition");
resultUpdatePosBuffer = new ComputeBuffer(size * 2, sizeof(float) * 3);
}
private void InitBookAndPosition()
{
books = new List<GameObject>(size);
// Get Buffer Data and Apply to GameObject
initPositions = new Vector3[size];
resultInitPosBuffer.GetData(initPositions);
for (int i = 0; i < initPositions.Length; i++)
{
GameObject book = Instantiate(prefab, initPositions[i], Quaternion.identity, transform);
books.Add(book);
}
nowPositions = new Vector3[size * 2];
}
private void UpdateBookPosition()
{
// Set Shader param
shader.SetFloat("ZStrength", ZStrength);
shader.SetFloat("ZTimeScale", ZTimeScale);
shader.SetFloat("Time", Time.time);
shader.SetVector("ParentRight", transform.right);
shader.SetVector("ParentForward", transform.forward);
shader.SetVector("ParentPos", transform.position);
shader.SetVector("BooksScale", InitScale);
var playerPos = player.transform.position;
shader.SetVector("PlayerWorldPos", playerPos);
shader.SetFloat("PlayerResponseStartDistance", PlayerResponseStartDistance);
shader.SetFloat("PlayerResponseRange", PlayerResponseRange);
shader.SetBuffer(kernelUpdateHandle, "UpdatePositions", resultUpdatePosBuffer);
shader.Dispatch(kernelUpdateHandle, 8, 8, 2);
// Get Buffer and Apply to GameObject
resultUpdatePosBuffer.GetData(nowPositions);
for (int i = 0; i < nowPositions.Length / 2; i++)
{
books[i].transform.position = nowPositions[i];
books[i].transform.rotation = Quaternion.EulerRotation(nowPositions[i + nowPositions.Length / 2]) * Quaternion.Euler(InitRotationEular);
}
}
private void OnDestroy()
{
resultInitPosBuffer.Release();
resultUpdatePosBuffer.Release();
}
}