Files
ImHex-Patterns/patterns/vox.hexpat
Jake Ryan 0b9e83ff8a patterns: Add support for MagicaVoxel .vox files (#390)
* patterns: Added support for MagicaVoxel .vox files

* Fixed incorrect IMAP field size and added basic chunk size mismatch detection and recovery.

* Fixed pattern for "_r" values and added RotationToMat3.

* Added test vox file.

---------

Co-authored-by: paxcut <53811119+paxcut@users.noreply.github.com>
2025-04-30 08:55:53 -07:00

270 lines
6.6 KiB
Rust

#pragma author Jake Ryan
#pragma description MagicaVoxel scene description format
#pragma magic [ 56 4F 58 20 ] @ 0
#pragma array_limit 0x100000
import std.io;
import std.core;
// Input and output variables
// If true, color data will be formatted as normalized floats.
bool formatColorsAsFloat in;
// If true, chunks will be treated as though they were in a single big array instead of a hierarchy.
bool readChunksAsLinear in;
bool attemptRecoveryOnChunkSizeMismatch in;
// Pattern definitions
// https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox.txt
// https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox-extension.txt
struct VoxHeader
{
char magic[4];
u32 version;
};
struct Voxel
{
u8 x;
u8 y;
u8 z;
u8 colorIndex;
} [[static, format("FormatVoxel")]];
fn FormatVoxel(ref Voxel v)
{
return std::format("{}, {}, {}", v.x, v.y, v.z);
};
struct RGBA8
{
u8 r [[color("FF0000")]];
u8 g [[color("00FF00")]];
u8 b [[color("0000FF")]];
u8 a;
} [[static, color(std::format("{:02X}{:02X}{:02X}", r, g, b)), format("FormatRGBA8")]];
fn FormatRGBA8(ref RGBA8 c)
{
if (formatColorsAsFloat)
{
return std::format("{:4.2f}, {:4.2f}, {:4.2f}, {:4.2f}", c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0);
}
else
{
return std::format("{:3}, {:3}, {:3}, {:3}", c.r, c.g, c.b, c.a);
}
};
struct String
{
u32 sizeBytes;
char data[sizeBytes];
} [[format("FormatString")]];
fn FormatString(ref String s)
{
return std::format("\"{}\"", s.data);
};
struct Vec3 { float v[3]; };
struct Mat3 { Vec3 r0, r1, r2; } [[format("FormatMat3")]];
fn FormatMat3(ref Mat3 m)
{
return std::format("({}, {}, {}) ({}, {}, {}) ({}, {}, {})",
m.r0.v[0], m.r0.v[1], m.r0.v[2],
m.r1.v[0], m.r1.v[1], m.r1.v[2],
m.r2.v[0], m.r2.v[1], m.r2.v[2]);
};
bitfield Rotation
{
firstRowIndex : 2;
secondRowIndex : 2;
firstRowSign : 1;
secondRowSign : 1;
thirdRowSign : 1;
padding : 1;
};
// Unused, but demonstrates how a 3x3 matrix could be constructed from Rotation.
fn RotationToMat3(Rotation r)
{
Mat3 mat;
mat.r0.v[r.firstRowIndex] = -(r.firstRowSign * 2 - 1);
mat.r1.v[r.secondRowIndex] = -(r.secondRowSign * 2 - 1);
mat.r2.v[0] = -(r.thirdRowSign * 2.0 - 1) * !(r.firstRowIndex == 0 || r.secondRowIndex == 0);
mat.r2.v[1] = -(r.thirdRowSign * 2.0 - 1) * !(r.firstRowIndex == 1 || r.secondRowIndex == 1);
mat.r2.v[2] = -(r.thirdRowSign * 2.0 - 1) * !(r.firstRowIndex == 2 || r.secondRowIndex == 2);
return mat;
};
struct KeyValue
{
String key;
// Depending on the key, the value must be parsed as one or more floats or integers.
String value;
} [[format("FormatKeyValue")]];
fn FormatKeyValue(ref KeyValue kv)
{
return std::format("{}: {}", kv.key, kv.value);
};
struct Dict
{
u32 count;
KeyValue pairs[count] [[inline]];
} [[format("FormatDict")]];
fn FormatDict(ref Dict d)
{
return std::format("count: {}", d.count);
};
struct Model
{
s32 modelId;
Dict attributes;
};
struct Chunk
{
char id[4];
u32 sizeBytes;
u32 childrenBytes;
// Contents of the chunk
s32 cursorPosBeforeContent = $;
match (id)
{
("MAIN"): {}
("PACK"):
{
u32 numModels;
}
("SIZE"):
{
u32 sizeX, sizeY, sizeZ;
}
("XYZI"):
{
u32 numVoxels;
Voxel voxels[numVoxels];
}
("RGBA"):
{
RGBA8 colors[256];
}
("nTRN"):
{
s32 nodeId;
Dict attributes;
s32 childNodeId;
s32 reservedMustBeNegative1;
s32 layerId;
u32 numFrames;
Dict frames[numFrames];
}
("nGRP"):
{
s32 nodeId;
Dict attributes;
u32 numChildren;
s32 childrenIds[numChildren];
}
("nSHP"):
{
s32 nodeId;
Dict attributes;
u32 numModels;
Model models[numModels];
}
("MATL"): // Material properties
{
s32 materialId;
Dict attributes;
}
("LAYR"):
{
s32 layerId;
Dict attributes;
s32 reservedMustBeNegative1;
}
("rOBJ"): // Rendering attributes
{
Dict attributes;
}
("rCAM"): // Camera attributes
{
s32 cameraId;
Dict attributes;
}
("NOTE"): // Names for colors
{
u32 numNames;
String names[numNames];
}
("IMAP"): // Index map
{
// The documentation says this field is i32x256, but it's u8x256 in actual models.
u8 paletteIndices[256];
}
(_):
{
u8 unknownData[sizeBytes];
std::warning(std::format("Unknown chunk ID at 0x{:X}: \"{}\"", $, id));
}
}
s32 actualContentSize = ($ - cursorPosBeforeContent);
if (actualContentSize != sizeBytes)
{
str warningTextSecondHalf = std::format("Content size mismatch! Expected: {}. Actual: {}", sizeBytes, actualContentSize);
str warningText = std::format("Chunk at 0x{:X} with id {}: {}", addressof(this), id, warningTextSecondHalf);
if (attemptRecoveryOnChunkSizeMismatch)
{
std::warning(warningText);
}
else
{
std::error(warningText);
}
// Limited recovery- reading past EoF is still possible if chunk is nested.
// Setting readChunksAsLinear to true can make recovery more robust as the outermost array will be broken out of.
break;
}
if (childrenBytes > 0 && !readChunksAsLinear)
{
s32 cursorPosBeforeChildren = $;
Chunk children[while(true)];
}
// The node with id MAIN is also the root node.
if (id != "MAIN" && !readChunksAsLinear)
{
// We are done parsing the children for this parent if the cursor has advanced parent.childrenBytes bytes since then.
if ($ - parent.cursorPosBeforeChildren >= parent.childrenBytes)
{
break;
}
}
} [[format("ChunkFormatter")]];
fn ChunkFormatter(ref Chunk chunk)
{
return chunk.id;
};
VoxHeader header @ 0 [[inline]];
Chunk mainChunk[readChunksAsLinear ? 0 : 1] @ $ [[inline]];
Chunk chunks[while(readChunksAsLinear && !std::mem::eof())] @ $;
if (std::core::member_count(mainChunk) > 0)
{
std::core::set_display_name(mainChunk[0], "mainChunk");
}