Penrose Tiling in Godot
I’m slowing learning Godot. Since Penrose Tiling is tickling my brain currently, I decided to do something with that. Based on work from this interesting page, here’s a basic sample using GDScript. Some output:
That’s three, four, five subdivisions of the tiles.
Here’s the code:
extends Node2D
# https://preshing.com/20110831/penrose-tiling-explained/ # https://en.wikipedia.org/wiki/Golden_triangle_(mathematics)
const golden_ratio = (1 + sqrt(5)) / 2 const interior_angle = PI/5 const subdivisions = 4 # try setting as 3 - 7
# background 9AC4F8 'baby blue eyes' const colour_one = Color("3F8EFC") # 'brilliant azure' const colour_two = Color("8B2635") # 'japanese carmine'
# Point (inner class) # val is Vector2 (for complex number - x real, y imaginary) class Point: var val var colour func _init(val, colour): self.val = val self.colour = colour func get_val(): return self.val
# Distance between polar coordinates func calc_distance(A, B): # same as sqrt( pow((B.x - A.x), 2) + pow((B.y - A.y), 2) ) return A.distance_to(B) # decimal form
func project(P1, P2, size): var dx = P2.x - P1.x var dy = P2.y - P1.y var magnitude = calc_distance(P1, P2) var x = P1.x + size/magnitude*dx var y = P1.y + size/magnitude*dy return Vector2(x, y)
# Divide into sub-triangles according to P2 Penrose Tiling rules func subdivide(triangles): var result = [] for t in triangles: var colour = t[0] # 0 = acute, 1 = obtuse var A = t[1] var B = t[2] var C = t[3] if colour == 0: # Actute subdivides into two acute and one obtuse triangle # So we introduce two additional vertices, P and Q var pbisect_edge = C var qbisect_edge = B if B.colour == A.colour: pbisect_edge = B qbisect_edge = C # Edge A->P will be distance of longer edges of actue triangle, # or the short side of the triangle var sz = calc_distance(pbisect_edge.val, qbisect_edge.val) var P = project(A.val, pbisect_edge.val, sz) # A->P and A->Q are long and short sides of obtuse # so the size from A->Q must be |A->P|/golden ratio var Q = project(A.val, qbisect_edge.val, sz/golden_ratio)
# Recolour the vertices according to substitution rules var pP = Point.new(P, A.colour) var pQ = Point.new(Q, int(not A.colour)) var pA = Point.new(A.val, int(not A.colour)) var pC = Point.new(qbisect_edge.val, A.colour) var pB = Point.new(pbisect_edge.val, int(not A.colour))
result.append([0, pC, pQ, pP]) result.append([0, pC, pB, pP]) result.append([1, pA, pP, pQ]) else: # Obtuse subdivides into one acute and one obtuse triangle # So we will introduce one additional vertex, P
# Bisect A->C or A->B, ending vertex # to be the opposite colour of the A vertex var bisect_edge = C var unmodified_edge = B if B.colour != A.colour: bisect_edge = B unmodified_edge = C # Size of new edge will be magnitude of A->X/golden_ratio var ne = calc_distance(A.val,bisect_edge.val)/golden_ratio var P = project(A.val, bisect_edge.val, ne) var pP = Point.new(P, bisect_edge.colour)
result.append([1, bisect_edge, pP, unmodified_edge]) result.append([0, A, pP, unmodified_edge]) return result
# Generate polar coordinates of two edges of a Robinson triangle func init_vertex_pair(x, s1, s2): # r * complex( cos(phi), sin(phi) ) var A = s1 * Vector2(cos(x * interior_angle), sin(x * interior_angle)) var Bsinphi = sin((x + 1) * interior_angle) var B = s2 * Vector2(cos((x + 1) * interior_angle), Bsinphi) return [A, B]
func initial_sun(x, size): var triangles = [] for i in range(x): var pair = init_vertex_pair(i, size, size) # Angle of 36 deg var A = pair[0] var B = pair[1] if i % 2 == 0: A = pair[1] B = pair[0] var Pa = Point.new(A, 0) var Pb = Point.new(B, 1) var triangle = [0, Point.new(Vector2(), 0), Pa, Pb] triangles.append(triangle) return triangles
func draw_penrose(triangles, sz): for t in triangles: var points = PoolVector2Array() var colours = PoolColorArray() var colour = colour_one if t[0] == 1: colour = colour_two for tp in range(1, t.size()): var p = t[tp].get_val() var point = Vector2(sz/2 + p.x, sz/2 + p.y) colours.append(colour) points.append(point) draw_polygon(points, colours) draw_polyline(points, Color(0, 0, 0, 1))
func _ready(): OS.set_window_size(Vector2(810, 810)) pass
func _draw(): var t = initial_sun(10, 400) draw_penrose(t, 800) for x in range(subdivisions): t = subdivide(t) draw_penrose(t, 800)
This code is:
Rough but works in Godot v3.1.1
Sprinkled with additional vars etc in attempt to reduce unhelpful line-breaks in formatting on this page :-)
Output with seven subdivisions:












