diff --git a/src/3d/Billboard.cpp b/src/3d/Billboard.cpp new file mode 100644 index 0000000..5937212 --- /dev/null +++ b/src/3d/Billboard.cpp @@ -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 +#include + +// GL headers based on backend +#if defined(MCRF_SDL2) + #ifdef __EMSCRIPTEN__ + #include + #else + #include + #include + #endif + #define MCRF_HAS_GL 1 +#elif !defined(MCRF_HEADLESS) + #include + #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 texture, int spriteIndex, const vec3& pos, + float scale, BillboardFacing facing) + : texture_(texture) + , spriteIndex_(spriteIndex) + , position_(pos) + , scale_(scale) + , facing_(facing) + , theta_(0.0f) + , phi_(0.0f) + , opacity_(1.0f) + , visible_(true) + , tilesPerRow_(1) + , tilesPerCol_(1) +{} + +Billboard::~Billboard() { + // Texture is not owned, don't delete it +} + +// ============================================================================= +// Configuration +// ============================================================================= + +void Billboard::setSpriteSheetLayout(int tilesPerRow, int tilesPerCol) { + tilesPerRow_ = tilesPerRow > 0 ? tilesPerRow : 1; + tilesPerCol_ = tilesPerCol > 0 ? tilesPerCol : 1; +} + +// ============================================================================= +// Static Geometry Management +// ============================================================================= + +void Billboard::initSharedGeometry() { +#ifdef MCRF_HAS_GL + if (geometryInitialized_ || !gl::isGLReady()) { + return; + } + + // Create a unit quad centered at origin, facing +Z + // Vertices: position (3) + texcoord (2) + normal (3) + color (4) = 12 floats + MeshVertex vertices[4] = { + // Bottom-left + MeshVertex(vec3(-0.5f, -0.5f, 0.0f), vec2(0, 1), vec3(0, 0, 1), vec4(1, 1, 1, 1)), + // Bottom-right + MeshVertex(vec3( 0.5f, -0.5f, 0.0f), vec2(1, 1), vec3(0, 0, 1), vec4(1, 1, 1, 1)), + // Top-right + MeshVertex(vec3( 0.5f, 0.5f, 0.0f), vec2(1, 0), vec3(0, 0, 1), vec4(1, 1, 1, 1)), + // Top-left + MeshVertex(vec3(-0.5f, 0.5f, 0.0f), vec2(0, 0), vec3(0, 0, 1), vec4(1, 1, 1, 1)), + }; + + unsigned short indices[6] = { + 0, 1, 2, // First triangle + 2, 3, 0 // Second triangle + }; + + glGenBuffers(1, &sharedVBO_); + glBindBuffer(GL_ARRAY_BUFFER, sharedVBO_); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + glGenBuffers(1, &sharedEBO_); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sharedEBO_); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + geometryInitialized_ = true; +#endif +} + +void Billboard::cleanupSharedGeometry() { +#ifdef MCRF_HAS_GL + if (sharedVBO_ != 0 && gl::isGLReady()) { + glDeleteBuffers(1, &sharedVBO_); + sharedVBO_ = 0; + } + if (sharedEBO_ != 0 && gl::isGLReady()) { + glDeleteBuffers(1, &sharedEBO_); + sharedEBO_ = 0; + } + geometryInitialized_ = false; +#endif +} + +// ============================================================================= +// Rendering +// ============================================================================= + +mat4 Billboard::computeModelMatrix(const vec3& cameraPos, const mat4& view) { + mat4 model = mat4::identity(); + + // First translate to world position + model = model * mat4::translate(position_); + + // Apply rotation based on facing mode + switch (facing_) { + case BillboardFacing::Camera: { + // Full rotation to face camera + // Extract camera right and up vectors from view matrix + vec3 right(view.m[0], view.m[4], view.m[8]); + vec3 up(view.m[1], view.m[5], view.m[9]); + + // Build rotation matrix that makes the quad face the camera + // The quad is initially facing +Z, we need to rotate it + mat4 rotation; + rotation.m[0] = right.x; rotation.m[4] = up.x; rotation.m[8] = -view.m[2]; rotation.m[12] = 0; + rotation.m[1] = right.y; rotation.m[5] = up.y; rotation.m[9] = -view.m[6]; rotation.m[13] = 0; + rotation.m[2] = right.z; rotation.m[6] = up.z; rotation.m[10] = -view.m[10]; rotation.m[14] = 0; + rotation.m[3] = 0; rotation.m[7] = 0; rotation.m[11] = 0; rotation.m[15] = 1; + + model = model * rotation; + break; + } + + case BillboardFacing::CameraY: { + // Only Y-axis rotation to face camera (stays upright) + vec3 toCamera(cameraPos.x - position_.x, 0, cameraPos.z - position_.z); + float length = std::sqrt(toCamera.x * toCamera.x + toCamera.z * toCamera.z); + if (length > 0.001f) { + float angle = std::atan2(toCamera.x, toCamera.z); + model = model * mat4::rotateY(angle); + } + break; + } + + case BillboardFacing::Fixed: { + // Use theta (Y rotation) and phi (X tilt) + model = model * mat4::rotateY(theta_); + model = model * mat4::rotateX(phi_); + break; + } + } + + // Apply scale + model = model * mat4::scale(vec3(scale_, scale_, scale_)); + + return model; +} + +void Billboard::render(unsigned int shader, const mat4& view, const mat4& projection, + const vec3& cameraPos) { +#ifdef MCRF_HAS_GL + if (!visible_ || !gl::isGLReady()) { + return; + } + + // Initialize shared geometry if needed + if (!geometryInitialized_) { + initSharedGeometry(); + } + + if (sharedVBO_ == 0 || sharedEBO_ == 0) { + return; + } + + // Compute model matrix + mat4 model = computeModelMatrix(cameraPos, view); + + // Set model matrix uniform + int modelLoc = glGetUniformLocation(shader, "u_model"); + if (modelLoc >= 0) { + glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.m); + } + + // Set entity color uniform (for tinting/opacity) + int colorLoc = glGetUniformLocation(shader, "u_entityColor"); + if (colorLoc >= 0) { + glUniform4f(colorLoc, 1.0f, 1.0f, 1.0f, opacity_); + } + + // Handle texture + bool hasTexture = (texture_ != nullptr); + int hasTexLoc = glGetUniformLocation(shader, "u_has_texture"); + if (hasTexLoc >= 0) { + glUniform1i(hasTexLoc, hasTexture ? 1 : 0); + } + + if (hasTexture) { + // Bind texture using PyTexture's underlying sf::Texture + const sf::Texture* sfTexture = texture_->getSFMLTexture(); + if (sfTexture) { + glBindTexture(GL_TEXTURE_2D, sfTexture->getNativeHandle()); + + // Use PyTexture's sprite sheet configuration + int sheetW = texture_->sprite_width > 0 ? texture_->sprite_width : 1; + int sheetH = texture_->sprite_height > 0 ? texture_->sprite_height : 1; + sf::Vector2u texSize = sfTexture->getSize(); + int tilesPerRow = texSize.x / sheetW; + int tilesPerCol = texSize.y / sheetH; + if (tilesPerRow < 1) tilesPerRow = 1; + if (tilesPerCol < 1) tilesPerCol = 1; + + // Calculate sprite UV offset + float tileU = 1.0f / tilesPerRow; + float tileV = 1.0f / tilesPerCol; + int tileX = spriteIndex_ % tilesPerRow; + int tileY = spriteIndex_ / tilesPerRow; + + // Set UV offset/scale uniforms if available + int uvOffsetLoc = glGetUniformLocation(shader, "u_uv_offset"); + int uvScaleLoc = glGetUniformLocation(shader, "u_uv_scale"); + if (uvOffsetLoc >= 0) { + glUniform2f(uvOffsetLoc, tileX * tileU, tileY * tileV); + } + if (uvScaleLoc >= 0) { + glUniform2f(uvScaleLoc, tileU, tileV); + } + } + } + + // Bind VBO + glBindBuffer(GL_ARRAY_BUFFER, sharedVBO_); + + // Set up vertex attributes + int stride = sizeof(MeshVertex); + + glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION); + glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, + stride, reinterpret_cast(offsetof(MeshVertex, position))); + + glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD); + glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, + stride, reinterpret_cast(offsetof(MeshVertex, texcoord))); + + glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL); + glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE, + stride, reinterpret_cast(offsetof(MeshVertex, normal))); + + glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR); + glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE, + stride, reinterpret_cast(offsetof(MeshVertex, color))); + + // Bind EBO and draw + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sharedEBO_); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); + + // Cleanup + glDisableVertexAttribArray(Shader3D::ATTRIB_POSITION); + glDisableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD); + glDisableVertexAttribArray(Shader3D::ATTRIB_NORMAL); + glDisableVertexAttribArray(Shader3D::ATTRIB_COLOR); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + // Unbind texture + if (hasTexture) { + glBindTexture(GL_TEXTURE_2D, 0); + } + + // Reset UV uniforms + int uvOffsetLoc = glGetUniformLocation(shader, "u_uv_offset"); + int uvScaleLoc = glGetUniformLocation(shader, "u_uv_scale"); + if (uvOffsetLoc >= 0) { + glUniform2f(uvOffsetLoc, 0.0f, 0.0f); + } + if (uvScaleLoc >= 0) { + glUniform2f(uvScaleLoc, 1.0f, 1.0f); + } +#endif +} + +// ============================================================================= +// Python API +// ============================================================================= + +PyGetSetDef Billboard::getsetters[] = { + {"texture", Billboard::get_texture, Billboard::set_texture, + "Sprite sheet texture (Texture or None)", NULL}, + {"sprite_index", Billboard::get_sprite_index, Billboard::set_sprite_index, + "Index into sprite sheet (int)", NULL}, + {"pos", Billboard::get_pos, Billboard::set_pos, + "World position as (x, y, z) tuple", NULL}, + {"scale", Billboard::get_scale, Billboard::set_scale, + "Uniform scale factor (float)", NULL}, + {"facing", Billboard::get_facing, Billboard::set_facing, + "Facing mode: 'camera', 'camera_y', or 'fixed' (str)", NULL}, + {"theta", Billboard::get_theta, Billboard::set_theta, + "Horizontal rotation for 'fixed' mode in radians (float)", NULL}, + {"phi", Billboard::get_phi, Billboard::set_phi, + "Vertical tilt for 'fixed' mode in radians (float)", NULL}, + {"opacity", Billboard::get_opacity, Billboard::set_opacity, + "Opacity from 0.0 (transparent) to 1.0 (opaque) (float)", NULL}, + {"visible", Billboard::get_visible, Billboard::set_visible, + "Visibility state (bool)", NULL}, + {NULL} +}; + +int Billboard::init(PyObject* self, PyObject* args, PyObject* kwds) { + PyBillboardObject* selfObj = (PyBillboardObject*)self; + + static const char* kwlist[] = {"texture", "sprite_index", "pos", "scale", "facing", "opacity", "visible", NULL}; + + PyObject* textureObj = nullptr; + int spriteIndex = 0; + PyObject* posObj = nullptr; + float scale = 1.0f; + const char* facingStr = "camera_y"; + float opacity = 1.0f; + int visible = 1; // Default to True + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OiOfsfp", const_cast(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(PyFloat_AsDouble(PyTuple_GetItem(posObj, 0))); + float y = static_cast(PyFloat_AsDouble(PyTuple_GetItem(posObj, 1))); + float z = static_cast(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(""); + } + + 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), "", + 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 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(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(PyFloat_AsDouble(PyTuple_GetItem(value, 0))); + float y = static_cast(PyFloat_AsDouble(PyTuple_GetItem(value, 1))); + float z = static_cast(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(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(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(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(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 diff --git a/src/3d/Billboard.h b/src/3d/Billboard.h new file mode 100644 index 0000000..f837baa --- /dev/null +++ b/src/3d/Billboard.h @@ -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 + +// 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 { +public: + // Python integration + PyObject* self = nullptr; + uint64_t serial_number = 0; + + Billboard(); + Billboard(std::shared_ptr 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 getTexture() const { return texture_; } + void setTexture(std::shared_ptr 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 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 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(std::make_shared()); + self->weakreflist = nullptr; + } + return (PyObject*)self; + } +}; + +} // namespace mcrfpydef diff --git a/src/3d/Entity3D.cpp b/src/3d/Entity3D.cpp index 633b690..fe15720 100644 --- a/src/3d/Entity3D.cpp +++ b/src/3d/Entity3D.cpp @@ -3,6 +3,8 @@ #include "Entity3D.h" #include "Viewport3D.h" #include "VoxelPoint.h" +#include "Model3D.h" +#include "Shader3D.h" #include "PyVector.h" #include "PyColor.h" #include "PythonObjectCache.h" @@ -54,6 +56,10 @@ Entity3D::~Entity3D() { // Cleanup cube geometry when last entity is destroyed? // For now, leave it - it's shared static data + + // Clean up Python animation callback + Py_XDECREF(py_anim_callback_); + py_anim_callback_ = nullptr; } // ============================================================================= @@ -281,23 +287,27 @@ void Entity3D::processNextMove() void Entity3D::update(float dt) { - if (!is_animating_) return; + // Update movement animation + if (is_animating_) { + move_progress_ += dt * move_speed_; - move_progress_ += dt * move_speed_; + if (move_progress_ >= 1.0f) { + // Animation complete + world_pos_ = target_world_pos_; + is_animating_ = false; - if (move_progress_ >= 1.0f) { - // Animation complete - world_pos_ = target_world_pos_; - is_animating_ = false; - - // Process next move in queue - if (!move_queue_.empty()) { - processNextMove(); + // Process next move in queue + if (!move_queue_.empty()) { + processNextMove(); + } + } else { + // Interpolate position + world_pos_ = vec3::lerp(move_start_pos_, target_world_pos_, move_progress_); } - } else { - // Interpolate position - world_pos_ = vec3::lerp(move_start_pos_, target_world_pos_, move_progress_); } + + // Update skeletal animation + updateAnimation(dt); } bool Entity3D::setProperty(const std::string& name, float value) @@ -384,6 +394,111 @@ bool Entity3D::hasProperty(const std::string& name) const name == "sprite_index" || name == "visible"; } +// ============================================================================= +// Skeletal Animation +// ============================================================================= + +void Entity3D::setAnimClip(const std::string& name) +{ + if (anim_clip_ == name) return; + + anim_clip_ = name; + anim_time_ = 0.0f; + anim_paused_ = false; + + // Initialize bone matrices if model has skeleton + if (model_ && model_->hasSkeleton()) { + size_t bone_count = model_->getBoneCount(); + bone_matrices_.resize(bone_count); + for (auto& m : bone_matrices_) { + m = mat4::identity(); + } + } +} + +void Entity3D::updateAnimation(float dt) +{ + // Handle auto-animate (play walk/idle based on movement state) + if (auto_animate_ && model_ && model_->hasSkeleton()) { + bool currently_moving = isMoving(); + if (currently_moving != was_moving_) { + was_moving_ = currently_moving; + if (currently_moving) { + // Started moving - play walk clip + if (model_->findClip(walk_clip_)) { + setAnimClip(walk_clip_); + } + } else { + // Stopped moving - play idle clip + if (model_->findClip(idle_clip_)) { + setAnimClip(idle_clip_); + } + } + } + } + + // Early out if no model, no skeleton, or no animation + if (!model_ || !model_->hasSkeleton()) return; + if (anim_clip_.empty() || anim_paused_) return; + + const AnimationClip* clip = model_->findClip(anim_clip_); + if (!clip) return; + + // Advance time + anim_time_ += dt * anim_speed_; + + // Handle loop/completion + if (anim_time_ >= clip->duration) { + if (anim_loop_) { + anim_time_ = std::fmod(anim_time_, clip->duration); + } else { + anim_time_ = clip->duration; + anim_paused_ = true; + + // Fire callback + if (on_anim_complete_) { + on_anim_complete_(this, anim_clip_); + } + + // Fire Python callback + if (py_anim_callback_) { + PyObject* result = PyObject_CallFunction(py_anim_callback_, "(Os)", + self, anim_clip_.c_str()); + if (result) { + Py_DECREF(result); + } else { + PyErr_Print(); + } + } + } + } + + // Sample animation + const Skeleton& skeleton = model_->getSkeleton(); + const std::vector& default_transforms = model_->getDefaultBoneTransforms(); + + std::vector local_transforms; + clip->sample(anim_time_, skeleton.bones.size(), default_transforms, local_transforms); + + // Compute global transforms + std::vector global_transforms; + skeleton.computeGlobalTransforms(local_transforms, global_transforms); + + // Compute final bone matrices (global * inverse_bind) + skeleton.computeBoneMatrices(global_transforms, bone_matrices_); +} + +int Entity3D::getAnimFrame() const +{ + if (!model_ || !model_->hasSkeleton()) return 0; + + const AnimationClip* clip = model_->findClip(anim_clip_); + if (!clip || clip->duration <= 0) return 0; + + // Approximate frame at 30fps + return static_cast(anim_time_ * 30.0f); +} + // ============================================================================= // Rendering // ============================================================================= @@ -467,6 +582,30 @@ void Entity3D::render(const mat4& view, const mat4& proj, unsigned int shader) { if (!visible_) return; + // Set entity color uniform (used by Model3D and placeholder) + int colorLoc = glGetUniformLocation(shader, "u_entityColor"); + if (colorLoc >= 0) { + glUniform4f(colorLoc, + color_.r / 255.0f, + color_.g / 255.0f, + color_.b / 255.0f, + color_.a / 255.0f); + } + + // If we have a model, use it + if (model_) { + mat4 model = getModelMatrix(); + + // Use skinned rendering if model has skeleton and we have bone matrices + if (model_->hasSkeleton() && !bone_matrices_.empty()) { + model_->renderSkinned(shader, model, view, proj, bone_matrices_); + } else { + model_->render(shader, model, view, proj); + } + return; + } + + // Otherwise, fall back to placeholder cube // Initialize cube geometry if needed if (!cubeInitialized_) { initCubeGeometry(); @@ -479,17 +618,9 @@ void Entity3D::render(const mat4& view, const mat4& proj, unsigned int shader) // Get uniform locations (assuming shader is already bound) int mvpLoc = glGetUniformLocation(shader, "u_mvp"); int modelLoc = glGetUniformLocation(shader, "u_model"); - int colorLoc = glGetUniformLocation(shader, "u_entityColor"); if (mvpLoc >= 0) glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, mvp.data()); if (modelLoc >= 0) glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.data()); - if (colorLoc >= 0) { - glUniform4f(colorLoc, - color_.r / 255.0f, - color_.g / 255.0f, - color_.b / 255.0f, - color_.a / 255.0f); - } // Bind VBO and set up attributes glBindBuffer(GL_ARRAY_BUFFER, cubeVBO_); @@ -715,6 +846,182 @@ PyObject* Entity3D::get_viewport(PyEntity3DObject* self, void* closure) Py_RETURN_NONE; } +PyObject* Entity3D::get_model(PyEntity3DObject* self, void* closure) +{ + auto model = self->data->getModel(); + if (!model) { + Py_RETURN_NONE; + } + + // Create Python Model3D object wrapping the shared_ptr + PyTypeObject* type = &mcrfpydef::PyModel3DType; + PyModel3DObject* obj = (PyModel3DObject*)type->tp_alloc(type, 0); + if (!obj) return NULL; + + obj->data = model; + obj->weakreflist = nullptr; + + return (PyObject*)obj; +} + +int Entity3D::set_model(PyEntity3DObject* self, PyObject* value, void* closure) +{ + if (value == Py_None) { + self->data->setModel(nullptr); + return 0; + } + + if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyModel3DType)) { + PyErr_SetString(PyExc_TypeError, "model must be a Model3D or None"); + return -1; + } + + PyModel3DObject* model_obj = (PyModel3DObject*)value; + self->data->setModel(model_obj->data); + return 0; +} + +// Animation property getters/setters + +PyObject* Entity3D::get_anim_clip(PyEntity3DObject* self, void* closure) +{ + return PyUnicode_FromString(self->data->getAnimClip().c_str()); +} + +int Entity3D::set_anim_clip(PyEntity3DObject* self, PyObject* value, void* closure) +{ + if (!PyUnicode_Check(value)) { + PyErr_SetString(PyExc_TypeError, "anim_clip must be a string"); + return -1; + } + self->data->setAnimClip(PyUnicode_AsUTF8(value)); + return 0; +} + +PyObject* Entity3D::get_anim_time(PyEntity3DObject* self, void* closure) +{ + return PyFloat_FromDouble(self->data->getAnimTime()); +} + +int Entity3D::set_anim_time(PyEntity3DObject* self, PyObject* value, void* closure) +{ + if (!PyNumber_Check(value)) { + PyErr_SetString(PyExc_TypeError, "anim_time must be a number"); + return -1; + } + self->data->setAnimTime((float)PyFloat_AsDouble(value)); + return 0; +} + +PyObject* Entity3D::get_anim_speed(PyEntity3DObject* self, void* closure) +{ + return PyFloat_FromDouble(self->data->getAnimSpeed()); +} + +int Entity3D::set_anim_speed(PyEntity3DObject* self, PyObject* value, void* closure) +{ + if (!PyNumber_Check(value)) { + PyErr_SetString(PyExc_TypeError, "anim_speed must be a number"); + return -1; + } + self->data->setAnimSpeed((float)PyFloat_AsDouble(value)); + return 0; +} + +PyObject* Entity3D::get_anim_loop(PyEntity3DObject* self, void* closure) +{ + return PyBool_FromLong(self->data->getAnimLoop() ? 1 : 0); +} + +int Entity3D::set_anim_loop(PyEntity3DObject* self, PyObject* value, void* closure) +{ + self->data->setAnimLoop(PyObject_IsTrue(value)); + return 0; +} + +PyObject* Entity3D::get_anim_paused(PyEntity3DObject* self, void* closure) +{ + return PyBool_FromLong(self->data->getAnimPaused() ? 1 : 0); +} + +int Entity3D::set_anim_paused(PyEntity3DObject* self, PyObject* value, void* closure) +{ + self->data->setAnimPaused(PyObject_IsTrue(value)); + return 0; +} + +PyObject* Entity3D::get_anim_frame(PyEntity3DObject* self, void* closure) +{ + return PyLong_FromLong(self->data->getAnimFrame()); +} + +PyObject* Entity3D::get_on_anim_complete(PyEntity3DObject* self, void* closure) +{ + if (self->data->py_anim_callback_) { + Py_INCREF(self->data->py_anim_callback_); + return self->data->py_anim_callback_; + } + Py_RETURN_NONE; +} + +int Entity3D::set_on_anim_complete(PyEntity3DObject* self, PyObject* value, void* closure) +{ + // Clear existing callback + Py_XDECREF(self->data->py_anim_callback_); + + if (value == Py_None) { + self->data->py_anim_callback_ = nullptr; + } else if (PyCallable_Check(value)) { + Py_INCREF(value); + self->data->py_anim_callback_ = value; + } else { + PyErr_SetString(PyExc_TypeError, "on_anim_complete must be callable or None"); + return -1; + } + return 0; +} + +PyObject* Entity3D::get_auto_animate(PyEntity3DObject* self, void* closure) +{ + return PyBool_FromLong(self->data->getAutoAnimate() ? 1 : 0); +} + +int Entity3D::set_auto_animate(PyEntity3DObject* self, PyObject* value, void* closure) +{ + self->data->setAutoAnimate(PyObject_IsTrue(value)); + return 0; +} + +PyObject* Entity3D::get_walk_clip(PyEntity3DObject* self, void* closure) +{ + return PyUnicode_FromString(self->data->getWalkClip().c_str()); +} + +int Entity3D::set_walk_clip(PyEntity3DObject* self, PyObject* value, void* closure) +{ + if (!PyUnicode_Check(value)) { + PyErr_SetString(PyExc_TypeError, "walk_clip must be a string"); + return -1; + } + self->data->setWalkClip(PyUnicode_AsUTF8(value)); + return 0; +} + +PyObject* Entity3D::get_idle_clip(PyEntity3DObject* self, void* closure) +{ + return PyUnicode_FromString(self->data->getIdleClip().c_str()); +} + +int Entity3D::set_idle_clip(PyEntity3DObject* self, PyObject* value, void* closure) +{ + if (!PyUnicode_Check(value)) { + PyErr_SetString(PyExc_TypeError, "idle_clip must be a string"); + return -1; + } + self->data->setIdleClip(PyUnicode_AsUTF8(value)); + return 0; +} + // Methods PyObject* Entity3D::py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds) @@ -814,6 +1121,47 @@ PyObject* Entity3D::py_animate(PyEntity3DObject* self, PyObject* args, PyObject* return NULL; } +PyObject* Entity3D::py_follow_path(PyEntity3DObject* self, PyObject* args) +{ + PyObject* path_list; + if (!PyArg_ParseTuple(args, "O", &path_list)) { + return NULL; + } + + if (!PyList_Check(path_list)) { + PyErr_SetString(PyExc_TypeError, "follow_path() requires a list of (x, z) tuples"); + return NULL; + } + + std::vector> path; + Py_ssize_t len = PyList_Size(path_list); + for (Py_ssize_t i = 0; i < len; ++i) { + PyObject* item = PyList_GetItem(path_list, i); + if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) { + PyErr_SetString(PyExc_TypeError, "Each path element must be (x, z) tuple"); + return NULL; + } + int x = static_cast(PyLong_AsLong(PyTuple_GetItem(item, 0))); + int z = static_cast(PyLong_AsLong(PyTuple_GetItem(item, 1))); + if (PyErr_Occurred()) return NULL; + path.emplace_back(x, z); + } + + self->data->followPath(path); + Py_RETURN_NONE; +} + +PyObject* Entity3D::py_clear_path(PyEntity3DObject* self, PyObject* args) +{ + self->data->clearPath(); + Py_RETURN_NONE; +} + +PyObject* Entity3D::get_is_moving(PyEntity3DObject* self, void* closure) +{ + return PyBool_FromLong(self->data->isMoving() ? 1 : 0); +} + // Method and GetSet tables PyMethodDef Entity3D::methods[] = { @@ -834,6 +1182,14 @@ PyMethodDef Entity3D::methods[] = { {"animate", (PyCFunction)Entity3D::py_animate, METH_VARARGS | METH_KEYWORDS, "animate(property, target, duration, easing=None, callback=None)\n\n" "Animate a property over time. (Not yet implemented)"}, + {"follow_path", (PyCFunction)Entity3D::py_follow_path, METH_VARARGS, + "follow_path(path)\n\n" + "Queue path positions for smooth movement.\n\n" + "Args:\n" + " path: List of (x, z) tuples (as returned by path_to())"}, + {"clear_path", (PyCFunction)Entity3D::py_clear_path, METH_NOARGS, + "clear_path()\n\n" + "Clear the movement queue and stop at current position."}, {NULL} // Sentinel }; @@ -854,6 +1210,33 @@ PyGetSetDef Entity3D::getsetters[] = { "Entity render color.", NULL}, {"viewport", (getter)Entity3D::get_viewport, NULL, "Owning Viewport3D (read-only).", NULL}, + {"model", (getter)Entity3D::get_model, (setter)Entity3D::set_model, + "3D model (Model3D). If None, uses placeholder cube.", NULL}, + + // Animation properties + {"anim_clip", (getter)Entity3D::get_anim_clip, (setter)Entity3D::set_anim_clip, + "Current animation clip name. Set to play an animation.", NULL}, + {"anim_time", (getter)Entity3D::get_anim_time, (setter)Entity3D::set_anim_time, + "Current time position in animation (seconds).", NULL}, + {"anim_speed", (getter)Entity3D::get_anim_speed, (setter)Entity3D::set_anim_speed, + "Animation playback speed multiplier. 1.0 = normal speed.", NULL}, + {"anim_loop", (getter)Entity3D::get_anim_loop, (setter)Entity3D::set_anim_loop, + "Whether animation loops when it reaches the end.", NULL}, + {"anim_paused", (getter)Entity3D::get_anim_paused, (setter)Entity3D::set_anim_paused, + "Whether animation playback is paused.", NULL}, + {"anim_frame", (getter)Entity3D::get_anim_frame, NULL, + "Current animation frame number (read-only, approximate at 30fps).", NULL}, + {"on_anim_complete", (getter)Entity3D::get_on_anim_complete, (setter)Entity3D::set_on_anim_complete, + "Callback(entity, clip_name) when non-looping animation ends.", NULL}, + {"auto_animate", (getter)Entity3D::get_auto_animate, (setter)Entity3D::set_auto_animate, + "Enable auto-play of walk/idle clips based on movement.", NULL}, + {"walk_clip", (getter)Entity3D::get_walk_clip, (setter)Entity3D::set_walk_clip, + "Animation clip to play when entity is moving.", NULL}, + {"idle_clip", (getter)Entity3D::get_idle_clip, (setter)Entity3D::set_idle_clip, + "Animation clip to play when entity is stationary.", NULL}, + {"is_moving", (getter)Entity3D::get_is_moving, NULL, + "Whether entity is currently moving (read-only).", NULL}, + {NULL} // Sentinel }; diff --git a/src/3d/Entity3D.h b/src/3d/Entity3D.h index c317f5b..ce1d69a 100644 --- a/src/3d/Entity3D.h +++ b/src/3d/Entity3D.h @@ -11,11 +11,13 @@ #include #include #include +#include namespace mcrf { // Forward declarations class Viewport3D; +class Model3D; } // namespace mcrf @@ -94,6 +96,10 @@ public: int getSpriteIndex() const { return sprite_index_; } void setSpriteIndex(int idx) { sprite_index_ = idx; } + // 3D model (if null, uses placeholder cube) + std::shared_ptr getModel() const { return model_; } + void setModel(std::shared_ptr m) { model_ = m; } + // ========================================================================= // Viewport Integration // ========================================================================= @@ -153,6 +159,57 @@ public: bool getProperty(const std::string& name, float& value) const; bool hasProperty(const std::string& name) const; + // ========================================================================= + // Skeletal Animation + // ========================================================================= + + /// Get current animation clip name + const std::string& getAnimClip() const { return anim_clip_; } + + /// Set animation clip by name (starts playing) + void setAnimClip(const std::string& name); + + /// Get/set animation time (position in clip) + float getAnimTime() const { return anim_time_; } + void setAnimTime(float t) { anim_time_ = t; } + + /// Get/set playback speed (1.0 = normal) + float getAnimSpeed() const { return anim_speed_; } + void setAnimSpeed(float s) { anim_speed_ = s; } + + /// Get/set looping state + bool getAnimLoop() const { return anim_loop_; } + void setAnimLoop(bool l) { anim_loop_ = l; } + + /// Get/set pause state + bool getAnimPaused() const { return anim_paused_; } + void setAnimPaused(bool p) { anim_paused_ = p; } + + /// Get current animation frame (approximate) + int getAnimFrame() const; + + /// Update skeletal animation (call before render) + void updateAnimation(float dt); + + /// Get computed bone matrices for shader + const std::vector& getBoneMatrices() const { return bone_matrices_; } + + /// Animation complete callback type + using AnimCompleteCallback = std::function; + + /// Set animation complete callback + void setOnAnimComplete(AnimCompleteCallback cb) { on_anim_complete_ = cb; } + + /// Auto-animate settings (play walk/idle based on movement) + bool getAutoAnimate() const { return auto_animate_; } + void setAutoAnimate(bool a) { auto_animate_ = a; } + + const std::string& getWalkClip() const { return walk_clip_; } + void setWalkClip(const std::string& c) { walk_clip_ = c; } + + const std::string& getIdleClip() const { return idle_clip_; } + void setIdleClip(const std::string& c) { idle_clip_ = c; } + // ========================================================================= // Rendering // ========================================================================= @@ -185,6 +242,29 @@ public: static PyObject* get_color(PyEntity3DObject* self, void* closure); static int set_color(PyEntity3DObject* self, PyObject* value, void* closure); static PyObject* get_viewport(PyEntity3DObject* self, void* closure); + static PyObject* get_model(PyEntity3DObject* self, void* closure); + static int set_model(PyEntity3DObject* self, PyObject* value, void* closure); + + // Animation property getters/setters + static PyObject* get_anim_clip(PyEntity3DObject* self, void* closure); + static int set_anim_clip(PyEntity3DObject* self, PyObject* value, void* closure); + static PyObject* get_anim_time(PyEntity3DObject* self, void* closure); + static int set_anim_time(PyEntity3DObject* self, PyObject* value, void* closure); + static PyObject* get_anim_speed(PyEntity3DObject* self, void* closure); + static int set_anim_speed(PyEntity3DObject* self, PyObject* value, void* closure); + static PyObject* get_anim_loop(PyEntity3DObject* self, void* closure); + static int set_anim_loop(PyEntity3DObject* self, PyObject* value, void* closure); + static PyObject* get_anim_paused(PyEntity3DObject* self, void* closure); + static int set_anim_paused(PyEntity3DObject* self, PyObject* value, void* closure); + static PyObject* get_anim_frame(PyEntity3DObject* self, void* closure); + static PyObject* get_on_anim_complete(PyEntity3DObject* self, void* closure); + static int set_on_anim_complete(PyEntity3DObject* self, PyObject* value, void* closure); + static PyObject* get_auto_animate(PyEntity3DObject* self, void* closure); + static int set_auto_animate(PyEntity3DObject* self, PyObject* value, void* closure); + static PyObject* get_walk_clip(PyEntity3DObject* self, void* closure); + static int set_walk_clip(PyEntity3DObject* self, PyObject* value, void* closure); + static PyObject* get_idle_clip(PyEntity3DObject* self, void* closure); + static int set_idle_clip(PyEntity3DObject* self, PyObject* value, void* closure); // Methods static PyObject* py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds); @@ -192,6 +272,9 @@ public: static PyObject* py_at(PyEntity3DObject* self, PyObject* args, PyObject* kwds); static PyObject* py_update_visibility(PyEntity3DObject* self, PyObject* args); static PyObject* py_animate(PyEntity3DObject* self, PyObject* args, PyObject* kwds); + static PyObject* py_follow_path(PyEntity3DObject* self, PyObject* args); + static PyObject* py_clear_path(PyEntity3DObject* self, PyObject* args); + static PyObject* get_is_moving(PyEntity3DObject* self, void* closure); static PyMethodDef methods[]; static PyGetSetDef getsetters[]; @@ -217,6 +300,7 @@ private: bool visible_ = true; sf::Color color_ = sf::Color(200, 100, 50); // Default orange int sprite_index_ = 0; + std::shared_ptr model_; // 3D model (null = placeholder cube) // Viewport (weak reference to avoid cycles) std::weak_ptr viewport_; @@ -232,6 +316,24 @@ private: float move_speed_ = 5.0f; // Cells per second vec3 move_start_pos_; + // Skeletal animation state + std::string anim_clip_; // Current animation clip name + float anim_time_ = 0.0f; // Current time in animation + float anim_speed_ = 1.0f; // Playback speed multiplier + bool anim_loop_ = true; // Loop animation + bool anim_paused_ = false; // Pause playback + std::vector bone_matrices_; // Computed bone matrices for shader + AnimCompleteCallback on_anim_complete_; // Callback when animation ends + + // Auto-animate state + bool auto_animate_ = true; // Auto-play walk/idle based on movement + std::string walk_clip_ = "walk"; // Clip to play when moving + std::string idle_clip_ = "idle"; // Clip to play when stopped + bool was_moving_ = false; // Track movement state for auto-animate + + // Python callback for animation complete + PyObject* py_anim_callback_ = nullptr; + // Helper to initialize voxel state void initVoxelState() const; diff --git a/src/3d/Math3D.h b/src/3d/Math3D.h index c337343..5f6d9f8 100644 --- a/src/3d/Math3D.h +++ b/src/3d/Math3D.h @@ -610,6 +610,116 @@ struct quat { } }; +// ============================================================================= +// Frustum - View frustum for culling +// ============================================================================= + +struct Plane { + vec3 normal; + float distance; + + Plane() : normal(0, 1, 0), distance(0) {} + Plane(const vec3& n, float d) : normal(n), distance(d) {} + + // Distance from plane to point (positive = in front, negative = behind) + float distanceToPoint(const vec3& point) const { + return normal.dot(point) + distance; + } +}; + +struct Frustum { + // Six planes: left, right, bottom, top, near, far + Plane planes[6]; + + // Extract frustum planes from view-projection matrix + // Uses Gribb/Hartmann method + void extractFromMatrix(const mat4& viewProj) { + const float* m = viewProj.m; + + // Left plane + planes[0].normal.x = m[3] + m[0]; + planes[0].normal.y = m[7] + m[4]; + planes[0].normal.z = m[11] + m[8]; + planes[0].distance = m[15] + m[12]; + + // Right plane + planes[1].normal.x = m[3] - m[0]; + planes[1].normal.y = m[7] - m[4]; + planes[1].normal.z = m[11] - m[8]; + planes[1].distance = m[15] - m[12]; + + // Bottom plane + planes[2].normal.x = m[3] + m[1]; + planes[2].normal.y = m[7] + m[5]; + planes[2].normal.z = m[11] + m[9]; + planes[2].distance = m[15] + m[13]; + + // Top plane + planes[3].normal.x = m[3] - m[1]; + planes[3].normal.y = m[7] - m[5]; + planes[3].normal.z = m[11] - m[9]; + planes[3].distance = m[15] - m[13]; + + // Near plane + planes[4].normal.x = m[3] + m[2]; + planes[4].normal.y = m[7] + m[6]; + planes[4].normal.z = m[11] + m[10]; + planes[4].distance = m[15] + m[14]; + + // Far plane + planes[5].normal.x = m[3] - m[2]; + planes[5].normal.y = m[7] - m[6]; + planes[5].normal.z = m[11] - m[10]; + planes[5].distance = m[15] - m[14]; + + // Normalize all planes + for (int i = 0; i < 6; i++) { + float len = planes[i].normal.length(); + if (len > 0.0001f) { + planes[i].normal = planes[i].normal / len; + planes[i].distance /= len; + } + } + } + + // Test if a point is inside the frustum + bool containsPoint(const vec3& point) const { + for (int i = 0; i < 6; i++) { + if (planes[i].distanceToPoint(point) < 0) { + return false; + } + } + return true; + } + + // Test if a sphere intersects or is inside the frustum + bool containsSphere(const vec3& center, float radius) const { + for (int i = 0; i < 6; i++) { + if (planes[i].distanceToPoint(center) < -radius) { + return false; // Sphere is completely behind this plane + } + } + return true; + } + + // Test if an axis-aligned bounding box intersects the frustum + bool containsAABB(const vec3& min, const vec3& max) const { + for (int i = 0; i < 6; i++) { + // Find the positive vertex (furthest along plane normal) + vec3 pVertex; + pVertex.x = (planes[i].normal.x >= 0) ? max.x : min.x; + pVertex.y = (planes[i].normal.y >= 0) ? max.y : min.y; + pVertex.z = (planes[i].normal.z >= 0) ? max.z : min.z; + + // If positive vertex is behind plane, box is outside + if (planes[i].distanceToPoint(pVertex) < 0) { + return false; + } + } + return true; + } +}; + // ============================================================================= // Utility constants and functions // ============================================================================= diff --git a/src/3d/MeshLayer.cpp b/src/3d/MeshLayer.cpp index faabcb1..7314a14 100644 --- a/src/3d/MeshLayer.cpp +++ b/src/3d/MeshLayer.cpp @@ -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 // 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(offsetof(MeshVertex, position))); + + glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD); + glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, + stride, reinterpret_cast(offsetof(MeshVertex, texcoord))); + + glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL); + glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE, + stride, reinterpret_cast(offsetof(MeshVertex, normal))); + + glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR); + glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE, + stride, reinterpret_cast(offsetof(MeshVertex, color))); + + // Draw triangles + glDrawArrays(GL_TRIANGLES, 0, static_cast(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(offsetof(MeshVertex, position))); - - glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD); - glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, - stride, reinterpret_cast(offsetof(MeshVertex, texcoord))); - - glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL); - glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE, - stride, reinterpret_cast(offsetof(MeshVertex, normal))); - - glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR); - glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE, - stride, reinterpret_cast(offsetof(MeshVertex, color))); - - // Draw triangles - glDrawArrays(GL_TRIANGLES, 0, static_cast(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 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 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(std::ceil(extentX / cellSize))); + int footprintD = std::max(1, static_cast(std::ceil(extentZ / cellSize))); + + // Calculate grid position (center the footprint on the world position) + int gridX = static_cast(std::floor(worldPos.x / cellSize - footprintW * 0.5f)); + int gridZ = static_cast(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 diff --git a/src/3d/MeshLayer.h b/src/3d/MeshLayer.h index f098f0f..55e86ca 100644 --- a/src/3d/MeshLayer.h +++ b/src/3d/MeshLayer.h @@ -10,6 +10,12 @@ #include #include // 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 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 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 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 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 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 diff --git a/src/3d/Model3D.cpp b/src/3d/Model3D.cpp new file mode 100644 index 0000000..1f21f9b --- /dev/null +++ b/src/3d/Model3D.cpp @@ -0,0 +1,1451 @@ +// 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 + #else + #include + #include + #endif + #define MCRF_HAS_GL 1 +#elif !defined(MCRF_HEADLESS) + #include + #define MCRF_HAS_GL 1 +#endif + +#include +#include + +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; +} + +// ============================================================================= +// SkinnedMesh Implementation +// ============================================================================= + +SkinnedMesh::SkinnedMesh(SkinnedMesh&& other) noexcept + : vbo(other.vbo) + , ebo(other.ebo) + , vertex_count(other.vertex_count) + , index_count(other.index_count) + , material_index(other.material_index) + , is_skinned(other.is_skinned) +{ + other.vbo = 0; + other.ebo = 0; + other.vertex_count = 0; + other.index_count = 0; +} + +SkinnedMesh& SkinnedMesh::operator=(SkinnedMesh&& 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; + is_skinned = other.is_skinned; + other.vbo = 0; + other.ebo = 0; + other.vertex_count = 0; + other.index_count = 0; + } + return *this; +} + +// ============================================================================= +// AnimationChannel Implementation +// ============================================================================= + +void AnimationChannel::sample(float time, vec3& trans_out, quat& rot_out, vec3& scale_out) const +{ + if (times.empty()) return; + + // Clamp time to animation range + float t = std::max(times.front(), std::min(time, times.back())); + + // Find surrounding keyframes + size_t k0 = 0, k1 = 0; + float blend = 0.0f; + + for (size_t i = 0; i < times.size() - 1; i++) { + if (t >= times[i] && t <= times[i + 1]) { + k0 = i; + k1 = i + 1; + float dt = times[k1] - times[k0]; + blend = (dt > 0.0001f) ? (t - times[k0]) / dt : 0.0f; + break; + } + } + + // If time is past the last keyframe, use last keyframe + if (t >= times.back()) { + k0 = k1 = times.size() - 1; + blend = 0.0f; + } + + // Interpolate based on path type + switch (path) { + case Path::Translation: + if (!translations.empty()) { + trans_out = vec3::lerp(translations[k0], translations[k1], blend); + } + break; + + case Path::Rotation: + if (!rotations.empty()) { + rot_out = quat::slerp(rotations[k0], rotations[k1], blend); + } + break; + + case Path::Scale: + if (!scales.empty()) { + scale_out = vec3::lerp(scales[k0], scales[k1], blend); + } + break; + } +} + +// ============================================================================= +// AnimationClip Implementation +// ============================================================================= + +void AnimationClip::sample(float time, size_t num_bones, + const std::vector& default_transforms, + std::vector& local_out) const +{ + // Initialize with default transforms + local_out.resize(num_bones); + for (size_t i = 0; i < num_bones && i < default_transforms.size(); i++) { + local_out[i] = default_transforms[i]; + } + + // Track which components have been animated per bone + struct BoneAnimState { + vec3 translation = vec3(0, 0, 0); + quat rotation; + vec3 scale = vec3(1, 1, 1); + bool has_translation = false; + bool has_rotation = false; + bool has_scale = false; + }; + std::vector bone_states(num_bones); + + // Sample all channels + for (const auto& channel : channels) { + if (channel.bone_index < 0 || channel.bone_index >= static_cast(num_bones)) { + continue; + } + + auto& state = bone_states[channel.bone_index]; + vec3 trans_dummy, scale_dummy; + quat rot_dummy; + + channel.sample(time, trans_dummy, rot_dummy, scale_dummy); + + switch (channel.path) { + case AnimationChannel::Path::Translation: + state.translation = trans_dummy; + state.has_translation = true; + break; + case AnimationChannel::Path::Rotation: + state.rotation = rot_dummy; + state.has_rotation = true; + break; + case AnimationChannel::Path::Scale: + state.scale = scale_dummy; + state.has_scale = true; + break; + } + } + + // Build final local transforms for animated bones + for (size_t i = 0; i < num_bones; i++) { + const auto& state = bone_states[i]; + + // Only rebuild if at least one component was animated + if (state.has_translation || state.has_rotation || state.has_scale) { + // Extract default values from default transform if not animated + // (simplified: assume default is identity or use stored values) + vec3 t = state.has_translation ? state.translation : vec3(0, 0, 0); + quat r = state.has_rotation ? state.rotation : quat(); + vec3 s = state.has_scale ? state.scale : vec3(1, 1, 1); + + // If not fully animated, try to extract from default transform + if (!state.has_translation || !state.has_rotation || !state.has_scale) { + // For now, assume default transform contains the rest pose + // A more complete implementation would decompose default_transforms[i] + if (!state.has_translation) { + t = vec3(default_transforms[i].at(3, 0), + default_transforms[i].at(3, 1), + default_transforms[i].at(3, 2)); + } + } + + // Compose: T * R * S + local_out[i] = mat4::translate(t) * r.toMatrix() * mat4::scale(s); + } + } +} + +// ============================================================================= +// Model3D Implementation +// ============================================================================= + +Model3D::Model3D() + : name_("unnamed") +{ +} + +Model3D::~Model3D() +{ + cleanupGPU(); +} + +Model3D::Model3D(Model3D&& other) noexcept + : name_(std::move(other.name_)) + , meshes_(std::move(other.meshes_)) + , skinned_meshes_(std::move(other.skinned_meshes_)) + , bounds_min_(other.bounds_min_) + , bounds_max_(other.bounds_max_) + , has_skeleton_(other.has_skeleton_) + , skeleton_(std::move(other.skeleton_)) + , animation_clips_(std::move(other.animation_clips_)) + , default_bone_transforms_(std::move(other.default_bone_transforms_)) +{ +} + +Model3D& Model3D::operator=(Model3D&& other) noexcept +{ + if (this != &other) { + cleanupGPU(); + name_ = std::move(other.name_); + meshes_ = std::move(other.meshes_); + skinned_meshes_ = std::move(other.skinned_meshes_); + bounds_min_ = other.bounds_min_; + bounds_max_ = other.bounds_max_; + has_skeleton_ = other.has_skeleton_; + skeleton_ = std::move(other.skeleton_); + animation_clips_ = std::move(other.animation_clips_); + default_bone_transforms_ = std::move(other.default_bone_transforms_); + } + 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; + } + } + for (auto& mesh : skinned_meshes_) { + if (mesh.vbo) { + glDeleteBuffers(1, &mesh.vbo); + mesh.vbo = 0; + } + if (mesh.ebo) { + glDeleteBuffers(1, &mesh.ebo); + mesh.ebo = 0; + } + } + } +#endif + meshes_.clear(); + skinned_meshes_.clear(); +} + +void Model3D::computeBounds(const std::vector& 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& vertices, + const std::vector& indices) +{ + ModelMesh mesh; + mesh.vertex_count = static_cast(vertices.size()); + mesh.index_count = static_cast(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; + } + for (const auto& mesh : skinned_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; + } + } + for (const auto& mesh : skinned_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::cube(float size) +{ + auto model = std::make_shared(); + model->name_ = "cube"; + + float s = size * 0.5f; + + // 24 vertices (4 per face for proper normals) + std::vector 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 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::plane(float width, float depth, int segments) +{ + auto model = std::make_shared(); + model->name_ = "plane"; + + segments = std::max(1, segments); + float hw = width * 0.5f; + float hd = depth * 0.5f; + + std::vector vertices; + std::vector 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(x) / segments; + float w = static_cast(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::sphere(float radius, int segments, int rings) +{ + auto model = std::make_shared(); + model->name_ = "sphere"; + + segments = std::max(3, segments); + rings = std::max(2, rings); + + std::vector vertices; + std::vector indices; + + // Generate vertices + for (int y = 0; y <= rings; ++y) { + float v = static_cast(y) / rings; + float phi = v * PI; + + for (int x = 0; x <= segments; ++x) { + float u = static_cast(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::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(); + + // 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 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 positions; + std::vector normals; + std::vector texcoords; + std::vector colors; + std::vector joints; // Bone indices (as floats for shader compatibility) + std::vector weights; // Bone weights + + // 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; + } + } + } + else if (attr->type == cgltf_attribute_type_joints && attr->index == 0) { + // Bone indices - can be unsigned byte or unsigned short + joints.resize(accessor->count); + for (size_t v = 0; v < accessor->count; ++v) { + // Read as uint then convert to float for shader compatibility + cgltf_uint indices[4] = {0, 0, 0, 0}; + cgltf_accessor_read_uint(accessor, v, indices, 4); + joints[v].x = static_cast(indices[0]); + joints[v].y = static_cast(indices[1]); + joints[v].z = static_cast(indices[2]); + joints[v].w = static_cast(indices[3]); + } + } + else if (attr->type == cgltf_attribute_type_weights && attr->index == 0) { + // Bone weights + weights.resize(accessor->count); + for (size_t v = 0; v < accessor->count; ++v) { + cgltf_accessor_read_float(accessor, v, &weights[v].x, 4); + } + } + } + + // 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)); + } + + // Extract indices + std::vector 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(cgltf_accessor_read_index(accessor, idx)); + } + } + + // Check if this is a skinned mesh (has joints and weights) + bool isSkinned = !joints.empty() && !weights.empty() && model->has_skeleton_; + + if (isSkinned) { + // Create skinned mesh with bone data + std::vector skinnedVertices; + skinnedVertices.reserve(vertCount); + for (size_t v = 0; v < vertCount; ++v) { + SkinnedVertex sv; + sv.position = positions[v]; + sv.texcoord = texcoords[v]; + sv.normal = normals[v]; + sv.color = colors[v]; + sv.bone_ids = joints[v]; + sv.bone_weights = weights[v]; + skinnedVertices.push_back(sv); + + // Also track for bounds calculation + MeshVertex mv; + mv.position = positions[v]; + mv.texcoord = texcoords[v]; + mv.normal = normals[v]; + mv.color = colors[v]; + allVertices.push_back(mv); + } + model->skinned_meshes_.push_back(model->createSkinnedMesh(skinnedVertices, indices)); + } else { + // Interleave vertex data for regular mesh + std::vector 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); + } + model->meshes_.push_back(createMesh(vertices, indices)); + } + } + } + + // Compute bounds from all vertices + model->computeBounds(allVertices); + + // Load skeleton and animations if present + if (model->has_skeleton_) { + model->loadSkeleton(data); + model->loadAnimations(data); + } + + cgltf_free(data); + return model; +} + +// ============================================================================= +// Skeleton Loading from glTF +// ============================================================================= + +int Model3D::findJointIndex(void* cgltf_skin_ptr, void* node_ptr) +{ + cgltf_skin* skin = static_cast(cgltf_skin_ptr); + cgltf_node* node = static_cast(node_ptr); + + if (!skin || !node) return -1; + + for (size_t i = 0; i < skin->joints_count; i++) { + if (skin->joints[i] == node) { + return static_cast(i); + } + } + return -1; +} + +void Model3D::loadSkeleton(void* cgltf_data_ptr) +{ + cgltf_data* data = static_cast(cgltf_data_ptr); + if (!data || data->skins_count == 0) { + has_skeleton_ = false; + return; + } + + cgltf_skin* skin = &data->skins[0]; // Use first skin + + // Resize skeleton + skeleton_.bones.resize(skin->joints_count); + default_bone_transforms_.resize(skin->joints_count); + + // Load inverse bind matrices + if (skin->inverse_bind_matrices) { + cgltf_accessor* ibm = skin->inverse_bind_matrices; + for (size_t i = 0; i < skin->joints_count && i < ibm->count; i++) { + float mat_data[16]; + cgltf_accessor_read_float(ibm, i, mat_data, 16); + + // cgltf gives us column-major matrices (same as our mat4) + for (int j = 0; j < 16; j++) { + skeleton_.bones[i].inverse_bind_matrix.m[j] = mat_data[j]; + } + } + } + + // Load bone hierarchy + for (size_t i = 0; i < skin->joints_count; i++) { + cgltf_node* joint = skin->joints[i]; + Bone& bone = skeleton_.bones[i]; + + // Name + bone.name = joint->name ? joint->name : ("bone_" + std::to_string(i)); + + // Find parent index + bone.parent_index = findJointIndex(skin, joint->parent); + + // Track root bones + if (bone.parent_index < 0) { + skeleton_.root_bones.push_back(static_cast(i)); + } + + // Local transform + if (joint->has_matrix) { + for (int j = 0; j < 16; j++) { + bone.local_transform.m[j] = joint->matrix[j]; + } + } else { + // Compose from TRS + vec3 t(0, 0, 0); + quat r; + vec3 s(1, 1, 1); + + if (joint->has_translation) { + t = vec3(joint->translation[0], joint->translation[1], joint->translation[2]); + } + if (joint->has_rotation) { + r = quat(joint->rotation[0], joint->rotation[1], + joint->rotation[2], joint->rotation[3]); + } + if (joint->has_scale) { + s = vec3(joint->scale[0], joint->scale[1], joint->scale[2]); + } + + bone.local_transform = mat4::translate(t) * r.toMatrix() * mat4::scale(s); + } + + default_bone_transforms_[i] = bone.local_transform; + } + + has_skeleton_ = true; +} + +// ============================================================================= +// Animation Loading from glTF +// ============================================================================= + +void Model3D::loadAnimations(void* cgltf_data_ptr) +{ + cgltf_data* data = static_cast(cgltf_data_ptr); + if (!data || data->skins_count == 0) return; + + cgltf_skin* skin = &data->skins[0]; + + for (size_t i = 0; i < data->animations_count; i++) { + cgltf_animation* anim = &data->animations[i]; + + AnimationClip clip; + clip.name = anim->name ? anim->name : ("animation_" + std::to_string(i)); + clip.duration = 0.0f; + + for (size_t j = 0; j < anim->channels_count; j++) { + cgltf_animation_channel* chan = &anim->channels[j]; + cgltf_animation_sampler* sampler = chan->sampler; + + if (!sampler || !chan->target_node) continue; + + AnimationChannel channel; + channel.bone_index = findJointIndex(skin, chan->target_node); + + if (channel.bone_index < 0) continue; // Not a bone we're tracking + + // Determine path type + switch (chan->target_path) { + case cgltf_animation_path_type_translation: + channel.path = AnimationChannel::Path::Translation; + break; + case cgltf_animation_path_type_rotation: + channel.path = AnimationChannel::Path::Rotation; + break; + case cgltf_animation_path_type_scale: + channel.path = AnimationChannel::Path::Scale; + break; + default: + continue; // Skip unsupported paths (weights, etc.) + } + + // Load keyframe times + cgltf_accessor* input = sampler->input; + if (input) { + channel.times.resize(input->count); + for (size_t k = 0; k < input->count; k++) { + cgltf_accessor_read_float(input, k, &channel.times[k], 1); + } + + // Update clip duration + if (!channel.times.empty() && channel.times.back() > clip.duration) { + clip.duration = channel.times.back(); + } + } + + // Load keyframe values + cgltf_accessor* output = sampler->output; + if (output) { + switch (channel.path) { + case AnimationChannel::Path::Translation: + case AnimationChannel::Path::Scale: + { + std::vector& target = (channel.path == AnimationChannel::Path::Translation) + ? channel.translations : channel.scales; + target.resize(output->count); + for (size_t k = 0; k < output->count; k++) { + float v[3]; + cgltf_accessor_read_float(output, k, v, 3); + target[k] = vec3(v[0], v[1], v[2]); + } + break; + } + case AnimationChannel::Path::Rotation: + { + channel.rotations.resize(output->count); + for (size_t k = 0; k < output->count; k++) { + float v[4]; + cgltf_accessor_read_float(output, k, v, 4); + // glTF stores quaternions as (x, y, z, w) + channel.rotations[k] = quat(v[0], v[1], v[2], v[3]); + } + break; + } + } + } + + clip.channels.push_back(std::move(channel)); + } + + if (!clip.channels.empty()) { + animation_clips_.push_back(std::move(clip)); + } + } +} + +// ============================================================================= +// Skinned Mesh Creation +// ============================================================================= + +SkinnedMesh Model3D::createSkinnedMesh(const std::vector& vertices, + const std::vector& indices) +{ + SkinnedMesh mesh; + mesh.vertex_count = static_cast(vertices.size()); + mesh.index_count = static_cast(indices.size()); + mesh.is_skinned = true; + +#ifdef MCRF_HAS_GL + if (!gl::isGLReady()) { + return mesh; + } + + // Create VBO + glGenBuffers(1, &mesh.vbo); + glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo); + glBufferData(GL_ARRAY_BUFFER, + vertices.size() * sizeof(SkinnedVertex), + 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; +} + +// ============================================================================= +// Skinned Rendering +// ============================================================================= + +void Model3D::renderSkinned(unsigned int shader, const mat4& model, + const mat4& view, const mat4& projection, + const std::vector& bone_matrices) +{ +#ifdef MCRF_HAS_GL + if (!gl::isGLReady()) return; + + // Calculate MVP + mat4 mvp = projection * view * model; + + // Set uniforms + int mvpLoc = glGetUniformLocation(shader, "u_mvp"); + int modelLoc = glGetUniformLocation(shader, "u_model"); + int bonesLoc = glGetUniformLocation(shader, "u_bones"); + + if (mvpLoc >= 0) glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, mvp.data()); + if (modelLoc >= 0) glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.data()); + + // Upload bone matrices (max 64 bones) + if (bonesLoc >= 0 && !bone_matrices.empty()) { + int count = std::min(static_cast(bone_matrices.size()), 64); + glUniformMatrix4fv(bonesLoc, count, GL_FALSE, bone_matrices[0].data()); + } + + // For now, fall back to regular rendering for non-skinned meshes + // TODO: Add skinned mesh rendering with bone weight attributes + + // Render skinned meshes + for (const auto& mesh : skinned_meshes_) { + if (mesh.vertex_count == 0) continue; + + glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo); + + // Position (location 0) + glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION); + glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, + sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, position)); + + // Texcoord (location 1) + glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD); + glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, + sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, texcoord)); + + // Normal (location 2) + glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL); + glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE, + sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, normal)); + + // Color (location 3) + glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR); + glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE, + sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, color)); + + // Bone IDs (location 4) - as vec4 float for GLES2 compatibility + glEnableVertexAttribArray(4); + glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, + sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, bone_ids)); + + // Bone Weights (location 5) + glEnableVertexAttribArray(5); + glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, + sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, bone_weights)); + + // 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); + glDisableVertexAttribArray(4); + glDisableVertexAttribArray(5); + } + + // Also render regular meshes (may not have skinning) + for (const auto& mesh : meshes_) { + if (mesh.vertex_count == 0) continue; + + glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo); + + glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION); + glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, + sizeof(MeshVertex), (void*)offsetof(MeshVertex, position)); + + glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD); + glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, + sizeof(MeshVertex), (void*)offsetof(MeshVertex, texcoord)); + + glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL); + glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE, + sizeof(MeshVertex), (void*)offsetof(MeshVertex, normal)); + + glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR); + glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE, + sizeof(MeshVertex), (void*)offsetof(MeshVertex, color)); + + 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); + } + + glDisableVertexAttribArray(Shader3D::ATTRIB_POSITION); + glDisableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD); + glDisableVertexAttribArray(Shader3D::ATTRIB_NORMAL); + glDisableVertexAttribArray(Shader3D::ATTRIB_COLOR); + } + + glBindBuffer(GL_ARRAY_BUFFER, 0); +#endif +} + +// ============================================================================= +// 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(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(); + } + + return 0; +} + +PyObject* Model3D::repr(PyObject* self) +{ + PyModel3DObject* obj = (PyModel3DObject*)self; + if (!obj->data) { + return PyUnicode_FromString(""); + } + + char buf[256]; + snprintf(buf, sizeof(buf), "", + 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(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(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(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(obj->data->getMeshCount())); +} + +PyObject* Model3D::get_bone_count(PyObject* self, void* closure) +{ + PyModel3DObject* obj = (PyModel3DObject*)self; + if (!obj->data) { + return PyLong_FromLong(0); + } + return PyLong_FromLong(static_cast(obj->data->getBoneCount())); +} + +PyObject* Model3D::get_animation_clips(PyObject* self, void* closure) +{ + PyModel3DObject* obj = (PyModel3DObject*)self; + if (!obj->data) { + return PyList_New(0); + } + + auto names = obj->data->getAnimationClipNames(); + PyObject* list = PyList_New(names.size()); + if (!list) return NULL; + + for (size_t i = 0; i < names.size(); i++) { + PyObject* name = PyUnicode_FromString(names[i].c_str()); + if (!name) { + Py_DECREF(list); + return NULL; + } + PyList_SET_ITEM(list, i, name); // Steals reference + } + + return list; +} + +// 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}, + {"bone_count", get_bone_count, NULL, "Number of bones in skeleton (read-only)", NULL}, + {"animation_clips", get_animation_clips, NULL, "List of animation clip names (read-only)", NULL}, + {NULL} +}; + +} // namespace mcrf diff --git a/src/3d/Model3D.h b/src/3d/Model3D.h new file mode 100644 index 0000000..7fb9bb2 --- /dev/null +++ b/src/3d/Model3D.h @@ -0,0 +1,431 @@ +// 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 +#include +#include + +namespace mcrf { + +// Forward declarations +class Shader3D; + +// ============================================================================= +// Bone - Single bone in a skeleton +// ============================================================================= + +struct Bone { + std::string name; + int parent_index = -1; // -1 for root bones + mat4 inverse_bind_matrix; // Transforms from model space to bone space + mat4 local_transform; // Default local transform (rest pose) +}; + +// ============================================================================= +// Skeleton - Bone hierarchy for skeletal animation +// ============================================================================= + +struct Skeleton { + std::vector bones; + std::vector root_bones; // Indices of bones with parent_index == -1 + + /// Find bone by name, returns -1 if not found + int findBone(const std::string& name) const { + for (size_t i = 0; i < bones.size(); i++) { + if (bones[i].name == name) return static_cast(i); + } + return -1; + } + + /// Compute global (model-space) transforms for all bones + void computeGlobalTransforms(const std::vector& local_transforms, + std::vector& global_out) const { + global_out.resize(bones.size()); + for (size_t i = 0; i < bones.size(); i++) { + if (bones[i].parent_index < 0) { + global_out[i] = local_transforms[i]; + } else { + global_out[i] = global_out[bones[i].parent_index] * local_transforms[i]; + } + } + } + + /// Compute final bone matrices for shader (global * inverse_bind) + void computeBoneMatrices(const std::vector& global_transforms, + std::vector& matrices_out) const { + matrices_out.resize(bones.size()); + for (size_t i = 0; i < bones.size(); i++) { + matrices_out[i] = global_transforms[i] * bones[i].inverse_bind_matrix; + } + } +}; + +// ============================================================================= +// AnimationChannel - Animates a single property of a single bone +// ============================================================================= + +struct AnimationChannel { + int bone_index = -1; + + enum class Path { + Translation, + Rotation, + Scale + } path = Path::Translation; + + // Keyframe times (shared for all values in this channel) + std::vector times; + + // Keyframe values (only one of these is populated based on path) + std::vector translations; + std::vector rotations; + std::vector scales; + + /// Sample the channel at a given time, returning the interpolated transform component + /// For Translation/Scale: writes to trans_out + /// For Rotation: writes to rot_out + void sample(float time, vec3& trans_out, quat& rot_out, vec3& scale_out) const; +}; + +// ============================================================================= +// AnimationClip - Named animation containing multiple channels +// ============================================================================= + +struct AnimationClip { + std::string name; + float duration = 0.0f; + std::vector channels; + + /// Sample the animation at a given time, producing bone local transforms + /// @param time Current time in the animation + /// @param num_bones Total number of bones (for output sizing) + /// @param default_transforms Default local transforms for bones without animation + /// @param local_out Output: interpolated local transforms for each bone + void sample(float time, size_t num_bones, + const std::vector& default_transforms, + std::vector& local_out) const; +}; + +// ============================================================================= +// SkinnedVertex - Vertex with bone weights for skeletal animation +// ============================================================================= + +struct SkinnedVertex { + vec3 position; + vec2 texcoord; + vec3 normal; + vec4 color; + vec4 bone_ids; // Up to 4 bone indices (as floats for GLES2 compatibility) + vec4 bone_weights; // Corresponding weights (should sum to 1.0) +}; + +// ============================================================================= +// SkinnedMesh - Submesh with skinning data +// ============================================================================= + +struct SkinnedMesh { + unsigned int vbo = 0; + unsigned int ebo = 0; + int vertex_count = 0; + int index_count = 0; + int material_index = -1; + bool is_skinned = false; // True if this mesh has bone weights + + SkinnedMesh() = default; + ~SkinnedMesh() = default; + + SkinnedMesh(const SkinnedMesh&) = delete; + SkinnedMesh& operator=(const SkinnedMesh&) = delete; + SkinnedMesh(SkinnedMesh&& other) noexcept; + SkinnedMesh& operator=(SkinnedMesh&& other) noexcept; +}; + +// ============================================================================= +// ModelMesh - Single submesh within a Model3D (legacy non-skinned) +// ============================================================================= + +struct ModelMesh { + unsigned int vbo = 0; // Vertex buffer object + unsigned int ebo = 0; // Element (index) buffer object + int vertex_count = 0; // Number of vertices + int index_count = 0; // Number of indices (0 if non-indexed) + int material_index = -1; // Index into materials array (-1 = no material) + + ModelMesh() = default; + ~ModelMesh() = default; + + // Move only + ModelMesh(const ModelMesh&) = delete; + ModelMesh& operator=(const ModelMesh&) = delete; + ModelMesh(ModelMesh&& other) noexcept; + ModelMesh& operator=(ModelMesh&& other) noexcept; +}; + +// ============================================================================= +// Model3D - 3D model resource +// ============================================================================= + +class Model3D : public std::enable_shared_from_this { +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 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 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 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 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 getBounds() const { return {bounds_min_, bounds_max_}; } + + /// Check if model has skeletal animation data + bool hasSkeleton() const { return has_skeleton_; } + + /// Get number of submeshes (regular + skinned) + size_t getMeshCount() const { return meshes_.size() + skinned_meshes_.size(); } + + // ========================================================================= + // Skeleton & Animation + // ========================================================================= + + /// Get skeleton (may be empty if no skeleton) + const Skeleton& getSkeleton() const { return skeleton_; } + + /// Get number of bones + size_t getBoneCount() const { return skeleton_.bones.size(); } + + /// Get animation clips + const std::vector& getAnimationClips() const { return animation_clips_; } + + /// Get animation clip names + std::vector getAnimationClipNames() const { + std::vector names; + for (const auto& clip : animation_clips_) { + names.push_back(clip.name); + } + return names; + } + + /// Find animation clip by name (returns nullptr if not found) + const AnimationClip* findClip(const std::string& name) const { + for (const auto& clip : animation_clips_) { + if (clip.name == name) return &clip; + } + return nullptr; + } + + /// Get default bone transforms (rest pose) + const std::vector& getDefaultBoneTransforms() const { return default_bone_transforms_; } + + // ========================================================================= + // Rendering + // ========================================================================= + + /// Render all meshes + /// @param shader Shader program handle (already bound) + /// @param model Model transformation matrix + /// @param view View matrix + /// @param projection Projection matrix + void render(unsigned int shader, const mat4& model, const mat4& view, const mat4& projection); + + /// Render with skeletal animation + /// @param shader Shader program handle (already bound, should be skinned shader) + /// @param model Model transformation matrix + /// @param view View matrix + /// @param projection Projection matrix + /// @param bone_matrices Final bone matrices (global * inverse_bind) + void renderSkinned(unsigned int shader, const mat4& model, const mat4& view, + const mat4& projection, const std::vector& bone_matrices); + + // ========================================================================= + // Python API + // ========================================================================= + + static int init(PyObject* self, PyObject* args, PyObject* kwds); + static PyObject* repr(PyObject* self); + + // Class methods (static constructors) + static PyObject* py_cube(PyObject* cls, PyObject* args, PyObject* kwds); + static PyObject* py_plane(PyObject* cls, PyObject* args, PyObject* kwds); + static PyObject* py_sphere(PyObject* cls, PyObject* args, PyObject* kwds); + + // Property getters + static PyObject* get_vertex_count(PyObject* self, void* closure); + static PyObject* get_triangle_count(PyObject* self, void* closure); + static PyObject* get_has_skeleton(PyObject* self, void* closure); + static PyObject* get_bounds(PyObject* self, void* closure); + static PyObject* get_name(PyObject* self, void* closure); + static PyObject* get_mesh_count(PyObject* self, void* closure); + static PyObject* get_bone_count(PyObject* self, void* closure); + static PyObject* get_animation_clips(PyObject* self, void* closure); + + static PyMethodDef methods[]; + static PyGetSetDef getsetters[]; + +private: + // Model data + std::string name_; + std::vector meshes_; + std::vector skinned_meshes_; // Skinned meshes with bone weights + + // Bounds + vec3 bounds_min_ = vec3(0, 0, 0); + vec3 bounds_max_ = vec3(0, 0, 0); + + // Skeletal animation data + bool has_skeleton_ = false; + Skeleton skeleton_; + std::vector animation_clips_; + std::vector default_bone_transforms_; // Rest pose local transforms + + // Error handling + static std::string lastError_; + + // Helper methods + void cleanupGPU(); + void computeBounds(const std::vector& vertices); + + /// Create VBO/EBO from vertex and index data + /// @return ModelMesh with GPU resources allocated + static ModelMesh createMesh(const std::vector& vertices, + const std::vector& indices); + + /// Create VBO/EBO from skinned vertex and index data + static SkinnedMesh createSkinnedMesh(const std::vector& vertices, + const std::vector& indices); + + // glTF loading helpers + void loadSkeleton(void* cgltf_data); // void* to avoid header dependency + void loadAnimations(void* cgltf_data); + int findJointIndex(void* cgltf_skin, void* node); +}; + +} // namespace mcrf + +// ============================================================================= +// Python type definition +// ============================================================================= + +typedef struct PyModel3DObject { + PyObject_HEAD + std::shared_ptr data; + PyObject* weakreflist; +} PyModel3DObject; + +namespace mcrfpydef { + +inline PyTypeObject PyModel3DType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.Model3D", + .tp_basicsize = sizeof(PyModel3DObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)[](PyObject* self) + { + PyModel3DObject* obj = (PyModel3DObject*)self; + PyObject_GC_UnTrack(self); + if (obj->weakreflist != NULL) { + PyObject_ClearWeakRefs(self); + } + obj->data.reset(); + Py_TYPE(self)->tp_free(self); + }, + .tp_repr = mcrf::Model3D::repr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_doc = PyDoc_STR( + "Model3D(path=None)\n\n" + "A 3D model resource that can be rendered by Entity3D.\n\n" + "Args:\n" + " path (str, optional): Path to .glb file to load. If None, creates empty model.\n\n" + "Class Methods:\n" + " cube(size=1.0) -> Model3D: Create a unit cube\n" + " plane(width=1.0, depth=1.0, segments=1) -> Model3D: Create a flat plane\n" + " sphere(radius=0.5, segments=16, rings=12) -> Model3D: Create a UV sphere\n\n" + "Properties:\n" + " name (str, read-only): Model name\n" + " vertex_count (int, read-only): Total vertices across all meshes\n" + " triangle_count (int, read-only): Total triangles across all meshes\n" + " has_skeleton (bool, read-only): Whether model has skeletal animation data\n" + " bounds (tuple, read-only): AABB as ((min_x, min_y, min_z), (max_x, max_y, max_z))\n" + " mesh_count (int, read-only): Number of submeshes\n" + " bone_count (int, read-only): Number of bones in skeleton\n" + " animation_clips (list, read-only): List of animation clip names" + ), + .tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int { + return 0; + }, + .tp_clear = [](PyObject* self) -> int { + return 0; + }, + .tp_methods = mcrf::Model3D::methods, + .tp_getset = mcrf::Model3D::getsetters, + .tp_init = mcrf::Model3D::init, + .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* + { + PyModel3DObject* self = (PyModel3DObject*)type->tp_alloc(type, 0); + if (self) { + self->data = std::make_shared(); + self->weakreflist = nullptr; + } + return (PyObject*)self; + } +}; + +} // namespace mcrfpydef diff --git a/src/3d/PyVoxelGrid.cpp b/src/3d/PyVoxelGrid.cpp new file mode 100644 index 0000000..f3d4a65 --- /dev/null +++ b/src/3d/PyVoxelGrid.cpp @@ -0,0 +1,1271 @@ +// PyVoxelGrid.cpp - Python bindings for VoxelGrid implementation +// Part of McRogueFace 3D Extension - Milestone 9 + +#include "PyVoxelGrid.h" +#include "../McRFPy_API.h" +#include "../PyColor.h" +#include + +// ============================================================================= +// Python type interface +// ============================================================================= + +PyObject* PyVoxelGrid::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { + PyVoxelGridObject* self = (PyVoxelGridObject*)type->tp_alloc(type, 0); + if (self) { + self->data = nullptr; // Will be initialized in init + self->weakreflist = nullptr; + } + return (PyObject*)self; +} + +int PyVoxelGrid::init(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = {"size", "cell_size", nullptr}; + + PyObject* size_obj = nullptr; + float cell_size = 1.0f; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast(kwlist), + &size_obj, &cell_size)) { + return -1; + } + + // Parse size tuple + if (!PyTuple_Check(size_obj) && !PyList_Check(size_obj)) { + PyErr_SetString(PyExc_TypeError, "size must be a tuple or list of 3 integers"); + return -1; + } + + if (PySequence_Size(size_obj) != 3) { + PyErr_SetString(PyExc_ValueError, "size must have exactly 3 elements (width, height, depth)"); + return -1; + } + + int width = 0, height = 0, depth = 0; + + PyObject* w_obj = PySequence_GetItem(size_obj, 0); + PyObject* h_obj = PySequence_GetItem(size_obj, 1); + PyObject* d_obj = PySequence_GetItem(size_obj, 2); + + bool valid = true; + if (PyLong_Check(w_obj)) width = (int)PyLong_AsLong(w_obj); else valid = false; + if (PyLong_Check(h_obj)) height = (int)PyLong_AsLong(h_obj); else valid = false; + if (PyLong_Check(d_obj)) depth = (int)PyLong_AsLong(d_obj); else valid = false; + + Py_DECREF(w_obj); + Py_DECREF(h_obj); + Py_DECREF(d_obj); + + if (!valid) { + PyErr_SetString(PyExc_TypeError, "size elements must be integers"); + return -1; + } + + if (width <= 0 || height <= 0 || depth <= 0) { + PyErr_SetString(PyExc_ValueError, "size dimensions must be positive"); + return -1; + } + + if (cell_size <= 0.0f) { + PyErr_SetString(PyExc_ValueError, "cell_size must be positive"); + return -1; + } + + // Create the VoxelGrid + try { + self->data = std::make_shared(width, height, depth, cell_size); + } catch (const std::exception& e) { + PyErr_SetString(PyExc_RuntimeError, e.what()); + return -1; + } + + return 0; +} + +void PyVoxelGrid::dealloc(PyVoxelGridObject* self) { + PyObject_GC_UnTrack(self); + if (self->weakreflist != nullptr) { + PyObject_ClearWeakRefs((PyObject*)self); + } + self->data.reset(); + Py_TYPE(self)->tp_free((PyObject*)self); +} + +PyObject* PyVoxelGrid::repr(PyObject* obj) { + PyVoxelGridObject* self = (PyVoxelGridObject*)obj; + if (!self->data) { + return PyUnicode_FromString(""); + } + + std::ostringstream oss; + oss << "data->width() << "x" + << self->data->height() << "x" << self->data->depth() + << " cells=" << self->data->totalVoxels() + << " materials=" << self->data->materialCount() + << " non_air=" << self->data->countNonAir() << ">"; + return PyUnicode_FromString(oss.str().c_str()); +} + +// ============================================================================= +// Properties - dimensions (read-only) +// ============================================================================= + +PyObject* PyVoxelGrid::get_size(PyVoxelGridObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + return Py_BuildValue("(iii)", self->data->width(), self->data->height(), self->data->depth()); +} + +PyObject* PyVoxelGrid::get_width(PyVoxelGridObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + return PyLong_FromLong(self->data->width()); +} + +PyObject* PyVoxelGrid::get_height(PyVoxelGridObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + return PyLong_FromLong(self->data->height()); +} + +PyObject* PyVoxelGrid::get_depth(PyVoxelGridObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + return PyLong_FromLong(self->data->depth()); +} + +PyObject* PyVoxelGrid::get_cell_size(PyVoxelGridObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + return PyFloat_FromDouble(self->data->cellSize()); +} + +PyObject* PyVoxelGrid::get_material_count(PyVoxelGridObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + return PyLong_FromSize_t(self->data->materialCount()); +} + +// ============================================================================= +// Properties - transform (read-write) +// ============================================================================= + +PyObject* PyVoxelGrid::get_offset(PyVoxelGridObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + mcrf::vec3 offset = self->data->getOffset(); + return Py_BuildValue("(fff)", offset.x, offset.y, offset.z); +} + +int PyVoxelGrid::set_offset(PyVoxelGridObject* self, PyObject* value, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return -1; + } + + if (!PyTuple_Check(value) && !PyList_Check(value)) { + PyErr_SetString(PyExc_TypeError, "offset must be a tuple or list of 3 floats"); + return -1; + } + + if (PySequence_Size(value) != 3) { + PyErr_SetString(PyExc_ValueError, "offset must have exactly 3 elements (x, y, z)"); + return -1; + } + + float x = 0, y = 0, z = 0; + PyObject* x_obj = PySequence_GetItem(value, 0); + PyObject* y_obj = PySequence_GetItem(value, 1); + PyObject* z_obj = PySequence_GetItem(value, 2); + + bool valid = true; + if (PyNumber_Check(x_obj)) x = (float)PyFloat_AsDouble(PyNumber_Float(x_obj)); else valid = false; + if (PyNumber_Check(y_obj)) y = (float)PyFloat_AsDouble(PyNumber_Float(y_obj)); else valid = false; + if (PyNumber_Check(z_obj)) z = (float)PyFloat_AsDouble(PyNumber_Float(z_obj)); else valid = false; + + Py_DECREF(x_obj); + Py_DECREF(y_obj); + Py_DECREF(z_obj); + + if (!valid) { + PyErr_SetString(PyExc_TypeError, "offset elements must be numbers"); + return -1; + } + + self->data->setOffset(x, y, z); + return 0; +} + +PyObject* PyVoxelGrid::get_rotation(PyVoxelGridObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + return PyFloat_FromDouble(self->data->getRotation()); +} + +int PyVoxelGrid::set_rotation(PyVoxelGridObject* self, PyObject* value, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return -1; + } + + if (!PyNumber_Check(value)) { + PyErr_SetString(PyExc_TypeError, "rotation must be a number"); + return -1; + } + + float rotation = (float)PyFloat_AsDouble(PyNumber_Float(value)); + self->data->setRotation(rotation); + return 0; +} + +// Greedy meshing (Milestone 13) +PyObject* PyVoxelGrid::get_greedy_meshing(PyVoxelGridObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + return PyBool_FromLong(self->data->isGreedyMeshingEnabled()); +} + +int PyVoxelGrid::set_greedy_meshing(PyVoxelGridObject* self, PyObject* value, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return -1; + } + + if (!PyBool_Check(value)) { + PyErr_SetString(PyExc_TypeError, "greedy_meshing must be a boolean"); + return -1; + } + + bool enabled = (value == Py_True); + self->data->setGreedyMeshing(enabled); + return 0; +} + +// ============================================================================= +// Voxel access methods +// ============================================================================= + +PyObject* PyVoxelGrid::get(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + int x, y, z; + if (!PyArg_ParseTuple(args, "iii", &x, &y, &z)) { + return nullptr; + } + + // Bounds check with warning (returns 0 for out-of-bounds, like C++ API) + if (!self->data->isValid(x, y, z)) { + // Return 0 (air) for out-of-bounds, matching C++ behavior + return PyLong_FromLong(0); + } + + return PyLong_FromLong(self->data->get(x, y, z)); +} + +PyObject* PyVoxelGrid::set(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + int x, y, z, material; + if (!PyArg_ParseTuple(args, "iiii", &x, &y, &z, &material)) { + return nullptr; + } + + if (material < 0 || material > 255) { + PyErr_SetString(PyExc_ValueError, "material must be 0-255"); + return nullptr; + } + + // Bounds check - silently ignore out-of-bounds, like C++ API + self->data->set(x, y, z, static_cast(material)); + Py_RETURN_NONE; +} + +// ============================================================================= +// Material methods +// ============================================================================= + +PyObject* PyVoxelGrid::add_material(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + static const char* kwlist[] = {"name", "color", "sprite_index", "transparent", "path_cost", nullptr}; + + const char* name = nullptr; + PyObject* color_obj = nullptr; + int sprite_index = -1; + int transparent = 0; // Python bool maps to int + float path_cost = 1.0f; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|Oipf", const_cast(kwlist), + &name, &color_obj, &sprite_index, &transparent, &path_cost)) { + return nullptr; + } + + // Default color is white + sf::Color color = sf::Color::White; + + // Parse color if provided + if (color_obj && color_obj != Py_None) { + color = PyColor::fromPy(color_obj); + if (PyErr_Occurred()) { + return nullptr; + } + } + + try { + uint8_t id = self->data->addMaterial(name, color, sprite_index, transparent != 0, path_cost); + return PyLong_FromLong(id); + } catch (const std::exception& e) { + PyErr_SetString(PyExc_RuntimeError, e.what()); + return nullptr; + } +} + +PyObject* PyVoxelGrid::get_material(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + int id; + if (!PyArg_ParseTuple(args, "i", &id)) { + return nullptr; + } + + if (id < 0 || id > 255) { + PyErr_SetString(PyExc_ValueError, "material id must be 0-255"); + return nullptr; + } + + const mcrf::VoxelMaterial& mat = self->data->getMaterial(static_cast(id)); + + // Create color object + PyObject* color_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"); + if (!color_type) { + return nullptr; + } + + PyObject* color_obj = PyObject_Call(color_type, + Py_BuildValue("(iiii)", mat.color.r, mat.color.g, mat.color.b, mat.color.a), + nullptr); + Py_DECREF(color_type); + + if (!color_obj) { + return nullptr; + } + + // Build result dict + PyObject* result = Py_BuildValue("{s:s, s:N, s:i, s:O, s:f}", + "name", mat.name.c_str(), + "color", color_obj, + "sprite_index", mat.spriteIndex, + "transparent", mat.transparent ? Py_True : Py_False, + "path_cost", mat.pathCost); + + return result; +} + +// ============================================================================= +// Bulk operations +// ============================================================================= + +PyObject* PyVoxelGrid::fill(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + int material; + if (!PyArg_ParseTuple(args, "i", &material)) { + return nullptr; + } + + if (material < 0 || material > 255) { + PyErr_SetString(PyExc_ValueError, "material must be 0-255"); + return nullptr; + } + + self->data->fill(static_cast(material)); + Py_RETURN_NONE; +} + +PyObject* PyVoxelGrid::clear(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + self->data->clear(); + Py_RETURN_NONE; +} + +PyObject* PyVoxelGrid::fill_box(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + PyObject* min_obj = nullptr; + PyObject* max_obj = nullptr; + int material; + + if (!PyArg_ParseTuple(args, "OOi", &min_obj, &max_obj, &material)) { + return nullptr; + } + + if (material < 0 || material > 255) { + PyErr_SetString(PyExc_ValueError, "material must be 0-255"); + return nullptr; + } + + // Parse min tuple (x0, y0, z0) + if (!PyTuple_Check(min_obj) && !PyList_Check(min_obj)) { + PyErr_SetString(PyExc_TypeError, "min_coord must be a tuple or list of 3 integers"); + return nullptr; + } + if (PySequence_Size(min_obj) != 3) { + PyErr_SetString(PyExc_ValueError, "min_coord must have exactly 3 elements"); + return nullptr; + } + + // Parse max tuple (x1, y1, z1) + if (!PyTuple_Check(max_obj) && !PyList_Check(max_obj)) { + PyErr_SetString(PyExc_TypeError, "max_coord must be a tuple or list of 3 integers"); + return nullptr; + } + if (PySequence_Size(max_obj) != 3) { + PyErr_SetString(PyExc_ValueError, "max_coord must have exactly 3 elements"); + return nullptr; + } + + int x0, y0, z0, x1, y1, z1; + PyObject* items[6]; + + items[0] = PySequence_GetItem(min_obj, 0); + items[1] = PySequence_GetItem(min_obj, 1); + items[2] = PySequence_GetItem(min_obj, 2); + items[3] = PySequence_GetItem(max_obj, 0); + items[4] = PySequence_GetItem(max_obj, 1); + items[5] = PySequence_GetItem(max_obj, 2); + + bool valid = true; + if (PyLong_Check(items[0])) x0 = (int)PyLong_AsLong(items[0]); else valid = false; + if (PyLong_Check(items[1])) y0 = (int)PyLong_AsLong(items[1]); else valid = false; + if (PyLong_Check(items[2])) z0 = (int)PyLong_AsLong(items[2]); else valid = false; + if (PyLong_Check(items[3])) x1 = (int)PyLong_AsLong(items[3]); else valid = false; + if (PyLong_Check(items[4])) y1 = (int)PyLong_AsLong(items[4]); else valid = false; + if (PyLong_Check(items[5])) z1 = (int)PyLong_AsLong(items[5]); else valid = false; + + for (int i = 0; i < 6; i++) Py_DECREF(items[i]); + + if (!valid) { + PyErr_SetString(PyExc_TypeError, "coordinate elements must be integers"); + return nullptr; + } + + self->data->fillBox(x0, y0, z0, x1, y1, z1, static_cast(material)); + Py_RETURN_NONE; +} + +// ============================================================================= +// Mesh caching methods (Milestone 10) +// ============================================================================= + +PyObject* PyVoxelGrid::get_vertex_count(PyVoxelGridObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + return PyLong_FromSize_t(self->data->vertexCount()); +} + +PyObject* PyVoxelGrid::rebuild_mesh(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + self->data->rebuildMesh(); + Py_RETURN_NONE; +} + +// ============================================================================= +// Statistics +// ============================================================================= + +PyObject* PyVoxelGrid::count_non_air(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + return PyLong_FromSize_t(self->data->countNonAir()); +} + +PyObject* PyVoxelGrid::count_material(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + int material; + if (!PyArg_ParseTuple(args, "i", &material)) { + return nullptr; + } + + if (material < 0 || material > 255) { + PyErr_SetString(PyExc_ValueError, "material must be 0-255"); + return nullptr; + } + + return PyLong_FromSize_t(self->data->countMaterial(static_cast(material))); +} + +// ============================================================================= +// Bulk operations - Milestone 11 +// ============================================================================= + +PyObject* PyVoxelGrid::fill_box_hollow(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + static const char* kwlist[] = {"min_coord", "max_coord", "material", "thickness", nullptr}; + + PyObject* min_obj = nullptr; + PyObject* max_obj = nullptr; + int material; + int thickness = 1; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi|i", const_cast(kwlist), + &min_obj, &max_obj, &material, &thickness)) { + return nullptr; + } + + if (material < 0 || material > 255) { + PyErr_SetString(PyExc_ValueError, "material must be 0-255"); + return nullptr; + } + + if (thickness < 1) { + PyErr_SetString(PyExc_ValueError, "thickness must be >= 1"); + return nullptr; + } + + // Parse coordinates + if (!PyTuple_Check(min_obj) && !PyList_Check(min_obj)) { + PyErr_SetString(PyExc_TypeError, "min_coord must be a tuple or list of 3 integers"); + return nullptr; + } + if (!PyTuple_Check(max_obj) && !PyList_Check(max_obj)) { + PyErr_SetString(PyExc_TypeError, "max_coord must be a tuple or list of 3 integers"); + return nullptr; + } + if (PySequence_Size(min_obj) != 3 || PySequence_Size(max_obj) != 3) { + PyErr_SetString(PyExc_ValueError, "coordinates must have exactly 3 elements"); + return nullptr; + } + + int x0, y0, z0, x1, y1, z1; + PyObject* items[6]; + items[0] = PySequence_GetItem(min_obj, 0); + items[1] = PySequence_GetItem(min_obj, 1); + items[2] = PySequence_GetItem(min_obj, 2); + items[3] = PySequence_GetItem(max_obj, 0); + items[4] = PySequence_GetItem(max_obj, 1); + items[5] = PySequence_GetItem(max_obj, 2); + + bool valid = true; + if (PyLong_Check(items[0])) x0 = (int)PyLong_AsLong(items[0]); else valid = false; + if (PyLong_Check(items[1])) y0 = (int)PyLong_AsLong(items[1]); else valid = false; + if (PyLong_Check(items[2])) z0 = (int)PyLong_AsLong(items[2]); else valid = false; + if (PyLong_Check(items[3])) x1 = (int)PyLong_AsLong(items[3]); else valid = false; + if (PyLong_Check(items[4])) y1 = (int)PyLong_AsLong(items[4]); else valid = false; + if (PyLong_Check(items[5])) z1 = (int)PyLong_AsLong(items[5]); else valid = false; + + for (int i = 0; i < 6; i++) Py_DECREF(items[i]); + + if (!valid) { + PyErr_SetString(PyExc_TypeError, "coordinate elements must be integers"); + return nullptr; + } + + self->data->fillBoxHollow(x0, y0, z0, x1, y1, z1, static_cast(material), thickness); + Py_RETURN_NONE; +} + +PyObject* PyVoxelGrid::fill_sphere(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + PyObject* center_obj = nullptr; + int radius; + int material; + + if (!PyArg_ParseTuple(args, "Oii", ¢er_obj, &radius, &material)) { + return nullptr; + } + + if (material < 0 || material > 255) { + PyErr_SetString(PyExc_ValueError, "material must be 0-255"); + return nullptr; + } + + if (radius < 0) { + PyErr_SetString(PyExc_ValueError, "radius must be >= 0"); + return nullptr; + } + + if (!PyTuple_Check(center_obj) && !PyList_Check(center_obj)) { + PyErr_SetString(PyExc_TypeError, "center must be a tuple or list of 3 integers"); + return nullptr; + } + if (PySequence_Size(center_obj) != 3) { + PyErr_SetString(PyExc_ValueError, "center must have exactly 3 elements"); + return nullptr; + } + + int cx, cy, cz; + PyObject* items[3]; + items[0] = PySequence_GetItem(center_obj, 0); + items[1] = PySequence_GetItem(center_obj, 1); + items[2] = PySequence_GetItem(center_obj, 2); + + bool valid = true; + if (PyLong_Check(items[0])) cx = (int)PyLong_AsLong(items[0]); else valid = false; + if (PyLong_Check(items[1])) cy = (int)PyLong_AsLong(items[1]); else valid = false; + if (PyLong_Check(items[2])) cz = (int)PyLong_AsLong(items[2]); else valid = false; + + for (int i = 0; i < 3; i++) Py_DECREF(items[i]); + + if (!valid) { + PyErr_SetString(PyExc_TypeError, "center elements must be integers"); + return nullptr; + } + + self->data->fillSphere(cx, cy, cz, radius, static_cast(material)); + Py_RETURN_NONE; +} + +PyObject* PyVoxelGrid::fill_cylinder(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + PyObject* base_obj = nullptr; + int radius; + int height; + int material; + + if (!PyArg_ParseTuple(args, "Oiii", &base_obj, &radius, &height, &material)) { + return nullptr; + } + + if (material < 0 || material > 255) { + PyErr_SetString(PyExc_ValueError, "material must be 0-255"); + return nullptr; + } + + if (radius < 0) { + PyErr_SetString(PyExc_ValueError, "radius must be >= 0"); + return nullptr; + } + + if (height < 1) { + PyErr_SetString(PyExc_ValueError, "height must be >= 1"); + return nullptr; + } + + if (!PyTuple_Check(base_obj) && !PyList_Check(base_obj)) { + PyErr_SetString(PyExc_TypeError, "base_pos must be a tuple or list of 3 integers"); + return nullptr; + } + if (PySequence_Size(base_obj) != 3) { + PyErr_SetString(PyExc_ValueError, "base_pos must have exactly 3 elements"); + return nullptr; + } + + int cx, cy, cz; + PyObject* items[3]; + items[0] = PySequence_GetItem(base_obj, 0); + items[1] = PySequence_GetItem(base_obj, 1); + items[2] = PySequence_GetItem(base_obj, 2); + + bool valid = true; + if (PyLong_Check(items[0])) cx = (int)PyLong_AsLong(items[0]); else valid = false; + if (PyLong_Check(items[1])) cy = (int)PyLong_AsLong(items[1]); else valid = false; + if (PyLong_Check(items[2])) cz = (int)PyLong_AsLong(items[2]); else valid = false; + + for (int i = 0; i < 3; i++) Py_DECREF(items[i]); + + if (!valid) { + PyErr_SetString(PyExc_TypeError, "base_pos elements must be integers"); + return nullptr; + } + + self->data->fillCylinder(cx, cy, cz, radius, height, static_cast(material)); + Py_RETURN_NONE; +} + +PyObject* PyVoxelGrid::fill_noise(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + static const char* kwlist[] = {"min_coord", "max_coord", "material", "threshold", "scale", "seed", nullptr}; + + PyObject* min_obj = nullptr; + PyObject* max_obj = nullptr; + int material; + float threshold = 0.5f; + float scale = 0.1f; + unsigned int seed = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi|ffI", const_cast(kwlist), + &min_obj, &max_obj, &material, &threshold, &scale, &seed)) { + return nullptr; + } + + if (material < 0 || material > 255) { + PyErr_SetString(PyExc_ValueError, "material must be 0-255"); + return nullptr; + } + + // Parse coordinates + if (!PyTuple_Check(min_obj) && !PyList_Check(min_obj)) { + PyErr_SetString(PyExc_TypeError, "min_coord must be a tuple or list of 3 integers"); + return nullptr; + } + if (!PyTuple_Check(max_obj) && !PyList_Check(max_obj)) { + PyErr_SetString(PyExc_TypeError, "max_coord must be a tuple or list of 3 integers"); + return nullptr; + } + if (PySequence_Size(min_obj) != 3 || PySequence_Size(max_obj) != 3) { + PyErr_SetString(PyExc_ValueError, "coordinates must have exactly 3 elements"); + return nullptr; + } + + int x0, y0, z0, x1, y1, z1; + PyObject* items[6]; + items[0] = PySequence_GetItem(min_obj, 0); + items[1] = PySequence_GetItem(min_obj, 1); + items[2] = PySequence_GetItem(min_obj, 2); + items[3] = PySequence_GetItem(max_obj, 0); + items[4] = PySequence_GetItem(max_obj, 1); + items[5] = PySequence_GetItem(max_obj, 2); + + bool valid = true; + if (PyLong_Check(items[0])) x0 = (int)PyLong_AsLong(items[0]); else valid = false; + if (PyLong_Check(items[1])) y0 = (int)PyLong_AsLong(items[1]); else valid = false; + if (PyLong_Check(items[2])) z0 = (int)PyLong_AsLong(items[2]); else valid = false; + if (PyLong_Check(items[3])) x1 = (int)PyLong_AsLong(items[3]); else valid = false; + if (PyLong_Check(items[4])) y1 = (int)PyLong_AsLong(items[4]); else valid = false; + if (PyLong_Check(items[5])) z1 = (int)PyLong_AsLong(items[5]); else valid = false; + + for (int i = 0; i < 6; i++) Py_DECREF(items[i]); + + if (!valid) { + PyErr_SetString(PyExc_TypeError, "coordinate elements must be integers"); + return nullptr; + } + + self->data->fillNoise(x0, y0, z0, x1, y1, z1, static_cast(material), threshold, scale, seed); + Py_RETURN_NONE; +} + +// ============================================================================= +// Copy/paste operations - Milestone 11 +// ============================================================================= + +PyObject* PyVoxelGrid::copy_region(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + PyObject* min_obj = nullptr; + PyObject* max_obj = nullptr; + + if (!PyArg_ParseTuple(args, "OO", &min_obj, &max_obj)) { + return nullptr; + } + + // Parse coordinates + if (!PyTuple_Check(min_obj) && !PyList_Check(min_obj)) { + PyErr_SetString(PyExc_TypeError, "min_coord must be a tuple or list of 3 integers"); + return nullptr; + } + if (!PyTuple_Check(max_obj) && !PyList_Check(max_obj)) { + PyErr_SetString(PyExc_TypeError, "max_coord must be a tuple or list of 3 integers"); + return nullptr; + } + if (PySequence_Size(min_obj) != 3 || PySequence_Size(max_obj) != 3) { + PyErr_SetString(PyExc_ValueError, "coordinates must have exactly 3 elements"); + return nullptr; + } + + int x0, y0, z0, x1, y1, z1; + PyObject* items[6]; + items[0] = PySequence_GetItem(min_obj, 0); + items[1] = PySequence_GetItem(min_obj, 1); + items[2] = PySequence_GetItem(min_obj, 2); + items[3] = PySequence_GetItem(max_obj, 0); + items[4] = PySequence_GetItem(max_obj, 1); + items[5] = PySequence_GetItem(max_obj, 2); + + bool valid = true; + if (PyLong_Check(items[0])) x0 = (int)PyLong_AsLong(items[0]); else valid = false; + if (PyLong_Check(items[1])) y0 = (int)PyLong_AsLong(items[1]); else valid = false; + if (PyLong_Check(items[2])) z0 = (int)PyLong_AsLong(items[2]); else valid = false; + if (PyLong_Check(items[3])) x1 = (int)PyLong_AsLong(items[3]); else valid = false; + if (PyLong_Check(items[4])) y1 = (int)PyLong_AsLong(items[4]); else valid = false; + if (PyLong_Check(items[5])) z1 = (int)PyLong_AsLong(items[5]); else valid = false; + + for (int i = 0; i < 6; i++) Py_DECREF(items[i]); + + if (!valid) { + PyErr_SetString(PyExc_TypeError, "coordinate elements must be integers"); + return nullptr; + } + + // Copy the region + mcrf::VoxelRegion region = self->data->copyRegion(x0, y0, z0, x1, y1, z1); + + // Create Python object + PyVoxelRegionObject* result = (PyVoxelRegionObject*)mcrfpydef::PyVoxelRegionType.tp_alloc( + &mcrfpydef::PyVoxelRegionType, 0); + if (!result) { + return nullptr; + } + + result->data = std::make_shared(std::move(region)); + return (PyObject*)result; +} + +PyObject* PyVoxelGrid::paste_region(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + static const char* kwlist[] = {"region", "position", "skip_air", nullptr}; + + PyObject* region_obj = nullptr; + PyObject* pos_obj = nullptr; + int skip_air = 1; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|p", const_cast(kwlist), + ®ion_obj, &pos_obj, &skip_air)) { + return nullptr; + } + + // Check region type + if (!PyObject_TypeCheck(region_obj, &mcrfpydef::PyVoxelRegionType)) { + PyErr_SetString(PyExc_TypeError, "region must be a VoxelRegion object"); + return nullptr; + } + + PyVoxelRegionObject* py_region = (PyVoxelRegionObject*)region_obj; + if (!py_region->data || !py_region->data->isValid()) { + PyErr_SetString(PyExc_ValueError, "VoxelRegion is empty or invalid"); + return nullptr; + } + + // Parse position + if (!PyTuple_Check(pos_obj) && !PyList_Check(pos_obj)) { + PyErr_SetString(PyExc_TypeError, "position must be a tuple or list of 3 integers"); + return nullptr; + } + if (PySequence_Size(pos_obj) != 3) { + PyErr_SetString(PyExc_ValueError, "position must have exactly 3 elements"); + return nullptr; + } + + int x, y, z; + PyObject* items[3]; + items[0] = PySequence_GetItem(pos_obj, 0); + items[1] = PySequence_GetItem(pos_obj, 1); + items[2] = PySequence_GetItem(pos_obj, 2); + + bool valid = true; + if (PyLong_Check(items[0])) x = (int)PyLong_AsLong(items[0]); else valid = false; + if (PyLong_Check(items[1])) y = (int)PyLong_AsLong(items[1]); else valid = false; + if (PyLong_Check(items[2])) z = (int)PyLong_AsLong(items[2]); else valid = false; + + for (int i = 0; i < 3; i++) Py_DECREF(items[i]); + + if (!valid) { + PyErr_SetString(PyExc_TypeError, "position elements must be integers"); + return nullptr; + } + + self->data->pasteRegion(*py_region->data, x, y, z, skip_air != 0); + Py_RETURN_NONE; +} + +// ============================================================================= +// Serialization (Milestone 14) +// ============================================================================= + +PyObject* PyVoxelGrid::save(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + const char* path = nullptr; + if (!PyArg_ParseTuple(args, "s", &path)) { + return nullptr; + } + + if (self->data->save(path)) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + +PyObject* PyVoxelGrid::load(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + const char* path = nullptr; + if (!PyArg_ParseTuple(args, "s", &path)) { + return nullptr; + } + + if (self->data->load(path)) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + +PyObject* PyVoxelGrid::to_bytes(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + std::vector buffer; + if (!self->data->saveToBuffer(buffer)) { + PyErr_SetString(PyExc_RuntimeError, "Failed to serialize VoxelGrid"); + return nullptr; + } + + return PyBytes_FromStringAndSize(reinterpret_cast(buffer.data()), buffer.size()); +} + +PyObject* PyVoxelGrid::from_bytes(PyVoxelGridObject* self, PyObject* args) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + Py_buffer buffer; + if (!PyArg_ParseTuple(args, "y*", &buffer)) { + return nullptr; + } + + bool success = self->data->loadFromBuffer( + static_cast(buffer.buf), buffer.len); + + PyBuffer_Release(&buffer); + + if (success) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + +// ============================================================================= +// Navigation Projection (Milestone 12) +// ============================================================================= + +static PyObject* project_column(PyVoxelGridObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = {"x", "z", "headroom", nullptr}; + int x = 0, z = 0, headroom = 2; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|i", const_cast(kwlist), + &x, &z, &headroom)) { + return nullptr; + } + + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid not initialized"); + return nullptr; + } + + if (headroom < 0) { + PyErr_SetString(PyExc_ValueError, "headroom must be non-negative"); + return nullptr; + } + + mcrf::VoxelGrid::NavInfo nav = self->data->projectColumn(x, z, headroom); + + // Return as dictionary with nav info + return Py_BuildValue("{s:f,s:O,s:O,s:f}", + "height", nav.height, + "walkable", nav.walkable ? Py_True : Py_False, + "transparent", nav.transparent ? Py_True : Py_False, + "path_cost", nav.pathCost); +} + +// ============================================================================= +// PyVoxelRegion implementation +// ============================================================================= + +void PyVoxelRegion::dealloc(PyVoxelRegionObject* self) { + self->data.reset(); + Py_TYPE(self)->tp_free((PyObject*)self); +} + +PyObject* PyVoxelRegion::repr(PyObject* obj) { + PyVoxelRegionObject* self = (PyVoxelRegionObject*)obj; + if (!self->data || !self->data->isValid()) { + return PyUnicode_FromString(""); + } + + std::ostringstream oss; + oss << "data->width << "x" + << self->data->height << "x" << self->data->depth + << " voxels=" << self->data->totalVoxels() << ">"; + return PyUnicode_FromString(oss.str().c_str()); +} + +PyObject* PyVoxelRegion::get_size(PyVoxelRegionObject* self, void* closure) { + if (!self->data || !self->data->isValid()) { + return Py_BuildValue("(iii)", 0, 0, 0); + } + return Py_BuildValue("(iii)", self->data->width, self->data->height, self->data->depth); +} + +PyObject* PyVoxelRegion::get_width(PyVoxelRegionObject* self, void* closure) { + if (!self->data) return PyLong_FromLong(0); + return PyLong_FromLong(self->data->width); +} + +PyObject* PyVoxelRegion::get_height(PyVoxelRegionObject* self, void* closure) { + if (!self->data) return PyLong_FromLong(0); + return PyLong_FromLong(self->data->height); +} + +PyObject* PyVoxelRegion::get_depth(PyVoxelRegionObject* self, void* closure) { + if (!self->data) return PyLong_FromLong(0); + return PyLong_FromLong(self->data->depth); +} + +PyGetSetDef PyVoxelRegion::getsetters[] = { + {"size", (getter)get_size, nullptr, + "Dimensions (width, height, depth) of the region. Read-only.", nullptr}, + {"width", (getter)get_width, nullptr, "Region width. Read-only.", nullptr}, + {"height", (getter)get_height, nullptr, "Region height. Read-only.", nullptr}, + {"depth", (getter)get_depth, nullptr, "Region depth. Read-only.", nullptr}, + {nullptr} // Sentinel +}; + +// ============================================================================= +// Method definitions +// ============================================================================= + +PyMethodDef PyVoxelGrid::methods[] = { + {"get", (PyCFunction)get, METH_VARARGS, + "get(x, y, z) -> int\n\n" + "Get the material ID at integer coordinates.\n\n" + "Returns 0 (air) for out-of-bounds coordinates."}, + {"set", (PyCFunction)set, METH_VARARGS, + "set(x, y, z, material) -> None\n\n" + "Set the material ID at integer coordinates.\n\n" + "Out-of-bounds coordinates are silently ignored."}, + {"add_material", (PyCFunction)add_material, METH_VARARGS | METH_KEYWORDS, + "add_material(name, color=Color(255,255,255), sprite_index=-1, transparent=False, path_cost=1.0) -> int\n\n" + "Add a new material to the palette. Returns the material ID (1-indexed).\n\n" + "Material 0 is always air (implicit, never stored in palette).\n" + "Maximum 255 materials can be added."}, + {"get_material", (PyCFunction)get_material, METH_VARARGS, + "get_material(id) -> dict\n\n" + "Get material properties by ID.\n\n" + "Returns dict with keys: name, color, sprite_index, transparent, path_cost.\n" + "ID 0 returns the implicit air material."}, + {"fill", (PyCFunction)fill, METH_VARARGS, + "fill(material) -> None\n\n" + "Fill the entire grid with the specified material ID."}, + {"fill_box", (PyCFunction)fill_box, METH_VARARGS, + "fill_box(min_coord, max_coord, material) -> None\n\n" + "Fill a rectangular region with the specified material.\n\n" + "Args:\n" + " min_coord: (x0, y0, z0) - minimum corner (inclusive)\n" + " max_coord: (x1, y1, z1) - maximum corner (inclusive)\n" + " material: material ID (0-255)\n\n" + "Coordinates are clamped to grid bounds."}, + {"fill_box_hollow", (PyCFunction)fill_box_hollow, METH_VARARGS | METH_KEYWORDS, + "fill_box_hollow(min_coord, max_coord, material, thickness=1) -> None\n\n" + "Create a hollow rectangular room (walls only, hollow inside).\n\n" + "Args:\n" + " min_coord: (x0, y0, z0) - minimum corner (inclusive)\n" + " max_coord: (x1, y1, z1) - maximum corner (inclusive)\n" + " material: material ID for walls (0-255)\n" + " thickness: wall thickness in voxels (default 1)"}, + {"fill_sphere", (PyCFunction)fill_sphere, METH_VARARGS, + "fill_sphere(center, radius, material) -> None\n\n" + "Fill a spherical region.\n\n" + "Args:\n" + " center: (cx, cy, cz) - sphere center coordinates\n" + " radius: sphere radius in voxels\n" + " material: material ID (0-255, use 0 to carve)"}, + {"fill_cylinder", (PyCFunction)fill_cylinder, METH_VARARGS, + "fill_cylinder(base_pos, radius, height, material) -> None\n\n" + "Fill a vertical cylinder (Y-axis aligned).\n\n" + "Args:\n" + " base_pos: (cx, cy, cz) - base center position\n" + " radius: cylinder radius in voxels\n" + " height: cylinder height in voxels\n" + " material: material ID (0-255)"}, + {"fill_noise", (PyCFunction)fill_noise, METH_VARARGS | METH_KEYWORDS, + "fill_noise(min_coord, max_coord, material, threshold=0.5, scale=0.1, seed=0) -> None\n\n" + "Fill region with 3D noise-based pattern (caves, clouds).\n\n" + "Args:\n" + " min_coord: (x0, y0, z0) - minimum corner\n" + " max_coord: (x1, y1, z1) - maximum corner\n" + " material: material ID for solid areas\n" + " threshold: noise threshold (0-1, higher = more solid)\n" + " scale: noise scale (smaller = larger features)\n" + " seed: random seed (0 for default)"}, + {"copy_region", (PyCFunction)copy_region, METH_VARARGS, + "copy_region(min_coord, max_coord) -> VoxelRegion\n\n" + "Copy a rectangular region to a VoxelRegion prefab.\n\n" + "Args:\n" + " min_coord: (x0, y0, z0) - minimum corner (inclusive)\n" + " max_coord: (x1, y1, z1) - maximum corner (inclusive)\n\n" + "Returns:\n" + " VoxelRegion object that can be pasted elsewhere."}, + {"paste_region", (PyCFunction)paste_region, METH_VARARGS | METH_KEYWORDS, + "paste_region(region, position, skip_air=True) -> None\n\n" + "Paste a VoxelRegion prefab at the specified position.\n\n" + "Args:\n" + " region: VoxelRegion from copy_region()\n" + " position: (x, y, z) - paste destination\n" + " skip_air: if True, air voxels don't overwrite (default True)"}, + {"clear", (PyCFunction)clear, METH_NOARGS, + "clear() -> None\n\n" + "Clear the grid (fill with air, material 0)."}, + {"rebuild_mesh", (PyCFunction)rebuild_mesh, METH_NOARGS, + "rebuild_mesh() -> None\n\n" + "Force immediate mesh rebuild for rendering."}, + {"count_non_air", (PyCFunction)count_non_air, METH_NOARGS, + "count_non_air() -> int\n\n" + "Count the number of non-air voxels in the grid."}, + {"count_material", (PyCFunction)count_material, METH_VARARGS, + "count_material(material) -> int\n\n" + "Count the number of voxels with the specified material ID."}, + {"project_column", (PyCFunction)project_column, METH_VARARGS | METH_KEYWORDS, + "project_column(x, z, headroom=2) -> dict\n\n" + "Project a single column to navigation info.\n\n" + "Scans the column from top to bottom, finding the topmost floor\n" + "(solid voxel with air above) and checking for adequate headroom.\n\n" + "Args:\n" + " x: X coordinate in voxel grid\n" + " z: Z coordinate in voxel grid\n" + " headroom: Required air voxels above floor (default 2)\n\n" + "Returns:\n" + " dict with keys:\n" + " height (float): World Y of floor surface\n" + " walkable (bool): True if floor found with adequate headroom\n" + " transparent (bool): True if no opaque voxels in column\n" + " path_cost (float): Floor material's path cost"}, + {"save", (PyCFunction)PyVoxelGrid::save, METH_VARARGS, + "save(path) -> bool\n\n" + "Save the voxel grid to a binary file.\n\n" + "Args:\n" + " path: File path to save to (.mcvg extension recommended)\n\n" + "Returns:\n" + " True on success, False on failure.\n\n" + "The file format includes grid dimensions, cell size, material palette,\n" + "and RLE-compressed voxel data."}, + {"load", (PyCFunction)PyVoxelGrid::load, METH_VARARGS, + "load(path) -> bool\n\n" + "Load voxel data from a binary file.\n\n" + "Args:\n" + " path: File path to load from\n\n" + "Returns:\n" + " True on success, False on failure.\n\n" + "Note: This replaces the current grid data entirely, including\n" + "dimensions and material palette."}, + {"to_bytes", (PyCFunction)PyVoxelGrid::to_bytes, METH_NOARGS, + "to_bytes() -> bytes\n\n" + "Serialize the voxel grid to a bytes object.\n\n" + "Returns:\n" + " bytes object containing the serialized grid data.\n\n" + "Useful for network transmission or custom storage."}, + {"from_bytes", (PyCFunction)PyVoxelGrid::from_bytes, METH_VARARGS, + "from_bytes(data) -> bool\n\n" + "Load voxel data from a bytes object.\n\n" + "Args:\n" + " data: bytes object containing serialized grid data\n\n" + "Returns:\n" + " True on success, False on failure.\n\n" + "Note: This replaces the current grid data entirely."}, + {nullptr} // Sentinel +}; + +// ============================================================================= +// Property definitions +// ============================================================================= + +PyGetSetDef PyVoxelGrid::getsetters[] = { + {"size", (getter)get_size, nullptr, + "Dimensions (width, height, depth) of the grid. Read-only.", nullptr}, + {"width", (getter)get_width, nullptr, + "Grid width (X dimension). Read-only.", nullptr}, + {"height", (getter)get_height, nullptr, + "Grid height (Y dimension). Read-only.", nullptr}, + {"depth", (getter)get_depth, nullptr, + "Grid depth (Z dimension). Read-only.", nullptr}, + {"cell_size", (getter)get_cell_size, nullptr, + "World units per voxel. Read-only.", nullptr}, + {"material_count", (getter)get_material_count, nullptr, + "Number of materials in the palette. Read-only.", nullptr}, + {"vertex_count", (getter)get_vertex_count, nullptr, + "Number of vertices after mesh generation. Read-only.", nullptr}, + {"offset", (getter)get_offset, (setter)set_offset, + "World-space position (x, y, z) of the grid origin.", nullptr}, + {"rotation", (getter)get_rotation, (setter)set_rotation, + "Y-axis rotation in degrees.", nullptr}, + {"greedy_meshing", (getter)get_greedy_meshing, (setter)set_greedy_meshing, + "Enable greedy meshing optimization (reduces vertex count for uniform regions).", nullptr}, + {nullptr} // Sentinel +}; diff --git a/src/3d/PyVoxelGrid.h b/src/3d/PyVoxelGrid.h new file mode 100644 index 0000000..dd99cf6 --- /dev/null +++ b/src/3d/PyVoxelGrid.h @@ -0,0 +1,179 @@ +// PyVoxelGrid.h - Python bindings for VoxelGrid +// Part of McRogueFace 3D Extension - Milestones 9, 11 +#pragma once + +#include "../Common.h" +#include "Python.h" +#include "VoxelGrid.h" +#include + +// ============================================================================= +// Python object structures +// ============================================================================= + +typedef struct PyVoxelGridObject { + PyObject_HEAD + std::shared_ptr data; + PyObject* weakreflist; +} PyVoxelGridObject; + +typedef struct PyVoxelRegionObject { + PyObject_HEAD + std::shared_ptr data; +} PyVoxelRegionObject; + +// ============================================================================= +// Python binding classes +// ============================================================================= + +class PyVoxelGrid { +public: + // Python type interface + static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds); + static int init(PyVoxelGridObject* self, PyObject* args, PyObject* kwds); + static void dealloc(PyVoxelGridObject* self); + static PyObject* repr(PyObject* obj); + + // Properties - dimensions (read-only) + static PyObject* get_size(PyVoxelGridObject* self, void* closure); + static PyObject* get_width(PyVoxelGridObject* self, void* closure); + static PyObject* get_height(PyVoxelGridObject* self, void* closure); + static PyObject* get_depth(PyVoxelGridObject* self, void* closure); + static PyObject* get_cell_size(PyVoxelGridObject* self, void* closure); + static PyObject* get_material_count(PyVoxelGridObject* self, void* closure); + + // Properties - transform (read-write) + static PyObject* get_offset(PyVoxelGridObject* self, void* closure); + static int set_offset(PyVoxelGridObject* self, PyObject* value, void* closure); + static PyObject* get_rotation(PyVoxelGridObject* self, void* closure); + static int set_rotation(PyVoxelGridObject* self, PyObject* value, void* closure); + + // Properties - mesh generation (Milestone 13) + static PyObject* get_greedy_meshing(PyVoxelGridObject* self, void* closure); + static int set_greedy_meshing(PyVoxelGridObject* self, PyObject* value, void* closure); + + // Voxel access methods + static PyObject* get(PyVoxelGridObject* self, PyObject* args); + static PyObject* set(PyVoxelGridObject* self, PyObject* args); + + // Material methods + static PyObject* add_material(PyVoxelGridObject* self, PyObject* args, PyObject* kwds); + static PyObject* get_material(PyVoxelGridObject* self, PyObject* args); + + // Bulk operations + static PyObject* fill(PyVoxelGridObject* self, PyObject* args); + static PyObject* fill_box(PyVoxelGridObject* self, PyObject* args); + static PyObject* clear(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)); + + // Bulk operations - Milestone 11 + static PyObject* fill_box_hollow(PyVoxelGridObject* self, PyObject* args, PyObject* kwds); + static PyObject* fill_sphere(PyVoxelGridObject* self, PyObject* args); + static PyObject* fill_cylinder(PyVoxelGridObject* self, PyObject* args); + static PyObject* fill_noise(PyVoxelGridObject* self, PyObject* args, PyObject* kwds); + + // Copy/paste operations - Milestone 11 + static PyObject* copy_region(PyVoxelGridObject* self, PyObject* args); + static PyObject* paste_region(PyVoxelGridObject* self, PyObject* args, PyObject* kwds); + + // Mesh caching (Milestone 10) + static PyObject* get_vertex_count(PyVoxelGridObject* self, void* closure); + static PyObject* rebuild_mesh(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)); + + // Serialization (Milestone 14) + static PyObject* save(PyVoxelGridObject* self, PyObject* args); + static PyObject* load(PyVoxelGridObject* self, PyObject* args); + static PyObject* to_bytes(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)); + static PyObject* from_bytes(PyVoxelGridObject* self, PyObject* args); + + // Statistics + static PyObject* count_non_air(PyVoxelGridObject* self, PyObject* Py_UNUSED(args)); + static PyObject* count_material(PyVoxelGridObject* self, PyObject* args); + + // Type registration + static PyMethodDef methods[]; + static PyGetSetDef getsetters[]; +}; + +class PyVoxelRegion { +public: + static void dealloc(PyVoxelRegionObject* self); + static PyObject* repr(PyObject* obj); + static PyObject* get_size(PyVoxelRegionObject* self, void* closure); + static PyObject* get_width(PyVoxelRegionObject* self, void* closure); + static PyObject* get_height(PyVoxelRegionObject* self, void* closure); + static PyObject* get_depth(PyVoxelRegionObject* self, void* closure); + + static PyGetSetDef getsetters[]; +}; + +// ============================================================================= +// Python type definitions (in mcrfpydef namespace) +// ============================================================================= + +namespace mcrfpydef { + +inline PyTypeObject PyVoxelGridType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.VoxelGrid", + .tp_basicsize = sizeof(PyVoxelGridObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)PyVoxelGrid::dealloc, + .tp_repr = PyVoxelGrid::repr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_doc = PyDoc_STR( + "VoxelGrid(size: tuple[int, int, int], cell_size: float = 1.0)\n\n" + "A dense 3D grid of voxel material IDs with a material palette.\n\n" + "VoxelGrids provide volumetric storage for 3D structures like buildings,\n" + "caves, and dungeon walls. Each cell stores a uint8 material ID (0-255),\n" + "where 0 is always air.\n\n" + "Args:\n" + " size: (width, height, depth) dimensions. Immutable after creation.\n" + " cell_size: World units per voxel. Default 1.0.\n\n" + "Properties:\n" + " size (tuple, read-only): Grid dimensions as (width, height, depth)\n" + " width, height, depth (int, read-only): Individual dimensions\n" + " cell_size (float, read-only): World units per voxel\n" + " offset (tuple): World-space position (x, y, z)\n" + " rotation (float): Y-axis rotation in degrees\n" + " material_count (int, read-only): Number of defined materials\n\n" + "Example:\n" + " voxels = mcrfpy.VoxelGrid(size=(16, 8, 16), cell_size=1.0)\n" + " stone = voxels.add_material('stone', color=mcrfpy.Color(128, 128, 128))\n" + " voxels.set(5, 0, 5, stone)\n" + " assert voxels.get(5, 0, 5) == stone\n" + " print(f'Non-air voxels: {voxels.count_non_air()}')" + ), + .tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int { + return 0; + }, + .tp_clear = [](PyObject* self) -> int { + return 0; + }, + .tp_weaklistoffset = offsetof(PyVoxelGridObject, weakreflist), + .tp_methods = nullptr, // Set before PyType_Ready + .tp_getset = nullptr, // Set before PyType_Ready + .tp_init = (initproc)PyVoxelGrid::init, + .tp_new = PyVoxelGrid::pynew, +}; + +inline PyTypeObject PyVoxelRegionType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.VoxelRegion", + .tp_basicsize = sizeof(PyVoxelRegionObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)PyVoxelRegion::dealloc, + .tp_repr = PyVoxelRegion::repr, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR( + "VoxelRegion - Portable voxel data for copy/paste operations.\n\n" + "Created by VoxelGrid.copy_region(), used with paste_region().\n" + "Cannot be instantiated directly.\n\n" + "Properties:\n" + " size (tuple, read-only): Dimensions as (width, height, depth)\n" + " width, height, depth (int, read-only): Individual dimensions" + ), + .tp_getset = nullptr, // Set before PyType_Ready + .tp_new = nullptr, // Cannot instantiate directly +}; + +} // namespace mcrfpydef diff --git a/src/3d/Shader3D.cpp b/src/3d/Shader3D.cpp index c842e4c..5729051 100644 --- a/src/3d/Shader3D.cpp +++ b/src/3d/Shader3D.cpp @@ -245,6 +245,223 @@ void main() { } )"; +// ============================================================================= +// Skinned Vertex Shaders (for skeletal animation) +// ============================================================================= + +const char* PS1_SKINNED_VERTEX_ES2 = R"( +// PS1-style skinned vertex shader for OpenGL ES 2.0 / WebGL 1.0 +precision mediump float; + +uniform mat4 u_model; +uniform mat4 u_view; +uniform mat4 u_projection; +uniform mat4 u_bones[32]; +uniform vec2 u_resolution; +uniform bool u_enable_snap; +uniform float u_fog_start; +uniform float u_fog_end; +uniform vec3 u_light_dir; +uniform vec3 u_ambient; + +attribute vec3 a_position; +attribute vec2 a_texcoord; +attribute vec3 a_normal; +attribute vec4 a_color; +attribute vec4 a_bone_ids; +attribute vec4 a_bone_weights; + +varying vec4 v_color; +varying vec2 v_texcoord; +varying float v_w; +varying float v_fog; + +mat4 getBoneMatrix(int index) { + if (index < 8) { + if (index < 4) { + if (index < 2) { + if (index == 0) return u_bones[0]; + else return u_bones[1]; + } else { + if (index == 2) return u_bones[2]; + else return u_bones[3]; + } + } else { + if (index < 6) { + if (index == 4) return u_bones[4]; + else return u_bones[5]; + } else { + if (index == 6) return u_bones[6]; + else return u_bones[7]; + } + } + } else if (index < 16) { + if (index < 12) { + if (index < 10) { + if (index == 8) return u_bones[8]; + else return u_bones[9]; + } else { + if (index == 10) return u_bones[10]; + else return u_bones[11]; + } + } else { + if (index < 14) { + if (index == 12) return u_bones[12]; + else return u_bones[13]; + } else { + if (index == 14) return u_bones[14]; + else return u_bones[15]; + } + } + } else if (index < 24) { + if (index < 20) { + if (index < 18) { + if (index == 16) return u_bones[16]; + else return u_bones[17]; + } else { + if (index == 18) return u_bones[18]; + else return u_bones[19]; + } + } else { + if (index < 22) { + if (index == 20) return u_bones[20]; + else return u_bones[21]; + } else { + if (index == 22) return u_bones[22]; + else return u_bones[23]; + } + } + } else { + if (index < 28) { + if (index < 26) { + if (index == 24) return u_bones[24]; + else return u_bones[25]; + } else { + if (index == 26) return u_bones[26]; + else return u_bones[27]; + } + } else { + if (index < 30) { + if (index == 28) return u_bones[28]; + else return u_bones[29]; + } else { + if (index == 30) return u_bones[30]; + else return u_bones[31]; + } + } + } + return mat4(1.0); +} + +void main() { + int b0 = int(a_bone_ids.x); + int b1 = int(a_bone_ids.y); + int b2 = int(a_bone_ids.z); + int b3 = int(a_bone_ids.w); + + mat4 skin_matrix = + getBoneMatrix(b0) * a_bone_weights.x + + getBoneMatrix(b1) * a_bone_weights.y + + getBoneMatrix(b2) * a_bone_weights.z + + getBoneMatrix(b3) * a_bone_weights.w; + + vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0); + vec3 skinned_normal = mat3(skin_matrix[0].xyz, skin_matrix[1].xyz, skin_matrix[2].xyz) * a_normal; + + vec4 worldPos = u_model * skinned_pos; + vec4 viewPos = u_view * worldPos; + vec4 clipPos = u_projection * viewPos; + + if (u_enable_snap) { + vec4 ndc = clipPos; + ndc.xyz /= ndc.w; + vec2 grid = u_resolution * 0.5; + ndc.xy = floor(ndc.xy * grid + 0.5) / grid; + ndc.xyz *= clipPos.w; + clipPos = ndc; + } + + gl_Position = clipPos; + + vec3 worldNormal = mat3(u_model[0].xyz, u_model[1].xyz, u_model[2].xyz) * skinned_normal; + worldNormal = normalize(worldNormal); + float diffuse = max(dot(worldNormal, -u_light_dir), 0.0); + vec3 lighting = u_ambient + vec3(diffuse); + v_color = vec4(a_color.rgb * lighting, a_color.a); + + v_texcoord = a_texcoord * clipPos.w; + v_w = clipPos.w; + + float depth = -viewPos.z; + v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0); +} +)"; + +const char* PS1_SKINNED_VERTEX = R"( +#version 150 core + +uniform mat4 u_model; +uniform mat4 u_view; +uniform mat4 u_projection; +uniform mat4 u_bones[64]; +uniform vec2 u_resolution; +uniform bool u_enable_snap; +uniform float u_fog_start; +uniform float u_fog_end; +uniform vec3 u_light_dir; +uniform vec3 u_ambient; + +in vec3 a_position; +in vec2 a_texcoord; +in vec3 a_normal; +in vec4 a_color; +in vec4 a_bone_ids; +in vec4 a_bone_weights; + +out vec4 v_color; +noperspective out vec2 v_texcoord; +out float v_fog; + +void main() { + ivec4 bone_ids = ivec4(a_bone_ids); + + mat4 skin_matrix = + u_bones[bone_ids.x] * a_bone_weights.x + + u_bones[bone_ids.y] * a_bone_weights.y + + u_bones[bone_ids.z] * a_bone_weights.z + + u_bones[bone_ids.w] * a_bone_weights.w; + + vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0); + vec3 skinned_normal = mat3(skin_matrix) * a_normal; + + vec4 worldPos = u_model * skinned_pos; + vec4 viewPos = u_view * worldPos; + vec4 clipPos = u_projection * viewPos; + + if (u_enable_snap) { + vec4 ndc = clipPos; + ndc.xyz /= ndc.w; + vec2 grid = u_resolution * 0.5; + ndc.xy = floor(ndc.xy * grid + 0.5) / grid; + ndc.xyz *= clipPos.w; + clipPos = ndc; + } + + gl_Position = clipPos; + + vec3 worldNormal = mat3(u_model) * skinned_normal; + worldNormal = normalize(worldNormal); + float diffuse = max(dot(worldNormal, -u_light_dir), 0.0); + vec3 lighting = u_ambient + vec3(diffuse); + v_color = vec4(a_color.rgb * lighting, a_color.a); + + v_texcoord = a_texcoord; + + float depth = -viewPos.z; + v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0); +} +)"; + } // namespace shaders // ============================================================================= @@ -274,6 +491,20 @@ bool Shader3D::loadPS1Shaders() { #endif } +bool Shader3D::loadPS1SkinnedShaders() { +#ifdef MCRF_HAS_GL +#ifdef __EMSCRIPTEN__ + // Use GLES2 skinned shaders for Emscripten/WebGL + return load(shaders::PS1_SKINNED_VERTEX_ES2, shaders::PS1_FRAGMENT_ES2); +#else + // Use desktop GL 3.2+ skinned shaders + return load(shaders::PS1_SKINNED_VERTEX, shaders::PS1_FRAGMENT); +#endif +#else + return false; +#endif +} + bool Shader3D::load(const char* vertexSource, const char* fragmentSource) { if (!gl::isGLReady()) { return false; diff --git a/src/3d/Shader3D.h b/src/3d/Shader3D.h index ffb6b2d..f457ac4 100644 --- a/src/3d/Shader3D.h +++ b/src/3d/Shader3D.h @@ -18,6 +18,9 @@ public: // Automatically selects desktop vs ES2 shaders based on platform bool loadPS1Shaders(); + // Load skinned (skeletal animation) shaders + bool loadPS1SkinnedShaders(); + // Load from custom source strings bool load(const char* vertexSource, const char* fragmentSource); diff --git a/src/3d/Viewport3D.cpp b/src/3d/Viewport3D.cpp index 9a5a211..f284ac4 100644 --- a/src/3d/Viewport3D.cpp +++ b/src/3d/Viewport3D.cpp @@ -5,6 +5,10 @@ #include "MeshLayer.h" #include "Entity3D.h" #include "EntityCollection3D.h" +#include "Billboard.h" +#include "Model3D.h" +#include "VoxelGrid.h" +#include "PyVoxelGrid.h" #include "../platform/GLContext.h" #include "PyVector.h" #include "PyColor.h" @@ -42,6 +46,7 @@ namespace mcrf { Viewport3D::Viewport3D() : size_(320.0f, 240.0f) , entities_(std::make_shared>>()) + , billboards_(std::make_shared>>()) { position = sf::Vector2f(0, 0); camera_.setAspect(size_.x / size_.y); @@ -50,6 +55,7 @@ Viewport3D::Viewport3D() Viewport3D::Viewport3D(float x, float y, float width, float height) : size_(width, height) , entities_(std::make_shared>>()) + , billboards_(std::make_shared>>()) { position = sf::Vector2f(x, y); camera_.setAspect(size_.x / size_.y); @@ -58,6 +64,15 @@ Viewport3D::Viewport3D(float x, float y, float width, float height) Viewport3D::~Viewport3D() { cleanupTestGeometry(); cleanupFBO(); + + // Clean up voxel VBO (Milestone 10) +#ifdef MCRF_HAS_GL + if (voxelVBO_ != 0) { + glDeleteBuffers(1, &voxelVBO_); + voxelVBO_ = 0; + } +#endif + if (tcodMap_) { delete tcodMap_; tcodMap_ = nullptr; @@ -181,6 +196,67 @@ void Viewport3D::orbitCamera(float angle, float distance, float height) { camera_.setTarget(vec3(0, 0, 0)); } +vec3 Viewport3D::screenToWorld(float screenX, float screenY) { + // Convert screen coordinates to normalized device coordinates (-1 to 1) + // screenX/Y are relative to the viewport position + float ndcX = (2.0f * screenX / size_.x) - 1.0f; + float ndcY = 1.0f - (2.0f * screenY / size_.y); // Flip Y for OpenGL + + // Get inverse matrices + mat4 proj = camera_.getProjectionMatrix(); + mat4 view = camera_.getViewMatrix(); + mat4 invProj = proj.inverse(); + mat4 invView = view.inverse(); + + // Unproject near plane point to get ray direction + vec4 rayClip(ndcX, ndcY, -1.0f, 1.0f); + vec4 rayEye = invProj * rayClip; + rayEye = vec4(rayEye.x, rayEye.y, -1.0f, 0.0f); // Direction in eye space + + vec4 rayWorld4 = invView * rayEye; + vec3 rayDir = vec3(rayWorld4.x, rayWorld4.y, rayWorld4.z).normalized(); + vec3 rayOrigin = camera_.getPosition(); + + // Intersect with Y=0 plane (ground level) + // This is a simplification - for hilly terrain, you'd want ray-marching + if (std::abs(rayDir.y) > 0.0001f) { + float t = -rayOrigin.y / rayDir.y; + if (t > 0) { + return rayOrigin + rayDir * t; + } + } + + // Ray parallel to ground or pointing away - return invalid position + return vec3(-1.0f, -1.0f, -1.0f); +} + +void Viewport3D::followEntity(std::shared_ptr entity, float distance, float height, float smoothing) { + if (!entity) return; + + // Get entity's world position + vec3 entityPos = entity->getWorldPos(); + + // Calculate desired camera position behind and above entity + float entityRotation = radians(entity->getRotation()); + float camX = entityPos.x - std::sin(entityRotation) * distance; + float camZ = entityPos.z - std::cos(entityRotation) * distance; + float camY = entityPos.y + height; + + vec3 desiredPos(camX, camY, camZ); + vec3 currentPos = camera_.getPosition(); + + // Smooth interpolation (smoothing is 0-1, where 1 = instant) + if (smoothing >= 1.0f) { + camera_.setPosition(desiredPos); + } else { + vec3 newPos = vec3::lerp(currentPos, desiredPos, smoothing); + camera_.setPosition(newPos); + } + + // Look at entity (slightly above ground) + camera_.setTarget(vec3(entityPos.x, entityPos.y + 0.5f, entityPos.z)); +} + // ============================================================================= // Mesh Layer Management // ============================================================================= @@ -195,6 +271,7 @@ std::shared_ptr Viewport3D::addLayer(const std::string& name, int zIn // Create new layer auto layer = std::make_shared(name, zIndex); + layer->setViewport(this); // Allow layer to mark cells as blocking meshLayers_.push_back(layer); // Disable test cube when layers are added @@ -449,14 +526,139 @@ void Viewport3D::renderEntities(const mat4& view, const mat4& proj) { #ifdef MCRF_HAS_GL if (!entities_ || !shader_ || !shader_->isValid()) return; - // Entity rendering uses the same shader as terrain - shader_->bind(); + // Extract frustum for culling + mat4 viewProj = proj * view; + Frustum frustum; + frustum.extractFromMatrix(viewProj); + // Render non-skeletal entities first + shader_->bind(); for (auto& entity : *entities_) { if (entity && entity->isVisible()) { - entity->render(view, proj, shader_->getProgram()); + // Frustum culling - use entity position with generous bounding radius + vec3 pos = entity->getWorldPos(); + float boundingRadius = entity->getScale().x * 2.0f; // Approximate bounding sphere + + if (!frustum.containsSphere(pos, boundingRadius)) { + continue; // Skip this entity - outside view frustum + } + + auto model = entity->getModel(); + if (!model || !model->hasSkeleton()) { + entity->render(view, proj, shader_->getProgram()); + } } } + shader_->unbind(); + + // Then render skeletal entities with skinned shader + if (skinnedShader_ && skinnedShader_->isValid()) { + skinnedShader_->bind(); + + // Set up common uniforms for skinned shader + skinnedShader_->setUniform("u_view", view); + skinnedShader_->setUniform("u_projection", proj); + skinnedShader_->setUniform("u_resolution", vec2(static_cast(internalWidth_), + static_cast(internalHeight_))); + skinnedShader_->setUniform("u_enable_snap", vertexSnapEnabled_); + + // Lighting + vec3 lightDir = vec3(0.5f, -0.7f, 0.5f).normalized(); + skinnedShader_->setUniform("u_light_dir", lightDir); + skinnedShader_->setUniform("u_ambient", vec3(0.3f, 0.3f, 0.3f)); + + // Fog + skinnedShader_->setUniform("u_fog_start", fogNear_); + skinnedShader_->setUniform("u_fog_end", fogFar_); + skinnedShader_->setUniform("u_fog_color", fogColor_); + + // Texture + skinnedShader_->setUniform("u_has_texture", false); + skinnedShader_->setUniform("u_enable_dither", ditheringEnabled_); + + for (auto& entity : *entities_) { + if (entity && entity->isVisible()) { + // Frustum culling for skeletal entities too + vec3 pos = entity->getWorldPos(); + float boundingRadius = entity->getScale().x * 2.0f; + + if (!frustum.containsSphere(pos, boundingRadius)) { + continue; + } + + auto model = entity->getModel(); + if (model && model->hasSkeleton()) { + entity->render(view, proj, skinnedShader_->getProgram()); + } + } + } + skinnedShader_->unbind(); + } +#endif +} + +// ============================================================================= +// Billboard Management +// ============================================================================= + +void Viewport3D::addBillboard(std::shared_ptr 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& 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; + + // Extract frustum for culling + mat4 viewProj = proj * view; + Frustum frustum; + frustum.extractFromMatrix(viewProj); + + 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()) { + // Frustum culling for billboards + vec3 pos = billboard->getPosition(); + float boundingRadius = billboard->getScale() * 2.0f; // Approximate + + if (!frustum.containsSphere(pos, boundingRadius)) { + continue; // Skip - outside frustum + } + + billboard->render(shaderProgram, view, proj, cameraPos); + } + } + + // Restore depth writing + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); shader_->unbind(); #endif @@ -498,6 +700,12 @@ void Viewport3D::initShader() { if (!shader_->loadPS1Shaders()) { shader_.reset(); // Shader loading failed } + + // Also create skinned shader for skeletal animation + skinnedShader_ = std::make_unique(); + if (!skinnedShader_->loadPS1SkinnedShaders()) { + skinnedShader_.reset(); // Skinned shader loading failed + } } void Viewport3D::initTestGeometry() { @@ -626,12 +834,235 @@ 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(); +#endif +} + +// ============================================================================= +// Voxel Layer Management (Milestone 10) +// ============================================================================= + +void Viewport3D::addVoxelLayer(std::shared_ptr grid, int zIndex) { + if (!grid) return; + voxelLayers_.push_back({grid, zIndex}); + + // Disable test cube when real content is added + renderTestCube_ = false; +} + +bool Viewport3D::removeVoxelLayer(std::shared_ptr grid) { + if (!grid) return false; + + auto it = std::find_if(voxelLayers_.begin(), voxelLayers_.end(), + [&grid](const auto& pair) { return pair.first == grid; }); + + if (it != voxelLayers_.end()) { + voxelLayers_.erase(it); + return true; + } + return false; +} + +// ============================================================================= +// Voxel-to-Nav Projection (Milestone 12) +// ============================================================================= + +void Viewport3D::clearVoxelNavRegion(std::shared_ptr grid) { + if (!grid || navGrid_.empty()) return; + + // Get voxel grid offset in world space + vec3 offset = grid->getOffset(); + float cellSize = grid->cellSize(); + + // Calculate nav grid cell offset from voxel grid offset + int navOffsetX = static_cast(std::floor(offset.x / cellSize_)); + int navOffsetZ = static_cast(std::floor(offset.z / cellSize_)); + + // Clear nav cells corresponding to voxel grid footprint + for (int vz = 0; vz < grid->depth(); vz++) { + for (int vx = 0; vx < grid->width(); vx++) { + int navX = navOffsetX + vx; + int navZ = navOffsetZ + vz; + + if (isValidCell(navX, navZ)) { + VoxelPoint& cell = at(navX, navZ); + cell.walkable = true; + cell.transparent = true; + cell.height = 0.0f; + cell.cost = 1.0f; + } + } + } + + // Sync to TCOD + syncToTCOD(); +} + +void Viewport3D::projectVoxelToNav(std::shared_ptr grid, int headroom) { + if (!grid || navGrid_.empty()) return; + + // Get voxel grid offset in world space + vec3 offset = grid->getOffset(); + float voxelCellSize = grid->cellSize(); + + // Calculate nav grid cell offset from voxel grid offset + // Assuming nav cell size matches voxel cell size for 1:1 mapping + int navOffsetX = static_cast(std::floor(offset.x / cellSize_)); + int navOffsetZ = static_cast(std::floor(offset.z / cellSize_)); + + // Project each column of the voxel grid to the navigation grid + for (int vz = 0; vz < grid->depth(); vz++) { + for (int vx = 0; vx < grid->width(); vx++) { + int navX = navOffsetX + vx; + int navZ = navOffsetZ + vz; + + if (!isValidCell(navX, navZ)) continue; + + // Get projection info from voxel column + VoxelGrid::NavInfo navInfo = grid->projectColumn(vx, vz, headroom); + + // Update nav cell + VoxelPoint& cell = at(navX, navZ); + cell.height = navInfo.height + offset.y; // Add world Y offset + cell.walkable = navInfo.walkable; + cell.transparent = navInfo.transparent; + cell.cost = navInfo.pathCost; + + // Sync this cell to TCOD + syncTCODCell(navX, navZ); + } + } +} + +void Viewport3D::projectAllVoxelsToNav(int headroom) { + if (navGrid_.empty()) return; + + // First, reset all nav cells to default state + for (auto& cell : navGrid_) { + cell.walkable = true; + cell.transparent = true; + cell.height = 0.0f; + cell.cost = 1.0f; + } + + // Project each voxel layer in order (later layers overwrite earlier) + // Sort by z_index so higher z_index layers take precedence + std::vector, int>> sortedLayers = voxelLayers_; + std::sort(sortedLayers.begin(), sortedLayers.end(), + [](const auto& a, const auto& b) { return a.second < b.second; }); + + for (const auto& pair : sortedLayers) { + if (pair.first) { + projectVoxelToNav(pair.first, headroom); + } + } + + // Final sync to TCOD (redundant but ensures consistency) + syncToTCOD(); +} + +void Viewport3D::renderVoxelLayers(const mat4& view, const mat4& proj) { +#ifdef MCRF_HAS_GL + if (voxelLayers_.empty() || !shader_ || !shader_->isValid()) { + return; + } + + // Sort layers by z_index (lower = rendered first) + std::vector> sortedLayers; + sortedLayers.reserve(voxelLayers_.size()); + for (auto& pair : voxelLayers_) { + if (pair.first) { + sortedLayers.push_back({pair.first.get(), pair.second}); + } + } + std::sort(sortedLayers.begin(), sortedLayers.end(), + [](const auto& a, const auto& b) { return a.second < b.second; }); + + shader_->bind(); + + // Set up view and projection matrices + shader_->setUniform("u_view", view); + shader_->setUniform("u_projection", proj); + + // PS1 effect uniforms + shader_->setUniform("u_resolution", vec2(static_cast(internalWidth_), + static_cast(internalHeight_))); + shader_->setUniform("u_enable_snap", vertexSnapEnabled_); + shader_->setUniform("u_enable_dither", ditheringEnabled_); + + // Lighting + vec3 lightDir = vec3(0.5f, -0.7f, 0.5f).normalized(); + shader_->setUniform("u_light_dir", lightDir); + shader_->setUniform("u_ambient", vec3(0.3f, 0.3f, 0.3f)); + + // Fog + shader_->setUniform("u_fog_start", fogNear_); + shader_->setUniform("u_fog_end", fogFar_); + shader_->setUniform("u_fog_color", fogColor_); + + // No texture for voxels (use vertex colors) + shader_->setUniform("u_has_texture", false); + + // Create VBO if needed + if (voxelVBO_ == 0) { + glGenBuffers(1, &voxelVBO_); + } + + // Render each voxel grid + for (auto& pair : sortedLayers) { + VoxelGrid* grid = pair.first; + + // Get vertices (triggers rebuild if dirty) + const std::vector& vertices = grid->getVertices(); + if (vertices.empty()) continue; + + // Set model matrix for this grid + shader_->setUniform("u_model", grid->getModelMatrix()); + + // Upload vertices to VBO + glBindBuffer(GL_ARRAY_BUFFER, voxelVBO_); + glBufferData(GL_ARRAY_BUFFER, + vertices.size() * sizeof(MeshVertex), + vertices.data(), + GL_DYNAMIC_DRAW); + + // Set up vertex attributes (same as MeshLayer) + size_t stride = sizeof(MeshVertex); + + // Position + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(MeshVertex, position)); + + // TexCoord + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(MeshVertex, texcoord)); + + // Normal + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(MeshVertex, normal)); + + // Color + glEnableVertexAttribArray(3); + glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(MeshVertex, color)); + + // Draw + glDrawArrays(GL_TRIANGLES, 0, static_cast(vertices.size())); + + // Cleanup + glDisableVertexAttribArray(0); + glDisableVertexAttribArray(1); + glDisableVertexAttribArray(2); + glDisableVertexAttribArray(3); + glBindBuffer(GL_ARRAY_BUFFER, 0); } shader_->unbind(); @@ -645,6 +1076,19 @@ void Viewport3D::render3DContent() { } #ifdef MCRF_HAS_GL + // Calculate delta time for animation updates + static sf::Clock frameClock; + float currentTime = frameClock.getElapsedTime().asSeconds(); + float dt = firstFrame_ ? 0.016f : (currentTime - lastFrameTime_); + lastFrameTime_ = currentTime; + firstFrame_ = false; + + // Cap delta time to avoid huge jumps (e.g., after window minimize) + if (dt > 0.1f) dt = 0.016f; + + // Update entity animations + updateEntities(dt); + // Save GL state gl::pushState(); @@ -668,11 +1112,17 @@ void Viewport3D::render3DContent() { // Render mesh layers first (terrain, etc.) - sorted by z_index renderMeshLayers(); - // Render entities + // Render voxel layers (Milestone 10) mat4 view = camera_.getViewMatrix(); mat4 projection = camera_.getProjectionMatrix(); + renderVoxelLayers(view, projection); + + // Render entities 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 +2245,427 @@ 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(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(PyFloat_AsDouble(PyTuple_GetItem(posObj, 0))); + float py = static_cast(PyFloat_AsDouble(PyTuple_GetItem(posObj, 1))); + float pz = static_cast(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(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(PyLong_AsLong(PyTuple_GetItem(gridPosObj, 0))); + int gridZ = static_cast(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(PyLong_AsLong(PyTuple_GetItem(footprintObj, 0))); + int footD = static_cast(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(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(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(billboards->size())); +} + +// ============================================================================= +// Camera & Input Methods (Milestone 8) +// ============================================================================= + +static PyObject* Viewport3D_screen_to_world(PyViewport3DObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = {"x", "y", NULL}; + + float x = 0.0f, y = 0.0f; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ff", const_cast(kwlist), &x, &y)) { + return NULL; + } + + // Adjust for viewport position (user passes screen coords relative to viewport) + vec3 worldPos = self->data->screenToWorld(x, y); + + // Return None if no intersection (ray parallel to ground or invalid) + if (worldPos.x < 0 && worldPos.y < 0 && worldPos.z < 0) { + Py_RETURN_NONE; + } + + return Py_BuildValue("(fff)", worldPos.x, worldPos.y, worldPos.z); +} + +static PyObject* Viewport3D_follow(PyViewport3DObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = {"entity", "distance", "height", "smoothing", NULL}; + + PyObject* entityObj = nullptr; + float distance = 10.0f; + float height = 5.0f; + float smoothing = 1.0f; // Default to instant (for single-call positioning) + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|fff", const_cast(kwlist), + &entityObj, &distance, &height, &smoothing)) { + return NULL; + } + + // Check if it's an Entity3D object + if (!PyObject_IsInstance(entityObj, (PyObject*)&mcrfpydef::PyEntity3DType)) { + PyErr_SetString(PyExc_TypeError, "Expected an Entity3D object"); + return NULL; + } + + PyEntity3DObject* entObj = (PyEntity3DObject*)entityObj; + if (!entObj->data) { + PyErr_SetString(PyExc_ValueError, "Invalid Entity3D object"); + return NULL; + } + + self->data->followEntity(entObj->data, distance, height, smoothing); + Py_RETURN_NONE; +} + +// ============================================================================= +// Voxel Layer Methods (Milestone 10) +// ============================================================================= + +static PyObject* Viewport3D_add_voxel_layer(PyViewport3DObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = {"voxel_grid", "z_index", NULL}; + PyObject* voxel_grid_obj = nullptr; + int z_index = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|i", const_cast(kwlist), + &voxel_grid_obj, &z_index)) { + return NULL; + } + + // Check if it's a VoxelGrid object + PyTypeObject* voxelGridType = (PyTypeObject*)PyObject_GetAttrString( + McRFPy_API::mcrf_module, "VoxelGrid"); + if (!voxelGridType) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid type not found"); + return NULL; + } + + if (!PyObject_IsInstance(voxel_grid_obj, (PyObject*)voxelGridType)) { + Py_DECREF(voxelGridType); + PyErr_SetString(PyExc_TypeError, "voxel_grid must be a VoxelGrid object"); + return NULL; + } + Py_DECREF(voxelGridType); + + PyVoxelGridObject* vg = (PyVoxelGridObject*)voxel_grid_obj; + if (!vg->data) { + PyErr_SetString(PyExc_ValueError, "VoxelGrid not initialized"); + return NULL; + } + + self->data->addVoxelLayer(vg->data, z_index); + Py_RETURN_NONE; +} + +static PyObject* Viewport3D_remove_voxel_layer(PyViewport3DObject* self, PyObject* args) { + PyObject* voxel_grid_obj = nullptr; + + if (!PyArg_ParseTuple(args, "O", &voxel_grid_obj)) { + return NULL; + } + + // Check if it's a VoxelGrid object + PyTypeObject* voxelGridType = (PyTypeObject*)PyObject_GetAttrString( + McRFPy_API::mcrf_module, "VoxelGrid"); + if (!voxelGridType) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid type not found"); + return NULL; + } + + if (!PyObject_IsInstance(voxel_grid_obj, (PyObject*)voxelGridType)) { + Py_DECREF(voxelGridType); + PyErr_SetString(PyExc_TypeError, "voxel_grid must be a VoxelGrid object"); + return NULL; + } + Py_DECREF(voxelGridType); + + PyVoxelGridObject* vg = (PyVoxelGridObject*)voxel_grid_obj; + if (!vg->data) { + PyErr_SetString(PyExc_ValueError, "VoxelGrid not initialized"); + return NULL; + } + + bool removed = self->data->removeVoxelLayer(vg->data); + return PyBool_FromLong(removed); +} + +static PyObject* Viewport3D_voxel_layer_count(PyViewport3DObject* self, PyObject* Py_UNUSED(args)) { + return PyLong_FromSize_t(self->data->getVoxelLayerCount()); +} + +// ============================================================================= +// Voxel-to-Nav Projection Methods (Milestone 12) +// ============================================================================= + +static PyObject* Viewport3D_project_voxel_to_nav(PyViewport3DObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = {"voxel_grid", "headroom", NULL}; + PyObject* voxel_grid_obj = nullptr; + int headroom = 2; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|i", const_cast(kwlist), + &voxel_grid_obj, &headroom)) { + return NULL; + } + + // Check if it's a VoxelGrid object + PyTypeObject* voxelGridType = (PyTypeObject*)PyObject_GetAttrString( + McRFPy_API::mcrf_module, "VoxelGrid"); + if (!voxelGridType) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid type not found"); + return NULL; + } + + if (!PyObject_IsInstance(voxel_grid_obj, (PyObject*)voxelGridType)) { + Py_DECREF(voxelGridType); + PyErr_SetString(PyExc_TypeError, "voxel_grid must be a VoxelGrid object"); + return NULL; + } + Py_DECREF(voxelGridType); + + PyVoxelGridObject* vg = (PyVoxelGridObject*)voxel_grid_obj; + if (!vg->data) { + PyErr_SetString(PyExc_ValueError, "VoxelGrid not initialized"); + return NULL; + } + + if (headroom < 0) { + PyErr_SetString(PyExc_ValueError, "headroom must be non-negative"); + return NULL; + } + + self->data->projectVoxelToNav(vg->data, headroom); + Py_RETURN_NONE; +} + +static PyObject* Viewport3D_project_all_voxels_to_nav(PyViewport3DObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = {"headroom", NULL}; + int headroom = 2; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", const_cast(kwlist), &headroom)) { + return NULL; + } + + if (headroom < 0) { + PyErr_SetString(PyExc_ValueError, "headroom must be non-negative"); + return NULL; + } + + self->data->projectAllVoxelsToNav(headroom); + Py_RETURN_NONE; +} + +static PyObject* Viewport3D_clear_voxel_nav_region(PyViewport3DObject* self, PyObject* args) { + PyObject* voxel_grid_obj = nullptr; + + if (!PyArg_ParseTuple(args, "O", &voxel_grid_obj)) { + return NULL; + } + + // Check if it's a VoxelGrid object + PyTypeObject* voxelGridType = (PyTypeObject*)PyObject_GetAttrString( + McRFPy_API::mcrf_module, "VoxelGrid"); + if (!voxelGridType) { + PyErr_SetString(PyExc_RuntimeError, "VoxelGrid type not found"); + return NULL; + } + + if (!PyObject_IsInstance(voxel_grid_obj, (PyObject*)voxelGridType)) { + Py_DECREF(voxelGridType); + PyErr_SetString(PyExc_TypeError, "voxel_grid must be a VoxelGrid object"); + return NULL; + } + Py_DECREF(voxelGridType); + + PyVoxelGridObject* vg = (PyVoxelGridObject*)voxel_grid_obj; + if (!vg->data) { + PyErr_SetString(PyExc_ValueError, "VoxelGrid not initialized"); + return NULL; + } + + self->data->clearVoxelNavRegion(vg->data); + Py_RETURN_NONE; +} + } // namespace mcrf // Methods array - outside namespace but PyObjectType still in scope via typedef @@ -1903,5 +2774,120 @@ 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"}, + + // Camera & Input methods (Milestone 8) + {"screen_to_world", (PyCFunction)mcrf::Viewport3D_screen_to_world, METH_VARARGS | METH_KEYWORDS, + "screen_to_world(x, y) -> tuple or None\n\n" + "Convert screen coordinates to world position via ray casting.\n\n" + "Args:\n" + " x: Screen X coordinate relative to viewport\n" + " y: Screen Y coordinate relative to viewport\n\n" + "Returns:\n" + " (x, y, z) world position tuple, or None if no intersection with ground plane"}, + {"follow", (PyCFunction)mcrf::Viewport3D_follow, METH_VARARGS | METH_KEYWORDS, + "follow(entity, distance=10, height=5, smoothing=1.0)\n\n" + "Position camera to follow an entity.\n\n" + "Args:\n" + " entity: Entity3D to follow\n" + " distance: Distance behind entity\n" + " height: Camera height above entity\n" + " smoothing: Interpolation factor (0-1). 1 = instant, lower = smoother"}, + + // Voxel layer methods (Milestone 10) + {"add_voxel_layer", (PyCFunction)mcrf::Viewport3D_add_voxel_layer, METH_VARARGS | METH_KEYWORDS, + "add_voxel_layer(voxel_grid, z_index=0)\n\n" + "Add a VoxelGrid as a renderable layer.\n\n" + "Args:\n" + " voxel_grid: VoxelGrid object to render\n" + " z_index: Render order (lower = rendered first)"}, + {"remove_voxel_layer", (PyCFunction)mcrf::Viewport3D_remove_voxel_layer, METH_VARARGS, + "remove_voxel_layer(voxel_grid) -> bool\n\n" + "Remove a VoxelGrid layer from the viewport.\n\n" + "Args:\n" + " voxel_grid: VoxelGrid object to remove\n\n" + "Returns:\n" + " True if the layer was found and removed"}, + {"voxel_layer_count", (PyCFunction)mcrf::Viewport3D_voxel_layer_count, METH_NOARGS, + "voxel_layer_count() -> int\n\n" + "Get the number of voxel layers.\n\n" + "Returns:\n" + " Number of voxel layers in the viewport"}, + + // Voxel-to-Nav projection methods (Milestone 12) + {"project_voxel_to_nav", (PyCFunction)mcrf::Viewport3D_project_voxel_to_nav, METH_VARARGS | METH_KEYWORDS, + "project_voxel_to_nav(voxel_grid, headroom=2)\n\n" + "Project a VoxelGrid to the navigation grid.\n\n" + "Scans each column of the voxel grid and updates corresponding\n" + "navigation cells with walkability, transparency, height, and cost.\n\n" + "Args:\n" + " voxel_grid: VoxelGrid to project\n" + " headroom: Required air voxels above floor for walkability (default: 2)"}, + {"project_all_voxels_to_nav", (PyCFunction)mcrf::Viewport3D_project_all_voxels_to_nav, METH_VARARGS | METH_KEYWORDS, + "project_all_voxels_to_nav(headroom=2)\n\n" + "Project all voxel layers to the navigation grid.\n\n" + "Resets navigation grid and projects each voxel layer in z_index order.\n" + "Later layers (higher z_index) overwrite earlier ones.\n\n" + "Args:\n" + " headroom: Required air voxels above floor for walkability (default: 2)"}, + {"clear_voxel_nav_region", (PyCFunction)mcrf::Viewport3D_clear_voxel_nav_region, METH_VARARGS, + "clear_voxel_nav_region(voxel_grid)\n\n" + "Clear navigation cells in a voxel grid's footprint.\n\n" + "Resets walkability, transparency, height, and cost to defaults\n" + "for all nav cells corresponding to the voxel grid's XZ extent.\n\n" + "Args:\n" + " voxel_grid: VoxelGrid whose nav region to clear"}, {NULL} // Sentinel }; diff --git a/src/3d/Viewport3D.h b/src/3d/Viewport3D.h index ab7d5b2..c38901f 100644 --- a/src/3d/Viewport3D.h +++ b/src/3d/Viewport3D.h @@ -30,6 +30,8 @@ namespace mcrf { class Viewport3D; class Shader3D; class MeshLayer; +class Billboard; +class VoxelGrid; } // namespace mcrf @@ -84,6 +86,19 @@ public: // Camera orbit helper for demos void orbitCamera(float angle, float distance, float height); + /// Convert screen coordinates to world position via ray casting + /// @param screenX X position relative to viewport + /// @param screenY Y position relative to viewport + /// @return World position on Y=0 plane, or (-1,-1,-1) if no intersection + vec3 screenToWorld(float screenX, float screenY); + + /// Position camera to follow an entity + /// @param entity Entity to follow + /// @param distance Distance behind entity + /// @param height Height above entity + /// @param smoothing Interpolation factor (0-1, where 1 = instant) + void followEntity(std::shared_ptr entity, float distance, float height, float smoothing = 1.0f); + // ========================================================================= // Mesh Layer Management // ========================================================================= @@ -190,6 +205,68 @@ public: /// Render all entities void renderEntities(const mat4& view, const mat4& proj); + // ========================================================================= + // Billboard Management + // ========================================================================= + + /// Get the billboard list + std::shared_ptr>> getBillboards() { return billboards_; } + + /// Add a billboard + void addBillboard(std::shared_ptr bb); + + /// Remove a billboard by pointer + void removeBillboard(Billboard* bb); + + /// Clear all billboards + void clearBillboards(); + + /// Get billboard count + size_t getBillboardCount() const { return billboards_ ? billboards_->size() : 0; } + + /// Render all billboards + void renderBillboards(const mat4& view, const mat4& proj); + + // ========================================================================= + // VoxelGrid Layer Management (Milestone 10) + // ========================================================================= + + /// Add a voxel layer to the viewport + /// @param grid The VoxelGrid to add + /// @param zIndex Render order (lower = rendered first, behind higher values) + void addVoxelLayer(std::shared_ptr grid, int zIndex = 0); + + /// Remove a voxel layer from the viewport + /// @param grid The VoxelGrid to remove + /// @return true if the layer was found and removed + bool removeVoxelLayer(std::shared_ptr grid); + + /// Get all voxel layers (read-only) + const std::vector, int>>& getVoxelLayers() const { return voxelLayers_; } + + /// Get number of voxel layers + size_t getVoxelLayerCount() const { return voxelLayers_.size(); } + + /// Render all voxel layers + void renderVoxelLayers(const mat4& view, const mat4& proj); + + // ========================================================================= + // Voxel-to-Nav Projection (Milestone 12) + // ========================================================================= + + /// Project a single voxel grid to the navigation grid + /// @param grid The voxel grid to project + /// @param headroom Required air voxels above floor for walkability + void projectVoxelToNav(std::shared_ptr grid, int headroom = 2); + + /// Project all voxel layers to the navigation grid + /// @param headroom Required air voxels above floor for walkability + void projectAllVoxelsToNav(int headroom = 2); + + /// Clear nav cells in a voxel grid's footprint (before re-projection) + /// @param grid The voxel grid whose footprint to clear + void clearVoxelNavRegion(std::shared_ptr grid); + // Background color void setBackgroundColor(const sf::Color& color) { bgColor_ = color; } sf::Color getBackgroundColor() const { return bgColor_; } @@ -262,6 +339,10 @@ private: float testRotation_ = 0.0f; bool renderTestCube_ = true; // Set to false when layers are added + // Animation timing + float lastFrameTime_ = 0.0f; + bool firstFrame_ = true; + // Mesh layers for terrain, static geometry std::vector> meshLayers_; @@ -276,8 +357,17 @@ private: // Entity3D storage std::shared_ptr>> entities_; + // Billboard storage + std::shared_ptr>> billboards_; + + // Voxel layer storage (Milestone 10) + // Pairs of (VoxelGrid, z_index) for render ordering + std::vector, int>> voxelLayers_; + unsigned int voxelVBO_ = 0; // Shared VBO for voxel rendering + // Shader for PS1-style rendering std::unique_ptr shader_; + std::unique_ptr skinnedShader_; // For skeletal animation // Test geometry VBO (cube) unsigned int testVBO_ = 0; diff --git a/src/3d/VoxelGrid.cpp b/src/3d/VoxelGrid.cpp new file mode 100644 index 0000000..f69e71e --- /dev/null +++ b/src/3d/VoxelGrid.cpp @@ -0,0 +1,795 @@ +// VoxelGrid.cpp - Dense 3D voxel array implementation +// Part of McRogueFace 3D Extension - Milestones 9-11 + +#include "VoxelGrid.h" +#include "VoxelMesher.h" +#include "MeshLayer.h" // For MeshVertex +#include +#include +#include // For memcpy, memcmp +#include // For file I/O + +namespace mcrf { + +// Static air material for out-of-bounds or ID=0 queries +static VoxelMaterial airMaterial{"air", sf::Color::Transparent, -1, true, 0.0f}; + +// ============================================================================= +// Constructor +// ============================================================================= + +VoxelGrid::VoxelGrid(int w, int h, int d, float cellSize) + : width_(w), height_(h), depth_(d), cellSize_(cellSize), + offset_(0, 0, 0), rotation_(0.0f) +{ + if (w <= 0 || h <= 0 || d <= 0) { + throw std::invalid_argument("VoxelGrid dimensions must be positive"); + } + if (cellSize <= 0.0f) { + throw std::invalid_argument("VoxelGrid cell size must be positive"); + } + + // Allocate dense array, initialized to air (0) + size_t totalSize = static_cast(w) * h * d; + data_.resize(totalSize, 0); +} + +// ============================================================================= +// Per-voxel access +// ============================================================================= + +bool VoxelGrid::isValid(int x, int y, int z) const { + return x >= 0 && x < width_ && + y >= 0 && y < height_ && + z >= 0 && z < depth_; +} + +uint8_t VoxelGrid::get(int x, int y, int z) const { + if (!isValid(x, y, z)) { + return 0; // Out of bounds returns air + } + return data_[index(x, y, z)]; +} + +void VoxelGrid::set(int x, int y, int z, uint8_t material) { + if (!isValid(x, y, z)) { + return; // Out of bounds is no-op + } + data_[index(x, y, z)] = material; + meshDirty_ = true; +} + +// ============================================================================= +// Material palette +// ============================================================================= + +uint8_t VoxelGrid::addMaterial(const VoxelMaterial& mat) { + if (materials_.size() >= 255) { + throw std::runtime_error("Material palette full (max 255 materials)"); + } + materials_.push_back(mat); + return static_cast(materials_.size()); // 1-indexed +} + +uint8_t VoxelGrid::addMaterial(const std::string& name, sf::Color color, + int spriteIndex, bool transparent, float pathCost) { + return addMaterial(VoxelMaterial(name, color, spriteIndex, transparent, pathCost)); +} + +const VoxelMaterial& VoxelGrid::getMaterial(uint8_t id) const { + if (id == 0 || id > materials_.size()) { + return airMaterial; + } + return materials_[id - 1]; // 1-indexed, so ID 1 = materials_[0] +} + +// ============================================================================= +// Bulk operations +// ============================================================================= + +void VoxelGrid::fill(uint8_t material) { + std::fill(data_.begin(), data_.end(), material); + meshDirty_ = true; +} + +// ============================================================================= +// Transform +// ============================================================================= + +mat4 VoxelGrid::getModelMatrix() const { + // Apply translation first, then rotation around Y axis + mat4 translation = mat4::translate(offset_); + mat4 rotation = mat4::rotateY(rotation_ * DEG_TO_RAD); + return translation * rotation; +} + +// ============================================================================= +// Statistics +// ============================================================================= + +size_t VoxelGrid::countNonAir() const { + size_t count = 0; + for (uint8_t v : data_) { + if (v != 0) { + count++; + } + } + return count; +} + +size_t VoxelGrid::countMaterial(uint8_t material) const { + size_t count = 0; + for (uint8_t v : data_) { + if (v == material) { + count++; + } + } + return count; +} + +// ============================================================================= +// fillBox (Milestone 10) +// ============================================================================= + +void VoxelGrid::fillBox(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t material) { + // Ensure proper ordering (min to max) + if (x0 > x1) std::swap(x0, x1); + if (y0 > y1) std::swap(y0, y1); + if (z0 > z1) std::swap(z0, z1); + + // Clamp to valid range + x0 = std::max(0, std::min(x0, width_ - 1)); + x1 = std::max(0, std::min(x1, width_ - 1)); + y0 = std::max(0, std::min(y0, height_ - 1)); + y1 = std::max(0, std::min(y1, height_ - 1)); + z0 = std::max(0, std::min(z0, depth_ - 1)); + z1 = std::max(0, std::min(z1, depth_ - 1)); + + for (int z = z0; z <= z1; z++) { + for (int y = y0; y <= y1; y++) { + for (int x = x0; x <= x1; x++) { + data_[index(x, y, z)] = material; + } + } + } + meshDirty_ = true; +} + +// ============================================================================= +// Bulk Operations - Milestone 11 +// ============================================================================= + +void VoxelGrid::fillBoxHollow(int x0, int y0, int z0, int x1, int y1, int z1, + uint8_t material, int thickness) { + // Ensure proper ordering (min to max) + if (x0 > x1) std::swap(x0, x1); + if (y0 > y1) std::swap(y0, y1); + if (z0 > z1) std::swap(z0, z1); + + // Fill entire box with material + fillBox(x0, y0, z0, x1, y1, z1, material); + + // Carve out interior (inset by thickness on all sides) + int ix0 = x0 + thickness; + int iy0 = y0 + thickness; + int iz0 = z0 + thickness; + int ix1 = x1 - thickness; + int iy1 = y1 - thickness; + int iz1 = z1 - thickness; + + // Only carve if there's interior space + if (ix0 <= ix1 && iy0 <= iy1 && iz0 <= iz1) { + fillBox(ix0, iy0, iz0, ix1, iy1, iz1, 0); // Air + } + // meshDirty_ already set by fillBox calls +} + +void VoxelGrid::fillSphere(int cx, int cy, int cz, int radius, uint8_t material) { + int r2 = radius * radius; + + for (int z = cz - radius; z <= cz + radius; z++) { + for (int y = cy - radius; y <= cy + radius; y++) { + for (int x = cx - radius; x <= cx + radius; x++) { + int dx = x - cx; + int dy = y - cy; + int dz = z - cz; + if (dx * dx + dy * dy + dz * dz <= r2) { + if (isValid(x, y, z)) { + data_[index(x, y, z)] = material; + } + } + } + } + } + meshDirty_ = true; +} + +void VoxelGrid::fillCylinder(int cx, int cy, int cz, int radius, int height, uint8_t material) { + int r2 = radius * radius; + + for (int y = cy; y < cy + height; y++) { + for (int z = cz - radius; z <= cz + radius; z++) { + for (int x = cx - radius; x <= cx + radius; x++) { + int dx = x - cx; + int dz = z - cz; + if (dx * dx + dz * dz <= r2) { + if (isValid(x, y, z)) { + data_[index(x, y, z)] = material; + } + } + } + } + } + meshDirty_ = true; +} + +// Simple 3D noise implementation (hash-based, similar to value noise) +namespace { + // Simple hash function for noise + inline unsigned int hash3D(int x, int y, int z, unsigned int seed) { + unsigned int h = seed; + h ^= static_cast(x) * 374761393u; + h ^= static_cast(y) * 668265263u; + h ^= static_cast(z) * 2147483647u; + h = (h ^ (h >> 13)) * 1274126177u; + return h; + } + + // Convert hash to 0-1 float + inline float hashToFloat(unsigned int h) { + return static_cast(h & 0xFFFFFF) / static_cast(0xFFFFFF); + } + + // Linear interpolation + inline float lerp(float a, float b, float t) { + return a + t * (b - a); + } + + // Smoothstep for smoother interpolation + inline float smoothstep(float t) { + return t * t * (3.0f - 2.0f * t); + } + + // 3D value noise + float noise3D(float x, float y, float z, unsigned int seed) { + int xi = static_cast(std::floor(x)); + int yi = static_cast(std::floor(y)); + int zi = static_cast(std::floor(z)); + + float xf = x - xi; + float yf = y - yi; + float zf = z - zi; + + // Smoothstep the fractions + float u = smoothstep(xf); + float v = smoothstep(yf); + float w = smoothstep(zf); + + // Hash corners of the unit cube + float c000 = hashToFloat(hash3D(xi, yi, zi, seed)); + float c100 = hashToFloat(hash3D(xi + 1, yi, zi, seed)); + float c010 = hashToFloat(hash3D(xi, yi + 1, zi, seed)); + float c110 = hashToFloat(hash3D(xi + 1, yi + 1, zi, seed)); + float c001 = hashToFloat(hash3D(xi, yi, zi + 1, seed)); + float c101 = hashToFloat(hash3D(xi + 1, yi, zi + 1, seed)); + float c011 = hashToFloat(hash3D(xi, yi + 1, zi + 1, seed)); + float c111 = hashToFloat(hash3D(xi + 1, yi + 1, zi + 1, seed)); + + // Trilinear interpolation + float x00 = lerp(c000, c100, u); + float x10 = lerp(c010, c110, u); + float x01 = lerp(c001, c101, u); + float x11 = lerp(c011, c111, u); + + float y0 = lerp(x00, x10, v); + float y1 = lerp(x01, x11, v); + + return lerp(y0, y1, w); + } +} + +void VoxelGrid::fillNoise(int x0, int y0, int z0, int x1, int y1, int z1, + uint8_t material, float threshold, float scale, unsigned int seed) { + // Ensure proper ordering + if (x0 > x1) std::swap(x0, x1); + if (y0 > y1) std::swap(y0, y1); + if (z0 > z1) std::swap(z0, z1); + + // Clamp to valid range + x0 = std::max(0, std::min(x0, width_ - 1)); + x1 = std::max(0, std::min(x1, width_ - 1)); + y0 = std::max(0, std::min(y0, height_ - 1)); + y1 = std::max(0, std::min(y1, height_ - 1)); + z0 = std::max(0, std::min(z0, depth_ - 1)); + z1 = std::max(0, std::min(z1, depth_ - 1)); + + for (int z = z0; z <= z1; z++) { + for (int y = y0; y <= y1; y++) { + for (int x = x0; x <= x1; x++) { + float n = noise3D(x * scale, y * scale, z * scale, seed); + if (n > threshold) { + data_[index(x, y, z)] = material; + } + } + } + } + meshDirty_ = true; +} + +// ============================================================================= +// Copy/Paste Operations - Milestone 11 +// ============================================================================= + +VoxelRegion VoxelGrid::copyRegion(int x0, int y0, int z0, int x1, int y1, int z1) const { + // Ensure proper ordering + if (x0 > x1) std::swap(x0, x1); + if (y0 > y1) std::swap(y0, y1); + if (z0 > z1) std::swap(z0, z1); + + // Clamp to valid range + x0 = std::max(0, std::min(x0, width_ - 1)); + x1 = std::max(0, std::min(x1, width_ - 1)); + y0 = std::max(0, std::min(y0, height_ - 1)); + y1 = std::max(0, std::min(y1, height_ - 1)); + z0 = std::max(0, std::min(z0, depth_ - 1)); + z1 = std::max(0, std::min(z1, depth_ - 1)); + + int rw = x1 - x0 + 1; + int rh = y1 - y0 + 1; + int rd = z1 - z0 + 1; + + VoxelRegion region(rw, rh, rd); + + for (int rz = 0; rz < rd; rz++) { + for (int ry = 0; ry < rh; ry++) { + for (int rx = 0; rx < rw; rx++) { + int sx = x0 + rx; + int sy = y0 + ry; + int sz = z0 + rz; + size_t ri = static_cast(rz) * (rw * rh) + + static_cast(ry) * rw + rx; + region.data[ri] = get(sx, sy, sz); + } + } + } + + return region; +} + +void VoxelGrid::pasteRegion(const VoxelRegion& region, int x, int y, int z, bool skipAir) { + if (!region.isValid()) return; + + for (int rz = 0; rz < region.depth; rz++) { + for (int ry = 0; ry < region.height; ry++) { + for (int rx = 0; rx < region.width; rx++) { + size_t ri = static_cast(rz) * (region.width * region.height) + + static_cast(ry) * region.width + rx; + uint8_t mat = region.data[ri]; + + if (skipAir && mat == 0) continue; + + int dx = x + rx; + int dy = y + ry; + int dz = z + rz; + + if (isValid(dx, dy, dz)) { + data_[index(dx, dy, dz)] = mat; + } + } + } + } + meshDirty_ = true; +} + +// ============================================================================= +// Navigation Projection - Milestone 12 +// ============================================================================= + +VoxelGrid::NavInfo VoxelGrid::projectColumn(int x, int z, int headroom) const { + NavInfo info; + info.height = 0.0f; + info.walkable = false; + info.transparent = true; + info.pathCost = 1.0f; + + // Out of bounds check + if (x < 0 || x >= width_ || z < 0 || z >= depth_) { + return info; + } + + // Scan from top to bottom, find first solid with air above (floor) + int floorY = -1; + for (int y = height_ - 1; y >= 0; y--) { + uint8_t mat = get(x, y, z); + if (mat != 0) { + // Found solid - check if it's a floor (air above) or ceiling + bool hasAirAbove = (y == height_ - 1) || (get(x, y + 1, z) == 0); + if (hasAirAbove) { + floorY = y; + break; + } + } + } + + if (floorY >= 0) { + // Found a floor + info.height = (floorY + 1) * cellSize_; // Top of floor voxel + info.walkable = true; + + // Check headroom (need enough air voxels above floor) + int airCount = 0; + for (int y = floorY + 1; y < height_; y++) { + if (get(x, y, z) == 0) { + airCount++; + } else { + break; + } + } + if (airCount < headroom) { + info.walkable = false; // Can't fit entity + } + + // Get path cost from floor material + uint8_t floorMat = get(x, floorY, z); + info.pathCost = getMaterial(floorMat).pathCost; + } + + // Check transparency: any non-transparent solid in column blocks FOV + for (int y = 0; y < height_; y++) { + uint8_t mat = get(x, y, z); + if (mat != 0 && !getMaterial(mat).transparent) { + info.transparent = false; + break; + } + } + + return info; +} + +// ============================================================================= +// Mesh Caching (Milestone 10) +// ============================================================================= + +const std::vector& VoxelGrid::getVertices() const { + if (meshDirty_) { + rebuildMesh(); + } + return cachedVertices_; +} + +void VoxelGrid::rebuildMesh() const { + cachedVertices_.clear(); + if (greedyMeshing_) { + VoxelMesher::generateGreedyMesh(*this, cachedVertices_); + } else { + VoxelMesher::generateMesh(*this, cachedVertices_); + } + meshDirty_ = false; +} + +// ============================================================================= +// Serialization - Milestone 14 +// ============================================================================= + +// File format: +// Magic "MCVG" (4 bytes) +// Version (1 byte) - currently 1 +// Width, Height, Depth (3 x int32 = 12 bytes) +// Cell Size (float32 = 4 bytes) +// Material count (uint8 = 1 byte) +// For each material: +// Name length (uint16) + name bytes +// Color RGBA (4 bytes) +// Sprite index (int32) +// Transparent (uint8) +// Path cost (float32) +// Voxel data length (uint32) +// Voxel data: RLE encoded (run_length: uint8, material: uint8) pairs +// If run_length == 255, read extended_length: uint16 for longer runs + +namespace { + const char MAGIC[4] = {'M', 'C', 'V', 'G'}; + const uint8_t FORMAT_VERSION = 1; + + // Write helpers + void writeU8(std::vector& buf, uint8_t v) { + buf.push_back(v); + } + + void writeU16(std::vector& buf, uint16_t v) { + buf.push_back(static_cast(v & 0xFF)); + buf.push_back(static_cast((v >> 8) & 0xFF)); + } + + void writeI32(std::vector& buf, int32_t v) { + buf.push_back(static_cast(v & 0xFF)); + buf.push_back(static_cast((v >> 8) & 0xFF)); + buf.push_back(static_cast((v >> 16) & 0xFF)); + buf.push_back(static_cast((v >> 24) & 0xFF)); + } + + void writeU32(std::vector& buf, uint32_t v) { + buf.push_back(static_cast(v & 0xFF)); + buf.push_back(static_cast((v >> 8) & 0xFF)); + buf.push_back(static_cast((v >> 16) & 0xFF)); + buf.push_back(static_cast((v >> 24) & 0xFF)); + } + + void writeF32(std::vector& buf, float v) { + static_assert(sizeof(float) == 4, "Expected 4-byte float"); + const uint8_t* bytes = reinterpret_cast(&v); + buf.insert(buf.end(), bytes, bytes + 4); + } + + void writeString(std::vector& buf, const std::string& s) { + uint16_t len = static_cast(std::min(s.size(), size_t(65535))); + writeU16(buf, len); + buf.insert(buf.end(), s.begin(), s.begin() + len); + } + + // Read helpers + class Reader { + const uint8_t* data_; + size_t size_; + size_t pos_; + public: + Reader(const uint8_t* data, size_t size) : data_(data), size_(size), pos_(0) {} + + bool hasBytes(size_t n) const { return pos_ + n <= size_; } + size_t position() const { return pos_; } + + bool readU8(uint8_t& v) { + if (!hasBytes(1)) return false; + v = data_[pos_++]; + return true; + } + + bool readU16(uint16_t& v) { + if (!hasBytes(2)) return false; + v = static_cast(data_[pos_]) | + (static_cast(data_[pos_ + 1]) << 8); + pos_ += 2; + return true; + } + + bool readI32(int32_t& v) { + if (!hasBytes(4)) return false; + v = static_cast(data_[pos_]) | + (static_cast(data_[pos_ + 1]) << 8) | + (static_cast(data_[pos_ + 2]) << 16) | + (static_cast(data_[pos_ + 3]) << 24); + pos_ += 4; + return true; + } + + bool readU32(uint32_t& v) { + if (!hasBytes(4)) return false; + v = static_cast(data_[pos_]) | + (static_cast(data_[pos_ + 1]) << 8) | + (static_cast(data_[pos_ + 2]) << 16) | + (static_cast(data_[pos_ + 3]) << 24); + pos_ += 4; + return true; + } + + bool readF32(float& v) { + if (!hasBytes(4)) return false; + static_assert(sizeof(float) == 4, "Expected 4-byte float"); + std::memcpy(&v, data_ + pos_, 4); + pos_ += 4; + return true; + } + + bool readString(std::string& s) { + uint16_t len; + if (!readU16(len)) return false; + if (!hasBytes(len)) return false; + s.assign(reinterpret_cast(data_ + pos_), len); + pos_ += len; + return true; + } + + bool readBytes(uint8_t* out, size_t n) { + if (!hasBytes(n)) return false; + std::memcpy(out, data_ + pos_, n); + pos_ += n; + return true; + } + }; + + // RLE encode voxel data + void rleEncode(const std::vector& data, std::vector& out) { + if (data.empty()) return; + + size_t i = 0; + while (i < data.size()) { + uint8_t mat = data[i]; + size_t runStart = i; + + // Count consecutive same materials + while (i < data.size() && data[i] == mat && (i - runStart) < 65535 + 255) { + i++; + } + + size_t runLen = i - runStart; + + if (runLen < 255) { + writeU8(out, static_cast(runLen)); + } else { + // Extended run: 255 marker + uint16 length + writeU8(out, 255); + writeU16(out, static_cast(runLen - 255)); + } + writeU8(out, mat); + } + } + + // RLE decode voxel data + bool rleDecode(Reader& reader, std::vector& data, size_t expectedSize) { + data.clear(); + data.reserve(expectedSize); + + while (data.size() < expectedSize) { + uint8_t runLen8; + if (!reader.readU8(runLen8)) return false; + + size_t runLen = runLen8; + if (runLen8 == 255) { + uint16_t extLen; + if (!reader.readU16(extLen)) return false; + runLen = 255 + extLen; + } + + uint8_t mat; + if (!reader.readU8(mat)) return false; + + for (size_t j = 0; j < runLen && data.size() < expectedSize; j++) { + data.push_back(mat); + } + } + + return data.size() == expectedSize; + } +} + +bool VoxelGrid::saveToBuffer(std::vector& buffer) const { + buffer.clear(); + buffer.reserve(1024 + data_.size()); // Rough estimate + + // Magic + buffer.insert(buffer.end(), MAGIC, MAGIC + 4); + + // Version + writeU8(buffer, FORMAT_VERSION); + + // Dimensions + writeI32(buffer, width_); + writeI32(buffer, height_); + writeI32(buffer, depth_); + + // Cell size + writeF32(buffer, cellSize_); + + // Materials + writeU8(buffer, static_cast(materials_.size())); + for (const auto& mat : materials_) { + writeString(buffer, mat.name); + writeU8(buffer, mat.color.r); + writeU8(buffer, mat.color.g); + writeU8(buffer, mat.color.b); + writeU8(buffer, mat.color.a); + writeI32(buffer, mat.spriteIndex); + writeU8(buffer, mat.transparent ? 1 : 0); + writeF32(buffer, mat.pathCost); + } + + // RLE encode voxel data + std::vector rleData; + rleEncode(data_, rleData); + + // Write RLE data length and data + writeU32(buffer, static_cast(rleData.size())); + buffer.insert(buffer.end(), rleData.begin(), rleData.end()); + + return true; +} + +bool VoxelGrid::loadFromBuffer(const uint8_t* data, size_t size) { + Reader reader(data, size); + + // Check magic + uint8_t magic[4]; + if (!reader.readBytes(magic, 4)) return false; + if (std::memcmp(magic, MAGIC, 4) != 0) return false; + + // Check version + uint8_t version; + if (!reader.readU8(version)) return false; + if (version != FORMAT_VERSION) return false; + + // Read dimensions + int32_t w, h, d; + if (!reader.readI32(w) || !reader.readI32(h) || !reader.readI32(d)) return false; + if (w <= 0 || h <= 0 || d <= 0) return false; + + // Read cell size + float cs; + if (!reader.readF32(cs)) return false; + if (cs <= 0.0f) return false; + + // Read materials + uint8_t matCount; + if (!reader.readU8(matCount)) return false; + + std::vector newMaterials; + newMaterials.reserve(matCount); + for (uint8_t i = 0; i < matCount; i++) { + VoxelMaterial mat; + if (!reader.readString(mat.name)) return false; + + uint8_t r, g, b, a; + if (!reader.readU8(r) || !reader.readU8(g) || !reader.readU8(b) || !reader.readU8(a)) + return false; + mat.color = sf::Color(r, g, b, a); + + int32_t sprite; + if (!reader.readI32(sprite)) return false; + mat.spriteIndex = sprite; + + uint8_t transp; + if (!reader.readU8(transp)) return false; + mat.transparent = (transp != 0); + + if (!reader.readF32(mat.pathCost)) return false; + + newMaterials.push_back(mat); + } + + // Read RLE data length + uint32_t rleLen; + if (!reader.readU32(rleLen)) return false; + + // Decode voxel data + size_t expectedVoxels = static_cast(w) * h * d; + std::vector newData; + if (!rleDecode(reader, newData, expectedVoxels)) return false; + + // Success - update the grid + width_ = w; + height_ = h; + depth_ = d; + cellSize_ = cs; + materials_ = std::move(newMaterials); + data_ = std::move(newData); + meshDirty_ = true; + + return true; +} + +bool VoxelGrid::save(const std::string& path) const { + std::vector buffer; + if (!saveToBuffer(buffer)) return false; + + std::ofstream file(path, std::ios::binary); + if (!file) return false; + + file.write(reinterpret_cast(buffer.data()), buffer.size()); + return file.good(); +} + +bool VoxelGrid::load(const std::string& path) { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) return false; + + std::streamsize size = file.tellg(); + if (size <= 0) return false; + + file.seekg(0, std::ios::beg); + + std::vector buffer(static_cast(size)); + if (!file.read(reinterpret_cast(buffer.data()), size)) return false; + + return loadFromBuffer(buffer.data(), buffer.size()); +} + +} // namespace mcrf diff --git a/src/3d/VoxelGrid.h b/src/3d/VoxelGrid.h new file mode 100644 index 0000000..740e8c7 --- /dev/null +++ b/src/3d/VoxelGrid.h @@ -0,0 +1,194 @@ +// VoxelGrid.h - Dense 3D voxel array with material palette +// Part of McRogueFace 3D Extension - Milestones 9-11 +#pragma once + +#include "../Common.h" +#include "Math3D.h" +#include "MeshLayer.h" // For MeshVertex (needed for std::vector) +#include +#include +#include +#include + +namespace mcrf { + +// ============================================================================= +// VoxelMaterial - Properties for a voxel material type +// ============================================================================= + +struct VoxelMaterial { + std::string name; + sf::Color color; // Fallback solid color + int spriteIndex = -1; // Texture atlas index (-1 = use color) + bool transparent = false; // For FOV projection and face culling + float pathCost = 1.0f; // Navigation cost multiplier (0 = impassable) + + VoxelMaterial() : name("unnamed"), color(sf::Color::White) {} + VoxelMaterial(const std::string& n, sf::Color c, int sprite = -1, + bool transp = false, float cost = 1.0f) + : name(n), color(c), spriteIndex(sprite), transparent(transp), pathCost(cost) {} +}; + +// ============================================================================= +// VoxelRegion - Portable voxel data for copy/paste operations (Milestone 11) +// ============================================================================= + +struct VoxelRegion { + int width, height, depth; + std::vector data; + + VoxelRegion() : width(0), height(0), depth(0) {} + VoxelRegion(int w, int h, int d) : width(w), height(h), depth(d), + data(static_cast(w) * h * d, 0) {} + + bool isValid() const { return width > 0 && height > 0 && depth > 0; } + size_t totalVoxels() const { return static_cast(width) * height * depth; } +}; + +// ============================================================================= +// VoxelGrid - Dense 3D array of material IDs +// ============================================================================= + +class VoxelGrid { +private: + int width_, height_, depth_; + float cellSize_; + std::vector data_; // Material ID per cell (0 = air) + std::vector materials_; + + // Transform + vec3 offset_; + float rotation_ = 0.0f; // Y-axis only, degrees + + // Mesh caching (Milestones 10, 13) + mutable bool meshDirty_ = true; + mutable std::vector cachedVertices_; + bool greedyMeshing_ = false; // Use greedy meshing algorithm + + // Index calculation (row-major: X varies fastest, then Y, then Z) + inline size_t index(int x, int y, int z) const { + return static_cast(z) * (width_ * height_) + + static_cast(y) * width_ + + static_cast(x); + } + +public: + // Constructor + VoxelGrid(int w, int h, int d, float cellSize = 1.0f); + + // Dimensions (read-only) + int width() const { return width_; } + int height() const { return height_; } + int depth() const { return depth_; } + float cellSize() const { return cellSize_; } + size_t totalVoxels() const { return static_cast(width_) * height_ * depth_; } + + // Per-voxel access + uint8_t get(int x, int y, int z) const; + void set(int x, int y, int z, uint8_t material); + bool isValid(int x, int y, int z) const; + + // Material palette + // Returns 1-indexed material ID (0 = air, always implicit) + uint8_t addMaterial(const VoxelMaterial& mat); + uint8_t addMaterial(const std::string& name, sf::Color color, + int spriteIndex = -1, bool transparent = false, + float pathCost = 1.0f); + const VoxelMaterial& getMaterial(uint8_t id) const; + size_t materialCount() const { return materials_.size(); } + + // Bulk operations - Basic + void fill(uint8_t material); + void clear() { fill(0); } + void fillBox(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t material); + + // Bulk operations - Milestone 11 + void fillBoxHollow(int x0, int y0, int z0, int x1, int y1, int z1, + uint8_t material, int thickness = 1); + void fillSphere(int cx, int cy, int cz, int radius, uint8_t material); + void fillCylinder(int cx, int cy, int cz, int radius, int height, uint8_t material); + void fillNoise(int x0, int y0, int z0, int x1, int y1, int z1, + uint8_t material, float threshold = 0.5f, + float scale = 0.1f, unsigned int seed = 0); + + // Copy/paste operations - Milestone 11 + VoxelRegion copyRegion(int x0, int y0, int z0, int x1, int y1, int z1) const; + void pasteRegion(const VoxelRegion& region, int x, int y, int z, bool skipAir = true); + + // Navigation projection - Milestone 12 + struct NavInfo { + float height = 0.0f; + bool walkable = false; + bool transparent = true; + float pathCost = 1.0f; + }; + + /// Project a single column to get navigation info + /// @param x X coordinate in voxel grid + /// @param z Z coordinate in voxel grid + /// @param headroom Required air voxels above floor (default 2) + /// @return Navigation info for this column + NavInfo projectColumn(int x, int z, int headroom = 2) const; + + // Transform + void setOffset(const vec3& offset) { offset_ = offset; } + void setOffset(float x, float y, float z) { offset_ = vec3(x, y, z); } + vec3 getOffset() const { return offset_; } + void setRotation(float degrees) { rotation_ = degrees; } + float getRotation() const { return rotation_; } + mat4 getModelMatrix() const; + + // Statistics + size_t countNonAir() const; + size_t countMaterial(uint8_t material) const; + + // Mesh caching (Milestones 10, 13) + /// Mark mesh as needing rebuild (called automatically by set/fill operations) + void markDirty() { meshDirty_ = true; } + + /// Check if mesh needs rebuild + bool isMeshDirty() const { return meshDirty_; } + + /// Get vertices for rendering (rebuilds mesh if dirty) + const std::vector& getVertices() const; + + /// Force immediate mesh rebuild + void rebuildMesh() const; + + /// Get vertex count after mesh generation + size_t vertexCount() const { return cachedVertices_.size(); } + + /// Enable/disable greedy meshing (Milestone 13) + /// Greedy meshing merges coplanar faces to reduce vertex count + void setGreedyMeshing(bool enabled) { greedyMeshing_ = enabled; markDirty(); } + bool isGreedyMeshingEnabled() const { return greedyMeshing_; } + + // Memory info (for debugging) + size_t memoryUsageBytes() const { + return data_.size() + materials_.size() * sizeof(VoxelMaterial); + } + + // Serialization (Milestone 14) + /// Save voxel grid to binary file + /// @param path File path to save to + /// @return true on success + bool save(const std::string& path) const; + + /// Load voxel grid from binary file + /// @param path File path to load from + /// @return true on success + bool load(const std::string& path); + + /// Save to memory buffer + /// @param buffer Output buffer (resized as needed) + /// @return true on success + bool saveToBuffer(std::vector& buffer) const; + + /// Load from memory buffer + /// @param data Buffer to load from + /// @param size Buffer size + /// @return true on success + bool loadFromBuffer(const uint8_t* data, size_t size); +}; + +} // namespace mcrf diff --git a/src/3d/VoxelMesher.cpp b/src/3d/VoxelMesher.cpp new file mode 100644 index 0000000..bb85278 --- /dev/null +++ b/src/3d/VoxelMesher.cpp @@ -0,0 +1,317 @@ +// VoxelMesher.cpp - Face-culled mesh generation for VoxelGrid +// Part of McRogueFace 3D Extension - Milestone 10 + +#include "VoxelMesher.h" +#include + +namespace mcrf { + +void VoxelMesher::generateMesh(const VoxelGrid& grid, std::vector& outVertices) { + const float cs = grid.cellSize(); + + for (int z = 0; z < grid.depth(); z++) { + for (int y = 0; y < grid.height(); y++) { + for (int x = 0; x < grid.width(); x++) { + uint8_t mat = grid.get(x, y, z); + if (mat == 0) continue; // Skip air + + const VoxelMaterial& material = grid.getMaterial(mat); + + // Voxel center in local space + vec3 center((x + 0.5f) * cs, (y + 0.5f) * cs, (z + 0.5f) * cs); + + // Check each face direction + // +X face + if (shouldGenerateFace(grid, x, y, z, x + 1, y, z)) { + emitFace(outVertices, center, vec3(1, 0, 0), cs, material); + } + // -X face + if (shouldGenerateFace(grid, x, y, z, x - 1, y, z)) { + emitFace(outVertices, center, vec3(-1, 0, 0), cs, material); + } + // +Y face (top) + if (shouldGenerateFace(grid, x, y, z, x, y + 1, z)) { + emitFace(outVertices, center, vec3(0, 1, 0), cs, material); + } + // -Y face (bottom) + if (shouldGenerateFace(grid, x, y, z, x, y - 1, z)) { + emitFace(outVertices, center, vec3(0, -1, 0), cs, material); + } + // +Z face + if (shouldGenerateFace(grid, x, y, z, x, y, z + 1)) { + emitFace(outVertices, center, vec3(0, 0, 1), cs, material); + } + // -Z face + if (shouldGenerateFace(grid, x, y, z, x, y, z - 1)) { + emitFace(outVertices, center, vec3(0, 0, -1), cs, material); + } + } + } + } +} + +bool VoxelMesher::shouldGenerateFace(const VoxelGrid& grid, + int x, int y, int z, + int nx, int ny, int nz) { + // Out of bounds = air, so generate face + if (!grid.isValid(nx, ny, nz)) { + return true; + } + + uint8_t neighbor = grid.get(nx, ny, nz); + + // Air neighbor = generate face + if (neighbor == 0) { + return true; + } + + // Check if neighbor is transparent + // Transparent materials allow faces to be visible behind them + return grid.getMaterial(neighbor).transparent; +} + +void VoxelMesher::emitQuad(std::vector& vertices, + const vec3& corner, + const vec3& uAxis, + const vec3& vAxis, + const vec3& normal, + const VoxelMaterial& material) { + // 4 corners of the quad + vec3 corners[4] = { + corner, // 0: origin + corner + uAxis, // 1: +U + corner + uAxis + vAxis, // 2: +U+V + corner + vAxis // 3: +V + }; + + // Calculate UV based on quad size (for potential texture tiling) + float uLen = uAxis.length(); + float vLen = vAxis.length(); + vec2 uvs[4] = { + vec2(0, 0), // 0 + vec2(uLen, 0), // 1 + vec2(uLen, vLen), // 2 + vec2(0, vLen) // 3 + }; + + // Color from material + vec4 color( + material.color.r / 255.0f, + material.color.g / 255.0f, + material.color.b / 255.0f, + material.color.a / 255.0f + ); + + // Emit 2 triangles (6 vertices) - CCW winding + // Triangle 1: 0-2-1 + vertices.push_back(MeshVertex(corners[0], uvs[0], normal, color)); + vertices.push_back(MeshVertex(corners[2], uvs[2], normal, color)); + vertices.push_back(MeshVertex(corners[1], uvs[1], normal, color)); + + // Triangle 2: 0-3-2 + vertices.push_back(MeshVertex(corners[0], uvs[0], normal, color)); + vertices.push_back(MeshVertex(corners[3], uvs[3], normal, color)); + vertices.push_back(MeshVertex(corners[2], uvs[2], normal, color)); +} + +void VoxelMesher::generateGreedyMesh(const VoxelGrid& grid, std::vector& outVertices) { + const float cs = grid.cellSize(); + const int width = grid.width(); + const int height = grid.height(); + const int depth = grid.depth(); + + // Process each face direction + // Axis 0 = X, 1 = Y, 2 = Z + // Direction: +1 = positive, -1 = negative + + for (int axis = 0; axis < 3; axis++) { + for (int dir = -1; dir <= 1; dir += 2) { + // Determine slice dimensions based on axis + int sliceW, sliceH, sliceCount; + if (axis == 0) { // X-axis: slices in YZ plane + sliceW = depth; + sliceH = height; + sliceCount = width; + } else if (axis == 1) { // Y-axis: slices in XZ plane + sliceW = width; + sliceH = depth; + sliceCount = height; + } else { // Z-axis: slices in XY plane + sliceW = width; + sliceH = height; + sliceCount = depth; + } + + // Create mask for this slice + std::vector mask(sliceW * sliceH); + + // Process each slice + for (int sliceIdx = 0; sliceIdx < sliceCount; sliceIdx++) { + // Fill mask with material IDs where faces should be generated + std::fill(mask.begin(), mask.end(), 0); + + for (int v = 0; v < sliceH; v++) { + for (int u = 0; u < sliceW; u++) { + // Map (u, v, sliceIdx) to (x, y, z) based on axis + int x, y, z, nx, ny, nz; + if (axis == 0) { + x = sliceIdx; y = v; z = u; + nx = x + dir; ny = y; nz = z; + } else if (axis == 1) { + x = u; y = sliceIdx; z = v; + nx = x; ny = y + dir; nz = z; + } else { + x = u; y = v; z = sliceIdx; + nx = x; ny = y; nz = z + dir; + } + + uint8_t mat = grid.get(x, y, z); + if (mat == 0) continue; + + // Check if face should be generated + if (shouldGenerateFace(grid, x, y, z, nx, ny, nz)) { + mask[v * sliceW + u] = mat; + } + } + } + + // Greedy rectangle merging + for (int v = 0; v < sliceH; v++) { + for (int u = 0; u < sliceW; ) { + uint8_t mat = mask[v * sliceW + u]; + if (mat == 0) { + u++; + continue; + } + + // Find width of rectangle (extend along U) + int rectW = 1; + while (u + rectW < sliceW && mask[v * sliceW + u + rectW] == mat) { + rectW++; + } + + // Find height of rectangle (extend along V) + int rectH = 1; + bool canExtend = true; + while (canExtend && v + rectH < sliceH) { + // Check if entire row matches + for (int i = 0; i < rectW; i++) { + if (mask[(v + rectH) * sliceW + u + i] != mat) { + canExtend = false; + break; + } + } + if (canExtend) rectH++; + } + + // Clear mask for merged area + for (int dv = 0; dv < rectH; dv++) { + for (int du = 0; du < rectW; du++) { + mask[(v + dv) * sliceW + u + du] = 0; + } + } + + // Emit quad for this merged rectangle + const VoxelMaterial& material = grid.getMaterial(mat); + + // Calculate corner and axes based on face direction + vec3 corner, uAxis, vAxis, normal; + + if (axis == 0) { // X-facing + float faceX = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs; + corner = vec3(faceX, v * cs, u * cs); + uAxis = vec3(0, 0, rectW * cs); + vAxis = vec3(0, rectH * cs, 0); + normal = vec3(static_cast(dir), 0, 0); + // Flip winding for back faces + if (dir < 0) std::swap(uAxis, vAxis); + } else if (axis == 1) { // Y-facing + float faceY = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs; + corner = vec3(u * cs, faceY, v * cs); + uAxis = vec3(rectW * cs, 0, 0); + vAxis = vec3(0, 0, rectH * cs); + normal = vec3(0, static_cast(dir), 0); + if (dir < 0) std::swap(uAxis, vAxis); + } else { // Z-facing + float faceZ = (dir > 0) ? (sliceIdx + 1) * cs : sliceIdx * cs; + corner = vec3(u * cs, v * cs, faceZ); + uAxis = vec3(rectW * cs, 0, 0); + vAxis = vec3(0, rectH * cs, 0); + normal = vec3(0, 0, static_cast(dir)); + if (dir < 0) std::swap(uAxis, vAxis); + } + + emitQuad(outVertices, corner, uAxis, vAxis, normal, material); + + u += rectW; + } + } + } + } + } +} + +void VoxelMesher::emitFace(std::vector& vertices, + const vec3& center, + const vec3& normal, + float size, + const VoxelMaterial& material) { + // Calculate face corners based on normal direction + vec3 up, right; + + if (std::abs(normal.y) > 0.5f) { + // Horizontal face (floor/ceiling) + // For +Y (top), we want the face to look correct from above + // For -Y (bottom), we want it to look correct from below + up = vec3(0, 0, normal.y); // Z direction based on face direction + right = vec3(1, 0, 0); // Always X axis for horizontal faces + } else if (std::abs(normal.x) > 0.5f) { + // X-facing wall + up = vec3(0, 1, 0); // Y axis is up + right = vec3(0, 0, normal.x); // Z direction based on face direction + } else { + // Z-facing wall + up = vec3(0, 1, 0); // Y axis is up + right = vec3(-normal.z, 0, 0); // X direction based on face direction + } + + float halfSize = size * 0.5f; + vec3 faceCenter = center + normal * halfSize; + + // 4 corners of the face + vec3 corners[4] = { + faceCenter - right * halfSize - up * halfSize, // Bottom-left + faceCenter + right * halfSize - up * halfSize, // Bottom-right + faceCenter + right * halfSize + up * halfSize, // Top-right + faceCenter - right * halfSize + up * halfSize // Top-left + }; + + // UV coordinates (solid color or single sprite tile) + vec2 uvs[4] = { + vec2(0, 0), // Bottom-left + vec2(1, 0), // Bottom-right + vec2(1, 1), // Top-right + vec2(0, 1) // Top-left + }; + + // Color from material (convert 0-255 to 0-1) + vec4 color( + material.color.r / 255.0f, + material.color.g / 255.0f, + material.color.b / 255.0f, + material.color.a / 255.0f + ); + + // Emit 2 triangles (6 vertices) - CCW winding for OpenGL front faces + // Triangle 1: 0-2-1 (bottom-left, top-right, bottom-right) - CCW + vertices.push_back(MeshVertex(corners[0], uvs[0], normal, color)); + vertices.push_back(MeshVertex(corners[2], uvs[2], normal, color)); + vertices.push_back(MeshVertex(corners[1], uvs[1], normal, color)); + + // Triangle 2: 0-3-2 (bottom-left, top-left, top-right) - CCW + vertices.push_back(MeshVertex(corners[0], uvs[0], normal, color)); + vertices.push_back(MeshVertex(corners[3], uvs[3], normal, color)); + vertices.push_back(MeshVertex(corners[2], uvs[2], normal, color)); +} + +} // namespace mcrf diff --git a/src/3d/VoxelMesher.h b/src/3d/VoxelMesher.h new file mode 100644 index 0000000..a0b3c4d --- /dev/null +++ b/src/3d/VoxelMesher.h @@ -0,0 +1,80 @@ +// VoxelMesher.h - Face-culled mesh generation for VoxelGrid +// Part of McRogueFace 3D Extension - Milestones 10, 13 +#pragma once + +#include "VoxelGrid.h" +#include "MeshLayer.h" // For MeshVertex +#include + +namespace mcrf { + +// ============================================================================= +// VoxelMesher - Static class for generating triangle meshes from VoxelGrid +// ============================================================================= + +class VoxelMesher { +public: + /// Generate face-culled mesh from voxel data (simple per-voxel faces) + /// Output vertices in local space (model matrix applies world transform) + /// @param grid The VoxelGrid to generate mesh from + /// @param outVertices Output vector of vertices (appended to, not cleared) + static void generateMesh( + const VoxelGrid& grid, + std::vector& outVertices + ); + + /// Generate mesh using greedy meshing algorithm (Milestone 13) + /// Merges coplanar faces of the same material into larger rectangles, + /// significantly reducing vertex count for uniform regions. + /// @param grid The VoxelGrid to generate mesh from + /// @param outVertices Output vector of vertices (appended to, not cleared) + static void generateGreedyMesh( + const VoxelGrid& grid, + std::vector& outVertices + ); + +private: + /// Check if face should be generated (neighbor is air or transparent) + /// @param grid The VoxelGrid + /// @param x, y, z Current voxel position + /// @param nx, ny, nz Neighbor voxel position + /// @return true if face should be generated + static bool shouldGenerateFace( + const VoxelGrid& grid, + int x, int y, int z, + int nx, int ny, int nz + ); + + /// Generate a single face (2 triangles = 6 vertices) + /// @param vertices Output vector to append vertices to + /// @param center Center of the voxel + /// @param normal Face normal direction + /// @param size Voxel cell size + /// @param material Material for coloring + static void emitFace( + std::vector& vertices, + const vec3& center, + const vec3& normal, + float size, + const VoxelMaterial& material + ); + + /// Generate a rectangular face (2 triangles = 6 vertices) + /// Used by greedy meshing to emit merged quads + /// @param vertices Output vector to append vertices to + /// @param corner Base corner of the rectangle + /// @param uAxis Direction and length along U axis + /// @param vAxis Direction and length along V axis + /// @param normal Face normal direction + /// @param material Material for coloring + static void emitQuad( + std::vector& vertices, + const vec3& corner, + const vec3& uAxis, + const vec3& vAxis, + const vec3& normal, + const VoxelMaterial& material + ); +}; + +} // namespace mcrf diff --git a/src/3d/cgltf.h b/src/3d/cgltf.h new file mode 100644 index 0000000..316a11d --- /dev/null +++ b/src/3d/cgltf.h @@ -0,0 +1,7240 @@ +/** + * cgltf - a single-file glTF 2.0 parser written in C99. + * + * Version: 1.15 + * + * Website: https://github.com/jkuhlmann/cgltf + * + * Distributed under the MIT License, see notice at the end of this file. + * + * Building: + * Include this file where you need the struct and function + * declarations. Have exactly one source file where you define + * `CGLTF_IMPLEMENTATION` before including this file to get the + * function definitions. + * + * Reference: + * `cgltf_result cgltf_parse(const cgltf_options*, const void*, + * cgltf_size, cgltf_data**)` parses both glTF and GLB data. If + * this function returns `cgltf_result_success`, you have to call + * `cgltf_free()` on the created `cgltf_data*` variable. + * Note that contents of external files for buffers and images are not + * automatically loaded. You'll need to read these files yourself using + * URIs in the `cgltf_data` structure. + * + * `cgltf_options` is the struct passed to `cgltf_parse()` to control + * parts of the parsing process. You can use it to force the file type + * and provide memory allocation as well as file operation callbacks. + * Should be zero-initialized to trigger default behavior. + * + * `cgltf_data` is the struct allocated and filled by `cgltf_parse()`. + * It generally mirrors the glTF format as described by the spec (see + * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0). + * + * `void cgltf_free(cgltf_data*)` frees the allocated `cgltf_data` + * variable. + * + * `cgltf_result cgltf_load_buffers(const cgltf_options*, cgltf_data*, + * const char* gltf_path)` can be optionally called to open and read buffer + * files using the `FILE*` APIs. The `gltf_path` argument is the path to + * the original glTF file, which allows the parser to resolve the path to + * buffer files. + * + * `cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, + * cgltf_size size, const char* base64, void** out_data)` decodes + * base64-encoded data content. Used internally by `cgltf_load_buffers()`. + * This is useful when decoding data URIs in images. + * + * `cgltf_result cgltf_parse_file(const cgltf_options* options, const + * char* path, cgltf_data** out_data)` can be used to open the given + * file using `FILE*` APIs and parse the data using `cgltf_parse()`. + * + * `cgltf_result cgltf_validate(cgltf_data*)` can be used to do additional + * checks to make sure the parsed glTF data is valid. + * + * `cgltf_node_transform_local` converts the translation / rotation / scale properties of a node + * into a mat4. + * + * `cgltf_node_transform_world` calls `cgltf_node_transform_local` on every ancestor in order + * to compute the root-to-node transformation. + * + * `cgltf_accessor_unpack_floats` reads in the data from an accessor, applies sparse data (if any), + * and converts them to floating point. Assumes that `cgltf_load_buffers` has already been called. + * By passing null for the output pointer, users can find out how many floats are required in the + * output buffer. + * + * `cgltf_accessor_unpack_indices` reads in the index data from an accessor. Assumes that + * `cgltf_load_buffers` has already been called. By passing null for the output pointer, users can + * find out how many indices are required in the output buffer. Returns 0 if the accessor is + * sparse or if the output component size is less than the accessor's component size. + * + * `cgltf_num_components` is a tiny utility that tells you the dimensionality of + * a certain accessor type. This can be used before `cgltf_accessor_unpack_floats` to help allocate + * the necessary amount of memory. `cgltf_component_size` and `cgltf_calc_size` exist for + * similar purposes. + * + * `cgltf_accessor_read_float` reads a certain element from a non-sparse accessor and converts it to + * floating point, assuming that `cgltf_load_buffers` has already been called. The passed-in element + * size is the number of floats in the output buffer, which should be in the range [1, 16]. Returns + * false if the passed-in element_size is too small, or if the accessor is sparse. + * + * `cgltf_accessor_read_uint` is similar to its floating-point counterpart, but limited to reading + * vector types and does not support matrix types. The passed-in element size is the number of uints + * in the output buffer, which should be in the range [1, 4]. Returns false if the passed-in + * element_size is too small, or if the accessor is sparse. + * + * `cgltf_accessor_read_index` is similar to its floating-point counterpart, but it returns size_t + * and only works with single-component data types. + * + * `cgltf_copy_extras_json` allows users to retrieve the "extras" data that can be attached to many + * glTF objects (which can be arbitrary JSON data). This is a legacy function, consider using + * cgltf_extras::data directly instead. You can parse this data using your own JSON parser + * or, if you've included the cgltf implementation using the integrated JSMN JSON parser. + */ +#ifndef CGLTF_H_INCLUDED__ +#define CGLTF_H_INCLUDED__ + +#include +#include /* For uint8_t, uint32_t */ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef size_t cgltf_size; +typedef long long int cgltf_ssize; +typedef float cgltf_float; +typedef int cgltf_int; +typedef unsigned int cgltf_uint; +typedef int cgltf_bool; + +typedef enum cgltf_file_type +{ + cgltf_file_type_invalid, + cgltf_file_type_gltf, + cgltf_file_type_glb, + cgltf_file_type_max_enum +} cgltf_file_type; + +typedef enum cgltf_result +{ + cgltf_result_success, + cgltf_result_data_too_short, + cgltf_result_unknown_format, + cgltf_result_invalid_json, + cgltf_result_invalid_gltf, + cgltf_result_invalid_options, + cgltf_result_file_not_found, + cgltf_result_io_error, + cgltf_result_out_of_memory, + cgltf_result_legacy_gltf, + cgltf_result_max_enum +} cgltf_result; + +typedef struct cgltf_memory_options +{ + void* (*alloc_func)(void* user, cgltf_size size); + void (*free_func) (void* user, void* ptr); + void* user_data; +} cgltf_memory_options; + +typedef struct cgltf_file_options +{ + cgltf_result(*read)(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, const char* path, cgltf_size* size, void** data); + void (*release)(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, void* data, cgltf_size size); + void* user_data; +} cgltf_file_options; + +typedef struct cgltf_options +{ + cgltf_file_type type; /* invalid == auto detect */ + cgltf_size json_token_count; /* 0 == auto */ + cgltf_memory_options memory; + cgltf_file_options file; +} cgltf_options; + +typedef enum cgltf_buffer_view_type +{ + cgltf_buffer_view_type_invalid, + cgltf_buffer_view_type_indices, + cgltf_buffer_view_type_vertices, + cgltf_buffer_view_type_max_enum +} cgltf_buffer_view_type; + +typedef enum cgltf_attribute_type +{ + cgltf_attribute_type_invalid, + cgltf_attribute_type_position, + cgltf_attribute_type_normal, + cgltf_attribute_type_tangent, + cgltf_attribute_type_texcoord, + cgltf_attribute_type_color, + cgltf_attribute_type_joints, + cgltf_attribute_type_weights, + cgltf_attribute_type_custom, + cgltf_attribute_type_max_enum +} cgltf_attribute_type; + +typedef enum cgltf_component_type +{ + cgltf_component_type_invalid, + cgltf_component_type_r_8, /* BYTE */ + cgltf_component_type_r_8u, /* UNSIGNED_BYTE */ + cgltf_component_type_r_16, /* SHORT */ + cgltf_component_type_r_16u, /* UNSIGNED_SHORT */ + cgltf_component_type_r_32u, /* UNSIGNED_INT */ + cgltf_component_type_r_32f, /* FLOAT */ + cgltf_component_type_max_enum +} cgltf_component_type; + +typedef enum cgltf_type +{ + cgltf_type_invalid, + cgltf_type_scalar, + cgltf_type_vec2, + cgltf_type_vec3, + cgltf_type_vec4, + cgltf_type_mat2, + cgltf_type_mat3, + cgltf_type_mat4, + cgltf_type_max_enum +} cgltf_type; + +typedef enum cgltf_primitive_type +{ + cgltf_primitive_type_invalid, + cgltf_primitive_type_points, + cgltf_primitive_type_lines, + cgltf_primitive_type_line_loop, + cgltf_primitive_type_line_strip, + cgltf_primitive_type_triangles, + cgltf_primitive_type_triangle_strip, + cgltf_primitive_type_triangle_fan, + cgltf_primitive_type_max_enum +} cgltf_primitive_type; + +typedef enum cgltf_alpha_mode +{ + cgltf_alpha_mode_opaque, + cgltf_alpha_mode_mask, + cgltf_alpha_mode_blend, + cgltf_alpha_mode_max_enum +} cgltf_alpha_mode; + +typedef enum cgltf_animation_path_type { + cgltf_animation_path_type_invalid, + cgltf_animation_path_type_translation, + cgltf_animation_path_type_rotation, + cgltf_animation_path_type_scale, + cgltf_animation_path_type_weights, + cgltf_animation_path_type_max_enum +} cgltf_animation_path_type; + +typedef enum cgltf_interpolation_type { + cgltf_interpolation_type_linear, + cgltf_interpolation_type_step, + cgltf_interpolation_type_cubic_spline, + cgltf_interpolation_type_max_enum +} cgltf_interpolation_type; + +typedef enum cgltf_camera_type { + cgltf_camera_type_invalid, + cgltf_camera_type_perspective, + cgltf_camera_type_orthographic, + cgltf_camera_type_max_enum +} cgltf_camera_type; + +typedef enum cgltf_light_type { + cgltf_light_type_invalid, + cgltf_light_type_directional, + cgltf_light_type_point, + cgltf_light_type_spot, + cgltf_light_type_max_enum +} cgltf_light_type; + +typedef enum cgltf_data_free_method { + cgltf_data_free_method_none, + cgltf_data_free_method_file_release, + cgltf_data_free_method_memory_free, + cgltf_data_free_method_max_enum +} cgltf_data_free_method; + +typedef struct cgltf_extras { + cgltf_size start_offset; /* this field is deprecated and will be removed in the future; use data instead */ + cgltf_size end_offset; /* this field is deprecated and will be removed in the future; use data instead */ + + char* data; +} cgltf_extras; + +typedef struct cgltf_extension { + char* name; + char* data; +} cgltf_extension; + +typedef struct cgltf_buffer +{ + char* name; + cgltf_size size; + char* uri; + void* data; /* loaded by cgltf_load_buffers */ + cgltf_data_free_method data_free_method; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_buffer; + +typedef enum cgltf_meshopt_compression_mode { + cgltf_meshopt_compression_mode_invalid, + cgltf_meshopt_compression_mode_attributes, + cgltf_meshopt_compression_mode_triangles, + cgltf_meshopt_compression_mode_indices, + cgltf_meshopt_compression_mode_max_enum +} cgltf_meshopt_compression_mode; + +typedef enum cgltf_meshopt_compression_filter { + cgltf_meshopt_compression_filter_none, + cgltf_meshopt_compression_filter_octahedral, + cgltf_meshopt_compression_filter_quaternion, + cgltf_meshopt_compression_filter_exponential, + cgltf_meshopt_compression_filter_color, + cgltf_meshopt_compression_filter_max_enum +} cgltf_meshopt_compression_filter; + +typedef struct cgltf_meshopt_compression +{ + cgltf_buffer* buffer; + cgltf_size offset; + cgltf_size size; + cgltf_size stride; + cgltf_size count; + cgltf_meshopt_compression_mode mode; + cgltf_meshopt_compression_filter filter; + cgltf_bool is_khr; +} cgltf_meshopt_compression; + +typedef struct cgltf_buffer_view +{ + char *name; + cgltf_buffer* buffer; + cgltf_size offset; + cgltf_size size; + cgltf_size stride; /* 0 == automatically determined by accessor */ + cgltf_buffer_view_type type; + void* data; /* overrides buffer->data if present, filled by extensions */ + cgltf_bool has_meshopt_compression; + cgltf_meshopt_compression meshopt_compression; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_buffer_view; + +typedef struct cgltf_accessor_sparse +{ + cgltf_size count; + cgltf_buffer_view* indices_buffer_view; + cgltf_size indices_byte_offset; + cgltf_component_type indices_component_type; + cgltf_buffer_view* values_buffer_view; + cgltf_size values_byte_offset; +} cgltf_accessor_sparse; + +typedef struct cgltf_accessor +{ + char* name; + cgltf_component_type component_type; + cgltf_bool normalized; + cgltf_type type; + cgltf_size offset; + cgltf_size count; + cgltf_size stride; + cgltf_buffer_view* buffer_view; + cgltf_bool has_min; + cgltf_float min[16]; + cgltf_bool has_max; + cgltf_float max[16]; + cgltf_bool is_sparse; + cgltf_accessor_sparse sparse; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_accessor; + +typedef struct cgltf_attribute +{ + char* name; + cgltf_attribute_type type; + cgltf_int index; + cgltf_accessor* data; +} cgltf_attribute; + +typedef struct cgltf_image +{ + char* name; + char* uri; + cgltf_buffer_view* buffer_view; + char* mime_type; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_image; + +typedef enum cgltf_filter_type { + cgltf_filter_type_undefined = 0, + cgltf_filter_type_nearest = 9728, + cgltf_filter_type_linear = 9729, + cgltf_filter_type_nearest_mipmap_nearest = 9984, + cgltf_filter_type_linear_mipmap_nearest = 9985, + cgltf_filter_type_nearest_mipmap_linear = 9986, + cgltf_filter_type_linear_mipmap_linear = 9987 +} cgltf_filter_type; + +typedef enum cgltf_wrap_mode { + cgltf_wrap_mode_clamp_to_edge = 33071, + cgltf_wrap_mode_mirrored_repeat = 33648, + cgltf_wrap_mode_repeat = 10497 +} cgltf_wrap_mode; + +typedef struct cgltf_sampler +{ + char* name; + cgltf_filter_type mag_filter; + cgltf_filter_type min_filter; + cgltf_wrap_mode wrap_s; + cgltf_wrap_mode wrap_t; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_sampler; + +typedef struct cgltf_texture +{ + char* name; + cgltf_image* image; + cgltf_sampler* sampler; + cgltf_bool has_basisu; + cgltf_image* basisu_image; + cgltf_bool has_webp; + cgltf_image* webp_image; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_texture; + +typedef struct cgltf_texture_transform +{ + cgltf_float offset[2]; + cgltf_float rotation; + cgltf_float scale[2]; + cgltf_bool has_texcoord; + cgltf_int texcoord; +} cgltf_texture_transform; + +typedef struct cgltf_texture_view +{ + cgltf_texture* texture; + cgltf_int texcoord; + cgltf_float scale; /* equivalent to strength for occlusion_texture */ + cgltf_bool has_transform; + cgltf_texture_transform transform; +} cgltf_texture_view; + +typedef struct cgltf_pbr_metallic_roughness +{ + cgltf_texture_view base_color_texture; + cgltf_texture_view metallic_roughness_texture; + + cgltf_float base_color_factor[4]; + cgltf_float metallic_factor; + cgltf_float roughness_factor; +} cgltf_pbr_metallic_roughness; + +typedef struct cgltf_pbr_specular_glossiness +{ + cgltf_texture_view diffuse_texture; + cgltf_texture_view specular_glossiness_texture; + + cgltf_float diffuse_factor[4]; + cgltf_float specular_factor[3]; + cgltf_float glossiness_factor; +} cgltf_pbr_specular_glossiness; + +typedef struct cgltf_clearcoat +{ + cgltf_texture_view clearcoat_texture; + cgltf_texture_view clearcoat_roughness_texture; + cgltf_texture_view clearcoat_normal_texture; + + cgltf_float clearcoat_factor; + cgltf_float clearcoat_roughness_factor; +} cgltf_clearcoat; + +typedef struct cgltf_transmission +{ + cgltf_texture_view transmission_texture; + cgltf_float transmission_factor; +} cgltf_transmission; + +typedef struct cgltf_ior +{ + cgltf_float ior; +} cgltf_ior; + +typedef struct cgltf_specular +{ + cgltf_texture_view specular_texture; + cgltf_texture_view specular_color_texture; + cgltf_float specular_color_factor[3]; + cgltf_float specular_factor; +} cgltf_specular; + +typedef struct cgltf_volume +{ + cgltf_texture_view thickness_texture; + cgltf_float thickness_factor; + cgltf_float attenuation_color[3]; + cgltf_float attenuation_distance; +} cgltf_volume; + +typedef struct cgltf_sheen +{ + cgltf_texture_view sheen_color_texture; + cgltf_float sheen_color_factor[3]; + cgltf_texture_view sheen_roughness_texture; + cgltf_float sheen_roughness_factor; +} cgltf_sheen; + +typedef struct cgltf_emissive_strength +{ + cgltf_float emissive_strength; +} cgltf_emissive_strength; + +typedef struct cgltf_iridescence +{ + cgltf_float iridescence_factor; + cgltf_texture_view iridescence_texture; + cgltf_float iridescence_ior; + cgltf_float iridescence_thickness_min; + cgltf_float iridescence_thickness_max; + cgltf_texture_view iridescence_thickness_texture; +} cgltf_iridescence; + +typedef struct cgltf_diffuse_transmission +{ + cgltf_texture_view diffuse_transmission_texture; + cgltf_float diffuse_transmission_factor; + cgltf_float diffuse_transmission_color_factor[3]; + cgltf_texture_view diffuse_transmission_color_texture; +} cgltf_diffuse_transmission; + +typedef struct cgltf_anisotropy +{ + cgltf_float anisotropy_strength; + cgltf_float anisotropy_rotation; + cgltf_texture_view anisotropy_texture; +} cgltf_anisotropy; + +typedef struct cgltf_dispersion +{ + cgltf_float dispersion; +} cgltf_dispersion; + +typedef struct cgltf_material +{ + char* name; + cgltf_bool has_pbr_metallic_roughness; + cgltf_bool has_pbr_specular_glossiness; + cgltf_bool has_clearcoat; + cgltf_bool has_transmission; + cgltf_bool has_volume; + cgltf_bool has_ior; + cgltf_bool has_specular; + cgltf_bool has_sheen; + cgltf_bool has_emissive_strength; + cgltf_bool has_iridescence; + cgltf_bool has_diffuse_transmission; + cgltf_bool has_anisotropy; + cgltf_bool has_dispersion; + cgltf_pbr_metallic_roughness pbr_metallic_roughness; + cgltf_pbr_specular_glossiness pbr_specular_glossiness; + cgltf_clearcoat clearcoat; + cgltf_ior ior; + cgltf_specular specular; + cgltf_sheen sheen; + cgltf_transmission transmission; + cgltf_volume volume; + cgltf_emissive_strength emissive_strength; + cgltf_iridescence iridescence; + cgltf_diffuse_transmission diffuse_transmission; + cgltf_anisotropy anisotropy; + cgltf_dispersion dispersion; + cgltf_texture_view normal_texture; + cgltf_texture_view occlusion_texture; + cgltf_texture_view emissive_texture; + cgltf_float emissive_factor[3]; + cgltf_alpha_mode alpha_mode; + cgltf_float alpha_cutoff; + cgltf_bool double_sided; + cgltf_bool unlit; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_material; + +typedef struct cgltf_material_mapping +{ + cgltf_size variant; + cgltf_material* material; + cgltf_extras extras; +} cgltf_material_mapping; + +typedef struct cgltf_morph_target { + cgltf_attribute* attributes; + cgltf_size attributes_count; +} cgltf_morph_target; + +typedef struct cgltf_draco_mesh_compression { + cgltf_buffer_view* buffer_view; + cgltf_attribute* attributes; + cgltf_size attributes_count; +} cgltf_draco_mesh_compression; + +typedef struct cgltf_mesh_gpu_instancing { + cgltf_attribute* attributes; + cgltf_size attributes_count; +} cgltf_mesh_gpu_instancing; + +typedef struct cgltf_primitive { + cgltf_primitive_type type; + cgltf_accessor* indices; + cgltf_material* material; + cgltf_attribute* attributes; + cgltf_size attributes_count; + cgltf_morph_target* targets; + cgltf_size targets_count; + cgltf_extras extras; + cgltf_bool has_draco_mesh_compression; + cgltf_draco_mesh_compression draco_mesh_compression; + cgltf_material_mapping* mappings; + cgltf_size mappings_count; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_primitive; + +typedef struct cgltf_mesh { + char* name; + cgltf_primitive* primitives; + cgltf_size primitives_count; + cgltf_float* weights; + cgltf_size weights_count; + char** target_names; + cgltf_size target_names_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_mesh; + +typedef struct cgltf_node cgltf_node; + +typedef struct cgltf_skin { + char* name; + cgltf_node** joints; + cgltf_size joints_count; + cgltf_node* skeleton; + cgltf_accessor* inverse_bind_matrices; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_skin; + +typedef struct cgltf_camera_perspective { + cgltf_bool has_aspect_ratio; + cgltf_float aspect_ratio; + cgltf_float yfov; + cgltf_bool has_zfar; + cgltf_float zfar; + cgltf_float znear; + cgltf_extras extras; +} cgltf_camera_perspective; + +typedef struct cgltf_camera_orthographic { + cgltf_float xmag; + cgltf_float ymag; + cgltf_float zfar; + cgltf_float znear; + cgltf_extras extras; +} cgltf_camera_orthographic; + +typedef struct cgltf_camera { + char* name; + cgltf_camera_type type; + union { + cgltf_camera_perspective perspective; + cgltf_camera_orthographic orthographic; + } data; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_camera; + +typedef struct cgltf_light { + char* name; + cgltf_float color[3]; + cgltf_float intensity; + cgltf_light_type type; + cgltf_float range; + cgltf_float spot_inner_cone_angle; + cgltf_float spot_outer_cone_angle; + cgltf_extras extras; +} cgltf_light; + +struct cgltf_node { + char* name; + cgltf_node* parent; + cgltf_node** children; + cgltf_size children_count; + cgltf_skin* skin; + cgltf_mesh* mesh; + cgltf_camera* camera; + cgltf_light* light; + cgltf_float* weights; + cgltf_size weights_count; + cgltf_bool has_translation; + cgltf_bool has_rotation; + cgltf_bool has_scale; + cgltf_bool has_matrix; + cgltf_float translation[3]; + cgltf_float rotation[4]; + cgltf_float scale[3]; + cgltf_float matrix[16]; + cgltf_extras extras; + cgltf_bool has_mesh_gpu_instancing; + cgltf_mesh_gpu_instancing mesh_gpu_instancing; + cgltf_size extensions_count; + cgltf_extension* extensions; +}; + +typedef struct cgltf_scene { + char* name; + cgltf_node** nodes; + cgltf_size nodes_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_scene; + +typedef struct cgltf_animation_sampler { + cgltf_accessor* input; + cgltf_accessor* output; + cgltf_interpolation_type interpolation; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation_sampler; + +typedef struct cgltf_animation_channel { + cgltf_animation_sampler* sampler; + cgltf_node* target_node; + cgltf_animation_path_type target_path; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation_channel; + +typedef struct cgltf_animation { + char* name; + cgltf_animation_sampler* samplers; + cgltf_size samplers_count; + cgltf_animation_channel* channels; + cgltf_size channels_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation; + +typedef struct cgltf_material_variant +{ + char* name; + cgltf_extras extras; +} cgltf_material_variant; + +typedef struct cgltf_asset { + char* copyright; + char* generator; + char* version; + char* min_version; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_asset; + +typedef struct cgltf_data +{ + cgltf_file_type file_type; + void* file_data; + cgltf_size file_size; + + cgltf_asset asset; + + cgltf_mesh* meshes; + cgltf_size meshes_count; + + cgltf_material* materials; + cgltf_size materials_count; + + cgltf_accessor* accessors; + cgltf_size accessors_count; + + cgltf_buffer_view* buffer_views; + cgltf_size buffer_views_count; + + cgltf_buffer* buffers; + cgltf_size buffers_count; + + cgltf_image* images; + cgltf_size images_count; + + cgltf_texture* textures; + cgltf_size textures_count; + + cgltf_sampler* samplers; + cgltf_size samplers_count; + + cgltf_skin* skins; + cgltf_size skins_count; + + cgltf_camera* cameras; + cgltf_size cameras_count; + + cgltf_light* lights; + cgltf_size lights_count; + + cgltf_node* nodes; + cgltf_size nodes_count; + + cgltf_scene* scenes; + cgltf_size scenes_count; + + cgltf_scene* scene; + + cgltf_animation* animations; + cgltf_size animations_count; + + cgltf_material_variant* variants; + cgltf_size variants_count; + + cgltf_extras extras; + + cgltf_size data_extensions_count; + cgltf_extension* data_extensions; + + char** extensions_used; + cgltf_size extensions_used_count; + + char** extensions_required; + cgltf_size extensions_required_count; + + const char* json; + cgltf_size json_size; + + const void* bin; + cgltf_size bin_size; + + cgltf_memory_options memory; + cgltf_file_options file; +} cgltf_data; + +cgltf_result cgltf_parse( + const cgltf_options* options, + const void* data, + cgltf_size size, + cgltf_data** out_data); + +cgltf_result cgltf_parse_file( + const cgltf_options* options, + const char* path, + cgltf_data** out_data); + +cgltf_result cgltf_load_buffers( + const cgltf_options* options, + cgltf_data* data, + const char* gltf_path); + +cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, cgltf_size size, const char* base64, void** out_data); + +cgltf_size cgltf_decode_string(char* string); +cgltf_size cgltf_decode_uri(char* uri); + +cgltf_result cgltf_validate(cgltf_data* data); + +void cgltf_free(cgltf_data* data); + +void cgltf_node_transform_local(const cgltf_node* node, cgltf_float* out_matrix); +void cgltf_node_transform_world(const cgltf_node* node, cgltf_float* out_matrix); + +const uint8_t* cgltf_buffer_view_data(const cgltf_buffer_view* view); + +const cgltf_accessor* cgltf_find_accessor(const cgltf_primitive* prim, cgltf_attribute_type type, cgltf_int index); + +cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size); +cgltf_bool cgltf_accessor_read_uint(const cgltf_accessor* accessor, cgltf_size index, cgltf_uint* out, cgltf_size element_size); +cgltf_size cgltf_accessor_read_index(const cgltf_accessor* accessor, cgltf_size index); + +cgltf_size cgltf_num_components(cgltf_type type); +cgltf_size cgltf_component_size(cgltf_component_type component_type); +cgltf_size cgltf_calc_size(cgltf_type type, cgltf_component_type component_type); + +cgltf_size cgltf_accessor_unpack_floats(const cgltf_accessor* accessor, cgltf_float* out, cgltf_size float_count); +cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, void* out, cgltf_size out_component_size, cgltf_size index_count); + +/* this function is deprecated and will be removed in the future; use cgltf_extras::data instead */ +cgltf_result cgltf_copy_extras_json(const cgltf_data* data, const cgltf_extras* extras, char* dest, cgltf_size* dest_size); + +cgltf_size cgltf_mesh_index(const cgltf_data* data, const cgltf_mesh* object); +cgltf_size cgltf_material_index(const cgltf_data* data, const cgltf_material* object); +cgltf_size cgltf_accessor_index(const cgltf_data* data, const cgltf_accessor* object); +cgltf_size cgltf_buffer_view_index(const cgltf_data* data, const cgltf_buffer_view* object); +cgltf_size cgltf_buffer_index(const cgltf_data* data, const cgltf_buffer* object); +cgltf_size cgltf_image_index(const cgltf_data* data, const cgltf_image* object); +cgltf_size cgltf_texture_index(const cgltf_data* data, const cgltf_texture* object); +cgltf_size cgltf_sampler_index(const cgltf_data* data, const cgltf_sampler* object); +cgltf_size cgltf_skin_index(const cgltf_data* data, const cgltf_skin* object); +cgltf_size cgltf_camera_index(const cgltf_data* data, const cgltf_camera* object); +cgltf_size cgltf_light_index(const cgltf_data* data, const cgltf_light* object); +cgltf_size cgltf_node_index(const cgltf_data* data, const cgltf_node* object); +cgltf_size cgltf_scene_index(const cgltf_data* data, const cgltf_scene* object); +cgltf_size cgltf_animation_index(const cgltf_data* data, const cgltf_animation* object); +cgltf_size cgltf_animation_sampler_index(const cgltf_animation* animation, const cgltf_animation_sampler* object); +cgltf_size cgltf_animation_channel_index(const cgltf_animation* animation, const cgltf_animation_channel* object); + +#ifdef __cplusplus +} +#endif + +#endif /* #ifndef CGLTF_H_INCLUDED__ */ + +/* + * + * Stop now, if you are only interested in the API. + * Below, you find the implementation. + * + */ + +#if defined(__INTELLISENSE__) || defined(__JETBRAINS_IDE__) +/* This makes MSVC/CLion intellisense work. */ +#define CGLTF_IMPLEMENTATION +#endif + +#ifdef CGLTF_IMPLEMENTATION + +#include /* For assert */ +#include /* For strncpy */ +#include /* For fopen */ +#include /* For UINT_MAX etc */ +#include /* For FLT_MAX */ + +#if !defined(CGLTF_MALLOC) || !defined(CGLTF_FREE) || !defined(CGLTF_ATOI) || !defined(CGLTF_ATOF) || !defined(CGLTF_ATOLL) +#include /* For malloc, free, atoi, atof */ +#endif + +/* JSMN_PARENT_LINKS is necessary to make parsing large structures linear in input size */ +#define JSMN_PARENT_LINKS + +/* JSMN_STRICT is necessary to reject invalid JSON documents */ +#define JSMN_STRICT + +/* + * -- jsmn.h start -- + * Source: https://github.com/zserge/jsmn + * License: MIT + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3, + JSMN_PRIMITIVE = 4 +} jsmntype_t; +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; +typedef struct { + jsmntype_t type; + ptrdiff_t start; + ptrdiff_t end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; +typedef struct { + size_t pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g parent object or array */ +} jsmn_parser; +static void jsmn_init(jsmn_parser *parser); +static int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, jsmntok_t *tokens, size_t num_tokens); +/* + * -- jsmn.h end -- + */ + + +#ifndef CGLTF_CONSTS +#define GlbHeaderSize 12 +#define GlbChunkHeaderSize 8 +static const uint32_t GlbVersion = 2; +static const uint32_t GlbMagic = 0x46546C67; +static const uint32_t GlbMagicJsonChunk = 0x4E4F534A; +static const uint32_t GlbMagicBinChunk = 0x004E4942; +#define CGLTF_CONSTS +#endif + +#ifndef CGLTF_MALLOC +#define CGLTF_MALLOC(size) malloc(size) +#endif +#ifndef CGLTF_FREE +#define CGLTF_FREE(ptr) free(ptr) +#endif +#ifndef CGLTF_ATOI +#define CGLTF_ATOI(str) atoi(str) +#endif +#ifndef CGLTF_ATOF +#define CGLTF_ATOF(str) atof(str) +#endif +#ifndef CGLTF_ATOLL +#define CGLTF_ATOLL(str) atoll(str) +#endif +#ifndef CGLTF_VALIDATE_ENABLE_ASSERTS +#define CGLTF_VALIDATE_ENABLE_ASSERTS 0 +#endif + +static void* cgltf_default_alloc(void* user, cgltf_size size) +{ + (void)user; + return CGLTF_MALLOC(size); +} + +static void cgltf_default_free(void* user, void* ptr) +{ + (void)user; + CGLTF_FREE(ptr); +} + +static void* cgltf_calloc(cgltf_options* options, size_t element_size, cgltf_size count) +{ + if (SIZE_MAX / element_size < count) + { + return NULL; + } + void* result = options->memory.alloc_func(options->memory.user_data, element_size * count); + if (!result) + { + return NULL; + } + memset(result, 0, element_size * count); + return result; +} + +static cgltf_result cgltf_default_file_read(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, const char* path, cgltf_size* size, void** data) +{ + (void)file_options; + void* (*memory_alloc)(void*, cgltf_size) = memory_options->alloc_func ? memory_options->alloc_func : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = memory_options->free_func ? memory_options->free_func : &cgltf_default_free; + + FILE* file = fopen(path, "rb"); + if (!file) + { + return cgltf_result_file_not_found; + } + + cgltf_size file_size = size ? *size : 0; + + if (file_size == 0) + { + fseek(file, 0, SEEK_END); + +#ifdef _MSC_VER + __int64 length = _ftelli64(file); +#else + long length = ftell(file); +#endif + + if (length < 0) + { + fclose(file); + return cgltf_result_io_error; + } + + fseek(file, 0, SEEK_SET); + file_size = (cgltf_size)length; + } + + char* file_data = (char*)memory_alloc(memory_options->user_data, file_size); + if (!file_data) + { + fclose(file); + return cgltf_result_out_of_memory; + } + + cgltf_size read_size = fread(file_data, 1, file_size, file); + + fclose(file); + + if (read_size != file_size) + { + memory_free(memory_options->user_data, file_data); + return cgltf_result_io_error; + } + + if (size) + { + *size = file_size; + } + if (data) + { + *data = file_data; + } + + return cgltf_result_success; +} + +static void cgltf_default_file_release(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, void* data, cgltf_size size) +{ + (void)file_options; + (void)size; + void (*memfree)(void*, void*) = memory_options->free_func ? memory_options->free_func : &cgltf_default_free; + memfree(memory_options->user_data, data); +} + +static cgltf_result cgltf_parse_json(cgltf_options* options, const uint8_t* json_chunk, cgltf_size size, cgltf_data** out_data); + +cgltf_result cgltf_parse(const cgltf_options* options, const void* data, cgltf_size size, cgltf_data** out_data) +{ + if (size < GlbHeaderSize) + { + return cgltf_result_data_too_short; + } + + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + cgltf_options fixed_options = *options; + if (fixed_options.memory.alloc_func == NULL) + { + fixed_options.memory.alloc_func = &cgltf_default_alloc; + } + if (fixed_options.memory.free_func == NULL) + { + fixed_options.memory.free_func = &cgltf_default_free; + } + + uint32_t tmp; + // Magic + memcpy(&tmp, data, 4); + if (tmp != GlbMagic) + { + if (fixed_options.type == cgltf_file_type_invalid) + { + fixed_options.type = cgltf_file_type_gltf; + } + else if (fixed_options.type == cgltf_file_type_glb) + { + return cgltf_result_unknown_format; + } + } + + if (fixed_options.type == cgltf_file_type_gltf) + { + cgltf_result json_result = cgltf_parse_json(&fixed_options, (const uint8_t*)data, size, out_data); + if (json_result != cgltf_result_success) + { + return json_result; + } + + (*out_data)->file_type = cgltf_file_type_gltf; + + return cgltf_result_success; + } + + const uint8_t* ptr = (const uint8_t*)data; + // Version + memcpy(&tmp, ptr + 4, 4); + uint32_t version = tmp; + if (version != GlbVersion) + { + return version < GlbVersion ? cgltf_result_legacy_gltf : cgltf_result_unknown_format; + } + + // Total length + memcpy(&tmp, ptr + 8, 4); + if (tmp > size) + { + return cgltf_result_data_too_short; + } + + const uint8_t* json_chunk = ptr + GlbHeaderSize; + + if (GlbHeaderSize + GlbChunkHeaderSize > size) + { + return cgltf_result_data_too_short; + } + + // JSON chunk: length + uint32_t json_length; + memcpy(&json_length, json_chunk, 4); + if (json_length > size - GlbHeaderSize - GlbChunkHeaderSize) + { + return cgltf_result_data_too_short; + } + + // JSON chunk: magic + memcpy(&tmp, json_chunk + 4, 4); + if (tmp != GlbMagicJsonChunk) + { + return cgltf_result_unknown_format; + } + + json_chunk += GlbChunkHeaderSize; + + const void* bin = NULL; + cgltf_size bin_size = 0; + + if (GlbChunkHeaderSize <= size - GlbHeaderSize - GlbChunkHeaderSize - json_length) + { + // We can read another chunk + const uint8_t* bin_chunk = json_chunk + json_length; + + // Bin chunk: length + uint32_t bin_length; + memcpy(&bin_length, bin_chunk, 4); + if (bin_length > size - GlbHeaderSize - GlbChunkHeaderSize - json_length - GlbChunkHeaderSize) + { + return cgltf_result_data_too_short; + } + + // Bin chunk: magic + memcpy(&tmp, bin_chunk + 4, 4); + if (tmp != GlbMagicBinChunk) + { + return cgltf_result_unknown_format; + } + + bin_chunk += GlbChunkHeaderSize; + + bin = bin_chunk; + bin_size = bin_length; + } + + cgltf_result json_result = cgltf_parse_json(&fixed_options, json_chunk, json_length, out_data); + if (json_result != cgltf_result_success) + { + return json_result; + } + + (*out_data)->file_type = cgltf_file_type_glb; + (*out_data)->bin = bin; + (*out_data)->bin_size = bin_size; + + return cgltf_result_success; +} + +cgltf_result cgltf_parse_file(const cgltf_options* options, const char* path, cgltf_data** out_data) +{ + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + cgltf_result (*file_read)(const struct cgltf_memory_options*, const struct cgltf_file_options*, const char*, cgltf_size*, void**) = options->file.read ? options->file.read : &cgltf_default_file_read; + void (*file_release)(const struct cgltf_memory_options*, const struct cgltf_file_options*, void* data, cgltf_size size) = options->file.release ? options->file.release : cgltf_default_file_release; + + void* file_data = NULL; + cgltf_size file_size = 0; + cgltf_result result = file_read(&options->memory, &options->file, path, &file_size, &file_data); + if (result != cgltf_result_success) + { + return result; + } + + result = cgltf_parse(options, file_data, file_size, out_data); + + if (result != cgltf_result_success) + { + file_release(&options->memory, &options->file, file_data, file_size); + return result; + } + + (*out_data)->file_data = file_data; + (*out_data)->file_size = file_size; + + return cgltf_result_success; +} + +static void cgltf_combine_paths(char* path, const char* base, const char* uri) +{ + const char* s0 = strrchr(base, '/'); + const char* s1 = strrchr(base, '\\'); + const char* slash = s0 ? (s1 && s1 > s0 ? s1 : s0) : s1; + + if (slash) + { + size_t prefix = slash - base + 1; + + strncpy(path, base, prefix); + strcpy(path + prefix, uri); + } + else + { + strcpy(path, uri); + } +} + +static cgltf_result cgltf_load_buffer_file(const cgltf_options* options, cgltf_size size, const char* uri, const char* gltf_path, void** out_data) +{ + void* (*memory_alloc)(void*, cgltf_size) = options->memory.alloc_func ? options->memory.alloc_func : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = options->memory.free_func ? options->memory.free_func : &cgltf_default_free; + cgltf_result (*file_read)(const struct cgltf_memory_options*, const struct cgltf_file_options*, const char*, cgltf_size*, void**) = options->file.read ? options->file.read : &cgltf_default_file_read; + + char* path = (char*)memory_alloc(options->memory.user_data, strlen(uri) + strlen(gltf_path) + 1); + if (!path) + { + return cgltf_result_out_of_memory; + } + + cgltf_combine_paths(path, gltf_path, uri); + + // after combining, the tail of the resulting path is a uri; decode_uri converts it into path + cgltf_decode_uri(path + strlen(path) - strlen(uri)); + + void* file_data = NULL; + cgltf_result result = file_read(&options->memory, &options->file, path, &size, &file_data); + + memory_free(options->memory.user_data, path); + + *out_data = (result == cgltf_result_success) ? file_data : NULL; + + return result; +} + +cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, cgltf_size size, const char* base64, void** out_data) +{ + void* (*memory_alloc)(void*, cgltf_size) = options->memory.alloc_func ? options->memory.alloc_func : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = options->memory.free_func ? options->memory.free_func : &cgltf_default_free; + + unsigned char* data = (unsigned char*)memory_alloc(options->memory.user_data, size); + if (!data) + { + return cgltf_result_out_of_memory; + } + + unsigned int buffer = 0; + unsigned int buffer_bits = 0; + + for (cgltf_size i = 0; i < size; ++i) + { + while (buffer_bits < 8) + { + char ch = *base64++; + + int index = + (unsigned)(ch - 'A') < 26 ? (ch - 'A') : + (unsigned)(ch - 'a') < 26 ? (ch - 'a') + 26 : + (unsigned)(ch - '0') < 10 ? (ch - '0') + 52 : + ch == '+' ? 62 : + ch == '/' ? 63 : + -1; + + if (index < 0) + { + memory_free(options->memory.user_data, data); + return cgltf_result_io_error; + } + + buffer = (buffer << 6) | index; + buffer_bits += 6; + } + + data[i] = (unsigned char)(buffer >> (buffer_bits - 8)); + buffer_bits -= 8; + } + + *out_data = data; + + return cgltf_result_success; +} + +static int cgltf_unhex(char ch) +{ + return + (unsigned)(ch - '0') < 10 ? (ch - '0') : + (unsigned)(ch - 'A') < 6 ? (ch - 'A') + 10 : + (unsigned)(ch - 'a') < 6 ? (ch - 'a') + 10 : + -1; +} + +cgltf_size cgltf_decode_string(char* string) +{ + char* read = string + strcspn(string, "\\"); + if (*read == 0) + { + return read - string; + } + char* write = string; + char* last = string; + + for (;;) + { + // Copy characters since last escaped sequence + cgltf_size written = read - last; + memmove(write, last, written); + write += written; + + if (*read++ == 0) + { + break; + } + + // jsmn already checked that all escape sequences are valid + switch (*read++) + { + case '\"': *write++ = '\"'; break; + case '/': *write++ = '/'; break; + case '\\': *write++ = '\\'; break; + case 'b': *write++ = '\b'; break; + case 'f': *write++ = '\f'; break; + case 'r': *write++ = '\r'; break; + case 'n': *write++ = '\n'; break; + case 't': *write++ = '\t'; break; + case 'u': + { + // UCS-2 codepoint \uXXXX to UTF-8 + int character = 0; + for (cgltf_size i = 0; i < 4; ++i) + { + character = (character << 4) + cgltf_unhex(*read++); + } + + if (character <= 0x7F) + { + *write++ = character & 0xFF; + } + else if (character <= 0x7FF) + { + *write++ = 0xC0 | ((character >> 6) & 0xFF); + *write++ = 0x80 | (character & 0x3F); + } + else + { + *write++ = 0xE0 | ((character >> 12) & 0xFF); + *write++ = 0x80 | ((character >> 6) & 0x3F); + *write++ = 0x80 | (character & 0x3F); + } + break; + } + default: + break; + } + + last = read; + read += strcspn(read, "\\"); + } + + *write = 0; + return write - string; +} + +cgltf_size cgltf_decode_uri(char* uri) +{ + char* write = uri; + char* i = uri; + + while (*i) + { + if (*i == '%') + { + int ch1 = cgltf_unhex(i[1]); + + if (ch1 >= 0) + { + int ch2 = cgltf_unhex(i[2]); + + if (ch2 >= 0) + { + *write++ = (char)(ch1 * 16 + ch2); + i += 3; + continue; + } + } + } + + *write++ = *i++; + } + + *write = 0; + return write - uri; +} + +cgltf_result cgltf_load_buffers(const cgltf_options* options, cgltf_data* data, const char* gltf_path) +{ + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + if (data->buffers_count && data->buffers[0].data == NULL && data->buffers[0].uri == NULL && data->bin) + { + if (data->bin_size < data->buffers[0].size) + { + return cgltf_result_data_too_short; + } + + data->buffers[0].data = (void*)data->bin; + data->buffers[0].data_free_method = cgltf_data_free_method_none; + } + + for (cgltf_size i = 0; i < data->buffers_count; ++i) + { + if (data->buffers[i].data) + { + continue; + } + + const char* uri = data->buffers[i].uri; + + if (uri == NULL) + { + continue; + } + + if (strncmp(uri, "data:", 5) == 0) + { + const char* comma = strchr(uri, ','); + + if (comma && comma - uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0) + { + cgltf_result res = cgltf_load_buffer_base64(options, data->buffers[i].size, comma + 1, &data->buffers[i].data); + data->buffers[i].data_free_method = cgltf_data_free_method_memory_free; + + if (res != cgltf_result_success) + { + return res; + } + } + else + { + return cgltf_result_unknown_format; + } + } + else if (strstr(uri, "://") == NULL && gltf_path) + { + cgltf_result res = cgltf_load_buffer_file(options, data->buffers[i].size, uri, gltf_path, &data->buffers[i].data); + data->buffers[i].data_free_method = cgltf_data_free_method_file_release; + + if (res != cgltf_result_success) + { + return res; + } + } + else + { + return cgltf_result_unknown_format; + } + } + + return cgltf_result_success; +} + +static cgltf_size cgltf_calc_index_bound(cgltf_buffer_view* buffer_view, cgltf_size offset, cgltf_component_type component_type, cgltf_size count) +{ + char* data = (char*)buffer_view->buffer->data + offset + buffer_view->offset; + cgltf_size bound = 0; + + switch (component_type) + { + case cgltf_component_type_r_8u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned char*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + case cgltf_component_type_r_16u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned short*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + case cgltf_component_type_r_32u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned int*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + default: + ; + } + + return bound; +} + +#if CGLTF_VALIDATE_ENABLE_ASSERTS +#define CGLTF_ASSERT_IF(cond, result) assert(!(cond)); if (cond) return result; +#else +#define CGLTF_ASSERT_IF(cond, result) if (cond) return result; +#endif + +cgltf_result cgltf_validate(cgltf_data* data) +{ + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + cgltf_accessor* accessor = &data->accessors[i]; + + CGLTF_ASSERT_IF(data->accessors[i].component_type == cgltf_component_type_invalid, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->accessors[i].type == cgltf_type_invalid, cgltf_result_invalid_gltf); + + cgltf_size element_size = cgltf_calc_size(accessor->type, accessor->component_type); + + if (accessor->buffer_view) + { + cgltf_size req_size = accessor->offset + accessor->stride * (accessor->count - 1) + element_size; + + CGLTF_ASSERT_IF(accessor->buffer_view->size < req_size, cgltf_result_data_too_short); + } + + if (accessor->is_sparse) + { + cgltf_accessor_sparse* sparse = &accessor->sparse; + + cgltf_size indices_component_size = cgltf_component_size(sparse->indices_component_type); + cgltf_size indices_req_size = sparse->indices_byte_offset + indices_component_size * sparse->count; + cgltf_size values_req_size = sparse->values_byte_offset + element_size * sparse->count; + + CGLTF_ASSERT_IF(sparse->indices_buffer_view->size < indices_req_size || + sparse->values_buffer_view->size < values_req_size, cgltf_result_data_too_short); + + CGLTF_ASSERT_IF(sparse->indices_component_type != cgltf_component_type_r_8u && + sparse->indices_component_type != cgltf_component_type_r_16u && + sparse->indices_component_type != cgltf_component_type_r_32u, cgltf_result_invalid_gltf); + + if (sparse->indices_buffer_view->buffer->data) + { + cgltf_size index_bound = cgltf_calc_index_bound(sparse->indices_buffer_view, sparse->indices_byte_offset, sparse->indices_component_type, sparse->count); + + CGLTF_ASSERT_IF(index_bound >= accessor->count, cgltf_result_data_too_short); + } + } + } + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + cgltf_size req_size = data->buffer_views[i].offset + data->buffer_views[i].size; + + CGLTF_ASSERT_IF(data->buffer_views[i].buffer && data->buffer_views[i].buffer->size < req_size, cgltf_result_data_too_short); + + if (data->buffer_views[i].has_meshopt_compression) + { + cgltf_meshopt_compression* mc = &data->buffer_views[i].meshopt_compression; + + CGLTF_ASSERT_IF(mc->buffer == NULL || mc->buffer->size < mc->offset + mc->size, cgltf_result_data_too_short); + + CGLTF_ASSERT_IF(data->buffer_views[i].stride && mc->stride != data->buffer_views[i].stride, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(data->buffer_views[i].size != mc->stride * mc->count, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_invalid, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_attributes && !(mc->stride % 4 == 0 && mc->stride <= 256), cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_triangles && mc->count % 3 != 0, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF((mc->mode == cgltf_meshopt_compression_mode_triangles || mc->mode == cgltf_meshopt_compression_mode_indices) && mc->stride != 2 && mc->stride != 4, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF((mc->mode == cgltf_meshopt_compression_mode_triangles || mc->mode == cgltf_meshopt_compression_mode_indices) && mc->filter != cgltf_meshopt_compression_filter_none, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->filter == cgltf_meshopt_compression_filter_octahedral && mc->stride != 4 && mc->stride != 8, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(mc->filter == cgltf_meshopt_compression_filter_quaternion && mc->stride != 8, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(mc->filter == cgltf_meshopt_compression_filter_color && mc->stride != 4 && mc->stride != 8, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + if (data->meshes[i].weights) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives_count && data->meshes[i].primitives[0].targets_count != data->meshes[i].weights_count, cgltf_result_invalid_gltf); + } + + if (data->meshes[i].target_names) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives_count && data->meshes[i].primitives[0].targets_count != data->meshes[i].target_names_count, cgltf_result_invalid_gltf); + } + + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].type == cgltf_primitive_type_invalid, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].targets_count != data->meshes[i].primitives[0].targets_count, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].attributes_count == 0, cgltf_result_invalid_gltf); + + cgltf_accessor* first = data->meshes[i].primitives[j].attributes[0].data; + + CGLTF_ASSERT_IF(first->count == 0, cgltf_result_invalid_gltf); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].attributes[k].data->count != first->count, cgltf_result_invalid_gltf); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].targets[k].attributes[m].data->count != first->count, cgltf_result_invalid_gltf); + } + } + + cgltf_accessor* indices = data->meshes[i].primitives[j].indices; + + CGLTF_ASSERT_IF(indices && + indices->component_type != cgltf_component_type_r_8u && + indices->component_type != cgltf_component_type_r_16u && + indices->component_type != cgltf_component_type_r_32u, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(indices && indices->type != cgltf_type_scalar, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(indices && indices->stride != cgltf_component_size(indices->component_type), cgltf_result_invalid_gltf); + + if (indices && indices->buffer_view && indices->buffer_view->buffer->data) + { + cgltf_size index_bound = cgltf_calc_index_bound(indices->buffer_view, indices->offset, indices->component_type, indices->count); + + CGLTF_ASSERT_IF(index_bound >= first->count, cgltf_result_data_too_short); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].mappings[k].variant >= data->variants_count, cgltf_result_invalid_gltf); + } + } + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + if (data->nodes[i].weights && data->nodes[i].mesh) + { + CGLTF_ASSERT_IF(data->nodes[i].mesh->primitives_count && data->nodes[i].mesh->primitives[0].targets_count != data->nodes[i].weights_count, cgltf_result_invalid_gltf); + } + + if (data->nodes[i].has_mesh_gpu_instancing) + { + CGLTF_ASSERT_IF(data->nodes[i].mesh == NULL, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->nodes[i].mesh_gpu_instancing.attributes_count == 0, cgltf_result_invalid_gltf); + + cgltf_accessor* first = data->nodes[i].mesh_gpu_instancing.attributes[0].data; + + for (cgltf_size k = 0; k < data->nodes[i].mesh_gpu_instancing.attributes_count; ++k) + { + CGLTF_ASSERT_IF(data->nodes[i].mesh_gpu_instancing.attributes[k].data->count != first->count, cgltf_result_invalid_gltf); + } + } + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + cgltf_node* p1 = data->nodes[i].parent; + cgltf_node* p2 = p1 ? p1->parent : NULL; + + while (p1 && p2) + { + CGLTF_ASSERT_IF(p1 == p2, cgltf_result_invalid_gltf); + + p1 = p1->parent; + p2 = p2->parent ? p2->parent->parent : NULL; + } + } + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + for (cgltf_size j = 0; j < data->scenes[i].nodes_count; ++j) + { + CGLTF_ASSERT_IF(data->scenes[i].nodes[j]->parent, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + cgltf_animation_channel* channel = &data->animations[i].channels[j]; + + if (!channel->target_node) + { + continue; + } + + cgltf_size components = 1; + + if (channel->target_path == cgltf_animation_path_type_weights) + { + CGLTF_ASSERT_IF(!channel->target_node->mesh || !channel->target_node->mesh->primitives_count, cgltf_result_invalid_gltf); + + components = channel->target_node->mesh->primitives[0].targets_count; + } + + cgltf_size values = channel->sampler->interpolation == cgltf_interpolation_type_cubic_spline ? 3 : 1; + + CGLTF_ASSERT_IF(channel->sampler->input->count * components * values != channel->sampler->output->count, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->variants_count; ++i) + { + CGLTF_ASSERT_IF(!data->variants[i].name, cgltf_result_invalid_gltf); + } + + return cgltf_result_success; +} + +cgltf_result cgltf_copy_extras_json(const cgltf_data* data, const cgltf_extras* extras, char* dest, cgltf_size* dest_size) +{ + cgltf_size json_size = extras->end_offset - extras->start_offset; + + if (!dest) + { + if (dest_size) + { + *dest_size = json_size + 1; + return cgltf_result_success; + } + return cgltf_result_invalid_options; + } + + if (*dest_size + 1 < json_size) + { + strncpy(dest, data->json + extras->start_offset, *dest_size - 1); + dest[*dest_size - 1] = 0; + } + else + { + strncpy(dest, data->json + extras->start_offset, json_size); + dest[json_size] = 0; + } + + return cgltf_result_success; +} + +static void cgltf_free_extras(cgltf_data* data, cgltf_extras* extras) +{ + data->memory.free_func(data->memory.user_data, extras->data); +} + +static void cgltf_free_extensions(cgltf_data* data, cgltf_extension* extensions, cgltf_size extensions_count) +{ + for (cgltf_size i = 0; i < extensions_count; ++i) + { + data->memory.free_func(data->memory.user_data, extensions[i].name); + data->memory.free_func(data->memory.user_data, extensions[i].data); + } + data->memory.free_func(data->memory.user_data, extensions); +} + +void cgltf_free(cgltf_data* data) +{ + if (!data) + { + return; + } + + void (*file_release)(const struct cgltf_memory_options*, const struct cgltf_file_options*, void* data, cgltf_size size) = data->file.release ? data->file.release : cgltf_default_file_release; + + data->memory.free_func(data->memory.user_data, data->asset.copyright); + data->memory.free_func(data->memory.user_data, data->asset.generator); + data->memory.free_func(data->memory.user_data, data->asset.version); + data->memory.free_func(data->memory.user_data, data->asset.min_version); + + cgltf_free_extensions(data, data->asset.extensions, data->asset.extensions_count); + cgltf_free_extras(data, &data->asset.extras); + + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->accessors[i].name); + + cgltf_free_extensions(data, data->accessors[i].extensions, data->accessors[i].extensions_count); + cgltf_free_extras(data, &data->accessors[i].extras); + } + data->memory.free_func(data->memory.user_data, data->accessors); + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->buffer_views[i].name); + data->memory.free_func(data->memory.user_data, data->buffer_views[i].data); + + cgltf_free_extensions(data, data->buffer_views[i].extensions, data->buffer_views[i].extensions_count); + cgltf_free_extras(data, &data->buffer_views[i].extras); + } + data->memory.free_func(data->memory.user_data, data->buffer_views); + + for (cgltf_size i = 0; i < data->buffers_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->buffers[i].name); + + if (data->buffers[i].data_free_method == cgltf_data_free_method_file_release) + { + file_release(&data->memory, &data->file, data->buffers[i].data, data->buffers[i].size); + } + else if (data->buffers[i].data_free_method == cgltf_data_free_method_memory_free) + { + data->memory.free_func(data->memory.user_data, data->buffers[i].data); + } + + data->memory.free_func(data->memory.user_data, data->buffers[i].uri); + + cgltf_free_extensions(data, data->buffers[i].extensions, data->buffers[i].extensions_count); + cgltf_free_extras(data, &data->buffers[i].extras); + } + data->memory.free_func(data->memory.user_data, data->buffers); + + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].name); + + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].attributes[k].name); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].attributes); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].targets[k].attributes[m].name); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].targets[k].attributes); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].targets); + + if (data->meshes[i].primitives[j].has_draco_mesh_compression) + { + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].draco_mesh_compression.attributes_count; ++k) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].draco_mesh_compression.attributes[k].name); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].draco_mesh_compression.attributes); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + cgltf_free_extras(data, &data->meshes[i].primitives[j].mappings[k].extras); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].mappings); + + cgltf_free_extensions(data, data->meshes[i].primitives[j].extensions, data->meshes[i].primitives[j].extensions_count); + cgltf_free_extras(data, &data->meshes[i].primitives[j].extras); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives); + data->memory.free_func(data->memory.user_data, data->meshes[i].weights); + + for (cgltf_size j = 0; j < data->meshes[i].target_names_count; ++j) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].target_names[j]); + } + + cgltf_free_extensions(data, data->meshes[i].extensions, data->meshes[i].extensions_count); + cgltf_free_extras(data, &data->meshes[i].extras); + + data->memory.free_func(data->memory.user_data, data->meshes[i].target_names); + } + + data->memory.free_func(data->memory.user_data, data->meshes); + + for (cgltf_size i = 0; i < data->materials_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->materials[i].name); + + cgltf_free_extensions(data, data->materials[i].extensions, data->materials[i].extensions_count); + cgltf_free_extras(data, &data->materials[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->materials); + + for (cgltf_size i = 0; i < data->images_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->images[i].name); + data->memory.free_func(data->memory.user_data, data->images[i].uri); + data->memory.free_func(data->memory.user_data, data->images[i].mime_type); + + cgltf_free_extensions(data, data->images[i].extensions, data->images[i].extensions_count); + cgltf_free_extras(data, &data->images[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->images); + + for (cgltf_size i = 0; i < data->textures_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->textures[i].name); + + cgltf_free_extensions(data, data->textures[i].extensions, data->textures[i].extensions_count); + cgltf_free_extras(data, &data->textures[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->textures); + + for (cgltf_size i = 0; i < data->samplers_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->samplers[i].name); + + cgltf_free_extensions(data, data->samplers[i].extensions, data->samplers[i].extensions_count); + cgltf_free_extras(data, &data->samplers[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->samplers); + + for (cgltf_size i = 0; i < data->skins_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->skins[i].name); + data->memory.free_func(data->memory.user_data, data->skins[i].joints); + + cgltf_free_extensions(data, data->skins[i].extensions, data->skins[i].extensions_count); + cgltf_free_extras(data, &data->skins[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->skins); + + for (cgltf_size i = 0; i < data->cameras_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->cameras[i].name); + + if (data->cameras[i].type == cgltf_camera_type_perspective) + { + cgltf_free_extras(data, &data->cameras[i].data.perspective.extras); + } + else if (data->cameras[i].type == cgltf_camera_type_orthographic) + { + cgltf_free_extras(data, &data->cameras[i].data.orthographic.extras); + } + + cgltf_free_extensions(data, data->cameras[i].extensions, data->cameras[i].extensions_count); + cgltf_free_extras(data, &data->cameras[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->cameras); + + for (cgltf_size i = 0; i < data->lights_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->lights[i].name); + + cgltf_free_extras(data, &data->lights[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->lights); + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->nodes[i].name); + data->memory.free_func(data->memory.user_data, data->nodes[i].children); + data->memory.free_func(data->memory.user_data, data->nodes[i].weights); + + if (data->nodes[i].has_mesh_gpu_instancing) + { + for (cgltf_size j = 0; j < data->nodes[i].mesh_gpu_instancing.attributes_count; ++j) + { + data->memory.free_func(data->memory.user_data, data->nodes[i].mesh_gpu_instancing.attributes[j].name); + } + + data->memory.free_func(data->memory.user_data, data->nodes[i].mesh_gpu_instancing.attributes); + } + + cgltf_free_extensions(data, data->nodes[i].extensions, data->nodes[i].extensions_count); + cgltf_free_extras(data, &data->nodes[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->nodes); + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->scenes[i].name); + data->memory.free_func(data->memory.user_data, data->scenes[i].nodes); + + cgltf_free_extensions(data, data->scenes[i].extensions, data->scenes[i].extensions_count); + cgltf_free_extras(data, &data->scenes[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->scenes); + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->animations[i].name); + for (cgltf_size j = 0; j < data->animations[i].samplers_count; ++j) + { + cgltf_free_extensions(data, data->animations[i].samplers[j].extensions, data->animations[i].samplers[j].extensions_count); + cgltf_free_extras(data, &data->animations[i].samplers[j].extras); + } + data->memory.free_func(data->memory.user_data, data->animations[i].samplers); + + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + cgltf_free_extensions(data, data->animations[i].channels[j].extensions, data->animations[i].channels[j].extensions_count); + cgltf_free_extras(data, &data->animations[i].channels[j].extras); + } + data->memory.free_func(data->memory.user_data, data->animations[i].channels); + + cgltf_free_extensions(data, data->animations[i].extensions, data->animations[i].extensions_count); + cgltf_free_extras(data, &data->animations[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->animations); + + for (cgltf_size i = 0; i < data->variants_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->variants[i].name); + + cgltf_free_extras(data, &data->variants[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->variants); + + cgltf_free_extensions(data, data->data_extensions, data->data_extensions_count); + cgltf_free_extras(data, &data->extras); + + for (cgltf_size i = 0; i < data->extensions_used_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->extensions_used[i]); + } + + data->memory.free_func(data->memory.user_data, data->extensions_used); + + for (cgltf_size i = 0; i < data->extensions_required_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->extensions_required[i]); + } + + data->memory.free_func(data->memory.user_data, data->extensions_required); + + file_release(&data->memory, &data->file, data->file_data, data->file_size); + + data->memory.free_func(data->memory.user_data, data); +} + +void cgltf_node_transform_local(const cgltf_node* node, cgltf_float* out_matrix) +{ + cgltf_float* lm = out_matrix; + + if (node->has_matrix) + { + memcpy(lm, node->matrix, sizeof(float) * 16); + } + else + { + float tx = node->translation[0]; + float ty = node->translation[1]; + float tz = node->translation[2]; + + float qx = node->rotation[0]; + float qy = node->rotation[1]; + float qz = node->rotation[2]; + float qw = node->rotation[3]; + + float sx = node->scale[0]; + float sy = node->scale[1]; + float sz = node->scale[2]; + + lm[0] = (1 - 2 * qy*qy - 2 * qz*qz) * sx; + lm[1] = (2 * qx*qy + 2 * qz*qw) * sx; + lm[2] = (2 * qx*qz - 2 * qy*qw) * sx; + lm[3] = 0.f; + + lm[4] = (2 * qx*qy - 2 * qz*qw) * sy; + lm[5] = (1 - 2 * qx*qx - 2 * qz*qz) * sy; + lm[6] = (2 * qy*qz + 2 * qx*qw) * sy; + lm[7] = 0.f; + + lm[8] = (2 * qx*qz + 2 * qy*qw) * sz; + lm[9] = (2 * qy*qz - 2 * qx*qw) * sz; + lm[10] = (1 - 2 * qx*qx - 2 * qy*qy) * sz; + lm[11] = 0.f; + + lm[12] = tx; + lm[13] = ty; + lm[14] = tz; + lm[15] = 1.f; + } +} + +void cgltf_node_transform_world(const cgltf_node* node, cgltf_float* out_matrix) +{ + cgltf_float* lm = out_matrix; + cgltf_node_transform_local(node, lm); + + const cgltf_node* parent = node->parent; + + while (parent) + { + float pm[16]; + cgltf_node_transform_local(parent, pm); + + for (int i = 0; i < 4; ++i) + { + float l0 = lm[i * 4 + 0]; + float l1 = lm[i * 4 + 1]; + float l2 = lm[i * 4 + 2]; + + float r0 = l0 * pm[0] + l1 * pm[4] + l2 * pm[8]; + float r1 = l0 * pm[1] + l1 * pm[5] + l2 * pm[9]; + float r2 = l0 * pm[2] + l1 * pm[6] + l2 * pm[10]; + + lm[i * 4 + 0] = r0; + lm[i * 4 + 1] = r1; + lm[i * 4 + 2] = r2; + } + + lm[12] += pm[12]; + lm[13] += pm[13]; + lm[14] += pm[14]; + + parent = parent->parent; + } +} + +static cgltf_ssize cgltf_component_read_integer(const void* in, cgltf_component_type component_type) +{ + switch (component_type) + { + case cgltf_component_type_r_16: + return *((const int16_t*) in); + case cgltf_component_type_r_16u: + return *((const uint16_t*) in); + case cgltf_component_type_r_32u: + return *((const uint32_t*) in); + case cgltf_component_type_r_8: + return *((const int8_t*) in); + case cgltf_component_type_r_8u: + return *((const uint8_t*) in); + default: + return 0; + } +} + +static cgltf_size cgltf_component_read_index(const void* in, cgltf_component_type component_type) +{ + switch (component_type) + { + case cgltf_component_type_r_16u: + return *((const uint16_t*) in); + case cgltf_component_type_r_32u: + return *((const uint32_t*) in); + case cgltf_component_type_r_8u: + return *((const uint8_t*) in); + default: + return 0; + } +} + +static cgltf_float cgltf_component_read_float(const void* in, cgltf_component_type component_type, cgltf_bool normalized) +{ + if (component_type == cgltf_component_type_r_32f) + { + return *((const float*) in); + } + + if (normalized) + { + switch (component_type) + { + // note: glTF spec doesn't currently define normalized conversions for 32-bit integers + case cgltf_component_type_r_16: + return *((const int16_t*) in) / (cgltf_float)32767; + case cgltf_component_type_r_16u: + return *((const uint16_t*) in) / (cgltf_float)65535; + case cgltf_component_type_r_8: + return *((const int8_t*) in) / (cgltf_float)127; + case cgltf_component_type_r_8u: + return *((const uint8_t*) in) / (cgltf_float)255; + default: + return 0; + } + } + + return (cgltf_float)cgltf_component_read_integer(in, component_type); +} + +static cgltf_bool cgltf_element_read_float(const uint8_t* element, cgltf_type type, cgltf_component_type component_type, cgltf_bool normalized, cgltf_float* out, cgltf_size element_size) +{ + cgltf_size num_components = cgltf_num_components(type); + + if (element_size < num_components) { + return 0; + } + + // There are three special cases for component extraction, see #data-alignment in the 2.0 spec. + + cgltf_size component_size = cgltf_component_size(component_type); + + if (type == cgltf_type_mat2 && component_size == 1) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 1, component_type, normalized); + out[2] = cgltf_component_read_float(element + 4, component_type, normalized); + out[3] = cgltf_component_read_float(element + 5, component_type, normalized); + return 1; + } + + if (type == cgltf_type_mat3 && component_size == 1) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 1, component_type, normalized); + out[2] = cgltf_component_read_float(element + 2, component_type, normalized); + out[3] = cgltf_component_read_float(element + 4, component_type, normalized); + out[4] = cgltf_component_read_float(element + 5, component_type, normalized); + out[5] = cgltf_component_read_float(element + 6, component_type, normalized); + out[6] = cgltf_component_read_float(element + 8, component_type, normalized); + out[7] = cgltf_component_read_float(element + 9, component_type, normalized); + out[8] = cgltf_component_read_float(element + 10, component_type, normalized); + return 1; + } + + if (type == cgltf_type_mat3 && component_size == 2) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 2, component_type, normalized); + out[2] = cgltf_component_read_float(element + 4, component_type, normalized); + out[3] = cgltf_component_read_float(element + 8, component_type, normalized); + out[4] = cgltf_component_read_float(element + 10, component_type, normalized); + out[5] = cgltf_component_read_float(element + 12, component_type, normalized); + out[6] = cgltf_component_read_float(element + 16, component_type, normalized); + out[7] = cgltf_component_read_float(element + 18, component_type, normalized); + out[8] = cgltf_component_read_float(element + 20, component_type, normalized); + return 1; + } + + for (cgltf_size i = 0; i < num_components; ++i) + { + out[i] = cgltf_component_read_float(element + component_size * i, component_type, normalized); + } + return 1; +} + +const uint8_t* cgltf_buffer_view_data(const cgltf_buffer_view* view) +{ + if (view->data) + return (const uint8_t*)view->data; + + if (!view->buffer->data) + return NULL; + + const uint8_t* result = (const uint8_t*)view->buffer->data; + result += view->offset; + return result; +} + +const cgltf_accessor* cgltf_find_accessor(const cgltf_primitive* prim, cgltf_attribute_type type, cgltf_int index) +{ + for (cgltf_size i = 0; i < prim->attributes_count; ++i) + { + const cgltf_attribute* attr = &prim->attributes[i]; + if (attr->type == type && attr->index == index) + return attr->data; + } + + return NULL; +} + +static const uint8_t* cgltf_find_sparse_index(const cgltf_accessor* accessor, cgltf_size needle) +{ + const cgltf_accessor_sparse* sparse = &accessor->sparse; + const uint8_t* index_data = cgltf_buffer_view_data(sparse->indices_buffer_view); + const uint8_t* value_data = cgltf_buffer_view_data(sparse->values_buffer_view); + + if (index_data == NULL || value_data == NULL) + return NULL; + + index_data += sparse->indices_byte_offset; + value_data += sparse->values_byte_offset; + + cgltf_size index_stride = cgltf_component_size(sparse->indices_component_type); + + cgltf_size offset = 0; + cgltf_size length = sparse->count; + + while (length) + { + cgltf_size rem = length % 2; + length /= 2; + + cgltf_size index = cgltf_component_read_index(index_data + (offset + length) * index_stride, sparse->indices_component_type); + offset += index < needle ? length + rem : 0; + } + + if (offset == sparse->count) + return NULL; + + cgltf_size index = cgltf_component_read_index(index_data + offset * index_stride, sparse->indices_component_type); + return index == needle ? value_data + offset * accessor->stride : NULL; +} + +cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size) +{ + if (accessor->is_sparse) + { + const uint8_t* element = cgltf_find_sparse_index(accessor, index); + if (element) + return cgltf_element_read_float(element, accessor->type, accessor->component_type, accessor->normalized, out, element_size); + } + if (accessor->buffer_view == NULL) + { + memset(out, 0, element_size * sizeof(cgltf_float)); + return 1; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset + accessor->stride * index; + return cgltf_element_read_float(element, accessor->type, accessor->component_type, accessor->normalized, out, element_size); +} + +cgltf_size cgltf_accessor_unpack_floats(const cgltf_accessor* accessor, cgltf_float* out, cgltf_size float_count) +{ + cgltf_size floats_per_element = cgltf_num_components(accessor->type); + cgltf_size available_floats = accessor->count * floats_per_element; + if (out == NULL) + { + return available_floats; + } + + float_count = available_floats < float_count ? available_floats : float_count; + cgltf_size element_count = float_count / floats_per_element; + + // First pass: convert each element in the base accessor. + if (accessor->buffer_view == NULL) + { + memset(out, 0, element_count * floats_per_element * sizeof(cgltf_float)); + } + else + { + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset; + + if (accessor->component_type == cgltf_component_type_r_32f && accessor->stride == floats_per_element * sizeof(cgltf_float)) + { + memcpy(out, element, element_count * floats_per_element * sizeof(cgltf_float)); + } + else + { + cgltf_float* dest = out; + + for (cgltf_size index = 0; index < element_count; index++, dest += floats_per_element, element += accessor->stride) + { + if (!cgltf_element_read_float(element, accessor->type, accessor->component_type, accessor->normalized, dest, floats_per_element)) + { + return 0; + } + } + } + } + + // Second pass: write out each element in the sparse accessor. + if (accessor->is_sparse) + { + const cgltf_accessor_sparse* sparse = &accessor->sparse; + + const uint8_t* index_data = cgltf_buffer_view_data(sparse->indices_buffer_view); + const uint8_t* reader_head = cgltf_buffer_view_data(sparse->values_buffer_view); + + if (index_data == NULL || reader_head == NULL) + { + return 0; + } + + index_data += sparse->indices_byte_offset; + reader_head += sparse->values_byte_offset; + + cgltf_size index_stride = cgltf_component_size(sparse->indices_component_type); + for (cgltf_size reader_index = 0; reader_index < sparse->count; reader_index++, index_data += index_stride, reader_head += accessor->stride) + { + size_t writer_index = cgltf_component_read_index(index_data, sparse->indices_component_type); + float* writer_head = out + writer_index * floats_per_element; + + if (!cgltf_element_read_float(reader_head, accessor->type, accessor->component_type, accessor->normalized, writer_head, floats_per_element)) + { + return 0; + } + } + } + + return element_count * floats_per_element; +} + +static cgltf_uint cgltf_component_read_uint(const void* in, cgltf_component_type component_type) +{ + switch (component_type) + { + case cgltf_component_type_r_8: + return *((const int8_t*) in); + + case cgltf_component_type_r_8u: + return *((const uint8_t*) in); + + case cgltf_component_type_r_16: + return *((const int16_t*) in); + + case cgltf_component_type_r_16u: + return *((const uint16_t*) in); + + case cgltf_component_type_r_32u: + return *((const uint32_t*) in); + + default: + return 0; + } +} + +static cgltf_bool cgltf_element_read_uint(const uint8_t* element, cgltf_type type, cgltf_component_type component_type, cgltf_uint* out, cgltf_size element_size) +{ + cgltf_size num_components = cgltf_num_components(type); + + if (element_size < num_components) + { + return 0; + } + + // Reading integer matrices is not a valid use case + if (type == cgltf_type_mat2 || type == cgltf_type_mat3 || type == cgltf_type_mat4) + { + return 0; + } + + cgltf_size component_size = cgltf_component_size(component_type); + + for (cgltf_size i = 0; i < num_components; ++i) + { + out[i] = cgltf_component_read_uint(element + component_size * i, component_type); + } + return 1; +} + +cgltf_bool cgltf_accessor_read_uint(const cgltf_accessor* accessor, cgltf_size index, cgltf_uint* out, cgltf_size element_size) +{ + if (accessor->is_sparse) + { + const uint8_t* element = cgltf_find_sparse_index(accessor, index); + if (element) + return cgltf_element_read_uint(element, accessor->type, accessor->component_type, out, element_size); + } + if (accessor->buffer_view == NULL) + { + memset(out, 0, element_size * sizeof(cgltf_uint)); + return 1; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset + accessor->stride * index; + return cgltf_element_read_uint(element, accessor->type, accessor->component_type, out, element_size); +} + +cgltf_size cgltf_accessor_read_index(const cgltf_accessor* accessor, cgltf_size index) +{ + if (accessor->is_sparse) + { + const uint8_t* element = cgltf_find_sparse_index(accessor, index); + if (element) + return cgltf_component_read_index(element, accessor->component_type); + } + if (accessor->buffer_view == NULL) + { + return 0; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; // This is an error case, but we can't communicate the error with existing interface. + } + element += accessor->offset + accessor->stride * index; + return cgltf_component_read_index(element, accessor->component_type); +} + +cgltf_size cgltf_mesh_index(const cgltf_data* data, const cgltf_mesh* object) +{ + assert(object && (cgltf_size)(object - data->meshes) < data->meshes_count); + return (cgltf_size)(object - data->meshes); +} + +cgltf_size cgltf_material_index(const cgltf_data* data, const cgltf_material* object) +{ + assert(object && (cgltf_size)(object - data->materials) < data->materials_count); + return (cgltf_size)(object - data->materials); +} + +cgltf_size cgltf_accessor_index(const cgltf_data* data, const cgltf_accessor* object) +{ + assert(object && (cgltf_size)(object - data->accessors) < data->accessors_count); + return (cgltf_size)(object - data->accessors); +} + +cgltf_size cgltf_buffer_view_index(const cgltf_data* data, const cgltf_buffer_view* object) +{ + assert(object && (cgltf_size)(object - data->buffer_views) < data->buffer_views_count); + return (cgltf_size)(object - data->buffer_views); +} + +cgltf_size cgltf_buffer_index(const cgltf_data* data, const cgltf_buffer* object) +{ + assert(object && (cgltf_size)(object - data->buffers) < data->buffers_count); + return (cgltf_size)(object - data->buffers); +} + +cgltf_size cgltf_image_index(const cgltf_data* data, const cgltf_image* object) +{ + assert(object && (cgltf_size)(object - data->images) < data->images_count); + return (cgltf_size)(object - data->images); +} + +cgltf_size cgltf_texture_index(const cgltf_data* data, const cgltf_texture* object) +{ + assert(object && (cgltf_size)(object - data->textures) < data->textures_count); + return (cgltf_size)(object - data->textures); +} + +cgltf_size cgltf_sampler_index(const cgltf_data* data, const cgltf_sampler* object) +{ + assert(object && (cgltf_size)(object - data->samplers) < data->samplers_count); + return (cgltf_size)(object - data->samplers); +} + +cgltf_size cgltf_skin_index(const cgltf_data* data, const cgltf_skin* object) +{ + assert(object && (cgltf_size)(object - data->skins) < data->skins_count); + return (cgltf_size)(object - data->skins); +} + +cgltf_size cgltf_camera_index(const cgltf_data* data, const cgltf_camera* object) +{ + assert(object && (cgltf_size)(object - data->cameras) < data->cameras_count); + return (cgltf_size)(object - data->cameras); +} + +cgltf_size cgltf_light_index(const cgltf_data* data, const cgltf_light* object) +{ + assert(object && (cgltf_size)(object - data->lights) < data->lights_count); + return (cgltf_size)(object - data->lights); +} + +cgltf_size cgltf_node_index(const cgltf_data* data, const cgltf_node* object) +{ + assert(object && (cgltf_size)(object - data->nodes) < data->nodes_count); + return (cgltf_size)(object - data->nodes); +} + +cgltf_size cgltf_scene_index(const cgltf_data* data, const cgltf_scene* object) +{ + assert(object && (cgltf_size)(object - data->scenes) < data->scenes_count); + return (cgltf_size)(object - data->scenes); +} + +cgltf_size cgltf_animation_index(const cgltf_data* data, const cgltf_animation* object) +{ + assert(object && (cgltf_size)(object - data->animations) < data->animations_count); + return (cgltf_size)(object - data->animations); +} + +cgltf_size cgltf_animation_sampler_index(const cgltf_animation* animation, const cgltf_animation_sampler* object) +{ + assert(object && (cgltf_size)(object - animation->samplers) < animation->samplers_count); + return (cgltf_size)(object - animation->samplers); +} + +cgltf_size cgltf_animation_channel_index(const cgltf_animation* animation, const cgltf_animation_channel* object) +{ + assert(object && (cgltf_size)(object - animation->channels) < animation->channels_count); + return (cgltf_size)(object - animation->channels); +} + +cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, void* out, cgltf_size out_component_size, cgltf_size index_count) +{ + if (out == NULL) + { + return accessor->count; + } + + cgltf_size numbers_per_element = cgltf_num_components(accessor->type); + cgltf_size available_numbers = accessor->count * numbers_per_element; + + index_count = available_numbers < index_count ? available_numbers : index_count; + cgltf_size index_component_size = cgltf_component_size(accessor->component_type); + + if (accessor->is_sparse) + { + return 0; + } + if (accessor->buffer_view == NULL) + { + return 0; + } + if (index_component_size > out_component_size) + { + return 0; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset; + + if (index_component_size == out_component_size && accessor->stride == out_component_size * numbers_per_element) + { + memcpy(out, element, index_count * index_component_size); + return index_count; + } + + // Data couldn't be copied with memcpy due to stride being larger than the component size. + // OR + // The component size of the output array is larger than the component size of the index data, so index data will be padded. + switch (out_component_size) + { + case 1: + for (cgltf_size index = 0; index < index_count; index++, element += accessor->stride) + { + ((uint8_t*)out)[index] = (uint8_t)cgltf_component_read_index(element, accessor->component_type); + } + break; + case 2: + for (cgltf_size index = 0; index < index_count; index++, element += accessor->stride) + { + ((uint16_t*)out)[index] = (uint16_t)cgltf_component_read_index(element, accessor->component_type); + } + break; + case 4: + for (cgltf_size index = 0; index < index_count; index++, element += accessor->stride) + { + ((uint32_t*)out)[index] = (uint32_t)cgltf_component_read_index(element, accessor->component_type); + } + break; + default: + return 0; + } + + return index_count; +} + +#define CGLTF_ERROR_JSON -1 +#define CGLTF_ERROR_NOMEM -2 +#define CGLTF_ERROR_LEGACY -3 + +#define CGLTF_CHECK_TOKTYPE(tok_, type_) if ((tok_).type != (type_)) { return CGLTF_ERROR_JSON; } +#define CGLTF_CHECK_TOKTYPE_RET(tok_, type_, ret_) if ((tok_).type != (type_)) { return ret_; } +#define CGLTF_CHECK_KEY(tok_) if ((tok_).type != JSMN_STRING || (tok_).size == 0) { return CGLTF_ERROR_JSON; } /* checking size for 0 verifies that a value follows the key */ + +#define CGLTF_PTRINDEX(type, idx) (type*)((cgltf_size)idx + 1) +#define CGLTF_PTRFIXUP(var, data, size) if (var) { if ((cgltf_size)var > size) { return CGLTF_ERROR_JSON; } var = &data[(cgltf_size)var-1]; } +#define CGLTF_PTRFIXUP_REQ(var, data, size) if (!var || (cgltf_size)var > size) { return CGLTF_ERROR_JSON; } var = &data[(cgltf_size)var-1]; + +static int cgltf_json_strcmp(jsmntok_t const* tok, const uint8_t* json_chunk, const char* str) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_STRING); + size_t const str_len = strlen(str); + size_t const name_length = (size_t)(tok->end - tok->start); + return (str_len == name_length) ? strncmp((const char*)json_chunk + tok->start, str, str_len) : 128; +} + +static int cgltf_json_to_int(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_PRIMITIVE); + char tmp[128]; + int size = (size_t)(tok->end - tok->start) < sizeof(tmp) ? (int)(tok->end - tok->start) : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + return CGLTF_ATOI(tmp); +} + +static cgltf_size cgltf_json_to_size(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE_RET(*tok, JSMN_PRIMITIVE, 0); + char tmp[128]; + int size = (size_t)(tok->end - tok->start) < sizeof(tmp) ? (int)(tok->end - tok->start) : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + long long res = CGLTF_ATOLL(tmp); + return res < 0 ? 0 : (cgltf_size)res; +} + +static cgltf_float cgltf_json_to_float(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_PRIMITIVE); + char tmp[128]; + int size = (size_t)(tok->end - tok->start) < sizeof(tmp) ? (int)(tok->end - tok->start) : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + return (cgltf_float)CGLTF_ATOF(tmp); +} + +static cgltf_bool cgltf_json_to_bool(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int size = (int)(tok->end - tok->start); + return size == 4 && memcmp(json_chunk + tok->start, "true", 4) == 0; +} + +static int cgltf_skip_json(jsmntok_t const* tokens, int i) +{ + int end = i + 1; + + while (i < end) + { + switch (tokens[i].type) + { + case JSMN_OBJECT: + end += tokens[i].size * 2; + break; + + case JSMN_ARRAY: + end += tokens[i].size; + break; + + case JSMN_PRIMITIVE: + case JSMN_STRING: + break; + + default: + return -1; + } + + i++; + } + + return i; +} + +static void cgltf_fill_float_array(float* out_array, int size, float value) +{ + for (int j = 0; j < size; ++j) + { + out_array[j] = value; + } +} + +static int cgltf_parse_json_float_array(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, float* out_array, int size) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + if (tokens[i].size != size) + { + return CGLTF_ERROR_JSON; + } + ++i; + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_array[j] = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + return i; +} + +static int cgltf_parse_json_string(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, char** out_string) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_STRING); + if (*out_string) + { + return CGLTF_ERROR_JSON; + } + int size = (int)(tokens[i].end - tokens[i].start); + char* result = (char*)options->memory.alloc_func(options->memory.user_data, size + 1); + if (!result) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(result, (const char*)json_chunk + tokens[i].start, size); + result[size] = 0; + *out_string = result; + return i + 1; +} + +static int cgltf_parse_json_array(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, size_t element_size, void** out_array, cgltf_size* out_size) +{ + (void)json_chunk; + if (tokens[i].type != JSMN_ARRAY) + { + return tokens[i].type == JSMN_OBJECT ? CGLTF_ERROR_LEGACY : CGLTF_ERROR_JSON; + } + if (*out_array) + { + return CGLTF_ERROR_JSON; + } + int size = tokens[i].size; + void* result = cgltf_calloc(options, element_size, size); + if (!result) + { + return CGLTF_ERROR_NOMEM; + } + *out_array = result; + *out_size = size; + return i + 1; +} + +static int cgltf_parse_json_string_array(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, char*** out_array, cgltf_size* out_size) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(char*), (void**)out_array, out_size); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < *out_size; ++j) + { + i = cgltf_parse_json_string(options, tokens, i, json_chunk, j + (*out_array)); + if (i < 0) + { + return i; + } + } + return i; +} + +static void cgltf_parse_attribute_type(const char* name, cgltf_attribute_type* out_type, int* out_index) +{ + if (*name == '_') + { + *out_type = cgltf_attribute_type_custom; + return; + } + + const char* us = strchr(name, '_'); + size_t len = us ? (size_t)(us - name) : strlen(name); + + if (len == 8 && strncmp(name, "POSITION", 8) == 0) + { + *out_type = cgltf_attribute_type_position; + } + else if (len == 6 && strncmp(name, "NORMAL", 6) == 0) + { + *out_type = cgltf_attribute_type_normal; + } + else if (len == 7 && strncmp(name, "TANGENT", 7) == 0) + { + *out_type = cgltf_attribute_type_tangent; + } + else if (len == 8 && strncmp(name, "TEXCOORD", 8) == 0) + { + *out_type = cgltf_attribute_type_texcoord; + } + else if (len == 5 && strncmp(name, "COLOR", 5) == 0) + { + *out_type = cgltf_attribute_type_color; + } + else if (len == 6 && strncmp(name, "JOINTS", 6) == 0) + { + *out_type = cgltf_attribute_type_joints; + } + else if (len == 7 && strncmp(name, "WEIGHTS", 7) == 0) + { + *out_type = cgltf_attribute_type_weights; + } + else + { + *out_type = cgltf_attribute_type_invalid; + } + + if (us && *out_type != cgltf_attribute_type_invalid) + { + *out_index = CGLTF_ATOI(us + 1); + if (*out_index < 0) + { + *out_type = cgltf_attribute_type_invalid; + *out_index = 0; + } + } +} + +static int cgltf_parse_json_attribute_list(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_attribute** out_attributes, cgltf_size* out_attributes_count) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + if (*out_attributes) + { + return CGLTF_ERROR_JSON; + } + + *out_attributes_count = tokens[i].size; + *out_attributes = (cgltf_attribute*)cgltf_calloc(options, sizeof(cgltf_attribute), *out_attributes_count); + ++i; + + if (!*out_attributes) + { + return CGLTF_ERROR_NOMEM; + } + + for (cgltf_size j = 0; j < *out_attributes_count; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + i = cgltf_parse_json_string(options, tokens, i, json_chunk, &(*out_attributes)[j].name); + if (i < 0) + { + return CGLTF_ERROR_JSON; + } + + cgltf_parse_attribute_type((*out_attributes)[j].name, &(*out_attributes)[j].type, &(*out_attributes)[j].index); + + (*out_attributes)[j].data = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + + return i; +} + +static int cgltf_parse_json_extras(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_extras* out_extras) +{ + if (out_extras->data) + { + return CGLTF_ERROR_JSON; + } + + /* fill deprecated fields for now, this will be removed in the future */ + out_extras->start_offset = tokens[i].start; + out_extras->end_offset = tokens[i].end; + + size_t start = tokens[i].start; + size_t size = tokens[i].end - start; + out_extras->data = (char*)options->memory.alloc_func(options->memory.user_data, size + 1); + if (!out_extras->data) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(out_extras->data, (const char*)json_chunk + start, size); + out_extras->data[size] = '\0'; + + i = cgltf_skip_json(tokens, i); + return i; +} + +static int cgltf_parse_json_unprocessed_extension(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_extension* out_extension) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_STRING); + CGLTF_CHECK_TOKTYPE(tokens[i+1], JSMN_OBJECT); + if (out_extension->name) + { + return CGLTF_ERROR_JSON; + } + + cgltf_size name_length = tokens[i].end - tokens[i].start; + out_extension->name = (char*)options->memory.alloc_func(options->memory.user_data, name_length + 1); + if (!out_extension->name) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(out_extension->name, (const char*)json_chunk + tokens[i].start, name_length); + out_extension->name[name_length] = 0; + i++; + + size_t start = tokens[i].start; + size_t size = tokens[i].end - start; + out_extension->data = (char*)options->memory.alloc_func(options->memory.user_data, size + 1); + if (!out_extension->data) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(out_extension->data, (const char*)json_chunk + start, size); + out_extension->data[size] = '\0'; + + i = cgltf_skip_json(tokens, i); + + return i; +} + +static int cgltf_parse_json_unprocessed_extensions(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_size* out_extensions_count, cgltf_extension** out_extensions) +{ + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(*out_extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + *out_extensions_count = 0; + *out_extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!*out_extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int j = 0; j < extensions_size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + cgltf_size extension_index = (*out_extensions_count)++; + cgltf_extension* extension = &((*out_extensions)[extension_index]); + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, extension); + + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_draco_mesh_compression(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_draco_mesh_compression* out_draco_mesh_compression) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "attributes") == 0) + { + i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_draco_mesh_compression->attributes, &out_draco_mesh_compression->attributes_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "bufferView") == 0) + { + ++i; + out_draco_mesh_compression->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_mesh_gpu_instancing(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_mesh_gpu_instancing* out_mesh_gpu_instancing) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "attributes") == 0) + { + i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_mesh_gpu_instancing->attributes, &out_mesh_gpu_instancing->attributes_count); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_material_mapping_data(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material_mapping* out_mappings, cgltf_size* offset) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int obj_size = tokens[i].size; + ++i; + + int material = -1; + int variants_tok = -1; + int extras_tok = -1; + + for (int k = 0; k < obj_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "material") == 0) + { + ++i; + material = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "variants") == 0) + { + variants_tok = i+1; + CGLTF_CHECK_TOKTYPE(tokens[variants_tok], JSMN_ARRAY); + + i = cgltf_skip_json(tokens, i+1); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + extras_tok = i + 1; + i = cgltf_skip_json(tokens, extras_tok); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + if (material < 0 || variants_tok < 0) + { + return CGLTF_ERROR_JSON; + } + + if (out_mappings) + { + for (int k = 0; k < tokens[variants_tok].size; ++k) + { + int variant = cgltf_json_to_int(&tokens[variants_tok + 1 + k], json_chunk); + if (variant < 0) + return variant; + + out_mappings[*offset].material = CGLTF_PTRINDEX(cgltf_material, material); + out_mappings[*offset].variant = variant; + + if (extras_tok >= 0) + { + int e = cgltf_parse_json_extras(options, tokens, extras_tok, json_chunk, &out_mappings[*offset].extras); + if (e < 0) + return e; + } + + (*offset)++; + } + } + else + { + (*offset) += tokens[variants_tok].size; + } + } + + return i; +} + +static int cgltf_parse_json_material_mappings(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_primitive* out_prim) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "mappings") == 0) + { + if (out_prim->mappings) + { + return CGLTF_ERROR_JSON; + } + + cgltf_size mappings_offset = 0; + int k = cgltf_parse_json_material_mapping_data(options, tokens, i + 1, json_chunk, NULL, &mappings_offset); + if (k < 0) + { + return k; + } + + out_prim->mappings_count = mappings_offset; + out_prim->mappings = (cgltf_material_mapping*)cgltf_calloc(options, sizeof(cgltf_material_mapping), out_prim->mappings_count); + + mappings_offset = 0; + i = cgltf_parse_json_material_mapping_data(options, tokens, i + 1, json_chunk, out_prim->mappings, &mappings_offset); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static cgltf_primitive_type cgltf_json_to_primitive_type(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int type = cgltf_json_to_int(tok, json_chunk); + + switch (type) + { + case 0: + return cgltf_primitive_type_points; + case 1: + return cgltf_primitive_type_lines; + case 2: + return cgltf_primitive_type_line_loop; + case 3: + return cgltf_primitive_type_line_strip; + case 4: + return cgltf_primitive_type_triangles; + case 5: + return cgltf_primitive_type_triangle_strip; + case 6: + return cgltf_primitive_type_triangle_fan; + default: + return cgltf_primitive_type_invalid; + } +} + +static int cgltf_parse_json_primitive(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_primitive* out_prim) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_prim->type = cgltf_primitive_type_triangles; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "mode") == 0) + { + ++i; + out_prim->type = cgltf_json_to_primitive_type(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0) + { + ++i; + out_prim->indices = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "material") == 0) + { + ++i; + out_prim->material = CGLTF_PTRINDEX(cgltf_material, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "attributes") == 0) + { + i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_prim->attributes, &out_prim->attributes_count); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "targets") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_morph_target), (void**)&out_prim->targets, &out_prim->targets_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_prim->targets_count; ++k) + { + i = cgltf_parse_json_attribute_list(options, tokens, i, json_chunk, &out_prim->targets[k].attributes, &out_prim->targets[k].attributes_count); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_prim->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_prim->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_prim->extensions_count = 0; + out_prim->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_prim->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_draco_mesh_compression") == 0) + { + out_prim->has_draco_mesh_compression = 1; + i = cgltf_parse_json_draco_mesh_compression(options, tokens, i + 1, json_chunk, &out_prim->draco_mesh_compression); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_variants") == 0) + { + i = cgltf_parse_json_material_mappings(options, tokens, i + 1, json_chunk, out_prim); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_prim->extensions[out_prim->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_mesh(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_mesh* out_mesh) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_mesh->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "primitives") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_primitive), (void**)&out_mesh->primitives, &out_mesh->primitives_count); + if (i < 0) + { + return i; + } + + for (cgltf_size prim_index = 0; prim_index < out_mesh->primitives_count; ++prim_index) + { + i = cgltf_parse_json_primitive(options, tokens, i, json_chunk, &out_mesh->primitives[prim_index]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "weights") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_float), (void**)&out_mesh->weights, &out_mesh->weights_count); + if (i < 0) + { + return i; + } + + i = cgltf_parse_json_float_array(tokens, i - 1, json_chunk, out_mesh->weights, (int)out_mesh->weights_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + ++i; + + out_mesh->extras.start_offset = tokens[i].start; + out_mesh->extras.end_offset = tokens[i].end; + + if (tokens[i].type == JSMN_OBJECT) + { + int extras_size = tokens[i].size; + ++i; + + for (int k = 0; k < extras_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "targetNames") == 0 && tokens[i+1].type == JSMN_ARRAY) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_mesh->target_names, &out_mesh->target_names_count); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i); + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_mesh->extensions_count, &out_mesh->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_meshes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_mesh), (void**)&out_data->meshes, &out_data->meshes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->meshes_count; ++j) + { + i = cgltf_parse_json_mesh(options, tokens, i, json_chunk, &out_data->meshes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static cgltf_component_type cgltf_json_to_component_type(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int type = cgltf_json_to_int(tok, json_chunk); + + switch (type) + { + case 5120: + return cgltf_component_type_r_8; + case 5121: + return cgltf_component_type_r_8u; + case 5122: + return cgltf_component_type_r_16; + case 5123: + return cgltf_component_type_r_16u; + case 5125: + return cgltf_component_type_r_32u; + case 5126: + return cgltf_component_type_r_32f; + default: + return cgltf_component_type_invalid; + } +} + +static int cgltf_parse_json_accessor_sparse(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor_sparse* out_sparse) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_sparse->count = cgltf_json_to_size(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int indices_size = tokens[i].size; + ++i; + + for (int k = 0; k < indices_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_sparse->indices_buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_sparse->indices_byte_offset = cgltf_json_to_size(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "componentType") == 0) + { + ++i; + out_sparse->indices_component_type = cgltf_json_to_component_type(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "values") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int values_size = tokens[i].size; + ++i; + + for (int k = 0; k < values_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_sparse->values_buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_sparse->values_byte_offset = cgltf_json_to_size(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_accessor(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor* out_accessor) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_accessor->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_accessor->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_accessor->offset = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "componentType") == 0) + { + ++i; + out_accessor->component_type = cgltf_json_to_component_type(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "normalized") == 0) + { + ++i; + out_accessor->normalized = cgltf_json_to_bool(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_accessor->count = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "SCALAR") == 0) + { + out_accessor->type = cgltf_type_scalar; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC2") == 0) + { + out_accessor->type = cgltf_type_vec2; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC3") == 0) + { + out_accessor->type = cgltf_type_vec3; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC4") == 0) + { + out_accessor->type = cgltf_type_vec4; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT2") == 0) + { + out_accessor->type = cgltf_type_mat2; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT3") == 0) + { + out_accessor->type = cgltf_type_mat3; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT4") == 0) + { + out_accessor->type = cgltf_type_mat4; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "min") == 0) + { + ++i; + out_accessor->has_min = 1; + // note: we can't parse the precise number of elements since type may not have been computed yet + int min_size = tokens[i].size > 16 ? 16 : tokens[i].size; + i = cgltf_parse_json_float_array(tokens, i, json_chunk, out_accessor->min, min_size); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "max") == 0) + { + ++i; + out_accessor->has_max = 1; + // note: we can't parse the precise number of elements since type may not have been computed yet + int max_size = tokens[i].size > 16 ? 16 : tokens[i].size; + i = cgltf_parse_json_float_array(tokens, i, json_chunk, out_accessor->max, max_size); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "sparse") == 0) + { + out_accessor->is_sparse = 1; + i = cgltf_parse_json_accessor_sparse(tokens, i + 1, json_chunk, &out_accessor->sparse); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_accessor->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_accessor->extensions_count, &out_accessor->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture_transform(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture_transform* out_texture_transform) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "offset") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_texture_transform->offset, 2); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "rotation") == 0) + { + ++i; + out_texture_transform->rotation = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_texture_transform->scale, 2); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "texCoord") == 0) + { + ++i; + out_texture_transform->has_texcoord = 1; + out_texture_transform->texcoord = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture_view(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture_view* out_texture_view) +{ + (void)options; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_texture_view->scale = 1.0f; + cgltf_fill_float_array(out_texture_view->transform.scale, 2, 1.0f); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "index") == 0) + { + ++i; + out_texture_view->texture = CGLTF_PTRINDEX(cgltf_texture, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "texCoord") == 0) + { + ++i; + out_texture_view->texcoord = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0) + { + ++i; + out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "strength") == 0) + { + ++i; + out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int extensions_size = tokens[i].size; + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_texture_transform") == 0) + { + out_texture_view->has_transform = 1; + i = cgltf_parse_json_texture_transform(tokens, i + 1, json_chunk, &out_texture_view->transform); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_pbr_metallic_roughness(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_pbr_metallic_roughness* out_pbr) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "metallicFactor") == 0) + { + ++i; + out_pbr->metallic_factor = + cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "roughnessFactor") == 0) + { + ++i; + out_pbr->roughness_factor = + cgltf_json_to_float(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "baseColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->base_color_factor, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "baseColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->base_color_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "metallicRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->metallic_roughness_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_pbr_specular_glossiness(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_pbr_specular_glossiness* out_pbr) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "diffuseFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->diffuse_factor, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->specular_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "glossinessFactor") == 0) + { + ++i; + out_pbr->glossiness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "diffuseTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->diffuse_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularGlossinessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->specular_glossiness_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_clearcoat(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_clearcoat* out_clearcoat) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatFactor") == 0) + { + ++i; + out_clearcoat->clearcoat_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatRoughnessFactor") == 0) + { + ++i; + out_clearcoat->clearcoat_roughness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_roughness_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatNormalTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_normal_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_ior(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_ior* out_ior) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default values + out_ior->ior = 1.5f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "ior") == 0) + { + ++i; + out_ior->ior = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_specular(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_specular* out_specular) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default values + out_specular->specular_factor = 1.0f; + cgltf_fill_float_array(out_specular->specular_color_factor, 3, 1.0f); + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "specularFactor") == 0) + { + ++i; + out_specular->specular_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_specular->specular_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_specular->specular_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "specularColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_specular->specular_color_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_transmission(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_transmission* out_transmission) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "transmissionFactor") == 0) + { + ++i; + out_transmission->transmission_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "transmissionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_transmission->transmission_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_volume(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_volume* out_volume) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "thicknessFactor") == 0) + { + ++i; + out_volume->thickness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "thicknessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_volume->thickness_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "attenuationColor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_volume->attenuation_color, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "attenuationDistance") == 0) + { + ++i; + out_volume->attenuation_distance = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_sheen(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_sheen* out_sheen) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_sheen->sheen_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_sheen->sheen_color_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenRoughnessFactor") == 0) + { + ++i; + out_sheen->sheen_roughness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_sheen->sheen_roughness_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_emissive_strength(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_emissive_strength* out_emissive_strength) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default + out_emissive_strength->emissive_strength = 1.f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "emissiveStrength") == 0) + { + ++i; + out_emissive_strength->emissive_strength = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_iridescence(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_iridescence* out_iridescence) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default + out_iridescence->iridescence_ior = 1.3f; + out_iridescence->iridescence_thickness_min = 100.f; + out_iridescence->iridescence_thickness_max = 400.f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceFactor") == 0) + { + ++i; + out_iridescence->iridescence_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_iridescence->iridescence_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceIor") == 0) + { + ++i; + out_iridescence->iridescence_ior = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceThicknessMinimum") == 0) + { + ++i; + out_iridescence->iridescence_thickness_min = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceThicknessMaximum") == 0) + { + ++i; + out_iridescence->iridescence_thickness_max = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceThicknessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_iridescence->iridescence_thickness_texture); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_diffuse_transmission(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_diffuse_transmission* out_diff_transmission) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Defaults + cgltf_fill_float_array(out_diff_transmission->diffuse_transmission_color_factor, 3, 1.0f); + out_diff_transmission->diffuse_transmission_factor = 0.f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionFactor") == 0) + { + ++i; + out_diff_transmission->diffuse_transmission_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_diff_transmission->diffuse_transmission_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_diff_transmission->diffuse_transmission_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_diff_transmission->diffuse_transmission_color_texture); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_anisotropy(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_anisotropy* out_anisotropy) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "anisotropyStrength") == 0) + { + ++i; + out_anisotropy->anisotropy_strength = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "anisotropyRotation") == 0) + { + ++i; + out_anisotropy->anisotropy_rotation = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "anisotropyTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_anisotropy->anisotropy_texture); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_dispersion(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_dispersion* out_dispersion) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "dispersion") == 0) + { + ++i; + out_dispersion->dispersion = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_image(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_image* out_image) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "uri") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->uri); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_image->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "mimeType") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->mime_type); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_image->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_image->extensions_count, &out_image->extensions); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_sampler(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_sampler* out_sampler) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_sampler->wrap_s = cgltf_wrap_mode_repeat; + out_sampler->wrap_t = cgltf_wrap_mode_repeat; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_sampler->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "magFilter") == 0) + { + ++i; + out_sampler->mag_filter + = (cgltf_filter_type)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "minFilter") == 0) + { + ++i; + out_sampler->min_filter + = (cgltf_filter_type)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapS") == 0) + { + ++i; + out_sampler->wrap_s + = (cgltf_wrap_mode)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapT") == 0) + { + ++i; + out_sampler->wrap_t + = (cgltf_wrap_mode)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_sampler->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sampler->extensions_count, &out_sampler->extensions); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture* out_texture) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_texture->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "sampler") == 0) + { + ++i; + out_texture->sampler = CGLTF_PTRINDEX(cgltf_sampler, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_texture->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if (out_texture->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + ++i; + out_texture->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + out_texture->extensions_count = 0; + + if (!out_texture->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_texture_basisu") == 0) + { + out_texture->has_basisu = 1; + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int num_properties = tokens[i].size; + ++i; + + for (int t = 0; t < num_properties; ++t) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->basisu_image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "EXT_texture_webp") == 0) + { + out_texture->has_webp = 1; + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int num_properties = tokens[i].size; + ++i; + + for (int t = 0; t < num_properties; ++t) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->webp_image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_texture->extensions[out_texture->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_material(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material* out_material) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + cgltf_fill_float_array(out_material->pbr_metallic_roughness.base_color_factor, 4, 1.0f); + out_material->pbr_metallic_roughness.metallic_factor = 1.0f; + out_material->pbr_metallic_roughness.roughness_factor = 1.0f; + + cgltf_fill_float_array(out_material->pbr_specular_glossiness.diffuse_factor, 4, 1.0f); + cgltf_fill_float_array(out_material->pbr_specular_glossiness.specular_factor, 3, 1.0f); + out_material->pbr_specular_glossiness.glossiness_factor = 1.0f; + + cgltf_fill_float_array(out_material->volume.attenuation_color, 3, 1.0f); + out_material->volume.attenuation_distance = FLT_MAX; + + out_material->alpha_cutoff = 0.5f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_material->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "pbrMetallicRoughness") == 0) + { + out_material->has_pbr_metallic_roughness = 1; + i = cgltf_parse_json_pbr_metallic_roughness(options, tokens, i + 1, json_chunk, &out_material->pbr_metallic_roughness); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "emissiveFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_material->emissive_factor, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "normalTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->normal_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "occlusionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->occlusion_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "emissiveTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->emissive_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "alphaMode") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "OPAQUE") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_opaque; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "MASK") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_mask; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "BLEND") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_blend; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "alphaCutoff") == 0) + { + ++i; + out_material->alpha_cutoff = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "doubleSided") == 0) + { + ++i; + out_material->double_sided = + cgltf_json_to_bool(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_material->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_material->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + ++i; + out_material->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + out_material->extensions_count= 0; + + if (!out_material->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_pbrSpecularGlossiness") == 0) + { + out_material->has_pbr_specular_glossiness = 1; + i = cgltf_parse_json_pbr_specular_glossiness(options, tokens, i + 1, json_chunk, &out_material->pbr_specular_glossiness); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_unlit") == 0) + { + out_material->unlit = 1; + i = cgltf_skip_json(tokens, i+1); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_clearcoat") == 0) + { + out_material->has_clearcoat = 1; + i = cgltf_parse_json_clearcoat(options, tokens, i + 1, json_chunk, &out_material->clearcoat); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_ior") == 0) + { + out_material->has_ior = 1; + i = cgltf_parse_json_ior(tokens, i + 1, json_chunk, &out_material->ior); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_specular") == 0) + { + out_material->has_specular = 1; + i = cgltf_parse_json_specular(options, tokens, i + 1, json_chunk, &out_material->specular); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_transmission") == 0) + { + out_material->has_transmission = 1; + i = cgltf_parse_json_transmission(options, tokens, i + 1, json_chunk, &out_material->transmission); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_volume") == 0) + { + out_material->has_volume = 1; + i = cgltf_parse_json_volume(options, tokens, i + 1, json_chunk, &out_material->volume); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_sheen") == 0) + { + out_material->has_sheen = 1; + i = cgltf_parse_json_sheen(options, tokens, i + 1, json_chunk, &out_material->sheen); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_emissive_strength") == 0) + { + out_material->has_emissive_strength = 1; + i = cgltf_parse_json_emissive_strength(tokens, i + 1, json_chunk, &out_material->emissive_strength); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_iridescence") == 0) + { + out_material->has_iridescence = 1; + i = cgltf_parse_json_iridescence(options, tokens, i + 1, json_chunk, &out_material->iridescence); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_diffuse_transmission") == 0) + { + out_material->has_diffuse_transmission = 1; + i = cgltf_parse_json_diffuse_transmission(options, tokens, i + 1, json_chunk, &out_material->diffuse_transmission); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_anisotropy") == 0) + { + out_material->has_anisotropy = 1; + i = cgltf_parse_json_anisotropy(options, tokens, i + 1, json_chunk, &out_material->anisotropy); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_dispersion") == 0) + { + out_material->has_dispersion = 1; + i = cgltf_parse_json_dispersion(tokens, i + 1, json_chunk, &out_material->dispersion); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_material->extensions[out_material->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_accessors(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_accessor), (void**)&out_data->accessors, &out_data->accessors_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->accessors_count; ++j) + { + i = cgltf_parse_json_accessor(options, tokens, i, json_chunk, &out_data->accessors[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_materials(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_material), (void**)&out_data->materials, &out_data->materials_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->materials_count; ++j) + { + i = cgltf_parse_json_material(options, tokens, i, json_chunk, &out_data->materials[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_images(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_image), (void**)&out_data->images, &out_data->images_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->images_count; ++j) + { + i = cgltf_parse_json_image(options, tokens, i, json_chunk, &out_data->images[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_textures(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_texture), (void**)&out_data->textures, &out_data->textures_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->textures_count; ++j) + { + i = cgltf_parse_json_texture(options, tokens, i, json_chunk, &out_data->textures[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_samplers(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_sampler), (void**)&out_data->samplers, &out_data->samplers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->samplers_count; ++j) + { + i = cgltf_parse_json_sampler(options, tokens, i, json_chunk, &out_data->samplers[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_meshopt_compression(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_meshopt_compression* out_meshopt_compression) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "buffer") == 0) + { + ++i; + out_meshopt_compression->buffer = CGLTF_PTRINDEX(cgltf_buffer, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_meshopt_compression->offset = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_meshopt_compression->size = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteStride") == 0) + { + ++i; + out_meshopt_compression->stride = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_meshopt_compression->count = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "mode") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "ATTRIBUTES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_attributes; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "TRIANGLES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_triangles; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "INDICES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_indices; + } + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "filter") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "NONE") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_none; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "OCTAHEDRAL") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_octahedral; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "QUATERNION") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_quaternion; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "EXPONENTIAL") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_exponential; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "COLOR") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_color; + } + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffer_view(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_buffer_view* out_buffer_view) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer_view->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "buffer") == 0) + { + ++i; + out_buffer_view->buffer = CGLTF_PTRINDEX(cgltf_buffer, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_buffer_view->offset = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_buffer_view->size = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteStride") == 0) + { + ++i; + out_buffer_view->stride = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "target") == 0) + { + ++i; + int type = cgltf_json_to_int(tokens+i, json_chunk); + switch (type) + { + case 34962: + type = cgltf_buffer_view_type_vertices; + break; + case 34963: + type = cgltf_buffer_view_type_indices; + break; + default: + type = cgltf_buffer_view_type_invalid; + break; + } + out_buffer_view->type = (cgltf_buffer_view_type)type; + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_buffer_view->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_buffer_view->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_buffer_view->extensions_count = 0; + out_buffer_view->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_buffer_view->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "EXT_meshopt_compression") == 0) + { + out_buffer_view->has_meshopt_compression = 1; + i = cgltf_parse_json_meshopt_compression(options, tokens, i + 1, json_chunk, &out_buffer_view->meshopt_compression); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_meshopt_compression") == 0) + { + out_buffer_view->has_meshopt_compression = 1; + out_buffer_view->meshopt_compression.is_khr = 1; + i = cgltf_parse_json_meshopt_compression(options, tokens, i + 1, json_chunk, &out_buffer_view->meshopt_compression); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_buffer_view->extensions[out_buffer_view->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffer_views(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_buffer_view), (void**)&out_data->buffer_views, &out_data->buffer_views_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->buffer_views_count; ++j) + { + i = cgltf_parse_json_buffer_view(options, tokens, i, json_chunk, &out_data->buffer_views[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_buffer(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_buffer* out_buffer) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_buffer->size = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "uri") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer->uri); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_buffer->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_buffer->extensions_count, &out_buffer->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffers(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_buffer), (void**)&out_data->buffers, &out_data->buffers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->buffers_count; ++j) + { + i = cgltf_parse_json_buffer(options, tokens, i, json_chunk, &out_data->buffers[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_skin(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_skin* out_skin) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_skin->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "joints") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_skin->joints, &out_skin->joints_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_skin->joints_count; ++k) + { + out_skin->joints[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "skeleton") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_skin->skeleton = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "inverseBindMatrices") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_skin->inverse_bind_matrices = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_skin->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_skin->extensions_count, &out_skin->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_skins(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_skin), (void**)&out_data->skins, &out_data->skins_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->skins_count; ++j) + { + i = cgltf_parse_json_skin(options, tokens, i, json_chunk, &out_data->skins[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_camera(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_camera* out_camera) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_camera->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "perspective") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + if (out_camera->type != cgltf_camera_type_invalid) + { + return CGLTF_ERROR_JSON; + } + + out_camera->type = cgltf_camera_type_perspective; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "aspectRatio") == 0) + { + ++i; + out_camera->data.perspective.has_aspect_ratio = 1; + out_camera->data.perspective.aspect_ratio = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "yfov") == 0) + { + ++i; + out_camera->data.perspective.yfov = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "zfar") == 0) + { + ++i; + out_camera->data.perspective.has_zfar = 1; + out_camera->data.perspective.zfar = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "znear") == 0) + { + ++i; + out_camera->data.perspective.znear = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_camera->data.perspective.extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "orthographic") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + if (out_camera->type != cgltf_camera_type_invalid) + { + return CGLTF_ERROR_JSON; + } + + out_camera->type = cgltf_camera_type_orthographic; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "xmag") == 0) + { + ++i; + out_camera->data.orthographic.xmag = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "ymag") == 0) + { + ++i; + out_camera->data.orthographic.ymag = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "zfar") == 0) + { + ++i; + out_camera->data.orthographic.zfar = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "znear") == 0) + { + ++i; + out_camera->data.orthographic.znear = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_camera->data.orthographic.extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_camera->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_camera->extensions_count, &out_camera->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_cameras(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_camera), (void**)&out_data->cameras, &out_data->cameras_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->cameras_count; ++j) + { + i = cgltf_parse_json_camera(options, tokens, i, json_chunk, &out_data->cameras[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_light(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_light* out_light) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_light->color[0] = 1.f; + out_light->color[1] = 1.f; + out_light->color[2] = 1.f; + out_light->intensity = 1.f; + + out_light->spot_inner_cone_angle = 0.f; + out_light->spot_outer_cone_angle = 3.1415926535f / 4.0f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_light->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "color") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_light->color, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "intensity") == 0) + { + ++i; + out_light->intensity = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "directional") == 0) + { + out_light->type = cgltf_light_type_directional; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "point") == 0) + { + out_light->type = cgltf_light_type_point; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "spot") == 0) + { + out_light->type = cgltf_light_type_spot; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "range") == 0) + { + ++i; + out_light->range = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "spot") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "innerConeAngle") == 0) + { + ++i; + out_light->spot_inner_cone_angle = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "outerConeAngle") == 0) + { + ++i; + out_light->spot_outer_cone_angle = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_light->extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_lights(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_light), (void**)&out_data->lights, &out_data->lights_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->lights_count; ++j) + { + i = cgltf_parse_json_light(options, tokens, i, json_chunk, &out_data->lights[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_node(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_node* out_node) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_node->rotation[3] = 1.0f; + out_node->scale[0] = 1.0f; + out_node->scale[1] = 1.0f; + out_node->scale[2] = 1.0f; + out_node->matrix[0] = 1.0f; + out_node->matrix[5] = 1.0f; + out_node->matrix[10] = 1.0f; + out_node->matrix[15] = 1.0f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_node->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "children") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_node->children, &out_node->children_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_node->children_count; ++k) + { + out_node->children[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "mesh") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->mesh = CGLTF_PTRINDEX(cgltf_mesh, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "skin") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->skin = CGLTF_PTRINDEX(cgltf_skin, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "camera") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->camera = CGLTF_PTRINDEX(cgltf_camera, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "translation") == 0) + { + out_node->has_translation = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->translation, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "rotation") == 0) + { + out_node->has_rotation = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->rotation, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "scale") == 0) + { + out_node->has_scale = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->scale, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "matrix") == 0) + { + out_node->has_matrix = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->matrix, 16); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "weights") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_float), (void**)&out_node->weights, &out_node->weights_count); + if (i < 0) + { + return i; + } + + i = cgltf_parse_json_float_array(tokens, i - 1, json_chunk, out_node->weights, (int)out_node->weights_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_node->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_node->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_node->extensions_count= 0; + out_node->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_node->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_lights_punctual") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "light") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->light = CGLTF_PTRINDEX(cgltf_light, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "EXT_mesh_gpu_instancing") == 0) + { + out_node->has_mesh_gpu_instancing = 1; + i = cgltf_parse_json_mesh_gpu_instancing(options, tokens, i + 1, json_chunk, &out_node->mesh_gpu_instancing); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_node->extensions[out_node->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_nodes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_node), (void**)&out_data->nodes, &out_data->nodes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->nodes_count; ++j) + { + i = cgltf_parse_json_node(options, tokens, i, json_chunk, &out_data->nodes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_scene(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_scene* out_scene) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_scene->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "nodes") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_scene->nodes, &out_scene->nodes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_scene->nodes_count; ++k) + { + out_scene->nodes[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_scene->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_scene->extensions_count, &out_scene->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_scenes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_scene), (void**)&out_data->scenes, &out_data->scenes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->scenes_count; ++j) + { + i = cgltf_parse_json_scene(options, tokens, i, json_chunk, &out_data->scenes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_animation_sampler(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation_sampler* out_sampler) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "input") == 0) + { + ++i; + out_sampler->input = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "output") == 0) + { + ++i; + out_sampler->output = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "interpolation") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "LINEAR") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_linear; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "STEP") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_step; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "CUBICSPLINE") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_cubic_spline; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_sampler->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sampler->extensions_count, &out_sampler->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animation_channel(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation_channel* out_channel) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "sampler") == 0) + { + ++i; + out_channel->sampler = CGLTF_PTRINDEX(cgltf_animation_sampler, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "target") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int target_size = tokens[i].size; + ++i; + + for (int k = 0; k < target_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "node") == 0) + { + ++i; + out_channel->target_node = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "path") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "translation") == 0) + { + out_channel->target_path = cgltf_animation_path_type_translation; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "rotation") == 0) + { + out_channel->target_path = cgltf_animation_path_type_rotation; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "scale") == 0) + { + out_channel->target_path = cgltf_animation_path_type_scale; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "weights") == 0) + { + out_channel->target_path = cgltf_animation_path_type_weights; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_channel->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_channel->extensions_count, &out_channel->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animation(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation* out_animation) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_animation->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "samplers") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_animation_sampler), (void**)&out_animation->samplers, &out_animation->samplers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_animation->samplers_count; ++k) + { + i = cgltf_parse_json_animation_sampler(options, tokens, i, json_chunk, &out_animation->samplers[k]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "channels") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_animation_channel), (void**)&out_animation->channels, &out_animation->channels_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_animation->channels_count; ++k) + { + i = cgltf_parse_json_animation_channel(options, tokens, i, json_chunk, &out_animation->channels[k]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_animation->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_animation->extensions_count, &out_animation->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animations(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_animation), (void**)&out_data->animations, &out_data->animations_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->animations_count; ++j) + { + i = cgltf_parse_json_animation(options, tokens, i, json_chunk, &out_data->animations[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_variant(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material_variant* out_variant) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_variant->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_variant->extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_variants(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_material_variant), (void**)&out_data->variants, &out_data->variants_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->variants_count; ++j) + { + i = cgltf_parse_json_variant(options, tokens, i, json_chunk, &out_data->variants[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_asset(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_asset* out_asset) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "copyright") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->copyright); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "generator") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->generator); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "version") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->version); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "minVersion") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->min_version); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_asset->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_asset->extensions_count, &out_asset->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + if (out_asset->version && CGLTF_ATOF(out_asset->version) < 2) + { + return CGLTF_ERROR_LEGACY; + } + + return i; +} + +cgltf_size cgltf_num_components(cgltf_type type) { + switch (type) + { + case cgltf_type_vec2: + return 2; + case cgltf_type_vec3: + return 3; + case cgltf_type_vec4: + return 4; + case cgltf_type_mat2: + return 4; + case cgltf_type_mat3: + return 9; + case cgltf_type_mat4: + return 16; + case cgltf_type_invalid: + case cgltf_type_scalar: + default: + return 1; + } +} + +cgltf_size cgltf_component_size(cgltf_component_type component_type) { + switch (component_type) + { + case cgltf_component_type_r_8: + case cgltf_component_type_r_8u: + return 1; + case cgltf_component_type_r_16: + case cgltf_component_type_r_16u: + return 2; + case cgltf_component_type_r_32u: + case cgltf_component_type_r_32f: + return 4; + case cgltf_component_type_invalid: + default: + return 0; + } +} + +cgltf_size cgltf_calc_size(cgltf_type type, cgltf_component_type component_type) +{ + cgltf_size component_size = cgltf_component_size(component_type); + if (type == cgltf_type_mat2 && component_size == 1) + { + return 8 * component_size; + } + else if (type == cgltf_type_mat3 && (component_size == 1 || component_size == 2)) + { + return 12 * component_size; + } + return component_size * cgltf_num_components(type); +} + +static int cgltf_fixup_pointers(cgltf_data* out_data); + +static int cgltf_parse_json_root(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "asset") == 0) + { + i = cgltf_parse_json_asset(options, tokens, i + 1, json_chunk, &out_data->asset); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "meshes") == 0) + { + i = cgltf_parse_json_meshes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "accessors") == 0) + { + i = cgltf_parse_json_accessors(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "bufferViews") == 0) + { + i = cgltf_parse_json_buffer_views(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "buffers") == 0) + { + i = cgltf_parse_json_buffers(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "materials") == 0) + { + i = cgltf_parse_json_materials(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "images") == 0) + { + i = cgltf_parse_json_images(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "textures") == 0) + { + i = cgltf_parse_json_textures(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "samplers") == 0) + { + i = cgltf_parse_json_samplers(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "skins") == 0) + { + i = cgltf_parse_json_skins(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "cameras") == 0) + { + i = cgltf_parse_json_cameras(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "nodes") == 0) + { + i = cgltf_parse_json_nodes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scenes") == 0) + { + i = cgltf_parse_json_scenes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scene") == 0) + { + ++i; + out_data->scene = CGLTF_PTRINDEX(cgltf_scene, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "animations") == 0) + { + i = cgltf_parse_json_animations(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_data->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_data->data_extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_data->data_extensions_count = 0; + out_data->data_extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_data->data_extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_lights_punctual") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "lights") == 0) + { + i = cgltf_parse_json_lights(options, tokens, i + 1, json_chunk, out_data); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_variants") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "variants") == 0) + { + i = cgltf_parse_json_variants(options, tokens, i + 1, json_chunk, out_data); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_data->data_extensions[out_data->data_extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensionsUsed") == 0) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_data->extensions_used, &out_data->extensions_used_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensionsRequired") == 0) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_data->extensions_required, &out_data->extensions_required_count); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +cgltf_result cgltf_parse_json(cgltf_options* options, const uint8_t* json_chunk, cgltf_size size, cgltf_data** out_data) +{ + jsmn_parser parser = { 0, 0, 0 }; + + if (options->json_token_count == 0) + { + int token_count = jsmn_parse(&parser, (const char*)json_chunk, size, NULL, 0); + + if (token_count <= 0) + { + return cgltf_result_invalid_json; + } + + options->json_token_count = token_count; + } + + jsmntok_t* tokens = (jsmntok_t*)options->memory.alloc_func(options->memory.user_data, sizeof(jsmntok_t) * (options->json_token_count + 1)); + + if (!tokens) + { + return cgltf_result_out_of_memory; + } + + jsmn_init(&parser); + + int token_count = jsmn_parse(&parser, (const char*)json_chunk, size, tokens, options->json_token_count); + + if (token_count <= 0) + { + options->memory.free_func(options->memory.user_data, tokens); + return cgltf_result_invalid_json; + } + + // this makes sure that we always have an UNDEFINED token at the end of the stream + // for invalid JSON inputs this makes sure we don't perform out of bound reads of token data + tokens[token_count].type = JSMN_UNDEFINED; + + cgltf_data* data = (cgltf_data*)options->memory.alloc_func(options->memory.user_data, sizeof(cgltf_data)); + + if (!data) + { + options->memory.free_func(options->memory.user_data, tokens); + return cgltf_result_out_of_memory; + } + + memset(data, 0, sizeof(cgltf_data)); + data->memory = options->memory; + data->file = options->file; + + int i = cgltf_parse_json_root(options, tokens, 0, json_chunk, data); + + options->memory.free_func(options->memory.user_data, tokens); + + if (i < 0) + { + cgltf_free(data); + + switch (i) + { + case CGLTF_ERROR_NOMEM: return cgltf_result_out_of_memory; + case CGLTF_ERROR_LEGACY: return cgltf_result_legacy_gltf; + default: return cgltf_result_invalid_gltf; + } + } + + if (cgltf_fixup_pointers(data) < 0) + { + cgltf_free(data); + return cgltf_result_invalid_gltf; + } + + data->json = (const char*)json_chunk; + data->json_size = size; + + *out_data = data; + + return cgltf_result_success; +} + +static int cgltf_fixup_pointers(cgltf_data* data) +{ + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + CGLTF_PTRFIXUP(data->meshes[i].primitives[j].indices, data->accessors, data->accessors_count); + CGLTF_PTRFIXUP(data->meshes[i].primitives[j].material, data->materials, data->materials_count); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].attributes[k].data, data->accessors, data->accessors_count); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].targets[k].attributes[m].data, data->accessors, data->accessors_count); + } + } + + if (data->meshes[i].primitives[j].has_draco_mesh_compression) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].draco_mesh_compression.buffer_view, data->buffer_views, data->buffer_views_count); + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].draco_mesh_compression.attributes_count; ++m) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].draco_mesh_compression.attributes[m].data, data->accessors, data->accessors_count); + } + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].mappings[k].material, data->materials, data->materials_count); + } + } + } + + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + CGLTF_PTRFIXUP(data->accessors[i].buffer_view, data->buffer_views, data->buffer_views_count); + + if (data->accessors[i].is_sparse) + { + CGLTF_PTRFIXUP_REQ(data->accessors[i].sparse.indices_buffer_view, data->buffer_views, data->buffer_views_count); + CGLTF_PTRFIXUP_REQ(data->accessors[i].sparse.values_buffer_view, data->buffer_views, data->buffer_views_count); + } + + if (data->accessors[i].buffer_view) + { + data->accessors[i].stride = data->accessors[i].buffer_view->stride; + } + + if (data->accessors[i].stride == 0) + { + data->accessors[i].stride = cgltf_calc_size(data->accessors[i].type, data->accessors[i].component_type); + } + } + + for (cgltf_size i = 0; i < data->textures_count; ++i) + { + CGLTF_PTRFIXUP(data->textures[i].image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].basisu_image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].webp_image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].sampler, data->samplers, data->samplers_count); + } + + for (cgltf_size i = 0; i < data->images_count; ++i) + { + CGLTF_PTRFIXUP(data->images[i].buffer_view, data->buffer_views, data->buffer_views_count); + } + + for (cgltf_size i = 0; i < data->materials_count; ++i) + { + CGLTF_PTRFIXUP(data->materials[i].normal_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].emissive_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].occlusion_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].pbr_metallic_roughness.base_color_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].pbr_metallic_roughness.metallic_roughness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].pbr_specular_glossiness.diffuse_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].pbr_specular_glossiness.specular_glossiness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_roughness_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_normal_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].specular.specular_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].specular.specular_color_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].transmission.transmission_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].volume.thickness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].sheen.sheen_color_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].sheen.sheen_roughness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].iridescence.iridescence_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].iridescence.iridescence_thickness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].diffuse_transmission.diffuse_transmission_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].diffuse_transmission.diffuse_transmission_color_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].anisotropy.anisotropy_texture.texture, data->textures, data->textures_count); + } + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + CGLTF_PTRFIXUP_REQ(data->buffer_views[i].buffer, data->buffers, data->buffers_count); + + if (data->buffer_views[i].has_meshopt_compression) + { + CGLTF_PTRFIXUP_REQ(data->buffer_views[i].meshopt_compression.buffer, data->buffers, data->buffers_count); + } + } + + for (cgltf_size i = 0; i < data->skins_count; ++i) + { + for (cgltf_size j = 0; j < data->skins[i].joints_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->skins[i].joints[j], data->nodes, data->nodes_count); + } + + CGLTF_PTRFIXUP(data->skins[i].skeleton, data->nodes, data->nodes_count); + CGLTF_PTRFIXUP(data->skins[i].inverse_bind_matrices, data->accessors, data->accessors_count); + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + for (cgltf_size j = 0; j < data->nodes[i].children_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->nodes[i].children[j], data->nodes, data->nodes_count); + + if (data->nodes[i].children[j]->parent) + { + return CGLTF_ERROR_JSON; + } + + data->nodes[i].children[j]->parent = &data->nodes[i]; + } + + CGLTF_PTRFIXUP(data->nodes[i].mesh, data->meshes, data->meshes_count); + CGLTF_PTRFIXUP(data->nodes[i].skin, data->skins, data->skins_count); + CGLTF_PTRFIXUP(data->nodes[i].camera, data->cameras, data->cameras_count); + CGLTF_PTRFIXUP(data->nodes[i].light, data->lights, data->lights_count); + + if (data->nodes[i].has_mesh_gpu_instancing) + { + for (cgltf_size m = 0; m < data->nodes[i].mesh_gpu_instancing.attributes_count; ++m) + { + CGLTF_PTRFIXUP_REQ(data->nodes[i].mesh_gpu_instancing.attributes[m].data, data->accessors, data->accessors_count); + } + } + } + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + for (cgltf_size j = 0; j < data->scenes[i].nodes_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->scenes[i].nodes[j], data->nodes, data->nodes_count); + + if (data->scenes[i].nodes[j]->parent) + { + return CGLTF_ERROR_JSON; + } + } + } + + CGLTF_PTRFIXUP(data->scene, data->scenes, data->scenes_count); + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + for (cgltf_size j = 0; j < data->animations[i].samplers_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->animations[i].samplers[j].input, data->accessors, data->accessors_count); + CGLTF_PTRFIXUP_REQ(data->animations[i].samplers[j].output, data->accessors, data->accessors_count); + } + + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->animations[i].channels[j].sampler, data->animations[i].samplers, data->animations[i].samplers_count); + CGLTF_PTRFIXUP(data->animations[i].channels[j].target_node, data->nodes, data->nodes_count); + } + } + + return 0; +} + +/* + * -- jsmn.c start -- + * Source: https://github.com/zserge/jsmn + * License: MIT + * + * Copyright (c) 2010 Serge A. Zaitsev + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Allocates a fresh unused token from the token pull. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, + ptrdiff_t start, ptrdiff_t end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + ptrdiff_t start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t' : case '\r' : case '\n' : case ' ' : + case ',' : case ']' : case '}' : + goto found; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + + ptrdiff_t start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': case '/' : case '\\' : case 'b' : + case 'f' : case 'r' : case 'n' : case 't' : + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + /* If it isn't a hex character we have an error */ + if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +static int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, size_t num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + return JSMN_ERROR_NOMEM; + if (parser->toksuper != -1) { + tokens[parser->toksuper].size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': case ']': + if (tokens == NULL) + break; + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + if(token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) return JSMN_ERROR_INVAL; + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + case '\t' : case '\r' : case '\n' : case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': case '0': case '1' : case '2': case '3' : case '4': + case '5': case '6': case '7' : case '8': case '9': + case 't': case 'f': case 'n' : + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +static void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} +/* + * -- jsmn.c end -- + */ + +#endif /* #ifdef CGLTF_IMPLEMENTATION */ + +/* cgltf is distributed under MIT license: + * + * Copyright (c) 2018-2021 Johannes Kuhlmann + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ diff --git a/src/3d/cgltf_impl.cpp b/src/3d/cgltf_impl.cpp new file mode 100644 index 0000000..a205e5a --- /dev/null +++ b/src/3d/cgltf_impl.cpp @@ -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" diff --git a/src/3d/shaders/ps1_skinned_vertex.glsl b/src/3d/shaders/ps1_skinned_vertex.glsl new file mode 100644 index 0000000..406de9e --- /dev/null +++ b/src/3d/shaders/ps1_skinned_vertex.glsl @@ -0,0 +1,108 @@ +// PS1-style skinned vertex shader for OpenGL 3.2+ +// Implements skeletal animation, vertex snapping, Gouraud shading, and fog + +#version 150 core + +// Uniforms - transform matrices +uniform mat4 u_model; +uniform mat4 u_view; +uniform mat4 u_projection; + +// Uniforms - skeletal animation (max 64 bones) +uniform mat4 u_bones[64]; + +// Uniforms - PS1 effects +uniform vec2 u_resolution; // Internal render resolution for vertex snapping +uniform bool u_enable_snap; // Enable vertex snapping to pixel grid +uniform float u_fog_start; // Fog start distance +uniform float u_fog_end; // Fog end distance + +// Uniforms - lighting +uniform vec3 u_light_dir; // Directional light direction (normalized) +uniform vec3 u_ambient; // Ambient light color + +// Attributes +in vec3 a_position; +in vec2 a_texcoord; +in vec3 a_normal; +in vec4 a_color; +in vec4 a_bone_ids; // Up to 4 bone indices (as float for compatibility) +in vec4 a_bone_weights; // Corresponding weights + +// Varyings - passed to fragment shader +out vec4 v_color; // Gouraud-shaded vertex color +noperspective out vec2 v_texcoord; // Texture coordinates (affine interpolation!) +out float v_fog; // Fog factor (0 = no fog, 1 = full fog) + +void main() { + // ========================================================================= + // Skeletal Animation: Vertex Skinning + // Transform vertex and normal by weighted bone matrices + // ========================================================================= + ivec4 bone_ids = ivec4(a_bone_ids); // Convert to integer indices + + // Compute skinned position and normal + mat4 skin_matrix = + u_bones[bone_ids.x] * a_bone_weights.x + + u_bones[bone_ids.y] * a_bone_weights.y + + u_bones[bone_ids.z] * a_bone_weights.z + + u_bones[bone_ids.w] * a_bone_weights.w; + + vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0); + vec3 skinned_normal = mat3(skin_matrix) * a_normal; + + // Transform vertex to clip space + vec4 worldPos = u_model * skinned_pos; + vec4 viewPos = u_view * worldPos; + vec4 clipPos = u_projection * viewPos; + + // ========================================================================= + // PS1 Effect: Vertex Snapping + // The PS1 had limited precision for vertex positions, causing vertices + // to "snap" to a grid, creating the characteristic jittery look. + // ========================================================================= + if (u_enable_snap) { + // Convert to NDC + vec4 ndc = clipPos; + ndc.xyz /= ndc.w; + + // Snap to pixel grid based on render resolution + vec2 grid = u_resolution * 0.5; + ndc.xy = floor(ndc.xy * grid + 0.5) / grid; + + // Convert back to clip space + ndc.xyz *= clipPos.w; + clipPos = ndc; + } + + gl_Position = clipPos; + + // ========================================================================= + // PS1 Effect: Gouraud Shading + // Per-vertex lighting was used on PS1 due to hardware limitations. + // This creates characteristic flat-shaded polygons. + // ========================================================================= + vec3 worldNormal = mat3(u_model) * skinned_normal; + worldNormal = normalize(worldNormal); + + // Simple directional light + ambient + float diffuse = max(dot(worldNormal, -u_light_dir), 0.0); + vec3 lighting = u_ambient + vec3(diffuse); + + // Apply lighting to vertex color + v_color = vec4(a_color.rgb * lighting, a_color.a); + + // ========================================================================= + // PS1 Effect: Affine Texture Mapping + // Using 'noperspective' qualifier disables perspective-correct interpolation + // This creates the characteristic texture warping on large polygons + // ========================================================================= + v_texcoord = a_texcoord; + + // ========================================================================= + // Fog Distance Calculation + // Calculate linear fog factor based on view-space depth + // ========================================================================= + float depth = -viewPos.z; // View space depth (positive) + v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0); +} diff --git a/src/3d/shaders/ps1_skinned_vertex_es2.glsl b/src/3d/shaders/ps1_skinned_vertex_es2.glsl new file mode 100644 index 0000000..498b924 --- /dev/null +++ b/src/3d/shaders/ps1_skinned_vertex_es2.glsl @@ -0,0 +1,195 @@ +// PS1-style skinned vertex shader for OpenGL ES 2.0 / WebGL 1.0 +// Implements skeletal animation, vertex snapping, Gouraud shading, and fog + +precision mediump float; + +// Uniforms - transform matrices +uniform mat4 u_model; +uniform mat4 u_view; +uniform mat4 u_projection; + +// Uniforms - skeletal animation (max 64 bones) +// GLES2 doesn't guarantee support for arrays > 128 vec4s in vertex shaders +// 64 bones * 4 vec4s = 256 vec4s, so we use 32 bones for safety +uniform mat4 u_bones[32]; + +// Uniforms - PS1 effects +uniform vec2 u_resolution; // Internal render resolution for vertex snapping +uniform bool u_enable_snap; // Enable vertex snapping to pixel grid +uniform float u_fog_start; // Fog start distance +uniform float u_fog_end; // Fog end distance + +// Uniforms - lighting +uniform vec3 u_light_dir; // Directional light direction (normalized) +uniform vec3 u_ambient; // Ambient light color + +// Attributes +attribute vec3 a_position; +attribute vec2 a_texcoord; +attribute vec3 a_normal; +attribute vec4 a_color; +attribute vec4 a_bone_ids; // Up to 4 bone indices (as floats) +attribute vec4 a_bone_weights; // Corresponding weights + +// Varyings - passed to fragment shader +varying vec4 v_color; // Gouraud-shaded vertex color +varying vec2 v_texcoord; // Texture coordinates (multiplied by w for affine trick) +varying float v_w; // Clip space w for affine mapping restoration +varying float v_fog; // Fog factor (0 = no fog, 1 = full fog) + +// Helper to get bone matrix by index (GLES2 doesn't support dynamic array indexing well) +mat4 getBoneMatrix(int index) { + // GLES2 workaround: use if-chain for dynamic indexing + if (index < 8) { + if (index < 4) { + if (index < 2) { + if (index == 0) return u_bones[0]; + else return u_bones[1]; + } else { + if (index == 2) return u_bones[2]; + else return u_bones[3]; + } + } else { + if (index < 6) { + if (index == 4) return u_bones[4]; + else return u_bones[5]; + } else { + if (index == 6) return u_bones[6]; + else return u_bones[7]; + } + } + } else if (index < 16) { + if (index < 12) { + if (index < 10) { + if (index == 8) return u_bones[8]; + else return u_bones[9]; + } else { + if (index == 10) return u_bones[10]; + else return u_bones[11]; + } + } else { + if (index < 14) { + if (index == 12) return u_bones[12]; + else return u_bones[13]; + } else { + if (index == 14) return u_bones[14]; + else return u_bones[15]; + } + } + } else if (index < 24) { + if (index < 20) { + if (index < 18) { + if (index == 16) return u_bones[16]; + else return u_bones[17]; + } else { + if (index == 18) return u_bones[18]; + else return u_bones[19]; + } + } else { + if (index < 22) { + if (index == 20) return u_bones[20]; + else return u_bones[21]; + } else { + if (index == 22) return u_bones[22]; + else return u_bones[23]; + } + } + } else { + if (index < 28) { + if (index < 26) { + if (index == 24) return u_bones[24]; + else return u_bones[25]; + } else { + if (index == 26) return u_bones[26]; + else return u_bones[27]; + } + } else { + if (index < 30) { + if (index == 28) return u_bones[28]; + else return u_bones[29]; + } else { + if (index == 30) return u_bones[30]; + else return u_bones[31]; + } + } + } + return mat4(1.0); // Identity fallback +} + +void main() { + // ========================================================================= + // Skeletal Animation: Vertex Skinning + // Transform vertex and normal by weighted bone matrices + // ========================================================================= + int b0 = int(a_bone_ids.x); + int b1 = int(a_bone_ids.y); + int b2 = int(a_bone_ids.z); + int b3 = int(a_bone_ids.w); + + // Compute skinned position and normal + mat4 skin_matrix = + getBoneMatrix(b0) * a_bone_weights.x + + getBoneMatrix(b1) * a_bone_weights.y + + getBoneMatrix(b2) * a_bone_weights.z + + getBoneMatrix(b3) * a_bone_weights.w; + + vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0); + vec3 skinned_normal = mat3(skin_matrix[0].xyz, skin_matrix[1].xyz, skin_matrix[2].xyz) * a_normal; + + // Transform vertex to clip space + vec4 worldPos = u_model * skinned_pos; + vec4 viewPos = u_view * worldPos; + vec4 clipPos = u_projection * viewPos; + + // ========================================================================= + // PS1 Effect: Vertex Snapping + // The PS1 had limited precision for vertex positions, causing vertices + // to "snap" to a grid, creating the characteristic jittery look. + // ========================================================================= + if (u_enable_snap) { + // Convert to NDC + vec4 ndc = clipPos; + ndc.xyz /= ndc.w; + + // Snap to pixel grid based on render resolution + vec2 grid = u_resolution * 0.5; + ndc.xy = floor(ndc.xy * grid + 0.5) / grid; + + // Convert back to clip space + ndc.xyz *= clipPos.w; + clipPos = ndc; + } + + gl_Position = clipPos; + + // ========================================================================= + // PS1 Effect: Gouraud Shading + // Per-vertex lighting was used on PS1 due to hardware limitations. + // This creates characteristic flat-shaded polygons. + // ========================================================================= + vec3 worldNormal = mat3(u_model[0].xyz, u_model[1].xyz, u_model[2].xyz) * skinned_normal; + worldNormal = normalize(worldNormal); + + // Simple directional light + ambient + float diffuse = max(dot(worldNormal, -u_light_dir), 0.0); + vec3 lighting = u_ambient + vec3(diffuse); + + // Apply lighting to vertex color + v_color = vec4(a_color.rgb * lighting, a_color.a); + + // ========================================================================= + // PS1 Effect: Affine Texture Mapping Trick + // GLES2 doesn't have 'noperspective' interpolation, so we manually + // multiply texcoords by w here and divide by w in fragment shader. + // This creates the characteristic texture warping on large polygons. + // ========================================================================= + v_texcoord = a_texcoord * clipPos.w; + v_w = clipPos.w; + + // ========================================================================= + // Fog Distance Calculation + // Calculate linear fog factor based on view-space depth + // ========================================================================= + float depth = -viewPos.z; // View space depth (positive) + v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0); +} diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index a5940dc..ec4ffe1 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -34,6 +34,9 @@ #include "3d/Viewport3D.h" // 3D rendering viewport #include "3d/Entity3D.h" // 3D game entities #include "3d/EntityCollection3D.h" // Entity3D collection +#include "3d/Model3D.h" // 3D model resource +#include "3d/Billboard.h" // Billboard sprites +#include "3d/PyVoxelGrid.h" // Voxel grid for 3D structures (Milestone 9) #include "McRogueFaceVersion.h" #include "GameEngine.h" // ImGui is only available for SFML builds @@ -439,7 +442,9 @@ PyObject* PyInit_mcrfpy() /*3D entities*/ &mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType, - &mcrfpydef::PyEntityCollection3DIterType, + &mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType, + &mcrfpydef::PyBillboardType, &mcrfpydef::PyVoxelGridType, + &mcrfpydef::PyVoxelRegionType, /*grid layers (#147)*/ &PyColorLayerType, &PyTileLayerType, @@ -536,6 +541,13 @@ PyObject* PyInit_mcrfpy() mcrfpydef::PyNoiseSourceType.tp_methods = PyNoiseSource::methods; mcrfpydef::PyNoiseSourceType.tp_getset = PyNoiseSource::getsetters; + // Set up PyVoxelGridType methods and getsetters (Milestone 9) + mcrfpydef::PyVoxelGridType.tp_methods = PyVoxelGrid::methods; + mcrfpydef::PyVoxelGridType.tp_getset = PyVoxelGrid::getsetters; + + // Set up PyVoxelRegionType getsetters (Milestone 11) + mcrfpydef::PyVoxelRegionType.tp_getset = PyVoxelRegion::getsetters; + // Set up PyShaderType methods and getsetters (#106) mcrfpydef::PyShaderType.tp_methods = PyShader::methods; mcrfpydef::PyShaderType.tp_getset = PyShader::getsetters; @@ -559,6 +571,8 @@ PyObject* PyInit_mcrfpy() PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist); PyViewport3DType.tp_weaklistoffset = offsetof(PyViewport3DObject, weakreflist); mcrfpydef::PyEntity3DType.tp_weaklistoffset = offsetof(PyEntity3DObject, weakreflist); + mcrfpydef::PyModel3DType.tp_weaklistoffset = offsetof(PyModel3DObject, weakreflist); + mcrfpydef::PyBillboardType.tp_weaklistoffset = offsetof(PyBillboardObject, weakreflist); // #219 - Initialize PyLock context manager type if (PyLock::init() < 0) { diff --git a/src/PyColor.cpp b/src/PyColor.cpp index 9e352af..7bc8b10 100644 --- a/src/PyColor.cpp +++ b/src/PyColor.cpp @@ -68,8 +68,68 @@ PyObject* PyColor::pyObject() sf::Color PyColor::fromPy(PyObject* obj) { - PyColorObject* self = (PyColorObject*)obj; - return self->data; + // Handle None or NULL + if (!obj || obj == Py_None) { + return sf::Color::White; + } + + // Check if it's already a Color object + PyTypeObject* color_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"); + if (color_type) { + bool is_color = PyObject_TypeCheck(obj, color_type); + Py_DECREF(color_type); + if (is_color) { + PyColorObject* self = (PyColorObject*)obj; + return self->data; + } + } + + // Handle tuple or list input + if (PyTuple_Check(obj) || PyList_Check(obj)) { + Py_ssize_t size = PySequence_Size(obj); + if (size < 3 || size > 4) { + PyErr_SetString(PyExc_TypeError, "Color tuple/list must have 3 or 4 elements (r, g, b[, a])"); + return sf::Color::White; + } + + int r = 255, g = 255, b = 255, a = 255; + + PyObject* item0 = PySequence_GetItem(obj, 0); + PyObject* item1 = PySequence_GetItem(obj, 1); + PyObject* item2 = PySequence_GetItem(obj, 2); + + if (PyLong_Check(item0)) r = (int)PyLong_AsLong(item0); + if (PyLong_Check(item1)) g = (int)PyLong_AsLong(item1); + if (PyLong_Check(item2)) b = (int)PyLong_AsLong(item2); + + Py_DECREF(item0); + Py_DECREF(item1); + Py_DECREF(item2); + + if (size == 4) { + PyObject* item3 = PySequence_GetItem(obj, 3); + if (PyLong_Check(item3)) a = (int)PyLong_AsLong(item3); + Py_DECREF(item3); + } + + // Clamp values + r = std::max(0, std::min(255, r)); + g = std::max(0, std::min(255, g)); + b = std::max(0, std::min(255, b)); + a = std::max(0, std::min(255, a)); + + return sf::Color(r, g, b, a); + } + + // Handle integer (grayscale) + if (PyLong_Check(obj)) { + int v = std::max(0, std::min(255, (int)PyLong_AsLong(obj))); + return sf::Color(v, v, v, 255); + } + + // Unknown type - set error and return white + PyErr_SetString(PyExc_TypeError, "Color must be a Color object, tuple, list, or integer"); + return sf::Color::White; } sf::Color PyColor::fromPy(PyColorObject* self) diff --git a/src/PyTexture.cpp b/src/PyTexture.cpp index 2bea741..7f4d256 100644 --- a/src/PyTexture.cpp +++ b/src/PyTexture.cpp @@ -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(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; } diff --git a/src/PyTexture.h b/src/PyTexture.h index b2375c8..fa5befb 100644 --- a/src/PyTexture.h +++ b/src/PyTexture.h @@ -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*); diff --git a/tests/demo/screens/billboard_building_demo.py b/tests/demo/screens/billboard_building_demo.py new file mode 100644 index 0000000..8b74efd --- /dev/null +++ b/tests/demo/screens/billboard_building_demo.py @@ -0,0 +1,314 @@ +# billboard_building_demo.py - Visual demo of Billboard and Mesh Instances +# Demonstrates camera-facing sprites and static mesh placement + +import mcrfpy +import sys +import math + +# Create demo scene +scene = mcrfpy.Scene("billboard_building_demo") + +# Dark background frame +bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25)) +scene.children.append(bg) + +# Title +title = mcrfpy.Caption(text="Billboard & Building Demo - 3D Sprites and Static Meshes", pos=(20, 10)) +title.fill_color = mcrfpy.Color(255, 255, 255) +scene.children.append(title) + +# Create the 3D viewport +viewport = mcrfpy.Viewport3D( + pos=(50, 60), + size=(600, 450), + render_resolution=(320, 240), # PS1 resolution + fov=60.0, + camera_pos=(16.0, 10.0, 20.0), + camera_target=(8.0, 0.0, 8.0), + bg_color=mcrfpy.Color(80, 120, 180) # Sky blue background +) +scene.children.append(viewport) + +# Set up the navigation grid +GRID_SIZE = 16 +viewport.set_grid_size(GRID_SIZE, GRID_SIZE) + +# Generate terrain +print("Generating terrain...") +hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) +hm.mid_point_displacement(0.2, seed=456) # Gentle terrain +hm.normalize(0.0, 0.5) # Keep it low for placing objects + +# Apply heightmap +viewport.apply_heightmap(hm, 2.0) + +# Build terrain mesh +vertex_count = viewport.build_terrain( + layer_name="terrain", + heightmap=hm, + y_scale=2.0, + cell_size=1.0 +) +print(f"Terrain built with {vertex_count} vertices") + +# Create terrain colors (earthy tones) +r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) +g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) +b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) + +for y in range(GRID_SIZE): + for x in range(GRID_SIZE): + h = hm[x, y] + # Earth/grass colors + r_map[x, y] = 0.25 + h * 0.2 + g_map[x, y] = 0.35 + h * 0.25 + b_map[x, y] = 0.15 + h * 0.1 + +viewport.apply_terrain_colors("terrain", r_map, g_map, b_map) + +# ============================================================================= +# PART 1: Building Placement using Mesh Instances +# ============================================================================= +print("Placing buildings...") + +# Add a layer for buildings +viewport.add_layer("buildings", z_index=1) + +# Create a simple building model (cube-like structure) +building_model = mcrfpy.Model3D() + +# Place several buildings at different locations with transforms +building_positions = [ + ((2, 0, 2), 0, 1.5), # Position, rotation, scale + ((12, 0, 2), 45, 1.2), + ((4, 0, 12), 90, 1.0), + ((10, 0, 10), 30, 1.8), +] + +for pos, rotation, scale in building_positions: + idx = viewport.add_mesh("buildings", building_model, pos=pos, rotation=rotation, scale=scale) + print(f" Placed building {idx} at {pos}") + + # Mark the footprint as blocking + gx, gz = int(pos[0]), int(pos[2]) + footprint_size = max(1, int(scale)) + viewport.place_blocking(grid_pos=(gx, gz), footprint=(footprint_size, footprint_size)) + +print(f"Placed {len(building_positions)} buildings") + +# ============================================================================= +# PART 2: Billboard Sprites (camera-facing) +# ============================================================================= +print("Creating billboards...") + +# Create billboards for "trees" - camera_y mode (stays upright) +tree_positions = [ + (3, 0, 5), (5, 0, 3), (6, 0, 8), (9, 0, 5), + (11, 0, 7), (7, 0, 11), (13, 0, 13), (1, 0, 9) +] + +# Note: Without actual textures, billboards will render as simple quads +# In a real game, you'd load a tree sprite texture +for i, pos in enumerate(tree_positions): + bb = mcrfpy.Billboard( + pos=pos, + scale=1.5, + facing="camera_y", # Stays upright, only rotates on Y axis + opacity=1.0 + ) + viewport.add_billboard(bb) + +print(f" Created {len(tree_positions)} tree billboards (camera_y facing)") + +# Create some particle-like billboards - full camera facing +particle_positions = [ + (8, 3, 8), (8.5, 3.5, 8.2), (7.5, 3.2, 7.8), # Floating particles +] + +for i, pos in enumerate(particle_positions): + bb = mcrfpy.Billboard( + pos=pos, + scale=0.3, + facing="camera", # Full rotation to face camera + opacity=0.7 + ) + viewport.add_billboard(bb) + +print(f" Created {len(particle_positions)} particle billboards (camera facing)") + +# Create a fixed-orientation billboard (signpost) +signpost = mcrfpy.Billboard( + pos=(5, 1.5, 5), + scale=1.0, + facing="fixed", # Manual orientation +) +signpost.theta = math.pi / 4 # 45 degrees horizontal +signpost.phi = 0.0 # No vertical tilt +viewport.add_billboard(signpost) + +print(f" Created 1 signpost billboard (fixed facing)") +print(f"Total billboards: {viewport.billboard_count()}") + +# ============================================================================= +# Info Panel +# ============================================================================= +info_panel = mcrfpy.Frame(pos=(670, 60), size=(330, 450), + fill_color=mcrfpy.Color(30, 30, 40), + outline_color=mcrfpy.Color(80, 80, 100), + outline=2.0) +scene.children.append(info_panel) + +# Panel title +panel_title = mcrfpy.Caption(text="Billboard & Mesh Demo", pos=(690, 70)) +panel_title.fill_color = mcrfpy.Color(200, 200, 255) +scene.children.append(panel_title) + +# Billboard info +bb_info = [ + ("", ""), + ("Billboard Modes:", ""), + (" camera", "Full rotation to face camera"), + (" camera_y", "Y-axis only (stays upright)"), + (" fixed", "Manual theta/phi angles"), + ("", ""), + (f"Trees:", f"{len(tree_positions)} (camera_y)"), + (f"Particles:", f"{len(particle_positions)} (camera)"), + (f"Signpost:", "1 (fixed)"), + ("", ""), + ("Mesh Instances:", ""), + (f" Buildings:", f"{len(building_positions)}"), +] + +y_offset = 100 +for label, value in bb_info: + if label or value: + text = f"{label} {value}" if value else label + cap = mcrfpy.Caption(text=text, pos=(690, y_offset)) + cap.fill_color = mcrfpy.Color(150, 150, 170) + scene.children.append(cap) + y_offset += 22 + +# Dynamic camera info +camera_label = mcrfpy.Caption(text="Camera: Following...", pos=(690, y_offset + 20)) +camera_label.fill_color = mcrfpy.Color(180, 180, 200) +scene.children.append(camera_label) + +# Instructions at bottom +instructions = mcrfpy.Caption( + text="[Space] Toggle orbit | [1-3] Change billboard mode | [C] Clear buildings | [ESC] Quit", + pos=(20, 530) +) +instructions.fill_color = mcrfpy.Color(150, 150, 150) +scene.children.append(instructions) + +# Status line +status = mcrfpy.Caption(text="Status: Billboard & Building demo loaded", pos=(20, 555)) +status.fill_color = mcrfpy.Color(100, 200, 100) +scene.children.append(status) + +# Animation state +animation_time = [0.0] +camera_orbit = [True] + +# Update function +def update(timer, runtime): + animation_time[0] += runtime / 1000.0 + + # Camera orbit + if camera_orbit[0]: + angle = animation_time[0] * 0.3 + radius = 18.0 + center_x = 8.0 + center_z = 8.0 + height = 10.0 + math.sin(animation_time[0] * 0.2) * 2.0 + + x = center_x + math.cos(angle) * radius + z = center_z + math.sin(angle) * radius + + viewport.camera_pos = (x, height, z) + viewport.camera_target = (center_x, 1.0, center_z) + + camera_label.text = f"Camera: Orbit ({x:.1f}, {height:.1f}, {z:.1f})" + + # Animate particle billboards (bobbing up and down) + bb_count = viewport.billboard_count() + if bb_count > len(tree_positions): + particle_start = len(tree_positions) + for i in range(particle_start, particle_start + len(particle_positions)): + if i < bb_count: + bb = viewport.get_billboard(i) + pos = bb.pos + new_y = 3.0 + math.sin(animation_time[0] * 2.0 + i * 0.5) * 0.5 + bb.pos = (pos[0], new_y, pos[2]) + +# Key handler +def on_key(key, state): + if state != mcrfpy.InputState.PRESSED: + return + + if key == mcrfpy.Key.SPACE: + camera_orbit[0] = not camera_orbit[0] + status.text = f"Camera orbit: {'ON' if camera_orbit[0] else 'OFF'}" + + elif key == mcrfpy.Key.NUM_1: + # Change all tree billboards to "camera" mode + for i in range(len(tree_positions)): + viewport.get_billboard(i).facing = "camera" + status.text = "Trees now use 'camera' facing (full rotation)" + + elif key == mcrfpy.Key.NUM_2: + # Change all tree billboards to "camera_y" mode + for i in range(len(tree_positions)): + viewport.get_billboard(i).facing = "camera_y" + status.text = "Trees now use 'camera_y' facing (upright)" + + elif key == mcrfpy.Key.NUM_3: + # Change all tree billboards to "fixed" mode + for i in range(len(tree_positions)): + bb = viewport.get_billboard(i) + bb.facing = "fixed" + bb.theta = i * 0.5 # Different angles + status.text = "Trees now use 'fixed' facing (manual angles)" + + elif key == mcrfpy.Key.C: + viewport.clear_meshes("buildings") + status.text = "Cleared all buildings from layer" + + elif key == mcrfpy.Key.O: + # Adjust tree opacity + for i in range(len(tree_positions)): + bb = viewport.get_billboard(i) + bb.opacity = 0.5 if bb.opacity > 0.7 else 1.0 + status.text = f"Tree opacity toggled" + + elif key == mcrfpy.Key.V: + # Toggle tree visibility + for i in range(len(tree_positions)): + bb = viewport.get_billboard(i) + bb.visible = not bb.visible + status.text = f"Tree visibility toggled" + + elif key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + +# Set up scene +scene.on_key = on_key + +# Create timer for updates +timer = mcrfpy.Timer("billboard_update", update, 16) # ~60fps + +# Activate scene +mcrfpy.current_scene = scene + +print() +print("Billboard & Building Demo loaded!") +print() +print("Controls:") +print(" [Space] Toggle camera orbit") +print(" [1] Trees -> 'camera' facing") +print(" [2] Trees -> 'camera_y' facing (default)") +print(" [3] Trees -> 'fixed' facing") +print(" [O] Toggle tree opacity") +print(" [V] Toggle tree visibility") +print(" [C] Clear buildings") +print(" [ESC] Quit") diff --git a/tests/demo/screens/integration_demo.py b/tests/demo/screens/integration_demo.py new file mode 100644 index 0000000..9d3b1ce --- /dev/null +++ b/tests/demo/screens/integration_demo.py @@ -0,0 +1,462 @@ +# integration_demo.py - Milestone 8 Integration Demo +# Showcases all 3D features: terrain, entities, pathfinding, FOV, billboards, UI, input + +import mcrfpy +import math +import random + +DEMO_NAME = "3D Integration Demo" +DEMO_DESCRIPTION = """Complete 3D demo with terrain, player, NPC, FOV, and UI overlay. + +Controls: + Arrow keys: Move player + Click: Move to clicked position + ESC: Quit +""" + +# Create the main scene +scene = mcrfpy.Scene("integration_demo") + +# ============================================================================= +# Constants +# ============================================================================= +GRID_WIDTH = 32 +GRID_DEPTH = 32 +CELL_SIZE = 1.0 +TERRAIN_Y_SCALE = 3.0 +FOV_RADIUS = 10 + +# ============================================================================= +# 3D Viewport +# ============================================================================= +viewport = mcrfpy.Viewport3D( + pos=(10, 10), + size=(700, 550), + render_resolution=(350, 275), + fov=60.0, + camera_pos=(16.0, 15.0, 25.0), + camera_target=(16.0, 0.0, 16.0), + bg_color=mcrfpy.Color(40, 60, 100) +) +viewport.enable_fog = True +viewport.fog_near = 10.0 +viewport.fog_far = 40.0 +viewport.fog_color = mcrfpy.Color(40, 60, 100) +scene.children.append(viewport) + +# Set up navigation grid +viewport.set_grid_size(GRID_WIDTH, GRID_DEPTH) + +# ============================================================================= +# Terrain Generation +# ============================================================================= +print("Generating terrain...") + +# Create heightmap with hills +hm = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) +hm.mid_point_displacement(roughness=0.5) +hm.normalize(0.0, 1.0) + +# Build terrain mesh +viewport.build_terrain( + layer_name="terrain", + heightmap=hm, + y_scale=TERRAIN_Y_SCALE, + cell_size=CELL_SIZE +) + +# Apply heightmap to navigation grid +viewport.apply_heightmap(hm, TERRAIN_Y_SCALE) + +# Mark steep slopes and water as unwalkable +viewport.apply_threshold(hm, 0.0, 0.12, False) # Low areas = water (unwalkable) +viewport.set_slope_cost(0.4, 2.0) + +# Create base terrain colors (green/brown based on height) +r_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) +g_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) +b_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) + +# Storage for base colors (for FOV dimming) +base_colors = [] + +for z in range(GRID_DEPTH): + row = [] + for x in range(GRID_WIDTH): + h = hm[x, z] + if h < 0.12: # Water + r, g, b = 0.1, 0.2, 0.4 + elif h < 0.25: # Sand/beach + r, g, b = 0.6, 0.5, 0.3 + elif h < 0.6: # Grass + r, g, b = 0.2 + random.random() * 0.1, 0.4 + random.random() * 0.15, 0.15 + else: # Rock/mountain + r, g, b = 0.4, 0.35, 0.3 + + r_map[x, z] = r + g_map[x, z] = g + b_map[x, z] = b + row.append((r, g, b)) + base_colors.append(row) + +viewport.apply_terrain_colors("terrain", r_map, g_map, b_map) + +# ============================================================================= +# Find walkable starting positions +# ============================================================================= +def find_walkable_pos(): + """Find a random walkable position""" + for _ in range(100): + x = random.randint(2, GRID_WIDTH - 3) + z = random.randint(2, GRID_DEPTH - 3) + cell = viewport.at(x, z) + if cell.walkable: + return (x, z) + return (GRID_WIDTH // 2, GRID_DEPTH // 2) + +# ============================================================================= +# Player Entity +# ============================================================================= +player_start = find_walkable_pos() +player = mcrfpy.Entity3D(pos=player_start, scale=0.8, color=mcrfpy.Color(50, 150, 255)) +viewport.entities.append(player) +print(f"Player at {player_start}") + +# Track discovered cells +discovered = set() +discovered.add(player_start) + +# ============================================================================= +# NPC Entity with Patrol AI +# ============================================================================= +npc_start = find_walkable_pos() +while abs(npc_start[0] - player_start[0]) < 5 and abs(npc_start[1] - player_start[1]) < 5: + npc_start = find_walkable_pos() + +npc = mcrfpy.Entity3D(pos=npc_start, scale=0.7, color=mcrfpy.Color(255, 100, 100)) +viewport.entities.append(npc) +print(f"NPC at {npc_start}") + +# NPC patrol system +class NPCController: + def __init__(self, entity, waypoints): + self.entity = entity + self.waypoints = waypoints + self.current_wp = 0 + self.path = [] + self.path_index = 0 + + def update(self): + if self.entity.is_moving: + return + + # If we have a path, follow it + if self.path_index < len(self.path): + next_pos = self.path[self.path_index] + self.entity.pos = next_pos + self.path_index += 1 + return + + # Reached waypoint, go to next + self.current_wp = (self.current_wp + 1) % len(self.waypoints) + target = self.waypoints[self.current_wp] + + # Compute path to next waypoint + self.path = self.entity.path_to(target[0], target[1]) + self.path_index = 0 + +# Create patrol waypoints +npc_waypoints = [] +for _ in range(4): + wp = find_walkable_pos() + npc_waypoints.append(wp) + +npc_controller = NPCController(npc, npc_waypoints) + +# ============================================================================= +# FOV Visualization +# ============================================================================= +def update_fov_colors(): + """Update terrain colors based on FOV""" + # Compute FOV from player position + visible_cells = viewport.compute_fov((player.pos[0], player.pos[1]), FOV_RADIUS) + visible_set = set((c[0], c[1]) for c in visible_cells) + + # Update discovered + discovered.update(visible_set) + + # Update terrain colors + r_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) + g_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) + b_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) + + for z in range(GRID_DEPTH): + for x in range(GRID_WIDTH): + base_r, base_g, base_b = base_colors[z][x] + + if (x, z) in visible_set: + # Fully visible + r_map[x, z] = base_r + g_map[x, z] = base_g + b_map[x, z] = base_b + elif (x, z) in discovered: + # Discovered but not visible - dim + r_map[x, z] = base_r * 0.4 + g_map[x, z] = base_g * 0.4 + b_map[x, z] = base_b * 0.4 + else: + # Never seen - very dark + r_map[x, z] = base_r * 0.1 + g_map[x, z] = base_g * 0.1 + b_map[x, z] = base_b * 0.1 + + viewport.apply_terrain_colors("terrain", r_map, g_map, b_map) + +# Initial FOV update +update_fov_colors() + +# ============================================================================= +# UI Overlay +# ============================================================================= +ui_frame = mcrfpy.Frame( + pos=(720, 10), + size=(260, 200), + fill_color=mcrfpy.Color(20, 20, 30, 220), + outline_color=mcrfpy.Color(80, 80, 120), + outline=2.0 +) +scene.children.append(ui_frame) + +title_label = mcrfpy.Caption(text="3D Integration Demo", pos=(740, 20)) +title_label.fill_color = mcrfpy.Color(255, 255, 150) +scene.children.append(title_label) + +status_label = mcrfpy.Caption(text="Status: Idle", pos=(740, 50)) +status_label.fill_color = mcrfpy.Color(150, 255, 150) +scene.children.append(status_label) + +player_pos_label = mcrfpy.Caption(text="Player: (0, 0)", pos=(740, 75)) +player_pos_label.fill_color = mcrfpy.Color(100, 200, 255) +scene.children.append(player_pos_label) + +npc_pos_label = mcrfpy.Caption(text="NPC: (0, 0)", pos=(740, 100)) +npc_pos_label.fill_color = mcrfpy.Color(255, 150, 150) +scene.children.append(npc_pos_label) + +fps_label = mcrfpy.Caption(text="FPS: --", pos=(740, 125)) +fps_label.fill_color = mcrfpy.Color(200, 200, 200) +scene.children.append(fps_label) + +discovered_label = mcrfpy.Caption(text="Discovered: 0", pos=(740, 150)) +discovered_label.fill_color = mcrfpy.Color(180, 180, 100) +scene.children.append(discovered_label) + +# Controls info +controls_frame = mcrfpy.Frame( + pos=(720, 220), + size=(260, 120), + fill_color=mcrfpy.Color(20, 20, 30, 200), + outline_color=mcrfpy.Color(60, 60, 80), + outline=1.0 +) +scene.children.append(controls_frame) + +ctrl_title = mcrfpy.Caption(text="Controls:", pos=(740, 230)) +ctrl_title.fill_color = mcrfpy.Color(200, 200, 100) +scene.children.append(ctrl_title) + +ctrl_lines = [ + "Arrow keys: Move", + "Click: Pathfind", + "F: Toggle follow cam", + "ESC: Quit" +] +for i, line in enumerate(ctrl_lines): + cap = mcrfpy.Caption(text=line, pos=(740, 255 + i * 20)) + cap.fill_color = mcrfpy.Color(150, 150, 150) + scene.children.append(cap) + +# ============================================================================= +# Game State +# ============================================================================= +follow_camera = True +frame_count = 0 +fps_update_time = 0 + +# ============================================================================= +# Update Function +# ============================================================================= +def game_update(timer, runtime): + global frame_count, fps_update_time + + try: + # Calculate FPS + frame_count += 1 + if runtime - fps_update_time >= 1000: # Update FPS every second + fps = frame_count + fps_label.text = f"FPS: {fps}" + frame_count = 0 + fps_update_time = runtime + + # Update NPC patrol + npc_controller.update() + + # Update UI labels + px, pz = player.pos + player_pos_label.text = f"Player: ({px}, {pz})" + nx, nz = npc.pos + npc_pos_label.text = f"NPC: ({nx}, {nz})" + discovered_label.text = f"Discovered: {len(discovered)}" + + # Camera follow + if follow_camera: + viewport.follow(player, distance=12.0, height=8.0, smoothing=0.1) + + # Update status based on player state + if player.is_moving: + status_label.text = "Status: Moving" + status_label.fill_color = mcrfpy.Color(255, 255, 100) + else: + status_label.text = "Status: Idle" + status_label.fill_color = mcrfpy.Color(150, 255, 150) + except Exception as e: + print(f"Update error: {e}") + +# ============================================================================= +# Input Handling +# ============================================================================= +def try_move_player(dx, dz): + """Try to move player in direction""" + new_x = player.pos[0] + dx + new_z = player.pos[1] + dz + + if not viewport.is_in_fov(new_x, new_z): + # Allow moving into discovered cells even if not currently visible + if (new_x, new_z) not in discovered: + return False + + if new_x < 0 or new_x >= GRID_WIDTH or new_z < 0 or new_z >= GRID_DEPTH: + return False + + cell = viewport.at(new_x, new_z) + if not cell.walkable: + return False + + player.pos = (new_x, new_z) + update_fov_colors() + return True + +def on_key(key, state): + global follow_camera + + if state != mcrfpy.InputState.PRESSED: + return + + if player.is_moving: + return # Don't accept input while moving + + dx, dz = 0, 0 + if key == mcrfpy.Key.UP: + dz = -1 + elif key == mcrfpy.Key.DOWN: + dz = 1 + elif key == mcrfpy.Key.LEFT: + dx = -1 + elif key == mcrfpy.Key.RIGHT: + dx = 1 + elif key == mcrfpy.Key.F: + follow_camera = not follow_camera + status_label.text = f"Camera: {'Follow' if follow_camera else 'Free'}" + return + elif key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + return + + if dx != 0 or dz != 0: + try_move_player(dx, dz) + +# Click-to-move handling +def on_click(pos, button, state): + if button != mcrfpy.MouseButton.LEFT or state != mcrfpy.InputState.PRESSED: + return + + if player.is_moving: + return + + # Convert click position to viewport-relative coordinates + vp_x = pos.x - viewport.x + vp_y = pos.y - viewport.y + + # Check if click is within viewport + if vp_x < 0 or vp_x >= viewport.w or vp_y < 0 or vp_y >= viewport.h: + return + + # Convert to world position + world_pos = viewport.screen_to_world(vp_x, vp_y) + if world_pos is None: + return + + # Convert to grid position + grid_x = int(world_pos[0] / CELL_SIZE) + grid_z = int(world_pos[2] / CELL_SIZE) + + # Validate grid position + if grid_x < 0 or grid_x >= GRID_WIDTH or grid_z < 0 or grid_z >= GRID_DEPTH: + return + + cell = viewport.at(grid_x, grid_z) + if not cell.walkable: + status_label.text = "Status: Can't walk there!" + status_label.fill_color = mcrfpy.Color(255, 100, 100) + return + + # Find path + path = player.path_to(grid_x, grid_z) + if not path: + status_label.text = "Status: No path!" + status_label.fill_color = mcrfpy.Color(255, 100, 100) + return + + # Follow path (limited to FOV_RADIUS steps) + limited_path = path[:FOV_RADIUS] + player.follow_path(limited_path) + status_label.text = f"Status: Moving ({len(limited_path)} steps)" + status_label.fill_color = mcrfpy.Color(255, 255, 100) + + # Schedule FOV update after movement completes + fov_update_timer = None + + def update_fov_after_move(*args): + # Accept any number of args since timer may pass (runtime) or (timer, runtime) + nonlocal fov_update_timer + if not player.is_moving: + update_fov_colors() + if fov_update_timer: + fov_update_timer.stop() + + fov_update_timer = mcrfpy.Timer("fov_update", update_fov_after_move, 100) + +scene.on_key = on_key +viewport.on_click = on_click + +# ============================================================================= +# Start Game +# ============================================================================= +timer = mcrfpy.Timer("game_update", game_update, 16) # ~60 FPS + +mcrfpy.current_scene = scene + +print() +print("=" * 60) +print("3D Integration Demo Loaded!") +print("=" * 60) +print(f" Terrain: {GRID_WIDTH}x{GRID_DEPTH} cells") +print(f" Player starts at: {player_start}") +print(f" NPC patrolling {len(npc_waypoints)} waypoints") +print() +print("Controls:") +print(" Arrow keys: Move player") +print(" Click: Pathfind to location") +print(" F: Toggle camera follow") +print(" ESC: Quit") +print("=" * 60) diff --git a/tests/demo/screens/model_loading_demo.py b/tests/demo/screens/model_loading_demo.py new file mode 100644 index 0000000..79f9c25 --- /dev/null +++ b/tests/demo/screens/model_loading_demo.py @@ -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") diff --git a/tests/demo/screens/skeletal_animation_demo.py b/tests/demo/screens/skeletal_animation_demo.py new file mode 100644 index 0000000..2f82908 --- /dev/null +++ b/tests/demo/screens/skeletal_animation_demo.py @@ -0,0 +1,275 @@ +# skeletal_animation_demo.py - 3D Skeletal Animation Demo Screen +# Demonstrates Entity3D animation with real animated glTF models + +import mcrfpy +import sys +import os + +DEMO_NAME = "3D Skeletal Animation" +DEMO_DESCRIPTION = """Entity3D Animation API with real skeletal models""" + +# Create demo scene +scene = mcrfpy.Scene("skeletal_animation_demo") + +# Dark background frame +bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25)) +scene.children.append(bg) + +# Title +title = mcrfpy.Caption(text="Skeletal Animation Demo", pos=(20, 10)) +title.fill_color = mcrfpy.Color(255, 255, 100) +scene.children.append(title) + +# Create the 3D viewport +viewport = mcrfpy.Viewport3D( + pos=(50, 50), + size=(600, 500), + render_resolution=(600, 500), + fov=60.0, + camera_pos=(0.0, 2.0, 5.0), + camera_target=(0.0, 1.0, 0.0), + bg_color=mcrfpy.Color(30, 30, 50) +) +scene.children.append(viewport) + +# Set up navigation grid +GRID_SIZE = 16 +viewport.set_grid_size(GRID_SIZE, GRID_SIZE) + +# Build a simple flat floor +hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) +hm.normalize(0.0, 0.0) +viewport.apply_heightmap(hm, 0.0) +viewport.build_terrain( + layer_name="floor", + heightmap=hm, + y_scale=0.0, + cell_size=1.0 +) + +# Apply floor colors (dark gray) +r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) +g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) +b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) +for y in range(GRID_SIZE): + for x in range(GRID_SIZE): + checker = ((x + y) % 2) * 0.1 + 0.15 + r_map[x, y] = checker + g_map[x, y] = checker + b_map[x, y] = checker + 0.05 +viewport.apply_terrain_colors("floor", r_map, g_map, b_map) + +# Load animated models +animated_entity = None +model_info = "No animated model" + +# Try to load CesiumMan (humanoid with walk animation) +try: + model = mcrfpy.Model3D("../assets/models/CesiumMan.glb") + if model.has_skeleton: + animated_entity = mcrfpy.Entity3D(pos=(8, 8), scale=1.0, color=mcrfpy.Color(200, 180, 150)) + animated_entity.model = model + viewport.entities.append(animated_entity) + + # Set up animation + clips = model.animation_clips + if clips: + animated_entity.anim_clip = clips[0] + animated_entity.anim_loop = True + animated_entity.anim_speed = 1.0 + + model_info = f"CesiumMan: {model.bone_count} bones, {model.vertex_count} verts" + print(f"Loaded {model_info}") + print(f"Animation clips: {clips}") +except Exception as e: + print(f"Failed to load CesiumMan: {e}") + +# Also try RiggedSimple as a second model +try: + model2 = mcrfpy.Model3D("../assets/models/RiggedSimple.glb") + if model2.has_skeleton: + entity2 = mcrfpy.Entity3D(pos=(10, 8), scale=0.5, color=mcrfpy.Color(100, 200, 255)) + entity2.model = model2 + viewport.entities.append(entity2) + + clips = model2.animation_clips + if clips: + entity2.anim_clip = clips[0] + entity2.anim_loop = True + entity2.anim_speed = 1.5 + + print(f"Loaded RiggedSimple: {model2.bone_count} bones") +except Exception as e: + print(f"Failed to load RiggedSimple: {e}") + +# Info panel on the right +info_panel = mcrfpy.Frame(pos=(670, 50), size=(330, 500), + fill_color=mcrfpy.Color(30, 30, 40), + outline_color=mcrfpy.Color(80, 80, 100), + outline=2.0) +scene.children.append(info_panel) + +# Panel title +panel_title = mcrfpy.Caption(text="Animation Properties", pos=(690, 60)) +panel_title.fill_color = mcrfpy.Color(200, 200, 255) +scene.children.append(panel_title) + +# Status labels (will be updated by timer) +status_labels = [] +y_offset = 90 + +label_texts = [ + "Model: loading...", + "anim_clip: ", + "anim_time: 0.00", + "anim_speed: 1.00", + "anim_loop: True", + "anim_paused: False", + "anim_frame: 0", +] + +for text in label_texts: + label = mcrfpy.Caption(text=text, pos=(690, y_offset)) + label.fill_color = mcrfpy.Color(150, 200, 150) + scene.children.append(label) + status_labels.append(label) + y_offset += 25 + +# Set initial model info +status_labels[0].text = f"Model: {model_info}" + +# Controls section +y_offset += 20 +controls_title = mcrfpy.Caption(text="Controls:", pos=(690, y_offset)) +controls_title.fill_color = mcrfpy.Color(255, 255, 200) +scene.children.append(controls_title) +y_offset += 25 + +controls = [ + "[SPACE] Toggle pause", + "[L] Toggle loop", + "[+/-] Adjust speed", + "[R] Reset time", + "[1-3] Camera presets", +] + +for ctrl in controls: + cap = mcrfpy.Caption(text=ctrl, pos=(690, y_offset)) + cap.fill_color = mcrfpy.Color(180, 180, 150) + scene.children.append(cap) + y_offset += 20 + +# Auto-animate section +y_offset += 20 +auto_title = mcrfpy.Caption(text="Auto-Animate:", pos=(690, y_offset)) +auto_title.fill_color = mcrfpy.Color(255, 200, 200) +scene.children.append(auto_title) +y_offset += 25 + +auto_labels = [] +auto_texts = [ + "auto_animate: True", + "walk_clip: 'walk'", + "idle_clip: 'idle'", +] + +for text in auto_texts: + cap = mcrfpy.Caption(text=text, pos=(690, y_offset)) + cap.fill_color = mcrfpy.Color(180, 160, 160) + scene.children.append(cap) + auto_labels.append(cap) + y_offset += 20 + +# Instructions at bottom +status = mcrfpy.Caption(text="Status: Animation playing", pos=(20, 570)) +status.fill_color = mcrfpy.Color(100, 200, 100) +scene.children.append(status) + +# Camera presets +camera_presets = [ + ((0.0, 2.0, 5.0), (0.0, 1.0, 0.0), "Front view"), + ((5.0, 3.0, 0.0), (0.0, 1.0, 0.0), "Side view"), + ((0.0, 6.0, 0.1), (0.0, 0.0, 0.0), "Top-down view"), +] + +# Update function - updates display and entity rotation +def update(timer, runtime): + if animated_entity: + # Update status display + status_labels[1].text = f"anim_clip: '{animated_entity.anim_clip}'" + status_labels[2].text = f"anim_time: {animated_entity.anim_time:.2f}" + status_labels[3].text = f"anim_speed: {animated_entity.anim_speed:.2f}" + status_labels[4].text = f"anim_loop: {animated_entity.anim_loop}" + status_labels[5].text = f"anim_paused: {animated_entity.anim_paused}" + status_labels[6].text = f"anim_frame: {animated_entity.anim_frame}" + + auto_labels[0].text = f"auto_animate: {animated_entity.auto_animate}" + auto_labels[1].text = f"walk_clip: '{animated_entity.walk_clip}'" + auto_labels[2].text = f"idle_clip: '{animated_entity.idle_clip}'" + +# Key handler +def on_key(key, state): + if state != mcrfpy.InputState.PRESSED: + return + + if animated_entity: + if key == mcrfpy.Key.SPACE: + animated_entity.anim_paused = not animated_entity.anim_paused + status.text = f"Status: {'Paused' if animated_entity.anim_paused else 'Playing'}" + + elif key == mcrfpy.Key.L: + animated_entity.anim_loop = not animated_entity.anim_loop + status.text = f"Status: Loop {'ON' if animated_entity.anim_loop else 'OFF'}" + + elif key == mcrfpy.Key.EQUAL or key == mcrfpy.Key.ADD: + animated_entity.anim_speed = min(animated_entity.anim_speed + 0.25, 4.0) + status.text = f"Status: Speed {animated_entity.anim_speed:.2f}x" + + elif key == mcrfpy.Key.HYPHEN or key == mcrfpy.Key.SUBTRACT: + animated_entity.anim_speed = max(animated_entity.anim_speed - 0.25, 0.0) + status.text = f"Status: Speed {animated_entity.anim_speed:.2f}x" + + elif key == mcrfpy.Key.R: + animated_entity.anim_time = 0.0 + status.text = "Status: Animation reset" + + # Camera presets + if key == mcrfpy.Key.NUM_1: + pos, target, name = camera_presets[0] + viewport.camera_pos = pos + viewport.camera_target = target + status.text = f"Camera: {name}" + + elif key == mcrfpy.Key.NUM_2: + pos, target, name = camera_presets[1] + viewport.camera_pos = pos + viewport.camera_target = target + status.text = f"Camera: {name}" + + elif key == mcrfpy.Key.NUM_3: + pos, target, name = camera_presets[2] + viewport.camera_pos = pos + viewport.camera_target = target + status.text = f"Camera: {name}" + + elif key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + +# Set up scene +scene.on_key = on_key + +# Create timer for updates +timer = mcrfpy.Timer("anim_update", update, 16) + +# Activate scene +mcrfpy.current_scene = scene + +print() +print("Skeletal Animation Demo loaded!") +print("Controls:") +print(" [Space] Toggle pause") +print(" [L] Toggle loop") +print(" [+/-] Adjust speed") +print(" [R] Reset time") +print(" [1-3] Camera presets") +print(" [ESC] Quit") diff --git a/tests/demo/screens/voxel_core_demo.py b/tests/demo/screens/voxel_core_demo.py new file mode 100644 index 0000000..47c2627 --- /dev/null +++ b/tests/demo/screens/voxel_core_demo.py @@ -0,0 +1,263 @@ +"""VoxelGrid Core Demo (Milestone 9) + +Demonstrates the VoxelGrid data structure without rendering. +This is a "console demo" that creates VoxelGrids, defines materials, +places voxel patterns, and displays statistics. + +Note: Visual rendering comes in Milestone 10 (VoxelMeshing). +""" +import mcrfpy +from mcrfpy import Color + +def format_bytes(bytes_val): + """Format bytes as human-readable string""" + if bytes_val < 1024: + return f"{bytes_val} B" + elif bytes_val < 1024 * 1024: + return f"{bytes_val / 1024:.1f} KB" + else: + return f"{bytes_val / (1024 * 1024):.1f} MB" + +def print_header(title): + """Print a formatted header""" + print("\n" + "=" * 60) + print(f" {title}") + print("=" * 60) + +def print_grid_stats(vg, name="VoxelGrid"): + """Print statistics for a VoxelGrid""" + print(f"\n {name}:") + print(f" Dimensions: {vg.width} x {vg.height} x {vg.depth}") + print(f" Total voxels: {vg.width * vg.height * vg.depth:,}") + print(f" Cell size: {vg.cell_size} units") + print(f" Materials: {vg.material_count}") + print(f" Non-air voxels: {vg.count_non_air():,}") + print(f" Memory estimate: {format_bytes(vg.width * vg.height * vg.depth)}") + print(f" Offset: {vg.offset}") + print(f" Rotation: {vg.rotation} deg") + +def demo_basic_creation(): + """Demonstrate basic VoxelGrid creation""" + print_header("1. Basic VoxelGrid Creation") + + # Create various sizes + small = mcrfpy.VoxelGrid(size=(8, 4, 8)) + medium = mcrfpy.VoxelGrid(size=(16, 8, 16), cell_size=1.0) + large = mcrfpy.VoxelGrid(size=(32, 16, 32), cell_size=0.5) + + print_grid_stats(small, "Small (8x4x8)") + print_grid_stats(medium, "Medium (16x8x16)") + print_grid_stats(large, "Large (32x16x32, 0.5 cell size)") + +def demo_material_palette(): + """Demonstrate material palette system""" + print_header("2. Material Palette System") + + vg = mcrfpy.VoxelGrid(size=(16, 8, 16)) + + # Define a palette of building materials + materials = {} + materials['stone'] = vg.add_material("stone", color=Color(128, 128, 128)) + materials['brick'] = vg.add_material("brick", color=Color(165, 42, 42)) + materials['wood'] = vg.add_material("wood", color=Color(139, 90, 43)) + materials['glass'] = vg.add_material("glass", + color=Color(200, 220, 255, 128), + transparent=True, + path_cost=1.0) + materials['metal'] = vg.add_material("metal", + color=Color(180, 180, 190), + path_cost=0.8) + materials['grass'] = vg.add_material("grass", color=Color(60, 150, 60)) + + print(f"\n Defined {vg.material_count} materials:") + print(f" ID 0: air (implicit, always transparent)") + + for name, mat_id in materials.items(): + mat = vg.get_material(mat_id) + c = mat['color'] + props = [] + if mat['transparent']: + props.append("transparent") + if mat['path_cost'] != 1.0: + props.append(f"cost={mat['path_cost']}") + props_str = f" ({', '.join(props)})" if props else "" + print(f" ID {mat_id}: {name} RGB({c.r},{c.g},{c.b},{c.a}){props_str}") + + return vg, materials + +def demo_voxel_placement(): + """Demonstrate voxel placement patterns""" + print_header("3. Voxel Placement Patterns") + + vg, materials = demo_material_palette() + stone = materials['stone'] + brick = materials['brick'] + wood = materials['wood'] + + # Pattern 1: Solid cube + print("\n Pattern: Solid 4x4x4 cube at origin") + for z in range(4): + for y in range(4): + for x in range(4): + vg.set(x, y, z, stone) + print(f" Placed {vg.count_material(stone)} stone voxels") + + # Pattern 2: Checkerboard floor + print("\n Pattern: Checkerboard floor at y=0, x=6-14, z=0-8") + for z in range(8): + for x in range(6, 14): + mat = stone if (x + z) % 2 == 0 else brick + vg.set(x, 0, z, mat) + print(f" Stone: {vg.count_material(stone)}, Brick: {vg.count_material(brick)}") + + # Pattern 3: Hollow cube (walls only) + print("\n Pattern: Hollow cube frame 4x4x4 at x=10, z=10") + for x in range(4): + for y in range(4): + for z in range(4): + # Only place on edges + on_edge_x = (x == 0 or x == 3) + on_edge_y = (y == 0 or y == 3) + on_edge_z = (z == 0 or z == 3) + if sum([on_edge_x, on_edge_y, on_edge_z]) >= 2: + vg.set(10 + x, y, 10 + z, wood) + print(f" Wood voxels: {vg.count_material(wood)}") + + print_grid_stats(vg, "After patterns") + + # Material breakdown + print("\n Material breakdown:") + print(f" Air: {vg.count_material(0):,} ({100 * vg.count_material(0) / (16*8*16):.1f}%)") + print(f" Stone: {vg.count_material(stone):,}") + print(f" Brick: {vg.count_material(brick):,}") + print(f" Wood: {vg.count_material(wood):,}") + +def demo_bulk_operations(): + """Demonstrate bulk fill and clear operations""" + print_header("4. Bulk Operations") + + vg = mcrfpy.VoxelGrid(size=(32, 8, 32)) + total = 32 * 8 * 32 + + stone = vg.add_material("stone", color=Color(128, 128, 128)) + + print(f"\n Grid: 32x8x32 = {total:,} voxels") + + # Fill + vg.fill(stone) + print(f" After fill(stone): {vg.count_non_air():,} non-air") + + # Clear + vg.clear() + print(f" After clear(): {vg.count_non_air():,} non-air") + +def demo_transforms(): + """Demonstrate transform properties""" + print_header("5. Transform Properties") + + vg = mcrfpy.VoxelGrid(size=(8, 8, 8)) + + print(f"\n Default state:") + print(f" Offset: {vg.offset}") + print(f" Rotation: {vg.rotation} deg") + + # Position for a building + vg.offset = (100.0, 0.0, 50.0) + vg.rotation = 45.0 + + print(f"\n After positioning:") + print(f" Offset: {vg.offset}") + print(f" Rotation: {vg.rotation} deg") + + # Multiple buildings with different transforms + print("\n Example: Village layout with 3 buildings") + buildings = [] + positions = [(0, 0, 0), (20, 0, 0), (10, 0, 15)] + rotations = [0, 90, 45] + + for i, (pos, rot) in enumerate(zip(positions, rotations)): + b = mcrfpy.VoxelGrid(size=(8, 6, 8)) + b.offset = pos + b.rotation = rot + buildings.append(b) + print(f" Building {i+1}: offset={pos}, rotation={rot} deg") + +def demo_edge_cases(): + """Test edge cases and limits""" + print_header("6. Edge Cases and Limits") + + # Maximum practical size + print("\n Testing large grid (64x64x64)...") + large = mcrfpy.VoxelGrid(size=(64, 64, 64)) + mat = large.add_material("test", color=Color(128, 128, 128)) + large.fill(mat) + print(f" Created and filled: {large.count_non_air():,} voxels") + large.clear() + print(f" Cleared: {large.count_non_air()} voxels") + + # Bounds checking + print("\n Bounds checking (should not crash):") + small = mcrfpy.VoxelGrid(size=(4, 4, 4)) + test_mat = small.add_material("test", color=Color(255, 0, 0)) + small.set(-1, 0, 0, test_mat) + small.set(100, 0, 0, test_mat) + print(f" Out-of-bounds get(-1,0,0): {small.get(-1, 0, 0)} (expected 0)") + print(f" Out-of-bounds get(100,0,0): {small.get(100, 0, 0)} (expected 0)") + + # Material palette capacity + print("\n Material palette capacity test:") + full_vg = mcrfpy.VoxelGrid(size=(4, 4, 4)) + for i in range(255): + full_vg.add_material(f"mat_{i}", color=Color(i, i, i)) + print(f" Added 255 materials: count = {full_vg.material_count}") + + try: + full_vg.add_material("overflow", color=Color(255, 255, 255)) + print(" ERROR: Should have raised exception!") + except RuntimeError as e: + print(f" 256th material correctly rejected: {e}") + +def demo_memory_usage(): + """Show memory usage for various grid sizes""" + print_header("7. Memory Usage Estimates") + + sizes = [ + (8, 8, 8), + (16, 8, 16), + (32, 16, 32), + (64, 32, 64), + (80, 16, 45), # Example dungeon size + ] + + print("\n Size Voxels Memory") + print(" " + "-" * 40) + + for w, h, d in sizes: + voxels = w * h * d + memory = voxels # 1 byte per voxel + print(f" {w:3}x{h:3}x{d:3} {voxels:>10,} {format_bytes(memory):>10}") + +def main(): + """Run all demos""" + print("\n" + "=" * 60) + print(" VOXELGRID CORE DEMO (Milestone 9)") + print(" Dense 3D Voxel Array with Material Palette") + print("=" * 60) + + demo_basic_creation() + demo_material_palette() + demo_voxel_placement() + demo_bulk_operations() + demo_transforms() + demo_edge_cases() + demo_memory_usage() + + print_header("Demo Complete!") + print("\n Next milestone (10): Voxel Mesh Generation") + print(" The VoxelGrid data will be converted to renderable 3D meshes.") + print() + +if __name__ == "__main__": + import sys + main() + sys.exit(0) diff --git a/tests/demo/screens/voxel_dungeon_demo.py b/tests/demo/screens/voxel_dungeon_demo.py new file mode 100644 index 0000000..33a5799 --- /dev/null +++ b/tests/demo/screens/voxel_dungeon_demo.py @@ -0,0 +1,273 @@ +# voxel_dungeon_demo.py - Procedural dungeon demonstrating bulk voxel operations +# Milestone 11: Bulk Operations and Building Primitives + +import mcrfpy +import sys +import math +import random + +# Create demo scene +scene = mcrfpy.Scene("voxel_dungeon_demo") + +# Dark background +bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(20, 20, 30)) +scene.children.append(bg) + +# Title +title = mcrfpy.Caption(text="Voxel Dungeon Demo - Bulk Operations (Milestone 11)", pos=(20, 10)) +title.fill_color = mcrfpy.Color(255, 255, 255) +scene.children.append(title) + +# Create the 3D viewport +viewport = mcrfpy.Viewport3D( + pos=(50, 60), + size=(620, 520), + render_resolution=(400, 320), + fov=60.0, + camera_pos=(40.0, 30.0, 40.0), + camera_target=(16.0, 4.0, 16.0), + bg_color=mcrfpy.Color(30, 30, 40) # Dark atmosphere +) +scene.children.append(viewport) + +# Global voxel grid reference +voxels = None +seed = 42 + +def generate_dungeon(dungeon_seed=42): + """Generate a procedural dungeon showcasing all bulk operations""" + global voxels, seed + seed = dungeon_seed + random.seed(seed) + + # Create voxel grid for dungeon + print(f"Generating dungeon (seed={seed})...") + voxels = mcrfpy.VoxelGrid(size=(32, 12, 32), cell_size=1.0) + + # Define materials + STONE_WALL = voxels.add_material("stone_wall", color=mcrfpy.Color(80, 80, 90)) + STONE_FLOOR = voxels.add_material("stone_floor", color=mcrfpy.Color(100, 95, 90)) + MOSS = voxels.add_material("moss", color=mcrfpy.Color(40, 80, 40)) + WATER = voxels.add_material("water", color=mcrfpy.Color(40, 80, 160, 180), transparent=True) + PILLAR = voxels.add_material("pillar", color=mcrfpy.Color(120, 110, 100)) + GOLD = voxels.add_material("gold", color=mcrfpy.Color(255, 215, 0)) + + print(f"Defined {voxels.material_count} materials") + + # 1. Main room using fill_box_hollow + print("Building main room with fill_box_hollow...") + voxels.fill_box_hollow((2, 0, 2), (29, 10, 29), STONE_WALL, thickness=1) + + # 2. Floor with slight variation using fill_box + voxels.fill_box((3, 0, 3), (28, 0, 28), STONE_FLOOR) + + # 3. Spherical alcoves carved into walls using fill_sphere + print("Carving alcoves with fill_sphere...") + alcove_positions = [ + (2, 5, 16), # West wall + (29, 5, 16), # East wall + (16, 5, 2), # North wall + (16, 5, 29), # South wall + ] + for pos in alcove_positions: + voxels.fill_sphere(pos, 3, 0) # Carve out (air) + + # 4. Small decorative spheres (gold orbs in alcoves) + print("Adding gold orbs in alcoves...") + for i, pos in enumerate(alcove_positions): + # Offset inward so orb is visible + ox, oy, oz = pos + if ox < 10: + ox += 2 + elif ox > 20: + ox -= 2 + if oz < 10: + oz += 2 + elif oz > 20: + oz -= 2 + voxels.fill_sphere((ox, oy - 1, oz), 1, GOLD) + + # 5. Support pillars using fill_cylinder + print("Building pillars with fill_cylinder...") + pillar_positions = [ + (8, 1, 8), (8, 1, 24), + (24, 1, 8), (24, 1, 24), + (16, 1, 8), (16, 1, 24), + (8, 1, 16), (24, 1, 16), + ] + for px, py, pz in pillar_positions: + voxels.fill_cylinder((px, py, pz), 1, 9, PILLAR) + + # 6. Moss patches using fill_noise + print("Adding moss patches with fill_noise...") + voxels.fill_noise((3, 1, 3), (28, 1, 28), MOSS, threshold=0.65, scale=0.15, seed=seed) + + # 7. Central water pool + print("Creating water pool...") + voxels.fill_box((12, 0, 12), (20, 0, 20), 0) # Carve depression + voxels.fill_box((12, 0, 12), (20, 0, 20), WATER) + + # 8. Copy a pillar as prefab and paste variations + print("Creating prefab from pillar and pasting copies...") + pillar_prefab = voxels.copy_region((8, 1, 8), (9, 9, 9)) + print(f" Pillar prefab: {pillar_prefab.size}") + + # Paste smaller pillars at corners (offset from main room) + corner_positions = [(4, 1, 4), (4, 1, 27), (27, 1, 4), (27, 1, 27)] + for cx, cy, cz in corner_positions: + voxels.paste_region(pillar_prefab, (cx, cy, cz), skip_air=True) + + # Build mesh + voxels.rebuild_mesh() + + print(f"\nDungeon generated:") + print(f" Non-air voxels: {voxels.count_non_air()}") + print(f" Vertices: {voxels.vertex_count}") + print(f" Faces: {voxels.vertex_count // 6}") + + # Add to viewport + # First remove old layer if exists + if viewport.voxel_layer_count() > 0: + pass # Can't easily remove, so we regenerate the whole viewport + viewport.add_voxel_layer(voxels, z_index=0) + + return voxels + +# Generate initial dungeon +generate_dungeon(42) + +# Create info panel +info_frame = mcrfpy.Frame(pos=(690, 60), size=(300, 280), fill_color=mcrfpy.Color(40, 40, 60, 220)) +scene.children.append(info_frame) + +info_title = mcrfpy.Caption(text="Dungeon Stats", pos=(700, 70)) +info_title.fill_color = mcrfpy.Color(255, 255, 100) +scene.children.append(info_title) + +def update_stats(): + global stats_caption + stats_text = f"""Grid: {voxels.width}x{voxels.height}x{voxels.depth} +Total cells: {voxels.width * voxels.height * voxels.depth} +Non-air: {voxels.count_non_air()} +Materials: {voxels.material_count} + +Mesh Stats: + Vertices: {voxels.vertex_count} + Faces: {voxels.vertex_count // 6} + +Seed: {seed} + +Operations Used: + - fill_box_hollow (walls) + - fill_sphere (alcoves) + - fill_cylinder (pillars) + - fill_noise (moss) + - copy/paste (prefabs)""" + stats_caption.text = stats_text + +stats_caption = mcrfpy.Caption(text="", pos=(700, 100)) +stats_caption.fill_color = mcrfpy.Color(200, 200, 200) +scene.children.append(stats_caption) +update_stats() + +# Controls panel +controls_frame = mcrfpy.Frame(pos=(690, 360), size=(300, 180), fill_color=mcrfpy.Color(40, 40, 60, 220)) +scene.children.append(controls_frame) + +controls_title = mcrfpy.Caption(text="Controls", pos=(700, 370)) +controls_title.fill_color = mcrfpy.Color(255, 255, 100) +scene.children.append(controls_title) + +controls_text = """R - Regenerate dungeon (new seed) +1-4 - Camera presets ++/- - Zoom in/out +SPACE - Reset camera +ESC - Exit demo""" + +controls = mcrfpy.Caption(text=controls_text, pos=(700, 400)) +controls.fill_color = mcrfpy.Color(200, 200, 200) +scene.children.append(controls) + +# Camera animation state +rotation_enabled = False +camera_distance = 50.0 +camera_angle = 45.0 # degrees +camera_height = 30.0 + +camera_presets = [ + (40.0, 30.0, 40.0, 16.0, 4.0, 16.0), # Default diagonal + (16.0, 30.0, 50.0, 16.0, 4.0, 16.0), # Front view + (50.0, 30.0, 16.0, 16.0, 4.0, 16.0), # Side view + (16.0, 50.0, 16.0, 16.0, 4.0, 16.0), # Top-down +] + +def rotate_camera(timer_name, runtime): + """Timer callback for camera rotation""" + global camera_angle, rotation_enabled + if rotation_enabled: + camera_angle += 0.5 + if camera_angle >= 360.0: + camera_angle = 0.0 + rad = camera_angle * math.pi / 180.0 + x = 16.0 + camera_distance * math.cos(rad) + z = 16.0 + camera_distance * math.sin(rad) + viewport.camera_pos = (x, camera_height, z) + +# Set up rotation timer +timer = mcrfpy.Timer("rotate_cam", rotate_camera, 33) + +def handle_key(key, action): + """Keyboard handler""" + global rotation_enabled, seed, camera_distance, camera_height + if action != mcrfpy.InputState.PRESSED: + return + + if key == mcrfpy.Key.R: + seed = random.randint(1, 99999) + generate_dungeon(seed) + update_stats() + print(f"Regenerated dungeon with seed {seed}") + elif key == mcrfpy.Key.NUM_1: + viewport.camera_pos = camera_presets[0][:3] + viewport.camera_target = camera_presets[0][3:] + rotation_enabled = False + elif key == mcrfpy.Key.NUM_2: + viewport.camera_pos = camera_presets[1][:3] + viewport.camera_target = camera_presets[1][3:] + rotation_enabled = False + elif key == mcrfpy.Key.NUM_3: + viewport.camera_pos = camera_presets[2][:3] + viewport.camera_target = camera_presets[2][3:] + rotation_enabled = False + elif key == mcrfpy.Key.NUM_4: + viewport.camera_pos = camera_presets[3][:3] + viewport.camera_target = camera_presets[3][3:] + rotation_enabled = False + elif key == mcrfpy.Key.SPACE: + rotation_enabled = not rotation_enabled + print(f"Camera rotation: {'ON' if rotation_enabled else 'OFF'}") + elif key == mcrfpy.Key.EQUALS or key == mcrfpy.Key.ADD: + camera_distance = max(20.0, camera_distance - 5.0) + camera_height = max(15.0, camera_height - 2.0) + elif key == mcrfpy.Key.DASH or key == mcrfpy.Key.SUBTRACT: + camera_distance = min(80.0, camera_distance + 5.0) + camera_height = min(50.0, camera_height + 2.0) + elif key == mcrfpy.Key.ESCAPE: + print("Exiting demo...") + sys.exit(0) + +scene.on_key = handle_key + +# Activate the scene +mcrfpy.current_scene = scene +print("\nVoxel Dungeon Demo ready!") +print("Press SPACE to toggle camera rotation, R to regenerate") + +# Main entry point for --exec mode +if __name__ == "__main__": + print("\n=== Voxel Dungeon Demo Summary ===") + print(f"Grid size: {voxels.width}x{voxels.height}x{voxels.depth}") + print(f"Non-air voxels: {voxels.count_non_air()}") + print(f"Generated vertices: {voxels.vertex_count}") + print(f"Rendered faces: {voxels.vertex_count // 6}") + print("===================================\n") diff --git a/tests/demo/screens/voxel_meshing_demo.py b/tests/demo/screens/voxel_meshing_demo.py new file mode 100644 index 0000000..862002b --- /dev/null +++ b/tests/demo/screens/voxel_meshing_demo.py @@ -0,0 +1,218 @@ +# voxel_meshing_demo.py - Visual demo of VoxelGrid mesh rendering +# Shows voxel building rendered in Viewport3D with PS1 effects + +import mcrfpy +import sys +import math + +# Create demo scene +scene = mcrfpy.Scene("voxel_meshing_demo") + +# Dark background frame +bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25)) +scene.children.append(bg) + +# Title +title = mcrfpy.Caption(text="VoxelGrid Meshing Demo - Face-Culled 3D Voxels", pos=(20, 10)) +title.fill_color = mcrfpy.Color(255, 255, 255) +scene.children.append(title) + +# Create the 3D viewport +viewport = mcrfpy.Viewport3D( + pos=(50, 60), + size=(600, 500), + render_resolution=(320, 240), # PS1 resolution + fov=60.0, + camera_pos=(20.0, 15.0, 20.0), + camera_target=(4.0, 2.0, 4.0), + bg_color=mcrfpy.Color(50, 70, 100) # Sky color +) +scene.children.append(viewport) + +# Create voxel grid for building +print("Creating voxel building...") +voxels = mcrfpy.VoxelGrid(size=(12, 8, 12), cell_size=1.0) + +# Define materials +STONE = voxels.add_material("stone", color=mcrfpy.Color(128, 128, 128)) +BRICK = voxels.add_material("brick", color=mcrfpy.Color(165, 82, 42)) +WOOD = voxels.add_material("wood", color=mcrfpy.Color(139, 90, 43)) +GLASS = voxels.add_material("glass", color=mcrfpy.Color(180, 220, 255, 180), transparent=True) +GRASS = voxels.add_material("grass", color=mcrfpy.Color(60, 150, 60)) + +print(f"Defined {voxels.material_count} materials") + +# Build a simple house structure + +# Ground/foundation +voxels.fill_box((0, 0, 0), (11, 0, 11), GRASS) + +# Floor +voxels.fill_box((1, 1, 1), (10, 1, 10), STONE) + +# Walls +# Front wall (Z=1) +voxels.fill_box((1, 2, 1), (10, 5, 1), BRICK) +# Back wall (Z=10) +voxels.fill_box((1, 2, 10), (10, 5, 10), BRICK) +# Left wall (X=1) +voxels.fill_box((1, 2, 1), (1, 5, 10), BRICK) +# Right wall (X=10) +voxels.fill_box((10, 2, 1), (10, 5, 10), BRICK) + +# Door opening (front wall) +voxels.fill_box((4, 2, 1), (6, 4, 1), 0) # Clear door opening + +# Windows +# Front windows (beside door) +voxels.fill_box((2, 3, 1), (3, 4, 1), GLASS) +voxels.fill_box((8, 3, 1), (9, 4, 1), GLASS) +# Side windows +voxels.fill_box((1, 3, 4), (1, 4, 5), GLASS) +voxels.fill_box((1, 3, 7), (1, 4, 8), GLASS) +voxels.fill_box((10, 3, 4), (10, 4, 5), GLASS) +voxels.fill_box((10, 3, 7), (10, 4, 8), GLASS) + +# Ceiling +voxels.fill_box((1, 6, 1), (10, 6, 10), WOOD) + +# Simple roof (peaked) +voxels.fill_box((0, 7, 0), (11, 7, 11), WOOD) +voxels.fill_box((1, 8, 1), (10, 8, 10), WOOD) +voxels.fill_box((2, 9, 2), (9, 9, 9), WOOD) +voxels.fill_box((3, 10, 3), (8, 10, 8), WOOD) +voxels.fill_box((4, 11, 4), (7, 11, 7), WOOD) + +# Build the mesh +voxels.rebuild_mesh() + +print(f"Built voxel house:") +print(f" Non-air voxels: {voxels.count_non_air()}") +print(f" Vertices: {voxels.vertex_count}") +print(f" Faces: {voxels.vertex_count // 6}") + +# Position the building +voxels.offset = (0.0, 0.0, 0.0) +voxels.rotation = 0.0 + +# Add to viewport +viewport.add_voxel_layer(voxels, z_index=0) +print(f"Added voxel layer to viewport (count: {viewport.voxel_layer_count()})") + +# Create info panel +info_frame = mcrfpy.Frame(pos=(680, 60), size=(300, 250), fill_color=mcrfpy.Color(40, 40, 60, 200)) +scene.children.append(info_frame) + +info_title = mcrfpy.Caption(text="Building Stats", pos=(690, 70)) +info_title.fill_color = mcrfpy.Color(255, 255, 100) +scene.children.append(info_title) + +stats_text = f"""Grid: {voxels.width}x{voxels.height}x{voxels.depth} +Total voxels: {voxels.width * voxels.height * voxels.depth} +Non-air: {voxels.count_non_air()} +Materials: {voxels.material_count} +Vertices: {voxels.vertex_count} +Faces: {voxels.vertex_count // 6} + +Without culling would be: + {voxels.count_non_air() * 36} vertices + ({100 - (voxels.vertex_count / (voxels.count_non_air() * 36) * 100):.0f}% reduction)""" + +stats = mcrfpy.Caption(text=stats_text, pos=(690, 100)) +stats.fill_color = mcrfpy.Color(200, 200, 200) +scene.children.append(stats) + +# Controls info +controls_frame = mcrfpy.Frame(pos=(680, 330), size=(300, 180), fill_color=mcrfpy.Color(40, 40, 60, 200)) +scene.children.append(controls_frame) + +controls_title = mcrfpy.Caption(text="Controls", pos=(690, 340)) +controls_title.fill_color = mcrfpy.Color(255, 255, 100) +scene.children.append(controls_title) + +controls_text = """R - Toggle rotation +1-5 - Change camera angle +SPACE - Reset camera +ESC - Exit demo""" + +controls = mcrfpy.Caption(text=controls_text, pos=(690, 370)) +controls.fill_color = mcrfpy.Color(200, 200, 200) +scene.children.append(controls) + +# Animation state +rotation_enabled = False +current_angle = 0.0 +camera_angles = [ + (20.0, 15.0, 20.0), # Default - diagonal view + (0.0, 15.0, 25.0), # Front view + (25.0, 15.0, 0.0), # Side view + (5.5, 25.0, 5.5), # Top-down view + (5.5, 3.0, 20.0), # Low angle +] +current_camera = 0 + +def rotate_building(timer, runtime): + """Timer callback for building rotation""" + global current_angle, rotation_enabled + if rotation_enabled: + current_angle += 1.0 + if current_angle >= 360.0: + current_angle = 0.0 + voxels.rotation = current_angle + +# Set up rotation timer +timer = mcrfpy.Timer("rotate", rotate_building, 33) # ~30 FPS + +def handle_key(key, action): + """Keyboard handler""" + global rotation_enabled, current_camera + if action != mcrfpy.InputState.PRESSED: + return + + if key == mcrfpy.Key.R: + rotation_enabled = not rotation_enabled + print(f"Rotation: {'ON' if rotation_enabled else 'OFF'}") + elif key == mcrfpy.Key.NUM_1: + current_camera = 0 + viewport.camera_pos = camera_angles[0] + print("Camera: Default diagonal") + elif key == mcrfpy.Key.NUM_2: + current_camera = 1 + viewport.camera_pos = camera_angles[1] + print("Camera: Front view") + elif key == mcrfpy.Key.NUM_3: + current_camera = 2 + viewport.camera_pos = camera_angles[2] + print("Camera: Side view") + elif key == mcrfpy.Key.NUM_4: + current_camera = 3 + viewport.camera_pos = camera_angles[3] + print("Camera: Top-down view") + elif key == mcrfpy.Key.NUM_5: + current_camera = 4 + viewport.camera_pos = camera_angles[4] + print("Camera: Low angle") + elif key == mcrfpy.Key.SPACE: + current_camera = 0 + voxels.rotation = 0.0 + viewport.camera_pos = camera_angles[0] + print("Camera: Reset") + elif key == mcrfpy.Key.ESCAPE: + print("Exiting demo...") + sys.exit(0) + +scene.on_key = handle_key + +# Activate the scene +mcrfpy.current_scene = scene +print("Voxel Meshing Demo ready! Press R to toggle rotation.") + +# Main entry point for --exec mode +if __name__ == "__main__": + # Demo is set up, print summary + print("\n=== Voxel Meshing Demo Summary ===") + print(f"Grid size: {voxels.width}x{voxels.height}x{voxels.depth}") + print(f"Non-air voxels: {voxels.count_non_air()}") + print(f"Generated vertices: {voxels.vertex_count}") + print(f"Rendered faces: {voxels.vertex_count // 6}") + print("===================================\n") diff --git a/tests/demo/screens/voxel_navigation_demo.py b/tests/demo/screens/voxel_navigation_demo.py new file mode 100644 index 0000000..7cb36e9 --- /dev/null +++ b/tests/demo/screens/voxel_navigation_demo.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Visual Demo: Milestone 12 - VoxelGrid Navigation Projection + +Demonstrates projection of 3D voxel terrain to 2D navigation grid for pathfinding. +Shows: +1. Voxel dungeon with multiple levels +2. Navigation grid projection (walkable/unwalkable areas) +3. A* pathfinding through the projected terrain +4. FOV computation from voxel transparency +""" + +import mcrfpy +import sys +import math + +def create_demo_scene(): + """Create the navigation projection demo scene""" + + scene = mcrfpy.Scene("voxel_nav_demo") + + # ========================================================================= + # Create a small dungeon-style voxel grid + # ========================================================================= + + vg = mcrfpy.VoxelGrid((16, 8, 16), cell_size=1.0) + + # Add materials + floor_mat = vg.add_material("floor", (100, 80, 60)) # Brown floor + wall_mat = vg.add_material("wall", (80, 80, 90), transparent=False) # Gray walls + pillar_mat = vg.add_material("pillar", (60, 60, 70), transparent=False) # Dark pillars + glass_mat = vg.add_material("glass", (150, 200, 255), transparent=True) # Transparent glass + water_mat = vg.add_material("water", (50, 100, 200), transparent=True, path_cost=3.0) # Slow water + + # Create floor + vg.fill_box((0, 0, 0), (15, 0, 15), floor_mat) + + # Create outer walls + vg.fill_box((0, 1, 0), (15, 4, 0), wall_mat) # North wall + vg.fill_box((0, 1, 15), (15, 4, 15), wall_mat) # South wall + vg.fill_box((0, 1, 0), (0, 4, 15), wall_mat) # West wall + vg.fill_box((15, 1, 0), (15, 4, 15), wall_mat) # East wall + + # Interior walls creating rooms + vg.fill_box((5, 1, 0), (5, 4, 10), wall_mat) # Vertical wall + vg.fill_box((10, 1, 5), (15, 4, 5), wall_mat) # Horizontal wall + + # Doorways (carve holes) + vg.fill_box((5, 1, 3), (5, 2, 4), 0) # Door in vertical wall + vg.fill_box((12, 1, 5), (13, 2, 5), 0) # Door in horizontal wall + + # Central pillars + vg.fill_box((8, 1, 8), (8, 4, 8), pillar_mat) + vg.fill_box((8, 1, 12), (8, 4, 12), pillar_mat) + + # Water pool in one corner (slow movement) + vg.fill_box((1, 0, 11), (3, 0, 14), water_mat) + + # Glass window + vg.fill_box((10, 2, 5), (11, 3, 5), glass_mat) + + # Raised platform in one area (height variation) + vg.fill_box((12, 1, 8), (14, 1, 13), floor_mat) # Platform at y=1 + + # ========================================================================= + # Create Viewport3D with navigation grid + # ========================================================================= + + viewport = mcrfpy.Viewport3D(pos=(10, 10), size=(600, 400)) + viewport.set_grid_size(16, 16) + viewport.cell_size = 1.0 + + # Configure camera for top-down view + viewport.camera_pos = (8, 15, 20) + viewport.camera_target = (8, 0, 8) + + # Add voxel layer + viewport.add_voxel_layer(vg, z_index=0) + + # Project voxels to navigation grid with headroom=2 (entity needs 2 voxels height) + viewport.project_voxel_to_nav(vg, headroom=2) + + # ========================================================================= + # Info panel + # ========================================================================= + + info_frame = mcrfpy.Frame(pos=(620, 10), size=(250, 400)) + info_frame.fill_color = mcrfpy.Color(30, 30, 40, 220) + info_frame.outline_color = mcrfpy.Color(100, 100, 120) + info_frame.outline = 2.0 + + title = mcrfpy.Caption(text="Nav Projection Demo", pos=(10, 10)) + title.fill_color = mcrfpy.Color(255, 255, 100) + + desc = mcrfpy.Caption(text="Voxels projected to\n2D nav grid", pos=(10, 35)) + desc.fill_color = mcrfpy.Color(200, 200, 200) + + info1 = mcrfpy.Caption(text="Grid: 16x16 cells", pos=(10, 75)) + info1.fill_color = mcrfpy.Color(150, 200, 255) + + info2 = mcrfpy.Caption(text="Headroom: 2 voxels", pos=(10, 95)) + info2.fill_color = mcrfpy.Color(150, 200, 255) + + # Count walkable cells + walkable_count = 0 + for x in range(16): + for z in range(16): + cell = viewport.at(x, z) + if cell.walkable: + walkable_count += 1 + + info3 = mcrfpy.Caption(text=f"Walkable: {walkable_count}/256", pos=(10, 115)) + info3.fill_color = mcrfpy.Color(100, 255, 100) + + # Find path example + path = viewport.find_path((1, 1), (13, 13)) + info4 = mcrfpy.Caption(text=f"Path length: {len(path)}", pos=(10, 135)) + info4.fill_color = mcrfpy.Color(255, 200, 100) + + # FOV example + fov = viewport.compute_fov((8, 8), 10) + info5 = mcrfpy.Caption(text=f"FOV cells: {len(fov)}", pos=(10, 155)) + info5.fill_color = mcrfpy.Color(200, 150, 255) + + # Legend + legend_title = mcrfpy.Caption(text="Materials:", pos=(10, 185)) + legend_title.fill_color = mcrfpy.Color(255, 255, 255) + + leg1 = mcrfpy.Caption(text=" Floor (walkable)", pos=(10, 205)) + leg1.fill_color = mcrfpy.Color(100, 80, 60) + + leg2 = mcrfpy.Caption(text=" Wall (blocking)", pos=(10, 225)) + leg2.fill_color = mcrfpy.Color(80, 80, 90) + + leg3 = mcrfpy.Caption(text=" Water (slow)", pos=(10, 245)) + leg3.fill_color = mcrfpy.Color(50, 100, 200) + + leg4 = mcrfpy.Caption(text=" Glass (see-through)", pos=(10, 265)) + leg4.fill_color = mcrfpy.Color(150, 200, 255) + + controls = mcrfpy.Caption(text="[Space] Recompute FOV\n[P] Show path\n[Q] Quit", pos=(10, 300)) + controls.fill_color = mcrfpy.Color(150, 150, 150) + + info_frame.children.extend([ + title, desc, info1, info2, info3, info4, info5, + legend_title, leg1, leg2, leg3, leg4, controls + ]) + + # ========================================================================= + # Status bar + # ========================================================================= + + status_frame = mcrfpy.Frame(pos=(10, 420), size=(860, 50)) + status_frame.fill_color = mcrfpy.Color(20, 20, 30, 220) + status_frame.outline_color = mcrfpy.Color(80, 80, 100) + status_frame.outline = 1.0 + + status_text = mcrfpy.Caption( + text="Milestone 12: VoxelGrid Navigation Projection - Project 3D voxels to 2D pathfinding grid", + pos=(10, 15) + ) + status_text.fill_color = mcrfpy.Color(180, 180, 200) + status_frame.children.append(status_text) + + # ========================================================================= + # Add elements to scene + # ========================================================================= + + scene.children.extend([viewport, info_frame, status_frame]) + + # Store references for interaction (using module-level globals) + global demo_viewport, demo_voxelgrid, demo_path, demo_fov_origin + demo_viewport = viewport + demo_voxelgrid = vg + demo_path = path + demo_fov_origin = (8, 8) + + # ========================================================================= + # Keyboard handler + # ========================================================================= + + def on_key(key, state): + global demo_fov_origin + if state != mcrfpy.InputState.PRESSED: + return + + if key == mcrfpy.Key.Q or key == mcrfpy.Key.ESCAPE: + # Exit + sys.exit(0) + elif key == mcrfpy.Key.SPACE: + # Recompute FOV from different origin + ox, oz = demo_fov_origin + ox = (ox + 3) % 14 + 1 + oz = (oz + 5) % 14 + 1 + demo_fov_origin = (ox, oz) + fov = demo_viewport.compute_fov((ox, oz), 8) + info5.text = f"FOV from ({ox},{oz}): {len(fov)}" + elif key == mcrfpy.Key.P: + # Show path info + print(f"Path from (1,1) to (13,13): {len(demo_path)} steps") + for i, (px, pz) in enumerate(demo_path[:10]): + cell = demo_viewport.at(px, pz) + print(f" Step {i}: ({px},{pz}) h={cell.height:.1f} cost={cell.cost:.1f}") + if len(demo_path) > 10: + print(f" ... and {len(demo_path) - 10} more steps") + + scene.on_key = on_key + + return scene + +def main(): + """Main entry point""" + print("=== Milestone 12: VoxelGrid Navigation Projection Demo ===") + print() + print("This demo shows how 3D voxel terrain is projected to a 2D") + print("navigation grid for pathfinding and FOV calculations.") + print() + print("The projection scans each column from top to bottom, finding") + print("the topmost walkable floor with adequate headroom.") + print() + + scene = create_demo_scene() + mcrfpy.current_scene = scene + + # Print nav grid summary + grid_w, grid_d = demo_viewport.grid_size + print("Navigation grid summary:") + print(f" Grid size: {grid_w}x{grid_d}") + + # Count by walkability and transparency + walkable = 0 + blocking = 0 + transparent = 0 + for x in range(grid_w): + for z in range(grid_d): + cell = demo_viewport.at(x, z) + if cell.walkable: + walkable += 1 + else: + blocking += 1 + if cell.transparent: + transparent += 1 + + print(f" Walkable cells: {walkable}") + print(f" Blocking cells: {blocking}") + print(f" Transparent cells: {transparent}") + print() + +if __name__ == "__main__": + main() + sys.exit(0) diff --git a/tests/demo/screens/voxel_serialization_demo.py b/tests/demo/screens/voxel_serialization_demo.py new file mode 100644 index 0000000..9d11b91 --- /dev/null +++ b/tests/demo/screens/voxel_serialization_demo.py @@ -0,0 +1,314 @@ +"""Voxel Serialization Demo - Milestone 14 + +Demonstrates save/load functionality for VoxelGrid, including: +- Saving to file with .mcvg format +- Loading from file +- Serialization to bytes (for network/custom storage) +- RLE compression effectiveness +""" + +import mcrfpy +import os +import tempfile + +def create_demo_scene(): + """Create a scene demonstrating voxel serialization.""" + scene = mcrfpy.Scene("voxel_serialization_demo") + ui = scene.children + + # Dark background + bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=(20, 20, 30)) + ui.append(bg) + + # Title + title = mcrfpy.Caption(text="Milestone 14: VoxelGrid Serialization", + pos=(30, 20)) + title.font_size = 28 + title.fill_color = (255, 220, 100) + ui.append(title) + + # Create demo VoxelGrid with interesting structure + grid = mcrfpy.VoxelGrid((16, 16, 16), cell_size=1.0) + + # Add materials + stone = grid.add_material("stone", (100, 100, 110)) + wood = grid.add_material("wood", (139, 90, 43)) + glass = grid.add_material("glass", (180, 200, 220, 100), transparent=True) + gold = grid.add_material("gold", (255, 215, 0)) + + # Build a small structure + grid.fill_box((0, 0, 0), (15, 0, 15), stone) # Floor + grid.fill_box((0, 1, 0), (0, 4, 15), stone) # Wall 1 + grid.fill_box((15, 1, 0), (15, 4, 15), stone) # Wall 2 + grid.fill_box((0, 1, 0), (15, 4, 0), stone) # Wall 3 + grid.fill_box((0, 1, 15), (15, 4, 15), stone) # Wall 4 + + # Windows (clear some wall, add glass) + grid.fill_box((6, 2, 0), (10, 3, 0), 0) # Clear for window + grid.fill_box((6, 2, 0), (10, 3, 0), glass) # Add glass + + # Pillars + grid.fill_box((4, 1, 4), (4, 3, 4), wood) + grid.fill_box((12, 1, 4), (12, 3, 4), wood) + grid.fill_box((4, 1, 12), (4, 3, 12), wood) + grid.fill_box((12, 1, 12), (12, 3, 12), wood) + + # Gold decorations + grid.set(8, 1, 8, gold) + grid.set(7, 1, 8, gold) + grid.set(9, 1, 8, gold) + grid.set(8, 1, 7, gold) + grid.set(8, 1, 9, gold) + + # Get original stats + original_voxels = grid.count_non_air() + original_materials = grid.material_count + + # === Test save/load to file === + with tempfile.NamedTemporaryFile(suffix='.mcvg', delete=False) as f: + temp_path = f.name + + save_success = grid.save(temp_path) + file_size = os.path.getsize(temp_path) if save_success else 0 + + # Load into new grid + loaded_grid = mcrfpy.VoxelGrid((1, 1, 1)) + load_success = loaded_grid.load(temp_path) + os.unlink(temp_path) # Clean up + + loaded_voxels = loaded_grid.count_non_air() if load_success else 0 + loaded_materials = loaded_grid.material_count if load_success else 0 + + # === Test to_bytes/from_bytes === + data_bytes = grid.to_bytes() + bytes_size = len(data_bytes) + + bytes_grid = mcrfpy.VoxelGrid((1, 1, 1)) + bytes_success = bytes_grid.from_bytes(data_bytes) + bytes_voxels = bytes_grid.count_non_air() if bytes_success else 0 + + # === Calculate compression === + raw_size = 16 * 16 * 16 # Uncompressed voxel data + compression_ratio = raw_size / bytes_size if bytes_size > 0 else 0 + + # Display information + y_pos = 80 + + # Original Grid Info + info1 = mcrfpy.Caption(text="Original VoxelGrid:", + pos=(30, y_pos)) + info1.font_size = 20 + info1.fill_color = (100, 200, 255) + ui.append(info1) + y_pos += 30 + + for line in [ + f" Dimensions: 16x16x16 = 4096 voxels", + f" Non-air voxels: {original_voxels}", + f" Materials defined: {original_materials}", + f" Structure: Walled room with pillars, windows, gold decor" + ]: + cap = mcrfpy.Caption(text=line, pos=(30, y_pos)) + cap.font_size = 16 + cap.fill_color = (200, 200, 210) + ui.append(cap) + y_pos += 22 + + y_pos += 20 + + # File Save/Load Results + info2 = mcrfpy.Caption(text="File Serialization (.mcvg):", + pos=(30, y_pos)) + info2.font_size = 20 + info2.fill_color = (100, 255, 150) + ui.append(info2) + y_pos += 30 + + save_status = "SUCCESS" if save_success else "FAILED" + load_status = "SUCCESS" if load_success else "FAILED" + match_status = "MATCH" if loaded_voxels == original_voxels else "MISMATCH" + + for line in [ + f" Save to file: {save_status}", + f" File size: {file_size} bytes", + f" Load from file: {load_status}", + f" Loaded voxels: {loaded_voxels} ({match_status})", + f" Loaded materials: {loaded_materials}" + ]: + color = (150, 255, 150) if "SUCCESS" in line or "MATCH" in line else (200, 200, 210) + if "FAILED" in line or "MISMATCH" in line: + color = (255, 100, 100) + cap = mcrfpy.Caption(text=line, pos=(30, y_pos)) + cap.font_size = 16 + cap.fill_color = color + ui.append(cap) + y_pos += 22 + + y_pos += 20 + + # Bytes Serialization Results + info3 = mcrfpy.Caption(text="Memory Serialization (to_bytes/from_bytes):", + pos=(30, y_pos)) + info3.font_size = 20 + info3.fill_color = (255, 200, 100) + ui.append(info3) + y_pos += 30 + + bytes_status = "SUCCESS" if bytes_success else "FAILED" + bytes_match = "MATCH" if bytes_voxels == original_voxels else "MISMATCH" + + for line in [ + f" Serialized size: {bytes_size} bytes", + f" Raw voxel data: {raw_size} bytes", + f" Compression ratio: {compression_ratio:.1f}x", + f" from_bytes(): {bytes_status}", + f" Restored voxels: {bytes_voxels} ({bytes_match})" + ]: + color = (200, 200, 210) + if "SUCCESS" in line or "MATCH" in line: + color = (150, 255, 150) + cap = mcrfpy.Caption(text=line, pos=(30, y_pos)) + cap.font_size = 16 + cap.fill_color = color + ui.append(cap) + y_pos += 22 + + y_pos += 20 + + # RLE Compression Demo + info4 = mcrfpy.Caption(text="RLE Compression Effectiveness:", + pos=(30, y_pos)) + info4.font_size = 20 + info4.fill_color = (200, 150, 255) + ui.append(info4) + y_pos += 30 + + # Create uniform grid for compression test + uniform_grid = mcrfpy.VoxelGrid((32, 32, 32)) + uniform_mat = uniform_grid.add_material("solid", (128, 128, 128)) + uniform_grid.fill(uniform_mat) + uniform_bytes = uniform_grid.to_bytes() + uniform_raw = 32 * 32 * 32 + uniform_ratio = uniform_raw / len(uniform_bytes) + + for line in [ + f" Uniform 32x32x32 filled grid:", + f" Raw: {uniform_raw} bytes", + f" Compressed: {len(uniform_bytes)} bytes", + f" Compression: {uniform_ratio:.0f}x", + f" ", + f" RLE excels at runs of identical values." + ]: + cap = mcrfpy.Caption(text=line, pos=(30, y_pos)) + cap.font_size = 16 + cap.fill_color = (200, 180, 220) + ui.append(cap) + y_pos += 22 + + y_pos += 30 + + # File Format Info + info5 = mcrfpy.Caption(text="File Format (.mcvg):", + pos=(30, y_pos)) + info5.font_size = 20 + info5.fill_color = (255, 150, 200) + ui.append(info5) + y_pos += 30 + + for line in [ + " Header: Magic 'MCVG' + version + dimensions + cell_size", + " Materials: name, color (RGBA), sprite_index, transparent, path_cost", + " Voxel data: RLE-encoded material IDs", + " ", + " Note: Transform (offset, rotation) is runtime state, not serialized" + ]: + cap = mcrfpy.Caption(text=line, pos=(30, y_pos)) + cap.font_size = 14 + cap.fill_color = (200, 180, 200) + ui.append(cap) + y_pos += 20 + + # API Reference on right side + y_ref = 80 + x_ref = 550 + + api_title = mcrfpy.Caption(text="Python API:", pos=(x_ref, y_ref)) + api_title.font_size = 20 + api_title.fill_color = (150, 200, 255) + ui.append(api_title) + y_ref += 35 + + for line in [ + "# Save to file", + "success = grid.save('world.mcvg')", + "", + "# Load from file", + "grid = VoxelGrid((1,1,1))", + "success = grid.load('world.mcvg')", + "", + "# Save to bytes", + "data = grid.to_bytes()", + "", + "# Load from bytes", + "success = grid.from_bytes(data)", + "", + "# Network example:", + "# send_to_server(grid.to_bytes())", + "# data = recv_from_server()", + "# grid.from_bytes(data)" + ]: + cap = mcrfpy.Caption(text=line, pos=(x_ref, y_ref)) + cap.font_size = 14 + if line.startswith("#"): + cap.fill_color = (100, 150, 100) + elif "=" in line or "(" in line: + cap.fill_color = (255, 220, 150) + else: + cap.fill_color = (180, 180, 180) + ui.append(cap) + y_ref += 18 + + return scene + + +# Run demonstration +if __name__ == "__main__": + import sys + # Create and activate the scene + scene = create_demo_scene() + mcrfpy.current_scene = scene + + # When run directly, print summary and exit for headless testing + print("\n=== Voxel Serialization Demo (Milestone 14) ===\n") + + # Run a quick verification + grid = mcrfpy.VoxelGrid((8, 8, 8)) + mat = grid.add_material("test", (100, 100, 100)) + grid.fill_box((0, 0, 0), (7, 0, 7), mat) + + print(f"Created 8x8x8 grid with {grid.count_non_air()} non-air voxels") + + # Test to_bytes + data = grid.to_bytes() + print(f"Serialized to {len(data)} bytes") + + # Test from_bytes + grid2 = mcrfpy.VoxelGrid((1, 1, 1)) + success = grid2.from_bytes(data) + print(f"from_bytes(): {'SUCCESS' if success else 'FAILED'}") + print(f"Restored size: {grid2.size}") + print(f"Restored voxels: {grid2.count_non_air()}") + + # Compression test + big_grid = mcrfpy.VoxelGrid((32, 32, 32)) + big_mat = big_grid.add_material("solid", (128, 128, 128)) + big_grid.fill(big_mat) + big_data = big_grid.to_bytes() + raw_size = 32 * 32 * 32 + print(f"\nCompression test (32x32x32 uniform):") + print(f" Raw: {raw_size} bytes") + print(f" Compressed: {len(big_data)} bytes") + print(f" Ratio: {raw_size / len(big_data):.0f}x") + + print("\n=== Demo complete ===") + sys.exit(0)