pathfinding on heightmap
This commit is contained in:
parent
e572269eac
commit
63008bdefd
6 changed files with 1350 additions and 0 deletions
|
|
@ -54,6 +54,10 @@ Viewport3D::Viewport3D(float x, float y, float width, float height)
|
|||
Viewport3D::~Viewport3D() {
|
||||
cleanupTestGeometry();
|
||||
cleanupFBO();
|
||||
if (tcodMap_) {
|
||||
delete tcodMap_;
|
||||
tcodMap_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -210,6 +214,215 @@ bool Viewport3D::removeLayer(const std::string& name) {
|
|||
return false;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Navigation Grid (VoxelPoint System)
|
||||
// =============================================================================
|
||||
|
||||
void Viewport3D::setGridSize(int width, int depth) {
|
||||
if (width <= 0 || depth <= 0) {
|
||||
throw std::invalid_argument("Grid dimensions must be positive");
|
||||
}
|
||||
|
||||
gridWidth_ = width;
|
||||
gridDepth_ = depth;
|
||||
|
||||
// Resize and initialize grid
|
||||
navGrid_.resize(width * depth);
|
||||
for (int z = 0; z < depth; z++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int idx = z * width + x;
|
||||
navGrid_[idx] = VoxelPoint(x, z, this);
|
||||
}
|
||||
}
|
||||
|
||||
// Create/recreate TCODMap
|
||||
if (tcodMap_) {
|
||||
delete tcodMap_;
|
||||
}
|
||||
tcodMap_ = new TCODMap(width, depth);
|
||||
|
||||
// Sync initial state
|
||||
syncToTCOD();
|
||||
}
|
||||
|
||||
VoxelPoint& Viewport3D::at(int x, int z) {
|
||||
if (!isValidCell(x, z)) {
|
||||
throw std::out_of_range("Grid coordinates out of range");
|
||||
}
|
||||
return navGrid_[z * gridWidth_ + x];
|
||||
}
|
||||
|
||||
const VoxelPoint& Viewport3D::at(int x, int z) const {
|
||||
if (!isValidCell(x, z)) {
|
||||
throw std::out_of_range("Grid coordinates out of range");
|
||||
}
|
||||
return navGrid_[z * gridWidth_ + x];
|
||||
}
|
||||
|
||||
bool Viewport3D::isValidCell(int x, int z) const {
|
||||
return x >= 0 && x < gridWidth_ && z >= 0 && z < gridDepth_;
|
||||
}
|
||||
|
||||
void Viewport3D::syncToTCOD() {
|
||||
if (!tcodMap_) return;
|
||||
|
||||
for (int z = 0; z < gridDepth_; z++) {
|
||||
for (int x = 0; x < gridWidth_; x++) {
|
||||
const VoxelPoint& vp = at(x, z);
|
||||
tcodMap_->setProperties(x, z, vp.transparent, vp.walkable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport3D::syncTCODCell(int x, int z) {
|
||||
if (!tcodMap_ || !isValidCell(x, z)) return;
|
||||
|
||||
const VoxelPoint& vp = at(x, z);
|
||||
tcodMap_->setProperties(x, z, vp.transparent, vp.walkable);
|
||||
}
|
||||
|
||||
void Viewport3D::applyHeightmap(TCOD_heightmap_t* hm, float yScale) {
|
||||
if (!hm) return;
|
||||
|
||||
// Ensure grid matches heightmap dimensions
|
||||
if (gridWidth_ != hm->w || gridDepth_ != hm->h) {
|
||||
setGridSize(hm->w, hm->h);
|
||||
}
|
||||
|
||||
// Apply heights
|
||||
for (int z = 0; z < gridDepth_; z++) {
|
||||
for (int x = 0; x < gridWidth_; x++) {
|
||||
int idx = z * hm->w + x;
|
||||
navGrid_[z * gridWidth_ + x].height = hm->values[idx] * yScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport3D::applyThreshold(TCOD_heightmap_t* hm, float minHeight, float maxHeight, bool walkable) {
|
||||
if (!hm) return;
|
||||
|
||||
// Grid must match heightmap dimensions
|
||||
if (gridWidth_ != hm->w || gridDepth_ != hm->h) {
|
||||
return; // Dimension mismatch
|
||||
}
|
||||
|
||||
for (int z = 0; z < gridDepth_; z++) {
|
||||
for (int x = 0; x < gridWidth_; x++) {
|
||||
int idx = z * hm->w + x;
|
||||
float h = hm->values[idx];
|
||||
if (h >= minHeight && h <= maxHeight) {
|
||||
navGrid_[z * gridWidth_ + x].walkable = walkable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncToTCOD();
|
||||
}
|
||||
|
||||
void Viewport3D::setSlopeCost(float maxSlope, float costMultiplier) {
|
||||
if (gridWidth_ < 2 || gridDepth_ < 2) return;
|
||||
|
||||
// Neighbor offsets (4-directional)
|
||||
const int dx[] = {-1, 1, 0, 0};
|
||||
const int dz[] = {0, 0, -1, 1};
|
||||
|
||||
for (int z = 0; z < gridDepth_; z++) {
|
||||
for (int x = 0; x < gridWidth_; x++) {
|
||||
VoxelPoint& vp = navGrid_[z * gridWidth_ + x];
|
||||
float maxNeighborDiff = 0.0f;
|
||||
|
||||
// Check all neighbors
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nx = x + dx[i];
|
||||
int nz = z + dz[i];
|
||||
if (isValidCell(nx, nz)) {
|
||||
float diff = std::abs(vp.height - at(nx, nz).height);
|
||||
maxNeighborDiff = std::max(maxNeighborDiff, diff);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark unwalkable if too steep, otherwise set cost
|
||||
if (maxNeighborDiff > maxSlope) {
|
||||
vp.walkable = false;
|
||||
} else {
|
||||
vp.cost = 1.0f + maxNeighborDiff * costMultiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncToTCOD();
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, int>> Viewport3D::findPath(int startX, int startZ, int endX, int endZ) {
|
||||
std::vector<std::pair<int, int>> result;
|
||||
|
||||
if (!tcodMap_ || !isValidCell(startX, startZ) || !isValidCell(endX, endZ)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ensure TCOD is synced
|
||||
syncToTCOD();
|
||||
|
||||
// Create path with cost callback
|
||||
struct PathUserData {
|
||||
Viewport3D* viewport;
|
||||
};
|
||||
PathUserData userData = {this};
|
||||
|
||||
// Use TCODPath with diagonal movement
|
||||
TCODPath path(tcodMap_, 1.41f);
|
||||
|
||||
// Compute path
|
||||
if (!path.compute(startX, startZ, endX, endZ)) {
|
||||
return result; // No path found
|
||||
}
|
||||
|
||||
// Extract path
|
||||
int x, z;
|
||||
while (path.walk(&x, &z, true)) {
|
||||
result.push_back({x, z});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, int>> Viewport3D::computeFOV(int originX, int originZ, int radius) {
|
||||
std::vector<std::pair<int, int>> visible;
|
||||
|
||||
if (!tcodMap_ || !isValidCell(originX, originZ)) {
|
||||
return visible;
|
||||
}
|
||||
|
||||
// Thread-safe FOV computation
|
||||
std::lock_guard<std::mutex> lock(fovMutex_);
|
||||
|
||||
// Ensure TCOD is synced
|
||||
syncToTCOD();
|
||||
|
||||
// Compute FOV
|
||||
tcodMap_->computeFov(originX, originZ, radius, true, FOV_BASIC);
|
||||
|
||||
// Collect visible cells
|
||||
for (int z = 0; z < gridDepth_; z++) {
|
||||
for (int x = 0; x < gridWidth_; x++) {
|
||||
if (tcodMap_->isInFov(x, z)) {
|
||||
visible.push_back({x, z});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
bool Viewport3D::isInFOV(int x, int z) const {
|
||||
if (!tcodMap_ || !isValidCell(x, z)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(fovMutex_);
|
||||
return tcodMap_->isInFov(x, z);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FBO Management
|
||||
// =============================================================================
|
||||
|
|
@ -862,6 +1075,56 @@ static int Viewport3D_set_fog_far(PyViewport3DObject* self, PyObject* value, voi
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Navigation grid property getters/setters
|
||||
static PyObject* Viewport3D_get_grid_size_prop(PyViewport3DObject* self, void* closure) {
|
||||
return Py_BuildValue("(ii)", self->data->getGridWidth(), self->data->getGridDepth());
|
||||
}
|
||||
|
||||
static int Viewport3D_set_grid_size_prop(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||
if (!PyTuple_Check(value) || PyTuple_Size(value) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple of (width, depth)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int width, depth;
|
||||
if (!PyArg_ParseTuple(value, "ii", &width, &depth)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
try {
|
||||
self->data->setGridSize(width, depth);
|
||||
} catch (const std::exception& e) {
|
||||
PyErr_SetString(PyExc_ValueError, e.what());
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_get_cell_size_prop(PyViewport3DObject* self, void* closure) {
|
||||
return PyFloat_FromDouble(self->data->getCellSize());
|
||||
}
|
||||
|
||||
static int Viewport3D_set_cell_size_prop(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||
double size;
|
||||
if (PyFloat_Check(value)) {
|
||||
size = PyFloat_AsDouble(value);
|
||||
} else if (PyLong_Check(value)) {
|
||||
size = static_cast<double>(PyLong_AsLong(value));
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "cell_size must be a number");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (size <= 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "cell_size must be positive");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data->setCellSize(static_cast<float>(size));
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyGetSetDef Viewport3D::getsetters[] = {
|
||||
// Position and size
|
||||
{"x", (getter)Viewport3D_get_x, (setter)Viewport3D_set_x,
|
||||
|
|
@ -909,6 +1172,12 @@ PyGetSetDef Viewport3D::getsetters[] = {
|
|||
{"fog_far", (getter)Viewport3D_get_fog_far, (setter)Viewport3D_set_fog_far,
|
||||
MCRF_PROPERTY(fog_far, "Fog end distance."), NULL},
|
||||
|
||||
// Navigation grid properties
|
||||
{"grid_size", (getter)Viewport3D_get_grid_size_prop, (setter)Viewport3D_set_grid_size_prop,
|
||||
MCRF_PROPERTY(grid_size, "Navigation grid dimensions as (width, depth) tuple."), NULL},
|
||||
{"cell_size", (getter)Viewport3D_get_cell_size_prop, (setter)Viewport3D_set_cell_size_prop,
|
||||
MCRF_PROPERTY(cell_size, "World units per navigation grid cell."), NULL},
|
||||
|
||||
// Common UIDrawable properties
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIVIEWPORT3D),
|
||||
|
|
@ -1238,6 +1507,232 @@ static PyObject* Viewport3D_layer_count(PyViewport3DObject* self, PyObject* Py_U
|
|||
return PyLong_FromSize_t(self->data->getLayerCount());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Navigation Grid Python Methods
|
||||
// =============================================================================
|
||||
|
||||
static PyObject* Viewport3D_set_grid_size(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"width", "depth", NULL};
|
||||
int width = 0;
|
||||
int depth = 0;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast<char**>(kwlist),
|
||||
&width, &depth)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
try {
|
||||
self->data->setGridSize(width, depth);
|
||||
} catch (const std::exception& e) {
|
||||
PyErr_SetString(PyExc_ValueError, e.what());
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_at(PyViewport3DObject* self, PyObject* args) {
|
||||
int x, z;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &z)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!self->data->isValidCell(x, z)) {
|
||||
PyErr_Format(PyExc_IndexError, "Grid coordinates (%d, %d) out of range", x, z);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create Python VoxelPoint wrapper using tp_alloc to properly construct shared_ptr
|
||||
auto type = &mcrfpydef::PyVoxelPointType;
|
||||
auto vp_obj = (PyVoxelPointObject*)type->tp_alloc(type, 0);
|
||||
if (!vp_obj) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
vp_obj->data = &(self->data->at(x, z));
|
||||
vp_obj->viewport = self->data;
|
||||
|
||||
return (PyObject*)vp_obj;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_apply_heightmap(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"heightmap", "y_scale", NULL};
|
||||
PyObject* hm_obj = nullptr;
|
||||
float y_scale = 1.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(kwlist),
|
||||
&hm_obj, &y_scale)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Validate HeightMap type
|
||||
PyObject* heightmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap");
|
||||
if (!heightmap_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!PyObject_IsInstance(hm_obj, heightmap_type)) {
|
||||
Py_DECREF(heightmap_type);
|
||||
PyErr_SetString(PyExc_TypeError, "heightmap must be a HeightMap object");
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(heightmap_type);
|
||||
|
||||
PyHeightMapObject* hm = reinterpret_cast<PyHeightMapObject*>(hm_obj);
|
||||
if (!hm->heightmap) {
|
||||
PyErr_SetString(PyExc_ValueError, "HeightMap has no data");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->data->applyHeightmap(hm->heightmap, y_scale);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_apply_threshold(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"heightmap", "min_height", "max_height", "walkable", NULL};
|
||||
PyObject* hm_obj = nullptr;
|
||||
float min_height = 0.0f;
|
||||
float max_height = 1.0f;
|
||||
int walkable = 1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Off|p", const_cast<char**>(kwlist),
|
||||
&hm_obj, &min_height, &max_height, &walkable)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Validate HeightMap type
|
||||
PyObject* heightmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap");
|
||||
if (!heightmap_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!PyObject_IsInstance(hm_obj, heightmap_type)) {
|
||||
Py_DECREF(heightmap_type);
|
||||
PyErr_SetString(PyExc_TypeError, "heightmap must be a HeightMap object");
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(heightmap_type);
|
||||
|
||||
PyHeightMapObject* hm = reinterpret_cast<PyHeightMapObject*>(hm_obj);
|
||||
if (!hm->heightmap) {
|
||||
PyErr_SetString(PyExc_ValueError, "HeightMap has no data");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->data->applyThreshold(hm->heightmap, min_height, max_height, walkable != 0);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_set_slope_cost(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"max_slope", "cost_multiplier", NULL};
|
||||
float max_slope = 0.5f;
|
||||
float cost_multiplier = 1.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ff", const_cast<char**>(kwlist),
|
||||
&max_slope, &cost_multiplier)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->data->setSlopeCost(max_slope, cost_multiplier);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_find_path(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"start", "end", NULL};
|
||||
PyObject* start_obj = nullptr;
|
||||
PyObject* end_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", const_cast<char**>(kwlist),
|
||||
&start_obj, &end_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse start tuple
|
||||
int start_x, start_z;
|
||||
if (!PyArg_ParseTuple(start_obj, "ii", &start_x, &start_z)) {
|
||||
PyErr_SetString(PyExc_TypeError, "start must be a tuple of (x, z) integers");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse end tuple
|
||||
int end_x, end_z;
|
||||
if (!PyArg_ParseTuple(end_obj, "ii", &end_x, &end_z)) {
|
||||
PyErr_SetString(PyExc_TypeError, "end must be a tuple of (x, z) integers");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Find path
|
||||
std::vector<std::pair<int, int>> path = self->data->findPath(start_x, start_z, end_x, end_z);
|
||||
|
||||
// Convert to Python list
|
||||
PyObject* result = PyList_New(path.size());
|
||||
if (!result) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < path.size(); i++) {
|
||||
PyObject* tuple = Py_BuildValue("(ii)", path[i].first, path[i].second);
|
||||
if (!tuple) {
|
||||
Py_DECREF(result);
|
||||
return NULL;
|
||||
}
|
||||
PyList_SET_ITEM(result, i, tuple);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_compute_fov(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"origin", "radius", NULL};
|
||||
PyObject* origin_obj = nullptr;
|
||||
int radius = 10;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|i", const_cast<char**>(kwlist),
|
||||
&origin_obj, &radius)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse origin tuple
|
||||
int origin_x, origin_z;
|
||||
if (!PyArg_ParseTuple(origin_obj, "ii", &origin_x, &origin_z)) {
|
||||
PyErr_SetString(PyExc_TypeError, "origin must be a tuple of (x, z) integers");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Compute FOV
|
||||
std::vector<std::pair<int, int>> visible = self->data->computeFOV(origin_x, origin_z, radius);
|
||||
|
||||
// Convert to Python list
|
||||
PyObject* result = PyList_New(visible.size());
|
||||
if (!result) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < visible.size(); i++) {
|
||||
PyObject* tuple = Py_BuildValue("(ii)", visible[i].first, visible[i].second);
|
||||
if (!tuple) {
|
||||
Py_DECREF(result);
|
||||
return NULL;
|
||||
}
|
||||
PyList_SET_ITEM(result, i, tuple);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_is_in_fov(PyViewport3DObject* self, PyObject* args) {
|
||||
int x, z;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &z)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(self->data->isInFOV(x, z));
|
||||
}
|
||||
|
||||
} // namespace mcrf
|
||||
|
||||
// Methods array - outside namespace but PyObjectType still in scope via typedef
|
||||
|
|
@ -1286,5 +1781,65 @@ PyMethodDef Viewport3D_methods[] = {
|
|||
{"layer_count", (PyCFunction)mcrf::Viewport3D_layer_count, METH_NOARGS,
|
||||
"layer_count() -> int\n\n"
|
||||
"Get the number of mesh layers."},
|
||||
|
||||
// Navigation grid methods
|
||||
{"set_grid_size", (PyCFunction)mcrf::Viewport3D_set_grid_size, METH_VARARGS | METH_KEYWORDS,
|
||||
"set_grid_size(width, depth)\n\n"
|
||||
"Initialize navigation grid with specified dimensions.\n\n"
|
||||
"Args:\n"
|
||||
" width: Grid width (X axis)\n"
|
||||
" depth: Grid depth (Z axis)"},
|
||||
{"at", (PyCFunction)mcrf::Viewport3D_at, METH_VARARGS,
|
||||
"at(x, z) -> VoxelPoint\n\n"
|
||||
"Get VoxelPoint at grid coordinates.\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate in grid\n"
|
||||
" z: Z coordinate in grid\n\n"
|
||||
"Returns:\n"
|
||||
" VoxelPoint object for the cell"},
|
||||
{"apply_heightmap", (PyCFunction)mcrf::Viewport3D_apply_heightmap, METH_VARARGS | METH_KEYWORDS,
|
||||
"apply_heightmap(heightmap, y_scale=1.0)\n\n"
|
||||
"Set cell heights from HeightMap.\n\n"
|
||||
"Args:\n"
|
||||
" heightmap: HeightMap object\n"
|
||||
" y_scale: Vertical scale factor"},
|
||||
{"apply_threshold", (PyCFunction)mcrf::Viewport3D_apply_threshold, METH_VARARGS | METH_KEYWORDS,
|
||||
"apply_threshold(heightmap, min_height, max_height, walkable=True)\n\n"
|
||||
"Set cell walkability based on height thresholds.\n\n"
|
||||
"Args:\n"
|
||||
" heightmap: HeightMap object\n"
|
||||
" min_height: Minimum height (0-1)\n"
|
||||
" max_height: Maximum height (0-1)\n"
|
||||
" walkable: Walkability value for cells in range"},
|
||||
{"set_slope_cost", (PyCFunction)mcrf::Viewport3D_set_slope_cost, METH_VARARGS | METH_KEYWORDS,
|
||||
"set_slope_cost(max_slope=0.5, cost_multiplier=1.0)\n\n"
|
||||
"Calculate slope costs and mark steep cells unwalkable.\n\n"
|
||||
"Args:\n"
|
||||
" max_slope: Maximum height difference before marking unwalkable\n"
|
||||
" cost_multiplier: Cost increase per unit slope"},
|
||||
{"find_path", (PyCFunction)mcrf::Viewport3D_find_path, METH_VARARGS | METH_KEYWORDS,
|
||||
"find_path(start, end) -> list\n\n"
|
||||
"Find A* path between two points.\n\n"
|
||||
"Args:\n"
|
||||
" start: Starting point as (x, z) tuple\n"
|
||||
" end: End point as (x, z) tuple\n\n"
|
||||
"Returns:\n"
|
||||
" List of (x, z) tuples forming the path, or empty list if no path"},
|
||||
{"compute_fov", (PyCFunction)mcrf::Viewport3D_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
"compute_fov(origin, radius=10) -> list\n\n"
|
||||
"Compute field of view from a position.\n\n"
|
||||
"Args:\n"
|
||||
" origin: Origin point as (x, z) tuple\n"
|
||||
" radius: FOV radius\n\n"
|
||||
"Returns:\n"
|
||||
" List of visible (x, z) positions"},
|
||||
{"is_in_fov", (PyCFunction)mcrf::Viewport3D_is_in_fov, METH_VARARGS,
|
||||
"is_in_fov(x, z) -> bool\n\n"
|
||||
"Check if a cell is in the current FOV (after compute_fov).\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate\n"
|
||||
" z: Z coordinate\n\n"
|
||||
"Returns:\n"
|
||||
" True if the cell is visible"},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@
|
|||
#include "PyDrawable.h"
|
||||
#include "Math3D.h"
|
||||
#include "Camera3D.h"
|
||||
#include "VoxelPoint.h"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <libtcod.h>
|
||||
|
||||
namespace mcrf {
|
||||
|
||||
|
|
@ -99,6 +102,75 @@ public:
|
|||
/// Get number of layers
|
||||
size_t getLayerCount() const { return meshLayers_.size(); }
|
||||
|
||||
// =========================================================================
|
||||
// Navigation Grid (VoxelPoint System)
|
||||
// =========================================================================
|
||||
|
||||
/// Set navigation grid dimensions
|
||||
/// @param width Grid width (X axis)
|
||||
/// @param depth Grid depth (Z axis)
|
||||
void setGridSize(int width, int depth);
|
||||
|
||||
/// Get grid dimensions
|
||||
int getGridWidth() const { return gridWidth_; }
|
||||
int getGridDepth() const { return gridDepth_; }
|
||||
|
||||
/// Access a VoxelPoint at grid coordinates
|
||||
/// @throws std::out_of_range if coordinates are invalid
|
||||
VoxelPoint& at(int x, int z);
|
||||
const VoxelPoint& at(int x, int z) const;
|
||||
|
||||
/// Check if coordinates are within grid bounds
|
||||
bool isValidCell(int x, int z) const;
|
||||
|
||||
/// Set cell size (world units per grid cell)
|
||||
void setCellSize(float size) { cellSize_ = size; }
|
||||
float getCellSize() const { return cellSize_; }
|
||||
|
||||
/// Synchronize all cells to libtcod TCODMap
|
||||
void syncToTCOD();
|
||||
|
||||
/// Synchronize a single cell to TCODMap
|
||||
void syncTCODCell(int x, int z);
|
||||
|
||||
/// Apply heights from HeightMap to navigation grid
|
||||
/// @param hm HeightMap to read heights from
|
||||
/// @param yScale Scale factor for Y values
|
||||
void applyHeightmap(TCOD_heightmap_t* hm, float yScale);
|
||||
|
||||
/// Set cell walkability by height threshold
|
||||
/// @param hm HeightMap to sample
|
||||
/// @param minHeight Minimum height for threshold
|
||||
/// @param maxHeight Maximum height for threshold
|
||||
/// @param walkable Walkability value to set for cells in range
|
||||
void applyThreshold(TCOD_heightmap_t* hm, float minHeight, float maxHeight, bool walkable);
|
||||
|
||||
/// Calculate slope costs and mark steep cells unwalkable
|
||||
/// @param maxSlope Maximum height difference before marking unwalkable
|
||||
/// @param costMultiplier Cost increase per unit slope
|
||||
void setSlopeCost(float maxSlope, float costMultiplier);
|
||||
|
||||
/// Find path using A* pathfinding
|
||||
/// @param startX Start X coordinate
|
||||
/// @param startZ Start Z coordinate
|
||||
/// @param endX End X coordinate
|
||||
/// @param endZ End Z coordinate
|
||||
/// @return Vector of (x, z) positions, or empty if no path
|
||||
std::vector<std::pair<int, int>> findPath(int startX, int startZ, int endX, int endZ);
|
||||
|
||||
/// Compute field of view from a position
|
||||
/// @param originX Origin X coordinate
|
||||
/// @param originZ Origin Z coordinate
|
||||
/// @param radius FOV radius
|
||||
/// @return Set of visible (x, z) positions
|
||||
std::vector<std::pair<int, int>> computeFOV(int originX, int originZ, int radius);
|
||||
|
||||
/// Check if a cell is in current FOV (after computeFOV call)
|
||||
bool isInFOV(int x, int z) const;
|
||||
|
||||
/// Get TCODMap pointer (for advanced usage)
|
||||
TCODMap* getTCODMap() const { return tcodMap_; }
|
||||
|
||||
// Background color
|
||||
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
|
||||
sf::Color getBackgroundColor() const { return bgColor_; }
|
||||
|
|
@ -174,6 +246,14 @@ private:
|
|||
// Mesh layers for terrain, static geometry
|
||||
std::vector<std::shared_ptr<MeshLayer>> meshLayers_;
|
||||
|
||||
// Navigation grid (VoxelPoint system)
|
||||
std::vector<VoxelPoint> navGrid_;
|
||||
int gridWidth_ = 0;
|
||||
int gridDepth_ = 0;
|
||||
float cellSize_ = 1.0f;
|
||||
TCODMap* tcodMap_ = nullptr;
|
||||
mutable std::mutex fovMutex_;
|
||||
|
||||
// Shader for PS1-style rendering
|
||||
std::unique_ptr<Shader3D> shader_;
|
||||
|
||||
|
|
|
|||
225
src/3d/VoxelPoint.cpp
Normal file
225
src/3d/VoxelPoint.cpp
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
// VoxelPoint.cpp - Navigation grid cell implementation
|
||||
|
||||
#include "VoxelPoint.h"
|
||||
#include "Viewport3D.h"
|
||||
#include <cstdio>
|
||||
|
||||
// Default constructor
|
||||
VoxelPoint::VoxelPoint()
|
||||
: walkable(true)
|
||||
, transparent(true)
|
||||
, height(0.0f)
|
||||
, cost(1.0f)
|
||||
, grid_x(0)
|
||||
, grid_z(0)
|
||||
, parent_viewport(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
// Constructor with position
|
||||
VoxelPoint::VoxelPoint(int x, int z, mcrf::Viewport3D* parent)
|
||||
: walkable(true)
|
||||
, transparent(true)
|
||||
, height(0.0f)
|
||||
, cost(1.0f)
|
||||
, grid_x(x)
|
||||
, grid_z(z)
|
||||
, parent_viewport(parent)
|
||||
{
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Python Property Accessors
|
||||
// =============================================================================
|
||||
|
||||
// Member offsets for bool properties
|
||||
enum VoxelPointBoolMember {
|
||||
VOXEL_WALKABLE = 0,
|
||||
VOXEL_TRANSPARENT = 1
|
||||
};
|
||||
|
||||
// Member offsets for float properties
|
||||
enum VoxelPointFloatMember {
|
||||
VOXEL_HEIGHT = 0,
|
||||
VOXEL_COST = 1
|
||||
};
|
||||
|
||||
PyObject* VoxelPoint::get_bool_member(PyVoxelPointObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "VoxelPoint data is null");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
long member = reinterpret_cast<long>(closure);
|
||||
bool value = false;
|
||||
|
||||
switch (member) {
|
||||
case VOXEL_WALKABLE:
|
||||
value = self->data->walkable;
|
||||
break;
|
||||
case VOXEL_TRANSPARENT:
|
||||
value = self->data->transparent;
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_AttributeError, "Invalid bool member");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(value ? 1 : 0);
|
||||
}
|
||||
|
||||
int VoxelPoint::set_bool_member(PyVoxelPointObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "VoxelPoint data is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyBool_Check(value) && !PyLong_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Value must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool newValue = PyObject_IsTrue(value);
|
||||
long member = reinterpret_cast<long>(closure);
|
||||
|
||||
switch (member) {
|
||||
case VOXEL_WALKABLE:
|
||||
self->data->walkable = newValue;
|
||||
break;
|
||||
case VOXEL_TRANSPARENT:
|
||||
self->data->transparent = newValue;
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_AttributeError, "Invalid bool member");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Trigger TCOD synchronization
|
||||
if (self->data->parent_viewport) {
|
||||
self->data->parent_viewport->syncTCODCell(self->data->grid_x, self->data->grid_z);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* VoxelPoint::get_float_member(PyVoxelPointObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "VoxelPoint data is null");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
long member = reinterpret_cast<long>(closure);
|
||||
float value = 0.0f;
|
||||
|
||||
switch (member) {
|
||||
case VOXEL_HEIGHT:
|
||||
value = self->data->height;
|
||||
break;
|
||||
case VOXEL_COST:
|
||||
value = self->data->cost;
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_AttributeError, "Invalid float member");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PyFloat_FromDouble(value);
|
||||
}
|
||||
|
||||
int VoxelPoint::set_float_member(PyVoxelPointObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "VoxelPoint data is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
double newValue;
|
||||
if (PyFloat_Check(value)) {
|
||||
newValue = PyFloat_AsDouble(value);
|
||||
} else if (PyLong_Check(value)) {
|
||||
newValue = static_cast<double>(PyLong_AsLong(value));
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "Value must be a number");
|
||||
return -1;
|
||||
}
|
||||
|
||||
long member = reinterpret_cast<long>(closure);
|
||||
|
||||
switch (member) {
|
||||
case VOXEL_HEIGHT:
|
||||
self->data->height = static_cast<float>(newValue);
|
||||
break;
|
||||
case VOXEL_COST:
|
||||
if (newValue < 0.0) {
|
||||
PyErr_SetString(PyExc_ValueError, "Cost must be non-negative");
|
||||
return -1;
|
||||
}
|
||||
self->data->cost = static_cast<float>(newValue);
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_AttributeError, "Invalid float member");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* VoxelPoint::get_grid_pos(PyVoxelPointObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "VoxelPoint data is null");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_z);
|
||||
}
|
||||
|
||||
PyObject* VoxelPoint::get_entities(PyVoxelPointObject* self, void* closure)
|
||||
{
|
||||
// TODO: Implement when Entity3D is created
|
||||
// For now, return an empty list
|
||||
return PyList_New(0);
|
||||
}
|
||||
|
||||
PyObject* VoxelPoint::repr(PyVoxelPointObject* self)
|
||||
{
|
||||
if (!self->data) {
|
||||
return PyUnicode_FromString("<VoxelPoint (null)>");
|
||||
}
|
||||
|
||||
// Use snprintf then PyUnicode_FromString because PyUnicode_FromFormat doesn't support %.2f
|
||||
char buffer[128];
|
||||
snprintf(buffer, sizeof(buffer),
|
||||
"<VoxelPoint (%d, %d) walkable=%s transparent=%s height=%.2f cost=%.2f>",
|
||||
self->data->grid_x,
|
||||
self->data->grid_z,
|
||||
self->data->walkable ? "True" : "False",
|
||||
self->data->transparent ? "True" : "False",
|
||||
self->data->height,
|
||||
self->data->cost
|
||||
);
|
||||
return PyUnicode_FromString(buffer);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Python GetSetDef Table
|
||||
// =============================================================================
|
||||
|
||||
PyGetSetDef VoxelPoint::getsetters[] = {
|
||||
{"walkable", (getter)VoxelPoint::get_bool_member, (setter)VoxelPoint::set_bool_member,
|
||||
"Whether entities can walk through this cell.", (void*)VOXEL_WALKABLE},
|
||||
{"transparent", (getter)VoxelPoint::get_bool_member, (setter)VoxelPoint::set_bool_member,
|
||||
"Whether FOV can see through this cell.", (void*)VOXEL_TRANSPARENT},
|
||||
{"height", (getter)VoxelPoint::get_float_member, (setter)VoxelPoint::set_float_member,
|
||||
"World-space Y coordinate from terrain.", (void*)VOXEL_HEIGHT},
|
||||
{"cost", (getter)VoxelPoint::get_float_member, (setter)VoxelPoint::set_float_member,
|
||||
"Movement cost multiplier (1.0 = normal).", (void*)VOXEL_COST},
|
||||
{"grid_pos", (getter)VoxelPoint::get_grid_pos, NULL,
|
||||
"Grid coordinates as (x, z) tuple (read-only).", NULL},
|
||||
{"entities", (getter)VoxelPoint::get_entities, NULL,
|
||||
"List of Entity3D objects at this cell (read-only).", NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
94
src/3d/VoxelPoint.h
Normal file
94
src/3d/VoxelPoint.h
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// VoxelPoint.h - Navigation grid cell for 3D terrain
|
||||
// Provides walkability, transparency, and cost data for pathfinding and FOV
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "structmember.h"
|
||||
#include <vector>
|
||||
|
||||
// Forward declarations
|
||||
namespace mcrf {
|
||||
class Viewport3D;
|
||||
class Entity3D;
|
||||
}
|
||||
|
||||
class VoxelPoint;
|
||||
|
||||
// Python object struct for VoxelPoint
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
VoxelPoint* data;
|
||||
std::shared_ptr<mcrf::Viewport3D> viewport;
|
||||
} PyVoxelPointObject;
|
||||
|
||||
// VoxelPoint - navigation grid cell for 3D terrain
|
||||
// Similar to UIGridPoint but with height and cost for 3D pathfinding
|
||||
class VoxelPoint {
|
||||
public:
|
||||
// Navigation properties
|
||||
bool walkable = true; // Can entities walk through this cell?
|
||||
bool transparent = true; // Can FOV see through this cell?
|
||||
float height = 0.0f; // World-space Y coordinate from terrain
|
||||
float cost = 1.0f; // Movement cost multiplier (1.0 = normal)
|
||||
|
||||
// Position in parent grid
|
||||
int grid_x = 0;
|
||||
int grid_z = 0;
|
||||
|
||||
// Parent viewport reference for TCOD synchronization
|
||||
mcrf::Viewport3D* parent_viewport = nullptr;
|
||||
|
||||
// Default constructor
|
||||
VoxelPoint();
|
||||
|
||||
// Constructor with position
|
||||
VoxelPoint(int x, int z, mcrf::Viewport3D* parent);
|
||||
|
||||
// Python property accessors
|
||||
static PyGetSetDef getsetters[];
|
||||
|
||||
// Bool property getter/setter (walkable, transparent)
|
||||
static PyObject* get_bool_member(PyVoxelPointObject* self, void* closure);
|
||||
static int set_bool_member(PyVoxelPointObject* self, PyObject* value, void* closure);
|
||||
|
||||
// Float property getter/setter (height, cost)
|
||||
static PyObject* get_float_member(PyVoxelPointObject* self, void* closure);
|
||||
static int set_float_member(PyVoxelPointObject* self, PyObject* value, void* closure);
|
||||
|
||||
// grid_pos property (read-only tuple)
|
||||
static PyObject* get_grid_pos(PyVoxelPointObject* self, void* closure);
|
||||
|
||||
// entities property (read-only list of Entity3D at this cell)
|
||||
static PyObject* get_entities(PyVoxelPointObject* self, void* closure);
|
||||
|
||||
// __repr__
|
||||
static PyObject* repr(PyVoxelPointObject* self);
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
|
||||
// Python type definition for VoxelPoint
|
||||
inline PyTypeObject PyVoxelPointType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.VoxelPoint",
|
||||
.tp_basicsize = sizeof(PyVoxelPointObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_repr = (reprfunc)VoxelPoint::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("VoxelPoint - Navigation grid cell for 3D terrain.\n\n"
|
||||
"VoxelPoints are accessed via Viewport3D.at(x, z) and cannot be\n"
|
||||
"instantiated directly.\n\n"
|
||||
"Properties:\n"
|
||||
" walkable (bool): Can entities walk through this cell?\n"
|
||||
" transparent (bool): Can FOV see through this cell?\n"
|
||||
" height (float): World-space Y coordinate from terrain.\n"
|
||||
" cost (float): Movement cost multiplier (1.0 = normal).\n"
|
||||
" grid_pos (tuple, read-only): (x, z) position in grid.\n"
|
||||
" entities (list, read-only): Entity3D objects at this cell."),
|
||||
.tp_getset = VoxelPoint::getsetters,
|
||||
.tp_new = NULL, // Cannot instantiate from Python - access via viewport.at()
|
||||
};
|
||||
|
||||
} // namespace mcrfpydef
|
||||
|
|
@ -483,6 +483,9 @@ PyObject* PyInit_mcrfpy()
|
|||
/*game map & perspective data - returned by Grid.at() but not directly instantiable*/
|
||||
&PyUIGridPointType, &PyUIGridPointStateType,
|
||||
|
||||
/*3D navigation grid - returned by Viewport3D.at() but not directly instantiable*/
|
||||
&mcrfpydef::PyVoxelPointType,
|
||||
|
||||
/*collections & iterators - returned by .children/.entities but not directly instantiable*/
|
||||
&PyUICollectionType, &PyUICollectionIterType,
|
||||
&PyUIEntityCollectionType, &PyUIEntityCollectionIterType,
|
||||
|
|
|
|||
393
tests/demo/screens/navigation_demo.py
Normal file
393
tests/demo/screens/navigation_demo.py
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
# navigation_demo.py - Visual demo of 3D navigation system
|
||||
# Shows pathfinding and FOV on terrain using VoxelPoint grid
|
||||
# Includes 2D Grid minimap with ColorLayer visualization
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
import math
|
||||
|
||||
# Grid size
|
||||
GRID_W, GRID_H = 40, 40
|
||||
|
||||
# Create demo scene
|
||||
scene = mcrfpy.Scene("navigation_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="Navigation System Demo - Pathfinding & FOV", pos=(20, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
scene.children.append(title)
|
||||
|
||||
# Create the 3D viewport (left side, smaller)
|
||||
viewport = mcrfpy.Viewport3D(
|
||||
pos=(20, 50),
|
||||
size=(480, 360),
|
||||
render_resolution=(320, 240),
|
||||
fov=60.0,
|
||||
camera_pos=(20.0, 35.0, 50.0),
|
||||
camera_target=(20.0, 0.0, 20.0),
|
||||
bg_color=mcrfpy.Color(100, 150, 200)
|
||||
)
|
||||
scene.children.append(viewport)
|
||||
|
||||
# Generate terrain using HeightMap
|
||||
print("Generating terrain heightmap...")
|
||||
hm = mcrfpy.HeightMap((GRID_W, GRID_H))
|
||||
hm.mid_point_displacement(0.5, seed=42)
|
||||
hm.normalize(0.0, 1.0)
|
||||
hm.rain_erosion(drops=500, erosion=0.08, sedimentation=0.04, seed=42)
|
||||
hm.normalize(0.0, 1.0)
|
||||
|
||||
# Shift terrain up so most is walkable land instead of water
|
||||
# Add 0.3 offset, then renormalize to 0.1-1.0 range (keeps some water)
|
||||
for y in range(GRID_H):
|
||||
for x in range(GRID_W):
|
||||
hm[x, y] = min(1.0, hm[x, y] + 0.3)
|
||||
|
||||
# Build terrain mesh
|
||||
print("Building terrain mesh...")
|
||||
vertex_count = viewport.build_terrain(
|
||||
layer_name="terrain",
|
||||
heightmap=hm,
|
||||
y_scale=8.0,
|
||||
cell_size=1.0
|
||||
)
|
||||
print(f"Terrain built with {vertex_count} vertices")
|
||||
|
||||
# Create color maps based on height for 3D terrain
|
||||
r_map = mcrfpy.HeightMap((GRID_W, GRID_H))
|
||||
g_map = mcrfpy.HeightMap((GRID_W, GRID_H))
|
||||
b_map = mcrfpy.HeightMap((GRID_W, GRID_H))
|
||||
|
||||
for y in range(GRID_H):
|
||||
for x in range(GRID_W):
|
||||
h = hm[x, y]
|
||||
if h < 0.25: # Water (unwalkable)
|
||||
r_map[x, y] = 0.2
|
||||
g_map[x, y] = 0.4
|
||||
b_map[x, y] = 0.8
|
||||
elif h < 0.6: # Grass (walkable)
|
||||
r_map[x, y] = 0.2 + h * 0.2
|
||||
g_map[x, y] = 0.5 + h * 0.2
|
||||
b_map[x, y] = 0.1
|
||||
elif h < 0.8: # Hills (walkable but costly)
|
||||
r_map[x, y] = 0.5
|
||||
g_map[x, y] = 0.4
|
||||
b_map[x, y] = 0.3
|
||||
else: # Mountains (unwalkable)
|
||||
r_map[x, y] = 0.6
|
||||
g_map[x, y] = 0.6
|
||||
b_map[x, y] = 0.6
|
||||
|
||||
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
|
||||
|
||||
# Initialize navigation grid
|
||||
print("Setting up navigation grid...")
|
||||
viewport.grid_size = (GRID_W, GRID_H)
|
||||
viewport.cell_size = 1.0
|
||||
|
||||
# Apply heights from heightmap
|
||||
viewport.apply_heightmap(hm, y_scale=8.0)
|
||||
|
||||
# Mark water as unwalkable (height < 0.25)
|
||||
viewport.apply_threshold(hm, 0.0, 0.25, walkable=False)
|
||||
|
||||
# Mark mountains as unwalkable (height > 0.8)
|
||||
viewport.apply_threshold(hm, 0.8, 1.0, walkable=False)
|
||||
|
||||
# Apply slope costs
|
||||
viewport.set_slope_cost(max_slope=2.0, cost_multiplier=3.0)
|
||||
|
||||
# ============================================================================
|
||||
# Create 2D Grid with ColorLayers (minimap on the right)
|
||||
# ============================================================================
|
||||
print("Creating 2D minimap grid...")
|
||||
|
||||
# Calculate cell size for minimap to fit nicely
|
||||
minimap_width = 320
|
||||
minimap_height = 320
|
||||
cell_px = minimap_width // GRID_W # 8 pixels per cell
|
||||
|
||||
# Create 2D Grid (no texture needed for color layers)
|
||||
grid_2d = mcrfpy.Grid(
|
||||
grid_size=(GRID_W, GRID_H),
|
||||
pos=(520, 50),
|
||||
size=(minimap_width, minimap_height)
|
||||
)
|
||||
scene.children.append(grid_2d)
|
||||
|
||||
# Create and add terrain ColorLayer (z_index=0, bottom layer)
|
||||
terrain_layer = mcrfpy.ColorLayer(z_index=0, name="terrain")
|
||||
grid_2d.add_layer(terrain_layer)
|
||||
|
||||
# Fill terrain layer with colors matching the heightmap
|
||||
for y in range(GRID_H):
|
||||
for x in range(GRID_W):
|
||||
h = hm[x, y]
|
||||
if h < 0.25: # Water
|
||||
terrain_layer.set((x, y), mcrfpy.Color(50, 100, 200))
|
||||
elif h < 0.6: # Grass
|
||||
green = int(100 + h * 100)
|
||||
terrain_layer.set((x, y), mcrfpy.Color(50, green, 30))
|
||||
elif h < 0.8: # Hills
|
||||
terrain_layer.set((x, y), mcrfpy.Color(130, 100, 70))
|
||||
else: # Mountains
|
||||
terrain_layer.set((x, y), mcrfpy.Color(150, 150, 150))
|
||||
|
||||
# Create and add path ColorLayer (z_index=1, on top of terrain)
|
||||
path_layer = mcrfpy.ColorLayer(z_index=1, name="path")
|
||||
grid_2d.add_layer(path_layer)
|
||||
# Initialize transparent
|
||||
path_layer.fill(mcrfpy.Color(0, 0, 0, 0))
|
||||
|
||||
# Create and add FOV ColorLayer (z_index=2, on top of path)
|
||||
fov_layer = mcrfpy.ColorLayer(z_index=2, name="fov")
|
||||
grid_2d.add_layer(fov_layer)
|
||||
# Initialize transparent
|
||||
fov_layer.fill(mcrfpy.Color(0, 0, 0, 0))
|
||||
|
||||
# ============================================================================
|
||||
# State for demo
|
||||
# ============================================================================
|
||||
path_start = [10, 20]
|
||||
path_end = [30, 20]
|
||||
current_path = []
|
||||
fov_visible = []
|
||||
show_fov = [True]
|
||||
show_path = [True]
|
||||
|
||||
def clear_path_layer():
|
||||
"""Clear the path visualization layer"""
|
||||
path_layer.fill(mcrfpy.Color(0, 0, 0, 0))
|
||||
|
||||
def clear_fov_layer():
|
||||
"""Clear the FOV visualization layer"""
|
||||
fov_layer.fill(mcrfpy.Color(0, 0, 0, 0))
|
||||
|
||||
def update_path_visualization():
|
||||
"""Update path layer to show current path"""
|
||||
clear_path_layer()
|
||||
if not show_path[0]:
|
||||
return
|
||||
|
||||
# Draw path in yellow
|
||||
for x, z in current_path:
|
||||
path_layer.set((x, z), mcrfpy.Color(255, 255, 0, 200))
|
||||
|
||||
# Draw start point in green
|
||||
path_layer.set((path_start[0], path_start[1]), mcrfpy.Color(0, 255, 0, 255))
|
||||
|
||||
# Draw end point in red
|
||||
path_layer.set((path_end[0], path_end[1]), mcrfpy.Color(255, 0, 0, 255))
|
||||
|
||||
def update_fov_visualization():
|
||||
"""Update FOV layer to show visible cells"""
|
||||
clear_fov_layer()
|
||||
if not show_fov[0]:
|
||||
return
|
||||
|
||||
# Draw visible cells in semi-transparent blue
|
||||
for x, z in fov_visible:
|
||||
# Don't overwrite start/end markers
|
||||
if [x, z] != path_start and [x, z] != path_end:
|
||||
fov_layer.set((x, z), mcrfpy.Color(100, 200, 255, 80))
|
||||
|
||||
def update_path():
|
||||
"""Recompute path between start and end"""
|
||||
global current_path
|
||||
current_path = viewport.find_path(tuple(path_start), tuple(path_end))
|
||||
print(f"Path: {len(current_path)} steps")
|
||||
update_path_visualization()
|
||||
|
||||
def update_fov():
|
||||
"""Recompute FOV from start position"""
|
||||
global fov_visible
|
||||
fov_visible = viewport.compute_fov(tuple(path_start), radius=8)
|
||||
print(f"FOV: {len(fov_visible)} cells visible")
|
||||
update_fov_visualization()
|
||||
|
||||
# Initial computation
|
||||
update_path()
|
||||
update_fov()
|
||||
|
||||
# ============================================================================
|
||||
# Info panel
|
||||
# ============================================================================
|
||||
info_y = 390
|
||||
|
||||
# Status labels
|
||||
start_label = mcrfpy.Caption(text=f"Start (green): ({path_start[0]}, {path_start[1]})", pos=(520, info_y))
|
||||
start_label.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
scene.children.append(start_label)
|
||||
|
||||
end_label = mcrfpy.Caption(text=f"End (red): ({path_end[0]}, {path_end[1]})", pos=(520, info_y + 22))
|
||||
end_label.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
scene.children.append(end_label)
|
||||
|
||||
path_label = mcrfpy.Caption(text=f"Path (yellow): {len(current_path)} steps", pos=(520, info_y + 44))
|
||||
path_label.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
scene.children.append(path_label)
|
||||
|
||||
fov_label = mcrfpy.Caption(text=f"FOV (blue): {len(fov_visible)} cells", pos=(520, info_y + 66))
|
||||
fov_label.fill_color = mcrfpy.Color(100, 200, 255)
|
||||
scene.children.append(fov_label)
|
||||
|
||||
# Terrain legend
|
||||
legend_y = info_y + 100
|
||||
legend_title = mcrfpy.Caption(text="Terrain Legend:", pos=(520, legend_y))
|
||||
legend_title.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
scene.children.append(legend_title)
|
||||
|
||||
legends = [
|
||||
("Water (blue)", mcrfpy.Color(50, 100, 200), "unwalkable"),
|
||||
("Grass (green)", mcrfpy.Color(80, 150, 30), "walkable"),
|
||||
("Hills (brown)", mcrfpy.Color(130, 100, 70), "costly"),
|
||||
("Mountains (gray)", mcrfpy.Color(150, 150, 150), "unwalkable"),
|
||||
]
|
||||
|
||||
for i, (name, color, desc) in enumerate(legends):
|
||||
cap = mcrfpy.Caption(text=f" {name}: {desc}", pos=(520, legend_y + 22 + i * 20))
|
||||
cap.fill_color = color
|
||||
scene.children.append(cap)
|
||||
|
||||
# Instructions
|
||||
instructions = mcrfpy.Caption(
|
||||
text="[WASD] Move start | [IJKL] Move end | [F] FOV | [P] Path | [Space] Orbit",
|
||||
pos=(20, 430)
|
||||
)
|
||||
instructions.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
scene.children.append(instructions)
|
||||
|
||||
# Status line
|
||||
status = mcrfpy.Caption(text="Status: Navigation demo ready - orbit OFF", pos=(20, 455))
|
||||
status.fill_color = mcrfpy.Color(100, 200, 100)
|
||||
scene.children.append(status)
|
||||
|
||||
# ============================================================================
|
||||
# Animation state - orbit disabled by default
|
||||
# ============================================================================
|
||||
animation_time = [0.0]
|
||||
camera_orbit = [False] # Disabled by default
|
||||
|
||||
def update_display():
|
||||
"""Update info display"""
|
||||
start_label.text = f"Start (green): ({path_start[0]}, {path_start[1]})"
|
||||
end_label.text = f"End (red): ({path_end[0]}, {path_end[1]})"
|
||||
path_label.text = f"Path (yellow): {len(current_path)} steps"
|
||||
fov_label.text = f"FOV (blue): {len(fov_visible)} cells"
|
||||
|
||||
# Camera animation
|
||||
def update_camera(timer, runtime):
|
||||
animation_time[0] += runtime / 1000.0
|
||||
|
||||
if camera_orbit[0]:
|
||||
angle = animation_time[0] * 0.2
|
||||
radius = 35.0
|
||||
center_x = 20.0
|
||||
center_z = 20.0
|
||||
height = 25.0 + math.sin(animation_time[0] * 0.15) * 5.0
|
||||
|
||||
x = center_x + math.cos(angle) * radius
|
||||
z = center_z + math.sin(angle) * radius
|
||||
|
||||
viewport.camera_pos = (x, height, z)
|
||||
viewport.camera_target = (center_x, 2.0, center_z)
|
||||
|
||||
# Key handler
|
||||
def on_key(key, state):
|
||||
if state != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
global current_path, fov_visible
|
||||
|
||||
# Movement for start point (WASD)
|
||||
moved_start = False
|
||||
if key == mcrfpy.Key.W:
|
||||
path_start[1] = max(0, path_start[1] - 1)
|
||||
moved_start = True
|
||||
elif key == mcrfpy.Key.S:
|
||||
path_start[1] = min(GRID_H - 1, path_start[1] + 1)
|
||||
moved_start = True
|
||||
elif key == mcrfpy.Key.A:
|
||||
path_start[0] = max(0, path_start[0] - 1)
|
||||
moved_start = True
|
||||
elif key == mcrfpy.Key.D:
|
||||
path_start[0] = min(GRID_W - 1, path_start[0] + 1)
|
||||
moved_start = True
|
||||
|
||||
# Movement for end point (IJKL)
|
||||
moved_end = False
|
||||
if key == mcrfpy.Key.I:
|
||||
path_end[1] = max(0, path_end[1] - 1)
|
||||
moved_end = True
|
||||
elif key == mcrfpy.Key.K:
|
||||
path_end[1] = min(GRID_H - 1, path_end[1] + 1)
|
||||
moved_end = True
|
||||
elif key == mcrfpy.Key.J:
|
||||
path_end[0] = max(0, path_end[0] - 1)
|
||||
moved_end = True
|
||||
elif key == mcrfpy.Key.L:
|
||||
path_end[0] = min(GRID_W - 1, path_end[0] + 1)
|
||||
moved_end = True
|
||||
|
||||
# Toggle FOV display
|
||||
if key == mcrfpy.Key.F:
|
||||
show_fov[0] = not show_fov[0]
|
||||
update_fov_visualization()
|
||||
status.text = f"FOV display: {'ON' if show_fov[0] else 'OFF'}"
|
||||
|
||||
# Toggle path display
|
||||
if key == mcrfpy.Key.P:
|
||||
show_path[0] = not show_path[0]
|
||||
update_path_visualization()
|
||||
status.text = f"Path display: {'ON' if show_path[0] else 'OFF'}"
|
||||
|
||||
# Toggle camera orbit
|
||||
if key == mcrfpy.Key.SPACE:
|
||||
camera_orbit[0] = not camera_orbit[0]
|
||||
status.text = f"Camera orbit: {'ON' if camera_orbit[0] else 'OFF'}"
|
||||
|
||||
# Quit
|
||||
if key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
|
||||
# Update pathfinding and FOV if moved
|
||||
if moved_start or moved_end:
|
||||
update_path()
|
||||
if moved_start:
|
||||
update_fov()
|
||||
update_display()
|
||||
|
||||
# Show cell info
|
||||
vp = viewport.at(path_start[0], path_start[1])
|
||||
status.text = f"Start cell: walkable={vp.walkable}, height={vp.height:.2f}, cost={vp.cost:.2f}"
|
||||
|
||||
# Set up scene
|
||||
scene.on_key = on_key
|
||||
|
||||
# Create timer for camera animation
|
||||
timer = mcrfpy.Timer("camera_update", update_camera, 16)
|
||||
|
||||
# Activate scene
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
print()
|
||||
print("Navigation Demo loaded!")
|
||||
print(f"A {GRID_W}x{GRID_H} terrain with VoxelPoint navigation grid.")
|
||||
print()
|
||||
print("Left: 3D terrain view")
|
||||
print("Right: 2D minimap with ColorLayer overlays")
|
||||
print(" - Terrain layer shows heightmap colors")
|
||||
print(" - Path layer shows computed A* path (yellow)")
|
||||
print(" - FOV layer shows visible cells (blue tint)")
|
||||
print()
|
||||
print("Controls:")
|
||||
print(" [WASD] Move start point (green)")
|
||||
print(" [IJKL] Move end point (red)")
|
||||
print(" [F] Toggle FOV display")
|
||||
print(" [P] Toggle path display")
|
||||
print(" [Space] Toggle camera orbit")
|
||||
print(" [ESC] Quit")
|
||||
Loading…
Add table
Add a link
Reference in a new issue