Zrób Splash z Dynamicznymi 2D Efektami Wody
() translation by (you can also view the original English article)
Sploosh! Wtym samouczku pokażę Wam, jak korzystać z prostej matematyki,fizyki i efektów cząsteczkowych, aby symulować świetnie wyglądające wodne fale i kropelki 2D.
Uwaga:Chociaż ten samouczek jest napisany przy użyciu C # i XNA,powinieneś być w stanie używać tych samych technik i koncepcji wprawie każdym środowisku programowania gier.
Końcowy podgląd wyników
Jeśli masz XNA, możesz pobrać pliki źródłowe i samemu skompilować demo. Wprzeciwnym razie sprawdź film demonstracyjny poniżej:
Istnieją dwie w większości niezależne części do symulacji wody. Najpierw zrobimy fale za pomocą modelu sprężynowego. Podrugie, użyjemy efektów cząsteczkowych, aby dodać plamy.
Tworzenie fal
Aby stworzyć fale, modelujemy powierzchnię wody jako serię pionowych sprężyn, jak pokazano na tym schemacie:



To pozwoli falom podskakiwać w górę iw dół. Następnie doprowadzimy cząstki wody do przyciągania sąsiednich cząstek, aby umożliwić rozprzestrzenienie się fal.
Springs and Hooke's Law
Wielką zaletą sprężyn jest to, że można je łatwo symulować. Sprężyny mają określoną naturalną długość; jeśli rozciągniesz lub ściśnie sprężynę, spróbujesz powrócić do tej naturalnej długości.
Siła dostarczona przez sprężynę jest podana przez prawo Hooke'a:
\[
F = -kx
\]
F
jest siłą wytwarzaną przez sprężynę, k
jest stałą sprężyny, a x
jest przesunięciem sprężyny od jej naturalnej długości. Znak ujemny wskazuje, że siła jest w przeciwnym kierunku, w którym sprężyna jest przemieszczana; jeśli popchniesz sprężynę, popchnie ją z powrotem i na odwrót.
Stała sprężyny, k
, określa sztywność sprężyny.
Aby symulować sprężyny, musimy dowiedzieć się, jak przesuwać cząstki w oparciu o prawo Hooke'a. Aby to zrobić, potrzebujemy jeszcze kilku formuł z fizyki. Po pierwsze, drugie prawo ruchu Newtona:
\[
F = ma
м
Tutaj F
jest siłą, m
jest masą, a
a jest przyspieszeniem. Oznacza to, że im silniejsza siła popycha obiekt, a im lżejszy obiekt, tym bardziej przyspiesza.
ołączenie tych dwóch formuł i rearanżacji daje nam:
\[
a = -\frac{k}{m} x
\]
To daje nam przyspieszenie dla naszych cząstek. Zakładamy, że wszystkie nasze cząstki będą miały tę samą masę, więc możemy połączyć k/m
w jedną stałą.
Aby określić pozycję od przyspieszenia, musimy wykonać integrację numeryczną. Użyjemy najprostszej formy integracji numerycznej - każda klatka po prostu wykonuje następujące czynności:
1 |
|
2 |
Position += Velocity; |
3 |
Velocity += Acceleration; |
Nazywa się to metodą Eulera. Nie jest to najdokładniejszy typ numerycznej integracji, ale jest szybki, prosty i adekwatny do naszych celów.
Składając to wszystko razem, nasze cząstki powierzchni wody wykonają następujące po sobie klatki:
1 |
|
2 |
public float Position, Velocity; |
3 |
public void Update() |
4 |
{
|
5 |
const float k = 0.025f; // adjust this value to your liking |
6 |
float x = Height - TargetHeight; |
7 |
float acceleration = -k * x; |
8 |
|
9 |
Position += Velocity; |
10 |
Velocity += acceleration; |
11 |
}
|
Tutaj TargetHeight
jest naturalną pozycją wierzchołka sprężyny, gdy nie jest ani rozciągnięta, ani skompresowana. Powinieneś ustawić tę wartość w miejscu, w którym chcesz mieć powierzchnię wody. Dla wersji demonstracyjnej ustawiłem go w połowie ekranu, na 240 pikseli.
Napięcie i tłumienie
Wspomniałem wcześniej, że stała sprężyny, k
, kontroluje sztywność sprężyny. Możesz dostosować tę wartość, aby zmienić właściwości wody Niska stała sprężyny spowoduje poluzowanie sprężyn. Oznacza to, że siła spowoduje duże fale, które oscylują powoli. Odwrotnie, wysoka stała sprężyny zwiększy napięcie sprężyny. Siły stworzą małe fale, które szybko oscylują. Wysoka stała sprężyna sprawi, że woda będzie wyglądać bardziej jak Jello.
Słowo ostrzeżenia: nie ustawiaj stałej sprężyny zbyt wysoko. Bardzo sztywne sprężyny nakładają bardzo silne siły, które zmieniają się bardzo w bardzo krótkim czasie Nie gra to dobrze z integracją numeryczną, która symuluje sprężyny jako serię dyskretnych skoków w regularnych odstępach czasu. Bardzo sztywna sprężyna może mieć nawet okres oscylacji, który jest krótszy niż twój czas. Co gorsza, metoda integracji Eulera ma tendencję do uzyskiwania energii, ponieważ symulacja staje się mniej dokładna, powodując eksplozję sztywnych sprężyn.
Jak dotąd mamy problem z naszym modelem sprężynowym. Gdy wiosna zacznie się oscylować, nigdy się nie zatrzyma. Aby rozwiązać ten problem musimy zastosować niektóre tłumienia. Chodzi o to, aby zastosować siłę w przeciwnym kierunku, który nas Wiosna jest w ruchu w celu spowolnić go. To wymaga niewielkiej korekty do naszego wzoru wiosna:
\[
a = -\frac{k}{m} x - dv
\]
Tutaj v
jest prędkością, a d
jest czynnikiem tłumiącym - kolejna stała, którą można poprawić, aby dostosować odczucie wody. Powinien być dość mały, jeśli chcesz, aby fale się oscylowały. Wersja demonstracyjna używa współczynnika tłumienia 0,025. Wysoki współczynnik tłumienia spowoduje, że woda będzie wyglądać gęsto jak melasa, a niska wartość pozwoli falom na długi czas oscylować.
Tworzenie fal propagujących
Teraz, kiedy możemy zrobić wiosnę, użyjmy ich do modelowania wody. Jak pokazano na pierwszym schemacie, modelujemy wodę za pomocą serii równoległych, pionowych sprężyn. Oczywiście, jeśli wszystkie sprężyny są niezależne, fale nigdy się nie rozprzestrzenią, tak jak robią to prawdziwe fale.
Najpierw pokażę kod, a potem przejdę dalej:
1 |
|
2 |
for (int i = 0; i < springs.Length; i++) |
3 |
springs[i].Update(Dampening, Tension); |
4 |
|
5 |
float[] leftDeltas = new float[springs.Length]; |
6 |
float[] rightDeltas = new float[springs.Length]; |
7 |
|
8 |
// do some passes where springs pull on their neighbours
|
9 |
for (int j = 0; j < 8; j++) |
10 |
{
|
11 |
for (int i = 0; i < springs.Length; i++) |
12 |
{
|
13 |
if (i > 0) |
14 |
{
|
15 |
leftDeltas[i] = Spread * (springs[i].Height - springs [i - 1].Height); |
16 |
springs[i - 1].Speed += leftDeltas[i]; |
17 |
}
|
18 |
if (i < springs.Length - 1) |
19 |
{
|
20 |
rightDeltas[i] = Spread * (springs[i].Height - springs [i + 1].Height); |
21 |
springs[i + 1].Speed += rightDeltas[i]; |
22 |
}
|
23 |
}
|
24 |
|
25 |
for (int i = 0; i < springs.Length; i++) |
26 |
{
|
27 |
if (i > 0) |
28 |
springs[i - 1].Height += leftDeltas[i]; |
29 |
if (i < springs.Length - 1) |
30 |
springs[i + 1].Height += rightDeltas[i]; |
31 |
}
|
32 |
}
|
Ten kod byłby nazywany każdą klatką z metody Update ()
. Tutaj sprężyny
to zestaw sprężyn ułożonych od lewej do prawej. leftDeltas
to zestaw pływaków, który przechowuje różnicę wysokości między każdą sprężyną i jej lewym sąsiadem. rightDeltas
jest odpowiednikiem dla właściwych sąsiadów. Przechowujemy wszystkie te różnice wysokości w tablicach, ponieważ ostatnie dwie instrukcje if
modyfikują wysokości sprężyn. Musimy zmierzyć różnice wysokości zanim jakiekolwiek wysokości zostaną zmodyfikowane.
Kod zaczyna się od uruchomienia Prawa Hooke'a na każdą sprężynę, jak opisano wcześniej. Następnie analizuje różnicę wysokości między każdą sprężyną i jej sąsiadami, a każda sprężyna przyciąga do siebie sąsiednie sprężyny, zmieniając położenie i prędkość sąsiadów. Krok ciągnięcia sąsiada jest powtarzany osiem razy, aby fale mogły się szybciej rozprzestrzeniać.
Jest jeszcze jedna doskonała wartość o nazwie Spread
. Kontroluje szybkość rozprzestrzeniania się fal. Może przyjmować wartości z zakresu od 0 do 0,5, a większe wartości powodują, że fale rozprzestrzeniają się szybciej
Aby rozpocząć ruch fal, dodamy prostą metodę o nazwie Splash()
.
1 |
|
2 |
public void Splash(int index, float speed) |
3 |
{
|
4 |
if (index >= 0 && index < springs.Length) |
5 |
springs[i].Speed = speed; |
6 |
}
|
Za każdym razem, gdy chcesz tworzyć fale, wywołaj Splash ()
. Parametr index
określa, z której wiosny powinien pochodzić splash
, a parametr prędkości określa, jak duże będą fale.
Wykonanie
Będziemy używać klasy XNA PrimitiveBatch
z XNA PrimitivesSample. Klasa PrimitiveBatch
pomaga nam rysować linie i trójkąty bezpośrednio z GPU. Używasz go tak:
1 |
|
2 |
// in LoadContent()
|
3 |
primitiveBatch = new PrimitiveBatch(GraphicsDevice); |
4 |
|
5 |
// in Draw()
|
6 |
primitiveBatch.Begin(PrimitiveType.TriangleList); |
7 |
foreach (Triangle triangle in trianglesToDraw) |
8 |
{
|
9 |
primitiveBatch.AddVertex(triangle.Point1, Color.Red); |
10 |
primitiveBatch.AddVertex(triangle.Point2, Color.Red); |
11 |
primitiveBatch.AddVertex(triangle.Point3, Color.Red); |
12 |
}
|
13 |
primitiveBatch.End(); |
Należy pamiętać, że domyślnie należy określić wierzchołki trójkąta w kolejności zgodnej z ruchem wskazówek zegara Jeśli dodasz je w kolejności przeciwnej do ruchu wskazówek zegara, trójkąt zostanie poddany ubojowi, a Ty go nie zobaczysz.
Nie trzeba mieć sprężyny dla każdego piksela szerokości. W wersji demonstracyjnej wykorzystałem 201 sprężyn rozmieszczonych w oknie o szerokości 800 pikseli. Daje to dokładnie 4 piksele między każdą sprężyną, przy czym pierwsza sprężyna ma 0, a ostatnia 800 pikseli. Prawdopodobnie możesz użyć jeszcze mniej sprężyn i nadal mieć gładką wodę.
Chcemy narysować cienkie, wysokie trapezoidy, które rozciągają się od dolnej części ekranu do powierzchni wody i łączą sprężyny, jak pokazano na tym schemacie:

Ponieważ karty graficzne nie pobierają bezpośrednio trapezów, musimy narysować każdy trapez jako dwa trójkąty. Aby wyglądać nieco ładniej, sprawimy, że woda będzie ciemniejsza, gdy się pogłębi, barwiąc dolne wierzchołki ciemnoniebieskie. GPU automatycznie interpoluje kolory między wierzchołkami.
1 |
|
2 |
primitiveBatch.Begin(PrimitiveType.TriangleList); |
3 |
Color midnightBlue = new Color(0, 15, 40) * 0.9f; |
4 |
Color lightBlue = new Color(0.2f, 0.5f, 1f) * 0.8f; |
5 |
|
6 |
var viewport = GraphicsDevice.Viewport; |
7 |
float bottom = viewport.Height; |
8 |
|
9 |
// stretch the springs' x positions to take up the entire window
|
10 |
float scale = viewport.Width / (springs.Length - 1f); // be sure to use float division |
11 |
|
12 |
for (int i = 1; i < springs.Length; i++) |
13 |
{
|
14 |
// create the four corners of our triangle.
|
15 |
Vector2 p1 = new Vector2((i - 1) * scale, springs[i - 1].Height); |
16 |
Vector2 p2 = new Vector2(i * scale, springs[i].Height); |
17 |
Vector2 p3 = new Vector2(p2.X, bottom); |
18 |
Vector2 p4 = new Vector2(p1.X, bottom); |
19 |
|
20 |
primitiveBatch.AddVertex(p1, lightBlue); |
21 |
primitiveBatch.AddVertex(p2, lightBlue); |
22 |
primitiveBatch.AddVertex(p3, midnightBlue); |
23 |
|
24 |
primitiveBatch.AddVertex(p1, lightBlue); |
25 |
primitiveBatch.AddVertex(p3, midnightBlue); |
26 |
primitiveBatch.AddVertex(p4, midnightBlue); |
27 |
}
|
28 |
|
29 |
primitiveBatch.End(); |
Oto wynik:
Robienie plam
Fale wyglądają całkiem nieźle, ale chciałbym zobaczyć plusk, gdy skała uderzy w wodę. Efekty cząsteczek są do tego idealne.
Efekty cząstek
Efekt cząsteczkowy wykorzystuje dużą liczbę małych cząstek, aby uzyskać pewien efekt wizualny. Czasami są używane do takich rzeczy, jak dym czy iskry. Zamierzamy użyć cząstek do kropel wody w rozpryskach.
Pierwszą rzeczą, której potrzebujemy, jest nasza klasa cząstek:
1 |
|
2 |
class Particle |
3 |
{
|
4 |
public Vector2 Position; |
5 |
public Vector2 Velocity; |
6 |
public float Orientation; |
7 |
|
8 |
public Particle(Vector2 position, Vector2 velocity, float orientation) |
9 |
{
|
10 |
Position = position; |
11 |
Velocity = velocity; |
12 |
Orientation = orientation; |
13 |
}
|
14 |
}
|
Ta klasa ma właściwości, które może posiadać cząstka. Następnie tworzymy listę cząstek. Każda klatka, musimy zaktualizować i narysować cząstki
1 |
|
2 |
List<Particle> particles = new List<Particle>(); |
Każda klatka, musimy zaktualizować i narysować cząstki
1 |
|
2 |
void UpdateParticle(Particle particle) |
3 |
{
|
4 |
const float Gravity = 0.3f; |
5 |
particle.Velocity.Y += Gravity; |
6 |
particle.Position += particle.Velocity; |
7 |
particle.Orientation = GetAngle(particle.Velocity); |
8 |
}
|
9 |
|
10 |
private float GetAngle(Vector2 vector) |
11 |
{
|
12 |
return (float)Math.Atan2(vector.Y, vector.X); |
13 |
}
|
14 |
|
15 |
public void Update() |
16 |
{
|
17 |
foreach (var particle in particles) |
18 |
UpdateParticle(particle); |
19 |
|
20 |
// delete particles that are off-screen or under water
|
21 |
particles = particles.Where(x => x.Position.X >= 0 && x.Position.X <= 800 && x.Position.Y <= GetHeight(x.Position.X)).ToList(); |
22 |
}
|
Aktualizujemy cząstki pod wpływem grawitacji i ustawiamy kierunek cząstek tak, aby pasowały do kierunku, w którym zmierza Następnie pozbywamy się cząstek, które są poza ekranem lub pod wodą, kopiując wszystkie cząsteczki, które chcemy zachować na nową listę i przypisywanie jej do cząstek. Następnie narysujemy cząstki.
1 |
|
2 |
void DrawParticle(Particle particle) |
3 |
{
|
4 |
Vector2 origin = new Vector2(ParticleImage.Width, ParticleImage.Height) / 2f; |
5 |
spriteBatch.Draw(ParticleImage, particle.Position, null, Color.White, particle.Orientation, origin, 0.6f, 0, 0); |
6 |
}
|
7 |
|
8 |
public void Draw() |
9 |
{
|
10 |
foreach (var particle in particles) |
11 |
DrawParticle(particle); |
12 |
}
|
Poniżej znajduje się faktura, której użyłem dla cząstek.

eraz, gdy tworzymy splash, tworzymy wiązkę cząstek.
1 |
|
2 |
private void CreateSplashParticles(float xPosition, float speed) |
3 |
{
|
4 |
float y = GetHeight(xPosition); |
5 |
|
6 |
if (speed > 60) |
7 |
{
|
8 |
for (int i = 0; i < speed / 8; i++) |
9 |
{
|
10 |
Vector2 pos = new Vector2(xPosition, y) + GetRandomVector2(40); |
11 |
Vector2 vel = FromPolar(MathHelper.ToRadians(GetRandomFloat(-150, -30)), GetRandomFloat(0, 0.5f * (float)Math.Sqrt(speed))); |
12 |
particles.Add(new Particle(pos, velocity, 0)); |
13 |
}
|
14 |
}
|
15 |
}
|
Możesz wywołać tę metodę z metody Splash ()
używanej do generowania fal. Szybkość parametru to szybkość uderzenia skały w wodę. Jeśli kamień porusza się szybciej, zrobimy większe plamy.
GetRandomVector2(40)
zwraca wektor o losowym kierunku i losowej długości od 0 do 40. Chcemy dodać trochę losowości do pozycji, aby cząsteczki nie pojawiały się w jednym punkcie. FromPolar ()
zwraca wektor2
o danym kierunku i długości.
Oto wynik:
Używanie Metaballs jako cząstek
Nasze plamy wyglądają całkiem przyzwoicie, a niektóre świetne gry, takie jak World of Goo, mają plamy cząstek, które wyglądają podobnie do naszych. Jednak pokażę wam technikę, która sprawi, że plamy będą wyglądać bardziej płynnie. Technika ta wykorzystuje metabolity, organiczne bloby, o których wcześniej pisałem samouczek. Jeśli interesują Cię szczegóły dotyczące metaball i ich działania, przeczytaj ten samouczek. Jeśli chcesz wiedzieć, jak zastosować je do naszych rozprysków, czytaj dalej.
Metaballe wyglądają jak ciecze w sposób, w jaki łączą się ze sobą, dzięki czemu dobrze pasują do naszych płynnych rozprysków. Aby utworzyć metaball, będziemy musieli dodać nowe zmienne klasy:
1 |
|
2 |
RenderTarget2D metaballTarget; |
3 |
AlphaTestEffect alphaTest; |
Które zainicjujemy tak:
1 |
|
2 |
var view = GraphicsDevice.Viewport; |
3 |
metaballTarget = new RenderTarget2D(GraphicsDevice, view.Width, view.Height); |
4 |
alphaTest = new AlphaTestEffect(GraphicsDevice); |
5 |
alphaTest.ReferenceAlpha = 175; |
6 |
|
7 |
alphaTest.Projection = Matrix.CreateTranslation(-0.5f, -0.5f, 0) * |
8 |
Matrix.CreateOrthographicOffCenter(0, view.Width, view.Height, 0, 0, 1); |
Następnie narysujemy metaball:
1 |
|
2 |
GraphicsDevice.SetRenderTarget(metaballTarget); |
3 |
GraphicsDevice.Clear(Color.Transparent); |
4 |
|
5 |
Color lightBlue = new Color(0.2f, 0.5f, 1f); |
6 |
|
7 |
spriteBatch.Begin(0, BlendState.Additive); |
8 |
foreach (var particle in particles) |
9 |
{
|
10 |
Vector2 origin = new Vector2(ParticleImage.Width, ParticleImage.Height) / 2f; |
11 |
spriteBatch.Draw(ParticleImage, particle.Position, null, lightBlue, particle.Orientation, origin, 2f, 0, 0); |
12 |
}
|
13 |
spriteBatch.End(); |
14 |
|
15 |
GraphicsDevice.SetRenderTarget(null); |
16 |
device.Clear(Color.CornflowerBlue); |
17 |
spriteBatch.Begin(0, null, null, null, null, alphaTest); |
18 |
spriteBatch.Draw(metaballTarget, Vector2.Zero, Color.White); |
19 |
spriteBatch.End(); |
20 |
|
21 |
// draw waves and other things
|
Efekt metaboliczny zależy od tego, czy tekstura cząstki zanika, gdy oddalamy się od centrum. Oto, co użyłem, ustawić na czarnym tle, aby było widoczne

Oto, jak to wygląda:
Kropelki wody łączą się teraz, gdy są blisko. Jednak nie łączą się one z powierzchnią wody. Możemy o naprawić, dodając gradient do powierzchni wody. Który sprawia, że stopniowo zanika i oddaje go naszemu celowi metalizacji.
Dodaj następujący kod do powyższej metody przed wierszem
GraphicsDevice.SetRendertarget (null)
:
1 |
|
2 |
primitiveBatch.Begin(PrimitiveType.TriangleList); |
3 |
|
4 |
const float thickness = 20; |
5 |
float scale = GraphicsDevice.Viewport.Width / (springs.Length - 1f); |
6 |
for (int i = 1; i < springs.Length; i++) |
7 |
{
|
8 |
Vector2 p1 = new Vector2((i - 1) * scale, springs[i - 1].Height); |
9 |
Vector2 p2 = new Vector2(i * scale, springs[i].Height); |
10 |
Vector2 p3 = new Vector2(p1.X, p1.Y - thickness); |
11 |
Vector2 p4 = new Vector2(p2.X, p2.Y - thickness); |
12 |
|
13 |
primitiveBatch.AddVertex(p2, lightBlue); |
14 |
primitiveBatch.AddVertex(p1, lightBlue); |
15 |
primitiveBatch.AddVertex(p3, Color.Transparent); |
16 |
|
17 |
primitiveBatch.AddVertex(p3, Color.Transparent); |
18 |
primitiveBatch.AddVertex(p4, Color.Transparent); |
19 |
primitiveBatch.AddVertex(p2, lightBlue); |
20 |
}
|
21 |
|
22 |
primitiveBatch.End(); |
Teraz cząstki zleją się z powierzchnią wody.
Dodanie efektu Beveling
Cząsteczki wody wyglądają nieco płasko i dobrze byłoby dać im cień Najlepiej byłoby zrobić to w module cieniującym. Jednak ze względu na prostotę tego samouczka, zastosujemy szybką i prostą sztuczkę: po prostu rysujemy trzykrotnie cząstki z różnymi zabarwieniami i przesunięciami, jak pokazano na poniższym schemacie.



Aby to zrobić, chcemy przechwycić cząstki metaball w nowym celu renderowania. Następnie narysujemy ten cel renderowania raz dla każdego odcienia
Najpierw zadeklaruj nowy RenderTarget2D
, tak jak zrobiliśmy to dla metaballów:
1 |
|
2 |
particlesTarget = new RenderTarget2D(GraphicsDevice, view.Width, view.Height); |
Następnie, zamiast rysować metaballTarget
bezpośrednio do backbuffera, chcemy go narysować na particleTarget
Aby to zrobić, przejdź do metody, w której narysuj metrum i po prostu zmień te linie
1 |
|
2 |
GraphicsDevice.SetRenderTarget(null); |
3 |
device.Clear(Color.CornflowerBlue); |
...do:
1 |
|
2 |
GraphicsDevice.SetRenderTarget(particlesTarget); |
3 |
device.Clear(Color.Transparent); |
Następnie użyj następującego kodu, aby narysować trzykrotnie cząsteczki z różnymi odcieniami i przesunięciami:
1 |
|
2 |
Color lightBlue = new Color(0.2f, 0.5f, 1f); |
3 |
|
4 |
GraphicsDevice.SetRenderTarget(null); |
5 |
device.Clear(Color.CornflowerBlue); |
6 |
|
7 |
spriteBatch.Begin(); |
8 |
spriteBatch.Draw(particlesTarget, -Vector2.One, new Color(0.8f, 0.8f, 1f)); |
9 |
spriteBatch.Draw(particlesTarget, Vector2.One, new Color(0f, 0f, 0.2f)); |
10 |
spriteBatch.Draw(particlesTarget, Vector2.Zero, lightBlue); |
11 |
spriteBatch.End(); |
12 |
|
13 |
// draw waves and other stuff
|
Wniosek
To wszystko w przypadku podstawowej wody 2D. Do wersji demo dodałem kamień, który można wpaść do wody. Rysuję wodę z pewną przezroczystością na górze skały, aby wyglądała jak pod wodą i spowolniam ją, gdy jest pod wodą z powodu odporności na wodę.
Aby wyglądało na nieco ładniejsze, poszedłem na opengameart.org i znalazłem obraz dla rocka i tła nieba. Możesz znaleźć kamień i niebo na http://opengameart.org/content/rocks i opengameart.org/content/sky-backdrop odpowiednio.