Unity Compute Shader - 7 网格变形

准备工作

在上一章中,我们了解了如何获取Compute Shader Buffer的数据,在本章中,我们将结合前两张所了解的内容,将一个立方体变成球形的效果

首先,我们需要使用blender制作一个立方体的模型,效果如下图:

这个立方体与Unity中自带的立方体不同,在Blender中制作的立方体会分很多段,是为了将其变换为球形时能有更平滑的表面
将制作好的立方体模型导入unity中后,需要勾选Model标签页的 Read/Write选项

获取Mesh数据并传输、接收

接下来创建C#和Compute Shader脚本,名称均为 MeshDeform,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
using UnityEditor.Performance.ProfileAnalyzer;
using UnityEngine;

public class MeshDeform : MonoBehaviour
{
public ComputeShader shader;
public float radius;

private int kernelhandle;
private Mesh mesh;

//存储球形的每个顶点信息
private Vertex[] vertexArray;
//存储立方体的每个顶点信息
private Vertex[] initialArray;

//接收GPU数据的buffer
private ComputeBuffer vertexBuffer;
//初始化的数据传输给GPU的buffer
private ComputeBuffer initialBuffer;

private void Start()
{

}

private void Update()
{

}

private void InitVertexArrays(Mesh _mesh)
{

}

private void InitGPUBuffers()
{

}

private void GetVertexFromGPU()
{

}
}

public struct Vertex
{
public Vector3 vPosition;
public Vector3 vNormal;

public Vertex(Vector3 _p, Vector3 _n)
{
vPosition.x = _p.x;
vPosition.y = _p.y;
vPosition.z = _p.z;

vNormal.x = _n.x;
vNormal.y = _n.y;
vNormal.z = _n.z;
}
}

在以上代码中,我们创建了两个ComputeBuffer,initialBuffer 用来存储立方体中初始状态下每个顶点的位置和法线信息,vertexBuffer 是用来获取Compute Shader计算后的结果
Start 方法中需要初始化自身和Compute Shader中各类的参数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
private void Start()
{
MeshFilter _mf = gameObject.GetComponent<MeshFilter>();
mesh = _mf.mesh;
kernelhandle = shader.FindKernel("CSMain");
shader.SetFloat("radius", radius);
shader.SetFloat("radius", radius);

InitVertexArrays(_mf.mesh);
InitGPUBuffers();
}

初始化完成后,我们需要使用立方体mesh中的顶点数据来初始化 vertexArrayinitialArray 数组,这两个数组在初始化时,里面的数据是完全相同的,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void InitVertexArrays(Mesh _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;
}
}

数组初始化完成后,接下来初始化 vertexBufferinitialBuffer
这两个字段总长度和元素长度都相同,只是 initialBuffer 用来保存立方体状态下mesh的顶点信息数据,vertexBuffer 用来接收数据并对当前立方体mesh进行变换,代码如下

1
2
3
4
5
6
7
8
9
10
11
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);
}

ComputeBuffer初始化完成后,我们需要完成 GetVertexFromGPU 方法,作用是将GPU中的数据保存到 vertexArray 中,并按照顺序赋值给立方体mesh中的每个顶点,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void GetVertexFromGPU()
{
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].vPosition;
_normals[i] = vertexArray[i].vNormal;
}

mesh.vertices = _vertices;
mesh.normals = _normals;
}

最后是在Update方法中,传入一个在0到1之间的浮点数,使Compute Shader可以进行周期性的变化,也就是在球形和立方体之间来回变换,代码如下

1
2
3
4
5
6
7
8
private void Update()
{
float _delta = (Mathf.Sin(Time.time) + 1) / 2;
shader.SetFloat("delta", _delta);
shader.Dispatch(kernelhandle, vertexArray.Length, 1, 1);

GetVertexFromGPU();
}

Compute Shader中计算顶点

完成了C#部分,我们接下来完成Compute 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
#pragma kernel CSMain

struct Vertex
{
float3 position;
float3 normal;
};

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

float delta;
float radius;


[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
float3 initialPos = initialBuffer[id.x].position;
float3 initialNormal = initialBuffer[id.x].normal;

float3 s = float3(normalize(initialPos) * radius);
float3 pos = lerp(initialPos, s, delta);

float3 snormal = normalize(initialPos);
float3 norm = lerp(initialNormal, snormal, delta);

vertexBuffer[id.x].position = pos;
vertexBuffer[id.x].normal = norm;
}

在Compute Shader中,我们先通过 initialBuffer获取到立方体上每个顶点的信息,然后对每个定点进行归一化,再乘以球形的半径,这样做是因为当前顶点从立方体表面到球形表面,实际上是沿着归一化向量的方向运动的,运动的目的地就是归一化向量乘以半径的位置
计算出了每个顶点的初始位置和目标位置,我们就可以用lerp方法求出当前顶点的位置,最后将所有变换后的顶点信息放入 vertexBuffer等待C#脚本来获取

最终我们就实现了一个立方体变为球体的功能了,如下图:

在下一章中,我们将了解如何生成噪点图,以及一些经典噪点图的生成算法,并在Compute Shader中导入和使用它们