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();
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,9 +68,69 @@ PyObject* PyColor::pyObject()
sf::Color PyColor::fromPy(PyObject* obj)
{
// 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)
{

View file

@ -0,0 +1,263 @@
"""VoxelGrid Core Demo (Milestone 9)
Demonstrates the VoxelGrid data structure without rendering.
This is a "console demo" that creates VoxelGrids, defines materials,
places voxel patterns, and displays statistics.
Note: Visual rendering comes in Milestone 10 (VoxelMeshing).
"""
import mcrfpy
from mcrfpy import Color
def format_bytes(bytes_val):
"""Format bytes as human-readable string"""
if bytes_val < 1024:
return f"{bytes_val} B"
elif bytes_val < 1024 * 1024:
return f"{bytes_val / 1024:.1f} KB"
else:
return f"{bytes_val / (1024 * 1024):.1f} MB"
def print_header(title):
"""Print a formatted header"""
print("\n" + "=" * 60)
print(f" {title}")
print("=" * 60)
def print_grid_stats(vg, name="VoxelGrid"):
"""Print statistics for a VoxelGrid"""
print(f"\n {name}:")
print(f" Dimensions: {vg.width} x {vg.height} x {vg.depth}")
print(f" Total voxels: {vg.width * vg.height * vg.depth:,}")
print(f" Cell size: {vg.cell_size} units")
print(f" Materials: {vg.material_count}")
print(f" Non-air voxels: {vg.count_non_air():,}")
print(f" Memory estimate: {format_bytes(vg.width * vg.height * vg.depth)}")
print(f" Offset: {vg.offset}")
print(f" Rotation: {vg.rotation} deg")
def demo_basic_creation():
"""Demonstrate basic VoxelGrid creation"""
print_header("1. Basic VoxelGrid Creation")
# Create various sizes
small = mcrfpy.VoxelGrid(size=(8, 4, 8))
medium = mcrfpy.VoxelGrid(size=(16, 8, 16), cell_size=1.0)
large = mcrfpy.VoxelGrid(size=(32, 16, 32), cell_size=0.5)
print_grid_stats(small, "Small (8x4x8)")
print_grid_stats(medium, "Medium (16x8x16)")
print_grid_stats(large, "Large (32x16x32, 0.5 cell size)")
def demo_material_palette():
"""Demonstrate material palette system"""
print_header("2. Material Palette System")
vg = mcrfpy.VoxelGrid(size=(16, 8, 16))
# Define a palette of building materials
materials = {}
materials['stone'] = vg.add_material("stone", color=Color(128, 128, 128))
materials['brick'] = vg.add_material("brick", color=Color(165, 42, 42))
materials['wood'] = vg.add_material("wood", color=Color(139, 90, 43))
materials['glass'] = vg.add_material("glass",
color=Color(200, 220, 255, 128),
transparent=True,
path_cost=1.0)
materials['metal'] = vg.add_material("metal",
color=Color(180, 180, 190),
path_cost=0.8)
materials['grass'] = vg.add_material("grass", color=Color(60, 150, 60))
print(f"\n Defined {vg.material_count} materials:")
print(f" ID 0: air (implicit, always transparent)")
for name, mat_id in materials.items():
mat = vg.get_material(mat_id)
c = mat['color']
props = []
if mat['transparent']:
props.append("transparent")
if mat['path_cost'] != 1.0:
props.append(f"cost={mat['path_cost']}")
props_str = f" ({', '.join(props)})" if props else ""
print(f" ID {mat_id}: {name} RGB({c.r},{c.g},{c.b},{c.a}){props_str}")
return vg, materials
def demo_voxel_placement():
"""Demonstrate voxel placement patterns"""
print_header("3. Voxel Placement Patterns")
vg, materials = demo_material_palette()
stone = materials['stone']
brick = materials['brick']
wood = materials['wood']
# Pattern 1: Solid cube
print("\n Pattern: Solid 4x4x4 cube at origin")
for z in range(4):
for y in range(4):
for x in range(4):
vg.set(x, y, z, stone)
print(f" Placed {vg.count_material(stone)} stone voxels")
# Pattern 2: Checkerboard floor
print("\n Pattern: Checkerboard floor at y=0, x=6-14, z=0-8")
for z in range(8):
for x in range(6, 14):
mat = stone if (x + z) % 2 == 0 else brick
vg.set(x, 0, z, mat)
print(f" Stone: {vg.count_material(stone)}, Brick: {vg.count_material(brick)}")
# Pattern 3: Hollow cube (walls only)
print("\n Pattern: Hollow cube frame 4x4x4 at x=10, z=10")
for x in range(4):
for y in range(4):
for z in range(4):
# Only place on edges
on_edge_x = (x == 0 or x == 3)
on_edge_y = (y == 0 or y == 3)
on_edge_z = (z == 0 or z == 3)
if sum([on_edge_x, on_edge_y, on_edge_z]) >= 2:
vg.set(10 + x, y, 10 + z, wood)
print(f" Wood voxels: {vg.count_material(wood)}")
print_grid_stats(vg, "After patterns")
# Material breakdown
print("\n Material breakdown:")
print(f" Air: {vg.count_material(0):,} ({100 * vg.count_material(0) / (16*8*16):.1f}%)")
print(f" Stone: {vg.count_material(stone):,}")
print(f" Brick: {vg.count_material(brick):,}")
print(f" Wood: {vg.count_material(wood):,}")
def demo_bulk_operations():
"""Demonstrate bulk fill and clear operations"""
print_header("4. Bulk Operations")
vg = mcrfpy.VoxelGrid(size=(32, 8, 32))
total = 32 * 8 * 32
stone = vg.add_material("stone", color=Color(128, 128, 128))
print(f"\n Grid: 32x8x32 = {total:,} voxels")
# Fill
vg.fill(stone)
print(f" After fill(stone): {vg.count_non_air():,} non-air")
# Clear
vg.clear()
print(f" After clear(): {vg.count_non_air():,} non-air")
def demo_transforms():
"""Demonstrate transform properties"""
print_header("5. Transform Properties")
vg = mcrfpy.VoxelGrid(size=(8, 8, 8))
print(f"\n Default state:")
print(f" Offset: {vg.offset}")
print(f" Rotation: {vg.rotation} deg")
# Position for a building
vg.offset = (100.0, 0.0, 50.0)
vg.rotation = 45.0
print(f"\n After positioning:")
print(f" Offset: {vg.offset}")
print(f" Rotation: {vg.rotation} deg")
# Multiple buildings with different transforms
print("\n Example: Village layout with 3 buildings")
buildings = []
positions = [(0, 0, 0), (20, 0, 0), (10, 0, 15)]
rotations = [0, 90, 45]
for i, (pos, rot) in enumerate(zip(positions, rotations)):
b = mcrfpy.VoxelGrid(size=(8, 6, 8))
b.offset = pos
b.rotation = rot
buildings.append(b)
print(f" Building {i+1}: offset={pos}, rotation={rot} deg")
def demo_edge_cases():
"""Test edge cases and limits"""
print_header("6. Edge Cases and Limits")
# Maximum practical size
print("\n Testing large grid (64x64x64)...")
large = mcrfpy.VoxelGrid(size=(64, 64, 64))
mat = large.add_material("test", color=Color(128, 128, 128))
large.fill(mat)
print(f" Created and filled: {large.count_non_air():,} voxels")
large.clear()
print(f" Cleared: {large.count_non_air()} voxels")
# Bounds checking
print("\n Bounds checking (should not crash):")
small = mcrfpy.VoxelGrid(size=(4, 4, 4))
test_mat = small.add_material("test", color=Color(255, 0, 0))
small.set(-1, 0, 0, test_mat)
small.set(100, 0, 0, test_mat)
print(f" Out-of-bounds get(-1,0,0): {small.get(-1, 0, 0)} (expected 0)")
print(f" Out-of-bounds get(100,0,0): {small.get(100, 0, 0)} (expected 0)")
# Material palette capacity
print("\n Material palette capacity test:")
full_vg = mcrfpy.VoxelGrid(size=(4, 4, 4))
for i in range(255):
full_vg.add_material(f"mat_{i}", color=Color(i, i, i))
print(f" Added 255 materials: count = {full_vg.material_count}")
try:
full_vg.add_material("overflow", color=Color(255, 255, 255))
print(" ERROR: Should have raised exception!")
except RuntimeError as e:
print(f" 256th material correctly rejected: {e}")
def demo_memory_usage():
"""Show memory usage for various grid sizes"""
print_header("7. Memory Usage Estimates")
sizes = [
(8, 8, 8),
(16, 8, 16),
(32, 16, 32),
(64, 32, 64),
(80, 16, 45), # Example dungeon size
]
print("\n Size Voxels Memory")
print(" " + "-" * 40)
for w, h, d in sizes:
voxels = w * h * d
memory = voxels # 1 byte per voxel
print(f" {w:3}x{h:3}x{d:3} {voxels:>10,} {format_bytes(memory):>10}")
def main():
"""Run all demos"""
print("\n" + "=" * 60)
print(" VOXELGRID CORE DEMO (Milestone 9)")
print(" Dense 3D Voxel Array with Material Palette")
print("=" * 60)
demo_basic_creation()
demo_material_palette()
demo_voxel_placement()
demo_bulk_operations()
demo_transforms()
demo_edge_cases()
demo_memory_usage()
print_header("Demo Complete!")
print("\n Next milestone (10): Voxel Mesh Generation")
print(" The VoxelGrid data will be converted to renderable 3D meshes.")
print()
if __name__ == "__main__":
import sys
main()
sys.exit(0)

View file

@ -0,0 +1,273 @@
# voxel_dungeon_demo.py - Procedural dungeon demonstrating bulk voxel operations
# Milestone 11: Bulk Operations and Building Primitives
import mcrfpy
import sys
import math
import random
# Create demo scene
scene = mcrfpy.Scene("voxel_dungeon_demo")
# Dark background
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(20, 20, 30))
scene.children.append(bg)
# Title
title = mcrfpy.Caption(text="Voxel Dungeon Demo - Bulk Operations (Milestone 11)", pos=(20, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
scene.children.append(title)
# Create the 3D viewport
viewport = mcrfpy.Viewport3D(
pos=(50, 60),
size=(620, 520),
render_resolution=(400, 320),
fov=60.0,
camera_pos=(40.0, 30.0, 40.0),
camera_target=(16.0, 4.0, 16.0),
bg_color=mcrfpy.Color(30, 30, 40) # Dark atmosphere
)
scene.children.append(viewport)
# Global voxel grid reference
voxels = None
seed = 42
def generate_dungeon(dungeon_seed=42):
"""Generate a procedural dungeon showcasing all bulk operations"""
global voxels, seed
seed = dungeon_seed
random.seed(seed)
# Create voxel grid for dungeon
print(f"Generating dungeon (seed={seed})...")
voxels = mcrfpy.VoxelGrid(size=(32, 12, 32), cell_size=1.0)
# Define materials
STONE_WALL = voxels.add_material("stone_wall", color=mcrfpy.Color(80, 80, 90))
STONE_FLOOR = voxels.add_material("stone_floor", color=mcrfpy.Color(100, 95, 90))
MOSS = voxels.add_material("moss", color=mcrfpy.Color(40, 80, 40))
WATER = voxels.add_material("water", color=mcrfpy.Color(40, 80, 160, 180), transparent=True)
PILLAR = voxels.add_material("pillar", color=mcrfpy.Color(120, 110, 100))
GOLD = voxels.add_material("gold", color=mcrfpy.Color(255, 215, 0))
print(f"Defined {voxels.material_count} materials")
# 1. Main room using fill_box_hollow
print("Building main room with fill_box_hollow...")
voxels.fill_box_hollow((2, 0, 2), (29, 10, 29), STONE_WALL, thickness=1)
# 2. Floor with slight variation using fill_box
voxels.fill_box((3, 0, 3), (28, 0, 28), STONE_FLOOR)
# 3. Spherical alcoves carved into walls using fill_sphere
print("Carving alcoves with fill_sphere...")
alcove_positions = [
(2, 5, 16), # West wall
(29, 5, 16), # East wall
(16, 5, 2), # North wall
(16, 5, 29), # South wall
]
for pos in alcove_positions:
voxels.fill_sphere(pos, 3, 0) # Carve out (air)
# 4. Small decorative spheres (gold orbs in alcoves)
print("Adding gold orbs in alcoves...")
for i, pos in enumerate(alcove_positions):
# Offset inward so orb is visible
ox, oy, oz = pos
if ox < 10:
ox += 2
elif ox > 20:
ox -= 2
if oz < 10:
oz += 2
elif oz > 20:
oz -= 2
voxels.fill_sphere((ox, oy - 1, oz), 1, GOLD)
# 5. Support pillars using fill_cylinder
print("Building pillars with fill_cylinder...")
pillar_positions = [
(8, 1, 8), (8, 1, 24),
(24, 1, 8), (24, 1, 24),
(16, 1, 8), (16, 1, 24),
(8, 1, 16), (24, 1, 16),
]
for px, py, pz in pillar_positions:
voxels.fill_cylinder((px, py, pz), 1, 9, PILLAR)
# 6. Moss patches using fill_noise
print("Adding moss patches with fill_noise...")
voxels.fill_noise((3, 1, 3), (28, 1, 28), MOSS, threshold=0.65, scale=0.15, seed=seed)
# 7. Central water pool
print("Creating water pool...")
voxels.fill_box((12, 0, 12), (20, 0, 20), 0) # Carve depression
voxels.fill_box((12, 0, 12), (20, 0, 20), WATER)
# 8. Copy a pillar as prefab and paste variations
print("Creating prefab from pillar and pasting copies...")
pillar_prefab = voxels.copy_region((8, 1, 8), (9, 9, 9))
print(f" Pillar prefab: {pillar_prefab.size}")
# Paste smaller pillars at corners (offset from main room)
corner_positions = [(4, 1, 4), (4, 1, 27), (27, 1, 4), (27, 1, 27)]
for cx, cy, cz in corner_positions:
voxels.paste_region(pillar_prefab, (cx, cy, cz), skip_air=True)
# Build mesh
voxels.rebuild_mesh()
print(f"\nDungeon generated:")
print(f" Non-air voxels: {voxels.count_non_air()}")
print(f" Vertices: {voxels.vertex_count}")
print(f" Faces: {voxels.vertex_count // 6}")
# Add to viewport
# First remove old layer if exists
if viewport.voxel_layer_count() > 0:
pass # Can't easily remove, so we regenerate the whole viewport
viewport.add_voxel_layer(voxels, z_index=0)
return voxels
# Generate initial dungeon
generate_dungeon(42)
# Create info panel
info_frame = mcrfpy.Frame(pos=(690, 60), size=(300, 280), fill_color=mcrfpy.Color(40, 40, 60, 220))
scene.children.append(info_frame)
info_title = mcrfpy.Caption(text="Dungeon Stats", pos=(700, 70))
info_title.fill_color = mcrfpy.Color(255, 255, 100)
scene.children.append(info_title)
def update_stats():
global stats_caption
stats_text = f"""Grid: {voxels.width}x{voxels.height}x{voxels.depth}
Total cells: {voxels.width * voxels.height * voxels.depth}
Non-air: {voxels.count_non_air()}
Materials: {voxels.material_count}
Mesh Stats:
Vertices: {voxels.vertex_count}
Faces: {voxels.vertex_count // 6}
Seed: {seed}
Operations Used:
- fill_box_hollow (walls)
- fill_sphere (alcoves)
- fill_cylinder (pillars)
- fill_noise (moss)
- copy/paste (prefabs)"""
stats_caption.text = stats_text
stats_caption = mcrfpy.Caption(text="", pos=(700, 100))
stats_caption.fill_color = mcrfpy.Color(200, 200, 200)
scene.children.append(stats_caption)
update_stats()
# Controls panel
controls_frame = mcrfpy.Frame(pos=(690, 360), size=(300, 180), fill_color=mcrfpy.Color(40, 40, 60, 220))
scene.children.append(controls_frame)
controls_title = mcrfpy.Caption(text="Controls", pos=(700, 370))
controls_title.fill_color = mcrfpy.Color(255, 255, 100)
scene.children.append(controls_title)
controls_text = """R - Regenerate dungeon (new seed)
1-4 - Camera presets
+/- - Zoom in/out
SPACE - Reset camera
ESC - Exit demo"""
controls = mcrfpy.Caption(text=controls_text, pos=(700, 400))
controls.fill_color = mcrfpy.Color(200, 200, 200)
scene.children.append(controls)
# Camera animation state
rotation_enabled = False
camera_distance = 50.0
camera_angle = 45.0 # degrees
camera_height = 30.0
camera_presets = [
(40.0, 30.0, 40.0, 16.0, 4.0, 16.0), # Default diagonal
(16.0, 30.0, 50.0, 16.0, 4.0, 16.0), # Front view
(50.0, 30.0, 16.0, 16.0, 4.0, 16.0), # Side view
(16.0, 50.0, 16.0, 16.0, 4.0, 16.0), # Top-down
]
def rotate_camera(timer_name, runtime):
"""Timer callback for camera rotation"""
global camera_angle, rotation_enabled
if rotation_enabled:
camera_angle += 0.5
if camera_angle >= 360.0:
camera_angle = 0.0
rad = camera_angle * math.pi / 180.0
x = 16.0 + camera_distance * math.cos(rad)
z = 16.0 + camera_distance * math.sin(rad)
viewport.camera_pos = (x, camera_height, z)
# Set up rotation timer
timer = mcrfpy.Timer("rotate_cam", rotate_camera, 33)
def handle_key(key, action):
"""Keyboard handler"""
global rotation_enabled, seed, camera_distance, camera_height
if action != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.R:
seed = random.randint(1, 99999)
generate_dungeon(seed)
update_stats()
print(f"Regenerated dungeon with seed {seed}")
elif key == mcrfpy.Key.NUM_1:
viewport.camera_pos = camera_presets[0][:3]
viewport.camera_target = camera_presets[0][3:]
rotation_enabled = False
elif key == mcrfpy.Key.NUM_2:
viewport.camera_pos = camera_presets[1][:3]
viewport.camera_target = camera_presets[1][3:]
rotation_enabled = False
elif key == mcrfpy.Key.NUM_3:
viewport.camera_pos = camera_presets[2][:3]
viewport.camera_target = camera_presets[2][3:]
rotation_enabled = False
elif key == mcrfpy.Key.NUM_4:
viewport.camera_pos = camera_presets[3][:3]
viewport.camera_target = camera_presets[3][3:]
rotation_enabled = False
elif key == mcrfpy.Key.SPACE:
rotation_enabled = not rotation_enabled
print(f"Camera rotation: {'ON' if rotation_enabled else 'OFF'}")
elif key == mcrfpy.Key.EQUALS or key == mcrfpy.Key.ADD:
camera_distance = max(20.0, camera_distance - 5.0)
camera_height = max(15.0, camera_height - 2.0)
elif key == mcrfpy.Key.DASH or key == mcrfpy.Key.SUBTRACT:
camera_distance = min(80.0, camera_distance + 5.0)
camera_height = min(50.0, camera_height + 2.0)
elif key == mcrfpy.Key.ESCAPE:
print("Exiting demo...")
sys.exit(0)
scene.on_key = handle_key
# Activate the scene
mcrfpy.current_scene = scene
print("\nVoxel Dungeon Demo ready!")
print("Press SPACE to toggle camera rotation, R to regenerate")
# Main entry point for --exec mode
if __name__ == "__main__":
print("\n=== Voxel Dungeon Demo Summary ===")
print(f"Grid size: {voxels.width}x{voxels.height}x{voxels.depth}")
print(f"Non-air voxels: {voxels.count_non_air()}")
print(f"Generated vertices: {voxels.vertex_count}")
print(f"Rendered faces: {voxels.vertex_count // 6}")
print("===================================\n")

View file

@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""Visual Demo: Milestone 12 - VoxelGrid Navigation Projection
Demonstrates projection of 3D voxel terrain to 2D navigation grid for pathfinding.
Shows:
1. Voxel dungeon with multiple levels
2. Navigation grid projection (walkable/unwalkable areas)
3. A* pathfinding through the projected terrain
4. FOV computation from voxel transparency
"""
import mcrfpy
import sys
import math
def create_demo_scene():
"""Create the navigation projection demo scene"""
scene = mcrfpy.Scene("voxel_nav_demo")
# =========================================================================
# Create a small dungeon-style voxel grid
# =========================================================================
vg = mcrfpy.VoxelGrid((16, 8, 16), cell_size=1.0)
# Add materials
floor_mat = vg.add_material("floor", (100, 80, 60)) # Brown floor
wall_mat = vg.add_material("wall", (80, 80, 90), transparent=False) # Gray walls
pillar_mat = vg.add_material("pillar", (60, 60, 70), transparent=False) # Dark pillars
glass_mat = vg.add_material("glass", (150, 200, 255), transparent=True) # Transparent glass
water_mat = vg.add_material("water", (50, 100, 200), transparent=True, path_cost=3.0) # Slow water
# Create floor
vg.fill_box((0, 0, 0), (15, 0, 15), floor_mat)
# Create outer walls
vg.fill_box((0, 1, 0), (15, 4, 0), wall_mat) # North wall
vg.fill_box((0, 1, 15), (15, 4, 15), wall_mat) # South wall
vg.fill_box((0, 1, 0), (0, 4, 15), wall_mat) # West wall
vg.fill_box((15, 1, 0), (15, 4, 15), wall_mat) # East wall
# Interior walls creating rooms
vg.fill_box((5, 1, 0), (5, 4, 10), wall_mat) # Vertical wall
vg.fill_box((10, 1, 5), (15, 4, 5), wall_mat) # Horizontal wall
# Doorways (carve holes)
vg.fill_box((5, 1, 3), (5, 2, 4), 0) # Door in vertical wall
vg.fill_box((12, 1, 5), (13, 2, 5), 0) # Door in horizontal wall
# Central pillars
vg.fill_box((8, 1, 8), (8, 4, 8), pillar_mat)
vg.fill_box((8, 1, 12), (8, 4, 12), pillar_mat)
# Water pool in one corner (slow movement)
vg.fill_box((1, 0, 11), (3, 0, 14), water_mat)
# Glass window
vg.fill_box((10, 2, 5), (11, 3, 5), glass_mat)
# Raised platform in one area (height variation)
vg.fill_box((12, 1, 8), (14, 1, 13), floor_mat) # Platform at y=1
# =========================================================================
# Create Viewport3D with navigation grid
# =========================================================================
viewport = mcrfpy.Viewport3D(pos=(10, 10), size=(600, 400))
viewport.set_grid_size(16, 16)
viewport.cell_size = 1.0
# Configure camera for top-down view
viewport.camera_pos = (8, 15, 20)
viewport.camera_target = (8, 0, 8)
# Add voxel layer
viewport.add_voxel_layer(vg, z_index=0)
# Project voxels to navigation grid with headroom=2 (entity needs 2 voxels height)
viewport.project_voxel_to_nav(vg, headroom=2)
# =========================================================================
# Info panel
# =========================================================================
info_frame = mcrfpy.Frame(pos=(620, 10), size=(250, 400))
info_frame.fill_color = mcrfpy.Color(30, 30, 40, 220)
info_frame.outline_color = mcrfpy.Color(100, 100, 120)
info_frame.outline = 2.0
title = mcrfpy.Caption(text="Nav Projection Demo", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 100)
desc = mcrfpy.Caption(text="Voxels projected to\n2D nav grid", pos=(10, 35))
desc.fill_color = mcrfpy.Color(200, 200, 200)
info1 = mcrfpy.Caption(text="Grid: 16x16 cells", pos=(10, 75))
info1.fill_color = mcrfpy.Color(150, 200, 255)
info2 = mcrfpy.Caption(text="Headroom: 2 voxels", pos=(10, 95))
info2.fill_color = mcrfpy.Color(150, 200, 255)
# Count walkable cells
walkable_count = 0
for x in range(16):
for z in range(16):
cell = viewport.at(x, z)
if cell.walkable:
walkable_count += 1
info3 = mcrfpy.Caption(text=f"Walkable: {walkable_count}/256", pos=(10, 115))
info3.fill_color = mcrfpy.Color(100, 255, 100)
# Find path example
path = viewport.find_path((1, 1), (13, 13))
info4 = mcrfpy.Caption(text=f"Path length: {len(path)}", pos=(10, 135))
info4.fill_color = mcrfpy.Color(255, 200, 100)
# FOV example
fov = viewport.compute_fov((8, 8), 10)
info5 = mcrfpy.Caption(text=f"FOV cells: {len(fov)}", pos=(10, 155))
info5.fill_color = mcrfpy.Color(200, 150, 255)
# Legend
legend_title = mcrfpy.Caption(text="Materials:", pos=(10, 185))
legend_title.fill_color = mcrfpy.Color(255, 255, 255)
leg1 = mcrfpy.Caption(text=" Floor (walkable)", pos=(10, 205))
leg1.fill_color = mcrfpy.Color(100, 80, 60)
leg2 = mcrfpy.Caption(text=" Wall (blocking)", pos=(10, 225))
leg2.fill_color = mcrfpy.Color(80, 80, 90)
leg3 = mcrfpy.Caption(text=" Water (slow)", pos=(10, 245))
leg3.fill_color = mcrfpy.Color(50, 100, 200)
leg4 = mcrfpy.Caption(text=" Glass (see-through)", pos=(10, 265))
leg4.fill_color = mcrfpy.Color(150, 200, 255)
controls = mcrfpy.Caption(text="[Space] Recompute FOV\n[P] Show path\n[Q] Quit", pos=(10, 300))
controls.fill_color = mcrfpy.Color(150, 150, 150)
info_frame.children.extend([
title, desc, info1, info2, info3, info4, info5,
legend_title, leg1, leg2, leg3, leg4, controls
])
# =========================================================================
# Status bar
# =========================================================================
status_frame = mcrfpy.Frame(pos=(10, 420), size=(860, 50))
status_frame.fill_color = mcrfpy.Color(20, 20, 30, 220)
status_frame.outline_color = mcrfpy.Color(80, 80, 100)
status_frame.outline = 1.0
status_text = mcrfpy.Caption(
text="Milestone 12: VoxelGrid Navigation Projection - Project 3D voxels to 2D pathfinding grid",
pos=(10, 15)
)
status_text.fill_color = mcrfpy.Color(180, 180, 200)
status_frame.children.append(status_text)
# =========================================================================
# Add elements to scene
# =========================================================================
scene.children.extend([viewport, info_frame, status_frame])
# Store references for interaction (using module-level globals)
global demo_viewport, demo_voxelgrid, demo_path, demo_fov_origin
demo_viewport = viewport
demo_voxelgrid = vg
demo_path = path
demo_fov_origin = (8, 8)
# =========================================================================
# Keyboard handler
# =========================================================================
def on_key(key, state):
global demo_fov_origin
if state != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.Q or key == mcrfpy.Key.ESCAPE:
# Exit
sys.exit(0)
elif key == mcrfpy.Key.SPACE:
# Recompute FOV from different origin
ox, oz = demo_fov_origin
ox = (ox + 3) % 14 + 1
oz = (oz + 5) % 14 + 1
demo_fov_origin = (ox, oz)
fov = demo_viewport.compute_fov((ox, oz), 8)
info5.text = f"FOV from ({ox},{oz}): {len(fov)}"
elif key == mcrfpy.Key.P:
# Show path info
print(f"Path from (1,1) to (13,13): {len(demo_path)} steps")
for i, (px, pz) in enumerate(demo_path[:10]):
cell = demo_viewport.at(px, pz)
print(f" Step {i}: ({px},{pz}) h={cell.height:.1f} cost={cell.cost:.1f}")
if len(demo_path) > 10:
print(f" ... and {len(demo_path) - 10} more steps")
scene.on_key = on_key
return scene
def main():
"""Main entry point"""
print("=== Milestone 12: VoxelGrid Navigation Projection Demo ===")
print()
print("This demo shows how 3D voxel terrain is projected to a 2D")
print("navigation grid for pathfinding and FOV calculations.")
print()
print("The projection scans each column from top to bottom, finding")
print("the topmost walkable floor with adequate headroom.")
print()
scene = create_demo_scene()
mcrfpy.current_scene = scene
# Print nav grid summary
grid_w, grid_d = demo_viewport.grid_size
print("Navigation grid summary:")
print(f" Grid size: {grid_w}x{grid_d}")
# Count by walkability and transparency
walkable = 0
blocking = 0
transparent = 0
for x in range(grid_w):
for z in range(grid_d):
cell = demo_viewport.at(x, z)
if cell.walkable:
walkable += 1
else:
blocking += 1
if cell.transparent:
transparent += 1
print(f" Walkable cells: {walkable}")
print(f" Blocking cells: {blocking}")
print(f" Transparent cells: {transparent}")
print()
if __name__ == "__main__":
main()
sys.exit(0)

View file

@ -0,0 +1,314 @@
"""Voxel Serialization Demo - Milestone 14
Demonstrates save/load functionality for VoxelGrid, including:
- Saving to file with .mcvg format
- Loading from file
- Serialization to bytes (for network/custom storage)
- RLE compression effectiveness
"""
import mcrfpy
import os
import tempfile
def create_demo_scene():
"""Create a scene demonstrating voxel serialization."""
scene = mcrfpy.Scene("voxel_serialization_demo")
ui = scene.children
# Dark background
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=(20, 20, 30))
ui.append(bg)
# Title
title = mcrfpy.Caption(text="Milestone 14: VoxelGrid Serialization",
pos=(30, 20))
title.font_size = 28
title.fill_color = (255, 220, 100)
ui.append(title)
# Create demo VoxelGrid with interesting structure
grid = mcrfpy.VoxelGrid((16, 16, 16), cell_size=1.0)
# Add materials
stone = grid.add_material("stone", (100, 100, 110))
wood = grid.add_material("wood", (139, 90, 43))
glass = grid.add_material("glass", (180, 200, 220, 100), transparent=True)
gold = grid.add_material("gold", (255, 215, 0))
# Build a small structure
grid.fill_box((0, 0, 0), (15, 0, 15), stone) # Floor
grid.fill_box((0, 1, 0), (0, 4, 15), stone) # Wall 1
grid.fill_box((15, 1, 0), (15, 4, 15), stone) # Wall 2
grid.fill_box((0, 1, 0), (15, 4, 0), stone) # Wall 3
grid.fill_box((0, 1, 15), (15, 4, 15), stone) # Wall 4
# Windows (clear some wall, add glass)
grid.fill_box((6, 2, 0), (10, 3, 0), 0) # Clear for window
grid.fill_box((6, 2, 0), (10, 3, 0), glass) # Add glass
# Pillars
grid.fill_box((4, 1, 4), (4, 3, 4), wood)
grid.fill_box((12, 1, 4), (12, 3, 4), wood)
grid.fill_box((4, 1, 12), (4, 3, 12), wood)
grid.fill_box((12, 1, 12), (12, 3, 12), wood)
# Gold decorations
grid.set(8, 1, 8, gold)
grid.set(7, 1, 8, gold)
grid.set(9, 1, 8, gold)
grid.set(8, 1, 7, gold)
grid.set(8, 1, 9, gold)
# Get original stats
original_voxels = grid.count_non_air()
original_materials = grid.material_count
# === Test save/load to file ===
with tempfile.NamedTemporaryFile(suffix='.mcvg', delete=False) as f:
temp_path = f.name
save_success = grid.save(temp_path)
file_size = os.path.getsize(temp_path) if save_success else 0
# Load into new grid
loaded_grid = mcrfpy.VoxelGrid((1, 1, 1))
load_success = loaded_grid.load(temp_path)
os.unlink(temp_path) # Clean up
loaded_voxels = loaded_grid.count_non_air() if load_success else 0
loaded_materials = loaded_grid.material_count if load_success else 0
# === Test to_bytes/from_bytes ===
data_bytes = grid.to_bytes()
bytes_size = len(data_bytes)
bytes_grid = mcrfpy.VoxelGrid((1, 1, 1))
bytes_success = bytes_grid.from_bytes(data_bytes)
bytes_voxels = bytes_grid.count_non_air() if bytes_success else 0
# === Calculate compression ===
raw_size = 16 * 16 * 16 # Uncompressed voxel data
compression_ratio = raw_size / bytes_size if bytes_size > 0 else 0
# Display information
y_pos = 80
# Original Grid Info
info1 = mcrfpy.Caption(text="Original VoxelGrid:",
pos=(30, y_pos))
info1.font_size = 20
info1.fill_color = (100, 200, 255)
ui.append(info1)
y_pos += 30
for line in [
f" Dimensions: 16x16x16 = 4096 voxels",
f" Non-air voxels: {original_voxels}",
f" Materials defined: {original_materials}",
f" Structure: Walled room with pillars, windows, gold decor"
]:
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = (200, 200, 210)
ui.append(cap)
y_pos += 22
y_pos += 20
# File Save/Load Results
info2 = mcrfpy.Caption(text="File Serialization (.mcvg):",
pos=(30, y_pos))
info2.font_size = 20
info2.fill_color = (100, 255, 150)
ui.append(info2)
y_pos += 30
save_status = "SUCCESS" if save_success else "FAILED"
load_status = "SUCCESS" if load_success else "FAILED"
match_status = "MATCH" if loaded_voxels == original_voxels else "MISMATCH"
for line in [
f" Save to file: {save_status}",
f" File size: {file_size} bytes",
f" Load from file: {load_status}",
f" Loaded voxels: {loaded_voxels} ({match_status})",
f" Loaded materials: {loaded_materials}"
]:
color = (150, 255, 150) if "SUCCESS" in line or "MATCH" in line else (200, 200, 210)
if "FAILED" in line or "MISMATCH" in line:
color = (255, 100, 100)
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = color
ui.append(cap)
y_pos += 22
y_pos += 20
# Bytes Serialization Results
info3 = mcrfpy.Caption(text="Memory Serialization (to_bytes/from_bytes):",
pos=(30, y_pos))
info3.font_size = 20
info3.fill_color = (255, 200, 100)
ui.append(info3)
y_pos += 30
bytes_status = "SUCCESS" if bytes_success else "FAILED"
bytes_match = "MATCH" if bytes_voxels == original_voxels else "MISMATCH"
for line in [
f" Serialized size: {bytes_size} bytes",
f" Raw voxel data: {raw_size} bytes",
f" Compression ratio: {compression_ratio:.1f}x",
f" from_bytes(): {bytes_status}",
f" Restored voxels: {bytes_voxels} ({bytes_match})"
]:
color = (200, 200, 210)
if "SUCCESS" in line or "MATCH" in line:
color = (150, 255, 150)
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = color
ui.append(cap)
y_pos += 22
y_pos += 20
# RLE Compression Demo
info4 = mcrfpy.Caption(text="RLE Compression Effectiveness:",
pos=(30, y_pos))
info4.font_size = 20
info4.fill_color = (200, 150, 255)
ui.append(info4)
y_pos += 30
# Create uniform grid for compression test
uniform_grid = mcrfpy.VoxelGrid((32, 32, 32))
uniform_mat = uniform_grid.add_material("solid", (128, 128, 128))
uniform_grid.fill(uniform_mat)
uniform_bytes = uniform_grid.to_bytes()
uniform_raw = 32 * 32 * 32
uniform_ratio = uniform_raw / len(uniform_bytes)
for line in [
f" Uniform 32x32x32 filled grid:",
f" Raw: {uniform_raw} bytes",
f" Compressed: {len(uniform_bytes)} bytes",
f" Compression: {uniform_ratio:.0f}x",
f" ",
f" RLE excels at runs of identical values."
]:
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = (200, 180, 220)
ui.append(cap)
y_pos += 22
y_pos += 30
# File Format Info
info5 = mcrfpy.Caption(text="File Format (.mcvg):",
pos=(30, y_pos))
info5.font_size = 20
info5.fill_color = (255, 150, 200)
ui.append(info5)
y_pos += 30
for line in [
" Header: Magic 'MCVG' + version + dimensions + cell_size",
" Materials: name, color (RGBA), sprite_index, transparent, path_cost",
" Voxel data: RLE-encoded material IDs",
" ",
" Note: Transform (offset, rotation) is runtime state, not serialized"
]:
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 14
cap.fill_color = (200, 180, 200)
ui.append(cap)
y_pos += 20
# API Reference on right side
y_ref = 80
x_ref = 550
api_title = mcrfpy.Caption(text="Python API:", pos=(x_ref, y_ref))
api_title.font_size = 20
api_title.fill_color = (150, 200, 255)
ui.append(api_title)
y_ref += 35
for line in [
"# Save to file",
"success = grid.save('world.mcvg')",
"",
"# Load from file",
"grid = VoxelGrid((1,1,1))",
"success = grid.load('world.mcvg')",
"",
"# Save to bytes",
"data = grid.to_bytes()",
"",
"# Load from bytes",
"success = grid.from_bytes(data)",
"",
"# Network example:",
"# send_to_server(grid.to_bytes())",
"# data = recv_from_server()",
"# grid.from_bytes(data)"
]:
cap = mcrfpy.Caption(text=line, pos=(x_ref, y_ref))
cap.font_size = 14
if line.startswith("#"):
cap.fill_color = (100, 150, 100)
elif "=" in line or "(" in line:
cap.fill_color = (255, 220, 150)
else:
cap.fill_color = (180, 180, 180)
ui.append(cap)
y_ref += 18
return scene
# Run demonstration
if __name__ == "__main__":
import sys
# Create and activate the scene
scene = create_demo_scene()
mcrfpy.current_scene = scene
# When run directly, print summary and exit for headless testing
print("\n=== Voxel Serialization Demo (Milestone 14) ===\n")
# Run a quick verification
grid = mcrfpy.VoxelGrid((8, 8, 8))
mat = grid.add_material("test", (100, 100, 100))
grid.fill_box((0, 0, 0), (7, 0, 7), mat)
print(f"Created 8x8x8 grid with {grid.count_non_air()} non-air voxels")
# Test to_bytes
data = grid.to_bytes()
print(f"Serialized to {len(data)} bytes")
# Test from_bytes
grid2 = mcrfpy.VoxelGrid((1, 1, 1))
success = grid2.from_bytes(data)
print(f"from_bytes(): {'SUCCESS' if success else 'FAILED'}")
print(f"Restored size: {grid2.size}")
print(f"Restored voxels: {grid2.count_non_air()}")
# Compression test
big_grid = mcrfpy.VoxelGrid((32, 32, 32))
big_mat = big_grid.add_material("solid", (128, 128, 128))
big_grid.fill(big_mat)
big_data = big_grid.to_bytes()
raw_size = 32 * 32 * 32
print(f"\nCompression test (32x32x32 uniform):")
print(f" Raw: {raw_size} bytes")
print(f" Compressed: {len(big_data)} bytes")
print(f" Ratio: {raw_size / len(big_data):.0f}x")
print("\n=== Demo complete ===")
sys.exit(0)