├── gradle.properties ├── settings.gradle ├── .gitignore ├── data └── fonts │ ├── default.otf │ └── license.txt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .file-dialog.properties ├── .github └── workflows │ ├── build-on-commit.yaml │ ├── publish-binaries-macos.yaml │ ├── publish-binaries-linux-x64.yaml │ └── publish-binaries-windows.yaml ├── src └── main │ ├── kotlin │ ├── simulation │ │ ├── Agent.kt │ │ ├── Predator.kt │ │ ├── Simulation.kt │ │ └── Boid.kt │ ├── Main.kt │ ├── utils │ │ ├── MathUtils.kt │ │ ├── QuadTree.kt │ │ └── CoroutinedBoidQuadTree.kt │ └── SimulationRenderer.kt │ └── resources │ └── log4j2.yaml ├── README.md ├── gradlew.bat └── gradlew /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'openrndr-boids-simulation' 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | application.log 2 | build 3 | out 4 | .idea 5 | .gradle 6 | ffmpegOutput.txt 7 | video -------------------------------------------------------------------------------- /data/fonts/default.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jorkoh/openrndr-boids-simulation/HEAD/data/fonts/default.otf -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jorkoh/openrndr-boids-simulation/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.file-dialog.properties: -------------------------------------------------------------------------------- 1 | #File dialog properties for Main 2 | #Fri Aug 28 22:33:59 CEST 2020 3 | Main.gui.parameters=C\:\\Users\\Kohru\\IdeaProjects\\GraphicsStuff\\openrndr-boids-simulation\\.\\data\\parameters 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Aug 01 12:32:31 CEST 2019 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /.github/workflows/build-on-commit.yaml: -------------------------------------------------------------------------------- 1 | name: Build on commit 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | runs-on: ubuntu-18.04 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-java@v1 12 | with: 13 | java-version: 14 14 | - name: Build sources 15 | run: ./gradlew build -------------------------------------------------------------------------------- /src/main/kotlin/simulation/Agent.kt: -------------------------------------------------------------------------------- 1 | package simulation 2 | 3 | import utils.Vector2withAngleCache 4 | import org.openrndr.math.Vector2 5 | 6 | interface Agent { 7 | var position: Vector2 8 | var velocity: Vector2withAngleCache 9 | var forces: MutableList 10 | 11 | fun interact(sameSpecies: List, differentSpecies: List) 12 | fun move() { 13 | position += velocity.vector 14 | } 15 | } -------------------------------------------------------------------------------- /.github/workflows/publish-binaries-macos.yaml: -------------------------------------------------------------------------------- 1 | name: Publish macOS binaries 2 | 3 | on: 4 | push: 5 | tags: 6 | - v0.* 7 | - v0.*.* 8 | - v1.* 9 | - v1.*.* 10 | 11 | jobs: 12 | build: 13 | runs-on: macos-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 14 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 14 20 | - name: Build with Gradle 21 | run: ./gradlew jpackageZip 22 | - name: Create Release 23 | uses: ncipollo/release-action@v1.6.1 24 | id: create_release 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | allowUpdates: true 28 | replaceArtifacts: false 29 | body: Fully automated release 30 | artifacts: "./build/distributions/openrndr-application-macos.zip" -------------------------------------------------------------------------------- /.github/workflows/publish-binaries-linux-x64.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Linux/x64 binaries 2 | 3 | on: 4 | push: 5 | tags: 6 | - v0.* 7 | - v0.*.* 8 | - v1.* 9 | - v1.*.* 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 14 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 14 20 | - name: Build with Gradle 21 | run: ./gradlew jpackageZip 22 | - name: Create Release 23 | uses: ncipollo/release-action@v1.6.1 24 | id: create_release 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | allowUpdates: true 28 | replaceArtifacts: false 29 | body: Fully automated release 30 | artifacts: "./build/distributions/openrndr-application-linux-x64.zip" -------------------------------------------------------------------------------- /.github/workflows/publish-binaries-windows.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Windows binaries 2 | 3 | on: 4 | push: 5 | tags: 6 | - v0.* 7 | - v0.*.* 8 | - v1.* 9 | - v1.*.* 10 | - 11 | jobs: 12 | build: 13 | runs-on: windows-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 14 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 14 20 | - name: Build with Gradle 21 | run: ./gradlew jpackageZip 22 | - name: Create Release 23 | uses: ncipollo/release-action@v1.6.1 24 | id: create_release 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | allowUpdates: true 28 | replaceArtifacts: false 29 | body: Fully automated release 30 | artifacts: "./build/distributions/openrndr-application-windows.zip" -------------------------------------------------------------------------------- /src/main/resources/log4j2.yaml: -------------------------------------------------------------------------------- 1 | Configuration: 2 | status: warn 3 | Appenders: 4 | Console: 5 | - name: Console_Info 6 | target: SYSTEM_ERR 7 | PatternLayout: 8 | Pattern: "%highlight{${LOG_LEVEL_PATTERN:-%5p}}{FATAL=red, ERROR=red, WARN=yellow, INFO=green, DEBUG=green, TRACE=green} %style{[%t]}{white} %style{%-30.30c{1.}}{white} %style{ ↘ %m%n%ex}{white}" 9 | #Pattern: "%style{%d{yyyy-MM-dd HH:mm:ss.SSS}}{white} %highlight{${LOG_LEVEL_PATTERN:-%5p}}{FATAL=red, ERROR=red, WARN=yellow, INFO=green, DEBUG=green, TRACE=green} %style{[%t]}{white} %style{%-30.30c{1.}}{cyan} %style{:%m%n%ex}{white}" 10 | File: 11 | append: false 12 | name: File_Appender 13 | fileName: application.log 14 | PatternLayout: 15 | Pattern: "%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" 16 | Loggers: 17 | Root: 18 | level: info 19 | AppenderRef: 20 | - ref: Console_Info 21 | - ref: File_Appender -------------------------------------------------------------------------------- /src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import org.openrndr.application 2 | import org.openrndr.extra.gui.GUI 3 | import simulation.Simulation 4 | import simulation.Simulation.Settings.AREA_HEIGHT 5 | import simulation.Simulation.Settings.AREA_WIDTH 6 | 7 | fun main() = application { 8 | configure { 9 | width = AREA_WIDTH.toInt() 10 | height = AREA_HEIGHT.toInt() 11 | windowResizable = false 12 | } 13 | 14 | program { 15 | // (Optional) for performance checks 16 | // var secondsLastPrint = 0.0 17 | 18 | // Install a gui for changing settings on the fly 19 | extend(GUI().apply { 20 | compartmentsCollapsedByDefault = false 21 | add(Simulation.Settings, "Simulation settings") 22 | add(SimulationRenderer.Settings, "Rendering settings") 23 | }) 24 | 25 | // Initialize the simulation renderer 26 | SimulationRenderer.init(this) 27 | 28 | // Add a mouse listener to highlight specific agents 29 | mouse.clicked.listen { mouseEvent -> 30 | if (Simulation.selectedAgent == null) { 31 | Simulation.selectedAgent = Simulation.getClosestAgent(mouseEvent.position) 32 | } else { 33 | Simulation.selectedAgent = null 34 | } 35 | } 36 | 37 | // (Optional) Install a screen recorder to get a video 38 | // extend(ScreenRecorder().apply { frameRate = 60 }) 39 | 40 | // Install the rendering loop 41 | extend { 42 | Simulation.update() 43 | SimulationRenderer.activeComposition.draw(drawer) 44 | 45 | // (Optional) for performance checks 46 | // if (frameCount % 375 == 0) { 47 | // println("FPS: ${375 / (seconds - secondsLastPrint)}") 48 | // secondsLastPrint = seconds 49 | // } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boids simulation with OPENRNDR 2 | 3 | [![Demo on YouTube](https://i.imgur.com/d6VM9fb.png)](https://www.youtube.com/watch?v=6SdYMsDuIJg "Demo on YouTube") 4 | 5 | Classic [boids simulation](https://www.red3d.com/cwr/boids/) where each agent is only aware of other agents in close proximity (range shown at 0:40) and follows the three basic rules first defined by Craig Reynolds: 6 | 7 | - Separation, steer to avoid crowding local agents 8 | - Alignment, steer towards the average heading of local agents 9 | - Cohesion, steer to move towards the average position (center of mass) of local agents 10 | 11 | Aditionally the agents avoid walls, and predators (on their vision range). The predators simply avoid walls, other predators and chase the closest boid (on their vision range). 12 | 13 | Performance is improved by using a [**quadtree**](https://en.wikipedia.org/wiki/Quadtree) for [spatial partitioning](https://gameprogrammingpatterns.com/spatial-partition.html) (shown at 0:40). This way we don't need to check the distance of a boid against every other boid to determine which ones are in his interaction range. The data structure dynamically organizes the objects by their positions and the query is much more efficient. The quadtree divides the space aiming to keep a maximum amount of agents on each region, in the video the regions turn red as they reach max capacity. 14 | 15 | Aditionally **coroutines** are used to parallelize the calculation of the interactions between the agents and the updates of the quadtree. Finally the angle of the velocity vector of the agents is cached since it needs to be used few times each update. 16 | 17 | The simulation is completely independent from the rendering so it's easy to run the simulation headless or change the graphics layer to other engine. Currently using [OPENRNDR](https://github.com/openrndr/openrndr) to draw the boids and it has worked pretty well. 18 | 19 | Note: the bucket size of the quadtree shown on the demo is not the most optimal but the subdivision of the quadtree is more visible with smaller bucket sizes like the one used. 20 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/kotlin/utils/MathUtils.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import org.openrndr.math.Vector2 4 | import java.lang.Math.toDegrees 5 | import java.lang.Math.toRadians 6 | import kotlin.math.atan2 7 | import kotlin.math.cos 8 | import kotlin.math.sin 9 | import kotlin.math.sqrt 10 | 11 | // TODO way too many radians/utils.angle transformations, it's cheap but not free 12 | // TODO floats are not used (because openrndr uses Double) but could be used 13 | fun Vector2.Companion.unitWithAngle(angle: Double): Vector2 { 14 | val theta = toRadians(angle) 15 | return Vector2(cos(theta), sin(theta)) 16 | } 17 | 18 | fun Vector2.crs(secondVector: Vector2) = this.x * secondVector.y - this.y * secondVector.x 19 | 20 | fun Vector2.angle() = toDegrees(atan2(y, x)) 21 | 22 | fun Vector2.setAngle(newAngle: Double): Vector2 { 23 | val radians = toRadians(newAngle) 24 | return Vector2(length * cos(radians), length * sin(radians)) 25 | } 26 | 27 | fun Vector2.setLength(newLength: Double): Vector2 { 28 | return this * sqrt(newLength * newLength / squaredLength) 29 | } 30 | 31 | // https://math.stackexchange.com/a/1649850 32 | fun Double.angleDifference(secondAngle: Double): Double { 33 | return (this - secondAngle + 540) % 360 - 180 34 | } 35 | 36 | fun Vector2.angleDifference(secondVector: Vector2) = 37 | toDegrees(atan2(this.crs(secondVector), dot(secondVector))) 38 | 39 | 40 | fun Vector2.clampAngleChange(secondVector: Vector2, maxAngleChange: Double): Vector2 { 41 | val previousVectorAngle = secondVector.angle() 42 | val turnRate = angle().angleDifference(previousVectorAngle) 43 | return when { 44 | turnRate > maxAngleChange -> { 45 | setAngle(previousVectorAngle + maxAngleChange) 46 | } 47 | turnRate < -maxAngleChange -> { 48 | setAngle(previousVectorAngle - maxAngleChange) 49 | } 50 | else -> this 51 | } 52 | } 53 | 54 | fun Vector2withAngleCache.clampAngleChange( 55 | secondVector: Vector2withAngleCache, 56 | maxAngleChange: Double 57 | ): Vector2withAngleCache { 58 | val previousVectorAngle = secondVector.angle 59 | val turnRate = angle.angleDifference(previousVectorAngle) 60 | return when { 61 | turnRate > maxAngleChange -> { 62 | Vector2withAngleCache(vector.setAngle(previousVectorAngle + maxAngleChange)) 63 | } 64 | turnRate < -maxAngleChange -> { 65 | Vector2withAngleCache(vector.setAngle(previousVectorAngle - maxAngleChange)) 66 | } 67 | else -> this 68 | } 69 | } 70 | 71 | fun Vector2.clampLength(min: Double, max: Double): Vector2 { 72 | val squaredMax = max * max 73 | val squaredMin = min * min 74 | 75 | return when { 76 | squaredLength == 0.0 -> this 77 | squaredLength > squaredMax -> this * (sqrt(squaredMax / squaredLength)) 78 | squaredLength < squaredMin -> this * (sqrt(squaredMin / squaredLength)) 79 | else -> this 80 | } 81 | } 82 | 83 | // Goes from ~50 to +70 fps at the cost of making the code ugly 84 | class Vector2withAngleCache { 85 | constructor(x: Double, y: Double) { 86 | this.vector = Vector2(x, y) 87 | } 88 | 89 | constructor(vector: Vector2) { 90 | this.vector = vector 91 | } 92 | 93 | var vector: Vector2 94 | set(value) { 95 | if (field != value) angleCache = null 96 | field = value 97 | } 98 | val x 99 | get() = vector.x 100 | 101 | val y 102 | get() = vector.y 103 | 104 | private var angleCache: Double? = null 105 | val angle: Double 106 | get() { 107 | if (angleCache == null) { 108 | angleCache = vector.angle() 109 | } 110 | // TODO fix this with proper nullability here 111 | return angleCache!! 112 | } 113 | } -------------------------------------------------------------------------------- /src/main/kotlin/simulation/Predator.kt: -------------------------------------------------------------------------------- 1 | package simulation 2 | 3 | import utils.Vector2withAngleCache 4 | import utils.clampAngleChange 5 | import utils.clampLength 6 | import org.openrndr.math.Vector2 7 | import utils.setLength 8 | import simulation.Simulation.Settings.AGENT_SPAWN_MARGIN 9 | import simulation.Simulation.Settings.AREA_HEIGHT 10 | import simulation.Simulation.Settings.AREA_WIDTH 11 | import simulation.Simulation.Settings.BOID_CHASING_FACTOR 12 | import simulation.Simulation.Settings.WALL_AVOIDANCE_FACTOR 13 | import utils.unitWithAngle 14 | import kotlin.math.max 15 | import kotlin.random.Random 16 | 17 | class Predator( 18 | override var position: Vector2, 19 | override var velocity: Vector2withAngleCache, 20 | override var forces: MutableList = mutableListOf() 21 | ) : Agent { 22 | companion object { 23 | const val PERCEPTION_RADIUS = 120.0 24 | const val PERCEPTION_CONE_DEGREES = 360.0 25 | 26 | const val MAX_TURN_RATE = 2.5 27 | const val MINIMUM_SPEED = 1.0 28 | const val MAXIMUM_SPEED = 3.0 29 | 30 | fun createRandomPredator() = Predator( 31 | Vector2( 32 | Random.nextDouble(AGENT_SPAWN_MARGIN, (AREA_WIDTH - AGENT_SPAWN_MARGIN)), 33 | Random.nextDouble(AGENT_SPAWN_MARGIN, (AREA_HEIGHT - AGENT_SPAWN_MARGIN)) 34 | ), 35 | Vector2withAngleCache(Vector2.unitWithAngle(Random.nextDouble(0.0, 360.0)) 36 | .setLength(Random.nextDouble(MINIMUM_SPEED, MAXIMUM_SPEED))) 37 | ) 38 | } 39 | 40 | override fun interact(sameSpecies: List, differentSpecies: List) { 41 | forces.clear() 42 | forces.add(wallAvoidanceForce()) 43 | if (differentSpecies.isNotEmpty()) { 44 | forces.add(boidChasingForce(differentSpecies)) 45 | } 46 | if (sameSpecies.isNotEmpty()) { 47 | forces.add(rivalAvoidanceForce(sameSpecies)) 48 | } 49 | 50 | velocity.vector = calculateNewVelocity() 51 | } 52 | 53 | private fun wallAvoidanceForce(): Vector2 { 54 | var force = Vector2.ZERO 55 | 56 | // Left wall 57 | val dstToLeft = max(position.x, 0.000001) 58 | force += Vector2(1.0, 0.0) * (1 / (dstToLeft * dstToLeft)) * WALL_AVOIDANCE_FACTOR 59 | // Right wall 60 | val dstToRight = max(AREA_WIDTH - position.x, 0.000001) 61 | force += Vector2(-1.0, 0.0) * (1 / (dstToRight * dstToRight)) * WALL_AVOIDANCE_FACTOR 62 | // Bottom wall 63 | val dstToBottom = max(position.y, 0.000001) 64 | force += Vector2(0.0, 1.0) * (1 / (dstToBottom * dstToBottom)) * WALL_AVOIDANCE_FACTOR 65 | // Top wall 66 | val dstToTop = max(AREA_HEIGHT - position.y, 0.000001) 67 | force += Vector2(0.0, -1.0) * (1 / (dstToTop * dstToTop)) * WALL_AVOIDANCE_FACTOR 68 | 69 | return force 70 | } 71 | 72 | private fun boidChasingForce(visibleBoids: List): Vector2 { 73 | var force = Vector2.ZERO 74 | visibleBoids.minBy { boid -> 75 | position.squaredDistanceTo(boid.position) 76 | }?.let { closestBoid -> 77 | force = (closestBoid.position - position).normalized 78 | } 79 | 80 | return force * BOID_CHASING_FACTOR 81 | } 82 | 83 | private fun rivalAvoidanceForce(visiblePredators: List): Vector2 { 84 | var force = Vector2.ZERO 85 | visiblePredators.forEach { predator -> 86 | val positionDifference = position - predator.position 87 | force += positionDifference.normalized() * (1 / positionDifference.squaredLength) 88 | } 89 | return force * Simulation.Settings.RIVAL_AVOIDANCE_FACTOR 90 | } 91 | 92 | private fun calculateNewVelocity(): Vector2 { 93 | var newVelocity = Vector2withAngleCache(velocity.vector.copy()) 94 | // Add the forces 95 | for (force in forces) { 96 | newVelocity.vector += force 97 | } 98 | // Clamp the turn rate 99 | newVelocity = newVelocity.clampAngleChange(velocity, MAX_TURN_RATE) 100 | // Clamp the speed 101 | newVelocity.vector = newVelocity.vector.clampLength(MINIMUM_SPEED, MAXIMUM_SPEED) 102 | 103 | return newVelocity.vector 104 | } 105 | } -------------------------------------------------------------------------------- /data/fonts/license.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION & CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. -------------------------------------------------------------------------------- /src/main/kotlin/simulation/Simulation.kt: -------------------------------------------------------------------------------- 1 | package simulation 2 | 3 | import utils.CoroutinedBoidQuadTree 4 | import utils.QuadTree 5 | import utils.angleDifference 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.launch 8 | import kotlinx.coroutines.runBlocking 9 | import org.openrndr.extra.parameters.DoubleParameter 10 | import org.openrndr.math.Vector2 11 | import org.openrndr.shape.Rectangle 12 | import kotlin.math.abs 13 | 14 | object Simulation { 15 | object Settings { 16 | const val BOIDS_AMOUNT = 1500 17 | const val PREDATOR_AMOUNT = 2 18 | const val AREA_WIDTH = 1600.0 19 | const val AREA_HEIGHT = 900.0 20 | const val AGENT_SPAWN_MARGIN = 100.0 21 | 22 | const val WALL_AVOIDANCE_FACTOR = 5e3 23 | const val PREDATOR_AVOIDANCE_FACTOR = 5e6 24 | const val BOID_CHASING_FACTOR = 1.5 25 | const val RIVAL_AVOIDANCE_FACTOR = 5e6 26 | 27 | @DoubleParameter("separation", 0.0, 2500.0) 28 | var SEPARATION_FACTOR = 250.0 29 | 30 | @DoubleParameter("alignment", 0.0, 5.0) 31 | var ALIGNMENT_FACTOR = 0.75 32 | 33 | @DoubleParameter("cohesion", 0.0, 0.5) 34 | var COHESION_FACTOR = 0.05 35 | } 36 | 37 | // To be fair separating the move tasks by quads to parallelize doesn't really give a performance benefit 38 | // with boids amount <4000. Calculating the interactions in parallel does provide a real benefit 39 | val coroutinedBoidsQuad = CoroutinedBoidQuadTree(Rectangle(0.0, 0.0, Settings.AREA_WIDTH, Settings.AREA_HEIGHT), 64) 40 | 41 | private val agents 42 | get() = boids + predators 43 | val boids = mutableListOf() 44 | val predators = mutableListOf() 45 | 46 | var selectedAgent: Agent? = null 47 | 48 | init { 49 | repeat(Settings.BOIDS_AMOUNT) { 50 | boids.add(Boid.createRandomBoid()) 51 | } 52 | repeat(Settings.PREDATOR_AMOUNT) { 53 | predators.add(Predator.createRandomPredator()) 54 | } 55 | 56 | coroutinedBoidsQuad.addBoids(boids) 57 | } 58 | 59 | fun update() { 60 | coroutinedBoidsQuad.moveBoids(boids) 61 | 62 | runBlocking { 63 | boids.forEach { boid -> 64 | launch(Dispatchers.Default) { 65 | boid.interact( 66 | coroutinedBoidsQuad.visibleToAgent( 67 | boid, 68 | Boid.PERCEPTION_RADIUS, 69 | Boid.PERCEPTION_CONE_DEGREES 70 | ), 71 | predators.visibleToAgent( 72 | boid, 73 | Boid.PERCEPTION_RADIUS, 74 | Boid.PERCEPTION_CONE_DEGREES 75 | ) 76 | ) 77 | } 78 | } 79 | predators.forEach { predator -> 80 | launch(Dispatchers.Default) { 81 | predator.interact( 82 | predators.visibleToAgent( 83 | predator, 84 | Predator.PERCEPTION_RADIUS, 85 | Predator.PERCEPTION_CONE_DEGREES 86 | ), 87 | coroutinedBoidsQuad.visibleToAgent( 88 | predator, 89 | Predator.PERCEPTION_RADIUS, 90 | Predator.PERCEPTION_CONE_DEGREES 91 | ) 92 | ) 93 | } 94 | } 95 | } 96 | 97 | agents.forEach { agent -> agent.move() } 98 | } 99 | 100 | private fun List.visibleToAgent(agent: Agent, perceptionRadius: Double, perceptionConeDegrees: Double) = 101 | filter { otherAgent -> 102 | otherAgent != agent && otherAgent.position.squaredDistanceTo(agent.position) <= perceptionRadius * perceptionRadius 103 | && (perceptionConeDegrees == 360.0 104 | || abs(agent.velocity.vector.angleDifference(otherAgent.position - agent.position)) <= perceptionConeDegrees / 2f) 105 | } 106 | 107 | private fun CoroutinedBoidQuadTree.visibleToAgent( 108 | agent: Agent, 109 | perceptionRadius: Double, 110 | perceptionConeDegrees: Double 111 | ) = 112 | queryRange( 113 | Rectangle( 114 | agent.position.x - perceptionRadius, 115 | agent.position.y - perceptionRadius, 116 | perceptionRadius * 2, 117 | perceptionRadius * 2 118 | ) 119 | ).filter { boid -> 120 | boid != agent && boid.position.squaredDistanceTo(agent.position) <= perceptionRadius * perceptionRadius 121 | && (perceptionConeDegrees == 360.0 122 | || abs(agent.velocity.vector.angleDifference(boid.position - agent.position)) <= perceptionConeDegrees / 2f) 123 | } 124 | 125 | fun getClosestAgent(position: Vector2) = agents.minBy { agent -> agent.position.distanceTo(position) } 126 | } -------------------------------------------------------------------------------- /src/main/kotlin/simulation/Boid.kt: -------------------------------------------------------------------------------- 1 | package simulation 2 | 3 | import utils.Vector2withAngleCache 4 | import utils.clampAngleChange 5 | import utils.clampLength 6 | import org.openrndr.math.Vector2 7 | import utils.setLength 8 | import simulation.Simulation.Settings.AGENT_SPAWN_MARGIN 9 | import simulation.Simulation.Settings.ALIGNMENT_FACTOR 10 | import simulation.Simulation.Settings.AREA_HEIGHT 11 | import simulation.Simulation.Settings.AREA_WIDTH 12 | import simulation.Simulation.Settings.COHESION_FACTOR 13 | import simulation.Simulation.Settings.PREDATOR_AVOIDANCE_FACTOR 14 | import simulation.Simulation.Settings.WALL_AVOIDANCE_FACTOR 15 | import utils.unitWithAngle 16 | import kotlin.math.max 17 | import kotlin.random.Random 18 | 19 | class Boid( 20 | override var position: Vector2, 21 | override var velocity: Vector2withAngleCache, 22 | override var forces: MutableList = mutableListOf() 23 | ) : Agent { 24 | companion object { 25 | const val PERCEPTION_RADIUS = 60.0 26 | const val PERCEPTION_CONE_DEGREES = 360.0 27 | 28 | const val MAX_TURN_RATE = 15.0 29 | const val MINIMUM_SPEED = 1.0 30 | const val MAXIMUM_SPEED = 4.0 31 | 32 | fun createRandomBoid() = Boid( 33 | Vector2( 34 | Random.nextDouble(AGENT_SPAWN_MARGIN, (AREA_WIDTH - AGENT_SPAWN_MARGIN)), 35 | Random.nextDouble(AGENT_SPAWN_MARGIN, (AREA_HEIGHT - AGENT_SPAWN_MARGIN)) 36 | ), 37 | Vector2withAngleCache(Vector2.unitWithAngle(Random.nextDouble(0.0, 360.0)) 38 | .setLength(Random.nextDouble(MINIMUM_SPEED, MAXIMUM_SPEED))) 39 | ) 40 | } 41 | 42 | var oldPosition = position 43 | 44 | override fun interact(sameSpecies: List, differentSpecies: List) { 45 | forces.clear() 46 | forces.add(wallAvoidanceForce()) 47 | if (sameSpecies.isNotEmpty()) { 48 | forces.add(separationRuleForce(sameSpecies)) 49 | forces.add(alignmentRuleForce(sameSpecies)) 50 | forces.add(cohesionRuleForce(sameSpecies)) 51 | } 52 | forces.add(predatorAvoidanceForce(differentSpecies)) 53 | 54 | velocity.vector = calculateNewVelocity() 55 | } 56 | 57 | private fun wallAvoidanceForce(): Vector2 { 58 | var force = Vector2.ZERO 59 | 60 | // Left wall 61 | val dstToLeft = max(position.x, 0.000001) 62 | force += Vector2(1.0, 0.0) * (1 / (dstToLeft * dstToLeft)) * WALL_AVOIDANCE_FACTOR 63 | // Right wall 64 | val dstToRight = max(AREA_WIDTH - position.x, 0.000001) 65 | force += Vector2(-1.0, 0.0) * (1 / (dstToRight * dstToRight)) * WALL_AVOIDANCE_FACTOR 66 | // Bottom wall 67 | val dstToBottom = max(position.y, 0.000001) 68 | force += Vector2(0.0, 1.0) * (1 / (dstToBottom * dstToBottom)) * WALL_AVOIDANCE_FACTOR 69 | // Top wall 70 | val dstToTop = max(AREA_HEIGHT - position.y, 0.000001) 71 | force += Vector2(0.0, -1.0) * (1 / (dstToTop * dstToTop)) * WALL_AVOIDANCE_FACTOR 72 | 73 | return force 74 | } 75 | 76 | private fun separationRuleForce(visibleBoids: List): Vector2 { 77 | var force = Vector2.ZERO 78 | visibleBoids.forEach { otherAgent -> 79 | val positionDifference = position - otherAgent.position 80 | force += positionDifference.normalized() * (1 / positionDifference.squaredLength) 81 | } 82 | return force * Simulation.Settings.SEPARATION_FACTOR 83 | } 84 | 85 | private fun alignmentRuleForce(visibleBoids: List): Vector2 { 86 | var force = Vector2.ZERO 87 | visibleBoids.forEach { otherAgent -> 88 | force += Vector2.unitWithAngle(otherAgent.velocity.angle) 89 | } 90 | return force.normalized() * ALIGNMENT_FACTOR 91 | } 92 | 93 | private fun cohesionRuleForce(visibleBoids: List): Vector2 { 94 | var force = Vector2.ZERO 95 | visibleBoids.forEach { otherAgent -> 96 | force += otherAgent.position 97 | } 98 | return (force / visibleBoids.size.toDouble() - position) * COHESION_FACTOR 99 | } 100 | 101 | private fun predatorAvoidanceForce(visiblePredators: List): Vector2 { 102 | var force = Vector2.ZERO 103 | visiblePredators.forEach { predator -> 104 | val positionDifference = position - predator.position 105 | force += positionDifference.normalized() * (1 / positionDifference.squaredLength) 106 | } 107 | return force * PREDATOR_AVOIDANCE_FACTOR 108 | } 109 | 110 | private fun calculateNewVelocity(): Vector2 { 111 | var newVelocity = Vector2withAngleCache(velocity.vector.copy()) 112 | // Add the forces 113 | for (force in forces) { 114 | newVelocity.vector += force 115 | } 116 | // Clamp the turn rate 117 | newVelocity = newVelocity.clampAngleChange(velocity, MAX_TURN_RATE) 118 | // Clamp the speed 119 | newVelocity.vector = newVelocity.vector.clampLength(MINIMUM_SPEED, MAXIMUM_SPEED) 120 | 121 | return newVelocity.vector 122 | } 123 | 124 | override fun move() { 125 | oldPosition = position 126 | super.move() 127 | } 128 | } -------------------------------------------------------------------------------- /src/main/kotlin/utils/QuadTree.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import org.openrndr.math.Vector2 4 | import org.openrndr.shape.Rectangle 5 | import org.openrndr.shape.intersects 6 | 7 | class QuadTree(val bounds: Rectangle, val maxRegionCapacity: Int = 64) { 8 | 9 | data class QuadTreeEntry(var position: Vector2, val item: T) 10 | 11 | var children: List> = emptyList() 12 | var entries: MutableList> = mutableListOf() 13 | var size = 0; private set 14 | 15 | fun queryRange(rectangle: Rectangle): List { 16 | return when { 17 | !intersects(bounds, rectangle) -> emptyList() 18 | children.isEmpty() -> entries.filter { it.position in rectangle }.map { it.item } 19 | else -> children.map { it.queryRange(rectangle) }.flatten() 20 | } 21 | } 22 | 23 | fun add(position: Vector2, item: T): Boolean { 24 | return when { 25 | position !in bounds -> false 26 | children.isNotEmpty() -> children.any { it.add(position, item) } 27 | entries.size < maxRegionCapacity -> entries.add(QuadTreeEntry(position, item)) 28 | else -> { 29 | subdivide() 30 | children.any { it.add(position, item) } 31 | } 32 | }.also { addSucceeded -> 33 | if (addSucceeded) { 34 | size++ 35 | } 36 | } 37 | } 38 | 39 | fun remove(position: Vector2, item: T): Boolean { 40 | return when { 41 | position !in bounds -> false 42 | children.isEmpty() -> entries.remove(QuadTreeEntry(position, item)) 43 | else -> { 44 | val removed = children.find { position in it.bounds }?.remove(position, item) ?: false 45 | if (removed) { 46 | if (size - 1 < maxRegionCapacity) { 47 | entries = children.flatMap { it.entries }.toMutableList() 48 | children = emptyList() 49 | } 50 | } 51 | removed 52 | } 53 | }.also { removed -> 54 | if (removed) { 55 | size-- 56 | } 57 | } 58 | } 59 | 60 | private fun subdivide() { 61 | val childWith = bounds.width / 2 62 | val childHeight = bounds.height / 2 63 | children = listOf( 64 | QuadTree(Rectangle(bounds.x, bounds.y, childWith, childHeight), maxRegionCapacity), 65 | QuadTree(Rectangle(bounds.x + childWith, bounds.y, childWith, childHeight), maxRegionCapacity), 66 | QuadTree(Rectangle(bounds.x, bounds.y + childHeight, childWith, childHeight), maxRegionCapacity), 67 | QuadTree(Rectangle(bounds.x + childWith, bounds.y + childHeight, childWith, childHeight), maxRegionCapacity) 68 | ) 69 | entries.forEach { entry -> children.any { child -> child.add(entry.position, entry.item) } } 70 | entries.clear() 71 | } 72 | 73 | enum class MoveResult { 74 | MOVED_INSIDE_QUAD, 75 | CHANGED_QUAD, 76 | NOT_FOUND 77 | } 78 | 79 | fun move(newPosition: Vector2, oldPosition: Vector2, item: T) { 80 | if (moveEntry(newPosition, oldPosition, item) == MoveResult.CHANGED_QUAD) { 81 | // quad change requires removal 82 | remove(oldPosition, item) 83 | } 84 | } 85 | 86 | private fun moveEntry(newPosition: Vector2, oldPosition: Vector2, item: T): MoveResult { 87 | return when { 88 | newPosition !in bounds -> MoveResult.NOT_FOUND 89 | children.isEmpty() -> { 90 | val entry = entries.firstOrNull { it.position == oldPosition } 91 | if (entry != null) { 92 | // It's here so move it 93 | entry.position = newPosition 94 | MoveResult.MOVED_INSIDE_QUAD 95 | } else { 96 | // It's not here but it should be so add it 97 | add(newPosition, item) 98 | // Avoid increasing size twice when we increase sizes retroactively 99 | size-- 100 | MoveResult.CHANGED_QUAD 101 | } 102 | } 103 | else -> { 104 | var resultFinal = MoveResult.NOT_FOUND 105 | for (child in children) { 106 | val result = child.moveEntry(newPosition, oldPosition, item) 107 | if (result != MoveResult.NOT_FOUND) { 108 | resultFinal = result 109 | break 110 | } 111 | } 112 | resultFinal 113 | } 114 | }.also { result -> 115 | if (result == MoveResult.CHANGED_QUAD) { 116 | size++ 117 | } 118 | } 119 | } 120 | 121 | fun contains(point: Vector2): Boolean { 122 | return when { 123 | point !in bounds -> false 124 | children.isEmpty() -> entries.any { it.position == point } 125 | else -> children.any { it.contains(point) } 126 | } 127 | } 128 | 129 | fun clear() { 130 | children = emptyList() 131 | entries.clear() 132 | size = 0 133 | } 134 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/kotlin/utils/CoroutinedBoidQuadTree.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.launch 5 | import kotlinx.coroutines.runBlocking 6 | import kotlinx.coroutines.withContext 7 | import org.openrndr.math.Vector2 8 | import org.openrndr.shape.Rectangle 9 | import simulation.Boid 10 | 11 | class CoroutinedBoidQuadTree(private val bounds: Rectangle, maxRegionCapacity: Int = 64) { 12 | 13 | enum class Task { 14 | MOVE, 15 | REMOVE, 16 | ADD 17 | } 18 | 19 | data class TreeWithTasks( 20 | val tree: QuadTree, 21 | val tasks: MutableList> = mutableListOf() 22 | ) 23 | 24 | private val treesWithTasks: List 25 | 26 | val children 27 | get() = treesWithTasks.map { it.tree } 28 | 29 | init { 30 | val boundsQuadrant = bounds.scaled(0.5, 0.5) 31 | 32 | val quadrantNE = boundsQuadrant.moved(Vector2(boundsQuadrant.width, 0.0)) 33 | val quadrantSE = boundsQuadrant.moved(Vector2(boundsQuadrant.width, boundsQuadrant.height)) 34 | val quadrantSW = boundsQuadrant.moved(Vector2(0.0, boundsQuadrant.height)) 35 | 36 | treesWithTasks = listOf( 37 | TreeWithTasks(QuadTree(boundsQuadrant, maxRegionCapacity)), 38 | TreeWithTasks(QuadTree(quadrantNE, maxRegionCapacity)), 39 | TreeWithTasks(QuadTree(quadrantSE, maxRegionCapacity)), 40 | TreeWithTasks(QuadTree(quadrantSW, maxRegionCapacity)) 41 | ) 42 | } 43 | 44 | fun addBoids(boids: List) { 45 | for (boid in boids) { 46 | // Outside of the root bounds, ignore 47 | if (boid.position !in bounds) continue 48 | 49 | for (treeWithTasks in treesWithTasks) { 50 | if (boid.position in treeWithTasks.tree.bounds) { 51 | treeWithTasks.tasks.add(Pair(Task.ADD, boid)) 52 | break 53 | } 54 | } 55 | } 56 | processTasks() 57 | } 58 | 59 | fun moveBoids(boids: List) { 60 | for (boid in boids) { 61 | // The position hasn't actually changed, ignore 62 | if (boid.position == boid.oldPosition) continue 63 | 64 | val nowInRootTreeBounds = boid.position in bounds 65 | val beforeInRootTreeBounds = boid.oldPosition in bounds 66 | 67 | // Moving while outside of the root bounds, ignore 68 | if (!nowInRootTreeBounds && !beforeInRootTreeBounds) continue 69 | 70 | when { 71 | // Went outside of the root bounds, only need to remove from specific quad 72 | !nowInRootTreeBounds -> { 73 | for (treeWithTasks in treesWithTasks) { 74 | if (boid.oldPosition in treeWithTasks.tree.bounds) { 75 | treeWithTasks.tasks.add(Pair(Task.REMOVE, boid)) 76 | break 77 | } 78 | } 79 | } 80 | // Came from outside of the root bounds, only need to add to specific quad 81 | !beforeInRootTreeBounds -> { 82 | for (treeWithTasks in treesWithTasks) { 83 | if (boid.position in treeWithTasks.tree.bounds) { 84 | treeWithTasks.tasks.add(Pair(Task.ADD, boid)) 85 | break 86 | } 87 | } 88 | } 89 | // Move inside one of the trees or between two of them 90 | else -> { 91 | var removed = false 92 | var added = false 93 | 94 | for (treeWithTasks in treesWithTasks) { 95 | val nowInTreeBounds = boid.position in treeWithTasks.tree.bounds 96 | val oldInTreeBounds = boid.oldPosition in treeWithTasks.tree.bounds 97 | 98 | // Nothing to do with this tree 99 | if (!nowInTreeBounds && !oldInTreeBounds) continue 100 | 101 | when { 102 | nowInTreeBounds && oldInTreeBounds -> { 103 | treeWithTasks.tasks.add(Pair(Task.MOVE, boid)) 104 | removed = true 105 | added = true 106 | } 107 | oldInTreeBounds -> { 108 | treeWithTasks.tasks.add(Pair(Task.REMOVE, boid)) 109 | removed = true 110 | } 111 | nowInTreeBounds -> { 112 | treeWithTasks.tasks.add(Pair(Task.ADD, boid)) 113 | added = true 114 | } 115 | } 116 | 117 | if (removed && added) break 118 | } 119 | } 120 | } 121 | } 122 | processTasks() 123 | } 124 | 125 | private fun processTasks() { 126 | runBlocking { 127 | for (treeWithTask in treesWithTasks) { 128 | launch { 129 | treeWithTask.runTasks() 130 | } 131 | } 132 | } 133 | 134 | // Clear the tasks 135 | for (treeWithTask in treesWithTasks) { 136 | treeWithTask.tasks.clear() 137 | } 138 | } 139 | 140 | private suspend fun TreeWithTasks.runTasks() = withContext(Dispatchers.Default) { 141 | for (task in tasks) { 142 | when (task.first) { 143 | Task.MOVE -> tree.move( 144 | task.second.position, 145 | task.second.oldPosition, 146 | task.second 147 | ) 148 | Task.REMOVE -> tree.remove( 149 | task.second.oldPosition, 150 | task.second 151 | ) 152 | Task.ADD -> tree.add( 153 | task.second.position, 154 | task.second 155 | ) 156 | } 157 | } 158 | } 159 | 160 | fun queryRange(rectangle: Rectangle): List { 161 | return treesWithTasks.map { it.tree }.map { it.queryRange(rectangle) }.flatten() 162 | } 163 | } -------------------------------------------------------------------------------- /src/main/kotlin/SimulationRenderer.kt: -------------------------------------------------------------------------------- 1 | import org.openrndr.Program 2 | import org.openrndr.color.ColorHSVa 3 | import org.openrndr.color.ColorRGBa 4 | import org.openrndr.color.ColorXSLa 5 | import org.openrndr.color.mix 6 | import org.openrndr.draw.Drawer 7 | import org.openrndr.extra.compositor.* 8 | import org.openrndr.extra.fx.blur.FrameBlur 9 | import org.openrndr.extra.fx.blur.GaussianBloom 10 | import org.openrndr.extra.fx.blur.HashBlur 11 | import org.openrndr.extra.fx.color.ChromaticAberration 12 | import org.openrndr.extra.fx.distort.Perturb 13 | import org.openrndr.extra.noise.simplex 14 | import org.openrndr.extra.parameters.OptionParameter 15 | import org.openrndr.math.Matrix44 16 | import org.openrndr.math.transforms.rotateZ 17 | import org.openrndr.math.transforms.scale 18 | import org.openrndr.math.transforms.translate 19 | import org.openrndr.shape.Circle 20 | import org.openrndr.shape.Segment 21 | import org.openrndr.shape.ShapeContour 22 | import org.openrndr.shape.contour 23 | import simulation.Agent 24 | import simulation.Boid 25 | import simulation.Predator 26 | import simulation.Simulation 27 | import utils.QuadTree 28 | import java.lang.Math.PI 29 | import kotlin.math.cos 30 | 31 | object SimulationRenderer { 32 | object Settings { 33 | enum class CompositionType { 34 | Debug, 35 | Night, 36 | Colorful 37 | } 38 | 39 | @OptionParameter("Composition") 40 | var activeCompositionType = CompositionType.Debug 41 | set(value) { 42 | if (value == field) return 43 | activeComposition = when (value) { 44 | CompositionType.Debug -> program.debugComposition() 45 | CompositionType.Night -> program.nightComposition() 46 | CompositionType.Colorful -> program.colorfulComposition() 47 | } 48 | field = value 49 | } 50 | } 51 | 52 | private lateinit var program: Program 53 | 54 | lateinit var activeComposition: Composite 55 | private set 56 | 57 | 58 | fun init(program: Program) { 59 | this.program = program 60 | activeComposition = program.debugComposition() 61 | } 62 | 63 | fun Program.debugComposition() = compose { 64 | draw { 65 | drawer.clear(ColorRGBa.WHITE) 66 | 67 | // Quad tree quads 68 | drawer.fill = null 69 | drawer.stroke = ColorRGBa.BLACK 70 | drawer.strokeWeight = 0.4 71 | Simulation.coroutinedBoidsQuad.children.draw(drawer) 72 | 73 | // Selected agent 74 | Simulation.selectedAgent?.let { agent -> 75 | drawer.stroke = null 76 | drawer.fill = ColorRGBa.GRAY.opacify(0.8) 77 | drawer.strokeWeight = 0.0 78 | when (agent) { 79 | is Boid -> drawer.circle(agent.position, Boid.PERCEPTION_RADIUS) 80 | is Predator -> drawer.circle(agent.position, Predator.PERCEPTION_RADIUS) 81 | } 82 | 83 | drawer.strokeWeight = 4.0 84 | agent.forces.forEachIndexed { index, force -> 85 | drawer.stroke = ColorHSVa(360 * (index / agent.forces.size.toDouble()), 1.0, 1.0).toRGBa() 86 | drawer.lineSegment(agent.position, agent.position + force * 60.0) 87 | } 88 | } 89 | 90 | // Boids 91 | val boidBodies = Simulation.boids.map { boid -> Circle(boid.position, 5.0) } 92 | val boidVelocities = Simulation.boids.map { boid -> 93 | Segment(boid.position, boid.position + boid.velocity.vector * 4.0) 94 | } 95 | drawer.fill = ColorRGBa.BLACK 96 | drawer.stroke = null 97 | drawer.strokeWeight = 0.0 98 | drawer.circles(boidBodies) 99 | 100 | drawer.fill = null 101 | drawer.stroke = ColorRGBa.BLACK 102 | drawer.strokeWeight = 1.0 103 | drawer.segments(boidVelocities) 104 | 105 | // Predators 106 | val predatorBodies = Simulation.predators.map { predator -> Circle(predator.position, 15.0) } 107 | val predatorVelocities = Simulation.predators.map { predator -> 108 | Segment(predator.position, predator.position + predator.velocity.vector * 10.0) 109 | } 110 | drawer.fill = ColorRGBa.BLACK 111 | drawer.stroke = null 112 | drawer.strokeWeight = 0.0 113 | drawer.circles(predatorBodies) 114 | 115 | drawer.fill = null 116 | drawer.stroke = ColorRGBa.BLACK 117 | drawer.strokeWeight = 1.0 118 | drawer.segments(predatorVelocities) 119 | } 120 | } 121 | 122 | private fun List>.draw(drawer: Drawer) { 123 | forEach { child -> 124 | if (child.children.isNotEmpty()) { 125 | child.children.draw(drawer) 126 | } else { 127 | drawer.fill = mix(ColorRGBa.WHITE, ColorRGBa.RED, child.size / child.maxRegionCapacity.toDouble()) 128 | drawer.rectangle(child.bounds) 129 | } 130 | } 131 | } 132 | 133 | fun Program.nightComposition() = compose { 134 | // Background layer 135 | layer { 136 | draw { 137 | drawer.fill = ColorRGBa.WHITE 138 | val resolution = 8 139 | val points = mutableListOf() 140 | for (y in 0 until height / resolution) { 141 | for (x in 0 until width / resolution) { 142 | val xDouble = x.toDouble() 143 | val yDouble = y.toDouble() 144 | 145 | val simplex = simplex(100, xDouble + seconds, yDouble + seconds) 146 | if (simplex > 0.712) { 147 | points.add(Circle(xDouble * resolution, yDouble * resolution, 3.0)) 148 | } 149 | } 150 | } 151 | drawer.circles(points) 152 | } 153 | post(FrameBlur().apply { blend = 0.1 }) 154 | } 155 | // Boids layer 156 | layer { 157 | draw { 158 | drawer.fill = ColorRGBa.WHITE 159 | drawer.stroke = null 160 | val boidShapes = Simulation.boids.map { boid -> 161 | fishShape.toAgentPositionAndRotation(boid) 162 | } 163 | drawer.contours(boidShapes) 164 | } 165 | post(GaussianBloom().apply { sigma = 2.0 }, { gain = cos(seconds * 0.8 * PI) * 2.0 + 2.0 }) 166 | post(ChromaticAberration(), { aberrationFactor = cos(seconds * 0.8 * 0.5 * PI) * 4.0 }) 167 | } 168 | // Predators layer 169 | layer { 170 | draw { 171 | drawer.fill = ColorRGBa.RED 172 | drawer.stroke = null 173 | val predatorShapes = Simulation.predators.map { predator -> 174 | sharkShape.toAgentPositionAndRotation(predator) 175 | } 176 | drawer.contours(predatorShapes) 177 | } 178 | post( 179 | GaussianBloom().apply { sigma = 2.0 }, { gain = cos(seconds * 0.8 * PI + PI) * 8.0 + 2.0 }) 180 | } 181 | // Underwater kind of effect 182 | post(Perturb().apply { 183 | scale = 6.0 184 | gain = 0.005 185 | decay = 0.0 186 | }, { phase = (seconds * 0.02) % 4 - 2 }) 187 | } 188 | 189 | fun Program.colorfulComposition() = compose { 190 | layer { 191 | draw { 192 | drawer.stroke = null 193 | Simulation.boids.forEachIndexed { index, boid -> 194 | drawer.fill = ColorXSLa(360 * (index.toDouble() / Simulation.boids.size), 0.85, 0.4, 1.0).toRGBa() 195 | drawer.contour(fishShape.toAgentPositionAndRotation(boid)) 196 | } 197 | } 198 | post(GaussianBloom().apply { gain = 1.0 }) 199 | post(FrameBlur().apply { blend = 0.4 }) 200 | } 201 | layer { 202 | draw { 203 | drawer.fill = ColorRGBa.WHITE 204 | drawer.stroke = null 205 | val predatorShapes = Simulation.predators.map { predator -> 206 | sharkShape.toAgentPositionAndRotation(predator) 207 | } 208 | drawer.contours(predatorShapes) 209 | } 210 | post(GaussianBloom().apply { gain = 7.0 }) 211 | post(HashBlur().apply { radius = 1.0 }) 212 | post(FrameBlur().apply { blend = 0.08 }) 213 | } 214 | } 215 | 216 | private const val fishShapeScape = 6.0 217 | private val fishShape = contour { 218 | moveTo(-1.5, 0.0) 219 | lineTo(0.4, -0.3) 220 | lineTo(0.6, 0.0) 221 | lineTo(0.4, 0.3) 222 | close() 223 | }.transform(Matrix44.scale(fishShapeScape, fishShapeScape, 0.0)) 224 | 225 | // TODO change this to an actual shark shape 226 | private const val sharkShapeScale = 20.0 227 | private val sharkShape = contour { 228 | moveTo(-1.5, 0.0) 229 | lineTo(-0.8, 0.0) 230 | lineTo(0.4, -0.3) 231 | lineTo(0.6, 0.0) 232 | lineTo(0.4, 0.3) 233 | lineTo(-0.8, 0.0) 234 | close() 235 | }.transform(Matrix44.scale(sharkShapeScale, sharkShapeScale, 0.0)) 236 | 237 | private fun ShapeContour.toAgentPositionAndRotation(agent: Agent) = 238 | transform( 239 | Matrix44.translate(agent.position.x, agent.position.y, 0.0) 240 | * Matrix44.rotateZ(agent.velocity.angle) 241 | ) 242 | } --------------------------------------------------------------------------------