-
Notifications
You must be signed in to change notification settings - Fork 119
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added Wave Function Collapse functionality
- Loading branch information
Showing
5 changed files
with
342 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
// Copyright 2018 Wouter van Oortmerssen. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
|
||
// Very simple tile based Wave Function Collapse ("Simple Tiled Model") implementation. | ||
// See: https://github.com/mxgmn/WaveFunctionCollapse | ||
// Derives adjacencies from an example rather than explicitly specified neighbors. | ||
// Does not do any symmetries/rotations unless they're in the example. | ||
|
||
// Algorithm has a lot of similarities to A* in how its implemented. | ||
// Uses bitmasks to store the set of possible tiles, which currently limits the number of | ||
// unique tiles to 64. This restriction cool be lifted by using std::bitset instead. | ||
|
||
// In my testing, generates a 50x50 tile map in <1 msec. 58% of such maps are conflict free. | ||
// At 100x100 that is 3 msec and 34%. | ||
// At 200x200 that is 24 msec and 13% | ||
// At 400x400 that is 205 msec and ~1% | ||
// Algorithm may need to extended to flood more than 2 neighbor levels to make it suitable | ||
// for really gigantic maps. | ||
|
||
// inmap & outmap must point to row-major 2D arrays of the given size. | ||
// each in tile char must be in range 0..127, of which max 64 may actually be in use (may be | ||
// sparse). | ||
// Returns false if too many unique tiles in input. | ||
template<typename T> bool WaveFunctionCollapse(const int2 &insize, const char **inmap, | ||
const int2 &outsize, char **outmap, | ||
RandomNumberGenerator<T> &rnd, | ||
int &num_contradictions) { | ||
num_contradictions = 0; | ||
typedef uint64_t bitmask_t; | ||
const auto nbits = sizeof(bitmask_t) * 8; | ||
array<int, 256> tile_lookup; | ||
tile_lookup.fill(-1); | ||
struct Tile { bitmask_t sides[4] = {}; size_t freq = 0; char tidx = 0; }; | ||
vector<Tile> tiles; | ||
int2 neighbors[] = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } }; | ||
// Collect unique tiles and their frequency of occurrence. | ||
for (int iny = 0; iny < insize.y; iny++) { | ||
for (int inx = 0; inx < insize.x; inx++) { | ||
auto t = inmap[iny][inx]; | ||
if (tile_lookup[t] < 0) { | ||
// We use a bitmask_t mask for valid neighbors. | ||
if (tiles.size() == nbits - 1) return false; | ||
tile_lookup[t] = (int)tiles.size(); | ||
tiles.push_back(Tile()); | ||
} | ||
auto &tile = tiles[tile_lookup[t]]; | ||
tile.freq++; | ||
tile.tidx = t; | ||
} | ||
} | ||
// Construct valid neighbor bitmasks. | ||
for (int iny = 0; iny < insize.y; iny++) { | ||
for (int inx = 0; inx < insize.x; inx++) { | ||
auto t = inmap[iny][inx]; | ||
auto &tile = tiles[tile_lookup[t]]; | ||
int ni = 0; | ||
for (auto n : neighbors) { | ||
auto p = (n + int2(inx, iny) + insize) % insize; | ||
auto tn = inmap[p.y][p.x]; | ||
assert(tile_lookup[tn] >= 0); | ||
tile.sides[ni] |= 1 << tile_lookup[tn]; | ||
ni++; | ||
} | ||
} | ||
} | ||
size_t most_common_tile_id = 0; | ||
size_t most_common_tile_freq = 0; | ||
for (auto &tile : tiles) if (tile.freq > most_common_tile_freq) { | ||
most_common_tile_freq = tile.freq; | ||
most_common_tile_id = &tile - &tiles[0]; | ||
} | ||
// Track an open list (much like A*) of next options, sorted by best candidate at the end. | ||
list<pair<int2, int>> open, temp; | ||
// Store a bitmask per output cell of remaining possible choices. | ||
auto max_bitmask = (1 << tiles.size()) - 1; | ||
enum class State : uchar { NEW, OPEN, CLOSED }; | ||
struct Cell { | ||
bitmask_t wf; | ||
uchar popcnt = 0; | ||
State state = State::NEW; | ||
decltype(open)::iterator it; | ||
Cell(bitmask_t wf, uchar popcnt) : wf(wf), popcnt(popcnt) {} | ||
}; | ||
vector<vector<Cell>> cells(outsize.y, vector<Cell>(outsize.x, Cell(max_bitmask, tiles.size()))); | ||
auto start = rndivec<int, 2>(rnd, outsize); | ||
open.push_back({ start, 0 }); // Start. | ||
auto &scell = cells[start.y][start.x]; | ||
scell.state = State::OPEN; | ||
scell.it = open.begin(); | ||
// Pick tiles until no more possible. | ||
while (!open.empty()) { | ||
// Simply picking the first list item results in the same chance of conflicts as | ||
// random picks over equal options, but it is assumed the latter could generate more | ||
// interesting maps. | ||
size_t num_candidates = 1; | ||
auto numopts_0 = cells[open.back().first.y][open.back().first.x].popcnt; | ||
for (auto it = ++open.rbegin(); it != open.rend(); ++it) | ||
if (numopts_0 == cells[it->first.y][it->first.x].popcnt && | ||
open.back().second == it->second) | ||
num_candidates++; | ||
else | ||
break; | ||
auto candidate_i = rnd(num_candidates); | ||
auto candidate_it = --open.end(); | ||
for (int i = 0; i < candidate_i; i++) --candidate_it; | ||
auto cur = candidate_it->first; | ||
temp.splice(temp.end(), open, candidate_it); | ||
auto &cell = cells[cur.y][cur.x]; | ||
assert(cell.state == State::OPEN); | ||
cell.state = State::CLOSED; | ||
bool contradiction = !cell.popcnt; | ||
if (contradiction) { | ||
num_contradictions++; | ||
// Rather than failing right here, fill in the whole map as best as possible just in | ||
// case a map with bad tile neighbors is still useful to the caller. | ||
// As a heuristic lets just use the most common tile, as that will likely have the | ||
// most neighbor options. | ||
cell.wf = 1 << most_common_tile_id; | ||
cell.popcnt = 1; | ||
} | ||
// From our options, pick one randomly, weighted by frequency of tile occurrence. | ||
// First find total frequency. | ||
size_t total_freq = 0; | ||
for (size_t i = 0; i < tiles.size(); i++) if (cell.wf & (1 << i)) total_freq += tiles[i].freq; | ||
auto freqpick = rnd(total_freq); | ||
// Now pick. | ||
size_t picked = 0; | ||
for (size_t i = 0; i < tiles.size(); i++) if (cell.wf & (1 << i)) { | ||
picked = i; | ||
if ((freqpick -= tiles[i].freq) <= 0) break; | ||
} | ||
assert(freqpick <= 0); | ||
// Modify the picked tile. | ||
auto &tile = tiles[picked]; | ||
outmap[cur.y][cur.x] = tile.tidx; | ||
cell.wf = 1 << picked; // Exactly one option remains. | ||
cell.popcnt = 1; | ||
// Now lets cycle thru neighbors, reduce their options (and maybe their neighbors options), | ||
// and add them to the open list for next pick. | ||
int ni = 0; | ||
for (auto n : neighbors) { | ||
auto p = (cur + n + outsize) % outsize; | ||
auto &ncell = cells[p.y][p.x]; | ||
if (ncell.state != State::CLOSED) { | ||
ncell.wf &= tile.sides[ni]; // Reduce options. | ||
ncell.popcnt = PopCount(ncell.wf); | ||
int totalnnumopts = 0; | ||
if (!contradiction) { | ||
// Hardcoded second level of neighbors of neighbors, to reduce chance of | ||
// contradiction. | ||
// Only do this when our current tile isn't a contradiction, to avoid | ||
// artificially shrinking options. | ||
int nni = 0; | ||
for (auto nn : neighbors) { | ||
auto pnn = (p + nn + outsize) % outsize; | ||
auto &nncell = cells[pnn.y][pnn.x]; | ||
if (nncell.state != State::CLOSED) { | ||
// Collect the superset of possible options. If we remove anything but | ||
// these, we are guaranteed the direct neigbor always has a possible | ||
//pick. | ||
bitmask_t superopts = 0; | ||
for (size_t i = 0; i < tiles.size(); i++) | ||
if (ncell.wf & (1 << i)) | ||
superopts |= tiles[i].sides[nni]; | ||
nncell.wf &= superopts; | ||
nncell.popcnt = PopCount(nncell.wf); | ||
} | ||
totalnnumopts += nncell.popcnt; | ||
nni++; | ||
} | ||
} | ||
if (ncell.state == State::OPEN) { | ||
// Already in the open list, remove it for it to be re-added just in case | ||
// its location is not optimal anymore. | ||
totalnnumopts = min(totalnnumopts, ncell.it->second); | ||
temp.splice(temp.end(), open, ncell.it); // Avoid alloc. | ||
} | ||
// Insert this neighbor, sorted by lowest possibilities. | ||
// Use total possibilities of neighbors as a tie-breaker to avoid causing | ||
// contradictions by needless surrounding of tiles. | ||
decltype(open)::iterator dit = open.begin(); | ||
for (auto it = open.rbegin(); it != open.rend(); ++it) { | ||
auto onumopts = cells[it->first.y][it->first.x].popcnt; | ||
if (onumopts > ncell.popcnt || | ||
(onumopts == ncell.popcnt && it->second >= totalnnumopts)) { | ||
dit = it.base(); | ||
break; | ||
} | ||
} | ||
if (temp.empty()) temp.push_back({}); | ||
open.splice(dit, temp, ncell.it = temp.begin()); | ||
*ncell.it = { p, totalnnumopts }; | ||
ncell.state = State::OPEN; | ||
} | ||
ni++; | ||
} | ||
} | ||
return true; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// Example of using Wave Function Collapse to generate gameworlds based on tiles. | ||
// Using ascii chars here for simplicity. | ||
|
||
// Using """ string literals so we don't have to escape \ :( | ||
|
||
tilemap :== [ | ||
""" /--\ """, | ||
""" | | """, | ||
""" | | """, | ||
"""/--J L--\ """, | ||
"""| | """, | ||
"""| | """, | ||
"""L--\ /--J """, | ||
""" | | """, | ||
""" | | """, | ||
""" L--J """, | ||
""" """, | ||
] | ||
|
||
benchmark :== false | ||
|
||
if benchmark: | ||
no_conflicts := 0 | ||
for(1000) i: | ||
outmap, conflicts := wave_function_collapse(tilemap, xy { 100, 100 }) | ||
print i + ": " + conflicts | ||
if not conflicts: no_conflicts++ | ||
print no_conflicts | ||
print seconds_elapsed() | ||
|
||
else: | ||
// Just print a single no-conflict example. | ||
for(100) i: | ||
outmap, conflicts := wave_function_collapse(tilemap, xy { 100, 50 }) | ||
if not conflicts: | ||
print "iteration: " + i | ||
for(outmap) s: print s | ||
return from program | ||
|
||
/* | ||
|
||
prints: | ||
|
||
| | /-\ /-------J | | L-J /--J | ||
| /--J | | | | | | | ||
| | | L-J | | /-\ | | ||
-----------J /-J | | | /------------\ | | | / | ||
| L-\ /----J | | | | | | | | ||
| | | /--J | | | L----J | | ||
| | L-\ | | | | | | ||
----\ | L-\ | | /-\ /-J | | L | ||
| | | | | | | | L-------------\ | /---\ | ||
| | | /-----J L-J L--J | L-\ | | | ||
| | | | | | | | | ||
| | | | /-------\ | | | | | ||
| | | | | | | L----J | | ||
| | /---------J L-----\ | /---J | | | ||
/-J | | | /-----J | /-J /----J | ||
| | | | /----\ | | | | | ||
| | | /-\ /----J | | | | | | | ||
| | | | | | | | | | | /------J | ||
| | | | | /-J /--\ /-----J | | | | | | ||
| L-J | | | | | | | | | /--J | | ||
L-----\ | | | | L--J | L---\ | | | /--\ /----\ | ||
| /---\ | | | | | | L------J | | | | | | ||
| | | | | | | | | | | | | | | ||
--\ | | L-----J | | | | | | | | | L-- | ||
| | | | L--\ | /--------J | | | | /--J | ||
--J /--J | /-\ | | | | | /--J | | | /----- | ||
| L---\ | | | | | | | | | L---J | | ||
| | | | | | | | | /----\ | | | | ||
| L--J | | | | | /-\ | | | | | /----\ L----\ | ||
| | | L-J | | | | | | | | | | | | ||
L------------\ | | L-------J | | | | | | | | | | ||
| | | | | | | | | | L--\ | | ||
/-------------J | | /---\ | L------J | | | | | | | ||
| /-J | | | /---J | | | | | | | ||
| | L------\ | L---\ | | | | L--\ /--J | | ||
| /-\ L-\ | | | | | L-----\ | | | | | ||
| | | | | | | | /----\ | /-\ | L--\ | | | | ||
--\ | | | | L---J | L--\ | | | | | | | | | L | ||
| | | | | | | | /-J | L-J | | | | | ||
L-J L-J L---------\ | | | | | /--J | | | | ||
| L-\ | | | | | | | L---\ | ||
----\ /---\ | | L------J | | | | | | /-- | ||
| | | | | | | L--\ | | | | | ||
| L---J | /---J | | | L--J | | | ||
| | /--\ | /--\ | | | | | | ||
L------\ /-----J | | | | | | | /--J | | | ||
| /--\ | | | | L--J | | | | | | ||
| | | | | | | /-\ L-------\ | L----------\ | | | ||
| | L---J | | | | | | | | L--J | ||
| | | | | | | L-J | | ||
|
||
*/ | ||
|