Switch to 16-bit truecolor framebuffer and PNG assets

pbio moves to git.bnd.wtf/irrlicht/pbio.git branch truecolor16: the
framebuffer is now RGB565 u16, no palette. Assets are loaded from PNG via
the png crate and quantized to RGB565 on load; transparency comes from the
alpha channel as a per-tile opacity mask, so any color (black included) can
be opaque. The TGA loader and the palette-order fragility are gone.

overworld.png is the unmodified Mini-Medieval Overworld sheet (same 24-column
layout as the old TGA, so map tile ids are unchanged); entities.png holds the
player sprite extracted from the old entities.tga.

Adds an ignored snapshot test that renders the first frame headlessly to a
PNG for eyeballing the renderer without a window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irrlicht
2026-09-18 23:17:41 +02:00
co-authored by Claude Opus 5
parent 64a6e1d42d
commit 5a055c048d
14 changed files with 212 additions and 96 deletions
+19 -12
View File
@@ -2,15 +2,22 @@
## Current state
TGA loading is implemented in `game/src/assets.rs`. `Image::from_tga(path)` reads the file,
parses the 18-byte header, skips the colormap block, and returns an `Image` — supporting
type 1 (uncompressed) and type 9 (RLE) via the private `decode_rle()` function in the same
module. `game.rs` uses it directly: `Image::from_tga("assets/tilesets/overworld.tga")`.
PNG loading is implemented in `game/src/assets.rs`. `Image::from_png(path)` decodes via the
`png` crate (any color type, expanded to 8-bit RGBA) and quantizes on load to the
framebuffer's RGB565 format with `pbio::rgb565`; alpha ≥ 128 counts as opaque. The result
is an `Image { width, height, pixels: Vec<u16>, opaque: Vec<bool> }`. Artists work in
truecolor; there is no palette an asset could be "in the wrong order" for. `game.rs` uses it
directly: `Image::from_png("assets/tilesets/overworld.png")`.
`Image` also provides `to_tileset() -> Vec<[u8; 64]>`, which splits the image into 8×8
tiles row-major. Every tile is emitted; tile ID is the flat row-major index into the Vec,
so tiles are never skipped or reordered. (Transparency is a per-pixel concern handled at
blit time by `blit_tile`, not by the splitter.)
`Image` also provides `to_tileset() -> Vec<Tile>`, which splits the image into 8×8 tiles
row-major. A `Tile` is 64 RGB565 pixels plus a `u64` opacity bitmask. Every tile is
emitted; tile ID is the flat row-major index into the Vec, so tiles are never skipped or
reordered. `overworld.png` is the unmodified Mini-Medieval `Overworld.png` sheet (24
columns), so Tiled tile ids line up with the Vec index.
History: the first iteration used 8-bit indexed TGAs on a palette framebuffer (RGB332,
`pbio` master branch). The colormap-ordering fragility and the index-0-is-transparent
convention are gone with the switch to `pbio`'s `truecolor16` branch.
`AssetStore` (a named registry) is **not yet implemented**. Assets are currently loaded
inline in `Game::start()`.
@@ -22,12 +29,12 @@ files directly at the call site.
## Design notes
- `Image` struct: `width: u32`, `height: u32`, `pixels: Vec<u8>` — done.
- `Image` struct: `width: u32`, `height: u32`, `pixels: Vec<u16>`, `opaque: Vec<bool>` — done.
- A simple `AssetStore` could be a `HashMap<&'static str, Image>` loaded at startup.
- No streaming needed — the whole game is small enough to load everything upfront in `start()`.
- Palette index 0 is transparent when blitting sprites. This convention is enforced by
`blit_tile()` in `pixelhelper.rs`; the loader itself does not need to handle it.
- Pixel assets are stored as flat `Vec<u8>` of palette indices, row-major, width × height bytes.
- Transparency is the tile's mask bit, checked by `blit_tile()` in `pixelhelper.rs`. Any
color — black included — can be an opaque pixel.
- Pixel assets are stored as flat `Vec<u16>` of RGB565 values, row-major, width × height.
- Future: consider embedding assets with `include_bytes!` to produce a single binary.
## Open questions