From 831d75dcf4630454ea5809d0803cd9c01a191a9f Mon Sep 17 00:00:00 2001 From: kro-ma Date: Tue, 29 Apr 2025 09:19:27 +0200 Subject: [PATCH 01/35] Added file and directory stubs for boids simulation --- demo/demos/boids_simulation/gd_solver.gd | 53 ++++++ demo/demos/boids_simulation/main.tscn | 194 ++++++++++++++++++++++ demo/demos/boids_simulation/metadata.json | 6 + demo/demos/boids_simulation/nd_solver.gd | 71 ++++++++ demo/demos/boids_simulation/sirs_model.gd | 123 ++++++++++++++ demo/demos/boids_simulation/solver.gd | 16 ++ 6 files changed, 463 insertions(+) create mode 100644 demo/demos/boids_simulation/gd_solver.gd create mode 100644 demo/demos/boids_simulation/main.tscn create mode 100644 demo/demos/boids_simulation/metadata.json create mode 100644 demo/demos/boids_simulation/nd_solver.gd create mode 100644 demo/demos/boids_simulation/sirs_model.gd create mode 100644 demo/demos/boids_simulation/solver.gd diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd new file mode 100644 index 00000000..c0ad313e --- /dev/null +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -0,0 +1,53 @@ +extends SIRSolver + +@onready var grid: Array +@onready var gridp: Array + +func initialize() -> void: + grid = create_matrix(params.N) + + # boundary conditions + grid[0].fill(params.tau0 + 1) + grid[-1].fill(params.tau0 + 1) + + for arr in grid: + arr[0] = params.tau0 + 1 + arr[-1] = params.tau0 + 1 + + place_random() + gridp = grid.duplicate(true) + +func simulation_step() -> void: + var infp := 0. + + for i in range(1, params.N-1): + for j in range(1, params.N-1): + if gridp[i][j] == 0: + infp = [gridp[i+1][j], gridp[i-1][j], gridp[i][j-1], gridp[i][j+1]].map(func(elt): return (elt > 0) and (elt <= params.tauI)).count(true) / 4. + if randf() < infp * params.spread: + grid[i][j] = 1 + elif gridp[i][j] < params.tau0: + grid[i][j] += 1 + elif gridp[i][j] == params.tau0: + grid[i][j] = 0 + + gridp = grid.duplicate(true) + +func on_draw() -> void: + for i in params.N: + for j in params.N: + params._image.set_pixel(i, j, params.colors[grid[i][j]]) + + params.update_texture() + +func create_matrix(N: int) -> Array: + var matrix := [] + for x in range(N): + matrix.append([]) + for y in range(N): + matrix[x].append(0) + + return matrix + +func place_random() -> void: + grid[randi_range(1, params.N-2)][randi_range(1, params.N-2)] = 1 diff --git a/demo/demos/boids_simulation/main.tscn b/demo/demos/boids_simulation/main.tscn new file mode 100644 index 00000000..ceeba77b --- /dev/null +++ b/demo/demos/boids_simulation/main.tscn @@ -0,0 +1,194 @@ +[gd_scene load_steps=5 format=3 uid="uid://bujidut18snxp"] + +[ext_resource type="Script" path="res://demos/sirs_model/sirs_model.gd" id="1_ioo0u"] +[ext_resource type="Script" path="res://demos/sirs_model/gd_solver.gd" id="2_onaa5"] +[ext_resource type="Script" path="res://demos/sirs_model/nd_solver.gd" id="3_p3baa"] + +[sub_resource type="ImageTexture" id="ImageTexture_03ndn"] + +[node name="SIRSModel" type="Node2D" node_paths=PackedStringArray("solver", "texture_rect")] +script = ExtResource("1_ioo0u") +solver = NodePath("Solvers/GDSolver") +N = 30 +tauI = 8 +tauR = 9 +color_susceptible = Color(0.803922, 0.803922, 0.776471, 1) +color_infected = Color(0.737255, 0.223529, 0.133333, 1) +color_recovered = Color(0.921569, 0.584314, 0.14902, 1) +texture_rect = NodePath("CanvasLayer/TextureRect") + +[node name="Solvers" type="Node" parent="."] + +[node name="GDSolver" type="Node" parent="Solvers" node_paths=PackedStringArray("params")] +script = ExtResource("2_onaa5") +params = NodePath("../..") + +[node name="NDSolver" type="Node" parent="Solvers" node_paths=PackedStringArray("params")] +script = ExtResource("3_p3baa") +params = NodePath("../..") + +[node name="ColorRect" type="ColorRect" parent="."] +show_behind_parent = true +custom_minimum_size = Vector2(1152, 648) +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +offset_right = 1152.0 +offset_bottom = 648.0 +grow_horizontal = 2 +color = Color(0.0431373, 0.0745098, 0.101961, 1) + +[node name="Labels" type="VBoxContainer" parent="."] +offset_left = 23.0 +offset_top = 18.0 +offset_right = 223.0 +offset_bottom = 118.0 +metadata/_edit_group_ = true + +[node name="FPSLabel" type="Label" parent="Labels"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 30 +text = "FPS: 60" + +[node name="FrameTimeLabel" type="Label" parent="Labels"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 30 +text = "Frame time: 1" + +[node name="SliderOptions" type="VBoxContainer" parent="."] +custom_minimum_size = Vector2(250, 0) +offset_left = 22.0 +offset_top = 365.0 +offset_right = 272.0 +offset_bottom = 625.0 +metadata/_edit_group_ = true + +[node name="PointLabel" type="Label" parent="SliderOptions"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 30 +text = "Grid: 20x20" + +[node name="PointSlider" type="HSlider" parent="SliderOptions"] +unique_name_in_owner = true +layout_mode = 2 +min_value = 10.0 +max_value = 250.0 +value = 20.0 + +[node name="SpreadLabel" type="Label" parent="SliderOptions"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 30 +text = "Spread: 1" + +[node name="SpreadSlider" type="HSlider" parent="SliderOptions"] +unique_name_in_owner = true +layout_mode = 2 +min_value = 0.01 +max_value = 4.0 +step = 0.0 +value = 1.0 +exp_edit = true + +[node name="InfectedLabel" type="Label" parent="SliderOptions"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 30 +text = "Infection time: 4" + +[node name="InfectedSlider" type="HSlider" parent="SliderOptions"] +unique_name_in_owner = true +layout_mode = 2 +min_value = 1.0 +value = 4.0 +exp_edit = true + +[node name="RecoveryLabel" type="Label" parent="SliderOptions"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 30 +text = "Recovery time: 6" + +[node name="RecoverySlider" type="HSlider" parent="SliderOptions"] +unique_name_in_owner = true +layout_mode = 2 +min_value = 1.0 +value = 6.0 +exp_edit = true + +[node name="RestartButton" type="Button" parent="."] +offset_left = 865.0 +offset_top = 79.0 +offset_right = 1140.0 +offset_bottom = 129.0 +theme_override_font_sizes/font_size = 30 +text = "Restart +" + +[node name="SolverOption" type="OptionButton" parent="."] +offset_left = 865.0 +offset_top = 23.0 +offset_right = 1137.0 +offset_bottom = 73.0 +theme_override_font_sizes/font_size = 30 +selected = 0 +item_count = 2 +popup/item_0/text = "GDScript" +popup/item_1/text = "NumDot" +popup/item_1/id = 1 + +[node name="CanvasLayer" type="CanvasLayer" parent="."] + +[node name="TextureRect" type="TextureRect" parent="CanvasLayer"] +texture_filter = 1 +offset_right = 40.0 +offset_bottom = 40.0 +size_flags_horizontal = 4 +size_flags_vertical = 4 +texture = SubResource("ImageTexture_03ndn") + +[node name="LegendInfected" type="TextureRect" parent="."] +offset_left = 288.0 +offset_top = 541.0 +offset_right = 308.0 +offset_bottom = 561.0 + +[node name="LegendRecovered" type="TextureRect" parent="."] +offset_left = 288.0 +offset_top = 607.0 +offset_right = 308.0 +offset_bottom = 627.0 + +[node name="SolverOptions" type="VBoxContainer" parent="."] +custom_minimum_size = Vector2(200, 0) +offset_left = 940.0 +offset_top = 570.0 +offset_right = 1140.0 +offset_bottom = 635.0 +metadata/_edit_group_ = true + +[node name="StepsPerSecondLabel" type="Label" parent="SolverOptions"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 30 +text = "Speed: 20" + +[node name="StepsPerSecondSlider" type="HSlider" parent="SolverOptions"] +unique_name_in_owner = true +layout_mode = 2 +min_value = 0.1 +max_value = 10000.0 +step = 0.5 +value = 30.1 +exp_edit = true + +[connection signal="drag_ended" from="SliderOptions/PointSlider" to="." method="_on_point_slider_drag_ended"] +[connection signal="drag_ended" from="SliderOptions/SpreadSlider" to="." method="_on_spread_slider_drag_ended"] +[connection signal="drag_ended" from="SliderOptions/InfectedSlider" to="." method="_on_infected_slider_drag_ended"] +[connection signal="drag_ended" from="SliderOptions/RecoverySlider" to="." method="_on_recovery_slider_drag_ended"] +[connection signal="pressed" from="RestartButton" to="." method="_on_restart_button_pressed"] +[connection signal="item_selected" from="SolverOption" to="." method="_on_solver_option_item_selected"] +[connection signal="drag_ended" from="SolverOptions/StepsPerSecondSlider" to="." method="_on_steps_per_second_slider_drag_ended"] diff --git a/demo/demos/boids_simulation/metadata.json b/demo/demos/boids_simulation/metadata.json new file mode 100644 index 00000000..1803fcd7 --- /dev/null +++ b/demo/demos/boids_simulation/metadata.json @@ -0,0 +1,6 @@ +{ + "name": "Boids Simulation", + "description": "A simulation of flocking behavior modeled on how birds and other animals move collectively.", + "link": "https://en.wikipedia.org/wiki/Boids", + "authors": "versjon, kro-ma" +} diff --git a/demo/demos/boids_simulation/nd_solver.gd b/demo/demos/boids_simulation/nd_solver.gd new file mode 100644 index 00000000..3f27abba --- /dev/null +++ b/demo/demos/boids_simulation/nd_solver.gd @@ -0,0 +1,71 @@ +extends SIRSolver + +var grid_time_since_infection: NDArray +var grid_can_infect_neighbor: NDArray +var grid_is_infectable: NDArray +var grid_infected_neighbor_count: NDArray +var grid_infected_neighbor_count_inner: NDArray +var grid_infected_neighbor_ratio: NDArray +var grid_new_infected_this_step: NDArray + +var neighbor_kernel: NDArray +var rng := nd.default_rng() + +func initialize() -> void: + var grid_size := [params.N, params.N] + + grid_time_since_infection = nd.zeros(grid_size, nd.Int64) + grid_time_since_infection.set(params.tau0 + 1) + grid_can_infect_neighbor = nd.zeros(grid_size, nd.Bool) + grid_is_infectable = nd.zeros(grid_size, nd.Bool) + grid_infected_neighbor_count = nd.zeros(grid_size, nd.Int64) + grid_infected_neighbor_count_inner = grid_infected_neighbor_count.get(nd.range(1, -1), nd.range(1, -1)) + grid_infected_neighbor_ratio = nd.zeros(grid_size, nd.Float64) + grid_new_infected_this_step = nd.zeros(grid_size, nd.Bool) + + neighbor_kernel = nd.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]], nd.Int64); + + # random initial infection + place_random() + set_border_color() + +func simulation_step() -> void: + # Infect + grid_can_infect_neighbor.assign_less_equal(grid_time_since_infection, params.tauI) + grid_is_infectable.assign_greater(grid_time_since_infection, params.tau0) + + grid_infected_neighbor_count_inner.assign_convolve(grid_can_infect_neighbor, neighbor_kernel) + grid_infected_neighbor_ratio.assign_divide(grid_infected_neighbor_count, 4.0) + + grid_infected_neighbor_ratio.assign_multiply(grid_infected_neighbor_ratio, params.spread) + grid_new_infected_this_step.assign_less(rng.random(grid_time_since_infection.shape()), grid_infected_neighbor_ratio) + grid_new_infected_this_step.assign_logical_and(grid_new_infected_this_step, grid_is_infectable) + + grid_time_since_infection.set(0, grid_new_infected_this_step) + + # Time Pass + grid_time_since_infection.assign_add(grid_time_since_infection, 1) + # clamp value (not strictly necessary given Int64, but just for completeness) + grid_time_since_infection.assign_minimum(grid_time_since_infection, params.tau0 + 1) + +func on_draw() -> void: + for i in range(1, params.N-1): + for j in range(1, params.N-1): + var color_idx = grid_time_since_infection.get_int(i, j) + if color_idx > params.tau0: + color_idx = 0 + params._image.set_pixel(i, j, params.colors[color_idx]) + + params.update_texture() + +func place_random() -> void: + grid_time_since_infection.set(0, randi_range(1, params.N-2), randi_range(1, params.N-2)) + +func set_border_color() -> void: + for i in params.N: + params._image.set_pixel(i, params.N-1, Color.BLACK) + params._image.set_pixel(i, 0, Color.BLACK) + params._image.set_pixel(params.N-1, i, Color.BLACK) + params._image.set_pixel(0, i, Color.BLACK) + + pass diff --git a/demo/demos/boids_simulation/sirs_model.gd b/demo/demos/boids_simulation/sirs_model.gd new file mode 100644 index 00000000..d7396b83 --- /dev/null +++ b/demo/demos/boids_simulation/sirs_model.gd @@ -0,0 +1,123 @@ +extends Node2D + +@export_category("Simulation parameters") +@export var solver: SIRSolver + +@export var N: int = 20 +@export var spread: float = 1.0 +@export var tauI: int = 4 +@export var tauR: int = 6 +@onready var tau0 := tauI + tauR + +@export var steps_per_second: float = 20.0 +var current_step: float = 0.0 + +@export_category("Visual parameters") +@export var colors = [] + +@export var color_susceptible: Color = Color.WHITE +@export var color_infected: Color = Color.RED +@export var color_recovered: Color = Color.ORANGE + +@export var texture_rect: TextureRect +var _image: Image + +var frame_time: float = 0. + +func _ready() -> void: + %PointSlider.value = N + %PointLabel.text = "Grid: " + str(N) + "x" + str(N) + + %SpreadSlider.value = spread + %SpreadLabel.text = "Spread: %.3f" % spread + + %InfectedSlider.value = tauI + %InfectedLabel.text = "Infection time: " + str(tauI) + + %RecoverySlider.value = tauR + %RecoveryLabel.text = "Recovery time: " + str(tauR) + + generate_colors() + resize_image() + create_legend() + + solver.initialize() + +func _process(delta: float) -> void: + + %FPSLabel.text = "FPS: " + str(Engine.get_frames_per_second()) + + var next_step := current_step + delta * steps_per_second + for i in (int(next_step) - int(current_step)): + frame_time = Time.get_ticks_usec() + solver.simulation_step() + frame_time = Time.get_ticks_usec() - frame_time + current_step = next_step + + %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) + solver.on_draw() + +func generate_colors() -> void: + colors = [] + colors.push_back(color_susceptible) + for i in tauI: colors.push_back(color_infected) + for i in tauR: colors.push_back(color_recovered) + colors.push_back(Color.BLACK) + +func _on_point_slider_drag_ended(value_changed: bool) -> void: + N = %PointSlider.value + %PointLabel.text = "Grid: " + str(N) + "x" + str(N) + resize_image() + solver.initialize() + +func _on_spread_slider_drag_ended(value_changed: bool) -> void: + spread = %SpreadSlider.value + %SpreadLabel.text = "Spread: %.3f" % spread + solver.initialize() + +func _on_infected_slider_drag_ended(value_changed: bool) -> void: + tauI = %InfectedSlider.value + %InfectedLabel.text = str("Infection time: " + str(tauI)) + tau0 = tauI + tauR + solver.initialize() + generate_colors() + +func _on_recovery_slider_drag_ended(value_changed: bool) -> void: + tauR = %RecoverySlider.value + %RecoveryLabel.text = str("Recovery time: " + str(tauR)) + tau0 = tauI + tauR + solver.initialize() + generate_colors() + +func _on_steps_per_second_slider_drag_ended(value_changed: bool) -> void: + steps_per_second = %StepsPerSecondSlider.value + %StepsPerSecondLabel.text = "Speed: %.2f" % steps_per_second + +func _on_restart_button_pressed() -> void: + solver.initialize() + +func _on_solver_option_item_selected(index: int) -> void: + solver = $Solvers.get_child(index) + solver.initialize() + +func update_texture() -> void: + texture_rect.texture.update(_image) + +func resize_image() -> void: + _image = Image.create(N, N, false, Image.FORMAT_RGBA8) + + texture_rect.texture = ImageTexture.create_from_image(_image) + texture_rect.set_size(Vector2(500, 500)) + var origin = get_viewport_rect().get_center() - texture_rect.size/2 + texture_rect.position = origin + +func create_legend() -> void: + var _img = Image.create(1, 1, false, Image.FORMAT_RGBA8) + + # infected + _img.set_pixel(0, 0, color_infected) + $LegendInfected.texture = ImageTexture.create_from_image(_img) + + # recovered + _img.set_pixel(0, 0, color_recovered) + $LegendRecovered.texture = ImageTexture.create_from_image(_img) diff --git a/demo/demos/boids_simulation/solver.gd b/demo/demos/boids_simulation/solver.gd new file mode 100644 index 00000000..e533b3ed --- /dev/null +++ b/demo/demos/boids_simulation/solver.gd @@ -0,0 +1,16 @@ +extends Node +class_name SIRSolver + +@export var params: Node2D + +func initialize() -> void: + pass + +func simulation_step() -> void: + pass + +func on_draw() -> void: + pass + +func place_random() -> void: + pass From 582c5d29ae3fcda3c8611ba15b96d30857a5f503 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 8 May 2025 18:23:58 +0200 Subject: [PATCH 02/35] Added boid scene and started structuring boids_simulation demo scene --- .gitignore | 3 + demo/demos/boids_simulation/boid.png | Bin 0 -> 3635 bytes demo/demos/boids_simulation/boid.png.import | 34 ++++++ demo/demos/boids_simulation/boid.tscn | 20 ++++ demo/demos/boids_simulation/boids_model.gd | 32 +++++ demo/demos/boids_simulation/gd_solver.gd | 64 +++------- demo/demos/boids_simulation/main.tscn | 87 ++++++++------ demo/demos/boids_simulation/nd_solver.gd | 72 +----------- demo/demos/boids_simulation/sirs_model.gd | 123 -------------------- demo/demos/boids_simulation/solver.gd | 9 +- demo/demos/game_of_life/game_of_life.gd | 6 +- demo/demos/game_of_life/nd_solver.gd | 8 +- demo/demos/game_of_life/solver.gd | 2 +- demo/demos/kuramoto_model/kuramoto_model.gd | 16 +-- demo/demos/kuramoto_model/solver.gd | 4 +- demo/demos/launcher/launcher.gd | 4 +- demo/demos/sirs_model/gd_solver.gd | 10 +- demo/demos/sirs_model/sirs_model.gd | 16 +-- demo/demos/sirs_model/solver.gd | 2 +- demo/demos/wave_equation/gd_solver.gd | 12 +- demo/demos/wave_equation/solver.gd | 2 +- demo/demos/wave_equation/wave_equation.gd | 16 +-- 22 files changed, 211 insertions(+), 331 deletions(-) create mode 100644 demo/demos/boids_simulation/boid.png create mode 100644 demo/demos/boids_simulation/boid.png.import create mode 100644 demo/demos/boids_simulation/boid.tscn create mode 100644 demo/demos/boids_simulation/boids_model.gd delete mode 100644 demo/demos/boids_simulation/sirs_model.gd diff --git a/.gitignore b/.gitignore index 3093fe03..81533fd7 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ __pycache__ # Custom config /custom.py /numdot_config.py + +# Program Debug Database +*.pdb \ No newline at end of file diff --git a/demo/demos/boids_simulation/boid.png b/demo/demos/boids_simulation/boid.png new file mode 100644 index 0000000000000000000000000000000000000000..1a8d7c76fa721bd2bb4941e7cdceb4d10cc01f24 GIT binary patch literal 3635 zcmXw+dsGwG7RKkngn}br!FwG$3Zepn+9Y#TP$a3(suU3wvA%-9q)Jh)_^vglR*Uv( zi7y&31$?xk;uI0dPlj6{>Ocy=uwt?c@Ei4O)QWx&CsNn!mbY=mKEApkM_tMG5!e43ic|FSl19F?8CF~L(s6+OE|?wy7-&^olPdPo07bqJ^=40Jqyn6cP*t8k4Noo*BgBQ~e=TL5Od`kx{ssZ{vtzw{d~7yXe@f zKj7?44lL#1B_mbX(uOl^;L+N(xKh6)s{X1gLoX(F%2BK>VA7E`ERbMCI>^@BF z#AjF=!=sTCTwEemuL{K4drWLaA=UzU)O&)9(^2(GHP#5m*z^!<6px%D<_72~`v~k8 z5WyI47GcfKqgx{828fh>IQ9!rF~){gtX1-;EYQWJQ1+hvur`P>PTz&K13WSZ-p5=r zW%nC|wSJ6o>aSSa$fI?EuAE7f-B*P*jxkQD$67Lv<_5N4ZXsoVAJ0CZ2VEQO3+d6y2wu1%qdMC$~>o`mhBZyZXGBu5NJgPabbu2EQh3Xh@@)y zl@)#ECxtr<{(>&RUB`Ep=X1)<9?I{WO*XdIIC5)0IZH(={!Lsk%2fcFh$M@A&x%Mt zt-&Glf{t)6&~zH|xXU>nm-C$(8~ZSt>=-B?Rna3eh&Marh8(g_4)JBR{KSgB^V4b^ za$iw9(F+GrqBc0_UEo>Z-D&txDDZX1fkr5xecuI4x~8)!le_#Cje{&MUGTlT-cjx^ z+!a0V&Uci52!WS$#%?f((za;yMYY^zMIQ`8Z!NM4f(YW(cKM!(9HAoH`pZ=+ddWVb zt)E<_rkDIl+#1`JrB~dN+)idX{cY^Yuc(tTUA9D6A#sqTn43(^4VGI}^w2n>!6N@= zMgQSZs$E_uN=%S6%wdQU?ov#8o-^wldr~69r*>5(D$L20Iat1@q+{ZU7K@x6LK=^gGvze&ErUc0-gXc?C&P4h_uJ zooUZ=+N#+b=g8Fnr6|f0sWO1n^qpAZXtjLHibn9L!ERVDO5K8_REI_*++~<{v-9XV zb~QjTT@8tf$V4hKSbn0U^J0mtYWY(u3g%Ij-7s2|=pZS{Q4T$kOnX@8f}LDDqH*v& z*S^i;T|m8_+|?i|*4-o44V0ec2IfSNKUST2) z(+Q15J`O@QPIPF3%0wyOmUjSFjJHs5TEaHE@*TJlABQuo3ma~G@o!7(k|N2 zHE-n9KjMntfG~J89Y2)lw3QBtqT{0oN50s|6+1xq*Tq^ivzxZ-dE+rbca08^JC{^* z#g9Q*cf!sl{6g3@S%V|!_()*%*?Y*a7|0@Ayo9cK0#vh1&>aV8js6f<{0Bf~CG~8= zA%MOZJdBPX3DBAB95U<^faYJcqL~0?RH_BtZh#^dZ{dn-0OF5VvI%xfUJv1?Lkxgo1^TZEZ*d9SX|S5ATcG-+_7b<1Mx@9Sdno`cP}?JE_@d9XF4t`-%6&6WXAS<70mxpKfv&JKpHxx0jbB(Mpb96_sb zH!~iI+6!Q_CG<9H!QEKuE}_rC#&_Of&WO9&H)R;DhHiqto*@KW1)JpUq9Pf0Gq}!* z27^uYfJ>|echh*lM9zk8g7^3d0b{{NJy}Jop_{4k&3f%OU{f9HWH;k(tmkd$A=vDk z_mK0!-5l#Snhu0+4sN(C1USHEMvs+>ak!h%^R=iDYz_@*WjEt)u5C1v)1aG!xke!X z&nDC}f)2#p%xo65m0)uy^cK4rcT;oz68a2mrq3(nd~i31yA7iQ@obpAqV@pXsM+I9 zoX;Xq;(LgSaf9IGC!e>X5MWAjeS`pf*NTEF+0C(#t>-2a*~1IqnOF7Njo>r;S_h{* z4JC!`iK0b(*B<{=iyGnHl$<&w=tQW^6iJIvnztr;4=F&=nf^arLbIWr=7Wm_-F0ZR z{n{>0nFya{u@#=2|4VK-JprDa_gi)kxgTCn#rg_SD#VbfpP;J;DBWu0l=}eU zMAQ1A*vpEPinO8}bNutL#+o!YLXDLTyW zf>5AeJsM80?Sq5IYI-d^;r|DI(D&OFIpkBBU>>gE8U>qe_qp^J1Pta$9eZ*lY1dd+EKI$LweFEC`jQ;SApC}yc%I*p-*n_PPvhMwMMSC7EZ%Vml(Jdn;aX7VV8dgs-m zSs3a*RWFqQ6kBzbYi|Xp@P0G9dN@G!j&wyNhF(OP$t(=zuFoN}@ThRm1h-=BymBjg z1_wJfZGkAgf-~P+3O^+>U>t?dF0!xJ!K8|s^SSmlFsXzJ_(=k9Epvrap#OkD6=!Qs zJZ*uiRGeL2n)cSV)ZgE~eCf@X void: + # Set correct values in GUI + + # Initialize boids as scenes from res://demos/boids_simulation/boid.tscn + # Add border around screen redirecting boids to screen center + + #solver.initialize() + pass + +func _process(delta: float) -> void: + # Call simulation_step(delta) + # Update GUI + + pass + + +# TODO handle GUI inputs diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index c0ad313e..b19fc4de 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -1,53 +1,27 @@ -extends SIRSolver +extends BoidsSolver -@onready var grid: Array -@onready var gridp: Array +# parameters (speed, boids, etc.) defined in params +# params is set to BoidsModel and can be modified in Editor and in boids_model.gd func initialize() -> void: - grid = create_matrix(params.N) + # create position vector and initialize with random Vector2s inside screen + # create velocity vector and initialize with Vector2s of same value in random directions - # boundary conditions - grid[0].fill(params.tau0 + 1) - grid[-1].fill(params.tau0 + 1) - - for arr in grid: - arr[0] = params.tau0 + 1 - arr[-1] = params.tau0 + 1 + pass - place_random() - gridp = grid.duplicate(true) +func simulation_step(delta: float) -> void: + # for each boid collect others in visual range + # calculate new velocity directions according to: + # Seperation + # Alignment + # Cohesion + # (Additional noise) + # apply velocities to positions -func simulation_step() -> void: - var infp := 0. - - for i in range(1, params.N-1): - for j in range(1, params.N-1): - if gridp[i][j] == 0: - infp = [gridp[i+1][j], gridp[i-1][j], gridp[i][j-1], gridp[i][j+1]].map(func(elt): return (elt > 0) and (elt <= params.tauI)).count(true) / 4. - if randf() < infp * params.spread: - grid[i][j] = 1 - elif gridp[i][j] < params.tau0: - grid[i][j] += 1 - elif gridp[i][j] == params.tau0: - grid[i][j] = 0 + pass - gridp = grid.duplicate(true) +func update_boids() -> void: + # apply position from position vector to each boid + # derive and apply rotation from velocity vector direction to each boid -func on_draw() -> void: - for i in params.N: - for j in params.N: - params._image.set_pixel(i, j, params.colors[grid[i][j]]) - - params.update_texture() - -func create_matrix(N: int) -> Array: - var matrix := [] - for x in range(N): - matrix.append([]) - for y in range(N): - matrix[x].append(0) - - return matrix - -func place_random() -> void: - grid[randi_range(1, params.N-2)][randi_range(1, params.N-2)] = 1 + pass diff --git a/demo/demos/boids_simulation/main.tscn b/demo/demos/boids_simulation/main.tscn index ceeba77b..55c8a43b 100644 --- a/demo/demos/boids_simulation/main.tscn +++ b/demo/demos/boids_simulation/main.tscn @@ -1,30 +1,23 @@ [gd_scene load_steps=5 format=3 uid="uid://bujidut18snxp"] -[ext_resource type="Script" path="res://demos/sirs_model/sirs_model.gd" id="1_ioo0u"] -[ext_resource type="Script" path="res://demos/sirs_model/gd_solver.gd" id="2_onaa5"] -[ext_resource type="Script" path="res://demos/sirs_model/nd_solver.gd" id="3_p3baa"] +[ext_resource type="Script" path="res://demos/boids_simulation/boids_model.gd" id="1_2fw6l"] +[ext_resource type="Script" path="res://demos/boids_simulation/gd_solver.gd" id="2_yi4li"] +[ext_resource type="Script" path="res://demos/boids_simulation/nd_solver.gd" id="3_av8qh"] [sub_resource type="ImageTexture" id="ImageTexture_03ndn"] -[node name="SIRSModel" type="Node2D" node_paths=PackedStringArray("solver", "texture_rect")] -script = ExtResource("1_ioo0u") -solver = NodePath("Solvers/GDSolver") -N = 30 -tauI = 8 -tauR = 9 -color_susceptible = Color(0.803922, 0.803922, 0.776471, 1) -color_infected = Color(0.737255, 0.223529, 0.133333, 1) -color_recovered = Color(0.921569, 0.584314, 0.14902, 1) -texture_rect = NodePath("CanvasLayer/TextureRect") +[node name="BoidsModel" type="Node2D" node_paths=PackedStringArray("params")] +script = ExtResource("1_2fw6l") +params = NodePath(".") [node name="Solvers" type="Node" parent="."] [node name="GDSolver" type="Node" parent="Solvers" node_paths=PackedStringArray("params")] -script = ExtResource("2_onaa5") +script = ExtResource("2_yi4li") params = NodePath("../..") [node name="NDSolver" type="Node" parent="Solvers" node_paths=PackedStringArray("params")] -script = ExtResource("3_p3baa") +script = ExtResource("3_av8qh") params = NodePath("../..") [node name="ColorRect" type="ColorRect" parent="."] @@ -60,29 +53,16 @@ text = "Frame time: 1" [node name="SliderOptions" type="VBoxContainer" parent="."] custom_minimum_size = Vector2(250, 0) offset_left = 22.0 -offset_top = 365.0 +offset_top = 432.0 offset_right = 272.0 -offset_bottom = 625.0 +offset_bottom = 626.0 metadata/_edit_group_ = true -[node name="PointLabel" type="Label" parent="SliderOptions"] -unique_name_in_owner = true -layout_mode = 2 -theme_override_font_sizes/font_size = 30 -text = "Grid: 20x20" - -[node name="PointSlider" type="HSlider" parent="SliderOptions"] -unique_name_in_owner = true -layout_mode = 2 -min_value = 10.0 -max_value = 250.0 -value = 20.0 - [node name="SpreadLabel" type="Label" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Spread: 1" +text = "Separation: 1" [node name="SpreadSlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true @@ -97,7 +77,7 @@ exp_edit = true unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Infection time: 4" +text = "Alignment: 4" [node name="InfectedSlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true @@ -110,7 +90,7 @@ exp_edit = true unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Recovery time: 6" +text = "Cohesion: 6" [node name="RecoverySlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true @@ -164,10 +144,10 @@ offset_bottom = 627.0 [node name="SolverOptions" type="VBoxContainer" parent="."] custom_minimum_size = Vector2(200, 0) -offset_left = 940.0 -offset_top = 570.0 -offset_right = 1140.0 -offset_bottom = 635.0 +offset_left = 902.0 +offset_top = 432.0 +offset_right = 1130.0 +offset_bottom = 626.0 metadata/_edit_group_ = true [node name="StepsPerSecondLabel" type="Label" parent="SolverOptions"] @@ -185,10 +165,41 @@ step = 0.5 value = 30.1 exp_edit = true -[connection signal="drag_ended" from="SliderOptions/PointSlider" to="." method="_on_point_slider_drag_ended"] +[node name="StepsPerSecondLabel2" type="Label" parent="SolverOptions"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 30 +text = "Boids: 20" + +[node name="StepsPerSecondSlider2" type="HSlider" parent="SolverOptions"] +unique_name_in_owner = true +layout_mode = 2 +min_value = 0.1 +max_value = 10000.0 +step = 0.5 +value = 30.1 +exp_edit = true + +[node name="StepsPerSecondLabel3" type="Label" parent="SolverOptions"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 30 +text = "Visual range: 20" + +[node name="StepsPerSecondSlider3" type="HSlider" parent="SolverOptions"] +unique_name_in_owner = true +layout_mode = 2 +min_value = 0.1 +max_value = 10000.0 +step = 0.5 +value = 30.1 +exp_edit = true + [connection signal="drag_ended" from="SliderOptions/SpreadSlider" to="." method="_on_spread_slider_drag_ended"] [connection signal="drag_ended" from="SliderOptions/InfectedSlider" to="." method="_on_infected_slider_drag_ended"] [connection signal="drag_ended" from="SliderOptions/RecoverySlider" to="." method="_on_recovery_slider_drag_ended"] [connection signal="pressed" from="RestartButton" to="." method="_on_restart_button_pressed"] [connection signal="item_selected" from="SolverOption" to="." method="_on_solver_option_item_selected"] [connection signal="drag_ended" from="SolverOptions/StepsPerSecondSlider" to="." method="_on_steps_per_second_slider_drag_ended"] +[connection signal="drag_ended" from="SolverOptions/StepsPerSecondSlider2" to="." method="_on_steps_per_second_slider_drag_ended"] +[connection signal="drag_ended" from="SolverOptions/StepsPerSecondSlider3" to="." method="_on_steps_per_second_slider_drag_ended"] diff --git a/demo/demos/boids_simulation/nd_solver.gd b/demo/demos/boids_simulation/nd_solver.gd index 3f27abba..a2b95b26 100644 --- a/demo/demos/boids_simulation/nd_solver.gd +++ b/demo/demos/boids_simulation/nd_solver.gd @@ -1,71 +1,3 @@ -extends SIRSolver +extends BoidsSolver -var grid_time_since_infection: NDArray -var grid_can_infect_neighbor: NDArray -var grid_is_infectable: NDArray -var grid_infected_neighbor_count: NDArray -var grid_infected_neighbor_count_inner: NDArray -var grid_infected_neighbor_ratio: NDArray -var grid_new_infected_this_step: NDArray - -var neighbor_kernel: NDArray -var rng := nd.default_rng() - -func initialize() -> void: - var grid_size := [params.N, params.N] - - grid_time_since_infection = nd.zeros(grid_size, nd.Int64) - grid_time_since_infection.set(params.tau0 + 1) - grid_can_infect_neighbor = nd.zeros(grid_size, nd.Bool) - grid_is_infectable = nd.zeros(grid_size, nd.Bool) - grid_infected_neighbor_count = nd.zeros(grid_size, nd.Int64) - grid_infected_neighbor_count_inner = grid_infected_neighbor_count.get(nd.range(1, -1), nd.range(1, -1)) - grid_infected_neighbor_ratio = nd.zeros(grid_size, nd.Float64) - grid_new_infected_this_step = nd.zeros(grid_size, nd.Bool) - - neighbor_kernel = nd.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]], nd.Int64); - - # random initial infection - place_random() - set_border_color() - -func simulation_step() -> void: - # Infect - grid_can_infect_neighbor.assign_less_equal(grid_time_since_infection, params.tauI) - grid_is_infectable.assign_greater(grid_time_since_infection, params.tau0) - - grid_infected_neighbor_count_inner.assign_convolve(grid_can_infect_neighbor, neighbor_kernel) - grid_infected_neighbor_ratio.assign_divide(grid_infected_neighbor_count, 4.0) - - grid_infected_neighbor_ratio.assign_multiply(grid_infected_neighbor_ratio, params.spread) - grid_new_infected_this_step.assign_less(rng.random(grid_time_since_infection.shape()), grid_infected_neighbor_ratio) - grid_new_infected_this_step.assign_logical_and(grid_new_infected_this_step, grid_is_infectable) - - grid_time_since_infection.set(0, grid_new_infected_this_step) - - # Time Pass - grid_time_since_infection.assign_add(grid_time_since_infection, 1) - # clamp value (not strictly necessary given Int64, but just for completeness) - grid_time_since_infection.assign_minimum(grid_time_since_infection, params.tau0 + 1) - -func on_draw() -> void: - for i in range(1, params.N-1): - for j in range(1, params.N-1): - var color_idx = grid_time_since_infection.get_int(i, j) - if color_idx > params.tau0: - color_idx = 0 - params._image.set_pixel(i, j, params.colors[color_idx]) - - params.update_texture() - -func place_random() -> void: - grid_time_since_infection.set(0, randi_range(1, params.N-2), randi_range(1, params.N-2)) - -func set_border_color() -> void: - for i in params.N: - params._image.set_pixel(i, params.N-1, Color.BLACK) - params._image.set_pixel(i, 0, Color.BLACK) - params._image.set_pixel(params.N-1, i, Color.BLACK) - params._image.set_pixel(0, i, Color.BLACK) - - pass +# TODO diff --git a/demo/demos/boids_simulation/sirs_model.gd b/demo/demos/boids_simulation/sirs_model.gd deleted file mode 100644 index d7396b83..00000000 --- a/demo/demos/boids_simulation/sirs_model.gd +++ /dev/null @@ -1,123 +0,0 @@ -extends Node2D - -@export_category("Simulation parameters") -@export var solver: SIRSolver - -@export var N: int = 20 -@export var spread: float = 1.0 -@export var tauI: int = 4 -@export var tauR: int = 6 -@onready var tau0 := tauI + tauR - -@export var steps_per_second: float = 20.0 -var current_step: float = 0.0 - -@export_category("Visual parameters") -@export var colors = [] - -@export var color_susceptible: Color = Color.WHITE -@export var color_infected: Color = Color.RED -@export var color_recovered: Color = Color.ORANGE - -@export var texture_rect: TextureRect -var _image: Image - -var frame_time: float = 0. - -func _ready() -> void: - %PointSlider.value = N - %PointLabel.text = "Grid: " + str(N) + "x" + str(N) - - %SpreadSlider.value = spread - %SpreadLabel.text = "Spread: %.3f" % spread - - %InfectedSlider.value = tauI - %InfectedLabel.text = "Infection time: " + str(tauI) - - %RecoverySlider.value = tauR - %RecoveryLabel.text = "Recovery time: " + str(tauR) - - generate_colors() - resize_image() - create_legend() - - solver.initialize() - -func _process(delta: float) -> void: - - %FPSLabel.text = "FPS: " + str(Engine.get_frames_per_second()) - - var next_step := current_step + delta * steps_per_second - for i in (int(next_step) - int(current_step)): - frame_time = Time.get_ticks_usec() - solver.simulation_step() - frame_time = Time.get_ticks_usec() - frame_time - current_step = next_step - - %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) - solver.on_draw() - -func generate_colors() -> void: - colors = [] - colors.push_back(color_susceptible) - for i in tauI: colors.push_back(color_infected) - for i in tauR: colors.push_back(color_recovered) - colors.push_back(Color.BLACK) - -func _on_point_slider_drag_ended(value_changed: bool) -> void: - N = %PointSlider.value - %PointLabel.text = "Grid: " + str(N) + "x" + str(N) - resize_image() - solver.initialize() - -func _on_spread_slider_drag_ended(value_changed: bool) -> void: - spread = %SpreadSlider.value - %SpreadLabel.text = "Spread: %.3f" % spread - solver.initialize() - -func _on_infected_slider_drag_ended(value_changed: bool) -> void: - tauI = %InfectedSlider.value - %InfectedLabel.text = str("Infection time: " + str(tauI)) - tau0 = tauI + tauR - solver.initialize() - generate_colors() - -func _on_recovery_slider_drag_ended(value_changed: bool) -> void: - tauR = %RecoverySlider.value - %RecoveryLabel.text = str("Recovery time: " + str(tauR)) - tau0 = tauI + tauR - solver.initialize() - generate_colors() - -func _on_steps_per_second_slider_drag_ended(value_changed: bool) -> void: - steps_per_second = %StepsPerSecondSlider.value - %StepsPerSecondLabel.text = "Speed: %.2f" % steps_per_second - -func _on_restart_button_pressed() -> void: - solver.initialize() - -func _on_solver_option_item_selected(index: int) -> void: - solver = $Solvers.get_child(index) - solver.initialize() - -func update_texture() -> void: - texture_rect.texture.update(_image) - -func resize_image() -> void: - _image = Image.create(N, N, false, Image.FORMAT_RGBA8) - - texture_rect.texture = ImageTexture.create_from_image(_image) - texture_rect.set_size(Vector2(500, 500)) - var origin = get_viewport_rect().get_center() - texture_rect.size/2 - texture_rect.position = origin - -func create_legend() -> void: - var _img = Image.create(1, 1, false, Image.FORMAT_RGBA8) - - # infected - _img.set_pixel(0, 0, color_infected) - $LegendInfected.texture = ImageTexture.create_from_image(_img) - - # recovered - _img.set_pixel(0, 0, color_recovered) - $LegendRecovered.texture = ImageTexture.create_from_image(_img) diff --git a/demo/demos/boids_simulation/solver.gd b/demo/demos/boids_simulation/solver.gd index e533b3ed..2fd207c1 100644 --- a/demo/demos/boids_simulation/solver.gd +++ b/demo/demos/boids_simulation/solver.gd @@ -1,16 +1,13 @@ extends Node -class_name SIRSolver +class_name BoidsSolver @export var params: Node2D func initialize() -> void: pass -func simulation_step() -> void: - pass - -func on_draw() -> void: +func simulation_step(delta: float) -> void: pass -func place_random() -> void: +func update_boids() -> void: pass diff --git a/demo/demos/game_of_life/game_of_life.gd b/demo/demos/game_of_life/game_of_life.gd index 35361590..e8e39721 100644 --- a/demo/demos/game_of_life/game_of_life.gd +++ b/demo/demos/game_of_life/game_of_life.gd @@ -16,9 +16,9 @@ var frame_time: float = 0. func _ready() -> void: %PointSlider.value = N %PointLabel.text = "Grid: " + str(N) + "x" + str(N) - + resize_image() - + solver.initialize() func _process(delta: float) -> void: @@ -30,7 +30,7 @@ func _process(delta: float) -> void: solver.simulation_step() frame_time = Time.get_ticks_usec() - frame_time current_step = next_step - + %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) solver.on_draw() diff --git a/demo/demos/game_of_life/nd_solver.gd b/demo/demos/game_of_life/nd_solver.gd index 3704d997..b9703ec9 100644 --- a/demo/demos/game_of_life/nd_solver.gd +++ b/demo/demos/game_of_life/nd_solver.gd @@ -11,21 +11,21 @@ var rng := nd.default_rng() func initialize() -> void: var grid_size := [params.N + 2, params.N + 2] - + is_alive = nd.zeros(grid_size, nd.Bool) is_alive_inner = is_alive.get(nd.range(1, -1), nd.range(1, -1)) image_data = nd.empty_like(is_alive_inner) neighour_count = nd.zeros_like(is_alive_inner, nd.Int8) tmp_inner = nd.empty_like(is_alive_inner, nd.Bool) - + neighbor_kernel = nd.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]], nd.Int8); place_random() - + func simulation_step() -> void: neighour_count.assign_convolve(is_alive, neighbor_kernel) - + # This uses masks, not sure if that's the best performing action here. # Boolean operations may be accelerated better! is_alive_inner.set(false, tmp_inner.assign_less(neighour_count, 2)) diff --git a/demo/demos/game_of_life/solver.gd b/demo/demos/game_of_life/solver.gd index 12292ee8..73ef4738 100644 --- a/demo/demos/game_of_life/solver.gd +++ b/demo/demos/game_of_life/solver.gd @@ -8,7 +8,7 @@ func initialize() -> void: func simulation_step() -> void: pass - + func on_draw() -> void: pass diff --git a/demo/demos/kuramoto_model/kuramoto_model.gd b/demo/demos/kuramoto_model/kuramoto_model.gd index 31d90106..41044a6b 100644 --- a/demo/demos/kuramoto_model/kuramoto_model.gd +++ b/demo/demos/kuramoto_model/kuramoto_model.gd @@ -30,7 +30,7 @@ func _ready() -> void: %MeanLabel.text = "Frequency mean: " + str(%MeanSlider.value) %StdSlider.value = frequency_sigma %StdLabel.text = "Frequency std: " + str(%StdSlider.value) - + _restart_simulation() func _process(delta: float) -> void: @@ -39,8 +39,8 @@ func _process(delta: float) -> void: frame_time = Time.get_ticks_usec() solver.simulation_step() frame_time = Time.get_ticks_usec() - frame_time - - %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) + + %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) solver.on_draw() #func _draw() -> void: @@ -89,14 +89,14 @@ func _generate_fireflies() -> void: TEXTURE_SIZE = int(ceil(sqrt(N))) fireflies_data = Image.create(TEXTURE_SIZE, TEXTURE_SIZE, false, Image.FORMAT_RGBAH) fireflies_texture = ImageTexture.create_from_image(fireflies_data) - + fireflies.amount = N fireflies.process_material.set_shader_parameter("fireflies_data", fireflies_texture) - + positions.resize(N) for i in N: positions[i] = Vector2(randf_range(0, get_viewport_rect().size.x), randf_range(0, get_viewport_rect().size.y)) - + func _randomize_positions() -> void: for i in N: positions[i].x = randf_range(0, get_viewport_rect().size.x) @@ -110,12 +110,12 @@ func set_alphas(alphas: Variant) -> void: pixel_pos.x, pixel_pos.y, Color(positions[i].x, positions[i].y, sin(alphas[i])/2 + 0.5, 0) ) - + elif alphas is NDArray: for i in alphas.size(): var pixel_pos := Vector2(int(i / TEXTURE_SIZE), int(i % TEXTURE_SIZE)) fireflies_data.set_pixel( - pixel_pos.x, pixel_pos.y, + pixel_pos.x, pixel_pos.y, Color(positions[i].x, positions[i].y, alphas.get_float(i), 0) ) diff --git a/demo/demos/kuramoto_model/solver.gd b/demo/demos/kuramoto_model/solver.gd index 43b8bd97..1b5dbf78 100644 --- a/demo/demos/kuramoto_model/solver.gd +++ b/demo/demos/kuramoto_model/solver.gd @@ -9,12 +9,12 @@ func initialize() -> void: func simulation_step() -> void: pass - + func on_draw() -> void: pass func generate_frequencies() -> void: pass - + func set_integrator(idx: int) -> void: pass diff --git a/demo/demos/launcher/launcher.gd b/demo/demos/launcher/launcher.gd index 678a4c4d..ce2b39cf 100644 --- a/demo/demos/launcher/launcher.gd +++ b/demo/demos/launcher/launcher.gd @@ -13,7 +13,7 @@ func _ready() -> void: card.set_data(demo) demo_list.add_child(card) demo_list.add_spacer(false) - + func _process(delta: float) -> void: pass @@ -34,6 +34,6 @@ func read_json(path: String) -> void: print(data["path"] + " missing main.tscn!") else: print("JSON file (" + path + ") is missing required keys (name, description, link).") - + func _on_texture_button_pressed() -> void: OS.shell_open("https://godotengine.org/asset-library/asset/3351") diff --git a/demo/demos/sirs_model/gd_solver.gd b/demo/demos/sirs_model/gd_solver.gd index c0ad313e..563ec741 100644 --- a/demo/demos/sirs_model/gd_solver.gd +++ b/demo/demos/sirs_model/gd_solver.gd @@ -9,17 +9,17 @@ func initialize() -> void: # boundary conditions grid[0].fill(params.tau0 + 1) grid[-1].fill(params.tau0 + 1) - + for arr in grid: - arr[0] = params.tau0 + 1 + arr[0] = params.tau0 + 1 arr[-1] = params.tau0 + 1 place_random() gridp = grid.duplicate(true) func simulation_step() -> void: - var infp := 0. - + var infp := 0. + for i in range(1, params.N-1): for j in range(1, params.N-1): if gridp[i][j] == 0: @@ -39,7 +39,7 @@ func on_draw() -> void: params._image.set_pixel(i, j, params.colors[grid[i][j]]) params.update_texture() - + func create_matrix(N: int) -> Array: var matrix := [] for x in range(N): diff --git a/demo/demos/sirs_model/sirs_model.gd b/demo/demos/sirs_model/sirs_model.gd index d7396b83..b7d587d7 100644 --- a/demo/demos/sirs_model/sirs_model.gd +++ b/demo/demos/sirs_model/sirs_model.gd @@ -30,17 +30,17 @@ func _ready() -> void: %SpreadSlider.value = spread %SpreadLabel.text = "Spread: %.3f" % spread - + %InfectedSlider.value = tauI %InfectedLabel.text = "Infection time: " + str(tauI) - + %RecoverySlider.value = tauR %RecoveryLabel.text = "Recovery time: " + str(tauR) - + generate_colors() resize_image() create_legend() - + solver.initialize() func _process(delta: float) -> void: @@ -53,7 +53,7 @@ func _process(delta: float) -> void: solver.simulation_step() frame_time = Time.get_ticks_usec() - frame_time current_step = next_step - + %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) solver.on_draw() @@ -102,7 +102,7 @@ func _on_solver_option_item_selected(index: int) -> void: func update_texture() -> void: texture_rect.texture.update(_image) - + func resize_image() -> void: _image = Image.create(N, N, false, Image.FORMAT_RGBA8) @@ -113,11 +113,11 @@ func resize_image() -> void: func create_legend() -> void: var _img = Image.create(1, 1, false, Image.FORMAT_RGBA8) - + # infected _img.set_pixel(0, 0, color_infected) $LegendInfected.texture = ImageTexture.create_from_image(_img) - + # recovered _img.set_pixel(0, 0, color_recovered) $LegendRecovered.texture = ImageTexture.create_from_image(_img) diff --git a/demo/demos/sirs_model/solver.gd b/demo/demos/sirs_model/solver.gd index e533b3ed..a7f3c7a1 100644 --- a/demo/demos/sirs_model/solver.gd +++ b/demo/demos/sirs_model/solver.gd @@ -8,7 +8,7 @@ func initialize() -> void: func simulation_step() -> void: pass - + func on_draw() -> void: pass diff --git a/demo/demos/wave_equation/gd_solver.gd b/demo/demos/wave_equation/gd_solver.gd index 8c7559e4..3b0eb531 100644 --- a/demo/demos/wave_equation/gd_solver.gd +++ b/demo/demos/wave_equation/gd_solver.gd @@ -9,7 +9,7 @@ func initialize() -> void: x = range(params.num_points).map(func(elt): return (params.dx * elt + params.xmin)) u = params.u.duplicate() uprev = params.uprev.duplicate() - + tmp.resize(params.num_points) func simulation_step(delta: float) -> void: @@ -18,21 +18,21 @@ func simulation_step(delta: float) -> void: for n in params.num_steps_per_frame: for i in range(1, u.size()-1): tmp[i] = 2 * (1 - rsq) * u[i] - uprev[i] + (rsq * (u[i-1] + u[i+1])) - + # boundary condition tmp[0] = tmp[1] if (params.bc_left) else (2 * (1 - rsq) * u[0] - uprev[0] + rsq * u[1]) tmp[-1] = tmp[-2] if (params.bc_right) else (2 * (1 - rsq) * u[-1] - uprev[-1] + rsq * u[-2]) - + uprev = u.duplicate() - u = tmp.duplicate() + u = tmp.duplicate() func on_draw() -> void: for idx in params.draw_array.size(): params.draw_array[idx].x = params.xscale * x[params.draw_range[idx]] params.draw_array[idx].y = params.yscale * u[params.draw_range[idx]] - + params.draw_polyline(params.draw_array, params.point_color, params.point_size) - + if params.bc_left: params.draw_circle(Vector2(x[0], u[0] * params.yscale), params.anchor_size, params.anchor_color) diff --git a/demo/demos/wave_equation/solver.gd b/demo/demos/wave_equation/solver.gd index 9dc04e06..6b9c5aaa 100644 --- a/demo/demos/wave_equation/solver.gd +++ b/demo/demos/wave_equation/solver.gd @@ -8,6 +8,6 @@ func initialize() -> void: func simulation_step(delta: float) -> void: pass - + func on_draw() -> void: pass diff --git a/demo/demos/wave_equation/wave_equation.gd b/demo/demos/wave_equation/wave_equation.gd index 0b75a9fb..dd7dfdf9 100644 --- a/demo/demos/wave_equation/wave_equation.gd +++ b/demo/demos/wave_equation/wave_equation.gd @@ -24,7 +24,7 @@ extends Node2D # simulation parameters computed @onready var dx: float = (xmax - xmin)/num_points @onready var num_steps_per_frame: int = max(1, ceili(wave_speed/ dx / frame_rate)) # to satisfy CFL condition - + @export var init_params = [ {"x0": 0.5, "sig": 0.005, "amplitude": -2.}, {"mode": 5, "amplitude": 1.}, @@ -60,7 +60,7 @@ func _process(delta: float) -> void: frame_time = Time.get_ticks_usec() solver.simulation_step(delta) frame_time = Time.get_ticks_usec() - frame_time - + %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) queue_redraw() @@ -72,11 +72,11 @@ func _draw() -> void: func _on_solver_option_item_selected(index: int) -> void: solver = $Solvers.get_child(index) restart_simulation() - + func _on_point_slider_drag_ended(value_changed: bool) -> void: %PointLabel.text = "Points: " + str(%PointSlider.value) %CFLLabel.text = "CFL: " + str(snappedf(wave_speed * 1/(frame_rate * num_steps_per_frame * dx), 1e-3)) - + num_points = %PointSlider.value dx = (xmax - xmin)/num_points num_steps_per_frame = max(1, ceili(wave_speed/ dx / frame_rate)) # to satisfy CFL condition @@ -90,10 +90,10 @@ func _on_init_option_item_selected(index: int) -> void: func set_initial_condition(idx) -> void: x = range(num_points).map(func(elt): return (dx * elt + xmin)) - + u.resize(num_points) uprev.resize(num_points) - + match idx: 0: for i in u.size(): @@ -103,7 +103,7 @@ func set_initial_condition(idx) -> void: for i in u.size(): u[i] = init_params[1]["amplitude"] * sin(init_params[1]["mode"] * PI * x[i]) uprev[i] = u[i] - 2: + 2: for i in u.size(): u[i] = init_params[0]["amplitude"] * exp(-(x[i] - init_params[2]["xi"])**2/init_params[0]["sig"]) var delx = wave_speed/frame_rate/num_steps_per_frame @@ -111,7 +111,7 @@ func set_initial_condition(idx) -> void: 3: for i in u.size(): u[i] = init_params[0]["amplitude"] * exp(-(x[i] - init_params[3]["x1"])**2/init_params[0]["sig"]) - init_params[0]["amplitude"] * exp(-(x[i] - init_params[3]["x2"])**2/init_params[0]["sig"]) - + var delx = wave_speed/frame_rate/num_steps_per_frame uprev[i] = init_params[0]["amplitude"] * exp(-(x[i] - init_params[3]["x1"] - delx)**2/init_params[0]["sig"]) - init_params[0]["amplitude"] * exp(-(x[i] - init_params[3]["x2"] + delx)**2/init_params[0]["sig"]) From 6ec259f206d6f125715c4d1f796a865bbe1c6406 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sat, 10 May 2025 18:30:48 +0200 Subject: [PATCH 03/35] Set solver in boids_model.gd --- demo/demos/boids_simulation/boids_model.gd | 16 +++++++++++++--- demo/demos/boids_simulation/gd_solver.gd | 20 +++++++++++++++++++- demo/demos/boids_simulation/main.tscn | 6 ++++-- demo/demos/boids_simulation/nd_solver.gd | 15 +++++++++++++++ demo/demos/boids_simulation/solver.gd | 4 ++-- 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index 8dde8250..f8bfa91e 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -1,8 +1,15 @@ -extends Node +extends Node2D -@export var params: Node2D +@export_category("Simulation parameters") +@export var solver: BoidsSolver + + + + + +@export var N: int = 1 # Implement slider functionalities for: # Number of Boids # Speed @@ -14,12 +21,15 @@ extends Node # Implement reset func _ready() -> void: + # Set correct values in GUI # Initialize boids as scenes from res://demos/boids_simulation/boid.tscn # Add border around screen redirecting boids to screen center + - #solver.initialize() + + solver.initialize() pass func _process(delta: float) -> void: diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index b19fc4de..669a9a55 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -3,8 +3,25 @@ extends BoidsSolver # parameters (speed, boids, etc.) defined in params # params is set to BoidsModel and can be modified in Editor and in boids_model.gd -func initialize() -> void: +var position: Vector2 = Vector2(500, 500) # position vector +var velocity: Vector2 = Vector2.ZERO + +var texture = preload("res://demos/boids_simulation/boid.png") + +#@onready var boid_sprite: Sprite2D = $Sprite2D + +func initialize() -> void:# + + # Load the boid sprite + + #boid_sprite.texture = texture + + # Set the initial position + #boid_sprite.position = position + + # create position vector and initialize with random Vector2s inside screen + # create velocity vector and initialize with Vector2s of same value in random directions pass @@ -25,3 +42,4 @@ func update_boids() -> void: # derive and apply rotation from velocity vector direction to each boid pass + diff --git a/demo/demos/boids_simulation/main.tscn b/demo/demos/boids_simulation/main.tscn index 55c8a43b..6b96d817 100644 --- a/demo/demos/boids_simulation/main.tscn +++ b/demo/demos/boids_simulation/main.tscn @@ -6,9 +6,11 @@ [sub_resource type="ImageTexture" id="ImageTexture_03ndn"] -[node name="BoidsModel" type="Node2D" node_paths=PackedStringArray("params")] +[node name="BoidsModel" type="Node2D"] script = ExtResource("1_2fw6l") -params = NodePath(".") + +[node name="Sprite2D" type="Sprite2D" parent="."] +position = Vector2(395.3, 326.62) [node name="Solvers" type="Node" parent="."] diff --git a/demo/demos/boids_simulation/nd_solver.gd b/demo/demos/boids_simulation/nd_solver.gd index a2b95b26..0229d637 100644 --- a/demo/demos/boids_simulation/nd_solver.gd +++ b/demo/demos/boids_simulation/nd_solver.gd @@ -1,3 +1,18 @@ extends BoidsSolver +func initialize() -> void:# + + # Load the boid sprite + + #boid_sprite.texture = texture + + # Set the initial position + #boid_sprite.position = position + + + # create position vector and initialize with random Vector2s inside screen + + # create velocity vector and initialize with Vector2s of same value in random directions + + pass # TODO diff --git a/demo/demos/boids_simulation/solver.gd b/demo/demos/boids_simulation/solver.gd index 2fd207c1..ab52e913 100644 --- a/demo/demos/boids_simulation/solver.gd +++ b/demo/demos/boids_simulation/solver.gd @@ -6,8 +6,8 @@ class_name BoidsSolver func initialize() -> void: pass -func simulation_step(delta: float) -> void: - pass +#func simulation_step(delta: float) -> void: + #pass func update_boids() -> void: pass From f2bf2c8270f6bf166fea45b2e874184c54b5b35b Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sat, 10 May 2025 21:32:56 +0200 Subject: [PATCH 04/35] workaround empty solver in boids_model.gd --- demo/demos/boids_simulation/boids_model.gd | 20 ++++++++++++++++++-- demo/demos/boids_simulation/gd_solver.gd | 4 ++-- demo/demos/boids_simulation/main.tscn | 3 --- demo/demos/boids_simulation/nd_solver.gd | 2 ++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index f8bfa91e..5b88049a 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -9,7 +9,7 @@ extends Node2D -@export var N: int = 1 +@export var N: int = 20 # Implement slider functionalities for: # Number of Boids # Speed @@ -28,7 +28,8 @@ func _ready() -> void: # Add border around screen redirecting boids to screen center - + #TODO fix workaround below + self.solver = $Solvers/GDSolver solver.initialize() pass @@ -38,5 +39,20 @@ func _process(delta: float) -> void: pass +func _on_point_slider_drag_ended(value_changed: bool) -> void: + N = %PointSlider.value + %PointLabel.text = "Grid: " + str(N) + "x" + str(N) + #resize_image() + solver.initialize() + +#func _on_steps_per_second_slider_drag_ended(value_changed: bool) -> void: + #steps_per_second = %StepsPerSecondSlider.value + #%StepsPerSecondLabel.text = "Speed: %.2f" % steps_per_second +func _on_restart_button_pressed() -> void: + solver.initialize() + +func _on_solver_option_item_selected(index: int) -> void: + solver = $Solvers.get_child(index) + solver.initialize() # TODO handle GUI inputs diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index 669a9a55..b6c26e8b 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -4,9 +4,9 @@ extends BoidsSolver # params is set to BoidsModel and can be modified in Editor and in boids_model.gd var position: Vector2 = Vector2(500, 500) # position vector -var velocity: Vector2 = Vector2.ZERO +#var velocity: Vector2 = Vector2.ZERO -var texture = preload("res://demos/boids_simulation/boid.png") +#var texture = preload("res://demos/boids_simulation/boid.png") #@onready var boid_sprite: Sprite2D = $Sprite2D diff --git a/demo/demos/boids_simulation/main.tscn b/demo/demos/boids_simulation/main.tscn index 6b96d817..3a31daa7 100644 --- a/demo/demos/boids_simulation/main.tscn +++ b/demo/demos/boids_simulation/main.tscn @@ -9,9 +9,6 @@ [node name="BoidsModel" type="Node2D"] script = ExtResource("1_2fw6l") -[node name="Sprite2D" type="Sprite2D" parent="."] -position = Vector2(395.3, 326.62) - [node name="Solvers" type="Node" parent="."] [node name="GDSolver" type="Node" parent="Solvers" node_paths=PackedStringArray("params")] diff --git a/demo/demos/boids_simulation/nd_solver.gd b/demo/demos/boids_simulation/nd_solver.gd index 0229d637..5b9727d7 100644 --- a/demo/demos/boids_simulation/nd_solver.gd +++ b/demo/demos/boids_simulation/nd_solver.gd @@ -1,5 +1,7 @@ extends BoidsSolver +var position: Vector2 = Vector2(500, 500) + func initialize() -> void:# # Load the boid sprite From 76290e1a7faa66465d8e337907bf8cc33e37550a Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sat, 10 May 2025 21:46:29 +0200 Subject: [PATCH 05/35] spawning first boid --- demo/demos/boids_simulation/gd_solver.gd | 11 ++++++----- demo/demos/boids_simulation/main.tscn | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index b6c26e8b..f63df6de 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -6,18 +6,19 @@ extends BoidsSolver var position: Vector2 = Vector2(500, 500) # position vector #var velocity: Vector2 = Vector2.ZERO -#var texture = preload("res://demos/boids_simulation/boid.png") +var texture = preload("res://demos/boids_simulation/boid.png") -#@onready var boid_sprite: Sprite2D = $Sprite2D +@onready var boid_sprite: Sprite2D = $/root/BoidsModel/Sprite2D func initialize() -> void:# # Load the boid sprite - #boid_sprite.texture = texture + boid_sprite.texture = texture - # Set the initial position - #boid_sprite.position = position + # Set the initial position and size + boid_sprite.scale = Vector2(0.1, 0.1) + boid_sprite.position = position # create position vector and initialize with random Vector2s inside screen diff --git a/demo/demos/boids_simulation/main.tscn b/demo/demos/boids_simulation/main.tscn index 3a31daa7..646668b1 100644 --- a/demo/demos/boids_simulation/main.tscn +++ b/demo/demos/boids_simulation/main.tscn @@ -194,6 +194,8 @@ step = 0.5 value = 30.1 exp_edit = true +[node name="Sprite2D" type="Sprite2D" parent="."] + [connection signal="drag_ended" from="SliderOptions/SpreadSlider" to="." method="_on_spread_slider_drag_ended"] [connection signal="drag_ended" from="SliderOptions/InfectedSlider" to="." method="_on_infected_slider_drag_ended"] [connection signal="drag_ended" from="SliderOptions/RecoverySlider" to="." method="_on_recovery_slider_drag_ended"] From e6dd5fc413f761d611005613fa530052c396a0ac Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 11 May 2025 08:06:13 +0200 Subject: [PATCH 06/35] boid facing right direction and moving --- demo/demos/boids_simulation/boids_model.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index 5b88049a..3db70fb5 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -35,6 +35,7 @@ func _ready() -> void: func _process(delta: float) -> void: # Call simulation_step(delta) + solver.simulation_step(delta) # Update GUI pass From 0f420511db6323c41a91725887d5cc32eada3ba0 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 11 May 2025 08:20:15 +0200 Subject: [PATCH 07/35] boid moving with noise in circle --- demo/demos/boids_simulation/gd_solver.gd | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index f63df6de..e7cbb14c 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -4,12 +4,15 @@ extends BoidsSolver # params is set to BoidsModel and can be modified in Editor and in boids_model.gd var position: Vector2 = Vector2(500, 500) # position vector -#var velocity: Vector2 = Vector2.ZERO +var velocity: Vector2 = Vector2(100,50) + var texture = preload("res://demos/boids_simulation/boid.png") @onready var boid_sprite: Sprite2D = $/root/BoidsModel/Sprite2D +@export var speed: float = 200.0 # Pixels per second + func initialize() -> void:# # Load the boid sprite @@ -28,13 +31,25 @@ func initialize() -> void:# pass func simulation_step(delta: float) -> void: + + + # for each boid collect others in visual range # calculate new velocity directions according to: # Seperation # Alignment # Cohesion # (Additional noise) + # Apply a random angle noise + var noise_angle = randf_range(-0.2, 0.1) + var new_direction = velocity.rotated(noise_angle).normalized() + velocity = new_direction * speed # apply velocities to positions + + boid_sprite.position += velocity * delta + + #offset angle for right facing direction + boid_sprite.rotation = velocity.angle() + PI / 2 pass From 7ee180c769f9d3a851f0a9d04086ff754b12841c Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 11 May 2025 08:59:44 +0200 Subject: [PATCH 08/35] smoother angle movement of boid --- demo/demos/boids_simulation/boids_model.gd | 33 +++++++++++++++++++++- demo/demos/boids_simulation/gd_solver.gd | 27 +++++++----------- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index 3db70fb5..2b49d9e7 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -4,8 +4,16 @@ extends Node2D @export_category("Simulation parameters") @export var solver: BoidsSolver +#var position = Vector2(500, 500) # position vector +var velocity: Vector2 = Vector2(100,50) +var texture = preload("res://demos/boids_simulation/boid.png") + +@onready var boid_sprite: Sprite2D = $/root/BoidsModel/Sprite2D + +@export var speed: float = 200.0 # Pixels per second + @@ -25,6 +33,13 @@ func _ready() -> void: # Set correct values in GUI # Initialize boids as scenes from res://demos/boids_simulation/boid.tscn + # Load the boid sprite + + boid_sprite.texture = texture + + # Set the initial position and size + boid_sprite.scale = Vector2(0.1, 0.1) + boid_sprite.position = position # Add border around screen redirecting boids to screen center @@ -35,10 +50,26 @@ func _ready() -> void: func _process(delta: float) -> void: # Call simulation_step(delta) - solver.simulation_step(delta) + solver.simulation_step(delta, velocity, speed, position, boid_sprite) + wrap_around() # Update GUI pass + +func wrap_around(): + var screen_size = get_viewport_rect().size + + # Check horizontal boundaries + if boid_sprite.position.x > screen_size.x: + boid_sprite.position.x = 0 + elif boid_sprite.position.x < 0: + boid_sprite.position.x = screen_size.x + + # Check vertical boundaries + if boid_sprite.position.y > screen_size.y: + boid_sprite.position.y = 0 + elif boid_sprite.position.y < 0: + boid_sprite.position.y = screen_size.y func _on_point_slider_drag_ended(value_changed: bool) -> void: N = %PointSlider.value diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index e7cbb14c..2f72a140 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -3,25 +3,12 @@ extends BoidsSolver # parameters (speed, boids, etc.) defined in params # params is set to BoidsModel and can be modified in Editor and in boids_model.gd -var position: Vector2 = Vector2(500, 500) # position vector -var velocity: Vector2 = Vector2(100,50) - - -var texture = preload("res://demos/boids_simulation/boid.png") - -@onready var boid_sprite: Sprite2D = $/root/BoidsModel/Sprite2D - -@export var speed: float = 200.0 # Pixels per second +@export var rotation_speed: float = 2.0 # Higher value = faster rotation +var noise_angle = randf_range(-0.5, 0.5) func initialize() -> void:# - # Load the boid sprite - - boid_sprite.texture = texture - # Set the initial position and size - boid_sprite.scale = Vector2(0.1, 0.1) - boid_sprite.position = position # create position vector and initialize with random Vector2s inside screen @@ -30,7 +17,7 @@ func initialize() -> void:# pass -func simulation_step(delta: float) -> void: +func simulation_step(delta: float, velocity: Vector2, speed: float, position: Vector2, boid_sprite: Sprite2D) -> void: @@ -41,7 +28,7 @@ func simulation_step(delta: float) -> void: # Cohesion # (Additional noise) # Apply a random angle noise - var noise_angle = randf_range(-0.2, 0.1) + noise_angle += randf_range(-0.05, 0.05) var new_direction = velocity.rotated(noise_angle).normalized() velocity = new_direction * speed # apply velocities to positions @@ -51,6 +38,9 @@ func simulation_step(delta: float) -> void: #offset angle for right facing direction boid_sprite.rotation = velocity.angle() + PI / 2 + + + pass func update_boids() -> void: @@ -59,3 +49,6 @@ func update_boids() -> void: pass + + + From 742d4ca66c4cbc59ed674abe42820fb70c5269bc Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 11 May 2025 09:24:56 +0200 Subject: [PATCH 09/35] further smoother angle movement of boid --- demo/demos/boids_simulation/gd_solver.gd | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index 2f72a140..8e159429 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -4,7 +4,9 @@ extends BoidsSolver # params is set to BoidsModel and can be modified in Editor and in boids_model.gd @export var rotation_speed: float = 2.0 # Higher value = faster rotation -var noise_angle = randf_range(-0.5, 0.5) +@export var update_interval: int = 5 # Number of frames between angle updates +var frame_counter: int = 0 +var noise_angle = randf_range(-0.5, 0.5) # deafult random noise angle func initialize() -> void:# @@ -27,8 +29,12 @@ func simulation_step(delta: float, velocity: Vector2, speed: float, position: Ve # Alignment # Cohesion # (Additional noise) - # Apply a random angle noise - noise_angle += randf_range(-0.05, 0.05) + # Apply a random angle noise every n frames + frame_counter += 1 + if frame_counter >= update_interval: + frame_counter = 0 + noise_angle += randf_range(-0.1, 0.1) + var new_direction = velocity.rotated(noise_angle).normalized() velocity = new_direction * speed # apply velocities to positions From 1c51a4221bb20702d8456d822340605f34492b09 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 11 May 2025 12:36:41 +0200 Subject: [PATCH 10/35] dynamic number of boids (hard-coded) moving into different directions --- demo/demos/boids_simulation/boids_model.gd | 68 +++++++++++++++++----- demo/demos/boids_simulation/gd_solver.gd | 57 ++++++++++++------ 2 files changed, 91 insertions(+), 34 deletions(-) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index 2b49d9e7..191a413b 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -1,5 +1,9 @@ extends Node2D +@export var boid_count: int = 5 +# Store data for each sprite +var boids = [] +var boids_container: Node2D @export_category("Simulation parameters") @export var solver: BoidsSolver @@ -29,7 +33,36 @@ var texture = preload("res://demos/boids_simulation/boid.png") # Implement reset func _ready() -> void: - + # Create Boids container if it does not exist + if not has_node("Boids"): + boids_container = Node2D.new() + boids_container.name = "Boids" + add_child(boids_container) + else: + boids_container = $Boids + + # Create boids as children of the Boids container + for i in range(boid_count): + var boid = Sprite2D.new() + boid.texture = texture + #Assign a boid a random position and standard size + boid.scale = Vector2(0.1, 0.1) + boid.position = Vector2(randf_range(0, get_viewport_rect().size.x), randf_range(0, get_viewport_rect().size.y)) + # Assign a random start rotation (in radians) + boid.rotation = randf_range(0, PI * 2) + boid.name = "Boid " + str(i + 1) # Naming each boid + + boids_container.add_child(boid) + + + + # Initialize boid data + var boid_data = { + "node": boid, + #Assign same speed and random direction based on rotation + "velocity": Vector2(cos(boid.rotation), sin(boid.rotation)).normalized() * speed + } + boids.append(boid_data) # Set correct values in GUI # Initialize boids as scenes from res://demos/boids_simulation/boid.tscn @@ -50,26 +83,29 @@ func _ready() -> void: func _process(delta: float) -> void: # Call simulation_step(delta) - solver.simulation_step(delta, velocity, speed, position, boid_sprite) - wrap_around() + + solver.simulation_step(delta, velocity, speed, position, boid_sprite, boids) + wrap_around(boids) # Update GUI pass -func wrap_around(): +func wrap_around(boids: Array): var screen_size = get_viewport_rect().size - - # Check horizontal boundaries - if boid_sprite.position.x > screen_size.x: - boid_sprite.position.x = 0 - elif boid_sprite.position.x < 0: - boid_sprite.position.x = screen_size.x - - # Check vertical boundaries - if boid_sprite.position.y > screen_size.y: - boid_sprite.position.y = 0 - elif boid_sprite.position.y < 0: - boid_sprite.position.y = screen_size.y + for boid in boids: + + + # Check horizontal boundaries + if boid.node.position.x > screen_size.x: + boid.node.position.x = 0 + elif boid.node.position.x < 0: + boid.node.position.x = screen_size.x + + # Check vertical boundaries + if boid.node.position.y > screen_size.y: + boid.node.position.y = 0 + elif boid.node.position.y < 0: + boid.node.position.y = screen_size.y func _on_point_slider_drag_ended(value_changed: bool) -> void: N = %PointSlider.value diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index 8e159429..aee523f8 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -3,10 +3,10 @@ extends BoidsSolver # parameters (speed, boids, etc.) defined in params # params is set to BoidsModel and can be modified in Editor and in boids_model.gd -@export var rotation_speed: float = 2.0 # Higher value = faster rotation -@export var update_interval: int = 5 # Number of frames between angle updates + +@export var update_interval: int = 50 # Number of frames between angle updates var frame_counter: int = 0 -var noise_angle = randf_range(-0.5, 0.5) # deafult random noise angle +var new_direction = Vector2.ONE func initialize() -> void:# @@ -19,9 +19,8 @@ func initialize() -> void:# pass -func simulation_step(delta: float, velocity: Vector2, speed: float, position: Vector2, boid_sprite: Sprite2D) -> void: - - +func simulation_step(delta: float, velocity: Vector2, speed: float, position: Vector2, boid_sprite: Sprite2D, boids: Array) -> void: + # for each boid collect others in visual range # calculate new velocity directions according to: @@ -30,28 +29,50 @@ func simulation_step(delta: float, velocity: Vector2, speed: float, position: Ve # Cohesion # (Additional noise) # Apply a random angle noise every n frames + + update_boids(boids, speed, delta) + + frame_counter += 1 - if frame_counter >= update_interval: - frame_counter = 0 - noise_angle += randf_range(-0.1, 0.1) - var new_direction = velocity.rotated(noise_angle).normalized() - velocity = new_direction * speed - # apply velocities to positions - boid_sprite.position += velocity * delta - #offset angle for right facing direction - boid_sprite.rotation = velocity.angle() + PI / 2 pass -func update_boids() -> void: - # apply position from position vector to each boid - # derive and apply rotation from velocity vector direction to each boid +func update_boids(boids: Array, speed: float, delta: float) -> void: + if frame_counter >= update_interval: + frame_counter = 0 + for boid in boids: + + + + # Apply a small random variation to the current angle + var angle_variation = randf_range(-0.01, 0.01) + var new_angle = boid.node.rotation + angle_variation + new_direction = Vector2(cos(new_angle), sin(new_angle)) + boid.velocity = new_direction * speed + # apply velocities to positions + + boid.node.position += boid.velocity * delta + + #offset angle for right facing direction + boid.node.rotation = boid.velocity.angle() + PI / 2 + # apply position from position vector to each boid + # derive and apply rotation from velocity vector direction to each boid + else: + for boid in boids: + # apply velocities to positions + + boid.node.position += boid.velocity * delta + + #offset angle for right facing direction + boid.node.rotation = boid.velocity.angle() + PI / 2 + # apply position from position vector to each boid + # derive and apply rotation from velocity vector direction to each boid pass From 21373ff5001f7175bb0fce554b9a422542ba3630 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Mon, 12 May 2025 08:15:15 +0200 Subject: [PATCH 11/35] Now with smoother noise turns: dynamic number of boids (hard-coded) moving into different directions --- demo/demos/boids_simulation/gd_solver.gd | 6 +++--- demo/demos/boids_simulation/solver.gd | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index aee523f8..1df5cece 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -51,9 +51,9 @@ func update_boids(boids: Array, speed: float, delta: float) -> void: # Apply a small random variation to the current angle - var angle_variation = randf_range(-0.01, 0.01) - var new_angle = boid.node.rotation + angle_variation - new_direction = Vector2(cos(new_angle), sin(new_angle)) + var angle_variation = randf_range(-0.5, 0.5) + var adjusted_angle = boid.velocity.angle() + angle_variation + new_direction = Vector2(cos(adjusted_angle), sin(adjusted_angle)) boid.velocity = new_direction * speed # apply velocities to positions diff --git a/demo/demos/boids_simulation/solver.gd b/demo/demos/boids_simulation/solver.gd index ab52e913..4bb79afd 100644 --- a/demo/demos/boids_simulation/solver.gd +++ b/demo/demos/boids_simulation/solver.gd @@ -6,8 +6,8 @@ class_name BoidsSolver func initialize() -> void: pass -#func simulation_step(delta: float) -> void: - #pass +func simulation_step(delta: float, velocity: Vector2, speed: float, position: Vector2, boid_sprite: Sprite2D, boids: Array) -> void: + pass -func update_boids() -> void: +func update_boids(boids: Array, speed: float, delta: float) -> void: pass From 7efb9726fb4e6c008913aa473ac6cd55a1ce659e Mon Sep 17 00:00:00 2001 From: kro-ma Date: Mon, 12 May 2025 08:51:17 +0200 Subject: [PATCH 12/35] Slider for number of boids implemented --- demo/demos/boids_simulation/boids_model.gd | 52 +++++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index 191a413b..a57aa148 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -1,6 +1,6 @@ extends Node2D -@export var boid_count: int = 5 +@export var boid_count: int = 20 # Store data for each sprite var boids = [] var boids_container: Node2D @@ -68,11 +68,7 @@ func _ready() -> void: # Initialize boids as scenes from res://demos/boids_simulation/boid.tscn # Load the boid sprite - boid_sprite.texture = texture - # Set the initial position and size - boid_sprite.scale = Vector2(0.1, 0.1) - boid_sprite.position = position # Add border around screen redirecting boids to screen center @@ -90,6 +86,38 @@ func _process(delta: float) -> void: pass +func initialize_boids(target_count: int) -> void: + # Clear existing boids + for boid_data in boids: + boid_data["node"].queue_free() + boids.clear() + + # Create new boids + for i in range(target_count): + add_boid(i) + +func add_boid(i: int): + var boid = Sprite2D.new() + boid.texture = texture + #Assign a boid a random position and standard size + boid.scale = Vector2(0.1, 0.1) + boid.position = Vector2(randf_range(0, get_viewport_rect().size.x), randf_range(0, get_viewport_rect().size.y)) + # Assign a random start rotation (in radians) + boid.rotation = randf_range(0, PI * 2) + boid.name = "Boid " + str(i + 1) # Naming each boid + + boids_container.add_child(boid) + + + + # Initialize boid data + var boid_data = { + "node": boid, + #Assign same speed and random direction based on rotation + "velocity": Vector2(cos(boid.rotation), sin(boid.rotation)).normalized() * speed + } + boids.append(boid_data) + func wrap_around(boids: Array): var screen_size = get_viewport_rect().size for boid in boids: @@ -108,8 +136,8 @@ func wrap_around(boids: Array): boid.node.position.y = screen_size.y func _on_point_slider_drag_ended(value_changed: bool) -> void: - N = %PointSlider.value - %PointLabel.text = "Grid: " + str(N) + "x" + str(N) + boid_count = %NumberOfBoidsSlider.value + #resize_image() solver.initialize() @@ -123,4 +151,14 @@ func _on_restart_button_pressed() -> void: func _on_solver_option_item_selected(index: int) -> void: solver = $Solvers.get_child(index) solver.initialize() + + + # TODO handle GUI inputs + + +func _on_number_of_boids_slider_drag_ended(value_changed: bool) -> void: + boid_count = %NumberOfBoidsSlider.value + %NumberOfBoids.text = "Boids: "+str(boid_count) + initialize_boids(boid_count) + From a468cb188e0552e46c4a929ac0b1993ca2ad79ec Mon Sep 17 00:00:00 2001 From: kro-ma Date: Wed, 14 May 2025 18:40:12 +0200 Subject: [PATCH 13/35] Separation implemented --- demo/demos/boids_simulation/gd_solver.gd | 35 ++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index 1df5cece..980f8745 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -25,12 +25,21 @@ func simulation_step(delta: float, velocity: Vector2, speed: float, position: Ve # for each boid collect others in visual range # calculate new velocity directions according to: # Seperation + + var separation_distance = 200.0 + var separation_weight = 5.0 + + for boid in boids: + var separation = apply_separation(boid, boids, separation_distance) + if separation != Vector2.ZERO: + # Blend the separation with the current velocity + boid.velocity = (boid.velocity + separation * separation_weight).normalized() * speed # Alignment # Cohesion # (Additional noise) # Apply a random angle noise every n frames - update_boids(boids, speed, delta) + update_boids_noise(boids, speed, delta) frame_counter += 1 @@ -42,8 +51,30 @@ func simulation_step(delta: float, velocity: Vector2, speed: float, position: Ve pass + +func apply_separation(boid, boids: Array, separation_distance: float) -> Vector2: + var steer = Vector2.ZERO + var total = 0 + + for other in boids: + if other == boid: + continue + + var dist = boid.node.position.distance_to(other.node.position) + + if dist < separation_distance and dist > 0: + # Vector pointing away from the neighbor, inversely weighted by distance + var diff = (boid.node.position - other.node.position).normalized() / dist + steer += diff + total += 1 + + if total > 0: + steer /= total + steer = steer.normalized() + + return steer -func update_boids(boids: Array, speed: float, delta: float) -> void: +func update_boids_noise(boids: Array, speed: float, delta: float) -> void: if frame_counter >= update_interval: frame_counter = 0 for boid in boids: From 822bf02769f5c39279463fff126700c68fb64349 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Wed, 14 May 2025 18:45:32 +0200 Subject: [PATCH 14/35] Cohesion implemented --- demo/demos/boids_simulation/gd_solver.gd | 34 +++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index 980f8745..66a7c057 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -4,7 +4,7 @@ extends BoidsSolver # params is set to BoidsModel and can be modified in Editor and in boids_model.gd -@export var update_interval: int = 50 # Number of frames between angle updates +@export var update_interval: int = 5000 # Number of frames between angle updates var frame_counter: int = 0 var new_direction = Vector2.ONE @@ -26,8 +26,8 @@ func simulation_step(delta: float, velocity: Vector2, speed: float, position: Ve # calculate new velocity directions according to: # Seperation - var separation_distance = 200.0 - var separation_weight = 5.0 + var separation_distance = 50.0 + var separation_weight = 1.0 for boid in boids: var separation = apply_separation(boid, boids, separation_distance) @@ -35,6 +35,14 @@ func simulation_step(delta: float, velocity: Vector2, speed: float, position: Ve # Blend the separation with the current velocity boid.velocity = (boid.velocity + separation * separation_weight).normalized() * speed # Alignment + + var visual_range = 100.0 + var alignment_weight = 0.5 + + for boid in boids: + var alignment = apply_alignment(boid, boids, visual_range) + if alignment != Vector2.ZERO: + boid.velocity = (boid.velocity + alignment * alignment_weight).normalized() * speed # Cohesion # (Additional noise) # Apply a random angle noise every n frames @@ -73,6 +81,26 @@ func apply_separation(boid, boids: Array, separation_distance: float) -> Vector2 steer = steer.normalized() return steer + +func apply_alignment(boid: Dictionary, boids: Array, visual_range: float) -> Vector2: + var avg_velocity = Vector2.ZERO + var count = 0 + + for other in boids: + if other == boid: + continue + + var dist = boid.node.position.distance_to(other.node.position) + if dist < visual_range: + avg_velocity += other.velocity + count += 1 + + if count > 0: + avg_velocity /= count + avg_velocity = avg_velocity.normalized() + return avg_velocity + else: + return Vector2.ZERO func update_boids_noise(boids: Array, speed: float, delta: float) -> void: if frame_counter >= update_interval: From 791e1f7825b70c53d533473b9ebaa7b515a83b68 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Wed, 14 May 2025 19:16:55 +0200 Subject: [PATCH 15/35] GUI sliders implemented --- demo/demos/boids_simulation/boids_model.gd | 50 ++++++++++++++------- demo/demos/boids_simulation/gd_solver.gd | 52 +++++++++++++++------- demo/demos/boids_simulation/solver.gd | 6 ++- 3 files changed, 73 insertions(+), 35 deletions(-) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index a57aa148..5f2fdd66 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -1,6 +1,12 @@ extends Node2D @export var boid_count: int = 20 +@export var speed: float = 200.0 # Pixels per second +@export var visual_range: float = 100.0 +@export var separation_weight: float = 1.0 +@export var alignment_weight: float = 0.5 +@export var cohesion_weight: float = 0.5 + # Store data for each sprite var boids = [] var boids_container: Node2D @@ -16,7 +22,7 @@ var texture = preload("res://demos/boids_simulation/boid.png") @onready var boid_sprite: Sprite2D = $/root/BoidsModel/Sprite2D -@export var speed: float = 200.0 # Pixels per second + @@ -80,7 +86,7 @@ func _ready() -> void: func _process(delta: float) -> void: # Call simulation_step(delta) - solver.simulation_step(delta, velocity, speed, position, boid_sprite, boids) + solver.simulation_step(delta, velocity, speed, visual_range, separation_weight, alignment_weight, cohesion_weight, position, boid_sprite, boids) wrap_around(boids) # Update GUI @@ -135,16 +141,31 @@ func wrap_around(boids: Array): elif boid.node.position.y < 0: boid.node.position.y = screen_size.y -func _on_point_slider_drag_ended(value_changed: bool) -> void: - boid_count = %NumberOfBoidsSlider.value - - #resize_image() - solver.initialize() - -#func _on_steps_per_second_slider_drag_ended(value_changed: bool) -> void: - #steps_per_second = %StepsPerSecondSlider.value - #%StepsPerSecondLabel.text = "Speed: %.2f" % steps_per_second - +func _on_speed_slider_drag_ended(value_changed: bool) -> void: + if value_changed: + speed = %SpeedSlider.value + %SpeedLabel.text = "Speed: " + str(round(speed)) + +func _on_visual_range_slider_drag_ended(value_changed: bool) -> void: + if value_changed: + visual_range = %VisualRangeSlider.value + %VisualRangeLabel.text = "Visual Range: " + str(round(visual_range)) + +func _on_separation_slider_drag_ended(value_changed: bool) -> void: + if value_changed: + separation_weight = %SeparationSlider.value + %SeparationLabel.text = "Separation: " + str(snapped(separation_weight, 0.1)) + +func _on_alignment_slider_drag_ended(value_changed: bool) -> void: + if value_changed: + alignment_weight = %AlignmentSlider.value + %AlignmentLabel.text = "Alignment: " + str(snapped(alignment_weight, 0.1)) + +func _on_cohesion_slider_drag_ended(value_changed: bool) -> void: + if value_changed: + cohesion_weight = %CohesionSlider.value + %CohesionLabel.text = "Cohesion: " + str(snapped(cohesion_weight, 0.1)) + func _on_restart_button_pressed() -> void: solver.initialize() @@ -152,11 +173,6 @@ func _on_solver_option_item_selected(index: int) -> void: solver = $Solvers.get_child(index) solver.initialize() - - -# TODO handle GUI inputs - - func _on_number_of_boids_slider_drag_ended(value_changed: bool) -> void: boid_count = %NumberOfBoidsSlider.value %NumberOfBoids.text = "Boids: "+str(boid_count) diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index 66a7c057..04882d46 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -19,31 +19,30 @@ func initialize() -> void:# pass -func simulation_step(delta: float, velocity: Vector2, speed: float, position: Vector2, boid_sprite: Sprite2D, boids: Array) -> void: +func simulation_step(delta: float, velocity: Vector2, speed: float, + visual_range: float, separation_weight: float, alignment_weight: float, + cohesion_weight: float, position: Vector2, boid_sprite: Sprite2D, boids: Array) -> void: # for each boid collect others in visual range # calculate new velocity directions according to: # Seperation - - var separation_distance = 50.0 - var separation_weight = 1.0 - - for boid in boids: - var separation = apply_separation(boid, boids, separation_distance) - if separation != Vector2.ZERO: - # Blend the separation with the current velocity - boid.velocity = (boid.velocity + separation * separation_weight).normalized() * speed # Alignment - - var visual_range = 100.0 - var alignment_weight = 0.5 + # Cohesion# + + var separation_range = 50.0 + for boid in boids: + var separation = apply_separation(boid, boids, separation_range) var alignment = apply_alignment(boid, boids, visual_range) - if alignment != Vector2.ZERO: - boid.velocity = (boid.velocity + alignment * alignment_weight).normalized() * speed - # Cohesion + var cohesion = apply_cohesion(boid, boids, visual_range) + + # Combine the three steering behaviors + var steer = separation * separation_weight + alignment * alignment_weight + cohesion * cohesion_weight + + # Blend with current velocity and normalize to maintain speed + boid.velocity = (boid.velocity + steer).normalized() * speed # (Additional noise) # Apply a random angle noise every n frames @@ -101,6 +100,27 @@ func apply_alignment(boid: Dictionary, boids: Array, visual_range: float) -> Vec return avg_velocity else: return Vector2.ZERO + +func apply_cohesion(boid: Dictionary, boids: Array, visual_range: float) -> Vector2: + var center = Vector2.ZERO + var count = 0 + + for other in boids: + if other == boid: + continue + + var dist = boid.node.position.distance_to(other.node.position) + if dist < visual_range: + center += other.node.position + count += 1 + + if count > 0: + center /= count + var direction = (center - boid.node.position).normalized() + return direction + else: + return Vector2.ZERO + func update_boids_noise(boids: Array, speed: float, delta: float) -> void: if frame_counter >= update_interval: diff --git a/demo/demos/boids_simulation/solver.gd b/demo/demos/boids_simulation/solver.gd index 4bb79afd..9cb6fd78 100644 --- a/demo/demos/boids_simulation/solver.gd +++ b/demo/demos/boids_simulation/solver.gd @@ -6,8 +6,10 @@ class_name BoidsSolver func initialize() -> void: pass -func simulation_step(delta: float, velocity: Vector2, speed: float, position: Vector2, boid_sprite: Sprite2D, boids: Array) -> void: +func simulation_step(delta: float, velocity: Vector2, speed: float, + visual_range: float, separation_weight: float, alignment_weight: float, + cohesion_weight: float, position: Vector2, boid_sprite: Sprite2D, boids: Array) -> void: pass -func update_boids(boids: Array, speed: float, delta: float) -> void: +func update_boids_noise(boids: Array, speed: float, delta: float) -> void: pass From b47ce3e483ec22ab139affe4b896fa1e0fae5466 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Wed, 14 May 2025 19:25:31 +0200 Subject: [PATCH 16/35] FPS Label activated --- demo/demos/boids_simulation/boids_model.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index 5f2fdd66..f47c7418 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -84,6 +84,7 @@ func _ready() -> void: pass func _process(delta: float) -> void: + %FPSLabel.text = "FPS: " + str(Engine.get_frames_per_second()) # Call simulation_step(delta) solver.simulation_step(delta, velocity, speed, visual_range, separation_weight, alignment_weight, cohesion_weight, position, boid_sprite, boids) From 9d81074f7cec2fb30fdb1b70755685b9b1b98d42 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Fri, 16 May 2025 19:04:25 +0200 Subject: [PATCH 17/35] cleanup and TODOs --- demo/demos/boids_simulation/boids_model.gd | 18 ++---------------- demo/demos/boids_simulation/gd_solver.gd | 8 +++++--- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index f47c7418..9c2c0a55 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -22,21 +22,7 @@ var texture = preload("res://demos/boids_simulation/boid.png") @onready var boid_sprite: Sprite2D = $/root/BoidsModel/Sprite2D - - - - - -@export var N: int = 20 -# Implement slider functionalities for: - # Number of Boids - # Speed - # Visual range - # Seperation - # Alignment - # Cohesion - -# Implement reset +#TODO Implement reset func _ready() -> void: # Create Boids container if it does not exist @@ -86,7 +72,7 @@ func _ready() -> void: func _process(delta: float) -> void: %FPSLabel.text = "FPS: " + str(Engine.get_frames_per_second()) # Call simulation_step(delta) - + #TODO delta display solver.simulation_step(delta, velocity, speed, visual_range, separation_weight, alignment_weight, cohesion_weight, position, boid_sprite, boids) wrap_around(boids) # Update GUI diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index 04882d46..1d5712c7 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -3,8 +3,8 @@ extends BoidsSolver # parameters (speed, boids, etc.) defined in params # params is set to BoidsModel and can be modified in Editor and in boids_model.gd - -@export var update_interval: int = 5000 # Number of frames between angle updates +#TODO make noise independent of frame change +@export var update_interval: int = 50 # Number of frames between angle updates var frame_counter: int = 0 var new_direction = Vector2.ONE @@ -48,7 +48,7 @@ func simulation_step(delta: float, velocity: Vector2, speed: float, update_boids_noise(boids, speed, delta) - + #TODO apply here: boid.node.position += boid.velocity * delta frame_counter += 1 @@ -132,6 +132,7 @@ func update_boids_noise(boids: Array, speed: float, delta: float) -> void: # Apply a small random variation to the current angle var angle_variation = randf_range(-0.5, 0.5) var adjusted_angle = boid.velocity.angle() + angle_variation + #TODO explain this more new_direction = Vector2(cos(adjusted_angle), sin(adjusted_angle)) boid.velocity = new_direction * speed # apply velocities to positions @@ -139,6 +140,7 @@ func update_boids_noise(boids: Array, speed: float, delta: float) -> void: boid.node.position += boid.velocity * delta #offset angle for right facing direction + #TODO initially direct sprite boid.node.rotation = boid.velocity.angle() + PI / 2 # apply position from position vector to each boid # derive and apply rotation from velocity vector direction to each boid From 90b4252b1c11bf50d5a1b681b026d0af49e8e90a Mon Sep 17 00:00:00 2001 From: kro-ma Date: Fri, 16 May 2025 19:44:16 +0200 Subject: [PATCH 18/35] signals for scene nodes and sliders --- demo/demos/boids_simulation/main.tscn | 42 +++++++++++++++------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/demo/demos/boids_simulation/main.tscn b/demo/demos/boids_simulation/main.tscn index 646668b1..173b50d0 100644 --- a/demo/demos/boids_simulation/main.tscn +++ b/demo/demos/boids_simulation/main.tscn @@ -57,13 +57,13 @@ offset_right = 272.0 offset_bottom = 626.0 metadata/_edit_group_ = true -[node name="SpreadLabel" type="Label" parent="SliderOptions"] +[node name="SeparationLabel" type="Label" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 text = "Separation: 1" -[node name="SpreadSlider" type="HSlider" parent="SliderOptions"] +[node name="SeparationSlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 min_value = 0.01 @@ -72,26 +72,26 @@ step = 0.0 value = 1.0 exp_edit = true -[node name="InfectedLabel" type="Label" parent="SliderOptions"] +[node name="AlignmentLabel" type="Label" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 text = "Alignment: 4" -[node name="InfectedSlider" type="HSlider" parent="SliderOptions"] +[node name="AlignmentSlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 min_value = 1.0 value = 4.0 exp_edit = true -[node name="RecoveryLabel" type="Label" parent="SliderOptions"] +[node name="CohesionLabel" type="Label" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 text = "Cohesion: 6" -[node name="RecoverySlider" type="HSlider" parent="SliderOptions"] +[node name="CohesionSlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 min_value = 1.0 @@ -149,13 +149,13 @@ offset_right = 1130.0 offset_bottom = 626.0 metadata/_edit_group_ = true -[node name="StepsPerSecondLabel" type="Label" parent="SolverOptions"] +[node name="SpeedLabel" type="Label" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 text = "Speed: 20" -[node name="StepsPerSecondSlider" type="HSlider" parent="SolverOptions"] +[node name="SpeedSlider" type="HSlider" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 min_value = 0.1 @@ -164,13 +164,13 @@ step = 0.5 value = 30.1 exp_edit = true -[node name="StepsPerSecondLabel2" type="Label" parent="SolverOptions"] +[node name="NumberOfBoids" type="Label" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 text = "Boids: 20" -[node name="StepsPerSecondSlider2" type="HSlider" parent="SolverOptions"] +[node name="NumberOfBoidsSlider" type="HSlider" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 min_value = 0.1 @@ -179,13 +179,13 @@ step = 0.5 value = 30.1 exp_edit = true -[node name="StepsPerSecondLabel3" type="Label" parent="SolverOptions"] +[node name="VisualRangeLabel" type="Label" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 text = "Visual range: 20" -[node name="StepsPerSecondSlider3" type="HSlider" parent="SolverOptions"] +[node name="VisualRangeSlider" type="HSlider" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 min_value = 0.1 @@ -196,11 +196,17 @@ exp_edit = true [node name="Sprite2D" type="Sprite2D" parent="."] -[connection signal="drag_ended" from="SliderOptions/SpreadSlider" to="." method="_on_spread_slider_drag_ended"] -[connection signal="drag_ended" from="SliderOptions/InfectedSlider" to="." method="_on_infected_slider_drag_ended"] -[connection signal="drag_ended" from="SliderOptions/RecoverySlider" to="." method="_on_recovery_slider_drag_ended"] +[connection signal="drag_ended" from="SliderOptions/SeparationSlider" to="." method="_on_spread_slider_drag_ended"] +[connection signal="drag_ended" from="SliderOptions/SeparationSlider" to="." method="_on_separation_slider_drag_ended"] +[connection signal="drag_ended" from="SliderOptions/AlignmentSlider" to="." method="_on_infected_slider_drag_ended"] +[connection signal="drag_ended" from="SliderOptions/AlignmentSlider" to="." method="_on_alignment_slider_drag_ended"] +[connection signal="drag_ended" from="SliderOptions/CohesionSlider" to="." method="_on_recovery_slider_drag_ended"] +[connection signal="drag_ended" from="SliderOptions/CohesionSlider" to="." method="_on_cohesion_slider_drag_ended"] [connection signal="pressed" from="RestartButton" to="." method="_on_restart_button_pressed"] [connection signal="item_selected" from="SolverOption" to="." method="_on_solver_option_item_selected"] -[connection signal="drag_ended" from="SolverOptions/StepsPerSecondSlider" to="." method="_on_steps_per_second_slider_drag_ended"] -[connection signal="drag_ended" from="SolverOptions/StepsPerSecondSlider2" to="." method="_on_steps_per_second_slider_drag_ended"] -[connection signal="drag_ended" from="SolverOptions/StepsPerSecondSlider3" to="." method="_on_steps_per_second_slider_drag_ended"] +[connection signal="drag_ended" from="SolverOptions/SpeedSlider" to="." method="_on_speed_slider_drag_ended"] +[connection signal="drag_ended" from="SolverOptions/SpeedSlider" to="." method="_on_steps_per_second_slider_drag_ended"] +[connection signal="drag_ended" from="SolverOptions/NumberOfBoidsSlider" to="." method="_on_number_of_boids_slider_drag_ended"] +[connection signal="drag_ended" from="SolverOptions/NumberOfBoidsSlider" to="." method="_on_steps_per_second_slider_drag_ended"] +[connection signal="drag_ended" from="SolverOptions/VisualRangeSlider" to="." method="_on_visual_range_slider_drag_ended"] +[connection signal="drag_ended" from="SolverOptions/VisualRangeSlider" to="." method="_on_steps_per_second_slider_drag_ended"] From 76567b9b17aa332228cc3d011c0d9e69d495bea2 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 19 May 2025 00:42:13 +0200 Subject: [PATCH 19/35] Added ND-Solver updating Boids and handling params, rules yet to be implemented. Major refactor, Godot implementation broken for now --- demo/demos/boids_simulation/boid.png.import | 2 +- demo/demos/boids_simulation/boid.tres | 4 + demo/demos/boids_simulation/boid.tscn | 8 +- demo/demos/boids_simulation/boids_model.gd | 174 ++++++++------------ demo/demos/boids_simulation/gd_solver.gd | 118 +++++++------ demo/demos/boids_simulation/main.tscn | 96 +++++------ demo/demos/boids_simulation/nd_solver.gd | 123 ++++++++++++-- demo/demos/boids_simulation/solver.gd | 6 +- demo/demos/game_of_life/gd_solver.gd | 8 +- demo/demos/sirs_model/nd_solver.gd | 18 +- godot-cpp | 2 +- xtensor | 2 +- 12 files changed, 309 insertions(+), 252 deletions(-) create mode 100644 demo/demos/boids_simulation/boid.tres diff --git a/demo/demos/boids_simulation/boid.png.import b/demo/demos/boids_simulation/boid.png.import index b0b3818a..63ca0840 100644 --- a/demo/demos/boids_simulation/boid.png.import +++ b/demo/demos/boids_simulation/boid.png.import @@ -2,7 +2,7 @@ importer="texture" type="CompressedTexture2D" -uid="uid://bsmuhgmkqrp0a" +uid="uid://wxlyngv1jx7x" path="res://.godot/imported/boid.png-3c7feec7ec9b604bb681557bcbd1345a.ctex" metadata={ "vram_texture": false diff --git a/demo/demos/boids_simulation/boid.tres b/demo/demos/boids_simulation/boid.tres new file mode 100644 index 00000000..1cb8e4fb --- /dev/null +++ b/demo/demos/boids_simulation/boid.tres @@ -0,0 +1,4 @@ +[gd_resource type="CompressedTexture2D" format=3 uid="uid://ci2unstco8670"] + +[resource] +load_path = "res://.godot/imported/boid.png-3c7feec7ec9b604bb681557bcbd1345a.ctex" diff --git a/demo/demos/boids_simulation/boid.tscn b/demo/demos/boids_simulation/boid.tscn index a8123b57..6edafb34 100644 --- a/demo/demos/boids_simulation/boid.tscn +++ b/demo/demos/boids_simulation/boid.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=2 format=3 uid="uid://bjcka7l1qf7by"] +[gd_scene load_steps=2 format=3 uid="uid://b65qrclghjwb1"] -[ext_resource type="Texture2D" uid="uid://bsmuhgmkqrp0a" path="res://demos/boids_simulation/boid.png" id="1_u4ev8"] +[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_nx014"] [node name="Boid" type="Control"] +modulate = Color(0.1634, 0.76712, 0.86, 1) layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -11,10 +12,11 @@ grow_horizontal = 2 grow_vertical = 2 [node name="BoidTexture" type="TextureRect" parent="."] +z_index = -1 layout_mode = 0 offset_left = -12.8 offset_top = -25.6 offset_right = 243.2 offset_bottom = 358.4 scale = Vector2(0.1, 0.1) -texture = ExtResource("1_u4ev8") +texture = SubResource("CompressedTexture2D_nx014") diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index 9c2c0a55..b47946d8 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -2,14 +2,13 @@ extends Node2D @export var boid_count: int = 20 @export var speed: float = 200.0 # Pixels per second -@export var visual_range: float = 100.0 +@export var range: float = 100.0 @export var separation_weight: float = 1.0 @export var alignment_weight: float = 0.5 @export var cohesion_weight: float = 0.5 +@export var noise_weight: float = 0.5 -# Store data for each sprite -var boids = [] -var boids_container: Node2D +@export var scale_factor: float = 0.1 @export_category("Simulation parameters") @export var solver: BoidsSolver @@ -18,7 +17,7 @@ var boids_container: Node2D var velocity: Vector2 = Vector2(100,50) -var texture = preload("res://demos/boids_simulation/boid.png") +var texture = preload("res://demos/boids_simulation/boid.tres") @onready var boid_sprite: Sprite2D = $/root/BoidsModel/Sprite2D @@ -27,95 +26,67 @@ var texture = preload("res://demos/boids_simulation/boid.png") func _ready() -> void: # Create Boids container if it does not exist if not has_node("Boids"): - boids_container = Node2D.new() + var boids_container = Node2D.new() boids_container.name = "Boids" add_child(boids_container) - else: - boids_container = $Boids - - # Create boids as children of the Boids container - for i in range(boid_count): - var boid = Sprite2D.new() - boid.texture = texture - #Assign a boid a random position and standard size - boid.scale = Vector2(0.1, 0.1) - boid.position = Vector2(randf_range(0, get_viewport_rect().size.x), randf_range(0, get_viewport_rect().size.y)) - # Assign a random start rotation (in radians) - boid.rotation = randf_range(0, PI * 2) - boid.name = "Boid " + str(i + 1) # Naming each boid - - boids_container.add_child(boid) - - - - # Initialize boid data - var boid_data = { - "node": boid, - #Assign same speed and random direction based on rotation - "velocity": Vector2(cos(boid.rotation), sin(boid.rotation)).normalized() * speed - } - boids.append(boid_data) + + # Create boids as children of the Boids container + initialize_boids(boid_count) + + solver.initialize() + ##TODO initialize in model? + ## Initialize boid data + #var boid_data = { + #"node": boid, + ##Assign same speed and random direction based on rotation + #"velocity": Vector2(cos(boid.rotation), sin(boid.rotation)).normalized() * speed + #} + #boids.append(boid_data) # Set correct values in GUI # Initialize boids as scenes from res://demos/boids_simulation/boid.tscn # Load the boid sprite - - - # Add border around screen redirecting boids to screen center - - #TODO fix workaround below - self.solver = $Solvers/GDSolver - solver.initialize() - pass + # TODO Add border around screen redirecting boids to screen center + func _process(delta: float) -> void: %FPSLabel.text = "FPS: " + str(Engine.get_frames_per_second()) + + solver.simulation_step(delta) + # Call simulation_step(delta) - #TODO delta display - solver.simulation_step(delta, velocity, speed, visual_range, separation_weight, alignment_weight, cohesion_weight, position, boid_sprite, boids) - wrap_around(boids) + # TODO delta display # Update GUI - pass - + func initialize_boids(target_count: int) -> void: - # Clear existing boids - for boid_data in boids: - boid_data["node"].queue_free() - boids.clear() - - # Create new boids - for i in range(target_count): - add_boid(i) - + var current_count = $Boids.get_child_count() + if current_count > target_count: + var boids = $Boids.get_children() + for boid in range(target_count, current_count).map(func(index): return boids[index]): + boid.queue_free() + else: + for i in range(target_count-current_count): + add_boid(current_count+i) + + func add_boid(i: int): var boid = Sprite2D.new() boid.texture = texture - #Assign a boid a random position and standard size + boid.set_modulate(Color.DARK_SLATE_GRAY) + boid.z_index = -1 boid.scale = Vector2(0.1, 0.1) - boid.position = Vector2(randf_range(0, get_viewport_rect().size.x), randf_range(0, get_viewport_rect().size.y)) - # Assign a random start rotation (in radians) - boid.rotation = randf_range(0, PI * 2) - boid.name = "Boid " + str(i + 1) # Naming each boid - - boids_container.add_child(boid) - - - - # Initialize boid data - var boid_data = { - "node": boid, - #Assign same speed and random direction based on rotation - "velocity": Vector2(cos(boid.rotation), sin(boid.rotation)).normalized() * speed - } - boids.append(boid_data) - + boid.name = "Boid" + str(i) # Naming each boid + $Boids.add_child(boid) + + func wrap_around(boids: Array): + return var screen_size = get_viewport_rect().size for boid in boids: - + # TODO in solver, add offset? # Check horizontal boundaries if boid.node.position.x > screen_size.x: boid.node.position.x = 0 @@ -128,40 +99,39 @@ func wrap_around(boids: Array): elif boid.node.position.y < 0: boid.node.position.y = screen_size.y -func _on_speed_slider_drag_ended(value_changed: bool) -> void: - if value_changed: - speed = %SpeedSlider.value - %SpeedLabel.text = "Speed: " + str(round(speed)) - -func _on_visual_range_slider_drag_ended(value_changed: bool) -> void: - if value_changed: - visual_range = %VisualRangeSlider.value - %VisualRangeLabel.text = "Visual Range: " + str(round(visual_range)) - -func _on_separation_slider_drag_ended(value_changed: bool) -> void: - if value_changed: - separation_weight = %SeparationSlider.value - %SeparationLabel.text = "Separation: " + str(snapped(separation_weight, 0.1)) - -func _on_alignment_slider_drag_ended(value_changed: bool) -> void: - if value_changed: - alignment_weight = %AlignmentSlider.value - %AlignmentLabel.text = "Alignment: " + str(snapped(alignment_weight, 0.1)) - -func _on_cohesion_slider_drag_ended(value_changed: bool) -> void: - if value_changed: - cohesion_weight = %CohesionSlider.value - %CohesionLabel.text = "Cohesion: " + str(snapped(cohesion_weight, 0.1)) - + +func _on_speed_slider_value_changed(value) -> void: + speed = value + %SpeedLabel.text = "Speed: " + str(speed) + +func _on_range_slider_value_changed(value) -> void: + range = value + %RangeLabel.text = "Range: " + str(range) + +func _on_separation_slider_value_changed(value) -> void: + separation_weight = value + %SeparationLabel.text = "Separation: " + str(separation_weight) + +func _on_alignment_slider_value_changed(value) -> void: + alignment_weight = value + %AlignmentLabel.text = "Alignment: " + str(alignment_weight) + +func _on_cohesion_slider_value_changed(value) -> void: + cohesion_weight = value + %CohesionLabel.text = "Cohesion: " + str(cohesion_weight) + +func _on_noise_slider_value_changed(value) -> void: + noise_weight = value + %NoiseLabel.text = "Noise: " + str(noise_weight) + func _on_restart_button_pressed() -> void: solver.initialize() func _on_solver_option_item_selected(index: int) -> void: solver = $Solvers.get_child(index) solver.initialize() - -func _on_number_of_boids_slider_drag_ended(value_changed: bool) -> void: - boid_count = %NumberOfBoidsSlider.value - %NumberOfBoids.text = "Boids: "+str(boid_count) + +func _on_number_of_boids_slider_value_changed(value) -> void: + boid_count = value + %NumberOfBoids.text = "Boids: " + str(boid_count) initialize_boids(boid_count) - diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index 1d5712c7..5f08c383 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -6,59 +6,59 @@ extends BoidsSolver #TODO make noise independent of frame change @export var update_interval: int = 50 # Number of frames between angle updates var frame_counter: int = 0 -var new_direction = Vector2.ONE +var new_direction: Vector2 func initialize() -> void:# - - - - + print("GD Solver") # create position vector and initialize with random Vector2s inside screen - - # create velocity vector and initialize with Vector2s of same value in random directions + #boid.position = Vector2(randf_range(0, get_viewport_rect().size.x), randf_range(0, get_viewport_rect().size.y)) + #Assign a boid a random position and standard size + + # create velocity vector and initialize with Vector2s of same value in random directions + # Assign a random start rotation (in radians) + #boid.rotation = randf_range(0, PI * 2) pass -func simulation_step(delta: float, velocity: Vector2, speed: float, - visual_range: float, separation_weight: float, alignment_weight: float, - cohesion_weight: float, position: Vector2, boid_sprite: Sprite2D, boids: Array) -> void: - - - # for each boid collect others in visual range - # calculate new velocity directions according to: - # Seperation - # Alignment - # Cohesion# - - var separation_range = 50.0 - - - for boid in boids: - var separation = apply_separation(boid, boids, separation_range) - var alignment = apply_alignment(boid, boids, visual_range) - var cohesion = apply_cohesion(boid, boids, visual_range) - - # Combine the three steering behaviors - var steer = separation * separation_weight + alignment * alignment_weight + cohesion * cohesion_weight - - # Blend with current velocity and normalize to maintain speed - boid.velocity = (boid.velocity + steer).normalized() * speed - # (Additional noise) - # Apply a random angle noise every n frames - - update_boids_noise(boids, speed, delta) - - #TODO apply here: boid.node.position += boid.velocity * delta - frame_counter += 1 - - - - - - +# TODO as dictionary? Which inputs are actual inputs +func simulation_step(delta: float) -> void: +# +# + ## for each boid collect others in visual range + ## calculate new velocity directions according to: + ## Seperation + ## Alignment + ## Cohesion# +# + #var separation_range = 50.0 +# +# + #for boid in boids: + #var separation = apply_separation(boid, boids, separation_range) + #var alignment = apply_alignment(boid, boids, visual_range) + #var cohesion = apply_cohesion(boid, boids, visual_range) +# + ## Combine the three steering behaviors + #var steer = separation * separation_weight + alignment * alignment_weight + cohesion * cohesion_weight +# + ## Blend with current velocity and normalize to maintain speed + #boid.velocity = (boid.velocity + steer).normalized() * speed + ## (Additional noise) + ## Apply a random angle noise every n frames +# + #update_boids_noise(boids, speed, delta) +# + ##TODO apply here: boid.node.position += boid.velocity * delta + #frame_counter += 1 + + + + + + pass - + func apply_separation(boid, boids: Array, separation_distance: float) -> Vector2: var steer = Vector2.ZERO var total = 0 @@ -78,9 +78,9 @@ func apply_separation(boid, boids: Array, separation_distance: float) -> Vector2 if total > 0: steer /= total steer = steer.normalized() - + return steer - + func apply_alignment(boid: Dictionary, boids: Array, visual_range: float) -> Vector2: var avg_velocity = Vector2.ZERO var count = 0 @@ -100,7 +100,7 @@ func apply_alignment(boid: Dictionary, boids: Array, visual_range: float) -> Vec return avg_velocity else: return Vector2.ZERO - + func apply_cohesion(boid: Dictionary, boids: Array, visual_range: float) -> Vector2: var center = Vector2.ZERO var count = 0 @@ -126,9 +126,9 @@ func update_boids_noise(boids: Array, speed: float, delta: float) -> void: if frame_counter >= update_interval: frame_counter = 0 for boid in boids: - - - + + + # Apply a small random variation to the current angle var angle_variation = randf_range(-0.5, 0.5) var adjusted_angle = boid.velocity.angle() + angle_variation @@ -136,27 +136,23 @@ func update_boids_noise(boids: Array, speed: float, delta: float) -> void: new_direction = Vector2(cos(adjusted_angle), sin(adjusted_angle)) boid.velocity = new_direction * speed # apply velocities to positions - + boid.node.position += boid.velocity * delta - + #offset angle for right facing direction #TODO initially direct sprite - boid.node.rotation = boid.velocity.angle() + PI / 2 + boid.node.rotation = boid.velocity.angle() + PI / 2 # apply position from position vector to each boid # derive and apply rotation from velocity vector direction to each boid else: for boid in boids: # apply velocities to positions - + boid.node.position += boid.velocity * delta - + #offset angle for right facing direction - boid.node.rotation = boid.velocity.angle() + PI / 2 + boid.node.rotation = boid.velocity.angle() + PI / 2 # apply position from position vector to each boid # derive and apply rotation from velocity vector direction to each boid pass - - - - diff --git a/demo/demos/boids_simulation/main.tscn b/demo/demos/boids_simulation/main.tscn index 173b50d0..5be7bca0 100644 --- a/demo/demos/boids_simulation/main.tscn +++ b/demo/demos/boids_simulation/main.tscn @@ -6,8 +6,9 @@ [sub_resource type="ImageTexture" id="ImageTexture_03ndn"] -[node name="BoidsModel" type="Node2D"] +[node name="BoidsModel" type="Node2D" node_paths=PackedStringArray("solver")] script = ExtResource("1_2fw6l") +solver = NodePath("Solvers/NDSolver") [node name="Solvers" type="Node" parent="."] @@ -21,6 +22,7 @@ params = NodePath("../..") [node name="ColorRect" type="ColorRect" parent="."] show_behind_parent = true +z_index = -10 custom_minimum_size = Vector2(1152, 648) anchors_preset = 5 anchor_left = 0.5 @@ -52,7 +54,7 @@ text = "Frame time: 1" [node name="SliderOptions" type="VBoxContainer" parent="."] custom_minimum_size = Vector2(250, 0) offset_left = 22.0 -offset_top = 432.0 +offset_top = 366.0 offset_right = 272.0 offset_bottom = 626.0 metadata/_edit_group_ = true @@ -61,42 +63,53 @@ metadata/_edit_group_ = true unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Separation: 1" +text = "Separation: 0.5" [node name="SeparationSlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 -min_value = 0.01 -max_value = 4.0 -step = 0.0 -value = 1.0 -exp_edit = true +max_value = 1.0 +step = 0.01 +value = 0.5 [node name="AlignmentLabel" type="Label" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Alignment: 4" +text = "Alignment: 0.5" [node name="AlignmentSlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 -min_value = 1.0 -value = 4.0 -exp_edit = true +max_value = 1.0 +step = 0.01 +value = 0.5 [node name="CohesionLabel" type="Label" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Cohesion: 6" +text = "Cohesion: 0.5" [node name="CohesionSlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 -min_value = 1.0 -value = 6.0 -exp_edit = true +max_value = 1.0 +step = 0.01 +value = 0.5 + +[node name="NoiseLabel" type="Label" parent="SliderOptions"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 30 +text = "Noise: 0.5" + +[node name="NoiseSlider" type="HSlider" parent="SliderOptions"] +unique_name_in_owner = true +layout_mode = 2 +max_value = 1.0 +step = 0.01 +value = 0.5 [node name="RestartButton" type="Button" parent="."] offset_left = 865.0 @@ -153,16 +166,13 @@ metadata/_edit_group_ = true unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Speed: 20" +text = "Speed: 0.5" [node name="SpeedSlider" type="HSlider" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 -min_value = 0.1 -max_value = 10000.0 -step = 0.5 -value = 30.1 -exp_edit = true +max_value = 1000.0 +value = 0.5 [node name="NumberOfBoids" type="Label" parent="SolverOptions"] unique_name_in_owner = true @@ -173,40 +183,30 @@ text = "Boids: 20" [node name="NumberOfBoidsSlider" type="HSlider" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 -min_value = 0.1 -max_value = 10000.0 +max_value = 1000.0 step = 0.5 -value = 30.1 +value = 20.0 exp_edit = true -[node name="VisualRangeLabel" type="Label" parent="SolverOptions"] +[node name="RangeLabel" type="Label" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Visual range: 20" +text = "Range: 0.5" -[node name="VisualRangeSlider" type="HSlider" parent="SolverOptions"] +[node name="RangeSlider" type="HSlider" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 -min_value = 0.1 -max_value = 10000.0 -step = 0.5 -value = 30.1 -exp_edit = true - -[node name="Sprite2D" type="Sprite2D" parent="."] - -[connection signal="drag_ended" from="SliderOptions/SeparationSlider" to="." method="_on_spread_slider_drag_ended"] -[connection signal="drag_ended" from="SliderOptions/SeparationSlider" to="." method="_on_separation_slider_drag_ended"] -[connection signal="drag_ended" from="SliderOptions/AlignmentSlider" to="." method="_on_infected_slider_drag_ended"] -[connection signal="drag_ended" from="SliderOptions/AlignmentSlider" to="." method="_on_alignment_slider_drag_ended"] -[connection signal="drag_ended" from="SliderOptions/CohesionSlider" to="." method="_on_recovery_slider_drag_ended"] -[connection signal="drag_ended" from="SliderOptions/CohesionSlider" to="." method="_on_cohesion_slider_drag_ended"] +max_value = 1.0 +step = 0.01 +value = 0.5 + +[connection signal="value_changed" from="SliderOptions/SeparationSlider" to="." method="_on_separation_slider_value_changed"] +[connection signal="value_changed" from="SliderOptions/AlignmentSlider" to="." method="_on_alignment_slider_value_changed"] +[connection signal="value_changed" from="SliderOptions/CohesionSlider" to="." method="_on_cohesion_slider_value_changed"] +[connection signal="value_changed" from="SliderOptions/NoiseSlider" to="." method="_on_noise_slider_value_changed"] [connection signal="pressed" from="RestartButton" to="." method="_on_restart_button_pressed"] [connection signal="item_selected" from="SolverOption" to="." method="_on_solver_option_item_selected"] -[connection signal="drag_ended" from="SolverOptions/SpeedSlider" to="." method="_on_speed_slider_drag_ended"] -[connection signal="drag_ended" from="SolverOptions/SpeedSlider" to="." method="_on_steps_per_second_slider_drag_ended"] -[connection signal="drag_ended" from="SolverOptions/NumberOfBoidsSlider" to="." method="_on_number_of_boids_slider_drag_ended"] -[connection signal="drag_ended" from="SolverOptions/NumberOfBoidsSlider" to="." method="_on_steps_per_second_slider_drag_ended"] -[connection signal="drag_ended" from="SolverOptions/VisualRangeSlider" to="." method="_on_visual_range_slider_drag_ended"] -[connection signal="drag_ended" from="SolverOptions/VisualRangeSlider" to="." method="_on_steps_per_second_slider_drag_ended"] +[connection signal="value_changed" from="SolverOptions/SpeedSlider" to="." method="_on_speed_slider_value_changed"] +[connection signal="value_changed" from="SolverOptions/NumberOfBoidsSlider" to="." method="_on_number_of_boids_slider_value_changed"] +[connection signal="value_changed" from="SolverOptions/RangeSlider" to="." method="_on_range_slider_value_changed"] diff --git a/demo/demos/boids_simulation/nd_solver.gd b/demo/demos/boids_simulation/nd_solver.gd index 5b9727d7..63f65a17 100644 --- a/demo/demos/boids_simulation/nd_solver.gd +++ b/demo/demos/boids_simulation/nd_solver.gd @@ -1,20 +1,107 @@ extends BoidsSolver -var position: Vector2 = Vector2(500, 500) - -func initialize() -> void:# - - # Load the boid sprite - - #boid_sprite.texture = texture - - # Set the initial position - #boid_sprite.position = position - - - # create position vector and initialize with random Vector2s inside screen - - # create velocity vector and initialize with Vector2s of same value in random directions - - pass -# TODO +var positions: NDArray +var directions: NDArray +var noise_positions: NDArray + +var rng := nd.default_rng() +var screen_size: Vector2 + +func initialize() -> void: + screen_size = params.get_viewport_rect().size + + # Initialize position and direction vector + positions = initialize_position_array(params.boid_count) + directions = initialize_direction_array(params.boid_count) + + # Initalize vector with random noise sampling positions + noise_positions = initialize_sample_position_array(params.boid_count) + + +# Helper function to create position direction vector with length +func initialize_position_array(length: int) -> NDArray: + # Initialize position vector of shape [length, 2] + # Values are random 2D positions on screen + var positions_xy = rng.random([length, 2]) + positions_xy.assign_multiply(positions_xy, nd.array([screen_size.x, screen_size.y], 2)) + return positions_xy + + +# Helper function to create random direction vector with length +func initialize_direction_array(length: int) -> NDArray: + # Initialize angle vector of shape [length] + # Values are random angles in [0, 2*PI), used to create direction vector + var angles := rng.random([length]) + angles.assign_multiply(angles, 2.0 * PI) + + # Initialize direction vector of shape [length, 2] + # Values are normalized 2D direction vectors according to angles + var directions_x := nd.cos(angles) + var directions_y := nd.sin(angles) + return nd.stack([directions_x, directions_y], 1) + + +# Helper function to create random sample position vector with length +func initialize_sample_position_array(length: int) -> NDArray: + # Initialize noise sampling position vector of shape [length] + return nd.add(rng.integers(1e3, null, [length]), rng.random([length])) + + +func simulation_step(delta: float) -> void: + # Check if boid_count has been changed, update vector sizes accordingly + var boid_count_difference = params.boid_count-positions.shape()[0] + if boid_count_difference < 0: + positions = positions.get(nd.range(params.boid_count), nd.range(2)) + directions = directions.get(nd.range(params.boid_count), nd.range(2)) + noise_positions = noise_positions.get(nd.range(params.boid_count)) + elif boid_count_difference > 0: + var new_positions := initialize_position_array(boid_count_difference) + var new_directions := initialize_direction_array(boid_count_difference) + var new_noise_positions := initialize_sample_position_array(boid_count_difference) + positions = nd.vstack([positions, new_positions]) + directions = nd.vstack([directions, new_directions]) + noise_positions = nd.concatenate([noise_positions, new_noise_positions]) + + # Move positions in directions by delta*speed + var offset := nd.multiply(directions, delta*params.speed) + positions.assign_add(positions, offset) + + # Make boid positions wrap around at borders of screen + for axis in [0, 1]: + var positions_axis := positions.get(nd.range(params.boid_count), axis) + var wrap_positive := nd.greater(positions_axis, screen_size[axis]).as_type(nd.Int16) + var wrap_negative := nd.less(positions_axis, 0).as_type(nd.Int16) + wrap_positive.assign_multiply(wrap_positive, -screen_size[axis]) + wrap_negative.assign_multiply(wrap_negative, screen_size[axis]) + positions_axis.assign_add(positions_axis, wrap_positive) + positions_axis.assign_add(positions_axis, wrap_negative) + positions.set(positions_axis, nd.range(params.boid_count), axis) + + # For each force: + # Calculate masks for boids in range + # Calulcate difference to current boid + # Sum over differences and normalize + + # TODO Separation + # TODO Alignment + # TODO Cohesion + + # TODO Noise + + # TODO Add to direction according to weigths and normalize + + update_boids() + +func update_boids() -> void: + var boids := params.get_node("Boids").get_children() + for i in range(boids.size()): + var boid: Node2D = boids[i] + + # Set position of boids by updating origin of transform + boid.transform.origin = positions.get_vector2(i, nd.range(2)) + + # Set rotation of boids by aligning direction with up-vector of transform + var up := directions.get_vector2(i, nd.range(2)) + var right := Vector2(up.y, -up.x) + boid.transform.x = right*params.scale_factor + boid.transform.y = -up*params.scale_factor diff --git a/demo/demos/boids_simulation/solver.gd b/demo/demos/boids_simulation/solver.gd index 9cb6fd78..2fd207c1 100644 --- a/demo/demos/boids_simulation/solver.gd +++ b/demo/demos/boids_simulation/solver.gd @@ -6,10 +6,8 @@ class_name BoidsSolver func initialize() -> void: pass -func simulation_step(delta: float, velocity: Vector2, speed: float, - visual_range: float, separation_weight: float, alignment_weight: float, - cohesion_weight: float, position: Vector2, boid_sprite: Sprite2D, boids: Array) -> void: +func simulation_step(delta: float) -> void: pass -func update_boids_noise(boids: Array, speed: float, delta: float) -> void: +func update_boids() -> void: pass diff --git a/demo/demos/game_of_life/gd_solver.gd b/demo/demos/game_of_life/gd_solver.gd index a8a0992b..f2c379c5 100644 --- a/demo/demos/game_of_life/gd_solver.gd +++ b/demo/demos/game_of_life/gd_solver.gd @@ -6,11 +6,11 @@ var image_data: PackedByteArray func initialize() -> void: grid_front = PackedByteArray() - + # Need one row / column on each side extra so the lookup later is easier grid_front.resize((params.N + 2) * (params.N + 2)) grid_back = grid_front.duplicate() - + image_data = PackedByteArray() image_data.resize(params.N * params.N) place_random() @@ -29,7 +29,7 @@ func simulation_step() -> void: grid_back[to_idx(i, j)] = 1 if (neighbor_count >= 2 and neighbor_count <= 3) else 0 else: grid_back[to_idx(i, j)] = 1 if (neighbor_count == 3) else 0 - + # swap front / back var tmp := grid_front grid_front = grid_back @@ -39,7 +39,7 @@ func on_draw() -> void: for i in range(0, params.N): for j in range(0, params.N): image_data[i + params.N * j] = grid_front[to_idx(i + 1, j + 1)] - + params._image.set_data(params.N, params.N, false, Image.FORMAT_R8, image_data) params.update_texture() diff --git a/demo/demos/sirs_model/nd_solver.gd b/demo/demos/sirs_model/nd_solver.gd index 3f27abba..3e0b2195 100644 --- a/demo/demos/sirs_model/nd_solver.gd +++ b/demo/demos/sirs_model/nd_solver.gd @@ -13,7 +13,7 @@ var rng := nd.default_rng() func initialize() -> void: var grid_size := [params.N, params.N] - + grid_time_since_infection = nd.zeros(grid_size, nd.Int64) grid_time_since_infection.set(params.tau0 + 1) grid_can_infect_neighbor = nd.zeros(grid_size, nd.Bool) @@ -22,32 +22,32 @@ func initialize() -> void: grid_infected_neighbor_count_inner = grid_infected_neighbor_count.get(nd.range(1, -1), nd.range(1, -1)) grid_infected_neighbor_ratio = nd.zeros(grid_size, nd.Float64) grid_new_infected_this_step = nd.zeros(grid_size, nd.Bool) - + neighbor_kernel = nd.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]], nd.Int64); - + # random initial infection place_random() set_border_color() - + func simulation_step() -> void: # Infect grid_can_infect_neighbor.assign_less_equal(grid_time_since_infection, params.tauI) grid_is_infectable.assign_greater(grid_time_since_infection, params.tau0) - + grid_infected_neighbor_count_inner.assign_convolve(grid_can_infect_neighbor, neighbor_kernel) grid_infected_neighbor_ratio.assign_divide(grid_infected_neighbor_count, 4.0) - + grid_infected_neighbor_ratio.assign_multiply(grid_infected_neighbor_ratio, params.spread) grid_new_infected_this_step.assign_less(rng.random(grid_time_since_infection.shape()), grid_infected_neighbor_ratio) grid_new_infected_this_step.assign_logical_and(grid_new_infected_this_step, grid_is_infectable) - + grid_time_since_infection.set(0, grid_new_infected_this_step) - + # Time Pass grid_time_since_infection.assign_add(grid_time_since_infection, 1) # clamp value (not strictly necessary given Int64, but just for completeness) grid_time_since_infection.assign_minimum(grid_time_since_infection, params.tau0 + 1) - + func on_draw() -> void: for i in range(1, params.N-1): for j in range(1, params.N-1): diff --git a/godot-cpp b/godot-cpp index 171fdf82..f3a1a2fd 160000 --- a/godot-cpp +++ b/godot-cpp @@ -1 +1 @@ -Subproject commit 171fdf82afc33caedf99a7bdfdc256602a254f73 +Subproject commit f3a1a2fd458dfaf4de08c906f22a2fe9e924b16f diff --git a/xtensor b/xtensor index 6281ed7e..502b4410 160000 --- a/xtensor +++ b/xtensor @@ -1 +1 @@ -Subproject commit 6281ed7e8312f191792c747daa67e652459fec2a +Subproject commit 502b441086e3a23acdb6d5119d37a6a3b1fb1a3d From 21235ba703c557d1d8cee7cd07a6db119d2c8458 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 20 May 2025 02:35:35 +0200 Subject: [PATCH 20/35] Numdot solver working, removed noise from model --- demo/demos/boids_simulation/boids_model.gd | 67 +++++-------------- demo/demos/boids_simulation/main.tscn | 48 +++++-------- demo/demos/boids_simulation/nd_solver.gd | 78 +++++++++++++++------- 3 files changed, 86 insertions(+), 107 deletions(-) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index b47946d8..a2da2980 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -1,27 +1,20 @@ extends Node2D @export var boid_count: int = 20 -@export var speed: float = 200.0 # Pixels per second +@export var speed: float = 200.0 @export var range: float = 100.0 @export var separation_weight: float = 1.0 @export var alignment_weight: float = 0.5 @export var cohesion_weight: float = 0.5 -@export var noise_weight: float = 0.5 @export var scale_factor: float = 0.1 @export_category("Simulation parameters") @export var solver: BoidsSolver -#var position = Vector2(500, 500) # position vector -var velocity: Vector2 = Vector2(100,50) - - var texture = preload("res://demos/boids_simulation/boid.tres") -@onready var boid_sprite: Sprite2D = $/root/BoidsModel/Sprite2D - -#TODO Implement reset +var frame_time: float = 0. func _ready() -> void: # Create Boids container if it does not exist @@ -30,34 +23,27 @@ func _ready() -> void: boids_container.name = "Boids" add_child(boids_container) + # Synchronize initial values with GUI + boid_count = %NumberOfBoidsSlider.value + speed = %SpeedSlider.value + range = %RangeSlider.value + separation_weight = %SeparationSlider.value + alignment_weight = %AlignmentSlider.value + cohesion_weight = %CohesionSlider.value + # Create boids as children of the Boids container initialize_boids(boid_count) solver.initialize() - ##TODO initialize in model? - ## Initialize boid data - #var boid_data = { - #"node": boid, - ##Assign same speed and random direction based on rotation - #"velocity": Vector2(cos(boid.rotation), sin(boid.rotation)).normalized() * speed - #} - #boids.append(boid_data) - # Set correct values in GUI - - # Initialize boids as scenes from res://demos/boids_simulation/boid.tscn - # Load the boid sprite - - # TODO Add border around screen redirecting boids to screen center func _process(delta: float) -> void: - %FPSLabel.text = "FPS: " + str(Engine.get_frames_per_second()) - + frame_time = Time.get_ticks_usec() solver.simulation_step(delta) + frame_time = Time.get_ticks_usec() - frame_time - # Call simulation_step(delta) - # TODO delta display - # Update GUI + %FPSLabel.text = "FPS: " + str(Engine.get_frames_per_second()) + %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) func initialize_boids(target_count: int) -> void: @@ -77,29 +63,10 @@ func add_boid(i: int): boid.set_modulate(Color.DARK_SLATE_GRAY) boid.z_index = -1 boid.scale = Vector2(0.1, 0.1) - boid.name = "Boid" + str(i) # Naming each boid + boid.name = "Boid" + str(i) $Boids.add_child(boid) -func wrap_around(boids: Array): - return - var screen_size = get_viewport_rect().size - for boid in boids: - - # TODO in solver, add offset? - # Check horizontal boundaries - if boid.node.position.x > screen_size.x: - boid.node.position.x = 0 - elif boid.node.position.x < 0: - boid.node.position.x = screen_size.x - - # Check vertical boundaries - if boid.node.position.y > screen_size.y: - boid.node.position.y = 0 - elif boid.node.position.y < 0: - boid.node.position.y = screen_size.y - - func _on_speed_slider_value_changed(value) -> void: speed = value %SpeedLabel.text = "Speed: " + str(speed) @@ -120,10 +87,6 @@ func _on_cohesion_slider_value_changed(value) -> void: cohesion_weight = value %CohesionLabel.text = "Cohesion: " + str(cohesion_weight) -func _on_noise_slider_value_changed(value) -> void: - noise_weight = value - %NoiseLabel.text = "Noise: " + str(noise_weight) - func _on_restart_button_pressed() -> void: solver.initialize() diff --git a/demo/demos/boids_simulation/main.tscn b/demo/demos/boids_simulation/main.tscn index 5be7bca0..d5413c17 100644 --- a/demo/demos/boids_simulation/main.tscn +++ b/demo/demos/boids_simulation/main.tscn @@ -54,7 +54,7 @@ text = "Frame time: 1" [node name="SliderOptions" type="VBoxContainer" parent="."] custom_minimum_size = Vector2(250, 0) offset_left = 22.0 -offset_top = 366.0 +offset_top = 432.0 offset_right = 272.0 offset_bottom = 626.0 metadata/_edit_group_ = true @@ -63,53 +63,42 @@ metadata/_edit_group_ = true unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Separation: 0.5" +text = "Separation: 0.2" [node name="SeparationSlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 max_value = 1.0 step = 0.01 -value = 0.5 +value = 0.2 [node name="AlignmentLabel" type="Label" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Alignment: 0.5" +text = "Alignment: 0.4 +" [node name="AlignmentSlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 max_value = 1.0 step = 0.01 -value = 0.5 +value = 0.4 [node name="CohesionLabel" type="Label" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Cohesion: 0.5" +text = "Cohesion: 0.6 +" [node name="CohesionSlider" type="HSlider" parent="SliderOptions"] unique_name_in_owner = true layout_mode = 2 max_value = 1.0 step = 0.01 -value = 0.5 - -[node name="NoiseLabel" type="Label" parent="SliderOptions"] -unique_name_in_owner = true -layout_mode = 2 -theme_override_font_sizes/font_size = 30 -text = "Noise: 0.5" - -[node name="NoiseSlider" type="HSlider" parent="SliderOptions"] -unique_name_in_owner = true -layout_mode = 2 -max_value = 1.0 -step = 0.01 -value = 0.5 +value = 0.6 [node name="RestartButton" type="Button" parent="."] offset_left = 865.0 @@ -126,7 +115,7 @@ offset_top = 23.0 offset_right = 1137.0 offset_bottom = 73.0 theme_override_font_sizes/font_size = 30 -selected = 0 +selected = 1 item_count = 2 popup/item_0/text = "GDScript" popup/item_1/text = "NumDot" @@ -166,45 +155,44 @@ metadata/_edit_group_ = true unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Speed: 0.5" +text = "Speed: 200" [node name="SpeedSlider" type="HSlider" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 max_value = 1000.0 -value = 0.5 +value = 200.0 [node name="NumberOfBoids" type="Label" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Boids: 20" +text = "Boids: 50" [node name="NumberOfBoidsSlider" type="HSlider" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 +min_value = 1.0 max_value = 1000.0 step = 0.5 -value = 20.0 +value = 50.0 exp_edit = true [node name="RangeLabel" type="Label" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Range: 0.5" +text = "Range: 100" [node name="RangeSlider" type="HSlider" parent="SolverOptions"] unique_name_in_owner = true layout_mode = 2 -max_value = 1.0 -step = 0.01 -value = 0.5 +max_value = 1000.0 +value = 100.0 [connection signal="value_changed" from="SliderOptions/SeparationSlider" to="." method="_on_separation_slider_value_changed"] [connection signal="value_changed" from="SliderOptions/AlignmentSlider" to="." method="_on_alignment_slider_value_changed"] [connection signal="value_changed" from="SliderOptions/CohesionSlider" to="." method="_on_cohesion_slider_value_changed"] -[connection signal="value_changed" from="SliderOptions/NoiseSlider" to="." method="_on_noise_slider_value_changed"] [connection signal="pressed" from="RestartButton" to="." method="_on_restart_button_pressed"] [connection signal="item_selected" from="SolverOption" to="." method="_on_solver_option_item_selected"] [connection signal="value_changed" from="SolverOptions/SpeedSlider" to="." method="_on_speed_slider_value_changed"] diff --git a/demo/demos/boids_simulation/nd_solver.gd b/demo/demos/boids_simulation/nd_solver.gd index 63f65a17..0ca48192 100644 --- a/demo/demos/boids_simulation/nd_solver.gd +++ b/demo/demos/boids_simulation/nd_solver.gd @@ -2,9 +2,9 @@ extends BoidsSolver var positions: NDArray var directions: NDArray -var noise_positions: NDArray var rng := nd.default_rng() + var screen_size: Vector2 func initialize() -> void: @@ -14,9 +14,6 @@ func initialize() -> void: positions = initialize_position_array(params.boid_count) directions = initialize_direction_array(params.boid_count) - # Initalize vector with random noise sampling positions - noise_positions = initialize_sample_position_array(params.boid_count) - # Helper function to create position direction vector with length func initialize_position_array(length: int) -> NDArray: @@ -41,26 +38,17 @@ func initialize_direction_array(length: int) -> NDArray: return nd.stack([directions_x, directions_y], 1) -# Helper function to create random sample position vector with length -func initialize_sample_position_array(length: int) -> NDArray: - # Initialize noise sampling position vector of shape [length] - return nd.add(rng.integers(1e3, null, [length]), rng.random([length])) - - func simulation_step(delta: float) -> void: # Check if boid_count has been changed, update vector sizes accordingly var boid_count_difference = params.boid_count-positions.shape()[0] if boid_count_difference < 0: positions = positions.get(nd.range(params.boid_count), nd.range(2)) directions = directions.get(nd.range(params.boid_count), nd.range(2)) - noise_positions = noise_positions.get(nd.range(params.boid_count)) elif boid_count_difference > 0: var new_positions := initialize_position_array(boid_count_difference) var new_directions := initialize_direction_array(boid_count_difference) - var new_noise_positions := initialize_sample_position_array(boid_count_difference) positions = nd.vstack([positions, new_positions]) directions = nd.vstack([directions, new_directions]) - noise_positions = nd.concatenate([noise_positions, new_noise_positions]) # Move positions in directions by delta*speed var offset := nd.multiply(directions, delta*params.speed) @@ -77,21 +65,61 @@ func simulation_step(delta: float) -> void: positions_axis.assign_add(positions_axis, wrap_negative) positions.set(positions_axis, nd.range(params.boid_count), axis) - # For each force: - # Calculate masks for boids in range - # Calulcate difference to current boid - # Sum over differences and normalize - - # TODO Separation - # TODO Alignment - # TODO Cohesion - - # TODO Noise - - # TODO Add to direction according to weigths and normalize + # Create pair-wise position differences of boids with shape [n, n, 2] + var position_differences := nd.subtract(positions.get(null, &"newaxis", null), positions.get(&"newaxis", null, null)) + # Calculate distances from position differences with shape [n, n] + var position_distances := nd.norm(position_differences, 2, 2) + # Mark every pair of boids with distance smaller than range in separation mask with shape [n, n] + var vision_mask := nd.less(position_distances, params.range).as_type(nd.Int16) + # Mark every pair of boids with distance smaller than 0.5*range in separation mask with shape [n, n] + var separation_mask := nd.less(position_distances, params.range*0.5).as_type(nd.Int16) + + # Separation + # Normalize position differences to length 1, set identities to length 0 with shape [n, n, 1] + var separation_directions := nd.divide(position_differences, nd.add(position_distances, nd.eye(params.boid_count)).get(null, null, &"newaxis")) + # Ignore boids not marked in separation mask + separation_directions.assign_multiply(position_differences, separation_mask.get(null, null, &"newaxis")) + # Calculate sum of separation directions per boid with shape [n, 2] + var separations := nd.sum(separation_directions, 0) + # Normalize separation directions for each boid + separations.assign_divide(separations, nd.sum(separation_mask, 0).get(null, &"newaxis")) + # Make seperation directions point away from boids in separation range + separations.assign_multiply(separations, -1) + + # Alignment + # Ignore boids not marked in vision mask + var alignment_directions := nd.multiply(directions.get(null, &"newaxis", null), vision_mask.get(null, null, &"newaxis")) + # Calculate sum of alignment directions per boid with shape [n, 2] + var alignments := nd.sum(alignment_directions, 0) + # Normalize alignment directions for each boid + alignments.assign_divide(alignments, nd.norm(alignments, 2, 1).get(null, &"newaxis")) + + # Cohesion + # Ignore boids not marked in cohesion mask + var cohesion_positions := nd.multiply(positions.get(null, &"newaxis", null), vision_mask.get(null, null, &"newaxis")) + # Calculate sum of cohesion positions with shape [n, 1] + var cohesions := nd.sum(cohesion_positions, 0) + # Find cohesion centers by calculating averages + cohesions.assign_divide(cohesions, nd.sum(vision_mask, 0).get(null, &"newaxis")) + # Calculate cohesion directions by taking difference between boids and respective cohesion centers + cohesions.assign_subtract(cohesions, positions) + # Calculate cohesion direction normalization divisor (dist to cohesion center if existing, else 1) + var cohesion_normalization = nd.norm(cohesions, 2, 1) + cohesion_normalization.assign_add(cohesion_normalization, nd.equal(cohesion_normalization, 0.0).as_type(nd.Int16)) + # Normalize cohesion directions + cohesions.assign_divide(cohesions, cohesion_normalization.get(null, &"newaxis")) + + # Update direcctions vector according to separation, alignment and cohesion with respective weights + directions.assign_add(directions, separations.assign_multiply(separations, params.separation_weight*delta)) + directions.assign_add(directions, alignments.assign_multiply(alignments, params.alignment_weight*delta)) + directions.assign_add(directions, cohesions.assign_multiply(cohesions, params.cohesion_weight*delta)) + + # Normalize direction vection lengths to 1 + directions.assign_divide(directions, nd.norm(directions, 2, 1).get(null, &"newaxis")) update_boids() + func update_boids() -> void: var boids := params.get_node("Boids").get_children() for i in range(boids.size()): From e8353ad01859cf15376942902f5ba6517210a3a3 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 20 May 2025 12:27:05 +0200 Subject: [PATCH 21/35] Godot Solver working --- demo/demos/boids_simulation/boids_model.gd | 14 +- demo/demos/boids_simulation/gd_solver.gd | 238 ++++++++------------- demo/demos/boids_simulation/main.tscn | 4 +- demo/demos/boids_simulation/nd_solver.gd | 26 ++- 4 files changed, 115 insertions(+), 167 deletions(-) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index a2da2980..3097f0cb 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -17,12 +17,6 @@ var texture = preload("res://demos/boids_simulation/boid.tres") var frame_time: float = 0. func _ready() -> void: - # Create Boids container if it does not exist - if not has_node("Boids"): - var boids_container = Node2D.new() - boids_container.name = "Boids" - add_child(boids_container) - # Synchronize initial values with GUI boid_count = %NumberOfBoidsSlider.value speed = %SpeedSlider.value @@ -31,7 +25,11 @@ func _ready() -> void: alignment_weight = %AlignmentSlider.value cohesion_weight = %CohesionSlider.value - # Create boids as children of the Boids container + # Instantiate Boid nodes under $Boids + if not has_node("Boids"): + var boids_container = Node2D.new() + boids_container.name = "Boids" + add_child(boids_container) initialize_boids(boid_count) solver.initialize() @@ -62,7 +60,7 @@ func add_boid(i: int): boid.texture = texture boid.set_modulate(Color.DARK_SLATE_GRAY) boid.z_index = -1 - boid.scale = Vector2(0.1, 0.1) + boid.scale *= scale_factor boid.name = "Boid" + str(i) $Boids.add_child(boid) diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index 5f08c383..7d092649 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -1,158 +1,102 @@ extends BoidsSolver -# parameters (speed, boids, etc.) defined in params -# params is set to BoidsModel and can be modified in Editor and in boids_model.gd +var positions: Array[Vector2] +var directions: Array[Vector2] -#TODO make noise independent of frame change -@export var update_interval: int = 50 # Number of frames between angle updates -var frame_counter: int = 0 -var new_direction: Vector2 +var screen_size: Vector2 -func initialize() -> void:# - print("GD Solver") - # create position vector and initialize with random Vector2s inside screen - #boid.position = Vector2(randf_range(0, get_viewport_rect().size.x), randf_range(0, get_viewport_rect().size.y)) - #Assign a boid a random position and standard size +func initialize() -> void: + screen_size = params.get_viewport_rect().size + # Initialize position and direction vector + positions = initialize_position_array(params.boid_count) + directions = initialize_direction_array(params.boid_count) - # create velocity vector and initialize with Vector2s of same value in random directions - # Assign a random start rotation (in radians) - #boid.rotation = randf_range(0, PI * 2) - pass -# TODO as dictionary? Which inputs are actual inputs -func simulation_step(delta: float) -> void: -# -# - ## for each boid collect others in visual range - ## calculate new velocity directions according to: - ## Seperation - ## Alignment - ## Cohesion# -# - #var separation_range = 50.0 -# -# - #for boid in boids: - #var separation = apply_separation(boid, boids, separation_range) - #var alignment = apply_alignment(boid, boids, visual_range) - #var cohesion = apply_cohesion(boid, boids, visual_range) -# - ## Combine the three steering behaviors - #var steer = separation * separation_weight + alignment * alignment_weight + cohesion * cohesion_weight -# - ## Blend with current velocity and normalize to maintain speed - #boid.velocity = (boid.velocity + steer).normalized() * speed - ## (Additional noise) - ## Apply a random angle noise every n frames -# - #update_boids_noise(boids, speed, delta) -# - ##TODO apply here: boid.node.position += boid.velocity * delta - #frame_counter += 1 - - - - - - - - pass - -func apply_separation(boid, boids: Array, separation_distance: float) -> Vector2: - var steer = Vector2.ZERO - var total = 0 - - for other in boids: - if other == boid: - continue - - var dist = boid.node.position.distance_to(other.node.position) - - if dist < separation_distance and dist > 0: - # Vector pointing away from the neighbor, inversely weighted by distance - var diff = (boid.node.position - other.node.position).normalized() / dist - steer += diff - total += 1 - - if total > 0: - steer /= total - steer = steer.normalized() - - return steer - -func apply_alignment(boid: Dictionary, boids: Array, visual_range: float) -> Vector2: - var avg_velocity = Vector2.ZERO - var count = 0 - - for other in boids: - if other == boid: - continue - - var dist = boid.node.position.distance_to(other.node.position) - if dist < visual_range: - avg_velocity += other.velocity - count += 1 - - if count > 0: - avg_velocity /= count - avg_velocity = avg_velocity.normalized() - return avg_velocity - else: - return Vector2.ZERO - -func apply_cohesion(boid: Dictionary, boids: Array, visual_range: float) -> Vector2: - var center = Vector2.ZERO - var count = 0 - - for other in boids: - if other == boid: - continue - - var dist = boid.node.position.distance_to(other.node.position) - if dist < visual_range: - center += other.node.position - count += 1 - - if count > 0: - center /= count - var direction = (center - boid.node.position).normalized() - return direction - else: - return Vector2.ZERO - - -func update_boids_noise(boids: Array, speed: float, delta: float) -> void: - if frame_counter >= update_interval: - frame_counter = 0 - for boid in boids: - - - - # Apply a small random variation to the current angle - var angle_variation = randf_range(-0.5, 0.5) - var adjusted_angle = boid.velocity.angle() + angle_variation - #TODO explain this more - new_direction = Vector2(cos(adjusted_angle), sin(adjusted_angle)) - boid.velocity = new_direction * speed - # apply velocities to positions - - boid.node.position += boid.velocity * delta +# Helper function to create position direction vector with length +func initialize_position_array(length: int) -> Array[Vector2]: + # Initialize position Array with |length| Vector2s + # Values are random 2D-positions on screen + var positions_xy: Array[Vector2] = [] + for i in range(length): + positions_xy.append(Vector2(randf()*screen_size.x, randf()*screen_size.y)) + return positions_xy - #offset angle for right facing direction - #TODO initially direct sprite - boid.node.rotation = boid.velocity.angle() + PI / 2 - # apply position from position vector to each boid - # derive and apply rotation from velocity vector direction to each boid - else: - for boid in boids: - # apply velocities to positions - boid.node.position += boid.velocity * delta +# Helper function to create random direction vector with length +func initialize_direction_array(length: int) -> Array[Vector2]: + # Initialize direction Array with |length| Vector2s + # Values are normalized 2D-vectors with random angle + var directions_xy: Array[Vector2] = [] + for i in range(length): + var angle = 2*PI*randf() + var direction = Vector2(cos(angle), sin(angle)) + directions_xy.append(direction) + return directions_xy - #offset angle for right facing direction - boid.node.rotation = boid.velocity.angle() + PI / 2 - # apply position from position vector to each boid - # derive and apply rotation from velocity vector direction to each boid - pass +func simulation_step(delta: float) -> void: + # Check if boid_count has been changed, update vector sizes accordingly + var boid_count_difference = params.boid_count-positions.size() + if boid_count_difference < 0: + positions.resize(params.boid_count) + directions.resize(params.boid_count) + elif boid_count_difference > 0: + var new_positions := initialize_position_array(boid_count_difference) + var new_directions := initialize_direction_array(boid_count_difference) + positions.append_array(new_positions) + directions.append_array(new_directions) + + # Calculate separation, alignment and cohesion per boid + var separations: Array[Vector2] = [] + var alignments: Array[Vector2] = [] + var cohesions: Array[Vector2] = [] + for i in range(params.boid_count): + var separation := Vector2(0, 0) + var alignment := Vector2(0, 0) + var cohesion := Vector2(0, 0) + for j in range(params.boid_count): + var distance = (positions[i] - positions[j]).length() + if distance < params.range * 0.5 and distance != 0: + separation += (positions[i]-positions[j])/(distance**2) + if distance < params.range: + alignment += directions[j] + cohesion += positions[j]-positions[i] + if separation != Vector2(0, 0): + separation /= separation.length() + if cohesion != Vector2(0, 0): + cohesion /= cohesion.length() + if alignment != Vector2(0, 0): + alignment /= alignment.length() + separations.append(separation) + cohesions.append(cohesion) + alignments.append(alignment) + + for i in range(params.boid_count): + # Apply separation, alignment and cohesion to direction vector and normalize + directions[i] += separations[i] * params.separation_weight * delta * 2.0 + directions[i] += cohesions[i] * params.cohesion_weight * delta + directions[i] += alignments[i] * params.alignment_weight * delta + directions[i] /= directions[i].length() + + # Move positions in directions by delta*speed + positions[i] += directions[i]*delta*params.speed + + # Make boid positions wrap around at borders of screen + positions[i] = Vector2(fposmod(positions[i].x, screen_size.x), fposmod(positions[i].y, screen_size.y)) + update_boids() + + +func update_boids() -> void: + var boids := params.get_node("Boids").get_children() + for i in range(params.boid_count): + var boid: Node2D = boids[i] + + # Set position of boids by updating origin of transform + boid.transform.origin = positions[i] + + # Set rotation of boids by aligning direction with up-vector of transform + var up := directions[i] + var right := Vector2(up.y, -up.x) + boid.transform.x = right*params.scale_factor + boid.transform.y = -up*params.scale_factor diff --git a/demo/demos/boids_simulation/main.tscn b/demo/demos/boids_simulation/main.tscn index d5413c17..4c61bbf2 100644 --- a/demo/demos/boids_simulation/main.tscn +++ b/demo/demos/boids_simulation/main.tscn @@ -8,7 +8,7 @@ [node name="BoidsModel" type="Node2D" node_paths=PackedStringArray("solver")] script = ExtResource("1_2fw6l") -solver = NodePath("Solvers/NDSolver") +solver = NodePath("Solvers/GDSolver") [node name="Solvers" type="Node" parent="."] @@ -115,7 +115,7 @@ offset_top = 23.0 offset_right = 1137.0 offset_bottom = 73.0 theme_override_font_sizes/font_size = 30 -selected = 1 +selected = 0 item_count = 2 popup/item_0/text = "GDScript" popup/item_1/text = "NumDot" diff --git a/demo/demos/boids_simulation/nd_solver.gd b/demo/demos/boids_simulation/nd_solver.gd index 0ca48192..0d0af0ce 100644 --- a/demo/demos/boids_simulation/nd_solver.gd +++ b/demo/demos/boids_simulation/nd_solver.gd @@ -75,16 +75,22 @@ func simulation_step(delta: float) -> void: var separation_mask := nd.less(position_distances, params.range*0.5).as_type(nd.Int16) # Separation - # Normalize position differences to length 1, set identities to length 0 with shape [n, n, 1] - var separation_directions := nd.divide(position_differences, nd.add(position_distances, nd.eye(params.boid_count)).get(null, null, &"newaxis")) + # Calculate separation direction normalization divisor with shape [n, n] + var separation_normalization := nd.square(position_distances) + separation_normalization.assign_add(separation_normalization, nd.eye(params.boid_count)) + # Normalize separation directions inversely proportional to distances with shape [n, n, 1] + var separation_directions := nd.divide(position_differences, separation_normalization.get(null, null, &"newaxis")) # Ignore boids not marked in separation mask separation_directions.assign_multiply(position_differences, separation_mask.get(null, null, &"newaxis")) # Calculate sum of separation directions per boid with shape [n, 2] var separations := nd.sum(separation_directions, 0) - # Normalize separation directions for each boid - separations.assign_divide(separations, nd.sum(separation_mask, 0).get(null, &"newaxis")) # Make seperation directions point away from boids in separation range separations.assign_multiply(separations, -1) + # Calculate separation direction normalization divisor with shape [n] + var separations_normalization := nd.norm(separations, 2, 1) + separations_normalization.assign_add(separations_normalization, nd.equal(separations_normalization, 0.0).as_type(nd.Int16)) + # Normalize separation directions for each boid + separations.assign_divide(separations, separations_normalization.get(null, &"newaxis")) # Alignment # Ignore boids not marked in vision mask @@ -104,13 +110,13 @@ func simulation_step(delta: float) -> void: # Calculate cohesion directions by taking difference between boids and respective cohesion centers cohesions.assign_subtract(cohesions, positions) # Calculate cohesion direction normalization divisor (dist to cohesion center if existing, else 1) - var cohesion_normalization = nd.norm(cohesions, 2, 1) - cohesion_normalization.assign_add(cohesion_normalization, nd.equal(cohesion_normalization, 0.0).as_type(nd.Int16)) + var cohesions_normalization = nd.norm(cohesions, 2, 1) + cohesions_normalization.assign_add(cohesions_normalization, nd.equal(cohesions_normalization, 0.0).as_type(nd.Int16)) # Normalize cohesion directions - cohesions.assign_divide(cohesions, cohesion_normalization.get(null, &"newaxis")) + cohesions.assign_divide(cohesions, cohesions_normalization.get(null, &"newaxis")) - # Update direcctions vector according to separation, alignment and cohesion with respective weights - directions.assign_add(directions, separations.assign_multiply(separations, params.separation_weight*delta)) + # Update directions vector according to separation, alignment and cohesion with respective weights + directions.assign_add(directions, separations.assign_multiply(separations, params.separation_weight*delta*2)) directions.assign_add(directions, alignments.assign_multiply(alignments, params.alignment_weight*delta)) directions.assign_add(directions, cohesions.assign_multiply(cohesions, params.cohesion_weight*delta)) @@ -122,7 +128,7 @@ func simulation_step(delta: float) -> void: func update_boids() -> void: var boids := params.get_node("Boids").get_children() - for i in range(boids.size()): + for i in range(params.boid_count): var boid: Node2D = boids[i] # Set position of boids by updating origin of transform From 39d088ba9b6d8092e2d4c667b7b37c7786974e0e Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 20 May 2025 19:51:08 +0200 Subject: [PATCH 22/35] Fixed ND Solver crashing n range 0 --- demo/demos/boids_simulation/nd_solver.gd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/demos/boids_simulation/nd_solver.gd b/demo/demos/boids_simulation/nd_solver.gd index 0d0af0ce..95412384 100644 --- a/demo/demos/boids_simulation/nd_solver.gd +++ b/demo/demos/boids_simulation/nd_solver.gd @@ -70,9 +70,9 @@ func simulation_step(delta: float) -> void: # Calculate distances from position differences with shape [n, n] var position_distances := nd.norm(position_differences, 2, 2) # Mark every pair of boids with distance smaller than range in separation mask with shape [n, n] - var vision_mask := nd.less(position_distances, params.range).as_type(nd.Int16) + var vision_mask := nd.less_equal(position_distances, params.range).as_type(nd.Int16) # Mark every pair of boids with distance smaller than 0.5*range in separation mask with shape [n, n] - var separation_mask := nd.less(position_distances, params.range*0.5).as_type(nd.Int16) + var separation_mask := nd.less_equal(position_distances, params.range*0.5).as_type(nd.Int16) # Separation # Calculate separation direction normalization divisor with shape [n, n] From bcdab21a225fa3f462486e84224a961030bd70bb Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 21 May 2025 20:44:26 +0200 Subject: [PATCH 23/35] Quickfix to avoid crashing --- demo/demos/boids_simulation/nd_solver.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/demos/boids_simulation/nd_solver.gd b/demo/demos/boids_simulation/nd_solver.gd index 95412384..828f4be5 100644 --- a/demo/demos/boids_simulation/nd_solver.gd +++ b/demo/demos/boids_simulation/nd_solver.gd @@ -128,7 +128,7 @@ func simulation_step(delta: float) -> void: func update_boids() -> void: var boids := params.get_node("Boids").get_children() - for i in range(params.boid_count): + for i in range(min(params.boid_count, boids.size())): var boid: Node2D = boids[i] # Set position of boids by updating origin of transform From 786c26f9aab6d5c08a88a3b36b95c1737aed636e Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 12:30:08 +0200 Subject: [PATCH 24/35] doc strings for boids_model functions --- demo/demos/boids_simulation/boids_model.gd | 66 ++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/demo/demos/boids_simulation/boids_model.gd b/demo/demos/boids_simulation/boids_model.gd index 3097f0cb..b05b5608 100644 --- a/demo/demos/boids_simulation/boids_model.gd +++ b/demo/demos/boids_simulation/boids_model.gd @@ -17,6 +17,9 @@ var texture = preload("res://demos/boids_simulation/boid.tres") var frame_time: float = 0. func _ready() -> void: + """ + Synchronize initial values with GUI, instantiate boid nodes, and initialize solver. + """ # Synchronize initial values with GUI boid_count = %NumberOfBoidsSlider.value speed = %SpeedSlider.value @@ -36,6 +39,12 @@ func _ready() -> void: func _process(delta: float) -> void: + """ + Called every frame. Performs simulation step and updates GUI metrics. + + Parameters: + delta (float): Time since last frame in seconds. + """ frame_time = Time.get_ticks_usec() solver.simulation_step(delta) frame_time = Time.get_ticks_usec() - frame_time @@ -45,6 +54,12 @@ func _process(delta: float) -> void: func initialize_boids(target_count: int) -> void: + """ + Initializes boid nodes to match the specified target count. + + Parameters: + target_count (int): Desired number of boids. + """ var current_count = $Boids.get_child_count() if current_count > target_count: var boids = $Boids.get_children() @@ -56,6 +71,12 @@ func initialize_boids(target_count: int) -> void: func add_boid(i: int): + """ + Adds a single boid sprite to the simulation. + + Parameters: + i (int): Index identifier for naming the new boid. + """ var boid = Sprite2D.new() boid.texture = texture boid.set_modulate(Color.DARK_SLATE_GRAY) @@ -66,33 +87,78 @@ func add_boid(i: int): func _on_speed_slider_value_changed(value) -> void: + """ + Updates speed based on slider input and updates GUI label. + + Parameters: + value (float): New speed value from the slider. + """ speed = value %SpeedLabel.text = "Speed: " + str(speed) func _on_range_slider_value_changed(value) -> void: + """ + Updates range based on slider input and updates GUI label. + + Parameters: + value (float): New range value from the slider. + """ range = value %RangeLabel.text = "Range: " + str(range) func _on_separation_slider_value_changed(value) -> void: + """ + Updates separation weight based on slider input and updates GUI label. + + Parameters: + value (float): New separation weight value. + """ separation_weight = value %SeparationLabel.text = "Separation: " + str(separation_weight) func _on_alignment_slider_value_changed(value) -> void: + """ + Updates alignment weight based on slider input and updates GUI label. + + Parameters: + value (float): New alignment weight value. + """ alignment_weight = value %AlignmentLabel.text = "Alignment: " + str(alignment_weight) func _on_cohesion_slider_value_changed(value) -> void: + """ + Updates cohesion weight based on slider input and updates GUI label. + + Parameters: + value (float): New cohesion weight value. + """ cohesion_weight = value %CohesionLabel.text = "Cohesion: " + str(cohesion_weight) func _on_restart_button_pressed() -> void: + """ + Resets and reinitializes the solver upon pressing the restart button. + """ solver.initialize() func _on_solver_option_item_selected(index: int) -> void: + """ + Switches solver based on selection from options. + + Parameters: + index (int): Index of the newly selected solver. + """ solver = $Solvers.get_child(index) solver.initialize() func _on_number_of_boids_slider_value_changed(value) -> void: + """ + Adjusts the number of boids based on slider input and updates GUI label. + + Parameters: + value (int): New desired count of boids. + """ boid_count = value %NumberOfBoids.text = "Boids: " + str(boid_count) initialize_boids(boid_count) From 787431ecdd23dd22cc87bcd13cee0cae102da842 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 12:41:55 +0200 Subject: [PATCH 25/35] doc strings for gd_solver functions --- demo/demos/boids_simulation/gd_solver.gd | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/demo/demos/boids_simulation/gd_solver.gd b/demo/demos/boids_simulation/gd_solver.gd index 7d092649..8272eeb0 100644 --- a/demo/demos/boids_simulation/gd_solver.gd +++ b/demo/demos/boids_simulation/gd_solver.gd @@ -6,6 +6,10 @@ var directions: Array[Vector2] var screen_size: Vector2 func initialize() -> void: + """ + Initializes the solver by setting the screen size + and generating initial position and direction arrays. + """ screen_size = params.get_viewport_rect().size # Initialize position and direction vector @@ -15,6 +19,15 @@ func initialize() -> void: # Helper function to create position direction vector with length func initialize_position_array(length: int) -> Array[Vector2]: + """ + Creates array of random positions within screen bounds. + + Parameters: + length (int): Number of positions. + + Returns: + Array[Vector2]: Random position vectors. + """ # Initialize position Array with |length| Vector2s # Values are random 2D-positions on screen var positions_xy: Array[Vector2] = [] @@ -25,6 +38,15 @@ func initialize_position_array(length: int) -> Array[Vector2]: # Helper function to create random direction vector with length func initialize_direction_array(length: int) -> Array[Vector2]: + """ + Creates array of normalized random direction vectors. + + Parameters: + length (int): Number of directions. + + Returns: + Array[Vector2]: Random direction vectors. + """ # Initialize direction Array with |length| Vector2s # Values are normalized 2D-vectors with random angle var directions_xy: Array[Vector2] = [] @@ -36,6 +58,13 @@ func initialize_direction_array(length: int) -> Array[Vector2]: func simulation_step(delta: float) -> void: + """ + Updates positions and directions based on separation, + alignment, and cohesion during simulation. + + Parameters: + delta (float): Time since last frame. + """ # Check if boid_count has been changed, update vector sizes accordingly var boid_count_difference = params.boid_count-positions.size() if boid_count_difference < 0: @@ -88,6 +117,9 @@ func simulation_step(delta: float) -> void: func update_boids() -> void: + """ + Updates graphical positions and orientations of boids. + """ var boids := params.get_node("Boids").get_children() for i in range(params.boid_count): var boid: Node2D = boids[i] From 5e3f0401db5c68aebd9e977620c728282657cca6 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 21:49:48 +0200 Subject: [PATCH 26/35] doc strings for nd_solver functions --- demo/demos/boids_simulation/nd_solver.gd | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/demo/demos/boids_simulation/nd_solver.gd b/demo/demos/boids_simulation/nd_solver.gd index 828f4be5..671ad341 100644 --- a/demo/demos/boids_simulation/nd_solver.gd +++ b/demo/demos/boids_simulation/nd_solver.gd @@ -8,6 +8,10 @@ var rng := nd.default_rng() var screen_size: Vector2 func initialize() -> void: + """ + Initializes solver by setting the screen size + and generating initial position and direction arrays. + """ screen_size = params.get_viewport_rect().size # Initialize position and direction vector @@ -17,6 +21,15 @@ func initialize() -> void: # Helper function to create position direction vector with length func initialize_position_array(length: int) -> NDArray: + """ + Generates an NDArray of random positions within screen bounds. + + Parameters: + length (int): Number of positions to generate. + + Returns: + NDArray: Array containing random position vectors. + """ # Initialize position vector of shape [length, 2] # Values are random 2D positions on screen var positions_xy = rng.random([length, 2]) @@ -26,6 +39,15 @@ func initialize_position_array(length: int) -> NDArray: # Helper function to create random direction vector with length func initialize_direction_array(length: int) -> NDArray: + """ + Creates an NDArray of normalized random direction vectors. + + Parameters: + length (int): Number of directions to generate. + + Returns: + NDArray: Array containing random normalized direction vectors. + """ # Initialize angle vector of shape [length] # Values are random angles in [0, 2*PI), used to create direction vector var angles := rng.random([length]) @@ -39,6 +61,13 @@ func initialize_direction_array(length: int) -> NDArray: func simulation_step(delta: float) -> void: + """ + Executes a simulation step updating positions and directions + using NumDot operations for efficiency. + + Parameters: + delta (float): Time elapsed since the last frame. + """ # Check if boid_count has been changed, update vector sizes accordingly var boid_count_difference = params.boid_count-positions.shape()[0] if boid_count_difference < 0: @@ -127,6 +156,10 @@ func simulation_step(delta: float) -> void: func update_boids() -> void: + """ + Updates the graphical representations of boids + to match the computed positions and directions. + """ var boids := params.get_node("Boids").get_children() for i in range(min(params.boid_count, boids.size())): var boid: Node2D = boids[i] From 52810c95782303be8c4984a9c5e3e42182c8758a Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 22:12:10 +0200 Subject: [PATCH 27/35] revert whitespace changes in demo/demos/game_of_life/gd_solver.gd --- demo/demos/game_of_life/gd_solver.gd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/demo/demos/game_of_life/gd_solver.gd b/demo/demos/game_of_life/gd_solver.gd index f2c379c5..a8a0992b 100644 --- a/demo/demos/game_of_life/gd_solver.gd +++ b/demo/demos/game_of_life/gd_solver.gd @@ -6,11 +6,11 @@ var image_data: PackedByteArray func initialize() -> void: grid_front = PackedByteArray() - + # Need one row / column on each side extra so the lookup later is easier grid_front.resize((params.N + 2) * (params.N + 2)) grid_back = grid_front.duplicate() - + image_data = PackedByteArray() image_data.resize(params.N * params.N) place_random() @@ -29,7 +29,7 @@ func simulation_step() -> void: grid_back[to_idx(i, j)] = 1 if (neighbor_count >= 2 and neighbor_count <= 3) else 0 else: grid_back[to_idx(i, j)] = 1 if (neighbor_count == 3) else 0 - + # swap front / back var tmp := grid_front grid_front = grid_back @@ -39,7 +39,7 @@ func on_draw() -> void: for i in range(0, params.N): for j in range(0, params.N): image_data[i + params.N * j] = grid_front[to_idx(i + 1, j + 1)] - + params._image.set_data(params.N, params.N, false, Image.FORMAT_R8, image_data) params.update_texture() From 14608c3952cf24bdeffd947f33307ec54eb93443 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 22:17:39 +0200 Subject: [PATCH 28/35] revert whitespace changes in demo/demos/game_of_life/ --- demo/demos/game_of_life/game_of_life.gd | 6 +++--- demo/demos/game_of_life/nd_solver.gd | 10 +++++----- demo/demos/game_of_life/solver.gd | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/demo/demos/game_of_life/game_of_life.gd b/demo/demos/game_of_life/game_of_life.gd index e8e39721..35361590 100644 --- a/demo/demos/game_of_life/game_of_life.gd +++ b/demo/demos/game_of_life/game_of_life.gd @@ -16,9 +16,9 @@ var frame_time: float = 0. func _ready() -> void: %PointSlider.value = N %PointLabel.text = "Grid: " + str(N) + "x" + str(N) - + resize_image() - + solver.initialize() func _process(delta: float) -> void: @@ -30,7 +30,7 @@ func _process(delta: float) -> void: solver.simulation_step() frame_time = Time.get_ticks_usec() - frame_time current_step = next_step - + %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) solver.on_draw() diff --git a/demo/demos/game_of_life/nd_solver.gd b/demo/demos/game_of_life/nd_solver.gd index 7362f5ba..3704d997 100644 --- a/demo/demos/game_of_life/nd_solver.gd +++ b/demo/demos/game_of_life/nd_solver.gd @@ -11,21 +11,21 @@ var rng := nd.default_rng() func initialize() -> void: var grid_size := [params.N + 2, params.N + 2] - + is_alive = nd.zeros(grid_size, nd.Bool) is_alive_inner = is_alive.get(nd.range(1, -1), nd.range(1, -1)) image_data = nd.empty_like(is_alive_inner) neighour_count = nd.zeros_like(is_alive_inner, nd.Int8) tmp_inner = nd.empty_like(is_alive_inner, nd.Bool) - + neighbor_kernel = nd.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]], nd.Int8); place_random() - + func simulation_step() -> void: neighour_count.assign_convolve(is_alive, neighbor_kernel) - + # This uses masks, not sure if that's the best performing action here. # Boolean operations may be accelerated better! is_alive_inner.set(false, tmp_inner.assign_less(neighour_count, 2)) @@ -39,4 +39,4 @@ func on_draw() -> void: params.update_texture() func place_random() -> void: - is_alive_inner.set(rng.integers(0, 2, is_alive_inner.shape, nd.Bool)) + is_alive_inner.set(rng.integers(0, 2, is_alive_inner.shape(), nd.Bool)) diff --git a/demo/demos/game_of_life/solver.gd b/demo/demos/game_of_life/solver.gd index 73ef4738..12292ee8 100644 --- a/demo/demos/game_of_life/solver.gd +++ b/demo/demos/game_of_life/solver.gd @@ -8,7 +8,7 @@ func initialize() -> void: func simulation_step() -> void: pass - + func on_draw() -> void: pass From fe13483f7414d47fa649573dcb05cfe0c54bd012 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 22:18:59 +0200 Subject: [PATCH 29/35] revert whitespace changes in demo/demos/sirs_model/ --- demo/demos/sirs_model/gd_solver.gd | 10 +++++----- demo/demos/sirs_model/nd_solver.gd | 20 ++++++++++---------- demo/demos/sirs_model/sirs_model.gd | 16 ++++++++-------- demo/demos/sirs_model/solver.gd | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/demo/demos/sirs_model/gd_solver.gd b/demo/demos/sirs_model/gd_solver.gd index 563ec741..c0ad313e 100644 --- a/demo/demos/sirs_model/gd_solver.gd +++ b/demo/demos/sirs_model/gd_solver.gd @@ -9,17 +9,17 @@ func initialize() -> void: # boundary conditions grid[0].fill(params.tau0 + 1) grid[-1].fill(params.tau0 + 1) - + for arr in grid: - arr[0] = params.tau0 + 1 + arr[0] = params.tau0 + 1 arr[-1] = params.tau0 + 1 place_random() gridp = grid.duplicate(true) func simulation_step() -> void: - var infp := 0. - + var infp := 0. + for i in range(1, params.N-1): for j in range(1, params.N-1): if gridp[i][j] == 0: @@ -39,7 +39,7 @@ func on_draw() -> void: params._image.set_pixel(i, j, params.colors[grid[i][j]]) params.update_texture() - + func create_matrix(N: int) -> Array: var matrix := [] for x in range(N): diff --git a/demo/demos/sirs_model/nd_solver.gd b/demo/demos/sirs_model/nd_solver.gd index dd400da4..3f27abba 100644 --- a/demo/demos/sirs_model/nd_solver.gd +++ b/demo/demos/sirs_model/nd_solver.gd @@ -13,7 +13,7 @@ var rng := nd.default_rng() func initialize() -> void: var grid_size := [params.N, params.N] - + grid_time_since_infection = nd.zeros(grid_size, nd.Int64) grid_time_since_infection.set(params.tau0 + 1) grid_can_infect_neighbor = nd.zeros(grid_size, nd.Bool) @@ -22,32 +22,32 @@ func initialize() -> void: grid_infected_neighbor_count_inner = grid_infected_neighbor_count.get(nd.range(1, -1), nd.range(1, -1)) grid_infected_neighbor_ratio = nd.zeros(grid_size, nd.Float64) grid_new_infected_this_step = nd.zeros(grid_size, nd.Bool) - + neighbor_kernel = nd.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]], nd.Int64); - + # random initial infection place_random() set_border_color() - + func simulation_step() -> void: # Infect grid_can_infect_neighbor.assign_less_equal(grid_time_since_infection, params.tauI) grid_is_infectable.assign_greater(grid_time_since_infection, params.tau0) - + grid_infected_neighbor_count_inner.assign_convolve(grid_can_infect_neighbor, neighbor_kernel) grid_infected_neighbor_ratio.assign_divide(grid_infected_neighbor_count, 4.0) - + grid_infected_neighbor_ratio.assign_multiply(grid_infected_neighbor_ratio, params.spread) - grid_new_infected_this_step.assign_less(rng.random(grid_time_since_infection.shape), grid_infected_neighbor_ratio) + grid_new_infected_this_step.assign_less(rng.random(grid_time_since_infection.shape()), grid_infected_neighbor_ratio) grid_new_infected_this_step.assign_logical_and(grid_new_infected_this_step, grid_is_infectable) - + grid_time_since_infection.set(0, grid_new_infected_this_step) - + # Time Pass grid_time_since_infection.assign_add(grid_time_since_infection, 1) # clamp value (not strictly necessary given Int64, but just for completeness) grid_time_since_infection.assign_minimum(grid_time_since_infection, params.tau0 + 1) - + func on_draw() -> void: for i in range(1, params.N-1): for j in range(1, params.N-1): diff --git a/demo/demos/sirs_model/sirs_model.gd b/demo/demos/sirs_model/sirs_model.gd index b7d587d7..d7396b83 100644 --- a/demo/demos/sirs_model/sirs_model.gd +++ b/demo/demos/sirs_model/sirs_model.gd @@ -30,17 +30,17 @@ func _ready() -> void: %SpreadSlider.value = spread %SpreadLabel.text = "Spread: %.3f" % spread - + %InfectedSlider.value = tauI %InfectedLabel.text = "Infection time: " + str(tauI) - + %RecoverySlider.value = tauR %RecoveryLabel.text = "Recovery time: " + str(tauR) - + generate_colors() resize_image() create_legend() - + solver.initialize() func _process(delta: float) -> void: @@ -53,7 +53,7 @@ func _process(delta: float) -> void: solver.simulation_step() frame_time = Time.get_ticks_usec() - frame_time current_step = next_step - + %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) solver.on_draw() @@ -102,7 +102,7 @@ func _on_solver_option_item_selected(index: int) -> void: func update_texture() -> void: texture_rect.texture.update(_image) - + func resize_image() -> void: _image = Image.create(N, N, false, Image.FORMAT_RGBA8) @@ -113,11 +113,11 @@ func resize_image() -> void: func create_legend() -> void: var _img = Image.create(1, 1, false, Image.FORMAT_RGBA8) - + # infected _img.set_pixel(0, 0, color_infected) $LegendInfected.texture = ImageTexture.create_from_image(_img) - + # recovered _img.set_pixel(0, 0, color_recovered) $LegendRecovered.texture = ImageTexture.create_from_image(_img) diff --git a/demo/demos/sirs_model/solver.gd b/demo/demos/sirs_model/solver.gd index a7f3c7a1..e533b3ed 100644 --- a/demo/demos/sirs_model/solver.gd +++ b/demo/demos/sirs_model/solver.gd @@ -8,7 +8,7 @@ func initialize() -> void: func simulation_step() -> void: pass - + func on_draw() -> void: pass From 2a2e92fb7000687ee244c14d3517a665f29d480d Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 22:23:30 +0200 Subject: [PATCH 30/35] revert whitespace changes in demo/demos/game_of_life/nd_solver.gd --- demo/demos/game_of_life/nd_solver.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/demos/game_of_life/nd_solver.gd b/demo/demos/game_of_life/nd_solver.gd index 3704d997..a2cc0ca6 100644 --- a/demo/demos/game_of_life/nd_solver.gd +++ b/demo/demos/game_of_life/nd_solver.gd @@ -39,4 +39,4 @@ func on_draw() -> void: params.update_texture() func place_random() -> void: - is_alive_inner.set(rng.integers(0, 2, is_alive_inner.shape(), nd.Bool)) + is_alive_inner.set(rng.integers(0, 2, is_alive_inner.shape, nd.Bool)) From 4eef504d8cbb31c6d201eba99e608638ceb760ed Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 22:25:37 +0200 Subject: [PATCH 31/35] revert whitespace changes in demo/demos/kuramoto_model/ --- demo/demos/kuramoto_model/kuramoto_model.gd | 16 ++++++++-------- demo/demos/kuramoto_model/nd_solver.gd | 2 +- demo/demos/kuramoto_model/solver.gd | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/demo/demos/kuramoto_model/kuramoto_model.gd b/demo/demos/kuramoto_model/kuramoto_model.gd index 41044a6b..31d90106 100644 --- a/demo/demos/kuramoto_model/kuramoto_model.gd +++ b/demo/demos/kuramoto_model/kuramoto_model.gd @@ -30,7 +30,7 @@ func _ready() -> void: %MeanLabel.text = "Frequency mean: " + str(%MeanSlider.value) %StdSlider.value = frequency_sigma %StdLabel.text = "Frequency std: " + str(%StdSlider.value) - + _restart_simulation() func _process(delta: float) -> void: @@ -39,8 +39,8 @@ func _process(delta: float) -> void: frame_time = Time.get_ticks_usec() solver.simulation_step() frame_time = Time.get_ticks_usec() - frame_time - - %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) + + %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) solver.on_draw() #func _draw() -> void: @@ -89,14 +89,14 @@ func _generate_fireflies() -> void: TEXTURE_SIZE = int(ceil(sqrt(N))) fireflies_data = Image.create(TEXTURE_SIZE, TEXTURE_SIZE, false, Image.FORMAT_RGBAH) fireflies_texture = ImageTexture.create_from_image(fireflies_data) - + fireflies.amount = N fireflies.process_material.set_shader_parameter("fireflies_data", fireflies_texture) - + positions.resize(N) for i in N: positions[i] = Vector2(randf_range(0, get_viewport_rect().size.x), randf_range(0, get_viewport_rect().size.y)) - + func _randomize_positions() -> void: for i in N: positions[i].x = randf_range(0, get_viewport_rect().size.x) @@ -110,12 +110,12 @@ func set_alphas(alphas: Variant) -> void: pixel_pos.x, pixel_pos.y, Color(positions[i].x, positions[i].y, sin(alphas[i])/2 + 0.5, 0) ) - + elif alphas is NDArray: for i in alphas.size(): var pixel_pos := Vector2(int(i / TEXTURE_SIZE), int(i % TEXTURE_SIZE)) fireflies_data.set_pixel( - pixel_pos.x, pixel_pos.y, + pixel_pos.x, pixel_pos.y, Color(positions[i].x, positions[i].y, alphas.get_float(i), 0) ) diff --git a/demo/demos/kuramoto_model/nd_solver.gd b/demo/demos/kuramoto_model/nd_solver.gd index 1751d8dd..d0c690d6 100644 --- a/demo/demos/kuramoto_model/nd_solver.gd +++ b/demo/demos/kuramoto_model/nd_solver.gd @@ -40,7 +40,7 @@ func simulation_step() -> void: func compute_derivative(df: NDArray, phase: NDArray): phase_sin.assign_sin(phase) phase_cos.assign_cos(phase) - phase_coherence.assign_divide(nd.sqrt(nd.add(nd.square(nd.sum(phase_cos)), nd.square(nd.sum(phase_sin)))), phase.size) + phase_coherence.assign_divide(nd.sqrt(nd.add(nd.square(nd.sum(phase_cos)), nd.square(nd.sum(phase_sin)))), phase.size()) avg_phase.assign_atan2(nd.sum(phase_sin), nd.sum(phase_cos)) df.assign_subtract(omega, nd.multiply(nd.multiply(params.coupling, phase_coherence), nd.sin(nd.subtract(phase, avg_phase)))) diff --git a/demo/demos/kuramoto_model/solver.gd b/demo/demos/kuramoto_model/solver.gd index 1b5dbf78..43b8bd97 100644 --- a/demo/demos/kuramoto_model/solver.gd +++ b/demo/demos/kuramoto_model/solver.gd @@ -9,12 +9,12 @@ func initialize() -> void: func simulation_step() -> void: pass - + func on_draw() -> void: pass func generate_frequencies() -> void: pass - + func set_integrator(idx: int) -> void: pass From 60a8355e55b401addeddcc966ca87dbde87b5eb4 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 22:28:30 +0200 Subject: [PATCH 32/35] revert whitespace changes in demo/demos/wave_equation/ and launcher --- demo/demos/launcher/launcher.gd | 4 ++-- demo/demos/wave_equation/gd_solver.gd | 12 ++++++------ demo/demos/wave_equation/nd_solver_opt.gd | 2 +- demo/demos/wave_equation/solver.gd | 2 +- demo/demos/wave_equation/wave_equation.gd | 16 ++++++++-------- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/demo/demos/launcher/launcher.gd b/demo/demos/launcher/launcher.gd index ce2b39cf..678a4c4d 100644 --- a/demo/demos/launcher/launcher.gd +++ b/demo/demos/launcher/launcher.gd @@ -13,7 +13,7 @@ func _ready() -> void: card.set_data(demo) demo_list.add_child(card) demo_list.add_spacer(false) - + func _process(delta: float) -> void: pass @@ -34,6 +34,6 @@ func read_json(path: String) -> void: print(data["path"] + " missing main.tscn!") else: print("JSON file (" + path + ") is missing required keys (name, description, link).") - + func _on_texture_button_pressed() -> void: OS.shell_open("https://godotengine.org/asset-library/asset/3351") diff --git a/demo/demos/wave_equation/gd_solver.gd b/demo/demos/wave_equation/gd_solver.gd index 3b0eb531..8c7559e4 100644 --- a/demo/demos/wave_equation/gd_solver.gd +++ b/demo/demos/wave_equation/gd_solver.gd @@ -9,7 +9,7 @@ func initialize() -> void: x = range(params.num_points).map(func(elt): return (params.dx * elt + params.xmin)) u = params.u.duplicate() uprev = params.uprev.duplicate() - + tmp.resize(params.num_points) func simulation_step(delta: float) -> void: @@ -18,21 +18,21 @@ func simulation_step(delta: float) -> void: for n in params.num_steps_per_frame: for i in range(1, u.size()-1): tmp[i] = 2 * (1 - rsq) * u[i] - uprev[i] + (rsq * (u[i-1] + u[i+1])) - + # boundary condition tmp[0] = tmp[1] if (params.bc_left) else (2 * (1 - rsq) * u[0] - uprev[0] + rsq * u[1]) tmp[-1] = tmp[-2] if (params.bc_right) else (2 * (1 - rsq) * u[-1] - uprev[-1] + rsq * u[-2]) - + uprev = u.duplicate() - u = tmp.duplicate() + u = tmp.duplicate() func on_draw() -> void: for idx in params.draw_array.size(): params.draw_array[idx].x = params.xscale * x[params.draw_range[idx]] params.draw_array[idx].y = params.yscale * u[params.draw_range[idx]] - + params.draw_polyline(params.draw_array, params.point_color, params.point_size) - + if params.bc_left: params.draw_circle(Vector2(x[0], u[0] * params.yscale), params.anchor_size, params.anchor_color) diff --git a/demo/demos/wave_equation/nd_solver_opt.gd b/demo/demos/wave_equation/nd_solver_opt.gd index 3e3b4775..97522263 100644 --- a/demo/demos/wave_equation/nd_solver_opt.gd +++ b/demo/demos/wave_equation/nd_solver_opt.gd @@ -71,4 +71,4 @@ func on_draw() -> void: params.draw_circle(Vector2(x.get_float(0), u.get_float(0) * params.yscale), params.anchor_size, params.anchor_color) if params.bc_right: - params.draw_circle(Vector2(x.get_float(x.size - 1) * params.xscale, u.get_float(u.size - 1) * params.yscale), params.anchor_size, params.anchor_color) + params.draw_circle(Vector2(x.get_float(x.size() - 1) * params.xscale, u.get_float(u.size() - 1) * params.yscale), params.anchor_size, params.anchor_color) diff --git a/demo/demos/wave_equation/solver.gd b/demo/demos/wave_equation/solver.gd index 6b9c5aaa..9dc04e06 100644 --- a/demo/demos/wave_equation/solver.gd +++ b/demo/demos/wave_equation/solver.gd @@ -8,6 +8,6 @@ func initialize() -> void: func simulation_step(delta: float) -> void: pass - + func on_draw() -> void: pass diff --git a/demo/demos/wave_equation/wave_equation.gd b/demo/demos/wave_equation/wave_equation.gd index dd7dfdf9..0b75a9fb 100644 --- a/demo/demos/wave_equation/wave_equation.gd +++ b/demo/demos/wave_equation/wave_equation.gd @@ -24,7 +24,7 @@ extends Node2D # simulation parameters computed @onready var dx: float = (xmax - xmin)/num_points @onready var num_steps_per_frame: int = max(1, ceili(wave_speed/ dx / frame_rate)) # to satisfy CFL condition - + @export var init_params = [ {"x0": 0.5, "sig": 0.005, "amplitude": -2.}, {"mode": 5, "amplitude": 1.}, @@ -60,7 +60,7 @@ func _process(delta: float) -> void: frame_time = Time.get_ticks_usec() solver.simulation_step(delta) frame_time = Time.get_ticks_usec() - frame_time - + %FrameTimeLabel.text = "Delta (ms): " + str(snappedf(frame_time/1000, 1e-3)) queue_redraw() @@ -72,11 +72,11 @@ func _draw() -> void: func _on_solver_option_item_selected(index: int) -> void: solver = $Solvers.get_child(index) restart_simulation() - + func _on_point_slider_drag_ended(value_changed: bool) -> void: %PointLabel.text = "Points: " + str(%PointSlider.value) %CFLLabel.text = "CFL: " + str(snappedf(wave_speed * 1/(frame_rate * num_steps_per_frame * dx), 1e-3)) - + num_points = %PointSlider.value dx = (xmax - xmin)/num_points num_steps_per_frame = max(1, ceili(wave_speed/ dx / frame_rate)) # to satisfy CFL condition @@ -90,10 +90,10 @@ func _on_init_option_item_selected(index: int) -> void: func set_initial_condition(idx) -> void: x = range(num_points).map(func(elt): return (dx * elt + xmin)) - + u.resize(num_points) uprev.resize(num_points) - + match idx: 0: for i in u.size(): @@ -103,7 +103,7 @@ func set_initial_condition(idx) -> void: for i in u.size(): u[i] = init_params[1]["amplitude"] * sin(init_params[1]["mode"] * PI * x[i]) uprev[i] = u[i] - 2: + 2: for i in u.size(): u[i] = init_params[0]["amplitude"] * exp(-(x[i] - init_params[2]["xi"])**2/init_params[0]["sig"]) var delx = wave_speed/frame_rate/num_steps_per_frame @@ -111,7 +111,7 @@ func set_initial_condition(idx) -> void: 3: for i in u.size(): u[i] = init_params[0]["amplitude"] * exp(-(x[i] - init_params[3]["x1"])**2/init_params[0]["sig"]) - init_params[0]["amplitude"] * exp(-(x[i] - init_params[3]["x2"])**2/init_params[0]["sig"]) - + var delx = wave_speed/frame_rate/num_steps_per_frame uprev[i] = init_params[0]["amplitude"] * exp(-(x[i] - init_params[3]["x1"] - delx)**2/init_params[0]["sig"]) - init_params[0]["amplitude"] * exp(-(x[i] - init_params[3]["x2"] + delx)**2/init_params[0]["sig"]) From 0fdbcbdce19d98399c33dfc08ac4885106ccffd9 Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 22:39:10 +0200 Subject: [PATCH 33/35] revert changes () in nd_solvers --- demo/demos/kuramoto_model/nd_solver.gd | 2 +- demo/demos/sirs_model/nd_solver.gd | 2 +- demo/demos/wave_equation/nd_solver_opt.gd | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/demos/kuramoto_model/nd_solver.gd b/demo/demos/kuramoto_model/nd_solver.gd index d0c690d6..1751d8dd 100644 --- a/demo/demos/kuramoto_model/nd_solver.gd +++ b/demo/demos/kuramoto_model/nd_solver.gd @@ -40,7 +40,7 @@ func simulation_step() -> void: func compute_derivative(df: NDArray, phase: NDArray): phase_sin.assign_sin(phase) phase_cos.assign_cos(phase) - phase_coherence.assign_divide(nd.sqrt(nd.add(nd.square(nd.sum(phase_cos)), nd.square(nd.sum(phase_sin)))), phase.size()) + phase_coherence.assign_divide(nd.sqrt(nd.add(nd.square(nd.sum(phase_cos)), nd.square(nd.sum(phase_sin)))), phase.size) avg_phase.assign_atan2(nd.sum(phase_sin), nd.sum(phase_cos)) df.assign_subtract(omega, nd.multiply(nd.multiply(params.coupling, phase_coherence), nd.sin(nd.subtract(phase, avg_phase)))) diff --git a/demo/demos/sirs_model/nd_solver.gd b/demo/demos/sirs_model/nd_solver.gd index 3f27abba..89a5a6c7 100644 --- a/demo/demos/sirs_model/nd_solver.gd +++ b/demo/demos/sirs_model/nd_solver.gd @@ -38,7 +38,7 @@ func simulation_step() -> void: grid_infected_neighbor_ratio.assign_divide(grid_infected_neighbor_count, 4.0) grid_infected_neighbor_ratio.assign_multiply(grid_infected_neighbor_ratio, params.spread) - grid_new_infected_this_step.assign_less(rng.random(grid_time_since_infection.shape()), grid_infected_neighbor_ratio) + grid_new_infected_this_step.assign_less(rng.random(grid_time_since_infection.shape), grid_infected_neighbor_ratio) grid_new_infected_this_step.assign_logical_and(grid_new_infected_this_step, grid_is_infectable) grid_time_since_infection.set(0, grid_new_infected_this_step) diff --git a/demo/demos/wave_equation/nd_solver_opt.gd b/demo/demos/wave_equation/nd_solver_opt.gd index 97522263..3e3b4775 100644 --- a/demo/demos/wave_equation/nd_solver_opt.gd +++ b/demo/demos/wave_equation/nd_solver_opt.gd @@ -71,4 +71,4 @@ func on_draw() -> void: params.draw_circle(Vector2(x.get_float(0), u.get_float(0) * params.yscale), params.anchor_size, params.anchor_color) if params.bc_right: - params.draw_circle(Vector2(x.get_float(x.size() - 1) * params.xscale, u.get_float(u.size() - 1) * params.yscale), params.anchor_size, params.anchor_color) + params.draw_circle(Vector2(x.get_float(x.size - 1) * params.xscale, u.get_float(u.size - 1) * params.yscale), params.anchor_size, params.anchor_color) From c2a3d3f59a969b3962d270a93775e544523a209b Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 22:46:33 +0200 Subject: [PATCH 34/35] revert changes in xtensor --- xtensor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xtensor b/xtensor index 502b4410..6281ed7e 160000 --- a/xtensor +++ b/xtensor @@ -1 +1 @@ -Subproject commit 502b441086e3a23acdb6d5119d37a6a3b1fb1a3d +Subproject commit 6281ed7e8312f191792c747daa67e652459fec2a From 6967335810d38a2d9e7ee49d321009f7a6433fde Mon Sep 17 00:00:00 2001 From: kro-ma Date: Sun, 1 Jun 2025 22:47:58 +0200 Subject: [PATCH 35/35] revert changes in godot-cpp --- godot-cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/godot-cpp b/godot-cpp index f3a1a2fd..171fdf82 160000 --- a/godot-cpp +++ b/godot-cpp @@ -1 +1 @@ -Subproject commit f3a1a2fd458dfaf4de08c906f22a2fe9e924b16f +Subproject commit 171fdf82afc33caedf99a7bdfdc256602a254f73