Skip to content

Instantly share code, notes, and snippets.

@tostc
Last active November 10, 2022 18:59
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tostc/7f049207a2e5a7ccb714499702b5e2fd to your computer and use it in GitHub Desktop.
Save tostc/7f049207a2e5a7ccb714499702b5e2fd to your computer and use it in GitHub Desktop.
Reverse engineered Qubicle file format

Qubicle's proprietary format QBCL v2.0

Header

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)

Thumbnail

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

Metadata

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.

  1. Title
  2. Description
  3. Tags
  4. Author
  5. Company
  6. Website
  7. Copyright

GUID or timestamp?

16 bytes. I don't know exactly what this value means.

Model tree

The tree starts always with a model node.

Model

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.

Matrix

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

Compounds

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

Zlib data

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.

Parsing

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.
}
@mgerhardy
Copy link

I identified a few more fields:

Here is the beginning of an imhex pattern

struct String {
	u32 len;
	char string[len];
};

struct Matrix {
	u32 sizex;
	u32 sizey;
	u32 sizez;
	s32 tx;
	s32 ty;
	s32 tz;
	float pivotx;
	float pivoty;
	float pivotz;
	u32 compressedDataSize;
	u8 zip[compressedDataSize];
};

struct Matrices {
	u8 mat1[9];
	u8 mat2[9];
	u8 mat3[9];
	u8 mat4[9];
};

using Node;

struct Model {
	Matrices rotation;
	u32 childCount;
	Node nodes[childCount];
};

struct Compound {
	Matrix matrix;
	u32 childCount;
	Node nodes[childCount];
};

struct Digest {
  u8 first[4]; // changes on each re-save
  u8 second[4]; // changes if the model changes
  u8 third[4]; // chnages on each re-save
  u8 fourth[4]; // changes if the model changes
};

struct Node {
	u32 type;
	u32 unknown;
	String name;
	u8 visible;
	u8 unknown2;
	u8 locked;
	if (type == 0) {
		Matrix matrix;
	} else if (type == 1) {
		Model model;
	} else if (type == 2) {
		Compound compound;
	}
};

struct Header {
	u32 magic;
	u32 version;
	u32 fileversion;
	u32 thumbwidth;
	u32 thumbheight;
	char bgra[thumbwidth * thumbheight * 4];
	String title;
	String desc;
	String metadata;
	String author;
	String company;
	String website;
	String copyright;
	Digest digest;
	Node node;
};

Header hdr @0x00;

@tostc
Copy link
Author

tostc commented Nov 4, 2022

Hey @mgerhardy,

very nice and thanks for the contribution. Very nice tool, I didn't know about imhex.
Using your discovery about the "Digest" structure, I found out that these 16 bytes are timestamps, Delphi timestamps. Delphi timestamp or data type "TDateTime" is just a double, which is always 8 bytes long. So maybe we have a creation and modification time?
I'm including a modified version of the imhex pattern below and also a small HTML tool that converts these doubles into human-readable timestamps.

imhex pattern:

struct String {
	u32 len;
	char string[len];
};

struct Matrix {
	u32 sizex;
	u32 sizey;
	u32 sizez;
	s32 tx;
	s32 ty;
	s32 tz;
	float pivotx;
	float pivoty;
	float pivotz;
	u32 compressedDataSize;
	u8 zip[compressedDataSize];
};

// Rotation matrix according to wikipedia
// https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations
struct Matrices {
	float row1[3];
	float row2[3];
	float row3[3];
};

using Node;

struct Model {
	Matrices rotation;
	u32 childCount;
	Node nodes[childCount];
};

struct Compound {
	Matrix matrix;
	u32 childCount;
	Node nodes[childCount];
};

struct Node {
	u32 type;
	u32 unknown;
	String name;
	u8 visible;
	u8 unknown2;
	u8 locked;
	if (type == 0) {
		Matrix matrix;
	} else if (type == 1) {
		Model model;
	} else if (type == 2) {
		Compound compound;
	}
};

struct Header {
	u32 magic;
	u32 version;
	u32 fileversion;
	u32 thumbwidth;
	u32 thumbheight;
	char bgra[thumbwidth * thumbheight * 4];
	String title;
	String desc;
	String metadata;
	String author;
	String company;
	String website;
	String copyright;
	// Maybe change and creation time?
	double time1;
	double time2;
	Node node;
};

Header hdr @0x00;

Converter tool:

<html>
<head>
    <title>Delphi zu Zeit</title>
    <script>
        function convertTime() {
            var delphiTime = parseFloat(document.getElementById("delphiTime").value);

            // https://stackoverflow.com/a/5073935
            // First convert these weird delphi time to unxitime
            var unixtime = Math.round((delphiTime - 25569) * 86400);

            // Javascript uses unixtime in milliseconds not seconds.
            var date = new Date(unixtime * 1000);
            document.getElementById("time").innerHTML = date.toLocaleString();
        }
    </script>
</head>
<body>
    <input type="number" id="delphiTime"><button onclick="convertTime()">Convert</button>
    <br>
    <div id="time"></div>
</body>
</html>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment