Marching squares

Le contour d'un champ de bruit, redessiné pendant que le seuil balaie. Le champ ne change jamais, seule la coupe change.

L'éditeur ci-dessus fait tourner ce projet. Changez une ligne et il se recharge.

Classes Godot utilisées

Node2DFastNoiseLiteLabel

Le code

scripts/main.gd
extends Node

const COLUMNS: int = 32
const ROWS: int = 18
const SEED: int = 20260814

# marching squares case table, each pair of edges is one segment
const CASES: Array = [
	[], [3, 0], [0, 1], [3, 1], [1, 2], [3, 0, 1, 2], [0, 2], [3, 2],
	[2, 3], [2, 0], [0, 1, 2, 3], [2, 1], [1, 3], [1, 0], [0, 3], [],
]

var samples: PackedFloat32Array = PackedFloat32Array()
var canvas: Node2D
var label: Label
var origin: Vector2
var cell: float
var threshold: float = 0.5
var elapsed: float = 0.0


func _ready() -> void:
	_add_background()

	var noise: FastNoiseLite = FastNoiseLite.new()
	noise.seed = SEED
	noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
	noise.fractal_octaves = 3
	noise.frequency = 0.09

	for y in ROWS + 1:
		for x in COLUMNS + 1:
			samples.append(noise.get_noise_2d(x, y) * 0.5 + 0.5)

	var view: Vector2 = get_viewport().get_visible_rect().size
	cell = minf(view.x / COLUMNS, view.y / ROWS)
	origin = (view - Vector2(COLUMNS, ROWS) * cell) * 0.5

	canvas = Node2D.new()
	# draw_line only works while the canvas item is drawing
	canvas.draw.connect(_redraw)
	add_child(canvas)

	label = Label.new()
	label.position = Vector2(40, 32)
	label.add_theme_color_override('font_color', Color('#e6ecff'))
	label.add_theme_color_override('font_shadow_color', Color('#12141a'))
	label.add_theme_constant_override('shadow_offset_y', 2)
	add_child(label)

	print('the field never changes, only the threshold the contour follows')


func _process(delta: float) -> void:
	elapsed += delta
	threshold = 0.5 + sin(elapsed * 0.6) * 0.16
	label.text = 'threshold %.3f' % threshold
	canvas.queue_redraw()


func _redraw() -> void:
	for y in ROWS + 1:
		for x in COLUMNS + 1:
			var inside: bool = _sample(x, y) > threshold
			canvas.draw_circle(_point(x, y), 4.0 if inside else 2.5, Color('#8da5f3') if inside else Color('#39415a'))

	for y in ROWS:
		for x in COLUMNS:
			var pattern: int = 0
			pattern |= 1 if _sample(x, y) > threshold else 0
			pattern |= 2 if _sample(x + 1, y) > threshold else 0
			pattern |= 4 if _sample(x + 1, y + 1) > threshold else 0
			pattern |= 8 if _sample(x, y + 1) > threshold else 0

			var edges: Array = CASES[pattern]

			for i in range(0, edges.size(), 2):
				canvas.draw_line(_edge_point(edges[i], x, y), _edge_point(edges[i + 1], x, y), Color('#8eef97'), 3.0)


func _edge_point(edge: int, x: int, y: int) -> Vector2:
	match edge:
		0:
			return _mix(x, y, x + 1, y)
		1:
			return _mix(x + 1, y, x + 1, y + 1)
		2:
			return _mix(x + 1, y + 1, x, y + 1)

	return _mix(x, y + 1, x, y)


func _mix(ax: int, ay: int, bx: int, by: int) -> Vector2:
	var a: float = _sample(ax, ay)
	var b: float = _sample(bx, by)
	var weight: float = 0.5 if is_equal_approx(a, b) else clampf(inverse_lerp(a, b, threshold), 0.0, 1.0)

	return _point(ax, ay).lerp(_point(bx, by), weight)


func _sample(x: int, y: int) -> float:
	return samples[y * (COLUMNS + 1) + x]


func _point(x: int, y: int) -> Vector2:
	return origin + Vector2(x, y) * cell


func _add_background() -> void:
	var background: ColorRect = ColorRect.new()
	background.color = Color('#12141a')
	add_child(background)
	background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)

Plus d'exemples de Procédural