Tworzenie - ProtoSegmentis

Proto-segmentis to prototyp stworzony w silniku Unity w 2018 roku, służący do proceduralnego generowania budynków. Praca dyplomowa pozwoliła mi zgłębić niezbędne zagadnienia matematyczne i przetestować ten koncept w formie prototypu.

Narzędzie Segmentis umożliwiało tworzenie budynków zarówno za pomocą kodu, jak i bezpośrednio w edytorze Unity. Projektant poziomów mógł rozmieszczać ściany w sposób przypominający mechanikę znaną z gry „The Sims”, tworząc w ten sposób własne konstrukcje.

Brak gotowych komponentów siatek (mesh): Siatki ścian były generowane programowo poprzez obliczanie wierzchołków, trójkątów oraz wektorów normalnych.
Architektura segmentowa: Ściany składały się z prostych segmentów lub zintegrowanych elementów, takich jak okna i drzwi.
Optymalizacja poprzez łączenie siatek: Sąsiadujące ze sobą proste ściany były łączone w jedną całość, co pozwalało zoptymalizować liczbę wywołań rysowania (draw calls).
Edycja w czasie rzeczywistym i serializacja: System umożliwiał zapisywanie wygenerowanych siatek oraz ich edycję w trakcie działania aplikacji.

Przykład obliczania wierzchołków i trójkątów:

Proces wyznaczania wierzchołków oraz trójkątów jest nieskomplikowany. Najpierw należy narysować obiekt, wyraźnie zaznaczając punkty i odpowiednio je numerując. Następnie każdemu z punktów przypisuje się wektor trójskładnikowy (Vector3) o wartościach z zakresu od 0 do 1, co ułatwia późniejszą analizę. W przypadku trójkątów należy przyjąć kolejność wierzchołków przeciwną do ruchu wskazówek zegara (na poniższych ilustracjach strzałki wskazują tę kolejność; punkt początkowy nie ma znaczenia, choć zaleca się rozpoczynanie od 0 lub od prawego górnego rogu).
W dalszej kolejności punkty wymagają przekształcenia (translacji), aby umożliwić zmianę rozmiaru obiektu zgodnie z założonymi wymiarami. W tym celu stosuje się następujący wzór podstawowy:

0 = (-SegmentSize. * 0.5f) 1 = (SegmentSize. * 0.5f) 0.25f = (-SegmentSize. * 0.5f) 0.75f = (SegmentSize. * 0.5f) 0.1f = (SegmentSize. * 0.5f) for front And 0.1f = -(SegmentSize. * 0.5f) for back

Następnie należy dodać `SegmentPosition.x` dla współrzędnej x, `SegmentPosition.y` dla y oraz `SegmentPosition.z` dla z.
W przypadku segmentu z drzwiami, wartość (np. `DoorSize.x`) należy dodać do wierzchołków odpowiadających za drzwi – na przykład do wierzchołków o numerach 1, 3, 5 i 7. Z kolei w przypadku segmentu z oknem, odpowiednią wartość (np. `WindowSize.x`) należy dodać do wierzchołków o numerach 1, 3, 5 i 7.

Trujkąty:2, 1, 0, 2, 0, 3, 4, 7, 6, 6, 5, 4, Point Translation: 0 – ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y, (SegmentSize.z * 0.5f) + SegmentPosition.z), 1 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y, (SegmentSize.z * 0.5f) + SegmentPosition.z), 18 Daniel Szczypior 2- ((SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y, (SegmentSize.z * 0.5f) + SegmentPosition.z), 3 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y, (SegmentSize.z * 0.5f) + SegmentPosition.z), 4 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z), 5 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z), 6 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z), 7 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z)

Punkty:
0 - (0, 0, 0) 1 - (0.25, 0.25, 0) 2 - (0, 1, 0) 3 - (0.25, 0.75, 0) 4 - (1, 1, 0) 5 - (0.75, 0.75, 0) 6 - (1, 0, 0) 7 - (0.75, 0.25, 0) Point Translation: 0 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 1 - ((-WindowSize.x * 0.5f) + SegmentPosition.x, (-WindowSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 2 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 3 - ((-WindowSize.x * 0.5f) + SegmentPosition.x, (WindowSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 4 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 5 - ((WindowSize.x * 0.5f) + SegmentPosition.x, (WindowSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 6 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 7 - ((WindowSize.x * 0.5f) + SegmentPosition.x, (-WindowSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z)

Punkty:

0 - (0, 0, 0)
1 - (0.25, 0, 0)
2 - (0, 1, 0)
3 - (0.25, 0.25, 0)
4 - (1, 1, 0)
5 - (0.75, 0.25, 0)
6 - (1, 0, 0)
7 - (0.75, 0, 0)

Transformacja punktów:
0 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + 21 SegmentPosition.z) Daniel Szczypior 1 - ((-DoorSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 2 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 3 - ((-DoorSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + (DoorSize.y) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 4 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 5 - ((DoorSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + (DoorSize.y) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 6 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z) 7 - ((DoorSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y, (-SegmentSize.z * 0.5f) + SegmentPosition.z)

Przykład obliczania mapy UV
Obliczanie mapy UV jest zbliżone do obliczania wierzchołków, choć występują tu pewne różnice. Mapa UV wykorzystuje strukturę Vector2 (ponieważ jest dwuwymiarowa), więc współrzędna Z, obecna w danych wierzchołków, jest pomijana. Dla każdego wierzchołka należy wyznaczyć odpowiadający mu punkt UV. Dzięki uwzględnieniu pozycji segmentu, elementy umieszczone obok siebie tworzą płynne przejście (tzw. tiling), które bierze pod uwagę sąsiedni segment. W rezultacie, podczas przesuwania ściany, tekstury pozostają na swoim miejscu – sprawia to, że na ścianie nie widać łączeń, a całość prezentuje się estetycznie.
Płaski segment:

0 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x , (SegmentSize.y * 0.5f) + SegmentPosition.y), 1 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y), 2 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y), 3 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y), 4 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y), 5 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y), 6 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y), 7 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y)
Segment typu okno:
0 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y), 1 - ((-WindowSize.x * 0.5f) + SegmentPosition.x, (-WindowSize.y * 0.5f) + SegmentPosition.y), 2 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y), 3 - ((-WindowSize.x * 0.5f) + SegmentPosition.x, (WindowSize.y * 0.5f) + SegmentPosition.y), 4 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y), 5 - ((WindowSize.x * 0.5f) + SegmentPosition.x, (WindowSize.y * 0.5f) + SegmentPosition.y), 6 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y), 7 - ((WindowSize.x * 0.5f) + SegmentPosition.x, (-WindowSize.y * 0.5f) + SegmentPosition.y),
Segment typu drzwi:
0 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y), 1 - ((-DoorSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y), 2 - ((-SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y), 3 - ((-DoorSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + (DoorSize.y) + SegmentPosition.y), 4 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (SegmentSize.y * 0.5f) + SegmentPosition.y), 5 - ((DoorSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + (DoorSize.y) + SegmentPosition.y), 6 - ((SegmentSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y), 7 - ((DoorSize.x * 0.5f) + SegmentPosition.x, (-SegmentSize.y * 0.5f) + SegmentPosition.y),

Algorytm budowania

W pierwszej kolejności generowany jest zestaw losowych punktów w oparciu o określone kryteria, takie jak wymagana liczba pięter, liczba pomieszczeń na piętrze itp. Każdy punkt otrzymuje numer, począwszy od 0. Przyjęto założenie, że punkty tworzą pewnego rodzaju siatkę (np. prostokątną – sąsiednie punkty w poziomie muszą znajdować się na tej samej osi; to samo dotyczy pionu, co gwarantuje, że pomieszczenia będą miały kształt prostokąta). Poniżej znajduje się przykład wygenerowanej siatki.

Następnie algorytm losowo łączy punkty, tworząc pomieszczenia, przy założeniu, że muszą one mieć kształt prostokątny – na przykład punkty 0, 1, 3, 4 tworzą poprawny układ, natomiast punkty 0, 1, 7, 3 – nie. Łatwo jest określić, które ściany są zewnętrzne, a które nie; ponieważ ściany mogą powstawać wyłącznie w oparciu o tę siatkę, wiadomo, że odcinki 0–1 oraz 1–2 zawsze będą ścianami zewnętrznymi. Jeśli więc w budynku brakuje okien lub drzwi, powinny one zostać tam umieszczone. Poniższa ilustracja przedstawia przykład wygenerowanych pomieszczeń. Każdy kolor odpowiada innemu pomieszczeniu. Między punktami 6 i 7 nie ma ściany.

System „wie”, że w budynku, w oparciu o wygenerowane pomieszczenia, okna mogą znajdować się tylko na określonych ścianach. Wynika to z faktu, że ściany te nie są przesłonięte przez inne elementy konstrukcyjne – przykładowo, nie ma żadnego punktu znajdującego się powyżej punktu 0, poniżej punktu 9, na lewo od punktu 3 ani na prawo od punktu 5 itd.
1. 0, 1
2. 1, 4
3. 4, 5
4. 5, 8
5. 8, 7
6. 7, 10
7. 10, 9
8. 9, 3
9. 3, 0
Pozostaje jeszcze ustalenie, na których ścianach powinny znaleźć się drzwi. Na poniższej ilustracji widać punkt centralny każdego pomieszczenia; służy on do określenia wzajemnego położenia pomieszczeń (np. które z nich znajduje się powyżej, po lewej lub po prawej stronie innego), co znacząco upraszcza obliczenia. Wiadomo, że każde z pomieszczeń – oznaczone jako A, B oraz C – musi posiadać drzwi. Algorytm osiągalności pozwala sprawdzić, jak przemieszczać się między pomieszczeniami, co z kolei determinuje umiejscowienie drzwi na odpowiednich ścianach. Przykładowo, aby przejść z pomieszczenia A do B, należy utworzyć przejście na ścianie łączącej punkty 3 i 4; aby dostać się z C do B, drzwi powinny znaleźć się na ścianie między punktami 4 i 7. Ponadto, aby umożliwić wejście do budynku z zewnątrz, system może wygenerować dodatkowe drzwi na tych samych ścianach, na których mogą znajdować się okna. Dzięki porównaniu poszczególnych punktów w ramach całej struktury możliwe jest rozróżnienie ścian zewnętrznych od wewnętrznych.

Szczegółowe informacje oraz pełny kod projektu można znaleźć pod adresem: https://github.com/DanScallion/Proto-Segmentis