Wczoraj siedziałem sobie na ławeczce w parku, grzejąc się do słoneczka i relaksując się w otoczeniu kolorowych, szumiących drzew, rozmyślałem na temat różnych ficzerów, które docelowo chciałbym umieścić w swojej grze. Jednym z nich jest dźwięk przestrzenny, którego dość prosty silnik zamierzam zaimplementować. I tutaj być może istnieje pewien nieco paradoksalny fenomen, którego na razie nie jestem w stanie sprawdzić w praktyce (silnik dźwięków będę implementował najwcześniej za rok). Na razie to kwestia czysto filozoficzna.
Docelowy silnik graficzny w mojej grze, czyli ten odpowiedzialny za obsługę trójwymiarowej mapy, będzie renderowany w formie rzutu ortograficznego, co da efekt podobny do retro gier z gatunku action adventure (jak często wspominana przez mnie Zelda z konsoli SNES). Teren ze swoją zawartością będzie na ekranie widoczny z jednej strony jako płaski (dzięki braku wsparcia perspektywy), ale z drugiej strony jako trójwymiarowy (dzięki oświetleniu/światłocieniowi). Koniec końców, efekt widoczny na ekranie będzie bliski przestrzeni dwuwymiarowej. To powoduje, że trzeci wymiar, czyli pozycja w osi Z
(upraszczając, pozycja obiektu nad gruntem) będzie miała niewielkie znaczenie dla systemu dźwięków.
Tak więc istotne dla systemu dźwięku przestrzennego będą dwie osie (osie X
i Y
), co znacznie lepiej pasuje do dźwięku stereo — dwie osie kierunków dla dwóch kanałów audio.
Aby stworzyć prosty dźwięk przestrzenny, wystarczy odpowiednio operować głośnością lewego i prawego kanału. Im dalej od bohatera znajduje się źródło dźwięku, tym bardziej należy przyciszyć oba kanały. Druga sprawa — im bardziej po lewej stronie znajduje się źródło, tym ciszej musi grać prawy kanał (i vice versa). Tutaj obsługuje się dwie właściwości — dystans od źródła dźwięku oraz kierunek, z którego dźwięk nadlatuje, obie funkcje za pomocą jednego suwaka. Do tego SDL2 posiada funkcję Mix_SetPanning
. Oczywiście są też funkcje Mix_SetPosition
oraz Mix_SetDistance
, ale ich użycia na razie nie rozważam, bo skupiam się na aspektach niskopoziomowych i zrozumieniu problemu.
Powyższe załatwia nam na razie jedną oś — lewo i prawo, jako głośność lewego i prawego kanału. Ale to dopiero pierwszy krok.
Dźwięk przestrzenny to nie tylko panning. Człowiek inaczej słyszy dźwięk jeśli jego źródło jest przed nim, a inaczej jeśli jest za nim. Dodatkowo, inaczej też słyszy dźwięk jeśli jego źródło jest wysoko, a inaczej jeśli nisko (uwzględniając stałe położenie głowy) — to jednak oddrzucam, dlatego że nie ma zastosowania w moim silniku, ze względu na rzut ortograficzny.
Skupiając się na kierunku przód-tył — tego nie da się rozwiązać panningiem, tutaj potrzebne jest filtrowanie sygnału. Jeśli dźwięk dochodzi z przodu słuchacza, nie filtrujemy sygnału, a im bardziej ucieka do tyłu, dla przykładu tym bardziej go tłumimy (np. silniej obcinamy wysokie częstotliwości). Dźwięk dochodzący całkiem z tyłu słuchacza powinien się znacząco różnić od tego dobiegającego od frontu — musi być wyraźna różnica w jakości, ale nie jakoś przesadnie duża.
Filtrowanie również stosujemy dla kierunków lewo-prawo — im bardziej źródło dźwięku ucieka na lewo, nie tylko tym mocniej przyciszamy prawy kanał, ale też mocniej go filtrujemy. To da jeszcze większą głębię, większą różnorodność i łatwość w określaniu kierunku, z którego dźwięk dochodzi.
Nie wiem jeszcze dokładnie w jaki sposób w SDL2 można zaimplementować przetwarzanie kanałów na żywo (ale mam na to dużo czasu, więc nie ma spiny). Jednak po dość pobieżnym przeglądnięciu API, wygląda na to, że służą do tego efekty i callbacki, w których dostarczane są bufory do obróki, przed ich ostatecznym zmiksowaniem i wysłaniem do karty dźwiękowej. A skoro tak, to implementacja całego silnika dźwiękowego w opracowanej formie nie będzie mocno skomplikowana (choć wymagać pewnie będzie nastukania kilku tysięcy linijek, ale to nie jest dla problemem).
Opisane wyżej manipulacje głośnością dźwięków oraz ich filtrowanie, da co prawda pewną ich przestrzenność, ale można temat jeszcze pociągnąć i dodać więcej prostych ficzerów, jeszcze bardziej urozmaicających udźwiękowienie i dodających mu jeszcze większej głębi. Rozważam nad jeszcze kilkoma efektami:
Tutaj mam zagwozdkę — czy mogę podejść do tematu luźno i stworzyć dowolne filtry przetwarzające sygnały dźwiękowe, aby uzyskać dobry efekt przestrzenny, czy muszę skorzystać z jakichś konkretnych, bo inaczej to stworzona przestrzenność będzie wybijać z imersji lub wręcz irytować gracza?
Ludzie potrafią się szybko przyzwyczajać to dziwnych, wręcz topornych mechanizmów istniejących w grach. Czy powszechne przyzwyczajenie do dźwięku stereo powoduje, że gra z dźwiękiem mono przeszkadza? IMO nie przeszkadza. Czy przyzwyczajenie do responsywnego sterowania uniemożliwia granie w gry z bardziej topornym sterowaniem i powoduje, że gracz odbija się i nie jest w stanie go opanować? IMO też nie, bo jest masa gier z gównianym sterowaniem, do którego gracze — choć z początku negatywnie nastawieni — są w stanie przywyknąć i go z biegiem czasu wymasterować.
Do czego dążę — czy ludzie przyzwyczajeni do naturalnych, przestrzennych dźwięków z życia oraz rewelacyjnych systemów dźwiękowych w grach AAA są w stanie przywyknąć do dźwięku przestrzennego, który de facto jest przestrzenny, ale który wykorzystuje filtrowanie tylko podobne do naturalnego lub wręcz w ogóle z nim niezgodne? Czy będą w stanie przyzwyczaić się do niego tak szybko jak np. do topornego sterowania czy specyfinczej oprawy graficznej?
Jestem bardzo ciekaw, czy w temacie filtrowania mam pewną dowolność, czy jednak muszę iść w kierunku jak najdokładniejszego odwzorowania jakości dźwięku istniejącego w prawdziwym otoczeniu. Czy mogę — brzydko pisząc — na pałę wybrać sobie jakiekolwiek sensowniejsze filtrowanie dla lewo-prawo i przód-tył, nie psując graczowi odbioru gry? Czy istnieje tutaj paradoks, pewien fenomen związany z ludzkimi zdolnościami adaptacyjnymi, czy nie istnieje?
Za pomocą panningu oraz prostego filtrowania kanałów, można bardzo łatwo uzyskać namiastkę przestrzenności dźwięku. Pierwsze co należy zrobić to oczywiście zaimplementować silnik dźwiękowy w taki sposób, aby obsłużyć głośność kanałów stereo oraz dać możliwość filtrowania każdego dzięku, którego źródło znajduje się wewnątrz świata gry (dźwięki UI powinny być odtwarzane w sposób standardowy).
Wszystko to musi być zaprogramowane w taki sposób, aby dało się ten system kalibrować. Implementację należy uprościć tak, aby za pomocą kilku stałych (np. znormalizowanych floatów jako mnożników/thresholdów) móc łatwo manipulować siłą wyciszenia dźwięku wraz ze wzrostem odległości od słuchacza oraz siłą filtrowania. Po określeniu idealnych wartości tych stałych, więcej nie trzeba będzie tych stałych dotykać (w końcu kalibruje się raz na zawsze).
Ustawianie głośności, panningu i filtrowanie związane z kierunkami, z którym dochodzą dźwięki, nie jest trudne. Callback musi kuknąć do mapy, sprawdzić odległość i kąt względem słuchacza, a to pikuś. Tłumienie względem dużych przeszkód, znajdujących się pomiędzy źródłem dźwięku a słuchaczem też nie wydaje się trudne — prosty ray casting ze źródła dźwieku w stronę bohatera, detekcja duzych obiektów i wyliczenie znormalizowanego współczynnika wytłumienia. Echo będzie znacznie trudniejsze, bo o ile łatwo jest sprawdzić czy źródło dźwięku znajduje się w zamkniętym obiekcie, o tyle trudniej będzie napisać filtr nakładający echo na próbkę. Podobnie z efektem Dopplera, bo tutaj zmienić należy nie jakość dźwięku, a jego częstotliwość. Ale hej, Google na pewno da odpowiedzi. :D
Coż, temat ten od jakiegoś czasu nie daje mi spokoju. Trudno mi zaspokoić ciekawość, bo prócz możliwości dogłębnego przemyślenia tematu, nie mam na razie możliwości sprawdzenia tych teorii. Oczywiście mógłbym się pobawić różnymi narzędziami i zrobić sobie prototyp, jednak czym innym jest oderwany od projektu prototyp, a czym innym gotowa implementacja do zabawy w docelowym silniku.
Na razie więc kwestia przestrzenności dźwięku oraz teorii domniemanej dowolności w filtrowaniu pozostanie zagadką. :]
@nie_tak_wiele_postow: po pierwsze, zapoznaj się ze znaczeniem słowa „spam”, bo nie wiesz co ono oznacza. Po drugie, na swoim blogu mogę pisać co chcę i ile chcę i z tego prawa korzystam (każdy użytkownik 4p ma możliwość prowadzenia bloga tutaj). Po trzecie, powyższe jest wpisem blogowym (dlatego jest na mikroblogach), natomiast forum służy do dyskusji, nie do umieszczania artykułów. Po czwarte — nie chcesz to nie czytaj, nie masz obowiązku.
@nie_tak_wiele_postow: to, że zamieszcza wiele wpisów nie znaczy, że spamuje. Są to wpisy techniczne, interesujące, obszernie coś wyjaśniające. Nie podoba Ci sie? To po prostu nie czytaj.
Jutro zabieram się za kod odpowiedzialny za framerate — szykuje się kolejny wpis w niedalekiej przyszłości. ;)
@furious programming: dawaj kolejne wpisy i nie przejmój się miałczeniem randomów ;-)
Wydaje mi się, że namęczysz się strasznie z tym dźwiękiem przestrzennym, a różnica i tak będzie ledwo słyszalna.
@Manna5: namęczyłem się strasznie np. z mapperem inputu. No i mam teraz API, dzięki któremu gracz będzie mógł mapować input tak jak będzie tego chciał, do takich urządzeń jakie akurat ma i dodatkowo go kalibrować, zmieniać thresholdy, czułość itd.
Ten wpis jest długi i sprawia wrażenie, że strasznie to wszystko skomplikowane. Ale popatrz na to z innej strony — połowę roboty związanej z przestrzennym dźwiękiem załatwię funkcją Mix_SetPanning
, a drugą połowę własnym callbackiem rejestrowanym raz za pomocą Mix_RegisterEffect
. Co będę musiał napisać? Tylko jeden callback, który pobierze sobie pozycję dwóch obiektów (ew. kamery), wykona prostą arytmetykę w celu określenia dystansu i odległości, a następnie przetworzy tablicę intów/floatów (czyli próbkę wejściową) w jakiś ~dowolny sposób (który od razu może ustawiać panning, skoro już ma wszystkie potrzebne dane). Tak więc całą funkcjonalność przestrzennego dźwięku można zapisać w kilkudziesięciu linijkach kodu, w ramach jednego callbacku. :]
Jedyne co będzie bardziej skomplikowane to mikro-silnik zarządzający dźwiękami przypisanymi do poszczególnych kanałów miksera audio (dwa kanały stereo, ale setki kanałów do miksowania dźwieków). Rzecz taka jak tłumienie dźwięku dochodzącego zza przeszkody to prosty ray casting w octree. I tak mi się to przyda, np. na potrzeby AI, aby sprawdzić, czy jakiś potworek widzi gracza. Pewne rzeczy będą tak czy siak implementowane, więc sam silnik dźwięków będzie jedynie nieco rozwiniętą listą z informacjami na temat aktualnie miksowanych dźwięków.
Jonathan Blow w wywiadzie do filmu Indie Game: The Movie mówił, że błędem jest tworzenie gry indie w taki sposób, aby dbać o każdy szczegół i maksymalnie windować jakość (bo zwykle życia na to braknie). I to jest prawda, jednak to nie oznacza automatycznie, że trzeba iść po linii najmniejszego oporu i rezygnować z ficzerów, dzięki którym dana gra może się mocno wyróżniać z ogółu i przyciągać smaczkami uwagę publiki. Dlatego też nie przejmuję się jakoś szczególnie jakością kodu, natomiast nie zrezygnuję ani z przestrzennego dźwięku, ani z raytracingu, choćbym ten silnik miał pięć lat programować. :D
@core1983: tworzenie tej gry jest dla mnie dość trudne, ze względu na spore ciśnienie z zewnątrz. Bo o ile z kodem zawsze sobie jakoś poradzę, a w przypadku dużych problemów można iść w kompromisy, o tyle ze stresem powodowanym przez otoczenie (w tym najbliższe) jest dużo trudniejsza sprawa. Krytyka pomaga iść dalej i wyciągać przydatne wnioski (np. taka jak ze strony @Manna5), jednak ciągłe docinki związane z typem projektu, z używaną technologią (Pascal to przecież obiekt drwin) i wieloma innymi aspektami (w tym osobistymi, czasem uderzającymi w prywatność/intymność) potrafią skutecznie odebrać motywację i chęć do pracy.
Niestety od tego nie da się odciąć (minęło 10 lat i nadal jest ~tak samo), a kiedy zacznę nagrywać materiały wideo na temat prac nad tym projektem (za kilka miechów), to będzie tylko gorzej i coraz trudniej pod względem psychologicznym. Tym bardziej, że jestem potężnym introwertykiem i wystawianie się na publiczną krytykę bywa dla mnie trudne i stresujące. Dlatego też trzeba z tym walczyć i nie poddawać się, nawet jeśli nie ma się wokół siebie nikogo kto by okazał wsparcie, poklepał po ramieniu i zmotywował do dalszych prac.
@furious programming: spoko, jak się robi swój game engine i to w dodatku w języku, który nie jest pierwyszm wyborem w game devie ;-) Ja tam robiłem małą gierkę w unity i przy praktycznie gotowych assetach i tonach tutoriali uważam, że to ciężka praca i należy się szacunek za wkład. Twoje wpisy to jest esncja mikrobloga bo są techniczne i fajnie się czyta z czym tam miałeś problem. Pozdro!
natomiast nie zrezygnuję z przestrzennego dźwięku, choćbym ten silnik miał pięć lat programować
- pytanie było trochę inne: czy jest sens, czy tą różnicę da się usłyszeć. O to chodziło @Manna5 - nie czepiał/krytykował/wyśmiewał, tylko nie był pewien, czy jest sens poświęcać czas na ficzer, który będzie niezauważalny.
@cerrato: lubię takie wpisy jak często pisze @Manna5, są potrzebne i przydatne. ;)
pytanie było trochę inne: czy jest sens, czy tą różnicę da się usłyszeć.
Pytasz czy jest różnica między dźwiękami o stałej intensywności a dźwiękiem stereofonicznym i przestrzennym? Różnica jest gigantyczna, większa niż między mono a stereo. Sam ten mikro-silnik do dźwięków i tak będzie potrzebny, bo tak czy siak muszę mieć możliwość zarządzania dźwiękami, dodawania do miksera nowych oraz usuwania tych, których źródła uciekają poza kamerę (w uproszczeniu). Dodanie panningu i filtrowania niewiele pracy dodaje, a efekt końcowy na pewno będzie zauważalny — tutaj nie mam żadnych wątpliwości.
Efekt Dopplera może nie być zauważalny, dlatego roważę jego zastosowanie. Ale np. trudno by było nie zauważyć dźwięku przechodzącego z jednej słuchawki do drugiej, albo dźwięku z ustawionym tłumieniem i echem (np. dla pomieszczeń). Tutaj kwestią nie jest to czy taki przestrzenny dźwięk robić, a jak go zrobić, aby sprawiał wrażenie naturalnego i dawał jak najlepsze poczucie imersji.
@nie_tak_wiele_postow: człowieku, o co Ci chodzi? Po co komentujesz treści, które Cię nie interesują? Idź sobie na jakieś grupy fb i tam komentuj.
Ogólnie warto trochę poeksperymentować i pamiętać, że najłatwiejszą metodą dla symulacji lokalizacji dźwięku są osobne wyliczania odległości do ucha lewego i prawego w celu ustalenia odpowiedniego przesunięcia odtwarzania kanałów dla każdego ucha. Ważne jest także odpowiednie tłumienie wysokich częstotliwości. "Najtańsza" implementacyjnie metoda to filtrowanie wysokich częstotliwości (np. w paśmie 6kHz - 20khz obcinać odpowiednio np. od -3dB do -8dB) dla ucha po przeciwnej stronie głowy względem źródła. Jeśli źródło dźwięku jest po lewej stronie to do prawego nie tylko dźwięk dociera ciszej i później ale także o innej charakterystyce częstotliwościowej. Analogicznie przód/tył. Ze źródła z tyłu głowy słyszymy mniej najwyższych częstotliwości.
Druga sprawa to szerokość głowy a ta ma około 20cm. Dźwięk tę odległość przemierza około 1,5ms a ucho takie różnice już doskonale wyłapuje jako zaburzenia lokalizacji źródła dźwięku. Warto więc dźwięk dla jednego ucha przesuwać w tym małym zakresie bo to też dużo da (oczywiście uwzględniając kąt względem źródła). Zrób sobie sztuczną głowę z gąbki w miejsce uszu włóż mikrofony i pobaw się tematem np. w Audacity... Do tego wszystkiego jeszcze niestety dochodzą odbicia od obiektów w pomieszczeniu, które odgrywają chyba najważniejszą rolę ale wyliczenie tego już nie jest takie banalne.
@katakrowa: bardzo dobre podsumowanie napisałeś. :]
W moim przypadku, silnik dźwięku będzie okrojony do kilku efektów, dlatego że bardziej niż na absolutny realizm, stawiam na prostotę. Bo tak jak wskazałeś, faktycznie w tym temacie jest masa różnych czynników, jednak ich wszystkich nie opłaca się implementować, ze wzlgędu na specyfikę mojej gry (w końcu będzie to nieduża gra w pixelarcie). Mimo wszystko wszystkiemu o czym napisałeś dokładniej przyjrzę się podczas implementacji tego silnika i wtedy się zobaczy, jak dużo można zrobić, a co okaże się trudne lub zbędne. Na razie tylko teoretyzuję i przygotowuję głowę na przyszłość.
@furious programming: czemu tutaj spamujesz zamiast na jakimś forum gier?