
Laser cutting Doom WAD maps by fmeyer
I’ve heard a lot about classic Doom’s data format and decided to write some Rust code to extract its maps and convert that to vector graphics I could laser cut.
I’ll go through the process, from the data extraction to the geometry reconstruction to outputting laser-cuttable SVGs, including the geometrical dead end I enthusiastically ran to when I tried to preview the result using bevy.
DOOM’s data was designed with modding as a goal : the entire game is an executable and a .WAD
data pack. The shareware/demo version shipped with DOOM1.WAD, which is still available freely.
Doom WAD format, illustrated with MotionCanvas
The WAD format is well documented – see the Unofficial DOOM spec or the ZDoom wiki. The gist of it is :
- the WAD file starts with a header follow by the actual data, ends with directory entries describing that data
- the header contains a pointer to the first directory entry and the entry count
- each directory entry contains a pointer to the data and its size
I’ll skip some (fascinating!) details, as the DOOM game engine black book of the wonderful Fabien Sanglard already covers all of that.
Those data items are called “lumps” in doom parlance. Some contains map geometry, others textures, sounds, text, … everything needed for a game.
A map is described by multiple lumps
VERTEXES
is a list of world positionsLINEDEFS
describes lines joining two segments and references one SIDEDEF per line “side”SIDEDEFS
are “walls”, textures for a line, and belong to a SECTORSECTORS
are polygons with a floor and ceiling height
The map data also includes a BSP tree whose leaves are subsectors, sectors split to be convex polygons, but also texture definitions compositing multiple sprites and much more.
Implementation
I used nom, a rust parser combinators library that can parse text and binary formats. Here is a typical usage: parsing THINGS
, the map items/power ups:
pub struct Thing {
pub pos_x: i16,
pub pos_y: i16,
pub angle: i16,
pub thing_type: u16,
pub flags: ThingFlags,
}
impl Lump for Thing {
// used to determine how many items in a lump
const SIZE: usize = 10;
}
pub fn parse_thing(input: &[u8]) -> IResult<&[u8], Thing> {
map(
// parse 5 unsigned shorts
tuple((le_i16, le_i16, le_i16, le_u16, le_u16)),
// map them to the struct fields
|(pos_x, pos_y, angle, thing_type, flags)| Thing {
pos_x,
pos_y,
angle,
thing_type,
flags: ThingFlags::from_bits(flags).unwrap(),
},
)(input)
}
I have a nice parse_lump
function in a WAD struct, taking the lump name and the parsing function :
let things: Vec<Thing> = wad.parse_lump("THINGS", thing::parse_thing);
Getting line segments is relatively easy (group linedefs
by sidedef.sector
). However, I also intend to group sectors by similar floor heights to reduce the layer count and I need to mark edges as cut or engrave lines if they are polygon borders or internal lines.
The parsed WAD data is an unordered collection of edges. We have a guarantee that edges won’t cross. Merging polygons is just a set union, and removing internal lines is a matter of finding duplicate edges in a polygon.
Strictly speaking, this is enough to produce a SVG that can be laser cut.
It is now time to separate sectors into layers, according to their floor height. This is done by providing an array of heights and grouping sectors by the smallest upper bound, eg. I used [-70, -24, 0, 40]
for E1M1.
It could be automated, but I went for artistic control. Sectors could be sorted by ascending height, then split into groups of similar size. That group size could be in function of sector counts or areas. Another criteria could be the sector properties – if a sector is flagged as causing damage (aci