vyy
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { Direction } from "./WfcConstants.js";
|
||||
import { WfcCell } from "./WfcCell.js";
|
||||
import { SourceGrid } from "./SourceGrid.js";
|
||||
import { Xorshift32 } from "../utils/random.js";
|
||||
|
||||
/**
|
||||
* A WfcGrid represents the output of a Wave Function Collapse operation.
|
||||
@@ -8,18 +10,26 @@ export class WfcGrid {
|
||||
/**
|
||||
* @param {number} w width (in cells)
|
||||
* @param {number} h height (in cells)
|
||||
* @param {TrainingGrid} trainingGrid the training grid that will be the source from which we populate this grid.
|
||||
* @param {SourceGrid} sourceGrid the source grid that will be the source from which we populate this grid.
|
||||
* @type {Xorshift32} pre-seeded pseudo random number generator
|
||||
*/
|
||||
constructor(w, h, trainingGrid, rng) {
|
||||
constructor(w, h, sourceGrid, rng) {
|
||||
/** @type {number} */
|
||||
this.width = w;
|
||||
/** @type {number} */
|
||||
this.height = h;
|
||||
this.trainingGrid = trainingGrid;
|
||||
/** @type {SourceGrid} */
|
||||
this.sourceGrid = sourceGrid;
|
||||
/** @type {number[]} */
|
||||
this.lowEntropyCellIdCache = sourceGrid.cells.keys;
|
||||
/** @type {number} */
|
||||
this.lowestEntropy = sourceGrid.dim ** 2;
|
||||
/** @type {Xorshift32} */
|
||||
this.rng = rng;
|
||||
|
||||
//
|
||||
// Populate the cells so each has all available options
|
||||
// For now, this means *copying* all TrainingCell options into each cell
|
||||
/** @type {WfcCell[]} */
|
||||
this.cells = [];
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
@@ -27,139 +37,165 @@ export class WfcGrid {
|
||||
console.log("Resetting Cells");
|
||||
const [w, h] = [this.width, this.height];
|
||||
const len = w * h;
|
||||
this.cells = new Array(len);
|
||||
this.cells = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
const x = i % w;
|
||||
const y = Math.floor(i / w);
|
||||
|
||||
this.cells[i] = new WfcCell(i, x, y, this.trainingGrid.clone().pixels);
|
||||
this.cells.push(new WfcCell(i, x, y, this.sourceGrid.clone().cells));
|
||||
}
|
||||
console.log("Done");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cells that currently have the lowest entropy
|
||||
* @returns {number[]}
|
||||
*/
|
||||
cellsIdsWithLowestEntropy() {
|
||||
console.log("Finding cells with lowest entopy");
|
||||
let result = [];
|
||||
refreshLowEntropyCellIdCache() {
|
||||
this.lowEntropyCellIdCache = [];
|
||||
|
||||
// set lowestEntropy to the highest possible entropy,
|
||||
// and let's search for lower entropy in the cells
|
||||
let lowestEntropy = this.trainingGrid.dim ** 2;
|
||||
this.lowestEntropy = this.sourceGrid.dim ** 2;
|
||||
|
||||
this.cells.forEach((cell, idx) => {
|
||||
console.log("\t checking cell %d (entropy: %d)", idx, cell.getEntropy());
|
||||
const entropy = cell.getEntropy();
|
||||
|
||||
// Cell is locked in, and should not be included
|
||||
if (entropy <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Have we found cells with low entropy?
|
||||
if (cell.getEntropy() < lowestEntropy) {
|
||||
if (entropy < this.lowestEntropy) {
|
||||
// we've found a cell with lower entropy that the ones we've been looking
|
||||
// at so far Clear the search results and start over with this cell's
|
||||
// entropy as our target
|
||||
result = [idx];
|
||||
lowestEntropy = cell.getEntropy();
|
||||
this.lowEntropyCellIdCache = [idx];
|
||||
this.lowestEntropy = entropy;
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Cell matches current entropy level, add it to search results.
|
||||
if (cell.getEntropy() === lowestEntropy) {
|
||||
if (entropy === this.lowestEntropy) {
|
||||
// Cell matches our current level of entropy, so we add it to our search results.
|
||||
// at so far! Clear the results and start over.
|
||||
result.push(idx);
|
||||
this.lowEntropyCellIdCache.push(idx);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (result.length <= 0) {
|
||||
console.log("Found zero lowest-entropy cells.", { lowestEntropy });
|
||||
if (this.lowEntropyCellIdCache.length === 0) {
|
||||
console.log("Found zero lowest-entropy cells.", { entropy: this.lowestEntropy });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
collapse() {
|
||||
console.log("Starting collaps()");
|
||||
let count = this.cells.length;
|
||||
while (count > 0) {
|
||||
count--;
|
||||
// Get a list of possible target cells
|
||||
const lowEntropyCellIds = this.cellIdsWithLowestEntropy();
|
||||
/**
|
||||
* Collapse the grid by one iteration by locking in a random option for the given cell.
|
||||
*
|
||||
* If no cell given, a random cell will be chosen from the cache of lowest-entropy cells.
|
||||
*
|
||||
* @param {number?} The index of the cell that is to be collapsed around
|
||||
*/
|
||||
collapse(cellId = undefined) {
|
||||
if (this.lowEntropyCellIdCache.length === 0) {
|
||||
this.refreshLowEntropyCellIdCache();
|
||||
}
|
||||
|
||||
//
|
||||
// We've hit a dead end
|
||||
// No appropriate target cells found.
|
||||
if (lowEntropyCellIds.length === 0) {
|
||||
console.log("Found no lowest-entropy cells. This should not happen");
|
||||
return count;
|
||||
}
|
||||
|
||||
const rCellId = this.rng.randomElement(lowEntropyCellIds);
|
||||
const rCell = this.cells[rCellId];
|
||||
|
||||
/** @type {TrainingCell} a randomly chosen option that was available to rCell */
|
||||
const rOption = this.rng.randomElement(rCell.options);
|
||||
|
||||
// Lock in the choice for this cell
|
||||
rCell.options = [rOption];
|
||||
|
||||
// _____ ____ _ _
|
||||
// | ____|_ __ _ __ ___ _ __| __ ) ___| | _____ _| |
|
||||
// | _| | '__| '__/ _ \| '__| _ \ / _ \ |/ _ \ \ /\ / / |
|
||||
// | |___| | | | | (_) | | | |_) | __/ | (_) \ V V /|_|
|
||||
// |_____|_| |_| \___/|_| |____/ \___|_|\___/ \_/\_/ (_)
|
||||
// Locking in this cell has changed the grid.
|
||||
// We must look at the cell's cardinal neighbours and update their options.
|
||||
for (let nArr of this.getNeighboursFor(rCell)) {
|
||||
/** @type {number} direction of the neighbour */
|
||||
const neighbourDirection = nArr[0];
|
||||
|
||||
/** @type {WfcCell} the neighbouring cell */
|
||||
const neighbourCell = nArr[1];
|
||||
|
||||
// Clear the neighbour's options, and
|
||||
// repopulate with valid options.
|
||||
const newOptions = [];
|
||||
|
||||
for (let neighbourOption of neighbourCell.options) {
|
||||
if (neighbourOption.potentialNeighbours(rOption, neighbourDirection)) {
|
||||
newOptions.push(neighbourOption);
|
||||
}
|
||||
}
|
||||
|
||||
// We've collapsed too deep.
|
||||
if (newOptions.length === 0) {
|
||||
console.error("We've removed all options from a neighbour!", {
|
||||
rCell,
|
||||
rOption,
|
||||
neighbourCell,
|
||||
neighbourDirection,
|
||||
newOptions,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
neighbourCell.options = newOptions;
|
||||
if (cellId === undefined) {
|
||||
cellId = this.rng.randomElement(this.lowEntropyCellIdCache);
|
||||
if (cellId === undefined) {
|
||||
throw new Error("Could not find a valid cell to start the collapse");
|
||||
}
|
||||
}
|
||||
console.log("Done");
|
||||
return 0;
|
||||
|
||||
const targetCell = this.cells[cellId];
|
||||
if (!targetCell) {
|
||||
throw new Error(`Could not find cell with index ${cellId}`);
|
||||
}
|
||||
|
||||
/** @type {SourceCell} a randomly chosen option that was available to targetCell */
|
||||
const targetOption = this.rng.randomElement(targetCell.options);
|
||||
|
||||
// Lock in the choice for this cell
|
||||
targetCell.options = [targetOption];
|
||||
|
||||
// _____ ____ _ _
|
||||
// | ____|_ __ _ __ ___ _ __| __ ) ___| | _____ _| |
|
||||
// | _| | '__| '__/ _ \| '__| _ \ / _ \ |/ _ \ \ /\ / / |
|
||||
// | |___| | | | | (_) | | | |_) | __/ | (_) \ V V /|_|
|
||||
// |_____|_| |_| \___/|_| |____/ \___|_|\___/ \_/\_/ (_)
|
||||
// Locking in this cell has changed the grid.
|
||||
// We must look at the cell's cardinal neighbours and update their options.
|
||||
for (let nArr of this.neighbourCells(targetCell)) {
|
||||
/** @type {number} direction of the neighbour */
|
||||
const neighbourDirection = nArr[0];
|
||||
|
||||
/** @type {WfcCell} the neighbouring cell */
|
||||
const neighbourCell = nArr[1];
|
||||
|
||||
// Clear the neighbour's options, and
|
||||
// repopulate with valid options.
|
||||
const newOptions = [];
|
||||
|
||||
for (let neighbourOption of neighbourCell.options) {
|
||||
if (neighbourOption.potentialNeighbours(targetOption, neighbourDirection)) {
|
||||
newOptions.push(neighbourOption);
|
||||
}
|
||||
}
|
||||
|
||||
const newEntropyLevel = newOptions.length;
|
||||
|
||||
// We've collapsed too deep.
|
||||
if (newOptions.length === 0) {
|
||||
const oldOptions = neighbourCell.options;
|
||||
neighbourCell.options = newOptions;
|
||||
console.error("We've removed all options from a neighbour!", {
|
||||
targetCell,
|
||||
targetOption,
|
||||
neighbourCell,
|
||||
oldOptions,
|
||||
Direction: Direction[neighbourDirection],
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
neighbourCell.options = newOptions;
|
||||
|
||||
if (newEntropyLevel < this.lowestEntropy) {
|
||||
this.lowestEntropy = newEntropyLevel;
|
||||
this.lowEntropyCellIdCache = [];
|
||||
}
|
||||
if (newEntropyLevel === this.lowestEntropy) {
|
||||
if (!this.lowEntropyCellIdCache.includes(neighbourCell.i)) {
|
||||
this.lowEntropyCellIdCache.push(neighbourCell.i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the neighbours of a cell.
|
||||
* @param {WfcCell} cell
|
||||
*/
|
||||
getNeighboursFor(cell) {
|
||||
neighbourCells(cell) {
|
||||
const result = [];
|
||||
|
||||
//
|
||||
// Northern neighbour
|
||||
//
|
||||
const yNorth = cell.y - 1;
|
||||
if (yNorth >= 0) {
|
||||
if (yNorth > 0) {
|
||||
const xNorth = cell.x;
|
||||
const idx = this.width * yNorth + xNorth;
|
||||
result.push([Direction.N, this.cells[idx]]);
|
||||
}
|
||||
|
||||
//
|
||||
// Southern neighbour
|
||||
//
|
||||
const ySouth = cell.y + 1;
|
||||
if (ySouth < this.height) {
|
||||
const xSouth = cell.x;
|
||||
@@ -167,6 +203,9 @@ export class WfcGrid {
|
||||
result.push([Direction.S, this.cells[idx]]);
|
||||
}
|
||||
|
||||
//
|
||||
// Eastern neighbour
|
||||
//
|
||||
const xEast = cell.x + 1;
|
||||
if (xEast < this.width) {
|
||||
const yEast = cell.y;
|
||||
@@ -174,6 +213,9 @@ export class WfcGrid {
|
||||
result.push([Direction.E, this.cells[idx]]);
|
||||
}
|
||||
|
||||
//
|
||||
// Western neighbour
|
||||
//
|
||||
const xWest = cell.x - 1;
|
||||
if (xWest >= 0) {
|
||||
const yWest = cell.y;
|
||||
|
||||
Reference in New Issue
Block a user