Building imgplex: part 6

imgplex tooling game-dev

This is a series of posts on building imgplex, best read in order:
Part 1 - The why, what, and how of imgplex
Part 2 - Getting things up and running
Part 3 - The node definition system
Part 4 - Executing the node graph, making it fast
Part 5 - Two graphs in one
Part 6 - Multiple inputs and outputs, processing images as sets
Part 7 - The small, measured optimizations beneath the big ones

One in, one out - until it wasn’t

The earlier posts described the pipeline as if it had a single entrance and a single exit: images come in, flow through the nodes, and land at the Output node. That was true for a while, and it was the right place to start. But real image work almost never fits that shape.

You want to pull frames from one folder and a set of masks from another. You want the same processed image written out as a full-resolution PNG and have its computed dimensions dumped to a text file and have the whole batch tiled into a single contact sheet for review. One source and one destination stops being enough almost immediately.

If you’ve built graphs in Substance Designer, you already know the shape this wants to take. A Substance graph doesn’t have one output - it has a basecolor output, a normal output, a roughness output, all fed from shared upstream nodes, each producing a different deliverable from the same network. imgplex ended up in the same place: any number of inputs, any number of typed outputs, all on one canvas.

Multiple inputs

The first half of that is straightforward to describe and was a real change under the hood. Instead of a single implicit source, the canvas supports any number of Input nodes, and each one owns its own image list and its own filmstrip. Click one Input node and the filmstrip shows its images; click another and you see that one’s. They’re independent sources that happen to share a canvas.

This matters the moment a graph branches. A compositing workflow might take a base image from one input and an overlay from another. Keeping them as separate nodes, each with its own queue, means the graph reflects what’s actually happening - two distinct sources meeting downstream - rather than forcing everything through one funnel.

The wrinkle this introduces is that the engine can no longer assume it knows where an image “came from.” With one input that question is trivial; with several, every output needs to know which source feeds it. So when a run starts, the engine walks backwards from each output node - following the wires upstream, hop by hop, until it reaches an Input node. That trace is what tells it which queue of images to push through that particular branch of the graph. Multiple inputs and multiple outputs are really the same feature viewed from two ends: the graph became a many-to-many network, and the engine has to resolve which end connects to which.

Multiple typed outputs

The other half is outputs, and this is where the typed-wire system from the last post pays off again. There are three kinds of output node, and they’re genuinely different things:

Each is a first-class node on the canvas with its own inspector and its own settings. Interestingly, this wasn’t the original design - the output started life as a single node with a mode switch, and the flipbook was folded in as just another mode. That worked until it didn’t: each output type accumulated enough of its own settings that cramming them behind a dropdown on one node got awkward. Splitting them into separate typed nodes let each one have exactly the inspector it needed, and made a graph with several outputs readable at a glance - you can see the three destinations sitting on the canvas instead of having to click into one node to discover it’s secretly doing three jobs.

When you run the workflow, every valid output node is processed in turn. One graph, one Run, several deliverables - the same network feeding a PNG, a manifest, and a contact sheet in a single pass.

All three output types share the same “skip existing or overwrite” choice, too. That started as an Image Output feature and later got extended to the other two, because re-running a workflow over files that already exist should behave predictably no matter which kind of output produced them.

Processing in sets

The features so far treat each image as an independent unit. But a lot of image work comes in groups that belong together and have to be processed as a unit: PBR texture sets (diffuse, normal, channel packed mask), the six faces of a cubemap, a run of frames that will become a sprite sheet. Processing those one loose image at a time loses the thing that makes them a set.

This is what the Set Input node is for. Instead of treating a folder as a flat list of images, it groups them by a naming convention - a shared prefix or suffix - so that T_Example_D.png and T_Example_N.png are understood as a PBR texture set, or Frame_01, Frame_02, Frame_03 as one flipbook sequence set. The group travels through the pipeline together as a unit, and nodes that operate on sets (the flipbook being the obvious one) receive the whole group rather than a single frame.

For a technical artist this is a familiar mental model. It’s the same grouping you rely on when a tool ingests a texture set - basecolor, normal, roughness, metallic - by matching filename suffixes, and treats the four maps as one material rather than four unrelated images. The naming convention is the grouping logic, and it’s the convention art teams already follow.

The prefix that drives the grouping is itself a wireable port, which is a small thing with an outsized payoff. Because it’s typed as a string input, you can drive it from the value graph instead of typing it in by hand - compute a prefix from some other property and the grouping adapts automatically. It’s the two-graph model from the previous post showing up exactly where you’d want it.

Flipbooks

The Flipbook Output deserves its own mention because the name is not a coincidence. In real-time VFX, a flipbook is a grid of animation frames packed into one texture that a shader steps through over time - a staple of particle systems. Tiling a set of images into a single sheet is the same operation, whether you’re building an actual flipbook texture for a particle effect or just want a contact sheet to eyeball a hundred processed images at once.

The node takes a set, lays the frames out in a grid, and composites them into one image. The background between and behind tiles is a configurable color, which matters more than it sounds - a transparent versus a solid background is the difference between a usable particle flipbook and one with fringing artifacts, and the choice gets passed straight through to the composite step.

Running it without nasty surprises

Once a graph can have several inputs and several outputs, “run the workflow” is no longer a simple instruction. Some outputs might be ready; others might be missing a required setting or not wired to a source at all. The worst possible behavior here is a silent partial run - you hit Run, some outputs quietly get skipped, and you don’t find out until you go looking for files that were never written.

So a run doesn’t just start. First every output node is validated independently, and a dialog lists each one with its status and, for any that can’t run, a plain reason why - this output isn’t connected to an input, that one has no file path set. You see exactly what will and won’t run before committing to it, rather than discovering the gaps afterward.

A couple of related behaviors back this up. Nodes with no path to any output are skipped entirely during both preview and batch, so a half-wired experiment left sitting on the canvas doesn’t throw errors or slow anything down - it’s simply inert until you connect it. And saved workflows carry the app version that wrote them, so opening a file made by a newer build warns you about the mismatch instead of silently misreading a format that has since changed. When a run is underway, the progress modal names the file it’s currently on, so on a long batch you can see where the pipeline actually is rather than watching a bar inch along with no context.

None of this is glamorous, but it’s the difference between a tool you trust with a 2000-image job and one you have to babysit.

Next post in this series: Part 7 - The small, measured optimizations beneath the big ones