glTF model loading
This commit is contained in:
parent
8636e766f8
commit
544c44ca31
8 changed files with 8601 additions and 9 deletions
|
|
@ -3,6 +3,8 @@
|
||||||
#include "Entity3D.h"
|
#include "Entity3D.h"
|
||||||
#include "Viewport3D.h"
|
#include "Viewport3D.h"
|
||||||
#include "VoxelPoint.h"
|
#include "VoxelPoint.h"
|
||||||
|
#include "Model3D.h"
|
||||||
|
#include "Shader3D.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PyColor.h"
|
#include "PyColor.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
|
|
@ -467,6 +469,24 @@ void Entity3D::render(const mat4& view, const mat4& proj, unsigned int shader)
|
||||||
{
|
{
|
||||||
if (!visible_) return;
|
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();
|
||||||
|
model_->render(shader, model, view, proj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, fall back to placeholder cube
|
||||||
// Initialize cube geometry if needed
|
// Initialize cube geometry if needed
|
||||||
if (!cubeInitialized_) {
|
if (!cubeInitialized_) {
|
||||||
initCubeGeometry();
|
initCubeGeometry();
|
||||||
|
|
@ -479,17 +499,9 @@ void Entity3D::render(const mat4& view, const mat4& proj, unsigned int shader)
|
||||||
// Get uniform locations (assuming shader is already bound)
|
// Get uniform locations (assuming shader is already bound)
|
||||||
int mvpLoc = glGetUniformLocation(shader, "u_mvp");
|
int mvpLoc = glGetUniformLocation(shader, "u_mvp");
|
||||||
int modelLoc = glGetUniformLocation(shader, "u_model");
|
int modelLoc = glGetUniformLocation(shader, "u_model");
|
||||||
int colorLoc = glGetUniformLocation(shader, "u_entityColor");
|
|
||||||
|
|
||||||
if (mvpLoc >= 0) glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, mvp.data());
|
if (mvpLoc >= 0) glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, mvp.data());
|
||||||
if (modelLoc >= 0) glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.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
|
// Bind VBO and set up attributes
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO_);
|
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO_);
|
||||||
|
|
@ -715,6 +727,41 @@ PyObject* Entity3D::get_viewport(PyEntity3DObject* self, void* closure)
|
||||||
Py_RETURN_NONE;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
|
|
||||||
PyObject* Entity3D::py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
|
PyObject* Entity3D::py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
|
@ -854,6 +901,8 @@ PyGetSetDef Entity3D::getsetters[] = {
|
||||||
"Entity render color.", NULL},
|
"Entity render color.", NULL},
|
||||||
{"viewport", (getter)Entity3D::get_viewport, NULL,
|
{"viewport", (getter)Entity3D::get_viewport, NULL,
|
||||||
"Owning Viewport3D (read-only).", NULL},
|
"Owning Viewport3D (read-only).", NULL},
|
||||||
|
{"model", (getter)Entity3D::get_model, (setter)Entity3D::set_model,
|
||||||
|
"3D model (Model3D). If None, uses placeholder cube.", NULL},
|
||||||
{NULL} // Sentinel
|
{NULL} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ namespace mcrf {
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
class Viewport3D;
|
class Viewport3D;
|
||||||
|
class Model3D;
|
||||||
|
|
||||||
} // namespace mcrf
|
} // namespace mcrf
|
||||||
|
|
||||||
|
|
@ -94,6 +95,10 @@ public:
|
||||||
int getSpriteIndex() const { return sprite_index_; }
|
int getSpriteIndex() const { return sprite_index_; }
|
||||||
void setSpriteIndex(int idx) { sprite_index_ = idx; }
|
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
|
// Viewport Integration
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -185,6 +190,8 @@ public:
|
||||||
static PyObject* get_color(PyEntity3DObject* self, void* closure);
|
static PyObject* get_color(PyEntity3DObject* self, void* closure);
|
||||||
static int set_color(PyEntity3DObject* self, PyObject* value, void* closure);
|
static int set_color(PyEntity3DObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* get_viewport(PyEntity3DObject* self, 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);
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
static PyObject* py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
@ -217,6 +224,7 @@ private:
|
||||||
bool visible_ = true;
|
bool visible_ = true;
|
||||||
sf::Color color_ = sf::Color(200, 100, 50); // Default orange
|
sf::Color color_ = sf::Color(200, 100, 50); // Default orange
|
||||||
int sprite_index_ = 0;
|
int sprite_index_ = 0;
|
||||||
|
std::shared_ptr<Model3D> model_; // 3D model (null = placeholder cube)
|
||||||
|
|
||||||
// Viewport (weak reference to avoid cycles)
|
// Viewport (weak reference to avoid cycles)
|
||||||
std::weak_ptr<Viewport3D> viewport_;
|
std::weak_ptr<Viewport3D> viewport_;
|
||||||
|
|
|
||||||
805
src/3d/Model3D.cpp
Normal file
805
src/3d/Model3D.cpp
Normal file
|
|
@ -0,0 +1,805 @@
|
||||||
|
// Model3D.cpp - 3D model resource implementation
|
||||||
|
|
||||||
|
#include "Model3D.h"
|
||||||
|
#include "Shader3D.h"
|
||||||
|
#include "cgltf.h"
|
||||||
|
#include "../platform/GLContext.h"
|
||||||
|
|
||||||
|
// Include appropriate 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
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
// Static members
|
||||||
|
std::string Model3D::lastError_;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ModelMesh Implementation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
ModelMesh::ModelMesh(ModelMesh&& other) noexcept
|
||||||
|
: vbo(other.vbo)
|
||||||
|
, ebo(other.ebo)
|
||||||
|
, vertex_count(other.vertex_count)
|
||||||
|
, index_count(other.index_count)
|
||||||
|
, material_index(other.material_index)
|
||||||
|
{
|
||||||
|
other.vbo = 0;
|
||||||
|
other.ebo = 0;
|
||||||
|
other.vertex_count = 0;
|
||||||
|
other.index_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelMesh& ModelMesh::operator=(ModelMesh&& other) noexcept
|
||||||
|
{
|
||||||
|
if (this != &other) {
|
||||||
|
vbo = other.vbo;
|
||||||
|
ebo = other.ebo;
|
||||||
|
vertex_count = other.vertex_count;
|
||||||
|
index_count = other.index_count;
|
||||||
|
material_index = other.material_index;
|
||||||
|
other.vbo = 0;
|
||||||
|
other.ebo = 0;
|
||||||
|
other.vertex_count = 0;
|
||||||
|
other.index_count = 0;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Model3D Implementation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
Model3D::Model3D()
|
||||||
|
: name_("unnamed")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Model3D::~Model3D()
|
||||||
|
{
|
||||||
|
cleanupGPU();
|
||||||
|
}
|
||||||
|
|
||||||
|
Model3D::Model3D(Model3D&& other) noexcept
|
||||||
|
: name_(std::move(other.name_))
|
||||||
|
, meshes_(std::move(other.meshes_))
|
||||||
|
, bounds_min_(other.bounds_min_)
|
||||||
|
, bounds_max_(other.bounds_max_)
|
||||||
|
, has_skeleton_(other.has_skeleton_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Model3D& Model3D::operator=(Model3D&& other) noexcept
|
||||||
|
{
|
||||||
|
if (this != &other) {
|
||||||
|
cleanupGPU();
|
||||||
|
name_ = std::move(other.name_);
|
||||||
|
meshes_ = std::move(other.meshes_);
|
||||||
|
bounds_min_ = other.bounds_min_;
|
||||||
|
bounds_max_ = other.bounds_max_;
|
||||||
|
has_skeleton_ = other.has_skeleton_;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Model3D::cleanupGPU()
|
||||||
|
{
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
if (gl::isGLReady()) {
|
||||||
|
for (auto& mesh : meshes_) {
|
||||||
|
if (mesh.vbo) {
|
||||||
|
glDeleteBuffers(1, &mesh.vbo);
|
||||||
|
mesh.vbo = 0;
|
||||||
|
}
|
||||||
|
if (mesh.ebo) {
|
||||||
|
glDeleteBuffers(1, &mesh.ebo);
|
||||||
|
mesh.ebo = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
meshes_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Model3D::computeBounds(const std::vector<MeshVertex>& vertices)
|
||||||
|
{
|
||||||
|
if (vertices.empty()) {
|
||||||
|
bounds_min_ = vec3(0, 0, 0);
|
||||||
|
bounds_max_ = vec3(0, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds_min_ = vertices[0].position;
|
||||||
|
bounds_max_ = vertices[0].position;
|
||||||
|
|
||||||
|
for (const auto& v : vertices) {
|
||||||
|
bounds_min_.x = std::min(bounds_min_.x, v.position.x);
|
||||||
|
bounds_min_.y = std::min(bounds_min_.y, v.position.y);
|
||||||
|
bounds_min_.z = std::min(bounds_min_.z, v.position.z);
|
||||||
|
bounds_max_.x = std::max(bounds_max_.x, v.position.x);
|
||||||
|
bounds_max_.y = std::max(bounds_max_.y, v.position.y);
|
||||||
|
bounds_max_.z = std::max(bounds_max_.z, v.position.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelMesh Model3D::createMesh(const std::vector<MeshVertex>& vertices,
|
||||||
|
const std::vector<uint32_t>& indices)
|
||||||
|
{
|
||||||
|
ModelMesh mesh;
|
||||||
|
mesh.vertex_count = static_cast<int>(vertices.size());
|
||||||
|
mesh.index_count = static_cast<int>(indices.size());
|
||||||
|
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
// Only create GPU resources if GL is ready
|
||||||
|
if (!gl::isGLReady()) {
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create VBO
|
||||||
|
glGenBuffers(1, &mesh.vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER,
|
||||||
|
vertices.size() * sizeof(MeshVertex),
|
||||||
|
vertices.data(),
|
||||||
|
GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
// Create EBO if indexed
|
||||||
|
if (!indices.empty()) {
|
||||||
|
glGenBuffers(1, &mesh.ebo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ebo);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
|
||||||
|
indices.size() * sizeof(uint32_t),
|
||||||
|
indices.data(),
|
||||||
|
GL_STATIC_DRAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Model Information
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
int Model3D::getVertexCount() const
|
||||||
|
{
|
||||||
|
int total = 0;
|
||||||
|
for (const auto& mesh : meshes_) {
|
||||||
|
total += mesh.vertex_count;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Model3D::getTriangleCount() const
|
||||||
|
{
|
||||||
|
int total = 0;
|
||||||
|
for (const auto& mesh : meshes_) {
|
||||||
|
if (mesh.index_count > 0) {
|
||||||
|
total += mesh.index_count / 3;
|
||||||
|
} else {
|
||||||
|
total += mesh.vertex_count / 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Rendering
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Model3D::render(unsigned int shader, const mat4& model,
|
||||||
|
const mat4& view, const mat4& projection)
|
||||||
|
{
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
if (!gl::isGLReady()) return;
|
||||||
|
|
||||||
|
// Calculate MVP
|
||||||
|
mat4 mvp = projection * view * model;
|
||||||
|
|
||||||
|
// Set uniforms (shader should already be bound)
|
||||||
|
int mvpLoc = glGetUniformLocation(shader, "u_mvp");
|
||||||
|
int modelLoc = glGetUniformLocation(shader, "u_model");
|
||||||
|
|
||||||
|
if (mvpLoc >= 0) glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, mvp.data());
|
||||||
|
if (modelLoc >= 0) glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.data());
|
||||||
|
|
||||||
|
// Render each mesh
|
||||||
|
for (const auto& mesh : meshes_) {
|
||||||
|
if (mesh.vertex_count == 0) continue;
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo);
|
||||||
|
|
||||||
|
// Set up vertex attributes (matching MeshVertex layout)
|
||||||
|
// Position (location 0)
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(MeshVertex), (void*)offsetof(MeshVertex, position));
|
||||||
|
|
||||||
|
// Texcoord (location 1)
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(MeshVertex), (void*)offsetof(MeshVertex, texcoord));
|
||||||
|
|
||||||
|
// Normal (location 2)
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(MeshVertex), (void*)offsetof(MeshVertex, normal));
|
||||||
|
|
||||||
|
// Color (location 3)
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(MeshVertex), (void*)offsetof(MeshVertex, color));
|
||||||
|
|
||||||
|
// Draw
|
||||||
|
if (mesh.index_count > 0) {
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ebo);
|
||||||
|
glDrawElements(GL_TRIANGLES, mesh.index_count, GL_UNSIGNED_INT, 0);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||||
|
} else {
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, mesh.vertex_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_POSITION);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Procedural Primitives
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
std::shared_ptr<Model3D> Model3D::cube(float size)
|
||||||
|
{
|
||||||
|
auto model = std::make_shared<Model3D>();
|
||||||
|
model->name_ = "cube";
|
||||||
|
|
||||||
|
float s = size * 0.5f;
|
||||||
|
|
||||||
|
// 24 vertices (4 per face for proper normals)
|
||||||
|
std::vector<MeshVertex> vertices;
|
||||||
|
vertices.reserve(24);
|
||||||
|
|
||||||
|
// Helper to add a face
|
||||||
|
auto addFace = [&](vec3 p0, vec3 p1, vec3 p2, vec3 p3, vec3 normal) {
|
||||||
|
MeshVertex v;
|
||||||
|
v.normal = normal;
|
||||||
|
v.color = vec4(1, 1, 1, 1);
|
||||||
|
|
||||||
|
v.position = p0; v.texcoord = vec2(0, 0); vertices.push_back(v);
|
||||||
|
v.position = p1; v.texcoord = vec2(1, 0); vertices.push_back(v);
|
||||||
|
v.position = p2; v.texcoord = vec2(1, 1); vertices.push_back(v);
|
||||||
|
v.position = p3; v.texcoord = vec2(0, 1); vertices.push_back(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Front face (+Z)
|
||||||
|
addFace(vec3(-s, -s, s), vec3( s, -s, s), vec3( s, s, s), vec3(-s, s, s), vec3(0, 0, 1));
|
||||||
|
// Back face (-Z)
|
||||||
|
addFace(vec3( s, -s, -s), vec3(-s, -s, -s), vec3(-s, s, -s), vec3( s, s, -s), vec3(0, 0, -1));
|
||||||
|
// Right face (+X)
|
||||||
|
addFace(vec3( s, -s, s), vec3( s, -s, -s), vec3( s, s, -s), vec3( s, s, s), vec3(1, 0, 0));
|
||||||
|
// Left face (-X)
|
||||||
|
addFace(vec3(-s, -s, -s), vec3(-s, -s, s), vec3(-s, s, s), vec3(-s, s, -s), vec3(-1, 0, 0));
|
||||||
|
// Top face (+Y)
|
||||||
|
addFace(vec3(-s, s, s), vec3( s, s, s), vec3( s, s, -s), vec3(-s, s, -s), vec3(0, 1, 0));
|
||||||
|
// Bottom face (-Y)
|
||||||
|
addFace(vec3(-s, -s, -s), vec3( s, -s, -s), vec3( s, -s, s), vec3(-s, -s, s), vec3(0, -1, 0));
|
||||||
|
|
||||||
|
// Indices for 6 faces (2 triangles each)
|
||||||
|
std::vector<uint32_t> indices;
|
||||||
|
indices.reserve(36);
|
||||||
|
for (int face = 0; face < 6; ++face) {
|
||||||
|
uint32_t base = face * 4;
|
||||||
|
indices.push_back(base + 0);
|
||||||
|
indices.push_back(base + 1);
|
||||||
|
indices.push_back(base + 2);
|
||||||
|
indices.push_back(base + 0);
|
||||||
|
indices.push_back(base + 2);
|
||||||
|
indices.push_back(base + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
model->meshes_.push_back(createMesh(vertices, indices));
|
||||||
|
model->bounds_min_ = vec3(-s, -s, -s);
|
||||||
|
model->bounds_max_ = vec3(s, s, s);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Model3D> Model3D::plane(float width, float depth, int segments)
|
||||||
|
{
|
||||||
|
auto model = std::make_shared<Model3D>();
|
||||||
|
model->name_ = "plane";
|
||||||
|
|
||||||
|
segments = std::max(1, segments);
|
||||||
|
float hw = width * 0.5f;
|
||||||
|
float hd = depth * 0.5f;
|
||||||
|
|
||||||
|
std::vector<MeshVertex> vertices;
|
||||||
|
std::vector<uint32_t> indices;
|
||||||
|
|
||||||
|
// Generate grid of vertices
|
||||||
|
int cols = segments + 1;
|
||||||
|
int rows = segments + 1;
|
||||||
|
vertices.reserve(cols * rows);
|
||||||
|
|
||||||
|
for (int z = 0; z < rows; ++z) {
|
||||||
|
for (int x = 0; x < cols; ++x) {
|
||||||
|
MeshVertex v;
|
||||||
|
float u = static_cast<float>(x) / segments;
|
||||||
|
float w = static_cast<float>(z) / segments;
|
||||||
|
|
||||||
|
v.position = vec3(
|
||||||
|
-hw + u * width,
|
||||||
|
0.0f,
|
||||||
|
-hd + w * depth
|
||||||
|
);
|
||||||
|
v.texcoord = vec2(u, w);
|
||||||
|
v.normal = vec3(0, 1, 0);
|
||||||
|
v.color = vec4(1, 1, 1, 1);
|
||||||
|
|
||||||
|
vertices.push_back(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate indices
|
||||||
|
indices.reserve(segments * segments * 6);
|
||||||
|
for (int z = 0; z < segments; ++z) {
|
||||||
|
for (int x = 0; x < segments; ++x) {
|
||||||
|
uint32_t i0 = z * cols + x;
|
||||||
|
uint32_t i1 = i0 + 1;
|
||||||
|
uint32_t i2 = i0 + cols;
|
||||||
|
uint32_t i3 = i2 + 1;
|
||||||
|
|
||||||
|
indices.push_back(i0);
|
||||||
|
indices.push_back(i2);
|
||||||
|
indices.push_back(i1);
|
||||||
|
|
||||||
|
indices.push_back(i1);
|
||||||
|
indices.push_back(i2);
|
||||||
|
indices.push_back(i3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model->meshes_.push_back(createMesh(vertices, indices));
|
||||||
|
model->bounds_min_ = vec3(-hw, 0, -hd);
|
||||||
|
model->bounds_max_ = vec3(hw, 0, hd);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Model3D> Model3D::sphere(float radius, int segments, int rings)
|
||||||
|
{
|
||||||
|
auto model = std::make_shared<Model3D>();
|
||||||
|
model->name_ = "sphere";
|
||||||
|
|
||||||
|
segments = std::max(3, segments);
|
||||||
|
rings = std::max(2, rings);
|
||||||
|
|
||||||
|
std::vector<MeshVertex> vertices;
|
||||||
|
std::vector<uint32_t> indices;
|
||||||
|
|
||||||
|
// Generate vertices
|
||||||
|
for (int y = 0; y <= rings; ++y) {
|
||||||
|
float v = static_cast<float>(y) / rings;
|
||||||
|
float phi = v * PI;
|
||||||
|
|
||||||
|
for (int x = 0; x <= segments; ++x) {
|
||||||
|
float u = static_cast<float>(x) / segments;
|
||||||
|
float theta = u * 2.0f * PI;
|
||||||
|
|
||||||
|
MeshVertex vert;
|
||||||
|
vert.normal = vec3(
|
||||||
|
std::sin(phi) * std::cos(theta),
|
||||||
|
std::cos(phi),
|
||||||
|
std::sin(phi) * std::sin(theta)
|
||||||
|
);
|
||||||
|
vert.position = vert.normal * radius;
|
||||||
|
vert.texcoord = vec2(u, v);
|
||||||
|
vert.color = vec4(1, 1, 1, 1);
|
||||||
|
|
||||||
|
vertices.push_back(vert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate indices
|
||||||
|
for (int y = 0; y < rings; ++y) {
|
||||||
|
for (int x = 0; x < segments; ++x) {
|
||||||
|
uint32_t i0 = y * (segments + 1) + x;
|
||||||
|
uint32_t i1 = i0 + 1;
|
||||||
|
uint32_t i2 = i0 + (segments + 1);
|
||||||
|
uint32_t i3 = i2 + 1;
|
||||||
|
|
||||||
|
indices.push_back(i0);
|
||||||
|
indices.push_back(i2);
|
||||||
|
indices.push_back(i1);
|
||||||
|
|
||||||
|
indices.push_back(i1);
|
||||||
|
indices.push_back(i2);
|
||||||
|
indices.push_back(i3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model->meshes_.push_back(createMesh(vertices, indices));
|
||||||
|
model->bounds_min_ = vec3(-radius, -radius, -radius);
|
||||||
|
model->bounds_max_ = vec3(radius, radius, radius);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// glTF Loading
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
std::shared_ptr<Model3D> Model3D::load(const std::string& path)
|
||||||
|
{
|
||||||
|
lastError_.clear();
|
||||||
|
|
||||||
|
cgltf_options options = {};
|
||||||
|
cgltf_data* data = nullptr;
|
||||||
|
|
||||||
|
// Parse the file
|
||||||
|
cgltf_result result = cgltf_parse_file(&options, path.c_str(), &data);
|
||||||
|
if (result != cgltf_result_success) {
|
||||||
|
lastError_ = "Failed to parse glTF file: " + path;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load buffers
|
||||||
|
result = cgltf_load_buffers(&options, data, path.c_str());
|
||||||
|
if (result != cgltf_result_success) {
|
||||||
|
lastError_ = "Failed to load glTF buffers: " + path;
|
||||||
|
cgltf_free(data);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto model = std::make_shared<Model3D>();
|
||||||
|
|
||||||
|
// Extract filename for model name
|
||||||
|
size_t lastSlash = path.find_last_of("/\\");
|
||||||
|
if (lastSlash != std::string::npos) {
|
||||||
|
model->name_ = path.substr(lastSlash + 1);
|
||||||
|
} else {
|
||||||
|
model->name_ = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove extension
|
||||||
|
size_t dot = model->name_.rfind('.');
|
||||||
|
if (dot != std::string::npos) {
|
||||||
|
model->name_ = model->name_.substr(0, dot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for skeleton
|
||||||
|
model->has_skeleton_ = (data->skins_count > 0);
|
||||||
|
|
||||||
|
// Track all vertices for bounds calculation
|
||||||
|
std::vector<MeshVertex> allVertices;
|
||||||
|
|
||||||
|
// Process each mesh
|
||||||
|
for (size_t i = 0; i < data->meshes_count; ++i) {
|
||||||
|
cgltf_mesh* mesh = &data->meshes[i];
|
||||||
|
|
||||||
|
for (size_t j = 0; j < mesh->primitives_count; ++j) {
|
||||||
|
cgltf_primitive* prim = &mesh->primitives[j];
|
||||||
|
|
||||||
|
// Only support triangles
|
||||||
|
if (prim->type != cgltf_primitive_type_triangles) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<vec3> positions;
|
||||||
|
std::vector<vec3> normals;
|
||||||
|
std::vector<vec2> texcoords;
|
||||||
|
std::vector<vec4> colors;
|
||||||
|
|
||||||
|
// Extract attributes
|
||||||
|
for (size_t k = 0; k < prim->attributes_count; ++k) {
|
||||||
|
cgltf_attribute* attr = &prim->attributes[k];
|
||||||
|
cgltf_accessor* accessor = attr->data;
|
||||||
|
|
||||||
|
if (attr->type == cgltf_attribute_type_position) {
|
||||||
|
positions.resize(accessor->count);
|
||||||
|
for (size_t v = 0; v < accessor->count; ++v) {
|
||||||
|
cgltf_accessor_read_float(accessor, v, &positions[v].x, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (attr->type == cgltf_attribute_type_normal) {
|
||||||
|
normals.resize(accessor->count);
|
||||||
|
for (size_t v = 0; v < accessor->count; ++v) {
|
||||||
|
cgltf_accessor_read_float(accessor, v, &normals[v].x, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (attr->type == cgltf_attribute_type_texcoord && attr->index == 0) {
|
||||||
|
texcoords.resize(accessor->count);
|
||||||
|
for (size_t v = 0; v < accessor->count; ++v) {
|
||||||
|
cgltf_accessor_read_float(accessor, v, &texcoords[v].x, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (attr->type == cgltf_attribute_type_color && attr->index == 0) {
|
||||||
|
colors.resize(accessor->count);
|
||||||
|
for (size_t v = 0; v < accessor->count; ++v) {
|
||||||
|
// Color can be vec3 or vec4
|
||||||
|
if (accessor->type == cgltf_type_vec4) {
|
||||||
|
cgltf_accessor_read_float(accessor, v, &colors[v].x, 4);
|
||||||
|
} else {
|
||||||
|
cgltf_accessor_read_float(accessor, v, &colors[v].x, 3);
|
||||||
|
colors[v].w = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if no positions
|
||||||
|
if (positions.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in defaults for missing attributes
|
||||||
|
size_t vertCount = positions.size();
|
||||||
|
if (normals.empty()) {
|
||||||
|
normals.resize(vertCount, vec3(0, 1, 0));
|
||||||
|
}
|
||||||
|
if (texcoords.empty()) {
|
||||||
|
texcoords.resize(vertCount, vec2(0, 0));
|
||||||
|
}
|
||||||
|
if (colors.empty()) {
|
||||||
|
colors.resize(vertCount, vec4(1, 1, 1, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interleave vertex data
|
||||||
|
std::vector<MeshVertex> vertices;
|
||||||
|
vertices.reserve(vertCount);
|
||||||
|
for (size_t v = 0; v < vertCount; ++v) {
|
||||||
|
MeshVertex mv;
|
||||||
|
mv.position = positions[v];
|
||||||
|
mv.texcoord = texcoords[v];
|
||||||
|
mv.normal = normals[v];
|
||||||
|
mv.color = colors[v];
|
||||||
|
vertices.push_back(mv);
|
||||||
|
allVertices.push_back(mv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract indices
|
||||||
|
std::vector<uint32_t> indices;
|
||||||
|
if (prim->indices) {
|
||||||
|
cgltf_accessor* accessor = prim->indices;
|
||||||
|
indices.resize(accessor->count);
|
||||||
|
for (size_t idx = 0; idx < accessor->count; ++idx) {
|
||||||
|
indices[idx] = static_cast<uint32_t>(cgltf_accessor_read_index(accessor, idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create mesh
|
||||||
|
model->meshes_.push_back(createMesh(vertices, indices));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute bounds from all vertices
|
||||||
|
model->computeBounds(allVertices);
|
||||||
|
|
||||||
|
cgltf_free(data);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Python API Implementation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
int Model3D::init(PyObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
static const char* kwlist[] = {"path", NULL};
|
||||||
|
|
||||||
|
const char* path = nullptr;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|s", const_cast<char**>(kwlist), &path)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)self;
|
||||||
|
|
||||||
|
if (path && path[0] != '\0') {
|
||||||
|
// Load from file
|
||||||
|
obj->data = Model3D::load(path);
|
||||||
|
if (!obj->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, Model3D::getLastError().c_str());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Empty model
|
||||||
|
obj->data = std::make_shared<Model3D>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::repr(PyObject* self)
|
||||||
|
{
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)self;
|
||||||
|
if (!obj->data) {
|
||||||
|
return PyUnicode_FromString("<Model3D (null)>");
|
||||||
|
}
|
||||||
|
|
||||||
|
char buf[256];
|
||||||
|
snprintf(buf, sizeof(buf), "<Model3D '%s' verts=%d tris=%d%s>",
|
||||||
|
obj->data->getName().c_str(),
|
||||||
|
obj->data->getVertexCount(),
|
||||||
|
obj->data->getTriangleCount(),
|
||||||
|
obj->data->hasSkeleton() ? " skeletal" : "");
|
||||||
|
return PyUnicode_FromString(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::py_cube(PyObject* cls, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
static const char* kwlist[] = {"size", NULL};
|
||||||
|
float size = 1.0f;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|f", const_cast<char**>(kwlist), &size)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new Python object
|
||||||
|
PyTypeObject* type = (PyTypeObject*)cls;
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)type->tp_alloc(type, 0);
|
||||||
|
if (!obj) return NULL;
|
||||||
|
|
||||||
|
obj->data = Model3D::cube(size);
|
||||||
|
obj->weakreflist = nullptr;
|
||||||
|
|
||||||
|
return (PyObject*)obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::py_plane(PyObject* cls, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
static const char* kwlist[] = {"width", "depth", "segments", NULL};
|
||||||
|
float width = 1.0f;
|
||||||
|
float depth = 1.0f;
|
||||||
|
int segments = 1;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffi", const_cast<char**>(kwlist),
|
||||||
|
&width, &depth, &segments)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyTypeObject* type = (PyTypeObject*)cls;
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)type->tp_alloc(type, 0);
|
||||||
|
if (!obj) return NULL;
|
||||||
|
|
||||||
|
obj->data = Model3D::plane(width, depth, segments);
|
||||||
|
obj->weakreflist = nullptr;
|
||||||
|
|
||||||
|
return (PyObject*)obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::py_sphere(PyObject* cls, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
static const char* kwlist[] = {"radius", "segments", "rings", NULL};
|
||||||
|
float radius = 0.5f;
|
||||||
|
int segments = 16;
|
||||||
|
int rings = 12;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fii", const_cast<char**>(kwlist),
|
||||||
|
&radius, &segments, &rings)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyTypeObject* type = (PyTypeObject*)cls;
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)type->tp_alloc(type, 0);
|
||||||
|
if (!obj) return NULL;
|
||||||
|
|
||||||
|
obj->data = Model3D::sphere(radius, segments, rings);
|
||||||
|
obj->weakreflist = nullptr;
|
||||||
|
|
||||||
|
return (PyObject*)obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::get_vertex_count(PyObject* self, void* closure)
|
||||||
|
{
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)self;
|
||||||
|
if (!obj->data) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
return PyLong_FromLong(obj->data->getVertexCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::get_triangle_count(PyObject* self, void* closure)
|
||||||
|
{
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)self;
|
||||||
|
if (!obj->data) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
return PyLong_FromLong(obj->data->getTriangleCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::get_has_skeleton(PyObject* self, void* closure)
|
||||||
|
{
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)self;
|
||||||
|
if (!obj->data) {
|
||||||
|
Py_RETURN_FALSE;
|
||||||
|
}
|
||||||
|
return PyBool_FromLong(obj->data->hasSkeleton());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::get_bounds(PyObject* self, void* closure)
|
||||||
|
{
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)self;
|
||||||
|
if (!obj->data) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [min, max] = obj->data->getBounds();
|
||||||
|
PyObject* minTuple = Py_BuildValue("(fff)", min.x, min.y, min.z);
|
||||||
|
PyObject* maxTuple = Py_BuildValue("(fff)", max.x, max.y, max.z);
|
||||||
|
|
||||||
|
if (!minTuple || !maxTuple) {
|
||||||
|
Py_XDECREF(minTuple);
|
||||||
|
Py_XDECREF(maxTuple);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* result = PyTuple_Pack(2, minTuple, maxTuple);
|
||||||
|
Py_DECREF(minTuple);
|
||||||
|
Py_DECREF(maxTuple);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::get_name(PyObject* self, void* closure)
|
||||||
|
{
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)self;
|
||||||
|
if (!obj->data) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
return PyUnicode_FromString(obj->data->getName().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::get_mesh_count(PyObject* self, void* closure)
|
||||||
|
{
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)self;
|
||||||
|
if (!obj->data) {
|
||||||
|
return PyLong_FromLong(0);
|
||||||
|
}
|
||||||
|
return PyLong_FromLong(static_cast<long>(obj->data->getMeshCount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method and property tables
|
||||||
|
PyMethodDef Model3D::methods[] = {
|
||||||
|
{"cube", (PyCFunction)py_cube, METH_VARARGS | METH_KEYWORDS | METH_CLASS,
|
||||||
|
"cube(size=1.0) -> Model3D\n\nCreate a unit cube centered at origin."},
|
||||||
|
{"plane", (PyCFunction)py_plane, METH_VARARGS | METH_KEYWORDS | METH_CLASS,
|
||||||
|
"plane(width=1.0, depth=1.0, segments=1) -> Model3D\n\nCreate a flat plane."},
|
||||||
|
{"sphere", (PyCFunction)py_sphere, METH_VARARGS | METH_KEYWORDS | METH_CLASS,
|
||||||
|
"sphere(radius=0.5, segments=16, rings=12) -> Model3D\n\nCreate a UV sphere."},
|
||||||
|
{NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
PyGetSetDef Model3D::getsetters[] = {
|
||||||
|
{"vertex_count", get_vertex_count, NULL, "Total vertex count across all meshes (read-only)", NULL},
|
||||||
|
{"triangle_count", get_triangle_count, NULL, "Total triangle count across all meshes (read-only)", NULL},
|
||||||
|
{"has_skeleton", get_has_skeleton, NULL, "Whether model has skeletal animation data (read-only)", NULL},
|
||||||
|
{"bounds", get_bounds, NULL, "AABB as ((min_x, min_y, min_z), (max_x, max_y, max_z)) (read-only)", NULL},
|
||||||
|
{"name", get_name, NULL, "Model name (read-only)", NULL},
|
||||||
|
{"mesh_count", get_mesh_count, NULL, "Number of submeshes (read-only)", NULL},
|
||||||
|
{NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
242
src/3d/Model3D.h
Normal file
242
src/3d/Model3D.h
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ModelMesh - Single submesh within a Model3D
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
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
|
||||||
|
size_t getMeshCount() const { return meshes_.size(); }
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 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 PyMethodDef methods[];
|
||||||
|
static PyGetSetDef getsetters[];
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Model data
|
||||||
|
std::string name_;
|
||||||
|
std::vector<ModelMesh> meshes_;
|
||||||
|
|
||||||
|
// Bounds
|
||||||
|
vec3 bounds_min_ = vec3(0, 0, 0);
|
||||||
|
vec3 bounds_max_ = vec3(0, 0, 0);
|
||||||
|
|
||||||
|
// Future: skeletal animation data
|
||||||
|
bool has_skeleton_ = false;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // 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"
|
||||||
|
),
|
||||||
|
.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
|
||||||
7240
src/3d/cgltf.h
Normal file
7240
src/3d/cgltf.h
Normal file
File diff suppressed because it is too large
Load diff
6
src/3d/cgltf_impl.cpp
Normal file
6
src/3d/cgltf_impl.cpp
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// 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"
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
#include "3d/Viewport3D.h" // 3D rendering viewport
|
#include "3d/Viewport3D.h" // 3D rendering viewport
|
||||||
#include "3d/Entity3D.h" // 3D game entities
|
#include "3d/Entity3D.h" // 3D game entities
|
||||||
#include "3d/EntityCollection3D.h" // Entity3D collection
|
#include "3d/EntityCollection3D.h" // Entity3D collection
|
||||||
|
#include "3d/Model3D.h" // 3D model resource
|
||||||
#include "McRogueFaceVersion.h"
|
#include "McRogueFaceVersion.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
// ImGui is only available for SFML builds
|
// ImGui is only available for SFML builds
|
||||||
|
|
@ -439,7 +440,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
|
|
||||||
/*3D entities*/
|
/*3D entities*/
|
||||||
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
||||||
&mcrfpydef::PyEntityCollection3DIterType,
|
&mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType,
|
||||||
|
|
||||||
/*grid layers (#147)*/
|
/*grid layers (#147)*/
|
||||||
&PyColorLayerType, &PyTileLayerType,
|
&PyColorLayerType, &PyTileLayerType,
|
||||||
|
|
@ -559,6 +560,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist);
|
PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist);
|
||||||
PyViewport3DType.tp_weaklistoffset = offsetof(PyViewport3DObject, weakreflist);
|
PyViewport3DType.tp_weaklistoffset = offsetof(PyViewport3DObject, weakreflist);
|
||||||
mcrfpydef::PyEntity3DType.tp_weaklistoffset = offsetof(PyEntity3DObject, weakreflist);
|
mcrfpydef::PyEntity3DType.tp_weaklistoffset = offsetof(PyEntity3DObject, weakreflist);
|
||||||
|
mcrfpydef::PyModel3DType.tp_weaklistoffset = offsetof(PyModel3DObject, weakreflist);
|
||||||
|
|
||||||
// #219 - Initialize PyLock context manager type
|
// #219 - Initialize PyLock context manager type
|
||||||
if (PyLock::init() < 0) {
|
if (PyLock::init() < 0) {
|
||||||
|
|
|
||||||
240
tests/demo/screens/model_loading_demo.py
Normal file
240
tests/demo/screens/model_loading_demo.py
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
# 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")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue