billboards
This commit is contained in:
parent
544c44ca31
commit
b85f225789
10 changed files with 1750 additions and 46 deletions
596
src/3d/Billboard.cpp
Normal file
596
src/3d/Billboard.cpp
Normal file
|
|
@ -0,0 +1,596 @@
|
|||
// 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) {
|
||||
sf::Texture::bind(sfTexture);
|
||||
|
||||
// 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) {
|
||||
sf::Texture::bind(nullptr);
|
||||
}
|
||||
|
||||
// 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
|
||||
229
src/3d/Billboard.h
Normal file
229
src/3d/Billboard.h
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
// 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
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
// 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)
|
||||
|
|
@ -377,53 +380,57 @@ void MeshLayer::uploadToGPU() {
|
|||
// Rendering
|
||||
// =============================================================================
|
||||
|
||||
void MeshLayer::render(const mat4& model, const mat4& view, const mat4& projection) {
|
||||
void MeshLayer::render(unsigned int shader, const mat4& model, const mat4& view, const mat4& projection) {
|
||||
#ifdef MCRF_HAS_GL
|
||||
if (!gl::isGLReady() || vertices_.empty()) {
|
||||
if (!gl::isGLReady()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload to GPU if needed
|
||||
if (dirty_ || vbo_ == 0) {
|
||||
uploadToGPU();
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// Render mesh instances
|
||||
renderMeshInstances(shader, view, projection);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -446,6 +453,114 @@ 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,6 +10,12 @@
|
|||
#include <vector>
|
||||
#include <libtcod.h> // For TCOD_heightmap_t
|
||||
|
||||
// Forward declarations
|
||||
namespace mcrf {
|
||||
class Viewport3D;
|
||||
class Model3D;
|
||||
}
|
||||
|
||||
namespace mcrf {
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -49,6 +55,25 @@ 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
|
||||
// =============================================================================
|
||||
|
|
@ -115,6 +140,60 @@ 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
|
||||
// =========================================================================
|
||||
|
|
@ -124,10 +203,11 @@ 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(const mat4& model, const mat4& view, const mat4& projection);
|
||||
void render(unsigned int shader, const mat4& model, const mat4& view, const mat4& projection);
|
||||
|
||||
/// Get model matrix (identity by default, override for positioned layers)
|
||||
mat4 getModelMatrix() const { return modelMatrix_; }
|
||||
|
|
@ -164,10 +244,19 @@ 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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
#include "MeshLayer.h"
|
||||
#include "Entity3D.h"
|
||||
#include "EntityCollection3D.h"
|
||||
#include "Billboard.h"
|
||||
#include "Model3D.h"
|
||||
#include "../platform/GLContext.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyColor.h"
|
||||
|
|
@ -42,6 +44,7 @@ namespace mcrf {
|
|||
Viewport3D::Viewport3D()
|
||||
: size_(320.0f, 240.0f)
|
||||
, entities_(std::make_shared<std::list<std::shared_ptr<Entity3D>>>())
|
||||
, billboards_(std::make_shared<std::vector<std::shared_ptr<Billboard>>>())
|
||||
{
|
||||
position = sf::Vector2f(0, 0);
|
||||
camera_.setAspect(size_.x / size_.y);
|
||||
|
|
@ -50,6 +53,7 @@ Viewport3D::Viewport3D()
|
|||
Viewport3D::Viewport3D(float x, float y, float width, float height)
|
||||
: size_(width, height)
|
||||
, entities_(std::make_shared<std::list<std::shared_ptr<Entity3D>>>())
|
||||
, billboards_(std::make_shared<std::vector<std::shared_ptr<Billboard>>>())
|
||||
{
|
||||
position = sf::Vector2f(x, y);
|
||||
camera_.setAspect(size_.x / size_.y);
|
||||
|
|
@ -195,6 +199,7 @@ std::shared_ptr<MeshLayer> Viewport3D::addLayer(const std::string& name, int zIn
|
|||
|
||||
// Create new layer
|
||||
auto layer = std::make_shared<MeshLayer>(name, zIndex);
|
||||
layer->setViewport(this); // Allow layer to mark cells as blocking
|
||||
meshLayers_.push_back(layer);
|
||||
|
||||
// Disable test cube when layers are added
|
||||
|
|
@ -462,6 +467,60 @@ void Viewport3D::renderEntities(const mat4& view, const mat4& proj) {
|
|||
#endif
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Billboard Management
|
||||
// =============================================================================
|
||||
|
||||
void Viewport3D::addBillboard(std::shared_ptr<Billboard> bb) {
|
||||
if (billboards_ && bb) {
|
||||
billboards_->push_back(bb);
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport3D::removeBillboard(Billboard* bb) {
|
||||
if (!billboards_ || !bb) return;
|
||||
auto it = std::find_if(billboards_->begin(), billboards_->end(),
|
||||
[bb](const std::shared_ptr<Billboard>& p) { return p.get() == bb; });
|
||||
if (it != billboards_->end()) {
|
||||
billboards_->erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport3D::clearBillboards() {
|
||||
if (billboards_) {
|
||||
billboards_->clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport3D::renderBillboards(const mat4& view, const mat4& proj) {
|
||||
#ifdef MCRF_HAS_GL
|
||||
if (!billboards_ || billboards_->empty() || !shader_ || !shader_->isValid()) return;
|
||||
|
||||
shader_->bind();
|
||||
unsigned int shaderProgram = shader_->getProgram();
|
||||
vec3 cameraPos = camera_.getPosition();
|
||||
|
||||
// Enable blending for transparency
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// Disable depth write but keep depth test for proper ordering
|
||||
glDepthMask(GL_FALSE);
|
||||
|
||||
for (auto& billboard : *billboards_) {
|
||||
if (billboard && billboard->isVisible()) {
|
||||
billboard->render(shaderProgram, view, proj, cameraPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore depth writing
|
||||
glDepthMask(GL_TRUE);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
shader_->unbind();
|
||||
#endif
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FBO Management
|
||||
// =============================================================================
|
||||
|
|
@ -626,12 +685,13 @@ void Viewport3D::renderMeshLayers() {
|
|||
shader_->setUniform("u_has_texture", false);
|
||||
|
||||
// Render each layer
|
||||
unsigned int shaderProgram = shader_->getProgram();
|
||||
for (auto* layer : sortedLayers) {
|
||||
// Set model matrix for this layer
|
||||
shader_->setUniform("u_model", layer->getModelMatrix());
|
||||
|
||||
// Render the layer's geometry
|
||||
layer->render(layer->getModelMatrix(), view, projection);
|
||||
// Render the layer's geometry (terrain + mesh instances)
|
||||
layer->render(shaderProgram, layer->getModelMatrix(), view, projection);
|
||||
}
|
||||
|
||||
shader_->unbind();
|
||||
|
|
@ -673,6 +733,9 @@ void Viewport3D::render3DContent() {
|
|||
mat4 projection = camera_.getProjectionMatrix();
|
||||
renderEntities(view, projection);
|
||||
|
||||
// Render billboards (after opaque geometry for proper transparency)
|
||||
renderBillboards(view, projection);
|
||||
|
||||
// Render test cube if enabled (disabled when layers are added)
|
||||
if (renderTestCube_ && shader_ && shader_->isValid() && testVBO_ != 0) {
|
||||
shader_->bind();
|
||||
|
|
@ -1795,6 +1858,206 @@ static PyObject* Viewport3D_is_in_fov(PyViewport3DObject* self, PyObject* args)
|
|||
return PyBool_FromLong(self->data->isInFOV(x, z));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Mesh Instance Methods (Milestone 6)
|
||||
// =============================================================================
|
||||
|
||||
static PyObject* Viewport3D_add_mesh(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"layer_name", "model", "pos", "rotation", "scale", NULL};
|
||||
|
||||
const char* layerName = nullptr;
|
||||
PyObject* modelObj = nullptr;
|
||||
PyObject* posObj = nullptr;
|
||||
float rotation = 0.0f;
|
||||
float scale = 1.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOO|ff", const_cast<char**>(kwlist),
|
||||
&layerName, &modelObj, &posObj, &rotation, &scale)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Validate model
|
||||
if (!PyObject_IsInstance(modelObj, (PyObject*)&mcrfpydef::PyModel3DType)) {
|
||||
PyErr_SetString(PyExc_TypeError, "model must be a Model3D object");
|
||||
return NULL;
|
||||
}
|
||||
PyModel3DObject* modelPy = (PyModel3DObject*)modelObj;
|
||||
if (!modelPy->data) {
|
||||
PyErr_SetString(PyExc_ValueError, "model is invalid");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse position
|
||||
if (!PyTuple_Check(posObj) || PyTuple_Size(posObj) < 3) {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, y, z)");
|
||||
return NULL;
|
||||
}
|
||||
float px = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(posObj, 0)));
|
||||
float py = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(posObj, 1)));
|
||||
float pz = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(posObj, 2)));
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
|
||||
// Get or create layer
|
||||
auto layer = self->data->getLayer(layerName);
|
||||
if (!layer) {
|
||||
layer = self->data->addLayer(layerName, 0);
|
||||
}
|
||||
|
||||
// Add mesh instance
|
||||
size_t index = layer->addMesh(modelPy->data, vec3(px, py, pz), rotation, vec3(scale, scale, scale));
|
||||
|
||||
return PyLong_FromSize_t(index);
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_place_blocking(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"grid_pos", "footprint", "walkable", "transparent", NULL};
|
||||
|
||||
PyObject* gridPosObj = nullptr;
|
||||
PyObject* footprintObj = nullptr;
|
||||
int walkable = 0; // Default: not walkable
|
||||
int transparent = 0; // Default: not transparent
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|pp", const_cast<char**>(kwlist),
|
||||
&gridPosObj, &footprintObj, &walkable, &transparent)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse grid_pos
|
||||
if (!PyTuple_Check(gridPosObj) || PyTuple_Size(gridPosObj) < 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple of (x, z)");
|
||||
return NULL;
|
||||
}
|
||||
int gridX = static_cast<int>(PyLong_AsLong(PyTuple_GetItem(gridPosObj, 0)));
|
||||
int gridZ = static_cast<int>(PyLong_AsLong(PyTuple_GetItem(gridPosObj, 1)));
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
|
||||
// Parse footprint
|
||||
if (!PyTuple_Check(footprintObj) || PyTuple_Size(footprintObj) < 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "footprint must be a tuple of (width, depth)");
|
||||
return NULL;
|
||||
}
|
||||
int footW = static_cast<int>(PyLong_AsLong(PyTuple_GetItem(footprintObj, 0)));
|
||||
int footD = static_cast<int>(PyLong_AsLong(PyTuple_GetItem(footprintObj, 1)));
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
|
||||
// Mark cells
|
||||
for (int dz = 0; dz < footD; dz++) {
|
||||
for (int dx = 0; dx < footW; dx++) {
|
||||
int cx = gridX + dx;
|
||||
int cz = gridZ + dz;
|
||||
if (self->data->isValidCell(cx, cz)) {
|
||||
VoxelPoint& cell = self->data->at(cx, cz);
|
||||
cell.walkable = walkable != 0;
|
||||
cell.transparent = transparent != 0;
|
||||
self->data->syncTCODCell(cx, cz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_clear_meshes(PyViewport3DObject* self, PyObject* args) {
|
||||
const char* layerName = nullptr;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "s", &layerName)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto layer = self->data->getLayer(layerName);
|
||||
if (!layer) {
|
||||
PyErr_SetString(PyExc_ValueError, "Layer not found");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
layer->clearMeshes();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Billboard Management Methods
|
||||
// =============================================================================
|
||||
|
||||
static PyObject* Viewport3D_add_billboard(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"billboard", NULL};
|
||||
|
||||
PyObject* billboardObj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast<char**>(kwlist), &billboardObj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check if it's a Billboard object
|
||||
if (!PyObject_IsInstance(billboardObj, (PyObject*)&mcrfpydef::PyBillboardType)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Expected a Billboard object");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyBillboardObject* bbObj = (PyBillboardObject*)billboardObj;
|
||||
if (!bbObj->data) {
|
||||
PyErr_SetString(PyExc_ValueError, "Invalid Billboard object");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->data->addBillboard(bbObj->data);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_remove_billboard(PyViewport3DObject* self, PyObject* args) {
|
||||
PyObject* billboardObj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "O", &billboardObj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!PyObject_IsInstance(billboardObj, (PyObject*)&mcrfpydef::PyBillboardType)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Expected a Billboard object");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyBillboardObject* bbObj = (PyBillboardObject*)billboardObj;
|
||||
if (bbObj->data) {
|
||||
self->data->removeBillboard(bbObj->data.get());
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_clear_billboards(PyViewport3DObject* self, PyObject* args) {
|
||||
self->data->clearBillboards();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_get_billboard(PyViewport3DObject* self, PyObject* args) {
|
||||
int index = 0;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "i", &index)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto billboards = self->data->getBillboards();
|
||||
if (index < 0 || index >= static_cast<int>(billboards->size())) {
|
||||
PyErr_SetString(PyExc_IndexError, "Billboard index out of range");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto bb = (*billboards)[index];
|
||||
|
||||
// Create Python wrapper for billboard
|
||||
auto type = &mcrfpydef::PyBillboardType;
|
||||
auto obj = (PyBillboardObject*)type->tp_alloc(type, 0);
|
||||
if (!obj) return NULL;
|
||||
|
||||
obj->data = bb;
|
||||
obj->weakreflist = nullptr;
|
||||
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
static PyObject* Viewport3D_billboard_count(PyViewport3DObject* self, PyObject* args) {
|
||||
auto billboards = self->data->getBillboards();
|
||||
return PyLong_FromLong(static_cast<long>(billboards->size()));
|
||||
}
|
||||
|
||||
} // namespace mcrf
|
||||
|
||||
// Methods array - outside namespace but PyObjectType still in scope via typedef
|
||||
|
|
@ -1903,5 +2166,58 @@ PyMethodDef Viewport3D_methods[] = {
|
|||
" z: Z coordinate\n\n"
|
||||
"Returns:\n"
|
||||
" True if the cell is visible"},
|
||||
|
||||
// Mesh instance methods (Milestone 6)
|
||||
{"add_mesh", (PyCFunction)mcrf::Viewport3D_add_mesh, METH_VARARGS | METH_KEYWORDS,
|
||||
"add_mesh(layer_name, model, pos, rotation=0, scale=1.0) -> int\n\n"
|
||||
"Add a Model3D instance to a layer at the specified position.\n\n"
|
||||
"Args:\n"
|
||||
" layer_name: Name of layer to add mesh to (created if needed)\n"
|
||||
" model: Model3D object to place\n"
|
||||
" pos: World position as (x, y, z) tuple\n"
|
||||
" rotation: Y-axis rotation in degrees\n"
|
||||
" scale: Uniform scale factor\n\n"
|
||||
"Returns:\n"
|
||||
" Index of the mesh instance"},
|
||||
{"place_blocking", (PyCFunction)mcrf::Viewport3D_place_blocking, METH_VARARGS | METH_KEYWORDS,
|
||||
"place_blocking(grid_pos, footprint, walkable=False, transparent=False)\n\n"
|
||||
"Mark grid cells as blocking for pathfinding and FOV.\n\n"
|
||||
"Args:\n"
|
||||
" grid_pos: Top-left grid position as (x, z) tuple\n"
|
||||
" footprint: Size in cells as (width, depth) tuple\n"
|
||||
" walkable: Whether cells should be walkable (default: False)\n"
|
||||
" transparent: Whether cells should be transparent (default: False)"},
|
||||
{"clear_meshes", (PyCFunction)mcrf::Viewport3D_clear_meshes, METH_VARARGS,
|
||||
"clear_meshes(layer_name)\n\n"
|
||||
"Clear all mesh instances from a layer.\n\n"
|
||||
"Args:\n"
|
||||
" layer_name: Name of layer to clear"},
|
||||
|
||||
// Billboard methods (Milestone 6)
|
||||
{"add_billboard", (PyCFunction)mcrf::Viewport3D_add_billboard, METH_VARARGS | METH_KEYWORDS,
|
||||
"add_billboard(billboard)\n\n"
|
||||
"Add a Billboard to the viewport.\n\n"
|
||||
"Args:\n"
|
||||
" billboard: Billboard object to add"},
|
||||
{"remove_billboard", (PyCFunction)mcrf::Viewport3D_remove_billboard, METH_VARARGS,
|
||||
"remove_billboard(billboard)\n\n"
|
||||
"Remove a Billboard from the viewport.\n\n"
|
||||
"Args:\n"
|
||||
" billboard: Billboard object to remove"},
|
||||
{"clear_billboards", (PyCFunction)mcrf::Viewport3D_clear_billboards, METH_NOARGS,
|
||||
"clear_billboards()\n\n"
|
||||
"Remove all billboards from the viewport."},
|
||||
{"get_billboard", (PyCFunction)mcrf::Viewport3D_get_billboard, METH_VARARGS,
|
||||
"get_billboard(index) -> Billboard\n\n"
|
||||
"Get a Billboard by index.\n\n"
|
||||
"Args:\n"
|
||||
" index: Index of the billboard\n\n"
|
||||
"Returns:\n"
|
||||
" Billboard object"},
|
||||
{"billboard_count", (PyCFunction)mcrf::Viewport3D_billboard_count, METH_NOARGS,
|
||||
"billboard_count() -> int\n\n"
|
||||
"Get the number of billboards.\n\n"
|
||||
"Returns:\n"
|
||||
" Number of billboards in the viewport"},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ namespace mcrf {
|
|||
class Viewport3D;
|
||||
class Shader3D;
|
||||
class MeshLayer;
|
||||
class Billboard;
|
||||
|
||||
} // namespace mcrf
|
||||
|
||||
|
|
@ -190,6 +191,28 @@ 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);
|
||||
|
||||
// Background color
|
||||
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
|
||||
sf::Color getBackgroundColor() const { return bgColor_; }
|
||||
|
|
@ -276,6 +299,9 @@ 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_;
|
||||
|
||||
// Shader for PS1-style rendering
|
||||
std::unique_ptr<Shader3D> shader_;
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
#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 "McRogueFaceVersion.h"
|
||||
#include "GameEngine.h"
|
||||
// ImGui is only available for SFML builds
|
||||
|
|
@ -441,6 +442,7 @@ PyObject* PyInit_mcrfpy()
|
|||
/*3D entities*/
|
||||
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
||||
&mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType,
|
||||
&mcrfpydef::PyBillboardType,
|
||||
|
||||
/*grid layers (#147)*/
|
||||
&PyColorLayerType, &PyTileLayerType,
|
||||
|
|
@ -561,6 +563,7 @@ PyObject* PyInit_mcrfpy()
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -67,16 +67,29 @@ 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 {
|
||||
((PyTextureObject*)obj)->data = shared_from_this();
|
||||
// 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());
|
||||
}
|
||||
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,6 +28,9 @@ 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*);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue