From 1acabf13072dc27b2b848afa2bc94ea3bfb9b007 Mon Sep 17 00:00:00 2001 From: Marc Gilleron Date: Sun, 1 May 2016 15:00:02 +0200 Subject: [PATCH] Added project: first working prototype, but lots of TODOs --- .gitignore | 2 + SCsub | 5 + config.py | 11 +++ register_types.cpp | 16 ++++ register_types.h | 2 + vector3i.h | 36 ++++++++ voxel.cpp | 102 +++++++++++++++++++++ voxel.h | 70 ++++++++++++++ voxel_buffer.cpp | 202 +++++++++++++++++++++++++++++++++++++++++ voxel_buffer.h | 86 ++++++++++++++++++ voxel_mesh_builder.cpp | 123 +++++++++++++++++++++++++ voxel_mesh_builder.h | 35 +++++++ 12 files changed, 690 insertions(+) create mode 100644 .gitignore create mode 100644 SCsub create mode 100644 config.py create mode 100644 register_types.cpp create mode 100644 register_types.h create mode 100644 vector3i.h create mode 100644 voxel.cpp create mode 100644 voxel.h create mode 100644 voxel_buffer.cpp create mode 100644 voxel_buffer.h create mode 100644 voxel_mesh_builder.cpp create mode 100644 voxel_mesh_builder.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a70261 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.obj +*.pyc diff --git a/SCsub b/SCsub new file mode 100644 index 0000000..53577c2 --- /dev/null +++ b/SCsub @@ -0,0 +1,5 @@ +Import('env') + +env.add_source_files(env.modules_sources,"*.cpp") +env.add_source_files(env.modules_sources,"lib/*.c") + diff --git a/config.py b/config.py new file mode 100644 index 0000000..ea7e833 --- /dev/null +++ b/config.py @@ -0,0 +1,11 @@ + + +def can_build(platform): + return True + + +def configure(env): + pass + + + diff --git a/register_types.cpp b/register_types.cpp new file mode 100644 index 0000000..cb94a9e --- /dev/null +++ b/register_types.cpp @@ -0,0 +1,16 @@ +#include "register_types.h" +#include "voxel_buffer.h" +#include "voxel_mesh_builder.h" + +void register_voxel_types() { + + ObjectTypeDB::register_type(); + ObjectTypeDB::register_type(); + ObjectTypeDB::register_type(); + +} + +void unregister_voxel_types() { + +} + diff --git a/register_types.h b/register_types.h new file mode 100644 index 0000000..3ebc5c4 --- /dev/null +++ b/register_types.h @@ -0,0 +1,2 @@ +void register_voxel_types(); +void unregister_voxel_types(); diff --git a/vector3i.h b/vector3i.h new file mode 100644 index 0000000..d8f7b72 --- /dev/null +++ b/vector3i.h @@ -0,0 +1,36 @@ +#ifndef VOXEL_VECTOR3I_H +#define VOXEL_VECTOR3I_H + +struct Vector3i { + + int x; + int y; + int z; + + Vector3i() : x(0), y(0), z(0) {} + + Vector3i(int px, int py, int pz) : x(px), y(py), z(pz) {} + + Vector3i(const Vector3i & other) { + *this = other; + } + + Vector3i & operator=(const Vector3i & other) { + x = other.x; + y = other.y; + z = other.z; + return *this; + } + + bool operator==(const Vector3i & other) { + return x == other.x && y == other.y && z == other.z; + } + + bool operator!=(const Vector3i & other) { + return x != other.x && y != other.y && z != other.z; + } + +}; + +#endif // VOXEL_VECTOR3I_H + diff --git a/voxel.cpp b/voxel.cpp new file mode 100644 index 0000000..5a00747 --- /dev/null +++ b/voxel.cpp @@ -0,0 +1,102 @@ +#include "voxel.h" + +Voxel::Voxel() : Reference(), _id(0), _material_id(0), _is_transparent(false), _color(1.f, 1.f, 1.f) { + +} + +void Voxel::set_id(int id) { + ERR_FAIL_COND(id < 0 || id >= 256); + _id = id; +} + +void Voxel::set_cube_geometry(float sy) { + const Vector3 vertices[SIDE_COUNT][6] = { + { + // LEFT + Vector3(0, 0, 0), + Vector3(0, sy, 0), + Vector3(0, sy, 1), + Vector3(0, 0, 0), + Vector3(0, sy, 1), + Vector3(0, 0, 1), + }, + { + // RIGHT + Vector3(1, 0, 0), + Vector3(1, sy, 1), + Vector3(1, sy, 0), + Vector3(1, 0, 0), + Vector3(1, 0, 1), + Vector3(1, sy, 1) + }, + { + // BOTTOM + Vector3(0, 0, 0), + Vector3(1, 0, 1), + Vector3(1, 0, 0), + Vector3(0, 0, 0), + Vector3(0, 0, 1), + Vector3(1, 0, 1) + }, + { + // TOP + Vector3(0, sy, 0), + Vector3(1, sy, 0), + Vector3(1, sy, 1), + Vector3(0, sy, 0), + Vector3(1, sy, 1), + Vector3(0, sy, 1) + }, + { + // BACK + Vector3(0, 0, 0), + Vector3(1, 0, 0), + Vector3(1, sy, 0), + Vector3(0, 0, 0), + Vector3(1, sy, 0), + Vector3(0, sy, 0), + }, + { + // FRONT + Vector3(1, 0, 1), + Vector3(0, 0, 1), + Vector3(1, sy, 1), + Vector3(0, 0, 1), + Vector3(0, sy, 1), + Vector3(1, sy, 1) + } + }; + + for (unsigned int side = 0; side < SIDE_COUNT; ++side) { + _model_side_vertices[side].resize(6); + DVector::Write w = _model_side_vertices[side].write(); + for (unsigned int i = 0; i < 6; ++i) { + w[i] = vertices[side][i]; + } + } +} + +void Voxel::set_cube_uv_all_sides(Vector3 atlas_pos) { + // TODO +} + +void Voxel::set_cube_uv_tbs_sides(Vector3 top_atlas_pos, Vector3 side_atlas_pos, Vector3 bottom_atlas_pos) { + // TODO +} + +void Voxel::_bind_methods() { + + ObjectTypeDB::bind_method(_MD("set_name", "name"), &Voxel::set_name); + ObjectTypeDB::bind_method(_MD("get_name"), &Voxel::get_name); + + ObjectTypeDB::bind_method(_MD("set_id", "id"), &Voxel::set_id); + ObjectTypeDB::bind_method(_MD("get_id"), &Voxel::get_id); + + ObjectTypeDB::bind_method(_MD("set_color", "color"), &Voxel::set_color); + ObjectTypeDB::bind_method(_MD("get_color"), &Voxel::get_color); + + ObjectTypeDB::bind_method(_MD("set_cube_geometry", "height"), &Voxel::set_cube_geometry, DEFVAL(1.f)); + // TODO + +} + diff --git a/voxel.h b/voxel.h new file mode 100644 index 0000000..3aa8447 --- /dev/null +++ b/voxel.h @@ -0,0 +1,70 @@ +#ifndef VOXEL_TYPE_H +#define VOXEL_TYPE_H + +#include + +// Definition of one type of voxel. +// A voxel can be a simple coloured cube, or a more complex model. +class Voxel : public Reference { + OBJ_TYPE(Voxel, Reference) + +public: + enum Side { + SIDE_LEFT = 0, + SIDE_RIGHT, + SIDE_BOTTOM, + SIDE_TOP, + SIDE_BACK, + SIDE_FRONT, + + SIDE_COUNT + }; + +private: + int _id; + String _name; + int _material_id; + bool _is_transparent; + + Color _color; + DVector _model_vertices; + DVector _model_normals; + DVector _model_uv; + DVector _model_side_vertices[SIDE_COUNT]; + DVector _model_side_uv[SIDE_COUNT]; + + // TODO Child voxel types + +public: + + Voxel(); + + _FORCE_INLINE_ void set_name(String name) { _name = name; } + _FORCE_INLINE_ String get_name() const { return _name; } + + void set_id(int id); + _FORCE_INLINE_ int get_id() const { return _id; } + + _FORCE_INLINE_ void set_color(Color color) { _color = color; } + _FORCE_INLINE_ Color get_color() const { return _color; } + + _FORCE_INLINE_ void set_material_id(unsigned int id) { _material_id = id; } + _FORCE_INLINE_ unsigned int get_material_id() const { return _material_id; } + + void set_cube_geometry(float sy = 1); + void set_cube_uv_all_sides(Vector3 atlas_pos); + void set_cube_uv_tbs_sides(Vector3 top_atlas_pos, Vector3 side_atlas_pos, Vector3 bottom_atlas_pos); + + const DVector & get_model_vertices() const { return _model_vertices; } + const DVector & get_model_normals() const { return _model_normals; } + const DVector & get_model_uv() const { return _model_uv; } + const DVector & get_model_side_vertices(unsigned int side) const { return _model_side_vertices[side]; } + const DVector & get_model_side_uv(unsigned int side) const { return _model_side_uv[side]; } + +protected: + static void _bind_methods(); + +}; + +#endif // VOXEL_TYPE_H + diff --git a/voxel_buffer.cpp b/voxel_buffer.cpp new file mode 100644 index 0000000..f65259d --- /dev/null +++ b/voxel_buffer.cpp @@ -0,0 +1,202 @@ +#include "voxel_buffer.h" + + +VoxelBuffer::VoxelBuffer() { + +} + +VoxelBuffer::~VoxelBuffer() { + clear(); +} + +void VoxelBuffer::create(int sx, int sy, int sz) { + if (sx <= 0 || sy <= 0 || sz <= 0) { + return; + } + Vector3i new_size(sx, sy, sz); + if (new_size != _size) { + for (unsigned int i = 0; i < MAX_CHANNELS; ++i) { + Channel & channel = _channels[i]; + if (channel.data) { + // TODO Optimize with realloc + delete_channel(i, _size); + create_channel(i, new_size); + } + } + _size = new_size; + } +} + +void VoxelBuffer::clear() { + for (unsigned int i = 0; i < MAX_CHANNELS; ++i) { + Channel & channel = _channels[i]; + if (channel.data) { + delete_channel(i, _size); + } + } +} + +void VoxelBuffer::clear_channel(unsigned int channel_index, int clear_value) { + ERR_FAIL_INDEX(channel_index, MAX_CHANNELS); + delete_channel(channel_index, _size); + _channels[channel_index].defval = clear_value; +} + +int VoxelBuffer::get_voxel(int x, int y, int z, unsigned int channel_index) const { + ERR_FAIL_INDEX_V(channel_index, MAX_CHANNELS, 0); + + x -= _offset.x; + y -= _offset.y; + z -= _offset.z; + + const Channel & channel = _channels[channel_index]; + + if (validate_local_pos(x, y, z) && channel.data) { + return channel.data[z][x][y]; + } + else { + return channel.defval; + } +} + +int VoxelBuffer::get_voxel_local(int x, int y, int z, unsigned int channel_index) const { + ERR_FAIL_INDEX_V(channel_index, MAX_CHANNELS, 0); + + const Channel & channel = _channels[channel_index]; + + if (validate_local_pos(x, y, z) && channel.data) { + return channel.data[z][x][y]; + } + else { + return channel.defval; + } +} + +void VoxelBuffer::set_voxel(int value, int x, int y, int z, unsigned int channel_index) { + ERR_FAIL_INDEX(channel_index, MAX_CHANNELS); + + x -= _offset.x; + y -= _offset.y; + z -= _offset.z; + + ERR_FAIL_COND(!validate_local_pos(x, y, z)); + + Channel & channel = _channels[channel_index]; + + if (channel.defval != value) { + if (channel.data == NULL) { + create_channel(channel_index, _size); + } + channel.data[z][x][y] = value; + } +} + +void VoxelBuffer::set_voxel_v(int value, Vector3 pos, unsigned int channel_index) { + set_voxel(value, pos.x, pos.y, pos.z, channel_index); +} + +void VoxelBuffer::fill(int defval, unsigned int channel_index) { + ERR_FAIL_INDEX(channel_index, MAX_CHANNELS); + + Channel & channel = _channels[channel_index]; + + for (unsigned int z = 0; z < _size.z; ++z) { + for (unsigned int x = 0; x < _size.x; ++x) { + uint8_t * column = channel.data[z][x]; + for (unsigned int y = 0; y < _size.y; ++y) { + column[y] = defval; + } + } + } +} + +bool VoxelBuffer::is_uniform(unsigned int channel_index) { + ERR_FAIL_INDEX_V(channel_index, MAX_CHANNELS, true); + + Channel & channel = _channels[channel_index]; + if (channel.data == NULL) + return true; + uint8_t voxel = channel.data[0][0][0]; + for (unsigned int z = 0; z < _size.z; ++z) { + for (unsigned int x = 0; x < _size.x; ++x) { + uint8_t * column = channel.data[z][x]; + for (unsigned int y = 0; y < _size.y; ++y) { + if (column[y] != voxel) { + return false; + } + } + } + } + return true; +} + +void VoxelBuffer::optimize() { + for (unsigned int i = 0; i < MAX_CHANNELS; ++i) { + if (_channels[i].data && is_uniform(i)) { + clear_channel(i, _channels[i].data[0][0][0]); + } + } +} + +void VoxelBuffer::create_channel(int i, Vector3i size, uint8_t defval) { + + Channel & channel = _channels[i]; + channel.data = (uint8_t***)memalloc(size.z * sizeof(uint8_t**)); + + for (unsigned int z = 0; z < size.z; ++z) { + + uint8_t ** plane = (uint8_t**)memalloc(size.x * sizeof(uint8_t*)); + channel.data[z] = plane; + + for (unsigned int x = 0; x < size.x; ++x) { + + uint8_t * column = (uint8_t*)memalloc(size.y * sizeof(uint8_t)); + plane[x] = column; + + for (unsigned int y = 0; y < size.y; ++y) { + column[y] = defval; + } + } + } +} + +void VoxelBuffer::delete_channel(int i, Vector3i size) { + + Channel & channel = _channels[i]; + + for (unsigned int z = 0; z < size.z; ++z) { + for (unsigned int x = 0; x < size.x; ++x) { + memfree(channel.data[z][x]); + } + memfree(channel.data[z]); + } + memfree(channel.data); + channel.data = NULL; +} + +void VoxelBuffer::_bind_methods() { + + ObjectTypeDB::bind_method(_MD("create", "sx", "sy", "sz"), &VoxelBuffer::create); + ObjectTypeDB::bind_method(_MD("clear"), &VoxelBuffer::clear); + + ObjectTypeDB::bind_method(_MD("get_size_x"), &VoxelBuffer::get_size_x); + ObjectTypeDB::bind_method(_MD("get_size_y"), &VoxelBuffer::get_size_y); + ObjectTypeDB::bind_method(_MD("get_size_z"), &VoxelBuffer::get_size_z); + + ObjectTypeDB::bind_method(_MD("get_offset_x"), &VoxelBuffer::get_offset_x); + ObjectTypeDB::bind_method(_MD("get_offset_y"), &VoxelBuffer::get_offset_y); + ObjectTypeDB::bind_method(_MD("get_offset_z"), &VoxelBuffer::get_offset_z); + + ObjectTypeDB::bind_method(_MD("set_offset", "x", "y", "z"), &VoxelBuffer::set_offset); + + ObjectTypeDB::bind_method(_MD("set_voxel", "value", "x", "y", "z", "channel"), &VoxelBuffer::set_voxel, DEFVAL(0)); + ObjectTypeDB::bind_method(_MD("set_voxel_v", "value", "pos", "channel"), &VoxelBuffer::set_voxel, DEFVAL(0)); + ObjectTypeDB::bind_method(_MD("get_voxel", "x", "y", "z", "channel"), &VoxelBuffer::set_voxel, DEFVAL(0)); + + ObjectTypeDB::bind_method(_MD("fill", "value", "channel"), &VoxelBuffer::fill, DEFVAL(0)); + + ObjectTypeDB::bind_method(_MD("is_uniform", "channel"), &VoxelBuffer::is_uniform, DEFVAL(0)); + ObjectTypeDB::bind_method(_MD("optimize"), &VoxelBuffer::optimize); + +} + diff --git a/voxel_buffer.h b/voxel_buffer.h new file mode 100644 index 0000000..8ef77a3 --- /dev/null +++ b/voxel_buffer.h @@ -0,0 +1,86 @@ +#ifndef VOXEL_BUFFER_H +#define VOXEL_BUFFER_H + +#include +#include +#include "vector3i.h" + +// Dense voxels data storage. +// Organized in 8-bit channels like images, all optional. +// Note: for float storage (marching cubes for example), you can map [0..256] to [0..1] and save 3 bytes per cell + +class VoxelBuffer : public Reference { + OBJ_TYPE(VoxelBuffer, Reference) + + // Arbitrary value, 8 should be enough. Tweak for your needs. + static const int MAX_CHANNELS = 8; + + struct Channel { + // Allocated when the channel is populated. + // Array of array of arrays, in order [z][x][y] because it makes vertical-wise access faster (the engine is Y-up). + uint8_t *** data; + + uint8_t defval; // Default value when data is null + + Channel() : data(NULL), defval(0) {} + }; + + // Each channel can store arbitary data. + // For example, you can decide to store colors (R, G, B, A), gameplay types (type, state, light) or both. + Channel _channels[MAX_CHANNELS]; + + // How many voxels are there in the three directions. All populated channels have the same size. + Vector3i _size; + + // Offset applied to coordinates when accessing voxels. + // Use _local versions to bypass this. + Vector3i _offset; + +public: + VoxelBuffer(); + ~VoxelBuffer(); + + void create(int sx, int sy, int sz); + void clear(); + void clear_channel(unsigned int channel_index, int clear_value=0); + + _FORCE_INLINE_ int get_size_x() const { return _size.x; } + _FORCE_INLINE_ int get_size_y() const { return _size.y; } + _FORCE_INLINE_ int get_size_z() const { return _size.z; } + + _FORCE_INLINE_ int get_offset_x() const { return _offset.x; } + _FORCE_INLINE_ int get_offset_y() const { return _offset.y; } + _FORCE_INLINE_ int get_offset_z() const { return _offset.z; } + + _FORCE_INLINE_ void set_offset(int x, int y, int z) { _offset = Vector3i(x,y,z); } + + int get_voxel(int x, int y, int z, unsigned int channel_index=0) const; + int get_voxel_local(int x, int y, int z, unsigned int channel_index=0) const; + void set_voxel(int value, int x, int y, int z, unsigned int channel_index=0); + void set_voxel_v(int value, Vector3 pos, unsigned int channel_index = 0); + + void fill(int defval, unsigned int channel_index = 0); + + bool is_uniform(unsigned int channel_index = 0); + + void optimize(); + + //void copy_from(Ref other); + + _FORCE_INLINE_ bool validate_local_pos(unsigned int x, unsigned int y, unsigned int z) const { + return x < _size.x + && y < _size.y + && z < _size.x; + } + +private: + void create_channel(int i, Vector3i size, uint8_t defval=0); + void delete_channel(int i, Vector3i size); + +protected: + static void _bind_methods(); + +}; + +#endif // VOXEL_BUFFER_H + diff --git a/voxel_mesh_builder.cpp b/voxel_mesh_builder.cpp new file mode 100644 index 0000000..c41745d --- /dev/null +++ b/voxel_mesh_builder.cpp @@ -0,0 +1,123 @@ +#include "voxel_mesh_builder.h" + +static const Vector3i g_side_normals[Voxel::SIDE_COUNT] = { + Vector3i(-1, 0, 0), + Vector3i(1, 0, 0), + Vector3i(0, -1, 0), + Vector3i(0, 1, 0), + Vector3i(0, 0, -1), + Vector3i(0, 0, 1), +}; + +VoxelMeshBuilder::VoxelMeshBuilder() { + +} + +void VoxelMeshBuilder::add_voxel_type(Ref voxel) { + ERR_FAIL_COND(voxel.is_null()); + ERR_FAIL_COND(voxel->get_id() >= MAX_VOXEL_TYPES); + ERR_FAIL_COND(voxel->get_material_id() >= MAX_MATERIALS); + unsigned int id = voxel->get_id(); + _voxel_types[id] = voxel; +} + +void VoxelMeshBuilder::set_material(Ref material, unsigned int id) { + ERR_FAIL_COND(id >= MAX_MATERIALS); + _materials[id] = material; + _surface_tool[id].set_material(material); +} + +Ref VoxelMeshBuilder::build(Ref buffer_ref) { + ERR_FAIL_COND_V(buffer_ref.is_null(), Ref()); + + const VoxelBuffer & buffer = **buffer_ref; + + for (unsigned int i = 0; i < MAX_MATERIALS; ++i) { + _surface_tool[i].begin(Mesh::PRIMITIVE_TRIANGLES); + } + + // Iterate 3D padded data to extract voxel faces. + // This is the most intensive job in this class, so all required data should be as fit as possible. + for (unsigned int z = 1; z < buffer.get_size_z()-1; ++z) { + for (unsigned int x = 1; x < buffer.get_size_x()-1; ++x) { + for (unsigned int y = 1; y < buffer.get_size_y()-1; ++y) { + + int voxel_id = buffer.get_voxel_local(x, y, z, 0); + + if (voxel_id != 0 && !_voxel_types[voxel_id].is_null()) { + + const Voxel & voxel = **_voxel_types[voxel_id]; + + SurfaceTool & st = _surface_tool[voxel.get_material_id()]; + + // Hybrid approach: extract cube faces and decimate those that aren't visible, + // and still allow voxels to have geometry that is not a cube + + // Sides + for (unsigned int side = 0; side < Voxel::SIDE_COUNT; ++side) { + + const DVector & vertices = voxel.get_model_side_vertices(side); + if (vertices.size() != 0) { + + Vector3i normal = g_side_normals[side]; + unsigned nx = x + normal.x; + unsigned ny = y + normal.y; + unsigned nz = z + normal.z; + + int neighbor_voxel_id = buffer.get_voxel_local(nx, ny, nz, 0); + // TODO Better face visibility test + if (neighbor_voxel_id == 0) { + + DVector::Read r = vertices.read(); + Vector3 pos(x - 1, y - 1, z - 1); + + for (unsigned int i = 0; i < vertices.size(); ++i) { + st.add_normal(Vector3(normal.x, normal.y, normal.z)); + st.add_vertex(r[i] + pos); + } + } + } + } + + // Inside + if (voxel.get_model_vertices().size() != 0) { + + const DVector & vertices = voxel.get_model_vertices(); + DVector::Read rv = voxel.get_model_vertices().read(); + DVector::Read rn = voxel.get_model_normals().read(); + Vector3 pos(x - 1, y - 1, z - 1); + + for (unsigned int i = 0; i < vertices.size(); ++i) { + st.add_normal(rn[i]); + st.add_vertex(rv[i] + pos); + } + } + + } + + } + } + } + + // Commit mesh + Ref mesh_ref = _surface_tool[0].commit(); + _surface_tool[0].clear(); + for (unsigned int i = 1; i < MAX_MATERIALS; ++i) { + if (_materials[i].is_valid()) { + SurfaceTool & st = _surface_tool[i]; + st.commit(mesh_ref); + st.clear(); + } + } + + return mesh_ref; +} + +void VoxelMeshBuilder::_bind_methods() { + + ObjectTypeDB::bind_method(_MD("add_voxel_type", "voxel"), &VoxelMeshBuilder::add_voxel_type); + ObjectTypeDB::bind_method(_MD("set_material", "material", "id"), &VoxelMeshBuilder::set_material); + ObjectTypeDB::bind_method(_MD("build", "voxel_buffer"), &VoxelMeshBuilder::build); + +} + diff --git a/voxel_mesh_builder.h b/voxel_mesh_builder.h new file mode 100644 index 0000000..480a42c --- /dev/null +++ b/voxel_mesh_builder.h @@ -0,0 +1,35 @@ +#ifndef VOXEL_MESH_BUILDER +#define VOXEL_MESH_BUILDER + +#include +#include +#include +#include "voxel.h" +#include "voxel_buffer.h" + +class VoxelMeshBuilder : public Reference { + OBJ_TYPE(VoxelMeshBuilder, Reference); + + static const unsigned int MAX_VOXEL_TYPES = 256; // Required limit because voxel types are stored in 8 bits + static const unsigned int MAX_MATERIALS = 8; // Arbitrary. Tweak if needed. + + Ref _voxel_types[MAX_VOXEL_TYPES]; + Ref _materials[MAX_MATERIALS]; + SurfaceTool _surface_tool[MAX_MATERIALS]; + +public: + + VoxelMeshBuilder(); + + void add_voxel_type(Ref voxel); + void set_material(Ref material, unsigned int id); + + Ref build(Ref buffer_ref); + +protected: + static void _bind_methods(); + +}; + + +#endif // VOXEL_MESH_BUILDER