Wstęp
Jeżeli poszukujesz klasy która pozwoli ci w nieco bardziej rozbudowany sposób urozmaicić kształty w SFML lub chcesz stworzyć swoje własne figury to ten tutorial jest dla ciebie.
Zadajmy sobie pytanie czym jest Vertex Array? Z angielskiego wiemy, że to po prostu tablica wierzchołków. Vertex jest najmniejszą dostępną jednostką jaką możemy manipulować. Posiada pozycję (x,y lub x,y,z), a także kolor i parę współrzędnych tekstury.
Wierzchołki pojedynczo nie robią wiele, dlatego łączy się je w prymitywy: 1 wierzchołek tworzy punkt, 2 tworzą linię, 3 – trójkąt, 4 -czworokąt. Gdy połączysz 1 lub więcej wierzchołków razem otrzymasz geometryczną reprezentację figury.
VertexArray
Na początku przyjrzyjmy się strukturze sf::Vertex, która przechowuje 3 publiczne elementy: pozycję (position), kolor (color), teksturę (texCoords). Przy tworzeniu musisz się zdecydować czy wybierasz użycie koloru, czy tekstury. Możesz te elementy uzupełnić poprzez wywołanie je jak w każdej standardowej klasie:
1 2 3 4 5 6 7 8 9 10 11 |
// stworzenie nowego vertex'a sf::Vertex vertex; // ustawienie pozycji vertex.position = sf::Vector2f(10, 50); // ustawienie koloru vertex.color = sf::Color::Red; // wstawienie współrzędnych tekstury vertex.texCoords = sf::Vector2f(100, 100); |
lub możesz uzupełnić je używając konstruktora:
1 |
sf::Vertex vertex(sf::Vector2f(10, 50), sf::Color::Red, sf::Vector2f(100, 100)); |
Teraz sprawmy aby wierzchołki utworzyły nam typ prymitywny. Pamiętamy przy tym, że aby stworzyć typ prymitywny musimy użyć kilku wierzchołków, czyli potrzebujemy na nie tablicy sf::VertexArray. Jest to typ danych podobnych do std::vector i jego użycie jest analogiczne.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// ustawienie rodzaju prymitywu oraz ustawienie z ilu wierzchołków się składa sf::VertexArray triangle(sf::Triangles, 3); // nadanie pozycji wszystkim wierzchołkom triangle[0].position = sf::Vector2f(10, 10); triangle[1].position = sf::Vector2f(100, 10); triangle[2].position = sf::Vector2f(100, 100); // ustawienie koloru triangle[0].color = sf::Color::Red; triangle[1].color = sf::Color::Blue; triangle[2].color = sf::Color::Green; // brak texCoords, o tym porozmawiamy później |
Nasz trójkąt jest już gotowy, jak zauważyłeś użyliśmy 3 różnych kolorów, narysujmy to i zobaczmy jak to się prezentuje.
1 |
window.draw(triangle); |
W ten sposób możesz tworzyć m.in. gradienty.
Zauważ, że nie musisz korzystać z VertexArray, możesz równie dobrze posłużyć się wcześniej wspomnianym std::vector, wtedy rysowanie wygląda w trochę inny sposób.
1 2 3 4 5 |
std::vector<sf::Vertex> vertices; vertices.push_back(sf::Vertex(...)); ... window.draw(&vertices[0], vertices.size(), sf::Triangles); |
Możesz też użyć tradycyjnych tablic:
1 2 3 4 5 6 7 |
sf::Vertex vertices[2] = { sf::Vertex(...), sf::Vertex(...) }; window.draw(vertices, 2, sf::Lines); |
Rodzaje prymitywów
Najwyższa pora to poznania rodzajów prymitywów jakie możesz spotkać w SFML.
sf::Points
Jest to zbiór niepołączonych ze sobą punktów, punkty nie mają swojej grubości, zawsze są o wielkości 1 piksela, niezależnie od przekształceń i view (klasa w SFML).
sf::Lines
Zbiór niepołączonych ze sobą linii, są zawsze grubości 1 piksela, niezależnie od przekształceń i użycia view.
sf::LinesStrip
Zbiór połączonych ze sobą linii, ostatni wierzchołek jest łączony z pierwszym wierzchołkiem i tworzy figurę zamkniętą.
sf::Triangles
Zbiór niepołączonych ze sobą trójkątów.
sf::TrianglesStrip
Zbiór połączonych ze sobą trójkątów, każdy trójkąt udostępnia 2 ostatnie wierzchołki następnemu trójkątowi.
sf::TrianglesFan
Zbiór połączonych trójkątów połączonych z 1 centralnym punktem. Pierwszy wierzchołek jest centralnym punktem, każdy nowy vertex tworzy nowy trójkąt, używając środek i poprzedni vertex.
sf::Quads
Zbiór niepołączonych ze sobą kwadratów. Każde z 4 wierzchołków muszą być zdefiniowane w kolejności zgodnej lub przeciwnej do wskazówek zegara.
Nadawanie tekstur (texturing)
Tak jak reszta obiektów w SFML tak i Vertex może być teksturowany. Aby to zrobić musisz użyć texCoords, ta zmienna definiuje który piksel tekstury ma być mapowany do Vertex‚a.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// stworzenie czworoboku sf::VertexArray quad(sf::Quads, 4); // utworzenie prostokąta, na pozycji (10, 10) i rozmiarach 100x100 quad[0].position = sf::Vector2f(10, 10); quad[1].position = sf::Vector2f(110, 10); quad[2].position = sf::Vector2f(110, 110); quad[3].position = sf::Vector2f(10, 110); // utworzenie obszaru tekstury, która będzie kwadratem 25x50 od pozycji (0, 0) quad[0].texCoords = sf::Vector2f(0, 0); quad[1].texCoords = sf::Vector2f(25, 0); quad[2].texCoords = sf::Vector2f(25, 50); quad[3].texCoords = sf::Vector2f(0, 50); |
Współrzędne tekstury są w pikselach i nie są normalizowane (nie mają wartości pomiędzy 0, a 1), tak jak osoby piszące w openGL mogłyby się spodziewać.
VertexArray to nisko poziomowy typ, a więc nie przechowują one tekstury. Aby narysować je z teksturą należy ją dostarczyć bezpośrednio do funkcji draw.
1 2 3 4 5 6 |
sf::VertexArray vertices; sf::Texture texture; ... window.draw(vertices, &texture); |
To jest ta prostsza wersja, jednak gdybyś potrzebował użyć render states (przekształceń lub blend mode) to możesz użyć rozszerzonej wersji używającej sf::RenderStates.
1 2 3 4 5 6 7 8 9 |
sf::VertexArray vertices; sf::Texture texture; ... sf::RenderStates states; states.texture = &texture; window.draw(vertices, states); |
Przekształcenia VertexArray
VertexArray ulega przekształceniom w podobny sposób jak w przypadku teksturowania.
1 2 3 4 5 6 |
sf::VertexArray vertices; sf::Transform transform; ... window.draw(vertices, transform); |
Lub:
1 2 3 4 5 6 7 8 9 |
sf::VertexArray vertices; sf::Transform transform; ... sf::RenderStates states; states.transform = transform; window.draw(vertices, states); |
Aby wiedzieć o samej klasie sf::Tranform przeczytaj kurs o Przekształceniach.
Tworzenie „SFML-like” jednostek
Jeżeli twoim zdaniem pokazane wcześniej klasy są dla ciebie niewystarczające to za pomocą sf::Drawable oraz sf::Transformable. Te dwie klasy są bazą dla klas takich jak sf::Sprite, sf::Text, czy sf::Shape.
sf::Drawable to interfejs, ta funkcja jedynie deklaruje pustą wirtualną funkcję draw, której dziedziczenie pozwoli ci użyć funkcji draw to narysowania obiektów.
1 2 3 4 5 6 7 8 9 |
class MyEntity : public sf::Drawable { private: virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const; }; MyEntity entity; window.draw(entity); // wewnętrzne uruchomienie entity.draw |
Zauważ że funkcja draw wykonana przez ciebie jest uruchamiana wewnątrz window.draw. W ten sposób kod wygląda przejrzyściej i ładniej.
Kolejną klasą jest sf::Transformable, która nie ma funkcji wirtualnych. Dziedziczenie po niej daje ci automatycznie standardowe dla tej klasy metody (obracanie, ustawianie pozycji, itd.).
Standardowa klasa graficzna w SFML wygląda w ten sposób:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class MyEntity : public sf::Drawable, public sf::Transformable { public: // dodanie funkcji do obsługi klasy: rotacja, zmiana kolorów, etc. private: virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const { // ustawienie przekształceń obiektu states.transform *= getTransform(); // getTransform() jest zdefiniowane przez sf::Transformable // ustawienie tekstury states.texture = &m_texture; // możesz także ustawić states.shader lub states.blendMode jeżeli chcesz // narysowanie target.draw(m_vertices, states); } sf::VertexArray m_vertices; sf::Texture m_texture; }; |
Możesz także korzystać z wbudowanych w SFML klas:
1 2 3 4 5 6 7 8 |
MyEntity entity; // możesz przekształcać obiekty entity.setPosition(10, 50); entity.setRotation(45); // rysowanie window.draw(entity); |
Przykład: mapa kafelkowa
Wykorzystajmy to co widzieliśmy powyżej, cała mapa będzie zawarta w pojedynczym VertexArray, dzięki temu będzie rysowana bardzo szybko. Zauważ, że w tym przykładzie pobieramy kafelki zakładając, że wszystkie tektury kafelków są w 1 pliku (tileset).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
class TileMap : public sf::Drawable, public sf::Transformable { public: bool load(const std::string& tileset, sf::Vector2u tileSize, const int* tiles, unsigned int width, unsigned int height) { // wczytanie atlasu tekstur if (!m_tileset.loadFromFile(tileset)) return false; // zmiana rozmiaru tablicy, tak aby pasowała do wielkości mapy m_vertices.setPrimitiveType(sf::Quads); m_vertices.resize(width * height * 4); // zapełnianie vertex array, po jednym na kafel for (unsigned int i = 0; i < width; ++i) for (unsigned int j = 0; j < height; ++j) { // pobranie obecnego numeru kafla int tileNumber = tiles[i + j * width]; // znalezienie jego pozycji na tileset int tu = tileNumber % (m_tileset.getSize().x / tileSize.x); int tv = tileNumber / (m_tileset.getSize().x / tileSize.x); // pobranie wskaźnika do bieżącego kafla czworokąta sf::Vertex* quad = &m_vertices[(i + j * width) * 4]; // ustawienie 4 rogów quad[0].position = sf::Vector2f(i * tileSize.x, j * tileSize.y); quad[1].position = sf::Vector2f((i + 1) * tileSize.x, j * tileSize.y); quad[2].position = sf::Vector2f((i + 1) * tileSize.x, (j + 1) * tileSize.y); quad[3].position = sf::Vector2f(i * tileSize.x, (j + 1) * tileSize.y); // ustawienie 4 współrzędnych tekstury quad[0].texCoords = sf::Vector2f(tu * tileSize.x, tv * tileSize.y); quad[1].texCoords = sf::Vector2f((tu + 1) * tileSize.x, tv * tileSize.y); quad[2].texCoords = sf::Vector2f((tu + 1) * tileSize.x, (tv + 1) * tileSize.y); quad[3].texCoords = sf::Vector2f(tu * tileSize.x, (tv + 1) * tileSize.y); } return true; } private: virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const { // ustawienie przekształceń states.transform *= getTransform(); // ustawienie tekstury states.texture = &m_tileset; // narysowanie target.draw(m_vertices, states); } sf::VertexArray m_vertices; sf::Texture m_tileset; }; |
A także użycie tego przez program:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
int main() { // stworzenie okna sf::RenderWindow window(sf::VideoMode(512, 256), "Tilemap"); // stworzenie mapy, za pomocą tablicy, te cyfry to typy pól const int level[] = { 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 1, 0, 0, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2, 0, 0, 0, 0, 1, 0, 3, 0, 2, 2, 0, 0, 1, 1, 1, 1, 2, 0, 2, 0, 1, 0, 3, 0, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 3, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, }; // stworzenie mapy TileMap map; if (!map.load("tileset.png", sf::Vector2u(32, 32), level, 16, 8)) return -1; // uruchomienie pętli while (window.isOpen()) { // przejmowanie event'ów sf::Event event; while (window.pollEvent(event)) { if(event.type == sf::Event::Closed) window.close(); } // narysowanie mapy window.clear(); window.draw(map); window.display(); } return 0; } |
Przykład: system cząsteczek
Drugim przykładem będzie system cząsteczek, będzie to o tyle prostsze, że nie będziemy musieli używać tekstur. Jeżeli chcesz wiedzieć na jakiej zasadzie działają systemy cząsteczek odsyłam cię do strony Nature of Code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
class ParticleSystem : public sf::Drawable, public sf::Transformable { public: ParticleSystem(unsigned int count) : m_particles(count), m_vertices(sf::Points, count), m_lifetime(sf::seconds(3)), m_emitter(0, 0) { } void setEmitter(sf::Vector2f position) { m_emitter = position; } void update(sf::Time elapsed) { for (std::size_t i = 0; i < m_particles.size(); ++i) { // zaktualizowanie czasu życia cząsteczki Particle& p = m_particles[i]; p.lifetime -= elapsed; // jeżeli martwa, stwórzmy nową if (p.lifetime <= sf::Time::Zero) resetParticle(i); // zaktualizowanie pozycji m_vertices[i].position += p.velocity * elapsed.asSeconds(); // zaktualizowanie przezroczystości cząsteczki, na podstawie czasu życia float ratio = p.lifetime.asSeconds() / m_lifetime.asSeconds(); m_vertices[i].color.a = static_cast<sf::Uint8>(ratio * 255); } } private: virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const { // przekształcenia states.transform *= getTransform(); // brak tekstur states.texture = NULL; // rysowanie target.draw(m_vertices, states); } private: struct Particle { sf::Vector2f velocity; sf::Time lifetime; }; void resetParticle(std::size_t index) { // nadanie losowej prędkości float angle = (std::rand() % 360) * 3.14f / 180.f; float speed = (std::rand() % 50) + 50.f; m_particles[index].velocity = sf::Vector2f(std::cos(angle) * speed, std::sin(angle) * speed); m_particles[index].lifetime = sf::milliseconds((std::rand() % 2000) + 1000); // nadanie początkowej pozycji m_vertices[index].position = m_emitter; } std::vector<Particle> m_particles; sf::VertexArray m_vertices; sf::Time m_lifetime; sf::Vector2f m_emitter; }; |
Oraz sposób użycia:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
int main() { // stworzenie okna sf::RenderWindow window(sf::VideoMode(512, 256), "Particles"); // stworzenie 1000 cząteczek ParticleSystem particles(1000); // stworzenie zegara do śledzenia czasu sf::Clock clock; // pętla główna while (window.isOpen()) { // przejmowanie event'ów sf::Event event; while (window.pollEvent(event)) { if(event.type == sf::Event::Closed) window.close(); } // ustawienie tak aby system cząsteczki wylatywały z pozycji myszy sf::Vector2i mouse = sf::Mouse::getPosition(window); particles.setEmitter(window.mapPixelToCoords(mouse)); // aktualizowanie sf::Time elapsed = clock.restart(); particles.update(elapsed); // rysowanie window.clear(); window.draw(particles); window.display(); } return 0; } |