how to avoid pixmap cutting issues when we using qpainter while rotation - qt

label=new QLabel(this);
label->setGeometry(this->width()/2,this->height()/2,label->width(),label->height());
QPixmap myPixmapForNow;
myPixmapForNow.load("C://Users//abc//Documents//QpixMap//hub_needle.png");
label->setMinimumSize(QSize(myPixmapForNow.width(),myPixmapForNow.width()));
label->setAlignment(Qt::AlignCenter);
QPixmap rotated(label->width(),label->width());
QPainter p(&rotated);
p.setRenderHint(QPainter::Antialiasing);
p.setRenderHint(QPainter::SmoothPixmapTransform);
p.setRenderHint(QPainter::HighQualityAntialiasing);
p.translate(myPixmapForNow.size().width() / 2,
(myPixmapForNow.size().height() / 2));
qDebug()<<"before rotation width:"<<rotated.size().width()<<"height:"<<rotated.size().width();
p.rotate(arg1);
p.translate(-myPixmapForNow.size().width() / 2,
-(myPixmapForNow.size().height() / 2));
qDebug()<<"after rotation height:"<<-rotated.size().width()<<"height:"<<-rotated.size().height();[![enter image description here][1]][1]
p.drawPixmap(QRect(0,0,myPixmapForNow.width(),myPixmapForNow.height()), myPixmapForNow);
p.end();
label->setPixmap(rotated);
After rotation
before rotation

I must admit the OP could have explained the issue a bit more in detail. Unfortunately, OP didn't react on comments.
However, out of curiosity, I tried to puzzle this out in a little demo. (I really like to write little Qt demos, especially with image manipulation and cat pictures.)
My first assumption was that OP has struggled with the order of transformations.
While translations are commutative (changing order doesn't change result), this is not the case for rotations (and other transformations).
However, after having wrapped OPs code into a MCVE, I convinced myself that the order of transformations matched my expectation – a rotation about the center of image.
So, I focused on the title
how to avoid pixmap cutting issues when we using qpainter while rotation
The reason for the “cutting issue” is simple:
To paint a rotated image (rectangle), the output may require a greater range of pixels then the original.
There are two possibilities to fix this:
enlarge the QPixmap for output
scale the result to match the original size of QPixmap.
So, this leaves the task to determine the output size of the rotated image beforehand, to either make the output QPixmap respectively larger or to add the respective scaling.
The bounding rectangle of a rotated rectangle can be calculated with trigonometric functions (sin, cos, etc.) I decided instead (for an IMHO more naïve way) to let Qt do the work for me.
To achieve this, the transformation has to be calculated before creating the QPixmap and QPainter. Hence, the prior
qPainter.translate(cx, cy);
qPainter.rotate(ra);
qPainter.translate(-cx, -cy);
is replaced by:
QTransform xform;
xform.translate(cx, cy);
xform.rotate(ra);
xform.translate(-cx, -cy);
which can be later applied as is:
qPainter.setTransform(xform);
I used the fact that all four corners of the rotated rectangle will touch the bounding rectangle. So, the bounding rectangle can be calculated by applying min() and max() to the x and y components of the rotated image corners:
const QPoint ptTL = xform * QPoint(0, 0);
const QPoint ptTR = xform * QPoint(w - 1, 0);
const QPoint ptBL = xform * QPoint(0, h - 1);
const QPoint ptBR = xform * QPoint(w - 1, h - 1);
QRect qRectBB(
QPoint(
min(ptTL.x(), ptTR.x(), ptBL.x(), ptBR.x()),
min(ptTL.y(), ptTR.y(), ptBL.y(), ptBR.y())),
QPoint(
max(ptTL.x(), ptTR.x(), ptBL.x(), ptBR.x()),
max(ptTL.y(), ptTR.y(), ptBL.y(), ptBR.y())));
Afterwards, the output may be adjusted using the origin and size of qRectBB.
The whole demo application testQPainterRotateCenter.cc:
#include <algorithm>
// Qt header:
#include <QtWidgets>
int min(int x0, int x1, int x2, int x3)
{
return std::min(std::min(x0, x1), std::min(x2, x3));
}
int max(int x0, int x1, int x2, int x3)
{
return std::max(std::max(x0, x1), std::max(x2, x3));
}
QPixmap rotate(
const QPixmap &qPixMapOrig, int cx, int cy, int ra,
bool fitIn, bool keepSize)
{
int w = qPixMapOrig.width(), h = qPixMapOrig.height();
QTransform xform;
xform.translate(cx, cy);
xform.rotate(ra);
xform.translate(-cx, -cy);
if (fitIn) {
// find bounding rect
const QPoint ptTL = xform * QPoint(0, 0);
const QPoint ptTR = xform * QPoint(w - 1, 0);
const QPoint ptBL = xform * QPoint(0, h - 1);
const QPoint ptBR = xform * QPoint(w - 1, h - 1);
QRect qRectBB(
QPoint(
min(ptTL.x(), ptTR.x(), ptBL.x(), ptBR.x()),
min(ptTL.y(), ptTR.y(), ptBL.y(), ptBR.y())),
QPoint(
max(ptTL.x(), ptTR.x(), ptBL.x(), ptBR.x()),
max(ptTL.y(), ptTR.y(), ptBL.y(), ptBR.y())));
qDebug() << "Bounding box:" << qRectBB;
// translate top left corner to (0, 0)
xform *= QTransform().translate(-qRectBB.left(), -qRectBB.top());
if (keepSize) {
// center align scaled image
xform *= w > h
? QTransform().translate((w - h) / 2, 0)
: QTransform().translate(0, (h - w) / 2);
// add scaling to transform
const qreal sx = qreal(w) / qRectBB.width();
const qreal sy = qreal(h) / qRectBB.height();
const qreal s = std::min(sx, sy);
xform *= QTransform().scale(s, s);
} else {
// adjust w and h
w = qRectBB.width(); h = qRectBB.height();
}
}
QPixmap qPixMap(w, h);
qPixMap.fill(Qt::gray);
{ QPainter qPainter(&qPixMap);
qPainter.setRenderHint(QPainter::Antialiasing);
qPainter.setRenderHint(QPainter::SmoothPixmapTransform);
qPainter.setRenderHint(QPainter::HighQualityAntialiasing);
qPainter.setTransform(xform);
qPainter.drawPixmap(0, 0, qPixMapOrig.width(), qPixMapOrig.height(), qPixMapOrig);
} // end of scope -> finalize QPainter
return qPixMap;
}
// main application
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// setup data
const QString file = QString::fromUtf8("cats.jpg");
QPixmap qPixMapOrig;
qPixMapOrig.load(file);
int cx = qPixMapOrig.width() / 2, cy = qPixMapOrig.height() / 2;
int ra = 0;
// setup GUI
QWidget qWin;
qWin.setWindowTitle(
file % QString(" (")
% QString::number(qPixMapOrig.width())
% " x " % QString::number(qPixMapOrig.height())
% ") - testQPainterRotateCenter");
QVBoxLayout qVBox;
QHBoxLayout qHBox1;
QLabel qLblCX(QString::fromUtf8("center x:"));
qHBox1.addWidget(&qLblCX);
QLineEdit qEditCX;
qEditCX.setText(QString::number(cx));
qHBox1.addWidget(&qEditCX, 1);
QLabel qLblCY(QString::fromUtf8("center y:"));
qHBox1.addWidget(&qLblCY);
QLineEdit qEditCY;
qEditCY.setText(QString::number(cy));
qHBox1.addWidget(&qEditCY, 1);
QLabel qLblRA(QString::fromUtf8("rotation angle:"));
qHBox1.addWidget(&qLblRA);
QSpinBox qEditRA;
qEditRA.setValue(ra);
qHBox1.addWidget(&qEditRA, 1);
qVBox.addLayout(&qHBox1);
QHBoxLayout qHBox2;
QCheckBox qTglFitIn(QString::fromUtf8("Zoom to Fit"));
qTglFitIn.setChecked(false);
qHBox2.addWidget(&qTglFitIn);
QCheckBox qTglKeepSize(QString::fromUtf8("Keep Size"));
qTglKeepSize.setChecked(false);
qHBox2.addWidget(&qTglKeepSize);
qVBox.addLayout(&qHBox2);
QLabel qLblImg;
qLblImg.setPixmap(qPixMapOrig);
qLblImg.setAlignment(Qt::AlignCenter);
qVBox.addWidget(&qLblImg, 1);
qWin.setLayout(&qVBox);
qWin.show();
// helper to update pixmap
auto update = [&]() {
cx = qEditCX.text().toInt();
cy = qEditCY.text().toInt();
ra = qEditRA.value();
const bool fitIn = qTglFitIn.isChecked();
const bool keepSize = qTglKeepSize.isChecked();
QPixmap qPixMap = rotate(qPixMapOrig, cx, cy, ra, fitIn, keepSize);
qLblImg.setPixmap(qPixMap);
};
// install signal handlers
QObject::connect(&qEditCX, &QLineEdit::textChanged,
[&](const QString&) { update(); });
QObject::connect(&qEditCY, &QLineEdit::textChanged,
[&](const QString&) { update(); });
QObject::connect(&qEditRA, QOverload<int>::of(&QSpinBox::valueChanged),
[&](int) { update(); });
QObject::connect(&qTglFitIn, &QCheckBox::toggled,
[&](bool) { update(); });
QObject::connect(&qTglKeepSize, &QCheckBox::toggled,
[&](bool) { update(); });
// runtime loop
return app.exec();
}
The Qt project file testQPainterRotateCenter.pro:
SOURCES = testQPainterRotateCenter.cc
QT += widgets
Output:
The rotated image without zoom to fit:
Zoomed to fit:
Zoomed to fit original size:
Notes:
While fiddling originally with a square image of 300×300 pixels, I became aware that rotating a non-square rectangle may result in a bounding box with a different aspect-ratio than the original. Hence, an additional translation might be desirable to align the scaled output in the original bounding box again. I switched to a non-square sample image of 300×200 pixels to illustrate this.
With the fit in calculations, the translations before/after rotation are actually obsolete. The result will be translated in any case to the intended position.
Instead of Qt::gray, the “background color” (i.e. the color the QPixmap is filled with initially) might be set complete transparently. I decided to stick to the Qt::gray for illustration.

Related

drawRect function of Qpainter not giving rectangles of equal dimension

In Qt i have a Qpixmap of 5 px width and 4 px height ,I am filling it with rectangles of width .08 and height .08 but the rectangles formed are not of equal dimension as I have given`.
I want to draw alternate black and white rectangles of equal dimension
QLabel *a=new QLabel();
QPixmap b(5,4);
a->setFixedSize(b.size());
QPainter painter(&b);
heightOfCheckeredBox=.08;
widthOfCheckeredBox=.08;
bool switchBetweenBlackAndWhiteBoxRowDirection=true;
int switchBetweenBlackAndWhiteBoxColumnDirection=0;
for(qreal i=0;i<4;i+=heightOfCheckeredBox)
{
for(qreal j=0;j<5;j+=widthOfCheckeredBox)
{
if(switchBetweenBlackAndWhiteBoxRowDirection)
{
painter.setPen(Qt::white);
painter.setBrush(Qt::white);
switchBetweenBlackAndWhiteBoxRowDirection=false;
}
else
{
painter.setPen(Qt::gray);
painter.setBrush(Qt::gray);
switchBetweenBlackAndWhiteBoxRowDirection=true;
}
QRectF rectangle(j,i,widthOfCheckeredBox,heightOfCheckeredBox);
painter.drawRect(rectangle);
}
switchBetweenBlackAndWhiteBoxColumnDirection++;
if(int(switchBetweenBlackAndWhiteBoxColumnDirection)%2==0)
switchBetweenBlackAndWhiteBoxRowDirection=true;
else
switchBetweenBlackAndWhiteBoxRowDirection=false;
}
a->setPixmap(b);
a->show();
Since you're drawing without antialiasing, the tiny pixel size makes little sense - what did you expect?
The size of the pixmap is given in pixels. Your pixmap contains 5x4 = 20 pixels. You're trying to draw 20 rectangles on it. For this to work, each rectangle must be a square exactly one pixel wide and high. Alas, you're trying to draw squares 12.5x smaller than a pixel. That doesn't make much sense, and I have no idea where you got the value of 0.08 from. Just draw the squares one pixel wide.
Example:
// https://github.com/KubaO/stackoverflown/tree/master/questions/checkered-paint-42205180
#include <QtWidgets>
QPixmap checkers(const QSizeF & rectSize, const QSize & pixmapSize) {
QPixmap pixmap{pixmapSize};
QPainter painter{&pixmap};
painter.setPen(Qt::NoPen);
const QColor colors[] = {Qt::white, Qt::black};
QPointF pos{};
bool oddRow{}, color{};
while (pos.y() < pixmap.height()) {
painter.setBrush(colors[color ? 1 : 0]);
painter.drawRect({pos, rectSize});
color = !color;
pos.setX(pos.x() + rectSize.width());
if (pos.x() >= pixmap.width()) {
pos.setX({});
pos.setY(pos.y() + rectSize.height());
oddRow = !oddRow;
color = oddRow;
}
}
return pixmap;
}
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QLabel label;
auto pix = checkers({1, 1}, {60, 30});
label.resize(200, 200);
label.setAlignment(Qt::AlignCenter);
label.setPixmap(pix);
label.show();
return app.exec();
}

Qt 4.8, Screenshot of large QGraphicsScene

I have QGraphicsScene, which size is 62450x4750. Somethimes I need to make screenshot of whole scene and save it to file. I tried like this:
QPixmap wholeScene(scene.sceneRect().size().toSize());
{
QPainter wholeScenePainter(&wholeScene);
scene.render(&wholeScenePainter);
}
// saving pixmap
or
QPixmap wholeScene(scene.sceneRect().size().toSize());
{
QPainter wholeScenePainter(&wholeScene);
int x = 0;
int portion = 32768; //
while( x < scene.sceneRect().width()) {
int width = scene.sceneRect().width() - x > portion ? portion : scene.sceneRect().width() - x;
QRect rect(x, 0, width, scene.sceneRect().height());
scene.render(&wholeScenePainter, rect, rect);
x += width;
}
}
// saving pixmap
or
QPixmap wholeScene(scene.sceneRect().size().toSize());
{
QPainter wholeScenePainter(&wholeScene);
int x = 0;
int portion = 4096;
while( x < scene.sceneRect().width()) {
int width = scene.sceneRect().width() - x > portion ? portion : scene.sceneRect().width() - x;
QRect rect(x, 0, width, scene.sceneRect().height());
QPixmap temp(rect.size());
QPainter p(&temp);
scene.render(&p, QRect(0, 0, rect.width(), rect.height()), rect);
wholeScenePainter.drawPixmap(x,0, temp);
//temp.save(QString("print%1.png").arg(QString::number(x)), "PNG");
x += width;
}
}
// saving pixmap
But every time I get the Image(Pixmap) cuted by width on 32768 px.
According to the documentation, QPainter does not support coordinates larger than +/- 32768. This does not appear to be fixed in Qt 5 either.
Maybe you can solve this by rendering the scene in multiple passes, e.g. through translation and clipping. You can render the scene in multiple blocks of max 32768x32768 pixels and put them in the proper position in the final image.

Why do I see gaps between pixmap tiles in QGraphicsView when I enable Antialiasing

I am using QGraphicsView framework to display a big image from smaller QPixmap tiles. I also want to enable Antialiasing since the scene will contain line items. Why do I see gaps between the tiles when I enable Antialiasing?
class MainWindow : public QWidget
{
public:
MainWindow(QWidget *parent = 0);
protected:
void resizeEvent(QResizeEvent*);
private:
QGraphicsScene* _scene;
QGraphicsView* _view;
qreal _scale;
static const int _imageWidth = 512;
static const int _imageHeight = 128;
};
MainWindow::MainWindow(QWidget *parent)
: QWidget(parent)
{
_scene = new QGraphicsScene(this);
_view = new QGraphicsView(_scene, this);
//this causes gaps to appear ?
_view->setRenderHints(QPainter::Antialiasing);
_scene->setBackgroundBrush( QBrush( QColor( Qt::lightGray ) ) );
QHBoxLayout *layout = new QHBoxLayout(this);
layout->addWidget(_view);
setWindowTitle(QString("GapsBetweenTiles- QT Version %1")
.arg(QT_VERSION_STR));
QImage img = QImage(_imageWidth, _imageHeight, QImage::Format_RGB32);
img.fill(QColor(00, 50, 50));
int offset = 0;
for (int k=0; k < 10; ++k) {
QGraphicsPixmapItem* pixitem = _scene->addPixmap(
QPixmap::fromImage(img));
pixitem->setTransformationMode(Qt::SmoothTransformation);
pixitem->setPos(0, offset);
offset += _imageHeight;
}
_scale = 1.0;
}
void MainWindow::resizeEvent(QResizeEvent* )
{
int scaledWidth = (qreal)_view->width() -
_view->verticalScrollBar()->width() ;
qreal scale = (qreal)scaledWidth / (qreal)_imageWidth;
qreal scaleMult = scale / _scale;
_view->scale(scaleMult, scaleMult);
_scale = scale;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
The gaps appear when scaled image height (_imageHeight * scale) becomes fractional.
Each QGraphicsPixmapItem is drawn as a separate object. If such object has fractional height the border is smoothed when anti-aliasing enabled (fractional bordering line is partially painted).
There are three possible gap layouts in your case:
no gaps if scaled image height is integer
periodic series of 1 object with gap and 1 without gap if fractional part of height is 0.5
periodic series of 3 objects with gaps and 1 object without gap if fractional part is 0.25 or 0.75; here the 2nd gap is brighter than the 1st and 3rd gaps.
So, if you want perfect object alignment the scaled height should be integer.
In your example the scaled height is integer when scaled width is divisible by 4.
It can be verified by adding into resizeEvent the following line:
scaledWidth = (scaledWidth / 4) * 4;
By the way, you can disable anti-aliasing only for QGraphicsPixmapItem objects by removing the line:
pixitem->setTransformationMode(Qt::SmoothTransformation);

QGraphicsScene & OpenGL Fragment Shader Not Working

I have a very large QGraphicsScene that can contain a very large number of graphics. I'm using a QGLWidget as the viewport so that I can leverage OpenGL to try to improve how some things get rendered. I have created a custom QGraphicsItem that I can use to draw several quads with the same texture in one render call rather than having hundreds or thousands of different QGraphicsItems in the scene that really all get drawn the same way, just in different locations. In my custom QGraphicsItem's paint() method, I called beginNativePainting() and endNativePainting() and do all of my OpenGL calls between them.
I want to use shader programs so that I can manipulate the vertices somewhat within the vertex shader, so I copied Qt's OpenGL Textures Example which uses a shader program to draw 6 textured quads. That example works just fine as is, but when I try to use the same approach within a QGraphicsItem's paint() method, all of my quads just get drawn white. My best guess is that my fragment shader just isn't getting used. I've even tried hardcoding the color within the fragment shader and nothing changes.
Here's the source code of my custom QGraphicsItem class.
class BatchGraphics : public QGraphicsPixmapItem
{
enum {PROGRAM_VERTEX_ATTRIBUTE = 0,
PROGRAM_TEXCOORD_ATTRIBUTE = 1};
public:
BatchGraphics()
: _program(0),
_texture(0),
_dirty(false)
{
}
// Returns the custom bounding rect for this item which encompasses all quads
QRectF boundingRect() const
{
return _boundingRect;
}
// Add a quad to the batch. Only the center point is necessary
void addQuad(int id, float x, float y)
{
_quads.insert(id, QPointF(x, y));
updateBoundingRect();
_dirty = true;
}
// Remove a quad from the batch.
void removeQuad(int id)
{
if (_quads.contains(id))
{
_quads.remove(id);
updateBoundingRect();
_dirty = true;
}
}
// Return the number of quads in the batch
int count() {return _quads.count();}
// Paint the batch using a custom implementation.
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
// If the item is dirty (has been modified, update the geometry)
if (_dirty) {
updateGeometry();
}
if (_program)
{
painter->beginNativePainting();
// Enable GL states
//glEnable(GL_TEXTURE_2D);
// Set the MVP matrix
_program->setUniformValue("matrix", painter->transform());
// Enable and set the vertex and texture attributes
_program->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE);
_program->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE);
_program->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, GL_FLOAT, _vertices.constData(), 3, 5*sizeof(GLfloat));
_program->setAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE, GL_FLOAT, _vertices.constData()+2, 2, 5*sizeof(GLfloat));
// Bind the texture
_texture->bind();
// Draw the arrays
glDrawArrays(GL_TRIANGLES, 0, _quads.count()*6); // 6 vertices per quad
painter->endNativePainting();
}
}
private:
// Initialize the shader and texture
void initialize()
{
// Create the OpenGL texture
_texture = new QOpenGLTexture(pixmap().toImage());
// Vertex Shader
QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex);
const char *vsrc =
"attribute highp vec4 vertex;\n"
"attribute mediump vec4 texCoord;\n"
"varying mediump vec4 texc;\n"
"uniform mediump mat4 matrix;\n"
"void main(void)\n"
"{\n"
" gl_Position = matrix * vertex;\n"
" texc = texCoord;\n"
"}\n";
vshader->compileSourceCode(vsrc);
// Fragment Shader
QOpenGLShader *fshader = new QOpenGLShader(QOpenGLShader::Fragment);
const char *fsrc =
"uniform sampler2D texture;\n"
"varying mediump vec4 texc;\n"
"void main(void)\n"
"{\n"
" gl_FragColor = texture2D(texture, texc.st);\n"
"}\n";
fshader->compileSourceCode(fsrc);
// Program
_program = new QOpenGLShaderProgram;
_program->addShader(vshader);
_program->addShader(fshader);
_program->bindAttributeLocation("vertex", PROGRAM_VERTEX_ATTRIBUTE);
_program->bindAttributeLocation("texCoord", PROGRAM_TEXCOORD_ATTRIBUTE);
_program->link();
_program->bind();
_program->setUniformValue("texture", 0);
}
// Update the vertex array. Calls initialize the first time.
void updateGeometry()
{
if (_program == 0) {
initialize();
}
_vertices.clear();
// Half pixmap size
QPointF s = QPointF(pixmap().width()/2, pixmap().height()/2);
// Build vertex data for each quad
foreach (const QPointF& point, _quads)
{
// Top Left
_vertices << point.x()-s.x(); // x
_vertices << point.y()-s.y(); // y
_vertices << 1; // z
_vertices << 0; // tu
_vertices << 1; // tv
// Top Right
_vertices << point.x()+s.x(); // x
_vertices << point.y()-s.y(); // y
_vertices << 1; // z
_vertices << 1; // tu
_vertices << 1; // tv
// Bottom Left
_vertices << point.x()-s.x(); // x
_vertices << point.y()+s.y(); // y
_vertices << 1; // z
_vertices << 0; // tu
_vertices << 0; // tv
// Top Right
_vertices << point.x()+s.x(); // x
_vertices << point.y()-s.y(); // y
_vertices << 1; // z
_vertices << 1; // tu
_vertices << 1; // tv
// Bottom Left
_vertices << point.x()-s.x(); // x
_vertices << point.y()+s.y(); // y
_vertices << 1; // z
_vertices << 0; // tu
_vertices << 0; // tv
// Bottom Right
_vertices << point.x()+s.x(); // x
_vertices << point.y()+s.y(); // y
_vertices << 1; // z
_vertices << 1; // tu
_vertices << 0; // tv
}
_dirty = false;
}
private:
// Updates the bounding rect based on the quads in the batch.
void updateBoundingRect()
{
prepareGeometryChange();
double left = 9999;
double right = -9999;
double top = 9999;
double bottom = -9999;
double w = pixmap().width()/2;
double h = pixmap().width()/2;
foreach (const QPointF& p, _quads)
{
left = qMin(left, p.x()-w);
right = qMax(right, p.x()+w);
top = qMin(top, p.y()-h);
bottom = qMax(bottom, p.y()+h);
}
_boundingRect = QRectF(left, top, (right-left), (bottom-top));
}
private:
QOpenGLShaderProgram* _program;
QOpenGLTexture* _texture;
QRectF _boundingRect;
QMap<int, QPointF> _quads;
QVector<GLfloat> _vertices;
bool _dirty;
};
I understand the basics of the render pipeline and how to use shaders, but as far as dependencies between things and other OpenGL methods that must be called when using certain features I'm pretty clueless on. I can get the quads to be rendered with the texture using a fixed function pipeline approach, but that's old school and like I said, I want to be able to manipulate the vertices in the vertex shader once I get this working.
I'm not doing anything special when creating the QGLWidget, and its QGLFormat ends up being 2.0. I'v also tried calling glEnable(GL_TEXTURE_2D), but that just makes the quads get rendered black instead of white. I've also tried binding the program each time paint() is called, thinking perhaps Qt is binding a different shader somewhere else behind the scenes, but that just causes NOTHING to appear.
Can anyone provide any help please? I can't figure out why this approach works fine in Qt's Textures example but not when I try to do it inside of a QGraphicsItem.
I finally figured it out after looking at Qt's source code and what happens when beginNativePainting(). First, I DID have to bind my shader each time paint() was called, and second I had to get the correct MVP matrix.
I was trying to just pass the QPainter's transform to my shader to act as the modelview projection matrix, but the transform was only the modelview matrix. I needed to get the projection matrix as well, which Qt sets when beginNativePainting() is called.
I got the project and modelview matrices from OpenGL directly and combined them to pass to my shader after binding my texture and presto! It worked!
Here are the relevant changes I had to make:
painter->beginNativePainting();
// Enable GL states
//glEnable(GL_TEXTURE_2D);
// === Begin New Code ======
// Bind my program
_program->bind();
QMatrix4x4 proj;
glGetFloatv(GL_PROJECTION_MATRIX, proj.data());
QMatrix4x4 model;
glGetFloatv(GL_MODELVIEW_MATRIX, model.data());
// Set the MVP matrix
_program->setUniformValue("matrix", proj * model);
// === End New Code ======
// Enable and set the vertex and texture attributes
_program->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE);
_program->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE);
_program->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, GL_FLOAT, _vertices.constData(), 3, 5*sizeof(GLfloat));
_program->setAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE, GL_FLOAT, _vertices.constData()+2, 2, 5*sizeof(GLfloat));

Finding the point of intersection between a line and a QPainterPath

I'm trying to determine the point where a hitscan projectile's path (basically a line, but I've represented it as a QPainterPath in my example) intersects with an item in my scene. I am not sure if there is a way to find this point using the functions provided by QPainterPath, QLineF, etc. The code below illustrates what I'm trying to do:
#include <QtWidgets>
bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
if (!itemsInPath.isEmpty()) {
const QPointF projectileStartPos = projectilePath.elementAt(0);
float shortestDistance = std::numeric_limits<float>::max();
QGraphicsItem *closest = 0;
foreach (QGraphicsItem *item, itemsInPath) {
QPointF distanceAsPoint = item->pos() - projectileStartPos;
float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
if (distance < shortestDistance) {
shortestDistance = distance;
closest = item;
}
}
QPainterPath targetShape = closest->mapToScene(closest->shape());
// hitPos = /* the point at which projectilePath hits targetShape */
hitPos = closest->pos(); // incorrect; always gives top left
qDebug() << projectilePath.intersects(targetShape); // true
qDebug() << projectilePath.intersected(targetShape); // QPainterPath: Element count=0
// To show that they do actually intersect..
QPen p1(Qt::green);
p1.setWidth(2);
QPen p2(Qt::blue);
p2.setWidth(2);
scene->addPath(projectilePath, p1);
scene->addPath(targetShape, p2);
return true;
}
return false;
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
view.setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
target->setTransformOriginPoint(QPointF(12.5, 12.5));
target->setRotation(35);
target->setPos(100, 100);
QPainterPath projectilePath;
projectilePath.moveTo(200, 200);
projectilePath.lineTo(0, 0);
projectilePath.lineTo(200, 200);
QPointF hitPos;
if (hit(projectilePath, scene, hitPos)) {
scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
}
scene->addPath(projectilePath, QPen(Qt::DashLine));
scene->addText("start")->setPos(180, 150);
scene->addText("end")->setPos(20, 0);
view.show();
return app.exec();
}
projectilePath.intersects(targetShape) returns true, but projectilePath.intersected(targetShape) returns an empty path.
Is there a way to achieve this?
As the answer to Intersection point of QPainterPath and line (find QPainterPath y by x) points out, QPainterPath::intersected() only accounts for paths which have fill areas. The rectangular path trick which is also mentioned there can be implemented like this:
#include <QtWidgets>
/*!
Returns the closest element (position) in \a sourcePath to \a target,
using \l{QPoint::manhattanLength()} to determine the distances.
*/
QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
Q_ASSERT(!sourcePath.isEmpty());
QPointF shortestDistance = sourcePath.elementAt(0) - target;
qreal shortestLength = shortestDistance.manhattanLength();
for (int i = 1; i < sourcePath.elementCount(); ++i) {
const QPointF distance(sourcePath.elementAt(i) - target);
const qreal length = distance.manhattanLength();
if (length < shortestLength) {
shortestDistance = sourcePath.elementAt(i);
shortestLength = length;
}
}
return shortestDistance;
}
/*!
Returns \c true if \a projectilePath intersects with any items in \a scene,
setting \a hitPos to the position of the intersection.
*/
bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
if (!itemsInPath.isEmpty()) {
const QPointF projectileStartPos = projectilePath.elementAt(0);
float shortestDistance = std::numeric_limits<float>::max();
QGraphicsItem *closest = 0;
foreach (QGraphicsItem *item, itemsInPath) {
QPointF distanceAsPoint = item->pos() - projectileStartPos;
float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
if (distance < shortestDistance) {
shortestDistance = distance;
closest = item;
}
}
QPainterPath targetShape = closest->mapToScene(closest->shape());
// QLineF has normalVector(), which is useful for extending our path to a rectangle.
// The path needs to be a rectangle, as QPainterPath::intersected() only accounts
// for intersections between fill areas, which projectilePath doesn't have.
QLineF pathAsLine(projectileStartPos, projectilePath.elementAt(1));
// Extend the first point in the path out by 1 pixel.
QLineF startEdge = pathAsLine.normalVector();
startEdge.setLength(1);
// Swap the points in the line so the normal vector is at the other end of the line.
pathAsLine.setPoints(pathAsLine.p2(), pathAsLine.p1());
QLineF endEdge = pathAsLine.normalVector();
// The end point is currently pointing the wrong way; move it to face the same
// direction as startEdge.
endEdge.setLength(-1);
// Now we can create a rectangle from our edges.
QPainterPath rectPath(startEdge.p1());
rectPath.lineTo(startEdge.p2());
rectPath.lineTo(endEdge.p2());
rectPath.lineTo(endEdge.p1());
rectPath.lineTo(startEdge.p1());
// Visualize the rectangle that we created.
scene->addPath(rectPath, QPen(QBrush(Qt::blue), 2));
// Visualize the intersection of the rectangle with the item.
scene->addPath(targetShape.intersected(rectPath), QPen(QBrush(Qt::cyan), 2));
// The hit position will be the element (point) of the rectangle that is the
// closest to where the projectile was fired from.
hitPos = closestPointTo(projectileStartPos, targetShape.intersected(rectPath));
return true;
}
return false;
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
target->setTransformOriginPoint(QPointF(12.5, 12.5));
target->setRotation(35);
target->setPos(100, 100);
QPainterPath projectilePath;
projectilePath.moveTo(200, 200);
projectilePath.lineTo(0, 0);
projectilePath.lineTo(200, 200);
QPointF hitPos;
if (hit(projectilePath, scene, hitPos)) {
scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
}
scene->addPath(projectilePath, QPen(Qt::DashLine));
scene->addText("start")->setPos(180, 150);
scene->addText("end")->setPos(20, 0);
view.show();
return app.exec();
}
This has pretty good precision (± 1 pixel, since QLineF::length() is an integer), but there might be a neater way to achieve the same thing.
Just for the record (and if someone else steps here). The above answer is excellent. There's just a little bug in the closestPoint function that may happens if the first point is already the closest one. It should return elementAt(0) instead of elementAt(0) - target.
Here is the fixed function:
QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
Q_ASSERT(!sourcePath.isEmpty());
QPointF shortestDistance;
qreal shortestLength = std::numeric_limits<int>::max();
for (int i = 0; i < sourcePath.elementCount(); ++i) {
const QPointF distance(sourcePath.elementAt(i) - target);
const qreal length = distance.manhattanLength();
if (length < shortestLength) {
shortestDistance = sourcePath.elementAt(i);
shortestLength = length;
}
}
return shortestDistance;
}

Resources