From 2523700b098f497154cc9821479287799ae5cb26 Mon Sep 17 00:00:00 2001 From: Valentin Stark Date: Thu, 29 Sep 2022 23:57:24 +0200 Subject: [PATCH] =?UTF-8?q?param=C3=A8tres=20+=20pr=C3=A9visualisation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 +++ Poisson.gd | 28 ++++ Poisson.tscn | 92 +++++++++++- Previsualisation.gd | 23 +++ .../PoissonDiscSampling/Demo/PolygonTester.gd | 32 ++++ .../Demo/PolygonTester.tscn | 12 ++ .../PoissonDiscSampling.gd | 137 ++++++++++++++++++ project.godot | 10 ++ 8 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 LICENSE create mode 100644 Poisson.gd create mode 100644 Previsualisation.gd create mode 100644 addons/PoissonDiscSampling/Demo/PolygonTester.gd create mode 100644 addons/PoissonDiscSampling/Demo/PolygonTester.tscn create mode 100644 addons/PoissonDiscSampling/PoissonDiscSampling.gd diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2baf2e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Udit Parmar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Poisson.gd b/Poisson.gd new file mode 100644 index 0000000..986bdbd --- /dev/null +++ b/Poisson.gd @@ -0,0 +1,28 @@ +extends Control + +export var default_width = 500 +export var default_height = 500 +export var default_spacing = 5 +var width = default_width +var height = default_height +var spacing = default_spacing + +var poisson_disc_sampling: PoissonDiscSampling = PoissonDiscSampling.new() +var points = [] + +signal new_poisson(points) + +func _ready(): + $HBoxContainer/Properties/width/width_edit.text = str(default_width) + $HBoxContainer/Properties/height/height_edit.text = str(default_height) + $HBoxContainer/Properties/spacing/spacing_edit.text = str(default_spacing) + pass + +func _on_Button_pressed(): + width = int($HBoxContainer/Properties/width/width_edit.text) + height = int($HBoxContainer/Properties/height/height_edit.text) + spacing = int($HBoxContainer/Properties/spacing/spacing_edit.text) + var rect = Rect2(Vector2(0, 0), Vector2(width, height)) + points = poisson_disc_sampling.generate_points(spacing, rect, 20) + emit_signal("new_poisson", points) + diff --git a/Poisson.tscn b/Poisson.tscn index 00a6290..07c3aa3 100644 --- a/Poisson.tscn +++ b/Poisson.tscn @@ -1,5 +1,93 @@ -[gd_scene format=2] +[gd_scene load_steps=3 format=2] -[node name="Control" type="Control"] +[ext_resource path="res://Poisson.gd" type="Script" id=1] +[ext_resource path="res://Previsualisation.gd" type="Script" id=2] + +[node name="Poisson" type="Control"] anchor_right = 1.0 anchor_bottom = 1.0 +script = ExtResource( 1 ) + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +margin_right = 40.0 +margin_bottom = 40.0 + +[node name="Properties" type="VBoxContainer" parent="HBoxContainer"] +margin_right = 121.0 +margin_bottom = 152.0 + +[node name="width" type="HBoxContainer" parent="HBoxContainer/Properties"] +margin_right = 121.0 +margin_bottom = 24.0 + +[node name="width_label" type="Label" parent="HBoxContainer/Properties/width"] +margin_top = 5.0 +margin_right = 55.0 +margin_bottom = 19.0 +text = "Largeur :" + +[node name="width_edit" type="LineEdit" parent="HBoxContainer/Properties/width"] +margin_left = 59.0 +margin_right = 117.0 +margin_bottom = 24.0 + +[node name="height" type="HBoxContainer" parent="HBoxContainer/Properties"] +margin_top = 28.0 +margin_right = 121.0 +margin_bottom = 52.0 + +[node name="height_label" type="Label" parent="HBoxContainer/Properties/height"] +margin_top = 5.0 +margin_right = 59.0 +margin_bottom = 19.0 +text = "Hauteur :" + +[node name="height_edit" type="LineEdit" parent="HBoxContainer/Properties/height"] +margin_left = 63.0 +margin_right = 121.0 +margin_bottom = 24.0 + +[node name="spacing" type="HBoxContainer" parent="HBoxContainer/Properties"] +margin_top = 56.0 +margin_right = 121.0 +margin_bottom = 80.0 + +[node name="spacing_label" type="Label" parent="HBoxContainer/Properties/spacing"] +margin_top = 5.0 +margin_right = 58.0 +margin_bottom = 19.0 +text = "Densité :" + +[node name="spacing_edit" type="LineEdit" parent="HBoxContainer/Properties/spacing"] +margin_left = 62.0 +margin_right = 120.0 +margin_bottom = 24.0 + +[node name="generate" type="Button" parent="HBoxContainer/Properties"] +margin_top = 84.0 +margin_right = 121.0 +margin_bottom = 104.0 +text = "Générer" + +[node name="open" type="Button" parent="HBoxContainer/Properties"] +margin_top = 108.0 +margin_right = 121.0 +margin_bottom = 128.0 +text = "Ouvrir" + +[node name="save" type="Button" parent="HBoxContainer/Properties"] +margin_top = 132.0 +margin_right = 121.0 +margin_bottom = 152.0 +text = "Enregistrer" + +[node name="ScrollContainer" type="ScrollContainer" parent="."] +margin_left = 125.0 +margin_right = 653.0 +margin_bottom = 502.0 + +[node name="Previsualisation" type="TextureRect" parent="ScrollContainer"] +script = ExtResource( 2 ) + +[connection signal="new_poisson" from="." to="ScrollContainer/Previsualisation" method="_on_Control_new_poisson"] +[connection signal="pressed" from="HBoxContainer/Properties/generate" to="." method="_on_Button_pressed"] diff --git a/Previsualisation.gd b/Previsualisation.gd new file mode 100644 index 0000000..22409ac --- /dev/null +++ b/Previsualisation.gd @@ -0,0 +1,23 @@ +extends TextureRect + +var image + +func create_image(points): + image = Image.new() + image.create(get_parent().get_parent().width, get_parent().get_parent().height, false, Image.FORMAT_RGBA8) + image.fill(Color.white) + image.lock() + for point in points: + image.set_pixel(point.x, point.y, Color.black) + + image.unlock() + +func update_texture(): + var texture = ImageTexture.new() + texture.create_from_image(image) + set_texture(texture) + +func _on_Control_new_poisson(points): + create_image(points) + update_texture() + diff --git a/addons/PoissonDiscSampling/Demo/PolygonTester.gd b/addons/PoissonDiscSampling/Demo/PolygonTester.gd new file mode 100644 index 0000000..c9d95b6 --- /dev/null +++ b/addons/PoissonDiscSampling/Demo/PolygonTester.gd @@ -0,0 +1,32 @@ +extends Node2D + + +onready var polygon: Array = $Polygon2D.polygon +onready var n = polygon.size() + +var radius: int = 20 +var k: int = 0 +var points := [] + + +func _draw() -> void: + for i in n: + draw_line(polygon[i], polygon[(i+1)%n], Color(1,1,0), 2, 1) + + draw_circle(points[k], radius / 2, Color( 1, 0, 0, 1 )) + draw_circle(points[k], 2, Color( 1, 1, 0, 1 )) + +func _ready() -> void: + var pds = PoissonDiscSampling.new() + + var start_time = OS.get_ticks_msec() + points = pds.generate_points(radius, $Polygon2D.polygon, 30) + print(points.size(), " points generated in ", OS.get_ticks_msec() - start_time, " miliseconds" ) + + get_viewport().render_target_clear_mode = Viewport.UPDATE_ONCE + + +func _process(delta: float) -> void: + if k < points.size() - 1: + update() + k += 1 diff --git a/addons/PoissonDiscSampling/Demo/PolygonTester.tscn b/addons/PoissonDiscSampling/Demo/PolygonTester.tscn new file mode 100644 index 0000000..0f4b6d1 --- /dev/null +++ b/addons/PoissonDiscSampling/Demo/PolygonTester.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/PoissonDiscSampling/Demo/PolygonTester.gd" type="Script" id=1] + +[node name="PolygonTester" type="Node2D"] +script = ExtResource( 1 ) + +[node name="Polygon2D" type="Polygon2D" parent="."] +position = Vector2( -0.568787, 0 ) +color = Color( 0.921569, 1, 0, 0 ) +antialiased = true +polygon = PoolVector2Array( 1652.09, 404.164, 1020.9, 114.599, 791.346, 514.627, 474.889, 192.896, 357.097, 495.288, 277.983, 75.1039, 70.5279, 184.106, 35.3661, 625.387, 239.305, 918.988, 156.674, 609.564, 126.787, 249.155, 300.838, 609.564, 581.675, 703.491, 506.535, 345.85, 833.54, 739.663, 1089.23, 313.065, 1499.18, 501.771, 1495.92, 836.886, 1356.02, 862.915, 1050.19, 882.436, 1144.54, 583.11, 1277.93, 599.377, 1398.32, 758.801, 1378.79, 573.349, 1125.02, 443.207, 907.03, 934.493, 1609.8, 983.296 ) diff --git a/addons/PoissonDiscSampling/PoissonDiscSampling.gd b/addons/PoissonDiscSampling/PoissonDiscSampling.gd new file mode 100644 index 0000000..a47ea72 --- /dev/null +++ b/addons/PoissonDiscSampling/PoissonDiscSampling.gd @@ -0,0 +1,137 @@ +class_name PoissonDiscSampling + +var _radius: float +var _sample_region_shape +var _retries: int +var _start_pos: Vector2 +var _sample_region_rect: Rect2 +var _cell_size: float +var _rows: int +var _cols: int +var _cell_size_scaled: Vector2 +var _grid: Array = [] +var _points: Array = [] +var _spawn_points: Array = [] +var _transpose: Vector2 + +# radius - minimum distance between points +# sample_region_shape - takes any of the following: +# -a Rect2 for rectangular region +# -an array of Vector2 for polygon region +# -a Vector3 with x,y as the position and z as the radius of the circle +# retries - maximum number of attempts to look around a sample point, reduce this value to speed up generation +# start_pos - optional parameter specifying the starting point +# +# returns an Array of Vector2D with points in the order of their discovery +func generate_points(radius: float, sample_region_shape, retries: int, start_pos := Vector2(INF, INF)) -> Array: + _radius = radius + _sample_region_shape = sample_region_shape + _retries = retries + _start_pos = start_pos + _init_vars() + + while _spawn_points.size() > 0: + var spawn_index: int = randi() % _spawn_points.size() + var spawn_centre: Vector2 = _spawn_points[spawn_index] + var sample_accepted: bool = false + for i in retries: + var angle: float = 2 * PI * randf() + var sample: Vector2 = spawn_centre + Vector2(cos(angle), sin(angle)) * (radius + radius * randf()) + if _is_valid_sample(sample): + _grid[int((_transpose.x + sample.x) / _cell_size_scaled.x)][int((_transpose.y + sample.y) / _cell_size_scaled.y)] = _points.size() + _points.append(sample) + _spawn_points.append(sample) + sample_accepted = true + break + if not sample_accepted: + _spawn_points.remove(spawn_index) + return _points + + +func _is_valid_sample(sample: Vector2) -> bool: + if _is_point_in_sample_region(sample): + var cell := Vector2(int((_transpose.x + sample.x) / _cell_size_scaled.x), int((_transpose.y + sample.y) / _cell_size_scaled.y)) + var cell_start := Vector2(max(0, cell.x - 2), max(0, cell.y - 2)) + var cell_end := Vector2(min(cell.x + 2, _cols - 1), min(cell.y + 2, _rows - 1)) + + for i in range(cell_start.x, cell_end.x + 1): + for j in range(cell_start.y, cell_end.y + 1): + var search_index: int = _grid[i][j] + if search_index != -1: + var dist: float = _points[search_index].distance_to(sample) + if dist < _radius: + return false + return true + return false + + +func _is_point_in_sample_region(sample: Vector2) -> bool: + if _sample_region_rect.has_point(sample): + match typeof(_sample_region_shape): + TYPE_RECT2: + return true + TYPE_VECTOR2_ARRAY, TYPE_ARRAY: + if Geometry.is_point_in_polygon(sample, _sample_region_shape): + return true + TYPE_VECTOR3: + if Geometry.is_point_in_circle(sample, Vector2(_sample_region_shape.x, _sample_region_shape.y), _sample_region_shape.z): + return true + _: + return false + return false + +func _init_vars() -> void: + randomize() + + # identify the type of shape and it's bounding rectangle and starting point + match typeof(_sample_region_shape): + TYPE_RECT2: + _sample_region_rect = _sample_region_shape + if _start_pos.x == INF: + _start_pos.x = _sample_region_rect.position.x + _sample_region_rect.size.x * randf() + _start_pos.y = _sample_region_rect.position.y + _sample_region_rect.size.y * randf() + + TYPE_VECTOR2_ARRAY, TYPE_ARRAY: + var start: Vector2 = _sample_region_shape[0] + var end: Vector2 = _sample_region_shape[0] + for i in range(1, _sample_region_shape.size()): + start.x = min(start.x, _sample_region_shape[i].x) + start.y = min(start.y, _sample_region_shape[i].y) + end.x = max(end.x, _sample_region_shape[i].x) + end.y = max(end.y, _sample_region_shape[i].y) + _sample_region_rect = Rect2(start, end - start) + if _start_pos.x == INF: + var n: int = _sample_region_shape.size() + var i: int = randi() % n + _start_pos = _sample_region_shape[i] + (_sample_region_shape[(i + 1) % n] - _sample_region_shape[i]) * randf() + + TYPE_VECTOR3: + var x = _sample_region_shape.x + var y = _sample_region_shape.y + var r = _sample_region_shape.z + _sample_region_rect = Rect2(x - r, y - r, r * 2, r * 2) + if _start_pos.x == INF: + var angle: float = 2 * PI * randf() + _start_pos = Vector2(x, y) + Vector2(cos(angle), sin(angle)) * r * randf() + _: + _sample_region_shape = Rect2(0, 0, 0, 0) + push_error("Unrecognized shape!!! Please input a valid shape") + + _cell_size = _radius / sqrt(2) + _cols = max(floor(_sample_region_rect.size.x / _cell_size), 1) + _rows = max(floor(_sample_region_rect.size.y / _cell_size), 1) + # scale the cell size in each axis + _cell_size_scaled.x = _sample_region_rect.size.x / _cols + _cell_size_scaled.y = _sample_region_rect.size.y / _rows + # use tranpose to map points starting from origin to calculate grid position + _transpose = -_sample_region_rect.position + + _grid = [] + for i in _cols: + _grid.append([]) + for j in _rows: + _grid[i].append(-1) + + _points = [] + _spawn_points = [] + _spawn_points.append(_start_pos) diff --git a/project.godot b/project.godot index 3fc760b..af74a52 100644 --- a/project.godot +++ b/project.godot @@ -8,6 +8,16 @@ config_version=4 +_global_script_classes=[ { +"base": "Reference", +"class": "PoissonDiscSampling", +"language": "GDScript", +"path": "res://addons/PoissonDiscSampling/PoissonDiscSampling.gd" +} ] +_global_script_class_icons={ +"PoissonDiscSampling": "" +} + [application] config/name="Societer Utils"