4
\$\begingroup\$

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

\$\endgroup\$
4
+100
\$\begingroup\$

I don't see much refactoring there, mostly just a matter of taste.

There are a lot of thisis - 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);
        }
    }
}
\$\endgroup\$
2
\$\begingroup\$

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);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.