Extract the pre-rendered room backgrounds from BioForge (Origin Systems, 1995, DOS) .CAM
files to PNG.
BioForge draws its rooms as fixed pre-rendered 320x200 8-bit images with real-time 3D characters
composited on top. Each room lives in ROOMS/*.CAM on the game disc. This tool rebuilds the full
background image from those files. The format was worked out by reverse engineering; see
Format.
This repository contains no game data. You need your own copy of BioForge (for example the 1995 CD-ROM release). Extract the
.CAMfiles from the disc yourself. Do not commit or redistribute the game's files or images extracted from them; they are the property of Electronic Arts. This project is not affiliated with or endorsed by EA or Origin.
Requires Python 3.8+ and Pillow (pip install pillow).
python cam_decode.py ROOMS/CE31.CAM # writes CE31.png next to the input
python cam_decode.py ROOMS/CE31.CAM -o room.png
python cam_decode.py ROOMS/*.CAM -d out/ # every room into out/
python cam_decode.py ROOMS/AIR1.CAM --frames frames/ # also dump the animation frames
python cam_decode.py ROOMS/X.CAM --palette dac.pal # use your own 768-byte 6-bit paletteThe output is an RGBA PNG. Pixels that could not be recovered are fully transparent, and the status line says how many there were. The palette is read from the file itself.
As a library:
import cam_decode
res = cam_decode.decode_file('CE31.CAM')
# res['pixels']: 200 rows of 320 palette indices (None = unrecovered); res['palette']: 256 (r, g, b)Run the tests (they use a synthetic file, no game data needed): python -m unittest discover -s tests
Tested against all 229 ROOMS/*.CAM files of the retail CD:
| Result | Files |
|---|---|
| Complete 320x200 background, no unrecovered pixels | 195 |
| Complete apart from a few pixels (2-757) that live in an animation layer | 14 |
No room background in the file (character / effect / dialogue-portrait files, DEMO1) |
20 |
Accuracy: CE31 is verified pixel-exact against a frame captured from the running game (apart
from the composited 3D characters). It has no sprites, so it only tests the base image and the
token grammar. For the other rooms I checked the results by eye; the images are seamless, which
strongly suggests the layout below is right, but only one room was checked against ground truth.
Some details are heuristics, see Known gaps.
All integers are little-endian. A .CAM file starts with the text (C) 1994 Origin Systems Inc.
and a header with these fields (offsets are in the file):
0x80 uint32 palette offset (always marker #1 - 768)
0x84 uint32 palette size, 0x300
0x9D entries chunk directory, 21 bytes each: name[8], 0, depth u32, offset u32, size u32
... palette: 256 x RGB, 6-bit VGA values (0-63)
40 01 C8 00 320 x 200 as two uint16 (marker #1)
... 10 bytes
40 01 C8 00 again (marker #2, "h")
uint16 x 200 row table at h+8, then the base image data
The directory is cut short by the palette, so its last entry is a stub without offset/size. Its
chunks are contiguous and the last one runs to the end of the file. depth is the distance
used to sort the sprite behind or in front of the 3D characters (88888 / 99999 mean "always
background").
Row tables. Entry i holds the distance from its own position to the first byte of row
i, so row_start[i] = table + 2*i + uint16[table + 2*i]. Identical rows share data. A row
ends where the next stored row begins, except that rows can also share tails, so the base
image is read once bounded and once running on.
Base image rows are token streams:
| Bytes | Meaning |
|---|---|
vv (non-zero) |
one pixel of colour vv |
00 nn vv, nn odd |
run of nn >> 1 pixels of colour vv |
00 nn + k bytes, nn even, k = nn >> 1 |
k literal pixels follow; they may contain zeros |
00 81 vv |
contributes no pixels (rare; purpose unknown) |
A base row is not always 320 pixels long. It only stores the pixels that no sprite covers.
Sprites. A directory chunk that starts w h 01 00 0c 00 00 00 <size> w h x y is a sprite:
a cut-out of the picture (a doorway interior, a pillar, ...) with its own row table at chunk+22.
(-x, -y) is its top-left corner on the screen. Its rows are lists of spans:
[skip][nn][data] [skip][nn][data] ... [right margin]
skip transparent columns, then nn as above (odd: run of nn>>1 copies of the next byte;
even: nn>>1 literal bytes). The single closing byte is the right margin, so a row always adds up
to exactly w columns; a row of one byte is empty.
Assembling a room. Draw every sprite's spans at their exact columns, farthest first. Then fill the columns nobody covers from the base row, left to right. The base row holds about one extra pixel at each edge of every covered stretch (an antialiasing fringe), which is skipped.
Short base rows. In some rows the base is shorter than the sprites leave room for: the missing pixels belong to an animated element stored in an animation chunk. The decoder inserts a gap of the right size where the row lines up best with the row above (the tail of the row would otherwise shift left) and copies the row above into it, so the picture stays aligned and only a few pixels are approximated. Rows that share a tail with another row are read past their nominal end.
Animation chunks start with the 320x200 marker instead: a list of frames
(uint32 offset, uint16 size per frame), each flag w h x y, a row table and span rows. They are
door openings, blinking lights and the like, and are only dumped with --frames, not composited.
The disc's room files were unreadable to every generic tool, so the format was recovered from
the running game. DOSBox-X's debugger was driven over its TCP debug protocol to dump the VGA
frame buffer of a known room, giving a known plaintext to compare against the bytes of the
matching .CAM. That fixed the token grammar. Everything about sprites came from noticing
that base rows are shorter than 320 pixels by exactly the number of pixels the sprites carry,
and then from checking candidate layouts by looking at the seams.
- The number of fringe pixels skipped per covered stretch is inferred from the row lengths, and which edge they belong to is a guess. It only matters for a pixel or two at sprite edges.
- The 20 non-room files have no background at all; their pictures are animation frames
(
--frames). 00 81 vvtokens are ignored.- Pixels that belong to animation frames are approximated from the row above (a few per row in about a dozen rooms).
- Depth-sorting between overlapping sprites is by the directory
depth(nearest wins). - The data is 8-bit indexed. Some rooms are drawn with a palette that the game alters at run time (fades, lighting); this tool uses the palette stored in the file.
MIT, see LICENSE. This applies to the code and documentation only, not to any game content.