Czasem lubię sobie spędzić wieczór aby wytężyć nieco umysł i napisać sobie coś prostego. Coś niekoniecznie przydatnego, ale coś co sprawi, że będę musiał wziąć kartkę i długopis i zastanowić się jak to zaimplementować.
Kiedyś dawno temu wrzucałem shader, który symulował efekt oświetlenia kafli na standardowej tilemap. Kiedyś muszę podrzucić jeszcze aktualizację bo nieco go zmieniłem ale o tym kiedy indziej.
Tym razem postanowiłem zrobić coś całkowicie głupiego. Narysować trawę. Jest to o tyle głupie, że jest pełno rozwiązań tego typu w grach 3D i opiera się zazwyczaj na VERTEX, czyli np. wiatr modyfikuje geometrię zaś tekstura zostaje bez zmian. W moim przypadku modyfikowana jest tylko tekstura w grze 2D, która jest tłem. Czy można tego jakoś realnie użyć? Nie wiem. Główny problem to fakt, że jest to tekstura i nie ma przez to z-order. Możemy sobie teraz wyobrazić, że chcemy zaimplementować osobę chodzącą w tej trawie. Wyzwaniem może być aby niektóre trawy były za a niektóre przed obrazkiem. Oczywiście nie jest to niemożliwe. Można przekazać do shadera SCREEN_TEXTURE i jakiś dodatkowy parametr mówiący o tym od jakiego Y brać SCREEN_TEXTURE zamiast liczyć trawę. Ale wszystko wtedy co normalnie jest robione w karcie graficznej musiałoby być robione teraz manualnie.
Niemniej i tak chciałem zaimplementować trawę aby poćwiczyć proceduralnie generowane tekstury. Jeśli generujemy coś tego typu pierwsza bardzo ważna sprawa to nasza funkcja fragment()
, którą mamy w shaderze jest wywoływana dla każdego piksela i nie mamy informacji o tych pikselach obok. To sprawia, że w kodzie czasami musi się dziać więcej magii niż ktoś mógłby przypuszczać. No bo jak narysować źdźbło trawy? Znamy lokalizację piksela, którego rysujemy. Więc najlepszym sposobem jest stworzenie matematycznej funkcji, która dla f(x,y) zwróci nam informację czy jest to źdźbło czy nie. Poniższy przykład prezentuje mniej-więcej to co mam na myśli.
Kafle
No dobra, ale tutaj mamy jeden rysunek. Jak poradzić sobie z większą liczbą, szczególnie czymś takim co ma wyglądać jak trawa?
Nie wiem czy moje rozwiązanie jest najlepsze, ale w sumie to było pierwsze rozwiązanie jakie mi przyszło do głowy. Czyli podzielenie całej tekstury na małe prostokąty, i każdy z nich będzie reprezentować kafelek danych.
Jest to o tyle istotne, że musimy znaleźć miejsce gdzie nasza trawa się znajduje. Jeśli wiemy, że pewna grupa pikseli należy do tego samego koloru to też możemy łatwo użyć pseudolosowości, ale o tym już za chwilę. Istotne jest to, że teraz każdy kafelek możemy traktować indywidualnie tak samo jak wcześniej mieliśmy cały obraz.
Losowość
Przy pomocy szumu możemy teraz w łatwy sposób nieco zmodyfikować pewne wartości wewnątrz kafli. Najprostszy przykład to w ogóle czy coś ma się wyświetlić czy nie. Ale możemy też losować kolory i inne parametry.
Zakończenie
Gdy już opisałem ideę, która stoi za moim shaderem to teraz już po krótce opiszę co oprócz tego musiałem zrobić. Po pierwsze trawa jest wyższa niż jeden kafelek, aby obejść ten problem iteruję po kafelkach od tego, który jest o X niżej i sprawdzam czy dany pixel jest częścią trawy, jeśli tak to kończę pętlę. Jest też dodana funkcja wiatru, która modyfikuje funkcję geometrii trawy. Aby nie wyglądało to statycznie to dodaję pewien szum w funkcji czasu aby ładnie się kołysały.
Kod jaki mi wyszedł na koniec to:
float shader_grass(inout vec4 color, vec2 uv, vec2 tile, float density, float a_value, float wind_strength, float wind_direction, int grass_height) {
vec4 grass_1 = vec4(0., 0.5, 0., 1.);
vec4 grass_2 = vec4(0., 0.8, 0.2, 1.);
float wind_direction_rad = wind_direction / 180.0 * PI;
vec2 wind_offset = normalize(vec2(sin(wind_direction_rad), cos(wind_direction_rad)));
vec2 dir = normalize(vec2(sin(wind_direction_rad), cos(wind_direction_rad)));
float n = sin(TIME/ 1000.) * wind_strength;
color = vec4(0.3, mod(texture(noise_texture, uv).g * 100. - 100., 0.1)+0.2, 0.1, 0.5);
for (int i = grass_height; i >= 0; i--) {
for (int j = -grass_height / 2; j <= grass_height; j++) {
vec2 tile_m = vec2(tile.x + float(j) / density,tile.y + float(i) / density);
// Skip if color already calculated
if (color.a == 1.0) {
return float(i);
}
float data = texture(noise_texture, tile_m).r;
if (mod(data*10. - 10., 0.3) < 0.1) {
// Additional random offset to avid grass in lines
vec2 offset = vec2(((mod(data * 1000., 0.934) - 0.5) * 2.) / density, 0.);
float grass_height_offset_rand = float(grass_height) - mod(data*100., float(grass_height - 1));
float distance_factor = length(tile_m - uv);
float bending_factor = pow(distance_factor, 1.5) * (1. + mod(data * 1000., 1.)); // Randomize bending
float wind_wave = (sin(texture(noise_texture, n* dir * data)).r + 1.0) / 4.;
vec2 wind_offset = vec2(
sin(wind_direction/180.*PI),
cos(wind_direction/180.*PI)
) * wind_strength * bending_factor * float(grass_height) / density;
wind_offset = wind_offset + wind_offset * wind_wave;
float n = (abs(tile_m.x + offset.x - uv.x + wind_offset.x * (tile_m.y - uv.y) ) + length(tile_m - uv) / (10. * grass_height_offset_rand)) * density;
if (n < 0.11){
if (color.a != 1.0) {
float c = ((mod(data * 1000., 0.934) - 0.5) * 2.);
color = grass_1 + vec4(c, c, 0.0,0.);
}
if (n < 0.09){
color = mix(color, grass_2, 0.5);
}
color = mix(vec4(0.,0.,0.,1.), color, length(tile_m - uv) * float(grass_height));
}
}
}
}
return 0.0;
}
A efekt można zobaczyć tutaj na YT:
oraz na shadertoy na żywo:
https://www.shadertoy.com/view/4XfcR7
jakiego programu do tego używasz?
Zazwyczaj shadery piszę w godocie. Wygonie można zaimplementować modyfikację parametrów tak jak to widać na filmiku. Ale pewnie jak zauważyłeś po screenach użyłem www.shadertoy.com gdzie można sobie robić shadery dość wygodnie w przeglądarce. Ten ostatni link we wpisie prowadzi do kodu właśnie na tym serwisie.
Jeden z najbardziej popularnych shaderów napisanych na shadertoy to efekt wody:
https://www.shadertoy.com/view/Ms2SD1
Jak widać, nie ma do shadera przekazanych żadnych tekstur a wszystko jest wygenerowane :-)
Masz duże doświadczenie z godotem?
Tylko hobbistyczne bo nie pracuję w Gamedev. Ale kiedyś robiłem z synem platformówkę w stylu Mario i w sumie można było grę napisać w 30 minut z tutorialem. Ogólnie oceniam go dobrze. Z Unity mam mało doświadczenia bo tylko kiedyś coś testowałem, a Unreal wg mnie ma bardzo wysoki próg wejścia. Jeśli ktoś chce się pobawić w pisanie gierek to chyba Godot z tych wszystkich silników jest najlepszy.
No właśnie też odniosłem takie wrażenie że jest najlepszy i ostatnimi czasy trochę się bawię tym silnikiem. Duże + to to że jest darmowy, próg wejścia jest niski, program jest łatwy do zrozumienia i przejrzysty. Dokumentacja też całkiem spora.
Zasadniczo chyba najważniejsza rzecz to brak opłat bo dajmy na to że nawet trochę mniej tych funkcji tam jest i trochę mniej bajerów ale powiedzmy że jednak uda ci się w końcu coś kiedyś stworzyć co osiągnie jakiś tam sukces. Wtedy nagle to 30% czy ileś tam % zysku oddawane na UE albo Unity za korzystanie będzie bolało. A tak co twoje to twoje. Z kolei dla zabawy to nie musi być też narzędzie pro.
A no i oczywiście sławna zmiana licencji Unity na płatność od instalacji. Oczywiście nie przeszło, ale ucząc się silnika lepiej brać pod uwagę, że nagle zmienią licencję i trzeba będzie grę przepisać.
W teorii Unity i UE biorą opłaty od bardzo wysokich poziomów zarobków więc programiści gier Indie mają problem to przekroczyć, ale i tak zawsze lepiej użyć otwartego rozwiązania. Ostatnio myślałem aby zaimplementować coś przy użyciu tilemap. Popatrzyłem w kod Godota i stwierdziłem, że w moim przypadku się nie nada. Musiałbym samego godota zmienić więc zdecydowałem się na inne rozwiązanie. Zresztą wiele pro firm będzie się przerzucać na godota bo jeśli czegoś im brakuje mogą łatwo doimplementować zamiast prosić Unity o poprawę czegoś. To tak jak z Linuksem. Dużo profesjonalnych firm go używa bo czasem łatwiej coś samemu zmienić niż prosić o to MS. Patrz SteamOS vs urządzenia z windowsem tego typu.
No więc właśnie. Dla mnie chyba najlepszym przykładem porównawczym do silnika Godot jest Blender. Blender tez na początku był daleko za programami 3d dziś moim zdaniem to chyba najlepsze oprogramowanie do do modelowania i projektowania 3d.
Z resztą z linuxa też korzystam już od kilkunastu lat i dziś dawno wyprzedził Windowsa zarówno w funkcjonalności, stabilności jak i lekkości. Jak mam się przesiąść czasem na windowsa 10, 11 albo 12 to aż się wzdrygam i mnie szlag trafia jak korzystam z tego systemu :P
Że też mimo tylu lat i takiego budżetu wciąż nie potrafili stworzyć tak funkcjonalnego interfejsu i bez dodatkowych programów nie działają np takie proste rzeczy jak przeciągnięcie obrazka bezpośrednio z przeglądarki obrazów np do okna przeglądarki po prostu chwytając go myszką.
Oh pamiętam jak bawiłem się w blenderze pierwszy raz w okolicach 2005 roku. To dobry przykład programu, który na początku był wyśmiewany, a potem całkowicie pozamiatał.