Lekce 7 - XNA a HLSL - Postprocesory Sepia, Alfa masking a Noise
V předchozí lekci, XNA a HLSL - Negativ, embos, gamma, toonshading a sobel, jsme si připravili hned několik postprocesorových shaderů.
Vítejte znovu, v dnešním díle si přidáme další postprocesorové efekty. Začneme zlehka shaderem nazývaným sepia. Přidáme si také alfa masking a pak taky výsledný obraz zaneřádíme náhodným šumem. Práce mnoho, začněme.
Sepia
Sepia je jednoduchý shader, skrze který je možné obraz pozměnit do podoby staré fotografie. Taky určitě znáte tento efekt z mobilů, kde se v různých obměnách objevuje. Vzorec vypadá následovně:
R=r*0.393 + g*0.769 + b*0.189 G=r*0.349 + g*0.686 + b*0.168 B=r*0.272 + g*0.534 + b*0.131
Jak se k tomu přišlo se mě neptejte, nevím to. Shader je pak snadný jako facka:
float4 color = tex2D(tex[0], UV); float4 ret = color; ret.r = (color.r * 0.393) + (color.g * 0.769) + (color.b * 0.189); ret.g = (color.r * 0.349) + (color.g * 0.686) + (color.b * 0.168); ret.b = (color.r * 0.272) + (color.g * 0.534) + (color.b * 0.131); return ret;
Pouze jsem přepsal konstanty zmíněné výše. Výsledný efekt vypadá následovně:
Tak to bychom se rozehřáli po kratší odmlce a nyní už něco pořádného a užitečného.
Alfa masking
Alfa masking je shader, který nám umožní překrýt celý obraz obrázkem jiným – maskou. Tento efekt určitě znáte ze stříleček, když zaměřujete. Ukážeme si dva přístupy, jeden s maskou černo-bílou a druhý, který používá alfa kanál. Ale co budeme potřebovat vždycky, tak nahrát texturu s maskou a propašovat ji do shaderu. Proto v souboru se třídou přidáme proměnné:
Texture2D Mask;
string texture;
V konstruktoru si nastavíme jméno souboru s texturou:
public AlfaMask(Game g, string texture): base("postprocesory/alfamask",g){ this.texture = texture; }
A jako obyčejně v metodě Load
texturu ze jména
nahrajeme:
public override void Load(){ base.Load(); Mask = Game.Content.Load<Texture2D>(texture); }
Před vykreslením nesmíme zapomenout texturu nastavit jako aktivní. Provedeme to stejně jako minule nastavením:
public override void Draw(Texture2D input){ Game.GraphicsDevice.Textures[1] = Mask; base.Draw(input); }
Pořadí úkonů jsem si nespletl, musí být pouze takto, jinak se velmi pravděpodobně nic nevykreslí. Pojďme si připravit také shader. Předně zvýšíme počet samplerů na dva:
sampler2D tex[2];
A můžeme přistoupit k samotnému shaderu. Jako vždy si vybereme barvu:
float4 color = tex2D(tex[0], UV);
a také vybereme barvu z masky:
float4 mask=tex2D(tex[1],UV);
Výslednou barvu získáme vynásobením obou takto získaných barev.
return color*mask;
Tento jednoduchý postup bude fungovat třebas pro tuto masku:
Jednoduchá černo-bílá maska. Vše co je černé na masce překryje obraz. Vyplývá to už z kódu shaderu. Černá barva jsou vlastně samé nuly a pokud cokoliv násobíme nulou, tak jak jistě víme je výsledek zase nula, tedy černá. Bílá barva naopak přenese všechnu barvu původní. Pokud ale budeme chtít použít následující masku:
Která používá krom černé a bílé také další barvičky, mírně narazíme. I když jak kdy. Záleží, jaký efekt potřebujeme. Ostatní barvy budou více či méně průhledné. Bude záležet na jejich jasu. Někdy je to efekt žádoucí, ale třebas u zaměřovače pušky, který je napevno barvou natištěn, to chtít nebudeme. A právě na tyto případy budeme potřebovat texturu s alfa kanálem, který nám určí jak moc je daný pixel průhledný. Alfa kanál do ní dostaneme přes nějaký lepší editor na fotky. Já jsem použil Photo Filtre, který je podle mě snadný na pochopení. Není to sice taková mašinka jak obchod s fotkami, ale vše co jsem kdy potřeboval se mi s ním nějak podařilo udělat. Pro představu jak alfa maska vypadá viz následující obrázek:
Zelené čáry mimo kruh jsou pevné, uvnitř průhledné a červený kruh je také mírně průhledný. Do shaderu si to přeneseme mírnou úpravou. Vzorec pro tento druh alfa blendingu je následující:
c=color*(1-mask.afa)+mask
A ten pak pouze převedeme do shaderu následujícně:
return color*(1-mask.a)+mask;
Jak vzorec ale funguje? Vyložíme si to na příkladu. Dejme tomu, že máme pixel, který je naprosto neprůhledný. Má tedy alfu jedna. Tu odečteme od jedničky a získáme tak v závorce nulu. Takže ve výsledku bude jen a pouze obsažena barva z masky. A to je to co přesně chceme. Obdobně tomu je i u dalších hodnot.
A jak vidno i výsledek tomu odpovídá.
Noise - šum
Tento shader využijeme k zašumění celého obrazu. Celý princip je velmi snadný, shaderem vypočteme jakýsi virtuální a náhodný offset a s pomocí něj jen ze sampleru vybereme výslednou barvu. Jak ovšem generovat náhodné číslo. Náhodné číslo nikdy nebude moc dobře náhodné, ale pro naše velmi skromné účely jej vytvoříme z funkce sinus a souřadnic daného bodu. Nutno podotknout, že tento shader jsem našel zde a budu se originálu držet.
Přidáme si tedy do shaderu proměnnou Seed
, která nám bude
reprezentovat semínko:
float Seed;
V pixel shaderu si vypočteme náhodné číslo ze souřadnic za použití semínka:
float noiseX=Seed*sin(UV.x*UV.y);
Tuto „náhodnou“ hodnotu upravíme ještě za pomocí funkce
fmod
. Ta stejně pracuje stejně jako modulo akorát je i pro
čísla s plovoucí řádovou čárkou. Vrací nám tedy zbytek po dělení.
noiseX=fmod(noiseX,8)*fmod(noiseX,4);
A pak vypočteme offset pro souřadnice. Opět za pomocí zabudované funkce
fmod
:
float2 dis=float2(fmod(noiseX,NoiseAmount),fmod(noiseX,NoiseAmount+0.002));
Nesmíme zapomenout přidat proměnnou NoiseAmount
jako parametr
shaderu. Ta nám bude říkat jak moc se má šum projevovat. Já jsem ji
nastavil rovnou na hodnotu 0.01 která se ukázala poměrně pěkná:
float NoiseAmount=0.01;
A na závěr offset aplikujeme v sampleru a výslednou barvu vracíme jako finální:
float4 color = tex2D(tex[0], UV+dis); return color;
Shader je hotov. Ve třídě se shaderem přepíšeme metodu
Draw
a nastavíme semínko na námi zvolenou hodnotu. Viděl bych
ji v řádech stovek až tak dva tisíce. Já jsem použil 523.
float Seed; public Noise(Game g) : base("postprocesory/noise",g){ Seed = 523; } public override void Draw(Microsoft.Xna.Framework.Graphics.Texture2D input){ Effect.Parameters["Seed"].SetValue(Seed); base.Draw(input); }
A máme hotovo. Pokud nyní program spustíme, dostaneme nádherně
zašmudlaný obraz. A to by bylo pro dneš... moment. A co když si zastavím
animaci. Tak to šumět přestane. A to my nechceme. Budeme muset do shaderu
přidat také vliv času. Bohužel na to náš systém není stavěný, ale to
se dá rychle napravit. Ve třídě s obecným postprocesorem upravíme metodu
Update
a přidáme jí parametr a herním časem, takže bude
vypadat následovně:
public virtual void Update(GameTime time){ }
A do volání této metody v hlavní hře tento parametr nezapomeňte předat. V našem shaderu pak už jen přidáme proměnnou pro čas:
float Time;
A do funkce sinus ji promítneme následovně:
float noiseX=Seed*Time*sin(UV.x*UV.y+Time);
Do třídy postprocesoru přidáme metodu Update
kde si čas
upravíme:
public override void Update(GameTime time){ base.Update(time); Time += time.ElapsedGameTime.Milliseconds/500.0f; }
A na úplný závěr v metodě Draw
čas nastavíme a jsme
hotoví.
Effect.Parameters["Time"].SetValue(Time);
Nyní se bude šum hýbat i při statické scéně.
To by bylo opravdu pro tento díl všechno. Příště se podíváme na možnosti rozmazávání obrazu, bez kterých se ve hrách prostě neobejdete. Těším se na otázky, názory, nápady a tak dále v komentářích. Na shledanou příště.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 120x (3.1 MB)
Aplikace je včetně zdrojových kódů v jazyce C# .NET