Generative Art Basics - Random 3D Creature Generator w/Unity

I was invited to give a short workshop for Stereloux's OpenTalk! series on creative programming. The following is a transcription of the workshop in english, in which we explore generative art through the lense of a random creature generator built in Unity.

The goal of this workshop is to show how a very simple system with a few basic rules can output a variety of interesting results. The information presented here is by no means exhaustive, but I've included links to sources and resources for you to further explore the ideas presented. The scripts included in the Unity project are also full of comments, so keep your eyes peeled for hints and explanations in the code.

Find the Unity project here. Navigate over to the Scenes folder to find a scene for each step outlined here. All the code we'll be using can be found on the Generator component. TLDR; the finished code is on a script by the same name - otherwise for each scene (step1, step2...) there is a script by the same name attached to the Generator component in each of the scenes.

The Basic Template

To start things off, I've created a simple base template that features a bit of starter code. This includes two variables, auto and autoGenerateTime which are used inside the Start() function's If statement to regenerate the creature we'll be making at a timed interval.

Every few seconds, InvokeReapeating, a function that's provided to us by Unity, will call GenerateShapes(), a function that I've created for the purpose of this workshop, inside which we'll be placing all our code for the random generator.

Note the materials variable, which loads some colorful materials located in /Resources/Materials. These materials feature a variety of colorful textures we'll be making available to our creatures.

The template also features a skeleton structure for our creature: three variables, parent, left & right. Parent will be a general container for the creature, while left & right represent each of the creatures sides. The idea is that each side is a reflection of the other, and will contain the objects generated by The For loop.

If you hit play in the Unity editor, you'll see the skeleton structure in the Hierarchy:

image

public class Generator : MonoBehaviour
{
    // Regenerate creature every 30 seconds
    public bool auto = true;
    public float autoGenerateTime = 30;

    private GameObject parent;
    private GameObject left;
    private GameObject right;

    public List<GameObject> shapes;
    private Object[] materials;

    // Start is called before the first frame update
    void Start()
    {
        // regenerate creature
        if (auto)
        {
            InvokeRepeating("GenerateShapes", 0.0f, autoGenerateTime);
        }

        // load materials from Resources folder
        materials = Resources.LoadAll("Materials", typeof(Material));
    }

    void GenerateShapes()
    {
        // clear for new creature
        Destroy(parent);

        // choose random material
        // Material material = (Material)materials[Random.Range(0, materials.Length)];

        // Skeleton 
        parent = new GameObject();
        left = new GameObject();
        right = new GameObject();

        parent.name = "Creature (Parent)";
        left.name = "left";
        right.name = "right";

        // set parent
        left.transform.parent = parent.transform;
        right.transform.parent = parent.transform;

        // loop to generate creature parts
        for (int i = 0; i < 10; i++){}
    }

    // Update is called once per frame
    void Update() {}
}

Step 1

Note shapes on line 16 of our code. This loads a list of shapes from the Unity editor. Our component has a dropdown Shapes option, which is a list that we can set to the length that we want, and add shapes of our choosing to.

In the Hierarchy, we can right click and create some 3D shapes, then drag them into the slots available on our component.

Going back to our code now, inside our loop, we'll want to generate some randomly selected shapes from our list of shapes.

halfimage halfimage

 // loop to generate creature parts
        for (int i = 0; i < 10; i++)
        {
            int randShape = Random.Range(0, shapes.Count);
        }

In the Unity editod, once the shapes are loading into the list, you can deactivate the shapes in the Hierarchy by clicking on the checkbox next the shape's name in the Inspector.

⚠️ Deactivating an gameobject doesn't delete it from the scene. It simply renders it invisilbe. When we run the program, it can still access all the components associated to the gameobject. We just won't see it in the scene or gameview. This is really helpful for generating gameobjects on the fly from within a script, where we can set the gameobject to be active within our scene when we are ready for it to be visible.

Step 2

Now that we have some shapes to play with, we can start instantiating them at random for each side of our creture. This means we need two gamobjects generated at mirrored positons for each iteration of the loop.

We'll be using Instantiate() which is provided to us by Unity. It requires three parameters: the gameobject we want to instantiate, and it's location (transform.position) and rotation (Quaternion.identity*) in space. While we won't concern ourself with our shapes rotation just yet, we'll be setting randomly generated positons for the x, y and z axis of each shape, which we choose at random from our shapes list.

Finally, we'll set each instantiated gameobject to Active so that we can see it in our scene.

*Quaternion.identity is given to us by Unity and indicates that we're setting 'no rotation' on the gameboject.

 for (int i = 0; i < 10; i++)
        {
            int randShape = Random.Range(0, shapes.Count);

            GameObject go = Instantiate(shapes[randShape], new Vector3(Random.Range(0.35f, 3.0f), 
            Random.Range(-1.0f, 3.0f), Random.Range(-1.0f, 3.0f)), Quaternion.identity);
            GameObject go2 = Instantiate(shapes[randShape], new Vector3(-go.transform.position.x, 
            go.transform.position.y, go.transform.position.z), Quaternion.identity);

            go.transform.parent = left.transform;
            go2.transform.parent = right.transform;

            go.SetActive(true);
            go2.SetActive(true);
        }

The trick to mirror positions between left and right requires us to pass the x, y and z position that we generated randomly for the first gameobject to the second gameoject (the opposite site) and then to simply add a minus (-) to the x position, since that's the axis we're mirroring.

If you head back into the Unity editor and hit play, you should now the begginings of a creature. Reset the Auto Generate Time to a small number like 1 or even 0.5 to see variations created based on the shapes you included in your list.

image

Step 3

Let's add some color! As mentioned, there are some gradient materials available in the Resources folder which we load into our Generator component in Start() (line 29)

Inside GenerateShapes(), on line 38, there's a line of code that has been commented out. We can now activate this line, which will choose a random gradient. Inside the For loop, we can now apply the material to each side of our creature.

image

void GenerateShapes()
    {
        ...
        // choose random material
        Material material = (Material)materials[Random.Range(0, materials.Length)];
        ...
        // loop to generate creature parts
        for (int i = 0; i < 10; i++)
        {
            int randShape = Random.Range(0, shapes.Count);

            GameObject go = Instantiate(shapes[randShape], new Vector3(Random.Range(0.35f, 3.0f), 
            Random.Range(-1.0f, 3.0f), Random.Range(-1.0f, 3.0f)), Quaternion.identity);
            GameObject go2 = Instantiate(shapes[randShape], new Vector3(-go.transform.position.x, 
            go.transform.position.y, go.transform.position.z), Quaternion.identity);

            // add color
            go.GetComponent<Renderer>().material = material;
            go2.GetComponent<Renderer>().material = material;

            go.transform.parent = left.transform;
            go2.transform.parent = right.transform;

            go.SetActive(true);
            go2.SetActive(true);   
        }
    }

image

Step 4

At this point we have a pretty good base. We've used randomness to generate variety in both the color and the various elements that comprise our creature, as well as their positions in space.

Let's go one step further now and add some variety to the amount of elements each side of the creature contains. As we've done many times before at this point, we're going to create a variable that contains a number chosen at random, which we'll then add to our For loop, replacing the loop continuation condition.

int elements = Random.Range(10, 50);
// loop to generate creature parts
for (int i = 0; i < elements; i++)
{
    ...   
}

Step 5

We'll now use the same approach to change the scale and the rotation of the individual elements that make up each side of our creature.

Inside the For loop, we'll create the variables scale and randRot, giving each a random range from which to generate numbers that we'll pass along to the first gameobject.

⚠️ Remember that since we're mirroring, we are going to pass the x, y and z of one side to the other. In the case of scale, this means grabbing the localScale parameters. And let's not forget to add the minus (-) to the x axis. You might wonder why we're doing this for scale. In fact it's a bit of a hack to ensure that the gradient pattern is mirrored properly on both sides of our create. to and rotation transofrms of the first side to the second, which we are assigning the

As for rotations, since Quaternions are in 4 dimensions, we'll simplify things a little and use Unity's Quaternion.Euler() function, which allows us to set a rotation on the x, y and z axis. In our case, we want to and variation on the z axis only. Unity also kindly gives us the very handy Quaternion.Inverse() function to flip our rotation on the opposite side. It takes only one parameter, the rotation transform to be inversed.

   void GenerateShapes()
    {
        ...
        int elements = Random.Range(10, 50);

        // loop to generate creature parts
        for (int i = 0; i < elements; i++)
        {
            int randShape = Random.Range(0, shapes.Count);
            float scale = Random.Range(0.25f, 1.25f);
            float randRot = Random.Range(-360.00f, 360.00f);
            ...
            // 1 - scale
            go.transform.localScale = new Vector3(scale, scale, scale);
            go2.transform.localScale = new Vector3(-go.transform.localScale.x, 
            go.transform.localScale.y, go.transform.localScale.z);

            // 2 - rotation
            go.transform.rotation = Quaternion.Euler(0, 0, randRot);
            go2.transform.rotation = Quaternion.Inverse(go.transform.rotation);

            // add color
            go.GetComponent<Renderer>().material = material;
            go2.GetComponent<Renderer>().material = material;
         
            go.transform.parent = left.transform;
            go2.transform.parent = right.transform;

            go.SetActive(true);
            go2.SetActive(true);
           
        }

    }

image

Step 6

To round things off, let's give our creature a more life-like feel by animating each of it's sides, as if they were wings. For this, we'll move out of the For loop, and in fact, we'll be moving out of the GenerateShape() function altogehter, and into Unity's Update() function is called every frame. Since we declared the variables that contain our creature's elements at the top of our class, we can access them from anywhere in our code.

We'll be animating the rotation on the y axis of each side, left and right. To do this, we'll need a bit of math. Unity has a really handy set of math functions, including Mathf.Sin() which is a periodic function to which we'll give Unity's Time.time, a counter that starts as soon as the program starts to run. This is what creates the rythm of our flapping wings.

Two new variables are needed here, speed, to control how quickly the wings flap, and maxRotation which will allow us to set the breadth of the wings angle of rotation. These two variables can be declared at the top of our class.

public class Step6 : MonoBehaviour
{
    ...
    // wing mmovement
    public float speed = 2f;
    public float maxRotation = 45f;

    // Start is called before the first frame update
    void Start()
    {
        ...
    }

    void GenerateShapes()
    {
        ...
    }

     void Update()
    {
        // wing movement
        left.transform.rotation = Quaternion.Euler(0f, maxRotation * Mathf.Sin(Time.time * 
        speed), 0f);
        right.transform.rotation = Quaternion.Euler(0f, maxRotation * -Mathf.Sin(Time.time * 
        speed), 0f);
    }

Next, inside Update() we'll grab the rotation transforms for each side, and do our math calculations on the y axis. Inside Mathf.Sin(), we multiply Time.time by speed, and we multiply its result but maxRotation.

Now when you hit Play in Unity, you'll see you're creature feels much more alive. And there you have it, a simple random generator. Some hints to push things further:

  • • We've mainly used randomness to add variety to our project, but noise offers a nice and slightly more poetic option. Look at the differenet types of noise that exist and experiment with implementing them with different aspects of your creature.
  • Keijiro has a wonderful library for this that's very simple to implement. Hint: the library is called Klak, and it's already in the base project we've been working on.
  • • Daniel Shiffman has a wonderful bunch of videos on the topic as well, that are definitely worth exploring.
  • • Consider adding audio or other types of real-time data to the scene to influence different elements, such as movement, scale or rotation.