[toc]

# 1. 浮动的圆

# 效果拆解

  1. 单体圆的绘制:

Bresenham 并发画圆算法 有了上述并发画圆的方法,我们就可以在每帧绘制一个单体圆。
如果我们想绘制更多的圆,只需要分配更多的 ThreadGroup , 每个线程负责画一个 (如果想也可以画多个)

  1. 圆形如何移动?

只需要在时序上更新圆的中心点即可

  1. 如何实现不同圆有不同的匀速?

这就需要用到 SSBO Shader Storage Buffer Object , 首先需要知道的是,这个 Buffer 的适配性很差,至少移动端基本不同架构的数量相差巨大,可以说不能用 (? 比如有的只允许 30 个,有的却可以允许 10000 个???),但是作为 Demo 可以用来玩玩 直接类似于 CPU 端一样,在 GPU 上分配每个圆的数据 Circle

1
2
3
4
5
6
7
8
9
struct circle
{
float2 origin;
float2 velocity;
float radius;
};

StructuredBuffer<circle> circlesBuffer;

然后在 kernel 里使用线程下标来找到线程对应圆的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
[numthreads(32,1,1)]
void Circles (uint3 id : SV_DispatchThreadID)
{
int2 center = (int2)(circlesBuffer[id.x].origin + circlesBuffer[id.x].velocity * time);
int radius = (int)circlesBuffer[id.x].radius;

while(center.x > texResolution) center.x -= texResolution;
while(center.x < 0) center.x += texResolution;
while(center.y > texResolution) center.y -= texResolution;
while(center.y < 0) center.y += texResolution;

drawCircle(center,radius);
}

# 2. 瞎转圈的星星

# 效果拆解

  1. 从圆心随机一个方向 Vec

  1. 然后求出某一个随机圆切面上的一个 正交基

- 随机一个 x 方向 (sinDir)
- 叉积 vec 与 sinDir 得到一个 cosDir 向量,其中 cosDir 和 sinDir 必然互相垂直 ( 其实和vec也是互相垂直 )
- 则 sinDir 和 cosDir 就是对应圆切面上的一组正交基,然后按单位圆的 x/y 轴计算坐标即可

1
2
3
4
5
6
7
8
9
10
11
12
13
[numthreads(64,1,1)]
void OrbitingStars(uint3 id : SV_DispatchThreadID)
{
float3 sinDir = normalize(random3(id.x) - 0.5);
float3 vec = normalize(random3(id.x + 7.1393) - 0.5);
float3 cosDir = normalize(cross(sinDir,vec));

float scaledTime = time *0.5+random(id.x) * 712.131234;

float3 pos = sinDir * sin(scaledTime) + cosDir * cos(scaledTime);

Result[id.x] = pos * 2;
}
  1. 如何同步数据和显示

实际上这里 ComputeShader 只是用来计算位置的 (这玩意完全可以用 Job 代替,因为使用 ComputeShader 还需要回读,回读性能消耗也很大的), 所以 Dispatch 之后,直接 resultBuffer.GetData(目标数组) 即可,其中 resultBuffer 的顶层封装是 ComputeBuffer , 获取到数据后对 Gameobject 的 Transform 进行赋值 (完全 Demo 做法 XD)

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
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;

public class OrbitingStars : MonoBehaviour
{
public int starCount = 17;
public ComputeShader shader;
public GameObject prefab;

private ComputeBuffer resultBuffer;
private int kernelHandle;
private uint threadGroupSizeX;
private int groupSizeX;
private Vector3[] output;
private Transform[] stars;


// Start is called before the first frame update
void Start()
{
kernelHandle = shader.FindKernel("OrbitingStars");
shader.GetKernelThreadGroupSizes(kernelHandle,out threadGroupSizeX,out _,out _);
groupSizeX = (int)((starCount + threadGroupSizeX - 1) / threadGroupSizeX);

//buffer on the gpu in the ram
resultBuffer = new ComputeBuffer(starCount, UnsafeUtility.SizeOf<Vector3>());
shader.SetBuffer(kernelHandle,"Result",resultBuffer);
output = new Vector3[starCount];

//star we use for visual
stars = new Transform[starCount];
for (int i = 0; i < starCount; ++i)
{
stars[i] = Instantiate(prefab, transform).transform;
}
}

// Update is called once per frame
void Update()
{
shader.SetFloat("time",Time.time);
shader.Dispatch(kernelHandle,groupSizeX,1,1);
resultBuffer.GetData(output);

for (int i = 0; i < stars.Length; ++i)
{
stars[i].localPosition = output[i];
}
}

private void OnDestroy()
{
resultBuffer.Dispose();
}
}


# 3. 简单 Mesh 变形

  1. 计算模型最适合的圆心的位置上 (假如 0.0 在最中心也可以)

  2. 计算每个顶点到圆心的方向向量,然后乘上半径

  3. 使用时间进行 sin/cos 的循环,然后 lerp

Tips, 个人感觉这种其实用隐式表达 ( 如SDF ) 会更好,因为对于顶点而言的数据量是巨大的,直接上传或者回读对于性能的影响也是巨大的

# 代码

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
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain

struct Vertex
{
float3 position;
float3 normal;
};

RWStructuredBuffer<Vertex> vertexBuffer;
StructuredBuffer<Vertex> initialBuffer;

float delta;
float radius;

[numthreads(1,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
float3 initialPos = initialBuffer[id.x].position;
float3 s = float3(normalize(initialPos)*radius*0.01);
float3 pos = lerp(initialPos,s,delta);

float3 initialNormal = initialBuffer[id.x].normal;
float3 snormal = normalize(initialPos);
float3 norm = lerp(initialNormal,snormal,delta);

vertexBuffer[id.x].normal = norm;
vertexBuffer[id.x].position = pos;
}
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
using UnityEngine;
using System.Collections;
using System.Numerics;
using Vector3 = UnityEngine.Vector3;


public class MeshDeform : MonoBehaviour
{
public ComputeShader shader;
[Range(0.5f, 2.0f)]
public float radius;

public struct Vertex
{
public Vector3 position;
public Vector3 normal;

public Vertex(Vector3 p, Vector3 n)
{
position.x = p.x;
position.y = p.y;
position.z = p.z;
normal.x = n.x;
normal.y = n.y;
normal.z = n.z;
}
}


int kernelHandle;
Mesh mesh;

private Vertex[] vertexArray;
private Vertex[] initialArray;
private ComputeBuffer vertexBuffer;
private ComputeBuffer initialBuffer;

// Use this for initialization
void Start()
{

if (InitData())
{
InitShader();
}
}

private bool InitData()
{
kernelHandle = shader.FindKernel("CSMain");

MeshFilter mf = GetComponent<MeshFilter>();

if (mf == null)
{
Debug.Log("No MeshFilter found");
return false;
}

InitVertexArrays(mf.mesh);
InitGPUBuffers();

mesh = mf.mesh;

return true;
}

private void InitShader()
{
shader.SetFloat("radius", radius);

}

private void InitVertexArrays(Mesh mesh)
{
vertexArray = new Vertex[mesh.vertices.Length];
initialArray = new Vertex[mesh.vertices.Length];

for (int i = 0; i < vertexArray.Length; ++i)
{
Vertex v1 = new Vertex(mesh.vertices[i], mesh.normals[i]);
vertexArray[i] = v1;
Vertex v2 = new Vertex(mesh.vertices[i], mesh.normals[i]);
initialArray[i] = v2;
}
}

private void InitGPUBuffers()
{
vertexBuffer = new ComputeBuffer(vertexArray.Length,sizeof(float) * 6);
vertexBuffer.SetData(vertexArray);
initialBuffer = new ComputeBuffer(initialArray.Length, sizeof(float) * 6);
initialBuffer.SetData(initialArray);

shader.SetBuffer(kernelHandle,"vertexBuffer",vertexBuffer);
shader.SetBuffer(kernelHandle,"initialBuffer",initialBuffer);
}

void GetVerticesFromGPU()
{
vertexBuffer.GetData(vertexArray);
Vector3[] vertices = new Vector3[vertexArray.Length];
Vector3[] normals = new Vector3[vertexArray.Length];

for (int i = 0; i < vertexArray.Length; ++i)
{
vertices[i] = vertexArray[i].position;
normals[i] = vertexArray[i].normal;
}

mesh.vertices = vertices;
mesh.normals = normals;
}

void Update(){
if (shader)
{
shader.SetFloat("radius", radius);
float delta = (Mathf.Sin(Time.time) + 1)/ 2;
shader.SetFloat("delta", delta);
shader.Dispatch(kernelHandle, vertexArray.Length, 1, 1);

GetVerticesFromGPU();
}
}

void OnDestroy()
{

}
}