MWS_2021/Treść zadań/cw7.md
2021-05-28 23:21:02 +02:00

171 lines
12 KiB
Markdown

# Dynamiczne rozmieszczenie traw
>#### Ustawienie gęstości detali
>W dalszej części będziemy zajmować się rozwojem trawy z użyciem równań różniczkowych. Dlatego zalecam zmniejszyć liczbę detali w ustawieniach terenu. Wybierz ustawienia terenu i ustaw `Detail Resolution per Patch` na 16 i `Detail Resolution` na 256 lub 128.
>![details](img\2.JPG)
W trakcie tych zajęć skupimy się na dynamicznym modelu rozwoju trawy opartym od równaniach różniczkowych, który pozwoli na stworzenie rozmaitych wzorców wegetacji. Zanim przejdziemy do samego modelu, zapoznamy się z interfacem pozwalającym modyfikować trawę.
## API detali
Trawa jest rozmieszczana na terenia na podstawie map detali. Są to dwuwymiarowe tablice typu `int`, której wartości określają zagęszczenie danego detalu w kolejnych polach polu.
Tablicę ustawia się za pomocą funkcji
```
terrain.terrainData.SetDetailLayer(int xBase, int yBase, int layer, int[,] details)
```
gdzie `xBase` i `yBase` są początkowymi współrzędnymi (domyślnie 0,0), `layer` jest indeksem detalu a `details` to tablica opisująca wystąpienia detalu. Ostatnia może mieć maksymalnie wymiar `terrain.terrainData.detailWidth-xBase` na `terrain.terrainData.detailHeight-yBase`.
### Zadanie
Uzupełnij funkcję `reset` o instrukcje, które ustawią trawę w krzyżyk idący po przekątnej terenu.
## Model rozwoju trawy
Do rozwoju trawy wykorzystamy model reakcji-dyfuzji wzorując się na [pracy](https://agupubs.onlinelibrary.wiley.com/doi/10.1029/2007RG000256). Wykorzystamy układ równań różniczkowych
$$ \frac{\partial w}{\partial t} = D_w\Delta w-w^2r^2+\text{feed}(0.5+w)$$
$$ \frac{\partial r}{\partial t} = D_r\Delta r+r(wr-\text{kill})$$
gdzie $r$ i $w$ oznaczają odpowiednio wegetację (ilość roślin) i zawartość wody. Natomiast parametry $D_w$, $D_r$, kill i feed są parametrami, które będą kontrolować interakcję.
Analitycznie $r$ i $w$ są funkcjami od współrzędnych terenu $x$ i $y$ oraz czasu $t$. Nas nie interesuje rozwiązanie przybliżone, więc współrzędne $x$ i $y$ będą realizowane jako tablica dwuwymiarowa, natomiast krok czasowy będzie dyskretny. Przy takich założeniach możemy zapisać następująco:
$$ \text{newWater}[i,j] = {oldWater}[i,j]+dt*(D_w\Delta(\text{oldWater},i,j)-\text{oldWater}[i,j]^2*\text{oldVegetation}[i,j]^2+\text{feed}(0.5+\text{oldWater}[i,j]))$$
$$ \text{newVegetation}[i,j] = \text{oldVegetation}[i,j]+dt*(D_r\Delta(\text{oldVegetation},i,j)+\text{oldVegetation}[i,j]*(\text{oldWater}[i,j]*\text{oldVegetation}[i,j]-\text{kill}) )$$
gdzie
$\text{newWater}[i,j]$ oznacza poziom wody w następnym kroku we współrzędnych i,j, $\text{oldWater}[i,j]$ oznacza poziom wody w poprzednim kroku we współrzędnych i,j, analogicznie $\text{newVegetation}[i,j]$ i $\text{oldVegetation}[i,j]$. Natomiast $dt$ to krok czasowy, jakaś mała wartość około 0.05.
Pozostaje symbol $\Delta$ oznacza on operator Laplace i oznacza on sumę drugich pochodnych po współrzędnych, czyli w naszym przypadku:
$$\Delta = \frac{\partial^2}{\partial x^2}+\frac{\partial^2}{\partial y^2}.$$
Opisuje on różnicę między wartością funkcji w danym punkcie a jego otoczeniem. Ponieważ nasza przestrzeń jest dyskretna musimy skorzystać z dyskretnego laplaciana, który wygląda następująco
$$ T[i,j] = 0.2*(T[i+1,j]+T[i-1,j]+T[i,j+1]+T[i,j-1])+\\0.05*(T[i+1,j+1]+T[i+1,j-1]+T[i-1,j-1]+T[i-1,j+1])-T[i,j].$$
Można je zobrazować za pomocą ilusracji:
![aa](img/kernel.jpg)
## Implementacja
Do implementacji wykorzystamy compute shader. Compute shader to program wykonywany na karcie graficznej. Służy do wykonywania dużej liczby obliczeń, które można wykonywać współbieżnie. Nasz przypadek nadaje się idealnie.
Utwórz nowy compute shader, nazwij go na przykład `GrassCalculation`. Otwórz go w edytorze, będzie on wyglądać tak
```C
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain
// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
///// Miejsce na parametry przesyłane
RWTexture2D<float4> Result;
//Kernel czyli funkcja wywoływana przy aktywacji shadera
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
// TODO: insert actual code here!
Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}
```
Ten kod zawiera definicję jednej zmiennej i funkcji, która jest kernelem. Kernel przyjmuje jeden argument typu `uint3`, który zawiera identyfikator wywołania. Shader uruchamiany jednocześnie w wielu instancjach z różnymi wartościami tego argumentu. Można je indeksować po 3 wymiarach. W naszym przypadku będziemy indeksować za ich pomocą po tablicach.
Zmienne definiowane globalnie, jak `RWTexture2D<float4> Result`, są parametrami, które przesyła się ze strony CPU. Typy, których nazwy zaczynają się od `RW`, mogą być modyfikowane przez shader. Dzięki temu mogą być wykorzystywane do zapisywania wyników.
Zaczniemy od napisania prostego shadera shadera, który będzie stopniowo obniżał wegetację o 0.01 całym na terenie. Zacznij od usunięcia definicji zmiennej `Result` i ciała funkcji `CSMain`. Tablicę z wegetacja prześlemy jako `RWStructuredBuffer<float>`, zakładamy, że może ona osiągnąć wartości od 0 do 1. Zdefiniuj globalną zmienną `vegetation` typu `RWStructuredBuffer<float>`.
Shadery nie obsługują 2-wymiarowych tablic, więc ją zastąpić tablicą 1-wymiarową o rozmiarze $x*y$ i ręcznie indeksować. Do tego potrzebujemy znać wymiary tablicy, dodaj zmienne `int sizeX` i `int sizeY`, których do tego użyjemy. Tablicę będziemy zapisywać wierszami, jak na obrazku.
![index](img/index.jpg)
Wartość o współrzędnych x,y znajduje się pod indeksem $x+y*\text{sizeY}$. Napisz funkcję `int index(int x,int y)`, która będzie konwertować indeksy z jednego zapisu do drugiego.
Uzupełnij funkcję `CSMain` o instrukcje, które będzie obniżać wegetację we współrzędnych `id.x` i `id.y` wartość obetnij od dołu przez zero a przez jeden od góry z użyciem funkcji `clamp`. Całość powinna wyglądać tak:
```C
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain
// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
///// Miejsce na parametry przesyłane
RWStructuredBuffer<float> vegetation;
int sizeX;
int sizeY;
int index(int x, int y){
return x+y*sizeX;
}
//Kernel czyli funkcja wywoływana przy aktywacji shadera
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
vegetation[index(id.x, id.y)] = clamp(vegetation[index(id.x, id.y)] - 0.01,0,1);
}
```
### Obsługa shadera
Mamy napisany shader, teraz pozostaje przesłać dane,uruchomić go i odczytać wyniki. Skrypt `GrassController` Zawiera szkielet kodu potrzebnego do obsługi shadera. Zanim zaczniemy podepnij `GrassComputation` w inspektorze pod zmienną `computeShader`.
By zainicjalizować zwykłe zmienne wykorzystuje się metody `SetInt` lub `SetFloat` w zależności od typu. Pierwszym jej argumentem jest id danej zmiennej, pozyskuje się ją za pomocą funkcji `Shader.PropertyToID`, która przyjmuje nazwę zmiennej jako argument. Drugim jest wartość tej zmiennej.
Uzupełnij funkcję `initParameters` o inicjalizację zmiennych `sizeX` i `sizeY`. przypisz im wartość atrybutów `sizeX` i `sizeY` (wartości tych zmiennych pokrywają się z rozmiarem mapy detali).
Bufor trzeba najpierw stworzyć po stronie C#, klasa je obsługująca nazywa się `ComputeBuffer`. konstruktor przyjmuje 2 argumenty: liczbę komórek i rozmiar jednego pola. Pierwsza z nich w naszym przypadku wynosi `sizeX*sizeY` a druga `sizeof(float)`. Stworzony bufor zapisz w jako atrybut o nazwie `vegetation`, będzie on potrzebny później, żeby odzyskać dane. Bufor inicjalizuje się za pomocą metody `SetBuffer(int kernelIndex,str Name, ComputeBuffer buffer)` compute shadera. Pierwszy jej argument w naszym przypadku wynosi zero, drugi to nazwa bufora (czyli `vegetation`) a trzeci to bufor.
Uzupełnij funkcję `initComputeShader` o inicjalizację bufora wegetacji.
W metodzie `initBufferValues` stwórz tablicę o wymiarze `sizeX*sizeY` i zapełnij ją losowymi wartościami. Następnie zapisz ją do bufora `vegetation` za pomocą metody `SetData`. usuń Wszystko z metody `reset`.
Teraz należy wywołać shader i odczytać dane. Shader wywołuje się za pomocą metody `Dispatch(int kernelIndex, int threadGroupsX, int threadGroupsY, int threadGroupsZ)`, która jako pierwszy argument przyjmuje numer kernela (czyli 0). Natomiast pozostałe to liczba wątków jakie wywołą w danym wymiarze. Te liczby odpowiadają zakresowi jaki przyjmie wektor `id` w `CSMain`. W naszym przypadku te wartości mają odpowiadać wymiarom tablicy, czyli odpowiednio `sizeX`, `sizeY` i `1`.
Uzupełnij funkcję `doSimulationSteps(int steps)` o wywołanie shader `steps` razy w pętli.
Odczytanie danych wykonuje się za pomocą metody `GetData` bufora. Jako argument należy podać tablicę, w której dane zostaną umieszczone. W metodzie `recoverData` utwórz tablicę typu `float` o nazwie `result` o wymiarze `sizeX*sizeY` i pobierz do niej wartości wegetacji.
Pozostaje zinterpretować wyniki i wykorzystać je do rysowania trawy. Wciąż wewnątrz `recoverData` utwórz tablicę typu `int` o wymiarach `sizeX` na `sizeY`. W niej umieść podłogę z wyniku z `result` pomnożonego przez 10.
### Dyfuzja
Teraz zmodyfikujemy compute shader tak, żeby zachodziłą dyfuzja trawy w terenie z użyciem operatora Laplace. Operator odwołuje się do sąsiednich komórek, to może powodować wyjście poza tablicę przy indeksowaniu. Musimy obsłużyć to w jakiś sposób. napisz funkcję `float get(StructuredBuffer<float> buffer, int x, int y)`, która zwróci wartość tablicy jeżeli `index(x,y)` jest pomiędzy 0 a `sizeX*sizeY` i zero w przeciwnym wypadku. Następnie wykorzystaj ją, żeby napisać funkcję `float laplace(StructuredBuffer<float> buffer, int x, int y)`, która obliczy laplacian dla punkty x,y.
Dyfuzje można opisać wzorem
$$\frac{\partial v}{\partial t}=\Delta v.$$
W wersji dyskretnej będzie to:
$$\text{newV[i,j]} = \text{oldV}[i,j] + \Delta(\text{oldV},i,j).$$
Należy dodać jeszcze jeden `RWStructuredBuffer<float>`, w którym będziemy przechowywać poprzedni krok, nazwij go `oldVegetation`. Obliczenia będziemy wykonywać na danych z niego i zapisywać w `vegetation`. to pozwoli uniknąć problemów z synchronizacją bufora. Zaimplementuj powyższy wzór.
Zainicjalizuj go po stronie C# analogicznie co bufor `vegetation`. Zapisz go jako atrybut o nazwie . W `initBufferValues` przypisz mu te same wartości co buforowi `vegetation`.
Zaimplementuj funkcję `swapBuffers`, w której zamień ze sobą bufory `vegetation` i `oldVegetation` i wywołaj dla nich ponownie `computeShader.SetBuffer`. W `doSimulationSteps` dodaj wywołanie `swapBuffers` przed wywołaniem w pętli shadera.
### Interakcja z terenem
W tej chwili trawa rośnie niezależnie od tekstury terenu i jego wysokości, powoduje to, że mamy trawę wyrastającą na drodze albo na szczycie góry. By zniwelować ten problem można uzależnić parametry `feed` i `kill` od terenu.
### Zadanie
W tym celu stwórz dwa bufory typu `float` o nazwie `feedModifier` i `killModifier` i rozmiarze `sizeX*sizeY`. Następnie zapełnij je wartościami. Uzależnić `killModifier` od rodzaju tekstury.
Po stronie shadera odczytaj wartość wartości `feedModifier` i `killModifier` dla danej współrzędnej i przemnóż je przez parametry `feed` i `kill` w równaniach.
### Zadanie domowe
Uzależnij `feedModifier` od wysokości terenu. możesz do tego wykorzystać na przykład funkcję `transition` napisaną wcześniej.