Skip to content

Commit ecb4305

Browse files
authored
Incremental Loading (#51)
* Auto generate tile edge matching Split the code into more but smaller files. I find it easier to find functions and switch between them when files are smaller and more focused. Add UI to select the tiles to generate, restart the generation and show tile neighbors options. The UI also gives access to debugging features. Add capacity to load any image. - Add button to load an image from disk and used it as single-image, auto-split tiling. - Make loader selection and file loading work together. - Add tile size and step UI to edit loaded or existing single-image tiles. - When creating tiles from an image, allow wrapping around, to mimick the overlapped tile algorithm. - Allow to optionally flip tiles. - Allow to optionally rotate tiles. - Rotations and flips be off by default because for some tilings, rotations and flips would generate too many tiles and be slow. When loading separate tiles, automatically detect which tile can be adjacent to which other tiles by analyzing their edges. This avoids having to specify which edges match which others by hand. This also enable the capability to load a single image and divide it in tiles. - Extract the four edges of each tile and fuzzily match it with other edges. - Make edges size calculation dependent on largest width of tiles. - Allow edges to match deeper within the tile, which more exactly mimick the overlapped tile algorithm. - Allow applying a filter to the edge images to ignore some differences, for example a backgropund that should be ignored. - Compare edges then fuzzy image compare to find identical tiles. - Choose the edge size to be compatible with the small tiles generated from a single-image. - Limit the comparison range to zero when the edge size is small. - Make tile rotations be optional. - Optionally use tile frequencies to choose among the tile options of a cell. Optimize the wave-function-collapse by using a new Bitmap class. - Use bitmap unions and intersections to update the grid. - This is much faster when there are a lots of tiles. - Change the cell options to be bitmaps to use optimized unions and intersections. - Replaced the tile up/down/left/right tile options with bitmaps. - Limit the change propagation to a given radius to speed-up the algorithm. - But take into account the case where we choose to collapse a cell that was not updated recently, which could lead to chosing a tile that should have been disallowed. We always update the cell options before collapsing it to make sure it has updated options. Use change propagation instead of updating all cells. - Each cell keeps track if it has been updated - The bitmap intersection function tells if the bitmap changed. - Make the updateGrid keep track of which cells need to be updated. - It starts with the collapsed cell and propagate to other cells. - The updateCell function keeps track if updating the cell changed it and add its neighbors to the list of cells needing an update only if the cell changed. - Only draw updated cells. - Move drawing in a draw function of the cell. - Redraw cells even if not collapsed when their options change. - Draw non-collapsed cell with the average of the option centers. (We could also update its neighbors, etc... but we don't need to: if they are already collapsed then they are valid. If they're not collapsed, they will update themselves later on and if this leads to a contradiction, then we will rewind.) Add grid history and rewind to avoid restarting from scratch. This is in the `history.js` file. Increase and decrease the rewind distance to get rid of contradictions. That is, by increasing the rewind distance, we can go back far enough to remove the state that invariably lead to a contradiction. Move the loading of tiles to its own file named `tileLoader.js`. This allows different methods of loading tiles and allow switching between which tiles are used on the fly. Split the draw and update functions into smaller functions. Add debug function to help diagnose problems. The debugging code is in the `debug.js` file. - Add a function to log a cell tile index and the options of its neighbor, to help debug when there seems to be a contradiction. - Add a function to show tile edges and cell tile options. - Add a function to show each tile possible neighbors. - Report cell options when collapse fails. - Draw cells in red when a contradiction is found in those cells. Added a GIF image of the hybrid algorithm. * Made tile loading be incremental with user feedback. Made loading and generation be cancellable. * Make loading faster by doing more work per redraw * Convert WFC to a multi-step algorithm Add a multi-steps algorithm base class which allows running an algorithm as a generator of progress reports. This allows running multiple steps of an algorithm during each draw call instead of just one step. It also allows converting more code to this mode, allowing speeding up more things than just tile loading. Make the cell collapse be a multi-step algorithm. This allows running multiple steps per redraw, greatly accelerating the algorithm. Move the grid update in the collapse file since they go together. Make the tile loader be a multi-step algorithm. It was already doing the equivalent, but now with the base class, the code is cleaner. Simplified some progress reports to make progress easier to follow. Fixed what seemed like a bug in the similar-tile elimination. Allow full redraw when toggling debug modes. * Draw background in debug and redraw when toggling smooth drawing on and off. * Fix how the frame rate is read. * Fix drawing tile options: use real tile center pixel.
1 parent 5952786 commit ecb4305

File tree

10 files changed

+399
-201
lines changed

10 files changed

+399
-201
lines changed

p5js/hybrid-model/cell.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,18 @@ class Cell {
4848
let g = 0
4949
let b = 0
5050
let count = 0
51-
for (let tileIndex in this.options.bits) {
51+
for (let tileIndex of this.options) {
5252
const tile = tiles[tileIndex]
5353
if (tile != undefined) {
54-
const center = tile.img.pixels.length / 2
54+
const center = (Math.floor((tile.img.height) / 2) * tile.img.width + Math.floor((tile.img.width) / 2)) * 4
5555
r += tile.img.pixels[center + 0] * tile.frequency
5656
g += tile.img.pixels[center + 1] * tile.frequency
5757
b += tile.img.pixels[center + 2] * tile.frequency
5858
count += tile.frequency
5959
}
6060
}
6161
if (count > 0)
62-
fill(r/count | 0, g/count | 0, b/count | 0, 48)
62+
fill(r/count | 0, g/count | 0, b/count | 0, 80)
6363
else
6464
fill(51)
6565
rect(x + 1, y + 1, w-2, h-2)

p5js/hybrid-model/collapse.js

Lines changed: 144 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,159 @@
11

2-
function collapseLowestEntropy() {
3-
// Build an array of all cells with the lowest entropy
4-
let lowestEntropy = 10000000;
5-
let lowestIndexes= [];
6-
7-
for (let i = 0; i < grid.length; i++) {
8-
let cell = grid[i];
9-
if (cell.collapsed)
10-
continue
11-
let entropy = cell.options.size()
12-
if (entropy < lowestEntropy) {
13-
lowestIndexes = [i]
14-
lowestEntropy = entropy
2+
class HybridWFC extends MultiStepsAlgo {
3+
constructor() {
4+
super()
5+
}
6+
7+
*run() {
8+
while (true) {
9+
// Build an array of all cells with the lowest entropy
10+
let lowestEntropy = 10000000;
11+
let lowestIndexes= [];
12+
13+
for (let i = 0; i < grid.length; i++) {
14+
let cell = grid[i];
15+
if (cell.collapsed)
16+
continue
17+
let entropy = cell.options.size()
18+
if (entropy < lowestEntropy) {
19+
lowestIndexes = [i]
20+
lowestEntropy = entropy
21+
}
22+
else if (entropy == lowestEntropy) {
23+
lowestIndexes.push(i)
24+
}
25+
}
26+
27+
// No lowest entropy means all cells are collapsed
28+
// we are done with the WFC
29+
if (lowestIndexes.length == 0) {
30+
clearContradictions()
31+
drawGrid()
32+
return this.finished()
33+
}
34+
35+
// If the lowest entropy is zero, it means there is a cell
36+
// without any option left, we reached a contraction, so
37+
// rewind history and try again.
38+
if (lowestEntropy <= 0) {
39+
rewindHistory()
40+
yield 'Found zero-entropy contradiction'
41+
}
42+
43+
// Pick a random cell with lowest entropy.
44+
let chosenCellIndex = random(lowestIndexes)
45+
46+
if (!grid[chosenCellIndex].collapse()) {
47+
contradictions[chosenCellIndex] = 200
48+
rewindHistory()
49+
yield 'Cannot collapse cell'
50+
}
51+
52+
_updateGrid(chosenCellIndex)
53+
decreaseRewind()
54+
55+
const cellX = chosenCellIndex % DIM
56+
const cellY = chosenCellIndex / DIM | 0
57+
yield `Collapsed cell at ${cellX} / ${cellY}`
1558
}
16-
else if (entropy == lowestEntropy) {
17-
lowestIndexes.push(i)
59+
}
60+
}
61+
62+
function _updateCell(cellIndex, indexesNeedingUpdate, forceAdd) {
63+
let cell = grid[cellIndex]
64+
let newCell
65+
if (cell.collapsed) {
66+
newCell = cell
67+
}
68+
else {
69+
newCell = new Cell(cell.options)
70+
}
71+
72+
const updatedIndexes = []
73+
let cellChanged = forceAdd
74+
75+
{
76+
if (cellIndex >= DIM) {
77+
let upIndex = cellIndex - DIM;
78+
const upCell = grid[upIndex]
79+
updatedIndexes.push(upIndex)
80+
if (!newCell.collapsed) {
81+
updateOtherOptions.clear()
82+
for (let option of upCell.options) {
83+
updateOtherOptions.in_place_union(tiles[option].down)
84+
}
85+
cellChanged |= newCell.options.in_place_intersection(updateOtherOptions)
86+
}
1887
}
1988
}
2089

21-
// No lowest entropy means all cells are collapsed
22-
// we are done with the WFC
23-
if (lowestIndexes.length == 0) {
24-
clearContradictions()
25-
drawGrid()
26-
noLoop()
27-
return false
90+
{
91+
if (cellIndex < grid.length - DIM) {
92+
let downIndex = cellIndex + DIM;
93+
const downCell = grid[downIndex]
94+
updatedIndexes.push(downIndex)
95+
if (!newCell.collapsed) {
96+
updateOtherOptions.clear()
97+
for (let option of downCell.options) {
98+
updateOtherOptions.in_place_union(tiles[option].up)
99+
}
100+
cellChanged |= newCell.options.in_place_intersection(updateOtherOptions)
101+
}
102+
}
28103
}
29104

30-
// If the lowest entropy is zero, it means there is a cell
31-
// wihtout any option left, we reached a contraction, so
32-
// rewind history and try again.
33-
if (lowestEntropy <= 0) {
34-
for (let cellIndex of lowestIndexes) {
35-
contradictions[cellIndex] = 200
36-
logCellOptions(cellIndex, 'lowest entropy is zero')
105+
{
106+
if (cellIndex % DIM != 0) {
107+
let leftIndex = cellIndex - 1;
108+
const leftCell = grid[leftIndex]
109+
updatedIndexes.push(leftIndex)
110+
if (!newCell.collapsed) {
111+
updateOtherOptions.clear()
112+
for (let option of leftCell.options) {
113+
updateOtherOptions.in_place_union(tiles[option].right)
114+
}
115+
cellChanged |= newCell.options.in_place_intersection(updateOtherOptions)
116+
}
37117
}
38-
rewindHistory()
39-
return false
40118
}
41119

42-
// Pick a random cell with lowest entropy.
43-
let chosenCellIndex = random(lowestIndexes)
120+
{
121+
let rightIndex = cellIndex + 1;
122+
if (rightIndex % DIM != 0) {
123+
const rightCell = grid[rightIndex]
124+
updatedIndexes.push(rightIndex)
125+
if (!newCell.collapsed) {
126+
updateOtherOptions.clear()
127+
for (let option of rightCell.options) {
128+
updateOtherOptions.in_place_union(tiles[option].left)
129+
}
130+
cellChanged |= newCell.options.in_place_intersection(updateOtherOptions)
131+
}
132+
}
133+
}
44134

45-
if (!grid[chosenCellIndex].collapse()) {
46-
contradictions[chosenCellIndex] = 200
47-
logCellOptions(chosenCellIndex, 'cannot collapse cell')
48-
rewindHistory()
49-
return false
135+
if (cellChanged) {
136+
if (!cell.collapsed)
137+
cell.updated = true
138+
for (let index of updatedIndexes) {
139+
indexesNeedingUpdate.push(index)
140+
}
50141
}
51142

52-
updateGrid(chosenCellIndex)
143+
return newCell
144+
}
53145

54-
return true
146+
function _updateGrid(pickedCellIndex) {
147+
gridHistory.push(grid)
148+
grid = grid.slice();
149+
150+
// let updateCount = 0
151+
let indexesNeedingUpdate = []
152+
_updateCell(pickedCellIndex, indexesNeedingUpdate, true)
153+
while (indexesNeedingUpdate.length > 0) {
154+
// updateCount++
155+
const index = indexesNeedingUpdate.pop()
156+
grid[index] = _updateCell(index, indexesNeedingUpdate, false)
157+
}
158+
// console.log(`Updated ${updateCount} cells`)
55159
}

p5js/hybrid-model/debug.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ function drawEdges() {
1818
else
1919
noSmooth()
2020

21+
background(0)
22+
2123
let spacing = 10
2224

2325
let sizes = edges.map(function(edge) {
@@ -77,6 +79,8 @@ function drawTileOptions() {
7779
else
7880
noSmooth()
7981

82+
background(0)
83+
8084
drawnTileIndex = (drawnTileIndex + tiles.length) % tiles.length
8185

8286
const w = width / DIM

p5js/hybrid-model/draw.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ function enableDrawSmooth(enabled) {
33
isSmoothDrawingEnabled = enabled
44
}
55

6+
let isFullRedrawTriggered = false
7+
function triggerFullRedraw() {
8+
isFullRedrawTriggered = true
9+
}
10+
611
function drawGrid() {
712
if (isSmoothDrawingEnabled)
813
smooth()
@@ -19,12 +24,14 @@ function drawGrid() {
1924
let cell = grid[i + j * DIM]
2025
if (cell == undefined)
2126
continue
22-
if (!cell.updated)
27+
if (!cell.updated && !isFullRedrawTriggered)
2328
continue
2429
cell.updated = false
2530
cell.draw(i, j, w, h)
2631
}
2732
}
33+
34+
isFullRedrawTriggered = false
2835

2936
smooth()
3037
}

p5js/hybrid-model/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
<title>Wave Function Collapse</title>
77
<style>
88
body {
9-
background-color: #555;
9+
background-color: #565;
1010
}
1111
</style>
1212
</head>
1313

1414
<body>
1515
<main></main>
1616
<script src="bitmap.js"></script>
17+
<script src="multi-steps.js"></script>
1718
<script src="edge.js"></script>
1819
<script src="tile.js"></script>
1920
<script src="cell.js"></script>
@@ -22,7 +23,6 @@
2223
<script src="history.js"></script>
2324
<script src="ui.js"></script>
2425
<script src="draw.js"></script>
25-
<script src="update.js"></script>
2626
<script src="collapse.js"></script>
2727
<script src="sketch.js"></script>
2828
</body>

0 commit comments

Comments
 (0)