Skip to content

Instantly share code, notes, and snippets.

@falkreon
Last active January 6, 2024 12:18
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save falkreon/8b873ec6797ffad247375fc73614fd08 to your computer and use it in GitHub Desktop.
Save falkreon/8b873ec6797ffad247375fc73614fd08 to your computer and use it in GitHub Desktop.

*excerpt from slab6.txt in the SLAB6 download found at http://advsys.net/ken/download.htm#slab6 *

VOX file format

Both SLABSPRI&SLAB6(D) support a simpler, uncompressed voxel format using the VOX file extension. Here's some C pseudocode that describes the format:

long xsiz, ysiz, zsiz;          //Variable declarations
char voxel[xsiz][ysiz][zsiz];
char palette[256][3];

fil = open("?.vox",...);
read(fil,&xsiz,4);              //Dimensions of 3-D array of voxels
read(fil,&ysiz,4);
read(fil,&zsiz,4);
read(fil,voxel,xsiz*ysiz*zsiz); //The 3-D array itself!
read(fil,palette,768);          //VGA palette (values range from 0-63)
close(fil);

VOX files use color 255 to define empty space (air). This is a limitation of the VOX format. Fortunately, KVX doesn't have this problem. For interior voxels (ones you can never see), do not use color 255, because it will prevent SLABSPRI/SLAB6(D) from being able to take advantage of back-face culling.


KVX file format

I'm always interested in adding more sample voxels to my collection. Because I'm such a nice guy, I am describing my KVX voxel format here so you can use them in your own programs. The KVX file format was designed to be compact, yet also renderable directly from its format. Storing a byte for every voxel is not efficient for large models, so I use a form of run-length encoding and store only the voxels that are visible - just the surface voxels. The "runs" are stored in the ceiling to floor direction because that is the best axis to use for fast rendering in the Build Engine.

Each KVX file uses this structure for each of its mip-map levels:

   long xsiz, ysiz, zsiz, xpivot, ypivot, zpivot;
   long xoffset[xsiz+1];
   short xyoffset[xsiz][ysiz+1];
   char rawslabdata[?];

The file can be loaded like this:

   if ((fil = open("?.kvx",O_BINARY|O_RDWR,S_IREAD)) == -1) return(0);
   nummipmaplevels = 1;  //nummipmaplevels = 5 for unstripped KVX files
   for(i=0;i<nummipmaplevels;i++)
   {
      read(fil,&numbytes,4);
      read(fil,&xsiz,4);
      read(fil,&ysiz,4);
      read(fil,&zsiz,4);
      read(fil,&xpivot,4);
      read(fil,&ypivot,4);
      read(fil,&zpivot,4);
      read(fil,xoffset,(xsiz+1)*4);
      read(fil,xyoffset,xsiz*(ysiz+1)*2);
      read(fil,voxdata,numbytes-24-(xsiz+1)*4-xsiz*(ysiz+1)*2);
   }
   read(fil,palette,768);

numbytes: Total # of bytes (not including numbytes) in each mip-map level

xsiz, ysiz, zsiz: Dimensions of voxel. (zsiz is height)

xpivot, ypivot, zpivot: Centroid of voxel. For extra precision, this location has been shifted up by 8 bits.

xoffset, xyoffset: For compression purposes, I store the column pointers in a way that offers quick access to the data, but with slightly more overhead in calculating the positions. See example of usage in voxdata. NOTE: xoffset[0] = (xsiz+1)4 + xsiz(ysiz+1)*2 (ALWAYS)

voxdata: stored in sequential format. Here's how you can get pointers to the start and end of any (x, y) column:

      //pointer to start of slabs on column (x, y):
   startptr = &voxdata[xoffset[x] + xyoffset[x][y]];

      //pointer to end of slabs on column (x, y):
   endptr = &voxdata[xoffset[x] + xyoffset[x][y+1]];

Note: endptr is actually the first piece of data in the next column

Once you get these pointers, you can run through all of the "slabs" in the column. Each slab has 3 bytes of header, then an array of colors. Here's the format:

   char slabztop;             //Starting z coordinate of top of slab
   char slabzleng;            //# of bytes in the color array - slab height
   char slabbackfacecullinfo; //Low 6 bits tell which of 6 faces are exposed
   char col[slabzleng];       //The array of colors from top to bottom

palette: The last 768 bytes of the KVX file is a standard 256-color VGA palette. The palette is in (Red:0, Green:1, Blue:2) order and intensities range from 0-63.

Note: To keep this ZIP size small, I have stripped out the lower mip-map levels. KVX files from Shadow Warrior or Blood include this data. To get the palette data, I recommend seeking 768 bytes before the end of the KVX file.


KV6 file format

   //C pseudocode for loader:

   typedef struct { long col; unsigned short z; char vis, dir; } kv6voxtype;
   long xsiz, ysiz, zsiz;
   float xpiv, ypiv, zpiv;
   unsigned long xlen[xsiz];
   unsigned short ylen[xsiz][ysiz];
   long numvoxs;

   FILE *fil = fopen("?.KV6",rb");

   fread(&fileid,4,1,fil); //'Kvxl' (= 0x6c78764b in Little Endian)

       //Voxel grid dimensions
   fread(&xsiz,4,1,fil); fread(&ysiz,4,1,fil); fread(&zsiz,4,1,fil);

      //Pivot point. Floating point format. Voxel units.
   fread(&xpiv,4,1,fil); fread(&ypiv,4,1,fil); fread(&zpiv,4,1,fil);

   fread(&numvoxs,4,1,fil); //Total number of surface voxels
   for(i=0;i<numvoxs;i++) //8 bytes per surface voxel, Z's must be sorted
   {
      red  [i]    = fgetc(fil); //Range: 0..255
      green[i]    = fgetc(fil); //"
      blue [i]    = fgetc(fil); //"
      dummy       = fgetc(fil); //Always 128. Ignore.
      height_low  = fgetc(fil); //Z coordinate of this surface voxel
      height_high = fgetc(fil); //"
      visibility  = fgetc(fil); //Low 6 bits say if neighbor is solid or air
      normalindex = fgetc(fil); //Uses 256-entry lookup table
   }

      //Number of surface voxels present in plane x (extra information)
   for(x=0;x<xsiz;x++) fread(&xlen[x],4,1,fil);

      //Number of surface voxels present in column (x,y)
   for(x=0;x<xsiz;x++) for(y=0;y<ysiz;y++) fread(&ylen[x][y],2,1,fil);

      //Added 06/30/2007: suggested palette (optional)
   fread(&suggpalid,4,1,fil); //'SPal' (= 0x6C615053 in Little Endian)
   fread(suggestedpalette,768,1,fil); //R,G,B,R,G,.. Value range: 0-63

   fclose(fil);

Proper file specs

When you see a data type bigger than 8 bytes, it is almost definitely stored little-endian, so java users should be extra careful to flip those bytes around into big-endian.

VOX

uint32 xsize
uint32 ysize
uint32 zsize

uint8[xsize*ysize*zsize] voxels

uint8[256*3] palette

voxels are byte indices into the palette

When Silverman says char palette[256][3] in the pseudocode, he means that the array goes rgb,rgb,rgb,rgb - C stores arrays in row-major order, so the last subscript varies the fastest. You may want to go in and multiply the color values by 4 (or shift left by 2) to get 0..255 values instead of their 0..63 ranges.

KVX

struct mipLevel {
  uint32 numbytes //number of bytes in this struct not including numbytes
  uint32 xsize
  uint32 ysize
  uint32 zsize  //Z == down
  uint32 xpivot //shift the x/y/z pivot right 8 bits to get the correct rotation point
  uint32 ypivot
  uint32 zpivot
  uint32[xsize+1] xoffset          // xoffset and xyoffset are just to help build engine
  uint16[xsize*(ysize+1)] xyoffset // render, they can be inferred from the voxel data.
  uint8[numbytes - (24) - ((xsize+1)*4) - (xsize*(ysize+1)*2)] rawslabdata
}

The hilarious mess of an array size on rawslabdata is basically saying

All the bytes in the mipLevel struct:

  • Minus the fixed parts of the struct we already read in, from xsize through zpivot (24 bytes)
  • Minus the xoffset block ((xsize+1) * 4 bytes)
  • Minus the xyoffset block (xsize*(ysize+1)*2 bytes)

Overall File Structure

There's no header, magic word, or preamble. All the mipLevels, then 768 bytes of palette. That's all.

Depending on whether it's pulled straight from Build Engine stuff, or whether it's coming "stripped" out of one of Silverman's editors or converters, you get either 5 mipLevels or 1. There are two ways to deal with this:

Silverman's recommendation is just to read in the first mipLevel from the start of the file, then seek to filesize-768 and read in the palette. Since you're unlikely to need the extra mips, and the biggest mip is right at the front, this is a decent strategy.

If you need all the mips, you can use the file size to figure it out. Take the file size, subtract 768 to get rid of the palette size, then subtract another 4 to get rid of the numbytes field of the first mip level. Now read numbytes. If the two numbers match, there's only one mip level. If the file size is greater, you've got five mips instead of one.

The palette is exactly the same as VOX. Shift each channel left by 2 before using!

rawslabdata is awkward. Each 'slab' is a vertical stack of voxels in the following format:

struct Slab {
  uint8 slabztop
  uint8 slabzheight
  uint8 slabBackfaceCullInfo //bottom 6 bits contian hidden surface removal info
  uint8[slabzheight] colors  //palette indices of the voxel colors from top to bottom
}

You can just keep reading slabs until you've read all the bytes in rawslabdata.

KV6

const byte[4] magic = 'Kvxl' // 0x4b76786c big-endian

uint32 xsize
uint32 ysize
uint32 zsize

float32 xpivot //in whole voxels
float32 ypivot
float32 zpivot

uint32 numvoxels

VoxelData[numvoxels] voxels // sizeof(VoxelData) == 8

uint32[xsize] xlen         // cached data for speed in Build engine
uint16[xsize][ysize] ylen  // more cached data for speed in Build engine

const byte[4] = 'SPal' // optional, 0x5350616C big-endian
uint8[768] palette      // optional, again, 0-63, and also not needed because each voxel stores its own color
struct VoxelData {
  uint8 red;   // 0..255
  uint8 green; // 0..255
  uint8 blue;  // 0..255
  uint8 dummy; // always 128, was probably once an alpha value
  
  uint16 height    // little-endian
  uint8 visibility  // low 6 bits are hidden surface removal info
  uint8 normalindex // should probably ignore
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment