Lekce 6 - XNA a HLSL - Negativ, embos, gamma, toonshading a sobel
V předchozí lekci, {PEVIOUS}, jsme si napsali systém pro postprocesorové efekty.
Vítejte znovu. V tomto dílu přidáme další snadné postprocesové efekty. Ale ještě něž začneme, tak pro jistotu zopakuji kostru shaderu, do které budeme přidávat veškerý obsah, nebude-li řečeno jinak:
sampler2D tex[1]; float4 PixelShaderFunction(float4 Position : POSITION0, float2 UV : TEXCOORD0) : COLOR0 { float4 color = tex2D(tex[0], UV); //dalsi kod semhle } technique Technique1 { pass Pass1 { PixelShader = compile ps_2_0 PixelShaderFunction(); } }
Negativ
Dalším velmi snadným efektem je vytvoření negativu. Vlastně jen barvy otočíme a to takto:
1-barva
To jen abyste nevypadli ze vzorečků Vezmeme kostru shaderu a otrocky vzoreček aplikujeme na všechny
barevné složky vyjma alfa kanál:
return float4(1-color.r,1-color.g,1-color.b,1);
Shader do programu nasaďte jako minule a výsledek by mohl vypadat nějak takto.

Vše lze vyřešit také efektivněji pomocí takzvaného swizlingu. Je to specialita jazyka jako takového, kéž by něco takového bylo možného i jinde:
return float4(1-color.rgb, color.a);
Operace se provede pro všechny komponenty odděleně. Super vlastnost
usnadňující zápis. A výsledek je naprosto stejný. Ještě bych podotkl,
že složky musí být ze stejné sady tedy buď z x-y-z-w
a nebo z
r-g-b-a
. Kombinovat je navzájem nelze. Pomocí inverze si
vytvoříme další efekt a tím bude embos.
Embos
Efekt embosu dovede zdůraznit přechody barev. Principiálně se skládá originální barva s negativem posunutým v určitém směru o několik pixelů. Vzoreček je následující:
(barva[u,v] + (1- barva[u+1,v+1]))/2
Pro posun o jeden pixel doprava dolů. Dvěma dělíme, aby byla výsledná barva v korektním barevném rozsahu. Posun musí být pro dosažení daného efektu vždy na diagonále. Implementaci provedeme jako vždy do kostry:
float3 ret=(color.rgb+(1-tex2D(tex[0],UV+offset).rgb))/2; return float4(ret, color.a);
Kde offset je proměnná, kterou jsem si vystrčil ven jako parametr.
float offset=0.001;
Hodnotu lze zjistit buď pokusně a nebo výpočtem. Jelikož máme hodnoty pro texturování v rozsahu 0-1, tak budeme muset trošku kouzlit. Známe rozměr okna 800x480 (aspoň myslím). Rozměr jednoho pixelu zjistíme 1/800. Poměrně se to shoduje s pokusnou hodnotou.

Gamma korekce
Další shader, tentokráte ale už bude použitelnější. Gama korekce (anglicky gamma correction) je úprava obrazu, dříve používaná pro zobrazování na CRT obrazovkách, které nezobrazovaly lineárně. Proto bylo potřeba tento vliv kompenzovat. Lze ji použít také pro opravu expozice, kdy je požadovaný objekt schován v příliš tmavé nebo příliš světlé části obrazu a tímto způsobem ji lze vytáhnout ven. Opět jako vždy rovnice:
barva^gamma
Rovnice jako vždy snadná a pokud se zdá někomu v něčem známá, tak se ani moc neplete. Stejnou rovnici jsme měli pro modulaci jasu bodového světla. Opět i zde mohou nastat dva případy. Potřebujeme utlumit příliš jasný obraz a nebo naopak jas zvednout. Exponenty mezi 0-1 jas zvýší a naopak exponenty 1-nekonečno snižují. Na následujícím grafu vidíte výsledný průběh:

Implementace algoritmu je stejná jako u předchozích shaderů:
float3 ret=pow(color.rgb,gamma);
return float4(ret, color.a);
A proměnnou gamma dáváme jako vstupní parametr:
float gamma=2.2;
Na výsledek s původním obrázkem, obrázkem s gamma na 1/2,2 a s gammou 2,2 se můžete podívat níže:

Ne, není to montáž, výstup mi už takto generuje shader. Není to nic složitého, stačilo pouze zapodmínkovat výpočet asi takto:
float3 ret=color.rgb; if(UV.x>0.33)ret=pow(color.rgb,1/gamma); if(UV.x>0.66)ret=pow(color.rgb,gamma);
Obvyklá hodnota pro většinu obrazovek se pohybuje v rozmezí 1,8 až 2,4. To jen pro kompletní informaci.
Toonshading
Toonshading je mírně složitější technika. Podle jasu vybereme barvu z druhé pomocné textury a tak barvu zaměníme. Už je na nás jakou paletu zvolíme. Předně si vytvoříme texturu s novou paletou barev. Já jsem vzal deset náhodných barviček, šířka textury tedy bude 10 a výška 1.

A texturu přidáme do projektu. Pojďme na shader. Prvně musíme přidat jednu texturu do pole samplerů:
sampler2D tex[2];//2 textury
A vypočteme si intenzitu pro daný pixel, kupříkladu tím složitějším způsobem s váhami:
float intensity = 0.3f * color.r + 0.59f * color.g + 0.11f * color.b;
A tuto hodnotu použijeme pro určení nové barvy z druhého sampleru. Druhá souřadnice bude vždy nula, protože textura má vlastně jen jeden rozměr.
float3 toon=tex2D(tex[1],float2(intensity,0));
A vše pošleme na výstup:
return float4(toon, color.a);
A to je celý shader. Jeho obsluha bude velmi snadná. Ve třídě (je stejná jako ostatní) si pouze konstruktorem předáme jméno textury s paletou:
Texture2D Toon; string toontexture; public ToonShading(Game g, string toontexture): base("postprocesory/toon",g) { this.toontexture = toontexture; }
Přepíšeme metodu Load
a texturu nahrajeme:
public override void Load(){ base.Load(); Toon = Game.Content.Load<Texture2D>(toontexture); }
A v metodě Draw
nastavíme texturu do druhého sampleru:
public override void Draw(Texture2D input){ Game.GraphicsDevice.Textures[1] = Toon; base.Draw(input); }
Tím jsme nastavili texturu jako aktivní. A to je celé. Výsledek bude pro mou texturu vypadat následovně:

Detekce hran
Občas je potřeba také detekovat hrany v obraze. Je to kupříkladu jedna z metod anti-aliasingu. A určitě je dobré tento shader znát. Metod je mnoho, ale já použiji takzvaný sobelův filtr. Je to matice (ano zas matice) 3x3 o následujícím rozložení:
-1 0 1 -1 -2 -1 -2 0 2 0 0 0 1 0 1 1 2 1
Potřebujeme matice dvě, jedna je pro horizontální detekce a druhá pro vertikální. Avšak je možné matici rotovat po 45% a zjišťovat tak i další přechody. Předně si nadefinujeme konstanty. První bude hodnota, která nám určí hranici mezi hranou a nehranou:
float treshold=0.05;
Následují dvě proměnné pro velikosti jednoho pixelu:
float pixelx=0.001; float pixely=0.002;
Pro zjednodušení si napíšeme snadnou funkci, která nám vypočte jas na daném bodu. HLSL stejně jako další programovací jazyky podporuje psaní funkcí. Ta musí však být deklarována před tím, než ji použijeme, tedy stejně jako v C-čku. Funkce je to snadná, takže bez komentáře:
float Jas(float2 uv){ float3 color= tex2D(tex[0], uv); return (color.r+color.g+color.b)/3; }
Použil jsem snadnou variantu, ale můžeme použít i tu s váhami. Sobelovy matice aplikujeme následujícně:
float valx=-1*Jas(float2(UV.x-pixelx,UV.y-pixely))+Jas(float2(UV.x+pixelx,UV.y-pixely)); valx+=-2*Jas(float2(UV.x-pixelx,UV.y))+2*Jas(float2(UV.x+pixelx,UV.y)); valx+=-1*Jas(float2(UV.x-pixelx,UV.y+pixely))+Jas(float2(UV.x+pixelx,UV.y+pixely));
A pro vertikální směr:
float valy=-1*Jas(float2(UV.x-pixelx,UV.y-pixely))-2*Jas(float2(UV.x,UV.y-pixely))-1*Jas(float2(UV.x+pixelx,UV.y-pixely)); valy+=1*Jas(float2(UV.x-pixelx,UV.y+pixely))+2*Jas(float2(UV.x,UV.y+pixely))+1*Jas(float2(UV.x+pixelx,UV.y+pixely));
A na závěr pouze rozhodneme, zda-li je na daném místě přechod či nikoliv. A to tak, že sečteme druhé mocniny obou vypočtených hodnot a porovnáme je s hranicí:
if((valx*valx+valy*valy)<treshold)return tex2D(tex[0],UV); return float4(0,0,0, 1);
A to je vše Výsledek
vypadá nějak takto:

Tak a to by bylo pro dnešní díl vše. Postprocesové efekty jsou pěkné téma, ale moc to nevydrží. Neznám jich totiž tolik a protože jsou opravdu jen na pár řádků, tak se jich do jednoho dílu vejde opravdu dost. Snad vás detekce hran neodradila, je už pravda složitější.
Budu se těšit jako vždycky na komentáře, otázky, nápady, stížnosti a na viděnou zase příště, XNA a HLSL - Postprocesory Sepia, Alfa masking a Noise.
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 99x (3.08 MB)
Aplikace je včetně zdrojových kódů v jazyce C# .NET