diff --git a/README.md b/README.md index 0eaeaa9..502946b 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | VEADO | | [`patterns/veado.hexpat`](patterns/veado.hexpat) | veadotube mini avatar file | | VGM | | [`patterns/vgm.hexpat`](patterns/vgm.hexpat) | VGM (Video Game Music) sound log | | VHDX | | [`patterns/vhdx.hexpat`](patterns/vhdx.hexpat) | Microsoft Hyper-V Virtual Hard Disk format | +| VOX | | [`patterns/vox.hexpat`](patterns/vox.hexpat) | MagicaVoxel scene description format | | WAV | `audio/x-wav` | [`patterns/wav.hexpat`](patterns/wav.hexpat) | RIFF header, WAVE header, PCM header | | WAS | | [`patterns\was_oskasoftware.hexpat`](patterns\was_oskasoftware.hexpat) | Oska Software DeskMates WAS/WA3 (WAVE/MP3 Set) file | WAD | | [`patterns/wad.hexpat`](patterns/wad.hexpat) | DOOM WAD Archive | diff --git a/patterns/vox.hexpat b/patterns/vox.hexpat new file mode 100644 index 0000000..3e6e1f8 --- /dev/null +++ b/patterns/vox.hexpat @@ -0,0 +1,270 @@ +#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"); +} \ No newline at end of file diff --git a/tests/patterns/test_data/vox.hexpat.vox b/tests/patterns/test_data/vox.hexpat.vox new file mode 100644 index 0000000..717bd02 Binary files /dev/null and b/tests/patterns/test_data/vox.hexpat.vox differ