diff --git a/Set.gd b/Set.gd index fd9b5c2..ca6da7c 100644 --- a/Set.gd +++ b/Set.gd @@ -1,4 +1,5 @@ class_name Set extends RefCounted +## Adapted from: https://gist.github.com/NoodleSushi/3eb9cc08eb1d1c369bef308262804f1e ## A custom implementation of a set data structure in GDScript. ## ## Usage Example: @@ -48,47 +49,47 @@ func duplicate() -> Set: # O(min(|self|, |set|)) ## Returns a new [Set] containing elements that are present in the current set ## but not in the provided [param set]. -func difference(set: Set) -> Set: - if set.hash() == self.hash(): +func difference(param: Set) -> Set: + if param.hash() == self.hash(): return Set.new() var out := self.duplicate() - if set.size() > self.size(): + if param.size() > self.size(): for element in self.elements(): - if set.has(element): + if param.has(element): out.erase(element) else: - for element in set.elements(): + for element in param.elements(): out.erase(element) return out # O(min(|self|, |set|)) ## Modifies the current [Set] to contain elements present in the current set\ ## but not in the provided [param set]. -func difference_update(set: Set) -> void: - if set.hash() == self.hash(): +func difference_update(param: Set) -> void: + if param.hash() == self.hash(): self.clear() return - if set.size() > self.size(): + if param.size() > self.size(): for element in self.elements(): - if set.has(element): + if param.has(element): self.erase(element) else: - for element in set.elements(): + for element in param.elements(): self.erase(element) # O(min(|self|, |set|)) ## Returns a new [Set] containing elements common to both the current set and ## the provided [param set]. -func intersection(set: Set) -> Set: - if set.hash() == self.hash(): +func intersection(param: Set) -> Set: + if param.hash() == self.hash(): return duplicate() var out := Set.new() - if set.size() > self.size(): + if param.size() > self.size(): for element in self.elements(): - if set.has(element): + if param.has(element): out.add(element) else: - for element in set.elements(): + for element in param.elements(): if self.has(element): out.add(element) return out @@ -96,16 +97,16 @@ func intersection(set: Set) -> Set: # O(min(|self|, |set|)) ## Modifies the current set to contain only elements common to both the ## current set and the provided [param set]. -func intersection_update(set: Set) -> void: - if set.hash() == self.hash(): +func intersection_update(param: Set) -> void: + if param.hash() == self.hash(): return var out := Set.new() - if set.size() > self.size(): + if param.size() > self.size(): for element in self.elements(): - if set.has(element): + if param.has(element): out.add(element) else: - for element in set.elements(): + for element in param.elements(): if self.has(element): out.add(element) self.clear() @@ -114,13 +115,13 @@ func intersection_update(set: Set) -> void: # O([1, min(|self|, |set|)]) ## Returns [code]true[/code] if the sets have no elements in common; ## otherwise, returns [code]false[/code]. -func isdisjoint(set: Set) -> bool: - if set.size() > self.size(): +func isdisjoint(param: Set) -> bool: + if param.size() > self.size(): for element in self.elements(): - if set.has(element): + if param.has(element): return false else: - for element in set.elements(): + for element in param.elements(): if self.has(element): return false return true @@ -128,23 +129,23 @@ func isdisjoint(set: Set) -> bool: # O([1, |self|]) ## Returns [code]true[/code] if every element of the current set is present ## in the provided [param set]; otherwise, returns [code]false[/code]. -func issubset(set: Set) -> bool: - if set.size() < self.size(): +func issubset(param: Set) -> bool: + if param.size() < self.size(): return false else: for element in self.elements(): - if !set.has(element): + if !param.has(element): return false return true # O([1, |set|]) ## Returns [code]true[/code] if every element of the provided [param set] ## is present in the current set; otherwise, returns [code]false[/code]. -func issuperset(set: Set) -> bool: - if self.size() < set.size(): +func issuperset(param: Set) -> bool: + if self.size() < param.size(): return false else: - for element in set.elements(): + for element in param.elements(): if !self.has(element): return false return true @@ -164,18 +165,18 @@ func erase(element: Variant) -> void: # O(min(|self|, |set|)) ## Returns a new [Set] containing elements that are present in either ## the current set or the provided [param set], but not in both. -func symmetric_difference(set: Set) -> Set: - if set.hash() == self.hash(): +func symmetric_difference(param: Set) -> Set: + if param.hash() == self.hash(): return Set.new() - if set.size() > self.size(): - var out := set.duplicate() + if param.size() > self.size(): + var out := param.duplicate() for element in self.elements(): - if set.has(element): + if param.has(element): out.remove(element) return out else: var out := self.duplicate() - for element in set.elements(): + for element in param.elements(): if self.has(element): out.remove(element) return out @@ -183,9 +184,9 @@ func symmetric_difference(set: Set) -> Set: # O(min(|self|, |set|)) ## Modifies the current set to contain elements that are present in either ## the current set or the provided [param set], but not in both. -func symmetric_difference_update(set: Set) -> void: - if set.size() > self.size(): - var temp := set.duplicate() +func symmetric_difference_update(param: Set) -> void: + if param.size() > self.size(): + var temp := param.duplicate() for element in self.elements(): if temp.has(element): temp.erase(element) @@ -193,7 +194,7 @@ func symmetric_difference_update(set: Set) -> void: temp.add(element) self._set = temp._set else: - for element in set.elements(): + for element in param.elements(): if self.has(element): self.erase(element) else: @@ -202,30 +203,30 @@ func symmetric_difference_update(set: Set) -> void: # O(min(|self|, |set|)) ## Returns a new [Set] containing all elements from both the current ## set and the provided [param set]. -func union(set: Set) -> Set: - if set.size() > self.size(): - var out := set.duplicate() +func union(param: Set) -> Set: + if param.size() > self.size(): + var out := param.duplicate() for element in self.elements(): out.add(element) return out else: var out := self.duplicate() - for element in set.elements(): + for element in param.elements(): out.add(element) return out # O(min(|self|, |set|)) ## Modifies the current set to contain all elements from both the current set ## and the provided [param set]. -func update(set: Set) -> void: - if set.size() > self.size(): - var temp := set.duplicate() +func update(param: Set) -> void: + if param.size() > self.size(): + var temp := param.duplicate() for element in self.elements(): temp.add(element) self._dict = temp._dict else: var temp := self.duplicate() - for element in set.elements(): + for element in param.elements(): temp.add(element) self._dict = temp.dict diff --git a/client.gd b/client.gd index 5ec51bb..71f3599 100644 --- a/client.gd +++ b/client.gd @@ -3,11 +3,6 @@ extends Node #Master list of chunks var _chunks: Array[Chunk] = [] -#Create some test samples - these samples must ALWAYS be 9-elements long, 5th element is the result -#var _samples: Array[PackedInt32Array] = [ - #[1,1,1, 1,1,1, 1,1,1], -#] - @onready var wfc: Node = get_node("../Generator") @@ -15,73 +10,14 @@ var wfc: Node = get_node("../Generator") var map: TileMapLayer = get_node("../TileMapLayer") func _ready() -> void: - var ruleset: PackedInt32Array = read_sample_ruleset("sample2.png") - var samples: Array[PackedInt32Array] = parse_samples_from_ruleset(ruleset) + var samples: Array[PackedInt32Array] = Rulesets.load_samples("sample3.png") var c: Chunk = wfc.generate_chunk_at(0,0,_chunks,samples) - #TODO: build a visual layout for the results - #TODO: handle the chunk-edges + #TODO: handle the chunk-edges, non-zero chunk coords - #print(ruleset) - #print(samples) - print(c.data) + print(c.data) #debugging draw_map_data(c) -## Read the png file, and parse it to a useable ruleset -func read_sample_ruleset(filename: String) -> PackedInt32Array: - var img: Image = Image.load_from_file(filename) - var png: PackedByteArray = img.get_data() - var hex: PackedInt32Array = [] - - @warning_ignore("integer_division") - var size: int = (png.size() / 3) - hex.resize(size) - - #print(png) - for i in range(size): #the file is assumed to be in RGB format - hex[i] = (png[i * 3] << 16) | (png[i * 3 + 1] << 8) | (png[i * 3 + 2] << 0) - #print(i, "(", hex[i], "): ", png[i * 3], ",", png[i * 3 + 1], ",", png[i * 3 + 2]) - return hex - -func parse_samples_from_ruleset(ruleset: PackedInt32Array) -> Array[PackedInt32Array]: - #for now, assume the ruleset is 8x8 - const RULESET_WIDTH: int = 8 - const RULESET_HEIGHT: int = 8 - - #using a custom container type - var samples: Set = Set.new() - - for x in range(1, RULESET_WIDTH-1): - for y in range(1, RULESET_HEIGHT-1): - var sample: PackedInt32Array = [ - ruleset[(y -1) * RULESET_WIDTH + (x -1)], - ruleset[(y -1) * RULESET_WIDTH + (x )], - ruleset[(y -1) * RULESET_WIDTH + (x +1)], - - ruleset[(y ) * RULESET_WIDTH + (x -1)], - ruleset[(y ) * RULESET_WIDTH + (x )], - ruleset[(y ) * RULESET_WIDTH + (x +1)], - - ruleset[(y +1) * RULESET_WIDTH + (x -1)], - ruleset[(y +1) * RULESET_WIDTH + (x )], - ruleset[(y +1) * RULESET_WIDTH + (x +1)], - ] - - samples.add(sample) - - var result: Array[PackedInt32Array] = [] - result.assign(samples.elements()) - return result - func draw_map_data(chunk: Chunk) -> void: - #hacky - var fix = func(x) -> int: - match x: - 0x000000: return 0 - 0xFF0000: return 1 - 0x00FF00: return 2 - 0x0000FF: return 3 - _: return -1 - for x in range(Chunk.CHUNK_WIDTH): for y in range(Chunk.CHUNK_HEIGHT): - map.set_cell(Vector2i(x, y), fix.call(chunk.data[y * Chunk.CHUNK_WIDTH + x]), Vector2i.ZERO) + map.set_cell(Vector2i(x, y), chunk.data[y * Chunk.CHUNK_WIDTH + x], Vector2i.ZERO) diff --git a/sample2.png b/sample2.png index ed2261f..62704be 100644 Binary files a/sample2.png and b/sample2.png differ diff --git a/sample3.png b/sample3.png new file mode 100644 index 0000000..e258df8 Binary files /dev/null and b/sample3.png differ diff --git a/sample3.png.import b/sample3.png.import new file mode 100644 index 0000000..ff6fe44 --- /dev/null +++ b/sample3.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cvusc1g2mcgj5" +path="res://.godot/imported/sample3.png-40d6657997a818597ed152abe7d0ca8d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://sample3.png" +dest_files=["res://.godot/imported/sample3.png-40d6657997a818597ed152abe7d0ca8d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/scene.tscn b/scene.tscn index 5f88a96..55dcdd7 100644 --- a/scene.tscn +++ b/scene.tscn @@ -29,10 +29,10 @@ texture_region_size = Vector2i(32, 32) [sub_resource type="TileSet" id="TileSet_gu47o"] tile_size = Vector2i(32, 32) -sources/0 = SubResource("TileSetAtlasSource_5juve") -sources/1 = SubResource("TileSetAtlasSource_fy5k1") -sources/2 = SubResource("TileSetAtlasSource_5c1cw") -sources/3 = SubResource("TileSetAtlasSource_akxrc") +sources/4 = SubResource("TileSetAtlasSource_akxrc") +sources/3 = SubResource("TileSetAtlasSource_5c1cw") +sources/2 = SubResource("TileSetAtlasSource_fy5k1") +sources/1 = SubResource("TileSetAtlasSource_5juve") [node name="Scene" type="Node2D"] diff --git a/wfc/generator.gd b/wfc/generator.gd index ffde457..2a75fef 100644 --- a/wfc/generator.gd +++ b/wfc/generator.gd @@ -12,20 +12,26 @@ func _reset_entropy() -> void: entropy_total.clear() entropy_total.resize(Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT) entropy_total.fill(-1) - + func _update_entropy(chunk: Chunk, samples: Array[PackedInt32Array]) -> void: #iterate over indeterminate tiles to find their valid neighbours for x in range(Chunk.CHUNK_WIDTH): for y in range((Chunk.CHUNK_HEIGHT)): - if entropy_total[y * Chunk.CHUNK_WIDTH + x] < 0: #There's some excess multiplication in this code, but I want to get it right first - entropy_valid[y * Chunk.CHUNK_WIDTH + x] = _find_valid_samples_at(chunk, x, y, samples) #this overrides any pre-existing cached data - entropy_total[y * Chunk.CHUNK_WIDTH + x] = entropy_valid[y * Chunk.CHUNK_WIDTH + x].size() #the entropy is stored separately or faster processing + var index: int = y * Chunk.CHUNK_WIDTH + x #reduce number crunching + + if chunk.data[index] > 0: #if the tile is already set, ignore + entropy_total[index] = 0 + + if entropy_total[index] < 0: + entropy_valid[index] = _find_valid_samples_at(chunk, x, y, samples) #this overrides any pre-existing cached data + entropy_total[index] = entropy_valid[index].size() #the entropy is stored separately for faster processing -func _clear_entropy_at(index: int) -> void: +func _zero_entropy_at(chunk: Chunk, index: int) -> void: var clear_at := func (dx: int, dy: int) -> void: - var check = index + (dy * Chunk.CHUNK_WIDTH) + (dx) - if check >= 0 and check < Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT: - if entropy_total[check] > 0: entropy_total[check] = -1 #unset surrounding tiles + var idx = index + (dy * Chunk.CHUNK_WIDTH) + (dx) + if idx >= 0 and idx < Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT: + if entropy_total[idx] > 0 and chunk.data[idx] <= 0: entropy_total[idx] = -1 #unset surrounding tiles + clear_at.call(-1, -1) clear_at.call( 0, -1) clear_at.call( 1, -1) @@ -40,16 +46,18 @@ func _clear_entropy_at(index: int) -> void: func generate_chunk_at(_x: int, _y: int, chunk_array: Array[Chunk], samples: Array[PackedInt32Array]) -> Chunk: var chunk: Chunk = Chunk.new(_x, _y) _reset_entropy() + #chunk.data[0] = 1 #DEBUG: seed _update_entropy(chunk, samples) - while true: + while true: #TODO: would a floodfill approach work better? var index = _find_lowest_entropy_tile_index() if index < 0: break #none found, finished - + print(index, ":", entropy_total[index]) + var s: PackedInt32Array = entropy_valid[index].pick_random() chunk.data[index] = s[4] - _clear_entropy_at(index) + _zero_entropy_at(chunk, index) _update_entropy(chunk, samples) chunk_array.append(chunk) @@ -59,18 +67,25 @@ func generate_chunk_at(_x: int, _y: int, chunk_array: Array[Chunk], samples: Arr func _find_lowest_entropy_tile_index() -> int: #find the lowest-entropy tile var lowest: int = -1 + var lowest_list: Array[int] #smooth out order bias for i in range(Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT): if lowest < 0: if entropy_total[i] <= 0: continue #no options lowest = i + lowest_list = [i] continue - - if entropy_total[i] > 0 and entropy_total[i] < entropy_total[lowest]: - lowest = i + + #actually update the lowest found + if entropy_total[i] > 0: + if entropy_total[i] < entropy_total[lowest]: + lowest = i + lowest_list = [i] + elif entropy_total[i] == entropy_total[lowest]: + lowest_list.append(i) #finished - return lowest + return lowest_list.pick_random() if lowest_list.size() > 0 else lowest func _find_valid_samples_at(chunk: Chunk, tile_x: int, tile_y: int, _samples: Array[PackedInt32Array]) -> Array[PackedInt32Array]: var valid: Array[PackedInt32Array] = [] diff --git a/wfc/rulesets.gd b/wfc/rulesets.gd new file mode 100644 index 0000000..72e7a2d --- /dev/null +++ b/wfc/rulesets.gd @@ -0,0 +1,63 @@ +class_name Rulesets extends Object +## These rulesets are loaded from a file and return samples for the generator + +## Different rulesets can have different file dimensions +var _size: Vector2i = Vector2i.ZERO + +static func load_samples(filename: String) -> Array[PackedInt32Array]: + var obj: Rulesets = Rulesets.new() + var ruleset: PackedInt32Array = obj._load_ruleset_file(filename) + var samples: Array[PackedInt32Array] = obj._parse_ruleset_to_samples(ruleset) + return samples + +## Read the png file, and parse it to a useable ruleset +func _load_ruleset_file(filename: String) -> PackedInt32Array: + var img: Image = Image.load_from_file(filename) + + img.decompress() + _size = img.get_size() + + var png: PackedByteArray = img.get_data() + var hex: PackedInt32Array = [] + + @warning_ignore("integer_division") + var hex_size: int = (png.size() / 3) + hex.resize(hex_size) + + for i in range(hex_size): #using RGB8 format + var rgb8: int = (png[i * 3] << 16) | (png[i * 3 + 1] << 8) | (png[i * 3 + 2] << 0) + match rgb8: #BUGFIX: remapped RGB to values the tilemap can handle + 0x000000: hex[i] = 1 + 0xFF0000: hex[i] = 2 + 0x00FF00: hex[i] = 3 + 0x0000FF: hex[i] = 4 + _: hex[i] = 1 + + return hex + +## Convert the raw hexcodes to usable WFC samples +func _parse_ruleset_to_samples(ruleset: PackedInt32Array) -> Array[PackedInt32Array]: + #wrapped in a custom container type + var samples: Set = Set.new() + + for x in range(1, _size.x-1): + for y in range(1, _size.y-1): + var sample: PackedInt32Array = [ + ruleset[(y -1) * _size.x + (x -1)], + ruleset[(y -1) * _size.x + (x )], + ruleset[(y -1) * _size.x + (x +1)], + + ruleset[(y ) * _size.x + (x -1)], + ruleset[(y ) * _size.x + (x )], + ruleset[(y ) * _size.x + (x +1)], + + ruleset[(y +1) * _size.x + (x -1)], + ruleset[(y +1) * _size.x + (x )], + ruleset[(y +1) * _size.x + (x +1)], + ] + + samples.add(sample) + + var result: Array[PackedInt32Array] = [] + result.assign(samples.elements()) + return result diff --git a/wfc/rulesets.gd.uid b/wfc/rulesets.gd.uid new file mode 100644 index 0000000..3589207 --- /dev/null +++ b/wfc/rulesets.gd.uid @@ -0,0 +1 @@ +uid://cb6akqmmarmxa