一、背景
在上一篇中,我们实现了静态点云,并且尝试直接在CPU中更新点云发现效率非常低。所以这篇文章,我们将更新点云的操作放在ComputeShader中。
二、思路
- 还是自定义mesh创建点云模型
- 点云模型中的顶点坐标和颜色在ComputeShader中赋值
- 自定义UnlitShader中获取ComputeShader中计算得到的顶点坐标和颜色进行渲染
三、实现
自定义mesh
默认情况下,在一个mesh中我们最多可以定义65535个顶点,如果还有更多的顶点那么我们需要修改一个参数。Unity - Scripting API: IndexFormat (unity3d.com)
我们新建一个空的游戏物体,然后给游戏物体添加MeshFilter和MeshRender组件。然后新建一个脚本,这个脚本就是用来初始化点云mesh的。
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class PointCloud : MonoBehaviour
{
public const int Width = 640, Height = 480;
private Vector3[] positions;
private int[] indices;
private Color[] colors;
private Mesh mesh;
// Use this for initialization
void Start()
{
var totalPointNum = Width * Height;
positions = new Vector3[totalPointNum];
indices = new int[totalPointNum];
colors = new Color[totalPointNum];
for (int i = 0; i < totalPointNum; i++)
{
positions[i] = new Vector3(0, 0, 0);
indices[i] = i;
colors[i] = Color.red;
}
mesh = GetComponent<MeshFilter>().mesh;
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
mesh.vertices = positions;
mesh.colors = colors;
mesh.SetIndices(indices, MeshTopology.Points, 0);
}
}
mesh定义好了后我们还需要在Render Shader中给顶点的坐标和颜色重新赋值,这样才是一个动态点云。
RenderShader
Shader "Unlit/PointCloudS"
{
Properties
{
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 col : COLOR0;
float4 vertex : SV_POSITION;
};
struct PointCloudData
{
float3 pos;
float4 col;
};
StructuredBuffer<PointCloudData> PointCloudDataBuffer;
v2f vert (uint id : SV_VertexID)
{
v2f o;
o.vertex = UnityObjectToClipPos(float4(PointCloudDataBuffer[id].pos, 1));
o.col = PointCloudDataBuffer[id].col;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return i.col;
}
ENDCG
}
}
}
比较重要的地方是:
- 在这里我们自定义了点云顶点的坐标和颜色数据类型,也创建了相应buffer用于接收用户传递进来的数据。
- 顶点函数(vert)中的参数使用了顶点索引,这样我们就可以在buffer中找到对应的数据了
为了方便,我们还是将结构体分开吧。
Shader "Unlit/PointCloudS"
{
Properties
{
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 col : COLOR0;
float4 vertex : SV_POSITION;
};
StructuredBuffer<float3> PointPos;
StructuredBuffer<float4> PointCol;
v2f vert (uint id : SV_VertexID)
{
v2f o;
o.vertex = UnityObjectToClipPos(float4(PointPos[id], 1));
o.col = PointCol[id];
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return i.col;
}
ENDCG
}
}
}
那么现在问题来了,我们在Shader中定义的buffer的数据该怎么得到呢???
给Buffer赋值
public class PointCloudCSHelper : MonoBehaviour
{
public Material material;
ComputeBuffer pointPosBuffer;
ComputeBuffer pointColBuffer;
int PointCloudPotNum { get { return PointCloud.Width * PointCloud.Height; } }
void Start()
{
pointPosBuffer = new ComputeBuffer(PointCloudPotNum, 12);
material.SetBuffer("PointPos", pointPosBuffer);
pointColBuffer = new ComputeBuffer(PointCloudPotNum, 16);
material.SetBuffer("PointCol", pointColBuffer);
StartCoroutine(UpdatePointCloudData());
}
IEnumerator UpdatePointCloudData()
{
while (true)
{
//模拟点云数据更新,这种方式当然会占CPU。
Vector3[] pos = GetPositionsFromCsv(@"D:\SIF2610-Demo-Kits-DX-PreV1.06.210901\SnapShots\20210904\1145146883\PointCloud.csv");
Color[] colors = GetColors(PointCloudPotNum);
pointPosBuffer.SetData(pos);
pointColBuffer.SetData(colors);
yield return new WaitForSeconds(1);
}
}
void OnDestroy()
{
pointPosBuffer.Release();
pointPosBuffer.Dispose();
pointColBuffer.Release();
pointColBuffer.Dispose();
}
Vector3[] GetPositionsFromCsv(string path)
{
List<Vector3> pos = new List<Vector3>();
using (StreamReader sr = new StreamReader(path))
{
while (!sr.EndOfStream)
{
var positions = sr.ReadLine().Split(',');
pos.Add(new Vector3(float.Parse(positions[0]), float.Parse(positions[1]), float.Parse(positions[2])));
}
}
return pos.ToArray();
}
private Color[] GetColors(int num)
{
Color[] colors = new Color[num];
for (int i = 0; i < colors.Length; i++)
{
colors[i] = UnityEngine.Random.ColorHSV();
}
return colors;
}
}
代码结构应该也很简单,就是先定义ComputeBuffer然后绑定到Render Shader中已经申明的buffer,最后给buffer赋值所以Render Shader中的值也会更新,那么顶点的坐标和颜色也会更新。
但是上述示例代码中,我们自己随机更新了一些点云坐标和颜色,这一块会占用一点CPU。但是在实际项目中,我们一般会直接拿到坐标和颜色数据。但是也会存在不能直接拿到坐标或者颜色数据的,可能需要我们简单计算一下,但是一般点云数据量比较大,即使简单的计算也会占用大量CPU,所以我们可以放在GPU中计算,也就是ComputeShader。
ComputeShader中计算大数据
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel UpdatePointCloud
// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWStructuredBuffer<float3> PointPos;
RWStructuredBuffer<float4> PointCol;
float Time;
[numthreads(8,8,1)]
void UpdatePointCloud (uint3 gid : SV_GroupID, uint index : SV_GroupIndex)
{
int pindex = gid.x *8*8*1 + index;
PointPos[pindex]=float3(pindex/1000.0-50+ sin(Time) , 20 * cos(pindex)+10*sin(Time), 20 * sin(pindex)+10*sin(Time));
PointCol[pindex]=float4((sin(Time) + 1)*0.5, (cos(Time) + 1)*0.5, abs(cos(Time) + sin(Time)), 1);
}
同样的,我们也在这里申请了一些buffer,这些buffer用于接收从CPU传进来的数据,然后将传递进来的数据做一些简单的计算得到最终的数据。
public class PointCloudCSHelper : MonoBehaviour
{
public ComputeShader shader;
public Material material;
ComputeBuffer pointPosBuffer;
ComputeBuffer pointColBuffer;
int PointCloudPotNum { get { return PointCloud.Width * PointCloud.Height; } }
int kernel;
uint x, y, z;
void Start()
{
kernel = shader.FindKernel("UpdatePointCloud");
pointPosBuffer = new ComputeBuffer(PointCloudPotNum, 12);
shader.SetBuffer(kernel, "PointPos", pointPosBuffer);
material.SetBuffer("PointPos", pointPosBuffer);
pointColBuffer = new ComputeBuffer(PointCloudPotNum, 16);
shader.SetBuffer(kernel, "PointCol", pointColBuffer);
material.SetBuffer("PointCol", pointColBuffer);
shader.GetKernelThreadGroupSizes(kernel, out x, out y, out z);
StartCoroutine(UpdatePointCloudData());
}
IEnumerator UpdatePointCloudData()
{
while (true)
{
shader.SetFloat("Time", Time.time);
shader.Dispatch(kernel, (int)(PointCloudPotNum / (x * y * z)), 1, 1);
yield return new WaitForSeconds(1);
}
}
void OnDestroy()
{
pointPosBuffer.Release();
pointPosBuffer.Dispose();
pointColBuffer.Release();
pointColBuffer.Dispose();
}
}
这里我们将定义的ComputeBuffer同样也关联到了ComputeShader中,注意,RenderShader和ComputeShader中的buffer是同一个,这个buffer的值是在ComputeShader中计算得到的,然后RenderShader只是调用,每一次更新的时候也是执行ComputeShader即可,将需要更新的值重新传入GPU中再次计算一次。
public class PointCloudCSHelper : MonoBehaviour
{
public ComputeShader shader;
public Material material;
ComputeBuffer pointPosBuffer;
ComputeBuffer pointColBuffer;
int PointCloudPotNum { get { return PointCloud.Width * PointCloud.Height; } }
int kernel;
uint x, y, z;
void Start()
{
kernel = shader.FindKernel("UpdatePointCloud");
pointPosBuffer = new ComputeBuffer(PointCloudPotNum, 12);
shader.SetBuffer(kernel, "PointPos", pointPosBuffer);
material.SetBuffer("PointPos", pointPosBuffer);
pointColBuffer = new ComputeBuffer(PointCloudPotNum, 16);
shader.SetBuffer(kernel, "PointCol", pointColBuffer);
material.SetBuffer("PointCol", pointColBuffer);
shader.GetKernelThreadGroupSizes(kernel, out x, out y, out z);
StartCoroutine(UpdatePointCloudData());
}
IEnumerator UpdatePointCloudData()
{
while (true)
{
shader.SetFloat("Time", Time.time);
shader.Dispatch(kernel, (int)(PointCloudPotNum / (x * y * z)), 1, 1);
yield return new WaitForSeconds(1);
}
}
void OnDestroy()
{
pointPosBuffer.Release();
pointPosBuffer.Dispose();
pointColBuffer.Release();
pointColBuffer.Dispose();
}
}
效果
项目代码:heater404/PointCloud (github.com)