Graphics and visuals
This chapter explains all visual formats used, e.g. wall/floor/ceiling textures, user interface images or animations.
Palettes
Ultima Underworld uses several palettes for different purposes. There are palettes with 256 indices, as well as auxiliary meta-palettes that have 16 or 32 entries that map indices to the 256-index palette. As the original games directly load the palettes into the VGA registers, effects as color flashes done with palette rotating are possible.
256 color palettes
In the file "pals.dat" there are stored 8 different palettes. A palette has the following layout:
0000 Int8 red intensity, index 0, range [0..63]
0001 Int8 green intensity, index 0, range [0..63]
0002 Int8 blue intensity, index 0, range [0..63]
0003 Int8 red intensity, index 1, range [0..63]
0004 Int8 green intensity, index 1, range [0..63]
...
02fe Int8 green intensity, index 255, range [0..63]
02ff Int8 blue intensity, index 255, range [0..63]
In each palette there are stored color intensities for 256 colors. All 8 palettes are stored sequentially in the file.
Palette index 0 always means transparent color.
16 color auxiliary palette mappings
In "allpals.dat" there are several auxiliary palettes, used for 4-bit images. All indices use palette #0.
0000 Int8 index to first color
0001 Int8 index to second color
...
000f Int8 index to 16th color
There are 16 values that are indices for palette #0. They build a 16 color palette from selected colors of the palette #0.
In the file "allpals.dat" there are stored 0x1f (=31) such palettes.
The critter animations (explained in chapter 3.6) use 32 color auxiliary palette mappings. They are stored within the animation files.
Palette mappings
Palette mappings for different light/darkness levels are stored in the file "light.dat". The file consists of 16 blocks of palette mappings. Each block contains 256 Int8 values which map colors to their palette indices in the game palette. The first block is the mapping for original colors, and the last one is for "almost black".
The file "mono.dat" contains palette mappings that maps colors to grayscale values. It has the same format as the "light.dat" file and can be interchanged to get a gray underworld look. It is used for the spell "invisibility".
Palette rotation animations
In several places of the game the palette is used to create animated effects, such as the lava and water textures. Here are the palette indices that have to be rotated to produce the animations:
Palette #0: in game graphics
- indices 16 through 23: lava fire effect
- indices 48 through 51: water effect
Palette #2: game start screen
- indices 64 through 127: "Ultima Underworld" logo warping effect
Images
Graphics are stored in "*.gr" files and can be stored with 8-bit or 4-bit indices.
0000 Int8 Graphic file format:
01 .gr
02 .tr
03 .cr [uw2]
04 .sr [uw2]
05 .ar [uw2]
0001 Int16 number of bitmaps
0003 Int32 offset to bitmap #0
0007 Int32 offset to bitmap #1
...
Each bitmap has its own header:
0000 Int8 bitmap type:
04: 8-bit uncompressed
08: 4-bit run-length
0A: 4-bit uncompressed
0001 Int8 width
0002 Int8 height
For the 4-bit formats, there follows another Int8 that selects the auxiliary palette to use (see 2.1).
000n Int16 size of data for the bitmap.
in 4-bit formats, this is the number of 4-bit nibbles, not
bytes.
The file "panels.gr" contains bitmaps that don't have a bitmap header, but immediately starts with image data. The bitmaps are of type 04 and have a width of 83 and a height of 114 pixels.
Uncompressed bitmaps (04: 8-bit, 0A: 4-bit)
All palette indices are stored sequentially, first one line, then the next, and so on. For the 4-bit format, first take the upper nibble, then the lower nibble.
Compressed bitmaps (type 08)
All pixels are run-length encoded. when a new byte has to be retrieved, first take the high nibble, then the low nibble of that byte.
Data consists of repeat and run records. Repeat records let the decoder repeat a single nibble a certain number of times. The run record takes a certain number of next nibbles to be as uncompressed. The two records alternate in the bitmap, starting with a repeat record.
For every record, first there is a count to retrieve. Get a nibble; if it is not 0, it is a count. Otherwise, get two more nibbles, n1 and n2. The count is c = (n1 << nibblesize) | n2.
If the count is still zero, take another three nibbles, and calculate the count:
c = (((n1 << nibblesize) | n2) << nibblesize) | n3;
A count is at most 6 nibbles long.
A run record consists of a count and then follows 'count' nibbles, that are the raw pixel data. A repeat record consists of a count and a single nibble, the nibble is then repeated 'count' times.
As there is no point in repeating a nibble <3 times, there are some special meanings for count:
- Skip this record, the next one is a run record again. may be used at the beginning of a file, when it should start with a run rather than a repeat.
- Multiple repeats. get another count, and process 'count' times a repeat record.
NOTE: There also exists a 5-bit compressed format which is exactly the same as the above except that the word length is 5 bits instead of 4. This is used for critter animation frames in the crit/ folder. The auxiliary palette contains 32 entries and is stored with the animation.
Bitmaps
Bitmaps in Ultima Underworld 1 are stored in "*.byt" files. They just are 320x200 bitmaps using different palettes. Here's a list of all bitmap files:
| File | Description |
|---|---|
| blnkmap.byt | blank map bitmap, palette #1 |
| chargen.byt | character generation bitmap, palette #3 |
| conv.byt | seems to be a conversation screenshot, palette #0 |
| main.byt | main game screen bitmap, palette #0 |
| opscr.byt | opening screen, palette #2 |
| pres1.byt | "origin presents" screen, palette #5 |
| pres2.byt | "a blue sky prod. game" screen, palette #5 |
| win1.byt | winning screen with text, palette #7 |
| win2.byt | blank winning screen for character info, palette #7 |
Ultima Underworld demo contains two separate bitmaps:
| File | Description |
|---|---|
| dmain.byt | main demo screen bitmap, palette #0 |
| presd.byt | "origin and blue sky prod. present..." screen, palette #5 |
Underworld 2 has no "*.byt" files. Instead there is one "byt.ark" that contains all images. Like every other "*.ark" file this starts with some tables followed by the data, in this case the images themselves. There are 11 entries, of which only 9 are valid:
| entry | palette | usage |
|---|---|---|
| 0 | 1 | Map framework - background, crystal, and the like |
| 1 | 0 | Character generation |
| 2 | 0 | Bartering |
| 3 | -unused- | |
| 4 | 0 | HUD - the frame, bottles, scroll |
| 5 | 0 | Underworld 2 Main menu - without menu entries |
| 6 | 5 | Origin presents |
| 7 | 5 | Looking Glass Technologies |
| 8 | 0 | Congratulation screen |
| 9 | 0 | like the above but without the text |
| 10 | -unused- |
Textures
Textures are stored in "*.tr" files, where "fXX.tr" files are floor/ceiling textures, and "wXX.tr" are wall textures. XX describes the width and height resolution of the texture (textures are always square).
0000 Int8 unknown, always seems to be 2
0001 Int8 x and y resolution
0002 Int16 number of textures in file
0004 Int32 offset to texture #0
0008 Int32 offset to texture #1
...
The offset of each texture points to the actual texture palette indices, which are xyres^2 bytes long. Textures always use palette #0.
Texture names are stored in string block 10, where wall textures start at position 0, and ceiling textures start at 510, going backwards. The string at position 511 is reserved for the ceiling.
Fonts
Fonts are stored in "font*.sys" files, and can be non-proportional (chars can have different lengths). The header looks as this:
0000 Int16 unknown, always 1 (might be size of character width field)
0002 Int16 size of single character, in bytes (=charsize)
0004 Int16 width of the blank (space) character, in pixels
0006 Int16 font height in pixels
0008 Int16 width of a character row in bytes
000A Int16 maximum width of a character in pixels (=maxwidth)
Then follow all bitmaps for each character. The number of chars can be determined by (filelen-12) / (charsize+1). Bitmaps are stored as 1-bit patterns, starting at the most significant bit in the current byte. When a new line in character bitmap begins, remaining bits are unused and a new byte in the file is taken.
After 'charsize' number of bytes, there is another Int8 that says the width for the current character in pixels.
Note: at least the fonts "fontbig.sys" and "font5x6p.sys" contain overly large characters, and for these the remaining bits at a line are used. The maxwidth field should be corrected for loading.
Critter animations
Critter animations are stored in the folder "crit". The file "assoc.anm" holds data for each of the 32 animations and for the 64 NPC types. The file starts with 8 bytes for the name of each animation. Shorter strings are padded with zeros. Empty strings denote animations not available (e.g. in the "uw_demo").
Next comes a table of infos for each NPC. The table is 64 entries (0x0080) long:
0000 Int8 anim
0001 Int8 auxpal
The "anim" field specifies which one of the 32 animations to take for a given NPC number (NPC object ID - 0x0040). A value of 0xff indicates that no animation is available.
The "auxpal" value describes which auxiliary palette to use. There are several critters that share the same animations but use different palettes, e.g. the bat and the vampire bat uses the same anim value, but different auxiliary palettes.
[uw2] The critter associations are stored in the file "as.an"; it doesn't contain the critter names, only the 64 entries for "anim" and "auxpal" are stored.
Animation for each critter is stored in a file named "CrXXpage.nYY", where
- XX = animation number (=anim), YY = page number
- XX and YY are octal numbers
There may be more than one page for each animation file.
The file starts with a header:
pos length desc.
0000 Int8 anim slot base
0001 Int8 number of anim slots (=nslot)
0002 nslot*Int8 list of segment indices
After this, a list of segment follows which contains up to 8 frame indices for every segment.
nslot+2 Int8 number of anim segments (=nsegs)
8*nsegs anim frame indices
Then the auxiliary palettes follow:
Int8 number of aux palettes (=npals)
npals*32 allaux palette indices in blocks of 32
Next is a list of all frames and their offsets into the file:
Int8 number of frame offsets (=noffsets)
Int8 compression type? (always 06)
noffsets*Int16 absolute offsets to frame headers
Each animation is stored in a segment which can contain a number of frames (stored in the "animation frame indices"). Each list is padded with 0xFF entries.
frame header
0000 Int8 width
0001 Int8 height
0002 Int8 hotspot x
0003 Int8 hotspot y
0004 Int8 compression type; (06: 5-bit word size)
0005 Int16 data length in number of words
0007 start of rle-encoded image data (see 2.3)
The hotspot coordinates are to "pin" the image at a specific position. The hotspot coordinates in the image should always be on the same place when rendered. The compression type can be 06, which is 5-bit run-length encoding, or 08, which is 4-bit run-length encoding (see 2.3. for more).
The slot lists group together segments of animations for various actions. Here's a list of slots and their actions:
| slot | action |
|---|---|
| 00 | combat idle |
| 01 | attack, bash (?) |
| 02 | attack, slash (?) |
| 03 | attack, thrust |
| 05 | second weapon attack |
| 07 | walking / running towards player |
| 0c | death |
| 0d | ?? |
| 20 | idle, facing away from player (180 degrees) |
| 21 | idle, 135 deg. |
| 22 | idle, angle 90 deg. |
| 23 | idle, angle 45 deg. |
| 24 | idle, facing towards player, 0 deg. |
| 25 | idle, angle -45 deg. |
| 26 | idle, angle -90 deg. |
| 27 | idle, angle -135 deg. |
Segment indices at 80-87 are the same as above, except that these are the walking animations. For the animations used in the Ethereal Void (level 9) the slot list is somewhat different.
Here is a list of animation files and their contents:
| file | assoc name | auxpals | used in | |
|---|---|---|---|---|
| cr00 | x | gngob32 |
4 | green goblin |
| cr01 | skela |
1 | skeleton | |
| cr02 | lizman |
3 | green, red and gray lizardman | |
| cr03 | x | bat |
3 | cave bat, vampire bat |
| cr04 | wiza |
5 | yellow male mage | |
| cr05 | x | spider |
3 | giant, wolf, dread spider |
| cr06 | gazer |
1 | gazer | |
| cr07 | troll |
3 | troll, fereal troll, great troll | |
| cr10 | femwiz |
4 | female mage | |
| cr11 | x | slug |
2 | flesh slug, acid slug |
| cr12 | fire |
2 | fire elemental | |
| cr13 | ghoul |
3 | ghoul, dark ghoul | |
| cr14 | demon |
1 | Slasher of the Veils | |
| cr15 | x | ghost |
4 | ghost, dire ghost |
| cr16 | x | graygob |
2 | gray goblin |
| cr17 | reaper |
1 | reaper | |
| cr20 | x | rat |
2 | giant rat |
| cr21 | femfite |
3 | female fighter | |
| cr22 | x | imp |
2 | imp, mongbat |
| cr23 | golem |
3 | earth, stone and metal golem | |
| cr24 | x | hedless |
1 | headless |
| cr25 | wizb |
3 | blue female mage | |
| cr26 | x | rotgrub |
2 | green rotworm, bloodworm |
| cr27 | wisp |
1 | wisp | |
| cr30 | batskull |
2 | bat, teeth, vortex, hound (level 9) | |
| cr31 | x | Lurk |
2 | lurker, deep lurker |
| cr32 | x | fight32 |
4 | male fighter, outcast, adventurer |
| cr33 | dwarf32 |
3 | mountainman | |
| cr34 | shadow |
2 | shadow beast | |
| cr35 | tybal |
1 | tyball | |
| cr36 | eye |
1 | eye, skull (level 9) | |
| cr37 | litening |
1 | lightning, fish (level 9) |
The animations marked with x are available in the uw_demo, too.
Cutscene animations
Cutscene animations are stored in folder "cuts". Text strings for cutscenes can be found in string blocks 0c00 through 0c21. Chapter 2.2.1 lists all animations available in Ultima Underworld 1.
The cutscene files are done with Amiga's DeluxePaint Animator (file extension *.anm). The complete description can be found in the zip file "anmformt.zip" in the "misc" folder. Here's a short overview for usage in Ultima Underworld 1:
The files, "large page files", consist of a header, followed by one or more large pages, where each page can store one or more animation frames. A large page is always 64k big, except for the last page (which is truncated at the end of usable data).
The file starts with a "large page file header":
0000 Int32 file ID, always contains "LPF "
...
0006 Int16 number of large pages in the file
0008 Int32 number of records in the file
...
0010 Int32 content type, always contains "ANIM"
0014 Int16 width in pixels
0016 Int16 height in pixels
The whole header is 128 bytes long. After the header color cycling info follows (which also is 128 bytes long), which is not used in uw1. Then comes the color palette:
0000 Int8 intensity for blue, ranges from 0..255
0001 Int8 intensity for green
0002 Int8 intensity for red
0003 Int8 padding byte
... repeated for all 256 color indices
After the palette an array with 256 "large page descriptors" follow:
0000 Int16 number of first record in the large page
0002 Int16 number of records in the large page
0004 Int16 total number of bytes, excluding header
Unused descriptors contain no information. After the array, the large pages start. A "large page" has the following layout:
0000 lpdesc large page descriptor
0006 Int16 empty
0008 Int16 length of first record
000A Int16 length of second record
...
The large page descriptor is repeated for the current large page. A record contains a frame (which may depend on the previous frame). The start can be calculated by summing up the length of the previous records. A "record" has the following structure:
0000 Int8 unknown
0001 Int8 flag
0002 Int16 extra offset, when flag != 0
0004 start of compressed data
The extra offset must be even (when odd, add an extra 1).
The compression scheme is a variation of run-length encoding, with some extras. There are "dump", "run" and "skip" records. "dump" records just copy the next bytes to the output buffer. "run" records get the next byte and repeat them according to the count. A "skip" record skips pixels in the output buffer (it is assumed that the previous decoded image is still in the buffer).
First, read a signed Int8. If it is positive, dump that many bytes. If it is 0, the next two Int8's are the count and the pixel byte for a "run" record. For negative values, remove the sign bit. If the resulting byte is != 0, skip that many bytes in the output, else a "long" operation is started.
For the long operation, retrieve the next two Int8's, treating as a little endian signed Int16. If the value is 0, the decoding ends. If the value is > 0, skip that many bytes in the output. For values < 0, remove the sign bit. If the resulting value is >= 0x4000, we have a "run" record with count = value & 0x3fff and the next Int8 as the pixel index. If the value is <= 0x4000, we have a long "dump" record.
Here is some meta-C code to describe the decoding:
while(true)
{
Int8 cnt = get_next_src8();
if (cnt>0) dump_pixels(cnt);
if (cnt==0) run_pixels(get_next_src8(),get_next_src8());
if (cnt<0)
{
cnt &= 0x7f;
if (cnt!=0) skip_pixels(cnt);
else
{
// we have a "long" operation
Int8 cnt2 = get_next_src16();
if (cnt2>0)
skip_pixels(cnt2);
else
if (cnt2==0)
break;
else
{
cnt2 &= 0x7fff;
if (cnt2>=0x4000)
run_pixels(cnt2-0x4000,get_next_src());
else
dump_pixels(cnt2);
}
}
}
}
Weapon animations
Attack animations:
The file weapons.gr contains animation frames for attacks with the various weapon types.
The file contains 224 image frames, split into 112 for right-handed attacks and 112 for left handed.
For each weapon, including the fist, 3 animations are stored: slash, stab and hack - even if identical. After these three animations one "ready" frame is stored.
Each animation has 4 "power-up" frames and 5 "attack frames", some of which can be black (a small 2x2 image) Therefore, each weapon type has 3*9+1 = 28 frames, 4 attack types gives 112 images.
TODO: mace seems not to fit this exactly!!!
Weapons.dat:
This file stores 8-bit coordinates for the frames of the various attack animations. For each attack type first 28 x-coordinates are stored, then 28 y-coordinates. There are 8 such sets of coordinates:
- right hand sword
- right hand axe
- right hand mace
- right hand fist
- left hand sword
- left hand axe
- left hand mace
- left hand fist
Coordinates pin-point the upper-left corner of the attack frame and are relative to the lower left corner of the 3d-view area.
Weapons.cm:
This file stores two aux 16-color palettes for the attack animation frames. These are needed to tint the weapon attack frames according to the skin color of the selected character. I haven't checked, but probably one of these match the "standard" aux pallette used for the .gr files...