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;
|
||||
}
|
||||
|
||||
// Greedy meshing (Milestone 13)
|
||||
PyObject* PyVoxelGrid::get_greedy_meshing(PyVoxelGridObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
return PyBool_FromLong(self->data->isGreedyMeshingEnabled());
|
||||
}
|
||||
|
||||
int PyVoxelGrid::set_greedy_meshing(PyVoxelGridObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyBool_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "greedy_meshing must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool enabled = (value == Py_True);
|
||||
self->data->setGreedyMeshing(enabled);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Voxel access methods
|
||||
// =============================================================================
|
||||
|
|
@ -522,6 +547,557 @@ PyObject* PyVoxelGrid::count_material(PyVoxelGridObject* self, PyObject* args) {
|
|||
return PyLong_FromSize_t(self->data->countMaterial(static_cast<uint8_t>(material)));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Bulk operations - Milestone 11
|
||||
// =============================================================================
|
||||
|
||||
PyObject* PyVoxelGrid::fill_box_hollow(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const char* kwlist[] = {"min_coord", "max_coord", "material", "thickness", nullptr};
|
||||
|
||||
PyObject* min_obj = nullptr;
|
||||
PyObject* max_obj = nullptr;
|
||||
int material;
|
||||
int thickness = 1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi|i", const_cast<char**>(kwlist),
|
||||
&min_obj, &max_obj, &material, &thickness)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (material < 0 || material > 255) {
|
||||
PyErr_SetString(PyExc_ValueError, "material must be 0-255");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (thickness < 1) {
|
||||
PyErr_SetString(PyExc_ValueError, "thickness must be >= 1");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Parse coordinates
|
||||
if (!PyTuple_Check(min_obj) && !PyList_Check(min_obj)) {
|
||||
PyErr_SetString(PyExc_TypeError, "min_coord must be a tuple or list of 3 integers");
|
||||
return nullptr;
|
||||
}
|
||||
if (!PyTuple_Check(max_obj) && !PyList_Check(max_obj)) {
|
||||
PyErr_SetString(PyExc_TypeError, "max_coord must be a tuple or list of 3 integers");
|
||||
return nullptr;
|
||||
}
|
||||
if (PySequence_Size(min_obj) != 3 || PySequence_Size(max_obj) != 3) {
|
||||
PyErr_SetString(PyExc_ValueError, "coordinates must have exactly 3 elements");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int x0, y0, z0, x1, y1, z1;
|
||||
PyObject* items[6];
|
||||
items[0] = PySequence_GetItem(min_obj, 0);
|
||||
items[1] = PySequence_GetItem(min_obj, 1);
|
||||
items[2] = PySequence_GetItem(min_obj, 2);
|
||||
items[3] = PySequence_GetItem(max_obj, 0);
|
||||
items[4] = PySequence_GetItem(max_obj, 1);
|
||||
items[5] = PySequence_GetItem(max_obj, 2);
|
||||
|
||||
bool valid = true;
|
||||
if (PyLong_Check(items[0])) x0 = (int)PyLong_AsLong(items[0]); else valid = false;
|
||||
if (PyLong_Check(items[1])) y0 = (int)PyLong_AsLong(items[1]); else valid = false;
|
||||
if (PyLong_Check(items[2])) z0 = (int)PyLong_AsLong(items[2]); else valid = false;
|
||||
if (PyLong_Check(items[3])) x1 = (int)PyLong_AsLong(items[3]); else valid = false;
|
||||
if (PyLong_Check(items[4])) y1 = (int)PyLong_AsLong(items[4]); else valid = false;
|
||||
if (PyLong_Check(items[5])) z1 = (int)PyLong_AsLong(items[5]); else valid = false;
|
||||
|
||||
for (int i = 0; i < 6; i++) Py_DECREF(items[i]);
|
||||
|
||||
if (!valid) {
|
||||
PyErr_SetString(PyExc_TypeError, "coordinate elements must be integers");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
self->data->fillBoxHollow(x0, y0, z0, x1, y1, z1, static_cast<uint8_t>(material), thickness);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyVoxelGrid::fill_sphere(PyVoxelGridObject* self, PyObject* args) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObject* center_obj = nullptr;
|
||||
int radius;
|
||||
int material;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "Oii", ¢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
|
||||
// =============================================================================
|
||||
|
|
@ -556,6 +1132,54 @@ PyMethodDef PyVoxelGrid::methods[] = {
|
|||
" max_coord: (x1, y1, z1) - maximum corner (inclusive)\n"
|
||||
" material: material ID (0-255)\n\n"
|
||||
"Coordinates are clamped to grid bounds."},
|
||||
{"fill_box_hollow", (PyCFunction)fill_box_hollow, METH_VARARGS | METH_KEYWORDS,
|
||||
"fill_box_hollow(min_coord, max_coord, material, thickness=1) -> None\n\n"
|
||||
"Create a hollow rectangular room (walls only, hollow inside).\n\n"
|
||||
"Args:\n"
|
||||
" min_coord: (x0, y0, z0) - minimum corner (inclusive)\n"
|
||||
" max_coord: (x1, y1, z1) - maximum corner (inclusive)\n"
|
||||
" material: material ID for walls (0-255)\n"
|
||||
" thickness: wall thickness in voxels (default 1)"},
|
||||
{"fill_sphere", (PyCFunction)fill_sphere, METH_VARARGS,
|
||||
"fill_sphere(center, radius, material) -> None\n\n"
|
||||
"Fill a spherical region.\n\n"
|
||||
"Args:\n"
|
||||
" center: (cx, cy, cz) - sphere center coordinates\n"
|
||||
" radius: sphere radius in voxels\n"
|
||||
" material: material ID (0-255, use 0 to carve)"},
|
||||
{"fill_cylinder", (PyCFunction)fill_cylinder, METH_VARARGS,
|
||||
"fill_cylinder(base_pos, radius, height, material) -> None\n\n"
|
||||
"Fill a vertical cylinder (Y-axis aligned).\n\n"
|
||||
"Args:\n"
|
||||
" base_pos: (cx, cy, cz) - base center position\n"
|
||||
" radius: cylinder radius in voxels\n"
|
||||
" height: cylinder height in voxels\n"
|
||||
" material: material ID (0-255)"},
|
||||
{"fill_noise", (PyCFunction)fill_noise, METH_VARARGS | METH_KEYWORDS,
|
||||
"fill_noise(min_coord, max_coord, material, threshold=0.5, scale=0.1, seed=0) -> None\n\n"
|
||||
"Fill region with 3D noise-based pattern (caves, clouds).\n\n"
|
||||
"Args:\n"
|
||||
" min_coord: (x0, y0, z0) - minimum corner\n"
|
||||
" max_coord: (x1, y1, z1) - maximum corner\n"
|
||||
" material: material ID for solid areas\n"
|
||||
" threshold: noise threshold (0-1, higher = more solid)\n"
|
||||
" scale: noise scale (smaller = larger features)\n"
|
||||
" seed: random seed (0 for default)"},
|
||||
{"copy_region", (PyCFunction)copy_region, METH_VARARGS,
|
||||
"copy_region(min_coord, max_coord) -> VoxelRegion\n\n"
|
||||
"Copy a rectangular region to a VoxelRegion prefab.\n\n"
|
||||
"Args:\n"
|
||||
" min_coord: (x0, y0, z0) - minimum corner (inclusive)\n"
|
||||
" max_coord: (x1, y1, z1) - maximum corner (inclusive)\n\n"
|
||||
"Returns:\n"
|
||||
" VoxelRegion object that can be pasted elsewhere."},
|
||||
{"paste_region", (PyCFunction)paste_region, METH_VARARGS | METH_KEYWORDS,
|
||||
"paste_region(region, position, skip_air=True) -> None\n\n"
|
||||
"Paste a VoxelRegion prefab at the specified position.\n\n"
|
||||
"Args:\n"
|
||||
" region: VoxelRegion from copy_region()\n"
|
||||
" position: (x, y, z) - paste destination\n"
|
||||
" skip_air: if True, air voxels don't overwrite (default True)"},
|
||||
{"clear", (PyCFunction)clear, METH_NOARGS,
|
||||
"clear() -> None\n\n"
|
||||
"Clear the grid (fill with air, material 0)."},
|
||||
|
|
@ -568,6 +1192,53 @@ PyMethodDef PyVoxelGrid::methods[] = {
|
|||
{"count_material", (PyCFunction)count_material, METH_VARARGS,
|
||||
"count_material(material) -> int\n\n"
|
||||
"Count the number of voxels with the specified material ID."},
|
||||
{"project_column", (PyCFunction)project_column, METH_VARARGS | METH_KEYWORDS,
|
||||
"project_column(x, z, headroom=2) -> dict\n\n"
|
||||
"Project a single column to navigation info.\n\n"
|
||||
"Scans the column from top to bottom, finding the topmost floor\n"
|
||||
"(solid voxel with air above) and checking for adequate headroom.\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate in voxel grid\n"
|
||||
" z: Z coordinate in voxel grid\n"
|
||||
" headroom: Required air voxels above floor (default 2)\n\n"
|
||||
"Returns:\n"
|
||||
" dict with keys:\n"
|
||||
" height (float): World Y of floor surface\n"
|
||||
" walkable (bool): True if floor found with adequate headroom\n"
|
||||
" transparent (bool): True if no opaque voxels in column\n"
|
||||
" path_cost (float): Floor material's path cost"},
|
||||
{"save", (PyCFunction)PyVoxelGrid::save, METH_VARARGS,
|
||||
"save(path) -> bool\n\n"
|
||||
"Save the voxel grid to a binary file.\n\n"
|
||||
"Args:\n"
|
||||
" path: File path to save to (.mcvg extension recommended)\n\n"
|
||||
"Returns:\n"
|
||||
" True on success, False on failure.\n\n"
|
||||
"The file format includes grid dimensions, cell size, material palette,\n"
|
||||
"and RLE-compressed voxel data."},
|
||||
{"load", (PyCFunction)PyVoxelGrid::load, METH_VARARGS,
|
||||
"load(path) -> bool\n\n"
|
||||
"Load voxel data from a binary file.\n\n"
|
||||
"Args:\n"
|
||||
" path: File path to load from\n\n"
|
||||
"Returns:\n"
|
||||
" True on success, False on failure.\n\n"
|
||||
"Note: This replaces the current grid data entirely, including\n"
|
||||
"dimensions and material palette."},
|
||||
{"to_bytes", (PyCFunction)PyVoxelGrid::to_bytes, METH_NOARGS,
|
||||
"to_bytes() -> bytes\n\n"
|
||||
"Serialize the voxel grid to a bytes object.\n\n"
|
||||
"Returns:\n"
|
||||
" bytes object containing the serialized grid data.\n\n"
|
||||
"Useful for network transmission or custom storage."},
|
||||
{"from_bytes", (PyCFunction)PyVoxelGrid::from_bytes, METH_VARARGS,
|
||||
"from_bytes(data) -> bool\n\n"
|
||||
"Load voxel data from a bytes object.\n\n"
|
||||
"Args:\n"
|
||||
" data: bytes object containing serialized grid data\n\n"
|
||||
"Returns:\n"
|
||||
" True on success, False on failure.\n\n"
|
||||
"Note: This replaces the current grid data entirely."},
|
||||
{nullptr} // Sentinel
|
||||
};
|
||||
|
||||
|
|
@ -594,5 +1265,7 @@ PyGetSetDef PyVoxelGrid::getsetters[] = {
|
|||
"World-space position (x, y, z) of the grid origin.", nullptr},
|
||||
{"rotation", (getter)get_rotation, (setter)set_rotation,
|
||||
"Y-axis rotation in degrees.", nullptr},
|
||||
{"greedy_meshing", (getter)get_greedy_meshing, (setter)set_greedy_meshing,
|
||||
"Enable greedy meshing optimization (reduces vertex count for uniform regions).", nullptr},
|
||||
{nullptr} // Sentinel
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// PyVoxelGrid.h - Python bindings for VoxelGrid
|
||||
// Part of McRogueFace 3D Extension - Milestone 9
|
||||
// Part of McRogueFace 3D Extension - Milestones 9, 11
|
||||
#pragma once
|
||||
|
||||
#include "../Common.h"
|
||||
|
|
@ -7,11 +7,8 @@
|
|||
#include "VoxelGrid.h"
|
||||
#include <memory>
|
||||
|
||||
// Forward declaration
|
||||
class PyVoxelGrid;
|
||||
|
||||
// =============================================================================
|
||||
// Python object structure
|
||||
// Python object structures
|
||||
// =============================================================================
|
||||
|
||||
typedef struct PyVoxelGridObject {
|
||||
|
|
@ -20,8 +17,13 @@ typedef struct PyVoxelGridObject {
|
|||
PyObject* weakreflist;
|
||||
} PyVoxelGridObject;
|
||||
|
||||
typedef struct PyVoxelRegionObject {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<mcrf::VoxelRegion> data;
|
||||
} PyVoxelRegionObject;
|
||||
|
||||
// =============================================================================
|
||||
// Python binding class
|
||||
// Python binding classes
|
||||
// =============================================================================
|
||||
|
||||
class PyVoxelGrid {
|
||||
|
|
@ -46,6 +48,10 @@ public:
|
|||
static PyObject* get_rotation(PyVoxelGridObject* self, void* closure);
|
||||
static int set_rotation(PyVoxelGridObject* self, PyObject* value, void* closure);
|
||||
|
||||
// Properties - mesh generation (Milestone 13)
|
||||
static PyObject* get_greedy_meshing(PyVoxelGridObject* self, void* closure);
|
||||
static int set_greedy_meshing(PyVoxelGridObject* self, PyObject* value, void* closure);
|
||||
|
||||
// Voxel access methods
|
||||
static PyObject* get(PyVoxelGridObject* self, PyObject* args);
|
||||
static PyObject* set(PyVoxelGridObject* self, PyObject* args);
|
||||
|
|
@ -59,10 +65,26 @@ public:
|
|||
static PyObject* fill_box(PyVoxelGridObject* self, PyObject* args);
|
||||
static PyObject* clear(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
|
||||
|
||||
// Bulk operations - Milestone 11
|
||||
static PyObject* fill_box_hollow(PyVoxelGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* fill_sphere(PyVoxelGridObject* self, PyObject* args);
|
||||
static PyObject* fill_cylinder(PyVoxelGridObject* self, PyObject* args);
|
||||
static PyObject* fill_noise(PyVoxelGridObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Copy/paste operations - Milestone 11
|
||||
static PyObject* copy_region(PyVoxelGridObject* self, PyObject* args);
|
||||
static PyObject* paste_region(PyVoxelGridObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Mesh caching (Milestone 10)
|
||||
static PyObject* get_vertex_count(PyVoxelGridObject* self, void* closure);
|
||||
static PyObject* rebuild_mesh(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
|
||||
|
||||
// Serialization (Milestone 14)
|
||||
static PyObject* save(PyVoxelGridObject* self, PyObject* args);
|
||||
static PyObject* load(PyVoxelGridObject* self, PyObject* args);
|
||||
static PyObject* to_bytes(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
|
||||
static PyObject* from_bytes(PyVoxelGridObject* self, PyObject* args);
|
||||
|
||||
// Statistics
|
||||
static PyObject* count_non_air(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
|
||||
static PyObject* count_material(PyVoxelGridObject* self, PyObject* args);
|
||||
|
|
@ -72,8 +94,20 @@ public:
|
|||
static PyGetSetDef getsetters[];
|
||||
};
|
||||
|
||||
class PyVoxelRegion {
|
||||
public:
|
||||
static void dealloc(PyVoxelRegionObject* self);
|
||||
static PyObject* repr(PyObject* obj);
|
||||
static PyObject* get_size(PyVoxelRegionObject* self, void* closure);
|
||||
static PyObject* get_width(PyVoxelRegionObject* self, void* closure);
|
||||
static PyObject* get_height(PyVoxelRegionObject* self, void* closure);
|
||||
static PyObject* get_depth(PyVoxelRegionObject* self, void* closure);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Python type definition
|
||||
// Python type definitions (in mcrfpydef namespace)
|
||||
// =============================================================================
|
||||
|
||||
namespace mcrfpydef {
|
||||
|
|
@ -122,4 +156,24 @@ inline PyTypeObject PyVoxelGridType = {
|
|||
.tp_new = PyVoxelGrid::pynew,
|
||||
};
|
||||
|
||||
inline PyTypeObject PyVoxelRegionType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.VoxelRegion",
|
||||
.tp_basicsize = sizeof(PyVoxelRegionObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)PyVoxelRegion::dealloc,
|
||||
.tp_repr = PyVoxelRegion::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR(
|
||||
"VoxelRegion - Portable voxel data for copy/paste operations.\n\n"
|
||||
"Created by VoxelGrid.copy_region(), used with paste_region().\n"
|
||||
"Cannot be instantiated directly.\n\n"
|
||||
"Properties:\n"
|
||||
" size (tuple, read-only): Dimensions as (width, height, depth)\n"
|
||||
" width, height, depth (int, read-only): Individual dimensions"
|
||||
),
|
||||
.tp_getset = nullptr, // Set before PyType_Ready
|
||||
.tp_new = nullptr, // Cannot instantiate directly
|
||||
};
|
||||
|
||||
} // namespace mcrfpydef
|
||||
|
|
|
|||
|
|
@ -872,6 +872,104 @@ bool Viewport3D::removeVoxelLayer(std::shared_ptr<VoxelGrid> grid) {
|
|||
return false;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Voxel-to-Nav Projection (Milestone 12)
|
||||
// =============================================================================
|
||||
|
||||
void Viewport3D::clearVoxelNavRegion(std::shared_ptr<VoxelGrid> grid) {
|
||||
if (!grid || navGrid_.empty()) return;
|
||||
|
||||
// Get voxel grid offset in world space
|
||||
vec3 offset = grid->getOffset();
|
||||
float cellSize = grid->cellSize();
|
||||
|
||||
// Calculate nav grid cell offset from voxel grid offset
|
||||
int navOffsetX = static_cast<int>(std::floor(offset.x / cellSize_));
|
||||
int navOffsetZ = static_cast<int>(std::floor(offset.z / cellSize_));
|
||||
|
||||
// Clear nav cells corresponding to voxel grid footprint
|
||||
for (int vz = 0; vz < grid->depth(); vz++) {
|
||||
for (int vx = 0; vx < grid->width(); vx++) {
|
||||
int navX = navOffsetX + vx;
|
||||
int navZ = navOffsetZ + vz;
|
||||
|
||||
if (isValidCell(navX, navZ)) {
|
||||
VoxelPoint& cell = at(navX, navZ);
|
||||
cell.walkable = true;
|
||||
cell.transparent = true;
|
||||
cell.height = 0.0f;
|
||||
cell.cost = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync to TCOD
|
||||
syncToTCOD();
|
||||
}
|
||||
|
||||
void Viewport3D::projectVoxelToNav(std::shared_ptr<VoxelGrid> grid, int headroom) {
|
||||
if (!grid || navGrid_.empty()) return;
|
||||
|
||||
// Get voxel grid offset in world space
|
||||
vec3 offset = grid->getOffset();
|
||||
float voxelCellSize = grid->cellSize();
|
||||
|
||||
// Calculate nav grid cell offset from voxel grid offset
|
||||
// Assuming nav cell size matches voxel cell size for 1:1 mapping
|
||||
int navOffsetX = static_cast<int>(std::floor(offset.x / cellSize_));
|
||||
int navOffsetZ = static_cast<int>(std::floor(offset.z / cellSize_));
|
||||
|
||||
// Project each column of the voxel grid to the navigation grid
|
||||
for (int vz = 0; vz < grid->depth(); vz++) {
|
||||
for (int vx = 0; vx < grid->width(); vx++) {
|
||||
int navX = navOffsetX + vx;
|
||||
int navZ = navOffsetZ + vz;
|
||||
|
||||
if (!isValidCell(navX, navZ)) continue;
|
||||
|
||||
// Get projection info from voxel column
|
||||
VoxelGrid::NavInfo navInfo = grid->projectColumn(vx, vz, headroom);
|
||||
|
||||
// Update nav cell
|
||||
VoxelPoint& cell = at(navX, navZ);
|
||||
cell.height = navInfo.height + offset.y; // Add world Y offset
|
||||
cell.walkable = navInfo.walkable;
|
||||
cell.transparent = navInfo.transparent;
|
||||
cell.cost = navInfo.pathCost;
|
||||
|
||||
// Sync this cell to TCOD
|
||||
syncTCODCell(navX, navZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport3D::projectAllVoxelsToNav(int headroom) {
|
||||
if (navGrid_.empty()) return;
|
||||
|
||||
// First, reset all nav cells to default state
|
||||
for (auto& cell : navGrid_) {
|
||||
cell.walkable = true;
|
||||
cell.transparent = true;
|
||||
cell.height = 0.0f;
|
||||
cell.cost = 1.0f;
|
||||
}
|
||||
|
||||
// Project each voxel layer in order (later layers overwrite earlier)
|
||||
// Sort by z_index so higher z_index layers take precedence
|
||||
std::vector<std::pair<std::shared_ptr<VoxelGrid>, int>> sortedLayers = voxelLayers_;
|
||||
std::sort(sortedLayers.begin(), sortedLayers.end(),
|
||||
[](const auto& a, const auto& b) { return a.second < b.second; });
|
||||
|
||||
for (const auto& pair : sortedLayers) {
|
||||
if (pair.first) {
|
||||
projectVoxelToNav(pair.first, headroom);
|
||||
}
|
||||
}
|
||||
|
||||
// Final sync to TCOD (redundant but ensures consistency)
|
||||
syncToTCOD();
|
||||
}
|
||||
|
||||
void Viewport3D::renderVoxelLayers(const mat4& view, const mat4& proj) {
|
||||
#ifdef MCRF_HAS_GL
|
||||
if (voxelLayers_.empty() || !shader_ || !shader_->isValid()) {
|
||||
|
|
@ -2475,6 +2573,99 @@ static PyObject* Viewport3D_voxel_layer_count(PyViewport3DObject* self, PyObject
|
|||
return PyLong_FromSize_t(self->data->getVoxelLayerCount());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Voxel-to-Nav Projection Methods (Milestone 12)
|
||||
// =============================================================================
|
||||
|
||||
static PyObject* Viewport3D_project_voxel_to_nav(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"voxel_grid", "headroom", NULL};
|
||||
PyObject* voxel_grid_obj = nullptr;
|
||||
int headroom = 2;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|i", const_cast<char**>(kwlist),
|
||||
&voxel_grid_obj, &headroom)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check if it's a VoxelGrid object
|
||||
PyTypeObject* voxelGridType = (PyTypeObject*)PyObject_GetAttrString(
|
||||
McRFPy_API::mcrf_module, "VoxelGrid");
|
||||
if (!voxelGridType) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid type not found");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!PyObject_IsInstance(voxel_grid_obj, (PyObject*)voxelGridType)) {
|
||||
Py_DECREF(voxelGridType);
|
||||
PyErr_SetString(PyExc_TypeError, "voxel_grid must be a VoxelGrid object");
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(voxelGridType);
|
||||
|
||||
PyVoxelGridObject* vg = (PyVoxelGridObject*)voxel_grid_obj;
|
||||
if (!vg->data) {
|
||||
PyErr_SetString(PyExc_ValueError, "VoxelGrid not initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (headroom < 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "headroom must be non-negative");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->data->projectVoxelToNav(vg->data, headroom);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_project_all_voxels_to_nav(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"headroom", NULL};
|
||||
int headroom = 2;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", const_cast<char**>(kwlist), &headroom)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (headroom < 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "headroom must be non-negative");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->data->projectAllVoxelsToNav(headroom);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_clear_voxel_nav_region(PyViewport3DObject* self, PyObject* args) {
|
||||
PyObject* voxel_grid_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "O", &voxel_grid_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check if it's a VoxelGrid object
|
||||
PyTypeObject* voxelGridType = (PyTypeObject*)PyObject_GetAttrString(
|
||||
McRFPy_API::mcrf_module, "VoxelGrid");
|
||||
if (!voxelGridType) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid type not found");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!PyObject_IsInstance(voxel_grid_obj, (PyObject*)voxelGridType)) {
|
||||
Py_DECREF(voxelGridType);
|
||||
PyErr_SetString(PyExc_TypeError, "voxel_grid must be a VoxelGrid object");
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(voxelGridType);
|
||||
|
||||
PyVoxelGridObject* vg = (PyVoxelGridObject*)voxel_grid_obj;
|
||||
if (!vg->data) {
|
||||
PyErr_SetString(PyExc_ValueError, "VoxelGrid not initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->data->clearVoxelNavRegion(vg->data);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
} // namespace mcrf
|
||||
|
||||
// Methods array - outside namespace but PyObjectType still in scope via typedef
|
||||
|
|
@ -2674,5 +2865,29 @@ PyMethodDef Viewport3D_methods[] = {
|
|||
"Get the number of voxel layers.\n\n"
|
||||
"Returns:\n"
|
||||
" Number of voxel layers in the viewport"},
|
||||
|
||||
// Voxel-to-Nav projection methods (Milestone 12)
|
||||
{"project_voxel_to_nav", (PyCFunction)mcrf::Viewport3D_project_voxel_to_nav, METH_VARARGS | METH_KEYWORDS,
|
||||
"project_voxel_to_nav(voxel_grid, headroom=2)\n\n"
|
||||
"Project a VoxelGrid to the navigation grid.\n\n"
|
||||
"Scans each column of the voxel grid and updates corresponding\n"
|
||||
"navigation cells with walkability, transparency, height, and cost.\n\n"
|
||||
"Args:\n"
|
||||
" voxel_grid: VoxelGrid to project\n"
|
||||
" headroom: Required air voxels above floor for walkability (default: 2)"},
|
||||
{"project_all_voxels_to_nav", (PyCFunction)mcrf::Viewport3D_project_all_voxels_to_nav, METH_VARARGS | METH_KEYWORDS,
|
||||
"project_all_voxels_to_nav(headroom=2)\n\n"
|
||||
"Project all voxel layers to the navigation grid.\n\n"
|
||||
"Resets navigation grid and projects each voxel layer in z_index order.\n"
|
||||
"Later layers (higher z_index) overwrite earlier ones.\n\n"
|
||||
"Args:\n"
|
||||
" headroom: Required air voxels above floor for walkability (default: 2)"},
|
||||
{"clear_voxel_nav_region", (PyCFunction)mcrf::Viewport3D_clear_voxel_nav_region, METH_VARARGS,
|
||||
"clear_voxel_nav_region(voxel_grid)\n\n"
|
||||
"Clear navigation cells in a voxel grid's footprint.\n\n"
|
||||
"Resets walkability, transparency, height, and cost to defaults\n"
|
||||
"for all nav cells corresponding to the voxel grid's XZ extent.\n\n"
|
||||
"Args:\n"
|
||||
" voxel_grid: VoxelGrid whose nav region to clear"},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
|
|
|||
|
|
@ -250,6 +250,23 @@ public:
|
|||
/// Render all voxel layers
|
||||
void renderVoxelLayers(const mat4& view, const mat4& proj);
|
||||
|
||||
// =========================================================================
|
||||
// Voxel-to-Nav Projection (Milestone 12)
|
||||
// =========================================================================
|
||||
|
||||
/// Project a single voxel grid to the navigation grid
|
||||
/// @param grid The voxel grid to project
|
||||
/// @param headroom Required air voxels above floor for walkability
|
||||
void projectVoxelToNav(std::shared_ptr<VoxelGrid> grid, int headroom = 2);
|
||||
|
||||
/// Project all voxel layers to the navigation grid
|
||||
/// @param headroom Required air voxels above floor for walkability
|
||||
void projectAllVoxelsToNav(int headroom = 2);
|
||||
|
||||
/// Clear nav cells in a voxel grid's footprint (before re-projection)
|
||||
/// @param grid The voxel grid whose footprint to clear
|
||||
void clearVoxelNavRegion(std::shared_ptr<VoxelGrid> grid);
|
||||
|
||||
// Background color
|
||||
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
|
||||
sf::Color getBackgroundColor() const { return bgColor_; }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
// VoxelGrid.cpp - Dense 3D voxel array implementation
|
||||
// Part of McRogueFace 3D Extension - Milestones 9-10
|
||||
// Part of McRogueFace 3D Extension - Milestones 9-11
|
||||
|
||||
#include "VoxelGrid.h"
|
||||
#include "VoxelMesher.h"
|
||||
#include "MeshLayer.h" // For MeshVertex
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <cstring> // For memcpy, memcmp
|
||||
#include <fstream> // For file I/O
|
||||
|
||||
namespace mcrf {
|
||||
|
||||
|
|
@ -151,6 +155,297 @@ void VoxelGrid::fillBox(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t
|
|||
meshDirty_ = true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Bulk Operations - Milestone 11
|
||||
// =============================================================================
|
||||
|
||||
void VoxelGrid::fillBoxHollow(int x0, int y0, int z0, int x1, int y1, int z1,
|
||||
uint8_t material, int thickness) {
|
||||
// Ensure proper ordering (min to max)
|
||||
if (x0 > x1) std::swap(x0, x1);
|
||||
if (y0 > y1) std::swap(y0, y1);
|
||||
if (z0 > z1) std::swap(z0, z1);
|
||||
|
||||
// Fill entire box with material
|
||||
fillBox(x0, y0, z0, x1, y1, z1, material);
|
||||
|
||||
// Carve out interior (inset by thickness on all sides)
|
||||
int ix0 = x0 + thickness;
|
||||
int iy0 = y0 + thickness;
|
||||
int iz0 = z0 + thickness;
|
||||
int ix1 = x1 - thickness;
|
||||
int iy1 = y1 - thickness;
|
||||
int iz1 = z1 - thickness;
|
||||
|
||||
// Only carve if there's interior space
|
||||
if (ix0 <= ix1 && iy0 <= iy1 && iz0 <= iz1) {
|
||||
fillBox(ix0, iy0, iz0, ix1, iy1, iz1, 0); // Air
|
||||
}
|
||||
// meshDirty_ already set by fillBox calls
|
||||
}
|
||||
|
||||
void VoxelGrid::fillSphere(int cx, int cy, int cz, int radius, uint8_t material) {
|
||||
int r2 = radius * radius;
|
||||
|
||||
for (int z = cz - radius; z <= cz + radius; z++) {
|
||||
for (int y = cy - radius; y <= cy + radius; y++) {
|
||||
for (int x = cx - radius; x <= cx + radius; x++) {
|
||||
int dx = x - cx;
|
||||
int dy = y - cy;
|
||||
int dz = z - cz;
|
||||
if (dx * dx + dy * dy + dz * dz <= r2) {
|
||||
if (isValid(x, y, z)) {
|
||||
data_[index(x, y, z)] = material;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
meshDirty_ = true;
|
||||
}
|
||||
|
||||
void VoxelGrid::fillCylinder(int cx, int cy, int cz, int radius, int height, uint8_t material) {
|
||||
int r2 = radius * radius;
|
||||
|
||||
for (int y = cy; y < cy + height; y++) {
|
||||
for (int z = cz - radius; z <= cz + radius; z++) {
|
||||
for (int x = cx - radius; x <= cx + radius; x++) {
|
||||
int dx = x - cx;
|
||||
int dz = z - cz;
|
||||
if (dx * dx + dz * dz <= r2) {
|
||||
if (isValid(x, y, z)) {
|
||||
data_[index(x, y, z)] = material;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
meshDirty_ = true;
|
||||
}
|
||||
|
||||
// Simple 3D noise implementation (hash-based, similar to value noise)
|
||||
namespace {
|
||||
// Simple hash function for noise
|
||||
inline unsigned int hash3D(int x, int y, int z, unsigned int seed) {
|
||||
unsigned int h = seed;
|
||||
h ^= static_cast<unsigned int>(x) * 374761393u;
|
||||
h ^= static_cast<unsigned int>(y) * 668265263u;
|
||||
h ^= static_cast<unsigned int>(z) * 2147483647u;
|
||||
h = (h ^ (h >> 13)) * 1274126177u;
|
||||
return h;
|
||||
}
|
||||
|
||||
// Convert hash to 0-1 float
|
||||
inline float hashToFloat(unsigned int h) {
|
||||
return static_cast<float>(h & 0xFFFFFF) / static_cast<float>(0xFFFFFF);
|
||||
}
|
||||
|
||||
// Linear interpolation
|
||||
inline float lerp(float a, float b, float t) {
|
||||
return a + t * (b - a);
|
||||
}
|
||||
|
||||
// Smoothstep for smoother interpolation
|
||||
inline float smoothstep(float t) {
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
}
|
||||
|
||||
// 3D value noise
|
||||
float noise3D(float x, float y, float z, unsigned int seed) {
|
||||
int xi = static_cast<int>(std::floor(x));
|
||||
int yi = static_cast<int>(std::floor(y));
|
||||
int zi = static_cast<int>(std::floor(z));
|
||||
|
||||
float xf = x - xi;
|
||||
float yf = y - yi;
|
||||
float zf = z - zi;
|
||||
|
||||
// Smoothstep the fractions
|
||||
float u = smoothstep(xf);
|
||||
float v = smoothstep(yf);
|
||||
float w = smoothstep(zf);
|
||||
|
||||
// Hash corners of the unit cube
|
||||
float c000 = hashToFloat(hash3D(xi, yi, zi, seed));
|
||||
float c100 = hashToFloat(hash3D(xi + 1, yi, zi, seed));
|
||||
float c010 = hashToFloat(hash3D(xi, yi + 1, zi, seed));
|
||||
float c110 = hashToFloat(hash3D(xi + 1, yi + 1, zi, seed));
|
||||
float c001 = hashToFloat(hash3D(xi, yi, zi + 1, seed));
|
||||
float c101 = hashToFloat(hash3D(xi + 1, yi, zi + 1, seed));
|
||||
float c011 = hashToFloat(hash3D(xi, yi + 1, zi + 1, seed));
|
||||
float c111 = hashToFloat(hash3D(xi + 1, yi + 1, zi + 1, seed));
|
||||
|
||||
// Trilinear interpolation
|
||||
float x00 = lerp(c000, c100, u);
|
||||
float x10 = lerp(c010, c110, u);
|
||||
float x01 = lerp(c001, c101, u);
|
||||
float x11 = lerp(c011, c111, u);
|
||||
|
||||
float y0 = lerp(x00, x10, v);
|
||||
float y1 = lerp(x01, x11, v);
|
||||
|
||||
return lerp(y0, y1, w);
|
||||
}
|
||||
}
|
||||
|
||||
void VoxelGrid::fillNoise(int x0, int y0, int z0, int x1, int y1, int z1,
|
||||
uint8_t material, float threshold, float scale, unsigned int seed) {
|
||||
// Ensure proper ordering
|
||||
if (x0 > x1) std::swap(x0, x1);
|
||||
if (y0 > y1) std::swap(y0, y1);
|
||||
if (z0 > z1) std::swap(z0, z1);
|
||||
|
||||
// Clamp to valid range
|
||||
x0 = std::max(0, std::min(x0, width_ - 1));
|
||||
x1 = std::max(0, std::min(x1, width_ - 1));
|
||||
y0 = std::max(0, std::min(y0, height_ - 1));
|
||||
y1 = std::max(0, std::min(y1, height_ - 1));
|
||||
z0 = std::max(0, std::min(z0, depth_ - 1));
|
||||
z1 = std::max(0, std::min(z1, depth_ - 1));
|
||||
|
||||
for (int z = z0; z <= z1; z++) {
|
||||
for (int y = y0; y <= y1; y++) {
|
||||
for (int x = x0; x <= x1; x++) {
|
||||
float n = noise3D(x * scale, y * scale, z * scale, seed);
|
||||
if (n > threshold) {
|
||||
data_[index(x, y, z)] = material;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
meshDirty_ = true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Copy/Paste Operations - Milestone 11
|
||||
// =============================================================================
|
||||
|
||||
VoxelRegion VoxelGrid::copyRegion(int x0, int y0, int z0, int x1, int y1, int z1) const {
|
||||
// Ensure proper ordering
|
||||
if (x0 > x1) std::swap(x0, x1);
|
||||
if (y0 > y1) std::swap(y0, y1);
|
||||
if (z0 > z1) std::swap(z0, z1);
|
||||
|
||||
// Clamp to valid range
|
||||
x0 = std::max(0, std::min(x0, width_ - 1));
|
||||
x1 = std::max(0, std::min(x1, width_ - 1));
|
||||
y0 = std::max(0, std::min(y0, height_ - 1));
|
||||
y1 = std::max(0, std::min(y1, height_ - 1));
|
||||
z0 = std::max(0, std::min(z0, depth_ - 1));
|
||||
z1 = std::max(0, std::min(z1, depth_ - 1));
|
||||
|
||||
int rw = x1 - x0 + 1;
|
||||
int rh = y1 - y0 + 1;
|
||||
int rd = z1 - z0 + 1;
|
||||
|
||||
VoxelRegion region(rw, rh, rd);
|
||||
|
||||
for (int rz = 0; rz < rd; rz++) {
|
||||
for (int ry = 0; ry < rh; ry++) {
|
||||
for (int rx = 0; rx < rw; rx++) {
|
||||
int sx = x0 + rx;
|
||||
int sy = y0 + ry;
|
||||
int sz = z0 + rz;
|
||||
size_t ri = static_cast<size_t>(rz) * (rw * rh) +
|
||||
static_cast<size_t>(ry) * rw + rx;
|
||||
region.data[ri] = get(sx, sy, sz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
void VoxelGrid::pasteRegion(const VoxelRegion& region, int x, int y, int z, bool skipAir) {
|
||||
if (!region.isValid()) return;
|
||||
|
||||
for (int rz = 0; rz < region.depth; rz++) {
|
||||
for (int ry = 0; ry < region.height; ry++) {
|
||||
for (int rx = 0; rx < region.width; rx++) {
|
||||
size_t ri = static_cast<size_t>(rz) * (region.width * region.height) +
|
||||
static_cast<size_t>(ry) * region.width + rx;
|
||||
uint8_t mat = region.data[ri];
|
||||
|
||||
if (skipAir && mat == 0) continue;
|
||||
|
||||
int dx = x + rx;
|
||||
int dy = y + ry;
|
||||
int dz = z + rz;
|
||||
|
||||
if (isValid(dx, dy, dz)) {
|
||||
data_[index(dx, dy, dz)] = mat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
meshDirty_ = true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Navigation Projection - Milestone 12
|
||||
// =============================================================================
|
||||
|
||||
VoxelGrid::NavInfo VoxelGrid::projectColumn(int x, int z, int headroom) const {
|
||||
NavInfo info;
|
||||
info.height = 0.0f;
|
||||
info.walkable = false;
|
||||
info.transparent = true;
|
||||
info.pathCost = 1.0f;
|
||||
|
||||
// Out of bounds check
|
||||
if (x < 0 || x >= width_ || z < 0 || z >= depth_) {
|
||||
return info;
|
||||
}
|
||||
|
||||
// Scan from top to bottom, find first solid with air above (floor)
|
||||
int floorY = -1;
|
||||
for (int y = height_ - 1; y >= 0; y--) {
|
||||
uint8_t mat = get(x, y, z);
|
||||
if (mat != 0) {
|
||||
// Found solid - check if it's a floor (air above) or ceiling
|
||||
bool hasAirAbove = (y == height_ - 1) || (get(x, y + 1, z) == 0);
|
||||
if (hasAirAbove) {
|
||||
floorY = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (floorY >= 0) {
|
||||
// Found a floor
|
||||
info.height = (floorY + 1) * cellSize_; // Top of floor voxel
|
||||
info.walkable = true;
|
||||
|
||||
// Check headroom (need enough air voxels above floor)
|
||||
int airCount = 0;
|
||||
for (int y = floorY + 1; y < height_; y++) {
|
||||
if (get(x, y, z) == 0) {
|
||||
airCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (airCount < headroom) {
|
||||
info.walkable = false; // Can't fit entity
|
||||
}
|
||||
|
||||
// Get path cost from floor material
|
||||
uint8_t floorMat = get(x, floorY, z);
|
||||
info.pathCost = getMaterial(floorMat).pathCost;
|
||||
}
|
||||
|
||||
// Check transparency: any non-transparent solid in column blocks FOV
|
||||
for (int y = 0; y < height_; y++) {
|
||||
uint8_t mat = get(x, y, z);
|
||||
if (mat != 0 && !getMaterial(mat).transparent) {
|
||||
info.transparent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Mesh Caching (Milestone 10)
|
||||
// =============================================================================
|
||||
|
|
@ -164,8 +459,337 @@ const std::vector<MeshVertex>& VoxelGrid::getVertices() const {
|
|||
|
||||
void VoxelGrid::rebuildMesh() const {
|
||||
cachedVertices_.clear();
|
||||
VoxelMesher::generateMesh(*this, cachedVertices_);
|
||||
if (greedyMeshing_) {
|
||||
VoxelMesher::generateGreedyMesh(*this, cachedVertices_);
|
||||
} else {
|
||||
VoxelMesher::generateMesh(*this, cachedVertices_);
|
||||
}
|
||||
meshDirty_ = false;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Serialization - Milestone 14
|
||||
// =============================================================================
|
||||
|
||||
// File format:
|
||||
// Magic "MCVG" (4 bytes)
|
||||
// Version (1 byte) - currently 1
|
||||
// Width, Height, Depth (3 x int32 = 12 bytes)
|
||||
// Cell Size (float32 = 4 bytes)
|
||||
// Material count (uint8 = 1 byte)
|
||||
// For each material:
|
||||
// Name length (uint16) + name bytes
|
||||
// Color RGBA (4 bytes)
|
||||
// Sprite index (int32)
|
||||
// Transparent (uint8)
|
||||
// Path cost (float32)
|
||||
// Voxel data length (uint32)
|
||||
// Voxel data: RLE encoded (run_length: uint8, material: uint8) pairs
|
||||
// If run_length == 255, read extended_length: uint16 for longer runs
|
||||
|
||||
namespace {
|
||||
const char MAGIC[4] = {'M', 'C', 'V', 'G'};
|
||||
const uint8_t FORMAT_VERSION = 1;
|
||||
|
||||
// Write helpers
|
||||
void writeU8(std::vector<uint8_t>& buf, uint8_t v) {
|
||||
buf.push_back(v);
|
||||
}
|
||||
|
||||
void writeU16(std::vector<uint8_t>& buf, uint16_t v) {
|
||||
buf.push_back(static_cast<uint8_t>(v & 0xFF));
|
||||
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
|
||||
}
|
||||
|
||||
void writeI32(std::vector<uint8_t>& buf, int32_t v) {
|
||||
buf.push_back(static_cast<uint8_t>(v & 0xFF));
|
||||
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
|
||||
buf.push_back(static_cast<uint8_t>((v >> 16) & 0xFF));
|
||||
buf.push_back(static_cast<uint8_t>((v >> 24) & 0xFF));
|
||||
}
|
||||
|
||||
void writeU32(std::vector<uint8_t>& buf, uint32_t v) {
|
||||
buf.push_back(static_cast<uint8_t>(v & 0xFF));
|
||||
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
|
||||
buf.push_back(static_cast<uint8_t>((v >> 16) & 0xFF));
|
||||
buf.push_back(static_cast<uint8_t>((v >> 24) & 0xFF));
|
||||
}
|
||||
|
||||
void writeF32(std::vector<uint8_t>& buf, float v) {
|
||||
static_assert(sizeof(float) == 4, "Expected 4-byte float");
|
||||
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&v);
|
||||
buf.insert(buf.end(), bytes, bytes + 4);
|
||||
}
|
||||
|
||||
void writeString(std::vector<uint8_t>& buf, const std::string& s) {
|
||||
uint16_t len = static_cast<uint16_t>(std::min(s.size(), size_t(65535)));
|
||||
writeU16(buf, len);
|
||||
buf.insert(buf.end(), s.begin(), s.begin() + len);
|
||||
}
|
||||
|
||||
// Read helpers
|
||||
class Reader {
|
||||
const uint8_t* data_;
|
||||
size_t size_;
|
||||
size_t pos_;
|
||||
public:
|
||||
Reader(const uint8_t* data, size_t size) : data_(data), size_(size), pos_(0) {}
|
||||
|
||||
bool hasBytes(size_t n) const { return pos_ + n <= size_; }
|
||||
size_t position() const { return pos_; }
|
||||
|
||||
bool readU8(uint8_t& v) {
|
||||
if (!hasBytes(1)) return false;
|
||||
v = data_[pos_++];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readU16(uint16_t& v) {
|
||||
if (!hasBytes(2)) return false;
|
||||
v = static_cast<uint16_t>(data_[pos_]) |
|
||||
(static_cast<uint16_t>(data_[pos_ + 1]) << 8);
|
||||
pos_ += 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readI32(int32_t& v) {
|
||||
if (!hasBytes(4)) return false;
|
||||
v = static_cast<int32_t>(data_[pos_]) |
|
||||
(static_cast<int32_t>(data_[pos_ + 1]) << 8) |
|
||||
(static_cast<int32_t>(data_[pos_ + 2]) << 16) |
|
||||
(static_cast<int32_t>(data_[pos_ + 3]) << 24);
|
||||
pos_ += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readU32(uint32_t& v) {
|
||||
if (!hasBytes(4)) return false;
|
||||
v = static_cast<uint32_t>(data_[pos_]) |
|
||||
(static_cast<uint32_t>(data_[pos_ + 1]) << 8) |
|
||||
(static_cast<uint32_t>(data_[pos_ + 2]) << 16) |
|
||||
(static_cast<uint32_t>(data_[pos_ + 3]) << 24);
|
||||
pos_ += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readF32(float& v) {
|
||||
if (!hasBytes(4)) return false;
|
||||
static_assert(sizeof(float) == 4, "Expected 4-byte float");
|
||||
std::memcpy(&v, data_ + pos_, 4);
|
||||
pos_ += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readString(std::string& s) {
|
||||
uint16_t len;
|
||||
if (!readU16(len)) return false;
|
||||
if (!hasBytes(len)) return false;
|
||||
s.assign(reinterpret_cast<const char*>(data_ + pos_), len);
|
||||
pos_ += len;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readBytes(uint8_t* out, size_t n) {
|
||||
if (!hasBytes(n)) return false;
|
||||
std::memcpy(out, data_ + pos_, n);
|
||||
pos_ += n;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// RLE encode voxel data
|
||||
void rleEncode(const std::vector<uint8_t>& data, std::vector<uint8_t>& out) {
|
||||
if (data.empty()) return;
|
||||
|
||||
size_t i = 0;
|
||||
while (i < data.size()) {
|
||||
uint8_t mat = data[i];
|
||||
size_t runStart = i;
|
||||
|
||||
// Count consecutive same materials
|
||||
while (i < data.size() && data[i] == mat && (i - runStart) < 65535 + 255) {
|
||||
i++;
|
||||
}
|
||||
|
||||
size_t runLen = i - runStart;
|
||||
|
||||
if (runLen < 255) {
|
||||
writeU8(out, static_cast<uint8_t>(runLen));
|
||||
} else {
|
||||
// Extended run: 255 marker + uint16 length
|
||||
writeU8(out, 255);
|
||||
writeU16(out, static_cast<uint16_t>(runLen - 255));
|
||||
}
|
||||
writeU8(out, mat);
|
||||
}
|
||||
}
|
||||
|
||||
// RLE decode voxel data
|
||||
bool rleDecode(Reader& reader, std::vector<uint8_t>& data, size_t expectedSize) {
|
||||
data.clear();
|
||||
data.reserve(expectedSize);
|
||||
|
||||
while (data.size() < expectedSize) {
|
||||
uint8_t runLen8;
|
||||
if (!reader.readU8(runLen8)) return false;
|
||||
|
||||
size_t runLen = runLen8;
|
||||
if (runLen8 == 255) {
|
||||
uint16_t extLen;
|
||||
if (!reader.readU16(extLen)) return false;
|
||||
runLen = 255 + extLen;
|
||||
}
|
||||
|
||||
uint8_t mat;
|
||||
if (!reader.readU8(mat)) return false;
|
||||
|
||||
for (size_t j = 0; j < runLen && data.size() < expectedSize; j++) {
|
||||
data.push_back(mat);
|
||||
}
|
||||
}
|
||||
|
||||
return data.size() == expectedSize;
|
||||
}
|
||||
}
|
||||
|
||||
bool VoxelGrid::saveToBuffer(std::vector<uint8_t>& buffer) const {
|
||||
buffer.clear();
|
||||
buffer.reserve(1024 + data_.size()); // Rough estimate
|
||||
|
||||
// Magic
|
||||
buffer.insert(buffer.end(), MAGIC, MAGIC + 4);
|
||||
|
||||
// Version
|
||||
writeU8(buffer, FORMAT_VERSION);
|
||||
|
||||
// Dimensions
|
||||
writeI32(buffer, width_);
|
||||
writeI32(buffer, height_);
|
||||
writeI32(buffer, depth_);
|
||||
|
||||
// Cell size
|
||||
writeF32(buffer, cellSize_);
|
||||
|
||||
// Materials
|
||||
writeU8(buffer, static_cast<uint8_t>(materials_.size()));
|
||||
for (const auto& mat : materials_) {
|
||||
writeString(buffer, mat.name);
|
||||
writeU8(buffer, mat.color.r);
|
||||
writeU8(buffer, mat.color.g);
|
||||
writeU8(buffer, mat.color.b);
|
||||
writeU8(buffer, mat.color.a);
|
||||
writeI32(buffer, mat.spriteIndex);
|
||||
writeU8(buffer, mat.transparent ? 1 : 0);
|
||||
writeF32(buffer, mat.pathCost);
|
||||
}
|
||||
|
||||
// RLE encode voxel data
|
||||
std::vector<uint8_t> rleData;
|
||||
rleEncode(data_, rleData);
|
||||
|
||||
// Write RLE data length and data
|
||||
writeU32(buffer, static_cast<uint32_t>(rleData.size()));
|
||||
buffer.insert(buffer.end(), rleData.begin(), rleData.end());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VoxelGrid::loadFromBuffer(const uint8_t* data, size_t size) {
|
||||
Reader reader(data, size);
|
||||
|
||||
// Check magic
|
||||
uint8_t magic[4];
|
||||
if (!reader.readBytes(magic, 4)) return false;
|
||||
if (std::memcmp(magic, MAGIC, 4) != 0) return false;
|
||||
|
||||
// Check version
|
||||
uint8_t version;
|
||||
if (!reader.readU8(version)) return false;
|
||||
if (version != FORMAT_VERSION) return false;
|
||||
|
||||
// Read dimensions
|
||||
int32_t w, h, d;
|
||||
if (!reader.readI32(w) || !reader.readI32(h) || !reader.readI32(d)) return false;
|
||||
if (w <= 0 || h <= 0 || d <= 0) return false;
|
||||
|
||||
// Read cell size
|
||||
float cs;
|
||||
if (!reader.readF32(cs)) return false;
|
||||
if (cs <= 0.0f) return false;
|
||||
|
||||
// Read materials
|
||||
uint8_t matCount;
|
||||
if (!reader.readU8(matCount)) return false;
|
||||
|
||||
std::vector<VoxelMaterial> newMaterials;
|
||||
newMaterials.reserve(matCount);
|
||||
for (uint8_t i = 0; i < matCount; i++) {
|
||||
VoxelMaterial mat;
|
||||
if (!reader.readString(mat.name)) return false;
|
||||
|
||||
uint8_t r, g, b, a;
|
||||
if (!reader.readU8(r) || !reader.readU8(g) || !reader.readU8(b) || !reader.readU8(a))
|
||||
return false;
|
||||
mat.color = sf::Color(r, g, b, a);
|
||||
|
||||
int32_t sprite;
|
||||
if (!reader.readI32(sprite)) return false;
|
||||
mat.spriteIndex = sprite;
|
||||
|
||||
uint8_t transp;
|
||||
if (!reader.readU8(transp)) return false;
|
||||
mat.transparent = (transp != 0);
|
||||
|
||||
if (!reader.readF32(mat.pathCost)) return false;
|
||||
|
||||
newMaterials.push_back(mat);
|
||||
}
|
||||
|
||||
// Read RLE data length
|
||||
uint32_t rleLen;
|
||||
if (!reader.readU32(rleLen)) return false;
|
||||
|
||||
// Decode voxel data
|
||||
size_t expectedVoxels = static_cast<size_t>(w) * h * d;
|
||||
std::vector<uint8_t> newData;
|
||||
if (!rleDecode(reader, newData, expectedVoxels)) return false;
|
||||
|
||||
// Success - update the grid
|
||||
width_ = w;
|
||||
height_ = h;
|
||||
depth_ = d;
|
||||
cellSize_ = cs;
|
||||
materials_ = std::move(newMaterials);
|
||||
data_ = std::move(newData);
|
||||
meshDirty_ = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VoxelGrid::save(const std::string& path) const {
|
||||
std::vector<uint8_t> buffer;
|
||||
if (!saveToBuffer(buffer)) return false;
|
||||
|
||||
std::ofstream file(path, std::ios::binary);
|
||||
if (!file) return false;
|
||||
|
||||
file.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
|
||||
return file.good();
|
||||
}
|
||||
|
||||
bool VoxelGrid::load(const std::string& path) {
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
if (!file) return false;
|
||||
|
||||
std::streamsize size = file.tellg();
|
||||
if (size <= 0) return false;
|
||||
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
std::vector<uint8_t> buffer(static_cast<size_t>(size));
|
||||
if (!file.read(reinterpret_cast<char*>(buffer.data()), size)) return false;
|
||||
|
||||
return loadFromBuffer(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
} // namespace mcrf
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// VoxelGrid.h - Dense 3D voxel array with material palette
|
||||
// Part of McRogueFace 3D Extension - Milestones 9-10
|
||||
// Part of McRogueFace 3D Extension - Milestones 9-11
|
||||
#pragma once
|
||||
|
||||
#include "../Common.h"
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
#include <vector>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mcrf {
|
||||
|
||||
|
|
@ -28,6 +29,22 @@ struct VoxelMaterial {
|
|||
: name(n), color(c), spriteIndex(sprite), transparent(transp), pathCost(cost) {}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// VoxelRegion - Portable voxel data for copy/paste operations (Milestone 11)
|
||||
// =============================================================================
|
||||
|
||||
struct VoxelRegion {
|
||||
int width, height, depth;
|
||||
std::vector<uint8_t> data;
|
||||
|
||||
VoxelRegion() : width(0), height(0), depth(0) {}
|
||||
VoxelRegion(int w, int h, int d) : width(w), height(h), depth(d),
|
||||
data(static_cast<size_t>(w) * h * d, 0) {}
|
||||
|
||||
bool isValid() const { return width > 0 && height > 0 && depth > 0; }
|
||||
size_t totalVoxels() const { return static_cast<size_t>(width) * height * depth; }
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// VoxelGrid - Dense 3D array of material IDs
|
||||
// =============================================================================
|
||||
|
|
@ -43,9 +60,10 @@ private:
|
|||
vec3 offset_;
|
||||
float rotation_ = 0.0f; // Y-axis only, degrees
|
||||
|
||||
// Mesh caching (Milestone 10)
|
||||
// Mesh caching (Milestones 10, 13)
|
||||
mutable bool meshDirty_ = true;
|
||||
mutable std::vector<MeshVertex> cachedVertices_;
|
||||
bool greedyMeshing_ = false; // Use greedy meshing algorithm
|
||||
|
||||
// Index calculation (row-major: X varies fastest, then Y, then Z)
|
||||
inline size_t index(int x, int y, int z) const {
|
||||
|
|
@ -79,11 +97,39 @@ public:
|
|||
const VoxelMaterial& getMaterial(uint8_t id) const;
|
||||
size_t materialCount() const { return materials_.size(); }
|
||||
|
||||
// Bulk operations
|
||||
// Bulk operations - Basic
|
||||
void fill(uint8_t material);
|
||||
void clear() { fill(0); }
|
||||
void fillBox(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t material);
|
||||
|
||||
// Bulk operations - Milestone 11
|
||||
void fillBoxHollow(int x0, int y0, int z0, int x1, int y1, int z1,
|
||||
uint8_t material, int thickness = 1);
|
||||
void fillSphere(int cx, int cy, int cz, int radius, uint8_t material);
|
||||
void fillCylinder(int cx, int cy, int cz, int radius, int height, uint8_t material);
|
||||
void fillNoise(int x0, int y0, int z0, int x1, int y1, int z1,
|
||||
uint8_t material, float threshold = 0.5f,
|
||||
float scale = 0.1f, unsigned int seed = 0);
|
||||
|
||||
// Copy/paste operations - Milestone 11
|
||||
VoxelRegion copyRegion(int x0, int y0, int z0, int x1, int y1, int z1) const;
|
||||
void pasteRegion(const VoxelRegion& region, int x, int y, int z, bool skipAir = true);
|
||||
|
||||
// Navigation projection - Milestone 12
|
||||
struct NavInfo {
|
||||
float height = 0.0f;
|
||||
bool walkable = false;
|
||||
bool transparent = true;
|
||||
float pathCost = 1.0f;
|
||||
};
|
||||
|
||||
/// Project a single column to get navigation info
|
||||
/// @param x X coordinate in voxel grid
|
||||
/// @param z Z coordinate in voxel grid
|
||||
/// @param headroom Required air voxels above floor (default 2)
|
||||
/// @return Navigation info for this column
|
||||
NavInfo projectColumn(int x, int z, int headroom = 2) const;
|
||||
|
||||
// Transform
|
||||
void setOffset(const vec3& offset) { offset_ = offset; }
|
||||
void setOffset(float x, float y, float z) { offset_ = vec3(x, y, z); }
|
||||
|
|
@ -96,7 +142,7 @@ public:
|
|||
size_t countNonAir() const;
|
||||
size_t countMaterial(uint8_t material) const;
|
||||
|
||||
// Mesh caching (Milestone 10)
|
||||
// Mesh caching (Milestones 10, 13)
|
||||
/// Mark mesh as needing rebuild (called automatically by set/fill operations)
|
||||
void markDirty() { meshDirty_ = true; }
|
||||
|
||||
|
|
@ -112,10 +158,37 @@ public:
|
|||
/// Get vertex count after mesh generation
|
||||
size_t vertexCount() const { return cachedVertices_.size(); }
|
||||
|
||||
/// Enable/disable greedy meshing (Milestone 13)
|
||||
/// Greedy meshing merges coplanar faces to reduce vertex count
|
||||
void setGreedyMeshing(bool enabled) { greedyMeshing_ = enabled; markDirty(); }
|
||||
bool isGreedyMeshingEnabled() const { return greedyMeshing_; }
|
||||
|
||||
// Memory info (for debugging)
|
||||
size_t memoryUsageBytes() const {
|
||||
return data_.size() + materials_.size() * sizeof(VoxelMaterial);
|
||||
}
|
||||
|
||||
// Serialization (Milestone 14)
|
||||
/// Save voxel grid to binary file
|
||||
/// @param path File path to save to
|
||||
/// @return true on success
|
||||
bool save(const std::string& path) const;
|
||||
|
||||
/// Load voxel grid from binary file
|
||||
/// @param path File path to load from
|
||||
/// @return true on success
|
||||
bool load(const std::string& path);
|
||||
|
||||
/// Save to memory buffer
|
||||
/// @param buffer Output buffer (resized as needed)
|
||||
/// @return true on success
|
||||
bool saveToBuffer(std::vector<uint8_t>& buffer) const;
|
||||
|
||||
/// Load from memory buffer
|
||||
/// @param data Buffer to load from
|
||||
/// @param size Buffer size
|
||||
/// @return true on success
|
||||
bool loadFromBuffer(const uint8_t* data, size_t size);
|
||||
};
|
||||
|
||||
} // namespace mcrf
|
||||
|
|
|
|||
|
|
@ -70,6 +70,187 @@ bool VoxelMesher::shouldGenerateFace(const VoxelGrid& grid,
|
|||
return grid.getMaterial(neighbor).transparent;
|
||||
}
|
||||
|
||||
void VoxelMesher::emitQuad(std::vector<MeshVertex>& vertices,
|
||||
const vec3& corner,
|
||||
const vec3& uAxis,
|
||||
const vec3& vAxis,
|
||||
const vec3& normal,
|
||||
const VoxelMaterial& material) {
|
||||
// 4 corners of the quad
|
||||
vec3 corners[4] = {
|
||||
corner, // 0: origin
|
||||
corner + uAxis, // 1: +U
|
||||
corner + uAxis + vAxis, // 2: +U+V
|
||||
corner + vAxis // 3: +V
|
||||
};
|
||||
|
||||
// Calculate UV based on quad size (for potential texture tiling)
|
||||
float uLen = uAxis.length();
|
||||
float vLen = vAxis.length();
|
||||
vec2 uvs[4] = {
|
||||
vec2(0, 0), // 0
|
||||
vec2(uLen, 0), // 1
|
||||
vec2(uLen, vLen), // 2
|
||||
vec2(0, vLen) // 3
|
||||
};
|
||||
|
||||
// Color from material
|
||||
vec4 color(
|
||||
material.color.r / 255.0f,
|
||||
material.color.g / 255.0f,
|
||||
material.color.b / 255.0f,
|
||||
material.color.a / 255.0f
|
||||
);
|
||||
|
||||
// Emit 2 triangles (6 vertices) - CCW winding
|
||||
// Triangle 1: 0-2-1
|
||||
vertices.push_back(MeshVertex(corners[0], uvs[0], normal, color));
|
||||
vertices.push_back(MeshVertex(corners[2], uvs[2], normal, color));
|
||||
vertices.push_back(MeshVertex(corners[1], uvs[1], normal, color));
|
||||
|
||||
// Triangle 2: 0-3-2
|
||||
vertices.push_back(MeshVertex(corners[0], uvs[0], normal, color));
|
||||
vertices.push_back(MeshVertex(corners[3], uvs[3], normal, color));
|
||||
vertices.push_back(MeshVertex(corners[2], uvs[2], normal, color));
|
||||
}
|
||||
|
||||
void VoxelMesher::generateGreedyMesh(const VoxelGrid& grid, std::vector<MeshVertex>& outVertices) {
|
||||
const float cs = grid.cellSize();
|
||||
const int width = grid.width();
|
||||
const int height = grid.height();
|
||||
const int depth = grid.depth();
|
||||
|
||||
// Process each face direction
|
||||
// Axis 0 = X, 1 = Y, 2 = Z
|
||||
// Direction: +1 = positive, -1 = negative
|
||||
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
for (int dir = -1; dir <= 1; dir += 2) {
|
||||
// Determine slice dimensions based on axis
|
||||
int sliceW, sliceH, sliceCount;
|
||||
if (axis == 0) { // X-axis: slices in YZ plane
|
||||
sliceW = depth;
|
||||
sliceH = height;
|
||||
sliceCount = width;
|
||||
} else if (axis == 1) { // Y-axis: slices in XZ plane
|
||||
sliceW = width;
|
||||
sliceH = depth;
|
||||
sliceCount = height;
|
||||
} else { // Z-axis: slices in XY plane
|
||||
sliceW = width;
|
||||
sliceH = height;
|
||||
sliceCount = depth;
|
||||
}
|
||||
|
||||
// Create mask for this slice
|
||||
std::vector<uint8_t> mask(sliceW * sliceH);
|
||||
|
||||
// Process each slice
|
||||
for (int sliceIdx = 0; sliceIdx < sliceCount; sliceIdx++) {
|
||||
// Fill mask with material IDs where faces should be generated
|
||||
std::fill(mask.begin(), mask.end(), 0);
|
||||
|
||||
for (int v = 0; v < sliceH; v++) {
|
||||
for (int u = 0; u < sliceW; u++) {
|
||||
// Map (u, v, sliceIdx) to (x, y, z) based on axis
|
||||
int x, y, z, nx, ny, nz;
|
||||
if (axis == 0) {
|
||||
x = sliceIdx; y = v; z = u;
|
||||
nx = x + dir; ny = y; nz = z;
|
||||
} else if (axis == 1) {
|
||||
x = u; y = sliceIdx; z = v;
|
||||
nx = x; ny = y + dir; nz = z;
|
||||
} else {
|
||||
x = u; y = v; z = sliceIdx;
|
||||
nx = x; ny = y; nz = z + dir;
|
||||
}
|
||||
|
||||
uint8_t mat = grid.get(x, y, z);
|
||||
if (mat == 0) continue;
|
||||
|
||||
// Check if face should be generated
|
||||
if (shouldGenerateFace(grid, x, y, z, nx, ny, nz)) {
|
||||
mask[v * sliceW + u] = mat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Greedy rectangle merging
|
||||
for (int v = 0; v < sliceH; v++) {
|
||||
for (int u = 0; u < sliceW; ) {
|
||||
uint8_t mat = mask[v * sliceW + u];
|
||||
if (mat == 0) {
|
||||
u++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find width of rectangle (extend along U)
|
||||
int rectW = 1;
|
||||
while (u + rectW < sliceW && mask[v * sliceW + u + rectW] == mat) {
|
||||
rectW++;
|
||||
}
|
||||
|
||||
// Find height of rectangle (extend along V)
|
||||
int rectH = 1;
|
||||
bool canExtend = true;
|
||||
while (canExtend && v + rectH < sliceH) {
|
||||
// Check if entire row matches
|
||||
for (int i = 0; i < rectW; i++) {
|
||||
if (mask[(v + rectH) * sliceW + u + i] != mat) {
|
||||
canExtend = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (canExtend) rectH++;
|
||||
}
|
||||
|
||||
// Clear mask for merged area
|
||||
for (int dv = 0; dv < rectH; dv++) {
|
||||
for (int du = 0; du < rectW; du++) {
|
||||
mask[(v + dv) * sliceW + u + du] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Emit quad for this merged rectangle
|
||||
const VoxelMaterial& material = grid.getMaterial(mat);
|
||||
|
||||
// Calculate corner and axes based on face direction
|
||||
vec3 corner, uAxis, vAxis, normal;
|
||||
|
||||
if (axis == 0) { // X-facing
|
||||
float faceX = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs;
|
||||
corner = vec3(faceX, v * cs, u * cs);
|
||||
uAxis = vec3(0, 0, rectW * cs);
|
||||
vAxis = vec3(0, rectH * cs, 0);
|
||||
normal = vec3(static_cast<float>(dir), 0, 0);
|
||||
// Flip winding for back faces
|
||||
if (dir < 0) std::swap(uAxis, vAxis);
|
||||
} else if (axis == 1) { // Y-facing
|
||||
float faceY = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs;
|
||||
corner = vec3(u * cs, faceY, v * cs);
|
||||
uAxis = vec3(rectW * cs, 0, 0);
|
||||
vAxis = vec3(0, 0, rectH * cs);
|
||||
normal = vec3(0, static_cast<float>(dir), 0);
|
||||
if (dir < 0) std::swap(uAxis, vAxis);
|
||||
} else { // Z-facing
|
||||
float faceZ = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs;
|
||||
corner = vec3(u * cs, v * cs, faceZ);
|
||||
uAxis = vec3(rectW * cs, 0, 0);
|
||||
vAxis = vec3(0, rectH * cs, 0);
|
||||
normal = vec3(0, 0, static_cast<float>(dir));
|
||||
if (dir < 0) std::swap(uAxis, vAxis);
|
||||
}
|
||||
|
||||
emitQuad(outVertices, corner, uAxis, vAxis, normal, material);
|
||||
|
||||
u += rectW;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VoxelMesher::emitFace(std::vector<MeshVertex>& vertices,
|
||||
const vec3& center,
|
||||
const vec3& normal,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// VoxelMesher.h - Face-culled mesh generation for VoxelGrid
|
||||
// Part of McRogueFace 3D Extension - Milestone 10
|
||||
// Part of McRogueFace 3D Extension - Milestones 10, 13
|
||||
#pragma once
|
||||
|
||||
#include "VoxelGrid.h"
|
||||
|
|
@ -14,7 +14,7 @@ namespace mcrf {
|
|||
|
||||
class VoxelMesher {
|
||||
public:
|
||||
/// Generate face-culled mesh from voxel data
|
||||
/// Generate face-culled mesh from voxel data (simple per-voxel faces)
|
||||
/// Output vertices in local space (model matrix applies world transform)
|
||||
/// @param grid The VoxelGrid to generate mesh from
|
||||
/// @param outVertices Output vector of vertices (appended to, not cleared)
|
||||
|
|
@ -23,6 +23,16 @@ public:
|
|||
std::vector<MeshVertex>& outVertices
|
||||
);
|
||||
|
||||
/// Generate mesh using greedy meshing algorithm (Milestone 13)
|
||||
/// Merges coplanar faces of the same material into larger rectangles,
|
||||
/// significantly reducing vertex count for uniform regions.
|
||||
/// @param grid The VoxelGrid to generate mesh from
|
||||
/// @param outVertices Output vector of vertices (appended to, not cleared)
|
||||
static void generateGreedyMesh(
|
||||
const VoxelGrid& grid,
|
||||
std::vector<MeshVertex>& outVertices
|
||||
);
|
||||
|
||||
private:
|
||||
/// Check if face should be generated (neighbor is air or transparent)
|
||||
/// @param grid The VoxelGrid
|
||||
|
|
@ -48,6 +58,23 @@ private:
|
|||
float size,
|
||||
const VoxelMaterial& material
|
||||
);
|
||||
|
||||
/// Generate a rectangular face (2 triangles = 6 vertices)
|
||||
/// Used by greedy meshing to emit merged quads
|
||||
/// @param vertices Output vector to append vertices to
|
||||
/// @param corner Base corner of the rectangle
|
||||
/// @param uAxis Direction and length along U axis
|
||||
/// @param vAxis Direction and length along V axis
|
||||
/// @param normal Face normal direction
|
||||
/// @param material Material for coloring
|
||||
static void emitQuad(
|
||||
std::vector<MeshVertex>& vertices,
|
||||
const vec3& corner,
|
||||
const vec3& uAxis,
|
||||
const vec3& vAxis,
|
||||
const vec3& normal,
|
||||
const VoxelMaterial& material
|
||||
);
|
||||
};
|
||||
|
||||
} // namespace mcrf
|
||||
|
|
|
|||
|
|
@ -444,6 +444,7 @@ PyObject* PyInit_mcrfpy()
|
|||
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
||||
&mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType,
|
||||
&mcrfpydef::PyBillboardType, &mcrfpydef::PyVoxelGridType,
|
||||
&mcrfpydef::PyVoxelRegionType,
|
||||
|
||||
/*grid layers (#147)*/
|
||||
&PyColorLayerType, &PyTileLayerType,
|
||||
|
|
@ -544,6 +545,9 @@ PyObject* PyInit_mcrfpy()
|
|||
mcrfpydef::PyVoxelGridType.tp_methods = PyVoxelGrid::methods;
|
||||
mcrfpydef::PyVoxelGridType.tp_getset = PyVoxelGrid::getsetters;
|
||||
|
||||
// Set up PyVoxelRegionType getsetters (Milestone 11)
|
||||
mcrfpydef::PyVoxelRegionType.tp_getset = PyVoxelRegion::getsetters;
|
||||
|
||||
// Set up PyShaderType methods and getsetters (#106)
|
||||
mcrfpydef::PyShaderType.tp_methods = PyShader::methods;
|
||||
mcrfpydef::PyShaderType.tp_getset = PyShader::getsetters;
|
||||
|
|
|
|||
|
|
@ -68,8 +68,68 @@ PyObject* PyColor::pyObject()
|
|||
|
||||
sf::Color PyColor::fromPy(PyObject* obj)
|
||||
{
|
||||
PyColorObject* self = (PyColorObject*)obj;
|
||||
return self->data;
|
||||
// Handle None or NULL
|
||||
if (!obj || obj == Py_None) {
|
||||
return sf::Color::White;
|
||||
}
|
||||
|
||||
// Check if it's already a Color object
|
||||
PyTypeObject* color_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
|
||||
if (color_type) {
|
||||
bool is_color = PyObject_TypeCheck(obj, color_type);
|
||||
Py_DECREF(color_type);
|
||||
if (is_color) {
|
||||
PyColorObject* self = (PyColorObject*)obj;
|
||||
return self->data;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tuple or list input
|
||||
if (PyTuple_Check(obj) || PyList_Check(obj)) {
|
||||
Py_ssize_t size = PySequence_Size(obj);
|
||||
if (size < 3 || size > 4) {
|
||||
PyErr_SetString(PyExc_TypeError, "Color tuple/list must have 3 or 4 elements (r, g, b[, a])");
|
||||
return sf::Color::White;
|
||||
}
|
||||
|
||||
int r = 255, g = 255, b = 255, a = 255;
|
||||
|
||||
PyObject* item0 = PySequence_GetItem(obj, 0);
|
||||
PyObject* item1 = PySequence_GetItem(obj, 1);
|
||||
PyObject* item2 = PySequence_GetItem(obj, 2);
|
||||
|
||||
if (PyLong_Check(item0)) r = (int)PyLong_AsLong(item0);
|
||||
if (PyLong_Check(item1)) g = (int)PyLong_AsLong(item1);
|
||||
if (PyLong_Check(item2)) b = (int)PyLong_AsLong(item2);
|
||||
|
||||
Py_DECREF(item0);
|
||||
Py_DECREF(item1);
|
||||
Py_DECREF(item2);
|
||||
|
||||
if (size == 4) {
|
||||
PyObject* item3 = PySequence_GetItem(obj, 3);
|
||||
if (PyLong_Check(item3)) a = (int)PyLong_AsLong(item3);
|
||||
Py_DECREF(item3);
|
||||
}
|
||||
|
||||
// Clamp values
|
||||
r = std::max(0, std::min(255, r));
|
||||
g = std::max(0, std::min(255, g));
|
||||
b = std::max(0, std::min(255, b));
|
||||
a = std::max(0, std::min(255, a));
|
||||
|
||||
return sf::Color(r, g, b, a);
|
||||
}
|
||||
|
||||
// Handle integer (grayscale)
|
||||
if (PyLong_Check(obj)) {
|
||||
int v = std::max(0, std::min(255, (int)PyLong_AsLong(obj)));
|
||||
return sf::Color(v, v, v, 255);
|
||||
}
|
||||
|
||||
// Unknown type - set error and return white
|
||||
PyErr_SetString(PyExc_TypeError, "Color must be a Color object, tuple, list, or integer");
|
||||
return sf::Color::White;
|
||||
}
|
||||
|
||||
sf::Color PyColor::fromPy(PyColorObject* self)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue