voxel example
This commit is contained in:
parent
7ebca63db3
commit
3e6b6a5847
10 changed files with 1691 additions and 2 deletions
598
src/3d/PyVoxelGrid.cpp
Normal file
598
src/3d/PyVoxelGrid.cpp
Normal file
|
|
@ -0,0 +1,598 @@
|
||||||
|
// PyVoxelGrid.cpp - Python bindings for VoxelGrid implementation
|
||||||
|
// Part of McRogueFace 3D Extension - Milestone 9
|
||||||
|
|
||||||
|
#include "PyVoxelGrid.h"
|
||||||
|
#include "../McRFPy_API.h"
|
||||||
|
#include "../PyColor.h"
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Python type interface
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
|
||||||
|
PyVoxelGridObject* self = (PyVoxelGridObject*)type->tp_alloc(type, 0);
|
||||||
|
if (self) {
|
||||||
|
self->data = nullptr; // Will be initialized in init
|
||||||
|
self->weakreflist = nullptr;
|
||||||
|
}
|
||||||
|
return (PyObject*)self;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PyVoxelGrid::init(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"size", "cell_size", nullptr};
|
||||||
|
|
||||||
|
PyObject* size_obj = nullptr;
|
||||||
|
float cell_size = 1.0f;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(kwlist),
|
||||||
|
&size_obj, &cell_size)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse size tuple
|
||||||
|
if (!PyTuple_Check(size_obj) && !PyList_Check(size_obj)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "size must be a tuple or list of 3 integers");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PySequence_Size(size_obj) != 3) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "size must have exactly 3 elements (width, height, depth)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = 0, height = 0, depth = 0;
|
||||||
|
|
||||||
|
PyObject* w_obj = PySequence_GetItem(size_obj, 0);
|
||||||
|
PyObject* h_obj = PySequence_GetItem(size_obj, 1);
|
||||||
|
PyObject* d_obj = PySequence_GetItem(size_obj, 2);
|
||||||
|
|
||||||
|
bool valid = true;
|
||||||
|
if (PyLong_Check(w_obj)) width = (int)PyLong_AsLong(w_obj); else valid = false;
|
||||||
|
if (PyLong_Check(h_obj)) height = (int)PyLong_AsLong(h_obj); else valid = false;
|
||||||
|
if (PyLong_Check(d_obj)) depth = (int)PyLong_AsLong(d_obj); else valid = false;
|
||||||
|
|
||||||
|
Py_DECREF(w_obj);
|
||||||
|
Py_DECREF(h_obj);
|
||||||
|
Py_DECREF(d_obj);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "size elements must be integers");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width <= 0 || height <= 0 || depth <= 0) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "size dimensions must be positive");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cell_size <= 0.0f) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "cell_size must be positive");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the VoxelGrid
|
||||||
|
try {
|
||||||
|
self->data = std::make_shared<mcrf::VoxelGrid>(width, height, depth, cell_size);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, e.what());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PyVoxelGrid::dealloc(PyVoxelGridObject* self) {
|
||||||
|
PyObject_GC_UnTrack(self);
|
||||||
|
if (self->weakreflist != nullptr) {
|
||||||
|
PyObject_ClearWeakRefs((PyObject*)self);
|
||||||
|
}
|
||||||
|
self->data.reset();
|
||||||
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::repr(PyObject* obj) {
|
||||||
|
PyVoxelGridObject* self = (PyVoxelGridObject*)obj;
|
||||||
|
if (!self->data) {
|
||||||
|
return PyUnicode_FromString("<VoxelGrid (uninitialized)>");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "<VoxelGrid " << self->data->width() << "x"
|
||||||
|
<< self->data->height() << "x" << self->data->depth()
|
||||||
|
<< " cells=" << self->data->totalVoxels()
|
||||||
|
<< " materials=" << self->data->materialCount()
|
||||||
|
<< " non_air=" << self->data->countNonAir() << ">";
|
||||||
|
return PyUnicode_FromString(oss.str().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Properties - dimensions (read-only)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::get_size(PyVoxelGridObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return Py_BuildValue("(iii)", self->data->width(), self->data->height(), self->data->depth());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::get_width(PyVoxelGridObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return PyLong_FromLong(self->data->width());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::get_height(PyVoxelGridObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return PyLong_FromLong(self->data->height());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::get_depth(PyVoxelGridObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return PyLong_FromLong(self->data->depth());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::get_cell_size(PyVoxelGridObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return PyFloat_FromDouble(self->data->cellSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::get_material_count(PyVoxelGridObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return PyLong_FromSize_t(self->data->materialCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Properties - transform (read-write)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::get_offset(PyVoxelGridObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
mcrf::vec3 offset = self->data->getOffset();
|
||||||
|
return Py_BuildValue("(fff)", offset.x, offset.y, offset.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
int PyVoxelGrid::set_offset(PyVoxelGridObject* self, PyObject* value, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PyTuple_Check(value) && !PyList_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "offset must be a tuple or list of 3 floats");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PySequence_Size(value) != 3) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "offset must have exactly 3 elements (x, y, z)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
float x = 0, y = 0, z = 0;
|
||||||
|
PyObject* x_obj = PySequence_GetItem(value, 0);
|
||||||
|
PyObject* y_obj = PySequence_GetItem(value, 1);
|
||||||
|
PyObject* z_obj = PySequence_GetItem(value, 2);
|
||||||
|
|
||||||
|
bool valid = true;
|
||||||
|
if (PyNumber_Check(x_obj)) x = (float)PyFloat_AsDouble(PyNumber_Float(x_obj)); else valid = false;
|
||||||
|
if (PyNumber_Check(y_obj)) y = (float)PyFloat_AsDouble(PyNumber_Float(y_obj)); else valid = false;
|
||||||
|
if (PyNumber_Check(z_obj)) z = (float)PyFloat_AsDouble(PyNumber_Float(z_obj)); else valid = false;
|
||||||
|
|
||||||
|
Py_DECREF(x_obj);
|
||||||
|
Py_DECREF(y_obj);
|
||||||
|
Py_DECREF(z_obj);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "offset elements must be numbers");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->setOffset(x, y, z);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::get_rotation(PyVoxelGridObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return PyFloat_FromDouble(self->data->getRotation());
|
||||||
|
}
|
||||||
|
|
||||||
|
int PyVoxelGrid::set_rotation(PyVoxelGridObject* self, PyObject* value, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PyNumber_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "rotation must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
float rotation = (float)PyFloat_AsDouble(PyNumber_Float(value));
|
||||||
|
self->data->setRotation(rotation);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Voxel access methods
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::get(PyVoxelGridObject* self, PyObject* args) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x, y, z;
|
||||||
|
if (!PyArg_ParseTuple(args, "iii", &x, &y, &z)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds check with warning (returns 0 for out-of-bounds, like C++ API)
|
||||||
|
if (!self->data->isValid(x, y, z)) {
|
||||||
|
// Return 0 (air) for out-of-bounds, matching C++ behavior
|
||||||
|
return PyLong_FromLong(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PyLong_FromLong(self->data->get(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::set(PyVoxelGridObject* self, PyObject* args) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x, y, z, material;
|
||||||
|
if (!PyArg_ParseTuple(args, "iiii", &x, &y, &z, &material)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (material < 0 || material > 255) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "material must be 0-255");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds check - silently ignore out-of-bounds, like C++ API
|
||||||
|
self->data->set(x, y, z, static_cast<uint8_t>(material));
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Material methods
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::add_material(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* kwlist[] = {"name", "color", "sprite_index", "transparent", "path_cost", nullptr};
|
||||||
|
|
||||||
|
const char* name = nullptr;
|
||||||
|
PyObject* color_obj = nullptr;
|
||||||
|
int sprite_index = -1;
|
||||||
|
int transparent = 0; // Python bool maps to int
|
||||||
|
float path_cost = 1.0f;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|Oipf", const_cast<char**>(kwlist),
|
||||||
|
&name, &color_obj, &sprite_index, &transparent, &path_cost)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default color is white
|
||||||
|
sf::Color color = sf::Color::White;
|
||||||
|
|
||||||
|
// Parse color if provided
|
||||||
|
if (color_obj && color_obj != Py_None) {
|
||||||
|
color = PyColor::fromPy(color_obj);
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
uint8_t id = self->data->addMaterial(name, color, sprite_index, transparent != 0, path_cost);
|
||||||
|
return PyLong_FromLong(id);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, e.what());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::get_material(PyVoxelGridObject* self, PyObject* args) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int id;
|
||||||
|
if (!PyArg_ParseTuple(args, "i", &id)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id < 0 || id > 255) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "material id must be 0-255");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mcrf::VoxelMaterial& mat = self->data->getMaterial(static_cast<uint8_t>(id));
|
||||||
|
|
||||||
|
// Create color object
|
||||||
|
PyObject* color_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
|
||||||
|
if (!color_type) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* color_obj = PyObject_Call(color_type,
|
||||||
|
Py_BuildValue("(iiii)", mat.color.r, mat.color.g, mat.color.b, mat.color.a),
|
||||||
|
nullptr);
|
||||||
|
Py_DECREF(color_type);
|
||||||
|
|
||||||
|
if (!color_obj) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build result dict
|
||||||
|
PyObject* result = Py_BuildValue("{s:s, s:N, s:i, s:O, s:f}",
|
||||||
|
"name", mat.name.c_str(),
|
||||||
|
"color", color_obj,
|
||||||
|
"sprite_index", mat.spriteIndex,
|
||||||
|
"transparent", mat.transparent ? Py_True : Py_False,
|
||||||
|
"path_cost", mat.pathCost);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Bulk operations
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::fill(PyVoxelGridObject* self, PyObject* args) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int material;
|
||||||
|
if (!PyArg_ParseTuple(args, "i", &material)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (material < 0 || material > 255) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "material must be 0-255");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->fill(static_cast<uint8_t>(material));
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::clear(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->clear();
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::fill_box(PyVoxelGridObject* self, PyObject* args) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* min_obj = nullptr;
|
||||||
|
PyObject* max_obj = nullptr;
|
||||||
|
int material;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTuple(args, "OOi", &min_obj, &max_obj, &material)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (material < 0 || material > 255) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "material must be 0-255");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse min tuple (x0, y0, z0)
|
||||||
|
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 (PySequence_Size(min_obj) != 3) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "min_coord must have exactly 3 elements");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse max tuple (x1, y1, z1)
|
||||||
|
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(max_obj) != 3) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "max_coord 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->fillBox(x0, y0, z0, x1, y1, z1, static_cast<uint8_t>(material));
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Mesh caching methods (Milestone 10)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::get_vertex_count(PyVoxelGridObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return PyLong_FromSize_t(self->data->vertexCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::rebuild_mesh(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->rebuildMesh();
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Statistics
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::count_non_air(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PyLong_FromSize_t(self->data->countNonAir());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyVoxelGrid::count_material(PyVoxelGridObject* self, PyObject* args) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int material;
|
||||||
|
if (!PyArg_ParseTuple(args, "i", &material)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (material < 0 || material > 255) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "material must be 0-255");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PyLong_FromSize_t(self->data->countMaterial(static_cast<uint8_t>(material)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Method definitions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
PyMethodDef PyVoxelGrid::methods[] = {
|
||||||
|
{"get", (PyCFunction)get, METH_VARARGS,
|
||||||
|
"get(x, y, z) -> int\n\n"
|
||||||
|
"Get the material ID at integer coordinates.\n\n"
|
||||||
|
"Returns 0 (air) for out-of-bounds coordinates."},
|
||||||
|
{"set", (PyCFunction)set, METH_VARARGS,
|
||||||
|
"set(x, y, z, material) -> None\n\n"
|
||||||
|
"Set the material ID at integer coordinates.\n\n"
|
||||||
|
"Out-of-bounds coordinates are silently ignored."},
|
||||||
|
{"add_material", (PyCFunction)add_material, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"add_material(name, color=Color(255,255,255), sprite_index=-1, transparent=False, path_cost=1.0) -> int\n\n"
|
||||||
|
"Add a new material to the palette. Returns the material ID (1-indexed).\n\n"
|
||||||
|
"Material 0 is always air (implicit, never stored in palette).\n"
|
||||||
|
"Maximum 255 materials can be added."},
|
||||||
|
{"get_material", (PyCFunction)get_material, METH_VARARGS,
|
||||||
|
"get_material(id) -> dict\n\n"
|
||||||
|
"Get material properties by ID.\n\n"
|
||||||
|
"Returns dict with keys: name, color, sprite_index, transparent, path_cost.\n"
|
||||||
|
"ID 0 returns the implicit air material."},
|
||||||
|
{"fill", (PyCFunction)fill, METH_VARARGS,
|
||||||
|
"fill(material) -> None\n\n"
|
||||||
|
"Fill the entire grid with the specified material ID."},
|
||||||
|
{"fill_box", (PyCFunction)fill_box, METH_VARARGS,
|
||||||
|
"fill_box(min_coord, max_coord, material) -> None\n\n"
|
||||||
|
"Fill a rectangular region with the specified material.\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 (0-255)\n\n"
|
||||||
|
"Coordinates are clamped to grid bounds."},
|
||||||
|
{"clear", (PyCFunction)clear, METH_NOARGS,
|
||||||
|
"clear() -> None\n\n"
|
||||||
|
"Clear the grid (fill with air, material 0)."},
|
||||||
|
{"rebuild_mesh", (PyCFunction)rebuild_mesh, METH_NOARGS,
|
||||||
|
"rebuild_mesh() -> None\n\n"
|
||||||
|
"Force immediate mesh rebuild for rendering."},
|
||||||
|
{"count_non_air", (PyCFunction)count_non_air, METH_NOARGS,
|
||||||
|
"count_non_air() -> int\n\n"
|
||||||
|
"Count the number of non-air voxels in the grid."},
|
||||||
|
{"count_material", (PyCFunction)count_material, METH_VARARGS,
|
||||||
|
"count_material(material) -> int\n\n"
|
||||||
|
"Count the number of voxels with the specified material ID."},
|
||||||
|
{nullptr} // Sentinel
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Property definitions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
PyGetSetDef PyVoxelGrid::getsetters[] = {
|
||||||
|
{"size", (getter)get_size, nullptr,
|
||||||
|
"Dimensions (width, height, depth) of the grid. Read-only.", nullptr},
|
||||||
|
{"width", (getter)get_width, nullptr,
|
||||||
|
"Grid width (X dimension). Read-only.", nullptr},
|
||||||
|
{"height", (getter)get_height, nullptr,
|
||||||
|
"Grid height (Y dimension). Read-only.", nullptr},
|
||||||
|
{"depth", (getter)get_depth, nullptr,
|
||||||
|
"Grid depth (Z dimension). Read-only.", nullptr},
|
||||||
|
{"cell_size", (getter)get_cell_size, nullptr,
|
||||||
|
"World units per voxel. Read-only.", nullptr},
|
||||||
|
{"material_count", (getter)get_material_count, nullptr,
|
||||||
|
"Number of materials in the palette. Read-only.", nullptr},
|
||||||
|
{"vertex_count", (getter)get_vertex_count, nullptr,
|
||||||
|
"Number of vertices after mesh generation. Read-only.", nullptr},
|
||||||
|
{"offset", (getter)get_offset, (setter)set_offset,
|
||||||
|
"World-space position (x, y, z) of the grid origin.", nullptr},
|
||||||
|
{"rotation", (getter)get_rotation, (setter)set_rotation,
|
||||||
|
"Y-axis rotation in degrees.", nullptr},
|
||||||
|
{nullptr} // Sentinel
|
||||||
|
};
|
||||||
125
src/3d/PyVoxelGrid.h
Normal file
125
src/3d/PyVoxelGrid.h
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
// PyVoxelGrid.h - Python bindings for VoxelGrid
|
||||||
|
// Part of McRogueFace 3D Extension - Milestone 9
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../Common.h"
|
||||||
|
#include "Python.h"
|
||||||
|
#include "VoxelGrid.h"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
class PyVoxelGrid;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Python object structure
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
typedef struct PyVoxelGridObject {
|
||||||
|
PyObject_HEAD
|
||||||
|
std::shared_ptr<mcrf::VoxelGrid> data;
|
||||||
|
PyObject* weakreflist;
|
||||||
|
} PyVoxelGridObject;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Python binding class
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class PyVoxelGrid {
|
||||||
|
public:
|
||||||
|
// Python type interface
|
||||||
|
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
||||||
|
static int init(PyVoxelGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static void dealloc(PyVoxelGridObject* self);
|
||||||
|
static PyObject* repr(PyObject* obj);
|
||||||
|
|
||||||
|
// Properties - dimensions (read-only)
|
||||||
|
static PyObject* get_size(PyVoxelGridObject* self, void* closure);
|
||||||
|
static PyObject* get_width(PyVoxelGridObject* self, void* closure);
|
||||||
|
static PyObject* get_height(PyVoxelGridObject* self, void* closure);
|
||||||
|
static PyObject* get_depth(PyVoxelGridObject* self, void* closure);
|
||||||
|
static PyObject* get_cell_size(PyVoxelGridObject* self, void* closure);
|
||||||
|
static PyObject* get_material_count(PyVoxelGridObject* self, void* closure);
|
||||||
|
|
||||||
|
// Properties - transform (read-write)
|
||||||
|
static PyObject* get_offset(PyVoxelGridObject* self, void* closure);
|
||||||
|
static int set_offset(PyVoxelGridObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_rotation(PyVoxelGridObject* self, void* closure);
|
||||||
|
static int set_rotation(PyVoxelGridObject* self, PyObject* value, void* closure);
|
||||||
|
|
||||||
|
// Voxel access methods
|
||||||
|
static PyObject* get(PyVoxelGridObject* self, PyObject* args);
|
||||||
|
static PyObject* set(PyVoxelGridObject* self, PyObject* args);
|
||||||
|
|
||||||
|
// Material methods
|
||||||
|
static PyObject* add_material(PyVoxelGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static PyObject* get_material(PyVoxelGridObject* self, PyObject* args);
|
||||||
|
|
||||||
|
// Bulk operations
|
||||||
|
static PyObject* fill(PyVoxelGridObject* self, PyObject* args);
|
||||||
|
static PyObject* fill_box(PyVoxelGridObject* self, PyObject* args);
|
||||||
|
static PyObject* clear(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
|
||||||
|
|
||||||
|
// Mesh caching (Milestone 10)
|
||||||
|
static PyObject* get_vertex_count(PyVoxelGridObject* self, void* closure);
|
||||||
|
static PyObject* rebuild_mesh(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
static PyObject* count_non_air(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
|
||||||
|
static PyObject* count_material(PyVoxelGridObject* self, PyObject* args);
|
||||||
|
|
||||||
|
// Type registration
|
||||||
|
static PyMethodDef methods[];
|
||||||
|
static PyGetSetDef getsetters[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Python type definition
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
namespace mcrfpydef {
|
||||||
|
|
||||||
|
inline PyTypeObject PyVoxelGridType = {
|
||||||
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
.tp_name = "mcrfpy.VoxelGrid",
|
||||||
|
.tp_basicsize = sizeof(PyVoxelGridObject),
|
||||||
|
.tp_itemsize = 0,
|
||||||
|
.tp_dealloc = (destructor)PyVoxelGrid::dealloc,
|
||||||
|
.tp_repr = PyVoxelGrid::repr,
|
||||||
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||||
|
.tp_doc = PyDoc_STR(
|
||||||
|
"VoxelGrid(size: tuple[int, int, int], cell_size: float = 1.0)\n\n"
|
||||||
|
"A dense 3D grid of voxel material IDs with a material palette.\n\n"
|
||||||
|
"VoxelGrids provide volumetric storage for 3D structures like buildings,\n"
|
||||||
|
"caves, and dungeon walls. Each cell stores a uint8 material ID (0-255),\n"
|
||||||
|
"where 0 is always air.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" size: (width, height, depth) dimensions. Immutable after creation.\n"
|
||||||
|
" cell_size: World units per voxel. Default 1.0.\n\n"
|
||||||
|
"Properties:\n"
|
||||||
|
" size (tuple, read-only): Grid dimensions as (width, height, depth)\n"
|
||||||
|
" width, height, depth (int, read-only): Individual dimensions\n"
|
||||||
|
" cell_size (float, read-only): World units per voxel\n"
|
||||||
|
" offset (tuple): World-space position (x, y, z)\n"
|
||||||
|
" rotation (float): Y-axis rotation in degrees\n"
|
||||||
|
" material_count (int, read-only): Number of defined materials\n\n"
|
||||||
|
"Example:\n"
|
||||||
|
" voxels = mcrfpy.VoxelGrid(size=(16, 8, 16), cell_size=1.0)\n"
|
||||||
|
" stone = voxels.add_material('stone', color=mcrfpy.Color(128, 128, 128))\n"
|
||||||
|
" voxels.set(5, 0, 5, stone)\n"
|
||||||
|
" assert voxels.get(5, 0, 5) == stone\n"
|
||||||
|
" print(f'Non-air voxels: {voxels.count_non_air()}')"
|
||||||
|
),
|
||||||
|
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
.tp_clear = [](PyObject* self) -> int {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
.tp_weaklistoffset = offsetof(PyVoxelGridObject, weakreflist),
|
||||||
|
.tp_methods = nullptr, // Set before PyType_Ready
|
||||||
|
.tp_getset = nullptr, // Set before PyType_Ready
|
||||||
|
.tp_init = (initproc)PyVoxelGrid::init,
|
||||||
|
.tp_new = PyVoxelGrid::pynew,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mcrfpydef
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
#include "EntityCollection3D.h"
|
#include "EntityCollection3D.h"
|
||||||
#include "Billboard.h"
|
#include "Billboard.h"
|
||||||
#include "Model3D.h"
|
#include "Model3D.h"
|
||||||
|
#include "VoxelGrid.h"
|
||||||
|
#include "PyVoxelGrid.h"
|
||||||
#include "../platform/GLContext.h"
|
#include "../platform/GLContext.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PyColor.h"
|
#include "PyColor.h"
|
||||||
|
|
@ -62,6 +64,15 @@ Viewport3D::Viewport3D(float x, float y, float width, float height)
|
||||||
Viewport3D::~Viewport3D() {
|
Viewport3D::~Viewport3D() {
|
||||||
cleanupTestGeometry();
|
cleanupTestGeometry();
|
||||||
cleanupFBO();
|
cleanupFBO();
|
||||||
|
|
||||||
|
// Clean up voxel VBO (Milestone 10)
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
if (voxelVBO_ != 0) {
|
||||||
|
glDeleteBuffers(1, &voxelVBO_);
|
||||||
|
voxelVBO_ = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (tcodMap_) {
|
if (tcodMap_) {
|
||||||
delete tcodMap_;
|
delete tcodMap_;
|
||||||
tcodMap_ = nullptr;
|
tcodMap_ = nullptr;
|
||||||
|
|
@ -836,6 +847,130 @@ void Viewport3D::renderMeshLayers() {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Voxel Layer Management (Milestone 10)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Viewport3D::addVoxelLayer(std::shared_ptr<VoxelGrid> grid, int zIndex) {
|
||||||
|
if (!grid) return;
|
||||||
|
voxelLayers_.push_back({grid, zIndex});
|
||||||
|
|
||||||
|
// Disable test cube when real content is added
|
||||||
|
renderTestCube_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Viewport3D::removeVoxelLayer(std::shared_ptr<VoxelGrid> grid) {
|
||||||
|
if (!grid) return false;
|
||||||
|
|
||||||
|
auto it = std::find_if(voxelLayers_.begin(), voxelLayers_.end(),
|
||||||
|
[&grid](const auto& pair) { return pair.first == grid; });
|
||||||
|
|
||||||
|
if (it != voxelLayers_.end()) {
|
||||||
|
voxelLayers_.erase(it);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewport3D::renderVoxelLayers(const mat4& view, const mat4& proj) {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
if (voxelLayers_.empty() || !shader_ || !shader_->isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort layers by z_index (lower = rendered first)
|
||||||
|
std::vector<std::pair<VoxelGrid*, int>> sortedLayers;
|
||||||
|
sortedLayers.reserve(voxelLayers_.size());
|
||||||
|
for (auto& pair : voxelLayers_) {
|
||||||
|
if (pair.first) {
|
||||||
|
sortedLayers.push_back({pair.first.get(), pair.second});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(sortedLayers.begin(), sortedLayers.end(),
|
||||||
|
[](const auto& a, const auto& b) { return a.second < b.second; });
|
||||||
|
|
||||||
|
shader_->bind();
|
||||||
|
|
||||||
|
// Set up view and projection matrices
|
||||||
|
shader_->setUniform("u_view", view);
|
||||||
|
shader_->setUniform("u_projection", proj);
|
||||||
|
|
||||||
|
// PS1 effect uniforms
|
||||||
|
shader_->setUniform("u_resolution", vec2(static_cast<float>(internalWidth_),
|
||||||
|
static_cast<float>(internalHeight_)));
|
||||||
|
shader_->setUniform("u_enable_snap", vertexSnapEnabled_);
|
||||||
|
shader_->setUniform("u_enable_dither", ditheringEnabled_);
|
||||||
|
|
||||||
|
// Lighting
|
||||||
|
vec3 lightDir = vec3(0.5f, -0.7f, 0.5f).normalized();
|
||||||
|
shader_->setUniform("u_light_dir", lightDir);
|
||||||
|
shader_->setUniform("u_ambient", vec3(0.3f, 0.3f, 0.3f));
|
||||||
|
|
||||||
|
// Fog
|
||||||
|
shader_->setUniform("u_fog_start", fogNear_);
|
||||||
|
shader_->setUniform("u_fog_end", fogFar_);
|
||||||
|
shader_->setUniform("u_fog_color", fogColor_);
|
||||||
|
|
||||||
|
// No texture for voxels (use vertex colors)
|
||||||
|
shader_->setUniform("u_has_texture", false);
|
||||||
|
|
||||||
|
// Create VBO if needed
|
||||||
|
if (voxelVBO_ == 0) {
|
||||||
|
glGenBuffers(1, &voxelVBO_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render each voxel grid
|
||||||
|
for (auto& pair : sortedLayers) {
|
||||||
|
VoxelGrid* grid = pair.first;
|
||||||
|
|
||||||
|
// Get vertices (triggers rebuild if dirty)
|
||||||
|
const std::vector<MeshVertex>& vertices = grid->getVertices();
|
||||||
|
if (vertices.empty()) continue;
|
||||||
|
|
||||||
|
// Set model matrix for this grid
|
||||||
|
shader_->setUniform("u_model", grid->getModelMatrix());
|
||||||
|
|
||||||
|
// Upload vertices to VBO
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, voxelVBO_);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER,
|
||||||
|
vertices.size() * sizeof(MeshVertex),
|
||||||
|
vertices.data(),
|
||||||
|
GL_DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
// Set up vertex attributes (same as MeshLayer)
|
||||||
|
size_t stride = sizeof(MeshVertex);
|
||||||
|
|
||||||
|
// Position
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(MeshVertex, position));
|
||||||
|
|
||||||
|
// TexCoord
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(MeshVertex, texcoord));
|
||||||
|
|
||||||
|
// Normal
|
||||||
|
glEnableVertexAttribArray(2);
|
||||||
|
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(MeshVertex, normal));
|
||||||
|
|
||||||
|
// Color
|
||||||
|
glEnableVertexAttribArray(3);
|
||||||
|
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(MeshVertex, color));
|
||||||
|
|
||||||
|
// Draw
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(vertices.size()));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
glDisableVertexAttribArray(0);
|
||||||
|
glDisableVertexAttribArray(1);
|
||||||
|
glDisableVertexAttribArray(2);
|
||||||
|
glDisableVertexAttribArray(3);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
shader_->unbind();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void Viewport3D::render3DContent() {
|
void Viewport3D::render3DContent() {
|
||||||
// GL not available in current backend - skip 3D rendering
|
// GL not available in current backend - skip 3D rendering
|
||||||
if (!gl::isGLReady() || fbo_ == 0) {
|
if (!gl::isGLReady() || fbo_ == 0) {
|
||||||
|
|
@ -879,9 +1014,12 @@ void Viewport3D::render3DContent() {
|
||||||
// Render mesh layers first (terrain, etc.) - sorted by z_index
|
// Render mesh layers first (terrain, etc.) - sorted by z_index
|
||||||
renderMeshLayers();
|
renderMeshLayers();
|
||||||
|
|
||||||
// Render entities
|
// Render voxel layers (Milestone 10)
|
||||||
mat4 view = camera_.getViewMatrix();
|
mat4 view = camera_.getViewMatrix();
|
||||||
mat4 projection = camera_.getProjectionMatrix();
|
mat4 projection = camera_.getProjectionMatrix();
|
||||||
|
renderVoxelLayers(view, projection);
|
||||||
|
|
||||||
|
// Render entities
|
||||||
renderEntities(view, projection);
|
renderEntities(view, projection);
|
||||||
|
|
||||||
// Render billboards (after opaque geometry for proper transparency)
|
// Render billboards (after opaque geometry for proper transparency)
|
||||||
|
|
@ -2262,6 +2400,81 @@ static PyObject* Viewport3D_follow(PyViewport3DObject* self, PyObject* args, PyO
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Voxel Layer Methods (Milestone 10)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_add_voxel_layer(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"voxel_grid", "z_index", NULL};
|
||||||
|
PyObject* voxel_grid_obj = nullptr;
|
||||||
|
int z_index = 0;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|i", const_cast<char**>(kwlist),
|
||||||
|
&voxel_grid_obj, &z_index)) {
|
||||||
|
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->addVoxelLayer(vg->data, z_index);
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_remove_voxel_layer(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool removed = self->data->removeVoxelLayer(vg->data);
|
||||||
|
return PyBool_FromLong(removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_voxel_layer_count(PyViewport3DObject* self, PyObject* Py_UNUSED(args)) {
|
||||||
|
return PyLong_FromSize_t(self->data->getVoxelLayerCount());
|
||||||
|
}
|
||||||
|
|
||||||
} // 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
|
||||||
|
|
@ -2441,5 +2654,25 @@ PyMethodDef Viewport3D_methods[] = {
|
||||||
" distance: Distance behind entity\n"
|
" distance: Distance behind entity\n"
|
||||||
" height: Camera height above entity\n"
|
" height: Camera height above entity\n"
|
||||||
" smoothing: Interpolation factor (0-1). 1 = instant, lower = smoother"},
|
" smoothing: Interpolation factor (0-1). 1 = instant, lower = smoother"},
|
||||||
|
|
||||||
|
// Voxel layer methods (Milestone 10)
|
||||||
|
{"add_voxel_layer", (PyCFunction)mcrf::Viewport3D_add_voxel_layer, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"add_voxel_layer(voxel_grid, z_index=0)\n\n"
|
||||||
|
"Add a VoxelGrid as a renderable layer.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" voxel_grid: VoxelGrid object to render\n"
|
||||||
|
" z_index: Render order (lower = rendered first)"},
|
||||||
|
{"remove_voxel_layer", (PyCFunction)mcrf::Viewport3D_remove_voxel_layer, METH_VARARGS,
|
||||||
|
"remove_voxel_layer(voxel_grid) -> bool\n\n"
|
||||||
|
"Remove a VoxelGrid layer from the viewport.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" voxel_grid: VoxelGrid object to remove\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" True if the layer was found and removed"},
|
||||||
|
{"voxel_layer_count", (PyCFunction)mcrf::Viewport3D_voxel_layer_count, METH_NOARGS,
|
||||||
|
"voxel_layer_count() -> int\n\n"
|
||||||
|
"Get the number of voxel layers.\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" Number of voxel layers in the viewport"},
|
||||||
{NULL} // Sentinel
|
{NULL} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ class Viewport3D;
|
||||||
class Shader3D;
|
class Shader3D;
|
||||||
class MeshLayer;
|
class MeshLayer;
|
||||||
class Billboard;
|
class Billboard;
|
||||||
|
class VoxelGrid;
|
||||||
|
|
||||||
} // namespace mcrf
|
} // namespace mcrf
|
||||||
|
|
||||||
|
|
@ -226,6 +227,29 @@ public:
|
||||||
/// Render all billboards
|
/// Render all billboards
|
||||||
void renderBillboards(const mat4& view, const mat4& proj);
|
void renderBillboards(const mat4& view, const mat4& proj);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// VoxelGrid Layer Management (Milestone 10)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Add a voxel layer to the viewport
|
||||||
|
/// @param grid The VoxelGrid to add
|
||||||
|
/// @param zIndex Render order (lower = rendered first, behind higher values)
|
||||||
|
void addVoxelLayer(std::shared_ptr<VoxelGrid> grid, int zIndex = 0);
|
||||||
|
|
||||||
|
/// Remove a voxel layer from the viewport
|
||||||
|
/// @param grid The VoxelGrid to remove
|
||||||
|
/// @return true if the layer was found and removed
|
||||||
|
bool removeVoxelLayer(std::shared_ptr<VoxelGrid> grid);
|
||||||
|
|
||||||
|
/// Get all voxel layers (read-only)
|
||||||
|
const std::vector<std::pair<std::shared_ptr<VoxelGrid>, int>>& getVoxelLayers() const { return voxelLayers_; }
|
||||||
|
|
||||||
|
/// Get number of voxel layers
|
||||||
|
size_t getVoxelLayerCount() const { return voxelLayers_.size(); }
|
||||||
|
|
||||||
|
/// Render all voxel layers
|
||||||
|
void renderVoxelLayers(const mat4& view, const mat4& proj);
|
||||||
|
|
||||||
// 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_; }
|
||||||
|
|
@ -319,6 +343,11 @@ private:
|
||||||
// Billboard storage
|
// Billboard storage
|
||||||
std::shared_ptr<std::vector<std::shared_ptr<Billboard>>> billboards_;
|
std::shared_ptr<std::vector<std::shared_ptr<Billboard>>> billboards_;
|
||||||
|
|
||||||
|
// Voxel layer storage (Milestone 10)
|
||||||
|
// Pairs of (VoxelGrid, z_index) for render ordering
|
||||||
|
std::vector<std::pair<std::shared_ptr<VoxelGrid>, int>> voxelLayers_;
|
||||||
|
unsigned int voxelVBO_ = 0; // Shared VBO for voxel rendering
|
||||||
|
|
||||||
// Shader for PS1-style rendering
|
// Shader for PS1-style rendering
|
||||||
std::unique_ptr<Shader3D> shader_;
|
std::unique_ptr<Shader3D> shader_;
|
||||||
std::unique_ptr<Shader3D> skinnedShader_; // For skeletal animation
|
std::unique_ptr<Shader3D> skinnedShader_; // For skeletal animation
|
||||||
|
|
|
||||||
171
src/3d/VoxelGrid.cpp
Normal file
171
src/3d/VoxelGrid.cpp
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
// VoxelGrid.cpp - Dense 3D voxel array implementation
|
||||||
|
// Part of McRogueFace 3D Extension - Milestones 9-10
|
||||||
|
|
||||||
|
#include "VoxelGrid.h"
|
||||||
|
#include "VoxelMesher.h"
|
||||||
|
#include "MeshLayer.h" // For MeshVertex
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
// Static air material for out-of-bounds or ID=0 queries
|
||||||
|
static VoxelMaterial airMaterial{"air", sf::Color::Transparent, -1, true, 0.0f};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Constructor
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
VoxelGrid::VoxelGrid(int w, int h, int d, float cellSize)
|
||||||
|
: width_(w), height_(h), depth_(d), cellSize_(cellSize),
|
||||||
|
offset_(0, 0, 0), rotation_(0.0f)
|
||||||
|
{
|
||||||
|
if (w <= 0 || h <= 0 || d <= 0) {
|
||||||
|
throw std::invalid_argument("VoxelGrid dimensions must be positive");
|
||||||
|
}
|
||||||
|
if (cellSize <= 0.0f) {
|
||||||
|
throw std::invalid_argument("VoxelGrid cell size must be positive");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate dense array, initialized to air (0)
|
||||||
|
size_t totalSize = static_cast<size_t>(w) * h * d;
|
||||||
|
data_.resize(totalSize, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Per-voxel access
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
bool VoxelGrid::isValid(int x, int y, int z) const {
|
||||||
|
return x >= 0 && x < width_ &&
|
||||||
|
y >= 0 && y < height_ &&
|
||||||
|
z >= 0 && z < depth_;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t VoxelGrid::get(int x, int y, int z) const {
|
||||||
|
if (!isValid(x, y, z)) {
|
||||||
|
return 0; // Out of bounds returns air
|
||||||
|
}
|
||||||
|
return data_[index(x, y, z)];
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelGrid::set(int x, int y, int z, uint8_t material) {
|
||||||
|
if (!isValid(x, y, z)) {
|
||||||
|
return; // Out of bounds is no-op
|
||||||
|
}
|
||||||
|
data_[index(x, y, z)] = material;
|
||||||
|
meshDirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Material palette
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
uint8_t VoxelGrid::addMaterial(const VoxelMaterial& mat) {
|
||||||
|
if (materials_.size() >= 255) {
|
||||||
|
throw std::runtime_error("Material palette full (max 255 materials)");
|
||||||
|
}
|
||||||
|
materials_.push_back(mat);
|
||||||
|
return static_cast<uint8_t>(materials_.size()); // 1-indexed
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t VoxelGrid::addMaterial(const std::string& name, sf::Color color,
|
||||||
|
int spriteIndex, bool transparent, float pathCost) {
|
||||||
|
return addMaterial(VoxelMaterial(name, color, spriteIndex, transparent, pathCost));
|
||||||
|
}
|
||||||
|
|
||||||
|
const VoxelMaterial& VoxelGrid::getMaterial(uint8_t id) const {
|
||||||
|
if (id == 0 || id > materials_.size()) {
|
||||||
|
return airMaterial;
|
||||||
|
}
|
||||||
|
return materials_[id - 1]; // 1-indexed, so ID 1 = materials_[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Bulk operations
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void VoxelGrid::fill(uint8_t material) {
|
||||||
|
std::fill(data_.begin(), data_.end(), material);
|
||||||
|
meshDirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Transform
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
mat4 VoxelGrid::getModelMatrix() const {
|
||||||
|
// Apply translation first, then rotation around Y axis
|
||||||
|
mat4 translation = mat4::translate(offset_);
|
||||||
|
mat4 rotation = mat4::rotateY(rotation_ * DEG_TO_RAD);
|
||||||
|
return translation * rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Statistics
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
size_t VoxelGrid::countNonAir() const {
|
||||||
|
size_t count = 0;
|
||||||
|
for (uint8_t v : data_) {
|
||||||
|
if (v != 0) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t VoxelGrid::countMaterial(uint8_t material) const {
|
||||||
|
size_t count = 0;
|
||||||
|
for (uint8_t v : data_) {
|
||||||
|
if (v == material) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// fillBox (Milestone 10)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void VoxelGrid::fillBox(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t material) {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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++) {
|
||||||
|
data_[index(x, y, z)] = material;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
meshDirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Mesh Caching (Milestone 10)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const std::vector<MeshVertex>& VoxelGrid::getVertices() const {
|
||||||
|
if (meshDirty_) {
|
||||||
|
rebuildMesh();
|
||||||
|
}
|
||||||
|
return cachedVertices_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelGrid::rebuildMesh() const {
|
||||||
|
cachedVertices_.clear();
|
||||||
|
VoxelMesher::generateMesh(*this, cachedVertices_);
|
||||||
|
meshDirty_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
121
src/3d/VoxelGrid.h
Normal file
121
src/3d/VoxelGrid.h
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
// VoxelGrid.h - Dense 3D voxel array with material palette
|
||||||
|
// Part of McRogueFace 3D Extension - Milestones 9-10
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../Common.h"
|
||||||
|
#include "Math3D.h"
|
||||||
|
#include "MeshLayer.h" // For MeshVertex (needed for std::vector<MeshVertex>)
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// VoxelMaterial - Properties for a voxel material type
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct VoxelMaterial {
|
||||||
|
std::string name;
|
||||||
|
sf::Color color; // Fallback solid color
|
||||||
|
int spriteIndex = -1; // Texture atlas index (-1 = use color)
|
||||||
|
bool transparent = false; // For FOV projection and face culling
|
||||||
|
float pathCost = 1.0f; // Navigation cost multiplier (0 = impassable)
|
||||||
|
|
||||||
|
VoxelMaterial() : name("unnamed"), color(sf::Color::White) {}
|
||||||
|
VoxelMaterial(const std::string& n, sf::Color c, int sprite = -1,
|
||||||
|
bool transp = false, float cost = 1.0f)
|
||||||
|
: name(n), color(c), spriteIndex(sprite), transparent(transp), pathCost(cost) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// VoxelGrid - Dense 3D array of material IDs
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class VoxelGrid {
|
||||||
|
private:
|
||||||
|
int width_, height_, depth_;
|
||||||
|
float cellSize_;
|
||||||
|
std::vector<uint8_t> data_; // Material ID per cell (0 = air)
|
||||||
|
std::vector<VoxelMaterial> materials_;
|
||||||
|
|
||||||
|
// Transform
|
||||||
|
vec3 offset_;
|
||||||
|
float rotation_ = 0.0f; // Y-axis only, degrees
|
||||||
|
|
||||||
|
// Mesh caching (Milestone 10)
|
||||||
|
mutable bool meshDirty_ = true;
|
||||||
|
mutable std::vector<MeshVertex> cachedVertices_;
|
||||||
|
|
||||||
|
// Index calculation (row-major: X varies fastest, then Y, then Z)
|
||||||
|
inline size_t index(int x, int y, int z) const {
|
||||||
|
return static_cast<size_t>(z) * (width_ * height_) +
|
||||||
|
static_cast<size_t>(y) * width_ +
|
||||||
|
static_cast<size_t>(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Constructor
|
||||||
|
VoxelGrid(int w, int h, int d, float cellSize = 1.0f);
|
||||||
|
|
||||||
|
// Dimensions (read-only)
|
||||||
|
int width() const { return width_; }
|
||||||
|
int height() const { return height_; }
|
||||||
|
int depth() const { return depth_; }
|
||||||
|
float cellSize() const { return cellSize_; }
|
||||||
|
size_t totalVoxels() const { return static_cast<size_t>(width_) * height_ * depth_; }
|
||||||
|
|
||||||
|
// Per-voxel access
|
||||||
|
uint8_t get(int x, int y, int z) const;
|
||||||
|
void set(int x, int y, int z, uint8_t material);
|
||||||
|
bool isValid(int x, int y, int z) const;
|
||||||
|
|
||||||
|
// Material palette
|
||||||
|
// Returns 1-indexed material ID (0 = air, always implicit)
|
||||||
|
uint8_t addMaterial(const VoxelMaterial& mat);
|
||||||
|
uint8_t addMaterial(const std::string& name, sf::Color color,
|
||||||
|
int spriteIndex = -1, bool transparent = false,
|
||||||
|
float pathCost = 1.0f);
|
||||||
|
const VoxelMaterial& getMaterial(uint8_t id) const;
|
||||||
|
size_t materialCount() const { return materials_.size(); }
|
||||||
|
|
||||||
|
// Bulk operations
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Transform
|
||||||
|
void setOffset(const vec3& offset) { offset_ = offset; }
|
||||||
|
void setOffset(float x, float y, float z) { offset_ = vec3(x, y, z); }
|
||||||
|
vec3 getOffset() const { return offset_; }
|
||||||
|
void setRotation(float degrees) { rotation_ = degrees; }
|
||||||
|
float getRotation() const { return rotation_; }
|
||||||
|
mat4 getModelMatrix() const;
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
size_t countNonAir() const;
|
||||||
|
size_t countMaterial(uint8_t material) const;
|
||||||
|
|
||||||
|
// Mesh caching (Milestone 10)
|
||||||
|
/// Mark mesh as needing rebuild (called automatically by set/fill operations)
|
||||||
|
void markDirty() { meshDirty_ = true; }
|
||||||
|
|
||||||
|
/// Check if mesh needs rebuild
|
||||||
|
bool isMeshDirty() const { return meshDirty_; }
|
||||||
|
|
||||||
|
/// Get vertices for rendering (rebuilds mesh if dirty)
|
||||||
|
const std::vector<MeshVertex>& getVertices() const;
|
||||||
|
|
||||||
|
/// Force immediate mesh rebuild
|
||||||
|
void rebuildMesh() const;
|
||||||
|
|
||||||
|
/// Get vertex count after mesh generation
|
||||||
|
size_t vertexCount() const { return cachedVertices_.size(); }
|
||||||
|
|
||||||
|
// Memory info (for debugging)
|
||||||
|
size_t memoryUsageBytes() const {
|
||||||
|
return data_.size() + materials_.size() * sizeof(VoxelMaterial);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
136
src/3d/VoxelMesher.cpp
Normal file
136
src/3d/VoxelMesher.cpp
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
// VoxelMesher.cpp - Face-culled mesh generation for VoxelGrid
|
||||||
|
// Part of McRogueFace 3D Extension - Milestone 10
|
||||||
|
|
||||||
|
#include "VoxelMesher.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
void VoxelMesher::generateMesh(const VoxelGrid& grid, std::vector<MeshVertex>& outVertices) {
|
||||||
|
const float cs = grid.cellSize();
|
||||||
|
|
||||||
|
for (int z = 0; z < grid.depth(); z++) {
|
||||||
|
for (int y = 0; y < grid.height(); y++) {
|
||||||
|
for (int x = 0; x < grid.width(); x++) {
|
||||||
|
uint8_t mat = grid.get(x, y, z);
|
||||||
|
if (mat == 0) continue; // Skip air
|
||||||
|
|
||||||
|
const VoxelMaterial& material = grid.getMaterial(mat);
|
||||||
|
|
||||||
|
// Voxel center in local space
|
||||||
|
vec3 center((x + 0.5f) * cs, (y + 0.5f) * cs, (z + 0.5f) * cs);
|
||||||
|
|
||||||
|
// Check each face direction
|
||||||
|
// +X face
|
||||||
|
if (shouldGenerateFace(grid, x, y, z, x + 1, y, z)) {
|
||||||
|
emitFace(outVertices, center, vec3(1, 0, 0), cs, material);
|
||||||
|
}
|
||||||
|
// -X face
|
||||||
|
if (shouldGenerateFace(grid, x, y, z, x - 1, y, z)) {
|
||||||
|
emitFace(outVertices, center, vec3(-1, 0, 0), cs, material);
|
||||||
|
}
|
||||||
|
// +Y face (top)
|
||||||
|
if (shouldGenerateFace(grid, x, y, z, x, y + 1, z)) {
|
||||||
|
emitFace(outVertices, center, vec3(0, 1, 0), cs, material);
|
||||||
|
}
|
||||||
|
// -Y face (bottom)
|
||||||
|
if (shouldGenerateFace(grid, x, y, z, x, y - 1, z)) {
|
||||||
|
emitFace(outVertices, center, vec3(0, -1, 0), cs, material);
|
||||||
|
}
|
||||||
|
// +Z face
|
||||||
|
if (shouldGenerateFace(grid, x, y, z, x, y, z + 1)) {
|
||||||
|
emitFace(outVertices, center, vec3(0, 0, 1), cs, material);
|
||||||
|
}
|
||||||
|
// -Z face
|
||||||
|
if (shouldGenerateFace(grid, x, y, z, x, y, z - 1)) {
|
||||||
|
emitFace(outVertices, center, vec3(0, 0, -1), cs, material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VoxelMesher::shouldGenerateFace(const VoxelGrid& grid,
|
||||||
|
int x, int y, int z,
|
||||||
|
int nx, int ny, int nz) {
|
||||||
|
// Out of bounds = air, so generate face
|
||||||
|
if (!grid.isValid(nx, ny, nz)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t neighbor = grid.get(nx, ny, nz);
|
||||||
|
|
||||||
|
// Air neighbor = generate face
|
||||||
|
if (neighbor == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if neighbor is transparent
|
||||||
|
// Transparent materials allow faces to be visible behind them
|
||||||
|
return grid.getMaterial(neighbor).transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelMesher::emitFace(std::vector<MeshVertex>& vertices,
|
||||||
|
const vec3& center,
|
||||||
|
const vec3& normal,
|
||||||
|
float size,
|
||||||
|
const VoxelMaterial& material) {
|
||||||
|
// Calculate face corners based on normal direction
|
||||||
|
vec3 up, right;
|
||||||
|
|
||||||
|
if (std::abs(normal.y) > 0.5f) {
|
||||||
|
// Horizontal face (floor/ceiling)
|
||||||
|
// For +Y (top), we want the face to look correct from above
|
||||||
|
// For -Y (bottom), we want it to look correct from below
|
||||||
|
up = vec3(0, 0, normal.y); // Z direction based on face direction
|
||||||
|
right = vec3(1, 0, 0); // Always X axis for horizontal faces
|
||||||
|
} else if (std::abs(normal.x) > 0.5f) {
|
||||||
|
// X-facing wall
|
||||||
|
up = vec3(0, 1, 0); // Y axis is up
|
||||||
|
right = vec3(0, 0, normal.x); // Z direction based on face direction
|
||||||
|
} else {
|
||||||
|
// Z-facing wall
|
||||||
|
up = vec3(0, 1, 0); // Y axis is up
|
||||||
|
right = vec3(-normal.z, 0, 0); // X direction based on face direction
|
||||||
|
}
|
||||||
|
|
||||||
|
float halfSize = size * 0.5f;
|
||||||
|
vec3 faceCenter = center + normal * halfSize;
|
||||||
|
|
||||||
|
// 4 corners of the face
|
||||||
|
vec3 corners[4] = {
|
||||||
|
faceCenter - right * halfSize - up * halfSize, // Bottom-left
|
||||||
|
faceCenter + right * halfSize - up * halfSize, // Bottom-right
|
||||||
|
faceCenter + right * halfSize + up * halfSize, // Top-right
|
||||||
|
faceCenter - right * halfSize + up * halfSize // Top-left
|
||||||
|
};
|
||||||
|
|
||||||
|
// UV coordinates (solid color or single sprite tile)
|
||||||
|
vec2 uvs[4] = {
|
||||||
|
vec2(0, 0), // Bottom-left
|
||||||
|
vec2(1, 0), // Bottom-right
|
||||||
|
vec2(1, 1), // Top-right
|
||||||
|
vec2(0, 1) // Top-left
|
||||||
|
};
|
||||||
|
|
||||||
|
// Color from material (convert 0-255 to 0-1)
|
||||||
|
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 for OpenGL front faces
|
||||||
|
// Triangle 1: 0-2-1 (bottom-left, top-right, bottom-right) - CCW
|
||||||
|
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 (bottom-left, top-left, top-right) - CCW
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
53
src/3d/VoxelMesher.h
Normal file
53
src/3d/VoxelMesher.h
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
// VoxelMesher.h - Face-culled mesh generation for VoxelGrid
|
||||||
|
// Part of McRogueFace 3D Extension - Milestone 10
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VoxelGrid.h"
|
||||||
|
#include "MeshLayer.h" // For MeshVertex
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// VoxelMesher - Static class for generating triangle meshes from VoxelGrid
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class VoxelMesher {
|
||||||
|
public:
|
||||||
|
/// Generate face-culled mesh from voxel data
|
||||||
|
/// 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)
|
||||||
|
static void generateMesh(
|
||||||
|
const VoxelGrid& grid,
|
||||||
|
std::vector<MeshVertex>& outVertices
|
||||||
|
);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// Check if face should be generated (neighbor is air or transparent)
|
||||||
|
/// @param grid The VoxelGrid
|
||||||
|
/// @param x, y, z Current voxel position
|
||||||
|
/// @param nx, ny, nz Neighbor voxel position
|
||||||
|
/// @return true if face should be generated
|
||||||
|
static bool shouldGenerateFace(
|
||||||
|
const VoxelGrid& grid,
|
||||||
|
int x, int y, int z,
|
||||||
|
int nx, int ny, int nz
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Generate a single face (2 triangles = 6 vertices)
|
||||||
|
/// @param vertices Output vector to append vertices to
|
||||||
|
/// @param center Center of the voxel
|
||||||
|
/// @param normal Face normal direction
|
||||||
|
/// @param size Voxel cell size
|
||||||
|
/// @param material Material for coloring
|
||||||
|
static void emitFace(
|
||||||
|
std::vector<MeshVertex>& vertices,
|
||||||
|
const vec3& center,
|
||||||
|
const vec3& normal,
|
||||||
|
float size,
|
||||||
|
const VoxelMaterial& material
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
#include "3d/EntityCollection3D.h" // Entity3D collection
|
#include "3d/EntityCollection3D.h" // Entity3D collection
|
||||||
#include "3d/Model3D.h" // 3D model resource
|
#include "3d/Model3D.h" // 3D model resource
|
||||||
#include "3d/Billboard.h" // Billboard sprites
|
#include "3d/Billboard.h" // Billboard sprites
|
||||||
|
#include "3d/PyVoxelGrid.h" // Voxel grid for 3D structures (Milestone 9)
|
||||||
#include "McRogueFaceVersion.h"
|
#include "McRogueFaceVersion.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
// ImGui is only available for SFML builds
|
// ImGui is only available for SFML builds
|
||||||
|
|
@ -442,7 +443,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
/*3D entities*/
|
/*3D entities*/
|
||||||
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
||||||
&mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType,
|
&mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType,
|
||||||
&mcrfpydef::PyBillboardType,
|
&mcrfpydef::PyBillboardType, &mcrfpydef::PyVoxelGridType,
|
||||||
|
|
||||||
/*grid layers (#147)*/
|
/*grid layers (#147)*/
|
||||||
&PyColorLayerType, &PyTileLayerType,
|
&PyColorLayerType, &PyTileLayerType,
|
||||||
|
|
@ -539,6 +540,10 @@ PyObject* PyInit_mcrfpy()
|
||||||
mcrfpydef::PyNoiseSourceType.tp_methods = PyNoiseSource::methods;
|
mcrfpydef::PyNoiseSourceType.tp_methods = PyNoiseSource::methods;
|
||||||
mcrfpydef::PyNoiseSourceType.tp_getset = PyNoiseSource::getsetters;
|
mcrfpydef::PyNoiseSourceType.tp_getset = PyNoiseSource::getsetters;
|
||||||
|
|
||||||
|
// Set up PyVoxelGridType methods and getsetters (Milestone 9)
|
||||||
|
mcrfpydef::PyVoxelGridType.tp_methods = PyVoxelGrid::methods;
|
||||||
|
mcrfpydef::PyVoxelGridType.tp_getset = PyVoxelGrid::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;
|
||||||
|
|
|
||||||
218
tests/demo/screens/voxel_meshing_demo.py
Normal file
218
tests/demo/screens/voxel_meshing_demo.py
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
# voxel_meshing_demo.py - Visual demo of VoxelGrid mesh rendering
|
||||||
|
# Shows voxel building rendered in Viewport3D with PS1 effects
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
|
||||||
|
# Create demo scene
|
||||||
|
scene = mcrfpy.Scene("voxel_meshing_demo")
|
||||||
|
|
||||||
|
# Dark background frame
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25))
|
||||||
|
scene.children.append(bg)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="VoxelGrid Meshing Demo - Face-Culled 3D Voxels", 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=(600, 500),
|
||||||
|
render_resolution=(320, 240), # PS1 resolution
|
||||||
|
fov=60.0,
|
||||||
|
camera_pos=(20.0, 15.0, 20.0),
|
||||||
|
camera_target=(4.0, 2.0, 4.0),
|
||||||
|
bg_color=mcrfpy.Color(50, 70, 100) # Sky color
|
||||||
|
)
|
||||||
|
scene.children.append(viewport)
|
||||||
|
|
||||||
|
# Create voxel grid for building
|
||||||
|
print("Creating voxel building...")
|
||||||
|
voxels = mcrfpy.VoxelGrid(size=(12, 8, 12), cell_size=1.0)
|
||||||
|
|
||||||
|
# Define materials
|
||||||
|
STONE = voxels.add_material("stone", color=mcrfpy.Color(128, 128, 128))
|
||||||
|
BRICK = voxels.add_material("brick", color=mcrfpy.Color(165, 82, 42))
|
||||||
|
WOOD = voxels.add_material("wood", color=mcrfpy.Color(139, 90, 43))
|
||||||
|
GLASS = voxels.add_material("glass", color=mcrfpy.Color(180, 220, 255, 180), transparent=True)
|
||||||
|
GRASS = voxels.add_material("grass", color=mcrfpy.Color(60, 150, 60))
|
||||||
|
|
||||||
|
print(f"Defined {voxels.material_count} materials")
|
||||||
|
|
||||||
|
# Build a simple house structure
|
||||||
|
|
||||||
|
# Ground/foundation
|
||||||
|
voxels.fill_box((0, 0, 0), (11, 0, 11), GRASS)
|
||||||
|
|
||||||
|
# Floor
|
||||||
|
voxels.fill_box((1, 1, 1), (10, 1, 10), STONE)
|
||||||
|
|
||||||
|
# Walls
|
||||||
|
# Front wall (Z=1)
|
||||||
|
voxels.fill_box((1, 2, 1), (10, 5, 1), BRICK)
|
||||||
|
# Back wall (Z=10)
|
||||||
|
voxels.fill_box((1, 2, 10), (10, 5, 10), BRICK)
|
||||||
|
# Left wall (X=1)
|
||||||
|
voxels.fill_box((1, 2, 1), (1, 5, 10), BRICK)
|
||||||
|
# Right wall (X=10)
|
||||||
|
voxels.fill_box((10, 2, 1), (10, 5, 10), BRICK)
|
||||||
|
|
||||||
|
# Door opening (front wall)
|
||||||
|
voxels.fill_box((4, 2, 1), (6, 4, 1), 0) # Clear door opening
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
# Front windows (beside door)
|
||||||
|
voxels.fill_box((2, 3, 1), (3, 4, 1), GLASS)
|
||||||
|
voxels.fill_box((8, 3, 1), (9, 4, 1), GLASS)
|
||||||
|
# Side windows
|
||||||
|
voxels.fill_box((1, 3, 4), (1, 4, 5), GLASS)
|
||||||
|
voxels.fill_box((1, 3, 7), (1, 4, 8), GLASS)
|
||||||
|
voxels.fill_box((10, 3, 4), (10, 4, 5), GLASS)
|
||||||
|
voxels.fill_box((10, 3, 7), (10, 4, 8), GLASS)
|
||||||
|
|
||||||
|
# Ceiling
|
||||||
|
voxels.fill_box((1, 6, 1), (10, 6, 10), WOOD)
|
||||||
|
|
||||||
|
# Simple roof (peaked)
|
||||||
|
voxels.fill_box((0, 7, 0), (11, 7, 11), WOOD)
|
||||||
|
voxels.fill_box((1, 8, 1), (10, 8, 10), WOOD)
|
||||||
|
voxels.fill_box((2, 9, 2), (9, 9, 9), WOOD)
|
||||||
|
voxels.fill_box((3, 10, 3), (8, 10, 8), WOOD)
|
||||||
|
voxels.fill_box((4, 11, 4), (7, 11, 7), WOOD)
|
||||||
|
|
||||||
|
# Build the mesh
|
||||||
|
voxels.rebuild_mesh()
|
||||||
|
|
||||||
|
print(f"Built voxel house:")
|
||||||
|
print(f" Non-air voxels: {voxels.count_non_air()}")
|
||||||
|
print(f" Vertices: {voxels.vertex_count}")
|
||||||
|
print(f" Faces: {voxels.vertex_count // 6}")
|
||||||
|
|
||||||
|
# Position the building
|
||||||
|
voxels.offset = (0.0, 0.0, 0.0)
|
||||||
|
voxels.rotation = 0.0
|
||||||
|
|
||||||
|
# Add to viewport
|
||||||
|
viewport.add_voxel_layer(voxels, z_index=0)
|
||||||
|
print(f"Added voxel layer to viewport (count: {viewport.voxel_layer_count()})")
|
||||||
|
|
||||||
|
# Create info panel
|
||||||
|
info_frame = mcrfpy.Frame(pos=(680, 60), size=(300, 250), fill_color=mcrfpy.Color(40, 40, 60, 200))
|
||||||
|
scene.children.append(info_frame)
|
||||||
|
|
||||||
|
info_title = mcrfpy.Caption(text="Building Stats", pos=(690, 70))
|
||||||
|
info_title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
scene.children.append(info_title)
|
||||||
|
|
||||||
|
stats_text = f"""Grid: {voxels.width}x{voxels.height}x{voxels.depth}
|
||||||
|
Total voxels: {voxels.width * voxels.height * voxels.depth}
|
||||||
|
Non-air: {voxels.count_non_air()}
|
||||||
|
Materials: {voxels.material_count}
|
||||||
|
Vertices: {voxels.vertex_count}
|
||||||
|
Faces: {voxels.vertex_count // 6}
|
||||||
|
|
||||||
|
Without culling would be:
|
||||||
|
{voxels.count_non_air() * 36} vertices
|
||||||
|
({100 - (voxels.vertex_count / (voxels.count_non_air() * 36) * 100):.0f}% reduction)"""
|
||||||
|
|
||||||
|
stats = mcrfpy.Caption(text=stats_text, pos=(690, 100))
|
||||||
|
stats.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
scene.children.append(stats)
|
||||||
|
|
||||||
|
# Controls info
|
||||||
|
controls_frame = mcrfpy.Frame(pos=(680, 330), size=(300, 180), fill_color=mcrfpy.Color(40, 40, 60, 200))
|
||||||
|
scene.children.append(controls_frame)
|
||||||
|
|
||||||
|
controls_title = mcrfpy.Caption(text="Controls", pos=(690, 340))
|
||||||
|
controls_title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
scene.children.append(controls_title)
|
||||||
|
|
||||||
|
controls_text = """R - Toggle rotation
|
||||||
|
1-5 - Change camera angle
|
||||||
|
SPACE - Reset camera
|
||||||
|
ESC - Exit demo"""
|
||||||
|
|
||||||
|
controls = mcrfpy.Caption(text=controls_text, pos=(690, 370))
|
||||||
|
controls.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
scene.children.append(controls)
|
||||||
|
|
||||||
|
# Animation state
|
||||||
|
rotation_enabled = False
|
||||||
|
current_angle = 0.0
|
||||||
|
camera_angles = [
|
||||||
|
(20.0, 15.0, 20.0), # Default - diagonal view
|
||||||
|
(0.0, 15.0, 25.0), # Front view
|
||||||
|
(25.0, 15.0, 0.0), # Side view
|
||||||
|
(5.5, 25.0, 5.5), # Top-down view
|
||||||
|
(5.5, 3.0, 20.0), # Low angle
|
||||||
|
]
|
||||||
|
current_camera = 0
|
||||||
|
|
||||||
|
def rotate_building(timer, runtime):
|
||||||
|
"""Timer callback for building rotation"""
|
||||||
|
global current_angle, rotation_enabled
|
||||||
|
if rotation_enabled:
|
||||||
|
current_angle += 1.0
|
||||||
|
if current_angle >= 360.0:
|
||||||
|
current_angle = 0.0
|
||||||
|
voxels.rotation = current_angle
|
||||||
|
|
||||||
|
# Set up rotation timer
|
||||||
|
timer = mcrfpy.Timer("rotate", rotate_building, 33) # ~30 FPS
|
||||||
|
|
||||||
|
def handle_key(key, action):
|
||||||
|
"""Keyboard handler"""
|
||||||
|
global rotation_enabled, current_camera
|
||||||
|
if action != mcrfpy.InputState.PRESSED:
|
||||||
|
return
|
||||||
|
|
||||||
|
if key == mcrfpy.Key.R:
|
||||||
|
rotation_enabled = not rotation_enabled
|
||||||
|
print(f"Rotation: {'ON' if rotation_enabled else 'OFF'}")
|
||||||
|
elif key == mcrfpy.Key.NUM_1:
|
||||||
|
current_camera = 0
|
||||||
|
viewport.camera_pos = camera_angles[0]
|
||||||
|
print("Camera: Default diagonal")
|
||||||
|
elif key == mcrfpy.Key.NUM_2:
|
||||||
|
current_camera = 1
|
||||||
|
viewport.camera_pos = camera_angles[1]
|
||||||
|
print("Camera: Front view")
|
||||||
|
elif key == mcrfpy.Key.NUM_3:
|
||||||
|
current_camera = 2
|
||||||
|
viewport.camera_pos = camera_angles[2]
|
||||||
|
print("Camera: Side view")
|
||||||
|
elif key == mcrfpy.Key.NUM_4:
|
||||||
|
current_camera = 3
|
||||||
|
viewport.camera_pos = camera_angles[3]
|
||||||
|
print("Camera: Top-down view")
|
||||||
|
elif key == mcrfpy.Key.NUM_5:
|
||||||
|
current_camera = 4
|
||||||
|
viewport.camera_pos = camera_angles[4]
|
||||||
|
print("Camera: Low angle")
|
||||||
|
elif key == mcrfpy.Key.SPACE:
|
||||||
|
current_camera = 0
|
||||||
|
voxels.rotation = 0.0
|
||||||
|
viewport.camera_pos = camera_angles[0]
|
||||||
|
print("Camera: Reset")
|
||||||
|
elif key == mcrfpy.Key.ESCAPE:
|
||||||
|
print("Exiting demo...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
scene.on_key = handle_key
|
||||||
|
|
||||||
|
# Activate the scene
|
||||||
|
mcrfpy.current_scene = scene
|
||||||
|
print("Voxel Meshing Demo ready! Press R to toggle rotation.")
|
||||||
|
|
||||||
|
# Main entry point for --exec mode
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Demo is set up, print summary
|
||||||
|
print("\n=== Voxel Meshing 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")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue