Skip to content

Instantly share code, notes, and snippets.

@Eisenwave
Created June 7, 2020 20:05
Show Gist options
  • Save Eisenwave/aca18b48fdaea3259894ceb4d8e0b846 to your computer and use it in GitHub Desktop.
Save Eisenwave/aca18b48fdaea3259894ceb4d8e0b846 to your computer and use it in GitHub Desktop.
Voxel Reader for VOX
#include "vox.hpp"
#include "constants.hpp"
#include "util_private.hpp"
#include "core_util/boundingbox.hpp"
#include "core_util/log.hpp"
#include "core_util/rgb32.hpp"
#include "core_util/string.hpp"
#include "core_util/vec_math.hpp"
#include <set>
#define LOG_CURRENT_VOXEL_CHUNK() \
LOG(SPAM, \
"now reading voxel chunk " + std::to_string(state.modelIndex + 1) + "/" + \
std::to_string(voxelChunkInfos.size()) + " - " + voxelChunkInfos[state.modelIndex].toString());
#define LOG_CURRENT_SHAPE() LOG(SPAM, "now reading shape index " + std::to_string(state.parentIndex));
namespace voxelio::vox {
/**
* Performs a division but rounds towards negative infinity.
* For positive numbers, this is equivalent to regular division.
* For all numbers, this is equivalent to a floating point division and then a floor().
* Negative numbers will be decremented before division, leading to this type of rounding.
*
* Examples:
* floor(-1/2) = floor(-0.5) = -1
* floor(-2/2) = floor(-1) = -1
*
* This function imitates such behavior but without the use of any floating point arithmetic.
*
* @param n the number to divide
* @return the number divided by two, rounded towards negative infinity
*/
constexpr static i32 div2Floor(i32 n)
{
return (n - (n < 0)) / 2;
}
static const std::set<ChunkType> CHUNK_TYPE_VALUES_SET{CHUNK_TYPE_VALUES.begin(), CHUNK_TYPE_VALUES.end()};
static bool isValidChunkType(u32 type)
{
return CHUNK_TYPE_VALUES_SET.find(static_cast<ChunkType>(type)) != CHUNK_TYPE_VALUES_SET.end();
}
constexpr const char *nameOf(ChunkType type)
{
switch (type) {
case ChunkType::MAIN: return "MAIN";
case ChunkType::SIZE: return "SIZE";
case ChunkType::XYZI: return "XYZI";
case ChunkType::RGBA: return "RGBA";
case ChunkType::MATT: return "MATT";
case ChunkType::PACK: return "PACK";
case ChunkType::nGRP: return "nGRP";
case ChunkType::nSHP: return "nSHP";
case ChunkType::nTRN: return "nTRN";
case ChunkType::MATL: return "MATL";
case ChunkType::LAYR: return "LAYR";
case ChunkType::IMAP: return "IMAP";
case ChunkType::rOBJ: return "rOBJ";
}
DEBUG_ASSERT_UNREACHABLE();
return nullptr;
}
constexpr const char *prettyNameOf(ChunkType type)
{
switch (type) {
case ChunkType::MAIN: return "Main";
case ChunkType::SIZE: return "Model Size";
case ChunkType::XYZI: return "Model Voxels";
case ChunkType::RGBA: return "RGBA-Color";
case ChunkType::MATT: return "Deprecated Material";
case ChunkType::PACK: return "Pack";
case ChunkType::nTRN: return "Transform Node";
case ChunkType::nGRP: return "Group Node";
case ChunkType::nSHP: return "Shape Node";
case ChunkType::LAYR: return "Layer";
case ChunkType::MATL: return "Material";
case ChunkType::IMAP: return "IMAP (?)";
case ChunkType::rOBJ: return "Renderer Settings";
}
DEBUG_ASSERT_UNREACHABLE();
return nullptr;
}
constexpr static const char *nameOf(NodeType type)
{
switch (type) {
case NodeType::GROUP: return "GROUP";
case NodeType::SHAPE: return "SHAPE";
case NodeType::TRANSFORM: return "TRANSFORM";
}
DEBUG_ASSERT_UNREACHABLE();
return nullptr;
}
constexpr static const char *voxNameOf(NodeType type)
{
switch (type) {
case NodeType::GROUP: return "nGRP";
case NodeType::SHAPE: return "nSHP";
case NodeType::TRANSFORM: return "nTRN";
}
DEBUG_ASSERT_UNREACHABLE();
return nullptr;
}
constexpr bool isDeprecated(ChunkType type)
{
return type == ChunkType::MATT;
}
constexpr const char *MAGIC = magicOf(FileType::VOX);
constexpr size_t MAGIC_LENGTH = CHUNK_NAME_LENGTH;
constexpr u32 CURRENT_VERSION = 150;
constexpr const char *KEY_ROTATION = "_r";
constexpr const char *KEY_TRANSLATION = "_t";
ReadResult Reader::init() noexcept
{
if (initialized) {
return {0, ResultCode::WARNING_DOUBLE_INIT};
}
FORWARD_BAD_RESULT(readMagicAndVersion());
FORWARD_BAD_RESULT(readChunk(false)); // main chunk, eof not allowed because it must exist
while (not stream.eof()) {
FORWARD_BAD_RESULT(readChunk(true)); // eof is allowed for all other chunks
}
LOG(DEBUG, "first/init pass of VOX complete, reader initialized");
DEBUG_ASSERT(!stream.bad());
stream.clear(); // we must clear eof and other flags to read again
stream.seekg(voxelChunkInfos[0].pos);
DEBUG_ASSERT(!stream.fail());
FORWARD_BAD_RESULT(processSceneGraph());
LOG_CURRENT_VOXEL_CHUNK();
LOG_CURRENT_SHAPE();
updateTransformForCurrentShape();
initialized = true;
return ReadResult::ok();
}
ReadResult Reader::processSceneGraph() noexcept
{
for (u32 shapeNodeId : shapeNodeIds) {
u32 modelId = nodeMap.find(shapeNodeId)->second.contentId;
auto [parentsBegin, parentsEnd] = nodeParentMap.equal_range(shapeNodeId);
for (auto &parentIter = parentsBegin; parentIter != parentsEnd; ++parentIter) {
u32 parentNodeId = parentIter->second;
if (auto parentType = nodeMap.find(parentNodeId)->second.type; parentType != NodeType::TRANSFORM) {
return ReadResult::parseError(
tellg(), "Parent of nSHP expected to be nTRN but was " + std::string{voxNameOf(parentType)});
}
voxelChunkInfos[modelId].parentIds.push_back(parentNodeId);
}
}
return ReadResult::ok();
}
ReadResult Reader::read(Voxel32 buffer[], size_t bufferLength)
{
if (not initialized) {
LOG(DEBUG, "calling voxelio::vox::Reader::init()");
return init();
}
writeHelper.reset(buffer, bufferLength);
return doRead();
}
ReadResult Reader::read(Voxel64 buffer[], size_t bufferLength) noexcept
{
if (not initialized) {
LOG(DEBUG, "calling voxelio::vox::Reader::init()");
return init();
}
writeHelper.reset(buffer, bufferLength);
return doRead();
}
Vec3i8 Transformation::row(size_t index) const
{
DEBUG_ASSERT_LT(index, 3);
return matrix[index];
}
Vec3i8 Transformation::col(size_t index) const
{
DEBUG_ASSERT_LT(index, 3);
return {matrix[0][index], matrix[1][index], matrix[2][index]};
}
Transformation Transformation::concat(const Transformation &lhs, const Transformation &rhs)
{
Vec3i32 resultTranslation = lhs.translation;
std::array<Vec3i8, 3> resultMatrix;
for (size_t row = 0; row < 3; ++row) {
const auto lhsRow = lhs.row(row);
for (size_t col = 0; col < 3; ++col) {
resultMatrix[row][col] = lhsRow * rhs.col(col);
}
resultTranslation[row] += lhsRow * rhs.translation;
}
return {std::move(resultMatrix), resultTranslation};
}
Vec3i32 Transformation::apply(const Vec3u32 &pointInChunk, const Vec3u32 &doublePivot) const
{
Vec3i32 doublePointRelToCenter = static_vec_cast<i32>(pointInChunk * 2) - static_vec_cast<i32>(doublePivot);
Vec3i32 rotated{};
for (size_t row = 0; row < matrix.size(); ++row) {
rotated[row] = div2Floor(matrix[row] * doublePointRelToCenter);
}
return rotated + translation;
}
/*
* This implementation is based on the statements made by ephtracy.
* transform * ( v - ( modelSize / 2 ) )
Vec3i32 Transformation::apply(const Vec3u32 &pointInChunk, const Vec3u32 &chunkSize) const
{
Vec3i32 pRelToCenter = static_vec_cast<i32>(pointInChunk) - static_vec_cast<i32>(chunkSize / 2);
Vec3i32 rotated{};
for (size_t row = 0; row < matrix.size(); ++row) {
rotated[row] = matrix[row] * pRelToCenter;
}
return rotated + translation;
}
*/
std::string Transformation::toString() const
{
std::stringstream stream;
stream << "Transformation{r={";
for (size_t i = 0; i < 3; ++i) {
const auto &row = matrix[i];
stream << std::to_string(row[0]) << " " << std::to_string(row[1]) << " " << std::to_string(row[2]);
if (i != 2) stream << "; ";
}
stream << "}, t={" << translation[0] << ", " << translation[1] << ", " << translation[2] << "}";
stream << "}";
return stream.str();
}
std::string VoxelChunkInfo::toString() const
{
std::stringstream stream;
stream << "VoxelChunkInfo";
stream << "{size=" << size;
stream << ", voxelCount=" << voxelCount;
stream << ", pos=" << pos << "}";
return stream.str();
}
void Reader::updateTransformForCurrentShape()
{
const auto baseParentId = voxelChunkInfos[state.modelIndex].parentIds[state.parentIndex];
const auto &baseParentNode = nodeMap.at(baseParentId);
DEBUG_ASSERT(baseParentNode.type == NodeType::TRANSFORM);
state.transform = transformations[baseParentNode.contentId];
auto parentId = baseParentId;
for (auto iter = nodeParentMap.find(parentId); iter != nodeParentMap.end(); iter = nodeParentMap.find(parentId)) {
parentId = iter->second;
const auto &parentNode = nodeMap.at(parentId);
if (parentNode.type == NodeType::TRANSFORM) {
const auto &parentTransform = transformations.at(parentNode.contentId);
state.transform = Transformation::concat(parentTransform, state.transform);
}
}
LOG(SPAM,
"updated transform for current parent (" + std::to_string(baseParentId) + ") to " + state.transform.toString() +
" (" + std::to_string(baseParentNode.contentId) + ")");
}
[[nodiscard]] ReadResult Reader::readOneVoxel(const Vec3u32 &doublePivot) noexcept
{
static_assert(std::numeric_limits<u8>::max() < PALETTE_SIZE);
u8 xyzi[4];
stream.read(reinterpret_cast<char *>(xyzi), sizeof(xyzi));
NO_EOF;
Vec3i32 pos = state.transform.apply(Vec3i32{xyzi[0], xyzi[1], xyzi[2]}, doublePivot);
// DEBUG_ASSERT(bounds.containsIncl(pos));
// swap necessary because gravity axis is z for magica and y for us
std::swap(pos[1], pos[2]);
pos[2] = -pos[2];
auto rgb = palette[xyzi[3]];
writeHelper.write(Voxel32{pos, {rgb}});
LOG(SUPERSPAM,
"voxel " + pos.toString() + ", color index " + std::to_string(xyzi[3]) + " raw " +
Vec3u8(xyzi[0], xyzi[1], xyzi[2]).toString() + " i " + std::to_string(xyzi[3]));
return ReadResult::ok();
}
ReadResult Reader::doRead() noexcept
{
DEBUG_ASSERT(initialized);
ALWAYS_ASSERT(not stream.fail());
while (state.modelIndex < voxelChunkInfos.size()) {
const auto seekModel = [this]() -> void {
stream.seekg(voxelChunkInfos[state.modelIndex].pos);
};
const VoxelChunkInfo &chunk = voxelChunkInfos[state.modelIndex];
// The pivot of rotation is always on the grid between voxels in Magica.
// Subtracting the double position from this double pivot gives us the double position rel. to the center.
// Adding one() is important because this is a 0.5 offset in actual coordinates.
// We must calculate with the double coordinate system, otherwise we could not represent a 0.5.
//
// By doing & ~1 we drop the least significant bit in double coordinates.
// In actual coordinates, this snaps the pivot to the next lower grid vertex.
// This is another reason why we need to use the double coordinate system.
const Vec3u32 doublePivot = (chunk.size & ~1u) - Vec3u32::one();
while (state.parentIndex < chunk.parentIds.size()) {
for (; state.voxelIndex < chunk.voxelCount; ++state.voxelIndex) {
if (writeHelper.isFull()) {
LOG(SPAM, "buffer is full, pausing read process");
return ReadResult::ok(writeHelper.voxelsWritten());
}
FORWARD_BAD_RESULT(readOneVoxel(doublePivot));
}
state.voxelIndex = 0;
if (++state.parentIndex < chunk.parentIds.size()) {
LOG_CURRENT_SHAPE();
updateTransformForCurrentShape();
seekModel();
DEBUG_ASSERT(not stream.fail());
}
}
if (++state.modelIndex < voxelChunkInfos.size()) {
state.parentIndex = 0;
LOG_CURRENT_VOXEL_CHUNK();
LOG_CURRENT_SHAPE();
updateTransformForCurrentShape();
seekModel();
DEBUG_ASSERT(not stream.fail());
}
}
return ReadResult::end(writeHelper.voxelsWritten());
}
ReadResult Reader::expectChars(const char name[CHUNK_NAME_LENGTH]) noexcept
{
char buffer[MAGIC_LENGTH];
stream.read(buffer, MAGIC_LENGTH);
for (size_t i = 0; i < MAGIC_LENGTH; ++i) {
if (buffer[i] != name[i]) {
Error error = {tellg(), std::string{"expected magic \""} + MAGIC + '"'};
return {0, ResultCode::READ_ERROR_UNEXPECTED_SYMBOL, std::move(error)};
}
}
return ReadResult::ok();
}
[[nodiscard]] ReadResult Reader::readString(std::string &out) noexcept
{
u32 size = read_little<u32>(stream);
NO_EOF;
out.resize(size);
stream.read(out.data(), size);
NO_EOF;
return ReadResult::ok();
}
[[nodiscard]] ReadResult Reader::readDict(std::unordered_map<std::string, std::string> &out) noexcept
{
std::string key;
std::string value;
u32 size = read_little<u32>(stream);
NO_EOF;
for (size_t i = 0; i < size; ++i) {
FORWARD_BAD_RESULT(readString(key));
FORWARD_BAD_RESULT(readString(value));
out.emplace(key, value);
}
return ReadResult::ok();
}
[[nodiscard]] ReadResult Reader::skipChunk() noexcept
{
ChunkHeader header;
FORWARD_BAD_RESULT(readChunkHeader(false, header));
stream.ignore(static_cast<std::streamsize>(header.totalSize()));
return ReadResult::ok();
}
[[nodiscard]] ReadResult Reader::skipString() noexcept
{
u32 size = read_little<u32>(stream);
NO_EOF;
stream.ignore(size);
NO_EOF;
return ReadResult::ok();
}
[[nodiscard]] ReadResult Reader::skipDict() noexcept
{
u32 size = read_little<u32>(stream);
NO_EOF;
for (size_t i = 0; i < size * 2; ++i) {
FORWARD_BAD_RESULT(skipString());
}
return ReadResult::ok();
}
ReadResult Reader::readMagicAndVersion() noexcept
{
auto result = expectChars(MAGIC);
if (result.type == ResultCode::READ_ERROR_UNEXPECTED_SYMBOL) {
Error error = {tellg(), std::string{"expected magic \""} + MAGIC + '"'};
return {0, ResultCode::READ_ERROR_UNEXPECTED_MAGIC, std::move(error)};
}
else if (result.isBad()) {
return result;
}
u32 version = read_little<u32>(stream);
NO_EOF;
if (version != CURRENT_VERSION) {
return {0, ResultCode::READ_ERROR_UNKNOWN_VERSION};
}
return ReadResult::ok();
}
ReadResult Reader::readChunk(bool isEofAtFirstByteAllowed) noexcept
{
ChunkHeader header;
auto result = readChunkHeader(isEofAtFirstByteAllowed, header);
if (result.isBad()) return result;
if (result.type == ResultCode::READ_OBJECT_END) return ReadResult::ok();
result = readChunkContent(header);
state.previousChunkType = header.type;
return result;
}
ReadResult Reader::readChunkHeader(bool isEofAtFirstByteAllowed, ChunkHeader &out) noexcept
{
ChunkType type;
if (auto result = readChunkType(type); result.isBad()) {
if (isEofAtFirstByteAllowed && result.type == ResultCode::READ_ERROR_UNEXPECTED_EOF) {
return ReadResult::nextObject();
}
else
return result;
}
u32 selfSize = read_little<u32>(stream);
u32 childrenSize = read_little<u32>(stream);
NO_EOF;
const auto produceLogMessage = [=]() -> std::string {
std::stringstream ss;
ss << "reading " << nameOf(type);
ss << " (";
ss << std::to_string(selfSize) << "self + ";
ss << std::to_string(childrenSize) << "children = ";
ss << std::to_string(selfSize + childrenSize);
ss << ')';
return ss.str();
};
LOG(SPAM, produceLogMessage());
out = {type, selfSize, childrenSize};
return ReadResult::ok();
}
ReadResult Reader::readChunkType(ChunkType &out) noexcept
{
u32 id = read_big<u32>(stream);
if (this->stream.eof()) {
return ReadResult::unexpectedEof(tellg());
}
if (not isValidChunkType(id)) {
Error error = {tellg(), "invalid chunk id: 0x" + mve::str::to_hex_string(id)};
return {0, ResultCode::READ_ERROR_CORRUPTED_ENUM, std::move(error)};
}
out = static_cast<ChunkType>(id);
return ReadResult::ok();
}
ReadResult Reader::readChunkContent(const ChunkHeader &header) noexcept
{
switch (header.type) {
case ChunkType::PACK: {
Error error = {tellg(), "PACK chunks are not supported"};
return {0, ResultCode::READ_ERROR_UNSUPPORTED_FEATURE, error};
}
// we don't need materials or renderer settings
case ChunkType::MATL:
case ChunkType::MATT:
case ChunkType::IMAP:
case ChunkType::rOBJ:
this->stream.ignore(static_cast<std::streamsize>(header.totalSize()));
return ReadResult::ok();
case ChunkType::MAIN: return readChunkContent_main();
case ChunkType::SIZE: return readChunkContent_size();
case ChunkType::XYZI: return readChunkContent_xyzi();
case ChunkType::RGBA: return readChunkContent_rgba();
case ChunkType::nTRN: return readChunkContent_nodeTransform();
case ChunkType::nGRP: return readChunkContent_nodeGroup();
case ChunkType::nSHP: return readChunkContent_nodeShape();
case ChunkType::LAYR: return readChunkContent_layer();
}
DEBUG_ASSERT_UNREACHABLE();
}
ReadResult Reader::readChunkContent_main() noexcept
{
if (initialized) {
Error error = {tellg(), "multiple main chunks found"};
return {0, ResultCode::READ_ERROR_MULTIPLE_ROOTS};
}
while (true) {
ChunkHeader header;
FORWARD_BAD_RESULT(readChunkHeader(true, header));
if (header.type != ChunkType::SIZE) {
LOG(SPAM, "No longer skipping because found " + std::string{nameOf(header.type)});
FORWARD_BAD_RESULT(readChunkContent(header));
break;
}
FORWARD_BAD_RESULT(readChunkContent_size());
FORWARD_BAD_RESULT(readChunkHeader(false, header));
if (header.type != ChunkType::XYZI) {
return {
0,
ResultCode::READ_ERROR_UNEXPECTED_SYMBOL,
{{tellg(), std::string{"Expected SIZE chunk to be followed by XYZI, but got "} + nameOf(header.type)}}};
}
ALWAYS_ASSERT(not voxelChunkInfos.empty());
auto &voxelChunk = voxelChunkInfos.back();
voxelChunk.voxelCount = read_little<u32>(stream);
voxelChunk.pos = stream.tellg();
NO_EOF;
ALWAYS_ASSERT(voxelChunk.pos != -1);
LOG(SPAM, "Memorizing " + voxelChunk.toString() + " for 2nd pass");
stream.ignore(header.totalSize() - sizeof(u32));
}
return ReadResult::ok();
}
ReadResult Reader::readChunkContent_rgba() noexcept
{
for (size_t i = 0; i < PALETTE_SIZE; ++i) {
u32 rgba = read_big<u32>(stream);
NO_EOF;
this->palette[(i + 1) % PALETTE_SIZE] = color_cast<RGBA, ARGB>(rgba);
}
return ReadResult::ok();
}
ReadResult Reader::readChunkContent_size() noexcept
{
Vec3u32 size;
FORWARD_BAD_RESULT(readVecLe(stream, size));
this->voxelChunkInfos.push_back({size, 0, -1});
return ReadResult::ok();
}
ReadResult Reader::readChunkContent_xyzi() noexcept
{
return ReadResult::ok();
}
ReadResult Reader::makeError_expectedButGot(const std::string &field, i64 expected, i64 actual) noexcept
{
std::stringstream messageStream;
messageStream << "Expected " << field << " to be " << expected << " but got " << actual;
return {0, ResultCode::READ_ERROR_UNEXPECTED_SYMBOL, {{tellg(), messageStream.str()}}};
}
ReadResult Reader::readChunkContent_nodeTransform() noexcept
{
u32 nodeId = read_little<u32>(stream);
NO_EOF;
FORWARD_BAD_RESULT(skipDict()); // attributes, unused
u32 childNodeId = read_little<u32>(stream);
i32 reservedId = read_little<i32>(stream);
ignore_no_eof<i32>(stream); // layerId, unused
u32 numOfFrames = read_little<u32>(stream);
NO_EOF;
if (reservedId != -1) return makeError_expectedButGot("reservedId", -1, reservedId);
if (numOfFrames != 1) return makeError_expectedButGot("numOfFrames", 1, numOfFrames);
Transformation transform;
FORWARD_BAD_RESULT(readTransformationDict(transform));
u32 transformId = static_cast<u32>(transformations.size());
transformations.push_back(std::move(transform));
LOG(SPAM,
"decoded transform " + transform.toString() + " for node " + std::to_string(nodeId) + " as transform " +
std::to_string(transformId));
u32 parentNodeId;
auto parentIter = nodeParentMap.find(nodeId);
if (parentIter == nodeParentMap.end()) {
this->rootNodeId = parentNodeId = nodeId;
}
else {
parentNodeId = parentIter->second;
}
nodeMap.emplace(nodeId, SceneNode{NodeType::TRANSFORM, transformId});
nodeParentMap.emplace(childNodeId, nodeId);
return ReadResult::ok();
}
ReadResult Reader::readChunkContent_nodeGroup() noexcept
{
u32 nodeId = read_little<u32>(stream);
NO_EOF;
FORWARD_BAD_RESULT(skipDict()); // attributes, unused
u32 numOfChildNodes = read_little<u32>(stream);
NO_EOF;
std::vector<u32> children;
for (size_t i = 0; i < numOfChildNodes; ++i) {
children.push_back(read_little<u32>(stream));
}
NO_EOF;
auto parentIter = nodeParentMap.find(nodeId);
if (parentIter == nodeParentMap.end()) {
return ReadResult::parseError(tellg(), "nGRP without parent found");
}
nodeMap.emplace(nodeId, SceneNode{NodeType::GROUP, 0});
for (u32 childNodeId : children) {
nodeParentMap.emplace(childNodeId, nodeId);
}
return ReadResult::ok();
}
ReadResult Reader::readChunkContent_nodeShape() noexcept
{
u32 nodeId = read_little<u32>(stream);
FORWARD_BAD_RESULT(skipDict()); // attributes, unused
u32 numOfModels = read_little<u32>(stream);
NO_EOF;
if (numOfModels != 1) {
return makeError_expectedButGot("numOfModels", 1, numOfModels);
}
// for every model, but there is only one, so we don't loop
u32 modelId = read_little<u32>(stream);
NO_EOF;
if (modelId >= voxelChunkInfos.size()) {
return ReadResult::parseError(tellg(), "modelId " + std::to_string(modelId) + " out of range");
}
FORWARD_BAD_RESULT(skipDict()); // this dict is reserved
auto [parentsBegin, parentsEnd] = nodeParentMap.equal_range(nodeId);
if (parentsBegin == parentsEnd) {
return ReadResult::parseError(tellg(), "nSHP without parents found");
}
const auto &[iter, success] = nodeMap.emplace(nodeId, SceneNode{NodeType::SHAPE, modelId});
DEBUG_ASSERT(success);
shapeNodeIds.push_back(nodeId);
return ReadResult::ok();
}
ReadResult Reader::readChunkContent_layer() noexcept
{
ignore_no_eof<u32>(stream); // layerId, unused
NO_EOF;
FORWARD_BAD_RESULT(skipDict()); // attributes, unused
i32 reservedId = read_little<i32>(stream);
if (reservedId != -1) {
return makeError_expectedButGot("reservedId", -1, reservedId);
}
return ReadResult::ok();
}
ReadResult Reader::decodeRotation(u8 bits, Transformation &out) noexcept
{
constexpr uint32_t row2IndexTable[] = {UINT32_MAX, UINT32_MAX, UINT32_MAX, 2, UINT32_MAX, 1, 0, UINT32_MAX};
// compute the per-row indexes into k_vectors[] array.
// unpack rotation bits.
// bits : meaning
// 0 - 1 : index of the non-zero entry in the first row
// 2 - 3 : index of the non-zero entry in the second row
// index in last row the needs to be in the remaining column, chosen via lookup table
// 4 - 6 : sign bits of rows, 0 is positive
u32 indicesOfOnesPerRow[3]{(bits >> 0) & 0b11u,
(bits >> 2) & 0b11u,
row2IndexTable[(1 << indicesOfOnesPerRow[0]) | (1 << indicesOfOnesPerRow[1])]};
if (indicesOfOnesPerRow[2] == UINT32_MAX) {
return ReadResult::unexpectedSymbol(tellg(), "invalid rotation: 0b" + mve::str::to_bin_string(bits));
}
for (size_t i = 0; i < 3; ++i) {
const u8 sign = (bits >> (i + 4)) & 1;
const auto indexOfOne = indicesOfOnesPerRow[i];
out.matrix[i][indexOfOne] = 1 - static_cast<i8>(2 * sign);
out.matrix[i][(indexOfOne + 1) % 3] = 0;
out.matrix[i][(indexOfOne + 2) % 3] = 0;
}
return ReadResult::ok();
}
[[nodiscard]] static ReadResult parseTranslation(u64 pos, const std::string &str, Vec3i32 &out)
{
auto splits = mve::str::split_at_delimiter(str, ' ', 3);
if (splits.size() != 3) {
Error error = {
pos, "Expected value of " + std::string{KEY_ROTATION} + " to be 3 space-separated integers, got " + str};
return {0, ResultCode::READ_ERROR_ILLEGAL_DATA_LENGTH, error};
}
for (size_t i = 0; i < 3; ++i) {
if (not mve::str::parse(splits[i], out[i])) {
Error error = {
pos,
"Failed to parse translation integer " + splits[i] + " at index " + std::to_string(i) + " in " + str};
return {0, ResultCode::READ_ERROR_TEXT_DATA_PARSE_FAIL, error};
}
}
return ReadResult::ok();
}
ReadResult Reader::readTransformationDict(Transformation &out) noexcept
{
dict_t dict;
FORWARD_BAD_RESULT(readDict(dict));
if (auto iter = dict.find(KEY_ROTATION); iter != dict.end()) {
const auto &str = iter->second;
u8 bits;
if (not mve::str::parse(str, bits)) {
Error error = {tellg(), "Failed to parse rotation integer \"" + str + '"'};
return {0, ResultCode::READ_ERROR_TEXT_DATA_PARSE_FAIL, error};
}
FORWARD_BAD_RESULT(decodeRotation(bits, out));
}
else {
out.matrix[0] = {i8{1}, i8{0}, i8{0}};
out.matrix[1] = {i8{0}, i8{1}, i8{0}};
out.matrix[2] = {i8{0}, i8{0}, i8{1}};
}
if (auto iter = dict.find(KEY_TRANSLATION); iter != dict.end()) {
FORWARD_BAD_RESULT(parseTranslation(tellg(), iter->second, out.translation));
}
return ReadResult::ok();
}
} // namespace voxelio::vox
#ifndef READER_HPP
#define READER_HPP
#include "util_public.hpp"
#include "voxelio.hpp"
#include "core_util/boundingboxfwd.hpp"
#include <cstddef>
#include <map>
#include <memory>
namespace voxelio::vox {
constexpr size_t CHUNK_NAME_LENGTH = 4;
constexpr size_t PALETTE_SIZE = 256;
#define REGISTER_CHUNK_TYPE(name) name = (#name[0] << 24) | (#name[1] << 16) | (#name[2] << 8) | #name[3]
enum class ChunkType : u32 {
// BASE
REGISTER_CHUNK_TYPE(MAIN),
REGISTER_CHUNK_TYPE(SIZE),
REGISTER_CHUNK_TYPE(XYZI),
REGISTER_CHUNK_TYPE(RGBA),
REGISTER_CHUNK_TYPE(MATT),
REGISTER_CHUNK_TYPE(PACK),
// EXTENDED
REGISTER_CHUNK_TYPE(nGRP),
REGISTER_CHUNK_TYPE(nSHP),
REGISTER_CHUNK_TYPE(nTRN),
REGISTER_CHUNK_TYPE(LAYR),
REGISTER_CHUNK_TYPE(MATL),
// UNDOCUMENTED
REGISTER_CHUNK_TYPE(IMAP), // I don't fucking know
REGISTER_CHUNK_TYPE(rOBJ) // renderer settings
};
#undef REGISTER_CHUNK_TYPE
static_assert(static_cast<unsigned>(ChunkType::MAIN) == 0x4d41494e);
constexpr std::array<ChunkType, 13> CHUNK_TYPE_VALUES{ChunkType::MAIN,
ChunkType::SIZE,
ChunkType::XYZI,
ChunkType::RGBA,
ChunkType::MATT,
ChunkType::PACK,
ChunkType::nGRP,
ChunkType::nSHP,
ChunkType::nTRN,
ChunkType::LAYR,
ChunkType::MATL,
ChunkType::IMAP,
ChunkType::rOBJ};
enum class NodeType { TRANSFORM, GROUP, SHAPE };
struct SceneNode {
NodeType type;
/** Polymorphic content ID. The index of the voxel chunk for SHAPE nodes, the index of the transform for
* TRANSFORM nodes and 0 for group nodes. */
u32 contentId;
};
struct ChunkHeader {
ChunkType type;
u32 selfSize;
u32 childrenSize;
u32 totalSize() const
{
return selfSize + childrenSize;
}
};
struct Transformation {
static Transformation concat(const Transformation &lhs, const Transformation &rhs);
std::array<Vec3i8, 3> matrix{Vec3i8{i8{1}, i8{0}, i8{0}}, Vec3i8{i8{0}, i8{1}, i8{0}}, {i8{0}, i8{0}, i8{1}}};
Vec3i32 translation{};
Vec3i8 row(size_t index) const;
Vec3i8 col(size_t index) const;
Vec3i32 apply(const Vec3u32 &pointInChunk, const Vec3u32 &doublePivot) const;
std::string toString() const;
};
struct VoxelChunkInfo {
Vec3u32 size;
u32 voxelCount;
std::streamsize pos;
std::vector<u32> parentIds{};
std::string toString() const;
};
class Reader : public AbstractReader {
private:
struct State {
ChunkType previousChunkType;
size_t modelIndex = 0;
size_t parentIndex = 0;
size_t voxelIndex = 0;
Transformation transform;
};
using dict_t = std::unordered_map<std::string, std::string>;
std::unique_ptr<argb32[]> palette = std::make_unique<argb32[]>(PALETTE_SIZE);
VoxelBufferWriteHelper writeHelper;
std::multimap<u32, u32> nodeParentMap;
std::map<u32, SceneNode> nodeMap;
std::vector<VoxelChunkInfo> voxelChunkInfos;
std::vector<Transformation> transformations;
std::vector<u32> shapeNodeIds;
State state;
u32 rootNodeId = 0;
bool initialized = false;
public:
Reader(std::istream &istream, u64 dataLen = DATA_LENGTH_UNKNOWN) : AbstractReader{istream, dataLen} {}
[[nodiscard]] ReadResult init() noexcept override;
[[nodiscard]] ReadResult read(Voxel64 buffer[], size_t bufferLength) noexcept override;
[[nodiscard]] ReadResult read(Voxel32 buffer[], size_t bufferLength);
private:
[[nodiscard]] ReadResult processSceneGraph() noexcept;
[[nodiscard]] ReadResult expectChars(const char name[CHUNK_NAME_LENGTH]) noexcept;
[[nodiscard]] ReadResult readString(std::string &out) noexcept;
[[nodiscard]] ReadResult readDict(dict_t &out) noexcept;
[[nodiscard]] ReadResult skipChunk() noexcept;
[[nodiscard]] ReadResult skipString() noexcept;
[[nodiscard]] ReadResult skipDict() noexcept;
[[nodiscard]] ReadResult doRead() noexcept;
[[nodiscard]] ReadResult readMagicAndVersion() noexcept;
[[nodiscard]] ReadResult readTransformationDict(Transformation &out) noexcept;
[[nodiscard]] ReadResult decodeRotation(u8 in, Transformation &out) noexcept;
[[nodiscard]] ReadResult readOneVoxel(const Vec3u32 &chunkSize) noexcept;
[[nodiscard]] ReadResult readChunk(bool isEofAtFirstByteAllowed) noexcept;
[[nodiscard]] ReadResult readChunkHeader(bool isEofAtFirstByteAllowed, ChunkHeader &out) noexcept;
[[nodiscard]] ReadResult readChunkType(ChunkType &out) noexcept;
[[nodiscard]] ReadResult readChunkContent(const ChunkHeader &header) noexcept;
[[nodiscard]] ReadResult readChunkContent_main() noexcept;
[[nodiscard]] ReadResult readChunkContent_size() noexcept;
[[nodiscard]] ReadResult readChunkContent_xyzi() noexcept;
[[nodiscard]] ReadResult readChunkContent_rgba() noexcept;
[[nodiscard]] ReadResult readChunkContent_nodeTransform() noexcept;
[[nodiscard]] ReadResult readChunkContent_nodeGroup() noexcept;
[[nodiscard]] ReadResult readChunkContent_nodeShape() noexcept;
[[nodiscard]] ReadResult readChunkContent_layer() noexcept;
[[nodiscard]] ReadResult makeError_expectedButGot(const std::string &field, i64 expected, i64 got) noexcept;
void updateTransformForCurrentShape();
};
} // namespace voxelio::vox
#endif // READER_HPP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment