====== Fluid Simulation ====== **Umgang mit AI:** * Der **Weg ist das Ziel**! * Challenge: * Verwende keine AI für die Logik. * Lasse dir von AI keinen Code schreiben. * Verwende AI höchstens als Tutor, um dir Dinge zu erklären. ===== - Fluid aus Teilchen (A) ===== Als Vorbereitung versuchen wir zuerst, ein 2D-Fluid aus vielen Teilchen zu simulieren, die sich gegenseitig stossen. Die Grundidee hinter diesem - physikalisch nicht sehr realistischen - Fluid ist wie folgt: 1. **Spawning:** Erzeuge eine möglichst grosse Anzahl an Teilchen, alle mit identischem Radius und Masse. Visualisiert werden sie als kleine, einfarbige Kreise. Spawne diese z.B. an Zufallspositionen mit zufälligem Anfangsgeschwindigkeitsvektor. 1. **Collision Detection:** Ermittle, welche Teilchen mit den Wänden und v.a. miteinander kollidieren, also aktuell überlappen. Für diesen Schritt werden wir verschiedene Möglichkeiten anschauen. 1. **Collision Handling:** Kümmere dich um alle Teilchen, die aktuell gerade kollidieren. Zum Beispiel muss man dafür die neuen Geschwindigkeitsvektoren nach der Kollision berechnen. Für einen gewissen Realismus sollten die Teilchen bei jeder Kollision einen gewissen Anteil ihrer Energie oder Geschwindigkeit verlieren. ==== Auftrag A - Teil I ==== 1. Erstelle ein **GitHub-Repo** für alle Projekte in diesem Modul. Nenne es z.B. `FluidSimulationsUnity` und wähle die passenden Einstellungen (private, gitignore-File). Clone es auf deinen Computer. Arbeite von nun an darin. **Add, commit, push selbständig und regelmässig**.\\ \\ 1. **Installiere Unity-Hub** und darin eine aktuelle Unity-Version.\\ \\ 1. Erstelle nun ein **neues Unity Projekt** vom Typ *Core: Universal 2D* (z.B. `FluidParticles`).\\ \\ 1. Implementiere nun das **Template-Projekt** (siehe unten), welche als Ausgangslage dient. Wird das Projekt ausgeführt, sollte ein farbiges Teilchen erscheinen, welches sich nach oben bewegt. Studiere den Code genau und stelle sicher, dass du diesen verstehst.\\ \\ 1. Passe den Code nun so an, dass eine **grössere Anzahl Teilchen** (sagen wir ca. $100$) erzeugt werden. Diese sollen an Zufallspositionen gespawned werden. Dafür werden zwei Arrays benötigt: Eines für die ParticleModels und eines für die ParticleViews.\\ \\ 1. Implementiere eine **erste Dynamik:** 1. Jedes Particle soll eine zufällige Anfangsgeschwindigkeit haben (versch. Richtungen und Speed) und sich entsprechend bewegen. 1. An den Wänden sollen sie ohne Energieverlust reflektiert werden. 1. Kollisionen zwischen Teilchen werden noch ignoriert, sie bewegen sich also einfach durch einander hindurch.\\ \\ 1. **Collision Detection V1: Brute Force** 1. Implementiere nun eine erste Version - Brute Force - der Collision Detection: Iteriere über alle Teilchen. Iteriere für jedes Teilchen über alle anderen Teilchen und schaue, ob sie kollidieren. Dafür benötigst du also zwei ineinander verschachtelte Schleifen. 1. Zwei Teilchen kollidieren dann, wenn deren Abstand kleiner ist als $2\times$ der Radius. 1. Alle Teilchen sollen, während sie miteinander kollidieren, eine **andere Farbe** annehmen. Siehe Tipps unten. 1. Für wie viele Teilchen kannst du deine Simulation flüssig laufen lassen? Warum geht nicht mehr? Was ist das Problem mit dieser Art von Collision Detection?\\ \\ === Unity Tipps === == Template-Projekt == 1. **Neues 2D Unity-Projekt** vom Type *Core: Universal 2D*\\ \\ 1. Erstelle unter Assets folgende Unterordner für saubere **Struktur**: Prefabs, Scripts. Ziehe dann später alle Scripts und Prefabs in den entsprechenden Ordner.\\ \\ 1. Erstelle ein neues, leeres GameObject z.B. mit Namen `Simulation`. Hänge diesem das Script `MainScript` (siehe unten) an. Alternativ könnte man das Scipt auch der `MainCamera` anhängen.\\ \\ 1. Füge die Scripts `Particle.cs` und `ParticleView.cs` an. Die Idee ist, dass unser Code einer Trennung von Modell (`Particle`) und Anzeige (`ParticleView.cs`) folgt.\\ \\ 1. **Particle Prefab:** 1. Erstelle in der Hierarchy eine farbige Kugel (2D Objects / Sprites / Circle), Farbe aussuchen. 1. Gib Namen `Particle`. 1. Hänge `ParticleView` Script an. 1. Speichere als Prefab: Einfach aus Hierarchy in Prefab-Ordner ziehen. 1. Lösche wieder aus Hierarchy.\\ \\ 1. Der Code sollte nun wie folgt funktionieren: Führt man den Code aus, so erschein in der Mitte des Bildschirms ein farbige Kugel, die sich nach oben bewegt. ++++Code von Template| **MainScript:** using UnityEngine; public class MainScript : MonoBehaviour { [Header("Simulation settings")] // creates header in Unity Inspector public GameObject prefabParticle; // drag particle prefab on this field in Inspector // and all other game settings // PRIVARTE FIELDS private ParticleModel particleModel; private ParticleView particleView; void Start() { InitParticles(); // create particles } void InitParticles() { Debug.Log("Instantiate Particles"); // Check if prefab is assigned if (prefabParticle == null) { Debug.LogError("prefabParticle is not assigned! Please assign a prefab in the Inspector."); return; } // CREATE A PARTICLE AND ITS VIEW // create model for particle particleModel = new ParticleModel(new Vector2(0, 0),new Vector2(0, 1),1.0f); // create game object for particle with prefab as image, will be added to Unity Hierarchy: GameObject particleGameObject = Instantiate(prefabParticle, particleModel.position, Quaternion.identity); if (particleGameObject != null) // check if instantiation successful { // Try to get ParticleView component, if not found, add it particleView = particleGameObject.GetComponent(); // doesn't work if forgot to assign ParticleView to particle prefab if (particleView == null) // add particle view to script via code { particleView = particleGameObject.AddComponent(); Debug.LogWarning("ParticleView component was missing on prefab, automatically added for particle"); } particleView.Bind(particleModel); } else { Debug.LogError("Failed to instantiate prefab for particle! Make sure prefabParticle is assigned in Inspector."); } } void Update() { Step(); // one simulation step } void Step() { float dt = Time.deltaTime; // ensures that velocity is independent of frame-rate particleModel.position += particleModel.velocity * dt; // change position according to velocity } } **ParticleModel:** using UnityEngine; public class ParticleModel { public Vector2 position; public Vector2 velocity; public float mass; public ParticleModel(Vector2 position, Vector2 velocity, float mass) { this.position = position; this.velocity = velocity; this.mass = mass; } } **ParticleView:** using UnityEngine; public class ParticleView : MonoBehaviour { ParticleModel particleModel; void Start() {} public void Bind(ParticleModel p) { particleModel = p; transform.position = particleModel.position; } void Update() { // position of view = position of model (of ParticleModel) if (particleModel != null) { transform.position = particleModel.position; } } } ++++ == Farbe von Kugel ändern == * Greife auf SpriteRenderer von `particleView`-Objekt zu und ändere seine Farbe: var spriteRenderer = particleView.GetComponent(); spriteRenderer.color = Color.blue; * Tipp 1: Damit man nicht für jedes Frame für jede particleView den `GetComponent()`-Befehl wiederholt aufrufen muss, lohnt es sich, ein Array `particleRenderers` zu erstellen, in dem man die spriteRenderer für die entsprechenden Particles speichert. \\ \\ * Tipp 2: Passe gegebenenfalls den Code so an, dass die Farbzuweisung `... = Color.blue` nur dann ausgeführt wird, wenn sich die Farbe des Teilchens auch *ändert*. Verwende dazu z.B. zwei Flags (bool) `wasColliding` (ob kollidierte im letzten Frame) und `isColliding`. Jetzt wird nur die Farbe neu gesetzt, falls sich der Zustand von `wasColliding` zu `isColliding` geändert hat. == Grösse von GameObject im Code ändern == ``` someGameObject.transform.localScale = Vector3.one * 0.1f; ``` == Zufallszahlen == UnityEngine.Random.InitState(42); // Zufallszahlen mit Seed (immer gleicher 'Zufall') UnityEngine.Random.Range(0,10); ++++Lösungen| using UnityEngine; public class MainScript : MonoBehaviour { [Header("Simulation settings")] // creates header in Unity Inspector public GameObject prefabParticle; // drag particle prefab on this field in Inspector // and all other game settings // PRIVARTE FIELDS private float xMin = -9; private float xMax = 9; private float yMin = -5; private float yMax = 5; private float vCompMax = 5; private float particleDiameter = 0.5f; private int particleCount = 10; // make public later s.t. can adjust in Inspector private ParticleModel[] particleModels; private ParticleView[] particleViews; private SpriteRenderer[] particleRenderers; float minDistanceSquared; private float particleRadius; private Color colorNoCollision = Color.green; private Color colorCollision = Color.red; void Start() { UnityEngine.Random.InitState(42); // with seed InitParticles(); // create particles } void InitParticles() { Debug.Log("Instantiate Particles"); minDistanceSquared = particleDiameter * particleDiameter; particleRadius = particleDiameter / 2; // Check if prefab is assigned if (prefabParticle == null) { Debug.LogError("prefabParticle is not assigned! Please assign a prefab in the Inspector."); return; } // Create new arrays for particle models and views particleModels = new ParticleModel[particleCount]; particleViews = new ParticleView[particleCount]; particleRenderers = new SpriteRenderer[particleCount]; // CREATE A PARTICLE AND ITS VIEW, THEN ASSIGN TO ARRAY for (int i = 0; i < particleCount; i++) { // create model for particle Vector2 position = new Vector2(UnityEngine.Random.Range(xMin + particleDiameter, xMax - particleDiameter), UnityEngine.Random.Range(yMin + particleDiameter, yMax - particleDiameter)); Vector2 velocity = new Vector2(UnityEngine.Random.Range(-vCompMax, vCompMax), UnityEngine.Random.Range(-vCompMax, vCompMax)); ParticleModel particleModel = new ParticleModel(position, velocity, 1.0f); // create game object for particle with prefab as image, will be added to Unity Hierarchy: GameObject particleGameObject = Instantiate(prefabParticle, particleModel.position, Quaternion.identity); particleGameObject.transform.localScale = Vector3.one * particleDiameter; if (particleGameObject != null) { // Try to get ParticleView component, if not found, add it ParticleView particleView = particleGameObject.GetComponent(); if (particleView == null) { particleView = particleGameObject.AddComponent(); Debug.LogWarning("ParticleView component was missing on prefab, automatically added for particle"); } particleView.Bind(particleModel); // assign particle model, view and renderer to array particleModels[i] = particleModel; particleViews[i] = particleView; particleRenderers[i] = particleView.GetComponent(); } else { Debug.LogError("Failed to instantiate prefab for particle! Make sure prefabParticle is assigned in Inspector."); } } // Set correct color for (int i = 0; i < particleCount; i++) { particleRenderers[i].color = colorNoCollision; } } void Update() { Step(); // one simulation step } void Step() { // UPDATE POSITION float dt = Time.deltaTime; // ensures that velocity is independent of frame-rate foreach (var particleModel in particleModels) { particleModel.position += particleModel.velocity * dt; } for (int i = 0; i < particleCount; i++) { particleModels[i].wasColliding = particleModels[i].isColliding; particleModels[i].isColliding = false; } // COLLISION DETECTION for (int i = 0; i < particleCount; i++) { ParticleModel particleModel1 = particleModels[i]; for (int j = i + 1; j < particleCount; j++) { if (i == j) continue; ParticleModel particleModel2 = particleModels[j]; Vector2 delta = particleModel1.position - particleModel2.position; float distSq = delta.sqrMagnitude; if (distSq < minDistanceSquared) { particleModel1.isColliding = true; particleModel2.isColliding = true; } } } // CHANGE COLOR IF COLLIDE for (int i = 0; i < particleCount; i++) { if (particleModels[i].isColliding && !particleModels[i].wasColliding) { particleRenderers[i].color = colorCollision; } else if (!particleModels[i].isColliding && particleModels[i].wasColliding) { particleRenderers[i].color = colorNoCollision; } } // COLLISION HANDLING: WALLS ONLY for (int i = 0; i < particleCount; i++) { ParticleModel particle = particleModels[i]; if (particle.position.x < xMin + particleRadius) { particle.position.x = xMin + particleRadius; particle.velocity.x = -particle.velocity.x; } else if (particle.position.x > xMax - particleRadius) { particle.position.x = xMax - particleRadius; particle.velocity.x = -particle.velocity.x; } if (particle.position.y < yMin + particleRadius) { particle.position.y = yMin + particleRadius; particle.velocity.y = -particle.velocity.y; } else if (particle.position.y > yMax - particleRadius) { particle.position.y = yMax - particleRadius; particle.velocity.y = -particle.velocity.y; } } } } using UnityEngine; public class ParticleModel { public Vector2 position; public Vector2 velocity; public float mass; public bool isColliding = false; // is colliding in current frame public bool wasColliding = false; // was colliding in last frame public ParticleModel(Vector2 position, Vector2 velocity, float mass) { this.position = position; this.velocity = velocity; this.mass = mass; } } using UnityEngine; public class ParticleView : MonoBehaviour { ParticleModel particleModel; void Start() {} public void Bind(ParticleModel p) { particleModel = p; transform.position = particleModel.position; } void Update() { // position of view = position of model (of ParticleModel) if (particleModel != null) { transform.position = particleModel.position; } } } ++++ ==== Auftrag A - Teil II ==== Wahrscheinlich hast du gemerkt, dass die Brute Force Methode für die Particle Collision Detection nicht sehr effizient ist, da man den Abstand von jedem Teilchen zu jedem anderen Teilchen berechnet, selbst wenn diese Teilchen sehr weit auseinander liegen. Hat man beispielsweise $1000$ Teilchen, so sind für diese Methode Rechenschritte in der Grössenordnung von $1000^2 =1’000’000$ notwendig. Dies wollen wir drastisch verbessern, indem wir **Spatial Hashing** für die Collision Detection verwenden. Auf dem MacBook eures Lehrers läuft die Simulation mit $5000$ Teilchens wie folgt: * Brute Force: $4-5$ FPS * Spatial Hashing: knapp $60$ FPS Implementiere nun **Spatial Hashing**, um die Collision Detection zu optimieren: 1. Der Bildschirm wird gleichmässig **unterteilt in Zellen**. Als Zellenbreite eignet sich der Teilchenradius. 1. Jeder Zelle wird ein (möglichst) **eindeutiger Hash** (key) zugewiesen. 1. Es wird ein **Dictionary** angelegt. Die Hashes dienen als key. Die Values sind Listen, die alle Particles beinhalten, die in der entsprechenden Zelle liegen. 1. Anstelle dass man ein bestimmtes Particle mit allen anderen vergleicht, vergleicht man es nur mit denjenigen in der gleichen und **angrenzenden Zellen**. Dadurch skaliert der Code nur noch mit $O(n)$! 1. Dazu ermittelt man zuerst die Hashes dieser (insg. $9$) Zellen. 1. Dann liest man die entsprechenden Particles aus dem Dictionary aus. 1. Nachdem man alle Zellen updated hat, **aktualisiert** man das Dictionary. === Unity Tipps === == Dictionaries == using System.Collections.Generic; // required for dictionary Dictionary> myDict = new Dictionary>(); // create empty dict myDict.clear(); // clear the dictionary myDict.Add(someObject); // add something to dict myDict.ContainsKey(someKey); // checks if myDict contains a certain key ++++Lösungen| // using static Unity.Mathematics.math; using UnityEngine; using System.Collections.Generic; // required for dictionary public enum CollisionDetectionType{ BruteForce, SpatialHashingDict } public class MainScript : MonoBehaviour { [Header("Simulation settings")] // creates header in Unity Inspector public GameObject prefabParticle; // drag particle prefab on this field in Inspector // and all other game settings // PRIVARTE FIELDS // screen boarders private float xMin = -9; private float xMax = 9; private float yMin = -5; private float yMax = 5; // particles private bool giveParticlesInitVel = true; private float vCompMax = 2.5f; private float particleDiameter = 0.05f; // 0.05f private float particleInitSep = 0.01f; // 0.01f private int particleCount = 5000; // 500, make public later s.t. can adjust in Inspector private float widthToHeightRatioInitState = 2.0f; // radio of rectangle in which particles are placed initially private float particleInitSepRandRange = 0.0f; // should be smaller than particleInitSep / 2 to avoid overlap // spatial hashing stuff private CollisionDetectionType collisionDetectionType = CollisionDetectionType.SpatialHashingDict; Dictionary> particleModelDict = new Dictionary>(); private float gridSize; // should equal diameter of largest particle // fields used in code below private ParticleModel[] particleModels; private ParticleView[] particleViews; private SpriteRenderer[] particleRenderers; float minDistanceSquared; private float particleRadius; private Color colorNoCollision = Color.green; private Color colorCollision = Color.red; void Start() { UnityEngine.Random.InitState(42); // with seed InitParticles(); // create particles } void InitParticles() { Debug.Log("Instantiate Particles"); Debug.Log("Collision Detection Type: " + collisionDetectionType); // some calcs minDistanceSquared = particleDiameter * particleDiameter; particleRadius = particleDiameter / 2; gridSize = particleDiameter; // for spatial hashing // Check if prefab is assigned if (prefabParticle == null) { Debug.LogError("prefabParticle is not assigned! Please assign a prefab in the Inspector."); return; } // Create new arrays for particle models and views particleModels = new ParticleModel[particleCount]; particleViews = new ParticleView[particleCount]; particleRenderers = new SpriteRenderer[particleCount]; // Setup stuff for inital positions int nx = Mathf.FloorToInt(Mathf.Sqrt((float)particleCount) * Mathf.Sqrt((float)widthToHeightRatioInitState)); int ny = Mathf.CeilToInt(Mathf.Sqrt((float)particleCount) / Mathf.Sqrt((float)widthToHeightRatioInitState)); if (nx * ny < particleCount) { Debug.Log("initial positions wrongly calculated!"); // throw; // new Exception("initial positions wrongly calculated!"); } // CREATE A PARTICLE AND ITS VIEW, THEN ASSIGN TO ARRAY for (int i = 0; i < particleCount; i++) { // create model for particle int ix = i % nx; int iy = i / nx; float x = -nx / 2 * (particleRadius + particleInitSep) + ix * (particleRadius + particleInitSep); float y = ny / 2 * (particleRadius + particleInitSep) - iy * (particleRadius + particleInitSep); Vector2 position = new Vector2(UnityEngine.Random.Range(x - particleInitSepRandRange, x + particleInitSepRandRange),UnityEngine.Random.Range(y - particleInitSepRandRange, y + particleInitSepRandRange)); Vector2 velocity; // initial velocity if (giveParticlesInitVel) { velocity = new Vector2(UnityEngine.Random.Range(-vCompMax, vCompMax), UnityEngine.Random.Range(-vCompMax, vCompMax)); } else { velocity = new Vector2(0, 0); } ParticleModel particleModel = new ParticleModel(position, velocity, 1.0f); // create game object for particle with prefab as image, will be added to Unity Hierarchy: GameObject particleGameObject = Instantiate(prefabParticle, particleModel.position, Quaternion.identity); particleGameObject.transform.localScale = Vector3.one * particleDiameter; if (particleGameObject != null) { // Try to get ParticleView component, if not found, add it ParticleView particleView = particleGameObject.GetComponent(); if (particleView == null) { particleView = particleGameObject.AddComponent(); Debug.LogWarning("ParticleView component was missing on prefab, automatically added for particle"); } particleView.Bind(particleModel); // assign particle model, view and renderer to array particleModels[i] = particleModel; particleViews[i] = particleView; particleRenderers[i] = particleView.GetComponent(); } else { Debug.LogError("Failed to instantiate prefab for particle! Make sure prefabParticle is assigned in Inspector."); } } // Set correct color for (int i = 0; i < particleCount; i++) { particleRenderers[i].color = colorNoCollision; } } void Update() { Step(); // one simulation step } void CollisionDetectionBruteForce() { for (int i = 0; i < particleCount; i++) { ParticleModel particleModel1 = particleModels[i]; for (int j = i + 1; j < particleCount; j++) { if (i == j) continue; ParticleModel particleModel2 = particleModels[j]; Vector2 delta = particleModel1.position - particleModel2.position; float distSq = delta.sqrMagnitude; if (distSq < minDistanceSquared) { particleModel1.isColliding = true; particleModel2.isColliding = true; } } } } // SPATIAL HASING METHODS int GetGridCellCoord(float x) { return Mathf.FloorToInt(x / gridSize); } int CalculateCellHash(int xCell, int yCell) { return xCell * 73856093 ^ yCell * 19349663; } void CollisionDetectionHashTableDict(){ // BUILD PARTICLE DICT // clear dict particleModelDict.Clear(); // iterate over all particles, determine hash and add to dict foreach (var particleModel in particleModels) { int xCell = GetGridCellCoord(particleModel.position.x); int yCell = GetGridCellCoord(particleModel.position.y); int hash = CalculateCellHash(xCell, yCell); if (!particleModelDict.ContainsKey(hash)) { particleModelDict[hash] = new List(); } particleModelDict[hash].Add(particleModel); } // CHECK FOR COLLISIONS foreach (var particleModel in particleModels) { foreach (var neighbour in GetNeighbors(particleModel.position)) { if (particleModel == neighbour) continue; float distanceSquared = (particleModel.position - neighbour.position).sqrMagnitude; if(distanceSquared < minDistanceSquared) { particleModel.isColliding = true; neighbour.isColliding = true; } } } } // get all particles in same and eight neighboring cells // two options: same method once as IEnumerable and once returning a list. Same performance. IEnumerable GetNeighbors(Vector2 position) { int xCell = GetGridCellCoord(position.x); int yCell = GetGridCellCoord(position.y); for (int dx = -1; dx <= 1; dx++) { int x = xCell + dx; for (int dy = -1; dy <= 1; dy++) { int y = yCell + dy; int hash = CalculateCellHash(x, y); if (!particleModelDict.ContainsKey(hash)) continue; foreach (var particle in particleModelDict[hash]) { yield return particle; } } } } List GetNeighborsList(Vector2 position) { List neigbours = new List(); int xCell = GetGridCellCoord(position.x); int yCell = GetGridCellCoord(position.y); for (int dx = -1; dx <= 1; dx++) { int x = xCell + dx; for (int dy = -1; dy <= 1; dy++) { int y = yCell + dy; int hash = CalculateCellHash(x, y); if (!particleModelDict.ContainsKey(hash)) continue; foreach (var particle in particleModelDict[hash]) { neigbours.Add(particle); } } } return neigbours; } void Step() { // UPDATE POSITION float dt = Time.deltaTime; // ensures that velocity is independent of frame-rate foreach (var particleModel in particleModels) { particleModel.position += particleModel.velocity * dt; } for (int i = 0; i < particleCount; i++) { particleModels[i].wasColliding = particleModels[i].isColliding; particleModels[i].isColliding = false; } // COLLISION DETECTION if (collisionDetectionType == CollisionDetectionType.BruteForce){ CollisionDetectionBruteForce(); } else if (collisionDetectionType == CollisionDetectionType.SpatialHashingDict) { CollisionDetectionHashTableDict(); } else{ Debug.Log("ERROR no valid collision detection selected"); } // CHANGE COLOR IF COLLIDE for (int i = 0; i < particleCount; i++) { if (particleModels[i].isColliding && !particleModels[i].wasColliding) { particleRenderers[i].color = colorCollision; } else if (!particleModels[i].isColliding && particleModels[i].wasColliding) { particleRenderers[i].color = colorNoCollision; } } // COLLISION HANDLING: WALLS ONLY for (int i = 0; i < particleCount; i++) { ParticleModel particle = particleModels[i]; if (particle.position.x < xMin + particleRadius) { particle.position.x = xMin + particleRadius; particle.velocity.x = -particle.velocity.x; } else if (particle.position.x > xMax - particleRadius) { particle.position.x = xMax - particleRadius; particle.velocity.x = -particle.velocity.x; } if (particle.position.y < yMin + particleRadius) { particle.position.y = yMin + particleRadius; particle.velocity.y = -particle.velocity.y; } else if (particle.position.y > yMax - particleRadius) { particle.position.y = yMax - particleRadius; particle.velocity.y = -particle.velocity.y; } } } } ++++ ==== Auftrag A - Teil III ==== Formel für elastischen Stoss in 2D: [[https://sca.ksr.ch/lib/exe/fetch.php?media=talit:collisions.pdf|Slides Collision]]