Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen der Seite angezeigt.
| Beide Seiten, vorherige Überarbeitung Vorherige Überarbeitung Nächste Überarbeitung | Vorherige Überarbeitung | ||
| talit:fluid_simulation [2025-11-03 12:19] – [Unity Tipps] sca | talit:fluid_simulation [2025-11-16 19:27] (aktuell) – sca | ||
|---|---|---|---|
| Zeile 6: | Zeile 6: | ||
| * Challenge: | * Challenge: | ||
| * Verwende keine AI für die Logik. | * Verwende keine AI für die Logik. | ||
| - | * Lasse dir von AI keinen Code schreiben, verwende | + | * Lasse dir von AI keinen Code schreiben. |
| + | * Verwende | ||
| ===== - Fluid aus Teilchen (A) ===== | ===== - Fluid aus Teilchen (A) ===== | ||
| Zeile 22: | Zeile 23: | ||
| 1. **Installiere Unity-Hub** und darin eine aktuelle Unity-Version.\\ \\ | 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. 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, | + | 1. Implementiere nun das **Template-Projekt** (siehe unten), welche als Ausgangslage dient. Wird das Projekt ausgeführt, |
| 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. 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. Implementiere eine **erste Dynamik:** | ||
| Zeile 28: | Zeile 29: | ||
| 1. An den Wänden sollen sie ohne Energieverlust reflektiert werden. | 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. Kollisionen zwischen Teilchen werden noch ignoriert, sie bewegen sich also einfach durch einander hindurch.\\ \\ | ||
| - | 1. **Collision Detection V1:** | + | 1. **Collision Detection V1: Brute Force** |
| - | 1. Implementiere nun eine erste Version 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. Implementiere nun eine erste Version |
| - | 1. Zwei Teilchen kollidieren dann, wenn deren Abstand kleiner ist als $2\times$ der Radius. Alle Teilchen sollen, während sie miteinander kollidieren, | + | 1. Zwei Teilchen kollidieren dann, wenn deren Abstand kleiner ist als $2\times$ der Radius. |
| - | 1. Was ist das Problem mit dieser Art der Collision Detection? | + | 1. Alle Teilchen sollen, während sie miteinander kollidieren, |
| + | | ||
| === Unity Tipps === | === Unity Tipps === | ||
| Zeile 49: | Zeile 51: | ||
| 1. Der Code sollte nun wie folgt funktionieren: | 1. Der Code sollte nun wie folgt funktionieren: | ||
| - | ++++Code| | + | ++++Code |
| **MainScript: | **MainScript: | ||
| Zeile 176: | Zeile 178: | ||
| </ | </ | ||
| * 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 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. ein Flag (bool) `changeColor`. | + | * 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 == | == Grösse von GameObject im Code ändern == | ||
| Zeile 184: | Zeile 186: | ||
| ``` | ``` | ||
| + | == Zufallszahlen == | ||
| + | |||
| + | <code csharp> | ||
| + | UnityEngine.Random.InitState(42); | ||
| + | UnityEngine.Random.Range(0, | ||
| + | </ | ||
| <nodisp 2> | <nodisp 2> | ||
| Zeile 410: | Zeile 418: | ||
| ++++ | ++++ | ||
| </ | </ | ||
| + | |||
| + | ==== 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 == | ||
| + | |||
| + | <code csharp> | ||
| + | using System.Collections.Generic; | ||
| + | |||
| + | Dictionary< | ||
| + | |||
| + | myDict.clear(); | ||
| + | |||
| + | myDict.Add(someObject); | ||
| + | |||
| + | myDict.ContainsKey(someKey); | ||
| + | </ | ||
| + | |||
| + | <nodisp 2> | ||
| + | |||
| + | ++++Lösungen| | ||
| + | |||
| + | <code csharp> | ||
| + | // using static Unity.Mathematics.math; | ||
| + | using UnityEngine; | ||
| + | using System.Collections.Generic; | ||
| + | |||
| + | public enum CollisionDetectionType{ | ||
| + | BruteForce, | ||
| + | SpatialHashingDict | ||
| + | } | ||
| + | |||
| + | public class MainScript : MonoBehaviour | ||
| + | { | ||
| + | [Header(" | ||
| + | public GameObject prefabParticle; | ||
| + | // 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< | ||
| + | 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); | ||
| + | InitParticles(); | ||
| + | } | ||
| + | |||
| + | void InitParticles() | ||
| + | { | ||
| + | Debug.Log(" | ||
| + | Debug.Log(" | ||
| + | |||
| + | // some calcs | ||
| + | minDistanceSquared = particleDiameter * particleDiameter; | ||
| + | particleRadius = particleDiameter / 2; | ||
| + | gridSize = particleDiameter; | ||
| + | |||
| + | // Check if prefab is assigned | ||
| + | if (prefabParticle == null) | ||
| + | { | ||
| + | Debug.LogError(" | ||
| + | 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(" | ||
| + | // throw; // new Exception(" | ||
| + | } | ||
| + | | ||
| + | // CREATE A PARTICLE AND ITS VIEW, THEN ASSIGN TO ARRAY | ||
| + | for (int i = 0; i < particleCount; | ||
| + | { | ||
| + | // 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, | ||
| + | |||
| + | Vector2 velocity; | ||
| + | // initial velocity | ||
| + | if (giveParticlesInitVel) | ||
| + | { | ||
| + | velocity = new Vector2(UnityEngine.Random.Range(-vCompMax, | ||
| + | } | ||
| + | else | ||
| + | { | ||
| + | velocity = new Vector2(0, 0); | ||
| + | } | ||
| + | |||
| + | ParticleModel particleModel = new ParticleModel(position, | ||
| + | |||
| + | // create game object for particle with prefab as image, will be added to Unity Hierarchy: | ||
| + | GameObject particleGameObject = Instantiate(prefabParticle, | ||
| + | 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.Bind(particleModel); | ||
| + | |||
| + | // assign particle model, view and renderer to array | ||
| + | particleModels[i] = particleModel; | ||
| + | particleViews[i] = particleView; | ||
| + | particleRenderers[i] = particleView.GetComponent< | ||
| + | } | ||
| + | else | ||
| + | { | ||
| + | Debug.LogError(" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // Set correct color | ||
| + | for (int i = 0; i < particleCount; | ||
| + | { | ||
| + | particleRenderers[i].color = colorNoCollision; | ||
| + | } | ||
| + | |||
| + | } | ||
| + | |||
| + | void Update() | ||
| + | { | ||
| + | Step(); // one simulation step | ||
| + | } | ||
| + | |||
| + | void CollisionDetectionBruteForce() | ||
| + | { | ||
| + | for (int i = 0; i < particleCount; | ||
| + | { | ||
| + | ParticleModel particleModel1 = particleModels[i]; | ||
| + | for (int j = i + 1; j < particleCount; | ||
| + | { | ||
| + | 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, | ||
| + | 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< | ||
| + | { | ||
| + | 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, | ||
| + | if (!particleModelDict.ContainsKey(hash)) continue; | ||
| + | foreach (var particle in particleModelDict[hash]) | ||
| + | { | ||
| + | yield return particle; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | } | ||
| + | } | ||
| + | |||
| + | List< | ||
| + | { | ||
| + | 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, | ||
| + | if (!particleModelDict.ContainsKey(hash)) continue; | ||
| + | foreach (var particle in particleModelDict[hash]) | ||
| + | { | ||
| + | neigbours.Add(particle); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | } | ||
| + | return neigbours; | ||
| + | } | ||
| + | | ||
| + | void Step() | ||
| + | { | ||
| + | // UPDATE POSITION | ||
| + | float dt = Time.deltaTime; | ||
| + | foreach (var particleModel in particleModels) | ||
| + | { | ||
| + | particleModel.position += particleModel.velocity * dt; | ||
| + | } | ||
| + | |||
| + | for (int i = 0; i < particleCount; | ||
| + | { | ||
| + | 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(" | ||
| + | } | ||
| + | |||
| + | // CHANGE COLOR IF COLLIDE | ||
| + | for (int i = 0; i < particleCount; | ||
| + | { | ||
| + | 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; | ||
| + | { | ||
| + | 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:// | ||
| + | |||