Skip to content

Instantly share code, notes, and snippets.

@JohnEarnest
Created January 6, 2016 03:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JohnEarnest/4cdf1f1c425bdcadec37 to your computer and use it in GitHub Desktop.
Save JohnEarnest/4cdf1f1c425bdcadec37 to your computer and use it in GitHub Desktop.
Quake WAD3 texture unpacker
import java.io.*;
import java.util.*;
import java.awt.image.*;
import javax.imageio.*;
// a very basic texture unpacker for the Quake WAD3 file format
// see http://hlbsp.sourceforge.net/index.php?content=waddef
public class WAD3 {
static int index = 0;
static byte[] data;
public static void main(String[] args) throws IOException {
// load the entire file into memory for convenient seeking:
File file = new File(args[0]);
DataInputStream in = new DataInputStream(new FileInputStream(file));
data = new byte[(int)file.length()];
in.readFully(data);
// read and display the header:
String m = readString(4);
if (m.equals("WAD3") || m.equals("WAD2")) { System.out.println(m+" format"); }
else {
System.err.println("Not a WAD file.");
System.exit(1);
}
int dirEntries = read32();
int dirOffset = read32();
System.out.println("File Size: "+data.length);
System.out.println("Directory Entries: "+dirEntries);
System.out.println("Directory Offset: "+dirOffset);
// load the directory listing
index = dirOffset;
List<DirectoryEntry> dir = new ArrayList<DirectoryEntry>();
for(int z=0; z<dirEntries; z++) {
DirectoryEntry d = new DirectoryEntry();
dir.add(d);
}
// unpack each texture
System.out.println("-----------------------------");
File basedir = new File("unpacked_"+args[0].split("\\.")[0]);
basedir.mkdir();
System.out.println("Unpacking into "+basedir+"/...");
for(DirectoryEntry d : dir) {
if (d.type != 67) { continue; } // skip non-textures
unpackTexture(d, new File(basedir+"/"+d.name+".png"));
}
System.out.println("...Done.");
}
static void unpackTexture(DirectoryEntry d, File destination) throws IOException {
index = d.offset;
readString(16); // name; we already have this.
int w = read32();
int h = read32();
int[] offsets = new int[]{read32(), read32(), read32(), read32()};
// texture data is packed as the first mipmap,
// at a location relative to the start of this record:
int[] texture = new int[w * h];
index = d.offset + offsets[0];
for(int z=0; z<w*h; z++) { texture[z] = read8(); }
// color lookup table is just past the end of the fourth mipmap.
// each mipmap divides the w/h by 2. We also skip 2 dummy bytes:
index = d.offset + offsets[3] + ((w/8) * (h/8)) + 2;
int[] clut = new int[256];
for(int z=0; z<256; z++) {
int cr = read8();
int cg = read8();
int cb = read8();
clut[z] = (0xFF << 24) | (cr << 16) | (cg << 8) | (cb);
}
// index through the CLUT and write the image data:
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
for(int x = 0; x < w; x++) {
for(int y = 0; y < h; y++) {
img.setRGB(x, y, clut[texture[x+(y*w)]]);
}
}
System.out.println(destination);
ImageIO.write(img, "PNG", destination);
}
static String readString(int len) {
String r = "";
for(int z=0; z<len; z++) { r += (char)read8(); }
return r.trim(); // trim will remove any null bytes too.
}
static int read8() {
return data[index++] & 0xFF;
}
static int read32() {
return read8() | (read8() << 8) | (read8() << 16) | (read8() << 24);
}
private static class DirectoryEntry {
final int offset;
final int size;
final byte type;
String name;
DirectoryEntry() {
offset = read32();
size = read32();
read32(); // uncompressed size; don't care
type = (byte)read8();
boolean compression = read8() != 0;
read8(); // dummy byte
read8(); // dummy byte
name = readString(16);
if (compression) {
System.err.println("Compressed textures are not currently supported.");
System.exit(1);
}
}
public String toString() {
return String.format("%d\t%s (%d bytes)", offset, name, size);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment