Använda HLSL och Pixel-Shaders i Silverlight 3

Den här artikeln är en uppdaterad/alternativ version av “ Använda HLSL och Pixel-Shaders i WPF ”.

I och med Silverlight 3 så händer det en hel del positiva saker med Silverlight. Den här artikeln är inte någon introduktion till Silverlight 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 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 vare sig WPF eller Silverlight (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!

Observera dock att Pixel-Shaders i Silverlight 3 är helt och hållet mjukvaru-renderande och kräver alltså inget avancerat grafik-kort, men kan inte heller dra nytta av det kort som potentiellt finns i den här versionen.

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.

Innan jag fortsätter så behöver du ha installerat DirectX SDK, Visual Studio 2008 SP1 och naturligtvis även utvecklarversionen av Silverlight 3. 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 Silverlight applikation genom att använda projektmallen Silverlight Application och döper lösningen och projektet till EffectTest.

image

Nästa dialogruta som dyker upp frågar om jag vill automatiskt generera ett testprojekt för att kunna prova min Silverlight-lösning i ASP.NET. I det här enkla exemplet väljer jag att inte göra det utan istället kommer jag att låta Visual Studio automatiskt generera en test-sida varje gång som jag exekverar lösningen. Jag kryssar alltså ur rutan i nedanstående dialogruta.

image

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

<UserControl x:Class="EffectTest.MainPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Height="300" Width="400">
    <Grid Background="White" x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition Height="53"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Slider x:Name="effectSlider" Margin="15" Grid.Row="0" Maximum="100" Minimum="0"/>
        <Image Grid.Row="1" Source="Tulips.jpg" />
    </Grid>
</UserControl>

Det kan vara värt att notera att jag använder mig av en av de bilder som följer med Windows 7 och som finns i den publika profilens exempelbilder, vid namn Tulips.jpg, du kan naturligtvis använda valfri annan bild men för mitt exempel passade den här. Jag har också lagt till den bilden till mitt projekt.

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 ett Silverlight Class Library som projektmall. Jag döper det nya projektet till MyEffects.

image

I det nya projektet genereras endast en grundläggande klass-fil som jag kan använda för det här exemplet. Jag döper om Class1.cs till WavyEffect.cs och får då frågan om jag vill även byta namn på klassen, naturligtvis vill jag det! Jag behöver också en ren text-fil som jag döper till WavyEffect.fx i klassbiblioteket.

High Level Shader Language (HLSL)
Min projektstruktur bör nu se ut som bilden här till vänster. Låt oss nu image öppna WavyEffect.fx och skapa vår Shader Effect i HLSL. Eftersom .fx är egentligen är ett obekant filformat för Visual Studio så belönas vi inte med vare sig färgkodning, 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. Det finns flera verktyg på marknaden för att jobba med .fx-filer och ett som är trevlig att använda sig av är Shazzam, en WPF-applikation som använder ClickOnce för installation. Shazzam använder sig av DirectX SDK och måste konfigureras med sökvägen till fxc-kompilatorn, men det är relativt självförklarande att göra det.

Den kod som vi nu ska skriva innehåller två 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 ShaderEffect med för att få till den önskade effekten. I vårt fall behövs två stycken:

float waviness : register(C0);
sampler2D implicitInput : 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 float, vilket helt enkelt är ett vanligt flyttal. Namnet på den första parametern är waviness 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, var för sig. Den heter implicitInput och finns alltså lagrad i register s0.

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

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

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.

I vår main-metod så tar vi med hjälp av waviness-konstanten och skapar vi 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 kompilera så att Silverlight förstår den. Det här är ett litet stökigt steg kan tyckas, men även här kan Shazzam bidra med hjälp genom att kunna kompilera .fx filer till rätt format. Men vi ska använda oss av den manuella metoden, mest för att det här artikeln ska bli lite längre :).

Om du har installerat DirectX SDK’t på samma ställe som jag (på en 64-bitars dator) så kan du hitta kompilatorn som ska användas i "c:\Program Files (x86)\Microsoft DirectX SDK (March 2009)\Utilities\bin\x64\”. Den exekverbara filen heter helt enkelt fxc.exe och har en del parametrar som jag inte kommer gå in på här. De som vi är intresserade av är “/T” som berättar för kompilatorn vilken profil av PixelShader som vi vill använda, och även “/Fo” som ger oss möjligheten att specificera filnamn på det kompilerade resultatet. Det slutliga kommandot blir helt enkelt:

fxc /T ps_2_0 /Fo WavyEffect.ps WavyEffect.fx

Det finns nu en risk att resultatet av kompilering blir följande utmatning i konsolfönstret:

Microsoft (R) Direct3D Shader Compiler 9.26.952.2844
Copyright (C) Microsoft Corporation 2002-2009. All rights reserved.

error X3501: 'main': entrypoint not found

compilation failed; no code produced

imageDet är inte fel i koden utan tyvärr fel i “kodningen” av .fx-filen som Visual Studio vanligen sparar i formatet Unicode. Det kan vi justera genom att välja menyalternativet File | Advanced Save Options, som ger oss konfigurationsfönstret här bredvid. Istället för Unicode, välj “US-ASCII – Codepage 20127”, långt ned i listan. Tryck sedan ok och spara effekt-filen igen. Kompilera om och nu borde det fungera betydligt bättre än tidigare. Nu har vi en binär version (WavyEffect.ps) av vår effekt och den kopierar vi nu in i MyEffects projektet och markerar som “Resource” i egenskapen “Build Action”. Det gör att filen kommer bakas in i vår effekt-assembly och kunna användas som en resurs.

Effekt-klassen
Om vi nu kompilerar vår lösning så bör allting fungera, men däremot så är vi inte klara med vår Shader Effect eftersom vi helt enkelt måste skriva den koden och se till att vår parameter waviness får sitt värde korrekt applicerat, därför öppnar vi nu WavyEffect.cs och skriver följande kod där.

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;

namespace MyEffects
{
    public class WavyEffect : ShaderEffect
    {
        public WavyEffect(){
            this.PixelShader = new PixelShader();
            this.PixelShader.UriSource = new Uri(@"MyEffects;component/WavyEffect.ps",
                                                 UriKind.Relative);

            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 double Waviness
        {
            get { return (double)GetValue(WavinessProperty); }
            set { SetValue(WavinessProperty, value); }
        } 

        public static readonly DependencyProperty WavinessProperty =
            DependencyProperty.Register("Waviness", typeof(double), typeof(WavyEffect),
                                        new PropertyMetadata(0.0, PixelShaderConstantCallback(0)));
    }
}

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 konstruktor som för projektet pekar ut vilken binär fil som innehåller vår binära effekt som vi kompilerat tidigare i förra steget. Konstruktorn 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 Silverlight 3, en otroligt cool funktion som jag dessvärre inte verkar fungera i den beta som jag har installerat, mer om det senare. 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.0 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å.

image

Använda ShaderEffect i Silverlight 3 
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 MainPage.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 <UserControl/> 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.

image

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="Tulips.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 skulle vilja ta detta lite längre och  istället för ett statiskt värde att mata in följande parameter på Waviness:

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

I Silverlight 3 är detta en av nyheterna, just att kunna databinda mellan element i XAML-koden, men dessvärre så har jag inte lyckats få det att fungera i den här betan på sättet ovan (som fungerade i WPF). Istället kan jag gå “från andra hållet” och uppdatera vår “slider”-kontroll enligt följande:

<Slider x:Name="effectSlider" Minimum="0" Maximum="100" Grid.Row="0" Margin="15"
        Value="{Binding Waviness, Mode=TwoWay, ElementName=myEffect}"/>

Jag talar alltså om för vår “slider”-kontroll att när jag uppdaterar värdet så ska den också uppdatera Waviness-egenskapen på effekten. När jav nu kör applikationen så får jag följande resultat när jag leker lite med slidern i fönstret:

image

Sammanfattning
Det här var en introduktion till vad som kan göras med hjälp av en av nyheterna i Silverlight 3 och även i viss mån med WPF i och med Service Pack 1 till Visual Studio 2008 och .NET Framework 3.5. Hör gärna av dig med kommentarer och berätta vad du tyckte om den här artikeln.

Länkar
Silverlight 3: https://silverlight.net/getstarted/silverlight3/default.aspx 
Senaste DirectX SDK’t: https://www.microsoft.com/directx
Verktyget Shazzam’s källkod: https://www.codeplex.com/shazzam
Verkyget Shazzam’s ClickOnce installation: https://shazzam-tool.com/publish.htm
En uppsättning färdiga effekter: https://www.sharpgis.net/post/2009/03/18/Silverlight-30-Pixel-shaders.aspx