Creating organic shapes like trees can be an interesting side project for potential game developers. You can use the same logic to create levels or other complicated logic structures. In this tutorial, we will be creating 2D tree shapes in Unity using two different approaches: Fractal and L-System.
1. Creating 2D With 3D
Although we call these 2D tree shapes, they are essentially 3D mesh objects in Unity. The game object which has the tree script will need to have these 3D mesh components attached in order to create our tree shape. Those components are the MeshRenderer
and the MeshFilter
, as shown below.
With these components attached, we will be creating a new mesh using the different algorithms for fractals and L-Systems.
Creating a Mesh
A 3D mesh is created using multiple vertices which combine to form faces. To make a single face, we will need a minimum of three vertices. When we connect three vertices in a clockwise sequence, we will get a face which has a normal pointing outwards. The visibility of a face is dependent on the direction of its normal, and hence the sequence in which the vertices are passed in to create a face matters. Please read the official Unity documentation for further details regarding the creation of a mesh.
With the power of mesh creation under our belt, let's proceed to our first method for creating a 2D tree: fractal.
2. Creating a Fractal Tree
A fractal is a shape created by repeating a pattern with varying scales. Theoretically a fractal can be an unending pattern, where the base pattern gets repeated indefinitely while its size gets reduced progressively. When it comes to a tree, the base fractal pattern can be a branch splitting into two branches. This base pattern can be repeated to create the symmetrical tree shape shown below.
We will need to stop the repetition after a certain number of iterations, and the result is obviously not a very realistic tree shape. Yet the beauty of this approach—and fractals in general—is that they can be easily created using simple recursive functions. The base pattern drawing method can recursively call itself while reducing the scale until a certain number of iterations are complete.
The Branch
The primary component in a tree shape is a branch. In our approach, we have a Branch
class, which has a CreateBranch
method as shown below.
private void CreateBranch(Vector3 origin, float branchLength,float branchWidth, float branchAngle, Vector3 offset, float widthDecreaseFactor) { Vector3 bottomLeft=new Vector3(origin.x,origin.y,origin.z),bottomRight=new Vector3(origin.x,origin.y,origin.z),topLeft=new Vector3(origin.x,origin.y,origin.z),topRight=new Vector3(origin.x,origin.y,origin.z); bottomLeft.x-=branchWidth*0.5f; bottomRight.x+=branchWidth*0.5f; topLeft.y=topRight.y=origin.y+branchLength; float newWidth=branchWidth*widthDecreaseFactor; topLeft.x-=newWidth*0.5f; topRight.x+=newWidth*0.5f; Vector3 axis=Vector3.back; Quaternion rotationValue = Quaternion.AngleAxis(branchAngle, axis); vertices.Add((rotationValue*(bottomLeft))+offset); vertices.Add((rotationValue*(topLeft))+offset); vertices.Add((rotationValue*(topRight))+offset); vertices.Add((rotationValue*(bottomRight))+offset); }
A branch essentially is a shape (or a Quad
) with four corner vertices: bottomLeft
, topLeft
, topRight
, and bottomRight
. The CreateBranch
method does the proper positioning of the branch by translating, rotating, and scaling these four vertices based on the shape, position, and rotation of the branch. The tip of the branch is tapered using the widthDecreaseFactor
value. The main tree method can call this method while passing in the position and rotation values for that branch.
Fractal Tree
The FractalTreeProper
class has a recursive CreateBranch
method, which in turn will create the Branch
class's CreateBranch
constructor method.
private void CreateBranch(int currentLayer,Vector3 branchOffset, float angle, int baseVertexPointer) { if(currentLayer>=numLayers)return; float length=trunkLength; float width=trunkBaseWidth; for(int i=0;i<currentLayer;i++){ length*=lengthDecreaseFactor; width*=widthDecreaseFactor; } Branch branch=new Branch(Vector3.zero,length,width,angle, branchOffset,widthDecreaseFactor); branches.Add(branch); Vector3 tipMidPoint=new Vector3(); tipMidPoint.x=(branch.vertices[1].x+branch.vertices[2].x)*0.5f; tipMidPoint.y=(branch.vertices[1].y+branch.vertices[2].y)*0.5f; //... CreateBranch(currentLayer+1,tipMidPoint,angle+branchAngle,baseVertexPointer); CreateBranch(currentLayer+1,tipMidPoint,angle-branchAngle,baseVertexPointer); }
Each call to CreateBranch
initiates two new calls to itself for its two child branches. For our example, we are using a branching angle of 30 degrees and a value of 8 as the number of branching iterations.
We use the points from these branches to create the necessary vertices, which are then used to create faces for our tree mesh.
faces=new List<int>(); vertices=new List<Vector3>(); fTree=GetComponent<MeshFilter>().mesh; fTree.name="fractal tree"; //...(in CreateBranch) if(currentLayer==0){ vertices.AddRange(branch.vertices); faces.Add(baseVertexPointer); faces.Add(baseVertexPointer+1); faces.Add(baseVertexPointer+3); faces.Add(baseVertexPointer+3); faces.Add(baseVertexPointer+1); faces.Add(baseVertexPointer+2); }else{ int vertexPointer=vertices.Count; vertices.Add(branch.vertices[1]); vertices.Add(branch.vertices[2]); int indexDelta=3; if(currentLayer!=1){ indexDelta=2; } faces.Add(baseVertexPointer-indexDelta); faces.Add(vertexPointer); faces.Add(baseVertexPointer-(indexDelta-1)); faces.Add(baseVertexPointer-(indexDelta-1)); faces.Add(vertexPointer); faces.Add(vertexPointer+1); } baseVertexPointer=vertices.Count; //... fTree.vertices=vertices.ToArray(); fTree.triangles = faces.ToArray(); fTree.RecalculateNormals();
The baseVertexPointer
value is used to
reuse existing vertices so that we avoid creating duplicate vertices, as
each branch can have four vertices but only two of those are new ones.
By adding some randomness to the branching angle, we can create asymmetrical variants of our fractal tree as well, which can look more realistic.
3. Creating an L-System Tree
The second method, the L-system, is an entirely different beast. It is a very complicated system which can be used to create intricately complex organic shapes or to create complex rule sets or string sequences. It stands for Lindenmayer System, the details of which can be found on Wikipedia.
The applications of L-systems include robotics and AI, and we will only be touching the tip of the iceberg while using it for our purposes. With an L-system, it is possible to create very realistic looking tree or shrub shapes manually with precise control or using automation.
The Branch
component remains the same as in the fractal example, but the way in which we create branches will change.
Dissecting the L-System
L-systems are used to create complicated fractals where the patterns are not easily evident. It becomes humanly impossible to find these repeating patterns visually, but L-systems make it easier to create them programmatically. L-systems consist of a set of alphabets which combine to form a string, along with a set of rules that mutate these strings in a single iteration. Applying these rules over multiple iterations creates a long, complicated string which can act as the basis for creating our tree.
The Alphabets
For our example, we will use this alphabet to create our tree string: F
, +
, -
, [
, and ]
.
The Rules
For our example, we will only need one rule where the alphabet F
changes into a sequence of alphabets, say F+[+FF-F-FF]-[-FF+F+F]
. On every iteration, we will make this swap while keeping all the other alphabets unchanged.
The Axiom
The axiom, or the starting string, will be F
. This essentially means that after the first iteration, the string will become F+[+FF-F-FF]-[-FF+F+F]
.
We will iterate three times to create a usable tree string as shown below.
lString="F"; rules=new Dictionary<string, string>(); rules["F"]="F+[+FF-F-FF]-[-FF+F+F]"; for(int i=0;i<iterations;i++){ lString=IterateLString(lString); } //.. string IterateLString(string currentLString){ string newLString=""; char[] chars=currentLString.ToCharArray(); for(int i=0;i<chars.Length;i++){ if(rules.ContainsKey(chars[i].ToString())){ newLString+=rules[chars[i].ToString()]; }else{ newLString+=chars[i].ToString(); } } return newLString; }
Parsing the Tree String
Now that we have the tree string using the L-system, we need to parse it to create our tree. We will look at each character in the tree string and do specific actions based on them as listed below.
- On finding
F
, we will create a branch with current parameters of length and rotation. - On finding
+
, we will add to the current rotation value. - On finding
-
, we will subtract from the current rotation value. - On finding
[
, we will store the current position, length, and rotation value. - On finding
]
, we will restore the above values from the stored state.
We use an angle value of 25
degrees for branch rotation for our example. The CreateTree
method in the LSystemTree
class does the parsing. For storing and restoring states, we will be using a LevelState
class which stores the necessary values along with a BranchState
struct.
levelStates=new List<LevelState>(); char[] chars=lString.ToCharArray(); float currentRotation=0; float currentLength=startLength; float currentWidth=startWidth; Vector3 currentPosition=treeOrigin; int levelIndex=0; LevelState levelState=new LevelState(); levelState.position=currentPosition; levelState.levelIndex=levelIndex; levelState.width=currentWidth; levelState.length=currentLength; levelState.rotation=currentRotation; levelState.logicBranches=new List<LevelState.BranchState>(); levelStates.Add(levelState); Vector3 tipPosition=new Vector3(); Queue<LevelState> savedStates=new Queue<LevelState>(); for(int i=0;i<chars.Length;i++){ switch(chars[i]){ case 'F': if(levelState.levelIndex!=levelIndex){ foreach(LevelState ls in levelStates){ if(ls.levelIndex==levelIndex){ levelState=ls; break; } } } tipPosition.x=levelState.position.x+(currentLength*Mathf.Sin(Mathf.Deg2Rad*currentRotation)); tipPosition.y=levelState.position.y+(currentLength*Mathf.Cos(Mathf.Deg2Rad*currentRotation)); levelIndex++; LevelState.BranchState branchState=new LevelState.BranchState(); branchState.rotation=currentRotation; branchState.length=currentLength; levelState.logicBranches.Add(branchState); currentWidth*=widthDecreaseFactor; currentPosition=tipPosition; levelState=new LevelState(); levelState.position=currentPosition; levelState.levelIndex=levelIndex; levelState.width=currentWidth; levelState.length=currentLength; levelState.rotation=currentRotation; levelState.logicBranches=new List<LevelState.BranchState>(); levelStates.Add(levelState); currentLength*=lengthDecreaseFactor; break; case '+': currentRotation+=angle; break; case '-': currentRotation-=angle; break; case '[': savedStates.Enqueue(levelState); break; case ']': levelState=savedStates.Dequeue(); currentPosition=levelState.position; currentRotation=levelState.rotation; currentLength=levelState.length; currentWidth=levelState.width; levelIndex=levelState.levelIndex; break; } }
The variable levelStates
stores a list of LevelState
instances, where a level can be considered as a branching point. As each
such branching point can have multiple branches or just one
branch, we store those branches in a list logicBranches
holding multiple BranchState
instances. The savedStates
Queue
tracks the storing and restoring of different LevelState
s. Once we have the logical structure for our tree in place, we can use the levelStates
list to create visual branches and create the tree mesh.
for(int i=0;i<levelStates.Count;i++){ levelState=levelStates[i]; int baseVertexPointer=vertices.Count; for(int j=0;j<levelState.logicBranches.Count;j++){ LevelState.BranchState bs=levelState.logicBranches[j]; Branch branch=new Branch(Vector3.zero,bs.length,levelState.width,bs.rotation, levelState.position,widthDecreaseFactor); branches.Add(branch); if(i==0&&j==0){ vertices.AddRange(branch.vertices); faces.Add(baseVertexPointer); faces.Add(baseVertexPointer+1); faces.Add(baseVertexPointer+3); faces.Add(baseVertexPointer+3); faces.Add(baseVertexPointer+1); faces.Add(baseVertexPointer+2); baseVertexPointer=vertices.Count; }else{ int vertexPointer=vertices.Count; vertices.Add(branch.vertices[1]); vertices.Add(branch.vertices[2]); Vector2Int vertexPointerXX=GetClosestVextexIndices(branch.vertices[0],branch.vertices[3],vertices,i); faces.Add(vertexPointerXX.x); faces.Add(vertexPointer); faces.Add(vertexPointerXX.y); faces.Add(vertexPointerXX.y); faces.Add(vertexPointer); faces.Add(vertexPointer+1); } } } lTree.vertices=vertices.ToArray(); lTree.triangles = faces.ToArray(); lTree.RecalculateNormals();
The GetClosestVextexIndices
method is used to find common vertices to avoid duplicates while creating the mesh.
By varying the rules slightly, we can get drastically different tree structures, as shown in the image below.
It is possible to manually create the tree string for designing a specific kind of tree, although this can be a very tedious task.
Conclusion
Playing with L-systems can be fun, and it could result in very
unpredictable results as well. Try changing the rules or adding more
rules to create other complicated shapes. The next logical thing to do would
be to try and extend this to 3D space by replacing these 2D Quads with
3D cylinders for branches and adding the Z dimension for branching.
If you are serious about creating 2D trees, I would suggest that you look into space colonization algorithms as the next step.