// Viewport3D.cpp - 3D rendering viewport implementation #include "Viewport3D.h" #include "Shader3D.h" #include "../platform/GLContext.h" #include "PyVector.h" #include "PyColor.h" #include "PyPositionHelper.h" #include "McRFPy_Doc.h" #include #include // 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) // SFML backend - use GLAD #include #define MCRF_HAS_GL 1 #endif namespace mcrf { // ============================================================================= // Construction / Destruction // ============================================================================= Viewport3D::Viewport3D() : size_(320.0f, 240.0f) { position = sf::Vector2f(0, 0); camera_.setAspect(size_.x / size_.y); } Viewport3D::Viewport3D(float x, float y, float width, float height) : size_(width, height) { position = sf::Vector2f(x, y); camera_.setAspect(size_.x / size_.y); } Viewport3D::~Viewport3D() { cleanupTestGeometry(); cleanupFBO(); } // ============================================================================= // UIDrawable Interface // ============================================================================= void Viewport3D::render(sf::Vector2f offset, sf::RenderTarget& target) { if (!visible) return; // Initialize resources if needed (only on GL-ready backends) if (gl::isGLReady()) { if (fbo_ == 0) { initFBO(); } if (!shader_) { initShader(); } if (testVBO_ == 0) { initTestGeometry(); } // Save SFML's GL state before raw GL rendering // This is REQUIRED when mixing SFML 2D and raw OpenGL target.pushGLStates(); } // Render 3D content to FBO render3DContent(); // Restore SFML's GL state after our GL calls if (gl::isGLReady()) { target.popGLStates(); } // Blit FBO to screen (using SFML's drawing, so after state restore) blitToScreen(offset, target); } PyObjectsEnum Viewport3D::derived_type() { return PyObjectsEnum::UIVIEWPORT3D; } UIDrawable* Viewport3D::click_at(sf::Vector2f point) { sf::FloatRect bounds = get_bounds(); if (bounds.contains(point)) { return this; } return nullptr; } sf::FloatRect Viewport3D::get_bounds() const { return sf::FloatRect(position.x, position.y, size_.x, size_.y); } void Viewport3D::move(float dx, float dy) { position.x += dx; position.y += dy; } void Viewport3D::resize(float w, float h) { size_.x = w; size_.y = h; camera_.setAspect(size_.x / size_.y); } // ============================================================================= // Size and Resolution // ============================================================================= void Viewport3D::setSize(float width, float height) { size_.x = width; size_.y = height; camera_.setAspect(size_.x / size_.y); } void Viewport3D::setInternalResolution(int width, int height) { if (width != internalWidth_ || height != internalHeight_) { internalWidth_ = width; internalHeight_ = height; cleanupFBO(); // Force recreation on next render } } // ============================================================================= // Fog Settings // ============================================================================= void Viewport3D::setFogColor(const sf::Color& color) { fogColor_ = vec3(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f); } sf::Color Viewport3D::getFogColor() const { return sf::Color( static_cast(fogColor_.x * 255), static_cast(fogColor_.y * 255), static_cast(fogColor_.z * 255) ); } void Viewport3D::setFogRange(float nearDist, float farDist) { fogNear_ = nearDist; fogFar_ = farDist; } // ============================================================================= // FBO Management // ============================================================================= void Viewport3D::initFBO() { if (fbo_ != 0) return; // Already initialized fbo_ = gl::createFramebuffer(internalWidth_, internalHeight_, &colorTexture_, &depthRenderbuffer_); // Create SFML texture wrapper for blitting // Note: We can't directly use the GL texture with SFML, so we'll // read pixels back for now. This is inefficient but works across backends. blitTexture_ = std::make_unique(); blitTexture_->create(internalWidth_, internalHeight_); } void Viewport3D::cleanupFBO() { blitTexture_.reset(); if (fbo_ != 0) { gl::deleteFramebuffer(fbo_, colorTexture_, depthRenderbuffer_); fbo_ = 0; colorTexture_ = 0; depthRenderbuffer_ = 0; } } // ============================================================================= // Shader and Geometry Initialization // ============================================================================= void Viewport3D::initShader() { shader_ = std::make_unique(); if (!shader_->loadPS1Shaders()) { shader_.reset(); // Shader loading failed } } void Viewport3D::initTestGeometry() { #ifdef MCRF_HAS_GL // Create a colored cube (no texture for now) // Each vertex: position (3) + texcoord (2) + normal (3) + color (4) = 12 floats // Cube has 6 faces * 2 triangles * 3 vertices = 36 vertices float cubeVertices[] = { // Front face (red) - normal (0, 0, 1) -1, -1, 1, 0, 0, 0, 0, 1, 1, 0.2f, 0.2f, 1, 1, -1, 1, 1, 0, 0, 0, 1, 1, 0.2f, 0.2f, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0.2f, 0.2f, 1, -1, -1, 1, 0, 0, 0, 0, 1, 1, 0.2f, 0.2f, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0.2f, 0.2f, 1, -1, 1, 1, 0, 1, 0, 0, 1, 1, 0.2f, 0.2f, 1, // Back face (cyan) - normal (0, 0, -1) 1, -1, -1, 0, 0, 0, 0,-1, 0.2f, 1, 1, 1, -1, -1, -1, 1, 0, 0, 0,-1, 0.2f, 1, 1, 1, -1, 1, -1, 1, 1, 0, 0,-1, 0.2f, 1, 1, 1, 1, -1, -1, 0, 0, 0, 0,-1, 0.2f, 1, 1, 1, -1, 1, -1, 1, 1, 0, 0,-1, 0.2f, 1, 1, 1, 1, 1, -1, 0, 1, 0, 0,-1, 0.2f, 1, 1, 1, // Top face (green) - normal (0, 1, 0) -1, 1, 1, 0, 0, 0, 1, 0, 0.2f, 1, 0.2f, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0.2f, 1, 0.2f, 1, 1, 1, -1, 1, 1, 0, 1, 0, 0.2f, 1, 0.2f, 1, -1, 1, 1, 0, 0, 0, 1, 0, 0.2f, 1, 0.2f, 1, 1, 1, -1, 1, 1, 0, 1, 0, 0.2f, 1, 0.2f, 1, -1, 1, -1, 0, 1, 0, 1, 0, 0.2f, 1, 0.2f, 1, // Bottom face (magenta) - normal (0, -1, 0) -1, -1, -1, 0, 0, 0,-1, 0, 1, 0.2f, 1, 1, 1, -1, -1, 1, 0, 0,-1, 0, 1, 0.2f, 1, 1, 1, -1, 1, 1, 1, 0,-1, 0, 1, 0.2f, 1, 1, -1, -1, -1, 0, 0, 0,-1, 0, 1, 0.2f, 1, 1, 1, -1, 1, 1, 1, 0,-1, 0, 1, 0.2f, 1, 1, -1, -1, 1, 0, 1, 0,-1, 0, 1, 0.2f, 1, 1, // Right face (blue) - normal (1, 0, 0) 1, -1, 1, 0, 0, 1, 0, 0, 0.2f, 0.2f, 1, 1, 1, -1, -1, 1, 0, 1, 0, 0, 0.2f, 0.2f, 1, 1, 1, 1, -1, 1, 1, 1, 0, 0, 0.2f, 0.2f, 1, 1, 1, -1, 1, 0, 0, 1, 0, 0, 0.2f, 0.2f, 1, 1, 1, 1, -1, 1, 1, 1, 0, 0, 0.2f, 0.2f, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0.2f, 0.2f, 1, 1, // Left face (yellow) - normal (-1, 0, 0) -1, -1, -1, 0, 0, -1, 0, 0, 1, 1, 0.2f, 1, -1, -1, 1, 1, 0, -1, 0, 0, 1, 1, 0.2f, 1, -1, 1, 1, 1, 1, -1, 0, 0, 1, 1, 0.2f, 1, -1, -1, -1, 0, 0, -1, 0, 0, 1, 1, 0.2f, 1, -1, 1, 1, 1, 1, -1, 0, 0, 1, 1, 0.2f, 1, -1, 1, -1, 0, 1, -1, 0, 0, 1, 1, 0.2f, 1, }; testVertexCount_ = 36; glGenBuffers(1, &testVBO_); glBindBuffer(GL_ARRAY_BUFFER, testVBO_); glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), cubeVertices, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); #endif } void Viewport3D::cleanupTestGeometry() { #ifdef MCRF_HAS_GL if (testVBO_ != 0) { glDeleteBuffers(1, &testVBO_); testVBO_ = 0; } #endif } // ============================================================================= // 3D Rendering // ============================================================================= void Viewport3D::render3DContent() { // GL not available in current backend - skip 3D rendering if (!gl::isGLReady() || fbo_ == 0) { return; } #ifdef MCRF_HAS_GL // Save GL state gl::pushState(); // Bind FBO gl::bindFramebuffer(fbo_); // Set viewport to internal resolution glViewport(0, 0, internalWidth_, internalHeight_); // Clear with background color glClearColor(bgColor_.r / 255.0f, bgColor_.g / 255.0f, bgColor_.b / 255.0f, bgColor_.a / 255.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Set up 3D state gl::setup3DState(); // Update test rotation for spinning geometry testRotation_ += 0.02f; // Render test cube if shader and geometry are ready if (shader_ && shader_->isValid() && testVBO_ != 0) { shader_->bind(); // Set up matrices mat4 model = mat4::rotateY(testRotation_) * mat4::rotateX(testRotation_ * 0.7f); mat4 view = camera_.getViewMatrix(); mat4 projection = camera_.getProjectionMatrix(); shader_->setUniform("u_model", model); shader_->setUniform("u_view", view); shader_->setUniform("u_projection", projection); // 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_); // Texture (none for test geometry) shader_->setUniform("u_has_texture", false); // Bind VBO and set up attributes glBindBuffer(GL_ARRAY_BUFFER, testVBO_); // Vertex format: pos(3) + texcoord(2) + normal(3) + color(4) = 12 floats int stride = 12 * sizeof(float); glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION); glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, stride, (void*)0); glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD); glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float))); glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL); glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE, stride, (void*)(5 * sizeof(float))); glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR); glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE, stride, (void*)(8 * sizeof(float))); // Draw cube glDrawArrays(GL_TRIANGLES, 0, testVertexCount_); // Cleanup glDisableVertexAttribArray(Shader3D::ATTRIB_POSITION); glDisableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD); glDisableVertexAttribArray(Shader3D::ATTRIB_NORMAL); glDisableVertexAttribArray(Shader3D::ATTRIB_COLOR); glBindBuffer(GL_ARRAY_BUFFER, 0); shader_->unbind(); } // Restore 2D state gl::restore2DState(); // Unbind FBO gl::bindDefaultFramebuffer(); // Restore GL state gl::popState(); #endif } void Viewport3D::blitToScreen(sf::Vector2f offset, sf::RenderTarget& target) { sf::Vector2f screenPos = position + offset; // If GL is not ready, just draw a placeholder rectangle if (!gl::isGLReady() || fbo_ == 0 || !blitTexture_) { sf::RectangleShape placeholder(size_); placeholder.setPosition(screenPos); placeholder.setFillColor(bgColor_); placeholder.setOutlineColor(sf::Color::White); placeholder.setOutlineThickness(1.0f); target.draw(placeholder); return; } #ifdef MCRF_HAS_GL // Read pixels from FBO and update SFML texture // Note: This is inefficient but portable. Future optimization: use GL texture directly. std::vector pixels(internalWidth_ * internalHeight_ * 4); gl::bindFramebuffer(fbo_); glReadPixels(0, 0, internalWidth_, internalHeight_, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); gl::bindDefaultFramebuffer(); // Flip vertically (OpenGL vs SFML coordinate system) std::vector flipped(pixels.size()); for (int y = 0; y < internalHeight_; ++y) { int srcRow = (internalHeight_ - 1 - y) * internalWidth_ * 4; int dstRow = y * internalWidth_ * 4; memcpy(&flipped[dstRow], &pixels[srcRow], internalWidth_ * 4); } blitTexture_->update(flipped.data()); // Draw to screen with nearest-neighbor scaling (PS1 style) sf::Sprite sprite(*blitTexture_); sprite.setPosition(screenPos); sprite.setScale(size_.x / internalWidth_, size_.y / internalHeight_); // Set nearest-neighbor filtering for that crispy PS1 look // Note: SFML 2.x doesn't have per-draw texture filtering, so this // affects the texture globally. In practice this is fine for our use. const_cast(sprite.getTexture())->setSmooth(false); target.draw(sprite); #else // Non-SDL2 fallback (SFML desktop without GL) sf::RectangleShape placeholder(size_); placeholder.setPosition(screenPos); placeholder.setFillColor(bgColor_); target.draw(placeholder); #endif } // ============================================================================= // Animation Property System // ============================================================================= bool Viewport3D::setProperty(const std::string& name, float value) { if (name == "x") { position.x = value; return true; } if (name == "y") { position.y = value; return true; } if (name == "w") { size_.x = value; camera_.setAspect(size_.x / size_.y); return true; } if (name == "h") { size_.y = value; camera_.setAspect(size_.x / size_.y); return true; } if (name == "fov") { camera_.setFOV(value); return true; } if (name == "fog_near") { fogNear_ = value; return true; } if (name == "fog_far") { fogFar_ = value; return true; } if (name == "opacity") { opacity = value; return true; } return false; } bool Viewport3D::setProperty(const std::string& name, const sf::Color& value) { if (name == "bg_color") { bgColor_ = value; return true; } if (name == "fog_color") { setFogColor(value); return true; } return false; } bool Viewport3D::setProperty(const std::string& name, const sf::Vector2f& value) { if (name == "pos") { position = value; return true; } if (name == "size") { size_ = value; camera_.setAspect(size_.x / size_.y); return true; } return false; } bool Viewport3D::getProperty(const std::string& name, float& value) const { if (name == "x") { value = position.x; return true; } if (name == "y") { value = position.y; return true; } if (name == "w") { value = size_.x; return true; } if (name == "h") { value = size_.y; return true; } if (name == "fov") { value = camera_.getFOV(); return true; } if (name == "fog_near") { value = fogNear_; return true; } if (name == "fog_far") { value = fogFar_; return true; } if (name == "opacity") { value = opacity; return true; } return false; } bool Viewport3D::getProperty(const std::string& name, sf::Color& value) const { if (name == "bg_color") { value = bgColor_; return true; } if (name == "fog_color") { value = getFogColor(); return true; } return false; } bool Viewport3D::getProperty(const std::string& name, sf::Vector2f& value) const { if (name == "pos") { value = position; return true; } if (name == "size") { value = size_; return true; } return false; } bool Viewport3D::hasProperty(const std::string& name) const { static const std::set props = { "x", "y", "w", "h", "pos", "size", "fov", "fog_near", "fog_far", "opacity", "bg_color", "fog_color" }; return props.count(name) > 0; } // ============================================================================= // Python API // ============================================================================= // Use PyObjectType for UIBase.h macros #define PyObjectType PyViewport3DObject // Helper to get vec3 from Python tuple static bool PyTuple_GetVec3(PyObject* tuple, mcrf::vec3& out) { if (!tuple || tuple == Py_None) return false; if (!PyTuple_Check(tuple) && !PyList_Check(tuple)) return false; Py_ssize_t size = PySequence_Size(tuple); if (size != 3) return false; PyObject* x = PySequence_GetItem(tuple, 0); PyObject* y = PySequence_GetItem(tuple, 1); PyObject* z = PySequence_GetItem(tuple, 2); bool ok = true; if (PyNumber_Check(x) && PyNumber_Check(y) && PyNumber_Check(z)) { out.x = static_cast(PyFloat_AsDouble(PyNumber_Float(x))); out.y = static_cast(PyFloat_AsDouble(PyNumber_Float(y))); out.z = static_cast(PyFloat_AsDouble(PyNumber_Float(z))); } else { ok = false; } Py_DECREF(x); Py_DECREF(y); Py_DECREF(z); return ok; } // Helper to create Python tuple from vec3 static PyObject* PyTuple_FromVec3(const mcrf::vec3& v) { return Py_BuildValue("(fff)", v.x, v.y, v.z); } // Position getters/setters static PyObject* Viewport3D_get_pos(PyViewport3DObject* self, void* closure) { return PyVector(self->data->position).pyObject(); } static int Viewport3D_set_pos(PyViewport3DObject* self, PyObject* value, void* closure) { PyVectorObject* vec = PyVector::from_arg(value); if (!vec) { PyErr_SetString(PyExc_TypeError, "pos must be a Vector or (x, y) tuple"); return -1; } self->data->position = vec->data; return 0; } static PyObject* Viewport3D_get_x(PyViewport3DObject* self, void* closure) { return PyFloat_FromDouble(self->data->position.x); } static int Viewport3D_set_x(PyViewport3DObject* self, PyObject* value, void* closure) { if (!PyNumber_Check(value)) { PyErr_SetString(PyExc_TypeError, "x must be a number"); return -1; } self->data->position.x = static_cast(PyFloat_AsDouble(value)); return 0; } static PyObject* Viewport3D_get_y(PyViewport3DObject* self, void* closure) { return PyFloat_FromDouble(self->data->position.y); } static int Viewport3D_set_y(PyViewport3DObject* self, PyObject* value, void* closure) { if (!PyNumber_Check(value)) { PyErr_SetString(PyExc_TypeError, "y must be a number"); return -1; } self->data->position.y = static_cast(PyFloat_AsDouble(value)); return 0; } // Size getters/setters static PyObject* Viewport3D_get_w(PyViewport3DObject* self, void* closure) { return PyFloat_FromDouble(self->data->getWidth()); } static int Viewport3D_set_w(PyViewport3DObject* self, PyObject* value, void* closure) { if (!PyNumber_Check(value)) { PyErr_SetString(PyExc_TypeError, "w must be a number"); return -1; } self->data->setSize(static_cast(PyFloat_AsDouble(value)), self->data->getHeight()); return 0; } static PyObject* Viewport3D_get_h(PyViewport3DObject* self, void* closure) { return PyFloat_FromDouble(self->data->getHeight()); } static int Viewport3D_set_h(PyViewport3DObject* self, PyObject* value, void* closure) { if (!PyNumber_Check(value)) { PyErr_SetString(PyExc_TypeError, "h must be a number"); return -1; } self->data->setSize(self->data->getWidth(), static_cast(PyFloat_AsDouble(value))); return 0; } // Render resolution static PyObject* Viewport3D_get_render_resolution(PyViewport3DObject* self, void* closure) { return Py_BuildValue("(ii)", self->data->getInternalWidth(), self->data->getInternalHeight()); } static int Viewport3D_set_render_resolution(PyViewport3DObject* self, PyObject* value, void* closure) { int w, h; if (!PyArg_ParseTuple(value, "ii", &w, &h)) { PyErr_SetString(PyExc_TypeError, "render_resolution must be (width, height)"); return -1; } self->data->setInternalResolution(w, h); return 0; } // Camera position static PyObject* Viewport3D_get_camera_pos(PyViewport3DObject* self, void* closure) { return PyTuple_FromVec3(self->data->getCameraPosition()); } static int Viewport3D_set_camera_pos(PyViewport3DObject* self, PyObject* value, void* closure) { mcrf::vec3 pos; if (!PyTuple_GetVec3(value, pos)) { PyErr_SetString(PyExc_TypeError, "camera_pos must be (x, y, z)"); return -1; } self->data->setCameraPosition(pos); return 0; } // Camera target static PyObject* Viewport3D_get_camera_target(PyViewport3DObject* self, void* closure) { return PyTuple_FromVec3(self->data->getCameraTarget()); } static int Viewport3D_set_camera_target(PyViewport3DObject* self, PyObject* value, void* closure) { mcrf::vec3 target; if (!PyTuple_GetVec3(value, target)) { PyErr_SetString(PyExc_TypeError, "camera_target must be (x, y, z)"); return -1; } self->data->setCameraTarget(target); return 0; } // FOV static PyObject* Viewport3D_get_fov(PyViewport3DObject* self, void* closure) { return PyFloat_FromDouble(self->data->getCamera().getFOV()); } static int Viewport3D_set_fov(PyViewport3DObject* self, PyObject* value, void* closure) { if (!PyNumber_Check(value)) { PyErr_SetString(PyExc_TypeError, "fov must be a number"); return -1; } self->data->getCamera().setFOV(static_cast(PyFloat_AsDouble(value))); return 0; } // Background color static PyObject* Viewport3D_get_bg_color(PyViewport3DObject* self, void* closure) { return PyColor(self->data->getBackgroundColor()).pyObject(); } static int Viewport3D_set_bg_color(PyViewport3DObject* self, PyObject* value, void* closure) { sf::Color color = PyColor::fromPy(value); if (PyErr_Occurred()) { return -1; } self->data->setBackgroundColor(color); return 0; } // PS1 effect toggles static PyObject* Viewport3D_get_enable_vertex_snap(PyViewport3DObject* self, void* closure) { return PyBool_FromLong(self->data->isVertexSnapEnabled()); } static int Viewport3D_set_enable_vertex_snap(PyViewport3DObject* self, PyObject* value, void* closure) { self->data->setVertexSnapEnabled(PyObject_IsTrue(value)); return 0; } static PyObject* Viewport3D_get_enable_affine(PyViewport3DObject* self, void* closure) { return PyBool_FromLong(self->data->isAffineMappingEnabled()); } static int Viewport3D_set_enable_affine(PyViewport3DObject* self, PyObject* value, void* closure) { self->data->setAffineMappingEnabled(PyObject_IsTrue(value)); return 0; } static PyObject* Viewport3D_get_enable_dither(PyViewport3DObject* self, void* closure) { return PyBool_FromLong(self->data->isDitheringEnabled()); } static int Viewport3D_set_enable_dither(PyViewport3DObject* self, PyObject* value, void* closure) { self->data->setDitheringEnabled(PyObject_IsTrue(value)); return 0; } static PyObject* Viewport3D_get_enable_fog(PyViewport3DObject* self, void* closure) { return PyBool_FromLong(self->data->isFogEnabled()); } static int Viewport3D_set_enable_fog(PyViewport3DObject* self, PyObject* value, void* closure) { self->data->setFogEnabled(PyObject_IsTrue(value)); return 0; } // Fog color static PyObject* Viewport3D_get_fog_color(PyViewport3DObject* self, void* closure) { return PyColor(self->data->getFogColor()).pyObject(); } static int Viewport3D_set_fog_color(PyViewport3DObject* self, PyObject* value, void* closure) { sf::Color color = PyColor::fromPy(value); if (PyErr_Occurred()) { return -1; } self->data->setFogColor(color); return 0; } // Fog range static PyObject* Viewport3D_get_fog_near(PyViewport3DObject* self, void* closure) { return PyFloat_FromDouble(self->data->getFogNear()); } static int Viewport3D_set_fog_near(PyViewport3DObject* self, PyObject* value, void* closure) { if (!PyNumber_Check(value)) { PyErr_SetString(PyExc_TypeError, "fog_near must be a number"); return -1; } self->data->setFogRange(static_cast(PyFloat_AsDouble(value)), self->data->getFogFar()); return 0; } static PyObject* Viewport3D_get_fog_far(PyViewport3DObject* self, void* closure) { return PyFloat_FromDouble(self->data->getFogFar()); } static int Viewport3D_set_fog_far(PyViewport3DObject* self, PyObject* value, void* closure) { if (!PyNumber_Check(value)) { PyErr_SetString(PyExc_TypeError, "fog_far must be a number"); return -1; } self->data->setFogRange(self->data->getFogNear(), static_cast(PyFloat_AsDouble(value))); return 0; } PyGetSetDef Viewport3D::getsetters[] = { // Position and size {"x", (getter)Viewport3D_get_x, (setter)Viewport3D_set_x, MCRF_PROPERTY(x, "X position in pixels."), NULL}, {"y", (getter)Viewport3D_get_y, (setter)Viewport3D_set_y, MCRF_PROPERTY(y, "Y position in pixels."), NULL}, {"pos", (getter)Viewport3D_get_pos, (setter)Viewport3D_set_pos, MCRF_PROPERTY(pos, "Position as Vector (x, y)."), NULL}, {"w", (getter)Viewport3D_get_w, (setter)Viewport3D_set_w, MCRF_PROPERTY(w, "Display width in pixels."), NULL}, {"h", (getter)Viewport3D_get_h, (setter)Viewport3D_set_h, MCRF_PROPERTY(h, "Display height in pixels."), NULL}, // Render resolution {"render_resolution", (getter)Viewport3D_get_render_resolution, (setter)Viewport3D_set_render_resolution, MCRF_PROPERTY(render_resolution, "Internal render resolution (width, height). Lower values for PS1 effect."), NULL}, // Camera {"camera_pos", (getter)Viewport3D_get_camera_pos, (setter)Viewport3D_set_camera_pos, MCRF_PROPERTY(camera_pos, "Camera position as (x, y, z) tuple."), NULL}, {"camera_target", (getter)Viewport3D_get_camera_target, (setter)Viewport3D_set_camera_target, MCRF_PROPERTY(camera_target, "Camera look-at target as (x, y, z) tuple."), NULL}, {"fov", (getter)Viewport3D_get_fov, (setter)Viewport3D_set_fov, MCRF_PROPERTY(fov, "Camera field of view in degrees."), NULL}, // Background {"bg_color", (getter)Viewport3D_get_bg_color, (setter)Viewport3D_set_bg_color, MCRF_PROPERTY(bg_color, "Background clear color."), NULL}, // PS1 effects {"enable_vertex_snap", (getter)Viewport3D_get_enable_vertex_snap, (setter)Viewport3D_set_enable_vertex_snap, MCRF_PROPERTY(enable_vertex_snap, "Enable PS1-style vertex snapping (jittery vertices)."), NULL}, {"enable_affine", (getter)Viewport3D_get_enable_affine, (setter)Viewport3D_set_enable_affine, MCRF_PROPERTY(enable_affine, "Enable PS1-style affine texture mapping (warped textures)."), NULL}, {"enable_dither", (getter)Viewport3D_get_enable_dither, (setter)Viewport3D_set_enable_dither, MCRF_PROPERTY(enable_dither, "Enable PS1-style color dithering."), NULL}, {"enable_fog", (getter)Viewport3D_get_enable_fog, (setter)Viewport3D_set_enable_fog, MCRF_PROPERTY(enable_fog, "Enable distance fog."), NULL}, // Fog settings {"fog_color", (getter)Viewport3D_get_fog_color, (setter)Viewport3D_set_fog_color, MCRF_PROPERTY(fog_color, "Fog color."), NULL}, {"fog_near", (getter)Viewport3D_get_fog_near, (setter)Viewport3D_set_fog_near, MCRF_PROPERTY(fog_near, "Fog start distance."), NULL}, {"fog_far", (getter)Viewport3D_get_fog_far, (setter)Viewport3D_set_fog_far, MCRF_PROPERTY(fog_far, "Fog end distance."), NULL}, // Common UIDrawable properties UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIVIEWPORT3D), {NULL} // Sentinel }; PyObject* Viewport3D::repr(PyViewport3DObject* self) { char buffer[256]; snprintf(buffer, sizeof(buffer), "", self->data->position.x, self->data->position.y, self->data->getWidth(), self->data->getHeight(), self->data->getInternalWidth(), self->data->getInternalHeight()); return PyUnicode_FromString(buffer); } int Viewport3D::init(PyViewport3DObject* self, PyObject* args, PyObject* kwds) { static const char* kwlist[] = { "pos", "size", "render_resolution", "fov", "camera_pos", "camera_target", "bg_color", "enable_vertex_snap", "enable_affine", "enable_dither", "enable_fog", "fog_color", "fog_near", "fog_far", "visible", "z_index", "name", NULL }; PyObject* pos_obj = nullptr; PyObject* size_obj = nullptr; PyObject* render_res_obj = nullptr; float fov = 60.0f; PyObject* camera_pos_obj = nullptr; PyObject* camera_target_obj = nullptr; PyObject* bg_color_obj = nullptr; int enable_vertex_snap = 1; int enable_affine = 1; int enable_dither = 1; int enable_fog = 1; PyObject* fog_color_obj = nullptr; float fog_near = 10.0f; float fog_far = 100.0f; int visible = 1; int z_index = 0; const char* name = nullptr; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOfOOOppppOffpis", const_cast(kwlist), &pos_obj, &size_obj, &render_res_obj, &fov, &camera_pos_obj, &camera_target_obj, &bg_color_obj, &enable_vertex_snap, &enable_affine, &enable_dither, &enable_fog, &fog_color_obj, &fog_near, &fog_far, &visible, &z_index, &name)) { return -1; } // Position if (pos_obj && pos_obj != Py_None) { PyVectorObject* vec = PyVector::from_arg(pos_obj); if (!vec) { PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y)"); return -1; } self->data->position = vec->data; } // Size if (size_obj && size_obj != Py_None) { float w, h; if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { w = static_cast(PyFloat_AsDouble(PyTuple_GetItem(size_obj, 0))); h = static_cast(PyFloat_AsDouble(PyTuple_GetItem(size_obj, 1))); self->data->setSize(w, h); } else { PyErr_SetString(PyExc_TypeError, "size must be a tuple (width, height)"); return -1; } } // Render resolution if (render_res_obj && render_res_obj != Py_None) { int rw, rh; if (PyTuple_Check(render_res_obj) && PyTuple_Size(render_res_obj) == 2) { rw = static_cast(PyLong_AsLong(PyTuple_GetItem(render_res_obj, 0))); rh = static_cast(PyLong_AsLong(PyTuple_GetItem(render_res_obj, 1))); self->data->setInternalResolution(rw, rh); } } // FOV self->data->getCamera().setFOV(fov); // Camera position if (camera_pos_obj && camera_pos_obj != Py_None) { mcrf::vec3 cam_pos; if (PyTuple_GetVec3(camera_pos_obj, cam_pos)) { self->data->setCameraPosition(cam_pos); } } // Camera target if (camera_target_obj && camera_target_obj != Py_None) { mcrf::vec3 cam_target; if (PyTuple_GetVec3(camera_target_obj, cam_target)) { self->data->setCameraTarget(cam_target); } } // Background color if (bg_color_obj && bg_color_obj != Py_None) { sf::Color bg = PyColor::fromPy(bg_color_obj); if (!PyErr_Occurred()) { self->data->setBackgroundColor(bg); } } // PS1 effects self->data->setVertexSnapEnabled(enable_vertex_snap); self->data->setAffineMappingEnabled(enable_affine); self->data->setDitheringEnabled(enable_dither); self->data->setFogEnabled(enable_fog); // Fog color if (fog_color_obj && fog_color_obj != Py_None) { sf::Color fc = PyColor::fromPy(fog_color_obj); if (!PyErr_Occurred()) { self->data->setFogColor(fc); } } // Fog range self->data->setFogRange(fog_near, fog_far); // Common properties self->data->visible = visible; self->data->z_index = z_index; if (name) { self->data->name = name; } return 0; } #undef PyObjectType } // namespace mcrf // Methods array (outside namespace) PyMethodDef Viewport3D_methods[] = { // Add UIDRAWABLE_METHODS when ready {NULL} // Sentinel };