While I can often understand the performance reasons behind using fixed-size arrays to store vertex, triangle and UV data, I've often found it annoying because it makes dynamically generating meshes somewhat difficult. In order to fix this, I've built a small helper class to make the process much easier. It currently only supports vertex, triangle and UV generation, but that's really all I need at the moment.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace VoxelIslands.Engine.Utilities
{
/// <summary>
/// This class is simply a wrapper used to dynamically generate mesh data for
/// things like voxels and chunks. In the end, it will be converted to a normal
/// Unity3D mesh.
/// </summary>
public class DynamicMesh
{
private List<Vector3> Vertices { get; set; }
private List<int> Triangles { get; set; }
private List<Vector2> UVs { get; set; }
private List<Vector3> ColliderVertices { get; set; }
private List<int> ColliderTriangles { get; set; }
private Vector3 MeshOffset { get; set; }
private bool _GenerateColliderData { get; set; }
/// <summary>
/// Constructor for the DynamicMesh class.
/// </summary>
/// <param name="meshOffset">The offset of the mesh and it's vertices.</param>
/// <param name="generateColliderData">Whether or not to generate collider data.</param>
public DynamicMesh(Vector3 meshOffset, bool generateColliderData = true)
{
this.Vertices = new List<Vector3>() { };
this.Triangles = new List<int>() { };
this.UVs = new List<Vector2>() { };
this.ColliderVertices = new List<Vector3>() { };
this.ColliderTriangles = new List<int>() { };
this.MeshOffset = meshOffset;
this._GenerateColliderData = generateColliderData;
}
/// <summary>
/// This function creates a new Unity3D mesh from the collider data contained in the vertex,
/// and triangle data. The mesh returned by this function is intended to be used for collisions
/// only.
/// </summary>
/// <returns>A newly created collider mesh.</returns>
public Mesh CreateColliderMesh()
{
Mesh colliderMesh = new Mesh();
colliderMesh.vertices = this.ColliderVertices.ToArray();
colliderMesh.triangles = this.ColliderTriangles.ToArray();
colliderMesh.RecalculateNormals();
return colliderMesh;
}
/// <summary>
/// This function creates a new Unity3D mesh from the data contained in the vertex, triangle
/// and UV data. The mesh returned by this function is intended to be used for rendering only.
/// </summary>
/// <returns>A newly created rendering mesh.</returns>
public Mesh CreateRenderingMesh()
{
Mesh renderingMesh = new Mesh();
renderingMesh.vertices = this.Vertices.ToArray();
renderingMesh.triangles = this.Triangles.ToArray();
renderingMesh.RecalculateNormals();
renderingMesh.uv = this.UVs.ToArray();
return renderingMesh;
}
/// <summary>
/// Add a new UV coordinate to the UV data.
/// </summary>
/// <param name="uvCoordinate"></param>
public void AddUVs(Vector2[] uvCoordinates)
{
this.UVs.AddRange(uvCoordinates);
}
/// <summary>
/// Add two trianges to form a quad. This is based of the most recent vertex
/// data and will not work if your vertex data is empty. Collision data is generated
/// only if collision mesh generation is enabled. There is however an optional
/// parameter that allows for you to enable or disable collider generation on the fly.
/// </summary>
/// <param name="generateColliderData">Whether or not to generate collider data.</param>
public void AddQuad(bool generateColliderData = true)
{
if(this.Vertices.Count >= 4)
{
// Generate the first renderable triangle.
this.Triangles.Add(this.Vertices.Count - 4);
this.Triangles.Add(this.Vertices.Count - 3);
this.Triangles.Add(this.Vertices.Count - 2);
// Generate the second renderable triangle.
this.Triangles.Add(this.Vertices.Count - 4);
this.Triangles.Add(this.Vertices.Count - 2);
this.Triangles.Add(this.Vertices.Count - 1);
if(this._GenerateColliderData && generateColliderData)
{
// Generate the first collider triangle.
this.ColliderTriangles.Add(this.ColliderVertices.Count - 4);
this.ColliderTriangles.Add(this.ColliderVertices.Count - 3);
this.ColliderTriangles.Add(this.ColliderVertices.Count - 2);
// Generate the second collider triangle.
this.ColliderTriangles.Add(this.ColliderVertices.Count - 4);
this.ColliderTriangles.Add(this.ColliderVertices.Count - 2);
this.ColliderTriangles.Add(this.ColliderVertices.Count - 1);
}
}
else
{
throw new System.Exception("Rendering vertex data must contain enough vertices to generate a quad.");
}
}
/// <summary>
/// Add a vertex to the vertex data list. A collider vertex is generated only
/// if collision mesh generation is enabled. There is however an optional
/// parameter that allows for you to enable or disable collider generation on the fly.
/// </summary>
/// <param name="vertexPosition">The position of the vertex.</param>
/// <param name="vertexOffset">The offset of the vertex.</param>
/// <param name="generateColliderData">Whether or not to generate collider data.</param>
public void AddVertex(Vector3 vertexPosition, Vector3 vertexOffset, bool generateColliderData = true)
{
this.Vertices.Add((vertexPosition - this.MeshOffset) + vertexOffset);
if(this._GenerateColliderData && generateColliderData)
{
this.ColliderVertices.Add((vertexPosition - this.MeshOffset) + vertexOffset);
}
}
}
}
Is there anything that can be improved here?
This post is part one of a series based off of the contents of this GitHub repository
2 Answers 2
I don't see much refactoring there, mostly just a matter of taste.
There are a lot of this
is - you can do without them, most of the time they are only code bloat.
I prefer inversing the condition in the AddQuad
method to reduce nesting and remove the last else
if(this.Vertices.Count < 4)
{
throw...
}
One of the properies begins with an _
underscore, is there a reason for that?
As you don't expose any of them publicly I'd turn them to regular fields.
You don't need to use {}
for creating empty lists this new List<int>() { }
is the same as new List<int>()
. You can also initialize them together with the declaration.
It might be a good idea to prefix boolean variables with is or can if it makes sense.
You don't need to always explitly specify the type. If you define a new variable inside a method etc. you can use the var
keyword to not repeat yourself.
Example:
public class DynamicMesh
{
private List<Vector3> _vertices = new List<Vector3>();
private List<int> _triangles = new List<int>();
private List<Vector2> _uvs = new List<Vector2>();
private List<Vector3> _colliderVertices = new List<Vector3>();
private List<int> _colliderTriangles = new List<int>();
private Vector3 _meshOffset;
private bool _canGnerateColliderData;
/// <summary>
/// Constructor for the DynamicMesh class.
/// </summary>
/// <param name="meshOffset">The offset of the mesh and it's vertices.</param>
/// <param name="generateColliderData">Whether or not to generate collider data.</param>
public DynamicMesh(Vector3 meshOffset, bool canGenerateColliderData = true)
{
_meshOffset = meshOffset;
_canGnerateColliderData = canGenerateColliderData;
}
/// <summary>
/// This function creates a new Unity3D mesh from the collider data contained in the vertex,
/// and triangle data. The mesh returned by this function is intended to be used for collisions
/// only.
/// </summary>
/// <returns>A newly created collider mesh.</returns>
public Mesh CreateColliderMesh()
{
// using object initializer
var colliderMesh = new Mesh
{
vertices = _colliderVertices.ToArray(),
triangles = _colliderTriangles.ToArray(),
};
colliderMesh.RecalculateNormals();
return colliderMesh;
}
/// <summary>
/// This function creates a new Unity3D mesh from the data contained in the vertex, triangle
/// and UV data. The mesh returned by this function is intended to be used for rendering only.
/// </summary>
/// <returns>A newly created rendering mesh.</returns>
public Mesh CreateRenderingMesh()
{
var renderingMesh = new Mesh
{
vertices = _vertices.ToArray(),
triangles = _triangles.ToArray(),
uv = _uvs.ToArray(),
};
renderingMesh.RecalculateNormals();
return renderingMesh;
}
/// <summary>
/// Add a new UV coordinate to the UV data.
/// </summary>
/// <param name="uvCoordinate"></param>
public void AddUVs(Vector2[] uvCoordinates)
{
_uvs.AddRange(uvCoordinates);
}
/// <summary>
/// Add two trianges to form a quad. This is based of the most recent vertex
/// data and will not work if your vertex data is empty. Collision data is generated
/// only if collision mesh generation is enabled. There is however an optional
/// parameter that allows for you to enable or disable collider generation on the fly.
/// </summary>
/// <param name="generateColliderData">Whether or not to generate collider data.</param>
public void AddQuad(bool canGenerateColliderData = true)
{
if (_vertices.Count < 4)
{
throw new System.Exception("Rendering vertex data must contain enough vertices to generate a quad.");
}
// Generate the first renderable triangle.
_triangles.Add(_vertices.Count - 4);
_triangles.Add(_vertices.Count - 3);
_triangles.Add(_vertices.Count - 2);
// Generate the second renderable triangle.
_triangles.Add(_vertices.Count - 4);
_triangles.Add(_vertices.Count - 2);
_triangles.Add(_vertices.Count - 1);
if (_canGnerateColliderData && canGenerateColliderData)
{
// Generate the first collider triangle.
_colliderTriangles.Add(_colliderVertices.Count - 4);
_colliderTriangles.Add(_colliderVertices.Count - 3);
_colliderTriangles.Add(_colliderVertices.Count - 2);
// Generate the second collider triangle.
_colliderTriangles.Add(_colliderVertices.Count - 4);
_colliderTriangles.Add(_colliderVertices.Count - 2);
_colliderTriangles.Add(_colliderVertices.Count - 1);
}
}
/// <summary>
/// Add a vertex to the vertex data list. A collider vertex is generated only
/// if collision mesh generation is enabled. There is however an optional
/// parameter that allows for you to enable or disable collider generation on the fly.
/// </summary>
/// <param name="vertexPosition">The position of the vertex.</param>
/// <param name="vertexOffset">The offset of the vertex.</param>
/// <param name="generateColliderData">Whether or not to generate collider data.</param>
public void AddVertex(Vector3 vertexPosition, Vector3 vertexOffset, bool canGenerateColliderData = true)
{
// perform calculation only once, then add its result whereever needed
var vertexWithOffset = (vertexPosition - _meshOffset) + vertexOffset;
_vertices.Add(vertexWithOffset);
if (_canGnerateColliderData && canGenerateColliderData)
{
_colliderVertices.Add(vertexWithOffset);
}
}
}
The logical duplication here is both tedious and potentially error-prone:
// Generate the first renderable triangle. this.Triangles.Add(this.Vertices.Count - 4); this.Triangles.Add(this.Vertices.Count - 3); this.Triangles.Add(this.Vertices.Count - 2); // Generate the second renderable triangle. this.Triangles.Add(this.Vertices.Count - 4); this.Triangles.Add(this.Vertices.Count - 2); this.Triangles.Add(this.Vertices.Count - 1); if(this._GenerateColliderData && generateColliderData) { // Generate the first collider triangle. this.ColliderTriangles.Add(this.ColliderVertices.Count - 4); this.ColliderTriangles.Add(this.ColliderVertices.Count - 3); this.ColliderTriangles.Add(this.ColliderVertices.Count - 2); // Generate the second collider triangle. this.ColliderTriangles.Add(this.ColliderVertices.Count - 4); this.ColliderTriangles.Add(this.ColliderVertices.Count - 2); this.ColliderTriangles.Add(this.ColliderVertices.Count - 1); }
I suggest to remedy with helper methods:
private void AddFirstTriangle(List<int> triangles, int base)
{
triangles.Add(base - 4);
triangles.Add(base - 3);
triangles.Add(base - 2);
}
private void AddSecondTriangle(List<int> triangles, int base)
{
triangles.Add(base - 4);
triangles.Add(base - 2);
triangles.Add(base - 1);
}
Which will simplify the original code to:
AddFirstTriangle(Triangles, Vertices.Count);
AddSecondTriangle(Triangles, Vertices.Count);
if(this._GenerateColliderData && generateColliderData)
{
AddFirstTriangle(ColliderTriangles, ColliderVertices.Count);
AddSecondTriangle(ColliderTriangles, ColliderVertices.Count);
}
You could go one step further with another helper method:
private void AddTriangles(List<int> triangles, int base)
{
AddFirstTriangle(triangles, base);
AddSecondTriangle(triangles, base);
}