Compare commits

..

No commits in common. "992ea781cb2c9f11f087828da3aca6173dc1d4bd" and "8636e766f80cf452ad8d126c9f166fe311850b81" have entirely different histories.

36 changed files with 74 additions and 17974 deletions

View file

@ -1,596 +0,0 @@
// Billboard.cpp - Camera-facing 3D sprite implementation
#include "Billboard.h"
#include "Shader3D.h"
#include "MeshLayer.h" // For MeshVertex
#include "../platform/GLContext.h"
#include "../PyTexture.h"
#include "../PyTypeCache.h"
#include <cmath>
#include <iostream>
// GL headers based on backend
#if defined(MCRF_SDL2)
#ifdef __EMSCRIPTEN__
#include <GLES2/gl2.h>
#else
#include <GL/gl.h>
#include <GL/glext.h>
#endif
#define MCRF_HAS_GL 1
#elif !defined(MCRF_HEADLESS)
#include <glad/glad.h>
#define MCRF_HAS_GL 1
#endif
namespace mcrf {
// Static members
unsigned int Billboard::sharedVBO_ = 0;
unsigned int Billboard::sharedEBO_ = 0;
bool Billboard::geometryInitialized_ = false;
// =============================================================================
// Constructor / Destructor
// =============================================================================
Billboard::Billboard()
: texture_()
, spriteIndex_(0)
, position_(0, 0, 0)
, scale_(1.0f)
, facing_(BillboardFacing::CameraY)
, theta_(0.0f)
, phi_(0.0f)
, opacity_(1.0f)
, visible_(true)
, tilesPerRow_(1)
, tilesPerCol_(1)
{}
Billboard::Billboard(std::shared_ptr<PyTexture> texture, int spriteIndex, const vec3& pos,
float scale, BillboardFacing facing)
: texture_(texture)
, spriteIndex_(spriteIndex)
, position_(pos)
, scale_(scale)
, facing_(facing)
, theta_(0.0f)
, phi_(0.0f)
, opacity_(1.0f)
, visible_(true)
, tilesPerRow_(1)
, tilesPerCol_(1)
{}
Billboard::~Billboard() {
// Texture is not owned, don't delete it
}
// =============================================================================
// Configuration
// =============================================================================
void Billboard::setSpriteSheetLayout(int tilesPerRow, int tilesPerCol) {
tilesPerRow_ = tilesPerRow > 0 ? tilesPerRow : 1;
tilesPerCol_ = tilesPerCol > 0 ? tilesPerCol : 1;
}
// =============================================================================
// Static Geometry Management
// =============================================================================
void Billboard::initSharedGeometry() {
#ifdef MCRF_HAS_GL
if (geometryInitialized_ || !gl::isGLReady()) {
return;
}
// Create a unit quad centered at origin, facing +Z
// Vertices: position (3) + texcoord (2) + normal (3) + color (4) = 12 floats
MeshVertex vertices[4] = {
// Bottom-left
MeshVertex(vec3(-0.5f, -0.5f, 0.0f), vec2(0, 1), vec3(0, 0, 1), vec4(1, 1, 1, 1)),
// Bottom-right
MeshVertex(vec3( 0.5f, -0.5f, 0.0f), vec2(1, 1), vec3(0, 0, 1), vec4(1, 1, 1, 1)),
// Top-right
MeshVertex(vec3( 0.5f, 0.5f, 0.0f), vec2(1, 0), vec3(0, 0, 1), vec4(1, 1, 1, 1)),
// Top-left
MeshVertex(vec3(-0.5f, 0.5f, 0.0f), vec2(0, 0), vec3(0, 0, 1), vec4(1, 1, 1, 1)),
};
unsigned short indices[6] = {
0, 1, 2, // First triangle
2, 3, 0 // Second triangle
};
glGenBuffers(1, &sharedVBO_);
glBindBuffer(GL_ARRAY_BUFFER, sharedVBO_);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glGenBuffers(1, &sharedEBO_);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sharedEBO_);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
geometryInitialized_ = true;
#endif
}
void Billboard::cleanupSharedGeometry() {
#ifdef MCRF_HAS_GL
if (sharedVBO_ != 0 && gl::isGLReady()) {
glDeleteBuffers(1, &sharedVBO_);
sharedVBO_ = 0;
}
if (sharedEBO_ != 0 && gl::isGLReady()) {
glDeleteBuffers(1, &sharedEBO_);
sharedEBO_ = 0;
}
geometryInitialized_ = false;
#endif
}
// =============================================================================
// Rendering
// =============================================================================
mat4 Billboard::computeModelMatrix(const vec3& cameraPos, const mat4& view) {
mat4 model = mat4::identity();
// First translate to world position
model = model * mat4::translate(position_);
// Apply rotation based on facing mode
switch (facing_) {
case BillboardFacing::Camera: {
// Full rotation to face camera
// Extract camera right and up vectors from view matrix
vec3 right(view.m[0], view.m[4], view.m[8]);
vec3 up(view.m[1], view.m[5], view.m[9]);
// Build rotation matrix that makes the quad face the camera
// The quad is initially facing +Z, we need to rotate it
mat4 rotation;
rotation.m[0] = right.x; rotation.m[4] = up.x; rotation.m[8] = -view.m[2]; rotation.m[12] = 0;
rotation.m[1] = right.y; rotation.m[5] = up.y; rotation.m[9] = -view.m[6]; rotation.m[13] = 0;
rotation.m[2] = right.z; rotation.m[6] = up.z; rotation.m[10] = -view.m[10]; rotation.m[14] = 0;
rotation.m[3] = 0; rotation.m[7] = 0; rotation.m[11] = 0; rotation.m[15] = 1;
model = model * rotation;
break;
}
case BillboardFacing::CameraY: {
// Only Y-axis rotation to face camera (stays upright)
vec3 toCamera(cameraPos.x - position_.x, 0, cameraPos.z - position_.z);
float length = std::sqrt(toCamera.x * toCamera.x + toCamera.z * toCamera.z);
if (length > 0.001f) {
float angle = std::atan2(toCamera.x, toCamera.z);
model = model * mat4::rotateY(angle);
}
break;
}
case BillboardFacing::Fixed: {
// Use theta (Y rotation) and phi (X tilt)
model = model * mat4::rotateY(theta_);
model = model * mat4::rotateX(phi_);
break;
}
}
// Apply scale
model = model * mat4::scale(vec3(scale_, scale_, scale_));
return model;
}
void Billboard::render(unsigned int shader, const mat4& view, const mat4& projection,
const vec3& cameraPos) {
#ifdef MCRF_HAS_GL
if (!visible_ || !gl::isGLReady()) {
return;
}
// Initialize shared geometry if needed
if (!geometryInitialized_) {
initSharedGeometry();
}
if (sharedVBO_ == 0 || sharedEBO_ == 0) {
return;
}
// Compute model matrix
mat4 model = computeModelMatrix(cameraPos, view);
// Set model matrix uniform
int modelLoc = glGetUniformLocation(shader, "u_model");
if (modelLoc >= 0) {
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.m);
}
// Set entity color uniform (for tinting/opacity)
int colorLoc = glGetUniformLocation(shader, "u_entityColor");
if (colorLoc >= 0) {
glUniform4f(colorLoc, 1.0f, 1.0f, 1.0f, opacity_);
}
// Handle texture
bool hasTexture = (texture_ != nullptr);
int hasTexLoc = glGetUniformLocation(shader, "u_has_texture");
if (hasTexLoc >= 0) {
glUniform1i(hasTexLoc, hasTexture ? 1 : 0);
}
if (hasTexture) {
// Bind texture using PyTexture's underlying sf::Texture
const sf::Texture* sfTexture = texture_->getSFMLTexture();
if (sfTexture) {
glBindTexture(GL_TEXTURE_2D, sfTexture->getNativeHandle());
// Use PyTexture's sprite sheet configuration
int sheetW = texture_->sprite_width > 0 ? texture_->sprite_width : 1;
int sheetH = texture_->sprite_height > 0 ? texture_->sprite_height : 1;
sf::Vector2u texSize = sfTexture->getSize();
int tilesPerRow = texSize.x / sheetW;
int tilesPerCol = texSize.y / sheetH;
if (tilesPerRow < 1) tilesPerRow = 1;
if (tilesPerCol < 1) tilesPerCol = 1;
// Calculate sprite UV offset
float tileU = 1.0f / tilesPerRow;
float tileV = 1.0f / tilesPerCol;
int tileX = spriteIndex_ % tilesPerRow;
int tileY = spriteIndex_ / tilesPerRow;
// Set UV offset/scale uniforms if available
int uvOffsetLoc = glGetUniformLocation(shader, "u_uv_offset");
int uvScaleLoc = glGetUniformLocation(shader, "u_uv_scale");
if (uvOffsetLoc >= 0) {
glUniform2f(uvOffsetLoc, tileX * tileU, tileY * tileV);
}
if (uvScaleLoc >= 0) {
glUniform2f(uvScaleLoc, tileU, tileV);
}
}
}
// Bind VBO
glBindBuffer(GL_ARRAY_BUFFER, sharedVBO_);
// Set up vertex attributes
int stride = sizeof(MeshVertex);
glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION);
glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, position)));
glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, texcoord)));
glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, normal)));
glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR);
glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, color)));
// Bind EBO and draw
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sharedEBO_);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
// Cleanup
glDisableVertexAttribArray(Shader3D::ATTRIB_POSITION);
glDisableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
glDisableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
glDisableVertexAttribArray(Shader3D::ATTRIB_COLOR);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
// Unbind texture
if (hasTexture) {
glBindTexture(GL_TEXTURE_2D, 0);
}
// Reset UV uniforms
int uvOffsetLoc = glGetUniformLocation(shader, "u_uv_offset");
int uvScaleLoc = glGetUniformLocation(shader, "u_uv_scale");
if (uvOffsetLoc >= 0) {
glUniform2f(uvOffsetLoc, 0.0f, 0.0f);
}
if (uvScaleLoc >= 0) {
glUniform2f(uvScaleLoc, 1.0f, 1.0f);
}
#endif
}
// =============================================================================
// Python API
// =============================================================================
PyGetSetDef Billboard::getsetters[] = {
{"texture", Billboard::get_texture, Billboard::set_texture,
"Sprite sheet texture (Texture or None)", NULL},
{"sprite_index", Billboard::get_sprite_index, Billboard::set_sprite_index,
"Index into sprite sheet (int)", NULL},
{"pos", Billboard::get_pos, Billboard::set_pos,
"World position as (x, y, z) tuple", NULL},
{"scale", Billboard::get_scale, Billboard::set_scale,
"Uniform scale factor (float)", NULL},
{"facing", Billboard::get_facing, Billboard::set_facing,
"Facing mode: 'camera', 'camera_y', or 'fixed' (str)", NULL},
{"theta", Billboard::get_theta, Billboard::set_theta,
"Horizontal rotation for 'fixed' mode in radians (float)", NULL},
{"phi", Billboard::get_phi, Billboard::set_phi,
"Vertical tilt for 'fixed' mode in radians (float)", NULL},
{"opacity", Billboard::get_opacity, Billboard::set_opacity,
"Opacity from 0.0 (transparent) to 1.0 (opaque) (float)", NULL},
{"visible", Billboard::get_visible, Billboard::set_visible,
"Visibility state (bool)", NULL},
{NULL}
};
int Billboard::init(PyObject* self, PyObject* args, PyObject* kwds) {
PyBillboardObject* selfObj = (PyBillboardObject*)self;
static const char* kwlist[] = {"texture", "sprite_index", "pos", "scale", "facing", "opacity", "visible", NULL};
PyObject* textureObj = nullptr;
int spriteIndex = 0;
PyObject* posObj = nullptr;
float scale = 1.0f;
const char* facingStr = "camera_y";
float opacity = 1.0f;
int visible = 1; // Default to True
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OiOfsfp", const_cast<char**>(kwlist),
&textureObj, &spriteIndex, &posObj, &scale, &facingStr, &opacity, &visible)) {
return -1;
}
// Handle texture
if (textureObj && textureObj != Py_None) {
PyTypeObject* textureType = PyTypeCache::Texture();
if (textureType && PyObject_IsInstance(textureObj, (PyObject*)textureType)) {
PyTextureObject* texPy = (PyTextureObject*)textureObj;
if (texPy->data) {
selfObj->data->setTexture(texPy->data);
}
} else {
PyErr_SetString(PyExc_TypeError, "texture must be a Texture object or None");
return -1;
}
}
selfObj->data->setSpriteIndex(spriteIndex);
selfObj->data->setScale(scale);
// Handle position
if (posObj && posObj != Py_None) {
if (PyTuple_Check(posObj) && PyTuple_Size(posObj) >= 3) {
float x = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(posObj, 0)));
float y = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(posObj, 1)));
float z = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(posObj, 2)));
if (!PyErr_Occurred()) {
selfObj->data->setPosition(vec3(x, y, z));
}
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, y, z)");
return -1;
}
}
// Handle facing mode
std::string facing(facingStr);
if (facing == "camera") {
selfObj->data->setFacing(BillboardFacing::Camera);
} else if (facing == "camera_y") {
selfObj->data->setFacing(BillboardFacing::CameraY);
} else if (facing == "fixed") {
selfObj->data->setFacing(BillboardFacing::Fixed);
} else {
PyErr_SetString(PyExc_ValueError, "facing must be 'camera', 'camera_y', or 'fixed'");
return -1;
}
// Apply opacity and visibility
selfObj->data->setOpacity(opacity);
selfObj->data->setVisible(visible != 0);
return 0;
}
PyObject* Billboard::repr(PyObject* self) {
PyBillboardObject* obj = (PyBillboardObject*)self;
if (!obj->data) {
return PyUnicode_FromString("<Billboard (invalid)>");
}
vec3 pos = obj->data->getPosition();
const char* facingStr = "unknown";
switch (obj->data->getFacing()) {
case BillboardFacing::Camera: facingStr = "camera"; break;
case BillboardFacing::CameraY: facingStr = "camera_y"; break;
case BillboardFacing::Fixed: facingStr = "fixed"; break;
}
// PyUnicode_FromFormat doesn't support %f, so use snprintf
char buffer[256];
snprintf(buffer, sizeof(buffer), "<Billboard pos=(%.2f, %.2f, %.2f) scale=%.2f facing='%s'>",
pos.x, pos.y, pos.z, obj->data->getScale(), facingStr);
return PyUnicode_FromString(buffer);
}
// Property implementations
PyObject* Billboard::get_texture(PyObject* self, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
std::shared_ptr<PyTexture> tex = obj->data->getTexture();
if (!tex) {
Py_RETURN_NONE;
}
// Return the PyTexture's Python object
return tex->pyObject();
}
int Billboard::set_texture(PyObject* self, PyObject* value, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
if (value == Py_None) {
obj->data->setTexture(nullptr);
return 0;
}
// Use PyTypeCache to get properly initialized type object
PyTypeObject* textureType = PyTypeCache::Texture();
if (!textureType) {
PyErr_SetString(PyExc_RuntimeError, "Texture type not initialized");
return -1;
}
if (PyObject_IsInstance(value, (PyObject*)textureType)) {
PyTextureObject* texPy = (PyTextureObject*)value;
if (texPy->data) {
obj->data->setTexture(texPy->data);
return 0;
}
}
PyErr_SetString(PyExc_TypeError, "texture must be a Texture object or None");
return -1;
}
PyObject* Billboard::get_sprite_index(PyObject* self, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
return PyLong_FromLong(obj->data->getSpriteIndex());
}
int Billboard::set_sprite_index(PyObject* self, PyObject* value, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "sprite_index must be an integer");
return -1;
}
obj->data->setSpriteIndex(static_cast<int>(PyLong_AsLong(value)));
return 0;
}
PyObject* Billboard::get_pos(PyObject* self, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
vec3 pos = obj->data->getPosition();
return Py_BuildValue("(fff)", pos.x, pos.y, pos.z);
}
int Billboard::set_pos(PyObject* self, PyObject* value, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
if (!PyTuple_Check(value) || PyTuple_Size(value) < 3) {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, y, z)");
return -1;
}
float x = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 0)));
float y = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 1)));
float z = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 2)));
if (PyErr_Occurred()) return -1;
obj->data->setPosition(vec3(x, y, z));
return 0;
}
PyObject* Billboard::get_scale(PyObject* self, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
return PyFloat_FromDouble(obj->data->getScale());
}
int Billboard::set_scale(PyObject* self, PyObject* value, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
float scale = static_cast<float>(PyFloat_AsDouble(value));
if (PyErr_Occurred()) return -1;
obj->data->setScale(scale);
return 0;
}
PyObject* Billboard::get_facing(PyObject* self, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
switch (obj->data->getFacing()) {
case BillboardFacing::Camera: return PyUnicode_FromString("camera");
case BillboardFacing::CameraY: return PyUnicode_FromString("camera_y");
case BillboardFacing::Fixed: return PyUnicode_FromString("fixed");
}
return PyUnicode_FromString("unknown");
}
int Billboard::set_facing(PyObject* self, PyObject* value, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError, "facing must be a string");
return -1;
}
const char* str = PyUnicode_AsUTF8(value);
std::string facing(str);
if (facing == "camera") {
obj->data->setFacing(BillboardFacing::Camera);
} else if (facing == "camera_y") {
obj->data->setFacing(BillboardFacing::CameraY);
} else if (facing == "fixed") {
obj->data->setFacing(BillboardFacing::Fixed);
} else {
PyErr_SetString(PyExc_ValueError, "facing must be 'camera', 'camera_y', or 'fixed'");
return -1;
}
return 0;
}
PyObject* Billboard::get_theta(PyObject* self, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
return PyFloat_FromDouble(obj->data->getTheta());
}
int Billboard::set_theta(PyObject* self, PyObject* value, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
float theta = static_cast<float>(PyFloat_AsDouble(value));
if (PyErr_Occurred()) return -1;
obj->data->setTheta(theta);
return 0;
}
PyObject* Billboard::get_phi(PyObject* self, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
return PyFloat_FromDouble(obj->data->getPhi());
}
int Billboard::set_phi(PyObject* self, PyObject* value, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
float phi = static_cast<float>(PyFloat_AsDouble(value));
if (PyErr_Occurred()) return -1;
obj->data->setPhi(phi);
return 0;
}
PyObject* Billboard::get_opacity(PyObject* self, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
return PyFloat_FromDouble(obj->data->getOpacity());
}
int Billboard::set_opacity(PyObject* self, PyObject* value, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
float opacity = static_cast<float>(PyFloat_AsDouble(value));
if (PyErr_Occurred()) return -1;
obj->data->setOpacity(opacity);
return 0;
}
PyObject* Billboard::get_visible(PyObject* self, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
return PyBool_FromLong(obj->data->isVisible());
}
int Billboard::set_visible(PyObject* self, PyObject* value, void* closure) {
PyBillboardObject* obj = (PyBillboardObject*)self;
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
obj->data->setVisible(value == Py_True);
return 0;
}
} // namespace mcrf

View file

@ -1,229 +0,0 @@
// Billboard.h - Camera-facing 3D sprite for McRogueFace
// Supports camera-facing rotation modes for trees, items, particles, etc.
#pragma once
#include "Common.h"
#include "Math3D.h"
#include "Python.h"
#include "structmember.h"
#include <memory>
// Forward declaration
class PyTexture;
namespace mcrf {
// =============================================================================
// BillboardFacing - Billboard rotation mode
// =============================================================================
enum class BillboardFacing {
Camera, // Full rotation to always face camera
CameraY, // Only Y-axis rotation (stays upright)
Fixed // No automatic rotation, uses theta/phi angles
};
// =============================================================================
// Billboard - Camera-facing 3D sprite
// =============================================================================
class Billboard : public std::enable_shared_from_this<Billboard> {
public:
// Python integration
PyObject* self = nullptr;
uint64_t serial_number = 0;
Billboard();
Billboard(std::shared_ptr<PyTexture> texture, int spriteIndex, const vec3& pos,
float scale = 1.0f, BillboardFacing facing = BillboardFacing::CameraY);
~Billboard();
// No copy, allow move
Billboard(const Billboard&) = delete;
Billboard& operator=(const Billboard&) = delete;
// =========================================================================
// Properties
// =========================================================================
std::shared_ptr<PyTexture> getTexture() const { return texture_; }
void setTexture(std::shared_ptr<PyTexture> tex) { texture_ = tex; }
int getSpriteIndex() const { return spriteIndex_; }
void setSpriteIndex(int idx) { spriteIndex_ = idx; }
vec3 getPosition() const { return position_; }
void setPosition(const vec3& pos) { position_ = pos; }
float getScale() const { return scale_; }
void setScale(float s) { scale_ = s; }
BillboardFacing getFacing() const { return facing_; }
void setFacing(BillboardFacing f) { facing_ = f; }
// Fixed facing angles (radians)
float getTheta() const { return theta_; }
void setTheta(float t) { theta_ = t; }
float getPhi() const { return phi_; }
void setPhi(float p) { phi_ = p; }
float getOpacity() const { return opacity_; }
void setOpacity(float o) { opacity_ = o < 0 ? 0 : (o > 1 ? 1 : o); }
bool isVisible() const { return visible_; }
void setVisible(bool v) { visible_ = v; }
// Sprite sheet configuration
void setSpriteSheetLayout(int tilesPerRow, int tilesPerCol);
int getTilesPerRow() const { return tilesPerRow_; }
int getTilesPerCol() const { return tilesPerCol_; }
// =========================================================================
// Rendering
// =========================================================================
/// Render the billboard
/// @param shader Shader program handle
/// @param view View matrix
/// @param projection Projection matrix
/// @param cameraPos Camera world position (for facing computation)
void render(unsigned int shader, const mat4& view, const mat4& projection,
const vec3& cameraPos);
// =========================================================================
// Static Initialization
// =========================================================================
/// Initialize shared quad geometry (call once at startup)
static void initSharedGeometry();
/// Cleanup shared geometry (call at shutdown)
static void cleanupSharedGeometry();
// =========================================================================
// Python API
// =========================================================================
static int init(PyObject* self, PyObject* args, PyObject* kwds);
static PyObject* repr(PyObject* self);
static PyObject* get_texture(PyObject* self, void* closure);
static int set_texture(PyObject* self, PyObject* value, void* closure);
static PyObject* get_sprite_index(PyObject* self, void* closure);
static int set_sprite_index(PyObject* self, PyObject* value, void* closure);
static PyObject* get_pos(PyObject* self, void* closure);
static int set_pos(PyObject* self, PyObject* value, void* closure);
static PyObject* get_scale(PyObject* self, void* closure);
static int set_scale(PyObject* self, PyObject* value, void* closure);
static PyObject* get_facing(PyObject* self, void* closure);
static int set_facing(PyObject* self, PyObject* value, void* closure);
static PyObject* get_theta(PyObject* self, void* closure);
static int set_theta(PyObject* self, PyObject* value, void* closure);
static PyObject* get_phi(PyObject* self, void* closure);
static int set_phi(PyObject* self, PyObject* value, void* closure);
static PyObject* get_opacity(PyObject* self, void* closure);
static int set_opacity(PyObject* self, PyObject* value, void* closure);
static PyObject* get_visible(PyObject* self, void* closure);
static int set_visible(PyObject* self, PyObject* value, void* closure);
static PyGetSetDef getsetters[];
private:
std::shared_ptr<PyTexture> texture_; // Texture wrapper
int spriteIndex_ = 0;
vec3 position_;
float scale_ = 1.0f;
BillboardFacing facing_ = BillboardFacing::CameraY;
float theta_ = 0.0f; // Horizontal rotation for Fixed mode
float phi_ = 0.0f; // Vertical tilt for Fixed mode
float opacity_ = 1.0f;
bool visible_ = true;
// Sprite sheet configuration
int tilesPerRow_ = 1;
int tilesPerCol_ = 1;
// Shared quad geometry (one VBO for all billboards)
static unsigned int sharedVBO_;
static unsigned int sharedEBO_;
static bool geometryInitialized_;
// Compute billboard model matrix based on facing mode
mat4 computeModelMatrix(const vec3& cameraPos, const mat4& view);
};
} // namespace mcrf
// =============================================================================
// Python type definition
// =============================================================================
typedef struct PyBillboardObject {
PyObject_HEAD
std::shared_ptr<mcrf::Billboard> data;
PyObject* weakreflist;
} PyBillboardObject;
namespace mcrfpydef {
inline PyTypeObject PyBillboardType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Billboard",
.tp_basicsize = sizeof(PyBillboardObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self)
{
PyBillboardObject* obj = (PyBillboardObject*)self;
PyObject_GC_UnTrack(self);
if (obj->weakreflist != NULL) {
PyObject_ClearWeakRefs(self);
}
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_repr = mcrf::Billboard::repr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
.tp_doc = PyDoc_STR(
"Billboard(texture=None, sprite_index=0, pos=(0,0,0), scale=1.0, facing='camera_y')\n\n"
"A camera-facing 3D sprite for trees, items, particles, etc.\n\n"
"Args:\n"
" texture (Texture, optional): Sprite sheet texture. Default: None\n"
" sprite_index (int): Index into sprite sheet. Default: 0\n"
" pos (tuple): World position (x, y, z). Default: (0, 0, 0)\n"
" scale (float): Uniform scale factor. Default: 1.0\n"
" facing (str): Facing mode - 'camera', 'camera_y', or 'fixed'. Default: 'camera_y'\n\n"
"Properties:\n"
" texture (Texture): Sprite sheet texture\n"
" sprite_index (int): Index into sprite sheet\n"
" pos (tuple): World position (x, y, z)\n"
" scale (float): Uniform scale factor\n"
" facing (str): Facing mode - 'camera', 'camera_y', or 'fixed'\n"
" theta (float): Horizontal rotation for 'fixed' mode (radians)\n"
" phi (float): Vertical tilt for 'fixed' mode (radians)\n"
" opacity (float): Opacity 0.0 (transparent) to 1.0 (opaque)\n"
" visible (bool): Visibility state"
),
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
return 0;
},
.tp_clear = [](PyObject* self) -> int {
return 0;
},
.tp_getset = mcrf::Billboard::getsetters,
.tp_init = mcrf::Billboard::init,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
PyBillboardObject* self = (PyBillboardObject*)type->tp_alloc(type, 0);
if (self) {
// Use placement new to properly construct the shared_ptr
// tp_alloc zeroes memory but doesn't call C++ constructors
new (&self->data) std::shared_ptr<mcrf::Billboard>(std::make_shared<mcrf::Billboard>());
self->weakreflist = nullptr;
}
return (PyObject*)self;
}
};
} // namespace mcrfpydef

View file

@ -3,8 +3,6 @@
#include "Entity3D.h"
#include "Viewport3D.h"
#include "VoxelPoint.h"
#include "Model3D.h"
#include "Shader3D.h"
#include "PyVector.h"
#include "PyColor.h"
#include "PythonObjectCache.h"
@ -56,10 +54,6 @@ Entity3D::~Entity3D()
{
// Cleanup cube geometry when last entity is destroyed?
// For now, leave it - it's shared static data
// Clean up Python animation callback
Py_XDECREF(py_anim_callback_);
py_anim_callback_ = nullptr;
}
// =============================================================================
@ -287,27 +281,23 @@ void Entity3D::processNextMove()
void Entity3D::update(float dt)
{
// Update movement animation
if (is_animating_) {
move_progress_ += dt * move_speed_;
if (!is_animating_) return;
if (move_progress_ >= 1.0f) {
// Animation complete
world_pos_ = target_world_pos_;
is_animating_ = false;
move_progress_ += dt * move_speed_;
// Process next move in queue
if (!move_queue_.empty()) {
processNextMove();
}
} else {
// Interpolate position
world_pos_ = vec3::lerp(move_start_pos_, target_world_pos_, move_progress_);
if (move_progress_ >= 1.0f) {
// Animation complete
world_pos_ = target_world_pos_;
is_animating_ = false;
// Process next move in queue
if (!move_queue_.empty()) {
processNextMove();
}
} else {
// Interpolate position
world_pos_ = vec3::lerp(move_start_pos_, target_world_pos_, move_progress_);
}
// Update skeletal animation
updateAnimation(dt);
}
bool Entity3D::setProperty(const std::string& name, float value)
@ -394,111 +384,6 @@ bool Entity3D::hasProperty(const std::string& name) const
name == "sprite_index" || name == "visible";
}
// =============================================================================
// Skeletal Animation
// =============================================================================
void Entity3D::setAnimClip(const std::string& name)
{
if (anim_clip_ == name) return;
anim_clip_ = name;
anim_time_ = 0.0f;
anim_paused_ = false;
// Initialize bone matrices if model has skeleton
if (model_ && model_->hasSkeleton()) {
size_t bone_count = model_->getBoneCount();
bone_matrices_.resize(bone_count);
for (auto& m : bone_matrices_) {
m = mat4::identity();
}
}
}
void Entity3D::updateAnimation(float dt)
{
// Handle auto-animate (play walk/idle based on movement state)
if (auto_animate_ && model_ && model_->hasSkeleton()) {
bool currently_moving = isMoving();
if (currently_moving != was_moving_) {
was_moving_ = currently_moving;
if (currently_moving) {
// Started moving - play walk clip
if (model_->findClip(walk_clip_)) {
setAnimClip(walk_clip_);
}
} else {
// Stopped moving - play idle clip
if (model_->findClip(idle_clip_)) {
setAnimClip(idle_clip_);
}
}
}
}
// Early out if no model, no skeleton, or no animation
if (!model_ || !model_->hasSkeleton()) return;
if (anim_clip_.empty() || anim_paused_) return;
const AnimationClip* clip = model_->findClip(anim_clip_);
if (!clip) return;
// Advance time
anim_time_ += dt * anim_speed_;
// Handle loop/completion
if (anim_time_ >= clip->duration) {
if (anim_loop_) {
anim_time_ = std::fmod(anim_time_, clip->duration);
} else {
anim_time_ = clip->duration;
anim_paused_ = true;
// Fire callback
if (on_anim_complete_) {
on_anim_complete_(this, anim_clip_);
}
// Fire Python callback
if (py_anim_callback_) {
PyObject* result = PyObject_CallFunction(py_anim_callback_, "(Os)",
self, anim_clip_.c_str());
if (result) {
Py_DECREF(result);
} else {
PyErr_Print();
}
}
}
}
// Sample animation
const Skeleton& skeleton = model_->getSkeleton();
const std::vector<mat4>& default_transforms = model_->getDefaultBoneTransforms();
std::vector<mat4> local_transforms;
clip->sample(anim_time_, skeleton.bones.size(), default_transforms, local_transforms);
// Compute global transforms
std::vector<mat4> global_transforms;
skeleton.computeGlobalTransforms(local_transforms, global_transforms);
// Compute final bone matrices (global * inverse_bind)
skeleton.computeBoneMatrices(global_transforms, bone_matrices_);
}
int Entity3D::getAnimFrame() const
{
if (!model_ || !model_->hasSkeleton()) return 0;
const AnimationClip* clip = model_->findClip(anim_clip_);
if (!clip || clip->duration <= 0) return 0;
// Approximate frame at 30fps
return static_cast<int>(anim_time_ * 30.0f);
}
// =============================================================================
// Rendering
// =============================================================================
@ -582,30 +467,6 @@ void Entity3D::render(const mat4& view, const mat4& proj, unsigned int shader)
{
if (!visible_) return;
// Set entity color uniform (used by Model3D and placeholder)
int colorLoc = glGetUniformLocation(shader, "u_entityColor");
if (colorLoc >= 0) {
glUniform4f(colorLoc,
color_.r / 255.0f,
color_.g / 255.0f,
color_.b / 255.0f,
color_.a / 255.0f);
}
// If we have a model, use it
if (model_) {
mat4 model = getModelMatrix();
// Use skinned rendering if model has skeleton and we have bone matrices
if (model_->hasSkeleton() && !bone_matrices_.empty()) {
model_->renderSkinned(shader, model, view, proj, bone_matrices_);
} else {
model_->render(shader, model, view, proj);
}
return;
}
// Otherwise, fall back to placeholder cube
// Initialize cube geometry if needed
if (!cubeInitialized_) {
initCubeGeometry();
@ -618,9 +479,17 @@ void Entity3D::render(const mat4& view, const mat4& proj, unsigned int shader)
// Get uniform locations (assuming shader is already bound)
int mvpLoc = glGetUniformLocation(shader, "u_mvp");
int modelLoc = glGetUniformLocation(shader, "u_model");
int colorLoc = glGetUniformLocation(shader, "u_entityColor");
if (mvpLoc >= 0) glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, mvp.data());
if (modelLoc >= 0) glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.data());
if (colorLoc >= 0) {
glUniform4f(colorLoc,
color_.r / 255.0f,
color_.g / 255.0f,
color_.b / 255.0f,
color_.a / 255.0f);
}
// Bind VBO and set up attributes
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO_);
@ -846,182 +715,6 @@ PyObject* Entity3D::get_viewport(PyEntity3DObject* self, void* closure)
Py_RETURN_NONE;
}
PyObject* Entity3D::get_model(PyEntity3DObject* self, void* closure)
{
auto model = self->data->getModel();
if (!model) {
Py_RETURN_NONE;
}
// Create Python Model3D object wrapping the shared_ptr
PyTypeObject* type = &mcrfpydef::PyModel3DType;
PyModel3DObject* obj = (PyModel3DObject*)type->tp_alloc(type, 0);
if (!obj) return NULL;
obj->data = model;
obj->weakreflist = nullptr;
return (PyObject*)obj;
}
int Entity3D::set_model(PyEntity3DObject* self, PyObject* value, void* closure)
{
if (value == Py_None) {
self->data->setModel(nullptr);
return 0;
}
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyModel3DType)) {
PyErr_SetString(PyExc_TypeError, "model must be a Model3D or None");
return -1;
}
PyModel3DObject* model_obj = (PyModel3DObject*)value;
self->data->setModel(model_obj->data);
return 0;
}
// Animation property getters/setters
PyObject* Entity3D::get_anim_clip(PyEntity3DObject* self, void* closure)
{
return PyUnicode_FromString(self->data->getAnimClip().c_str());
}
int Entity3D::set_anim_clip(PyEntity3DObject* self, PyObject* value, void* closure)
{
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError, "anim_clip must be a string");
return -1;
}
self->data->setAnimClip(PyUnicode_AsUTF8(value));
return 0;
}
PyObject* Entity3D::get_anim_time(PyEntity3DObject* self, void* closure)
{
return PyFloat_FromDouble(self->data->getAnimTime());
}
int Entity3D::set_anim_time(PyEntity3DObject* self, PyObject* value, void* closure)
{
if (!PyNumber_Check(value)) {
PyErr_SetString(PyExc_TypeError, "anim_time must be a number");
return -1;
}
self->data->setAnimTime((float)PyFloat_AsDouble(value));
return 0;
}
PyObject* Entity3D::get_anim_speed(PyEntity3DObject* self, void* closure)
{
return PyFloat_FromDouble(self->data->getAnimSpeed());
}
int Entity3D::set_anim_speed(PyEntity3DObject* self, PyObject* value, void* closure)
{
if (!PyNumber_Check(value)) {
PyErr_SetString(PyExc_TypeError, "anim_speed must be a number");
return -1;
}
self->data->setAnimSpeed((float)PyFloat_AsDouble(value));
return 0;
}
PyObject* Entity3D::get_anim_loop(PyEntity3DObject* self, void* closure)
{
return PyBool_FromLong(self->data->getAnimLoop() ? 1 : 0);
}
int Entity3D::set_anim_loop(PyEntity3DObject* self, PyObject* value, void* closure)
{
self->data->setAnimLoop(PyObject_IsTrue(value));
return 0;
}
PyObject* Entity3D::get_anim_paused(PyEntity3DObject* self, void* closure)
{
return PyBool_FromLong(self->data->getAnimPaused() ? 1 : 0);
}
int Entity3D::set_anim_paused(PyEntity3DObject* self, PyObject* value, void* closure)
{
self->data->setAnimPaused(PyObject_IsTrue(value));
return 0;
}
PyObject* Entity3D::get_anim_frame(PyEntity3DObject* self, void* closure)
{
return PyLong_FromLong(self->data->getAnimFrame());
}
PyObject* Entity3D::get_on_anim_complete(PyEntity3DObject* self, void* closure)
{
if (self->data->py_anim_callback_) {
Py_INCREF(self->data->py_anim_callback_);
return self->data->py_anim_callback_;
}
Py_RETURN_NONE;
}
int Entity3D::set_on_anim_complete(PyEntity3DObject* self, PyObject* value, void* closure)
{
// Clear existing callback
Py_XDECREF(self->data->py_anim_callback_);
if (value == Py_None) {
self->data->py_anim_callback_ = nullptr;
} else if (PyCallable_Check(value)) {
Py_INCREF(value);
self->data->py_anim_callback_ = value;
} else {
PyErr_SetString(PyExc_TypeError, "on_anim_complete must be callable or None");
return -1;
}
return 0;
}
PyObject* Entity3D::get_auto_animate(PyEntity3DObject* self, void* closure)
{
return PyBool_FromLong(self->data->getAutoAnimate() ? 1 : 0);
}
int Entity3D::set_auto_animate(PyEntity3DObject* self, PyObject* value, void* closure)
{
self->data->setAutoAnimate(PyObject_IsTrue(value));
return 0;
}
PyObject* Entity3D::get_walk_clip(PyEntity3DObject* self, void* closure)
{
return PyUnicode_FromString(self->data->getWalkClip().c_str());
}
int Entity3D::set_walk_clip(PyEntity3DObject* self, PyObject* value, void* closure)
{
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError, "walk_clip must be a string");
return -1;
}
self->data->setWalkClip(PyUnicode_AsUTF8(value));
return 0;
}
PyObject* Entity3D::get_idle_clip(PyEntity3DObject* self, void* closure)
{
return PyUnicode_FromString(self->data->getIdleClip().c_str());
}
int Entity3D::set_idle_clip(PyEntity3DObject* self, PyObject* value, void* closure)
{
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError, "idle_clip must be a string");
return -1;
}
self->data->setIdleClip(PyUnicode_AsUTF8(value));
return 0;
}
// Methods
PyObject* Entity3D::py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
@ -1121,47 +814,6 @@ PyObject* Entity3D::py_animate(PyEntity3DObject* self, PyObject* args, PyObject*
return NULL;
}
PyObject* Entity3D::py_follow_path(PyEntity3DObject* self, PyObject* args)
{
PyObject* path_list;
if (!PyArg_ParseTuple(args, "O", &path_list)) {
return NULL;
}
if (!PyList_Check(path_list)) {
PyErr_SetString(PyExc_TypeError, "follow_path() requires a list of (x, z) tuples");
return NULL;
}
std::vector<std::pair<int, int>> path;
Py_ssize_t len = PyList_Size(path_list);
for (Py_ssize_t i = 0; i < len; ++i) {
PyObject* item = PyList_GetItem(path_list, i);
if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) {
PyErr_SetString(PyExc_TypeError, "Each path element must be (x, z) tuple");
return NULL;
}
int x = static_cast<int>(PyLong_AsLong(PyTuple_GetItem(item, 0)));
int z = static_cast<int>(PyLong_AsLong(PyTuple_GetItem(item, 1)));
if (PyErr_Occurred()) return NULL;
path.emplace_back(x, z);
}
self->data->followPath(path);
Py_RETURN_NONE;
}
PyObject* Entity3D::py_clear_path(PyEntity3DObject* self, PyObject* args)
{
self->data->clearPath();
Py_RETURN_NONE;
}
PyObject* Entity3D::get_is_moving(PyEntity3DObject* self, void* closure)
{
return PyBool_FromLong(self->data->isMoving() ? 1 : 0);
}
// Method and GetSet tables
PyMethodDef Entity3D::methods[] = {
@ -1182,14 +834,6 @@ PyMethodDef Entity3D::methods[] = {
{"animate", (PyCFunction)Entity3D::py_animate, METH_VARARGS | METH_KEYWORDS,
"animate(property, target, duration, easing=None, callback=None)\n\n"
"Animate a property over time. (Not yet implemented)"},
{"follow_path", (PyCFunction)Entity3D::py_follow_path, METH_VARARGS,
"follow_path(path)\n\n"
"Queue path positions for smooth movement.\n\n"
"Args:\n"
" path: List of (x, z) tuples (as returned by path_to())"},
{"clear_path", (PyCFunction)Entity3D::py_clear_path, METH_NOARGS,
"clear_path()\n\n"
"Clear the movement queue and stop at current position."},
{NULL} // Sentinel
};
@ -1210,33 +854,6 @@ PyGetSetDef Entity3D::getsetters[] = {
"Entity render color.", NULL},
{"viewport", (getter)Entity3D::get_viewport, NULL,
"Owning Viewport3D (read-only).", NULL},
{"model", (getter)Entity3D::get_model, (setter)Entity3D::set_model,
"3D model (Model3D). If None, uses placeholder cube.", NULL},
// Animation properties
{"anim_clip", (getter)Entity3D::get_anim_clip, (setter)Entity3D::set_anim_clip,
"Current animation clip name. Set to play an animation.", NULL},
{"anim_time", (getter)Entity3D::get_anim_time, (setter)Entity3D::set_anim_time,
"Current time position in animation (seconds).", NULL},
{"anim_speed", (getter)Entity3D::get_anim_speed, (setter)Entity3D::set_anim_speed,
"Animation playback speed multiplier. 1.0 = normal speed.", NULL},
{"anim_loop", (getter)Entity3D::get_anim_loop, (setter)Entity3D::set_anim_loop,
"Whether animation loops when it reaches the end.", NULL},
{"anim_paused", (getter)Entity3D::get_anim_paused, (setter)Entity3D::set_anim_paused,
"Whether animation playback is paused.", NULL},
{"anim_frame", (getter)Entity3D::get_anim_frame, NULL,
"Current animation frame number (read-only, approximate at 30fps).", NULL},
{"on_anim_complete", (getter)Entity3D::get_on_anim_complete, (setter)Entity3D::set_on_anim_complete,
"Callback(entity, clip_name) when non-looping animation ends.", NULL},
{"auto_animate", (getter)Entity3D::get_auto_animate, (setter)Entity3D::set_auto_animate,
"Enable auto-play of walk/idle clips based on movement.", NULL},
{"walk_clip", (getter)Entity3D::get_walk_clip, (setter)Entity3D::set_walk_clip,
"Animation clip to play when entity is moving.", NULL},
{"idle_clip", (getter)Entity3D::get_idle_clip, (setter)Entity3D::set_idle_clip,
"Animation clip to play when entity is stationary.", NULL},
{"is_moving", (getter)Entity3D::get_is_moving, NULL,
"Whether entity is currently moving (read-only).", NULL},
{NULL} // Sentinel
};

View file

@ -11,13 +11,11 @@
#include <queue>
#include <vector>
#include <string>
#include <functional>
namespace mcrf {
// Forward declarations
class Viewport3D;
class Model3D;
} // namespace mcrf
@ -96,10 +94,6 @@ public:
int getSpriteIndex() const { return sprite_index_; }
void setSpriteIndex(int idx) { sprite_index_ = idx; }
// 3D model (if null, uses placeholder cube)
std::shared_ptr<Model3D> getModel() const { return model_; }
void setModel(std::shared_ptr<Model3D> m) { model_ = m; }
// =========================================================================
// Viewport Integration
// =========================================================================
@ -159,57 +153,6 @@ public:
bool getProperty(const std::string& name, float& value) const;
bool hasProperty(const std::string& name) const;
// =========================================================================
// Skeletal Animation
// =========================================================================
/// Get current animation clip name
const std::string& getAnimClip() const { return anim_clip_; }
/// Set animation clip by name (starts playing)
void setAnimClip(const std::string& name);
/// Get/set animation time (position in clip)
float getAnimTime() const { return anim_time_; }
void setAnimTime(float t) { anim_time_ = t; }
/// Get/set playback speed (1.0 = normal)
float getAnimSpeed() const { return anim_speed_; }
void setAnimSpeed(float s) { anim_speed_ = s; }
/// Get/set looping state
bool getAnimLoop() const { return anim_loop_; }
void setAnimLoop(bool l) { anim_loop_ = l; }
/// Get/set pause state
bool getAnimPaused() const { return anim_paused_; }
void setAnimPaused(bool p) { anim_paused_ = p; }
/// Get current animation frame (approximate)
int getAnimFrame() const;
/// Update skeletal animation (call before render)
void updateAnimation(float dt);
/// Get computed bone matrices for shader
const std::vector<mat4>& getBoneMatrices() const { return bone_matrices_; }
/// Animation complete callback type
using AnimCompleteCallback = std::function<void(Entity3D*, const std::string&)>;
/// Set animation complete callback
void setOnAnimComplete(AnimCompleteCallback cb) { on_anim_complete_ = cb; }
/// Auto-animate settings (play walk/idle based on movement)
bool getAutoAnimate() const { return auto_animate_; }
void setAutoAnimate(bool a) { auto_animate_ = a; }
const std::string& getWalkClip() const { return walk_clip_; }
void setWalkClip(const std::string& c) { walk_clip_ = c; }
const std::string& getIdleClip() const { return idle_clip_; }
void setIdleClip(const std::string& c) { idle_clip_ = c; }
// =========================================================================
// Rendering
// =========================================================================
@ -242,29 +185,6 @@ public:
static PyObject* get_color(PyEntity3DObject* self, void* closure);
static int set_color(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_viewport(PyEntity3DObject* self, void* closure);
static PyObject* get_model(PyEntity3DObject* self, void* closure);
static int set_model(PyEntity3DObject* self, PyObject* value, void* closure);
// Animation property getters/setters
static PyObject* get_anim_clip(PyEntity3DObject* self, void* closure);
static int set_anim_clip(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_anim_time(PyEntity3DObject* self, void* closure);
static int set_anim_time(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_anim_speed(PyEntity3DObject* self, void* closure);
static int set_anim_speed(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_anim_loop(PyEntity3DObject* self, void* closure);
static int set_anim_loop(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_anim_paused(PyEntity3DObject* self, void* closure);
static int set_anim_paused(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_anim_frame(PyEntity3DObject* self, void* closure);
static PyObject* get_on_anim_complete(PyEntity3DObject* self, void* closure);
static int set_on_anim_complete(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_auto_animate(PyEntity3DObject* self, void* closure);
static int set_auto_animate(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_walk_clip(PyEntity3DObject* self, void* closure);
static int set_walk_clip(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_idle_clip(PyEntity3DObject* self, void* closure);
static int set_idle_clip(PyEntity3DObject* self, PyObject* value, void* closure);
// Methods
static PyObject* py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
@ -272,9 +192,6 @@ public:
static PyObject* py_at(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_update_visibility(PyEntity3DObject* self, PyObject* args);
static PyObject* py_animate(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_follow_path(PyEntity3DObject* self, PyObject* args);
static PyObject* py_clear_path(PyEntity3DObject* self, PyObject* args);
static PyObject* get_is_moving(PyEntity3DObject* self, void* closure);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
@ -300,7 +217,6 @@ private:
bool visible_ = true;
sf::Color color_ = sf::Color(200, 100, 50); // Default orange
int sprite_index_ = 0;
std::shared_ptr<Model3D> model_; // 3D model (null = placeholder cube)
// Viewport (weak reference to avoid cycles)
std::weak_ptr<Viewport3D> viewport_;
@ -316,24 +232,6 @@ private:
float move_speed_ = 5.0f; // Cells per second
vec3 move_start_pos_;
// Skeletal animation state
std::string anim_clip_; // Current animation clip name
float anim_time_ = 0.0f; // Current time in animation
float anim_speed_ = 1.0f; // Playback speed multiplier
bool anim_loop_ = true; // Loop animation
bool anim_paused_ = false; // Pause playback
std::vector<mat4> bone_matrices_; // Computed bone matrices for shader
AnimCompleteCallback on_anim_complete_; // Callback when animation ends
// Auto-animate state
bool auto_animate_ = true; // Auto-play walk/idle based on movement
std::string walk_clip_ = "walk"; // Clip to play when moving
std::string idle_clip_ = "idle"; // Clip to play when stopped
bool was_moving_ = false; // Track movement state for auto-animate
// Python callback for animation complete
PyObject* py_anim_callback_ = nullptr;
// Helper to initialize voxel state
void initVoxelState() const;

View file

@ -610,116 +610,6 @@ struct quat {
}
};
// =============================================================================
// Frustum - View frustum for culling
// =============================================================================
struct Plane {
vec3 normal;
float distance;
Plane() : normal(0, 1, 0), distance(0) {}
Plane(const vec3& n, float d) : normal(n), distance(d) {}
// Distance from plane to point (positive = in front, negative = behind)
float distanceToPoint(const vec3& point) const {
return normal.dot(point) + distance;
}
};
struct Frustum {
// Six planes: left, right, bottom, top, near, far
Plane planes[6];
// Extract frustum planes from view-projection matrix
// Uses Gribb/Hartmann method
void extractFromMatrix(const mat4& viewProj) {
const float* m = viewProj.m;
// Left plane
planes[0].normal.x = m[3] + m[0];
planes[0].normal.y = m[7] + m[4];
planes[0].normal.z = m[11] + m[8];
planes[0].distance = m[15] + m[12];
// Right plane
planes[1].normal.x = m[3] - m[0];
planes[1].normal.y = m[7] - m[4];
planes[1].normal.z = m[11] - m[8];
planes[1].distance = m[15] - m[12];
// Bottom plane
planes[2].normal.x = m[3] + m[1];
planes[2].normal.y = m[7] + m[5];
planes[2].normal.z = m[11] + m[9];
planes[2].distance = m[15] + m[13];
// Top plane
planes[3].normal.x = m[3] - m[1];
planes[3].normal.y = m[7] - m[5];
planes[3].normal.z = m[11] - m[9];
planes[3].distance = m[15] - m[13];
// Near plane
planes[4].normal.x = m[3] + m[2];
planes[4].normal.y = m[7] + m[6];
planes[4].normal.z = m[11] + m[10];
planes[4].distance = m[15] + m[14];
// Far plane
planes[5].normal.x = m[3] - m[2];
planes[5].normal.y = m[7] - m[6];
planes[5].normal.z = m[11] - m[10];
planes[5].distance = m[15] - m[14];
// Normalize all planes
for (int i = 0; i < 6; i++) {
float len = planes[i].normal.length();
if (len > 0.0001f) {
planes[i].normal = planes[i].normal / len;
planes[i].distance /= len;
}
}
}
// Test if a point is inside the frustum
bool containsPoint(const vec3& point) const {
for (int i = 0; i < 6; i++) {
if (planes[i].distanceToPoint(point) < 0) {
return false;
}
}
return true;
}
// Test if a sphere intersects or is inside the frustum
bool containsSphere(const vec3& center, float radius) const {
for (int i = 0; i < 6; i++) {
if (planes[i].distanceToPoint(center) < -radius) {
return false; // Sphere is completely behind this plane
}
}
return true;
}
// Test if an axis-aligned bounding box intersects the frustum
bool containsAABB(const vec3& min, const vec3& max) const {
for (int i = 0; i < 6; i++) {
// Find the positive vertex (furthest along plane normal)
vec3 pVertex;
pVertex.x = (planes[i].normal.x >= 0) ? max.x : min.x;
pVertex.y = (planes[i].normal.y >= 0) ? max.y : min.y;
pVertex.z = (planes[i].normal.z >= 0) ? max.z : min.z;
// If positive vertex is behind plane, box is outside
if (planes[i].distanceToPoint(pVertex) < 0) {
return false;
}
}
return true;
}
};
// =============================================================================
// Utility constants and functions
// =============================================================================

View file

@ -1,11 +1,8 @@
// MeshLayer.cpp - Static 3D geometry layer implementation
#include "MeshLayer.h"
#include "Model3D.h"
#include "Viewport3D.h"
#include "Shader3D.h"
#include "../platform/GLContext.h"
#include <cmath>
// GL headers based on backend
#if defined(MCRF_SDL2)
@ -380,57 +377,53 @@ void MeshLayer::uploadToGPU() {
// Rendering
// =============================================================================
void MeshLayer::render(unsigned int shader, const mat4& model, const mat4& view, const mat4& projection) {
void MeshLayer::render(const mat4& model, const mat4& view, const mat4& projection) {
#ifdef MCRF_HAS_GL
if (!gl::isGLReady()) {
if (!gl::isGLReady() || vertices_.empty()) {
return;
}
// Render terrain geometry if present
if (!vertices_.empty()) {
// Upload to GPU if needed
if (dirty_ || vbo_ == 0) {
uploadToGPU();
}
if (vbo_ != 0) {
// Bind VBO
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
// Vertex format: pos(3) + texcoord(2) + normal(3) + color(4) = 12 floats = 48 bytes
int stride = sizeof(MeshVertex);
// Set up vertex attributes
glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION);
glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, position)));
glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, texcoord)));
glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, normal)));
glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR);
glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, color)));
// Draw triangles
glDrawArrays(GL_TRIANGLES, 0, static_cast<int>(vertices_.size()));
// Cleanup
glDisableVertexAttribArray(Shader3D::ATTRIB_POSITION);
glDisableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
glDisableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
glDisableVertexAttribArray(Shader3D::ATTRIB_COLOR);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
// Upload to GPU if needed
if (dirty_ || vbo_ == 0) {
uploadToGPU();
}
// Render mesh instances
renderMeshInstances(shader, view, projection);
if (vbo_ == 0) {
return;
}
// Bind VBO
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
// Vertex format: pos(3) + texcoord(2) + normal(3) + color(4) = 12 floats = 48 bytes
int stride = sizeof(MeshVertex);
// Set up vertex attributes
glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION);
glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, position)));
glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, texcoord)));
glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, normal)));
glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR);
glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, color)));
// Draw triangles
glDrawArrays(GL_TRIANGLES, 0, static_cast<int>(vertices_.size()));
// Cleanup
glDisableVertexAttribArray(Shader3D::ATTRIB_POSITION);
glDisableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
glDisableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
glDisableVertexAttribArray(Shader3D::ATTRIB_COLOR);
glBindBuffer(GL_ARRAY_BUFFER, 0);
#endif
}
@ -453,114 +446,6 @@ vec3 MeshLayer::computeFaceNormal(const vec3& v0, const vec3& v1, const vec3& v2
return edge1.cross(edge2).normalized();
}
// =============================================================================
// Mesh Instances
// =============================================================================
size_t MeshLayer::addMesh(std::shared_ptr<Model3D> model, const vec3& pos,
float rotation, const vec3& scale) {
MeshInstance instance(model, pos, rotation, scale);
meshInstances_.push_back(std::move(instance));
return meshInstances_.size() - 1;
}
void MeshLayer::removeMesh(size_t index) {
if (index < meshInstances_.size()) {
meshInstances_.erase(meshInstances_.begin() + index);
}
}
void MeshLayer::clearMeshes() {
meshInstances_.clear();
}
MeshInstance* MeshLayer::getMeshInstance(size_t index) {
if (index < meshInstances_.size()) {
return &meshInstances_[index];
}
return nullptr;
}
const MeshInstance* MeshLayer::getMeshInstance(size_t index) const {
if (index < meshInstances_.size()) {
return &meshInstances_[index];
}
return nullptr;
}
void MeshLayer::renderMeshInstances(unsigned int shader, const mat4& view, const mat4& projection) {
#ifdef MCRF_HAS_GL
if (!gl::isGLReady() || meshInstances_.empty()) {
return;
}
for (const auto& inst : meshInstances_) {
if (!inst.model) continue;
// Build model matrix: translate * rotateY * scale
mat4 model = mat4::identity();
model = model * mat4::translate(inst.position);
model = model * mat4::rotateY(inst.rotation * 3.14159265f / 180.0f);
model = model * mat4::scale(inst.scale);
// Render the model
inst.model->render(shader, model, view, projection);
}
#endif
}
// =============================================================================
// Collision Helpers
// =============================================================================
void MeshLayer::placeBlocking(int gridX, int gridZ, int footprintW, int footprintD,
bool walkable, bool transparent) {
if (!viewport_) return;
for (int dz = 0; dz < footprintD; dz++) {
for (int dx = 0; dx < footprintW; dx++) {
int cx = gridX + dx;
int cz = gridZ + dz;
if (viewport_->isValidCell(cx, cz)) {
VoxelPoint& cell = viewport_->at(cx, cz);
cell.walkable = walkable;
cell.transparent = transparent;
viewport_->syncTCODCell(cx, cz);
}
}
}
}
void MeshLayer::placeBlockingAuto(std::shared_ptr<Model3D> model, const vec3& worldPos,
float rotation, bool walkable, bool transparent) {
if (!viewport_ || !model) return;
float cellSize = viewport_->getCellSize();
if (cellSize <= 0) cellSize = 1.0f;
// Get model bounds
auto [minBounds, maxBounds] = model->getBounds();
// Calculate world-space extents (ignoring rotation for simplicity)
float extentX = (maxBounds.x - minBounds.x);
float extentZ = (maxBounds.z - minBounds.z);
// Calculate footprint in cells (always at least 1x1)
int footprintW = std::max(1, static_cast<int>(std::ceil(extentX / cellSize)));
int footprintD = std::max(1, static_cast<int>(std::ceil(extentZ / cellSize)));
// Calculate grid position (center the footprint on the world position)
int gridX = static_cast<int>(std::floor(worldPos.x / cellSize - footprintW * 0.5f));
int gridZ = static_cast<int>(std::floor(worldPos.z / cellSize - footprintD * 0.5f));
// Place blocking cells
placeBlocking(gridX, gridZ, footprintW, footprintD, walkable, transparent);
}
// =============================================================================
// Private Helpers
// =============================================================================
void MeshLayer::computeVertexNormals() {
// For terrain mesh, we can average normals at shared positions
// This is a simplified approach - works well for regular grids

View file

@ -10,12 +10,6 @@
#include <vector>
#include <libtcod.h> // For TCOD_heightmap_t
// Forward declarations
namespace mcrf {
class Viewport3D;
class Model3D;
}
namespace mcrf {
// =============================================================================
@ -55,25 +49,6 @@ struct TextureRange {
: minHeight(min), maxHeight(max), spriteIndex(index) {}
};
// =============================================================================
// MeshInstance - Instance of a Model3D placed in the world
// =============================================================================
struct MeshInstance {
std::shared_ptr<Model3D> model; // The model to render
vec3 position; // World position
float rotation = 0.0f; // Y-axis rotation in degrees
vec3 scale = vec3(1.0f, 1.0f, 1.0f); // Scale factors
MeshInstance()
: model(nullptr), position(0, 0, 0), rotation(0.0f), scale(1.0f, 1.0f, 1.0f)
{}
MeshInstance(std::shared_ptr<Model3D> m, const vec3& pos, float rot = 0.0f, const vec3& s = vec3(1, 1, 1))
: model(m), position(pos), rotation(rot), scale(s)
{}
};
// =============================================================================
// MeshLayer - Container for static 3D geometry
// =============================================================================
@ -140,60 +115,6 @@ public:
/// Clear all geometry
void clear();
// =========================================================================
// Mesh Instances (Model3D placement)
// =========================================================================
/// Add a Model3D instance at a world position
/// @param model The Model3D to render
/// @param pos World position
/// @param rotation Y-axis rotation in degrees
/// @param scale Scale factor (uniform or per-axis)
/// @return Instance index for later removal
size_t addMesh(std::shared_ptr<Model3D> model, const vec3& pos,
float rotation = 0.0f, const vec3& scale = vec3(1, 1, 1));
/// Remove a mesh instance by index
/// @param index Index returned by addMesh()
void removeMesh(size_t index);
/// Clear all mesh instances
void clearMeshes();
/// Get number of mesh instances
size_t getMeshInstanceCount() const { return meshInstances_.size(); }
/// Get mesh instance by index (for Python access)
MeshInstance* getMeshInstance(size_t index);
const MeshInstance* getMeshInstance(size_t index) const;
// =========================================================================
// Collision Helpers
// =========================================================================
/// Set parent viewport (for collision marking)
void setViewport(Viewport3D* vp) { viewport_ = vp; }
Viewport3D* getViewport() const { return viewport_; }
/// Mark grid cells as blocking for pathfinding/FOV
/// @param gridX Grid X coordinate (top-left of footprint)
/// @param gridZ Grid Z coordinate (top-left of footprint)
/// @param footprintW Footprint width in cells
/// @param footprintD Footprint depth in cells
/// @param walkable Set cells walkable state
/// @param transparent Set cells transparent state
void placeBlocking(int gridX, int gridZ, int footprintW, int footprintD,
bool walkable = false, bool transparent = false);
/// Auto-detect footprint from model bounds and place blocking
/// @param model The model to compute footprint from
/// @param worldPos World position of the model
/// @param rotation Y-axis rotation of the model
/// @param walkable Set cells walkable state
/// @param transparent Set cells transparent state
void placeBlockingAuto(std::shared_ptr<Model3D> model, const vec3& worldPos,
float rotation, bool walkable = false, bool transparent = false);
// =========================================================================
// GPU Upload and Rendering
// =========================================================================
@ -203,11 +124,10 @@ public:
void uploadToGPU();
/// Render this layer
/// @param shader Shader program handle (for mesh instances)
/// @param model Model transformation matrix
/// @param view View matrix from camera
/// @param projection Projection matrix from camera
void render(unsigned int shader, const mat4& model, const mat4& view, const mat4& projection);
void render(const mat4& model, const mat4& view, const mat4& projection);
/// Get model matrix (identity by default, override for positioned layers)
mat4 getModelMatrix() const { return modelMatrix_; }
@ -244,19 +164,10 @@ private:
// Transform
mat4 modelMatrix_ = mat4::identity();
// Mesh instances (Model3D placements)
std::vector<MeshInstance> meshInstances_;
// Parent viewport for collision helpers
Viewport3D* viewport_ = nullptr;
// Helper methods
void cleanupGPU();
vec3 computeFaceNormal(const vec3& v0, const vec3& v1, const vec3& v2);
void computeVertexNormals();
// Render mesh instances
void renderMeshInstances(unsigned int shader, const mat4& view, const mat4& projection);
};
} // namespace mcrf

File diff suppressed because it is too large Load diff

View file

@ -1,431 +0,0 @@
// Model3D.h - 3D model resource for McRogueFace
// Supports loading from glTF 2.0 (.glb) files and procedural primitives
#pragma once
#include "Common.h"
#include "Math3D.h"
#include "MeshLayer.h" // For MeshVertex
#include "Python.h"
#include "structmember.h"
#include <memory>
#include <string>
#include <vector>
namespace mcrf {
// Forward declarations
class Shader3D;
// =============================================================================
// Bone - Single bone in a skeleton
// =============================================================================
struct Bone {
std::string name;
int parent_index = -1; // -1 for root bones
mat4 inverse_bind_matrix; // Transforms from model space to bone space
mat4 local_transform; // Default local transform (rest pose)
};
// =============================================================================
// Skeleton - Bone hierarchy for skeletal animation
// =============================================================================
struct Skeleton {
std::vector<Bone> bones;
std::vector<int> root_bones; // Indices of bones with parent_index == -1
/// Find bone by name, returns -1 if not found
int findBone(const std::string& name) const {
for (size_t i = 0; i < bones.size(); i++) {
if (bones[i].name == name) return static_cast<int>(i);
}
return -1;
}
/// Compute global (model-space) transforms for all bones
void computeGlobalTransforms(const std::vector<mat4>& local_transforms,
std::vector<mat4>& global_out) const {
global_out.resize(bones.size());
for (size_t i = 0; i < bones.size(); i++) {
if (bones[i].parent_index < 0) {
global_out[i] = local_transforms[i];
} else {
global_out[i] = global_out[bones[i].parent_index] * local_transforms[i];
}
}
}
/// Compute final bone matrices for shader (global * inverse_bind)
void computeBoneMatrices(const std::vector<mat4>& global_transforms,
std::vector<mat4>& matrices_out) const {
matrices_out.resize(bones.size());
for (size_t i = 0; i < bones.size(); i++) {
matrices_out[i] = global_transforms[i] * bones[i].inverse_bind_matrix;
}
}
};
// =============================================================================
// AnimationChannel - Animates a single property of a single bone
// =============================================================================
struct AnimationChannel {
int bone_index = -1;
enum class Path {
Translation,
Rotation,
Scale
} path = Path::Translation;
// Keyframe times (shared for all values in this channel)
std::vector<float> times;
// Keyframe values (only one of these is populated based on path)
std::vector<vec3> translations;
std::vector<quat> rotations;
std::vector<vec3> scales;
/// Sample the channel at a given time, returning the interpolated transform component
/// For Translation/Scale: writes to trans_out
/// For Rotation: writes to rot_out
void sample(float time, vec3& trans_out, quat& rot_out, vec3& scale_out) const;
};
// =============================================================================
// AnimationClip - Named animation containing multiple channels
// =============================================================================
struct AnimationClip {
std::string name;
float duration = 0.0f;
std::vector<AnimationChannel> channels;
/// Sample the animation at a given time, producing bone local transforms
/// @param time Current time in the animation
/// @param num_bones Total number of bones (for output sizing)
/// @param default_transforms Default local transforms for bones without animation
/// @param local_out Output: interpolated local transforms for each bone
void sample(float time, size_t num_bones,
const std::vector<mat4>& default_transforms,
std::vector<mat4>& local_out) const;
};
// =============================================================================
// SkinnedVertex - Vertex with bone weights for skeletal animation
// =============================================================================
struct SkinnedVertex {
vec3 position;
vec2 texcoord;
vec3 normal;
vec4 color;
vec4 bone_ids; // Up to 4 bone indices (as floats for GLES2 compatibility)
vec4 bone_weights; // Corresponding weights (should sum to 1.0)
};
// =============================================================================
// SkinnedMesh - Submesh with skinning data
// =============================================================================
struct SkinnedMesh {
unsigned int vbo = 0;
unsigned int ebo = 0;
int vertex_count = 0;
int index_count = 0;
int material_index = -1;
bool is_skinned = false; // True if this mesh has bone weights
SkinnedMesh() = default;
~SkinnedMesh() = default;
SkinnedMesh(const SkinnedMesh&) = delete;
SkinnedMesh& operator=(const SkinnedMesh&) = delete;
SkinnedMesh(SkinnedMesh&& other) noexcept;
SkinnedMesh& operator=(SkinnedMesh&& other) noexcept;
};
// =============================================================================
// ModelMesh - Single submesh within a Model3D (legacy non-skinned)
// =============================================================================
struct ModelMesh {
unsigned int vbo = 0; // Vertex buffer object
unsigned int ebo = 0; // Element (index) buffer object
int vertex_count = 0; // Number of vertices
int index_count = 0; // Number of indices (0 if non-indexed)
int material_index = -1; // Index into materials array (-1 = no material)
ModelMesh() = default;
~ModelMesh() = default;
// Move only
ModelMesh(const ModelMesh&) = delete;
ModelMesh& operator=(const ModelMesh&) = delete;
ModelMesh(ModelMesh&& other) noexcept;
ModelMesh& operator=(ModelMesh&& other) noexcept;
};
// =============================================================================
// Model3D - 3D model resource
// =============================================================================
class Model3D : public std::enable_shared_from_this<Model3D> {
public:
// Python integration
PyObject* self = nullptr;
uint64_t serial_number = 0;
Model3D();
~Model3D();
// No copy, allow move
Model3D(const Model3D&) = delete;
Model3D& operator=(const Model3D&) = delete;
Model3D(Model3D&& other) noexcept;
Model3D& operator=(Model3D&& other) noexcept;
// =========================================================================
// Loading
// =========================================================================
/// Load model from glTF 2.0 binary file (.glb)
/// @param path Path to the .glb file
/// @return Shared pointer to loaded model, or nullptr on failure
static std::shared_ptr<Model3D> load(const std::string& path);
/// Get last error message from load()
static const std::string& getLastError() { return lastError_; }
// =========================================================================
// Procedural Primitives
// =========================================================================
/// Create a unit cube (1x1x1 centered at origin)
static std::shared_ptr<Model3D> cube(float size = 1.0f);
/// Create a flat plane
/// @param width Size along X axis
/// @param depth Size along Z axis
/// @param segments Subdivisions (1 = single quad)
static std::shared_ptr<Model3D> plane(float width = 1.0f, float depth = 1.0f, int segments = 1);
/// Create a UV sphere
/// @param radius Sphere radius
/// @param segments Horizontal segments (longitude)
/// @param rings Vertical rings (latitude)
static std::shared_ptr<Model3D> sphere(float radius = 0.5f, int segments = 16, int rings = 12);
// =========================================================================
// Model Information
// =========================================================================
/// Get model name (from file or "primitive")
const std::string& getName() const { return name_; }
void setName(const std::string& n) { name_ = n; }
/// Get total vertex count across all meshes
int getVertexCount() const;
/// Get total triangle count across all meshes
int getTriangleCount() const;
/// Get axis-aligned bounding box
/// @return Pair of (min, max) corners
std::pair<vec3, vec3> getBounds() const { return {bounds_min_, bounds_max_}; }
/// Check if model has skeletal animation data
bool hasSkeleton() const { return has_skeleton_; }
/// Get number of submeshes (regular + skinned)
size_t getMeshCount() const { return meshes_.size() + skinned_meshes_.size(); }
// =========================================================================
// Skeleton & Animation
// =========================================================================
/// Get skeleton (may be empty if no skeleton)
const Skeleton& getSkeleton() const { return skeleton_; }
/// Get number of bones
size_t getBoneCount() const { return skeleton_.bones.size(); }
/// Get animation clips
const std::vector<AnimationClip>& getAnimationClips() const { return animation_clips_; }
/// Get animation clip names
std::vector<std::string> getAnimationClipNames() const {
std::vector<std::string> names;
for (const auto& clip : animation_clips_) {
names.push_back(clip.name);
}
return names;
}
/// Find animation clip by name (returns nullptr if not found)
const AnimationClip* findClip(const std::string& name) const {
for (const auto& clip : animation_clips_) {
if (clip.name == name) return &clip;
}
return nullptr;
}
/// Get default bone transforms (rest pose)
const std::vector<mat4>& getDefaultBoneTransforms() const { return default_bone_transforms_; }
// =========================================================================
// Rendering
// =========================================================================
/// Render all meshes
/// @param shader Shader program handle (already bound)
/// @param model Model transformation matrix
/// @param view View matrix
/// @param projection Projection matrix
void render(unsigned int shader, const mat4& model, const mat4& view, const mat4& projection);
/// Render with skeletal animation
/// @param shader Shader program handle (already bound, should be skinned shader)
/// @param model Model transformation matrix
/// @param view View matrix
/// @param projection Projection matrix
/// @param bone_matrices Final bone matrices (global * inverse_bind)
void renderSkinned(unsigned int shader, const mat4& model, const mat4& view,
const mat4& projection, const std::vector<mat4>& bone_matrices);
// =========================================================================
// Python API
// =========================================================================
static int init(PyObject* self, PyObject* args, PyObject* kwds);
static PyObject* repr(PyObject* self);
// Class methods (static constructors)
static PyObject* py_cube(PyObject* cls, PyObject* args, PyObject* kwds);
static PyObject* py_plane(PyObject* cls, PyObject* args, PyObject* kwds);
static PyObject* py_sphere(PyObject* cls, PyObject* args, PyObject* kwds);
// Property getters
static PyObject* get_vertex_count(PyObject* self, void* closure);
static PyObject* get_triangle_count(PyObject* self, void* closure);
static PyObject* get_has_skeleton(PyObject* self, void* closure);
static PyObject* get_bounds(PyObject* self, void* closure);
static PyObject* get_name(PyObject* self, void* closure);
static PyObject* get_mesh_count(PyObject* self, void* closure);
static PyObject* get_bone_count(PyObject* self, void* closure);
static PyObject* get_animation_clips(PyObject* self, void* closure);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
private:
// Model data
std::string name_;
std::vector<ModelMesh> meshes_;
std::vector<SkinnedMesh> skinned_meshes_; // Skinned meshes with bone weights
// Bounds
vec3 bounds_min_ = vec3(0, 0, 0);
vec3 bounds_max_ = vec3(0, 0, 0);
// Skeletal animation data
bool has_skeleton_ = false;
Skeleton skeleton_;
std::vector<AnimationClip> animation_clips_;
std::vector<mat4> default_bone_transforms_; // Rest pose local transforms
// Error handling
static std::string lastError_;
// Helper methods
void cleanupGPU();
void computeBounds(const std::vector<MeshVertex>& vertices);
/// Create VBO/EBO from vertex and index data
/// @return ModelMesh with GPU resources allocated
static ModelMesh createMesh(const std::vector<MeshVertex>& vertices,
const std::vector<uint32_t>& indices);
/// Create VBO/EBO from skinned vertex and index data
static SkinnedMesh createSkinnedMesh(const std::vector<SkinnedVertex>& vertices,
const std::vector<uint32_t>& indices);
// glTF loading helpers
void loadSkeleton(void* cgltf_data); // void* to avoid header dependency
void loadAnimations(void* cgltf_data);
int findJointIndex(void* cgltf_skin, void* node);
};
} // namespace mcrf
// =============================================================================
// Python type definition
// =============================================================================
typedef struct PyModel3DObject {
PyObject_HEAD
std::shared_ptr<mcrf::Model3D> data;
PyObject* weakreflist;
} PyModel3DObject;
namespace mcrfpydef {
inline PyTypeObject PyModel3DType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Model3D",
.tp_basicsize = sizeof(PyModel3DObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self)
{
PyModel3DObject* obj = (PyModel3DObject*)self;
PyObject_GC_UnTrack(self);
if (obj->weakreflist != NULL) {
PyObject_ClearWeakRefs(self);
}
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_repr = mcrf::Model3D::repr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
.tp_doc = PyDoc_STR(
"Model3D(path=None)\n\n"
"A 3D model resource that can be rendered by Entity3D.\n\n"
"Args:\n"
" path (str, optional): Path to .glb file to load. If None, creates empty model.\n\n"
"Class Methods:\n"
" cube(size=1.0) -> Model3D: Create a unit cube\n"
" plane(width=1.0, depth=1.0, segments=1) -> Model3D: Create a flat plane\n"
" sphere(radius=0.5, segments=16, rings=12) -> Model3D: Create a UV sphere\n\n"
"Properties:\n"
" name (str, read-only): Model name\n"
" vertex_count (int, read-only): Total vertices across all meshes\n"
" triangle_count (int, read-only): Total triangles across all meshes\n"
" has_skeleton (bool, read-only): Whether model has skeletal animation data\n"
" bounds (tuple, read-only): AABB as ((min_x, min_y, min_z), (max_x, max_y, max_z))\n"
" mesh_count (int, read-only): Number of submeshes\n"
" bone_count (int, read-only): Number of bones in skeleton\n"
" animation_clips (list, read-only): List of animation clip names"
),
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
return 0;
},
.tp_clear = [](PyObject* self) -> int {
return 0;
},
.tp_methods = mcrf::Model3D::methods,
.tp_getset = mcrf::Model3D::getsetters,
.tp_init = mcrf::Model3D::init,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
PyModel3DObject* self = (PyModel3DObject*)type->tp_alloc(type, 0);
if (self) {
self->data = std::make_shared<mcrf::Model3D>();
self->weakreflist = nullptr;
}
return (PyObject*)self;
}
};
} // namespace mcrfpydef

File diff suppressed because it is too large Load diff

View file

@ -1,179 +0,0 @@
// PyVoxelGrid.h - Python bindings for VoxelGrid
// Part of McRogueFace 3D Extension - Milestones 9, 11
#pragma once
#include "../Common.h"
#include "Python.h"
#include "VoxelGrid.h"
#include <memory>
// =============================================================================
// Python object structures
// =============================================================================
typedef struct PyVoxelGridObject {
PyObject_HEAD
std::shared_ptr<mcrf::VoxelGrid> data;
PyObject* weakreflist;
} PyVoxelGridObject;
typedef struct PyVoxelRegionObject {
PyObject_HEAD
std::shared_ptr<mcrf::VoxelRegion> data;
} PyVoxelRegionObject;
// =============================================================================
// Python binding classes
// =============================================================================
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);
// Properties - mesh generation (Milestone 13)
static PyObject* get_greedy_meshing(PyVoxelGridObject* self, void* closure);
static int set_greedy_meshing(PyVoxelGridObject* self, PyObject* value, void* closure);
// Voxel access methods
static PyObject* get(PyVoxelGridObject* self, PyObject* args);
static PyObject* set(PyVoxelGridObject* self, PyObject* args);
// 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));
// Bulk operations - Milestone 11
static PyObject* fill_box_hollow(PyVoxelGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* fill_sphere(PyVoxelGridObject* self, PyObject* args);
static PyObject* fill_cylinder(PyVoxelGridObject* self, PyObject* args);
static PyObject* fill_noise(PyVoxelGridObject* self, PyObject* args, PyObject* kwds);
// Copy/paste operations - Milestone 11
static PyObject* copy_region(PyVoxelGridObject* self, PyObject* args);
static PyObject* paste_region(PyVoxelGridObject* self, PyObject* args, PyObject* kwds);
// Mesh caching (Milestone 10)
static PyObject* get_vertex_count(PyVoxelGridObject* self, void* closure);
static PyObject* rebuild_mesh(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
// Serialization (Milestone 14)
static PyObject* save(PyVoxelGridObject* self, PyObject* args);
static PyObject* load(PyVoxelGridObject* self, PyObject* args);
static PyObject* to_bytes(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
static PyObject* from_bytes(PyVoxelGridObject* self, PyObject* args);
// Statistics
static PyObject* count_non_air(PyVoxelGridObject* self, PyObject* Py_UNUSED(args));
static PyObject* count_material(PyVoxelGridObject* self, PyObject* args);
// Type registration
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
};
class PyVoxelRegion {
public:
static void dealloc(PyVoxelRegionObject* self);
static PyObject* repr(PyObject* obj);
static PyObject* get_size(PyVoxelRegionObject* self, void* closure);
static PyObject* get_width(PyVoxelRegionObject* self, void* closure);
static PyObject* get_height(PyVoxelRegionObject* self, void* closure);
static PyObject* get_depth(PyVoxelRegionObject* self, void* closure);
static PyGetSetDef getsetters[];
};
// =============================================================================
// Python type definitions (in mcrfpydef namespace)
// =============================================================================
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,
};
inline PyTypeObject PyVoxelRegionType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.VoxelRegion",
.tp_basicsize = sizeof(PyVoxelRegionObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyVoxelRegion::dealloc,
.tp_repr = PyVoxelRegion::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"VoxelRegion - Portable voxel data for copy/paste operations.\n\n"
"Created by VoxelGrid.copy_region(), used with paste_region().\n"
"Cannot be instantiated directly.\n\n"
"Properties:\n"
" size (tuple, read-only): Dimensions as (width, height, depth)\n"
" width, height, depth (int, read-only): Individual dimensions"
),
.tp_getset = nullptr, // Set before PyType_Ready
.tp_new = nullptr, // Cannot instantiate directly
};
} // namespace mcrfpydef

View file

@ -245,223 +245,6 @@ void main() {
}
)";
// =============================================================================
// Skinned Vertex Shaders (for skeletal animation)
// =============================================================================
const char* PS1_SKINNED_VERTEX_ES2 = R"(
// PS1-style skinned vertex shader for OpenGL ES 2.0 / WebGL 1.0
precision mediump float;
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_projection;
uniform mat4 u_bones[32];
uniform vec2 u_resolution;
uniform bool u_enable_snap;
uniform float u_fog_start;
uniform float u_fog_end;
uniform vec3 u_light_dir;
uniform vec3 u_ambient;
attribute vec3 a_position;
attribute vec2 a_texcoord;
attribute vec3 a_normal;
attribute vec4 a_color;
attribute vec4 a_bone_ids;
attribute vec4 a_bone_weights;
varying vec4 v_color;
varying vec2 v_texcoord;
varying float v_w;
varying float v_fog;
mat4 getBoneMatrix(int index) {
if (index < 8) {
if (index < 4) {
if (index < 2) {
if (index == 0) return u_bones[0];
else return u_bones[1];
} else {
if (index == 2) return u_bones[2];
else return u_bones[3];
}
} else {
if (index < 6) {
if (index == 4) return u_bones[4];
else return u_bones[5];
} else {
if (index == 6) return u_bones[6];
else return u_bones[7];
}
}
} else if (index < 16) {
if (index < 12) {
if (index < 10) {
if (index == 8) return u_bones[8];
else return u_bones[9];
} else {
if (index == 10) return u_bones[10];
else return u_bones[11];
}
} else {
if (index < 14) {
if (index == 12) return u_bones[12];
else return u_bones[13];
} else {
if (index == 14) return u_bones[14];
else return u_bones[15];
}
}
} else if (index < 24) {
if (index < 20) {
if (index < 18) {
if (index == 16) return u_bones[16];
else return u_bones[17];
} else {
if (index == 18) return u_bones[18];
else return u_bones[19];
}
} else {
if (index < 22) {
if (index == 20) return u_bones[20];
else return u_bones[21];
} else {
if (index == 22) return u_bones[22];
else return u_bones[23];
}
}
} else {
if (index < 28) {
if (index < 26) {
if (index == 24) return u_bones[24];
else return u_bones[25];
} else {
if (index == 26) return u_bones[26];
else return u_bones[27];
}
} else {
if (index < 30) {
if (index == 28) return u_bones[28];
else return u_bones[29];
} else {
if (index == 30) return u_bones[30];
else return u_bones[31];
}
}
}
return mat4(1.0);
}
void main() {
int b0 = int(a_bone_ids.x);
int b1 = int(a_bone_ids.y);
int b2 = int(a_bone_ids.z);
int b3 = int(a_bone_ids.w);
mat4 skin_matrix =
getBoneMatrix(b0) * a_bone_weights.x +
getBoneMatrix(b1) * a_bone_weights.y +
getBoneMatrix(b2) * a_bone_weights.z +
getBoneMatrix(b3) * a_bone_weights.w;
vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0);
vec3 skinned_normal = mat3(skin_matrix[0].xyz, skin_matrix[1].xyz, skin_matrix[2].xyz) * a_normal;
vec4 worldPos = u_model * skinned_pos;
vec4 viewPos = u_view * worldPos;
vec4 clipPos = u_projection * viewPos;
if (u_enable_snap) {
vec4 ndc = clipPos;
ndc.xyz /= ndc.w;
vec2 grid = u_resolution * 0.5;
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
ndc.xyz *= clipPos.w;
clipPos = ndc;
}
gl_Position = clipPos;
vec3 worldNormal = mat3(u_model[0].xyz, u_model[1].xyz, u_model[2].xyz) * skinned_normal;
worldNormal = normalize(worldNormal);
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
vec3 lighting = u_ambient + vec3(diffuse);
v_color = vec4(a_color.rgb * lighting, a_color.a);
v_texcoord = a_texcoord * clipPos.w;
v_w = clipPos.w;
float depth = -viewPos.z;
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
}
)";
const char* PS1_SKINNED_VERTEX = R"(
#version 150 core
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_projection;
uniform mat4 u_bones[64];
uniform vec2 u_resolution;
uniform bool u_enable_snap;
uniform float u_fog_start;
uniform float u_fog_end;
uniform vec3 u_light_dir;
uniform vec3 u_ambient;
in vec3 a_position;
in vec2 a_texcoord;
in vec3 a_normal;
in vec4 a_color;
in vec4 a_bone_ids;
in vec4 a_bone_weights;
out vec4 v_color;
noperspective out vec2 v_texcoord;
out float v_fog;
void main() {
ivec4 bone_ids = ivec4(a_bone_ids);
mat4 skin_matrix =
u_bones[bone_ids.x] * a_bone_weights.x +
u_bones[bone_ids.y] * a_bone_weights.y +
u_bones[bone_ids.z] * a_bone_weights.z +
u_bones[bone_ids.w] * a_bone_weights.w;
vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0);
vec3 skinned_normal = mat3(skin_matrix) * a_normal;
vec4 worldPos = u_model * skinned_pos;
vec4 viewPos = u_view * worldPos;
vec4 clipPos = u_projection * viewPos;
if (u_enable_snap) {
vec4 ndc = clipPos;
ndc.xyz /= ndc.w;
vec2 grid = u_resolution * 0.5;
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
ndc.xyz *= clipPos.w;
clipPos = ndc;
}
gl_Position = clipPos;
vec3 worldNormal = mat3(u_model) * skinned_normal;
worldNormal = normalize(worldNormal);
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
vec3 lighting = u_ambient + vec3(diffuse);
v_color = vec4(a_color.rgb * lighting, a_color.a);
v_texcoord = a_texcoord;
float depth = -viewPos.z;
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
}
)";
} // namespace shaders
// =============================================================================
@ -491,20 +274,6 @@ bool Shader3D::loadPS1Shaders() {
#endif
}
bool Shader3D::loadPS1SkinnedShaders() {
#ifdef MCRF_HAS_GL
#ifdef __EMSCRIPTEN__
// Use GLES2 skinned shaders for Emscripten/WebGL
return load(shaders::PS1_SKINNED_VERTEX_ES2, shaders::PS1_FRAGMENT_ES2);
#else
// Use desktop GL 3.2+ skinned shaders
return load(shaders::PS1_SKINNED_VERTEX, shaders::PS1_FRAGMENT);
#endif
#else
return false;
#endif
}
bool Shader3D::load(const char* vertexSource, const char* fragmentSource) {
if (!gl::isGLReady()) {
return false;

View file

@ -18,9 +18,6 @@ public:
// Automatically selects desktop vs ES2 shaders based on platform
bool loadPS1Shaders();
// Load skinned (skeletal animation) shaders
bool loadPS1SkinnedShaders();
// Load from custom source strings
bool load(const char* vertexSource, const char* fragmentSource);

File diff suppressed because it is too large Load diff

View file

@ -30,8 +30,6 @@ namespace mcrf {
class Viewport3D;
class Shader3D;
class MeshLayer;
class Billboard;
class VoxelGrid;
} // namespace mcrf
@ -86,19 +84,6 @@ public:
// Camera orbit helper for demos
void orbitCamera(float angle, float distance, float height);
/// Convert screen coordinates to world position via ray casting
/// @param screenX X position relative to viewport
/// @param screenY Y position relative to viewport
/// @return World position on Y=0 plane, or (-1,-1,-1) if no intersection
vec3 screenToWorld(float screenX, float screenY);
/// Position camera to follow an entity
/// @param entity Entity to follow
/// @param distance Distance behind entity
/// @param height Height above entity
/// @param smoothing Interpolation factor (0-1, where 1 = instant)
void followEntity(std::shared_ptr<Entity3D> entity, float distance, float height, float smoothing = 1.0f);
// =========================================================================
// Mesh Layer Management
// =========================================================================
@ -205,68 +190,6 @@ public:
/// Render all entities
void renderEntities(const mat4& view, const mat4& proj);
// =========================================================================
// Billboard Management
// =========================================================================
/// Get the billboard list
std::shared_ptr<std::vector<std::shared_ptr<Billboard>>> getBillboards() { return billboards_; }
/// Add a billboard
void addBillboard(std::shared_ptr<Billboard> bb);
/// Remove a billboard by pointer
void removeBillboard(Billboard* bb);
/// Clear all billboards
void clearBillboards();
/// Get billboard count
size_t getBillboardCount() const { return billboards_ ? billboards_->size() : 0; }
/// 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);
// =========================================================================
// Voxel-to-Nav Projection (Milestone 12)
// =========================================================================
/// Project a single voxel grid to the navigation grid
/// @param grid The voxel grid to project
/// @param headroom Required air voxels above floor for walkability
void projectVoxelToNav(std::shared_ptr<VoxelGrid> grid, int headroom = 2);
/// Project all voxel layers to the navigation grid
/// @param headroom Required air voxels above floor for walkability
void projectAllVoxelsToNav(int headroom = 2);
/// Clear nav cells in a voxel grid's footprint (before re-projection)
/// @param grid The voxel grid whose footprint to clear
void clearVoxelNavRegion(std::shared_ptr<VoxelGrid> grid);
// Background color
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
sf::Color getBackgroundColor() const { return bgColor_; }
@ -339,10 +262,6 @@ private:
float testRotation_ = 0.0f;
bool renderTestCube_ = true; // Set to false when layers are added
// Animation timing
float lastFrameTime_ = 0.0f;
bool firstFrame_ = true;
// Mesh layers for terrain, static geometry
std::vector<std::shared_ptr<MeshLayer>> meshLayers_;
@ -357,17 +276,8 @@ private:
// Entity3D storage
std::shared_ptr<std::list<std::shared_ptr<Entity3D>>> entities_;
// 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
// Test geometry VBO (cube)
unsigned int testVBO_ = 0;

View file

@ -1,795 +0,0 @@
// VoxelGrid.cpp - Dense 3D voxel array implementation
// Part of McRogueFace 3D Extension - Milestones 9-11
#include "VoxelGrid.h"
#include "VoxelMesher.h"
#include "MeshLayer.h" // For MeshVertex
#include <cmath>
#include <algorithm>
#include <cstring> // For memcpy, memcmp
#include <fstream> // For file I/O
namespace mcrf {
// 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;
}
// =============================================================================
// Bulk Operations - Milestone 11
// =============================================================================
void VoxelGrid::fillBoxHollow(int x0, int y0, int z0, int x1, int y1, int z1,
uint8_t material, int thickness) {
// Ensure proper ordering (min to max)
if (x0 > x1) std::swap(x0, x1);
if (y0 > y1) std::swap(y0, y1);
if (z0 > z1) std::swap(z0, z1);
// Fill entire box with material
fillBox(x0, y0, z0, x1, y1, z1, material);
// Carve out interior (inset by thickness on all sides)
int ix0 = x0 + thickness;
int iy0 = y0 + thickness;
int iz0 = z0 + thickness;
int ix1 = x1 - thickness;
int iy1 = y1 - thickness;
int iz1 = z1 - thickness;
// Only carve if there's interior space
if (ix0 <= ix1 && iy0 <= iy1 && iz0 <= iz1) {
fillBox(ix0, iy0, iz0, ix1, iy1, iz1, 0); // Air
}
// meshDirty_ already set by fillBox calls
}
void VoxelGrid::fillSphere(int cx, int cy, int cz, int radius, uint8_t material) {
int r2 = radius * radius;
for (int z = cz - radius; z <= cz + radius; z++) {
for (int y = cy - radius; y <= cy + radius; y++) {
for (int x = cx - radius; x <= cx + radius; x++) {
int dx = x - cx;
int dy = y - cy;
int dz = z - cz;
if (dx * dx + dy * dy + dz * dz <= r2) {
if (isValid(x, y, z)) {
data_[index(x, y, z)] = material;
}
}
}
}
}
meshDirty_ = true;
}
void VoxelGrid::fillCylinder(int cx, int cy, int cz, int radius, int height, uint8_t material) {
int r2 = radius * radius;
for (int y = cy; y < cy + height; y++) {
for (int z = cz - radius; z <= cz + radius; z++) {
for (int x = cx - radius; x <= cx + radius; x++) {
int dx = x - cx;
int dz = z - cz;
if (dx * dx + dz * dz <= r2) {
if (isValid(x, y, z)) {
data_[index(x, y, z)] = material;
}
}
}
}
}
meshDirty_ = true;
}
// Simple 3D noise implementation (hash-based, similar to value noise)
namespace {
// Simple hash function for noise
inline unsigned int hash3D(int x, int y, int z, unsigned int seed) {
unsigned int h = seed;
h ^= static_cast<unsigned int>(x) * 374761393u;
h ^= static_cast<unsigned int>(y) * 668265263u;
h ^= static_cast<unsigned int>(z) * 2147483647u;
h = (h ^ (h >> 13)) * 1274126177u;
return h;
}
// Convert hash to 0-1 float
inline float hashToFloat(unsigned int h) {
return static_cast<float>(h & 0xFFFFFF) / static_cast<float>(0xFFFFFF);
}
// Linear interpolation
inline float lerp(float a, float b, float t) {
return a + t * (b - a);
}
// Smoothstep for smoother interpolation
inline float smoothstep(float t) {
return t * t * (3.0f - 2.0f * t);
}
// 3D value noise
float noise3D(float x, float y, float z, unsigned int seed) {
int xi = static_cast<int>(std::floor(x));
int yi = static_cast<int>(std::floor(y));
int zi = static_cast<int>(std::floor(z));
float xf = x - xi;
float yf = y - yi;
float zf = z - zi;
// Smoothstep the fractions
float u = smoothstep(xf);
float v = smoothstep(yf);
float w = smoothstep(zf);
// Hash corners of the unit cube
float c000 = hashToFloat(hash3D(xi, yi, zi, seed));
float c100 = hashToFloat(hash3D(xi + 1, yi, zi, seed));
float c010 = hashToFloat(hash3D(xi, yi + 1, zi, seed));
float c110 = hashToFloat(hash3D(xi + 1, yi + 1, zi, seed));
float c001 = hashToFloat(hash3D(xi, yi, zi + 1, seed));
float c101 = hashToFloat(hash3D(xi + 1, yi, zi + 1, seed));
float c011 = hashToFloat(hash3D(xi, yi + 1, zi + 1, seed));
float c111 = hashToFloat(hash3D(xi + 1, yi + 1, zi + 1, seed));
// Trilinear interpolation
float x00 = lerp(c000, c100, u);
float x10 = lerp(c010, c110, u);
float x01 = lerp(c001, c101, u);
float x11 = lerp(c011, c111, u);
float y0 = lerp(x00, x10, v);
float y1 = lerp(x01, x11, v);
return lerp(y0, y1, w);
}
}
void VoxelGrid::fillNoise(int x0, int y0, int z0, int x1, int y1, int z1,
uint8_t material, float threshold, float scale, unsigned int seed) {
// Ensure proper ordering
if (x0 > x1) std::swap(x0, x1);
if (y0 > y1) std::swap(y0, y1);
if (z0 > z1) std::swap(z0, z1);
// Clamp to valid range
x0 = std::max(0, std::min(x0, width_ - 1));
x1 = std::max(0, std::min(x1, width_ - 1));
y0 = std::max(0, std::min(y0, height_ - 1));
y1 = std::max(0, std::min(y1, height_ - 1));
z0 = std::max(0, std::min(z0, depth_ - 1));
z1 = std::max(0, std::min(z1, depth_ - 1));
for (int z = z0; z <= z1; z++) {
for (int y = y0; y <= y1; y++) {
for (int x = x0; x <= x1; x++) {
float n = noise3D(x * scale, y * scale, z * scale, seed);
if (n > threshold) {
data_[index(x, y, z)] = material;
}
}
}
}
meshDirty_ = true;
}
// =============================================================================
// Copy/Paste Operations - Milestone 11
// =============================================================================
VoxelRegion VoxelGrid::copyRegion(int x0, int y0, int z0, int x1, int y1, int z1) const {
// Ensure proper ordering
if (x0 > x1) std::swap(x0, x1);
if (y0 > y1) std::swap(y0, y1);
if (z0 > z1) std::swap(z0, z1);
// Clamp to valid range
x0 = std::max(0, std::min(x0, width_ - 1));
x1 = std::max(0, std::min(x1, width_ - 1));
y0 = std::max(0, std::min(y0, height_ - 1));
y1 = std::max(0, std::min(y1, height_ - 1));
z0 = std::max(0, std::min(z0, depth_ - 1));
z1 = std::max(0, std::min(z1, depth_ - 1));
int rw = x1 - x0 + 1;
int rh = y1 - y0 + 1;
int rd = z1 - z0 + 1;
VoxelRegion region(rw, rh, rd);
for (int rz = 0; rz < rd; rz++) {
for (int ry = 0; ry < rh; ry++) {
for (int rx = 0; rx < rw; rx++) {
int sx = x0 + rx;
int sy = y0 + ry;
int sz = z0 + rz;
size_t ri = static_cast<size_t>(rz) * (rw * rh) +
static_cast<size_t>(ry) * rw + rx;
region.data[ri] = get(sx, sy, sz);
}
}
}
return region;
}
void VoxelGrid::pasteRegion(const VoxelRegion& region, int x, int y, int z, bool skipAir) {
if (!region.isValid()) return;
for (int rz = 0; rz < region.depth; rz++) {
for (int ry = 0; ry < region.height; ry++) {
for (int rx = 0; rx < region.width; rx++) {
size_t ri = static_cast<size_t>(rz) * (region.width * region.height) +
static_cast<size_t>(ry) * region.width + rx;
uint8_t mat = region.data[ri];
if (skipAir && mat == 0) continue;
int dx = x + rx;
int dy = y + ry;
int dz = z + rz;
if (isValid(dx, dy, dz)) {
data_[index(dx, dy, dz)] = mat;
}
}
}
}
meshDirty_ = true;
}
// =============================================================================
// Navigation Projection - Milestone 12
// =============================================================================
VoxelGrid::NavInfo VoxelGrid::projectColumn(int x, int z, int headroom) const {
NavInfo info;
info.height = 0.0f;
info.walkable = false;
info.transparent = true;
info.pathCost = 1.0f;
// Out of bounds check
if (x < 0 || x >= width_ || z < 0 || z >= depth_) {
return info;
}
// Scan from top to bottom, find first solid with air above (floor)
int floorY = -1;
for (int y = height_ - 1; y >= 0; y--) {
uint8_t mat = get(x, y, z);
if (mat != 0) {
// Found solid - check if it's a floor (air above) or ceiling
bool hasAirAbove = (y == height_ - 1) || (get(x, y + 1, z) == 0);
if (hasAirAbove) {
floorY = y;
break;
}
}
}
if (floorY >= 0) {
// Found a floor
info.height = (floorY + 1) * cellSize_; // Top of floor voxel
info.walkable = true;
// Check headroom (need enough air voxels above floor)
int airCount = 0;
for (int y = floorY + 1; y < height_; y++) {
if (get(x, y, z) == 0) {
airCount++;
} else {
break;
}
}
if (airCount < headroom) {
info.walkable = false; // Can't fit entity
}
// Get path cost from floor material
uint8_t floorMat = get(x, floorY, z);
info.pathCost = getMaterial(floorMat).pathCost;
}
// Check transparency: any non-transparent solid in column blocks FOV
for (int y = 0; y < height_; y++) {
uint8_t mat = get(x, y, z);
if (mat != 0 && !getMaterial(mat).transparent) {
info.transparent = false;
break;
}
}
return info;
}
// =============================================================================
// Mesh Caching (Milestone 10)
// =============================================================================
const std::vector<MeshVertex>& VoxelGrid::getVertices() const {
if (meshDirty_) {
rebuildMesh();
}
return cachedVertices_;
}
void VoxelGrid::rebuildMesh() const {
cachedVertices_.clear();
if (greedyMeshing_) {
VoxelMesher::generateGreedyMesh(*this, cachedVertices_);
} else {
VoxelMesher::generateMesh(*this, cachedVertices_);
}
meshDirty_ = false;
}
// =============================================================================
// Serialization - Milestone 14
// =============================================================================
// File format:
// Magic "MCVG" (4 bytes)
// Version (1 byte) - currently 1
// Width, Height, Depth (3 x int32 = 12 bytes)
// Cell Size (float32 = 4 bytes)
// Material count (uint8 = 1 byte)
// For each material:
// Name length (uint16) + name bytes
// Color RGBA (4 bytes)
// Sprite index (int32)
// Transparent (uint8)
// Path cost (float32)
// Voxel data length (uint32)
// Voxel data: RLE encoded (run_length: uint8, material: uint8) pairs
// If run_length == 255, read extended_length: uint16 for longer runs
namespace {
const char MAGIC[4] = {'M', 'C', 'V', 'G'};
const uint8_t FORMAT_VERSION = 1;
// Write helpers
void writeU8(std::vector<uint8_t>& buf, uint8_t v) {
buf.push_back(v);
}
void writeU16(std::vector<uint8_t>& buf, uint16_t v) {
buf.push_back(static_cast<uint8_t>(v & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
}
void writeI32(std::vector<uint8_t>& buf, int32_t v) {
buf.push_back(static_cast<uint8_t>(v & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 16) & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 24) & 0xFF));
}
void writeU32(std::vector<uint8_t>& buf, uint32_t v) {
buf.push_back(static_cast<uint8_t>(v & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 16) & 0xFF));
buf.push_back(static_cast<uint8_t>((v >> 24) & 0xFF));
}
void writeF32(std::vector<uint8_t>& buf, float v) {
static_assert(sizeof(float) == 4, "Expected 4-byte float");
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&v);
buf.insert(buf.end(), bytes, bytes + 4);
}
void writeString(std::vector<uint8_t>& buf, const std::string& s) {
uint16_t len = static_cast<uint16_t>(std::min(s.size(), size_t(65535)));
writeU16(buf, len);
buf.insert(buf.end(), s.begin(), s.begin() + len);
}
// Read helpers
class Reader {
const uint8_t* data_;
size_t size_;
size_t pos_;
public:
Reader(const uint8_t* data, size_t size) : data_(data), size_(size), pos_(0) {}
bool hasBytes(size_t n) const { return pos_ + n <= size_; }
size_t position() const { return pos_; }
bool readU8(uint8_t& v) {
if (!hasBytes(1)) return false;
v = data_[pos_++];
return true;
}
bool readU16(uint16_t& v) {
if (!hasBytes(2)) return false;
v = static_cast<uint16_t>(data_[pos_]) |
(static_cast<uint16_t>(data_[pos_ + 1]) << 8);
pos_ += 2;
return true;
}
bool readI32(int32_t& v) {
if (!hasBytes(4)) return false;
v = static_cast<int32_t>(data_[pos_]) |
(static_cast<int32_t>(data_[pos_ + 1]) << 8) |
(static_cast<int32_t>(data_[pos_ + 2]) << 16) |
(static_cast<int32_t>(data_[pos_ + 3]) << 24);
pos_ += 4;
return true;
}
bool readU32(uint32_t& v) {
if (!hasBytes(4)) return false;
v = static_cast<uint32_t>(data_[pos_]) |
(static_cast<uint32_t>(data_[pos_ + 1]) << 8) |
(static_cast<uint32_t>(data_[pos_ + 2]) << 16) |
(static_cast<uint32_t>(data_[pos_ + 3]) << 24);
pos_ += 4;
return true;
}
bool readF32(float& v) {
if (!hasBytes(4)) return false;
static_assert(sizeof(float) == 4, "Expected 4-byte float");
std::memcpy(&v, data_ + pos_, 4);
pos_ += 4;
return true;
}
bool readString(std::string& s) {
uint16_t len;
if (!readU16(len)) return false;
if (!hasBytes(len)) return false;
s.assign(reinterpret_cast<const char*>(data_ + pos_), len);
pos_ += len;
return true;
}
bool readBytes(uint8_t* out, size_t n) {
if (!hasBytes(n)) return false;
std::memcpy(out, data_ + pos_, n);
pos_ += n;
return true;
}
};
// RLE encode voxel data
void rleEncode(const std::vector<uint8_t>& data, std::vector<uint8_t>& out) {
if (data.empty()) return;
size_t i = 0;
while (i < data.size()) {
uint8_t mat = data[i];
size_t runStart = i;
// Count consecutive same materials
while (i < data.size() && data[i] == mat && (i - runStart) < 65535 + 255) {
i++;
}
size_t runLen = i - runStart;
if (runLen < 255) {
writeU8(out, static_cast<uint8_t>(runLen));
} else {
// Extended run: 255 marker + uint16 length
writeU8(out, 255);
writeU16(out, static_cast<uint16_t>(runLen - 255));
}
writeU8(out, mat);
}
}
// RLE decode voxel data
bool rleDecode(Reader& reader, std::vector<uint8_t>& data, size_t expectedSize) {
data.clear();
data.reserve(expectedSize);
while (data.size() < expectedSize) {
uint8_t runLen8;
if (!reader.readU8(runLen8)) return false;
size_t runLen = runLen8;
if (runLen8 == 255) {
uint16_t extLen;
if (!reader.readU16(extLen)) return false;
runLen = 255 + extLen;
}
uint8_t mat;
if (!reader.readU8(mat)) return false;
for (size_t j = 0; j < runLen && data.size() < expectedSize; j++) {
data.push_back(mat);
}
}
return data.size() == expectedSize;
}
}
bool VoxelGrid::saveToBuffer(std::vector<uint8_t>& buffer) const {
buffer.clear();
buffer.reserve(1024 + data_.size()); // Rough estimate
// Magic
buffer.insert(buffer.end(), MAGIC, MAGIC + 4);
// Version
writeU8(buffer, FORMAT_VERSION);
// Dimensions
writeI32(buffer, width_);
writeI32(buffer, height_);
writeI32(buffer, depth_);
// Cell size
writeF32(buffer, cellSize_);
// Materials
writeU8(buffer, static_cast<uint8_t>(materials_.size()));
for (const auto& mat : materials_) {
writeString(buffer, mat.name);
writeU8(buffer, mat.color.r);
writeU8(buffer, mat.color.g);
writeU8(buffer, mat.color.b);
writeU8(buffer, mat.color.a);
writeI32(buffer, mat.spriteIndex);
writeU8(buffer, mat.transparent ? 1 : 0);
writeF32(buffer, mat.pathCost);
}
// RLE encode voxel data
std::vector<uint8_t> rleData;
rleEncode(data_, rleData);
// Write RLE data length and data
writeU32(buffer, static_cast<uint32_t>(rleData.size()));
buffer.insert(buffer.end(), rleData.begin(), rleData.end());
return true;
}
bool VoxelGrid::loadFromBuffer(const uint8_t* data, size_t size) {
Reader reader(data, size);
// Check magic
uint8_t magic[4];
if (!reader.readBytes(magic, 4)) return false;
if (std::memcmp(magic, MAGIC, 4) != 0) return false;
// Check version
uint8_t version;
if (!reader.readU8(version)) return false;
if (version != FORMAT_VERSION) return false;
// Read dimensions
int32_t w, h, d;
if (!reader.readI32(w) || !reader.readI32(h) || !reader.readI32(d)) return false;
if (w <= 0 || h <= 0 || d <= 0) return false;
// Read cell size
float cs;
if (!reader.readF32(cs)) return false;
if (cs <= 0.0f) return false;
// Read materials
uint8_t matCount;
if (!reader.readU8(matCount)) return false;
std::vector<VoxelMaterial> newMaterials;
newMaterials.reserve(matCount);
for (uint8_t i = 0; i < matCount; i++) {
VoxelMaterial mat;
if (!reader.readString(mat.name)) return false;
uint8_t r, g, b, a;
if (!reader.readU8(r) || !reader.readU8(g) || !reader.readU8(b) || !reader.readU8(a))
return false;
mat.color = sf::Color(r, g, b, a);
int32_t sprite;
if (!reader.readI32(sprite)) return false;
mat.spriteIndex = sprite;
uint8_t transp;
if (!reader.readU8(transp)) return false;
mat.transparent = (transp != 0);
if (!reader.readF32(mat.pathCost)) return false;
newMaterials.push_back(mat);
}
// Read RLE data length
uint32_t rleLen;
if (!reader.readU32(rleLen)) return false;
// Decode voxel data
size_t expectedVoxels = static_cast<size_t>(w) * h * d;
std::vector<uint8_t> newData;
if (!rleDecode(reader, newData, expectedVoxels)) return false;
// Success - update the grid
width_ = w;
height_ = h;
depth_ = d;
cellSize_ = cs;
materials_ = std::move(newMaterials);
data_ = std::move(newData);
meshDirty_ = true;
return true;
}
bool VoxelGrid::save(const std::string& path) const {
std::vector<uint8_t> buffer;
if (!saveToBuffer(buffer)) return false;
std::ofstream file(path, std::ios::binary);
if (!file) return false;
file.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
return file.good();
}
bool VoxelGrid::load(const std::string& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) return false;
std::streamsize size = file.tellg();
if (size <= 0) return false;
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(static_cast<size_t>(size));
if (!file.read(reinterpret_cast<char*>(buffer.data()), size)) return false;
return loadFromBuffer(buffer.data(), buffer.size());
}
} // namespace mcrf

View file

@ -1,194 +0,0 @@
// VoxelGrid.h - Dense 3D voxel array with material palette
// Part of McRogueFace 3D Extension - Milestones 9-11
#pragma once
#include "../Common.h"
#include "Math3D.h"
#include "MeshLayer.h" // For MeshVertex (needed for std::vector<MeshVertex>)
#include <vector>
#include <string>
#include <stdexcept>
#include <cstdint>
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) {}
};
// =============================================================================
// VoxelRegion - Portable voxel data for copy/paste operations (Milestone 11)
// =============================================================================
struct VoxelRegion {
int width, height, depth;
std::vector<uint8_t> data;
VoxelRegion() : width(0), height(0), depth(0) {}
VoxelRegion(int w, int h, int d) : width(w), height(h), depth(d),
data(static_cast<size_t>(w) * h * d, 0) {}
bool isValid() const { return width > 0 && height > 0 && depth > 0; }
size_t totalVoxels() const { return static_cast<size_t>(width) * height * depth; }
};
// =============================================================================
// VoxelGrid - Dense 3D array of material IDs
// =============================================================================
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 (Milestones 10, 13)
mutable bool meshDirty_ = true;
mutable std::vector<MeshVertex> cachedVertices_;
bool greedyMeshing_ = false; // Use greedy meshing algorithm
// Index calculation (row-major: X varies fastest, then Y, then Z)
inline size_t index(int x, int y, int z) const {
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 - Basic
void fill(uint8_t material);
void clear() { fill(0); }
void fillBox(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t material);
// Bulk operations - Milestone 11
void fillBoxHollow(int x0, int y0, int z0, int x1, int y1, int z1,
uint8_t material, int thickness = 1);
void fillSphere(int cx, int cy, int cz, int radius, uint8_t material);
void fillCylinder(int cx, int cy, int cz, int radius, int height, uint8_t material);
void fillNoise(int x0, int y0, int z0, int x1, int y1, int z1,
uint8_t material, float threshold = 0.5f,
float scale = 0.1f, unsigned int seed = 0);
// Copy/paste operations - Milestone 11
VoxelRegion copyRegion(int x0, int y0, int z0, int x1, int y1, int z1) const;
void pasteRegion(const VoxelRegion& region, int x, int y, int z, bool skipAir = true);
// Navigation projection - Milestone 12
struct NavInfo {
float height = 0.0f;
bool walkable = false;
bool transparent = true;
float pathCost = 1.0f;
};
/// Project a single column to get navigation info
/// @param x X coordinate in voxel grid
/// @param z Z coordinate in voxel grid
/// @param headroom Required air voxels above floor (default 2)
/// @return Navigation info for this column
NavInfo projectColumn(int x, int z, int headroom = 2) const;
// Transform
void setOffset(const vec3& offset) { offset_ = offset; }
void setOffset(float x, float y, float z) { offset_ = vec3(x, y, z); }
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 (Milestones 10, 13)
/// 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(); }
/// Enable/disable greedy meshing (Milestone 13)
/// Greedy meshing merges coplanar faces to reduce vertex count
void setGreedyMeshing(bool enabled) { greedyMeshing_ = enabled; markDirty(); }
bool isGreedyMeshingEnabled() const { return greedyMeshing_; }
// Memory info (for debugging)
size_t memoryUsageBytes() const {
return data_.size() + materials_.size() * sizeof(VoxelMaterial);
}
// Serialization (Milestone 14)
/// Save voxel grid to binary file
/// @param path File path to save to
/// @return true on success
bool save(const std::string& path) const;
/// Load voxel grid from binary file
/// @param path File path to load from
/// @return true on success
bool load(const std::string& path);
/// Save to memory buffer
/// @param buffer Output buffer (resized as needed)
/// @return true on success
bool saveToBuffer(std::vector<uint8_t>& buffer) const;
/// Load from memory buffer
/// @param data Buffer to load from
/// @param size Buffer size
/// @return true on success
bool loadFromBuffer(const uint8_t* data, size_t size);
};
} // namespace mcrf

View file

@ -1,317 +0,0 @@
// 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::emitQuad(std::vector<MeshVertex>& vertices,
const vec3& corner,
const vec3& uAxis,
const vec3& vAxis,
const vec3& normal,
const VoxelMaterial& material) {
// 4 corners of the quad
vec3 corners[4] = {
corner, // 0: origin
corner + uAxis, // 1: +U
corner + uAxis + vAxis, // 2: +U+V
corner + vAxis // 3: +V
};
// Calculate UV based on quad size (for potential texture tiling)
float uLen = uAxis.length();
float vLen = vAxis.length();
vec2 uvs[4] = {
vec2(0, 0), // 0
vec2(uLen, 0), // 1
vec2(uLen, vLen), // 2
vec2(0, vLen) // 3
};
// Color from material
vec4 color(
material.color.r / 255.0f,
material.color.g / 255.0f,
material.color.b / 255.0f,
material.color.a / 255.0f
);
// Emit 2 triangles (6 vertices) - CCW winding
// Triangle 1: 0-2-1
vertices.push_back(MeshVertex(corners[0], uvs[0], normal, color));
vertices.push_back(MeshVertex(corners[2], uvs[2], normal, color));
vertices.push_back(MeshVertex(corners[1], uvs[1], normal, color));
// Triangle 2: 0-3-2
vertices.push_back(MeshVertex(corners[0], uvs[0], normal, color));
vertices.push_back(MeshVertex(corners[3], uvs[3], normal, color));
vertices.push_back(MeshVertex(corners[2], uvs[2], normal, color));
}
void VoxelMesher::generateGreedyMesh(const VoxelGrid& grid, std::vector<MeshVertex>& outVertices) {
const float cs = grid.cellSize();
const int width = grid.width();
const int height = grid.height();
const int depth = grid.depth();
// Process each face direction
// Axis 0 = X, 1 = Y, 2 = Z
// Direction: +1 = positive, -1 = negative
for (int axis = 0; axis < 3; axis++) {
for (int dir = -1; dir <= 1; dir += 2) {
// Determine slice dimensions based on axis
int sliceW, sliceH, sliceCount;
if (axis == 0) { // X-axis: slices in YZ plane
sliceW = depth;
sliceH = height;
sliceCount = width;
} else if (axis == 1) { // Y-axis: slices in XZ plane
sliceW = width;
sliceH = depth;
sliceCount = height;
} else { // Z-axis: slices in XY plane
sliceW = width;
sliceH = height;
sliceCount = depth;
}
// Create mask for this slice
std::vector<uint8_t> mask(sliceW * sliceH);
// Process each slice
for (int sliceIdx = 0; sliceIdx < sliceCount; sliceIdx++) {
// Fill mask with material IDs where faces should be generated
std::fill(mask.begin(), mask.end(), 0);
for (int v = 0; v < sliceH; v++) {
for (int u = 0; u < sliceW; u++) {
// Map (u, v, sliceIdx) to (x, y, z) based on axis
int x, y, z, nx, ny, nz;
if (axis == 0) {
x = sliceIdx; y = v; z = u;
nx = x + dir; ny = y; nz = z;
} else if (axis == 1) {
x = u; y = sliceIdx; z = v;
nx = x; ny = y + dir; nz = z;
} else {
x = u; y = v; z = sliceIdx;
nx = x; ny = y; nz = z + dir;
}
uint8_t mat = grid.get(x, y, z);
if (mat == 0) continue;
// Check if face should be generated
if (shouldGenerateFace(grid, x, y, z, nx, ny, nz)) {
mask[v * sliceW + u] = mat;
}
}
}
// Greedy rectangle merging
for (int v = 0; v < sliceH; v++) {
for (int u = 0; u < sliceW; ) {
uint8_t mat = mask[v * sliceW + u];
if (mat == 0) {
u++;
continue;
}
// Find width of rectangle (extend along U)
int rectW = 1;
while (u + rectW < sliceW && mask[v * sliceW + u + rectW] == mat) {
rectW++;
}
// Find height of rectangle (extend along V)
int rectH = 1;
bool canExtend = true;
while (canExtend && v + rectH < sliceH) {
// Check if entire row matches
for (int i = 0; i < rectW; i++) {
if (mask[(v + rectH) * sliceW + u + i] != mat) {
canExtend = false;
break;
}
}
if (canExtend) rectH++;
}
// Clear mask for merged area
for (int dv = 0; dv < rectH; dv++) {
for (int du = 0; du < rectW; du++) {
mask[(v + dv) * sliceW + u + du] = 0;
}
}
// Emit quad for this merged rectangle
const VoxelMaterial& material = grid.getMaterial(mat);
// Calculate corner and axes based on face direction
vec3 corner, uAxis, vAxis, normal;
if (axis == 0) { // X-facing
float faceX = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs;
corner = vec3(faceX, v * cs, u * cs);
uAxis = vec3(0, 0, rectW * cs);
vAxis = vec3(0, rectH * cs, 0);
normal = vec3(static_cast<float>(dir), 0, 0);
// Flip winding for back faces
if (dir < 0) std::swap(uAxis, vAxis);
} else if (axis == 1) { // Y-facing
float faceY = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs;
corner = vec3(u * cs, faceY, v * cs);
uAxis = vec3(rectW * cs, 0, 0);
vAxis = vec3(0, 0, rectH * cs);
normal = vec3(0, static_cast<float>(dir), 0);
if (dir < 0) std::swap(uAxis, vAxis);
} else { // Z-facing
float faceZ = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs;
corner = vec3(u * cs, v * cs, faceZ);
uAxis = vec3(rectW * cs, 0, 0);
vAxis = vec3(0, rectH * cs, 0);
normal = vec3(0, 0, static_cast<float>(dir));
if (dir < 0) std::swap(uAxis, vAxis);
}
emitQuad(outVertices, corner, uAxis, vAxis, normal, material);
u += rectW;
}
}
}
}
}
}
void VoxelMesher::emitFace(std::vector<MeshVertex>& vertices,
const vec3& center,
const vec3& normal,
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

View file

@ -1,80 +0,0 @@
// VoxelMesher.h - Face-culled mesh generation for VoxelGrid
// Part of McRogueFace 3D Extension - Milestones 10, 13
#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 (simple per-voxel faces)
/// Output vertices in local space (model matrix applies world transform)
/// @param grid The VoxelGrid to generate mesh from
/// @param outVertices Output vector of vertices (appended to, not cleared)
static void generateMesh(
const VoxelGrid& grid,
std::vector<MeshVertex>& outVertices
);
/// Generate mesh using greedy meshing algorithm (Milestone 13)
/// Merges coplanar faces of the same material into larger rectangles,
/// significantly reducing vertex count for uniform regions.
/// @param grid The VoxelGrid to generate mesh from
/// @param outVertices Output vector of vertices (appended to, not cleared)
static void generateGreedyMesh(
const VoxelGrid& grid,
std::vector<MeshVertex>& outVertices
);
private:
/// Check if face should be generated (neighbor is air or transparent)
/// @param grid The VoxelGrid
/// @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
);
/// Generate a rectangular face (2 triangles = 6 vertices)
/// Used by greedy meshing to emit merged quads
/// @param vertices Output vector to append vertices to
/// @param corner Base corner of the rectangle
/// @param uAxis Direction and length along U axis
/// @param vAxis Direction and length along V axis
/// @param normal Face normal direction
/// @param material Material for coloring
static void emitQuad(
std::vector<MeshVertex>& vertices,
const vec3& corner,
const vec3& uAxis,
const vec3& vAxis,
const vec3& normal,
const VoxelMaterial& material
);
};
} // namespace mcrf

File diff suppressed because it is too large Load diff

View file

@ -1,6 +0,0 @@
// cgltf_impl.cpp - Implementation file for cgltf glTF loader
// This file defines CGLTF_IMPLEMENTATION to include the cgltf implementation
// exactly once in the project.
#define CGLTF_IMPLEMENTATION
#include "cgltf.h"

View file

@ -1,108 +0,0 @@
// PS1-style skinned vertex shader for OpenGL 3.2+
// Implements skeletal animation, vertex snapping, Gouraud shading, and fog
#version 150 core
// Uniforms - transform matrices
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_projection;
// Uniforms - skeletal animation (max 64 bones)
uniform mat4 u_bones[64];
// Uniforms - PS1 effects
uniform vec2 u_resolution; // Internal render resolution for vertex snapping
uniform bool u_enable_snap; // Enable vertex snapping to pixel grid
uniform float u_fog_start; // Fog start distance
uniform float u_fog_end; // Fog end distance
// Uniforms - lighting
uniform vec3 u_light_dir; // Directional light direction (normalized)
uniform vec3 u_ambient; // Ambient light color
// Attributes
in vec3 a_position;
in vec2 a_texcoord;
in vec3 a_normal;
in vec4 a_color;
in vec4 a_bone_ids; // Up to 4 bone indices (as float for compatibility)
in vec4 a_bone_weights; // Corresponding weights
// Varyings - passed to fragment shader
out vec4 v_color; // Gouraud-shaded vertex color
noperspective out vec2 v_texcoord; // Texture coordinates (affine interpolation!)
out float v_fog; // Fog factor (0 = no fog, 1 = full fog)
void main() {
// =========================================================================
// Skeletal Animation: Vertex Skinning
// Transform vertex and normal by weighted bone matrices
// =========================================================================
ivec4 bone_ids = ivec4(a_bone_ids); // Convert to integer indices
// Compute skinned position and normal
mat4 skin_matrix =
u_bones[bone_ids.x] * a_bone_weights.x +
u_bones[bone_ids.y] * a_bone_weights.y +
u_bones[bone_ids.z] * a_bone_weights.z +
u_bones[bone_ids.w] * a_bone_weights.w;
vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0);
vec3 skinned_normal = mat3(skin_matrix) * a_normal;
// Transform vertex to clip space
vec4 worldPos = u_model * skinned_pos;
vec4 viewPos = u_view * worldPos;
vec4 clipPos = u_projection * viewPos;
// =========================================================================
// PS1 Effect: Vertex Snapping
// The PS1 had limited precision for vertex positions, causing vertices
// to "snap" to a grid, creating the characteristic jittery look.
// =========================================================================
if (u_enable_snap) {
// Convert to NDC
vec4 ndc = clipPos;
ndc.xyz /= ndc.w;
// Snap to pixel grid based on render resolution
vec2 grid = u_resolution * 0.5;
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
// Convert back to clip space
ndc.xyz *= clipPos.w;
clipPos = ndc;
}
gl_Position = clipPos;
// =========================================================================
// PS1 Effect: Gouraud Shading
// Per-vertex lighting was used on PS1 due to hardware limitations.
// This creates characteristic flat-shaded polygons.
// =========================================================================
vec3 worldNormal = mat3(u_model) * skinned_normal;
worldNormal = normalize(worldNormal);
// Simple directional light + ambient
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
vec3 lighting = u_ambient + vec3(diffuse);
// Apply lighting to vertex color
v_color = vec4(a_color.rgb * lighting, a_color.a);
// =========================================================================
// PS1 Effect: Affine Texture Mapping
// Using 'noperspective' qualifier disables perspective-correct interpolation
// This creates the characteristic texture warping on large polygons
// =========================================================================
v_texcoord = a_texcoord;
// =========================================================================
// Fog Distance Calculation
// Calculate linear fog factor based on view-space depth
// =========================================================================
float depth = -viewPos.z; // View space depth (positive)
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
}

View file

@ -1,195 +0,0 @@
// PS1-style skinned vertex shader for OpenGL ES 2.0 / WebGL 1.0
// Implements skeletal animation, vertex snapping, Gouraud shading, and fog
precision mediump float;
// Uniforms - transform matrices
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_projection;
// Uniforms - skeletal animation (max 64 bones)
// GLES2 doesn't guarantee support for arrays > 128 vec4s in vertex shaders
// 64 bones * 4 vec4s = 256 vec4s, so we use 32 bones for safety
uniform mat4 u_bones[32];
// Uniforms - PS1 effects
uniform vec2 u_resolution; // Internal render resolution for vertex snapping
uniform bool u_enable_snap; // Enable vertex snapping to pixel grid
uniform float u_fog_start; // Fog start distance
uniform float u_fog_end; // Fog end distance
// Uniforms - lighting
uniform vec3 u_light_dir; // Directional light direction (normalized)
uniform vec3 u_ambient; // Ambient light color
// Attributes
attribute vec3 a_position;
attribute vec2 a_texcoord;
attribute vec3 a_normal;
attribute vec4 a_color;
attribute vec4 a_bone_ids; // Up to 4 bone indices (as floats)
attribute vec4 a_bone_weights; // Corresponding weights
// Varyings - passed to fragment shader
varying vec4 v_color; // Gouraud-shaded vertex color
varying vec2 v_texcoord; // Texture coordinates (multiplied by w for affine trick)
varying float v_w; // Clip space w for affine mapping restoration
varying float v_fog; // Fog factor (0 = no fog, 1 = full fog)
// Helper to get bone matrix by index (GLES2 doesn't support dynamic array indexing well)
mat4 getBoneMatrix(int index) {
// GLES2 workaround: use if-chain for dynamic indexing
if (index < 8) {
if (index < 4) {
if (index < 2) {
if (index == 0) return u_bones[0];
else return u_bones[1];
} else {
if (index == 2) return u_bones[2];
else return u_bones[3];
}
} else {
if (index < 6) {
if (index == 4) return u_bones[4];
else return u_bones[5];
} else {
if (index == 6) return u_bones[6];
else return u_bones[7];
}
}
} else if (index < 16) {
if (index < 12) {
if (index < 10) {
if (index == 8) return u_bones[8];
else return u_bones[9];
} else {
if (index == 10) return u_bones[10];
else return u_bones[11];
}
} else {
if (index < 14) {
if (index == 12) return u_bones[12];
else return u_bones[13];
} else {
if (index == 14) return u_bones[14];
else return u_bones[15];
}
}
} else if (index < 24) {
if (index < 20) {
if (index < 18) {
if (index == 16) return u_bones[16];
else return u_bones[17];
} else {
if (index == 18) return u_bones[18];
else return u_bones[19];
}
} else {
if (index < 22) {
if (index == 20) return u_bones[20];
else return u_bones[21];
} else {
if (index == 22) return u_bones[22];
else return u_bones[23];
}
}
} else {
if (index < 28) {
if (index < 26) {
if (index == 24) return u_bones[24];
else return u_bones[25];
} else {
if (index == 26) return u_bones[26];
else return u_bones[27];
}
} else {
if (index < 30) {
if (index == 28) return u_bones[28];
else return u_bones[29];
} else {
if (index == 30) return u_bones[30];
else return u_bones[31];
}
}
}
return mat4(1.0); // Identity fallback
}
void main() {
// =========================================================================
// Skeletal Animation: Vertex Skinning
// Transform vertex and normal by weighted bone matrices
// =========================================================================
int b0 = int(a_bone_ids.x);
int b1 = int(a_bone_ids.y);
int b2 = int(a_bone_ids.z);
int b3 = int(a_bone_ids.w);
// Compute skinned position and normal
mat4 skin_matrix =
getBoneMatrix(b0) * a_bone_weights.x +
getBoneMatrix(b1) * a_bone_weights.y +
getBoneMatrix(b2) * a_bone_weights.z +
getBoneMatrix(b3) * a_bone_weights.w;
vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0);
vec3 skinned_normal = mat3(skin_matrix[0].xyz, skin_matrix[1].xyz, skin_matrix[2].xyz) * a_normal;
// Transform vertex to clip space
vec4 worldPos = u_model * skinned_pos;
vec4 viewPos = u_view * worldPos;
vec4 clipPos = u_projection * viewPos;
// =========================================================================
// PS1 Effect: Vertex Snapping
// The PS1 had limited precision for vertex positions, causing vertices
// to "snap" to a grid, creating the characteristic jittery look.
// =========================================================================
if (u_enable_snap) {
// Convert to NDC
vec4 ndc = clipPos;
ndc.xyz /= ndc.w;
// Snap to pixel grid based on render resolution
vec2 grid = u_resolution * 0.5;
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
// Convert back to clip space
ndc.xyz *= clipPos.w;
clipPos = ndc;
}
gl_Position = clipPos;
// =========================================================================
// PS1 Effect: Gouraud Shading
// Per-vertex lighting was used on PS1 due to hardware limitations.
// This creates characteristic flat-shaded polygons.
// =========================================================================
vec3 worldNormal = mat3(u_model[0].xyz, u_model[1].xyz, u_model[2].xyz) * skinned_normal;
worldNormal = normalize(worldNormal);
// Simple directional light + ambient
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
vec3 lighting = u_ambient + vec3(diffuse);
// Apply lighting to vertex color
v_color = vec4(a_color.rgb * lighting, a_color.a);
// =========================================================================
// PS1 Effect: Affine Texture Mapping Trick
// GLES2 doesn't have 'noperspective' interpolation, so we manually
// multiply texcoords by w here and divide by w in fragment shader.
// This creates the characteristic texture warping on large polygons.
// =========================================================================
v_texcoord = a_texcoord * clipPos.w;
v_w = clipPos.w;
// =========================================================================
// Fog Distance Calculation
// Calculate linear fog factor based on view-space depth
// =========================================================================
float depth = -viewPos.z; // View space depth (positive)
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
}

View file

@ -34,9 +34,6 @@
#include "3d/Viewport3D.h" // 3D rendering viewport
#include "3d/Entity3D.h" // 3D game entities
#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,9 +439,7 @@ PyObject* PyInit_mcrfpy()
/*3D entities*/
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
&mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType,
&mcrfpydef::PyBillboardType, &mcrfpydef::PyVoxelGridType,
&mcrfpydef::PyVoxelRegionType,
&mcrfpydef::PyEntityCollection3DIterType,
/*grid layers (#147)*/
&PyColorLayerType, &PyTileLayerType,
@ -541,13 +536,6 @@ 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 PyVoxelRegionType getsetters (Milestone 11)
mcrfpydef::PyVoxelRegionType.tp_getset = PyVoxelRegion::getsetters;
// Set up PyShaderType methods and getsetters (#106)
mcrfpydef::PyShaderType.tp_methods = PyShader::methods;
mcrfpydef::PyShaderType.tp_getset = PyShader::getsetters;
@ -571,8 +559,6 @@ PyObject* PyInit_mcrfpy()
PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist);
PyViewport3DType.tp_weaklistoffset = offsetof(PyViewport3DObject, weakreflist);
mcrfpydef::PyEntity3DType.tp_weaklistoffset = offsetof(PyEntity3DObject, weakreflist);
mcrfpydef::PyModel3DType.tp_weaklistoffset = offsetof(PyModel3DObject, weakreflist);
mcrfpydef::PyBillboardType.tp_weaklistoffset = offsetof(PyBillboardObject, weakreflist);
// #219 - Initialize PyLock context manager type
if (PyLock::init() < 0) {

View file

@ -68,68 +68,8 @@ PyObject* PyColor::pyObject()
sf::Color PyColor::fromPy(PyObject* obj)
{
// Handle None or NULL
if (!obj || obj == Py_None) {
return sf::Color::White;
}
// Check if it's already a Color object
PyTypeObject* color_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
if (color_type) {
bool is_color = PyObject_TypeCheck(obj, color_type);
Py_DECREF(color_type);
if (is_color) {
PyColorObject* self = (PyColorObject*)obj;
return self->data;
}
}
// Handle tuple or list input
if (PyTuple_Check(obj) || PyList_Check(obj)) {
Py_ssize_t size = PySequence_Size(obj);
if (size < 3 || size > 4) {
PyErr_SetString(PyExc_TypeError, "Color tuple/list must have 3 or 4 elements (r, g, b[, a])");
return sf::Color::White;
}
int r = 255, g = 255, b = 255, a = 255;
PyObject* item0 = PySequence_GetItem(obj, 0);
PyObject* item1 = PySequence_GetItem(obj, 1);
PyObject* item2 = PySequence_GetItem(obj, 2);
if (PyLong_Check(item0)) r = (int)PyLong_AsLong(item0);
if (PyLong_Check(item1)) g = (int)PyLong_AsLong(item1);
if (PyLong_Check(item2)) b = (int)PyLong_AsLong(item2);
Py_DECREF(item0);
Py_DECREF(item1);
Py_DECREF(item2);
if (size == 4) {
PyObject* item3 = PySequence_GetItem(obj, 3);
if (PyLong_Check(item3)) a = (int)PyLong_AsLong(item3);
Py_DECREF(item3);
}
// Clamp values
r = std::max(0, std::min(255, r));
g = std::max(0, std::min(255, g));
b = std::max(0, std::min(255, b));
a = std::max(0, std::min(255, a));
return sf::Color(r, g, b, a);
}
// Handle integer (grayscale)
if (PyLong_Check(obj)) {
int v = std::max(0, std::min(255, (int)PyLong_AsLong(obj)));
return sf::Color(v, v, v, 255);
}
// Unknown type - set error and return white
PyErr_SetString(PyExc_TypeError, "Color must be a Color object, tuple, list, or integer");
return sf::Color::White;
PyColorObject* self = (PyColorObject*)obj;
return self->data;
}
sf::Color PyColor::fromPy(PyColorObject* self)

View file

@ -67,29 +67,16 @@ sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s)
PyObject* PyTexture::pyObject()
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture");
if (!type) {
PyErr_SetString(PyExc_RuntimeError, "Failed to get Texture type from module");
return NULL;
}
PyObject* obj = PyTexture::pynew(type, Py_None, Py_None);
Py_DECREF(type); // GetAttrString returns new reference
if (!obj) {
return NULL;
}
try {
// Use placement new to properly construct the shared_ptr
// tp_alloc zeroes memory but doesn't call C++ constructors
new (&((PyTextureObject*)obj)->data) std::shared_ptr<PyTexture>(shared_from_this());
((PyTextureObject*)obj)->data = shared_from_this();
}
catch (std::bad_weak_ptr& e)
{
std::cout << "Bad weak ptr: shared_from_this() failed in PyTexture::pyObject(); did you create a PyTexture outside of std::make_shared? enjoy your segfault, soon!" << std::endl;
Py_DECREF(obj);
PyErr_SetString(PyExc_RuntimeError, "PyTexture was not created with std::make_shared");
return NULL;
}
// TODO - shared_from_this will raise an exception if the object does not have a shared pointer. Constructor should be made private; write a factory function
return obj;
}

View file

@ -28,9 +28,6 @@ public:
sf::Sprite sprite(int index, sf::Vector2f pos = sf::Vector2f(0, 0), sf::Vector2f s = sf::Vector2f(1.0, 1.0));
int getSpriteCount() const { return sheet_width * sheet_height; }
// Get the underlying sf::Texture for 3D rendering
const sf::Texture* getSFMLTexture() const { return &texture; }
PyObject* pyObject();
static PyObject* repr(PyObject*);
static Py_hash_t hash(PyObject*);

View file

@ -1,314 +0,0 @@
# billboard_building_demo.py - Visual demo of Billboard and Mesh Instances
# Demonstrates camera-facing sprites and static mesh placement
import mcrfpy
import sys
import math
# Create demo scene
scene = mcrfpy.Scene("billboard_building_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="Billboard & Building Demo - 3D Sprites and Static Meshes", pos=(20, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
scene.children.append(title)
# Create the 3D viewport
viewport = mcrfpy.Viewport3D(
pos=(50, 60),
size=(600, 450),
render_resolution=(320, 240), # PS1 resolution
fov=60.0,
camera_pos=(16.0, 10.0, 20.0),
camera_target=(8.0, 0.0, 8.0),
bg_color=mcrfpy.Color(80, 120, 180) # Sky blue background
)
scene.children.append(viewport)
# Set up the navigation grid
GRID_SIZE = 16
viewport.set_grid_size(GRID_SIZE, GRID_SIZE)
# Generate terrain
print("Generating terrain...")
hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
hm.mid_point_displacement(0.2, seed=456) # Gentle terrain
hm.normalize(0.0, 0.5) # Keep it low for placing objects
# Apply heightmap
viewport.apply_heightmap(hm, 2.0)
# Build terrain mesh
vertex_count = viewport.build_terrain(
layer_name="terrain",
heightmap=hm,
y_scale=2.0,
cell_size=1.0
)
print(f"Terrain built with {vertex_count} vertices")
# Create terrain colors (earthy tones)
r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
for y in range(GRID_SIZE):
for x in range(GRID_SIZE):
h = hm[x, y]
# Earth/grass colors
r_map[x, y] = 0.25 + h * 0.2
g_map[x, y] = 0.35 + h * 0.25
b_map[x, y] = 0.15 + h * 0.1
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
# =============================================================================
# PART 1: Building Placement using Mesh Instances
# =============================================================================
print("Placing buildings...")
# Add a layer for buildings
viewport.add_layer("buildings", z_index=1)
# Create a simple building model (cube-like structure)
building_model = mcrfpy.Model3D()
# Place several buildings at different locations with transforms
building_positions = [
((2, 0, 2), 0, 1.5), # Position, rotation, scale
((12, 0, 2), 45, 1.2),
((4, 0, 12), 90, 1.0),
((10, 0, 10), 30, 1.8),
]
for pos, rotation, scale in building_positions:
idx = viewport.add_mesh("buildings", building_model, pos=pos, rotation=rotation, scale=scale)
print(f" Placed building {idx} at {pos}")
# Mark the footprint as blocking
gx, gz = int(pos[0]), int(pos[2])
footprint_size = max(1, int(scale))
viewport.place_blocking(grid_pos=(gx, gz), footprint=(footprint_size, footprint_size))
print(f"Placed {len(building_positions)} buildings")
# =============================================================================
# PART 2: Billboard Sprites (camera-facing)
# =============================================================================
print("Creating billboards...")
# Create billboards for "trees" - camera_y mode (stays upright)
tree_positions = [
(3, 0, 5), (5, 0, 3), (6, 0, 8), (9, 0, 5),
(11, 0, 7), (7, 0, 11), (13, 0, 13), (1, 0, 9)
]
# Note: Without actual textures, billboards will render as simple quads
# In a real game, you'd load a tree sprite texture
for i, pos in enumerate(tree_positions):
bb = mcrfpy.Billboard(
pos=pos,
scale=1.5,
facing="camera_y", # Stays upright, only rotates on Y axis
opacity=1.0
)
viewport.add_billboard(bb)
print(f" Created {len(tree_positions)} tree billboards (camera_y facing)")
# Create some particle-like billboards - full camera facing
particle_positions = [
(8, 3, 8), (8.5, 3.5, 8.2), (7.5, 3.2, 7.8), # Floating particles
]
for i, pos in enumerate(particle_positions):
bb = mcrfpy.Billboard(
pos=pos,
scale=0.3,
facing="camera", # Full rotation to face camera
opacity=0.7
)
viewport.add_billboard(bb)
print(f" Created {len(particle_positions)} particle billboards (camera facing)")
# Create a fixed-orientation billboard (signpost)
signpost = mcrfpy.Billboard(
pos=(5, 1.5, 5),
scale=1.0,
facing="fixed", # Manual orientation
)
signpost.theta = math.pi / 4 # 45 degrees horizontal
signpost.phi = 0.0 # No vertical tilt
viewport.add_billboard(signpost)
print(f" Created 1 signpost billboard (fixed facing)")
print(f"Total billboards: {viewport.billboard_count()}")
# =============================================================================
# Info Panel
# =============================================================================
info_panel = mcrfpy.Frame(pos=(670, 60), size=(330, 450),
fill_color=mcrfpy.Color(30, 30, 40),
outline_color=mcrfpy.Color(80, 80, 100),
outline=2.0)
scene.children.append(info_panel)
# Panel title
panel_title = mcrfpy.Caption(text="Billboard & Mesh Demo", pos=(690, 70))
panel_title.fill_color = mcrfpy.Color(200, 200, 255)
scene.children.append(panel_title)
# Billboard info
bb_info = [
("", ""),
("Billboard Modes:", ""),
(" camera", "Full rotation to face camera"),
(" camera_y", "Y-axis only (stays upright)"),
(" fixed", "Manual theta/phi angles"),
("", ""),
(f"Trees:", f"{len(tree_positions)} (camera_y)"),
(f"Particles:", f"{len(particle_positions)} (camera)"),
(f"Signpost:", "1 (fixed)"),
("", ""),
("Mesh Instances:", ""),
(f" Buildings:", f"{len(building_positions)}"),
]
y_offset = 100
for label, value in bb_info:
if label or value:
text = f"{label} {value}" if value else label
cap = mcrfpy.Caption(text=text, pos=(690, y_offset))
cap.fill_color = mcrfpy.Color(150, 150, 170)
scene.children.append(cap)
y_offset += 22
# Dynamic camera info
camera_label = mcrfpy.Caption(text="Camera: Following...", pos=(690, y_offset + 20))
camera_label.fill_color = mcrfpy.Color(180, 180, 200)
scene.children.append(camera_label)
# Instructions at bottom
instructions = mcrfpy.Caption(
text="[Space] Toggle orbit | [1-3] Change billboard mode | [C] Clear buildings | [ESC] Quit",
pos=(20, 530)
)
instructions.fill_color = mcrfpy.Color(150, 150, 150)
scene.children.append(instructions)
# Status line
status = mcrfpy.Caption(text="Status: Billboard & Building demo loaded", pos=(20, 555))
status.fill_color = mcrfpy.Color(100, 200, 100)
scene.children.append(status)
# Animation state
animation_time = [0.0]
camera_orbit = [True]
# Update function
def update(timer, runtime):
animation_time[0] += runtime / 1000.0
# Camera orbit
if camera_orbit[0]:
angle = animation_time[0] * 0.3
radius = 18.0
center_x = 8.0
center_z = 8.0
height = 10.0 + math.sin(animation_time[0] * 0.2) * 2.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, 1.0, center_z)
camera_label.text = f"Camera: Orbit ({x:.1f}, {height:.1f}, {z:.1f})"
# Animate particle billboards (bobbing up and down)
bb_count = viewport.billboard_count()
if bb_count > len(tree_positions):
particle_start = len(tree_positions)
for i in range(particle_start, particle_start + len(particle_positions)):
if i < bb_count:
bb = viewport.get_billboard(i)
pos = bb.pos
new_y = 3.0 + math.sin(animation_time[0] * 2.0 + i * 0.5) * 0.5
bb.pos = (pos[0], new_y, pos[2])
# Key handler
def on_key(key, state):
if state != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.SPACE:
camera_orbit[0] = not camera_orbit[0]
status.text = f"Camera orbit: {'ON' if camera_orbit[0] else 'OFF'}"
elif key == mcrfpy.Key.NUM_1:
# Change all tree billboards to "camera" mode
for i in range(len(tree_positions)):
viewport.get_billboard(i).facing = "camera"
status.text = "Trees now use 'camera' facing (full rotation)"
elif key == mcrfpy.Key.NUM_2:
# Change all tree billboards to "camera_y" mode
for i in range(len(tree_positions)):
viewport.get_billboard(i).facing = "camera_y"
status.text = "Trees now use 'camera_y' facing (upright)"
elif key == mcrfpy.Key.NUM_3:
# Change all tree billboards to "fixed" mode
for i in range(len(tree_positions)):
bb = viewport.get_billboard(i)
bb.facing = "fixed"
bb.theta = i * 0.5 # Different angles
status.text = "Trees now use 'fixed' facing (manual angles)"
elif key == mcrfpy.Key.C:
viewport.clear_meshes("buildings")
status.text = "Cleared all buildings from layer"
elif key == mcrfpy.Key.O:
# Adjust tree opacity
for i in range(len(tree_positions)):
bb = viewport.get_billboard(i)
bb.opacity = 0.5 if bb.opacity > 0.7 else 1.0
status.text = f"Tree opacity toggled"
elif key == mcrfpy.Key.V:
# Toggle tree visibility
for i in range(len(tree_positions)):
bb = viewport.get_billboard(i)
bb.visible = not bb.visible
status.text = f"Tree visibility toggled"
elif key == mcrfpy.Key.ESCAPE:
mcrfpy.exit()
# Set up scene
scene.on_key = on_key
# Create timer for updates
timer = mcrfpy.Timer("billboard_update", update, 16) # ~60fps
# Activate scene
mcrfpy.current_scene = scene
print()
print("Billboard & Building Demo loaded!")
print()
print("Controls:")
print(" [Space] Toggle camera orbit")
print(" [1] Trees -> 'camera' facing")
print(" [2] Trees -> 'camera_y' facing (default)")
print(" [3] Trees -> 'fixed' facing")
print(" [O] Toggle tree opacity")
print(" [V] Toggle tree visibility")
print(" [C] Clear buildings")
print(" [ESC] Quit")

View file

@ -1,462 +0,0 @@
# integration_demo.py - Milestone 8 Integration Demo
# Showcases all 3D features: terrain, entities, pathfinding, FOV, billboards, UI, input
import mcrfpy
import math
import random
DEMO_NAME = "3D Integration Demo"
DEMO_DESCRIPTION = """Complete 3D demo with terrain, player, NPC, FOV, and UI overlay.
Controls:
Arrow keys: Move player
Click: Move to clicked position
ESC: Quit
"""
# Create the main scene
scene = mcrfpy.Scene("integration_demo")
# =============================================================================
# Constants
# =============================================================================
GRID_WIDTH = 32
GRID_DEPTH = 32
CELL_SIZE = 1.0
TERRAIN_Y_SCALE = 3.0
FOV_RADIUS = 10
# =============================================================================
# 3D Viewport
# =============================================================================
viewport = mcrfpy.Viewport3D(
pos=(10, 10),
size=(700, 550),
render_resolution=(350, 275),
fov=60.0,
camera_pos=(16.0, 15.0, 25.0),
camera_target=(16.0, 0.0, 16.0),
bg_color=mcrfpy.Color(40, 60, 100)
)
viewport.enable_fog = True
viewport.fog_near = 10.0
viewport.fog_far = 40.0
viewport.fog_color = mcrfpy.Color(40, 60, 100)
scene.children.append(viewport)
# Set up navigation grid
viewport.set_grid_size(GRID_WIDTH, GRID_DEPTH)
# =============================================================================
# Terrain Generation
# =============================================================================
print("Generating terrain...")
# Create heightmap with hills
hm = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
hm.mid_point_displacement(roughness=0.5)
hm.normalize(0.0, 1.0)
# Build terrain mesh
viewport.build_terrain(
layer_name="terrain",
heightmap=hm,
y_scale=TERRAIN_Y_SCALE,
cell_size=CELL_SIZE
)
# Apply heightmap to navigation grid
viewport.apply_heightmap(hm, TERRAIN_Y_SCALE)
# Mark steep slopes and water as unwalkable
viewport.apply_threshold(hm, 0.0, 0.12, False) # Low areas = water (unwalkable)
viewport.set_slope_cost(0.4, 2.0)
# Create base terrain colors (green/brown based on height)
r_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
g_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
b_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
# Storage for base colors (for FOV dimming)
base_colors = []
for z in range(GRID_DEPTH):
row = []
for x in range(GRID_WIDTH):
h = hm[x, z]
if h < 0.12: # Water
r, g, b = 0.1, 0.2, 0.4
elif h < 0.25: # Sand/beach
r, g, b = 0.6, 0.5, 0.3
elif h < 0.6: # Grass
r, g, b = 0.2 + random.random() * 0.1, 0.4 + random.random() * 0.15, 0.15
else: # Rock/mountain
r, g, b = 0.4, 0.35, 0.3
r_map[x, z] = r
g_map[x, z] = g
b_map[x, z] = b
row.append((r, g, b))
base_colors.append(row)
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
# =============================================================================
# Find walkable starting positions
# =============================================================================
def find_walkable_pos():
"""Find a random walkable position"""
for _ in range(100):
x = random.randint(2, GRID_WIDTH - 3)
z = random.randint(2, GRID_DEPTH - 3)
cell = viewport.at(x, z)
if cell.walkable:
return (x, z)
return (GRID_WIDTH // 2, GRID_DEPTH // 2)
# =============================================================================
# Player Entity
# =============================================================================
player_start = find_walkable_pos()
player = mcrfpy.Entity3D(pos=player_start, scale=0.8, color=mcrfpy.Color(50, 150, 255))
viewport.entities.append(player)
print(f"Player at {player_start}")
# Track discovered cells
discovered = set()
discovered.add(player_start)
# =============================================================================
# NPC Entity with Patrol AI
# =============================================================================
npc_start = find_walkable_pos()
while abs(npc_start[0] - player_start[0]) < 5 and abs(npc_start[1] - player_start[1]) < 5:
npc_start = find_walkable_pos()
npc = mcrfpy.Entity3D(pos=npc_start, scale=0.7, color=mcrfpy.Color(255, 100, 100))
viewport.entities.append(npc)
print(f"NPC at {npc_start}")
# NPC patrol system
class NPCController:
def __init__(self, entity, waypoints):
self.entity = entity
self.waypoints = waypoints
self.current_wp = 0
self.path = []
self.path_index = 0
def update(self):
if self.entity.is_moving:
return
# If we have a path, follow it
if self.path_index < len(self.path):
next_pos = self.path[self.path_index]
self.entity.pos = next_pos
self.path_index += 1
return
# Reached waypoint, go to next
self.current_wp = (self.current_wp + 1) % len(self.waypoints)
target = self.waypoints[self.current_wp]
# Compute path to next waypoint
self.path = self.entity.path_to(target[0], target[1])
self.path_index = 0
# Create patrol waypoints
npc_waypoints = []
for _ in range(4):
wp = find_walkable_pos()
npc_waypoints.append(wp)
npc_controller = NPCController(npc, npc_waypoints)
# =============================================================================
# FOV Visualization
# =============================================================================
def update_fov_colors():
"""Update terrain colors based on FOV"""
# Compute FOV from player position
visible_cells = viewport.compute_fov((player.pos[0], player.pos[1]), FOV_RADIUS)
visible_set = set((c[0], c[1]) for c in visible_cells)
# Update discovered
discovered.update(visible_set)
# Update terrain colors
r_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
g_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
b_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
for z in range(GRID_DEPTH):
for x in range(GRID_WIDTH):
base_r, base_g, base_b = base_colors[z][x]
if (x, z) in visible_set:
# Fully visible
r_map[x, z] = base_r
g_map[x, z] = base_g
b_map[x, z] = base_b
elif (x, z) in discovered:
# Discovered but not visible - dim
r_map[x, z] = base_r * 0.4
g_map[x, z] = base_g * 0.4
b_map[x, z] = base_b * 0.4
else:
# Never seen - very dark
r_map[x, z] = base_r * 0.1
g_map[x, z] = base_g * 0.1
b_map[x, z] = base_b * 0.1
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
# Initial FOV update
update_fov_colors()
# =============================================================================
# UI Overlay
# =============================================================================
ui_frame = mcrfpy.Frame(
pos=(720, 10),
size=(260, 200),
fill_color=mcrfpy.Color(20, 20, 30, 220),
outline_color=mcrfpy.Color(80, 80, 120),
outline=2.0
)
scene.children.append(ui_frame)
title_label = mcrfpy.Caption(text="3D Integration Demo", pos=(740, 20))
title_label.fill_color = mcrfpy.Color(255, 255, 150)
scene.children.append(title_label)
status_label = mcrfpy.Caption(text="Status: Idle", pos=(740, 50))
status_label.fill_color = mcrfpy.Color(150, 255, 150)
scene.children.append(status_label)
player_pos_label = mcrfpy.Caption(text="Player: (0, 0)", pos=(740, 75))
player_pos_label.fill_color = mcrfpy.Color(100, 200, 255)
scene.children.append(player_pos_label)
npc_pos_label = mcrfpy.Caption(text="NPC: (0, 0)", pos=(740, 100))
npc_pos_label.fill_color = mcrfpy.Color(255, 150, 150)
scene.children.append(npc_pos_label)
fps_label = mcrfpy.Caption(text="FPS: --", pos=(740, 125))
fps_label.fill_color = mcrfpy.Color(200, 200, 200)
scene.children.append(fps_label)
discovered_label = mcrfpy.Caption(text="Discovered: 0", pos=(740, 150))
discovered_label.fill_color = mcrfpy.Color(180, 180, 100)
scene.children.append(discovered_label)
# Controls info
controls_frame = mcrfpy.Frame(
pos=(720, 220),
size=(260, 120),
fill_color=mcrfpy.Color(20, 20, 30, 200),
outline_color=mcrfpy.Color(60, 60, 80),
outline=1.0
)
scene.children.append(controls_frame)
ctrl_title = mcrfpy.Caption(text="Controls:", pos=(740, 230))
ctrl_title.fill_color = mcrfpy.Color(200, 200, 100)
scene.children.append(ctrl_title)
ctrl_lines = [
"Arrow keys: Move",
"Click: Pathfind",
"F: Toggle follow cam",
"ESC: Quit"
]
for i, line in enumerate(ctrl_lines):
cap = mcrfpy.Caption(text=line, pos=(740, 255 + i * 20))
cap.fill_color = mcrfpy.Color(150, 150, 150)
scene.children.append(cap)
# =============================================================================
# Game State
# =============================================================================
follow_camera = True
frame_count = 0
fps_update_time = 0
# =============================================================================
# Update Function
# =============================================================================
def game_update(timer, runtime):
global frame_count, fps_update_time
try:
# Calculate FPS
frame_count += 1
if runtime - fps_update_time >= 1000: # Update FPS every second
fps = frame_count
fps_label.text = f"FPS: {fps}"
frame_count = 0
fps_update_time = runtime
# Update NPC patrol
npc_controller.update()
# Update UI labels
px, pz = player.pos
player_pos_label.text = f"Player: ({px}, {pz})"
nx, nz = npc.pos
npc_pos_label.text = f"NPC: ({nx}, {nz})"
discovered_label.text = f"Discovered: {len(discovered)}"
# Camera follow
if follow_camera:
viewport.follow(player, distance=12.0, height=8.0, smoothing=0.1)
# Update status based on player state
if player.is_moving:
status_label.text = "Status: Moving"
status_label.fill_color = mcrfpy.Color(255, 255, 100)
else:
status_label.text = "Status: Idle"
status_label.fill_color = mcrfpy.Color(150, 255, 150)
except Exception as e:
print(f"Update error: {e}")
# =============================================================================
# Input Handling
# =============================================================================
def try_move_player(dx, dz):
"""Try to move player in direction"""
new_x = player.pos[0] + dx
new_z = player.pos[1] + dz
if not viewport.is_in_fov(new_x, new_z):
# Allow moving into discovered cells even if not currently visible
if (new_x, new_z) not in discovered:
return False
if new_x < 0 or new_x >= GRID_WIDTH or new_z < 0 or new_z >= GRID_DEPTH:
return False
cell = viewport.at(new_x, new_z)
if not cell.walkable:
return False
player.pos = (new_x, new_z)
update_fov_colors()
return True
def on_key(key, state):
global follow_camera
if state != mcrfpy.InputState.PRESSED:
return
if player.is_moving:
return # Don't accept input while moving
dx, dz = 0, 0
if key == mcrfpy.Key.UP:
dz = -1
elif key == mcrfpy.Key.DOWN:
dz = 1
elif key == mcrfpy.Key.LEFT:
dx = -1
elif key == mcrfpy.Key.RIGHT:
dx = 1
elif key == mcrfpy.Key.F:
follow_camera = not follow_camera
status_label.text = f"Camera: {'Follow' if follow_camera else 'Free'}"
return
elif key == mcrfpy.Key.ESCAPE:
mcrfpy.exit()
return
if dx != 0 or dz != 0:
try_move_player(dx, dz)
# Click-to-move handling
def on_click(pos, button, state):
if button != mcrfpy.MouseButton.LEFT or state != mcrfpy.InputState.PRESSED:
return
if player.is_moving:
return
# Convert click position to viewport-relative coordinates
vp_x = pos.x - viewport.x
vp_y = pos.y - viewport.y
# Check if click is within viewport
if vp_x < 0 or vp_x >= viewport.w or vp_y < 0 or vp_y >= viewport.h:
return
# Convert to world position
world_pos = viewport.screen_to_world(vp_x, vp_y)
if world_pos is None:
return
# Convert to grid position
grid_x = int(world_pos[0] / CELL_SIZE)
grid_z = int(world_pos[2] / CELL_SIZE)
# Validate grid position
if grid_x < 0 or grid_x >= GRID_WIDTH or grid_z < 0 or grid_z >= GRID_DEPTH:
return
cell = viewport.at(grid_x, grid_z)
if not cell.walkable:
status_label.text = "Status: Can't walk there!"
status_label.fill_color = mcrfpy.Color(255, 100, 100)
return
# Find path
path = player.path_to(grid_x, grid_z)
if not path:
status_label.text = "Status: No path!"
status_label.fill_color = mcrfpy.Color(255, 100, 100)
return
# Follow path (limited to FOV_RADIUS steps)
limited_path = path[:FOV_RADIUS]
player.follow_path(limited_path)
status_label.text = f"Status: Moving ({len(limited_path)} steps)"
status_label.fill_color = mcrfpy.Color(255, 255, 100)
# Schedule FOV update after movement completes
fov_update_timer = None
def update_fov_after_move(*args):
# Accept any number of args since timer may pass (runtime) or (timer, runtime)
nonlocal fov_update_timer
if not player.is_moving:
update_fov_colors()
if fov_update_timer:
fov_update_timer.stop()
fov_update_timer = mcrfpy.Timer("fov_update", update_fov_after_move, 100)
scene.on_key = on_key
viewport.on_click = on_click
# =============================================================================
# Start Game
# =============================================================================
timer = mcrfpy.Timer("game_update", game_update, 16) # ~60 FPS
mcrfpy.current_scene = scene
print()
print("=" * 60)
print("3D Integration Demo Loaded!")
print("=" * 60)
print(f" Terrain: {GRID_WIDTH}x{GRID_DEPTH} cells")
print(f" Player starts at: {player_start}")
print(f" NPC patrolling {len(npc_waypoints)} waypoints")
print()
print("Controls:")
print(" Arrow keys: Move player")
print(" Click: Pathfind to location")
print(" F: Toggle camera follow")
print(" ESC: Quit")
print("=" * 60)

View file

@ -1,240 +0,0 @@
# model_loading_demo.py - Visual demo of Model3D model loading
# Shows both procedural primitives and loaded .glb models
import mcrfpy
import sys
import math
import os
# Create demo scene
scene = mcrfpy.Scene("model_loading_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="Model3D Demo - Procedural & glTF Models", pos=(20, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
scene.children.append(title)
# Create the 3D viewport
viewport = mcrfpy.Viewport3D(
pos=(50, 60),
size=(600, 450),
# render_resolution=(320, 240), # PS1 resolution
render_resolution=(600,450),
fov=60.0,
camera_pos=(0.0, 3.0, 8.0),
camera_target=(0.0, 1.0, 0.0),
bg_color=mcrfpy.Color(30, 30, 50)
)
scene.children.append(viewport)
# Set up navigation grid
GRID_SIZE = 32
viewport.set_grid_size(GRID_SIZE, GRID_SIZE)
# Build a simple flat floor
hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
hm.normalize(0.0, 0.0)
viewport.apply_heightmap(hm, 0.0)
vertex_count = viewport.build_terrain(
layer_name="floor",
heightmap=hm,
y_scale=0.0,
cell_size=1.0
)
# Apply floor colors (checkerboard pattern)
r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
for y in range(GRID_SIZE):
for x in range(GRID_SIZE):
checker = ((x + y) % 2) * 0.1 + 0.15
r_map[x, y] = checker
g_map[x, y] = checker
b_map[x, y] = checker + 0.05
viewport.apply_terrain_colors("floor", r_map, g_map, b_map)
# Create procedural models
print("Creating procedural models...")
cube_model = mcrfpy.Model3D.cube(1.0)
sphere_model = mcrfpy.Model3D.sphere(0.5, 12, 8)
# Try to load glTF models
loaded_models = {}
models_dir = "../assets/models"
if os.path.exists(models_dir):
for filename in ["Duck.glb", "Box.glb", "Lantern.glb", "WaterBottle.glb"]:
path = os.path.join(models_dir, filename)
if os.path.exists(path):
try:
model = mcrfpy.Model3D(path)
loaded_models[filename] = model
print(f"Loaded {filename}: {model.vertex_count} verts, {model.triangle_count} tris")
except Exception as e:
print(f"Failed to load {filename}: {e}")
# Create entities with different models
entities = []
# Row 1: Procedural primitives
entity_configs = [
((12, 16), cube_model, 1.0, mcrfpy.Color(255, 100, 100), "Cube"),
((16, 16), sphere_model, 1.0, mcrfpy.Color(100, 255, 100), "Sphere"),
((20, 16), None, 1.0, mcrfpy.Color(200, 200, 200), "Placeholder"),
]
# Row 2: Loaded glTF models (if available)
if "Duck.glb" in loaded_models:
# Duck is huge (~160 units), scale it down significantly
entity_configs.append(((14, 12), loaded_models["Duck.glb"], 0.006, mcrfpy.Color(255, 200, 50), "Duck"))
if "Box.glb" in loaded_models:
entity_configs.append(((16, 12), loaded_models["Box.glb"], 1.5, mcrfpy.Color(150, 100, 50), "Box (glb)"))
if "Lantern.glb" in loaded_models:
# Lantern is ~25 units tall
entity_configs.append(((18, 12), loaded_models["Lantern.glb"], 0.08, mcrfpy.Color(255, 200, 100), "Lantern"))
if "WaterBottle.glb" in loaded_models:
# WaterBottle is ~0.26 units tall
entity_configs.append(((20, 12), loaded_models["WaterBottle.glb"], 4.0, mcrfpy.Color(100, 150, 255), "Bottle"))
for pos, model, scale, color, name in entity_configs:
e = mcrfpy.Entity3D(pos=pos, scale=scale, color=color)
if model:
e.model = model
viewport.entities.append(e)
entities.append((e, name, model))
print(f"Created {len(entities)} entities")
# Info panel on the right
info_panel = mcrfpy.Frame(pos=(670, 60), size=(330, 450),
fill_color=mcrfpy.Color(30, 30, 40),
outline_color=mcrfpy.Color(80, 80, 100),
outline=2.0)
scene.children.append(info_panel)
# Panel title
panel_title = mcrfpy.Caption(text="Model Information", pos=(690, 70))
panel_title.fill_color = mcrfpy.Color(200, 200, 255)
scene.children.append(panel_title)
# Model info labels
y_offset = 100
for e, name, model in entities:
if model:
info = f"{name}: {model.vertex_count}v, {model.triangle_count}t"
else:
info = f"{name}: Placeholder (36v, 12t)"
label = mcrfpy.Caption(text=info, pos=(690, y_offset))
label.fill_color = e.color
scene.children.append(label)
y_offset += 22
# Separator
y_offset += 10
sep = mcrfpy.Caption(text="--- glTF Support ---", pos=(690, y_offset))
sep.fill_color = mcrfpy.Color(150, 150, 150)
scene.children.append(sep)
y_offset += 22
# glTF info
gltf_info = [
"Format: glTF 2.0 (.glb, .gltf)",
"Library: cgltf (C99)",
f"Loaded models: {len(loaded_models)}",
]
for info in gltf_info:
label = mcrfpy.Caption(text=info, pos=(690, y_offset))
label.fill_color = mcrfpy.Color(150, 150, 170)
scene.children.append(label)
y_offset += 20
# Instructions at bottom
instructions = mcrfpy.Caption(
text="[Space] Toggle rotation | [1-3] Camera presets | [ESC] Quit",
pos=(20, 530)
)
instructions.fill_color = mcrfpy.Color(150, 150, 150)
scene.children.append(instructions)
# Status line
status = mcrfpy.Caption(text="Status: Showing procedural and glTF models", pos=(20, 555))
status.fill_color = mcrfpy.Color(100, 200, 100)
scene.children.append(status)
# Animation state
animation_time = [0.0]
rotate_entities = [True]
# Camera presets
camera_presets = [
((0.0, 5.0, 12.0), (0.0, 1.0, 0.0), "Front view"),
((12.0, 8.0, 0.0), (0.0, 1.0, 0.0), "Side view"),
((0.0, 15.0, 0.1), (0.0, 0.0, 0.0), "Top-down view"),
]
current_preset = [0]
# Update function
def update(timer, runtime):
animation_time[0] += runtime / 1000.0
if rotate_entities[0]:
for i, (e, name, model) in enumerate(entities):
e.rotation = (animation_time[0] * 30.0 + i * 45.0) % 360.0
# Key handler
def on_key(key, state):
if state != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.SPACE:
rotate_entities[0] = not rotate_entities[0]
status.text = f"Rotation: {'ON' if rotate_entities[0] else 'OFF'}"
elif key == mcrfpy.Key.NUM_1:
pos, target, name = camera_presets[0]
viewport.camera_pos = pos
viewport.camera_target = target
status.text = f"Camera: {name}"
elif key == mcrfpy.Key.NUM_2:
pos, target, name = camera_presets[1]
viewport.camera_pos = pos
viewport.camera_target = target
status.text = f"Camera: {name}"
elif key == mcrfpy.Key.NUM_3:
pos, target, name = camera_presets[2]
viewport.camera_pos = pos
viewport.camera_target = target
status.text = f"Camera: {name}"
elif key == mcrfpy.Key.ESCAPE:
mcrfpy.exit()
# Set up scene
scene.on_key = on_key
# Create timer for updates
timer = mcrfpy.Timer("model_update", update, 16)
# Activate scene
mcrfpy.current_scene = scene
print()
print("Model3D Demo loaded!")
print(f"Procedural models: cube, sphere")
print(f"glTF models loaded: {list(loaded_models.keys())}")
print()
print("Controls:")
print(" [Space] Toggle rotation")
print(" [1-3] Camera presets")
print(" [ESC] Quit")

View file

@ -1,275 +0,0 @@
# skeletal_animation_demo.py - 3D Skeletal Animation Demo Screen
# Demonstrates Entity3D animation with real animated glTF models
import mcrfpy
import sys
import os
DEMO_NAME = "3D Skeletal Animation"
DEMO_DESCRIPTION = """Entity3D Animation API with real skeletal models"""
# Create demo scene
scene = mcrfpy.Scene("skeletal_animation_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="Skeletal Animation Demo", pos=(20, 10))
title.fill_color = mcrfpy.Color(255, 255, 100)
scene.children.append(title)
# Create the 3D viewport
viewport = mcrfpy.Viewport3D(
pos=(50, 50),
size=(600, 500),
render_resolution=(600, 500),
fov=60.0,
camera_pos=(0.0, 2.0, 5.0),
camera_target=(0.0, 1.0, 0.0),
bg_color=mcrfpy.Color(30, 30, 50)
)
scene.children.append(viewport)
# Set up navigation grid
GRID_SIZE = 16
viewport.set_grid_size(GRID_SIZE, GRID_SIZE)
# Build a simple flat floor
hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
hm.normalize(0.0, 0.0)
viewport.apply_heightmap(hm, 0.0)
viewport.build_terrain(
layer_name="floor",
heightmap=hm,
y_scale=0.0,
cell_size=1.0
)
# Apply floor colors (dark gray)
r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
for y in range(GRID_SIZE):
for x in range(GRID_SIZE):
checker = ((x + y) % 2) * 0.1 + 0.15
r_map[x, y] = checker
g_map[x, y] = checker
b_map[x, y] = checker + 0.05
viewport.apply_terrain_colors("floor", r_map, g_map, b_map)
# Load animated models
animated_entity = None
model_info = "No animated model"
# Try to load CesiumMan (humanoid with walk animation)
try:
model = mcrfpy.Model3D("../assets/models/CesiumMan.glb")
if model.has_skeleton:
animated_entity = mcrfpy.Entity3D(pos=(8, 8), scale=1.0, color=mcrfpy.Color(200, 180, 150))
animated_entity.model = model
viewport.entities.append(animated_entity)
# Set up animation
clips = model.animation_clips
if clips:
animated_entity.anim_clip = clips[0]
animated_entity.anim_loop = True
animated_entity.anim_speed = 1.0
model_info = f"CesiumMan: {model.bone_count} bones, {model.vertex_count} verts"
print(f"Loaded {model_info}")
print(f"Animation clips: {clips}")
except Exception as e:
print(f"Failed to load CesiumMan: {e}")
# Also try RiggedSimple as a second model
try:
model2 = mcrfpy.Model3D("../assets/models/RiggedSimple.glb")
if model2.has_skeleton:
entity2 = mcrfpy.Entity3D(pos=(10, 8), scale=0.5, color=mcrfpy.Color(100, 200, 255))
entity2.model = model2
viewport.entities.append(entity2)
clips = model2.animation_clips
if clips:
entity2.anim_clip = clips[0]
entity2.anim_loop = True
entity2.anim_speed = 1.5
print(f"Loaded RiggedSimple: {model2.bone_count} bones")
except Exception as e:
print(f"Failed to load RiggedSimple: {e}")
# Info panel on the right
info_panel = mcrfpy.Frame(pos=(670, 50), size=(330, 500),
fill_color=mcrfpy.Color(30, 30, 40),
outline_color=mcrfpy.Color(80, 80, 100),
outline=2.0)
scene.children.append(info_panel)
# Panel title
panel_title = mcrfpy.Caption(text="Animation Properties", pos=(690, 60))
panel_title.fill_color = mcrfpy.Color(200, 200, 255)
scene.children.append(panel_title)
# Status labels (will be updated by timer)
status_labels = []
y_offset = 90
label_texts = [
"Model: loading...",
"anim_clip: ",
"anim_time: 0.00",
"anim_speed: 1.00",
"anim_loop: True",
"anim_paused: False",
"anim_frame: 0",
]
for text in label_texts:
label = mcrfpy.Caption(text=text, pos=(690, y_offset))
label.fill_color = mcrfpy.Color(150, 200, 150)
scene.children.append(label)
status_labels.append(label)
y_offset += 25
# Set initial model info
status_labels[0].text = f"Model: {model_info}"
# Controls section
y_offset += 20
controls_title = mcrfpy.Caption(text="Controls:", pos=(690, y_offset))
controls_title.fill_color = mcrfpy.Color(255, 255, 200)
scene.children.append(controls_title)
y_offset += 25
controls = [
"[SPACE] Toggle pause",
"[L] Toggle loop",
"[+/-] Adjust speed",
"[R] Reset time",
"[1-3] Camera presets",
]
for ctrl in controls:
cap = mcrfpy.Caption(text=ctrl, pos=(690, y_offset))
cap.fill_color = mcrfpy.Color(180, 180, 150)
scene.children.append(cap)
y_offset += 20
# Auto-animate section
y_offset += 20
auto_title = mcrfpy.Caption(text="Auto-Animate:", pos=(690, y_offset))
auto_title.fill_color = mcrfpy.Color(255, 200, 200)
scene.children.append(auto_title)
y_offset += 25
auto_labels = []
auto_texts = [
"auto_animate: True",
"walk_clip: 'walk'",
"idle_clip: 'idle'",
]
for text in auto_texts:
cap = mcrfpy.Caption(text=text, pos=(690, y_offset))
cap.fill_color = mcrfpy.Color(180, 160, 160)
scene.children.append(cap)
auto_labels.append(cap)
y_offset += 20
# Instructions at bottom
status = mcrfpy.Caption(text="Status: Animation playing", pos=(20, 570))
status.fill_color = mcrfpy.Color(100, 200, 100)
scene.children.append(status)
# Camera presets
camera_presets = [
((0.0, 2.0, 5.0), (0.0, 1.0, 0.0), "Front view"),
((5.0, 3.0, 0.0), (0.0, 1.0, 0.0), "Side view"),
((0.0, 6.0, 0.1), (0.0, 0.0, 0.0), "Top-down view"),
]
# Update function - updates display and entity rotation
def update(timer, runtime):
if animated_entity:
# Update status display
status_labels[1].text = f"anim_clip: '{animated_entity.anim_clip}'"
status_labels[2].text = f"anim_time: {animated_entity.anim_time:.2f}"
status_labels[3].text = f"anim_speed: {animated_entity.anim_speed:.2f}"
status_labels[4].text = f"anim_loop: {animated_entity.anim_loop}"
status_labels[5].text = f"anim_paused: {animated_entity.anim_paused}"
status_labels[6].text = f"anim_frame: {animated_entity.anim_frame}"
auto_labels[0].text = f"auto_animate: {animated_entity.auto_animate}"
auto_labels[1].text = f"walk_clip: '{animated_entity.walk_clip}'"
auto_labels[2].text = f"idle_clip: '{animated_entity.idle_clip}'"
# Key handler
def on_key(key, state):
if state != mcrfpy.InputState.PRESSED:
return
if animated_entity:
if key == mcrfpy.Key.SPACE:
animated_entity.anim_paused = not animated_entity.anim_paused
status.text = f"Status: {'Paused' if animated_entity.anim_paused else 'Playing'}"
elif key == mcrfpy.Key.L:
animated_entity.anim_loop = not animated_entity.anim_loop
status.text = f"Status: Loop {'ON' if animated_entity.anim_loop else 'OFF'}"
elif key == mcrfpy.Key.EQUAL or key == mcrfpy.Key.ADD:
animated_entity.anim_speed = min(animated_entity.anim_speed + 0.25, 4.0)
status.text = f"Status: Speed {animated_entity.anim_speed:.2f}x"
elif key == mcrfpy.Key.HYPHEN or key == mcrfpy.Key.SUBTRACT:
animated_entity.anim_speed = max(animated_entity.anim_speed - 0.25, 0.0)
status.text = f"Status: Speed {animated_entity.anim_speed:.2f}x"
elif key == mcrfpy.Key.R:
animated_entity.anim_time = 0.0
status.text = "Status: Animation reset"
# Camera presets
if key == mcrfpy.Key.NUM_1:
pos, target, name = camera_presets[0]
viewport.camera_pos = pos
viewport.camera_target = target
status.text = f"Camera: {name}"
elif key == mcrfpy.Key.NUM_2:
pos, target, name = camera_presets[1]
viewport.camera_pos = pos
viewport.camera_target = target
status.text = f"Camera: {name}"
elif key == mcrfpy.Key.NUM_3:
pos, target, name = camera_presets[2]
viewport.camera_pos = pos
viewport.camera_target = target
status.text = f"Camera: {name}"
elif key == mcrfpy.Key.ESCAPE:
mcrfpy.exit()
# Set up scene
scene.on_key = on_key
# Create timer for updates
timer = mcrfpy.Timer("anim_update", update, 16)
# Activate scene
mcrfpy.current_scene = scene
print()
print("Skeletal Animation Demo loaded!")
print("Controls:")
print(" [Space] Toggle pause")
print(" [L] Toggle loop")
print(" [+/-] Adjust speed")
print(" [R] Reset time")
print(" [1-3] Camera presets")
print(" [ESC] Quit")

View file

@ -1,263 +0,0 @@
"""VoxelGrid Core Demo (Milestone 9)
Demonstrates the VoxelGrid data structure without rendering.
This is a "console demo" that creates VoxelGrids, defines materials,
places voxel patterns, and displays statistics.
Note: Visual rendering comes in Milestone 10 (VoxelMeshing).
"""
import mcrfpy
from mcrfpy import Color
def format_bytes(bytes_val):
"""Format bytes as human-readable string"""
if bytes_val < 1024:
return f"{bytes_val} B"
elif bytes_val < 1024 * 1024:
return f"{bytes_val / 1024:.1f} KB"
else:
return f"{bytes_val / (1024 * 1024):.1f} MB"
def print_header(title):
"""Print a formatted header"""
print("\n" + "=" * 60)
print(f" {title}")
print("=" * 60)
def print_grid_stats(vg, name="VoxelGrid"):
"""Print statistics for a VoxelGrid"""
print(f"\n {name}:")
print(f" Dimensions: {vg.width} x {vg.height} x {vg.depth}")
print(f" Total voxels: {vg.width * vg.height * vg.depth:,}")
print(f" Cell size: {vg.cell_size} units")
print(f" Materials: {vg.material_count}")
print(f" Non-air voxels: {vg.count_non_air():,}")
print(f" Memory estimate: {format_bytes(vg.width * vg.height * vg.depth)}")
print(f" Offset: {vg.offset}")
print(f" Rotation: {vg.rotation} deg")
def demo_basic_creation():
"""Demonstrate basic VoxelGrid creation"""
print_header("1. Basic VoxelGrid Creation")
# Create various sizes
small = mcrfpy.VoxelGrid(size=(8, 4, 8))
medium = mcrfpy.VoxelGrid(size=(16, 8, 16), cell_size=1.0)
large = mcrfpy.VoxelGrid(size=(32, 16, 32), cell_size=0.5)
print_grid_stats(small, "Small (8x4x8)")
print_grid_stats(medium, "Medium (16x8x16)")
print_grid_stats(large, "Large (32x16x32, 0.5 cell size)")
def demo_material_palette():
"""Demonstrate material palette system"""
print_header("2. Material Palette System")
vg = mcrfpy.VoxelGrid(size=(16, 8, 16))
# Define a palette of building materials
materials = {}
materials['stone'] = vg.add_material("stone", color=Color(128, 128, 128))
materials['brick'] = vg.add_material("brick", color=Color(165, 42, 42))
materials['wood'] = vg.add_material("wood", color=Color(139, 90, 43))
materials['glass'] = vg.add_material("glass",
color=Color(200, 220, 255, 128),
transparent=True,
path_cost=1.0)
materials['metal'] = vg.add_material("metal",
color=Color(180, 180, 190),
path_cost=0.8)
materials['grass'] = vg.add_material("grass", color=Color(60, 150, 60))
print(f"\n Defined {vg.material_count} materials:")
print(f" ID 0: air (implicit, always transparent)")
for name, mat_id in materials.items():
mat = vg.get_material(mat_id)
c = mat['color']
props = []
if mat['transparent']:
props.append("transparent")
if mat['path_cost'] != 1.0:
props.append(f"cost={mat['path_cost']}")
props_str = f" ({', '.join(props)})" if props else ""
print(f" ID {mat_id}: {name} RGB({c.r},{c.g},{c.b},{c.a}){props_str}")
return vg, materials
def demo_voxel_placement():
"""Demonstrate voxel placement patterns"""
print_header("3. Voxel Placement Patterns")
vg, materials = demo_material_palette()
stone = materials['stone']
brick = materials['brick']
wood = materials['wood']
# Pattern 1: Solid cube
print("\n Pattern: Solid 4x4x4 cube at origin")
for z in range(4):
for y in range(4):
for x in range(4):
vg.set(x, y, z, stone)
print(f" Placed {vg.count_material(stone)} stone voxels")
# Pattern 2: Checkerboard floor
print("\n Pattern: Checkerboard floor at y=0, x=6-14, z=0-8")
for z in range(8):
for x in range(6, 14):
mat = stone if (x + z) % 2 == 0 else brick
vg.set(x, 0, z, mat)
print(f" Stone: {vg.count_material(stone)}, Brick: {vg.count_material(brick)}")
# Pattern 3: Hollow cube (walls only)
print("\n Pattern: Hollow cube frame 4x4x4 at x=10, z=10")
for x in range(4):
for y in range(4):
for z in range(4):
# Only place on edges
on_edge_x = (x == 0 or x == 3)
on_edge_y = (y == 0 or y == 3)
on_edge_z = (z == 0 or z == 3)
if sum([on_edge_x, on_edge_y, on_edge_z]) >= 2:
vg.set(10 + x, y, 10 + z, wood)
print(f" Wood voxels: {vg.count_material(wood)}")
print_grid_stats(vg, "After patterns")
# Material breakdown
print("\n Material breakdown:")
print(f" Air: {vg.count_material(0):,} ({100 * vg.count_material(0) / (16*8*16):.1f}%)")
print(f" Stone: {vg.count_material(stone):,}")
print(f" Brick: {vg.count_material(brick):,}")
print(f" Wood: {vg.count_material(wood):,}")
def demo_bulk_operations():
"""Demonstrate bulk fill and clear operations"""
print_header("4. Bulk Operations")
vg = mcrfpy.VoxelGrid(size=(32, 8, 32))
total = 32 * 8 * 32
stone = vg.add_material("stone", color=Color(128, 128, 128))
print(f"\n Grid: 32x8x32 = {total:,} voxels")
# Fill
vg.fill(stone)
print(f" After fill(stone): {vg.count_non_air():,} non-air")
# Clear
vg.clear()
print(f" After clear(): {vg.count_non_air():,} non-air")
def demo_transforms():
"""Demonstrate transform properties"""
print_header("5. Transform Properties")
vg = mcrfpy.VoxelGrid(size=(8, 8, 8))
print(f"\n Default state:")
print(f" Offset: {vg.offset}")
print(f" Rotation: {vg.rotation} deg")
# Position for a building
vg.offset = (100.0, 0.0, 50.0)
vg.rotation = 45.0
print(f"\n After positioning:")
print(f" Offset: {vg.offset}")
print(f" Rotation: {vg.rotation} deg")
# Multiple buildings with different transforms
print("\n Example: Village layout with 3 buildings")
buildings = []
positions = [(0, 0, 0), (20, 0, 0), (10, 0, 15)]
rotations = [0, 90, 45]
for i, (pos, rot) in enumerate(zip(positions, rotations)):
b = mcrfpy.VoxelGrid(size=(8, 6, 8))
b.offset = pos
b.rotation = rot
buildings.append(b)
print(f" Building {i+1}: offset={pos}, rotation={rot} deg")
def demo_edge_cases():
"""Test edge cases and limits"""
print_header("6. Edge Cases and Limits")
# Maximum practical size
print("\n Testing large grid (64x64x64)...")
large = mcrfpy.VoxelGrid(size=(64, 64, 64))
mat = large.add_material("test", color=Color(128, 128, 128))
large.fill(mat)
print(f" Created and filled: {large.count_non_air():,} voxels")
large.clear()
print(f" Cleared: {large.count_non_air()} voxels")
# Bounds checking
print("\n Bounds checking (should not crash):")
small = mcrfpy.VoxelGrid(size=(4, 4, 4))
test_mat = small.add_material("test", color=Color(255, 0, 0))
small.set(-1, 0, 0, test_mat)
small.set(100, 0, 0, test_mat)
print(f" Out-of-bounds get(-1,0,0): {small.get(-1, 0, 0)} (expected 0)")
print(f" Out-of-bounds get(100,0,0): {small.get(100, 0, 0)} (expected 0)")
# Material palette capacity
print("\n Material palette capacity test:")
full_vg = mcrfpy.VoxelGrid(size=(4, 4, 4))
for i in range(255):
full_vg.add_material(f"mat_{i}", color=Color(i, i, i))
print(f" Added 255 materials: count = {full_vg.material_count}")
try:
full_vg.add_material("overflow", color=Color(255, 255, 255))
print(" ERROR: Should have raised exception!")
except RuntimeError as e:
print(f" 256th material correctly rejected: {e}")
def demo_memory_usage():
"""Show memory usage for various grid sizes"""
print_header("7. Memory Usage Estimates")
sizes = [
(8, 8, 8),
(16, 8, 16),
(32, 16, 32),
(64, 32, 64),
(80, 16, 45), # Example dungeon size
]
print("\n Size Voxels Memory")
print(" " + "-" * 40)
for w, h, d in sizes:
voxels = w * h * d
memory = voxels # 1 byte per voxel
print(f" {w:3}x{h:3}x{d:3} {voxels:>10,} {format_bytes(memory):>10}")
def main():
"""Run all demos"""
print("\n" + "=" * 60)
print(" VOXELGRID CORE DEMO (Milestone 9)")
print(" Dense 3D Voxel Array with Material Palette")
print("=" * 60)
demo_basic_creation()
demo_material_palette()
demo_voxel_placement()
demo_bulk_operations()
demo_transforms()
demo_edge_cases()
demo_memory_usage()
print_header("Demo Complete!")
print("\n Next milestone (10): Voxel Mesh Generation")
print(" The VoxelGrid data will be converted to renderable 3D meshes.")
print()
if __name__ == "__main__":
import sys
main()
sys.exit(0)

View file

@ -1,273 +0,0 @@
# voxel_dungeon_demo.py - Procedural dungeon demonstrating bulk voxel operations
# Milestone 11: Bulk Operations and Building Primitives
import mcrfpy
import sys
import math
import random
# Create demo scene
scene = mcrfpy.Scene("voxel_dungeon_demo")
# Dark background
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(20, 20, 30))
scene.children.append(bg)
# Title
title = mcrfpy.Caption(text="Voxel Dungeon Demo - Bulk Operations (Milestone 11)", pos=(20, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
scene.children.append(title)
# Create the 3D viewport
viewport = mcrfpy.Viewport3D(
pos=(50, 60),
size=(620, 520),
render_resolution=(400, 320),
fov=60.0,
camera_pos=(40.0, 30.0, 40.0),
camera_target=(16.0, 4.0, 16.0),
bg_color=mcrfpy.Color(30, 30, 40) # Dark atmosphere
)
scene.children.append(viewport)
# Global voxel grid reference
voxels = None
seed = 42
def generate_dungeon(dungeon_seed=42):
"""Generate a procedural dungeon showcasing all bulk operations"""
global voxels, seed
seed = dungeon_seed
random.seed(seed)
# Create voxel grid for dungeon
print(f"Generating dungeon (seed={seed})...")
voxels = mcrfpy.VoxelGrid(size=(32, 12, 32), cell_size=1.0)
# Define materials
STONE_WALL = voxels.add_material("stone_wall", color=mcrfpy.Color(80, 80, 90))
STONE_FLOOR = voxels.add_material("stone_floor", color=mcrfpy.Color(100, 95, 90))
MOSS = voxels.add_material("moss", color=mcrfpy.Color(40, 80, 40))
WATER = voxels.add_material("water", color=mcrfpy.Color(40, 80, 160, 180), transparent=True)
PILLAR = voxels.add_material("pillar", color=mcrfpy.Color(120, 110, 100))
GOLD = voxels.add_material("gold", color=mcrfpy.Color(255, 215, 0))
print(f"Defined {voxels.material_count} materials")
# 1. Main room using fill_box_hollow
print("Building main room with fill_box_hollow...")
voxels.fill_box_hollow((2, 0, 2), (29, 10, 29), STONE_WALL, thickness=1)
# 2. Floor with slight variation using fill_box
voxels.fill_box((3, 0, 3), (28, 0, 28), STONE_FLOOR)
# 3. Spherical alcoves carved into walls using fill_sphere
print("Carving alcoves with fill_sphere...")
alcove_positions = [
(2, 5, 16), # West wall
(29, 5, 16), # East wall
(16, 5, 2), # North wall
(16, 5, 29), # South wall
]
for pos in alcove_positions:
voxels.fill_sphere(pos, 3, 0) # Carve out (air)
# 4. Small decorative spheres (gold orbs in alcoves)
print("Adding gold orbs in alcoves...")
for i, pos in enumerate(alcove_positions):
# Offset inward so orb is visible
ox, oy, oz = pos
if ox < 10:
ox += 2
elif ox > 20:
ox -= 2
if oz < 10:
oz += 2
elif oz > 20:
oz -= 2
voxels.fill_sphere((ox, oy - 1, oz), 1, GOLD)
# 5. Support pillars using fill_cylinder
print("Building pillars with fill_cylinder...")
pillar_positions = [
(8, 1, 8), (8, 1, 24),
(24, 1, 8), (24, 1, 24),
(16, 1, 8), (16, 1, 24),
(8, 1, 16), (24, 1, 16),
]
for px, py, pz in pillar_positions:
voxels.fill_cylinder((px, py, pz), 1, 9, PILLAR)
# 6. Moss patches using fill_noise
print("Adding moss patches with fill_noise...")
voxels.fill_noise((3, 1, 3), (28, 1, 28), MOSS, threshold=0.65, scale=0.15, seed=seed)
# 7. Central water pool
print("Creating water pool...")
voxels.fill_box((12, 0, 12), (20, 0, 20), 0) # Carve depression
voxels.fill_box((12, 0, 12), (20, 0, 20), WATER)
# 8. Copy a pillar as prefab and paste variations
print("Creating prefab from pillar and pasting copies...")
pillar_prefab = voxels.copy_region((8, 1, 8), (9, 9, 9))
print(f" Pillar prefab: {pillar_prefab.size}")
# Paste smaller pillars at corners (offset from main room)
corner_positions = [(4, 1, 4), (4, 1, 27), (27, 1, 4), (27, 1, 27)]
for cx, cy, cz in corner_positions:
voxels.paste_region(pillar_prefab, (cx, cy, cz), skip_air=True)
# Build mesh
voxels.rebuild_mesh()
print(f"\nDungeon generated:")
print(f" Non-air voxels: {voxels.count_non_air()}")
print(f" Vertices: {voxels.vertex_count}")
print(f" Faces: {voxels.vertex_count // 6}")
# Add to viewport
# First remove old layer if exists
if viewport.voxel_layer_count() > 0:
pass # Can't easily remove, so we regenerate the whole viewport
viewport.add_voxel_layer(voxels, z_index=0)
return voxels
# Generate initial dungeon
generate_dungeon(42)
# Create info panel
info_frame = mcrfpy.Frame(pos=(690, 60), size=(300, 280), fill_color=mcrfpy.Color(40, 40, 60, 220))
scene.children.append(info_frame)
info_title = mcrfpy.Caption(text="Dungeon Stats", pos=(700, 70))
info_title.fill_color = mcrfpy.Color(255, 255, 100)
scene.children.append(info_title)
def update_stats():
global stats_caption
stats_text = f"""Grid: {voxels.width}x{voxels.height}x{voxels.depth}
Total cells: {voxels.width * voxels.height * voxels.depth}
Non-air: {voxels.count_non_air()}
Materials: {voxels.material_count}
Mesh Stats:
Vertices: {voxels.vertex_count}
Faces: {voxels.vertex_count // 6}
Seed: {seed}
Operations Used:
- fill_box_hollow (walls)
- fill_sphere (alcoves)
- fill_cylinder (pillars)
- fill_noise (moss)
- copy/paste (prefabs)"""
stats_caption.text = stats_text
stats_caption = mcrfpy.Caption(text="", pos=(700, 100))
stats_caption.fill_color = mcrfpy.Color(200, 200, 200)
scene.children.append(stats_caption)
update_stats()
# Controls panel
controls_frame = mcrfpy.Frame(pos=(690, 360), size=(300, 180), fill_color=mcrfpy.Color(40, 40, 60, 220))
scene.children.append(controls_frame)
controls_title = mcrfpy.Caption(text="Controls", pos=(700, 370))
controls_title.fill_color = mcrfpy.Color(255, 255, 100)
scene.children.append(controls_title)
controls_text = """R - Regenerate dungeon (new seed)
1-4 - Camera presets
+/- - Zoom in/out
SPACE - Reset camera
ESC - Exit demo"""
controls = mcrfpy.Caption(text=controls_text, pos=(700, 400))
controls.fill_color = mcrfpy.Color(200, 200, 200)
scene.children.append(controls)
# Camera animation state
rotation_enabled = False
camera_distance = 50.0
camera_angle = 45.0 # degrees
camera_height = 30.0
camera_presets = [
(40.0, 30.0, 40.0, 16.0, 4.0, 16.0), # Default diagonal
(16.0, 30.0, 50.0, 16.0, 4.0, 16.0), # Front view
(50.0, 30.0, 16.0, 16.0, 4.0, 16.0), # Side view
(16.0, 50.0, 16.0, 16.0, 4.0, 16.0), # Top-down
]
def rotate_camera(timer_name, runtime):
"""Timer callback for camera rotation"""
global camera_angle, rotation_enabled
if rotation_enabled:
camera_angle += 0.5
if camera_angle >= 360.0:
camera_angle = 0.0
rad = camera_angle * math.pi / 180.0
x = 16.0 + camera_distance * math.cos(rad)
z = 16.0 + camera_distance * math.sin(rad)
viewport.camera_pos = (x, camera_height, z)
# Set up rotation timer
timer = mcrfpy.Timer("rotate_cam", rotate_camera, 33)
def handle_key(key, action):
"""Keyboard handler"""
global rotation_enabled, seed, camera_distance, camera_height
if action != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.R:
seed = random.randint(1, 99999)
generate_dungeon(seed)
update_stats()
print(f"Regenerated dungeon with seed {seed}")
elif key == mcrfpy.Key.NUM_1:
viewport.camera_pos = camera_presets[0][:3]
viewport.camera_target = camera_presets[0][3:]
rotation_enabled = False
elif key == mcrfpy.Key.NUM_2:
viewport.camera_pos = camera_presets[1][:3]
viewport.camera_target = camera_presets[1][3:]
rotation_enabled = False
elif key == mcrfpy.Key.NUM_3:
viewport.camera_pos = camera_presets[2][:3]
viewport.camera_target = camera_presets[2][3:]
rotation_enabled = False
elif key == mcrfpy.Key.NUM_4:
viewport.camera_pos = camera_presets[3][:3]
viewport.camera_target = camera_presets[3][3:]
rotation_enabled = False
elif key == mcrfpy.Key.SPACE:
rotation_enabled = not rotation_enabled
print(f"Camera rotation: {'ON' if rotation_enabled else 'OFF'}")
elif key == mcrfpy.Key.EQUALS or key == mcrfpy.Key.ADD:
camera_distance = max(20.0, camera_distance - 5.0)
camera_height = max(15.0, camera_height - 2.0)
elif key == mcrfpy.Key.DASH or key == mcrfpy.Key.SUBTRACT:
camera_distance = min(80.0, camera_distance + 5.0)
camera_height = min(50.0, camera_height + 2.0)
elif key == mcrfpy.Key.ESCAPE:
print("Exiting demo...")
sys.exit(0)
scene.on_key = handle_key
# Activate the scene
mcrfpy.current_scene = scene
print("\nVoxel Dungeon Demo ready!")
print("Press SPACE to toggle camera rotation, R to regenerate")
# Main entry point for --exec mode
if __name__ == "__main__":
print("\n=== Voxel Dungeon Demo Summary ===")
print(f"Grid size: {voxels.width}x{voxels.height}x{voxels.depth}")
print(f"Non-air voxels: {voxels.count_non_air()}")
print(f"Generated vertices: {voxels.vertex_count}")
print(f"Rendered faces: {voxels.vertex_count // 6}")
print("===================================\n")

View file

@ -1,218 +0,0 @@
# voxel_meshing_demo.py - Visual demo of VoxelGrid mesh rendering
# Shows voxel building rendered in Viewport3D with PS1 effects
import mcrfpy
import sys
import math
# Create demo scene
scene = mcrfpy.Scene("voxel_meshing_demo")
# Dark background frame
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25))
scene.children.append(bg)
# Title
title = mcrfpy.Caption(text="VoxelGrid Meshing Demo - Face-Culled 3D Voxels", pos=(20, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
scene.children.append(title)
# Create the 3D viewport
viewport = mcrfpy.Viewport3D(
pos=(50, 60),
size=(600, 500),
render_resolution=(320, 240), # PS1 resolution
fov=60.0,
camera_pos=(20.0, 15.0, 20.0),
camera_target=(4.0, 2.0, 4.0),
bg_color=mcrfpy.Color(50, 70, 100) # Sky color
)
scene.children.append(viewport)
# Create voxel grid for building
print("Creating voxel building...")
voxels = mcrfpy.VoxelGrid(size=(12, 8, 12), cell_size=1.0)
# Define materials
STONE = voxels.add_material("stone", color=mcrfpy.Color(128, 128, 128))
BRICK = voxels.add_material("brick", color=mcrfpy.Color(165, 82, 42))
WOOD = voxels.add_material("wood", color=mcrfpy.Color(139, 90, 43))
GLASS = voxels.add_material("glass", color=mcrfpy.Color(180, 220, 255, 180), transparent=True)
GRASS = voxels.add_material("grass", color=mcrfpy.Color(60, 150, 60))
print(f"Defined {voxels.material_count} materials")
# Build a simple house structure
# Ground/foundation
voxels.fill_box((0, 0, 0), (11, 0, 11), GRASS)
# Floor
voxels.fill_box((1, 1, 1), (10, 1, 10), STONE)
# Walls
# Front wall (Z=1)
voxels.fill_box((1, 2, 1), (10, 5, 1), BRICK)
# Back wall (Z=10)
voxels.fill_box((1, 2, 10), (10, 5, 10), BRICK)
# Left wall (X=1)
voxels.fill_box((1, 2, 1), (1, 5, 10), BRICK)
# Right wall (X=10)
voxels.fill_box((10, 2, 1), (10, 5, 10), BRICK)
# Door opening (front wall)
voxels.fill_box((4, 2, 1), (6, 4, 1), 0) # Clear door opening
# Windows
# Front windows (beside door)
voxels.fill_box((2, 3, 1), (3, 4, 1), GLASS)
voxels.fill_box((8, 3, 1), (9, 4, 1), GLASS)
# Side windows
voxels.fill_box((1, 3, 4), (1, 4, 5), GLASS)
voxels.fill_box((1, 3, 7), (1, 4, 8), GLASS)
voxels.fill_box((10, 3, 4), (10, 4, 5), GLASS)
voxels.fill_box((10, 3, 7), (10, 4, 8), GLASS)
# Ceiling
voxels.fill_box((1, 6, 1), (10, 6, 10), WOOD)
# Simple roof (peaked)
voxels.fill_box((0, 7, 0), (11, 7, 11), WOOD)
voxels.fill_box((1, 8, 1), (10, 8, 10), WOOD)
voxels.fill_box((2, 9, 2), (9, 9, 9), WOOD)
voxels.fill_box((3, 10, 3), (8, 10, 8), WOOD)
voxels.fill_box((4, 11, 4), (7, 11, 7), WOOD)
# Build the mesh
voxels.rebuild_mesh()
print(f"Built voxel house:")
print(f" Non-air voxels: {voxels.count_non_air()}")
print(f" Vertices: {voxels.vertex_count}")
print(f" Faces: {voxels.vertex_count // 6}")
# Position the building
voxels.offset = (0.0, 0.0, 0.0)
voxels.rotation = 0.0
# Add to viewport
viewport.add_voxel_layer(voxels, z_index=0)
print(f"Added voxel layer to viewport (count: {viewport.voxel_layer_count()})")
# Create info panel
info_frame = mcrfpy.Frame(pos=(680, 60), size=(300, 250), fill_color=mcrfpy.Color(40, 40, 60, 200))
scene.children.append(info_frame)
info_title = mcrfpy.Caption(text="Building Stats", pos=(690, 70))
info_title.fill_color = mcrfpy.Color(255, 255, 100)
scene.children.append(info_title)
stats_text = f"""Grid: {voxels.width}x{voxels.height}x{voxels.depth}
Total voxels: {voxels.width * voxels.height * voxels.depth}
Non-air: {voxels.count_non_air()}
Materials: {voxels.material_count}
Vertices: {voxels.vertex_count}
Faces: {voxels.vertex_count // 6}
Without culling would be:
{voxels.count_non_air() * 36} vertices
({100 - (voxels.vertex_count / (voxels.count_non_air() * 36) * 100):.0f}% reduction)"""
stats = mcrfpy.Caption(text=stats_text, pos=(690, 100))
stats.fill_color = mcrfpy.Color(200, 200, 200)
scene.children.append(stats)
# Controls info
controls_frame = mcrfpy.Frame(pos=(680, 330), size=(300, 180), fill_color=mcrfpy.Color(40, 40, 60, 200))
scene.children.append(controls_frame)
controls_title = mcrfpy.Caption(text="Controls", pos=(690, 340))
controls_title.fill_color = mcrfpy.Color(255, 255, 100)
scene.children.append(controls_title)
controls_text = """R - Toggle rotation
1-5 - Change camera angle
SPACE - Reset camera
ESC - Exit demo"""
controls = mcrfpy.Caption(text=controls_text, pos=(690, 370))
controls.fill_color = mcrfpy.Color(200, 200, 200)
scene.children.append(controls)
# Animation state
rotation_enabled = False
current_angle = 0.0
camera_angles = [
(20.0, 15.0, 20.0), # Default - diagonal view
(0.0, 15.0, 25.0), # Front view
(25.0, 15.0, 0.0), # Side view
(5.5, 25.0, 5.5), # Top-down view
(5.5, 3.0, 20.0), # Low angle
]
current_camera = 0
def rotate_building(timer, runtime):
"""Timer callback for building rotation"""
global current_angle, rotation_enabled
if rotation_enabled:
current_angle += 1.0
if current_angle >= 360.0:
current_angle = 0.0
voxels.rotation = current_angle
# Set up rotation timer
timer = mcrfpy.Timer("rotate", rotate_building, 33) # ~30 FPS
def handle_key(key, action):
"""Keyboard handler"""
global rotation_enabled, current_camera
if action != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.R:
rotation_enabled = not rotation_enabled
print(f"Rotation: {'ON' if rotation_enabled else 'OFF'}")
elif key == mcrfpy.Key.NUM_1:
current_camera = 0
viewport.camera_pos = camera_angles[0]
print("Camera: Default diagonal")
elif key == mcrfpy.Key.NUM_2:
current_camera = 1
viewport.camera_pos = camera_angles[1]
print("Camera: Front view")
elif key == mcrfpy.Key.NUM_3:
current_camera = 2
viewport.camera_pos = camera_angles[2]
print("Camera: Side view")
elif key == mcrfpy.Key.NUM_4:
current_camera = 3
viewport.camera_pos = camera_angles[3]
print("Camera: Top-down view")
elif key == mcrfpy.Key.NUM_5:
current_camera = 4
viewport.camera_pos = camera_angles[4]
print("Camera: Low angle")
elif key == mcrfpy.Key.SPACE:
current_camera = 0
voxels.rotation = 0.0
viewport.camera_pos = camera_angles[0]
print("Camera: Reset")
elif key == mcrfpy.Key.ESCAPE:
print("Exiting demo...")
sys.exit(0)
scene.on_key = handle_key
# Activate the scene
mcrfpy.current_scene = scene
print("Voxel Meshing Demo ready! Press R to toggle rotation.")
# Main entry point for --exec mode
if __name__ == "__main__":
# Demo is set up, print summary
print("\n=== Voxel Meshing Demo Summary ===")
print(f"Grid size: {voxels.width}x{voxels.height}x{voxels.depth}")
print(f"Non-air voxels: {voxels.count_non_air()}")
print(f"Generated vertices: {voxels.vertex_count}")
print(f"Rendered faces: {voxels.vertex_count // 6}")
print("===================================\n")

View file

@ -1,250 +0,0 @@
#!/usr/bin/env python3
"""Visual Demo: Milestone 12 - VoxelGrid Navigation Projection
Demonstrates projection of 3D voxel terrain to 2D navigation grid for pathfinding.
Shows:
1. Voxel dungeon with multiple levels
2. Navigation grid projection (walkable/unwalkable areas)
3. A* pathfinding through the projected terrain
4. FOV computation from voxel transparency
"""
import mcrfpy
import sys
import math
def create_demo_scene():
"""Create the navigation projection demo scene"""
scene = mcrfpy.Scene("voxel_nav_demo")
# =========================================================================
# Create a small dungeon-style voxel grid
# =========================================================================
vg = mcrfpy.VoxelGrid((16, 8, 16), cell_size=1.0)
# Add materials
floor_mat = vg.add_material("floor", (100, 80, 60)) # Brown floor
wall_mat = vg.add_material("wall", (80, 80, 90), transparent=False) # Gray walls
pillar_mat = vg.add_material("pillar", (60, 60, 70), transparent=False) # Dark pillars
glass_mat = vg.add_material("glass", (150, 200, 255), transparent=True) # Transparent glass
water_mat = vg.add_material("water", (50, 100, 200), transparent=True, path_cost=3.0) # Slow water
# Create floor
vg.fill_box((0, 0, 0), (15, 0, 15), floor_mat)
# Create outer walls
vg.fill_box((0, 1, 0), (15, 4, 0), wall_mat) # North wall
vg.fill_box((0, 1, 15), (15, 4, 15), wall_mat) # South wall
vg.fill_box((0, 1, 0), (0, 4, 15), wall_mat) # West wall
vg.fill_box((15, 1, 0), (15, 4, 15), wall_mat) # East wall
# Interior walls creating rooms
vg.fill_box((5, 1, 0), (5, 4, 10), wall_mat) # Vertical wall
vg.fill_box((10, 1, 5), (15, 4, 5), wall_mat) # Horizontal wall
# Doorways (carve holes)
vg.fill_box((5, 1, 3), (5, 2, 4), 0) # Door in vertical wall
vg.fill_box((12, 1, 5), (13, 2, 5), 0) # Door in horizontal wall
# Central pillars
vg.fill_box((8, 1, 8), (8, 4, 8), pillar_mat)
vg.fill_box((8, 1, 12), (8, 4, 12), pillar_mat)
# Water pool in one corner (slow movement)
vg.fill_box((1, 0, 11), (3, 0, 14), water_mat)
# Glass window
vg.fill_box((10, 2, 5), (11, 3, 5), glass_mat)
# Raised platform in one area (height variation)
vg.fill_box((12, 1, 8), (14, 1, 13), floor_mat) # Platform at y=1
# =========================================================================
# Create Viewport3D with navigation grid
# =========================================================================
viewport = mcrfpy.Viewport3D(pos=(10, 10), size=(600, 400))
viewport.set_grid_size(16, 16)
viewport.cell_size = 1.0
# Configure camera for top-down view
viewport.camera_pos = (8, 15, 20)
viewport.camera_target = (8, 0, 8)
# Add voxel layer
viewport.add_voxel_layer(vg, z_index=0)
# Project voxels to navigation grid with headroom=2 (entity needs 2 voxels height)
viewport.project_voxel_to_nav(vg, headroom=2)
# =========================================================================
# Info panel
# =========================================================================
info_frame = mcrfpy.Frame(pos=(620, 10), size=(250, 400))
info_frame.fill_color = mcrfpy.Color(30, 30, 40, 220)
info_frame.outline_color = mcrfpy.Color(100, 100, 120)
info_frame.outline = 2.0
title = mcrfpy.Caption(text="Nav Projection Demo", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 100)
desc = mcrfpy.Caption(text="Voxels projected to\n2D nav grid", pos=(10, 35))
desc.fill_color = mcrfpy.Color(200, 200, 200)
info1 = mcrfpy.Caption(text="Grid: 16x16 cells", pos=(10, 75))
info1.fill_color = mcrfpy.Color(150, 200, 255)
info2 = mcrfpy.Caption(text="Headroom: 2 voxels", pos=(10, 95))
info2.fill_color = mcrfpy.Color(150, 200, 255)
# Count walkable cells
walkable_count = 0
for x in range(16):
for z in range(16):
cell = viewport.at(x, z)
if cell.walkable:
walkable_count += 1
info3 = mcrfpy.Caption(text=f"Walkable: {walkable_count}/256", pos=(10, 115))
info3.fill_color = mcrfpy.Color(100, 255, 100)
# Find path example
path = viewport.find_path((1, 1), (13, 13))
info4 = mcrfpy.Caption(text=f"Path length: {len(path)}", pos=(10, 135))
info4.fill_color = mcrfpy.Color(255, 200, 100)
# FOV example
fov = viewport.compute_fov((8, 8), 10)
info5 = mcrfpy.Caption(text=f"FOV cells: {len(fov)}", pos=(10, 155))
info5.fill_color = mcrfpy.Color(200, 150, 255)
# Legend
legend_title = mcrfpy.Caption(text="Materials:", pos=(10, 185))
legend_title.fill_color = mcrfpy.Color(255, 255, 255)
leg1 = mcrfpy.Caption(text=" Floor (walkable)", pos=(10, 205))
leg1.fill_color = mcrfpy.Color(100, 80, 60)
leg2 = mcrfpy.Caption(text=" Wall (blocking)", pos=(10, 225))
leg2.fill_color = mcrfpy.Color(80, 80, 90)
leg3 = mcrfpy.Caption(text=" Water (slow)", pos=(10, 245))
leg3.fill_color = mcrfpy.Color(50, 100, 200)
leg4 = mcrfpy.Caption(text=" Glass (see-through)", pos=(10, 265))
leg4.fill_color = mcrfpy.Color(150, 200, 255)
controls = mcrfpy.Caption(text="[Space] Recompute FOV\n[P] Show path\n[Q] Quit", pos=(10, 300))
controls.fill_color = mcrfpy.Color(150, 150, 150)
info_frame.children.extend([
title, desc, info1, info2, info3, info4, info5,
legend_title, leg1, leg2, leg3, leg4, controls
])
# =========================================================================
# Status bar
# =========================================================================
status_frame = mcrfpy.Frame(pos=(10, 420), size=(860, 50))
status_frame.fill_color = mcrfpy.Color(20, 20, 30, 220)
status_frame.outline_color = mcrfpy.Color(80, 80, 100)
status_frame.outline = 1.0
status_text = mcrfpy.Caption(
text="Milestone 12: VoxelGrid Navigation Projection - Project 3D voxels to 2D pathfinding grid",
pos=(10, 15)
)
status_text.fill_color = mcrfpy.Color(180, 180, 200)
status_frame.children.append(status_text)
# =========================================================================
# Add elements to scene
# =========================================================================
scene.children.extend([viewport, info_frame, status_frame])
# Store references for interaction (using module-level globals)
global demo_viewport, demo_voxelgrid, demo_path, demo_fov_origin
demo_viewport = viewport
demo_voxelgrid = vg
demo_path = path
demo_fov_origin = (8, 8)
# =========================================================================
# Keyboard handler
# =========================================================================
def on_key(key, state):
global demo_fov_origin
if state != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.Q or key == mcrfpy.Key.ESCAPE:
# Exit
sys.exit(0)
elif key == mcrfpy.Key.SPACE:
# Recompute FOV from different origin
ox, oz = demo_fov_origin
ox = (ox + 3) % 14 + 1
oz = (oz + 5) % 14 + 1
demo_fov_origin = (ox, oz)
fov = demo_viewport.compute_fov((ox, oz), 8)
info5.text = f"FOV from ({ox},{oz}): {len(fov)}"
elif key == mcrfpy.Key.P:
# Show path info
print(f"Path from (1,1) to (13,13): {len(demo_path)} steps")
for i, (px, pz) in enumerate(demo_path[:10]):
cell = demo_viewport.at(px, pz)
print(f" Step {i}: ({px},{pz}) h={cell.height:.1f} cost={cell.cost:.1f}")
if len(demo_path) > 10:
print(f" ... and {len(demo_path) - 10} more steps")
scene.on_key = on_key
return scene
def main():
"""Main entry point"""
print("=== Milestone 12: VoxelGrid Navigation Projection Demo ===")
print()
print("This demo shows how 3D voxel terrain is projected to a 2D")
print("navigation grid for pathfinding and FOV calculations.")
print()
print("The projection scans each column from top to bottom, finding")
print("the topmost walkable floor with adequate headroom.")
print()
scene = create_demo_scene()
mcrfpy.current_scene = scene
# Print nav grid summary
grid_w, grid_d = demo_viewport.grid_size
print("Navigation grid summary:")
print(f" Grid size: {grid_w}x{grid_d}")
# Count by walkability and transparency
walkable = 0
blocking = 0
transparent = 0
for x in range(grid_w):
for z in range(grid_d):
cell = demo_viewport.at(x, z)
if cell.walkable:
walkable += 1
else:
blocking += 1
if cell.transparent:
transparent += 1
print(f" Walkable cells: {walkable}")
print(f" Blocking cells: {blocking}")
print(f" Transparent cells: {transparent}")
print()
if __name__ == "__main__":
main()
sys.exit(0)

View file

@ -1,314 +0,0 @@
"""Voxel Serialization Demo - Milestone 14
Demonstrates save/load functionality for VoxelGrid, including:
- Saving to file with .mcvg format
- Loading from file
- Serialization to bytes (for network/custom storage)
- RLE compression effectiveness
"""
import mcrfpy
import os
import tempfile
def create_demo_scene():
"""Create a scene demonstrating voxel serialization."""
scene = mcrfpy.Scene("voxel_serialization_demo")
ui = scene.children
# Dark background
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=(20, 20, 30))
ui.append(bg)
# Title
title = mcrfpy.Caption(text="Milestone 14: VoxelGrid Serialization",
pos=(30, 20))
title.font_size = 28
title.fill_color = (255, 220, 100)
ui.append(title)
# Create demo VoxelGrid with interesting structure
grid = mcrfpy.VoxelGrid((16, 16, 16), cell_size=1.0)
# Add materials
stone = grid.add_material("stone", (100, 100, 110))
wood = grid.add_material("wood", (139, 90, 43))
glass = grid.add_material("glass", (180, 200, 220, 100), transparent=True)
gold = grid.add_material("gold", (255, 215, 0))
# Build a small structure
grid.fill_box((0, 0, 0), (15, 0, 15), stone) # Floor
grid.fill_box((0, 1, 0), (0, 4, 15), stone) # Wall 1
grid.fill_box((15, 1, 0), (15, 4, 15), stone) # Wall 2
grid.fill_box((0, 1, 0), (15, 4, 0), stone) # Wall 3
grid.fill_box((0, 1, 15), (15, 4, 15), stone) # Wall 4
# Windows (clear some wall, add glass)
grid.fill_box((6, 2, 0), (10, 3, 0), 0) # Clear for window
grid.fill_box((6, 2, 0), (10, 3, 0), glass) # Add glass
# Pillars
grid.fill_box((4, 1, 4), (4, 3, 4), wood)
grid.fill_box((12, 1, 4), (12, 3, 4), wood)
grid.fill_box((4, 1, 12), (4, 3, 12), wood)
grid.fill_box((12, 1, 12), (12, 3, 12), wood)
# Gold decorations
grid.set(8, 1, 8, gold)
grid.set(7, 1, 8, gold)
grid.set(9, 1, 8, gold)
grid.set(8, 1, 7, gold)
grid.set(8, 1, 9, gold)
# Get original stats
original_voxels = grid.count_non_air()
original_materials = grid.material_count
# === Test save/load to file ===
with tempfile.NamedTemporaryFile(suffix='.mcvg', delete=False) as f:
temp_path = f.name
save_success = grid.save(temp_path)
file_size = os.path.getsize(temp_path) if save_success else 0
# Load into new grid
loaded_grid = mcrfpy.VoxelGrid((1, 1, 1))
load_success = loaded_grid.load(temp_path)
os.unlink(temp_path) # Clean up
loaded_voxels = loaded_grid.count_non_air() if load_success else 0
loaded_materials = loaded_grid.material_count if load_success else 0
# === Test to_bytes/from_bytes ===
data_bytes = grid.to_bytes()
bytes_size = len(data_bytes)
bytes_grid = mcrfpy.VoxelGrid((1, 1, 1))
bytes_success = bytes_grid.from_bytes(data_bytes)
bytes_voxels = bytes_grid.count_non_air() if bytes_success else 0
# === Calculate compression ===
raw_size = 16 * 16 * 16 # Uncompressed voxel data
compression_ratio = raw_size / bytes_size if bytes_size > 0 else 0
# Display information
y_pos = 80
# Original Grid Info
info1 = mcrfpy.Caption(text="Original VoxelGrid:",
pos=(30, y_pos))
info1.font_size = 20
info1.fill_color = (100, 200, 255)
ui.append(info1)
y_pos += 30
for line in [
f" Dimensions: 16x16x16 = 4096 voxels",
f" Non-air voxels: {original_voxels}",
f" Materials defined: {original_materials}",
f" Structure: Walled room with pillars, windows, gold decor"
]:
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = (200, 200, 210)
ui.append(cap)
y_pos += 22
y_pos += 20
# File Save/Load Results
info2 = mcrfpy.Caption(text="File Serialization (.mcvg):",
pos=(30, y_pos))
info2.font_size = 20
info2.fill_color = (100, 255, 150)
ui.append(info2)
y_pos += 30
save_status = "SUCCESS" if save_success else "FAILED"
load_status = "SUCCESS" if load_success else "FAILED"
match_status = "MATCH" if loaded_voxels == original_voxels else "MISMATCH"
for line in [
f" Save to file: {save_status}",
f" File size: {file_size} bytes",
f" Load from file: {load_status}",
f" Loaded voxels: {loaded_voxels} ({match_status})",
f" Loaded materials: {loaded_materials}"
]:
color = (150, 255, 150) if "SUCCESS" in line or "MATCH" in line else (200, 200, 210)
if "FAILED" in line or "MISMATCH" in line:
color = (255, 100, 100)
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = color
ui.append(cap)
y_pos += 22
y_pos += 20
# Bytes Serialization Results
info3 = mcrfpy.Caption(text="Memory Serialization (to_bytes/from_bytes):",
pos=(30, y_pos))
info3.font_size = 20
info3.fill_color = (255, 200, 100)
ui.append(info3)
y_pos += 30
bytes_status = "SUCCESS" if bytes_success else "FAILED"
bytes_match = "MATCH" if bytes_voxels == original_voxels else "MISMATCH"
for line in [
f" Serialized size: {bytes_size} bytes",
f" Raw voxel data: {raw_size} bytes",
f" Compression ratio: {compression_ratio:.1f}x",
f" from_bytes(): {bytes_status}",
f" Restored voxels: {bytes_voxels} ({bytes_match})"
]:
color = (200, 200, 210)
if "SUCCESS" in line or "MATCH" in line:
color = (150, 255, 150)
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = color
ui.append(cap)
y_pos += 22
y_pos += 20
# RLE Compression Demo
info4 = mcrfpy.Caption(text="RLE Compression Effectiveness:",
pos=(30, y_pos))
info4.font_size = 20
info4.fill_color = (200, 150, 255)
ui.append(info4)
y_pos += 30
# Create uniform grid for compression test
uniform_grid = mcrfpy.VoxelGrid((32, 32, 32))
uniform_mat = uniform_grid.add_material("solid", (128, 128, 128))
uniform_grid.fill(uniform_mat)
uniform_bytes = uniform_grid.to_bytes()
uniform_raw = 32 * 32 * 32
uniform_ratio = uniform_raw / len(uniform_bytes)
for line in [
f" Uniform 32x32x32 filled grid:",
f" Raw: {uniform_raw} bytes",
f" Compressed: {len(uniform_bytes)} bytes",
f" Compression: {uniform_ratio:.0f}x",
f" ",
f" RLE excels at runs of identical values."
]:
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = (200, 180, 220)
ui.append(cap)
y_pos += 22
y_pos += 30
# File Format Info
info5 = mcrfpy.Caption(text="File Format (.mcvg):",
pos=(30, y_pos))
info5.font_size = 20
info5.fill_color = (255, 150, 200)
ui.append(info5)
y_pos += 30
for line in [
" Header: Magic 'MCVG' + version + dimensions + cell_size",
" Materials: name, color (RGBA), sprite_index, transparent, path_cost",
" Voxel data: RLE-encoded material IDs",
" ",
" Note: Transform (offset, rotation) is runtime state, not serialized"
]:
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 14
cap.fill_color = (200, 180, 200)
ui.append(cap)
y_pos += 20
# API Reference on right side
y_ref = 80
x_ref = 550
api_title = mcrfpy.Caption(text="Python API:", pos=(x_ref, y_ref))
api_title.font_size = 20
api_title.fill_color = (150, 200, 255)
ui.append(api_title)
y_ref += 35
for line in [
"# Save to file",
"success = grid.save('world.mcvg')",
"",
"# Load from file",
"grid = VoxelGrid((1,1,1))",
"success = grid.load('world.mcvg')",
"",
"# Save to bytes",
"data = grid.to_bytes()",
"",
"# Load from bytes",
"success = grid.from_bytes(data)",
"",
"# Network example:",
"# send_to_server(grid.to_bytes())",
"# data = recv_from_server()",
"# grid.from_bytes(data)"
]:
cap = mcrfpy.Caption(text=line, pos=(x_ref, y_ref))
cap.font_size = 14
if line.startswith("#"):
cap.fill_color = (100, 150, 100)
elif "=" in line or "(" in line:
cap.fill_color = (255, 220, 150)
else:
cap.fill_color = (180, 180, 180)
ui.append(cap)
y_ref += 18
return scene
# Run demonstration
if __name__ == "__main__":
import sys
# Create and activate the scene
scene = create_demo_scene()
mcrfpy.current_scene = scene
# When run directly, print summary and exit for headless testing
print("\n=== Voxel Serialization Demo (Milestone 14) ===\n")
# Run a quick verification
grid = mcrfpy.VoxelGrid((8, 8, 8))
mat = grid.add_material("test", (100, 100, 100))
grid.fill_box((0, 0, 0), (7, 0, 7), mat)
print(f"Created 8x8x8 grid with {grid.count_non_air()} non-air voxels")
# Test to_bytes
data = grid.to_bytes()
print(f"Serialized to {len(data)} bytes")
# Test from_bytes
grid2 = mcrfpy.VoxelGrid((1, 1, 1))
success = grid2.from_bytes(data)
print(f"from_bytes(): {'SUCCESS' if success else 'FAILED'}")
print(f"Restored size: {grid2.size}")
print(f"Restored voxels: {grid2.count_non_air()}")
# Compression test
big_grid = mcrfpy.VoxelGrid((32, 32, 32))
big_mat = big_grid.add_material("solid", (128, 128, 128))
big_grid.fill(big_mat)
big_data = big_grid.to_bytes()
raw_size = 32 * 32 * 32
print(f"\nCompression test (32x32x32 uniform):")
print(f" Raw: {raw_size} bytes")
print(f" Compressed: {len(big_data)} bytes")
print(f" Ratio: {raw_size / len(big_data):.0f}x")
print("\n=== Demo complete ===")
sys.exit(0)