diff --git a/assets/test/shaders/pathfinding/CMakeLists.txt b/assets/test/shaders/pathfinding/CMakeLists.txt
new file mode 100644
index 0000000000..2d30f59426
--- /dev/null
+++ b/assets/test/shaders/pathfinding/CMakeLists.txt
@@ -0,0 +1,4 @@
+install(DIRECTORY "."
+ DESTINATION "${ASSET_DIR}/test/shaders/pathfinding"
+ FILES_MATCHING PATTERN "*.glsl"
+)
diff --git a/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl
new file mode 100644
index 0000000000..7ccb86dcf4
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl
@@ -0,0 +1,17 @@
+#version 330
+
+in float v_cost;
+
+out vec4 out_col;
+
+void main()
+{
+ if (v_cost == 255.0) {
+ out_col = vec4(0.0, 0.0, 0.0, 1.0);
+ return;
+ }
+ float cost = (v_cost / 256) * 2.0;
+ float red = clamp(cost, 0.0, 1.0);
+ float green = clamp(2.0 - cost, 0.0, 1.0);
+ out_col = vec4(red, green, 0.0, 1.0);
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl
new file mode 100644
index 0000000000..d12f0db1d9
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl
@@ -0,0 +1,15 @@
+#version 330
+
+layout (location = 0) in vec3 position;
+layout (location = 1) in float cost;
+
+uniform mat4 model;
+uniform mat4 view;
+uniform mat4 proj;
+
+out float v_cost;
+
+void main() {
+ gl_Position = proj * view * model * vec4(position, 1.0);
+ v_cost = cost;
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_display.frag.glsl b/assets/test/shaders/pathfinding/demo_0_display.frag.glsl
new file mode 100644
index 0000000000..a6732d0c7f
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_display.frag.glsl
@@ -0,0 +1,10 @@
+#version 330
+
+uniform sampler2D color_texture;
+
+in vec2 v_uv;
+out vec4 col;
+
+void main() {
+ col = texture(color_texture, v_uv);
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_display.vert.glsl b/assets/test/shaders/pathfinding/demo_0_display.vert.glsl
new file mode 100644
index 0000000000..6112530242
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_display.vert.glsl
@@ -0,0 +1,10 @@
+#version 330
+
+layout(location=0) in vec2 position;
+layout(location=1) in vec2 uv;
+out vec2 v_uv;
+
+void main() {
+ gl_Position = vec4(position, 0.0, 1.0);
+ v_uv = uv;
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl
new file mode 100644
index 0000000000..b5ff4042a3
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl
@@ -0,0 +1,29 @@
+#version 330
+
+in float v_cost;
+
+out vec4 outcol;
+
+void main() {
+ int cost = int(v_cost);
+ if (bool(cost & 0x40)) {
+ // wavefront blocked
+ outcol = vec4(0.9, 0.9, 0.9, 1.0);
+ return;
+ }
+
+ if (bool(cost & 0x20)) {
+ // line of sight
+ outcol = vec4(1.0, 1.0, 1.0, 1.0);
+ return;
+ }
+
+ if (bool(cost & 0x10)) {
+ // pathable
+ outcol = vec4(0.7, 0.7, 0.7, 1.0);
+ return;
+ }
+
+ // not pathable
+ outcol = vec4(0.0, 0.0, 0.0, 1.0);
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl
new file mode 100644
index 0000000000..2c2190e57c
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl
@@ -0,0 +1,15 @@
+#version 330
+
+layout(location=0) in vec3 position;
+layout(location=1) in float cost;
+
+uniform mat4 model;
+uniform mat4 view;
+uniform mat4 proj;
+
+out float v_cost;
+
+void main() {
+ gl_Position = proj * view * model * vec4(position, 1.0);
+ v_cost = cost;
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_grid.frag.glsl b/assets/test/shaders/pathfinding/demo_0_grid.frag.glsl
new file mode 100644
index 0000000000..19d2e459c3
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_grid.frag.glsl
@@ -0,0 +1,8 @@
+#version 330
+
+out vec4 out_col;
+
+void main()
+{
+ out_col = vec4(0.0, 0.0, 0.0, 0.3);
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_grid.vert.glsl b/assets/test/shaders/pathfinding/demo_0_grid.vert.glsl
new file mode 100644
index 0000000000..2f5eaeaa15
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_grid.vert.glsl
@@ -0,0 +1,11 @@
+#version 330
+
+layout (location = 0) in vec3 position;
+
+uniform mat4 model;
+uniform mat4 view;
+uniform mat4 proj;
+
+void main() {
+ gl_Position = proj * view * model * vec4(position, 1.0);
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl
new file mode 100644
index 0000000000..d40715a338
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl
@@ -0,0 +1,16 @@
+#version 330
+
+in float v_cost;
+
+out vec4 out_col;
+
+void main()
+{
+ if (v_cost > 512.0) {
+ out_col = vec4(0.0, 0.0, 0.0, 1.0);
+ return;
+ }
+ float cost = 0.05 * v_cost;
+ float green = clamp(1.0 - cost, 0.0, 1.0);
+ out_col = vec4(0.75, green, 0.5, 1.0);
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl
new file mode 100644
index 0000000000..d12f0db1d9
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl
@@ -0,0 +1,15 @@
+#version 330
+
+layout (location = 0) in vec3 position;
+layout (location = 1) in float cost;
+
+uniform mat4 model;
+uniform mat4 view;
+uniform mat4 proj;
+
+out float v_cost;
+
+void main() {
+ gl_Position = proj * view * model * vec4(position, 1.0);
+ v_cost = cost;
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_obj.frag.glsl b/assets/test/shaders/pathfinding/demo_0_obj.frag.glsl
new file mode 100644
index 0000000000..ebbb10bc8f
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_obj.frag.glsl
@@ -0,0 +1,9 @@
+#version 330
+
+uniform vec4 color;
+
+out vec4 outcol;
+
+void main() {
+ outcol = color;
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_obj.vert.glsl b/assets/test/shaders/pathfinding/demo_0_obj.vert.glsl
new file mode 100644
index 0000000000..527e0bb652
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_obj.vert.glsl
@@ -0,0 +1,7 @@
+#version 330
+
+layout(location=0) in vec2 position;
+
+void main() {
+ gl_Position = vec4(position, 0.0, 1.0);
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_vector.frag.glsl b/assets/test/shaders/pathfinding/demo_0_vector.frag.glsl
new file mode 100644
index 0000000000..ebbb10bc8f
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_vector.frag.glsl
@@ -0,0 +1,9 @@
+#version 330
+
+uniform vec4 color;
+
+out vec4 outcol;
+
+void main() {
+ outcol = color;
+}
diff --git a/assets/test/shaders/pathfinding/demo_0_vector.vert.glsl b/assets/test/shaders/pathfinding/demo_0_vector.vert.glsl
new file mode 100644
index 0000000000..8b6b015408
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_0_vector.vert.glsl
@@ -0,0 +1,11 @@
+#version 330
+
+layout(location=0) in vec3 position;
+
+uniform mat4 model;
+uniform mat4 view;
+uniform mat4 proj;
+
+void main() {
+ gl_Position = proj * view * model * vec4(position, 1.0);
+}
diff --git a/assets/test/shaders/pathfinding/demo_1_display.frag.glsl b/assets/test/shaders/pathfinding/demo_1_display.frag.glsl
new file mode 100644
index 0000000000..a6732d0c7f
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_1_display.frag.glsl
@@ -0,0 +1,10 @@
+#version 330
+
+uniform sampler2D color_texture;
+
+in vec2 v_uv;
+out vec4 col;
+
+void main() {
+ col = texture(color_texture, v_uv);
+}
diff --git a/assets/test/shaders/pathfinding/demo_1_display.vert.glsl b/assets/test/shaders/pathfinding/demo_1_display.vert.glsl
new file mode 100644
index 0000000000..6112530242
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_1_display.vert.glsl
@@ -0,0 +1,10 @@
+#version 330
+
+layout(location=0) in vec2 position;
+layout(location=1) in vec2 uv;
+out vec2 v_uv;
+
+void main() {
+ gl_Position = vec4(position, 0.0, 1.0);
+ v_uv = uv;
+}
diff --git a/assets/test/shaders/pathfinding/demo_1_grid.frag.glsl b/assets/test/shaders/pathfinding/demo_1_grid.frag.glsl
new file mode 100644
index 0000000000..19d2e459c3
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_1_grid.frag.glsl
@@ -0,0 +1,8 @@
+#version 330
+
+out vec4 out_col;
+
+void main()
+{
+ out_col = vec4(0.0, 0.0, 0.0, 0.3);
+}
diff --git a/assets/test/shaders/pathfinding/demo_1_grid.vert.glsl b/assets/test/shaders/pathfinding/demo_1_grid.vert.glsl
new file mode 100644
index 0000000000..cbf6712eef
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_1_grid.vert.glsl
@@ -0,0 +1,11 @@
+#version 330
+
+layout (location = 0) in vec2 position;
+
+uniform mat4 model;
+uniform mat4 view;
+uniform mat4 proj;
+
+void main() {
+ gl_Position = proj * view * model * vec4(position, 0.0, 1.0);
+}
diff --git a/assets/test/shaders/pathfinding/demo_1_obj.frag.glsl b/assets/test/shaders/pathfinding/demo_1_obj.frag.glsl
new file mode 100644
index 0000000000..ebbb10bc8f
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_1_obj.frag.glsl
@@ -0,0 +1,9 @@
+#version 330
+
+uniform vec4 color;
+
+out vec4 outcol;
+
+void main() {
+ outcol = color;
+}
diff --git a/assets/test/shaders/pathfinding/demo_1_obj.vert.glsl b/assets/test/shaders/pathfinding/demo_1_obj.vert.glsl
new file mode 100644
index 0000000000..bf804ffe53
--- /dev/null
+++ b/assets/test/shaders/pathfinding/demo_1_obj.vert.glsl
@@ -0,0 +1,11 @@
+#version 330
+
+layout(location=0) in vec2 position;
+
+uniform mat4 model;
+uniform mat4 view;
+uniform mat4 proj;
+
+void main() {
+ gl_Position = proj * view * model * vec4(position, 0.0, 1.0);
+}
diff --git a/doc/code/images/pathfinder_architecture.svg b/doc/code/images/pathfinder_architecture.svg
new file mode 100644
index 0000000000..2046a1a486
--- /dev/null
+++ b/doc/code/images/pathfinder_architecture.svg
@@ -0,0 +1,175 @@
+
+
+
diff --git a/doc/code/images/pathfinder_architecture.uxf b/doc/code/images/pathfinder_architecture.uxf
new file mode 100644
index 0000000000..3a051b2acf
--- /dev/null
+++ b/doc/code/images/pathfinder_architecture.uxf
@@ -0,0 +1,271 @@
+
+
+ 10
+
+ UMLClass
+
+ 710
+ 510
+ 120
+ 60
+
+
+*Grid*
+
+
+
+ UMLClass
+
+ 710
+ 620
+ 120
+ 60
+
+
+*Sector*
+
+
+
+ UMLClass
+
+ 630
+ 730
+ 120
+ 60
+
+
+*CostField*
+
+
+
+ UMLClass
+
+ 790
+ 730
+ 120
+ 60
+
+
+*Portal*
+
+
+
+ UMLClass
+
+ 710
+ 400
+ 120
+ 60
+
+
+*Pathfinder*
+
+
+
+ UMLClass
+
+ 980
+ 400
+ 120
+ 60
+
+
+*Integrator*
+
+
+
+ UMLClass
+
+ 1040
+ 490
+ 160
+ 60
+
+
+*IntegrationField*
+
+
+
+ UMLClass
+
+ 1040
+ 560
+ 120
+ 60
+
+
+*FlowField*
+
+
+
+ UMLClass
+
+ 520
+ 400
+ 120
+ 60
+
+
+*Path*
+
+
+
+ Relation
+
+ 760
+ 450
+ 30
+ 80
+
+ lt=<.
+ 10.0;60.0;10.0;10.0
+
+
+ Relation
+
+ 760
+ 560
+ 30
+ 80
+
+ lt=<.
+ 10.0;60.0;10.0;10.0
+
+
+ Relation
+
+ 720
+ 670
+ 30
+ 80
+
+ lt=<.
+ 10.0;60.0;10.0;10.0
+
+
+ Relation
+
+ 800
+ 670
+ 30
+ 80
+
+ lt=<.
+ 10.0;60.0;10.0;10.0
+
+
+ Relation
+
+ 820
+ 420
+ 180
+ 30
+
+ lt=<.
+ 160.0;10.0;10.0;10.0
+
+
+ Relation
+
+ 990
+ 450
+ 30
+ 160
+
+ lt=.
+ 10.0;140.0;10.0;10.0
+
+
+ Relation
+
+ 990
+ 510
+ 70
+ 30
+
+ lt=<.
+ 50.0;10.0;10.0;10.0
+
+
+ Relation
+
+ 990
+ 580
+ 70
+ 30
+
+ lt=<.
+ 50.0;10.0;10.0;10.0
+
+
+ Relation
+
+ 630
+ 420
+ 100
+ 30
+
+ lt=<.
+ 10.0;10.0;80.0;10.0
+
+
+ Relation
+
+ 760
+ 200
+ 30
+ 220
+
+ lt=<.
+ 10.0;200.0;10.0;10.0
+
+
+ Text
+
+ 460
+ 330
+ 240
+ 70
+
+ *Pathfinder*
+fontsize=22
+style=wordwrap
+
+
+
+ Relation
+
+ 450
+ 300
+ 770
+ 30
+
+ lt=-
+ 10.0;10.0;750.0;10.0
+
+
+ Text
+
+ 730
+ 160
+ 130
+ 70
+
+ Movement, collision, etc.
+style=wordwrap
+
+
+
+ Text
+
+ 460
+ 160
+ 240
+ 70
+
+ *Gamestate*
+fontsize=22
+style=wordwrap
+
+
+
diff --git a/doc/code/pathfinding/README.md b/doc/code/pathfinding/README.md
new file mode 100644
index 0000000000..a7ec70d1bd
--- /dev/null
+++ b/doc/code/pathfinding/README.md
@@ -0,0 +1,122 @@
+# Pathfinding
+
+openage's pathfinding subsystem implements structures used for navigating game entities in the
+game world. These structures define movement costs for a map and allow search for a path
+from one coordinate in the game world to another.
+
+Pathfinding is a subsystem of the [game simulation](/doc/code/game_simulation/README.md) where
+it is primarily used for movement and placement of game entities.
+
+1. [Architecture](#architecture)
+2. [Workflow](#workflow)
+
+
+## Architecture
+
+The architecture of the pathfinder is heavily based on the article *Crowd Pathfinding and Steering*
+*Using Flow Field Tiles* by Elijah Emerson (available [here](http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf)). A core design
+decision taken from this article was the usage of flow fields as the basis for the pathing algorithm.
+
+Flow fields offer a few advantages for large group movements, i.e. the movement you typically see in
+RTS games like Age of Empires. For example, they allow reusing already computed path results for
+subsequent pathing requests and can also be used for smart collision avoidance. One downside
+of using flow fields can be the heavy upfront cost of pathing calculations. However, the downsides
+can be mitigated to some degree with caching and high-level optimizations.
+
+The openage pathfinder is tile-based, like most flow field pathfinding implementations. Every tile
+has a movement cost associated with it that represents the cost of a game entity moving on that tile.
+When computing a path from A to B, the pathfinder tries to find the sequence of tiles with the cheapest
+accumulated movement cost.
+
+In openage, the pathfinding subsystem is independent from the terrain implementation in the game
+simulation. Terrain data may be used to influence movement cost during gameplay, but pathfinding
+can be initialized without any requirements for terrain code. By only loosely connecting these two system,
+we have a much more fine-grained control that allows for better optimization of the pathfinder.
+The most relevant similarity between terrain and pathfinding code is that they use the same
+[coordinate systems](/doc/code/coordinate-systems.md#tiletile3). To make the distinction between
+pathfinding and terrain more clear, we use the term *cells* for tiles in the pathfinder.
+
+![UML pathfinding classes](/doc/code/images/pathfinder_architecture.svg)
+
+The relationship between classes in the pathfinder can be seen above. `Grid` is the top-level structure
+for storage of movement cost. There may be multiple grids defined, one for each movement type
+used by game entities in the game simulation.
+
+Every grid is subdivided into sectors, represented by the `Sector` class. Each sectors holds a
+pointer to a `CostField` which stores the movement cost of individual cells. All sectors on the
+same grid have a fixed square size that is determined by the grid. Thus, the sector size on
+a grid is always consistent but different grid may utilize different sector sizes.
+
+Sectors on a grid are connect to each other with so-called portals, see the `Portal`
+class. Portals represent a passable gateway between two sectors where game entities
+can pass through. As such, portals store the coordinates of the cells in each sector
+where game entities can pass. `Portal` objects are created for every continuous sequence of cells
+on the edges between two sectors where the cells are passable on both sides of the two sectors.
+Therefore, there may be multiple portals defined for the edge between two sectors.
+
+Additionally, each portal stores which other portals it can reach in a specific sector. The
+result is a portal "graph" that can be searched separately to determine which portals
+and sectors are visited for a specific pathing request. This is used in the high-level
+pathfinder to preselect the sectors that flow fields need to be generated for (see the
+[Workflow section](#workflow)).
+
+The individual movement cost of each cell in a sector are recorded in a `CostField` object.
+The cost of a cell can range from 1 (minimum cost) to 254 (maximum cost), while a cost of 255
+makes a cell impassible. For Age of Empires, usually only the minimum and impassable cost
+values are relevant. The cost field is built when the grid is first initialized and
+individual cost of cells can be altered during gameplay events.
+
+To get a path between two coordinates, the game simulation mainly interfaces with a `Pathfinder`
+object. The `Pathfinder` class calls the actual pathfinding algorithms used for searching
+for a path and stores references to all available grids in the pathfinding subsystems. It can receive
+`PathRequest` objects that contain information about the grid that should be searched as well as
+the start and target coordinates of the desired path. Found paths are returned as `Path` objects
+which store the waypoint coordinates for the computed path.
+
+Flow field calculations are controlled by an `Integrator` object, which process a cost field
+from a sector as well as target coordinates to compute an `IntegrationField` and a `FlowField` object.
+`IntegrationField`s are intermediary objects that store the accumulated cost of reaching
+the target cell for each other cell in the field, using the cell values in the cost field as basis.
+From the integration field, a `FlowField` object is created. Cells in the flow field store
+movement vectors that point to their next cheapest neighbor cell in the integration field. `Path`s
+may be computed from these flow field by simply following the movement vectors until the
+target coordinate is reached.
+
+
+## Workflow
+
+To initiate a new search, the pathfinder receives a `PathRequest` object, e.g. from the game simulation.
+Every path request contains the following information:
+
+- ID of the grid to search
+- start cell coordinates
+- target cell coordinates
+
+The actual pathfinding algorithm is split into two stages.
+
+1. High-level portal-based search to identify the visited sectors on the grid
+2. Low-level flow field-based search in the identified sectors to find the waypoints for the path
+
+The high-level search is accomplished by utilizing the portal graph, i.e. the
+connections between portals in each sector. From the portal graph, a node mesh is created
+that can be searched with a graph-traversal algorithm. For this purpose, the A\* algorithm
+is used. The result of the high-level search is a list of sectors and portals that are
+traversed by the path.
+
+The high-level search is mainly motivated by the need to avoid costly flow field
+calculations on the whole grid. As the portal graph should already be precomputed when
+a path request is made, the main influence on performance is the A\* algorithm. Given
+a limited number of portals, the A\* search should overall be very cheap.
+
+The resulting list of sectors and portals is subsequently used in the low-level flow
+field calculations. As a first step, the pathfinder uses its integrator to generate
+a flow field for each identified sector. Generation starts with the target sector
+and ends with the start sector. Flow field results are passed through at the cells
+of the identified portals to make the flow between sectors seamless.
+
+
+
+In a second step, the pathfinder follows the movement vectors in the flow fields from
+the start cell to the target cell. Waypoints are created for every direction change, so
+that game entities can travel in straight lines between them. The list of waypoints
+from start to target is then returned by the pathfinder via a `Path` object.
diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py
index ca8f92083c..0dfac99af5 100644
--- a/etc/gdb_pretty/printers.py
+++ b/etc/gdb_pretty/printers.py
@@ -279,6 +279,136 @@ def children(self):
yield ('time', self.__val['time'])
yield ('value', self.__val['value'])
+
+@printer_typedef('openage::path::flow_t')
+class PathFlowTypePrinter:
+ """
+ Pretty printer for openage::path::flow_t.
+
+ TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros.
+ """
+
+ FLOW_FLAGS: dict = {
+ 0x10: 'PATHABLE',
+ 0x20: 'LOS',
+ 0x40: 'WAVEFRONT_BLOCKED',
+ 0x80: 'UNUSED',
+ }
+
+ FLOW_DIRECTION: dict = {
+ 0x00: 'NORTH',
+ 0x01: 'NORTHEAST',
+ 0x02: 'EAST',
+ 0x03: 'SOUTHEAST',
+ 0x04: 'SOUTH',
+ 0x05: 'SOUTHWEST',
+ 0x06: 'WEST',
+ 0x07: 'NORTHWEST',
+ }
+
+ def __init__(self, val: gdb.Value):
+ self.__val = val
+
+ def to_string(self):
+ """
+ Get the flow type as a string.
+ """
+ flow = int(self.__val)
+ flags = flow & 0xF0
+ direction = flow & 0x0F
+ return (f"{self.FLOW_DIRECTION.get(direction, 'INVALID')} ("
+ f"{', '.join([flag for mask, flag in self.FLOW_FLAGS.items() if mask & flags])})")
+
+ def children(self):
+ """
+ Get the displayed children of the flow type.
+ """
+ flow = int(self.__val)
+ flags = flow & 0xF0
+ direction = flow & 0x0F
+ yield ('direction', self.FLOW_DIRECTION[direction])
+ for mask, flag in self.FLOW_FLAGS.items():
+ yield (flag, bool(flags & mask))
+
+
+# Integrated flags
+INTEGRATED_FLAGS: dict = {
+ 0x01: 'LOS',
+ 0x02: 'WAVEFRONT_BLOCKED',
+}
+
+
+def get_integrated_flags_list(value: int) -> str:
+ """
+ Get the list of flags as a string.
+
+ :param value: The value to get the flags for.
+ :type value: int
+ """
+ flags = []
+ for mask, flag in INTEGRATED_FLAGS.items():
+ if value & mask:
+ flags.append(flag)
+
+ return ' | '.join(flags)
+
+
+@printer_typedef('openage::path::integrated_flags_t')
+class PathIntegratedFlagsTypePrinter:
+ """
+ Pretty printer for openage::path::integrated_flags_t.
+
+ TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros.
+ """
+
+ def __init__(self, val: gdb.Value):
+ self.__val = val
+
+ def to_string(self):
+ """
+ Get the integrate type as a string.
+ """
+ integrate = int(self.__val)
+ return get_integrated_flags_list(integrate)
+
+ def children(self):
+ """
+ Get the displayed children of the integrate type.
+ """
+ integrate = int(self.__val)
+ for mask, flag in INTEGRATED_FLAGS.items():
+ yield (flag, bool(integrate & mask))
+
+
+@printer_typedef('openage::path::integrated_t')
+class PathIntegratedTypePrinter:
+ """
+ Pretty printer for openage::path::integrated_t.
+
+ TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros.
+ """
+
+ def __init__(self, val: gdb.Value):
+ self.__val = val
+
+ def to_string(self):
+ """
+ Get the integrate type as a string.
+ """
+ output_str = f'cost = {self.__val["cost"]}'
+ flags = get_integrated_flags_list(int(self.__val['flags']))
+ if len(flags) > 0:
+ output_str += f' ({flags})'
+ return output_str
+
+ def children(self):
+ """
+ Get the displayed children of the integrate type.
+ """
+ yield ('cost', self.__val['cost'])
+ yield ('flags', self.__val['flags'])
+
+
# TODO: curve types
# TODO: pathfinding types
# TODO: input event codes
diff --git a/libopenage/coord/tile.h b/libopenage/coord/tile.h
index 5cb3f2475d..e8e61a09c2 100644
--- a/libopenage/coord/tile.h
+++ b/libopenage/coord/tile.h
@@ -1,4 +1,4 @@
-// Copyright 2016-2023 the openage authors. See copying.md for legal info.
+// Copyright 2016-2024 the openage authors. See copying.md for legal info.
#pragma once
@@ -9,9 +9,6 @@
#include "declarations.h"
namespace openage {
-
-class Terrain;
-
namespace coord {
/*
diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt
index ad828995c9..7f85264d01 100644
--- a/libopenage/pathfinding/CMakeLists.txt
+++ b/libopenage/pathfinding/CMakeLists.txt
@@ -1,6 +1,17 @@
add_sources(libopenage
- a_star.cpp
- heuristics.cpp
+ cost_field.cpp
+ definitions.cpp
+ flow_field.cpp
+ grid.cpp
+ integration_field.cpp
+ integrator.cpp
path.cpp
+ pathfinder.cpp
+ portal.cpp
+ sector.cpp
tests.cpp
+ types.cpp
)
+
+add_subdirectory("demo")
+add_subdirectory("legacy")
diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp
new file mode 100644
index 0000000000..248a260d8e
--- /dev/null
+++ b/libopenage/pathfinding/cost_field.cpp
@@ -0,0 +1,53 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#include "cost_field.h"
+
+#include "error/error.h"
+#include "log/log.h"
+
+#include "coord/tile.h"
+#include "pathfinding/definitions.h"
+
+
+namespace openage::path {
+
+CostField::CostField(size_t size) :
+ size{size},
+ cells(this->size * this->size, COST_MIN) {
+ log::log(DBG << "Created cost field with size " << this->size << "x" << this->size);
+}
+
+size_t CostField::get_size() const {
+ return this->size;
+}
+
+cost_t CostField::get_cost(const coord::tile &pos) const {
+ return this->cells.at(pos.ne + pos.se * this->size);
+}
+
+cost_t CostField::get_cost(size_t idx) const {
+ return this->cells.at(idx);
+}
+
+void CostField::set_cost(const coord::tile &pos, cost_t cost) {
+ this->cells[pos.ne + pos.se * this->size] = cost;
+}
+
+void CostField::set_cost(size_t idx, cost_t cost) {
+ this->cells[idx] = cost;
+}
+
+const std::vector &CostField::get_costs() const {
+ return this->cells;
+}
+
+void CostField::set_costs(std::vector &&cells) {
+ ENSURE(cells.size() == this->cells.size(),
+ "cells vector has wrong size: " << cells.size()
+ << "; expected: "
+ << this->cells.size());
+
+ this->cells = std::move(cells);
+}
+
+} // namespace openage::path
diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h
new file mode 100644
index 0000000000..ffb27bdec8
--- /dev/null
+++ b/libopenage/pathfinding/cost_field.h
@@ -0,0 +1,96 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#pragma once
+
+#include
+#include
+
+#include "pathfinding/types.h"
+
+
+namespace openage {
+namespace coord {
+struct tile;
+} // namespace coord
+
+namespace path {
+
+/**
+ * Cost field in the flow-field pathfinding algorithm.
+ */
+class CostField {
+public:
+ /**
+ * Create a square cost field with a specified size.
+ *
+ * @param size Side length of the field.
+ */
+ CostField(size_t size);
+
+ /**
+ * Get the size of the cost field.
+ *
+ * @return Size of the cost field.
+ */
+ size_t get_size() const;
+
+ /**
+ * Get the cost at a specified position.
+ *
+ * @param pos Coordinates of the cell.
+ * @return Cost at the specified position.
+ */
+ cost_t get_cost(const coord::tile &pos) const;
+
+ /**
+ * Get the cost at a specified position.
+ *
+ * @param idx Index of the cell.
+ * @return Cost at the specified position.
+ */
+ cost_t get_cost(size_t idx) const;
+
+ /**
+ * Set the cost at a specified position.
+ *
+ * @param pos Coordinates of the cell.
+ * @param cost Cost to set.
+ */
+ void set_cost(const coord::tile &pos, cost_t cost);
+
+ /**
+ * Set the cost at a specified position.
+ *
+ * @param idx Index of the cell.
+ * @param cost Cost to set.
+ */
+ void set_cost(size_t idx, cost_t cost);
+
+ /**
+ * Get the cost field values.
+ *
+ * @return Cost field values.
+ */
+ const std::vector &get_costs() const;
+
+ /**
+ * Set the cost field values.
+ *
+ * @param cells Cost field values.
+ */
+ void set_costs(std::vector &&cells);
+
+private:
+ /**
+ * Side length of the field.
+ */
+ size_t size;
+
+ /**
+ * Cost field values.
+ */
+ std::vector cells;
+};
+
+} // namespace path
+} // namespace openage
diff --git a/libopenage/pathfinding/definitions.cpp b/libopenage/pathfinding/definitions.cpp
new file mode 100644
index 0000000000..6428dd7ded
--- /dev/null
+++ b/libopenage/pathfinding/definitions.cpp
@@ -0,0 +1,10 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#include "types.h"
+
+
+namespace openage::path {
+
+// this file is intentionally empty
+
+} // namespace openage::path
diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h
new file mode 100644
index 0000000000..d61c5fe43c
--- /dev/null
+++ b/libopenage/pathfinding/definitions.h
@@ -0,0 +1,86 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#pragma once
+
+#include
+
+#include "pathfinding/types.h"
+
+
+namespace openage::path {
+
+/**
+ * Init value for a cells in the cost grid.
+ *
+ * Should not be used for actual costs.
+ */
+constexpr cost_t COST_INIT = 0;
+
+/**
+ * Minimum possible cost for a passable cell in the cost grid.
+ */
+constexpr cost_t COST_MIN = 1;
+
+/**
+ * Maximum possible cost for a passable cell in the cost grid.
+ */
+constexpr cost_t COST_MAX = 254;
+
+/**
+ * Cost value for an impassable cell in the cost grid.
+ */
+constexpr cost_t COST_IMPASSABLE = 255;
+
+
+/**
+ * Start value for goal cells.
+ */
+const integrated_cost_t INTEGRATED_COST_START = 0;
+
+/**
+ * Unreachable value for a cells in the integration grid.
+ */
+constexpr integrated_cost_t INTEGRATED_COST_UNREACHABLE = std::numeric_limits::max();
+
+/**
+ * Line of sight flag in an integrated_flags_t value.
+ */
+constexpr integrated_flags_t INTEGRATE_LOS_MASK = 0x01;
+
+/**
+ * Wavefront blocked flag in an integrated_flags_t value.
+ */
+constexpr integrated_flags_t INTEGRATE_WAVEFRONT_BLOCKED_MASK = 0x02;
+
+/**
+ * Initial value for a cell in the integration grid.
+ */
+constexpr integrated_t INTEGRATE_INIT = {INTEGRATED_COST_UNREACHABLE, 0};
+
+
+/**
+ * Initial value for a flow field cell.
+ */
+constexpr flow_t FLOW_INIT = 0;
+
+/**
+ * Mask for the flow direction bits in a flow_t value.
+ */
+constexpr flow_t FLOW_DIR_MASK = 0x0F;
+
+/**
+ * Pathable flag in a flow_t value.
+ */
+constexpr flow_t FLOW_PATHABLE_MASK = 0x10;
+
+/**
+ * Line of sight flag in a flow_t value.
+ */
+constexpr flow_t FLOW_LOS_MASK = 0x20;
+
+/**
+ * Wavefront blocked flag in a flow_t value.
+ */
+constexpr flow_t FLOW_WAVEFRONT_BLOCKED_MASK = 0x40;
+
+} // namespace openage::path
diff --git a/libopenage/pathfinding/demo/CMakeLists.txt b/libopenage/pathfinding/demo/CMakeLists.txt
new file mode 100644
index 0000000000..938d6ee3f0
--- /dev/null
+++ b/libopenage/pathfinding/demo/CMakeLists.txt
@@ -0,0 +1,9 @@
+add_sources(libopenage
+ demo_0.cpp
+ demo_1.cpp
+ tests.cpp
+)
+
+pxdgen(
+ tests.h
+)
diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp
new file mode 100644
index 0000000000..da7ae17492
--- /dev/null
+++ b/libopenage/pathfinding/demo/demo_0.cpp
@@ -0,0 +1,892 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#include "demo_0.h"
+
+#include
+#include
+
+#include "coord/tile.h"
+#include "pathfinding/cost_field.h"
+#include "pathfinding/flow_field.h"
+#include "pathfinding/integration_field.h"
+#include "renderer/camera/camera.h"
+#include "renderer/gui/integration/public/gui_application_with_logger.h"
+#include "renderer/opengl/window.h"
+#include "renderer/render_pass.h"
+#include "renderer/renderable.h"
+#include "renderer/renderer.h"
+#include "renderer/resources/mesh_data.h"
+#include "renderer/resources/shader_source.h"
+#include "renderer/resources/texture_info.h"
+#include "renderer/shader_program.h"
+#include "util/math_constants.h"
+#include "util/vector.h"
+
+
+namespace openage::path::tests {
+
+void path_demo_0(const util::Path &path) {
+ // Side length of the field
+ // Creates a 10x10 grid
+ auto field_length = 10;
+
+ // Cost field with some obstacles
+ auto cost_field = std::make_shared(field_length);
+ cost_field->set_cost(coord::tile{0, 0}, COST_IMPASSABLE);
+ cost_field->set_cost(coord::tile{1, 0}, 254);
+ cost_field->set_cost(coord::tile{4, 3}, 128);
+ cost_field->set_cost(coord::tile{5, 3}, 128);
+ cost_field->set_cost(coord::tile{6, 3}, 128);
+ cost_field->set_cost(coord::tile{4, 4}, 128);
+ cost_field->set_cost(coord::tile{5, 4}, 128);
+ cost_field->set_cost(coord::tile{6, 4}, 128);
+ cost_field->set_cost(coord::tile{1, 7}, COST_IMPASSABLE);
+ log::log(INFO << "Created cost field");
+
+ // Create an integration field from the cost field
+ auto integration_field = std::make_shared(field_length);
+ log::log(INFO << "Created integration field");
+
+ // Set cell (7, 7) to be the initial target cell
+ auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile{7, 7});
+ integration_field->integrate_cost(cost_field, std::move(wavefront_blocked));
+ log::log(INFO << "Calculated integration field for target cell (7, 7)");
+
+ // Create a flow field from the integration field
+ auto flow_field = std::make_shared(field_length);
+ log::log(INFO << "Created flow field");
+
+ flow_field->build(integration_field);
+ log::log(INFO << "Built flow field from integration field");
+
+ // Render the grid and the pathfinding results
+ auto qtapp = std::make_shared();
+
+ // Create a window for rendering
+ renderer::window_settings settings;
+ settings.width = 1440;
+ settings.height = 720;
+ auto window = std::make_shared("openage pathfinding test", settings);
+ auto render_manager = std::make_shared(qtapp, window, path);
+ log::log(INFO << "Created render manager for pathfinding demo");
+
+ // Show the cost field on startup
+ render_manager->show_cost_field(cost_field);
+ auto current_field = RenderManager0::field_t::COST;
+ log::log(INFO << "Showing cost field");
+
+ // Make steering vector visibility toggleable
+ auto vectors_visible = false;
+
+ // Enable mouse button callbacks
+ window->add_mouse_button_callback([&](const QMouseEvent &ev) {
+ if (ev.type() == QEvent::MouseButtonRelease) {
+ if (ev.button() == Qt::RightButton) { // Set target cell
+ auto tile_pos = render_manager->select_tile(ev.position().x(), ev.position().y());
+ auto grid_x = tile_pos.first;
+ auto grid_y = tile_pos.second;
+
+ if (grid_x >= 0 and grid_x < field_length and grid_y >= 0 and grid_y < field_length) {
+ log::log(INFO << "Selected new target cell (" << grid_x << ", " << grid_y << ")");
+
+ // Recalculate the integration field and the flow field
+ integration_field->reset();
+ auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile{grid_x, grid_y});
+ integration_field->integrate_cost(cost_field, std::move(wavefront_blocked));
+ log::log(INFO << "Calculated integration field for target cell ("
+ << grid_x << ", " << grid_y << ")");
+
+ flow_field->reset();
+ flow_field->build(integration_field);
+ log::log(INFO << "Built flow field from integration field");
+
+ // Show the new field values and vectors
+ switch (current_field) {
+ case RenderManager0::field_t::COST:
+ render_manager->show_cost_field(cost_field);
+ break;
+ case RenderManager0::field_t::INTEGRATION:
+ render_manager->show_integration_field(integration_field);
+ break;
+ case RenderManager0::field_t::FLOW:
+ render_manager->show_flow_field(flow_field);
+ break;
+ }
+
+ if (vectors_visible) {
+ render_manager->show_vectors(flow_field);
+ }
+ }
+ }
+ }
+ });
+
+ // Enable key callbacks
+ window->add_key_callback([&](const QKeyEvent &ev) {
+ if (ev.type() == QEvent::KeyRelease) {
+ if (ev.key() == Qt::Key_F1) { // Show cost field
+ render_manager->show_cost_field(cost_field);
+ current_field = RenderManager0::field_t::COST;
+ log::log(INFO << "Showing cost field");
+ }
+ else if (ev.key() == Qt::Key_F2) { // Show integration field
+ render_manager->show_integration_field(integration_field);
+ current_field = RenderManager0::field_t::INTEGRATION;
+ log::log(INFO << "Showing integration field");
+ }
+ else if (ev.key() == Qt::Key_F3) { // Show flow field
+ render_manager->show_flow_field(flow_field);
+ current_field = RenderManager0::field_t::FLOW;
+ log::log(INFO << "Showing flow field");
+ }
+ else if (ev.key() == Qt::Key_F4) { // Show steering vectors
+ if (vectors_visible) {
+ render_manager->hide_vectors();
+ vectors_visible = false;
+ log::log(INFO << "Hiding steering vectors");
+ }
+ else {
+ render_manager->show_vectors(flow_field);
+ vectors_visible = true;
+ log::log(INFO << "Showing steering vectors");
+ }
+ }
+ }
+ });
+
+ log::log(INFO << "Instructions:");
+ log::log(INFO << " 1. Press F1/F2/F3 to show cost/integration/flow field");
+ log::log(INFO << " 2. Press F4 to toggle steering vectors");
+
+ // Run the render loop
+ render_manager->run();
+}
+
+
+RenderManager0::RenderManager0(const std::shared_ptr &app,
+ const std::shared_ptr &window,
+ const util::Path &path) :
+ path{path},
+ app{app},
+ window{window},
+ renderer{window->make_renderer()},
+ camera{std::make_shared(renderer, window->get_size())} {
+ // Position camera to look at center of the grid
+ camera->look_at_coord({5, 5, 0});
+ window->add_resize_callback([&](size_t w, size_t h, double /*scale*/) {
+ camera->resize(w, h);
+ });
+
+ this->init_shaders();
+ this->init_passes();
+}
+
+
+void RenderManager0::run() {
+ while (not this->window->should_close()) {
+ this->app->process_events();
+
+ this->renderer->render(this->background_pass);
+ this->renderer->render(this->field_pass);
+ this->renderer->render(this->vector_pass);
+ this->renderer->render(this->grid_pass);
+ this->renderer->render(this->display_pass);
+
+ this->window->update();
+
+ this->renderer->check_error();
+ }
+ this->window->close();
+}
+
+void RenderManager0::show_cost_field(const std::shared_ptr &field) {
+ Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
+ auto unifs = this->cost_shader->new_uniform_input(
+ "model",
+ model,
+ "view",
+ this->camera->get_view_matrix(),
+ "proj",
+ this->camera->get_projection_matrix());
+ auto mesh = RenderManager0::get_cost_field_mesh(field);
+ auto geometry = this->renderer->add_mesh_geometry(mesh);
+ renderer::Renderable renderable{
+ unifs,
+ geometry,
+ true,
+ true,
+ };
+ this->field_pass->set_renderables({renderable});
+}
+
+void RenderManager0::show_integration_field(const std::shared_ptr &field) {
+ Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
+ auto unifs = this->integration_shader->new_uniform_input(
+ "model",
+ model,
+ "view",
+ this->camera->get_view_matrix(),
+ "proj",
+ this->camera->get_projection_matrix());
+ auto mesh = get_integration_field_mesh(field, 4);
+ auto geometry = this->renderer->add_mesh_geometry(mesh);
+ renderer::Renderable renderable{
+ unifs,
+ geometry,
+ true,
+ true,
+ };
+ this->field_pass->set_renderables({renderable});
+}
+
+void RenderManager0::show_flow_field(const std::shared_ptr &field) {
+ Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
+ auto unifs = this->flow_shader->new_uniform_input(
+ "model",
+ model,
+ "view",
+ this->camera->get_view_matrix(),
+ "proj",
+ this->camera->get_projection_matrix());
+ auto mesh = get_flow_field_mesh(field, 4);
+ auto geometry = this->renderer->add_mesh_geometry(mesh);
+ renderer::Renderable renderable{
+ unifs,
+ geometry,
+ true,
+ true,
+ };
+ this->field_pass->set_renderables({renderable});
+}
+
+void RenderManager0::show_vectors(const std::shared_ptr &field) {
+ static const std::unordered_map offset_dir{
+ {flow_dir_t::NORTH, {-0.25f, 0.0f, 0.0f}},
+ {flow_dir_t::NORTH_EAST, {-0.25f, 0.0f, -0.25f}},
+ {flow_dir_t::EAST, {0.0f, 0.0f, -0.25f}},
+ {flow_dir_t::SOUTH_EAST, {0.25f, 0.0f, -0.25f}},
+ {flow_dir_t::SOUTH, {0.25f, 0.0f, 0.0f}},
+ {flow_dir_t::SOUTH_WEST, {0.25f, 0.0f, 0.25f}},
+ {flow_dir_t::WEST, {0.0f, 0.0f, 0.25f}},
+ {flow_dir_t::NORTH_WEST, {-0.25f, 0.0f, 0.25f}},
+ };
+
+ this->vector_pass->clear_renderables();
+ for (size_t y = 0; y < field->get_size(); ++y) {
+ for (size_t x = 0; x < field->get_size(); ++x) {
+ auto cell = field->get_cell(coord::tile(x, y));
+ if (cell & FLOW_PATHABLE_MASK and not(cell & FLOW_LOS_MASK)) {
+ Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity();
+ arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5));
+ auto dir = static_cast(cell & FLOW_DIR_MASK);
+ arrow_model.translate(offset_dir.at(dir));
+
+ auto rotation_rad = (cell & FLOW_DIR_MASK) * -45 * math::DEGSPERRAD;
+ arrow_model.rotate(Eigen::AngleAxisf(rotation_rad, Eigen::Vector3f::UnitY()));
+ auto arrow_unifs = this->vector_shader->new_uniform_input(
+ "model",
+ arrow_model.matrix(),
+ "view",
+ camera->get_view_matrix(),
+ "proj",
+ camera->get_projection_matrix(),
+ "color",
+ Eigen::Vector4f{0.0f, 0.0f, 0.0f, 1.0f});
+ auto arrow_mesh = get_arrow_mesh();
+ auto arrow_geometry = renderer->add_mesh_geometry(arrow_mesh);
+ renderer::Renderable arrow_renderable{
+ arrow_unifs,
+ arrow_geometry,
+ true,
+ true,
+ };
+ this->vector_pass->add_renderables(std::move(arrow_renderable));
+ }
+ }
+ }
+}
+
+void RenderManager0::hide_vectors() {
+ this->vector_pass->clear_renderables();
+}
+
+std::pair RenderManager0::select_tile(double x, double y) {
+ auto grid_plane_normal = Eigen::Vector3f{0, 1, 0};
+ auto grid_plane_point = Eigen::Vector3f{0, 0, 0};
+ auto camera_direction = renderer::camera::cam_direction;
+ auto camera_position = camera->get_input_pos(
+ coord::input(x, y));
+
+ Eigen::Vector3f intersect = camera_position + camera_direction * (grid_plane_point - camera_position).dot(grid_plane_normal) / camera_direction.dot(grid_plane_normal);
+ auto grid_x = static_cast(-1 * intersect[2]);
+ auto grid_y = static_cast(intersect[0]);
+
+ return {grid_x, grid_y};
+}
+
+void RenderManager0::init_shaders() {
+ // Shader sources
+ auto shaderdir = this->path / "assets" / "test" / "shaders" / "pathfinding";
+
+ // Shader for rendering the cost field
+ auto cf_vshader_file = shaderdir / "demo_0_cost_field.vert.glsl";
+ auto cf_vshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::vertex,
+ cf_vshader_file);
+
+ auto cf_fshader_file = shaderdir / "demo_0_cost_field.frag.glsl";
+ auto cf_fshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::fragment,
+ cf_fshader_file);
+
+ // Shader for rendering the integration field
+ auto if_vshader_file = shaderdir / "demo_0_integration_field.vert.glsl";
+ auto if_vshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::vertex,
+ if_vshader_file);
+
+ auto if_fshader_file = shaderdir / "demo_0_integration_field.frag.glsl";
+ auto if_fshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::fragment,
+ if_fshader_file);
+
+ // Shader for rendering the flow field
+ auto ff_vshader_file = shaderdir / "demo_0_flow_field.vert.glsl";
+ auto ff_vshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::vertex,
+ ff_vshader_file);
+
+ auto ff_fshader_file = shaderdir / "demo_0_flow_field.frag.glsl";
+ auto ff_fshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::fragment,
+ ff_fshader_file);
+
+ // Shader for rendering steering vectors
+ auto vec_vshader_file = shaderdir / "demo_0_vector.vert.glsl";
+ auto vec_vshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::vertex,
+ vec_vshader_file);
+
+ auto vec_fshader_file = shaderdir / "demo_0_vector.frag.glsl";
+ auto vec_fshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::fragment,
+ vec_fshader_file);
+
+ // Shader for rendering the grid
+ auto grid_vshader_file = shaderdir / "demo_0_grid.vert.glsl";
+ auto grid_vshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::vertex,
+ grid_vshader_file);
+
+ auto grid_fshader_file = shaderdir / "demo_0_grid.frag.glsl";
+ auto grid_fshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::fragment,
+ grid_fshader_file);
+
+ // Shader for 2D monocolored objects
+ auto obj_vshader_file = shaderdir / "demo_0_obj.vert.glsl";
+ auto obj_vshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::vertex,
+ obj_vshader_file);
+
+ auto obj_fshader_file = shaderdir / "demo_0_obj.frag.glsl";
+ auto obj_fshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::fragment,
+ obj_fshader_file);
+
+ // Shader for rendering to the display target
+ auto display_vshader_file = shaderdir / "demo_0_display.vert.glsl";
+ auto display_vshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::vertex,
+ display_vshader_file);
+
+ auto display_fshader_file = shaderdir / "demo_0_display.frag.glsl";
+ auto display_fshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::fragment,
+ display_fshader_file);
+
+ // Create the shaders
+ this->cost_shader = renderer->add_shader({cf_vshader_src, cf_fshader_src});
+ this->integration_shader = renderer->add_shader({if_vshader_src, if_fshader_src});
+ this->flow_shader = renderer->add_shader({ff_vshader_src, ff_fshader_src});
+ this->vector_shader = renderer->add_shader({vec_vshader_src, vec_fshader_src});
+ this->grid_shader = renderer->add_shader({grid_vshader_src, grid_fshader_src});
+ this->obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src});
+ this->display_shader = renderer->add_shader({display_vshader_src, display_fshader_src});
+}
+
+void RenderManager0::init_passes() {
+ auto size = this->window->get_size();
+
+ // Make a framebuffer for the background render pass to draw into
+ auto background_texture = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::rgba8));
+ auto depth_texture = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::depth24));
+ auto fbo = renderer->create_texture_target({background_texture, depth_texture});
+
+ // Background object for contrast between field and display
+ auto background_unifs = obj_shader->new_uniform_input(
+ "color",
+ Eigen::Vector4f{64.0 / 256, 128.0 / 256, 196.0 / 256, 1.0});
+ auto background = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad());
+ renderer::Renderable background_obj{
+ background_unifs,
+ background,
+ true,
+ true,
+ };
+ this->background_pass = renderer->add_render_pass({background_obj}, fbo);
+
+ // Make a framebuffer for the field render pass to draw into
+ auto field_texture = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::rgba8));
+ auto depth_texture_2 = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::depth24));
+ auto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2});
+ this->field_pass = renderer->add_render_pass({}, field_fbo);
+
+ // Make a framebuffer for the vector render passes to draw into
+ auto vector_texture = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::rgba8));
+ auto depth_texture_3 = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::depth24));
+ auto vector_fbo = renderer->create_texture_target({vector_texture, depth_texture_3});
+ this->vector_pass = renderer->add_render_pass({}, vector_fbo);
+
+ // Make a framebuffer for the grid render passes to draw into
+ auto grid_texture = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::rgba8));
+ auto depth_texture_4 = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::depth24));
+ auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_4});
+
+ // Create object for the grid
+ Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
+ auto grid_unifs = grid_shader->new_uniform_input(
+ "model",
+ model,
+ "view",
+ camera->get_view_matrix(),
+ "proj",
+ camera->get_projection_matrix());
+ auto grid_mesh = this->get_grid_mesh(10);
+ auto grid_geometry = renderer->add_mesh_geometry(grid_mesh);
+ renderer::Renderable grid_obj{
+ grid_unifs,
+ grid_geometry,
+ true,
+ true,
+ };
+ this->grid_pass = renderer->add_render_pass({grid_obj}, grid_fbo);
+
+ // Make two objects that draw the results of the previous passes onto the screen
+ // in the display render pass
+ auto bg_texture_unif = display_shader->new_uniform_input("color_texture", background_texture);
+ auto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad());
+ renderer::Renderable bg_pass_obj{
+ bg_texture_unif,
+ quad,
+ true,
+ true,
+ };
+ auto field_texture_unif = display_shader->new_uniform_input("color_texture", field_texture);
+ renderer::Renderable field_pass_obj{
+ field_texture_unif,
+ quad,
+ true,
+ true,
+ };
+ auto vector_texture_unif = display_shader->new_uniform_input("color_texture", vector_texture);
+ renderer::Renderable vector_pass_obj{
+ vector_texture_unif,
+ quad,
+ true,
+ true,
+ };
+ auto grid_texture_unif = display_shader->new_uniform_input("color_texture", grid_texture);
+ renderer::Renderable grid_pass_obj{
+ grid_texture_unif,
+ quad,
+ true,
+ true,
+ };
+ this->display_pass = renderer->add_render_pass(
+ {bg_pass_obj, field_pass_obj, vector_pass_obj, grid_pass_obj},
+ renderer->get_display_target());
+}
+
+renderer::resources::MeshData RenderManager0::get_cost_field_mesh(const std::shared_ptr &field,
+ size_t resolution) {
+ // increase by 1 in every dimension because to get the vertex length
+ // of each dimension
+ util::Vector2s size{
+ field->get_size() * resolution + 1,
+ field->get_size() * resolution + 1,
+ };
+ auto vert_distance = 1.0f / resolution;
+
+ // add vertices for the cells of the grid
+ std::vector verts{};
+ auto vert_count = size[0] * size[1];
+ verts.reserve(vert_count * 4);
+ for (int i = 0; i < static_cast(size[0]); ++i) {
+ for (int j = 0; j < static_cast(size[1]); ++j) {
+ // for each vertex, compare the surrounding tiles
+ std::vector surround{};
+ if (j - 1 >= 0 and i - 1 >= 0) {
+ auto cost = field->get_cost(coord::tile((i - 1) / resolution, (j - 1) / resolution));
+ surround.push_back(cost);
+ }
+ if (j < static_cast(field->get_size()) and i - 1 >= 0) {
+ auto cost = field->get_cost(coord::tile((i - 1) / resolution, j / resolution));
+ surround.push_back(cost);
+ }
+ if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) {
+ auto cost = field->get_cost(coord::tile(i / resolution, j / resolution));
+ surround.push_back(cost);
+ }
+ if (j - 1 >= 0 and i < static_cast(field->get_size())) {
+ auto cost = field->get_cost(coord::tile(i / resolution, (j - 1) / resolution));
+ surround.push_back(cost);
+ }
+ // use the cost of the most expensive surrounding tile
+ auto max_cost = *std::max_element(surround.begin(), surround.end());
+
+ coord::scene3 v{
+ static_cast(i * vert_distance),
+ static_cast(j * vert_distance),
+ 0,
+ };
+ auto world_v = v.to_world_space();
+ verts.push_back(world_v[0]);
+ verts.push_back(world_v[1]);
+ verts.push_back(world_v[2]);
+ verts.push_back(max_cost);
+ }
+ }
+
+ // split the grid into triangles using an index array
+ std::vector idxs;
+ idxs.reserve((size[0] - 1) * (size[1] - 1) * 6);
+ // iterate over all tiles in the grid by columns, i.e. starting
+ // from the left corner to the bottom corner if you imagine it from
+ // the camera's point of view
+ for (size_t i = 0; i < size[0] - 1; ++i) {
+ for (size_t j = 0; j < size[1] - 1; ++j) {
+ // since we are working on tiles, we split each tile into two triangles
+ // with counter-clockwise vertex order
+ idxs.push_back(j + i * size[1]); // bottom left
+ idxs.push_back(j + 1 + i * size[1]); // bottom right
+ idxs.push_back(j + size[1] + i * size[1]); // top left
+ idxs.push_back(j + 1 + i * size[1]); // bottom right
+ idxs.push_back(j + size[1] + 1 + i * size[1]); // top right
+ idxs.push_back(j + size[1] + i * size[1]); // top left
+ }
+ }
+
+ renderer::resources::VertexInputInfo info{
+ {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32},
+ renderer::resources::vertex_layout_t::AOS,
+ renderer::resources::vertex_primitive_t::TRIANGLES,
+ renderer::resources::index_t::U16};
+
+ auto const vert_data_size = verts.size() * sizeof(float);
+ std::vector vert_data(vert_data_size);
+ std::memcpy(vert_data.data(), verts.data(), vert_data_size);
+
+ auto const idx_data_size = idxs.size() * sizeof(uint16_t);
+ std::vector idx_data(idx_data_size);
+ std::memcpy(idx_data.data(), idxs.data(), idx_data_size);
+
+ return {std::move(vert_data), std::move(idx_data), info};
+}
+
+renderer::resources::MeshData RenderManager0::get_integration_field_mesh(const std::shared_ptr &field,
+ size_t resolution) {
+ // increase by 1 in every dimension because to get the vertex length
+ // of each dimension
+ util::Vector2s size{
+ field->get_size() * resolution + 1,
+ field->get_size() * resolution + 1,
+ };
+ auto vert_distance = 1.0f / resolution;
+
+ // add vertices for the cells of the grid
+ std::vector verts{};
+ auto vert_count = size[0] * size[1];
+ verts.reserve(vert_count * 4);
+ for (int i = 0; i < static_cast(size[0]); ++i) {
+ for (int j = 0; j < static_cast(size[1]); ++j) {
+ // for each vertex, compare the surrounding tiles
+ std::vector surround{};
+ if (j - 1 >= 0 and i - 1 >= 0) {
+ auto cost = field->get_cell(coord::tile((i - 1) / resolution, (j - 1) / resolution)).cost;
+ surround.push_back(cost);
+ }
+ if (j < static_cast(field->get_size()) and i - 1 >= 0) {
+ auto cost = field->get_cell(coord::tile((i - 1) / resolution, j / resolution)).cost;
+ surround.push_back(cost);
+ }
+ if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) {
+ auto cost = field->get_cell(coord::tile(i / resolution, j / resolution)).cost;
+ surround.push_back(cost);
+ }
+ if (j - 1 >= 0 and i < static_cast(field->get_size())) {
+ auto cost = field->get_cell(coord::tile(i / resolution, (j - 1) / resolution)).cost;
+ surround.push_back(cost);
+ }
+ // use the cost of the most expensive surrounding tile
+ auto max_cost = *std::max_element(surround.begin(), surround.end());
+
+ coord::scene3 v{
+ static_cast(i * vert_distance),
+ static_cast(j * vert_distance),
+ 0,
+ };
+ auto world_v = v.to_world_space();
+ verts.push_back(world_v[0]);
+ verts.push_back(world_v[1]);
+ verts.push_back(world_v[2]);
+ verts.push_back(max_cost);
+ }
+ }
+
+ // split the grid into triangles using an index array
+ std::vector idxs;
+ idxs.reserve((size[0] - 1) * (size[1] - 1) * 6);
+ // iterate over all tiles in the grid by columns, i.e. starting
+ // from the left corner to the bottom corner if you imagine it from
+ // the camera's point of view
+ for (size_t i = 0; i < size[0] - 1; ++i) {
+ for (size_t j = 0; j < size[1] - 1; ++j) {
+ // since we are working on tiles, we split each tile into two triangles
+ // with counter-clockwise vertex order
+ idxs.push_back(j + i * size[1]); // bottom left
+ idxs.push_back(j + 1 + i * size[1]); // bottom right
+ idxs.push_back(j + size[1] + i * size[1]); // top left
+ idxs.push_back(j + 1 + i * size[1]); // bottom right
+ idxs.push_back(j + size[1] + 1 + i * size[1]); // top right
+ idxs.push_back(j + size[1] + i * size[1]); // top left
+ }
+ }
+
+ renderer::resources::VertexInputInfo info{
+ {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32},
+ renderer::resources::vertex_layout_t::AOS,
+ renderer::resources::vertex_primitive_t::TRIANGLES,
+ renderer::resources::index_t::U16};
+
+ auto const vert_data_size = verts.size() * sizeof(float);
+ std::vector vert_data(vert_data_size);
+ std::memcpy(vert_data.data(), verts.data(), vert_data_size);
+
+ auto const idx_data_size = idxs.size() * sizeof(uint16_t);
+ std::vector idx_data(idx_data_size);
+ std::memcpy(idx_data.data(), idxs.data(), idx_data_size);
+
+ return {std::move(vert_data), std::move(idx_data), info};
+}
+
+renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::shared_ptr &field,
+ size_t resolution) {
+ // increase by 1 in every dimension because to get the vertex length
+ // of each dimension
+ util::Vector2s size{
+ field->get_size() * resolution + 1,
+ field->get_size() * resolution + 1,
+ };
+ auto vert_distance = 1.0f / resolution;
+
+ // add vertices for the cells of the grid
+ std::vector verts{};
+ auto vert_count = size[0] * size[1];
+ verts.reserve(vert_count * 4);
+ for (int i = 0; i < static_cast(size[0]); ++i) {
+ for (int j = 0; j < static_cast(size[1]); ++j) {
+ // for each vertex, compare the surrounding tiles
+ std::vector surround{};
+ if (j - 1 >= 0 and i - 1 >= 0) {
+ auto cost = field->get_cell(coord::tile((i - 1) / resolution, (j - 1) / resolution));
+ surround.push_back(cost);
+ }
+ if (j < static_cast(field->get_size()) and i - 1 >= 0) {
+ auto cost = field->get_cell(coord::tile((i - 1) / resolution, j / resolution));
+ surround.push_back(cost);
+ }
+ if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) {
+ auto cost = field->get_cell(coord::tile(i / resolution, j / resolution));
+ surround.push_back(cost);
+ }
+ if (j - 1 >= 0 and i < static_cast(field->get_size())) {
+ auto cost = field->get_cell(coord::tile(i / resolution, (j - 1) / resolution));
+ surround.push_back(cost);
+ }
+ // use the cost of the most expensive surrounding tile
+ auto max_cost = *std::max_element(surround.begin(), surround.end());
+
+ coord::scene3 v{
+ static_cast(i * vert_distance),
+ static_cast(j * vert_distance),
+ 0,
+ };
+ auto world_v = v.to_world_space();
+ verts.push_back(world_v[0]);
+ verts.push_back(world_v[1]);
+ verts.push_back(world_v[2]);
+ verts.push_back(max_cost);
+ }
+ }
+
+ // split the grid into triangles using an index array
+ std::vector idxs;
+ idxs.reserve((size[0] - 1) * (size[1] - 1) * 6);
+ // iterate over all tiles in the grid by columns, i.e. starting
+ // from the left corner to the bottom corner if you imagine it from
+ // the camera's point of view
+ for (size_t i = 0; i < size[0] - 1; ++i) {
+ for (size_t j = 0; j < size[1] - 1; ++j) {
+ // since we are working on tiles, we split each tile into two triangles
+ // with counter-clockwise vertex order
+ idxs.push_back(j + i * size[1]); // bottom left
+ idxs.push_back(j + 1 + i * size[1]); // bottom right
+ idxs.push_back(j + size[1] + i * size[1]); // top left
+ idxs.push_back(j + 1 + i * size[1]); // bottom right
+ idxs.push_back(j + size[1] + 1 + i * size[1]); // top right
+ idxs.push_back(j + size[1] + i * size[1]); // top left
+ }
+ }
+
+ renderer::resources::VertexInputInfo info{
+ {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32},
+ renderer::resources::vertex_layout_t::AOS,
+ renderer::resources::vertex_primitive_t::TRIANGLES,
+ renderer::resources::index_t::U16};
+
+ auto const vert_data_size = verts.size() * sizeof(float);
+ std::vector vert_data(vert_data_size);
+ std::memcpy(vert_data.data(), verts.data(), vert_data_size);
+
+ auto const idx_data_size = idxs.size() * sizeof(uint16_t);
+ std::vector idx_data(idx_data_size);
+ std::memcpy(idx_data.data(), idxs.data(), idx_data_size);
+
+ return {std::move(vert_data), std::move(idx_data), info};
+}
+
+renderer::resources::MeshData RenderManager0::get_arrow_mesh() {
+ // vertices for the arrow
+ // x, y, z
+ std::vector verts{
+ // clang-format off
+ 0.2f, 0.01f, -0.05f,
+ 0.2f, 0.01f, 0.05f,
+ -0.2f, 0.01f, -0.05f,
+ -0.2f, 0.01f, 0.05f,
+ // clang-format on
+ };
+
+ renderer::resources::VertexInputInfo info{
+ {renderer::resources::vertex_input_t::V3F32},
+ renderer::resources::vertex_layout_t::AOS,
+ renderer::resources::vertex_primitive_t::TRIANGLE_STRIP};
+
+ auto const vert_data_size = verts.size() * sizeof(float);
+ std::vector vert_data(vert_data_size);
+ std::memcpy(vert_data.data(), verts.data(), vert_data_size);
+
+ return {std::move(vert_data), info};
+}
+
+renderer::resources::MeshData RenderManager0::get_grid_mesh(size_t side_length) {
+ // increase by 1 in every dimension because to get the vertex length
+ // of each dimension
+ util::Vector2s size{side_length + 1, side_length + 1};
+
+ // add vertices for the cells of the grid
+ std::vector verts{};
+ auto vert_count = size[0] * size[1];
+ verts.reserve(vert_count * 3);
+ for (int i = 0; i < (int)size[0]; ++i) {
+ for (int j = 0; j < (int)size[1]; ++j) {
+ coord::scene3 v{
+ static_cast(i),
+ static_cast(j),
+ 0,
+ };
+ auto world_v = v.to_world_space();
+ verts.push_back(world_v[0]);
+ verts.push_back(world_v[1]);
+ verts.push_back(world_v[2]);
+ }
+ }
+
+ // split the grid into lines using an index array
+ std::vector idxs;
+ idxs.reserve((size[0] - 1) * (size[1] - 1) * 8);
+ // iterate over all tiles in the grid by columns, i.e. starting
+ // from the left corner to the bottom corner if you imagine it from
+ // the camera's point of view
+ for (size_t i = 0; i < size[0] - 1; ++i) {
+ for (size_t j = 0; j < size[1] - 1; ++j) {
+ // since we are working on tiles, we just draw a square using the 4 vertices
+ idxs.push_back(j + i * size[1]); // bottom left
+ idxs.push_back(j + 1 + i * size[1]); // bottom right
+ idxs.push_back(j + 1 + i * size[1]); // bottom right
+ idxs.push_back(j + size[1] + 1 + i * size[1]); // top right
+ idxs.push_back(j + size[1] + 1 + i * size[1]); // top right
+ idxs.push_back(j + size[1] + i * size[1]); // top left
+ idxs.push_back(j + size[1] + i * size[1]); // top left
+ idxs.push_back(j + i * size[1]); // bottom left
+ }
+ }
+
+ renderer::resources::VertexInputInfo info{
+ {renderer::resources::vertex_input_t::V3F32},
+ renderer::resources::vertex_layout_t::AOS,
+ renderer::resources::vertex_primitive_t::LINES,
+ renderer::resources::index_t::U16};
+
+ auto const vert_data_size = verts.size() * sizeof(float);
+ std::vector vert_data(vert_data_size);
+ std::memcpy(vert_data.data(), verts.data(), vert_data_size);
+
+ auto const idx_data_size = idxs.size() * sizeof(uint16_t);
+ std::vector idx_data(idx_data_size);
+ std::memcpy(idx_data.data(), idxs.data(), idx_data_size);
+
+ return {std::move(vert_data), std::move(idx_data), info};
+}
+
+
+} // namespace openage::path::tests
diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h
new file mode 100644
index 0000000000..7a07ebfd7e
--- /dev/null
+++ b/libopenage/pathfinding/demo/demo_0.h
@@ -0,0 +1,278 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#pragma once
+
+#include
+
+#include "util/path.h"
+
+
+namespace openage {
+namespace renderer {
+class RenderPass;
+class Renderer;
+class ShaderProgram;
+class Window;
+
+namespace camera {
+class Camera;
+} // namespace camera
+
+namespace gui {
+class GuiApplicationWithLogger;
+} // namespace gui
+
+namespace resources {
+class MeshData;
+} // namespace resources
+} // namespace renderer
+
+namespace path {
+class CostField;
+class IntegrationField;
+class FlowField;
+
+namespace tests {
+
+/**
+ * Show the functionality of the different flowfield types:
+ * - Cost field
+ * - Integration field
+ * - Flow field
+ *
+ * Visualizes the pathfinding results using our rendering backend.
+ *
+ * @param path Path to the project rootdir.
+ */
+void path_demo_0(const util::Path &path);
+
+
+/**
+ * Manages the graphical display of the pathfinding demo.
+ */
+class RenderManager0 {
+public:
+ enum class field_t {
+ COST,
+ INTEGRATION,
+ FLOW
+ };
+
+ /**
+ * Create a new render manager.
+ *
+ * @param app GUI application.
+ * @param window Window to render to.
+ * @param path Path to the project rootdir.
+ */
+ RenderManager0(const std::shared_ptr &app,
+ const std::shared_ptr &window,
+ const util::Path &path);
+ ~RenderManager0() = default;
+
+ /**
+ * Run the render loop.
+ */
+ void run();
+
+ /**
+ * Draw a cost field to the screen.
+ *
+ * @param field Cost field.
+ */
+ void show_cost_field(const std::shared_ptr &field);
+
+ /**
+ * Draw an integration field to the screen.
+ *
+ * @param field Integration field.
+ */
+ void show_integration_field(const std::shared_ptr &field);
+
+ /**
+ * Draw a flow field to the screen.
+ *
+ * @param field Flow field.
+ */
+ void show_flow_field(const std::shared_ptr &field);
+
+ /**
+ * Draw the steering vectors of a flow field to the screen.
+ *
+ * @param field Flow field.
+ */
+ void show_vectors(const std::shared_ptr &field);
+
+ /**
+ * Hide drawn steering vectors.
+ */
+ void hide_vectors();
+
+ /**
+ * Get the cell coordinates at a given screen position.
+ *
+ * @param x X coordinate.
+ * @param y Y coordinate.
+ */
+ std::pair select_tile(double x, double y);
+
+private:
+ /**
+ * Load the shader sources for the demo and create the shader programs.
+ */
+ void init_shaders();
+
+ /**
+ * Create the following render passes for the demo:
+ * - Background pass: Mono-colored background object.
+ * - Field pass; Renders the cost, integration and flow fields.
+ * - Vector pass: Renders the steering vectors of a flow field.
+ * - Grid pass: Renders a grid on top of the fields.
+ * - Display pass: Draws the results of previous passes to the screen.
+ */
+ void init_passes();
+
+
+ /**
+ * Create a mesh for the cost field.
+ *
+ * @param field Cost field to visualize.
+ * @param resolution Determines the number of subdivisions per grid cell. The number of
+ * quads per cell is resolution^2. (default = 2)
+ *
+ * @return Mesh data for the cost field.
+ */
+ static renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr &field,
+ size_t resolution = 2);
+
+ /**
+ * Create a mesh for the integration field.
+ *
+ * @param field Integration field to visualize.
+ * @param resolution Determines the number of subdivisions per grid cell. The number of
+ * quads per cell is resolution^2. (default = 2)
+ *
+ * @return Mesh data for the integration field.
+ */
+ static renderer::resources::MeshData get_integration_field_mesh(const std::shared_ptr &field,
+ size_t resolution = 2);
+
+ /**
+ * Create a mesh for the flow field.
+ *
+ * @param field Flow field to visualize.
+ */
+ static renderer::resources::MeshData get_flow_field_mesh(const std::shared_ptr &field,
+ size_t resolution = 2);
+
+ /**
+ * Create a mesh for an arrow.
+ *
+ * @return Mesh data for an arrow.
+ */
+ static renderer::resources::MeshData get_arrow_mesh();
+
+ /**
+ * Create a mesh for the grid.
+ *
+ * @param side_length Length of the grid's side.
+ *
+ * @return Mesh data for the grid.
+ */
+ static renderer::resources::MeshData get_grid_mesh(size_t side_length);
+
+ /**
+ * Path to the project rootdir.
+ */
+ const util::Path &path;
+
+ /* Renderer objects */
+
+ /**
+ * Qt GUI application.
+ */
+ std::shared_ptr app;
+
+ /**
+ * openage window to render to.
+ */
+ std::shared_ptr window;
+
+ /**
+ * openage renderer instance.
+ */
+ std::shared_ptr renderer;
+
+ /**
+ * Camera to view the scene.
+ */
+ std::shared_ptr camera;
+
+ /* Shader programs */
+
+ /**
+ * Shader program for rendering a cost field.
+ */
+ std::shared_ptr cost_shader;
+
+ /**
+ * Shader program for rendering a integration field.
+ */
+ std::shared_ptr integration_shader;
+
+ /**
+ * Shader program for rendering a flow field.
+ */
+ std::shared_ptr flow_shader;
+
+ /**
+ * Shader program for rendering steering vectors.
+ */
+ std::shared_ptr vector_shader;
+
+ /**
+ * Shader program for rendering a grid.
+ */
+ std::shared_ptr grid_shader;
+
+ /**
+ * Shader program for rendering 2D mono-colored objects.
+ */
+ std::shared_ptr obj_shader;
+
+ /**
+ * Shader program for rendering the final display.
+ */
+ std::shared_ptr display_shader;
+
+ /* Render passes */
+
+ /**
+ * Background pass: Mono-colored background object.
+ */
+ std::shared_ptr background_pass;
+
+ /**
+ * Field pass: Renders the cost, integration and flow fields.
+ */
+ std::shared_ptr field_pass;
+
+ /**
+ * Vector pass: Renders the steering vectors of a flow field.
+ */
+ std::shared_ptr vector_pass;
+
+ /**
+ * Grid pass: Renders a grid on top of the fields.
+ */
+ std::shared_ptr grid_pass;
+
+ /**
+ * Display pass: Draws the results of previous passes to the screen.
+ */
+ std::shared_ptr display_pass;
+};
+
+} // namespace tests
+} // namespace path
+} // namespace openage
diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp
new file mode 100644
index 0000000000..21aaa2c9d8
--- /dev/null
+++ b/libopenage/pathfinding/demo/demo_1.cpp
@@ -0,0 +1,716 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#include "demo_1.h"
+
+#include
+
+#include "pathfinding/cost_field.h"
+#include "pathfinding/grid.h"
+#include "pathfinding/path.h"
+#include "pathfinding/pathfinder.h"
+#include "pathfinding/portal.h"
+#include "pathfinding/sector.h"
+#include "util/timer.h"
+
+#include "renderer/gui/integration/public/gui_application_with_logger.h"
+#include "renderer/opengl/window.h"
+#include "renderer/render_pass.h"
+#include "renderer/renderable.h"
+#include "renderer/resources/shader_source.h"
+#include "renderer/resources/texture_info.h"
+#include "renderer/shader_program.h"
+#include "renderer/window.h"
+
+
+namespace openage::path::tests {
+
+void path_demo_1(const util::Path &path) {
+ auto grid = std::make_shared(0, util::Vector2s{4, 3}, 10);
+
+ // Initialize the cost field for each sector.
+ for (auto §or : grid->get_sectors()) {
+ auto cost_field = sector->get_cost_field();
+ std::vector sector_cost = sectors_cost.at(sector->get_id());
+ cost_field->set_costs(std::move(sector_cost));
+ }
+
+ // Initialize portals between sectors.
+ util::Vector2s grid_size = grid->get_size();
+ portal_id_t portal_id = 0;
+ for (size_t y = 0; y < grid_size[1]; y++) {
+ for (size_t x = 0; x < grid_size[0]; x++) {
+ auto sector = grid->get_sector(x, y);
+
+ if (x < grid_size[0] - 1) {
+ auto neighbor = grid->get_sector(x + 1, y);
+ auto portals = sector->find_portals(neighbor, PortalDirection::EAST_WEST, portal_id);
+ for (auto &portal : portals) {
+ sector->add_portal(portal);
+ neighbor->add_portal(portal);
+ }
+ portal_id += portals.size();
+ }
+ if (y < grid_size[1] - 1) {
+ auto neighbor = grid->get_sector(x, y + 1);
+ auto portals = sector->find_portals(neighbor, PortalDirection::NORTH_SOUTH, portal_id);
+ for (auto &portal : portals) {
+ sector->add_portal(portal);
+ neighbor->add_portal(portal);
+ }
+ portal_id += portals.size();
+ }
+ }
+ }
+
+ // Connect portals inside sectors.
+ for (auto sector : grid->get_sectors()) {
+ sector->connect_exits();
+
+ log::log(MSG(info) << "Sector " << sector->get_id() << " has " << sector->get_portals().size() << " portals.");
+ for (auto portal : sector->get_portals()) {
+ log::log(MSG(info) << " Portal " << portal->get_id() << ":");
+ log::log(MSG(info) << " Connects sectors: " << sector->get_id() << " to "
+ << portal->get_exit_sector(sector->get_id()));
+ log::log(MSG(info) << " Entry start: " << portal->get_entry_start(sector->get_id()));
+ log::log(MSG(info) << " Entry end: " << portal->get_entry_end(sector->get_id()));
+ log::log(MSG(info) << " Connected portals: " << portal->get_connected(sector->get_id()).size());
+ }
+ }
+
+ // Create a pathfinder for searching paths on the grid
+ auto pathfinder = std::make_shared();
+ pathfinder->add_grid(grid);
+
+ util::Timer timer;
+
+ // Create a path request and get the path
+ coord::tile start{2, 26};
+ coord::tile target{36, 2};
+
+ PathRequest path_request{
+ grid->get_id(),
+ start,
+ target,
+ };
+ timer.start();
+ Path path_result = pathfinder->get_path(path_request);
+ timer.stop();
+
+ log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps");
+
+ // Create a renderer to display the grid and path
+ auto qtapp = std::make_shared();
+
+ renderer::window_settings settings;
+ settings.width = 1024;
+ settings.height = 768;
+ auto window = std::make_shared("openage pathfinding test", settings);
+ auto render_manager = std::make_shared(qtapp, window, path, grid);
+ log::log(INFO << "Created render manager for pathfinding demo");
+
+ window->add_mouse_button_callback([&](const QMouseEvent &ev) {
+ if (ev.type() == QEvent::MouseButtonRelease) {
+ auto cell_count_x = grid->get_size()[0] * grid->get_sector_size();
+ auto cell_count_y = grid->get_size()[1] * grid->get_sector_size();
+ auto window_size = window->get_size();
+
+ double cell_size_x = static_cast(window_size[0]) / cell_count_x;
+ double cell_size_y = static_cast(window_size[1]) / cell_count_y;
+
+ coord::tile_t grid_x = ev.position().x() / cell_size_x;
+ coord::tile_t grid_y = ev.position().y() / cell_size_y;
+
+ if (ev.button() == Qt::RightButton) { // Set target cell
+ target = coord::tile{grid_x, grid_y};
+ PathRequest new_path_request{
+ grid->get_id(),
+ start,
+ target,
+ };
+
+ timer.reset();
+ timer.start();
+ path_result = pathfinder->get_path(new_path_request);
+ timer.stop();
+
+ log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps");
+
+ // Create renderables for the waypoints of the path
+ render_manager->create_waypoint_tiles(path_result);
+ }
+ else if (ev.button() == Qt::LeftButton) { // Set start cell
+ start = coord::tile{grid_x, grid_y};
+ PathRequest new_path_request{
+ grid->get_id(),
+ start,
+ target,
+ };
+
+ timer.reset();
+ timer.start();
+ path_result = pathfinder->get_path(new_path_request);
+ timer.stop();
+
+ log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps");
+
+ // Create renderables for the waypoints of the path
+ render_manager->create_waypoint_tiles(path_result);
+ }
+ }
+ });
+
+ // Create renderables for the waypoints of the path
+ render_manager->create_waypoint_tiles(path_result);
+
+ // Run the renderer pss to draw the grid and path into a window
+ // TODO: Make this a while (not window.should_close()) loop
+ render_manager->run();
+}
+
+
+RenderManager1::RenderManager1(const std::shared_ptr &app,
+ const std::shared_ptr &window,
+ const util::Path &path,
+ const std::shared_ptr &grid) :
+ path{path},
+ grid{grid},
+ app{app},
+ window{window},
+ renderer{window->make_renderer()} {
+ this->init_shaders();
+ this->init_passes();
+}
+
+void RenderManager1::run() {
+ while (not this->window->should_close()) {
+ this->app->process_events();
+
+ this->renderer->render(this->background_pass);
+ this->renderer->render(this->field_pass);
+ this->renderer->render(this->waypoint_pass);
+ this->renderer->render(this->grid_pass);
+ this->renderer->render(this->display_pass);
+
+ this->window->update();
+
+ this->renderer->check_error();
+ }
+ this->window->close();
+}
+
+void RenderManager1::init_passes() {
+ auto size = this->window->get_size();
+
+ // Make a framebuffer for the background render pass to draw into
+ auto background_texture = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::rgba8));
+ auto depth_texture = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::depth24));
+ auto fbo = renderer->create_texture_target({background_texture, depth_texture});
+
+ // Background object for contrast between field and display
+ Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity();
+ auto background_unifs = obj_shader->new_uniform_input(
+ "color",
+ Eigen::Vector4f{1.0, 1.0, 1.0, 1.0},
+ "model",
+ id_matrix,
+ "view",
+ id_matrix,
+ "proj",
+ id_matrix);
+ auto background = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad());
+ renderer::Renderable background_obj{
+ background_unifs,
+ background,
+ true,
+ true,
+ };
+ this->background_pass = renderer->add_render_pass({background_obj}, fbo);
+
+ // Make a framebuffer for the field render pass to draw into
+ auto field_texture = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::rgba8));
+ auto depth_texture_2 = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::depth24));
+ auto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2});
+ this->field_pass = renderer->add_render_pass({}, field_fbo);
+ this->create_impassible_tiles(this->grid);
+ this->create_portal_tiles(this->grid);
+
+ // Make a framebuffer for the grid render passes to draw into
+ auto grid_texture = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::rgba8));
+ auto depth_texture_3 = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::depth24));
+ auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_3});
+
+ // Make a framebuffer for the waypoint render pass to draw into
+ auto waypoint_texture = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::rgba8));
+ auto depth_texture_4 = renderer->add_texture(
+ renderer::resources::Texture2dInfo(size[0],
+ size[1],
+ renderer::resources::pixel_format::depth24));
+ auto waypoint_fbo = renderer->create_texture_target({waypoint_texture, depth_texture_4});
+ this->waypoint_pass = renderer->add_render_pass({}, waypoint_fbo);
+
+ // Create object for the grid
+ auto model = Eigen::Affine3f::Identity();
+ model.prescale(Eigen::Vector3f{
+ 2.0f / (this->grid->get_size()[0] * this->grid->get_sector_size()),
+ 2.0f / (this->grid->get_size()[1] * this->grid->get_sector_size()),
+ 1.0f});
+ model.pretranslate(Eigen::Vector3f{-1.0f, -1.0f, 0.0f});
+ auto grid_unifs = grid_shader->new_uniform_input(
+ "model",
+ model.matrix(),
+ "view",
+ id_matrix,
+ "proj",
+ id_matrix);
+ auto grid_mesh = this->get_grid_mesh(this->grid);
+ auto grid_geometry = renderer->add_mesh_geometry(grid_mesh);
+ renderer::Renderable grid_obj{
+ grid_unifs,
+ grid_geometry,
+ true,
+ true,
+ };
+ this->grid_pass = renderer->add_render_pass({grid_obj}, grid_fbo);
+
+ // Make two objects that draw the results of the previous passes onto the screen
+ // in the display render pass
+ auto bg_texture_unif = display_shader->new_uniform_input("color_texture", background_texture);
+ auto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad());
+ renderer::Renderable bg_pass_obj{
+ bg_texture_unif,
+ quad,
+ true,
+ true,
+ };
+ auto field_texture_unif = display_shader->new_uniform_input("color_texture", field_texture);
+ renderer::Renderable field_pass_obj{
+ field_texture_unif,
+ quad,
+ true,
+ true,
+ };
+ auto waypoint_texture_unif = display_shader->new_uniform_input("color_texture", waypoint_texture);
+ renderer::Renderable waypoint_pass_obj{
+ waypoint_texture_unif,
+ quad,
+ true,
+ true,
+ };
+ auto grid_texture_unif = display_shader->new_uniform_input("color_texture", grid_texture);
+ renderer::Renderable grid_pass_obj{
+ grid_texture_unif,
+ quad,
+ true,
+ true,
+ };
+
+ this->display_pass = renderer->add_render_pass(
+ {bg_pass_obj, field_pass_obj, waypoint_pass_obj, grid_pass_obj},
+ renderer->get_display_target());
+}
+
+void RenderManager1::init_shaders() {
+ // Shader sources
+ auto shaderdir = this->path / "assets" / "test" / "shaders" / "pathfinding";
+
+ // Shader for rendering the grid
+ auto grid_vshader_file = shaderdir / "demo_1_grid.vert.glsl";
+ auto grid_vshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::vertex,
+ grid_vshader_file);
+
+ auto grid_fshader_file = shaderdir / "demo_1_grid.frag.glsl";
+ auto grid_fshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::fragment,
+ grid_fshader_file);
+
+ // Shader for 2D monocolored objects
+ auto obj_vshader_file = shaderdir / "demo_1_obj.vert.glsl";
+ auto obj_vshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::vertex,
+ obj_vshader_file);
+
+ auto obj_fshader_file = shaderdir / "demo_1_obj.frag.glsl";
+ auto obj_fshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::fragment,
+ obj_fshader_file);
+
+ // Shader for rendering to the display target
+ auto display_vshader_file = shaderdir / "demo_1_display.vert.glsl";
+ auto display_vshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::vertex,
+ display_vshader_file);
+
+ auto display_fshader_file = shaderdir / "demo_1_display.frag.glsl";
+ auto display_fshader_src = renderer::resources::ShaderSource(
+ renderer::resources::shader_lang_t::glsl,
+ renderer::resources::shader_stage_t::fragment,
+ display_fshader_file);
+
+ // Create the shaders
+ this->grid_shader = renderer->add_shader({grid_vshader_src, grid_fshader_src});
+ this->obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src});
+ this->display_shader = renderer->add_shader({display_vshader_src, display_fshader_src});
+}
+
+
+renderer::resources::MeshData RenderManager1::get_grid_mesh(const std::shared_ptr &grid) {
+ // increase by 1 in every dimension because to get the vertex length
+ // of each dimension
+ util::Vector2s size{
+ grid->get_size()[0] * grid->get_sector_size() + 1,
+ grid->get_size()[1] * grid->get_sector_size() + 1};
+
+ // add vertices for the cells of the grid
+ std::vector verts{};
+ auto vert_count = size[0] * size[1];
+ verts.reserve(vert_count * 2);
+ for (int i = 0; i < (int)size[0]; ++i) {
+ for (int j = 0; j < (int)size[1]; ++j) {
+ verts.push_back(i);
+ verts.push_back(j);
+ }
+ }
+
+ // split the grid into lines using an index array
+ std::vector idxs;
+ idxs.reserve((size[0] - 1) * (size[1] - 1) * 8);
+ // iterate over all tiles in the grid by columns, i.e. starting
+ // from the left corner to the bottom corner if you imagine it from
+ // the camera's point of view
+ for (size_t i = 0; i < size[0] - 1; ++i) {
+ for (size_t j = 0; j < size[1] - 1; ++j) {
+ // since we are working on tiles, we just draw a square using the 4 vertices
+ idxs.push_back(j + i * size[1]); // bottom left
+ idxs.push_back(j + 1 + i * size[1]); // bottom right
+ idxs.push_back(j + 1 + i * size[1]); // bottom right
+ idxs.push_back(j + size[1] + 1 + i * size[1]); // top right
+ idxs.push_back(j + size[1] + 1 + i * size[1]); // top right
+ idxs.push_back(j + size[1] + i * size[1]); // top left
+ idxs.push_back(j + size[1] + i * size[1]); // top left
+ idxs.push_back(j + i * size[1]); // bottom left
+ }
+ }
+
+ renderer::resources::VertexInputInfo info{
+ {renderer::resources::vertex_input_t::V2F32},
+ renderer::resources::vertex_layout_t::AOS,
+ renderer::resources::vertex_primitive_t::LINES,
+ renderer::resources::index_t::U16};
+
+ auto const vert_data_size = verts.size() * sizeof(float);
+ std::vector vert_data(vert_data_size);
+ std::memcpy(vert_data.data(), verts.data(), vert_data_size);
+
+ auto const idx_data_size = idxs.size() * sizeof(uint16_t);
+ std::vector idx_data(idx_data_size);
+ std::memcpy(idx_data.data(), idxs.data(), idx_data_size);
+
+ return {std::move(vert_data), std::move(idx_data), info};
+}
+
+void RenderManager1::create_impassible_tiles(const std::shared_ptr &grid) {
+ auto width = grid->get_size()[0];
+ auto height = grid->get_size()[1];
+ auto sector_size = grid->get_sector_size();
+
+ float tile_offset_width = 2.0f / (width * sector_size);
+ float tile_offset_height = 2.0f / (height * sector_size);
+
+ for (size_t sector_x = 0; sector_x < width; sector_x++) {
+ for (size_t sector_y = 0; sector_y < height; sector_y++) {
+ auto sector = grid->get_sector(sector_x, sector_y);
+ auto cost_field = sector->get_cost_field();
+ for (size_t y = 0; y < sector_size; y++) {
+ for (size_t x = 0; x < sector_size; x++) {
+ auto cost = cost_field->get_cost(coord::tile(x, y));
+ if (cost == COST_IMPASSABLE) {
+ std::array tile_data{
+ -1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width,
+ 1.0f - y * tile_offset_height - sector_size * sector_y * tile_offset_height,
+ -1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width,
+ 1.0f - (y + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height,
+ -1.0f + (x + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width,
+ 1.0f - y * tile_offset_height - sector_size * sector_y * tile_offset_height,
+ -1.0f + (x + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width,
+ 1.0f - (y + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height,
+ };
+ renderer::resources::VertexInputInfo info{
+ {renderer::resources::vertex_input_t::V2F32},
+ renderer::resources::vertex_layout_t::AOS,
+ renderer::resources::vertex_primitive_t::TRIANGLE_STRIP};
+
+ auto const data_size = tile_data.size() * sizeof(float);
+ std::vector verts(data_size);
+ std::memcpy(verts.data(), tile_data.data(), data_size);
+
+ auto tile_mesh = renderer::resources::MeshData(std::move(verts), info);
+ auto tile_geometry = renderer->add_mesh_geometry(tile_mesh);
+
+ Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity();
+ auto tile_unifs = obj_shader->new_uniform_input(
+ "color",
+ Eigen::Vector4f{0.0, 0.0, 0.0, 1.0},
+ "model",
+ id_matrix,
+ "view",
+ id_matrix,
+ "proj",
+ id_matrix);
+ auto tile_obj = renderer::Renderable{
+ tile_unifs,
+ tile_geometry,
+ true,
+ true,
+ };
+ this->field_pass->add_renderables({tile_obj});
+ }
+ }
+ }
+ }
+ }
+}
+
+void RenderManager1::create_portal_tiles(const std::shared_ptr &grid) {
+ auto width = grid->get_size()[0];
+ auto height = grid->get_size()[1];
+ auto sector_size = grid->get_sector_size();
+
+ float tile_offset_width = 2.0f / (width * sector_size);
+ float tile_offset_height = 2.0f / (height * sector_size);
+
+ for (size_t sector_x = 0; sector_x < width; sector_x++) {
+ for (size_t sector_y = 0; sector_y < height; sector_y++) {
+ auto sector = grid->get_sector(sector_x, sector_y);
+
+ for (auto &portal : sector->get_portals()) {
+ auto start = portal->get_entry_start(sector->get_id());
+ auto end = portal->get_entry_end(sector->get_id());
+ auto direction = portal->get_direction();
+
+ std::vector tiles;
+ if (direction == PortalDirection::NORTH_SOUTH) {
+ auto y = start.se;
+ for (auto x = start.ne; x <= end.ne; ++x) {
+ tiles.push_back(coord::tile(x, y));
+ }
+ }
+ else {
+ auto x = start.ne;
+ for (auto y = start.se; y <= end.se; ++y) {
+ tiles.push_back(coord::tile(x, y));
+ }
+ }
+
+ for (auto tile : tiles) {
+ std::array tile_data{
+ -1.0f + tile.ne * tile_offset_width + sector_size * sector_x * tile_offset_width,
+ 1.0f - tile.se * tile_offset_height - sector_size * sector_y * tile_offset_height,
+ -1.0f + tile.ne * tile_offset_width + sector_size * sector_x * tile_offset_width,
+ 1.0f - (tile.se + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height,
+ -1.0f + (tile.ne + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width,
+ 1.0f - tile.se * tile_offset_height - sector_size * sector_y * tile_offset_height,
+ -1.0f + (tile.ne + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width,
+ 1.0f - (tile.se + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height,
+ };
+ renderer::resources::VertexInputInfo info{
+ {renderer::resources::vertex_input_t::V2F32},
+ renderer::resources::vertex_layout_t::AOS,
+ renderer::resources::vertex_primitive_t::TRIANGLE_STRIP};
+
+ auto const data_size = tile_data.size() * sizeof(float);
+ std::vector verts(data_size);
+ std::memcpy(verts.data(), tile_data.data(), data_size);
+
+ auto tile_mesh = renderer::resources::MeshData(std::move(verts), info);
+ auto tile_geometry = renderer->add_mesh_geometry(tile_mesh);
+
+ Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity();
+ auto tile_unifs = obj_shader->new_uniform_input(
+ "color",
+ Eigen::Vector4f{0.0, 0.0, 0.0, 0.3},
+ "model",
+ id_matrix,
+ "view",
+ id_matrix,
+ "proj",
+ id_matrix);
+ auto tile_obj = renderer::Renderable{
+ tile_unifs,
+ tile_geometry,
+ true,
+ true,
+ };
+ this->field_pass->add_renderables({tile_obj});
+ }
+ }
+ }
+ }
+}
+
+void RenderManager1::create_waypoint_tiles(const Path &path) {
+ auto width = grid->get_size()[0];
+ auto height = grid->get_size()[1];
+ auto sector_size = grid->get_sector_size();
+
+ float tile_offset_width = 2.0f / (width * sector_size);
+ float tile_offset_height = 2.0f / (height * sector_size);
+
+ Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity();
+
+ this->waypoint_pass->clear_renderables();
+
+ // Draw in-between waypoints
+ for (size_t i = 1; i < path.waypoints.size() - 1; i++) {
+ auto tile = path.waypoints[i];
+ std::array tile_data{
+ -1.0f + tile.ne * tile_offset_width,
+ 1.0f - tile.se * tile_offset_height,
+ -1.0f + tile.ne * tile_offset_width,
+ 1.0f - (tile.se + 1) * tile_offset_height,
+ -1.0f + (tile.ne + 1) * tile_offset_width,
+ 1.0f - tile.se * tile_offset_height,
+ -1.0f + (tile.ne + 1) * tile_offset_width,
+ 1.0f - (tile.se + 1) * tile_offset_height,
+ };
+
+ renderer::resources::VertexInputInfo info{
+ {renderer::resources::vertex_input_t::V2F32},
+ renderer::resources::vertex_layout_t::AOS,
+ renderer::resources::vertex_primitive_t::TRIANGLE_STRIP};
+
+ auto const data_size = tile_data.size() * sizeof(float);
+ std::vector verts(data_size);
+ std::memcpy(verts.data(), tile_data.data(), data_size);
+
+ auto tile_mesh = renderer::resources::MeshData(std::move(verts), info);
+ auto tile_geometry = renderer->add_mesh_geometry(tile_mesh);
+
+ auto tile_unifs = obj_shader->new_uniform_input(
+ "color",
+ Eigen::Vector4f{0.0, 0.25, 1.0, 1.0},
+ "model",
+ id_matrix,
+ "view",
+ id_matrix,
+ "proj",
+ id_matrix);
+ auto tile_obj = renderer::Renderable{
+ tile_unifs,
+ tile_geometry,
+ true,
+ true,
+ };
+ this->waypoint_pass->add_renderables({tile_obj});
+ }
+
+ // Draw start and end waypoints with different colors
+ auto start_tile = path.waypoints.front();
+ std::array start_tile_data{
+ -1.0f + start_tile.ne * tile_offset_width,
+ 1.0f - start_tile.se * tile_offset_height,
+ -1.0f + start_tile.ne * tile_offset_width,
+ 1.0f - (start_tile.se + 1) * tile_offset_height,
+ -1.0f + (start_tile.ne + 1) * tile_offset_width,
+ 1.0f - start_tile.se * tile_offset_height,
+ -1.0f + (start_tile.ne + 1) * tile_offset_width,
+ 1.0f - (start_tile.se + 1) * tile_offset_height,
+ };
+
+ renderer::resources::VertexInputInfo start_info{
+ {renderer::resources::vertex_input_t::V2F32},
+ renderer::resources::vertex_layout_t::AOS,
+ renderer::resources::vertex_primitive_t::TRIANGLE_STRIP};
+
+ auto const start_data_size = start_tile_data.size() * sizeof(float);
+ std::vector start_verts(start_data_size);
+ std::memcpy(start_verts.data(), start_tile_data.data(), start_data_size);
+
+ auto start_tile_mesh = renderer::resources::MeshData(std::move(start_verts), start_info);
+ auto start_tile_geometry = renderer->add_mesh_geometry(start_tile_mesh);
+ auto start_tile_unifs = obj_shader->new_uniform_input(
+ "color",
+ Eigen::Vector4f{0.0, 0.5, 0.0, 1.0},
+ "model",
+ id_matrix,
+ "view",
+ id_matrix,
+ "proj",
+ id_matrix);
+ auto start_tile_obj = renderer::Renderable{
+ start_tile_unifs,
+ start_tile_geometry,
+ true,
+ true,
+ };
+
+ auto end_tile = path.waypoints.back();
+ std::array end_tile_data{
+ -1.0f + end_tile.ne * tile_offset_width,
+ 1.0f - end_tile.se * tile_offset_height,
+ -1.0f + end_tile.ne * tile_offset_width,
+ 1.0f - (end_tile.se + 1) * tile_offset_height,
+ -1.0f + (end_tile.ne + 1) * tile_offset_width,
+ 1.0f - end_tile.se * tile_offset_height,
+ -1.0f + (end_tile.ne + 1) * tile_offset_width,
+ 1.0f - (end_tile.se + 1) * tile_offset_height,
+ };
+
+ renderer::resources::VertexInputInfo end_info{
+ {renderer::resources::vertex_input_t::V2F32},
+ renderer::resources::vertex_layout_t::AOS,
+ renderer::resources::vertex_primitive_t::TRIANGLE_STRIP};
+
+ auto const end_data_size = end_tile_data.size() * sizeof(float);
+ std::vector end_verts(end_data_size);
+ std::memcpy(end_verts.data(), end_tile_data.data(), end_data_size);
+
+ auto end_tile_mesh = renderer::resources::MeshData(std::move(end_verts), end_info);
+ auto end_tile_geometry = renderer->add_mesh_geometry(end_tile_mesh);
+ auto end_tile_unifs = obj_shader->new_uniform_input(
+ "color",
+ Eigen::Vector4f{1.0, 0.5, 0.0, 1.0},
+ "model",
+ id_matrix,
+ "view",
+ id_matrix,
+ "proj",
+ id_matrix);
+
+ auto end_tile_obj = renderer::Renderable{
+ end_tile_unifs,
+ end_tile_geometry,
+ true,
+ true,
+ };
+
+ this->waypoint_pass->add_renderables({start_tile_obj, end_tile_obj});
+}
+
+} // namespace openage::path::tests
diff --git a/libopenage/pathfinding/demo/demo_1.h b/libopenage/pathfinding/demo/demo_1.h
new file mode 100644
index 0000000000..db70efe084
--- /dev/null
+++ b/libopenage/pathfinding/demo/demo_1.h
@@ -0,0 +1,357 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#pragma once
+
+#include "pathfinding/definitions.h"
+#include "pathfinding/path.h"
+#include "renderer/resources/mesh_data.h"
+#include "util/path.h"
+
+
+namespace openage {
+namespace renderer {
+class Renderer;
+class RenderPass;
+class ShaderProgram;
+class Window;
+
+namespace gui {
+class GuiApplicationWithLogger;
+} // namespace gui
+
+} // namespace renderer
+
+namespace path {
+class Grid;
+
+namespace tests {
+
+
+/**
+ * Show the functionality of the high-level pathfinder:
+ * - Grids
+ * - Sectors
+ * - Portals
+ *
+ * Visualizes the pathfinding results using our rendering backend.
+ *
+ * @param path Path to the project rootdir.
+ */
+void path_demo_1(const util::Path &path);
+
+
+/**
+ * Manages the graphical display of the pathfinding demo.
+ */
+class RenderManager1 {
+public:
+ /**
+ * Create a new render manager.
+ *
+ * @param app GUI application.
+ * @param window Window to render to.
+ * @param path Path to the project rootdir.
+ */
+ RenderManager1(const std::shared_ptr &app,
+ const std::shared_ptr &window,
+ const util::Path &path,
+ const std::shared_ptr &grid);
+ ~RenderManager1() = default;
+
+ /**
+ * Run the render loop.
+ */
+ void run();
+
+ /**
+ * Create renderables for the waypoint tiles of a path.
+ *
+ * @param path Path object.
+ */
+ void create_waypoint_tiles(const Path &path);
+
+private:
+ /**
+ * Load the shader sources for the demo and create the shader programs.
+ */
+ void init_shaders();
+
+ /**
+ * Create the following render passes for the demo:
+ * - Background pass: Mono-colored background object.
+ * - Field pass; Renders the cost, integration and flow fields.
+ * - Grid pass: Renders a grid on top of the cost field.
+ * - Display pass: Draws the results of previous passes to the screen.
+ */
+ void init_passes();
+
+ /**
+ * Create a mesh for the grid.
+ *
+ * @param grid Pathing grid.
+ *
+ * @return Mesh data for the grid.
+ */
+ static renderer::resources::MeshData get_grid_mesh(const std::shared_ptr &grid);
+
+ /**
+ * Create renderables for the impassible tiles in the grid.
+ *
+ * @param grid Pathing grid.
+ */
+ void create_impassible_tiles(const std::shared_ptr &grid);
+
+ /**
+ * Create renderables for the portal tiles in the grid.
+ *
+ * @param grid Pathing grid.
+ */
+ void create_portal_tiles(const std::shared_ptr &grid);
+
+ /**
+ * Path to the project rootdir.
+ */
+ const util::Path &path;
+
+ /**
+ * Pathing grid of the demo.
+ */
+ std::shared_ptr grid;
+
+ /* Renderer objects */
+
+ /**
+ * Qt GUI application.
+ */
+ std::shared_ptr app;
+
+ /**
+ * openage window to render to.
+ */
+ std::shared_ptr window;
+
+ /**
+ * openage renderer instance.
+ */
+ std::shared_ptr renderer;
+
+ /* Shader programs */
+
+ /**
+ * Shader program for rendering a grid.
+ */
+ std::shared_ptr grid_shader;
+
+ /**
+ * Shader program for rendering 2D mono-colored objects.
+ */
+ std::shared_ptr obj_shader;
+
+ /**
+ * Shader program for rendering the final display.
+ */
+ std::shared_ptr display_shader;
+
+ /* Render passes */
+
+ /**
+ * Background pass: Mono-colored background object.
+ */
+ std::shared_ptr background_pass;
+
+ /**
+ * Field pass: Renders the cost field.
+ */
+ std::shared_ptr field_pass;
+
+ /**
+ * Grid pass: Renders a grid on top of the cost field.
+ */
+ std::shared_ptr grid_pass;
+
+ /**
+ * Waypoint pass: Renders the path and its waypoints.
+ */
+ std::shared_ptr waypoint_pass;
+
+ /**
+ * Display pass: Draws the results of previous passes to the screen.
+ */
+ std::shared_ptr display_pass;
+};
+
+// Cost for the sectors in the grid
+// taken from Figure 23.1 in "Crowd Pathfinding and Steering Using Flow Field Tiles"
+const std::vector> sectors_cost = {
+ {
+ // clang-format off
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 255, 255, 255, 255, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 255, 255, 255, 255, 1, 1,
+ 1, 1, 1, 255, 255, 255, 255, 255, 1, 1,
+ 1, 1, 255, 255, 255, 255, 255, 1, 1, 1,
+ 1, 1, 255, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 255, 1, 1, 1, 1, 1, 1, 1,
+ // clang-format on
+ },
+ {
+ // clang-format off
+ 1, 1, 255, 255, 255, 255, 255, 255, 255, 255,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 255, 1, 1, 255, 1, 1, 1,
+ 1, 1, 1, 255, 1, 255, 255, 1, 1, 1,
+ 1, 1, 1, 255, 255, 255, 255, 255, 255, 255,
+ 1, 1, 1, 1, 255, 255, 255, 1, 1, 1,
+ 1, 1, 1, 1, 255, 255, 1, 1, 1, 1,
+ 1, 1, 1, 255, 1, 1, 1, 1, 1, 1,
+ // clang-format on
+ },
+ {
+ // clang-format off
+ 255, 255, 1, 1, 1, 1, 1, 255, 255, 255,
+ 1, 1, 1, 1, 1, 1, 1, 1, 255, 255,
+ 1, 1, 1, 1, 1, 1, 1, 1, 255, 255,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 255, 255, 255, 1, 1, 1,
+ 1, 1, 1, 1, 1, 255, 255, 255, 255, 255,
+ 1, 1, 1, 1, 255, 255, 255, 255, 255, 1,
+ 1, 1, 1, 1, 255, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 255, 1, 1, 1, 1, 255,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ // clang-format on
+ },
+ {
+ // clang-format off
+ 255, 255, 1, 1, 1, 1, 1, 1, 1, 1,
+ 255, 255, 1, 1, 1, 1, 1, 1, 1, 1,
+ 255, 255, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 255, 1, 1, 1, 1, 1, 1,
+ 1, 1, 255, 255, 1, 1, 1, 1, 1, 1,
+ 255, 255, 255, 255, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ // clang-format on
+ },
+ {
+ // clang-format off
+ 1, 1, 255, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 255, 1, 1, 1, 1, 1, 255, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 255, 1, 1, 1, 1, 1,
+ 1, 1, 1, 255, 255, 1, 1, 1, 1, 1,
+ 1, 1, 255, 1, 255, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 255, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 255, 255, 255, 255, 255, 255,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 255, 255, 255,
+ // clang-format on
+ },
+ {
+ // clang-format off
+ 1, 1, 1, 255, 1, 1, 1, 1, 1, 255,
+ 1, 1, 1, 255, 1, 255, 255, 255, 255, 255,
+ 1, 1, 1, 255, 1, 1, 1, 1, 255, 255,
+ 1, 1, 1, 255, 1, 1, 1, 1, 255, 255,
+ 1, 255, 255, 1, 1, 1, 1, 1, 255, 255,
+ 1, 1, 255, 1, 1, 255, 1, 1, 1, 1,
+ 1, 1, 255, 1, 1, 255, 1, 1, 1, 1,
+ 255, 255, 255, 255, 255, 255, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 255, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ // clang-format on
+ },
+ {
+ // clang-format off
+ 255, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 255, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 255, 1, 1, 1, 1, 1, 1, 1, 255, 255,
+ 255, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 255, 1, 1, 1, 1, 1, 255, 1, 1, 1,
+ 255, 1, 1, 1, 1, 1, 255, 1, 1, 1,
+ 255, 1, 1, 1, 1, 1, 255, 1, 1, 1,
+ 255, 1, 1, 1, 1, 1, 255, 255, 255, 255,
+ 255, 1, 1, 1, 255, 255, 255, 255, 255, 1,
+ 255, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ // clang-format on
+ },
+ {
+ // clang-format off
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 255, 255, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 255, 255, 255, 255, 255, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 255, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 255, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ // clang-format on
+ },
+ {
+ // clang-format off
+ 1, 1, 255, 1, 1, 1, 1, 255, 255, 255,
+ 1, 1, 255, 1, 1, 1, 255, 255, 1, 1,
+ 1, 1, 1, 1, 1, 255, 255, 255, 1, 1,
+ 1, 1, 1, 1, 255, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 255, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 255,
+ 1, 1, 1, 1, 1, 1, 1, 1, 255, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ // clang-format on
+ },
+ {
+ // clang-format off
+ 255, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 255, 1, 1, 1, 1, 1, 1,
+ 1, 1, 255, 255, 255, 255, 255, 255, 1, 1,
+ 1, 255, 255, 255, 1, 1, 1, 1, 1, 1,
+ 255, 255, 255, 255, 1, 1, 1, 1, 1, 1,
+ 255, 1, 1, 1, 1, 1, 1, 1, 1, 255,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ // clang-format on
+ },
+ {
+ // clang-format off
+ 255, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 255, 255, 255, 255, 255, 1, 1,
+ 1, 1, 255, 255, 1, 1, 1, 255, 1, 1,
+ 1, 255, 255, 1, 1, 1, 1, 1, 1, 1,
+ 255, 255, 1, 1, 1, 1, 1, 1, 1, 1,
+ 255, 255, 1, 1, 1, 255, 255, 255, 1, 1,
+ 1, 1, 1, 1, 1, 1, 255, 1, 1, 1,
+ 1, 1, 1, 255, 255, 255, 255, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ // clang-format on
+ },
+ {
+ // clang-format off
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ // clang-format on
+ }};
+
+} // namespace tests
+} // namespace path
+} // namespace openage
diff --git a/libopenage/pathfinding/demo/tests.cpp b/libopenage/pathfinding/demo/tests.cpp
new file mode 100644
index 0000000000..633dddb621
--- /dev/null
+++ b/libopenage/pathfinding/demo/tests.cpp
@@ -0,0 +1,29 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#include "tests.h"
+
+#include "log/log.h"
+
+#include "pathfinding/demo/demo_0.h"
+#include "pathfinding/demo/demo_1.h"
+
+
+namespace openage::path::tests {
+
+void path_demo(int demo_id, const util::Path &path) {
+ switch (demo_id) {
+ case 0:
+ path_demo_0(path);
+ break;
+
+ case 1:
+ path_demo_1(path);
+ break;
+
+ default:
+ log::log(MSG(err) << "Unknown pathfinding demo requested: " << demo_id << ".");
+ break;
+ }
+}
+
+} // namespace openage::path::tests
diff --git a/libopenage/pathfinding/demo/tests.h b/libopenage/pathfinding/demo/tests.h
new file mode 100644
index 0000000000..2f411f6bf1
--- /dev/null
+++ b/libopenage/pathfinding/demo/tests.h
@@ -0,0 +1,20 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#pragma once
+
+#include "../../util/compiler.h"
+// pxd: from libopenage.util.path cimport Path
+
+
+namespace openage {
+namespace util {
+class Path;
+} // namespace util
+
+namespace path::tests {
+
+// pxd: void path_demo(int demo_id, Path path) except +
+OAAPI void path_demo(int demo_id, const util::Path &path);
+
+} // namespace path::tests
+} // namespace openage
diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp
new file mode 100644
index 0000000000..0aa8daf84b
--- /dev/null
+++ b/libopenage/pathfinding/flow_field.cpp
@@ -0,0 +1,234 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#include "flow_field.h"
+
+#include "error/error.h"
+#include "log/log.h"
+
+#include "coord/tile.h"
+#include "pathfinding/integration_field.h"
+#include "pathfinding/portal.h"
+
+
+namespace openage::path {
+
+FlowField::FlowField(size_t size) :
+ size{size},
+ cells(this->size * this->size, FLOW_INIT) {
+ log::log(DBG << "Created flow field with size " << this->size << "x" << this->size);
+}
+
+FlowField::FlowField(const std::shared_ptr &integration_field) :
+ size{integration_field->get_size()},
+ cells(this->size * this->size, FLOW_INIT) {
+ this->build(integration_field);
+}
+
+size_t FlowField::get_size() const {
+ return this->size;
+}
+
+flow_t FlowField::get_cell(const coord::tile &pos) const {
+ return this->cells.at(pos.ne + pos.se * this->size);
+}
+
+flow_dir_t FlowField::get_dir(const coord::tile &pos) const {
+ return static_cast(this->get_cell(pos) & FLOW_DIR_MASK);
+}
+
+void FlowField::build(const std::shared_ptr &integration_field,
+ const std::unordered_set &target_cells) {
+ ENSURE(integration_field->get_size() == this->get_size(),
+ "integration field size "
+ << integration_field->get_size() << "x" << integration_field->get_size()
+ << " does not match flow field size "
+ << this->get_size() << "x" << this->get_size());
+
+ auto &integrate_cells = integration_field->get_cells();
+ auto &flow_cells = this->cells;
+
+ for (size_t y = 0; y < this->size; ++y) {
+ for (size_t x = 0; x < this->size; ++x) {
+ size_t idx = y * this->size + x;
+
+ if (target_cells.contains(idx)) {
+ // Ignore target cells
+ continue;
+ }
+
+ if (integrate_cells[idx].cost == INTEGRATED_COST_UNREACHABLE) {
+ // Cell cannot be used as path
+ continue;
+ }
+
+ if (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) {
+ // Cell is in line of sight
+ this->cells[idx] = this->cells[idx] | FLOW_LOS_MASK;
+
+ // we can skip calculating the flow direction as we can
+ // move straight to the target from this cell
+ this->cells[idx] = this->cells[idx] | FLOW_PATHABLE_MASK;
+ continue;
+ }
+
+ if (integrate_cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) {
+ // Cell is blocked by a line-of-sight corner
+ this->cells[idx] = this->cells[idx] | FLOW_WAVEFRONT_BLOCKED_MASK;
+ }
+
+ // Find the neighbor with the smallest cost.
+ flow_dir_t direction = static_cast(this->cells[idx] & FLOW_DIR_MASK);
+ auto smallest_cost = INTEGRATED_COST_UNREACHABLE;
+ if (y > 0) {
+ auto cost = integrate_cells[idx - this->size].cost;
+ if (cost < smallest_cost) {
+ smallest_cost = cost;
+ direction = flow_dir_t::NORTH;
+ }
+ }
+ if (x < this->size - 1 && y > 0) {
+ auto cost = integrate_cells[idx - this->size + 1].cost;
+ if (cost < smallest_cost) {
+ smallest_cost = cost;
+ direction = flow_dir_t::NORTH_EAST;
+ }
+ }
+ if (x < this->size - 1) {
+ auto cost = integrate_cells[idx + 1].cost;
+ if (cost < smallest_cost) {
+ smallest_cost = cost;
+ direction = flow_dir_t::EAST;
+ }
+ }
+ if (x < this->size - 1 && y < this->size - 1) {
+ auto cost = integrate_cells[idx + this->size + 1].cost;
+ if (cost < smallest_cost) {
+ smallest_cost = cost;
+ direction = flow_dir_t::SOUTH_EAST;
+ }
+ }
+ if (y < this->size - 1) {
+ auto cost = integrate_cells[idx + this->size].cost;
+ if (cost < smallest_cost) {
+ smallest_cost = cost;
+ direction = flow_dir_t::SOUTH;
+ }
+ }
+ if (x > 0 && y < this->size - 1) {
+ auto cost = integrate_cells[idx + this->size - 1].cost;
+ if (cost < smallest_cost) {
+ smallest_cost = cost;
+ direction = flow_dir_t::SOUTH_WEST;
+ }
+ }
+ if (x > 0) {
+ auto cost = integrate_cells[idx - 1].cost;
+ if (cost < smallest_cost) {
+ smallest_cost = cost;
+ direction = flow_dir_t::WEST;
+ }
+ }
+ if (x > 0 && y > 0) {
+ auto cost = integrate_cells[idx - this->size - 1].cost;
+ if (cost < smallest_cost) {
+ smallest_cost = cost;
+ direction = flow_dir_t::NORTH_WEST;
+ }
+ }
+
+ // Set the flow field cell to pathable.
+ flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK;
+
+ // Set the flow field cell to the direction of the smallest cost.
+ flow_cells[idx] = flow_cells[idx] | static_cast(direction);
+ }
+ }
+}
+
+void FlowField::build(const std::shared_ptr &integration_field,
+ const std::shared_ptr & /* other */,
+ sector_id_t other_sector_id,
+ const std::shared_ptr &portal) {
+ ENSURE(integration_field->get_size() == this->get_size(),
+ "integration field size "
+ << integration_field->get_size() << "x" << integration_field->get_size()
+ << " does not match flow field size "
+ << this->get_size() << "x" << this->get_size());
+
+ auto &flow_cells = this->cells;
+ auto direction = portal->get_direction();
+
+ // portal entry and exit cell coordinates
+ auto entry_start = portal->get_entry_start(other_sector_id);
+ auto exit_start = portal->get_exit_start(other_sector_id);
+ auto exit_end = portal->get_exit_end(other_sector_id);
+
+ // TODO: Compare integration values from other side of portal
+ // auto &integrate_cells = integration_field->get_cells();
+
+ // cells that are part of the portal
+ std::unordered_set portal_cells;
+
+ // set the direction for the flow field cells that are part of the portal
+ if (direction == PortalDirection::NORTH_SOUTH) {
+ bool other_is_north = entry_start.se > exit_start.se;
+ if (other_is_north) {
+ auto y = exit_start.se;
+ for (auto x = exit_start.ne; x <= exit_end.ne; ++x) {
+ auto idx = y * this->size + x;
+ flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK;
+ flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::NORTH);
+ portal_cells.insert(idx);
+ }
+ }
+ else {
+ auto y = exit_start.se;
+ for (auto x = exit_start.ne; x <= exit_end.ne; ++x) {
+ auto idx = y * this->size + x;
+ flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK;
+ flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::SOUTH);
+ portal_cells.insert(idx);
+ }
+ }
+ }
+ else if (direction == PortalDirection::EAST_WEST) {
+ bool other_is_east = entry_start.ne < exit_start.ne;
+ if (other_is_east) {
+ auto x = exit_start.ne;
+ for (auto y = exit_start.se; y <= exit_end.se; ++y) {
+ auto idx = y * this->size + x;
+ flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK;
+ flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::EAST);
+ portal_cells.insert(idx);
+ }
+ }
+ else {
+ auto x = exit_start.ne;
+ for (auto y = exit_start.se; y <= exit_end.se; ++y) {
+ auto idx = y * this->size + x;
+ flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK;
+ flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::WEST);
+ portal_cells.insert(idx);
+ }
+ }
+ }
+ else {
+ throw Error(ERR << "Invalid portal direction: " << static_cast(direction));
+ }
+
+ this->build(integration_field, portal_cells);
+}
+
+const std::vector &FlowField::get_cells() const {
+ return this->cells;
+}
+
+void FlowField::reset() {
+ for (auto &cell : this->cells) {
+ cell = FLOW_INIT;
+ }
+
+ log::log(DBG << "Flow field has been reset");
+}
+
+} // namespace openage::path
diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h
new file mode 100644
index 0000000000..1c1adcfe33
--- /dev/null
+++ b/libopenage/pathfinding/flow_field.h
@@ -0,0 +1,112 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "pathfinding/definitions.h"
+#include "pathfinding/types.h"
+
+
+namespace openage {
+namespace coord {
+struct tile;
+} // namespace coord
+
+namespace path {
+class IntegrationField;
+class Portal;
+
+class FlowField {
+public:
+ /**
+ * Create a square flow field with a specified size.
+ *
+ * @param size Side length of the field.
+ */
+ FlowField(size_t size);
+
+ /**
+ * Create a flow field from an existing integration field.
+ *
+ * @param integration_field Integration field.
+ */
+ FlowField(const std::shared_ptr &integration_field);
+
+ /**
+ * Get the size of the flow field.
+ *
+ * @return Size of the flow field.
+ */
+ size_t get_size() const;
+
+ /**
+ * Get the flow field value at a specified position.
+ *
+ * @param pos Coordinates of the cell.
+ *
+ * @return Flowfield value at the specified position.
+ */
+ flow_t get_cell(const coord::tile &pos) const;
+
+ /**
+ * Get the flow field direction at a specified position.
+ *
+ * @param pos Coordinates of the cell.
+ *
+ * @return Flowfield direction at the specified position.
+ */
+ flow_dir_t get_dir(const coord::tile &pos) const;
+
+ /**
+ * Build the flow field.
+ *
+ * @param integration_field Integration field.
+ * @param target_cells Target cells of the flow field. These cells are ignored
+ * when building the field.
+ */
+ void build(const std::shared_ptr &integration_field,
+ const std::unordered_set &target_cells = {});
+
+ /**
+ * Build the flow field for a portal.
+ *
+ * @param integration_field Integration field.
+ * @param other Integration field of the other sector.
+ * @param other_sector_id Sector ID of the other field.
+ * @param portal Portal connecting the two fields.
+ */
+ void build(const std::shared_ptr &integration_field,
+ const std::shared_ptr &other,
+ sector_id_t other_sector_id,
+ const std::shared_ptr &portal);
+
+ /**
+ * Get the flow field values.
+ *
+ * @return Flow field values.
+ */
+ const std::vector &get_cells() const;
+
+ /**
+ * Reset the flow field values for rebuilding the field.
+ */
+ void reset();
+
+private:
+ /**
+ * Side length of the field.
+ */
+ size_t size;
+
+ /**
+ * Flow field cells.
+ */
+ std::vector cells;
+};
+
+} // namespace path
+} // namespace openage
diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp
new file mode 100644
index 0000000000..62a2d3156f
--- /dev/null
+++ b/libopenage/pathfinding/grid.cpp
@@ -0,0 +1,101 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#include "grid.h"
+
+#include "error/error.h"
+
+#include "coord/chunk.h"
+#include "pathfinding/cost_field.h"
+#include "pathfinding/sector.h"
+
+
+namespace openage::path {
+
+Grid::Grid(grid_id_t id,
+ const util::Vector2s &size,
+ size_t sector_size) :
+ id{id},
+ size{size},
+ sector_size{sector_size} {
+ for (size_t y = 0; y < size[1]; y++) {
+ for (size_t x = 0; x < size[0]; x++) {
+ this->sectors.push_back(
+ std::make_shared(x + y * this->size[0],
+ coord::chunk(x, y),
+ sector_size));
+ }
+ }
+}
+
+Grid::Grid(grid_id_t id,
+ const util::Vector2s &size,
+ std::vector> &§ors) :
+ id{id},
+ size{size},
+ sectors{std::move(sectors)} {
+ ENSURE(this->sectors.size() == size[0] * size[1],
+ "Grid has size " << size[0] << "x" << size[1] << " (" << size[0] * size[1] << " sectors), "
+ << "but only " << this->sectors.size() << " sectors were provided");
+
+ this->sector_size = sectors.at(0)->get_cost_field()->get_size();
+}
+
+grid_id_t Grid::get_id() const {
+ return this->id;
+}
+
+const util::Vector2s &Grid::get_size() const {
+ return this->size;
+}
+
+size_t Grid::get_sector_size() const {
+ return this->sector_size;
+}
+
+const std::shared_ptr &Grid::get_sector(size_t x, size_t y) {
+ return this->sectors.at(x + y * this->size[0]);
+}
+
+const std::shared_ptr &Grid::get_sector(sector_id_t id) const {
+ return this->sectors.at(id);
+}
+
+const std::vector> &Grid::get_sectors() const {
+ return this->sectors;
+}
+
+void Grid::init_portals() {
+ // Create portals between neighboring sectors.
+ portal_id_t portal_id = 0;
+ for (size_t y = 0; y < this->size[1]; y++) {
+ for (size_t x = 0; x < this->size[0]; x++) {
+ auto sector = this->get_sector(x, y);
+
+ if (x < this->size[0] - 1) {
+ auto neighbor = this->get_sector(x + 1, y);
+ auto portals = sector->find_portals(neighbor, PortalDirection::EAST_WEST, portal_id);
+ for (auto &portal : portals) {
+ sector->add_portal(portal);
+ neighbor->add_portal(portal);
+ }
+ portal_id += portals.size();
+ }
+ if (y < this->size[1] - 1) {
+ auto neighbor = this->get_sector(x, y + 1);
+ auto portals = sector->find_portals(neighbor, PortalDirection::NORTH_SOUTH, portal_id);
+ for (auto &portal : portals) {
+ sector->add_portal(portal);
+ neighbor->add_portal(portal);
+ }
+ portal_id += portals.size();
+ }
+ }
+ }
+
+ // Connect mutually reachable exits of sectors.
+ for (auto §or : this->sectors) {
+ sector->connect_exits();
+ }
+}
+
+} // namespace openage::path
diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h
new file mode 100644
index 0000000000..83b4c2b721
--- /dev/null
+++ b/libopenage/pathfinding/grid.h
@@ -0,0 +1,121 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "pathfinding/types.h"
+#include "util/vector.h"
+
+
+namespace openage::path {
+class Sector;
+
+/**
+ * Grid for flow field pathfinding.
+ */
+class Grid {
+public:
+ /**
+ * Create a new empty grid of width x height sectors with a specified size.
+ *
+ * @param id ID of the grid.
+ * @param size Size of the grid (width x height).
+ * @param sector_size Side length of each sector.
+ */
+ Grid(grid_id_t id,
+ const util::Vector2s &size,
+ size_t sector_size);
+
+ /**
+ * Create a grid of width x height sectors from a list of existing sectors.
+ *
+ * @param id ID of the grid.
+ * @param size Size of the grid (width x height).
+ * @param sectors Existing sectors.
+ */
+ Grid(grid_id_t id,
+ const util::Vector2s &size,
+ std::vector> &§ors);
+
+ /**
+ * Get the ID of the grid.
+ *
+ * @return ID of the grid.
+ */
+ grid_id_t get_id() const;
+
+ /**
+ * Get the size of the grid.
+ *
+ * @return Size of the grid (width x height).
+ */
+ const util::Vector2s &get_size() const;
+
+ /**
+ * Get the side length of the sectors on the grid.
+ *
+ * @return Sector side length.
+ */
+ size_t get_sector_size() const;
+
+ /**
+ * Get the sector at a specified position.
+ *
+ * @param x X coordinate.
+ * @param y Y coordinate.
+ *
+ * @return Sector at the specified position.
+ */
+ const std::shared_ptr &get_sector(size_t x, size_t y);
+
+ /**
+ * Get the sector with a specified ID
+ *
+ * @param id ID of the sector.
+ *
+ * @return Sector with the specified ID.
+ */
+ const std::shared_ptr &get_sector(sector_id_t id) const;
+
+ /**
+ * Get the sectors of the grid.
+ *
+ * @return Sectors of the grid.
+ */
+ const std::vector> &get_sectors() const;
+
+ /**
+ * Initialize the portals of the sectors on the grid.
+ *
+ * This should be called after all sectors' cost fields have been initialized.
+ */
+ void init_portals();
+
+private:
+ /**
+ * ID of the grid.
+ */
+ grid_id_t id;
+
+ /**
+ * Size of the grid (width x height).
+ */
+ util::Vector2s size;
+
+ /**
+ * Side length of the grid sectors.
+ */
+ size_t sector_size;
+
+ /**
+ * Sectors of the grid.
+ */
+ std::vector> sectors;
+};
+
+
+} // namespace openage::path
diff --git a/libopenage/pathfinding/heuristics.cpp b/libopenage/pathfinding/heuristics.cpp
deleted file mode 100644
index 9a1a1213dc..0000000000
--- a/libopenage/pathfinding/heuristics.cpp
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2014-2018 the openage authors. See copying.md for legal info.
-
-#include "heuristics.h"
-
-#include
-#include
-
-
-namespace openage {
-namespace path {
-
-cost_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end) {
- cost_t dx = std::abs(start.ne - end.ne).to_float();
- cost_t dy = std::abs(start.se - end.se).to_float();
- return dx + dy;
-}
-
-cost_t chebyshev_cost(const coord::phys3 &start, const coord::phys3 &end) {
- cost_t dx = std::abs(start.ne - end.ne).to_float();
- cost_t dy = std::abs(start.se - end.se).to_float();
- return std::max(dx, dy);
-}
-
-cost_t euclidean_cost(const coord::phys3 &start, const coord::phys3 &end) {
- return (end - start).length();
-}
-
-cost_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 &end) {
- cost_t dx = (start.ne - end.ne).to_float();
- cost_t dy = (start.se - end.se).to_float();
- return dx * dx + dy * dy;
-}
-
-}} // openage::path
diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp
new file mode 100644
index 0000000000..bc2745879f
--- /dev/null
+++ b/libopenage/pathfinding/integration_field.cpp
@@ -0,0 +1,520 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#include "integration_field.h"
+
+#include
+
+#include "error/error.h"
+#include "log/log.h"
+
+#include "coord/tile.h"
+#include "pathfinding/cost_field.h"
+#include "pathfinding/definitions.h"
+#include "pathfinding/portal.h"
+
+
+namespace openage::path {
+
+IntegrationField::IntegrationField(size_t size) :
+ size{size},
+ cells(this->size * this->size, INTEGRATE_INIT) {
+ log::log(DBG << "Created integration field with size " << this->size << "x" << this->size);
+}
+
+size_t IntegrationField::get_size() const {
+ return this->size;
+}
+
+const integrated_t &IntegrationField::get_cell(const coord::tile &pos) const {
+ return this->cells.at(pos.ne + pos.se * this->size);
+}
+
+const integrated_t &IntegrationField::get_cell(size_t idx) const {
+ return this->cells.at(idx);
+}
+
+std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field,
+ const coord::tile &target) {
+ ENSURE(cost_field->get_size() == this->get_size(),
+ "cost field size "
+ << cost_field->get_size() << "x" << cost_field->get_size()
+ << " does not match integration field size "
+ << this->get_size() << "x" << this->get_size());
+
+ // Store the wavefront_blocked cells
+ std::vector wavefront_blocked;
+
+ // Target cell index
+ auto target_idx = target.ne + target.se * this->size;
+
+ // Lookup table for cells that have been found
+ std::unordered_set found;
+ found.reserve(this->size * this->size);
+
+ // Cells that still have to be visited by the current wave
+ std::deque current_wave;
+
+ // Cells that have to be visited in the next wave
+ std::deque next_wave;
+
+ // Cost of the current wave
+ integrated_cost_t cost = INTEGRATED_COST_START;
+
+ // Move outwards from the target cell, updating the integration field
+ current_wave.push_back(target_idx);
+ do {
+ while (!current_wave.empty()) {
+ auto idx = current_wave.front();
+ current_wave.pop_front();
+
+ if (found.contains(idx)) {
+ // Skip cells that have already been found
+ continue;
+ }
+ else if (this->cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) {
+ // Stop at cells that are blocked by a LOS corner
+ this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx);
+ found.insert(idx);
+ continue;
+ }
+
+ // Add the current cell to the found cells
+ found.insert(idx);
+
+ // Get the x and y coordinates of the current cell
+ auto x = idx % this->size;
+ auto y = idx / this->size;
+
+ // Get the cost of the current cell
+ auto cell_cost = cost_field->get_cost(idx);
+
+ if (cell_cost > COST_MIN) {
+ // cell blocks line of sight
+ // and we have to check for corners
+ if (cell_cost != COST_IMPASSABLE) {
+ // Add the current cell to the blocked wavefront if it's not a wall
+ wavefront_blocked.push_back(idx);
+ this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx);
+ }
+
+ // check each neighbor for a corner
+ auto corners = this->get_los_corners(cost_field, target, coord::tile(x, y));
+ for (auto &corner : corners) {
+ // draw a line from the corner to the edge of the field
+ // to get the cells blocked by the corner
+ auto blocked_cells = this->bresenhams_line(target, corner.first, corner.second);
+ for (auto &blocked_idx : blocked_cells) {
+ if (cost_field->get_cost(blocked_idx) > COST_MIN) {
+ // stop if blocked_idx is impassable
+ break;
+ }
+ // set the blocked flag for the cell
+ this->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK;
+
+ // clear los flag if it was set
+ this->cells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK;
+ }
+ wavefront_blocked.insert(wavefront_blocked.end(), blocked_cells.begin(), blocked_cells.end());
+ }
+ continue;
+ }
+
+ // The cell is in the line of sight at min cost
+ // Set the LOS flag and cost
+ this->cells[idx].cost = cost;
+ this->cells[idx].flags |= INTEGRATE_LOS_MASK;
+
+ // Search the neighbors of the current cell
+ if (y > 0) {
+ next_wave.push_back(idx - this->size);
+ }
+ if (x > 0) {
+ next_wave.push_back(idx - 1);
+ }
+ if (y < this->size - 1) {
+ next_wave.push_back(idx + this->size);
+ }
+ if (x < this->size - 1) {
+ next_wave.push_back(idx + 1);
+ }
+ }
+
+ // increment the cost and advance the wavefront outwards
+ cost += 1;
+ current_wave.swap(next_wave);
+ }
+ while (!current_wave.empty());
+
+ return wavefront_blocked;
+}
+
+void IntegrationField::integrate_cost(const std::shared_ptr &cost_field,
+ const coord::tile &target) {
+ ENSURE(cost_field->get_size() == this->get_size(),
+ "cost field size "
+ << cost_field->get_size() << "x" << cost_field->get_size()
+ << " does not match integration field size "
+ << this->get_size() << "x" << this->get_size());
+
+ // Target cell index
+ auto target_idx = target.ne + target.se * this->size;
+
+ // Move outwards from the target cell, updating the integration field
+ this->cells[target_idx].cost = INTEGRATED_COST_START;
+ this->integrate_cost(cost_field, {target_idx});
+}
+
+void IntegrationField::integrate_cost(const std::shared_ptr &cost_field,
+ sector_id_t other_sector_id,
+ const std::shared_ptr &portal) {
+ ENSURE(cost_field->get_size() == this->get_size(),
+ "cost field size "
+ << cost_field->get_size() << "x" << cost_field->get_size()
+ << " does not match integration field size "
+ << this->get_size() << "x" << this->get_size());
+
+ // Integrate the cost of the cells on the exit side (this) of the portal
+ std::vector start_cells;
+ auto exit_start = portal->get_exit_start(other_sector_id);
+ auto exit_end = portal->get_exit_end(other_sector_id);
+ for (auto y = exit_start.se; y <= exit_end.se; ++y) {
+ for (auto x = exit_start.ne; x <= exit_end.ne; ++x) {
+ // every portal cell is a target cell
+ auto target_idx = x + y * this->size;
+
+ // Set the cost of all target cells to the start value
+ this->cells[target_idx].cost = INTEGRATED_COST_START;
+ start_cells.push_back(target_idx);
+
+ // TODO: Transfer flags and cost from the other integration field
+ }
+ }
+
+ // Integrate the rest of the cost field
+ this->integrate_cost(cost_field, std::move(start_cells));
+}
+
+void IntegrationField::integrate_cost(const std::shared_ptr &cost_field,
+ std::vector &&start_cells) {
+ // Lookup table for cells that are in the open list
+ std::unordered_set in_list;
+ in_list.reserve(this->size * this->size);
+
+ // Cells that still have to be visited
+ // they may be visited multiple times
+ std::deque open_list;
+
+ // Stores neighbors of the current cell
+ std::vector neighbors;
+ neighbors.reserve(4);
+
+ // Move outwards from the wavefront, updating the integration field
+ open_list.insert(open_list.end(), start_cells.begin(), start_cells.end());
+ while (!open_list.empty()) {
+ auto idx = open_list.front();
+ open_list.pop_front();
+
+ // Get the x and y coordinates of the current cell
+ auto x = idx % this->size;
+ auto y = idx / this->size;
+
+ auto integrated_current = this->cells.at(idx).cost;
+
+ // Get the neighbors of the current cell
+ if (y > 0) {
+ neighbors.push_back(idx - this->size);
+ }
+ if (x > 0) {
+ neighbors.push_back(idx - 1);
+ }
+ if (y < this->size - 1) {
+ neighbors.push_back(idx + this->size);
+ }
+ if (x < this->size - 1) {
+ neighbors.push_back(idx + 1);
+ }
+
+ // Update the integration field of the neighboring cells
+ for (auto &neighbor_idx : neighbors) {
+ this->update_neighbor(neighbor_idx,
+ cost_field->get_cost(neighbor_idx),
+ integrated_current,
+ open_list,
+ in_list);
+ }
+
+ // Clear the neighbors vector
+ neighbors.clear();
+
+ // Remove the current cell from the open list lookup table
+ in_list.erase(idx);
+ }
+}
+
+const std::vector &IntegrationField::get_cells() const {
+ return this->cells;
+}
+
+void IntegrationField::reset() {
+ for (auto &cell : this->cells) {
+ cell = INTEGRATE_INIT;
+ }
+ log::log(DBG << "Integration field has been reset");
+}
+
+void IntegrationField::update_neighbor(size_t idx,
+ cost_t cell_cost,
+ integrated_cost_t integrated_cost,
+ std::deque &open_list,
+ std::unordered_set &in_list) {
+ ENSURE(cell_cost > COST_INIT, "cost field cell value must be non-zero");
+
+ // Check if the cell is impassable
+ // then we don't need to update the integration field
+ if (cell_cost == COST_IMPASSABLE) {
+ return;
+ }
+
+ auto cost = integrated_cost + cell_cost;
+ if (cost < this->cells.at(idx).cost) {
+ // If the new integration value is smaller than the current one,
+ // update the cell and add it to the open list
+ this->cells[idx].cost = cost;
+
+ if (not in_list.contains(idx)) {
+ in_list.insert(idx);
+ open_list.push_back(idx);
+ }
+ }
+}
+
+std::vector> IntegrationField::get_los_corners(const std::shared_ptr &cost_field,
+ const coord::tile &target,
+ const coord::tile &blocker) {
+ std::vector> corners;
+
+ // Get the cost of the blocking cell's neighbors
+
+ // Set all costs to IMPASSABLE at the beginning
+ auto top_cost = COST_IMPASSABLE;
+ auto left_cost = COST_IMPASSABLE;
+ auto bottom_cost = COST_IMPASSABLE;
+ auto right_cost = COST_IMPASSABLE;
+
+ std::pair top_left{blocker.ne, blocker.se};
+ std::pair top_right{blocker.ne + 1, blocker.se};
+ std::pair bottom_left{blocker.ne, blocker.se + 1};
+ std::pair bottom_right{blocker.ne + 1, blocker.se + 1};
+
+ // Get neighbor costs (if they exist)
+ if (blocker.se > 0) {
+ top_cost = cost_field->get_cost(coord::tile{blocker.ne, blocker.se - 1});
+ }
+ if (blocker.ne > 0) {
+ left_cost = cost_field->get_cost(coord::tile{blocker.ne - 1, blocker.se});
+ }
+ if (static_cast(blocker.se) < this->size - 1) {
+ bottom_cost = cost_field->get_cost(coord::tile{blocker.ne, blocker.se + 1});
+ }
+ if (static_cast(blocker.ne) < this->size - 1) {
+ right_cost = cost_field->get_cost(coord::tile{blocker.ne + 1, blocker.se});
+ }
+
+ // Check which corners are blocking LOS
+ // TODO: Currently super complicated and could likely be optimized
+ if (blocker.ne == target.ne) {
+ // blocking cell is parallel to target on y-axis
+ if (blocker.se < target.se) {
+ if (left_cost == COST_MIN) {
+ // top
+ corners.push_back(bottom_left);
+ }
+ if (right_cost == COST_MIN) {
+ // top
+ corners.push_back(bottom_right);
+ }
+ }
+ else {
+ if (left_cost == COST_MIN) {
+ // bottom
+ corners.push_back(top_left);
+ }
+ if (right_cost == COST_MIN) {
+ // bottom
+ corners.push_back(top_right);
+ }
+ }
+ }
+ else if (blocker.se == target.se) {
+ // blocking cell is parallel to target on x-axis
+ if (blocker.ne < target.ne) {
+ if (top_cost == COST_MIN) {
+ // right
+ corners.push_back(top_right);
+ }
+ if (bottom_cost == COST_MIN) {
+ // right
+ corners.push_back(bottom_right);
+ }
+ }
+ else {
+ if (top_cost == COST_MIN) {
+ // left
+ corners.push_back(top_left);
+ }
+ if (bottom_cost == COST_MIN) {
+ // left
+ corners.push_back(bottom_left);
+ }
+ }
+ }
+ else {
+ // blocking cell is diagonal to target on
+ if (blocker.ne < target.ne) {
+ if (blocker.se < target.se) {
+ // top and right
+ if (top_cost == COST_MIN and right_cost == COST_MIN) {
+ // right
+ corners.push_back(top_right);
+ }
+ if (left_cost == COST_MIN and bottom_cost == COST_MIN) {
+ // bottom
+ corners.push_back(bottom_left);
+ }
+ }
+ else {
+ // bottom and right
+ if (bottom_cost == COST_MIN and right_cost == COST_MIN) {
+ // right
+ corners.push_back(bottom_right);
+ }
+ if (left_cost == COST_MIN and top_cost == COST_MIN) {
+ // top
+ corners.push_back(top_left);
+ }
+ }
+ }
+ else {
+ if (blocker.se < target.se) {
+ // top and left
+ if (top_cost == COST_MIN and left_cost == COST_MIN) {
+ // left
+ corners.push_back(top_left);
+ }
+ if (right_cost == COST_MIN and bottom_cost == COST_MIN) {
+ // bottom
+ corners.push_back(bottom_right);
+ }
+ }
+ else {
+ // bottom and left
+ if (bottom_cost == COST_MIN and left_cost == COST_MIN) {
+ // left
+ corners.push_back(bottom_left);
+ }
+ if (right_cost == COST_MIN and top_cost == COST_MIN) {
+ // top
+ corners.push_back(top_right);
+ }
+ }
+ }
+ }
+
+ return corners;
+}
+
+std::vector IntegrationField::bresenhams_line(const coord::tile &target,
+ int corner_x,
+ int corner_y) {
+ std::vector cells;
+
+ // cell coordinates
+ // these have to be offset depending on the line direction
+ auto cell_x = corner_x;
+ auto cell_y = corner_y;
+
+ // field edge boundary
+ int boundary = this->size;
+
+ // target coordinates
+ // offset by 0.5 to get the center of the cell
+ double tx = target.ne + 0.5;
+ double ty = target.se + 0.5;
+
+ // slope of the line
+ double dx = std::abs(tx - corner_x);
+ double dy = std::abs(ty - corner_y);
+ auto m = dy / dx;
+
+ // error margin for the line
+ // if the error is greater than 1.0, we have to move in the y direction
+ auto error = m;
+
+ // Check which direction the line is going
+ if (corner_x < tx) {
+ if (corner_y < ty) { // left and up
+ cell_y -= 1;
+ cell_x -= 1;
+ while (cell_x >= 0 and cell_y >= 0) {
+ cells.push_back(cell_x + cell_y * this->size);
+ if (error > 1.0) {
+ cell_y -= 1;
+ error -= 1.0;
+ }
+ else {
+ cell_x -= 1;
+ error += m;
+ }
+ }
+ }
+
+ else { // left and down
+ cell_x -= 1;
+ while (cell_x >= 0 and cell_y < boundary) {
+ cells.push_back(cell_x + cell_y * this->size);
+ if (error > 1.0) {
+ cell_y += 1;
+ error -= 1.0;
+ }
+ else {
+ cell_x -= 1;
+ error += m;
+ }
+ }
+ }
+ }
+ else {
+ if (corner_y < ty) { // right and up
+ cell_y -= 1;
+ while (cell_x < boundary and cell_y >= 0) {
+ cells.push_back(cell_x + cell_y * this->size);
+ if (error > 1.0) {
+ cell_y -= 1;
+ error -= 1.0;
+ }
+ else {
+ cell_x += 1;
+ error += m;
+ }
+ }
+ }
+ else { // right and down
+ while (cell_x < boundary and cell_y < boundary) {
+ cells.push_back(cell_x + cell_y * this->size);
+ if (error > 1.0) {
+ cell_y += 1;
+ error -= 1.0;
+ }
+ else {
+ cell_x += 1;
+ error += m;
+ }
+ }
+ }
+ }
+
+ return cells;
+}
+
+
+} // namespace openage::path
diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h
new file mode 100644
index 0000000000..183fb9cff6
--- /dev/null
+++ b/libopenage/pathfinding/integration_field.h
@@ -0,0 +1,175 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "pathfinding/types.h"
+
+
+namespace openage {
+namespace coord {
+struct tile;
+} // namespace coord
+
+namespace path {
+class CostField;
+class Portal;
+
+/**
+ * Integration field in the flow-field pathfinding algorithm.
+ */
+class IntegrationField {
+public:
+ /**
+ * Create a square integration field with a specified size.
+ *
+ * @param size Side length of the field.
+ */
+ IntegrationField(size_t size);
+
+ /**
+ * Get the size of the integration field.
+ *
+ * @return Size of the integration field.
+ */
+ size_t get_size() const;
+
+ /**
+ * Get the integration value at a specified position.
+ *
+ * @param pos Coordinates of the cell.
+ * @return Integration value at the specified position.
+ */
+ const integrated_t &get_cell(const coord::tile &pos) const;
+
+ /**
+ * Get the integration value at a specified position.
+ *
+ * @param idx Index of the cell.
+ * @return Integration value at the specified position.
+ */
+ const integrated_t &get_cell(size_t idx) const;
+
+ /**
+ * Calculate the line-of-sight integration flags for a target cell.
+ *
+ * Returns a list of cells that are flagged as "wavefront blocked". These cells
+ * can be used as a starting point for the cost integration.
+ *
+ * @param cost_field Cost field to integrate.
+ * @param target Coordinates of the target cell.
+ *
+ * @return Cells flagged as "wavefront blocked".
+ */
+ std::vector integrate_los(const std::shared_ptr &cost_field,
+ const coord::tile &target);
+
+ /**
+ * Calculate the cost integration field starting from a target cell.
+ *
+ * @param cost_field Cost field to integrate.
+ * @param target Coordinates of the target cell.
+ */
+ void integrate_cost(const std::shared_ptr &cost_field,
+ const coord::tile &target);
+
+ /**
+ * Calculate the cost integration field starting from a portal to another
+ * integration field.
+ *
+ * @param cost_field Cost field to integrate.
+ * @param other_sector_id Sector ID of the other integration field.
+ * @param portal Portal connecting the two fields.
+ */
+ void integrate_cost(const std::shared_ptr &cost_field,
+ sector_id_t other_sector_id,
+ const std::shared_ptr &portal);
+
+ /**
+ * Calculate the cost integration field starting from a wavefront.
+ *
+ * @param cost_field Cost field to integrate.
+ * @param start_cells Cells flagged as "wavefront blocked" from a LOS pass.
+ */
+ void integrate_cost(const std::shared_ptr &cost_field,
+ std::vector &&start_cells);
+
+ /**
+ * Get the integration field values.
+ *
+ * @return Integration field values.
+ */
+ const std::vector &get_cells() const;
+
+ /**
+ * Reset the integration field for a new integration.
+ */
+ void reset();
+
+private:
+ /**
+ * Update a neigbor cell during the cost integration process.
+ *
+ * @param idx Index of the neighbor cell that is updated.
+ * @param cell_cost Cost of the neighbor cell from the cost field.
+ * @param integrated_cost Current integrated cost of the updating cell in the integration field.
+ * @param open_list List of cells to be updated.
+ * @param in_list Set of cells that have been updated.
+ *
+ * @return New integration value of the cell.
+ */
+ void update_neighbor(size_t idx,
+ cost_t cell_cost,
+ integrated_cost_t integrated_cost,
+ std::deque &open_list,
+ std::unordered_set &in_list);
+
+ /**
+ * Get the LOS corners around a cell.
+ *
+ * @param cost_field Cost field to integrate.
+ * @param target Cell coordinates of the target.
+ * @param blocker Cell coordinates of the cell blocking LOS.
+ *
+ * @return Field coordinates of the LOS corners.
+ */
+ std::vector> get_los_corners(const std::shared_ptr &cost_field,
+ const coord::tile &target,
+ const coord::tile &blocker);
+
+ /**
+ * Get the cells in a bresenham's line between the corner cell and the field edge.
+ *
+ * This function is a modified version of the bresenham's line algorithm that
+ * retrieves the cells between the corner point and the field's edge, rather than
+ * the cells between two arbitrary points. We do this because the intersection
+ * point with the field edge is unknown.
+ *
+ * @param target Cell coordinates of the target.
+ * @param corner_x X field coordinate edge of the LOS corner.
+ * @param corner_y Y field coordinate edge of the LOS corner.
+ *
+ * @return Cell indices of the LOS line.
+ */
+ std::vector bresenhams_line(const coord::tile &target,
+ int corner_x,
+ int corner_y);
+
+ /**
+ * Side length of the field.
+ */
+ size_t size;
+
+ /**
+ * Integration field values.
+ */
+ std::vector cells;
+};
+
+} // namespace path
+} // namespace openage
diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp
new file mode 100644
index 0000000000..28f8a829ca
--- /dev/null
+++ b/libopenage/pathfinding/integrator.cpp
@@ -0,0 +1,66 @@
+// Copyright 2024-2024 the openage authors. See copying.md for legal info.
+
+#include "integrator.h"
+
+#include "pathfinding/cost_field.h"
+#include "pathfinding/flow_field.h"
+#include "pathfinding/integration_field.h"
+
+
+namespace openage::path {
+
+std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field,
+ const coord::tile &target) {
+ auto integration_field = std::make_shared(cost_field->get_size());
+
+ auto wavefront_blocked = integration_field->integrate_los(cost_field, target);
+ integration_field->integrate_cost(cost_field, std::move(wavefront_blocked));
+
+ return integration_field;
+}
+
+std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field,
+ sector_id_t other_sector_id,
+ const std::shared_ptr &portal) {
+ auto integration_field = std::make_shared