VOXELFORMAT: added vxl (command & conquer) voxel model support
parent
a9bf0d6e12
commit
1108506c1d
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue