検索 - User list
Full Version: Cappy's Quest - Building a Houdini Pipeline for the Playdate
Root » 2025 Game Art Challenge » Cappy's Quest - Building a Houdini Pipeline for the Playdate
wolfwood
My entry for BEST DISQUALIFIED TOOL!

Why follow the rules when getting disqualified means you can win lustrous No Prize!

Prelude
Upon discovering the Elderwoods contest a little over a month ago I was excited to take the VFX hat off and dabble in some Game Dev. But upon seeing the requirements for Unity or Unreal was temporarily disheartened as I was not particular keen on dealing with their ecosystems.

So it was decided to simply ignore that rule and instead make a faux submission for the Playdate [play.date]! The small handheld Playdate only has 400x240 resolution and 1 bit depth. What else do you need? That's more than last console (Gameboy) I owned had!


Yes, that is the Capybara among the trees.

The Playdate
For those of you missing out, the Playdate came out a few years back and is a lovely little handheld device. As mentioned above the display is 1-bit, and the resolution is 400x240. An extra gimmick is it has a crank on the side, that games can use as an extra input device to great effect [play.date]. It also has wifi so it can connect to the internet to download games or they can be side-loaded. They even provide a 3D Model [assets.play.date] of the device in USD! :>




To develop games on the Playdate they provide a Lua API [sdk.play.date] and a C API [sdk.play.date]. There is no "game engine" and the bulk of the API functions for drawing bitmaps and audio playback. Your hook into the execution is a callback that triggers on every frame.

To ease the development process a Simulator [help.play.date] is provided which can be used to test games, monitor memory usage and access other debug information.


The Elderwoods

The challenge I set for The Elderwoods was to make tooling to allow for full content creation in Houdini than can be into the Playdate in a moderately streamlined fashion. Early on the game mechanic I wrote and built was a side scroller (Super Mario Bros.), but about half way through opted for a level tile approach (Legend of Zelda) since thought it would be more fun to work on connecting tiles. (Plus Zelda was a superior game.)

For the hero sprite we will obviously be using the Capybara (since it is already animated and is also cute).



The general workflow is -

  1. 3D model creation using standard SOPs
  2. Rasterization and post processing in COPs (Various COP HDAs and OpenCL nodes)
  3. Aggregating COP "volume" images into a library (Bitmap Library SOP)
  4. Level editor which uses bitmap library. (Level Builder SOP)
  5. Map layout/editor which is for connecting the different levels. (Map Builder SOP)
  6. Bitmap Library, Level Builder and Map Builder all export out JSON data.
  7. The Elderwood game executable, reads the JSON on load, drawing the levels and setting
    collisions, and transition points as defined by the Level Builder and Map Builder.

1-Bit and COPs

There were a handful of HDAs created to assist with this project and these mainly focused making moving COPs -> SOPs -> COPs a bit easier. But there are two HDAs that are worthy of mention.

Dither COP
This node offers the majority of the dithering algorithms commonly used for 1-bit graphics. Including the standard Bayer methods as well as the error diffusion methods like Floyd-Steinburg and Atkinson. The simple Bayer algorithms were able to run directly as OpenCL COP nodes. But for the error diffusion based methods COPs wasn't well suited due to the iterative nature of the algorithms. For these SOPs was used instead. Internally the HDA moved the data to SOPs, processed the data in an AttribWrangle, updated the volume through VolumeWrangle then sent the data back to COPs.


Bit Composite COP
COPs being a 3D not-compositor compositor is lacking UX when working directly in pixel space (IMHO) which is extremely important when working with pixel art. For example if I want to move a 32x32 sprite to x=300,y=100 on a 400x240 image it was surprising difficult to do so without it linearly interpolating values. So to work around this a Bit-Composite COP was created which wraps some OpenCL that does pixel index lookups.


The Bitmap Library SOP
The Bitmap Library SOP is responsible for collecting all the bitmaps into one place and also exporting that data so that the Elderwoods Playdate game can ingest and use.

The Playdate SDK can convert .png files into its internal .pdi files, but for this project all the bitmaps were encoded into a single JSON file. The reason for this is that long term I plan on setting up the Elderwoods game to pull data directly from an active Houdini session via hwebserver while the game is running. Basically a poor man's hot reloading of a game.

The basic information that is exported is whether the image has a mask or not, if static or animated and the volume image data. While Houdini's volume image data is 32-bit floats, this data is converted to a bitarray encoded as 32 bit integers(1). With array is then encoded into base64 for storage in the JSON. The benefit of this is within the Elderwoods game, the base64 can be decoded directly into the bitmap's memory address reducing the need for a bunch of temporary data buffers(2).

(1) - Volume Conversion Code [github.com]
(2) - Reading encoded bitmap [github.com]

The Level Builder SOP

The Level Builder SOP takes the Bitmap Library SOP as input and uses those source bitmaps to draw a composited image volume. This node probably consumed a lot more time than expected. Partly because this was the first time I've created a ViewerState as well as the numerous false starts when trying to display a composited result to the user interactively.




Drawing the Level
False starts:
RGBA Image Volumes
Initially I tried converting by Mono Img / Mono Mask COP images to RGBA images and displaying them in the viewport (with small depth differences). But this made viewport incredibly slow the more images that were used. Being almost unusable past 60 bitmaps. Mono Imgs don't suffer from this, only Image Volumes with an alpha it seems.

Stamp COP
This had promise, but the interface was clunky when wanting to deal with pixel coordinates. Additionally it only allowed ten different stamps at a time. This coupled with COP's lack of looping meant coming up with a solution in SOPs instead.

Volume Wrangle
The Level Builder HDA stores each bitmap id placed in a multiparm along with pixel coordinates. A Attribute From Parameters
SOP is used to fetch all that data into a compact form in SOPs. A "background" Volume is used set to resolution of the Playdate with some padding. The then a Volume Wrangle is used composite all the reference bitmaps into the background volume. The basic algorithm being
- Sort Bitmaps order by depth
- Foreach Voxel
- For each bitmap in multi-parm
- Check if that voxel is bounded by one of the bitmaps specified in the multiparm
- If it is evaluate the mask and composite via volumesample other input.
- Exit out of loop as anything else will be covered.

This worked pretty well overall and was quite fast even up to 100 bitmaps being composited in a level. A little bit of lag but not an overwhelming amount.

Volume Wrangle Revisited
The Volume Wrangle had a lot of point() function look-ups, since that is where I was storing data about resolution and
general metadata about each bitmap from the library. There was a Houdini Hive talk by Kai in 2024 [www.youtube.com] that went over ways of making VEX faster. One of the tricks mentioned is to avoid querying the node inputs via functions like point() and to instead encode this as arrays in the detail attribute. Which is what I did for the point lookups and it actually resulted in about a 10% speed up of the Volume Wrangle. Woooooo.

Volume Wrangle III
After getting rid of all my point() calls...what if... I got rid of all the volumesample() calls too? So for the next experiment
I encoded the volumes as large detail array attributes. For some reason this slowed things significantly. So I quickly abandoned
this approach.

OpenCL
Part of the reason for giving up so quickly on the last experiment, is because once I had compactly encoded all the data as arrays
why was I even bothering with VEX at all? Let's just bind this data in OpenCL and make the GPU do the work. This ended up being
almost 4x faster than the Volume Wrangle approach.


The ViewerState
Some of the other features of the Level Builder ViewerState include
  • Drawing outline of the bitmap's mask, which is handled with a Trace SOP Verb
  • Selection mode. Since the picking is against a single volume image, the volume image location is sampled and multiparms evaluated to see which parm was selected.
  • Depth modification with mouse wheel. Alternative the depth of the image can be automatically set using position of the bitmap relative to the Y axis.
  • Flipping images in x axis

The Map Builder SOP






The Map Builder takes all the various levels you have created as inputs into the SOP. There is a multiparm which allows you to set constraints between the different levels. For example "Grass Planes" is "West Of" "Forest Entrance". Once a list of constraints is built the map will automatically layout the levels and connect them. When the JSON [github.com] data is exported it contains data about which level IDs connect to each other. Then within the Elderwoods game, if a player walks outside of a level boundary this information is used to know which level to switch to. Additionally it allows specifying which level the game starts in and where that player should be positioned.

The "Game"

While the Playdate has both Lua and C APIs, I'm not a fan of either so I wrote the back-end in Zig [www.ziglang.org]. Zig has great interop with C so I could do the bulk of the work in Zig and rely on Daniel Bokser's lovely bindings.

The game starts up, reads the JSON bitmap library and map JSON. Using the map data it determines which levels to load and build.
Collision objects are setup to block the player from places they shouldn't move, which are set in the Level Builder SOP.





Thanks for running the contest and I look forward to my glorious No Prize.
wolfwood
All the materials for this are in Github in various degrees of organization.

Elderwood Demo [github.com]

The Houdini related stuff is here:
https://github.com/shadeops/elderwood/tree/Elderwood_Demo/hips [github.com]

If you clone the repo and launch Houdini in the hips directly everything *should* be in the Houdini Path.

The Zig code for the game is here:
https://github.com/shadeops/elderwood/tree/Elderwood_Demo/src [github.com]

If you have the Playdate SDK on disk and the PLAYDATE_SDK_PATH env var set, as well as the Zig 0.14.1 compiler you should be able to do `zig build run` and launch the game. (Just don't venture past the first two levels as they are completely empty.)
wolfwood
Video version explaining the process, warning you have to listen to me talk.



While I'm not a huge fan of AI, I will gladly throw money at some AI that can remove all the "umms" and "ahhs" when I talk. :P
This is a "lo-fi" version of our main content. To view the full version with more information, formatting and images, please click here.
Powered by DjangoBB