dotNed

Welkom bij dotNed Inloggen | Aanmelden | Help
in Zoeken

Dennis' avonturen in .net

3D Graphics in WPF, deel 9 (Materialen)

Tot nu toe hebben we twee kubussen en vlakken gezien: een zwarte (in het begin, voordat er licht was) en een blauwe. Als je naar mijn eerste post over dit onderwerp kijkt, zie je dat ik daar iets meer kleuren en textures gebruik. Hoe werkt dat dan?

Het geheim zit in het gebruik van materialen, oftewel Materials. Dat is waar we nu naar gaan kijken.

Ik heb een beetje uitgelegd hoe de renderer van WPF werkt: voor ieder punt wordt gekeken hoeveel licht er op valt en dan wordt aan de hand van dat percentage licht berekend hoe zichtbaar iets is. Maar in het echte leven (je weet wel, die wereld BUITEN je computer) is de hoeveelheid en kleur van het licht maar een deel van de reden dat we de dingen zien zoals we ze zien: als je een geel papiertje naast een rood papiertje legt,  dan krijgen ze allebei exact dezelfde hoeveelheid licht maar toch zie je twee kleuren. Dat ligt uiteraard aan de kleur van het papier. En niet alleen de kleur, maar ook de textuur en de bedrukking van het papier draagt bij aan het beeld dat we hebben van het papiertje.

In WPF kunnen we dat gedrag simuleren door het gebruik van de verschillende materials. Je moet een material zien als een soort behang dat je over je objecten plakt. Hoe dat in code werkt hebben we al gezien, maar ik zal het nog even herhalen:

   1:          <detrio:Box LeftBottomCorner="0 0 0" Width="1" Height="1" Depth="1">
   2:              <detrio:Box.Material>
   3:                  <DiffuseMaterial Brush="Blue"/>
   4:              </detrio:Box.Material>
   5:          </detrio:Box>
   6:   
   7:          <detrio:Box LeftBottomCorner="1 0 -2" Width="0.5" Height="1.0" Depth="0.5">
   8:              <detrio:Box.Material>
   9:                  <DiffuseMaterial Brush="Yellow"/>
  10:              </detrio:Box.Material>
  11:          </detrio:Box>

In regel 1 en in regel 7 maak ik twee boxen aan, met behulp van de code uit post 8a. In regel 2-4 en 8-10 geef ik ze een kleurtje. De eerste wordt blauw, de tweede geel (mits je het licht wit hebt gemaakt uiteraard). Maar eigenlijk klopt dat niet: we schilderen de box niet blauw en geel, maar we geven ze een Material mee die blauw of geel is. En dat maakt een groot verschil!

In dit voorbeeld gebruik ik de DiffuseMaterial. Deze zul je waarschijnlijk het meest gebruiken: dit is een Material die het licht dat er op valt in alle richtingen weerkaatst. Het is te vergelijken met het vel papier dat ik eerder noemde. Een vel papier weerkaatst licht heel anders dan een glimmend stuk plastic, een gelakt tafelblad of een stuk glas (al dan niet matglas). De keuze van het materiaal bepaalt dus hoe je je objecten ziet.

In WPF hebben we de keuze uit de volgende materialen, allen afgeleid van de class Material:

  • DiffuseMaterial, die hebben we net besproken
  • SpecularMaterial, een material die licht 'weerkaatst' (het werkt niet als een spiegel,maar lijkt er wel een beetje op)
  • EmmisiveMaterial, deze straalt meer licht uit dan dat er op komt. Let op: dit is geen light object, dus andere objecten worden hier niet mee verlicht. Dit houdt in dat als je twee objecten zou hebben in je scene, een met een DiffuseMaterial en een met een EmmisiveMaterial, maar je hebt geen lights in je scene, dan zie je het object met de DiffuseMaterial als een zwart object, en de EmmisiveMaterial zie je met de kleur die je het gegeven hebt
  • MaterialGroup. Dit is geen echt material, maar hiermee kun je verschillende materials combineren in een object (de DependencyProperty Material van GeometryModel3D heeft een content, wat maar een object kan bevatten: door hier een MaterialGroup in te stoppen kun je materialen combineren).

Hieronder zie je een voorbeeld van een scene met 2 objecten en geen lampen: het voorste object heeft een DiffuseMaterial, het achterste heeft een EmmisiveMaterial, beiden hebben als Brush 'Blue'.

twee materialen

Je ziet dat de voorste gewoon zwart wordt (er valt immers geen licht op), terwijl bij de achterste alles gelijkmatig blauw is: er valt immers geen licht op dus er is geen verschil in de belichting per kant. Dat hij toch zichtbaar is, komt omdat hij zelf licht uitstraalt (maar zoals je aan de voorste box ziet straalt hij niet echt licht uit naar andere objecten).

Het wordt pas leuk als we materials gaan combineren. Ik heb twee cylinders genomen (het effect is sterker bij gebogen oppervlaktes) en die naast elkaar geplaatst:

image De material voor de linker cylinder is als volgt gedefinieerd:

<detrioCL:Cylinder.Material>
    <MaterialGroup>
        <DiffuseMaterial Brush="Blue"/>
        <SpecularMaterial Brush="White" SpecularPower="0.8"/>
    </MaterialGroup>
</detrioCL:Cylinder.Material>

Hier combineer ik twee materialen, een DiffuseMaterial met een blauwe brush, en een SpecularMaterial. Deze laatste weerkaatst wit licht, met een intensiteit van 80%. Dus 80% van de hoeveelheid licht die er opvalt wordt wit teruggegeven. De rechter cylinder heeft alleen een DiffuseMaterial met een blauwe brush. Je ziet het verschil.

Naast de keuze voor de Material soort moet je ook een bepaalde brush kiezen. Tot nu toe hebben we steeds een standaard Brush genomen, uit de Enum Brushes (die heel veel kleuren in zich heeft). Je bent echter niet gebonden aan een brush met een kleur: je kunt heel veel brushes toepassen.

Brushes zijn afgeleid van System.Windows.Media.Brush, een abstract class (gebruik niet de oude System.Drawing.Brush, die werkt niet in 3D WPF!). We hebben de volgende brushes tot onze beschikking:

  • SolidColorBrush. De standaard die we al gezien hebben, deze tekent met 1 kleur
  • LinearGradientBrush. Een verloop die van een kleur naar een ander loopt, in een rechthoek (bijvoorbeeld van linksboven naar rechtsonder)
  • RadialGradientBrush. Een verloop in een cirkel, dus van binnen naar buiten.
  • ImageBrush. Deze kan een image bevatten die getekend wordt. Hier kun je dus een jpg, png, gif enz. gebruiken als brush
  • DrawingBrush. Een brush die een 2D drawing bevat. Hier teken je zelf de inhoud van de brush
  • VisualBrush. Deze bevat een visual als tekenobject.

We gaan even terug naar onze begin applicatie:

<Window x:Class="WpfApplication11.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Viewport3D>
        <!-- Te tekenen objecten -->
        <ModelVisual3D>            
            <ModelVisual3D.Content>
                <GeometryModel3D>
                    <GeometryModel3D.Geometry>
                        <MeshGeometry3D Positions="0 0 0, 1 0 0, 1 1 0, 0 1 0"
                                        TriangleIndices="0 1 2 0 2 3"
                                        />
                    </GeometryModel3D.Geometry>
                    <GeometryModel3D.Material>
                        <!-- Hier gaan we met de material spelen -->
                        <DiffuseMaterial Brush="Blue"/>
                    </GeometryModel3D.Material>
                </GeometryModel3D>
            </ModelVisual3D.Content>
        </ModelVisual3D>

        <!-- Lampjes! -->
        <ModelVisual3D>
            <ModelVisual3D.Content>
                <Model3DGroup>
                    <AmbientLight Color="#404040"/>
                    <DirectionalLight Color="#BFBFBF" Direction="1 -1 -1"/>
                </Model3DGroup>
            </ModelVisual3D.Content>
        </ModelVisual3D>

        <!-- Camera -->
        <Viewport3D.Camera>
            <PerspectiveCamera Position="1.5 1.0 2.0" 
                           LookDirection="-0.5 -0.3 -1" 
                           UpDirection="0 1 0" 
                           FieldOfView="45.0" 
                           />
        </Viewport3D.Camera>
    </Viewport3D>
</Window>

Dit is onze standaard applicatie, met een vierkant in het midden, wat verlichting en een camera. Als we de regel <DiffuseMaterial Brush="Blue"/> vervangen door de volgende code:

<!-- Hier gaan we met de material spelen -->
<DiffuseMaterial>
    <DiffuseMaterial.Brush>
        <SolidColorBrush Color="Blue"/>
    </DiffuseMaterial.Brush>
</DiffuseMaterial>

krijgen we precies hetzelfde effect. Dit is eigenlijk wat de standaard Brushes collectie doet: het maakt een SolidColorBrush aan. Dit ziet er dus als volgt uit:

image

Als we de Brush nu eens vervangen door een LinearGradientBrush, krijgen we de volgende code en het volgende resultaat:

<!-- Hier gaan we met de material spelen -->
<DiffuseMaterial>
    <DiffuseMaterial.Brush>
        <LinearGradientBrush>
            <LinearGradientBrush.GradientStops>
                <GradientStop Color="Blue" Offset="0.0"/>
                <GradientStop Color="White" Offset="0.5"/>
                <GradientStop Color="Red" Offset="1.0"/>
            </LinearGradientBrush.GradientStops>
        </LinearGradientBrush>
    </DiffuseMaterial.Brush>
</DiffuseMaterial>

image Geef toe: dit is niet het effect wat je verwacht had.

Dit ga ik zo oplossen. Laten we eens naar de ImageBrush kijken. Voeg aan je applicatie een plaatje toe, bijvoorbeeld een jpeg of een png. Gewoon toevoegen aan je solution:

Solution with image Ik heb ons logo genomen en die gesleept naar de solution. Standaard staan de properties al goed: het wordt een resource die meegelinkt wordt in onze applicatie.

De code ziet er dan als volgt uit:

<!-- Hier gaan we met de material spelen -->
<DiffuseMaterial>
    <DiffuseMaterial.Brush>
        <ImageBrush ImageSource="DETRIO-logo.png"/>
    </DiffuseMaterial.Brush>
</DiffuseMaterial>

Run de applicatie en zie het resultaat: niets...

De reden hiervoor is dezelfde als dat we in het vorige voorbeeld een rood vlak zagen. We hebben nog iets extras te doen om het aan de praat te krijgen.

Ik zei al dat je een Material moet zien als een behang dat je op je objecten plakt. Dat zei ik niet zomaar, die analogie gaat verder dan je misschien dacht. Als je gaat behangen en je hebt simpel, eenkleurig behang, maakt het eigenlijk niet uit hoe je de banen plakt (de naadjes tussen de banen even daargelaten). Of je ze nu van boven naar beneden, van beneden naar boven of voor mijn part van links naar rechts plakt: het is allemaal hetzelfde. Maar als je behang met een print hebt, is de richting waarin je plakt wel belangrijk! Bij Materials is dat precies hetzelfde: WPF moet weten in welke richting je materials op je objecten geplaats moeten worden.

Om dat aan te geven moeten we een mapping maken van onze 2D materialen naar de 3D coordinaten van onze objecten.

Neem even onze referentie tekening met een kubus. Ik heb hierin de coordinaten gezet van de hoekpunten. Ook zie je een blauwe material op het voorvlak. In het rood zie je de 2D coordinaten van ons plaatje (het blauwe vierkant)

image Vergeet niet: in 3D is een positieve Y waarde een richting naar boven, in 2D is dat naar beneden! Dus de linkeronderhoek van onze kubus heeft als coordinaten (0,0,0) maar van onze material plaatje is dat (0,1)! Ik heb al gewaarschuwd dat we hier nog last mee gaan krijgen, dit is waar dat gebeurdt!

We moeten voor ieder punt in onze kubus aangeven hoe de mapping met onze toekomstige material is. Dit is een property van de MeshGeometry3D, en heet TextureCoordinates.

We moeten dus aan WPF zeggen dat op punt (0,0,0) van onze kubus het punt (0,1) van de brush geplaatst moet worden, op punt (1,0,0) moet brush coordinaat (1,1) komen, enzovoorts. (ik ga er even vanuit dat je wilt dat het plaatje rechtop staat en niet op zijn kop). We krijgen dus nu de volgende definitie van onze MeshGeometry3D

<MeshGeometry3D Positions="0 0 0, 1 0 0, 1 1 0, 0 1 0"
                TriangleIndices="0 1 2 0 2 3"
                TextureCoordinates="0 1, 1 1, 1 0, 0 0"
                />

We gebruiken relatieve coordinaten, we normaliseren de coordinaten van onze texture naar waardes tussen 0 en 1. Als je dus alleen de bovenste helft wilt laten zien, dan gebruik je als TextureCoordinates 0 0.5, 1 0.5, 1 0, 0 0.

Nu krijgen we dit plaatje:

image

Als je nu bij het voorbeeld van de LinearGradientBrush ook de TextureCoordinates plaatst, zul je zien dat het daar ook werkt.

Volgende keer meer over de DrawingBrush en de VisualBrush. Die laatste is echt leuk, hiermee kunnen we bijvoorbeeld een video afspelen op onze kubus!

Published Wednesday, September 03, 2008 11:17 AM door dvroegop
Filed Under: ,

Comments

 

JorisBos said:

Erg helder verhaal Dennis. Echter wel erg op de XAML :)
Zelf ben ik momenteel ook druk aan de slag met 3D en kom toch wel tot de ontdekking dat tools als Blender in samenwerking met een XAML exporter, of ZAM3D, onmisbare tools zijn bij WPF 3D. Ben je al eens mee aan de slag geweest? Hier kun je met directe visuele feedback je creaties bekijken en bekleden met materialen en textures (tevens texture mapping) en als XAML resources exporteren. Direct te gebruiken in een 3DViewport in WPF dus :) Daarnaast is de output in XAML nog erg netjes ook.

zie : http://www.erain.com/products/zam3d/screenshots/
en
http://www.codeplex.com/xamlexporter

Geweldig om te zien dat jullie WPF toepassen voor business applicaties, wellicht binnenkort een showcase?
September 3, 2008 7:04 PM
 

dvroegop said:

Dank je voor het compliment. Inderdaad: het is erg op de XAML gericht. En dat is ook precies de bedoeling.
Ik ken de tools waar je het over hebt, en ik heb er ook mee gewerkt. Nu hebben wij iemand bij ons die helemaal thuis is in 3D Max en ook daar zijn exporters voor. Onze graficus maakt mooie dingen en die worden dan naar XAML geexporteerd: die komen dan bij ons in de applicatie terecht.
Maar voordat je met dergelijke tools aan de gang gaat, lijkt het me handig dat je weet wat er nu precies in WPF gebeurt. En daar gaat deze reeks over.
Daarnaast zijn de geexporteerde XAML bestanden of snippets niet echt herbruikbaar als je alles vanuit C# wilt regelen: dan moet je toch echt wat kunstgrepen uithalen (dus wij gebruiken een mengvorm :-) )
En ja: de showcase komt er aan. Als eerste ga ik het presenteren op de SDC (http://www.sdc.n) en ook bij dotNed ga ik dat verhaal nog een keer houden. Als laatste was dit ook een van mijn topics op de TechEd in Barcelona vorig jaar.
Maar zolang ik er nog vol enthousiasme over kan vertellen, ga ik daar mee door!
September 4, 2008 10:05 AM
Anonymous comments are disabled

About dvroegop

Programmeert al sinds 1982. Microsoft Surface MVP.
Powered by Community Server, by Telligent Systems