VOXELFORMAT: added stl read and write support
parent
6ea5d25908
commit
86f2563c55
|
@ -11,7 +11,7 @@
|
|||
\fB@COMMANDLINE@\fP is a command line application that can convert several voxel
|
||||
volume formats into others. Supported formats are e.g. cub (CubeWorld), qb/qbt
|
||||
(Qubicle), vox (MagicaVoxel), vmx (VoxEdit Sandbox), kvx (Build engine), kv6 (SLAB6),
|
||||
binvox and others. It can also export to mesh formats like obj and ply with a number
|
||||
binvox and others. It can also export to mesh formats like obj, stl and ply with a number
|
||||
of options.
|
||||
.SH OPTIONS
|
||||
|
||||
|
@ -177,6 +177,8 @@ Chronovox (*.csm)
|
|||
Nicks Voxel Model (*.nvm)
|
||||
.TP
|
||||
Wavefront Object (*.obj)
|
||||
.TP
|
||||
Standard Triangle Language (*.stl)
|
||||
|
||||
.SH SAVE
|
||||
.TP
|
||||
|
@ -205,6 +207,8 @@ Qubicle Exchange (*.qef)
|
|||
Wavefront Object (*.obj)
|
||||
.TP
|
||||
Polygon File Format (*.ply)
|
||||
.TP
|
||||
Standard Triangle Language (*.stl)
|
||||
|
||||
.SH LAYERS
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
solid test STL
|
||||
facet normal -0.000000e+000 1.000000e+000 -0.000000e+000
|
||||
outer loop
|
||||
vertex -1.070657e+001 2.020000e+001 1.343576e+001
|
||||
vertex -1.116853e+001 2.020000e+001 1.345557e+001
|
||||
vertex -1.129289e+001 2.020000e+001 1.360711e+001
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0.000000e+000 1.000000e+000 0.000000e+000
|
||||
outer loop
|
||||
vertex -1.083594e+001 2.020000e+001 1.367780e+001
|
||||
vertex -1.070657e+001 2.020000e+001 1.343576e+001
|
||||
vertex -1.129289e+001 2.020000e+001 1.360711e+001
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0.000000e+000 9.999999e-001 -0.000000e+000
|
||||
outer loop
|
||||
vertex -1.083594e+001 2.020000e+001 1.367780e+001
|
||||
vertex -1.129289e+001 2.020000e+001 1.360711e+001
|
||||
vertex -1.144443e+001 2.020000e+001 1.373147e+001
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0.000000e+000 1.000000e+000 -0.000000e+000
|
||||
outer loop
|
||||
vertex -1.083594e+001 2.020000e+001 1.367780e+001
|
||||
vertex -1.144443e+001 2.020000e+001 1.373147e+001
|
||||
vertex -1.161732e+001 2.020000e+001 1.382388e+001
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0.000000e+000 1.000000e+000 -0.000000e+000
|
||||
outer loop
|
||||
vertex -1.083594e+001 2.020000e+001 1.367780e+001
|
||||
vertex -1.161732e+001 2.020000e+001 1.382388e+001
|
||||
vertex -1.180491e+001 2.020000e+001 1.388078e+001
|
||||
endloop
|
||||
endfacet
|
||||
endsolid test STL
|
Binary file not shown.
|
@ -10,6 +10,7 @@ vengi (0.0.19.0-1) UNRELEASED; urgency=low
|
|||
* The palette handling was refactored
|
||||
* Allow to save the MATL chunk in magicavoxel vox files
|
||||
* Ability to scale exported mesh with different values for each axis
|
||||
* Added stl voxelization support
|
||||
|
||||
* VoxEdit:
|
||||
* Added new command to fill hollows in models
|
||||
|
|
|
@ -21,6 +21,7 @@ General:
|
|||
- The palette handling was refactored
|
||||
- Allow to save the MATL chunk in magicavoxel vox files
|
||||
- Ability to scale exported mesh with different values for each axis
|
||||
- Added stl voxelization support
|
||||
|
||||
VoxEdit:
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
| SLAB6 | kv6 | X | | X | X |
|
||||
| Sproxel | csv | X | X | X | |
|
||||
| Wavefront Object | obj | X | X | | |
|
||||
| Standard Triangle Language | stl | X | X | | |
|
||||
|
||||
|
||||
## Meshes
|
||||
|
|
|
@ -31,6 +31,7 @@ set(SRCS
|
|||
SceneGraph.h SceneGraph.cpp
|
||||
SceneGraphNode.h SceneGraphNode.cpp
|
||||
SproxelFormat.h SproxelFormat.cpp
|
||||
STLFormat.h STLFormat.cpp
|
||||
VolumeCache.h VolumeCache.cpp
|
||||
VoxFormat.h VoxFormat.cpp
|
||||
VoxOldFormat.h VoxOldFormat.cpp
|
||||
|
@ -56,6 +57,7 @@ set(TEST_SRCS
|
|||
tests/QEFFormatTest.cpp
|
||||
tests/SceneGraphTest.cpp
|
||||
tests/SproxelFormatTest.cpp
|
||||
tests/STLFormatTest.cpp
|
||||
tests/VoxFormatTest.cpp
|
||||
tests/VXLFormatTest.cpp
|
||||
tests/VXRFormatTest.cpp
|
||||
|
@ -82,6 +84,8 @@ set(TEST_FILES
|
|||
tests/magicavoxel.vox
|
||||
tests/cube.obj
|
||||
tests/cube.mtl
|
||||
tests/cube.stl
|
||||
tests/ascii.stl
|
||||
tests/test.gox
|
||||
tests/test.vxm
|
||||
tests/test2.vxm
|
||||
|
|
|
@ -0,0 +1,278 @@
|
|||
/**
|
||||
* @file
|
||||
*/
|
||||
|
||||
#include "STLFormat.h"
|
||||
#include "core/Color.h"
|
||||
#include "core/FourCC.h"
|
||||
#include "core/Log.h"
|
||||
#include "core/Var.h"
|
||||
#include "voxel/Mesh.h"
|
||||
#include <SDL_stdinc.h>
|
||||
|
||||
namespace voxel {
|
||||
|
||||
namespace priv {
|
||||
static constexpr const size_t BinaryHeaderSize = 80;
|
||||
}
|
||||
|
||||
void STLFormat::subdivideShape(const core::DynamicArray<Face> &faces, core::DynamicArray<Tri> &subdivided) {
|
||||
const float scale = core::Var::getSafe(cfg::VoxformatScale)->floatVal();
|
||||
|
||||
float scaleX = core::Var::getSafe(cfg::VoxformatScaleX)->floatVal();
|
||||
float scaleY = core::Var::getSafe(cfg::VoxformatScaleY)->floatVal();
|
||||
float scaleZ = core::Var::getSafe(cfg::VoxformatScaleZ)->floatVal();
|
||||
|
||||
scaleX = scaleX != 1.0f ? scaleX : scale;
|
||||
scaleY = scaleY != 1.0f ? scaleY : scale;
|
||||
scaleZ = scaleZ != 1.0f ? scaleZ : scale;
|
||||
|
||||
for (const Face &face : faces) {
|
||||
Tri tri;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
tri.vertices[i].x = face.tri[i].x * scaleX;
|
||||
tri.vertices[i].y = face.tri[i].y * scaleY;
|
||||
tri.vertices[i].z = face.tri[i].z * scaleZ;
|
||||
tri.uv[i] = glm::vec2(0.0f);
|
||||
}
|
||||
|
||||
subdivideTri(tri, subdivided);
|
||||
}
|
||||
}
|
||||
|
||||
void STLFormat::calculateAABB(const core::DynamicArray<Face> &faces, glm::vec3 &mins, glm::vec3 &maxs) {
|
||||
maxs = glm::vec3(-100000.0f);
|
||||
mins = glm::vec3(+100000.0f);
|
||||
|
||||
for (const Face &face : faces) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
maxs.x = core_max(maxs.x, face.tri[i].x);
|
||||
maxs.y = core_max(maxs.y, face.tri[i].y);
|
||||
maxs.z = core_max(maxs.z, face.tri[i].z);
|
||||
mins.x = core_min(mins.x, face.tri[i].x);
|
||||
mins.y = core_min(mins.y, face.tri[i].y);
|
||||
mins.z = core_min(mins.z, face.tri[i].z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool STLFormat::parseAscii(io::SeekableReadStream &stream, core::DynamicArray<Face> &faces) {
|
||||
char line[512];
|
||||
stream.seek(0);
|
||||
while (stream.readLine(sizeof(line), line)) {
|
||||
if (!strncmp(line, "solid", 5)) {
|
||||
while (stream.readLine(sizeof(line), line)) {
|
||||
const char *ptr = line;
|
||||
while (*ptr == ' ') {
|
||||
++ptr;
|
||||
}
|
||||
if (!strncmp(ptr, "endsolid", 8)) {
|
||||
break;
|
||||
}
|
||||
if (!strncmp(ptr, "facet", 5)) {
|
||||
Face face;
|
||||
glm::vec3 &norm = face.normal;
|
||||
if (SDL_sscanf(ptr, "facet normal %f %f %f", &norm.x, &norm.y, &norm.z) != 3) {
|
||||
Log::error("Failed to parse facet normal");
|
||||
return false;
|
||||
}
|
||||
if (!stream.readLine(sizeof(line), line)) {
|
||||
return false;
|
||||
}
|
||||
ptr = line;
|
||||
while (*ptr == ' ') {
|
||||
++ptr;
|
||||
}
|
||||
if (!strncmp(ptr, "outer loop", 10)) {
|
||||
int vi = 0;
|
||||
while (stream.readLine(sizeof(line), line)) {
|
||||
ptr = line;
|
||||
while (*ptr == ' ') {
|
||||
++ptr;
|
||||
}
|
||||
if (!strncmp(ptr, "endloop", 7)) {
|
||||
break;
|
||||
}
|
||||
glm::vec3 &vert = face.tri[vi];
|
||||
if (SDL_sscanf(ptr, "vertex %f %f %f", &vert.x, &vert.y, &vert.z) != 3) {
|
||||
Log::error("Failed to parse vertex");
|
||||
return false;
|
||||
}
|
||||
++vi;
|
||||
}
|
||||
if (vi != 3) {
|
||||
return false;
|
||||
}
|
||||
faces.push_back(face);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#define wrap(read) \
|
||||
if ((read) != 0) { \
|
||||
Log::error("Failed to read stl " CORE_STRINGIFY(read)); \
|
||||
return false; \
|
||||
}
|
||||
|
||||
bool STLFormat::parseBinary(io::SeekableReadStream &stream, core::DynamicArray<Face> &faces) {
|
||||
stream.seek(priv::BinaryHeaderSize);
|
||||
uint32_t numFaces = 0;
|
||||
wrap(stream.readUInt32(numFaces))
|
||||
Log::debug("faces: %u", numFaces);
|
||||
if (numFaces == 0) {
|
||||
Log::error("No faces in stl file");
|
||||
return false;
|
||||
}
|
||||
faces.reserve(numFaces);
|
||||
for (uint32_t fn = 0; fn < numFaces; ++fn) {
|
||||
Face face{};
|
||||
wrap(stream.readFloat(face.normal.x))
|
||||
wrap(stream.readFloat(face.normal.y))
|
||||
wrap(stream.readFloat(face.normal.z))
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
wrap(stream.readFloat(face.tri[i].x))
|
||||
wrap(stream.readFloat(face.tri[i].y))
|
||||
wrap(stream.readFloat(face.tri[i].z))
|
||||
}
|
||||
stream.skip(2);
|
||||
faces.push_back(face);
|
||||
}
|
||||
|
||||
return !faces.empty();
|
||||
}
|
||||
|
||||
bool STLFormat::loadGroups(const core::String &filename, io::SeekableReadStream &stream, SceneGraph &sceneGraph) {
|
||||
uint32_t magic;
|
||||
wrap(stream.readUInt32(magic));
|
||||
const bool ascii = FourCC('s', 'o', 'l', 'i') == magic;
|
||||
|
||||
core::DynamicArray<Face> faces;
|
||||
if (ascii) {
|
||||
Log::debug("found ascii format");
|
||||
if (!parseAscii(stream, faces)) {
|
||||
Log::error("Failed to parse ascii stl file %s", filename.c_str());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
Log::debug("found binary format");
|
||||
if (!parseBinary(stream, faces)) {
|
||||
Log::error("Failed to parse binary stl file %s", filename.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 mins;
|
||||
glm::vec3 maxs;
|
||||
calculateAABB(faces, mins, maxs);
|
||||
voxel::Region region(glm::floor(mins), glm::ceil(maxs));
|
||||
if (!region.isValid()) {
|
||||
Log::error("Invalid region: %s", region.toString().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (glm::any(glm::greaterThan(region.getDimensionsInVoxels(), glm::ivec3(512)))) {
|
||||
Log::warn("Large meshes will take a lot of time and use a lot of memory. Consider scaling the mesh!");
|
||||
}
|
||||
RawVolume *volume = new RawVolume(region);
|
||||
SceneGraphNode node;
|
||||
node.setVolume(volume, true);
|
||||
node.setName(filename);
|
||||
core::DynamicArray<Tri> subdivided;
|
||||
subdivideShape(faces, subdivided);
|
||||
voxelizeTris(volume, subdivided);
|
||||
sceneGraph.emplace(core::move(node));
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef wrap
|
||||
|
||||
bool STLFormat::writeVertex(io::SeekableWriteStream &stream, const MeshExt &meshExt, const voxel::VoxelVertex &v1, const glm::vec3 &offset, const glm::vec3 &scale) {
|
||||
glm::vec3 pos;
|
||||
if (meshExt.applyTransform) {
|
||||
pos = meshExt.transform.apply(v1.position, meshExt.size);
|
||||
} else {
|
||||
pos = v1.position;
|
||||
}
|
||||
pos = (offset + pos) * scale;
|
||||
if (!stream.writeFloat(pos.x)) {
|
||||
return false;
|
||||
}
|
||||
if (!stream.writeFloat(pos.y)) {
|
||||
return false;
|
||||
}
|
||||
if (!stream.writeFloat(pos.z)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool STLFormat::saveMeshes(const Meshes &meshes, const core::String &filename, io::SeekableWriteStream &stream,
|
||||
const glm::vec3 &scale, bool quad, bool withColor, bool withTexCoords) {
|
||||
stream.writeStringFormat(false, "github.com/mgerhardy/vengi");
|
||||
const size_t delta = priv::BinaryHeaderSize - stream.pos();
|
||||
for (size_t i = 0; i < delta; ++i) {
|
||||
stream.writeUInt8(0);
|
||||
}
|
||||
|
||||
int faceCount = 0;
|
||||
for (const auto &meshExt : meshes) {
|
||||
const voxel::Mesh *mesh = meshExt.mesh;
|
||||
const int ni = (int)mesh->getNoOfIndices();
|
||||
if (ni % 3 != 0) {
|
||||
Log::error("Unexpected indices amount");
|
||||
return false;
|
||||
}
|
||||
faceCount += ni / 3;
|
||||
}
|
||||
stream.writeUInt32(faceCount);
|
||||
|
||||
int idxOffset = 0;
|
||||
for (const auto &meshExt : meshes) {
|
||||
const voxel::Mesh *mesh = meshExt.mesh;
|
||||
Log::debug("Exporting layer %s", meshExt.name.c_str());
|
||||
const int nv = (int)mesh->getNoOfVertices();
|
||||
const int ni = (int)mesh->getNoOfIndices();
|
||||
const glm::vec3 offset(mesh->getOffset());
|
||||
const voxel::VoxelVertex *vertices = mesh->getRawVertexData();
|
||||
const voxel::IndexType *indices = mesh->getRawIndexData();
|
||||
|
||||
for (int i = 0; i < ni; i += 3) {
|
||||
const uint32_t one = idxOffset + indices[i + 0] + 1;
|
||||
const uint32_t two = idxOffset + indices[i + 1] + 1;
|
||||
const uint32_t three = idxOffset + indices[i + 2] + 1;
|
||||
|
||||
const voxel::VoxelVertex &v1 = vertices[one];
|
||||
const voxel::VoxelVertex &v2 = vertices[two];
|
||||
const voxel::VoxelVertex &v3 = vertices[three];
|
||||
|
||||
// normal
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
if (!stream.writeFloat(0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!writeVertex(stream, meshExt, v1, offset, scale)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!writeVertex(stream, meshExt, v2, offset, scale)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!writeVertex(stream, meshExt, v3, offset, scale)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream.writeUInt16(0);
|
||||
}
|
||||
idxOffset += nv;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace voxel
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* @file
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "MeshExporter.h"
|
||||
|
||||
namespace voxel {
|
||||
|
||||
class VoxelVertex;
|
||||
|
||||
/**
|
||||
* @brief Standard Triangle Language
|
||||
*
|
||||
* @p Binary
|
||||
* UINT8[80] – Header
|
||||
* UINT32 – Number of triangles
|
||||
* foreach triangle
|
||||
* REAL32[3] – Normal vector
|
||||
* REAL32[3] – Vertex 1
|
||||
* REAL32[3] – Vertex 2
|
||||
* REAL32[3] – Vertex 3
|
||||
* UINT16 – Attribute byte count
|
||||
* end
|
||||
*/
|
||||
class STLFormat : public MeshExporter {
|
||||
private:
|
||||
struct Face {
|
||||
glm::vec3 normal {};
|
||||
glm::vec3 tri[3] {};
|
||||
uint16_t attribute = 0;
|
||||
};
|
||||
|
||||
static void calculateAABB(const core::DynamicArray<Face> &faces, glm::vec3 &mins, glm::vec3 &maxs);
|
||||
static void subdivideShape(const core::DynamicArray<Face> &faces, core::DynamicArray<Tri> &subdivided);
|
||||
|
||||
bool writeVertex(io::SeekableWriteStream &stream, const MeshExt &meshExt, const voxel::VoxelVertex &v1, const glm::vec3 &offset, const glm::vec3 &scale);
|
||||
|
||||
bool parseBinary(io::SeekableReadStream &stream, core::DynamicArray<Face> &faces);
|
||||
bool parseAscii(io::SeekableReadStream &stream, core::DynamicArray<Face> &faces);
|
||||
|
||||
public:
|
||||
bool saveMeshes(const Meshes &meshes, const core::String &filename, io::SeekableWriteStream &stream,
|
||||
const glm::vec3 &scale, bool quad, bool withColor, bool withTexCoords) override;
|
||||
/**
|
||||
* @brief Voxelizes the input mesh
|
||||
*/
|
||||
bool loadGroups(const core::String &filename, io::SeekableReadStream &stream, SceneGraph &sceneGraph) override;
|
||||
};
|
||||
} // namespace voxel
|
|
@ -27,6 +27,7 @@
|
|||
#include "voxelformat/VXLFormat.h"
|
||||
#include "voxelformat/CubFormat.h"
|
||||
#include "voxelformat/GoxFormat.h"
|
||||
#include "voxelformat/STLFormat.h"
|
||||
#include "voxelformat/BinVoxFormat.h"
|
||||
#include "voxelformat/KVXFormat.h"
|
||||
#include "voxelformat/KV6Format.h"
|
||||
|
@ -62,6 +63,7 @@ const io::FormatDescription SUPPORTED_VOXEL_FORMATS_LOAD[] = {
|
|||
{"Minecraft region", "mca", nullptr, VOX_FORMAT_FLAG_PALETTE_EMBEDDED},
|
||||
{"Sproxel csv", "csv", nullptr, 0u},
|
||||
{"Wavefront Object", "obj", nullptr, 0u},
|
||||
{"Standard Triangle Language", "stl", nullptr, 0u},
|
||||
{"Build engine", "kvx", nullptr, VOX_FORMAT_FLAG_PALETTE_EMBEDDED},
|
||||
{"Ace of Spades", "kv6", [] (uint32_t magic) {return magic == FourCC('K','v','x','l');}, VOX_FORMAT_FLAG_PALETTE_EMBEDDED},
|
||||
{"Tiberian Sun", "vxl", [] (uint32_t magic) {return magic == FourCC('V','o','x','e');}, VOX_FORMAT_FLAG_PALETTE_EMBEDDED},
|
||||
|
@ -176,6 +178,8 @@ static core::SharedPtr<voxel::Format> getFormat(const io::FormatDescription *des
|
|||
format = core::make_shared<voxel::QBCLFormat>();
|
||||
} else if (ext == "obj") {
|
||||
format = core::make_shared<voxel::OBJFormat>();
|
||||
} else if (ext == "stl") {
|
||||
format = core::make_shared<voxel::STLFormat>();
|
||||
} else if (ext == "ply") {
|
||||
format = core::make_shared<voxel::PLYFormat>();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* @file
|
||||
*/
|
||||
|
||||
#include "voxelformat/STLFormat.h"
|
||||
#include "AbstractVoxFormatTest.h"
|
||||
#include "io/File.h"
|
||||
|
||||
namespace voxel {
|
||||
|
||||
class STLFormatTest : public AbstractVoxFormatTest {};
|
||||
|
||||
TEST_F(STLFormatTest, testVoxelizeAscii) {
|
||||
STLFormat f;
|
||||
const core::String filename = "ascii.stl";
|
||||
const io::FilePtr &file = open(filename);
|
||||
io::FileStream stream(file);
|
||||
SceneGraph sceneGraph;
|
||||
EXPECT_TRUE(f.loadGroups(filename, stream, sceneGraph));
|
||||
EXPECT_TRUE(sceneGraph.size() > 0);
|
||||
}
|
||||
|
||||
TEST_F(STLFormatTest, testVoxelizeCube) {
|
||||
STLFormat f;
|
||||
const core::String filename = "cube.stl";
|
||||
const io::FilePtr &file = open(filename);
|
||||
io::FileStream stream(file);
|
||||
SceneGraph sceneGraph;
|
||||
EXPECT_TRUE(f.loadGroups(filename, stream, sceneGraph));
|
||||
EXPECT_TRUE(sceneGraph.size() > 0);
|
||||
}
|
||||
|
||||
} // namespace voxel
|
Loading…
Reference in New Issue