Naive WFC kind of working

This commit is contained in:
2026-01-07 17:40:58 +11:00
commit 7206c1b038
15 changed files with 274 additions and 0 deletions

4
.editorconfig Normal file
View File

@@ -0,0 +1,4 @@
root = true
[*]
charset = utf-8

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Godot 4+ specific ignores
.godot/
/android/

63
client.gd Normal file
View File

@@ -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

1
client.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://jmttidfcknea

1
icon.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

After

Width:  |  Height:  |  Size: 995 B

43
icon.svg.import Normal file
View File

@@ -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

20
project.godot Normal file
View File

@@ -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"

BIN
sample1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

40
sample1.png.import Normal file
View File

@@ -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

12
scene.tscn Normal file
View File

@@ -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")

31
wfc/chunk.gd Normal file
View File

@@ -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

1
wfc/chunk.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://due7w7hripbuk

52
wfc/generator.gd Normal file
View File

@@ -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]

1
wfc/generator.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://bf6phxhnvh0ul