01Quick Start
Dwie ścieżki: sam shader (30 sekund, zero skryptów) albo pełny system ze sterowaniem globalnym i triggerami. Zacznij od pierwszej, przejdź na drugą kiedy potrzebujesz fali i skoków.
A. Sam shader — minimum
- Wrzuć na scenę Quada albo dołączony model
plate_crowd.fbx. - Stwórz materiał i ustaw shader
SerwusStudio/CrowdCheer. - Podepnij swój atlas w slot Crowd Texture Atlas.
- Ustaw Columns in Atlas = liczba wariantów postaci (poziomo) i Anim Rows in Atlas = liczba klatek animacji (pionowo).
- Pokręć Tiling X / Y — tyle postaci zmieści się na płaszczyźnie.
Gotowe. Postacie animują się same, z losowym wariantem i losowym przesunięciem fazy. Żadnych skryptów.
B. Pełny system — zalecane
- Rozstaw prefaby
plate_crowdtam, gdzie ma być publiczność (trybuny, sektory, widownia). - Dodaj
CrowdControllerdo każdej płaszczyzny i ustaw per-plane: atlas, kolumny, wiersze, tiling, offset. - Stwórz pusty GameObject i dodaj
CrowdManager. Kliknij Auto Collect All — zbierze wszystkie kontrolery ze sceny. - Steruj całym tłumem z Managera: gęstość, prędkość animacji, fade, kolor, losowość, jakość.
- Opcjonalnie dodaj
CrowdWaveTrigger— fala i skok z klawisza, przycisku UI albo collidera. Bez pisania kodu.
Unity 2020.3 LTS lub nowszy, Universal Render Pipeline (URP 10+). Shader jest pisany ręcznie w HLSL — Shader Graph nie jest potrzebny. Wszystkie komponenty mają [ExecuteAlways], więc tłum widać w edytorze bez wchodzenia w Play.
02Jak to działa
Tłum renderuje się w całości na GPU. Nie ma GameObjectu na postać, nie ma Animatorów, nie ma skinowanych meshy. Jedna płaska płaszczyzna wyświetla kilkadziesiąt animowanych postaci przez tilowanie atlasu tekstur — jeden draw call niezależnie od tego, ile postaci widać.
Ścieżka fragmentu
- Clip bounds — jeśli włączone, fragmenty poza prostopadłościanem world-space odpadają natychmiast.
- Distance fade — odległość do kamery liczona pierwsza; poza Fade End fragment jest odrzucany, zanim cokolwiek zostanie policzone. To jest oszczędność fillrate, nie tylko efekt wizualny.
- Tiling — UV mnożone przez Tiling X/Y, część całkowita to
tileID(numer kafla = numer postaci), część ułamkowa to lokalne UV wewnątrz kafla. - Plane seed — ziarno losowości liczone z pivota obiektu (
unity_ObjectToWorld). Stałe dla całego mesha, różne dla każdej płaszczyzny na scenie — dwie identyczne płyty obok siebie pokażą inny tłum. - Person ID —
planeSeed + tileID + RandomSeed. Z tego jednego hasha wychodzi: czy kafel jest zajęty (gęstość), który wariant postaci, czy odbita w poziomie, jakie przesunięcie fazy animacji, jaki odcień tintu. - Animacja —
frame = floor(_Time.y * AnimSpeed + timeOffset), modulo liczba wierszy. Każda postać ma własnytimeOffset, więc nikt nie klaszcze w rytm sąsiada. - Fala / La Ola — pozycja world-space rzutowana na kierunek fali; jeśli fragment mieści się w paśmie Wave Width, wiersz animacji jest podmieniany na Wave Pose. Przy Wave Speed = 0 cały blok jest pomijany — zero kosztu.
- Sampling i maska — UV atlasu z paddingiem, miękka maska krawędzi (Edge Smoothness), alpha cutout.
Cała losowość jest deterministyczna — ten sam hash daje ten sam wynik w każdej klatce i na każdej maszynie. Tłum nie miga, puste miejsca nie skaczą, a scena wygląda tak samo w edytorze i w buildzie. Żeby przetasować układ, zmień Random Seed.
Passy
Shader ma dwa passy: ForwardLit (kolor, oświetlenie z głównego światła kierunkowego + ambient SH) oraz DepthOnly. Oba dzielą tę samą funkcję CrowdTile(), więc głębia zawsze zgadza się z tym, co widać. Kolejka AlphaTest z ZWrite On — bez sortowania przezroczystości.
03Atlas tekstur
Atlas to równa siatka. Kolumny to warianty postaci (kolor koszulki, sylwetka), wiersze to klatki animacji odtwarzane po kolei z góry na dół.
Atlas 4 × 4 → 4 warianty postaci × 4 klatki animacji. Z włączonym Random Flip X wygląda jak 8 wariantów, bez dodatkowej pamięci.
Wymagania
- Wszystkie komórki tej samej wielkości, siatka bez przerw.
- Tło w pełni przezroczyste (alpha = 0) — cutout tego wymaga.
- Kilka pikseli przezroczystego marginesu wokół każdej postaci — inaczej sąsiednie komórki będą się wzajemnie podbierać przy filtrowaniu.
- PNG lub TGA z kanałem alpha, rozdzielczość potęga dwójki (512², 1024², 2048²).
- Na mobile / VR: Crunch compression w ustawieniach importu, 512² lub 1024² w zupełności wystarcza.
- W materiale ustaw Columns in Atlas i Anim Rows in Atlas dokładnie tak, jak wygląda siatka. Niezgodność = pocięte postacie.
Pola Tiling/Offset Unity są dla tej tekstury ukryte ([NoScaleOffset]) — świadomie. Używaj sekcji UV Tiling w materiale albo pól na CrowdController; dają precyzyjniejszą kontrolę i działają razem z per-plane MaterialPropertyBlock.
04Komponenty
Trzy komponenty runtime i dwa custom inspektory. Kluczowa zasada: CrowdManager jest opcjonalny. CrowdController działa samodzielnie — Manager tylko przejmuje nad nim sterowanie globalne.
CrowdController
Komponent per-płaszczyzna. [ExecuteAlways], [DisallowMultipleComponent], wymaga Renderer na tym samym obiekcie. Wszystkie wartości idą do shadera przez MaterialPropertyBlock — współdzielony materiał nigdy nie jest modyfikowany, więc nie powstają instancje materiału i GPU instancing zostaje zachowany.
- Per-plane: atlas, liczba kolumn i wierszy, Tiling X/Y, Offset X/Y, clip bounds.
- Globalne: gęstość, prędkość animacji, fade, kolor, losowość, jakość, parametry fali — sterowane z Managera, edytowalne lokalnie gdy Managera nie ma.
- Brak Update i LateUpdate. Wartości lecą do GPU tylko przy zmianie. Zerowy koszt CPU na klatkę.
CrowdManager
Komponent na poziomie sceny, zwykle jeden. Jeden suwak zmienia gęstość na wszystkich płaszczyznach naraz. Trzyma listę kontrolerów (Auto Collect zbiera je automatycznie na Awake) i przy każdej zmianie robi jeden batchowany SetGlobalParams() na kontroler — nie jedno wywołanie na właściwość.
- Globalne: Density, Anim Speed, Fade Start/End, Tint Color, Tint Variation, Random Seed, Flip, Edge Smoothness, Alpha Clip.
- Fala: kierunek, prędkość, szerokość pasa, wiersz pozy dopingu, czas trwania (0 = w nieskończoność).
- Skok: wysokość, czas trwania, propagacja falowa i opóźnienie na jednostkę świata.
CrowdWaveTrigger
Odpala falę i skok bez pisania kodu. Wymaga referencji do CrowdManagera. Trzy tryby, można łączyć:
- Klawisz — Trigger On Key Press + wybrany klawisz (domyślnie Space).
- Przycisk UI — w
onClickprzeciągnij obiekt i wybierzTriggerWave()lubTriggerJump(). - Strefa collidera — Collider z Is Trigger, opcjonalny filtr po tagu (domyślnie
Player).
Do tego Cooldown (minimalna przerwa między falami — blokuje spam) oraz dwa UnityEventy: On Wave Triggered i On Wave Blocked. Podepnij pod nie dźwięk tłumu, wibrację pada albo cokolwiek innego.
Custom inspektory
Oba komponenty mają własne inspektory z foldoutami. CrowdControllerEditor wykrywa obecność Managera na scenie: sekcje globalne stają się read-only (żeby nie edytować w dwóch miejscach naraz), a per-plane zostają zawsze aktywne. Ostrzega o braku Renderera, braku materiału i o złym shaderze. CrowdManagerEditor daje licznik kontrolerów, przyciski Auto Collect All / Apply Globals Now oraz Trigger Wave / Jump i Stop w trybie Play. Waliduje też, czy Fade Start jest mniejszy od Fade End.
05Scripting API
Namespace SerwusStudio. Wszystko, co ustawiasz w inspektorze, ma odpowiednik w kodzie — settery same wysyłają wartości do GPU, nie trzeba nic „odświeżać".
CrowdManager — właściwości
| Składowa | Typ | Opis |
|---|---|---|
| DensityLevel | float 0–1 | Gęstość tłumu na wszystkich płaszczyznach. |
| AnimSpeed | float 0.1–10 | Prędkość animacji w klatkach na sekundę. |
| FadeStart | float 0–200 | Odległość początku zanikania. |
| FadeEnd | float 0–500 | Odległość pełnego zaniku (dalej fragmenty są odrzucane). |
| TintColor | Color | Globalny kolor bazowy tłumu. |
| TintVariation | float 0–1 | Wariacja ciepły/zimny per postać. |
| RandomSeed | float 0–100 | Ziarno losowości — zmiana przetasowuje cały tłum. |
| FlipEnabled | bool | Losowe odbicie lustrzane ~50% postaci. |
| Smoothness | float 0–0.3 | Miękkość krawędzi kafla. |
| AlphaClip | float 0–1 | Próg alpha cutout. |
| JumpHeight | float 0.05–5 | Wysokość skoku w jednostkach świata. |
| JumpDuration | float 0.1–3 | Czas trwania skoku jednej płaszczyzny. |
| Controllers | IReadOnlyList | Lista zarejestrowanych kontrolerów (tylko odczyt). |
| IsWaveActive | bool | Czy fala aktualnie leci (tylko odczyt). |
CrowdManager — metody
| Metoda | Opis |
|---|---|
| AutoCollect() | Znajduje wszystkie CrowdControllery na scenie i wysyła im globalne ustawienia. |
| RegisterController(c) | Dopisuje kontroler do listy i od razu przekazuje mu globalne — dla płaszczyzn spawnowanych w runtime. |
| UnregisterController(c) | Usuwa kontroler z listy (wywołaj przed zniszczeniem obiektu). |
| ApplyGlobals() | Wypycha wszystkie globalne ustawienia do każdego kontrolera. |
| TriggerWave() | Fala z ustawieniami z inspektora. Bezpieczne dla onClick przycisku UI. |
| TriggerWave(angle, speed, width, waveRow, duration) | Fala z własnymi parametrami. duration = 0 → leci w nieskończoność. |
| TriggerWaveFrom(worldOrigin) | Fala wychodząca OD punktu w świecie — np. od bramki po golu. |
| TriggerWaveToward(worldTarget) | Fala zbiegająca DO punktu — np. w stronę sceny. |
| StopWave() | Natychmiast zatrzymuje falę na wszystkich płaszczyznach. |
| TriggerJump() | Skok wszystkich płaszczyzn. Z włączoną propagacją falową każda skacze z opóźnieniem wg pozycji. Tylko w Play. |
| TriggerJumpFrom(worldOrigin) | Skok promieniowy od punktu — bliższe płaszczyzny skaczą pierwsze. Tylko w Play. |
| StopJump() | Zatrzymuje skoki i przywraca pozycje. |
CrowdController — metody
| Metoda | Opis |
|---|---|
| Apply() | Wysyła wszystkie wartości do MaterialPropertyBlock. Wywoływane automatycznie przy każdej zmianie. |
| SetGlobalParams(…) | Ustawia 10 globalnych parametrów naraz i robi jedno Apply(). Używane przez Managera. |
| SetWaveParams(angle, speed, width, row) | Ustawia parametry fali naraz i robi jedno Apply(). |
| TriggerWave(duration = 5f) | Jednorazowa fala na tej płaszczyźnie. Jeśli Wave Speed = 0, przyjmuje 2. Tylko w Play. |
| StopWave() | Zatrzymuje falę, zeruje Wave Speed. |
| TriggerJump(height, duration, delay = 0f) | Podskok po krzywej sinusoidalnej: y = sin(t · π) · height. Opóźnienie służy propagacji. Tylko w Play. |
| StopJump() | Zatrzymuje skok i przywraca pozycję wyjściową. |
| SetClipBounds(min, max) | Włącza i ustawia prostopadłościan przycinający w world space. |
| SetClipBoundsFromRenderer() | Ustawia bounds z aktualnych granic Renderera. |
| ClearClipBounds() | Wyłącza przycinanie. |
CrowdWaveTrigger — metody
| Metoda | Opis |
|---|---|
| TriggerWave() | Fala z uwzględnieniem cooldownu. Jeśli włączone Also Trigger Jump — odpala też skok. |
| TriggerJump() | Sam skok, bez fali. Też z cooldownem. |
| TriggerWaveFromThisPosition() | Fala wychodząca z pozycji tego obiektu. |
| StopWave() | Zatrzymuje falę na Managerze. |
Przykłady
using SerwusStudio; public class MatchEvents : MonoBehaviour { public CrowdManager crowd; public Transform homeGoal; // gol — tłum wariuje: szybsza animacja, fala od bramki, skok public void OnGoalScored() { crowd.AnimSpeed = 8f; crowd.TriggerWaveFrom(homeGoal.position); crowd.TriggerJump(); Invoke(nameof(CalmDown), 6f); } void CalmDown() => crowd.AnimSpeed = 2f; // sektory drużyn — dwa Managery albo dwa zestawy materiałów public void SetHomeColors() { crowd.TintColor = new Color(1f, 0.82f, 0.82f); crowd.TintVariation = 0.25f; } // stadion pustoszeje po meczu IEnumerator EmptyStadium(float seconds) { float t = 0f; while (t < seconds) { t += Time.deltaTime; crowd.DensityLevel = Mathf.Lerp(1f, 0f, t / seconds); yield return null; } } // profil jakości pod VR / Quest public void ApplyVRProfile() { crowd.FadeStart = 30f; crowd.FadeEnd = 60f; } }
Płaszczyzny tworzone w runtime rejestruj ręcznie — dostaną wtedy komplet globalnych ustawień:
var plane = Instantiate(crowdPrefab, position, rotation); crowd.RegisterController(plane.GetComponent<CrowdController>()); // przy usuwaniu crowd.UnregisterController(controller); Destroy(controller.gameObject);
Jeśli wolisz sterować bezpośrednio materiałem, wszystkie właściwości są dostępne przez Material.SetFloat / SetColor — nazwy w tabeli w sekcji 06. Pamiętaj tylko, że modyfikacja współdzielonego materiału tworzy jego instancję; komponenty robią to poprawnie przez MaterialPropertyBlock.
06Parametry shadera
Kompletna lista właściwości shadera SerwusStudio/CrowdCheer — nazwa w inspektorze, nazwa właściwości do kodu, zakres i wartość domyślna.
| Właściwość | W inspektorze | Zakres / domyślna | Opis |
|---|---|---|---|
| _MainTex | Crowd Texture Atlas | Texture2D | Atlas sprite'ów: kolumny = warianty, wiersze = klatki. Tiling/Offset Unity ukryte — użyj sekcji UV Tiling. |
| _AnimSpeed | Animation Speed (FPS) | 0.1–10 · 2 | Tempo przewijania klatek. 1–2 spokojny tłum, 3–5 doping, 6–10 szybkie klaskanie. |
| _AnimRowCount | Anim Rows in Atlas | 1–8 int · 4 | Liczba wierszy klatek w atlasie. Musi zgadzać się z teksturą. |
| _ColumnsCount | Columns in Atlas | 1–16 int · 4 | Liczba kolumn wariantów postaci. Musi zgadzać się z teksturą. |
| _TilingX | Tiling X | 0.1–50 · 1 | Ile postaci mieści się na szerokość płaszczyzny. |
| _TilingY | Tiling Y | 0.1–50 · 1 | Ile rzędów postaci na wysokość płaszczyzny. |
| _OffsetX | Offset X | -1–1 · 0 | Przesunięcie UV w poziomie — dostrojenie do geometrii. |
| _OffsetY | Offset Y | -1–1 · 0 | Przesunięcie w pionie — ustawienie „linii stóp" na krawędzi siedzeń. |
| _RandomSeed | Random Seed | 0–100 · 0 | Globalne przetasowanie wariantów bez ruszania geometrii. |
| _FlipEnabled | Random Flip X | toggle · on | Lustrzane odbicie ~50% postaci. Podwaja różnorodność za darmo. Wyłącz przy numerach na koszulkach. |
| _DensityLevel | Density Level | 0–1 · 1 | Procent zajętych miejsc. 1 = komplet, 0.3 = rzadka publika, 0 = pusto. Deterministyczne — nie miga. |
| _FadeStart | Fade Start | 0–200 · 50 | Odległość, od której tłum zaczyna zanikać. |
| _FadeEnd | Fade End | 0–500 · 100 | Odległość pełnego zaniku. Dalej fragmenty odrzucane — realna oszczędność fillrate. |
| _TintColor | Base Tint | Color · white | Globalny mnożnik koloru. Barwy klubowe, nocne chłodne światło, zachód słońca. |
| _TintVariation | Tint Variation | 0–1 · 0 | Ciepły/zimny odcień per postać. Już 0.1–0.2 wyraźnie ożywia tłum przy małej liczbie kolumn. |
| _WaveSpeed | Wave Speed | 0–10 · 0 | Prędkość fali. 0 = wyłączona (blok pomijany w shaderze). 1–2 dostojna, 3–5 naturalna, 6–10 szybka. |
| _WaveRow | Wave Pose | 0–7 int · 2 | Wiersz atlasu z pozą dopingu (ręce w górze). Indeksowanie od 0. |
| _WaveWidth | Wave Width | 0.5–30 · 5 | Szerokość pasa fali w jednostkach świata — ile osób doping robi jednocześnie. |
| _WaveAngle | Wave Direction | 0–360 · 0 | Kierunek propagacji w stopniach. 0° = +X, 90° = +Z, 180° = −X, 270° = −Z. |
| _Smoothness | Edge Smoothness | 0–0.3 · 0.05 | Miękkie wygaszenie krawędzi kafla. Widzisz czarne linie między postaciami? Podnieś do 0.05–0.1. |
| _AlphaClip | Alpha Clip Threshold | 0–1 · 0.3 | Próg odcięcia alpha. Twarde krawędzie atlasu: 0.3–0.5. Miękkie, antyaliasowane: 0.1–0.2. |
| _ClipBoundsEnabled | Enable Clip Bounds | toggle · off | Przycinanie do prostopadłościanu world-space. Domyślnie wyłączone — zero kosztu, gdy nieużywane. |
| _ClipBoundsMin / Max | Clip Bounds Min / Max XYZ | Vector3 | Narożniki bryły przycinającej w przestrzeni świata. |
07Clip Bounds
Płaskie płaszczyzny tłumu na zakrzywionych trybunach lubią się przecinać — róg jednej płyty wystaje zza drugiej albo przebija barierkę. Clip Bounds rozwiązuje to bez cięcia geometrii: fragmenty poza zadanym prostopadłościanem world-space są po prostu odrzucane.
- Włącz Enable Clip Bounds na
CrowdControlleri ustaw narożniki Min/Max. - Zaznaczony obiekt rysuje pomarańczowy gizmo bryły — widać dokładnie, co zostanie przycięte.
SetClipBoundsFromRenderer()ustawia bryłę na aktualne granice Renderera — dobry punkt startowy do ręcznego zawężenia.- Test jest pierwszą rzeczą w shaderze, więc przycięte fragmenty nie kosztują nic więcej.
// przytnij tłum do bryły sektora controller.SetClipBounds(sector.bounds.min, sector.bounds.max); // wyjściowo: dopasuj do własnych granic mesha, potem zawęź ręcznie controller.SetClipBoundsFromRenderer(); // wyłącz controller.ClearClipBounds();
08Wydajność i VR
System jest lekki z założenia, ale wąskim gardłem zawsze będzie fillrate — liczba pikseli ekranu pokrytych płaszczyznami tłumu. Wszystko poniżej sprowadza się do tego, żeby ich było mniej.
Co jest darmowe z definicji
- Zero kosztu CPU na klatkę — żaden komponent nie ma Update ani LateUpdate. Dane lecą do GPU tylko przy zmianie.
- MaterialPropertyBlock — brak duplikacji materiałów, GPU instancing zachowany.
- Batchowane Apply() — zmiana globalna to jedno wywołanie na kontroler, nie jedno na właściwość.
- Fala kosztuje zero, gdy śpi — przy Wave Speed = 0 shader nie wchodzi w blok fali.
- Alpha cutout, nie transparency — kolejka AlphaTest z ZWrite On, bez sortowania przezroczystości.
VR / Quest / mobile
- Distance Fade agresywnie. Na Queście Fade Start 30, Fade End 60. Na mobile 40 / 80. Odległe trybuny nic nie wnoszą, a kosztują fillrate.
- Mniejszy tiling na dalekich płytach. Płaszczyzna 40 metrów od kamery nie potrzebuje 10×10 postaci — 3×3 wygląda tak samo.
- Atlas 512² lub 1024² z Crunch compression. Postaci ogląda się z dystansu, nie widać różnicy.
- Włącz GPU Instancing w materiale, jeśli używasz wielu płaszczyzn z tym samym materiałem.
- Single Pass Instanced działa. Shader ma pełne wsparcie stereo:
UNITY_VERTEX_INPUT_INSTANCE_ID,UNITY_VERTEX_OUTPUT_STEREO,UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX. Testowane na Quest 2 i Quest 3.
PC i konsole
Można pozwolić sobie na atlas 2048², Fade End 200+ i gęsty tiling. Oświetlenie to główne światło kierunkowe plus ambient SH — realistyczna reakcja na scenę przy minimalnym koszcie.
Kolejność strojenia przy spadkach FPS: najpierw Fade End, potem tiling na dalekich płytach, dopiero na końcu rozdzielczość atlasu. Pierwsze dwa dają największy zysk, bo bezpośrednio zmniejszają liczbę renderowanych pikseli.
09FAQ
TriggerWave() tworzy korutynę, więc działa tylko w trybie Play.10Changelog
Major update — Crowd Cheer to teraz pełny system skryptowy, nie tylko shader. Nowe: CrowdManager / CrowdController / CrowdWaveTrigger, runtime API (TriggerWave, TriggerJump…), efekt skoku/bounce z propagacją falową, world-space clip bounds, dwie sceny demo (Koncert, Stadion), custom inspektory z podglądem, rozszerzona dokumentacja EN + PL. Zmiana: seeding losowości oparty o pivot mesha (stabilniejszy). Usunięto: parametr „Randomize Scale".
Pierwsze wydanie. Atlasowy shader tłumu na GPU dla URP (bez riggowanych meshy), sterowanie materiałem (gęstość, prędkość, UV tiling, losowość, distance fade, tint, edge smoothness, alpha clip), wbudowane parametry fali La Ola, scena demo, prefaby, przykładowe materiały i tekstury, dokumentacja EN + PL.