Compare commits
No commits in common. "992ea781cb2c9f11f087828da3aca6173dc1d4bd" and "8636e766f80cf452ad8d126c9f166fe311850b81" have entirely different histories.
992ea781cb
...
8636e766f8
36 changed files with 74 additions and 17974 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
110
src/3d/Math3D.h
110
src/3d/Math3D.h
|
|
@ -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
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1451
src/3d/Model3D.cpp
1451
src/3d/Model3D.cpp
File diff suppressed because it is too large
Load diff
431
src/3d/Model3D.h
431
src/3d/Model3D.h
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
7240
src/3d/cgltf.h
7240
src/3d/cgltf.h
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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*);
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue