diff --git a/src/3d/Billboard.cpp b/src/3d/Billboard.cpp deleted file mode 100644 index 5937212..0000000 --- a/src/3d/Billboard.cpp +++ /dev/null @@ -1,596 +0,0 @@ -// Billboard.cpp - Camera-facing 3D sprite implementation - -#include "Billboard.h" -#include "Shader3D.h" -#include "MeshLayer.h" // For MeshVertex -#include "../platform/GLContext.h" -#include "../PyTexture.h" -#include "../PyTypeCache.h" -#include -#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 deleted file mode 100644 index f837baa..0000000 --- a/src/3d/Billboard.h +++ /dev/null @@ -1,229 +0,0 @@ -// Billboard.h - Camera-facing 3D sprite for McRogueFace -// Supports camera-facing rotation modes for trees, items, particles, etc. - -#pragma once - -#include "Common.h" -#include "Math3D.h" -#include "Python.h" -#include "structmember.h" -#include - -// 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 fe15720..633b690 100644 --- a/src/3d/Entity3D.cpp +++ b/src/3d/Entity3D.cpp @@ -3,8 +3,6 @@ #include "Entity3D.h" #include "Viewport3D.h" #include "VoxelPoint.h" -#include "Model3D.h" -#include "Shader3D.h" #include "PyVector.h" #include "PyColor.h" #include "PythonObjectCache.h" @@ -56,10 +54,6 @@ Entity3D::~Entity3D() { // Cleanup cube geometry when last entity is destroyed? // For now, leave it - it's shared static data - - // Clean up Python animation callback - Py_XDECREF(py_anim_callback_); - py_anim_callback_ = nullptr; } // ============================================================================= @@ -287,27 +281,23 @@ void Entity3D::processNextMove() void Entity3D::update(float dt) { - // Update movement animation - if (is_animating_) { - move_progress_ += dt * move_speed_; + if (!is_animating_) return; - if (move_progress_ >= 1.0f) { - // Animation complete - world_pos_ = target_world_pos_; - is_animating_ = false; + move_progress_ += dt * move_speed_; - // Process next move in queue - if (!move_queue_.empty()) { - processNextMove(); - } - } else { - // Interpolate position - world_pos_ = vec3::lerp(move_start_pos_, target_world_pos_, move_progress_); + if (move_progress_ >= 1.0f) { + // Animation complete + world_pos_ = target_world_pos_; + is_animating_ = false; + + // Process next move in queue + if (!move_queue_.empty()) { + processNextMove(); } + } else { + // Interpolate position + world_pos_ = vec3::lerp(move_start_pos_, target_world_pos_, move_progress_); } - - // Update skeletal animation - updateAnimation(dt); } bool Entity3D::setProperty(const std::string& name, float value) @@ -394,111 +384,6 @@ bool Entity3D::hasProperty(const std::string& name) const name == "sprite_index" || name == "visible"; } -// ============================================================================= -// Skeletal Animation -// ============================================================================= - -void Entity3D::setAnimClip(const std::string& name) -{ - if (anim_clip_ == name) return; - - anim_clip_ = name; - anim_time_ = 0.0f; - anim_paused_ = false; - - // Initialize bone matrices if model has skeleton - if (model_ && model_->hasSkeleton()) { - size_t bone_count = model_->getBoneCount(); - bone_matrices_.resize(bone_count); - for (auto& m : bone_matrices_) { - m = mat4::identity(); - } - } -} - -void Entity3D::updateAnimation(float dt) -{ - // Handle auto-animate (play walk/idle based on movement state) - if (auto_animate_ && model_ && model_->hasSkeleton()) { - bool currently_moving = isMoving(); - if (currently_moving != was_moving_) { - was_moving_ = currently_moving; - if (currently_moving) { - // Started moving - play walk clip - if (model_->findClip(walk_clip_)) { - setAnimClip(walk_clip_); - } - } else { - // Stopped moving - play idle clip - if (model_->findClip(idle_clip_)) { - setAnimClip(idle_clip_); - } - } - } - } - - // Early out if no model, no skeleton, or no animation - if (!model_ || !model_->hasSkeleton()) return; - if (anim_clip_.empty() || anim_paused_) return; - - const AnimationClip* clip = model_->findClip(anim_clip_); - if (!clip) return; - - // Advance time - anim_time_ += dt * anim_speed_; - - // Handle loop/completion - if (anim_time_ >= clip->duration) { - if (anim_loop_) { - anim_time_ = std::fmod(anim_time_, clip->duration); - } else { - anim_time_ = clip->duration; - anim_paused_ = true; - - // Fire callback - if (on_anim_complete_) { - on_anim_complete_(this, anim_clip_); - } - - // Fire Python callback - if (py_anim_callback_) { - PyObject* result = PyObject_CallFunction(py_anim_callback_, "(Os)", - self, anim_clip_.c_str()); - if (result) { - Py_DECREF(result); - } else { - PyErr_Print(); - } - } - } - } - - // Sample animation - const Skeleton& skeleton = model_->getSkeleton(); - const std::vector& 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 // ============================================================================= @@ -582,30 +467,6 @@ void Entity3D::render(const mat4& view, const mat4& proj, unsigned int shader) { if (!visible_) return; - // Set entity color uniform (used by Model3D and placeholder) - int colorLoc = glGetUniformLocation(shader, "u_entityColor"); - if (colorLoc >= 0) { - glUniform4f(colorLoc, - color_.r / 255.0f, - color_.g / 255.0f, - color_.b / 255.0f, - color_.a / 255.0f); - } - - // If we have a model, use it - if (model_) { - mat4 model = getModelMatrix(); - - // Use skinned rendering if model has skeleton and we have bone matrices - if (model_->hasSkeleton() && !bone_matrices_.empty()) { - model_->renderSkinned(shader, model, view, proj, bone_matrices_); - } else { - model_->render(shader, model, view, proj); - } - return; - } - - // Otherwise, fall back to placeholder cube // Initialize cube geometry if needed if (!cubeInitialized_) { initCubeGeometry(); @@ -618,9 +479,17 @@ void Entity3D::render(const mat4& view, const mat4& proj, unsigned int shader) // Get uniform locations (assuming shader is already bound) int mvpLoc = glGetUniformLocation(shader, "u_mvp"); int modelLoc = glGetUniformLocation(shader, "u_model"); + int colorLoc = glGetUniformLocation(shader, "u_entityColor"); if (mvpLoc >= 0) glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, mvp.data()); if (modelLoc >= 0) glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.data()); + if (colorLoc >= 0) { + glUniform4f(colorLoc, + color_.r / 255.0f, + color_.g / 255.0f, + color_.b / 255.0f, + color_.a / 255.0f); + } // Bind VBO and set up attributes glBindBuffer(GL_ARRAY_BUFFER, cubeVBO_); @@ -846,182 +715,6 @@ PyObject* Entity3D::get_viewport(PyEntity3DObject* self, void* closure) Py_RETURN_NONE; } -PyObject* Entity3D::get_model(PyEntity3DObject* self, void* closure) -{ - auto model = self->data->getModel(); - if (!model) { - Py_RETURN_NONE; - } - - // Create Python Model3D object wrapping the shared_ptr - PyTypeObject* type = &mcrfpydef::PyModel3DType; - PyModel3DObject* obj = (PyModel3DObject*)type->tp_alloc(type, 0); - if (!obj) return NULL; - - obj->data = model; - obj->weakreflist = nullptr; - - return (PyObject*)obj; -} - -int Entity3D::set_model(PyEntity3DObject* self, PyObject* value, void* closure) -{ - if (value == Py_None) { - self->data->setModel(nullptr); - return 0; - } - - if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyModel3DType)) { - PyErr_SetString(PyExc_TypeError, "model must be a Model3D or None"); - return -1; - } - - PyModel3DObject* model_obj = (PyModel3DObject*)value; - self->data->setModel(model_obj->data); - return 0; -} - -// Animation property getters/setters - -PyObject* Entity3D::get_anim_clip(PyEntity3DObject* self, void* closure) -{ - return PyUnicode_FromString(self->data->getAnimClip().c_str()); -} - -int Entity3D::set_anim_clip(PyEntity3DObject* self, PyObject* value, void* closure) -{ - if (!PyUnicode_Check(value)) { - PyErr_SetString(PyExc_TypeError, "anim_clip must be a string"); - return -1; - } - self->data->setAnimClip(PyUnicode_AsUTF8(value)); - return 0; -} - -PyObject* Entity3D::get_anim_time(PyEntity3DObject* self, void* closure) -{ - return PyFloat_FromDouble(self->data->getAnimTime()); -} - -int Entity3D::set_anim_time(PyEntity3DObject* self, PyObject* value, void* closure) -{ - if (!PyNumber_Check(value)) { - PyErr_SetString(PyExc_TypeError, "anim_time must be a number"); - return -1; - } - self->data->setAnimTime((float)PyFloat_AsDouble(value)); - return 0; -} - -PyObject* Entity3D::get_anim_speed(PyEntity3DObject* self, void* closure) -{ - return PyFloat_FromDouble(self->data->getAnimSpeed()); -} - -int Entity3D::set_anim_speed(PyEntity3DObject* self, PyObject* value, void* closure) -{ - if (!PyNumber_Check(value)) { - PyErr_SetString(PyExc_TypeError, "anim_speed must be a number"); - return -1; - } - self->data->setAnimSpeed((float)PyFloat_AsDouble(value)); - return 0; -} - -PyObject* Entity3D::get_anim_loop(PyEntity3DObject* self, void* closure) -{ - return PyBool_FromLong(self->data->getAnimLoop() ? 1 : 0); -} - -int Entity3D::set_anim_loop(PyEntity3DObject* self, PyObject* value, void* closure) -{ - self->data->setAnimLoop(PyObject_IsTrue(value)); - return 0; -} - -PyObject* Entity3D::get_anim_paused(PyEntity3DObject* self, void* closure) -{ - return PyBool_FromLong(self->data->getAnimPaused() ? 1 : 0); -} - -int Entity3D::set_anim_paused(PyEntity3DObject* self, PyObject* value, void* closure) -{ - self->data->setAnimPaused(PyObject_IsTrue(value)); - return 0; -} - -PyObject* Entity3D::get_anim_frame(PyEntity3DObject* self, void* closure) -{ - return PyLong_FromLong(self->data->getAnimFrame()); -} - -PyObject* Entity3D::get_on_anim_complete(PyEntity3DObject* self, void* closure) -{ - if (self->data->py_anim_callback_) { - Py_INCREF(self->data->py_anim_callback_); - return self->data->py_anim_callback_; - } - Py_RETURN_NONE; -} - -int Entity3D::set_on_anim_complete(PyEntity3DObject* self, PyObject* value, void* closure) -{ - // Clear existing callback - Py_XDECREF(self->data->py_anim_callback_); - - if (value == Py_None) { - self->data->py_anim_callback_ = nullptr; - } else if (PyCallable_Check(value)) { - Py_INCREF(value); - self->data->py_anim_callback_ = value; - } else { - PyErr_SetString(PyExc_TypeError, "on_anim_complete must be callable or None"); - return -1; - } - return 0; -} - -PyObject* Entity3D::get_auto_animate(PyEntity3DObject* self, void* closure) -{ - return PyBool_FromLong(self->data->getAutoAnimate() ? 1 : 0); -} - -int Entity3D::set_auto_animate(PyEntity3DObject* self, PyObject* value, void* closure) -{ - self->data->setAutoAnimate(PyObject_IsTrue(value)); - return 0; -} - -PyObject* Entity3D::get_walk_clip(PyEntity3DObject* self, void* closure) -{ - return PyUnicode_FromString(self->data->getWalkClip().c_str()); -} - -int Entity3D::set_walk_clip(PyEntity3DObject* self, PyObject* value, void* closure) -{ - if (!PyUnicode_Check(value)) { - PyErr_SetString(PyExc_TypeError, "walk_clip must be a string"); - return -1; - } - self->data->setWalkClip(PyUnicode_AsUTF8(value)); - return 0; -} - -PyObject* Entity3D::get_idle_clip(PyEntity3DObject* self, void* closure) -{ - return PyUnicode_FromString(self->data->getIdleClip().c_str()); -} - -int Entity3D::set_idle_clip(PyEntity3DObject* self, PyObject* value, void* closure) -{ - if (!PyUnicode_Check(value)) { - PyErr_SetString(PyExc_TypeError, "idle_clip must be a string"); - return -1; - } - self->data->setIdleClip(PyUnicode_AsUTF8(value)); - return 0; -} - // Methods PyObject* Entity3D::py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds) @@ -1121,47 +814,6 @@ PyObject* Entity3D::py_animate(PyEntity3DObject* self, PyObject* args, PyObject* return NULL; } -PyObject* Entity3D::py_follow_path(PyEntity3DObject* self, PyObject* args) -{ - PyObject* path_list; - if (!PyArg_ParseTuple(args, "O", &path_list)) { - return NULL; - } - - if (!PyList_Check(path_list)) { - PyErr_SetString(PyExc_TypeError, "follow_path() requires a list of (x, z) tuples"); - return NULL; - } - - std::vector> 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[] = { @@ -1182,14 +834,6 @@ PyMethodDef Entity3D::methods[] = { {"animate", (PyCFunction)Entity3D::py_animate, METH_VARARGS | METH_KEYWORDS, "animate(property, target, duration, easing=None, callback=None)\n\n" "Animate a property over time. (Not yet implemented)"}, - {"follow_path", (PyCFunction)Entity3D::py_follow_path, METH_VARARGS, - "follow_path(path)\n\n" - "Queue path positions for smooth movement.\n\n" - "Args:\n" - " path: List of (x, z) tuples (as returned by path_to())"}, - {"clear_path", (PyCFunction)Entity3D::py_clear_path, METH_NOARGS, - "clear_path()\n\n" - "Clear the movement queue and stop at current position."}, {NULL} // Sentinel }; @@ -1210,33 +854,6 @@ PyGetSetDef Entity3D::getsetters[] = { "Entity render color.", NULL}, {"viewport", (getter)Entity3D::get_viewport, NULL, "Owning Viewport3D (read-only).", NULL}, - {"model", (getter)Entity3D::get_model, (setter)Entity3D::set_model, - "3D model (Model3D). If None, uses placeholder cube.", NULL}, - - // Animation properties - {"anim_clip", (getter)Entity3D::get_anim_clip, (setter)Entity3D::set_anim_clip, - "Current animation clip name. Set to play an animation.", NULL}, - {"anim_time", (getter)Entity3D::get_anim_time, (setter)Entity3D::set_anim_time, - "Current time position in animation (seconds).", NULL}, - {"anim_speed", (getter)Entity3D::get_anim_speed, (setter)Entity3D::set_anim_speed, - "Animation playback speed multiplier. 1.0 = normal speed.", NULL}, - {"anim_loop", (getter)Entity3D::get_anim_loop, (setter)Entity3D::set_anim_loop, - "Whether animation loops when it reaches the end.", NULL}, - {"anim_paused", (getter)Entity3D::get_anim_paused, (setter)Entity3D::set_anim_paused, - "Whether animation playback is paused.", NULL}, - {"anim_frame", (getter)Entity3D::get_anim_frame, NULL, - "Current animation frame number (read-only, approximate at 30fps).", NULL}, - {"on_anim_complete", (getter)Entity3D::get_on_anim_complete, (setter)Entity3D::set_on_anim_complete, - "Callback(entity, clip_name) when non-looping animation ends.", NULL}, - {"auto_animate", (getter)Entity3D::get_auto_animate, (setter)Entity3D::set_auto_animate, - "Enable auto-play of walk/idle clips based on movement.", NULL}, - {"walk_clip", (getter)Entity3D::get_walk_clip, (setter)Entity3D::set_walk_clip, - "Animation clip to play when entity is moving.", NULL}, - {"idle_clip", (getter)Entity3D::get_idle_clip, (setter)Entity3D::set_idle_clip, - "Animation clip to play when entity is stationary.", NULL}, - {"is_moving", (getter)Entity3D::get_is_moving, NULL, - "Whether entity is currently moving (read-only).", NULL}, - {NULL} // Sentinel }; diff --git a/src/3d/Entity3D.h b/src/3d/Entity3D.h index ce1d69a..c317f5b 100644 --- a/src/3d/Entity3D.h +++ b/src/3d/Entity3D.h @@ -11,13 +11,11 @@ #include #include #include -#include namespace mcrf { // Forward declarations class Viewport3D; -class Model3D; } // namespace mcrf @@ -96,10 +94,6 @@ public: int getSpriteIndex() const { return sprite_index_; } void setSpriteIndex(int idx) { sprite_index_ = idx; } - // 3D model (if null, uses placeholder cube) - std::shared_ptr getModel() const { return model_; } - void setModel(std::shared_ptr m) { model_ = m; } - // ========================================================================= // Viewport Integration // ========================================================================= @@ -159,57 +153,6 @@ public: bool getProperty(const std::string& name, float& value) const; bool hasProperty(const std::string& name) const; - // ========================================================================= - // Skeletal Animation - // ========================================================================= - - /// Get current animation clip name - const std::string& getAnimClip() const { return anim_clip_; } - - /// Set animation clip by name (starts playing) - void setAnimClip(const std::string& name); - - /// Get/set animation time (position in clip) - float getAnimTime() const { return anim_time_; } - void setAnimTime(float t) { anim_time_ = t; } - - /// Get/set playback speed (1.0 = normal) - float getAnimSpeed() const { return anim_speed_; } - void setAnimSpeed(float s) { anim_speed_ = s; } - - /// Get/set looping state - bool getAnimLoop() const { return anim_loop_; } - void setAnimLoop(bool l) { anim_loop_ = l; } - - /// Get/set pause state - bool getAnimPaused() const { return anim_paused_; } - void setAnimPaused(bool p) { anim_paused_ = p; } - - /// Get current animation frame (approximate) - int getAnimFrame() const; - - /// Update skeletal animation (call before render) - void updateAnimation(float dt); - - /// Get computed bone matrices for shader - const std::vector& 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 // ========================================================================= @@ -242,29 +185,6 @@ public: static PyObject* get_color(PyEntity3DObject* self, void* closure); static int set_color(PyEntity3DObject* self, PyObject* value, void* closure); static PyObject* get_viewport(PyEntity3DObject* self, void* closure); - static PyObject* get_model(PyEntity3DObject* self, void* closure); - static int set_model(PyEntity3DObject* self, PyObject* value, void* closure); - - // Animation property getters/setters - static PyObject* get_anim_clip(PyEntity3DObject* self, void* closure); - static int set_anim_clip(PyEntity3DObject* self, PyObject* value, void* closure); - static PyObject* get_anim_time(PyEntity3DObject* self, void* closure); - static int set_anim_time(PyEntity3DObject* self, PyObject* value, void* closure); - static PyObject* get_anim_speed(PyEntity3DObject* self, void* closure); - static int set_anim_speed(PyEntity3DObject* self, PyObject* value, void* closure); - static PyObject* get_anim_loop(PyEntity3DObject* self, void* closure); - static int set_anim_loop(PyEntity3DObject* self, PyObject* value, void* closure); - static PyObject* get_anim_paused(PyEntity3DObject* self, void* closure); - static int set_anim_paused(PyEntity3DObject* self, PyObject* value, void* closure); - static PyObject* get_anim_frame(PyEntity3DObject* self, void* closure); - static PyObject* get_on_anim_complete(PyEntity3DObject* self, void* closure); - static int set_on_anim_complete(PyEntity3DObject* self, PyObject* value, void* closure); - static PyObject* get_auto_animate(PyEntity3DObject* self, void* closure); - static int set_auto_animate(PyEntity3DObject* self, PyObject* value, void* closure); - static PyObject* get_walk_clip(PyEntity3DObject* self, void* closure); - static int set_walk_clip(PyEntity3DObject* self, PyObject* value, void* closure); - static PyObject* get_idle_clip(PyEntity3DObject* self, void* closure); - static int set_idle_clip(PyEntity3DObject* self, PyObject* value, void* closure); // Methods static PyObject* py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds); @@ -272,9 +192,6 @@ public: static PyObject* py_at(PyEntity3DObject* self, PyObject* args, PyObject* kwds); static PyObject* py_update_visibility(PyEntity3DObject* self, PyObject* args); static PyObject* py_animate(PyEntity3DObject* self, PyObject* args, PyObject* kwds); - static PyObject* py_follow_path(PyEntity3DObject* self, PyObject* args); - static PyObject* py_clear_path(PyEntity3DObject* self, PyObject* args); - static PyObject* get_is_moving(PyEntity3DObject* self, void* closure); static PyMethodDef methods[]; static PyGetSetDef getsetters[]; @@ -300,7 +217,6 @@ private: bool visible_ = true; sf::Color color_ = sf::Color(200, 100, 50); // Default orange int sprite_index_ = 0; - std::shared_ptr model_; // 3D model (null = placeholder cube) // Viewport (weak reference to avoid cycles) std::weak_ptr viewport_; @@ -316,24 +232,6 @@ private: float move_speed_ = 5.0f; // Cells per second vec3 move_start_pos_; - // Skeletal animation state - std::string anim_clip_; // Current animation clip name - float anim_time_ = 0.0f; // Current time in animation - float anim_speed_ = 1.0f; // Playback speed multiplier - bool anim_loop_ = true; // Loop animation - bool anim_paused_ = false; // Pause playback - std::vector 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 5f6d9f8..c337343 100644 --- a/src/3d/Math3D.h +++ b/src/3d/Math3D.h @@ -610,116 +610,6 @@ struct quat { } }; -// ============================================================================= -// Frustum - View frustum for culling -// ============================================================================= - -struct Plane { - vec3 normal; - float distance; - - Plane() : normal(0, 1, 0), distance(0) {} - Plane(const vec3& n, float d) : normal(n), distance(d) {} - - // Distance from plane to point (positive = in front, negative = behind) - float distanceToPoint(const vec3& point) const { - return normal.dot(point) + distance; - } -}; - -struct Frustum { - // Six planes: left, right, bottom, top, near, far - Plane planes[6]; - - // Extract frustum planes from view-projection matrix - // Uses Gribb/Hartmann method - void extractFromMatrix(const mat4& viewProj) { - const float* m = viewProj.m; - - // Left plane - planes[0].normal.x = m[3] + m[0]; - planes[0].normal.y = m[7] + m[4]; - planes[0].normal.z = m[11] + m[8]; - planes[0].distance = m[15] + m[12]; - - // Right plane - planes[1].normal.x = m[3] - m[0]; - planes[1].normal.y = m[7] - m[4]; - planes[1].normal.z = m[11] - m[8]; - planes[1].distance = m[15] - m[12]; - - // Bottom plane - planes[2].normal.x = m[3] + m[1]; - planes[2].normal.y = m[7] + m[5]; - planes[2].normal.z = m[11] + m[9]; - planes[2].distance = m[15] + m[13]; - - // Top plane - planes[3].normal.x = m[3] - m[1]; - planes[3].normal.y = m[7] - m[5]; - planes[3].normal.z = m[11] - m[9]; - planes[3].distance = m[15] - m[13]; - - // Near plane - planes[4].normal.x = m[3] + m[2]; - planes[4].normal.y = m[7] + m[6]; - planes[4].normal.z = m[11] + m[10]; - planes[4].distance = m[15] + m[14]; - - // Far plane - planes[5].normal.x = m[3] - m[2]; - planes[5].normal.y = m[7] - m[6]; - planes[5].normal.z = m[11] - m[10]; - planes[5].distance = m[15] - m[14]; - - // Normalize all planes - for (int i = 0; i < 6; i++) { - float len = planes[i].normal.length(); - if (len > 0.0001f) { - planes[i].normal = planes[i].normal / len; - planes[i].distance /= len; - } - } - } - - // Test if a point is inside the frustum - bool containsPoint(const vec3& point) const { - for (int i = 0; i < 6; i++) { - if (planes[i].distanceToPoint(point) < 0) { - return false; - } - } - return true; - } - - // Test if a sphere intersects or is inside the frustum - bool containsSphere(const vec3& center, float radius) const { - for (int i = 0; i < 6; i++) { - if (planes[i].distanceToPoint(center) < -radius) { - return false; // Sphere is completely behind this plane - } - } - return true; - } - - // Test if an axis-aligned bounding box intersects the frustum - bool containsAABB(const vec3& min, const vec3& max) const { - for (int i = 0; i < 6; i++) { - // Find the positive vertex (furthest along plane normal) - vec3 pVertex; - pVertex.x = (planes[i].normal.x >= 0) ? max.x : min.x; - pVertex.y = (planes[i].normal.y >= 0) ? max.y : min.y; - pVertex.z = (planes[i].normal.z >= 0) ? max.z : min.z; - - // If positive vertex is behind plane, box is outside - if (planes[i].distanceToPoint(pVertex) < 0) { - return false; - } - } - return true; - } -}; - // ============================================================================= // Utility constants and functions // ============================================================================= diff --git a/src/3d/MeshLayer.cpp b/src/3d/MeshLayer.cpp index 7314a14..faabcb1 100644 --- a/src/3d/MeshLayer.cpp +++ b/src/3d/MeshLayer.cpp @@ -1,11 +1,8 @@ // MeshLayer.cpp - Static 3D geometry layer implementation #include "MeshLayer.h" -#include "Model3D.h" -#include "Viewport3D.h" #include "Shader3D.h" #include "../platform/GLContext.h" -#include // GL headers based on backend #if defined(MCRF_SDL2) @@ -380,57 +377,53 @@ void MeshLayer::uploadToGPU() { // Rendering // ============================================================================= -void MeshLayer::render(unsigned int shader, const mat4& model, const mat4& view, const mat4& projection) { +void MeshLayer::render(const mat4& model, const mat4& view, const mat4& projection) { #ifdef MCRF_HAS_GL - if (!gl::isGLReady()) { + if (!gl::isGLReady() || vertices_.empty()) { return; } - // Render terrain geometry if present - if (!vertices_.empty()) { - // Upload to GPU if needed - if (dirty_ || vbo_ == 0) { - uploadToGPU(); - } - - if (vbo_ != 0) { - // Bind VBO - glBindBuffer(GL_ARRAY_BUFFER, vbo_); - - // Vertex format: pos(3) + texcoord(2) + normal(3) + color(4) = 12 floats = 48 bytes - int stride = sizeof(MeshVertex); - - // Set up vertex attributes - glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION); - glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, - stride, reinterpret_cast(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); - } + // Upload to GPU if needed + if (dirty_ || vbo_ == 0) { + uploadToGPU(); } - // Render mesh instances - renderMeshInstances(shader, view, projection); + if (vbo_ == 0) { + return; + } + + // Bind VBO + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + + // Vertex format: pos(3) + texcoord(2) + normal(3) + color(4) = 12 floats = 48 bytes + int stride = sizeof(MeshVertex); + + // Set up vertex attributes + glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION); + glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, + stride, reinterpret_cast(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); #endif } @@ -453,114 +446,6 @@ vec3 MeshLayer::computeFaceNormal(const vec3& v0, const vec3& v1, const vec3& v2 return edge1.cross(edge2).normalized(); } -// ============================================================================= -// Mesh Instances -// ============================================================================= - -size_t MeshLayer::addMesh(std::shared_ptr 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 55e86ca..f098f0f 100644 --- a/src/3d/MeshLayer.h +++ b/src/3d/MeshLayer.h @@ -10,12 +10,6 @@ #include #include // For TCOD_heightmap_t -// Forward declarations -namespace mcrf { -class Viewport3D; -class Model3D; -} - namespace mcrf { // ============================================================================= @@ -55,25 +49,6 @@ struct TextureRange { : minHeight(min), maxHeight(max), spriteIndex(index) {} }; -// ============================================================================= -// MeshInstance - Instance of a Model3D placed in the world -// ============================================================================= - -struct MeshInstance { - std::shared_ptr 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 // ============================================================================= @@ -140,60 +115,6 @@ public: /// Clear all geometry void clear(); - // ========================================================================= - // Mesh Instances (Model3D placement) - // ========================================================================= - - /// Add a Model3D instance at a world position - /// @param model The Model3D to render - /// @param pos World position - /// @param rotation Y-axis rotation in degrees - /// @param scale Scale factor (uniform or per-axis) - /// @return Instance index for later removal - size_t addMesh(std::shared_ptr 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 // ========================================================================= @@ -203,11 +124,10 @@ public: void uploadToGPU(); /// Render this layer - /// @param shader Shader program handle (for mesh instances) /// @param model Model transformation matrix /// @param view View matrix from camera /// @param projection Projection matrix from camera - void render(unsigned int shader, const mat4& model, const mat4& view, const mat4& projection); + void render(const mat4& model, const mat4& view, const mat4& projection); /// Get model matrix (identity by default, override for positioned layers) mat4 getModelMatrix() const { return modelMatrix_; } @@ -244,19 +164,10 @@ private: // Transform mat4 modelMatrix_ = mat4::identity(); - // Mesh instances (Model3D placements) - std::vector 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 deleted file mode 100644 index 1f21f9b..0000000 --- a/src/3d/Model3D.cpp +++ /dev/null @@ -1,1451 +0,0 @@ -// 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 deleted file mode 100644 index 7fb9bb2..0000000 --- a/src/3d/Model3D.h +++ /dev/null @@ -1,431 +0,0 @@ -// Model3D.h - 3D model resource for McRogueFace -// Supports loading from glTF 2.0 (.glb) files and procedural primitives - -#pragma once - -#include "Common.h" -#include "Math3D.h" -#include "MeshLayer.h" // For MeshVertex -#include "Python.h" -#include "structmember.h" -#include -#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 deleted file mode 100644 index f3d4a65..0000000 --- a/src/3d/PyVoxelGrid.cpp +++ /dev/null @@ -1,1271 +0,0 @@ -// 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 deleted file mode 100644 index dd99cf6..0000000 --- a/src/3d/PyVoxelGrid.h +++ /dev/null @@ -1,179 +0,0 @@ -// PyVoxelGrid.h - Python bindings for VoxelGrid -// Part of McRogueFace 3D Extension - Milestones 9, 11 -#pragma once - -#include "../Common.h" -#include "Python.h" -#include "VoxelGrid.h" -#include - -// ============================================================================= -// 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 5729051..c842e4c 100644 --- a/src/3d/Shader3D.cpp +++ b/src/3d/Shader3D.cpp @@ -245,223 +245,6 @@ void main() { } )"; -// ============================================================================= -// Skinned Vertex Shaders (for skeletal animation) -// ============================================================================= - -const char* PS1_SKINNED_VERTEX_ES2 = R"( -// PS1-style skinned vertex shader for OpenGL ES 2.0 / WebGL 1.0 -precision mediump float; - -uniform mat4 u_model; -uniform mat4 u_view; -uniform mat4 u_projection; -uniform mat4 u_bones[32]; -uniform vec2 u_resolution; -uniform bool u_enable_snap; -uniform float u_fog_start; -uniform float u_fog_end; -uniform vec3 u_light_dir; -uniform vec3 u_ambient; - -attribute vec3 a_position; -attribute vec2 a_texcoord; -attribute vec3 a_normal; -attribute vec4 a_color; -attribute vec4 a_bone_ids; -attribute vec4 a_bone_weights; - -varying vec4 v_color; -varying vec2 v_texcoord; -varying float v_w; -varying float v_fog; - -mat4 getBoneMatrix(int index) { - if (index < 8) { - if (index < 4) { - if (index < 2) { - if (index == 0) return u_bones[0]; - else return u_bones[1]; - } else { - if (index == 2) return u_bones[2]; - else return u_bones[3]; - } - } else { - if (index < 6) { - if (index == 4) return u_bones[4]; - else return u_bones[5]; - } else { - if (index == 6) return u_bones[6]; - else return u_bones[7]; - } - } - } else if (index < 16) { - if (index < 12) { - if (index < 10) { - if (index == 8) return u_bones[8]; - else return u_bones[9]; - } else { - if (index == 10) return u_bones[10]; - else return u_bones[11]; - } - } else { - if (index < 14) { - if (index == 12) return u_bones[12]; - else return u_bones[13]; - } else { - if (index == 14) return u_bones[14]; - else return u_bones[15]; - } - } - } else if (index < 24) { - if (index < 20) { - if (index < 18) { - if (index == 16) return u_bones[16]; - else return u_bones[17]; - } else { - if (index == 18) return u_bones[18]; - else return u_bones[19]; - } - } else { - if (index < 22) { - if (index == 20) return u_bones[20]; - else return u_bones[21]; - } else { - if (index == 22) return u_bones[22]; - else return u_bones[23]; - } - } - } else { - if (index < 28) { - if (index < 26) { - if (index == 24) return u_bones[24]; - else return u_bones[25]; - } else { - if (index == 26) return u_bones[26]; - else return u_bones[27]; - } - } else { - if (index < 30) { - if (index == 28) return u_bones[28]; - else return u_bones[29]; - } else { - if (index == 30) return u_bones[30]; - else return u_bones[31]; - } - } - } - return mat4(1.0); -} - -void main() { - int b0 = int(a_bone_ids.x); - int b1 = int(a_bone_ids.y); - int b2 = int(a_bone_ids.z); - int b3 = int(a_bone_ids.w); - - mat4 skin_matrix = - getBoneMatrix(b0) * a_bone_weights.x + - getBoneMatrix(b1) * a_bone_weights.y + - getBoneMatrix(b2) * a_bone_weights.z + - getBoneMatrix(b3) * a_bone_weights.w; - - vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0); - vec3 skinned_normal = mat3(skin_matrix[0].xyz, skin_matrix[1].xyz, skin_matrix[2].xyz) * a_normal; - - vec4 worldPos = u_model * skinned_pos; - vec4 viewPos = u_view * worldPos; - vec4 clipPos = u_projection * viewPos; - - if (u_enable_snap) { - vec4 ndc = clipPos; - ndc.xyz /= ndc.w; - vec2 grid = u_resolution * 0.5; - ndc.xy = floor(ndc.xy * grid + 0.5) / grid; - ndc.xyz *= clipPos.w; - clipPos = ndc; - } - - gl_Position = clipPos; - - vec3 worldNormal = mat3(u_model[0].xyz, u_model[1].xyz, u_model[2].xyz) * skinned_normal; - worldNormal = normalize(worldNormal); - float diffuse = max(dot(worldNormal, -u_light_dir), 0.0); - vec3 lighting = u_ambient + vec3(diffuse); - v_color = vec4(a_color.rgb * lighting, a_color.a); - - v_texcoord = a_texcoord * clipPos.w; - v_w = clipPos.w; - - float depth = -viewPos.z; - v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0); -} -)"; - -const char* PS1_SKINNED_VERTEX = R"( -#version 150 core - -uniform mat4 u_model; -uniform mat4 u_view; -uniform mat4 u_projection; -uniform mat4 u_bones[64]; -uniform vec2 u_resolution; -uniform bool u_enable_snap; -uniform float u_fog_start; -uniform float u_fog_end; -uniform vec3 u_light_dir; -uniform vec3 u_ambient; - -in vec3 a_position; -in vec2 a_texcoord; -in vec3 a_normal; -in vec4 a_color; -in vec4 a_bone_ids; -in vec4 a_bone_weights; - -out vec4 v_color; -noperspective out vec2 v_texcoord; -out float v_fog; - -void main() { - ivec4 bone_ids = ivec4(a_bone_ids); - - mat4 skin_matrix = - u_bones[bone_ids.x] * a_bone_weights.x + - u_bones[bone_ids.y] * a_bone_weights.y + - u_bones[bone_ids.z] * a_bone_weights.z + - u_bones[bone_ids.w] * a_bone_weights.w; - - vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0); - vec3 skinned_normal = mat3(skin_matrix) * a_normal; - - vec4 worldPos = u_model * skinned_pos; - vec4 viewPos = u_view * worldPos; - vec4 clipPos = u_projection * viewPos; - - if (u_enable_snap) { - vec4 ndc = clipPos; - ndc.xyz /= ndc.w; - vec2 grid = u_resolution * 0.5; - ndc.xy = floor(ndc.xy * grid + 0.5) / grid; - ndc.xyz *= clipPos.w; - clipPos = ndc; - } - - gl_Position = clipPos; - - vec3 worldNormal = mat3(u_model) * skinned_normal; - worldNormal = normalize(worldNormal); - float diffuse = max(dot(worldNormal, -u_light_dir), 0.0); - vec3 lighting = u_ambient + vec3(diffuse); - v_color = vec4(a_color.rgb * lighting, a_color.a); - - v_texcoord = a_texcoord; - - float depth = -viewPos.z; - v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0); -} -)"; - } // namespace shaders // ============================================================================= @@ -491,20 +274,6 @@ bool Shader3D::loadPS1Shaders() { #endif } -bool Shader3D::loadPS1SkinnedShaders() { -#ifdef MCRF_HAS_GL -#ifdef __EMSCRIPTEN__ - // Use GLES2 skinned shaders for Emscripten/WebGL - return load(shaders::PS1_SKINNED_VERTEX_ES2, shaders::PS1_FRAGMENT_ES2); -#else - // Use desktop GL 3.2+ skinned shaders - return load(shaders::PS1_SKINNED_VERTEX, shaders::PS1_FRAGMENT); -#endif -#else - return false; -#endif -} - bool Shader3D::load(const char* vertexSource, const char* fragmentSource) { if (!gl::isGLReady()) { return false; diff --git a/src/3d/Shader3D.h b/src/3d/Shader3D.h index f457ac4..ffb6b2d 100644 --- a/src/3d/Shader3D.h +++ b/src/3d/Shader3D.h @@ -18,9 +18,6 @@ public: // Automatically selects desktop vs ES2 shaders based on platform bool loadPS1Shaders(); - // Load skinned (skeletal animation) shaders - bool loadPS1SkinnedShaders(); - // Load from custom source strings bool load(const char* vertexSource, const char* fragmentSource); diff --git a/src/3d/Viewport3D.cpp b/src/3d/Viewport3D.cpp index f284ac4..9a5a211 100644 --- a/src/3d/Viewport3D.cpp +++ b/src/3d/Viewport3D.cpp @@ -5,10 +5,6 @@ #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" @@ -46,7 +42,6 @@ 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); @@ -55,7 +50,6 @@ 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); @@ -64,15 +58,6 @@ 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; @@ -196,67 +181,6 @@ 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 // ============================================================================= @@ -271,7 +195,6 @@ 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 @@ -526,139 +449,14 @@ void Viewport3D::renderEntities(const mat4& view, const mat4& proj) { #ifdef MCRF_HAS_GL if (!entities_ || !shader_ || !shader_->isValid()) return; - // Extract frustum for culling - mat4 viewProj = proj * view; - Frustum frustum; - frustum.extractFromMatrix(viewProj); - - // Render non-skeletal entities first + // Entity rendering uses the same shader as terrain shader_->bind(); + for (auto& entity : *entities_) { if (entity && entity->isVisible()) { - // 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()); - } + 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 @@ -700,12 +498,6 @@ 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() { @@ -834,235 +626,12 @@ 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 (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); + // Render the layer's geometry + layer->render(layer->getModelMatrix(), view, projection); } shader_->unbind(); @@ -1076,19 +645,6 @@ 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(); @@ -1112,17 +668,11 @@ void Viewport3D::render3DContent() { // Render mesh layers first (terrain, etc.) - sorted by z_index renderMeshLayers(); - // Render voxel layers (Milestone 10) + // Render entities 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(); @@ -2245,427 +1795,6 @@ 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 @@ -2774,120 +1903,5 @@ 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 c38901f..ab7d5b2 100644 --- a/src/3d/Viewport3D.h +++ b/src/3d/Viewport3D.h @@ -30,8 +30,6 @@ namespace mcrf { class Viewport3D; class Shader3D; class MeshLayer; -class Billboard; -class VoxelGrid; } // namespace mcrf @@ -86,19 +84,6 @@ public: // Camera orbit helper for demos void orbitCamera(float angle, float distance, float height); - /// Convert screen coordinates to world position via ray casting - /// @param screenX X position relative to viewport - /// @param screenY Y position relative to viewport - /// @return World position on Y=0 plane, or (-1,-1,-1) if no intersection - vec3 screenToWorld(float screenX, float screenY); - - /// Position camera to follow an entity - /// @param entity Entity to follow - /// @param distance Distance behind entity - /// @param height Height above entity - /// @param smoothing Interpolation factor (0-1, where 1 = instant) - void followEntity(std::shared_ptr entity, float distance, float height, float smoothing = 1.0f); - // ========================================================================= // Mesh Layer Management // ========================================================================= @@ -205,68 +190,6 @@ public: /// Render all entities void renderEntities(const mat4& view, const mat4& proj); - // ========================================================================= - // Billboard Management - // ========================================================================= - - /// Get the billboard list - std::shared_ptr>> 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_; } @@ -339,10 +262,6 @@ private: float testRotation_ = 0.0f; bool renderTestCube_ = true; // Set to false when layers are added - // Animation timing - float lastFrameTime_ = 0.0f; - bool firstFrame_ = true; - // Mesh layers for terrain, static geometry std::vector> meshLayers_; @@ -357,17 +276,8 @@ 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 deleted file mode 100644 index f69e71e..0000000 --- a/src/3d/VoxelGrid.cpp +++ /dev/null @@ -1,795 +0,0 @@ -// VoxelGrid.cpp - Dense 3D voxel array implementation -// Part of McRogueFace 3D Extension - Milestones 9-11 - -#include "VoxelGrid.h" -#include "VoxelMesher.h" -#include "MeshLayer.h" // For MeshVertex -#include -#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 deleted file mode 100644 index 740e8c7..0000000 --- a/src/3d/VoxelGrid.h +++ /dev/null @@ -1,194 +0,0 @@ -// VoxelGrid.h - Dense 3D voxel array with material palette -// Part of McRogueFace 3D Extension - Milestones 9-11 -#pragma once - -#include "../Common.h" -#include "Math3D.h" -#include "MeshLayer.h" // For MeshVertex (needed for std::vector) -#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 deleted file mode 100644 index bb85278..0000000 --- a/src/3d/VoxelMesher.cpp +++ /dev/null @@ -1,317 +0,0 @@ -// 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 deleted file mode 100644 index a0b3c4d..0000000 --- a/src/3d/VoxelMesher.h +++ /dev/null @@ -1,80 +0,0 @@ -// VoxelMesher.h - Face-culled mesh generation for VoxelGrid -// Part of McRogueFace 3D Extension - Milestones 10, 13 -#pragma once - -#include "VoxelGrid.h" -#include "MeshLayer.h" // For MeshVertex -#include - -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 deleted file mode 100644 index 316a11d..0000000 --- a/src/3d/cgltf.h +++ /dev/null @@ -1,7240 +0,0 @@ -/** - * 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 deleted file mode 100644 index a205e5a..0000000 --- a/src/3d/cgltf_impl.cpp +++ /dev/null @@ -1,6 +0,0 @@ -// cgltf_impl.cpp - Implementation file for cgltf glTF loader -// This file defines CGLTF_IMPLEMENTATION to include the cgltf implementation -// exactly once in the project. - -#define CGLTF_IMPLEMENTATION -#include "cgltf.h" diff --git a/src/3d/shaders/ps1_skinned_vertex.glsl b/src/3d/shaders/ps1_skinned_vertex.glsl deleted file mode 100644 index 406de9e..0000000 --- a/src/3d/shaders/ps1_skinned_vertex.glsl +++ /dev/null @@ -1,108 +0,0 @@ -// PS1-style skinned vertex shader for OpenGL 3.2+ -// Implements skeletal animation, vertex snapping, Gouraud shading, and fog - -#version 150 core - -// Uniforms - transform matrices -uniform mat4 u_model; -uniform mat4 u_view; -uniform mat4 u_projection; - -// Uniforms - skeletal animation (max 64 bones) -uniform mat4 u_bones[64]; - -// Uniforms - PS1 effects -uniform vec2 u_resolution; // Internal render resolution for vertex snapping -uniform bool u_enable_snap; // Enable vertex snapping to pixel grid -uniform float u_fog_start; // Fog start distance -uniform float u_fog_end; // Fog end distance - -// Uniforms - lighting -uniform vec3 u_light_dir; // Directional light direction (normalized) -uniform vec3 u_ambient; // Ambient light color - -// Attributes -in vec3 a_position; -in vec2 a_texcoord; -in vec3 a_normal; -in vec4 a_color; -in vec4 a_bone_ids; // Up to 4 bone indices (as float for compatibility) -in vec4 a_bone_weights; // Corresponding weights - -// Varyings - passed to fragment shader -out vec4 v_color; // Gouraud-shaded vertex color -noperspective out vec2 v_texcoord; // Texture coordinates (affine interpolation!) -out float v_fog; // Fog factor (0 = no fog, 1 = full fog) - -void main() { - // ========================================================================= - // Skeletal Animation: Vertex Skinning - // Transform vertex and normal by weighted bone matrices - // ========================================================================= - ivec4 bone_ids = ivec4(a_bone_ids); // Convert to integer indices - - // Compute skinned position and normal - mat4 skin_matrix = - u_bones[bone_ids.x] * a_bone_weights.x + - u_bones[bone_ids.y] * a_bone_weights.y + - u_bones[bone_ids.z] * a_bone_weights.z + - u_bones[bone_ids.w] * a_bone_weights.w; - - vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0); - vec3 skinned_normal = mat3(skin_matrix) * a_normal; - - // Transform vertex to clip space - vec4 worldPos = u_model * skinned_pos; - vec4 viewPos = u_view * worldPos; - vec4 clipPos = u_projection * viewPos; - - // ========================================================================= - // PS1 Effect: Vertex Snapping - // The PS1 had limited precision for vertex positions, causing vertices - // to "snap" to a grid, creating the characteristic jittery look. - // ========================================================================= - if (u_enable_snap) { - // Convert to NDC - vec4 ndc = clipPos; - ndc.xyz /= ndc.w; - - // Snap to pixel grid based on render resolution - vec2 grid = u_resolution * 0.5; - ndc.xy = floor(ndc.xy * grid + 0.5) / grid; - - // Convert back to clip space - ndc.xyz *= clipPos.w; - clipPos = ndc; - } - - gl_Position = clipPos; - - // ========================================================================= - // PS1 Effect: Gouraud Shading - // Per-vertex lighting was used on PS1 due to hardware limitations. - // This creates characteristic flat-shaded polygons. - // ========================================================================= - vec3 worldNormal = mat3(u_model) * skinned_normal; - worldNormal = normalize(worldNormal); - - // Simple directional light + ambient - float diffuse = max(dot(worldNormal, -u_light_dir), 0.0); - vec3 lighting = u_ambient + vec3(diffuse); - - // Apply lighting to vertex color - v_color = vec4(a_color.rgb * lighting, a_color.a); - - // ========================================================================= - // PS1 Effect: Affine Texture Mapping - // Using 'noperspective' qualifier disables perspective-correct interpolation - // This creates the characteristic texture warping on large polygons - // ========================================================================= - v_texcoord = a_texcoord; - - // ========================================================================= - // Fog Distance Calculation - // Calculate linear fog factor based on view-space depth - // ========================================================================= - float depth = -viewPos.z; // View space depth (positive) - v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0); -} diff --git a/src/3d/shaders/ps1_skinned_vertex_es2.glsl b/src/3d/shaders/ps1_skinned_vertex_es2.glsl deleted file mode 100644 index 498b924..0000000 --- a/src/3d/shaders/ps1_skinned_vertex_es2.glsl +++ /dev/null @@ -1,195 +0,0 @@ -// PS1-style skinned vertex shader for OpenGL ES 2.0 / WebGL 1.0 -// Implements skeletal animation, vertex snapping, Gouraud shading, and fog - -precision mediump float; - -// Uniforms - transform matrices -uniform mat4 u_model; -uniform mat4 u_view; -uniform mat4 u_projection; - -// Uniforms - skeletal animation (max 64 bones) -// GLES2 doesn't guarantee support for arrays > 128 vec4s in vertex shaders -// 64 bones * 4 vec4s = 256 vec4s, so we use 32 bones for safety -uniform mat4 u_bones[32]; - -// Uniforms - PS1 effects -uniform vec2 u_resolution; // Internal render resolution for vertex snapping -uniform bool u_enable_snap; // Enable vertex snapping to pixel grid -uniform float u_fog_start; // Fog start distance -uniform float u_fog_end; // Fog end distance - -// Uniforms - lighting -uniform vec3 u_light_dir; // Directional light direction (normalized) -uniform vec3 u_ambient; // Ambient light color - -// Attributes -attribute vec3 a_position; -attribute vec2 a_texcoord; -attribute vec3 a_normal; -attribute vec4 a_color; -attribute vec4 a_bone_ids; // Up to 4 bone indices (as floats) -attribute vec4 a_bone_weights; // Corresponding weights - -// Varyings - passed to fragment shader -varying vec4 v_color; // Gouraud-shaded vertex color -varying vec2 v_texcoord; // Texture coordinates (multiplied by w for affine trick) -varying float v_w; // Clip space w for affine mapping restoration -varying float v_fog; // Fog factor (0 = no fog, 1 = full fog) - -// Helper to get bone matrix by index (GLES2 doesn't support dynamic array indexing well) -mat4 getBoneMatrix(int index) { - // GLES2 workaround: use if-chain for dynamic indexing - if (index < 8) { - if (index < 4) { - if (index < 2) { - if (index == 0) return u_bones[0]; - else return u_bones[1]; - } else { - if (index == 2) return u_bones[2]; - else return u_bones[3]; - } - } else { - if (index < 6) { - if (index == 4) return u_bones[4]; - else return u_bones[5]; - } else { - if (index == 6) return u_bones[6]; - else return u_bones[7]; - } - } - } else if (index < 16) { - if (index < 12) { - if (index < 10) { - if (index == 8) return u_bones[8]; - else return u_bones[9]; - } else { - if (index == 10) return u_bones[10]; - else return u_bones[11]; - } - } else { - if (index < 14) { - if (index == 12) return u_bones[12]; - else return u_bones[13]; - } else { - if (index == 14) return u_bones[14]; - else return u_bones[15]; - } - } - } else if (index < 24) { - if (index < 20) { - if (index < 18) { - if (index == 16) return u_bones[16]; - else return u_bones[17]; - } else { - if (index == 18) return u_bones[18]; - else return u_bones[19]; - } - } else { - if (index < 22) { - if (index == 20) return u_bones[20]; - else return u_bones[21]; - } else { - if (index == 22) return u_bones[22]; - else return u_bones[23]; - } - } - } else { - if (index < 28) { - if (index < 26) { - if (index == 24) return u_bones[24]; - else return u_bones[25]; - } else { - if (index == 26) return u_bones[26]; - else return u_bones[27]; - } - } else { - if (index < 30) { - if (index == 28) return u_bones[28]; - else return u_bones[29]; - } else { - if (index == 30) return u_bones[30]; - else return u_bones[31]; - } - } - } - return mat4(1.0); // Identity fallback -} - -void main() { - // ========================================================================= - // Skeletal Animation: Vertex Skinning - // Transform vertex and normal by weighted bone matrices - // ========================================================================= - int b0 = int(a_bone_ids.x); - int b1 = int(a_bone_ids.y); - int b2 = int(a_bone_ids.z); - int b3 = int(a_bone_ids.w); - - // Compute skinned position and normal - mat4 skin_matrix = - getBoneMatrix(b0) * a_bone_weights.x + - getBoneMatrix(b1) * a_bone_weights.y + - getBoneMatrix(b2) * a_bone_weights.z + - getBoneMatrix(b3) * a_bone_weights.w; - - vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0); - vec3 skinned_normal = mat3(skin_matrix[0].xyz, skin_matrix[1].xyz, skin_matrix[2].xyz) * a_normal; - - // Transform vertex to clip space - vec4 worldPos = u_model * skinned_pos; - vec4 viewPos = u_view * worldPos; - vec4 clipPos = u_projection * viewPos; - - // ========================================================================= - // PS1 Effect: Vertex Snapping - // The PS1 had limited precision for vertex positions, causing vertices - // to "snap" to a grid, creating the characteristic jittery look. - // ========================================================================= - if (u_enable_snap) { - // Convert to NDC - vec4 ndc = clipPos; - ndc.xyz /= ndc.w; - - // Snap to pixel grid based on render resolution - vec2 grid = u_resolution * 0.5; - ndc.xy = floor(ndc.xy * grid + 0.5) / grid; - - // Convert back to clip space - ndc.xyz *= clipPos.w; - clipPos = ndc; - } - - gl_Position = clipPos; - - // ========================================================================= - // PS1 Effect: Gouraud Shading - // Per-vertex lighting was used on PS1 due to hardware limitations. - // This creates characteristic flat-shaded polygons. - // ========================================================================= - vec3 worldNormal = mat3(u_model[0].xyz, u_model[1].xyz, u_model[2].xyz) * skinned_normal; - worldNormal = normalize(worldNormal); - - // Simple directional light + ambient - float diffuse = max(dot(worldNormal, -u_light_dir), 0.0); - vec3 lighting = u_ambient + vec3(diffuse); - - // Apply lighting to vertex color - v_color = vec4(a_color.rgb * lighting, a_color.a); - - // ========================================================================= - // PS1 Effect: Affine Texture Mapping Trick - // GLES2 doesn't have 'noperspective' interpolation, so we manually - // multiply texcoords by w here and divide by w in fragment shader. - // This creates the characteristic texture warping on large polygons. - // ========================================================================= - v_texcoord = a_texcoord * clipPos.w; - v_w = clipPos.w; - - // ========================================================================= - // Fog Distance Calculation - // Calculate linear fog factor based on view-space depth - // ========================================================================= - float depth = -viewPos.z; // View space depth (positive) - v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0); -} diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index ec4ffe1..a5940dc 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -34,9 +34,6 @@ #include "3d/Viewport3D.h" // 3D rendering viewport #include "3d/Entity3D.h" // 3D game entities #include "3d/EntityCollection3D.h" // Entity3D collection -#include "3d/Model3D.h" // 3D model resource -#include "3d/Billboard.h" // Billboard sprites -#include "3d/PyVoxelGrid.h" // Voxel grid for 3D structures (Milestone 9) #include "McRogueFaceVersion.h" #include "GameEngine.h" // ImGui is only available for SFML builds @@ -442,9 +439,7 @@ PyObject* PyInit_mcrfpy() /*3D entities*/ &mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType, - &mcrfpydef::PyEntityCollection3DIterType, &mcrfpydef::PyModel3DType, - &mcrfpydef::PyBillboardType, &mcrfpydef::PyVoxelGridType, - &mcrfpydef::PyVoxelRegionType, + &mcrfpydef::PyEntityCollection3DIterType, /*grid layers (#147)*/ &PyColorLayerType, &PyTileLayerType, @@ -541,13 +536,6 @@ PyObject* PyInit_mcrfpy() mcrfpydef::PyNoiseSourceType.tp_methods = PyNoiseSource::methods; mcrfpydef::PyNoiseSourceType.tp_getset = PyNoiseSource::getsetters; - // Set up PyVoxelGridType methods and getsetters (Milestone 9) - mcrfpydef::PyVoxelGridType.tp_methods = PyVoxelGrid::methods; - mcrfpydef::PyVoxelGridType.tp_getset = PyVoxelGrid::getsetters; - - // Set up PyVoxelRegionType getsetters (Milestone 11) - mcrfpydef::PyVoxelRegionType.tp_getset = PyVoxelRegion::getsetters; - // Set up PyShaderType methods and getsetters (#106) mcrfpydef::PyShaderType.tp_methods = PyShader::methods; mcrfpydef::PyShaderType.tp_getset = PyShader::getsetters; @@ -571,8 +559,6 @@ PyObject* PyInit_mcrfpy() PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist); PyViewport3DType.tp_weaklistoffset = offsetof(PyViewport3DObject, weakreflist); mcrfpydef::PyEntity3DType.tp_weaklistoffset = offsetof(PyEntity3DObject, weakreflist); - mcrfpydef::PyModel3DType.tp_weaklistoffset = offsetof(PyModel3DObject, weakreflist); - mcrfpydef::PyBillboardType.tp_weaklistoffset = offsetof(PyBillboardObject, weakreflist); // #219 - Initialize PyLock context manager type if (PyLock::init() < 0) { diff --git a/src/PyColor.cpp b/src/PyColor.cpp index 7bc8b10..9e352af 100644 --- a/src/PyColor.cpp +++ b/src/PyColor.cpp @@ -68,68 +68,8 @@ PyObject* PyColor::pyObject() sf::Color PyColor::fromPy(PyObject* obj) { - // Handle None or NULL - if (!obj || obj == Py_None) { - return sf::Color::White; - } - - // Check if it's already a Color object - PyTypeObject* color_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"); - if (color_type) { - bool is_color = PyObject_TypeCheck(obj, color_type); - Py_DECREF(color_type); - if (is_color) { - PyColorObject* self = (PyColorObject*)obj; - return self->data; - } - } - - // Handle tuple or list input - if (PyTuple_Check(obj) || PyList_Check(obj)) { - Py_ssize_t size = PySequence_Size(obj); - if (size < 3 || size > 4) { - PyErr_SetString(PyExc_TypeError, "Color tuple/list must have 3 or 4 elements (r, g, b[, a])"); - return sf::Color::White; - } - - int r = 255, g = 255, b = 255, a = 255; - - PyObject* item0 = PySequence_GetItem(obj, 0); - PyObject* item1 = PySequence_GetItem(obj, 1); - PyObject* item2 = PySequence_GetItem(obj, 2); - - if (PyLong_Check(item0)) r = (int)PyLong_AsLong(item0); - if (PyLong_Check(item1)) g = (int)PyLong_AsLong(item1); - if (PyLong_Check(item2)) b = (int)PyLong_AsLong(item2); - - Py_DECREF(item0); - Py_DECREF(item1); - Py_DECREF(item2); - - if (size == 4) { - PyObject* item3 = PySequence_GetItem(obj, 3); - if (PyLong_Check(item3)) a = (int)PyLong_AsLong(item3); - Py_DECREF(item3); - } - - // Clamp values - r = std::max(0, std::min(255, r)); - g = std::max(0, std::min(255, g)); - b = std::max(0, std::min(255, b)); - a = std::max(0, std::min(255, a)); - - return sf::Color(r, g, b, a); - } - - // Handle integer (grayscale) - if (PyLong_Check(obj)) { - int v = std::max(0, std::min(255, (int)PyLong_AsLong(obj))); - return sf::Color(v, v, v, 255); - } - - // Unknown type - set error and return white - PyErr_SetString(PyExc_TypeError, "Color must be a Color object, tuple, list, or integer"); - return sf::Color::White; + PyColorObject* self = (PyColorObject*)obj; + return self->data; } sf::Color PyColor::fromPy(PyColorObject* self) diff --git a/src/PyTexture.cpp b/src/PyTexture.cpp index 7f4d256..2bea741 100644 --- a/src/PyTexture.cpp +++ b/src/PyTexture.cpp @@ -67,29 +67,16 @@ sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s) PyObject* PyTexture::pyObject() { auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"); - if (!type) { - PyErr_SetString(PyExc_RuntimeError, "Failed to get Texture type from module"); - return NULL; - } PyObject* obj = PyTexture::pynew(type, Py_None, Py_None); - Py_DECREF(type); // GetAttrString returns new reference - - if (!obj) { - return NULL; - } try { - // Use placement new to properly construct the shared_ptr - // tp_alloc zeroes memory but doesn't call C++ constructors - new (&((PyTextureObject*)obj)->data) std::shared_ptr(shared_from_this()); + ((PyTextureObject*)obj)->data = shared_from_this(); } catch (std::bad_weak_ptr& e) { std::cout << "Bad weak ptr: shared_from_this() failed in PyTexture::pyObject(); did you create a PyTexture outside of std::make_shared? enjoy your segfault, soon!" << std::endl; - Py_DECREF(obj); - PyErr_SetString(PyExc_RuntimeError, "PyTexture was not created with std::make_shared"); - return NULL; } + // TODO - shared_from_this will raise an exception if the object does not have a shared pointer. Constructor should be made private; write a factory function return obj; } diff --git a/src/PyTexture.h b/src/PyTexture.h index fa5befb..b2375c8 100644 --- a/src/PyTexture.h +++ b/src/PyTexture.h @@ -28,9 +28,6 @@ public: sf::Sprite sprite(int index, sf::Vector2f pos = sf::Vector2f(0, 0), sf::Vector2f s = sf::Vector2f(1.0, 1.0)); int getSpriteCount() const { return sheet_width * sheet_height; } - // Get the underlying sf::Texture for 3D rendering - const sf::Texture* getSFMLTexture() const { return &texture; } - PyObject* pyObject(); static PyObject* repr(PyObject*); static Py_hash_t hash(PyObject*); diff --git a/tests/demo/screens/billboard_building_demo.py b/tests/demo/screens/billboard_building_demo.py deleted file mode 100644 index 8b74efd..0000000 --- a/tests/demo/screens/billboard_building_demo.py +++ /dev/null @@ -1,314 +0,0 @@ -# billboard_building_demo.py - Visual demo of Billboard and Mesh Instances -# Demonstrates camera-facing sprites and static mesh placement - -import mcrfpy -import sys -import math - -# Create demo scene -scene = mcrfpy.Scene("billboard_building_demo") - -# Dark background frame -bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25)) -scene.children.append(bg) - -# Title -title = mcrfpy.Caption(text="Billboard & Building Demo - 3D Sprites and Static Meshes", pos=(20, 10)) -title.fill_color = mcrfpy.Color(255, 255, 255) -scene.children.append(title) - -# Create the 3D viewport -viewport = mcrfpy.Viewport3D( - pos=(50, 60), - size=(600, 450), - render_resolution=(320, 240), # PS1 resolution - fov=60.0, - camera_pos=(16.0, 10.0, 20.0), - camera_target=(8.0, 0.0, 8.0), - bg_color=mcrfpy.Color(80, 120, 180) # Sky blue background -) -scene.children.append(viewport) - -# Set up the navigation grid -GRID_SIZE = 16 -viewport.set_grid_size(GRID_SIZE, GRID_SIZE) - -# Generate terrain -print("Generating terrain...") -hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) -hm.mid_point_displacement(0.2, seed=456) # Gentle terrain -hm.normalize(0.0, 0.5) # Keep it low for placing objects - -# Apply heightmap -viewport.apply_heightmap(hm, 2.0) - -# Build terrain mesh -vertex_count = viewport.build_terrain( - layer_name="terrain", - heightmap=hm, - y_scale=2.0, - cell_size=1.0 -) -print(f"Terrain built with {vertex_count} vertices") - -# Create terrain colors (earthy tones) -r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) -g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) -b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) - -for y in range(GRID_SIZE): - for x in range(GRID_SIZE): - h = hm[x, y] - # Earth/grass colors - r_map[x, y] = 0.25 + h * 0.2 - g_map[x, y] = 0.35 + h * 0.25 - b_map[x, y] = 0.15 + h * 0.1 - -viewport.apply_terrain_colors("terrain", r_map, g_map, b_map) - -# ============================================================================= -# PART 1: Building Placement using Mesh Instances -# ============================================================================= -print("Placing buildings...") - -# Add a layer for buildings -viewport.add_layer("buildings", z_index=1) - -# Create a simple building model (cube-like structure) -building_model = mcrfpy.Model3D() - -# Place several buildings at different locations with transforms -building_positions = [ - ((2, 0, 2), 0, 1.5), # Position, rotation, scale - ((12, 0, 2), 45, 1.2), - ((4, 0, 12), 90, 1.0), - ((10, 0, 10), 30, 1.8), -] - -for pos, rotation, scale in building_positions: - idx = viewport.add_mesh("buildings", building_model, pos=pos, rotation=rotation, scale=scale) - print(f" Placed building {idx} at {pos}") - - # Mark the footprint as blocking - gx, gz = int(pos[0]), int(pos[2]) - footprint_size = max(1, int(scale)) - viewport.place_blocking(grid_pos=(gx, gz), footprint=(footprint_size, footprint_size)) - -print(f"Placed {len(building_positions)} buildings") - -# ============================================================================= -# PART 2: Billboard Sprites (camera-facing) -# ============================================================================= -print("Creating billboards...") - -# Create billboards for "trees" - camera_y mode (stays upright) -tree_positions = [ - (3, 0, 5), (5, 0, 3), (6, 0, 8), (9, 0, 5), - (11, 0, 7), (7, 0, 11), (13, 0, 13), (1, 0, 9) -] - -# Note: Without actual textures, billboards will render as simple quads -# In a real game, you'd load a tree sprite texture -for i, pos in enumerate(tree_positions): - bb = mcrfpy.Billboard( - pos=pos, - scale=1.5, - facing="camera_y", # Stays upright, only rotates on Y axis - opacity=1.0 - ) - viewport.add_billboard(bb) - -print(f" Created {len(tree_positions)} tree billboards (camera_y facing)") - -# Create some particle-like billboards - full camera facing -particle_positions = [ - (8, 3, 8), (8.5, 3.5, 8.2), (7.5, 3.2, 7.8), # Floating particles -] - -for i, pos in enumerate(particle_positions): - bb = mcrfpy.Billboard( - pos=pos, - scale=0.3, - facing="camera", # Full rotation to face camera - opacity=0.7 - ) - viewport.add_billboard(bb) - -print(f" Created {len(particle_positions)} particle billboards (camera facing)") - -# Create a fixed-orientation billboard (signpost) -signpost = mcrfpy.Billboard( - pos=(5, 1.5, 5), - scale=1.0, - facing="fixed", # Manual orientation -) -signpost.theta = math.pi / 4 # 45 degrees horizontal -signpost.phi = 0.0 # No vertical tilt -viewport.add_billboard(signpost) - -print(f" Created 1 signpost billboard (fixed facing)") -print(f"Total billboards: {viewport.billboard_count()}") - -# ============================================================================= -# Info Panel -# ============================================================================= -info_panel = mcrfpy.Frame(pos=(670, 60), size=(330, 450), - fill_color=mcrfpy.Color(30, 30, 40), - outline_color=mcrfpy.Color(80, 80, 100), - outline=2.0) -scene.children.append(info_panel) - -# Panel title -panel_title = mcrfpy.Caption(text="Billboard & Mesh Demo", pos=(690, 70)) -panel_title.fill_color = mcrfpy.Color(200, 200, 255) -scene.children.append(panel_title) - -# Billboard info -bb_info = [ - ("", ""), - ("Billboard Modes:", ""), - (" camera", "Full rotation to face camera"), - (" camera_y", "Y-axis only (stays upright)"), - (" fixed", "Manual theta/phi angles"), - ("", ""), - (f"Trees:", f"{len(tree_positions)} (camera_y)"), - (f"Particles:", f"{len(particle_positions)} (camera)"), - (f"Signpost:", "1 (fixed)"), - ("", ""), - ("Mesh Instances:", ""), - (f" Buildings:", f"{len(building_positions)}"), -] - -y_offset = 100 -for label, value in bb_info: - if label or value: - text = f"{label} {value}" if value else label - cap = mcrfpy.Caption(text=text, pos=(690, y_offset)) - cap.fill_color = mcrfpy.Color(150, 150, 170) - scene.children.append(cap) - y_offset += 22 - -# Dynamic camera info -camera_label = mcrfpy.Caption(text="Camera: Following...", pos=(690, y_offset + 20)) -camera_label.fill_color = mcrfpy.Color(180, 180, 200) -scene.children.append(camera_label) - -# Instructions at bottom -instructions = mcrfpy.Caption( - text="[Space] Toggle orbit | [1-3] Change billboard mode | [C] Clear buildings | [ESC] Quit", - pos=(20, 530) -) -instructions.fill_color = mcrfpy.Color(150, 150, 150) -scene.children.append(instructions) - -# Status line -status = mcrfpy.Caption(text="Status: Billboard & Building demo loaded", pos=(20, 555)) -status.fill_color = mcrfpy.Color(100, 200, 100) -scene.children.append(status) - -# Animation state -animation_time = [0.0] -camera_orbit = [True] - -# Update function -def update(timer, runtime): - animation_time[0] += runtime / 1000.0 - - # Camera orbit - if camera_orbit[0]: - angle = animation_time[0] * 0.3 - radius = 18.0 - center_x = 8.0 - center_z = 8.0 - height = 10.0 + math.sin(animation_time[0] * 0.2) * 2.0 - - x = center_x + math.cos(angle) * radius - z = center_z + math.sin(angle) * radius - - viewport.camera_pos = (x, height, z) - viewport.camera_target = (center_x, 1.0, center_z) - - camera_label.text = f"Camera: Orbit ({x:.1f}, {height:.1f}, {z:.1f})" - - # Animate particle billboards (bobbing up and down) - bb_count = viewport.billboard_count() - if bb_count > len(tree_positions): - particle_start = len(tree_positions) - for i in range(particle_start, particle_start + len(particle_positions)): - if i < bb_count: - bb = viewport.get_billboard(i) - pos = bb.pos - new_y = 3.0 + math.sin(animation_time[0] * 2.0 + i * 0.5) * 0.5 - bb.pos = (pos[0], new_y, pos[2]) - -# Key handler -def on_key(key, state): - if state != mcrfpy.InputState.PRESSED: - return - - if key == mcrfpy.Key.SPACE: - camera_orbit[0] = not camera_orbit[0] - status.text = f"Camera orbit: {'ON' if camera_orbit[0] else 'OFF'}" - - elif key == mcrfpy.Key.NUM_1: - # Change all tree billboards to "camera" mode - for i in range(len(tree_positions)): - viewport.get_billboard(i).facing = "camera" - status.text = "Trees now use 'camera' facing (full rotation)" - - elif key == mcrfpy.Key.NUM_2: - # Change all tree billboards to "camera_y" mode - for i in range(len(tree_positions)): - viewport.get_billboard(i).facing = "camera_y" - status.text = "Trees now use 'camera_y' facing (upright)" - - elif key == mcrfpy.Key.NUM_3: - # Change all tree billboards to "fixed" mode - for i in range(len(tree_positions)): - bb = viewport.get_billboard(i) - bb.facing = "fixed" - bb.theta = i * 0.5 # Different angles - status.text = "Trees now use 'fixed' facing (manual angles)" - - elif key == mcrfpy.Key.C: - viewport.clear_meshes("buildings") - status.text = "Cleared all buildings from layer" - - elif key == mcrfpy.Key.O: - # Adjust tree opacity - for i in range(len(tree_positions)): - bb = viewport.get_billboard(i) - bb.opacity = 0.5 if bb.opacity > 0.7 else 1.0 - status.text = f"Tree opacity toggled" - - elif key == mcrfpy.Key.V: - # Toggle tree visibility - for i in range(len(tree_positions)): - bb = viewport.get_billboard(i) - bb.visible = not bb.visible - status.text = f"Tree visibility toggled" - - elif key == mcrfpy.Key.ESCAPE: - mcrfpy.exit() - -# Set up scene -scene.on_key = on_key - -# Create timer for updates -timer = mcrfpy.Timer("billboard_update", update, 16) # ~60fps - -# Activate scene -mcrfpy.current_scene = scene - -print() -print("Billboard & Building Demo loaded!") -print() -print("Controls:") -print(" [Space] Toggle camera orbit") -print(" [1] Trees -> 'camera' facing") -print(" [2] Trees -> 'camera_y' facing (default)") -print(" [3] Trees -> 'fixed' facing") -print(" [O] Toggle tree opacity") -print(" [V] Toggle tree visibility") -print(" [C] Clear buildings") -print(" [ESC] Quit") diff --git a/tests/demo/screens/integration_demo.py b/tests/demo/screens/integration_demo.py deleted file mode 100644 index 9d3b1ce..0000000 --- a/tests/demo/screens/integration_demo.py +++ /dev/null @@ -1,462 +0,0 @@ -# integration_demo.py - Milestone 8 Integration Demo -# Showcases all 3D features: terrain, entities, pathfinding, FOV, billboards, UI, input - -import mcrfpy -import math -import random - -DEMO_NAME = "3D Integration Demo" -DEMO_DESCRIPTION = """Complete 3D demo with terrain, player, NPC, FOV, and UI overlay. - -Controls: - Arrow keys: Move player - Click: Move to clicked position - ESC: Quit -""" - -# Create the main scene -scene = mcrfpy.Scene("integration_demo") - -# ============================================================================= -# Constants -# ============================================================================= -GRID_WIDTH = 32 -GRID_DEPTH = 32 -CELL_SIZE = 1.0 -TERRAIN_Y_SCALE = 3.0 -FOV_RADIUS = 10 - -# ============================================================================= -# 3D Viewport -# ============================================================================= -viewport = mcrfpy.Viewport3D( - pos=(10, 10), - size=(700, 550), - render_resolution=(350, 275), - fov=60.0, - camera_pos=(16.0, 15.0, 25.0), - camera_target=(16.0, 0.0, 16.0), - bg_color=mcrfpy.Color(40, 60, 100) -) -viewport.enable_fog = True -viewport.fog_near = 10.0 -viewport.fog_far = 40.0 -viewport.fog_color = mcrfpy.Color(40, 60, 100) -scene.children.append(viewport) - -# Set up navigation grid -viewport.set_grid_size(GRID_WIDTH, GRID_DEPTH) - -# ============================================================================= -# Terrain Generation -# ============================================================================= -print("Generating terrain...") - -# Create heightmap with hills -hm = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) -hm.mid_point_displacement(roughness=0.5) -hm.normalize(0.0, 1.0) - -# Build terrain mesh -viewport.build_terrain( - layer_name="terrain", - heightmap=hm, - y_scale=TERRAIN_Y_SCALE, - cell_size=CELL_SIZE -) - -# Apply heightmap to navigation grid -viewport.apply_heightmap(hm, TERRAIN_Y_SCALE) - -# Mark steep slopes and water as unwalkable -viewport.apply_threshold(hm, 0.0, 0.12, False) # Low areas = water (unwalkable) -viewport.set_slope_cost(0.4, 2.0) - -# Create base terrain colors (green/brown based on height) -r_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) -g_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) -b_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) - -# Storage for base colors (for FOV dimming) -base_colors = [] - -for z in range(GRID_DEPTH): - row = [] - for x in range(GRID_WIDTH): - h = hm[x, z] - if h < 0.12: # Water - r, g, b = 0.1, 0.2, 0.4 - elif h < 0.25: # Sand/beach - r, g, b = 0.6, 0.5, 0.3 - elif h < 0.6: # Grass - r, g, b = 0.2 + random.random() * 0.1, 0.4 + random.random() * 0.15, 0.15 - else: # Rock/mountain - r, g, b = 0.4, 0.35, 0.3 - - r_map[x, z] = r - g_map[x, z] = g - b_map[x, z] = b - row.append((r, g, b)) - base_colors.append(row) - -viewport.apply_terrain_colors("terrain", r_map, g_map, b_map) - -# ============================================================================= -# Find walkable starting positions -# ============================================================================= -def find_walkable_pos(): - """Find a random walkable position""" - for _ in range(100): - x = random.randint(2, GRID_WIDTH - 3) - z = random.randint(2, GRID_DEPTH - 3) - cell = viewport.at(x, z) - if cell.walkable: - return (x, z) - return (GRID_WIDTH // 2, GRID_DEPTH // 2) - -# ============================================================================= -# Player Entity -# ============================================================================= -player_start = find_walkable_pos() -player = mcrfpy.Entity3D(pos=player_start, scale=0.8, color=mcrfpy.Color(50, 150, 255)) -viewport.entities.append(player) -print(f"Player at {player_start}") - -# Track discovered cells -discovered = set() -discovered.add(player_start) - -# ============================================================================= -# NPC Entity with Patrol AI -# ============================================================================= -npc_start = find_walkable_pos() -while abs(npc_start[0] - player_start[0]) < 5 and abs(npc_start[1] - player_start[1]) < 5: - npc_start = find_walkable_pos() - -npc = mcrfpy.Entity3D(pos=npc_start, scale=0.7, color=mcrfpy.Color(255, 100, 100)) -viewport.entities.append(npc) -print(f"NPC at {npc_start}") - -# NPC patrol system -class NPCController: - def __init__(self, entity, waypoints): - self.entity = entity - self.waypoints = waypoints - self.current_wp = 0 - self.path = [] - self.path_index = 0 - - def update(self): - if self.entity.is_moving: - return - - # If we have a path, follow it - if self.path_index < len(self.path): - next_pos = self.path[self.path_index] - self.entity.pos = next_pos - self.path_index += 1 - return - - # Reached waypoint, go to next - self.current_wp = (self.current_wp + 1) % len(self.waypoints) - target = self.waypoints[self.current_wp] - - # Compute path to next waypoint - self.path = self.entity.path_to(target[0], target[1]) - self.path_index = 0 - -# Create patrol waypoints -npc_waypoints = [] -for _ in range(4): - wp = find_walkable_pos() - npc_waypoints.append(wp) - -npc_controller = NPCController(npc, npc_waypoints) - -# ============================================================================= -# FOV Visualization -# ============================================================================= -def update_fov_colors(): - """Update terrain colors based on FOV""" - # Compute FOV from player position - visible_cells = viewport.compute_fov((player.pos[0], player.pos[1]), FOV_RADIUS) - visible_set = set((c[0], c[1]) for c in visible_cells) - - # Update discovered - discovered.update(visible_set) - - # Update terrain colors - r_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) - g_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) - b_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH)) - - for z in range(GRID_DEPTH): - for x in range(GRID_WIDTH): - base_r, base_g, base_b = base_colors[z][x] - - if (x, z) in visible_set: - # Fully visible - r_map[x, z] = base_r - g_map[x, z] = base_g - b_map[x, z] = base_b - elif (x, z) in discovered: - # Discovered but not visible - dim - r_map[x, z] = base_r * 0.4 - g_map[x, z] = base_g * 0.4 - b_map[x, z] = base_b * 0.4 - else: - # Never seen - very dark - r_map[x, z] = base_r * 0.1 - g_map[x, z] = base_g * 0.1 - b_map[x, z] = base_b * 0.1 - - viewport.apply_terrain_colors("terrain", r_map, g_map, b_map) - -# Initial FOV update -update_fov_colors() - -# ============================================================================= -# UI Overlay -# ============================================================================= -ui_frame = mcrfpy.Frame( - pos=(720, 10), - size=(260, 200), - fill_color=mcrfpy.Color(20, 20, 30, 220), - outline_color=mcrfpy.Color(80, 80, 120), - outline=2.0 -) -scene.children.append(ui_frame) - -title_label = mcrfpy.Caption(text="3D Integration Demo", pos=(740, 20)) -title_label.fill_color = mcrfpy.Color(255, 255, 150) -scene.children.append(title_label) - -status_label = mcrfpy.Caption(text="Status: Idle", pos=(740, 50)) -status_label.fill_color = mcrfpy.Color(150, 255, 150) -scene.children.append(status_label) - -player_pos_label = mcrfpy.Caption(text="Player: (0, 0)", pos=(740, 75)) -player_pos_label.fill_color = mcrfpy.Color(100, 200, 255) -scene.children.append(player_pos_label) - -npc_pos_label = mcrfpy.Caption(text="NPC: (0, 0)", pos=(740, 100)) -npc_pos_label.fill_color = mcrfpy.Color(255, 150, 150) -scene.children.append(npc_pos_label) - -fps_label = mcrfpy.Caption(text="FPS: --", pos=(740, 125)) -fps_label.fill_color = mcrfpy.Color(200, 200, 200) -scene.children.append(fps_label) - -discovered_label = mcrfpy.Caption(text="Discovered: 0", pos=(740, 150)) -discovered_label.fill_color = mcrfpy.Color(180, 180, 100) -scene.children.append(discovered_label) - -# Controls info -controls_frame = mcrfpy.Frame( - pos=(720, 220), - size=(260, 120), - fill_color=mcrfpy.Color(20, 20, 30, 200), - outline_color=mcrfpy.Color(60, 60, 80), - outline=1.0 -) -scene.children.append(controls_frame) - -ctrl_title = mcrfpy.Caption(text="Controls:", pos=(740, 230)) -ctrl_title.fill_color = mcrfpy.Color(200, 200, 100) -scene.children.append(ctrl_title) - -ctrl_lines = [ - "Arrow keys: Move", - "Click: Pathfind", - "F: Toggle follow cam", - "ESC: Quit" -] -for i, line in enumerate(ctrl_lines): - cap = mcrfpy.Caption(text=line, pos=(740, 255 + i * 20)) - cap.fill_color = mcrfpy.Color(150, 150, 150) - scene.children.append(cap) - -# ============================================================================= -# Game State -# ============================================================================= -follow_camera = True -frame_count = 0 -fps_update_time = 0 - -# ============================================================================= -# Update Function -# ============================================================================= -def game_update(timer, runtime): - global frame_count, fps_update_time - - try: - # Calculate FPS - frame_count += 1 - if runtime - fps_update_time >= 1000: # Update FPS every second - fps = frame_count - fps_label.text = f"FPS: {fps}" - frame_count = 0 - fps_update_time = runtime - - # Update NPC patrol - npc_controller.update() - - # Update UI labels - px, pz = player.pos - player_pos_label.text = f"Player: ({px}, {pz})" - nx, nz = npc.pos - npc_pos_label.text = f"NPC: ({nx}, {nz})" - discovered_label.text = f"Discovered: {len(discovered)}" - - # Camera follow - if follow_camera: - viewport.follow(player, distance=12.0, height=8.0, smoothing=0.1) - - # Update status based on player state - if player.is_moving: - status_label.text = "Status: Moving" - status_label.fill_color = mcrfpy.Color(255, 255, 100) - else: - status_label.text = "Status: Idle" - status_label.fill_color = mcrfpy.Color(150, 255, 150) - except Exception as e: - print(f"Update error: {e}") - -# ============================================================================= -# Input Handling -# ============================================================================= -def try_move_player(dx, dz): - """Try to move player in direction""" - new_x = player.pos[0] + dx - new_z = player.pos[1] + dz - - if not viewport.is_in_fov(new_x, new_z): - # Allow moving into discovered cells even if not currently visible - if (new_x, new_z) not in discovered: - return False - - if new_x < 0 or new_x >= GRID_WIDTH or new_z < 0 or new_z >= GRID_DEPTH: - return False - - cell = viewport.at(new_x, new_z) - if not cell.walkable: - return False - - player.pos = (new_x, new_z) - update_fov_colors() - return True - -def on_key(key, state): - global follow_camera - - if state != mcrfpy.InputState.PRESSED: - return - - if player.is_moving: - return # Don't accept input while moving - - dx, dz = 0, 0 - if key == mcrfpy.Key.UP: - dz = -1 - elif key == mcrfpy.Key.DOWN: - dz = 1 - elif key == mcrfpy.Key.LEFT: - dx = -1 - elif key == mcrfpy.Key.RIGHT: - dx = 1 - elif key == mcrfpy.Key.F: - follow_camera = not follow_camera - status_label.text = f"Camera: {'Follow' if follow_camera else 'Free'}" - return - elif key == mcrfpy.Key.ESCAPE: - mcrfpy.exit() - return - - if dx != 0 or dz != 0: - try_move_player(dx, dz) - -# Click-to-move handling -def on_click(pos, button, state): - if button != mcrfpy.MouseButton.LEFT or state != mcrfpy.InputState.PRESSED: - return - - if player.is_moving: - return - - # Convert click position to viewport-relative coordinates - vp_x = pos.x - viewport.x - vp_y = pos.y - viewport.y - - # Check if click is within viewport - if vp_x < 0 or vp_x >= viewport.w or vp_y < 0 or vp_y >= viewport.h: - return - - # Convert to world position - world_pos = viewport.screen_to_world(vp_x, vp_y) - if world_pos is None: - return - - # Convert to grid position - grid_x = int(world_pos[0] / CELL_SIZE) - grid_z = int(world_pos[2] / CELL_SIZE) - - # Validate grid position - if grid_x < 0 or grid_x >= GRID_WIDTH or grid_z < 0 or grid_z >= GRID_DEPTH: - return - - cell = viewport.at(grid_x, grid_z) - if not cell.walkable: - status_label.text = "Status: Can't walk there!" - status_label.fill_color = mcrfpy.Color(255, 100, 100) - return - - # Find path - path = player.path_to(grid_x, grid_z) - if not path: - status_label.text = "Status: No path!" - status_label.fill_color = mcrfpy.Color(255, 100, 100) - return - - # Follow path (limited to FOV_RADIUS steps) - limited_path = path[:FOV_RADIUS] - player.follow_path(limited_path) - status_label.text = f"Status: Moving ({len(limited_path)} steps)" - status_label.fill_color = mcrfpy.Color(255, 255, 100) - - # Schedule FOV update after movement completes - fov_update_timer = None - - def update_fov_after_move(*args): - # Accept any number of args since timer may pass (runtime) or (timer, runtime) - nonlocal fov_update_timer - if not player.is_moving: - update_fov_colors() - if fov_update_timer: - fov_update_timer.stop() - - fov_update_timer = mcrfpy.Timer("fov_update", update_fov_after_move, 100) - -scene.on_key = on_key -viewport.on_click = on_click - -# ============================================================================= -# Start Game -# ============================================================================= -timer = mcrfpy.Timer("game_update", game_update, 16) # ~60 FPS - -mcrfpy.current_scene = scene - -print() -print("=" * 60) -print("3D Integration Demo Loaded!") -print("=" * 60) -print(f" Terrain: {GRID_WIDTH}x{GRID_DEPTH} cells") -print(f" Player starts at: {player_start}") -print(f" NPC patrolling {len(npc_waypoints)} waypoints") -print() -print("Controls:") -print(" Arrow keys: Move player") -print(" Click: Pathfind to location") -print(" F: Toggle camera follow") -print(" ESC: Quit") -print("=" * 60) diff --git a/tests/demo/screens/model_loading_demo.py b/tests/demo/screens/model_loading_demo.py deleted file mode 100644 index 79f9c25..0000000 --- a/tests/demo/screens/model_loading_demo.py +++ /dev/null @@ -1,240 +0,0 @@ -# model_loading_demo.py - Visual demo of Model3D model loading -# Shows both procedural primitives and loaded .glb models - -import mcrfpy -import sys -import math -import os - -# Create demo scene -scene = mcrfpy.Scene("model_loading_demo") - -# Dark background frame -bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25)) -scene.children.append(bg) - -# Title -title = mcrfpy.Caption(text="Model3D Demo - Procedural & glTF Models", pos=(20, 10)) -title.fill_color = mcrfpy.Color(255, 255, 255) -scene.children.append(title) - -# Create the 3D viewport -viewport = mcrfpy.Viewport3D( - pos=(50, 60), - size=(600, 450), -# render_resolution=(320, 240), # PS1 resolution - render_resolution=(600,450), - fov=60.0, - camera_pos=(0.0, 3.0, 8.0), - camera_target=(0.0, 1.0, 0.0), - bg_color=mcrfpy.Color(30, 30, 50) -) -scene.children.append(viewport) - -# Set up navigation grid -GRID_SIZE = 32 -viewport.set_grid_size(GRID_SIZE, GRID_SIZE) - -# Build a simple flat floor -hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) -hm.normalize(0.0, 0.0) -viewport.apply_heightmap(hm, 0.0) -vertex_count = viewport.build_terrain( - layer_name="floor", - heightmap=hm, - y_scale=0.0, - cell_size=1.0 -) - -# Apply floor colors (checkerboard pattern) -r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) -g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) -b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) - -for y in range(GRID_SIZE): - for x in range(GRID_SIZE): - checker = ((x + y) % 2) * 0.1 + 0.15 - r_map[x, y] = checker - g_map[x, y] = checker - b_map[x, y] = checker + 0.05 - -viewport.apply_terrain_colors("floor", r_map, g_map, b_map) - -# Create procedural models -print("Creating procedural models...") -cube_model = mcrfpy.Model3D.cube(1.0) -sphere_model = mcrfpy.Model3D.sphere(0.5, 12, 8) - -# Try to load glTF models -loaded_models = {} -models_dir = "../assets/models" -if os.path.exists(models_dir): - for filename in ["Duck.glb", "Box.glb", "Lantern.glb", "WaterBottle.glb"]: - path = os.path.join(models_dir, filename) - if os.path.exists(path): - try: - model = mcrfpy.Model3D(path) - loaded_models[filename] = model - print(f"Loaded {filename}: {model.vertex_count} verts, {model.triangle_count} tris") - except Exception as e: - print(f"Failed to load {filename}: {e}") - -# Create entities with different models -entities = [] - -# Row 1: Procedural primitives -entity_configs = [ - ((12, 16), cube_model, 1.0, mcrfpy.Color(255, 100, 100), "Cube"), - ((16, 16), sphere_model, 1.0, mcrfpy.Color(100, 255, 100), "Sphere"), - ((20, 16), None, 1.0, mcrfpy.Color(200, 200, 200), "Placeholder"), -] - -# Row 2: Loaded glTF models (if available) -if "Duck.glb" in loaded_models: - # Duck is huge (~160 units), scale it down significantly - entity_configs.append(((14, 12), loaded_models["Duck.glb"], 0.006, mcrfpy.Color(255, 200, 50), "Duck")) - -if "Box.glb" in loaded_models: - entity_configs.append(((16, 12), loaded_models["Box.glb"], 1.5, mcrfpy.Color(150, 100, 50), "Box (glb)")) - -if "Lantern.glb" in loaded_models: - # Lantern is ~25 units tall - entity_configs.append(((18, 12), loaded_models["Lantern.glb"], 0.08, mcrfpy.Color(255, 200, 100), "Lantern")) - -if "WaterBottle.glb" in loaded_models: - # WaterBottle is ~0.26 units tall - entity_configs.append(((20, 12), loaded_models["WaterBottle.glb"], 4.0, mcrfpy.Color(100, 150, 255), "Bottle")) - -for pos, model, scale, color, name in entity_configs: - e = mcrfpy.Entity3D(pos=pos, scale=scale, color=color) - if model: - e.model = model - viewport.entities.append(e) - entities.append((e, name, model)) - -print(f"Created {len(entities)} entities") - -# Info panel on the right -info_panel = mcrfpy.Frame(pos=(670, 60), size=(330, 450), - fill_color=mcrfpy.Color(30, 30, 40), - outline_color=mcrfpy.Color(80, 80, 100), - outline=2.0) -scene.children.append(info_panel) - -# Panel title -panel_title = mcrfpy.Caption(text="Model Information", pos=(690, 70)) -panel_title.fill_color = mcrfpy.Color(200, 200, 255) -scene.children.append(panel_title) - -# Model info labels -y_offset = 100 -for e, name, model in entities: - if model: - info = f"{name}: {model.vertex_count}v, {model.triangle_count}t" - else: - info = f"{name}: Placeholder (36v, 12t)" - label = mcrfpy.Caption(text=info, pos=(690, y_offset)) - label.fill_color = e.color - scene.children.append(label) - y_offset += 22 - -# Separator -y_offset += 10 -sep = mcrfpy.Caption(text="--- glTF Support ---", pos=(690, y_offset)) -sep.fill_color = mcrfpy.Color(150, 150, 150) -scene.children.append(sep) -y_offset += 22 - -# glTF info -gltf_info = [ - "Format: glTF 2.0 (.glb, .gltf)", - "Library: cgltf (C99)", - f"Loaded models: {len(loaded_models)}", -] -for info in gltf_info: - label = mcrfpy.Caption(text=info, pos=(690, y_offset)) - label.fill_color = mcrfpy.Color(150, 150, 170) - scene.children.append(label) - y_offset += 20 - -# Instructions at bottom -instructions = mcrfpy.Caption( - text="[Space] Toggle rotation | [1-3] Camera presets | [ESC] Quit", - pos=(20, 530) -) -instructions.fill_color = mcrfpy.Color(150, 150, 150) -scene.children.append(instructions) - -# Status line -status = mcrfpy.Caption(text="Status: Showing procedural and glTF models", pos=(20, 555)) -status.fill_color = mcrfpy.Color(100, 200, 100) -scene.children.append(status) - -# Animation state -animation_time = [0.0] -rotate_entities = [True] - -# Camera presets -camera_presets = [ - ((0.0, 5.0, 12.0), (0.0, 1.0, 0.0), "Front view"), - ((12.0, 8.0, 0.0), (0.0, 1.0, 0.0), "Side view"), - ((0.0, 15.0, 0.1), (0.0, 0.0, 0.0), "Top-down view"), -] -current_preset = [0] - -# Update function -def update(timer, runtime): - animation_time[0] += runtime / 1000.0 - - if rotate_entities[0]: - for i, (e, name, model) in enumerate(entities): - e.rotation = (animation_time[0] * 30.0 + i * 45.0) % 360.0 - -# Key handler -def on_key(key, state): - if state != mcrfpy.InputState.PRESSED: - return - - if key == mcrfpy.Key.SPACE: - rotate_entities[0] = not rotate_entities[0] - status.text = f"Rotation: {'ON' if rotate_entities[0] else 'OFF'}" - - elif key == mcrfpy.Key.NUM_1: - pos, target, name = camera_presets[0] - viewport.camera_pos = pos - viewport.camera_target = target - status.text = f"Camera: {name}" - - elif key == mcrfpy.Key.NUM_2: - pos, target, name = camera_presets[1] - viewport.camera_pos = pos - viewport.camera_target = target - status.text = f"Camera: {name}" - - elif key == mcrfpy.Key.NUM_3: - pos, target, name = camera_presets[2] - viewport.camera_pos = pos - viewport.camera_target = target - status.text = f"Camera: {name}" - - elif key == mcrfpy.Key.ESCAPE: - mcrfpy.exit() - -# Set up scene -scene.on_key = on_key - -# Create timer for updates -timer = mcrfpy.Timer("model_update", update, 16) - -# Activate scene -mcrfpy.current_scene = scene - -print() -print("Model3D Demo loaded!") -print(f"Procedural models: cube, sphere") -print(f"glTF models loaded: {list(loaded_models.keys())}") -print() -print("Controls:") -print(" [Space] Toggle rotation") -print(" [1-3] Camera presets") -print(" [ESC] Quit") diff --git a/tests/demo/screens/skeletal_animation_demo.py b/tests/demo/screens/skeletal_animation_demo.py deleted file mode 100644 index 2f82908..0000000 --- a/tests/demo/screens/skeletal_animation_demo.py +++ /dev/null @@ -1,275 +0,0 @@ -# skeletal_animation_demo.py - 3D Skeletal Animation Demo Screen -# Demonstrates Entity3D animation with real animated glTF models - -import mcrfpy -import sys -import os - -DEMO_NAME = "3D Skeletal Animation" -DEMO_DESCRIPTION = """Entity3D Animation API with real skeletal models""" - -# Create demo scene -scene = mcrfpy.Scene("skeletal_animation_demo") - -# Dark background frame -bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25)) -scene.children.append(bg) - -# Title -title = mcrfpy.Caption(text="Skeletal Animation Demo", pos=(20, 10)) -title.fill_color = mcrfpy.Color(255, 255, 100) -scene.children.append(title) - -# Create the 3D viewport -viewport = mcrfpy.Viewport3D( - pos=(50, 50), - size=(600, 500), - render_resolution=(600, 500), - fov=60.0, - camera_pos=(0.0, 2.0, 5.0), - camera_target=(0.0, 1.0, 0.0), - bg_color=mcrfpy.Color(30, 30, 50) -) -scene.children.append(viewport) - -# Set up navigation grid -GRID_SIZE = 16 -viewport.set_grid_size(GRID_SIZE, GRID_SIZE) - -# Build a simple flat floor -hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) -hm.normalize(0.0, 0.0) -viewport.apply_heightmap(hm, 0.0) -viewport.build_terrain( - layer_name="floor", - heightmap=hm, - y_scale=0.0, - cell_size=1.0 -) - -# Apply floor colors (dark gray) -r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) -g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) -b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE)) -for y in range(GRID_SIZE): - for x in range(GRID_SIZE): - checker = ((x + y) % 2) * 0.1 + 0.15 - r_map[x, y] = checker - g_map[x, y] = checker - b_map[x, y] = checker + 0.05 -viewport.apply_terrain_colors("floor", r_map, g_map, b_map) - -# Load animated models -animated_entity = None -model_info = "No animated model" - -# Try to load CesiumMan (humanoid with walk animation) -try: - model = mcrfpy.Model3D("../assets/models/CesiumMan.glb") - if model.has_skeleton: - animated_entity = mcrfpy.Entity3D(pos=(8, 8), scale=1.0, color=mcrfpy.Color(200, 180, 150)) - animated_entity.model = model - viewport.entities.append(animated_entity) - - # Set up animation - clips = model.animation_clips - if clips: - animated_entity.anim_clip = clips[0] - animated_entity.anim_loop = True - animated_entity.anim_speed = 1.0 - - model_info = f"CesiumMan: {model.bone_count} bones, {model.vertex_count} verts" - print(f"Loaded {model_info}") - print(f"Animation clips: {clips}") -except Exception as e: - print(f"Failed to load CesiumMan: {e}") - -# Also try RiggedSimple as a second model -try: - model2 = mcrfpy.Model3D("../assets/models/RiggedSimple.glb") - if model2.has_skeleton: - entity2 = mcrfpy.Entity3D(pos=(10, 8), scale=0.5, color=mcrfpy.Color(100, 200, 255)) - entity2.model = model2 - viewport.entities.append(entity2) - - clips = model2.animation_clips - if clips: - entity2.anim_clip = clips[0] - entity2.anim_loop = True - entity2.anim_speed = 1.5 - - print(f"Loaded RiggedSimple: {model2.bone_count} bones") -except Exception as e: - print(f"Failed to load RiggedSimple: {e}") - -# Info panel on the right -info_panel = mcrfpy.Frame(pos=(670, 50), size=(330, 500), - fill_color=mcrfpy.Color(30, 30, 40), - outline_color=mcrfpy.Color(80, 80, 100), - outline=2.0) -scene.children.append(info_panel) - -# Panel title -panel_title = mcrfpy.Caption(text="Animation Properties", pos=(690, 60)) -panel_title.fill_color = mcrfpy.Color(200, 200, 255) -scene.children.append(panel_title) - -# Status labels (will be updated by timer) -status_labels = [] -y_offset = 90 - -label_texts = [ - "Model: loading...", - "anim_clip: ", - "anim_time: 0.00", - "anim_speed: 1.00", - "anim_loop: True", - "anim_paused: False", - "anim_frame: 0", -] - -for text in label_texts: - label = mcrfpy.Caption(text=text, pos=(690, y_offset)) - label.fill_color = mcrfpy.Color(150, 200, 150) - scene.children.append(label) - status_labels.append(label) - y_offset += 25 - -# Set initial model info -status_labels[0].text = f"Model: {model_info}" - -# Controls section -y_offset += 20 -controls_title = mcrfpy.Caption(text="Controls:", pos=(690, y_offset)) -controls_title.fill_color = mcrfpy.Color(255, 255, 200) -scene.children.append(controls_title) -y_offset += 25 - -controls = [ - "[SPACE] Toggle pause", - "[L] Toggle loop", - "[+/-] Adjust speed", - "[R] Reset time", - "[1-3] Camera presets", -] - -for ctrl in controls: - cap = mcrfpy.Caption(text=ctrl, pos=(690, y_offset)) - cap.fill_color = mcrfpy.Color(180, 180, 150) - scene.children.append(cap) - y_offset += 20 - -# Auto-animate section -y_offset += 20 -auto_title = mcrfpy.Caption(text="Auto-Animate:", pos=(690, y_offset)) -auto_title.fill_color = mcrfpy.Color(255, 200, 200) -scene.children.append(auto_title) -y_offset += 25 - -auto_labels = [] -auto_texts = [ - "auto_animate: True", - "walk_clip: 'walk'", - "idle_clip: 'idle'", -] - -for text in auto_texts: - cap = mcrfpy.Caption(text=text, pos=(690, y_offset)) - cap.fill_color = mcrfpy.Color(180, 160, 160) - scene.children.append(cap) - auto_labels.append(cap) - y_offset += 20 - -# Instructions at bottom -status = mcrfpy.Caption(text="Status: Animation playing", pos=(20, 570)) -status.fill_color = mcrfpy.Color(100, 200, 100) -scene.children.append(status) - -# Camera presets -camera_presets = [ - ((0.0, 2.0, 5.0), (0.0, 1.0, 0.0), "Front view"), - ((5.0, 3.0, 0.0), (0.0, 1.0, 0.0), "Side view"), - ((0.0, 6.0, 0.1), (0.0, 0.0, 0.0), "Top-down view"), -] - -# Update function - updates display and entity rotation -def update(timer, runtime): - if animated_entity: - # Update status display - status_labels[1].text = f"anim_clip: '{animated_entity.anim_clip}'" - status_labels[2].text = f"anim_time: {animated_entity.anim_time:.2f}" - status_labels[3].text = f"anim_speed: {animated_entity.anim_speed:.2f}" - status_labels[4].text = f"anim_loop: {animated_entity.anim_loop}" - status_labels[5].text = f"anim_paused: {animated_entity.anim_paused}" - status_labels[6].text = f"anim_frame: {animated_entity.anim_frame}" - - auto_labels[0].text = f"auto_animate: {animated_entity.auto_animate}" - auto_labels[1].text = f"walk_clip: '{animated_entity.walk_clip}'" - auto_labels[2].text = f"idle_clip: '{animated_entity.idle_clip}'" - -# Key handler -def on_key(key, state): - if state != mcrfpy.InputState.PRESSED: - return - - if animated_entity: - if key == mcrfpy.Key.SPACE: - animated_entity.anim_paused = not animated_entity.anim_paused - status.text = f"Status: {'Paused' if animated_entity.anim_paused else 'Playing'}" - - elif key == mcrfpy.Key.L: - animated_entity.anim_loop = not animated_entity.anim_loop - status.text = f"Status: Loop {'ON' if animated_entity.anim_loop else 'OFF'}" - - elif key == mcrfpy.Key.EQUAL or key == mcrfpy.Key.ADD: - animated_entity.anim_speed = min(animated_entity.anim_speed + 0.25, 4.0) - status.text = f"Status: Speed {animated_entity.anim_speed:.2f}x" - - elif key == mcrfpy.Key.HYPHEN or key == mcrfpy.Key.SUBTRACT: - animated_entity.anim_speed = max(animated_entity.anim_speed - 0.25, 0.0) - status.text = f"Status: Speed {animated_entity.anim_speed:.2f}x" - - elif key == mcrfpy.Key.R: - animated_entity.anim_time = 0.0 - status.text = "Status: Animation reset" - - # Camera presets - if key == mcrfpy.Key.NUM_1: - pos, target, name = camera_presets[0] - viewport.camera_pos = pos - viewport.camera_target = target - status.text = f"Camera: {name}" - - elif key == mcrfpy.Key.NUM_2: - pos, target, name = camera_presets[1] - viewport.camera_pos = pos - viewport.camera_target = target - status.text = f"Camera: {name}" - - elif key == mcrfpy.Key.NUM_3: - pos, target, name = camera_presets[2] - viewport.camera_pos = pos - viewport.camera_target = target - status.text = f"Camera: {name}" - - elif key == mcrfpy.Key.ESCAPE: - mcrfpy.exit() - -# Set up scene -scene.on_key = on_key - -# Create timer for updates -timer = mcrfpy.Timer("anim_update", update, 16) - -# Activate scene -mcrfpy.current_scene = scene - -print() -print("Skeletal Animation Demo loaded!") -print("Controls:") -print(" [Space] Toggle pause") -print(" [L] Toggle loop") -print(" [+/-] Adjust speed") -print(" [R] Reset time") -print(" [1-3] Camera presets") -print(" [ESC] Quit") diff --git a/tests/demo/screens/voxel_core_demo.py b/tests/demo/screens/voxel_core_demo.py deleted file mode 100644 index 47c2627..0000000 --- a/tests/demo/screens/voxel_core_demo.py +++ /dev/null @@ -1,263 +0,0 @@ -"""VoxelGrid Core Demo (Milestone 9) - -Demonstrates the VoxelGrid data structure without rendering. -This is a "console demo" that creates VoxelGrids, defines materials, -places voxel patterns, and displays statistics. - -Note: Visual rendering comes in Milestone 10 (VoxelMeshing). -""" -import mcrfpy -from mcrfpy import Color - -def format_bytes(bytes_val): - """Format bytes as human-readable string""" - if bytes_val < 1024: - return f"{bytes_val} B" - elif bytes_val < 1024 * 1024: - return f"{bytes_val / 1024:.1f} KB" - else: - return f"{bytes_val / (1024 * 1024):.1f} MB" - -def print_header(title): - """Print a formatted header""" - print("\n" + "=" * 60) - print(f" {title}") - print("=" * 60) - -def print_grid_stats(vg, name="VoxelGrid"): - """Print statistics for a VoxelGrid""" - print(f"\n {name}:") - print(f" Dimensions: {vg.width} x {vg.height} x {vg.depth}") - print(f" Total voxels: {vg.width * vg.height * vg.depth:,}") - print(f" Cell size: {vg.cell_size} units") - print(f" Materials: {vg.material_count}") - print(f" Non-air voxels: {vg.count_non_air():,}") - print(f" Memory estimate: {format_bytes(vg.width * vg.height * vg.depth)}") - print(f" Offset: {vg.offset}") - print(f" Rotation: {vg.rotation} deg") - -def demo_basic_creation(): - """Demonstrate basic VoxelGrid creation""" - print_header("1. Basic VoxelGrid Creation") - - # Create various sizes - small = mcrfpy.VoxelGrid(size=(8, 4, 8)) - medium = mcrfpy.VoxelGrid(size=(16, 8, 16), cell_size=1.0) - large = mcrfpy.VoxelGrid(size=(32, 16, 32), cell_size=0.5) - - print_grid_stats(small, "Small (8x4x8)") - print_grid_stats(medium, "Medium (16x8x16)") - print_grid_stats(large, "Large (32x16x32, 0.5 cell size)") - -def demo_material_palette(): - """Demonstrate material palette system""" - print_header("2. Material Palette System") - - vg = mcrfpy.VoxelGrid(size=(16, 8, 16)) - - # Define a palette of building materials - materials = {} - materials['stone'] = vg.add_material("stone", color=Color(128, 128, 128)) - materials['brick'] = vg.add_material("brick", color=Color(165, 42, 42)) - materials['wood'] = vg.add_material("wood", color=Color(139, 90, 43)) - materials['glass'] = vg.add_material("glass", - color=Color(200, 220, 255, 128), - transparent=True, - path_cost=1.0) - materials['metal'] = vg.add_material("metal", - color=Color(180, 180, 190), - path_cost=0.8) - materials['grass'] = vg.add_material("grass", color=Color(60, 150, 60)) - - print(f"\n Defined {vg.material_count} materials:") - print(f" ID 0: air (implicit, always transparent)") - - for name, mat_id in materials.items(): - mat = vg.get_material(mat_id) - c = mat['color'] - props = [] - if mat['transparent']: - props.append("transparent") - if mat['path_cost'] != 1.0: - props.append(f"cost={mat['path_cost']}") - props_str = f" ({', '.join(props)})" if props else "" - print(f" ID {mat_id}: {name} RGB({c.r},{c.g},{c.b},{c.a}){props_str}") - - return vg, materials - -def demo_voxel_placement(): - """Demonstrate voxel placement patterns""" - print_header("3. Voxel Placement Patterns") - - vg, materials = demo_material_palette() - stone = materials['stone'] - brick = materials['brick'] - wood = materials['wood'] - - # Pattern 1: Solid cube - print("\n Pattern: Solid 4x4x4 cube at origin") - for z in range(4): - for y in range(4): - for x in range(4): - vg.set(x, y, z, stone) - print(f" Placed {vg.count_material(stone)} stone voxels") - - # Pattern 2: Checkerboard floor - print("\n Pattern: Checkerboard floor at y=0, x=6-14, z=0-8") - for z in range(8): - for x in range(6, 14): - mat = stone if (x + z) % 2 == 0 else brick - vg.set(x, 0, z, mat) - print(f" Stone: {vg.count_material(stone)}, Brick: {vg.count_material(brick)}") - - # Pattern 3: Hollow cube (walls only) - print("\n Pattern: Hollow cube frame 4x4x4 at x=10, z=10") - for x in range(4): - for y in range(4): - for z in range(4): - # Only place on edges - on_edge_x = (x == 0 or x == 3) - on_edge_y = (y == 0 or y == 3) - on_edge_z = (z == 0 or z == 3) - if sum([on_edge_x, on_edge_y, on_edge_z]) >= 2: - vg.set(10 + x, y, 10 + z, wood) - print(f" Wood voxels: {vg.count_material(wood)}") - - print_grid_stats(vg, "After patterns") - - # Material breakdown - print("\n Material breakdown:") - print(f" Air: {vg.count_material(0):,} ({100 * vg.count_material(0) / (16*8*16):.1f}%)") - print(f" Stone: {vg.count_material(stone):,}") - print(f" Brick: {vg.count_material(brick):,}") - print(f" Wood: {vg.count_material(wood):,}") - -def demo_bulk_operations(): - """Demonstrate bulk fill and clear operations""" - print_header("4. Bulk Operations") - - vg = mcrfpy.VoxelGrid(size=(32, 8, 32)) - total = 32 * 8 * 32 - - stone = vg.add_material("stone", color=Color(128, 128, 128)) - - print(f"\n Grid: 32x8x32 = {total:,} voxels") - - # Fill - vg.fill(stone) - print(f" After fill(stone): {vg.count_non_air():,} non-air") - - # Clear - vg.clear() - print(f" After clear(): {vg.count_non_air():,} non-air") - -def demo_transforms(): - """Demonstrate transform properties""" - print_header("5. Transform Properties") - - vg = mcrfpy.VoxelGrid(size=(8, 8, 8)) - - print(f"\n Default state:") - print(f" Offset: {vg.offset}") - print(f" Rotation: {vg.rotation} deg") - - # Position for a building - vg.offset = (100.0, 0.0, 50.0) - vg.rotation = 45.0 - - print(f"\n After positioning:") - print(f" Offset: {vg.offset}") - print(f" Rotation: {vg.rotation} deg") - - # Multiple buildings with different transforms - print("\n Example: Village layout with 3 buildings") - buildings = [] - positions = [(0, 0, 0), (20, 0, 0), (10, 0, 15)] - rotations = [0, 90, 45] - - for i, (pos, rot) in enumerate(zip(positions, rotations)): - b = mcrfpy.VoxelGrid(size=(8, 6, 8)) - b.offset = pos - b.rotation = rot - buildings.append(b) - print(f" Building {i+1}: offset={pos}, rotation={rot} deg") - -def demo_edge_cases(): - """Test edge cases and limits""" - print_header("6. Edge Cases and Limits") - - # Maximum practical size - print("\n Testing large grid (64x64x64)...") - large = mcrfpy.VoxelGrid(size=(64, 64, 64)) - mat = large.add_material("test", color=Color(128, 128, 128)) - large.fill(mat) - print(f" Created and filled: {large.count_non_air():,} voxels") - large.clear() - print(f" Cleared: {large.count_non_air()} voxels") - - # Bounds checking - print("\n Bounds checking (should not crash):") - small = mcrfpy.VoxelGrid(size=(4, 4, 4)) - test_mat = small.add_material("test", color=Color(255, 0, 0)) - small.set(-1, 0, 0, test_mat) - small.set(100, 0, 0, test_mat) - print(f" Out-of-bounds get(-1,0,0): {small.get(-1, 0, 0)} (expected 0)") - print(f" Out-of-bounds get(100,0,0): {small.get(100, 0, 0)} (expected 0)") - - # Material palette capacity - print("\n Material palette capacity test:") - full_vg = mcrfpy.VoxelGrid(size=(4, 4, 4)) - for i in range(255): - full_vg.add_material(f"mat_{i}", color=Color(i, i, i)) - print(f" Added 255 materials: count = {full_vg.material_count}") - - try: - full_vg.add_material("overflow", color=Color(255, 255, 255)) - print(" ERROR: Should have raised exception!") - except RuntimeError as e: - print(f" 256th material correctly rejected: {e}") - -def demo_memory_usage(): - """Show memory usage for various grid sizes""" - print_header("7. Memory Usage Estimates") - - sizes = [ - (8, 8, 8), - (16, 8, 16), - (32, 16, 32), - (64, 32, 64), - (80, 16, 45), # Example dungeon size - ] - - print("\n Size Voxels Memory") - print(" " + "-" * 40) - - for w, h, d in sizes: - voxels = w * h * d - memory = voxels # 1 byte per voxel - print(f" {w:3}x{h:3}x{d:3} {voxels:>10,} {format_bytes(memory):>10}") - -def main(): - """Run all demos""" - print("\n" + "=" * 60) - print(" VOXELGRID CORE DEMO (Milestone 9)") - print(" Dense 3D Voxel Array with Material Palette") - print("=" * 60) - - demo_basic_creation() - demo_material_palette() - demo_voxel_placement() - demo_bulk_operations() - demo_transforms() - demo_edge_cases() - demo_memory_usage() - - print_header("Demo Complete!") - print("\n Next milestone (10): Voxel Mesh Generation") - print(" The VoxelGrid data will be converted to renderable 3D meshes.") - print() - -if __name__ == "__main__": - import sys - main() - sys.exit(0) diff --git a/tests/demo/screens/voxel_dungeon_demo.py b/tests/demo/screens/voxel_dungeon_demo.py deleted file mode 100644 index 33a5799..0000000 --- a/tests/demo/screens/voxel_dungeon_demo.py +++ /dev/null @@ -1,273 +0,0 @@ -# voxel_dungeon_demo.py - Procedural dungeon demonstrating bulk voxel operations -# Milestone 11: Bulk Operations and Building Primitives - -import mcrfpy -import sys -import math -import random - -# Create demo scene -scene = mcrfpy.Scene("voxel_dungeon_demo") - -# Dark background -bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(20, 20, 30)) -scene.children.append(bg) - -# Title -title = mcrfpy.Caption(text="Voxel Dungeon Demo - Bulk Operations (Milestone 11)", pos=(20, 10)) -title.fill_color = mcrfpy.Color(255, 255, 255) -scene.children.append(title) - -# Create the 3D viewport -viewport = mcrfpy.Viewport3D( - pos=(50, 60), - size=(620, 520), - render_resolution=(400, 320), - fov=60.0, - camera_pos=(40.0, 30.0, 40.0), - camera_target=(16.0, 4.0, 16.0), - bg_color=mcrfpy.Color(30, 30, 40) # Dark atmosphere -) -scene.children.append(viewport) - -# Global voxel grid reference -voxels = None -seed = 42 - -def generate_dungeon(dungeon_seed=42): - """Generate a procedural dungeon showcasing all bulk operations""" - global voxels, seed - seed = dungeon_seed - random.seed(seed) - - # Create voxel grid for dungeon - print(f"Generating dungeon (seed={seed})...") - voxels = mcrfpy.VoxelGrid(size=(32, 12, 32), cell_size=1.0) - - # Define materials - STONE_WALL = voxels.add_material("stone_wall", color=mcrfpy.Color(80, 80, 90)) - STONE_FLOOR = voxels.add_material("stone_floor", color=mcrfpy.Color(100, 95, 90)) - MOSS = voxels.add_material("moss", color=mcrfpy.Color(40, 80, 40)) - WATER = voxels.add_material("water", color=mcrfpy.Color(40, 80, 160, 180), transparent=True) - PILLAR = voxels.add_material("pillar", color=mcrfpy.Color(120, 110, 100)) - GOLD = voxels.add_material("gold", color=mcrfpy.Color(255, 215, 0)) - - print(f"Defined {voxels.material_count} materials") - - # 1. Main room using fill_box_hollow - print("Building main room with fill_box_hollow...") - voxels.fill_box_hollow((2, 0, 2), (29, 10, 29), STONE_WALL, thickness=1) - - # 2. Floor with slight variation using fill_box - voxels.fill_box((3, 0, 3), (28, 0, 28), STONE_FLOOR) - - # 3. Spherical alcoves carved into walls using fill_sphere - print("Carving alcoves with fill_sphere...") - alcove_positions = [ - (2, 5, 16), # West wall - (29, 5, 16), # East wall - (16, 5, 2), # North wall - (16, 5, 29), # South wall - ] - for pos in alcove_positions: - voxels.fill_sphere(pos, 3, 0) # Carve out (air) - - # 4. Small decorative spheres (gold orbs in alcoves) - print("Adding gold orbs in alcoves...") - for i, pos in enumerate(alcove_positions): - # Offset inward so orb is visible - ox, oy, oz = pos - if ox < 10: - ox += 2 - elif ox > 20: - ox -= 2 - if oz < 10: - oz += 2 - elif oz > 20: - oz -= 2 - voxels.fill_sphere((ox, oy - 1, oz), 1, GOLD) - - # 5. Support pillars using fill_cylinder - print("Building pillars with fill_cylinder...") - pillar_positions = [ - (8, 1, 8), (8, 1, 24), - (24, 1, 8), (24, 1, 24), - (16, 1, 8), (16, 1, 24), - (8, 1, 16), (24, 1, 16), - ] - for px, py, pz in pillar_positions: - voxels.fill_cylinder((px, py, pz), 1, 9, PILLAR) - - # 6. Moss patches using fill_noise - print("Adding moss patches with fill_noise...") - voxels.fill_noise((3, 1, 3), (28, 1, 28), MOSS, threshold=0.65, scale=0.15, seed=seed) - - # 7. Central water pool - print("Creating water pool...") - voxels.fill_box((12, 0, 12), (20, 0, 20), 0) # Carve depression - voxels.fill_box((12, 0, 12), (20, 0, 20), WATER) - - # 8. Copy a pillar as prefab and paste variations - print("Creating prefab from pillar and pasting copies...") - pillar_prefab = voxels.copy_region((8, 1, 8), (9, 9, 9)) - print(f" Pillar prefab: {pillar_prefab.size}") - - # Paste smaller pillars at corners (offset from main room) - corner_positions = [(4, 1, 4), (4, 1, 27), (27, 1, 4), (27, 1, 27)] - for cx, cy, cz in corner_positions: - voxels.paste_region(pillar_prefab, (cx, cy, cz), skip_air=True) - - # Build mesh - voxels.rebuild_mesh() - - print(f"\nDungeon generated:") - print(f" Non-air voxels: {voxels.count_non_air()}") - print(f" Vertices: {voxels.vertex_count}") - print(f" Faces: {voxels.vertex_count // 6}") - - # Add to viewport - # First remove old layer if exists - if viewport.voxel_layer_count() > 0: - pass # Can't easily remove, so we regenerate the whole viewport - viewport.add_voxel_layer(voxels, z_index=0) - - return voxels - -# Generate initial dungeon -generate_dungeon(42) - -# Create info panel -info_frame = mcrfpy.Frame(pos=(690, 60), size=(300, 280), fill_color=mcrfpy.Color(40, 40, 60, 220)) -scene.children.append(info_frame) - -info_title = mcrfpy.Caption(text="Dungeon Stats", pos=(700, 70)) -info_title.fill_color = mcrfpy.Color(255, 255, 100) -scene.children.append(info_title) - -def update_stats(): - global stats_caption - stats_text = f"""Grid: {voxels.width}x{voxels.height}x{voxels.depth} -Total cells: {voxels.width * voxels.height * voxels.depth} -Non-air: {voxels.count_non_air()} -Materials: {voxels.material_count} - -Mesh Stats: - Vertices: {voxels.vertex_count} - Faces: {voxels.vertex_count // 6} - -Seed: {seed} - -Operations Used: - - fill_box_hollow (walls) - - fill_sphere (alcoves) - - fill_cylinder (pillars) - - fill_noise (moss) - - copy/paste (prefabs)""" - stats_caption.text = stats_text - -stats_caption = mcrfpy.Caption(text="", pos=(700, 100)) -stats_caption.fill_color = mcrfpy.Color(200, 200, 200) -scene.children.append(stats_caption) -update_stats() - -# Controls panel -controls_frame = mcrfpy.Frame(pos=(690, 360), size=(300, 180), fill_color=mcrfpy.Color(40, 40, 60, 220)) -scene.children.append(controls_frame) - -controls_title = mcrfpy.Caption(text="Controls", pos=(700, 370)) -controls_title.fill_color = mcrfpy.Color(255, 255, 100) -scene.children.append(controls_title) - -controls_text = """R - Regenerate dungeon (new seed) -1-4 - Camera presets -+/- - Zoom in/out -SPACE - Reset camera -ESC - Exit demo""" - -controls = mcrfpy.Caption(text=controls_text, pos=(700, 400)) -controls.fill_color = mcrfpy.Color(200, 200, 200) -scene.children.append(controls) - -# Camera animation state -rotation_enabled = False -camera_distance = 50.0 -camera_angle = 45.0 # degrees -camera_height = 30.0 - -camera_presets = [ - (40.0, 30.0, 40.0, 16.0, 4.0, 16.0), # Default diagonal - (16.0, 30.0, 50.0, 16.0, 4.0, 16.0), # Front view - (50.0, 30.0, 16.0, 16.0, 4.0, 16.0), # Side view - (16.0, 50.0, 16.0, 16.0, 4.0, 16.0), # Top-down -] - -def rotate_camera(timer_name, runtime): - """Timer callback for camera rotation""" - global camera_angle, rotation_enabled - if rotation_enabled: - camera_angle += 0.5 - if camera_angle >= 360.0: - camera_angle = 0.0 - rad = camera_angle * math.pi / 180.0 - x = 16.0 + camera_distance * math.cos(rad) - z = 16.0 + camera_distance * math.sin(rad) - viewport.camera_pos = (x, camera_height, z) - -# Set up rotation timer -timer = mcrfpy.Timer("rotate_cam", rotate_camera, 33) - -def handle_key(key, action): - """Keyboard handler""" - global rotation_enabled, seed, camera_distance, camera_height - if action != mcrfpy.InputState.PRESSED: - return - - if key == mcrfpy.Key.R: - seed = random.randint(1, 99999) - generate_dungeon(seed) - update_stats() - print(f"Regenerated dungeon with seed {seed}") - elif key == mcrfpy.Key.NUM_1: - viewport.camera_pos = camera_presets[0][:3] - viewport.camera_target = camera_presets[0][3:] - rotation_enabled = False - elif key == mcrfpy.Key.NUM_2: - viewport.camera_pos = camera_presets[1][:3] - viewport.camera_target = camera_presets[1][3:] - rotation_enabled = False - elif key == mcrfpy.Key.NUM_3: - viewport.camera_pos = camera_presets[2][:3] - viewport.camera_target = camera_presets[2][3:] - rotation_enabled = False - elif key == mcrfpy.Key.NUM_4: - viewport.camera_pos = camera_presets[3][:3] - viewport.camera_target = camera_presets[3][3:] - rotation_enabled = False - elif key == mcrfpy.Key.SPACE: - rotation_enabled = not rotation_enabled - print(f"Camera rotation: {'ON' if rotation_enabled else 'OFF'}") - elif key == mcrfpy.Key.EQUALS or key == mcrfpy.Key.ADD: - camera_distance = max(20.0, camera_distance - 5.0) - camera_height = max(15.0, camera_height - 2.0) - elif key == mcrfpy.Key.DASH or key == mcrfpy.Key.SUBTRACT: - camera_distance = min(80.0, camera_distance + 5.0) - camera_height = min(50.0, camera_height + 2.0) - elif key == mcrfpy.Key.ESCAPE: - print("Exiting demo...") - sys.exit(0) - -scene.on_key = handle_key - -# Activate the scene -mcrfpy.current_scene = scene -print("\nVoxel Dungeon Demo ready!") -print("Press SPACE to toggle camera rotation, R to regenerate") - -# Main entry point for --exec mode -if __name__ == "__main__": - print("\n=== Voxel Dungeon Demo Summary ===") - print(f"Grid size: {voxels.width}x{voxels.height}x{voxels.depth}") - print(f"Non-air voxels: {voxels.count_non_air()}") - print(f"Generated vertices: {voxels.vertex_count}") - print(f"Rendered faces: {voxels.vertex_count // 6}") - print("===================================\n") diff --git a/tests/demo/screens/voxel_meshing_demo.py b/tests/demo/screens/voxel_meshing_demo.py deleted file mode 100644 index 862002b..0000000 --- a/tests/demo/screens/voxel_meshing_demo.py +++ /dev/null @@ -1,218 +0,0 @@ -# voxel_meshing_demo.py - Visual demo of VoxelGrid mesh rendering -# Shows voxel building rendered in Viewport3D with PS1 effects - -import mcrfpy -import sys -import math - -# Create demo scene -scene = mcrfpy.Scene("voxel_meshing_demo") - -# Dark background frame -bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25)) -scene.children.append(bg) - -# Title -title = mcrfpy.Caption(text="VoxelGrid Meshing Demo - Face-Culled 3D Voxels", pos=(20, 10)) -title.fill_color = mcrfpy.Color(255, 255, 255) -scene.children.append(title) - -# Create the 3D viewport -viewport = mcrfpy.Viewport3D( - pos=(50, 60), - size=(600, 500), - render_resolution=(320, 240), # PS1 resolution - fov=60.0, - camera_pos=(20.0, 15.0, 20.0), - camera_target=(4.0, 2.0, 4.0), - bg_color=mcrfpy.Color(50, 70, 100) # Sky color -) -scene.children.append(viewport) - -# Create voxel grid for building -print("Creating voxel building...") -voxels = mcrfpy.VoxelGrid(size=(12, 8, 12), cell_size=1.0) - -# Define materials -STONE = voxels.add_material("stone", color=mcrfpy.Color(128, 128, 128)) -BRICK = voxels.add_material("brick", color=mcrfpy.Color(165, 82, 42)) -WOOD = voxels.add_material("wood", color=mcrfpy.Color(139, 90, 43)) -GLASS = voxels.add_material("glass", color=mcrfpy.Color(180, 220, 255, 180), transparent=True) -GRASS = voxels.add_material("grass", color=mcrfpy.Color(60, 150, 60)) - -print(f"Defined {voxels.material_count} materials") - -# Build a simple house structure - -# Ground/foundation -voxels.fill_box((0, 0, 0), (11, 0, 11), GRASS) - -# Floor -voxels.fill_box((1, 1, 1), (10, 1, 10), STONE) - -# Walls -# Front wall (Z=1) -voxels.fill_box((1, 2, 1), (10, 5, 1), BRICK) -# Back wall (Z=10) -voxels.fill_box((1, 2, 10), (10, 5, 10), BRICK) -# Left wall (X=1) -voxels.fill_box((1, 2, 1), (1, 5, 10), BRICK) -# Right wall (X=10) -voxels.fill_box((10, 2, 1), (10, 5, 10), BRICK) - -# Door opening (front wall) -voxels.fill_box((4, 2, 1), (6, 4, 1), 0) # Clear door opening - -# Windows -# Front windows (beside door) -voxels.fill_box((2, 3, 1), (3, 4, 1), GLASS) -voxels.fill_box((8, 3, 1), (9, 4, 1), GLASS) -# Side windows -voxels.fill_box((1, 3, 4), (1, 4, 5), GLASS) -voxels.fill_box((1, 3, 7), (1, 4, 8), GLASS) -voxels.fill_box((10, 3, 4), (10, 4, 5), GLASS) -voxels.fill_box((10, 3, 7), (10, 4, 8), GLASS) - -# Ceiling -voxels.fill_box((1, 6, 1), (10, 6, 10), WOOD) - -# Simple roof (peaked) -voxels.fill_box((0, 7, 0), (11, 7, 11), WOOD) -voxels.fill_box((1, 8, 1), (10, 8, 10), WOOD) -voxels.fill_box((2, 9, 2), (9, 9, 9), WOOD) -voxels.fill_box((3, 10, 3), (8, 10, 8), WOOD) -voxels.fill_box((4, 11, 4), (7, 11, 7), WOOD) - -# Build the mesh -voxels.rebuild_mesh() - -print(f"Built voxel house:") -print(f" Non-air voxels: {voxels.count_non_air()}") -print(f" Vertices: {voxels.vertex_count}") -print(f" Faces: {voxels.vertex_count // 6}") - -# Position the building -voxels.offset = (0.0, 0.0, 0.0) -voxels.rotation = 0.0 - -# Add to viewport -viewport.add_voxel_layer(voxels, z_index=0) -print(f"Added voxel layer to viewport (count: {viewport.voxel_layer_count()})") - -# Create info panel -info_frame = mcrfpy.Frame(pos=(680, 60), size=(300, 250), fill_color=mcrfpy.Color(40, 40, 60, 200)) -scene.children.append(info_frame) - -info_title = mcrfpy.Caption(text="Building Stats", pos=(690, 70)) -info_title.fill_color = mcrfpy.Color(255, 255, 100) -scene.children.append(info_title) - -stats_text = f"""Grid: {voxels.width}x{voxels.height}x{voxels.depth} -Total voxels: {voxels.width * voxels.height * voxels.depth} -Non-air: {voxels.count_non_air()} -Materials: {voxels.material_count} -Vertices: {voxels.vertex_count} -Faces: {voxels.vertex_count // 6} - -Without culling would be: - {voxels.count_non_air() * 36} vertices - ({100 - (voxels.vertex_count / (voxels.count_non_air() * 36) * 100):.0f}% reduction)""" - -stats = mcrfpy.Caption(text=stats_text, pos=(690, 100)) -stats.fill_color = mcrfpy.Color(200, 200, 200) -scene.children.append(stats) - -# Controls info -controls_frame = mcrfpy.Frame(pos=(680, 330), size=(300, 180), fill_color=mcrfpy.Color(40, 40, 60, 200)) -scene.children.append(controls_frame) - -controls_title = mcrfpy.Caption(text="Controls", pos=(690, 340)) -controls_title.fill_color = mcrfpy.Color(255, 255, 100) -scene.children.append(controls_title) - -controls_text = """R - Toggle rotation -1-5 - Change camera angle -SPACE - Reset camera -ESC - Exit demo""" - -controls = mcrfpy.Caption(text=controls_text, pos=(690, 370)) -controls.fill_color = mcrfpy.Color(200, 200, 200) -scene.children.append(controls) - -# Animation state -rotation_enabled = False -current_angle = 0.0 -camera_angles = [ - (20.0, 15.0, 20.0), # Default - diagonal view - (0.0, 15.0, 25.0), # Front view - (25.0, 15.0, 0.0), # Side view - (5.5, 25.0, 5.5), # Top-down view - (5.5, 3.0, 20.0), # Low angle -] -current_camera = 0 - -def rotate_building(timer, runtime): - """Timer callback for building rotation""" - global current_angle, rotation_enabled - if rotation_enabled: - current_angle += 1.0 - if current_angle >= 360.0: - current_angle = 0.0 - voxels.rotation = current_angle - -# Set up rotation timer -timer = mcrfpy.Timer("rotate", rotate_building, 33) # ~30 FPS - -def handle_key(key, action): - """Keyboard handler""" - global rotation_enabled, current_camera - if action != mcrfpy.InputState.PRESSED: - return - - if key == mcrfpy.Key.R: - rotation_enabled = not rotation_enabled - print(f"Rotation: {'ON' if rotation_enabled else 'OFF'}") - elif key == mcrfpy.Key.NUM_1: - current_camera = 0 - viewport.camera_pos = camera_angles[0] - print("Camera: Default diagonal") - elif key == mcrfpy.Key.NUM_2: - current_camera = 1 - viewport.camera_pos = camera_angles[1] - print("Camera: Front view") - elif key == mcrfpy.Key.NUM_3: - current_camera = 2 - viewport.camera_pos = camera_angles[2] - print("Camera: Side view") - elif key == mcrfpy.Key.NUM_4: - current_camera = 3 - viewport.camera_pos = camera_angles[3] - print("Camera: Top-down view") - elif key == mcrfpy.Key.NUM_5: - current_camera = 4 - viewport.camera_pos = camera_angles[4] - print("Camera: Low angle") - elif key == mcrfpy.Key.SPACE: - current_camera = 0 - voxels.rotation = 0.0 - viewport.camera_pos = camera_angles[0] - print("Camera: Reset") - elif key == mcrfpy.Key.ESCAPE: - print("Exiting demo...") - sys.exit(0) - -scene.on_key = handle_key - -# Activate the scene -mcrfpy.current_scene = scene -print("Voxel Meshing Demo ready! Press R to toggle rotation.") - -# Main entry point for --exec mode -if __name__ == "__main__": - # Demo is set up, print summary - print("\n=== Voxel Meshing Demo Summary ===") - print(f"Grid size: {voxels.width}x{voxels.height}x{voxels.depth}") - print(f"Non-air voxels: {voxels.count_non_air()}") - print(f"Generated vertices: {voxels.vertex_count}") - print(f"Rendered faces: {voxels.vertex_count // 6}") - print("===================================\n") diff --git a/tests/demo/screens/voxel_navigation_demo.py b/tests/demo/screens/voxel_navigation_demo.py deleted file mode 100644 index 7cb36e9..0000000 --- a/tests/demo/screens/voxel_navigation_demo.py +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/env python3 -"""Visual Demo: Milestone 12 - VoxelGrid Navigation Projection - -Demonstrates projection of 3D voxel terrain to 2D navigation grid for pathfinding. -Shows: -1. Voxel dungeon with multiple levels -2. Navigation grid projection (walkable/unwalkable areas) -3. A* pathfinding through the projected terrain -4. FOV computation from voxel transparency -""" - -import mcrfpy -import sys -import math - -def create_demo_scene(): - """Create the navigation projection demo scene""" - - scene = mcrfpy.Scene("voxel_nav_demo") - - # ========================================================================= - # Create a small dungeon-style voxel grid - # ========================================================================= - - vg = mcrfpy.VoxelGrid((16, 8, 16), cell_size=1.0) - - # Add materials - floor_mat = vg.add_material("floor", (100, 80, 60)) # Brown floor - wall_mat = vg.add_material("wall", (80, 80, 90), transparent=False) # Gray walls - pillar_mat = vg.add_material("pillar", (60, 60, 70), transparent=False) # Dark pillars - glass_mat = vg.add_material("glass", (150, 200, 255), transparent=True) # Transparent glass - water_mat = vg.add_material("water", (50, 100, 200), transparent=True, path_cost=3.0) # Slow water - - # Create floor - vg.fill_box((0, 0, 0), (15, 0, 15), floor_mat) - - # Create outer walls - vg.fill_box((0, 1, 0), (15, 4, 0), wall_mat) # North wall - vg.fill_box((0, 1, 15), (15, 4, 15), wall_mat) # South wall - vg.fill_box((0, 1, 0), (0, 4, 15), wall_mat) # West wall - vg.fill_box((15, 1, 0), (15, 4, 15), wall_mat) # East wall - - # Interior walls creating rooms - vg.fill_box((5, 1, 0), (5, 4, 10), wall_mat) # Vertical wall - vg.fill_box((10, 1, 5), (15, 4, 5), wall_mat) # Horizontal wall - - # Doorways (carve holes) - vg.fill_box((5, 1, 3), (5, 2, 4), 0) # Door in vertical wall - vg.fill_box((12, 1, 5), (13, 2, 5), 0) # Door in horizontal wall - - # Central pillars - vg.fill_box((8, 1, 8), (8, 4, 8), pillar_mat) - vg.fill_box((8, 1, 12), (8, 4, 12), pillar_mat) - - # Water pool in one corner (slow movement) - vg.fill_box((1, 0, 11), (3, 0, 14), water_mat) - - # Glass window - vg.fill_box((10, 2, 5), (11, 3, 5), glass_mat) - - # Raised platform in one area (height variation) - vg.fill_box((12, 1, 8), (14, 1, 13), floor_mat) # Platform at y=1 - - # ========================================================================= - # Create Viewport3D with navigation grid - # ========================================================================= - - viewport = mcrfpy.Viewport3D(pos=(10, 10), size=(600, 400)) - viewport.set_grid_size(16, 16) - viewport.cell_size = 1.0 - - # Configure camera for top-down view - viewport.camera_pos = (8, 15, 20) - viewport.camera_target = (8, 0, 8) - - # Add voxel layer - viewport.add_voxel_layer(vg, z_index=0) - - # Project voxels to navigation grid with headroom=2 (entity needs 2 voxels height) - viewport.project_voxel_to_nav(vg, headroom=2) - - # ========================================================================= - # Info panel - # ========================================================================= - - info_frame = mcrfpy.Frame(pos=(620, 10), size=(250, 400)) - info_frame.fill_color = mcrfpy.Color(30, 30, 40, 220) - info_frame.outline_color = mcrfpy.Color(100, 100, 120) - info_frame.outline = 2.0 - - title = mcrfpy.Caption(text="Nav Projection Demo", pos=(10, 10)) - title.fill_color = mcrfpy.Color(255, 255, 100) - - desc = mcrfpy.Caption(text="Voxels projected to\n2D nav grid", pos=(10, 35)) - desc.fill_color = mcrfpy.Color(200, 200, 200) - - info1 = mcrfpy.Caption(text="Grid: 16x16 cells", pos=(10, 75)) - info1.fill_color = mcrfpy.Color(150, 200, 255) - - info2 = mcrfpy.Caption(text="Headroom: 2 voxels", pos=(10, 95)) - info2.fill_color = mcrfpy.Color(150, 200, 255) - - # Count walkable cells - walkable_count = 0 - for x in range(16): - for z in range(16): - cell = viewport.at(x, z) - if cell.walkable: - walkable_count += 1 - - info3 = mcrfpy.Caption(text=f"Walkable: {walkable_count}/256", pos=(10, 115)) - info3.fill_color = mcrfpy.Color(100, 255, 100) - - # Find path example - path = viewport.find_path((1, 1), (13, 13)) - info4 = mcrfpy.Caption(text=f"Path length: {len(path)}", pos=(10, 135)) - info4.fill_color = mcrfpy.Color(255, 200, 100) - - # FOV example - fov = viewport.compute_fov((8, 8), 10) - info5 = mcrfpy.Caption(text=f"FOV cells: {len(fov)}", pos=(10, 155)) - info5.fill_color = mcrfpy.Color(200, 150, 255) - - # Legend - legend_title = mcrfpy.Caption(text="Materials:", pos=(10, 185)) - legend_title.fill_color = mcrfpy.Color(255, 255, 255) - - leg1 = mcrfpy.Caption(text=" Floor (walkable)", pos=(10, 205)) - leg1.fill_color = mcrfpy.Color(100, 80, 60) - - leg2 = mcrfpy.Caption(text=" Wall (blocking)", pos=(10, 225)) - leg2.fill_color = mcrfpy.Color(80, 80, 90) - - leg3 = mcrfpy.Caption(text=" Water (slow)", pos=(10, 245)) - leg3.fill_color = mcrfpy.Color(50, 100, 200) - - leg4 = mcrfpy.Caption(text=" Glass (see-through)", pos=(10, 265)) - leg4.fill_color = mcrfpy.Color(150, 200, 255) - - controls = mcrfpy.Caption(text="[Space] Recompute FOV\n[P] Show path\n[Q] Quit", pos=(10, 300)) - controls.fill_color = mcrfpy.Color(150, 150, 150) - - info_frame.children.extend([ - title, desc, info1, info2, info3, info4, info5, - legend_title, leg1, leg2, leg3, leg4, controls - ]) - - # ========================================================================= - # Status bar - # ========================================================================= - - status_frame = mcrfpy.Frame(pos=(10, 420), size=(860, 50)) - status_frame.fill_color = mcrfpy.Color(20, 20, 30, 220) - status_frame.outline_color = mcrfpy.Color(80, 80, 100) - status_frame.outline = 1.0 - - status_text = mcrfpy.Caption( - text="Milestone 12: VoxelGrid Navigation Projection - Project 3D voxels to 2D pathfinding grid", - pos=(10, 15) - ) - status_text.fill_color = mcrfpy.Color(180, 180, 200) - status_frame.children.append(status_text) - - # ========================================================================= - # Add elements to scene - # ========================================================================= - - scene.children.extend([viewport, info_frame, status_frame]) - - # Store references for interaction (using module-level globals) - global demo_viewport, demo_voxelgrid, demo_path, demo_fov_origin - demo_viewport = viewport - demo_voxelgrid = vg - demo_path = path - demo_fov_origin = (8, 8) - - # ========================================================================= - # Keyboard handler - # ========================================================================= - - def on_key(key, state): - global demo_fov_origin - if state != mcrfpy.InputState.PRESSED: - return - - if key == mcrfpy.Key.Q or key == mcrfpy.Key.ESCAPE: - # Exit - sys.exit(0) - elif key == mcrfpy.Key.SPACE: - # Recompute FOV from different origin - ox, oz = demo_fov_origin - ox = (ox + 3) % 14 + 1 - oz = (oz + 5) % 14 + 1 - demo_fov_origin = (ox, oz) - fov = demo_viewport.compute_fov((ox, oz), 8) - info5.text = f"FOV from ({ox},{oz}): {len(fov)}" - elif key == mcrfpy.Key.P: - # Show path info - print(f"Path from (1,1) to (13,13): {len(demo_path)} steps") - for i, (px, pz) in enumerate(demo_path[:10]): - cell = demo_viewport.at(px, pz) - print(f" Step {i}: ({px},{pz}) h={cell.height:.1f} cost={cell.cost:.1f}") - if len(demo_path) > 10: - print(f" ... and {len(demo_path) - 10} more steps") - - scene.on_key = on_key - - return scene - -def main(): - """Main entry point""" - print("=== Milestone 12: VoxelGrid Navigation Projection Demo ===") - print() - print("This demo shows how 3D voxel terrain is projected to a 2D") - print("navigation grid for pathfinding and FOV calculations.") - print() - print("The projection scans each column from top to bottom, finding") - print("the topmost walkable floor with adequate headroom.") - print() - - scene = create_demo_scene() - mcrfpy.current_scene = scene - - # Print nav grid summary - grid_w, grid_d = demo_viewport.grid_size - print("Navigation grid summary:") - print(f" Grid size: {grid_w}x{grid_d}") - - # Count by walkability and transparency - walkable = 0 - blocking = 0 - transparent = 0 - for x in range(grid_w): - for z in range(grid_d): - cell = demo_viewport.at(x, z) - if cell.walkable: - walkable += 1 - else: - blocking += 1 - if cell.transparent: - transparent += 1 - - print(f" Walkable cells: {walkable}") - print(f" Blocking cells: {blocking}") - print(f" Transparent cells: {transparent}") - print() - -if __name__ == "__main__": - main() - sys.exit(0) diff --git a/tests/demo/screens/voxel_serialization_demo.py b/tests/demo/screens/voxel_serialization_demo.py deleted file mode 100644 index 9d11b91..0000000 --- a/tests/demo/screens/voxel_serialization_demo.py +++ /dev/null @@ -1,314 +0,0 @@ -"""Voxel Serialization Demo - Milestone 14 - -Demonstrates save/load functionality for VoxelGrid, including: -- Saving to file with .mcvg format -- Loading from file -- Serialization to bytes (for network/custom storage) -- RLE compression effectiveness -""" - -import mcrfpy -import os -import tempfile - -def create_demo_scene(): - """Create a scene demonstrating voxel serialization.""" - scene = mcrfpy.Scene("voxel_serialization_demo") - ui = scene.children - - # Dark background - bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=(20, 20, 30)) - ui.append(bg) - - # Title - title = mcrfpy.Caption(text="Milestone 14: VoxelGrid Serialization", - pos=(30, 20)) - title.font_size = 28 - title.fill_color = (255, 220, 100) - ui.append(title) - - # Create demo VoxelGrid with interesting structure - grid = mcrfpy.VoxelGrid((16, 16, 16), cell_size=1.0) - - # Add materials - stone = grid.add_material("stone", (100, 100, 110)) - wood = grid.add_material("wood", (139, 90, 43)) - glass = grid.add_material("glass", (180, 200, 220, 100), transparent=True) - gold = grid.add_material("gold", (255, 215, 0)) - - # Build a small structure - grid.fill_box((0, 0, 0), (15, 0, 15), stone) # Floor - grid.fill_box((0, 1, 0), (0, 4, 15), stone) # Wall 1 - grid.fill_box((15, 1, 0), (15, 4, 15), stone) # Wall 2 - grid.fill_box((0, 1, 0), (15, 4, 0), stone) # Wall 3 - grid.fill_box((0, 1, 15), (15, 4, 15), stone) # Wall 4 - - # Windows (clear some wall, add glass) - grid.fill_box((6, 2, 0), (10, 3, 0), 0) # Clear for window - grid.fill_box((6, 2, 0), (10, 3, 0), glass) # Add glass - - # Pillars - grid.fill_box((4, 1, 4), (4, 3, 4), wood) - grid.fill_box((12, 1, 4), (12, 3, 4), wood) - grid.fill_box((4, 1, 12), (4, 3, 12), wood) - grid.fill_box((12, 1, 12), (12, 3, 12), wood) - - # Gold decorations - grid.set(8, 1, 8, gold) - grid.set(7, 1, 8, gold) - grid.set(9, 1, 8, gold) - grid.set(8, 1, 7, gold) - grid.set(8, 1, 9, gold) - - # Get original stats - original_voxels = grid.count_non_air() - original_materials = grid.material_count - - # === Test save/load to file === - with tempfile.NamedTemporaryFile(suffix='.mcvg', delete=False) as f: - temp_path = f.name - - save_success = grid.save(temp_path) - file_size = os.path.getsize(temp_path) if save_success else 0 - - # Load into new grid - loaded_grid = mcrfpy.VoxelGrid((1, 1, 1)) - load_success = loaded_grid.load(temp_path) - os.unlink(temp_path) # Clean up - - loaded_voxels = loaded_grid.count_non_air() if load_success else 0 - loaded_materials = loaded_grid.material_count if load_success else 0 - - # === Test to_bytes/from_bytes === - data_bytes = grid.to_bytes() - bytes_size = len(data_bytes) - - bytes_grid = mcrfpy.VoxelGrid((1, 1, 1)) - bytes_success = bytes_grid.from_bytes(data_bytes) - bytes_voxels = bytes_grid.count_non_air() if bytes_success else 0 - - # === Calculate compression === - raw_size = 16 * 16 * 16 # Uncompressed voxel data - compression_ratio = raw_size / bytes_size if bytes_size > 0 else 0 - - # Display information - y_pos = 80 - - # Original Grid Info - info1 = mcrfpy.Caption(text="Original VoxelGrid:", - pos=(30, y_pos)) - info1.font_size = 20 - info1.fill_color = (100, 200, 255) - ui.append(info1) - y_pos += 30 - - for line in [ - f" Dimensions: 16x16x16 = 4096 voxels", - f" Non-air voxels: {original_voxels}", - f" Materials defined: {original_materials}", - f" Structure: Walled room with pillars, windows, gold decor" - ]: - cap = mcrfpy.Caption(text=line, pos=(30, y_pos)) - cap.font_size = 16 - cap.fill_color = (200, 200, 210) - ui.append(cap) - y_pos += 22 - - y_pos += 20 - - # File Save/Load Results - info2 = mcrfpy.Caption(text="File Serialization (.mcvg):", - pos=(30, y_pos)) - info2.font_size = 20 - info2.fill_color = (100, 255, 150) - ui.append(info2) - y_pos += 30 - - save_status = "SUCCESS" if save_success else "FAILED" - load_status = "SUCCESS" if load_success else "FAILED" - match_status = "MATCH" if loaded_voxels == original_voxels else "MISMATCH" - - for line in [ - f" Save to file: {save_status}", - f" File size: {file_size} bytes", - f" Load from file: {load_status}", - f" Loaded voxels: {loaded_voxels} ({match_status})", - f" Loaded materials: {loaded_materials}" - ]: - color = (150, 255, 150) if "SUCCESS" in line or "MATCH" in line else (200, 200, 210) - if "FAILED" in line or "MISMATCH" in line: - color = (255, 100, 100) - cap = mcrfpy.Caption(text=line, pos=(30, y_pos)) - cap.font_size = 16 - cap.fill_color = color - ui.append(cap) - y_pos += 22 - - y_pos += 20 - - # Bytes Serialization Results - info3 = mcrfpy.Caption(text="Memory Serialization (to_bytes/from_bytes):", - pos=(30, y_pos)) - info3.font_size = 20 - info3.fill_color = (255, 200, 100) - ui.append(info3) - y_pos += 30 - - bytes_status = "SUCCESS" if bytes_success else "FAILED" - bytes_match = "MATCH" if bytes_voxels == original_voxels else "MISMATCH" - - for line in [ - f" Serialized size: {bytes_size} bytes", - f" Raw voxel data: {raw_size} bytes", - f" Compression ratio: {compression_ratio:.1f}x", - f" from_bytes(): {bytes_status}", - f" Restored voxels: {bytes_voxels} ({bytes_match})" - ]: - color = (200, 200, 210) - if "SUCCESS" in line or "MATCH" in line: - color = (150, 255, 150) - cap = mcrfpy.Caption(text=line, pos=(30, y_pos)) - cap.font_size = 16 - cap.fill_color = color - ui.append(cap) - y_pos += 22 - - y_pos += 20 - - # RLE Compression Demo - info4 = mcrfpy.Caption(text="RLE Compression Effectiveness:", - pos=(30, y_pos)) - info4.font_size = 20 - info4.fill_color = (200, 150, 255) - ui.append(info4) - y_pos += 30 - - # Create uniform grid for compression test - uniform_grid = mcrfpy.VoxelGrid((32, 32, 32)) - uniform_mat = uniform_grid.add_material("solid", (128, 128, 128)) - uniform_grid.fill(uniform_mat) - uniform_bytes = uniform_grid.to_bytes() - uniform_raw = 32 * 32 * 32 - uniform_ratio = uniform_raw / len(uniform_bytes) - - for line in [ - f" Uniform 32x32x32 filled grid:", - f" Raw: {uniform_raw} bytes", - f" Compressed: {len(uniform_bytes)} bytes", - f" Compression: {uniform_ratio:.0f}x", - f" ", - f" RLE excels at runs of identical values." - ]: - cap = mcrfpy.Caption(text=line, pos=(30, y_pos)) - cap.font_size = 16 - cap.fill_color = (200, 180, 220) - ui.append(cap) - y_pos += 22 - - y_pos += 30 - - # File Format Info - info5 = mcrfpy.Caption(text="File Format (.mcvg):", - pos=(30, y_pos)) - info5.font_size = 20 - info5.fill_color = (255, 150, 200) - ui.append(info5) - y_pos += 30 - - for line in [ - " Header: Magic 'MCVG' + version + dimensions + cell_size", - " Materials: name, color (RGBA), sprite_index, transparent, path_cost", - " Voxel data: RLE-encoded material IDs", - " ", - " Note: Transform (offset, rotation) is runtime state, not serialized" - ]: - cap = mcrfpy.Caption(text=line, pos=(30, y_pos)) - cap.font_size = 14 - cap.fill_color = (200, 180, 200) - ui.append(cap) - y_pos += 20 - - # API Reference on right side - y_ref = 80 - x_ref = 550 - - api_title = mcrfpy.Caption(text="Python API:", pos=(x_ref, y_ref)) - api_title.font_size = 20 - api_title.fill_color = (150, 200, 255) - ui.append(api_title) - y_ref += 35 - - for line in [ - "# Save to file", - "success = grid.save('world.mcvg')", - "", - "# Load from file", - "grid = VoxelGrid((1,1,1))", - "success = grid.load('world.mcvg')", - "", - "# Save to bytes", - "data = grid.to_bytes()", - "", - "# Load from bytes", - "success = grid.from_bytes(data)", - "", - "# Network example:", - "# send_to_server(grid.to_bytes())", - "# data = recv_from_server()", - "# grid.from_bytes(data)" - ]: - cap = mcrfpy.Caption(text=line, pos=(x_ref, y_ref)) - cap.font_size = 14 - if line.startswith("#"): - cap.fill_color = (100, 150, 100) - elif "=" in line or "(" in line: - cap.fill_color = (255, 220, 150) - else: - cap.fill_color = (180, 180, 180) - ui.append(cap) - y_ref += 18 - - return scene - - -# Run demonstration -if __name__ == "__main__": - import sys - # Create and activate the scene - scene = create_demo_scene() - mcrfpy.current_scene = scene - - # When run directly, print summary and exit for headless testing - print("\n=== Voxel Serialization Demo (Milestone 14) ===\n") - - # Run a quick verification - grid = mcrfpy.VoxelGrid((8, 8, 8)) - mat = grid.add_material("test", (100, 100, 100)) - grid.fill_box((0, 0, 0), (7, 0, 7), mat) - - print(f"Created 8x8x8 grid with {grid.count_non_air()} non-air voxels") - - # Test to_bytes - data = grid.to_bytes() - print(f"Serialized to {len(data)} bytes") - - # Test from_bytes - grid2 = mcrfpy.VoxelGrid((1, 1, 1)) - success = grid2.from_bytes(data) - print(f"from_bytes(): {'SUCCESS' if success else 'FAILED'}") - print(f"Restored size: {grid2.size}") - print(f"Restored voxels: {grid2.count_non_air()}") - - # Compression test - big_grid = mcrfpy.VoxelGrid((32, 32, 32)) - big_mat = big_grid.add_material("solid", (128, 128, 128)) - big_grid.fill(big_mat) - big_data = big_grid.to_bytes() - raw_size = 32 * 32 * 32 - print(f"\nCompression test (32x32x32 uniform):") - print(f" Raw: {raw_size} bytes") - print(f" Compressed: {len(big_data)} bytes") - print(f" Ratio: {raw_size / len(big_data):.0f}x") - - print("\n=== Demo complete ===") - sys.exit(0)