Link Search Menu Expand Document

Qubicle Binary Tree File Specifications

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


Table of contents

  1. Data Structure
    1. Header
    2. Color Map
    3. Data Tree
      1. Model Node
      2. Matrix Node
      3. Compound Node
    4. Voxel Data
  2. Parsing

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.

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);
}