Qubicle Binary Tree File Specifications
Qubicle Binary Tree (QBT) is the successor of the widespread voxel exchange format Qubicle Binary.
Table of contents
Data Structure
Header
Magic
(4 bytes), must be 0x32204251 = “QB 2”Version.Major
(1 byte), currently = 1Version.Minor
(1 byte), currently = 0Global 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 usedColors
(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), = 1DataSize
(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 nodesChildren
ChildCount nodes of type Matrix Node or Compound Node
Matrix Node
TypeID
(4 bytes, uint) = 0DataSize
(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 nodeLocalScale 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 = 2DataSize
(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 nodeLocalScale 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 dataChildCount
(4 bytes, uint), number of child nodesChildren
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);
}