MagicaVoxelの.voxファイルを読む

https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox.txt を参考にしてMagicaVoxelの.vox形式のファイルをスクリプトで読んだ。 少ないバイト数でreadを呼ぶのはシステムコールを多用していて遅そうだったり、PACKというチャンクの実装をスキップしていたり、改善の余地はまだまだあるがバイナリファイルにも手出し出来そうだと分かった。

from io import BufferedReader
from typing import Tuple

# reference: https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox.txt

VOX_HEADER = b"VOX "
VOX_VERSION_NUMBER = 150


def main():
    file = "./blue.vox"
    with open(file, "rb") as f:
        header = f.read(4)
        assert header == VOX_HEADER
        version: int = int.from_bytes(f.read(4), byteorder="little")
        assert version == VOX_VERSION_NUMBER

        while True:
            chunk_id = f.read(4)
            if len(chunk_id) == 0:
                exit()
            if chunk_id == b"MAIN":
                read_chunk_header(f)
            elif chunk_id == b"SIZE":
                bytes_of_chunk_content, _ = read_chunk_header(f)
                size_bytes = f.read(bytes_of_chunk_content)
                assert len(size_bytes) == 12
                size_x = size_bytes[:4]
                size_y = size_bytes[4:8]
                size_z = size_bytes[8:12]
                print(size_x, size_y, size_z)
            elif chunk_id == b"XYZI":
                bytes_of_chunk_content, _ = read_chunk_header(f)
                num_voxels = int.from_bytes(f.read(4), byteorder="little")
                assert bytes_of_chunk_content == num_voxels * 4 + 4
                for _ in range(num_voxels):
                    x, y, z, i = f.read(4)
                    print(x, y, z, i)
            elif chunk_id == b"RGBA":
                read_chunk_header(f)
                for _ in range(256):
                    r, g, b, a = f.read(4)
                    print(r, g, b, a)

            elif chunk_id == b"PACK":
                raise NotImplementedError()
            else:
                print(f"unknown Chunk {chunk_id}")
                bytes_of_chunk_content, bytes_of_children_chunks = read_chunk_header(f)
                f.read(bytes_of_chunk_content)
                f.read(bytes_of_children_chunks)


def read_chunk_header(f: BufferedReader) -> Tuple[int, int]:
    bytes_of_chunk_content = int.from_bytes(f.read(4), byteorder="little")
    bytes_of_children_chunks = int.from_bytes(f.read(4), byteorder="little")
    return (bytes_of_chunk_content, bytes_of_children_chunks)


if __name__ == "__main__":
    main()