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 trianglesmaster
parent
8efd04d3a6
commit
7ab6accc96
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 ¢er = 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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue