VOXELFORMAT: added basic obj voxelization

currently only the surface is voxelized - it's not yet filled with solids
the second open task is to weight the colors for a voxel by all triangles
master
Martin Gerhardy 2022-01-16 23:07:11 +01:00
parent 8efd04d3a6
commit 7ab6accc96
12 changed files with 3700 additions and 1 deletions

24
data/tests/cube.mtl Normal file
View File

@ -0,0 +1,24 @@
newmtl white
Ka 0 0 0
Kd 1 1 1
Ks 0 0 0
newmtl red
Ka 0 0 0
Kd 1 0 0
Ks 0 0 0
newmtl green
Ka 0 0 0
Kd 0 1 0
Ks 0 0 0
newmtl blue
Ka 0 0 0
Kd 0 0 1
Ks 0 0 0
newmtl light
Ka 20 20 20
Kd 1 1 1
Ks 0 0 0

32
data/tests/cube.obj Normal file
View File

@ -0,0 +1,32 @@
mtllib cube.mtl
v 0.000000 2.000000 2.000000
v 0.000000 0.000000 2.000000
v 2.000000 0.000000 2.000000
v 2.000000 2.000000 2.000000
v 0.000000 2.000000 0.000000
v 0.000000 0.000000 0.000000
v 2.000000 0.000000 0.000000
v 2.000000 2.000000 0.000000
# 8 vertices
g front cube
usemtl white
f 1 2 3 4
# two white spaces between 'back' and 'cube'
g back cube
# expects white material
f 8 7 6 5
g right cube
usemtl red
f 4 3 7 8
g top cube
usemtl white
f 5 1 4 8
g left cube
usemtl green
f 5 6 2 1
g bottom cube
usemtl white
f 2 6 7 3
# 6 elements

3
debian/changelog vendored
View File

@ -6,6 +6,9 @@ vengi (0.0.17.0-1) UNRELEASED; urgency=low
* Fixed a few issues with the magicavoxel vox format (switched to ogt_vox)
* Load properties from supported voxel formats (vxr, vox, gox)
* Added support for loading minecraft region files (used enkimi)
* Fixed vxm pivot and black color issue
* Added obj voxelization
* Improved obj export
* VoxConvert:
* Added --crop parameter that reduces the volumes to their real voxel sizes

View File

@ -18,6 +18,9 @@ General:
- Fixed a few issues with the magicavoxel vox format (switched to ogt_vox)
- Load properties from supported voxel formats (vxr, vox, gox)
- Added support for loading minecraft region files (used enkimi)
- Fixed vxm pivot and black color issue
- Added obj voxelization
- Improved obj export
VoxConvert:

View File

@ -18,7 +18,7 @@
| Ace Of Spades | vxl | X | | X | |
| Chronovox-Studio | csm | X | | X | |
| Nick's Voxel Model | nvm | X | | X | |
| Minecraft Region | mcr | | | | |
| Minecraft Region | mcr | X | | X | X |
| Sproxel | csv | X | X | X | |
## Meshes

View File

@ -12,6 +12,12 @@ Merge several models into one:
`./vengi-voxconvert --input one.vox --input two.vox --output onetwo.vox`
## Voxelize an obj
Voxelize an obj and save as magicavoxel (including colors):
`./vengi-voxconvert --input mesh.obj --output voxels.vox`
## Generate from heightmap
Just specify the heightmap as input file like this:

View File

@ -1,6 +1,9 @@
set(LIB voxelformat)
set(SRCS
external/ogt_vox.h
external/enkimi.h
# external/enkimi.c
external/tiny_obj_loader.h
Format.h Format.cpp
AoSVXLFormat.h AoSVXLFormat.cpp
@ -43,6 +46,7 @@ set(TEST_SRCS
tests/KVXFormatTest.cpp
tests/KV6FormatTest.cpp
tests/MCRFormatTest.cpp
tests/OBJFormatTest.cpp
tests/QBTFormatTest.cpp
tests/QBFormatTest.cpp
tests/QBCLFormatTest.cpp
@ -69,6 +73,8 @@ set(TEST_FILES
tests/minecraft_110.mca
tests/minecraft_113.mca
tests/magicavoxel.vox
tests/cube.obj
tests/cube.mtl
tests/test.gox
tests/test.vxm
tests/test2.vxm
@ -80,6 +86,7 @@ set(TEST_FILES
tests/rgb.gox
tests/rgb.qb
tests/rgb.qef
tests/rgb.vxm
tests/rgb.cub
voxedit/chr_knight.qb
)

View File

@ -4,9 +4,14 @@
#include "OBJFormat.h"
#include "app/App.h"
#include "core/Color.h"
#include "core/Log.h"
#include "core/SharedPtr.h"
#include "core/StringUtil.h"
#include "core/Var.h"
#include "core/collection/StringMap.h"
#include "core/collection/DynamicArray.h"
#include "image/Image.h"
#include "io/File.h"
#include "io/FileStream.h"
#include "io/Filesystem.h"
@ -16,6 +21,9 @@
#include "voxelformat/SceneGraph.h"
#include "engine-config.h"
#define TINYOBJLOADER_IMPLEMENTATION
#include "external/tiny_obj_loader.h"
namespace voxel {
void OBJFormat::writeMtlFile(const core::String &mtlName, const core::String &paletteName) const {
@ -151,4 +159,260 @@ bool OBJFormat::saveMeshes(const Meshes &meshes, const core::String &filename, i
return true;
}
struct Tri {
glm::vec3 vertices[3];
glm::vec2 uv[3];
image::ImagePtr texture;
uint32_t color = 0xFFFFFFFF;
glm::vec2 centerUV() const {
return (uv[0] + uv[1] + uv[2]) / 3.0f;
}
glm::vec3 center() const {
return (vertices[0] + vertices[1] + vertices[2]) / 3.0f;
}
glm::vec3 mins() const {
glm::vec3 v;
for (int i = 0; i < 3; ++i) {
v[i] = core_min(vertices[0][i], core_min(vertices[1][i], vertices[2][i]));
}
return v;
}
glm::vec3 maxs() const {
glm::vec3 v;
for (int i = 0; i < 3; ++i) {
v[i] = core_max(vertices[0][i], core_max(vertices[1][i], vertices[2][i]));
}
return v;
}
uint32_t colorAt(const glm::vec2 &uv) const {
if (texture) {
const float w = (float)texture->width();
const float h = (float)texture->height();
float x = uv.x * w;
float y = uv.y * h;
while (x < 0.0f)
x += w;
while (x > w)
x -= w;
while (y < 0.0f)
y += h;
while (y > h)
y -= h;
const int xint = (int)glm::round(x - 0.5f);
const int yint = texture->height() - (int)glm::round(y - 0.5f) - 1;
const uint8_t *ptr = texture->at(xint, yint);
return *(const uint32_t*)ptr;
}
return color;
}
// Sierpinski gasket with keeping the middle
void subdivide(Tri out[4]) const {
const glm::vec3 midv[]{
glm::mix(vertices[0], vertices[1], 0.5f),
glm::mix(vertices[1], vertices[2], 0.5f),
glm::mix(vertices[2], vertices[0], 0.5f)
};
const glm::vec2 miduv[]{
glm::mix(uv[0], uv[1], 0.5f),
glm::mix(uv[1], uv[2], 0.5f),
glm::mix(uv[2], uv[0], 0.5f)
};
// the subdivided new three triangles
out[0] = Tri{{vertices[0], midv[0], midv[2]}, {uv[0], miduv[0], miduv[2]}, texture, color};
out[1] = Tri{{vertices[1], midv[1], midv[0]}, {uv[1], miduv[1], miduv[0]}, texture, color};
out[2] = Tri{{vertices[2], midv[2], midv[1]}, {uv[2], miduv[2], miduv[1]}, texture, color};
// keep the middle
out[3] = Tri{{midv[0], midv[1], midv[2]}, {miduv[0], miduv[1], miduv[2]}, texture, color};
}
};
/**
* Subdivide until we brought the triangles down to the size of 1 or smaller
*/
static void subdivideTri(const Tri& tri, core::DynamicArray<Tri> &tinyTris) {
const glm::vec3& mins = tri.mins();
const glm::vec3& maxs = tri.maxs();
const glm::vec3 size = maxs - mins;
if (glm::any(glm::greaterThan(size, glm::vec3(1.0f)))) {
Tri out[4];
tri.subdivide(out);
for (int i = 0; i < lengthof(out); ++i) {
subdivideTri(out[i], tinyTris);
}
return;
}
tinyTris.push_back(tri);
}
static void voxelizeShape(const tinyobj::shape_t &shape, const core::StringMap<image::ImagePtr> &textures,
const tinyobj::attrib_t &attrib, const std::vector<tinyobj::material_t> &materials, voxel::RawVolume *volume) {
core::DynamicArray<Tri> subdivided;
const tinyobj::mesh_t &mesh = shape.mesh;
int indexOffset = 0;
for (size_t faceNum = 0; faceNum < shape.mesh.num_face_vertices.size(); ++faceNum) {
const int faceVertices = shape.mesh.num_face_vertices[faceNum];
core_assert_msg(faceVertices == 3, "Unexpected indices for triangulated mesh: %i", faceVertices);
Tri tri;
for (int i = 0; i < faceVertices; ++i) {
const tinyobj::index_t &idx = mesh.indices[indexOffset + i];
tri.vertices[i].x = attrib.vertices[3 * idx.vertex_index + 0];
tri.vertices[i].y = attrib.vertices[3 * idx.vertex_index + 1];
tri.vertices[i].z = attrib.vertices[3 * idx.vertex_index + 2];
if (idx.texcoord_index >= 0) {
tri.uv[i].x = attrib.texcoords[2 * idx.texcoord_index + 0];
tri.uv[i].y = attrib.texcoords[2 * idx.texcoord_index + 1];
} else {
tri.uv[i] = glm::vec2(0.0f);
}
}
const int materialIndex = shape.mesh.material_ids[faceNum];
const tinyobj::material_t *material = materialIndex < 0 ? nullptr : &materials[materialIndex];
if (material != nullptr) {
const core::String diffuseTexture = material->diffuse_texname.c_str();
if (!diffuseTexture.empty()) {
auto textureIter = textures.find(diffuseTexture);
if (textureIter != textures.end()) {
tri.texture = textureIter->second;
}
}
const glm::vec4 diffuseColor(material->diffuse[0], material->diffuse[1], material->diffuse[2], 1.0f);
tri.color = core::Color::getRGBA(diffuseColor);
}
indexOffset += faceVertices;
subdivideTri(tri, subdivided);
}
core::DynamicArray<uint8_t> palette;
palette.reserve(subdivided.size());
const voxel::MaterialColorArray& materialColors = voxel::getMaterialColors();
for (const Tri &tri : subdivided) {
const glm::vec2 &uv = tri.centerUV();
const uint32_t rgba = tri.colorAt(uv);
const glm::vec4 &color = core::Color::fromRGBA(rgba);
const uint8_t index = core::Color::getClosestMatch(color, materialColors);
palette.push_back(index);
}
for (size_t i = 0; i < subdivided.size(); ++i) {
const Tri &tri = subdivided[i];
const uint8_t index = palette[i];
const voxel::Voxel voxel = voxel::createVoxel(voxel::VoxelType::Generic, index);
// TODO: different tris might contribute to the same voxel - merge the color values here
for (int v = 0; v < 3; v++) {
const glm::ivec3 p(glm::floor(tri.vertices[v]));
volume->setVoxel(p, voxel);
}
const glm::vec3 &center = tri.center();
const glm::ivec3 p2(glm::floor(center));
volume->setVoxel(p2, voxel);
}
// TODO: fill the inner parts of the model
}
static void calculateAABB(const tinyobj::shape_t &shape, const tinyobj::attrib_t &attrib, glm::vec3 &mins, glm::vec3 &maxs) {
maxs = glm::vec3(-100000.0f);
mins = glm::vec3(+100000.0f);
int indexOffset = 0;
const tinyobj::mesh_t &mesh = shape.mesh;
for (size_t faceNum = 0; faceNum < shape.mesh.num_face_vertices.size(); ++faceNum) {
const int faceVertices = shape.mesh.num_face_vertices[faceNum];
core_assert_msg(faceVertices == 3, "Unexpected indices for triangulated mesh: %i", faceVertices);
for (int i = 0; i < faceVertices; ++i) {
const tinyobj::index_t &idx = mesh.indices[indexOffset + i];
const float x = attrib.vertices[3 * idx.vertex_index + 0];
const float y = attrib.vertices[3 * idx.vertex_index + 1];
const float z = attrib.vertices[3 * idx.vertex_index + 2];
maxs.x = core_max(maxs.x, x);
maxs.y = core_max(maxs.y, y);
maxs.z = core_max(maxs.z, z);
mins.x = core_min(mins.x, x);
mins.y = core_min(mins.y, y);
mins.z = core_min(mins.z, z);
}
indexOffset += faceVertices;
}
}
bool OBJFormat::loadGroups(const core::String &filename, io::SeekableReadStream &stream, SceneGraph &sceneGraph) {
tinyobj::attrib_t attrib;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> materials;
std::string warn;
std::string err;
const core::String& mtlbasedir = core::string::extractPath(filename);
const bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, filename.c_str(), mtlbasedir.c_str(), true, true);
if (!warn.empty()) {
Log::warn("%s", warn.c_str());
}
if (!err.empty()) {
Log::error("%s", err.c_str());
}
if (!ret) {
Log::error("Failed to load: %s", filename.c_str());
return false;
}
if (shapes.size() == 0) {
Log::error("No shapes found in the model");
return false;
}
core::StringMap<image::ImagePtr> textures;
for (tinyobj::material_t &material : materials) {
core::String name = material.diffuse_texname.c_str();
if (name.empty()) {
continue;
}
if (textures.hasKey(name)) {
continue;
}
if (!core::string::isAbsolutePath(name)) {
const core::String& path = core::string::extractPath(filename);
Log::debug("Search image %s in path %s", name.c_str(), path.c_str());
name = path + name;
}
image::ImagePtr tex = image::loadImage(name, false);
if (tex->isLoaded()) {
Log::debug("Use image %s", name.c_str());
const core::String texname(material.diffuse_texname.c_str());
textures.put(texname, tex);
} else {
Log::warn("Failed to load %s", name.c_str());
}
}
for (tinyobj::shape_t &shape : shapes) {
glm::vec3 mins;
glm::vec3 maxs;
calculateAABB(shape, attrib, mins, maxs);
voxel::Region region(glm::floor(mins), glm::ceil(maxs));
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(shape.name.c_str());
voxelizeShape(shape, textures, attrib, materials, volume);
sceneGraph.emplace(core::move(node));
}
return true;
}
} // namespace voxel

View File

@ -15,5 +15,9 @@ private:
void writeMtlFile(const core::String& mtlName, const core::String &paletteName) const;
public:
bool saveMeshes(const Meshes& meshes, const core::String &filename, io::SeekableWriteStream& stream, float 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;
};
}

View File

@ -57,6 +57,7 @@ const io::FormatDescription SUPPORTED_VOXEL_FORMATS_LOAD[] = {
{"CubeWorld", "cub", nullptr, 0u},
{"Minecraft region", "mca", nullptr, VOX_FORMAT_FLAG_PALETTE_EMBEDDED},
{"Sproxel csv", "csv", nullptr, 0u},
{"Wavefront Object", "obj", 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},

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
/**
* @file
*/
#include "AbstractVoxFormatTest.h"
#include "voxelformat/OBJFormat.h"
namespace voxel {
class OBJFormatTest: public AbstractVoxFormatTest {
};
TEST_F(OBJFormatTest, testVoxelize) {
OBJFormat f;
const core::String filename = "cube.obj";
const io::FilePtr &file = open(filename);
io::FileStream stream(file.get());
SceneGraph sceneGraph;
EXPECT_TRUE(f.loadGroups(filename, stream, sceneGraph));
}
}