Name | Type | Size in bytes | Description |
---|---|---|---|
magic | string | 4 | Magic sum of the file(always QBCL) |
programVersion | uint | 4 | Version of Qubicle that wrote this file (major, minor, release, build) |
fileVersion | uint | 4 | Version of the file format(currently 2.0) |
Name | Type | Size in bytes | Description |
---|---|---|---|
width | uint | 4 | Width of the thumbnail |
height | uint | 4 | Height of the thumbnail |
thumbnail | array | width * height * 4 | Uncompressed BGRA pixel values |
Name | Type | Size in bytes | Description |
---|---|---|---|
valueSize | uint | 4 | Size of the metadata value (0 means not available ) |
metadataValue | string | valueSize | Metadata value |
Order of the metadata value is like in the GUI of Qubicle.
- Title
- Description
- Tags
- Author
- Company
- Website
- Copyright
16 bytes. I don't know exactly what this value means.
The tree starts always with a model node.
Name | Type | Size in bytes | Description |
---|---|---|---|
type | int | 4 | Type of the node (1 for model) |
unknown | int | 4 | It is presented in all nodes. I don't know exactly what this value means. (Always 1) |
nameLen | uint | 4 | Size of the name of this node. |
name | string | nameLen | Name of this node |
unknown | bytes | 3 | It is presented in all nodes. I don't know exactly what this value means. (Always 0x1 0x1 0x0) |
unknown | bytes | 36 | I don't know exactly what this value means. Maybe its a 3x3 matrix. (Always the same (0x01 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x01 Rest 0x00)) |
childCount | uint | 4 | The count of the child nodes. |
Name | Type | Size in bytes | Description |
---|---|---|---|
type | int | 4 | Type of the node (0 for matrix) |
unknown | int | 4 | It is presented in all nodes. I don't know exactly what this value means. |
nameLen | uint | 4 | Size of the name of this node. |
name | string | nameLen | Name of this node |
unknown | bytes | 3 | It is presented in all nodes. I don't know exactly what this value means. |
size | veci3 | 12 | The size of the model |
position | veci3 | 12 | The position in 3D space |
pivot | vecf3 | 12 | The pivot position |
compressedDataSize | uint | 4 | The size of the following compressed data |
compressedData | bytes | compressedDataSize | Zlib compressed voxel data |
A compound is a collection of matrices. The compressedData contains a merged version of all matrices childs.
Name | Type | Size in bytes | Description |
---|---|---|---|
type | int | 4 | Type of the node (2 for compounds) |
unknown | int | 4 | It is presented in all nodes. I don't know exactly what this value means. |
nameLen | uint | 4 | Size of the name of this node. |
name | string | nameLen | Name of this node |
unknown | bytes | 3 | It is presented in all nodes. I don't know exactly what this value means. |
size | veci3 | 12 | The size of the model |
position | veci3 | 12 | The position in 3D space |
pivot | vecf3 | 12 | The pivot position |
compressedDataSize | uint | 4 | The size of the following compressed data |
compressedData | bytes | compressedDataSize | Zlib compressed voxel data that is a merged version of all childrens. |
childCount | uint | 4 | The count of the child nodes. It seems that the child nodes are always matrices |
The content of the uncompressed Zlib data is compressed with an RLE (runtime length encoding). The voxel data goes from bottom to top, left to right, and front to back. (Starts by (0, 0, 0) and ends by size)
The structure of the rle data is as follows:
Name | Type | Size in bytes | Description |
---|---|---|---|
dataNum | ushort | 2 | Number of integers that are either RGBM values or length values for RLE |
data | array of int | dataNum | Voxel data |
Difference between RGBM and RLE data. If the M (or alpha) byte is 2, the red channel contains the number of how many times the next 4 bytes(RGBM value, always 0x00000000) should be repeated. The G and B value are some sort of metadata. If the M byte is not 2, this is an RGB value. The M byte is the mask byte.
Pseudo code
function loadQBCL(stream)
{
magic = stream.readString(4);
if(magic != "QBCL")
return ERROR
programVersion = stream.readInt
fileVersion = stream.readInt
if(fileVersion != 2)
return ERROR
width = stream.readUInt
height = stream.readUInt
stream.skip(width * height * 4) // Or read the image
for(i = 0; i < 7; i++)
{
size = stream.readUInt
stream.skip(size) // Or save the meta value
}
stream.skip(16) // GUID or timestamp
return loadNode(stream)
}
function loadNode(stream)
{
type = stream.readInt
stream.skip(4) // Unknown value
nameLen = stream.readUInt
stream.skip(nameLen) // Or save node name
stream.skip(3) // Unknown 3 bytes
switch(type)
{
case 0: //Matrix
loadMatrix(stream)
break
case 1: //Model
loadModel(stream)
break
case 2: //Compound
loadCompound(stream)
break
default
return ERROR
break
}
return OK
}
function loadModel(stream)
{
stream.skip(36) // Unkown chunk
childCount = stream.readUInt
for(i = 0; i < childCount; i++)
loadNode(stream)
}
function loadMatrix(stream)
{
size = stream.readVeci3
position = stream.readVeci3
pivot = stream.readVecf3
compressedDataSize = stream.readUInt
zlibStream = new zlibStream(stream, compressedDataSize)
index = 0
while(!zlibStream.isEOF)
{
y = 0
dataSize = zlibStream.readUShort
for(i = 0; i < dataSize; i++)
{
data = zlibStream.readColor
if(data.A == 2) //RLE
{
color = zlibStream.readColor
for(j = 0; j < data.R; j++)
{
x = (index / size.z);
z = index % size.z;
voxelGrid[x, y, z] = color.toUInt;
y++
}
i++
}
else // Uncompressed
{
x = (index / size.z);
z = index % size.z;
voxelGrid[x, y, z] = data.toUInt;
y++
}
}
index++
}
}
function loadCompound(stream)
{
size = stream.readVeci3
position = stream.readVeci3
pivot = stream.readVecf3
compressedDataSize = stream.readUInt
zlibStream = new zlibStream(stream, compressedDataSize) // Merged version of the children
index = 0
while(!zlibStream.isEOF)
{
y = 0
dataSize = zlibStream.readUShort
for(i = 0; i < dataSize; i++)
{
data = zlibStream.readColor
if(data.A == 2) //RLE
{
color = zlibStream.readColor
for(j = 0; j < data.R; j++)
{
x = (index / size.z);
z = index % size.z;
voxelGrid[x, y, z] = color.toUInt;
y++
}
i++
}
else // Uncompressed
{
x = (index / size.z);
z = index % size.z;
voxelGrid[x, y, z] = data.toUInt;
y++
}
}
index++
}
childCount = stream.readUInt
for(i = 0; i < childCount; i++)
loadNode(stream) // Seems to be always matrices.
}
I identified a few more fields:
Here is the beginning of an imhex pattern