一、背景

在上一篇中,我们实现了静态点云,并且尝试直接在CPU中更新点云发现效率非常低。所以这篇文章,我们将更新点云的操作放在ComputeShader中。

二、思路

  1. 还是自定义mesh创建点云模型
  2. 点云模型中的顶点坐标和颜色在ComputeShader中赋值
  3. 自定义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();
    }
}

效果

unity MeshBaker插件使用教程 unity mesh bounds_Time

项目代码:heater404/PointCloud (github.com)