VOXEL: introduced qubicle binary importer/exporter

master
Martin Gerhardy 2016-11-21 20:54:20 +01:00
parent 90d6ee1082
commit 10e2ef1174
8 changed files with 520 additions and 1 deletions

View File

@ -3,6 +3,7 @@ set(SRCS
model/VoxFileFormat.h model/VoxFileFormat.cpp
model/VoxFormat.h model/VoxFormat.cpp
model/QBTFormat.h model/QBTFormat.cpp
model/QBFormat.h model/QBFormat.cpp
model/MeshExporter.h model/MeshExporter.cpp
font/VoxelFont.h font/VoxelFont.cpp
BiomeManager.h BiomeManager.cpp
@ -53,7 +54,7 @@ target_compile_options(${LIB} PRIVATE -march=native PRIVATE -O3)
gtest_suite_files(tests
tests/AbstractVoxelTest.h
tests/AbstractVoxFormatTest.h
tests/AbstractVoxFormatTest.h tests/AbstractVoxFormatTest.cpp
tests/WorldTest.cpp
tests/WorldPersisterTest.cpp
tests/LSystemGeneratorTest.cpp
@ -63,6 +64,7 @@ gtest_suite_files(tests
tests/AmbientOcclusionTest.cpp
tests/VoxFormatTest.cpp
tests/QBTFormatTest.cpp
tests/QBFormatTest.cpp
tests/MeshExporterTest.cpp
tests/VolumeMergerTest.cpp
tests/VolumeRotatorTest.cpp

View File

@ -0,0 +1,386 @@
/**
* @file
*/
#include "QBFormat.h"
#include "voxel/polyvox/VolumeMerger.h"
#include "core/Common.h"
#include "core/Zip.h"
#include "core/Color.h"
namespace voxel {
namespace {
const int RLE_FLAG = 2;
const int NEXT_SLICE_FLAG = 6;
}
#define wrapSave(write) \
if (write == false) { \
Log::error("Could not save qb file: " CORE_STRINGIFY(write) " failed"); \
return false; \
}
#define wrap(read) \
if (read != 0) { \
Log::error("Could not load qb file: Not enough data in stream " CORE_STRINGIFY(read) " - still %i bytes left", (int)stream.remaining()); \
return nullptr; \
}
#define wrapBool(read) \
if (read == false) { \
Log::error("Could not load qb file: Not enough data in stream " CORE_STRINGIFY(read) " - still %i bytes left", (int)stream.remaining()); \
return nullptr; \
}
#define wrapColor(read) \
if (read != 0) { \
Log::error("Could not load qb file: Not enough data in stream " CORE_STRINGIFY(read) " - still %i bytes left", (int)stream.remaining()); \
return voxel::VoxelType::Invalid; \
}
#define setBit(val, index) val &= (1 << (index))
bool QBFormat::save(const RawVolume* volume, const io::FilePtr& file) {
io::FileStream stream(file.get());
wrapSave(stream.addInt(257))
ColorFormat colorFormat = ColorFormat::RGBA;
ZAxisOrientation zAxisOrientation = ZAxisOrientation::Right;
Compression compression = Compression::RLE;
VisibilityMask visibilityMask = VisibilityMask::AlphaChannelVisibleByValue;
wrapSave(stream.addInt((uint32_t)colorFormat))
wrapSave(stream.addInt((uint32_t)zAxisOrientation))
wrapSave(stream.addInt((uint32_t)compression))
wrapSave(stream.addInt((uint32_t)visibilityMask))
wrapSave(stream.addInt(1))
wrapSave(stream.addByte(0)); // no name
const voxel::Region& region = volume->getRegion();
const glm::ivec3 size = region.getDimensionsInVoxels();
wrapSave(stream.addInt(size.x));
wrapSave(stream.addInt(size.y));
wrapSave(stream.addInt(size.z));
const int offset = 0;
wrapSave(stream.addInt(offset));
wrapSave(stream.addInt(offset));
wrapSave(stream.addInt(offset));
int axisIndex1;
int axisIndex2;
if (zAxisOrientation == ZAxisOrientation::Right) {
axisIndex1 = 0;
axisIndex2 = 2;
} else {
axisIndex1 = 2;
axisIndex2 = 0;
}
const voxel::Voxel Empty = voxel::createVoxel(voxel::VoxelType::Air);
const int32_t EmptyColor = core::Color::GetRGB(getColor(Empty));
const glm::ivec3& mins = region.getLowerCorner();
const glm::ivec3& maxs = region.getUpperCorner();
int32_t currentColor = EmptyColor;
int count = 0;
for (int axis1 = mins[axisIndex2]; axis1 <= maxs[axisIndex2]; ++axis1) {
for (int y = maxs[1]; y >= mins[1]; --y) {
for (int axis2 = mins[axisIndex1]; axis2 <= maxs[axisIndex1]; ++axis2) {
int x, z;
const bool rightHanded = zAxisOrientation == ZAxisOrientation::Right;
if (rightHanded) {
x = axis2;
z = axis1;
} else {
x = axis1;
z = axis2;
}
const Voxel& voxel = volume->getVoxel(x, y, z);
int32_t newColor;
if (voxel == Empty) {
newColor = EmptyColor;
} else {
uint8_t visible;
if (visibilityMask == VisibilityMask::AlphaChannelVisibleSidesEncoded) {
// TODO: this looks wrong - use the Sides enum
voxel::RawVolume::Sampler sampler(volume);
sampler.setPosition(x, y, z);
visible = 0;
if (sampler.peekVoxel0px0py1pz() == Empty) {
setBit(visible, rightHanded ? 1 : 6);
}
if (sampler.peekVoxel0px0py1nz() == Empty) {
setBit(visible, rightHanded ? 2 : 5);
}
if (sampler.peekVoxel0px1py0pz() == Empty) {
setBit(visible, 3);
}
if (sampler.peekVoxel0px1ny0pz() == Empty) {
setBit(visible, 4);
}
if (sampler.peekVoxel1nx0py0pz() == Empty) {
setBit(visible, rightHanded ? 5 : 1);
}
if (sampler.peekVoxel1px0py0pz() == Empty) {
setBit(visible, rightHanded ? 6 : 2);
}
} else {
visible = 255;
}
const int32_t voxelColor = core::Color::GetRGBA(getColor(voxel));
const uint8_t red = (voxelColor >> 24) & 0xFF;
const uint8_t green = (voxelColor >> 16) & 0xFF;
const uint8_t blue = (voxelColor >> 8) & 0xFF;
if (colorFormat == ColorFormat::RGBA) {
newColor = ((uint32_t)red) << 24 | ((uint32_t)green) << 16 | ((uint32_t)blue) << 8 | ((uint32_t)visible) << 0;
} else {
newColor = ((uint32_t)blue) << 24 | ((uint32_t)green) << 16 | ((uint32_t)red) << 8 | ((uint32_t)visible) << 0;
}
}
if (compression == Compression::RLE) {
if (newColor != currentColor) {
if (count == 1) {
wrapSave(stream.addInt(currentColor))
} else if (count == 2) {
wrapSave(stream.addInt(currentColor))
wrapSave(stream.addInt(currentColor))
} else if (count == 3) {
wrapSave(stream.addInt(currentColor))
wrapSave(stream.addInt(currentColor))
wrapSave(stream.addInt(currentColor))
} else if (count > 3) {
wrapSave(stream.addInt(RLE_FLAG))
wrapSave(stream.addInt(count))
wrapSave(stream.addInt(currentColor))
}
count = 0;
}
currentColor = newColor;
count++;
} else {
wrapSave(stream.addInt(newColor))
}
}
}
if (compression == Compression::RLE) {
if (count == 1) {
wrapSave(stream.addInt(currentColor))
} else if (count == 2) {
wrapSave(stream.addInt(currentColor))
wrapSave(stream.addInt(currentColor))
} else if (count == 3) {
wrapSave(stream.addInt(currentColor))
wrapSave(stream.addInt(currentColor))
wrapSave(stream.addInt(currentColor))
} else if (count > 3) {
wrapSave(stream.addInt(RLE_FLAG))
wrapSave(stream.addInt(count))
wrapSave(stream.addInt(currentColor))
}
count = 0;
wrapSave(stream.addInt(NEXT_SLICE_FLAG));
}
}
return true;
}
void QBFormat::setVoxel(voxel::RawVolume* volume, uint32_t x, uint32_t y, uint32_t z, const glm::ivec3& offset, voxel::VoxelType type) {
const int32_t fx = offset.x + x;
const int32_t fy = offset.y + y;
const int32_t fz = offset.z + z;
Log::debug("Set voxel %i to %i:%i:%i", (int)type, fx, fy, fz);
if (_zAxisOrientation == ZAxisOrientation::Right) {
volume->setVoxel(fx, fy, fz, createVoxel(type));
} else {
volume->setVoxel(fz, fy, fx, createVoxel(type));
}
}
voxel::VoxelType QBFormat::getVoxelType(io::FileStream& stream) {
uint8_t red;
uint8_t green;
uint8_t blue;
uint8_t alpha;
wrapColor(stream.readByte(red))
wrapColor(stream.readByte(green))
wrapColor(stream.readByte(blue))
wrapColor(stream.readByte(alpha))
Log::debug("Red: %i, Green: %i, Blue: %i, Alpha: %i", (int)red, (int)green, (int)blue, (int)alpha);
if (alpha == 0) {
return voxel::VoxelType::Air;
}
glm::vec4 color;
if (_colorFormat == ColorFormat::RGBA) {
color = core::Color::FromRGBA(((uint32_t)red) << 24 | ((uint32_t)green) << 16 | ((uint32_t)blue) << 8 | ((uint32_t)255) << 0);
} else {
color = core::Color::FromRGBA(((uint32_t)blue) << 24 | ((uint32_t)green) << 16 | ((uint32_t)red) << 8 | ((uint32_t)255) << 0);
}
const glm::vec4& finalColor = findClosestMatch(color);
const voxel::VoxelType type = findVoxelType(finalColor);
return type;
}
voxel::RawVolume* QBFormat::loadMatrix(io::FileStream& stream) {
char buf[260] = "";
uint8_t nameLength;
wrap(stream.readByte(nameLength));
Log::debug("Matrix name length: %u", (uint32_t)nameLength);
wrapBool(stream.readString(nameLength, buf));
buf[nameLength] = '\0';
Log::debug("Matrix name: %s", buf);
glm::uvec3 size(glm::uninitialize);
wrap(stream.readInt(size.x));
wrap(stream.readInt(size.y));
wrap(stream.readInt(size.z));
Log::debug("Matrix size: %i:%i:%i", size.x, size.y, size.z);
if (size.x == 0 || size.y == 0 || size.z == 0) {
Log::error("Invalid size");
return nullptr;
}
glm::ivec3 offset(glm::uninitialize);
wrap(stream.readInt((uint32_t&)offset.x));
wrap(stream.readInt((uint32_t&)offset.y));
wrap(stream.readInt((uint32_t&)offset.z));
Log::debug("Matrix offset: %i:%i:%i", offset.x, offset.y, offset.z);
voxel::Region region;
const glm::ivec3 maxs(offset.x + size.x, offset.y + size.y, offset.z + size.z);
if (_zAxisOrientation == ZAxisOrientation::Right) {
region = voxel::Region(offset.x, offset.y, offset.z, maxs.x, maxs.y, maxs.z);
} else {
region = voxel::Region(offset.z, offset.y, offset.x, maxs.z, maxs.y, maxs.x);
}
core_assert(region.getDimensionsInCells() == glm::ivec3(size));
if (!region.isValid()) {
return nullptr;
}
voxel::RawVolume* volume = new voxel::RawVolume(region);
if (_compressed == Compression::None) {
Log::debug("qb matrix uncompressed");
for (uint32_t z = 0; z < size.z; ++z) {
for (uint32_t y = 0; y < size.y; ++y) {
for (uint32_t x = 0; x < size.x; ++x) {
const voxel::VoxelType type = getVoxelType(stream);
if (type == voxel::VoxelType::Invalid) {
return nullptr;
}
setVoxel(volume, x, y, z, offset, type);
}
}
}
return volume;
}
Log::debug("Matrix rle compressed");
uint32_t z = 0u;
while (z < size.z) {
int index = -1;
for (;;) {
uint32_t data;
wrap(stream.peekInt(data))
if (data == NEXT_SLICE_FLAG) {
stream.skip(sizeof(data));
break;
}
uint32_t count = 1;
if (data == RLE_FLAG) {
stream.skip(sizeof(data));
wrap(stream.readInt(count))
Log::debug("%u voxels of the same type", count);
}
const voxel::VoxelType type = getVoxelType(stream);
if (type == voxel::VoxelType::Invalid) {
return nullptr;
}
for (uint32_t j = 0; j < count; ++j, ++index) {
const int x = (index + 1) % size.x;
const int y = (index + 1) / size.x;
setVoxel(volume, x, y, z, offset, type);
}
}
++z;
}
Log::debug("Matrix read");
return volume;
}
RawVolume* QBFormat::loadFromStream(io::FileStream& stream) {
wrap(stream.readInt(_version))
wrap(stream.readInt((uint32_t&)_colorFormat))
wrap(stream.readInt((uint32_t&)_zAxisOrientation))
wrap(stream.readInt((uint32_t&)_compressed))
wrap(stream.readInt((uint32_t&)_visibilityMaskEncoded))
uint32_t numMatrices;
wrap(stream.readInt(numMatrices))
Log::debug("Version: %u", _version);
Log::debug("ColorFormat: %u", std::enum_value(_colorFormat));
Log::debug("ZAxisOrientation: %u", std::enum_value(_zAxisOrientation));
Log::debug("Compressed: %u", std::enum_value(_compressed));
Log::debug("VisibilityMaskEncoded: %u", std::enum_value(_visibilityMaskEncoded));
Log::debug("NumMatrices: %u", numMatrices);
glm::ivec3 mins = glm::ivec3(std::numeric_limits<int32_t>::max());
glm::ivec3 maxs = glm::ivec3(std::numeric_limits<int32_t>::min());
std::vector<voxel::RawVolume*> volumes;
volumes.reserve(numMatrices);
for (uint32_t i = 0; i < numMatrices; i++) {
Log::debug("Loading matrix: %u", i);
voxel::RawVolume* v = loadMatrix(stream);
if (v == nullptr) {
break;
}
const voxel::Region& region = v->getRegion();
mins = glm::min(mins, region.getLowerCorner());
maxs = glm::max(maxs, region.getUpperCorner());
volumes.push_back(v);
}
if (volumes.empty()) {
return nullptr;
}
const voxel::Region mergedRegion(glm::ivec3(0), maxs - mins);
Log::debug("Starting to merge volumes into one: %i:%i:%i - %i:%i:%i",
mergedRegion.getLowerX(), mergedRegion.getLowerY(), mergedRegion.getLowerZ(),
mergedRegion.getUpperX(), mergedRegion.getUpperY(), mergedRegion.getUpperZ());
Log::debug("Mins: %i:%i:%i Maxs %i:%i:%i", mins.x, mins.y, mins.z, maxs.x, maxs.y, maxs.z);
voxel::RawVolume* merged = new voxel::RawVolume(mergedRegion);
const glm::ivec3& center = mergedRegion.getCentre();
const glm::ivec3 lc(center.x, 0, center.z);
const glm::ivec3 uc(center.x, mergedRegion.getUpperY(), center.z);
for (voxel::RawVolume* v : volumes) {
const voxel::Region& sr = v->getRegion();
const glm::ivec3& destMins = lc + sr.getLowerCorner();
const voxel::Region dr(destMins, destMins + sr.getDimensionsInCells());
Log::debug("Merge %i:%i:%i - %i:%i:%i into %i:%i:%i - %i:%i:%i",
sr.getLowerX(), sr.getLowerY(), sr.getLowerZ(),
sr.getUpperX(), sr.getUpperY(), sr.getUpperZ(),
dr.getLowerX(), dr.getLowerY(), dr.getLowerZ(),
dr.getUpperX(), dr.getUpperY(), dr.getUpperZ());
voxel::mergeRawVolumes(merged, v, dr, sr);
delete v;
}
return merged;
}
RawVolume* QBFormat::load(const io::FilePtr& file) {
if (!(bool)file || !file->exists()) {
Log::error("Could not load qb file: File doesn't exist");
return nullptr;
}
io::FileStream stream(file.get());
voxel::RawVolume* volume = loadFromStream(stream);
return volume;
}
}

View File

@ -0,0 +1,64 @@
/**
* @file
*/
#pragma once
#include "VoxFileFormat.h"
#include "io/FileStream.h"
namespace voxel {
/**
* @brief Qubicle Binary (qb) format.
*
* http://minddesk.com/learn/article.php?id=22
*/
class QBFormat : public VoxFileFormat {
private:
uint32_t _version;
enum class ColorFormat : uint32_t {
RGBA = 0,
BGRA = 1
};
ColorFormat _colorFormat;
enum class ZAxisOrientation : uint32_t {
Left = 0,
Right = 1
};
ZAxisOrientation _zAxisOrientation;
enum class Compression : uint32_t {
None = 0,
RLE = 1
};
Compression _compressed;
// If set to 0 the A value of RGBA or BGRA is either 0 (invisble voxel) or 255 (visible voxel).
// If set to 1 the visibility mask of each voxel is encoded into the A value telling your software
// which sides of the voxel are visible. You can save a lot of render time using this option.
enum class VisibilityMask : uint32_t {
AlphaChannelVisibleByValue,
AlphaChannelVisibleSidesEncoded
};
VisibilityMask _visibilityMaskEncoded;
// left shift values for the vis mask for the single faces
enum class VisMaskSides : uint8_t {
Invisble,
Left,
Right,
Top,
Bottom,
Front,
Back
};
void setVoxel(voxel::RawVolume* volume, uint32_t x, uint32_t y, uint32_t z, const glm::ivec3& offset, voxel::VoxelType type);
voxel::VoxelType getVoxelType(io::FileStream& stream);
RawVolume* loadMatrix(io::FileStream& stream);
RawVolume* loadFromStream(io::FileStream& stream);
public:
RawVolume* load(const io::FilePtr& file) override;
bool save(const RawVolume* volume, const io::FilePtr& file) override;
};
}

View File

@ -0,0 +1,7 @@
#include "AbstractVoxFormatTest.h"
namespace voxel {
const voxel::Voxel AbstractVoxFormatTest::Empty = voxel::createVoxel(voxel::VoxelType::Air);
}

View File

@ -9,6 +9,8 @@ namespace voxel {
class AbstractVoxFormatTest: public AbstractVoxelTest {
protected:
static const voxel::Voxel Empty;
io::FilePtr open(const std::string_view filename, io::FileMode mode = io::FileMode::Read) {
const io::FilePtr& file = core::App::getInstance()->filesystem()->open(std::string(filename), mode);
return file;

View File

@ -0,0 +1,48 @@
/**
* @file
*/
#include "AbstractVoxFormatTest.h"
#include "voxel/model/QBFormat.h"
namespace voxel {
class QBFormatTest: public AbstractVoxFormatTest {
};
TEST_F(QBFormatTest, testLoad) {
QBFormat f;
RawVolume* volume = load("qubicle.qb", f);
// feets
EXPECT_NE(Empty, volume->getVoxel(18, 0, 1));
EXPECT_NE(Empty, volume->getVoxel(18, 0, 2));
EXPECT_NE(Empty, volume->getVoxel(18, 0, 3));
EXPECT_EQ(Empty, volume->getVoxel(18, 0, 4));
EXPECT_NE(Empty, volume->getVoxel(22, 0, 1));
EXPECT_NE(Empty, volume->getVoxel(22, 0, 2));
EXPECT_NE(Empty, volume->getVoxel(22, 0, 3));
EXPECT_EQ(Empty, volume->getVoxel(22, 0, 4));
// legs
EXPECT_NE(Empty, volume->getVoxel(18, 1, 3));
EXPECT_NE(Empty, volume->getVoxel(18, 2, 3));
EXPECT_NE(Empty, volume->getVoxel(18, 3, 3));
EXPECT_EQ(Empty, volume->getVoxel(18, 4, 3));
EXPECT_NE(Empty, volume->getVoxel(22, 1, 3));
EXPECT_NE(Empty, volume->getVoxel(22, 2, 3));
EXPECT_NE(Empty, volume->getVoxel(22, 3, 3));
EXPECT_EQ(Empty, volume->getVoxel(22, 4, 3));
ASSERT_NE(nullptr, volume) << "Could not load qb file";
delete volume;
}
TEST_F(QBFormatTest, testSave) {
QBFormat f;
RawVolume* volume = load("qubicle.qb", f);
ASSERT_NE(nullptr, volume);
ASSERT_TRUE(f.save(volume, open("qubicle-savetest.qb", io::FileMode::Write)));
ASSERT_TRUE(open("qubicle-savetest.qb")->length() > 177);
delete volume;
}
}

View File

@ -9,6 +9,7 @@
#include "voxel/generator/WorldGenerator.h"
#include "voxel/model/VoxFormat.h"
#include "voxel/model/QBTFormat.h"
#include "voxel/model/QBFormat.h"
#include "tool/Crop.h"
#include "tool/Expand.h"
#include "core/Random.h"
@ -40,6 +41,12 @@ bool Model::save(std::string_view file) {
_dirty = false;
return true;
}
} else if (filePtr->extension() == "qb") {
voxel::QBFormat f;
if (f.save(modelVolume(), filePtr)) {
_dirty = false;
return true;
}
}
return false;
}
@ -58,6 +65,9 @@ bool Model::load(std::string_view file) {
} else if (filePtr->extension() == "vox") {
voxel::VoxFormat f;
newVolume = f.load(filePtr);
} else if (filePtr->extension() == "qb") {
voxel::QBFormat f;
newVolume = f.load(filePtr);
} else {
newVolume = nullptr;
}