Building Tree’s Part 1: Runtime Meshes
This is part one of a series of tutorials on procedural Tree’s, using L-Systems. Based off how I did the tree generation in Tree Game. There will be three parts to this series of tutorials, this being the first going over the creation of the actual 3D models for the tree. The second tutorial will go over L-Systems in detail, what they are, and implementing them in code. The final tutorial will bring these two parts together, and put the finishing touches on our tree’s.
I will say now, at the outset I am not professional programmer, and am for the most part self taught. There are probably better ways to achieve some of if not all of these things, these are just the ones I know/used. Along with that, the code snippets in here are displaying all weird and bad, but hopefully copying and pasting them into your own code program should fix them; so I would suggest doing that if you are confused at any point. Feel free to take from here as you need. But lets get started otherwise.
For the most part these tutorials will presume an intermediate knowledge of unity and c#. But moving on, we start with meshes. For those who have not played with runtime meshes before I will offer -brief- introduction here, for those who have feel free to skip this part. The main meat starts with the first piece of code.
Meshes are the way unity stores and renders 3D objects, meshes are made up of at the between two things at their core. Vertices and Triangles. Each of these is some kind of list, that is used by the computer to create almost all the objects that will be rendered on screen. So let's get into what they each are.
We will start with the most important one, the vertices. These are a list of Vector3’s, that represent each of the points in 3D space that make up the model. These points basically represent all corners of a given mesh. Linked to this is triangle list, which is deeply entwined with our vertices list.
First what are the Triangles? Unity can't just use vectors to represent 3D objects, it needs to know instead what points make up planes that it will render. The way it does this is by linking vertices in groups of three in order to make triangles. The way this is done, through reference to the vertices list. To make this a bit clearer, an example;
Importantly, order matters when we are creating triangles. As order determines which direction a given triangle will face, and given that unity culls back faces (ie, does not draw the backside of a triangle, terminology is weird) we need to make sure that they are facing the right way, ie, ordered correctly inside the list of triangles. In the above example, the third diagram shows us the vertice references we will use, with a direction indicating the order. In the forth diagram there is an example of how this would look in the triangle array.
So now that we have a brief understanding of how Meshes work inside of unity we can get started making some. So now we have our first piece of code.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TreeTutorial : MonoBehaviour { Transform thisTransform; MeshFilter thisMeshFilter; Mesh thisMesh; // Use this for initialization void Start () { thisMeshFilter = this.GetComponent<meshfilter>(); thisMesh = thisMeshFilter.mesh; thisTransform = this.transform; thisMesh.Clear(); thisMesh.MarkDynamic(); } }
This is just setting things up for the moment, we are getting the component that draws our mesh, and the mesh we want to play with. The first thing we then do is clear it, ready for us to start messing with, as well as mark it as something we are going to do a lot of editing of at run time. This last bit will help unity know that it will be using this model a lot, and to not mark it as static (unchanging). Now we can start making things appear.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TreeTutorial : MonoBehaviour { Transform thisTransform; MeshFilter thisMeshFilter; Mesh thisMesh; List<vector3> verticies = new List<vector3>(); List<int> triangles = new List<int>(); [SerializeField] float width; // Use this for initialization void Start () { thisMeshFilter = this.GetComponent<meshfilter>(); thisMesh = thisMeshFilter.mesh; thisTransform = this.transform; thisMesh.Clear(); thisMesh.MarkDynamic(); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y, thisTransform.position.z - width)); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y, thisTransform.position.z - width)); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y + 1, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y + 1, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y + 1, thisTransform.position.z - width)); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y + 1, thisTransform.position.z - width)); } }
First thing first, all we are doing at the moment is setting up the vertices we will use as the basis of a simple cube. Note we are using a list for our vertices, this is a little planning for the future, as when we are making our tree we wont know how many vertices it will before we start our code. Moving on though, we will now use this list of vertices to create triangles that unity will render. To do this we tell the computer what vertices in our vertices list, and in what order to make into triangles. The triangle list is just along list of integers, but is used by the computer in groups of three, so for easy of understanding this, I have defined it in lumps of three.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TreeTutorial : MonoBehaviour { Transform thisTransform; MeshFilter thisMeshFilter; Mesh thisMesh; List<vector3> verticies = new List<vector3>(); List<int> triangles = new List<int>(); [SerializeField] float width; // Use this for initialization void Start () { thisMeshFilter = this.GetComponent<meshfilter>(); thisMesh = thisMeshFilter.mesh; thisTransform = this.transform; thisMesh.Clear(); thisMesh.MarkDynamic(); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y, thisTransform.position.z - width)); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y, thisTransform.position.z - width)); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y + 1, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y + 1, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y + 1, thisTransform.position.z - width)); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y + 1, thisTransform.position.z - width)); triangles.AddRange(new int[] {0,4,1, 4,5,1, 1,5,2, 5,6,2, 2,6,3, 6,7,3, 3,7,0, 7,4,0} ); thisMesh.vertices = verticies.ToArray(); thisMesh.triangles = triangles.ToArray(); thisMesh.RecalculateNormals(); thisMesh.RecalculateTangents(); thisMesh.RecalculateBounds(); } }
So now we have a way to draw a box. Let's break down what each part of this code is doing, and then see if we can abstract some parts of this, to build a slightly more robust system.
We start by defining the bottom of the box, this is done but hard coding in these points, as they will be the starting point for all our tree’s. We set these to be a certain distance from the origin, and add them to our vertices list. Now seeing as we know a set of vertices list references we can store, and use them later.
Following this we can begin defining more points; the first thing we need to do, is set a position to be our new reference, this will some movement from our origin. We then define four more points based on this new position, and add them to our vertices list.
We now have eight points that we can use to define our cube, the first four at the bottom, and the four above them. We now need to define the triangle that will make them visible.
Using this we can build a set of triangles; using our current vertices (the last verts added to our list) and the array of vertices references we last used. To build the triangle list, we add the references in groups of three (the triangles we want made visible). The order we as them is as follows:
We then add this array of integer references to the large triangle list.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TreeTutorial : MonoBehaviour { Transform thisTransform; MeshFilter thisMeshFilter; Mesh thisMesh; List<vector3> verticies = new List<vector3>(); List<int> triangles = new List<int>(); [SerializeField] float width; int[] lastRefrences; // Use this for initialization void Start () { thisMeshFilter = this.GetComponent<meshfilter>(); thisMesh = thisMeshFilter.mesh; thisTransform = this.transform; thisMesh.Clear(); thisMesh.MarkDynamic(); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y, thisTransform.position.z - width)); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y, thisTransform.position.z - width)); lastRefrences = new int[] { 0, 1, 2, 3 }; verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y + 1, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y + 1, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y + 1, thisTransform.position.z - width)); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y + 1, thisTransform.position.z - width)); triangles.AddRange(new int[] {lastRefrences[0],verticies.Count - 4,lastRefrences[1], verticies.Count - 4,verticies.Count - 3,lastRefrences[1], lastRefrences[1],verticies.Count - 3,lastRefrences[2], verticies.Count - 3,verticies.Count - 2,lastRefrences[2], lastRefrences[2],6,lastRefrences[3], verticies.Count - 2,verticies.Count - 1,lastRefrences[3], lastRefrences[3],verticies.Count - 1,lastRefrences[0], verticies.Count - 1,verticies.Count - 4,lastRefrences[0]} ); thisMesh.vertices = verticies.ToArray(); thisMesh.triangles = triangles.ToArray(); thisMesh.RecalculateNormals(); thisMesh.RecalculateTangents(); thisMesh.RecalculateBounds(); } }
We now have all the pieces to make our cube; and from here a relatively easy way to keep extending out cube, by adding our last vert list positions to our old references we can iteratively make additions to our cube as follows.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TreeTutorial : MonoBehaviour { Transform thisTransform; MeshFilter thisMeshFilter; Mesh thisMesh; List<vector3> verticies = new List<vector3>(); List<int> triangles = new List<int>(); [SerializeField] float width, height; [SerializeField] int numDivisions; int[] lastRefrences; // Use this for initialization void Start () { thisMeshFilter = this.GetComponent<meshfilter>(); thisMesh = thisMeshFilter.mesh; thisTransform = this.transform; thisMesh.Clear(); thisMesh.MarkDynamic(); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y, thisTransform.position.z - width)); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y, thisTransform.position.z - width)); lastRefrences = new int[] { 0, 1, 2, 3 }; for (int i = 0; i < numDivisions; i++) { AddToTree(i * height); } thisMesh.vertices = verticies.ToArray(); thisMesh.triangles = triangles.ToArray(); thisMesh.RecalculateNormals(); thisMesh.RecalculateTangents(); thisMesh.RecalculateBounds(); } void AddToTree(float stepLength) { verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y + stepLength, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y + stepLength, thisTransform.position.z + width)); verticies.Add(new Vector3(thisTransform.position.x - width, thisTransform.position.y + stepLength, thisTransform.position.z - width)); verticies.Add(new Vector3(thisTransform.position.x + width, thisTransform.position.y + stepLength, thisTransform.position.z - width)); triangles.AddRange(new int[] {lastRefrences[0],verticies.Count - 4,lastRefrences[1], verticies.Count - 4,verticies.Count - 3,lastRefrences[1], lastRefrences[1],verticies.Count - 3,lastRefrences[2], verticies.Count - 3,verticies.Count - 2,lastRefrences[2], lastRefrences[2],verticies.Count - 2,lastRefrences[3], verticies.Count - 2,verticies.Count - 1,lastRefrences[3], lastRefrences[3],verticies.Count - 1,lastRefrences[0], verticies.Count - 1,verticies.Count - 4,lastRefrences[0]} ); lastRefrences = new int[] { verticies.Count - 4, verticies.Count - 3, verticies.Count - 2, verticies.Count -1}; } }
What we have done here is moved the last parts of our vertices, and triangle creation into a new functions. Importantly this functions creates the new vertices, assigns the triangles in the same way we have before, and then sets our newly made vertices as the new last positions, so we can run this same functions multiple times and build up our tree.
From here we have one final part to add to our code, our tree does not only grow directly up, and instead is able to twist and turn. To enable this, we need to feed in something more then just a distance we want our vertices positions to move up. Instead what we we will do is give it a reference point and then create a series of tangents based on this. These tangents are used to make sure we are placing the vertices with the correct rotation, and distance away from our reference point.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TreeTutorial : MonoBehaviour { Transform thisTransform; MeshFilter thisMeshFilter; Mesh thisMesh; List<vector3> verticies = new List<vector3>(); List<int> triangles = new List<int>(); [SerializeField] float width, height; [SerializeField] int numDivisions; int[] lastRefrences; Vector3 currentPosition = Vector3.zero, currentHeading = Vector3.up; // Use this for initialization void Start () { thisMeshFilter = this.GetComponent<meshfilter>(); thisMesh = thisMeshFilter.mesh; thisTransform = this.transform; thisMesh.Clear(); thisMesh.MarkDynamic(); Vector3 tangentX, tangentZ; currentPosition = Vector3.zero; tangentX = Vector3.Cross(currentPosition.normalized, Vector3.forward); tangentZ = Vector3.Cross(currentPosition.normalized, Vector3.right); verticies.Add(currentPosition + (tangentX * width) + (tangentZ * width)); verticies.Add(currentPosition - (tangentX * width) + (tangentZ * width)); verticies.Add(currentPosition - (tangentX * width) - (tangentZ * width)); verticies.Add(currentPosition + (tangentX * width) - (tangentZ * width)); lastRefrences = new int[] { 0, 1, 2, 3 }; for (int i = 0; i < numDivisions; i++) { currentPosition += Quaternion.Euler(15 * i, 0, 0) * Vector3.up * height ; AddToTree(currentPosition); } thisMesh.vertices = verticies.ToArray(); thisMesh.triangles = triangles.ToArray(); thisMesh.RecalculateNormals(); thisMesh.RecalculateTangents(); thisMesh.RecalculateBounds(); } void AddToTree(Vector3 point) { Vector3 tangentX, tangentZ; tangentX = Vector3.Cross(point.normalized, Vector3.forward); tangentZ = Vector3.Cross(point.normalized, Vector3.right); verticies.Add(point + (tangentX * width) + (tangentZ * width)); verticies.Add(point - (tangentX * width) + (tangentZ * width)); verticies.Add(point - (tangentX * width) - (tangentZ * width)); verticies.Add(point + (tangentX * width) - (tangentZ * width)); triangles.AddRange(new int[] {lastRefrences[0],verticies.Count - 4,lastRefrences[1], verticies.Count - 4,verticies.Count - 3,lastRefrences[1], lastRefrences[1],verticies.Count - 3,lastRefrences[2], verticies.Count - 3,verticies.Count - 2,lastRefrences[2], lastRefrences[2],verticies.Count - 2,lastRefrences[3], verticies.Count - 2,verticies.Count - 1,lastRefrences[3], lastRefrences[3],verticies.Count - 1,lastRefrences[0], verticies.Count - 1,verticies.Count - 4,lastRefrences[0]} ); lastRefrences = new int[] { verticies.Count - 4, verticies.Count - 3, verticies.Count - 2, verticies.Count -1}; } }
With that we now have in place most of the mesh generation pieces we will need to build our tree's when the time comes, and as such this calls to an end this part of the tutorial. Part 2 on L-systems can be found here: https://mjm.itch.io/tree-game/devlog/60591/building-trees-part-2-l-systems. If you have any questions feel free to drop me a line here, in the comments or on twitter; @ApparentRaisin
So till next time
-Max
Get Tree Game
Tree Game
customise tree's, make terrariums, just chill
Status | In development |
Author | Max |
Genre | Simulation |
Tags | artgame, Casual, comfort, Experimental, Procedural Generation, Relaxing, terrariums, trees, Unity |
Languages | English |
More posts
- Issue FixedDec 25, 2018
- Infinite Trees ModeDec 24, 2018
- Building Tree’s Part 2: L-SystemsDec 18, 2018
- Quick Update the SecondDec 03, 2018
- Quick UpdateNov 29, 2018
Leave a comment
Log in with itch.io to leave a comment.