I am coding a small map editor (with rectangle tiles) and I need a way to draw a large amount of images OR one big image. The application is simple: You draw images on an empty screen with your mouse and when you are finished you can save it. A tile consists of a small image.
I tried out several solutions to display the tiles:
Each tile has its own QGraphicsItem (This works until you have a
1000x1000 map)
Each tile gets drawn on one big QPixmap (This means a very large image. Example: Map with 1000x100 and each tile has a size of 32x32 means that the QPixmap has a size of 32000x32000. This is a problem for QPainter.)
The current solution: Iterate through width & height of the TileLayer and draw each single tile with painter->drawPixmap(). The paint() method of my TileLayer looks like this:
void TileLayerGraphicsItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option,QWidget* /*widget*/)
{
painter->setClipRect(option->exposedRect);
int m_width=m_layer->getSize().width();
int m_height=m_layer->getSize().height();
for(int i=0;i<m_width;i++)
{
for(int j=0;j<(m_height);j++)
{
Tile* thetile=m_layer->getTile(i,j);
if(thetile==NULL)continue;
const QRectF target(thetile->getLayerPos().x()*thetile->getSize().width(),thetile->getLayerPos().y()*thetile->getSize().height(),thetile->getSize().width(),thetile->getSize().height());
const QRectF source(0, 0, thetile->getSize().width(), thetile->getSize().height());
painter->drawImage(target,*thetile->getImage(),source);
}
}}
This works for small maps with 100x100 or even 1000x100 tiles. But not for 1000x1000. The whole application begins to lag, this is of course because I have a for loop that is extremely expensive. To make my tool useful I need to be able to make at least 1000x1000 tilemaps without lags. Does anyone have an idea what I can do? How should I represent the tiles?
Update:
I changed the following: Only maps that exceed the window size of the minimap will be drawn with drawing single pixels for each tile. This is my render function now:
void RectangleRenderer::renderMinimapImage(QPainter* painter, TileMap* map,QSize windowSize)
{
for(int i=0;i<map->getLayers().size();i++)
{
TileLayer* currLayer=map->getLayers().at(i);
//if the layer is small draw it completly
if(windowSize.width()>currLayer->getSize().width()&&windowSize.height()>currLayer->getSize().height())
{
...
}
else // This is the part where the map is so big that only some pixels are drawn!
{
painter->fillRect(0,0,windowSize.width(),windowSize.height(),QBrush(QColor(map->MapColor)));
for(float i=0;i<windowSize.width();i++)
for(float j=0;j<windowSize.height();j++)
{
float tX=i/windowSize.width();
float tY=j/windowSize.height();
float pX=lerp(i,currLayer->getSize().width(),tX);
float pY=lerp(j,currLayer->getSize().height(),tY);
Tile* thetile=currLayer->getTile((int)pX,(int)pY);
if(thetile==NULL)continue;
QRgb pixelcolor=thetile->getImage()->toImage().pixel(thetile->getSize().width()/2,thetile->getSize().height()/2);
QPen pen;
pen.setColor(QColor::fromRgb(pixelcolor));
painter->setPen(pen);
painter->drawPoint(i,j);
}
}
}
}
This works not correct, however it is pretty fast. The problem is my lerp(linear interpolation) function to get the correct tiles to draw a pixel from.
Does anyone have a better solution to get the correct tiles while I iterate through the minimap pixels? At the moment I use linear interpolation between 0 and the maximum size of the tilemap and it does not work correctly.
UPDATE 2
//currLayer->getSize() returns how many tiles are in the map
// currLayer->getTileSize() returns how big each tile is (32 pixels width for example)
int raw_width = currLayer->getSize().width()*currLayer->getTileSize().width();
int raw_height = currLayer->getSize().height()*currLayer->getTileSize().height();
int desired_width = windowSize.width();
int desired_height = windowSize.height();
int calculated_width = 0;
int calculated_height = 0;
// if dealing with a one dimensional image buffer, this ensures
// the rows come out clean, and you don't lose a pixel occasionally
desired_width -= desired_width%2;
// http://qt-project.org/doc/qt-5/qt.html#AspectRatioMode-enum
// Qt::KeepAspectRatio, and the offset can be used for centering
qreal ratio_x = (qreal)desired_width / raw_width;
qreal ratio_y = (qreal)desired_height / raw_height;
qreal floating_factor = 1;
QPointF offset;
if(ratio_x < ratio_y)
{
floating_factor = ratio_x;
calculated_height = raw_height*ratio_x;
calculated_width = desired_width;
offset = QPointF(0, (qreal)(desired_height - calculated_height)/2);
}
else
{
floating_factor = ratio_y;
calculated_width = raw_width*ratio_y;
calculated_height = desired_height;
offset = QPointF((qreal)(desired_width - calculated_width)/2,0);
}
for (int r = 0; r < calculated_height; r++)
{
for (int c = 0; c < calculated_width; c++)
{
//trying to do the following: use your code to get the desired pixel. Then divide that number by the size of the tile to get the correct pixel
Tile* thetile=currLayer->getTile((int)((r * floating_factor)*raw_width)/currLayer->getTileSize().width(),(int)(((c * floating_factor)*raw_height)/currLayer->getTileSize().height()));
if(thetile==NULL)continue;
QRgb pixelcolor=thetile->getImage()->toImage().pixel(thetile->getSize().width()/2,thetile->getSize().height()/2);
QPen pen;
pen.setColor(QColor::fromRgb(pixelcolor));
painter->setPen(pen);
painter->drawPoint(r,c);
}
}
Trying to reverse engineer the example code, but it still does not work correctly.
Update 3
I tried (update 1) with linear interpolation again. And while I looked at the code I saw the error:
float pX=lerp(i,currLayer->getSize().width(),tX);
float pY=lerp(j,currLayer->getSize().height(),tY);
should be:
float pX=lerp(0,currLayer->getSize().width(),tX);
float pY=lerp(0,currLayer->getSize().height(),tY);
That's it. Now it works.
This shows how to do it properly. You use a level of detail (lod) variable to determine how to draw the elements that are currently visible on the screen, based on their zoom.
http://qt-project.org/doc/qt-5/qtwidgets-graphicsview-chip-example.html
Also don't iterate through all the elements that could be visible, but only go through the ones that have changed, and of those, only the ones that are currently visible.
Your next option to use is some other manual caching, so you don't have to repeatedly iterate through O(n^2) constantly.
If you can't optimize it for QGraphicsView/QGraphicsScene... then OpenGL is probably what you may want to look into. It can do a lot of the drawing and caching directly on the graphics card so you don't have to worry about it as much.
UPDATE:
Pushing changes to QImage on a worker thread can let you cache, and update a cache, while leaving the rest of your program responsive, and then you use a Queued connection to get back on the GUI thread to draw the QImage as a Pixmap.
QGraphicsView will let you know which tiles are visible if you ask nicely:
http://qt-project.org/doc/qt-5/qgraphicsview.html#items-5
UPDATE 2:
http://qt-project.org/doc/qt-5/qtwidgets-graphicsview-chip-chip-cpp.html
You may need to adjust the range of zooming out that is allowed on the project to test this feature...
Under where it has
const qreal lod = option->levelOfDetailFromTransform(painter->worldTransform());
if (lod < 0.2) {
if (lod < 0.125) {
painter->fillRect(QRectF(0, 0, 110, 70), fillColor);
return;
}
QBrush b = painter->brush();
painter->setBrush(fillColor);
painter->drawRect(13, 13, 97, 57);
painter->setBrush(b);
return;
}
Add in something like:
if(lod < 0.05)
{
// using some sort of row/col value to know which ones to not draw...
// This below would only draw 1/3 of the rows and 1/3 of the column
// speeding up the redraw by about 9x.
if(row%3 != 0 || col%3 != 0)
return;// don't do any painting, return
}
UPDATE 3:
Decimation Example:
// How to decimate an image to any size, properly
// aka fast scaling
int raw_width = 1000;
int raw_height = 1000;
int desired_width = 300;
int desired_height = 200;
int calculated_width = 0;
int calculated_height = 0;
// if dealing with a one dimensional image buffer, this ensures
// the rows come out clean, and you don't lose a pixel occasionally
desired_width -= desired_width%2;
// http://qt-project.org/doc/qt-5/qt.html#AspectRatioMode-enum
// Qt::KeepAspectRatio, and the offset can be used for centering
qreal ratio_x = (qreal)desired_width / raw_width();
qreal ratio_y = (qreal)desired_height / raw_height();
qreal floating_factor = 1;
QPointF offset;
if(ratio_x < ratio_y)
{
floating_factor = ratio_x;
calculated_height = raw_height*ratio_x;
calculated_width = desired_width;
offset = QPointF(0, (qreal)(desired_height - calculated_height)/2);
}
else
{
floating_factor = ratio_y;
calculated_width = raw_width*ratio_y;
calculated_height = desired_height;
offset = QPointF((qreal)(desired_width - calculated_width)/2);
}
for (int r = 0; r < calculated_height; r++)
{
for (int c = 0; c < calculated_width; c++)
{
pixel[r][c] = raw_pixel[(int)(r * floating_factor)*raw_width][(int)(c * floating_factor)];
}
}
Hope that helps.
Related
I'm trying to draw a 3-pixel large point with QPainter. But the following code instead draws a horizontal line with width of 3 pixels.
#include <QPainter>
#include <QImage>
int main()
{
const int w=1000, h=1000;
QImage img(w, h, QImage::Format_RGBX8888);
{
QPainter p(&img);
p.fillRect(0,0,w,h,Qt::black);
p.scale(w,h);
p.setPen(QPen(Qt::red, 3./w, Qt::SolidLine, Qt::RoundCap));
p.drawPoint(QPointF(0.1,0.1));
}
img.save("test.png");
}
Here's the top left corner of the resulting image:
I am expecting to get a point which is red circle, or at least a square — but instead I get this line segment. If I comment out p.scale(w,h) and draw the point with width 3 (instead of 3./w) at position (100,100), then I get a small mostly symmetric 3-pixel in height and width point.
What's going on? Why do I get a line segment instead of point as expected? And how to fix it, without resorting to drawing an ellipse or to avoiding QPainter::scale?
I'm using Qt 5.10.0 on Linux x86 with g++ 5.5.0. The same happens on Qt 5.5.1.
It appears that QPaintEngineEx::drawPoints renders points as line segments of length 1/63.. See the following code from qtbase/src/gui/painting/qpaintengineex.cpp in the Qt sources:
void QPaintEngineEx::drawPoints(const QPointF *points, int pointCount)
{
QPen pen = state()->pen;
if (pen.capStyle() == Qt::FlatCap)
pen.setCapStyle(Qt::SquareCap);
if (pen.brush().isOpaque()) {
while (pointCount > 0) {
int count = qMin(pointCount, 16);
qreal pts[64];
int oset = -1;
for (int i=0; i<count; ++i) {
pts[++oset] = points[i].x();
pts[++oset] = points[i].y();
pts[++oset] = points[i].x() + 1/63.;
pts[++oset] = points[i].y();
}
QVectorPath path(pts, count * 2, qpaintengineex_line_types_16, QVectorPath::LinesHint);
stroke(path, pen);
pointCount -= 16;
points += 16;
}
} else {
for (int i=0; i<pointCount; ++i) {
qreal pts[] = { points[i].x(), points[i].y(), points[i].x() + qreal(1/63.), points[i].y() };
QVectorPath path(pts, 2, 0);
stroke(path, pen);
}
}
}
Notice the pts[++oset] = points[i].x() + 1/63.; line in the opaque brush branch. This is the second vertex of the path — shifted with respect to the desired position of the point.
This explains why the line extends to the right of the position requested and why it depends on the scale. So, it seems the code in the OP isn't wrong for an ideal QPainter implementation, but just has come across a Qt bug (be it in the implementation of the method or in its documentation).
So the conclusion: one has to work around this problem by either using different scale, or drawing ellipses, or drawing line segments with much smaller lengths than what QPainter::drawPoints does.
I've reported this as QTBUG-70409.
Though I have not been able to pin point why exactly the problem occurs, I have got relatively close to the solution. The problem lies with scaling. I did a lots of trial and error with different scaling and width of point with the below code.
const int w=500, h=500;
const int scale = 100;
float xPos = 250;
float yPos = 250;
float widthF = 5;
QImage img(w, h, QImage::Format_RGBX8888);
{
QPainter p(&img);
p.setRenderHints(QPainter::Antialiasing);
p.fillRect(0,0,w,h,Qt::black);
p.scale(scale, scale);
p.setPen(QPen(Qt::red, widthF/(scale), Qt::SolidLine, Qt::RoundCap));
p.drawPoint(QPointF(xPos/scale, yPos/scale));
}
img.save("test.png");
The above code produces the image
My observations are
1) Due to high scaling, the point (which is just 3 pixel wide) is not able to scale proportionally at lower width (width of point), if you set width as something like 30 the round shape is visible.
2) If you want to keep the width of point low then you have to decrease the scaling.
Sadly I can not explain why at high scaling it is not expanding proportionally.
I have a high energy physics application that displays particle detectors. I draw hits in the detectors as points. But the points in basic jogl/opengl seem to be just squares. So what I would like to do is draw small images (pngs) at the points. So the basic problem is: draw images from png files (resources) at points.
I start by loading the image from a jar file via
_sprite = TextureIO.newTexture(_url, false, ".png");
this seems to work fine. For instance, the sprite reports the correct image size.Then I try to draw the (same sprite) at a set of points using
public static void drawSprites(GLAutoDrawable drawable, float coords[],
Texture sprite, float size) {
GL2 gl = drawable.getGL().getGL2();
gl.glPointSize(size);
sprite.bind(gl);
//how many points?
int np = coords.length / 3;
gl.glBegin(GL.GL_POINTS);
for (int i = 0; i < np; i++) {
int j = i * 3;
gl.glVertex3f(coords[j], coords[j + 1], coords[j + 2]);
}
gl.glEnd();
}
But all I get are the points-- i.e. squares. The squares are at the right location--but they are just squares. No images.
I am making a small game in C++11 with Qt. However, I am having some issues with scaling.
The background of my map is an image. Each pixel of that image represents a tile, on which a protagonist can walk and enemies/healthpacks can be.
To set the size of a tile, I calculat the maximum amount like so (where imageRows & imageCols is amount of pixels on x- and y-axis of the background image):
QRect rec = QApplication::desktop()->screenGeometry();
int maxRows = rec.height() / imageRows;
int maxCols = rec.width() / imageCols;
if(maxRows < maxCols){
pixSize = maxRows;
} else{
pixSize = maxCols;
}
Now that I have the size of a tile, I add the background-image to the scene (in GameScene ctor, extends from QGraphicsScene):
auto background = new QGraphicsPixmapItem();
background->setPixmap(QPixmap(":/images/map.png").scaledToWidth(imageCols * pixSize));
this->addItem(background);
Then for adding enemies (they extend from a QGraphicsPixMapItem):
Enemy *enemy = new Enemy();
enemy->setPixmap(QPixmap(":/images/enemy.png").scaledToWidth(pixSize));
scene->addItem(enemy);
This all works fine, except that on large maps images get scaled once (to a height of lets say 2 pixels), and when zooming in on that item it does not get more clear, but stays a big pixel. Here is an example: the left one is on a small map where pixSize is pretty big, the second one has a pixSize of pretty small.
So how should I solve this? In general having a pixSize based on the screen resolution is not really useful, since the QGrapicsScene is resized to fit the QGraphicsView it is in, so in the end the view still determines how big the pixels show on the screen.
MyGraphicsView w;
w.setScene(gameScene);
w.fitInView(gameScene->sceneRect(), Qt::KeepAspectRatio);
I think you might want to look at the chip example from Qt (link to Qt5 but also works for Qt4).
The thing that might help you is in the chip.cpp file:
in the paint method:
const qreal lod = option->levelOfDetailFromTransform(painter->worldTransform());
where painter is simply a QPainter and option is of type QStyleOptionGraphicsItem. This quantity gives you back a measure of the current zoom level of your QGraphicsView and thus as in the example you can adjust what is being drawn at which level, e.g.
if (lod < 0.2) {
if (lod < 0.125) {
painter->fillRect(QRectF(0, 0, 110, 70), fillColor);
return;
}
QBrush b = painter->brush();
painter->setBrush(fillColor);
painter->drawRect(13, 13, 97, 57);
painter->setBrush(b);
return;
}
[...]
if (lod >= 2) {
QFont font("Times", 10);
font.setStyleStrategy(QFont::ForceOutline);
painter->setFont(font);
painter->save();
painter->scale(0.1, 0.1);
painter->drawText(170, 180, QString("Model: VSC-2000 (Very Small Chip) at %1x%2").arg(x).arg(y));
painter->drawText(170, 200, QString("Serial number: DLWR-WEER-123L-ZZ33-SDSJ"));
painter->drawText(170, 220, QString("Manufacturer: Chip Manufacturer"));
painter->restore();
}
Does this help?
Well, my question is simple, I'm starting with cocos2d-x programming and I'm trying to make a tiled infinite background, lets say I have a png of an image I want to repeat infinitely as a background, What I did was that I created a Sprite as a container, then I added sprites with the same image, one aside the next, covering about 110% the screen, then, following the Space game tutorial I created the same sprite of sprites once again as a second background and I use it as two large identical images to scroll infinite... it works perfect... but I'm wondering if it is possible to create the second sprite by just copying the frist. Some code maybe will clear my situation:
this goes in the init():
acum = 0.0;
city1 = CCSprite::create();
for(int i = 0; i < 12; ++i) {
CCSprite *city = CCSprite::createWithSpriteFrameName("env_buildings_background2.png");
city->setPosition(ccp(acum, 0));
city1->addChild(city);
acum+= city->getContentSize().width*0.99;
}
city1->setContentSize(ccp(acum, 0));
city2 = CCSprite::create();
acum = 0.0;
for(int i = 0; i < 12; ++i) {
CCSprite *city = CCSprite::createWithSpriteFrameName("env_buildings_background2.png");
city->setPosition(ccp(acum, 0));
city2->addChild(city);
acum+= city->getContentSize().width*0.99;
}
city2->setContentSize(ccp(acum, 0));
_backgroundNode->addChild(city1, -1 , buildingspeed, ccp(0, winSize.height * 0.6));
_backgroundNode->addChild(city2, -1 , buildingspeed, ccp(city1->getContentSize().width, winSize.height * 0.6));
printf("%f - %f\n", city1->getContentSize().width, city2->getContentSize().width);
The main problem is here, where I need to create city2 from city1 and not just repeat code... is there a way to do this? I don't see a constructor in CCSprite that allows me to do so...
this goes in the update():
CCArray *cities = CCArray::createWithCapacity(2);
cities->addObject(city1);
cities->addObject(city2);
for ( int ii = 0; ii <cities->count(); ii++ ) {
CCSprite * city = (CCSprite *)(cities->objectAtIndex(ii));
float xPosition = _backgroundNode->convertToWorldSpace(city->getPosition()).x;
float size = city->getContentSize().width;
if ( xPosition < -size ) {
_backgroundNode->incrementOffset(ccp(city->getContentSize().width*2,0),city);
}
}
I will appreciate any help, thanks in advance.
This code will create the exact snapshot of your citySprite. And as this is a snapshot, you'll be having no access to the sprites that it contains. Cause they are now embedded in the image or sprite itself.
float citySpriteWidth = 480;
float citySpriteHeight = 320;
//Set position in order to make it fit inside CCRenderTexture (You can change this later)
citySprite->setPosition(ccp(citySpriteWidth/2, citySpriteHeight/2));
CCRenderTexture *render = CCRenderTexture::renderTextureWithWidthAndHeight(citySpriteWidth, citySpriteWidth);
render->beginWithClear(0, 0, 0, 0);
citySprite->visit();
render->end();
CCTexture2D *tex = render->getSprite()->getTexture();
CCSprite *newCitySprite = CCSprite::spriteWithTexture(tex);
newCitySprite->setFlipY(true); //Texture might be upside down
Hope this helps your requirements.
Else if your problem is just code repetition. Then you can write a function which would give you your city sprite containing other sprites like this one. And call it number of times you want it.
CCSprite* creteCitySprite()
{
float acum = 0.0;
CCSprite *city1 = CCSprite::create();
for(int i = 0; i < 12; ++i)
{
CCSprite *city = CCSprite::createWithSpriteFrameName("env_buildings_background2.png");
city->setPosition(ccp(acum, 0));
city1->addChild(city);
acum+= city->getContentSize().width*0.99;
}
city1->setContentSize(CCSizeMake(acum, 0));
return city1;
}
EDITED:
This gives you what you want. But what I wanted to say is, the sub sprites which you added to your city1 will remain static.
CCSptire *getSpriteFromSprite(CCSprite *citySprite, float citySpriteWidth, float citySpriteHeight)
{
CCPoint prevPosition = citySprite->getPosition();
//Set position in order to make it fit inside CCRenderTexture (You can change this later)
citySprite->setPosition(ccp(citySpriteWidth/2, citySpriteHeight/2));
CCRenderTexture *render = CCRenderTexture::renderTextureWithWidthAndHeight(citySpriteWidth, citySpriteWidth);
render->beginWithClear(0, 0, 0, 0);
citySprite->visit();
render->end();
citySprite->setPosition(prevPosition);
CCTexture2D *tex = render->getSprite()->getTexture();
CCSprite *newCitySprite = CCSprite::spriteWithTexture(tex);
newCitySprite->setFlipY(true); //Texture might be upside down
}
I've written some code that should make a new image. My background image has black areas, when the for loop comes on a black pixel then it should draw in the new image a blue one instead otherwise it should just draw the original pixel. Thought I could do that like this but the program keeps running.
QApplication a(argc, argv);
int c, m, y, k, al;
QColor color;
QColor drawColor;
QImage background;
QImage world(1500, 768, QImage::Format_RGB32);
QSize sizeImage;
int height, width;
background.load("Background.jpg");
world.fill(1);
QPainter painter(&background);
sizeImage = background.size();
width = sizeImage.width();
height = sizeImage.height();
for(int i = 0; i < height; i++)
{
for(int z = 0; z < width; z++)
{
color = QColor::fromRgb (background.pixel(i,z) );
color.getCmyk(&c,&m,&y,&k,&al);
if(c == 0 && m == 0 && y == 0 && k == 0) //then we have black as color and then we draw the color blue
{
drawColor.setBlue(255);
painter.setPen(drawColor);
painter.drawPoint(i,z);
}
}
}
//adding new image to the graphicsScene
QGraphicsPixmapItem item( QPixmap::fromImage(background));
QGraphicsScene* scene = new QGraphicsScene;
scene->addItem(&item);
QGraphicsView view(scene);
view.show();
Is my for loop wrong or is it my painter? It sais QImage::pixel: coordinate (292,981) out of range but for soo many pixels, it is also not fast enough to use.
As noted in the comments, drawing pixels one by one can be incredibly slow. Even pixel-by-pixel access can be quite slow. E.g. the following is probably faster, but still not very good:
const QRgb black = 0;
const QRgb blue = 255;
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
if (background.pixel(x,y) == black) {
background.SetPixel(blue);
}
}
}
The quicker solution involves direct bitoperations via scanline(). You might want to call convertToFormat() first, so you don't need to deal with the different possible scanline formats.
As a creative hack, call createMaskFromColor to make all black pixels transparent, and then paint over a blue background.