diff --git a/plugin/src/Caelestia/Blobs/CMakeLists.txt b/plugin/src/Caelestia/Blobs/CMakeLists.txt new file mode 100644 index 00000000..f44be008 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/CMakeLists.txt @@ -0,0 +1,19 @@ +qml_module(caelestia-blobs + URI Caelestia.Blobs + SOURCES + blobgroup.cpp + blobshape.cpp + blobrect.cpp + blobinvertedrect.cpp + blobmaterial.cpp + LIBRARIES + Qt::Quick +) + +qt_add_shaders(caelestia-blobs "blob_shaders" + BATCHABLE OPTIMIZED NOHLSL NOMSL + PREFIX "/" + FILES + shaders/blob.frag + shaders/blob.vert +) diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp new file mode 100644 index 00000000..f06c4d51 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp @@ -0,0 +1,75 @@ +#include "blobgroup.hpp" +#include "blobinvertedrect.hpp" +#include "blobshape.hpp" + +BlobGroup::BlobGroup(QObject* parent) + : QObject(parent) {} + +BlobGroup::~BlobGroup() { + for (auto* shape : std::as_const(m_shapes)) + shape->m_group = nullptr; + if (m_invertedRect) + static_cast(m_invertedRect)->m_group = nullptr; +} + +void BlobGroup::setSmoothing(qreal s) { + if (qFuzzyCompare(m_smoothing, s)) + return; + m_smoothing = s; + emit smoothingChanged(); + markDirty(); +} + +void BlobGroup::setColor(const QColor& c) { + if (m_color == c) + return; + m_color = c; + emit colorChanged(); + markDirty(); +} + +void BlobGroup::addShape(BlobShape* shape) { + if (!shape || m_shapes.contains(shape)) + return; + m_shapes.append(shape); + markDirty(); +} + +void BlobGroup::removeShape(BlobShape* shape) { + m_shapes.removeOne(shape); + markDirty(); +} + +void BlobGroup::setInvertedRect(BlobInvertedRect* rect) { + if (m_invertedRect == rect) + return; + m_invertedRect = rect; + markDirty(); +} + +void BlobGroup::clearInvertedRect(BlobInvertedRect* rect) { + if (m_invertedRect != rect) + return; + m_invertedRect = nullptr; + markDirty(); +} + +void BlobGroup::markDirty() { + m_physicsUpdated = false; + for (auto* shape : std::as_const(m_shapes)) { + shape->polish(); + shape->update(); + } + if (m_invertedRect) { + static_cast(m_invertedRect)->polish(); + static_cast(m_invertedRect)->update(); + } +} + +void BlobGroup::ensurePhysicsUpdated() { + if (m_physicsUpdated) + return; + m_physicsUpdated = true; + for (auto* shape : std::as_const(m_shapes)) + shape->updatePhysics(); +} diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp new file mode 100644 index 00000000..4c9ed691 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +class BlobShape; +class BlobInvertedRect; + +class BlobGroup : public QObject { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY + smoothingChanged) + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + +public: + explicit BlobGroup(QObject* parent = nullptr); + ~BlobGroup() override; + + qreal smoothing() const { return m_smoothing; } + + void setSmoothing(qreal s); + + QColor color() const { return m_color; } + + void setColor(const QColor& c); + + void addShape(BlobShape* shape); + void removeShape(BlobShape* shape); + + void setInvertedRect(BlobInvertedRect* rect); + void clearInvertedRect(BlobInvertedRect* rect); + + const QList& shapes() const { return m_shapes; } + + BlobInvertedRect* invertedRect() const { return m_invertedRect; } + + void markDirty(); + void ensurePhysicsUpdated(); + +signals: + void smoothingChanged(); + void colorChanged(); + +private: + qreal m_smoothing = 32.0; + QColor m_color{ 0x44, 0x88, 0xff }; + QList m_shapes; + BlobInvertedRect* m_invertedRect = nullptr; + bool m_physicsUpdated = false; +}; diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp new file mode 100644 index 00000000..68024738 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp @@ -0,0 +1,56 @@ +#include "blobinvertedrect.hpp" +#include "blobgroup.hpp" + +BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) + : BlobShape(parent) {} + +BlobInvertedRect::~BlobInvertedRect() { + if (m_group) + m_group->clearInvertedRect(this); +} + +void BlobInvertedRect::setBorderLeft(qreal v) { + if (qFuzzyCompare(m_borderLeft, v)) + return; + m_borderLeft = v; + emit borderLeftChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderRight(qreal v) { + if (qFuzzyCompare(m_borderRight, v)) + return; + m_borderRight = v; + emit borderRightChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderTop(qreal v) { + if (qFuzzyCompare(m_borderTop, v)) + return; + m_borderTop = v; + emit borderTopChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderBottom(qreal v) { + if (qFuzzyCompare(m_borderBottom, v)) + return; + m_borderBottom = v; + emit borderBottomChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::registerWithGroup() { + if (m_group) + m_group->setInvertedRect(this); +} + +void BlobInvertedRect::unregisterFromGroup() { + if (m_group) + m_group->clearInvertedRect(this); +} diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp new file mode 100644 index 00000000..958e2a8d --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "blobshape.hpp" + +#include + +class BlobInvertedRect : public BlobShape { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY + borderLeftChanged) + Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY + borderRightChanged) + Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY + borderTopChanged) + Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY + borderBottomChanged) + +public: + explicit BlobInvertedRect(QQuickItem* parent = nullptr); + ~BlobInvertedRect() override; + + qreal borderLeft() const { return m_borderLeft; } + + void setBorderLeft(qreal v); + + qreal borderRight() const { return m_borderRight; } + + void setBorderRight(qreal v); + + qreal borderTop() const { return m_borderTop; } + + void setBorderTop(qreal v); + + qreal borderBottom() const { return m_borderBottom; } + + void setBorderBottom(qreal v); + +signals: + void borderLeftChanged(); + void borderRightChanged(); + void borderTopChanged(); + void borderBottomChanged(); + +protected: + bool isInvertedRect() const override { return true; } + + void registerWithGroup() override; + void unregisterFromGroup() override; + +private: + qreal m_borderLeft = 0; + qreal m_borderRight = 0; + qreal m_borderTop = 0; + qreal m_borderBottom = 0; +}; diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.cpp b/plugin/src/Caelestia/Blobs/blobmaterial.cpp new file mode 100644 index 00000000..f2ead2ed --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobmaterial.cpp @@ -0,0 +1,98 @@ +#include "blobmaterial.hpp" + +#include + +QSGMaterialType* BlobMaterial::type() const { + static QSGMaterialType s_type; + return &s_type; +} + +QSGMaterialShader* BlobMaterial::createShader( + QSGRendererInterface::RenderMode) const { + return new BlobMaterialShader; +} + +int BlobMaterial::compare(const QSGMaterial* other) const { + if (this < other) + return -1; + if (this > other) + return 1; + return 0; +} + +BlobMaterialShader::BlobMaterialShader() { + setShaderFileName(VertexStage, QStringLiteral(":/shaders/blob.vert.qsb")); + setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb")); +} + +bool BlobMaterialShader::updateUniformData( + RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) { + Q_UNUSED(oldMaterial); + auto* mat = static_cast(newMaterial); + QByteArray* buf = state.uniformData(); + Q_ASSERT(buf->size() >= 1440); + + if (state.isMatrixDirty()) { + const QMatrix4x4 m = state.combinedMatrix(); + memcpy(buf->data(), m.constData(), 64); + } + if (state.isOpacityDirty()) { + const float opacity = state.opacity(); + memcpy(buf->data() + 64, &opacity, 4); + } + + // Padded rect (offset 68) + memcpy(buf->data() + 68, &mat->m_paddedX, 4); + memcpy(buf->data() + 72, &mat->m_paddedY, 4); + memcpy(buf->data() + 76, &mat->m_paddedW, 4); + memcpy(buf->data() + 80, &mat->m_paddedH, 4); + + // Smooth factor (offset 84) + memcpy(buf->data() + 84, &mat->m_smoothFactor, 4); + + // Rect count (offset 88) + memcpy(buf->data() + 88, &mat->m_rectCount, 4); + + // My index (offset 92) + memcpy(buf->data() + 92, &mat->m_myIndex, 4); + + // Color as vec4 (offset 96, 16 bytes) + const float color[4] = { + static_cast(mat->m_color.redF()), + static_cast(mat->m_color.greenF()), + static_cast(mat->m_color.blueF()), + static_cast(mat->m_color.alphaF()), + }; + memcpy(buf->data() + 96, color, 16); + + // Has inverted (offset 112) + memcpy(buf->data() + 112, &mat->m_hasInverted, 4); + + // Inverted radius (offset 116) + memcpy(buf->data() + 116, &mat->m_invertedRadius, 4); + + // Padding at 120-127 (skip) + + // Inverted outer (offset 128, 16 bytes) + memcpy(buf->data() + 128, mat->m_invertedOuter, 16); + + // Inverted inner (offset 144, 16 bytes) + memcpy(buf->data() + 144, mat->m_invertedInner, 16); + + // Rect data (offset 160, each rect = 5 vec4s = 80 bytes) + const int count = qMin(mat->m_rectCount, 16); + for (int i = 0; i < count; ++i) { + const auto& r = mat->m_rects[i]; + const int base = 160 + i * 80; + const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; + const float d1[4] = { r.radius, r.offsetX, r.offsetY, r.minEig }; + const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; + memcpy(buf->data() + base, d0, 16); + memcpy(buf->data() + base + 16, d1, 16); + memcpy(buf->data() + base + 32, r.invDeform, 16); + memcpy(buf->data() + base + 48, d3, 16); + memcpy(buf->data() + base + 64, r.cornerFill, 16); + } + + return true; +} diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.hpp b/plugin/src/Caelestia/Blobs/blobmaterial.hpp new file mode 100644 index 00000000..ef80fea1 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobmaterial.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +struct BlobRectData { + float cx = 0, cy = 0, hw = 0, hh = 0; + float radius = 0; + float offsetX = 0, offsetY = 0; + float minEig = 1.0f; + // Inverse of 2x2 deformation matrix, column-major for GLSL + float invDeform[4] = { 1, 0, 0, 1 }; + // Screen-space AABB half-extents of the deformed rect + float screenHalfX = 0, screenHalfY = 0; + // Pre-computed corner fill factors (tr, br, bl, tl) + float cornerFill[4] = { 1, 1, 1, 1 }; +}; + +class BlobMaterial : public QSGMaterial { +public: + QSGMaterialType* type() const override; + QSGMaterialShader* createShader( + QSGRendererInterface::RenderMode) const override; + int compare(const QSGMaterial* other) const override; + + float m_paddedX = 0; + float m_paddedY = 0; + float m_paddedW = 0; + float m_paddedH = 0; + float m_smoothFactor = 32.0f; + int m_rectCount = 0; + int m_myIndex = -2; + QColor m_color{ 0x44, 0x88, 0xff }; + int m_hasInverted = 0; + float m_invertedRadius = 0; + float m_invertedOuter[4] = {}; + float m_invertedInner[4] = {}; + BlobRectData m_rects[16] = {}; +}; + +class BlobMaterialShader : public QSGMaterialShader { +public: + BlobMaterialShader(); + bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, + QSGMaterial* oldMaterial) override; +}; diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp new file mode 100644 index 00000000..e03baa22 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -0,0 +1,132 @@ +#include "blobrect.hpp" +#include "blobgroup.hpp" + +#include +#include + +BlobRect::BlobRect(QQuickItem* parent) + : BlobShape(parent) {} + +BlobRect::~BlobRect() { + if (m_group) + m_group->removeShape(this); +} + +void BlobRect::updatePolish() { + BlobShape::updatePolish(); + + if (m_physicsActive) { + QMetaObject::invokeMethod( + this, + [this]() { + if (m_physicsActive && m_group) + m_group->markDirty(); + }, + Qt::QueuedConnection); + } +} + +void BlobRect::updatePhysics() { + const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); + + if (!m_hasPrevPos) { + m_prevScenePos = scenePos; + m_elapsed.start(); + m_hasPrevPos = true; + return; + } + + const float dt = static_cast(m_elapsed.restart()) / 1000.0f; + if (dt > 0.1f || dt < 0.001f) { + m_prevScenePos = scenePos; + // Still check atRest on skipped frames to avoid getting stuck + if (m_physicsActive) + checkAtRest(0.0f); + return; + } + + const float velX = + static_cast(scenePos.x() - m_prevScenePos.x()) / dt; + const float velY = + static_cast(scenePos.y() - m_prevScenePos.y()) / dt; + m_prevScenePos = scenePos; + + const float speed = std::sqrt(velX * velX + velY * velY); + + if (!m_physicsActive) { + if (speed < 5.0f) + return; + m_physicsActive = true; + } + + // Compute target deformation matrix from velocity + // R(θ) * diag(stretch, compress) * R(θ)^T + const float kStretchFactor = static_cast(m_deformScale); + constexpr float kMaxStretch = 0.35f; + + float target00 = 1.0f; + float target01 = 0.0f; + float target11 = 1.0f; + + if (speed > 5.0f) { + const float targetStretch = + 1.0f + std::min(speed * kStretchFactor, kMaxStretch); + const float targetCompress = 1.0f / targetStretch; + + const float cosA = velX / speed; + const float sinA = velY / speed; + const float cos2 = cosA * cosA; + const float sin2 = sinA * sinA; + const float cs = cosA * sinA; + + target00 = targetStretch * cos2 + targetCompress * sin2; + target01 = (targetStretch - targetCompress) * cs; + target11 = targetStretch * sin2 + targetCompress * cos2; + } + + // Underdamped spring on each matrix component + const float kStiffness = static_cast(m_stiffness); + const float kDamping = static_cast(m_damping); + + const float accel00 = + -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00; + m_dmVel00 += accel00 * dt; + m_dm00 += m_dmVel00 * dt; + + const float accel01 = + -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01; + m_dmVel01 += accel01 * dt; + m_dm01 += m_dmVel01 * dt; + + const float accel11 = + -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11; + m_dmVel11 += accel11 * dt; + m_dm11 += m_dmVel11 * dt; + + m_deformMatrix = QMatrix4x4( + m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + updateCenteredDeformMatrix(); + + checkAtRest(speed); +} + +void BlobRect::checkAtRest(float speed) { + constexpr float kEpsilon = 0.002f; + const bool atRest = + std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && + std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && + std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && + speed < 5.0f; + + if (atRest) { + m_dm00 = 1.0f; + m_dm01 = 0.0f; + m_dm11 = 1.0f; + m_dmVel00 = 0.0f; + m_dmVel01 = 0.0f; + m_dmVel11 = 0.0f; + m_deformMatrix = QMatrix4x4(); // identity + updateCenteredDeformMatrix(); + m_physicsActive = false; + } +} diff --git a/plugin/src/Caelestia/Blobs/blobrect.hpp b/plugin/src/Caelestia/Blobs/blobrect.hpp new file mode 100644 index 00000000..86d4666d --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobrect.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "blobshape.hpp" + +#include +#include + +class BlobRect : public BlobShape { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY + stiffnessChanged) + Q_PROPERTY( + qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) + Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY + deformScaleChanged) + +public: + explicit BlobRect(QQuickItem* parent = nullptr); + ~BlobRect() override; + + qreal stiffness() const { return m_stiffness; } + + void setStiffness(qreal s) { + if (!qFuzzyCompare(m_stiffness, s)) { + m_stiffness = s; + emit stiffnessChanged(); + } + } + + qreal damping() const { return m_damping; } + + void setDamping(qreal d) { + if (!qFuzzyCompare(m_damping, d)) { + m_damping = d; + emit dampingChanged(); + } + } + + qreal deformScale() const { return m_deformScale; } + + void setDeformScale(qreal s) { + if (!qFuzzyCompare(m_deformScale, s)) { + m_deformScale = s; + emit deformScaleChanged(); + } + } + +signals: + void stiffnessChanged(); + void dampingChanged(); + void deformScaleChanged(); + +protected: + void updatePolish() override; + void updatePhysics() override; + +private: + void checkAtRest(float speed); + + // Physics state + QPointF m_prevScenePos; + QElapsedTimer m_elapsed; + bool m_physicsActive = false; + bool m_hasPrevPos = false; + + // Symmetric 2x2 deformation matrix components (3 independent: m00, m01, + // m11) Rest state is identity: m00=1, m01=0, m11=1 + float m_dm00 = 1.0f; + float m_dm01 = 0.0f; + float m_dm11 = 1.0f; + + // Spring velocities for each component + float m_dmVel00 = 0.0f; + float m_dmVel01 = 0.0f; + float m_dmVel11 = 0.0f; + + qreal m_stiffness = 200.0; + qreal m_damping = 16.0; + qreal m_deformScale = 0.0005; +}; diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp new file mode 100644 index 00000000..73b5cfe6 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -0,0 +1,365 @@ +#include "blobshape.hpp" +#include "blobgroup.hpp" +#include "blobinvertedrect.hpp" + +#include +#include + +#include +#include + +static float deformPadding(const QMatrix4x4& dm, float hw, float hh) { + // Bounding box of the deformed shape: |M * corners| + const float dm00 = dm(0, 0), dm01 = dm(0, 1); + const float dm10 = dm(1, 0), dm11 = dm(1, 1); + const float boundX = std::abs(dm00) * hw + std::abs(dm01) * hh; + const float boundY = std::abs(dm10) * hw + std::abs(dm11) * hh; + const float extraX = std::max(boundX - hw, 0.0f) + std::abs(dm(0, 3)); + const float extraY = std::max(boundY - hh, 0.0f) + std::abs(dm(1, 3)); + return std::max(extraX, extraY); +} + +static float cpuSdBox(float px, float py, float cx, float cy, float hw, + float hh) { + const float dx = std::abs(px - cx) - hw; + const float dy = std::abs(py - cy) - hh; + const float mdx = std::max(dx, 0.0f); + const float mdy = std::max(dy, 0.0f); + return std::sqrt(mdx * mdx + mdy * mdy) + std::min(std::max(dx, dy), 0.0f); +} + +static float cpuSmoothstep(float edge0, float edge1, float x) { + const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); +} + +BlobShape::BlobShape(QQuickItem* parent) + : QQuickItem(parent) { + setFlag(ItemHasContents); +} + +void BlobShape::setGroup(BlobGroup* g) { + if (m_group == g) + return; + if (m_group && isComponentComplete()) + unregisterFromGroup(); + m_group = g; + if (m_group && isComponentComplete()) + registerWithGroup(); + emit groupChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobShape::setRadius(qreal r) { + if (qFuzzyCompare(m_radius, r)) + return; + m_radius = r; + emit radiusChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobShape::componentComplete() { + QQuickItem::componentComplete(); + if (m_group) + registerWithGroup(); +} + +void BlobShape::geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) { + QQuickItem::geometryChange(newGeometry, oldGeometry); + updateCenteredDeformMatrix(); + if (m_group) + m_group->markDirty(); +} + +void BlobShape::updateCenteredDeformMatrix() { + const auto cx = static_cast(width()) * 0.5f; + const auto cy = static_cast(height()) * 0.5f; + QMatrix4x4 result; + result.translate(cx, cy); + result *= m_deformMatrix; + result.translate(-cx, -cy); + m_centeredDeformMatrix = result; + emit deformMatrixChanged(); +} + +void BlobShape::registerWithGroup() { + if (m_group) + m_group->addShape(this); +} + +void BlobShape::unregisterFromGroup() { + if (m_group) + m_group->removeShape(this); +} + +void BlobShape::updatePolish() { + if (!m_group) + return; + + // Ensure all shapes have up-to-date physics (only once per frame) + m_group->ensurePhysicsUpdated(); + + // When inverted rect renders everything, skip spatial query for others + if (!isInvertedRect() && m_group->invertedRect()) + return; + + const QPointF scenePos = mapToScene(QPointF(0, 0)); + const float pad = static_cast(m_group->smoothing()); + + if (isInvertedRect()) { + m_cachedPaddedX = static_cast(scenePos.x()); + m_cachedPaddedY = static_cast(scenePos.y()); + m_cachedPaddedW = static_cast(width()); + m_cachedPaddedH = static_cast(height()); + m_localPaddedRect = QRectF(0, 0, width(), height()); + } else { + const float hw = static_cast(width()) * 0.5f; + const float hh = static_cast(height()) * 0.5f; + const float totalPad = pad + deformPadding(m_deformMatrix, hw, hh); + + m_cachedPaddedX = static_cast(scenePos.x()) - totalPad; + m_cachedPaddedY = static_cast(scenePos.y()) - totalPad; + m_cachedPaddedW = static_cast(width()) + 2.0f * totalPad; + m_cachedPaddedH = static_cast(height()) + 2.0f * totalPad; + m_localPaddedRect = QRectF(static_cast(-totalPad), + static_cast(-totalPad), + width() + 2.0 * static_cast(totalPad), + height() + 2.0 * static_cast(totalPad)); + } + + // Filter nearby normal rects + m_cachedRects.clear(); + m_cachedMyIndex = -2; + const QRectF myPadded(static_cast(m_cachedPaddedX), + static_cast(m_cachedPaddedY), + static_cast(m_cachedPaddedW), + static_cast(m_cachedPaddedH)); + + for (BlobShape* other : m_group->shapes()) { + if (other->isInvertedRect()) + continue; + + const QPointF otherScene = other->mapToScene(QPointF(0, 0)); + + bool include = false; + if (isInvertedRect()) { + include = true; + } else { + const float otherHW = static_cast(other->width()) * 0.5f; + const float otherHH = static_cast(other->height()) * 0.5f; + const float otherPad = + pad + deformPadding(other->m_deformMatrix, otherHW, otherHH); + const QRectF otherPadded( + otherScene.x() - static_cast(otherPad), + otherScene.y() - static_cast(otherPad), + other->width() + 2.0 * static_cast(otherPad), + other->height() + 2.0 * static_cast(otherPad)); + include = myPadded.intersects(otherPadded); + } + + if (include) { + if (other == this) + m_cachedMyIndex = static_cast(m_cachedRects.size()); + + const QMatrix4x4& dm = other->m_deformMatrix; + const float a = dm(0, 0), b = dm(1, 0); + const float c = dm(0, 1), d = dm(1, 1); + + BlobRectData r; + r.cx = static_cast(otherScene.x() + other->width() / 2.0); + r.cy = static_cast(otherScene.y() + other->height() / 2.0); + r.hw = static_cast(other->width() / 2.0); + r.hh = static_cast(other->height() / 2.0); + r.radius = static_cast(other->radius()); + r.offsetX = dm(0, 3); + r.offsetY = dm(1, 3); + + // Pre-compute inverse deformation matrix + const float det = a * d - c * b; + const float invDet = + std::abs(det) > 1e-6f ? 1.0f / det : 1.0f; + r.invDeform[0] = d * invDet; + r.invDeform[1] = -b * invDet; + r.invDeform[2] = -c * invDet; + r.invDeform[3] = a * invDet; + + // Pre-compute minimum eigenvalue (avoids per-pixel sqrt) + const float halfTr = 0.5f * (a + d); + const float halfDiff = 0.5f * (a - d); + r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c); + + // Pre-compute screen-space AABB half-extents + r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh; + r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh; + + m_cachedRects.append(r); + } + } + + if (isInvertedRect()) + m_cachedMyIndex = -1; + + // Cache inverted rect data + m_cachedHasInverted = false; + m_cachedInvertedRadius = 0; + memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter)); + memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner)); + + auto* inv = m_group->invertedRect(); + if (inv) { + m_cachedHasInverted = true; + m_cachedInvertedRadius = static_cast(inv->radius()); + + const QPointF invScene = inv->mapToScene(QPointF(0, 0)); + const float outerCX = + static_cast(invScene.x() + inv->width() / 2.0); + const float outerCY = + static_cast(invScene.y() + inv->height() / 2.0); + const float outerHW = static_cast(inv->width() / 2.0); + const float outerHH = static_cast(inv->height() / 2.0); + + const float innerCX = + outerCX + + static_cast((inv->borderLeft() - inv->borderRight()) / 2.0); + const float innerCY = + outerCY + + static_cast((inv->borderTop() - inv->borderBottom()) / 2.0); + const float innerHW = + outerHW - + static_cast((inv->borderLeft() + inv->borderRight()) / 2.0); + const float innerHH = + outerHH - + static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); + + m_cachedInvertedOuter[0] = outerCX; + m_cachedInvertedOuter[1] = outerCY; + m_cachedInvertedOuter[2] = outerHW; + m_cachedInvertedOuter[3] = outerHH; + + m_cachedInvertedInner[0] = innerCX; + m_cachedInvertedInner[1] = innerCY; + m_cachedInvertedInner[2] = innerHW; + m_cachedInvertedInner[3] = innerHH; + } + + // Pre-compute corner fill factors (moves O(N²) work from GPU to CPU) + const float smoothFactor = pad; + const auto rectCount = m_cachedRects.size(); + for (qsizetype i = 0; i < rectCount; ++i) { + auto& ri = m_cachedRects[i]; + float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; + + const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; + const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh; + const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; + const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; + + for (qsizetype j = 0; j < rectCount; ++j) { + if (j == i) + continue; + const auto& rj = m_cachedRects[j]; + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); + } + + if (m_cachedHasInverted) { + const float icx = m_cachedInvertedInner[0]; + const float icy = m_cachedInvertedInner[1]; + const float ihw = m_cachedInvertedInner[2]; + const float ihh = m_cachedInvertedInner[3]; + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); + } + + ri.cornerFill[0] = fTr; + ri.cornerFill[1] = fBr; + ri.cornerFill[2] = fBl; + ri.cornerFill[3] = fTl; + } +} + +QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { + if (!m_group) { + delete oldNode; + return nullptr; + } + + // When an inverted rect exists, it renders everything in a single pass + if (!isInvertedRect() && m_group->invertedRect()) { + delete oldNode; + return nullptr; + } + + auto* node = static_cast(oldNode); + if (!node) { + node = new QSGGeometryNode; + + auto* geometry = new QSGGeometry( + QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); + geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); + node->setGeometry(geometry); + node->setFlag(QSGNode::OwnsGeometry); + + auto* material = new BlobMaterial; + material->setFlag(QSGMaterial::Blending); + node->setMaterial(material); + node->setFlag(QSGNode::OwnsMaterial); + } + + // Update geometry + auto* geometry = node->geometry(); + auto* v = geometry->vertexDataAsTexturedPoint2D(); + + const float x0 = static_cast(m_localPaddedRect.x()); + const float y0 = static_cast(m_localPaddedRect.y()); + const float x1 = x0 + static_cast(m_localPaddedRect.width()); + const float y1 = y0 + static_cast(m_localPaddedRect.height()); + + v[0].set(x0, y0, 0.0f, 0.0f); + v[1].set(x1, y0, 1.0f, 0.0f); + v[2].set(x0, y1, 0.0f, 1.0f); + v[3].set(x1, y1, 1.0f, 1.0f); + + node->markDirty(QSGNode::DirtyGeometry); + + // Update material + auto* material = static_cast(node->material()); + material->m_paddedX = m_cachedPaddedX; + material->m_paddedY = m_cachedPaddedY; + material->m_paddedW = m_cachedPaddedW; + material->m_paddedH = m_cachedPaddedH; + material->m_smoothFactor = static_cast(m_group->smoothing()); + material->m_myIndex = m_cachedMyIndex; + material->m_color = m_group->color(); + material->m_hasInverted = m_cachedHasInverted ? 1 : 0; + material->m_invertedRadius = m_cachedInvertedRadius; + memcpy(material->m_invertedOuter, m_cachedInvertedOuter, + sizeof(m_cachedInvertedOuter)); + memcpy(material->m_invertedInner, m_cachedInvertedInner, + sizeof(m_cachedInvertedInner)); + + const int count = + static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + material->m_rectCount = count; + for (int i = 0; i < count; ++i) + material->m_rects[i] = m_cachedRects[i]; + + node->markDirty(QSGNode::DirtyMaterial); + + return node; +} diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp new file mode 100644 index 00000000..8c6f1165 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "blobmaterial.hpp" + +#include +#include +#include + +class BlobGroup; + +class BlobShape : public QQuickItem { + Q_OBJECT + Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged) + Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) + Q_PROPERTY( + QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) + + friend class BlobGroup; + +public: + explicit BlobShape(QQuickItem* parent = nullptr); + ~BlobShape() override = default; + + BlobGroup* group() const { return m_group; } + + void setGroup(BlobGroup* g); + + qreal radius() const { return m_radius; } + + void setRadius(qreal r); + + QMatrix4x4 deformMatrix() const { return m_centeredDeformMatrix; } + +signals: + void groupChanged(); + void radiusChanged(); + void deformMatrixChanged(); + +protected: + void componentComplete() override; + void geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) override; + void updatePolish() override; + QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; + + virtual bool isInvertedRect() const { return false; } + + virtual void updatePhysics() {} + + virtual void registerWithGroup(); + virtual void unregisterFromGroup(); + void updateCenteredDeformMatrix(); + + BlobGroup* m_group = nullptr; + qreal m_radius = 0; + QMatrix4x4 m_deformMatrix; // identity by default + QMatrix4x4 m_centeredDeformMatrix; + + // Cached data from updatePolish + float m_cachedPaddedX = 0; + float m_cachedPaddedY = 0; + float m_cachedPaddedW = 0; + float m_cachedPaddedH = 0; + QRectF m_localPaddedRect; + QVector m_cachedRects; + int m_cachedMyIndex = -2; + bool m_cachedHasInverted = false; + float m_cachedInvertedRadius = 0; + float m_cachedInvertedOuter[4] = {}; + float m_cachedInvertedInner[4] = {}; +}; diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag new file mode 100644 index 00000000..f4baa731 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -0,0 +1,277 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float paddedX; + float paddedY; + float paddedW; + float paddedH; + float smoothFactor; + int rectCount; + int myIndex; + vec4 color; + int hasInverted; + float invertedRadius; + vec4 invertedOuter; + vec4 invertedInner; + vec4 rectData[80]; +}; + +float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) { + vec2 d = abs(p - center) - halfSize + vec2(radius); + return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - radius; +} + +float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) { + // r = (topRight, bottomRight, bottomLeft, topLeft) + p -= center; + r.xy = (p.x > 0.0) ? r.xy : r.wz; + r.x = (p.y > 0.0) ? r.y : r.x; + vec2 q = abs(p) - halfSize + r.x; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; +} + +float sdBox(vec2 p, vec2 center, vec2 halfSize) { + vec2 d = abs(p - center) - halfSize; + return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0); +} + +float smin(float a, float b, float k) { + // Cubic smooth min (C2 continuous — no curvature kinks at blend boundary) + float h = max(k - abs(a - b), 0.0) / k; + return min(a, b) - h * h * h * k * (1.0/6.0); +} + +float sminNoBulge(float a, float b, float k) { + // Cubic smooth min with reduced outward expansion when shapes overlap + float h = max(k - abs(a - b), 0.0) / k; + float blend = h * h * h * k * (1.0/6.0); + blend *= smoothstep(-k, 0.0, min(a, b)); + return min(a, b) - blend; +} + +float smax(float a, float b, float k) { + float h = max(k - abs(a - b), 0.0) / k; + return max(a, b) + h * h * h * k * (1.0/6.0); +} + +float smaxSharpA(float a, float b, float k) { + // smax variant that keeps a's boundary sharp (no inward rounding at a = 0). + // Used for the frame outer edge so it always fills to the edges. + float h = max(k - abs(a - b), 0.0) / k; + float blend = h * h * h * k * (1.0/6.0); + blend *= smoothstep(0.0, k * 0.5, -a); + return max(a, b) + blend; +} + +void main() { + vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); + + float mergedSdf = 1e10; + int owner = -2; + float minDist = 1e10; + + for (int i = 0; i < rectCount; i++) { + vec4 rect = rectData[i * 5]; // cx, cy, hw, hh + vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig + vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix + vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 + vec4 fills = rectData[i * 5 + 4]; // f_tr, f_br, f_bl, f_tl + + // Offset center for asymmetric deformation + vec2 center = rect.xy + props.yz; + + // Apply pre-computed inverse deformation to the evaluation point + mat2 invDeform = mat2(invDm.xy, invDm.zw); + vec2 transformedPixel = center + invDeform * (pixel - center); + + // Use pre-computed corner fill factors + float br = props.x; + float minR = 2.0; + vec4 radii = max(br * fills, vec4(minR)); + float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); + + // Use pre-computed minimum eigenvalue for SDF correction + d *= max(props.w, 0.01); + + // Scale SDF on the axis facing a nearby border to narrow the smin blend zone + // in that direction only, without reducing k (which would cause sharp edges). + if (hasInverted != 0) { + vec2 screenHalf = sh.xy; + + float distY0 = (center.y + screenHalf.y) - (invertedInner.y - invertedInner.w); + float distY1 = (invertedInner.y + invertedInner.w) - (center.y - screenHalf.y); + float distX0 = (center.x + screenHalf.x) - (invertedInner.x - invertedInner.z); + float distX1 = (invertedInner.x + invertedInner.z) - (center.x - screenHalf.x); + + // 0 = far from border, 1 = at border (max compression) + float yProx = 1.0 - min( + smoothstep(0.0, smoothFactor, distY0), + smoothstep(0.0, smoothFactor, distY1) + ); + float xProx = 1.0 - min( + smoothstep(0.0, smoothFactor, distX0), + smoothstep(0.0, smoothFactor, distX1) + ); + + // Smooth axis weights: gradient-based at corners, face-based inside. + vec2 q = abs(pixel - center) - screenHalf; + vec2 qp = max(q, vec2(0.0)); + float cornerLen = length(qp); + + // Gradient direction in corner region (smooth 90-degree rotation) + float gradX = qp.x / max(cornerLen, 0.001); + float gradY = qp.y / max(cornerLen, 0.001); + + // Smooth face weights for inside/edge (no hard branch) + float faceY = smoothstep(-4.0, 4.0, q.y - q.x); + float faceX = 1.0 - faceY; + + // Blend: gradient in corner region, face-based inside + float t = smoothstep(0.0, 2.0, cornerLen); + float xWeight = mix(faceX, gradX, t); + float yWeight = mix(faceY, gradY, t); + + float boost = 3.0; + float scale = 1.0 + (xProx * xWeight + yProx * yWeight) * boost; + d *= scale; + } + + // Rect-to-rect edge sink: indent this rect's edge where another + // rect is slightly past it, fading once past threshold. + if (rectCount > 1) { + vec2 iSh = sh.xy; + float sinkT = smoothFactor * 0.75; + float sinkOff = smoothFactor * (1.0 / 6.0); + float rectSinkVal = 0.0; + + for (int j = 0; j < rectCount; j++) { + if (j == i) continue; + + vec4 jR = rectData[j * 5]; + vec4 jP = rectData[j * 5 + 1]; + vec2 jSh = rectData[j * 5 + 3].xy; + vec2 jC = jR.xy + jP.yz; + + // Penetration of j past i's edges (positive = past) + float pT = (jC.y + jSh.y) - (center.y - iSh.y) - sinkOff; + float pB = (center.y + iSh.y) - (jC.y - jSh.y) - sinkOff; + float pL = (jC.x + jSh.x) - (center.x - iSh.x) - sinkOff; + float pR = (center.x + iSh.x) - (jC.x - jSh.x) - sinkOff; + + // Smooth bump: rises then falls, zero outside [0, sinkT] + float aT = smoothstep(0.0, sinkT * 0.4, pT) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pT)); + float aB = smoothstep(0.0, sinkT * 0.4, pB) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pB)); + float aL = smoothstep(0.0, sinkT * 0.4, pL) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pL)); + float aR = smoothstep(0.0, sinkT * 0.4, pR) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pR)); + + // Lateral falloff from rect j's extent + float hLat = max(abs(pixel.x - jC.x) - jSh.x, 0.0); + float vLat = max(abs(pixel.y - jC.y) - jSh.y, 0.0); + float latF = smoothFactor * 2.0; + + // Perpendicular zone (near rect i's edge only) + float zT = 1.0 - smoothstep(center.y - iSh.y, center.y - iSh.y + smoothFactor, pixel.y); + float zB = smoothstep(center.y + iSh.y - smoothFactor, center.y + iSh.y, pixel.y); + float zL = 1.0 - smoothstep(center.x - iSh.x, center.x - iSh.x + smoothFactor, pixel.x); + float zR = smoothstep(center.x + iSh.x - smoothFactor, center.x + iSh.x, pixel.x); + + float s = max( + max(aT * smoothstep(latF, 0.0, hLat) * zT, + aB * smoothstep(latF, 0.0, hLat) * zB), + max(aL * smoothstep(latF, 0.0, vLat) * zL, + aR * smoothstep(latF, 0.0, vLat) * zR) + ); + rectSinkVal = max(rectSinkVal, s); + } + d += rectSinkVal * smoothFactor * 0.25; + } + + mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); + if (d < minDist) { + minDist = d; + owner = i; + } + } + + if (hasInverted != 0) { + float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; + float dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); + + // Border sinks: track the opposite rect edge, clamped to border thickness + float innerTop = invertedInner.y - invertedInner.w; + float innerBot = invertedInner.y + invertedInner.w; + float innerLeft = invertedInner.x - invertedInner.z; + float innerRight = invertedInner.x + invertedInner.z; + float outerTop = invertedOuter.y - invertedOuter.w; + float outerBot = invertedOuter.y + invertedOuter.w; + float outerLeft = invertedOuter.x - invertedOuter.z; + float outerRight = invertedOuter.x + invertedOuter.z; + + float sinkValue = 0.0; + for (int i = 0; i < rectCount; i++) { + vec4 rect = rectData[i * 5]; + vec4 sinkProps = rectData[i * 5 + 1]; + vec2 sinkSh = rectData[i * 5 + 3].xy; + + // Screen-space center (with offset) and pre-computed AABB half-extents + vec2 ctr = rect.xy + sinkProps.yz; + + // Delay sink to absorb smin blend depth (cubic smin max = k/6) + float preOff = smoothFactor * (1.0/6.0); + + // Top border: track rect's BOTTOM edge, only within border thickness + float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop); + + // Bottom border: track rect's TOP edge + float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0, outerBot - innerBot); + + // Left border: track rect's RIGHT edge + float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0, innerLeft - outerLeft); + + // Right border: track rect's LEFT edge + float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0, outerRight - innerRight); + + // Lateral distance from pixel to rect's extent along each edge + float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0); + float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0); + + // Perpendicular proximity: full strength in border, fade inside inner area + float topZone = 1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y); + float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y); + float leftZone = 1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x); + float rightZone = smoothstep(innerRight - smoothFactor, innerRight, pixel.x); + + float s = smoothFactor * 2.0; + float sink = max( + max(topPen * smoothstep(s, 0.0, hLat) * topZone, + botPen * smoothstep(s, 0.0, hLat) * botZone), + max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, + rightPen * smoothstep(s, 0.0, vLat) * rightZone) + ); + sinkValue = max(sinkValue, sink); + } + + dInner -= sinkValue; + + float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor); + + mergedSdf = smin(mergedSdf, dFrame, smoothFactor); + if (dFrame < minDist) { + owner = -1; + } + } + + // myIndex == -1: inverted rect renders everything (frame + blobs) + // myIndex >= 0: individual rect renders only its owned pixels + if (myIndex >= 0 && owner != myIndex) + discard; + + float fw = fwidth(mergedSdf); + float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf); + fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity; +} diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.vert b/plugin/src/Caelestia/Blobs/shaders/blob.vert new file mode 100644 index 00000000..e71d8104 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/shaders/blob.vert @@ -0,0 +1,29 @@ +#version 440 + +layout(location = 0) in vec4 qt_VertexPosition; +layout(location = 1) in vec2 qt_VertexTexCoord; + +layout(location = 0) out vec2 qt_TexCoord0; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float paddedX; + float paddedY; + float paddedW; + float paddedH; + float smoothFactor; + int rectCount; + int myIndex; + vec4 color; + int hasInverted; + float invertedRadius; + vec4 invertedOuter; + vec4 invertedInner; + vec4 rectData[80]; +}; + +void main() { + gl_Position = qt_Matrix * qt_VertexPosition; + qt_TexCoord0 = qt_VertexTexCoord; +} diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 1b7d0e49..e8b72479 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus) +find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus) find_package(PkgConfig REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) @@ -60,3 +60,4 @@ qml_module(caelestia add_subdirectory(Internal) add_subdirectory(Models) add_subdirectory(Services) +add_subdirectory(Blobs)