remove layer usage to reduce vram consumption
This commit is contained in:
@@ -5,6 +5,10 @@ qml_module(ZShell-blobs
|
||||
blobshape.cpp
|
||||
blobrect.cpp
|
||||
blobinvertedrect.cpp
|
||||
blobgroup2.cpp
|
||||
blobshape2.cpp
|
||||
blobrect2.cpp
|
||||
blobinvertedrect2.cpp
|
||||
blobmaterial.cpp
|
||||
LIBRARIES
|
||||
Qt::Quick
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
#include "blobgroup2.hpp"
|
||||
#include "blobinvertedrect2.hpp"
|
||||
#include "blobshape2.hpp"
|
||||
|
||||
#include <qsggeometry.h>
|
||||
#include <qsgnode.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static float cornerFillFactor(float sd, float smoothFactor) {
|
||||
const float outside = cpuSmoothstep(0.0f, smoothFactor, sd);
|
||||
const float inside = cpuSmoothstep(0.0f, -smoothFactor, sd);
|
||||
return std::max(outside, inside);
|
||||
}
|
||||
|
||||
BlobGroup2::BlobGroup2(QQuickItem* parent) : QQuickItem(parent) {
|
||||
setFlag(ItemHasContents);
|
||||
}
|
||||
|
||||
BlobGroup2::~BlobGroup2() {
|
||||
for (auto* shape : std::as_const(m_shapes))
|
||||
shape->m_group = nullptr;
|
||||
if (m_invertedRect)
|
||||
static_cast<BlobShape2*>(m_invertedRect)->m_group = nullptr;
|
||||
}
|
||||
|
||||
void BlobGroup2::setSmoothing(qreal s) {
|
||||
if (qFuzzyCompare(m_smoothing, s)) return;
|
||||
m_smoothing = s;
|
||||
emit smoothingChanged();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void BlobGroup2::setColor(const QColor& c) {
|
||||
if (m_color == c) return;
|
||||
m_color = c;
|
||||
emit colorChanged();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void BlobGroup2::setCornerFill(bool e) {
|
||||
if (m_cornerFill == e) return;
|
||||
m_cornerFill = e;
|
||||
emit cornerFillChanged();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void BlobGroup2::setShadowColor(const QColor& c) {
|
||||
if (m_shadowColor == c) return;
|
||||
m_shadowColor = c;
|
||||
emit shadowColorChanged();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void BlobGroup2::setShadowRadius(qreal r) {
|
||||
if (qFuzzyCompare(m_shadowRadius, r)) return;
|
||||
m_shadowRadius = r;
|
||||
emit shadowRadiusChanged();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void BlobGroup2::addShape(BlobShape2* shape) {
|
||||
if (!shape || m_shapes.contains(shape)) return;
|
||||
m_shapes.append(shape);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void BlobGroup2::removeShape(BlobShape2* shape) {
|
||||
m_shapes.removeOne(shape);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void BlobGroup2::setInvertedRect(BlobInvertedRect2* rect) {
|
||||
if (m_invertedRect == rect) return;
|
||||
m_invertedRect = rect;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void BlobGroup2::clearInvertedRect(BlobInvertedRect2* rect) {
|
||||
if (m_invertedRect != rect) return;
|
||||
m_invertedRect = nullptr;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void BlobGroup2::markDirty() {
|
||||
m_physicsUpdated = false;
|
||||
m_dataBuilt = false;
|
||||
// Physics runs during polish, before the render pass. The panel content is
|
||||
// transformed by deformMatrix from QML, so the matrix has to be final for this
|
||||
// frame; building the data in updatePaintNode would let the transform lag one
|
||||
// frame behind the material.
|
||||
polish();
|
||||
update();
|
||||
}
|
||||
|
||||
void BlobGroup2::updatePolish() {
|
||||
// The group is the only thing that draws, so it also drives the shapes'
|
||||
// physics.
|
||||
ensureDataBuilt();
|
||||
}
|
||||
|
||||
void BlobGroup2::ensurePhysicsUpdated() {
|
||||
if (m_physicsUpdated) return;
|
||||
m_physicsUpdated = true;
|
||||
for (auto* shape : std::as_const(m_shapes))
|
||||
shape->updatePhysics();
|
||||
}
|
||||
|
||||
void BlobGroup2::ensureDataBuilt() {
|
||||
if (m_dataBuilt) return;
|
||||
ensurePhysicsUpdated();
|
||||
|
||||
// Physics can request the next frame through markDirty(); the data is
|
||||
// refreshed here anyway, so the flag is set after it runs.
|
||||
m_dataBuilt = true;
|
||||
m_data.rects.clear();
|
||||
m_bounds = QRectF();
|
||||
|
||||
const float smoothFactor = static_cast<float>(m_smoothing);
|
||||
// The same reach the shader uses to decide whether a rect can contribute, plus
|
||||
// the shadow, so every pixel the shader can give a value to is inside the node.
|
||||
const double extent = static_cast<double>(smoothFactor) * 1.5 +
|
||||
static_cast<double>(m_shadowRadius);
|
||||
|
||||
// Group order, no pruning: the shader sees this list for every pixel.
|
||||
// entryShapes maps a shared entry back to the shape it came from.
|
||||
QVector<BlobShape2*> entryShapes;
|
||||
|
||||
for (auto* shape : std::as_const(m_shapes)) {
|
||||
if (shape->width() <= 0 || shape->height() <= 0) continue;
|
||||
if (static_cast<int>(m_data.rects.size()) >= BlobGroupData::kMaxRects)
|
||||
continue;
|
||||
|
||||
const QPointF scene = shape->mapToScene(QPointF(0, 0));
|
||||
const QMatrix4x4& dm = shape->m_deformMatrix;
|
||||
|
||||
BlobRectData r;
|
||||
r.cx = static_cast<float>(scene.x() + shape->width() / 2.0);
|
||||
r.cy = static_cast<float>(scene.y() + shape->height() / 2.0);
|
||||
r.hw = static_cast<float>(shape->width()) * 0.5f;
|
||||
r.hh = static_cast<float>(shape->height()) * 0.5f;
|
||||
shape->cornerRadii(r.radius);
|
||||
r.offsetX = dm(0, 3);
|
||||
r.offsetY = dm(1, 3);
|
||||
|
||||
const float a = dm(0, 0), b = dm(1, 0);
|
||||
const float c = dm(0, 1), e = dm(1, 1);
|
||||
const float det = a * e - c * b;
|
||||
const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f;
|
||||
r.invDeform[0] = e * invDet;
|
||||
r.invDeform[1] = -b * invDet;
|
||||
r.invDeform[2] = -c * invDet;
|
||||
r.invDeform[3] = a * invDet;
|
||||
|
||||
const float halfTr = 0.5f * (a + e);
|
||||
const float halfDiff = 0.5f * (a - e);
|
||||
r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c);
|
||||
|
||||
r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh;
|
||||
r.screenHalfY = std::abs(b) * r.hw + std::abs(e) * r.hh;
|
||||
|
||||
m_data.rects.append(r);
|
||||
m_bounds = m_bounds.united(QRectF(
|
||||
static_cast<double>(r.cx) + static_cast<double>(r.offsetX) -
|
||||
static_cast<double>(r.screenHalfX) - extent,
|
||||
static_cast<double>(r.cy) + static_cast<double>(r.offsetY) -
|
||||
static_cast<double>(r.screenHalfY) - extent,
|
||||
2.0 * (static_cast<double>(r.screenHalfX) + extent),
|
||||
2.0 * (static_cast<double>(r.screenHalfY) + extent)));
|
||||
|
||||
entryShapes.append(shape);
|
||||
}
|
||||
|
||||
const int count = static_cast<int>(m_data.rects.size());
|
||||
|
||||
// Exclusion gates only the smooth blend.
|
||||
for (int i = 0; i < count; ++i) {
|
||||
int mask = 0;
|
||||
BlobShape2* const si = entryShapes[i];
|
||||
for (int j = 0; j < count; ++j) {
|
||||
if (j == i) continue;
|
||||
BlobShape2* const sj = entryShapes[j];
|
||||
if (si->isExcluded(sj) || sj->isExcluded(si)) mask |= (1 << j);
|
||||
}
|
||||
m_data.rects[i].excludeMask = mask;
|
||||
}
|
||||
|
||||
constexpr float minR = 2.0f;
|
||||
const bool cornerFill = m_cornerFill;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto& ri = m_data.rects[i];
|
||||
BlobShape2* const si = entryShapes[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;
|
||||
|
||||
if (cornerFill) {
|
||||
for (int j = 0; j < count; ++j) {
|
||||
if (j == i) continue;
|
||||
if (ri.excludeMask & (1 << j)) continue;
|
||||
BlobShape2* const sj = entryShapes[j];
|
||||
if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si))
|
||||
continue;
|
||||
const auto& rj = m_data.rects[j];
|
||||
fTr = std::min(
|
||||
fTr,
|
||||
cornerFillFactor(
|
||||
cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh),
|
||||
smoothFactor));
|
||||
fBr = std::min(
|
||||
fBr,
|
||||
cornerFillFactor(
|
||||
cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh),
|
||||
smoothFactor));
|
||||
fBl = std::min(
|
||||
fBl,
|
||||
cornerFillFactor(
|
||||
cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh),
|
||||
smoothFactor));
|
||||
fTl = std::min(
|
||||
fTl,
|
||||
cornerFillFactor(
|
||||
cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh),
|
||||
smoothFactor));
|
||||
}
|
||||
}
|
||||
|
||||
ri.radius[0] = std::max(ri.radius[0] * fTr, minR);
|
||||
ri.radius[1] = std::max(ri.radius[1] * fBr, minR);
|
||||
ri.radius[2] = std::max(ri.radius[2] * fBl, minR);
|
||||
ri.radius[3] = std::max(ri.radius[3] * fTl, minR);
|
||||
}
|
||||
|
||||
// Inverted rect. The frame term is evaluated for every pixel in the bounds,
|
||||
// so the group flag cannot be gated per shape.
|
||||
if (!m_invertedRect) return;
|
||||
|
||||
auto* inv = static_cast<BlobShape2*>(m_invertedRect);
|
||||
if (inv->width() <= 0 || inv->height() <= 0) return;
|
||||
|
||||
const QPointF invScene = inv->mapToScene(QPointF(0, 0));
|
||||
const float outerCX = static_cast<float>(invScene.x() + inv->width() / 2.0);
|
||||
const float outerCY =
|
||||
static_cast<float>(invScene.y() + inv->height() / 2.0);
|
||||
const float outerHW = static_cast<float>(inv->width()) * 0.5f;
|
||||
const float outerHH = static_cast<float>(inv->height()) * 0.5f;
|
||||
|
||||
const auto* invRect = m_invertedRect;
|
||||
const float innerCX =
|
||||
outerCX + static_cast<float>(
|
||||
(invRect->borderLeft() - invRect->borderRight()) / 2.0);
|
||||
const float innerCY =
|
||||
outerCY + static_cast<float>(
|
||||
(invRect->borderTop() - invRect->borderBottom()) / 2.0);
|
||||
const float innerHW =
|
||||
outerHW - static_cast<float>(
|
||||
(invRect->borderLeft() + invRect->borderRight()) / 2.0);
|
||||
const float innerHH =
|
||||
outerHH - static_cast<float>(
|
||||
(invRect->borderTop() + invRect->borderBottom()) / 2.0);
|
||||
|
||||
m_data.hasInverted = true;
|
||||
m_data.invertedRadius = static_cast<float>(invRect->radius());
|
||||
|
||||
m_data.invertedOuter[0] = outerCX;
|
||||
m_data.invertedOuter[1] = outerCY;
|
||||
m_data.invertedOuter[2] = outerHW;
|
||||
m_data.invertedOuter[3] = outerHH;
|
||||
|
||||
m_data.invertedInner[0] = innerCX;
|
||||
m_data.invertedInner[1] = innerCY;
|
||||
m_data.invertedInner[2] = innerHW;
|
||||
m_data.invertedInner[3] = innerHH;
|
||||
|
||||
m_bounds = m_bounds.united(QRectF(
|
||||
static_cast<double>(outerCX) - static_cast<double>(outerHW) - extent,
|
||||
static_cast<double>(outerCY) - static_cast<double>(outerHH) - extent,
|
||||
2.0 * (static_cast<double>(outerHW) + extent),
|
||||
2.0 * (static_cast<double>(outerHH) + extent)));
|
||||
}
|
||||
|
||||
void BlobGroup2::fillMaterial(BlobMaterial* material) {
|
||||
material->m_paddedX = static_cast<float>(m_bounds.x());
|
||||
material->m_paddedY = static_cast<float>(m_bounds.y());
|
||||
material->m_paddedW = static_cast<float>(m_bounds.width());
|
||||
material->m_paddedH = static_cast<float>(m_bounds.height());
|
||||
|
||||
material->m_smoothFactor = static_cast<float>(m_smoothing);
|
||||
// The single group draw has no owner, so the shader skips the ownership test.
|
||||
material->m_myIndex = -2;
|
||||
material->m_color = m_color;
|
||||
material->m_hasInverted = m_data.hasInverted ? 1 : 0;
|
||||
material->m_invertedRadius = data().invertedRadius;
|
||||
memcpy(
|
||||
material->m_invertedOuter,
|
||||
data().invertedOuter,
|
||||
sizeof(data().invertedOuter));
|
||||
memcpy(
|
||||
material->m_invertedInner,
|
||||
data().invertedInner,
|
||||
sizeof(data().invertedInner));
|
||||
|
||||
material->m_shadowRadius = static_cast<float>(m_shadowRadius);
|
||||
material->m_shadowColor[0] = static_cast<float>(m_shadowColor.redF());
|
||||
material->m_shadowColor[1] = static_cast<float>(m_shadowColor.greenF());
|
||||
material->m_shadowColor[2] = static_cast<float>(m_shadowColor.blueF());
|
||||
material->m_shadowColor[3] = static_cast<float>(m_shadowColor.alphaF());
|
||||
|
||||
const int count =
|
||||
static_cast<int>(qMin(data().rects.size(), qsizetype(16)));
|
||||
material->m_rectCount = count;
|
||||
for (int i = 0; i < count; ++i)
|
||||
material->m_rects[i] = data().rects[i];
|
||||
}
|
||||
|
||||
QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
|
||||
// Data is built during polish. This only covers a render pass that happens
|
||||
// without one.
|
||||
ensureDataBuilt();
|
||||
|
||||
if (m_data.rects.isEmpty() && !m_data.hasInverted) {
|
||||
delete oldNode;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* node = static_cast<QSGGeometryNode*>(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);
|
||||
}
|
||||
|
||||
// The shader evaluates the field at the scene position a vertex maps to, so
|
||||
// the group item itself must not be transformed. The vertex positions are the
|
||||
// bounds in this item's frame; the material carries the same rect in scene
|
||||
// coordinates.
|
||||
auto* v = node->geometry()->vertexDataAsTexturedPoint2D();
|
||||
const QPointF p0 = mapFromScene(m_bounds.topLeft());
|
||||
const QPointF p1 = mapFromScene(m_bounds.bottomRight());
|
||||
|
||||
v[0].set(static_cast<float>(p0.x()), static_cast<float>(p0.y()), 0.0f, 0.0f);
|
||||
v[1].set(static_cast<float>(p1.x()), static_cast<float>(p0.y()), 1.0f, 0.0f);
|
||||
v[2].set(static_cast<float>(p0.x()), static_cast<float>(p1.y()), 0.0f, 1.0f);
|
||||
v[3].set(static_cast<float>(p1.x()), static_cast<float>(p1.y()), 1.0f, 1.0f);
|
||||
|
||||
node->markDirty(QSGNode::DirtyGeometry);
|
||||
|
||||
fillMaterial(static_cast<BlobMaterial*>(node->material()));
|
||||
|
||||
node->markDirty(QSGNode::DirtyMaterial);
|
||||
|
||||
return node;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
#include "blobmaterial.hpp"
|
||||
|
||||
#include <qcolor.h>
|
||||
#include <qlist.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qrect.h>
|
||||
#include <qsgnode.h>
|
||||
#include <qvector.h>
|
||||
|
||||
class BlobShape2;
|
||||
class BlobInvertedRect2;
|
||||
|
||||
// One shared valuation for a whole group, drawn by one node.
|
||||
//
|
||||
// The shapes here are data only: they say where the material should be. The group
|
||||
// draws a single quad over everything the shader can evaluate, so a pixel cannot
|
||||
// be evaluated by one shape and missed by another, and no second draw can
|
||||
// disagree with the first. That is what makes overlapping shapes read as one
|
||||
// surface - one value, one writer, one alpha per pixel.
|
||||
struct BlobGroupData {
|
||||
// The shader reads rectData[80] = 16 entries, and excludeMask is a 32-bit
|
||||
// mask. The shared list is capped at 16, in group order, so the cap is the
|
||||
// same list for every pixel.
|
||||
static constexpr int kMaxRects = 16;
|
||||
|
||||
QVector<BlobRectData> rects;
|
||||
bool hasInverted = false;
|
||||
float invertedRadius = 0;
|
||||
float invertedOuter[4] = {};
|
||||
float invertedInner[4] = {};
|
||||
};
|
||||
|
||||
class BlobGroup2 : public QQuickItem {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(
|
||||
qreal smoothing READ smoothing WRITE setSmoothing NOTIFY
|
||||
smoothingChanged)
|
||||
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
|
||||
Q_PROPERTY(
|
||||
bool cornerFill READ cornerFill WRITE setCornerFill NOTIFY
|
||||
cornerFillChanged)
|
||||
Q_PROPERTY(
|
||||
QColor shadowColor READ shadowColor WRITE setShadowColor NOTIFY
|
||||
shadowColorChanged)
|
||||
Q_PROPERTY(
|
||||
qreal shadowRadius READ shadowRadius WRITE setShadowRadius NOTIFY
|
||||
shadowRadiusChanged)
|
||||
|
||||
friend class BlobShape2;
|
||||
|
||||
public:
|
||||
explicit BlobGroup2(QQuickItem* parent = nullptr);
|
||||
~BlobGroup2() override;
|
||||
|
||||
[[nodiscard]] qreal smoothing() const { return m_smoothing; }
|
||||
|
||||
void setSmoothing(qreal s);
|
||||
|
||||
[[nodiscard]] QColor color() const { return m_color; }
|
||||
|
||||
void setColor(const QColor& c);
|
||||
|
||||
[[nodiscard]] bool cornerFill() const { return m_cornerFill; }
|
||||
|
||||
void setCornerFill(bool e);
|
||||
|
||||
[[nodiscard]] QColor shadowColor() const { return m_shadowColor; }
|
||||
|
||||
void setShadowColor(const QColor& c);
|
||||
|
||||
[[nodiscard]] qreal shadowRadius() const { return m_shadowRadius; }
|
||||
|
||||
void setShadowRadius(qreal r);
|
||||
|
||||
void addShape(BlobShape2* shape);
|
||||
void removeShape(BlobShape2* shape);
|
||||
|
||||
void setInvertedRect(BlobInvertedRect2* rect);
|
||||
void clearInvertedRect(BlobInvertedRect2* rect);
|
||||
|
||||
[[nodiscard]] const QList<BlobShape2*>& shapes() const { return m_shapes; }
|
||||
|
||||
[[nodiscard]] BlobInvertedRect2* invertedRect() const {
|
||||
return m_invertedRect;
|
||||
}
|
||||
|
||||
[[nodiscard]] const BlobGroupData& data() const { return m_data; }
|
||||
|
||||
// One draw per group, so a change to any shape changes the value for every
|
||||
// pixel. Refresh the whole group.
|
||||
void markDirty();
|
||||
|
||||
signals:
|
||||
void smoothingChanged();
|
||||
void colorChanged();
|
||||
void cornerFillChanged();
|
||||
void shadowColorChanged();
|
||||
void shadowRadiusChanged();
|
||||
|
||||
protected:
|
||||
void updatePolish() override;
|
||||
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override;
|
||||
|
||||
private:
|
||||
void ensurePhysicsUpdated();
|
||||
void ensureDataBuilt();
|
||||
void fillMaterial(BlobMaterial* material);
|
||||
|
||||
qreal m_smoothing = 32.0;
|
||||
QColor m_color{0x44, 0x88, 0xff};
|
||||
bool m_cornerFill = true;
|
||||
QColor m_shadowColor{0, 0, 0, 128};
|
||||
qreal m_shadowRadius = 0.0;
|
||||
QList<BlobShape2*> m_shapes;
|
||||
BlobInvertedRect2* m_invertedRect = nullptr;
|
||||
bool m_physicsUpdated = false;
|
||||
bool m_dataBuilt = false;
|
||||
BlobGroupData m_data;
|
||||
|
||||
// Scene-space union of what the shader can evaluate for any shape. The node
|
||||
// covers exactly this, so gaps are not possible.
|
||||
QRectF m_bounds;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "blobinvertedrect2.hpp"
|
||||
#include "blobgroup2.hpp"
|
||||
|
||||
BlobInvertedRect2::BlobInvertedRect2(QQuickItem* parent) : BlobShape2(parent) {}
|
||||
|
||||
BlobInvertedRect2::~BlobInvertedRect2() {
|
||||
if (m_group) m_group->clearInvertedRect(this);
|
||||
}
|
||||
|
||||
void BlobInvertedRect2::setBorderLeft(qreal v) {
|
||||
if (qFuzzyCompare(m_borderLeft, v)) return;
|
||||
m_borderLeft = v;
|
||||
emit borderLeftChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobInvertedRect2::setBorderRight(qreal v) {
|
||||
if (qFuzzyCompare(m_borderRight, v)) return;
|
||||
m_borderRight = v;
|
||||
emit borderRightChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobInvertedRect2::setBorderTop(qreal v) {
|
||||
if (qFuzzyCompare(m_borderTop, v)) return;
|
||||
m_borderTop = v;
|
||||
emit borderTopChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobInvertedRect2::setBorderBottom(qreal v) {
|
||||
if (qFuzzyCompare(m_borderBottom, v)) return;
|
||||
m_borderBottom = v;
|
||||
emit borderBottomChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobInvertedRect2::registerWithGroup() {
|
||||
if (m_group) m_group->setInvertedRect(this);
|
||||
}
|
||||
|
||||
void BlobInvertedRect2::unregisterFromGroup() {
|
||||
if (m_group) m_group->clearInvertedRect(this);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include "blobshape2.hpp"
|
||||
|
||||
#include <qqmlengine.h>
|
||||
|
||||
class BlobInvertedRect2 : public BlobShape2 {
|
||||
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 BlobInvertedRect2(QQuickItem* parent = nullptr);
|
||||
~BlobInvertedRect2() override;
|
||||
|
||||
[[nodiscard]] qreal borderLeft() const { return m_borderLeft; }
|
||||
|
||||
void setBorderLeft(qreal v);
|
||||
|
||||
[[nodiscard]] qreal borderRight() const { return m_borderRight; }
|
||||
|
||||
void setBorderRight(qreal v);
|
||||
|
||||
[[nodiscard]] qreal borderTop() const { return m_borderTop; }
|
||||
|
||||
void setBorderTop(qreal v);
|
||||
|
||||
[[nodiscard]] qreal borderBottom() const { return m_borderBottom; }
|
||||
|
||||
void setBorderBottom(qreal v);
|
||||
|
||||
signals:
|
||||
void borderLeftChanged();
|
||||
void borderRightChanged();
|
||||
void borderTopChanged();
|
||||
void borderBottomChanged();
|
||||
|
||||
protected:
|
||||
void registerWithGroup() override;
|
||||
void unregisterFromGroup() override;
|
||||
|
||||
private:
|
||||
qreal m_borderLeft = 0;
|
||||
qreal m_borderRight = 0;
|
||||
qreal m_borderTop = 0;
|
||||
qreal m_borderBottom = 0;
|
||||
};
|
||||
@@ -33,7 +33,7 @@ bool BlobMaterialShader::updateUniformData(
|
||||
Q_UNUSED(oldMaterial);
|
||||
auto* mat = static_cast<BlobMaterial*>(newMaterial);
|
||||
QByteArray* buf = state.uniformData();
|
||||
Q_ASSERT(buf->size() >= 1440);
|
||||
Q_ASSERT(buf->size() >= 1472);
|
||||
|
||||
if (state.isMatrixDirty()) {
|
||||
const QMatrix4x4 m = state.combinedMatrix();
|
||||
@@ -56,7 +56,7 @@ bool BlobMaterialShader::updateUniformData(
|
||||
// Rect count (offset 88)
|
||||
memcpy(buf->data() + 88, &mat->m_rectCount, 4);
|
||||
|
||||
// My index (offset 92)
|
||||
// My index (offset 92). -2 skips the ownership test in the shader.
|
||||
memcpy(buf->data() + 92, &mat->m_myIndex, 4);
|
||||
|
||||
// Color as vec4 (offset 96, 16 bytes)
|
||||
@@ -100,5 +100,10 @@ bool BlobMaterialShader::updateUniformData(
|
||||
memcpy(buf->data() + base + 64, r.radius, 16);
|
||||
}
|
||||
|
||||
// Shadow: after rectData, which ends at 1440. shadowColor is 16-byte aligned,
|
||||
// so it lands at 1456.
|
||||
memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4);
|
||||
memcpy(buf->data() + 1456, mat->m_shadowColor, 16);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -32,12 +32,17 @@ class BlobMaterial : public QSGMaterial {
|
||||
float m_paddedH = 0;
|
||||
float m_smoothFactor = 32.0f;
|
||||
int m_rectCount = 0;
|
||||
// -2 means no owner test: the single group draw writes every pixel it covers.
|
||||
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] = {};
|
||||
// Phase 2: shadow around the material edges. Matches the uniform block, which
|
||||
// puts these after rectData.
|
||||
float m_shadowRadius = 0.0f;
|
||||
float m_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f};
|
||||
BlobRectData m_rects[16] = {};
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
#include <cmath>
|
||||
#include <qnamespace.h>
|
||||
|
||||
namespace {
|
||||
constexpr float k_retireMs = 120.0f;
|
||||
}
|
||||
|
||||
BlobRect::BlobRect(QQuickItem* parent) : BlobShape(parent) {}
|
||||
|
||||
BlobRect::~BlobRect() {
|
||||
@@ -12,41 +16,75 @@ BlobRect::~BlobRect() {
|
||||
}
|
||||
|
||||
void BlobRect::updatePolish() {
|
||||
if (m_physicsActive) {
|
||||
float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
|
||||
std::abs(m_dm11 - 1.0f);
|
||||
float totalVel =
|
||||
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
|
||||
BlobShape::updatePolish();
|
||||
|
||||
if (totalDelta < 0.004f && totalVel < 0.05f) {
|
||||
if (!m_physicsActive) return;
|
||||
|
||||
if (m_retiring) {
|
||||
const float t = std::clamp(
|
||||
static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs,
|
||||
0.0f,
|
||||
1.0f);
|
||||
const float s = t * t * (3.0f - 2.0f * t);
|
||||
|
||||
if (t >= 1.0f) {
|
||||
m_dm00 = 1.0f;
|
||||
m_dm01 = 0.0f;
|
||||
m_dm11 = 1.0f;
|
||||
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
|
||||
m_deformMatrix = QMatrix4x4();
|
||||
emit rawDeformMatrixChanged();
|
||||
updateCenteredDeformMatrix();
|
||||
m_retiring = false;
|
||||
m_physicsActive = false;
|
||||
|
||||
if (m_group) {
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this]() {
|
||||
if (m_group) m_group->markDirty();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
} else {
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this]() {
|
||||
if (m_physicsActive && m_group) m_group->markDirty();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
m_dm00 = m_retire00 + (1.0f - m_retire00) * s;
|
||||
m_dm01 = m_retire01 - m_retire01 * s;
|
||||
m_dm11 = m_retire11 + (1.0f - m_retire11) * s;
|
||||
m_deformMatrix = QMatrix4x4(
|
||||
m_dm00,
|
||||
m_dm01,
|
||||
0,
|
||||
0,
|
||||
m_dm01,
|
||||
m_dm11,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1);
|
||||
}
|
||||
|
||||
emit rawDeformMatrixChanged();
|
||||
updateCenteredDeformMatrix();
|
||||
|
||||
if (!m_physicsActive) return;
|
||||
} else {
|
||||
const float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
|
||||
std::abs(m_dm11 - 1.0f);
|
||||
const float totalVel =
|
||||
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
|
||||
|
||||
if (totalDelta < 0.004f && totalVel < 0.05f) beginRetire();
|
||||
}
|
||||
|
||||
BlobShape::updatePolish();
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this]() {
|
||||
if (m_physicsActive && m_group) m_group->markDirty();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void BlobRect::beginRetire() {
|
||||
m_retire00 = m_dm00;
|
||||
m_retire01 = m_dm01;
|
||||
m_retire11 = m_dm11;
|
||||
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
|
||||
m_retireElapsed.start();
|
||||
m_retiring = true;
|
||||
}
|
||||
|
||||
void BlobRect::updatePhysics() {
|
||||
@@ -74,6 +112,13 @@ void BlobRect::updatePhysics() {
|
||||
|
||||
const float speed = std::sqrt(velX * velX + velY * velY);
|
||||
|
||||
if (m_retiring) {
|
||||
if (speed > 5.0f)
|
||||
m_retiring = false;
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_physicsActive) {
|
||||
if (speed < 5.0f) return;
|
||||
m_physicsActive = true;
|
||||
@@ -297,6 +342,8 @@ void BlobRect::excludeCornersRemoveLast(QQmlListProperty<BlobRect>* prop) {
|
||||
}
|
||||
|
||||
void BlobRect::checkAtRest(float speed) {
|
||||
if (m_retiring) return;
|
||||
|
||||
constexpr float kEpsilon = 0.002f;
|
||||
const bool atRest =
|
||||
std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
|
||||
@@ -304,25 +351,5 @@ void BlobRect::checkAtRest(float speed) {
|
||||
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();
|
||||
emit rawDeformMatrixChanged();
|
||||
updateCenteredDeformMatrix();
|
||||
m_physicsActive = false;
|
||||
|
||||
if (m_group) {
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this]() {
|
||||
if (m_group) m_group->markDirty();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
if (atRest) beginRetire();
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ class BlobRect : public BlobShape {
|
||||
|
||||
private:
|
||||
void checkAtRest(float speed);
|
||||
void beginRetire();
|
||||
|
||||
// Physics state
|
||||
QPointF m_prevScenePos;
|
||||
@@ -115,6 +116,12 @@ class BlobRect : public BlobShape {
|
||||
bool m_physicsActive = false;
|
||||
bool m_hasPrevPos = false;
|
||||
|
||||
bool m_retiring = false;
|
||||
QElapsedTimer m_retireElapsed;
|
||||
float m_retire00 = 1.0f;
|
||||
float m_retire01 = 0.0f;
|
||||
float m_retire11 = 1.0f;
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
#include "blobrect2.hpp"
|
||||
#include "blobgroup2.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
constexpr float k_retireMs = 120.0f;
|
||||
} // namespace
|
||||
|
||||
BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {}
|
||||
|
||||
BlobRect2::~BlobRect2() {
|
||||
if (m_group) m_group->removeShape(this);
|
||||
}
|
||||
|
||||
void BlobRect2::finishAtIdentity() {
|
||||
m_dm00 = 1.0f;
|
||||
m_dm01 = 0.0f;
|
||||
m_dm11 = 1.0f;
|
||||
m_deformMatrix = QMatrix4x4();
|
||||
m_retiring = false;
|
||||
m_physicsActive = false;
|
||||
emit rawDeformMatrixChanged();
|
||||
updateCenteredDeformMatrix();
|
||||
}
|
||||
|
||||
void BlobRect2::beginRetire() {
|
||||
// The settle check would otherwise re-enter this every frame and restart the
|
||||
// curve timer, so the curve never reaches t = 1.
|
||||
if (m_retiring) return;
|
||||
|
||||
m_retire00 = m_dm00;
|
||||
m_retire01 = m_dm01;
|
||||
m_retire11 = m_dm11;
|
||||
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
|
||||
m_retireElapsed.start();
|
||||
m_retiring = true;
|
||||
}
|
||||
|
||||
void BlobRect2::updatePhysics() {
|
||||
// Shapes are data only, so the group drives physics here.
|
||||
if (m_retiring) {
|
||||
const float t = std::clamp(
|
||||
static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs,
|
||||
0.0f,
|
||||
1.0f);
|
||||
const float s = t * t * (3.0f - 2.0f * t);
|
||||
|
||||
if (t >= 1.0f) {
|
||||
finishAtIdentity();
|
||||
return;
|
||||
}
|
||||
|
||||
m_dm00 = m_retire00 + (1.0f - m_retire00) * s;
|
||||
m_dm01 = m_retire01 - m_retire01 * s;
|
||||
m_dm11 = m_retire11 + (1.0f - m_retire11) * s;
|
||||
m_deformMatrix = QMatrix4x4(
|
||||
m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
emit rawDeformMatrixChanged();
|
||||
updateCenteredDeformMatrix();
|
||||
|
||||
if (!m_physicsActive) return;
|
||||
}
|
||||
|
||||
// No "not active" guard here: an inactive shape still has to be evaluated so it
|
||||
// can see the panel start moving and become active. The frame loop is only kept
|
||||
// alive while active, and it is requested before anything can return early —
|
||||
// requesting it at the end lets a shape stop being evaluated with the matrix
|
||||
// still off identity, which is what left the panel contents blurry.
|
||||
if (m_physicsActive && m_group) m_group->markDirty();
|
||||
|
||||
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<float>(m_elapsed.restart()) / 1000.0f;
|
||||
if (dt > 0.1f || dt < 0.001f) {
|
||||
m_prevScenePos = scenePos;
|
||||
if (m_physicsActive) checkAtRest(0.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
const float velX =
|
||||
static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt;
|
||||
const float velY =
|
||||
static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
|
||||
m_prevScenePos = scenePos;
|
||||
|
||||
const float speed = std::sqrt(velX * velX + velY * velY);
|
||||
|
||||
if (m_retiring) {
|
||||
if (speed > 5.0f)
|
||||
m_retiring = false;
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_physicsActive) {
|
||||
if (speed < 5.0f) return;
|
||||
m_physicsActive = true;
|
||||
}
|
||||
|
||||
const float kStretchFactor = static_cast<float>(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;
|
||||
}
|
||||
|
||||
const float kStiffness = static_cast<float>(m_stiffness);
|
||||
const float kDamping = static_cast<float>(m_damping);
|
||||
const float invDamp = 1.0f / (1.0f + kDamping * dt);
|
||||
|
||||
m_dmVel00 = (m_dmVel00 - kStiffness * (m_dm00 - target00) * dt) * invDamp;
|
||||
m_dm00 += m_dmVel00 * dt;
|
||||
|
||||
m_dmVel01 = (m_dmVel01 - kStiffness * (m_dm01 - target01) * dt) * invDamp;
|
||||
m_dm01 += m_dmVel01 * dt;
|
||||
|
||||
m_dmVel11 = (m_dmVel11 - kStiffness * (m_dm11 - target11) * dt) * invDamp;
|
||||
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);
|
||||
emit rawDeformMatrixChanged();
|
||||
updateCenteredDeformMatrix();
|
||||
|
||||
// Start the retire as soon as the matrix is close to identity, as in v1. Waiting
|
||||
// for every term to fall below kEpsilon keeps the matrix just off identity for
|
||||
// longer, and the panel content is transformed by it, which reads as blur on
|
||||
// text.
|
||||
const float totalDelta =
|
||||
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
|
||||
const float totalVel =
|
||||
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
|
||||
// The residual is already inside the rest epsilon, so the eased curve would only
|
||||
// be aborted or restarted every frame by the panel creeping. Snap instead.
|
||||
if (totalDelta < 0.004f && totalVel < 0.05f) {
|
||||
finishAtIdentity();
|
||||
return;
|
||||
}
|
||||
|
||||
checkAtRest(speed);
|
||||
}
|
||||
|
||||
void BlobRect2::setTopLeftRadius(qreal r) {
|
||||
if (!qFuzzyCompare(m_topLeftRadius, r)) {
|
||||
m_topLeftRadius = r;
|
||||
emit topLeftRadiusChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void BlobRect2::setTopRightRadius(qreal r) {
|
||||
if (!qFuzzyCompare(m_topRightRadius, r)) {
|
||||
m_topRightRadius = r;
|
||||
emit topRightRadiusChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void BlobRect2::setBottomLeftRadius(qreal r) {
|
||||
if (!qFuzzyCompare(m_bottomLeftRadius, r)) {
|
||||
m_bottomLeftRadius = r;
|
||||
emit bottomLeftRadiusChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void BlobRect2::setBottomRightRadius(qreal r) {
|
||||
if (!qFuzzyCompare(m_bottomRightRadius, r)) {
|
||||
m_bottomRightRadius = r;
|
||||
emit bottomRightRadiusChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void BlobRect2::cornerRadii(float out[4]) const {
|
||||
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
|
||||
const auto base = std::min(static_cast<float>(m_radius), maxR);
|
||||
out[0] = std::min(
|
||||
m_topRightRadius >= 0 ? static_cast<float>(m_topRightRadius) : base,
|
||||
maxR);
|
||||
out[1] = std::min(
|
||||
m_bottomRightRadius >= 0 ? static_cast<float>(m_bottomRightRadius)
|
||||
: base,
|
||||
maxR);
|
||||
out[2] = std::min(
|
||||
m_bottomLeftRadius >= 0 ? static_cast<float>(m_bottomLeftRadius) : base,
|
||||
maxR);
|
||||
out[3] = std::min(
|
||||
m_topLeftRadius >= 0 ? static_cast<float>(m_topLeftRadius) : base,
|
||||
maxR);
|
||||
}
|
||||
|
||||
bool BlobRect2::isExcluded(const BlobShape2* other) const {
|
||||
for (const auto& ptr : m_exclude) {
|
||||
if (ptr == other) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BlobRect2::isCornerExcluded(const BlobShape2* other) const {
|
||||
for (const auto& ptr : m_excludeCorners) {
|
||||
if (ptr == other) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QQmlListProperty<BlobRect2> BlobRect2::exclude() {
|
||||
return QQmlListProperty<BlobRect2>(
|
||||
this,
|
||||
nullptr,
|
||||
&excludeAppend,
|
||||
&excludeCount,
|
||||
&excludeAt,
|
||||
&excludeClear,
|
||||
&excludeReplace,
|
||||
&excludeRemoveLast);
|
||||
}
|
||||
|
||||
QQmlListProperty<BlobRect2> BlobRect2::excludeCorners() {
|
||||
return QQmlListProperty<BlobRect2>(
|
||||
this,
|
||||
nullptr,
|
||||
&excludeCornersAppend,
|
||||
&excludeCornersCount,
|
||||
&excludeCornersAt,
|
||||
&excludeCornersClear,
|
||||
&excludeCornersReplace,
|
||||
&excludeCornersRemoveLast);
|
||||
}
|
||||
|
||||
void BlobRect2::excludeAppend(
|
||||
QQmlListProperty<BlobRect2>* prop, BlobRect2* rect) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
self->m_exclude.append(rect);
|
||||
if (self->m_group) self->m_group->markDirty();
|
||||
emit self->excludeChanged();
|
||||
}
|
||||
|
||||
qsizetype BlobRect2::excludeCount(QQmlListProperty<BlobRect2>* prop) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
return self->m_exclude.size();
|
||||
}
|
||||
|
||||
BlobRect2* BlobRect2::excludeAt(
|
||||
QQmlListProperty<BlobRect2>* prop, qsizetype index) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
return self->m_exclude.at(index);
|
||||
}
|
||||
|
||||
void BlobRect2::excludeClear(QQmlListProperty<BlobRect2>* prop) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
if (self->m_exclude.isEmpty()) return;
|
||||
self->m_exclude.clear();
|
||||
if (self->m_group) self->m_group->markDirty();
|
||||
emit self->excludeChanged();
|
||||
}
|
||||
|
||||
void BlobRect2::excludeReplace(
|
||||
QQmlListProperty<BlobRect2>* prop, qsizetype index, BlobRect2* rect) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
self->m_exclude[index] = rect;
|
||||
if (self->m_group) self->m_group->markDirty();
|
||||
emit self->excludeChanged();
|
||||
}
|
||||
|
||||
void BlobRect2::excludeRemoveLast(QQmlListProperty<BlobRect2>* prop) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
if (self->m_exclude.isEmpty()) return;
|
||||
self->m_exclude.removeLast();
|
||||
if (self->m_group) self->m_group->markDirty();
|
||||
emit self->excludeChanged();
|
||||
}
|
||||
|
||||
void BlobRect2::excludeCornersAppend(
|
||||
QQmlListProperty<BlobRect2>* prop, BlobRect2* rect) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
self->m_excludeCorners.append(rect);
|
||||
if (self->m_group) self->m_group->markDirty();
|
||||
emit self->excludeCornersChanged();
|
||||
}
|
||||
|
||||
qsizetype BlobRect2::excludeCornersCount(QQmlListProperty<BlobRect2>* prop) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
return self->m_excludeCorners.size();
|
||||
}
|
||||
|
||||
BlobRect2* BlobRect2::excludeCornersAt(
|
||||
QQmlListProperty<BlobRect2>* prop, qsizetype index) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
return self->m_excludeCorners.at(index);
|
||||
}
|
||||
|
||||
void BlobRect2::excludeCornersClear(QQmlListProperty<BlobRect2>* prop) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
if (self->m_excludeCorners.isEmpty()) return;
|
||||
self->m_excludeCorners.clear();
|
||||
if (self->m_group) self->m_group->markDirty();
|
||||
emit self->excludeCornersChanged();
|
||||
}
|
||||
|
||||
void BlobRect2::excludeCornersReplace(
|
||||
QQmlListProperty<BlobRect2>* prop, qsizetype index, BlobRect2* rect) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
self->m_excludeCorners[index] = rect;
|
||||
if (self->m_group) self->m_group->markDirty();
|
||||
emit self->excludeCornersChanged();
|
||||
}
|
||||
|
||||
void BlobRect2::excludeCornersRemoveLast(QQmlListProperty<BlobRect2>* prop) {
|
||||
auto* self = static_cast<BlobRect2*>(prop->object);
|
||||
if (self->m_excludeCorners.isEmpty()) return;
|
||||
self->m_excludeCorners.removeLast();
|
||||
if (self->m_group) self->m_group->markDirty();
|
||||
emit self->excludeCornersChanged();
|
||||
}
|
||||
|
||||
void BlobRect2::checkAtRest(float speed) {
|
||||
if (m_retiring) return;
|
||||
|
||||
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) beginRetire();
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
#pragma once
|
||||
|
||||
#include "blobshape2.hpp"
|
||||
|
||||
#include <qelapsedtimer.h>
|
||||
#include <qpointer.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmllist.h>
|
||||
|
||||
class BlobRect2 : public BlobShape2 {
|
||||
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)
|
||||
Q_PROPERTY(
|
||||
QQmlListProperty<BlobRect2> exclude READ exclude NOTIFY excludeChanged)
|
||||
Q_PROPERTY(
|
||||
QQmlListProperty<BlobRect2> excludeCorners READ excludeCorners NOTIFY
|
||||
excludeCornersChanged)
|
||||
Q_PROPERTY(
|
||||
qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY
|
||||
topLeftRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY
|
||||
topRightRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius
|
||||
NOTIFY bottomLeftRadiusChanged)
|
||||
Q_PROPERTY(
|
||||
qreal bottomRightRadius READ bottomRightRadius WRITE
|
||||
setBottomRightRadius NOTIFY bottomRightRadiusChanged)
|
||||
|
||||
public:
|
||||
explicit BlobRect2(QQuickItem* parent = nullptr);
|
||||
~BlobRect2() override;
|
||||
|
||||
[[nodiscard]] qreal stiffness() const { return m_stiffness; }
|
||||
|
||||
void setStiffness(qreal s) {
|
||||
if (!qFuzzyCompare(m_stiffness, s)) {
|
||||
m_stiffness = s;
|
||||
emit stiffnessChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] qreal damping() const { return m_damping; }
|
||||
|
||||
void setDamping(qreal value) {
|
||||
if (!qFuzzyCompare(m_damping, value)) {
|
||||
m_damping = value;
|
||||
emit dampingChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] qreal deformScale() const { return m_deformScale; }
|
||||
|
||||
void setDeformScale(qreal s) {
|
||||
if (!qFuzzyCompare(m_deformScale, s)) {
|
||||
m_deformScale = s;
|
||||
emit deformScaleChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QQmlListProperty<BlobRect2> exclude();
|
||||
QQmlListProperty<BlobRect2> excludeCorners();
|
||||
|
||||
bool isExcluded(const BlobShape2* other) const override;
|
||||
bool isCornerExcluded(const BlobShape2* other) const override;
|
||||
void cornerRadii(float out[4]) const override;
|
||||
|
||||
[[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; }
|
||||
|
||||
void setTopLeftRadius(qreal r);
|
||||
|
||||
[[nodiscard]] qreal topRightRadius() const { return m_topRightRadius; }
|
||||
|
||||
void setTopRightRadius(qreal r);
|
||||
|
||||
[[nodiscard]] qreal bottomLeftRadius() const { return m_bottomLeftRadius; }
|
||||
|
||||
void setBottomLeftRadius(qreal r);
|
||||
|
||||
[[nodiscard]] qreal bottomRightRadius() const {
|
||||
return m_bottomRightRadius;
|
||||
}
|
||||
|
||||
void setBottomRightRadius(qreal r);
|
||||
|
||||
signals:
|
||||
void stiffnessChanged();
|
||||
void dampingChanged();
|
||||
void deformScaleChanged();
|
||||
void excludeChanged();
|
||||
void excludeCornersChanged();
|
||||
void topLeftRadiusChanged();
|
||||
void topRightRadiusChanged();
|
||||
void bottomLeftRadiusChanged();
|
||||
void bottomRightRadiusChanged();
|
||||
|
||||
protected:
|
||||
void updatePhysics() override;
|
||||
|
||||
private:
|
||||
void checkAtRest(float speed);
|
||||
void beginRetire();
|
||||
void finishAtIdentity();
|
||||
|
||||
// Physics state
|
||||
QPointF m_prevScenePos;
|
||||
QElapsedTimer m_elapsed;
|
||||
bool m_physicsActive = false;
|
||||
bool m_hasPrevPos = false;
|
||||
|
||||
bool m_retiring = false;
|
||||
QElapsedTimer m_retireElapsed;
|
||||
float m_retire00 = 1.0f;
|
||||
float m_retire01 = 0.0f;
|
||||
float m_retire11 = 1.0f;
|
||||
|
||||
// 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;
|
||||
|
||||
qreal m_topLeftRadius = -1;
|
||||
qreal m_topRightRadius = -1;
|
||||
qreal m_bottomLeftRadius = -1;
|
||||
qreal m_bottomRightRadius = -1;
|
||||
|
||||
QList<QPointer<BlobRect2>> m_exclude;
|
||||
QList<QPointer<BlobRect2>> m_excludeCorners;
|
||||
|
||||
static void excludeAppend(
|
||||
QQmlListProperty<BlobRect2>* prop, BlobRect2* rect);
|
||||
static qsizetype excludeCount(QQmlListProperty<BlobRect2>* prop);
|
||||
static BlobRect2* excludeAt(
|
||||
QQmlListProperty<BlobRect2>* prop, qsizetype index);
|
||||
static void excludeClear(QQmlListProperty<BlobRect2>* prop);
|
||||
static void excludeReplace(
|
||||
QQmlListProperty<BlobRect2>* prop, qsizetype index, BlobRect2* rect);
|
||||
static void excludeRemoveLast(QQmlListProperty<BlobRect2>* prop);
|
||||
|
||||
static void excludeCornersAppend(
|
||||
QQmlListProperty<BlobRect2>* prop, BlobRect2* rect);
|
||||
static qsizetype excludeCornersCount(QQmlListProperty<BlobRect2>* prop);
|
||||
static BlobRect2* excludeCornersAt(
|
||||
QQmlListProperty<BlobRect2>* prop, qsizetype index);
|
||||
static void excludeCornersClear(QQmlListProperty<BlobRect2>* prop);
|
||||
static void excludeCornersReplace(
|
||||
QQmlListProperty<BlobRect2>* prop, qsizetype index, BlobRect2* rect);
|
||||
static void excludeCornersRemoveLast(QQmlListProperty<BlobRect2>* prop);
|
||||
};
|
||||
@@ -168,8 +168,10 @@ void BlobShape::updatePolish() {
|
||||
|
||||
if (other->width() <= 0 || other->height() <= 0) continue;
|
||||
|
||||
if (isExcluded(other)) continue;
|
||||
|
||||
// Excluded shapes stay in the rect data. Exclusion only gates the smooth
|
||||
// blend (via excludeMask below); ownership must still see every shape,
|
||||
// otherwise two nodes can both believe they own a pixel and the pixel is
|
||||
// blended twice.
|
||||
const QPointF otherScene = other->mapToScene(QPointF(0, 0));
|
||||
|
||||
bool include = false;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "blobshape2.hpp"
|
||||
#include "blobgroup2.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
BlobShape2::BlobShape2(QQuickItem* parent) : QQuickItem(parent) {}
|
||||
|
||||
void BlobShape2::setGroup(BlobGroup2* 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 BlobShape2::setRadius(qreal r) {
|
||||
if (qFuzzyCompare(m_radius, r)) return;
|
||||
m_radius = r;
|
||||
emit radiusChanged();
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobShape2::componentComplete() {
|
||||
QQuickItem::componentComplete();
|
||||
if (m_group) registerWithGroup();
|
||||
}
|
||||
|
||||
void BlobShape2::geometryChange(
|
||||
const QRectF& newGeometry, const QRectF& oldGeometry) {
|
||||
QQuickItem::geometryChange(newGeometry, oldGeometry);
|
||||
updateCenteredDeformMatrix();
|
||||
|
||||
// One draw covers the whole group, so any shape change is a group change.
|
||||
if (m_group) m_group->markDirty();
|
||||
}
|
||||
|
||||
void BlobShape2::updateCenteredDeformMatrix() {
|
||||
const auto cx = static_cast<float>(width()) * 0.5f;
|
||||
const auto cy = static_cast<float>(height()) * 0.5f;
|
||||
QMatrix4x4 result;
|
||||
result.translate(cx, cy);
|
||||
result *= m_deformMatrix;
|
||||
result.translate(-cx, -cy);
|
||||
if (m_centeredDeformMatrix != result) {
|
||||
m_centeredDeformMatrix = result;
|
||||
emit deformMatrixChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void BlobShape2::cornerRadii(float out[4]) const {
|
||||
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
|
||||
const auto r = std::min(static_cast<float>(m_radius), maxR);
|
||||
out[0] = r;
|
||||
out[1] = r;
|
||||
out[2] = r;
|
||||
out[3] = r;
|
||||
}
|
||||
|
||||
void BlobShape2::registerWithGroup() {
|
||||
if (m_group) m_group->addShape(this);
|
||||
}
|
||||
|
||||
void BlobShape2::unregisterFromGroup() {
|
||||
if (m_group) m_group->removeShape(this);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "blobgroup2.hpp"
|
||||
|
||||
#include <qmatrix4x4.h>
|
||||
#include <qquickitem.h>
|
||||
|
||||
class BlobGroup2;
|
||||
|
||||
// Data-only shape.
|
||||
//
|
||||
// The group draws a single node for the whole group, so a shape here contributes
|
||||
// only what the shader needs: position, radii, exclusion, physics. A shape does
|
||||
// not create a render node, so no pixel can be evaluated by one shape and missed
|
||||
// by another.
|
||||
class BlobShape2 : public QQuickItem {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(BlobGroup2* 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)
|
||||
Q_PROPERTY(
|
||||
QMatrix4x4 rawDeformMatrix READ rawDeformMatrix NOTIFY
|
||||
rawDeformMatrixChanged)
|
||||
|
||||
friend class BlobGroup2;
|
||||
|
||||
public:
|
||||
explicit BlobShape2(QQuickItem* parent = nullptr);
|
||||
~BlobShape2() override = default;
|
||||
|
||||
[[nodiscard]] BlobGroup2* group() const { return m_group; }
|
||||
|
||||
void setGroup(BlobGroup2* g);
|
||||
|
||||
[[nodiscard]] qreal radius() const { return m_radius; }
|
||||
|
||||
void setRadius(qreal r);
|
||||
|
||||
[[nodiscard]] QMatrix4x4 deformMatrix() const {
|
||||
return m_centeredDeformMatrix;
|
||||
}
|
||||
|
||||
[[nodiscard]] QMatrix4x4 rawDeformMatrix() const { return m_deformMatrix; }
|
||||
|
||||
signals:
|
||||
void groupChanged();
|
||||
void radiusChanged();
|
||||
void deformMatrixChanged();
|
||||
void rawDeformMatrixChanged();
|
||||
|
||||
protected:
|
||||
void componentComplete() override;
|
||||
void geometryChange(
|
||||
const QRectF& newGeometry, const QRectF& oldGeometry) override;
|
||||
|
||||
virtual bool isExcluded(const BlobShape2*) const { return false; }
|
||||
|
||||
virtual bool isCornerExcluded(const BlobShape2* /*other*/) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual void cornerRadii(float out[4]) const;
|
||||
|
||||
// Called by the group when it rebuilds the shared data, once per frame.
|
||||
virtual void updatePhysics() {}
|
||||
|
||||
virtual void registerWithGroup();
|
||||
virtual void unregisterFromGroup();
|
||||
void updateCenteredDeformMatrix();
|
||||
|
||||
BlobGroup2* m_group = nullptr;
|
||||
qreal m_radius = 0;
|
||||
QMatrix4x4 m_deformMatrix; // identity by default
|
||||
QMatrix4x4 m_centeredDeformMatrix;
|
||||
};
|
||||
@@ -19,6 +19,8 @@ layout(std140, binding = 0) uniform buf {
|
||||
vec4 invertedOuter;
|
||||
vec4 invertedInner;
|
||||
vec4 rectData[80];
|
||||
float shadowRadius;
|
||||
vec4 shadowColor;
|
||||
};
|
||||
|
||||
float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) {
|
||||
@@ -63,9 +65,10 @@ float smaxSharpA(float a, float b, float k) {
|
||||
void main() {
|
||||
vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH);
|
||||
|
||||
// Phase 1: compute per-rect SDF, track owner. We can't smin yet because
|
||||
// excluded pairs need to skip the smooth blend, which requires pairwise pass
|
||||
// below.
|
||||
// Phase 1: compute per-rect SDF, and track which rect owns the pixel for the
|
||||
// per-shape path (BlobGroup). The single group draw (BlobGroup2) has no owner:
|
||||
// myIndex stays -2, so the ownership test is skipped and this pass writes every
|
||||
// pixel the quad covers. Excluded pairs still need the pairwise pass below.
|
||||
float dArr[16];
|
||||
int owner = -2;
|
||||
float minDist = 1e10;
|
||||
@@ -82,8 +85,10 @@ void main() {
|
||||
// Offset center for asymmetric deformation
|
||||
vec2 center = rect.xy + props.yz;
|
||||
|
||||
// AABB early-out: skip rects far from this pixel
|
||||
vec2 extent = sh.xy + vec2(smoothFactor * 1.5);
|
||||
// AABB early-out: skip rects far from this pixel. The reach has to include the
|
||||
// shadow, otherwise a rect that is only inside the shadow range would be
|
||||
// skipped and its distance could not be used.
|
||||
vec2 extent = sh.xy + vec2(smoothFactor * 1.5 + shadowRadius);
|
||||
if (abs(pixel.x - center.x) > extent.x ||
|
||||
abs(pixel.y - center.y) > extent.y) {
|
||||
dArr[i] = 1e10;
|
||||
@@ -251,14 +256,39 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Each renderer only outputs pixels it owns, but allow rendering
|
||||
// blend zones to prevent gaps (mergedSdf < smoothFactor means in blend)
|
||||
// myIndex == -1: inverted rect renders border-owned pixels
|
||||
// myIndex >= 0: individual rect renders its owned pixels
|
||||
if (owner != myIndex && mergedSdf > smoothFactor)
|
||||
// A pixel this far from every rect cannot have a value, so it is not written.
|
||||
// The one-unit margin keeps the outermost pixels of the quad out of the AA: their
|
||||
// derivatives are undefined (the quad neighbour is outside the primitive), which
|
||||
// is what drew a line along the coverage edge.
|
||||
float reach = max(smoothFactor, shadowRadius);
|
||||
if (mergedSdf > reach - 1.0)
|
||||
discard;
|
||||
|
||||
float fw = fwidth(mergedSdf);
|
||||
float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf);
|
||||
fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity;
|
||||
// Only the owner writes a pixel, which is what makes the per-shape path look
|
||||
// like one surface: qt_Opacity is applied once per pixel instead of once per
|
||||
// overlapping draw. The single group draw has no owner (myIndex == -2), so the
|
||||
// test is skipped and the value below is the only value a pixel can have.
|
||||
if (myIndex >= -1 && owner != myIndex)
|
||||
discard;
|
||||
|
||||
// Fixed-width AA: fwidth is capped at one unit, so a bad derivative near the quad
|
||||
// boundary cannot make a wide edge.
|
||||
float fw = min(fwidth(mergedSdf), 1.0);
|
||||
float materialAlpha = 1.0 - smoothstep(-fw, fw, mergedSdf);
|
||||
|
||||
// Phase 2: the shadow, as a distance falloff from the merged surface. The
|
||||
// quadratic reaches zero with zero slope at its outer edge, so that edge needs no
|
||||
// AA of its own. Only the single group draw writes a shadow: in the per-shape
|
||||
// path overlapping draws would darken the same area twice.
|
||||
float shadowAlpha = 0.0;
|
||||
if (shadowRadius > 0.0 && myIndex == -2) {
|
||||
float t = clamp(mergedSdf / shadowRadius, 0.0, 1.0);
|
||||
shadowAlpha = (1.0 - t) * (1.0 - t) * shadowColor.a;
|
||||
}
|
||||
|
||||
// Material over shadow, premultiplied.
|
||||
float totalAlpha = materialAlpha + shadowAlpha * (1.0 - materialAlpha);
|
||||
vec3 rgb = color.rgb * materialAlpha
|
||||
+ shadowColor.rgb * shadowAlpha * (1.0 - materialAlpha);
|
||||
fragColor = vec4(rgb, totalAlpha) * qt_Opacity;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user