VOXELFORMAT: added stl read and write support

master
Martin Gerhardy 2022-03-08 16:56:59 +01:00
parent 6ea5d25908
commit 86f2563c55
11 changed files with 415 additions and 1 deletions

View File

@ -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

37
data/tests/ascii.stl Normal file
View File

@ -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

BIN
data/tests/cube.stl Normal file

Binary file not shown.

1
debian/changelog vendored
View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>();
}

View File

@ -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