Skip to content

Commit

Permalink
VOXELFORMAT: added stl read and write support
Browse files Browse the repository at this point in the history
  • Loading branch information
mgerhardy committed Mar 8, 2022
1 parent 6ea5d25 commit 86f2563
Show file tree
Hide file tree
Showing 11 changed files with 415 additions and 1 deletion.
6 changes: 5 additions & 1 deletion contrib/installer/linux/voxconvert.man.in
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -177,6 +177,8 @@ Chronovox (*.csm)
Nicks Voxel Model (*.nvm)
.TP
Wavefront Object (*.obj)
.TP
Standard Triangle Language (*.stl)

.SH SAVE
.TP
Expand Down Expand Up @@ -205,6 +207,8 @@ Qubicle Exchange (*.qef)
Wavefront Object (*.obj)
.TP
Polygon File Format (*.ply)
.TP
Standard Triangle Language (*.stl)

.SH LAYERS

Expand Down
37 changes: 37 additions & 0 deletions data/tests/ascii.stl
Original file line number Diff line number Diff line change
@@ -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 added data/tests/cube.stl
Binary file not shown.
1 change: 1 addition & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
1 change: 1 addition & 0 deletions docs/Formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/modules/voxelformat/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
278 changes: 278 additions & 0 deletions src/modules/voxelformat/STLFormat.cpp
Original file line number Diff line number Diff line change
@@ -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 comments on commit 86f2563

Please sign in to comment.