A Wave Function Collapse Overlapping Model Guide

栏目: IT技术 · 发布时间: 4年前

内容简介:Wave Function Collapse is quite a nice algorithm invented byWhat the hell is Wave Function Collapse, exactly? This is a concept loosely borrowed from Quantum Machanics. I really advise you to watch

Overlapping Wave Function Collapse

Wave Function Collapse is quite a nice algorithm invented by Maxim Gumin . However, Gumin’s source file could be really obscure and hard to understand, so I am writing what I’ve learned so far here, so that I could remember it when it is needed. Also, we are talking about the overlapping model here, not the tiled model (as it is what I am interested in). Sorry! All Wave Function Collapse below refers to Wave Function Collapse, the overlapping model.

Introduction

What the hell is Wave Function Collapse, exactly? This is a concept loosely borrowed from Quantum Machanics. I really advise you to watch this video if you are interested in that kind of stuffs. But here we are talking about WaveFunctionCollapse, the PCG (ProCedural Generation) algorithm . It’s usage is quite simple: You feed it an input image, or whatever looks like an image, and it outputs errily similar results. Let’s take a look at a few (stolen directly from GitHub repo):

A Wave Function Collapse Overlapping Model Guide

Now, isn’t this cool? Without all sorts of “machine learning” and stuffs, by only solving the constraints, lots of lively stuffs could appear out of nowhere!

Try it out

Now, I’ve implemented one in Javascript. You could play with it for a little bit, however I am not really sure whether my generator could get the same result with Maxim Gumin’s or not. Also I will cap N at 3 (Trust me, you browser won’t want to go beyond). The color stripe at the bottom is the palatte. Use it to switch colors.

Implementation

Cool, eh? The thing I implement almost works, however it has a major drawback: it is extremely slow . Regardless, I think it’s worth spending 3 day? Or how many? Looking into sources and stuffs. So let’s implement it again! I will be using buttload of pseudocodes down below. And hopefully you can understand!

This algorithm, down to its heart, is “constraint solving”, said by Gumin himself. However, 2D map / texture generation is the most mature usage of this algorithm. It could also be used to generate 3D constructions & poetry, as you can see from its project page . But without further ado, let’s look at the core-est procedure:

int main() {
        getInput();
        processInputsIntoLoadsOfPatterns(N);
        initializeOutput();
        while (notFullyCollapsed()) {
            observe();
            propagate();
        } 
    }

That’s it. In a way, it is quite simple. There might be some word you don’t understand there, but don’t worry, we’ll get to it!

Getting input

There are lots of ways to get user input. In C++ you might use “stb_image”, or if you don’t want to use an image (such as if you are a roguelike fan, then why not just cutesy ASCII characters?), you could always implement one yourself. Thus, we are not going to cover this here.

Processing input into loads of patterns

Now here’s an important part. in WFC, patterns are really just a subsection of the original image. This one important parameter, N, stands for pattern size. Such as the thing you drawn above right now (did you)? As its original size is 12x12, if N = 2, there will be at most

patterns. However, additional patterns will be spawned by rotating around the pattern (which I implemented above), and symmetry patterns (which I did not implement).

A Wave Function Collapse Overlapping Model Guide

So, the image was chopped up into lots of (overlapping) little pieces; and then, additional patterns are added by transforming these original little pieces. That would be a LOT of little pieces, and certainly we wouldn’t want that much. That’s why we execute the elimination process at the end; we kill identical patterns, and counting pattern frequency at the same time (as it will be needed later). Using code, it looks a little bit like this:

for (int y = 0; y <= imageHeight - N; y++) {
    for (int x = 0; x <= imageWidth - N; x++) {
        patterns.add(getImageArea(x, y, N, N), "0deg"); // Add the subimage at (x, y), size (N, N)
        patterns.add(getImageArea(x, y, N, N), "90deg");
        patterns.add(getImageArea(x, y, N, N), "180deg");
        patterns.add(getImageArea(x, y, N, N), "270deg");
    }
}
for (int i = 0; i < patterns.size(); i++) {
    for (int j = i + 1; j < patterns.size(); j++) {
        if (patterns[i] == patterns[j]) { // If the pattern is identical
            patterns[i].frequency++; // Count up the frequency
            patterns.remove(patterns[j]); // Delete the identical twin
            j--; // Go back one, so we don't skip the index
            continue;
        }
    }
}

From the code it is not hard to infer that as N goes up, the pattern diversity will increase greatly. And that’s why I don’t want you to go beyond

, that’s enough to lag your browser.

After pattern generation has been done, we need to begin the overlap rule recording phase. That was called propagator in a number of sources, including Gumin himself. However I find that very confusing, so this is what I am gonna call it. Here you should get what I mean, assuming

(stolen shamelessly from a paper):

A Wave Function Collapse Overlapping Model Guide

Here as we can see, when N = 2, pattern overlapping could be done in 9 ways. We collect the rules about “when those patterns overlaps, their overlap area is the same” (which is called “agrees” in the original source). So how is that? So here’s another figure, still stolen shamelessly from that paper:

A Wave Function Collapse Overlapping Model Guide

Let’s think about it. When

, the only overlappable pattern with the said pattern is itself

. That makes sense, right? Let’s take a look at the figure above, such as when the overlapping pattern is at the original pattern’s (-1, -1) direction: we could see that the only overlapped tile is the original pattern’s top left corner, which is white; and the overlapping pattern’s bottom right corner. Thus, all patterns whose bottom rights are white are allow to overlap with the original pattern, and will be added to the overlapping rules (when offset = (-1, -1)).

After patterns have all been processed, and overlapping rules about each pattern had been added, we move onto the next phase: initialize output!

Initialize output

Be noted that initializing output should only be done after pattern generation is complete. Why? Because in here output isn’t just about initializing an empty image; nope. We are going to initialize a wave! A wave is a two-dimensional array (or any dimension, if you prefer) whose size is exactly the size of the output image’s. Then, for each output cell, we store an array of potential patterns into it (which means all available patterns), in order to mimik it being all those states at once:

for (int y = 0; y <= outputHeight; y++) {
    for (int x = 0; x <= outputWidth; x++) {
        wave.at(x, y) = allPossiblePatterns;
    }
}

Et voila! We got the wave! And we are going to collapse it later.

Entering the loop

As the wave has been initialized, we are about to enter a loop to collapse all those waves, until in each cell, there is only one possible pattern left. If we could reach such circumstance, then a done scenairo happened. What if there is one (or multiple) cells got no possible patterns left? Then we’ve reached a contradiction ! But how would we get to those scenarioes? The first thing we do is observe .

Observe

In the double slit experiment, when electrons were not observed, they act as a wave; however when they were observed, they collapsed into a particle (I don’t know if I am saying this correctly, as my profession is not QM). Here we are going to do the same. We observe a random cell, and by doing that I mean collapse a random cell into its definite state. But which cell to collapse?

Well, Gumin is using the “lowest entropy” rule, and so are the others, so we are going to do the same! “entropy” here means the sum of all possible patterns’ frequencies inside a cell. By finding out the lowest entropy cell in the potential output (wave), and collapsing it into a definite state, we could make sure that the effect of this collapse is at minimum (so we don’t have to propagate that much changes).

function findLowestEntropyCells() {
    int currentLowestEntropy = -1; // Undefined entropy at first
    Cell[] cells = []; // Don't care, this is not actual code anyway
    for (int y = 0; y <= outputHeight; y++) {
        for (int x = 0; x <= outputWidth; x++) {
            if (wave.at(x, y).isDefinite()) {
                continue; // We won't want to deal with definite cells; we have observed them already
            }
            if (currentLowestEntropy == -1 ||  // If the lowest is still undefined or
                wave.at(x, y).entropy < currenetLowestEntropy) { // If this one is lower than the current lowest entropy
                cells = []; // we clear the cells
                currentLowestEntropy = wave.at(x, y).entropy;
            }
            if (currentLowestEntropy == wave.at(x, y).entropy) { // This, together with the if above, 
                                                                 // means if this cell's entropy is less or equal than the current lowest entropy, we add it to the cells array.
                cells.push(wave.at(x, y));
            }
        }
    }
    return cells;
}

So by executing the findLowestEntropyCells() above, it is quite easy to know the current state of observation. If it returns an empty array, that means we’re done. If it returns an array with minimum entropy of 0, that means we’ve reached a contradictive situation, and the observation has to be put to a stop.

cells = findLowestEntropyCells();
if (cells.size() == 0) {
    return DONE;
} else if (cells[0].entropy == 0) {
    return CONTRADICTION;
} else {
    // We're fine
}

Now, after we found all these lowest entropy cells, it’s time to collapse one of those! Sometimes there will be only one, and we could just collapse it. But what about other times when there are more than one in this array? Well, how about we just let RNG decide! That’s how others do anyway.

cells.chooseOneAtRandom().collapse();
flag(cells.neighbors());

Now, even though choosing which cell to collapse is completely at random, choosing which pattern to collapse to is another story. As we got this “frequency” member in pattern, we could collapse patterns by weight.

Cell::collapse() {
    int e = distribute(0, entropy);
    int i;
    for (i = 0; i < patterns.size(); i++) {
        e -= patterns[i].frequency;
        if (e <= 0) {
            break;
        }
    }
    patterns.splice(0, i); // or patterns.erase(patterns.begin(), patterns.begin() + i)
    patterns.splice(1, this.patterns.length - 1); // or patterns.erase(patterns.begin() + i, patterns.end())
}

A Wave Function Collapse Overlapping Model Guide

Why should we flag its neighbors? That’s because we will need to

Propagate.

But first, you might wonder: what classifies as a “neighbor”? Well, a neighbor is a potential overlapping cell with the collapsed cell. That would be all cells in [(x - N + 1, y - N + 1), (x + N - 1, y + N - 1)]. Which, in case you didn’t notice, are all the cells in the stolen paper figure 1.

Imagine all flagged cells as an array, so we could get one flagged cell out at a time, like a queue. In fact, the flagged array is a queue. Take one down, propagate around, …

So here’s what we are gonna do: we take a cell down, and iterate through its patterns. Could this pattern be placed here, now that its neighbors are changed? Thus, we check out each pattern’s rules, and if one rule had been violated, this pattern is no longer possible to be placed here, and should be collapsed away.

If this cell’s patterns are also changed, then of course, we should also notify its neighbors. It spreads like wildfire!

while (!flagged.empty()) {
    Cell cell = flagged.pop();
    bool dirty = false; // At first we assume no patterns of this cell has been collapsed
    for (int i = 0; i < cell.patterns.size(); i++) {
        bool legit = true; // At first we assume this pattern of this cell is legit
        for (int j = 0; j < cell.patterns[i].rules.size(); j++) {
            Vec2 offset = cell.patterns[i].rules[j].offset;
            Cell anotherCell = cell.position + offset;
            if (anotherCell.violates(cell.patterns[i].rules[j])) {
                // This pattern could not be placed here anymore.
                legit = false;
                break;
            }
        }
        if (!legit) { // If not legit anymore
            cell.patterns.splice(i, 1);
            dirty = true;
            i--;
        }
    }

    if (dirty) {
        flag(cell.neighbors()); // Yep
    }
}

And by “violating”, it means that no pattern in rules exists in anotherCell .

A Wave Function Collapse Overlapping Model Guide

And that’s the loop!

Let’s review it again:

int main() {
    getInput();
    processInputsIntoLoadsOfPatterns(N);
    initializeOutput();
    while (notFullyCollapsed()) {
        observe();
        propagate();
    } 
}

So, unless this whole thing was collapsed (or entered contradiction), we are going to do this forever. Cells will collapse one after another, and each one has a more stricter condition than the last (sometimes). Now there is one question left. As we store patterns into wave, but we need to display pixels, what should we do?

Here’s the place I don’t fully understands… Yet. Gumin uses the following code:

int dy = y < FMY - N + 1 ? 0 : N - 1;
int dx = x < FMX - N + 1 ? 0 : N - 1;
Color c = colors[patterns[observed[x - dx + (y - dy) * FMX]][dx + dy * N]];
bitmapData[x + y * FMX] = unchecked((int)0xff000000 | (c.R << 16) | (c.G << 8) | c.B);

Looks like when x and y is near the border, the color will be the edge of the pattern; otherwise it will be the top left corner of the pattern. I tried it and sometimes its wrong (it must be my problem, yeah), so I am just sampling the top left corner, no matter the position. The great thing is it still works!

If you want to see fancy animations like the one in the GitHub Repo, you could just separate the loop apart, and render it whenever observe() happens. If the corresponding cell has not been fully collapsed yet, you could average its possibilities out, also by sampling all possible patterns’ specific tiles.

Conclusion

So that’s it! Wave Function Collapsed, the overlapping model. I might be wrong, and if I am, please tell me. Also the version I written is, again, extremely slow . So don’t use mine! Use the others! I am using the others as well. And there you have it!

References:

  1. Awesome paper that I stole: WFC Is Constraint Solving In The Wild It is really awesome!
  2. GitHub Repository
  3. Procedural Generation with Wave Function Collapse
  4. The Wavefunction Collapse Algorithm explained very clearly
  5. The Very Simple Tiled Model by Robert Theaton
  6. EasyWFC, an C# implementation
  7. FastWFC, an C++ implementation (I still don’t know how could it be this quick)
  8. My shitty C++ implemention, for ASCII chars And by shitty I mean really shitty. Generating a 100x100 map takes 2 minutes.
  9. My shitty JavaScript implementation Well… It is kinda embedded into the blog source tree. It is also shitty, by the way.

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C算法(第二卷:图算法)(第3版)

C算法(第二卷:图算法)(第3版)

塞德威克(Sedgewick Robert) / 周良忠 / 第1版 (2004年1月1日) / 2004-4 / 38.0

《C算法(第2卷)(图算法)(第3版)(中文版)》所讨论的图算法,都是实际中解决图问题的最重要的已知方法。《C算法(第2卷)(图算法)(第3版)(中文版)》的主要宗旨是让越来越多需要了解这些算法的人的能够掌握这些方法及基本原理。书中根据基本原理从基本住处开始循序渐进地讲解,然后再介绍一些经典方法,最后介绍仍在进行研究和发展的现代技术。精心挑选的实例、详尽的图示以及完整的实现代码与正文中的算法和应用......一起来看看 《C算法(第二卷:图算法)(第3版)》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换