VOXELFORMAT: added vxl (command & conquer) voxel model support

master
Martin Gerhardy 2020-06-14 12:51:13 +02:00
parent a9bf0d6e12
commit 1108506c1d
11 changed files with 361 additions and 3 deletions

View File

@ -12,6 +12,7 @@ A more detailed changelog can be found at: https://github.com/mgerhardy/engine/c
- Improved scene graph support for Magicavoxel vox files
- Fixed invisible voxels for qb and qbt (Qubicle) volume format
- Support automatic loading different volume formats for assets
- Support loading Command&Conquer vxl files
#### 0.0.4 (2020-06-07)

View File

@ -1,4 +1,4 @@
[Thumbnailer Entry]
TryExec=@CMAKE_PROJECT_NAME@-@NAME@
Exec=@CMAKE_PROJECT_NAME@-@NAME@ -s %s %i %o
MimeType=application/x-slab6;application/x-build-engine;application/x-cubeworld;application/x-binvox;application/x-magicavoxel;application/x-qubicle-binary;application/x-qubicle-tree;application/x-sandbox-voxedit
MimeType=application/x-cc-vxl;application/x-slab6;application/x-build-engine;application/x-cubeworld;application/x-binvox;application/x-magicavoxel;application/x-qubicle-binary;application/x-qubicle-tree;application/x-sandbox-voxedit

View File

@ -10,4 +10,4 @@ Type=Application
Keywords=3d;modeling;game;voxel;engine;pixel;art
Categories=Graphics
StartupNotify=true
MimeType=application/x-slab6;application/x-build-engine;application/x-cubeworld;application/x-binvox;application/x-magicavoxel;application/x-qubicle-binary;application/x-qubicle-tree;application/x-sandbox-voxedit
MimeType=application/x-cc-vxl;application/x-slab6;application/x-build-engine;application/x-cubeworld;application/x-binvox;application/x-magicavoxel;application/x-qubicle-binary;application/x-qubicle-tree;application/x-sandbox-voxedit

View File

@ -41,4 +41,8 @@
</magic>
<glob pattern="*.kv6"/>
</mime-type>
<mime-type type="application/x-cc-vxl">
<comment>Voxel model used by Command and Conquer</comment>
<glob pattern="*.vxl"/>
</mime-type>
</mime-info>

BIN
data/tests/cc.vxl Normal file

Binary file not shown.

1
debian/changelog vendored
View File

@ -10,6 +10,7 @@ vengi (0.0.5.0-1) UNRELEASED; urgency=low
* Improved scene graph support for Magicavoxel vox files
* Fixed invisible voxels for qb and qbt (Qubicle) volume format
* Support automatic loading different volume formats for assets
* Support loading Command&Conquer vxl files
-- Martin Gerhardy <martin.gerhardy@gmail.com> Sun, 7 Jun 2020 10:32:10 +0200

View File

@ -8,6 +8,7 @@ set(SRCS
QBTFormat.h QBTFormat.cpp
QBFormat.h QBFormat.cpp
VXMFormat.h VXMFormat.cpp
VXLFormat.h VXLFormat.cpp
MeshCache.h MeshCache.cpp
CubFormat.h CubFormat.cpp
VolumeCache.h VolumeCache.cpp
@ -25,11 +26,13 @@ set(TEST_SRCS
tests/CubFormatTest.cpp
tests/KVXFormatTest.cpp
tests/KV6FormatTest.cpp
tests/VXLFormatTest.cpp
tests/VXMFormatTest.cpp
)
set(TEST_FILES
tests/qubicle.qb
tests/qubicle.qbt
tests/cc.vxl
tests/test.binvox
tests/test.kvx
tests/test.kv6

View File

@ -8,6 +8,7 @@
#include "QBTFormat.h"
#include "QBFormat.h"
#include "VXMFormat.h"
#include "VXLFormat.h"
#include "CubFormat.h"
#include "BinVoxFormat.h"
#include "KVXFormat.h"
@ -15,7 +16,7 @@
namespace voxelformat {
const char *SUPPORTED_VOXEL_FORMATS_LOAD = "vox,qbt,qb,vxm,binvox,cub,kvx,kv6";
const char *SUPPORTED_VOXEL_FORMATS_LOAD = "vox,qbt,qb,vxm,binvox,cub,kvx,kv6,vxl";
const char *SUPPORTED_VOXEL_FORMATS_LOAD_LIST[] = { "qb", "vox", nullptr };
const char *SUPPORTED_VOXEL_FORMATS_SAVE = "vox,qbt,qb,binvox,cub";
@ -60,6 +61,11 @@ bool loadVolumeFormat(const io::FilePtr& filePtr, voxel::VoxelVolumes& newVolume
if (!f.loadGroups(filePtr, newVolumes)) {
voxelformat::clearVolumes(newVolumes);
}
} else if (ext == "vxl") {
voxel::VXLFormat f;
if (!f.loadGroups(filePtr, newVolumes)) {
voxelformat::clearVolumes(newVolumes);
}
} else if (ext == "binvox") {
voxel::BinVoxFormat f;
if (!f.loadGroups(filePtr, newVolumes)) {

View File

@ -0,0 +1,245 @@
/**
* @file
*/
#include "VXLFormat.h"
#include "core/Common.h"
#include "core/Color.h"
#include "core/Assert.h"
#include "core/io/FileStream.h"
#include "core/Log.h"
#include "voxel/MaterialColor.h"
#include "core/collection/Buffer.h"
#include <SDL_assert.h>
namespace voxel {
#define wrap(read) \
if (read != 0) { \
Log::debug("Error: " CORE_STRINGIFY(read) " at " SDL_FILE ":%i", SDL_LINE); \
return false; \
}
#define wrapBool(read) \
if (!(read)) { \
Log::debug("Error: " CORE_STRINGIFY(read) " at " SDL_FILE ":%i", SDL_LINE); \
return false; \
}
bool VXLFormat::saveGroups(const VoxelVolumes& volumes, const io::FilePtr& file) {
return false;
}
bool VXLFormat::readLimb(io::FileStream& stream, vxl_mdl& mdl, uint32_t limbIdx, VoxelVolumes& volumes) const {
const vxl_limb_tailer &footer = mdl.limb_tailers[limbIdx];
const vxl_limb_header &header = mdl.limb_headers[limbIdx];
const uint32_t baseSize = footer.xsize * footer.ysize;
core::Buffer<int32_t> colStart(baseSize);
// switch axis
RawVolume *volume = new RawVolume(Region{0, 0, 0, footer.xsize, footer.zsize, footer.ysize});
volumes[mdl.volumeIdx].volume = volume;
volumes[mdl.volumeIdx].name = header.limb_name;
volumes[mdl.volumeIdx].pivot = volume->region().getCenter();
++mdl.volumeIdx;
wrap(stream.seek(802 + 28 * mdl.header.n_limbs + footer.span_start_off))
for (uint32_t i = 0; i < baseSize; i++) {
uint32_t v;
wrap(stream.readInt(v))
colStart[i] = *(int32_t*)&v;
}
stream.skip(4 * baseSize);
const uint64_t dataStart = stream.pos();
// Count the voxels in this limb
uint32_t voxelCount = 0;
for (uint32_t i = 0u; i < baseSize; ++i) {
// Empty column
if (colStart[i] == -1) {
continue;
}
wrap(stream.seek(dataStart + colStart[i]))
uint32_t z = 0;
do {
uint8_t v;
wrap(stream.readByte(v))
z += v;
wrap(stream.readByte(v))
z += v;
voxelCount += v;
stream.skip(2 * v + 1);
} while (z < footer.zsize);
}
// Read the data
for (uint32_t i = 0u; i < baseSize; ++i) {
// Empty column
if (colStart[i] == -1) {
continue;
}
wrap(stream.seek(dataStart + colStart[i]))
uint8_t x = (uint8_t)(i % footer.xsize);
uint8_t y = (uint8_t)(i / footer.xsize);
uint8_t z = 0;
do {
uint8_t v;
wrap(stream.readByte(v))
z += v;
uint8_t count;
wrap(stream.readByte(count))
for (uint8_t j = 0u; j < count; ++j) {
uint8_t color;
wrap(stream.readByte(color))
uint8_t normal;
wrap(stream.readByte(normal))
const uint8_t palIdx = convertPaletteIndex(color);
const voxel::Voxel v = voxel::createColorVoxel(voxel::VoxelType::Generic, palIdx);
volume->setVoxel(x, z, y, v);
++z;
}
// Skip duplicate count
stream.skip(1);
} while (z < footer.zsize);
}
return true;
}
bool VXLFormat::readLimbs(io::FileStream& stream, vxl_mdl& mdl, VoxelVolumes& volumes) const {
volumes.resize(mdl.header.n_limbs);
for (uint32_t i = 0; i < mdl.header.n_limbs; ++i) {
wrapBool(readLimb(stream, mdl, i, volumes))
}
return true;
}
bool VXLFormat::readLimbHeader(io::FileStream& stream, vxl_mdl& mdl, uint32_t limbIdx) const {
vxl_limb_header &header = mdl.limb_headers[limbIdx];
wrapBool(stream.readString(sizeof(header.limb_name), header.limb_name))
Log::debug("Limb %u name: %s", limbIdx, header.limb_name);
wrap(stream.readInt(header.unknown))
wrap(stream.readInt(header.limb_number))
wrap(stream.readInt(header.unknown))
wrap(stream.readInt(header.unknown2))
return true;
}
bool VXLFormat::readLimbHeaders(io::FileStream& stream, vxl_mdl& mdl) const {
// 802 is the unpadded size of vxl_header
stream.seek(802);
for (uint32_t i = 0; i < mdl.header.n_limbs; ++i) {
wrapBool(readLimbHeader(stream, mdl, i))
}
return true;
}
bool VXLFormat::readLimbFooter(io::FileStream& stream, vxl_mdl& mdl, uint32_t limbIdx) const {
vxl_limb_tailer &footer = mdl.limb_tailers[limbIdx];
wrap(stream.readInt(footer.span_start_off))
wrap(stream.readInt(footer.span_end_off))
wrap(stream.readInt(footer.span_data_off))
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
wrap(stream.readFloat(footer.transform[i][j]))
}
}
for (int i = 0; i < 3; ++i) {
wrap(stream.readFloat(footer.scale[i]))
}
wrap(stream.readByte(footer.xsize))
wrap(stream.readByte(footer.ysize))
wrap(stream.readByte(footer.zsize))
wrap(stream.readByte(footer.type))
return true;
}
bool VXLFormat::readLimbFooters(io::FileStream& stream, vxl_mdl& mdl) const {
// 802 is the unpadded size of vxl_header
// 28 is the unpadded size of vxl_limb_header
stream.seek(802 + 28 * mdl.header.n_limbs + mdl.header.bodysize);
for (uint32_t i = 0; i < mdl.header.n_limbs; ++i) {
wrapBool(readLimbFooter(stream, mdl, i))
}
return true;
}
bool VXLFormat::readHeader(io::FileStream& stream, vxl_mdl& mdl) {
wrapBool(stream.readString(sizeof(mdl.header.filetype), mdl.header.filetype))
if (SDL_strcmp(mdl.header.filetype, "Voxel Animation")) {
return false;
}
wrap(stream.readInt(mdl.header.unknown))
wrap(stream.readInt(mdl.header.n_limbs))
wrap(stream.readInt(mdl.header.n_limbs2))
wrap(stream.readInt(mdl.header.bodysize))
wrap(stream.readShort(mdl.header.unknown2))
_paletteSize = 256;
_palette.resize(_paletteSize);
bool valid = false;
for (uint32_t i = 0; i < _paletteSize; ++i) {
vxl_header& hdr = mdl.header;
wrap(stream.readByte(hdr.palette[i][0]))
wrap(stream.readByte(hdr.palette[i][1]))
wrap(stream.readByte(hdr.palette[i][2]))
if (hdr.palette[i][0] != 0 || hdr.palette[i][1] != 0 || hdr.palette[i][2] != 0) {
valid = true;
}
}
if (valid) {
// convert to our palette
const MaterialColorArray& materialColors = getMaterialColors();
for (uint32_t i = 0; i < _paletteSize; ++i) {
const uint8_t *p = mdl.header.palette[i];
const glm::vec4& color = core::Color::fromRGBA(p[0], p[1], p[2], 0xff);
const int index = core::Color::getClosestMatch(color, materialColors);
Log::info("Convert color %i:%i:%i to index: %i", p[0], p[1], p[2], index);
_palette[i] = index;
}
} else {
_paletteSize = 0;
}
return true;
}
bool VXLFormat::prepareModel(vxl_mdl& mdl) const {
if (mdl.header.n_limbs > 512) {
Log::error("Limb size exceeded the max allowed value of 512: %u", mdl.header.n_limbs);
return false;
}
mdl.limb_headers = new vxl_limb_header[mdl.header.n_limbs];
mdl.limb_bodies = new vxl_limb_body[mdl.header.n_limbs];
mdl.limb_tailers = new vxl_limb_tailer[mdl.header.n_limbs];
return true;
}
bool VXLFormat::loadGroups(const io::FilePtr& file, VoxelVolumes& volumes) {
if (!(bool)file || !file->exists()) {
Log::error("Could not load vxl file: File doesn't exist");
return false;
}
io::FileStream stream(file.get());
vxl_mdl mdl;
wrapBool(readHeader(stream, mdl))
wrapBool(prepareModel(mdl))
wrapBool(readLimbHeaders(stream, mdl))
wrapBool(readLimbFooters(stream, mdl))
wrapBool(readLimbs(stream, mdl, volumes))
return true;
}
#undef wrap
#undef wrapBool
}

View File

@ -0,0 +1,76 @@
/**
* @file
*/
#pragma once
#include "VoxFileFormat.h"
#include "core/io/FileStream.h"
namespace voxel {
/**
* @brief Tiberian Sun Voxel Animation Format
*
* http://xhp.xwis.net/documents/VXL_Format.txt
*/
class VXLFormat : public VoxFileFormat {
private:
struct vxl_limb_header {
char limb_name[16]; /* ASCIIZ string - name of section */
uint32_t limb_number; /* Limb number */
uint32_t unknown; /* Always 1 */
uint32_t unknown2; /* Always 0 */
};
struct vxl_limb_body {
uint32_t *span_start; /* List of span start addresses or -1 - number of limb times */
uint32_t *span_end; /* List of span end addresses or -1 - number of limb times */
uint8_t *span_data; /* Byte data for each span length */
};
struct vxl_header {
char filetype[16]; /* ASCIIZ string - "Voxel Animation" */
uint32_t unknown; /* Always 1 - number of animation frames? */
uint32_t n_limbs; /* Number of limb headers/bodies/tailers */
uint32_t n_limbs2; /* Always the same as n_limbs */
uint32_t bodysize; /* Total size in bytes of all limb bodies */
uint16_t unknown2; /* Always 0x1f10 - ID or end of header code? */
uint8_t palette[256][3]; /* 256 colour palette for the voxel in RGB format */
};
struct vxl_limb_tailer {
uint32_t span_start_off; /* Offset into body section to span start list */
uint32_t span_end_off; /* Offset into body section to span end list */
uint32_t span_data_off; /* Offset into body section to span data */
float transform[4][4]; /* Inverse(?) transformation matrix */
float scale[3]; /* Scaling vector for the image */
uint8_t xsize; /* Width of the voxel limb */
uint8_t ysize; /* Breadth of the voxel limb */
uint8_t zsize; /* Height of the voxel limb */
uint8_t type; /* Always 2? type? unknown */
};
struct vxl_mdl {
~vxl_mdl() {
delete[] limb_headers;
delete[] limb_bodies;
delete[] limb_tailers;
}
vxl_header header;
vxl_limb_header *limb_headers = nullptr; /* number of limb times */
vxl_limb_body *limb_bodies = nullptr; /* number of limb times */
vxl_limb_tailer *limb_tailers = nullptr; /* number of limb times */
int volumeIdx = 0;
};
bool readLimbHeader(io::FileStream& stream, vxl_mdl& mdl, uint32_t limbIdx) const;
bool readLimbFooter(io::FileStream& stream, vxl_mdl& mdl, uint32_t limbIdx) const;
bool readLimb(io::FileStream& stream, vxl_mdl& mdl, uint32_t limbIdx, VoxelVolumes& volumes) const;
bool readLimbs(io::FileStream& stream, vxl_mdl& mdl, VoxelVolumes& volumes) const;
bool readLimbFooters(io::FileStream& stream, vxl_mdl& mdl) const;
bool readLimbHeaders(io::FileStream& stream, vxl_mdl& mdl) const;
bool prepareModel(vxl_mdl& mdl) const;
bool readHeader(io::FileStream& stream, vxl_mdl& mdl);
public:
bool loadGroups(const io::FilePtr& file, VoxelVolumes& volumes) override;
bool saveGroups(const VoxelVolumes& volumes, const io::FilePtr& file) override;
};
}

View File

@ -0,0 +1,22 @@
/**
* @file
*/
#include "AbstractVoxFormatTest.h"
#include "voxelformat/VXLFormat.h"
namespace voxel {
class VXLFormatTest: public AbstractVoxFormatTest {
};
TEST_F(VXLFormatTest, testLoad) {
const io::FilePtr& file = _testApp->filesystem()->open("cc.vxl");
ASSERT_TRUE((bool)file) << "Could not open vxl file";
VXLFormat f;
RawVolume* volume = f.load(file);
ASSERT_NE(nullptr, volume) << "Could not load vxl file";
delete volume;
}
}