From 7206c1b038c8c33daac0e02655e9a450bf04eee1 Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Wed, 7 Jan 2026 17:40:58 +1100 Subject: [PATCH] Naive WFC kind of working --- .editorconfig | 4 +++ .gitattributes | 2 ++ .gitignore | 3 +++ client.gd | 63 +++++++++++++++++++++++++++++++++++++++++++ client.gd.uid | 1 + icon.svg | 1 + icon.svg.import | 43 +++++++++++++++++++++++++++++ project.godot | 20 ++++++++++++++ sample1.png | Bin 0 -> 102 bytes sample1.png.import | 40 +++++++++++++++++++++++++++ scene.tscn | 12 +++++++++ wfc/chunk.gd | 31 +++++++++++++++++++++ wfc/chunk.gd.uid | 1 + wfc/generator.gd | 52 +++++++++++++++++++++++++++++++++++ wfc/generator.gd.uid | 1 + 15 files changed, 274 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 client.gd create mode 100644 client.gd.uid create mode 100644 icon.svg create mode 100644 icon.svg.import create mode 100644 project.godot create mode 100644 sample1.png create mode 100644 sample1.png.import create mode 100644 scene.tscn create mode 100644 wfc/chunk.gd create mode 100644 wfc/chunk.gd.uid create mode 100644 wfc/generator.gd create mode 100644 wfc/generator.gd.uid diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/client.gd b/client.gd new file mode 100644 index 0000000..b172200 --- /dev/null +++ b/client.gd @@ -0,0 +1,63 @@ +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") + +func _ready() -> void: + var ruleset: PackedInt32Array = read_sample_ruleset("sample1.png") + var samples: Array[PackedInt32Array] = parse_samples_from_ruleset(ruleset) + + #print(ruleset) + #print(samples) + wfc.generate_chunk_at(0,0,_chunks,samples) + +## 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 + + var samples: Array[PackedInt32Array] = [] + + 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.append(sample) + + return samples diff --git a/client.gd.uid b/client.gd.uid new file mode 100644 index 0000000..6eae30c --- /dev/null +++ b/client.gd.uid @@ -0,0 +1 @@ +uid://jmttidfcknea diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..c6bbb7d --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..fa6d245 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ba3kljgd5yvc2" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..22939e0 --- /dev/null +++ b/project.godot @@ -0,0 +1,20 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Wave" +run/main_scene="uid://d2g4ooi0x6ebo" +config/features=PackedStringArray("4.5", "GL Compatibility") +config/icon="res://icon.svg" + +[rendering] + +renderer/rendering_method="mobile" diff --git a/sample1.png b/sample1.png new file mode 100644 index 0000000000000000000000000000000000000000..598a7bfebde465a04fd0443789baaa9f32a08dcb GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^T~8Os5RU7~fByfsXEr;)vQ$#? yqJ)peB99^;9w5kA_V@q!0|yRxoZ!>)U|_II=Hfjr^702zBZH@_pUXO@geCy`;vJU& literal 0 HcmV?d00001 diff --git a/sample1.png.import b/sample1.png.import new file mode 100644 index 0000000..1c1fcb4 --- /dev/null +++ b/sample1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dy1xsv1acvc4i" +path="res://.godot/imported/sample1.png-8b828765c2f6336dd2e8e4a034a21542.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://sample1.png" +dest_files=["res://.godot/imported/sample1.png-8b828765c2f6336dd2e8e4a034a21542.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 new file mode 100644 index 0000000..93fc410 --- /dev/null +++ b/scene.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=3 format=3 uid="uid://d2g4ooi0x6ebo"] + +[ext_resource type="Script" uid="uid://bf6phxhnvh0ul" path="res://wfc/generator.gd" id="1_ulcgi"] +[ext_resource type="Script" uid="uid://jmttidfcknea" path="res://client.gd" id="2_nxogm"] + +[node name="Scene" type="Node2D"] + +[node name="Generator" type="Node" parent="."] +script = ExtResource("1_ulcgi") + +[node name="Client" type="Node" parent="."] +script = ExtResource("2_nxogm") diff --git a/wfc/chunk.gd b/wfc/chunk.gd new file mode 100644 index 0000000..183dadd --- /dev/null +++ b/wfc/chunk.gd @@ -0,0 +1,31 @@ +class_name Chunk extends Object +## A Chunk of map data and bundled metadata, always aligned to CHUNK_WIDTH and CHUNK_HEIGHT +## The default data values are 0, which means "NOT SET" +## Chunks can be partially unset, to allow for prefabs +## Any values of -1 are "NOT VALID" + +const CHUNK_WIDTH: int = 16 +const CHUNK_HEIGHT: int = 16 + +var data: PackedInt32Array +var x: int +var y: int + +func _init(_x: int, _y: int): + assert(_x % CHUNK_WIDTH == 0) + assert(_y % CHUNK_HEIGHT == 0) + x = _x + y = _y + data = PackedInt32Array() + data.resize(CHUNK_WIDTH * CHUNK_HEIGHT) + data.fill(0) + +func get_tile(tile_x: int, tile_y: int) -> int: + if tile_x < 0 or tile_y < 0 or tile_x >= CHUNK_WIDTH or tile_y >= CHUNK_HEIGHT: return -1 + return data[tile_y * CHUNK_WIDTH + tile_x] + +func set_tile(tile_x: int, tile_y: int, value: int) -> int: + assert(value > 0) #do NOT clear a tile with this function + if tile_x < 0 or tile_y < 0 or tile_x >= CHUNK_WIDTH or tile_y >= CHUNK_HEIGHT: return -1 + data[tile_y * CHUNK_WIDTH + tile_x] = value + return value diff --git a/wfc/chunk.gd.uid b/wfc/chunk.gd.uid new file mode 100644 index 0000000..c9f2115 --- /dev/null +++ b/wfc/chunk.gd.uid @@ -0,0 +1 @@ +uid://due7w7hripbuk diff --git a/wfc/generator.gd b/wfc/generator.gd new file mode 100644 index 0000000..0a68ba3 --- /dev/null +++ b/wfc/generator.gd @@ -0,0 +1,52 @@ +extends Node +## The generator works with pure numbers, you'll need something else to convert it to actual tiles + +## Makes a new chunk, derived from the given WFC samples +func generate_chunk_at(_x: int, _y: int, _chunk_array: Array[Chunk], _samples: Array[PackedInt32Array]) -> Chunk: + #TODO: fix the chunk-edges + var chunk: Chunk = Chunk.new(_x, _y) + + var latch: int = 1 + while latch: + latch = 0 #if set to true, the generator needs another pass + #iterate over unset tiles with valid neighbours + for x in range(Chunk.CHUNK_WIDTH): + for y in range((Chunk.CHUNK_HEIGHT)): + if chunk.data[y * Chunk.CHUNK_HEIGHT + x] == 0: #direct access, to skip extra checks + #TODO: convert this to the low-entropy method + if _set_tile_from_samples(chunk, x, y, _samples) > 0: + latch += 1 + print("latch: ", latch) + + print(chunk.data) + _chunk_array.append(chunk) + return chunk + +## Returns the value set, or -1 if no options found +func _set_tile_from_samples(chunk: Chunk, tile_x: int, tile_y: int, _samples: Array[PackedInt32Array]) -> int: + var valid: Array[PackedInt32Array] = [] + + #use a lambda for easy reading below + var compare := func (tile_value: int, sample: int) -> bool: + return tile_value <= 0 or tile_value == sample + + #find all valid samples + for sample in _samples: + if !compare.call(chunk.get_tile(tile_x -1, tile_y -1), sample[0]): continue + if !compare.call(chunk.get_tile(tile_x , tile_y -1), sample[1]): continue + if !compare.call(chunk.get_tile(tile_x +1, tile_y -1), sample[2]): continue + if !compare.call(chunk.get_tile(tile_x -1, tile_y ), sample[3]): continue + # // + if !compare.call(chunk.get_tile(tile_x +1, tile_y ), sample[5]): continue + if !compare.call(chunk.get_tile(tile_x -1, tile_y +1), sample[6]): continue + if !compare.call(chunk.get_tile(tile_x , tile_y +1), sample[7]): continue + if !compare.call(chunk.get_tile(tile_x +1, tile_y +1), sample[8]): continue + + valid.append(sample) + print("Valid samples: ", valid.size()) + if valid.size() <= 0: return -1 + var s = valid.pick_random() + + #set the tile to the randomly selected value + chunk.set_tile(tile_x, tile_y, s[4]) + return s[4] diff --git a/wfc/generator.gd.uid b/wfc/generator.gd.uid new file mode 100644 index 0000000..c60c4a2 --- /dev/null +++ b/wfc/generator.gd.uid @@ -0,0 +1 @@ +uid://bf6phxhnvh0ul