PSXIFY
Solo Developer · Developer Tooling
A postmortem on building a Rust tool that bakes high-poly models into PS1-style assets, covering a staged cache pipeline, a Blender reader that needs no Blender, and the transparency bugs that shaped the culling design.
Stack
Rust, egui, glTF, FBX, meshoptimizer, rayon
Links

Overview
PSXIFY takes a high-poly model and produces a low-poly mesh with a small, palette-limited texture. The hard part is not any single step. Each stage depends on the one before it, and the user needs to tweak the result interactively, so the pipeline has to avoid redoing work it does not need to redo.
This postmortem covers how the pipeline is structured, the three problems that changed the design the most, and what the tool still cannot do.
Pipeline as a chain of caches
The processing runs in three stages. Each stage stores its output under a key built from the parameters that affect it:
- Geometry. The mesh is welded, small pieces are cleaned up, hidden faces are culled with a BVH raycast, and the mesh is decimated with meshoptimizer. The cache key holds the reduction mode, the target, the per-piece flag, the cleanup threshold, and the cull mode.
- Bake. New UV charts are unwrapped and packed, and albedo, occlusion, normals, metalness, and emission are baked from the high-poly source. The key adds the texture size, because the bake resolution is four times the final size, capped at 1024.
- Compose and quantize. The AO, relief, and light sliders are applied to the stored bake, the image is downscaled, and the palette is quantized and dithered. Nothing is recomputed here except this step.
The payoff is that moving the colour count or the dither slider costs milliseconds, because the geometry and the bake are reused. The cost is that every parameter has to be classified correctly. For example, the absolute triangle target is ignored when the mode is percentage, so changing it must not invalidate the geometry cache. I wrote tests for those edges, because a wrong key produces a stale result that looks correct.
Reading .blend without Blender
Blender files are a common input, and requiring a local Blender install would have put the tool out of reach for anyone without one. The app therefore reads .blend files directly.
Each file carries its own description of its internal structures (SDNA), so the reader looks up fields by name instead of hard-coding offsets. That is what lets one reader handle Blender 3.x mesh data (MVert, MPoly, MLoop) and 4.x/5.x attribute storage, compressed or not. The reader covers the parts the pipeline needs: mesh data, object hierarchies with the common rotation modes, and the Principled BSDF inputs that feed the bake. Anything else is ignored with a warning shown in the app, so the failure is visible rather than silent.
Transparent cards and the culling decision
Hidden-face culling assumes that anything in front of a surface is solid. On fences and foliage that assumption is wrong: a transparent card in front of a surface would hide the surface behind it, and the visibility test would remove geometry that is actually seen through the cut-outs. The geometry stage therefore checks has_transparency() on the model. When it is true, culling is set to off, and piece cleanup and decimation still run. The bake also lets light through according to opacity, so AO does not darken regions that are visible through a cut-out. The preview uses depth for opaque texels and ordered compositing for transparent triangles. Intersecting transparent surfaces can still show artefacts, which is the usual limitation of that ordering.
Reduction that never erases a piece
Whole-model decimation has a predictable failure: small separate parts, such as the separate cards in a foliage model, can lose all their triangles when the budget is tight. In Pieces mode each connected component keeps at least four triangles, or all of them if it had fewer. If the decimator drops below that floor, the piece’s budget is raised until it survives.
The consequence is that a very low target can produce more triangles than requested. The UI and the README say so, because the alternative was silently deleting geometry. A test checks that ten two-face cards survive reduction, baking, and a switch between cached modes.
If reduction removes every face, the pipeline returns an explicit error instead of an empty mesh, and the message suggests raising the target or enabling Pieces.
Results
The Poly Haven payphone used in the promo video is a 48,132-triangle model with 2K PBR textures, CC0 by UM JOORIN on Poly Haven. The output is 798 triangles, a 256 px texture, and 15 colours. The video shows the two side by side, with the camera changing angle through the sequence.
Limits
- Static meshes only. Skeletons, animation, and UDIM textures are out of scope.
- Procedural Blender materials are not evaluated. Only the base colour node is used.
- Of Blender modifiers, only Mirror is applied. Subdivision, Edge Split, Weighted Normal, Triangulate, and Smooth by Angle do not change the result and are skipped. Other modifiers are ignored with a warning, so Array, Boolean, and Geometry Nodes must be applied in Blender first.
- The macOS build is universal. Windows and Linux are labelled beta because they have had less testing.
My contribution
I designed and built the whole tool: the pipeline and its cache keys, the .blend reader, the transparency handling, the preview shader, the export presets for Unity, Unreal, Godot, and Blender, the command-line interface, and the release.
Stack
- Rust, with
eframe/ egui for the interface and the preview ufbxfor FBX and OBJ, andgltffor glTF and GLBmeshopt(meshoptimizer) for decimation,rayonfor parallel work,glamfor the mathsruzstdfor compressed.blendfiles,image,png, andexrfor texturesobjc2and AppKit for the native macOS file dialogs