Unity使用Mesh实现实时点云(一)
一、渲染事物
unity是基于mesh去做渲染的,也就是说想在unity里看见东西的话,就必须使用mesh。它可以来自于其他软件制作的3D模型进行导入,可以是有代码动态生成出来的,也可以是一个sprite、UI元素或者是粒子系统。
mesh是图形硬件用来绘制复杂事物的框架,它至少包含一个顶点集合(这些顶点是三维空间中的一些坐标)以及连接这些点的一组三角形(最基本的2D形状)。这些三角形集合在一起就构成任何mesh所代表的表面形状。由于三角形是平的,是直线的边,所以它们可以用来完美地显示平面和直线的事物,unity中默认为我们创建了胶囊、立方体、球体等模型,在scene的视窗下面有一个下拉菜单Wire frame,可以看到线框展示,也就是mesh网格。
如果想用一个GameObject展示一个3D模型,那么它必须要两个组件才可以:
- mesh filter:它决定了你想展示哪一个mesh
- mesh renderer:它决定了你应该如何渲染mesh,比如使用什么材质球,是否接收阴影或者投影等。
为什么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。
二、自定义顶点网格
现在我们创建一个简单的平面网格,先创建一个空游戏物体后再创建一个脚本
[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枚举类型决定。
虽然,我们的重点是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;
}
}
但是这样我们有一个疑问?我们材质的贴图呢?原来如果不提供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设置为标准的。
颜色偏暗是因为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);
}
}
但是这里有一个问题,当顶点刷新率上升后CPU消耗会上升,那么我们是否可以在GPU中进行顶点坐标更新呢?那么就需要使用到Shader。使用Mesh实现实时点云(二)