Använda HLSL och Pixel-Shaders i WPF

Innan jag börjar: Denna artikel finns också i video-form på Channel9.

I och med Service Pack 1 till .NET Framework 3.5 så händer det en hel del positiva saker med WPF. Den här artikeln är inte någon introduktion till WPF utan istället en demonstration av en av de funktioner som tilltalat mig mest i och med min brinnande ådra för spelprogrammering, nämligen möjligheten att använda en funktion på grafikkortet som kallas Pixel-Shader för att göra avancerade grafikfunktioner lite effektivare än tidigare.

Pixel-Shader är en av de tre just nu tillgänliga tillstånden och funktionerna som erbjuds av moderna grafikkort och som är specialiserad på att skapa effekter på enstaka pixlar på skärmen med hjälp av grafikkortets avancerade funktioner och tekniker. De andra två tillstånden är Vertex Shader och Geometry Shader, men de finns idag inte tillgängliga för WPF (och jag är inte säker på om de kommer att finnas tillgänliga i framtiden heller) så de tänker jag inte berätta mer om. Låt mig istället konkret visa hur vi kan använda det som finns idag!

Inledning
Det kan vara bra att ha en viss förståelse för HLSL eller High Level Shader Language som är ett C-liknande språk som Microsoft har skapat för att just programmera effekter eller så kallade ”shaders” mot grafikkorten, men i det här exemplet kommer jag att hålla en låg profil och skapa en mycket enkel effekt som du kan ta till dig utan särskilt djup kunskap i just HLSL.

Vad det här stödet i praktiken går ut på är att vi kan skapa en fil med ändelsen .fx som kompileras till en binär fil som samkompileras med en kompletterande C# eller Visual Basic fil och som sedan läggs till som resurs till vår applikation. Denna resurs kan vi sedan använda som Effect på något av våra UIElement i vår applikation.

Under utvecklingen av stödet för Pixel Shaders för WPF så har själva processen att skapa effekten och applicera den på korrekt element varit ganska komplex, med en del onödiga (kan tyckas) element som tack och lov har abstraherats bort nästan helt i och med den slutliga versionen samt ett tillägg till Visual Studio 2008 som finns publicerat på https://www.codeplex.com/wpf. Detta tillägg gör så att den skapade .fx-filen kompileras med hjälp av DirectX SDK effektkompilator och automatiskt läggs till som en resurs i vår applikation. Det finns några steg kvar, och de har jag som ambition att gå igenom snart. Innan jag fortsätter så behöver du ha installerat DirectX SDK, Visual Studio 2008 SP1 och även det tillägg som nyss nämndes. Länkar till vart du kan hitta dessa resuser hittar du i slutet av artikeln.

Skapa applikationen
Efter att du installerat de nödvändiga komponenterna så väljer du att skapa ett nytt projekt. I det här exemplet skapar jag en ny Windows Presentation Application genom att använda projektmallen WPF Application och döper lösningen och projektet till EffectTest.

clip_image002

Efter att projektet och dess tillhörande filer genererats av Visual Studio så modifierar jag XAML-koden för Window1.xaml till att se ut som följande:

<Window x:Class="EffectTest.Window1" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" Title="EffectTest" Height="300" Width="300">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="53"/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <Slider x:Name="effectSlider" Minimum="0" Maximum="100" Grid.Row="0" Margin="15"/>
    <Image Grid.Row="1" Source="C:\Users\Public\Pictures\Sample Pictures\Forest.jpg"/>
  </Grid>
</Window>

Det kan vara värt att notera att jag använder mig av en av de bilder som följer med Windows Vista och som finns i den publika profilens exempelbilder, vid namn Forest.jpg, du kan naturligtvis använda valfri annan bild men för mitt exempel passade den här.

Skapa en ShaderEffect
Nästa steg är att skapa den effekt som jag slutligen kommer att använda på mitt image-objekt, därför lägger jag till ett nytt projekt till den befintliga lösningen och använder den nyligen installerade projektmallen för WPF Shader Effect Library och döper det nya projektet till MyEffects.

clip_image004

När jag trycker OK i dialogen för att lägga till ett projekt så kommer jag att få upp en varningsruta att den mall som jag nu försöker att använda har modifierats och innehåller anpassade kompileringssteg. Jag väljer att ladda projektet som normalt och trycker på OK.

clip_image006

I det nya projektet genereras en del filer, jag väljer omedelbart att radera Effect1.cs och Effect1.fx eftersom jag vill skapa mina egna effektfiler istället. Nu väljer jag att lägga till ett nytt objekt (item) till det sist skapade projektet och kan till min glädje notera att det också finns en ny mall för den Shader Effect som jag vill skapa under kategorin WPF. Jag döper min nya Shader Effect till WavyEffect och då genereras på nytt två filer, en WavyEffect.cs och en WavyEffect.fx.

clip_image008

High Level Shader Language (HLSL)
Min projektstruktur bör nu se ut som bilden här till vänster. Låt oss nu clip_image010öppna WavyEffect.fx och skapa vår Shader Effect i HLSL. Koden som har genererats har tyvärr ingen färgkodning eftersom .fx är egentligen ett obekant format för Visual Studio, vi belönas inte heller med någon sorts IntelliSense eller intelligens från utvecklingsmiljön som vi kan vara vana vid, så det gäller att hålla tungan rätt i mun, eller att falla tillbaka på ”klipp-och-klistra arv” från exempelvis den här artikeln.

Den genererade koden innehåller två stycken sektioner som kan vara av intresse att förstå sig på. Den första sektionen består av något som ser ut som globala variabler men som egentligen är parametrar som vi behöver populera våra Shader Effect med för att få till vår funktionalitet. I den generade filen finns två sådana parametrar:

float4 colorFilter : register(C0);
sampler2D implicitInputSampler : register(S0);

Det går lätt läsa från raderna att den första parametern, eller konstanten som det kallas i HLSL är av datatypen float4, vilket helt enkelt är en vektor med fyra stycken flyttal, något som vanligtvis används för att beskriva färger (röd, grön, blå och alpha-värden är alla flyttal) eller koordinater i 3D. Namnet på den första parametern är colorFilter och efter kolonet finns en instruktion som talar om för grafikkortet att värdet till denna konstant kan hittas i register c0, något som vi kanske inte är helt bekväma med i vår skyddade hanterade .NET värld, men räds icke, det finns en trevlig abstraktion även för .NET som vi snart kommer att se. Den andra konstanten är lite speciell, dess datatyp är en 2-dimensionell ”sampler” och beskriver egentligen den bild som vi för närvarande kommer att uppdatera varje pixel i, en och en. Den heter implicitInputSampler och finns alltså lagrad i register s0.

Sedan följer själva logiken för den automatgenererade effekten, main-metoden.

float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color = tex2D(implicitInputSampler, uv);
  return color * colorFilter;
}

Main-metodens signatur ser kanske lite konstig ut, den tar emot en parameter av typen float2, alltså två stycken flyttal, i vårt fall helt enkelt x- och y-koordinaten för den pixel som vi för närvarande låter grafikkortet modifiera med vår effekt. Versalerna TEXCOORD beskriver för grafikkortet att värdena ska betraktas som texturkoordinater och versalerna COLOR berättar att vi kommer att returnera en färg i form av det returvärde av typen float4 som vår metod returnerar. Detta betyder alltså att input till metoden är x- och y- för pixeln som just nu modiferas och returvärde är den färg som vi vill att just denna pixel ska ha när vi är klara att skicka den till skärmen. Vad metoden sedan gör är att den analyserar den 2-dimensionella ”sampler” som används med hjälp av x- och y-koordinaterna och hämtar den färg som för närvarande är på väg att skickas till skärmen, sedan multiplicerar vi den färgen med den konstant som vi har möjlighet att skicka till vår effekt via konstanten colorFilter. Vad som alltså kommer att hända är att vi får möjligheten att förstärka varje pixel med en statisk färg, vilket jag tycker är lite väl tråkigt för vårt exempel. Ersätt istället hela koden i WaveEffect.fx med följande kod.

float waviness : register(C0);
sampler2D implicitInputSampler : register(s0);

float4 main( float2 Tex : TEXCOORD ) : COLOR
{
  float4 Color;
  Tex.x = Tex.x + (sin(Tex.y * waviness)* 0.01);
  Color = tex2D( implicitInputSampler , Tex.xy);
  return Color;
}

Det första som är värt att notera är att vi byter datatyp och namn på den första konstanten till att vara ett clip_image012enkelt flyttal med namn waviness. Med hjälp av den konstanten skapar vi sedan ett vågmönster genom lite matematiska formler som förhoppningsvis syns i koden. Kort beskrivet så skapar vi en sinus-kurva som förstärks eller försvagas baserat på den konstant som vi skickar in i waviness och låter den sinus-kurvan förändra x-koordinaten på den pixel som vi jobbar med. Det gör att när vi läser av färgen från vår ”sampler” kommer att hämta färgen lite från sidan om den pixel som vi förväntar oss, vilket ger en vågeffekt vilket är ett kul exempel på vad en Effect Shader kan åstadkomma.

Ok, den här effekt-filen måste vi nu med hjälp av ett nytt kompileringssteg (som den här projektmallen innehåller) kompilera till kod som vi kan använda i våran applikation. Det gör vi genom att markera WavyEffect.fx filen i Solution Explorer och sedan sätta ”Build action” till Effect.

Effekt-klassen
Om vi nu kompilerar vår lösning så bör allting fungera, men däremot så är vi inte riktigt klara med vår Shader Effect eftersom vi också måste se till att vår parameter waviness får sitt värde korrekt applicerat, därför öppnar vi nu WavyEffect.cs istället och tittar på koden där. Jag tog mig friheten att modifiera koden enligt följande:

namespace MyEffects
{
  public class WavyEffect : ShaderEffect
  {

    static WavyEffect()
    {
      _pixelShader.UriSource = Global.MakePackUri("WavyEffect.ps");
    }

    public WavyEffect()
    {
      this.PixelShader = _pixelShader;
      UpdateShaderValue(InputProperty);
      UpdateShaderValue(WavinessProperty);
    }

    public Brush Input
    {
      get { return (Brush)GetValue(InputProperty); }
      set { SetValue(InputProperty, value); }
    }

    public static readonly DependencyProperty InputProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(WavyEffect), 0);

    public float Waviness
    {
      get { return (float)GetValue(WavinessProperty); }
      set { SetValue(WavinessProperty, value); }
    }

    public static readonly DependencyProperty WavinessProperty = DependencyProperty.Register("Waviness", typeof(float), typeof(WavyEffect), new UIPropertyMetadata(0.0f, PixelShaderConstantCallback(0)));

    private static PixelShader _pixelShader = new PixelShader();
  }
}

Den här klassen ärver från basklassen ShaderEffect och kommer att användas för att ”kommunicera” med den HLSL kod som vi tidigare skapat. Det finns en statisk konstruktor som för projektet pekar ut vilken binär fil som innehåller vår binära kod som redan kompilerats av Visual Studio i förra steget. Det finns också en vanlig konstruktor som skapar en ny Pixel Shader och som sedan registrerar våra konstanter så att vi kan modifiera dem utifrån. Egenskapen Input är den som med hjälp av en så kallad DependencyProperty mappas mot den ”sampler” som jag tidigare nämnt. Jag har också skapat en annan egenskap, nämligen Waviness av datatypen float som direkt kommunicerar med den Waviness konstant som finns i min effekt. Även den kommunikationen sker med hjälp av en DependencyProperty och ger mig bland annat möjligheten att databinda egenskapen mot andra element i WPF, en otroligt cool funktion som jag naturligtvis vill använda mig av i det här exemplet. Koden i sig är inte direkt självförklarande men i kort går det ut på att registrera vår egenskap med korrekt datatyp och koppla den inte bara mot ett namn i effekten utan även mot korrekt konstant med hjälp av PixelShaderConstantCallback(0) som alltså talar om vilket register mitt värde ska sättas för att kunna användas av effekten. Missa inte heller att ange 0.0f som ”default”-värde, det finns risk att applikationen inte kommer att fungera om du inte anger en korrekt datatyp som grund att stå på.

clip_image014

Använda ShaderEffect i WPF
Jag kompilerar återigen lösningen för att se så att det inte finns några syntax-fel och är redo att ta nästa steg, nämligen att använda effekten i min applikation. Jag börjar med att lägga till en referens till Shader Effect projektet så att korrekt .DLL-länkas till min applikation.

Sedan behöver jag lägga till en namnrymd i min Window1.xaml kod så att jag enkelt kan nyttja funktionaliteten i den refererade komponenten. Med hjälp av Visual Studio 2008’s intellisense går detta som en dans och jag kan inte hjälpas att dra på smilbanden av smidigheten när jag lägger till följande i elementet <Window/> i xaml-koden:

xmlns:eff=""

Omedelbart dyker det upp en lista över tillgängliga namnrymder och om jag bläddrar nedåt i listan httar jag mitt refererade projekt med min nyligen skapade effekt.

clip_image016

Om du känner att du inte vill använda dig av automagi som Intellisense så kan du istället skriva in följande rad i Window-elementet:

xmlns:eff="clr-namespace:MyEffects;assembly=MyEffects"

Nu kan jag helt enkelt uppdatera min Image genom att lägga till min effekt genom följande XAML-kod:

<Image Grid.Row="1" Source="C:\Users\Public\Pictures\Sample Pictures\Forest.jpg">
  <Image.Effect>
    <eff:WavyEffect Waviness="0"/>
  </Image.Effect>
</Image>

Nu kan jag direkt i Visual Studio 2008 laborera med olika värden på Waviness, prova exempelvis 20 eller varför inte 60 och se hur bilden förändras. Men jag är inte riktigt nöjd, jag vill ta detta lite längre och väljer istället för ett statiskt värde att mata in följande parameter på Waviness:

Waviness="{Binding ElementName=effectSlider, Path=Value}"

Vad detta attribut gör är att berätta för WPF att värdet till Waviness egenskapen ska vara det värde som slidern i applikationen har, vilket i det här exemplet kommer att gå från 0 till 100.

När jag nu kör applikationen så får jag följande resultat när jag leker lite med slidern i fönstret:

clip_image018

Sammanfattning
Det här var en introduktion till vad som kan göras med hjälp av en av nyheterna i WPF i och med Service Pack 1 till Visual Studio 2008 och .NET Framework 3.5. Hör gärna av dig med kommentarer vad du tyckte om denna artikel.

Länkar
Den här artikeln i videoform finns på channel9 - https://channel9.msdn.com/Tags/Sweden
Shader Effect BuildTask och projektmall – https://www.codeplex.com/wpf
Senaste DirectX SDK’t: https://www.microsoft.com/directx
Service Pack 1 till Visual Studio 2008: https://www.microsoft.com/vstudio