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 "Billboard.h"
|
||||
#include "Model3D.h"
|
||||
#include "VoxelGrid.h"
|
||||
#include "PyVoxelGrid.h"
|
||||
#include "../platform/GLContext.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyColor.h"
|
||||
|
|
@ -62,6 +64,15 @@ Viewport3D::Viewport3D(float x, float y, float width, float height)
|
|||
Viewport3D::~Viewport3D() {
|
||||
cleanupTestGeometry();
|
||||
cleanupFBO();
|
||||
|
||||
// Clean up voxel VBO (Milestone 10)
|
||||
#ifdef MCRF_HAS_GL
|
||||
if (voxelVBO_ != 0) {
|
||||
glDeleteBuffers(1, &voxelVBO_);
|
||||
voxelVBO_ = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (tcodMap_) {
|
||||
delete tcodMap_;
|
||||
tcodMap_ = nullptr;
|
||||
|
|
@ -836,6 +847,130 @@ void Viewport3D::renderMeshLayers() {
|
|||
#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() {
|
||||
// GL not available in current backend - skip 3D rendering
|
||||
if (!gl::isGLReady() || fbo_ == 0) {
|
||||
|
|
@ -879,9 +1014,12 @@ void Viewport3D::render3DContent() {
|
|||
// Render mesh layers first (terrain, etc.) - sorted by z_index
|
||||
renderMeshLayers();
|
||||
|
||||
// Render entities
|
||||
// Render voxel layers (Milestone 10)
|
||||
mat4 view = camera_.getViewMatrix();
|
||||
mat4 projection = camera_.getProjectionMatrix();
|
||||
renderVoxelLayers(view, projection);
|
||||
|
||||
// Render entities
|
||||
renderEntities(view, projection);
|
||||
|
||||
// Render billboards (after opaque geometry for proper transparency)
|
||||
|
|
@ -2262,6 +2400,81 @@ static PyObject* Viewport3D_follow(PyViewport3DObject* self, PyObject* args, PyO
|
|||
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
|
||||
|
||||
// Methods array - outside namespace but PyObjectType still in scope via typedef
|
||||
|
|
@ -2441,5 +2654,25 @@ PyMethodDef Viewport3D_methods[] = {
|
|||
" distance: Distance behind entity\n"
|
||||
" height: Camera height above entity\n"
|
||||
" 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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class Viewport3D;
|
|||
class Shader3D;
|
||||
class MeshLayer;
|
||||
class Billboard;
|
||||
class VoxelGrid;
|
||||
|
||||
} // namespace mcrf
|
||||
|
||||
|
|
@ -226,6 +227,29 @@ public:
|
|||
/// Render all billboards
|
||||
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
|
||||
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
|
||||
sf::Color getBackgroundColor() const { return bgColor_; }
|
||||
|
|
@ -319,6 +343,11 @@ private:
|
|||
// Billboard storage
|
||||
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
|
||||
std::unique_ptr<Shader3D> shader_;
|
||||
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/Model3D.h" // 3D model resource
|
||||
#include "3d/Billboard.h" // Billboard sprites
|
||||
#include "3d/PyVoxelGrid.h" // Voxel grid for 3D structures (Milestone 9)
|
||||
#include "McRogueFaceVersion.h"
|
||||
#include "GameEngine.h"
|
||||
// ImGui is only available for SFML builds
|
||||
|
|
@ -442,7 +443,7 @@ PyObject* PyInit_mcrfpy()
|
|||
/*3D entities*/
|
||||
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
||||
&mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType,
|
||||
&mcrfpydef::PyBillboardType,
|
||||
&mcrfpydef::PyBillboardType, &mcrfpydef::PyVoxelGridType,
|
||||
|
||||
/*grid layers (#147)*/
|
||||
&PyColorLayerType, &PyTileLayerType,
|
||||
|
|
@ -539,6 +540,10 @@ PyObject* PyInit_mcrfpy()
|
|||
mcrfpydef::PyNoiseSourceType.tp_methods = PyNoiseSource::methods;
|
||||
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)
|
||||
mcrfpydef::PyShaderType.tp_methods = PyShader::methods;
|
||||
mcrfpydef::PyShaderType.tp_getset = PyShader::getsetters;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue