Skip to content

Instantly share code, notes, and snippets.

@Eisenwave
Created June 7, 2020 15:22
Show Gist options
  • Save Eisenwave/799416ac162a4dddeb1312f357f1385c to your computer and use it in GitHub Desktop.
Save Eisenwave/799416ac162a4dddeb1312f357f1385c to your computer and use it in GitHub Desktop.
Voxel Writer for SVX file format
#include "svx.hpp"
#include "3rd_miniz.hpp"
#include "core_util/log.hpp"
#include "core_util/string.hpp"
#include <QBuffer>
#include <QImage>
#include <QImageWriter>
namespace voxelio::svx {
static constexpr const char *xmlNameOf(ChannelType type)
{
switch (type) {
case ChannelType::DENSITY: return "DENSITY";
case ChannelType::COLOR_RGB: return "COLOR-RGB";
case ChannelType::MATERIAL: return "MATERIAL";
case ChannelType::CUSTOM: return "CUSTOM";
}
DEBUG_ASSERT_UNREACHABLE();
}
static constexpr bool usesId(ChannelType type)
{
return type == ChannelType::MATERIAL || type == ChannelType::CUSTOM;
}
std::string Channel::fullFileNameFormat() const
{
return mve::str::to_lower_case<char>(xmlNameOf(type)) + '/' + fileNameFormat;
}
std::string Channel::toXml() const
{
std::stringstream result;
result << "<channel ";
result << "type=\"" << xmlNameOf(type);
if (usesId(type)) {
result << "(" << id << ")";
}
result << "\" bits=\"" << bits;
result << "\" slices=\"" << fullFileNameFormat();
result << "\"/>";
return result.str();
}
std::string Manifest::toXml() const
{
std::stringstream result;
result << "<?xml version=\"1.0\"?>";
result << "<grid version=\"1.0\" ";
result << "gridSizeX=\"" << gridSize_.x() << "\" ";
result << "gridSizeY=\"" << gridSize_.y() << "\" ";
result << "gridSizeZ=\"" << gridSize_.z() << "\" ";
result << "voxelSize=\"" << voxelSize_ << "\" ";
result << "subvoxelBits=\"" << subvoxelBits_ << "\" ";
result << "slicesOrientation=\"" << nameOf(slicesOrientation_) << "\">";
if (not channels.empty()) {
result << "<channels>";
for (const auto &channel : channels) {
result << channel.toXml();
}
result << "</channels>";
}
result << "<materials></materials>";
result << "<metadata></metadata>";
result << "</grid>";
return result.str();
}
bool Manifest::matches(const mve::VoxelArray voxels) const
{
return gridSize().x() >= voxels.sizeX && gridSize().y() >= voxels.sizeY && gridSize().z() >= voxels.sizeZ;
}
struct SvxLimits {
size_t slice;
size_t u;
size_t v;
};
/**
* Transforms world space coordinates into slice, u, v coordinates.
*/
template <mve::Axis AXIS>
static constexpr SvxLimits svxLimitsOf(Vec3size xyzLimits)
{
if constexpr (AXIS == mve::Axis::X) {
return {xyzLimits.x(), xyzLimits.y(), xyzLimits.z()};
}
else if constexpr (AXIS == mve::Axis::Y) {
return {xyzLimits.y(), xyzLimits.x(), xyzLimits.z()};
}
else {
return {xyzLimits.z(), xyzLimits.x(), xyzLimits.y()};
}
}
/**
* Transforms slice, u, v coordinates into world space.
*/
template <mve::Axis AXIS>
constexpr Vec3size svxProject(Vec3size suv)
{
if constexpr (AXIS == mve::Axis::X) {
return Vec3size(suv[0], suv[1], suv[2]);
}
else if constexpr (AXIS == mve::Axis::Y) {
return Vec3size(suv[1], suv[0], suv[2]);
}
else {
return Vec3size(suv[1], suv[2], suv[0]);
}
}
static QImage::Format densityFormatOf(const size_t bits)
{
switch (bits) {
case 1: return QImage::Format::Format_Mono;
case 8: return QImage::Format::Format_Grayscale8;
// case 16: return QImage::Format::Format_Grayscale16; only available in 5.13
default: return QImage::Format::Format_Invalid;
}
}
static QImage::Format rgbFormatOf(const size_t bits)
{
switch (bits) {
case 1: return QImage::Format::Format_Mono;
case 12: return QImage::Format::Format_RGB444;
case 15: return QImage::Format::Format_RGB555;
case 16: return QImage::Format::Format_RGB16;
case 18: return QImage::Format::Format_RGB666;
case 24: return QImage::Format::Format_RGB888;
case 30: return QImage::Format::Format_RGB30;
case 32: return QImage::Format::Format_RGBX8888;
default: return QImage::Format::Format_Invalid;
}
}
static QImage::Format formatOf(const Channel &channel)
{
switch (channel.type) {
case ChannelType::DENSITY: return densityFormatOf(channel.bits);
case ChannelType::COLOR_RGB: return rgbFormatOf(channel.bits);
default: return QImage::Format_Invalid;
}
}
static QColor qcolorof(RGB32 rgb)
{
return QColor{rgb.r(), rgb.g(), rgb.b(), rgb.a()};
}
using RgbEncoder = QRgb (*)(RGB32 rgb);
static RgbEncoder encoderOf(QImage::Format format)
{
switch (format) {
case QImage::Format_Mono:
return [](RGB32 rgb) -> QRgb {
return rgb.a() != 0;
};
case QImage::Format_Grayscale8:
return [](RGB32 rgb) -> QRgb {
return rgb.a();
};
case QImage::Format_RGB444:
case QImage::Format_RGB555:
case QImage::Format_RGB16:
case QImage::Format_RGB666:
case QImage::Format_RGB888:
case QImage::Format_RGB30:
case QImage::Format_RGBX8888:
return [](RGB32 rgb) -> QRgb {
return qcolorof(rgb).rgb();
};
default: return nullptr;
}
}
template <mve::Axis AXIS>
static void writeSvxLayerToImage(
QImage &image, const mve::VoxelArray &voxels, const SvxLimits &lims, size_t slice, QImage::Format format)
{
auto encoder = encoderOf(format);
DEBUG_ASSERT_NOTNULL(encoder);
for (size_t u = 0; u < lims.u; ++u) {
for (size_t v = 0; v < lims.v; ++v) {
Vec3size xyz = svxProject<AXIS>({slice, u, v});
RGB32 rgb = voxels.getRGB(xyz);
image.setPixel(static_cast<int>(u), static_cast<int>(v), encoder(rgb));
}
}
}
template <mve::Axis AXIS>
ResultCode Serializer::writeChannel(miniz_cpp::zip_file &archive, const SimpleVoxels &svx, const Channel &channel)
{
if (channel.type == ChannelType::CUSTOM || channel.type == ChannelType::MATERIAL) {
err = {0, "Writing CUSTOM or MATERIAL channels is not supported"};
return ResultCode::WRITE_ERROR_UNSUPPORTED_FORMAT;
}
std::string fileNameFormat = channel.fullFileNameFormat();
const char *fileNameFormatCstr = fileNameFormat.c_str();
const mve::VoxelArray &voxels = svx.voxels;
const SvxLimits lims = svxLimitsOf<AXIS>({voxels.sizeX, voxels.sizeY, voxels.sizeZ});
const QImage::Format format = formatOf(channel);
if (format == QImage::Format_Invalid) {
err = {0, "Can't determine valid image format for the " + std::string{xmlNameOf(channel.type)} + " channel"};
return ResultCode::USER_ERROR_INVALID_COLOR_FORMAT;
}
const QImage baseImage{static_cast<int>(lims.u), static_cast<int>(lims.v), format};
QByteArray byteArray;
QBuffer buffer{&byteArray};
QImageWriter writer{&buffer, "png"};
writer.setQuality(100);
ALWAYS_ASSERT(writer.canWrite());
for (size_t slice = 0; slice < lims.slice; ++slice) {
std::string fileName = mve::str::format(fileNameFormatCstr, slice);
QImage sliceImage = baseImage;
LOG(SPAM, "Writing slice #" + std::to_string(slice) + ", named" + fileName);
writeSvxLayerToImage<AXIS>(sliceImage, voxels, lims, slice, format);
buffer.close();
writer.write(sliceImage);
archive.writestr(fileName, {byteArray.data(), static_cast<size_t>(byteArray.size())});
}
return ResultCode::OK;
}
ResultCode Serializer::toStream(std::ostream &stream, const SimpleVoxels &svx)
{
miniz_cpp::zip_file archive;
archive.writestr("manifest.xml", svx.manifest.toXml());
const mve::Axis axis = svx.manifest.slicesOrientation();
for (const Channel &channel : svx.manifest.channels) {
ResultCode code;
// here we constexpr-ify the axis parameter
// this allows for compiler optimizations and cleaner code further down the line
switch (axis) {
case mve::Axis::X: code = writeChannel<mve::Axis::X>(archive, svx, channel); break;
case mve::Axis::Y: code = writeChannel<mve::Axis::Y>(archive, svx, channel); break;
case mve::Axis::Z: code = writeChannel<mve::Axis::Z>(archive, svx, channel); break;
}
if (code != ResultCode::OK) {
return code;
}
}
archive.save(stream);
if (not stream.good()) {
err = {static_cast<u64>(stream.tellp()), "Stream was not left in a good() state after writing file"};
return ResultCode::WRITE_ERROR_IO_FAIL;
}
return ResultCode::OK;
}
} // namespace voxelio::svx
#ifndef SVX_HPP
#define SVX_HPP
#include "3rd_minizfwd.hpp"
#include "types.hpp"
#include "voxelio.hpp"
#include "core_structs/voxelarray.hpp"
#include "core_util/macro.hpp"
#include "core_util/vec.hpp"
namespace voxelio::svx {
enum class ChannelType {
/** Represents an approximation to the percent of voxel fill. */
DENSITY,
/** COLOR – RGB. */
COLOR_RGB,
/** Fraction of amount a given material used in a voxel (all densities must sum to 100% or 0%). */
MATERIAL,
/** Custom value without defined semantic meaning. */
CUSTOM
};
struct Channel {
/** The type of information stored in this channel. */
ChannelType type;
/** The number of bits used for the channel, default is 8 bits. */
size_t bits = 0;
/** Optional: The material or custom id of this channel. Not necessary for density or color. */
size_t id = 0;
/** The printf-pattern for the paths of slices belonging to this channel. */
std::string fileNameFormat = "%04d.png";
std::string fullFileNameFormat() const;
std::string toXml() const;
};
/** A struct representing the manifest of an SVX file. */
class Manifest {
public:
/** The specification version this manifest follows. */
static constexpr const char *VERSION = "1.0";
private:
/** The number of voxels in each direction. */
Vec3size gridSize_;
/** The origin of the grid in physical space in meters. */
Vec3f origin_ = {0, 0, 0};
/** The axis(X,Y,Z) orthogonal to the slices plane. */
mve::Axis slicesOrientation_;
/** Number of bits used per voxel for density (1-16). */
uint32_t subvoxelBits_;
/** The physical size of each voxel in meters. */
float voxelSize_;
public:
std::vector<Channel> channels{};
Manifest(Vec3size gridSize,
Vec3f origin = {0, 0, 0},
mve::Axis slicesOrientation = mve::Axis::Y,
uint32_t subvoxelBits = 1,
float voxelSize = 1 / 16.f)
: gridSize_{std::move(gridSize)}
, origin_{origin}
, slicesOrientation_{slicesOrientation}
, subvoxelBits_{subvoxelBits}
, voxelSize_{voxelSize}
{
ALWAYS_ASSERT_NE(this->gridSize_.x(), 0);
ALWAYS_ASSERT_NE(this->gridSize_.y(), 0);
ALWAYS_ASSERT_NE(this->gridSize_.z(), 0);
ALWAYS_ASSERT(subvoxelBits > 0 && subvoxelBits <= 16);
}
DEFAULT_MOVE_AND_COPY_CONSTRUCTORS_AND_ASSIGNMENTS(Manifest)
constexpr Vec3size gridSize() const
{
return gridSize_;
}
constexpr Vec3f origin() const
{
return origin_;
}
constexpr mve::Axis slicesOrientation() const
{
return slicesOrientation_;
}
constexpr uint32_t subvoxelBits() const
{
return subvoxelBits_;
}
constexpr float voxelSize() const
{
return voxelSize_;
}
bool matches(const mve::VoxelArray voxels) const;
std::string toXml() const;
};
struct SimpleVoxels {
Manifest manifest;
mve::VoxelArray voxels;
SimpleVoxels(Manifest manifest, mve::VoxelArray voxels) : manifest{std::move(manifest)}, voxels{std::move(voxels)}
{
ALWAYS_ASSERT(this->manifest.matches(this->voxels));
}
};
class Serializer : public AbstractSerializer {
public:
ResultCode toStream(std::ostream &stream, const SimpleVoxels &svx) noexcept(false);
private:
template <mve::Axis AXIS>
ResultCode writeChannel(miniz_cpp::zip_file &archive, const SimpleVoxels &svx, const Channel &channel);
};
} // namespace voxelio::svx
#endif // SVX_HPP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment