resc zadan

This commit is contained in:
fingal 2021-05-28 23:21:02 +02:00
parent 37684c42f2
commit fb219706df
34 changed files with 1047 additions and 0 deletions

3
Treść zadań/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"cSpell.language": ",pl"
}

287
Treść zadań/cw6.html Normal file
View File

@ -0,0 +1,287 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<title>cw6</title>
<style>
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
span.underline{text-decoration: underline;}
div.column{display: inline-block; vertical-align: top; width: 50%;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
ul.task-list{list-style: none;}
pre > code.sourceCode { white-space: pre; position: relative; }
pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
pre > code.sourceCode > span:empty { height: 1.2em; }
code.sourceCode > span { color: inherit; text-decoration: inherit; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
pre > code.sourceCode { white-space: pre-wrap; }
pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
}
pre.numberSource code
{ counter-reset: source-line 0; }
pre.numberSource code > span
{ position: relative; left: -4em; counter-increment: source-line; }
pre.numberSource code > span > a:first-child::before
{ content: counter(source-line);
position: relative; left: -1em; text-align: right; vertical-align: baseline;
border: none; display: inline-block;
-webkit-touch-callout: none; -webkit-user-select: none;
-khtml-user-select: none; -moz-user-select: none;
-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
color: #aaaaaa;
}
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
div.sourceCode
{ }
@media screen {
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
}
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code span.at { color: #7d9029; } /* Attribute */
code span.bn { color: #40a070; } /* BaseN */
code span.bu { } /* BuiltIn */
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
code span.ch { color: #4070a0; } /* Char */
code span.cn { color: #880000; } /* Constant */
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
code span.dt { color: #902000; } /* DataType */
code span.dv { color: #40a070; } /* DecVal */
code span.er { color: #ff0000; font-weight: bold; } /* Error */
code span.ex { } /* Extension */
code span.fl { color: #40a070; } /* Float */
code span.fu { color: #06287e; } /* Function */
code span.im { } /* Import */
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
code span.op { color: #666666; } /* Operator */
code span.ot { color: #007020; } /* Other */
code span.pp { color: #bc7a00; } /* Preprocessor */
code span.sc { color: #4070a0; } /* SpecialChar */
code span.ss { color: #bb6688; } /* SpecialString */
code span.st { color: #4070a0; } /* String */
code span.va { color: #19177c; } /* Variable */
code span.vs { color: #4070a0; } /* VerbatimString */
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
</style>
<link rel="stylesheet" href="style.css" />
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js" type="text/javascript"></script>
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
<![endif]-->
</head>
<body>
<h1 id="tworzenie-krajobrazu">Tworzenie krajobrazu</h1>
<p>W trakcie tych zajęć skupimy się na tworzeniu krajobrazów z wykorzystaniem pakietu <em>Terrain Tools</em> i darmowych assetów dostępnych w sklepie unity. W pierwszej części skupimy się na ręcznym kształtowaniu terenu. Z tematem można sie zapoznać także korzystając z licznych tutoriali np <a href="https://youtu.be/smnLYvF40s4">tym</a> lub <a href="https://www.youtube.com/watch?v=ddy12WHqt-M">tym</a>.</p>
<h2 id="przygotowanie">Przygotowanie</h2>
<p>Do odtworzenia wyników z zajęć w nowym projekcie wymagany jest pakietu <em>Terrain Tools</em>, który jest w wersji preview, żeby go pobrać najpierw trzeba ustawić wyświetlanie tych pakietów w menadżerze. Poza tym potrzebne będą assety terenu, można wykorzystać te, które są dostępne jako przykładowe albo znaleźć inne (można na przykład pobrać jakieś darmowe sceny i ukraść z nich assety do celów edukacyjnych) lub zrobić je samemu.</p>
<h2 id="inicjalizacja-terenu">Inicjalizacja terenu</h2>
<p>Obok zakładki <strong>Inspector</strong> powinna być zakładka <strong>Terrain Toolbox</strong>, która służy do zarządzania obiektami terenu. Jeżeli jej tam nie ma, możesz ją znaleźć pod opcją <code>Window &gt; Terrain &gt; Terrain Toolbox</code>. Zaznacz <strong>Create New Terrain</strong>, powinno się pojawić poniższe okno:</p>
<figure>
<img src="img\1.JPG" alt="size" /><figcaption aria-hidden="true">size</figcaption>
</figure>
<p>ustaw wymiary (szerokość wysokość i długość) terenu wedle własnego uznania, domyślnie jest to 500 na 500 na 600, ale dla oszczędzenia mocy obliczeniowej systemu w dalszych punktach zalecam ograniczyć się do 100 na 100 na 200. (Te opcje można też później zmienić w ustawieniach terenu lub globalnie dla wszystkich terenów w <code>Terrain Toolbox &gt; Terrain Settings</code>.) Pozostaje stworzyć teren za pomocą przycisku <strong>Create</strong> na dole okna</p>
<h2 id="opcje">Opcje</h2>
<p>Stworzony teren pojawi się w postaci płaskiej szachownicy w widoku sceny i w hierarchii wewnątrz grupy terenów</p>
<figure>
<img src="img\3.JPG" alt="stworzony teren" /><figcaption aria-hidden="true">stworzony teren</figcaption>
</figure>
<p>Teraz skupimy się na opcjach jakie możliwości oferuje teren jako komponent terenu. Przełącz się na zakładkę <strong>Inspector</strong> i zaznacz teren. W inspektorze możesz zobaczyć szereg opcji jakie teren oferuje pogrupowanych w zakładki.</p>
<figure>
<img src="img\4.JPG" alt="stworzony teren" /><figcaption aria-hidden="true">stworzony teren</figcaption>
</figure>
<p>Interesujące nas części to:</p>
<ul>
<li><strong>Malowanie terenu</strong> służy do rzeźbienia powierzchni terenu i nakładaniu tekstury na terenie.</li>
<li><strong>Rozmieszenie drzew</strong>, jak nazwa wskazuje służy do rozmieszczania drzew.</li>
<li><strong>Rozmieszczenie detali</strong> takich jak trawy, kwiaty polne czy kamienie</li>
</ul>
<h2 id="formowanie-powierzchni">Formowanie Powierzchni</h2>
<p>Kształt terenu jest opisywany przez heightmapę. Czyli maskę, której jasność oznacza wysokość terenu. Ta jest interpretowana przez unity i przetwarzana na mesh. Dzięki temu podejściu projektant nie musi martwić się o geometrię terenu, która jest generowana przez silnik. Wadą jest, że wszelkie wypukłości i lub jaskinie muszą być obsługiwane w inny sposób.</p>
<p>Tworzenie sceny zaczniemy od wyrzeźbienia powierzchni terenu. Przejdź do zakładki <strong>Malowanie terenu</strong>. Pod paskiem z zakładkami jest lista rozwijana, w której można wybrać tryby malowania. Poniżej listy jest pole tekstowe, które opisuje co robi i dana opcja. Pod nią opcje pędzla, przede wszystkim można wybrać jakiego pędzla chce się używać, poza tym można modyfikować jego parametry, czyli rozmiar siłę, nacisku i spacing. Można dodawać swoje własne pędzle. Jeszcze niżej są opcje poszczególnych trybów - istotniejsze są omówione poniżej.</p>
<p>Do formowania terenu interesuje nas pierwsze pięć i ostatnie cztery opcji, są to:</p>
<ul>
<li><p>Sculpt - zbiór narzędzi związanych z rzeźbieniem, należą do nich:</p>
<ul>
<li>Bridge - łączy dwa zaznaczone obszary mostem.</li>
<li>Clone - kopiuje zaznaczony obszar w wybrane miejsce.</li>
<li>Noise - modyfikuje teren z uwzględnieniem tekstury szumu - opcje pozwalają dostosowywać teksturę szumu.</li>
<li>Terrace - tworzy tarasy na wzniesieniach - opcje pozwalają modyfikować liczbę tarasów i ostrość kątów.</li>
</ul></li>
<li><p>Effects - Różnego rodzaju efekty.</p>
<ul>
<li>Contrast - zwiększa rożnicę wysokości - opcje pozwalają ustalić rozmiar cech jakie będą brane pod uwagę.</li>
<li>Sharpen Peaks.</li>
<li>Slope Flatten.</li>
</ul></li>
<li><p>Erosion - różne rodzaje erozji</p></li>
<li><p>Mesh Stamp - pozwala odcisnąć mesh znajdujący się w assetach projektu.</p></li>
<li><p>Raise or Lower Terrain - podnosi lub opuszcza teren, główne narzędzie przy formowaniu.</p></li>
<li><p>Transform - ściskanie, przesuwanie i obracanie.</p></li>
<li><p>Set Height - ustawia teren na wpisanej przez użytkownika wysokości.</p></li>
<li><p>Smooth Height - wygładza teren.</p></li>
</ul>
<p>Poza tym można dodawać filtry na maski modyfikując ich możliwości.</p>
<p>Przydatne skróty:</p>
<ul>
<li><strong>A</strong> Modyfikuje siłę nacisku pędzla.</li>
<li><strong>S</strong> Modyfikuje rozmiar pędzla.</li>
<li><strong>D</strong> Modyfikuje obrót pędzla.</li>
<li><strong>Control+Lewy Przycisk Myszy</strong> Odwraca działanie pędzla lub uruchamia jego alternatywny tryb.</li>
<li><strong>Shift+Lewy Przycisk Myszy</strong> Przełącza na wygładzanie.</li>
</ul>
<blockquote>
<p>Nim zaczniesz modelować teren dodaj jakiś obiekt, który będzie służył za punkt odniesienia, na przykład model postaci albo jakąś figurę geometryczną. Model wykorzystany w trakcie zajęć pochodzi <a href="https://sketchfab.com/3d-models/shrek-3d-model-541d5a5dd9914679919cc1d9a437097e">stąd</a>.</p>
</blockquote>
<h3 id="zadanie">Zadanie</h3>
<p>wybierz jeden z krajobrazów znajdujących się w folderze <code>landscapes</code> i wymodeluj teren nimi inspirowany. Zdjęcia nie będą punktem odniesienia w ocenianiu. służą jedynie jako inspiracja do zrobienia terenu.</p>
<p>Gdy skończysz, przenieś kamerę w miejsce docelowe. Możesz założyć, że scena jest statyczna, to znaczy, kamera się nie porusza. Zaznacz kamerę i użyj <code>Ctrl+Shift+F</code> by ustawić kamerę z widokiem sceny.</p>
<h2 id="teksturowanie-terenu">Teksturowanie terenu</h2>
<blockquote>
<p>Tekstury terenu są jak cebula, mają warstwy</p>
</blockquote>
<p>Na teren nakłada różne tekstury odpowiadające za rożne rodzaje powierzchni, takie podejście pozwala uniknąć monotonii i czyni teren bardziej atrakcyjnym. Osiąga się to za pomocą palety warstw oraz splatmapy, która opisuje z jakimi parametrami silnik ma mieszać warstwy.</p>
<p>By przejść do teksturowania, należy wybrać opcję <strong>Texture Paint</strong> dostępną w rozwijanym menu.</p>
<h3 id="paleta-tekstur">Paleta Tekstur</h3>
<p>Pierwszym krokiem jest stworzenie lub wczytanie palety tekstur, które będą nakładane na teren. Ustawienia palety znajdziemy w zakładce <strong>Layers</strong>.</p>
<figure>
<img src="img\5.JPG" alt="details" /><figcaption aria-hidden="true">details</figcaption>
</figure>
<p>Gotowe warstwy można dodać za pomocą przycisku <strong>Add Layer</strong> a nowe za pomocą <strong>Create</strong>. Jednak tutaj trzeba wziąć pod uwagę, że dodaje się tylko teksturę, która odpowiada za kolor, pozostałe trzeba dodać do warstwy ręcznie.</p>
<p>Warstwa terenu składa się z 3 tekstur:</p>
<ul>
<li>Diffuse - kolor warstwy</li>
<li>Normal - normalna</li>
<li>Mask - zawiera na poszczególnych dodatkowo kanałach informacje standardowo to są:
<ul>
<li>Czerwony - Metallic</li>
<li>Zielony - Occlusion</li>
<li>Niebieski - Height poprawia jakoś mieszania tekstur</li>
<li>Alfa - Smoothness</li>
</ul></li>
</ul>
<p>Nie ma potrzeby, żeby wszystkie tekstury znalazły sie w warstwie, zalecane jest, żeby były przynajmniej dwie pierwsze.</p>
<p>Kolejność tekstur w palecie można swobodnie zmieniać, ale najlepiej ustalić ją na początku. Tekstury są indeksowane w splatmapie po ich kolejności w palecie, więc zmiana jej w trakcie malowania spowoduje nieprzewidziane w efekcie końcowym.</p>
<p>Warstwy podobnie jak materiały można skalować za pomocą opcji tilling, z jednej storny pozwoli to na dopasowanie ich do sceny, z drugiej, stworzenie kilku warstw różniących się tylko tym parametrem może być dobrym sposobem na szybkie urozmaicenie sceny</p>
<h3 id="rysowanie-terenu">Rysowanie terenu</h3>
<p>Pierwsza warstwa od góry jest tą domyślną, więc umieść ją na szczycie. Kolejność pozostałych nie ma znaczenia, póki nie zaczniesz nakładać ich na teren. Tekstury nakładasz wybierając teksturę i pędzel, następnie nakładasz ją na teren kliknięciami i pociągnięciami myszki. Spróbuj różnych ustawień pędzla. Przykładowo zwiększenie wartości <em>Brush Scatter</em>, <em>Brush Spacing</em> i <em>Jitter</em> i użycie nieregularnego pędzla da efekt rozrzucenia plam w losowych miejscach.</p>
<h3 id="zadanie-1">Zadanie</h3>
<p>Utwórz paletę korzystając z dostępnych warstw i tekstur. Następnie wykorzystaj ją do oteksturowania swojego terenu. Dodaj do swojego krajobrazu jakąś ścieżkę i nadaj jej odpowiednie tekstury.</p>
<h2 id="rozmieszczenie-drzew">Rozmieszczenie drzew</h2>
<blockquote>
<p>Zanim zaczniemy dodaj do kamery skrypt <code>Simple Camera Controller</code>, ułatwi on poruszanie się w scenie</p>
</blockquote>
<p>Kolejne narzędzie służy do rozmieszczania drzew i innych większych elementów krajobrazu.</p>
<figure>
<img src="img\6.JPG" alt="trees" /><figcaption aria-hidden="true">trees</figcaption>
</figure>
<p>Nowy rodzaj drzew dodaje się za pomocą <code>Edit Trees &gt; Add Trees</code>, opcje w <strong>Edit Trees</strong> służy także do zmiany modelu drzewa czy usuwaniu rodzaju drzewa.</p>
<p>Poniżej są suwaki odpowiadające za opcje ustawiania drzew. Dwa pierwsze modyfikują rozmiar i gęstość pędzla. kolejne odpowiadają za dostosowanie wielkości drzew. Opcja Lock <strong>Width to Height</strong> uzależnia szerokość drzewa od wysokości. Jej wyłączenie zwiększy różnorodność drzew, ale może prowadzić do niepożądanych efektów, przy źle dobranych parametrach.</p>
<p>Można też umieścić drzewa losowo za pomocą <code>Mass Place Trees</code>, gdzie użytkownik może wybrać ile drzew ma się pojawić, a te zostaną umieszone losowo w scenie.</p>
<h2 id="umieszczanie-detali">Umieszczanie detali</h2>
<p>Kolejna zakładka służy do dodawania detali, jak trawa i rośliny polne. Jest bardzo podobna do tej od drzew. Podobnie za pomocą Edit Details można dodać edytować lub usuwać detale. Dodatkowo jest opcja <code>Add Grass Texture</code>, która pozwala dodać teksturę jako <a href="https://youtu.be/puOTwCrEm7Q">bilboard</a>. Działanie pierwszego suwaka odpowiada za wielkość pędzla. Natomiast trzeciego za maksymalną ma być na tym terenie, a drugi za tępo w jakim tą wielkość się osiągnie. Jakby to porównać do malowania, to <strong>Target Strength</strong> by odpowiadało za siłę nasycenia, ale <strong>Opacity</strong> za siłę krycia.</p>
<figure>
<img src="img\7.JPG" alt="details" /><figcaption aria-hidden="true">details</figcaption>
</figure>
<h3 id="dodawanie-detali">Dodawanie Detali</h3>
<p>Poniżej jest okno jakie się pojawia przy dodawaniu/edytowaniu detalu. Można w nim dostosować rozmiar, żeby zgadzał się z rozmiarem reszty obiektów w scenie. Wartości kolorów odpowiadają za modyfikacje odcienia detali w zależności od położenia, by nadać im większą różnorodność. Warto dopasować wartości kolorów, <strong>Healthy Color</strong> z reguły powinien być biały, natomiast <strong>Dry Color</strong> nie powinnien zbytnio odbiegać od oryginalnego koloru obiektu.</p>
<figure>
<img src="img\8.JPG" alt="details" /><figcaption aria-hidden="true">details</figcaption>
</figure>
<h2 id="dodatkowe-informacje">Dodatkowe informacje</h2>
<h3 id="dodawanie-skał-i-innych-obiektów">Dodawanie Skał i innych obiektów</h3>
<p>Narzędzia terenu same w sobie się nie nadają na tworzenie ostrych skał czy klifów, by je dodać do sceny lepiej dodać je jako osobne obiekty. Można to zrobić za pomocą systemu drzew, co jednak może dać nierealistyczne efekty lub umieścić je samemu ręcznie.</p>
<p>Druga opcja jest preferowana, gdy chcemy wystającą skałę lub klif umieścić blisko kamery lub w centralnym punkcie naszej sceny. Słabym punktem takiego obiektu jest miejsce połączenia z terenem. Można je zamaskować poprzez dodanie w tym miejscu detali, jak trawa i/lub małe kamyki.</p>
<figure>
<img src="img\9.JPG" alt="details" /><figcaption aria-hidden="true">details</figcaption>
</figure>
<h3 id="post-processing">Post-processing</h3>
<p>Post processing to zbiór technik wykorzystywanych wykonywanych na wyrenderowanej klatce. Dzięki nim można poprawić wygląd sceny lub dodać efekty jak mgła, bloom czy flary.</p>
<p>Unity oferuje pakiet, który udostępnia podstawowe efekty postprocessingu o nazwie <strong>PostProcessing</strong>. By go zainicjalizować należy dodać do kamery komponenty <code>Post-Proces Layer</code> i <code>Post-Proces Volume</code>. Następnie dodać dodać nową warstwę w <code>Layer &gt; Add Layer...</code>, nazwij ją <strong>Post Processing</strong>. Na koniec ustawić warstwę na <strong>Post Processing</strong> w kamerze i w <code>Post-Proces Layer</code>.</p>
<figure>
<img src="img\10.JPG" alt="details" /><figcaption aria-hidden="true">details</figcaption>
</figure>
<p>Teraz wystarczy stworzyć profil w <code>Post-Proces Volume</code> i dodać do niego komponenty.</p>
<blockquote>
<p>Ustaw opcję <code>Is Global</code> żeby efekty były widoczne także w widoku sceny</p>
</blockquote>
<h4 id="niektóre-dostępne-obiekty-efekty">Niektóre dostępne obiekty efekty</h4>
<p><strong>Color Grading</strong> - pozwala dostosować kolorystykę sceny, zmodyfikować ekspozycję i kontrast. Do prawidłowego funkcjonowania należy zmienić przestrzeń kolorów na liniową można to zrobić w <code>Edit &gt; Project Settings &gt; Player &gt; Rendering &gt; Color Space</code>.</p>
<p><strong>Ambient Occlusion</strong> - dodaje okluzję otoczenia, czyli zacienienie dla światła rozproszonego wynikającego z geometrii. Zalecane jest wybrać <code>Scalable Ambient Obscurance</code> jako typ.</p>
<p><strong>Vigniette</strong>, <strong>Chromatic Aberration</strong>, <strong>Bloom</strong> - efekty symulujące działanie rzeczywistych kamer zwiększają <em>filmowość</em> obrazu.</p>
<p><strong>Motion Blur</strong> - rozmycie w ruchu, nada więcej realizmu ruchom trawy i gałęzi.</p>
<figure>
<img src="img\11.JPG" alt="details" /><figcaption aria-hidden="true">details</figcaption>
</figure>
<h3 id="zadanie-domowe">Zadanie domowe</h3>
<p>Znajdź w internecie model domku lub innego budynku i zbuduj wokół niego scenę przedstawiającą go na odludziu w otoczeniu przyrody. Wymodeluj teren, dodaj drzewa, trawę i inne detale, wykorzystaj post-processing. Możesz wykorzystać assety znajdujące się w projekcie lub ściągnąć własne. Postaraj się, żeby teren wyglądał estetycznie. Efekt końcowy może być statycznym ujęciem, w takim przypadku umieść kamerę w odpowiednim miejscu.</p>
<h2 id="teskturowanie-w-oparciu-o-ukształtowanie-terenu">Teskturowanie w oparciu o ukształtowanie terenu</h2>
<p>Teksturowanie można częściowo zautomatyzować uzależniając występowanie warstwę od własności terenu, takich jak wysokość czy kąt nachylenia. Przypomnijmy, że to jaka i w jakim stopniu dana warstwa zostanie narysowana jest dyktowane przez splatmapę. Po stronie skryptu jest to tablica trójwymiarowa, której 2 pierwsze współrzędne to szerokość i wysokość w mapie a trzecia jest indeksem warstwy.</p>
<p>Otwórz Scenę <code>Procedural Terrain</code>. Wewnątrz znajduje się teren z podpiętym skryptem <code>Procedural Texture Script</code>, otwórz go w edytorze. Skrypt zawiera funkcję <code>runProcedrualTexturing</code>, w która ma teksturować teren. W tej chwili przypisuje każdemu punktowi losową wartość i wygląda następująco.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode c#"><code class="sourceCode cs"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a> <span class="kw">public</span> <span class="dt">void</span> <span class="fu">runProcedrualTexturing</span>() {</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a> <span class="dt">var</span> terrain = gameObject.<span class="fu">GetComponent</span>&lt;Terrain&gt;();</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a> <span class="dt">var</span> layers = terrain.<span class="fu">terrainData</span>.<span class="fu">alphamapLayers</span>;</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a> <span class="dt">var</span> height = terrain.<span class="fu">terrainData</span>.<span class="fu">alphamapWidth</span>;</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a> <span class="dt">var</span> width = terrain.<span class="fu">terrainData</span>.<span class="fu">alphamapHeight</span>;</span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a> <span class="dt">var</span> newSplatMap = <span class="kw">new</span> <span class="dt">float</span>[width, height, layers];</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a> </span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a> <span class="kw">for</span> (<span class="dt">int</span> i=<span class="dv">0</span>; i &lt; width;i++) {</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a> <span class="kw">for</span> (<span class="dt">int</span> j = <span class="dv">0</span>; j &lt; width; j++) {</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a> <span class="dt">float</span> x = j / (<span class="dt">float</span>)height;</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a> <span class="dt">float</span> y = i / (<span class="dt">float</span>)width;</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a> <span class="dt">var</span> splatWeights = <span class="kw">new</span> <span class="dt">float</span>[layers];</span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a> <span class="kw">for</span> (<span class="dt">int</span> k = <span class="dv">0</span>; k &lt; layers; k++) {</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a> splatWeights[k] = Random.<span class="fu">RandomRange</span>(<span class="fl">0.0f</span>,<span class="fl">1.0f</span>);</span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a> <span class="dt">float</span> sum = splatWeights.<span class="fu">Sum</span>();</span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a> <span class="kw">for</span> (<span class="dt">int</span> k = <span class="dv">0</span>; k &lt; layers; k++) {</span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a> newSplatMap[i,j,k]=splatWeights[k]/sum;</span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a> terrain.<span class="fu">terrainData</span>.<span class="fu">SetAlphamaps</span>(<span class="dv">0</span>, <span class="dv">0</span>, newSplatMap);</span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a> }</span></code></pre></div>
<p>Przeanalizujmy instrukcje funkcji. Pierwsza pobiera komponent terenu. Następne trzy pobierają odpowiednio liczbę warstw, wysokość i szerokość splatmapy. Linia <code>var newSplatMap = new float[width, height, layers];</code> tworzy tablicę, która będzie służyć za nową splatmapę. Następnie w pętli przechodzi po wszystkich indeksach splatmapy.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode c#"><code class="sourceCode cs"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="dt">float</span> x = j / (<span class="dt">float</span>)height;</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="dt">float</span> y = i / (<span class="dt">float</span>)width;</span></code></pre></div>
<p>Powyższe instrukcje dają współrzędne unormowane, które będą przydatne póżniej. Tablica <code>splatWeights</code> będzie przechowywać wagi warstw.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode c#"><code class="sourceCode cs"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="kw">for</span> (<span class="dt">int</span> k = <span class="dv">0</span>; k &lt; layers; k++) {</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a> splatWeights[k] = Random.<span class="fu">RandomRange</span>(<span class="fl">0.0f</span>,<span class="fl">1.0f</span>);</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>Ustawia indeksy na losowe wartości</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode c#"><code class="sourceCode cs"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="dt">float</span> sum = splatWeights.<span class="fu">Sum</span>();</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="kw">for</span> (<span class="dt">int</span> k = <span class="dv">0</span>; k &lt; layers; k++) {</span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a> newSplatMap[i,j,k]=splatWeights[k]/sum;</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>Umieszcza wagi w splatmapie i normalizuje je. Suma wag powinna być równa zero, dlatego dzielimy przez sumę tablicy.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode c#"><code class="sourceCode cs"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>terrain.<span class="fu">terrainData</span>.<span class="fu">SetAlphamaps</span>(<span class="dv">0</span>, <span class="dv">0</span>, newSplatMap);</span></code></pre></div>
<p>Ustawia nową splatmapę</p>
<h3 id="zadanie-2">Zadanie</h3>
<p>Zmodyfikuj funkcję tak, żeby teren, który znajduje się wyżej niż <code>snowHeight</code> był śniegiem a poniżej trawą. Wykorzystaj metodę <code>terrain.terrainData.GetInterpolatedHeight</code> by uzyskać poziom we współrzędnych <code>x</code> i <code>y</code>.</p>
<p>Wykorzystanie zwykłego warunku logicznego da ostre przejście, które wygląda nierealistycznie. Napisz funkcję <code>transition(float start, float end,float value)</code> która:</p>
<ul>
<li>zwróci 0, jeżeli <span class="math inline">\(\text{value}&lt;\text{start}\)</span>;</li>
<li>zwróci 1, jeżeli <span class="math inline">\(\text{value}\ge \text{end}\)</span>;</li>
<li>będzie liniowo interpolować między 0 a 1 jeżeli <span class="math inline">\(\text{end}&gt;\text{value}\ge\text{start}\)</span>,</li>
</ul>
<p>I wykorzystaj ją by zrobić bardziej stopniowe przejście</p>
<h3 id="zadanie-domowe-1">Zadanie domowe</h3>
<p>Wykonaj coś podobnego dla tekstury kamieni, teraz zamiast wysokości terenu wykorzystaj kąt nachylenia, możesz go uzyskać za pomocą metody <code>terrain.terrainData.GetSteepness</code>.</p>
</body>
</html>

279
Treść zadań/cw6.md Normal file
View File

@ -0,0 +1,279 @@
# Tworzenie krajobrazu
W trakcie tych zajęć skupimy się na tworzeniu krajobrazów z wykorzystaniem pakietu *Terrain Tools* i darmowych assetów dostępnych w sklepie unity. W pierwszej części skupimy się na ręcznym kształtowaniu terenu. Z tematem można sie zapoznać także korzystając z licznych tutoriali np [tym](https://youtu.be/smnLYvF40s4) lub [tym](https://www.youtube.com/watch?v=ddy12WHqt-M).
## Przygotowanie
Do odtworzenia wyników z zajęć w nowym projekcie wymagany jest pakietu *Terrain Tools*, który jest w wersji preview, żeby go pobrać najpierw trzeba ustawić wyświetlanie tych pakietów w menadżerze. Poza tym potrzebne będą assety terenu, można wykorzystać te, które są dostępne jako przykładowe albo znaleźć inne (można na przykład pobrać jakieś darmowe sceny i ukraść z nich assety do celów edukacyjnych) lub zrobić je samemu.
## Inicjalizacja terenu
Obok zakładki **Inspector** powinna być zakładka **Terrain Toolbox**, która służy do zarządzania obiektami terenu. Jeżeli jej tam nie ma, możesz ją znaleźć pod opcją `Window > Terrain > Terrain Toolbox`. Zaznacz **Create New Terrain**, powinno się pojawić poniższe okno:
![size](img\1.JPG)
ustaw wymiary (szerokość wysokość i długość) terenu wedle własnego uznania, domyślnie jest to 500 na 500 na 600, ale dla oszczędzenia mocy obliczeniowej systemu w dalszych punktach zalecam ograniczyć się do 100 na 100 na 200. (Te opcje można też później zmienić w ustawieniach terenu lub globalnie dla wszystkich terenów w `Terrain Toolbox > Terrain Settings`.) Pozostaje stworzyć teren za pomocą przycisku **Create** na dole okna
## Opcje
Stworzony teren pojawi się w postaci płaskiej szachownicy w widoku sceny i w hierarchii wewnątrz grupy terenów
![stworzony teren](img\3.JPG)
Teraz skupimy się na opcjach jakie możliwości oferuje teren jako komponent terenu. Przełącz się na zakładkę **Inspector** i zaznacz teren. W inspektorze możesz zobaczyć szereg opcji jakie teren oferuje pogrupowanych w zakładki.
![stworzony teren](img\4.JPG)
Interesujące nas części to:
* **Malowanie terenu** służy do rzeźbienia powierzchni terenu i nakładaniu tekstury na terenie.
* **Rozmieszenie drzew**, jak nazwa wskazuje służy do rozmieszczania drzew.
* **Rozmieszczenie detali** takich jak trawy, kwiaty polne czy kamienie
## Formowanie Powierzchni
Kształt terenu jest opisywany przez heightmapę. Czyli maskę, której jasność oznacza wysokość terenu. Ta jest interpretowana przez unity i przetwarzana na mesh. Dzięki temu podejściu projektant nie musi martwić się o geometrię terenu, która jest generowana przez silnik. Wadą jest, że wszelkie wypukłości i lub jaskinie muszą być obsługiwane w inny sposób.
Tworzenie sceny zaczniemy od wyrzeźbienia powierzchni terenu. Przejdź do zakładki **Malowanie terenu**. Pod paskiem z zakładkami jest lista rozwijana, w której można wybrać tryby malowania. Poniżej listy jest pole tekstowe, które opisuje co robi i dana opcja. Pod nią opcje pędzla, przede wszystkim można wybrać jakiego pędzla chce się używać, poza tym można modyfikować jego parametry, czyli rozmiar siłę, nacisku i spacing. Można dodawać swoje własne pędzle. Jeszcze niżej są opcje poszczególnych trybów - istotniejsze są omówione poniżej.
Do formowania terenu interesuje nas pierwsze pięć i ostatnie cztery opcji, są to:
* Sculpt - zbiór narzędzi związanych z rzeźbieniem, należą do nich:
* Bridge - łączy dwa zaznaczone obszary mostem.
* Clone - kopiuje zaznaczony obszar w wybrane miejsce.
* Noise - modyfikuje teren z uwzględnieniem tekstury szumu - opcje pozwalają dostosowywać teksturę szumu.
* Terrace - tworzy tarasy na wzniesieniach - opcje pozwalają modyfikować liczbę tarasów i ostrość kątów.
* Effects - Różnego rodzaju efekty.
* Contrast - zwiększa rożnicę wysokości - opcje pozwalają ustalić rozmiar cech jakie będą brane pod uwagę.
* Sharpen Peaks.
* Slope Flatten.
* Erosion - różne rodzaje erozji
* Mesh Stamp - pozwala odcisnąć mesh znajdujący się w assetach projektu.
* Raise or Lower Terrain - podnosi lub opuszcza teren, główne narzędzie przy formowaniu.
* Transform - ściskanie, przesuwanie i obracanie.
* Set Height - ustawia teren na wpisanej przez użytkownika wysokości.
* Smooth Height - wygładza teren.
Poza tym można dodawać filtry na maski modyfikując ich możliwości.
Przydatne skróty:
* **A** Modyfikuje siłę nacisku pędzla.
* **S** Modyfikuje rozmiar pędzla.
* **D** Modyfikuje obrót pędzla.
* **Control+Lewy Przycisk Myszy** Odwraca działanie pędzla lub uruchamia jego alternatywny tryb.
* **Shift+Lewy Przycisk Myszy** Przełącza na wygładzanie.
> Nim zaczniesz modelować teren dodaj jakiś obiekt, który będzie służył za punkt odniesienia, na przykład model postaci albo jakąś figurę geometryczną. Model wykorzystany w trakcie zajęć pochodzi [stąd](https://sketchfab.com/3d-models/shrek-3d-model-541d5a5dd9914679919cc1d9a437097e).
### Zadanie
wybierz jeden z krajobrazów znajdujących się w folderze `landscapes` i wymodeluj teren nimi inspirowany. Zdjęcia nie będą punktem odniesienia w ocenianiu. służą jedynie jako inspiracja do zrobienia terenu.
Gdy skończysz, przenieś kamerę w miejsce docelowe. Możesz założyć, że scena jest statyczna, to znaczy, kamera się nie porusza. Zaznacz kamerę i użyj `Ctrl+Shift+F` by ustawić kamerę z widokiem sceny.
## Teksturowanie terenu
> Tekstury terenu są jak cebula, mają warstwy
Na teren nakłada różne tekstury odpowiadające za rożne rodzaje powierzchni, takie podejście pozwala uniknąć monotonii i czyni teren bardziej atrakcyjnym. Osiąga się to za pomocą palety warstw oraz splatmapy, która opisuje z jakimi parametrami silnik ma mieszać warstwy.
By przejść do teksturowania, należy wybrać opcję **Texture Paint** dostępną w rozwijanym menu.
### Paleta Tekstur
Pierwszym krokiem jest stworzenie lub wczytanie palety tekstur, które będą nakładane na teren. Ustawienia palety znajdziemy w zakładce **Layers**.
![details](img\5.JPG)
Gotowe warstwy można dodać za pomocą przycisku **Add Layer** a nowe za pomocą **Create**. Jednak tutaj trzeba wziąć pod uwagę, że dodaje się tylko teksturę, która odpowiada za kolor, pozostałe trzeba dodać do warstwy ręcznie.
Warstwa terenu składa się z 3 tekstur:
* Diffuse - kolor warstwy
* Normal - normalna
* Mask - zawiera na poszczególnych dodatkowo kanałach informacje standardowo to są:
* Czerwony - Metallic
* Zielony - Occlusion
* Niebieski - Height poprawia jakoś mieszania tekstur
* Alfa - Smoothness
Nie ma potrzeby, żeby wszystkie tekstury znalazły sie w warstwie, zalecane jest, żeby były przynajmniej dwie pierwsze.
Kolejność tekstur w palecie można swobodnie zmieniać, ale najlepiej ustalić ją na początku. Tekstury są indeksowane w splatmapie po ich kolejności w palecie, więc zmiana jej w trakcie malowania spowoduje nieprzewidziane w efekcie końcowym.
Warstwy podobnie jak materiały można skalować za pomocą opcji tilling, z jednej storny pozwoli to na dopasowanie ich do sceny, z drugiej, stworzenie kilku warstw różniących się tylko tym parametrem może być dobrym sposobem na szybkie urozmaicenie sceny
### Rysowanie terenu
Pierwsza warstwa od góry jest tą domyślną, więc umieść ją na szczycie. Kolejność pozostałych nie ma znaczenia, póki nie zaczniesz nakładać ich na teren. Tekstury nakładasz wybierając teksturę i pędzel, następnie nakładasz ją na teren kliknięciami i pociągnięciami myszki. Spróbuj różnych ustawień pędzla. Przykładowo zwiększenie wartości *Brush Scatter*, *Brush Spacing* i *Jitter* i użycie nieregularnego pędzla da efekt rozrzucenia plam w losowych miejscach.
### Zadanie
Utwórz paletę korzystając z dostępnych warstw i tekstur. Następnie wykorzystaj ją do oteksturowania swojego terenu. Dodaj do swojego krajobrazu jakąś ścieżkę i nadaj jej odpowiednie tekstury.
## Rozmieszczenie drzew
> Zanim zaczniemy dodaj do kamery skrypt `Simple Camera Controller`, ułatwi on poruszanie się w scenie
Kolejne narzędzie służy do rozmieszczania drzew i innych większych elementów krajobrazu.
![trees](img\6.JPG)
Nowy rodzaj drzew dodaje się za pomocą `Edit Trees > Add Trees`, opcje w **Edit Trees** służy także do zmiany modelu drzewa czy usuwaniu rodzaju drzewa.
Poniżej są suwaki odpowiadające za opcje ustawiania drzew. Dwa pierwsze modyfikują rozmiar i gęstość pędzla. kolejne odpowiadają za dostosowanie wielkości drzew. Opcja Lock **Width to Height** uzależnia szerokość drzewa od wysokości. Jej wyłączenie zwiększy różnorodność drzew, ale może prowadzić do niepożądanych efektów, przy źle dobranych parametrach.
Można też umieścić drzewa losowo za pomocą `Mass Place Trees`, gdzie użytkownik może wybrać ile drzew ma się pojawić, a te zostaną umieszone losowo w scenie.
## Umieszczanie detali
Kolejna zakładka służy do dodawania detali, jak trawa i rośliny polne. Jest bardzo podobna do tej od drzew. Podobnie za pomocą Edit Details można dodać edytować lub usuwać detale. Dodatkowo jest opcja `Add Grass Texture`, która pozwala dodać teksturę jako [bilboard](https://youtu.be/puOTwCrEm7Q). Działanie pierwszego suwaka odpowiada za wielkość pędzla. Natomiast trzeciego za maksymalną ma być na tym terenie, a drugi za tępo w jakim tą wielkość się osiągnie. Jakby to porównać do malowania, to **Target Strength** by odpowiadało za siłę nasycenia, ale **Opacity** za siłę krycia.
![details](img\7.JPG)
### Dodawanie Detali
Poniżej jest okno jakie się pojawia przy dodawaniu/edytowaniu detalu. Można w nim dostosować rozmiar, żeby zgadzał się z rozmiarem reszty obiektów w scenie. Wartości kolorów odpowiadają za modyfikacje odcienia detali w zależności od położenia, by nadać im większą różnorodność. Warto dopasować wartości kolorów, **Healthy Color** z reguły powinien być biały, natomiast **Dry Color** nie powinnien zbytnio odbiegać od oryginalnego koloru obiektu.
![details](img\8.JPG)
## Dodatkowe informacje
### Dodawanie Skał i innych obiektów
Narzędzia terenu same w sobie się nie nadają na tworzenie ostrych skał czy klifów, by je dodać do sceny lepiej dodać je jako osobne obiekty. Można to zrobić za pomocą systemu drzew, co jednak może dać nierealistyczne efekty lub umieścić je samemu ręcznie.
Druga opcja jest preferowana, gdy chcemy wystającą skałę lub klif umieścić blisko kamery lub w centralnym punkcie naszej sceny. Słabym punktem takiego obiektu jest miejsce połączenia z terenem. Można je zamaskować poprzez dodanie w tym miejscu detali, jak trawa i/lub małe kamyki.
![details](img\9.JPG)
### Post-processing
Post processing to zbiór technik wykorzystywanych wykonywanych na wyrenderowanej klatce. Dzięki nim można poprawić wygląd sceny lub dodać efekty jak mgła, bloom czy flary.
Unity oferuje pakiet, który udostępnia podstawowe efekty postprocessingu o nazwie **PostProcessing**. By go zainicjalizować należy dodać do kamery komponenty `Post-Proces Layer` i `Post-Proces Volume`. Następnie dodać dodać nową warstwę w `Layer > Add Layer...`, nazwij ją **Post Processing**. Na koniec ustawić warstwę na **Post Processing** w kamerze i w `Post-Proces Layer`.
![details](img\10.JPG)
Teraz wystarczy stworzyć profil w `Post-Proces Volume` i dodać do niego komponenty.
> Ustaw opcję `Is Global` żeby efekty były widoczne także w widoku sceny
#### Niektóre dostępne obiekty efekty
**Color Grading** - pozwala dostosować kolorystykę sceny, zmodyfikować ekspozycję i kontrast. Do prawidłowego funkcjonowania należy zmienić przestrzeń kolorów na liniową można to zrobić w `Edit > Project Settings > Player > Rendering > Color Space`.
**Ambient Occlusion** - dodaje okluzję otoczenia, czyli zacienienie dla światła rozproszonego wynikającego z geometrii. Zalecane jest wybrać `Scalable Ambient Obscurance` jako typ.
**Vigniette**, **Chromatic Aberration**, **Bloom** - efekty symulujące działanie rzeczywistych kamer zwiększają *filmowość* obrazu.
**Motion Blur** - rozmycie w ruchu, nada więcej realizmu ruchom trawy i gałęzi.
![details](img\11.JPG)
### Zadanie domowe
Znajdź w internecie model domku lub innego budynku i zbuduj wokół niego scenę przedstawiającą go na odludziu w otoczeniu przyrody. Wymodeluj teren, dodaj drzewa, trawę i inne detale, wykorzystaj post-processing. Możesz wykorzystać assety znajdujące się w projekcie lub ściągnąć własne. Postaraj się, żeby teren wyglądał estetycznie. Efekt końcowy może być statycznym ujęciem, w takim przypadku umieść kamerę w odpowiednim miejscu.
## Teskturowanie w oparciu o ukształtowanie terenu
Teksturowanie można częściowo zautomatyzować uzależniając występowanie warstwę od własności terenu, takich jak wysokość czy kąt nachylenia. Przypomnijmy, że to jaka i w jakim stopniu dana warstwa zostanie narysowana jest dyktowane przez splatmapę. Po stronie skryptu jest to tablica trójwymiarowa, której 2 pierwsze współrzędne to szerokość i wysokość w mapie a trzecia jest indeksem warstwy.
Otwórz Scenę `Procedural Terrain`. Wewnątrz znajduje się teren z podpiętym skryptem `Procedural Texture Script`, otwórz go w edytorze. Skrypt zawiera funkcję `runProcedrualTexturing`, w która ma teksturować teren. W tej chwili przypisuje każdemu punktowi losową wartość i wygląda następująco.
```C#
public void runProcedrualTexturing() {
var terrain = gameObject.GetComponent<Terrain>();
var layers = terrain.terrainData.alphamapLayers;
var height = terrain.terrainData.alphamapWidth;
var width = terrain.terrainData.alphamapHeight;
var newSplatMap = new float[width, height, layers];
for (int i=0; i < width;i++) {
for (int j = 0; j < width; j++) {
float x = j / (float)height;
float y = i / (float)width;
var splatWeights = new float[layers];
for (int k = 0; k < layers; k++) {
splatWeights[k] = Random.RandomRange(0.0f,1.0f);
}
float sum = splatWeights.Sum();
for (int k = 0; k < layers; k++) {
newSplatMap[i,j,k]=splatWeights[k]/sum;
}
}
}
terrain.terrainData.SetAlphamaps(0, 0, newSplatMap);
}
```
Przeanalizujmy instrukcje funkcji. Pierwsza pobiera komponent terenu. Następne trzy pobierają odpowiednio liczbę warstw, wysokość i szerokość splatmapy. Linia `var newSplatMap = new float[width, height, layers];` tworzy tablicę, która będzie służyć za nową splatmapę. Następnie w pętli przechodzi po wszystkich indeksach splatmapy.
```C#
float x = j / (float)height;
float y = i / (float)width;
```
Powyższe instrukcje dają współrzędne unormowane, które będą przydatne póżniej. Tablica `splatWeights` będzie przechowywać wagi warstw.
```C#
for (int k = 0; k < layers; k++) {
splatWeights[k] = Random.RandomRange(0.0f,1.0f);
}
```
Ustawia indeksy na losowe wartości
```C#
float sum = splatWeights.Sum();
for (int k = 0; k < layers; k++) {
newSplatMap[i,j,k]=splatWeights[k]/sum;
}
```
Umieszcza wagi w splatmapie i normalizuje je. Suma wag powinna być równa zero, dlatego dzielimy przez sumę tablicy.
```C#
terrain.terrainData.SetAlphamaps(0, 0, newSplatMap);
```
Ustawia nową splatmapę
### Zadanie
Zmodyfikuj funkcję tak, żeby teren, który znajduje się wyżej niż `snowHeight` był śniegiem a poniżej trawą. Wykorzystaj metodę `terrain.terrainData.GetInterpolatedHeight` by uzyskać poziom we współrzędnych `x` i `y`.
Wykorzystanie zwykłego warunku logicznego da ostre przejście, które wygląda nierealistycznie. Napisz funkcję `transition(float start, float end,float value)` która:
* zwróci 0, jeżeli $\text{value}<\text{start}$;
* zwróci 1, jeżeli $\text{value}\ge \text{end}$;
* będzie liniowo interpolować między 0 a 1 jeżeli $\text{end}>\text{value}\ge\text{start}$,
I wykorzystaj ją by zrobić bardziej stopniowe przejście
### Zadanie domowe
Wykonaj coś podobnego dla tekstury kamieni, teraz zamiast wysokości terenu wykorzystaj kąt nachylenia, możesz go uzyskać za pomocą metody `terrain.terrainData.GetSteepness`.

183
Treść zadań/cw7.html Normal file
View File

@ -0,0 +1,183 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<title>cw7</title>
<style>
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
span.underline{text-decoration: underline;}
div.column{display: inline-block; vertical-align: top; width: 50%;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
ul.task-list{list-style: none;}
pre > code.sourceCode { white-space: pre; position: relative; }
pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
pre > code.sourceCode > span:empty { height: 1.2em; }
code.sourceCode > span { color: inherit; text-decoration: inherit; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
pre > code.sourceCode { white-space: pre-wrap; }
pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
}
pre.numberSource code
{ counter-reset: source-line 0; }
pre.numberSource code > span
{ position: relative; left: -4em; counter-increment: source-line; }
pre.numberSource code > span > a:first-child::before
{ content: counter(source-line);
position: relative; left: -1em; text-align: right; vertical-align: baseline;
border: none; display: inline-block;
-webkit-touch-callout: none; -webkit-user-select: none;
-khtml-user-select: none; -moz-user-select: none;
-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
color: #aaaaaa;
}
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
div.sourceCode
{ }
@media screen {
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
}
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code span.at { color: #7d9029; } /* Attribute */
code span.bn { color: #40a070; } /* BaseN */
code span.bu { } /* BuiltIn */
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
code span.ch { color: #4070a0; } /* Char */
code span.cn { color: #880000; } /* Constant */
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
code span.dt { color: #902000; } /* DataType */
code span.dv { color: #40a070; } /* DecVal */
code span.er { color: #ff0000; font-weight: bold; } /* Error */
code span.ex { } /* Extension */
code span.fl { color: #40a070; } /* Float */
code span.fu { color: #06287e; } /* Function */
code span.im { } /* Import */
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
code span.op { color: #666666; } /* Operator */
code span.ot { color: #007020; } /* Other */
code span.pp { color: #bc7a00; } /* Preprocessor */
code span.sc { color: #4070a0; } /* SpecialChar */
code span.ss { color: #bb6688; } /* SpecialString */
code span.st { color: #4070a0; } /* String */
code span.va { color: #19177c; } /* Variable */
code span.vs { color: #4070a0; } /* VerbatimString */
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
</style>
<link rel="stylesheet" href="style.css" />
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js" type="text/javascript"></script>
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
<![endif]-->
</head>
<body>
<h1 id="dynamiczne-rozmieszczenie-traw">Dynamiczne rozmieszczenie traw</h1>
<blockquote>
<h4 id="ustawienie-gęstości-detali">Ustawienie gęstości detali</h4>
<p>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 <code>Detail Resolution per Patch</code> na 16 i <code>Detail Resolution</code> na 256 lub 128. <img src="img\2.JPG" alt="details" /></p>
</blockquote>
<p>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ę.</p>
<h2 id="api-detali">API detali</h2>
<p>Trawa jest rozmieszczana na terenia na podstawie map detali. Są to dwuwymiarowe tablice typu <code>int</code>, której wartości określają zagęszczenie danego detalu w kolejnych polach polu. Tablicę ustawia się za pomocą funkcji <code>terrain.terrainData.SetDetailLayer(int xBase, int yBase, int layer, int[,] details)</code> gdzie <code>xBase</code> i <code>yBase</code> są początkowymi współrzędnymi (domyślnie 0,0), <code>layer</code> jest indeksem detalu a <code>details</code> to tablica opisująca wystąpienia detalu. Ostatnia może mieć maksymalnie wymiar <code>terrain.terrainData.detailWidth-xBase</code> na <code>terrain.terrainData.detailHeight-yBase</code>.</p>
<h3 id="zadanie">Zadanie</h3>
<p>Uzupełnij funkcję <code>reset</code> o instrukcje, które ustawią trawę w krzyżyk idący po przekątnej terenu.</p>
<h2 id="model-rozwoju-trawy">Model rozwoju trawy</h2>
<p>Do rozwoju trawy wykorzystamy model reakcji-dyfuzji wzorując się na <a href="https://agupubs.onlinelibrary.wiley.com/doi/10.1029/2007RG000256">pracy</a>. Wykorzystamy układ równań różniczkowych</p>
<p><span class="math display">\[ \frac{\partial w}{\partial t} = D_w\Delta w-w^2r^2+\text{feed}(0.5+w)\]</span> <span class="math display">\[ \frac{\partial r}{\partial t} = D_r\Delta r+r(wr-\text{kill})\]</span></p>
<p>gdzie <span class="math inline">\(r\)</span> i <span class="math inline">\(w\)</span> oznaczają odpowiednio wegetację (ilość roślin) i zawartość wody. Natomiast parametry <span class="math inline">\(D_w\)</span>, <span class="math inline">\(D_r\)</span>, kill i feed są parametrami, które będą kontrolować interakcję.</p>
<p>Analitycznie <span class="math inline">\(r\)</span> i <span class="math inline">\(w\)</span> są funkcjami od współrzędnych terenu <span class="math inline">\(x\)</span> i <span class="math inline">\(y\)</span> oraz czasu <span class="math inline">\(t\)</span>. Nas nie interesuje rozwiązanie przybliżone, więc współrzędne <span class="math inline">\(x\)</span> i <span class="math inline">\(y\)</span> będą realizowane jako tablica dwuwymiarowa, natomiast krok czasowy będzie dyskretny. Przy takich założeniach możemy zapisać następująco:</p>
<p><span class="math display">\[ \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]))\]</span> <span class="math display">\[ \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}) )\]</span></p>
<p>gdzie <span class="math inline">\(\text{newWater}[i,j]\)</span> oznacza poziom wody w następnym kroku we współrzędnych i,j, <span class="math inline">\(\text{oldWater}[i,j]\)</span> oznacza poziom wody w poprzednim kroku we współrzędnych i,j, analogicznie <span class="math inline">\(\text{newVegetation}[i,j]\)</span> i <span class="math inline">\(\text{oldVegetation}[i,j]\)</span>. Natomiast <span class="math inline">\(dt\)</span> to krok czasowy, jakaś mała wartość około 0.05.</p>
<p>Pozostaje symbol <span class="math inline">\(\Delta\)</span> oznacza on operator Laplace i oznacza on sumę drugich pochodnych po współrzędnych, czyli w naszym przypadku: <span class="math display">\[\Delta = \frac{\partial^2}{\partial x^2}+\frac{\partial^2}{\partial y^2}.\]</span></p>
<p>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</p>
<p><span class="math display">\[ 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].\]</span></p>
<p>Można je zobrazować za pomocą ilusracji: <img src="img/kernel.jpg" alt="aa" /></p>
<h2 id="implementacja">Implementacja</h2>
<p>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.</p>
<p>Utwórz nowy compute shader, nazwij go na przykład <code>GrassCalculation</code>. Otwórz go w edytorze, będzie on wyglądać tak</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode c"><code class="sourceCode c"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="co">// Each #kernel tells which function to compile; you can have many kernels</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="pp">#pragma kernel CSMain</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="co">// Create a RenderTexture with enableRandomWrite flag and set it</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="co">// with cs.SetTexture</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="co">///// Miejsce na parametry przesyłane </span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>RWTexture2D&lt;float4&gt; Result;</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="co">//Kernel czyli funkcja wywoływana przy aktywacji shadera</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>[numthreads(<span class="dv">8</span>,<span class="dv">8</span>,<span class="dv">1</span>)]</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="dt">void</span> CSMain (uint3 id : SV_DispatchThreadID)</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>{</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a> <span class="co">// </span><span class="al">TODO</span><span class="co">: insert actual code here!</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a> Result[id.xy] = float4(id.x &amp; id.y, (id.x &amp; <span class="dv">15</span>)/<span class="fl">15.0</span>, (id.y &amp; <span class="dv">15</span>)/<span class="fl">15.0</span>, <span class="fl">0.0</span>);</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>Ten kod zawiera definicję jednej zmiennej i funkcji, która jest kernelem. Kernel przyjmuje jeden argument typu <code>uint3</code>, 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.</p>
<p>Zmienne definiowane globalnie, jak <code>RWTexture2D&lt;float4&gt; Result</code>, są parametrami, które przesyła się ze strony CPU. Typy, których nazwy zaczynają się od <code>RW</code>, mogą być modyfikowane przez shader. Dzięki temu mogą być wykorzystywane do zapisywania wyników.</p>
<p>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 <code>Result</code> i ciała funkcji <code>CSMain</code>. Tablicę z wegetacja prześlemy jako <code>RWStructuredBuffer&lt;float&gt;</code>, zakładamy, że może ona osiągnąć wartości od 0 do 1. Zdefiniuj globalną zmienną <code>vegetation</code> typu <code>RWStructuredBuffer&lt;float&gt;</code>.</p>
<p>Shadery nie obsługują 2-wymiarowych tablic, więc ją zastąpić tablicą 1-wymiarową o rozmiarze <span class="math inline">\(x*y\)</span> i ręcznie indeksować. Do tego potrzebujemy znać wymiary tablicy, dodaj zmienne <code>int sizeX</code> i <code>int sizeY</code>, których do tego użyjemy. Tablicę będziemy zapisywać wierszami, jak na obrazku.</p>
<figure>
<img src="img/index.jpg" alt="index" /><figcaption aria-hidden="true">index</figcaption>
</figure>
<p>Wartość o współrzędnych x,y znajduje się pod indeksem <span class="math inline">\(x+y*\text{sizeY}\)</span>. Napisz funkcję <code>int index(int x,int y)</code>, która będzie konwertować indeksy z jednego zapisu do drugiego.</p>
<p>Uzupełnij funkcję <code>CSMain</code> o instrukcje, które będzie obniżać wegetację we współrzędnych <code>id.x</code> i <code>id.y</code> wartość obetnij od dołu przez zero a przez jeden od góry z użyciem funkcji <code>clamp</code>. Całość powinna wyglądać tak:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode c"><code class="sourceCode c"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co">// Each #kernel tells which function to compile; you can have many kernels</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="pp">#pragma kernel CSMain</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="co">// Create a RenderTexture with enableRandomWrite flag and set it</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="co">// with cs.SetTexture</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="co">///// Miejsce na parametry przesyłane </span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>RWStructuredBuffer&lt;<span class="dt">float</span>&gt; vegetation;</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a><span class="dt">int</span> sizeX;</span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="dt">int</span> sizeY;</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a><span class="dt">int</span> index(<span class="dt">int</span> x, <span class="dt">int</span> y){</span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> x+y*sizeX;</span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a><span class="co">//Kernel czyli funkcja wywoływana przy aktywacji shadera</span></span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a>[numthreads(<span class="dv">8</span>,<span class="dv">8</span>,<span class="dv">1</span>)]</span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a><span class="dt">void</span> CSMain (uint3 id : SV_DispatchThreadID)</span>
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a>{</span>
<span id="cb2-19"><a href="#cb2-19" aria-hidden="true" tabindex="-1"></a> vegetation[index(id.x, id.y)] = clamp(vegetation[index(id.x, id.y)] - <span class="fl">0.01</span>,<span class="dv">0</span>,<span class="dv">1</span>);</span>
<span id="cb2-20"><a href="#cb2-20" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<h3 id="obsługa-shadera">Obsługa shadera</h3>
<p>Mamy napisany shader, teraz pozostaje przesłać dane,uruchomić go i odczytać wyniki. Skrypt <code>GrassController</code> Zawiera szkielet kodu potrzebnego do obsługi shadera. Zanim zaczniemy podepnij <code>GrassComputation</code> w inspektorze pod zmienną <code>computeShader</code>.</p>
<p>By zainicjalizować zwykłe zmienne wykorzystuje się metody <code>SetInt</code> lub <code>SetFloat</code> w zależności od typu. Pierwszym jej argumentem jest id danej zmiennej, pozyskuje się ją za pomocą funkcji <code>Shader.PropertyToID</code>, która przyjmuje nazwę zmiennej jako argument. Drugim jest wartość tej zmiennej.</p>
<p>Uzupełnij funkcję <code>initParameters</code> o inicjalizację zmiennych <code>sizeX</code> i <code>sizeY</code>. przypisz im wartość atrybutów <code>sizeX</code> i <code>sizeY</code> (wartości tych zmiennych pokrywają się z rozmiarem mapy detali).</p>
<p>Bufor trzeba najpierw stworzyć po stronie C#, klasa je obsługująca nazywa się <code>ComputeBuffer</code>. konstruktor przyjmuje 2 argumenty: liczbę komórek i rozmiar jednego pola. Pierwsza z nich w naszym przypadku wynosi <code>sizeX*sizeY</code> a druga <code>sizeof(float)</code>. Stworzony bufor zapisz w jako atrybut o nazwie <code>vegetation</code>, będzie on potrzebny później, żeby odzyskać dane. Bufor inicjalizuje się za pomocą metody <code>SetBuffer(int kernelIndex,str Name, ComputeBuffer buffer)</code> compute shadera. Pierwszy jej argument w naszym przypadku wynosi zero, drugi to nazwa bufora (czyli <code>vegetation</code>) a trzeci to bufor.</p>
<p>Uzupełnij funkcję <code>initComputeShader</code> o inicjalizację bufora wegetacji.</p>
<p>W metodzie <code>initBufferValues</code> stwórz tablicę o wymiarze <code>sizeX*sizeY</code> i zapełnij ją losowymi wartościami. Następnie zapisz ją do bufora <code>vegetation</code> za pomocą metody <code>SetData</code>. usuń Wszystko z metody <code>reset</code>.</p>
<p>Teraz należy wywołać shader i odczytać dane. Shader wywołuje się za pomocą metody <code>Dispatch(int kernelIndex, int threadGroupsX, int threadGroupsY, int threadGroupsZ)</code>, 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 <code>id</code> w <code>CSMain</code>. W naszym przypadku te wartości mają odpowiadać wymiarom tablicy, czyli odpowiednio <code>sizeX</code>, <code>sizeY</code> i <code>1</code>.</p>
<p>Uzupełnij funkcję <code>doSimulationSteps(int steps)</code> o wywołanie shader <code>steps</code> razy w pętli.</p>
<p>Odczytanie danych wykonuje się za pomocą metody <code>GetData</code> bufora. Jako argument należy podać tablicę, w której dane zostaną umieszczone. W metodzie <code>recoverData</code> utwórz tablicę typu <code>float</code> o nazwie <code>result</code> o wymiarze <code>sizeX*sizeY</code> i pobierz do niej wartości wegetacji.</p>
<p>Pozostaje zinterpretować wyniki i wykorzystać je do rysowania trawy. Wciąż wewnątrz <code>recoverData</code> utwórz tablicę typu <code>int</code> o wymiarach <code>sizeX</code> na <code>sizeY</code>. W niej umieść podłogę z wyniku z <code>result</code> pomnożonego przez 10.</p>
<h3 id="dyfuzja">Dyfuzja</h3>
<p>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ę <code>float get(StructuredBuffer&lt;float&gt; buffer, int x, int y)</code>, która zwróci wartość tablicy jeżeli <code>index(x,y)</code> jest pomiędzy 0 a <code>sizeX*sizeY</code> i zero w przeciwnym wypadku. Następnie wykorzystaj ją, żeby napisać funkcję <code>float laplace(StructuredBuffer&lt;float&gt; buffer, int x, int y)</code>, która obliczy laplacian dla punkty x,y.</p>
<p>Dyfuzje można opisać wzorem</p>
<p><span class="math display">\[\frac{\partial v}{\partial t}=\Delta v.\]</span></p>
<p>W wersji dyskretnej będzie to:</p>
<p><span class="math display">\[\text{newV[i,j]} = \text{oldV}[i,j] + \Delta(\text{oldV},i,j).\]</span></p>
<p>Należy dodać jeszcze jeden <code>RWStructuredBuffer&lt;float&gt;</code>, w którym będziemy przechowywać poprzedni krok, nazwij go <code>oldVegetation</code>. Obliczenia będziemy wykonywać na danych z niego i zapisywać w <code>vegetation</code>. to pozwoli uniknąć problemów z synchronizacją bufora. Zaimplementuj powyższy wzór.</p>
<p>Zainicjalizuj go po stronie C# analogicznie co bufor <code>vegetation</code>. Zapisz go jako atrybut o nazwie . W <code>initBufferValues</code> przypisz mu te same wartości co buforowi <code>vegetation</code>.</p>
<p>Zaimplementuj funkcję <code>swapBuffers</code>, w której zamień ze sobą bufory <code>vegetation</code> i <code>oldVegetation</code> i wywołaj dla nich ponownie <code>computeShader.SetBuffer</code>. W <code>doSimulationSteps</code> dodaj wywołanie <code>swapBuffers</code> przed wywołaniem w pętli shadera.</p>
<h3 id="interakcja-z-terenem">Interakcja z terenem</h3>
<p>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 <code>feed</code> i <code>kill</code> od terenu.</p>
<h3 id="zadanie-1">Zadanie</h3>
<p>W tym celu stwórz dwa bufory typu <code>float</code> o nazwie <code>feedModifier</code> i <code>killModifier</code> i rozmiarze <code>sizeX*sizeY</code>. Następnie zapełnij je wartościami. Uzależnić <code>killModifier</code> od rodzaju tekstury.</p>
<p>Po stronie shadera odczytaj wartość wartości <code>feedModifier</code> i <code>killModifier</code> dla danej współrzędnej i przemnóż je przez parametry <code>feed</code> i <code>kill</code> w równaniach.</p>
<h3 id="zadanie-domowe">Zadanie domowe</h3>
<p>Uzależnij <code>feedModifier</code> od wysokości terenu. możesz do tego wykorzystać na przykład funkcję <code>transition</code> napisaną wcześniej.</p>
</body>
</html>

170
Treść zadań/cw7.md Normal file
View File

@ -0,0 +1,170 @@
# 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.

BIN
Treść zadań/img/1.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
Treść zadań/img/10.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

BIN
Treść zadań/img/11.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
Treść zadań/img/2.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
Treść zadań/img/3.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
Treść zadań/img/4.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
Treść zadań/img/5.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
Treść zadań/img/6.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
Treść zadań/img/7.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
Treść zadań/img/8.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
Treść zadań/img/9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

View File

@ -0,0 +1,11 @@
https://500px.com/photo/95948529/OMG-Never-ending-dunes-by-Nagaraju-Hanchanahal/
https://500px.com/photo/302447491/Badlands-from-Zabriskie-Point-by-Christopher-Petroff/
https://500px.com/photo/96326699/Eureka-Dunes-by-James-Marvin-Phelps/
https://500px.com/photo/88552513/Iceland-landscape-by-Luis-Courtot/
https://500px.com/photo/97138987/Calm-Anchor-in-Time-by-Martin-Walser/
https://500px.com/photo/89425869/Yellow-River-by-Tom-Rogula/
https://500px.com/photo/1032143380/Desert-wandering--by-Charly-Savely/
https://500px.com/photo/58341764/Landmannalaugar-by-Mathias-Beller
https://500px.com/photo/221480033/Burning-summits-by-Haag
https://500px.com/photo/90574585/The-mornings-of-Miyajima-by-E-Jacqui-Chan/
https://500px.com/photo/1006854216/Hokkaido-Flower-Field-by-David-Lew

7
Treść zadań/render.py Normal file
View File

@ -0,0 +1,7 @@
import os
rootdir = './'
for filename in os.listdir(rootdir):
if filename.endswith(".md"):
name = filename[:-3]
os.system(f'pandoc -s -o {name}.html {name}.md --mathjax --css style.css')

107
Treść zadań/style.css Normal file
View File

@ -0,0 +1,107 @@
html { font-size: 100%; overflow-y: scroll; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
body{
color:#444;
font-family:Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', serif;
font-size:12px;
line-height:1.5em;
padding:1em;
margin:auto;
max-width:42em;
background:#fefefe;
}
a{ color: #0645ad; text-decoration:none;}
a:visited{ color: #0b0080; }
a:hover{ color: #06e; }
a:active{ color:#faa700; }
a:focus{ outline: thin dotted; }
a:hover, a:active{ outline: 0; }
::-moz-selection{background:rgba(255,255,0,0.3);color:#000}
::selection{background:rgba(255,255,0,0.3);color:#000}
a::-moz-selection{background:rgba(255,255,0,0.3);color:#0645ad}
a::selection{background:rgba(255,255,0,0.3);color:#0645ad}
p{
margin:1em 0;
}
img{
max-width:100%;
}
h1,h2,h3,h4,h5,h6{
font-weight:normal;
color:#111;
line-height:1em;
}
h4,h5,h6{ font-weight: bold; }
h1{ font-size:2.5em; }
h2{ font-size:2em; }
h3{ font-size:1.5em; }
h4{ font-size:1.2em; }
h5{ font-size:1em; }
h6{ font-size:0.9em; }
blockquote{
color:#666666;
margin:0;
padding-left: 3em;
border-left: 0.5em #EEE solid;
}
hr { display: block; height: 2px; border: 0; border-top: 1px solid #aaa;border-bottom: 1px solid #eee; margin: 1em 0; padding: 0; }
pre, code, kbd, samp { color: #000; font-family: monospace, monospace; _font-family: 'courier new', monospace; font-size: 0.98em; }
pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
b, strong { font-weight: bold; }
dfn { font-style: italic; }
ins { background: #ff9; color: #000; text-decoration: none; }
mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
sup { top: -0.5em; }
sub { bottom: -0.25em; }
ul, ol { margin: 1em 0; padding: 0 0 0 2em; }
li p:last-child { margin:0 }
dd { margin: 0 0 0 2em; }
img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
}
th { border-bottom: 1px solid black; }
td { vertical-align: top; }
@media only screen and (min-width: 480px) {
body{font-size:14px;}
}
@media only screen and (min-width: 768px) {
body{font-size:16px;}
}
@media print {
* { background: transparent !important; color: black !important; filter:none !important; -ms-filter: none !important; }
body{font-size:12pt; max-width:100%;}
a, a:visited { text-decoration: underline; }
hr { height: 1px; border:0; border-bottom:1px solid black; }
a[href]:after { content: " (" attr(href) ")"; }
abbr[title]:after { content: " (" attr(title) ")"; }
.ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; }
pre, blockquote { border: 1px solid #999; padding-right: 1em; page-break-inside: avoid; }
tr, img { page-break-inside: avoid; }
img { max-width: 100% !important; }
@page :left { margin: 15mm 20mm 15mm 10mm; }
@page :right { margin: 15mm 10mm 15mm 20mm; }
p, h2, h3 { orphans: 3; widows: 3; }
h2, h3 { page-break-after: avoid; }
}