Expose parameters to make your procedural systems easier to control, explore, and develop.
p5.js + tweakpane
One of the most rewarding aspects of creating a procedural generation system is exploring what it can make. The initial investment of time spent coding is repaid by the ability to iterate easily and quickly. Many procedural systems can produce endless variations and can be pushed to surprising extremes. Exploring the range of the system reveals new ideas to consider and aesthetics to explore.
Procedural generators can provide enormous creative leverage. They allow expressive artistic control while automating much of the work. This control is afforded by exposing parameters. Parameters are adjustable values that influence the internal behavior of a system.
A parameter space is the set of all possible combinations of values for the parameters of a system. The parameter space of a system can grow very quickly. A system with one boolean parameter can be configured in just two ways. If the system has 8 boolean parameters, it can be configured in 256 ways. A system with 16 boolean parameters will have 65,536 combinations. This rapid growth is referred to as combinatorial explosion.
When changes to input parameters map to interesting changes in output, combinatorial explosion can make a procedural system very powerful. Consider a program that generates faces by choosing from 4 options for each of these traits: hair style, hair color, eye color, eye shape, nose shape, mouth shape, ear shape, and face shape. Such a system can generate
65,536 distinct faces. If the system supported two more similar traits that number of outputs would grow to
Leveraging combinatorial explosion in your procedural system does not guarantee variety.
Sometimes different parameters will lead to the same final output. Sometimes the output will differ only slightly or in ways that are not meaningul or important. When this happens a system’s output can feel monotonous, uninteresting, and “samey”. A system that takes just two numeric parameters (as 32-bit floats) has a little less than
18,446,744,073,709,551,616 (18.4 Quintillion!) states. This is an inconceivably large number, but it is quite likely that many of those states would look very similar. The p5.js
ellipse() function takes four parameters: x, y, width, and height. These are each 64-bit floats, so there are 2^256 possible combinations. Thats 115 quattuorvigintillion different circles! In practice though, a 50 pixel wide circle and a 50.001 pixel wide circle look the same and most of those circles won’t even fall on your canvas at all.
When creating interfaces for procedural systems, focus on exposing parameters that allow for interesting variation.
Imagine a program that draws squares like the one below. What parameters might such a program accept?
Parametric Design is a design approach where designs are defined as systems that produce output that can be customized by adjusting parameters. For example, a parametric design for a bicycle might accept a parameter for the rider’s height and provide a customized frame to suit.
A critical aspect of parametric designs is that they represent the design intent, rather than just the design product. This allows parametric designs to adjust to fit provided parameters and create new design products as needed.
We often think of parameters as inputs, but the parameters exposed in parametric design can also be thought of as desired traits of the output.
Imagine a machine that makes a snowman. The machine might take parameters for how large the bottom, middle, and top snowball should be. This interface would afford a good deal of flexibility, but might also lead to poorly proportioned or even infeasible snowmen. Instead the machine could be designed to take a single parameter representing the desired height of the snowman, and internally calculate the sizes of each snowball according to relationships determined by the designer rather than the user.
You explore the range of a procedural genertion system by changing the values of its parameters and seeing what happens. In simple sketches you might tweak parameters directly, by changing hard-coded values directly in the source.
It is almost always worth taking time to identify useful parameters in your code, consider their possible values, and expose them in a way that encourages exploration. Doing so will lead to better user experiences, better code, and better results.
Exploring the parameter space of a system by tweaking hard-coded values doesn’t work very well. Tweaking hard-coded values is slow—the project has to be re-built and re-run after each change—which discourages exploration. Also, a slow feedback cycle makes it harder to understand the effects of each change. Tweaking hard-coded values in the code is also difficult and error-prone. Which values should you change for particular effects? Do you need to change the value in multiple places? Will changing a particular value just break things?
Exposing key values in your program as parameters makes them easier to document, understand, and use. These benefits of a good interface usually far outweigh the time required to implement it.
Procedural generation code often grows organically and iteratively: tweak some code, run it to see what it builds, get inspired, and then tweak again. Iterative growth leads to code that is increasingly disorganized, hard to read, and hard to change. Exposing parameters helps to organize the code by separating configuration and implementation.
Exposing parameters doesn’t necessiarly require creatring a GUI for them. Early versions of a program often contain a lot of magic numbers. Simply changing these magic numbers into named constants helps make the code easier to read and easier reason about. Organizing your code into well-factored functions with carefully chosen parameters helps even more. Thinking of your program as a collection of components—each with their own parameter-driven interface—will generally lead to better organized, more maintainable code.
On small projects—projects that you don’t plan on sharing—it is tempting to skip the time needed to clean up code, factor out parameters, and create a better UI. This is often a false economy. These efforts are usually quickly repaid.
When you can explore the parameter space of your procedural systems more quickly, you can explore it more thoroughly. You will find more interesting possibilities, ideas, and aesthetics to explore further.
When thinking about software we often define the interface as the part of the application that is visible to and manipulated by the user. I think it is better consider an interface as a common boundary, or overlap, between two systems. Interfaces are shaped by the qualities of both systems.
Two of the most important types of interfaces of software systems are user interfaces (UIs) and application programming interfaces (APIs).
The UI is the part of a software system that a person uses to control it. The UI accepts user input and provides feedback. It overlaps with the user and is designed around the capabilities and nature of both the software and the user. The UI is the primary interface in most applications.
The API is the part of a software system that is used by programmers to connect it with other software systems. A well-designed API considers both the software system itself and how other software systems will want to use it. The API is the primary interface in most libraries.
It is common for a piece of software to have both a UI and an API. For example, twitter provides a user interface for making and reading tweets and an API for integrating twitter into existing systems.
Exposing parameters allows artists and designers to create systems that can be controlled by others—and themselves—more easily. Choosing which parameters to expose is a core concern of software interface design. When choosing, consider the following:
I sometimes find it helpful to consider wether my parameters should be method-oriented or goal-oriented. By method-oriented, I mean parameters that control what the procedure does. By goal-oriented, I mean parameters what control what the procedure achieves. This isn’t a practical difference and programming languages don’t make a distinction between these things. This is just a way of thinking about parameters and their purpose that can be helpful when designing interfaces. If you take an internal, hard-coded value of a function and turn it into a parameter, it will probably be a method-oriented parameter almost by defintion. It often takes a little more effort, and some additions to the code, to introduce a goal-oriented parameter because the code needs to figure out how to reach the goal. Constraint solvers are algorithms that try to find solutions satisfying several goal-oriented parameter at once.
|Exposing more…||Exposing less…|
|gives your user more control.||gives you more control.|
|makes your interface harder to understand.||makes your interface easier to understand.|
|allows your users to do more good things.||prevents your users from doing bad things.|
Once you have decided what to expose via your interface, you must consider how to communicate your interface options to your users:
Feedback shows users the impact their actions have on the system. Without feedback, systems are very hard to learn and use. With clear and responsive feedback, even systems that are difficult to describe can often be intuitively understood.
In simple cases, showing users the end result of their choices after each change may be enough. In more complex situations, it is often helpful to provide intermediate feedback. Many procedural systems are too complex to provide realtime feedback. For systems that take a long time to calculate, providing immediate confirmation of user input is important. Sometimes it can be very helpful to provide a low-quality, but quick, preview as well.
The way that you think about your software system is often very different from the way your users think about it.
When designing, step back and consider the relationship between your project and your user.
Practice designing user interfaces without real-world constraints.
Begin by thinking about your machine and your user. What does your machine do? What does your user need?
Think about how your user will control your machine. What options would the user want control of? What would the user want automated?
A quick-and-dirty way to make your comp form sketches “tweakable” is to use global constants for your parameters and group them at the top of the script. This is very easy to set up, and works particularly well for small one-off sketches. However, this approach slows down exploration because you still need to re-run your sketch after each change.
Take a moment to explore the parameter space of the sketch above by tweaking the globals. What happens if you set them both to 100?
These next two examples both have a function named
stipleRect() that fills a rectangular region with randomly placed dots. The first four parameters of each implementation are the same:
height. The fifth parameter is different. In the method-oriented example, the fifth parameter controls how many dots to draw directly. In the goal-oriented example, the fifth parameter controls how densley the dots should be placed, and the function internally calculates how many dots it needs to draw to achieve that goal.
The p5 DOM functions provide functions that allow you create HTML elements and use them as interface controls. This is a little more complicated to set up but still pretty quick. GUI interfaces are usually better than global variables if you want anyone else to adjust your parameters. You should consider this approach even for projects only you will use; it allows you to explore your parameter space without having to reload and restart your sketch.
Explore the study examples above by completing the following challenges.
Continue experimenting with procedurally-generated images. Focus on exposing parameters and exploring the parametric potential of your sketches. You can mix random and parametric elements, but consider doing a couple of sketches that are not random at all.
Build a face-generating tool. This tool will create an image of a face that can be adjusted by the user with sliders and other inputs.
ellipse(). Build your face from manually-created illustrations or photographic images.