Unity使用Mesh实现实时点云(一)

一、渲染事物

unity是基于mesh去做渲染的,也就是说想在unity里看见东西的话,就必须使用mesh。它可以来自于其他软件制作的3D模型进行导入,可以是有代码动态生成出来的,也可以是一个sprite、UI元素或者是粒子系统。

mesh是图形硬件用来绘制复杂事物的框架,它至少包含一个顶点集合(这些顶点是三维空间中的一些坐标)以及连接这些点的一组三角形(最基本的2D形状)。这些三角形集合在一起就构成任何mesh所代表的表面形状。由于三角形是平的,是直线的边,所以它们可以用来完美地显示平面和直线的事物,unity中默认为我们创建了胶囊、立方体、球体等模型,在scene的视窗下面有一个下拉菜单Wire frame,可以看到线框展示,也就是mesh网格。

如果想用一个GameObject展示一个3D模型,那么它必须要两个组件才可以:

  1. mesh filter:它决定了你想展示哪一个mesh
  2. mesh renderer:它决定了你应该如何渲染mesh,比如使用什么材质球,是否接收阴影或者投影等。

unity3d 温度云图 unity怎么做云_PointCloud

为什么material是复数的?

mesh renderer可以有多个materials。这主要用于绘制具有多个独立三角形集的mesh,称为subMesh。这些subMesh来自于导入的3D模型。这里不做讨论。

通过调整mesh的material,可以完全改变mesh的表现,unity的默认材料是纯白色的,你可以自己创建一个人新的材质球,并将其拖到游戏对象上来替换它。新的材质球使用的是unity的标准着色器,它会开放一组设置参数来让你调整不同的视觉效果。

纹理与mesh什么关系呢

向mesh中添加大量细节的一个快速方法是提供过一个albedo maps。这是一个纹理贴图,用来表示一个材质球的基本颜色,纹理贴图只有长和宽2个维度,而mesh往往是一个三维物体,所以要达到这个目的,我们需要知道如何将这个纹理投射到mesh的三角形上。这其实是通过向顶点添加二维纹理坐标来完成的。纹理空间的两个维度称为u和v,这就是为什么他们被称为uv坐标。这些坐标通常位于(0,0)和(1,1)之间,覆盖整个纹理图。根据纹理设置,该范围外的坐标要么被收紧,要么导致tiled。

unity3d 温度云图 unity怎么做云_点云_02

二、自定义顶点网格

现在我们创建一个简单的平面网格,先创建一个空游戏物体后再创建一个脚本

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class MyGrid : MonoBehaviour
{

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
}

RequireComponent特性会在挂载的物体上自动记载对应的组件。然后给mesh renderer设置材质,让mesh filter保持未引用的状态。

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class MyGrid : MonoBehaviour
{
    //矩形网格的大小  单位都是顶点个数
    const int width = 10, height = 5;

    private Vector3[] vectices;
    private int[] indices;
    private Color[] colors;
    // Use this for initialization
    void Start()
    {
        vectices = new Vector3[width * height];
        indices = new int[width * height];
        colors = new Color[width * height];
        for (int i = 0; i < height; i++)
        {
            for (int j = 0; j < width; j++)
            {
                vectices[i * width + j] = new Vector3(j, i);
                indices[i * width + j] = i * width + j;
                colors[i * width + j] = Color.green;
            }
        }

        var mesh = GetComponent<MeshFilter>().mesh;
        mesh.vertices = vectices;
        mesh.SetIndices(indices,MeshTopology.Points,0);
        mesh.colors = colors;
    }
}

这里有一个细节,材质的标准shader是不会显示顶点的颜色的,除非把shader设置为particles中的一个。

mesh的基本单元是可以设置的,我们上面有提到过三角形只是其中的一种,由MeshTopology枚举类型决定。

unity3d 温度云图 unity怎么做云_PointCloud_03

虽然,我们的重点是Points,但是还是想讲一讲triangles,因为可以顺便交易将uv坐标。

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class MyGrid : MonoBehaviour
{
    //矩形网格的大小  单位都是顶点个数
    const int width = 10, height = 5;

    private Vector3[] vectices;
    private int[] indices;
    private Color[] colors;
    private int[] triangles;
    // Use this for initialization
    void Start()
    {
        vectices = new Vector3[width * height];
        indices = new int[width * height];
        colors = new Color[width * height];
        triangles = new int[width * height * 6];//三角形的个数
        for (int i = 0; i < height; i++)
        {
            for (int j = 0; j < width; j++)
            {
                var index = i * width + j;

                vectices[index] = new Vector3(j, i);
                indices[index] = index;
                colors[index] = Color.green;

                if (i < height - 1 && j < width - 1)
                {
                    triangles[6 * index] = index;//顶点索引
                    triangles[6 * index + 1] = triangles[6 * index + 4] = index + width;
                    triangles[6 * index + 2] = triangles[6 * index + 3] = index + 1;
                    triangles[6 * index + 5] = index + width + 1;
                }
            }
        }
        var mesh = GetComponent<MeshFilter>().mesh;
        mesh.vertices = vectices;
        mesh.colors = colors;
        mesh.triangles = triangles;
    }
}

unity3d 温度云图 unity怎么做云_点云_04

但是这样我们有一个疑问?我们材质的贴图呢?原来如果不提供uv坐标,那么它们都是默认值也就是零,所以每一个顶点颜色都是贴图纹理坐标为零的像素的颜色。

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class MyGrid : MonoBehaviour
{
    //矩形网格的大小  单位都是顶点个数
    const int width = 10, height = 5;

    private Vector3[] vectices;
    private int[] indices;
    private Color[] colors;
    private int[] triangles;
    private Vector2[] uvs;
    // Use this for initialization
    void Start()
    {
        vectices = new Vector3[width * height];
        indices = new int[width * height];
        colors = new Color[width * height];
        triangles = new int[width * height * 6];//三角形的个数
        uvs = new Vector2[width * height];
        for (int i = 0; i < height; i++)
        {
            for (int j = 0; j < width; j++)
            {
                var index = i * width + j;

                vectices[index] = new Vector3(j, i);
                indices[index] = index;
                colors[index] = Color.green;
                uvs[index] = new Vector2(j * 1.0f / (width - 1), i * 1.0f / (height - 1));
                if (i < height - 1 && j < width - 1)
                {
                    triangles[6 * index] = index;//顶点索引
                    triangles[6 * index + 1] = triangles[6 * index + 4] = index + width;
                    triangles[6 * index + 2] = triangles[6 * index + 3] = index + 1;
                    triangles[6 * index + 5] = index + width + 1;
                }
            }
        }
        var mesh = GetComponent<MeshFilter>().mesh;
        mesh.vertices = vectices;
        mesh.colors = colors;
        mesh.triangles = triangles;
        mesh.uv = uvs;
    }
}

如果想将纹理贴图完全覆盖在mesh上,只需要计算比例就行(uv坐标范围为[0,1])。 最后我们需要将材质的shader设置为标准的。

unity3d 温度云图 unity怎么做云_PointCloud_05

颜色偏暗是因为Light的原因。

三、点云

制作点云的话我们一般都会使用到MeshTopology.Points,而不是三角形。

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class PointCloud : MonoBehaviour
{
    //矩形网格的大小  单位都是顶点个数
    const int width = 1000, height = 500;

    private Vector3[] positions;
    private int[] indices;
    private Color[] colors;
    private Mesh mesh;
    // Use this for initialization
    void Start()
    {
        positions = new Vector3[width * height];
        indices = new int[width * height];
        colors = new Color[width * height];
        for (int i = 0; i < height; i++)
        {
            for (int j = 0; j < width; j++)
            {
                var index = i * width + j;

                positions[index] = new Vector3(0,0,0);
                indices[index] = index;
                colors[index] = Color.green;
            }
        }
        mesh = GetComponent<MeshFilter>().mesh;
        mesh.vertices = positions;
        mesh.colors = colors;
        mesh.SetIndices(indices, MeshTopology.Points, 0);

        StartCoroutine(UpdatePositions());
    }

    IEnumerator UpdatePositions()
    {
        while (true)
        {
            var a = Random.Range(10, 500);
            for (int i = 0; i < height; i++)
            {
                for (int j = 0; j < width; j++)
                {
                    var index = i * width + j;

                    positions[index] = new Vector3(j - width / 2, i, a * Mathf.Sin((j - width / 2) / 10.0f));
                    //indices[index] = index;
                    //colors[index] = Color.green;
                    //uvs[index] = new Vector2(j * 1.0f / (width - 1), i * 1.0f / (height - 1));
                }
            }
            mesh.vertices = positions;
            yield return new WaitForSeconds(1);
        }
    }
}

因为,想做一个动态更新的,所以写了一个协程间隔1秒更新一下坐标。然后为了方便点云的旋转,我们需要实现IDragHandler接口,这个接口的时候需要注意几点:

  • 相机需要挂载PhysicsRaycaster脚本
  • 场景中需要EventSystem物体
  • 挂载该脚本的物体需要一个Collider

又因为我们的点云mesh是Points,它没有三角形结构,所以它不能作为mesh collider的mesh,我们不得以还是使用BoxCollider。问题又来了,BoxCollider的Bounds应该是多少呢?

我们在自定义mesh后可以直接使用mesh.RecalculateBounds();来刷新mesh的Bounds,然后通过mesh.bounds;直接获取。

void CalcBoxColliderBounds()
    {
        mesh.RecalculateBounds();
        this.GetComponent<BoxCollider>().center = mesh.bounds.center;
        this.GetComponent<BoxCollider>().size = mesh.bounds.size;
    }

然后每一次更新点云顶点坐标的时候调用这个方法。最后就是需要实现接口IDragHandler。

public void OnDrag(PointerEventData eventData)
    {
        if (eventData.button == PointerEventData.InputButton.Right)
        {
            this.transform.Rotate(eventData.delta.y, eventData.delta.x, 0);
        }
    }

云顶点坐标的时候调用这个方法。最后就是需要实现接口IDragHandler。

public void OnDrag(PointerEventData eventData)
    {
        if (eventData.button == PointerEventData.InputButton.Right)
        {
            this.transform.Rotate(eventData.delta.y, eventData.delta.x, 0);
        }
    }