Skip to content

Qubicle Binary Tree File Specifications

Qubicle Binary Tree (QBT) is the successor of the widespread voxel exchange format Qubicle Binary.

Data Structure

  • Magic (4 bytes), must be 0x32204251 = "QB 2"
  • Version.Major (1 byte), currently = 1
  • Version.Minor (1 byte), currently = 0
  • Global Scale (3 * 4 bytes, float), default 1, 1, 1, can be used to scale voxels globally

Color Map

  • SectionCaption (8 bytes), = "COLORMAP"
  • ColorCount (4 bytes, uint), if this value is 0 then no color map is used
  • Colors (ColorCount * 4 bytes), RGBA

Data Tree

  • SectionCaption (8 bytes), = "DATATREE"
  • Root, can either be Model Node, Compound Node or Matrix Node

Model Node

  • TypeID (4 bytes, uint), = 1
  • DataSize (4 bytes, uint), number of bytes used for this node and all child nodes (excluding TypeID and DataSize of this node)
  • ChildCount (4 bytes, uint), number of child nodes
  • Children ChildCount nodes of type Matrix Node or Compound Node

Matrix Node

  • TypeID (4 bytes, uint) = 0
  • DataSize (4 bytes, uint), number of bytes used for this node (excluding TypeID and DataSize)
  • NameLength (4 bytes)
  • Name (NameLength bytes, char)
  • Position X, Y, Z (3 * 4 bytes, int), position relative to parent node
  • LocalScale X, Y, Z (3 * 4 bytes, uint)
  • Pivot X, Y, Z (3 * 4 bytes, float)
  • Size X, Y, Z (3 * 4 bytes, uint)
  • VoxelDataSize (4 bytes, uint)
  • VoxelData (VoxelDataSize bytes), zlib compressed voxel data

Compound Node

  • TypeID (4 bytes, uint = 2
  • DataSize (4 bytes, uint, number of bytes used for this node and all child nodes (excluding TypeID and DataSize of this node)
  • NameLength (4 bytes)
  • Name (NameLength bytes, char)
  • Position X, Y, Z (3 * 4 bytes, int), position relative to parent node
  • LocalScale X, Y, Z (3 * 4 bytes, uint)
  • Pivot X, Y, Z (3 * 4 bytes, float)
  • Size X, Y, Z (3 * 4 bytes, uint)
  • CompoundVoxelDataSize (4 bytes, uint)
  • CompoundVoxelData (VoxelDataSize bytes), zlib compressed voxel data
  • ChildCount (4 bytes, uint), number of child nodes
  • Children ChildCount nodes of type Matrix Node or Compound Node

Voxel Data

Voxel data is stored in a 3D grid. The data is compressed using zlib and stored in X, Y, Z with Y running fastest and X running slowest. Each voxel uses 4 bytes: RGBM. RGB stores true color information and M the visibility Mask.

If a color map is included then the R byte references to a color of the color map. In this case the G and B bytes may contain additional secondary data references.

The M byte is used to store visibility of the 6 faces of a voxel and whether as voxel is solid or air. If M is bigger than 0 then the voxel is solid. Even when a voxel is solid is may not be needed to be rendered because it is a core voxel that is surrounded by 6 other voxels and thus invisible. If M = 1 then the voxel is a core voxel.

Parsing

The following pseudo code will help you to write your own parser for a QBT file.

js
function LoadQB2(stream)
{
	// Load Header
	magic = stream.readInt;
	major = stream.readByte;
	minor = stream.readByte;

	if  (magic != 0x32204251)
		return false;

	globalScale.x = stream.readFloat;
	globalScale.y = stream.readFloat;
	globalScale.z = stream.readFloat;

	// Load Color Map
	stream.readString(8); // = COLORMAP
	colorCount = stream.readUInt;
	for (i = 0; i < colorCount; i++)
		color[i] = stream.readRGBA;

	// Load Data Tree
	stream.readString(8); // = DATATREE
	LoadNode(stream);
}

function LoadNode(stream)
{
	nodeTypeID = stream.readUInt;
	dataSize = stream.readUInt;

	switch (nodeTypeID)
		case 0:
			loadMatrix(stream);
			break;
		case 1:
			loadModel(stream);
			break;
		case 2:
			loadCompound(stream);
			break;
		else
			stream.seek(dataSize) // skip node if unknown
}

function loadModel(stream)
{
	childCount = stream.loadUInt;
	for (i = 0; i < childCount; i++)
		loadNode(stream);
}

function loadMatrix(stream)
{
	nameLength = stream.readInt;
	name = stream.readString(nameLength);
	position.x = stream.readInt;
	position.y = stream.readInt;
	position.z = stream.readInt;
	localScale.x = stream.readInt;
	localScale.y = stream.readInt;
	localScale.z = stream.readInt;
	pivot.x = stream.readFloat;
	pivot.y = stream.readFloat;
	pivot.z = stream.readFloat;
	size.x = stream.readUInt;
	size.y = stream.readUInt;
	size.z = stream.readUInt;
	decompressStream = new zlibDecompressStream(stream);

	for (x = 0; x < size.x; x++)
		for (z = 0; z < size.z; z++)
			for (y = 0; y < size.y; y++)
				voxelGrid[x,y,z] = decompressStream.ReadBuffer(4);
}

function loadCompound(stream)
{
	nameLength = stream.readInt;
	name = stream.readString(nameLength);
	position.x = stream.readInt;
	position.y = stream.readInt;
	position.z = stream.readInt;
	localScale.x = stream.readInt;
	localScale.y = stream.readInt;
	localScale.z = stream.readInt;
	pivot.x = stream.readFloat;
	pivot.y = stream.readFloat;
	pivot.z = stream.readFloat;
	size.x = stream.readUInt;
	size.y = stream.readUInt;
	size.z = stream.readUInt;

	decompressStream = new zlibDecompressStream(stream);
	for (x = 0; x < size.x; x++)
		for (z = 0; z < size.z; z++)
			for (y = 0; y < size.y; y++)
				voxelGrid[x,y,z] = decompressStream.ReadBuffer(4);

	childCount = stream.loadUInt;
	if (mergeCompounds) // if you don't need the datatree you can skip child nodes
	{
		for (i = 0; i < childCount; i++)
			skipNode(stream);
	}
	else
	{
		for (i = 0; i < childCount; i++)
			LoadNode(stream);
	}
}

function skipNode(stream)
{
	stream.readInt; // node type, can be ignored
	dataSize = stream.readUInt;
	stream.seek(dataSize);
}