- Core Concepts
- Sub-division of Spaces
- Creating Content
Apparance is unusual because there are a few fairly major shifts in thinking required coming from traditional engine operation and content construction. In some ways, content creation is more like coding than sculpting, you need a greater depth of thought as you develop your creations because you need to consider more than just their look. You need to capture the very essence of what is making your design happen, the processes that led to it being, the mechanisms involved, and how it will function in, and interact with, its environment. Doing so allows the computer to then introduce variety and you to explore the parameter space you expressed in your design. Each variant will follow the intent, the style, the vision of your design, yet be it's own unique realisation that fits in and interacts properly with its surroundings.
The project has been vision led, and evolved towards goals of interactivity, flexibility, and parameterised modelling. As such, a lot has been learned during its development about the nature of procedural generation, what approaches and thought processes work, and which don't.
Let's look at some of the element and themes involved.
Procedural generation is driven by the process of building sets of instructions that a computer can follow to carry out a task. Each step in a procedure is an operation or decision, applied to the information that is passed in and flows through it. The end result is some form of asset to be displayed (or otherwise used) in the game world or environment you are constructing.
In this example we are building geometry (triangle meshes) to form the objects and surfaces of the game world we see (we can place assets too, see below). The output of our procedures will be geometry, the input can be any number of parameters, of various types. In our example above the input, operations, and output are:
- Input - Bounds describing the size and height of a table
- Operations -
- Slice off top of bounds for table top
- Underneath, generate four corner bounds of the same size, inset from table edge.
- Populate them with table legs
- Combine it all together
- Paint it yellow
- Output - Geometry representing a table
Procedures are the data that persists your design, they are your content, they are what makes your creation unique. Each is a collection of operations, other procedures, built-in constant values, and interconnections between them. Together they perform their designed task to produce your game world.
At some point, a procedure will need to perform some function to manipulate the information flowing through it. This is the job of operators; fundamental elements of procedural creations which encapsulate some general purpose function. Operators are like built-in procedures, hard coded to perform a single specific function. In-fact they are effectively functions in the same sense of any programming language; pass in data, get transformed data out. Unlike many languages though, operators can have multiple outputs as well as multiple inputs.
Procedures can contain other procedures, and they appear and behave just as operators do. They take input data and produce transformed output data. This approach is fundamental to creative efficiency, allowing re-use of more general functionality within less general layers of the design. The main entry point into your project, the top-level procedure, will be the most specific, and the procedures will get more general as you dig down.
Building this way allows accumulation of useful functionality and even larger scale assets into libraries of content that can be re-used within a project or outside in later projects.
An important aspect of this re-use is that it allows project-wide changes and improvements. Fixing a problem with your brick wall fixes all the walls, immediately. Procedures are easily added as placeholders, to allow quick prototyping and then can later be re-visited to flesh-out properly.
Procedures are evaluated non-destructively, accumulating calculated and derived information as it progresses, without replacing any previous values. Any operator output value, once calculated, remains fixed, immutable.
This means that procedures are completely functional, i.e. not having side effects. Whilst there is a bit of an evaluation cost associated with this, it simplifies the thinking needed when building procedure graphs.
The inputs to these processes are the controlling parameters, each presenting an axis along which the resulting content can be varied. The parameters you choose to expose depend on how your content is to be used, they represent a design choice.
Constant values at unwired operator inputs are equally considered parameters, except these are design time parameters, constant at runtime. This is in contrast to procedure inputs which are run time parameters, variable at runtime.
The procedure hierarchy represents a large interconnected graph of operators, all feeding information ultimately towards the final outputs. Evaluation of this is performed by the synthesisers running on background threads by starting with the top level procedure and requesting output values. By following the graph back, evaluation of procedures and sub-procedures will eventually yield the results. This evaluation is lazy in nature, on three levels:
- An operators output is only calculated when needed.
- Only operator inputs that are actually needed are subsequently evaluated.
- Sub-procedures are only instantiated when an output is actually required.
By way of example, the
If conditional operator shown here represents a decision point in the procedure. A huge amount of work could be required to calculate each of the two possible inputs and it would be wasteful to work them both out. Instead, after the controlling boolean value is evaluated, only the input path corresponding to the result is requested to be passed back up the evaluation chain to its output.
|Choose one of two values|
This approach obviously saves us time as only calculations that are needed are performed. However, step 3 is critical to recursion support as you can't afford to speculatively instantiate an unknown number of recursions.
While the engine and tools can provide the features needed to build procedurally, the user needs to work and think procedurally too.
As well as the engine implementation intrinsicly supporting procedural generation, the actual procedures themselves require particular approaches and techniques to be applied during their design too. Some of these are documented here and should be considered when you set about your project.
Sub-division of Spaces
There are a couple of ways geometric content can be created, what I will call 'bottom up' and 'top down'. Bottom up is where you just start creating primitives (triangles, cubes, cylinders, etc) and placing object (meshes, components, etc), manipulating them and positioning them around some starting position and orientation to create your object. Top down is where you start with a volume which you partition up into portions correctly sized to create your primitives inside.
Whilst both are supported by Apparance, the top-down approach is recommended as it fits a "divide-and-conquer/refinement" model better in that you start low detail/large area and break it down to add more detail over smaller area. This also proves easier to control when you want to dynamically adjust detail level, you can just stop earlier to reduce the amount genereated. It also helps manage the interaction between adjacent objects, structures, and spaces, starting with the area you need to fill is easier to enforce as you subdivide than stopping when you get near the boundaries.
|Modelling Approach||Example Operations|
|Bottom Up||Create cube → Transform vertices to desired position|
|Top Down||Sub-divide/manipulate space to where cube needed → Create cube|
An important data type used in this process, and through-out many of the example procedures, is the Frame.
A Frame is a data type that describes an oriented bounding-box, or more precisely an oriented rectangular cuboid. A three-dimensional region of space with a width, height, depth, orientation, and position.
Most of the geometry primitives and object placement operators take a Frame as their input and construct geometry to fill it.
- Cube - Construct a cuboid fitted to the frame bounds
- Cylinder - Construct a cylinder fitted to the frame bounds
- Sphere - Construct a sphere fitted to the frame bounds
- Sheet - Construct a single sided flat sheet fitted to the lower Z face of the frame bounds
- Place - Place an asset such that it is scaled to fit the frame bounds
Full details of these operators available on the Operators manual page. A lot more primitive types and operations on them will be added in the future.
Frames don't normally need to be created as such, they are usually set up at the top-level procedure inputs (as a constant value) and everything beneath derived from them.
Frames can be manipulated in a variety of ways in local-space:
- Offset - Move the frame to another position in space relative to current position, by an amount or proportion
- Resize - Shrink (or grow) the frame in place, to an amount or proportion, and aligned to sides or centred
- Turn - Rotate about part of the frame in one axis
- Reorient - Re-assign the axes and orientation of them within space without changing its position or size in space
There are some world-space operations available too:
- Translate - Move the frame in world space
- Rotate - Rotate the frame about a point in world space in one axis (can imply translation)
- Scale - Scale the frame relative to a point in world space (can imply translation)
And various types of sub-frame can be derived from a frame:
- Split - Divide in-two in one axis, by an amount or proportion
- Shrink - Divide into three in one axis, creating a shrunken centre section and two equal side sections (by size or proportion)
- Extend - Generate to frames extending out of opposite sides in one axis, by an amount or proportion
- Diagonal - Generate a frame that sits down one of the frames diagonals and intersects the remaining corner with its top.
Full details of these operators available on the Operators manual page.
The inputs to the modelling process are the controlling parameters, and for a given set of these values you will always generate the same result. This is important for experiences that require consistency between repeat vists of a player and between separate players.
To maintain this, yet still achieve the (seemingly random) variety required of a rich and interesting world, all variety decisions must be controlled by a repeatable random number generator. That is one that will for a given input seed, produce the same output value or sequence of values.
An important type of information that we need to pass around a lot during procedural generation are random seeds. These are used a lot to drive variation within your world. By deriving new seeds, splitting off random sequences, and sharing seeds between instances, variation can be layered on at different levels of detail to produce good variability, patterning, and grouping of features as you would expect in the real world.
Propagating seeds allows for design elements to be shared across your world bringing consistency, yet still be unique to your world. We don't want everything to be completely random, managing seeds allows us to control this.
Random number generation in Apparance is supported via two Operators: Random and Twist.
|Generate a random number in a given range|
Random (in both integer and floating point versions) generates a random value between (inclusive) two specified values. Each value has equal chance of being generated (flat distribution), and is dependent on the input seed.
Design#for a design seed,
Variety#for a variety seed.
Along with each generated random number the Random operators create a new seed value that can be used to generate the next random number in the sequence.
Where you need to seed multiple sub-procedures, but don't want to have each generate the same content, you could just use a chain of random operators to generate separate seeds, but this can have problems within the sub-procedures when they are used to generate multiple values. This is because generating multiple seeds like this means they are related i.e. they are from the nearby points in the same random sequence.
To avoid this, an operator is provided to basically 'jump tracks' from the current position in the random sequence to another wildly different position. This means that subsequent values and seeds won't be related in the same way.
|Switches the random stream sequence to avoid duplication when sub-dividing procedures with similar content|
This operator is also parameterised to allow splitting the random stream into several different streams and should be used when you are propagating seeds to related sub-procedures, especially where recursion is involved.
A common need in many languages is to be able to repeat something, be it lamp-posts along a street, or fins around a rocket. At the moment recursion, whilst not the most intuitive technique is the only way Apparance supports this. There are several ways this could be done, and alternatives will be explored in the future.
In its simplest form, recursion allows the chaining together of a series of identical procedures, accumulating the result as you go, until some stop condition is met, e.g. a counter reaches zero.
Whilst this works fine and is the simplest to understand, it has some drawbacks that need to be considered sometimes:
- Stack depth - every iteration uses up a chunk of stack space so you are limited in quantity.
- Filtering cost - if you are only need to generate a portion of the sequence then filtering (say in a bounding box) needs to still be applied to all the elements in the sequence.
The solution here is to use a 'binary chop' approach, each time dividing the problem into two halves of similar size and recursing into both of them. This both uses less stack space and plays better with any filtering/culling tests. To help implement this, the
Stacker.Splitter Operator, and friends, are provided to do all the heavy lifting for you, as well as providing some extra useful features.
A useful capability is to be able to divide up space and fill it with repeated objects, sometimes with intermediate spacers and end-caps.
A pair of supporting Operators are provided to help with this process, handling all the calculation and recursive construction of the parts together for you.
|General stacking/packing utility. Recursively split up space to contain a repeat series of objects with optional start/end caps, all separated with spacers.|
|Combine required elements from stacking process.|
Here they are in use; in the recursive part of the process.
The top two sub-procedures are where the recursion happens, and the stacking problem is divided in two.
The lower sub-procedures are where you create geometry to form the relevant parts (here a placeholder coloured element):
- Lower Cap - optional fixed size part to include at the start
- Spacer - optional fixed size part to include between objects and to optionally pack the end-caps
- Object - always required, and with the size being adjusted to ensure full occupancy of the available space
- Upper Cap - optional fixed size part to include at the end
The following additional configuration for the stacking process can be specified:
- Axis - which axis to perform the stacking on (0 for X, 1 for Y, 2 for Z)
- Object Size - The object size to aim for in the stacking direction
- Spacer Size - The exact spacer size (if used)
- Lower Cap Size - The exact lower cap size (if used)
- Upper Cap Size - The exact upper cap size (if used)
An additional feature is indexing support, where the Splitter operator will provide the Index and Total Objects count to each object (or spacer) when they are instantiated. To do this; pass the starting index and the total objects parameters as procedure inputs, and wiring the up the lower and upper recursion instances to the Lower Index, Upper Index, and Total Object splitter outputs. Then you can pass the Total Objects and Object Index outputs to the geometry generation procedures that need it.
When the repeat procedure is placed for use, in a containing procedure, the
Stacker.Options operator can be used to configure which spacing and capping parts are required.
|Helper to generate selection inputs to a stacker based object.|
Here a single repeat procedure is placed down and configured for use. The frame passed in will provide the volume to be filled, and the options determine which spacers and caps are required.
Together they give rise to the following output (in this case, with all parts enabled).
By configuring the spacing options, end cap options, and providing the appropriate geometry generation procedures, you can use the Stacking Operators to construct a wide variety of complex composite objects. Some examples might be:
- A fence with narrow posts and larger end posts - end-caps, objects, and spacers all provided
- A row of columns - columns as spacers, with small non-geometry objects as flexible gaps between
- A stack of books - split on z axis, no caps or spacers, object is book of random orientation
- A course of brickwork - bricks as objects with inset cement pieces as spacers
- A grid of objects - by nesting a second recursive stacker as the object/spacer but along a different axis
Apparance will in future also support repetition by a) in-place iteration of procedures, and b) geometry duplication.
Objects such as meshes, and other instanceable components, can be placed as part of the generation process. This is performed first by using the
Resource.Resolve operator to lookup an engine-side asset to be placed. This yields an ID to reference it with and a bounding box describing its original size.
|Look up resource information (placement identifier and bounds)|
Using this information you can build an appropriate frame for placing your object, either with the correct (original) sizing at some location, or you can stretch/squash the asset to some arbitrary size and aspect as you position it.
|Place a resource instance within a given bounds|
The output of this operator can be merged with other output connections the same way geometry is accumulated.
Resources support variants, which are basically alternate assets that can be used in it's place. These are simple way for you to introduce variety into your procedures as artists can just drop in more models and they are used automatically.
Variants output can be used to randomly select a variant of a resource (1 to N) and then feed back to the
Variant input to actually look up the ID for it.
0for normal resources, and from
1to the number of
Variantsfor selecting a variant version
Because Apparance doesn't know (or need to know) about the engine it's plugged into, a way of describing the asset to be placed down is needed. On the Apparance side this is a descriptor string, and on the Engine side this is a lookup table mapping descriptors to actual project assets.
Descriptor input to the node is a label that uniquely identifies the resource required. This is a string that, by convention is partitioned into two or more parts separated by dots.
The Resource lookup tables are implemented engine-side as a collection of assets with descriptor-to-asset mapping lists. Each of these has the asset descriptor Category and Sub-categories exposed as an editable hierarchy to aid organisation. The Named leaf nodes in this hierarchy have slots for one or more game asset to be referenced (one by default, but an expandable list for Resources supporting Variants.
Generally, you can organise your Resource lookup with mutiple Resource Table assets, each handling a specific Category of asset, and all referenced from a 'Root Resource Table' which has to be configured in the projects settings.
A variety of experimental geometry generation Operators are available (see full primitive operator list). These fall into a couple of category axes:
- Primitive - flat triangle, or solid shape such as cube, cylinder or sphere
- Compound - constructed from more data e.g. filling or extruding a polygon outline
- Frame - location specified by bounding frame, usually exactly encompasing
- Vertex - explicit world-space coordinates, e.g. vertices
- Uncoloured - colour to be applied later, usually the more complex, compound shapes
- Vertex colour - colour specified for each vertex, usually more basic primitives
During the synthesis process, operator generated geometry (and object placement) is accumulated in the synthesis buffer. The Model Segment (white connection points) data type is a reference to where in the buffer this geometry has been created. Passing this around allows specific pieces to be referred to and have further processing operations applied (see below). A special
Merge operator provides a way of combining separate pieces of geometry together to be treated as one.
|Merge geometry together|
The geometry referred to by a Model Segment can have a number of manipulation Operators applied to it. Currently operations of these types are available but more will be added in the future. See full the modelling operator list.
- Surface - e.g. Colour or Material
- Mapping - e.g. UV scale/translate/constant/projection
- Warp - e.g. taper, noise distortion
- Transform - e.g. rotate, offset, scale, or move (between frames)
- Normals - e.g. normal blending
Materials control the surface appearance of the geometry. There is a fallback material specified in the plugin configuration for un-decorated meshes. The default engine resource list also has a simple material for flat shaded coloured surfaces you can reference with the
Apparance.Material.Colored descriptor. Geometry supports vertex colouring to allow multi-coloured geometry to be combined into single draw calls.
For all geometry primitives, you can apply an all-over colour using the
Colour Operator. This sets the vertex colour channel in the mesh data.
|Paints geometry vertices with a particular solid colour|
This sets all passed in vertices to the provided colour.
Material are applied in a similar way to the colour. A
Material Operator applies a material ID to the triangles/lines in the supplied Model Segment. Materials are obtained the same way other assets are found; by looking up with the
Resource.Resolve operator by descriptor string.
|Applied a particular material to the specified geometry|
By default, most primitives come with a default UV mapping scheme. You can manipulate this with the
- MappingConstant - Set the passed vertices to all have the same UV value
- MappingScale - Scale the passed vertices UV values
- MappingTranslate - Offset the passed vertices UV values
- MappingFrame - Project UV a mapping from the provided frame onto the vertices (planar mapping)
|Use a world space frame to project UV mapping onto geometry (XY across frame is 0 to 1 in UV)|
The mapping operators support multiple channels (up to three at the moment) that can be used to pass all sorts of extra, per-vertex, information to the material shaders in engine.