Belated Holiday Cheer


Hiya folks,

I read a clever code example over the holiday break, it rendered an xmas tree using trigonometry functions. I thought it would be interesting to remake this tree in Unity3D, but in an unconventional way. Today, I will create a similar tree using the Entity Component System. Hopefully it will be useful to somebody ❤️.

Entity Component Systems

Generally, Entity Component Systems (hereby refered to as ECS) are a design principle that employs composition instead of inheritance to construct objects/entities. With an ECS the entity is composed of one or more components, acted on by one or more systems. The code and the data are decoupled into component and system containers.

It is commonly agreed, favoring composition over inheritance is a design principle that grants a design greater simplicity and increased flexibility.

Unity 2019 supports ECS as a part of its Data-Oriented Technology Stack (hereby refered to as DOTS). DOTS consists of a multi-threaded job system, an ECS, and a burst compiler. The goal of DOTS is to easily grant vastly improved performance in Unity3D, while facilitatitating good data-oriented design.

T * sin (t)

In the example code that I have read, the tree itself is constructed using a method very similar to the following illustration:

trig func

If the above example was flattened onto the x/y plane at the point that the animation completes, it would look like a circle. This is because the radius does not change over time. If the radius converged on 0 at the end of the animation, it would flatten to a spiral.

If you picture this in your mind, flattened onto polar coordinates, it would look like the following Archemedean Spiral. spiral

There is an Archemedean Spiral that I very much like, it is called Fermats Spiral. The equation for Fermats Spiral is: $$ r^2 = a^2\theta $$

This function generates a spiral consisting of positive and negative branches as shown in the following illustration.

fermat spiral

I like this spiral because it does not always give a uniform distance between neighboring arcs. (Incidentally, spiral patterns are fascinating and quite beautiful. If you are curious, a list of common spirals are maintained here. Try reworking the example code using a different spiral for added fun 😌.)

If you picture the origin of the above graph being the highest point, and the branches sloping downwards we have a tree pattern. We will use this spiral to make our tree shape.

Hypotrochoid Star

Stars are commonly placed at the top of an xmas tree, we will need to generate one. There is a beautiful roulette curve function that we can use for this purpose. It is called a Hypotrochoid. I find that the best way to visualize and get an intuition for Hypotrochoid is to remember kids spirograph art toys. The Hypotrochoid is like a mathematical spirograph.

spirograph

The pattern above is a Hypotrochoid with the parameters

$$ R = 5\ r = 3\ d = 5 $$

Which produces the following curve (🌟 our star!).

hypotrochoid func

Some Assembly Required

Now that we have the pieces that we require, lets assemble our xmas tree. I am going to use the term voxel to describe each basic unit we use to build our tree. I will use a sphere mesh for these voxels. My code is sketch quality code, so please forgive it.

Components

VoxelIDComponent.cs

This is just an example entity that contains an entity Id setting. It is just used for illustrative purposes.

public struct VoxelIDComponent : IComponentData
{
    public int Id;
}

RotateSpeedComponent.cs

This component contains a speed setting that is used by the example rotate system below.

public struct RotateSpeedComponent : IComponentData
{
    public float Value;
}

Systems

TreeVoxelRotateSystem.cs

This system searches for any entity with a Translation and Rotation and RotateSpeedComponent. It uses the ref keyword so that it can operate on the data on the discovered entities. In this system, we simply rotate the entity rotation using the data in the rotate speed component.

public class TreeVoxelRotateSystem : ComponentSystem
{
    const float ANGLE_STEP = 10.0f;

    protected override void OnUpdate() {
        // select entities that have these components
        Entities.ForEach((ref Translation translation, ref Rotation rotation, ref RotateSpeedComponent rotateSpeed) => {
            var time = Time.deltaTime;

            var current = rotation.Value;
            var desired = math.mul(math.normalizesafe(current), quaternion.AxisAngle(math.up(), ANGLE_STEP * (time * rotateSpeed.Value))); //axis, angle
 
            rotation.Value = desired;
        });
    }
}

Putting the code together

XmasTree.cs

This class builds the entities from components, and initializes all of the data correctly.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;    // Entity support
using Unity.Transforms;  // Optimized transform representation
using Unity.Collections; // Native arrays
using Unity.Rendering;   // RenderMesh

public class XmasTree : MonoBehaviour
{
    public const int POSITIVE_DIRECTION = 1;
    public const int NEGATIVE_DIRECTION = -1;

    // This is the mesh that will be used by the renderer. I just used the unity default sphere mesh.
    [SerializeField]
    private Mesh voxelMesh;

    [Header("Properties For Tree")]
    // This is a black and white image that adds a little drama to the sphere rotation.
    [SerializeField]
    private Texture2D treeVoxelTexture;

    // This is the material that is used for the negative branch on the spiral. Its just the standard shader 
    // with colors and emission applied.
    [SerializeField]
    private Material treeSpiralVoxelRedMaterial;

    // This is the material that is used for the positive branch on the spiral. Its just the standard shader 
    // with colors and emission applied.
    [SerializeField]
    private Material treeSpiralVoxelGreenMaterial;

    // This is just the number of sphere voxels to use when generating the tree spiral branches.
    [SerializeField]
    private int treeSpiralVoxelCount = 250;

    [Header("Properties For Star")]
    // This is the scale that is used to scale down the star so that it matches the scale of the
    // tree.
    [SerializeField]
    float starScale = 0.3f;

    // This is the material that is used for the Hypotrochoid. Its just the standard shader 
    // with colors and emission applied.
    [SerializeField]
    private Material treeStarVoxelGoldMaterial;

    private void Start()
    {
        ////////////////////////////////////////////////////////////
        // Apply the black and white texture to the tree branch 
        // materials.
        ////////////////////////////////////////////////////////////
        treeSpiralVoxelRedMaterial.mainTexture = treeVoxelTexture;
        treeSpiralVoxelGreenMaterial.mainTexture = treeVoxelTexture;

        EntityManager entityManager = World.Active.EntityManager;

        ////////////////////////////////////////////////////////////
        // Build Tree Spirals
        ////////////////////////////////////////////////////////////

        // Make an entity archetype for the voxel entities that compose
        // the tree spiral branches.
        EntityArchetype treeSpiralEntityArchetype = entityManager.CreateArchetype(
            typeof(VoxelIDComponent), // voxel id component
            typeof(Translation),      // translation component
            typeof(Rotation),         // rotation component
            typeof(RotateSpeedComponent), // a rotate speed
            typeof(RenderMesh),       // rendermesh component
            typeof(LocalToWorld)      // local to world component (used by renderer)
        );

        //
        // Build the point lists for each tree spiral arm, we split the steps up to 
        // easily handle positive and negative directions
        //
        float thetaStart = 1.0f;
        float thetaEnd = 45.0f;

        List<Vector3> positiveSpiralPts = GetSpiralPts(thetaStart, thetaEnd, treeSpiralVoxelCount, POSITIVE_DIRECTION);
        List<Vector3> negativeSpiralPts = GetSpiralPts(thetaStart, thetaEnd, treeSpiralVoxelCount, NEGATIVE_DIRECTION);

        // Make two native arrays, one for each spiral tree branch.
        NativeArray<Entity> positiveTreeSpiralEntityArray = new NativeArray<Entity>(positiveSpiralPts.Count, Allocator.Temp);
        NativeArray<Entity> negativeTreeSpiralEntityArray = new NativeArray<Entity>(negativeSpiralPts.Count, Allocator.Temp);

        // Populate the entity arrays
        entityManager.CreateEntity(treeSpiralEntityArchetype, positiveTreeSpiralEntityArray);
        entityManager.CreateEntity(treeSpiralEntityArchetype, negativeTreeSpiralEntityArray);

        // For each array, initialize the entity components that need to be initialized
        InitTreeSpiralComponents(entityManager, positiveTreeSpiralEntityArray, positiveSpiralPts, treeSpiralVoxelGreenMaterial, POSITIVE_DIRECTION);
        InitTreeSpiralComponents(entityManager, negativeTreeSpiralEntityArray, negativeSpiralPts, treeSpiralVoxelRedMaterial, NEGATIVE_DIRECTION);

        ////////////////////////////////////////////////////////////
        // Build The Star
        ////////////////////////////////////////////////////////////

        // Make an entity archetype for the voxel entities that compose
        // the tree star. Note that they do not need to be rotated and 
        // so they do not have a rotate component.
        EntityArchetype starEntityArchetype = entityManager.CreateArchetype(
            typeof(VoxelIDComponent), // voxel component
            typeof(Translation),      // translation component
            typeof(RenderMesh),       // rendermesh component
            typeof(LocalToWorld)      // local to world component (used by renderer)
        );

        // Build a list of Hypotrochoid pts that represent the tree star
        // Recall from the illustration, that R = 5, r = 3, d = 5 paramaters makes the star shape
        float outerRadius = 5f;
        float innerRadius = 3f;
        float attachedPointDistance = 5f;
        float stepSize = 1f;
        List<Vector3> hypotrochoidPts = GetHypotrochoid(outerRadius, innerRadius, attachedPointDistance, stepSize, starScale);

        // Make a native array to hold the star point voxel entities
        NativeArray<Entity> starEntityArray = new NativeArray<Entity>(hypotrochoidPts.Count, Allocator.Temp);

        // Populate the array
        entityManager.CreateEntity(starEntityArchetype, starEntityArray);

        // Now initialize all of the entity component data that is requried
        InitTreeStarComponents(entityManager, starEntityArray, hypotrochoidPts, treeStarVoxelGoldMaterial);

        // Clean up native arrays, since they are not managed memory
        negativeTreeSpiralEntityArray.Dispose();
        positiveTreeSpiralEntityArray.Dispose();
        starEntityArray.Dispose();
    }

    //
    // initialize all of the entities for a tree spiral branch, position the voxels in the correct
    // locations -- also apply the correct material and mesh for the MeshRenderer and set up a random
    // rotation speed
    void InitTreeSpiralComponents(EntityManager entityManager, NativeArray<Entity> entityArray, List<Vector3> ptList, Material voxelMaterial, int direction)
    {
        // initialize every entity component datas
        for (int i = 0; i < entityArray.Length; i++)
        {
            // fetch the current voxel entity
            Entity thisVoxelEntity = entityArray[i];

            // set the data for the ID component
            entityManager.SetComponentData(thisVoxelEntity, new VoxelIDComponent { Id = i });

            // set the data for the Translation component
            entityManager.SetComponentData(thisVoxelEntity, new Translation
            {
                Value = new Unity.Mathematics.float3(
                    ptList[i].x,
                    ptList[i].y,
                    ptList[i].z
                )
            });

            // set the data for the rotate speed component, and make it random
            entityManager.SetComponentData(thisVoxelEntity, new RotateSpeedComponent
            {
                Value = Random.Range(0.5f, 2f)
            });

            // set the data for the rendermesh component
            entityManager.SetSharedComponentData(thisVoxelEntity, new RenderMesh
            {
                mesh = voxelMesh,
                material = voxelMaterial
            });
        }
    }

    // initialize all of the entities for a tree star, position the voxels in the correct
    // locations -- also apply the correct material and mesh for the MeshRenderer
    void InitTreeStarComponents(EntityManager entityManager, NativeArray<Entity> entityArray, List<Vector3> ptList, Material voxelMaterial)
    {
        for (int i = 0; i < entityArray.Length; i++)
        {
            Entity entity = entityArray[i];

            // set the data for the ID component
            entityManager.SetComponentData(entity, new VoxelIDComponent { Id = i });

            // set the data for the Translation component
            entityManager.SetComponentData(entity, new Translation
            {
                Value = new Unity.Mathematics.float3(
                    ptList[i].x,
                    ptList[i].y,
                    ptList[i].z
                )
            });

            // set the data for the rendermesh component
            entityManager.SetSharedComponentData(entity, new RenderMesh
            {
                mesh = voxelMesh,
                material = voxelMaterial
            });
        }
    }

    //
    // given a thetastart, thetaend, point count, and direction -- this helper function will
    // output a series of points that make up the spiral
    //
    List<Vector3> GetSpiralPts(float thetaStart, float thetaEnd, int pointCount, int direction)
    {
        float dt = (thetaEnd - thetaStart) / (float)pointCount;
        float y = 0f;

        List<Vector3> points = new List<Vector3>();

        // build up each point on the spiral
        for (int i = 0; i < pointCount; i++)
        {
            float theta = thetaStart + (i * dt);

            // Fermat spiral settings
            float r = Mathf.Sqrt(theta) + (i * 0.01f);

            // Polar to cartesian coords
            float x = direction * r * Mathf.Cos(theta);
            float z = direction * r * Mathf.Sin(theta);

            // this steps the y axis down to slope the spiral branches
            y -= 0.2f;

            points.Add(new Vector3(x, y, z));
        }
        return points;
    }

    //
    // Given an outer and inner radius, the distance to the attached point, a step size, and a scale, return
    // a list of points in 3d space that represend a complete Hypotrochoid
    //
    List<Vector3> GetHypotrochoid(float outerRadius, float innerRadius, float attachedPointDistance, float stepSize, float scale)
    {
        List<Vector3> points = new List<Vector3>();
        for (float theta = 0f; theta < 360; theta += stepSize)
        {
            Vector3 pt = HypotrochoidPointAtTheta(theta, outerRadius, innerRadius, attachedPointDistance, scale);

            // This is some sketch code, we rotate the final Hypotrochoid 20 degrees on the Z axis to make it look
            // like it is resting on the top of the tree.
            pt = RotateVector(pt, new Vector3(0, 0, 20));
            points.Add(pt);
        }
        return points;
    }

    //
    // Given an angle theta, an outer and inner radius, and the distance of the attached point (recall the figure above) 
    // the following helper function returns the point on a Hypotrochoid.
    //
    Vector3 HypotrochoidPointAtTheta(float theta, float outerRadius, float innerRadius, float attachedPointDistance, float scale)
    {
        // this is sketch code, we offset the star a little to make it sit correct
        float xOffset = 1.8f;
        float yOffset = 4.5f;

        Vector3 result = new Vector3();
        float R = outerRadius;
        float r = innerRadius;
        float d = attachedPointDistance;

        result.x = ((R - r) * Mathf.Cos(theta)) + d * Mathf.Cos(((R - r) / r) * theta) + xOffset;
        result.y = ((R - r) * Mathf.Sin(theta)) - d * Mathf.Sin(((R - r) / r) * theta) + yOffset;
        result.z = 0;

        return result * scale;
    }

    // 
    // The Hypotrochoid that we generate needs to be rotated a little in order to look ok. This is a simple
    // utility function that rotates a Vector3 by a specified Euler angle and returns the result.
    //
    Vector3 RotateVector(Vector3 source, Vector3 eulerAngles)
    {
        Quaternion rotation = Quaternion.Euler(eulerAngles.x, eulerAngles.y, eulerAngles.z);
        Matrix4x4 m = Matrix4x4.Rotate(rotation);
        return m.MultiplyPoint3x4(source);
    }
}

And The Finished Product

Voila!

hypotrochoid func