Juhana Honkanen
2024-10-22

Haus Designr AI

Alternative Party 2024 dynamic demo
write-up for nerds

Intro

Alternative Party is back after about a decade of silence! One demo competition category was "dynamic demo", where entries must be noticeably different every time they are run. I had been toying around with procedural mesh generation and this was a nice opportunity to put it to work in some serious design work.

This write-up explains parts of the demo at a general level, while details are available in the source code. These are intended for demo geeks or Unity users, and are probably not worth the time for other people, so you've been warned!

Text slider

The first part of the demo is just moving text, so nothing much to talk about here. But I did learn that you can modify the positions, meshes and other features of individual characters in a TextMeshPro element, without having to break the text into separate objects. The intro has just one such effect but something more complicated could be fun to make for text scrollers or greeting sections for example.

Walls (imagining unusual shapes)

The purpose of these walls was to create random building blocks that are not cubes, spheres or other predicatable shapes.

The ground (X/Z axis) here can be thought of as a 1000x1000 grid. A flock of normal Unity GameObjects fly across the grid, changing direction at random, but they also leave behind a trail of 1x1 blocks that snap into the grid. These blocks become walls that enclose random areas of the ground into random shapes.

The grid is a two-dimensional array[1000,1000] of integers, in which 0 means empty space and 1 means wall.

image

Even though the wall objects are about as simple as it gets (default cube, no physics, shared standard shader etc), having tens of thousands of active GameObjects is still hard on the CPU and quickly starts to bog down the framerate, so the number of these objects needs to be reduced.

image

The 1000x1000 area is divided into chunks, and whenever a wall object is spawned, it's added to a list of objects in that specific chunk. When a chunk is complete, the meshes of all objects in that chunk are combined into one object.

image

Just before the meshes are combined, straight sections of wall are replaced by stretched cubes as you see above. So for example if the script detects ten 1x1 objects side by side, it removes all of them and creates one 10x1 object in its place, reducing the number of required vertices to 10%. Since the objects don't have to move, they're set to static and kinematic for better performance.

Having an object pool instead of rapidly destroying and creating new objects helps performance substantially, and Unity has its own class that controls the pool size automatically. I assume this class has some under-the-hood optimisations that run faster than my manually-made pools would.

image

Once the entire play area is complete, all the wall chunks are combined into a single mesh in a single object. And once the colour fade-in is complete, all wall sections share a single material (solid colour) which is very light on the GPU.

Visually there's nothing to gain in running a demo like this beyond the monitor's refresh rate, but after doing these optimisations I wanted the viewer to see how it runs, so I set VSync to off by default and added an FPS counter.

Propagators (casting building blocks)

The building blocks could be created instantly based on the 1000x1000 array before, but filling them gradually is a fun visual effect for the demo.

This part works with 1x1 GameObjects like the walls did, but this time they "propagate" – meaning whenever such an object spawns, it checks if its neighbouring squares are occupied. If the square is empty, a new object is spawned into it during next round of propagation. This way all shapes are filled in a progressive and somewhat uneven-looking fashion. Instead of object positions, the checking is based on the values in the 1000x1000 array, which is much faster.

But before the propagaters are launched into action, a copy of the 1000x1000 array is flood-filled invisibly, marking down different values for each enclosed area. This way we get the total number of building blocks, a unique number for each block, starting positions for the propagators, and a way to reconstruct a simpler version of each building block later.

image

I thought I might propagate an entire shape with these 1x1 objects and replace it with a simpler version once it's completely filled, but due to randomness some objects could get very large, and the resulting tens or even hundreds of thousands of active GameObjects could ruin performance. To avoid this, the blocks have to be simplified before they are even complete.

Once one row of the block is filled and its colour fade-in is complete, it's replaced with a simple rectangle, much like the straight sections of walls earlier. Consecutive series of these rows are periodically combined into chunks, reducing active GameObjects to an acceptable number. Object pooling is also a must.

image

The CPU can now deal with these blocks. They consist of neat slices and are merged into a single GameObject once they're ready - but I wasn't quite happy with how many useless vertices there still were, duplicated and out of view between each slice. Therefore, once a block is completely filled, it's hidden and replaced with an (almost) identical-looking but much simpler object.

image

This block was reduced to about 25% of the vertices of the above "sliced" version.

The 1000x1000 array that was flood-filled earlier is now used to reconstruct these simpler versions of the blocks. By searching through the array with each block's unique number, we can "draw" the outer edge of a 2D object into a list of X/Z coordinates. Each object's height (Y) had been saved earlier, and it's now used to turn the object from 2D to 3D.

Travelling along the z-axis, whenever the outermost x-coordinate changes, it starts a new "slice" along the object's side, and each slice can be made into a simple quad (two triangles).

The top mesh (or "lid") was more difficult than the sides. I think it should be doable to connect points on the left and right side neatly into triangles, but since the sides aren't symmetrical and the number of points varies, it became a bit of a headache and a computational geometry inferiority complex for me. After struggling for a time, I went with plan B which involved creating new points along the middle of the lid, and connecting each point along the side to the nearest of these middle points. The shape above doesn't need this many extra points, but some thin and twisty shapes do.

The bottom mesh looks identical to the top and is made the same way.

Despite these optimised objects, the propagating scene is by far the most CPU-intensive part of the demo, because propagation happens a few hundred times per physics frame (10ms). As of release, propagating takes about half of the CPU time while colour changes (the fade-in of the blocks) take the other half.

Colours are calculated into premade lists ahead of time and swapped using material property blocks, which is faster than swapping entire materials. I planned to try to use a custom shader too, which I suspect could be much faster still, but in the end didn't have the time to get into that. Trying the shader approach would've certainly been a sensible thing to prioritise (among a few other things), but you make demos for fun, not to be sensible.

The final propagating speed is a compromise; I wanted the effect to be slow enough in case some audience members wanted to observe it, but also fast enough so that all the other people won't die of boredom in the meantime. Framerate in this scene is a combination of propagation speed, number of colours per object, and colour fading speed. (Once the colour fade is complete, the entire object can be a single material -> faster.) I guesstimated these values so that the scene runs 20-30 fps on my 2018 potato, hoping for it to be ~60ish fps on the much faster compo machine.

image

The energy counter is nonsense of course, but also a reminder of generative AI's massive, unclear and unregulated carbon footprint. The counter uses BigInteger to avoid overflow. This scene seemed like the best place for the meter, since it has the most "idle time" for the viewer in case they are not interested in how the propagation works.

Choosing cutest blocks

image

The random propagating usually generates around 200-250 building blocks, but it's useful to have a fixed number, and small ones are less "efficient" in terms of CPU usage to visuals. The demo here chooses 100 largest blocks, but makes it less obvious by skipping the two largest ones, lighting up the rest in random order, and using the word cute to distract you. Lies, all lies!

While you're being lied to, the demo uses the spare time to get rid of all the pooled GameObjects and other stuff that is no longer needed. Destroying objects is normally no issue, but the pools of walls and propagators end up typically containing around 50,000-100,000 disabled GameObjects, and calling destroy on them at once would freeze the demo for several seconds, so a coroutine destroys them gradually instead. I also destroy all scripts that no longer have a purpose in the demo, not for performance but to make the editor view clearer to look at: whatever scripts are still alive are what is left of the demo.

Analysing house construction

image

This part pokes some more fun at GenAI; this was mostly inspired by the ridiculous "loading lines" we get with ChatGPT o1-preview these days, although OpenAI isn't the only one doing this. The demo doesn't run any algorithms here, it just rotates blocks and highlights random ones for that deeply analytical vibe. I asked ChatGPT to come up with the text lines, manually chose 100 cutest ones, and added a random delay after each one is displayed.

The lines are picked at random, except for the "building block placement" which is always at the same position and "errors" after a slightly longer delay. Everything else seems to work so what could possibly go wrong?

The slowly disappearing black discarded blocks are a "dissolving" shader (easy tutorial on it for example here). Its normal appearance can be seen when the back wall dissolves to reveal the cloudy sky, but the blocks get a fun effect when the triangles aren't connected in any sensible way but just the numerical order.

Building house

image

Finally, the punchline! No custom code necessary here - just gave the objects rigidbodies and colliders and threw them around. The Unity/PhysX engine handles the physics, in my opinion in a wonderfully convincing way, and the slapstick basically creates itself. The simplified objects get nice FPS here despite everything having mesh colliders. The automatically generated invisible collider meshes don't follow concave parts of the objects exactly, but slight inaccuracies are not a problem in a use case like this.

The sounds are borrowed from an earlier project of mine called Parcel Manager. They're originally designed to show the heavy mishandling of mail, but it seems to suit house building okay too. Audio samples are categorised into light, medium and heavy impacts, decided based on collision velocity, and each category gets its separate audio player and cooldown. Sounds are played as OneShot with randomised pitch, but omitted if cooldown is still active. That way sounds don't get interrupted but you also don't get thousands playing simultaneously. And you (almost) always still hear that heavy impact when your eyes catch one.

The furniture, plants, road signs and dog house are free Unity Store assets, linked below.

Saving/loading houses

image

The houses seen on the street are the actual houses generated on previous runs of the demo. The files are saved to PersistentDataPath and include positions, meshes and triangles for the building blocks; positions and id for the furniture; traffic sign id, positions for the plants, and time stamp and dog name.

Unity's vectors don't serialize for saving, but custom "SerializableVector3" etc. classes get around this. We only need the positions and rotations anyway, none of the other stuff that vectors have.

There is a lot of room for improvement in the save file size. Could use binary instead of JSON, could save with less float precision, could reconstruct the blocks from contour points/height instead of verts/triangles, could find a better zip libary, and could probably do who knows what else. But I was running out of time and decided the current ~1mb per house is not too much of an issue. My biggest regret was that I wanted to give varying toys for each dog! :C

Music

A dynamic demo would be a fun spot for procedurally generated music, like letting an algorithm play a synthesizer or procedurally stitching together seamless loops.

I had just 1 day remaining to deadline though, so I didn't want to risk failing with more complicated dynamic attempts and slapped together a simple regular soundtrack instead. Normally I enjoy taking my time with tweaking sounds and making melodies, but this called for a faster approach with generous amounts of sample packs instead. I aimed for the "imagining" part to be more ethereal and the street view outro to be a less percussive variant, and that's about it.

If you replay the demo, it runs the faster version and you get royalty free jazz, which isn't synced to the scenes but does make for a funny switch of tone.

Controls

The demo can play at the default regular speed, or in a faster mode, which can be selected by the prompt at the start or the restart at the end. Pressing "O" will switch between these on the go, and pressing "P" selects an even faster speed for dev purposes. These affect various delays in the game but the TimeScale is always 1.

"M" mutes music in fast mode. ESC exits at any time.

"Haus designr"?

The phonetic spelling is aims to keep this application separate from any other house designer apps that might pop up in the generative AI market. I'm sure they couldn't handle the competition so I wanted to be merciful.

Asset credits

Dog house and potted tree
https://assetstore.unity.com/packages/3d/props/exterior/low-poly-houses-free-pack-243926

Road
https://assetstore.unity.com/packages/3d/environments/roadways/low-poly-road-pack-67288

Road signs
https://assetstore.unity.com/packages/3d/props/industrial/road-sign-big-pack-139858

Fonts
https://font.download/font/fairune-ui-chmc
https://font.download/font/fixedsys-excelsior-301
https://font.download/font/mouldy-cheese

Sounds
https://www.youtube.com/watch?v=NEG7BDJjAxg
https://freesound.org/people/acclivity/sounds/19989/
https://freesound.org/people/Stickinthemud/sounds/27880/
https://freesound.org/people/lyrislite/sounds/95569/

Skyboxes
https://assetstore.unity.com/packages/2d/textures-materials/sky/free-skyboxes-space-178953
https://assetstore.unity.com/packages/2d/textures-materials/sky/skybox-series-free-103633

Ground texture
https://assetstore.unity.com/packages/2d/textures-materials/4k-tiled-ground-textures-part-2-283704

Music for fast forward mode
https://pixabay.com/music/traditional-jazz-happy-positive-energic-jazz-243719/

Music for normal mode is self-made and everything else is generated procedurally.

Contact

http://discordapp.com/users/244907716231954442