Voxel functionality extension

This commit is contained in:
John McCardle 2026-02-05 12:52:18 -05:00
commit 992ea781cb
14 changed files with 3045 additions and 17 deletions

View file

@ -234,6 +234,31 @@ int PyVoxelGrid::set_rotation(PyVoxelGridObject* self, PyObject* value, void* cl
return 0;
}
// Greedy meshing (Milestone 13)
PyObject* PyVoxelGrid::get_greedy_meshing(PyVoxelGridObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
return PyBool_FromLong(self->data->isGreedyMeshingEnabled());
}
int PyVoxelGrid::set_greedy_meshing(PyVoxelGridObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return -1;
}
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "greedy_meshing must be a boolean");
return -1;
}
bool enabled = (value == Py_True);
self->data->setGreedyMeshing(enabled);
return 0;
}
// =============================================================================
// Voxel access methods
// =============================================================================
@ -522,6 +547,557 @@ PyObject* PyVoxelGrid::count_material(PyVoxelGridObject* self, PyObject* args) {
return PyLong_FromSize_t(self->data->countMaterial(static_cast<uint8_t>(material)));
}
// =============================================================================
// Bulk operations - Milestone 11
// =============================================================================
PyObject* PyVoxelGrid::fill_box_hollow(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
static const char* kwlist[] = {"min_coord", "max_coord", "material", "thickness", nullptr};
PyObject* min_obj = nullptr;
PyObject* max_obj = nullptr;
int material;
int thickness = 1;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi|i", const_cast<char**>(kwlist),
&min_obj, &max_obj, &material, &thickness)) {
return nullptr;
}
if (material < 0 || material > 255) {
PyErr_SetString(PyExc_ValueError, "material must be 0-255");
return nullptr;
}
if (thickness < 1) {
PyErr_SetString(PyExc_ValueError, "thickness must be >= 1");
return nullptr;
}
// Parse coordinates
if (!PyTuple_Check(min_obj) && !PyList_Check(min_obj)) {
PyErr_SetString(PyExc_TypeError, "min_coord must be a tuple or list of 3 integers");
return nullptr;
}
if (!PyTuple_Check(max_obj) && !PyList_Check(max_obj)) {
PyErr_SetString(PyExc_TypeError, "max_coord must be a tuple or list of 3 integers");
return nullptr;
}
if (PySequence_Size(min_obj) != 3 || PySequence_Size(max_obj) != 3) {
PyErr_SetString(PyExc_ValueError, "coordinates must have exactly 3 elements");
return nullptr;
}
int x0, y0, z0, x1, y1, z1;
PyObject* items[6];
items[0] = PySequence_GetItem(min_obj, 0);
items[1] = PySequence_GetItem(min_obj, 1);
items[2] = PySequence_GetItem(min_obj, 2);
items[3] = PySequence_GetItem(max_obj, 0);
items[4] = PySequence_GetItem(max_obj, 1);
items[5] = PySequence_GetItem(max_obj, 2);
bool valid = true;
if (PyLong_Check(items[0])) x0 = (int)PyLong_AsLong(items[0]); else valid = false;
if (PyLong_Check(items[1])) y0 = (int)PyLong_AsLong(items[1]); else valid = false;
if (PyLong_Check(items[2])) z0 = (int)PyLong_AsLong(items[2]); else valid = false;
if (PyLong_Check(items[3])) x1 = (int)PyLong_AsLong(items[3]); else valid = false;
if (PyLong_Check(items[4])) y1 = (int)PyLong_AsLong(items[4]); else valid = false;
if (PyLong_Check(items[5])) z1 = (int)PyLong_AsLong(items[5]); else valid = false;
for (int i = 0; i < 6; i++) Py_DECREF(items[i]);
if (!valid) {
PyErr_SetString(PyExc_TypeError, "coordinate elements must be integers");
return nullptr;
}
self->data->fillBoxHollow(x0, y0, z0, x1, y1, z1, static_cast<uint8_t>(material), thickness);
Py_RETURN_NONE;
}
PyObject* PyVoxelGrid::fill_sphere(PyVoxelGridObject* self, PyObject* args) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
PyObject* center_obj = nullptr;
int radius;
int material;
if (!PyArg_ParseTuple(args, "Oii", &center_obj, &radius, &material)) {
return nullptr;
}
if (material < 0 || material > 255) {
PyErr_SetString(PyExc_ValueError, "material must be 0-255");
return nullptr;
}
if (radius < 0) {
PyErr_SetString(PyExc_ValueError, "radius must be >= 0");
return nullptr;
}
if (!PyTuple_Check(center_obj) && !PyList_Check(center_obj)) {
PyErr_SetString(PyExc_TypeError, "center must be a tuple or list of 3 integers");
return nullptr;
}
if (PySequence_Size(center_obj) != 3) {
PyErr_SetString(PyExc_ValueError, "center must have exactly 3 elements");
return nullptr;
}
int cx, cy, cz;
PyObject* items[3];
items[0] = PySequence_GetItem(center_obj, 0);
items[1] = PySequence_GetItem(center_obj, 1);
items[2] = PySequence_GetItem(center_obj, 2);
bool valid = true;
if (PyLong_Check(items[0])) cx = (int)PyLong_AsLong(items[0]); else valid = false;
if (PyLong_Check(items[1])) cy = (int)PyLong_AsLong(items[1]); else valid = false;
if (PyLong_Check(items[2])) cz = (int)PyLong_AsLong(items[2]); else valid = false;
for (int i = 0; i < 3; i++) Py_DECREF(items[i]);
if (!valid) {
PyErr_SetString(PyExc_TypeError, "center elements must be integers");
return nullptr;
}
self->data->fillSphere(cx, cy, cz, radius, static_cast<uint8_t>(material));
Py_RETURN_NONE;
}
PyObject* PyVoxelGrid::fill_cylinder(PyVoxelGridObject* self, PyObject* args) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
PyObject* base_obj = nullptr;
int radius;
int height;
int material;
if (!PyArg_ParseTuple(args, "Oiii", &base_obj, &radius, &height, &material)) {
return nullptr;
}
if (material < 0 || material > 255) {
PyErr_SetString(PyExc_ValueError, "material must be 0-255");
return nullptr;
}
if (radius < 0) {
PyErr_SetString(PyExc_ValueError, "radius must be >= 0");
return nullptr;
}
if (height < 1) {
PyErr_SetString(PyExc_ValueError, "height must be >= 1");
return nullptr;
}
if (!PyTuple_Check(base_obj) && !PyList_Check(base_obj)) {
PyErr_SetString(PyExc_TypeError, "base_pos must be a tuple or list of 3 integers");
return nullptr;
}
if (PySequence_Size(base_obj) != 3) {
PyErr_SetString(PyExc_ValueError, "base_pos must have exactly 3 elements");
return nullptr;
}
int cx, cy, cz;
PyObject* items[3];
items[0] = PySequence_GetItem(base_obj, 0);
items[1] = PySequence_GetItem(base_obj, 1);
items[2] = PySequence_GetItem(base_obj, 2);
bool valid = true;
if (PyLong_Check(items[0])) cx = (int)PyLong_AsLong(items[0]); else valid = false;
if (PyLong_Check(items[1])) cy = (int)PyLong_AsLong(items[1]); else valid = false;
if (PyLong_Check(items[2])) cz = (int)PyLong_AsLong(items[2]); else valid = false;
for (int i = 0; i < 3; i++) Py_DECREF(items[i]);
if (!valid) {
PyErr_SetString(PyExc_TypeError, "base_pos elements must be integers");
return nullptr;
}
self->data->fillCylinder(cx, cy, cz, radius, height, static_cast<uint8_t>(material));
Py_RETURN_NONE;
}
PyObject* PyVoxelGrid::fill_noise(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
static const char* kwlist[] = {"min_coord", "max_coord", "material", "threshold", "scale", "seed", nullptr};
PyObject* min_obj = nullptr;
PyObject* max_obj = nullptr;
int material;
float threshold = 0.5f;
float scale = 0.1f;
unsigned int seed = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi|ffI", const_cast<char**>(kwlist),
&min_obj, &max_obj, &material, &threshold, &scale, &seed)) {
return nullptr;
}
if (material < 0 || material > 255) {
PyErr_SetString(PyExc_ValueError, "material must be 0-255");
return nullptr;
}
// Parse coordinates
if (!PyTuple_Check(min_obj) && !PyList_Check(min_obj)) {
PyErr_SetString(PyExc_TypeError, "min_coord must be a tuple or list of 3 integers");
return nullptr;
}
if (!PyTuple_Check(max_obj) && !PyList_Check(max_obj)) {
PyErr_SetString(PyExc_TypeError, "max_coord must be a tuple or list of 3 integers");
return nullptr;
}
if (PySequence_Size(min_obj) != 3 || PySequence_Size(max_obj) != 3) {
PyErr_SetString(PyExc_ValueError, "coordinates must have exactly 3 elements");
return nullptr;
}
int x0, y0, z0, x1, y1, z1;
PyObject* items[6];
items[0] = PySequence_GetItem(min_obj, 0);
items[1] = PySequence_GetItem(min_obj, 1);
items[2] = PySequence_GetItem(min_obj, 2);
items[3] = PySequence_GetItem(max_obj, 0);
items[4] = PySequence_GetItem(max_obj, 1);
items[5] = PySequence_GetItem(max_obj, 2);
bool valid = true;
if (PyLong_Check(items[0])) x0 = (int)PyLong_AsLong(items[0]); else valid = false;
if (PyLong_Check(items[1])) y0 = (int)PyLong_AsLong(items[1]); else valid = false;
if (PyLong_Check(items[2])) z0 = (int)PyLong_AsLong(items[2]); else valid = false;
if (PyLong_Check(items[3])) x1 = (int)PyLong_AsLong(items[3]); else valid = false;
if (PyLong_Check(items[4])) y1 = (int)PyLong_AsLong(items[4]); else valid = false;
if (PyLong_Check(items[5])) z1 = (int)PyLong_AsLong(items[5]); else valid = false;
for (int i = 0; i < 6; i++) Py_DECREF(items[i]);
if (!valid) {
PyErr_SetString(PyExc_TypeError, "coordinate elements must be integers");
return nullptr;
}
self->data->fillNoise(x0, y0, z0, x1, y1, z1, static_cast<uint8_t>(material), threshold, scale, seed);
Py_RETURN_NONE;
}
// =============================================================================
// Copy/paste operations - Milestone 11
// =============================================================================
PyObject* PyVoxelGrid::copy_region(PyVoxelGridObject* self, PyObject* args) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
PyObject* min_obj = nullptr;
PyObject* max_obj = nullptr;
if (!PyArg_ParseTuple(args, "OO", &min_obj, &max_obj)) {
return nullptr;
}
// Parse coordinates
if (!PyTuple_Check(min_obj) && !PyList_Check(min_obj)) {
PyErr_SetString(PyExc_TypeError, "min_coord must be a tuple or list of 3 integers");
return nullptr;
}
if (!PyTuple_Check(max_obj) && !PyList_Check(max_obj)) {
PyErr_SetString(PyExc_TypeError, "max_coord must be a tuple or list of 3 integers");
return nullptr;
}
if (PySequence_Size(min_obj) != 3 || PySequence_Size(max_obj) != 3) {
PyErr_SetString(PyExc_ValueError, "coordinates must have exactly 3 elements");
return nullptr;
}
int x0, y0, z0, x1, y1, z1;
PyObject* items[6];
items[0] = PySequence_GetItem(min_obj, 0);
items[1] = PySequence_GetItem(min_obj, 1);
items[2] = PySequence_GetItem(min_obj, 2);
items[3] = PySequence_GetItem(max_obj, 0);
items[4] = PySequence_GetItem(max_obj, 1);
items[5] = PySequence_GetItem(max_obj, 2);
bool valid = true;
if (PyLong_Check(items[0])) x0 = (int)PyLong_AsLong(items[0]); else valid = false;
if (PyLong_Check(items[1])) y0 = (int)PyLong_AsLong(items[1]); else valid = false;
if (PyLong_Check(items[2])) z0 = (int)PyLong_AsLong(items[2]); else valid = false;
if (PyLong_Check(items[3])) x1 = (int)PyLong_AsLong(items[3]); else valid = false;
if (PyLong_Check(items[4])) y1 = (int)PyLong_AsLong(items[4]); else valid = false;
if (PyLong_Check(items[5])) z1 = (int)PyLong_AsLong(items[5]); else valid = false;
for (int i = 0; i < 6; i++) Py_DECREF(items[i]);
if (!valid) {
PyErr_SetString(PyExc_TypeError, "coordinate elements must be integers");
return nullptr;
}
// Copy the region
mcrf::VoxelRegion region = self->data->copyRegion(x0, y0, z0, x1, y1, z1);
// Create Python object
PyVoxelRegionObject* result = (PyVoxelRegionObject*)mcrfpydef::PyVoxelRegionType.tp_alloc(
&mcrfpydef::PyVoxelRegionType, 0);
if (!result) {
return nullptr;
}
result->data = std::make_shared<mcrf::VoxelRegion>(std::move(region));
return (PyObject*)result;
}
PyObject* PyVoxelGrid::paste_region(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
static const char* kwlist[] = {"region", "position", "skip_air", nullptr};
PyObject* region_obj = nullptr;
PyObject* pos_obj = nullptr;
int skip_air = 1;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|p", const_cast<char**>(kwlist),
&region_obj, &pos_obj, &skip_air)) {
return nullptr;
}
// Check region type
if (!PyObject_TypeCheck(region_obj, &mcrfpydef::PyVoxelRegionType)) {
PyErr_SetString(PyExc_TypeError, "region must be a VoxelRegion object");
return nullptr;
}
PyVoxelRegionObject* py_region = (PyVoxelRegionObject*)region_obj;
if (!py_region->data || !py_region->data->isValid()) {
PyErr_SetString(PyExc_ValueError, "VoxelRegion is empty or invalid");
return nullptr;
}
// Parse position
if (!PyTuple_Check(pos_obj) && !PyList_Check(pos_obj)) {
PyErr_SetString(PyExc_TypeError, "position must be a tuple or list of 3 integers");
return nullptr;
}
if (PySequence_Size(pos_obj) != 3) {
PyErr_SetString(PyExc_ValueError, "position must have exactly 3 elements");
return nullptr;
}
int x, y, z;
PyObject* items[3];
items[0] = PySequence_GetItem(pos_obj, 0);
items[1] = PySequence_GetItem(pos_obj, 1);
items[2] = PySequence_GetItem(pos_obj, 2);
bool valid = true;
if (PyLong_Check(items[0])) x = (int)PyLong_AsLong(items[0]); else valid = false;
if (PyLong_Check(items[1])) y = (int)PyLong_AsLong(items[1]); else valid = false;
if (PyLong_Check(items[2])) z = (int)PyLong_AsLong(items[2]); else valid = false;
for (int i = 0; i < 3; i++) Py_DECREF(items[i]);
if (!valid) {
PyErr_SetString(PyExc_TypeError, "position elements must be integers");
return nullptr;
}
self->data->pasteRegion(*py_region->data, x, y, z, skip_air != 0);
Py_RETURN_NONE;
}
// =============================================================================
// Serialization (Milestone 14)
// =============================================================================
PyObject* PyVoxelGrid::save(PyVoxelGridObject* self, PyObject* args) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
const char* path = nullptr;
if (!PyArg_ParseTuple(args, "s", &path)) {
return nullptr;
}
if (self->data->save(path)) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
PyObject* PyVoxelGrid::load(PyVoxelGridObject* self, PyObject* args) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
const char* path = nullptr;
if (!PyArg_ParseTuple(args, "s", &path)) {
return nullptr;
}
if (self->data->load(path)) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
PyObject* PyVoxelGrid::to_bytes(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
std::vector<uint8_t> buffer;
if (!self->data->saveToBuffer(buffer)) {
PyErr_SetString(PyExc_RuntimeError, "Failed to serialize VoxelGrid");
return nullptr;
}
return PyBytes_FromStringAndSize(reinterpret_cast<const char*>(buffer.data()), buffer.size());
}
PyObject* PyVoxelGrid::from_bytes(PyVoxelGridObject* self, PyObject* args) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
Py_buffer buffer;
if (!PyArg_ParseTuple(args, "y*", &buffer)) {
return nullptr;
}
bool success = self->data->loadFromBuffer(
static_cast<const uint8_t*>(buffer.buf), buffer.len);
PyBuffer_Release(&buffer);
if (success) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
// =============================================================================
// Navigation Projection (Milestone 12)
// =============================================================================
static PyObject* project_column(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"x", "z", "headroom", nullptr};
int x = 0, z = 0, headroom = 2;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|i", const_cast<char**>(kwlist),
&x, &z, &headroom)) {
return nullptr;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
return nullptr;
}
if (headroom < 0) {
PyErr_SetString(PyExc_ValueError, "headroom must be non-negative");
return nullptr;
}
mcrf::VoxelGrid::NavInfo nav = self->data->projectColumn(x, z, headroom);
// Return as dictionary with nav info
return Py_BuildValue("{s:f,s:O,s:O,s:f}",
"height", nav.height,
"walkable", nav.walkable ? Py_True : Py_False,
"transparent", nav.transparent ? Py_True : Py_False,
"path_cost", nav.pathCost);
}
// =============================================================================
// PyVoxelRegion implementation
// =============================================================================
void PyVoxelRegion::dealloc(PyVoxelRegionObject* self) {
self->data.reset();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* PyVoxelRegion::repr(PyObject* obj) {
PyVoxelRegionObject* self = (PyVoxelRegionObject*)obj;
if (!self->data || !self->data->isValid()) {
return PyUnicode_FromString("<VoxelRegion (empty)>");
}
std::ostringstream oss;
oss << "<VoxelRegion " << self->data->width << "x"
<< self->data->height << "x" << self->data->depth
<< " voxels=" << self->data->totalVoxels() << ">";
return PyUnicode_FromString(oss.str().c_str());
}
PyObject* PyVoxelRegion::get_size(PyVoxelRegionObject* self, void* closure) {
if (!self->data || !self->data->isValid()) {
return Py_BuildValue("(iii)", 0, 0, 0);
}
return Py_BuildValue("(iii)", self->data->width, self->data->height, self->data->depth);
}
PyObject* PyVoxelRegion::get_width(PyVoxelRegionObject* self, void* closure) {
if (!self->data) return PyLong_FromLong(0);
return PyLong_FromLong(self->data->width);
}
PyObject* PyVoxelRegion::get_height(PyVoxelRegionObject* self, void* closure) {
if (!self->data) return PyLong_FromLong(0);
return PyLong_FromLong(self->data->height);
}
PyObject* PyVoxelRegion::get_depth(PyVoxelRegionObject* self, void* closure) {
if (!self->data) return PyLong_FromLong(0);
return PyLong_FromLong(self->data->depth);
}
PyGetSetDef PyVoxelRegion::getsetters[] = {
{"size", (getter)get_size, nullptr,
"Dimensions (width, height, depth) of the region. Read-only.", nullptr},
{"width", (getter)get_width, nullptr, "Region width. Read-only.", nullptr},
{"height", (getter)get_height, nullptr, "Region height. Read-only.", nullptr},
{"depth", (getter)get_depth, nullptr, "Region depth. Read-only.", nullptr},
{nullptr} // Sentinel
};
// =============================================================================
// Method definitions
// =============================================================================
@ -556,6 +1132,54 @@ PyMethodDef PyVoxelGrid::methods[] = {
" max_coord: (x1, y1, z1) - maximum corner (inclusive)\n"
" material: material ID (0-255)\n\n"
"Coordinates are clamped to grid bounds."},
{"fill_box_hollow", (PyCFunction)fill_box_hollow, METH_VARARGS | METH_KEYWORDS,
"fill_box_hollow(min_coord, max_coord, material, thickness=1) -> None\n\n"
"Create a hollow rectangular room (walls only, hollow inside).\n\n"
"Args:\n"
" min_coord: (x0, y0, z0) - minimum corner (inclusive)\n"
" max_coord: (x1, y1, z1) - maximum corner (inclusive)\n"
" material: material ID for walls (0-255)\n"
" thickness: wall thickness in voxels (default 1)"},
{"fill_sphere", (PyCFunction)fill_sphere, METH_VARARGS,
"fill_sphere(center, radius, material) -> None\n\n"
"Fill a spherical region.\n\n"
"Args:\n"
" center: (cx, cy, cz) - sphere center coordinates\n"
" radius: sphere radius in voxels\n"
" material: material ID (0-255, use 0 to carve)"},
{"fill_cylinder", (PyCFunction)fill_cylinder, METH_VARARGS,
"fill_cylinder(base_pos, radius, height, material) -> None\n\n"
"Fill a vertical cylinder (Y-axis aligned).\n\n"
"Args:\n"
" base_pos: (cx, cy, cz) - base center position\n"
" radius: cylinder radius in voxels\n"
" height: cylinder height in voxels\n"
" material: material ID (0-255)"},
{"fill_noise", (PyCFunction)fill_noise, METH_VARARGS | METH_KEYWORDS,
"fill_noise(min_coord, max_coord, material, threshold=0.5, scale=0.1, seed=0) -> None\n\n"
"Fill region with 3D noise-based pattern (caves, clouds).\n\n"
"Args:\n"
" min_coord: (x0, y0, z0) - minimum corner\n"
" max_coord: (x1, y1, z1) - maximum corner\n"
" material: material ID for solid areas\n"
" threshold: noise threshold (0-1, higher = more solid)\n"
" scale: noise scale (smaller = larger features)\n"
" seed: random seed (0 for default)"},
{"copy_region", (PyCFunction)copy_region, METH_VARARGS,
"copy_region(min_coord, max_coord) -> VoxelRegion\n\n"
"Copy a rectangular region to a VoxelRegion prefab.\n\n"
"Args:\n"
" min_coord: (x0, y0, z0) - minimum corner (inclusive)\n"
" max_coord: (x1, y1, z1) - maximum corner (inclusive)\n\n"
"Returns:\n"
" VoxelRegion object that can be pasted elsewhere."},
{"paste_region", (PyCFunction)paste_region, METH_VARARGS | METH_KEYWORDS,
"paste_region(region, position, skip_air=True) -> None\n\n"
"Paste a VoxelRegion prefab at the specified position.\n\n"
"Args:\n"
" region: VoxelRegion from copy_region()\n"
" position: (x, y, z) - paste destination\n"
" skip_air: if True, air voxels don't overwrite (default True)"},
{"clear", (PyCFunction)clear, METH_NOARGS,
"clear() -> None\n\n"
"Clear the grid (fill with air, material 0)."},
@ -568,6 +1192,53 @@ PyMethodDef PyVoxelGrid::methods[] = {
{"count_material", (PyCFunction)count_material, METH_VARARGS,
"count_material(material) -> int\n\n"
"Count the number of voxels with the specified material ID."},
{"project_column", (PyCFunction)project_column, METH_VARARGS | METH_KEYWORDS,
"project_column(x, z, headroom=2) -> dict\n\n"
"Project a single column to navigation info.\n\n"
"Scans the column from top to bottom, finding the topmost floor\n"
"(solid voxel with air above) and checking for adequate headroom.\n\n"
"Args:\n"
" x: X coordinate in voxel grid\n"
" z: Z coordinate in voxel grid\n"
" headroom: Required air voxels above floor (default 2)\n\n"
"Returns:\n"
" dict with keys:\n"
" height (float): World Y of floor surface\n"
" walkable (bool): True if floor found with adequate headroom\n"
" transparent (bool): True if no opaque voxels in column\n"
" path_cost (float): Floor material's path cost"},
{"save", (PyCFunction)PyVoxelGrid::save, METH_VARARGS,
"save(path) -> bool\n\n"
"Save the voxel grid to a binary file.\n\n"
"Args:\n"
" path: File path to save to (.mcvg extension recommended)\n\n"
"Returns:\n"
" True on success, False on failure.\n\n"
"The file format includes grid dimensions, cell size, material palette,\n"
"and RLE-compressed voxel data."},
{"load", (PyCFunction)PyVoxelGrid::load, METH_VARARGS,
"load(path) -> bool\n\n"
"Load voxel data from a binary file.\n\n"
"Args:\n"
" path: File path to load from\n\n"
"Returns:\n"
" True on success, False on failure.\n\n"
"Note: This replaces the current grid data entirely, including\n"
"dimensions and material palette."},
{"to_bytes", (PyCFunction)PyVoxelGrid::to_bytes, METH_NOARGS,
"to_bytes() -> bytes\n\n"
"Serialize the voxel grid to a bytes object.\n\n"
"Returns:\n"
" bytes object containing the serialized grid data.\n\n"
"Useful for network transmission or custom storage."},
{"from_bytes", (PyCFunction)PyVoxelGrid::from_bytes, METH_VARARGS,
"from_bytes(data) -> bool\n\n"
"Load voxel data from a bytes object.\n\n"
"Args:\n"
" data: bytes object containing serialized grid data\n\n"
"Returns:\n"
" True on success, False on failure.\n\n"
"Note: This replaces the current grid data entirely."},
{nullptr} // Sentinel
};
@ -594,5 +1265,7 @@ PyGetSetDef PyVoxelGrid::getsetters[] = {
"World-space position (x, y, z) of the grid origin.", nullptr},
{"rotation", (getter)get_rotation, (setter)set_rotation,
"Y-axis rotation in degrees.", nullptr},
{"greedy_meshing", (getter)get_greedy_meshing, (setter)set_greedy_meshing,
"Enable greedy meshing optimization (reduces vertex count for uniform regions).", nullptr},
{nullptr} // Sentinel
};

View file

@ -1,5 +1,5 @@
// PyVoxelGrid.h - Python bindings for VoxelGrid
// Part of McRogueFace 3D Extension - Milestone 9
// Part of McRogueFace 3D Extension - Milestones 9, 11
#pragma once
#include "../Common.h"
@ -7,11 +7,8 @@
#include "VoxelGrid.h"
#include <memory>
// Forward declaration
class PyVoxelGrid;
// =============================================================================
// Python object structure
// Python object structures
// =============================================================================
typedef struct PyVoxelGridObject {
@ -20,8 +17,13 @@ typedef struct PyVoxelGridObject {
PyObject* weakreflist;
} PyVoxelGridObject;
typedef struct PyVoxelRegionObject {
PyObject_HEAD
std::shared_ptr<mcrf::VoxelRegion> data;
} PyVoxelRegionObject;
// =============================================================================
// Python binding class
// Python binding classes
// =============================================================================
class PyVoxelGrid {
@ -46,6 +48,10 @@ public:
static PyObject* get_rotation(PyVoxelGridObject* self, void* closure);
static int set_rotation(PyVoxelGridObject* self, PyObject* value, void* closure);
// Properties - mesh generation (Milestone 13)
static PyObject* get_greedy_meshing(PyVoxelGridObject* self, void* closure);
static int set_greedy_meshing(PyVoxelGridObject* self, PyObject* value, void* closure);
// Voxel access methods
static PyObject* get(PyVoxelGridObject* self, PyObject* args);
static PyObject* set(PyVoxelGridObject* self, PyObject* args);
@ -59,10 +65,26 @@ public:
static PyObject* fill_box(PyVoxelGridObject* self, PyObject* args);
static PyObject* clear(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
// Bulk operations - Milestone 11
static PyObject* fill_box_hollow(PyVoxelGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* fill_sphere(PyVoxelGridObject* self, PyObject* args);
static PyObject* fill_cylinder(PyVoxelGridObject* self, PyObject* args);
static PyObject* fill_noise(PyVoxelGridObject* self, PyObject* args, PyObject* kwds);
// Copy/paste operations - Milestone 11
static PyObject* copy_region(PyVoxelGridObject* self, PyObject* args);
static PyObject* paste_region(PyVoxelGridObject* self, PyObject* args, PyObject* kwds);
// Mesh caching (Milestone 10)
static PyObject* get_vertex_count(PyVoxelGridObject* self, void* closure);
static PyObject* rebuild_mesh(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
// Serialization (Milestone 14)
static PyObject* save(PyVoxelGridObject* self, PyObject* args);
static PyObject* load(PyVoxelGridObject* self, PyObject* args);
static PyObject* to_bytes(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
static PyObject* from_bytes(PyVoxelGridObject* self, PyObject* args);
// Statistics
static PyObject* count_non_air(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
static PyObject* count_material(PyVoxelGridObject* self, PyObject* args);
@ -72,8 +94,20 @@ public:
static PyGetSetDef getsetters[];
};
class PyVoxelRegion {
public:
static void dealloc(PyVoxelRegionObject* self);
static PyObject* repr(PyObject* obj);
static PyObject* get_size(PyVoxelRegionObject* self, void* closure);
static PyObject* get_width(PyVoxelRegionObject* self, void* closure);
static PyObject* get_height(PyVoxelRegionObject* self, void* closure);
static PyObject* get_depth(PyVoxelRegionObject* self, void* closure);
static PyGetSetDef getsetters[];
};
// =============================================================================
// Python type definition
// Python type definitions (in mcrfpydef namespace)
// =============================================================================
namespace mcrfpydef {
@ -122,4 +156,24 @@ inline PyTypeObject PyVoxelGridType = {
.tp_new = PyVoxelGrid::pynew,
};
inline PyTypeObject PyVoxelRegionType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.VoxelRegion",
.tp_basicsize = sizeof(PyVoxelRegionObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyVoxelRegion::dealloc,
.tp_repr = PyVoxelRegion::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"VoxelRegion - Portable voxel data for copy/paste operations.\n\n"
"Created by VoxelGrid.copy_region(), used with paste_region().\n"
"Cannot be instantiated directly.\n\n"
"Properties:\n"
" size (tuple, read-only): Dimensions as (width, height, depth)\n"
" width, height, depth (int, read-only): Individual dimensions"
),
.tp_getset = nullptr, // Set before PyType_Ready
.tp_new = nullptr, // Cannot instantiate directly
};
} // namespace mcrfpydef

View file

@ -872,6 +872,104 @@ bool Viewport3D::removeVoxelLayer(std::shared_ptr<VoxelGrid> grid) {
return false;
}
// =============================================================================
// Voxel-to-Nav Projection (Milestone 12)
// =============================================================================
void Viewport3D::clearVoxelNavRegion(std::shared_ptr<VoxelGrid> grid) {
if (!grid || navGrid_.empty()) return;
// Get voxel grid offset in world space
vec3 offset = grid->getOffset();
float cellSize = grid->cellSize();
// Calculate nav grid cell offset from voxel grid offset
int navOffsetX = static_cast<int>(std::floor(offset.x / cellSize_));
int navOffsetZ = static_cast<int>(std::floor(offset.z / cellSize_));
// Clear nav cells corresponding to voxel grid footprint
for (int vz = 0; vz < grid->depth(); vz++) {
for (int vx = 0; vx < grid->width(); vx++) {
int navX = navOffsetX + vx;
int navZ = navOffsetZ + vz;
if (isValidCell(navX, navZ)) {
VoxelPoint& cell = at(navX, navZ);
cell.walkable = true;
cell.transparent = true;
cell.height = 0.0f;
cell.cost = 1.0f;
}
}
}
// Sync to TCOD
syncToTCOD();
}
void Viewport3D::projectVoxelToNav(std::shared_ptr<VoxelGrid> grid, int headroom) {
if (!grid || navGrid_.empty()) return;
// Get voxel grid offset in world space
vec3 offset = grid->getOffset();
float voxelCellSize = grid->cellSize();
// Calculate nav grid cell offset from voxel grid offset
// Assuming nav cell size matches voxel cell size for 1:1 mapping
int navOffsetX = static_cast<int>(std::floor(offset.x / cellSize_));
int navOffsetZ = static_cast<int>(std::floor(offset.z / cellSize_));
// Project each column of the voxel grid to the navigation grid
for (int vz = 0; vz < grid->depth(); vz++) {
for (int vx = 0; vx < grid->width(); vx++) {
int navX = navOffsetX + vx;
int navZ = navOffsetZ + vz;
if (!isValidCell(navX, navZ)) continue;
// Get projection info from voxel column
VoxelGrid::NavInfo navInfo = grid->projectColumn(vx, vz, headroom);
// Update nav cell
VoxelPoint& cell = at(navX, navZ);
cell.height = navInfo.height + offset.y; // Add world Y offset
cell.walkable = navInfo.walkable;
cell.transparent = navInfo.transparent;
cell.cost = navInfo.pathCost;
// Sync this cell to TCOD
syncTCODCell(navX, navZ);
}
}
}
void Viewport3D::projectAllVoxelsToNav(int headroom) {
if (navGrid_.empty()) return;
// First, reset all nav cells to default state
for (auto& cell : navGrid_) {
cell.walkable = true;
cell.transparent = true;
cell.height = 0.0f;
cell.cost = 1.0f;
}
// Project each voxel layer in order (later layers overwrite earlier)
// Sort by z_index so higher z_index layers take precedence
std::vector<std::pair<std::shared_ptr<VoxelGrid>, int>> sortedLayers = voxelLayers_;
std::sort(sortedLayers.begin(), sortedLayers.end(),
[](const auto& a, const auto& b) { return a.second < b.second; });
for (const auto& pair : sortedLayers) {
if (pair.first) {
projectVoxelToNav(pair.first, headroom);
}
}
// Final sync to TCOD (redundant but ensures consistency)
syncToTCOD();
}
void Viewport3D::renderVoxelLayers(const mat4& view, const mat4& proj) {
#ifdef MCRF_HAS_GL
if (voxelLayers_.empty() || !shader_ || !shader_->isValid()) {
@ -2475,6 +2573,99 @@ static PyObject* Viewport3D_voxel_layer_count(PyViewport3DObject* self, PyObject
return PyLong_FromSize_t(self->data->getVoxelLayerCount());
}
// =============================================================================
// Voxel-to-Nav Projection Methods (Milestone 12)
// =============================================================================
static PyObject* Viewport3D_project_voxel_to_nav(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"voxel_grid", "headroom", NULL};
PyObject* voxel_grid_obj = nullptr;
int headroom = 2;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|i", const_cast<char**>(kwlist),
&voxel_grid_obj, &headroom)) {
return NULL;
}
// Check if it's a VoxelGrid object
PyTypeObject* voxelGridType = (PyTypeObject*)PyObject_GetAttrString(
McRFPy_API::mcrf_module, "VoxelGrid");
if (!voxelGridType) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid type not found");
return NULL;
}
if (!PyObject_IsInstance(voxel_grid_obj, (PyObject*)voxelGridType)) {
Py_DECREF(voxelGridType);
PyErr_SetString(PyExc_TypeError, "voxel_grid must be a VoxelGrid object");
return NULL;
}
Py_DECREF(voxelGridType);
PyVoxelGridObject* vg = (PyVoxelGridObject*)voxel_grid_obj;
if (!vg->data) {
PyErr_SetString(PyExc_ValueError, "VoxelGrid not initialized");
return NULL;
}
if (headroom < 0) {
PyErr_SetString(PyExc_ValueError, "headroom must be non-negative");
return NULL;
}
self->data->projectVoxelToNav(vg->data, headroom);
Py_RETURN_NONE;
}
static PyObject* Viewport3D_project_all_voxels_to_nav(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"headroom", NULL};
int headroom = 2;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", const_cast<char**>(kwlist), &headroom)) {
return NULL;
}
if (headroom < 0) {
PyErr_SetString(PyExc_ValueError, "headroom must be non-negative");
return NULL;
}
self->data->projectAllVoxelsToNav(headroom);
Py_RETURN_NONE;
}
static PyObject* Viewport3D_clear_voxel_nav_region(PyViewport3DObject* self, PyObject* args) {
PyObject* voxel_grid_obj = nullptr;
if (!PyArg_ParseTuple(args, "O", &voxel_grid_obj)) {
return NULL;
}
// Check if it's a VoxelGrid object
PyTypeObject* voxelGridType = (PyTypeObject*)PyObject_GetAttrString(
McRFPy_API::mcrf_module, "VoxelGrid");
if (!voxelGridType) {
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid type not found");
return NULL;
}
if (!PyObject_IsInstance(voxel_grid_obj, (PyObject*)voxelGridType)) {
Py_DECREF(voxelGridType);
PyErr_SetString(PyExc_TypeError, "voxel_grid must be a VoxelGrid object");
return NULL;
}
Py_DECREF(voxelGridType);
PyVoxelGridObject* vg = (PyVoxelGridObject*)voxel_grid_obj;
if (!vg->data) {
PyErr_SetString(PyExc_ValueError, "VoxelGrid not initialized");
return NULL;
}
self->data->clearVoxelNavRegion(vg->data);
Py_RETURN_NONE;
}
} // namespace mcrf
// Methods array - outside namespace but PyObjectType still in scope via typedef
@ -2674,5 +2865,29 @@ PyMethodDef Viewport3D_methods[] = {
"Get the number of voxel layers.\n\n"
"Returns:\n"
" Number of voxel layers in the viewport"},
// Voxel-to-Nav projection methods (Milestone 12)
{"project_voxel_to_nav", (PyCFunction)mcrf::Viewport3D_project_voxel_to_nav, METH_VARARGS | METH_KEYWORDS,
"project_voxel_to_nav(voxel_grid, headroom=2)\n\n"
"Project a VoxelGrid to the navigation grid.\n\n"
"Scans each column of the voxel grid and updates corresponding\n"
"navigation cells with walkability, transparency, height, and cost.\n\n"
"Args:\n"
" voxel_grid: VoxelGrid to project\n"
" headroom: Required air voxels above floor for walkability (default: 2)"},
{"project_all_voxels_to_nav", (PyCFunction)mcrf::Viewport3D_project_all_voxels_to_nav, METH_VARARGS | METH_KEYWORDS,
"project_all_voxels_to_nav(headroom=2)\n\n"
"Project all voxel layers to the navigation grid.\n\n"
"Resets navigation grid and projects each voxel layer in z_index order.\n"
"Later layers (higher z_index) overwrite earlier ones.\n\n"
"Args:\n"
" headroom: Required air voxels above floor for walkability (default: 2)"},
{"clear_voxel_nav_region", (PyCFunction)mcrf::Viewport3D_clear_voxel_nav_region, METH_VARARGS,
"clear_voxel_nav_region(voxel_grid)\n\n"
"Clear navigation cells in a voxel grid's footprint.\n\n"
"Resets walkability, transparency, height, and cost to defaults\n"
"for all nav cells corresponding to the voxel grid's XZ extent.\n\n"
"Args:\n"
" voxel_grid: VoxelGrid whose nav region to clear"},
{NULL} // Sentinel
};

View file

@ -250,6 +250,23 @@ public:
/// Render all voxel layers
void renderVoxelLayers(const mat4& view, const mat4& proj);
// =========================================================================
// Voxel-to-Nav Projection (Milestone 12)
// =========================================================================
/// Project a single voxel grid to the navigation grid
/// @param grid The voxel grid to project
/// @param headroom Required air voxels above floor for walkability
void projectVoxelToNav(std::shared_ptr<VoxelGrid> grid, int headroom = 2);
/// Project all voxel layers to the navigation grid
/// @param headroom Required air voxels above floor for walkability
void projectAllVoxelsToNav(int headroom = 2);
/// Clear nav cells in a voxel grid's footprint (before re-projection)
/// @param grid The voxel grid whose footprint to clear
void clearVoxelNavRegion(std::shared_ptr<VoxelGrid> grid);
// Background color
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
sf::Color getBackgroundColor() const { return bgColor_; }

View file

@ -1,9 +1,13 @@
// VoxelGrid.cpp - Dense 3D voxel array implementation
// Part of McRogueFace 3D Extension - Milestones 9-10
// Part of McRogueFace 3D Extension - Milestones 9-11
#include "VoxelGrid.h"
#include "VoxelMesher.h"
#include "MeshLayer.h" // For MeshVertex
#include <cmath>
#include <algorithm>
#include <cstring> // For memcpy, memcmp
#include <fstream> // For file I/O
namespace mcrf {
@ -151,6 +155,297 @@ void VoxelGrid::fillBox(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t
meshDirty_ = true;
}
// =============================================================================
// Bulk Operations - Milestone 11
// =============================================================================
void VoxelGrid::fillBoxHollow(int x0, int y0, int z0, int x1, int y1, int z1,
uint8_t material, int thickness) {
// Ensure proper ordering (min to max)
if (x0 > x1) std::swap(x0, x1);
if (y0 > y1) std::swap(y0, y1);
if (z0 > z1) std::swap(z0, z1);
// Fill entire box with material
fillBox(x0, y0, z0, x1, y1, z1, material);
// Carve out interior (inset by thickness on all sides)
int ix0 = x0 + thickness;
int iy0 = y0 + thickness;
int iz0 = z0 + thickness;
int ix1 = x1 - thickness;
int iy1 = y1 - thickness;
int iz1 = z1 - thickness;
// Only carve if there's interior space
if (ix0 <= ix1 && iy0 <= iy1 && iz0 <= iz1) {
fillBox(ix0, iy0, iz0, ix1, iy1, iz1, 0); // Air
}
// meshDirty_ already set by fillBox calls
}
void VoxelGrid::fillSphere(int cx, int cy, int cz, int radius, uint8_t material) {
int r2 = radius * radius;
for (int z = cz - radius; z <= cz + radius; z++) {
for (int y = cy - radius; y <= cy + radius; y++) {
for (int x = cx - radius; x <= cx + radius; x++) {
int dx = x - cx;
int dy = y - cy;
int dz = z - cz;
if (dx * dx + dy * dy + dz * dz <= r2) {
if (isValid(x, y, z)) {
data_[index(x, y, z)] = material;
}
}
}
}
}
meshDirty_ = true;
}
void VoxelGrid::fillCylinder(int cx, int cy, int cz, int radius, int height, uint8_t material) {
int r2 = radius * radius;
for (int y = cy; y < cy + height; y++) {
for (int z = cz - radius; z <= cz + radius; z++) {
for (int x = cx - radius; x <= cx + radius; x++) {
int dx = x - cx;
int dz = z - cz;
if (dx * dx + dz * dz <= r2) {
if (isValid(x, y, z)) {
data_[index(x, y, z)] = material;
}
}
}
}
}
meshDirty_ = true;
}
// Simple 3D noise implementation (hash-based, similar to value noise)
namespace {
// Simple hash function for noise
inline unsigned int hash3D(int x, int y, int z, unsigned int seed) {
unsigned int h = seed;
h ^= static_cast<unsigned int>(x) * 374761393u;
h ^= static_cast<unsigned int>(y) * 668265263u;
h ^= static_cast<unsigned int>(z) * 2147483647u;
h = (h ^ (h >> 13)) * 1274126177u;
return h;
}
// Convert hash to 0-1 float
inline float hashToFloat(unsigned int h) {
return static_cast<float>(h & 0xFFFFFF) / static_cast<float>(0xFFFFFF);
}
// Linear interpolation
inline float lerp(float a, float b, float t) {
return a + t * (b - a);
}
// Smoothstep for smoother interpolation
inline float smoothstep(float t) {
return t * t * (3.0f - 2.0f * t);
}
// 3D value noise
float noise3D(float x, float y, float z, unsigned int seed) {
int xi = static_cast<int>(std::floor(x));
int yi = static_cast<int>(std::floor(y));
int zi = static_cast<int>(std::floor(z));
float xf = x - xi;
float yf = y - yi;
float zf = z - zi;
// Smoothstep the fractions
float u = smoothstep(xf);
float v = smoothstep(yf);
float w = smoothstep(zf);
// Hash corners of the unit cube
float c000 = hashToFloat(hash3D(xi, yi, zi, seed));
float c100 = hashToFloat(hash3D(xi + 1, yi, zi, seed));
float c010 = hashToFloat(hash3D(xi, yi + 1, zi, seed));
float c110 = hashToFloat(hash3D(xi + 1, yi + 1, zi, seed));
float c001 = hashToFloat(hash3D(xi, yi, zi + 1, seed));
float c101 = hashToFloat(hash3D(xi + 1, yi, zi + 1, seed));
float c011 = hashToFloat(hash3D(xi, yi + 1, zi + 1, seed));
float c111 = hashToFloat(hash3D(xi + 1, yi + 1, zi + 1, seed));
// Trilinear interpolation
float x00 = lerp(c000, c100, u);
float x10 = lerp(c010, c110, u);
float x01 = lerp(c001, c101, u);
float x11 = lerp(c011, c111, u);
float y0 = lerp(x00, x10, v);
float y1 = lerp(x01, x11, v);
return lerp(y0, y1, w);
}
}
void VoxelGrid::fillNoise(int x0, int y0, int z0, int x1, int y1, int z1,
uint8_t material, float threshold, float scale, unsigned int seed) {
// Ensure proper ordering
if (x0 > x1) std::swap(x0, x1);
if (y0 > y1) std::swap(y0, y1);
if (z0 > z1) std::swap(z0, z1);
// Clamp to valid range
x0 = std::max(0, std::min(x0, width_ - 1));
x1 = std::max(0, std::min(x1, width_ - 1));
y0 = std::max(0, std::min(y0, height_ - 1));
y1 = std::max(0, std::min(y1, height_ - 1));
z0 = std::max(0, std::min(z0, depth_ - 1));
z1 = std::max(0, std::min(z1, depth_ - 1));
for (int z = z0; z <= z1; z++) {
for (int y = y0; y <= y1; y++) {
for (int x = x0; x <= x1; x++) {
float n = noise3D(x * scale, y * scale, z * scale, seed);
if (n > threshold) {
data_[index(x, y, z)] = material;
}
}
}
}
meshDirty_ = true;
}
// =============================================================================
// Copy/Paste Operations - Milestone 11
// =============================================================================
VoxelRegion VoxelGrid::copyRegion(int x0, int y0, int z0, int x1, int y1, int z1) const {
// Ensure proper ordering
if (x0 > x1) std::swap(x0, x1);
if (y0 > y1) std::swap(y0, y1);
if (z0 > z1) std::swap(z0, z1);
// Clamp to valid range
x0 = std::max(0, std::min(x0, width_ - 1));
x1 = std::max(0, std::min(x1, width_ - 1));
y0 = std::max(0, std::min(y0, height_ - 1));
y1 = std::max(0, std::min(y1, height_ - 1));
z0 = std::max(0, std::min(z0, depth_ - 1));
z1 = std::max(0, std::min(z1, depth_ - 1));
int rw = x1 - x0 + 1;
int rh = y1 - y0 + 1;
int rd = z1 - z0 + 1;
VoxelRegion region(rw, rh, rd);
for (int rz = 0; rz < rd; rz++) {
for (int ry = 0; ry < rh; ry++) {
for (int rx = 0; rx < rw; rx++) {
int sx = x0 + rx;
int sy = y0 + ry;
int sz = z0 + rz;
size_t ri = static_cast<size_t>(rz) * (rw * rh) +
static_cast<size_t>(ry) * rw + rx;
region.data[ri] = get(sx, sy, sz);
}
}
}
return region;
}
void VoxelGrid::pasteRegion(const VoxelRegion& region, int x, int y, int z, bool skipAir) {
if (!region.isValid()) return;
for (int rz = 0; rz < region.depth; rz++) {
for (int ry = 0; ry < region.height; ry++) {
for (int rx = 0; rx < region.width; rx++) {
size_t ri = static_cast<size_t>(rz) * (region.width * region.height) +
static_cast<size_t>(ry) * region.width + rx;
uint8_t mat = region.data[ri];
if (skipAir && mat == 0) continue;
int dx = x + rx;
int dy = y + ry;
int dz = z + rz;
if (isValid(dx, dy, dz)) {
data_[index(dx, dy, dz)] = mat;
}
}
}
}
meshDirty_ = true;
}
// =============================================================================
// Navigation Projection - Milestone 12
// =============================================================================
VoxelGrid::NavInfo VoxelGrid::projectColumn(int x, int z, int headroom) const {
NavInfo info;
info.height = 0.0f;
info.walkable = false;
info.transparent = true;
info.pathCost = 1.0f;
// Out of bounds check
if (x < 0 || x >= width_ || z < 0 || z >= depth_) {
return info;
}
// Scan from top to bottom, find first solid with air above (floor)
int floorY = -1;
for (int y = height_ - 1; y >= 0; y--) {
uint8_t mat = get(x, y, z);
if (mat != 0) {
// Found solid - check if it's a floor (air above) or ceiling
bool hasAirAbove = (y == height_ - 1) || (get(x, y + 1, z) == 0);
if (hasAirAbove) {
floorY = y;
break;
}
}
}
if (floorY >= 0) {
// Found a floor
info.height = (floorY + 1) * cellSize_; // Top of floor voxel
info.walkable = true;
// Check headroom (need enough air voxels above floor)
int airCount = 0;
for (int y = floorY + 1; y < height_; y++) {
if (get(x, y, z) == 0) {
airCount++;
} else {
break;
}
}
if (airCount < headroom) {
info.walkable = false; // Can't fit entity
}
// Get path cost from floor material
uint8_t floorMat = get(x, floorY, z);
info.pathCost = getMaterial(floorMat).pathCost;
}
// Check transparency: any non-transparent solid in column blocks FOV
for (int y = 0; y < height_; y++) {
uint8_t mat = get(x, y, z);
if (mat != 0 && !getMaterial(mat).transparent) {
info.transparent = false;
break;
}
}
return info;
}
// =============================================================================
// Mesh Caching (Milestone 10)
// =============================================================================
@ -164,8 +459,337 @@ const std::vector<MeshVertex>& VoxelGrid::getVertices() const {
void VoxelGrid::rebuildMesh() const {
cachedVertices_.clear();
VoxelMesher::generateMesh(*this, cachedVertices_);
if (greedyMeshing_) {
VoxelMesher::generateGreedyMesh(*this, cachedVertices_);
} else {
VoxelMesher::generateMesh(*this, cachedVertices_);
}
meshDirty_ = false;
}
// =============================================================================
// Serialization - Milestone 14
// =============================================================================
// File format:
// Magic "MCVG" (4 bytes)
// Version (1 byte) - currently 1
// Width, Height, Depth (3 x int32 = 12 bytes)
// Cell Size (float32 = 4 bytes)
// Material count (uint8 = 1 byte)
// For each material:
// Name length (uint16) + name bytes
// Color RGBA (4 bytes)
// Sprite index (int32)
// Transparent (uint8)
// Path cost (float32)
// Voxel data length (uint32)
// Voxel data: RLE encoded (run_length: uint8, material: uint8) pairs
// If run_length == 255, read extended_length: uint16 for longer runs
namespace {
const char MAGIC[4] = {'M', 'C', 'V', 'G'};
const uint8_t FORMAT_VERSION = 1;
// Write helpers
void writeU8(std::vector<uint8_t>& buf, uint8_t v) {
buf.push_back(v);
}
void writeU16(std::vector<uint8_t>& buf, uint16_t v) {
buf.push_back(static_cast<uint8_t>(v & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
}
void writeI32(std::vector<uint8_t>& buf, int32_t v) {
buf.push_back(static_cast<uint8_t>(v & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 16) & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 24) & 0xFF));
}
void writeU32(std::vector<uint8_t>& buf, uint32_t v) {
buf.push_back(static_cast<uint8_t>(v & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 16) & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 24) & 0xFF));
}
void writeF32(std::vector<uint8_t>& buf, float v) {
static_assert(sizeof(float) == 4, "Expected 4-byte float");
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&v);
buf.insert(buf.end(), bytes, bytes + 4);
}
void writeString(std::vector<uint8_t>& buf, const std::string& s) {
uint16_t len = static_cast<uint16_t>(std::min(s.size(), size_t(65535)));
writeU16(buf, len);
buf.insert(buf.end(), s.begin(), s.begin() + len);
}
// Read helpers
class Reader {
const uint8_t* data_;
size_t size_;
size_t pos_;
public:
Reader(const uint8_t* data, size_t size) : data_(data), size_(size), pos_(0) {}
bool hasBytes(size_t n) const { return pos_ + n <= size_; }
size_t position() const { return pos_; }
bool readU8(uint8_t& v) {
if (!hasBytes(1)) return false;
v = data_[pos_++];
return true;
}
bool readU16(uint16_t& v) {
if (!hasBytes(2)) return false;
v = static_cast<uint16_t>(data_[pos_]) |
(static_cast<uint16_t>(data_[pos_ + 1]) << 8);
pos_ += 2;
return true;
}
bool readI32(int32_t& v) {
if (!hasBytes(4)) return false;
v = static_cast<int32_t>(data_[pos_]) |
(static_cast<int32_t>(data_[pos_ + 1]) << 8) |
(static_cast<int32_t>(data_[pos_ + 2]) << 16) |
(static_cast<int32_t>(data_[pos_ + 3]) << 24);
pos_ += 4;
return true;
}
bool readU32(uint32_t& v) {
if (!hasBytes(4)) return false;
v = static_cast<uint32_t>(data_[pos_]) |
(static_cast<uint32_t>(data_[pos_ + 1]) << 8) |
(static_cast<uint32_t>(data_[pos_ + 2]) << 16) |
(static_cast<uint32_t>(data_[pos_ + 3]) << 24);
pos_ += 4;
return true;
}
bool readF32(float& v) {
if (!hasBytes(4)) return false;
static_assert(sizeof(float) == 4, "Expected 4-byte float");
std::memcpy(&v, data_ + pos_, 4);
pos_ += 4;
return true;
}
bool readString(std::string& s) {
uint16_t len;
if (!readU16(len)) return false;
if (!hasBytes(len)) return false;
s.assign(reinterpret_cast<const char*>(data_ + pos_), len);
pos_ += len;
return true;
}
bool readBytes(uint8_t* out, size_t n) {
if (!hasBytes(n)) return false;
std::memcpy(out, data_ + pos_, n);
pos_ += n;
return true;
}
};
// RLE encode voxel data
void rleEncode(const std::vector<uint8_t>& data, std::vector<uint8_t>& out) {
if (data.empty()) return;
size_t i = 0;
while (i < data.size()) {
uint8_t mat = data[i];
size_t runStart = i;
// Count consecutive same materials
while (i < data.size() && data[i] == mat && (i - runStart) < 65535 + 255) {
i++;
}
size_t runLen = i - runStart;
if (runLen < 255) {
writeU8(out, static_cast<uint8_t>(runLen));
} else {
// Extended run: 255 marker + uint16 length
writeU8(out, 255);
writeU16(out, static_cast<uint16_t>(runLen - 255));
}
writeU8(out, mat);
}
}
// RLE decode voxel data
bool rleDecode(Reader& reader, std::vector<uint8_t>& data, size_t expectedSize) {
data.clear();
data.reserve(expectedSize);
while (data.size() < expectedSize) {
uint8_t runLen8;
if (!reader.readU8(runLen8)) return false;
size_t runLen = runLen8;
if (runLen8 == 255) {
uint16_t extLen;
if (!reader.readU16(extLen)) return false;
runLen = 255 + extLen;
}
uint8_t mat;
if (!reader.readU8(mat)) return false;
for (size_t j = 0; j < runLen && data.size() < expectedSize; j++) {
data.push_back(mat);
}
}
return data.size() == expectedSize;
}
}
bool VoxelGrid::saveToBuffer(std::vector<uint8_t>& buffer) const {
buffer.clear();
buffer.reserve(1024 + data_.size()); // Rough estimate
// Magic
buffer.insert(buffer.end(), MAGIC, MAGIC + 4);
// Version
writeU8(buffer, FORMAT_VERSION);
// Dimensions
writeI32(buffer, width_);
writeI32(buffer, height_);
writeI32(buffer, depth_);
// Cell size
writeF32(buffer, cellSize_);
// Materials
writeU8(buffer, static_cast<uint8_t>(materials_.size()));
for (const auto& mat : materials_) {
writeString(buffer, mat.name);
writeU8(buffer, mat.color.r);
writeU8(buffer, mat.color.g);
writeU8(buffer, mat.color.b);
writeU8(buffer, mat.color.a);
writeI32(buffer, mat.spriteIndex);
writeU8(buffer, mat.transparent ? 1 : 0);
writeF32(buffer, mat.pathCost);
}
// RLE encode voxel data
std::vector<uint8_t> rleData;
rleEncode(data_, rleData);
// Write RLE data length and data
writeU32(buffer, static_cast<uint32_t>(rleData.size()));
buffer.insert(buffer.end(), rleData.begin(), rleData.end());
return true;
}
bool VoxelGrid::loadFromBuffer(const uint8_t* data, size_t size) {
Reader reader(data, size);
// Check magic
uint8_t magic[4];
if (!reader.readBytes(magic, 4)) return false;
if (std::memcmp(magic, MAGIC, 4) != 0) return false;
// Check version
uint8_t version;
if (!reader.readU8(version)) return false;
if (version != FORMAT_VERSION) return false;
// Read dimensions
int32_t w, h, d;
if (!reader.readI32(w) || !reader.readI32(h) || !reader.readI32(d)) return false;
if (w <= 0 || h <= 0 || d <= 0) return false;
// Read cell size
float cs;
if (!reader.readF32(cs)) return false;
if (cs <= 0.0f) return false;
// Read materials
uint8_t matCount;
if (!reader.readU8(matCount)) return false;
std::vector<VoxelMaterial> newMaterials;
newMaterials.reserve(matCount);
for (uint8_t i = 0; i < matCount; i++) {
VoxelMaterial mat;
if (!reader.readString(mat.name)) return false;
uint8_t r, g, b, a;
if (!reader.readU8(r) || !reader.readU8(g) || !reader.readU8(b) || !reader.readU8(a))
return false;
mat.color = sf::Color(r, g, b, a);
int32_t sprite;
if (!reader.readI32(sprite)) return false;
mat.spriteIndex = sprite;
uint8_t transp;
if (!reader.readU8(transp)) return false;
mat.transparent = (transp != 0);
if (!reader.readF32(mat.pathCost)) return false;
newMaterials.push_back(mat);
}
// Read RLE data length
uint32_t rleLen;
if (!reader.readU32(rleLen)) return false;
// Decode voxel data
size_t expectedVoxels = static_cast<size_t>(w) * h * d;
std::vector<uint8_t> newData;
if (!rleDecode(reader, newData, expectedVoxels)) return false;
// Success - update the grid
width_ = w;
height_ = h;
depth_ = d;
cellSize_ = cs;
materials_ = std::move(newMaterials);
data_ = std::move(newData);
meshDirty_ = true;
return true;
}
bool VoxelGrid::save(const std::string& path) const {
std::vector<uint8_t> buffer;
if (!saveToBuffer(buffer)) return false;
std::ofstream file(path, std::ios::binary);
if (!file) return false;
file.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
return file.good();
}
bool VoxelGrid::load(const std::string& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) return false;
std::streamsize size = file.tellg();
if (size <= 0) return false;
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(static_cast<size_t>(size));
if (!file.read(reinterpret_cast<char*>(buffer.data()), size)) return false;
return loadFromBuffer(buffer.data(), buffer.size());
}
} // namespace mcrf

View file

@ -1,5 +1,5 @@
// VoxelGrid.h - Dense 3D voxel array with material palette
// Part of McRogueFace 3D Extension - Milestones 9-10
// Part of McRogueFace 3D Extension - Milestones 9-11
#pragma once
#include "../Common.h"
@ -8,6 +8,7 @@
#include <vector>
#include <string>
#include <stdexcept>
#include <cstdint>
namespace mcrf {
@ -28,6 +29,22 @@ struct VoxelMaterial {
: name(n), color(c), spriteIndex(sprite), transparent(transp), pathCost(cost) {}
};
// =============================================================================
// VoxelRegion - Portable voxel data for copy/paste operations (Milestone 11)
// =============================================================================
struct VoxelRegion {
int width, height, depth;
std::vector<uint8_t> data;
VoxelRegion() : width(0), height(0), depth(0) {}
VoxelRegion(int w, int h, int d) : width(w), height(h), depth(d),
data(static_cast<size_t>(w) * h * d, 0) {}
bool isValid() const { return width > 0 && height > 0 && depth > 0; }
size_t totalVoxels() const { return static_cast<size_t>(width) * height * depth; }
};
// =============================================================================
// VoxelGrid - Dense 3D array of material IDs
// =============================================================================
@ -43,9 +60,10 @@ private:
vec3 offset_;
float rotation_ = 0.0f; // Y-axis only, degrees
// Mesh caching (Milestone 10)
// Mesh caching (Milestones 10, 13)
mutable bool meshDirty_ = true;
mutable std::vector<MeshVertex> cachedVertices_;
bool greedyMeshing_ = false; // Use greedy meshing algorithm
// Index calculation (row-major: X varies fastest, then Y, then Z)
inline size_t index(int x, int y, int z) const {
@ -79,11 +97,39 @@ public:
const VoxelMaterial& getMaterial(uint8_t id) const;
size_t materialCount() const { return materials_.size(); }
// Bulk operations
// Bulk operations - Basic
void fill(uint8_t material);
void clear() { fill(0); }
void fillBox(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t material);
// Bulk operations - Milestone 11
void fillBoxHollow(int x0, int y0, int z0, int x1, int y1, int z1,
uint8_t material, int thickness = 1);
void fillSphere(int cx, int cy, int cz, int radius, uint8_t material);
void fillCylinder(int cx, int cy, int cz, int radius, int height, uint8_t material);
void fillNoise(int x0, int y0, int z0, int x1, int y1, int z1,
uint8_t material, float threshold = 0.5f,
float scale = 0.1f, unsigned int seed = 0);
// Copy/paste operations - Milestone 11
VoxelRegion copyRegion(int x0, int y0, int z0, int x1, int y1, int z1) const;
void pasteRegion(const VoxelRegion& region, int x, int y, int z, bool skipAir = true);
// Navigation projection - Milestone 12
struct NavInfo {
float height = 0.0f;
bool walkable = false;
bool transparent = true;
float pathCost = 1.0f;
};
/// Project a single column to get navigation info
/// @param x X coordinate in voxel grid
/// @param z Z coordinate in voxel grid
/// @param headroom Required air voxels above floor (default 2)
/// @return Navigation info for this column
NavInfo projectColumn(int x, int z, int headroom = 2) const;
// Transform
void setOffset(const vec3& offset) { offset_ = offset; }
void setOffset(float x, float y, float z) { offset_ = vec3(x, y, z); }
@ -96,7 +142,7 @@ public:
size_t countNonAir() const;
size_t countMaterial(uint8_t material) const;
// Mesh caching (Milestone 10)
// Mesh caching (Milestones 10, 13)
/// Mark mesh as needing rebuild (called automatically by set/fill operations)
void markDirty() { meshDirty_ = true; }
@ -112,10 +158,37 @@ public:
/// Get vertex count after mesh generation
size_t vertexCount() const { return cachedVertices_.size(); }
/// Enable/disable greedy meshing (Milestone 13)
/// Greedy meshing merges coplanar faces to reduce vertex count
void setGreedyMeshing(bool enabled) { greedyMeshing_ = enabled; markDirty(); }
bool isGreedyMeshingEnabled() const { return greedyMeshing_; }
// Memory info (for debugging)
size_t memoryUsageBytes() const {
return data_.size() + materials_.size() * sizeof(VoxelMaterial);
}
// Serialization (Milestone 14)
/// Save voxel grid to binary file
/// @param path File path to save to
/// @return true on success
bool save(const std::string& path) const;
/// Load voxel grid from binary file
/// @param path File path to load from
/// @return true on success
bool load(const std::string& path);
/// Save to memory buffer
/// @param buffer Output buffer (resized as needed)
/// @return true on success
bool saveToBuffer(std::vector<uint8_t>& buffer) const;
/// Load from memory buffer
/// @param data Buffer to load from
/// @param size Buffer size
/// @return true on success
bool loadFromBuffer(const uint8_t* data, size_t size);
};
} // namespace mcrf

View file

@ -70,6 +70,187 @@ bool VoxelMesher::shouldGenerateFace(const VoxelGrid& grid,
return grid.getMaterial(neighbor).transparent;
}
void VoxelMesher::emitQuad(std::vector<MeshVertex>& vertices,
const vec3& corner,
const vec3& uAxis,
const vec3& vAxis,
const vec3& normal,
const VoxelMaterial& material) {
// 4 corners of the quad
vec3 corners[4] = {
corner, // 0: origin
corner + uAxis, // 1: +U
corner + uAxis + vAxis, // 2: +U+V
corner + vAxis // 3: +V
};
// Calculate UV based on quad size (for potential texture tiling)
float uLen = uAxis.length();
float vLen = vAxis.length();
vec2 uvs[4] = {
vec2(0, 0), // 0
vec2(uLen, 0), // 1
vec2(uLen, vLen), // 2
vec2(0, vLen) // 3
};
// Color from material
vec4 color(
material.color.r / 255.0f,
material.color.g / 255.0f,
material.color.b / 255.0f,
material.color.a / 255.0f
);
// Emit 2 triangles (6 vertices) - CCW winding
// Triangle 1: 0-2-1
vertices.push_back(MeshVertex(corners[0], uvs[0], normal, color));
vertices.push_back(MeshVertex(corners[2], uvs[2], normal, color));
vertices.push_back(MeshVertex(corners[1], uvs[1], normal, color));
// Triangle 2: 0-3-2
vertices.push_back(MeshVertex(corners[0], uvs[0], normal, color));
vertices.push_back(MeshVertex(corners[3], uvs[3], normal, color));
vertices.push_back(MeshVertex(corners[2], uvs[2], normal, color));
}
void VoxelMesher::generateGreedyMesh(const VoxelGrid& grid, std::vector<MeshVertex>& outVertices) {
const float cs = grid.cellSize();
const int width = grid.width();
const int height = grid.height();
const int depth = grid.depth();
// Process each face direction
// Axis 0 = X, 1 = Y, 2 = Z
// Direction: +1 = positive, -1 = negative
for (int axis = 0; axis < 3; axis++) {
for (int dir = -1; dir <= 1; dir += 2) {
// Determine slice dimensions based on axis
int sliceW, sliceH, sliceCount;
if (axis == 0) { // X-axis: slices in YZ plane
sliceW = depth;
sliceH = height;
sliceCount = width;
} else if (axis == 1) { // Y-axis: slices in XZ plane
sliceW = width;
sliceH = depth;
sliceCount = height;
} else { // Z-axis: slices in XY plane
sliceW = width;
sliceH = height;
sliceCount = depth;
}
// Create mask for this slice
std::vector<uint8_t> mask(sliceW * sliceH);
// Process each slice
for (int sliceIdx = 0; sliceIdx < sliceCount; sliceIdx++) {
// Fill mask with material IDs where faces should be generated
std::fill(mask.begin(), mask.end(), 0);
for (int v = 0; v < sliceH; v++) {
for (int u = 0; u < sliceW; u++) {
// Map (u, v, sliceIdx) to (x, y, z) based on axis
int x, y, z, nx, ny, nz;
if (axis == 0) {
x = sliceIdx; y = v; z = u;
nx = x + dir; ny = y; nz = z;
} else if (axis == 1) {
x = u; y = sliceIdx; z = v;
nx = x; ny = y + dir; nz = z;
} else {
x = u; y = v; z = sliceIdx;
nx = x; ny = y; nz = z + dir;
}
uint8_t mat = grid.get(x, y, z);
if (mat == 0) continue;
// Check if face should be generated
if (shouldGenerateFace(grid, x, y, z, nx, ny, nz)) {
mask[v * sliceW + u] = mat;
}
}
}
// Greedy rectangle merging
for (int v = 0; v < sliceH; v++) {
for (int u = 0; u < sliceW; ) {
uint8_t mat = mask[v * sliceW + u];
if (mat == 0) {
u++;
continue;
}
// Find width of rectangle (extend along U)
int rectW = 1;
while (u + rectW < sliceW && mask[v * sliceW + u + rectW] == mat) {
rectW++;
}
// Find height of rectangle (extend along V)
int rectH = 1;
bool canExtend = true;
while (canExtend && v + rectH < sliceH) {
// Check if entire row matches
for (int i = 0; i < rectW; i++) {
if (mask[(v + rectH) * sliceW + u + i] != mat) {
canExtend = false;
break;
}
}
if (canExtend) rectH++;
}
// Clear mask for merged area
for (int dv = 0; dv < rectH; dv++) {
for (int du = 0; du < rectW; du++) {
mask[(v + dv) * sliceW + u + du] = 0;
}
}
// Emit quad for this merged rectangle
const VoxelMaterial& material = grid.getMaterial(mat);
// Calculate corner and axes based on face direction
vec3 corner, uAxis, vAxis, normal;
if (axis == 0) { // X-facing
float faceX = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs;
corner = vec3(faceX, v * cs, u * cs);
uAxis = vec3(0, 0, rectW * cs);
vAxis = vec3(0, rectH * cs, 0);
normal = vec3(static_cast<float>(dir), 0, 0);
// Flip winding for back faces
if (dir < 0) std::swap(uAxis, vAxis);
} else if (axis == 1) { // Y-facing
float faceY = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs;
corner = vec3(u * cs, faceY, v * cs);
uAxis = vec3(rectW * cs, 0, 0);
vAxis = vec3(0, 0, rectH * cs);
normal = vec3(0, static_cast<float>(dir), 0);
if (dir < 0) std::swap(uAxis, vAxis);
} else { // Z-facing
float faceZ = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs;
corner = vec3(u * cs, v * cs, faceZ);
uAxis = vec3(rectW * cs, 0, 0);
vAxis = vec3(0, rectH * cs, 0);
normal = vec3(0, 0, static_cast<float>(dir));
if (dir < 0) std::swap(uAxis, vAxis);
}
emitQuad(outVertices, corner, uAxis, vAxis, normal, material);
u += rectW;
}
}
}
}
}
}
void VoxelMesher::emitFace(std::vector<MeshVertex>& vertices,
const vec3& center,
const vec3& normal,

View file

@ -1,5 +1,5 @@
// VoxelMesher.h - Face-culled mesh generation for VoxelGrid
// Part of McRogueFace 3D Extension - Milestone 10
// Part of McRogueFace 3D Extension - Milestones 10, 13
#pragma once
#include "VoxelGrid.h"
@ -14,7 +14,7 @@ namespace mcrf {
class VoxelMesher {
public:
/// Generate face-culled mesh from voxel data
/// Generate face-culled mesh from voxel data (simple per-voxel faces)
/// Output vertices in local space (model matrix applies world transform)
/// @param grid The VoxelGrid to generate mesh from
/// @param outVertices Output vector of vertices (appended to, not cleared)
@ -23,6 +23,16 @@ public:
std::vector<MeshVertex>& outVertices
);
/// Generate mesh using greedy meshing algorithm (Milestone 13)
/// Merges coplanar faces of the same material into larger rectangles,
/// significantly reducing vertex count for uniform regions.
/// @param grid The VoxelGrid to generate mesh from
/// @param outVertices Output vector of vertices (appended to, not cleared)
static void generateGreedyMesh(
const VoxelGrid& grid,
std::vector<MeshVertex>& outVertices
);
private:
/// Check if face should be generated (neighbor is air or transparent)
/// @param grid The VoxelGrid
@ -48,6 +58,23 @@ private:
float size,
const VoxelMaterial& material
);
/// Generate a rectangular face (2 triangles = 6 vertices)
/// Used by greedy meshing to emit merged quads
/// @param vertices Output vector to append vertices to
/// @param corner Base corner of the rectangle
/// @param uAxis Direction and length along U axis
/// @param vAxis Direction and length along V axis
/// @param normal Face normal direction
/// @param material Material for coloring
static void emitQuad(
std::vector<MeshVertex>& vertices,
const vec3& corner,
const vec3& uAxis,
const vec3& vAxis,
const vec3& normal,
const VoxelMaterial& material
);
};
} // namespace mcrf

View file

@ -444,6 +444,7 @@ PyObject* PyInit_mcrfpy()
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
&mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType,
&mcrfpydef::PyBillboardType, &mcrfpydef::PyVoxelGridType,
&mcrfpydef::PyVoxelRegionType,
/*grid layers (#147)*/
&PyColorLayerType, &PyTileLayerType,
@ -544,6 +545,9 @@ PyObject* PyInit_mcrfpy()
mcrfpydef::PyVoxelGridType.tp_methods = PyVoxelGrid::methods;
mcrfpydef::PyVoxelGridType.tp_getset = PyVoxelGrid::getsetters;
// Set up PyVoxelRegionType getsetters (Milestone 11)
mcrfpydef::PyVoxelRegionType.tp_getset = PyVoxelRegion::getsetters;
// Set up PyShaderType methods and getsetters (#106)
mcrfpydef::PyShaderType.tp_methods = PyShader::methods;
mcrfpydef::PyShaderType.tp_getset = PyShader::getsetters;

View file

@ -68,8 +68,68 @@ PyObject* PyColor::pyObject()
sf::Color PyColor::fromPy(PyObject* obj)
{
PyColorObject* self = (PyColorObject*)obj;
return self->data;
// Handle None or NULL
if (!obj || obj == Py_None) {
return sf::Color::White;
}
// Check if it's already a Color object
PyTypeObject* color_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
if (color_type) {
bool is_color = PyObject_TypeCheck(obj, color_type);
Py_DECREF(color_type);
if (is_color) {
PyColorObject* self = (PyColorObject*)obj;
return self->data;
}
}
// Handle tuple or list input
if (PyTuple_Check(obj) || PyList_Check(obj)) {
Py_ssize_t size = PySequence_Size(obj);
if (size < 3 || size > 4) {
PyErr_SetString(PyExc_TypeError, "Color tuple/list must have 3 or 4 elements (r, g, b[, a])");
return sf::Color::White;
}
int r = 255, g = 255, b = 255, a = 255;
PyObject* item0 = PySequence_GetItem(obj, 0);
PyObject* item1 = PySequence_GetItem(obj, 1);
PyObject* item2 = PySequence_GetItem(obj, 2);
if (PyLong_Check(item0)) r = (int)PyLong_AsLong(item0);
if (PyLong_Check(item1)) g = (int)PyLong_AsLong(item1);
if (PyLong_Check(item2)) b = (int)PyLong_AsLong(item2);
Py_DECREF(item0);
Py_DECREF(item1);
Py_DECREF(item2);
if (size == 4) {
PyObject* item3 = PySequence_GetItem(obj, 3);
if (PyLong_Check(item3)) a = (int)PyLong_AsLong(item3);
Py_DECREF(item3);
}
// Clamp values
r = std::max(0, std::min(255, r));
g = std::max(0, std::min(255, g));
b = std::max(0, std::min(255, b));
a = std::max(0, std::min(255, a));
return sf::Color(r, g, b, a);
}
// Handle integer (grayscale)
if (PyLong_Check(obj)) {
int v = std::max(0, std::min(255, (int)PyLong_AsLong(obj)));
return sf::Color(v, v, v, 255);
}
// Unknown type - set error and return white
PyErr_SetString(PyExc_TypeError, "Color must be a Color object, tuple, list, or integer");
return sf::Color::White;
}
sf::Color PyColor::fromPy(PyColorObject* self)