forked from andkok/MWS_2021
171 lines
12 KiB
Markdown
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.
|