-
-
Save Eisenwave/aca18b48fdaea3259894ceb4d8e0b846 to your computer and use it in GitHub Desktop.
Voxel Reader for VOX
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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