Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WangTiles: Better filling methods, wang brush #1649

Merged
merged 16 commits into from
Jul 31, 2017

Conversation

Bdtrotte
Copy link
Contributor

A work in progress ppull request for the GSOC Wang Tiles project:

In this PR I will commit, first improvements to how wang filling and brushing is done. Next will be a major project to add in a new tool, the wang brush. This tool will function very closely to how the current terrain brush works.

Along with this there are sure to be a host of improvements to the general architecture of wangTiles.

Copy link
Member

@bjorn bjorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, please push the updated testing files (with the wang Ids in hex format)!

Some feedback on the code below.

@@ -133,7 +133,7 @@ TileLayer *WangFiller::fillRegion(const TileLayer &back,
if (lookForward) {
for (int i = 0; i < 8; ++i) {
QPoint p = currentPoint + adjacentPoints[i];
if (!fillRegion.contains(p))
if (!fillRegion.contains(p) || !tileLayer->cellAt(p - QPoint(tileLayer->x(), tileLayer->y())).isEmpty())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QPoint(tileLayer->x(), tileLayer->y()) is tileLayer->position()

if (wangTiles.isEmpty())
fill = true;
else
fill = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be:

fill = wangTiles.isEmpty();

if (index & 1)
return cornerColor(index / 2);
else
return edgeColor(index / 2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need indexColor, I guess it will make more sense to implement cornerColor and edgeColor using indexColor, rather than the other way around.

@@ -134,6 +187,71 @@ void WangId::flipVertically()
rotate(2);
}

WangIdVariationIterator::WangIdVariationIterator(int edgeColors, int cornerColors, WangId wangId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting approach, but I think it only becomes really useful if it can be used to avoid the allocation of the QVector returned from WangId::variations. For this, WangId::variations would need to return a WangIdVariations instance which provides begin and end members returning this iterator. In addition, the iterator needs to be changed such that it is not valid to dereference the one returned by end. Then, you can write this and it will not allocate the actual list of variations:

for (WangId id : wangId.variations(mEdgeColors, mCornerColors))

As currently implemented I rather fail to see the point of using an iterator.

That said, I still worry that there is a performance issue here when the color count goes above about 4, even when we avoid the allocation of the vector.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of it is to make "WildWangIdUsed" a lot faster. That function is pretty important, so Im still working on speeding it up more.

In regards to the iterator itself, I know it isn't proper with end being valid, but the problem is in the 15 x 15 case, all unsigned ints are valid, so I'm not sure there is a way to make it invalid to dereference. So I'm not sure I can make it proper? Though I see your point it would be nice to avoid the allocation... Though currently, the variations function is unused (it is in the findMatchingWangTiles function, but that can be changed to use the iterator as is), so It could be removed all together. Ill work on a more elegant solution though,

Copy link
Contributor Author

@Bdtrotte Bdtrotte Jul 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the solution I've put together implements a proper iterator to replace to old messy one. It works for the end case by using the biggest wangId for given input + 1 to the underlying id. This works great for all sets, other than those 15x15, where this value will be zero, and immediately fail. As a work around, everywhere this iterator is used must be wrapped with an if testing if it is the 15x15 case, and if so takes on slightly different behavior.

Of course if this is actually run, that would mean 15^8 loops, which may not be ideal...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, it will be better for it to fail immediately, so maybe you don't need that if...


unsigned WangSet::completeSetSize() const
{
return qPow(mEdgeColors, 4) * qPow(mCornerColors, 4);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this would be faster when writing like the little more verbose:

    return mEdgeColors * mEdgeColors * mEdgeColors * mEdgeColors *
           mCornerColors * mCornerColors * mCornerColors * mCornerColors;

I think it makes little sense to return unsigned rather than int, btw. We can't go out of int range here anyway due to limitations on the color counts, right? So all it does it add a need for casting int to unsigned elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't go out of int range here anyway due to limitations on the color counts, right?

I think we can. Max color is 15, so max return is 15^8 = 2562890625 > 0x7fffffff

To avoid casting, I the new function for unique keys will return unsigned as well.

{
if (isEmpty())
return false;
return (unsigned) mWangIdToWangTile.uniqueKeys().size() == completeSetSize();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this check happens often (looks like it will, because it is passed to findFittingCell), it would make sense to cache this information and calculate it when wang Ids are changing. Also, the mWangIdToWangTile.uniqueKeys().size() would be faster when implemented like:

template <class Key, class T>
static int uniqueKeyCount(const QHash<Key, T> &hash) const
{
    int count = 0;
    
    auto i = hash.begin();
    auto i_end = hash.end();
    
    if (i != i_end) {
        for (;;) {
            ++count;
            const Key &key = i.key();
            do {
                if (++i == i_end)
                    return count;
            } while (key == i.key());
        }
    }
    
    return count;
}

bool WangSet::isComplete() const
{
    if (mWangIdToWangTile.size() < completeSetSize())
        return false;    // can't possibly be complete
    return uniqueKeyCount(mWangIdToWangTile) == completeSetSize())
}

That is to avoid the allocation of the list and in most cases also the whole iteration (the uniqueKeyCount implementation is based on QHash::uniqueKeys, but I didn't compile-check it).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also realized since it should be possible to assign a wangId with 0s (wildcards) this has to check for the count of unique keys with no wild cards. So I'll write a specific function for this in WangSet based on your suggestion and that.

for (int i = 0; i < wangIds.size(); ++i) {
QPoint point(i % tileLayer->width() + tileLayer->x(),
i / tileLayer->width() + tileLayer->y());
if (fillRegion.contains(point))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you only care for points within the fill region, you should really iterate over fillRegion.rects(). Otherwise all these contains checks could be quite a performance hit.

While iterating just the rectangles you could even go one step further, since really you only care for points at the borders of these rectangles. You don't need to iterate the possibly large area in the middle because those wangIds will always be 0.

boundingRect.x(),
boundingRect.y(),
boundingRect.width(),
boundingRect.height());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation is off by one.

int currentIndex = (currentPoint.y() - tileLayer->y()) * tileLayer->width() + (currentPoint.x() - tileLayer->x());
QList<WangTile> wangTiles = mWangSet->findMatchingWangTiles(wangIds[currentIndex]);

while(!wangTiles.isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coding style: missing space after while.

if (mWangTemplateView->isVisible())
setColorView(true);
else
setTemplateView(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way of switching appears to behave a little strange (with the height changing upon switching). I would suggest to use a QStackedWidget (or QStackedLayout if possible) to switch between the two views.

@bjorn
Copy link
Member

bjorn commented Jul 21, 2017

I think we'll need to sort by tile ID when saving the wang tiles, because their order is essentially randomized every time the file is saved currently due to using a QHash. I think using a QHash is still better for performance than a QMap, but upon saving we'll need to get the keys, sort them and then use that to write out the file.

@Bdtrotte
Copy link
Contributor Author

At this point this PR should be ready for final review and merge! Also completes the major mile stones of Wang Tiles in whole. Next to come will be a host of features and improvements for wang tiles to polish off the feature and get it totally ready for merge into master.

Copy link
Member

@bjorn bjorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small comments. Unfortunately, I no longer had time for a complete review and in testing I could not figure out how to make the Wang Brush do something. I selected a color but when hovering or clicking the map nothing appeared to happen, apart from a paint command being added to the undo history.

I'll try to get around to more reviewing as soon as possible.

tilePos.setY(floor(tilePosF.y()));

QPointF tileLocalPoint(modf(tilePosF.x(), nullptr),
modf(tilePosF.y(), nullptr));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is crashing for me. Reading the documentation for modf, I don't see anything about it being valid to pass in a null pointer.

Copy link
Contributor Author

@Bdtrotte Bdtrotte Jul 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting... I've had no problems on mine... I'll pass a local int to be safe.

And fix the spacing!

switch (mEdgeDir) {
case 0:
if (tileLocalPoint.y() < EDGE_DEAD_ZONE)
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My compiler is warning about the fall-through here. Is it intensional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was not, forgot my breaks

Copy link
Member

@bjorn bjorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! It really starts to take up shape with the addition of the wang brush!

I've made a lot of small to medium inline comments. In addition, one thing I missed was the ability to capture the current color from the map using right mouse button, similar to how it works for the stamp and terrain tools.

But I think after you've processed the feedback this can be merged and we can move on to some polishing and other things on your list (do you have it written down somewhere? I don't remember everything you listed on IRC) in preparation of merging to master early next week (I'd say the target for this is 7th or 9th of August). After that we'd have three weeks for remaining tasks as well as hearing feedback from early users.

if (wangId
&& !mWangIdToWangTile.contains(wangId)
&& ((mEdgeColors > 1 && !wangId.hasEdgeWildCards())
|| (mCornerColors > 1 && !wangId.hasCornerWildCards())))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition still needs the same correction as the one in addWangTile for the case where we have both edges and corners.

}
}
for (WangId id : wangId.variations(mEdgeColors, mCornerColors))
list.append(mWangIdToWangTile.values(id));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this is nice and short, it is not the most efficient since it allocates a QList on the heap every iteration. See the documentation of QMultiHash for an example showing a more efficient approach using find and iterating while checking the key.

mRandomCellPicker.add(cell, tile->probability());
if (mIsWangFill && mWangFiller->wangSet()) {
const SharedTileset &tileset = mWangFiller->wangSet()->tileset()->sharedPointer();
if (!mMissingTilesets.contains(tileset)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contains check is superfluous, because mMissingTilesets was just cleared.

@@ -605,6 +605,9 @@ void MapDocument::toggleOtherLayers(Layer *layer)
*/
void MapDocument::insertTileset(int index, const SharedTileset &tileset)
{
if (mMap->tilesets().contains(tileset))
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like I said on IRC, this isn't the right fix for that problem. If insertTileset is silently doing nothing, then AddTileset::undo will do the wrong thing. So this is just moving the problem to the undo action. The proper fix is to not even try adding tilesets to the map that are already there (no AddTileset command should be constructed).

mMissingTilesets.clear();

const SharedTileset &tileset = wangSet->tileset()->sharedPointer();
if (!mMissingTilesets.contains(tileset)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contains check superfluous here as well.


if (fill) {
tileLayer->setCell(currentPoint.x() - tileLayer->x(),
currentPoint.y() - tileLayer->y(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation off by one.

if (!fillRegion.contains(adjacentPoint) && back.contains(adjacentPoint))
surroundingCells[i] = back.cellAt(adjacentPoint);
else
surroundingCells[i] = Cell();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment shouldn't be necessary.

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset name="GrassAndWater" tilewidth="64" tileheight="64" tilecount="24" columns="4">
<image source="C:/Users/topha/Desktop/grass_and_water.png" width="256" height="384"/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure there is a relative reference here.


mMissingTilesets.clear();

const SharedTileset &tileset = wangSet->tileset()->sharedPointer();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is crashing when the wangSet is null (happens to me when I create a new map, for example).

* */
TileLayer *fillRegion(const TileLayer &back,
const QRegion &fillRegion,
bool lookForward = true) const;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation is off.

@bjorn bjorn merged commit f4776b9 into mapeditor:wip/wangtiles Jul 31, 2017
@Bdtrotte Bdtrotte deleted the wip/wangtiles branch July 31, 2017 22:51
@Bdtrotte Bdtrotte restored the wip/wangtiles branch July 31, 2017 23:03
@Bdtrotte Bdtrotte deleted the wip/wangtiles branch July 31, 2017 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants