Voxel functionality extension
This commit is contained in:
parent
3e6b6a5847
commit
992ea781cb
14 changed files with 3045 additions and 17 deletions
|
|
@ -234,6 +234,31 @@ int PyVoxelGrid::set_rotation(PyVoxelGridObject* self, PyObject* value, void* cl
|
||||||
return 0;
|
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
|
// 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)));
|
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", ¢er_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),
|
||||||
|
®ion_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
|
// Method definitions
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -556,6 +1132,54 @@ PyMethodDef PyVoxelGrid::methods[] = {
|
||||||
" max_coord: (x1, y1, z1) - maximum corner (inclusive)\n"
|
" max_coord: (x1, y1, z1) - maximum corner (inclusive)\n"
|
||||||
" material: material ID (0-255)\n\n"
|
" material: material ID (0-255)\n\n"
|
||||||
"Coordinates are clamped to grid bounds."},
|
"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", (PyCFunction)clear, METH_NOARGS,
|
||||||
"clear() -> None\n\n"
|
"clear() -> None\n\n"
|
||||||
"Clear the grid (fill with air, material 0)."},
|
"Clear the grid (fill with air, material 0)."},
|
||||||
|
|
@ -568,6 +1192,53 @@ PyMethodDef PyVoxelGrid::methods[] = {
|
||||||
{"count_material", (PyCFunction)count_material, METH_VARARGS,
|
{"count_material", (PyCFunction)count_material, METH_VARARGS,
|
||||||
"count_material(material) -> int\n\n"
|
"count_material(material) -> int\n\n"
|
||||||
"Count the number of voxels with the specified material ID."},
|
"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
|
{nullptr} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -594,5 +1265,7 @@ PyGetSetDef PyVoxelGrid::getsetters[] = {
|
||||||
"World-space position (x, y, z) of the grid origin.", nullptr},
|
"World-space position (x, y, z) of the grid origin.", nullptr},
|
||||||
{"rotation", (getter)get_rotation, (setter)set_rotation,
|
{"rotation", (getter)get_rotation, (setter)set_rotation,
|
||||||
"Y-axis rotation in degrees.", nullptr},
|
"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
|
{nullptr} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// PyVoxelGrid.h - Python bindings for VoxelGrid
|
// PyVoxelGrid.h - Python bindings for VoxelGrid
|
||||||
// Part of McRogueFace 3D Extension - Milestone 9
|
// Part of McRogueFace 3D Extension - Milestones 9, 11
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../Common.h"
|
#include "../Common.h"
|
||||||
|
|
@ -7,11 +7,8 @@
|
||||||
#include "VoxelGrid.h"
|
#include "VoxelGrid.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
// Forward declaration
|
|
||||||
class PyVoxelGrid;
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Python object structure
|
// Python object structures
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
typedef struct PyVoxelGridObject {
|
typedef struct PyVoxelGridObject {
|
||||||
|
|
@ -20,8 +17,13 @@ typedef struct PyVoxelGridObject {
|
||||||
PyObject* weakreflist;
|
PyObject* weakreflist;
|
||||||
} PyVoxelGridObject;
|
} PyVoxelGridObject;
|
||||||
|
|
||||||
|
typedef struct PyVoxelRegionObject {
|
||||||
|
PyObject_HEAD
|
||||||
|
std::shared_ptr<mcrf::VoxelRegion> data;
|
||||||
|
} PyVoxelRegionObject;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Python binding class
|
// Python binding classes
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
class PyVoxelGrid {
|
class PyVoxelGrid {
|
||||||
|
|
@ -46,6 +48,10 @@ public:
|
||||||
static PyObject* get_rotation(PyVoxelGridObject* self, void* closure);
|
static PyObject* get_rotation(PyVoxelGridObject* self, void* closure);
|
||||||
static int set_rotation(PyVoxelGridObject* self, PyObject* value, 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
|
// Voxel access methods
|
||||||
static PyObject* get(PyVoxelGridObject* self, PyObject* args);
|
static PyObject* get(PyVoxelGridObject* self, PyObject* args);
|
||||||
static PyObject* set(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* fill_box(PyVoxelGridObject* self, PyObject* args);
|
||||||
static PyObject* clear(PyVoxelGridObject* self, PyObject* Py_UNUSED(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)
|
// Mesh caching (Milestone 10)
|
||||||
static PyObject* get_vertex_count(PyVoxelGridObject* self, void* closure);
|
static PyObject* get_vertex_count(PyVoxelGridObject* self, void* closure);
|
||||||
static PyObject* rebuild_mesh(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
|
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
|
// Statistics
|
||||||
static PyObject* count_non_air(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
|
static PyObject* count_non_air(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
|
||||||
static PyObject* count_material(PyVoxelGridObject* self, PyObject* args);
|
static PyObject* count_material(PyVoxelGridObject* self, PyObject* args);
|
||||||
|
|
@ -72,8 +94,20 @@ public:
|
||||||
static PyGetSetDef getsetters[];
|
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 {
|
namespace mcrfpydef {
|
||||||
|
|
@ -122,4 +156,24 @@ inline PyTypeObject PyVoxelGridType = {
|
||||||
.tp_new = PyVoxelGrid::pynew,
|
.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
|
} // namespace mcrfpydef
|
||||||
|
|
|
||||||
|
|
@ -872,6 +872,104 @@ bool Viewport3D::removeVoxelLayer(std::shared_ptr<VoxelGrid> grid) {
|
||||||
return false;
|
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) {
|
void Viewport3D::renderVoxelLayers(const mat4& view, const mat4& proj) {
|
||||||
#ifdef MCRF_HAS_GL
|
#ifdef MCRF_HAS_GL
|
||||||
if (voxelLayers_.empty() || !shader_ || !shader_->isValid()) {
|
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());
|
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
|
} // namespace mcrf
|
||||||
|
|
||||||
// Methods array - outside namespace but PyObjectType still in scope via typedef
|
// 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"
|
"Get the number of voxel layers.\n\n"
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" Number of voxel layers in the viewport"},
|
" 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
|
{NULL} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,23 @@ public:
|
||||||
/// Render all voxel layers
|
/// Render all voxel layers
|
||||||
void renderVoxelLayers(const mat4& view, const mat4& proj);
|
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
|
// Background color
|
||||||
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
|
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
|
||||||
sf::Color getBackgroundColor() const { return bgColor_; }
|
sf::Color getBackgroundColor() const { return bgColor_; }
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
// VoxelGrid.cpp - Dense 3D voxel array implementation
|
// 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 "VoxelGrid.h"
|
||||||
#include "VoxelMesher.h"
|
#include "VoxelMesher.h"
|
||||||
#include "MeshLayer.h" // For MeshVertex
|
#include "MeshLayer.h" // For MeshVertex
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring> // For memcpy, memcmp
|
||||||
|
#include <fstream> // For file I/O
|
||||||
|
|
||||||
namespace mcrf {
|
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;
|
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)
|
// Mesh Caching (Milestone 10)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -164,8 +459,337 @@ const std::vector<MeshVertex>& VoxelGrid::getVertices() const {
|
||||||
|
|
||||||
void VoxelGrid::rebuildMesh() const {
|
void VoxelGrid::rebuildMesh() const {
|
||||||
cachedVertices_.clear();
|
cachedVertices_.clear();
|
||||||
VoxelMesher::generateMesh(*this, cachedVertices_);
|
if (greedyMeshing_) {
|
||||||
|
VoxelMesher::generateGreedyMesh(*this, cachedVertices_);
|
||||||
|
} else {
|
||||||
|
VoxelMesher::generateMesh(*this, cachedVertices_);
|
||||||
|
}
|
||||||
meshDirty_ = false;
|
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
|
} // namespace mcrf
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// VoxelGrid.h - Dense 3D voxel array with material palette
|
// 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
|
#pragma once
|
||||||
|
|
||||||
#include "../Common.h"
|
#include "../Common.h"
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
namespace mcrf {
|
namespace mcrf {
|
||||||
|
|
||||||
|
|
@ -28,6 +29,22 @@ struct VoxelMaterial {
|
||||||
: name(n), color(c), spriteIndex(sprite), transparent(transp), pathCost(cost) {}
|
: 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
|
// VoxelGrid - Dense 3D array of material IDs
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -43,9 +60,10 @@ private:
|
||||||
vec3 offset_;
|
vec3 offset_;
|
||||||
float rotation_ = 0.0f; // Y-axis only, degrees
|
float rotation_ = 0.0f; // Y-axis only, degrees
|
||||||
|
|
||||||
// Mesh caching (Milestone 10)
|
// Mesh caching (Milestones 10, 13)
|
||||||
mutable bool meshDirty_ = true;
|
mutable bool meshDirty_ = true;
|
||||||
mutable std::vector<MeshVertex> cachedVertices_;
|
mutable std::vector<MeshVertex> cachedVertices_;
|
||||||
|
bool greedyMeshing_ = false; // Use greedy meshing algorithm
|
||||||
|
|
||||||
// Index calculation (row-major: X varies fastest, then Y, then Z)
|
// Index calculation (row-major: X varies fastest, then Y, then Z)
|
||||||
inline size_t index(int x, int y, int z) const {
|
inline size_t index(int x, int y, int z) const {
|
||||||
|
|
@ -79,11 +97,39 @@ public:
|
||||||
const VoxelMaterial& getMaterial(uint8_t id) const;
|
const VoxelMaterial& getMaterial(uint8_t id) const;
|
||||||
size_t materialCount() const { return materials_.size(); }
|
size_t materialCount() const { return materials_.size(); }
|
||||||
|
|
||||||
// Bulk operations
|
// Bulk operations - Basic
|
||||||
void fill(uint8_t material);
|
void fill(uint8_t material);
|
||||||
void clear() { fill(0); }
|
void clear() { fill(0); }
|
||||||
void fillBox(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t material);
|
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
|
// Transform
|
||||||
void setOffset(const vec3& offset) { offset_ = offset; }
|
void setOffset(const vec3& offset) { offset_ = offset; }
|
||||||
void setOffset(float x, float y, float z) { offset_ = vec3(x, y, z); }
|
void setOffset(float x, float y, float z) { offset_ = vec3(x, y, z); }
|
||||||
|
|
@ -96,7 +142,7 @@ public:
|
||||||
size_t countNonAir() const;
|
size_t countNonAir() const;
|
||||||
size_t countMaterial(uint8_t material) 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)
|
/// Mark mesh as needing rebuild (called automatically by set/fill operations)
|
||||||
void markDirty() { meshDirty_ = true; }
|
void markDirty() { meshDirty_ = true; }
|
||||||
|
|
||||||
|
|
@ -112,10 +158,37 @@ public:
|
||||||
/// Get vertex count after mesh generation
|
/// Get vertex count after mesh generation
|
||||||
size_t vertexCount() const { return cachedVertices_.size(); }
|
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)
|
// Memory info (for debugging)
|
||||||
size_t memoryUsageBytes() const {
|
size_t memoryUsageBytes() const {
|
||||||
return data_.size() + materials_.size() * sizeof(VoxelMaterial);
|
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
|
} // namespace mcrf
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,187 @@ bool VoxelMesher::shouldGenerateFace(const VoxelGrid& grid,
|
||||||
return grid.getMaterial(neighbor).transparent;
|
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,
|
void VoxelMesher::emitFace(std::vector<MeshVertex>& vertices,
|
||||||
const vec3& center,
|
const vec3& center,
|
||||||
const vec3& normal,
|
const vec3& normal,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// VoxelMesher.h - Face-culled mesh generation for VoxelGrid
|
// 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
|
#pragma once
|
||||||
|
|
||||||
#include "VoxelGrid.h"
|
#include "VoxelGrid.h"
|
||||||
|
|
@ -14,7 +14,7 @@ namespace mcrf {
|
||||||
|
|
||||||
class VoxelMesher {
|
class VoxelMesher {
|
||||||
public:
|
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)
|
/// Output vertices in local space (model matrix applies world transform)
|
||||||
/// @param grid The VoxelGrid to generate mesh from
|
/// @param grid The VoxelGrid to generate mesh from
|
||||||
/// @param outVertices Output vector of vertices (appended to, not cleared)
|
/// @param outVertices Output vector of vertices (appended to, not cleared)
|
||||||
|
|
@ -23,6 +23,16 @@ public:
|
||||||
std::vector<MeshVertex>& outVertices
|
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:
|
private:
|
||||||
/// Check if face should be generated (neighbor is air or transparent)
|
/// Check if face should be generated (neighbor is air or transparent)
|
||||||
/// @param grid The VoxelGrid
|
/// @param grid The VoxelGrid
|
||||||
|
|
@ -48,6 +58,23 @@ private:
|
||||||
float size,
|
float size,
|
||||||
const VoxelMaterial& material
|
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
|
} // namespace mcrf
|
||||||
|
|
|
||||||
|
|
@ -444,6 +444,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
||||||
&mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType,
|
&mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType,
|
||||||
&mcrfpydef::PyBillboardType, &mcrfpydef::PyVoxelGridType,
|
&mcrfpydef::PyBillboardType, &mcrfpydef::PyVoxelGridType,
|
||||||
|
&mcrfpydef::PyVoxelRegionType,
|
||||||
|
|
||||||
/*grid layers (#147)*/
|
/*grid layers (#147)*/
|
||||||
&PyColorLayerType, &PyTileLayerType,
|
&PyColorLayerType, &PyTileLayerType,
|
||||||
|
|
@ -544,6 +545,9 @@ PyObject* PyInit_mcrfpy()
|
||||||
mcrfpydef::PyVoxelGridType.tp_methods = PyVoxelGrid::methods;
|
mcrfpydef::PyVoxelGridType.tp_methods = PyVoxelGrid::methods;
|
||||||
mcrfpydef::PyVoxelGridType.tp_getset = PyVoxelGrid::getsetters;
|
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)
|
// Set up PyShaderType methods and getsetters (#106)
|
||||||
mcrfpydef::PyShaderType.tp_methods = PyShader::methods;
|
mcrfpydef::PyShaderType.tp_methods = PyShader::methods;
|
||||||
mcrfpydef::PyShaderType.tp_getset = PyShader::getsetters;
|
mcrfpydef::PyShaderType.tp_getset = PyShader::getsetters;
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,68 @@ PyObject* PyColor::pyObject()
|
||||||
|
|
||||||
sf::Color PyColor::fromPy(PyObject* obj)
|
sf::Color PyColor::fromPy(PyObject* obj)
|
||||||
{
|
{
|
||||||
PyColorObject* self = (PyColorObject*)obj;
|
// Handle None or NULL
|
||||||
return self->data;
|
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)
|
sf::Color PyColor::fromPy(PyColorObject* self)
|
||||||
|
|
|
||||||
263
tests/demo/screens/voxel_core_demo.py
Normal file
263
tests/demo/screens/voxel_core_demo.py
Normal 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)
|
||||||
273
tests/demo/screens/voxel_dungeon_demo.py
Normal file
273
tests/demo/screens/voxel_dungeon_demo.py
Normal 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")
|
||||||
250
tests/demo/screens/voxel_navigation_demo.py
Normal file
250
tests/demo/screens/voxel_navigation_demo.py
Normal 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)
|
||||||
314
tests/demo/screens/voxel_serialization_demo.py
Normal file
314
tests/demo/screens/voxel_serialization_demo.py
Normal 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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue